一、模块化概述
- 模块化开发是当下最重要的前端开发范式之一。
- 随着前端应用的日益复杂,我们的项目代码已经逐渐膨胀到了不得不花大量时间去管理的程度了。
- 模块化就是一种最主流的代码组织方式,它通过把我们的复杂代码按照功能的不同,划分为不同的模块单独维护的这种方式,去提高我们的开发效率,降低维护成本。
- 模块化只是思想,不包含具体实现。
二、模块化演变过程
Stage1 - 文件划分方式
具体做法:
将每个功能以及它相关的一些状态数据,单独存放到不同的文件当中,我们去约定每一个文件就是一个独立的模块。我们去使用这个模块,就是将这个模块引入到页面当中,然后直接调用模块中的成员(变量 / 函数)。一个script标签就对应一个模块,所有模块都在全局范围内工作。
缺点:
- 污染全局作用域
- 命名冲突问题
- 无法管理模块依赖关系
早期模块化完全依靠约定。
Stage2 - 命名空间方式
具体做法:
我们约定每个模块只暴露一个全局的对象,我们所有的模块成员都挂载到这个全局对象下面。
在第一阶段的基础上,通过将每个模块「包裹」为一个全局对象的形式实现,有点类似于为模块内的成员添加了「命名空间」的感觉。
缺点:
- 没有私有空间
- 模块成员仍然可以在外部被访问/修改
- 无法管理模块依赖关系
Stage3 - IIFE
具体做法:
使用立即执行函数的方式,去为我们的模块提供私有空间。将模块中每个成员都放在一个函数提供的私有作用域当中,对于需要暴露给外部的成员,我们可以通过挂载到全局对象上的这种方式去实现。确保了私有成员的安全。
有了私有成员的概念,私有成员只能在模块成员内通过闭包的形式访问。
Stage4 - 利用 IIFE 参数作为依赖声明使用
具体做法:
在第三阶段的基础上,利用立即执行函数的参数传递模块依赖项。
这使得每一个模块之间的关系变得更加明显。
以上四个阶段就是早期在没有工具和规范的情况下,通过约定的方式,对模块化的落地方式。
三、模块化规范
我们需要的是模块化标准+模块加载器。
CommonJS规范(nodeJS):
- 一个文件就是一个模块
- 每个模块都有单独的作用域
- 通过module.exports导出成员
- 通过require函数载入模块
CommonJS约定的是以同步模式加载模块,在浏览器端使用会导致效率低下。
AMD:(异步的模块定义规范)
Require.js实现了这个规范。
目前绝大多数的第三方库都支持AMD规范。
- AMD使用起来相对复杂
- 模块JS文件请求频繁
Sea.js+CMD
类似CommonJS规范,使用上跟Require.js差不多。
模块化标准规范
模块化的最佳实践:
ES Modules
通过给script添加type=module的属性,就可以以ES Module 的标准执行其中的JS代码。
<script type="module">
console.log('This is es module.'); //This is es module.
</script>
ES Modules基本特性
- ESM 自动采用严格模式,忽略 ‘use strict’
<script> console.log(this); //Window </script> <script type="module"> console.log(this); //undefined </script>
- 每个 ES Module 都是运行在单独的私有作用域中
<script type="module"> var bar = 88 console.log(bar); //88 </script> <script type="module"> console.log(bar); //Uncaught ReferenceError: bar is not defined </script>
- ESM 是通过 CORS(服务端必须支持) 的方式请求外部 JS 模块的
<script type="module" src="https://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script> <!-- Access to script at 'https://libs.baidu.com/jquery/2.0.0/jquery.min.js' from origin 'http://127.0.0.1:5500' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. --> <script type="module" src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script> <!-- 成功请求到 -->
- ESM 的 script 标签会自动延迟执行脚本 (类似defer属性,等待网页渲染完成,再去执行脚本,这样不会阻塞页面元素的显示)
<script type="module" src="demo.js"></script> <p>hello world</p>
ES Modules导入和导出
//导出
const bar = 'hello'
export {
bar}
//导入
import {
foo} from './module.js'
console.log(foo)
注意:
- 导出的成员并不是一个字面量对象;导入的时候也不是解构,是固定语法
- 导出时,并不是导出的成员的值,只是导出的成员的存放地址
- 外部导入的成员是只读的,不可以修改
ES Modules导入的注意事项:
- 导入模块路径(路径必须写完整,不能省略后缀名、相对路径/不可省略、可以使用绝对路径或者完整的URL)
import { name, age } from './module.js'
- 执行模块,并不提取其中的成员
import { } form './module.js' import './module.js' //简写法
- 导入成员多
import * as obj from './module.js' console.log(obj.name);
- 动态导入模块
import('./module.js').then(module => { console.log(module); })
- 同时导入默认成员和命名成员
//导出默认成员和命名成员 export { name, age } export default 'default export' //导入默认成员和命名成员 import { name, age, default as title } from './module.js' //简写 import title, { name, age } from './module.js'
ES Modules导出导入成员: