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

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

源码地址

有新项目的????‍????,并且想尝试 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)

路由懒加载官方文档

自动化路由

通过 webpack 的 require.context 来进行递归式模块引入

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

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

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

api 管理

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

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

Vuex / 自定义的状态管理

Vuex

  • 命名空间

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

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

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

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

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

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

once(eventName: string, cb: CbType) {const { eventStack } = thisconst eventValue = eventStack[eventName]const tempCb = () => {      let isOutOfDate = false  return (data: object) => {if (isOutOfDate) returncb(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;// 不重复开启 Loadingif (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 结合队列来实现。(队列中存放的是当前请求的信息(自定义的一些规则,来判断是否是同一个请求)和取消函数)

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

[缺点:]

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

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

Web Workers 的引入

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

postMessage 不能发送函数

WebSocket 的嵌入

见 封装一个简单的 WebSocket 库

Element-ui 列表组件的封装

[思路:]

  • 划分组件,头部、内容、底部
    <header class="list-header animate__animated animate__fadeIn">  <slot name="head" />  <xw-searchv-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-paginationv-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[]
}复制代码

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

复杂例子

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

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

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 文件

效果如下:Vue + TypeScript + Element-ui + Axios 搭建前端项目基础框架_Vue_09

指令的封装

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