JavaScript模块管理机制

在之前的项目开发中一直使用RequireJS进行模块化管理,在NodeJS中使用的是CommonJS规范的模块管理,在Vue-cli中使用ES6内置的模块管理。恰好昨天面试题有一问提到他们之间的区别,之前并没有太深入这些知识,回答的不是很好,这里整理一下。

<!--more-->

参考:

RequireJS是基于AMD规范实现的,那么这个问题变成了:AMDCommonJSES6内置模块各自的区别。 在回答问题之前,还是先回顾一下这三种方式引入和导出模块的使用方法吧。

1. AMD

RequireJS

  • 使用define()导出模块
  • 使用require()引入模块

1.1. 使用方法

导出模块

// lib1.js
define([], function() {
    return {
        test: function() {
            console.log("this is test in lib1.js");
        },
        test2: function() {
              console.log("this is test 2 in lib1.js");
        }
    }
});

引入模块

// index.js
require(["lib1"], function (lib1) {
    lib1.test();
});

可以看见,lib1模块的全部方法都会被require引入并作为模块的方法进行调用。

1.2. 路径解析

之前写过RequireJS使用心得,这里简单回顾一下,关于具体的使用方式就不展开了。

  • 基础路径baseUrl
    • 相对路径
      • 没有配置data-main,则baseurl为引入require.js的html文档所在路径
      • 已配置data-main,则baseurldata-main指向模块所处的路径
      • 显示调用require.config()进行配置
    • 绝对路径
  • 模块路径paths
    • 具体文件名
    • 文件夹
  • 模块IDmodule ID
    • 常规,不带.js

1.3. 小结

由于浏览器在解析文档时,遇见脚本会加载解析和执行,为了提高页面性能,一般的处理办法是异步延迟加载,这正是AMD全称中Asynchronous的含义。 异步带来的问题是:在浏览器中,必须等待依赖的模块加载成功,对应的声明模块才能够执行。换句话说,AMD中的模块是依赖前置的。

2. CommonJS

NodeJS

  • 使用module.exports导出模块
  • 使用require引入模块

2.1. 使用方法

定义模块

function add(...args) {
    var sum = 0;
    for (var v of args){
        sum += v;
    }

    console.log(sum);
}


// 导出方式1
module.exports = {
    add
};

// 导出方式2
exports.add = add;

引入模块

var math = require("./module1");
math.add(1, 2, 3, 4);

需要注意的是CommonJS是同步加载的,也就是说加载模块时会阻塞后续代码的执行,这对于服务器端来讲问题不大,但是对于浏览器而言网络传输的效率是不容忽视的,所以才有了AMD规范。

2.2. module.exports和exports

初始使用NodeJS中的模块时,难免对于module.exportsexports的用法产生疑惑。实际上社区的很详细的讲解了这个问题,总结一下;

  • 整个文件导出的模块(也就是require() 的返回)是 module.exports
  • module.exportsexports在初始时指向同一个对象,因此可以使用exports向模块对象上增加属性和方法
  • module.exports如果指向了另外一个对象,则exports的修改全部无效了(因为最后导出的是module.exports

可以理解为exportsmodule.exports的一个快捷方式,无论是修改了module.exports还是exports的指向,都会修改模块的导出。

如果还有疑问,可以移步:

2.3. 模块加载顺序

NodeJS中,我们使用npm来安装和管理第三方工具包,然后可以使用require很方便的引入插件。那么,NodeJS中的require到底是怎么样的加载顺序呢?参考阮老师的require()源码解读,一个require(X)表达式,可能存在下面几种情况:

  • 如果X是内置模块(比如require("http")
    • 返回该模块
    • 不再继续执行
  • 如果 X 以./ 或者 / 或者 ../ 开头
    • 根据 X 所在的父模块,确定 X 的绝对路径。
    • 将 X 当成文件,依次查找X,X.js,X.json,X.node,只要其中有一个存在,就返回该文件,不再继续执行。
  • 将 X 当成目录,依次查找X/package.json,X/index.js,X/index.json,X/index.node,只要其中有一个存在,就返回该文件,不再继续执行
  • 如果 X 不带路径(加载第三方模块比如require("gulp")
    • 根据 X 所在的父模块,确定 X 可能的安装目录
    • 依次在每个目录中,将 X 当成文件名或目录名加载
  • 抛出 not found

可以看见,规则还是比较复杂的,require会尝试尽可能去成功加载对应的文件,在内部源码中是Module类实现的,这里暂时就没有深入了解了。

3. ES6中的模块

ES6中,采用了跟python类似的模块语法

由于还存在很多兼容的问题,因此这里直接把文档拿过来了,主要只是在Vue-cli中使用到。

3.1. 导出模块

导出方式分为两种:

// 命名导出(每个模块可以多次使用)
export { myFunction };
export const foo = Math.sqrt(2);

// 默认导出(每个脚本只能有一个默认导出)
export default myFunctionOrClass

3.2. 引入模块

ES6与CommonJSAMD最大的区别在于引入模块的方式,既可以引入整个模块,也可以引入某块的某一部分!

// 导入整个模块的内容
import  * as myModule from "my-module";

// 导入模块的单个成员
import {myMember} from "my-module

// 导入模块的多个成员
import {foo, bar} from "my-module

4. 简易的模块系统

这里我们实现一个极其简易的模块系统(代码参考《你不知道的JavaScript上卷》)。

(function () {
    // 定义一个保存模块的闭包变量
    var modules = {};

    // define和require接口用于修改和使用modules变量
    function define(name, deps, fn) {
        for (var i = 0; i < deps.length; i++) {
            deps[i] = modules[deps[i]];
        }

        modules[name] = fn.apply(fn, deps);
    }

    function require(name) {
        return modules[name];
    }

    window.require = require;
    window.define = define;
})();

上面这段代码声明了一个局部变量modules用于保存模块,向外提供了requiredefine两个接口加载和声明模块。当然,这段代码并不能用在实际生产中,因为还缺少很多东西(比如AMD最重要的异步加载)。 不过,这里还是能大概了解模块的含义,我对于模块的理解就是:按功能将独立的逻辑,封装成可重用的代码块,然后对外提供模块的调用接口。归根结底,都是为了更加规范更加方便的写代码和维护代码嘛~

《同构JavaScript应用开发》读书笔记 微信小程序之路由拦截器