你的npm仓库也许不需要任何打包工具

为什么你的仓库不需要打包工具

如果你的仓库使用的是打包工具进行编译和打包,那么

  • 优点是仓库对业务方没有影响
  • 缺点是它可能会增加仓库的复杂性,并且有可能对业务方造成一些限制

举个实际性的例子,你的仓库可能会考虑通过异步的import()来进行代码分割,如果你将仓库打包成一个单一的bundle文件,然后业务方是无法通过异步的import()来对该仓库的代码进行代码分割的。

打包仓库前先考虑以下问题

如果你想生成一个UMD文件以至于能通过<script>标签来引入到业务方的项目中,或许你需要一个打包工具。但是未来UMD的形式将不再流行,你也许会更倾向于使用单一的ESM模块。但这都是后话了,你仍然需要考虑

  • 如果你不需要打包依赖项,那么使用打包工具的收益是什么?
  • 如果你需要打包依赖项,那么你就不允许业务方通过semver语义化的方式更新你的子依赖项吗?

我的建议:别使用打包工具,tscbabel能解决大部分的组件库编译场景

如何正确使用 babel 和 tsc 编译我们的 npm 包

通过 babel 进行编译

.babelrc 关键配置示例如下

export default {
  presets: [
    [
      '@babel/preset-env',
      {
        modules: false,  // 保持ES modules,可根据需求修改
      },
    ],
    '@babel/preset-typescript', // 编译ts
  ],
  plugins: [
    [
      '@babel/plugin-transform-runtime' // 将通用helper方法通过@babel/runtime的形式引入
    ],
  ],
}
  • modules:false:保持 ES modules,我的是前端环境,可根据自身需求修改
  • 也许你会问我为什么不引入usage的 polyfill,个人建议不在 npm 包中引入 polyfill,有可能你引入的和业务方的有冲突,写好库的 README 比什么都强

通过 tsc 对类型进行检查并生成声明文件

tsconfig.json 配置示例如下

{
  "include": ["src"],
  "compilerOptions": {
    "target": "es2018",
    "outDir": "dist",
    "lib": ["dom", "esnext"],
    "declaration": true,
    // "moduleResolution": "node",
    "sourceMap": true,
    "strict": true,
    "esModuleInterop": true
  }
}
  • moduleResolution:node:如果你不使用 ES modules,那么你应该设置此属性

修改包配置启用编译

package.json 示例如下

{
  "name": "hello-world",
  "version": "1.0.0",
  "main": "dist/index.js",
  "type": "module",
  "scripts": {
    "prebuild": "rm -rf dist && tsc",
    "build": "babel src --out-dir dist --extensions '.ts,.tsx' --delete-dir-on-start --source-maps"
  },
  "files": ["src", "dist"],
  "peerDependencies": {
    "antd": "^4.0.0"
  }
}
  • files 里面为啥要包括 src 目录? - 为了方便 sourcemap
  • antd 为什么要放在 peerDependencies 里? -

    1. 为了防止和业务方的 antd 仓库冲突;
    2. 为了防止 antd 仓库的多次安装

顺便提一嘴仓库在使用 dependencies 声明依赖库的特点,来表明仓库使用 peerDependencies 的重要性

  • 业务方显示依赖的核心库,会忽略仓库的 peerDependency 声明
  • 业务方没有显示依赖核心库,则按照插件的 peerDependency 中声明的版本将库安装到项目根目录中
  • 当业务方依赖的版本和仓库依赖的版本不一致,会报错让用户修复

所以一般会有冲突的包,比如react-domreactantd等都可通过放在 peerDependency 中引入,就不会和业务方的版本冲突啦!

那么问题来了,peerDependency 在开的时候很影响开发体验啊,npm install不会安装它。解决方法如下:

  1. npm i -D install-peers-cli
  2. 在 npm scripts 中添加 "prepare": "install-peers",,prepare 会在每次npm install后执行,非常 nice~

当然,你也可以既兼容 cjs,也可兼容 esm。去除 type:module,添加 module: esm/index.js 为 ESM 的入口,main:dist/index.js 为 Commonjs 的入口,如果业务方不支持 esm 会进入到 main 入口哒~

处理样式文件

有以下四种方案:

  1. 告知业务方增加 less-loader。会导致业务方使用成本增加;
  2. 打包出一份完整的 css 文件,进行全量引入。无法进行按需引入;
  3. css in js 方案;
  4. 每个组自己提供一份 css.js 文件,引入该组件 css 样式依赖,而非 less 依赖,组件库底层抹平差异。

对于组件库来说,如果需要开放出去,最好考虑 3 和 4 方案,优缺点可看我参考的 React 组件库编译打包文章

由于我这里是公司内部使用,采用一方案了直接拷贝 less 文件:

在 babel 参数添加 --copy-files ,会自动将不符合编译条件(未编译)的文件拷贝到目标文件夹中,这样做的缺点很明显,若业务方没使用 less 预处理器,使用的是 sass 方案甚至原生 css 方案,那现有方案就搞不定了。

babel src --out-dir dist --extensions '.ts,.tsx' --delete-dir-on-start --source-maps --copy-files

如果需要在保持开发体验的同时,减少业务方使用的成本。那就最好采用 2,3,4 方案。如果还需要按需加载 css 样式的要求,那就只能考虑 3,4 方案了。4 方案是 antd 采用的方案,但是需要业务方自己手动管理依赖,但是也可以通过 babel-plugin-import 插件来自动帮我们管理依赖。看了以下示例你们就懂第四种的方式了。

假设我们有以下文件:

  • index.js (引入 styles.less)
  • Button.jsx
  • styles.less
  • css.js (引入 styles.css,由 index.js 生成,最后提供给业务方使用)
  • styles.css (styles.less 编译生成,最后提供给业务方使用)

css.js

import "style.less"

Example.jsx 文件中不引入样式文件,如果业务方要使用我们的组件,他们需要

import { Button } from "ui"
import "ui/Button/css"

我们需要构建的时候就可以抹平这些差异,将styles.less编译成styles.css,再将css.js中的内容替换成import styles.css就可以了。这样不仅可以兼顾开发体验和使用成本,还能按需加载样式,但是增加了使用成本,下面会介绍 babel-plugin-import 插件。

按需加载

按需加载的原理是结合 ES module 和 tree-shaking 特性实现

说到按需加载,就不得聊聊 package.json 中的sideEffects属性和 babel 的babel-plugin-import 插件

  • babel-plugin-import 插件是将类似import {array} from lodash 转化为import array from lodash/array,改变组件的引用路径实现按需加载,如果你的组件库有个统一的导出出口 index.js 就没必要用此插件了
  • sideEffects 是告诉 webpack 等打包工具,哪些文件是有副作用的,没办法被安全 tree shaking

sideEffects

说一下它常用的两个类型

  • false:标记哪些文件没有副作用,则表示组件库的所有 js 文件都没有副作用,可以用路径替换的方式放心实现 tree-shaking。
  • 数组:数组内的文件都标记为有副作用的,打包工具不能将这些文件 shaking 掉。有个常见的 bug 就是样式丢失,如果不设置 sideEffects:[.less,.css],如果你在业务代码中import styles.less,会导致最后打包的文件没有你想要的样式,因为打包工具认为没有地方引用到了该文件的内容。

babel-plugin-import

该插件决定将哪个库通过改变路径来实现 tree-shaking,有个关于样式按需加载的属性是style,当我们不想通过css in js的方案进行样式加载的时候,我们需要告知业务方,你引入了我们的组件,但是你也要引入它的样式,也就是我们第四种方案。

import { Button } from "ui"
import "/ui/Button/style"

但是这样对于业务方来说,他们不可能引入一个组件就引入一个样式啊,这样的使用体验也太不好了。当然,组件库也可以导出一个完成的样式文件,只引入一次就够了,但是这又与我们的按需加载相去甚远。此时,告知业务方去使用 babel-plugin-import 的 style 属性就很重要。

通过插件源码可以发现:

else if (style === true) {
  addSideEffect(file.path, `${path}/style`);
} else if (style === 'css') {
  addSideEffect(file.path, `${path}/style/css`);
}

style属性的作用是当你引入组件库的时候,自动在业务组件中插入 import Button/styleimport Button/style/css 代码来按需引入样式的 js 文件

注意:两者的区别,style: true在编译时引入源文件而style: “css”在打包前按引入源文件。在编译时引入文件能有效利用 tree-shaking 减少 bundle 体积,而在打包前不能,这是 ES module 静态 import 的特性。

Q & A

  1. 为什么引入 Button 组件的样式不是直接引入 style.less,而是通过 css.js 文件做了个代理?

    • 管理样式依赖,保障组件库开发者的开发体验 DX;

      如果 Button 引用了 Icon 组件,那么使用方每次引入 Button 也需要去引入 Icon 的样式,如果我们拿 css.js 做一个统一的出口,那么只需要引入一次 css.js 就可以引入 Button 组件及其依赖组件的样式了。依赖复杂的情况下开发效率及其高效。

  2. 为什么不在 Button 中直接引入import style.less来引入 css 文件呢,这样外部就不用考虑 css 的按需加载了?

    • 减轻业务方的使用成本。

      这样的话就需要业务方配置好 less-loader,如果业务方不使用 less-loader,在组件库内直接使用 import style.css 的开发体验岂不是直线下降?所以需要业务方引入 css.js 文件,css.js 文件引用的是编译好的 css 样式文件依赖,而不是 less 依赖,组件库底层抹平差异。

参考

Published under  on .

Last updated on .

pipihua

我是皮皮花,一个前后端通吃的前端攻城狮,如果感觉不错欢迎点击小心心♥(ˆ◡ˆԅ) star on GitHub!