从dist到es:发一个NPM库,我蜕了一层皮

37次阅读

共计 5010 个字符,预计需要花费 13 分钟才能阅读完成。

这并不是自己第一次发 npm 包,所以这里没有多少入门的知识。在此之前已经有一篇前端脚手架,听起来玄乎,实际呢?,但这一次的 npm 包和上一次的不是一个概念,前者只是一个脚本工具,而这个 npm 包是日常开发中方法和组件的集合, 是一个库。在读本文前,假定你已经对 npm 包有一定概念,熟悉 Babel 编译和 webpack 打包的常规用法,知道一些前端工程化的知识。假如你也想自己发布一个 npm 仓库,但对这一块了解的不是很多,推荐 webpack 官方的创建一个 library
打包模式:日常构建与库的构建
在前端日常开发中,引入 npm 库,执行 webpack 构建已经是一件不能再平常的事情。但大多数时候,我们不关心这个 npm 库是怎样构成的,我们只需要知道怎么使用,像 antd;在工程化成熟的公司,也不关心 webpack 的配置到底是怎样的,只需要 npm run start 或 npm run build 去启动一次热加载或打包。但是如果你是编写一个 npm 仓库,这些东西你都需要知道。从那时的无知说起,起初,我用公司的构建工具(类似于 roadhog)去打包我的库,没有坎坷,构建出一个 2M 多的包并成功发布。

在测试项目中引入,构建成功。
import {EnhanceTable, WithSearch} from ‘antd-doddle’; // 引入仓库
在浏览器中打开,打击开始到来:提示要引入的对象没有正确导出,就是没有做 module.export,所以这是一个打包模式的问题,output.libraryTarget 需要了解一下。
webpack 的 output.libraryTarget 决定了打包时对外暴露出来的对象是那种模式,默认是 var,用于 script 标签引入,该模式也是我们日常开发构建最常用的模式,除了这一种,还支持的常用选项有:

commonjs(2):node 环境 CommonJS 规范,关于 commonjs 与 commonjs2 的区别,commonjs 规范只定义了 exports,而 module.exports 是 nodejs 对 commonjs 的实现,实现往往会在满足规范前提下作些扩展,所以把这种实现称为了 commonjs2;
amd:amd 规范,适用于 requireJS;
this:通过 this 对象访问(libraryTarget:’this’);
window:通过 window 对象访问,在浏览器中(libraryTarget:’window’)。
UMD:将你的 library 暴露为所有的模块定义下都可运行的方式。它将在 CommonJS, AMD 环境下运行,或将模块导出到 global 下的变量,(libraryTarget:’umd’)。
jsonp:这是一种比较特殊的模式,适用于有 extrnals 依赖的时候 (splitChunks)。将把入口起点的返回值,包裹到一个 jsonp 包装容器中。

所以在这里我们需要设置两个属性来明确打包模式
library: ‘antd-doddle’,
libraryTarget: ‘umd’,

2M 到 38KB, 这中间发什么了什么
上图是用 roadhog 打包出来的结果,其显示的是开启 gzip 后可以压缩到的大小,第一次打包的实际大小大概在 2M(antd+moment+react+css),后面仔细一想,公司的组件库也才 300kb 啊,自己是不是哪里搞错了,所以接着就有了下面的探寻之路。

抽离 css(300kb), 由于此 npm 库是基于 antd 的,所以就没有再把 antd 的 css 打包一次的必要了。基于 roadhog 给予的提示,配置了 disableAntdStyle 为 false,css 文件降到 2kb;
接着上面,虽然是基于 antd 的,但并没有完全用到 antd 的所有组件,其官方提供了一个按需打包 babel 插件 babel-plugin-import,并在 babelrc 中配置, js 打包体积由 1.6M 降为 1.2M;

[“import”, {
“libraryName”: “antd”,
“libraryDirectory”: “lib”,
“style”: “css”,
}]
如果对 webpack 多了解一下,或者在写一个库之前读过 创建一个 library,就会发现前面两点都是白扯没有用的,因为对于这个库来说 antd 就是一个外部依赖(externals),正好 roadhog 又支持, 打包出来,由 1.2M 变为 38kb, 这是一个质的提升。
externals: {
react: {
commonjs: ‘react’,
commonjs2: ‘react’,
amd: ‘react’,
},
antd: {
commonjs: ‘antd’,
commonjs2: ‘antd’,
amd: ‘antd’,
},
moment: {
commonjs: ‘moment’,
commonjs2: ‘moment’,
amd: ‘moment’,
},
}
打包大小优化至此就搞定了,但后面发现用 roadhog 打包库有一些很难解决的难题,为了解决还得去了解他源码逻辑,所以后面还是自己写了一个 webpack,非常简单的配置。
ES6 之后,光有 dist 是不够的
在写这个库之前,我曾想到在我们日常构建时有下面这样一段配置:
rules: [{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
loader: ‘babel-loader’,
query: {
presets: [‘@babel/preset-env’, ‘@babel/preset-react’]
}
}
这段配置是告诉 webpack,node_modules 中引用的代码不需要再由 babel 编译一次,但这些代码还是会被打包进 dist 文件的。在现在前端的主流开发套路中,被引用的库更希望是一个只编译而没有被打包过的,支持按需加载的库。所以 dist 中被编译打包过的代码再次被打包, 这样就会有不必要的代码出现。所以这样来看,我们只需要将代码编译。babel, 没错,就是它,但是多文件编译,还是找个第三方构建工具比较好,我选择了 gulp,直接上代码:
// 发布打包
gulp.task(‘lib’, gulp.series(‘clean’, () => {
return gulp.src(‘./src/**/*.js’)
.pipe(babel())
.pipe(gulp.dest(‘./lib’));
}, ‘lessToLib’)); // lessToLib 用于将 less 文件拷贝贷 lib 文件夹
编译过后大概是下面这样的,确实只编译没打包,函数基本原样:

本以为到这就结束了,但是这才开始。只编译不打包消除不必要的代码只是很小的原因,重要的东西我觉得换一行说比较好。不顾语文老师的责骂换行,那什么才是是最重要的:按需打包(tree shaking),对于这种组件和方法库,作为使用者,我们希望他能支持按需打包,像 lodash 和 antd 这样。所以怀着好奇的心理我去看了他们的 package.json, 然后发现了这样的配置:
“main”: “lib/index.js”,
“module”: “es/index.js”,
“name”: “antd”,
除了认知中的 main 入口定义,还多了一个 module 入口. 为什么需要这样呢,和我一样无知的,可以先读 webpack 官方的 tree shaking,如果不够直观,可以再看一位大佬写的一篇相关文章聊聊 package.json 文件中的 module 字段。看下面代码:
// es6 模块写法 fun.js
export function square(x) {
return x * x;
}

export function cube(x) {
return x * x * x;
}

// commonJs 写法 fun.js
exports.square = function(x) {
return x * x;
}

exports.cube = function(x) {
return x * x * x;
}

// index.js 引入
import {
square
} from ‘./fun.js’;

const res = square(10);
console.log(‘res:’, res);
简单来说,就是 index.js 打包编译时,引入 commonJs 写法的 fun.js, 打包会将 square 与 cube 两个函数同时打进来。而引入 es6 写法的 fun.js,只会将 square 打包。这样的操作,对于现在的主流趋势,就是必须的优化,特别对于 lodash 和 antd 这种庞大的库。而要使我们的库支持这样的操作,我们需要编译时,禁止 babel 将 es6 的 module 引入方式编译,其实只需要在前面的基础上多配置一个参数:
“@babel/preset-react” // lib 的打包方式

[“@babel/preset-env”, { “modules”: false}] // 保留 es6 模块引入的方式
得到的是下面这样的结果:

和上面的 lib 对比,感觉更接近原始代码。至此,编译已结束,但是我们还需要在 package.json 中加上相应的配置:
“description”: “antd 后台项目前端组件封装和方法库 ”,
“main”: “lib/index.js”,
“module”: “es/index.js”,
“scripts”: {
“build”: “webpack –config webpack.config.js”,
“lib”: “gulp lib”,
“es”: “gulp es”,
“prepublish”: “gulp && webpack –config webpack.config.js”
},
“files”: [
“es”,
“dist”,
“lib”,
“utils”
],
怎么让库支持多目录输出
因为我的库主要包括组件和方法,我把方法放到一起,通过 utils 作为默认输出。然后项目中引入是这样的:
import {EnhanceTable, WithSearch}, utils from ‘antd-doddle’;

// 要用里面的方法需要再分解一次或通过 utils.xxx
const {DATE_FORMAT, idCodeValid} = utils;
虽然感觉上不复杂,但是总感觉别扭,如果你用过 dva,就见过下面这样的引入:
import {routerRedux} from ‘dva/router’;
import dva from ‘dva’;
所以我去学习了一下,发现要这样实现也不难分三步,分目录打包,增加一个输出,并增加内部私有映射,package.json 增加一个这个映射目录的输出。具体可查看项目源码。实现后,项目引入是这样的:
import {EnhanceTable, WithSearch}, utils from ‘antd-doddle’;
import {DATE_FORMAT, idCodeValid} from‘antd-doddle/utils’; // 一步到位
小技巧分享
npx 执行本地命令
以前我们很多命令如 webpack,gulp 命令只有在全局安装(npm install xxx -g)才可以在命令行中直接运行或在项目中安装,通过 script 定义执行,但在 npm5.2 以后,我们可以只项目中安装,然后通过新增的 npx 执行。比如上面 scripts 中定义的 lib 打包(”lib”: “gulp”),我们可以直接在命令行中用:
npx gulp
命令行切换 npm registry
有可能你和我一样,在到处都是墙的世界,需要在 npm,cnpm, 公司的 npm registry 三者之间来回切换,每次都需要这样:
npm set registry ‘https://registry.npm.taobao.org/’
麻烦有没有? 幸好,这世界有很多牛逼的人,nrm registry 是个很好用的工具,下面这样:
// 安装
npm install -g nrm
// 设置入口 npm,cnpm,company
nrm add npm ‘http://registry.npmjs.org’
nrm add cnpm ‘https://registry.npm.taobao.org’
nrm add vnpm ‘http://npm.company.com’
// 切换入口到淘宝入口
nrm use cnpm
后续
一个春节自己断断续续就在倒腾这个,收获还是挺大的。后面自己会慢慢去学习怎么加入 demo‘,加入单元测试,去建造一个完整的 npm 库。源码库:githubnpm 仓库地址:npm

正文完
 0