手把手系列:打造企业级vue-hooks

2,017 阅读14分钟

背景

在项目开发中,为了提升开发效率,往往引用第三方组件库,例如element-ui、Vant、AntDesign;或者一些hooks库,例如vueuseahook

随着系统复杂度不断增加,开源库没法满足一些特殊场景,系统会产生自定义工具库。当只有一个系统时,工具库存放在项目文件夹管理是没问题的,但多个项目需共享该代码,这种管理模式就会导致代码被复制多份,当出现bug时会因为代码同步问题导致bug处理不彻底。

为了实现多项目共享代码,可以把公共代码独立提取到一个项目通过npm的形式管理。但作为一个工具库,我们希望可以支持TS自动单元测试支持ESM、commonjs打包文档化,另外还要规范编码风格自动生成版本日志

由于以前我们更多的使用vue-cli之类的脚手架生成项目,项目默认具备工程化特性,但如果不用cli,从头一步步搭建一个完整项目,到底需要什么步骤,下面将一步步给大家讲解。

一、初始化与构建脚本

1.1 创建目录并初始化项目

mkdir xboss-hooks && cd xboss-hooks && npm init -y

命令表示创建xboss-hooks目录并通过npm init -y 初始化项目,其中-y参数表示跳过询问模式使用默认配置。

创建成功后目录下会有一个package.json文件,内容如下。

// 不要复制下面内容,因为JSON文件不允许注释,你复制我的文件会出错的。
{
  "name": "xboss-hooks", // 项目名
  "version": "1.0.0",    // 版本号
  "description": "",     // 项目描述
  "main": "index.js",    // 项目入口文件
  "scripts": {           // 可执行脚本,例如npm run test就是执行下面的test命令
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],        // 关键字,如果发布到npm,可用于搜索
  "author": "",          // 作者
  "license": "ISC"       // 开源协议
}

1.2 TS支持

由于工具库需要较高的稳定性,建议基于typescript编写,这样也可以自动生成类型文件,对于使用TS的项目有更好的代码提示。

需要支持TS,核心是依赖typescript这个库,然后通过tsc命令对代码编译。

# 安装依赖
npm install typescript -D

package.json增加构建和开发脚本

  "scripts": {
    "build": "tsc src/* --outDir dist", // src/* ts源文件 --outDir 生成文件目录
    "dev": "tsc src/* --outDir dist -W"  // -W 表示监听文件变化,自动编译
  },

运行npm run build可在dist目录看到生成的文件,如果希望保存文件自动编译,那就执行npm run dev

创建src/index.ts文件,可以测试编译是否成功

export const getUser = (name: string) => {
    return name;
}

console.log(getUser('tom'))

编译成功会生成dist/index.js

"use strict";
exports.__esModule = true;
exports.getUser = void 0;
var getUser = function (name) {
    return name;
};
exports.getUser = getUser;
console.log((0, exports.getUser)('tom'));

刚才执行命令只是把ts文件转换为js,但并不会执行文件,可以用node执行

node dist/index.js

如果每次都手动运行文件,会过于繁琐,可以使用nodemon监听文件自动重新执行,这样你只要修改src/index.ts就可以在控制台看到运行结果。

# 全局安装依赖
npm install -g nodemon
# 启动nodemon监听文件dist/index.js文件
nodemon dist/index.js

注意:记得在另外一个终端保持npm run dev命令监听源码实时编译。

1.3 生成类型文件

现在dist文件夹只有js文件,没有.d.ts的类型描述,描述文件主要给基于TS项目的使用者友好的类型提示。

我们只要在编译时加入 -d参数,更多参数可参考官方文档

修改后的package.json如下

  "scripts": {
    "build": "tsc src/* -d --outDir dist", // 增加 -d 参数生成类型描述
    "dev": "tsc src/* --outDir dist -W"
  },

再次执行npm run build后,在dist文件夹看到一个index.d.ts文件

另外,大多项目会把类型描述文件集中到一个文件夹管理,可以增加参数 --declarationDir dist/types 把描述文件统一生成到dist/types目录,现在你的构建脚本如下。

  "scripts": {
    "build": "tsc src/* -d --declarationDir dist/types --outDir dist",
    "dev": "tsc src/* --outDir dist -W"
  },

1.4 基于esm、cjs打包

为了适应不同的运行环境,工具库往往提供不同的模块管理方式,最常用的两种是esmcjs,也就是ESModulecommonjs

我们可以先简单的理解esm提供浏览器使用,cjs提供nodejs环境使用。关于模块化可阅读 《前端模块化——彻底搞懂AMD、CMD、ESM和CommonJS》

tsc也提供了参数编译成不同的模式,只要加入-m commonjs即可编译为cjs,加入-m es2015编辑为esm,接着把package.json更新如下

  "scripts": {
    "build:esm": "tsc src/* -d --declarationDir dist/types -m es2015 --outDir dist/esm",
    "build:cjs": "tsc src/* -d --declarationDir dist/types -m commonjs --outDir dist/cjs",
    "dev": "tsc src/* --outDir dist -W"
  },

构建完毕后还要调整配置,否则发布到npm会导致找不到资源,在package.json修改main,新增moduletypings配置

{
  "main": "dist/cjs/index.js",
  "module": "dist/esm/index.js",
  "typings": "dist/types/index.d.ts",
}

1.5 rollup优化构建流程

上面的步骤虽然完成TS => JS的构建,但是我们还希望让构建流程更智能,例如在构建前自动删除dist文件夹,把未使用的代码不要打包到发布包里,代码压缩等。

平时我们接触比较多的可能是webpack,但是webpack更适合打包网站应用,对于组件库、工具库建议用rollup

这是rollup官方描述:

Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码,例如 library 或应用程序。

Rollup 对代码模块使用新的标准化格式,这些标准都包含在 JavaScript 的 ES6 版本中,而不是以前的特殊解决方案,如 CommonJS 和 AMD。ES6 模块可以使你自由、无缝地使用你最喜爱的 library 中那些最有用独立函数,而你的项目不必携带其他未使用的代码。

下面开始基于rollup改造之前的构建流程。首先安装依赖, 其中rollup-plugin-delete用于清空文件夹,rollup-plugin-typescript2用于编译TS。

# 安装依赖
npm i rollup rollup-plugin-delete rollup-plugin-typescript2 -D

在项目根目录创建rollup.config.js,配置如下

import typescript from 'rollup-plugin-typescript2';
import del from 'rollup-plugin-delete';

export default {
  // 入口文件
  input: 'src/index.ts',
  // 分别输出commonjs和ESModule
  output: [
    {
      dir: 'dist/cjs/index.js',
      format: 'cjs'
    },
    {
      dir: 'dist/esm/index.js',
      format: 'esm'
    }
  ],
  // 使用del插件删除dist目录下的文件
  plugins: [
    del({ targets: 'dist' }),
    // 使用typescript插件编译文件,tsconfig参数可省略,默认读取根目录tsconfig.json
    // useTsconfigDeclarationDir 表示读取tsconfig的declarationDir配置,如果是false会和js文件同一级目录输出
    typescript({ tsconfig: './tsconfig.json', useTsconfigDeclarationDir: true })
  ]
};

配置中还需要读取tsconfig.json文件,这个文件用来配置TS的构建参数,我们这里只配置生成类型定义文件和存放路径,更多参数可阅读链接

{
    "compilerOptions": {
        "declaration": true,
        "declarationDir": "dist/types",
    },
}

最后,在package.json的运行脚本改为rollup

  "scripts": {
    "dev": "rollup -w -c rollup.config.js",
    "build": "rollup -c rollup.config.js"
  },

现在执行npm run build就可生成资源,而且生成的资源会自动tree shaking,也就是未使用的代码不打包进去。

1.6 开发一个hooks

准备工作都已经完成,接着就可以开发hooks了,下面演示开发一个切换状态功能的hooks,我基于TDD(测试驱动开发)形式开发,关于TDD这里不做扩展,有兴趣可阅读《测试驱动开发(TDD)总结——原理篇》

先安装执行自动化测试依赖工具

# 安装依赖
npm i ts-jest @types/jest -D
# 初始化配置
npx ts-jest config:init

package.json增加测试脚本

  "scripts": {
    "build": "jest && rollup -c rollup.config.js",
    "test": "jest"
  },

在编写实现前,先编写测试用例

// src/useToggle/__test__/index.spec.ts
import { useToggle } from '../index';

describe('useToggle', () => {
  test('should change state when execute toggle', () => {
    // 执行useToggle方法返回一个数组,数组第一位是一个响应值,toggle是一个方法
    const [state, toggle] = useToggle(false);
    // 期望state.value等于初始化值
    expect(state.value).toBe(false);
    // 执行toggle方法后
    toggle();
    // 期望状态自动取反
    expect(state.value).toBe(true);
  });
});

因为还没实现代码,执行npm run test所有用例会失败,然后根据用例要求实现逻辑,编写逻辑前先安装vue3依赖。

# 安装vue3 
npm i vue@next -D
// src/useToggle/index.ts
import { ref } from 'vue';

export function useToggle(defaultValue) {
  const state = ref(defaultValue);
  const toggle = () => {
    state.value = !state.value;
  };
  return [state, toggle] as const;
}

再执行用例,用例通过即完成,因为有用例的保护,你的代码也不担心别人修改了

image.png

二、发布工具库

工具库可以发布到公有npm或者自己搭建私有npm,如果企业内部使用,建议搭建私有npm,搭建方法也是比较简单的,下面介绍两种方法的发布。

2.1 发布公有npm

首先要在npm官方注册账号,接着在控制台执行npm login命令输入账号密码登录,邮箱地址随便填就可以。

image.png

执行npm publish发布代码,发布成功会输入如下日志,并且可在官网查看

image.png

现在你可以执行安装命令,把工具库安装到其他项目使用了

npm install xboos-hooks

注意:因为我已经占用了xboos-hooks这个包名,你如果和我用相同名字,会禁止提交。

2.2 搭建私有npm

私有npm可以选择verdaccio,根据官方教程只要两步

# 全局安装verdaccio
npm install -g verdaccio
# 安装后启动应用
verdaccio

启动后仓库默认地址为http://localhost:4873/ ,我们需要在项目添加.npmrc文件,并设置如下内容:

registry=http://localhost:4873

如果你现在执行npm publish命令会提示账户错误,因为私有npm也是需要注册的,注册方法是执行下面命令,输入账号密码和邮箱

npm adduser --registry http://localhost:4873/

再执行npm publish即可发布,可以访问http://localhost:4873/ 查看已经发布成功

如果要在其他项目安装该依赖,可以执行下面命令安装,registry参数告知npm去私有服务器获取依赖包

npm install xboss-hooks --registry="http://localhost:4873"

这种方式的弊端是别人使用你项目时直接执行npm i无法获取这个安装包,更好的方式是使用scope

首先把xboss-hooks项目的package.json name属性改为@xboss/hooks并重新发布,在使用的项目创建.npmrc文件并添加@xboss:registry=http://localhost:4873,就可以直接执行npm i @xboss/hooks命令安装依赖

如果需要把已发布的包删除,执行npm unpublish @xboss/hooks -f

三、提升工程化

现在项目已经可以正常开发和发布,但在多人维护时,为了保证编码风格一致和防止他人破坏仓库稳定性,可增加风格检测、自动化单元测试、版本自动管理、持续集成等手段。

3.1 统一编码风格

代码风格通常用prettier实现自动格式化,prettier使用比较简单,安装依赖并在项目下创建.prettierrc.js文件定义规则,也可以使用json文件配置,但个人不建议,因为json没法添加注释

# 安装prettier
npm i prettier -D
// .prettierrc.js
module.exports = {
  printWidth: 80, //一行的字符数,如果超过会进行换行,默认为80
  tabWidth: 2, //一个tab代表几个空格数,默认为80
  useTabs: false, //是否使用tab进行缩进,默认为false,表示用空格进行缩减
  singleQuote: true, //字符串是否使用单引号,默认为false,使用双引号
  semi: true, //行位是否使用分号,默认为true
  trailingComma: 'none', //是否使用尾逗号,有三个可选值"<none|es5|all>"
  bracketSpacing: true //对象大括号直接是否有空格,默认为true,效果:{ foo: bar }
}

接着可以在项目根目录创建.vscode/settings.json文件,这样配置只在当前工作空间生效,它的好处是别人使用项目时,可保持配置一致,当然你也可以在用户空间配置。

下面是配置内容,它声明ts、js、json文件使用prettire规则格式化,editor.formatOnSave表示保存代码自动格式化。

{
    "editor.formatOnSave": true,
    "[typescript]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      },
      "[javascript]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      },
      "[json]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      },
}

另外,还需要给vscode安装prettier插件,安装后编写代码试一下是否可自动格式化,如果没效果可以重启vscode。

3.2 eslint与prettier整合

prettire只实现了代码风格的统一,但对于变量未被使用,常量未用const关键字声明等最佳实践没有提醒,这就需要eslint帮助我们,由于项目是基于TS编写,我们需要安装eslint@typescript-eslint/parser@typescript-eslint/eslint-plugineslint-plugin-import

npm i eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-import -D

接着创建.eslintrc.js配置检查规则

module.exports = {
  parser: '@typescript-eslint/parser',
  plugins: ['@typescript-eslint'],
  extends: ['plugin:@typescript-eslint/recommended']
};

除了对TS语法检测,还可以增加airbnb-base规则检查es语法,首先安装依赖,接着在extends增加配置,其中eslint-config-前缀可省略

# 安装airbnb的lint规则
npm i eslint-config-airbnb-base -D
// .eslintrc.js
module.exports = {
  parser: '@typescript-eslint/parser',
  plugins: ['@typescript-eslint'],
  extends: ['airbnb-base', 'plugin:@typescript-eslint/recommended']
};

回到代码,出现错误提示即表示配置成功,如果没有提示,需重启vscode

image.png

虽然已经已经有了错误提示,但手动修复问题会比较麻烦,可设置vscode保存自动修复

// .vscode/setting.json增加配置
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },

解决配置冲突

现在eslint已经配置完毕,但会出现eslint和prettier规则冲突,例如你把.prettierrc.js的semi属性改为false,表示代码末尾不添加分好,但eslint默认规则是需要分号的,这时候你去代码按保存键,就会出现prettier清空分号,eslint报警告。

为了解决这个问题,官方也提供了解决方案,优先使用prettier格式代码,需依赖eslint-config-prettier然后在extends增加prettier。 具体可阅读Integrating with Linters

# 安装依赖
npm i eslint-config-prettier -D
// .eslintrc.js
module.exports = {
  parser: '@typescript-eslint/parser',
  plugins: ['@typescript-eslint'],
  // extends有顺序关系,规则按从左到右应用,假设把prettier放最左边,他的规则会被后面规则覆盖
  extends: ['airbnb-base', 'plugin:@typescript-eslint/recommended', 'prettier']
};

3.3 代码提交前的验证

虽然我们前面已经配置了代码风格处理工具,但我们也没法确保所有开发人员都遵循规范开发,为了确保代码仓库的规范性,可以在代码进入仓库前检查代码风格,不合格的代码不准提交入库。

要实现这一特性,可以利用husky哈士奇和lint-stagedhusky作用是监听git代码提交,当发现代码提交,触发检测任务。但是整个代码库扫描会浪费资源,所以需要lint-staged只扫描暂存区的文件。下面是安装和配置流程。

#安装依赖 husky lint-staged
npm i husky lint-staged -D
#初始化husky
npx husky install
#增加钩子,提交代码自动执行 npx --no-install lint-staged
npx husky add .husky/pre-commit "npx --no-install lint-staged"

执行上面命令后会在项目根目录出现一个.husky文件夹,接着还需要在package.json增加lint-staged配置,声明匹配文件分别用prettier和eslint格式化

  "lint-staged": {
    "*.{ts,tsx,js,vue,less}": "prettier --write",
    "*.{ts,tsx,js,vue}": "eslint --fix"
  },

现在可以试着修改源码,然后提交到git仓库,就会触发脚本,当出现下面截图表示成功

image.png

另外,可以在package.json的script增加"prepare": "husky install",他的作用是当用户安装依赖完毕后执行prepare定义的命令,自动初始化husky

3.4 规范提交日志

git提交日志规范比较常用的是angular规范,它规定把提交内容划分不同种类,例如新功能需要关键字feat、bug修复用fix,具体的规范可参考官方文档,下图是vue-next仓库的提交日志,他也是遵守angular提交规范的。

image.png

我们同样可以使用husky增加提交hook,当提交代码时检测提交日志是否符合规范,日志规范检测需要用到@commitlint/cli@commitlint/config-conventional

# 安装@commitlint/cli
npm i @commitlint/cli @commitlint/config-conventional -D
# 命令行创建配置文件
echo "module.exports = { extends: ['@commitlint/config-conventional'] };" > commitlint.config.js
# husky添加提交hook
npx husky add .husky/commit-msg 'npx --no-install commitlint --edit $1'

配置完毕后,可以试着提交代码到仓库,因为没按规范提交,就会提示下图错误

image.png

修改提交日志后再提交即可成功

image.png

3.5 版本管理

开发完毕后,我们就可以发布出去了,在发布前我们还要修改版本号、编写更新说明、打tag、生成release包、推送npm。

由于工具库的更新迭代是比较频繁的,如果上面的操作都手动操作,不但会耗费大量时间,而且有可能因不同人发布导致发布标准不同,为了解决这个问题可以用release-it工具库实现发布自动化。

#安装relase-it
npm i release-it -D

在package.json增加执行脚本

  "scripts": {
    "release": "release-it"
  },

在执行命令前,需确保你仓库已经绑定远程仓库和本地代码已经产生提交历史,另外由于release-it会自动修改版本并产生一次提交,但是我们之前用commitlint限制了提交格式,所以要在项目根目录创建.release-it.json文件,配置如下内容

// .release-it.json
{
  "git": {
    "commitMessage": "chore: release v${version}"
  },
  "github": {
    "release": true
  },
  "hooks": {
    "before:init": ["npm run build"]
  }
}

配置完毕后,执行npm run release就会出现下面互动对话,根据需求填写即可。

image.png

发布后,github会产生release,而且更新日志根据提交历史自动生成

image.png

三、总结

我们发现实现一个企业级应用和demo差距是非常大的,企业级应用需要不断增加工程化和自动化工具解决重复劳动,规范编写风格等问题。

在下一期我还会带大家设计企业级组件库,把multirepo改造为monorepo,如果这系列课程对你有帮助,别忘了点赞留言哦。

3.1 项目仓库地址

github.com/zhengguoron…

3.2 参考资料

Prettier vs. Linters

Integrating with Linters

Getting Started - Linting your TypeScript Codebase

Contributing to Angular

commitlit-guides-local-setup

release-it#readme

前端模块化——彻底搞懂AMD、CMD、ESM和CommonJS