前端模块化-重新梳理前端模块化

前言

本文总结前端模块化相关的发展历程

为什么需要模块化

早期的 JS 没有模块化系统,所有的代码都是通过 script 的方式进行引入的

<script src="lib-a-dependency.js"></script>
<script src="lib-a.js"></script>
<script src="lib-b.js"></script>
<script src="my-script.js"></script>

上述的方式有两个问题:

  1. 命名冲突

由于代码之间会发生大量依赖与交互,如果结构不合理,不可避免地会出现命名冲突、变量污染等问题,比如上面的代码中,如果 lib-a.js 和 lib-b.js 中都定义了一个名为 foo 的变量,那么 my-script.js 中就无法正确访问到 lib-a.js 中的 foo 变量了,出了 bug 也很难排查了,

  1. 文件依赖

比如 lib-a.js 依赖 lib-a-dependency.js,如果 my-script.js 的开发者忘记引入 lib-a-dependency.js,那么就会出现错误,而且这种错误很难排查,因为浏览器不会报错,只会提示 foo is not defined,这种问题在项目很大的时候尤其严重,因为很难保证每个开发者都能记得引入所有的依赖。

长此以往代码就会变得逐渐变为一坨大家都不愿意维护的“屎山”。

命名空间/闭包/IIFE

为了解决上述问题,早期的前端开发者们发明了一种利用函数作用域的模式叫做 IIFE(Immediately Invoked Function Expression)的模式,如果你仔细观察 webpack 的打包结果,你会发现 webpack 会把所有的模块都包裹在一个 IIFE 中

lib-a.js

(function (root) {
  var foo = "bar";

  var libA = {
    foo: foo,
  };

  window.libA = libA;
})();

lib-a.js 文件被引入之后,会立即执行,执行的时候会创建一个函数作用域,这个函数作用域中的变量不会污染全局作用域,同时 lib-a.js 中的变量也不会被其他文件访问到,这样就解决了命名冲突的问题。

业务代码 my-script.js

(function (root) {
  var a = libA.foo; // "bar"
  alert(a);
})();

但这种方案并没有解决文件依赖的问题,且上述只是我们简化的场景,实际开发的时候会有更多奇奇怪怪的问题,随着前端日益复杂化,模块化的解决方案迫在眉睫。

模块化规范的要素

在介绍 JS 中各种模块化规范之前,我们先来看看一个模块化规范需要具备哪些要素。

在模块系统中,通常会涉及到以下三个概念:模块引用、模块定义和模块标识。

  • 模块定义: 模块定义指的是如何定义一个模块。在不同的模块系统中,定义模块的方式可能会有所不同。
  • 模块引用: 模块引用指的是在一个模块中引用另一个模块的方式。在不同的模块系统中,模块引用的方式可能会有所不同。
  • 模块标识 模块标识指的是一个字符串,用于表示模块的唯一标识符。在不同的模块系统中,模块标识的格式可能会有所不同。

以我们熟悉的 CommonJS 规范为例:

  1. 模块定义: 在 CommonJS 模块系统中,使用 module.exports 对象来导出模块。例如:
// lib-a.js
var foo = "foo";
module.exports = {
  foo,
};

2.模块引用: 在 CommonJS 模块系统中,使用 require() 函数来引入一个模块。例如:

const libA = require("./lib-a");

console.log(libA.foo); // "foo"
  1. 模块标识

在 CommonJS 模块系统中,模块标识可以是相对路径或绝对路径。例如:

const myModule = require("myModule");

同时需要注意的是,每种模块化规范社区都有不少的实现,比如 CommonJS 有 Node.js,AMD 有 RequireJS,ES Module 有 webpack 等等,这些实现之间也有一些差异,但是他们都遵循了各自的模块化规范。

模块化的最简单实现

// https://www.zhihu.com/question/21157540/answer/194952896

function require(path) {
  if (require.cache[path]) {
    return require.cache[path].exports;
  }
  var src = fs.readFileSync(path);
  var code = new Function("exports, module", src);
  var module = { exports: {} };
  code(module.exports, module);
  require.cache[path] = module;
  return module.exports;
}
require.cache = Object.create(null);

模块化中通用的原则与知识点

script 执行与 onload 的时机

当浏览器解析 DOM 时候,遇到 script 标签时,会暂停 DOM 的解析,先加载并执行 script 中的代码,然后再继续 DOM 的解析。

比如

<script>
  window.env = {
    version: "production",
  };
</script>

上面的代码的处理顺序如下:

  1. 浏览器解析到 script 标签时,会暂停 DOM 的解析
  2. 加载并执行 script 中的代码
  3. 执行完毕后,触发 script 的 load 事件
  4. 继续 DOM 的解析

这样会导致页面的加载和渲染被阻塞,影响用户体验。

async 和 defer 是用于在 HTML 中加载外部 JavaScript 脚本的两个属性,它们可以帮助优化页面加载和脚本执行的顺序,从而提高页面的性能。

匿名模块与 currentScript

define 模块的时候,如果没有指定模块的 id,那么就会使用当前执行的 script 的 src作为模块的 id,这样就可以保证每个模块的 id 都是唯一的。

问题是如何知道当前执行的 script 的 src 是什么呢?这里就要用到 document.currentScript 属性了,这个属性返回的是当前正在解析的 script 元素,也就是说,只要在 define 模块的时候,获取到当前正在解析的 script 元素,就可以获取到当前页面的 url 了。

<!-- index.html -->
<script src="./script.js"></script>
// script.js
var currentScript = document.currentScript;
alert("当前脚本的 src 属性:" + currentScript.src);

输出结果为:

 当前脚本的 src 属性: 当前脚本的 src 属性:http://127.0.0.1:5502/docs/Webpack/_demo/_common/currentScript/script.js

完整的 demo 请看 👉 在线效果预览, 查看示例代码请点击此处

此外, 还有很多依赖分析与构建, 循环依赖等问题需要处理

本文首发于个人 Github前端开发笔记,由于笔者能力有限,文章难免有疏漏之处,欢迎指正

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值