模块化的前世今生🔮

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

背景

我们的页面是由 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:

总结

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

相关推荐
秦jh_7 分钟前
【Linux】多线程(概念,控制)
linux·运维·前端
蜗牛快跑21319 分钟前
面向对象编程 vs 函数式编程
前端·函数式编程·面向对象编程
Dread_lxy20 分钟前
vue 依赖注入(Provide、Inject )和混入(mixins)
前端·javascript·vue.js
涔溪1 小时前
Ecmascript(ES)标准
前端·elasticsearch·ecmascript
用户3157476081351 小时前
成为程序员的必经之路” Git “,你学会了吗?
面试·github·全栈
榴莲千丞1 小时前
第8章利用CSS制作导航菜单
前端·css
奔跑草-1 小时前
【前端】深入浅出 - TypeScript 的详细讲解
前端·javascript·react.js·typescript
羡与1 小时前
echarts-gl 3D柱状图配置
前端·javascript·echarts
guokanglun2 小时前
CSS样式实现3D效果
前端·css·3d
咔咔库奇2 小时前
ES6进阶知识一
前端·ecmascript·es6