剥茧抽丝,细数模块化的前世今生

写在前面

本篇是前端工程化打怪升级的第 1 篇,关注专栏 | 小册传送门 | 案例代码

近几年,时常会感叹,前端,发展的太迅猛了。日新月异的新概念,异彩纷呈的新思想泉水般涌出;前端项目的复杂度、开发成本、维护难度也在不断提升,前端三件套开发模式(HTML-CSS-JS)滞后于前端的进步,这要求我们需要以更高层次的眼光来重新审视前端——工程化。工程化是一个工学概念,其核心是结合实际建立科学的、规范的设计和生产流程,目的是降本提效

前端工程化目前还没有比较完善的定义,各种论调众说纷纭,很多朋友很容易把工程化与 webpack 画等号,这其实一种不完善的理解,以我看来其是指借助科学的软件工程方法,更科学、严谨、系统的管理和维护整个前端开发的全流程。又或者理解为比较主流的工程四化:模块化、组件化、规范化、自动化

模块化是前端工程化的核心要素,重中之重,理解模块化是掌握工程化的重要一步。因此本文以早期模块化演变进程为着眼点,带你理解

  • 什么是模块化
  • 为什么需要模块化
  • 早期模块化的演变进程
  • 应用于模块化的依赖注入

模块化

维基百科的定义还是有几分难以理解,咱们用前端的话术修饰一下,模块化其实就是指解决一个复杂问题时自顶向下逐层把系统划分成若干模块的过程,每个模块完成一个特定的子功能(单一职责),模块内部私有,对外暴露接口与其他模块通信,所有的模块按某种方法组装起来,成为一个整体,从而完成整个系统所要求的功能。

还是有些难以理解,举一个现实中的栗子,火箭系统就是比较典型的模块化设计,一般会有三大模块:控制、动力、结构。三个模块各司其职;同时也存在部分的依赖关系,例如控制是核心模块,需要操纵结构模块与动力模块。

rocket-module

那么模块化有什么好处那?从火箭案例可以发现,各自模块都拥有自身的核心业务,擅长结构的从事结构模块研究,精研控制的设计控制程序,各司其职,模块化设计是一种降本提效的好方案。在前端中亦是如此,具体的好处小包暂且不多言,下面一起来经历模块化的演变过程,在此之后,你会有更深刻的理解。

文件划分

在早期刀耕火种的前端三件套时代,HTML 中通过引入到多个不同逻辑的 js 文件,构成了最原始的模块化实现方式——文件划分模式。

// moduleA.js
let moduleName = "moduleA";
// moduleB.js
let getModuleName = () => {
  console.log("This is moduleB!");
};
// entry.js
console.log(moduleName); // moduleA
getModuleName(); // This is moduleB!
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script src="./moduleA.js"></script>
    <script src="./moduleB.js"></script>
    <script src="./entry.js"></script>
  </body>
</html>

上述案例代码中,entry.js 分别使用了 moduleAmoduleB 的变量与函数,实现了简易的代码分离与组织,但随着代码量的增大及项目复杂度的增加,文件划分方式存在很多问题。

  • 命名冲突: moduleAmoduleB 定义在全局,并没有构造私有空间,如果 moduleB 同时定义了 moduleName,两个模块间就会发生变量名的覆盖,引起变量冲突
  • 依赖模糊: 无法清晰的确定模块之间的依赖关系和加载顺序。文件划分方式中被依赖项需要引用在前,也就是说 script 引入顺序仅仅提供了某些 js 文件的前后依赖,并不能确切的反应各模块间的依赖关系。
  • 维护性差: 代码间组织方式混乱,后期维护难度较高
  • 复用性差

文件划分模式的缺点简直可以举一大箩筐,后续模块化的演变正是对缺点的不断优化。

命名空间

命名空间是另一种模块化的实现方案,其目的在于解决命名冲突问题。

命名空间的核心实现在于将变量与函数声明为对象的属性,只要外层对象命名不发生冲突,内部的成员就不会发生覆盖。

// moduleA.js
let moduleA = {
  moduleName: "moduleA",
  getModuleName() {
    return this.moduleName;
  },
};
// moduleB.js
let moduleB = {
  moduleName: "moduleB",
  getModuleName() {
    return this.moduleName;
  },
};
// entry.js
console.log(moduleA.getModuleName()); // moduleA
console.log(moduleB.getModuleName()); // moduleB

命名空间的写法一定程度上减少了命名冲突问题,但其本质写法为对象,并没有构建私有作用域,所有模块的成员可以被外部访问,这违背了模块化的设计理念,同时也无法管理模块间的依赖问题。

// entry.js
moduleA.moduleName = "alterModule";
moduleA.getModuleName(); // alterModule

IIFE

JavaScript 中函数可以生成函数作用域,外部无法访问内部定义的成员,使用闭包思想,可以将内部成员暴露给外部使用。因此可以借助函数+闭包特性实现私有数据和共享方法,由于该函数只是为了辅助模块化的实现,因此采用 IIFE 立即执行函数的模式。

当外部使用 IIFE 构建的模块时,只能通过模块提供的接口进行操作,无法访问私有成员。这种方式成功的解决了命名冲突以及私有空间的问题,同时也是现代模块化规范的思想来源。

// moduleA.js
let moduleA = (function () {
  const _moduleName = "moduleA";
  const getModuleName = function () {
    return _moduleName;
  };
  return { getModuleName };
})();
// moduleB.js
let moduleB = (function () {
  const _moduleName = "moduleB";
  const getModuleName = function () {
    return _moduleName;
  };
  return { getModuleName };
})();
// entry.js
console.log(moduleA.getModuleName()); // moduleA
console.log(moduleB.getModuleName()); // moduleB

moduleA._moduleName = "alterModule";
console.log(moduleA.getModuleName()); // moduleA

新的问题来了,IIFE 方式可以解决依赖问题吗?

这时可以使用引入依赖,即通过 IIFE 的函数参数将依赖传入。

// moduleC.js
let moduleC = (function () {
  const _moduleName = "moduleC";
  const _moduleData = { x: 1 };
  const getModuleData = function () {
    console.log("Module: ", _moduleName, " Data: ", _moduleData.x);
  };
  return { getModuleData };
})();
// moduleA.js
let moduleA = (function (module) {
  const _moduleName = "moduleA";
  const getModuleName = function () {
    console.log(_moduleName);
    console.log(module.getModuleData());
  };
  return { getModuleName };
})(moduleC);
// entry.js

// Module:  moduleC  Data: 1
console.log(moduleA.getModuleName()); // moduleA
console.log(moduleB.getModuleName()); // moduleB

IIFE 方式大家其实都不陌生,jQuery 时期有很多类似的实现,例如 jQuery 源码如何将 $ 挂载到 window 实例上(换个角度可以理解为依赖了 jquery 模块)。

(function (global, factory) {
  "use strict";
  factory(global);
})(typeof window !== "undefined" ? window : this, function (window, noGlobal) {
  function jQuery() {}
  window.jQuery = window.$ = jQuery;
  return jQuery;
});

console.log(window.$); // function jQuery(){}

依赖注入

IIFE 方式之后,后续又出现了多种模块化方案,例如模板定义依赖、注册定义依赖、Sandbox 模式、依赖注入。

依赖注入的作为重要的开发思想,解耦大杀器,目前已经遍布前端世界,vue3、react、angular、nest 等都有使用,因此本文来特地讲解一下。

先来看看相关的几个重要概念:

依赖倒置原则

依赖倒置原则(Dependence Inversion Principle,DIP): 高层模块不应该依赖底层模块,两者都应该依赖其抽象,抽象不能够依赖于具体 ,具体必须依赖于抽象。

micro-module-inject

举个栗子,假设我们是一家微波炉生产商,我们有着成熟的上下游生产链,例如我们与微波炉核心模块供应商建立了合作关系。

import MicroCoreS from "vendor";
class Microwave {
  constructor(brand) {
    this.core = new MicroCoreS();
    this.brand = "zcxiaobao";
  }
}

在我们创业初期,上述的实现完全可以满足我们的需求,但随着时代的发展,codeS 类型核心开始难以满足最新市场,因此核心模块供应商推出了 codeL 类型核心。

这时我们就可以发现,我们的基类 MicrowavecoreS 锁住了,如果想要升级为 coreL,需要全部替换或者重新创建 MicrowaveL 基类,牵一发而动全身,低效高成本。

DIP 就是为了解决这个问题而提出的,我们重新回顾一下 DIP 的定义:

高层模块不应该依赖底层模块,两者都应该依赖其抽象,抽象不能够依赖于具体,具体必须依赖于抽象

因此在这里我们不再直接紧耦合于 core 的具体设计,而是引入基于 core 的抽象设计(接口),通俗来讲即微波炉核心的设计标准

micro-module-inject-dip

接下来不论供应商如何迭代升级,我们只需按照 MicroCore 的标准实现新核心即可。

从上面两张图可以清楚的看到,前者 MicrowavecoreScoreL 是紧耦合关系;后者 Microwave、coreL、coreS 共同依赖于抽象接口 MicroCore,实现了松耦合。

总结来说: 假设有两个类 classA 和 classB,如果 classA 依赖 classB,这便是紧耦合;如果 classA 依赖 interfaceB,而 classB 实现了 interfaceB,这便是依赖倒置。

控制反转

控制反转(Inversion of Control,IOC)是面向对象编程中的一种设计原则,它通过反转调用方和被调用方之间的关系,实现解耦合。

依赖注入

依赖注入(Dependency Injection,DI)是 IOC 最常见的方式,通过将被依赖对象的创建和管理由依赖方转移到外部容器(或者框架),然后将依赖的对象注入到需要使用的地方,实现解耦合。

微波炉的生产过程中,会需要多类组件,例如核心、外壳,各类组件也会依赖于其他组件,例如核心模块还会依赖于电导丝,电导丝还会有多种类型材料。诸如此类的生产关系,就会共同构建成一个复杂的依赖网络(Dependency Network)。

micro-module

const microwave = new Microwave(
  new coreL(new ElectricConductor(new coreMaterial()), ...),
  ...
);

看完例子后,我们重新品味一下依赖注入的定义,可以得出两大核心点: 存储被依赖对象的外部容器,将依赖注入所需地方

微波炉的生产过程类似于依赖注入过程,其全部模块就构成存储被依赖对象的外部容器。然后不同类型的微波炉使用不同的模块进行组装,例如电导丝 A 型与电导丝 B 型微波炉注入不同的电导丝模块。

实现

依赖注入是面向对象中常用的编程思想,因此上文主要以类和接口为切入点,更方便理解,那模块化中的依赖注入又是怎样的那?

module_inject_case

从图中案例我们可以清晰的理清模块化中的依赖注入。

  • 开发静态网站:注入前端传统三件套即可
  • 开发复杂网站: 依赖池全部注入,开发难度尚高,因此可以注册新的 vue 依赖
// 具象化三大模块
function vue() {
  return {
    module: "vue",
    ability: "code module",
  };
}

function vite() {
  return {
    module: "vite",
    ability: "bunde module",
  };
}

function server() {
  return {
    module: "server",
    ability: "data module",
  };
}

假设现在有 vue、vite、server 三个模块,我们试图借助这三个模块开发一些有意思的网站,可以将依赖模块借鉴 IIFE 引入依赖的方式以函数参数传入。

var developWeb = function (vue, vite, server) {
  let v = vue();
  let vi = vite();
  let s = server();
  console.log(v.ability, vi.ability, s.ability);
};

但问题来了,依赖的模块该如何进行管理那,例如后续开发需要依赖新的模块,只能修改函数参数或者构造新的函数,这可是大忌。

这时候,依赖注入闪亮登场,来分析一下实现要点。

  • 可以实现依赖关系的注册(即 IOC 容器来存储所有的依赖)
  • 依赖注入器可以接受一个函数,注入成功后返回一个可以获取所有依赖资源的函数
  • 注入应保持被传递函数的作用域
  • 被传递的函数应该能够接受自定义参数,而不仅仅是依赖描述

下面来简单实现一个依赖注册器 injectorinjector 由依赖容器、注册依赖函数、依赖注入函数三部分组组成。对于 resolve 函数,deps 代表被依赖 key 数组,func 代表需要注入依赖的函数,scope 代表 func 函数作用域。

const injector = {
  dependencies: {}, // 依赖管理中心
  register: function (key, value) {
    // 注册依赖关系
    this.dependencies[key] = value;
  },
  resolve: function (deps, func, scope) {}, // 依赖注入
};

resolve 函数目的在于将 deps 涉及的依赖注入到 func 函数中,实现并不复杂。首先根据 deps 数组将所需的依赖从 dependencies 取出添加到 dependModule 数组中,然后在返回的函数中使用 apply 方法传递 scope 作用域及其他参数。

var injector = {
  dependencies: {},
  register: function (key, value) {
    this.dependencies[key] = value;
  },
  resolve(deps, func, scope) {
    const dependModule = [];
    for (let i = 0; i < deps.length; i++) {
      const d = deps[i];
      // 分析依赖是否存在,收集所需依赖
      if (this.dependencies[d]) {
        dependModule.push(this.dependencies[d]);
      } else {
        throw new Error(d + "依赖不存在");
      }
    }
    return function () {
      // 传递函数作用域
      // 接受其他参数
      func.apply(
        scope,
        dependModule.concat(Array.prototype.slice.call(arguments, 0))
      );
    };
  },
};

来看看使用:

injector.register("vue", vue);
injector.register("vite", vite);

injector.resolve(["vue", "vite"], function (vue, vite) {
  let v = vue();
  let vi = vite();
  console.log(v.ability, vi.ability);
})();

// 传入其他参数
injector.resolve(["vue", "vite"], function (vue, vite, other) {
  let v = vue();
  let vi = vite();
  console.log(v.ability, vi.ability, other);
})("other");

到这里,实现了一个简单的依赖注入,但上述实现并不完美。例如使用时需要重复所需依赖两次,此外由于附加参数的存在,还不能混淆顺序。有兴趣的朋友们可以自己完善一下。

总结

模块化的演变是一个特别有意思的过程,前人们探索出数十种模块化方式,各种方式有其独特特点,其中也有相互借鉴,学习这个过程可以收获良多,也可以更好地体悟当下的模块化规范。

早期的模块化方案并没有完善的解决模块化问题,随着前端工程化的推进,开源社区中陆续出现较为优秀且完善的模块化解决方案,随着 ES6 的出现,JavaScript 官方退出 ESM,标志着模块化正式进入成熟阶段,模块化已然不可或缺。

经过早期一系列的探索,后续又诞生了那些模块化规范那,敬请期待下章《群雄逐鹿,前端模块化路在何方?》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/6027.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Python 自动化指南(繁琐工作自动化)第二版:十五、使用 PDF 和 WORD 文档

原文&#xff1a;https://automatetheboringstuff.com/2e/chapter15/ PDF 和 Word 文档是二进制文件&#xff0c;这使得它们比纯文本文件复杂得多。除了文本&#xff0c;它们还存储大量的字体、颜色和布局信息。如果您想让您的程序读写 PDF 或 Word 文档&#xff0c;您需要做的…

【从零开始学习 UVM】3.5、UVM TestBench架构 —— UVM Sequencer [uvm_sequencer]

文章目录 Usage(用法)Custom Sequencer(自定义sequencer)Class Hierarchy一个 sequencer 生成数据事务作为类对象并将其发送到driver以执行。建议扩展uvm_sequencer基类,因为它包含了允许sequence与driver通信所需的所有功能。基类是由可以被sequencer处理的requset和resp…

CookieSession

目录 一. 回顾cookie 二. 会话机制Session 1. cookie&#xff1a;标识用户的身份信息 2. cookie 和 session 的关联区别 三. 一些常用的核心方法及原理应用 1. HttpServletRequest 类中的相关方法 2. HttpServletResponse 类中的相关方法 3. HttpSession 类中的相关方…

博客2:YOLOv5车牌识别实战教程:理论基础

摘要&#xff1a;本篇博客介绍了YOLOv5车牌识别的理论基础&#xff0c;包括目标检测的概念、YOLO系列的发展历程、YOLOv5的网络结构和损失函数等。通过深入理解YOLOv5的原理&#xff0c;为后续实战应用打下坚实基础。 车牌识别视频正文&#xff1a; 2.1 目标检测概念 目标检测…

【01 Provider HAL and Device HAL】

1. Overview Camera Provider Hal 和 Camera Device Hal3 即在Hal3 整个架构中紫色框框出来的部分中: 2. 简介 (1). Android定义了几个Interface: ICameraProvider, ICameraDevice, ICameraDeviceSession, ICameraDeviceCallback 。 Camera Hal 层去实做了这些 Interface。…

chatGPT陪你读源码

概述 chatGPT从2022年11月份崭露头角以来&#xff0c;一直备受关注。他的人工智能对话颠覆了以往智能对话的刻板印象&#xff0c;跟chatGPT聊天&#xff0c;感觉就像百晓生一样&#xff0c;什么都懂。尤其在编程方面&#xff0c;chatGPT可以根据实际的业务场景需求&#xff0c…

GPT-4原论文详细解读(GPT-4 Technical Report)

GPT-4原论文详细解读&#xff08;GPT-4 Technical Report&#xff09;返回论文和资料目录 1.导读 相比之前的GPT-3.5等大型语言模型&#xff08;这里可以看我的InstructGPT解读&#xff0c;也方便理解本文内容&#xff09;&#xff0c;GPT-4最大的不同在于变成了多模态&#…

部署大数据集群时踩过的坑 (持续更新)

大数据集群踩过的坑 前言(必看) 如果你遇到了和我一样的问题并通过搜索引擎进入这篇文章&#xff0c;请善用CtrlF键搜索 该自检手册仅用于自己学习使用&#xff0c;记录所有自己遇到的问题。如果你没有检索到你的问题&#xff0c;请使用Bing或Google进行搜索 该自检手册严格…

PCB生产工艺流程一:PCB分类的三大要点

PCB生产工艺流程一&#xff1a;PCB分类的三大要点 PCB在材料、层数、制程上的多样化以适不同的电子产品及其特殊需求。因此其种类划分比较多&#xff0c;以下就归纳一些通用的区别办法,来简单介绍PCB的分类以及它的制造工艺。那么我们就从它的三个方面来分析一下吧。 1、材料…

某汽车零部件企业:定期反钓鱼演练是降低企业安全风险的优选方案

客户背景 作为一家主要从事于汽车、摩托车零部件的开发、制造和销售的中日合资企业&#xff0c;服务的客户大多为国内汽车生产领域领先企业&#xff0c;旗下进出口业务较多&#xff0c;该汽车零部件企业需要与海外企业一直保持着电子邮件的往来&#xff0c;电子邮件安全十分重…

eNSP 实现VLAN间通信实验

关于本实验本实验将通过上述方法&#xff08;配置Dot1q终结子接口和VLANIF接口&#xff09;实现不同VLAN间的通信&#xff0c;其中涵盖了与这两种方法相关的原理、配置命令和验证方法。实验目的掌握配置Dot1q终结子接口的方法&#xff0c;实现VLAN间通信。掌握配置VLANIF接口的…

window安装Redis服务

下载地址&#xff1a;Releases tporadowski/redis GitHub。 Redis 支持 32 位和 64 位。这个需要根据你系统平台的实际情况选择&#xff0c;这里我们下载 Redis-x64-xxx.zip压缩包&#xff0c;解压后&#xff0c;将文件夹重新命名为 redis。 命令创建Redis服务 上述方式虽然…

王炸!ChatGPT这算是彻底打脸马云。。。

在2019年的世界人工智能大会上&#xff0c;马斯克和马云针对人工智能话题上演了一场精彩对话。马云&#xff1a;我不觉得AI是一种威胁&#xff0c;我不认为人工智能是很恐怖的东西&#xff0c;因为人类很聪明。马斯克&#xff1a;一般大家都会低估人工智能的能力&#xff0c;实…

便携式明渠流量计技术背景及应用

便携式明渠流量计 是一款对现有在线水监测系统中流量监测的对比装置。该便携式明渠流量计实现了比对在线系统的液位误差及流量误差。引导式的操作方式&#xff0c;可自动每两分钟记录一次液位数据&#xff0c;连续记录6次&#xff0c;同时可以累计测量10分钟的流量数据&#xf…

嘉立创EDA专业版PCB的DRC与生产输出

前期为了满足各项设计的要求&#xff0c;通常会设置很多约束规则&#xff0c;当一个PCB设计完成之后&#xff0c;通常要进行DRC。DRC就是检查设计是否满足所设置的规则。一个完整的PCB设计必须经过各项连接性规则检查&#xff0c;常见的检查包括开路及短路的检查&#xff0c;更…

数据库管理-第六十四期 试玩Oracle 23c免费开发者版(20230404)

数据库管理 2023-04-24第六十四期 试玩Oracle 23c免费开发者版1 环境2 操作系统配置3 安装数据库4 配置并启动数据库5 访问数据库总结第六十四期 试玩Oracle 23c免费开发者版 四月第一篇文章&#xff0c;今天正好Oracle放出了Oracle Database 23c Free - Developer Release&am…

Python 自动化指南(繁琐工作自动化)第二版:十六、使用 CSV 文件和 JSON 数据

原文&#xff1a;https://automatetheboringstuff.com/2e/chapter16/ 在第 15 章&#xff0c;你学习了如何从 PDF 和 Word 文档中提取文本。这些文件是二进制格式的&#xff0c;需要特殊的 Python 模块来访问它们的数据。另一方面&#xff0c;CSV 和 JSON 文件只是纯文本文件。…

Linux下C/C++ redis协议(RESP)解析

Redis是一个开源的内存键值数据存储&#xff0c;最常用作主数据库、缓存、消息代理和队列。Redis提供了亚毫秒的响应时间&#xff0c;在游戏、金融科技、广告技术、社交媒体、医疗保健和物联网等行业实现了快速而强大的实时应用。 Redis连续五年成为开发人员最喜爱的数据库。开…

从小到大排序-课后程序(JavaScript前端开发案例教程-黑马程序员编著-第4章-课后作业)

【案例4-1】 从小到大排序 一、案例描述 考核知识点 函数的定义与调用&#xff0c;参数传递 练习目标 掌握函数的定义与调用。掌握for循环数据处理逻辑 需求分析 给出一组数据&#xff0c;要求按照从小到大进行排序。 案例分析 效果如图4-1所示。从小到大排序 具体实现步骤…

Final Cut Pro for Mac(中文fcpx视频剪辑)

Final Cut Pro for Mac是一款专业的视频剪辑软件&#xff0c;由苹果公司开发并发布。Final Cut Pro for Mac v10.6.5中文版是最新版本&#xff0c;支持中文界面&#xff0c;为用户提供了更加便捷的操作体验。 使用Final Cut Pro for Mac&#xff0c;用户可以轻松地进行视频编辑…