Vue + TypeScript + Element-ui + Axios 搭建前端项目基础框架

15,631 阅读6分钟

俗话说【工欲善其事,必先利其器】,在我们的前端领域也是一样的道理,想做好一个项目,必须要要有一个完整的项目架构做为支撑,才能更好的进行团队合作、为业务做嫁衣。

每每新建一个项目都需要进行一些基础的配置、当我们没有做一个基础模版的时候都需要从其它的项目复制过来,甚至需要自己重写一些工具类,这时候我们就急需要这个基础模版。

源码地址

有新项目的👩‍🎓,并且想尝试 Vue + TypeScript 开发的可用这套模板

基础功能列表

  • 目录结构的划分
  • 环境的区分(开发、测试、生产)
  • 路由自动化管理、按需加载
  • 页面加载进度提示
  • api 管理
  • Vuex / 自定义的状态管理
  • axios 的封装(重复请求取消,多个请求发送时只出现一个loading,token 失效重新刷新)
  • 通用的工具函数(防抖、截流等)
  • 常见指令的封装(动画指令、图片懒加载、复制指令等)
  • Web Workers 的引入(开启一个线程、分担主线程的计算压力、在处理特别耗时的任务中特别有用)
  • WebSocket 的嵌入(双向通讯)
  • 多页面配置
  • Element-ui(表格、搜索、分页组件的封装、主题、国际化等)
  • git commit 提交记录的优化
  • 移动、pc端的适配
  • 权限的处理(按钮权限, 根据权限动态添加路由
  • 自动化测试
  • 埋点

【红色部分还未完成】

创建项目

选择 [Vue Cli](https://cli.vuejs.org/zh/guide/) 脚手架 快速创建

`vue create xxx`

目录结构

mac 下安装 brew 

/bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)"

安装 tree

brew install tree

tree 列出目录结构

├── README.md			说明文件
├── babel.config.js		bable 配置文件
├── jest.config.js		单元测试配置文件
├── package.json		项目信息文件
├── public				
│   ├── favicon.ico
│   ├── index.html
│   ├── other.html
│   └── static			静态资源文件
│       ├── css			
│       │   └── reset.css
│       └── worker		Web Workers 文件夹(根据 Web Workers 的特殊性、需要放在在服务器)
│           └── test.worker.js
├── src
│   ├── api				api 管理,按多页面分文件夹
│   │   ├── default-page
│   │   │   ├── index.ts			导出 api
│   │   │   └── testModule.api.ts	页面下的小模块 api
│   │   └── other-page
│   │       ├── index.ts
│   │       └── newsModule.api.ts
│   ├── assets			静态资源文件,但会经过 webpack 进行编译,不需要编译的可以放到 public 目录下
│   │   └── styles		公共样式(基础样式、其它公用样式)
│   │       ├── common.scss
│   │       └── pageAnimate.scss
│   ├── components
│   │   ├── business	业务组件
│   │   │   └── xw-list
│   │   │       ├── index.ts
│   │   │       ├── index.type.ts
│   │   │       └── index.vue
│   │   ├── common		基础组件
│   │   │   ├── xw-pagination
│   │   │   │   ├── index.type.ts
│   │   │   │   └── index.vue
│   │   │   ├── xw-search
│   │   │   │   ├── generateEl.vue
│   │   │   │   ├── index.type.ts
│   │   │   │   └── index.vue
│   │   │   └── xw-table
│   │   │       ├── coustomColumn.vue
│   │   │       ├── generateElTable.ts
│   │   │       ├── generateElTableColumn.ts
│   │   │       ├── index.type.ts
│   │   │       └── index.vue
│   │   └── example		例子文件
│   │       ├── langExample.vue
│   │       ├── requestExample.vue
│   │       ├── vuexExample.vue
│   │       ├── workerExample.vue
│   │       └── wsExample.vue
│   ├── directive		指令
│   │   ├── animate.directive.ts
│   │   ├── copy.directive.ts
│   │   ├── debounce.directive.ts
│   │   ├── draggable.directive.ts
│   │   ├── emoji.directive.ts
│   │   ├── index.ts
│   │   ├── longpress.directive.ts
│   │   └── permissions.directive.ts
│   ├── i18n			国际化
│   │   ├── index.ts
│   │   └── lang
│   │       ├── en.ts
│   │       └── zh.ts
│   ├── layout			项目布局
│   │   ├── base.layout.vue
│   │   └── other.layout.vue
│   ├── mock			mock 数据
│   │   └── index.js
│   ├── plugins			项目插件
│   │   ├── config.ts
│   │   ├── index.ts
│   │   └── lazyLoad.plugin.ts
│   ├── router			路由管理,按多页面分文件夹
│   │   ├── config.ts
│   │   ├── default
│   │   │   └── module1.router.ts	页面下的小模块 api
│   │   ├── globalHook.ts			全局路由钩子
│   │   ├── index.ts				导出所有路由
│   │   └── other
│   │       └── module1.router.ts
│   ├── shims-tsx.d.ts
│   ├── shims-vue.d.ts
│   ├── store			Vuex管理,按多页面分文件夹
│   │   ├── common					基础的 Vuex 模块
│   │   │   ├── permissions.vuex.ts
│   │   │   └── user.vuex.ts
│   │   ├── default
│   │   │   └── home.vuex.ts
│   │   └── index.ts				导出基础的 Vuex 模块
│   ├── theme			主题
│   │   ├── fonts
│   │   │   ├── element-icons.ttf
│   │   │   └── element-icons.woff
│   │   └── index.css
│   ├── types			类型控制文件(提供语法提示)
│   │   └── vue.d.ts
│   ├── utils			工具函数文件夹
│   │   ├── common.ts				通用的 js 函数
│   │   ├── dom.ts					dom 操作相关的
│   │   ├── eventCenter.ts			发布订阅者模式(事件管理中心)
│   │   ├── progressBar.ts			页面进度条
│   │   ├── readyLocalStorage.ts	读取本地存储数据(用户信息、token、权限等)并存到 Vuex 中
│   │   ├── request					Ajax 请求封装
│   │   │   ├── index.ts
│   │   │   ├── index.type.ts
│   │   │   └── request.ts
│   │   ├── requestInstance.ts		Ajax 实例
│   │   ├── useElement.ts			按需使用 Element-ui
│   │   └── ws.ts					WebSocket 通讯
│   └── views			按多页面分文件夹
│       ├── 404.vue
│       ├── default-page	
│       │   ├── App.vue
│       │   ├── main.ts
│       │   └── test-module			小模块
│       │       ├── home			具体页面
│       │       │   └── index.vue
│       │       └── home2
│       │           └── index.vue
│       ├── login.vue
│       └── other-page
│           ├── App.vue
│           ├── main.ts
│           └── news-module			小模块
│               ├── news1			具体页面
│               │   ├── components	页面内组件
│               │   │   └── coustomColumnHeader.vue
│               │   └── index.vue
│               └── news2
│                   ├── components
│                   │   └── coustomColumnHeader.vue
│                   └── index.vue
├── tests
│   └── unit
│       └── example.spec.ts
├── tsconfig.json
├── vue.config.js		webpack 配置文件
├── yarn-error.log
└── yarn.lock
└── .env.development 	本地环境配置
└── .env.production		生产环境配置
└── .env.staging		测试环境配置

环境区分

通过 webpack 提供的模式来实现不同的 环境变量

  • 根目录下分别新建一下三个文件 .env.development
# 指定模式
NODE_ENV = "development"
# Ajax 地址
VUE_APP_REQUEST_URL = 'http://localhost:8080'

.env.production

NODE_ENV = "production"
VUE_APP_REQUEST_URL = 'http://prod.com'

.env.staging

NODE_ENV = "production"
VUE_APP_REQUEST_URL = 'http://staging.com'
  • 添加编译命令

package.json

"scripts": {
  "serve": "vue-cli-service serve --mode development",		// 开发
  "build:stage": "vue-cli-service build --mode staging",	// 测试
  "build": "vue-cli-service build"							// 生产
}

路由自动化管理、按需加载

按需加载

import() 方式 【推荐】

const App = () => import(/* webpackChunkName: app */ './app.vue')

/* webpackChunkName: app */ 组件分块

异步组件的方式

异步组件

const App = resolve => require(["./app.vue"], resolve)

路由懒加载官方文档

自动化路由

通过 webpackrequire.context 来进行递归式模块引入

require.context(
  directory: String,
  includeSubdirs: Boolean /* 可选的,默认值是 true */,
  filter: RegExp /* 可选的,默认值是 /^\.\/.*$/,所有文件 */,
  mode: String  /* 可选的, 'sync' | 'eager' | 'weak' | 'lazy' | 'lazy-once',默认值是 'sync' */
)
require.context 的参数不能接收变量

【好处:减少多人开发的冲突、新模块忘记引入】

api 管理

api 统一管理,项目中我们一个页面的【增删改查】url 都是统一的,只会改变请求方式,所以将 url 集中管理

Vuex / 自定义的状态管理

Vuex

  • 命名空间

    解决不同模块之间 actions mutions 之间的命名冲突

  • 动态注册模块 除基础模块之外、其它模块动态注册及卸载

自定义的状态管理(发布订阅者模式)

【思路:】 一个集合中存储了不同类型的事件函数,并功过监听、取消、派发等方法来处理这个集合

其中处理单次监听用到了闭包,来存贮是否已经执行过

once(eventName: string, cb: CbType) {
    const { eventStack } = this
    const eventValue = eventStack[eventName]
    const tempCb = () => {
      let isOutOfDate = false

      return (data: object) => {
        if (isOutOfDate) return
        cb(data)
        isOutOfDate = true
      }
    }

    eventValue ? eventValue.push(tempCb()) : eventStack[eventName] = [tempCb()]
  }

axios 的封装

【功能列表】

  • 请求地址的处理

    主要处理路由的规范性

    /**
     * 处理路径
     * @param url 路径
     * @param isBaseURL 是否是根路径
     */
    private transformUrl(url = "", isBaseURL = false) {
      if (!url) return url;
    
      if (isBaseURL) {
        if (!/\/$/.test(url)) {
          return `${url}/`;
        }
    
        return url;
      }
    
      if (/^\//.test(url)) {
        return `${url.substr(1)}`;
      }
    
      return url;
    }
    
  • 是否需要 loading,多个请求串行时只出现一个 loading

    用一个变量记录请求的个数,有新的请求的时候数量 +1, 当数量为 0 并且需要 loading 的时候开启 loading,当请求完成之后 -1,并关闭 loading

      /**
       * Loading 的开启关闭
       * @param customConfig 自定义配置项
       * @param isOpen 是否开启
       */
      private handleLoading(customConfig: CustomConfigType, isOpen: boolean) {
            if (!customConfig.isNeedLoading) return;
            // 不重复开启 Loading
            if (this.requestCount !== 0) return;
    
            if (isOpen) {
                console.log("开启 Loading");
                return
            }
    
            console.log("关闭 Loading");
      }
    
      /**
     * 发起请求
     * @param config 配置项
     * @param customConfig 自定义配置
     */
    private async transfromRquest(
      config: AxiosRequestConfig,
      customConfig: CustomConfigType = {}
    ): Promise<AxiosResponse> {
      customConfig = { ...this.defaultCustomConfig, ...customConfig };
    
      this.transformUrl(config.url);
      this.handleLoading(customConfig, true);
      this.addToken(config, customConfig);
      this.requestCount++
    
      try {
        const result = await this.axios.request(config);
        return result;
      } catch (error) {
        // ...
      } finally {
          this.requestCount--
    		this.handleLoading(customConfig, false);
      }
    }
    
  • 是否需要 token

  /**
   * token 处理
   * @param config 配置项
   * @param customConfig 自定义配置项
   */
  private addToken(config: AxiosRequestConfig, customConfig: CustomConfigType) {
    if (customConfig.isNeedToken) {
      config.headers = {
        token: store.getters['userStore/getToken'] || ''
      };
    } else {
      config.headers = {};
    }
  }
  • 请求错误的处理,当出现 token 失效的时候,重新刷新 token 再发送失败的请求
  /**
   * 发起请求
   * @param config 配置项
   * @param customConfig 自定义配置
   */
  private async transfromRquest(
    config: AxiosRequestConfig,
    customConfig: CustomConfigType = {}
  ): Promise<AxiosResponse> {
    customConfig = { ...this.defaultCustomConfig, ...customConfig };

    this.transformUrl(config.url);
    this.handleLoading(customConfig, true);
    this.addToken(config, customConfig);
		this.requestCount++

    try {
      const result = await this.axios.request(config);
      return result;
    } catch (error) {
      const { code, config } = error

      if (code === 401) {
        // 解决 token 失效的

        // 方案一 跳转至登录页
        // store.commit('userStore/setToken', '')
        // store.commit('permissionsStore/setPermissions', {})
        // router.replace({ path: '/login', query: {
        //   redirectUrl: router.currentRoute.fullPath
        // } })

        // 方式二 自动刷新 token 并重新发起失败的请求
        const res = await this.transfromRquest({
          method: 'post',
          url: '/refresh-token'
        })
        console.log(res, '/refresh-token')
        store.commit('userStore/setToken', res.data.token)
        return this.transfromRquest(config)

        // 方式三 在请求拦截里面先校验 token 是否过期 再发起请求
      }

      this.handleError(customConfig, error);
      return Promise.reject(error);
    } finally {
			this.requestCount--
      this.handleLoading(customConfig, false);
		}
  }
  • 取消请求

    利用 Axios 提供的 CancelToken 结合队列来实现。(队列中存放的是当前请求的信息(自定义的一些规则,来判断是否是同一个请求)和取消函数)

[缺点:]

类似这种取消请求,其实服务端是有收到的,只是浏览器层面做了一层处理等不到响应而已。

当需要做防止数据的重复提交的时,这种方式的实现是不准确的,可以考虑防抖、变量控制函数的执行、变量控制按钮的点击状态等

Web Workers 的引入

postMessage 不能发送函数

WebSocket 的嵌入

封装一个简单的 WebSocket 库

Element-ui 列表组件的封装

[思路:]

  • 划分组件,头部、内容、底部
    <header class="list-header animate__animated animate__fadeIn">
      <slot name="head" />
      <xw-search
        v-if="searchOption"
        :searchOption="searchOption"
        :searchParams="searchParams"
        @onSearch="getList"
      >
        <slot name="search" />
      </xw-search>
    </header>

    <main class="list-main">
      <slot name="main" />
      <xw-table :tableOption="tableOption" />
    </main>

    <footer class="list-footer">
      <slot name="footer" />
      <xw-pagination
        v-if="paginationOption"
        :paginationOption.sync="paginationOption"
        @onPagination="getList"
      />
    </footer>
  • 搜索结果由列表组件保管 searchParams: SearchParams = {};

  • 表格数据的组装

    为了方便开发过程中减少模版的编写,将表格的所有相关操作都封装成配置项的形式。

import { Component } from 'vue'

export interface TableOption {
  // element-ui 表格的配置属性
  tableAttribute: TableAttribute
  // 列的配置属性
  tableColumn: TableColumn[]
}

export interface TableAttribute {
  // 属性
  props: {
    data: object[]
    [index: string]: any
  }
  // 事件
  on: { [key: string]: Function | Function[] }
}

export interface TableColumn {
  // 属性
  props: {
    label?: string
    prop?: string
    [index: string]: any
  },
  // 插槽
  slots?: {
    [index: string]: {
      // 属性
      options?: object
      // 自定义组件
      component: Component
    }
  }
  // 多级表头
  columnChild?: TableColumn[]
}

复杂例子

当前行的编辑、根据权限展示不同的按钮、按钮的加载跟禁用状态

git commit 提交记录的优化

使用 commitizen 替代你的 git commit , commitizen 还需要适配器的配合,官方推荐 cz-conventional-changelog

  • 安装 npm install -D commitizen cz-conventional-changelog

  • 配置 package.json中配置:

"scripts": {
    "commit": "git-cz"
  },
"config": {
    "commitizen": {
      "path": "node_modules/cz-conventional-changelog"
    }
  }
  • 使用 npm run commit

  • 自定义适配器

    • 安装 npm i -D cz-customizable @commitlint/config-conventional @commitlint/cli
    • 配置
"config": {
    "commitizen": {
      "path": "node_modules/cz-customizable"
    }
  }

同时在项目目录下创建 .cz-config.js .commitlintrc.js 文件

效果如下:

指令的封装

  • 结合 animate.css 的自定义动画指令
  • 复制粘贴指令
  • 防抖指令
  • 拖拽指令
  • 禁止表情及特殊字符指令
  • 长按指令
  • 权限控制指令

源码地址

以上只是简单的概述,详细内容请看 源码地址

博文推荐

【笔记不易,如对您有帮助,请点赞,谢谢】