ai-vue3-admin/项目开发手册.md
2021-03-26 20:06:32 +08:00

844 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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只支持到vue2element-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---------------静态资源图片字体cssjs等
| ├─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.vueindex.vue代表home模块的主页面
```vue
<template>
home
</template>
```
同样的方式创建一个login模块
```vue
<template>
login
</template>
```
- 安装路由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
```
<template>
<router-view />
</template>
```
- 挂载路由
> 在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
<template>
login
<el-button type="primary" @click="login">登录</el-button>
</template>
<script>
import { defineComponent, onMounted } from "vue";
import { Login } from "@/api/login";
export default defineComponent({
name: "Login",
setup() {
const login = async () => {
const res = await Login({
userName: 'admin',
password: '123456'
})
}
return {
login
}
}
})
</script>
```
### 跨域管理
当前端调用后端接口的时候,假设后端接口的域名是`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
> 当我们登录成功之后后端其实会返回一个tokentoken就是用来做登录认证的后续所有请求都需要带上这个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
<style lang="less" scoped>
</style>
```
- autoprefixer自动处理css3浏览器前缀
安装autoprefixer
> 不需要安装postcssvite内部支持
```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---------------静态资源图片字体cssjs等
| ├─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----------打包配置文件
```
## 登录