开发一个 npm 库应该做哪些工程配置?

3,126 阅读19分钟

版权声明:本人文章仅在掘金平台发布,请勿抄袭搬运,转载请注明作者及原文链接。

网页版带有主题和代码高亮,阅读体验更佳~

刚学习 VueReact 的时候,还只会使用 cli 创建项目。Vue 内置的 ESlint 工具最令人头疼,常常报错导致项目跑不起来,最后只能删除项目重新创建。上一家公司也是,都是水平很差的前端,项目根本不敢开 ESlint,没有人能解决 ESlint 的报错问题,导致项目代码里充斥着各种奇珍异兽。这些东西如果不熟悉,不理解,遇到问题很难去解决,而此类工程化相关的东西,我觉得是 35 年前端能和别人最快拉开差距的知识点。抛开 JS、框架,我们每天打交道最多的就是这些东西,一个优秀的前端项目工程,代码提交检查、代码格式规范是基本配置,在此基础上才能去谈 TSCI CD、单元测试等内容。

本篇文章就围绕这些工程化工具为大家做一次入门引领,从 01 搭建一个开源项目,从整体的视角来看待这些工具,从而更好地掌握工具,而不是排斥工具、害怕工具。

阅读本篇文章,你将学到以下工具的使用:

工具描述
ESlint代码格式校验
husky执行 git 钩子函数,一般用来检查 commit 信息
lint-stagedhusky 配套使用,只检查暂存区的代码
conventional-changelog-cli生成 CHANGELOG.md 文件,记录库的版本日志
LICENSE如何选择开源协议及如何快速生成开源协议
.editorconfig编辑器配置
bumpp自动升级版本而不用每次都手动改
package.json依赖配置文件,一些你在平时项目不会用也不会关注的字段
vite如何构建一个 npm 包

从开源项目中学习

随便打开一个开源项目的 github 仓库,就会发现除了核心代码之外,还有很多工程化相关的配置文件:

  • .husky
  • .eslintrc
  • .eslintignore
  • .editorconfig
  • .npmrc
  • .stylelint.js
  • .commitlint.config.js
  • README.md
  • LICENSE
  • CHANGELOG.md

除此之外,还有一个最关键的文件:package.json,这是最关键的一员,我们会在具体的知识点中来讲解这个文件。

ant-design 的部分工程配置文件

image.png

基于 vite 搭建项目

出于方便高效等各种原因选择 Vite,但是这不是最关键的内容,你也可以使用 Webpack。为了保持学习的一致性,建议你和我保持一样的操作。

使用 Vite,对 Node 版本有一定要求,你可以选择和我保持同样的版本:

image.png

Vite 需要 Node.js 版本 14.18+,16+。然而,有些模板需要依赖更高的 Node 版本才能正常运行,当你的包管理器发出警告时,请注意升级你的 Node 版本。

包管理工具我选择 yarn,你也可以选择 pnpm

在终端执行命令创建项目:yarn create vite,我们这里并不依赖任何框架,上下箭头移动选择 others,然后回车:

image.png

它又会问我们是不是 Electron 项目,当然也不是,选择第一个回车:

image.png

接下来它会询问我们选择一个预设,这里我们选择库模式:

image.png

它还会问我们是否需要 Typescript,我们这里需要:

image.png

Vite 为我们生成的项目模板:

image.png

可以看到它自带了一个 lib 文件,这其实就是我们最终生成的库的源码包,但是现在还只是个初级版,还不符合我们的需求。接下来我们需要做一些额外的配置。

编写 vite.config.js

首先明确我们需要支持的模块化,如果需要同时支持 esmcjs,那么我们可以生成两个最终产物,分别定义两种模块化的入口,如果你只支持 esm 或者 cjs 那么只生成一个最终产物就可以了,当然你还可以使用 UMD,一个最终产物同时支持以上两种模块化。这里我们以同时生成 esmcjs 两种包为例编写配置文件。

import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    target: 'modules', // 详情参考官方文档:https://cn.vitejs.dev/config/build-options.html#build-target
    minify: true, // 是否开启代码压缩
    rollupOptions: {
      input: ['src/index.ts'], // 打包的入口文件 https://cn.rollupjs.org/configuration-options/#input
      output: [ // 产物输出配置
        {
          format: 'es', // 指定模块化类型 https://cn.rollupjs.org/configuration-options/#output-format
          entryFileNames: '[name].js', // 入口文件名,默认 https://cn.rollupjs.org/configuration-options/#output-entryfilenames
          preserveModules: true, // 该选项将使用原始模块名作为文件名
          dir: 'es', // 输出的目录
          preserveModulesRoot: 'src' // 确保输出的目录和输入时的一致
        },
        {
          format: 'cjs', // 指定模块化类型
          entryFileNames: '[name].js',
          preserveModules: true,
          dir: 'lib',
          preserveModulesRoot: 'src'
        }
      ]
    },
    lib: { // https://cn.vitejs.dev/config/build-options.html#build-lib
      entry: './index.ts', // 定义作为库的入口是哪个
    }
  },
});

我们执行下 yarn build 来看看当前配置会为我们生成什么样的产物:

来看个小插曲,因为我们 Vite 生成的模板 src 下的入口默认是 main.ts,但是我们在配置文件写的是 index.ts,它找不到 index.ts 文件就报错了:

image.png

首先需要将 main.ts 改名为 index.ts,然后需要改写里面的代码,我们随意写个变量并导出,因为我们是库,如果不导出模块是会报错的。

image.png

来看看最终打包的产物:一个 es 一个 lib,这里有了这两个文件后,我们接下来可以做的事就多了。

image.png

针对 es 和 lib 做处理

第一件事,这两个包我不希望他们被提交到仓库里,我需要在 .gitignore 文件里忽略它们:

...

es
lib

...

第二件事,这两个包必须得在 package.json 里定义它的入口文件,否则发布成 npm 库后,别人下载下来找不到入口文件,也就没法引用代码了。

{
  "name": "vite-project",
  "private": false,
  "version": "0.0.0",
  "type": "module",
  "main": "lib/index.js",
  "module": "es/index.js",
  "types": "./index.d.ts",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build"
  },
  "files": [
    "es",
    "lib",
    "index.d.ts"
  ],
  "devDependencies": {
    "typescript": "^4.9.4",
    "vite": "^4.0.4"
  }
}

main 字段我们定义 cjs 模块的入口,module 我们定义 es 模块的入口。另外,private 设置为 falsefiles 是指我们最终发布 npm 时要上传的文件,现在我们需要将 eslib 两个文件加进来,这样我们发布代码时才能将代码发布上去,不然别人下载你的包会连代码都没有,这里一定小心,非常重要!

配置 ESlint

第一步,在终端执行命令:npx eslint --init,它会让我们选择模式:

  • 仅仅检查语法
  • 检查语法,发现问题
  • 检查语法,发现问题,强制代码风格

这里我们选择第二个。

image.png

接下来它会询问我们使用的模块化,我们选择 esm

image.png

它会问我们的项目是否基于某一个框架,由于我们这里没有,就不选,你可以根据你自己的情况选择:

image.png

接下来的就不是很重要了,我的选择给大家一个参考:

image.png

最终生成一个 .eslintrc.cjs 的规则配置文件,但是它会报红。因为我们是使用的 esm,我们把后缀 cjs 换成 js 就行了。不过这样其实也不太好,因为模块化可能会有各种潜在问题,所以我们直接将文件格式改成 json,也是一样的。这里顺嘴提一句,当项目存在多个 .eslintrc 配置文件且后缀不同时,优先级是 js > json > yaml,没有后缀的话默认是 json 格式。

{
  "env": {
    "browser": true,
    "es2021": true
  },
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended"
  ],
  "overrides": [
  ],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaVersion": "latest",
    "sourceType": "module"
  },
  "plugins": [
    "@typescript-eslint"
  ],
  "rules": {
    "semi": 2
  }
}

我们可以在 rules 字段下新增一个规则 "semi": 2,检查下 index.ts 中有没有报红,报红就说明配置生效了。其中,0 是关闭,1 是警告,2 是报错,还有其它的一些规则值,大家感兴趣可以自行研究,我们这里使用 012 已经够用了。

image.png

接下来简单讲讲配置中各个主配置字段的含义:

eslintrc 配置字段含义解读

字段描述
env在配置文件中使用 env 键指定环境,并通过将每个环境设置为 true 来启用想要的环境
globals要在配置文件中配置全局变量,请将 globals 配置属性设置成对象,其中包含为你要使用的每个全局变量命名的键。对于每个全局变量的键,将相应的值设置为 writable 以允许变量被覆盖,或者 readonly 以禁止覆盖。比如你可以把 window 禁用了,设置为只读
extendseslint 检查用哪些规范,包括内置的和第三方的
overrides覆盖配置中基于文件 glob 模式的设置 (你只要知道基本用不到就对了)
parser配置解析器
parserOptionsESLint 允许你指定你想要支持的 JavaScript 语言选项。默认情况下,ESLint 希望使用 ECMAScript 5 语法。你可以通过使用解析器选项来覆盖这一设置,以实现对其他 ECMAScript 版本以及 JSX 的支持。
plugins对于特殊语法需要使用插件进行识别,例如对 TSReact 语法的检查都依赖插件
rules具体的校验规则,比如有分号没分号,允不允许 console.log

到这里我们的 eslint 基本就配置完了,其实很简单对不对?但是我们看到 .eslintrc 的缩进是 4 个,我个人是不喜欢 4 缩进的,这里就引出了另一个配置文件:.editorconfig

不过在讲 .editorconfig 之前,还有一个文件和 ESlint 有关,那就是 .eslinignore。并不是所有文件都需要代码检查,ESlint 一般也只辅助我们检查 jsts 代码,所以有些文件我们得忽略掉。在根目录下创建文件:.eslintignore

image.png

一些构建后的产物没有检查的意义,所以有必要忽略掉。目前我们需要忽略的文件就是 node_modulestestdistbuildeslib。你若有其它的可忽略文件加进来即可。

.editorconfig

这个文件有什么意义?

.editorconfig 有助于为跨不同编辑器和 ide 从事同一项目的多个开发人员维护一致的编码风格。.editorconfig 项目由用于定义编码样式的文件格式和一组文本编辑器插件组成,这些插件使编辑器能够读取文件格式并遵循已定义的样式。.editorconfig 文件很容易阅读,并且可以很好地与版本控制系统配合使用。

假如你用 macOS 开发,我用 windows 开发,那么两个系统使用的编码格式不同,就会出现乱码,你一定遇到过下载的代码在换行时有一个 DR 符号的情况,这就是系统差异导致的。还有就是,有的人喜欢 4 个缩进,有的喜欢 2 个,缩进不同就会导致代码差异比较大,有些人还比较喜欢使用代码格式化工具,一保存就会自动格式化,导致代码一堆冲突,这时候解决起来就麻烦了。

.editorconfig 长什么样呢?一般是在项目的根目录下新建一个 .editorconfig,不带任何后缀即可,它会被编辑器自动解析。

贴一下我的配置:

# Editor configuration, see http://editorconfig.org
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
max_line-length = off
trim_trailing_whitespace = false

我这里将代码缩进设置为 2 个,刚才的 .eslintrc 立马有了反应,缩进线变了,可以进行 2 个空格的缩进了:

image.png

这就是 .editorconfig 的能力,可以控制编辑器的缩进、utf 编码、去除空白符等等。这对团队协作非常重要,有利于大家的项目保持统一的风格,避免一些非代码问题导致的问题降低开发效率。

husky & lint-staged 代码检查并修复

团队协作最重要的就是标准和规范,没有标准规范就会有各种各样的问题,例如代码格式混乱、commit 信息千奇百怪。标准规范制定虽然容易,执行落地却很困难,所以必须借助于工具。ESlint 虽然已经帮助我们避免大量的代码格式问题了,但是人总是会出错的,为了错误代码不被提交,特别是有些人使用未定义的变量,这样一定会导致代码报错,从而导致生产故障,这时就需要其它工具来进行辅助检查。

husky 加上 lint-staged 就是这种帮助代码检查的工具,配合 ESlint 一起使用。

暂存区代码 commit 前进行修复

第一步,执行 yarn add husky lint-staged -D,先下载依赖。

第二步,执行 npx husky-init && husky install。该命令会在 package.json 中新增一个 script"prepare": "husky install"

"scripts": {
  "dev": "vite",
  "build": "tsc && vite build",
  "prepare": "husky install"
},

紧接着会下载 .husky 文件,也就是 git hooks,这些钩子会在一定的时机执行。huskyhookspre-commitcommit-msgpre-push 等。同时,它会生成一个 pre-commitshell 脚本文件,负责执行 pre-commit 钩子。这个脚本文件就是在里面输入 shell 命令,husky 会在一定的 git 执行时机执行这个命令。你可以理解为类似于在终端执行的命令,只不过写在了这个文件里而已。

image.png

pre-commit 钩子是在 git commit 之前执行,我们现在要做的就是利用这个钩子在代码 commit 前修复一些错误。

第三步,package.json 新增 lint-staged 配置,这里的意思是在每次 commit 执行之前也就是 pre-commit 的时候执行 lint-staged 命令。它会匹配以 .js.ts 结尾的文件,并修复这些文件里的代码格式错误,并继续将修改后的文件存入暂存区,pre-commit 之后才会执行我们提交的 commit 命令。

"lint-staged": {
  "src/**/*.{js,ts}": [
    "eslint --fix",
    "git add"
  ]
},

第四步,我们需要将 pre-commit 文件里的 npm test 替换为 npx lint-staged,这样它就会在 pre-commit 钩子执行时调用第三步里的方法,对暂存区的代码进行扫描和修复。

image.png

我们来测试一下,src/index.ts 里我们之前留了几个分号的报错,现在我们将它进行 commit

image.png

可以看到分号的错误已经被修复。

对 commit 信息进行格式检查

如果你接触过一些正式的 commit 规范,会知道我们的 commit 信息一般是这样:feat: 搭建项目骨架。首先是此次变更的关键词,常见的有 featstyledocschore 等。具体看下表:

关键字具体含义
feat新增特性 (feature)
fix修复 bug(bug fix)
docs修改文档 (document)
style代码格式修改
refactor代码重构 (refactor)
perf改善性能 (performance)
test测试
build变更项目构建或外部依赖
chore杂项
revert代码回退

这样的信息我们也有工具进行规范。我相信有人看到这里会觉得这种东西似乎没有什么作用,有点多此一举了。仁者见仁智者见智吧,我觉得专业的前端团队这种东西应该都会做,毕竟工程化的东西,其它的都能做,这件小事也就是顺手的事情,有利无弊的事,做了总比没做好。

第一步,下载两个依赖:yarn add @commitlint/cli @commitlint/config-conventional -D

第二步,终端执行命令:npx husky add .husky/commit-msg "npx --no-install commitlint --edit $1",它会在 .husky 下生成一个 commit-msgshell 脚本文件,并且会在我们 commit 提交 message 时执行 npx --no-install commitlint --edit 命令,对我们的信息进行校验。

image.png

但是怎么个校验法它并不知道,所以需要我们在根目录下编写一个配置文件 commitlint.config.js

const types = ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'release', 'chore', 'revert'];

export default {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-empty': [2, 'never'],
    'type-enum': [2, 'always', types],
    'scope-case': [0, 'always'],
    'subject-empty': [2, 'never'],
    'subject-case': [0, 'never'],
    'header-max-length': [2, 'always', 88],
  },
};

以上配置仅供参考。来测试下:

image.png

一个小插曲,报错的信息说不支持 esm,需要导出 cjs 模块。那么我们需要将 commitlint.config.js 替换为 commitlint.config.cjs,并将其进行 module.exports

继续测试,可以看到配置已经生效了。

image.png

不过有时候不想这个配置生效,觉得这个校验很麻烦怎么办?就比如前同事留下的垃圾代码,每次 commit 触发校验一堆报错,老代码又不想动,新代码又必须提交,怎么办?其实加个参数就可以了:--no-verify。例如:git commit -m 'feat: 完成项目骨架' --no-verify,它就会跳过 commit 校验,包括代码格式校验及 commit message 校验。

生成 CHANGELOG.md 更新日志

打开一些开源项目的 github 仓库,发现人家对每次版本更新都有很详细的记录,有时候觉得人家真细致,不愧是开源作者,但其实人家也是借助的工具。

第一步,执行命令:yarn add conventional-changelog-cli -D

第二步,在 package.json 中配置脚本:"changelog": "conventional-changelog -p -i CHANGELOG.md -s"

我们来测试下:yarn changelog

image.png

就这样两步就能生成 CHANGELOG.md

自动升级版本号

每次发布新的版本都要手动更改 package.jsonversion 实在是麻烦,而且最麻烦的是会忘记更改。可以下载 yarn add bumpp -D,每次 release 之前都执行下 bumpp,它会提示我们对版本进行升级,还有各种合适的版本号推荐,例如 beta 版、major 版等。

package.json 中新增一个 script"version": "bumpp"。终端执行 yarn version 看看效果:

image.png

它会继续问我们是否需要更改到其它版本,我们这里不需要,直接选择 as-is 0.0.2

image.png

最后它会问我们是否需要继续,我们选择 N

image.png

主要的功能是防止忘记更改版本,可以将其放到每次发布之前执行。

快速生产开源协议

开源项目离不开开源协议,但是不可能手写开源协议吧?其实这个也不用我们自己操作,vscode 有插件帮助我们快捷生成。

image.png

快捷键 shift command (ctrl) p 可进行选择:

image.png

image.png

开源协议有很多,这里就不展开了,一般选择 MIT 就可以。下面列个常见开源协议的资料,有兴趣的话可以细看。

协议名称协议描述
BSD (Berkeley Software Distribution license)BSD 允许使用者修改和重新发布代码,也允许基于 BSD 代码上开发商业软件的发布和销售。遵循 BSD 协议的代码完全可控,必要的时候可以修改或者二次开发
MIT(Massachusetts Institute of Technology)MIT 是宽松的许可协议,作者只想保留版权,而无任何其它限制。只需在发布的源代码、二进制可执行文件相关文档中包含 MIT 许可协议声明,便可自由的使用、修改源代码、作为商业软件再发布、甚至使用开源机构名字做产品的市场推广
Apache Licence 2.0详见:开源协议详解
GPL(General Public License)代码的开源免费使用和引用,修改及衍生代码的开源免费使用,但其不允许修改后和衍生的代码做为闭源的商业软件发布和销售(只要使用 GPL 协议的相关类库与代码,则该软件亦必须采用GPL 协议,必须开源与免费)
LGPL(Lesser General Public License)LGPL 允许商业软件通过类库引用方式使用 LGPL 类库而不需要开源商业软件的代码,如果修改了 LGPL 协议的代码或衍生,则所有修改的代码和衍生的代码都必须采用 LGPL 协议
Mozilla(Mozilla Public License)在自己已有的源代码库上加一个接口,除了对接 Mozilla Public License 开源库的接口程序源代码以MPL许可的形式对外许可外,源代码中的其他源码可以不用 MPL 许可证的方式强制对外许可

完善 package.json 配置

配置完开源协议文件后,还需要在 package.json 里新增字段:"license": "MIT"

除此之外,还可以设置关键词,简要标记下库的功能和技术点:

"keywords": [
  "vite",
  "commitlint",
  "husky",
  "CHANGELOG",
  "LICENSE",
  "ESlint"
],

files 字段再把 READMELICENSE 带上,因为使用了 TSindex.d.ts 也是必须上传的。另外,上传哪个类型声明文件,是因为我们配置了字段:"types": "./index.d.ts",。这里配置的是谁,你在 files 字段里上传的类型声明文件就是谁。

"files": [
  "es",
  "lib",
  "README",
  "LICENSE",
  "index.d.ts"
],

还有 repositorypublishConfig,分别记录库的代码仓库及发布时 npm 的镜像地址。如果是开源的话就是 https://registry.npmjs.org/,不是开源的话可以设置成自己或者公司的私有镜像地址。

"repository": {
  "type": "git",
  "url": "https://github.com"
},
"publishConfig": {
  "registry": "https://registry.npmjs.org/"
}

贴一下完整的 package.json

{
  "name": "open-source-config-template",
  "private": false,
  "version": "0.0.1",
  "type": "module",
  "main": "lib/index.js",
  "module": "es/index.js",
  "types": "./index.d.ts",
  "license": "MIT",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "lint": "eslint . --ext '.js,.ts' --fix",
    "prepare": "husky install",
    "commit": "git-cz",
    "version": "bumpp",
    "changelog": "conventional-changelog -p -i CHANGELOG.md -s",
    "before-release": "yarn changelog && yarn build && bumpp"
  },
  "lint-staged": {
    "src/**/*.{js,ts}": [
      "eslint --fix",
      "git add"
    ]
  },
  "config": {
    "commitizen": {
      "path": "./node_modules/cz-conventional-changelog"
    }
  },
  "files": [
    "es",
    "lib",
    "README",
    "LICENSE",
    "index.d.ts"
  ],
  "keywords": [
    "vite",
    "commitlint",
    "husky",
    "CHANGELOG",
    "LICENSE",
    "ESlint"
  ],
  "devDependencies": {
    "@commitlint/cli": "^17.6.5",
    "@commitlint/config-conventional": "^17.6.5",
    "@typescript-eslint/eslint-plugin": "^5.59.8",
    "@typescript-eslint/parser": "^5.59.8",
    "bumpp": "^9.1.0",
    "commitizen": "^4.3.0",
    "conventional-changelog-cli": "^2.2.2",
    "eslint": "^8.42.0",
    "husky": "^8.0.0",
    "lint-staged": "^13.2.2",
    "typescript": "^4.9.4",
    "vite": "^4.0.4"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com"
  },
  "publishConfig": {
    "registry": "https://registry.npmjs.org/"
  }
}

写在最后

做到现在,我们来看下整个项目的文件目录:

image.png

基本上已经与一个开源项目的配置差不太多了,如果需要 stylelint 等其它配置,继续往里面添加即可。不过不是所有项目都会写 CSS,更细化的配置就是根据项目需要再添加了。

如果本篇文章对你有帮助,请点赞收藏关注三连一波,可以通过我的 vscode 截图看到我的 vscode 背景从亮变黑,从早写到晚写了整整一天,感谢 ~。

下面是本期文章配对的代码仓库:open-source-config-template

上面的地址失效了,不知道怎么没的,可以参考我另外两个库,都是类似的:

console-log-button

xwg-cli

往期推荐

分享我在前端学习与开发中用到的神仙网站和工具 40+ 👍🏻 110+

uniapp 踩坑记录(二) 130+ 👍🏻 150+

闲来无事,摸鱼时让 chatgpt 帮忙,写了一个 console 样式增强库并发布 npm 100+ 👍🏻 110+

uniapp 初体验踩坑记录 30+ 👍🏻 60+

两小时学会 JS 正则表达式,终身不忘 50+ 👍🏻

【一年前端必知必会】如何写出简洁清晰的代码 50+ 👍🏻

【一年前端必知必会】了解 Blob,ArrayBuffer,Base64 40+ 👍🏻 90+