1-4、JS 模块化详解

背景

JS 最开始的定位为:简单的页面设计 - 简单动画 + 基本的表格提交(1995 年网景耗时 2 周开发出来的)
并无模块化或命名空间的概念
后面前端发展越来越复杂,就对 JS 提出了“模块”的要求

模块化的阶段

幼年期:无模块化

通过多个 JS 文件来处理,写多个<script src="xxx"/> 来强行分隔

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<head>
<title>xxx</title>
</head>
<body>
<script src="jquery.js"></script>
<script src="tool.js"></script>
<script src="main.js"></script>
</body>
</html>

存在的问题:

  • 污染全局作用域
    • 因为都在同一个 HTML 里面引入,将污染全局作用域,存在变量名冲突等问题
    • 不利于大型项目开发与多人团队共建

成长期:雏形-IIFE

IIFE:立即执行函数(语法侧优化),是模块化的基石
优势:作用域的封装,因为是函数所以有自己的作用域

1
2
3
(function(_w){
console.log(_w) // 打印出来的是:传入的 Window 对象
})(window)

使用 IIFE 实现一个简单的模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const module = (function(){
let count = 0

// 主流程功能
const add = () => count++
const reset = () => count = 0
const get = () => count

// 对外暴露接口
const returnApi = {
add,
reset,
get
}

return returnApi
})()

// module 为 {add: ƒ, reset: ƒ, get: ƒ}

成熟期:模块化爆发

CJS - CommonJS

来自于服务端(Nodejs)定义的模块化加载和导出规范(同步加载)

1
2
3
4
5
6
7
8
9
10
// 导出 exports
// tool.js
exports.add = (num) => num + 1
exports.reset = () => 0

// 加载 require
// main.js
const tool = require('./tool.js')
tool.add(5)
tool.reset()

其中通过require导入,通过exportsmodule.exports导出
其中的关系为:
require引入的是module.exports导出的
正常情况下exports === module.exports // true
异常情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 导出 exports
// tool.js
exports.add = (num) => num + 1
exports.reset = () => 0
module.exports = {
log(num){
console.log(num)
}
}

// 加载 require
// main.js
const tool = require('./tool.js')
tool.add(5) // 报错
tool.reset() // 报错
tool.log(77) // 正常

优点:

  • 同步加载,易于理解代码执行

缺点:

  • 同步加载大量文件时,会阻塞
  • 因为是同步加载,所以无法按需异步加载
  • 没有语言层面的支持: CommonJS 不是 JavaScript 的语言层面的特性,而是一种规范。相比之下,ES6 模块是语言层面的特性,得到了更好的集成和支持。

AMD - Asynchronous Module Definition(异步模块定义)

针对浏览器端的模块加载规范,支持异步加载,不阻塞页面渲染
著名的框架为 require.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 定义模块
define('moduleA',['dep1', 'dep2'], function(dep1, dep2){
return {
log(){
dep1.a++
dep2.b++
console.log(dep1.a)
console.log(dep2.b)
}
}
})

// 使用模块
require(['moduleA'], function(moduleA) {
moduleA.log()
})

优点:

  • 异步加载
  • 提高了模块化开发

缺点:

  • 语法繁琐
  • 需要显示声明依赖

UMD - Universal Module Definition(通用模块定义)

UMD 是兼容多种模式(CJS/AMD 等)的模块加载器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['dependency'], factory);
} else if (typeof exports === 'object') {
// CommonJS
module.exports = factory(require('dependency'));
} else {
// 浏览器全局变量
root.MyModule = factory(root.Dependency);
}
}(this, function (Dependency) {
// 模块的实际定义
function MyModule() {
// ...
}

// ...

return MyModule;
}));

CMD - Common Module Definition(同步模块加载)

强调模块的加载与使用是同时的
著名框架 sea.js

1
2
3
4
5
6
7
8
9
define('module1', (require, exports, module) => {
let $ = require('jquery.js') // 模块加载

// $('.name').style.xxx 模块使用

const getDom = (selector) => $(selector)

module.exports = { getDom } // 模块 API 的暴露
})

优点:

  • 对依赖的加载可控,能达到“按需加载”,依赖就近

缺点:

  • 不支持异步加载

新时代:官方支持

ESM - ES6 module

ES6 提供的模块加载机制

1
2
3
4
5
6
7
8
9
// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;

// main.js
import { add, subtract } from './math';

console.log(add(3, 4)); // 输出 7
console.log(subtract(8, 5)); // 输出 3
  1. 导出: 使用export关键字导出模块的功能。
  2. 导入: 使用import关键字导入其他模块的功能。
  3. 命名空间导入: 使用import * as aliasName from 'module';来导入整个模块的命名空间。
  4. 默认导出: 使用export default来指定一个模块的默认导出,可以使用import moduleName from 'module';进行导入。
  5. 动态导入: 使用import()来动态加载模块,返回一个 Promise

优点:

  • 模块文件有自己的作用域,不会污染全局
  • 默认使用严格模式

缺点:

  • 兼容性,低版本浏览器不一定兼容

模块化的目的

  • 隔离逻辑与作用域
  • 扩展协同的方便度

最终形成万物皆模块,作为前端工程化的基石

Webpack 知识

作用

将应用中的各种资源(js/css/图片/字体等)打包为浏览器可以直接运行静态文件。

打包流程

  1. 入口文件(Entry):从配置的入口文件为起点,进行依赖分析
    1. 遇到不同的资源(js/css/图片/字体等),将通过不同的加载器(loader),进行转换处理
  2. 依赖图:根据依赖关系生成一张依赖图,确保各模块的加载顺序
  3. 代码块(Chunk):根据依赖图生成代码块
  4. 插件(Plugin):处理各种任务。如:代码压缩、文件拷贝等
  5. 输出(Output):根据配置生成到对应目录,生成的目录有:js 文件夹、css 文件夹、img 文件夹、index.html、sourceMap 等

打包模式

默认使用 UMD,可以配置 CJS、AMD 等

一些问题

图片资源,最终打包成的代码是如何引用的?

举例:Vue + Webpack 的项目,模板(template)内的图片引入

1
2
3
4
5
<template>
<div>
<img src="./assets/image.jpg" alt="Image">
</div>
</template>

打包处理逻辑为:1、将该图片打包到输出目录;2、将解析该相对 src 路径为输出目录的图片绝对路径

Vite 知识

作用

基于 ES Module 的构建工具

打包流程

  1. 启动开发服务器: 当你运行 vite 命令时,Vite 会启动一个开发服务器(基于 Koa)。
  2. ES Module 编译: Vite 将项目中的源代码文件(包括 JavaScript、CSS、Vue 文件等)通过 ES Module 编译器进行处理。这个过程中,它不会将所有模块打包成一个或多个文件,而是保持模块的原始结构。
  3. 按需编译: Vite 采用按需编译的策略,只有在用户请求时才会编译和提供相应的模块。这意味着不需要预先编译整个应用,大大提高了启动速度。
  4. 服务端渲染(如果需要): 对于 Vue 项目,Vite 可以支持服务端渲染(SSR),此时会执行服务端渲染的相关逻辑。
  5. 构建: 当需要生成生产环境的构建时,Vite 会使用 Rollup(一个 JavaScript 模块打包器)进行构建。这个构建过程会将模块打包成传统的、优化过的 JavaScript 文件,以适应生产环境的需要。
  6. 输出: 构建完成后,生成的文件将被输出到指定的目录,可以被部署到服务器上供浏览器加载

打包模式

Build 出来的产物为 ES Module

Webpack 与 Vite 的优缺点

|

| Webpack | Vite |
| — | — | — |
| 冷启动 | 慢 | 快(基于 ES Module) |
| 热更新 | 慢 | 快(基于 ES Module) |
| 生态 | 完善 | 较新 |
| 其他 | 配置复杂 | 配置简单 |

其他知识

严格模式

作用:使 JS 更安全,减少不确定性
使用:’use strict’
特点:

  1. 禁止使用未声明的变量: 在严格模式下,如果使用未声明的变量,将抛出 ReferenceError。
  2. this 的值为 undefined: 在严格模式下,如果函数不是作为对象的方法调用,this 的值将为 undefined。

调用栈

使用new Error().stack可以获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function logCaller() {
console.log(new Error().stack);
}

function outer() {
logCaller();
}

outer();

// 打印结果:
// Error
// at logCaller (<anonymous>:2:17)
// at outer (<anonymous>:6:5)
// at <anonymous>:9:1

Rollup

一个 JS 模块打包器,产物为 ESModule。相比于 Webpack 更小巧高效

组件库搭建所需

面试题

script 标签的参数:async、defer

都是用于控制脚本执行时机

  1. 加载行为:
    • async 和 defer 都不会阻塞页面渲染,允许页面继续加载。
  2. 执行时机:
    • async:脚本加载完成后立即执行,与页面加载和其他脚本执行顺序无关。
    • defer:按照它们在页面上出现的顺序执行,但会在文档解析完成后、DOMContentLoaded 事件触发前执行。
  3. 依赖关系:
    • async:适用于相互独立、无依赖关系的脚本。
    • defer:适用于有顺序依赖关系的脚本。

JQuery 源码-依赖处理

IFEE + 传参调配

1
2
3
4
5
6
7
8
(function(window, undefined ) {
// 用一个函数域包起来,就是所谓的沙箱
// 在这里边var定义的变量,属于这个函数域内的局部变量,避免污染全局
// 把当前沙箱需要的外部变量通过函数参数引入进来
// 只要保证参数对内提供的接口的一致性,你还可以随意替换传进来的这个参数
"use strict";
window.jQuery = window.$ = jQuery;
})( window );

一行代码如何兼容 AMD、CJS?

AMD 关键:define
CJS 关键:module.exports

1
2
3
4
(factory => {

})(typeof module === 'objetc' && module.exports && typeof define === 'undefined' ?
cjsFactory : amdFactory)

1-4、JS 模块化详解
https://mrhzq.github.io/职业上一二事/前端面试/前端八股文/1-4、JS 模块化详解/
作者
黄智强
发布于
2024年1月13日
许可协议