Jade Dungeon

ES6的模块

ES6-Import

Module模块的特性

ES6的module模块,可以把复杂的JS拆分为不同的模块,实现模块化封装。 一个模块就是一个独立的文件,一个脚本就是一个模块。模块可以相互引用, 但必须通过exprotimport关键字来申明导出的接口、引入的接口。 因为一个模块(文件)内部是一个独立的作用域,与外界无关。

  • 静态编译:模块的exprotimport都是在编译时执行,会优先执行。 属于静态加载,不是运行态的。
  • 严格模式:模块始终是严格模式use strict,无需申明。
    • 变量必须先申明再使用,没了变量提升。
    • thisundefined,模块的顶级this指向undefined, 而不是我们熟悉的全局window对象。
  • 模块作用域,模块的作用域只在模块内,外面无法访问。 包括一个<script type="module">内部JS代码块也是如此。
    • 只有通过exprotimport申明的才可以外部访问,这有很好的保护作用。
  • 延迟执行:模块脚本是延迟的,效果类似defer,同其他资源一样并行加载。 等HTML解析完成才会(顺序)执行,所以需注意顺序。支持async, 异步脚本准备好后会立即执行。

在HTML中使用模块,需要<script type="module">来申明, 告诉浏览器这里要被当做模块来对待。包括内联代码、外部引用。

<script type="module" src="./foo.js"></script>

<script type="module">
	console.log(1) // 比下面的晚输出
</script>

<script async type="module">
	console.log(2)// 比上面的先输出
</script>

例子:study/javascript/es6-module/

export导出

export申明导出可以给外部访问的接口,可以是任意类型的变量、方法、类, 支持任意多个。

export const max = 100;。
  • export的本质是通过接口名与模块内部变量之间,建立了一一对应的关系。
  • export可以在任何位置使用,一般推荐在最后的地方统一导出,比较清晰。 注意由于模块是静态编译执行,只能放到顶级位置,不能在块级作用域里面。
  • as重命名,导出时给变量取一个艺名。export { sayHello as Hi };
  • 导入、导出合并使用,导出另一模块导入的接口,做一个中间商, 实现了类似继承的效果。export ... from '*.js'
// 
export let userInfo = { name: 'sam', age: 100, }

let book = { id: '001', price: 100, }

function sayHi(name) { console.log('Hi!', name); }

export const MAX = 100;

// 统一导出,as重命名
export { book, sayHi as sayHello }

// 导入、导出合并使用,把导入的转手导出
export { max, book } from './js/m-user.js'
export * from './js/m-user.js'

import导入

import从一个模块加载(导入)变量、函数、类:

import { max } from './js/m-user.js'
  • {接收}:用一个花括号包裹,接收的变量名称必须和导出时一致。
  • 可以用as重命名:import { sayHello as Hi }
  • url文件:导入的文件url支持相对路径、绝对路径。也可以只是模块文件名, 这时需要配置模块查找方式了。
    • Singleton模式:import模块的代码只会执行一次。 同一个url文件只会第一次导入时执行代码, 后续任何地方import都不会执行模块代码了。 也就是说,import语句是Singleton模式的。
    • 只读-共享:模块导入的接口的是只读的,不能修改。 当然引用对象的属性值是可以修改的,不建议这么干,注意模块是共享的, 导出的是一个引用,修改后其他方也会生效。
  • import具有提升效果,可以先消费,后导入,因为import是编译阶段执行的。
  • *一次性整体加载模块中所有接口,到一个as指定的变量上,此时没有{花括号}。 建议按需导出,不用*,这样有利于构建工具的优化,剔除没用到的代码。

<script type="module" src="../js/f2.js"></script>

<script type="module">
	import { MAX } from './js/m-user.js';
	import { book, sayHello as sayHi } from './js/m-user.js';

	// 整体加载,`*`一次全部导入到一个引用上
	import * as uall from './js/m-user.js';
	console.log(MAX, book);
	sayHi();
</script>

export default

import加载的时候必须指定模块中的名字,有一种不需要指定名字的方法——设置默认导出的变量:

export default MAX;
  • 导入时可以随意命名,此时不需要花括号。
  • default只能用一次。
  • 本质上就是输出一个叫做default的接口,把一个变量赋值给default。 所以也可以显示的使用default
// 设置默认导出接口
const MAX = 1000;
export default MAX;

// 只能用一次,下面这一行会报错
export { sayHi as default }
// 导出默认接口
import M from './js/m-user.js';
import {default as MX} from './js/m-user.js';

import(url)函数

import()用于运行时动态加载一个模块,也支持加载非模块的脚本,可以用在任何地方。 他是异步的,返回一个Promise对象,可以接then(),或者用await命令。 功能类似Node.js中的require(),区别就是require()是同步的。

<!-- 动态加载不需要设置type="module" -->
<script>
	import('./js/m-user.js').then(m => {
			//获得module,所有导出的接口都在m上
			console.log(m.book)
			m.sayHello("baby");
	});

	async function dosth() {
		let obj = await import('./js/m-user.js');
		let { book } = await import('./js/m-user.js');
		console.log("book:", obj.book, book);
	}
	
	dosth();
</script>

requireimport的区别

require是 CommonJS 模块的语法,CommonJS是Node.js 的模块机制,简称CJS。 CommonJS 与ES6 的模块并不兼容,两者的作用类似,但用法还是有些差异的。

比较 CommonJS ES6 模块
运行环境 Node.js 浏览器
导出语法 module.exports = { obj } export { obj }
加载语法 require():运行时动态加载 import:编译时静态引入
import(url):运行时动态加载
异步加载? 同步加载 异步加载
输出的什么? 导入的是一个值的拷贝,独享 导入的是一个引用,有福同享

常见错误

模块外异常

SyntaxError: Cannot use import statement outside a module

import要在模块内使用,如果不在模块内会报异常。

分两种场景,一种是在浏览器内,一个是在nodejs应用中。

浏览器中

js里使用了import

import string from './css.js'

使用时就要在模块里,不然会报错:

<!-- 
	SyntaxError: Cannot use import statement outside a module 错误,
	因为不在模块内 
	-->
<script src="main.js"></script>

改成指定为模块就可以了:

<script type="module" src="main.js"></script>

nodejs中

因为NodeJS环境默认使用的是CommonJS规范,要用require语句进行导入。 与ES6规范中的import不兼容。

例:

import * as fs from 'fs';

这时候如果使用node命令直接执行会报错:

$ node my-function.js

D:\workspace\test-app\src\my-function.ts:2
import * as fs from 'fs';
^^^^^^

SyntaxError: Cannot use import statement outside a module
    at Object.compileFunction (node:vm:352:18)
    at wrapSafe (node:internal/modules/cjs/loader:1033:15)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1159:10)
    at Module.load (node:internal/modules/cjs/loader:981:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Module.require (node:internal/modules/cjs/loader:1005:19)
    at require (node:internal/modules/cjs/helpers:102:18)
    at Array.forEach (<anonymous>)

解决方法:

  • 一种方法:在package.json中指定typemodule,然后用npm start命令执行。 但是这样混用CommonJS和ES6可能会有问题。
  • 另一个方法:使用.mjs扩展名。因为NodeJS版本v13.2开始已经可以支持ES6模块支持。 但要用.mjs扩展名。

第一种方法的例子:

{
	"name":"test-app",
	"scripts": {
		"start": "node src/my-function.js"
	},
	"type":"module",
	"dependencies": {
		"express":"~2.6.1"
	}
}
异常ERR_UNKNOW_FILE_EXTENSION

但是这样混用CommonJS和ES6可能会有问题,可能会遇到ERR_UNKNOW_FILE_EXTENSION。 这样可能要改回CommonJS中的require方式。

异常ERR_REQUIRE_ESM

还有一种可能是你引入的外部库版本比较新, 只支持ES6的import不支持CommonJS的require。 这种情况下可以试试把引用的外部库版本降低到旧一些的版本。