原生 ECMAScript 模块 - 第一次概述

738 阅读11分钟
原文链接: www.zcfy.cc

2016年浏览器和Node.js对于ECMAScript 2015 specification的应用取得了难以置信的发展。现在我们面临的状况是支持情况几乎都接近100%

但是标准也同时介绍了ECMAScript modules (如今也经常被叫作ES或者ES6模块)。它仅仅是会(并且会继续)花最多的时间来实现这一标准,因为还没有浏览器在稳定版本中实现。 最近 Safari 10.1 (Safari Beta), Firefox 54 (Nighly) 以及Edge 15 (next EDGE version)或在之后的标志下 即将实现原生的开箱即用,因此我们很快就可以不需要模块打包来实现。 为了更好地了解我们如何来到这一点,让我们从JS模块历史开始,然后看看当前原生ES模块的特性和实现。

历史

历史上JavaScript没有提供模块系统。有很多技术,我主要说一下最典型的: 1) script 标签里面的长脚本 E.g.:

<!--html-->
<script type="application/javascript">
    // module1 code
    // module2 code
</script>

2) 使用script 标签逻辑地将文件进行划分:

/* js */

// module1.js
    // module1 code

// module2.js
    // module2 code
<!--html-->
<script type="application/javascript" src="PATH/module1.js" ></script>
<script type="application/javascript" src="PATH/module2.js" ></script>

3) 模块作为一个函数(一个模块就是一个返回自我调用函数或者JavaScript构造函数的函数)

  • 应用程序文件/模型作为应用程序的入口点:
// polyfill-vendor.js
(function(){
    // polyfills-vendor code
}());

// module1.js
function module1(params){
    // module1 code
    return module1;
}

// module3.js
function module3(params){
    this.a = params.a;
}

module3.prototype.getA = function(){
    return this.a;
};

// app.js
var APP = {};

if(isModule1Needed){
    APP.module1 = module1({param1:1});
}

APP.module3 = new module3({a: 42});
<!--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>

之后社区发明了真正的分离技术来继续这种分离。主要的想法是提供一个系统,它将允许你只是包括一个链接到JS文件,如:html和其他一切都在bundler端。

让我们来看看主要的JavaScript模块技术标准:

异步模块定义(AMD)

这个方法与RequireJS库以及工具(如r.js)来构建结果打包。常用的语法是:

// polyfill-vendor.js
define(function () {
    // polyfills-vendor code
});

// module1.js
define(function () {
    // module1 code
    return module1;
});

// module2.js
define(function (params) {
    var a = params.a;

    function getA(){
        return a;
    }

    return {
        getA: getA
    }
});

// app.js
define(['PATH/polyfill-vendor'] , function () {
    define(['PATH/module1', 'PATH/module2'] , function (module1, module2) {
        var APP = {};

        if(isModule1Needed){
            APP.module1 = module1({param1:1});
        }

        APP.module2 = new module2({a: 42});
    });
});

CommonJS

它是Node.js生态系统中的基础JS打包。Browserify是使用它作为构建的主要工具之一。本标准的显著特点是为每个模块提供一个单独的作用域。 如下所示:

// polyfill-vendor.js
    // polyfills-vendor code

// module1.js
    // module1 code
    module.exports= module1;

// module2.js
module.exports= function(params){
    const a = params.a;

    return {
        getA: function(){
            return a;
        }
    };
};

// app.js
require('PATH/polyfill-vendor');

const module1 = require('PATH/module1');
const module2 = require('PATH/module2');

const APP = {};

if(isModule1Needed){
    APP.module1 = module1({param1:1});
}

APP.module2 = new module2({a: 42});

ECMAScript模块 (aka ES6/ES2015/Native JavaScript modules)

另外一个标准是 ES2015。它带来了社区需要的语法和功能。

  • 单独的模块作用域
  • 默认strict模式
  • 循环依赖
  • 你可以轻松地按照规范拆分代码 有一些加载器/编译器/方法支持系统中的一部分或者全部,例如:
  • Webpack

  • SystemJS

  • JSPM

  • Babel

  • UMD

工具

对于今天,我们习惯使用工具来打包JavaScript模块。如果我们谈论到ECMAScript,您可以使用以下方法之一:

通常该工具提供CLI和或或者配置和打包JS文件的能力。它获取入口点并且打包您的文件,通常加入use strict以及某些为了让代码在所有环境(老版本浏览器,Node.js等等)而对单吗进行转换。 让我们看看简化的Webpack配置,它设置了入口点并且使用Babel来处理JS文件:

// webpack.config.js
const path = require('path');

module.exports = {
  entry: path.resolve('src', 'webpack.entry.js'),
  output: {
    path: path.resolve('build'),
    filename: 'main.js',
    publicPath: '/'
  },
  module: {
    loaders: {
     "test": /\.js?$/,
     "exclude": /node_modules/,
     "loader": "babel"
   }
  }
};

配置的主要意思是: 1. 从webpack.entry.js 文件启动

2. 对于所有的JS文件应用Babel加载器(意味着代码将根据预置/插件+bundled来进行转化)

3. 将结果放入main.js文件

在这种情况下,通常index.html包含以下内容:

<script src="build/main.js"></script>

那么你的app使用的是打包以及转换后的JS代码。上述是使用打包的常见方法,接着让我们看看如何在浏览器中不使用打包来进行模块划分。

如何使JavaScript模块在浏览器中工作

浏览器支持

现如今,每个主流浏览器都在向支持ES6模块发展:

获取环境来测试

正如我们看到的,目前您可以在Safari 10.1+和EDGE 15 Preview Build中测试本机JS模块。让我们下载并启用其中的功能:

在Firefox中使用ES模块

目前你必须下载Firefox Nightly,这意味着它很快就会到来FF Developer Edition 并且会接着在稳定版本中实现。

启用ES模块:

  • 打开about:config页面
  • 点击“我接受风险!”
  • 搜索dom.moduleScripts.enabled启用标志
  • 双击并且将它的值转化为"true"

就这样,现在你可以测试模块脚本如何在FF中工作。

在启用ES模块的情况下获取Safari技术预览

如果您使用macOS,只需从developer.apple.com下载最新的Safari技术预览(TP)安装并打开它。

Safari 技术预览版本21+开始,并且会默认启用ES模块。

如果是Safari TP 19或20,请检查ES6模块功能是否已启用,打开“开发”菜单 - >“实验功能” - >“ES6模块”。

另一个选项是下载最新的Webkit Nightly并使用它。

启用ES模块的EDGE 15

你可以从Microsoft下载免费的虚拟机。

只需选择虚拟机(VM)“Win 10预览版(15.XXXXX)上的Microsoft EDGE”以及例如 “虚拟盒”(也免费)作为平台。

安装并运行VM,并打开EDGE浏览器。

打开about:flags页面,然后选中“启用实验性JavaScript功能”复选框

就这样,现在你有2个环境,你可以使用ECMAScript模块的本地实现

原生和捆绑模块的区别

让我们从本地模块功能开始:

1. 每个模块都有自己的作用域,而不是全局作用域 2. 它们总是处于严格模式,即使没有提供“use strict”指令 3. 模块可以使用import指令导入其他模块 4. 模块可以使用导出来导出绑定

到目前为止,我们没有面对与我们习惯于使用打包的巨大差异。

最大的区别在于为浏览器提供入口点的方式。您必须提供具有特定属性type =“module”script标签,例如:

<script type="module" scr="PATH/file.js"></script>

这告诉浏览器你的脚本可能包含其他脚本的导入,他们应该被相应地处理。

这里出现的主要的问题是:

为什么JavaScript解释器无法自行检测文件是否是模块?

这里是其中一个原因

模块默认处于严格模式,经典的 scripts- no:

1. 让我们说解释器解析文件,假设它是一个不是严格模式的经典脚本

2. 然后找到“import \ export”指令

3. 在这种情况下,它应该从头开始在严格模式下再次解析所有代码

另一个原因 - 相同的文件可以没有严格模式的有效,并与它无效。然后有效性取决于它被解释的方式,这导致意想不到的问题。

定义期望的文件加载的类型为许多优化方法创造了条件。例如,在解析文件的其余部分之前并行加载文件导入)。你可以找到一些使用Microsoft Chakra JavaScript引擎对于ES模块的示例

Node.js将文件标记为模块的方式

Node.js的性质不同于浏览器和使用``的解决方案也不适用。

目前,[合适的解决方案仍在讨论}(github.com/nodejs/node…

有一些解决方案被社区否决了:

1. 在每一个文件添加"use module";

2. Meta在package.json中

其他选项仍在考虑中(感谢@bmeck的突出贡献):

1. 决定源代码是否是ES 模块

2. 在ES6模块中使用新的文件拓展名.mjs,如果之前的版本没有办法工作工作的话,这个选择可以作为备选。

每个方法都各有利弊,目前,[Node.js如何发展还是未知的](https://github.com/nodejs/node-eps/blob/master/002-es6-modules.md))。

简单原生模块例子

好的,首先让我们先创建一个简单的演示(您可以在您设置的环境中运行来进行测试。因此,它将是一个简单的模块,它会引入另外一个模块并且调用其中的一个方法。第一步-使用``来包括这个文件:

<!--index.html-->
<!DOCTYPE html>
<html>
  <head>
    <script type="module" src="main.js"></script>
  </head>
  <body>
  </body>
</html>

模块文件

// main.js
import utils from "./utils.js";

utils.alert(
  JavaScript modules work in this browser:
  https://blog.whatwg.org/js-modules
);

最后,引入的utils:

// utils.js
export default {
    alert: (msg)=>{
        alert(msg);
    }
};

您可能会注意到,当使用import指令的时候,我们会提供.js文件拓展名。这是和通常的打包行为的另外一个区别-原生模块不会默认添加.js拓展名。

实际上,这意味着你必须提供确切的URL。主要的要求是资源应该有一个正确的MIME类型(感谢@bradleymeck纠正这个)

第二,让我们来检查模块的作用域(演示):

var x = 1;

alert(x === window.x);//false
alert(this === undefined);// true

第三-我们会检查原生模块是在严格模式下。例如,严格模式禁止删除纯名称。因此下面的演示会显示出在模块脚本中抛出错误:

// module.js
var x;
delete x; // !!! syntax error

alert(
    THIS ALERT SHOULDN'T be executed,
    the error is expected
    as the module's scripts are in the strict mode by default
);

// classic.js
var x;
delete x; // !!! syntax error

alert(
    THIS ALERT SHOULD be executed,
    as you can delete variables outside of the strict mode
 );

严格模式在模块脚本中是不可避免的。

注意

  • .js拓展名不能被省略(应该提供确切的 URL)

  • 作用域不是全局的, this 不指向任何变量

  • 原生模块默认处于严格模式下 (不再需要提供‘use strict’ )

内联模块脚本

和经典的脚本一样,你可以内敛代码,而不是在单独的文件中提供它。在之前的演示中,你可以将main.js直接在放在 ``之中,这会导致相同的行为

<script type="module">
  import utils from "./utils.js";

  utils.alert(
    JavaScript modules work in this browser:
    https://blog.whatwg.org/js-modules
  );
</script>

注意

  • 以执行脚本或加载外部文件并作为模块使用执行它

浏览器如何加载并且执行模块

模块都是默认延迟加载的。为了理解这一点,你可以想象它们都具有一个默认的defer属性。

下面是规范中解释这一行为的图片:

alt

这意味着,默认模块脚本不阻塞,并行加载,并在页面完成解析时执行。

因此您可以通过添加async属性来改变这种行为,因此这个脚本只会在加载的时候执行。

默认行为和经典主要的区别是:经典脚本会被立即读取和计算,直到这两步完成才会开始解析。

为了表示它,下面是一个带有脚本选项的演示,在这第一个将是一个没有 defer \ async 属性的经典脚本:

<!DOCTYPE html>
<html>
  <head>
    <script type="module" src="./script1.js"></script>
    <script src="./script2.js"></script>
    <script defer src="./script3.js"></script>
    <script async src="./script4.js"></script>
    <script type="module" async src="./script5.js"></script>
  </head>
  <body>
  </body>
</html>

其它顺序取决于浏览器的实现,脚本的大小,导入脚本的数量等。

注意

  • 模块脚本默认延迟加载

在下一篇文章中获取更多

到JS具有具有自己模块系统还有相当长的时间,因此现在我们来到浏览器需要原生支持并且它们的使用也是无法避免的。

这就是对于ECMAScript模块的第一次探索认知。

您可以阅读ES和打包模块之间的区别,与模块脚本交互的能力,提示和建议,在我的下一篇文章:原生ECMAScript 模块:新功能以及和Webpack的区别

P.S.:

老实说,当我第一次尝试令它在浏览器中工作,我感觉到很自然,因为const / let / arrow函数开始在浏览器中原生工作。 我希望你会喜欢它,以及有这样的感觉。