前言
前端的模块化之路经历了漫长的过程, 想详细了解的小伙伴可以看浪里行舟大神写的前端模块化详解(完整版), 这里根据大佬们写的文章, 做了汇总和整理, 希望读完的小伙伴能有些收获.
什么是模块
将一个复杂的程序依据一定的规则 (规范) 封装成几个块 (文件), 并进行组合在一起 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法) 与外部其它模块通信
CommonJS
Node 应用由模块组成, 采用 CommonJS 模块规范. 每个文件就是一个模块, 有自己的作用域. 在一个文件里面定义的变量, 函数, 类, 都是私有的, 对其他文件不可见. 在服务器端, 模块的加载是运行时同步加载的; 在浏览器端, 模块需要提前编译打包处理.
CommonJS 规范加载模块是同步的, 也就是说, 只有加载完成, 才能执行后面的操作.
基本语法:
暴露模块:
module.exports = value 或 exports.xxx = value
引入模块: require(xxx), 如果是第三方模块, xxx 为模块名; 如果是自定义模块, xxx 为模块文件路径
但是, CommonJs 有一个重大的局限使得它不适用于浏览器环境, 那就是 require 操作是同步的. 这对服务器端不是一个问题, 因为所有的模块都存放在本地硬盘, 可以同步加载完成, 等待时间就是硬盘的读取时间. 但是, 对于浏览器, 这却是一个大问题, 因为模块都放在服务器端, 等待时间取决于网速的快慢, 可能要等很长时间, 浏览器处于 "假死" 状态.
因此, 浏览器端的模块, 不能采用 "同步加载"(synchronous), 只能采用 "异步加载"(asynchronous), 这就是 AMD 规范诞生的背景.
AMD
特点: 非同步加载模块, 允许指定回调函数, 浏览器端一般采用 AMD 规范
代表作: require.JS
用法:
- // 定义没有依赖的模块
- define(function(){
return 模块
- })
- // 定义有依赖的模块
- define(['module1', 'module2'], function(m1, m2){
return 模块
- })
- // 引入使用模块
- require(['module1', 'module2'], function(m1, m2){
- // 使用 m1/m2
- })
- CMD
特点: 专门用于浏览器端, 模块的加载是异步的, 模块使用时才会加载执行
代表作: Sea.JS
用法:
- // 定义没有依赖的模块
- define(function(require, exports, module){
- exports.xxx = value
- module.exports = value
- })
- // 定义有依赖的模块
- define(function(require, exports, module){
- // 引入依赖模块(同步)
- var module2 = require('./module2')
- // 引入依赖模块(异步)
- require.async('./module3', function (m3) {
- })
- // 暴露模块
- exports.xxx = value
- })
- // 引入使用模块
- define(function (require) {
- var m1 = require('./module1')
- var m4 = require('./module4')
- m1.show()
- m4.show()
- })
CMD 与 AMD 区别
AMD 和 CMD 最大的区别是对依赖模块的执行时机处理不同, 而不是加载的时机或者方式不同, 二者皆为异步加载模块.
AMD 依赖前置, JS 可以方便知道依赖模块是谁, 立即加载;
而 CMD 就近依赖, 需要使用把模块变为字符串解析一遍才知道依赖了那些模块, 这也是很多人诟病 CMD 的一点, 牺牲性能来带来开发的便利性, 实际上解析模块用的时间短到可以忽略.
一句话总结:
两者都是异步加载, 只是执行时机不一样. AMD 是依赖前置, 提前执行, CMD 是依赖就近, 延迟执行.
UMD
UMD 是 AMD 和 CommonJS 的糅合:
AMD 模块以浏览器第一的原则发展, 异步加载模块.
CommonJS 模块以服务器第一原则发展, 选择同步加载, 它的模块无需包装(unwrapped modules).
这迫使人们又想出另一个更通用的模式 UMD (Universal Module Definition). 希望解决跨平台的解决方案.
UMD 先判断是否支持 Node.JS 的模块 (exports) 是否存在, 存在则使用 Node.JS 模块模式.
在判断是否支持 AMD(define 是否存在), 存在则使用 AMD 方式加载模块.
- (function (Windows, factory) {
- if (typeof exports === 'object') {
- module.exports = factory();
- } else if (typeof define === 'function' && define.amd) {
- define(factory);
- } else {
- Windows.eventUtil = factory();
- }
- })(this, function () {
- //module ...
- });
ES6 模块化
ES6 模块的设计思想是尽量的静态化, 使得编译时就能确定模块的依赖关系, 以及输入和输出的变量. CommonJS 和 AMD 模块, 都只能在运行时确定这些东西. 比如, CommonJS 模块就是对象, 输入时必须查找对象属性.
ES6 Module 默认目前还没有被浏览器支持, 需要使用 babel, 在日常写 demo 的时候经常会显示这个错误:
ES6 模块使用 import 关键字导入模块, export 关键字导出模块:
- /** 导出模块的方式 **/
- var a = 0;
- export {
- a
- }; // 第一种
- export const b = 1; // 第二种
- let c = 2;
- export default {
- c
- }// 第三种
- let d = 2;
- export default {
- d as e
- }// 第四种, 别名
- /** 导入模块的方式 **/
- import {
- a
- } from './a.js' // 针对 export 导出方式,.JS 后缀可省略
- import main from './c' // 针对 export default 导出方式, 使用时用 main.c
- import 'lodash' // 仅仅执行 lodash 模块, 但是不输入任何值
命名式导出与默认导出
export {<变量>}这种方式一般称为 命名式导出 或者 具名导出, 导出的是一个变量的引用.
export default 这种方式称为 默认导出 或者 匿名导出, 导出的是一个值.
举例:
- // a.JS
- let x = 10
- let y = 20
- setTimeout(()=>{
- x = 100
- y = 200
- },100)
- export { x }
- export default y
- // b.JS
- import { x } from './a.js'
- import y from './a.js'
- setTimeout(()=>{
- console.log(x,y) // 100,20
- },100)
ES6 模块与 CommonJS 模块的差异
1 CommonJS 模块输出的是一个值的拷贝, ES6 模块输出的是值的引用.
CommonJS 模块输出的是值的拷贝, 也就是说, 一旦输出一个值, 模块内部的变化就影响不到这个值. 而且, CommonJS 模块无论加载多少次, 都只会在第一次加载时运行一次, 以后再加载, 返回的都是第一次运行结果的缓存, 除非手动清除系统缓存.
?ES6 模块的运行机制与 CommonJS 不一样, JS 引擎对脚本静态分析的时候, 遇到模块加载命令 import, 就会生成一个只读引用, 等到脚本真正执行时, 再根据这个只读引用, 到被加载的那个模块里面去取值. 换句话说, ES6 的 import 有点像 Unix 系统的 "符号连接", 原始值变了, import 加载的值也会跟着变. 因此, ES6 模块是动态引用, 并且不会缓存值, 模块里面的变量绑定其所在的模块.
2 CommonJS 模块是运行时加载, ES6 模块是编译时输出接口.
CommonJS 加载的是一个对象(即 module.exports 属性), 该对象只有在脚本运行完才会生成. 即在输入时是先加载整个模块, 生成一个对象, 然后再从这个对象上面读取方法, 这种加载称为 "运行时加载".
例如:
- // CommonJS 模块
- let {
- stat, exists, readFile
- } = require('fs');
- // 等同于
- let _fs = require('fs');
- let stat = _fs.stat;
- let exists = _fs.exists;
- let readfile = _fs.readfile;
上面代码的实质是整体加载 fs 模块(即加载 fs 的所有方法), 生成一个对象(_fs), 然后再从这个对象上面读取 3 个方法. 因为只有运行时才能得到这个对象, 导致完全没办法在编译时做 "静态优化".
ES6 模块不是对象, 它的对外接口只是一种静态定义, 在代码静态解析阶段就会生成. 通过 export 命令显式指定输出的代码, import 时采用静态命令的形式. 即在 import 时可以指定加载某个输出值, 而不是加载整个模块, 这种加载称为 "编译时加载" 或者 "静态加载".
- // ES6 模块
- import {
- stat, exists, readFile
- } from 'fs';
上面代码的实质是从 fs 模块加载 3 个方法, 其他方法不加载. 即 ES6 可以在编译时就完成模块加载, 效率要比 CommonJS 模块的加载方式高. 当然, 这也导致了没法引用 ES6 模块本身, 因为它不是对象.
由于 ES6 模块是编译时加载, 使得静态分析成为可能. 有了它, 就能进一步拓宽 JavaScript 的语法, 比如引入宏 (macro) 和类型检验 (type system) 这些只能靠静态分析实现的功能.
除了静态加载带来的各种好处, ES6 模块还有以下好处:
不再需要 UMD 模块格式了, 将来服务器和浏览器都会支持 ES6 模块格式. 目前, 通过各种工具库, 其实已经做到了这一点.
将来浏览器的新 API 就能用模块格式提供, 不再必须做成全局变量或者 navigator 对象的属性.
不再需要对象作为命名空间(比如 Math 对象), 未来这些功能可以通过模块提供.
总结
CommonJS 规范主要用于服务端编程, 加载模块是同步的, 这并不适合在浏览器环境, 因为同步意味着阻塞加载, 浏览器资源是异步加载的, 因此有了 AMD,CMD 解决方案.
AMD 规范在浏览器环境中异步加载模块, 而且可以并行加载多个模块. 不过, AMD 规范开发成本高, 代码的阅读和书写比较困难, 模块定义方式的语义不顺畅.
CMD 规范与 AMD 规范很相似, 都用于浏览器编程, 依赖就近, 延迟执行, 可以很容易在 Node.JS 中运行. 不过, 依赖 SPM 打包, 模块的加载逻辑偏重.
ES6 在语言标准的层面上, 实现了模块功能, 而且实现得相当简单, 完全可以取代 CommonJS 和 AMD 规范, 成为浏览器和服务器通用的模块解决方案.
以上是本篇文章的内容, 欢迎大家提出自己的想法, 我们一起学习进步, 与君共勉.
参考资料
前端模块化详解(完整版)
ECMAScript 6 入门
近一万字的 ES6 语法知识点补充
彻底搞清楚 JavaScript 中的 require,import 和 export
CommonJS,AMD,CMD,ES6,require 和 import 详解
来源: https://www.2cto.com/kf/201904/805774.html