![cover](https://img-blog.csdnimg.cn/aadc9a17bff341d890f41024f1500b63.png)
Vue3项目实战开发
vue3基础理论、vue3的项目从0开始搭建、vue3的项目实战
@TOC## Vue3项目实战开发
一、技术栈说明
技术栈为 Vue3 + Vite4 + TypeScript + Element Plus + Pinia + Vue Router 等当前主流框架。
二、搭建项目
创建项目
Vue3的项目创建我们推荐使用Vite来搭建
打开Vite官网:https://cn.vitejs.dev/guide/#scaffolding-your-first-vite-project
执行以下的命令
npm init vite@latest vue3-element-woniu --template vue-ts
运行的效果如下:
接下来我们执行依赖的下载
cd vue3-element-woniu
npm install
下载好依赖后,就可以本地打开项目启动看效果了。
启动项目
在文件路径中选中所有路径,并输入cmd,这样就可以启动命令行窗口
当你执行npm run dev
的时候,项目就会加载并启动。
访问路径为:http://localhost:5174
打开vscode工具
使用vscode打开我们的项目代码,查看代码结构以及启动项目
src目录下面就是我们的资源文件,tsconfig.json
文件报错。
问题的原因是moduleResolution
的配置值为bundler
我们可以尝试着将值改为``moduleResolution:“none”,并删除掉
allowImportingTsExtensions`配置
“moduleresolution”: “bundler”
bundler是 TypeScript 中的一个配置选项,它表示模块解析策略使用的是打包工具。在这种情况下,TypeScript 会查找已经打包的模块,而不是直接查找源代码文件。这通常用于在打包后的代码中使用模块别名或路径映射。
“moduleresolution”: “node”
node是 TypeScript 中的一个配置选项,它表示模块解析策略使用 Node.js 的模块解析算法。在这种情况下,TypeScript 会按照 Node.js 的模块查找规则来查找模块,并且允许使用 Node.js 的内置模块、第三方模块以及项目中的自定义模块。这是 TypeScript 默认的模块解析策略。
如果你创建项目后,默认得到的配置是Node,那就无需修改,直接正常运行就可以了。
这个时候就可以正常在vscode中启动项目,并访问项目
三、TypeScript配置
tsconfig.json配置文件有很多配置项,所有配置项我们都可以参考TypeScript官网
地址为:https://www.tslang.cn/docs/handbook/compiler-options.html
在tsconfig.json文件中,有很多配置,每个配置含义如下:
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "node",
// "allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}
compilerOptions:代表编译选项,因为我们项目vue3+ts来进行开发,每个ts的项目都会有一个tsconfig.json配置文件,里面所有的配置项都是ts在开发过程中编译的选项。
target
:代表TS代码编译后的JS版本。你可以填写“ES2015、ES2016”等等
useDefineForClassFields
:它表示在编译期间是否将类字段声明为 JavaScript 的 defineProperty
调用。如果该选项设置为 true,则会在编译后的 JavaScript 代码中使用 Object.defineProperty
来定义类字段,从而使其成为类的属性访问器(accessor),这样可以提供更好的封装性和类型安全性。如果该选项设置为 false 或未设置,则会将类字段直接声明为 JavaScript 对象的属性,这种方式虽然简单但不具备类似于 getter/setter
的优势,也不利于进行类型检查。默认值为 false
module
:它表示使用的模块系统,即用于组织和加载代码的方式。模块系统的选择会影响到编译后的 JavaScript 代码的输出格式。
“module” 的可选值包括 “CommonJS”、“AMD”、“System”、“ES2015” 和 “ESNext”,默认为 “CommonJS”。同时,也可以将其设置为 “none”,这意味着不生成模块化的 JavaScript 代码,而是将所有模块合并到一个文件中输出(不推荐)。
ESNext是ECMAScript Next的缩写,它是ECMAScript标准的下一个版本的开发版本。换句话说,它是JavaScript语言的未来版本的提议和实验性功能的集合
lib
:配置项指定了要包括的目标平台API库,TypeScript编译器可以检查您的代码是否使用了特定的API,并防止意外使用不兼容的API。例如,如果您将"lib"设置为"ES2015",则编译器将仅允许使用ECMAScript 2015规范中定义的API,而禁止使用ES6之前的API
skipLibCheck
:选项用于控制 TypeScript 编译器是否检查引用的库文件的类型定义文件(.d.ts 文件)。如果设置为 true,则编译器将跳过对这些库的类型定义文件的检查,从而可以加快编译速度。但是,这也可能导致在使用库时出现类型错误或其他问题。默认情况下,该选项为 false,即会检查库文件的类型定义文件。
resolveJsonModule
:用于指示编译器是否应该解析 JSON 文件并将其作为模块来处理。如果设置为 true,则编译器将对以 .json 扩展名结尾的文件执行模块解析,并使它们可用于 import 语句中。
isolatedModules
:用于检测模块之间的循环依赖并防止生成不适合执行的 JavaScript 代码。启用 “isolatedModules” 选项后,TypeScript 编译器会将每个文件视为独立的模块,并在编译时禁止跨文件共享变量、函数等实体。
noEmit
:用于控制编译器是否生成 JavaScript 代码文件。当 “noEmit” 设置为 true 时,TypeScript 编译器将不会生成任何 JavaScript 代码文件,只进行类型检查和语法分析。这在某些场景下可能很有用,比如只需要对 TypeScript 代码进行语法检查或者作为编辑器插件工作时
jsx
:通过 “jsx” 字段来配置如何处理 JSX 语法。其选项包括:
- “preserve”:将 JSX 保留为原始字符串并输出。
- “react-native”:生成适用于 React Native 应用的代码。
- “react”:生成适用于 React 应用的代码。
- “none”:禁用 JSX 支持。
strict
:代表是否编译的代码采用严格模式
noUnusedLocals
:如果将 “nouncusedlocals” 设置为 true,则编译器会在编译时检测到函数中未使用的本地变量,并发出错误或警告。如果设置为 false,则编译器不会检查未使用的本地变量
noUnusedParameters
:当将该选项设置为 true 时,TypeScript 编译器会在编译时检查每个函数或方法的参数列表中是否存在未使用的参数,并在发现未使用的参数时发出错误或警告。该选项可以在 tsconfig.json 文件中进行配置
noFallthroughCasesInSwitch
:当将该选项设置为 true 时,TypeScript 编译器会在编译时检查每个 switch 语句中的 case 块是否存在 fall-through(落入)的情况。如果存在 fall-through 的情况,TypeScript 编译器会发出一个错误以提示开发者
四、路径别名配置
(1)在我们项目中引入自定义组件或者JS\TS文件,我们会采用相对路径来寻找,比如:
import Book from "../components/Book.vue"
import utils from "../../utils/demo.js"
相对路径有时候写起来很麻烦,我们可以在配置文件中设置路径别名
相对路径别名配置,使用 @ 代替 src
这个配置在Vue\cli脚手架中默认可以使用,但是在Vite搭建的Vue3项目中我们需要配置一下
(2)在Vite.config.js文件中,配置如下:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from "path";
const pathSrc = path.resolve(__dirname, "src");
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
})
当你在项目中引入path模块的时候,TS类型检测会编译报错。
原因:path模块始于Nodejs中模块,是第三方模块,在TS项目中找不到类型声明的。
(3)解决方案:官网已经提供了类型声明文件,无需我们自己写d.ts文件,下载依赖就可以了。
npm i @types/node --save-dev
下载node包对应的types声明文件,当你下载完毕后,报错信息就消失了。
完整的配置如下:
import vue from '@vitejs/plugin-vue'
import { UserConfig, ConfigEnv, loadEnv, defineConfig } from "vite";
import path from "path";
const pathSrc = path.resolve(__dirname, "src");
// https://vitejs.dev/config/
export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
return {
resolve: {
alias: {
"@": pathSrc,
},
},
plugins: [
vue()
]
}
})
(4)打开tsconfig.js文件
"compilerOptions": {
"baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录
"paths": { // 路径映射,相对于baseUrl
"@/*": ["src/*"]
}
}
在这个文件中配置了paths属性后,我们在页面中使用@/
来引入文件才不会报错。
<script setup lang="ts">
import HelloWorld from '@/components/HelloWorld.vue'
</script>
五、安装自动导入
基本概念
为了避免在多个页面重复引入 API
或 组件
,由此而产生的自动导入插件来节省重复代码和提高开发效率。
两种自动导入对应的不同的vite插件来实现。
名字 | 含义 | 详情 |
---|---|---|
unplugin-auto-import | 按需自动导入API | ref,reactive,watch,computed 等API |
unplugin-vue-components | 按需自动导入组件 | Element Plus 等三方库和指定目录下的自定义组件 |
unplugin-auto-import 插件自动导入带来的好处:
没有自动导入
<script setup lang="ts">
import {ref} from "vue"
const value = ref("xiaowang")
</script>
自动导入插件
<script setup lang="ts">
const value = ref("xiaowang")
</script>
unplugin-vue-components插件自动导入的好处:
没有自动导入
<script setup lang="ts">
import User from "@/components/User.vue"
</script>
<template>
<User></User>
</template>
自动导入插件
<script setup lang="ts">
</script>
<template>
<User></User>
</template>
如果配置这些插件,那以后我们项目开发过程中会更加的方便。效率更高
开发步骤
(1)下载依赖
npm install -D unplugin-auto-import unplugin-vue-components
(2)在src目录下面创建types文件夹
![image-20230510121529181](https://i-blog.csdnimg.cn/blog_migrate/ae4c58c601ee87d48ed77680581f6b2f.png)
这个types文件夹里面后续要存放我们自己的类型声明文件。以及eslint相关约束的类型文件
(3)打开vite.config.ts文件
import vue from '@vitejs/plugin-vue'
import { UserConfig, ConfigEnv, loadEnv, defineConfig } from "vite";
//配置路径别名
import path from "path";
const pathSrc = path.resolve(__dirname, "src");
//自动导入插件
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
// https://vitejs.dev/config/
export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
return {
resolve: {
alias: {
"@": pathSrc,
},
},
plugins: [
vue(),
AutoImport({
// 自动导入 Vue 相关函数,如:ref, reactive, toRef 等
imports: ["vue"],
eslintrc: {
enabled: true, // 是否自动生成 eslint 规则,建议生成之后设置 false
filepath: "./.eslintrc-auto-import.json", // 指定自动导入函数 eslint 规则的文件
},
dts: path.resolve(pathSrc, "types", "auto-imports.d.ts"), // 指定自动导入函数TS类型声明文件路径
}),
Components({
dts: path.resolve(pathSrc, "types", "components.d.ts"), // 指定自动导入组件TS类型声明文件路径
}),
]
}
})
(4) 执行项目启动
npm run dev
当执行完这个命令后,我们的项目中会动态生成一下的几个文件
![image-20230511110554351](https://i-blog.csdnimg.cn/blog_migrate/da5009ce4b6232069115c46521264382.png)
这些文件中就已经包含了自动导入API和自动导入组件的代码。
(5)测试效果
你可以创建一个新的组件,在components文件夹下面。
<template>
<div>
<h2>work</h2>
<p>{{data}}</p>
//无需引入组件,默认会自动从components文件夹下面导入
<Work></Work>
</div>
</template>
<script lang='ts' setup>
//无需引入ref,就可以引入
const data = ref(0)
</script>
<style lang='less' scoped>
</style>
原理是:只要每次启动项目,就会默认将components文件夹下面的内容全局导入加载
在types/components.d.ts中默认已经导入组件并注册了。
import '@vue/runtime-core'
export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
HelloWorld: typeof import('./../components/HelloWorld.vue')['default']
Work: typeof import('./../components/Work.vue')['default']
}
}
六、scss预编译处理器
我们在vue中我们可以引入scss来配合我们css样式开发设计。
当然你还可以将很多公共的样式,提取为变量和函数,放在指定文件中。达到以后一键切换效果
(1)下载依赖
npm i -D sass
(2)在src目录下面创建styles文件夹
/src/styles/variables.scss
//定义全局变量
$bg-color:#242424;
(3)vite.config.ts文件
css: {
// CSS 预处理器
preprocessorOptions: {
//define global scss variable
scss: {
javascriptEnabled: true,
additionalData: `@use "@/styles/variables.scss" as *;`
}
}
}
(4)在页面中使用scss编写样式,并引入全局变量
<template>
<div class="box">
<h2>work</h2>
<p class="msg">{{data}}</p>
</div>
</template>
<script lang='ts' setup>
const data = ref(0)
</script>
<style lang='scss' scoped>
.box{
.msg{
background-color: $bg-color;
}
}
</style>
七、less整合
less和scss的用法几乎一致,本文档就没有单独介绍。
八、配置环境变量
在真实的项目开发过程中,我们一般有多种环境
- 开发环境
- 测试环境
- 生产环境
每一种环境对应的服务器地址或者端口都可能发生变化。那代码中可以配置环境变量来实现不同环境下的切换。
vite官网也提供了环境变量的配置信息:https://cn.vitejs.dev/guide/env-and-mode.html
在项目的根目录下面创建指定的文件
![image-20230512103520626](https://i-blog.csdnimg.cn/blog_migrate/fff7a3c0273dac9c3cb50eafd869c484.png)
`.env.developmenet`代表开发环境的配置,内容如下:
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
VITE_APP_TITLE = 'vue3-element-woniu'
VITE_APP_PORT = 5555
VITE_APP_BASE_API = '/dev-api'
.env.production
代表生产环境配置,内容如下:
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
VITE_APP_TITLE = 'vue3-element-woniu'
VITE_APP_PORT = 5555
VITE_APP_BASE_API = '/pro-api'
默认情况下,Vite 在 vite/client.d.ts
中为 import.meta.env
提供了类型定义。随着在 .env[mode]
文件中自定义了越来越多的环境变量,你可能想要在代码中获取这些以 VITE_
为前缀的用户自定义环境变量的 TypeScript 智能提示。
要想做到这一点,你可以在 src
目录下创建一个 env.d.ts
文件,接着按下面这样增加 ImportMetaEnv
的定义:
interface ImportMetaEnv {
/**
* 应用标题
*/
VITE_APP_TITLE: string;
/**
* 应用端口
*/
VITE_APP_PORT: number;
/**
* API基础路径(反向代理)
*/
VITE_APP_BASE_API: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
以后在其他地方需要使用env文件中的配置信息。会产生提示
九、配置代理服务器
浏览器默认情况下会出现跨域报错的问题。这是因为浏览器同源策略的影响。
本地开发环境通过 Vite
配置反向代理解决浏览器跨域问题,生产环境则是通过 nginx
配置反向代理 。
也就说本地你配置Proxy代理服务只能在本地运行,一旦项目打包后,本地代理无效了,需要在nginx配置反向代理来解决这个问题。
在vite.config.js配置中
import { defineConfig, ConfigEnv, UserConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
//获取到env,env获取定义变量
const env = loadEnv(mode,process.cwd())
return {
plugins: [
....
],
/**
* 后端接口地址为:http://web.woniulab.com/users/findUser
* 前端访问地址:http://web.woniulab.com/dev-apis/users/findUser
*/
server:{
port:Number(env.VITE_APP_PORT),
open:true,
proxy:{
[env.VITE_APP_BASE_API]:{
target:env.VITE_APP_SERVER_PATH,
changeOrigin:true,
rewrite:path=>path.replace(new RegExp("^"+env.VITE_APP_BASE_API),"")
}
}
}
}
})
配置了代理服务器过后,请求地址发生的变化:
浏览器访问地址:http://127.0.0.1:5555/dev-api/users
代理服务器访问地址:http://vapi.youlai.tech/users
只要检测到访问地址里面有dev-api,那默认就会进入代理服务器。转发给后端服务器。
代码中baseURL
console.log(import.meta.env)
十、下载ElementUI Plus
开发这个项目我们需要用到ElementUI Plus这个UI组件库。当然你也可以选中用Ant Design来完成项目开发。
ElementUI Plus官方地址:https://element-plus.gitee.io/zh-CN/guide/design.html
我们主要关注的按需导入,官网的截图如下:
可以全局导入、也可以按需导入。全局导入相对来说比较简单。按需导入要麻烦一些。
开发步骤
(1)下载依赖
npm install element-plus
(2)vite.config.js配置对应的代码
// vite.config.ts
import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
// ...
plugins: [
// ...
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
})
(3)再项目中引入组件
<template>
<div>
<h2>登录页面</h2>
<el-button>我是 ElButton</el-button>
</div>
</template>
<script lang='ts' setup>
</script>
<style lang='less' scoped>
</style>
无需在引入任何样式,就可以正常使用el-button组件了。
所有样式、组件都已经默认导入了。参考第五步:安装自动导入文档。
十一、路由环境搭建
路由的概念
截至到目前编写文档,VueRouter最新的版本@4.0.x版本
路由的开发
下载路由
npm i vue-router@next
创建路由文件
在 src
中创建 router/index.ts
文件
import { createRouter, createWebHistory,RouteRecordRaw } from 'vue-router';
import Login from "../views/Login.vue"
import Home from "../views/Home.vue"
const routes: Array<RouteRecordRaw> = [
{
path: '/login',
component: Login
},
{
path: '/home',
component: Home,
beforeEnter: (to, from, next) => {
console.log(123);
}
}
]
const router = createRouter({
routes,
// 路由模式
history: createWebHistory()
})
你可以查看源码声明文件,里面包含了对路由属性的定义
当然如果你要自己定义一个路由属性,基础路由里面增加开发者自定义属性 router.ts的RouteRecordRaw类型校验.我们可以在router目录中自己写一个声明文件.vue-router.d.ts
export default router;
// interface IRouter {
// path: string,
// component?: Component,
// name?: string, // 命名路由
// components?: { [name: string]: Component }, // 命名视图组件
// redirect?: string | Location | Function,
// props?: boolean | Object | Function,
// alias?: string | Array<string>,
// children?: Array<IRouter>, // 嵌套路由
// meta?: any
// }
import { createRouter, createWebHistory,RouteRecordRaw } from 'vue-router';
import { _RouteRecordBase } from 'vue-router';
declare module 'vue-router'{
interface _RouteRecordBase{
hidden?: boolean | string | number
}
}
{
path: '/home',
component: Home,
hidden:false,
beforeEnter: (to, from, next) => {
console.log(123);
}
}
在main.ts文件中引入route
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App);
app.use(router);
app.mount('#app');
组件中获取路由对象
<script setup>
// 引入
import { useRouter, useRoute } from 'vue-router'
// 等同于 this.$router
const router = useRouter();
// 等同于 this.$route
const route = useRoute();
</script>
自己声明了这个文件后,我们可以在路由中使用
十二、登录页面配置
登录页面的基本代码
<template>
<div class="login-container">
<el-form
ref="loginFormRef"
:model="loginData"
:rules="loginRules"
class="login-form"
>
<div class="flex text-white items-center py-4">
<span class="text-2xl flex-1 text-center">综合管理系统</span>
<lang-select style="color: #fff" />
</div>
<el-form-item prop="username">
<div class="p-2 text-white">
<svg-icon icon-class="user" />
</div>
<el-input
class="flex-1"
ref="username"
size="large"
v-model="loginData.username"
:placeholder="用户名"
name="username"
/>
</el-form-item>
<el-tooltip
:disabled="isCapslock === false"
content="Caps lock is On"
placement="right"
>
<el-form-item prop="password">
<span class="p-2 text-white">
<svg-icon icon-class="password" />
</span>
<el-input
class="flex-1"
v-model="loginData.password"
:placeholder="密码"
:type="passwordVisible === false ? 'password' : 'input'"
size="large"
name="password"
@keyup="checkCapslock"
@keyup.enter="handleLogin"
/>
<span class="mr-2" @click="passwordVisible = !passwordVisible">
<svg-icon
:icon-class="passwordVisible === false ? 'eye' : 'eye-open'"
class="text-white cursor-pointer"
/>
</span>
</el-form-item>
</el-tooltip>
<!-- 验证码 -->
<el-form-item prop="verifyCode">
<span class="p-2 text-white">
<svg-icon icon-class="verify_code" />
</span>
<el-input
v-model="loginData.verifyCode"
auto-complete="off"
:placeholder="验证码"
class="w-[60%]"
@keyup.enter="handleLogin"
/>
<div class="captcha">
<img :src="captchaBase64" @click="getCaptcha" />
</div>
</el-form-item>
<el-button
size="default"
:loading="loading"
type="primary"
class="w-full"
@click.prevent="handleLogin"
>登录
</el-button>
<!-- 账号密码提示 -->
<div class="mt-4 text-white text-sm">
<span>登录账号: admin</span>
<span class="ml-4"> 登录密码: 123456</span>
</div>
</el-form>
</div>
</template>
<script setup lang="ts">
// API依赖
import { useRoute } from "vue-router";
import {LoginData} from "@/interface/LoginIF"
const route = useRoute();
/**
* 按钮loading
*/
const loading = ref(false);
/**
* 是否大写锁定
*/
const isCapslock = ref(false);
/**
* 密码是否可见
*/
const passwordVisible = ref(false);
/**
* 验证码图片Base64字符串
*/
const captchaBase64 = ref();
/**
* 登录表单引用
*/
const loginFormRef = ref(ElForm);
const loginData = ref<LoginData>({
username: "admin",
password: "123456",
});
const loginRules = {
username: [{ required: true, trigger: "blur" }],
password: [{ required: true, trigger: "blur", validator: passwordValidator }],
verifyCode: [{ required: true, trigger: "blur" }],
};
/**
* 密码校验器
*/
function passwordValidator(rule: any, value: any, callback: any) {
if (value.length < 6) {
callback(new Error("The password can not be less than 6 digits"));
} else {
callback();
}
}
/**
* 检查输入大小写状态
*/
function checkCapslock(e: any) {
const { key } = e;
isCapslock.value = key && key.length === 1 && key >= "A" && key <= "Z";
}
/**
* 获取验证码
*/
function getCaptcha() {
// getCaptchaApi().then(({ data }) => {
// const { verifyCodeBase64, verifyCodeKey } = data;
// loginData.value.verifyCodeKey = verifyCodeKey;
// captchaBase64.value = verifyCodeBase64;
// });
}
/**
* 登录
*/
function handleLogin() {
loginFormRef.value.validate((valid: boolean) => {
if (valid) {
loading.value = true;
console.log(loginData.value);
}
});
}
onMounted(() => {
getCaptcha();
});
</script>
<style lang="scss" scoped>
.login-container {
width: 100%;
min-height: 100vh;
overflow: hidden;
background-color: #2d3a4b;
.login-form {
width: 520px;
max-width: 100%;
padding: 160px 35px 0;
margin: 0 auto;
overflow: hidden;
.captcha {
position: absolute;
top: 0;
right: 0;
img {
width: 120px;
height: 48px;
cursor: pointer;
}
}
}
}
.el-form-item {
background: rgb(0 0 0 / 10%);
border: 1px solid rgb(255 255 255 / 10%);
border-radius: 5px;
}
.el-input {
background: transparent;
// 子组件 scoped 无效,使用 :deep
:deep(.el-input__wrapper) {
padding: 0;
background: transparent;
box-shadow: none;
.el-input__inner {
color: #fff;
background: transparent;
border: 0;
border-radius: 0;
caret-color: #fff;
&:-webkit-autofill {
box-shadow: 0 0 0 1000px transparent inset !important;
-webkit-text-fill-color: #fff !important;
}
// 设置输入框自动填充的延迟属性
&:-webkit-autofill,
&:-webkit-autofill:hover,
&:-webkit-autofill:focus,
&:-webkit-autofill:active {
transition: color 99999s ease-out, background-color 99999s ease-out;
transition-delay: 99999s;
}
}
}
}
</style>
十三、i18n国际化配置
基本概念
i18n 英文全拼 internationalization ,国际化的意思,英文 i 和 n 中间18个英文字母
i18n是“国际化”(internationalization)的缩写,指的是将应用程序或网站设计为能够轻松地在不同文化和语言环境下使用。它包括将文本、日期、时间、货币等本地化,并确保布局和界面适合各种语言和文化的习惯。 i18n可以提供更广泛的用户体验,使全球用户能够更容易地访问和使用应用程序或网站。
我们在项目中配置两种语言的切换。中文、英文
开发步骤
(1)下载依赖
npm install vue-i18n@9
(2)自定义语言包
创建src/lang
/package 文件夹,设计两个文件。
中文语言包 zh-cn.ts
export default {
login:{
title:"综合管理系统",
username:"用户名",
password:"密码",
login:"登录",
verifycode:"验证码"
}
}
中文语言包 en.ts
export default {
login:{
title:"ManagerSystem",
username:"Username",
password:"Password",
login:"Login",
verifycode:"Verify code"
}
}
(3) 创建i18n实例
创建src/lang/index.ts
文件
// src/lang/index.ts
import { createI18n } from 'vue-i18n';
// 本地语言包
import enLocale from './package/en';
import zhCnLocale from './package/zh-cn';
const messages = {
'zh-cn': {
...zhCnLocale
},
en: {
...enLocale
}
};
// 创建 i18n 实例
const i18n = createI18n({
legacy: false,
locale: "zh-cn",
messages: messages
});
// 导出 i18n 实例
export default i18n;
当locale的值为zh-cn
的时候,表示我们采用中文环境来渲染。
如果你把locale值更改为en
的时候,系统自动切换为英文语言包
效果如下:
十一、axios请求封装
axios概念
axios是基于ajax+promise封装的第三方优秀的请求库,借助于这个库可以方面进行前端异步请求编程。
npm i axios
axios使用
(1)封装axios工具
在src目录下面创建utils\axiosUtils.ts文件
import axios from "axios"
const newAxios = axios.create({
baseURL:"http://127.0.0.1:8002",
timeout:5000,
})
export default newAxios
创建一个axios的封装工具,可以设置基础的路径,后续不用在进行拼接
(2)在src目录下面创建apis文件夹
apis文件夹用于管理所有的网络请求,比如当我要获取用户的信息的时候。我们会创建一个userApi.ts
import axios from "@/utils/axiosUtils"
//将数据类型约束放在外部的types文件夹中
import {IUser} from "@/types/userInterface"
//get请求获取所有的数据
export const getAllUser = ()=>{
return axios.get("/users/findAll")
}
//get请求根据id获取数据
export const getUserById = (data:IUser)=>{
return axios.get("/users/findAll",{
params:data
})
}
//get请求根据id获取数据
export const addUser = (data:IUser)=>{
return axios.post("/users/add",data)
}
其中userInterface文件中的代码如下:
export interface IUser {
id:number,
name:string,
password:string
}
(3)组件中调用
在User.vue组件中使用请求
import {getAllUser} from "@/apis/userApi"
import {IUser} from "@/types/userInterface"
const users = ref<Array<IUser>>([])
onMounted(()=>{
fetchData()
})
const fetchData =async ()=>{
const res = await getAllUser()
users.value = res.data.data
}
//表格可以渲染users的数据
(1)ref定义数据
基础语法为:
十二、组合式api
(1)ref定义数据
基础语法为:
<script lang="ts" setup>
import { ref } from 'vue';
const count = ref<number>(0)
const changeProduct = () => {
count.value = "xiaowang"; //报错。无法将string类型分配给number
}
</script>
复杂的数据结构我们就使用interface来约束
<template>
<h2>主页</h2>
<p>{{ products }}</p>
<button @click="changeProduct">修改</button>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
interface IProduct {
_id: string,
name: string,
title: string,
price: number,
type: IType,
imgSrc: string,
msg: string,
delstate: number,
state: number
}
interface IType {
_id: string,
name: string,
parentId: string,
type: string,
updateDate: string | null
}
const products = ref<Array<IProduct>>([{
_id: "5fbf756526420000dc005032",
name: "IPhone12",
title: "Iphone12搭载最新的处理器,双面玻璃材质",
price: 5999,
type: {
_id: "5fbcd068ea6c0000ff007f41",
name: "手机",
parentId: "5fbccd4aea6c0000ff007f33",
type: "二级分类",
updateDate: null
},
imgSrc: "img/1.jpg",
msg: "iPhone 12采用后置双摄像头,有紫色、白色、绿色、黄色、黑色、红色六种颜色。宽:75.7毫米,高:150.9毫米,厚:8.",
delstate: 1,
state: 1
}])
const changeProduct = ()=>{
}
</script>
<style>
</style>
(2)reactive来定义数据
我们可以在reactive中定义很多种类型的数据,所以需要在泛型里面提供一个对象,分别对reactive里面定义各个属性进行数据类型约束。
<script lang="ts" setup>
import { reactive } from 'vue';
interface IStudent {
id:number,name:string
}
interface IUser {
id:number
}
const state = reactive<{username:string,students:Array<IStudent>,user:IUser}>({
username:"xiaowang",
students:[
{id:1,name:"xiaofei"}
],
user:{id:1}
})
</script>
在开发过程中一般为了方便代码维护,我们会将约束提取到独立的文件中,需要使用的时候,引入进来使用.我们在src目录下面创建一个types的文件夹,里面写一个home.ts文件
export interface IProduct {
_id: string,
name: string,
title: string,
price: number,
type: IType,
imgSrc: string,
msg: string,
delstate: number,
state: number
}
export interface IType {
_id: string,
name: string,
parentId: string,
type: string,
updateDate: string | null
}
在页面中引入对应的 约束信息
import {IProduct,IType} from "../types/home"
(3)计算属性
依赖于原来的数据,得到一个新的结果。所以我们需要引入computed进行计算属性
import {computed} from "vue"
const title = ref<string>("蜗牛学苑")
const newTitle = computed(()=>{
return title.value + "2.0"
})
const changeTitle = ()=>{
title.value = " 蜗牛"
}
默认也有缓存,如果调用多次计算属性,默认只会执行一次,以后结果从缓存中获取
(4)watch监听
watch也是选项式api
import {watch} from "vue"
const product = ref([{price:20,count:1}])
watch(()=>{
return product
},(newVal,oldVal)=>{
console.log("watch");
},{
deep:true,
immediate:true
})
watch需要接受三个参数,
参数一:回调函数,要求返回的值就是侦听的变量
参数二:侦听变化执行业务,这个函数可以接受两个值,一个新值,一个原来的值
参数三:额外的扩展功能,提供一个对象,立即侦听和深度侦听。默认不写不能执行这个操作
(5)watchEffect
提供一种新的侦听方式。
在函数中用到的变量,如果发生变化,执行watchEffect侦听
watchEffect(()=>{
console.log("watcheffect",product.value[0].count);
console.log("mycount",mycount.value);
})
当我检测到product.value内容发生变化,执行一次
当检测到mycount的值发生变化函数也会执行
(6)生命周期函数
采用组合式api来设计的。
生命周期:
第一阶段:挂载阶段
第二阶段:更新阶段
第三阶段:销毁阶段
函数 | 作用 |
---|---|
onBeforeMount | 被挂载之前被调用 |
onMounted | 组件挂载完成后,可以DOM节点 |
onBeforeUpdate | 组件数据更新之前 |
onUpdated | 组件数据更新完成 |
onBeforeUnmount() | 组件被销毁之前 |
onUnmounted() | 组件销毁完成 |
onActivated() | 组件被keepalive包裹了,才会存在 |
onDeactivated() | 组件被keepalive包裹了,才会存在 |
(7)组件传递值
父组件定义数据传递给子组件
<template>
<HeaderVue :count="count" @changeCount="changeCount"></HeaderVue>
</template>
<script setup lang="ts">
import { ref } from "vue"
import HeaderVue from '../components/Header.vue';
const count = ref<number>(10)
const changeCount = (val:number)=>{
count.value = val
}
</script>
分别定义了count数据和changeCount这个变量
子组件获取数据,并修改父组件数据
<template>
<p>header:{{count}}</p>
<button @click="emit('changeCount',20)">修改count</button>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const emit = defineEmits<{
(e:"changeCount",val:number):void
}>()
defineProps<{ count: number }>()
</script>
defineProps:获取外部数据,需要在泛型里面声明我们过来的数据类型
defineEmits:定义了我们子组件要调用的函数名字,以及传递的参数
要获取props的值来计算和使用
const props = defineProps<{count:number,user:IUser}>()
十三、pinia状态机
Pinia这个状态机目前默认在Vue3中使用。
这个状态机内部的设计思想和Vuex很多类似的,但是更偏向于组合式api
内部现在变成三大模块:state、getters、actions
(1)下载依赖
npm i pinia
(2)加载pinia状态机.main.ts文件中
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from "@/router"
import {createPinia} from "pinia"
const app = createApp(App)
app.use(router)
app.use(createPinia())
app.mount('#app')
(3)创建文件模块
在src目录下面创建store\useStore.ts
import { defineStore } from "pinia"
export const userStore = defineStore("userStore", {
state: () => {
return {
count: 10
}
},
getters: {
doubleCount(state) {
return state.count * 2
},
},
actions: {
//可以存放普通函数,也可以存放异步函数
increment(count: number) {
// context.commit("xxxx",state)
this.count += count
},
decrement() {
this.count -= 10
},
asyncIncrement() {
setTimeout(() => {
this.count += 10
}, 1000);
}
}
})
(4)页面中使用状态机
获取状态机
import {userStore} from "@/store/userStore"
const userStoreData = userStore()
//userStoreData = {userStore:{}}
console.log(userStoreData)
//调用actions中的函数进行修改
userStoreData.increment(100)
对比:目前两种状态机的区别
- Vuex和pinia状态机,都是Vue官方推出的状态机。不管Vue2还是Vue3都可以使用两者。
- Vuex状态机比pinia多出两个模块。motations和models
- 在pinia中actions可以同时处理同步函数和异步函数。
- pinia默认已经支持了TypeScript,Vuex对TS支持并没有那么友好。很多约束都需要手动写
十四、Vuex状态机
-
下载依赖
npm i vuex@next
-
创建对应的文件
在
src
目录下创建一个store/index.js
文件,在该文件中进行 Vuex 的配置import { createStore,Store,StoreOptions } from 'vuex'; import {IRootState} from "../types/root-types" const store = createStore<IRootState>({ state: { username:"xiaowang", users:[ {id:1,name:"王小二"} ] }, getters: {}, mutations: { changeUsername(state,payload){ state.username = payload } }, actions: { asyncChangeUsername(context,payload){ setTimeout(() => { context.commit("changeUsername",payload) }, 1000); } }, modules: {} }) export default store
root-types这个文件中的约束为
export interface IRootState { //根据实际情况里面定义自己需要的类型 username:string, users:Array<{id:number,name:string}> }
一旦定义类约束,state的数据内容就已经被约束了
-
在
main.js
中引入仓库对象:import { createApp } from 'vue' import App from './App.vue' import router from './router' import store from './store'; const app = createApp(App); app.use(router); app.use(store); app.mount('#app');
-
组件中操作仓库
在 Vue3 的 Vuex 中,首先
this.$store
不能使用了,辅助函数也不能使用了。因此,如果我们要获取仓库对象,需要调用
useStore()
方法:<script setup> import { useStore } from 'vuex' // 等同于 this.$store const store = useStore(); </script>
<template> <h3>这是App</h3> <!-- <ReactiveData></ReactiveData> --> <!-- <ReactiveData2></ReactiveData2> --> <!-- <ReactiveData3></ReactiveData3> --> <!-- <ComputedData></ComputedData> --> <!-- <WatchData></WatchData> --> <!-- <LifeFun></LifeFun> --> <Prize></Prize> <p>{{store.state.productModel}}</p> <button @click="changename">修改</button> </template> <script lang='ts' setup> import { useStore } from "vuex"; const store = useStore() const changename = ()=>{ store.commit("productModel/incrementAge") } </script> <style lang='scss' scoped> .box { .h3 { color: red; } } </style>
十五、自定义指令
全局指令
在main.ts文件中引入下面的代码
app.directive('focus', (el) => el.focus())
将指令挂在到app对象身上,任何一个组件都可以使用
app.directive('focus', (el, binding) => {
console.log(binding.value);
el.focus()
})
局部指令
按照官方要求的命名规则来设计,自定义指令就实现了
<input v-focus>
// 在模板中启用 v-focus
const vFocus2 = {
mounted: (el) => el.focus()
}
钩子函数介绍:
const myDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {
// 下面会介绍各个参数的细节
},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode, prevVnode) {}
}
在src目录下面创建directives/index.ts文件夹
// 在模板中启用 v-focus
export const vFocus2 = {
mounted: (el: any, bind: any) => {
console.log(bind.value);
el.focus()
}
}
在组件中引入
import {vFocus2} from "@/directives"
<input v-focus=`"red"`>
![Logo](https://devpress.csdnimg.cn/79de2bf0b7994defa4242ef90d5513fa.jpg)
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)