首先问一下自己,为什么要用 TypeScript 来开发 npm 包?
用 TypeScript 开发npm包,需要配置一堆东西,比如开发过程中需要使用ts-node
或者esno
这样的工具把 TS 转化为 JS后再执行,开发完成后也需要构建等等。
如果我的包比较简单,我直接用 JavaScript 不香吗?
之所以,建议使用 TypeScript 来开发 npm 包,首先最重要的一点,一定是对开发者与使用者的类型提示。
无论你的包里是只有一个函数的导出,还是有零零总总大几十个函数,类型的存在都能让这个包的使用更加友好。如果你是包的使用者,你是更想"哦,这里类型报错了,我的入参类型不太对",还是"为什么这个地方一直报错呢...可恶啊,查了一通,原来是我的变量到这里的时候类型不对了"?
即使你是在 JavaScript 工程中引用 TypeScript 编写的包,只要配置了 TypeScript 开发环境,也一样能够享受到这些基于类型的辅助能力。
另外一个重要的原因则是,如果你使用了高阶的语法,此时你需要引入 Babel / Rollup 这样的编译工具来做一次语法降级,Babel 的配置略显繁琐,简单场景上 Rollup 又像高射炮打蚊子。
这个时候 TypeScript 自带的语法降级能力就香起来了:只需要简单的几行配置就够用,还带类型,还带简单的 Lint 能力...何乐而不为?
因此,本文将会从头演示如何基于 TypeScript 来编写 + 发布一个 npm 包,包括环境搭建、本地调试、编译配置以及发布等等。
本文内容分为开发阶段和打包阶段。
本地开发阶段
由于 NodeJs 不能直接执行 TS 文件,我们需要 esno 的帮助,它是一个基于 ESBuild 的执行工具,借助 ESBuild 闪电般的速度,首先编译 TS 文件然后再使用 Node 执行。
你可能会想,ESBuild 不支持类型检查吧?不用担心,VS Code 本地有类型检查,加上发布前仍然会使用 tsc 进行编译。
js
$ npm i esno -D
$ npx esno ./src/index.ts
在日常开发时,往往会需要频繁地修改然后执行,不断查看代码的输出和表现。
在日常的网页开发中,Webpack Dev Server 已经帮我们很好地处理了这个需要,使用 JavaScript 开发 NodeJs 应用时你可能也习惯了 nodemon
,那么 TypeScript
下应该怎么做?
首先,这里也是使用 nodemon,一般习惯的 nodemon index.js
只是它最基本的用法,你其实可以自由地配置文件的执行程序,以及监听哪些文件等等,在 package.json
中进行配置:
js
{
"nodemonConfig": {
"delay": 500,
"env": {
"NODE_ENV": "development"
},
"execMap": {
"ts": "esno",
"js": "node"
},
"ignore": ["node_modules"],
"verbose": true
},
}
这样一来,在执行 nodemon index.ts
时,就能够自动使用 esno 来执行 ts 文件了。
编译配置方面,可以参考优雅的配置你的 tsconfig.json这篇文章,以模板中的配置文件为例:
js
{
"compilerOptions": {
"target": "ES2018",
"module": "ES2015",
"types": [],
"outDir": "dist", // 输出到 dist 目录下
"skipLibCheck": true,
"moduleResolution": "node",
"strictNullChecks": true, // 开启严格检查
"declaration": true, // 输出声明文件
"declarationDir": "dist/type", // 声明文件放置位置
"strict": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noUnusedParameters": false,
"noUnusedLocals": false,
"esModuleInterop": true, // ESM 与 CJS 相关,推荐开启来解决大部分问题
"allowSyntheticDefaultImports": true, // ESM 与 CJS 相关,推荐开启来解决大部分问题
"baseUrl": "."
}
}
由于 npm 包需要手动指定入口,请确保 outDir 与 package.json 中的 main 指向是一致的:
js
// commonjs格式时入口文件
"main": "dist/index.umd.js",
// es格式时入口文件
"module": "dist/index.es.js",
// 声明文件入口
"types": "dist/types/index.d.ts",
// 这个参数是表面该包是es格式,这里的目的是为了引入rollup.config.js必须要加的
"type": "module"
rollup 打包
首先看一下rollup.config.js
配置:
js
import { nodeResolve } from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import typescript from 'rollup-plugin-typescript2'
import json from '@rollup/plugin-json'
export default {
input: 'src/index.ts',
output: [
{
file: './dist/index.umd.js',
name: 'CrowllerTs',
format: 'umd'
},
{
file: './dist/index.es.js',
format: 'es'
}
],
plugins: [
json(),
typescript({ useTsconfigDeclarationDir: true }),
commonjs(),
nodeResolve()
]
}
@rollup/plugin-node-resolve
: 将编写的源码与源码依赖的第三方库进行合并,作为一个完整的包对外输出,不需要用户再去下载第三方库。@rollup/plugin-commonjs
:rollup 主要是用来打包es格式书写的模块,如果要打包commonjs格式书写的模块,需要使用这个插件。rollup-plugin-typescript2
: rollup-plugin-typescript2 插件在编译时底层也是调用typescript 把 ts 编译成 js,它也会使用tsconfig.json的配置。@rollup/plugin-json
: 对于json文件,比如 package.json,我们无法直接引用需要安装json插件。
最后在package.json
中加上打包命令:
js
"scripts": {
"build": "rollup -c"
}
这里最重要的是使用rollup-plugin-typescript2
来让rollup
能够打包TypeScript
代码。
另外,tsconfig.json中的"target": "ES5"
可以帮我们将代码打包为ES5
的语法,所以这里不用再使用babel
来进行语法转换了。
关于 rollup 这一部分可以参考网上的资料,这里就不做详细介绍了。