使用 Rollup 构建并发布一个 Typescipt NPM包

使用 Rollup 构建并发布一个 Typescipt NPM包

前言

最近使用 Github ActionsCICD 的时候,用到了企业微信提供的 Github 机器人,其提供了一套基于 Webhook 机制的消息推送功能,可以在构建打包或PR发起的时候将相关信息转发到群聊中。

关于 Webhook 的介绍推荐这篇文章:https://segmentfault.com/a/1190000020249988

根据官方提供的文档,其实也可以自己写代码,来处理一些自定义场景,比如转发 Sentry 告警到群聊中(Sentry 本身也提供了这种 Webhook 机制)。

为了方便使用和嵌入项目中去,我把官方文档发送消息的处理做了一个封装,并加上了 Typescript 类型支持,并以 NPM 包的形式进行发布(webhook-chatbot)。下面是过程中的一些记录:

需要用到的工具

  • npm 账号:将构建后的 lib 包发布到 npm 仓库
  • rollup:将所有使用到的模块打包到一个或多个最终的发布文件中
  • typescript:提供类型检测和代码提示
  • eslint + prettier:代码风格校验和自动格式化

项目初始化

大概要用到的东西理清楚后,就可以新建一个新项目了

  1. 创建包文件夹

    mkdir messaging-robot && cd messaging-robot
    
  2. yarn 初始化包

    yarn init -y
    
  3. git 初始化并关联 Github 远程仓库

    git init
    git add . && git commit -m "init"
    git remote add origin <Git Repository Url>
    git push -u origin master
    

使用 Typescript

安装

yarn add typescipt -D && npx tsc --init

配置

生成 tsconfig.json文件,用来配置 ts 的编译选项

// tsconfig.json
{
  "compilerOptions": {
    "declaration": true, // 生成 .d.ts 声明文件
    "esModuleInterop": true // // 提供两个helper函数__importStar和__importDefault,通过为导入内容创建命名空间,实现CommonJS和ES模块之间的互操作性
    "lib": ["ESNext"], // 最新的 es 语法提示,同时不引入 dom 相关的定义文件
    // "module": "esnext", // 源代码使用模块类型
    "moduleResolution": "Node", // 能在 esm 项目中解析 cjs 模块
     "outDir": "./", // 编译后的文件存到到哪个目录
    "removeComments": false, // 编译删除注释,这里选择保留
    "target": "es6", // 目标 ECMAScript 版本, Node v6之后就已经支持 es6 了
  },
  // 只编译 src 目录下的文件
  "include": [
    "src"
  ],
	"exclude": ["node_modules","lib"],
  "ts-node": {
    "esm": true
  }
}

编写测试源码并导出

src 目录下编写 ts 代码,并在 src/index.ts 中进行导出

// src/index.ts
export const a = 0
export const fn = () => { console.log(a) }

export default {
  a,
  fn
}

使用 tsc 命令测试构建

生成 js 文件和 .d.ts 文件,如下图

image-20220403154415749

image-20220403154348555

使用 ESlint + Prettier

安装

yarn add eslint prettier -D

配置

eslint

可以使用 npx eslint --init命令行的方式自动安装规则集、插件和生成配置文件

在这里选择直接手动安装

yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser 

新建 .eslintrc 配置文件

{
  "env": {
    "node": true
  },
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended"
  ],
  "plugins": [
    "@typescript-eslint"
  ],
  "parser": "@typescript-eslint/parser",
  "root": true
}
prettier
yarn add -D eslint-config-prettier eslint-plugin-prettier

添加 .pretteric.js 文件,这里只是根据个人的习惯进行配置

// .pretteric.js
module.exports = {
  printWidth: 80,
  semi: false,
  singleQuote: true,
  trailingComma: 'none',
  arrowParens: 'avoid'
}

修改 .eslintrc 文件

// .eslintrc
  "env": {
    "node": true
  },
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:prettier/recommended"
  ],
  "plugins": [
    "@typescript-eslint"
  ],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaVersion": "latest",
    "sourceType": "module"
  },
  "root": true

效果

上面配置完毕,可以看到就生效了

image-20220403162853148

使用 Rollup 打包

这个项目的模块化采用了 ESM 规范,并且是 TS 编写的,需要进行打包,这里我采用了 Rollup 作为构建工具。

Rollup 使用流程

浏览器环境
  • 若无需考虑浏览器兼容性

    1. 书写 esm 代码
    2. rollup 从项目入口文件出发,递归识别 esm 模块
    3. 最终打包成一个或多个 bundle.js
    4. 浏览器通过 <script type="module"></srcipt>引入
  • 若考虑浏览器兼容性

    相对而言就会比较复杂,需要用额外的 polyfill 库,或者结合 Webpack 使用

npm 包
  1. 书写 esm 代码 (也可以使用 cjs,需要插件@rollup/plugin-commonjs
  2. rollup 从项目入口文件出发,递归识别 esm 模块
  3. 配置输出多种格式的模块,如 esm、cjs、umd、amd
  4. 最终打包成一个或多个 bundle.js

Rollup 的优点和使用场景

  • 轻量级、配置简单、容易上手
  • 支持 Tree Shaking

这些特点决定了 Rollup 适合用来构建一些 JS 库(如 Vue、React等等),或高版本无需向下兼容的程序。与 Webpack 的臃肿相比,Rollup 打包后的代码普遍体积更小,也更容易看懂(Webpack 为了实现 Tree Shaking 会注入很多自定义的代码)

我们想要发布的 npm 包只在 Node 环境下使用,就适合使用 Rollup 对代码进行处理,甚至都不需要用到 Babel 进行 ES 语法的向下兼容。下面就正式开始吧!

安装

yarn add rollup -D

基本配置

安装必要的处理插件

  • @rollup/plugin-node-resolve: 查找和打包node_modules中的第三方模块
  • @rollup/plugin-commonjs: 将 CommonJS 转换成 ES2015 模块供 Rollup 处理
  • @rollup/plugin-typescript: 解析TypeScript
  • rollup-plugin-delete: 打包前删除之前的 bundle
yarn add @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-typescript @rollup-plugin-babel rollup-plugin-delete -D

新建 rollup.config.js 文件

// rollup.config.js
import nodeResolve from '@rollup/plugin-node-resolve' 
import commonjs from '@rollup/plugin-commonjs'
import typescript from '@rollup/plugin-typescript' 
import del from 'rollup-plugin-delete'

export default [
  {
    input: 'src/index.ts',
    output: {
      file: 'lib/index.cjs.js',
      format: 'cjs',
      entryFileNames: '[name].cjs.js'
    },
    plugins: [
      del({
        targets: ['lib/**/*']
      }),
      nodeResolve(), 
      commonjs(), 
      typescript({
        tsconfig: './tsconfig.json'
      }) 
    ],
    external: [...] // 外部引用的库,不要打包,用于处理 peerDependencies
  },
  {
    input: 'src/index.ts',
    output: {
      file: 'lib/index.esm.js',
      format: 'esm',
      entryFileNames: '[name].esm.js'
    },
    plugins: [
      nodeResolve(), 
      commonjs(), 
      typescript({
        tsconfig: './tsconfig.json'
      }) 
    ],
    external: [...]
  }

打包命令

package.json 文件中新增 scripts 打包命令

{
	...
  "scripts": {
    "build": "rollup -c",
  }
  ...
}

打包命令

package.json 文件中新增 scripts 打包命令

{
	...
  "scripts": {
    "build": "rollup -c",
  }
  ...
}

入口文件处导出模块

这是我这个项目中代码的目录结构

image-20220415091220829

只要在 index.ts 入口文件导出模块即可

// src/index.ts
import WXRobot from './robots/WXRobot'
import DingRobot from './robots/DingRobot'

export { WXRobot, DingRobot }

运行打包

使用 yarn build执行构建命令,就可以得到我们的目标代码产物了

image-20220415091502734

发布 npm 包

自动化 NPM Scripts

npm 包在发布的时候会触发一些钩子,可以在这些阶段做一些自动化的处理,详情参考 npm scripts
下面是这次使用到的一些配置,主要也就是代码的 lint 校验 和 自动 build

"scripts": {
   "build": "rollup -c",
    "format": "prettier --write \"src/**/*.ts\" \"src/**/*.js\"",
    "lint": "eslint . --ext .ts",
    "prepublishOnly": "npm run lint && npm run build",
    "preversion": "npm run lint"
  },

注册账户

前往 npm 官网 创建一个 npm 账号

发布

一般来说,我们会将构建好的 npm 包直接托管在 npm 的公有 registry 上, 这样每个人都可以自由地安装使用。有时候,可能只是一些内部公共业务的抽离,显然不能让所有人都有权限下载,这时候就需要考虑私有化部署一个 registry,再将 npm 发布到内网,实现不同项目的代码库共享。

公有

命令行使用 npm login 登录 npm 账号

image-20220411160521420

使用 npm publish 进行发布

image-20220414122936262

私有
  1. 部署 verdaccio

verdaccio 是一个私有仓库搭建工具,搭配镜像源管理工具 nrm 使用,可以方便地切换不同的 npm 源使用。

npm i -g verdaccio nrm

安装完毕可以直接运行 verdaccio 开启私有 npm 服务,也可以对 verdaccio 进行一些启动配置(/users/用户名/.config/verdaccio/config.yaml

使用 pm2 守护进程去启动 verdaccio,避免断开连接后服务关闭

npm install -g pm2
pm2 start verdaccio 

启动后,我们打开浏览器http://127.0.0.1:4873,就可以看到了

image-20220411154852639

  1. 发布 npm 包

在发布前,要先确保我们当前的 npm 源不是公有源

使用 nrm add local http://localhost:4873 添加一个本地源 ,使用 nrm ls命令查看

image-20220411155421167

使用 nrm use local 使用我们的本地源

image-20220414130924403

剩下的操作就跟使用公有源是一样的了,这里不再重复。

遇到的问题和处理

1. console 对象在 ts 文件中不存在

'console' is not defined.eslint(no-undef)1 problem (1 error, 0 warnings)

JavaScript 有很多种运行环境,常见的浏览器和 NodeJS,除此之外还有很多软件系统使用 JavaScript 作为脚本引擎,比如 RN、PostgreSQL等。而这些运行环境可能并不存在console这个对象。另外在浏览器环境下会有window对象,而 Node 下没有;在 Node 下会有process对象,而浏览器环境下没有。

所以在配置文件应该指明程序的目标环境:

// .eslintrc
{
  ......
  "env": {
    "node": true
  },
 ......
}

保存后重新执行检查,就不会有任何报错信息了。

https://morning.work/page/maintainable-nodejs/getting-started-with-eslint.html

2. 顶层 awiat 的使用

根据语法规范,await 只能出现在 async函数内部,否则会报错。如果我们就想在一个 NodeJS 模块里面使用顶层 await,有如下几种办法:

  1. IIFE

    (async () => {
      const res = await fn()
    })()
    
  2. NodeJS v14.8 后可用

    Node.js v14.8 版本中允许直接使用顶层 await,替代了之前使用 --harmony-top-level-await 标识的方案。

    $node --harmony-top-level-await app.js
    

    但这只能在 ESM 中使用,也即是说要保证每个 js 文件都是 ESM 模块

上面是针对 js 文件的做法,如果是 ts 的话,则有:

image-20220415084714709

最新的 es 提案(ES2022)支持了 top level await,允许 await关键字能在模块 modules 的顶层正常工作。(在 class 代码块或非 async 函数仍不支持),根据 eslint 的提示,我们只需要在 tsconfig.json 里面设置一下即可。

3. npm 构建的五种依赖

dependencies

指定了项目运行所需要的依赖

devDependcies

指定了项目开发所需要的依赖

peerDependencies

当开发一个模块的时候,如果当前模块A所依赖的模块B同时依赖一个第三方模块C

有几种情况需要讨论:

  1. A 和 B 所依赖的 C 模块版本相同

    A
    B
    C

    这种情况下将只安装一份模块C,对应的 node_mudules目录结构为:

    .
    ├── A.js
    ├── node_modules
    │   ├── C
    │   │   ├—— C.js
    │   │   └── package.json
    │   ├── B
    │   │   ├—— B.js
    │   │   └── package.json
    ├── package.json
    
  2. A 和 B 所依赖的 C 模块版本不相同

    A
    B
    C1
    C2

    这种情况下将会安装两份模块C,对应的 node_mudules目录结构为

    .
    ├── A.js
    ├── node_modules
    │   ├── B
    │   │   ├── B.js
    │   │   ├── node_modules
    │   │   │   └── C2
    │   │   │       ├── C2.js
    │   │   │       └── package.json
    │   │   └── package.json
    │   └── C1
    │       ├── C1.js
    │       └── package.json
    └── package.json
    

    大多数情况下,这也没什么问题,C模块的两个版本可以共存。但是,当 B 与 C 的依赖关系暴露给用户时,就可能会出现问题。

    典型的场景是插件,比如我们的项目 A 中用到了 Vue2.x (C1),我们还安装了 element-plus(B)。但我们知道,element-plus 本身是基于 Vue3.x(C2) 构建的。这时,用户要是将 2.x 版本的 Vue 实例传给 element-plus,就可能会出现问题。

    因此,需要一种机制,在依赖安装的时候,如果 B 和 C 一起安装,那么 C 必须是 3.x版本。

    peerDependencies字段,就是用来供插件(element-plus)指定其所需要的依赖的版本。可以通过peerDependencies字段来限制,使用 element-plus 必须依赖 vue3.x

      "peerDependencies": {
        "vue": "^3.2.0"
      },
    

bundledDependencies

指定发布的时候会被一起打包的模块

optionalDependencies

如果一个依赖模块可以被使用, 同时你也希望在该模块找不到或无法获取时npm继续运行,你可以把这个模块依赖放到optionalDependencies配置中。这个配置的写法和dependencies的写法一样,不同的是这里边写的模块安装失败不会导致npm install失败。

Semver 版本规范

它们的值都是一个对象,对象中的每个成员,分别由模块名和对应的版本要求组成,表示依赖的模块及其版本范围

版本号遵循 semver 规范(主版本号.次版本号.修订号)

  • 固定版本:如1.2.0,安装时只安装指定的版本
  • 波浪号:如~1.2.0,安装次版本号的最新修订版本,即1.2.x
  • 插入号:如^1.2.0,安装主版本号的最新次版本、修订版本,即1.x.x

4. url.parse() 已废弃

项目中用到了 https 模块发送 post 请求到指定的 webhook url,原生 node 提供了 http.request,需要传入解析过后的参数:

 https.request({
      hostname: 'example.org' ,
      protocol: 'https',
      path: '/foo/',
      port: '443',
      method: 'post',
      headers: {
        'Content-Type': 'application/json'
      },
 }

于是就使用到了 url 模块的 parse 方法进行处理,会出现提示说已经被废弃:

image-20220405103912283

url.parse() 方法使用一种宽松的非标准算法来解析 url 字符串,可能会引入安全问题,已经确定的有主机名欺骗以及用户名和密码处理不当的问题。

官方推荐使用 URL 内置类进行 url 解析,它提供了一组符合 WHATWG 规范 的 API。

const url = new URL('https://example.org:8080/foo/?id=1&name=who')
console.log(url);
URL {
  href: 'https://example.org:8080/foo/?id=1&name=who',
  origin: 'https://example.org:8080',
  protocol: 'https:',
  username: '',
  password: '',
  host: 'example.org:8080',
  hostname: 'example.org',
  port: '8080',
  pathname: '/foo/',
  search: '?id=1&name=who',
  searchParams: URLSearchParams { 'id' => '1', 'name' => 'who' },
  hash: ''
}

https://nodejs.org/api/url.html#class-url

5. 生产环境下缺少 ts-lib 报错

image-20220405135230232

其实这跟上面提到的 peerDependencies 也有关系,我们在安装依赖 @rollup/plugin-typescipt 的时候,命令行有提示过我们:

image-20220405135556290

rollup 官方插件仓库也有提示: Note that both typescript and tslib are peer dependencies of this plugin that need to be installed separately.

6. ?? 和 || 的区别

a ?? b

a || b

相同点:用法相同,都是前后为值,中间用运算符连接。根据前面的值判断最终值

不同点:使用 ??时,当 a 为 nullundefined时,才会返回 b;使用 ||时,a 会先进行隐式类型转换,变成 Boolean后进行判断,若为 true,则返回a,否则返回b

总体来说,空值合并运算符更适合在不知道变量是否有值的时候使用

总结

关于 Rollup ,其实还是比较好上手的,它配置起来足够轻量足够简洁。但这里只是简单地处理了一下 typescript,并生成两种不同模块的入口文件。如果在浏览器中使用,还需要考虑非 js 文件的识别、静态文件处理、ES语法降级、sourcemap、代码压缩、热更新等等。大多数我们能想到的功能, Rollup 都有对应的 plugin 去做处理,并且将它们收录到了官方的插件库中,使用的时候非常方便。

最后,关于这篇文章用到的代码,还有这个库本身,我都把它们放到 github 上了,感兴趣的话可以点这里,如果有哪些不对的地方欢迎指正,感谢。

参考

https://segmentfault.com/a/1190000008832423

https://www.ruanyifeng.com/blog/2016/10/npm_scripts.html

https://docs.npmjs.com/cli/v8/using-npm/scripts

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值