背景
在项目开发中,为了提升开发效率,往往引用第三方组件库,例如element-ui、Vant、AntDesign;或者一些hooks库,例如vueuse、ahook。
随着系统复杂度不断增加,开源库没法满足一些特殊场景,系统会产生自定义工具库。当只有一个系统时,工具库存放在项目文件夹管理是没问题的,但多个项目需共享该代码,这种管理模式就会导致代码被复制多份,当出现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打包
为了适应不同的运行环境,工具库往往提供不同的模块管理方式,最常用的两种是esm
和cjs
,也就是ESModule
和commonjs
。
我们可以先简单的理解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
,新增module
和typings
配置
{
"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;
}
再执行用例,用例通过即完成,因为有用例的保护,你的代码也不担心别人修改了
二、发布工具库
工具库可以发布到公有npm或者自己搭建私有npm,如果企业内部使用,建议搭建私有npm,搭建方法也是比较简单的,下面介绍两种方法的发布。
2.1 发布公有npm
首先要在npm官方注册账号,接着在控制台执行npm login
命令输入账号密码登录,邮箱地址随便填就可以。
执行npm publish
发布代码,发布成功会输入如下日志,并且可在官网查看
现在你可以执行安装命令,把工具库安装到其他项目使用了
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-plugin
、eslint-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
虽然已经已经有了错误提示,但手动修复问题会比较麻烦,可设置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-staged
,husky
作用是监听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仓库,就会触发脚本,当出现下面截图表示成功
另外,可以在package.json
的script增加"prepare": "husky install"
,他的作用是当用户安装依赖完毕后执行prepare
定义的命令,自动初始化husky
。
3.4 规范提交日志
git提交日志规范比较常用的是angular规范,它规定把提交内容划分不同种类,例如新功能需要关键字feat、bug修复用fix,具体的规范可参考官方文档,下图是vue-next仓库的提交日志,他也是遵守angular提交规范的。
我们同样可以使用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'
配置完毕后,可以试着提交代码到仓库,因为没按规范提交,就会提示下图错误
修改提交日志后再提交即可成功
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
就会出现下面互动对话,根据需求填写即可。
发布后,github会产生release,而且更新日志根据提交历史自动生成
三、总结
我们发现实现一个企业级应用和demo差距是非常大的,企业级应用需要不断增加工程化和自动化工具解决重复劳动,规范编写风格等问题。
在下一期我还会带大家设计企业级组件库,把multirepo改造为monorepo,如果这系列课程对你有帮助,别忘了点赞留言哦。