慈母手中线,游子身上衣。尽管这是个不恰当的比喻,但大脑里的很多知识对我来说就像手里的毛线团一样,是杂乱零散的,需要一张网才可以把他们编织成型,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成员参与其中。
特点
- 所有代码都运行在模块作用域中,不会污染全局变量;
- 模块按照在代码中的顺序,依次同步加载;
- 模块会在运行时加载且执行,执行得到对象A,后续通过require获取的都是对对象A值的拷贝(换句话说,模块可以多次加载,在第一次加载时执行并缓存其结果,后续加载会直接返回该结果),要想模块再次运行,必须清除缓存。
如果你想要多次执行一个模块,可以导出一个函数,然后调用函数。
NodeJs的模块化
- 在执行模块代码之前,NodeJs会使用如下的函数封装器将其封装;
- 通过闭包的形式避免了变量污染;
- 提供了看似全局,实际上是模块特定的变量;
(function(exports, require, module, __filename, __dirname) {
// 模块的代码实际上在这里
})
- 可以通过 module.exports 导出模块内容;
- 变量 exports 是对 module.exports 的引用,所以不能对exports有赋值操作;
- exports = module.exports;
- exports变量是在模块的文件级作用域内可用的,且在模块执行之前赋值给
module.exports
; - 因此
module.exports.f = ...
可以更简洁地写成exports.f = ...
;
// 错误用法,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';
- 通过 require(id) 引入模块、JSON、或本地文件;
- require.cache 被引入的模块将被缓存到这个对象中,如果删除该对象的某个模块会导致下次require的时候重新加载该模块。
AMD(Asynchronous Module Definition)
JavaScript的异步模块化定义方案。
概念
针对环境:web浏览器
项目目标:JavaScript生态的模块化解决方案
主要示例:require.js
如何识别:我们可以通过是否使用 define(id?, dependencies?, factory);
function来识别是否使用了AMD规范。
// 其中对于"require", "exports", "beta" 这几个依赖可不填
define("alpha", ["require", "exports", "beta"], function (require, exports, beta) {
exports.verb = function() {
return beta.verb();
//Or:
return require("beta").verb();
}
});
特点
- 所有代码都运行在模块作用域中,不会污染全局变量;
- 模块会被异步加载;
- 依赖模块加载完成后,会立即执行其回调函数(即factory函数);
- 主模块会等所有的依赖模块加载完成后,再调用其对应的回调函数(依赖前置);
require.js 的模块加载原理
简单使用
首先简单介绍一下 require.js 的使用:
- 在html文件内,需要有一个
script
标签引入require.js
以及项目的入口文件main.js
; - 文件 main.js 里的就是项目的主逻辑了。
项目结构如下:
_project-directory/_
- _project.html_
- _scripts/_
- _main.js_
- _helper/_
- _util.js_
My Sample Project
My Sample Project
// main.js
requirejs(["helper/util"], function(util) {
// you can do everything you want
});
原理介绍
不过 RequireJS 从2.0开始,也改成了可以延迟执行(暂不讨论)
目的:
- 理解require使用script标签
- 关于script标签是否加载成功可以通过
onload
事件来判断,具体实现细节并不讨论
- 引入require.js时,我们会通过
data-main
引入入口文件; - require.js获取到入口文件后,将文件以及对应的依赖通过
script
标签append到html上; - 依赖是依次、同步append到html,但是script标签的加载却是异步的;
- 依赖加载完成后,会立即调用其回调执行函数;
- 入口文件监听到所有的依赖都加载完成后,再调用其回调函数(即回调函数factory)。
CMD(Common Modules Definition)
CMD 是 sea.js 在推广过程中对模块定义的规范化产出。和 AMD 很像,这里只简单讨论他们的异同。
特点
- 所有代码都运行在模块作用域中,不会污染全局变量;
- 模块会被异步加载;
- 模块加载完成后,不会执行其回调函数,而是等到主函数运行且需要的执行依赖的时候才运行依赖函数(依赖后置、按需加载);
UMD(Universal Module Definition)
UMD 提供了支持多种风格的“通用”模式,在兼容 CommonJS 和 AMD 规范的同时,还兼容全局引用的方式。
(function (root, factory) {
if (typeof define === "function" && define.amd) {
define(["jquery", "underscore"], factory);
} else if (typeof exports === "object") {
module.exports = factory(require("jquery"), require("underscore"));
} else {
root.Requester = factory(root.$, root._);
}
}(this, function ($, _) {
// this is where I defined my module implementation
var Requester = { // ... };
return Requester;
}));
原理
实现原理很简单。
- 判断是否支持AMD,若存在则使用 AMD 方式加载模块,否则继续步骤2;
- 判断是否支持 CommonJs ,若存在则使用 Node.js 的模块格式,否则继续步骤3;
- 将模块公开到全局(window 或 global)
ES Module
ES Module 是用于处理模块的ECMAScript标准。现代浏览器(高版本)以基本支持 ES Module。
特点
- 所有代码都运行在模块作用域中,不会污染全局变量;
- 在编译时输出模块;
- 输出的模块内容为只读,不可修改;
- 不会缓存模块结果,每次都会动态执行模块内容;
ES6 的 import & export
ES6也是基操了,必须会的。
这篇文章写得非常好 require和import的区别 - 链接
- import 语句会被提升;
- import 的变量都是只读的;
- import 是静态执行,所以不能用表达式;
- import 语句支持 Singleton 模式(如果多次重复执行同一句import语句,那么只会执行一次,而不会执行多次。);
- export 需要输出一个对象 或 变量声明语句;
- export default 相当于输出了一个名叫default的变量/对象;
- export与import连用;
- 其中foo和bar其实并没有导入到当前文件,相当于通过该入口文件转发了出去,通常可用作utils/index.js的转接。其他地方可以通过
**import {foo} from 'util'**
引用该文件
- 其中foo和bar其实并没有导入到当前文件,相当于通过该入口文件转发了出去,通常可用作utils/index.js的转接。其他地方可以通过
export { foo, bar } from 'my_module';
// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };
Q&A 模块的运行时加载 和 编译时加载
在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为编译。“分词/词法分析” -> “解析/语法分析” -> “代码生成”。
ES6中的import
语句,就是在编译过程中生成的,引入的是模块的地址,所以在执行的时候会动态拿到地址去执行相应的内容。
requireJs内require('foo')
一个模块的时候,拿到的是这个模块运行后得到的值的(浅)拷贝,是一个“死”的内容,如果需要重新运行需要手动清理 require.cache
对应的模块。
结束
文章到此就全部结束了,内容以“理论”为主,我会将自己了解的的内容都输出为视频和文章的形式,考验自己基础的同时希望可以给迷惑的小伙伴“解解惑”。
感谢观看,下次再见!
其他推荐
参考文章列表:
- https://www.jianshu.com/p/eb5948a70294 JavaScript模块化 之( Commonjs、AMD、CMD、ES6 modules)演变史
- https://www.jianshu.com/p/d7fdcc89fbee CommonJS,AMD,CMD,ES6 Module
- https://www.jianshu.com/p/929b56dcfbbf 模块化开发
- https://segmentfault.com/a/1190000021911869 require和import的区别
- https://juejin.cn/post/6844903759009595405#heading-9 模块化之AMD与CMD原理(附源码)
- https://dev.to/iggredible/what-the-heck-are-cjs-amd-umd-and-esm-ikm What are CJS, AMD, UMD, and ESM in Javascript?