零.7种模块化方式
1.分节注释
<!--html-->
<script>
// module1 code
// module2 code
</script>
手动添加注释来标明模块范围,类似于CSS里的分节注释:
/* -----------------
* TOOLTIPS
* ----------------- */
惟一作用是让浏览代码变得容易一些,迅速找到指定模块,根本原因是单文件内容太长,已经遇到了维护的麻烦,所以手动插入一些锚点供快速跳转
非常原始的模块化方案,没有实质性的好处(比如模块作用域,依赖处理,模块间错误隔离等等)
2.多script标签
<!--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>
把各个模块拆分成独立文件,有3个好处:
通过控制资源加载顺序来处理模块依赖
有模块间错误隔离(
module1.js
初始化执行异常不会阻断module2.js
和app.js
的执行)各模块位于单独文件,切实提高了维护体验
但还存在2个问题:
没有模块作用域
资源请求数量与模块化粒度相关,需要寻找性能与模块化收益的平衡
3.IIFE
const myModule = (function (...deps){
// JavaScript chunk
return {hello : () => console.log('hello from myModule')};
})(dependencies);
可以作为补丁,配合其他方式使用,提供模块作用域
4.Asynchronous module definition (AMD)
RequireJS示例:
// polyfill-vendor.js
define(function () {
// polyfills-vendor code
});
// module1.js
define(function () {
//...
return module1;
});
// module2.js
define(function () {
//...
return module2;
});
// app.js
define(['PATH/polyfill-vendor'] , function () {
define(['PATH/module1', 'PATH/module2'] , function (module1, module2) {
var APP = {};
if (isModule1Needed) {
APP.module1 = module1({param: 1});
}
APP.module2 = new module2({a: 42});
});
});
一套比较完善的模块定义方案,解决了模块依赖问题,提供了模块作用域,错误隔离/捕获等方案。但看起来稍微有些冗余
P.S.另外还有SeaJS(官网都没了,不做介绍)。社区实现的模块化补丁都只是过渡产物,目前看来,JS似乎终将迎来模块化特性
5.CommonJS
NodeJS示例:
// polyfill-vendor.js
// polyfills-vendor code
// module1.js
// module1 code
module.exports= module1;
// module2.js
module.exports= module2;
// app.js
require('PATH/polyfill-vendor');
const module1 = require('PATH/module1');
const module2 = require('PATH/module2');
const APP = {};
if(isModule1Needed){
APP.module1 = module1({param:1});
}
APP.module2 = new module2({a: 42});
NodeJS遵循CommonJS规范,文件即模块,同样是一套相对完善的方案,但不适用于浏览器环境
6.UMD (Universal Module Dependency)
UMD示例:
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory() :
typeof define === 'function' && define.amd ? define(factory) :
(factory());
}(this, function () {
// JavaScript chunk
return {
hello : () => console.log(‘hello from myModule’)
}
});
同样是一个补丁,兼容AMD和CommonJS模块定义,实现了模块跨环境通用。出现UMD的根本原因是社区模块定义方式太多了,开源模块维护变得很麻烦(出现各种MD issue,只好换上UMD),所以迫切需要标准化,ES6肩负着这个使命
P.S.当然,开源模块的维护问题还在(为了迎合ES Module,又添上专门的ES6构建版本),但不会加剧,毕竟已经在标准化的路上了
7.ES6 Module
基本用法示例:
// myModule.js
export {fn1, fn2};
function fn1() {
console.log('fn1');
}
function fn2() {
console.log('fn2');
}
// app.js
import {fn1, fn2} from './myModule.js';
fn1();
fn2();
// index.html
<script type="module" src="app.js"></script>
注意:
script
标签必须声明type="module"
表明以ES Module方式解析内容,否则不会执行import
模块文件精确路径(./
)、文件后缀名(.js
)及对应的MIME类型必须要有,否则引入失败
目前各大主流浏览器都提供了ES Module实验性功能:
Safari 10.1.
Chrome Canary 60 – behind the Experimental Web Platform flag in chrome:flags.
Firefox 54 – behind the dom.moduleScripts.enabled setting in about:config.
Edge 15 – behind the Experimental JavaScript Features setting in about:flags.
等了2年的Demo终于能跑起来了:http://ayqy.net/temp/module/index.html
P.S.一般都叫ES Module,因为Module特性不存在多个版本,ES Module指的就是ES6引入的Module特性
一.语法
export
// 基本语法
export { name1, name2, …, nameN };
export { variable1 as name1, variable2 as name2, …, nameN };
export let name1, name2, …, nameN; // also var, function
export let name1 = …, name2 = …, …, nameN; // also var, const
// 默认导出
export default expression;
export default function (…) { … } // also class, function*
export default function name1(…) { … } // also class, function*
export { name1 as default, … };
// 聚合导出
export * from …;
export { name1, name2, …, nameN } from …;
export { import1 as name1, import2 as name2, …, nameN } from …;
注意export
与export default
的区别:
每个模块(/文件)只能有一个
export default
,可以有多个export
export default
后面可以接任意表达式,而export
语法只有3种
例如:
// 不合法,语法错误
export {
a: 1
};
// 而应该用export { name1, name2, …, nameN };
let a = 1;
export {
a
};
// 或者export let name1 = …, name2 = …, …, nameN; // also var, const
export let a = 1;
默认导出
默认导出是一种特殊的导出形式,例如:
// module.js
export {fn1, fn2};
function fn1() {
console.log('fn1');
}
function fn2() {
console.log('fn2');
}
export default {
a: 1
};
let b = 2;
export {
b
};
export let c = 3;
// app.js
import * as m from './module.js';
console.log(m);
// 输出结果
Module {
b: 2,
c: 3,
default: {
a: 1
},
fn1: ƒn1,
fn2: ƒn2
}
默认导出被隔离在Module
对象的default
属性里,与其它export
待遇不同
聚合导出
相当于import + export
,但不会在当前模块作用域引入各个API变量(导入后直接导出,无法引用),仅起API聚合的中转作用,例如:
// lib.js
let util = {name: 'util'};
let dialog = {name: 'core'};
let modal = {name: 'modal'};
export {
util,
dialog,
modal
}
// module.js
console.log(`before export from lib: ${typeof dialog}`);
export * from './lib.js';
console.log(`after export from lib: ${typeof dialog}`);
前后都是undefined
,因为仅中转,不在当前模块作用域引入。而import + export
会先引入,在当前模块可用
import
// 引入default export内容
import defaultMember from "module-name";
// 引入所有export内容,包括default,并打包到名为mame的对象
import * as name from "module-name";
// 按名引入指定export内容
import { member } from "module-name";
import { member as alias } from "module-name";
import { member1, member2 } from "module-name";
import { member1, member2 as alias2 , [...] } from "module-name";
// 引入default export内容,同时按名引入指定export内容
import defaultMember, { member [ , [...] ] } from "module-name";
import defaultMember, * as name from "module-name";
// 不引入模块里暴露的东西,仅执行该模块代码
import "module-name";
最后一种比较有意思,被称为Import a module for its side effects only,仅执行模块代码,不引入任何新东西(只有影响外部状态的部分会生效,即副作用)
P.S.关于ES Module语法的更多信息,请查看module_ES6笔记13,或者参考资料部分的ES Module Spec
P.S.NodeJS也在考虑支持ES Module,但遇到了怎么区分CommonJS模块和ES Module的问题,还在讨论中,更多信息请查看ES Module Detection in Node
二.加载机制
也就是说:
type="module"
的资源相当于自带defer
效果(等到HTML文档解析完毕才执行)async
依然有效(资源加载完毕后立即执行,执行完继续解析HTML文档)import
资源加载是并行的
自带defer
效果,与裸script
默认行为(加载资源立即执行,并且阻塞HTML文档解析)不同。另外,虽然import
加载同级资源是并行的,但寻找下一级依赖的过程不可避免是顺序串行的,这部分性能无法忽略,即便浏览器原生支持了ES Module,也不能肆无忌惮地import
类似于CSS中的@import
规则,可能会发展出最佳实践,在模块化与加载性能之间寻求平衡
三.特点
1.静态机制
不能在if
,try-catch
语句,函数或者eval
等地方使用import
,只能出现在模块最外层
并且import
有提升(Hosting)特性,如同变量声明被提升到当前作用域顶部一样,模块里声明的import
会被提升到模块顶部
P.S.静态模块机制有利于做解析/执行优化
2.新script类型
需要用新的script
类型属性type="module"
。因为解析器没有办法推测出内容是不是ES Module(比如没有import, export
关键字,也遵循严格模式,那么算不算个模块?)
另外,根据内容猜测存在多次解析的性能损耗
3.模块作用域
每个模块有自己的作用域,模块下的变量声明不会暴露到全局
4.默认开启严格模式
this
不指向global
,而是undefined
5.支持Data URI和Blob URI
import grape from 'data:text/javascript,export default "grape"';
// create an empty ES module
const scriptAsBlob = new Blob([''], {
type: 'application/javascript'
});
const srcObjectURL = URL.createObjectURL(scriptAsBlob);
// insert the ES module and listen events on it
const script = document.createElement('script');
script.type = 'module';
document.head.appendChild(script);
// start loading the script
script.src = srcObjectURL;
6.受CORS限制
跨域的模块资源无法import
引入,也无法通过script
标签以模块方式加载
7.HTTPS资源无法import
HTTP资源
类似于HTTPS页面加载HTTP资源,会被block掉
8.模块是单例
不同于普通script
,引入的模块是单例(只执行一次),无论是import
还是通过type="module"
的script
标签引入
9.请求模块资源不带身份凭证(credentials)
与Fetch API脾气一样,默认不带身份证,需要给script
标签添上crossorigin
属性
四.问题
1.import报错
必须要给出精确的模块文件路径,否则不会执行模块内容,并且Chrome 60连报错都没有
P.S.import
报错目前各浏览器还存在差异
2.模块间错误隔离仍然是个问题
资源加载错误:动态插入script
加载模块,onerror
监听加载异常
模块初始化错误:window.onerror
全局捕获,尝试通过错误信息找出模块名,记下模块初始化失败
3.请求数量爆炸
比如lodash demo,需要加载600多个文件
上HTTP2能缓解碎文件的问题,但从根源看,需要一套适用于生产环境的最佳实践,规范模块化的粒度
4.动态import
目前还没有实现,import()
API专门解决这个问题,规范还处于草案第3阶段,更多信息请查看Native ECMAScript modules: dynamic import()
5.模块环境检测
检查当前执行环境是不是模块:
const inModule = this === undefined;
看起来不很靠谱,但似乎只能这么干,因为document.currentScript
在ES Module是null
,没办法做type
检查
五.降级方案
1.特性检测
过一遍特性检测,由环境检测util引入模块,比较费劲且亏性能,例如malyw/es-modules-utils
typeof
行不通,因为import, export
是关键字,可以插入type="module"
的script
标签,加载空模块(可以用Blob URI或者Data URI),触发onload
说明支持
另外还有一种取巧的方法:
<script type="module">
window.__browserHasModules = true;
</script>
引入这样的模块做特性检测,但因为ES Module自带defer
效果,为了保证执行顺序,后续所有JS资源都要有defer
属性(包括用于降级的正常版本)
2.nomodule
nomodule
属性,作用类似于noscript
标签,<script nomodule>console.log('仅在不支持ES Module的环境执行')</script>
但依赖浏览器支持,在不支持该属性但支持ES Module的环境就有问题了(两个都执行),已经添到了HTML规范,但目前兼容性还比较差:
Firefox最新版支持
Edge不支持
Safari 10.1不支持,但有办法解决
Chrome 60支持
关于降级方案的更多信息,请查看Native ECMAScript modules: nomodule attribute for the migration