模块化的标准规范
模块化的最佳实践
在浏览器环境中使用 ES Modules 规范
在 nodeJS 环境中使用 CommonJS 规范
1、ES Modules 的语法特性
- 自动采用严格模式,忽略 ' use strict'
- 每个 ESM 模块都是单独的私有作用域
- ESM 是通过 CORS 跨域请求 去请求外部 JS 模块的
- ESM 的 script 标签会延迟执行脚本 (等待网页渲染完成再执行脚本)
<!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>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) // undefined
</script>
<!-- 2. 每个 ES Module 都是运行在单独的私有作用域中 -->
<script type="module">
var foo = 100
console.log(foo) // 100
</script>
<script type="module">
console.log(foo) // 报错
</script>
<!-- 3. ESM 是通过 CORS 的方式请求外部 JS 模块的 -->
<!-- <script type="module" src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script> -->
<!-- 4. ESM 的 script 标签会延迟执行脚本 -->
<script defer src="demo.js"></script>
<p>需要显示的内容</p>
</body>
</html>
2、ES Modules 导入和导出
export
- 导出变量
// ./modules.js
export var name = 'foo module'
- 导出函数或类
export function hello () {
console.log('hello')
}
export class Person {}
- 导出对象
var name = 'foo module'
function hello () {
console.log('hello')
}
class Person {}
export { name, hello, Person }
- 重命名
export {
hello as fooHello
}
- 导出成员设置为 defult ,那这个成员就会做为这个模块默认当初的成员
- 只能导出一个
export default name
import 导入
- 导入变量
import {name} from "./module.js"
- 导入函数或类
import {hello} from "./module.js"
- 导入对象
import {hello,person,name} from "./module.js"
- 导入export default
- 在进行export defalut时,只能接受一个导出变量,并且在导入时允许自定义变量名称
ES Module用法和注意事项
- 导出导出注意事项
- 导出成员并不是一个字面量对象
- 导出的成员并不是导出里面的值,而是一个存放成员的地址,拿到成员会受到当前模块修改的影响
- 在外部导入的成员,导入的模块成员是一个只读成员
- 导入
- from 后面是导入的是文件的路径,需要输入完成的文件名称,不能省略.js 扩展名
- 相对路径的 ./ 省略掉的话,esm 会认为在加载第三方的模块
- 手动要填写完整的路径
- 也可以输入完整路径去引用模块
- 也可以完整的 url 加载
import { name } from './module.js'
import { lowercase } from './utils/index.js'
import { name } from '/04-import/module.js'
import { name } from 'http://localhost:3000/04-import/module.js'
- 只是需要去执行某个模块,而不需要提取成员的话,就可以保持import{}当中的内容为空,就ESM就只会执行模块,不去提取成员
// import {} from './module.js'
import "./module.js" // 简写语法
- 如果模块中需要导出的成员特别多,而在导入时都会用到,可以使用 * 全部提取
module.js//
var name = "foo module"
function hello(){
console.log(hello)
export {
name as Name,
hello
}
导入
app.js//
import * as mod from './module.js'
console.log(mod.Name)
console.log(mod.hello())
- 在使用导入模块的时候 import 关键词可以理解成是一个导入模块的声明,在开发阶段就要明确表示我们开发的路径
- 但是有的时候,这个开发路径实在运行阶段才知道的,那么这个时候不能使用 import 去 from 一个变量
- 而且有时候会在某个情况下,当某些条件满足够后我们再去导入模块,拿在这种情况下,也没办法使用 import
- import 只能出现在最顶层,能不能出现在像 if 等语句中
- 这个时候需要动态导入
import('./module.js').then(function (module){
console.log(module);
})
- 如果在一个模块当中同时导出了一些命名成员,在导出一个默认的成员
导出
export default "defult export"
导入
import {name , age, default as tittle} from "./module.js"
//简写写法
import tittle, {name , age} from "./module.js"
console.log(name,age,tittle) ==》module.js 18 defult export
- 当导出多个组件时,可以新建一个当前目录下的所有组件,载入进来,然后在集中导出
创建index文件集中导出 Button.js 与 Avatar.js 两个模块文件
import { Button } from './button.js'
import { Avatar } from './avatar.js'
// 默认模块
// export { default as Button } from './button.js'
export { Button, Avatar }
// app.js 进行导入
import { Button, Avatar } from './components/index.js'
console.log(Button)
console.log(Avatar)
button.js模块导出采用默认模块
export default Button
index..js导出模块必须要采用
export {default as Button} from "./button.js"
3、ES modules 浏览器环境 polyfill 兼容方案
ES modules 对于早期浏览器是不支持的 例如IE
Polyfill 可以直接让低版本浏览器支持绝大多数的特性
拷贝这两个文件的地址:
<script src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>
<script src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>
还需要在引入一个 Promise Polyill :taylorhakes/promise-polyfill
<script src="https://unpkg.com/promise-polyfill@8.1.3/dist/polyfill.min.js"></script>
此时存在一个小问题,在支持ES6的浏览器中,代码被执行了2次
需要在 script 标签上添加新属性 nomodul
<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>
只适合进行测试,不要在生产阶段使用
4、ES Modules in Node.js
Node可以以原生方式使用ESM编写代码
使用方式:
- 更改js文件后缀为.mjs
- import js的时候,如import'./index.js';需要写成import'./index.mjs'
- 启动时需要额外添加命令行 `--experimental-modules` 参数;
import { foo, bar } from './module.mjs'
console.log(foo, bar)
// 此时我们也可以通过 esm 加载内置模块了
import fs from 'fs'
fs.writeFileSync('./foo.txt', 'es module working')
// 内置模块也可以直接提取模块内的成员,内置模块兼容了 ESM 的提取成员方式
import { writeFileSync } from 'fs'
writeFileSync('./bar.txt', 'es module working')
// 对于第三方的 NPM 模块也可以通过 esm 加载
import _ from 'lodash'
console.log(_.camelCase('ES Module'))
// 不支持,因为第三方模块都是导出默认成员 不是解构
// import { camelCase } from 'lodash'
// console.log(camelCase('ES Module'))
5、ES Modules in Node.js 与 CommonJS 模块交互
ES-modules 载入common.js模块
common.js
// CommonJS 模块始终只会导出一个默认成员
// module.exports = {
// foo: 'commonjs exports value'
// }
// exports.foo = 'commonjs exports value'
// 不能在 CommonJS 模块中通过 require 载入 ES Module
// const mod = require('./es-module.mjs')
// console.log(mod)
es-Modules.mjs
// ES Module 中可以导入 CommonJS 模块
// import mod from './commonjs.js'
// console.log(mod)
// 不能直接提取成员,注意 import 不是解构导出对象(已经更新,现在可以提取成员)
// import { foo } from './commonjs.js'
// console.log(foo)
// export const foo = 'es module export value'
- Node 环境中
- ES Module 中可以导入 CommonJS 模块
- CommonJS 中不能导入 ES Module 模块
- CommonJS 始终只会导出一个默认成员
- 注意 import 不是解构导出对象
Node环境中 ES Modules.JS 与 CommonJS 模块的差异
cmjs.js
// 加载模块函数
console.log(require)
// 模块对象
console.log(module)
// 导出对象别名
console.log(exports)
// 当前文件的绝对路径
console.log(__filename)
// 当前文件所在目录
console.log(__dirname)
nodemon common.js
ESM中没有CommonJS 中的那些模块全局成员了
common.js 中的 requie、module、export这些可以通过
ESM中的exports和import去代替。
而__firename和__dirname两个变量可以通过
import.mata.url去代替去拿到当前文件的路径,如下所示
// 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)
拿到结果
7、ES Modules in Node.js - 新版本进一步支持
通过创建package.json 添加字段:{ "type":"module"}将所有js文件以ES Module去工作
- 配置package.json文件type字段之后,如果想要继续使用 CommonJS 规范会打印失败
- package.json 中设置了type,所有js文件都以ESM去工作,而ES Module中并没有提供require。
- 想要运行Common.js文件,需要将文件扩展名改为 .cjs。
8、ES Modules in Node.js - Babel 兼容方案
npm i @babel/core @babel/preset-env @babel/node --dev
也可以yarn安装
$yarn add @babel/node @babel/core @babel/preset-env --dev
Babel是以插件为核心机制去实现的,核心模块并不会去转换代码,需要通过插件来实现。我们需要一个插件来转换代码中的一个特性,@babel/preset-env实际上是一个插件的集合,在这个集合当中包含了最新的js标准中的所有新特性,我们就可以借助preset 把当前代码中所有使用到的 ESModule 转换过来
在node_module文件夹中为我们提供了一个babel-node命令,我们可以通过它来运行ES Module代码
npx babel-node index.js --presets=@babel/preset-env
yarn babel-node index.js --presets=@babel/preset-env
每次都需要手动传入preset参数麻烦,我们可以将其放到配置文件当中,在项目根目录中创建一个.babelrc文件
{
"presets":["@babel/preset-env"]
}
帮我们去转换的是一个插件,并不是preset,preset只是一个集合,可以尝试移除preset,单独使用插件看看效果
移除:npm uninstall @babel/preset-env
移除:yarn remove @babel/preset-env
安装: npm i @babel/plugin-transform-modules-commonjs --dev
yarn安装: yarn add @babel/plugin-transform-modules-commonjs-dev
配置babelrc文件中的plugins:
{
"plugins": [
"@babel/plugin-transform-modules-commonjs"
]
}
npx babel-node index.js yarn babel-node index.js