前言
随着前端的不断发展,很多新特性出现的同时,越来越多的构建工具也如雨后春笋般冒了出来,那么不论是在日常的工作中,还是在平常自己的开源项目中,构建npm包的时候,大家首先想到的就是rollup来构建我们的npm包,但是为什么rollup目前会成为首选,还可以选择哪些工具来构建我们的npm包,希望读者通过阅读本篇能够得到答案,同时也能选择合适的构建工具来构建自己的npm包
内容分为四个部分
- npm包的运用场景分析
- 构建工具发展简介
- 选择npm包构建工具
- FAQ
npm包运用场景分析
在选择构建工具之前,先大致了解下npm包有哪些常用的使用场景,各个场景有什么明显的特征,知道了真实的运用场景之后,在结合实际的构建工具,能够快速构建出适合自己的npm包
首先我们知道npm包的作用,主要就是逻辑复用,将一些公共的内容,以npm包的形式组织起来,方便其它项目内使用
那么我们可以根据常用的使用场景做出如下分类 从上面可以看出,运行在nodejs端的npm包要求更低,运行在web端的npm包要求更高一点
而npm包的输出也围绕在输出模块格式、polyfill、包大小等上面,下面重点介绍两个点
模块格式
因为nodejs端项目是同步加载模块,而浏览器端项目是通过网络请求异步加载模块,在esm模块没有出来之前,二者之间使用的模块格式是有差异的,差异体现在,nodejs端的npm包使用cjs模块格式开发,且最终输出cjs格式的模块,web端的npm包使用amd or cmd or iife格式开发,且最终输出amd or cmd or iife格式的模块,而umd则是兼容amd or cmd or iife的一种格式
伴随着es6模块规范的发布,javascript有了自己的标准模块规范,于是不论是nodejs端、web端的npm包开发都是采用esm模块格式进行开发,为了向前兼容输出esm与cjs两份模块格式,这也就是目前看到的npm包大部分都是输出了两种模块格式的原因,如下图所示
polyfill
polyfill是什么?
polyfill的英文意思是填充工具,意义就是兜底的东西;为什么会有polyfill这个概念,因为ECMASCRIPT一直在发布新的api,当我们使用这些新的api的时候,在旧版本的浏览器上是无法使用的,因为旧的版本上是没有提供这些新的api的,所以为了让代码也能在旧的浏览器上跑起来,于是手动添加对应的api,这就是polyfill,如下图所示;
手动引入polyfill的方式除非我们知道自己只需要引入哪种polyfill,不然一般都推荐通过babel or swc自动引入polyfill代码,如下图所示
web项目与npm包之间的polyfill区别吗?
有
首先web项目是作为入口访问的,所以polyfill的时候,可以全局引入,可以按需引入,可以以污染的方式引入,因为影响范围就是本项目,而作为npm包是作为一个部分被项目引用的,那么肯定要尽可能少的对引用的项目产生影响,所以polyfill的选择上会更谨慎,因此有了无污染的polyfill方式,当然并不是说npm内就一定的选择无污染的polyfill方式,只不过选择无污染的polyfill方式对引用的项目影响范围最小
全局polyfill: 直接引入所有的polyfill代码
import "core-js";
按需polyfill: 仅引用代码中需要用到的polyfill代码
import 'core-js/modules/es.array.iterator';
import 'core-js/modules/es.object.to-string';
import 'core-js/modules/es.set';
var set = new Set([1, 2, 3]);
有污染方式的polyfill: 直接修改的是全局方法
import 'core-js/modules/es.array.of';
var array = Array.of(1, 2, 3);
无污染方式的polyfill:不会修改全局方法,仅影响引用的代码部分
import Set from 'core-js-pure/stable/set';
import Promise from 'core-js-pure/stable/promise';
new Set([1, 2, 3, 2, 1])
Promise.resolve(32).then(x => console.log(x));
更多polyfill内容,可以查看笔者之前总结的babel polyfill指南及深入理解polyfill
构建工具发展简介
在了解npm包的运用场景之后,接着了解下这些构建工具大概在什么时候出现,又是为了解决什么问题,因为这些构建工具的出现不仅针对项目场景,也针对任何需要打包构建的场景,所以在了解了之后,我们在做选择的时候,会有更多的参考与选择
ES5时期
首先是早期的webpack,webpack在es5时期就已经出现,当时网络性能还不怎么好,为了加快用户的访问速度,将产物打包成bundle,而当时还没有 javascript 语言标准的模块规范,所以webpack构建的产物包含了自己写的模块规范,webpack的bundle过程如下所示
webpack不仅能够构建项目,还能够构建npm包,另外就是gulp与grunt这两个工具在结合一些相应的插件也可以构建项目与npm包,但是随着时间的流逝,glup与grunt渐渐被淘汰了
ES6时期
随着时间来到2015年,es6发布,es6不仅包含了新语法,还包含了javascript一直没有的模块规范,es module。随着es module的出现,rollup诞生了,rollup依赖es module带来了更好的tree-shaking,可以有效的减少包体积,同时rollup输出的代码更清爽,是A就是输出A,不像webpack包含很多模块加载的胶水代码;因此rollup迅速占领了npm包构建场景。rollup的bundle过程如下所示
于此同时随着javascript的大量运用,一些大佬觉得动态语言,太灵活了,需要向静态语言看齐,于是2012年typescript发布了第一个正式版本,在javascript之上引入静态类型,但是从2015才被开始大量使用,伴随着typescript出现的还有自身的解释器tsc,tsc专门负责ts类型检查,将typescript转化成javascript,生成对应的类型文件等,tsc处理过程如下所示
项目里面要使用typescript,需要借助ts-loader这类封装了tsc的loader才能处理typescript,npm包场景下要使用typescript需要直接使用typescript or rollup-plugin-typescript这样基于typescript的插件
前面提到的随着es6的出现,不仅是javascript有了自己的模块规范,同时还带来了新的语法与API、但是此时很多浏览器还不支持新的语法与API,怎么办?于是又出现了babel这样的转化工具,专门处理javascript语法转化及按需添加polyfill代码,babel处理过程如下所示
后面 babel 又直接支持了typescript转化成javascript的能力,但是不支持typescipt类型检查与类型文件生成,过程如下所示
高性能时期
到了2020年左右,webpack、rollup、typescript、babel这些工具已经很成熟了,javascript中的构建工具链已经趋近完善,我们在开发中需要实现的功能都可以通过上面的工具达成,那是不是社区就没事做了,不是的,社区开始卷性能,开始通过其它性能更高的语言来实现等效功能的工具,比如使用rust写的swc,旨在替换babel,go写的esbuild,旨在替换webpack、rollup这样的构建工具,等等还有其它,而这些工具带来的性能提升巨大,使用场景也是越来越多,不仅用户本身对这些工具的使用,同时之前基于javascript编写的工具,也在自己的环节或者底层引入了这些工具,比如vite内部就使用了esbuild,rollup在4.0版本使用了swc替换acron来解析ast等
比如swc进行语法转换与polyfill过程,如下所示
比如esbuild构建bundle过程,如下所示
那么我们作为一个普通开发者应该怎么做,我个人的做法是积极拥抱变化,不断尝试,并总结使用经验
当我们了解了构建工具的大致由来,那么我们接着往下看构建npm包,应该怎么选择构建工具
选择npm包构建工具
如何选择构建工具
首先我们看下npm包资源的关系图,如下图所示 有些同学可能不需要构建工具到output这一步,直接将input发布即可,但是更多的场景我们还是会经历构建output这一步,原因有以下几点
- input现在大部分都是通过typescript编写,需要将ts转成js,并输出类型声明文件
- 浏览器兼容性考虑,需要对npm包输出es5及polyfill
- 方便浏览器端通过script方式直接加载,需要输出bundle形式的umd模块
等等还有其它的原因,就不一一列举
那么当我们需要构建输出这一步之后,对于构建工具的选择有多种,那么我们应该怎么去选择合适的构建工具,帮助我们快速、高效的构建出产物
我个人的理解,就是理清楚输入有哪些场景,输出有哪些场景,各个构建工具有什么优缺点,通过三者结合,就可以快速找出适合自己场景的构建工具
上面是npm包输入与输出的一些场景,那么构建工具的能力有哪些
从上面能够看出,支持场景最多的是rollup与vite,而vite是基于rollup的封装;其次是webpack支持的场景最多,但是webpack不支持原目录格式输出多文件,且输出esm模块格式还是实验特性;在其次是esbuild,但是esbuild不能输出es5、不支持按需polyfill、不支持emitDecoratorMetadata,在其次是swc,但是swc不支持less、saas等、不支持图片处理等,不能够处理.vue;最后就是typescript,不支持less、saas等,不能够处理.vue,不能进行按需polyfill,也不能生成单bundle.js
(为什么不借助babel or swc原因是代码本身已经被esbuild转化如果在转化一次不合算)
整理之后的表格如下所示
构建工具/功能 | 单entry | 多entry | 输出单bundle | 原目录输出多文件 | 处理ts、tsx | 处理.vue | 输出es5 | 处理css | 处理less、saas | 输出esm | 动态polyfill |
---|---|---|---|---|---|---|---|---|---|---|---|
rollup | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
vite | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
webpack | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
esbuild | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ |
swc | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ |
typescript | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ |
备注:这里的能处理,并不一定指这个构建工具自己本身能处理,而是有对应的插件能够处理。webpack不能支持原目录输出格式,原因是虽然可以通过多入库打包每个文件,但是如果文件内有引用关系的话,是会被打进去的
到这里我们已经知道各个构建工具适合的场景,下面用具体的案例进行示范
使用 swc 构建
先说下目标:
- 仅适用于nodejs端的包
- 输出esm模块
- 不需要polyfill,
- 不需要打包成bundle
- 输出es6语法
然后使用swc进行构建
源代码包含两个文件如下所示
import { getToken } from "./util";
export function getRandomToken() {
return `${getToken()}_${Math.random()}`
}
export function getToken() {
return 'xjskak8999'
}
首先,安装 SWC 作为开发依赖:
npm install --save-dev @swc/core
创建一个构建脚本,然后使用@swc/core编译ts文件
const fs = require('fs-extra')
const swc = require('@swc/core');
const glob = require('glob')
function transfrom(file, option) {
return swc
.transformFile(file, {
sourceMaps: false,
module: {
type: 'es6', // 输出esm模块
noInterop: true
},
jsc: {
parser: {
syntax: "typescript",
decorators: true,
},
transform: {
"legacyDecorator": true,
"decoratorMetadata": true
},
target: 'es2015' // 输出es6语法
},
...option
})
.then((output) => ({
file,
output,
}));
}
async function transformByswc({
entry,
dest = 'build',
option,
}) {
console.time('swc build');
// 需要排除.d.ts,避免覆盖同名的.ts文件
const files = Array.isArray(entry) ? entry : glob.sync(entry);
const result = await Promise.all(files.map((file) => transfrom(file, option)));
await Promise.all(result.map((item) => {
return fs.outputFile(item.file.replace('src', dest).replace('.ts', '.js'), item.output.code);
}));
console.timeEnd('swc build');
}
transformByswc({
entry: 'src/**/!(*.d).ts'
})
构建你的 npm 包,比如在 package.json 中添加一个构建脚本:
"scripts": {
"build": "node build.js"
}
构建结果如下所示
这样当我们的npm包发布之后,其它的项目内就可以通过import {getRandomToken} from '包名'使用我们npm包中提供的方法
使用 esbuild 构建
先说下目标:
- 仅适用于nodejs端的包
- 输出cjs模块
- 不需要polyfill
- 需要打包成bundle
- 需要包含node_modules中的依赖
- 需要压缩
源代码包含两个文件如下所示
import { getToken } from "./util";
export function getRandomToken() {
return `${getToken()}_${Math.random()}`
}
export function getToken() {
return 'xjskak8999'
}
首先,安装 esbuild 作为开发依赖:
pnpm add esbuild -D
创建一个build.js脚本
const esbuild = require('esbuild');
esbuild.build({
entryPoints: ['./src/check.ts'],
bundle: true, // 将所有文件打包到一个bundle文件里面
minify: true, // 压缩代码
target: ['node12'], // 生成的目标代码兼容node12
outfile: './build/check.js',
platform: 'node',
charset: 'utf8', // 保证中文不被转码
// packages: 'external' // 将node_modules下的依赖也包含进来
})
编写一个构建脚本,比如在 package.json 中:
"scripts": {
"build": "node build.js"
}
构建结果如下所示
这样当我们的npm包发布之后,其它的项目内就可以通过import '包名',我们的check逻辑就会自执行
使用 typeScript 构建
先说下目标:
- 仅适用于nodejs端的包
- 输出esm模块
- 不需要polyfill,
- 不需要打包成bundle
- 输出es6语法
首先,安装 yypeScript 作为开发依赖:
pnpm install --save-dev typescript
创建一个 TypeScript 配置文件(比如 tsconfig.json):
{
"compilerOptions": {
"target": "ES2015",
"module": "ems",
"outDir": "dist"
}
}
构建你的 npm 包,比如在 package.json 中添加一个构建脚本:
"scripts": {
"build": "node build.js"
}
使用 webpack 构建
先说下目标:
- 仅适用于浏览器端的包
- 输出umd模块
- 需要polyfill
- 需要打包成bundle
- 需要包含node_modules中的依赖
- 需要压缩
源代码包含两个文件如下所示
首先,安装 webpack 作为开发依赖
# 安装webpack依赖
pnpm add webpack webpack-cli -D
# 安装其它辅助依赖
pnpm add @babel/plugin-transform-runtime @babel/preset-env @babel/preset-typescript babel-loader @babel/runtime-corejs3 -D
创建一个webpack.config.js脚本
module.exports = {
mode: 'production',
entry: './src/index.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].min.js',
library: {
type: "umd",
name: 'MyLibrary',
}
},
resolve: {
extensions: ['.tsx', '.ts', '.jsx', '.js'],
},
optimization: {
minimize: true
},
module: {
rules: [
{
test: /\.[tj]sx?$/,
use: 'babel-loader',
exclude: /\bcore-js\b/
}
]
},
}
创建babel.config.js文件
module.exports = {
sourceType: 'unambiguous',
presets: [
[
"@babel/preset-env",
],
"@babel/preset-typescript",
],
plugins: [
['@babel/plugin-transform-runtime', {
'absoluteRuntime': false,
'corejs': 3,
'helpers': true,
'regenerator': true,
}]
]
}
在 package.json 中添加一个build命令
"scripts": {
"build": "webpack -c ./webpack.config.js"
}
构建结果如下所示
这样当我们的npm包发布之后,其它的项目内就可以通过script标签直接引用我们的js文件
使用 rollup 构建
先说下目标:
- 适用于浏览器端
- 输出cjs与esm模块
- 需要polyfill
- 不需要打包成bundle
- 不需要包含node_modules中的依赖
- 需要压缩
源代码包含两个文件如下所示
import { getToken } from "./util";
export function getRandomToken() {
return `${getToken()}_${Math.random()}`
}
export function getToken() {
return 'xjskak8999'
}
首先,安装 rollup 作为开发依赖:
pnpm add rollup @rollup/plugin-typescript @rollup/plugin-babel @rollup/plugin-commonjs @rollup/plugin-json @rollup/plugin-node-resolve -D
创建一个rollup.config.mjs脚本
import { getBabelOutputPlugin } from '@rollup/plugin-babel';
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
import resolve from '@rollup/plugin-node-resolve';
import typescript from '@rollup/plugin-typescript';
import pkg from './package.json' assert {type: 'json'};
export default [
{
input: './src/index.ts',
output: [{
file: pkg.main,
format: 'cjs',
exports: 'named',
},{
file: pkg.module,
format: 'esm',
exports: 'named',
}],
plugins: [
resolve(),
json(),
commonjs({
transformMixedEsModules: true,
}),
typescript(),
getBabelOutputPlugin(),
],
external: [], // 不需要打入包内的第三方npm包,例如['lodash']
}
];
创建babel.config.js
module.exports = {
"presets": [
[
"@babel/preset-env",
{
"debug": true
}
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"absoluteRuntime": false,
"corejs": {
"version": 3,
"proposals": false
},
"helpers": true,
"regenerator": true
}
]
]
}
在 package.json 中添加一个build命令
"scripts": {
"build": "rollup -c rollup.config.mjs"
}
构建结果如下所示
这样当我们的npm包发布之后,其它的项目内就可以通过import { getRandomToken } from '包名' 使用我们包提供的功能,同时由于我们的包提供了es module,那么webpack等构建工具在处理的时候,就可以进行tree-shaking,减少bundle体积
使用 vite 构建
先说下目标:
- 适用于浏览器端
- 输出esm模块
- 不需要polyfill
- 需要打包成bundle
- 不需要包含node_modules中的依赖
- 不需要压缩
源代码包含两个文件如下所示
import { getToken } from "./util";
export function getRandomToken() {
return `${getToken()}_${Math.random()}`
}
export function getToken() {
return 'xjskak8999'
}
首先,安装 vite 作为开发依赖:
pnpm add vite -D
创建一个vite.config.js脚本
import { defineConfig } from 'vite'
import { resolve } from 'path'
export default defineConfig({
build: {
target: 'es5',
lib: {
entry: resolve(__dirname, 'src/index.ts'),
},
minify: false,
rollupOptions: {
output: [{
format: 'es',
exports: 'named',
dir: './dist',
entryFileNames: '[name].bundle.js',
}],
},
},
})
在 package.json 中添加一个build命令
"scripts": {
"build": "rollup -c rollup.config.mjs"
}
构建结果如下所示
这样当我们的npm包发布之后,其它的项目内就可以通过import { getRandomToken } from '包名' 使用我们包提供的功能,同时由于我们的包输出的事es module,那么webpack等构建工具在处理的时候,就可以进行tree-shaking,减少bundle体积
demo地址npm-build-demo
FAQ
使用某个npm包之后页面白屏
其实我们在项目中安装了某个npm包之后,在低版本浏览器上访问出现白屏问题的概率是最高的,原因有二
- 我们在项目构建的时候,为了缩短构建时间,会排除babel-loader这类语法转换的loader对node_modules下的依赖包做处理
- 我们依赖的npm包,输出的是es6甚至更高的语法版本
这时候怎么办两个思路
- 如果这类npm包比较少,那么通过loader的exclude属性就可以完成,比如将exclude设置成
/node_modules[\\/](?!react-sortablejs)/,
排除node_modules下除react-sortablejs之外的npm包,也就是会处理react-sortablejs不会处理node_modules下的其它包 - 如果这里npm包比较多,那么直接处理node_modules下所有包,可能最终的产物在运行的时候报错,原因是node_modules下的有些包是不能够在使用babel-loader处理,这种场景就直接排除不能通过babel-loader处理的包即可,可以将exclude设置成
node_modules[\\/](core-js|core-js-pure|@babel|amfe-flexible|react-dom|react|css-loader|lodash|vconsole)
,实际的情况以自己的项目为准
总结
本篇首先讲了一下npm包的运用场景,然后简单介绍构建工具的发展过程,然后介绍了根据输入、输出、构建工具优缺点这三个要素来选择构建工具,最后又用具体的构建工具构建不同的demo
几种常用场景的最佳实践
- 目标1:不需要bundle、不需要polyfill、不需要处理样式、输出esm or cjs,比如仅支持nodejs场景的纯js工具包
- 构建工具选择:esbuild、swc、tsc、rollup、vite
- 目标2: 不需要bundle、需要polyfill、不需要处理样式、输出esm or cjs,比如仅支持浏览器场景的纯js工具包
- 构建工具选择:swc、rollup、vite
- 目标3: 需要bundle、需要polyfill、需要处理样式、输出umd,比如直接支持script标签加载的包
- 构建工具选择:rollup、vite、webpack
- 目标4: 需要bundle、需要polyfill、需要处理样式、输出esm or cjs,比如仅支持浏览器端的组件库包
- 构建工具选择:rollup、vite
更多场景,请参考如何选择构建工具一栏进行选择
如果你觉得此篇对你有所帮助那么就三连支持一下吧!