Part2 · 前端工程化实战
模块化开发与规范化标准
文章说明:本专栏内容为本人参加【拉钩大前端高新训练营】的学习笔记以及思考总结,学徒之心,仅为分享。如若有误,请在评论区支出,如果您觉得专栏内容还不错,请点赞、关注、评论。共同进步!
本篇主要内容是前端模块化开发与规范化标准
一、模块化演变过程
模块化概述:
模块化开发为当前最重要的前端开发范式之一。随着前端代码的日益复杂,的前端项目代码出现了不得不花费大量时间去整理。而模块化就是最主流的代码组织方式。他通过把复杂的代码通过功能不同划分为不同的模块,以单独维护的方式,提高开发效率,降低维护成本。【模块化】仅仅为一个思想,并没有提供具体的实现。
1.stage1 基于文件划分
将每一个模块独立为一个文件,在页面中引入这些文件(web中最原始的模块化系统)。
具体做法就是将每个功能及其相关状态数据各自单独放到不同的文件中,约定每个文件就是一个独立的模块,使用某个模块就是将这个模块引入页面,然后直接调用模块的中的成员(成员/函数)
特点:
- 所有的模块都直接在全局工作,并没有私有空间,所有的成员都可以在模块外部被访问或者修改;
- 文件模块过多时,容易产生命名冲突;
- 无法管理模块与模块之间的依赖关系
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Modular evolution stage 1</title>
</head>
<body>
<h1>模块化演变(第一阶段)</h1>
<h2>基于文件的划分模块的方式</h2>
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
// 命名冲突,此处命名冲突问题由于引入模块顺序问题而不同,name值最后指向module-b.js中的name值
method1()
// 模块成员可以被修改,外部可以任意修改模块内部成员
name = 'foo'
</script>
</body>
</html>
module-a.js
// module a 相关状态数据和功能函数
var name = 'module-a'
function method1 () {
console.log(name + '#method1')
}
function method2 () {
console.log(name + '#method2')
}
module-b.js
// module b 相关状态数据和功能函数
var name = 'module-b'
function method1 () {
console.log(name + '#method1')
}
function method2 () {
console.log(name + '#method2')
}
2.stage2 命名空间方式
命名空间方式每个模块只暴露一个全局对象,所有模块成员都挂载到这个对象中。具体做法就是在第一阶段的额基础上,通过将每个模块【包裹】为一个全局对象的形式实现,有点类似于为模块内的成员添加了【命名空间】的感觉。
特点:
- 通过【命名空间】的方式减少了命名冲的可能
- 同样没有私有空间,所有模块成员也可以在模块外部被访问或者修改
- 同样无法管理模块之间的依赖关系
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Modular evolution stage 2</title>
</head>
<body>
<h1>模块化演变(第二阶段)</h1>
<h2>每个模块只暴露一个全局对象,所有模块成员都挂载到这个对象中</h2>
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
moduleA.method1()
moduleB.method1()
// 模块成员可以被修改
moduleA.name = 'foo'
</script>
</body>
</html>
module-a.js
// module a 相关状态数据和功能函数
var moduleA = {
name: 'module-a',
method1: function () {
console.log(this.name + '#method1')
},
method2: function () {
console.log(this.name + '#method2')
}
}
module-b.js
// module b 相关状态数据和功能函数
var moduleB = {
name: 'module-b',
method1: function () {
console.log(this.name + '#method1')
},
method2: function () {
console.log(this.name + '#method2')
}
}
3.stage3 IIFE
使用立即执行函数表达式IIFE(Immediately-Invoked Function Expression)为模块提供私有空间。具体做法就是将每个模块成员都放在一个函数提供的私有作用域中,对于需要宝库给外部的成员,通过挂载全局对象的方式实现。
特点:
有了私有成员的概念,私有成员只能在模块成员内通过必报的形式访问。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Modular evolution stage 3</title>
</head>
<body>
<h1>模块化演变(第三阶段)</h1>
<h2>使用立即执行函数表达式(IIFE:Immediately-Invoked Function Expression)为模块提供私有空间</h2>
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
moduleA.method1()
moduleB.method1()
// 模块私有成员无法访问
console.log(moduleA.name) // => undefined
</script>
</body>
</html>
module-a.js
// module a 相关状态数据和功能函数
;(function () {
var name = 'module-a'
function method1 () {
console.log(name + '#method1')
}
function method2 () {
console.log(name + '#method2')
}
window.moduleA = {
method1: method1,
method2: method2
}
})()
module-b.js
// module b 相关状态数据和功能函数
;(function () {
var name = 'module-b'
function method1 () {
console.log(name + '#method1')
}
function method2 () {
console.log(name + '#method2')
}
window.moduleB = {
method1: method1,
method2: method2
}
})()
4.stage4 IIFE 参数
利用IIFE参数作为依赖声明使用,具体做法是在第三阶段的基础上,利用立即执行函数的参数传递模块依赖项
特点:
每个模块之间的关系变得更加明显
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Modular evolution stage 4</title>
</head>
<body>
<h1>模块化演变(第四阶段)</h1>
<h2>利用 IIFE 参数作为依赖声明使用</h2>
<p>
具体做法就是在第三阶段的基础上,利用立即执行函数的参数传递模块依赖项。
</p>
<p>
这使得每一个模块之间的关系变得更加明显。
</p>
<script src="https://unpkg.com/jquery"></script>
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
moduleA.method1()
moduleB.method1()
</script>
</body>
</html>
module-a.js
// module a 相关状态数据和功能函数
;(function ($) {
var name = 'module-a'
function method1 () {
console.log(name + '#method1')
$('body').animate({ margin: '200px' })
}
function method2 () {
console.log(name + '#method2')
}
window.moduleA = {
method1: method1,
method2: method2
}
})(jQuery)
module-b.js
// module b 相关状态数据和功能函数
;(function () {
var name = 'module-b'
function method1 () {
console.log(name + '#method1')
}
function method2 () {
console.log(name + '#method2')
}
window.moduleB = {
method1: method1,
method2: method2
}
})()
5.stage5 模块化规范的出现
require.js提供AMD模块化规范以及一个自动模块化加载器
目录结构:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Modular evolution stage 5</title>
</head>
<body>
<h1>模块化规范的出现</h1>
<h2>Require.js 提供了 AMD 模块化规范,以及一个自动化模块加载器</h2>
<script src="lib/require.js" data-main="main"></script>
</body>
</html>
module1.js
// 因为 jQuery 中定义的是一个名为 jquery 的 AMD 模块
// 所以使用时必须通过 'jquery' 这个名称获取这个模块
// 但是 jQuery.js 并不在同级目录下,所以需要指定路径
define('module1', ['jquery', './module2'], function ($, module2) {
return {
start: function () {
$('body').animate({ margin: '200px' })
module2()
}
}
})
module2.js
// 兼容 CMD 规范(类似 CommonJS 规范)
define(function (require, exports, module) {
// 通过 require 引入依赖
var $ = require('jquery')
// 通过 exports 或者 module.exports 对外暴露成员
module.exports = function () {
console.log('module 2~')
$('body').append('<p>module2</p>')
}
})
main.js
require.config({
paths: {
// 因为 jQuery 中定义的是一个名为 jquery 的 AMD 模块
// 所以使用时必须通过 'jquery' 这个名称获取这个模块
// 但是 jQuery.js 并不一定在同级目录下,所以需要指定路径
jquery: './lib/jquery'
}
})
require(['./modules/module1'], function (module1) {
module1.start()
})
二、模块化规范
1.CommonJS
在之前的每个阶段中,模块加载的方式都是通过script标签手动引入。也就是说,之前的方法中,模块的加载并不受代码的控制。一旦模块过多,会出现各种问题,比如HTML忘记引入模块等。
基于node.js的commonJS规范
- 一个文件就是一个模块
- 每个模块都有单独的作用域
- 通过module.exports导出成员
- 通过require函数载入模块
commonJS是以同步模式加载模块,node执行机制是在启动时加载模块,在执行过程中不需要加载,只会使用。在浏览器端使用commonJS规范的话,必然导致效率低下。每次页面加载都会导致大量的同步模式请求出现。
2.AMD
所以在早期并没有选择commonJS规范,而是专门为浏览器端且结合浏览器特点,重新设计了一个模块加载规范:AMD(Asynchronous Module Definition)异步模块定义规范。同时也除了一个require.js库,其实现了AMD规范,同时本身又是一个强大的模块加载器。
AMD规范中,require.js库规定每个模块使用define关键字去定义。可以传递2-3个参数,传递三个参数的话,第一个参数为该模块的名字;第二个参数为数组,声明该模块的依赖项,数组中每个数组的元素为具体依赖的其他模块;第三个模块为一个函数,该函数的参数与第二个参数中的依赖项一一对应,每一项分别为依赖项导出的成员,函数的作用是为当前的模块提供一个私有的空间。如果需要在本模块中向外部模块导出一些成员,通过return的方式去实现。
define('module1', ['jquery', './module2'], function () {
return {
start: function () {
$('body').animate({margin: '200px'})
module2()
}
}
})
除此之外,require.js还提供了一个require()函数,该函数用来自动加载模块。用法与define类似,区别是require只是用来加载模块,而define是用来定义模块。require函数去加载一个模块时,其内部会自动创建一个script标签,发送对应脚本文件的请求,并且执行相应的模块代码。
require(['module1'], function(module1) {
module1.start()
})
目前绝大多数第三方库都支持AMD规范,其生态相对较好,但是AMD使用起来比较复杂,除了业务代码,需要使用define定义模块以及require()函数去加载模块,导致代码复杂程度较高。如果项目中模块的划分较为细致时,模块JS文件请求频繁,从而呆滞页面效率比较低下。所以AMD也只能为前端模块化规范前进的一步,只是一种妥协的手段,并不是最终的解决方案。
同期淘宝出现了Sea.js + CMD标准,类似commonJS且用法上与require.js大致相同,这种方式在后来也被Require.js兼容。
// CMD规范(类似CommonJS)
define(function (require, exports, module) {
// 通过require引入依赖
var $ = require('jquery');
// 通过exports或者module.exports对外暴露成员
module.exports = function () {
console.log('module-2');
$('body').append('<p>module2</p>')
}
})
3.模块化标准规范
随着技术的发展,模块化 技术实现方式相对以往有了很大的变化。大家在前端模块化的方式也基本统一。在node.js中遵循CommonJS,在Browser中采用ES Module。
在node.js中,CommonJS为其内置的模块,正常使用require去导入模块,module.exports去导出模块。但是在ES Module在browser中就比较复杂一些。ES Module时ECMAScript2015(ES6)中定义的一个最新的模块系统,他是最近几年才定义的标准。定义初期,几乎所有主流浏览器都不支持这个特性,随着webpack等一系列打包工具的流行,这一规范才逐渐开始普及。
4.ES Module
1.基本特性
- 自动采用严格模式,忽略’use strict’
- 每个ESM模块都是单独的私有作用域
- ESM是通过CORS去请求外部JS模块的
- ESM的script标签会延迟执行脚本
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ES Module - 模块的特性</title>
</head>
<body>
<!-- 通过给 script 添加 type = module 的属性,就可以以 ES Module 的标准执行其中的 JS 代码了 -->
<script type="module">
console.log('this is es module')
</script>
<!-- 1. ESM 自动采用严格模式,忽略 'use strict' -->
<script type="module">
console.log(this) // undifined
</script>
<!-- 2. 每个 ES Module 都是运行在单独的私有作用域中 -->
<script type="module">
var foo = 100
console.log(foo)
</script>
<script type="module">
console.log(foo) // foo is not defined
</script>
<!-- 3. ESM 是通过 CORS 的方式请求外部 JS 模块的 -->
<!-- 所以应用外部js问价需要其CDn支持CORS -->
<!-- <script type="module" src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script> -->
<!-- CORS不支持文件的方式去访问,只能通过http的方式去访问 -->
<!-- 4. ESM 的 script 标签会延迟执行脚本 defer延迟执行 type="module"与defer属性功能相同 -->
<!-- <script defer src="demo.js"></script> -->
<script type="module" src="demo.js"></script>
<p>需要显示的内容</p>
</body>
</html>
2.导入和导出
-
export用法
1.单独导出每个成员export var name
module.js
export var name = 'foo module' export function hello () { console.log('hello') } export class Person {}
app.js
import { name, hello, Person } from './module.js' console.log(name) // foo module hello() // hello
2.模块尾部统一导出
module.js
var name = 'foo module'
function hello () {
console.log(‘hello’)
}
class Person {}
export { name, hello, Person }
app.js
```js
import { name, hello, Person } from './module.js'
console.log(name) // foo module
hello() // hello
3.导出别名
module.js
var name = 'foo module'
function hello () {
console.log('hello')
}
export {
name as fooName,
hello as fooHello
}
app.js
import { fooName, fooHello } from './module.js'
console.log(fooName) // foo module
fooHello() // hello
4.导出default
module.js
var name = 'foo module'
export {
name as default
}
app.js
// 由于default为关键字,所以引入该变量时需要重命名
import { default as fooName } from './module.js'
console.log(fooName) // foo module
5.模块默认导出,引入时可取任意变量名
module.js
var name = 'foo module'
export default name
app.js
import abc from './module.js'
console.log(abc) // foo module
-
注意事项
- export固定写法 { }
- import导入成员并不是复制一个副本,而是直接导入模块成员的引用地址。也就是说import得到的变量与export导出的变量在内存中是同一块空间。一旦模块中的成员被修改,引入的变量也会同时修改。
- import导入的变量是只读变量,但对象的读写属性不受影响
module.js
var name = 'jack' var age = 18 // var obj = { name, age } // export default { name, age } // 这里的 `{ name, hello }` 不是一个对象字面量, // 它只是语法上的规则而已 export { name, age } // export name // 错误的用法
// export ‘foo’ // 同样错误的用法
setTimeout(function () {
name = ‘ben’
}, 1000)
app.js
```js
// CommonJS 中是先将模块整体导入为一个对象,然后从对象中结构出需要的成员
// const { name, age } = require('./module.js')
// ES Module 中 { } 是固定语法,就是直接提取模块导出成员
import { name, age } from './module.js'
console.log(name, age)
// 导入成员并不是复制一个副本,
// 而是直接导入模块成员的引用地址,
// 也就是说 import 得到的变量与 export 导入的变量在内存中是同一块空间。
// 一旦模块中成员修改了,这里也会同时修改,
setTimeout(function () {
console.log(name, age)
}, 1500)
// 导入模块成员变量是只读的
// name = 'tom' // 报错
// 但是需要注意如果导入的是一个对象,对象的属性读写不受影响
// name.xxx = 'xxx' // 正常
-
import用法
app.js
// 1.导入规则 // import { name } from './module' // 不可以省略.js扩展名以及./,在commonJS中可以省略扩展名及./ import { name } from './module.js' console.log(name) // 'jack' // commonJS中可以直接导入模块,例如:import { lowercase } from './utils',但是在原生ESM中需要填写完整路径 // 后期使用打包工具后,可以省略扩展名以及省略index.js默认文件 import { lowercase } from './utils/index.js' console.log(lowercase("HHH")) // 导入模块时必须使用/开头,否则ESM认为是需要加载一个第三方模块 // import { name } from './module.js' // 或者从网站根目录开始 // import { name } from '/04-import/module.js' // 或者使用完整的url加载模块 import { name } from 'http://localhost:3000/04-import/module.js' // 意味着可以直接饮用CDN的模块文件 console.log(name) // 2.只是需要执行某个模块,并不需要提取模块中的成员 import {} from './module.js' // 或者import './module.js' ,在并不需要外界控制的子功能模块式使用此种导入方式 // 3.导入多个模块 import * as mod from './module.js' // 将所有的导出的成员全部导入并使用as重命名,全部放入一个对象中,每个成员都会作为对象的属性 console.log(mod) // 4.动态导入模块 // var modulePath = './module.js' // import { name } from modulePath // console.log(name) // 报错 // if (true) { // import { name } from './module.js' // } // 报错 // ESM提供全局函数import(),专门用于动态导入模块,该函数返回一个promise对象,当模块的异步加载完成后,会自动执行then中的回调函数,模块的对象可以通过参数获取 import('./module.js').then(function (module) { console.log(module) }) // 5.导入命名成员以及默认成员 import { name, age, default as title } from './module.js' // 或者 console.log(name, age, title) // 导入命名成员以及默认成员简写 import abc, {name, age} from './module.js' // abc可以使用任意变量名 console.log(abc, name, age)
module.js
var name = 'jack' var age = 18 export { name, age } console.log('module action') export default 'default export'
utils/index.js
export function lowercase (input) { return input.toLowerCase() }
-
直接导出所导入的成员
除了导入模块,import还可以配合export使用,效果是将导入的结果直接作为当前模块的导出成员。导出后,当前作用域不再可以访问导入的成员了。一般在index.js中使用,在index.js中把某些目录中散落的一些模块通过export组织到一起再进行导出
app.js
// export {name, age} from './module.js' // console.log(name) // name is not defined // 繁琐的方法 import {Button} from './components/button.js' import {Avatar} from './components/avatar.js' console.log(Button, Avatar) // 简单的方法,compotents中新增index.js,导入再导出组件 import { Button, Avatar } from './components/index.js' console.log(Button) console.log(Avatar)
components/button.js
var Button = 'Button Components' export default Button
components/avatar.js
export var Avatar = 'Avatar Components'
components/index.js
// import {Button} from './components/button.js' // import {Avatar} from './components/avatar.js' // export {Button, Avatar} export { default as Button } from './button.js' export { Avatar } from './avatar.js'
-
polyfill兼容方案
ESM2014年提出,早期的浏览器它不可能支持这个特性,另外呢,在IE还有一些国产的浏览器上,截止到目前为止都还没有支持,所以说在使用的时候还是需要去考虑将信所带来的一个问题。
可以借助一些编译工具在开发阶段将这些ES6的代码,编译成ES5的方式,然后呢,再到浏览器当中去执行。这里介绍一个模块browser-es-module-loader,将文件引入到网页中,网页就可以运行ESM了。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>ES Module 浏览器环境 Polyfill</title> </head> <body> <script nomodule src="https://unpkg.com/promise-polyfill@8.1.3/dist/polyfill.min.js"></script> <script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script> <script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script> <script type="module"> import { foo } from './module.js' console.log(foo) </script> <!-- 以上代码在支持ESm中的浏览器会运行两次,所以使用nomoudle确保在不支持ESM的浏览器工作 --> <script nomodule> alert('hello') </script> </body> </html>
这种兼容ESm的方式,它只适合于本地区测试,也就是开发阶段去玩一玩,但是,在生产阶段千万不要去用它,因为它的原理是在运行阶段动态的去解析脚本,效率非常的差。在生产阶段,还是应该预先去把这些代码编译出来,让它可以直接在浏览器当中去工作。
4.ESM in Node.js
-
与CommonJS交互
ESM作为JavaScript的语言层面的一个模块化标准,逐渐的会去统一所有JS应用领域的模块化需求,Node.js作为JavaScript的一个非常重要的一个应用领域,目前,已经开始逐步支持这样一个特性,从Node.js的8.5版本过后,内部就已经以实验特性的方式去支持ESM了,也就是说在Node.js当中可以直接原生的去使用ESM去编代码了。但是,考虑到原来的这个comment规范与现在的ESM他们之间的差距还是比较大的,所以说目前,这样一个特性一直还是处于一个过渡的状态,那接下来,就一起来尝试一下,直接在Node环境当中使用ESM编写代码。
需要在Node.js中使用ESM:
- 首先将文件扩展名改为.mjs
- 然后在命令行使用–experimental-modules参数,这个参数代表去启用ESM的实验特性。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jEsgLoEd-1608214600072)(./image-20201216231354541.png)]
提取第三方模块
import _ from 'lodash' console.log(_.camelCase('ES Module')) // esModule
提取node内置模块成员
// 也可以直接提取模块内的成员,内置模块兼容了 ESM 的提取成员方式 import {writeFileSync} from 'fs' writeFileSync('./bar.txt', 'es module working~') // 不支持,因为第三方模块都是导出默认成员 // import { camelCase } from 'lodash' // console.log(camelCase('ES Module'))
-
与CommonJS的差异
1.在ESM中使用CommonJS模块
es-module.mjs
// ES Module 中可以导入 CommonJS 模块 // 只能使用import载入默认成员的方式去使用commonJS模块 import mod from './commonjs.js' console.log(mod)
common.js
// CommonJS 模块始终只会导出一个默认成员 // module.exports = { // foo: 'commonjs exports value' // } // 使用commonJS的导出的别名exports exports.foo = 'commonjs exports value'
在命令行运行
D:\DeskTop\02-interoperability>node --experimental-modules es-module.mjs { foo: 'commonjs exports value' }
2.通过commonJS载入ESM(Node原生的环境中不能在 CommonJS 模块中通过 require 载入 ES Module)
es-module.js
export const foo = 'es module export value'
common.js
const mod = require('./es-module.js') console.log(mod) // !!!报错
总结:
- ESM中可以导入CommonJS模块
- CommonJS中不能导入ESM模块
- CommonJS始终只会导出一个默认成员
- 注意import不是解构导出对象
-
ES Modules in Node.js - 与 CommonJS 的差异
esm.mjs
// ESM 中没有模块全局成员了 // // 加载模块函数 // console.log(require) // // 模块对象 // console.log(module) // // 导出对象别名 // console.log(exports) // // 当前文件的绝对路径 // console.log(__filename) // // 当前文件所在目录 // console.log(__dirname) // -------------以上成员无法打印 // require, module, exports 自然是通过 import 和 export 代替 // __filename 和 __dirname 通过 import 对象的 meta 属性获取 // const currentUrl = import.meta.url // console.log(currentUrl) // 通过 url 模块的 fileURLToPath 方法转换为路径 import { fileURLToPath } from 'url' import { dirname } from 'path' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) console.log(__filename) console.log(__dirname)
cjs.js
// 加载模块函数 console.log(require) // 模块对象 console.log(module) // 导出对象别名 console.log(exports) // 当前文件的绝对路径 console.log(__filename) // 当前文件所在目录 console.log(__dirname)
-
新版本进一步支持ESM
在Node.js的最新版本当中,它进一步的支持了ESM,这里可以来尝试一下,可以通过Node刚刚experimental去执行一这个js文件,那此时执行的效果呢,跟之前所看到的也是一样的,不过呢,在这个新版本当中,可以通过给项目的package点当中去添加一个type字段断,将这个type字段的设置为module,这个时候这个项目向所有的js文件默认就会以ESM去工作了,也就是说不用再将扩展名改成mjs了,直接让他们改回来为js,再回到文件当中,将文件当中的路径也给它修改回来。
此时,就可以回到命令行当中,再次重新运行一下index.js,这个js文件就会按照ESM的形式去工作了,如果这个时候你还想去使用commonJS的话,例如再去新建一个common.js这样一个文件,这个时候需要单独对于commonJS这种方式,做一个偶尔的处理,那就是将这个common的文件修改为点cjs的这样一个扩展名,那此时再次去执行的话,就可以正常的去使用common js规范了。
index.js
// Node v12 之后的版本,可以通过 package.json 中添加 type 字段为 module, // 将默认模块系统修改为 ES Module // 此时就不需要修改文件扩展名为 .mjs 了 import { foo, bar } from './module.js' console.log(foo, bar)
module.js
export const foo = 'hello' export const bar = 'world'
common.cjs
// 如果需要在 type=module 的情况下继续使用 CommonJS, // 需要将文件扩展名修改为 .cjs const path = require('path') console.log(path.join(__dirname, 'foo'))
package.json
{ "type": "module" }
-
Babel兼容方案
如果你使用的是早期的Node,可以使用babel去实现ESM的兼容问题。babel是目前最主流的一块JavaScript的编译器,他可以用来帮助我们将一些使用了新特性的代码,编译成当前环境的代码。之后我们可以放心的去使用新特性。下面使用babel去实现低版本Node(这里使用8.0.0)。
index.js
// 对于早期的 Node.js 版本,可以使用 Babel 实现 ES Module 的兼容 import { foo, bar } from './module.js' console.log(foo, bar)
module.js
export const foo = 'hello' export const bar = 'world'
-
安装babel及其他插件
yarn add @babel/node @babel/core @babel/preset-env --dev
-
直接运行yarn babel-node index.js时会报错,不支持import。原因非常简单,因为babel,它是基于插件机制去实现的,它的核心模块,并不会去转换我们的代码,那具体要去转换代码当中的每一个特性,它是通过插件来去实现的,也就是说需要一个插件去转换代码当中的一个特性,那之前所安装的这个preset-env,它实际上是一个插件的集合,在这个插件集合当中去包含了最新的JS标准当中的所有的新特性,可以借助于这个preset直接去把我们当前这个代码当中所使用到的ESM就给它转换过来。
-
使用新命令
yarn babel-node index.js --presets=@babel/preset-env # hello world
-
如果说觉得每次手动的去传输这样一个参数会比较麻烦的话,那你也可以选择把它放到配置文件当中。项目中新建.babelrc文件,该文件为json格式的文件
{ "presets": ["@babel/preset-env"] }
此时可以直接使用yarn babel-node index.js命令直接运行,不需要额外加参数。
-
preset是一个插件集合,我们移除preset,直接使用插件
yarn remove @babel/preset-env yarn add @babel/plugin-transform-modules-commonjs --dev
这时修改配置文件
{ "plugins": [ "@babel/plugin-transform-modules-commonjs" ] }
继续运行命令yarn babel-node index.js,这样也是可以的。
-