模块化是一种思想
1、模块化的演进过程
初始阶段
首先在早期在没有工具和规范情况下模块化是怎么体现的呢?
- 最早的时候我们以文件的方式来划分模块,一个文件就是一个模块。将每个功能以及其相关的状态数据放到同一个文件当中,然后通过
srcipt
标签引入,一个script
标签对应一个模块。这样实现有几个比较明显缺点:1、没有私有空间的概念,所有变量都以全局的方式声明,会污染全局作用域,在外部也能够随意修改变量的值;2、变量命名比较容易产生冲突;3、模块的依赖关系混乱。 - 后来,为了减少命名冲突,在文件划分的基础之上,将每个模块包裹在一个全局对象中,每个文件为一个对象。但仍然没有私有空间的概念,外部还是可以修改模块中的成员,也无法管理模块之间的依赖关系。
- 再后来,为了让变量有个私有的作用域,开始将所有成员包裹在一个立即执行的函数中,而对于需要在外部访问的成员,将其挂载到全局变量中。函数执行时也可以将依赖的模块作为参数传入,依赖关系更加明显,容易管理。
探索阶段
在前端工程项目日趋壮大,各模块之间的依赖关系也越来越复杂,模块化的标准和规范需求也越来越迫切。开发者在摸索中总结践行自己的模块化的标准和规范,并推出了一些最佳的实践。
-
CommonJs
CommonJs
规范是node
内置的js
代码规范。其特点如下:- 一个文件就是一个模块
- 每个文件都有独立的作用域
- 通过
module.exports(exports ⇔ module.exports)
导出成员 - 通过
require
函数导入模块
// 导出 #! /user/bin/env node const name ='wsq' module.exports = { name, age: 123 } // 导入 const user = require('./a') console.log(user.name);
CommonJs规范是以同步的方式加载模块,node环境下会首先加载所有模块,然后执行时直接使用。不适合浏览器的执行环境。
-
AMD(Asynchronous Module Definition)
AMD
, 异步模块定义规范,require.js
实现了AMD
规范,其特点如下:- 通过
define
函数来定义模块,define
函数接收三个参数,第一个参数为模块名,第二个参数为可选参数,为模块的依赖,第三个参数为一个函数。 - 通过
require
函数加载模块,用法和define
函数类似。require
函数接收两个参数,第一个参数为加载的模块,第二个参数为模块加载之后的执行函数。
// 模块定义 /** * $1: 模块名, $2: 依赖的模块 $3为模块提供私有作用域,通过return将成员导出 * $3 函数的参数和 $2 的依赖项一一对应。 */ define('a', ['jQuery', './c'], function($, angular) { const user = {name: '123'} return { jquery: $, user, } }) // 导入模块 /** * $1 要加载的模块 $2 模块加载之后的执行方法,方法的参数对应加载的模块。 */ require(['./a', 'Angular'], function(a, angular) { })
AMD规范使用相对复杂,而且每次执行require时会生成script标签去请求对应模块,对于同一个模块可能会被多次加载,影响页面性能。
- 通过
-
CMD(Common Module Definition)
CMD
,通用模块定义规范,Sea.js
实现了CMD
规范。其特点如下:- 一个模块是一个文件
- 不应该在模块内引入新的自由变量
- 异步执行
- 通过
define
来定义模块和引入模块,define
接收一个函数作为参数,函数一次接收三个参数,require
,exports
,module
。require
用来导入模块,exports
用来导出模块,module
模块的描述对象。
// math.js define(function(require, exports, module) { exports.add = function() { var sum = 0, i = 0, args = arguments, l = args.length; while (i < l) { sum += args[i++]; } return sum; }; }); // increment.js define(function(require, exports, module) { var add = require('math').add; exports.increment = function(val) { return add(val, 1); }; }); // program.js define(function(require, exports, module) { var inc = require('increment').increment; var a = 1; inc(a); // 2 module.id == "program"; });
模块化方案统一
经过多年的探索和实践,模块化方案逐渐趋于统一,在 Node
中使用 CommonJs
规范, 在浏览器中则使用 ES Module
规范
2、ES Module(ESM)
ESM
是 ES2015
中定义的模块系统,其特性如下:
ESM
默认采用严格模式,忽略‘use strict’
。严格模式下this
为undefined
- 每个
ESM
都具有单独的作用域 ESM
是通过CORS
方式请求外部js
的。CORS
不能通过文件的形式请求,只能通过http
的方式请求ESM
是延迟执行的,相当于给script
标签添加了defer
属性
2.1、ES Module 的使用
-
浏览器中如何使用
ESM
执行js
<script type="module" src="./app.js"> const foo = 123; console.log(foo); </script>
-
模块导入:通过
import
关键字实现模块的导入-
from
后面的文件路径必须包含完整的文件名。可以是./
开头的相对路径、/
开头的绝对路径 或者模块所在位置的完整url
不能省略后缀或者index.js
import { name } from './module.js' import { name } from '/module.js' import { name } from 'http://localhost:3000/module.js' import $ from 'https://unpkg.com/jquery@3.4.1/dist/jquery.min.js'
-
不引用模块中的成员,而是直接加载模块时,通过以下两种方式
import {} from './module.js' import './module.js'
-
导入模块中的所有成员
import * as module from './module.js'
// module.js function hello() { console.log('hello'); } class Person{ constructor(name) { this.name = name } } export default name export { hello, Person } // app.js import * as module from './module.js' console.log(module);
-
import
只能在顶层作用域中使用,不能在if、for
等作用域中使用,如果想要动态导入模块,可以通过import(modulePath)
来导入,import(modulePath)
函数返回一个promise
。const modulePath = './module.js' import(modulePath).then(mod => { console.log(mod); })
-
同时导入命名成员和默认成员,有两种用法。
- 通过
as
对默认导出成员进行重命名import { default as name, Person, hello } from './module.js'
- 在花括号前面先导入默认成员,在导入其他成员
import name, { Person, hello } from './module.js'
- 通过
-
-
模块导出:通过
export
关键字实现模块的导出-
可以通过
export
直接导出变量、函数、类等export var name = 'Json' export function hello() { console.log('hello'); } export class Person { constructor(name) { this.name = name } }
-
也可以通过
export
导出一个对象,将变量作为一个对象的属性导出export { name, hello, Person }
-
导出时重命名,通过
as
来对导出的变量进行重命名,导入时只能使用重命名之后的变量名// 导出 export { name as userName, hello, Person } // 导入 import { name } from './module.js'
-
如果将变量重命名为
default
, 则该变量将作为模块默认导出成员。在import
导入时,直接使用任意名字来存储模块导出的变量。// 导出 export { name as default, hello, Person } // 导入 import personName from './module.js' console.log(personName);
-
通过
export default
变量名 可以将变量作为模块的默认导出来导出。在导入时的用法和4
一致。export default name
-
-
导入导出的注意事项
-
导入导出的写法是固定的语法,而不是对象的解构。主要体现在以下几个方面:
-
不能直接导出一个基本类型的常量
-
在默认导出一个对象字面量时,不能在导入时通过结构获取对象中的属性成员
//导出 export default { name, hello, Person } //导入 import { name } from './module.js' ~~console.log(name);~~
-
-
导入导出的是变量的引用,如果在使用了导入的变量之后修改变量,引用的变量的值将改变
// 导出 var name = 'Josn' setTimeout(function() { name = "Geogy" }, 1000) // 导入 import { name } from './module.js' console.log(name) setTimeout(function() { console.log(name); }, 1100)
-
导入模块时,模块的成员是只读的,不能修改
import { name } from './module.js' console.log(name); name = 'Annay'
-
-
直接导出导入的成员
export { Person, hello } from './module.js'
- 直接导出默认成员时,需要通过
as
进行重命名export { default as name, Person, hello } from './module.js'
-
ESM
浏览器环境polyfill
。通过引入browser-es-module-loader
来兼容不支持ESM
的浏览器环境。- 可以通过
https://unpkg.com/moduleName
来查找要引入的模块的源码文件。<script nomodule type="text/javascript" src="https://unpkg.com/browse/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script> <script nomodule type="text/javascript" src="https://unpkg.com/browse/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>
script
标签的nomodule
属性,在不支持ESM
的浏览器中才会运行其中的代码。可以利用这个属性来兼容对ESM
的支持<script nomodule> alert(1234) </script>
- 可以通过
-
在
node
环境下使用ESM
-
需要将后缀名改为.mjs。
-
使用
node
命令执行.mjs
文件时,需要添加--experimental-modules
参数
-
可以使用
import {} from ‘’
的形式导入内置模块的成员,因为内置模块对ESM
进行了兼容// import fs from 'fs' import { writeFileSync } from 'fs' writeFileSync('.test.txt', 'Hello Node 123')
-
不能使用
import {} from ‘’
的方式导入第三方模块,因为第三方模块导出的是默认成员(主要看node
第三方模块是否添加了ESM
支持)import { camelCase } from 'lodash' console.log(camelCase('ES Module'));
-
CommonJS
和ESM
在Node
环境下的交互ESM
可以导入CommonJs
的模块CommonJs
模块不能 导入ESM
的模块CommonJs
模块只会默认导出
-
ESM
和CommonJs
在 Node 中差异:主要ESM
是对于node
内置模块中的5
个变量不支持// 加载模块的函数 console.log(require); // 模块对象 console.log(module); // 导出对象别名 console.log(exports); // 当前文件的绝对路径 console.log(__filename); // 当前文件所在目录 console.log(__dirname);
在
ESM
中使用相关功能import { fileURLToPath } from 'url' import { dirname } from 'path' // 当前文件的url // console.log(import.meta.url); const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) console.log(__filename); console.log(__dirname);
-
在
node
中设置默认使用ESM
规范执行js
package.json
文件中的type
设置为module
{ "type": "module", "dependencies": { "lodash": "^4.17.20" } }
- 如果要使用
CommonJs
规范,则需要将后缀改为.cjs
-
使用
babel
让node
低版本环境兼容ESM
- 使用
preset-env
转换- 安装插件
@babel/node
@bable/core
@babel/preset-env
- 配置
babel
,在.babelrc
文件中添加配置"presets": ["@babel/preset-env"]
- 运行命令
yarn babel-node index.js
- 安装插件
- 使用插件
@babel/plugin-transform-modules-commonjs
转换- 安装插件
@babel/node
@bable/core
@babel/plugin-transform-modules-commonjs
- 配置
babel
,在.babelrc
文件中添加配置"plugins": ["@babel/plugin-transform-modules-commonjs"]
- 运行命令
yarn babel-node index.js
- 安装插件
- 使用
-