手写一个Webpack,带你了解构建流程

news2024/11/26 21:28:59

如果对前端八股文感兴趣,可以留意公重号:码农补给站,总有你要的干货。

前言

Webpack是一个强大的打包工具,拥有灵活、丰富的插件机制,网上关于如何使用WebpackWebpack原理分析的技术文档层出不穷。最近自己也是发现面试官问到Webpack特别喜欢问构建流程,那么本文主要探讨,Webpack的一次构建流程中,主要干了哪些事儿,带领您手写一个打包工具。

一脸懵逼.webp

真是卷...

本文主要讲的是基本的构建和输出打包,不包含treeshaking、热更新等其他功能的内容。

基本架构

webpack1.png

构建流程

准备阶段

从配置文件中读取到配置参数,传入配置参数实例化一个Compiler编译器,执行编译器的run方法开始编译。

 
const path = require('path');
const Compiler = require('../lib/Compiler.js');
let config = require(path.resolve('webpack.config.js')); // 从webpack.config.js中获取配置

let compiler = new Compiler(config); // 实例化一个Compiler编译器

compiler.run();  // 执行编译器的run方法

开始编译

 
class Compiler {
  constructor(config) {
    this.config = config; // 配置文件
    this.entryId; // 入口文件名字
    this.modules = {}; // 依赖模块的集合
    this.entry = config.entry; // 入口路径
    this.root = process.cwd();
  }

  run() {
    this.buildModule(path.resolve(this.root, this.entry), true);
  }
}

Compiler初始化阶段就存储了配置文件config、入口路径entry、根路径root,定义了依赖模块的集合modules和入口文件名entryId。其中后续我们解析到的所有模块内容都会存储在modules

run方法从配置中获取入口文件,从入口文件开始buildModule

 
buildModule(modulePath, isEntry) { // modulePath 模块路径  isEntry是否是入口文件
    // 拿到模块内容
    let source = this.getSource(modulePath); 
    let moduleName = './' + path.relative(this.root, modulePath); // src/index.js
    if (isEntry) {
        // 如果是入口文件获取入口文件名
      this.entryId = moduleName;
    }
    
    // 开始解析文件依赖
    const { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName));
    this.modules[moduleName] = sourceCode;
    dependencies.forEach(dep => { // 递归加载模块
      this.buildModule(path.join(this.root, dep), false);
    })
 }
 
 parse(source, parentPath) { // 解析源码返回依赖列表  parentPath ./src
 
    // 解析源码获取ast语法树
    let ast = babylon.parse(source);
    let dependencies = [];
    
    // 解析ast语法树获取关联的依赖
    traverse(ast, {
      CallExpression(p) {
        let node = p.node;
        if (node.callee.name === 'require') {
          node.callee.name = '__webpack_require__';
          let moduleName = node.arguments[0].value; // 取到模块的引用名字
          moduleName = moduleName + (path.extname(moduleName) ? '' : '.js');
          moduleName = './' + path.join(parentPath, moduleName);
          dependencies.push(moduleName);
          node.arguments = [t.stringLiteral(moduleName)];
        }
      }
    })
    let sourceCode = generator(ast).code;
    return {
      sourceCode, //  源码
      dependencies // 关联的依赖
    };
  }

过程如下: buildModule中接收两个参数modulePath模块路径、isEntry是否是入口文件。拿到模块文件中内容,并获取入口文件名称。 parse中也是接收两个参数source文件内容,以及父路径parentPath。将文件内容通过babylon插件解析成AST语法树,然后通过@babel/traverse解析语法树获取其关联的依赖文件。递归解析依赖文件将所有模块都存入modules中。

打包输出

 
run() {
    this.buildModule(path.resolve(this.root, this.entry), true);
    // 发射一个文件
    this.emitFile();
  }

  emitFile() { // 发射一个文件
    // 从配置文件中获取打包输入路径和文件名
    let main = path.join(this.config.output.path, this.config.output.filename);
    // 获取模板
    let templateStr = this.getSource(path.join(__dirname, 'main.ejs'));
    let code = ejs.render(templateStr, { entryId: this.entryId, modules: this.modules });
    this.assets = {};
    this.assets[main] = code;
    fs.writeFileSync(main, this.assets[main]);
  }

再此之间前我们的文件都已经解析好了存在modules中。从入口文件获取打包输出的文件路径和文件名,然后获取一个打包输出的文件模板,文件模板是要一个.ejs文件。

 
// main.ejs

(() => {
    var __webpack_modules__ = ({
      <%for(let key in modules){%>
        "<%-key%>":
        ((module, exports, __webpack_require__) => {
  
          eval(`<%-modules[key]%>`);
        }),
      <%}%>
    });
    var __webpack_module_cache__ = {};
    function __webpack_require__(moduleId) {
      var cachedModule = __webpack_module_cache__[moduleId];
      if (cachedModule !== undefined) {
        return cachedModule.exports;
      }
      var module = __webpack_module_cache__[moduleId] = {
        exports: {}
      };
      __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
      return module.exports;
    }
    var __webpack_exports__ = __webpack_require__("<%-entryId%>");
  })()
    ;

文件模板中我们可以看到,其实里面是一个自我执行函数,从入口<%-entryId%>开始依次从modules中获取文件代码内容,并执行。

最终生成assets,将每个assets打包到指定位置。

loader

loader本质上是一个函数,参数content是一段字符串,存储着文件的内容,最后将loader函数导出就可以提供给webpack使用。

我们来实现一个less-loaderstyle-loader:

 
// less-loader

const less = require('less'); // npm install less -D

function loader(content) {
  let css = '';
  less.render(content, (err, c) => {
    css= c.css;
  })
  return css;
}

module.exports = loader;

 
// style-loader
function loader(content) {
  let style = `
    let style = document.createElement('style');
    style.innerHTML = ${JSON.stringify(content)}
    document.head.appendChild(style)
  `;
  return style;
}

module.exports = loader;

所以我们编译阶段获取文件内容的时候就需要匹配文件名来判断是否需要使用该loader

 
getSource(modulePath) {
    // 获取我们配置的rules
    let rules = this.config.module.rules || [];
    // 获取到指定路径的文件内容
    let content = fs.readFileSync(modulePath, 'utf-8');
     // 循环匹配,拿到每个规则来处理
    for (let i = 0; i < rules.length; i++) {
      let rule = rules[i];
      let { test, use = [] } = rule;
      let len = use.length - 1;
      // test正则匹配文件路径
      if (test.test(modulePath)) {
        function normalLoader() {
          let loader = require(use[len--]);
          if (loader) {
            content = loader(content);
          }
          if (len >= 0) {
            normalLoader();
          }
        }
        normalLoader();
      }
    }
    return content;
  }

总结

e13f329ac1174d49bb8b6806c5d5dee7~tplv-k3u1fbpfcp-jj-mark_3024_0_0_0_q75 (1).webp

  1. 初始化参数。获取用户在webpack.config.js文件配置的参数
  2. 开始编译。初始化Compiler对象,执行run方法开始编译。
  3. 从入口文件出发,获取文件内容,如果配置了loader就匹配对应的loader来改变文件内容,开始解析文件构建AST语法树,找到依赖项,递归下去,并且将每个模块存储下来。
  4. 完成编译并输出。递归结束,得到每个文件结果,包含转换后的模块以及他们之前的依赖关系,根据entry以及output等配置生成代码块chunk
  5. 输出文件。


原文链接:https://juejin.cn/post/7298927442488197157
 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1188578.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

人大金仓物理备份异机恢复

概述 KingbaseES V8支持使用RMAN物理备份在异机环境恢复&#xff0c;通过重新克隆方式完扩展主备集群。 原集群环境&#xff1a;演示用例&#xff0c;仅供参考 查看原集群备份和物理备份路径 异机恢复 前置条件 *获取原集群物理备份文件&#xff0c;包括全量备份、增量备份…

基于springboot的医护人员排班系统 全套代码 全套视频教程

基于springboot的医护人员排班系统,springboot vue mysql (毕业论文10411字以上,共27页,程序代码,MySQL数据库) 代码获取&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/177HdCGtTvqiHP4O7qWAgxA?pwd0jlf 提取码&#xff1a;0jlf 【运行环境】 IDEA, JDK1.8, My…

后视镜为什么要检测反射率

后视镜反射率检测是评估后视镜质量的重要步骤&#xff0c;可以反映后视镜的反射效果是否满足设计要求。一般来说&#xff0c;后视镜的反射率越高&#xff0c;驾驶员观察车后的道路状况就越清晰&#xff0c;从而能够更好地判断与后方车辆的距离和速度差。 后视镜反射率检测的原理…

C++冒号的作用域

当同时定义了一个全局变量a和局部变量a&#xff1a; 结果输出了局部变量的10&#xff0c;因为程序遵循就近原则。 :: 代表全局作用域 如果想无视就近原则&#xff0c;打印全局变量的a&#xff0c;就在输出时把a的前面加两个冒号。 ::

【ChatGLM2-6B】小白入门及Docker下部署

【ChatGLM2-6B】小白入门及Docker下部署 一、简介1、ChatGLM2是什么2、组成部分3、相关地址 二、基于Docker安装部署1、前提2、CentOS7安装NVIDIA显卡驱动1&#xff09;查看服务器版本及显卡信息2&#xff09;相关依赖安装3&#xff09;显卡驱动安装 2、 CentOS7安装NVIDIA-Doc…

Nginx常用配置与命令,nginx代理转发配置

Nginx特点 高并发、高性能; 模块化架构使得它的扩展性非常好; 异步非阻塞的事件驱动模型这点和 Node.js 相似; 相对于其它服务器来说它可以连续几个月甚至更长而不需要重启服务器使得它具有高可靠性; 热部署、平滑升级; 完全开源,生态繁荣; Nginx作用 Nginx 的最重要的…

JAVA IDEA 下载

超简单步骤一&#xff1a; IntelliJ IDEA 官方下载链接 点击以上链接进入下图&#xff0c;点击下载 继续点下载&#xff0c;然后等待下载完后打开安装包即可 步骤二&#xff1a; 打开下好的安装包&#xff0c;点击Browse...我们把它下载到自己喜欢的地方&#xff08;主要是别占…

信息系统项目管理师第四版:第5章 信息系统工程

请点击↑关注、收藏&#xff0c;本博客免费为你获取精彩知识分享&#xff01;有惊喜哟&#xff01;&#xff01; 信息系统工程是用系统工程的原理、方法来指导信息系统建设与管理的一门工程技术学科&#xff0c;它是信息科学、管理科学、系统科学、计算机科学与通信技术相结合…

简析电能管理系统在某煤矿的应用

叶根胜 安科瑞电气股份有限公司 上海嘉定 201801 摘要&#xff1a;针对传统的煤矿电能管理主要是由专人人工抄表&#xff0c;存在抄收数据繁琐&#xff0c;统计困难&#xff0c;煤矿用电分析等方面数据缺乏&#xff0c;电量峰谷比不合理等问题。某煤矿应用电能管理系统&#…

11月9日星期四今日早报简报微语报早读

11月9日星期四&#xff0c;农历九月廿六&#xff0c;早报微语早读。 1、中国数字经济规模十年增至50.2万亿元&#xff0c;网民规模增至10.79亿&#xff1b; 2、世界互联网发展指数排名发布&#xff1a;中国位居第二&#xff1b; 3、中国—拉美开发性金融合作机制扩容&#x…

【修车案例】一波形一案例(10)

故障车型: 2005 teana 2.0日产 维修厂: 建兴汽车保养厂示波器诊断: 通道A – ABS霍尔传感器信号测量故障分析: 诊断计算机报错左后轮胎轮速异常, 速度与其他车轮差较大。 通过示波器量测ABS信号, 2线式霍尔传感器, 信道A正极接信号线, 负极接地线, 干扰较严重就不建议从蓄电池…

图解三傻排序 选择排序、冒泡排序、插入排序

&#xff08;1&#xff09;选择排序 // 交换 void swap(int arr[], int i, int j) {int tmp arr[i];arr[i] arr[j];arr[j] tmp; }// 选择排序 void selectionSort(int arr[],int len) {if (len < 2) return;for (int minIndex, i 0; i < len - 1; i) {minIndex i;f…

彻底改变您的用户体验设计:您需要了解的 5 个工具包和指南

问题 进行设计冲刺、设计思维工作坊期间&#xff0c;如何找到好用的UX工具&#xff1f; 市面上有很多优秀的UX书籍&#xff0c;但也有越来越多的在线 用户体验设计 工具包和方法指南详细介绍了大量的UX工具和方法&#xff0c;包括这些方法是什么、为什么要用、何时用还有怎么…

这8大优势你都不知道,你敢说你精通单元测试?

一、什么是单元测试 在计算机编程中&#xff0c;单元测试是一种软件测试方法&#xff0c;通过该方法可以测试源代码的各个单元以确定它们是否适合使用。单元是最小的可测试软件组件&#xff0c; 它通常执行单个内聚功能。单元测试就是是指对这个最小可测试组件——即单元进行检…

SAP 开发查找增强程序

参考文章https://blog.csdn.net/SAPmatinal/article/details/129987722?ops_request_misc%257B%2522request%255Fid%2522%253A%2522169949816116800225559526%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id16994981611680022555…

为什么冰酒会被视为珍品?

在某些年份&#xff0c;珍贵稀有的葡萄酒让酿酒师有了冒险的意愿&#xff0c;葡萄比平时在藤上停留更长时间&#xff0c;需要等待至少-7℃的温度&#xff0c;酿酒师需要与自然玩游戏&#xff0c;可以持续到1月&#xff0c;在罕见的情况下可以持续到2月。对于酿酒师来说&#xf…

雷电防护在线检测(监测)平台应用方案

雷电防护在线检测&#xff08;监测&#xff09;平台是一种利用云计算、物联网、传感器、智能算法等技术&#xff0c;对雷电防护设施进行实时监测、预警、分析和管理的系统。该系统可以有效提高防雷安全水平&#xff0c;降低雷电灾害风险&#xff0c;为各行各业提供全面的雷电防…

RT-Thread 12. BSP根目录下SConscript分析

(1)menuconfig 命令通过读取工程的各个Kconfig 文件&#xff0c;生成配置界面供用户配置内核&#xff0c;最后所有配置相关的宏定义都会自动保存到 BSP 目录里的rtconfig.h 文件中&#xff0c;每一个 BSP 都有一个 rtconfig.h 文件&#xff0c;也就是这个 BSP 的配置信息。 (2)…

计算机网络期末复习-Part1

1、列举几种接入网技术&#xff1a;ADSL&#xff0c;HFC&#xff0c;FTTH&#xff0c;LAN&#xff0c;WLAN ADSL&#xff08;Asymmetric Digital Subscriber Line&#xff09;&#xff1a;非对称数字用户线路。ADSL 是一种用于通过电话线连接到互联网的技术&#xff0c;它提供…