模块化的标准规范

模块化的最佳实践

在浏览器环境中使用 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` 参数;
执行node --experimental-modules index.mjs结果如上
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