美文网首页ES6
页面是如何加载ES6 Modules

页面是如何加载ES6 Modules

作者: xiaohesong | 来源:发表于2018-09-30 16:59 被阅读208次

    什么是模块

    模块就是javascript文件以不同方式去加载(这个就和scripts的方式相反了,scripts是以原始的javascript工作方式加载的)。这种不同的模式很有必要,并且他们代表的语义也是不一样的:

    • 模块模式会自动运行在严格模式下,没有办法选择。
    • 模块顶层创建的变量不会共享到全局范围,仅在模块的范围内。
    • 模块的顶层的this值是undefined
    • 模块不允许在代码中使用HTML样式的注释(JavaScript早期浏览器时代的遗留功能)。
    • 模块必须导出模块外部代码可用的任何内容。
    • 模块可以从其他模块导入绑定

    这些差异乍一看似乎很小,但它们代表了JavaScript代码加载和评估方式的重大变化,下面会进行介绍。模块的真正强大之处在于能够仅导出和导入所需的内容,而不是文件中的所有内容。

    Basic Exporting

    使用export把外部需要的内容导出。在最简单的情况下,将export放在任何变量,函数或类声明的前面,这样从模块中导出它。

    export var name = 'my name'
    export const say = () => 'your name'
    
    // this function is private to the module
    function subtract(num1, num2) {
        return num1 - num2;
    }
    
    // define a function...
    function multiply(num1, num2) {
        return num1 * num2;
    }
    
    // ...and then export it later
    export { multiply };
    

    可以看出,每个导出都有个名称,除非你导出默认(后面会说)。否则没法子使用这个语法导出匿名函数或类。当然也可以声明之后再导出。

    Basic Importing

    import { identifier1, identifier2 } from "./example.js";
    

    这个看起来类似于解构对象,但它不是。
    当你从module import的时候,他的行为就像const一样。这意味着无法定义具有相同名称的另一个变量(包括导入同名的另一个export),在import语句之前使用标识符,或更改值。

    Importing All of a Module

    import * as example from "./example.js";
    

    这里值得一提的是,无论在import语句中使用模块多少次,模块都只执行一次。在导入模块的代码执行之后,实例化的模块保存在内存中,并在另一个import语句引用它时重用。

    import { sum } from "./example.js";
    import { multiply } from "./example.js";
    import { magicNumber } from "./example.js";
    

    尽管这里使用了多次module,但是只会加载一次。如果同一应用程序中的其他模块要从example.js导入,那么这些模块将使用此代码使用的相同模块实例。

    模块语法限制
    导出和导入的一个重要限制是它们必须在其他语句和函数之外使用。

       if (flag) {
           export flag;    // syntax error
      }
    

    exportif语句里,这是不被允许的。export是不可以有条件的,也不可以以其他的方式动态export。这是为了静态的确定需要导出的内容。
    同样的,import也存在这个限制:

    function tryImport() {
       import flag from "./example.js";    // syntax error
    }
    

    你不可以动态的导入模块就像你不能动态的导出模块那样。导出和导入关键字设计为静态,因此文本编辑器等工具可以轻松地告知模块中可用的信息。

    A Subtle Quirk of Imported Bindings

    这个是有些意思,你可以在导出的内部更改,但是导出之后,就不可以更改了。

    # export.js
    export var name = "Nicholas";
    export function setName(newName) {
        name = newName;
    }
    
    # import.js
    import { name, setName } from "./example.js";
    
    console.log(name);       // "Nicholas"
    setName("Greg");
    console.log(name);       // "Greg"
    
    name = "Nicholas";       // error
    

    Renaming Exports and Imports

    这个就是通过as来改名字。

    #export.js
    function sum(num1, num2) {
        return num1 + num2;
    }
    
    export { sum as add };
    
    #import.js
    import { add as sum } from "./example.js";
    console.log(typeof add);            // "undefined"
    console.log(sum(1, 2));             // 3
    

    Exporting Default Values

    这个和上面的差不多,就是在导出的时候加上default关键字。大同小异吧。

    Importing Without Bindings

    某些模块可能不会导出任何内容,而只是对全局范围内的对象进行修改。尽管模块内的顶级变量,函数和类不会自动结束于全局范围,但这并不意味着模块无法访问全局范围。
    可以在模块内部访问内置对象(如ArrayObject)的共享定义,对这些对象的更改将反映在其他模块中。

    #export.js
    // module code without exports or imports
    Array.prototype.pushAll = function(items) {
    
        // items must be an array
        if (!Array.isArray(items)) {
            throw new TypeError("Argument must be an array.");
        }
    
        // use built-in push() and spread operator
        return this.push(...items);
    };
    
    #import.js
    import "./export.js";
    
    let colors = ["red", "green", "blue"];
    let items = [];
    
    items.pushAll(colors);
    

    像这种没有具体内容的导出,多用于垫片之类的功能。

    Loading Modules

    虽然ECMAScript 6定义了模块的语法,但它没有定义如何加载它们。这是规范的复杂性的一部分,该规范应该与实现环境无关。ECMAScript 6不是尝试创建适用于所有JavaScript环境的单一规范,而是仅指定语法并将加载机制抽象为未定义的内部操作HostResolveImportedModuleWeb浏览器和Node.js将决定如何以对各自环境有意义的方式实现HostResolveImportedModule`。

    Using Modules in Web Browsers

    ECMAScript 6之前,Web浏览器就有多种方法可以在Web应用程序中包含JavaScript。脚本加载有:

    1. 使用带有src属性的<script>元素加载JavaScript代码文件,该属性指定加载代码的位置。
    2. 使用没有src属性的<script>元素嵌入JavaScript代码。
    3. 加载JavaScript代码文件以作为worker(例如web workerservice worker)执行。

    为了完全支持模块,Web浏览器必须更新每个机制。这些细节在HTML规范中定义,在这里对它们进行总结。

    Using Modules With <script>

    <script>元素的默认行为是将JavaScript文件作为脚本(而不是模块)加载。缺少type属性或type属性包含JavaScript内容类型(例如“text / javascript”)时会发生这种情况。然后,<script>元素可以执行内联代码或加载src中指定的文件。为了支持模块,“模块”值被添加​​为类型选项。将类型设置为“module”会告诉浏览器将src指定的文件中包含的任何内联代码或代码作为模块而不是脚本加载。

    <!-- load a module JavaScript file -->
    <script type="module" src="module.js"></script>
    
    <!-- include a module inline -->
    <script type="module">
    
    import { sum } from "./example.js";
    
    let result = sum(1, 2);
    
    </script>
    

    第一个加载外部的module.第二个<script>元素包含直接嵌入网页的模块。变量结果不会全局公开,因为它仅存在于模块中(由<script>元素定义),因此不会作为属性添加到窗口中。

    正如所见,包括网页中的模块相当简单,类似于包含脚本。但是,模块的加载方式存在一些差异。

    可能已经注意到“模块”不是像“text / javascript”类型那样的内容类型。模块JavaScript文件使用与脚本JavaScript文件相同的内容类型提供,因此无法仅根据内容类型进行区分。此外,当类型无法识别时,浏览器会忽略<script>元素,因此不支持模块的浏览器将自动忽略<script type =“module”>行,从而提供良好的向后兼容性。

    Module Loading Sequence in Web Browsers

    模块的独特之处在于,与脚本不同,它们可以使用import来指定必须加载其他文件才能正确执行。为了支持该功能,<script type =“module”>始终表现为应用了defer属性。defer属性对于加载脚本文件是可选的,但始终应用于加载模块文件。 一旦HTML解析器遇到带有src属性的<script type =“module”>,模块文件就会开始下载,但是在完全解析Document之后才会执行。模块也按它们在HTML文件中出现的顺序执行。这意味着第一个<script type =“module”>始终保证在第二个之前执行,即使一个模块包含内联代码而不是指定src
    关于defer和async作用的script可以戳这里了解

    <!-- this will execute first -->
    <script type="module" src="module1.js"></script>
    
    <!-- this will execute second -->
    <script type="module">
    import { sum } from "./example.js";
    
    let result = sum(1, 2);
    </script>
    
    <!-- this will execute third -->
    <script type="module" src="module2.js"></script>
    

    每个模块可以从一个或多个其他模块导入,这使问题复杂化。 这就是为什么首先完全解析模块以识别所有import语句的原因。 然后,每个import语句都会触发一次获取(来自网络或来自缓存),并且在首次加载和执行所有导入资源之前不会执行任何模块。

    所有模块,包括使用<script type =“module”>显式包含的模块和使用import隐式包含的模块,都按顺序加载和执行。在前面的示例中,完整的加载顺序是:

    1. 下载解析module1.js.
    2. 递归下载并解析module1.js中的导入资源。
    3. 解析内联模块。
    4. 递归下载并解析内联模块中的导入资源
    5. 下载解析module2.js
    6. 递归下载并解析module2.js中的导入资源。

    加载完成后,在文档完全解析之后才会执行任何操作。文档解析完成后,将执行以下操作:

    1. 递归执行module1.js的导入资源
    2. 执行 module1.js
    3. 递归执行内联模块的导入资源
    4. 执行内联模块
    5. 递归执行module2.js的导入资源
    6. 执行module2.js

    内联模块的作用与其他两个模块类似,不是先下载代码。加载导入资源和执行模块的顺序完全相同。

    <script type =“module”>上会忽略​​defer属性,因为它的行为就像应用了defer一样。

    Asynchronous Module Loading in Web Browsers

    与脚本一起使用时,async会在下载和解析文件后立即执行脚本文件。 但是,文档中异步脚本的顺序不会影响脚本的执行顺序。 脚本在完成下载后始终执行,而不等待包含文档完成解析。

    async属性也可以应用于模块。 在<script type =“module”>上使用async会导致模块以类似于脚本的方式执行。 唯一的区别是模块的所有导入资源都是在执行模块之前下载的。 这保证了模块执行前所需的所有资源都将被下载; 但是你无法保证模块何时执行。看下面的代码:

    <!-- 不确定哪个先被执行 -->
    <script type="module" async src="module1.js"></script>
    <script type="module" async src="module2.js"></script>
    
    Loading Modules as Workers

    Web WorkerService WorkerWorkerWeb页面上下文之外执行JavaScript代码。 创建新worker需要创建一个新的实例Worker(或另一个类)并传入JavaScript文件的位置。 默认加载机制是将文件作为脚本加载,如下所示:

    // load script.js as a script
    let worker = new Worker("script.js");
    

    为了支持加载模块,HTML标准的开发人员为这些构造函数添加了第二个参数,第二个参数是一个具有type属性的对象,其默认值为“script”。您可以将类型设置为“module”以加载模块文件:

    // load module.js as a module
    let worker = new Worker("module.js", { type: "module" });
    

    module type worker一般类似于script type worker,但是有些例外。首先,script worker限制于同源,但是module worker不存在这些同源限制。 虽然module worker具有相同的默认限制,但它们也可以加载具有适当的跨源资源共享(CORS)标头的文件以允许访问。其次,虽然script worker可以使用self.importScripts方法将其他脚本加载到worker中,但self.importScripts总是在module worker上失败,因为更应该使用import

    本文原文请戳这里

    相关文章

      网友评论

        本文标题:页面是如何加载ES6 Modules

        本文链接:https://www.haomeiwen.com/subject/rdnioftx.html