ES Module

零.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.jsapp.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 …;

注意exportexport 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.静态机制

不能在iftry-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资源无法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

本文分享自微信公众号 - 前端向后(backward-fe),作者:ayqy

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2017-09-03

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • flexbox布局指南

    伸缩容器是display的计算值为flex或inline-flex的元素,其流内孩子就是伸缩项(flex item)

    ayqy贾杰
  • 运行时依赖收集机制

    精确数据绑定是指一次数据变化对视图的影响是可以精确预知的,不需要通过额外的检查(子树脏检查、子树diff)来进一步确认

    ayqy贾杰
  • 消息队列为什么说它像漏斗?

    试想,如果工作量持续增长,串行模式的延迟将会越来越大,而且无法通过加资源来解决,可扩展性无从谈起

    ayqy贾杰
  • 在windows下通过telnet连接virtualbox下的linux

    之前,在virtualbox安装了fedora 13,今天突发奇想,想通过客户机连接里头的虚拟机,或者,通过虚拟机连接客户机。

    williamwong
  • 右键添加"Open in Terminal"选项

    如果刷的系统是正版Ubuntu,想在右键添加”Open in Terminal”选项:

    JNingWei
  • leetcode: 36. Valid Sudoku

    JNingWei
  • mysql-表的操作

    数据库中的表也应该有不同的类型,表的类型不同,会对应mysql不同的存取机制,表类型又称为存储引擎

    py3study
  • 开源PaaS Rainbond 3.6.1 Released

    本次3.6.1版本更新,重点修复了3.6.0版本部分情况下会出现的BUG,同时改进了内部市场、参数验证、历史消息等功能,详细更新记录如下——

    Rainbond开源
  • 常见动态规划的解决思路

    给定一个词的集合words,使用badness(i,j)表示使用的单词是words[i,j]

    爬蜥
  • 压缩时间:宝洁供应链优化| 案例分享

    在宝洁的发展历程中,通过缩短距离,更加深入地研究消费者,是宝洁的第三核心竞争力。下面以宝洁公司的香波产品供应链优化为例,详细剖析宝洁供应链的优化方法。

    用户5495712

扫码关注云+社区

领取腾讯云代金券