CommonJs / ES Module的用法和区别

由于JavaScript缺乏模块体系,在ES6之前,社区指定了一些模块加载方案,比如node.js所遵循的commonJS规范。而在ES6在语言层面上实现了模块功能,完全可以取代现有的commonJS等规范,成为浏览器、服务器通用的模块解决方案。本篇博客目录如下:
1.如何使用exports
2.如何使用module.exports
3.exports与module.exports的区别
4.使环境支持module语法
5.export与export default的基本用法

1.如何使用exports

在es6还没有出现的日子里,通过commonJS实现模块加载会接触到两个方法,exports与module.exports。我们先来看exports,在node.js中,我们通常会以exports.name=something的形式来使用exports

//导出文件mout.js
function outputA(){
	console.log("this is output A");
}

function outputB(){
	console.log("this is output B");
}

exports.outputA=outputA;
exports.outputB=outputB;
//导入文件module.js
let mod=require("./mout.js");
console.log(mod);

这时会输出
这里写图片描述
如果你想在module.js中使用outputA或outputB函数,就得

let {outputA,outputB}=require("./mout.js");
//{}中变量名必须与mout.js导出函数名相同
outputA();
outputB();

上面的写法也等同于

let _mout=require("./mout.js");
//变量a,b的变量名可以随意
let a=_mout.outputA;
let b=_mout.outputB;
a();
b();

2.如何使用module.exports

在commonJS中还有一种方法可以导出模块。我们通常以module.exports=something的形式使用它

//还是那个导出文件
function outputA(){
	console.log("this is output A");
}

function outputB(){
	console.log("this is output B");
}

module.exports=outputA;
module.exports=outputB;
//还是那个导入文件
let mod=require("./mout.js");
console.log(mod);

这个时候我们会发现输出是这样子的
这里写图片描述
虽然我们在mout.js中module.exports两次,但输出却告诉我们只有最后一次module.exports是有效的,第一次module.exports被覆盖掉了。如果我们想在module.js中使用outputB只需mod()即可。
所以,当我们需要将模块定义为一个类时,使用module.exports是唯一的选择

3.exports与module.exports的区别

其实,module.exports 初始值为一个空对象 {}require() 返回的是 module.exports 而不是 exportsexports只不过是指向的 module.exports 的引用
赋给exports的所有属性或方法最终都赋值给了module.exports。但,这是有前提条件的,那就是module.exports是空对象,如果module.exports具有了一些属性或方法,那么赋给exports的所有属性或方法都会被忽略掉,譬如

//还是TA
function outputA(){
	console.log("this is output A");
}

function outputB(){
	console.log("this is output B");
}

exports.outputA=outputA;
module.exports=outputB;
//是TA是TA就是TA
let mod=require("./mout.js");
console.log(mod);

这样子,输出的就是酱紫的
这里写图片描述
而且无论exports在module.exports前还是后,结果都是module.exports生效,而exports看起来并没有发挥什么卵用。
通过上面的推导,我们可以猜测给module.exports添加属性等同于于给exports添加属性

//。。。。。
function outputA(){
	console.log("this is output A");
}

function outputB(){
	console.log("this is output B");
}

module.exports.outputA=outputA;
module.exports.outputB=outputB;
//。。。。。
let mod=require("./mout.js");
console.log(mod);

直接看结果
这里写图片描述
一目了然

4.使用ES module语法

原文编写时,node版本才更新到9.3.0,并不能直接在node环境中使用ES module,浏览器对ES module的支持也不完善,所以当时是通过webpack编译来使用ES module。
随着时代的进步,node从13.2.0开始支持ES module。而在浏览器方面,尤其是chrome浏览器,对ES module的支持越来越完善。所以我们有多种方式使用ES module。

4.1 通过webpack编译

为了在不支持ES module的node版本中进行体验,我们需要用到babel或者类似于babel的工具对代码进行编译。下面以babel为主进行讲解。
首先需要说明的是,如果你通过babel-node等工具试图直接在命令行中运行es6代码可能会出现错误。这是因为

Babel默认只转换新的JavaScript句法(syntax),而不转换新的API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。而且像import和export这两个命令现在在任何浏览器中都是不支持的, 同时babel也无法将其转换为浏览器支持的ES5, 原因在于:babel只是个翻译,假设a.js 里 import 了 b.js, 对a.js进行转码,只是翻译了a.js,并不会把b.js的内容给读取合并进来, 如果想在最终的某一个js里,包含 a.js,b.js 的代码,那就需要用到打包工具

接下来就以webpack为例展示如何进行转码
首先,你得有个目录,并且你安装了node与npm
进入到目录中,运行npm init
init完了之后,执行npm install webpack --save-dev --no-optional
接下来,你需要安装babel-loader与babel-core

npm install --save-dev --no-optional babel-loader babel-core

然后根据你的需求选择转码规则,目前有这些
这里写图片描述
我们使用latest吧,npm install --save-dev --no-optional babel-preset-latest
之后我们还会用到path,所以一并安装上npm install --save-dev --no-optional path
目前为止,目录下的webpack.config.js应该是酱紫的
这里写图片描述

这一堆工作做完之后,我们还需要在根目录下创建一个webpack配置文件——webpack.config.js,具体内容如下

const path=require("path");
module.exports = {
  entry:  __dirname + "/app/import.js",//入口文件
  output: {
    path: __dirname + "/dist",//打包后的文件存放的地方
    filename: "bundle.js"//打包后输出文件的文件名
  },
  module:{
  	loaders:[
		{
			test:/\.js$/,
			loader:'babel-loader',//-loader不可省略
			include:path.resolve(__dirname,'/app'),//babel只处理指定目录
			options:{
				presets:['latest']
			}
		}
  	]
  }
}

其中,options字段值得我们注意。一般来说babel有三种配置方式:
通过webpack.config.js loaders中的options字段
通过package.json中的babel字段
通过.babelr文件
三种方式具体的配置内容是一模一样的,我比较喜欢第一种

这样子一通折腾下来后,我们在app下创建两个文件import.js与export.js。

//import.js
import {alias,outputB} from "./export.js"
console.log(alias);
console.log(outputB);
//export.js
function outputA(){
	console.log("this is output A");
}

function outputB(){
	console.log("this is output B");
}

export {outputA as alias,outputB}

之后在根目录下运行webpack,等待打包完成之后,在dist目录下就会出现bundle.js,我们通过html引用也好,还是直接node运行都可以,输出结果是
这里写图片描述

4.2 通过babel-node

如果只是简单在本地node环境中使用ES module,其实有种简单的方法是使用babel-node执行文件。为此,我们需要安装三个包

npm i -g @babel/core @babel/node @babel/plugin-transform-modules-commonjs

然后就可以通过如下命令执行使用ES module的文件

babel-node [file path] --plugins @babel/plugin-transform-modules-commonjs

4.3 直接在node中使用

node从13.2.0开始支持ES module,可以通过以下两种方法使用

4.3.1 文件后缀名使用.mjs

举个例子,我们的文件目录可以是这样:

.
|____a.mjs
|____index.mjs
// a.mjs
export let foo = 'bar'
setTimeout(() => foo = 'baz', 500)
// index.mjs
import { foo } from './a.mjs'

console.log(foo)

setTimeout(() => console.log(foo), 500)

执行node index.mjs,代码可以正确执行,并且按照ES module的特性,开始执行时输出bar,0.5秒后输出 baz。

4.3.2 在项目的package.json中设置type

如果不想使用.mjs后缀的文件格式,也可以在根路径添加package.json文件,配置type:module 即可
在上一个例子的基础上,修改.mjs后缀为js,添加package.json:

.
|____a.js
|____index.js
|____package.json

package.json如下:

{
  ...
  "type": "module",
  ...
}

执行node index.js,结果和之前一致。

4.4 在浏览器中使用

在浏览器中使用ES module感觉会更方便直观一点,在这里我们可以查看浏览器的支持程度,以及完整的例子。总的来说,在浏览器中使用ES module最关键的一步是为<script>标签添加type="module",来声明这是一个模块。
比如如下目录:

.
|____a.js
|____index.js
|____main.js

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script type="module" src=".index.js"></script>
</body>
</html>

index.js和a.js同4.3.1,在浏览器中打开index.html,结果应该是一致的。

5.export与export default的基本用法

普通的、老老实实的、朴实无华的操作

function outputA(){
	console.log("this is output A");
}

function outputB(){
	console.log("this is output B");
}

export {outputA,outputB}
import {outputA,outputB} from "./export.js"
outputA()

在引入文件中,如果需要使用export.js中的函数,用户必须先知道它的名字,如果用户不想翻文档或者怎么滴,这样子就不太方便,所以就有了export default

function outputA(){
	console.log("this is output A");
}

export default outputB;
import alias from "./export.js"
console.log(alias);
alias();

注意,在import的时候,我们已经不用使用{}了
模块整体加载
如果我们需要加载模块的全部接口,那么只需

function outputA(){
	console.log("this is output A");
}

function outputB(){
	console.log("this is output B");
}

export {outputA,outputB}
import * as alias from "./export.js"

export时使用别名

function outputA(){
	console.log("this is output A");
}

function outputB(){
	console.log("this is output B");
}

export {outputA as alias,outputB}

接下来,我们进行一些比较骚气的操作

function outputA(){
	console.log("this is output A");
}

function outputB(){
	console.log("this is output B");
}

export {outputA as alias,outputB}
export default outputB;

这段代码既有export又有export default
我们先

import * as x from "./export.js"
console.log(x);

这时,console出来的x是
这里写图片描述
这里我们可以看出export default的本质是输出一个叫做default的方法,系统允许我们对它取任意名称
接下来,酱紫

import x from "./export.js"
console.log(x);

输出结果是
这里写图片描述
也就是说,如果import时变量只有一个,而且外面没有{},那么该变量接受的就是export default。如果import时变量放在{}里,那么就会拿到对应的export。我们可以用这种方式同时接受export与export default

import x,{alias,outputB} from "./export.js"

6.CommonJs和ES Module的区别

6.1 语法不同

commonjs是module.exports,exports导出,require导入
ES6则是export导出,import导入

6.2 是否存在提升

ES module在编译期间会将所有import提升到顶部,commonjs不会提升require。

foo()

import { foo } from 'module'

这代码并不会报错,因为import会被提升到文件顶部。

6.3 是否支持动态加载

commonjs是运行时加载模块,支持动态加载。
ES6是在静态编译期间就确定模块的依赖,不支持动态加载。

// 报错
import { 'f' + 'oo' } from 'module'

// 报错
let module = 'module'
import { foo } from module

// 报错
if (x === 1) {
  import { foo } from 'module1'
} else {
  import { foo } from 'module2'
}

由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。
但ES2020提案引入import()函数,支持动态加载模块。
import()函数接受一个参数specifier,为所要加载的模块的位置,返回一个 promise。
import()类似于Node的require方法,区别主要是前者是异步加载,后者是同步加载。
看个例子:

// main.js
function init() {
  const canvas = 'canvas'
  
  // 使用import函数
  import(`./modules/${canvas}.js`).then((module) => {
    console.log(module.default)
  })
}

init()
// canvas.js
console.log('canvas.js开始执行')

export default 'canvas'

6.4 导出值

commonjs导出的是一个值拷贝,会对加载结果进行缓存,一旦内部再修改这个值,则不会同步到外部。比如:

// a.js
// 第一次require之后就缓存了done
const { done } = require('./b.js')

console.log(done)

setTimeout(() => {
  console.log(done)
}, 500)
// b.js
var done = false

setTimeout(() => {
  done = true
}, 500)

exports.done = done

执行node a.js,两次打印结果应该都是false

ES6是导出的一个引用,内部修改可以同步到外部。看一个之前的例子:

// a.js
export var foo = 'bar'

setTimeout(() => foo = 'baz', 500)
// index.js
import { foo } from './a.js'

console.log(foo)

setTimeout(() => console.log(foo), 500)

根据ES module的特性,开始执行时输出bar,0.5秒后输出 baz。

6.5 this指向

commonjs中顶层的this指向这个模块本身,而ES6中顶层this指向undefined。
因为ES module采用严格模式,严格模式禁止this指向全局对象。

6.6 循环加载的处理

由于之前五个不同点,导致commonjs和es module在处理循环加载时的表现不同。可以参考这篇文章

6.6.1 CommonJs的表现

根据 6.4 ,在commonjs中,代码在require的时就会全部执行,然后在内存生成一个对象,内部修改不会同步到外部。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。
比如可以运行如下例子:

// mian.js
var a = require('./a.js')

var b = require('./b.js')

// 在 main.js 之中, a.done=true, b.done=true
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done)
// a.js
exports.done = false

console.log('a 开始执行')

var b = require('./b.js')

// 在 a.js 之中,b.done = true
console.log('在 a.js 之中,b.done = %j', b.done)

exports.done = true

console.log('a.js 执行完毕')
// b.js
exports.done = false

// 返回a.js已经执行的部分,此时done为false
var a = require('./a.js')

// 在 b.js 之中,a.done = false
console.log('在 b.js 之中,a.done = %j', a.done)

exports.done = true

console.log('b.js 执行完毕')
6.6.2 ES module的表现

在ES module中,import存在提升,且导出值是只是一个引用,等到真的需要用到时,会再去模块里面去取值。比如导出一个变量,内部修改会影响外部。
可以运行这个例子:

// a.js
console.log('执行a.js')

// import会被提升到顶部
import { bar } from './b.js'

export function foo() {
  bar()
  console.log('执行完毕')
}

foo()

console.log('a.js执行完毕')
// b.js
console.log('执行b.js')

import { foo } from './a.js'

export function bar() {  
  if (Math.random() > 0.5) {
    foo()
  }
}

console.log('b.js执行完毕')

执行a.js,输出结果应该类型于
在这里插入图片描述
“执行b.js” “b.js执行完毕” “执行a.js” “执行完毕” “a.js执行完毕”四种输出的顺序是固定的,但“执行完毕”的输出次数不定

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值