文章

什么是前端模块化?

Interview

前端模块化总结

什么是前端模块化?

题目

什么是前端模块化?你知道哪些前端模块化规范?

解题

说到前端模块化,可能最先映入脑海的就是 CommonJS 规范了,或者还有 ES Module,但这种规范解决了什么问题呢?

什么是前端模块化

随着 Web 应用日益复杂,就造成了需要更多的可管理和可重用的代码,于是模块化编程呼之欲出。也就是说,前端模块化解决了代码杂乱无章的问题,以应对越来越复杂的 Web 应用。

前端模块化规范有哪些?

以历史的发展历程来叙述,可以有以下规范:

  • 全局函数式编程
  • 命名空间模式
  • CommonJS
  • AMD
  • CMD
  • UMD
  • ES Module

就来说说这些规范吧

全局函数

顾名思义,通过全局声明变量和函数的方式来管理代码

var module = "moduleData";
function moduleFunction() {
  console.log(module);
}

该规范的缺点:

  • 容易造成命名冲突
  • 函数之间的依赖关系不明确
  • 维护困难,有一定成本

命名空间模式

通过定义全局对象,将所有函数和变量都封装进这个全局对象中

var myNameSpace = {
  moduleData: "moduleData",
  moduleFunction: function () {
    console.log(this.moduleData);
  },
};

该规范的缺点:

  • 虽然解决了命名冲突的问题,但是模块之间的依赖关系仍然不清晰
  • 所有依赖都需要在对象内手动进行管理

CommonJS

通过 require 函数加载模块,module.exports 导出模块

// a.js
module.exports = "Hello Module";
 
// b.js
var a = require("./a");
console.log(a); // 输出 'Hello Module'

在 Node 中,我们经常可以看到 require 和 module.exports(当然,时至今日也可以用 ES Module 来管理代码了),这就是 CommonJS 规范了。

require 函数的作用与实现
  • 作用:根据模块的文件路径读取模块文件,然后执行模块代码,最后返回模块的 exports 对象
  • 实现:
const fs = require("fs");
 
function require(modulePath) {
  // 读取模块代码
  const code = fs.readFileSync(modulePath);
 
  // 包装模块代码
  const wrapper = Function(
    "exports",
    "require",
    "module",
    "_filename",
    "_dirname",
    `${code}\n return module.exports`
  );
  const exports = {};
  const module = { exports };
 
  // 执行模块代码
  wrapper(exports, require, module);
 
  // 返回模块的exports对象
  return module.exports;
}

require 函数在执行模块代码时,会先将模块代码封装进一个函数中,然后调用该函数。

这样做的好处是:可以将模块代码隔离到一个函数作用域中,防止模块内的变量污染全局作用域。

exports 和 module.exports 之间有什么关系呢
module.exports = exports; // 在模块的顶层进行赋值

一旦 module.exports = { },则 exports 上的任何修改都不会影响 module.exports

require 的查找规则

导入格式:require(path)

  • 情况一:

    • 如果 path 是一个核心模块,如 fs、http 等模块
    • 直接返回核心模块,并且停止查找
  • 情况二:

    • 如果 path 是以 ./ 或 ../ 或 /(根目录) 开头的
    • 先当作文件来查找,后当作目录来查找
      • 将 path 当作文件在对应的目录下查找
      • 如果有后缀名,则按照后缀名的格式查找对应的文件
      • 如果没有后缀名,按照以下顺序:
        • 直接查找 path 文件
        • 查找 path.js 文件
        • 查找 path.json 文件
        • 查找 path.node 文件
      • 没有找到对应的文件,将 path 当作目录
        • 查找 path 目录下的 index 文件
        • 查找 path/index.js 文件
        • 查找 path/index.json 文件
        • 查找 path/index.node 文件
    • 如果当作文件和目录都没有找到,则报错:not found
  • 情况三:

    • 直接是一个 path,并且 path 不是一个核心模块时
    • 直接在 node_modules 中去找
    • 如果没找到,则报错:not found
module.exports 的作用
  • 在 CommonJS 中,每个模块都有一个 module 对象,在该对象中有一个 exports 属性用于导出模块
  • 当其他模块通过 require 函数导入一个模块时,其实获取到的就是 module.exports 对象
  • module.exports 的初始值是一个空对象:module.exports = { }

AMD

AMD(Asynchronous Module Definition,由 RequireJS 提出),根据名字就能看出,AMD 规范具有异步加载的特性。

通过 define 函数定义模块

// AMD
define(["dependency"], function () {
  return "module content";
});

优点:

  • 支持异步加载,适合浏览器环境

缺点:

  • 语法较为复杂

CMD

CMD(Common Module Definition),也是一种适用于浏览器的模块化解决方案

特点:

  • 支持异步加载模块
  • 适用于浏览器环境
  • 同时汲取了 CommonJS 的优点

UMD

UMD(Universal Module Definition),是一种前后端跨平台的模块化解决方案

UMD = CommonJS + AMD

实现原理:

  • 先判断是否支持 CommonJS 规范,即 Node 的 exports 对象是否存在,存在则使用
  • 再判断是否支持 AMD 规范,即 define 函数是否存在,存在则使用
  • 最后如果两者都不存在,则将模块暴露到全局中
(function (root, factory) {
  if (typeof define === "function" && define.amd) {
    // AMD
    define(["jquery"], factory);
  } else if (typeof exports === "object") {
    // Node, CommonJS
    module.exports = factory(require("jquery"));
  } else {
    // 暴露到全局中
    root.returnExports = factory(root.jQuery);
  }
})(this, function ($) {
  // 模块代码
});

ES Module

由 ECMA Script 自己推出的模块化解决方案

通过 import 导入模块、export 导出模块

// a.js
export const a = "Hello world";
 
// b.js
import { a } from "./a.js";
console.log(a); // 输出 'Hello world'

特点:

  • 自动开启严格模式
  • 支持异步加载模块
  • 具有静态性,使得模块之间的依赖关系更加清晰
  • 既可以在 Node 环境中使用,也可以在浏览器环境中使用
异步加载模块

异步加载意味着 JavaScript 引擎遇到 import 时会去获取这个文件,但这个获取的过程是异步的,并不会阻塞主线程的继续执行。

也就是说在 script 标签上设置了 type = module 的代码,相当于在 script 标签上加上了 async 属性

<script src="main.js" type="module"></script>
 
<!-- 下面的js文件不会被阻塞执行 -->
<scirpt src="index.js"></scirpt>
静态性

什么是静态性?

指模块在编译阶段进行静态分析和解析,并且模块的依赖关系是在编译时确定的,而不是在运行时。

这意味着在编译时,模块之间的依赖关系是固定的,不会受到运行时条件或动态变化的影响

静态性带来的优势:

  • 静态分析
    • 编译器可以在编译时分析模块的依赖关系,并进行优化和静态检查
  • 提前解析
    • 模块的依赖关系在编译时就已经确定了,因此可以提前解析和加载依赖,减少了运行时的解析和查找时间,提高了性能
  • 静态优化
    • 由于模块的依赖关系是在编译时确定的,如 tree-shaking 等优化技术就可以在编译时进行优化,从而减少最终的代码体积
ES Module 的跨域问题

通过 ES Module 规范引入一个本地 HTML 文件时,控制台会出现 CORS 跨域问题(这是由于 JavaScript 模块的安全性需要)

如何避免这种跨域问题?

  • 开启一个本地服务器
  • 通过 IDE 上的类似 Live Server 的工具进行开发

至此,前端模块化的知识算是梳理并总结完了,撒花~