模块化的前世今生🔮

大家好,我是哈默。今天我们来简单说一下前端模块化的发展。

背景

我们的页面是由 html, css, js 组成的,我们将逻辑放到了 js 里编写。随着逻辑越来越多,越来越复杂,js 文件也会越来越长,并难以阅读。

这个时候,我们就需要将不同的逻辑拆分到不同的模块。

  • 一个模块包含输入和输出
  • 模块内部的实现是私有的,对外暴露接口与其他模块进行通信

前端模块化只是一种标准,并不是具体的实现,那么具体实现模块化的方式也是逐步进行演化的。

使用全局函数

最简单最直接的方式,就是可以将不同逻辑封装到不同的函数中。

js 复制代码
function request() {
  return {
    code: 200,
    data: {
      scores: [90, 95, 88, 35, 55, 91],
    },
  };
}

function average(scores) {
  const total = scores.reduce(function (res, score) {
    return res + score;
  }, 0);

  return `你的平均分是${total / scores.length}。`;
}

function fail(scores) {
  const failScores = scores.filter((score) => score < 60);

  return `很抱歉,你有${failScores.length}次不合格。`;
}

const scores = request().data.scores;
console.log("average", average(scores));
console.log("fail", fail(scores));

这种方式的缺点也很明显,我们定义的 request、average、fail 函数,都是定义在 window 对象上的,如果我们引入了第三方或者其他小伙伴的 js,那么很难保证他们没有也去定义同名的函数,特别是当我们的函数名取的比较通用的时候,比如 add。

所以,全局函数的方式容易引发命名冲突。

使用全局 namespace

要解决 全局函数 的问题,我们可以把这些函数的定义放入一个对象中,比如 __module__:

js 复制代码
var __module__ = {
  request() {
    return {
      code: 200,
      data: {
        scores: [90, 95, 88, 35, 55, 91],
      },
    };
  },
  average(scores) {
    const total = scores.reduce(function (res, score) {
      return res + score;
    }, 0);

    return `你的平均分是${total / scores.length}。`;
  },
  fail(scores) {
    const failScores = scores.filter((score) => score < 60);

    return `很抱歉,你有${failScores.length}次不合格。`;
  },
};

var m = window.__module__;
const scores = m.request().data.scores;
console.log("average", m.average(scores));
console.log("fail", m.fail(scores));

通过这种方式,我们避免了直接将 request、average、fail 定义在 window 对象上,而是定义在 __module__ 上,而 __module__ 的名字,一般也很难与其他人冲突,如果我们取的名字在长一些的话。。。

但这种方式的问题就是:外部可以随意修改 __module__ 内部的数据:

js 复制代码
var __module__ = {
  num: 100,
  request() { ... },
  average(scores) { ... },
  fail(scores) { ... },
};

var m = window.__module__;

m.num = 200; // 可以随意修改模块内部的数据🤐

这违反了我们之前对于模块的定义:

模块内部的实现是私有的,对外暴露接口与其他模块进行通信

使用 iife

解决 namespace 方案的问题,可以通过 iife 自执行函数来创建闭包:

js 复制代码
(function (global) {
  var num = 100;

  function request() {
    return {
      code: 200,
      data: {
        scores: [90, 95, 88, 35, 55, 91],
      },
    };
  }

  function average(scores) {
    const total = scores.reduce(function (res, score) {
      return res + score;
    }, 0);

    return `你的平均分是${total / scores.length}。`;
  }

  function fail(scores) {
    const failScores = scores.filter((score) => score < 60);

    return `很抱歉,你有${failScores.length}次不合格。`;
  }

  function getNum() {
    return num;
  }

  function setNum(newValue) {
    num = newValue;
  }

  global.__module__ = {
    request,
    average,
    fail,
    getNum,
    setNum,
  };
})(window);

const m = window.__module__;
const scores = m.request().data.scores;
console.log("average", m.average(scores));
console.log("fail", m.fail(scores));

现在我们无法直接访问 num 变量了,只能通过模块内部提供的 getNum、setNum 来对 num 进行操作:

js 复制代码
console.log(m.getNum());
m.setNum(200);
console.log(m.getNum());

这种方式,是比较完美的,它让我们的数据更安全了。

但是,这仅限于我们只有一个模块的时候。

如果我们有多个模块,并且模块之间还存在着依赖,我们如何让一个模块去引用另一个模块呢?

iife + 传参

比如,我们有如下两个模块。

request 模块:

js 复制代码
(function (global) {
  function request() {
    return {
      code: 200,
      data: {
        scores: [90, 95, 88, 35, 55, 91],
      },
    };
  }

  global.__module__ = {
    request,
  };
})(window);

handle 模块:

js 复制代码
(function (global) {
  function average(scores) {
    const total = scores.reduce(function (res, score) {
      return res + score;
    }, 0);

    return `你的平均分是${total / scores.length}。`;
  }

  function fail(scores) {
    const failScores = scores.filter((score) => score < 60);

    return `很抱歉,你有${failScores.length}次不合格。`;
  }

  global.__module__ = {
    average,
    fail,
  };
})(window);

很明显,handle 模块是需要依赖 request 模块的数据的,那么此时,我们可以通过 传参 的形式,将 handle 模块传入 request 模块中。

request 模块改造一下:

diff 复制代码
 (function (global) {
   function request() { ... }

-  global.__module__ = {
-    request,
-  };

+  global.__module_request__ = {
+    request,
+  };
 })(window);

handle 模块也相应改造下:

diff 复制代码
- (function (global) {
+ (function (global, req) {
    function average(scores) { ... }

    function fail(scores) { ... }

    global.__module__ = {
+     req,
      average,
      fail,
    };
- )(window);
+ })(window, window.__module_request__);

+ const m = window.__Module;
+ const scores = m.req.request().data.scores;
+ console.log(m.average(scores));
+ console.log(m.fail(scores));

我们在定义 handle 模块的时候,将 request 模块作为第二个参数传入 iife 内部,这样我们就可以访问到 request 模块里的 request 方法了。

这样,我们就解决了模块之间依赖的问题。

但是,缺点也很明显:

  1. 当有多个依赖的时候,传入的参数也将变多,代码会越来越难以阅读
  2. 写法比较繁琐

。。。

CommonJS 和 ES Module

基于以上的的演化,我们有了基本的实现模块化的方式。

为了让书写模块化更方便,我们也有了很多的模块化标准,现在最流行的就是:CommonJS 和 ES Module。

在我们使用这两种模块化标准书写我们的代码的时候,我们的每一个模块,就可以定义在一个 js 的文件里面。

模块的引入和导出,就可以分别使用对应的语法来进行:

  • CommonJS 的导出:module.exports
  • CommonJS 的导入: require
  • ES Module 的导出:export
  • ES Module 的导入: import

如果将两种标准的代码进行打包,最终的结果也是基于 iife 来实现的。

CommonJS:

ES Module:

总结

通过这篇文章的学习,我们初步了解了模块化的一个发展历程,希望大家能对于模块化有一个更清晰的认识 😊

相关推荐
console.log('npc')11 分钟前
[特殊字符] AI Agent 工作台前端实战教程 · 新手入门指南
前端·人工智能
gCode Teacher 格码致知14 分钟前
前端教学2:ul标签的应用场合与经典案例
前端
const_qiu34 分钟前
第 2.5 章 2.5.6:前端登录、Token 无感续签与路由守卫
java·前端·人工智能·状态模式
用户831348593069843 分钟前
Vue3 事件:常用用法与开发高频坑点
前端·javascript·vue.js
weixin_4462608544 分钟前
ReAct‑Evolve:基于轨迹反馈的ReAct智能体自主提示演化系统
前端·react.js·前端框架
其实防守也摸鱼1 小时前
存储管理:页面置换算法学习任务(测验题要求给出置换页面顺序,缺页次数,缺页率)
android·前端·数据库·学习·oracle·自动化
怒放的生命19911 小时前
前端项目 Docker 打包实操指南
前端·docker·容器
tedcloud1231 小时前
Docker Compose 部署 Immich:手机照片自动备份与常见故障排查
运维·前端·docker·容器·智能手机·开源·流程图
时间的情敌1 小时前
《WebGIS 前端的成长路线,三维可视化工程师需要掌握什么》
前端·状态模式
漫步人生走在路上1 小时前
用小语种SEO突破外贸B2B天花板:法语、德语市场的实测数据
开发语言·前端·javascript·人工智能·搜索引擎·ecmascript