前端模块化开发-ESModules(笔记)
背景
目前最重要的前端开发范式之一;
早期设计的时候没想到前端开发会如今的规模,所要处理的业务也越来越复杂,传统的开发模式已经不满足越来越多和复杂的前端页面;从而出现了,模块化的思想;
模块化的演变
1、文件划分:最早是通过页面的script标签区分,一个标签就是一个模块,通过全局变量来通信;这种模式的弊端
- 污染全局作用域,变量可以在外部访问和修改;
- 命名容易冲突;
- 无法管理模块间的依赖关系;
- 全靠相互约定来实现,项目规模变大后,容易出问题;
2、命名空间方式:每个模块暴露一个全局对象,模块内的方法通过全局对象传递;
- 只解决了命名冲突问题;
3、函数包裹:利用立即执行函数,包裹内容,将其封装在私有空间内,利用全局对象暴露对外通信方法;
- 保证了内部变量的私有性,通过闭包的方式访问;
- 并且利用函数参数,作为模块儿依赖传入,解决模块间依赖的问题;
模块儿之间的引用靠人工来写很容易出问题和误操作,遗留隐患
模块化规范的出现
需要有模块化的规范和加载使用模块的能力
CommonJS 规范:node提出的标准,在node运行,以同步模式加载模块,但不适合浏览器中使用,早期未选择。
- 一个文件就是一个模块;
- 每个模块都有单独的作用域;
- 通过module.exports导出成员;
- 通过require函数载入模块儿
AMD(Asynchronous Module Definition):规范
Require.js实现了此规范
大多数第三方库都支持AMD,但是1、其使用相对较为复杂,容易和业务代码耦合,2、模块JS文件请求频繁;
CMD(Sea.js) 淘宝退出的规范
类似CommonJS规范,但是使用像AMD,为了降低开发者的学习成本;Require.js也兼容了。
模块化标准
目前比较统一的
在node中,就是CommonJS;
在浏览器中,就是ES Modules;(语言层面统一规范)
ES Modules
1、基本特性
- 自动采用严格模式,忽略"use strict";
- 每个ESM模块都是单独的私有作用域;
- ESM是通过CORS去请求外部JS模块的;
- ESM的script标签会延迟执行脚本,不会阻塞页面显然;
<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
</script>
<script type="module">
console.log(foo) //undefined
</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>
2、导入与导出
导入与导出有几种不同的方式
导出
export const name = 'Lili'
export function hello () {
console.log('hello')
}
// -----------------------------
const name = 'Lili'
function hello () {
console.log('hello')
}
export { name, hello }
// -----------------------------
export {
name as fooname, //利用as重命名
hello as foohello
}
// -----------------------------
export default name
导入
import { name, hello } from './module.js';
// -----------------------------
import { fooname, foohello } from './module.js';
// -----------------------------
import name from './module.js';
注意:
1、export { name, hello } 并不是导出一个对象,而是一种固定的写法结构,
同理import { name, hello } from './module.js';也不是导出了对对象的解构,也是一种固定的写法结构;
不可写为 export { name: age , hello } //报错
而 export default { name: age }是导出一个对象,可以这些写和使用;
2、export是导出变量的引用关系(无论是否对象),不是复制一份,改变原值这里也是改变;
并且,通过import导入的成员是只读的,不可修改;
3、关于导入一些用法
原生的ESModules import导入,后面不能省略,后缀名、不能省略index.js,必须一“./“或者”/“开始,相对和绝对路径,也可以直接使用连接完成的url例如:
import app from http://alcdn.cn/t6/apps/live/liveapp/js/app.js
import { name } from './module.js' //不能省略后缀名
import { lowercase } from './utils/index.js' //不能省略index.js
import { name } from '/04-import/module.js' //不能省略“./“或者”/“
import { name } from 'http://localhost:3000/04-import/module.js' //可以直接使用连接完成的url
// --------------
import {} from './module.js'
import './module.js'
// 只加载模块导入模块,不导入具体变量
// ---------------
import * as mod from './module.js'
console.log(mod)
// 导入全部变量,并且作为mod内的属性
// ---------------
var modulePath = './module.js'
import(modulePath).then(function (module) {
console.log(module)
})
// 当时导入路径为变量时,可通过全局函数 import导入,返回一个异步函数promise,在回调函数中输出导入的值
// ----------------
import { name, age, default as title } from './module.js'
import title, { name, age } from './module.js'
// title为导入的默认值
console.log(name, age, abc)
// 同时导入 其他值和默认值时,可以这样写
补充:
export { foo, bar } from './module.js'
export { default as foo1, bar } from './module.js'
// 直接导出导入的成员,一般可使用在index.js中
4、ESModules 浏览器兼容问题
利用,Polyfill引入脚本,但是实际工作中不推荐使用,效率较差
<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>
5、node中的ESModules
目前是实现阶段,生成中不推荐使用
目前node 8.5.0以上的版本已经支持,使用时要改扩展名为mjs,并且加”–experimental-modules“ 参数;
// 第一,将文件的扩展名由 .js 改为 .mjs;
// 第二,启动时需要额外添加 `--experimental-modules` 参数;
// 我们也可以通过 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'))
6、ESModules 在node中与CommonJS交互
- ESModules中可以导入 CommonJS模块
- 而 CommonJS 中不可以导入ESModulesmok
- CommonJS始终只会导出一个默认成员
- 注意 import 不是解构导出对象
// CommonJS 模块始终只会导出一个默认成员
// module.exports = {
// foo: 'commonjs exports value'
// }
exports.foo = 'commonjs exports value'
两者的一些差异
CommonJS中的一些全局成员
// 加载模块函数
console.log(require)
// 模块对象
console.log(module)
// 导出对象别名
console.log(exports)
// 当前文件的绝对路径
console.log(__filename)
// 当前文件所在目录
console.log(__dirname)
其在ESModules中都不能使用,相对应的提供了这些来替换
// 加载模块函数 ESModules
console.log(require) -> import
// 模块对象
console.log(module) -> export
// 导出对象别名
console.log(exports) -> export
// 当前文件的绝对路径
console.log(__filename)-> import.meta.url
// 当前文件所在目录
console.log(__dirname) -> import.meta.url
// ---------------------------------------
// 通过 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、ESModules 在低版本node中使用Babel兼容
安装,@babel/node、@babel/core、@babel/preset-env
.babelrc babel的配置文件
{
“presets”: ["@babel/preset-env"]
}
编译兼容最新特性
{
“plugins”: [
“@babel/plugin-transform-modules-commonjs”
]
}
单独插件的使用