制作一个同时支持 ESM, CJS, Browser 的 Package

1,864 阅读4分钟

新建项目.png

目标

最后打包的结果可以达到以下功能

  • 同时支持 CommonJsESModule 两种模块系统
  • 浏览器支持使用 script 标签引入
  • ESModule 支持 tree-shaking
  • 支持 TypeScript

接下来将逐步完成每一个功能点

开始吧

说明

  • 源代码编写采用 TypeScript

创建 Package

  1. 创建一个文件夹,名字叫它为 uodule 吧 😄

  2. 进入 uodule 文件夹

  3. 初始化 package.json

mkdir yyds

cd yyds

npm init -y
  1. 局部安装 TypeScript (全局也可以)
npm install typescript -D
  1. 初始化 tsconfig.json
npx tsc --init
  1. 精简 tsconfig.json

留下这些即可

{
  /* Visit https://aka.ms/tsconfig to read more about this file */
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "declaration": true,
    // 因为最后是采用 Rollup 进行打包
    // 所以这里只需要生成类型文件即可
    "emitDeclarationOnly": true,
    "declarationDir": "types",
    "removeComments": false,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "moduleResolution": "node"
  }
}

package.json

{
  "name": "uodule",
  "version": "0.0.1",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "typescript": "^4.7.4"
  }
}

编写代码

简单编写几个功能函数:

  • padZero: 对小于 10 的整形数字进行补 0
  • isMobileByUa: 通过 UserAgent 判断当前设备是否移动端

padZero.ts

/**
 * 向 < 10 的整形数值进行补 0
 * @param {number} n
 * @returns {string}
 */
export function padZero(n: number) {
    if (!Number.isInteger(n)) {
        return n
    }
    return (n < 10 ? `0${n}` : String(n))
}

isMobileByUa.ts

/**
 * 通过检测设备 ua
 * 判断是否是移动端设备
 * @returns {boolean}
 */
export function isMobileByUa(userAgent = window.navigator.userAgent) {
    const reg = /(Android|iPhone|Windows Phone|iPad|webOS|BlackBerry|mobile)/i
    return reg.test(userAgent)
}

index.ts

export * from './padZero'
export * from './isMobileByUa'

当前的目录结构如下:

├── src
│   ├── index.ts
│   ├── isMobileByUa.ts
│   └── padZero.ts
├── tsconfig.json
├── package-lock.json
└── package.json

模块导出

在构建之前,需要先了解清楚,我们究竟要如何去进行构建?

  1. 需要兼容 CommonJs 系统

    打包一份 CJS 格式的文件

  2. 需要兼容 ESModule 系统

    打包一份 ESM 格式的文件

  3. 需要兼容 Script 标签引入

    打包一份 UMD 格式的文件,关于什么是 UMD,可以参考文章最下面的参考文章

如果要打包 UMD 格式的文件的话,那么就可以把单独的 CJS 的文件给省略了。因为 UMD 格式的文件兼容 CJS 并且会向浏览器 window 对象中挂载全局对象。

那么最后只需要构建以下文件:

  • ESM 格式文件
  • UMD 格式文件
  • TS 类型文件

关于文件导出

可以参考 package.json 中的 exports、main、module 字段 文章。不再细述,直接给出配置 + 简单解释

{
  "name": "uodule",
  "version": "0.0.1",
  "description": "",
  // 旧版本的 Nodejs 文件入口
  "main": "index.js",
  // 类型文件入口
  "types": "index.d.ts",
  // 优先级高于 main 字段,支持条件导出
  // 下述意思: 支持 exports 字段的 Nodejs
  // 在遇到使用 import 关键字导入模块时,取 index.mjs 文件
  // 使用其他导入方式时,都取 index.js 文件
  "exports": {
    "import": "./index.mjs",
    "default": "./index.js"
  },
  "scripts": {
    ...
  },
  "devDependencies": {
    ...
  }
}

构建配置

往 package.json 中增加三个 script 命令

# 生成 TS 类型文件
npm set-script es "tsc"
# 构建 ESM、UMD 格式的文件
npm set-script build "rollup -c"
# 串行(同步)执行上述两命令
npm set-script generate "npm run es && npm run build"
  1. rollup 配置

需要先安装依赖

  • rollup 构建工具
  • @rollup/plugin-typescript 用于识别 TS 的 rollup 插件
  • rollup-plugin-delete 用于删除文件的 rollup 插件
  • rollup-plugin-dts 用于集合 TS 类型文件的 rollup 插件
  • rollup-plugin-terser 用于压缩文件的 rollup 插件
  • tslib @rollup/plugin-typescript 需要的依赖(同等依赖)
npm install rollup @rollup/plugin-typescript rollup-plugin-delete rollup-plugin-dts rollup-plugin-terser tslib -D

最终的 rollup.config.js 配置文件如下:

import typescript from '@rollup/plugin-typescript'
import dts from 'rollup-plugin-dts'
import del from 'rollup-plugin-delete'
import { terser } from 'rollup-plugin-terser'
import { defineConfig } from 'rollup'

const publicConfig = {
    format: 'umd',
    name: 'uodule'
}

const config = defineConfig([
    {
        input: 'src/index.ts',
        output: [
            {
                file: 'index.js',
                ...publicConfig
            },
            {
                file: 'index.min.js',
                ...publicConfig,
                plugins: [
                    terser()
                ]
            }
        ],
        plugins: [
            typescript({
                declaration: false,
                target: "ES5"
            })
        ]
    },
    {
        input: 'src/index.ts',
        output: {
            file: 'index.mjs',
            format: 'esm'
        },
        plugins: [
            typescript({
                declaration: false
            })
        ]
    },
    // 归并 .d.ts 文件
    {
        input: 'types/index.d.ts',
        output: {
            file: 'index.d.ts',
            format: 'es'
        },
        plugins: [
            // 将类型文件全部集中到一个文件中
            dts(),
            // 在构建完成后,删除 types 文件夹
            del({
                targets: 'types',
                hook: 'buildEnd'
            })
        ]
    }
])

export default config
  1. 执行构建命令
npm run generate

image.png

发布 Package

此处省略,请参考关于 发布 npm 包 的文章

使用

本文章的 uodule 已经发布到 npm 和上传至 github,仅供学习参考

Nodejs

CommonJs

const { padZero } = require('uodule')
const uodule = require('uodule')

console.log(padZero)
console.log(uodule)

/*

[Function: padZero]
{
  isMobileByUa: [Function: isMobileByUa],
  padZero: [Function: padZero]
}

*/

ESModule

import * as uodule from 'uodule'
import { padZero } from 'uodule'
// 这种引入方式将会报错,因为源码中并没有使用 export default
import uodule from 'uodule'

console.log(padZero)
console.log(uodule)

/*

[Function: padZero]
[Module: null prototype] {
  isMobileByUa: [Function: isMobileByUa],
  padZero: [Function: padZero]
}

*/

前端项目

用法和 ESModule 一样,并在构建的时候支持 tree-shaking

浏览器

<script src="https://unpkg.com/uodule@0.0.4/index.min.js"></script>

发布到 npm 上的包,都可以使用 unpkg CDN 服务,具体请看 unpkg

image.png

参考文章