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

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

前言

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.mfbz.cn/a/125591.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

回归预测 | Matlab实现PCA-PLS主成分降维结合偏最小二乘回归预测

回归预测 | Matlab实现PCA-PLS主成分降维结合偏最小二乘回归预测 目录 回归预测 | Matlab实现PCA-PLS主成分降维结合偏最小二乘回归预测效果一览基本介绍程序设计参考资料 效果一览 基本介绍 Matlab实现PCA-PLS主成分降维结合偏小二乘回归预测&#xff08;完整源码和数据) 1.输…

【19】c++11新特性 —>线程异步

什么是异步&#xff1f; async的两种方式 //方式1 async( Function&& f, Args&&... args ); //方式2 async( std::launch policy, Function&& f, Args&&... args );函数参数&#xff1a; f:任务函数 Args:传递给f的参数 policy:可调用对象f的…

微信的通讯录联系人,有没有什么办法导出来做备份

6-10 这是可以做到的&#xff0c;并且很简单&#xff0c;对于需要把微信通讯录备份出来&#xff0c;或者离职交接的人来说&#xff0c;本文非常适合阅读&#xff0c;只是一个简单的方法&#xff0c;即可快速地把微信的通讯录搞出来&#xff0c;本质其实就是使用正确的工具就行…

C++指针访问数组 函数中用指针传参

用指针访问数组 在函数中用指针传参

2023 年最新腾讯官方 QQ 机器人(QQ 群机器人 / QQ 频道机器人)超详细开发教程

注册 QQ 开放平台账号 QQ 开放平台是腾讯应用综合开放类平台&#xff0c;包含 QQ 机器人、QQ 小程序、QQ 小游戏 等集成化管理&#xff0c;也就是说你注册了QQ 开放平台&#xff0c;你开发 QQ 机器人还是 QQ 小程序都是在这个平台进行部署上线和管理。 如何注册 QQ 开放平台账…

小程序day05

使用npm包 Vant Weapp 类似于前端boostrap和element ui那些的样式框架。 安装过程 注意:这里建议直接去看官网的安装过程。 vant-weapp版本最好也不要指定 在项目目录里面先输入npm init -y 初始化一个包管理配置文件: package.json 使用css变量定制vant主题样式&#xff0…

记误删Linux的python与yum

根据各路大神的方法整理一下自己解决的步骤 注意&#xff1a;不要手贱删python2&#xff01;想用python3就安装并用python3命令 重新安装python2 查看系统版本&#xff1a; cat /etc/redhat-release进入默认的安装地址&#xff1a;注意查看一下rpm文件是不是删干净了&#x…

Python---列表的循环遍历,嵌套

循环遍历就是使用while或for循环对列表中的每个数据进行打印输出 while循环&#xff1a; list1 [貂蝉, 大乔, 小乔]# 定义计数器 i 0 # 编写循环条件 while i < len(list1):print(list1[i])# 更新计数器i 1 for循环&#xff08;推荐&#xff09;&#xff1a; list1 [貂…

Spring Boot 3.0正式发布及新特性解读

目录 【1】Spring Boot 3.0正式发布及新特性依赖调整升级的关键变更支持 GraalVM 原生镜像 Spring Boot 最新支持版本Spring Boo 版本版本 3.1.5前置系统清单三方包升级 Ref 个人主页: 【⭐️个人主页】 需要您的【&#x1f496; 点赞关注】支持 &#x1f4af; 【1】Spring Boo…

2023.11.8 hadoop学习-概述,hdfs dfs的shell命令

目录 1.分布式和集群 2.Hadoop框架 3.版本更新 4.hadoop架构详解 5.页面访问端口 6.Hadoop-HDFS HDFS架构 HDFS副本 7.SHELL命令 8.启动hive服务 1.分布式和集群 分布式: 多台服务器协同配合完成同一个大任务(每个服务器都只完成大任务拆分出来的单独1个子任务)集 群:…

phpstudy本地快速搭建网站,实现无公网IP外网访问

文章目录 [toc]使用工具1. 本地搭建web网站1.1 下载phpstudy后解压并安装1.2 打开默认站点&#xff0c;测试1.3 下载静态演示站点1.4 打开站点根目录1.5 复制演示站点到站网根目录1.6 在浏览器中&#xff0c;查看演示效果。 2. 将本地web网站发布到公网2.1 安装cpolar内网穿透2…

使用ESP8266构建家庭自动化系统

随着物联网技术的不断发展&#xff0c;家庭自动化系统变得越来越受欢迎。ESP8266是一款非常适合于构建家庭自动化系统的WiFi模块。它小巧、低成本&#xff0c;能够实现与各种传感器和执行器的连接&#xff0c;为家庭带来智能化、便利化的体验。在本篇文章中&#xff0c;我们将向…

有关python库

官方库 #1、导入某模块 import os #2、导入OS模块中的system方法 from os import system #3、导入某模块中的孙子模块中的xx方法&#xff0c;并重命名 from module.xx.xx import xx as rename #4、导入OS中的所有模块 #不用进行OS.method(),直接method&#xff08;&#xff0…

【网络】epoll理论 + 实践(LT模式服务器和ET模式服务器)详细讲解

epoll 前言正式开始epoll相关的接口epoll_createepoll_ctlepoll_wait epoll原理硬件上的数据是怎么交给上层的创建epoll模型epoll模型中的红黑树epoll中的就绪队列回调方法前面三个接口在模型中的体现一些细节 编写epoll服务器小组件正式开始编写对epoll接口进行封装epoll_crea…

API低代码开发应用场景

什么是API低代码开发平台 API低代码开发平台是一种基于低代码开发的技术平台&#xff0c;它可以帮助企业快速构建和部署API应用程序。该平台通过提供可视化的开发工具、预定义的组件和模板、自动化的代码生成等功能&#xff0c;使得开发者可以在不需要编写大量代码的情况下&am…

【Java】Netty创建网络服务端客户端(TCP/UDP)

&#x1f60f;★,:.☆(&#xffe3;▽&#xffe3;)/$:.★ &#x1f60f; 这篇文章主要介绍Netty创建网络服务端客户端示例。 学其所用&#xff0c;用其所学。——梁启超 欢迎来到我的博客&#xff0c;一起学习&#xff0c;共同进步。 喜欢的朋友可以关注一下&#xff0c;下次更…

HTML+CSS、Vue+less+、HTML+less 组件封装实现二级菜单切换样式跑(含全部代码)

一、HTMLCSS二级菜单 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0" /><title>Document</title><…

Tcl语言:基础入门(一)

Tcl语言https://blog.csdn.net/weixin_45791458/category_12488978.html?spm1001.2014.3001.5482 Tcl语言是一种脚本语言&#xff0c;类似于Bourne shell(sh)、C shell&#xff08;csh&#xff09;、Bourne-Again Shell(bash)等UNIX shell语言。Shell程序主要作为胶水缝合其他…

【CocoaPods安装环境和流程以及各种情况】

CocoaPods 环境HomebrewRubyrbenvRubyGems 和 Bundler安装Ruby管理Ruby更新Ruby替换Ruby镜像方式1方式2 CocoaPods安装CocoaPodsCocoaPods使用安装的一些问题单元测试引用问题 参考的链接 环境 Homebrew $ brew --config *可以发现打印有下面一行&#xff1a; Homebrew Ruby: …

spring基础,编写第一个程序

spring基础 前言SpringSpring概述Spring的8大模块Spring特点学习spring6软件版本Spring的入门程序第一个Spring程序 小结 前言 控制反转&#xff0c;是面向对象编程中的一种设计思想&#xff0c;可以用来降低代码之间的耦合度&#xff0c;符合依赖倒置原则。 控制反转的核心是…