ai-vue3-admin/项目开发手册.md
2021-03-25 18:29:58 +08:00

18 KiB
Raw Blame History

Vue3项目开发手册

初始化项目

技术选型

Vue3 + Element-plus + Vite

  • Vue3

    • 支持 Vue2 的大多数特性
    • 支持 Typescript
    • 使用 Proxy 代替 defineProperty 实现数据响应式
    • 重写虚拟 DOM 的实现和 Tree-Shaking
    • Vue3中文官网
  • Element-plus

  • Vite

    • 是一种新型前端构建工具,能够显著提升前端开发体验。

    • Vite中文官网

创建项目

我们将使用vite和vue3手动进行前端项目架构。为方便大家从vue2过渡到vue3本项目我们先不使用ts。

使用vite的命令创建项目

需要 Node.js 版本 >= 12.0.0

# 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

创建完后,会提示我们进入项目目录,安装依赖,运行项目:

cd erp-vue3
npm install
npm run dev

最后我们来看下项目结构

├─node_modules -----------依赖包
├─public -----------------静态文件
├─index.html--------------主页面
├─src---------------------核心文件目录我们开发的代码都在src目录
|  ├─assets---------------静态资源图片字体cssjs等
|  ├─components-----------公共组件
|  ├─App.vue--------------根组件
|  ├─main.js--------------入口文件
├─package.json------------项目基本信息和依赖包信息
├─vite.config.js----------配置文件

完善项目架构

目前项目中还没有路由、状态管理、UI组件库、ajax库

路由

  • 在src目录中创建views目录用来存放页面

    我们使用模块化的方式来管理页面,比如用户管理、权限管理等模块,每个模块下可能有多个页面,所以每个模块都要创建一个目录

    在views目录中创建home目录代表home模块home目录中创建index.vueindex.vue代表home模块的主页面

    <template>
    	home
    </template>
    

    同样的方式创建一个login模块

    <template>
    	login
    </template>
    
  • 安装路由vue-router

    # 安装最新版的vue-router
    npm install vue-router@next
    
  • 在src目录中创建router目录用来存放路由配置文件

    路由配置就是配置views模块中的页面通过什么地址来访问路由我们也使用模块化管理views中有多少模块就创建多少路由配置文件此外还需要一个index.js来统一分配路由模块

    router目录中创建modules目录 modules目录中创建home.js和login.js

    // home.js
    const Home = () => import("../../views/home/index.vue");
    
    export default [
      {
        path: "/home",
        name: "home",
        component: Home,
      }
    ]
    
    // login.js
    const Login = () => import("../../views/login/index.vue");
    
    export default [
      {
        path: "/login",
        name: "login",
        component: Login,
      }
    ]
    

    router目录中创建index.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;
    
  • 挂载路由

    在main.js中挂载路由

    // main.js
    import { createApp } from 'vue'
    import App from './App.vue'
    // 引入路由
    import router from './router'
    // 使用use注册路由
    createApp(App).use(router).mount('#app')
    
  • 配置alias路径别名

    我们可以看到上面的引入路径比较深这个时候可以配置一个src目录的别名需要在vite.config.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中

    // 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状态管理插件

    # 安装最新版的vuex
    npm install vuex@next
    
  • 在src目录中创建store目录用来存放状态模块

    状态管理也使用模块化的方式

    在store目录中创建modules目录modules中创建一个模块app.js

    // app.js
    export default {
      namespaced: true,
      state: {},
      mutations: {},
      actions: {},
    };
    

    在store目录中创建index.js

    //index.js
    import { createStore } from "vuex";
    import app from "./modules/app";
    
    export default createStore({
      modules: {
        app,
      },
    });
    
    
  • 挂载store

    // main.js
    import { createApp } from 'vue'
    import App from './App.vue'
    // 引入路由
    import router from './router'
    // 引入store
    import store from './store'
    // 使用use注册路由和store
    createApp(App).use(router).use(store).mount('#app')
    

组件库 Element Plus

  • 安装element-plus并且在main.js中引入

    npm install element-plus
    

    引入element-plus

    // main.js
    import { createApp } from 'vue'
    import App from './App.vue'
    // 引入路由
    import router from './router'
    // 引入store
    import store from './store'
    // 引入element-plus
    import ElementPlus from "element-plus";
    import "element-plus/lib/theme-chalk/index.css";
    
    // 使用use注册路由和store
    createApp(App).use(router).use(store).use(ElementPlus).mount('#app')
    
    
  • vue3中如何全局调用element-plus中的提示插件

    import {getCurrentInstance} from 'vue'; 
    
    setup () {
      const { ctx } = getCurrentInstance(); // 可以把ctx当成vue2中的this
      ctx.$message.success("yes")
      ctx.$loading()
    }
    

接口管理

  • 安装axios

    npm install axios
    
  • 接口管理

    项目中我们调用接口的时候,有可能有很多页面都会调用同一个接口,所以我们把所有的接口统一进行模块化管理

    在src目录中创建api目录用来存放所有的接口

    在api目录中创建一个模块login.js该文件包括跟登录注册相关的所有接口

    // login.js
    import axios from 'axios'
    
    // 登录接口
    export const Login = data => {
      return axios.request({
        url: "/api/login",
        method: "post",
        data,
      });
    };
    
    // 其它接口...
    

    在页面中调用登录接口

    在views/login/index.vue中

    <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中设置代理

// vite.config.js增加如下配置
server: {
  proxy: {
    "/api": {
      target: "http://dev.erp.com",
      changeOrigin: true,
    },
  },
},

mock管理

当后端接口没有开发完成的时候前端就需要根据接口文档mock数据进行开发

vite中可以使用vite-plugin-mock插件创建mock服务

  • 安装vite-plugin-mock

    npm install -D mockjs vite-plugin-mock
    
  • 配置mock需要在vite.config.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"),
          },
        },
      });
    };
    
  • package.json中配置mode为mock的启动命令

// ... "scripts": { "dev": "vite", "mock": "vite --mode mock", // ...


重新启动项目

```powershell
npm run mock
  • 在根目录中创建mock目录用来存放mock接口

    在mock目录中创建一个test.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

    // 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中为了更好的进行响应式控制

    // 登录
    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

    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目录主要是用来存放一些常用工具函数

    // 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

// 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失效的时候页面需要重定向到登录页

  • 全局路由守卫

    编辑router/index.js

    // 引入TOKEN变量
    import { TOKEN } from '@/store/modules/app'
    
    // 全局路由守卫注意vue-router4的路由守卫不需要next跳转而是通过return返回false或者一个路由地址
    router.beforeEach((to, from) => {
      if (!window.localStorage[TOKEN] && to.name !== 'login') {
        return {
          name: 'login',
          query: {
            redirect: to.path // redirect是指登录之后可以跳回到redirect指定的页面
          },
          replace: true
        }
      }
    })
    

css

  • css预处理器

    本项目我们使用less先安装less

    npm install -D less
    

    使用less

    <style lang="less" scoped>
    
    </style>
    
  • autoprefixer自动处理css3浏览器前缀

    安装autoprefixer

    不需要安装postcssvite内部支持

    npm install autoprefixer -D
    

    根目录创建postcss.config.js

    // postcss.config.js
    module.exports = {
      plugins: {
        // 兼容浏览器,添加前缀
        'autoprefixer': {}
      }
    }
    

    package.json中配置浏览器版本

    //package.json
    {
      "browserslist": [
        "> 1%",
        "last 2 versions"
      ],
    }
    

项目结构

最终的项目结构如下

没有后缀的,代表是一个文件夹

├─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----------打包配置文件

登录