前端工程化-ES Modules

ES Modules 是 2015 年推出的,语言层面的模块化规范,与运行环境无关,服务器和浏览器中都能使用。

在 html 中,通过给 script 添加 type = module 的属性,就可以用 ES Module 的标准执行其中的 JS 代码

特性

  • ESM 自动采用严格模式,忽略 'use strict',比如在全局范围不能使用 this

  • 每个 ES Module 都是运行在单独的私有作用域中,不会有全局作用域污染的问题。

  • ESM 通过 CORS【跨源资源共享(Cross-Origin Resource Sharing)】的方式请求外部 JS 模块,所以在跨域请求时,要求服务端支持 CORS。

  • ESM 的 script 标签会延迟执行脚本,等同于 script 标签的 defer 属性 【<script> 详情】。一般情况下,script 脚本在网页加载中默认立即执行 ,页面渲染会等待脚本加载完成之后才继续渲染,而 defer 这种延迟执行的机制可以使得脚本在网页渲染之后才进行执行。

导入和导出

ESM 分别通过 import 和 export 进行模块的导入和导出。

导出

export 可以导出模块内的变量成员、函数成员和类成员,其余未进行导出的成员将以私有成员的方式存在于模块中,外部不可调用。

导出写法有两种:

  1. 声明修饰,在导出成员声明前添加 export 关键字进行修饰。
    示例代码
    // module.js
    // 变量成员
    export var name = 'foo module'
    // 函数成员
    export function hello () {
      console.log('hello')
    }
    // 类成员
    export class Person {}
    
  2. 集中导出,在模块尾部通过 export 关键字导出对外成员组成的集合,注意,。还可以通过 as 关键字对导出成员进行重命名,在导入时则使用重命名之后的成员名。
    示例代码
    // module.js
    // 变量成员
    var name = 'foo module'
    // 函数成员
    function hello () {
      console.log('hello')
    }
    // 类成员
    class Person {}
    // 尾部集中导出
    export { name, hello, Person }
    // as 重命名
    export {
      name as fooName,
      hello as fooHello,
      Person as FooPerson
    } 
    

注意:

  • 将导出成员设置为 default 的则作为模块的默认导出成员。如果以export { name as default } 这种形式默认导出,则在导入的时候必须重命名 import { default as name } from './module.js'才能使用。或者通过 export default name 形式导出,则可以在 import 时设置任意成员名 import fooName from './module.js'
  • {}并不是对象字面量,只是 ESM 导出语法的一种固定用法,如果通过 export default { name } 这种形式 {} 内才为对象字面量。
  • export 所导出的成员,并不是复制,而且原成员的引用,也就是导出的成员名中存储的是原成员的地址,并不是原成员的值。

导入

import 在模块中负责引入其他模块的导出成员。

示例代码
// app.js
import { name, hello, Person } from './module.js'
  1. import name from './module.js' 的导出语法中,from 关键字后紧跟完整的模块路径,且不能省略模块的后缀名,即 .js,否则没法加载模块。

  2. 在加载自定义模块时,不能省略 ./ 的相对路径标识,或者 / 的绝对路径标识,否则会认为是加载第三方模块。也可以直接填写完整的 http 路径访问 CDN 的模块。

    示例代码
    import { name } from 'module.js'
    import { name } from './module.js'
    import { name } from '/04-import/module.js'
    import { name } from 'http://localhost:3000/04-import/module.js'
    
  3. 如果 {} 内为空,则只会执行导入的模块,而不会提取导出成员,可以进行简写,比如: import './module.js'

  4. 假如模块内导出的成员比较多,可以通过 * 进行全部导入,但是需要 as 命名一个对象字面量进行接收,比如:import * as mod from './module.js',mod 就是导出的成员对象字面量。

  5. import 只能是在模块的顶层使用,不能嵌套在其他作用域内,并且没法通过变量声明导入路径。如果想动态导入模块,可以使用 import() 函数在当前模块任意位置导入模块。import() 函数返回的是一个Promise 对象,当模块执行完成之后会自动执行 then 中的回调函数,因此可以通过 then 的回调函数的参数拿到模块的导出成员对象。

    示例代码
    import('./module.js').then(function (module) {
      console.log(module) // 输出模块导出成员对象
    })
    
  6. 如果导入的模块内除了命名的导出成员还有默认的导出成员,那么默认的导出成员需要通过 as 进行重命名,或者通过逗号分隔,逗号左边为默认成员(可以任意命名),右边为命名成员。

    示例代码
    // as 方式导入默认成员
    import { name, age, default as title } from './module.js'
    // 逗号重命名方式导入默认成员
    import abc, { name, age } from './module.js'
    

导入与导出协同

可以将导入的 import 改为 export ,这种写法表示将引入模块的成员直接导出,可以在模块包内设置一个 index 模块,在 index 统一用 export 导出模块包中每个模块的导出成员,这样在其他模块导入模块包时就不需要多次导入。


components 模块包

例如上图的 components 模块包中的 index.js ,统一导出了模块包内的模块导出成员,在外部的 app.js 只需要导入 index.js 就可以了。

// index.js
export { Button } from './button.js'
export { Avatar } from './avatar.js'
// app.js
import { Button, Avatar } from './components/index.js'

Polyfill 兼容方案

因为 ESM 是 2015年推出的,这意味着之前版本的浏览器并不兼容 ESM ,这种兼容问题可以通过在 html 中用 script 标签加载 Polyfill 的兼容方案进行解决。可以在 upkg 上获取对应的资源库。

<body>
  <script nomodule src="https://unpkg.com/promise-polyfill@8.1.3/dist/polyfill.min.js"></script>
  <script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>
  <script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>
</body>

该方案的原理是通过获取 ES Modules 的模块代码,经过 label 兼容转换之后交給 Polyfill 进行执行。 因此在已经支持 ES Modules 的浏览器上同样的代码会执行两次,可以通过 script 新增的 nomodule 属性判断是否支持 ES Modules,不支持则加载。

这种形式的兼容方案只适用于开发阶段的调试,因为其原理是动态的解析脚本,运行效率并不高,因此在生产阶段应直接将代码进行兼容转换,让其直接在浏览器上工作。


ES Modules in Node.js

Node.js 在 8.5 版本之后已经以实验特性的方式支持 ES modules ,因此可以通过原生方式在 Node.js 中编写 ES module 相关代码,但由于 ES modules 与 CommonJS
之间的差距比较大,因此该特性目前仍处于过渡阶段。

在 Node.js 中使用 ES Modules 需要将 JS 文件的扩展名改为 mjs,在命令行启动时需要加上 --experimental-modules

ES Modules 导入模块

Node.js 的内置模块是可以照常通过 import 进行导入的,比如:import fs from 'fs'。但是需要注意的是,第三方模块都是默认导出成员,所以不能通过对象结构的形式导入第三方模块的成员,比如:import { camelCase } from 'lodash' 是不成功的。而内置模块则因为兼容了 ESM 的提取成员方式,所以可以通过对象解构的方式导入,比如:import { writeFileSync } from 'fs' 是成功的。

ES Modules 与 CommonJS 模块交互

ES Modules 模块导入 CommonJS 模块时 CommonJS 模块始终只会导出一个默认成员,即 ES Modules ,模块只能以默认成员的方式导入 CommonJS 模块,同时因为 import 不是解构出对象,所以不能直接提前成员,比如 import { foo } from './commonjs.js' 是不成功的。

注意:Node.js 原生环境中不能在 CommonJS 模块中通过 require 载入 ES Modules

ES Modules 与 CommonJS 差异

ES Modules 中没有 CommonJS 中的那些模块全局成员,比如:requiremoduleexport__filename__dirname

  • moduleexport 在 ESM 可以通过 import 和 export 代替。
  • __filename 可以通过 import 对象下的 meta 属性内的 url 拿到当前文件 URL,再通过 url 模块下的 fileURLToPath 方法将文件 URL 转换成文件路径获得。
  • __dirname可以将上述的 __filename 接着使用 path 模块下的 dirname 方法提取目录部分获得。

Node.js 新版本对 ESM 的支持

Node.js 在最新的版本中可以在 package.json 中添加 "type":"module" 使得当前项目中的 js 文件全部以 ES Modules 的方式执行文件,不再需要将 js 文件扩展名改为 mjs ,如果此时还想使用 CommonJS 的话需要将 js 的扩展名改为 cjs

©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,458评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,030评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,879评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,278评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,296评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,019评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,633评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,541评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,068评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,181评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,318评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,991评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,670评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,183评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,302评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,655评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,327评论 2 358

推荐阅读更多精彩内容