篇八:大前端基础之ES Module模块化笔记

1,921 阅读3分钟

文章输出主要来源:拉勾大前端高新训练营(链接) 与 阮一峰老师电子书ECMAScript 6 入门。小哥哥小姐姐请不要嫌弃啰嗦,下面肯定都是干货。

1. 模块化介绍

ES Module之前,JavaScript本身不存在模块体系,实现模块化的方式基本为:

  • Node.js中: 内部实现了CommonJS规范
  • 浏览器:CommonJS加载模块为同步方式因此无法再浏览器使用,因此提出新的方案AMD与CMD,对应实现的库分别为require.js与sea.js

ES6之后推出了官方的模块体系,使得js可以原生支持模块化,目前大多在服务端node.js中使用CommonJS方案,在浏览器中使用ES Module方案。

2. ES Module特点

ES Module在浏览器中原生使用方式:

通过在script标签上添加type属性为module代表它遵循ES Module规范

特点:

  1. 使用ES Module默认会开启严格模式

    <script>
      console.log(this) // 非ES Module:打印全局Window对象
    </script>
    
    <script type="module">
      console.log(this) // ES Module: 打印undefined
    </script>
    
  2. 每个ES Module 都运行在单独的私有作用域中,其中定义的变量不会影响全局

    <script type="module">
      var tom = 'tom';
      console.log(tom) // tom
    </script>
    
    <script type="module">
      console.log(tom) // 报错:Uncaught ReferenceError: tom is not defined
    </script>
    
  3. ES Module通过<script type="module" src="xxx.js">在页面器中加载js文件是通过CORS方式请求JS文件的,因此如果请求了跨域的js文件则会报错

  4. ES Module默认对脚本执行进行延迟,与使用defer属性作用一致,都会在页面内容加载完后再执行脚本

3. 导入import与导出export

以下ESModule简称为ESM,由于ESM中的变量的声明都是在私有的作用域中,因此在一个模块中定义的内容需要通过export的方式导出出去才能被外部的模块获取到。一个模块想要使用其他模块中的内容,则需要使用import的方式进行导入。

ESModule中的导出支持以下几种方式:

  • 通过export 变量声明/函数声明/类声明等方式导出变量

    // app.js
    export const name = 'tom';
    
    export function sayHello() {
      console.log('hello world')
    }
    
    export class Person {
      constructor(name, age) {
        this.name = name || 'tom';
        this.age = age || 18;
      }
    }
    
    // module.js
    import { name, sayHello, Person } from './module.js';
    
    console.log(name);
    sayHello();
    console.log(new Person())
    
  • 单独使用export同一导出

    const name = 'tom';
    
    function sayHello() {
      console.log('hello world')
    }
    
    class Person {
      constructor(name, age) {
        this.name = name || 'tom';
        this.age = age || 18;
      }
    }
    
    export {
      name,
      sayHello,
      Person
    }
    
  • 导出重命名:通过oldname as newNmae的方式进行导出重命名,导入模块重命名也是同样的方式

    // module.js
    const name = 'tom';
    
    export {
      name as catName,
    }
    // app.js
    import { catName } from './module.js';
    
    console.log(catName);
    
  • 导出default默认模块与导入默认模块都有两种方式,一种是通过重命名的方式,一种为专门的默认导出与默认导入语法:

    // module.js
    const name = 'tom';
    
    export {
      name as default, // 通过重命名方式导出默认模块
    }
    
    // module.js
    const name = 'tom';
    
    export default name; // 通过默认导出方式导出默认模块
    
    // app.js
    import { default as catName } from './module.js'; // 重命名方式导入
    
    console.log(catName);
    
    // app.js 
    import catName from './module.js'; // 默认导入
    
    console.log(catName);
    

注意事项:

  • 通过export {name, sayHello} 的方式与import {name, sayHello} from 'module.js'的方式进行导入与导出,其中的{}是固定的语法,而不是ES6中的对象简写与对象结构

  • 通过导出与导入的内容是模块的引用,例如import 的name,就算name是个字符串,那么导入后的name也是定义的那个name变量地址的引用,实际内容都是同一个内存空间的值。

  • 通过导入的变量都是只读的,如果是基础类型变量,则无法进行重新赋值,如果是引用类型,无法更改变量指向

ES Module中的导入import

  • import xx from 'path'中 from后面跟的需要是完整的路径名,不能省略.js等后缀或index.js,且path需要为绝对路径或相对路径,例如相对路径需要添加./否则将会被当做第三方模块。

    注:这里所说完整路径名称是在原生情况下,如果利用了打包工具等则可以设置省略后缀或index.js

  • 除了完整的路径,还可以使用url形式引入包,例如import xxx from 'http://xxxx.com/module.js'

  • 仅加载模块:import {} from './module.js'可以仅仅加载模块并不提取其中的内容,简写方式为import 'moudle.js'

  • 导入模块所有内容:import * as mod from './module.js' 即可导入模块中所有成员,并将其组织为对象mod,(可以自己随意命名)

  • 动态导入:ES Module提供了全局的import(path)函数,返回一个promise,参数为一个路径,即可实现动态导入内容,当模块加载成功,即可自动执行其.then方法.

    例:

    import('./module.js').then(module => {
      const {name} = module;
      console.log(name); //tom
    })
    
  • 同时提取默认成员与具名成员:

    • 重命名方式:import {name, default as age} from './moudle.js'
    • 通过逗号分隔并自定义默认导出名称:import age, { name } from './module.js'

export与import复合写法

通过export { module1, module2 } from './module.js'的方式即可实现将exportimport进行合并,简化书写方式。

export { module1, module2 } from './module.js

// 等价于
import {module1, module2} from './module.js'
export {module1, module2}

重命名与整体导出

// 导出模块也可以重命名,例:
export {module1 as m1, module2 as m2} from './module.js'
// 对应的导入即为
import { m1, m2} from './modules/index.js'

// 整体导出
export * from './module.js'

ES2020补充写法:

// ES2020之前以下写法没有对应的复合写法
import * as mod from './module.js'
export {mod}

// ES2020补充了这种以下写法,等价于上方写法
export * as mod from './module.js'

4. ES Moudle在浏览器中的兼容方案

  • ES Module在老版本浏览器中无法原生支持,可以通过打包工具或构建工具配合babel等将es 6代码转换为es5或更低版本进行兼容。

  • 或通过使用browser-es-module-loader插件进行polyfill,在老版本浏览器中兼容ES Module,通过nomodule属性标记插件代码,避免其在支持ES Module的浏览器中重复执行,导致模块代码被执行两次的bug。

    例:

    <!-- promise polyfill 浏览器如果不支持promise则需要使用promise-polyfill-->
    <script nomodule src="https://unpkg.com/promise-polyfill@8.1.3/dist/polyfill.min.js"></script>
    <!--browser-es-module-loader插件中的第一个文件,运行在浏览器端的babel插件,可以将es6代码转换为es5-->
    <script nomodule src="https://unpkg.com/browse/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>
    <!--browser-es-module-loader插件中的第二个文件,核心插件:读取模块中的代码,转交给babel-->
    <script nomodule src="https://unpkg.com/browse/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>
    <script type="module">
      import {name} from './module.js';
      console.log(name);
    </script>
    

    注意:

    此方式可用于测试时使用,不推荐生产中进行使用,因为这种方法原理还是通过babel进行解析脚本,且在线进行动态解析,性能较差。

5. Node中的ES Module

Node中ES Module的原生支持:

Node8.5以上版本已通过实验特性原生支持ES Module,不过模块命名需要以.mjs为后缀名,且运行时需要指定--experimental-modules选项。或者在node 比较新的版本中(本地使用12.16.3),可以通过package.json中指定"type": "module"即可使用正常的.js作为文件后缀,此时如需使用CommonJS则后缀需要变为.cjs

例:

// module.mjs
const name = 'tom';
const age = 18;

export {
  name,
  age
}

// index.mjs
import { name, age } from './module.mjs'

console.log(name, age)

通过node --experimental-modules index.mjs即可运行index.mjs

Node中内置模块的成员官方做了兼容,可以通过单独导入或默认导入的方式进行使用,第三方模块如果没做兼容,可能就只能使用默认导入的方式进行使用

// 默认导入内置模块
import fs from 'fs';
fs.writeFileSync('./foo.txt', 'es module');

// 单独导入成员
import { writeFileSync } from 'fs';
writeFileSync('./foo.txt', 'es module');

ES Module与CommonJS交互:

ES Module中导入CommonJS中模块时,只能通过默认导入的当时导入模块,无法进行解构,import后的{}是固定语法,并非对象的结构。

// common.js
module.exports = {
  hello: 'hello world'
}

// index.mjs
import common from './common.js';

console.log(common) // { hello: 'hello world' }

在CommonJS模块中导入ES Module在node中无法被原生支持。

ES Module与CommonJS的区别:

在CommonJS中,模块默认拥有内置的成员:require,module,exports,__filename,__dirname

在ES Module模块中,这些成员全都无法被支持,可以通过如下方式获取__filename__dirname

import { fileURLToPath } from 'url';
import { dirname } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(import.meta.url)

通过babel进行ES Module的兼容:

安装babel: yarn add babel @babel/core @babel/preset-env

添加babel配置文件.babelrc

{
  "presets": ["@babel/preset-env"]
}

编写代码通过yarn babel-node xxx.jsnpx babel-node xxx.js运行脚本

// module.js
export const name = 'tom'

// index.js
import {name} from './module.js'

console.log(name)

yarn babel-node index.js,输出tom