模块化&包管理

    最后修改于

    模块化作为一个编程语言的基本功能,本不应该占有太多内容,所需要解决的问题是类似 / 相同的,因此本文仅对 JS 中的各种模块化方案 / 工具做简单介绍。

    无模块时代#

    在 JS 只停留在浏览器中的那些年,因为没有语言标准层面上的支持, 只能采取一些很另类的 hacker 方式来模块化实现(模拟命名空间、闭包模拟)。

    js
    // 1.模拟命名空间
    var MyLibrary = {
        utils: {
            formatDate: function() { },
            parseDate: function() { }
        },
        ui: {
            createDialog: function() { },
            showMessage: function() { }
        }
    };
    // 使用
    MyLibrary.utils.formatDate();
    MyLibrary.ui.showMessage();
    
    // 2. 通过闭包模拟进行封装
    var MyModule = (function() {
        // 私有变量和函数
        var privateVariable = 'Hello';
        function privateFunction() {
            console.log(privateVariable);
        }
    
        // 返回公共接口
        return {
            publicMethod: function() {
                privateFunction();
            }
        };
    })();
    
    // 使用方式
    MyModule.publicMethod();
    

    这种模式可以达到封装的目的,但难以清晰的声明依赖项,而且还有一个隐含假设:依赖项在执行这个脚本时一定是可用的,这也限制了依赖项的加载策略。

    将视角切换到另外一面。09 年 Ryan Dahl 创建了 NodeJS,它引入了一个运行时来在服务器上运行 JavaScript 代码。如同其他语言一样,Node 需要一个模块化 / 打包系统,也就是 CommonJS,通过exports/module.exportsrequire,可以定义 / 引用一个 Node 模块,再通过 npm registry 进行分发。这种架构的可复用强太多了。

    js
    //    filename: foo.js
    //    dependencies
    var $ = require('jquery');
    //    methods
    function myFunc(){};
    //    exposed public method (single)
    module.exports = myFunc;
    

    借着 CommonJS 给予的灵感,各种模块化标准、方案开始涌现。

    CommonJS#

    09 年 Ryan Dahl 创建了 NodeJS,它引入了一个运行时来在服务器上运行 JavaScript 代码。如同其他语言一样,Node 需要一个模块化 / 打包系统,也就是 CommonJS,通过exports/module.exportsrequire,可以定义 / 引用一个 Node 模块,再通过 npm registry 进行分发。这种架构的可复用强太多了。

    js
    //    filename: foo.js
    //    dependencies
    var $ = require('jquery');
    //    methods
    function myFunc(){};
    //    exposed public method (single)
    module.exports = myFunc;
    

    借着 CommonJS 给予的灵感,各种模块化标准、方案开始涌现。

    AMD(Asynchronous Module Definition)#

    异步模块定义,通过define来定义模块,显式声明依赖项。在所有依赖项加载完毕之后,再执行模块主体。

    js
    //Calling define with a dependency array and a factory function
    define(['dep1', 'dep2'], function (dep1, dep2) {
        //Define the module value by returning a value.
        return function () {};
    });
    

    相比于 CommonJS 适用于 Node,AMD 为浏览器进行了适配。一种典型实现是RequireJS

    CMD(Common Module Definition)#

    相比于 AMD,CMD 更有 CommonJS 的风格,但也用类似于 AMD 的方式进行模块定义。
    其中 iddeps两个字段可以省略(可以通过构建工具自动生成)

    js
    define('hello', ['./a','./b', './c', './d'], function(require, exports, module) {
     // 获取模块 a 的接口
      var a = require('./a');
      // 调用模块 a 的方法
      a.doSomething();
      // 异步加载一个模块,在加载完成时,执行回调
      require.async('./b', function(b) {
        b.doSomething();
      });
    
      // 异步加载多个模块,在加载完成时,执行回调
      require.async(['./c', './d'], function(c, d) {
        c.doSomething();
        d.doSomething();
      });
    
    });
    

    相应的实现是 Sea.js

    UMD(Universal Module Definition)#

    在经历各种 Module Definition 扰乱开发者的心智之后,有人受不了,尝试提出一种通用的模块定义规范umdjs/umd,尝试兼容AMD,CJS

    js
    (function (root, factory) {
        if (typeof define === 'function' && define.amd) {
            // AMD
            define(['jquery', 'underscore'], factory);
        } else if (typeof exports === 'object') {
            // Node, CommonJS-like
            module.exports = factory(require('jquery'), require('underscore'));
        } else {
            // Browser globals (root is window)
            root.returnExports = factory(root.jQuery, root._);
        }
    }(this, function ($, _) {
        //    methods
        function a(){};    //    private because it's not returned (see below)
        function b(){};    //    public because it's returned
        function c(){};    //    public because it's returned
    
        //    exposed public methods
        return {
            b: b,
            c: c
        }
    }));
    

    是的,他只是一种范式、可以同时兼容不同的模块化机制,没有提供另外一个实现(否则还会出现 UUMD、UUUMD),这种范式很丑陋,但确实有效。

    ESM(ESModule)#

    终于,ECMA 委员会也知道自己落后了,在 ES6(2015 年) 中提出了 模块化的标准,对 JS 模块化方案进行了统一。只是时至今日,CJS 因其先发优势,还有很高的使用率。

    js
    import { f } from "helper"; 
    export const a = () => { // dosomething }
    export const b = () => {
    	f()
    	// dosomething
    }
    export default b
    

    包管理#

    node_modules#

    先说说 Node 在处理依赖引用时的逻辑,这个流程会有如下两种情况
    我们通过 require/import 获取模块,如果是一个核心模块(如 'fs')或是一个明确的相对路径(如./lib.js),Node 会直接使用对应的文件。
    如果不是上面所述的情况,那么 Node 会开始寻找一个名为 node_modules 的目录。首先 Node 会在当前目录寻找,如果没有则到父目录查找,以此类推直到根目录。
    找到 node_modules 之后,按照导入路径定位再在该目录中寻找名对应的文件并使用(找不到就报错)。这个过程称为链接 (node-linker)。

    npm#

    npm 是 node 自带的包管理器,在 node 发布一年之后正式发布,它为 JS 的包管理设定了标准,让共享和管理代码变得简单。这里不讨论的 npm registry这样的基础设施。仅从包管理器的角度介绍 npm。
    node 支持通过 package.json 的方式声明 Node 项目的依赖,然后通过 npm install 这样的命令
    安装依赖到 node_modules 中。

    将所有依赖放在项目的node_modules文件夹下,而且不同项目不可复用,这使得node_modules十分庞大。

    在最初的版本中,实现非常简单,依赖的下载是串行化的,非常缓慢。无法处理包的依赖冲突。
    另外一方面,最初默认的安装方式不支持版本锁定(需要 shrinkwrap),每次安装时都会安装可用的最新版本,导致在不同环境下可能安装的依赖不一致。
    后续版本中又通过扁平化引入了幽灵依赖(可以直接使用间接依赖的包)等问题。

    随着 JS 社区的快速发展,NPM 开始显示出它在包安装的速度、安全性和一致性方面的局限性,Yarn 和 PNPM 等替代工具开始出现。

    yarn#

    Yarn 于 2016 年由 Facebook 推出,引入了离线缓存和lockfile等功能。

    yarn berry(PnP)#

    yarn2 做了很多重构,带来了一个重要特性 PnP (Plug'n'Play)
    从上面的 node_modules 描述中可以发现,依赖查找这个过程中涉及到大量的文件 I/O 操作,效率很低。PnP 尝试解决这个问题。
    Yarn 维护一张静态映射表(也就是*.pnp.js),该表中包含了以下信息:

    • 当前依赖树中包含了哪些依赖包的哪些版本
    • 这些依赖包是如何互相关联的
    • 这些依赖包在文件系统中的具体位置
      在安装依赖时,在第 3 步完成之后,Yarn 并不再拷贝依赖到 node_modules 目录,而是会在 .pnp.js 中记录下该依赖在缓存中的具体位置。这样就避免了大量的 I/O 操作同时项目目录也不会有 node_modules 目录生成。
      在支持的包管理器中通常可以通过设置node-linker启用。
    pnpm#

    PNPM 最初发布于 2017 年,为了解决 node_modules 过大的问题,通过符号链接来全局引用包,从而显著减小空间占用。对 workspace 有较好的支持,相对完善。

    • 🥳0
    • 👍0
    • 💩0
    • 🤩0