第一章 从零开发一个JavaScript库
- 写什么功能的框架,灵感可以来自于工作中。举例:做一个深拷贝工具库。
- 编写深拷贝代码:
function clone(data) {...}
- 这样写会出现的问题:
- A使用了CommonJS模块,不知道该如何引用这个库。
- B说这个库的代码在IE浏览器中会报错。
第二章 构建
1. 模块化解析,ECMAScript 2015(即ES6)带来了原生模块规范。
-
什么是模块:独立空间,能引用其他模块,也能被其他模块引用。
C - 宏编译
C++ - 命名空间
Python - 模块
Java - 包
PHP - 命名空间 -
原始模块(ES6之前)
(function (module, \$){ function clone(source) {...} mode.clone = clone; }((window.clone = window.clone || {}), jQuery);
代码通过函数参数注入,需要手动维护依赖的顺序,所以当时的jQuery需要先于代码被引用,否则会报错。随着模块增加,会变得不可维护。
-
AMD(Asynchronous Module Definition)异步模块加载规范
用于浏览器,需要借助RequireJS才能加载AMD:
define(id?, dependencies?, factory);
示例代码:define(function() { function clone(source) {...} return clone; });
define(['clone'], function(clone){ const a = { a: 1 }; const b = clone(b); //使用clone函数 });
-
CommonJS同步模块加载规范 主要用于Node.js环境:
define(function(require, exports, module) {...} //系统自动生成,无需自己写
示例代码:function clone(source) {...} module.exports = clone;
const clone = require('./clone.js'); const a = { a: 1 }; const b = clone(b); //使用clone函数
-
UMD(Universal Module Definition)
是原始模块、AMD、CommonJS三种的整合
示例代码:(function(root, factory) { var clone = factory(root); if (typeof define === 'function' && define.amd) { // AMD define('clone', function() { return clone; }); } else if (typeof exports === 'object') { // CommonJS module.exports = clone; } else { // 原始模块 var _clone = root.clone; clone.noConflict = function () { // 使用noConflict解决全局名称冲突 if (root.clone === clone) { root.clone = _clone; } return clone; }; root.clone = clone; } }) (this, function(root) { function clone(source) {...} })
-
ES Module(ES6原生模块系统) 示例代码:
export function clone(source) {...} module.exports = clone; import { sortedLastIndexOf } from 'lodash';
import { clone } from './clone.js'; const a = { a: 1 }; const b = clone(b); //使用clone函数
-
小结:开源库需要对上述每种模块提供支持
入口文件 支持的模块 index.js 原始模块、AMD模块、CommonJS模块、UMD模块 index.esm.js ES Module
2. 技术体系解析
一般一个库会依赖另外多个库,依赖关系会很复杂。
- 传统体系
<script src="lib/clone.js"></script>
- Node.js体系,遵循CommonJS规范
先在package.json
中定义入口文件:"main": "index.js"
const clone = require('./lib/clone.js'); const a = { a: 1 }; const b = clone(b); //使用clone函数
- 工具化体系
先在webpack.config.js
中定义入口文件和输出文件
现在主流的构建工具(Webpack,Rollup等)均已支持CommonJS & ES Module,打包时需要知道那个库时支持哪个规范的,所以需要在库的const path = require('path'); module.exports = { entry: './index.js', //入口文件:src/index.js output: { filename: 'index.js', path: path.resolve(__dirname, 'dist'), //输出文件:src/dist/index.js } }
package.json
中定义好。{ "main": "index.js", "module": "index.esm.js", "jsnext": "index.esm.js" }
module.exports = { //... resolve: { mainFields: ['module', 'main'], //打包工具优先使用module字段 } }
function clone(source) {...} module.exports = clone;
export function clone(source) {...}
- 小结:打包工具会优先查看库是否支持ES Module,如果不支持,会遵循CommonJS规范。
技术体系 模块规范 依赖库的处理逻辑 传统体系 原始模块 依赖打包 Node.js体系 CommonJS 无须处理 工具化体系 ES Module & CommonJS 无须处理
3. 打包方案
库的适配手动设置太麻烦,可以用打包工具自动完成,开源库需要支持浏览器、打包工具和Node.js环境,以及不同的模块规范,所以需要提供不同的入口文件。
浏览器(script、AMD、CMD) | 打包工具(wepack、rollup.js) | Node.js | |
---|---|---|---|
入口文件 | index.aio.js | index.esm.js | index.js |
模块规范 | UMD | ES Module | CommonJS |
自身依赖 | 打包 | 打包 | 打包 |
第三方依赖 | 打包 | 不打包 | 不打包 |
-
选择打包工具
webpack打包会生成很多冗余代码,对于业务代码来说问题不大,但是对于库来说就太不友好了,所以选择rollup.js。 -
打包步骤
Roolup的三个配置文件打包输出文件 配置文件 技术体系 模块规范 dist/index.js config/rollup.config.js node.js CommonJS dist/index.esm.js config/rollup.config.esm.js webpack ES Module dist/index.aio.js config/rollup.config.aio.js 浏览器 UMD module.exports = { input: 'src/index.js', output: { file: 'dist/index.js', format: 'cjs', // CommonJS } }
module.exports = { input: 'src/index.js', output: { file: 'dist/index.esm.js', format: 'es', // ES Module } }
//需要把第三方依赖都打包,需要安装插件 var nodeResolve = require('rollup-plugin-node-resolve'); module.exports = { input: 'src/index.js', output: { file: 'dist/index.aio.js', format: 'umd', // UMD }, plugins: [ nodeResolve({ main: true, extensions: ['.js'], }), ] };
再简化打包的命令。
-
添加banner
开源库顶部用于提供一些关于库的说明,如协议信息等,也可以配置。var pkg = require('../package.json'); var version = pkg.version; var banner = `/*! * ${pkg.name} ${version} * Licensed under MIT */ `; exports.banner = banner;
module.exports = { input: 'src/index.js', output: { file: 'dist/index.esm.js', format: 'es', banner: common.banner, } }
-
按需加载
Side Effect副作用:如果库里有代码:window.aaa=1;
,如果引用了该库,就会向windows写入一个变量,打包工具如果把这段代码屏蔽,则可能产生Bug。 如果库里没有副作用,打包工具则可以使用treeshaking来优化了。
{ "sideEffects": false }
4. 兼容方案
-
确定兼容环境
- 对于JavaScript:
浏览器:Chrome,Firefox,IE,Edge,Safari
移动端兼容性比浏览器好所以以浏览器兼容性为标准\ - 对于Node.js:
推荐如下表环境 版本 Chrome 45+ 版本较低是因为有些浏览器包装了Chromium的较低版本 Firefox 最近两个版本 IE 8+ Edge 最近两个版本 Safari 10+ Node.js 0.12+
- 对于JavaScript:
-
ES5兼容方案
ES5之前的特性是非常安全的。
ES5及之后的版本可能存在兼容性的问题。
ES5在IE8浏览器上的兼容问题,可以引入polyfill。 -
ES6兼容方案
把ES6转换成ES5,转换工具:Babel。
Babel为每个ES6的特性提供了一个插件,这样可以让开发者自己选择要转换哪些属性。手动维护需要转换的特性是比较繁琐的,这里推荐使用Babel的preset-env插件,通过配置,自动选择相应的插件。在rollup中使用Babel需要配置plugins,代码如下:function getCompiler(opt) { return babel({ babelrc: false, // 不使用独立的Babel配置文件 presets: [ [ '@babel/preset-env', { targets: { browsers: 'last 2 versions, > 1%, ie >= 8, Chrome >= 45, safari >= 10', node: '0.12', }, modules: false, // 不使用独立的Babel配置文件 loose: true // 为了兼容IE8,而避免使用Object.defineProperty特性 }, ], ], exclude: 'node_modules/**', }); } exports.getCompiler = getCompiler;
转换成ES5还是会有兼容性的问题,平时在项目中可以引入polfill,但对于库来说,会污染全局环境。core-js是一个ES5的polyfill库,提供了不污染全局环境的使用方式。Babel集成了core-js,需要安装两个插件,在配置使用。
5. 完整方案
能够解决库的开发者和库的使用者之间的矛盾:
1. 库的开发者编写ES6新特性代码。
2. 库的使用者能够在各种浏览器(IE6 ~ IE11)和Node.js(0.12 ~ 18)中运行我们的库。
3. 库的使用者能够使用AMD、CommonJS或ES Module模块规范。
4. 库的使用者能够使用webpack、rollup或PARCEL等打包工具。
编译和打包流程如下:
graph TD
ES6+ --编译--> Babel --> ES5 --打包--> rollup.js --> index.aio.js --> 浏览器RequireJS
rollup.js --> index.esm.js --> webpack/rollup.js/PARCEL/Vite
rollup.js --> index.js --> Node.js
6. 小结
mindmap
JavaScript库的构建与打包
JS需要支持的模块
原始模块
AMD
CommonJS
UMD
ES Module
JS需要支持的前端技术体系
传统体系(手动引入)
Node.js体系
工具化体系
JS的构建方案: Rollup.js
JS的兼容方案: Babel