1. 慈母手中线,游子身上衣。尽管这是个不恰当的比喻,但大脑里的很多知识对我来说就像手里的毛线团一样,是杂乱零散的,需要一张网才可以把他们编织成型,so 特以此篇文章来记录一下。

本文将从一下几点简单聊起:

前情提要

代码模块化早已是基操(基础操作)了,众所周知的有 CommonJs、AMD、CMD、UMD、ES Module 这五种解决规范,该文章是对自己学习的记录,如有错误欢迎大家批评指正。

CommonJs / CJS

CommonJs是一种规范。

概念

我将维基百科(CommonJS - wiki - 链接)上提到的几个重点,压缩为以下的内容:

针对环境:web浏览器外的(非web浏览器环境的)JavaScript项目
项目目标:JavaScript生态的模块化解决方案
主要示例:尤其是使用nodeJs用于服务端JavaScript编程
浏览器 :浏览器不能直接执行CommonJs代码,需要通过编译转化
如何识别:我们可以通过是否使用 require()function和module.exports来识别是否使用了CommonJs

CommonJs并没有成为ECMA组织发布的模块化标准(ES Module),但有很多ECMA成员参与其中。

特点

  1. 所有代码都运行在模块作用域中,不会污染全局变量;
  2. 模块按照在代码中的顺序,依次同步加载
  3. 模块会在运行时加载且执行,执行得到对象A,后续通过require获取的都是对对象A值的拷贝(换句话说,模块可以多次加载,在第一次加载时执行并缓存其结果,后续加载会直接返回该结果),要想模块再次运行,必须清除缓存。

如果你想要多次执行一个模块,可以导出一个函数,然后调用函数。

NodeJs的模块化

  1. 在执行模块代码之前,NodeJs会使用如下的函数封装器将其封装;
  • 通过闭包的形式避免了变量污染;
  • 提供了看似全局,实际上是模块特定的变量;
    1. (function(exports, require, module, __filename, __dirname) {
    2. // 模块的代码实际上在这里
    3. })
  1. 可以通过 module.exports 导出模块内容;
  2. 变量 exports 是对 module.exports 的引用,所以不能对exports有赋值操作;
  • exports = module.exports;
  • exports变量是在模块的文件级作用域内可用的,且在模块执行之前赋值给module.exports
  • 因此module.exports.f = ...可以更简洁地写成exports.f = ...; ```javascript // 错误用法,exports被重新赋值,此function并未被导出 exports = function(x) {console.log(x)};

// 正确用法 exports.a = function (x){ console.log(x);};

/* 错误写法二

这是由于module.exports 被改写,导致exports也被重新改写

这意味着,如果一个模块的对外接口,就是一个单一的值, 最好不要使用exports输出,最好使用module.exports输出。 */

exports.hello = function() { return ‘hello’; };

module.exports = ‘Hello world’;

  1. 4. **通过 require(id) 引入模块、JSON、或本地文件;**
  2. 5. **require.cache 被引入的模块将被缓存到这个对象中,如果删除该对象的某个模块会导致下次require的时候重新加载该模块。**
  3. <a name="Fqpms"></a>
  4. # AMD(Asynchronous Module Definition)
  5. JavaScript的异步模块化定义方案。
  6. <a name="CHqyR"></a>
  7. ## 概念
  8. **针对环境:**web浏览器<br />**项目目标:**JavaScript生态的**模块化解决方案**<br />**主要示例:**require.js<br />**如何识别:**我们可以通过是否使用 `define(id?, dependencies?, factory);`function来识别是否使用了AMD规范。
  9. ```javascript
  10. // 其中对于"require", "exports", "beta" 这几个依赖可不填
  11. define("alpha", ["require", "exports", "beta"], function (require, exports, beta) {
  12. exports.verb = function() {
  13. return beta.verb();
  14. //Or:
  15. return require("beta").verb();
  16. }
  17. });

特点

  1. 所有代码都运行在模块作用域中,不会污染全局变量;
  2. 模块会被异步加载
  3. 依赖模块加载完成后,会立即执行其回调函数(即factory函数);
  4. 主模块会等所有的依赖模块加载完成后,再调用其对应的回调函数(依赖前置);

require.js 的模块加载原理

简单使用

首先简单介绍一下 require.js 的使用:

  1. 在html文件内,需要有一个script标签引入require.js以及项目的入口文件main.js
  2. 文件 main.js 里的就是项目的主逻辑了。

项目结构如下:
project-directory/

  • project.html
  • scripts/
    • main.js
    • helper/
      • util.js ```html

<!DOCTYPE html>

My Sample Project

  1. ```javascript
  2. // main.js
  3. requirejs(["helper/util"], function(util) {
  4. // you can do everything you want
  5. });

原理介绍

不过 RequireJS 从2.0开始,也改成了可以延迟执行(暂不讨论)

目的:

  • 理解require使用script标签
  • 关于script标签是否加载成功可以通过onload事件来判断,具体实现细节并不讨论
  1. 引入require.js时,我们会通过data-main引入入口文件;
  2. require.js获取到入口文件后,将文件以及对应的依赖通过script标签append到html上;
  3. 依赖是依次、同步append到html,但是script标签的加载却是异步的;
  4. 依赖加载完成后,会立即调用其回调执行函数;
  5. 入口文件监听到所有的依赖都加载完成后,再调用其回调函数(即回调函数factory)。

CMD(Common Modules Definition)

CMD 是 sea.js 在推广过程中对模块定义的规范化产出。和 AMD 很像,这里只简单讨论他们的异同。

特点

  1. 所有代码都运行在模块作用域中,不会污染全局变量;
  2. 模块会被异步加载
  3. 模块加载完成后,不会执行其回调函数,而是等到主函数运行且需要的执行依赖的时候才运行依赖函数(依赖后置、按需加载);

UMD(Universal Module Definition)

UMD 提供了支持多种风格的“通用”模式,在兼容 CommonJS 和 AMD 规范的同时,还兼容全局引用的方式。

  1. (function (root, factory) {
  2. if (typeof define === "function" && define.amd) {
  3. define(["jquery", "underscore"], factory);
  4. } else if (typeof exports === "object") {
  5. module.exports = factory(require("jquery"), require("underscore"));
  6. } else {
  7. root.Requester = factory(root.$, root._);
  8. }
  9. }(this, function ($, _) {
  10. // this is where I defined my module implementation
  11. var Requester = { // ... };
  12. return Requester;
  13. }));

原理


实现原理很简单。

  1. 判断是否支持AMD,若存在则使用 AMD 方式加载模块,否则继续步骤2;
  2. 判断是否支持 CommonJs ,若存在则使用 Node.js 的模块格式,否则继续步骤3;
  3. 将模块公开到全局(window 或 global)

ES Module

ES Module 是用于处理模块的ECMAScript标准。现代浏览器(高版本)以基本支持 ES Module。

  1. <script type="module" src="index.js"></script>

特点

  1. 所有代码都运行在模块作用域中,不会污染全局变量;
  2. 在编译时输出模块;
  3. 输出的模块内容为只读,不可修改;
  4. 不会缓存模块结果,每次都会动态执行模块内容;

ES6 的 import & export

ES6也是基操了,必须会的。
这篇文章写得非常好 require和import的区别 - 链接

  1. import 语句会被提升;
  2. import 的变量都是只读的;
  3. import 是静态执行,所以不能用表达式;
  4. import 语句支持 Singleton 模式(如果多次重复执行同一句import语句,那么只会执行一次,而不会执行多次。);
  5. export 需要输出一个对象 或 变量声明语句;
  6. export default 相当于输出了一个名叫default的变量/对象;
  7. export与import连用;
    1. 其中foo和bar其实并没有导入到当前文件,相当于通过该入口文件转发了出去,通常可用作utils/index.js的转接。其他地方可以通过**import {foo} from 'util'**引用该文件 ```javascript export { foo, bar } from ‘my_module’;

// 可以简单理解为 import { foo, bar } from ‘my_module’; export { foo, bar }; ```

Q&A 模块的运行时加载 和 编译时加载

在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为编译。“分词/词法分析” -> “解析/语法分析” -> “代码生成”。

ES6中的import语句,就是在编译过程中生成的,引入的是模块的地址,所以在执行的时候会动态拿到地址去执行相应的内容。
requireJs内require('foo')一个模块的时候,拿到的是这个模块运行后得到的值的(浅)拷贝,是一个“死”的内容,如果需要重新运行需要手动清理 require.cache对应的模块。

结束

文章到此就全部结束了,内容以“理论”为主,我会将自己了解的的内容都输出为视频和文章的形式,考验自己基础的同时希望可以给迷惑的小伙伴“解解惑”。

感谢观看,下次再见!

其他推荐

参考文章列表:

  1. https://www.jianshu.com/p/eb5948a70294 JavaScript模块化 之( Commonjs、AMD、CMD、ES6 modules)演变史
  2. https://www.jianshu.com/p/d7fdcc89fbee CommonJS,AMD,CMD,ES6 Module
  3. https://www.jianshu.com/p/929b56dcfbbf 模块化开发
  4. https://segmentfault.com/a/1190000021911869 require和import的区别
  5. https://juejin.cn/post/6844903759009595405#heading-9 模块化之AMD与CMD原理(附源码)
  6. https://dev.to/iggredible/what-the-heck-are-cjs-amd-umd-and-esm-ikm What are CJS, AMD, UMD, and ESM in Javascript?