CommonJS,ES6 Module以及webpack模块打包原理

2023-11-13

模块化历程

一个模块就是实现特定功能的文件,有了模块就可以更方便地使用别人的代码,想要什么功能就加载什么模块。前端模块经历的过程如下:

  1. 函数封装
    在一个文件里面编写几个相关的函数就是最开始的模块

    function fn1(){
    	//do something
    }
    function fn2(){
    	//do other something
    }
    

    这样,在需要使用的地方加载这个文件调用函数就可以了
    缺点:无法保证不同模块之间的变量名不会发生冲突。

  2. 对象
    为了解决上面的问题,对象的写法应运而生,可以把所有的模块成员封装在一个对象上

    let myModule = {
    	var1: 1,
    	var2: 2,
    	fn1: function(){},
    	fn2: function(){}
    }
    

    这样就可以在需要调用模块的地方引用这个文件然后myModule.fn2()这样使用就好了,主要保证模块名唯一就可以避免变量的污染。
    缺点:外部引用的时候可以任意修改模块内部的成员值,myModule.var1 = 100,这样就会产生意外的安全问题。

  3. 立即执行函数
    可以通过立即执行函数来达到隐藏细节的目的(同上面闭包中描述的封装变量来达到构建“私有变量”的目的)

    let myModule = (function(){
    	var1: 1,
    	var2: 2,
    	fn1: function(){},
    	fn2: function(){}
    	return {
    		fn1: fn1,
    		fn2: fn2
    	}
    })()
    

    这样,在外部引用模块的时候就不能修改模块内部的变量、函数了。
    上述做法(立即执行函数)是我们模块化的基础。

开始模块化开发的一个前提就是大家必须以同样的方式编写模块,否则各行其道就乱套了,于是就出现了各种模块化规范:CommonJS、ES6 Module、AMD、CMD等

CommonJS

CommonJS 是 2009 年提出的包含模块、文件、IO、控制台在内的一系列标准。Node.js 的实现中采用了 CommonJS 标准的一部分,而非它的原始定义,现在一般谈到 CommonJS 其实是 Node.js 中的版本。

CommonJS 最初只为服务端而设计(因为在服务端需要与操作系统和其他应用程序互动,否则无法编程),直到有了 Browserify(一个运行在Node.js环境下的模块打包工具,可以将 CommonJS 模块打包为浏览器可以运行的单个文件),这也就意味着客户端的代码也可以遵循 CommonJS 标准来编写了。而且 Node.js 的包管理器 npm 允许开发者获取他人的代码库,以及发布自己的代码库,这种共享的传播方式使 CommonJS 在前端开发更加流行起来。

模块

CommonJS 中规定每个文件就是一个模块,会形成一个属于模块自身的作用域,所有变量只有自己能访问,外部不可见。

导出

CommonJS 中通过 module.exports (简化的为 exports)导出模块中内容,导出是模块向外暴露自身的唯一方式。

注意:浏览器是无法识别 CommonJS 模块的,所有以下这些 demo 需要在 Node.js 环境中去测试。

module.exports = {
  name: 'calculator',
  add: (a, b) => a + b
}

可以理解为,CommonJS 模块内部会用一个 module 对象存放当前模块的信息,其中 module.exports 用来指定该模块要对外暴露的内容,简化的导出方式可以直接使用 exports

exports.name = 'calculator'
exports.add = (a, b) => a + b

这两段代码实现效果上没有任何不同,其内在机制是将 exports 指向 module.exports

每个模块的最开始定义可以理解为:

let module = {
  // 模块内容
  // ...
  exports: {} // 模块要暴露内容的地方
}

let exports = module.exports

因此,在使用 exports 时要注意不要直接给它赋值,否则会切断它和 module.exports 的关系而使其失效。
通过模块定义就可以判断,当一个模块中既有 exports 又有 module.exports 导出内容时,最终到底导出的内容是什么,比如:

exports.add = (a, b) => a + b

module.exports = {
  name: 'calculator' // 只有这里的内容被导出了
}

另外,模块导出语句末尾的代码还是会照常执行的,只是,在实际使用中,为了提高可读性,不建议在导出语句后还写其他内容。

导入

CommonJS 中使用 require 语句进行模块导入,module.exports 对象作为其返回值返回。

// calculator.js
module.exports = {
  add: (a, b) => a + b
}

// index.js
const calculator = require('./calculator')
console.log(calculator.add(1, 2))

执行:

node index.js

结果:

// 3

当使用 require 导入一个模块时有两种情况

  • 该模块未曾被加载过,这时会首先执行该模块,然后获取到该模块最终导出的内容
  • 该模块已经被加载过,这时该模块的代码不会再执行,而是直接获取该模块上一次导出的内容

请看下面的例子说明:

// calculator.js
console.log('我被执行啦~~~')
module.exports = {
  name: 'calculator',
  add: (a, b) => a + b
}

// index.js
const name = require('./calculator').name
console.log(name)
const add = require('./calculator').add
console.log(add(1, 2))

执行:

node index.js

结果:

// 我被执行啦~~~
// calculator
// 3

这是因为,前面我们说模块有一个 module 对象用来存放其信息,其中有一个属性 loaded 用于记录该模块是否被加载过,第一次被加载时值被赋为 true,后面再次加载时检查这个值为 true 就不会再执行模块代码了。

有时候加载一个模块时,不需要获取其导出的内容,只需要执行这个模块代码,就直接导出 require 即可,并且 require 还可以接受表达式,例如:

const moduleNames = ['foo.js', 'bar.js']
moduleNames.forEach(name => {
  require('./' + name)
})

ES6 Module

JavaScript 设计之初并没有包含模块的概念,基于越来越多的工程需要,为了使用模块化开发,JavaScript 社区涌现了多种模块标准,包括上述所说的 CommonJS。直到2015年,发布了 ES6(ECMAScript 6.0),自此 JavaScript 语言才具备了模块这一特性(JavaScript 模块)。

模块

ES6 Module 也是将每个文件作为一个模块,每个模块拥有自身的作用域,不同的是导入、导出语句。ES6 的导入导出语句是 importexport

ES6 Module 会自动采用严格模式,即,在 ES6 Module 中不管开头是否有 use strict 都会采用严格模式。

导出

ES6 Module 中使用 export 命令来导出模块,有两种方式

  • 命名导出
  • 默认导出

命名导出

一个模块可以有多个命名导出,有两种不同写法:

// 写法1
export const name = 'calculator'
export const add = (a, b) => a + b

// 写法2
const name = 'calculator'
const add = (a, b) => a + b
export {name, add}

第1种写法是在声明变量的同时用 export 导出;第2种写法是先声明,再用同一个 export 语句导出,两种写法效果一样。

导出时,可以通过 as 关键字对变量重命名:

export {name, add as getSum} // 导入时即为 name 和 getSum

默认导出

默认导出只能有一个

// 导出对象
export default {
  name: 'calculator',
  add: (a, b) => a + b
}
// 导出字符串
export default 'This is a string'
// 导出匿名函数
export default function() {...}

可以将 export default 理解为对外输出了一个名为 default 的变量,因此不需要像命名导出那样进行变量声明,直接导出即可。

导入

ES6 Module 中使用 import 语法导入模块。

导入命名导出的模块

加载带有命名导出的模块时,导入变量的效果相当于在当前作用域下声明了这些变量,并且这些变量只读,不可对其进行更改,也可以通过 as 关键字对导入的变量重命名:

// calculator.js
const name = 'calculator'
const add = (a, b) => a + b
export {name, add}

// index.js
import {name as myName, add} from './calculator.js'
console.log(add(1, 2), myName)

在导入多个变量时,还可以采用整体导入的方式:

// index.js
import * as calculatorfrom './calculator.js'
console.log(calculator.add(1, 2), calculator.name)

因为 ES6 Module 是可以直接在浏览器中运行的模块方式,因此可以通过 HTML 文件直接引入这些脚本文件:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>ES6 Module</title>
</head>
<body>
  <script type="module" src="./index.js"></script>
</body>
</html>

直接通过浏览器打开 index.html 会报错

在这里插入图片描述
这是因为 type="module" 会造成所引用模块资源受限同源策略,是因为:

  • 给引用文件设置 type="module" 等于给文件设置了私有作用域,index.js 文件成了单独的模块,运行在自己的私有作用域中,而不是全局作用域中,因此受限同源策略
  • 虽然 script 标签自带跨域能力(这也是某些场合会通过 jsonp 结合 script 来请求资源的原因),但这种跨域能力仅限于 http 协议(协议、域名、端口一致),file 协议是不支持跨域的

MDN上也能找到给出的提示:

如果你通过本地加载 HTML 文件,你将会遇到 CORS 错误,因为JavaScript 模块安全性需要,你需要通过一个服务器来测试。

如果你用的是 VSCode,可以安装一个插件帮你启一个静态资源服务器,解决这个 CORS 错误

在这里插入图片描述
在这里插入图片描述
通过插件运行 index.html 文件,执行结果:

// 3 'calculator'

导入默认导出的模块

// calculator.js
export default {
  name: 'calculator',
  add: (a, b) => a + b
}

// index.js
import myCalculator from './calculator.js' // 注意需要写上 .js 后缀
console.log(myCalculator.add(1, 2))

对于默认导出来说,import 后面直接跟变量名,并且这个名字可以自由指定,它指代了 calculator.js 默认导出的值,从原理上可以这样理解:

import {default as myCalculator} from './calculator.js'

通过插件运行 index.html 文件,执行结果:

// 3 

CommonJS 与 ES6 Module 的区别

动态与静态

  • CommonJS 是“动态的”,即模块依赖关系的建立发生在代码运行阶段。前面的讲解中知道 require 甚至支持传入一个表达式,可以通过 if 语句判断是否加载,因此,在 CommonJS 模块被执行前,并没有办法确定明确的依赖关系,模块的导入、导出发生在代码的运行阶段。

  • ES6 Module 是“静态的”,即模块依赖关系的建立发生在代码编译阶段。不支持表达式作为导入路径,且导入导出语句必须位于模块的顶层作用域(比如不能放 if 语句中),因此在 ES6 代码的编译阶段就可以分析出模块的依赖关系

值复制与动态映射

在导入一个模块时,CommonJS 获取的是一份导出值的副本;ES6 Module 是对值的动态只读映射。

举例说明
首先是 CommonJS 说明:

// calculator.js
let count = 0
module.exports = {
  count: count,
  add: (a, b) => {
    count += 1
    console.log('count', count)
    return a + b
  }
}

// index.js
let count = require('./calculator').count
let add = require('./calculator').add
console.log(count)
add(1, 2)
add(2, 3)
console.log(count)
console.log(++count)

执行:

node index.js

结果:

// 0
// count 1
// count 2
// 0
// 1

说明:index.js 中的 countcalculator.jscount 的一份副本,因此在调用 add 函数时,虽然更改了 calculator.jscount 的值,但是并不会对 index.js 中导入时创建的副本造成影响。
能够影响这个副本的操作是在 index.js 中对 count 这个副本直接操作 ++count,这也从另一方面说明了,CommonJS 允许对导入的值进行更改

ES6 Module 说明:

// calculator.js
let count = 0
const add = (a, b) => {
  count += 1
  console.log('count', count)
  return a + b
}
export {count, add}

// index.js
import { count, add } from './calculator.js'
console.log(count)
add(1, 2)
add(2, 3)
console.log(count)
console.log(++count)

通过插件运行 index.html 文件,执行结果:

// 0
// count 1
// count 2
// 2
// Uncaught TypeError: Assignment to constant variable.

说明:index.js 中的 count 是对 calculator.jscount 值的实时反映,当通过调用 add 函数更改了 calculator.js 中的 count 值时,index.js 中的 count 的值也随之变化,并且通过在 index.js 中对 count 的操作 ++count 会导致报错信息可知,ES6 Module 规定不能对导入的变量进行修改

循环依赖

从软件设计的角度看,循环依赖应该是尽量避免的,但是当工程复杂度上升到足够大时难免会出现隐藏的循环依赖关系(比如 A 依赖 B,B 依赖 C,C 依赖 D,D 又依赖 A)。

CommonJS 中循环依赖的例子:

// foo.js
const bar = require('./bar')
console.log('value of bar: ', bar)
module.exports = 'This is foo.js'

// bar.js
const foo = require('./foo')
console.log('value of foo: ', foo)
module.exports = 'This is bar.js'

// index.js
require('./foo')

执行

node index.js

结果:

// value of foo:  {}
// value of bar:  This is bar.js

为什么 foo 的值时一个空对象呢?从头梳理一下代码的实际执行顺序:

  1. index.js 导入了 foo.js,此时开始执行 foo.js 中的代码;
  2. foo.js 的第 1 句导入了 bar.js ,这时 foo.js 不会继续向下执行,而是会进入 bar.js 内部;
  3. bar.js 中又对 foo.js 进行了导入,这里产生了循环依赖,这里要注意的是此时的执行权不会再回交给 foo.js,而是直接取 foo.js 的导出值,但是由于 foo.js 还未执行完毕,导出值就是默认的空对象;
  4. bar.js 执行完毕,执行权交回给 foo.jsfoo.js 向下执行打印出 value of bar: This is bar.js,流程结束。

由此可见,虽然循环依赖的模块都被执行了,但是执行结果却不是预想的。

从 Webpack 的实现角度来看这一段的原理:

// The require function
function __webpack_require__(moduleId) {
  if(installedModules[moduleId]) {
    return installedModules[moduleId].exports
  }
  // Create a new module (and put it into the cache)
  var module = installedModules[moduleId] = {
    i: moduleId,
    l: false,
    exports: {}
  }
  ...
}

index.js 引用了 foo.js 之后,相当于执行了这个 __webpack_require__ 函数,初始化了一个 module 对象并放入 installedModules 中。当 bar.js 再次引用 foo.js 时,又执行了该函数,但这次是直接从 installedModules 里面取值,此时它的 module.exports 是空对象,这就解释上上述步骤 3 的现象。

上面谈到,在导入一个模块时,CommonJS 获取到的是值的副本,ES6 Module 则是动态映射(所以在上述的 CommonJS 的循环依赖中,尽管后期程序的执行会改变这个模块的导出值,但是当下 CommonJS 只能获取到值的副本且不能动态映射,所以步骤 3 中只能获取到空对象),利用 ES6 Module 的这个特性如何使其支持循环依赖呢?看下面的例子:

// foo.js
import bar from './bar.js'
function foo(invoker) {
  console.log(invoker + ' invokes foo.js')
  bar('foo.js')
}
export default foo

// bar.js
import foo from './foo.js'
let invoked = false
function bar(invoker) {
  if(!invoked) {
    invoked = true
    console.log(invoker + ' invokes bar.js')
    foo('bar.js')
  }
}
export default bar

// index.js
import foo from './foo.js'
foo('index.js')

通过插件运行 index.html 文件,执行结果:

// index.js invokes foo.js
// foo.js invokes bar.js
// bar.js invokes foo.js

可以看到,foo.jsbar.js 这对循环依赖得到了正确的导出值,下面分析一下代码的执行过程:

  1. index.js 作为入口导入 foo.js,此时开始执行 foo.js 中的代码
  2. foo.js 的开头导入 bar.js,执行权交给 bar.js
  3. bar.js 中一直执行到结束,完成 bar() 函数的定义,此时继续回到未完成的 foo.js 内容直到执行完成,完成 foo() 函数的定义。由于 ES6 Module 动态映射的特性,此时 bar.js 中的 foo 的值已经成为了我们定义的函数了,这也是与 CommonJS 在解决循环依赖时的本质区别
  4. 执行权回到 index.js 并调用 foo('index.js') 函数,此时会依次执行得到输出值

由上面的例子可以看出,ES6 Module 的特性使其可以更好地支持循环依赖,只是需要开发人员在代码中保证当导入的值被使用时已经设置好正确的导出值即可。

模块打包原理

面对工程中成百上千个模块,Webpack 究竟是如何将它们有序组织在一起并按照我们预想的顺序运行在浏览器上的呢?

// index.js
const calculator = require('./calculator')
const foo = require('./foo')
console.log('sum, ', calculator.add(1, 2))
console.log('minus, ', foo.minus(4, 2))

// calculator.js
module.exports = {
  add: (a, b) => a + b
}

// foo.js
module.exports = {
  minus: (a, b) => a - b
}

执行(不知道如何打包的可以参考webpack入门到实战中打包第一个应用那一章节):

npm run build

结果(dist/main.js):

// 立即执行匿名函数
(() => { 
  var __webpack_modules__ = ({
    "./calculator.js": ((module) => {
      eval("module.exports = {\r\n  add: (a, b) => a + b\r\n}\n\n//# sourceURL=webpack://demo/./calculator.js?")
    }),
    "./foo.js": ((module) => {
      eval("module.exports = {\r\n  minus: (a, b) => a - b\r\n}\n\n//# sourceURL=webpack://demo/./foo.js?")
    }),
    "./index.js": ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
      eval("const calculator = __webpack_require__(/*! ./calculator */ \"./calculator.js\")\r\nconst foo = __webpack_require__(/*! ./foo */ \"./foo.js\")\r\nconsole.log('sum, ', calculator.add(1, 2))\r\nconsole.log('minus, ', foo.minus(4, 2))\n\n//# sourceURL=webpack://demo/./index.js?")
    })
  })

  var __webpack_module_cache__ = {}
  // The require function
  function __webpack_require__(moduleId) {
  	// Check if module is in cache
  	var cachedModule = __webpack_module_cache__[moduleId]
  	if (cachedModule !== undefined) {
  		return cachedModule.exports
  	}
  	// Create a new module (and put it into the cache)
  	var module = __webpack_module_cache__[moduleId] = {
  		// no module.id needed
  		// no module.loaded needed
  		exports: {}
  	}
  	// Execute the module function
  	__webpack_modules__[moduleId](module, module.exports, __webpack_require__)
  	// Return the exports of the module
  	return module.exports
  }
  var __webpack_exports__ = __webpack_require__("./index.js");
})()

这个,就是一个最简单的 Webpack 打包结果(bundle)。

  1. 最外层匿名函数中初始化浏览器执行环境,包括定义 __webpack_module_cache__ 对象、__webpack_require__ 函数等,为模块的加载和执行做一些准备工作
  2. 加载入口模块 index.js,浏览器从它开始执行
  3. 执行模块代码,如果执行到了 module.exports 则记录下模块的导出值;如果遇到 __webpack_require__函数,则会暂时交出执行权,进入函数体内进行加载其他模块的逻辑
  4. __webpack_require__ 函数中判断即将加载的模块是否存在于 __webpack_module_cache__ 中,如果存在则直接取导出值,否则回到第 3 步,执行该模块代码来获取导出值
  5. 所有依赖模块都已执行完毕后,最后执行权又回到入口模块,当入口模块代码执行完毕,也就意味着整个 bundle 运行结束。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

CommonJS,ES6 Module以及webpack模块打包原理 的相关文章

随机推荐

  • (海伦公式)已知三角形三条边长,求面积

    海伦公式 已知三角形三条边长 求面积 海伦公式 S p p a p b p c 其中p是三角形的周长的一半p a b c 2 以下转自百度百科 海伦公式海又译作希伦公式 海龙公式 希罗公式 海伦 秦九韶公式 传说是古代的叙拉古国王 希伦 H
  • jQuery与原生JS相互转化

    前端发展很快 现代浏览器原生 API 已经足够好用 我们并不需要为了操作 DOM Event 等再学习一下 jQuery 的 API 同时由于 React Angular Vue 等框架的流行 直接操作 DOM 不再是好的模式 jQuery
  • WSL 2(Ubuntu18.04)编译Linux内核(5.7.9)并替换掉WSL 2原有内核

    准备工作 配置库 由于编译过程中需要很多库 因此需要提前进行配置 如果编译过程中遇到的报错均在下文的报错信息中记录 准备安装的库的命令为 sudo apt get install libncurses5 dev libncursesw5 d
  • Android NDK Address Sanitizer

    文章目录 构建 运行 堆栈轨迹 二进制测试 此文章是基于官方文档 Address Sanitizer的基础上做了一些扩展说明 从 API 级别 27 Android O MR 1 开始 Android NDK 可支持 Address San
  • 华为OD机试真题2022Q4 A + 2023 B卷(Java)

    大家好 我是哪吒 五月份之前 如果你参加华为OD机试 收到的应该是2022Q4或2023Q1 这两个都是A卷题 5月10日之后 很多小伙伴收到的是B卷 那么恭喜你看到本文了 抓紧刷题吧 B卷新题库正在更新中 华为机试有三道题 第一道和第二道
  • 安装win10 和ubuntu18.04双系统时 device for boot installation的选择

    最近在笔记本上安装ubuntu18 04 电脑预装了win10 有两个盘 128G的SSD 1T的HDD win10装在了SSD上 磁盘是GPT UEFI启动模式 在HDD上压缩了空间安装Ubuntu 在安装界面上有一个 Device fo
  • Selenium - Tracy 小笔记2

    selenium本身是一个自动化测试工具 它可以让python代码调用浏览器 并获取到浏览器中加们可以利用selenium提供的各项功能 帮助我们完成数据的抓取 它容易被网站识别到 所以有些网站爬不到 它没有逻辑 只有相应的函数 直接搜索即
  • PAT乙级1052 卖个萌 (20 分)测试点123

    https pintia cn problem sets 994805260223102976 problems 994805273883951104 测试点0 Are you kidding me 中 为转义字符 要用双 表示 测试点1
  • 动态规划经典例题-最长公共子序列-python

    最长公共子序列 问题描述 题解 以问题中为例 A helloworld B loop res i j 表示 截止到B的第i个字符和截止到A的第j个字符的最长公共子序列 例如 res 2 5 2表示第2行第5列 也就是lo和hello的最长公
  • centos7 安装 bugfree3

    1 安装apache yum install httpd 2 安装mysql wget i c http dev mysql com get mysql57 community release el7 10 noarch rpm yum y
  • set的特点

    set不允许元素重复且无序 常用实现有HashSet LinkedHashSet和TreeSet HashSet通过HashMap实现 HashMap的key即HashSet存储的元素 所有key都使用相同的Value 一个名为PRESNT
  • 6. Modules

    6 Modules 如果你退出 Python 解释器并重新进入 你做的任何定义 变量和方法 都会丢失 因此 如果你想要编写一些更大的程序 最好使用文本编辑器先编写好 然后运行这个文件 这就是所谓的创建 脚本 随着你的程序变得越来越长 你可能
  • C语言--学生管理系统--(完整代码)

    本系统分成了三个文件 main c student c student h 功能 实现对班级成员的增加 删除 修改 遍历 根据成绩排序 增加 会检查 现在班级已有的人数 班级是否已满 没有学号重复检测 删除 根据学号查询 gt 删除 会判断
  • 【Java开发环境配置】1-JDK安装教程&环境变量配置(21)

    一 JDK概念 JDK 是Java开发工具包 Java Development Kit 的缩写 它是一种用于构建在 Java 平台上发布的应用程序 applet 和组件的开发环境 其中包括了Java编译器 JVM 大量的Java工具以及Ja
  • stata豪斯曼检验报错

    xsmle gdp gt cz gdzc ersan ur model sdm wmat Wzhusj hausman nolog Warning All regressors will be spatially lagged 在使用xsm
  • 转载:switch/case语句中,每个case都要加花括号

    前言 多写代码 才能发现问题 即使现成的代码 手打一遍也有收获 这不遇到一个很无语的问题 问题及原因 解决方法见以下转载内容 原创者 ChirlChen 博客地址 https blog csdn net qqmindyourwill art
  • 网络编程之网络丢包故障如何定位?如何解决?

    引言 本期分享一个比较常见的网络问题 丢包 例如我们去ping一个网站 如果能ping通 且网站返回信息全面 则说明与网站服务器的通信是畅通的 如果ping不通 或者网站返回的信息不全等 则很可能是数据被丢包了 类似情况想必大家都不陌生 针
  • 3分钟搞懂:JavaScript 和 ECMAScript

    JavaScript 和 ECMAScript ECMAScript 是 JavaScript 语言的国际标准 JavaScript 是 ECMAScript 的一种实现 Adobe ActionScript 和 JScript 同样实现了
  • 基于Bochs安装GeekOs

    开发环境介绍 1 Ubuntu 16 04 2 boch2 6 11 下载地址 http sourceforge net projects bochs files bochs 2 6 11 3 nasm 2 08 01 下载地址 http
  • CommonJS,ES6 Module以及webpack模块打包原理

    CommonJS ES6 Module以及webpack模块打包原理 模块化历程 CommonJS 模块 导出 导入 ES6 Module 模块 导出 命名导出 默认导出 导入 导入命名导出的模块 导入默认导出的模块 CommonJS 与