模块简介

一些模块历史,如:

  • AMD,代表是 require.js ,实现前端代码的模块化
  • CommonJs,主要是 node 使用
  • UMD,兼容AMD和CommonJs

ES6开始有了JS模块,通过关键字 export import 分别来导出和导入模块资源。

现代浏览器比如chrome已经能支持ES的原生模块语法,但是大部分情况下我们还是会用打包工具来在浏览器端实现模块化。

  1. // a.js
  2. export function hi() {}
  3. // main.js
  4. import {hi} from './a.js'
  5. hi()
  6. //index.html
  7. // 浏览器原生支持使用type=module标注这是一个模块脚本
  8. <sciprt type="module">
  9. import {hi} from './a.js'
  10. hi()
  11. </script>

核心功能

使用 use strict 模式

模块有自己的顶级作用域

  1. <script type="module">
  2. // 变量仅在这个 module script 内可见
  3. let user = "John";
  4. </script>
  5. <script type="module">
  6. alert(user); // Error: user is not defined
  7. </script>

模块代码仅仅在第一次导入时被解析(运行)

  1. // a.js
  2. alert('hello')
  3. // 1.js
  4. import 'a.js' // 仅仅这里弹框一次
  5. // 2.js
  6. import 'a.js'

模块是引用传递

  1. // admin.js
  2. export let admin = {name: 'john'}
  3. // 1.js
  4. import {admin} from './admin'
  5. admin.name = 'pete'
  6. // 2.js
  7. import {admin} from './admin'
  8. console.log(admin.name) // pete
  9. // 这里1.js 和 2.js 导入的是模块admin的引用,而不是复制一份admin

模块中this为undefind

  1. <script>
  2. alert(this); // window
  3. </script>
  4. <script type="module">
  5. alert(this); // undefined
  6. </script>

模块脚本是延迟加载的,与 defer 特性对外部脚本和内部脚本的影响相同。也就是说:

  • <script type="module" src="..."> 不会阻塞HTML的解析,与其他资源并行加载。
  • 等到HTML文档就绪后(DOM解析完毕,在DomContentLoaded前)执行。因此可以访问到位于模块脚本后面的DOM元素 ```javascript

相较于下面这个常规脚本:

  1. - 保证脚本的相对顺序,文档中排在前面的先执行
  2. <a name="Nm74s"></a>
  3. ### Async适用于内联脚本
  4. 对于非模块脚本(即没有 `type=module` )仅适用于引入外部脚本 `src=xxx` 。而模块脚本在内联脚本中也可用,同 `async` 的特性,异步加载,加载完就执行,不会等待任何东西。适用于广告,统计等等逻辑。
  5. <a name="C9kfl"></a>
  6. ### 外部脚本
  7. - 加载多次,只执行一次,同模块引入。
  8. - 不同源外部脚本,需要有跨域响应头设置 `Access-Control-Allow-Origin`
  9. <a name="0LMGn"></a>
  10. ### 不能裸模块
  11. 即模块引入的路径需要是相对路径,或者是绝对路径。
  12. ```javascript
  13. import _ from 'lodash'
  14. // Error了,不借助webpack等,直接在浏览器中这么使用,属于引用了裸模块
  15. // 需要使用绝对路径
  16. import _ from 'https://cdn/lodash.js'

导入和导出

导出和导入有多种变体,我们来了解下。

在声明前导出

声明变量之前导出

  1. export const a = 'a'
  2. export function b() {}
  3. export class c {}

导出和声明分开

  1. const a = 'a'
  2. function b() {}
  3. export {a, b}

import * 导入所有

  1. import * as Mod from './mod.js'
  2. // 通过
  3. Mod.xx 来使用

导入所有看起来方便,但是列明需要导入的项,可以方便现代构建工具做 tree shaking
更建议这样 import {a,b} from './mod.js'

import as

导入并重命名一个新的变量名称

  1. // mod.js
  2. export function a() {}
  3. // index.js
  4. import {a as say} from './mod.js'
  5. say()

export as

以新变量名重新导出

  1. // mod.js
  2. export {a as say};
  3. // index.js,这里引入就要用新变量名了
  4. import {say} from './mod'

export default

有两种模块形式,一种是导出很多模块,一种是只导出一个模块,这种通常用默认导出

  1. // user.js
  2. export default class User {}
  3. // index.js
  4. import User from 'user'

如果只有一个默认导出,可以没有类名,函数名

  1. export default class {}
  2. export default function() {}
  3. //没有这个
  4. export default const a = 1

default是一个名称,可以单独使用

  1. function a() {}
  2. export {a as default}

同时导出默认和命名模块

  1. // mod.js
  2. export default class User {}
  3. export function sayHi() {}
  4. // index.js
  5. import {default as User, sayHi} from 'mod'
  6. // 或者全部导进来
  7. import * as mod from 'mod'
  8. mod.default // class User
  9. mod.sayHi()

重新导出

说白了就是导入和导出合并

  1. export {say} from 'mod' // 从mod模块中引入并导出出去
  2. export {default as User} from 'mod' // 从mod模块中引入默认并重命名为User导出

通常这么做,有一种情况,编写了大量模块,有些功能是导出外部的,有些模块是内部自己使用,可以提供一个index.js入口文件,只暴露出对外使用的方法

重新导出默认的导出

  1. export * from 'mod'
  2. // 只会重新导出命名的导出部分,忽略default
  3. // 所以也需要导出默认
  4. export {default} from 'user'

动态导入

  1. import()函数,返回一个promise

import() 只是一种语法,不是函数,不能当做一等功能传递,使用apply/call