vlambda博客
学习文章列表

Vue2.x项目工程环境搭建思路

前言

去年年底,公司的前端团队确立了代码规范。然而离开了实际项目环境加上普遍紧张的开发周期,规范终究是纸上谈兵。鉴于此,一位同事去开发脚手架。我也花了几天时间搭建了一套 Vue2.x 的工程环境尝试给予一些支持,虽然后面看来对他的实际帮助不大。

由于公司目前使用 Vue3 的项目几乎没有,所以选择搭建 Vue2 的模板。我希望这个模板能强制代码规范、提交规范;保证代码的可靠性的同时提升开发效率;并且为未来可能升级 Vue3 提供一些便利。

本文将主要描述我在搭建环境与封装的整体思路,不会详细描述搭建细节,需要具体实现的小伙伴可以查看文末的参考。但是我不建议照搬这些操作,由于版本的差异会导致踩奇奇怪怪的坑,最可靠的方法的仍是直接去查阅官方文档。

技术栈

  • 编程语言:TypeScript 4.x + Javascript

  • 构建工具:Vue Cli 5

  • 前端框架:Vue2.x + Vue/CompositionApi

  • 路由工具:Vue Router 3.x

  • 状态管理:Pinia 2.x

  • CSS 预编译:Less

  • HTTP 工具:Axios

  • Git Hook 工具:Husky + LintStaged

  • 代码规范:Eslint + Prettier + EditorConfig

  • 提交规范:Commitizen + Commitlint

  • 单元测试:Jest

  • 依赖检测工具:Depcheck

架构搭建

使用 Vue Cli 快速初始化项目雏形

使用 Vue Cli 快速搭建基础模板,选择 vue-ts ,其他的例如 Eslint 等由于要修改配置与封装,我更倾向于自己看文档手动添加。

修改 vue.config.js 文件

const path = require('path');
const { defineConfig } = require('@vue/cli-service');

module.exports = defineConfig({
 transpileDependencies: true,
 chainWebpack: (config) => {
   // 配置别名后需要重启项目
   config.resolve.alias.set('@', path.resolve('src'));
},
 publicPath: './',
});

由于我对 webpack 缺乏系统的认知,所以只是做了最简单的配置。publicPath解决的是打包后资源文件路径错误的问题,chainWebpack中设置了路径别名。之前我看网上的方法增加了一个webpack.config.js将别名配置在里面,我认为这样不好,vue.config.jswebpack.config.js本就在功能上有很大的重复,完全没有必要如此。

规范目录结构

├── public/
└── src/
  ├── assets/                   // 静态资源目录
  ├── api/                       // 接口方法目录
  ├── components/               // 公共组件目录
  ├── router/                   // 路由配置目录
  ├── store/                     // 状态管理目录
  ├── style/                     // 通用 CSS 目录
  ├── utils/                     // 工具函数目录
  ├── views/                     // 页面组件目录
  ├── App.vue
  ├── main.ts
  ├── shims-vue.d.ts
├── tests/                         // 单元测试目录
// 配置文件放在 src 同级目录下

额外说下publicassets的区别:打包后public文件夹中的文件会原封不动地放到dist文件夹中,assets文件夹中的文件会被合并到一个文件中。图片、字体等静态资源我会放在assets,第三方程序则是放在public

Vue Router

在路由文件中,登录页、首页、Layout组件我会直接引入,其他的页面则使用按需加载。

状态管理工具 Pinia

简单介绍,Pinia 是新一代 Vuex ,3月份尤大直播时说过不会有 Vuex5 了。相较于 vuex4,使用更加简单。但是我碰到了个坑,在我有一个同事的电脑上项目中的 Pinia 会报错,但是其他人的则没有问题,初步猜测跟版本号有关系。

HTTP 工具 Axios

纠结了一下午如何去封装,看了许多封装的思路,最后还是选择直接从vue-element-admin中抄了一份。

// utils/request
import axios from 'axios';

// create an axios instance
const service = axios.create({
 baseURL: process.env.VUE_APP_BASE_URL, // url = base url + request url
 // withCredentials: true, // send cookies when cross-domain requests
 timeout: 5000, // request timeout
});

// request interceptor
service.interceptors.request.use(
 // do something before request is sent
(config) => config,
(error) => {
   // do something with request error
   console.log(error); // for debug
   return Promise.reject(error);
},
);

// response interceptor
service.interceptors.response.use(
 /**
  * If you want to get http information such as headers or status
  * Please return response => response
  */

 /**
  * Determine the request status by custom code
  * Here is just an example
  * You can also judge the status by HTTP Status Code
  */
(response) => {
   const { data } = response;

   return data;
},
(error) => {
   console.log(`err${error}`); // for debug

   return Promise.reject(error);
},
);

export default service;

api则单独抽一个文件夹出来 ,不要全部放在一个文件与注册到原型链中。否则如果有几百个接口会不好维护,并且这样搞原型链也太大了。

另外,我以前会单独封装postget方法,没有必要这样,axios本身就有相应的api了,我的用法如下:

// api/example
import request from '@/utils/request';

export function useAxiosGet(param1: unknown, param2: unknown) {
 return request.get('/banner', { params: { param1, param2 } });
}

export function useAxiosPost(data: unknown) {
 return request.post('/banner', data);
}

CSS 预编译器 Less

Less/Sass/Stylus 都差不多,sass 会有版本号的问题,所以同事更倾向于使用 less 。

Vue/CompositionApi

composition-api会提高代码的聚合度,在一些场景下非常好用。另外如果有未来升级 Vue3 的打算,也可以使用。

代码规范

EditorConfig

EditorConfig 有助于为不同 IDE 编辑器上处理同一项目的多个开发人员维护一致的编码风格。我主要是需要这个来配置换行符,由于公司使用的都是 windows ,所以换行符设置crlf

ESLint + Prettier

我在配置这两者上花费了相当多的时间,解决二者的冲突可以说是让我头痛的不行。我看到许多项目会二者都全部配上,但在实际体验后说句真心话,我认为单纯的 ESLint 已经够用了。毕竟二者本质上也只是在rules上的些许不同而已,引入它们其实只是想解决团队成员代码风格差异带来的维护困难。我的 ESLint 配置如下:

module.exports = {
 env: {
   browser: true,
   node: true,
   es2021: true,
},
 extends: [
   'airbnb-base',
   'eslint:recommended',
   'plugin:vue/recommended',
   'plugin:@typescript-eslint/recommended',
   'plugin:jest/recommended',
],
 parser: 'vue-eslint-parser',
 parserOptions: {
   ecmaVersion: 'latest',
   parser: '@typescript-eslint/parser',
   sourceType: 'module',
},
 plugins: [
   'vue',
   '@typescript-eslint',
],
 settings: {
   'import/resolver': {
     webpack: {
       // 此处config对应webpack.config.js的路径,我这个路径是vue-cli默认的路径
       config: 'node_modules/@vue/cli-service/webpack.config.js',
    },
  },
},
 rules: {
   'import/extensions': [
     'error',
     'ignorePackages',
    {
       js: 'never',
       jsx: 'never',
       ts: 'never',
       tsx: 'never',
    },
  ],
   'no-param-reassign': [
     'error',
    {
       props: true,
       ignorePropertyModificationsFor: [
         'res', // for Express responses
         'item', // for Express responses
         'state', // for vuex state 解决assignment to property of function parameter 'state'
      ],
    },
  ],
   indent: [2, 2, {
     SwitchCase: 1,
  }],
   complexity: [
     'error',
    {
       max: 40,
    },
  ],
   'linebreak-style': 0,
},
};

可以看到我的配置是采用airbnb风格的规则以及一些官方推荐的风格,尽管我如此配置,但不代表我完全喜欢这些规则。例如在我看来++&&运算符非常简洁却不被推荐;no-param-reassign这条规则影响了我处理接口拿到数据。具体在配置规则时,还是要从团队的角度出发,考虑如何减少维护成本。

Husky + LintStaged

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

在编码时,ESLint 和 Prettier,会对我们写的代码进行实时校验,在一定程度上能有效规范我们写的代码,但可能会有人觉得这些限制很麻烦,选择无视这些提示。

所以,我们还需要做一些限制,让没通过 ESLint 检测和修复的代码禁止提交,从而保证仓库代码都是符合规范的。

为了解决这个问题,我们需要用到 Git Hook,在本地执行 git commit 的时候,就对所提交的代码进行 ESLint 检测和修复(即执行 eslint --fix),如果这些代码没通过 ESLint 规则校验,则禁止提交。

提交规范

git commit的描述信息精准,在后期维护和 Bug 处理时会变得有据可查。我选用的是社区最流行、最知名、最受认可的 Angular 团队提交规范。

Commitizen

初次接触提交规范的同学可能会头晕,不知道怎么写。Commitizen 解决的就是这个问题,项目中通过npx cz命令,跟着提示走就可以完成一次规范的提交。需要自定义配置提交,修改.cz-config.js文件即可。我的配置如下:

module.exports = {
   // type 类型(定义之后,可通过上下键选择)
   types: [
      { value: 'feat', name: 'feat:     新增功能' },
      { value: 'fix', name: 'fix:     修复 bug' },
      { value: 'docs', name: 'docs:     文档变更' },
      { value: 'style', name: 'style:   代码格式(不影响功能,例如空格、分号等格式修正)' },
      { value: 'refactor', name: 'refactor: 代码重构(不包括 bug 修复、功能新增)' },
      { value: 'perf', name: 'perf:     性能优化' },
      { value: 'test', name: 'test:     添加、修改测试用例' },
      { value: 'build', name: 'build:   构建流程、外部依赖变更(如升级 npm 包、修改 webpack 配置等)' },
      { value: 'ci', name: 'ci:       修改 CI 配置、脚本' },
      { value: 'chore', name: 'chore:   对构建过程或辅助工具和库的更改(不影响源文件、测试用例)' },
      { value: 'revert', name: 'revert:   回滚 commit' }
  ],

   // scope 类型(定义之后,可通过上下键选择)
   scopes: [
      ['components', '组件相关'],
      ['hooks', 'hook 相关'],
      ['utils', 'utils 相关'],
      ['element-ui', '对 element-ui 的调整'],
      ['styles', '样式相关'],
      ['deps', '项目依赖'],
      ['auth', '对 auth 修改'],
      ['other', '其他修改'],
       // 如果选择 custom,后面会让你再输入一个自定义的 scope。也可以不设置此项,把后面的 allowCustomScopes 设置为 true
      ['custom', '以上都不是?我要自定义']
  ].map(([value, description]) => {
       return {
           value,
           name: `${value.padEnd(30)} (${description})`
      }
  }),

   // 是否允许自定义填写 scope,在 scope 选择的时候,会有 empty 和 custom 可以选择。
   // allowCustomScopes: true,

   // allowTicketNumber: false,
   // isTicketNumberRequired: false,
   // ticketNumberPrefix: 'TICKET-',
   // ticketNumberRegExp: '\\d{1,5}',


   // 针对每一个 type 去定义对应的 scopes,例如 fix
   /*
  scopeOverrides: {
    fix: [
      { name: 'merge' },
      { name: 'style' },
      { name: 'e2eTest' },
      { name: 'unitTest' }
    ]
  },
  */

   // 交互提示信息
   messages: {
       type: '确保本次提交遵循 Angular 规范!\n选择你要提交的类型:',
       scope: '\n选择一个 scope(可选):',
       // 选择 scope: custom 时会出下面的提示
       customScope: '请输入自定义的 scope:',
       subject: '填写简短精炼的变更描述:\n',
       body:
           '填写更加详细的变更描述(可选)。使用 "|" 换行:\n',
       breaking: '列举非兼容性重大的变更(可选):\n',
       footer: '列举出所有变更的 ISSUES CLOSED(可选)。例如: #31, #34:\n',
       confirmCommit: '确认提交?'
  },

   // 设置只有 type 选择了 feat 或 fix,才询问 breaking message
   allowBreakingChanges: ['feat', 'fix'],

   // 跳过要询问的步骤
   skipQuestions: ['body', 'footer'],

   // subject 限制长度
   subjectLimit: 100,
   breaklineChar: '|', // 支持 body 和 footer
   // footerPrefix : 'ISSUES CLOSED:'
   // askForBreakingChangeFirst : true,
}

CommitLint

尽管加入了 Commitizen 降低了规范提交的难度,但总是架不住有人我行我素。CommitLint 可以限制只让符合 Angular 规范的 commit 通过。

单元测试

选择了目前流行的 Jest 作为测试框架。目前还没有实践,打算之后补上。不得不说,看了网上非常多的文章讲单测只是单纯讲 api ,我觉得意义不大,就算学会了那些也只是有术无道。单测真正的难点在于到底在于测试点的选择与颗粒度的把握,分享一篇前两天看的文章:掘金 - 《前端单测,为什么不要测 “实现细节”?》。该文的作者主张多关注prosp以及render出来的内容。

遇到的问题

  1. Pinia 在某位同事电脑上报错

  2. 同样在某位同事电脑上热部署相当慢,要10s左右的时间,而我自己基本上是秒更新。

目前猜测这两个问题都是由版本号问题导致的,准备忙完手上的项目就去好好找找原因。

最后

这篇文章早在一个月前就想写了,但是一来最近总是加班(周末又摸鱼打游戏),二是觉得搭好的环境总得上项目实际用一下才行。模板我已经放在仓库 ivestszheng/vue2-typescript-starter,感兴趣的小伙伴可以查看,也希望能给我提出一些改进的建议。

参考

  1. 掘金 - 从 0 开始手把手带你搭建一套规范的 Vue3.x 项目工程环境