模块演化
JavaScript 模块演化简史
当年script
标签中:
<!--html-->
<script type="application/javascript">
// module1 code
// module2 code
</script>
不过随着单页应用与富客户端的流行,不断增长的代码库也急需合理的代码分割与依赖管理的解决方案,这也就是我们在软件工程领域所熟悉的模块化window
对象来存放未使用 const
定义的变量。大概在上世纪末,
// file greeting.js
const helloInLang = {
en: "Hello world!",
es: "¡Hola mundo!",
ru: "Привет мир!",
};
function writeHello(lang) {
document.write(helloInLang[lang]);
}
// file hello.js
function writeHello() {
document.write("The script is broken");
}
当我们在页面内同时引入这两个writeHello
函数起了冲突,最后调用的函数取决于我们引入的先后顺序。此外在大型应用中,我们不可能将所有的代码写入到单个jQuery
尚未定义这样的问题。不过物极必反,过度碎片化的模块同样会带来性能的损耗与包体尺寸的增大,这包括了模块加载、模块解析、因为
// index.js
const total = 0;
total += require("./module_0");
total += require("./module_1");
total += require("./module_2");
// etc.
console.log(total);
// module_0.js
module.exports = 0;
// module_1.js
module.exports = 1;
经过
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){const a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);const f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}const l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){const n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}const i=typeof require=="function"&&require;for(const o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
module.exports = 0
},{}],2:[function(require,module,exports){
module.exports = 1
},{}],3:[function(require,module,exports){
module.exports = 10
},{}],4:[function(require,module,exports){
module.exports = 100
// etc.
我们分别测试

命名空间模式
命名空间模式始于myApp_
前缀,譬如 myApp_address
,myApp_validateUser()
等等。同样,我们也可以将函数赋值给模块内的变量或者对象的属性,从而可以使得可以像 document.write()
这样在子命名空间下定义函数而避免冲突。首个采样该设计模式的界面库当属
// file app.js
const app = {};
// file greeting.js
app.helloInLang = {
en: "Hello world!",
es: "¡Hola mundo!",
ru: "Привет мир!",
};
// file hello.js
app.writeHello = function (lang) {
document.write(app.helloInLang[lang]);
};
我们可以发现自定义代码中的所有数据对象与函数都归属于全局对象 app
,不过显而易见这种方式对于大型多人协同项目的可维护性还是较差,并且没有解决模块间依赖管理的问题。另外有时候我们需要处理一些自动执行的
// polyfill-vendor.js
(function () {
// polyfills-vendor code
})();
// module1.js
function module1(params) {
// module1 code
return module1;
}
// module3.js
function module3(params) {
this.a = params.a;
}
module3.prototype.getA = function () {
return this.a;
};
// app.js
const APP = {};
if (isModule1Needed) {
APP.module1 = module1({ param1: 1 });
}
APP.module3 = new module3({ a: 42 });
那么在引入的时候我们需要手动地按照模块间依赖顺序引入进来:
<!--html-->
<script type="application/javascript" src="PATH/polyfill-vendor.js"></script>
<script type="application/javascript" src="PATH/module1.js"></script>
<script type="application/javascript" src="PATH/module2.js"></script>
<script type="application/javascript" src="PATH/app.js"></script>
不过这种方式对于模块间通信也是个麻烦。命名空间模式算是如今
依赖注入
// file greeting.js
angular.module("greeter", []).value("greeting", {
helloInLang: {
en: "Hello world!",
es: "¡Hola mundo!",
ru: "Привет мир!",
},
sayHello: function (lang) {
return this.helloInLang[lang];
},
});
// file app.js
angular.module("app", ["greeter"]).controller("GreetingController", [
"$scope",
"greeting",
function ($scope, greeting) {
$scope.phrase = greeting.sayHello("en");
},
]);
之后在 Angular 2 与 Slot 之中依赖注入仍是核心机制之一,这也是
CommonJS
在const commonjs = require("./commonjs");
,核心设计模式如下所示:
// file greeting.js
const helloInLang = {
en: "Hello world!",
es: "¡Hola mundo!",
ru: "Привет мир!",
};
const sayHello = function (lang) {
return helloInLang[lang];
};
module.exports.sayHello = sayHello;
// file hello.js
const sayHello = require("./lib/greeting").sayHello;
const phrase = sayHello("en");
console.log(phrase);
该模块实现方案主要包含 require
与 module
这两个关键字,其允许某个模块对外暴露部分接口并且由其他模块导入使用。在
(function (exports, require, module, __filename, __dirname) {
// ...
// Your code injects here!
// ...
});
//------ lib.js ------
const counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter, // (A)
incCounter: incCounter,
};
//------ main1.js ------
const counter = require("./lib").counter; // (B)
const incCounter = require("./lib").incCounter;
// The imported value is a (disconnected) copy of a copy
console.log(counter); // 3
incCounter();
console.log(counter); // 3
// The imported value can be changed
counter++;
console.log(counter); // 4
require
函数添加了 main
属性,该属性在执行模块所属文件时指向 module
对象。require
关键字:
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.foo = foo;
function foo() {}
exports.default = 123;
// 在将 ES6 模块编译为 CJS 模块后,可以使用默认导出;并且兼容 ES6 的默认导入
module.exports = require("./dist/index.js").default;
module.exports.default = module.exports;
require.ensure
、require.cache
、require.context
等等。
AMD
就在
define(["amd-module", "../file"], function (amdModule, file) {
require(["big-module/big/file"], function (big) {
const stuff = require("../my/stuff");
});
});
而将我们上述使用的例子改写为
// file lib/greeting.js
define(function () {
const helloInLang = {
en: "Hello world!",
es: "¡Hola mundo!",
ru: "Привет мир!",
};
return {
sayHello: function (lang) {
return helloInLang[lang];
},
};
});
// file hello.js
define(["./lib/greeting"], function (greeting) {
const phrase = greeting.sayHello("en");
document.write(phrase);
});
define
关键字声明了该模块以及外部依赖;当我们执行该模块代码时,也就是执行 define
函数的第二个参数中定义的函数功能,其会在框架将所有的其他依赖模块加载完毕后被执行。这种延迟代码执行的技术也就保证了依赖的并发加载。从我个人而言,npm
为主导的依赖管理机制的统一,越来越多的开发者放弃了使用
UMD
(function (define) {
define(function () {
const helloInLang = {
en: "Hello world!",
es: "¡Hola mundo!",
ru: "Привет мир!",
};
return {
sayHello: function (lang) {
return helloInLang[lang];
},
};
});
})(
typeof module === "object" && module.exports && typeof define !== "function"
? function (factory) {
module.exports = factory();
}
: define
);
该模式的核心思想在于所谓的
function (factory) {
module.exports = factory();
}
而如果是在define
。正是因为这种运行时的灵活性是我们能够将同一份代码运行于不同的环境中。
ES2015 Modules
// file lib/greeting.js
const helloInLang = {
en: "Hello world!",
es: "¡Hola mundo!",
ru: "Привет мир!",
};
export const greeting = {
sayHello: function (lang) {
return helloInLang[lang];
},
};
// file hello.js
import { greeting } from "./lib/greeting";
const phrase = greeting.sayHello("en");
document.write(phrase);
import
与 export
,前者负责导入模块而后者负责导出模块。完整的导出语法如下所示:
// default exports
export default 42;
export default {};
export default [];
export default foo;
export default function () {}
export default class {}
export default function foo () {}
export default class foo {}
// variables exports
export const foo = 1;
export const foo = function () {};
export const bar; // lazy initialization
export let foo = 2;
export let bar; // lazy initialization
export const foo = 3;
export function foo () {}
export class foo {}
// named exports
export {foo};
export {foo, bar};
export {foo as bar};
export {foo as default};
export {foo as default, bar};
// exports from
export * from "foo";
export {foo} from "foo";
export {foo, bar} from "foo";
export {foo as bar} from "foo";
export {foo as default} from "foo";
export {foo as default, bar} from "foo";
export {default} from "foo";
export {default as foo} from "foo";
相对应的完整的支持的导入方式如下所示:
// default imports
import foo from "foo";
import {default as foo} from "foo";
// named imports
import {bar} from "foo";
import {bar, baz} from "foo";
import {bar as baz} from "foo";
import {bar as baz, xyz} from "foo";
// glob imports
import * as foo from "foo";
// mixing imports
import foo, {baz as xyz} from "foo";
import * as bar, {baz as xyz} from "foo";
import foo, * as bar, {baz as xyz} from "foo";
function
里面或是 if
里面:
if(Math.random()>0.5){
import './module1.js'; // SyntaxError: Unexpected keyword 'import'
}
const import2 = (import './main2.js'); // SyntaxError
try{
import './module3.js'; // SyntaxError: Unexpected keyword 'import'
}catch(err){
console.error(err);
}
const moduleNumber = 4;
import module4 from `module${moduleNumber}`; // SyntaxError: Unexpected token
并且import
都必须已经导入完成:
import './module1.js';
alert('code1');
import module2 from './module2.js';
alert('code2');
import module3 from './module3.js';
// 执行结果
module1
module2
module3
code1
code2
并且import
的模块名只能是字符串常量,导入的值也是不可变对象;比如说你不能 import { a } from './a'
然后给