〖模块 故事〗JavaScript永不止步★

465 阅读6分钟

终结之谷

CommonJS蒸蒸日上,AMD坟头都快平了

1. 从前慢

一切都得从ES6之前说起...

...

1972 - Dennis Ritchie发明了一把射击时能同时向前和向后两个方向发射子弹的绝世好枪。但他对此发明造成的致死和终身残疾数量感到还不够满意,所以他又发明了C语言和Unix。

...

1983 - Bjarne Stroustrup把他所听说过的一切都试图嫁接到C上,创造出了C++。

...

1986 - Brad Cox和Tom Love创造了Objective-C,宣称“该语言完美地结合了C的内存安全性与Smalltalk的神奇效率”。现在的历史学家怀疑这两人其实是诵读障碍症患者。

...

1987 - Larry Wall在电脑前打了个盹,Larry Wall的脑门子压到了键盘上。醒来之后,Larry Wall深信 ,在Larry Wall的显示器上出现的神秘字符串并非是随机的,那是某种编程语言之程序样例的神谕。那必是上帝要他的先知,Larry Wall,去设计的。Perl语言就此诞生了。

...

1991 - 荷兰程序员Guido van Rossum为了一次神秘的手术而进行了一次阿根廷之旅。回来后他带着一个巨大的颅疤,发明了Python,而被数以军团计的追随者们加冕为“终生大独裁者”,并向全世界宣布“要办到一件事情,只可有唯一的一种方法!”。整个波兰陷入了恐慌。

...

1995 - 在家门口附近的一个意大利饭馆用餐时,Rasmus Lerdorf意识到他吃的那盘意面正好是一个用来理解WWW万维网的极好模型,而所有的Web应用都应该仿照它们的媒介那样去做。在他的餐巾的背后,他设计出了著名的“可编程超链接Pasta(Programmable Hyperlinked Pasta,PHP)”语言。PHP的文档至今仍然保留在那片餐巾上。

...

1995 - Brendan Eich读完了历史上所有在程序语言设计中曾经出现过的错误,自己又发明了一些更多的错误,然后用它们创造出了LiveScript。之后,为了紧跟Java语言的时髦潮流,它被重新命名为JavaScript。再然后,为了追随一种皮肤病的时髦潮流,这语言又被命名为ECMAScript。

...

世界上本来有十个编程语言

有人觉得太乱了,得搞个语言统一所有

于是现在世界上有了十一个编程语言

...

2. 2009

2009是个重要的年份

2009,CommonJS从ServerJS中诞生,它原来其实是一个项目,喊出 “javascript: not just for browsers any more!”,现在只剩下了规范。

2009,Node.js 诞生,第一版的 npm 被创建。一路高歌猛进,2020,Express 诞生、Socket.io 诞生...

2013年5月,Node.js包管理器npm的作者Isaac Z. Schlueter,宣布Node.js已经废弃了CommonJS,Node.js核心开发者应避免使用它[5]... 现在,node文档有专门CommonJS模块

AMD最初是CommonJS 传输形式的衍生产品,后来发展成自己的模块定义API,所以两者基本相似。

On the other hand, RequireJS implements AMD, which is designed to suit the browser environment (source). Apparently, AMD started as a spinoff of the CommonJS Transport format and evolved into its own module definition API. Hence the similarities between the two. The new feature in AMD is the define() function that allows the module to declare its dependencies before being loaded. ---stack overflow

2009,requirejs诞生。

2010,阿里的玉伯看着requirejs,摇了摇头,于是有了seajs。(编的)

2015,ES6带着模块兴冲冲地来了,它们的坟头也开始长草了。

3. CJS规范

CommonJS规范,官方原文,一篇很简短的描述。这里用谷歌翻译简单描述下:

3.1 模块作用域

  1. 在一个模块中,有一个变量“require”,也是一个函数。

    1. “require”函数接受一个模块标识符。
    2. “require”返回外部模块的导出API。
    3. 如果存在依赖循环,则外部模块可能没有在其传递依赖之一所需的时间完成执行;在这种情况下,“require”返回的对象必须至少包含外部模块在调用导致当前模块执行的 require 之前准备好的导出。
    4. 如果请求的模块无法返回,“require”必须抛出错误。
  2. 在模块中,有一个名为“exports”的变量,该对象是模块在执行时可以添加其 API 的对象。

  3. 模块必须使用“exports”对象作为唯一的导出方式。

3.2 模块标识符

  1. 模块标识符是由正斜杠分隔的“术语”字符串。
  2. 术语必须是驼峰式标识符、“.”或“..”。
  3. 模块标识符可能没有像“.js”这样的文件扩展名。
  4. 模块标识符可以是 “相对路径” 或 “绝对路径(Top-level identifiers)” 。如果以“.”或者 ”..”开头,则模块标识符是“相对路径”。
  5. 绝对路径从概念模块名称空间根目录中解析出来。
  6. 相对路径是相对于写入和调用“require”的模块的标识符进行解析的。

3.3 Sample Code

// math.js
exports.add = function() {
    var sum = 0, i = 0, args = arguments, l = args.length;
    while (i < l) {
        sum += args[i++];
    }
    return sum;
};

// increment.js
var add = require('math').add;
exports.increment = function(val) {
    return add(val, 1);
};

// program.js
var inc = require('increment').increment;
var a = 1;
inc(a); // 2

3.4 NodeJS模块

CMJ规范是很好的开始,但node的cmd模块更重要,毕竟大家都认这个

模块封装器,在执行模块代码之前,Node.js 将使用如下所示的函数封装器对其进行封装:

(function(exports, require, module, __filename, __dirname) {
// 模块代码实际存在于此处
});

模块加载是同步的,加载结束才会继续执行。因此也可以实现动态依赖,但会加大打包器静态分析难度

即:变量声明不会污染全局,模块拥有自己的私有变量

require.cache,模块在第一次加载后被缓存到require.cache,这意味着(类似其他缓存)每次调用 require('foo') 都会返回完全相同的(单例)对象(当然,你也可篡改require.cache以多次加载)。相同对象,也即是修改返回的对象,会对之后的require()对象造成改动,你导出primitive类型当我没说。

exports 对 module.exports 的引用,所以直接修改 exports 没有效果。

4. AMD

异步模块定义(Asynchronous Module Definition),AMD算是一份补充规范,作为JS网络传输上的一个补充。一般策略是让模块声明自己的依赖,而运行在浏览器中的模块会按需获取依赖,并在加载完成后立即执行。

AMD模块实现核心是全局define函数。define支持可选的字符串标识符,依赖模块数组,模块函数。

define(identifier: string, deps: string[], func: (...deps) => any)

AMD也支持require和exports对象 定义CommonJS风格的模块。

define('moduleA', ['require', 'exports'], function(require, exports) {
    const moduleB = require('moduleB')
    
    exports.stuff = moduleB.doStuff()
})
// 同时支持动态依赖
define('moduleA', ['require'], function(require) {
    if(condition) {
        const moduleB = require('moduleB')
    }
})

5. CMD

由seajs在 issue240中定义,推崇依赖就近,延迟执行

6. UMD

通用模块定义(Universal Module Definition),为统一AMD与CMD的生态而产生。实现也十分简单。

以下是安装"rollup": "^2.60.0"output.format = 'umd'打包后的样子,简单的判断环境,保留响应模块

// 设置标识符为 ModuleName
(function (global, factory) {
  // CMD
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  // AMD
  typeof define === 'function' && define.amd ? define(factory) :
  // Brower
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.ModuleName = factory());
})(this, (function () { 'use strict';
    ...
}));

以下是安装"webpack": "^5.64.0",设置output.library.type = "umd"打包后的样子,看起来就不明觉厉

// 设置标识符为 webpackModule
!(function (e, o) {
  "object" == typeof exports && "object" == typeof module
    ? (module.exports = o())
    : "function" == typeof define && define.amd
    ? define([], o)
    : "object" == typeof exports
    ? (exports.webpackModule = o())
    : (e.webpackModule = o());
})(self, function () {
  return (() => {
    "use strict";
    var e = {
        d: (o, t) => {
          for (var r in t)
            e.o(t, r) &&
              !e.o(o, r) &&
              Object.defineProperty(o, r, { enumerable: !0, get: t[r] });
        },
        o: (e, o) => Object.prototype.hasOwnProperty.call(e, o),
        r: (e) => {
          "undefined" != typeof Symbol &&
            Symbol.toStringTag &&
            Object.defineProperty(e, Symbol.toStringTag, { value: "Module" }),
            Object.defineProperty(e, "__esModule", { value: !0 });
        },
      },
      o = {};
    e.r(o), e.d(o, { default: () => t });
    ...
    return o;
  })();
});

7. ES6

<script>标签中,加载即执行,声明都在全局内。现在设置标签属性type="module",其内js就会作为模块执行。类似 <script defer> 一样延迟按顺序执行,只会在文档解析完成。也可以添加async属性、src属性等

<script>嵌入代码 无法被其它模块引入,所以只能作为入口模块

ES6模块也是单例,可以多次加载,但只会返回缓存。

ES6默认开启严格模式

ES6顶级this值为undefined,var声明也不会添加到window对象

ES6 import的文件会被认定为模块

书写形式就是平时写项目的样子。阅读MDN importexport

值得注意的是:

import 导入的模块如同const 不可修改,用*号如同Object.freeze()一样

NodeJS ECMAScript模块

是的,狗子又变了。

可以设置文件后缀js => mjs 使用ES模块,也可以设置package.json type属性为module

注意:不能混合使用,会报错。

懒加载

webpack文件分割加上懒加载实现按需引入,这算是在AMD坟头蹦迪?还是棺材板没按住!

参考