随着 Web 技术的蓬勃发展和依赖的基础设施日益完善,前端领域逐渐从浏览器扩展至服务端(Node.js),桌面端(PC、Android、iOS),乃至于物联网设备(IoT),其中 JavaScript 承载着这些应用程序的核心部分,随着其规模化和复杂度的成倍增加,其软件工程体系也随之创建起来(协同开发、单元测试、需求和缺陷管理等),模块化编程的需求日益迫切。javascript
JavaScript 对模块化编程的支持还没有造成规范,难以堪此重任;一时间,江湖侠士自告奋勇,一路披荆斩棘,从刀耕火种过渡到面向将来的模块化方案;css
<!-- more -->html
模块化编程就是经过组合一些__相对独立可复用的模块__来进行功能的实现,其最核心的两部分是__定义模块__和__引入模块__;前端
尽管 JavaScript 语言层面并未提供模块化的解决方案,但利用其可__面向对象__的语言特性,外加__设计模式__加持,可以实现一些简单的模块化的架构;经典的一个案例是利用单例模式模式去实现模块化,能够对模块进行较好的封装,只暴露部分信息给须要使用模块的地方;vue
// Define a module var moduleA = (function ($, doc) { var methodA = function() {}; var dataA = {}; return { methodA: methodA, dataA: dataA }; })(jQuery, document); // Use a module var result = moduleA.mehodA();
直观来看,经过当即执行函数(IIFE)来声明依赖以及导出数据,这与当下的模块化方案并没有巨大的差别,可本质上却有千差万别,没法知足的一些重要的特性;java
题外话:因为年代久远,这两种模块化方案逐渐淡出历史舞台,具体特性再也不细聊;
为了解决”刀耕火种”时代存留的需求,AMD 和 CMD 模块化规范问世,解决了在浏览器端的异步模块化编程的需求,__其最核心的原理是经过动态加载 script 和事件监听的方式来异步加载模块;__node
AMD 和 CMD 最具表明的两个做品分别对应 require.js 和 sea.js;其主要区别在于依赖声明和依赖加载的时机,其中 require.js 默认在声明时执行, sea.js 推崇懒加载和按需使用;另外值得一提的是,CMD 规范的写法和 CommonJS 极为相近,只需稍做修改,就能在 CommonJS 中使用。参考下面的 Case 更有助于理解;webpack
// AMD define(['./a','./b'], function (moduleA, moduleB) { // 依赖前置 moduleA.mehodA(); console.log(moduleB.dataB); // 导出数据 return {}; }); // CMD define(function (requie, exports, module) { // 依赖就近 var moduleA = require('./a'); moduleA.mehodA(); // 按需加载 if (needModuleB) { var moduleB = requie('./b'); moduleB.methodB(); } // 导出数据 exports = {}; });
2009 年 ry 发布 Node.js 的第一个版本,CommonJS 做为其中最核心的特性之一,适用于服务端下的场景;历年来的考察和时间的洗礼,以及前端工程化对其的充分支持,CommonJS 被普遍运用于 Node.js 和浏览器;ios
// Core Module const cp = require('child_process'); // Npm Module const axios = require('axios'); // Custom Module const foo = require('./foo'); module.exports = { axios }; exports.foo = foo;
一、模块定义git
默认任意 .node .js .json 文件都是符合规范的模块;
二、引入模块
首先从缓存(require.cache)优先读取模块,若是未命中缓存,则进行路径分析,而后按照不一样类型的模块处理:
其中在编译的过程当中,Node对获取的JavaScript文件内容进行了头尾包装,结果以下:
(function (exports, require, module, __filename, __dirname) { var circle = require('./circle.js'); console.log('The area of a circle of radius 4 is ' + circle.area(4)); });
ES Module 是语言层面的模块化方案,由 ES 2015 提出,其规范与 CommonJS 比之 ,导出的值<span data-type="color" style="color:rgb(26, 26, 26)"><span data-type="background" style="background-color:rgb(255, 255, 255)">均可以当作是一个具有多个属性或者方法的对象</span></span>,能够实现互相兼容;但写法上 ES Module 更简洁,与 Python 接近;
import fs from 'fs'; import color from 'color'; import service, { getArticles } from '../service'; export default service; export const getArticles = getArticles;
主要差别在于:
import
export
以及独有的 default
关键字,肯定默认的导出值;只读的值的引用
,不管基础类型和复杂类型,而在 CommonJS 中 require 的是值的拷贝,其中复杂类型是值的浅拷贝;// a.js export let a = 1; export function caculate() { a++; }; // b.js import { a, caculate } from 'a.js'; console.log(a); // 1 caculate(); console.log(a); // 2 a = 2; // Syntax Error: "a" is read-only
经过一层自执行函数来兼容各类模块化规范的写法,兼容 AMD / CMD / CommonJS 等模块化规范,贴上代码赛过千言万语,须要特别注意的是 ES Module 因为会对静态代码进行分析,故这种运行时的方案没法使用,此时经过 CommonJS 进行兼容;
(function (global, factory) { if (typeof exports === 'object') { module.exports = factory(); } else if (typeof define === 'function' && define.amd) { define(factory); } else { this.eventUtil = factory(); } })(this, function (exports) { // Define Module Object.defineProperty(exports, "__esModule", { value: true }); exports.default = 42; });
为了在浏览器环境中运行模块化的代码,须要借助一些模块化打包的工具进行打包( 以 webpack 为例),定义了项目入口以后,会先快速地进行依赖的分析,而后将全部依赖的模块转换成浏览器兼容的对应模块化规范的实现;
从上面的介绍中,咱们已经对其规范和实现有了必定的了解;在浏览器中,要实现 CommonJS 规范,只须要实现 module / exports / require / global 这几个属性,因为浏览器中是没法访问文件系统的,所以 require 过程当中的文件定位须要改造为加载对应的 JS 片断(webpack 采用的方式为经过函数传参实现依赖的引入)。具体实现能够参考:tiny-browser-require。
webpack 打包出来的代码快照以下,注意看注释中的时序;
(function (modules) { // The module cache var installedModules = {}; // The require function function __webpack_require__(moduleId) {} return __webpack_require__(0); // ---> 0 }) ({ 0: function (module, exports, __webpack_require__) { // Define module A var moduleB = __webpack_require__(1); // ---> 1 }, 1: function (module, exports, __webpack_require__) { // Define module B exports = {}; // ---> 2 } });
实际上,ES Module 的处理同 CommonJS 相差无几,只是在定义模块和引入模块时会去处理 __esModule 标识,从而兼容其在语法上的差别。
一、浏览器环境下,网络资源受到较大的限制,所以打包出来的文件若是体积巨大,对页面性能的损耗极大,所以须要对构建的目标文件进行拆分,同时模块也须要支持动态加载;
webpack 提供了两个方法 require.ensure() 和 import() (推荐使用)进行模块的动态加载,至于其中的原理,跟上面说起的 AMD & CMD 所见略同,import() 执行后返回一个 Promise 对象,其中所作的工做无非也是动态新增 script 标签,而后经过 onload / onerror 事件进一步处理。
二、因为 require 函数是彻底自定义的,咱们能够在模块化中实现更多的特性,好比经过修改 require.resolve 或 Module._extensions 扩展支持的文件类型,使得 css / .jsx / .vue / 图片等文件也能为模块化所使用;
模块化规范 | 加载方式 | 加载时机 | 运行环境 | 备注 |
---|---|---|---|---|
AMD | 异步 | 运行时 | 浏览器 | |
CMD | 异步 | 运行时 | 浏览器 | 依赖基于静态分析,require 时已经 module ready |
CommonJS | 同步/异步 | 运行时 | 浏览器 / Node | |
ES Module | 同步/异步 | 编译阶段 | 浏览器 / Node | 经过 import() 实现异步加载 |