webpack 拍了拍你,给了你一份图解指南(模块化部分)

发表于 4年以前  | 总阅读数:472 次

在前面一篇文章中《模块化系列》彻底理清 AMD,CommonJS,CDM,UMD,ES6 我们可以学到了各种模块化的机制。那么接下里我们就来分析一下 webpack 的模块化机制。(主要讲 JS 部分)

提到 webpack,可以说是与我们的开发工程非常密切的工具,不管是日常开发、进行面试还是对于自我的提高,都离不开它,因为它给我们的开发带了极大的便利以及学习的价值。但是由于webpack是一个非常庞大的工程体系,使得我们望之却步。本文想以这种图解的形式能够将它慢慢地剥开一层一层复杂的面纱,最终露出它的真面目。以下是我列出的关于 webpack 相关的体系。

webpack-2本文讲的是 打包 - CommonJS 模块,主要分为两个部分

  • webpack 的作用
  • webpack 的模块化机制与实现

webpack 的作用

在我们前端多样化的今天,很多工具为了满足我们日益增长的开发需求,都变得非常的庞大,例如 webpack 。在我们的印象中,它似乎集成了所有关于开发的功能,模块打包,代码降级,文件优化,代码校验等等。正是因为面对如此庞大的一个工具,所以才让我们望而却步,当然了还有一点就是,webpack 的频繁升级,周边的生态插件配套版本混乱,也加剧我们对它的恐惧。

那么我们是不是应该思考一下,webpack 的出现究竟给我们带来了什么?我们为啥需要用它?而上面所有的一些代码降级(babel转化)、编译SCSS 、代码规范检测都是得益于它的插件系统和loader机制,并不是完完全全属于它。

所以在我看来,它的功能核心是「打包」,而打包则是能够让模块化的规范得以在浏览器直接执行。因此我们来看看打包后所带来的功能:

  • 模块隔离
  • 模块依赖加载

模块隔离

如果我们不用打包的方式,我们所有的模块都是直接暴露在全局,也就是挂载在 window/global 这个对象。也许代码量少的时候还可以接受,不会有那么多的问题。特别是在代码增多,多人协作的情况下,给全局空间带来的影响是不可预估的,如果你的每一次开发都得去一遍一遍查找是否有他们使用当前的变量名。

举个例子(仅仅为例子说明,实际工程会比以下复杂许多),一开始我们的 user1 写了一下几个模块,跑起来非常的顺畅。

image-20200626231748187

├── bar.js    function bar(){}
├── baz.js    function baz(){}
└── foo.js function foo(){}

但是呢,随着业务迭代,工程的复杂性增加,来了一个 user2,这个时候 user2,需要开发一个 foo 业务,里面也有一个 baz 模块,代码也很快写好了,变成了下面这个样子。

├── bar.js    function bar(){}
├── baz.js    function baz(){}
├── foo
│   └── baz.js function baz(){}
└── foo.js function foo(){}

但是呢这个时候,老板来找 user2 了,为什么增加了新业务后,原来的业务出错了呢?这个时候发现原来是 user2 写的新模块覆盖了 user1 的模块,从而导致了这场事故。

image-20200626220806881

因此,当我们开发的时候将所有的模块都暴露在全局的时候,想要避免错误,一切都得非常的小心翼翼,我们很容易在不知情的偷偷覆盖我们以前定义的函数,从而酿成错误。

因此 webpack 带来的第一个核心作用就是隔离,将每个模块通过闭包的形式包裹成一个个新的模块,将其放于局部作用域,所有的函数声明都不会直接暴露在全局。

image-20200626220851909

原来我们调用的 是 foo 函数,但是 webpack 会帮我们生成独一无二的模块ID,完全不需要担心模块的冲突,现在可以愉快地书写代码啦。

baz.js
module.exports = function baz (){}

foo/baz.js
module.exports = function baz (){}

main.js
var baz = require('./baz.js');
var fooBaz = require('./foo/baz.js');

baz();
fooBaz();

可能你说会之前的方式也可以通过改变函数命名的方式,但是原来的作用范围是整个工程,你得保证,当前命名在整个工程中不冲突,现在,你只需要保证的是单个文件中命名不冲突。(对于顶层依赖也是非常容易发现冲突)

image-20200627140818771

模块依赖加载

还有一种重要的功能就是模块依赖加载。这种方式带来的好处是什么?我们同样先来看例子,看原来的方式会产生什么问题?

User1 现在写了3个模块,其中 baz 是依赖于 bar 的。

image-20200627000240836

写完后 user1 进行了上线,利用了顺序来指出了依赖关系。

<script src="./bar.js"></script>
<script src="./baz.js"></script>
<script src="./foo.js"></script>

可是过了不久 user2 又接手了这个业务。user 2 发现,他开发的 abc 模块,通过依赖 bar 模块,可以进行快速地开发。可是 粗心的 user2 不太明白依赖关系。竟然将 abc 的位置随意写了一下,这就导致 运行 abc 的时候,无法找到 bar 模块。

image-20200627000713100

<script src="./abc.js"></script>
<script src="./bar.js"></script>
<script src="./baz.js"></script>
<script src="./foo.js"></script>

因此这里 webpack 利用 CommonJS/ ES Modules 规范进行了处理。使得各个模块之间相互引用无需考虑最终实际呈现的顺序。最终会被打包为一个 bunlde 模块,无需按照顺序手动引入。

baz.js
const bar = require('./bar.js');
module.exports = function baz (){
 ...
 bar();
 ...
}

abc.js
const bar = require('./bar.js');
module.exports = function baz (){
 ...
 bar();
 ...
}
<script src="./bundle.js"></script>

image-20200627003815071

webpack 的模块化机制与实现

基于以上两项特性,模块的隔离以及模块的依赖聚合。我们现在可以非常清晰的知道了webpack所起的核心作用。

  • 为了尽可能降低编写的难度和理解成本,我没有使用 AST 的解析,(当然 AST 也不是什么很难的东西,以后的文章中我会讲解 AST是什么以及 AST 解析器的实现过程。
  • 仅实现了 CommonJS 的支持

bundle工作原理

为了能够实现 webpack, 我们可以通过反推的方法,先看webpack 打包后 bundle 是如何工作的。

「源文件」

// index.js
const b = require('./b');
b();
// b.js
module.exports = function () {
    console.log(11);
}

「build 后」(去除了一些干扰代码)

(function(modules) {
  var installedModules = {};
  function __webpack_require__(moduleId) {
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    var module = (installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {},
    });
    modules[moduleId].call(
      module.exports,
      module,
      module.exports,
      __webpack_require__
    );
    module.l = true;
    return module.exports;
  }
  return __webpack_require__((__webpack_require__.s = 0));
})([
  /* 0 */
  function(module, exports, __webpack_require__) {
    var b = __webpack_require__(1);
    b();
  },
  /* 1 */
  function(module, exports) {
    module.exports = function() {
      console.log(11);
    };
  },
]);

image-20200627135324956

以上就是 bundle 的运作原理。通过上述的流程图我们可以看到,有四个关键点

  • 已注册模块(存放已经注册的模块)
  • 模块列表(用来存放所有的包装模块)
  • 模块查找(从原来的树形的模块依赖,变成了扁平查找)
  • 模块的包装(原有的模块都进行了一次包装)

webpack实现

通过 bundle 的分析,我们只需要做的就是 4 件事

  • 遍历出所有的模块
  • 模块包装
  • 提供注册模块、模块列表变量和导入函数
  • 持久化导出

模块的遍历

首先来介绍一下模块的结构,能使我们快速有所了解, 结构比较简单,由内容和模块id组成。

interface GraphStruct {
    context: string;
    moduleId: string;
}
{
 "context": `function(module, exports, require) {
    const bar = require('./bar.js');
  const foo = require('./foo.js');
  console.log(bar());
  foo();
  }`,
  "moduleId": "./example/index.js"
}

接下来我们以拿到一个入口文件来进行讲解,当拿到一个入口文件时,我们需要对其依赖进行分析。说简单点就是拿到 require 中的值,以便我们去寻找下一个模块。由于在这一部分不想引入额外的知识,开头也说了,一般采用的是 AST 解析的方式,来获取 require 的模块,在这里我们使用正则。

用来匹配全局的 require 
const REQUIRE_REG_GLOBAL = /require\(("|')(.+)("|')\)/g;
用来匹配 require 中的内容
const REQUIRE_REG_SINGLE = /require\(("|')(.+)("|')\)/;
const context = `
const bar = require('./bar.js');
const foo = require('./foo.js');
console.log(bar());
foo();
`;
console.log(context.match(REQUIRE_REG_GLOBAL));
// ["require('./bar.js')", "require('./foo.js')"]

image-20200627202427794

由于模块的遍历并不是只有单纯的一层结构,一般为树形结构,因此在这里我采用了深度遍历。主要通过正则去匹配出require 中的依赖项,然后不断递归去获取模块,最后将通过深度遍历到的模块以数组形式存储。(不理解深度遍历,可以理解为递归获取模块)

image-20200627142130902

以下是代码实现

...
private entryPath: string
private graph: GraphStruct[]
...
createGraph(rootPath: string, relativePath: string) {
    // 通过获取文件内容
    const context = fs.readFileSync(rootPath, 'utf-8');
    // 匹配出依赖关系
    const childrens = context.match(REQUIRE_REG_GLOBAL);
   // 将当前的模块存储下来
    this.graph.push({
        context,
        moduleId: relativePath,
    })
    const dirname = path.dirname(rootPath);
    if (childrens) {
       // 如有有依赖,就进行递归
        childrens.forEach(child => {
            const childPath = child.match(REQUIRE_REG_SINGLE)[2];
            this.createGraph(path.join(dirname, childPath), childPath);
        });
    }
}

模块包装

为了能够使得模块隔离,我们在外部封装一层函数, 然后传入对应的模拟 requiremodule使得模块能进行正常的注册以及导入 。

function (module, exports, require){
    ...
},

提供注册模块、模块列表变量和导入函数

这一步比较简单,只要按照我们分析的流程图提供已注册模块变量、模块列表变量、导入函数。

/* modules = {
  "./example/index.js": function (module, exports, require) {
    const a = require("./a.js");
    const b = require("./b.js");

    console.log(a());
    b();
  },
  ...
};*/
bundle(graph: GraphStruct[]) {
    let modules = '';
    graph.forEach(module => {
        modules += `"${module.moduleId}":function (module, exports, require){
        ${module.context}
        },`;
    });
    const bundleOutput = `
    (function(modules) {
        var installedModules = {};
        // 导入函数
        function require(moduleId) {
            // 检查是否已经注册该模块
            if (installedModules[moduleId]) {
                return installedModules[moduleId].exports;
            }
           // 没有注册则从模块列表获取模块进行注册
            var module = (installedModules[moduleId] = {
                i: moduleId,
                l: false,
                exports: {},
            });
           // 执行包装函数,执行后更新模块的内容
            modules[moduleId].call(
                module.exports,
                module,
                module.exports,
                require
            );
            // 设置标记已经注册
            module.l = true;
            // 返回实际模块
            return module.exports;
        }
        require("${graph[0].moduleId}");
    })({${modules}})
    `;
    return bundleOutput;
}

持久化导出

最后将生成的 bundle 持久写入到磁盘就大功告成。

fs.writeFileSync('bundle.js', this.bundle(this.graph))

完整代码100行 代码不到,详情可以查看以下完整示例。

github地址: https://github.com/hua1995116/tiny-webpack

结尾

以上仅代表个人的理解,希望让你对webpack的理解有所帮助, 如有讲的不好的请多指出。

本文由哈喽比特于4年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/-BEjIUlFnQip5BKJ7_HTAA

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237231次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8065次阅读
 目录