模块化
将一个项目划分为不同的功能模块来开发维护的过程就是模块化。模块也叫组件。
模块化演变过程
模块化最早的时候是靠约定的方式将模块变量等暴露在全局来实现的,但是这种方式缺点很明显:
- 污染全局作用域
- 命名冲突问题
- 无法管理模块依赖关系
后来就诞生了命名空间的方式来实现模块化,具体操作就是将模块制作成一个全局对象,来供全局使用。但是这种方式只是解决了命名冲突问题,模块内部还是会被外部改变,并且无法管理模块依赖关系。
后来为了解决上面的问题,大家想到可以用立即执行函数(IIFE)来封锁作用域,来为模块提供私有空间,自执行的参数作为模块依赖的声明。对需要暴露在全局的成员,用”window.模块=成员”的方式来提供给全局调用。
但是这种方式并不好维护。模块没有一个统一的标准,不能自动加载(删除或增加一个模块都需要修改html文件中的引用)。
模块化规范
CommonJS规范
- 一个文件就是一个模块
- 每个模块都有单独的作用域
- 通过module.exports导出成员
- 通过require函数载入模块
CommonJS是以同步模式加载模块的,同步方式在服务器端问题不大,但是在浏览器上就会造成效率低下,网页加载慢,所以这个规范并不适用浏览器。
AMD规范
AMD规范,全称Asynchronous Module Definition(异步模块定义)规范。专门为浏览器制定的规范。
曾经很火的require.js就是AMD规范的实现。
目前绝大多数第三方库都支持AMD规范。但是,
- AMD使用起来相对复杂
- 模块JS文件请求频繁
taobao团队曾推出Sea.js+CMD的模块化实现方案,类似CommonJS的使用,后面被require.js吸收。
ES Moudules规范
ES Moudules规范是模块化标准规范,是浏览器端模块化的最佳实践,2014年才推出来的规范。开始浏览器对其兼容性并不好,随着各种打包工具的流行,各大浏览器也基本实现了对其的支持。所以,我们主要研究该模块规范的用法。
ES Moudules
用法
通过给script
标签添加type="module"
的属性,就可以以ES Moudule 的标准执行其中的JS代码了。
<script type="module">
</script>
复制代码
基本特性
- 自动采用严格模式,忽略
use strict
。比如ES Module中的this默认为undefined,而不是window。
<script type="module">
console.log(this)// 输出undefined
</script>
复制代码
- 每个ES Module都是运行在单独的私有作用域中。不会出现变量污染
- 通过CORS请求外部js模块。所以要求外部的服务器支持CORS跨域请求,也必须要求使用http serve 的方式来请求js文件
- ES Module的
script
标签会延迟执行脚本。会在网页渲染完成之后再执行脚本内容,这样就不会阻塞网页加载
export模块导出
// ./modules.js
const foo = 'es modules'
export { foo }
// as 重命名
// export { foo as fooName }
// export { foo as default } //作为默认成员导出
// export default foo //作为默认成员导出
复制代码
import模块导入
// ./app.js
import { foo } from './modules.js'
// 导入默认成员进行重命名
// import { default as fooName } from './modules.js'
// 简写 import fooName from './modules.js'
console.log(foo)
复制代码
import { 成员名 } from ‘模块的路径’,
- 如果是相对路径,
./
不能省略 - 绝对路径就要从网站根目录开始写起,如
/04-import/module.js
- 如果是字母开头则代表第三方模块,只能import默认成员
- 也可以是通过URL的方式访问,如
http://localhost:3000/04-import/module.js
如果成员名省略,则代表只加载模块,而不进行成员引用:
import {} from './modules.js'
// 简写方式
import './modules.js'
复制代码
如果导入成员很多,可以直接用*
代表所有成员名,而不用一个个写:
import * from './modules.js'
// 一般是用重命名的方式,将导入的所有成员存至对象中
import * as mod from './modules.js'
复制代码
不能用变量代替路径,也不能将import命令写入if等语句中,这时要实现动态导入模块,需要用到import()函数,返回值是一个promise:
import('./modules.js').then(function (module) {
console.log(module)// module对象就代表module.js的所有成员
})
复制代码
如果导入模块中同时存在命名成员和默认成员,
import { name, age, default as title } from './module.js'
// 简写方式
import title, { name, age } from './module.js'
复制代码
导入导出注意事项
export { foo, name }
并不是导出字面量对象,实际只是一种固定语法,同理,
import { foo, name }
也不是解构获取字面量对象的成员。- export 导出的成员只是导出成员的引用,并非值。
- import 导入进来的成员引用是
const
常量,无法被更改。
将导入的成员再次导出
有的时候我们需要将导入的成员再次导出,
import { name, age } from './module.js'
export { name, age }
// 简写方式
export { name, age } from './module.js'
// 对默认成员的导入再导出,需要重命名默认成员,否则会作为本次导出的默认成员
export { default as Button } from './button.js'
复制代码
Polyfill解决浏览器环境兼容问题
虽然现在大部分浏览器实现了对ES Modules的支持,但仍有IE和部分国内浏览器不兼容。所以我们要考虑兼容问题。
在此,推荐使用Polyfill来解决ES Modules的兼容问题,在github上的地址是:
github.com/ModuleLoade…
用法是将相关文件进行引用,即可实现ES Modules在绝大部分浏览器的兼容:
# 解决promise兼容性问题,nomodule表示只在不支持ES Modules的浏览器上运行
<script nomodule src="https://unpkg.com/promise-polyfill@8.1.3/dist/polyfill.min.js"></script>
# 完成ES6转换成ES5
<script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>
# 读取ES Modules相关代码
<script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>
复制代码
以上代码只适合在开发阶段使用,在生产环境需要将代码进行编译让浏览器可以直接使用,否则会影响效率。
ES Modules 在 Node.js 中与 CommonJS 的差异
在node.js的8.5以上版本开始了对ES Modules的支持,但目前也仍是过渡阶段,属于实验特性,建议不要在生产环境中使用。
不过我们也可以对其进行测试,两个要求:
- node.js 版本需在8.5以上
- .js模块后缀名需改为.mjs
煮个栗子,创建modules.mjs和app.mjs两个文件,modules.mjs文件内容为:
var foo = "Es Modules Test"
export { foo }
复制代码
app.mjs内容为:
import { foo } from './modules.mjs'
console.log(foo)
复制代码
在mjs文件根目录运行cmd命令行,输入
node --experimental-modules app.mjs
复制代码
ES Module中可以导入ConmmonJS模块:
import mod from './commonjs.js'
console.log(mod)
复制代码
但是在node.js中,ConmmonJS模块无法通过require调用ES Module:
const mod = require('./es-module.mjs')// 会报错
console.log(mod)
复制代码
ConmmonJS模块导出只会导出一个默认成员。
ES Modules 在 Node.js 最新版本中的支持特性
在Node.js 最新版本中只要在package.json中写入
{
"type":"module"
}
复制代码
即可以ES Modules模式识别js文件,但ConmmonJS文件后缀名要改成.cjs,不然不会作为ConmmonJS文件加载。
ES Modules 在 Node.js 中的babel兼容方案
在低版本node.js中,babel是最常用的ES Modules兼容性解决方案。
由于babel是通过插件实现新特性的转换,一个插件对应转换一个特性,实际开发中,我们一个个安装对应的特性转换插件是效率很低的。
推荐安装preset-env插件,它包含所有JS新特性的转换。安装命令如下:
# 首先安装babel依赖
yarn add @babel/node @babel/core @babel/preset-env --dev
# 再使用命令运行包含ES modules语法的 index.js
yarn babel-node index.js --presets=@babel/preset-env
复制代码
在安装babel依赖后,也可以再设置一个.babelrc的配置文件,内容如下:
{
"presets":["@babel/preset-env"]
}
复制代码
PS: presets是插件集,需要用哪个插件就填哪个插件
然后就可以直接使用 babel-node 运行 ES Modules 的 JS 文件,而不需要添加参数。
yarn babel-node index.js
复制代码
假如我们全然知道就几个特性需要转换,那我们使用单个的插件可能更快。比如ES Modules的转换模块是plugin-transform-modules-commonjs,安装命令如下:
yarn add @babel/plugin-transform-modules-commonjs --dev
复制代码
修改.babelrc配置文件:
{
"plugins":["@babel/plugin-transform-modules-commonjs"]
}
复制代码
直接使用 babel-node 运行 ES Modules 的 JS 文件:
yarn babel-node index.js
复制代码