用一个例子理解 ES6 的 export/import 用法

使用了一段时间的 Vue.js 以后,其中有大量的 ES6 的 export/import 用法,如
import axios from 'axios';
import Home from '../views/Home.vue';
export default {
  data() { .... }
因为目前对 ES6 的 export/import 用法是一知半解的,所以基本上都是基于 vue-cli 创建的项目上依葫芦画瓢,更是不太理解 vue 的项目是如何由 main.js -> App.vue 把所有的 router, views, 和 components 串联起来的。为了进一步理清 Vue.js 项目的初步运行机制,现在开始着手试图通过一个例子来尽可能多的理解 ES6 的 export/import 用法。

我不是标题党,所以不可能命名如:理解 export/import 一篇就够了,全网最全诸如此类的。且本人博客主要是记录自己学习过程的,而非为了招人眼球。所以尽力吧,在一个代码例子中尽可能多的展示 export/import 的用法,为不增加代码的复杂性,有些技巧在代码后进行解释。

首选推荐阅读 阮一峰 的 ECMAScript 6 入门中的 Module 的语法Module 的加载实现这两章。ES6 官方的名称是 ES2015, 规范一直在演进,每年一个,也就是说现在都 ES2020,我还要研究  ES6(ES2015)。

EditionOfficial nameDate published
ES11ES2020Summer 2020
ES10ES2019Summer 2019
ES9ES2018June 2018
ES8ES2017June 2017
ES7ES2016June 2016
ES6ES2015June 2015

在 ES6 之前,JavaScript 世界只能以曲线的方式来支持模块化,出现了 CommonJS 和 AMD(Asynchronously Module Definition), 和它们的统一品 UMD(Universal Module Definition),CommonJS 用于服务端或桌面的 JS 应用。AMD 用于 Web 项目,它的常用实现有 RequireJS 和国内的一个  Sea.js。然而 ES6 的模块化(export/import) 出现把 CommonJS 和 AMD,还是 UMD 都扫入了历史文件堆。

还是马上开始吧,先来一个模块定义 utils.js 文件,先只写上一行来验证 import
1console.info('utils module imported')
2
3export abc = 123

一句话,什么都可以导出, 变量,常量,函数和类定义,当然广义上讲一切都是变量,但字面常量有些特殊。

现在来看怎么使用它,依照先前 <script src="utils.js"></script> 的惯性思维,我们写一个 index.html 文件
1<script src="utils.js"></script>

用浏览器打开文件,比如 file:///Users/yanbin/test/index.html, 在浏览器的 Inspect 中会看到一个错误
utils.js:3 Uncaught SyntaxError: Unexpected token 'export'
不能这么用,那么试下 import, 把 index.html 的内容改为
1<script>
2  import utils from './utils.js'
3</script>

刷新 file:///Users/yanbin/test/index.html,新的错误
index.html:9 Uncaught SyntaxError: Cannot use import statement outside a module
google 了一下,说是 <script> 的 type 默认为 text/javascript,type 必须为  module 才能用 import 新语法,再次变 index.html 的内容为
1<script type="module">
2  import utils from './utils.js'
3</script>

再来,新问题又来了
index.html:1 Access to script at 'file:///Users/yanbin/test/utils.js' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https.
import 是被允许了,但不能像以往 <script src="utils.js"></script> 那样引入本地的 js 文件,而需要借由 http, https 等协议来引入 js 文件。还需要启动一个 web 服务来使用 import,我们这里选择 nodejs 的 http-server, 可以用以下方式之一来安装
$ npm install --global http-server
$ brew install http-server                 # 或
/Users/yanbin/test 目录下运行 http-server 后,现在可以通过 http://localhost:8080/index.html 来打开页面了, 继续最后的 index.html 内容,在浏览器的 Inspect 中看到
utils module imported
模块 export/import 工作正常。

再接再厉,来一个更完整的 utils.js 的 index.html 演示实例,内容分别为

utils.jsindex.html
 1console.info('utils module imported')   // 0
 2
 3export default {
 4  host: 'localhost'
 5}
 6
 7// 上面的 export default 写法相当于
 8//var obj = { host: 'localhost' }
 9//export { obj as default }
10
11function foo() {
12  console.info('function foo');
13}
14
15var schema = 'https';
16
17export {
18  schema,
19  foo as fn1     // 使用别名 fn1
20};
21
22export let endPoint = '/apis'
23export const PI = 3.14
24
25export function fn2() {
26  console.info('function fn2')
27}
28
29export class Circle {
30  constructor(radius) {
31    this.radius = radius
32  }
33
34  get area() {
35    return PI * Math.pow(this.radius, 2)
36  }
37}
 1<html>
 2  <head>
 3    <title>test ES6 module</title>
 4  </head>
 5  <body>
 6  </body>
 7</html>
 8<script type="module">
 9  import uu from './utils.js'
10  //相当于 import { default as uu } from './utils.js'
11 
12  console.info(uu.host)     // 1  output: localhost
13
14  import './utils.js'
15
16  import { fn1, fn2 } from './utils.js'  // {} 中是对象解析的写法
17  fn1()      // 2   output: function foo
18  fn2()      // 3   output: function fn2
19
20  console.info(api);  //4   output: /apis
21
22  // 可以写完整路径
23  import { schema, endPoint as api } from 'http://localhost:8081/utils.js'
24
25  import * as util from './utils.js' // 引入所有并命别名为 util
26  console.info(util.schema)   // 5    output: https
27  console.info(util.PI)       // 6    output: 3.14
28
29  // import 函数,动态引入, ES2020 加入的支持,Chrome 已支持
30  import('./utils.js')           // 得到一个 Promise
31    .then( ({ schema, Circle }) => {
32       const c = new Circle(2)
33       console.info(`circle area: ${c.area}`)  // 7   output: 12.56
34    })
35</script>

刷新 http://localhost:8080/index.html 后,在浏览器的 Console 中显示
utils module imported
localhost
function foo
function fn2
/apis
https
3.14
circle area: 12.56
为方便对应,输出内容添加到了 index.html 中的 output: 上。

一些说明:

  1. 一个模块可以 export 多个变量(常量,函数,类),一个模块只一个 export default { ... } 会更清晰些
  2. import 语句是编译时静态引入,任何给它安插运行时条件都无效,也因此早于 import 语句的模块中的代码是可运行的
  3. import 一个模块(js 文件) 多次只会加载模块一次,与 Java 等的 import 类型,不同于 C++ 的 #include
  4. default 是一个特殊别名,  方便于 import axios from 'axios' 这种用法。使用中注意 default 的有些特殊性
  5. import 的模块必须为 ./, ..//, import 'utils.js 会出错 Uncaught TypeError: Failed to resolve module specifier "utils.js". Relative references must start with either "/", "./", or "../".
  6. import 的模块默认不能省略扩展名,import './utils.js'import ./utils 会提示说类似找不到 http://localhost:8080/utils, 除非该 js 的文件名就是 utils, 或者像 Webpack 介入编译让 import 更智能,例如 import './utils' 自动查找 ./utils.js 文件或 ./utils/index.js 文件

模块中导入别的模块再导出

一个模块中导入别的模块,再导出,可以简化
1import { foo, bar } from 'third_party_module'
2export { foo, bar }
3
4// 上面两行简化为一个  export
5export ( foo, bar } from 'third_party_module'

别的应用别名(含 default 别名)和 * 发挥
1export { foo as myFoo } from 'my_module'
2export * from 'my_module'
3export { default } from 'foo'
4export { es6 as default } from './someModule'
5export * as ns from 'my_module'      // ES2020

实现模块继承

继承层次其实也是维护着不同的命名空间,就是当前类中找不到的顺着继承链接往父类去找。对于非面向对象的语言,如 C++ 要实现两个结构的继承也可以这么做,把"父" 结构放在"子"结构的前部,即让"子", "父"结构共同起始指针。ES6 的模块继承让导入的第三方模块的东西再次作为自己的导出
1export * from 'circle'
2
3export var e = 2.71828
4export default function(x) {
5  return Math.exp(x)
6}

ES6 的模块系统与 Python 的模块使用比较相近:

  1. 一个 js 或 py 文件就是一个模块
  2. from .. import ..,JS 是 import .. from ..
  3. import 时可以 as 别名
  4. Python  __all__ 指定的内容是 from .. import * 的所有,JS 的 import * from .. 为所有 export 的内容
  5. Python 中是 _ 私有内容不能被引入,JS 中只有用 export 明确的内容才能被引用

链接:

  1. Modules and import in ES6 for Python developers
  2. 各大浏览器对 ES6 的支持可以查看kangax.github.io/compat-table/es6/
永久链接 https://yanbin.blog/example-understand-es6-export-import/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。