# Vue3项目开发手册 ## 初始化项目 ### 技术选型 Vue3 + Element-plus + Vite - Vue3 >- 支持 Vue2 的大多数特性 >- 支持 Typescript >- 使用 Proxy 代替 defineProperty 实现数据响应式 >- 重写虚拟 DOM 的实现和 Tree-Shaking >- [Vue3中文官网](https://vue3js.cn/docs/zh/guide/introduction.html) - Element-plus > - 支持Vue3的element-ui组件库 > > > element-ui只支持到vue2,element-plus可以支持vue3 > > - [Element-plus中文官网](https://element-plus.gitee.io/#/zh-CN) - Vite >- 是一种新型前端构建工具,能够显著提升前端开发体验。 > >- [Vite中文官网](https://www.pipipi.net/vite/) ### 创建项目 > 我们将使用vite和vue3手动进行前端项目架构。为方便大家从vue2过渡到vue3,本项目我们先不使用ts。 > 使用vite的命令创建项目: > 需要 Node.js 版本 >= 12.0.0 ```powershell # npm 6.x npm init @vitejs/app erp-vue3 --template vue # npm 7+, 需要额外的双横线: npm init @vitejs/app erp-vue3 -- --template vue # yarn yarn create @vitejs/app erp-vue3 --template vue ``` - npm init @vitejs/app 是固定的写法 - erp-vue3 是项目名称 - --template vue 是指定模板预设。指定vue就是使用vue3并且不使用ts。 > 支持的模板预设如下,有兴趣可以挨个试一下 > > - `vanilla` > - `vue` > - `vue-ts` > - `react` > - `react-ts` > - `preact` > - `preact-ts` > - `lit-element` > - `lit-element-ts` 创建完后,会提示我们进入项目目录,安装依赖,运行项目: ```powershell cd erp-vue3 npm install npm run dev ``` 最后我们来看下项目结构 ```powershell ├─node_modules -----------依赖包 ├─public -----------------静态文件 ├─index.html--------------主页面 ├─src---------------------核心文件目录(我们开发的代码都在src目录) | ├─assets---------------静态资源(图片,字体,css,js等) | ├─components-----------公共组件 | ├─App.vue--------------根组件 | ├─main.js--------------入口文件 ├─package.json------------项目基本信息和依赖包信息 ├─vite.config.js----------配置文件 ``` ## 完善项目架构 > 目前项目中还没有路由、状态管理、UI组件库、ajax库 ### 组件库 Element Plus - 安装element-plus,并且在main.js中引入 ```powershell npm install element-plus ``` 引入element-plus ```js // main.js import { createApp } from 'vue' import App from './App.vue' // 引入element-plus import ElementPlus from "element-plus"; import "element-plus/lib/theme-chalk/index.css"; // 使用use注册ElementPlus createApp(App).use(ElementPlus).mount('#app') ``` - vue3中如何全局调用element-plus中的提示插件 ```js import {getCurrentInstance} from 'vue'; setup () { const { ctx } = getCurrentInstance(); // 可以把ctx当成vue2中的this ctx.$message.success("yes") ctx.$loading() } ``` ### 路由 - 在src目录中创建views目录,用来存放页面 > 我们使用模块化的方式来管理页面,比如用户管理、权限管理等模块,每个模块下可能有多个页面,所以每个模块都要创建一个目录 在views目录中创建home目录(代表home模块),home目录中创建index.vue,index.vue代表home模块的主页面 ```vue ``` 同样的方式创建一个login模块 ```vue ``` - 安装路由vue-router ```powershell # 安装最新版的vue-router npm install vue-router@next ``` - 在src目录中创建router目录,用来存放路由配置文件 > 路由配置就是配置views模块中的页面通过什么地址来访问,路由我们也使用模块化管理,views中有多少模块就创建多少路由配置文件,此外还需要一个index.js来统一分配路由模块 router目录中创建modules目录, modules目录中创建home.js和login.js ```js // home.js const Home = () => import("../../views/home/index.vue"); export default [ { path: "/home", name: "home", component: Home, } ] ``` ```js // login.js const Login = () => import("../../views/login/index.vue"); export default [ { path: "/login", name: "login", component: Login, } ] ``` router目录中创建index.js ```js // index.js import { createRouter, createWebHashHistory } from "vue-router" import home from './modules/home' import login from './modules/login' const router = createRouter({ history: createWebHashHistory(), routes: [ { path: '/', redirect: '/home' }, ...home, ...login ], }); export default router; ``` - 修改App.vue ```html ``` ``` - 挂载路由 > 在main.js中挂载路由 ```js // main.js import { createApp } from 'vue' import App from './App.vue' // 引入element-plus import ElementPlus from "element-plus"; import "element-plus/lib/theme-chalk/index.css"; // 引入路由 import router from './router' // 使用use注册路由 createApp(App).use(ElementPlus).use(router).mount('#app') ``` - 配置alias路径别名 > 文件引入路径比较深的时候,使用相对路径需要写很多`../`,例如上面的`router/modules/home.js`文件 > > 所以我们可以配置一个src目录的别名,需要在vite.config.js中配置 ```js // vite.config.js import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import path from "path"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue()], resolve: { alias: { "@": path.resolve(__dirname, "src"), }, }, }) ``` 然后我们就可以修改路由组件的引用路径了,在router/modules/home.js中 ```js // home.js // const Home = () => import("../../views/home/index.vue"); const Home = () => import("@/views/home/index.vue"); export default [ { path: "/home", name: "home", component: Home, } ] ``` ### 状态管理 - 安装vuex状态管理插件 ```powershell # 安装最新版的vuex npm install vuex@next ``` - 在src目录中创建store目录,用来存放状态模块 > 状态管理也使用模块化的方式 在store目录中创建modules目录,modules中创建一个模块app.js ```js // app.js export default { namespaced: true, state: {}, mutations: {}, actions: {}, }; ``` 在store目录中创建index.js ```js //index.js import { createStore } from "vuex"; import app from "./modules/app"; export default createStore({ modules: { app, }, }); ``` - 挂载store ```js // main.js import { createApp } from 'vue' import App from './App.vue' // 引入element-plus import ElementPlus from "element-plus"; import "element-plus/lib/theme-chalk/index.css"; // 引入路由 import router from './router' // 引入store import store from './store' // 使用use注册路由和store createApp(App).use(ElementPlus).use(router).use(store).mount('#app') ``` ### 接口管理 - 安装axios ```powershell npm install axios ``` - 接口管理 > 项目中我们调用接口的时候,有可能有很多页面都会调用同一个接口,所以我们把所有的接口统一进行模块化管理 在src目录中创建api目录,用来存放所有的接口 在api目录中创建一个模块login.js,该文件包括跟登录注册相关的所有接口 ```js // login.js import axios from 'axios' // 登录接口 export const Login = data => { return axios.request({ url: "/api/login", method: "post", data, }); }; // 其它接口... ``` 在页面中调用登录接口 > 在views/login/index.vue中: ```html ``` ### 跨域管理 当前端调用后端接口的时候,假设后端接口的域名是`http://dev.erp.com`,那`http://localhost`访问`http://dev.erp.com`是会跨域的,这时候我们就要在vite.config.js中设置代理 ```js // vite.config.js增加如下配置 server: { proxy: { "/api": { target: "http://dev.erp.com", changeOrigin: true, }, }, }, ``` ### mock管理 > 当后端接口没有开发完成的时候,前端就需要根据接口文档mock数据进行开发 > > vite中可以使用vite-plugin-mock插件创建mock服务 - 安装vite-plugin-mock ```powershell npm install -D mockjs vite-plugin-mock ``` - 配置mock,需要在vite.config.js中配置 ```js // vite.config.js import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import path from "path"; import { viteMockServe } from "vite-plugin-mock"; // https://vitejs.dev/config/ export default env => { // console.log(env); return defineConfig({ plugins: [ vue(), viteMockServe({ mockPath: "mock", // 指定mock目录中的文件全部是mock接口 localEnabled: env.mode === "mock", // 指定在mock模式下才启动mock服务(可以在package.json的启动命令中指定mode为mock) supportTs: false, // mockPath目录中的文件是否支持ts文件,现在我们不使用ts,所以设为false }), ], resolve: { alias: { "@": path.resolve(__dirname, "src"), }, }, server: { proxy: { "/api": { target: "http://dev.erp.com", changeOrigin: true, }, }, }, }); }; ``` - package.json中配置mode为mock的启动命令 ```js // ... "scripts": { "dev": "vite", "mock": "vite --mode mock", // ... ``` 重新启动项目 ```powershell npm run mock ``` - 在根目录中创建mock目录,用来存放mock接口 在mock目录中创建一个test.js ```js // test.js export default [ { url: "/api/get", method: "get", response: { code: 200, data: { name: "hello world", }, }, }, ] ``` 这个时候我们访问`http://localhost:3000/api/get`,可以看到返回的mock数据了 ### 封装axios > 当我们登录成功之后,后端其实会返回一个token(token就是用来做登录认证的),后续所有请求都需要带上这个token,后端校验token是否有效 > > 这个时候,前端要做的就是把token保存在本地存储中,然后所有的请求都要增加一个自定义请求头,用来传递token > > 为了更友好的对请求进行控制(比如添加全局请求头、统一处理错误),我们对axios进行封装,拦截请求和响应 - 在登录的mock接口中返回token ```js // mock/login.js export default [ { url: "/api/login", method: "post", timeout: 1000, response: { code: 200, message: "登录成功", data: { token: "@word(50, 100)", // @word()是mockjs的语法 refresh_token: "@word(50, 100)", // refresh_token是用来重新生成token的 } }, }, ] ``` - 登录之后,把token保存在localStorage和vuex中 > 保存在vuex中为了更好的进行响应式控制 ```js // 登录 const login = async () => { const { code, data, message } = await Login({ userName: 'admin', password: '123456' }) if (+code === 200) { store.commit("app/setToken", data); } } ``` store/modules/app.js ```js import { getItem, setItem, removeItem } from "@/utils/storage"; //getItem和setItem是封装的操作localStorage的方法 export const TOKEN = "TOKEN"; export default { namespaced: true, state: { authorization: getItem(TOKEN), }, mutations: { setToken(state, data) { state.authorization = data; // 保存到localStorage setItem(TOKEN, data); }, clearToken (state) { state.authorization = ''; // 保存到localStorage removeItem(TOKEN); }, }, actions: {}, }; ``` 在src中创建utils目录,在utils中创建storage.js > utils目录主要是用来存放一些常用工具函数 ```js // storage.js export const getItem = name => { const data = window.localStorage.getItem(name); try { return JSON.parse(data); } catch (err) { return data; } }; export const setItem = (name, value) => { if (typeof value === "object") { value = JSON.stringify(value); } window.localStorage.setItem(name, value); }; export const removeItem = name => { window.localStorage.removeItem(name); }; ``` - 在utils目录中创建request.js ```js // request.js import axios from "axios"; import { ElMessage } from "element-plus"; import store from "@/store"; import router from "@/router"; const service = axios.create({ baseURL: "/", timeout: 10000, withCredentials: true, }); // 拦截请求 service.interceptors.request.use( (config) => { const authorization = store.state.app; if (authorization) { config.headers.Authorization = `Bearer ${authorization.token}`; } return config; }, (error) => { // console.log(error); return Promise.reject(error); } ); // 拦截响应 service.interceptors.response.use( // 响应成功进入第1个函数,该函数的参数是响应对象 (response) => { return response.data; }, // 响应失败进入第2个函数,该函数的参数是错误对象 async (error) => { // 如果响应码是 401 ,则请求获取新的 token // 响应拦截器中的 error 就是那个响应的错误对象 if (error.response && error.response.status === 401) { // 校验是否有 refresh_token const { authorization } = store.state.app; if (!authorization || !authorization.refresh_token) { router.push("/login"); // 代码不要往后执行了 return; } // 如果有refresh_token,则请求获取新的 token try { const res = await axios({ method: "PUT", url: "/api/authorizations", headers: { Authorization: `Bearer ${authorization.refresh_token}`, }, }); // 如果获取成功,则把新的 token 更新到容器中 // console.log('刷新 token 成功', res) store.commit("app/setToken", { token: res.data.data.token, // 最新获取的可用 token refresh_token: authorization.refresh_token, // 还是原来的 refresh_token }); // 把之前失败的用户请求继续发出去 // config 是一个对象,其中包含本次失败请求相关的那些配置信息,例如 url、method 都有 // return 把 request 的请求结果继续返回给发请求的具体位置 return service(error.config); } catch (err) { // 如果获取失败,直接跳转 登录页 // console.log('请求刷线 token 失败', err) router.push("/login"); // 清除token store.commit("app/clearToken") } } ElMessage.error(error.response.message); return Promise.reject(error); } ); export default service; ``` 这时我们再回到api/login.js中,只需要引入上面的request.js就行了 ```js // login.js // import axios from 'axios' import request from '@/utils/request' // 登录接口 export const Login = data => { return request({ url: "/api/login", method: "post", data, }); }; ``` ### 权限控制 > 如果没有登录或者token失效的时候,页面需要重定向到登录页 > > 还有一些例如找回密码的页面应该也可以访问,所以我们还是设置一个白名单,没有token的时候只能访问白名单的页面 - 在src目录创建permission.js ```js // permission.js import router from '@/router' import { TOKEN } from '@/store/modules/app' // TOKEN变量名 // 白名单,里面是路由对象的name const WhiteList = ['login'] // vue-router4的路由守卫不再是通过next放行,而是通过return返回false或者一个路由地址 router.beforeEach((to) => { if (!window.localStorage[TOKEN] && !WhiteList.includes(to.name)) { return { name: 'login', query: { redirect: to.path // redirect是指登录之后可以跳回到redirect指定的页面 }, replace: true } } }) ``` - 在main.js中引入permission ```js // 权限控制 import './permission' ``` ### css - css预处理器 本项目我们使用less,先安装less ```powershell npm install -D less ``` 使用less ```html ``` - autoprefixer自动处理css3浏览器前缀 安装autoprefixer > 不需要安装postcss,vite内部支持 ```powershell npm install autoprefixer -D ``` 根目录创建postcss.config.js ```js // postcss.config.js module.exports = { plugins: { // 兼容浏览器,添加前缀 'autoprefixer': {} } } ``` package.json中配置浏览器版本 ```js //package.json { "browserslist": [ "> 1%", "last 2 versions" ], } ``` ## 项目结构 最终的项目结构如下 > 没有后缀的,代表是一个文件夹 ```powershell ├─node_modules -----------依赖包 ├─mock -------------------mock接口 ├─public -----------------静态文件 ├─index.html--------------主页面 ├─src---------------------核心文件目录(我们开发的代码都在src目录) | ├─api------------------接口模块 | ├─assets---------------静态资源(图片,字体,css,js等) | ├─components-----------公共组件 | ├─router---------------路由配置 | | ├─index.js----------路由主文件 | | ├─modules-----------路由模块 | ├─store----------------状态管理 | | ├─index.js----------状态主文件 | | ├─modules-----------状态模块 | ├─utils----------------工具函数 | | ├─request.js--------axios封装函数 | ├─views----------------页面 | ├─App.vue--------------根组件 | ├─main.js--------------入口文件 ├─package.json------------项目基本信息和依赖包信息 ├─postcss.config.js-------postcss配置文件 ├─vite.config.js----------打包配置文件 ``` ## 登录