《现代JavaScript库开发》笔记 - 模块规范和构建

119 阅读3分钟

第一章 从零开发一个JavaScript库

  1. 写什么功能的框架,灵感可以来自于工作中。举例:做一个深拷贝工具库。
  2. 编写深拷贝代码: function clone(data) {...}
  3. 这样写会出现的问题:
    • A使用了CommonJS模块,不知道该如何引用这个库。
    • B说这个库的代码在IE浏览器中会报错。

第二章 构建

1. 模块化解析,ECMAScript 2015(即ES6)带来了原生模块规范。

  1. 什么是模块:独立空间,能引用其他模块,也能被其他模块引用。
    C - 宏编译
    C++ - 命名空间
    Python - 模块
    Java - 包
    PHP - 命名空间

  2. 原始模块(ES6之前)

     (function (module, \$){
         function clone(source) {...}
    
         mode.clone = clone;
     }((window.clone = window.clone || {}), jQuery);
    

    代码通过函数参数注入,需要手动维护依赖的顺序,所以当时的jQuery需要先于代码被引用,否则会报错。随着模块增加,会变得不可维护。

  3. 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函数
    });
    
  4. 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函数
    
  5. 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) {...}
    })
    
  6. 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函数
    
  7. 小结:开源库需要对上述每种模块提供支持

    入口文件支持的模块
    index.js原始模块、AMD模块、CommonJS模块、UMD模块
    index.esm.jsES Module

2. 技术体系解析

一般一个库会依赖另外多个库,依赖关系会很复杂。

  1. 传统体系
    <script src="lib/clone.js"></script>
    
  2. Node.js体系,遵循CommonJS规范
    先在package.json中定义入口文件: "main": "index.js"
    const clone = require('./lib/clone.js');
    
    const a = { a: 1 };
    const b = clone(b);  //使用clone函数
    
  3. 工具化体系
    先在webpack.config.js中定义入口文件和输出文件
    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
        }
    }
    
    现在主流的构建工具(Webpack,Rollup等)均已支持CommonJS & ES Module,打包时需要知道那个库时支持哪个规范的,所以需要在库的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) {...}
    
  4. 小结:打包工具会优先查看库是否支持ES Module,如果不支持,会遵循CommonJS规范。
    技术体系模块规范依赖库的处理逻辑
    传统体系原始模块依赖打包
    Node.js体系CommonJS无须处理
    工具化体系ES Module & CommonJS无须处理

3. 打包方案

库的适配手动设置太麻烦,可以用打包工具自动完成,开源库需要支持浏览器、打包工具和Node.js环境,以及不同的模块规范,所以需要提供不同的入口文件。

浏览器(script、AMD、CMD)打包工具(wepack、rollup.js)Node.js
入口文件index.aio.jsindex.esm.jsindex.js
模块规范UMDES ModuleCommonJS
自身依赖打包打包打包
第三方依赖打包不打包不打包
  1. 选择打包工具
    webpack打包会生成很多冗余代码,对于业务代码来说问题不大,但是对于库来说就太不友好了,所以选择rollup.js。

  2. 打包步骤
    Roolup的三个配置文件

    打包输出文件配置文件技术体系模块规范
    dist/index.jsconfig/rollup.config.jsnode.jsCommonJS
    dist/index.esm.jsconfig/rollup.config.esm.jswebpackES Module
    dist/index.aio.jsconfig/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'],
            }),
        ]
    };
    

    再简化打包的命令。

  3. 添加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,
        }
    }
    
  4. 按需加载
    Side Effect副作用:如果库里有代码:window.aaa=1;,如果引用了该库,就会向windows写入一个变量,打包工具如果把这段代码屏蔽,则可能产生Bug。 如果库里没有副作用,打包工具则可以使用treeshaking来优化了。
    { "sideEffects": false }

4. 兼容方案

  1. 确定兼容环境

    1. 对于JavaScript:
      浏览器:Chrome,Firefox,IE,Edge,Safari
      移动端兼容性比浏览器好所以以浏览器兼容性为标准\
    2. 对于Node.js:
      推荐如下表
      环境版本
      Chrome45+版本较低是因为有些浏览器包装了Chromium的较低版本
      Firefox最近两个版本
      IE8+
      Edge最近两个版本
      Safari10+
      Node.js0.12+
  2. ES5兼容方案
    ES5之前的特性是非常安全的。
    ES5及之后的版本可能存在兼容性的问题。
    ES5在IE8浏览器上的兼容问题,可以引入polyfill。

  3. 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