[路飞]每日一答:用闭包完成模块化?(带你手撕webpack)

624 阅读5分钟

如何用闭包完成模块化(Webpack原理)

闭包

闭包的相关内容可以参考前文每日一答:什么是闭包?

这里主要想要问的是闭包在模块化中的应用,可以拿 webpack 打包进行说明。

手写一个 webpack

要弄懂闭包在模块化中的应用,其中以 webpack作为案例,最好的说明方式就是自己利用webpack打包原理去实现一个小的 mini webpack。

1. 创建两个模块

模块化的前提,首先创建两个模块, add.jsindex.js

add.js: 提供一个非常简单的函数,参数为 ab,函数目的是将 ab相加,并且返回最终结果。

add.js

exports.default = function (a, b) { return a + b }

index.js: 调用 add.js的函数,并且打印结果。

index.js

const add = require('./add.js').default
add(2, 3)

以上准备工作已经完成。

2. 尝试在环境中调用 index.js

node环境下执行

node ./index.js

运行结果: 5

说明在 node 环境中我们已经可以完成对 index.js 模块 和 add.js 模块进行调用。

在web环境下执行

编写一个 html 文件去引入 index.js 看看结果。

index.html

<script src="./index.js">

</script>

运行结果: ReferenceError: Can't find variable: require

说明我们的代码在web环境下是不能被完全执行的。

3.解决 web 环境下模块化问题

我们将已经写好的模块导入到 index.html 中时出现了错误。为了方便调试,我们可以将两个模块的代码先放到一块,直接写在 index.html中。

index.html

<script >
 exports.default = function (a, b) { console.log(a + b) }
 const add = require('./add.js').default
 add(2, 3)
</script>

将这三行代码原封不动拷贝过来,这是我们要做的第一步,直接运行肯定是报错的。

Uncaught ReferenceError: exports is not defined

exports 函数在单纯的 web 环境下是没有提供的这就是我们要解决的第一个问题。

1.模拟实现 exports 函数

(1). 声明 exports 对象

原报错代码中报错的原因是没有声明和定义 exports 对象或方法,那么我们可以在报错的上面声明一个 exports 对象。

实际上下一步就是调用 exports 对象的 default 方法。

<script >
  const exports = {} //创建一个 exports 对象
  exports.default = function (a, b) { console.log(a + b) }
  exports.default(2,3)
  //  const add = require('./add.js').default
  //  add(2, 3)
</script>

然而这么写 exports 对象显得非常不优雅,相当于将 exports 对象完全暴露出来了,并且很可能会影响他其他甚至是全局变量,真正的模块化,应该是每个模块都是独立的,有独立的作用域,所以我们需要解决污染全局的问题。

(2). 使用闭包和立即执行函数封装 exports
<script >
  const exports = {}; //创建一个 exports 对象
  (function (exports, code) {
    eval(code)
  })(exports, 'exports.default = function (a, b) { console.log(a + b) }')
  exports.default(2,3)
 //  const add = require('./add.js').default
 //  add(2, 3)
</script>

运行结果: 5

2. 模拟实现 require 函数

至此我们 exports 函数已经模拟好了,下面我们打开 require 函数的注释发现 web 也不提供 require函数,所以我们还需要模拟去实现 require函数。

(1).定义一个 require

试想一下我们在日常开发中如何使用 require 函数的呢?首先 require 是一个方法,紧接着他有至少一个参数是你想要引入模块的路径,最终返回一个对象或方法。我觉得这才是 require函数的基本结构。

所以我们要解决3个问题:

  • 函数的内部应当是什么

  • 函数的返回应当是什么

  • 函数的参数应当如何使用

暂且定义 require 函数框架:

const require = function (path) {
    return {}
}
(2).require 的内部实现

在考虑如何使用 path 入参之前我们应该先考虑 require 函数内部应该做什么事。require函数说白了就是将 exports的内容输出。所以我们其实已经解决了前面两个问题:

  • 函数的内部应当是什么: 是 exports 的对象内容

  • 函数的返回应当是什么: 是 exports 对象

所以我们可以大致写出:

const require = function (path) {
    const exports = {};
    (function (exports, code) {
      eval(code)
    })(exports, 'exports.default = function (a, b) { console.log(a + b) }')

    return exports
}

简单分析一下:

image.png

下面我们要解决第三个问题:路径参数的使用

require的参数是引用模块的路径,也就是说,我们传递的 path参数变量是为了告诉我们我们要找的是哪个一模块。 如此一来问题就比较好解决了,我们可以定义一个路由,通过 path 去找对应模块,这样就可以达到根据入参匹配不同模块的效果了。

(3).定义 require 路由
const router = {
    'add.js': `exports.default = function (a, b) { console.log(a + b) }`,
    'index.js': `const add = require('add.js').default
              add(2, 3)`
    }

路由为路径匹配执行代码,那么完整的代码如下:

image.png

执行结果:5

(4).完整实现模块化

这么写固然已经实现了我们所要的效果,但是并没有完全实现。路由又是一个新的变量,那么为了造成变量污染或变量之间的互相干扰,我们在最外层应当也使用闭包+立即执行函数进行包裹。

所以最终实现代码为:

<script >
  !(function () {
    const router = {
      'add.js': `exports.default = function (a, b) { console.log(a + b) }`,
      'index.js': `const add = require('add.js').default
                add(2, 3)`
    }
    const require = function (path) {
      const exports = {};
      (function (exports, code) {
        eval(code)
      })(exports, router[path])
      
      return exports
    }
    
    require('index.js')
  })()
</script>

4.重新实现入口文件 bundle.js

在上面代码中,我们一共有2个模块,分别是 add.jsindex.js,在解决模块化的时候我们使用了立即执行函数和闭包的方式将其导出,但是我们是直接为了测试方便直接写在了index.html的script标签中,并没有给外暴露接口。所以我们需要写一个入口文件,命名为 bundle.js,只需要将 script 中的代码原封不动copy过去就可以了。

bundle.js

  !(function () {
    const router = {
      'add.js': `exports.default = function (a, b) { console.log(a + b) }`,
      'index.js': `const add = require('add.js').default
                add(2, 3)`
    }
    const require = function (path) {
      const exports = {};
      (function (exports, code) {
        eval(code)
      })(exports, router[path])
      
      return exports
    }
    
    require('index.js')
  })()

5.测试结果

分别在 node 环境和 web 环境进行测试

(1).node 环境测试

新建 test.js:

require('./bundle.js')

执行结果: 5

(2).web 环境测试

index.html

<script src="./bundle.js">

</script>

执行结果: 5

至此,我们已经完成了一个模块化的输出。这也是 webpack 打包的基本架构原理。

webpack 打包

经历上面手写webpack,我们y也可以尝试分析一下利用 webpack 打包后的源码。

删除所写的测试文件,仅仅留下 index.jsadd.js 两个待打包的模块,并且放在src文件夹下

执行:

npm init -y
npx webpack

最终输出这样的结构:

image.png

main.js就是我们打包后的文件,我们尝试分析一下这个文件,即webpack是怎么做到模块化的。

main.js

(() = > {
  var r = {
    241: r = > {
      r.exports = function (r, o) {
        console.log(r + o)
      }
    }
  },
  o = {}; (0,
    function t(e) {
      var n = o[e];
      if (void 0 !== n) return n.exports;
      var s = o[e] = {
        exports: {}
      };
      return r[e](s, s.exports, t),
        s.exports
    }(241).
      default)(2, 3)
}) ();

这是进行压缩混淆后的代码,但是整体上看的是不是很眼熟?没错,和我们之前的实现异曲同工。

image.png

所以实际上你已经手写了一个 mini webpack!