vite+vue3+vuex+router+element-plus+axios二次封装 架子搭建

2,446 阅读3分钟

使用vite快速搭建

  1. 命令行直接操作
    # npm 6.x
    npm init @vitejs/app vite-vue3 --template vue-ts
    
    # npm 7+ (需要额外的双横线)
    npm create vite vite-vue3 -- --template vue-ts
    
    # yarn
    yarn create @vitejs/app vite-vue3 --template vue-ts
    
  2. 安装依赖 -> 启动项目
    npm i
    npm run dev
    

修改vite配置文件

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
// 参考文档:https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': resolve('./src')
    }
  },
  base: './', // 打包路径
  server: {
    port: 3000, // 服务端口号
    open: true, // 服务启动时是否自动打开浏览器
    cors: true // 允许跨域
  }
})

集成路由

  1. 安装 vue-router@4
    npm i vue-router@4 --save
    
  2. 创建src/router/index.ts
    import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
    
    const routes: Array<RouteRecordRaw> = [
      {
        path: '/home',
        name: 'Home',
        component: () => import(/* webpackChunkName: "Home" */ '@/views/home.vue')
      },
      { path: '/', redirect: { name: 'Home' } }
    ]
    
    const router = createRouter({
      history: createWebHashHistory(),
      routes
    })
    
    export default router
    
  3. main.ts集成
    import { createApp } from 'vue'
    import App from '@/App.vue'
    
    import router from '@/router/index'
    
    createApp(App).use(router).mount('#app')
    

集成vuex

  1. 安装vuex@next
    npm i vuex@next --save
    
  2. 创建src/store/index.ts
    import { createStore } from 'vuex'
    
    const defaultState = {
      count: 0
    }
    
    // Create a new store instance.
    export default createStore({
      state() {
        return defaultState
      },
      mutations: {
        increment(state: typeof defaultState) {
          state.count += 1
        }
      },
      actions: {
        increment(context) {
          context.commit('increment')
        }
      },
      getters: {
        double(state: typeof defaultState) {
          return 2 * state.count
        }
      }
    })
    
  3. main.ts里挂载
    import { createApp } from 'vue'
    import App from '@/App.vue'
    
    import router from '@/router/index'
    import store from '@/store/index'
    
    createApp(App).use(router).use(store).mount('#app')
    

代码规范

  1. 集成prettier
    1. 安装
      npm i prettier --save-dev
      
    2. 根目录下新建.prettierrc
      {
        "useTabs": false,
        "tabWidth": 2,
        "printWidth": 100,
        "singleQuote": true,
        "trailingComma": "none",
        "bracketSpacing": true,
        "semi": false
      }
      

集成huskylint-staged

  1. husky: Git Hook 工具,可以设置在 git 各个阶段(pre-commit、commit-msg、pre-push 等)触发我们的命令。
  2. lint-staged: 在 git 暂存的文件上运行 linters。

jest单元测试使用

  1. 安装需要的包
    npm i @babel/core @babel/preset-env @testing-library/jest-dom @types/jest @vue/test-utils@next @babel/preset-typescript @vue/babel-plugin-jsx vue-jest@next -D
    
    // 下面三个包的版本需要固定,有些版本和 vue-test 的对应不上,则会出错
    npm i babel-jest@26.0.0 jest@26.0.0 ts-jest@26.4.4 -D
    
  2. 根目录新建 jest.config.js 文件
    const path = require('path')
    
    module.exports = {
      rootDir: path.resolve(__dirname),
      clearMocks: true,
      coverageDirectory: 'coverage',
      coverageProvider: 'v8',
      moduleFileExtensions: ['vue', 'js', 'json', 'jsx', 'ts', 'tsx', 'node'],
      // 别名设置
      moduleNameMapper: {
        '@/(.*)$': '<rootDir>/src/components/$1'
      },
      preset: 'ts-jest',
      testEnvironment: 'jsdom',
      // 测试文件
      testMatch: ['<rootDir>/src/__tests__/**/*.spec.(ts|tsx|js)'],
      testPathIgnorePatterns: ['/node_modules/'],
      moduleFileExtensions: ['js', 'json', 'ts', 'tsx'],
      
      transform: {
        '^.+\\.vue$': 'vue-jest',
        '^.+\\.(ts|tsx|js|jsx)$': [
          'babel-jest', {
            presets: [
              ['@babel/preset-env', { targets: { node: 'current' } }],
              ['@babel/preset-typescript']
            ],
            plugins: ['@vue/babel-plugin-jsx']
          }
        ]
      }
    }
    
    

集成 element-plus

  1. 安装依赖
    npm install element-plus --save
    
  2. 全局导入
    1. 修改main.ts
    import { createApp } from 'vue'
    import ElementPlus from 'element-plus'
    import 'element-plus/dist/index.css'
    import App from './App.vue'
    
    const app = createApp(App)
    
    app.use(ElementPlus)
    app.mount('#app')
    
    Volar 支持 - 修改tsconfig.json
    // tsconfig.json
    {
      "compilerOptions": {
        // ...
        "types": ["element-plus/global"]
      }
    }
    
  3. 按需导入 -- 自动导入
    1. 安装依赖
      npm install unplugin-vue-components unplugin-auto-import/vite vite-plugin-style-import --save-d
      
    2. 修改 vite.config.ts
      // vite.config.ts
      import styleImport from 'vite-plugin-style-import';
      import AutoImport from 'unplugin-auto-import/vite'
      import Components from 'unplugin-vue-components/vite'
      import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
      
      export default {
        plugins: [
          // ...
          <!--styleImport({-->
          <!--  libs: [{-->
          <!--    libraryName: 'element-plus',-->
          <!--    esModule: true,-->
          <!--    resolveStyle: name => {-->
          <!--      return `element-plus/theme-chalk/${name}.css`-->
          <!--    }-->
          <!--  }],-->
          <!--}),-->
          AutoImport({
            resolvers: [ElementPlusResolver()],
          }),
          Components({
            resolvers: [ElementPlusResolver()],
          }),
        ],
      }
      
    3. 修改app.vue
    <style>
    @import url(@/styles/main.less);
    /* 当在axios.ts里直接使用ElLoading时页面会报错,需手动引入,但这样做会导致loading样式丢失,所以在这里直接引入element-plus全部样式 */
    @import url(element-plus/dist/index.css);
    #app {
      font-family: Avenir, Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      text-align: center;
      color: #2c3e50;
    }
    </style>
    
  4. 按需导入 -- 手动导入
    1. 修改 vite.config.ts
      // vite.config.ts
      import ElementPlus from 'unplugin-element-plus/vite'
      
      export default {
        plugins: [ElementPlus()],
      }
      
    2. 在相应页面引入
      <template>
        <el-button>I am ElButton</el-button>
      </template>
      <script>
        import { ElButton } from 'element-plus'
        export default {
          components: { ElButton },
        }
      </script>
      

引入 axios

  1. 安装相应依赖
    npm i axios qs --save
    
  2. 设计功能列表
    1. 发送http请求
    2. 分模块文件管理接口
    3. 采用json形式管理接口列表
    4. 根据接口配置确定是否显示加载图标
  3. 目录为
    |- src
    |- |- api
    |- |- |- axios.ts
    |- |- |- index.ts
    |- |- |- customer.ts
    
  4. 新建工具类 axios.ts
    /**
     * @time 2021-11-29
     *
     * axios类二次封装
     */
    
    import axios, { AxiosResponse, CancelToken } from 'axios';
    import { AxiosInstance, AxiosRequestConfig } from 'axios';
    import { useRouter } from 'vue-router';
    const $router = useRouter();
    
    // 引入 element-plus loading及messageBox组件 本不需引入,但页面会报错,所以这里加个导入
    // App.vue 里手动引入 @import url(element-plus/dist/index.css);
    import { ElLoading, ElMessage } from 'element-plus';
    
    const CODE_MESSAGE = {
      200: '服务器成功返回请求的数据。',
      201: '新建或修改数据成功。',
      202: '一个请求已经进入后台排队(异步任务)。',
      204: '删除数据成功。',
      400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
      401: '用户没有权限(令牌、用户名、密码错误)。',
      403: '用户得到授权,但是访问是被禁止的。',
      404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
      406: '请求的格式不可得。',
      410: '请求的资源被永久删除,且不会再得到的。',
      422: '当创建一个对象时,发生一个验证错误。',
      500: '服务器发生错误,请检查服务器。',
      502: '网关错误。',
      503: '服务不可用,服务器暂时过载或维护。',
      504: '网关超时。'
    };
    
    class HttpAxios {
      // axios实例
      instance: AxiosInstance;
    
      // 超时时间
      timeout: number = 10000;
    
      // elementUI loading实例
      loadingInstance: any;
    
      // axios cancelToken 数组,方便处理取消请求
      cancelTokenArr: Array<any> = [];
    
      constructor(config: AxiosRequestConfig) {
        let instance: AxiosInstance = axios.create(config);
    
        // 设置请求拦截
        instance.interceptors.request.use(this._requestInterceptors, (error) => {
          return Promise.reject(error);
        });
    
        instance.interceptors.response.use(this._responseInterceptors, this._checkResponseError);
    
        this.instance = instance;
      }
    
      /**
       * 请求拦截,处理header部分传值及是否显示loading
       * @param config
       */
      _requestInterceptors = (config: AxiosRequestConfig) => {
        let _config = {
          withCredentials: false, // 处理跨域问题
          timeout: this.timeout
        };
        config.cancelToken = new axios.CancelToken(cancel => {
          this.cancelTokenArr.push({ cancel });
        })
        return Object.assign({}, config, _config);
      };
    
      /**
       * 返回拦截
       * @param response
       * @returns
       */
      _responseInterceptors = (response: AxiosResponse) => {
        if (this.loadingInstance) {
          this.loadingInstance.close();
          this.loadingInstance = null;
        }
        let data = response.data || {};
        if (data.code === '0000') {
          return data.data || {};
        }
        this._checkResponseCode(data);
        return null;
      };
    
      /**
       * 处理请求错误时情况 根据不同的状态码
       * @param error
       */
      _checkResponseError = (error: any) => {
        if (this.loadingInstance) {
          this.loadingInstance.close();
          this.loadingInstance = null;
        }
        if (/timeout/g.test(error)) {
          ElMessage.error('请求超时,请稍后再试!!!');
          return Promise.reject(error);
        }
        const {
          response: {
            status,
            statusText,
            data: { msg = '服务器发生错误' }
          }
        } = error;
        const { response } = error;
        ElMessage.error(CODE_MESSAGE[status] || statusText);
        return Promise.reject(error);
      };
    
      /**
       * 后台返回错误处理
       * @param code 后台定义错误码
       */
      _checkResponseCode = ({ code, message }: any) => {
        ElMessage.error(message);
        if (code === '') {
          // TODO 处理登录失效问题
          sessionStorage.setItem('route_to_login', location.href);
          $router.push({ path: '/login' });
        }
      };
    
      /**
       * 发送请求
       * @param url
       * @param params
       * @param method
       * @param config
       * @returns
       */
      sendRequest = (url: string, params: any, method: string = 'post', config?: any) => {
        if (!this.instance) {
          return;
        }
        if (!config || !config.isCloseLoading) {
          // TODO show loading
          this.loadingInstance = ElLoading.service({
            lock: true,
            background: 'rgba(0, 0, 0, 0.7)'
          });
        }
        const _method = method.toLocaleLowerCase();
        if (_method === 'get') {
          params = {
            params: params
          };
          return this.instance.get(url, params);
        }
        if (_method === 'formdata') {
          let reqData = new FormData();
          for (let key in params) {
            reqData.append(key, params[key]);
          }
          return this.instance.post(url, reqData);
        }
        return this.instance.post(url, params);
      };
    
      /**
       * 清除axios 请求
       */
      async clearRequests() {
        if (this.cancelTokenArr.length === 0) {
          return;
        }
        this.cancelTokenArr.forEach(ele => {
          ele.cancel();
        })
        this.cancelTokenArr = [];
      }
    }
    
    export default HttpAxios;
    
    
  5. 生成api方法 index.ts
    import router from '@/router/index';
    
    router.beforeEach(async (to, from, next) => {
      console.log(1111);
      await httpAxios.clearRequests();
      next();
    })
    
    import customer from '@/api/customer'
    
    /**
     * api 对象接口定义
     */
    interface apiMap {
      method?: string;
      url: string,
      config?: {
        isCloseLoading: boolean,
      }
    }
    
    interface apiMaps {
      [propName: string]: apiMap
    }
    
    import HttpAxios from "@/api/axios";
    
    const httpAxios = new HttpAxios({});
    
    function generateApiMap(maps: apiMaps) {
      let methodMap: any = {};
      for(let key in maps) {
        methodMap[key] = toMethod(maps[key]);
      }
      return methodMap;
    }
    
    function toMethod(options: apiMap) {
      options.method = options.method || 'post';
      const { method = 'post', url, config } = options;
      return (param: any = {}) => {
        return httpAxios.sendRequest(url, param, method, config);
      }
    }
    
    const apis = generateApiMap({
      ...customer
    })
    export default {
      ...apis // 取出所有可遍历属性赋值在新的对象上
    }
    

(ps: 项目地址: gitee.com/xhuarui/fro…)