模块化开发与ES Modules详解
December 19, 2024 (1y ago)
1、概述
模块化的开发方式是一种代码管理方式的提现,用约定用语法 去约定复杂代码的管理方式,方便开发大型应用,方便 对大型项目的维护
我们开看一下 模块化,在前端的发展历史
- [x] 阶段1 直接script脚本导入的方式

缺点:污染全局,命名冲突,无法管理模块之间的互相依赖
- [x] 阶段2 解决全局命名的问题

缺点: 模块里的成员 在外部依然能被修改,而且模块依赖的我那天也没有得到很好的解决
- [x] 阶段3 立即执行函数!


缺点:基本上解决了上面的问题,但是还是有很多问题没有解决
上述的方案都是script的方式载入的,对于页面来说不好 模块的加载不受 代码的控制,比如你忘记加什么模块了什么的,如何解决上面的问题呢,“模块规范标准 + 模块预加载器”就是我们实现的一个思考方向。我们先看看 就规范来说,有哪些
- [x] Commonjs规范。
commonjs规范是nodejs中的,它的特点就是
- 一个文件就是一个模块
- 每个模块都有单独的作用域
- 通过module.export 导出成员
- 通过 require函数在载入模块
但是由于它是同步的,会堵塞浏览器,的执行,所以我们 由有了再浏览器环境下的AMD
- [x] AMD 规范 异步的模块定义
它竟是一个规范,又是一个模块加载器,具体的语法可以看这张图

图上的是定义模块

这个规范的内部就是创建一个scirpt标签去 发请求拿数据
缺点就是: 语法复杂,如果模块过多,同一个页面就会爆发很多请求 ,不友好
以上就是我们的前期,现在我们的模块化,在js的语法层面已经非常的统一了,在node环境下用Commonjs规范,在浏览器环境下使用 ES Module 规范,node下的Commonjs规范没什么要说的,要说的就是ES Module 它在不同环境下表现不太一致, 也有兼容问题,但是却是语法层面的规范,所以我们需要重点掌握它
3、ES Modules特性
我们需要了解他的特性,以便于我们后续聊聊他的兼容性和对应的解决方案
- 使用上的一些注意事项
<!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)
</script>
<!-- 2. 每个 ES Module 都是运行在单独的私有作用域中 -->
<script type="module">
var foo = 100
console.log(foo)
</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> -->
// 浏览器运行加载页面的时候,会堵塞html的渲染,它在加载时候是立即执行的,
<!-- 4. ESM 的 script 标签会延迟执行脚本 defer 与 type="module" -->
<script defer src="demo.js"></script>
<p>需要显示的内容</p>
</body>
</html>
demo.js
alert('hello ')4、ES Modules 导出
我们来看看文件夹的构建方式吗,这一讲我们主要是来看看 导入和导出的语法,主要的语法就是import 和export ,在模块内都是局部作用域,外部如果不用export 是拿不到的

app.js
// import { default as fooName } from './module.js'
// console.log(fooName)
import { name, hello, Person } from './module.js'
console.log(name, hello, Person)
module.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 { as 重命名
// // name as default,
// hello as fooHello
// }
// export default name 这是一个固定的语法!不是对象什么的语法 { } 就是一个固定的语法
export 只允许有两个方式
export default xxx
export { xxx }
// var obj = { name, hello, Person }
export { name, hello, Person }
5、ES Modules 导入导出的注意点
**注意!,我们的的import 不是结构!!!,**我们的导出不是一个对象,而是引用
import 导入的模块的时候,不能省去js扩展名,但是一般来说,我们开发的时候可以不写 是因为通过babel进行了转换 我们有下面的语法细节
app.js
// import { name } from './module'
// import { name } from './module.js'
// console.log(name)
// import { lowercase } from './utils'
// import { lowercase } from './utils/index.js'
// console.log(lowercase('HHH'))
// import { name } from 'module.js'
// import { name } from './module.js'
// import { name } from '/04-import/module.js'
// import { name } from 'http://localhost:3000/04-import/module.js'
// console.log(name)
// --------------
// import {} from './module.js'
// import './module.js'
// ---------------
// import * as mod from './module.js'
// console.log(mod)
// ---------------
// var modulePath = './module.js'
// import { name } from modulePath
// console.log(name)
// if (true) {
// import { name } from './module.js'
// }
// import('./module.js').then(function (module) {
// console.log(module)
// }) 动态路径导入
// ----------------
// import { name, age, default as title } from './module.js'
import abc, { name, age } from './module.js'
console.log(name, age, abc)
moudle.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()
}
一般的我们如果需要 导入导出一般是这样做的

8、ES Modules 浏览器环境兼容 Polyfill
我们只需要使用polyfill就好了,非常的简单
<!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 浏览器环境 Polyfill</title>
</head>
<body>
//nomodule 表示这个脚本只会在不支持 esmodule下去执行脚本,这个东西哈,都是执行阶段去装换,我们不要用到项目上去
<script nomodule src="https://unpkg.com/promise-polyfill@8.1.3/dist/polyfill.min.js"></script> [promise 兼用处理]
<script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script> [babel核心]
<script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script> [es-module插件]
<script type="module">
import { foo } from './module.js'
console.log(foo)
</script>
</body>
</html>
9、ES Modules in Nodejs
我们需要看看在node下如何使用 es modules
1、小demo
前置条件
# node version 要大于10
# 修改js的名称 变成 mjs
# 运行的时候需要调整一下运行时配置
node --expermental-modules index.mjs
app.mjs
// 第一,将文件的扩展名由 .js 改为 .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'
_.camelCase('ES Module')
// 不支持,因为第三方模块都是导出默认成员
// import { camelCase } from 'lodash'
// console.log(camelCase('ES Module'))
module.mjs
export const foo = 'hello'
export const bar = 'world'
2. ES Modules in Nodejs + Commonjs
我们来看看如何在es和commonjs一起混合使用
commonjs,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-module.js
// 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'
3. es 和cjs的差异
cjs
// 加载模块函数
console.log(require)
// 模块对象
console.log(module)
// 导出对象别名
console.log(exports)
// 当前文件的绝对路径
console.log(__filename)
// 当前文件所在目录
console.log(__dirname)
esjs
// 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)
11、ES Modules in Nodejs + 新版本进一步讲解
哪有什么新特性,实际上就是一个-配置项在pacjson中,你直接写js就好了如果你还需使用commonjs句需要 .cjs
{
"type": "module"
}
12、ES Modules in Nodejs + Babel
实际上就是配置一些插件,我们先来梳理一些 项目结构 babel是基于插件实现的,现在我们就来配置一些他们

配置好依赖项
"@babel/core": "^7.6.0",
"@babel/node": "^7.6.1",
"@babel/plugin-transform-modules-commonjs": "^7.6.0".babelrc
{
"plugins": [
"@babel/plugin-transform-modules-commonjs"
]
}
.如何运行它?
yarn babel-node xxxx.js