模块化开发是当下最重要的前端开发范式之一
模块化演变过程
-
Stage1 文件划分方式
具体的做法就是每个功能及其相关状态数据各自单独放到不同的文件中,约定每个文件就是一个独立的模块,使用某个模块就是将这个模块引入到页面中,然后直接调用模块中的成员(变量/函数)
缺点也就十分明显了:- 污染了全局作用域
- 命名冲突问题
-
无法管理模块依赖关系
-
Stage2 命名空间方式
每个模块只暴露一个全局对象,所有的模块成员都挂载到这个对象中,具体的做法就是在第一阶段基础之上,通过将每个模块包裹为一个全局对象的形式实现,有点类似于为模块内的成员添加了命名空间的感觉
通过【命名空间】这一概念减少了命名冲突的可能,但是同样的,没有私有空间,所有的模块成员都可以在模块外部被访问或者是被修改,而且没有办法管理模块之间的依赖关系
-
Stage3 IIFE 立即执行函数表达式
将每个模块成员都放在一个函数提供的私有作用域中,对于需要暴露给外部的成员,通过挂在到全局对象上的方式来实现,有了私有成员的概念,私有成员只能在模块成员内部通过闭包的形式访问
- Stage4 模块化演变
利用IIFE参数作用依赖声明使用,具体做法就是在第三阶段的基础上,利用立即执行函数的参数传递模块依赖项,使得每一个模块之间的关系变得更加明显
以上就是早期在没有工具和规范的情况下,对模块化的落地方式
模块化规范的出现
需要的内容就是:
模块化标准+模块加载器
CommonJS规范(node.js中的规范)
- 一个文件就是一个模块
- 每个模块都有单独的作用域
- 通过module.exports导出成员
- 通过require函数载入模块
CommonJS是以同步模式加载模块
在浏览器中必然会导致效率低下
AMD(Asynchronous Module Definition)
异步模块定义规范
require.js
require.js实现了AMD规范,本身也是很强大的模块加载器
目前绝大多数第三方库都支持AMD规范
- AMD使用起来相对复杂
- 模块JS文件请求频繁,效率低下
Sea.js+CMD
这些以前的知识在目前来看也是很重要的一环
模块化标准规范(模块化的最佳实践)
- 在node环境当中,会采用CommonJS规范
- 在浏览器环境中,会采用一个叫做ES Modules规范
现如今绝大多数浏览器都已经支持ES Modules,故而ES Modules的学习成为了重中之重
ES Modules
- 通过script 添加type = module 的属性,就可以以ES Module的标砖执行其中的JS代码
<script type="module">
console.log("this is ES modules")
</script>
- ESM 会自动采用严格模式,忽略use strict
(在非严格模式下,this指向的是window对象)
<script type="module">
console.log(this)
</script>
- 每个ES Module 都是运行在单独的私有作用域当中(第二个打印的foo就会报错undefined)
<script type="module">
var foo = 100
console.log(foo)
</script>
<script type="module">
console.log(foo)
</script>
- 在ESM中是通过CORS的方式请求外部JS模块的
- ESM 的script标签会延迟执行脚本
(延迟加载脚本,先渲染元素到页面上,一般的script标签就会等到脚本加载完成才会渲染元素)
这个小特点与script标签的defer属性是一样的
<script type= "module" src="demo.js"></script>
<p>需要显示的内容</p>
ES Modules导入和导出
- 可以导出变量,函数,类等等
export var name = 'foo module'
export function hello(){
console.log("foo hello")
}
export class Person{
}
- 也可以统一导出,比如:
export { name , hello , Person}
- 在另一个模块js文件要导入
import { hello, name } from './module.js'
console.log(name)
hello()
重命名
var name = 'foo module'
function hello(){
console.log("foo hello")
}
class Person{
}
export {
name as fooName,
hello as fooHello,
Person as fooPerson
}
重命名之后导入时也要注意名字变化
import { fooHello, fooName } from './module.js'
console.log(fooName)
fooHello()
重命名特殊情况
将导出成员名称设置为default,这个成员就会被设置为当前模块的默认导出成员,在导入的时候就必须要进行重命名
export {
name as default,
hello as fooHello,
Person as fooPerson
}
重命名default才能调用
import { fooHello, default as fooName } from './module.js'
ESM 关于针对default的特殊处理
将name变量设置为默认导出
export default name;
在导入的时候可以通过直接import + 变量名的方式接受默认导出的成员,变量名称随意
// fooName这里是可以随意取名的
import fooName from './module.js'
ESM 导入导出的注意事项
- export 后面跟上的花括弧包裹的不是字面量,是固定语法
- 导入时的那些成员是分享的内存空间,是完全相同的引用关系
- 导入的成员是只读的
ESM import用法
- 导入时from关键字后面跟的是字符串,内部的内容路径必须要完整的文件名称,不能省略js后缀名,跟CommonJS完全相反
- 也可以使用完整的url加载模块,也就是说可以使用CDN上面的模块,完整的
- 如果说只执行某个模块的功能,不去提取模块中的成员的话,可以保持花括弧为空,或者直接import跟上字符串,这个特性在我们导入一些不需要外界控制的子功能模块时就非常有用了
import {} from './module.js'
import './module.js'
- 需要导出的成员特别多,导入时都会用到他们,就可以用*全部提取出来,使用as关键字全部存在对象里面
import * as mod from './module.js'
console.log(mod)
- 动态导入
import('./module.js').then(function (module) {
console.log(module)
})
- 默认成员和明明成员同时导出
var name = 'jack'
var age = 18
export { name, age }
console.log('module action')
export default 'default export'
import abc, { name, age } from './module.js'
console.log(name, age, abc)
ESM 直接导出导入成员
- 具体的做法就是将import关键词修改为export,所有的导入成员会作为当前模块的导出成员,在当前作用域下,也就不再可以访问这些成员了。
一般用于index文件,把散落的模块通过这种方式组织到一起,导出,方便外部使用
avatar.js:
export var Avatar = 'Avatar Component'
button.js:
var Button = 'Button Component'
export default Button
index.js:
export { default as Button } from './button.js'
export { Avatar } from './avatar.js'
app.js(导入):
import { Button, Avatar } from './components/index.js'
console.log(Button)
console.log(Avatar)
avatar和button都是暴露了组件,index.js则是将这两个组件导入,并且导出,作为一个桥梁的作用
ESM in Browser(Ployfill兼容方案)
- 让浏览器支持ESM 的绝大特性
- 模块名字为Browser ESM Loader
针对NPM下的模块可以通过upkg这个网站的CDN服务来拿到所有的JS文件
https://unpkg.com/ + npm下的模块名
https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js
/dist/表示目录下的文件
- 引入IE所需要的promise,ployfill
<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>
- nomodule属性
解决了支持polyfill的浏览器不去加载标签内资源的问题
ESM in Node.js
- 在Node当中直接使用ESM ,要做的有:
-
第一,将文件的扩展名由 .js 改为 .mjs;
-
第二,启动时需要额外添加
--experimental-modules
参数;
-
- 也可以用ESM 载入原生模块
// // 此时我们也可以通过 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加载
(比如第三方模块lodash)
import _ from 'lodash'
_.camelCase('ES Module')
- 但是不能使用ESM的花括弧方式去载入第三方模块的成员
// // 不支持,因为第三方模块都是导出默认成员
import { camelCase } from 'lodash'
console.log(camelCase('ES Module'))
ESM in Node.js 与 CommonJS模块的交互
- CommonJS模块始终只会导出一个默认成员
- ESM 中是可以导入CommonJS模块的
- 不能直接提取成员,import不是解构导出对象
-
在CommonJS中通过require载入ESM 也是不可以的
ESM in Node.js与CommonJS的差异
在这之前先推荐使用nodemon工具,可以监听mjs文件的变化并且给出错误信息
先用npm 进行全局安装,再使用
- ESM中没有模块全局成员了
- 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)
Node的新版本更加支持ESM了
- 在新版本中的package.json添加type属性表示module,所有的JS文件就可以默认以ESM支持了
- 如果需要在 type=module 的情况下继续使用 CommonJS, 需要将文件扩展名修改为 .cjs
Babel兼容方案
- 早期的node版本,可以使用Babel进行ESM的兼容
- 主流的JavaScript编译器,可以将新特性的代码编译成当前环境支持的代码
需要安装babel一系列依赖
yarn add @babel/node @babel/core @babel/core @babel/preset-env --dev
-
检测babel命令:
- 安装插件
yarn add @babel/plugin-transform-commonjs --dev
- 建立一个.babelrc文件
{
"plugins": [
"@babel/plugin-transform-modules-commonjs"
]
}
- 运行文件
yarn babel-node .\index.js