零.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.jsdefine(function () {    // polyfills-vendor code});// module1.jsdefine(function () {    //...    return module1;});// module2.jsdefine(function () {    //...    return module2;});// app.jsdefine(['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.jsmodule.exports= module2;// app.jsrequire('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.jsexport {fn1, fn2};function fn1() {    console.log('fn1');}function fn2() {    console.log('fn2');}// app.jsimport {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, functionexport 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, constexport let a = 1;

默认导出
默认导出是一种特殊的导出形式,例如:

// module.jsexport {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.jsimport * 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.jslet util = {name: 'util'};let dialog = {name: 'core'};let modal = {name: 'modal'};export {    util,    dialog,    modal}// module.jsconsole.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 moduleconst scriptAsBlob = new Blob([''], {    type: 'application/javascript'});const srcObjectURL = URL.createObjectURL(scriptAsBlob); // insert the ES module and listen events on itconst script = document.createElement('script');script.type = 'module';document.head.appendChild(script);// start loading the scriptscript.src = srcObjectURL;

6.受CORS限制
跨域的模块资源无法import引入,也无法通过script标签以模块方式加载

7.HTTPS资源无法importHTTP资源
类似于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

参考资料
Native ECMAScript modules – the first overview:ES Module系列4篇文章都很不错

WHY CHOOSE ES2015 MODULES, BASED ON THE STATE OF THE ART OF JAVASCRIPT MODULARIZATION

import()

ECMAScript modules in browsers

ES Module Spec

MDN | import

MDN | export

更多相关文章

  1. jvm系列(3)类加载机制
  2. 每日学习-ansible replace模块
  3. IDEA + Spring Boot 的三种热加载方案,看完弄懂,不用加班~
  4. 如果原始页面加载是https,那么没有完整网址的jQuery $ .ajax会保
  5. 通过AJAX加载内容和预加载图像?
  6. Jquery点击加载更多
  7. 页面加载后的JQuery(窗口).load?
  8. 如何将加载微调器图像添加到jquery选项卡中的每个选项卡?
  9. ajax cache false无法加载图片

随机推荐

  1. 动手DIY制作个人NAS(一):3D打印硬盘支架
  2. 面试官:手写一个归并排序,并对其改进
  3. java中的对称加密算法
  4. Android碎片(一)
  5. 面试官:java中的编码转化方式都有哪些?(中兴
  6. 五分钟学会java中的基础类型封装类
  7. 设计模式之中介者模式
  8. 数字签名的原理是什么?这篇文章给你答案(ja
  9. 面试官:手写一个希尔排序,并对其改进
  10. 设计模式之访问者模式