面试官:webpack原理都不会?

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

引言

前一段时间我把webpack源码大概读了一遍,webpack4.x版本后,其源码已经比较庞大,对各种开发场景进行了高度抽象,阅读成本也愈发昂贵。

过度分析源码对于大家并没有太大的帮助。本文主要是想通过分析webpack的构建流程以及实现一个简单的webpack来让大家对webpack的内部原理有一个大概的了解。(保证能看懂,不懂你打我 )

webpack 构建流程分析

首先,无须多言,上图~

webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:首先会从配置文件和 Shell 语句中读取与合并参数,并初始化需要使用的插件和配置插件等执行环境所需要的参数;初始化完成后会调用Compilerrun来真正启动webpack编译构建过程,webpack的构建流程包括compilemakebuildsealemit阶段,执行完这些阶段就完成了构建过程。

初始化

entry-options 启动

从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。

run 实例化

compiler:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译

编译构建

entry 确定入口

根据配置中的 entry 找出所有的入口文件

make 编译模块

从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理

build module 完成模块编译

经过上面一步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系

seal 输出资源

根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会

emit 输出完成

在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

分析完构建流程,下面让我们自己动手实现一个简易的webpack吧~

实现一个简易的 webpack

准备工作

目录结构

我们先来初始化一个项目,结构如下:

|-- forestpack
    |-- dist
    |   |-- bundle.js
    |   |-- index.html
    |-- lib
    |   |-- compiler.js
    |   |-- index.js
    |   |-- parser.js
    |   |-- test.js
    |-- src
    |   |-- greeting.js
    |   |-- index.js
    |-- forstpack.config.js
    |-- package.json

这里我先解释下每个文件/文件夹对应的含义:

  • dist:打包目录

  • lib:核心文件,主要包括compilerparser

  • compiler.js:编译相关。Compiler为一个类, 并且有run方法去开启编译,还有构建modulebuildModule)和输出文件(emitFiles

  • parser.js:解析相关。包含解析ASTgetAST)、收集依赖(getDependencies)、转换(es6转es5

  • index.js:实例化Compiler类,并将配置参数(对应forstpack.config.js)传入

  • test.js:测试文件,用于测试方法函数打console使用

  • src:源代码。也就对应我们的业务代码

  • forstpack.config.js:配置文件。类似webpack.config.js

  • package.json:这个就不用我多说了~~~(什么,你不知道??)

先完成“造轮子”前 30%的代码

项目搞起来了,但似乎还少点东西~~

对了!基础的文件我们需要先完善下:forstpack.config.jssrc

首先是forstpack.config.js

const path = require("path");

module.exports = {
  entry: path.join(__dirname, "./src/index.js"),
  output: {
    path: path.join(__dirname, "./dist"),
    filename: "bundle.js",
  },
};

内容很简单,定义一下入口、出口(你这也太简单了吧!!别急,慢慢来嘛)

其次是src,这里在src目录下定义了两个文件:

  • greeting.js
// greeting.js
export function greeting(name) {
  return "你好" + name;
}
  • index.js
import { greeting } from "./greeting.js";

document.write(greeting("森林"));

ok,到这里我们已经把需要准备的工作都完成了。(问:为什么这么基础?答:当然要基础了,我们的核心是“造轮子”!!)

梳理下逻辑

短暂的停留一下,我们梳理下逻辑:

Q: 我们要做什么?

A: 做一个比webpack更强的super webpack(不好意思,失态了,一不小心说出了我的心声)。还是低调点(防止一会被疯狂打脸)

Q: 怎么去做?

A: 看下文(23333)

Q: 整个的流程是什么?

A: 哎嘿,大概流程就是:

  • 读取入口文件
  • 分析入口文件,递归的去读取模块所依赖的文件内容,生成AST语法树。
  • 根据AST语法树,生成浏览器能够运行的代码

正式开工

compile.js 编写

const path = require("path");
const fs = require("fs");

module.exports = class Compiler {
  // 接收通过lib/index.js new Compiler(options).run()传入的参数,对应`forestpack.config.js`的配置
  constructor(options) {
    const { entry, output } = options;
    this.entry = entry;
    this.output = output;
    this.modules = [];
  }
  // 开启编译
  run() {

  }
  // 构建模块相关
  buildModule(filename, isEntry) {
    // filename: 文件名称
    // isEntry: 是否是入口文件
  }
  // 输出文件
  emitFiles() {

  }
};

compile.js主要做了几个事情:

  • 接收forestpack.config.js配置参数,并初始化entryoutput
  • 开启编译run方法。处理构建模块、收集依赖、输出文件等。
  • buildModule方法。主要用于构建模块(被run方法调用)
  • emitFiles方法。输出文件(同样被run方法调用)

到这里,compiler.js的大致结构已经出来了,但是得到模块的源码后, 需要去解析,替换源码和获取模块的依赖项, 也就对应我们下面需要完善的parser.js

parser.js 编写

const fs = require("fs");
// const babylon = require("babylon");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const { transformFromAst } = require("babel-core");
module.exports = {
  // 解析我们的代码生成AST抽象语法树
  getAST: (path) => {
    const source = fs.readFileSync(path, "utf-8");

    return parser.parse(source, {
      sourceType: "module",  //表示我们要解析的是ES模块
    });
  },
  // 对AST节点进行递归遍历
  getDependencies: (ast) => {
    const dependencies = [];
    traverse(ast, {
      ImportDeclaration: ({ node }) => {
        dependencies.push(node.source.value);
      },
    });
    return dependencies;
  },
  // 将获得的ES6的AST转化成ES5
  transform: (ast) => {
    const { code } = transformFromAst(ast, null, {
      presets: ["env"],
    });
    return code;
  },
};

看完这代码是不是有点懵(说好的保证让看懂的 )

别着急,你听我辩解!!

这里要先着重说下用到的几个babel包:

  • @babel/parser:用于将源码生成AST
  • @babel/traverse:对AST节点进行递归遍历
  • babel-core/@babel/preset-env:将获得的ES6AST转化成ES5

parser.js中主要就三个方法:

  • getAST:将获取到的模块内容 解析成AST语法树
  • getDependencies:遍历AST,将用到的依赖收集起来
  • transform:把获得的ES6AST转化成ES5

完善 compiler.js

在上面我们已经将compiler.js中会用到的函数占好位置,下面我们需要完善一下compiler.js,当然会用到parser.js中的一些方法(废话,不然我上面干嘛要先把parser.js写完~~)

直接上代码:

const { getAST, getDependencies, transform } = require("./parser");
const path = require("path");
const fs = require("fs");

module.exports = class Compiler {
  constructor(options) {
    const { entry, output } = options;
    this.entry = entry;
    this.output = output;
    this.modules = [];
  }
  // 开启编译
  run() {
    const entryModule = this.buildModule(this.entry, true);
    this.modules.push(entryModule);
    this.modules.map((_module) => {
      _module.dependencies.map((dependency) => {
        this.modules.push(this.buildModule(dependency));
      });
    });
    // console.log(this.modules);
    this.emitFiles();
  }
  // 构建模块相关
  buildModule(filename, isEntry) {
    let ast;
    if (isEntry) {
      ast = getAST(filename);
    } else {
      const absolutePath = path.join(process.cwd(), "./src", filename);
      ast = getAST(absolutePath);
    }

    return {
      filename,  // 文件名称
      dependencies: getDependencies(ast),  // 依赖列表
      transformCode: transform(ast),  // 转化后的代码
    };
  }
  // 输出文件
  emitFiles() {
    const outputPath = path.join(this.output.path, this.output.filename);
    let modules = "";
    this.modules.map((_module) => {
      modules += `'${_module.filename}' : function(require, module, exports) {${_module.transformCode}},`;
    });

    const bundle = `
        (function(modules) {
          function require(fileName) {
            const fn = modules[fileName];
            const module = { exports:{}};
            fn(require, module, module.exports)
            return module.exports
          }
          require('${this.entry}')
        })({${modules}})
    `;

    fs.writeFileSync(outputPath, bundle, "utf-8");
  }
};

关于compiler.js的内部函数,上面我说过一遍,这里主要来看下emitFiles

emitFiles() {
    const outputPath = path.join(this.output.path, this.output.filename);
    let modules = "";
    this.modules.map((_module) => {
      modules += `'${_module.filename}' : function(require, module, exports) {${_module.transformCode}},`;
    });

    const bundle = `
        (function(modules) {
          function require(fileName) {
            const fn = modules[fileName];
            const module = { exports:{}};
            fn(require, module, module.exports)
            return module.exports
          }
          require('${this.entry}')
        })({${modules}})
    `;

    fs.writeFileSync(outputPath, bundle, "utf-8");
  }

这里的bundle一大坨,什么鬼?

我们先来了解下webpack的文件 机制。下面一段代码是经过webpack打包精简过后的代码:

// dist/index.xxxx.js
(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;
  }
  __webpack_require__(0);
})([
/* 0 module */
(function(module, exports, __webpack_require__) {
  ...
}),
/* 1 module */
(function(module, exports, __webpack_require__) {
  ...
}),
/* n module */
(function(module, exports, __webpack_require__) {
  ...
})]);

简单分析下:

  • webpack 将所有模块(可以简单理解成文件)包裹于一个函数中,并传入默认参数,将所有模块放入一个数组中,取名为 modules,并通过数组的下标来作为 moduleId
  • modules 传入一个自执行函数中,自执行函数中包含一个 installedModules 已经加载过的模块和一个模块加载函数,最后加载入口模块并返回。
  • __webpack_require__ 模块加载,先判断 installedModules 是否已加载,加载过了就直接返回 exports 数据,没有加载过该模块就通过 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__) 执行模块并且将 module.exports 给返回。

(你上面说的这一坨又是什么鬼?我听不懂啊啊啊啊!!!)

那我换个说法吧:

  • 经过webpack打包出来的是一个匿名闭包函数(IIFE
  • modules是一个数组,每一项是一个模块初始化函数
  • __webpack_require__用来加载模块,返回module.exports
  • 通过WEBPACK_REQUIRE_METHOD(0)启动程序

(小声 bb:怎么样,这样听懂了吧)

lib/index.js 入口文件编写

到这里,就剩最后一步了(似乎见到了胜利的曙光)。在lib目录创建index.js

const Compiler = require("./compiler");
const options = require("../forestpack.config");

new Compiler(options).run();

这里逻辑就比较简单了:实例化Compiler类,并将配置参数(对应forstpack.config.js)传入。

运行node lib/index.js就会在dist目录下生成bundle.js文件。

(function (modules) {
  function require(fileName) {
    const fn = modules[fileName];
    const module = { exports: {} };
    fn(require, module, module.exports);
    return module.exports;
  }
  require("/Users/fengshuan/Desktop/workspace/forestpack/src/index.js");
})({
  "/Users/fengshuan/Desktop/workspace/forestpack/src/index.js": function (
    require,
    module,
    exports
  ) {
    "use strict";

    var _greeting = require("./greeting.js");

    document.write((0, _greeting.greeting)("森林"));
  },
  "./greeting.js": function (require, module, exports) {
    "use strict";

    Object.defineProperty(exports, "__esModule", {
      value: true,
    });
    exports.greeting = greeting;

    function greeting(name) {
      return "你好" + name;
    }
  },
});

和上面用webpack打包生成的js文件作下对比,是不是很相似呢?

来吧!展示

我们在dist目录下创建index.html文件,引入打包生成的bundle.js文件:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script src="./bundle.js"></script>
  </body>
</html>

此时打开浏览器:

如你所愿,得到了我们预期的结果~

总结

通过对webpack构建流程的分析以及实现了一个简易的forestpack,相信你对webpack的构建原理已经有了一个清晰的认知!(当然,这里的forestpackwebpack相比还很弱很弱,,,,)

参考

本文是看过极客时间程柳锋老师的「玩转 webpack」课程后整理的。这里也十分推荐大家去学习这门课程~

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

 相关推荐

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

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

发布于: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次阅读
 目录