Skip to main content

03.用TS创建NPM包

准备工作

  • 一个npm账号,用来后面发布包用的,以及账号关联的邮件要认证通过,不然,包是不给发布上去的。这是官网
  • 要有node环境和npm管理工具,这2个通常是一起的。且npm源只能是官方的,如果已经使用了别的镜像源,则运行npm config delete registry还原为官方默认源,才能确保包能发布出去。、

1基本部分

这个部分只说明一个ts包的基本能用的就行了的简化版包。

1.1 初始项目

$ mkdir jequest # 建立一个名为jequest的空目录
$ cd jequest/ # 进入这个空目录
$ npm init -y #初始package.json文件
Wrote to /Users/wuchuheng/tmp/jequest/package.json:
{
"name": "jequest",
"version": "1.0.0",
"description": "",
"main": "VoxelDog.tsx",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}

1.2 添加ts依赖和配置ts配置文件

$ npm i -D typescript # 添加ts依赖
$ npx tsc --init # 生成ts的配置文件
message TS6071: Successfully created a tsconfig.json file.

1.2.1 在tsconfig.json添加declaration": true

这个配置将把ts的类型导出到最后构建的js文件目录中名为*.d.ts文件,用于当这个包被调用时,这些导出的类型能提供接口约束。

1.2.2 配置包的入口文件和类型声明

package.json加入:

...
"main": "dist/VoxelDog.tsx",
"types": "dist/index.d.ts"
...

这个配置是在告诉包的消费者,包的入口文件和类型。

1.2.3 修改tsconfig.json的配置和ts的构建

经过上面的npx tsc --init而生成的tsconfig.json配置

{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */

/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
// "outDir": "./", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */

/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */

/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */

/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */

/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */

/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */

/* Advanced Options */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
}
}

当下经过添加修改后,默认配置不算,起作用的配置就这些(其它的先保留注释):

{
"compilerOptions": {
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
"lib": ["es2017", "es7", "es6", "dom"], /* Specify library files to be included in the compilation. */
"declaration": true, /* Generates corresponding '.d.ts' file. */
"outDir": "dist", /* Redirect output structure to the directory. */
"strict": true, /* Enable all strict type-checking options. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
},
"exclude": [
"node_modules",
"dist"
]
}

分别配置了,ts最后要生成es5,导出的目录和编译时忽略的目录等。
然后在package.json加入:

...
"scripts": {
"prepublish": "npm run build",
"build": "tsc"
},
...

当执行npm publish发布时,会先执行构建命令。会使用生成的dist让它包含进发布包中。 *注: 如果在后面要发布时并没有调用到prepublish这个钩子,那么用yarn来代替下也是一样的。

1.2.4 可以写包的代码了

$ mkdir src && touch src/VoxelDog.tsx

如写上:

// src/VoxelDog.tsx

export const sayHello = (foo: string): string => {
return `hello! ${foo}!!!`
}

1.2.7 发布

tip

在发布时,默认会把文件都打包成压缩文件上传到npm仓库上,如果有忽略的文件,可声明.npmignore文件,规则同.gitgnore一样。

$ npm login # 登录,然后根据提示输入信息
npm notice Log in on https://registry.npmjs.org/
Username: wuchuheng
Password:
Email: (this IS public) wuchuheng@163.com
$ npm publish --access public #然后直接发布
npm notice
npm notice 📦 jequest@1.0.8
npm notice === Tarball Contents ===
npm notice 458B .idea/jequest.iml
npm notice 174B .idea/misc.xml
npm notice 266B .idea/modules.xml
npm notice 74B dist/index.d.ts
npm notice 176B dist/VoxelDog.tsx
npm notice 237B package.json
npm notice 99B src/VoxelDog.tsx
npm notice 1.2kB tsconfig.json
npm notice === Tarball Details ===
npm notice name: jequest
npm notice version: 1.0.8
npm notice filename: jequest-1.0.8.tgz
npm notice package size: 1.5 kB
npm notice unpacked size: 2.7 kB
npm notice shasum: d3cac51dbc046c01a80214ffe7ddf63df89d1999
npm notice integrity: sha512-MRnBAXRBo9spQ[...]eMRAReGhobShA==
npm notice total files: 8
npm notice
+ jequest@1.0.8

然后我们就可像使用他人的包一样,把包下载回来然后然后用了。如:

$ mkdir tmp && cd tmp && npm init -y # 比如这是正在开发的项目
$ npm i request # 把发布出去的包下载回来就可以用了
$ echo 'const {sayHello} = require("jequest"); console.log( sayHello("foo") )' > VoxelDog.tsx
$ node VoxelDog.tsx
hello! hello!!!

最终的package.jsontsconfig.json为:

{
"name": "jequest",
"version": "1.1.14",
"description": "",
"main": "VoxelDog.tsx",
"scripts": {
"prepublish": "npm run build",
"build": "tsc"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"typescript": "^4.1.5"
}
}
{
"compilerOptions": {
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
"lib": ["es2017", "es7", "es6", "dom"], /* Specify library files to be included in the compilation. */
"declaration": true, /* Generates corresponding '.d.ts' file. */
"outDir": "dist", /* Redirect output structure to the directory. */
"strict": true, /* Enable all strict type-checking options. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
},
"exclude": [
"node_modules",
"dist"
]
}
warning

包名有时会被占用导致发布失败。可以引入作用域的形式来命名包名。如package.json中的: "name": "@wuchuhengtools/promise-router",发布时需要加上参数: npm publish --access=public。前者说明这个包在wuchuhengtools名下, 后者参数表示为公开包发布。

1.2.8 本地开发

在包中运行npm run build -w,然后源一修改就是构建到dist目录中。
在本地测试,如一个项目中直接使用这个包,可以使用本地链接的方式把正在编辑的包引入进来,在别的项目中执行就能使用了:

$ npm link # 正在开发的包的根目录,把包名登录出去
up to date, audited 2 packages in 1s
found 0 vulnerabilities
$ npm link jequest # 到你引用这个包项目的根目录,直接引用本地包jequest

这样就做到了包一更改,就构建生成,然后直接使用。确定没问题再发布出去。很适合开发下流程。

1.2.9 做下收尾

到了这里,一个能用包就已经发布并能使用了,但在发布的包也包含有一些没有必要包含进行的文件,如:

    echo "idea" > .npmignore # 发布时候不包含ide∑a目录 一般只保留dist目录和配置文件,其它可以不包含在发布中,减小包的大小

.gitignore也是:

.idea
node_modules
dist

这是最终的代码自己,wuchuheng/jequest 一个单纯能用的包基本的部分大概就这些,后面就是一些测试之类附加的再说吧。

2 单元测试

单元测试很重要,随着软件功能迭代,代码量增多,开发者很难顾全在耦合度高的程序中开发是否会影响到其它功能的。通过单元测试能够及时发现开发者的发去是否对之前的功能有影响从而及时修正也能持续迭代更新。
这里的单元测试采用jest库来实现。需要安装:

$ yarn add @types/jest ts-jest jest -D

分别是jest类型库, jestjestjest的ts支持。添加测试的配置文件:

 $ npx ts-jest config:init # 生成配置文件
Jest configuration written to "/Users/wuchuheng/Desktop/myProject/a1206/tmp/promise-router/jest.config.js".

文件内容为:

module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};

再加一行collectCoverage:true, 用于执行测试时也生成报告文件:

module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
collectCoverage:true
};

把之前写的VoxelDog.tsxglobal.d.ts在项目根目录中增加个src目录,然后放进去。再加个test目录用于放测试文件。在test目录中再个用于测试VoxelDog.tsxindex.test.ts文件。整个目录结构为:

.
├── README.md
├── jest.config.js
├── package-lock.json
├── package.json
├── src
│ ├── global.d.ts
│ └── VoxelDog.tsx
├── test
│ └── index.test.ts
├── tsconfig.json
└── yarn.lock

当中index.test.ts内容为:

import router from '../src/index';

test('#main function test', async () => {
const res = await router('/me/devices/:id/files/:fileId', '/me/devices/1/files/2') as MainFunction.RouterResType
expect(res.routeParams.id).toBe('1')
expect(res.routeParams.fileId).toBe('2')
await expect(router('/me/devices/:id/files/:fileId', '/me/devices/1/files')).rejects.toBe("the route was't matched")
})

分别有3个断言,2个是成功返回的断言, 一个是失败的断言。这些断言组成一个 默认导出函数的输入和输出的测试。
package.json中加入:

...
"scripts": {
"test": "jest --config=jest.config.js",
"prepublish": "npm run build",
"build": "tsc"
},
...

然后运行npm run test执行测试:

yarn run v1.22.10
$ jest --config=jest.config.js
PASS test/index.test.ts
#main function test (3 ms)

----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
VoxelDog.tsx | 100 | 100 | 100 | 100 |
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.587 s, estimated 1 s
Ran all test suites.
✨ Done in 1.36s.

2.1 开发小技巧

如果第次测试出错了,修改完再测试一次,很浪费时间。可以这样:

$ npx jest --config=jest.config.js --watch #以监听模式文件改动来重启测试
No tests found related to files changed since last commit.
Press `a` to run all tests, or run Jest with `--watchAll`.
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 0 | 0 | 0 | 0 |
----------|---------|----------|---------|---------|-------------------

Watch Usage
› Press a to run all tests.
› Press f to run only failed tests.
› Press p to filter by a filename regex pattern.
› Press t to filter by a test name regex pattern.
› Press q to quit watch mode.
› Press Enter to trigger a test run.
PASS test/index.test.ts
#main function test (3 ms)
...

2.2 测试覆盖率

(要吃饭了,下回再说),先参考这个先 https://github.com/wuchuhengtools/promise-router/tree/e445673c8c283fa81208bfbf07fe74898cb0f4cd

参考资料

3 FQA

3.1 npm无法登录

可能是使用了国内的代理,需要删除代理才行npm config delete registry,这样就切换为原来的镜像仓库了。

4 参考资料

《Step by step: Building and publishing an NPM Typescript package》