es6之module 模块化

176 阅读6分钟
  1. es6之前的模块加载方案是: 社区制定了一些方案主要有CommonJs 和 AMD,前者用于服务器,后者用于浏览器
  2. es6的模块设计思想是尽量静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量,而CommonJs和AMD只能在运行时确定这些东西,比如CommonJs,模块就是对象,输入时必须查找对象属性
// CommonJS模块
let { stat, exists, readfile } = require('fs');

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

上面代码的实质是整体加载fs模块(即加载fs的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。

  1. es6模块不是对象,而是通过export命令显示指定输出的代码,再通过import命令输入。
  2. 由于es6模块是编译时加载,使得静态分析成为可能

import export

  1. 模块功能主要由两个命令构成: import 和export
  2. export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
  3. 一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取,如果想要获取就必须用export输出该变量
  4. 使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。
  5. import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。
import {a} from './xxx.js'

a = {}; // Syntax Error : 'a' is read-only;

但是,如果a是一个对象,改写a的属性是允许的。但是建议还是不要修改。都当做只读

  1. import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径。如果不带有路径,只是一个模块名,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。
  2. 注意,import命令具有提升效果,会提升到整个模块的头部,首先执行
foo();

import { foo } from 'my_module';

上面的代码不会报错,因为import的执行早于foo的调用。这种行为的本质是,import命令是编译阶段执行的,在代码运行之前。 8. 由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。

// 报错
import { 'f' + 'oo' } from 'my_module';

import()

  1. import 命令会被js引擎静态分析,引擎在处理import语句是在编译时,这样的设计导致了无法在运行时加载模块,如果import想要取代node的require方法就形成了障碍,因为require是在运行时加载模块,import命令无法取代require的动态加载功能。
  2. 所以引入了import()函数用来动态加载模块
  3. import 命令能够接受什么参数,import()就能接受什么参数,两者的区别主要是后者是动态加载
  4. import()的返回值是一个Promise对象
  5. import()函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用,它是运行时执行,什么时候运行到这一句,就会加载指定的模块
  6. import()类似于 Node.js 的require()方法,区别主要是前者是异步加载,后者是同步加载
  7. 由于import()返回 Promise 对象,所以需要使用then()方法指定处理函数。考虑到代码的清晰,更推荐使用await命令。

import()的适用场合

  1. 按需加载:如:import()方法放在click事件的监听函数之中,只有用户点击了按钮,才会加载这个模块
  2. 条件加载:根据不同的情况,加载不同的模块。
  3. 动态的模块路径
import(f()).then(...);

import()允许模块路径动态生成, 上面代码中,根据函数f的返回结果,加载不同的模块。

Module的加载规则

  1. 浏览器加载 ES6 模块,也使用<script>标签,但是要加入type="module"属性
  2. 浏览器对于带有type="module"<script>,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。
  3. 如果网页有多个<script type="module">,它们会按照在页面出现的顺序依次执行。
<script type="module" src="./foo.js"></script>
<!-- 等同于 -->
<script type="module" src="./foo.js" defer></script>
  1. <script>标签的async属性也可以打开,这时只要加载完成,渲染引擎就会中断渲染立即执行。执行完成后,再恢复渲染。
<script type="module" src="./foo.js" async></script>

ES6模块与CommonJs模块的区别

  1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
  2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
  3. CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。
  • 关于第一个差异: CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值
  • 关于第二个差异: 因为CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成,而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成
  • ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变
  • ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
  • CommonJS 模块是 Node.js 专用的,与 ES6 模块不兼容。语法上面,两者最明显的差异是,CommonJS 模块使用require()module.exports,ES6 模块使用importexport

package.json

  • 关于main字段 和exports字段
  1. 都可指定模块的入口文件
// ./node_modules/es-module-package/package.json
{
  "type": "module",
  "main": "./src/index.js"
}

上面代码指定项目的入口脚本为./src/index.js,它的格式为 ES6 模块。如果没有type字段,index.js就会被解释为 CommonJS 模块。然后import命令就可以加载这个模块了。

// ./my-app.mjs

import { something } from 'es-module-package';
// 实际加载的是 ./node_modules/es-module-package/src/index.js

上面代码中,运行该脚本以后,Node.js 就会到./node_modules目录下面,寻找es-module-package模块,然后根据该模块package.jsonmain字段去执行入口文件。 这时,如果用 CommonJS 模块的require()命令去加载es-module-package模块会报错,因为 CommonJS 模块不能处理export命令。 2. exports字段的优先级高于main字段。