From 7d5e0721df5ab70383678d1e181dafa03fa2072a Mon Sep 17 00:00:00 2001 From: huzhushan Date: Fri, 9 Apr 2021 14:01:17 +0800 Subject: [PATCH] update --- 项目开发手册.md | 845 ------------------------------------------------ 1 file changed, 845 deletions(-) delete mode 100644 项目开发手册.md diff --git a/项目开发手册.md b/项目开发手册.md deleted file mode 100644 index d2b30f6..0000000 --- a/项目开发手册.md +++ /dev/null @@ -1,845 +0,0 @@ -# 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 Promise.reject(error); - } - // 如果有refresh_token,则请求获取新的 token - try { - const res = await axios({ - method: "PUT", - url: "/api/authorizations", - timeout: 10000, - 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") - return Promise.reject(error); - } - } - - ElMessage.error(error.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预处理器 - - 由于element-plus使用sass开发,所以本项目我们也使用sass,先安装sass - - ```powershell - npm install -D sass - ``` - - 使用sass - - ```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----------打包配置文件 -``` - -## 登录 -