ES6模块简介

本文主要介绍ES6模块相关知识,包括模块简介,导入导出等。

什么是模块

一个模块就是一个文件,一个脚本就是一个模块。

模块可以相互加载,通过export和import指令来交换功能,从而可以从一个模块调用另外一个模块的功能。

  • export 关键字标记了可以从当前模块外部访问的变量和函数。
  • import 关键字允许从其他模块导入功能。

示例

                
                    // 📁 say.js
                    function sayHello(user) {
                      alert("Hello, "+user+"!");
                    }

                    function sayBye(user) {
                      alert("Bye, "+user+"!");
                    }

                    // 模块导出变量列表
                    export {sayHello, sayBye};
                
            
                
                    // 📁 app.js
                    // 模块导入
                    import {sayHello} from './say.js';
                    //使用
                    sayHello("zht");
                
            

模块特性

  • 始终使用 “use strict”严格模式。
  • 模块的作用域相关独立,一个模块中的顶级作用域变量和函数在其他脚本中是不可见的。
  • 模块代码仅在第一次导入时被解析运行。

    示例1

                            
                                // 📁 run_only_once.js
                                alert("Run only once!");
                            
                        
                            
                                // 在不同的文件中导入相同的模块
                                // 📁 app1.js
                                import `./run_only_once.js`; // Run only once!
                                // 📁 app2.js
                                import `./run_only_once.js`; // (什么都不显示)
                            
                        

    示例2

                            
                                // 📁 admin.js
                                export let admin = {
                                    name:""
                                };
    
                                export function sayHello() {
                                  alert(`Hello, ${admin.name}!`);
                                }
                            
                        
                            
                                // app1.js 和 app2.js 导入的是同一个对象
                                // 📁 app1.js
                                import {admin} from './admin.js';
                                admin.name = "zht";
    
                                // 📁 app2.js
                                import {admin} from './admin.js';
                                alert(admin.name); // zht
                            
                        

    我们可以在首次导入时设置模块。我们只需要设置其属性一次,然后在进一步的导入中就都可以直接使用了。

                            
                                // 📁 init.js
                                import {admin} from './admin.js';
                                admin.name = "zht";
                            
                        

    另一个模块也可以看到 admin.name

                            
                                // 📁 other.js
                                import {admin,sayHello} from './admin.js';
                                alert(admin.name);//zht;
                                sayHello();//Hello, zht;
                            
                        
  • 在一个模块中,“this” 是 undefined

在浏览器中使用模块

浏览器可以通过 <script type="module">标识的脚本来使用模块(可用于测试,一般不会用于生产环境)。

模块脚本特性

  • 模块脚本是延迟的,与defer对外部脚本和内联脚本的影响相同。
    • 下载外部模块脚本 <script type="module" src="..."> 不会阻塞 HTML 的处理,它们会与其他资源并行加载。
    • 模块脚本会等到 HTML 文档完全准备就绪(即使它们很小并且比 HTML 加载速度更快),然后才会运行。
    • 保持脚本的相对顺序:在文档中排在前面的脚本先执行。
  • Async适用于内联脚本(inline script)。

    对于非模块脚本,async 特性(attribute)仅适用于外部脚本。

  • 具有相同 src 的外部脚本仅运行一次。
  • 不允许裸模块

    在浏览器中,import 必须给出相对或绝对的 URL 路径。没有任何路径的模块被称为裸模块。在 import 中不允许这种模块。

    某些环境(例如Node.js/Webpack),允许没有任何路径的裸模块,因为它们有自己的查找模块的方法和钩子(hook)来对它们进行微调。但是浏览器尚不支持裸模块。

构建工具

在实际开发中,浏览器模块很少被以“原始”形式进行使用。通常,我们会使用一些特殊工具,例如 Webpack,将它们打包在一起,然后部署到生产环境的服务器。

构建流程

  • 从HTML中的 <script type="module"> “主”模块开始。
  • 分析它的依赖:它的导入,以及它的导入的导入等。
  • 使用所有模块构建一个文件(或者多个文件,这是可调的),并用打包函数替代原生的 import 调用,以使其正常工作。还支持像 HTML/CSS 模块等“特殊”的模块类型。
  • 在处理过程中,可能会应用其他转换和优化。
    • 删除未访问的代码
    • 删除未使用的导出
    • 删除特定于开发的像 console 和 debugger 这样的语句。
    • 可以使用 Babel 将前沿的现代的 JavaScript 语法转换为具有类似功能的旧的 JavaScript 语法。
    • 压缩生成的文件(删除空格,用短的名字替换变量等)。

模块导出

有如下导出方式

  • 在声明一个 class/function/… 之前
                            
                                //export [default] class/function/variable ...
    
                                // 导出数组
                                export let months = ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
    
                                // 导出 const 声明的变量
                                export const MODULES_BECAME_STANDARD_YEAR = 2015;
    
                                // 导出类
                                export class User {
                                  constructor(name) {
                                    this.name = name;
                                  }
                                }
                            
                        
  • 导出与声明分开
                            
                                // 📁 say.js
                                function sayHello(user) {
                                  alert("Hello, "+user+"!");
                                }
    
                                function sayBye(user) {
                                  alert("Bye, "+user+"!");
                                }
                                export {sayHello, sayBye}; // 导出变量列表
                            
                        

    从技术上讲,我们也可以把 export 放在函数上面。

  • 通过as对导出重命名
                            
                              export {sayHello as hello, sayBye as bye};
                            
                        
                            
                                import {hello, bye} from './say.js';
                                hello("zht");//Hello, zht!
                                bye("zht");//Bye, zht!
                            
                        
  • 默认导出

    模块提供了一个特殊的默认导出export default语法,用来标识一个模块只做一件事

    有两种默认导出方式

    • export default
                                      
                                          // 📁 user.js
                                          export default class User { // 只需要添加 "default" 即可
                                            constructor(name) {
                                              this.name = name;
                                            }
                                          }
                                      
                                  
    • export {sayHello as default};
                                      
                                          function sayHello(user) {
                                            alert(`Hello, ${user}!`);
                                          }
                                          // 就像我们在函数之前添加了 "export default" 一样
                                          export {sayHello as default};
                                      
                                  

    默认导出和一般的命名导出,有以下区别

    • 默认的导出,导入时不需要花括号。
                                      
                                          import User from './user.js'; // 不需要花括号 {User},只需要写成 User 即可
                                          new User('zht');
                                      
                                  
    • 每个文件只能有一个默认导出,而命名导出可以有多个。
    • 默认导出可以没有名称,而命名导出必须要有名称。
                                      
                                          export default class { // 没有类名
                                            constructor() { ... }
                                          }
                                      
                                  
                                      
                                          export default function(user) { // 没有函数名
                                            alert(`Hello, ${user}!`);
                                          }
                                      
                                  
                                      
                                          // 导出单个值,而不使用变量
                                          export default ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
                                      
                                  

      如果不用default,以下导出会报错

                                      
                                          export class { // Error!(非默认的导出需要名称)
                                            constructor() {}
                                          }
                                      
                                  
    • 默认导出,导入时可以自由选择名称,而命名导出不能选择名称
                                      
                                          //export default class User...
                                          import User from './user.js'; // 有效
                                          import MyUser from './user.js'; // 也有效
                                      
                                  
                                      
                                          //export class User...
                                          import {User} from './user.js'; // 有效
                                          import {MyUser} from './user.js'; // 无效
                                      
                                  

    一个模块默认导出和命名导出可同时存在。

                            
                                // 📁 user.js
                                export default class User {
                                  constructor(name) {
                                    this.name = name;
                                  }
                                }
    
                                export function sayHello(user) {
                                  alert(`Hello, ${user}!`);
                                }
                            
                        

    导入默认的导出以及命名的导出的方法如下

                            
                                import {default as User, sayHello} from './user.js';
                                new User('zht');
                            
                        
  • 重新导出(Re-export)

    有两种方式实现模块的重新导出

    • import & export
                                      
                                          // 导入sayHello 然后立即导出它们
                                          import {sayHello, sayBye} from './say.js';
                                          export {sayHello, sayBye};
      
                                          // 将默认导出导入为 User,然后导出它
                                          import User from './user.js';
                                          export {User};
                                      
                                  
    • export ... from ...
                                      
                                          export {sayHello, sayBye} from './say.js'; // 重新导出 sayHello和sayBye
                                          export {default as User} from './user.js'; // 将user默认导出以User重新导出
                                      
                                  

      export ... from ...是以上import & export的简写

    重新导出可用于组织代码和对外公开功能,通过一个主文件index.js可对外公开整个package(内部包含大量模块文件)中各个子模块的功能。

  • 重新导出默认导出

    重新导出时,默认导出需要单独处理。

                            
                                // user.js
                                export default class User {
                                  // ...
                                }
                            
                        
                            
                                //export default class User...
                                export * from './user.js'; // 重新导出只导出了命名的导出,但是忽略了默认的导出。
                                export {default} from './user.js'; // 重新导出默认的导出
                                export User from './user.js'; // 无效,必须明确写出 export {default as User}
                            
                        

模块导入

有如下导入方式

  • 导入的东西列在花括号 import {...} 中
                            
                                import {sayHello,sayBye} from './say.js';
                                sayHello('zht');
                            
                        
  • 将所有内容导入为一个对象
                            
                                import * as say from './say.js';
    
                                say.sayHello('zht');
                                say.sayBye('zht');
                            
                        

    这种全部导入的方式看起来比较便捷,但是明确导入的内容更利于优化。

    • 构建工具(Webpack)将模块打包到一起时,会对其进行优化,并从打包好的代码中删除那些未被使用的函数,从而使构建更小,而采用全部导入的方式则不会有此精简操作。
    • 明确列出要导入的内容会使得名称较短:sayHello() 而不是 say.sayHello()。
    • 利于代码重构。
  • 通过as对导入重命名
                            
                                import {sayHello as hello, sayBye as bye} from './say.js';
                                import {default as User} from './user.js';//默认导出重命名
    
                                hello('zht'); // Hello, zht!
                                bye('zht'); // Bye, zht!
                            
                        

模块动态导入

请注意,import/export必须放在顶层代码块中,在 {...} 中的 import/export 语句无效。

以下代码是无效的。

                
                    if (something) {
                      import {sayHello} from "./say.js"; // 异常: 导入必须在顶层代码块中
                    }
                
            

如果需要动态地按需导入模块的话,可以通过import() 表达式。

import(module) 表达式加载模块并返回一个 promise,该 promise resolve 为一个包含其所有导出的模块对象。我们可以在代码中的任意位置调用这个表达式。

例如:

                
                    let modulePath = prompt("Which module to load?");
                    import(modulePath)
                      .then(obj => <module object>)
                      .catch(err => <loading error, e.g. if no such module>)
                
            

在异步函数中,我们可以使用 let module = await import(modulePath)。

例如:

                
                    // 📁 say.js
                    export function hello() {
                      alert(`Hello`);
                    }
                    export function bye() {
                      alert(`Bye`);
                    }
                    export default function() {//默认导出
                      alert("Module loaded (export default)!");
                    }
                
            
                
                  async function load() {
                    let say = await import('./say.js');
                    say.hello(); // Hello!
                    say.bye(); // Bye!
                    say.default(); // Module loaded (export default)! 模块对象的 default 属性使用默认导出
                  }
                
            

动态导入在浏览器常规脚本中工作,不需要 script type="module"特性。


参考资料: Modules, introduction