模块化
模块化是为了解决在大型、复杂的应用程序中代码组织、维护和重用的问题。
模块化标准
JavaScript 历史上一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,这对开发大型的、复杂的项目形成了巨大障碍。
早先,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。
CommonJS: CommonJS 提出了模块的定义、导出和导入规范,Node.js 就是基于 CommonJS 模块系统构建的。
AMD: AMD 是为了解决在浏览器端异步加载模块的问题而产生的标准,随着 ES Module 的问世,已逐渐式微。RequireJS 是一个实现 AMD 的著名库。
ESM: ESM(ECMAScript Module)是 es6 引入的官方的模块系统。是项目开发中最为常用的模块化标准
CommonJS
CommonJS 是非常流行的 JS 模块化标准,这得益于 Node 宿主环境,Node 应用采用 CommonJS 模块规范。
CommonJS 规范每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。
导出(module)
每个模块,都有一个module对象,代表当前模块。它有以下属性
module.id模块的识别符,通常是带有绝对路径的模块文件名。module.filename模块的文件名,带有绝对路径。module.loaded返回一个布尔值,表示模块是否已经完成加载。module.parent返回一个对象,表示调用该模块的模块。module.children返回一个数组,表示该模块要用到的其他模块。module.exports表示模块对外导出的值。
使用 module.exports 来向外导出一个变量、函数或对象
// user.js
const name = 'Tom'
const sayHello = () => console.log('Hello, I am Tom')
module.exports = { name, sayHello }导入(require)
使用 require 加载一个模块
require(): 加载外部模块require.resolve():将模块名解析到一个绝对路径require.main:指向主模块require.cache:指向所有缓存的模块require.extensions:根据文件的后缀名,调用不同的执行函数
const user = require('./user.js')
console.log(user.name)
user.sayHello()模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
ES Module
ESM(ECMAScript Module)是 ECMAScript 在语言层面实现的模块化标准,不同于其它的社区标准
导出(export)
通过 export 关键字导出一个变量(如变量、函数、类等)
// user.js
export const name = '张三'
export const sayHello = () => {
alert('你好,我是张三')
}也可以将他们放在一个对象中在进行导出,两种方式的效果都是一样的,二者都是很常见的导出方式
// user.js
const name = '张三'
const sayHello = () => {
alert('你好,我是张三')
}
export { name, sayHello } // 通过对象导出导入(import)
使用 import 关键字可以加载 export 导出的变量等
import { name, sayhello } from './user.js' // 导入 user.js 中导出的 name 变量重命名导入的模块: 使用 as 关键字可以为导入的变量重新命名,如:
import { name as customName } from './user.js'提升效果: import 导入语句会提升到整个模块的头部,首先执行。
console.log(name) // '张三'
import { name } from './user.js'加载模块: import 会将加载的模块执行一次,所以当只需要执行某个模块时,可以:
import './init.js' // 将 init.js 执行一次整体导入: 导入时,使用 * 指定一个对象,该模块的导出值都会加载在这个对象上
import * as user from './user'
console.log(user.name)
user.sayHello()默认导出
使用 export default 将一个模块进行默认导出,一个模块只能有一个默认导出
// foo.js
export default function () {
console.log('foo模块')
}导入时,需要自定义一个名称接收默认导出的模块
import foo from './foo.js'
foo() // 'foo模块'想使用默认导出的同时,使用其它导出的变量,可以:
import _, { cloneDeep, isEqual } from 'lodash'复合写法
在一个模块中,导出另一个模块的内容,可以简写为:
export { cloneDeep, isEqual } from 'lodash'需要注意的是:cloneDeep和isEqual实际上并没有被导入当前模块,所以当前模块不能直接使用cloneDeep和isEqual。
复合写法的默认导出
// 将 lodash 的默认导出向外导出为 _ 变量
export { default as _ } from 'lodash'
// 当前模块的默认导出为 lodash 的 cloneDeep 模块
export { cloneDeep as default } from 'lodash'动态加载模块
使用 import() 函数动态加载模块,他返回一个 Promise
// 根据条件选择需要加载哪个模块
if (Math.random() > 0.5) {
import('./foo.js')
.then((module) => {
// Load Success
})
.catch((err) => {
// Load Fail
})
}为了代码的清晰,建议使用 async/await 的形式接收 Promise
const module = await import('./foo.js')import.meta
返回当前模块的元信息对象,该对象的各种属性就是当前运行的脚本的元信息。具体包含哪些属性,标准没有规定,由各个运行环境自行决定。
import.meta.url: 是一个只读的字符串属性,包含了当前模块文件的 URL 路径
console.log(import.meta.url)Node.js 环境中,import.meta.url返回的总是本地路径,即file:URL协议的字符串,比如file:///d:/Code/foo.mjs。
加载 ES Module
浏览器
使用 <script> 标签引入加载 js 时,默认是同步加载,即遇到 <script> 标签,浏览器就停止渲染,将当前的 js 文件下载完并运行后,再继续渲染。
<script src="./foo.js"></script>此外,还有两种异步加载的方式:defer 和 async,浏览器遇到这两个属性的<script>时,就会下载文件,但不会等待它下载完成和执行,而是继续渲染
<script src="./foo.js" defer></script>
<script src="./foo.js" async></script>defer: 等到整个页面的 DOM 结构完全生成,以及其他脚本执行完成,才会执行
async: 下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染
defer是渲染完再执行,async是下载完就执行。如果有多个defer脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能保证加载顺序的。
加载 ES Module 模块
需要将 <script> 标签添加 type="module" 属性
<script type="module">
import foo from './foo.js'
</script>对于带有type="module"的<script>,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。
也可以为 ES Module 模块开启 async 属性,这时只要加载完成,渲染引擎就会中断渲染立即执行。执行完成后,再恢复渲染。使用了async属性,模块就不会按照在页面出现的顺序执行
Node.js
CommonJS 模块是 Node.js 专用的模块化标准,与 ES Module 模块不兼容。
从 Node.js v13.2 版本开始,Node.js 开始支持 ESM,它要求 ES Module 模块采用 .mjs 文件后缀名
// foo.mjs
export const name = 'Tom'也可以指定 package.json 中的 type 字段
{
"type": "module"
}设置为
module后,该项目的 JS 脚本,就被解释成 ES Module 模块,而无需设置.mjs文件后缀名。此时,如果要使用 CommonJS 标准,需要将文件后缀名改为
.cjs设置为
commonjs或不设置type字段,该项目的 JS 脚本,就被解释成 CommonJS 模块
