背景
因为业务需求需要开发一个npm包,但没有一个npm开发规范,也没有一个开发npm的模板,看了已有的npm包。是一种百花齐放的状态,所以这里想完成一个npm开发套件,让开发者可以开箱即用。
目标
完成一个npm套件,让开发者开箱即用。那么这个套件应有包含以下功能:
- 统一的代码规范
- 统一的协作规范
- 零配置
- 自动化npm包创建【下篇实现】
- 一键发布发布
- 发布套件-cli 【下篇实现】
根据上面目标,这里给出了整个套件的架构
其实看到这里,你可能发现,npm和web或者其他应用其实类似,这里看完了你都了解了,配置其他类型的应用也很容易了。
认识package.json
虽然天天用这个,但是不是真的都完全了解了呢,不是吧!哈哈,这里有一篇很好的文章,还有这篇
这里列出一些,平常关注不多,但开发npm包又很重要的点。
main
这个属性是指定程序的主入口文件,如
// package.json
{
"main": "dist/index"
}
意思是,别人引入你的包,首先进入的是dist/index
这个文件
types
这个属性是指定你的声明文件入口, 这个主要针对TS项目
// package.json
{
"types": "typings/index.d.ts"
}
意思是,别人在ts项目里引入你的包,去你包下面的typings/index.d.ts
找声明文件。
files
这个属性是npm包发布时,指定发布哪些文件夹下面的所有内容
// package.json
{
"files": ["dist", "lib"]
}
意思是,别人在install
你的包后,将在你的包下看到这两个文件夹及他们下面的所有文件
publishConfig
这个属性是指定发布配置,一般是用于发布到内网的npm源。
// package.json
{
"publishConfig": {
"registry": "https://yourCompany.npmjs.org/"
}
}
指定发布到https://yourCompany.npmjs.org
这个npm源
依赖
依赖主要分为下面几类:
- dependencies - 业务依赖
- devDependencies - 开发依赖
- peerDependencies - 同伴依赖
- bundledDependencies / bundleDependencies - 打包依赖
- optionalDependencies - 可选依赖
这里主要将后面三个:
peerDependencies
在业务开发中,常常遇到这个场景,你的开发的包依赖包A
,你的宿主环境也依赖包A
,那就会带来一个问题,你们装的A版本不一致怎么办?
处理这个问题就是通过在peerDependencies
,中指定包A
,意思是我们开发的包用的包A
是宿主环境的包A
,这样就能解决版本不一致的问题。
bundledDependencies
指定打包的时候,把哪些包也打进包里
{
// package.json
"bundledDependencies": ["package1" , "package2"]
}
需要注意点是:在bundledDependencies中指定的依赖包,必须先在dependencies和devDependencies声明过,否则打包会报错。
optionalDependencies
这种依赖中的依赖项即使安装失败了,也不影响整个安装的过程。
需要注意的是,如果一个依赖同时出现在dependencies和optionalDependencies中,那么optionalDependencies会获得更高的优先级,可能造成一些预期之外的效果,所以尽量要避免这种情况发生。
包管理 - lerna
因为要开发一系列相关npm包, 随着项目深入,npm
包之间会产生多种依赖关系,版本以及多仓库的管理变得日益复杂。在参考多种开源项目管理案列后,且结合我们自身情况分析,Monorepos
理念是目前较理想的方案,其中lerna
是其在工具端实现中相对成熟的方案。所以选用lerna
,作为包管理工具。
我们选用lerna
作为npm
包的管理工具,其带来的优势有:
- 统一的lint,build,test和release流程
- 统一处理issue的地方
- 方便版本管理和依赖管理
- 方便生成总的CHANGELOG
- 方便跨项目操作和修改
由于采用Monorepos
,协作开发的问题又暴露了出来,为此我们需要制定一份项目管理指南,使项目组的成员能够拥有一致的行为模式。所以需要约束规范编码、发布流程。这一部分后面会详细讲到。
初始化项目
yarn add global lerna
mkdir <dir-name>
cd dir-name
lerna init
经过上面步骤,会在自定义目录里生成,以下目录和文件
packages/
lerna.json
package.json
安装依赖
对于新项目,安装包,独立模式下都需要加上 -W
lerna add husky # 给所有包都安装husky
lerna add husky -D --scope=@xxxx/package-name # 指定给 @xxxx/package-name 包安装husky
# 更多信息, 查看 lerna add -h
对于老项目
lerna bootstrap # 安装所有包的依赖
lerna bootstrap --scope @xxxx/package-name # 安装自定包下面的所有依赖
# 更多信息,查看 lerna bootstrap -h
由于包之间可能存在共同的包,不需要我们重复安装,我们可以这样
lerna bootstrap --hoist # 把公共包装在根目录下的node_modules,这样降低依赖安装、管理成本
还可以简化使用成本,修改配置文件
// lerna.json 这样 lerna bootstrap 等价于 lerna bootstrap --hoist
{
"command": {
"bootstrap": {
"hoist": true
}
}
}
统一代码规范
因为项目中可能用到TS 或 JS,所以 eslint 需要支持两种语法的lint检查。而这些检查是针对所有项目的,所以在根目录安装ESlint相关包
ESlint 检查JS
安装依赖
yarn add -D eslint
新增配置.eslintrc.js
, 完成eslint 对JS的语法检查
// .eslintrc.js
module.exports = {
extends: ['eslint:recommended'],
env: {
browser: true,
node: true,
},
parserOptions: {
ecmaVersion: 2019,
sourceType: 'module',
}
};
ESLint 检查TS
安装依赖
yarn add -D typescript @typescript-eslint/eslint-plugin @typescript-eslint/parser
配置.eslintrc.js
, 完成对eslint对TS的检查
// .eslintrc.js
module.exports = {
parser: '@typescript-eslint/parser',
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
plugins: ['@typescript-eslint'],
parserOptions: {
ecmaVersion: 2019,
sourceType: 'module',
},
env: {
browser: true,
node: true,
}
};
Prittier
安装依赖
yarn add -D prettier eslint-config-prettier eslint-plugin-prettier
修改.eslintrc.js
// .eslintrc.js
module.exports = {
parser: '@typescript-eslint/parser',
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
plugins: ['@typescript-eslint', 'prettier'],
parserOptions: {
ecmaVersion: 2019,
sourceType: 'module',
},
env: {
browser: true,
node: true,
}
};
需要注意的是:由于eslint和prettier 都会对代码的格式做校验。那么就会有冲突
- eslint-config-prettier 插件,可以让eslint使用prettier规则进行检查,并使用--fix选项。
- eslint-config-prettier 插件,之前说了eslint也会检查代码的格式,这个插件就是关闭所有不必要或可能跟prettier产生冲突的规则。
新增.prettierrc
{
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"overrides": [
{
"files": ".prettierrc",
"options": { "parser": "json" }
}
]
}
统一协作规范
对改动文件做ESLint检查
首先安装可以提供各种git钩子的husky
yarn add -D husky
注意默认你安装的应该是V6,husky配置发生了很大的改变,现在市面大都是V6以下的配置方式,所以你的husky配置不生效
npx husky install # 会在根目录生成 .husky文件夹,里面存放 githook 文件
安装lint-staged
, 顾名思义,他使用来对git add
后的代码做lint检查。
yarn add -D lint-staged
修改package.json
{
// other config
"lint-staged": {
"*.{js,ts}": [
"eslint --fix",
]
}
}
新增pre-commit hook
,对staged
代码做eslint
校验
npx husky add .husky/pre-commit "npx lint-staged"
Commit 信息规范化
这里要对每个人提交信息规范化,同时也方便后续自动化生成changelog
安装规范化commit的依赖
yarn add -D @commitlint/cli @commitlint/config-conventional
新建commitlint.config.js
文件。
module.exports = {
extends: ['@commitlint/config-conventional']
}
新增commit-msg hook
npx husky add .husky/commit-msg 'npx --no-install commitlint --edit "$1"'
自动生成changelog
前面我们规范化了我们的提交信息,这里我们希望利用我们的提交信息,在发布版本后,能自动生成changelog
。
安装依赖
yarn add -D commitizen cz-lerna-changelog
修改package.json
// package.json 新增config 配置
{
"config": {
"commitizen": {
"path": "./node_modules/cz-lerna-changelog"
}
}
}
至此我们完成了,所有代码规范和协作规范上的配置。我们可以正常开发npm包了。
打包工具 - rollup
我们一般都是用TS,或者ES6来开发npm包,但大部分应用都还是需要es5才能正常运行,所以我们需要一款打包工具。这里是为了从来没用过rollup,所以选择了rollup
看社区还有个基于rollup的、零配置的Bili,看起来更香
a word on Bili. Bili is just a nice, Rollup-based and zero-config (by default) bundler with built-in support for ES-Next and CSS
进入子包,安装依赖
cd packages/your-subpackage
yarn add -D bili rollup-plugin-typescript2
在子包根目录下新建配置文件,点击了解更多 Bili 官网
// config/bili.base.js
module.exports = {
input: 'src/index.ts',
output: {
moduleName: 'Package',
fileName: '[name][ext]',
},
plugins: {
typescript2: {
useTsconfigDeclarationDir: true,
},
// 这里是fix @rollup/plugin-replace 剔出的warning
replace: {
preventAssignment: true,
},
},
};
// config/bili.dev.js
import merge from 'rollup-merge-config';
import baseConfig from './bili.base';
module.exports = merge(baseConfig, {
output: {
// 这里输入dev 模式下配置
},
env: {
ENV: 'development',
},
});
// config/bili.prod.js
import merge from 'rollup-merge-config';
import baseConfig from './bili.base';
module.exports = merge(baseConfig, {
output: {
// 这里输入prod 模式下配置
minify: true,
sourceMap: false,
},
env: {
ENV: 'production',
},
});
开发生产模式的配置都在这了,是不是炒鸡简单。
修改子包的package.json
{
"scripts": {
// other npm scripts
"build": "rm -rf dist/ && bili --config config/bili.prod.js",
"start": "bili --watch --config config/bili.dev.js"
},
}
至此,所有项目配置完成。可以尽情的开发npm包了。
发布
配置发布脚本
由于bili
或者rollup
只是负责打包,如果我们需要发布npm包,需要做以下几件事:
- 将包目录下
package.json
拷贝到dist
- 将
changelog.md
和readme.md
拷贝到dist
ok,很容易,子包根目录新建build.sh
文件。
cp package.json dist/
cp CHANGELOG.md dist/
cp README.md dist/
# 如果npm是ts项目,则将typings拷到dist
tsconfig=$(cd `dirname $0`; pwd)"/tsconfig.json"
typingsDir=$(cd `dirname $0`; pwd)"/typings/"
if [ -f "$tsconfig" ] && [ -d "$typingsDir" ]; then
cp -r typings dist/
fi
这里typings
是啥?
这里存放npm自己的声明文件。
在根目录下增加命令行
// package.json
// 这里要做三件事:
// 1. 更新版本号
// 2. 执行子包下面的build.sh,【因为这里要是最终的版本号,所以要在lerna version 后】
// 3. 发布
{
"scripts": {
// other npm scripts
"prePublish": "npx lerna exec ./build.sh",
"pub:test": "npx lerna version prerelease --preid=beta --yes && npm run prePublish && npx lerna publish from-git --yes", // 发beta包
"pub:patch": "npx lerna version patch --yes && npm run prePublish && npx lerna publish from-git --yes", // 发 patch 包
"pub:minor": "npx lerna version minor --yes && npm run prePublish && npx lerna publish from-git --yes", // 发 minor 包
"pub:major": "npx lerna version major --yes && npm run prePublish && npx lerna publish from-git --yes" // 发 major 包
},
}
执行发布命令
npm run pub:test # 发beta
npm run pub:xxx # 发其他正式包
补充
lerna.json
这里给出lerna.json
完整代码
{
"packages": [
"packages/*"
],
"command": {
"bootstrap": {
"hoist": true
},
"version": {
"conventionalCommits": true,
"noCommitHooks": true
},
"publish": {
"conventionalCommits": true,
"message": "chore(release): publish %s"
}
},
"ignoreChanges": ["**/__tests__/**", "**/*.md"],
"version": "independent"
}
提取ts相同配置
在根目录配置一份tsconfig.base.json
{
"compilerOptions": {
"module": "esnext",
"lib": ["esnext", "dom"],
"strict": true,
"declaration": true,
"esModuleInterop": true,
"moduleResolution": "Node"
},
"files": []
}
在子包根目录配置子包的tsconfig
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"baseUrl": "./",
"declarationDir": "./typings"
},
"include": ["src/**/*"],
"exclude": [
"node_modules",
"**/__tests__/*",
"**/__e2e__/*"
],
}