webpack 执行流程 — 实现 myWebpack

前言

实现 myWebpack 主要是为了更好的理解,webpack 中的工作流程,一切都是最简单的实现,不包含细节内容和边界处理,涉及到 ast 抽象语法树和编译代码部分,最好可以打印出来观察一下,方便后续的理解。

react 项目中的 webapck

为了更好的了解 Webpack 的执行流程,我们可以先通过观察 react 项目结构中,有关 webpack 的一些内容。当然我们得先拥有一个新的项目,可以通过下面的步骤得到:

  • 使用 【create-react-app 项目名称】命令创建项目
  • 进入对应的项目目录,运行 npm run eject 命令拉取,react 项目中和 webapck 相关的配置
    相比于正常的 react 项目,会多出来两个文件目录分别是 configscripts

image.png

config 目录下主要存放的是 webpack 相关的配置内容:

image.png

scripts 目录下主要存放的是在 pakage.json 中存放的 3 个默认 script 脚本相关的内容:

image.png
image.png

主要看 scripts 目录下的 build.js 文件,这里面引入了 webpack 并且调用 webpack(config) 得到了 compiler 对象,最后使用了 compiler.run(callback) 的方式开始进行打包,代码中具体位置如下:

在这里插入图片描述

image.png

Webpack 执行流程

通过 react 项目中的目录结构结合以及 webpack 中的 Node 接口相关内容,可得到以下几个阶段:

  • 解析配置参数 —— 合并 shell 传入和 webpack.config.js 文件配置参数
  • 初始化 Compiler —— 通过 webpack(config) 得到 Compiler 对象,并注册所有配置插件,插件监听 webpack 构建生命周期的事件节点,做出相应处理
  • 开始编译 —— 调用 Compiler 对象 run() 方法开始执行编译
  • 确定入口 —— 根据配置的 entry 找出所有入口文件,开始解析文件,并构建 AST 语法树,找出依赖模块,进行递归处理
  • 编译模块 —— 递归中根据文件类型和 loader 配置,调用所有配置的 loader 对文件进行转换,再找出该模块依赖的模块,再递归本步骤,直到所有入口依赖的文件都经过了本步骤的处理
  • 完成模块编译 —— 模块编译结束后,得到每个文件结果,包含每个模块以及他们之间的依赖关系
  • 输出资源 —— 根据 entry 或分包配置生成代码块 chunk,再把每个 chunk 转换成一个单独的文件加入到输出列表

    PS:输出资源 这一步是修改输出内容的最后机会

  • 输出完成 —— 根据配置确定输出路径和命名,输出所有 chunk 到文件系统

核心流程图示:

实现 myWebpack

准备工作

根据 react 项目中的目录结构,可以得到一个简单 my-webpac 的项目结构:

  • config 目录 —— 存放的是 webpack.config.js 相关配置
  • script 目录 —— 存放的是 script 脚本需要执行的 js 文件
  • lib 目录 —— 存放的就是 myWebpack 库(需要自己实现)
  • src 目录 —— 就是 webpack.config.js 中默认的入口文件目录,其中的 index.js 为入口文件,其他的 js 文件均属于要测试打包的 js 模块

在后面的内容中为了更好的实现模块化,目录结构可能会稍微进行修改,最初的文件结构如下:

my-webpack
├─ config
│  └─ webpack.config.js
├─ lib
│  └─ myWebpack
│     └─ index.js
├─ package-lock.json
├─ package.json
├─ script
│  └─ build.js
└─ src
   ├─ add.js
   ├─ desc.js
   └─ index.js

开始实现

简单配置 config 目录下的 webpack.config.js

// config/webpack.config.js

module.exports = {
    entry: "./src/index.js",
    output: {
        path: 'dist',
        filename: 'index.js'
    }
};

实现 script 目录下的 build.js

这里只需要引入 myWebpack.jswebapck.config.js 文件,通过把配置内容 config 传入 myWebpack() 方法,并执行得到 compiler 对象,最终通过 compiler.run() 开始执行打包的处理程序

// script/build.js

const myWebpack  = require('../lib/myWebpack'); // 这里相当于 require('../lib/myWebpack/index.js')
const config  = require('../config/webpack.config.js');

//  获得 compiler 对象
const compiler = myWebpack(config);

// 开始打包
compiler.run();

实现 lib 目录下的 myWebpack 的具体内容(即其目录下的 index.js

根据 build.js 中对 myWebpack 使用方式,可以知道在 myWebpack 必然是一个 function 且,其返回值必须是 Compiler 类的实例对象,毕竟被称为 compiler 对象。在这就出现了一个 Compiler 类的相关内容,为了更好的模块化,我们在 lib/myWebpack 目录下新建 compiler.js 文件,里面专门实现 Compiler 类的相关逻辑。

所以,在 lib/myWebpack/index.js 中的处理就是实现 myWebpack 函数,引入 Compiler 类并把 new Compiler(config) 的结果进行返回即可:

// lib/myWebpack/index.js

const Compiler = require('./Compiler.js')

function myWebpack(config) {
  return new Compiler(config)
}

module.exports = myWebpack

实现 lib 目录下 myWebpack 中的 compiler.js 内容

根据 build.js 中对 compiler 对象的使用方式,compiler.js 中必然会存在 Compiler 类 ,并且肯定存在 run() 方法,而且 run() 方法中需要处理的几件事可以归纳为:

  • 根据 entry 配置中的路径,将文件解析成 ast 抽象语法树
  • 根据 ast 收集依赖存放自定义 deps 对象上
  • 根据 ast 编译成可以在浏览器上正常运行的 code 内容
  • 以及把编译好的 code 通过 output 配置中的 pathfilename 写入到文件系统
    其中,前三步属于编译解析的内容,因此,具体逻辑我们可以抽离到 lib/myWebpack/parser.js 中实现并向外暴露对应内容即可,并且放在 Compiler 类里面的 build()方法统一处理,最后一步输出资源可以抽离到 Compiler 类里面的 generate()方法中。
// lib/myWebpack/compiler.js

const { getAst, getDeps, getCode } = require('./parser.js')
const fs = require('fs')
const path = require('path')

class Compiler {
  constructor(options = {}) {
    // webpack 配置对象
    this.options = options
    // 所有依赖的容器
    this.modules = []
  }

  // 启动打包
  run() {
    // 获取 options 中的路径
    const filePath = this.options.entry

    // 首次构建,获取入口文件信息
    const fileInfo = this.build(filePath)

    // 保存文件信息
    this.modules.push(fileInfo)

    // 遍历所有依赖
    this.modules.forEach((fileInfo) => {

      // 获取当前文件所有依赖: { relativePath: absolutePath }
      const deps = fileInfo.deps
      for (const relativePath in deps) {
        // 获取对应绝对路径
        const absolutePath = deps[relativePath]
        // 对依赖文件进行打包处理
        const fileInfo = this.build(absolutePath)
        // 将打包后的结果保存到 modules 中,方便后面进行处理
        this.modules.push(fileInfo)
      }
      
    })

    // 将 modules 数组整理成更好的依赖关系图
    /*
      {
        'index.js': {
            'code': 'xxx',
             'deps': {
                [relativePath]: [absolutePath]
             } 
        }
      }
    */
    const depsGraph = this.modules.reduce(function (graph, module) {
      return {
        ...graph,
        [module.filePath]: {
          code: module.code,
          deps: module.deps,
        },
      }
    }, {})

    // 根据依赖关系图构建输出内容
    this.generate(depsGraph)
  }

  // 开始构建
  build(filePath) {
    // 将文件解析成 ast 抽象语法树
    const ast = getAst(filePath)

    // 根据 ast 收集依赖:{ relativePath: absolutePath }
    const deps = getDeps(ast, filePath)

    // 根据 ast 编译成 code
    const code = getCode(ast)

    return {
      filePath, // 当前文件路径
      deps, // 当前文件的所有依赖
      code, // 当前文件解析过的代码
    }
  }

  // 生成输出资源
  generate(depsGraph) {
    const bundle = `
        (function(depsGraph){

            // require 加载入口文件
            function require(module){

                // 定义暴露对象
                var exports = {};

                // require 内部在定义 localRequire 是为了让 require 递归
                function localRequire(relativePath){
                    // 找到引入模块的绝对路径,通过 require 进行加载
                    return require(depsGraph[module].deps[relativePath]);
                }

                (function(require, exports, code){
                    eval(code);
                })(localRequire, exports, depsGraph[module].code);

                // 作为 require 的返回值 —— 让后面的 require 函数能得到被暴露的内容
                return exports;
            }

            require('${this.options.entry}');

        })(${JSON.stringify(depsGraph)});
      `
    const { output } = this.options
    const dirPath = path.resolve(output.path)
    const filePath = path.join(dirPath, output.filename)

    // 如果指定目录不存在就创建目录
    if (!fs.existsSync(dirPath)) {
      fs.mkdirSync(dirPath)
    }

    // 写入文件
    fs.writeFileSync(filePath, bundle.trim(), 'utf-8')
  }
}

module.exports = Compiler
generate() 方法中 bundle 变量内容的解释
  • 外部包裹一个立即执行的匿名函数,主要就是为了生成独立作用域,实现 js 的模块化
  • 其中的 require() 方法,就是通过 eval 函数去执行,被编译后的 code,因为被编译后的 code 是字符串形式的 js 代码
  • require() 方法中的 localRequire () 方法实际上执行的还是 require() 方法本身,但对当前模块路径做了一定处理,这里其实就是递归
  • require() 方法中还有一个立即执行的匿名函数,接收三个参数:require, exports, code,其中 code 参数容易理解,但是为什么我们需要传递 require, exports 参数呢?
    • 这一点我们可以通过看被编译之后的 code 的内容就知道了,例如入口文件 index.js 和它里面引入 add.js 的编译结果 code 如下:
// index.js 内容编译结果 => 这里需要使用到 require 方法,因此外部必须传入
"'use strict';
var _add = _interopRequireDefault(require('./add.js'));
var _desc = _interopRequireDefault(require('./desc.js'));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
console.log('add = ', (0, _add['default'])(1, 2));
console.log('desc = ', (0, _desc['default'])(3, 1));"

// add.js 内容编译结果 => 这里需要使用到 exports 对象,因此外部必须传入
"'use strict';
Object.defineProperty(exports, '__esModule', {  value: true});
exports['default'] = void 0;
function add(x, y) {  return x + y;}
var _default = add;
exports['default'] = _default;"

实现 lib 目录下 myWebpack 中的 parser.js 内容

这里需要做的就是下面的三件事:

  • 根据 entry 配置中的路径,将文件解析成 ast 抽象语法树,需要借助 @babel/parser 中的 parse 方法
  • 根据 ast 收集依赖存放自定义 deps 对象上,借助 @babel/traverse 遍历 ast 中的 program.body,方便在特定时机收集依赖
  • 根据 ast 编译成可以在浏览器上正常运行的 code 内容,需要借助 @babel/core 中的 transformFromAst 方法
// lib/myWebpack/parser.js

const babelParser = require('@babel/parser')
const { transformFromAst } = require('@babel/core')
const babelTraverse = require('@babel/traverse').default
const path = require('path')
const fs = require('fs')

const parser = {
  getAst(filePath) {
    // 通过 options.entry 读入口文件
    const file = fs.readFileSync(filePath, 'utf-8')

    // 将入口文件内容解析成 ast —— 抽象语法树
    const ast = babelParser.parse(file, {
      sourceType: 'module', // 处理被解析文件中的 ES module
    })

    return ast
  },

  getDeps(ast, filePath) {
    // 获取到文件所在文件夹的路径
    const dirname = path.dirname(filePath)
    // 存储依赖的容器
    const deps = {}

    // 根据 ast 收集依赖
    babelTraverse(ast, {
      // 内部会遍历 ast 中的 program.body,根据对应的语句类型进行执行
      // ImportDeclaration(code) 方法会在 type === "ImportDeclaration" 时触发
      ImportDeclaration({ node }) {
        // 获取当前文件的相对路径
        const relativePath = node.source.value
        // 添加依赖:{ relativePath: absolutePath }
        deps[relativePath] = path.resolve(dirname, relativePath)
      },
    })
    return deps
  },

  getCode(ast) {
    // 编译代码: 将浏览器中不能被识别的语法进行编译
    const { code } = transformFromAst(ast, null, {
      presets: ['@babel/preset-env'],
    })
    return code
  },
}

module.exports = parser

最终的目录结构

my-webpack
├─ config
│  └─ webpack.config.js
├─ lib
│  └─ myWebpack
│     ├─ compiler.js
│     ├─ index.js
│     └─ parser.js
├─ package-lock.json
├─ package.json
├─ README.md
├─ script
│  └─ build.js
└─ src
   ├─ add.js
   ├─ desc.js
   └─ index.js

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

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

相关文章

Hadoop生态圈框架部署(五)- Zookeeper完全分布式部署

文章目录 前言一、Zookeeper完全分布式部署(手动部署)1. 下载Zookeeper2. 上传安装包2. 解压zookeeper安装包3. 配置zookeeper配置文件3.1 创建 zoo.cfg 配置文件3.2 修改 zoo.cfg 配置文件3.3 创建数据持久化目录并创建myid文件 4. 虚拟机hadoop2安装并…

Python小白学习教程从入门到入坑------第二十九课 访问模式(语法进阶)

目录 一、访问模式 1.1 r 1.2 w 1.3 1.3.1 r 1.3.2 w 1.3.3 a 1.4 a 一、访问模式 模式可做操作若文件不存在是否覆盖r只能读报错-r可读可写报错是w只能写创建是w可读可写创建是a只能写创建否,追加写a可读可写创建否,追加写 1.1 r r&…

巡检任务管理系统(源码+文档+部署+讲解)

本文将深入解析“巡检任务管理系统”的项目,探究其架构、功能以及技术栈,并分享获取完整源码的途径。 系统概述 巡检任务管理、巡检抽查、巡检任务随机分派等功能 本项目名称为巡检管理系统,是对巡检工作进行数字化管理的系统。该系统适用…

自动驾驶系列—自动驾驶车辆的姿态与定位:IMU数据在复杂环境中的关键作用

🌟🌟 欢迎来到我的技术小筑,一个专为技术探索者打造的交流空间。在这里,我们不仅分享代码的智慧,还探讨技术的深度与广度。无论您是资深开发者还是技术新手,这里都有一片属于您的天空。让我们在知识的海洋中…

如何运营Github Org

目录 前言 正文 关于分支保护 特别说明 如何在Windows环境下配置GitHub Desktop GPG签名? 推荐分支保护选择 关于good first issue 如何设置good first issue? 关于Project 尾声 🔭 Hi,I’m Pleasure1234🌱 I’m currently learni…

odrive代码阅读笔记

电机参数 电流环带宽 atan2 #include "float.h" #define MACRO_MAX(x, y) (((x) > (y)) ? (x) : (y)) #define MACRO_MIN(x, y) (((x) < (y)) ? (x) : (y)) #define f_abs(x) ((x > 0) ? x : -x) // based on https://math.stackexchange.com/a/11050…

笔记本怎么开启TPM2.0_笔记本开启TPM2.0教程(不同笔记本开启tpm2.0方法)

在win11最低要求是提示&#xff0c;电脑必须满足 TPM 2.0&#xff0c;并开需要开启TPM 才能正常安装windows11系统&#xff0c;有很多笔记本的用户问我&#xff0c;笔记本怎么开启tpm功能呢&#xff1f;下面小编就给大家详细介绍一下笔记本开启tpm功能的方法。 如何确认你笔记本…

ModuleNotFoundError: No module named ‘_ssl‘ centos7中的Python报错

报错 ModuleNotFoundError: No module named ‘_ssl’ 解决步骤&#xff1a; 1.下载openssl wget https://www.openssl.org/source/openssl-3.0.7.tar.gz tar -zxvf openssl-3.0.7.tar.gz cd openssl-3.0.72.编译安装 ./config --prefix/usr/local/openssl make make install3…

迁移学习相关基础

迁移学习 目标 将某个领域或任务上学习到的知识或模式应用到不同但相关的领域或问题中。 主要思想 从相关领域中迁移标注数据或者知识结构、完成或改进目标领域或任务的学习效果。 概述 Target data&#xff1a;和你的任务有直接关系的数据&#xff0c;但数据量少&#xff…

ReactPress系列—Next.js 的动态路由使用介绍

ReactPress Github项目地址&#xff1a;https://github.com/fecommunity/reactpress 欢迎提出宝贵的建议&#xff0c;感谢Star。 Next.js 的动态路由使用介绍 Next.js 是一个流行的 React 框架&#xff0c;支持服务端渲染、静态站点生成和动态路由等功能&#xff0c;极大地简化…

一文熟悉新版llama.cpp使用并本地部署LLAMA

0. 简介 最近是快到双十一了再给大家上点干货。去年我们写了一个大模型的系列&#xff0c;经过一年&#xff0c;大模型的发展已经日新月异。这一次我们来看一下使用llama.cpp这个项目&#xff0c;其主要解决的是推理过程中的性能问题。主要有两点优化&#xff1a; llama.cpp …

yolov8涨点系列之轻量化主干网络替换

文章目录 YOLOv8 替换成efficientvit轻量级主干网络的好处计算效率提升模型部署更便捷方便模型移植 模型可扩展性增强便于集成其他模块支持模型压缩技术 主干网络替换1.创建yolov8_efficeintVit.py2.修改task.py(1)引入创建的efficientViT文件(2)修改_predict_once函数(3)修改p…

python代码打包exe文件(可执行文件)

一、exe打包 1、构建虚拟环境 conda create -n env_name python3.8 #env_name,python根据自己需求修改2、保存和安装项目所需的所有库 pip freeze > requirements.txt3、虚拟环境安装项目包、库 pip install -r requirements.txt4、安装pyinstaller pip install pyinst…

scala学习记录,Set,Map

set&#xff1a;集合&#xff0c;表示没有重复元素的集合&#xff0c;特点&#xff1a;唯一 语法格式&#xff1a;val 变量名 Set [类型]&#xff08;元素1&#xff0c;元素2...&#xff09; 可变不可变 可变&#xff08;mutable&#xff09;可对元素进行添加&#xff0c;删…

ai外呼机器人的作用有哪些?

ai外呼机器人具有极高的工作效率。日拨打成千上万通不是问题&#xff0c;同时&#xff0c;机器人还可以快速筛选潜在客户&#xff0c;将更多精力集中在有价值的客户身上&#xff0c;进一步提升营销效果。183-3601-7550 ai外呼机器人的作用&#xff1a; 1、搭建系统&#xff0c…

Matlab实现鲸鱼优化算法优化随机森林算法模型 (WOA-RF)(附源码)

目录 1.内容介绍 2.部分代码 3.实验结果 4.内容获取 1内容介绍 鲸鱼优化算法&#xff08;Whale Optimization Algorithm, WOA&#xff09;是受座头鲸捕食行为启发而提出的一种新型元启发式优化算法。该算法通过模拟座头鲸围绕猎物的螺旋游动和缩小包围圈的方式&#xff0c;在…

Linux基础4-进程5(程序地址空间详解)

上篇文章:Linux基础4-进程4&#xff08;环境变量&#xff0c;命令行参数详解&#xff09;-CSDN博客 本章重点&#xff1a; 1 重新理解c/c地址空间 2 虚拟地址空间 一. c/c地址空间 地址空间布局图: 运行下列代码&#xff0c;进行观察 #include <stdio.h> #include <…

本地连接IP地址的自主设置指南‌

在数字化时代&#xff0c;网络连接已成为我们日常生活和工作中不可或缺的一部分。无论是家庭网络还是企业网络&#xff0c;正确配置IP地址是确保网络畅通无阻的基础。IP地址&#xff0c;即互联网协议地址&#xff0c;是网络中每个设备的唯一标识。掌握如何自主设置本地连接的IP…

对 fn.apply(this, arguments) 的使用还在疑惑?快进来看看它的设计含义及常见使用场景吧~

&#x1f64c; 如文章有误&#xff0c;恳请评论区指正&#xff0c;谢谢&#xff01; ❤ 写作不易&#xff0c;「点赞」「收藏」「转发」 谢谢支持&#xff01; 背景 近期在研究高阶函数封装的过程中&#xff0c;看到 fn.apply(this, arguments) 的出镜率非常高&#xff0c;而如…

【ReactPress】React + antd + NestJS + NextJS + MySQL 的简洁兼时尚的博客网站

ReactPress 是使用React开发的开源发布平台&#xff0c;用户可以在支持React和MySQL数据库的服务器上架设属于自己的博客、网站。也可以把 ReactPress 当作一个内容管理系统&#xff08;CMS&#xff09;来使用。 前言 此项目是用于构建博客网站的&#xff0c;包含前台展示、管理…