技术一直在变化,我们的流程和做法也需要跟上这些变化。因此,虽然npm已经有12年的历史了,但围绕 npm 包创建的做法应该更现代。

在这节课中,我们使用现代最佳实践(截至2022年)一步一步地创建一个npm包。首先学习如何创建一个npm包,这样你就可以熟悉构建和发布一个包到 npm 注册表。

然后,再学习如何通过建立测试框架、持续集成和部署管道、安全检查以及发布的自动语义版本管理,来制作一个更健壮、可用于生产的npm包。

简单的npm包示例

我们先通过一个简单的例子来熟悉创建和发布npm包的过程。

创建项目
  1. 创建一个 GitHub 仓库: https://github.com/new
  2. 克隆本地的 repo。例如:git clone https://github.com/snyk-labs/simple-npm-package.git
  3. 打开你的终端,进入到克隆的项目文件夹。例如:cd simple-npm-package
  4. 运行 npm init -y 来创建 package.json 文件。注意:如果克隆了示例仓库,就不需要做这一步。
  5. 在package.json 取一个名称,对应 name 字段
  6. 为该包编写你的代码
创建 npm 账户

为了能够让我们的 npm 包供他人使用,需要一个npm账户。

> npm login npm notice Log in on https://registry.npmjs.org/ Username: clarkio Password: Email: (this IS public) <email address> npm notice Please use the one-time password (OTP) from your authenticator application Enter one-time password from our authenticator app: <OTP> Logged in as clarkio on https://registry.npmjs.org/.

如何发布 npm 包

一旦你有了一个npm项目和一个npm账户,你就可以把你的npm包发布到公开的官方npmjs注册表上,让其他人可以使用。以下是你要遵循的步骤,在执行之前检查将发布的内容,然后运行实际的发布过程。

  1. 在终端,运行 npx npm-packlist 来查看将被包含在发布版本的软件包中的内容。

这可以确保我们没有遗漏任何源代码文件,这些文件是软件包正常运行所需要的。这也是一个好的做法,以确保我们不会意外地将敏感信息泄露给公众,如带有数据库凭证或API密钥的本地配置文件。

> npx npm-packlist LICENSE index.js package.json README.md

在终端,运行npm publish --dry-run,看看实际运行命令时将会做什么。

> npm publish --dry-run npm notice npm notice @clarkio/simple-npm-package@0.0.1 npm notice === Tarball Contents === npm notice 1.1kB LICENSE npm notice 1.2kB README.md npm notice 95B index.js npm notice 690B package.json npm notice === Tarball Details=== npm notice name: @clarkio/simple-npm-package npm notice version: 0.0.1 npm notice filename:@clarkio/simple-npm-package-0.0.1.tgz npm notice package size:1.7 kB npm notice unpacked size: 3.1 kB npm notice shasum:40ede3ed630fa8857c0c9b8d4c81664374aa811c npm notice integrity:sha512-QZCyWZTspkcUXL... ]L60ZKBOOBRLTg== npm notice total files:4 npm notice @clarkio/simple-npm-package@0.0.1

  1. 在终端,运行 npm publish --access=public 来发布软件包到npm。

注意:**--access=public**对于作用哉内的包(@clarkio/modern-npm-package)是需要的,因为它们默认是私有的。如果它不是作用哉内的,并且在你的 package.json 中没有将private 字段设置为 true,它也将是公开的。

> npm publish --access=public npm notice npm notice @clarkio/simple-npm-package@0.0.1 npm notice === Tarball Contents === npm notice 1.1kB LICENSE npm notice 1.2kB README.md npm notice 95B index.js npm notice 690B package.json npm notice === Tarball Details=== npm notice name: @clarkio/simple-npm-package npm notice version: 0.0.1 npm notice filename:@clarkio/simple-npm-package-0.0.1.tgz npm notice package size:2.1 kB npm notice unpacked size: 4.1 kB npm notice shasum:6f335d6254ebb77a5a24ee729650052a69994594 npm notice integrity:sha512-VZ1K1eMFOKeJW[...]7ZjKFVAxLcpdQ== npm notice total files:4 npm notice This operation requires a one-time password. Enter OTP: <OTP> @clarkio/simple-npm-package@0.0.1

现在,我们已经完成了构建和部署自己的npm包。接下来,我们来看一下如何制作一个更强大的包,为生产环境做好准备,并得到更广泛的使用。

生产就绪的npm包

虽然前面的例子的包可以在生产中使用,但它涉及到人工成本来保持其长期的维护。使用工具和自动化以及适当的测试和安全检查将有助于最大限度地减少保持软件包顺利运行的总工作量。让我们深入了解一下这其中的内容。

  1. 构建CommonJS(CJS)和ECMAScript(ESM)模块
  2. 设置和编写单元测试
  3. 实施安全检查
  4. 实现版本管理和发布的自动化
构建 CommonJS(CJS)和ECMAScript(ESM)模块

虽然ECMAScript模块格式现在在Node.js的12 版本中被原生支持,但它还没有被社区广泛采用。为了面向未来并支持这两种格式,我们来看下使用 TypeScript怎么来配置。

首先,创建一个基本的 TypeScript 配置文件 tsconfig.base.json。这是通用的编译设置,无论你的目标是哪种模块格式,都可以使用。

{ "compilerOptions": { "strict": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "skipLibCheck": true, "checkJs": true, "allowJs": true, "declaration": true, "declarationMap": true, "allowSyntheticDefaultImports": true }, "files": ["../src/index.ts"] }

然后为 CommonJS 格式创建一个TypeScript配置文件,命名为tsconfig.cjs.json。

{ "extends": "./tsconfig.base.json", "compilerOptions": { "lib": ["ES6", "DOM"], "target": "ES6", "module": "CommonJS", "moduleResolution": "Node", "outDir": "../lib/cjs", "declarationDir": "../lib/cjs/types" } }

之后,为 ECMAScript 格式创建一个TypeScript配置文件,命名为tsconfig.esm.json。这里的属性与你在 CommonJS 配置中看到的相同,但现在针对现代ECMAScript模块格式作为其输出。

{ "extends": "./tsconfig.base.json", "compilerOptions": { "lib": ["ES2022", "DOM"], "target": "ES2022", "module": "ESNext", "moduleResolution": "NodeNext", "outDir": "../lib/ESM", "declarationDir": "../lib/esm/types" } }

更新 package.json 文件,增加一个 files 字段,指向lib文件夹,里面有 TypeScript为你构建软件包的结果。

更新 package.json 文件中的 exports 字段,以定义如何根据使用的模块加载器(CJS vs. ESM)查找源文件。

"exports": { ".": { "import": { "types": "./lib/esm/types/index.d.ts", "default": "./lib/esm/index.mjs" }, "require": { "types": "./lib/cjs/types/index.d.ts", "default": "./lib/cjs/index.js" } } },

更新 package.json 文件的 main和 types 字段,以指向软件包的CJS版本。这将作为一个默认的、后备的选项。

“types": "./lib/cjs/types/index.d.ts", "main": "./lib/cjs/index.js",

在 package.json 文件中添加一个 files 字段,以表明当 npm 打包你的代码进行发布时,应该包括哪些文件。

"files": [ "lib/**/*" ],

通过 package.json 中的 scripts 字段创建命令,使用 tsc 并编译包的 CJS 和 ESM 格式,并生成 lib 文件。

clean 命令是用来删除过去构建的输出,并从一个干净的地方开始。

build:esm命令末尾的 mv lib/esm/index.js lib/esm/index.mjs 重命名了文件扩展名,这样Node.js模块加载器就知道它是一个ESM模块。

prepack命令是npm在打包npm包准备发布到注册表之前使用的。

"clean": "rm -rf ./lib", "build": "npm run clean && npm run build:esm && npm run build:cjs", "build:esm": "tsc -p ./configs/tsconfig.esm.json && mv lib/esm/index.js lib/esm/index.mjs", "build:cjs": "tsc -p ./configs/tsconfig.cjs.json", "prepack": "npm run build"

现在可以在终端运行 npm run build,让TypeScript构建你的项目,为使用和发布做准备

这就是使用 TypeScript 构建 npm 包所需要做的所有设置,它同时支持 CommonJS 和ECMAScript模块格式。

设置和添加测试

为了对代码的行为和结果有信心,我们需要有一个测试过程。测试迫使在第一次创建代码时,在happy-path 之外,以不同的方式思考代码的功能。举个例子,可以想办法打破一个函数,使它抛出一个错误或产生一个非预期的结果。这样做将使你的应用程序更有弹性和可持续性,并确保在添加更多内容时不会出现问题。

单元测试

要确保库以我们想要的方式运行,需要针对代码编写测试。我们需要一些工具来帮助设置我们项目来运行单元测试并显示结果。

这些工具有 Mocha.js、Chai.js和 ts-node。Mocha.js 是一个测试运行器,Chai.js是一个断言库,帮助确定你是否从你的代码中得到你所期望的结果,而 ts-node 帮助我们在TypeScript项目中使用这些工具。按照下面的步骤,为 npm包设置和运行测试。

{ "extension": ["ts"], "spec": "./**/*.spec.ts", "require": "ts-node/register" }

"scripts": { "clean": "rm -rf ./lib", "build": "npm run clean && npm run build:esm && npm run build:cjs", "build:esm": "tsc -p ./configs/tsconfig.esm.json && mv lib/esm/index.js lib/esm/index.mjs", "build:cjs": "tsc -p ./configs/tsconfig.cjs.json", "prepack": "npm run build", "test": "mocha" },

bc@mbp-snyk modern-npm-package % npm test > @clarkio/modern-npm-package@0.0.0-development test > mocha NPM Package ✔️ should be an object ✔️ should have a helloworld property Hello World Function ✔️ should be a function ✔️ should return the hello world message 4 passing (22ms)

管道中的测试

按照下面的步骤,创建一个测试工作流,作为项目管道的一部分。

  1. 为仓库创建一个新的GitHub Action :https://github.com/<your-account-or-organization>/<your-repo-name>/actions/new
  2. 将工作流程重命名为 test.yml
  3. 在工作流程文件中插入以下Snyk动作脚本:

name: Tests on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [12.x, 14.x, 16.x, 18.x] steps: - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: npm ci - run: npm test

这个YAML脚本检查出你的最新代码,安装其依赖性,并运行 npm test命令来执行测试。它对node-version字段中列出的每一个Node.js版本都会这样做,所以可以确保代码在每次运行时都能按预期工作。

现在已经完成了对项目的设置,以便对npm包的代码进行运行和评估测试。然而,你可能在想 "我如何在另一个项目中使用我的npm包进行测试?" 让我们来看看。

包测试

包上传完成后,除了单元测试外,我们还要测试在另一个项目引入我们包使用的情况,看看是否像我们所期望那样。这里有五种可以测试的方法:

  1. 通过 npm pack 输出安装
  2. 通过相对路径安装
  3. 通过npm链接安装
  4. 通过注册表安装(如npmjs.com的npm公共注册表)。
  5. 使用Verdaccio(一个开源的npm私有npm注册项目)来运行端到端的软件包发布和安装步骤,作为你CI的一部分。
npm pack

这种方法将利用npm pack命令将 npm 包打包并压缩成一个文件(<package-name>.tgz)。然后你可以到你想使用该包的项目中,通过这个文件安装它。这样做的步骤如下。

npm link

利用 npm link 命令来安装本地包:

  1. 在当前包目录中,在终端运行 npm link
  2. 改变目录到你想使用npm包的项目目录。例如:cd /path/to/project
  3. 在项目中运行 npm link <name-of-your-package>

这样在项目中就可以使用我们的包。

相对路径

这种类似于npm link。

与 npm link 的方法类似,这允许我们在项目中快速测试包的功能,但不会给你完整的类似生产的体验。这是因为它指向完整的软件包源代码目录,而不是你在npm注册表中找到的软件包的构建版本。

npm registry

这种方法利用了npm包的公共(或你自己)注册表。它涉及到发布的包,并像你通常对任何其他npm包那样进行安装。

实施安全检查

就像你不希望在自己的项目中出现安全漏洞一样,你也不希望在其他人的项目中引入漏洞。构建一个预计会在许多其他项目中使用的npm包,这就增加了确保事情安全的责任。你需要有安全检查,以帮助监测、提醒和提供帮助来减少漏洞。这就是像Snyk这样的工具可以简化完成这些需求所需的工作的地方。

对于这个例子中的npm包,你使用GitHub作为你的源码控制管理工具,所以利用它的GitHub Actions功能将Snyk整合到工作流程中。Snyk 有一个GitHub Actions参考项目,可以帮助启动这方面的工作,并为你的项目可能使用的其他编程语言和工具提供例子。

  1. Snyk是免费的,这里可以进行注册。
  2. 在GitHub上将你的Snyk API令牌添加为仓库秘密:https://github.com/<your-account-or-organization>/<your-repo-name>/settings/secrets/actions/new
  3. 仓库创建一个新的GitHub Action: https://github.com/<your-account-or-organization>/<your-repo-name>/actions/new
  4. 将workflow 重命名为 snyk.yml
  5. 在 workflow 文件中插入以下Snyk Action 脚本:

name: Snyk Security Check on: [push,pull_request] jobs: security: runs-on: ubuntu-latest steps: - uses: actions/checkout@main - name: Run Snyk to check for vulnerabilities uses: snyk/actions/node@master env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}

  1. 提交你的修改。
  2. 验证Action 成功运行: https://github.com/<your-account-or-organization>/<your-repo-name>/actions

有了这个设置,任何时候任何人推送到你的版本库或针对它打开一个拉动请求,都会进行安全检查,以确保它不会在软件包中引入任何漏洞。如果发现了问题,行动将失败,并提醒你发现的安全问题的细节。接下来,你将围绕版本管理和发布你的npm包进行自动化处理。

关于目前的设置,需要注意的一点是,它只利用了Snyk开源(SCA)产品,而不是Snyk代码(SAST)。Snyk Code是我们的代码安全产品,你需要首先通过你的Snyk账户启用它(免费),然后在这里添加到你的工作流程脚本中,以充分利用它。

实现版本管理和发布的自动化

每当在主分支中合并变化时,我们不想每次都手动更新npm包的版本并发布它。相反,会想让这个过程自动发生。如果你还记得本篇文章前面那个简单的npm包的例子,用以下命令来更新npm包的版本,然后发布它。

npm version <major|minor|patch> npm publish

什么是语义版本管理?

语义版本管理规定,版本要用三个占位符进行编号。第一个是主要版本,第二个是次要版本,而最后一个是补丁版本。

Semantic Release的工具可以与 GitHub Actions 整合来帮助我们自动修改版本并发布。实现这一过程自动化的关键是,你在向项目提交变更时使用所谓的常规提交。这使得自动化能够相应地更新一切,并知道如何为你准备项目的下一个版本。

  1. 运行:npm i -D semantic-release
  2. npx semantic-release-cli setup
  3. 按照终端的提示,提供所需的令牌

npm包实例教程(创建现代npm包的最佳实践)(1)

  1. 还需要一个来自npm的自动化类型的访问令牌,只在CI环境中使用,这样它就能绕过你的账户的2FA。要创建一个,请到https://www.npmjs.com/settings//tokens。请确保选择 "Automation"类型,因为这将用于CI/CD工作流程中。

npm包实例教程(创建现代npm包的最佳实践)(2)

bc@mbp-snyk modern-npm-package % npx semantic-release-cli setup ? What is your npm registry? https://registry.npmjs.org/ ? What is vour nom username? clarkio ? What is your pm password? [hidden] ? What is your NPM two-factor authentication code? <2FA code> Provide a GitHub Personal Access Token (create a token at https://github.com/settings/tokens/new?scopes=repo <token> ? What CI are you using? Github Actions bc@mbp-snyk modern-npm-package %

  1. 将npm令牌作为仓库秘密添加到GitHub仓库中:https://github.com/<your-name-or-organization/<your-repository>/settings/secrets/actions/new。将秘密的名称设置为NPM_TOKEN,其值是你在前面步骤中检索到的

npm包实例教程(创建现代npm包的最佳实践)(3)

  1. 回到项目中,进入package.json文件,像下面这样添加一个release键。如果你的版本库的主分支仍然叫master而不是main,那么就相应地更新上述分支的值。

"release": { "branches": ["main"] }

  1. 在 package.json 文件中也添加一个publishConfig键。

"publishConfig": { "access": "public" }

  1. 通过使用semantic-release npm脚本进行模拟运行来测试一切。采用以下命令,并将NPM_TOKEN=和GH_TOKEN=值设置为使用您各自的令牌值。然后在你的终端中复制并运行完整的命令,看看一切是否运行正常。你会看到进程被记录在终端的输出中。如果出现任何问题,它们会在这里显示出来,并提供解决这些问题的细节。
  2. 在确认试运行成功后,可以为GitHub仓库设置一个新的GitHub动作来为你处理发布过程。转到你在GitHub上的仓库,点击 "Actions"。
  3. 点击新建工作流程选项。
  4. 将工作流程重命名为release.yml。
  5. 在新的工作流程文件中加入以下YAML脚本。这个脚本主要是说,一旦Snyk安全检查工作成功完成,就运行发布工作。发布作业会检查代码,设置Node.js环境,安装你的依赖项,然后使用你的GitHub和npm令牌运行语义发布。

name: Release on: workflow_run: workflows: ['Snyk Security Check', 'Tests'] branches: [main] types: - completed jobs: release: name: Release runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 with: fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v2 with: node-version: 'lts/*' - name: Install dependencies run: npm ci - name: Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} run: npx semantic-release

13.提交你的本地修改并推送到你的GitHub仓库

  1. 在所有这些设置完成后,现在可以使用传统的提交方式将修改推送到你的主分支(或通过合并拉动请求),然后发布工作流就会运行(当然是在Snyk安全检查之后)。你可以在modern-npm-package版本库工作流程的例子中看到这种情况。
总结

我们总结一下在本文中学到的一切。首先,熟悉了设置、创建和部署一个简单的npm包。这对于熟悉首次发布自己的npm包来说是很好的。然而,如果想制作一个供生产使用的npm包,这样做是相当费力的,也是不可持续的。

为了完成制作一个可用于生产的包,随后学会了如何为CommonJS(CJS)和ECMAScript(ESM)模块格式进行构建,设置和编写单元测试,实现安全检查,并自动进行版本管理和发布。有了这些知识,现在已经准备好制作更多属于你自己的npm包了,这些包很容易被社区或你的公司所使用。

来源:https://snyk.io/blog/best-practices-create-modern-npm-package/

,