创建现代 Npm 包的优秀实践
Best practices for creating a modern npm package
技术在日新月异的变化,你的开发流程、最佳实践同样需要更新迭代。npm
发展到如今已经12个年头,因此我们创建npm package
的流程应该更加现代化。
在本教程中,我们将逐步介绍如何使用现代的最佳实践流程来创建 npm
包。首先你学习如何创建 npm
包,这样你就可以熟悉构建包并将其发布到 npm registry
。然后,您将通过学习集成测试框架、CI
和CD
、安全检查和发布的自动语义版本管理来完成一个健壮的、生产环境下可用的npm
包。
Simple example npm package
现在就跟着我们一步步完成一个demo npm package
。
SET UP YOUR PROJECT
首先我们需要在github
上新建一个项目。
- 新建一个项目:
https://github.com/new
。 - 克隆你新建好的项目到本地,
https://github.com/NuoHui/simple-npm-package.git
。 - 打开终端,进入你的项目,
cd simple-npm-package
。 - 执行
npm init -y
初始化你的package.json
。 - 修改你的
package.json
中的项目名,带上自己的scope
,例如@snyk-labs/simple-npm-package
。 - 新建一个入口文件
index.js
,内容如下。
function helloWorld() {
console.log('Hello World from this npm package');
}
module.exports = helloWorld;
SET UP AN NPM ACCOUNT
为了能够让您的 npm
包可供其他人使用,您需要一个 npm
帐户。
- 通过 www.npmjs.com/signup 注册。
- 为了提高安全性,请在您的npm账户上启用2FA: docs.npmjs.com/configuring… 。
- 使用
npm login
命令在终端中用你的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/.
如果开启了2FA,需要进行二次验证。
HOW TO PUBLISH YOUR NPM PACKAGE
一旦你有了一个npm
项目和一个npm
账户,你就可以把你的npm
包发布到公开的官方npmjs
注册表上,让其他人可以使用。以下是你要遵循的步骤,在执行之前检查将发布的内容,然后运行实际的发布过程。
- 打开终端,运行
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
- 在终端,运行
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包。接下来,我们来看一下如何制作一个更强大的包,为生产环境做好准备,并得到更广泛的使用。
Production-ready npm package
虽然前面的示例包可能会用于生产环境,但是随着时间迭代它需要花费很多人工成本来维护。使用工具和自动化以及适当的测试和安全检查将有助于最大限度地减少保持软件包顺利运行的总工作量。下面我们继续了解这块内容。
以下内容包括:
- 创建设置你的
modern-npm-package
项目。 - 支持构建
CommonJS (CJS)
和ECMAScript (ESM)
模块格式。 - 设置和编写单元测试。
- 支持安全检查。
- 自动化版本管理和发布。
前面创建、初始化的项目流程一致,就不重复,重点我们看下其他不同的。
BUILDING FOR BOTH COMMONJS AND ECMASCRIPT MODULE FORMATS
通常我们开发一个npm包,需要支持ESM
、CJS
。ESM
是未来,目前在Nodejs 12+
版本以上已经原生支持。
- 首先,创建一个名为
tsconfig.base.json
的基础 TypeScript 配置文件。这是用于通用编译设置的,无论您的目标是哪种模块格式,都可以使用这些设置。
{
"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 属性向TypeScript指出要编译的项目代码的JavaScript版本。
"target": "ES6",
// module 属性向 TypeScript 指出在编译的项目代码时应该使用哪种JavaScript模块格式。
"module": "CommonJS",
"moduleResolution": "Node",
// outDir 和 declarationDir 属性向TypeScript指出了将编译的代码和定义其中使用的类型的结果放在哪里。
"outDir": "../lib/cjs",
"declarationDir": "../lib/cjs/types"
}
}
- 为 ECMAScript 格式创建一个TypeScript配置文件,命名为
tsconfig.esm.json
。
{
"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文件夹,以表明当 npm 打包你的代码进行发布时,应该包括哪些文件。
"files": [
"lib/**/*"
],
- 更新 package.json 文件中的
exports
字段,以定义如何根据使用的模块加载器(CJS vs. ESM)查找源文件。详情可以了解Node 最新 Module 导入导出规范-条件导出。
"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 文件的
scripts
,增加构建相关的脚本。
"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"
},
这就是使用 TypeScript 构建支持 CommonJS 和 ECMAScript 模块格式的 npm 包所需的所有设置。
SETTING UP AND ADDING TESTS
为了保证代码的质量、以及后续可维护性,我们需要单元测试。如果您想更深入地研究测试并了解它的最佳实践,请务必通读 Yoni Goldberg 的 JavaScript 最佳实践存储库。
关于测试工具的选择,我们采用Mocha.js
、Chai.js
和 ts-node
。Mocha.js
是一个测试运行器,Chai.js
是一个断言库,帮助确定你是否从你的代码中得到你所期望的结果,而 ts-node
帮助我们在TypeScript项目中使用这些工具。按照下面的步骤,为 npm包设置和运行测试。
-
安装相关依赖,
npm i -D mocha @type/mocha chai @types/chai ts-node
。 -
在根目录创建
.mocharc.json
。
{
"extension": ["ts"],
"spec": "./**/*.spec.ts",
"require": "ts-node/register"
}
-
在项目的根目录下创建一个
tests
文件夹。 -
在
tests
文件夹下创建index.spec.ts
文件。 -
编写示例代码单测。
import 'mocha';
import { assert } from 'chai';
import { helloWorld, goodBye } from '../index';
import npmPackage from '../index';
describe('NPM Package', () => {
it('should be an object', () => {
assert.isObject(npmPackage);
});
it('should have a helloWorld property', () => {
assert.property(npmPackage, 'helloWorld');
});
});
describe('Hello World Function', () => {
it('should be a function', () => {
assert.isFunction(helloWorld);
});
it('should return the hello world message', () => {
const expected = 'Hello World from my example modern npm package!';
const actual = helloWorld();
assert.equal(actual, expected);
});
});
describe('Goodbye Function', () => {
it('should be a function', () => {
assert.isFunction(goodBye);
});
it('should return the goodbye message', () => {
const expected = 'Goodbye from my example modern npm package!';
const actual = goodBye();
assert.equal(actual, expected);
});
});
- 在package.json 文件的 scripts 部分添加一个
test
相关脚本。
"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"
},
- 在终端执行
npm run test
。
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)
TESTING IN A PIPELINE
接下来我们希望在流水线中能够自动化跑测试流程。
-
为仓库创建一个新的GitHub Action :
https://github.com/<your-account-or-organization>/<your-repo-name>/actions/new
。 -
将工作流程重命名为
test.yml
-
编写相关脚本。
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
命令来执行测试。
PACKAGE TESTING
通过单元测试保证你的npm包的代码有信心是一回事,保证npm包整体的使用体验又是另一回事。这涉及将您的 npm 包作为依赖项引入另一个项目,并查看它在那里的使用是否如您预期的那样顺利。您可以通过以下五种方法对此进行测试:
npm pack
这种方法将利用npm pack
命令将 npm 包打包并压缩成一个文件(.tgz)。然后你可以到你想使用该包的项目中,通过这个文件安装它。这样做的步骤如下。
-
终端运行npm pack。注意它产生的.tgz文件和它的位置。
-
改变目录到你想使用npm 包的项目目录。例如:cd /path/to/project
-
运行
npm install /path/to/package.tgz
-
然后就可以在项目中使用该包来测试东西了
npm link
利用 npm link 命令来安装本地包:
- 在当前包目录中,在终端运行
npm link
- 改变目录到你想使用npm包的项目目录。例如:
cd /path/to/project
- 在项目中运行
npm link <name-of-your-package>
- 这样在项目中就可以使用我们的包。
相对路径
这种类似于npm link
。
-
在终端运行
npm install /path/to/your/package
与 npm link 的方法类似,这允许我们在项目中快速测试包的功能,但不会给你完整的类似生产的体验。这是因为它指向完整的软件包源代码目录,而不是你在npm注册表中找到的软件包的构建版本。
npm registry
这种方法利用了npm包的公共(或你自己)注册表。它涉及到发布的包,并像你通常对任何其他npm包那样进行安装。
- 使用本文前面概述的步骤,通过npm publish 命令发布npm包
- 改变目录到想使用npm包的项目目录。例如:cd /path/to/project
- 在项目目录中运行npm install
实施安全检查
就像你不希望在自己的项目中出现安全漏洞一样,你也不希望在其他人的项目中引入漏洞。构建一个预计会在许多其他项目中使用的npm包,这就增加了确保事情安全的责任。你需要有安全检查,以帮助监测、提醒和提供帮助来减少漏洞。这就是像Snyk这样的工具可以简化完成这些需求所需的工作的地方。
对于这个例子中的npm包,你使用GitHub作为你的源码控制管理工具,所以利用它的GitHub Actions功能将Snyk整合到工作流程中。Snyk 有一个GitHub Actions参考项目,可以帮助启动这方面的工作,并为你的项目可能使用的其他编程语言和工具提供例子。
-
Snyk是免费的,这里可以进行注册。
-
在GitHub上将你的Snyk API令牌添加为仓库秘密:github.com///settings/secrets/actions/new
-
仓库创建一个新的GitHub Action:github.com///actions/new
-
将workflow 重命名为snyk.yml
-
在 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 }}
有了这个设置,任何时候任何人推送到你的版本库或针对它打开一个拉动请求,都会进行安全检查,以确保它不会在软件包中引入任何漏洞。如果发现了问题,行动将失败,并提醒你发现的安全问题的细节。接下来,你将围绕版本管理和发布你的npm包进行自动化处理。
关于目前的设置,需要注意的一点是,它只利用了Snyk开源(SCA)产品,而不是Snyk代码(SAST)。Snyk Code是我们的代码安全产品,你需要首先通过你的Snyk账户启用它(免费),然后在这里添加到你的工作流程脚本中,以充分利用它。
Automating version management and publishing
每当在主分支中合并变化时,我们不想每次都手动更新npm包的版本并发布它。相反,会想让这个过程自动发生。如果你还记得本篇文章前面那个简单的npm包的例子,用以下命令来更新npm包的版本,然后发布它。
npm version <major|minor|patch>
npm publish
语义版本管理规定,版本要用三个占位符进行编号。第一个是主要版本,第二个是次要版本,而最后一个是补丁版本。如果想详细了解可以阅读 What is Package Lock JSON and How a Lockfile Works with Yarn and NPM packages。
Semantic Release的工具可以与 GitHub Actions 整合来帮助我们自动修改版本并发布。实现这一过程自动化的关键是,你在向项目提交变更时使用所谓的conventional commits。这使得自动化能够相应地更新一切,并知道如何为你准备项目的下一个版本。
以下步骤将引导您为现代 npm 包进行设置。
- 在你的终端执行
npm i -D semantic-release
。 - 在你的终端继续执行
npx semantic-release-cli setup
。 - 按照终端的提示,提供所需的令牌
需要一个来自 GitHub 的个人访问令牌。要创建一个,请到
https://github.com///settings/secrets/actions/new</your-name-or-github-organization>
在创建此令牌时,请使用以下作用域
还需要一个来自npm的自动化类型的访问令牌,只在CI环境中使用,这样它就能绕过你的账户的2FA。要创建一个,请到https://www.npmjs.com/settings//tokens
。请确保选择 "Automation"类型,因为这将用于CI/CD工作流程中。
将npm令牌作为仓库秘密添加到GitHub仓库中:https://github.com/<your-name-or-organization/<your-repository>/settings/secrets/actions/new。
将秘密的名称设置为NPM_TOKEN,其值是你在前面步骤中检索到的。
。
回到您的项目,转到您的 package.json 文件并添加一个 releases
的 key,如下所示。如果您的存储库的主分支仍称为 master 而不是 main,则相应地更新上述分支值。
"release": {
"branches": [
"main"
]
},
在 package.json 文件中添加一个 publishConfig
配置。
"publishConfig": {
"access": "public"
},
接下来我们创建一条release.yml
流水线来进行自动化发布。
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
在所有这些设置完成后,现在可以使用 conventional commits 将更改推送到您的主分支(或通过合并拉取请求)并且发布工作流将运行(当然在 Snyk 安全检查之后)。
当然其实建议将conventional commits
集成到git工作流
。
总结
通过上述示例,我们手把手完成了一个一个现代化npm package
包的创建过程,涉及unit test
、security checks
、CI
、Github Action
等。
相关示例代码可以查阅simple-npm-package。如果你喜欢Nodejs
可以关注我哦[公众号:NodeJs早早聊],持续更新相关文章。
参考文献
转载自:https://juejin.cn/post/7187055440318955575