1. 什么是模块化?
2. 模块化极简史
2.1. 无模块化时代
2.2. 传统模块化阶段
2.3. 百家争鸣:CommonJS、AMD、CMD
2.4. 一统天下,ES6 Module
3. Webpack模块化原理
3.1. 基本原理
3.2. webpack 怎么对待 CommonJS 模块?
3.3. webpack 怎么对待 AMD 模块?
3.4. webpack 怎么对待 CMD 模块?
3.5. webpack 怎么对待 ES6 模块?
3.6. webpack 怎么对待 动态 模块?
1. 什么是模块化?
模块化开发就是封装细节,提供使用接口,彼此之间互不影响,每个模块都是实现某一特定的功能。
——《软件工程》
在模块化编程中,开发者将程序分解成离散功能块(discrete chunks of functionality),并称之为模块。
每个模块具有比完整程序更小的接触面,使得校验、调试、测试轻而易举。精心编写的模块提供了可靠的抽象和封装界限,使得应用程序中每个模块都具有条理清楚的设计和明确的目的。
——《Webpack官网》
有啥好处?
a. 避免命名冲突(不占全局命名空间);
b. 便于依赖管理(无须手动组织JS文件顺序);
c. 利于性能优化(异步模块加载);
e. 提高可维护性;
f. 利于代码复用;
2. 模块化极简史
2.1. 无模块化时代
最初,大家只是把项目中的功能,以文件为单位进行划分;这么干的结果是.....所有的变量、函数都暴露在全局作用域;多人协作开发时,极易出现命名冲突,也容易为了避免命名冲突,硬造一些稀奇古怪的名字....时间越长,麻烦越多...维护成本也越高...
2.2. 传统模块化阶段
这一阶段,WEB开发人员主要是利用JS语言的闭包、原型、函数作用域等特性,减少对全局命名空间的污染;方式方法各有不同,但结果都差不多,比较混乱...
A. “对象”型模块
B. “仿Java类”型模块
C. “立即执行函数(IIFE)”型模块
D. “全局变量输入”型模块
注:上面仅列举了传统模块化方法中的几种常见代码组织形式,还有“放大型”、“宽松放大型”等其它方法,这里就不一一列举了,有兴趣可以看看下面这篇文章... http://www.adequatelygood.com/JavaScript-Module-Pattern-In-Depth.html
传统模块化方法中
基本做到了让模块更独立、减少模块间冲突
但还有个更重要的问题没解决掉
如何清晰地描述模块间依赖?
小结一下:
优点: 传统模块化相比于无模块化时代,显然是进步的:减少了命名冲突,增强了模块的独立性; 缺点: 1. 污染全局作用域:虽然我们通过各种手段尽力避免,但实际未从根本上解决; 2. 依赖关系不明显:对于大型项目,模块数量巨大,开发人员必须手动解决模块间依赖,这在复杂项目中极易出错且维护成本高!
2.3. 百家争鸣:CommonJS、AMD、CMD
JavaScript在语言层面迟迟不推出模块化功能,这个背景下,各“民间组织”提出了CommonJS、AMD、CMD模块化规范.....
——《高手在民间》
A. CommonJS
Node.js的诞生,使JavaScript扩展到了服务器端, 为了让JavaScript在服务器端能跟Java、Phyton一样编写大型程序,于是有了CommonJS模块化规范;
(1). CommonJS是针对服务器端(非浏览器环境)的JavaScript开发,是Node.js的默认模块化规范; (2). CommonJS是一种只适用于JavaScript的静态模块化规范; 注:只适用于JavaScript,意味着它无法把CSS等前端资源纳入模块化管理范围,但显然CSS也是组成前端模块的重要部分; 注:静态模块化规范,意味着它无法实现按需加载; (3). CommonJS所有模块均是同步阻塞式加载,无法实现异步加载; 注:服务器端加载模块是从硬盘直接读取,时间消耗和忽略不计;但浏览器端需要经网络下载,时间消耗取决于网速,同步加载策略容易出现“假死”,因此“同步阻塞式”加载策略不适用于浏览器环境;
CommonJS模块示例:
CommonJS是针对服务器端JavaScript的规范
但不适用于浏览器端
于是衍生出针对浏览器端的
AMD和CMD规范
B. AMD
AMD(Asynchronous Module Definition),异步模块定义;
实现:RequireJS; 特性:依赖前置,提前执行;
AMD模块示例:
C. CMD(Common Module Definition)
CMD(Common Module Definition),通用模块定义;CMD与AMD很类似,只是在模块的运行、解析时机上有所不同;
实现:SeaJS; 特性:依赖就近,延迟执行;
CMD模块示例:
注:CommonJS、AMD、CMD都是语言规范缺失背景下的产物,了解即可;
2.4. 一统天下,ES6 Module
ES6在语言规格的层面上实现了模块功能,而且实现的相当简单,完全可以取代现有的CommonJS、AMD和CMD规范,成为浏览器和服务器通用的模块解决方案;
特点:
语言级、静态模块化规范; 实现按需加载的dynamic import语法提案现处于stage3阶段; 完全可替代CommonJS、AMD、CMD;
ES6 Module示例1(静态):
ES6 Module示例2(动态):
注:ES6模块规范和dynamic-import提案,各浏览器还不能完美支持,仍需要JavaScript的编译器babel帮忙...
小结一下
传统模块化手段:通过JS的闭包、对象、自执行函数等语言特性,避免模块间的命名冲突,提高模块的内聚性,但无统一编程标准,也无法把模块间的依赖关系描述清晰; CommonJS:Node.js让JavaScript延伸到“服务端”领域,促使针对“服务端”的JavaScript静态模块化规范CommonJS诞生,但此规范的“同步阻塞式”模块加载策略不适用于浏览器端环境; AMD,CMD:CommonJS规范的衍生品,支持模块“异步并行加载”,适用浏览器环境;AMD推崇“依赖前置”、CMD则是“依赖后置”;AMD规范的产物为RequireJS,CMD是SeaJS; ES6 Module:官方模块化标准,是语言的一部分,无需额外引入第三方库;ES6 Module同CommonJS一样,也是静态模块化规范,无法实现“按需加载”;但目前有一份处于stage3阶段的 dynamic import(tc39/proposal-dynamic-import)提案可用于动态模块加载;ES6完全可以取代CommonJS、AMD、CMD,成为浏览器和服务器端通用的模块化解决方案;
3. Webpack 模块化原理
3.1. 基本原理
先来观察最、最、最简单的 webpack 打包示例。
webpack.config.js:
// "webpack": "^4.42.1",
module.exports = {
mode: 'development',
devtool: false,
entry: './src/entry.js'
};
src/entry.js:【只有这么一个文件】
alert("This is entry!");
dist/main.js:【webpack 打包输出】
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = "./src/entry.js");
/******/ })
/************************************************************************/
/******/ ({
/***/ "./src/entry.js":
/*!**********************!*\
!*** ./src/entry.js ***!
\**********************/
/*! no static exports found */
/***/ (function(module, exports) {
alert("This is entry!");
/***/ })
/******/ });
3.2. webpack 怎么对待 CommonJS 模块?
webpack.config.js:
module.exports = {
mode: 'development',
devtool: false,
entry: './src/entry.js'
};
src/cjs.js:【CommonJS模块】
module.exports.m1 = function(){
console.log("cjs-m1");
};
module.exports.m2 = function(){
console.log("cjs-m2");
};
src/entry.js:【CommonJS模块】
const cjs = require("./cjs");
cjs.m1();
cjs.m2();
dist/main.js:【打包结果】
3.3. webpack 怎么对待 AMD 模块?
webpack.config.js:
module.exports = {
mode: 'development',
devtool: false,
entry: './src/entry.js'
};
src/amd.js:【AMD模块】
define(["./amd1"], function ([amd1]) {
amd1.m3();
amd1.m4();
return {
m1: function(){
console.log("amd-m1");
},
m2: function(){
console.log("amd-m2");
}
}
});
src/amd1.js:【AMD模块】
define(function () {
return {
m3: function(){
console.log("amd-m3");
},
m4: function(){
console.log("amd-m4");
}
}
});
src/entry.js:【CommonJS模块】
const amd = require("./amd");
amd.m1();
amd.m2();
dist/main.js:【打包结果】
3.4. webpack 怎么对待 CMD 模块?
webpack.config.js:
module.exports = {
mode: 'development',
devtool: false,
entry: './src/entry.js'
};
src/cmd.js:【CMD模块】
define(function(require, exports, module) {
const m = require('./cmd1');
m.m3();
m.m4();
exports.m1 = function(){
console.log("cmd-m1");
};
exports.m2 = function(){
console.log("cmd-m2");
}
});
src/cmd1.js:【CMD模块】
define(function(require, exports, module) {
exports.m3 = function(){
console.log("cmd-m3");
};
exports.m4 = function(){
console.log("cmd-m4");
}
});
src/entry.js:【CommonJS模块】
const cmd = require("./cmd");
cmd.m1();
cmd.m2();
dist/main.js:【打包结果】
3.5. webpack 怎么对待 ES6 模块?
webpack.config.js:
module.exports = {
mode: 'development',
devtool: false,
entry: './src/entry.js'
};
src/es.js:【ES6模块】
import es1default, {m1 as es1m1, m2 as es1m2} from "./es1"
es1default();
es1m1();
es1m2();
export default function m(){
console.log('es-default');
}
export function m1(){
console.log('es-m1');
}
export function m2(){
console.log('es-m2');
}
src/es1.js:【ES6模块】
export default function m(){
console.log('es1-default');
}
export function m1(){
console.log('es1-m1');
}
export function m2(){
console.log('es1-m2');
}
src/entry.js:【CommonJS模块】
const es= require("./es");
es.default();
es.m1();
es.m2();
dist/main.js:【打包结果】
3.6. webpack 怎么对待 动态 模块?
webpack.config.js:
module.exports = {
mode: 'development',
devtool: false,
entry: './src/entry.js'
};
src/folder/es2.js:【ES6模块】
export default function m(){
console.log('es2-default');
}
export function m1(){
console.log('es2-m1');
}
export function m2(){
console.log('es2-m2');
}
src/folder/es3.js:【ES6模块】
export default function m(){
console.log('es3-default');
}
export function m1(){
console.log('es3-m1');
}
export function m2(){
console.log('es3-m2');
}
src/entry.js:【ES6模块】
import (/* webpackChunkName:"dynamic" */ "./folder/es2").then((module)=>{
module.default();
module.m2();
module.m1();
});
import (/* webpackChunkName:"dynamic" */ "./folder/es3").then((module)=>{
module.default();
module.m2();
module.m1();
});
dist/main.js:【打包结果-入口模块】
/******/ (function(modules) { // webpackBootstrap
/******/ // install a JSONP callback for chunk loading
/******/ function webpackJsonpCallback(data) {
/******/ var chunkIds = data[0];
/******/ var moreModules = data[1];
/******/
/******/
/******/ // add "moreModules" to the modules object,
/******/ // then flag all "chunkIds" as loaded and fire callback
/******/ var moduleId, chunkId, i = 0, resolves = [];
/******/ for(;i < chunkIds.length; i++) {
/******/ chunkId = chunkIds[i];
/******/ if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
/******/ resolves.push(installedChunks[chunkId][0]);
/******/ }
/******/ installedChunks[chunkId] = 0;
/******/ }
/******/ for(moduleId in moreModules) {
/******/ if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
/******/ modules[moduleId] = moreModules[moduleId];
/******/ }
/******/ }
/******/ if(parentJsonpFunction) parentJsonpFunction(data);
/******/
/******/ while(resolves.length) {
/******/ resolves.shift()();
/******/ }
/******/
/******/ };
/******/
/******/
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // object to store loaded and loading chunks
/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched
/******/ // Promise = chunk loading, 0 = chunk loaded
/******/ var installedChunks = {
/******/ "main": 0
/******/ };
/******/
/******/
/******/
/******/ // script path function
/******/ function jsonpScriptSrc(chunkId) {
/******/ return __webpack_require__.p + "" + ({"dynamic":"dynamic"}[chunkId]||chunkId) + ".js"
/******/ }
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) // 没变;
/******/
/******/ // This file contains only the entry chunk.
/******/ // The chunk loading function for additional chunks
/******/ __webpack_require__.e = function requireEnsure(chunkId) {
/******/ var promises = [];
/******/
/******/
/******/ // JSONP chunk loading for javascript
/******/
/******/ var installedChunkData = installedChunks[chunkId];
/******/ if(installedChunkData !== 0) { // 0 means "already installed".
/******/
/******/ // a Promise means "currently loading".
/******/ if(installedChunkData) {
/******/ promises.push(installedChunkData[2]);
/******/ } else {
/******/ // setup Promise in chunk cache
/******/ var promise = new Promise(function(resolve, reject) {
/******/ installedChunkData = installedChunks[chunkId] = [resolve, reject];
/******/ });
/******/ promises.push(installedChunkData[2] = promise);
/******/
/******/ // start chunk loading
/******/ var script = document.createElement('script');
/******/ var onScriptComplete;
/******/
/******/ script.charset = 'utf-8';
/******/ script.timeout = 120;
/******/ if (__webpack_require__.nc) {
/******/ script.setAttribute("nonce", __webpack_require__.nc);
/******/ }
/******/ script.src = jsonpScriptSrc(chunkId);
/******/
/******/ // create error before stack unwound to get useful stacktrace later
/******/ var error = new Error();
/******/ onScriptComplete = function (event) {
/******/ // avoid mem leaks in IE.
/******/ script.onerror = script.onload = null;
/******/ clearTimeout(timeout);
/******/ var chunk = installedChunks[chunkId];
/******/ if(chunk !== 0) {
/******/ if(chunk) {
/******/ var errorType = event && (event.type === 'load' ? 'missing' : event.type);
/******/ var realSrc = event && event.target && event.target.src;
/******/ error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
/******/ error.name = 'ChunkLoadError';
/******/ error.type = errorType;
/******/ error.request = realSrc;
/******/ chunk[1](error);
/******/ }
/******/ installedChunks[chunkId] = undefined;
/******/ }
/******/ };
/******/ var timeout = setTimeout(function(){
/******/ onScriptComplete({ type: 'timeout', target: script });
/******/ }, 120000);
/******/ script.onerror = script.onload = onScriptComplete;
/******/ document.head.appendChild(script);
/******/ }
/******/ }
/******/ return Promise.all(promises);
/******/ };
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = // 没变;
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = // 没变;
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = // 没变;
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = // 没变;
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/ // on error function for async loading
/******/ __webpack_require__.oe = function(err) { console.error(err); throw err; };
/******/
/******/ var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
/******/ var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
/******/ jsonpArray.push = webpackJsonpCallback;
/******/ jsonpArray = jsonpArray.slice();
/******/ for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
/******/ var parentJsonpFunction = oldJsonpFunction;
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = "./src/entry.js");
/******/ })
/************************************************************************/
/******/ ({
/***/ "./src/entry.js":
/*!**********************!*\
!*** ./src/entry.js ***!
\**********************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
__webpack_require__.e(/*! import() | dynamic */ "dynamic").then(__webpack_require__.bind(null, /*! ./folder/es2 */ "./src/folder/es2.js")).then((module)=>{
module.default();
module.m2();
module.m1();
});
__webpack_require__.e(/*! import() | dynamic */ "dynamic").then(__webpack_require__.bind(null, /*! ./folder/es3 */ "./src/folder/es3.js")).then((module)=>{
module.default();
module.m2();
module.m1();
});
/***/ })
/******/ });
dist/dynamic.js:【打包结果-动态模块】
几个关键点:
参考:
传统模块化编程方法: http://www.adequatelygood.com/JavaScript-Module-Pattern-In-Depth.html AMD规范:https://github.com/amdjs/amdjs-api/wiki RequireJS:http://requirejs.org/ CMD规范:https://github.com/seajs/seajs/issues/242 SeaJS:http://www.zhangxinxu.com/sp/seajs/ 《前端工程化体系设计与实践》 avaScript Module Systems Showdown: CommonJS vs AMD vs ES2015: https://auth0.com/blog/javascript-module-systems-showdown/