ES6 的模块体系

此文为翻译,原文地点在这儿:https://hacks.mozilla.org/2015/08/es6-in-depth-modules/

ES6 是 ECMAScript 第 6 版本的简称,这是新一代的 JavaScript 的范例。ES6 in Depth 是关于 ES6 的一系列新特征的引见。

遐想 2007 年,笔者最先在 Mozilla 的 JavaScript 团队事变的时刻,谁人时刻典范的 JavaScript 顺序只需一行代码。

两年以后, Google Map 被宣布。然则在那之前不久,JavaScript 的重要用途照样表单考证,固然啦,你的<input onchange=>处置惩罚器均匀来讲只需一行。

事过情迁,JavaScript 项目已变得异常巨大,社区也生长出了一些有助于开辟可扩大顺序的东西。起首你须要的就是模块系统。模块系统让你得以将你的事变疏散在差别的文件和目次中,让它们之前得以相互接见,而且能够异常有效地加载它们。自但是然地,JavaScript 生长出了模块系统,事实上是多个模块系统(AMD,CommonJS,CMD,译者注)。不仅如此,社区还供应了包管理东西(NPM,译者注),让你能够装置和拷贝高度依靠其他模块的软件。或许你会以为,带有模块特征的 ES6,来得有些晚了。

模块基础

一个 ES6 的模块是一个包含了 JS 代码的文件。ES6 里没有所谓的 module 症结字。一个模块看起来就和一个一般的剧本文件一样,除了以下两个区分:

  • ES6 的模块自动开启严厉情势,纵然你没有写 'use strict'

  • 你能够在模块中运用 importexport

让我们先来看看 export。在模块中声明的任何东西都是默许私有的,假如你想对其他模块 Public,你必需 export 那部份代码。我们有几种完成要领,最简朴的体式格局是增加一个 export 症结字。

// kittydar.js - Find the locations of all the cats in an image.
// (Heather Arthur wrote this library for real)
// (but she didn't use modules, because it was 2013)

export function detectCats(canvas, options) {
  var kittydar = new Kittydar(options);
  return kittydar.detectCats(canvas);
}

export class Kittydar {
  ... several methods doing image processing ...
}

// This helper function isn't exported.
function resizeCanvas() {
  ...
}
...

你能够在 functionclassvarletconst 前增加 export

假如你想写一个模块,有这些就够了!再也不必把代码放在 IIFE 或许一个回调函数里了。既然你的代码是一个模块,而非剧本文件,那末你性命的统统都邑被封装进模块的作用域,不再会有跨模块或跨文件的全局变量。你导出的声明部份则会成为这个模块的 Public API。

除此之外,模块里的代码和一般代码没啥大区分。它能够接见一些基础的全局变量,比方 ObjectArray。假如你的模块跑在浏览器里,它将能够接见 documentXMLHttpRequest

在别的一个文件中,我们能够导入这个模块而且运用 detectCats() 函数:

// demo.js - Kittydar demo program

import {detectCats} from "kittydar.js";

function go() {
    var canvas = document.getElementById("catpix");
    var cats = detectCats(canvas);
    drawRectangles(canvas, cats);
}

要导入多个模块中的接口,你能够如许写:

import {detectCats, Kittydar} from "kittydar.js";

当你运转一个包含 import 声明的模块,被引入的模块会先被导入并加载,然后依据依靠关联,每一个模块的内容会运用深度优先的准绳举行遍历。跳过已实行过的模块,以此防止依靠轮回。

这就是模块的基础部份,挺简朴的。

导出表

假如你以为在每一个要导出的部份前都写上 export 很贫苦,你能够只写一行你想要导出的变量列表,再用花括号包起来。

export {detectCats, Kittydar};

// no `export` keyword required here
function detectCats(canvas, options) { ... }
class Kittydar { ... }

导出表不一定要涌现在文件的第一行,它能够涌现在模块顶级作用域中的任何一行。你能够写多个导出表,也能够在列表中再写上其他 export 声明,只需没有变量名被反复导出即可。

重名命导出和导入

假如导入的变量名正好和你模块中的变量名争执了,ES6 许可你给你导入的东西重命名:

// suburbia.js

// Both these modules export something named `flip`.
// To import them both, we must rename at least one.
import {flip as flipOmelet} from "eggs.js";
import {flip as flipHouse} from "real-estate.js";
...

相似地,你在导出变量的时刻也能重命名。这个特征在你想将同一个变量名导出两次的场景下异常轻易,举个栗子:

// unlicensed_nuclear_accelerator.js - media streaming without drm
// (not a real library, but maybe it should be)

function v1() { ... }
function v2() { ... }

export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};

默许导出

新一代的范例的设想理念是兼容现有的 CommonJSAMD 模块。所以假如你有一个 Node 项目,而且方才实行完 npm install lodash,你的 ES6 代码能够自力引入 Lodash 中的函数:

import {each, map} from "lodash";

each([3, 2, 1], x => console.log(x));

但是假如你已习惯了 _.each 或许看不见 _ 的话就满身难熬痛苦,固然如许运用 Lodash 也是不错的体式格局

这类情况下,你能够轻微转变一下你的 import 写法,不写花括号:

import _ from "lodash";

这个简写等价于 import {default as _} from "lodash";。一切 CommonJS 和 AMD 模块在被 ES6 代码运用的时刻都已有了默许的导出,这个导出和你在 CommonJS 中 require() 获得的东西是一样的,那就是 exports 对象。

ES6 的模块系统被设想成让你能够一次性引入多个变量。但关于已存在的 CommonJS 模块来讲,你能获得的只需默许导出。举个栗子,在撰写此文之时,据笔者所知,有名的 colors 模块并未特地支撑 ES6。这是一个由多个 CommonJS 模块构成的模块,正如 npm 上的那些包。但是你依旧能够直接将其引入到你的 ES6 代码中。

// ES6 equivalent of `var colors = require("colors/safe");`
import colors from "colors/safe";

假如你想写本身的默许导出,那也很简朴。这内里并没有什么高科技,它和一般的导出没什么两样,除了它的导着名是 default。你能够运用我们之前已引见过的语法:

let myObject = {
  field1: value1,
  field2: value2
};
export {myObject as default};

如许更好:

export default {
  field1: value1,
  field2: value2
};

export default 症结字后能够追随任何值:函数,对象,对象字面量,任何你能说得出的东西。

模块对象

抱歉,这篇文章的内容有点多,但 JavaScript 已算好的了:由于一些缘由,一切言语的模块系统都有一大堆没什么卵用的特征。所幸的是,我们只需一个话题要议论了,呃,好吧,两个。

import * as cows from "cows";

当你 import *,被引入进来的是一个 module namespace object。它的属性是谁人模块的导出,所以假如 “cows” 模块导出了一个名为 moo() 的函数,当你像如许引入了 “cows” 以后,你能够如许写 cows.moo()

聚合模块

有时刻一个包的主模块会引入很多其他模块,然后再将它们以一个一致的体式格局导出。为了简化如许的代码,我们有一个 import-and-export 的简写要领:

// world-foods.js - good stuff from all over

// import "sri-lanka" and re-export some of its exports
export {Tea, Cinnamon} from "sri-lanka";

// import "equatorial-guinea" and re-export some of its exports
export {Coffee, Cocoa} from "equatorial-guinea";

// import "singapore" and export ALL of its exports
export * from "singapore";

这类 export-from 的表达式和背面跟了一个 exportimport-from 表达式相似。但和真正的导入差别,它并不会在你的作用域中到场二次导出的变量绑定。所以假如你打算在 world-foods.js 写用到了 Tea 的代码,就别运用这个简写情势。

假如 “singapore” 导出的某一个变量碰巧和其他的导出变量名争执了,那末这里就会涌现一个毛病。所以你应当郑重运用 export *

Whew!我们引见完语法了,接下来进入风趣的环节。

import 究竟干了啥

啥也没干,信不信由你。

噢,你彷佛看起来没那末好骗。好吧,那你置信范例几乎没有谈到 import 该做什么吗?你以为这是一件功德照样坏事呢?

ES6 将模块的加载细节完整交给了完成,其他的实行部份则划定得异常细致

大抵来讲,当 JS 引擎运转一个模块的时刻,它的行动大抵可归结为以下四步:

  1. 剖析:引擎完成会浏览模块的源码,而且搜检是不是有语法毛病。

  2. 加载:引擎完成会(递归地)加载一切被引入的模块。这部份咱还没范例化。

  3. 链接:引擎完成会为每一个新加载的模块建立一个作用域,而且将模块中的声明绑定填入个中,包含从其他模块中引入的。

当你尝试 import {cake} from "paleo" 然则 “paleo” 模块并没有导出叫 cake 的东西时刻,你也会在此时获得毛病。这很蹩脚,由于你离实行 JS,品味 cake 只差一步了!

  1. 实行:终究,JS 引擎最先实行刚加载进来的模块中的代码。到这个时刻,import 的处置惩罚历程已完成,因而当 JS 引擎实行到一行 import 声明的时刻,它啥也不会干。

看到了不?我说了 import “啥也没干”,没骗你吧?有关编程言语的庄重话题,哥从不撒谎。

不过,现在我们能够引见这个系统中风趣的部份了,这是一个异常酷的 trick。正由于这个系统并没有指定加载的细节,也由于你只须要看一眼源码中的 import 声明就能够在运转前搞清楚模块的依靠,某些 ES6 的完成以至能够经由过程预处置惩罚就完成一切的事变,然后将模块悉数打包成一个文件,末了经由过程收集分发。像 webpack 如许的东西就是做这个事变的。

这异常的了不得,由于经由过程收集加载资本是异常耗时的。假定你要求一个资本,接着发明内里有 import 声明,然后你又得要求更多的资本,这又会消耗更多的时候。一个 naive 的 loader 完成能够会提议很屡次收集要求。但有了 webpack,你不仅能够在本日就最先运用 ES6,还能够获得统统模块化的优点而且不向运转时机能让步。

本来我们计划过一个细致定义的 ES6 模块加载范例,而且我们做出来了。它没有成为终究范例的缘由之一是它没法与打包这一特征折衷。模块系统须要被范例化,打包也不该当被摒弃,由于它太好了。

动态 VS 静态,或许说:礼貌和怎样突破礼貌

作为一门动态编程言语,JavaScript 使人惊奇地具有一个静态的模块系统。

  • importexport 只能写在顶级作用域中。你没法在前提语句中运用引入和导出,你也不能在你写的函数作用域中运用import

  • 一切的导出必需显现地指定一个变量名,你也没法经由过程一个轮回动态地引入一堆变量。

  • 模块对象被封装起来了,我们没法经由过程 polyfill 去 hack 一个新 feature。

  • 在模块代码运转之前,一切的模块都必需阅历加载,剖析,链接的历程。没有能够耽误加载,惰性 import 的语法。

  • 关于 import 毛病,你没法在运转时举行 recovery。一个运用能够包含了几百个模块,个中的任何一个加载失利或链接失利,这个运用就不会运转。你没法在 try/catch 语句中 import。(不过正由于 ES6 的模块系统是如此地静态,webpack 能够在预处置惩罚时就为你检测出这些毛病)。

  • 你没办法 hook 一个模块,然后在它被加载之前运转你的一些代码。这意味着模块没法控制它的依靠是怎样被加载的。

只需你的需求都是静态的话,这个模块系统照样很 nice 的。但你照样想 hack 一下,是吗?

这就是为啥你运用的模块加载系统能够会供应 API。举个栗子,webpack 有一个 API,许可你 “code splitting”,根据你的需求去惰性加载模块。这个 API 也能帮你突破上面列出的一切礼貌。

ES6 的模块是异常静态的,这很好——很多壮大的编译器东西因而收益。而且,静态的语法已被设想成能够和动态的,可编程的 loader API 协同事变。

我何时能最先运用 ES6 模块?

假如你本日就要最先运用,你须要诸如 TraceurBabel 如许的预处置惩罚东西。这个系列专题之前也有文章引见了怎样运用 Babel 和 Broccoli 去天生可用于 Web 的 ES6 代码。那篇文章的栗子也被开源在了 GitHub 上。笔者的这篇文章也引见了怎样运用 Babel 和 webpack。

ES6 模块系统的重要设想者是 Dave Herman 和 Sam Tobin-Hochstadt,此二人不顾包含笔者在内的数位委员的阻挡,一直对峙现在你见到的 ES6 模块系统的静态部份,争辩长达数年。Jon Coppeard 正在火狐浏览器上完成 ES6 的模块。以后包含 JavaScript Loader 范例在内的事变已在举行中。HTML 中相似 <script type=module> 如许的东西以后也会和人人晤面。

这就是 ES6 了。

迎接人人对 ES6 举行吐槽,请期待下周 ES6 in Depth 系列的总结文章。

    原文作者:PortWatcher
    原文地址: https://segmentfault.com/a/1190000003410285
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞