丐版Webpack的实现(上)

写在最前

看官们好,我叫JetTsang,之前都是在掘金潜水来着,现在偶尔做一些内容输出吧。

关于webapck

webpack是现在前端开发当中打交道最多的角色,初次接触时,难免被它的配置项、概念所困扰。

webpack对于很多人来说是一个黑盒工具(你并不知道里面的实现),掌握基本的配置,和一些优化相关的配置就可以处理大部分日常工作的内容

而本文(上篇)将从webpack构建产物分析、源码分析再到代码框架的搭建讲述如何实现一个丐版的webpack。

webpack为啥能成为前端工具链的核心角色

我认为主要原因是解决了如下痛点:

  1. 模块化,支持CommonJs、ESModule等模块化规范的同时,将各种资源,如字体,图片,css等也视为模块
  2. 生态系统,众多的Plugins/Loaders使得前端开发更加规范、方便、高效
  3. 打包构建,前端CI/CD过程的重要工具

可以说wbpack是前端工程化的利器,我们编写的代码,经过webpack的处理,能成功在浏览器、node等运行时环境上执行

回顾webpack的使用

前面了解webpack 的作用,现在回顾一下webpack的使用

本着极简实现的原则,这里只针对js,以及使用commonJs规范

  1. 首先webpack安装
bash 复制代码
npm init
npm install webpack webpack-cli --save-dev
  1. 根目录下创建webpack.config.js文件
javascript 复制代码
// webpack.config.js 
const path = require('path');

module.exports = {
  entry: './src/index.js', // 入口文件
  output: {
    filename: 'bundle.js', // 输出文件名
    path: path.resolve(__dirname, 'dist') // 输出目录
  }
};

这里涉及到一些概念,可以自行回顾

  • entry
  • output
  • loader
  • plugin
  1. package.json当中配置scripts,用来启动webpack
json 复制代码
{
    "scripts": { 
        "build:dev": "webpack --mode", 
        "build": "webpack --mode production" 
    },
}
  1. 编写一些js代码
javascript 复制代码
// src/index.js

const bar = require('./bar');

const foo = require('./foo');

console.log(bar.add(1,1))

console.log(foo.name)

// src/bar.js
function add(a, b) {
    return a + b;
}

module.exports = {
    add,
};

// src/foo.js
const name = "JetTsang"

exports.name = name

此时的依赖关系为:

  1. 打包,在终端中执行webpack命令 这里使用development模式,默认使用devtool:"eval",方便我们看打包产物
bash 复制代码
npm run build:dev

此时的目录结构大概是这样子

lua 复制代码
my-project/
├── src/
│   ├── index.js
│   ├── foo.js
│   └── bar.js
├── dist/
├── node_modules/
├── package.json
├── package-lock.json
└── webpack.config.js

再来看看产出的结果:

bundle.js分析

分析一下产出结果:

  • 首先是一个自执行函数,即(function() { ... })(),这是为了创建一个独立的作用域,避免变量污染。 这里写成箭头函数

  • 在函数内部,首先定义了一个__webpack_modules__对象,它包含了所有被打包的模块。

这里对换行做了处理,方便阅读 可以看到这个对象是以路径作为keyvalue则是一个箭头函数:(module,exports,webpack_require)=>{eval(/*你写的代码*/)}

eval执行的是我们打包好的代码,代码里用到的commonJs规范里的requiremoduleexports 正是通过这个函数的入参传入

ps: 为了避免混淆,require函数被webpack处理成它自己的__webpack_require__

  • 函数内部,接下来是一个__webpack_module_cache__对象,用于缓存已加载的模块,避免重复加载 以及解决模块之间循环引用造成的问题。

  • 紧接着定义了一个__webpack_require__函数,它是Webpack的模块加载器。入参moduleId,即为路径 ,同时通过闭包引用了前面的__webpack_module_cache__

  • 最后一行,通过调用__webpack_require__("./src/index.js")来加载入口模块

以上这部分可以称作是webpack的运行时

webpack的工作流程

对webpack的产物有了一定了解之后,再梳理一下webpack的工作流程,就可以动手开始实现了。

那么webpack的工作流程大致可以分为如下

  • 获取配置(入口/loaders/plugins等等)生成compiler
  • 解析入口,调用对应的loader,构建AST语法树,同时整理出一份依赖图,在必要的时机调用loader/plugin
  • 将产物放到配置好的位置

参考webpack的设计框架

要实现它,我们首先需要参考一下它的源码。 找到node_modules/webpack/package.json,这里的main字段描述了它的入口

在入口lib/index.js当中可以看到这里做了合并导出

其核心是在lib/webpack.js,webpack是一个函数,返回的是一个compiler实例

这个compiler主要就是基于事件流来管理整个打包流程的,也就是webpack的核心,

它里面也有生命周期钩子hook,在打包的过程当中,可以在适当的生命周期去call对应的hook,这种解耦的关系就是webpack的plugins系统,这里的生命周期可以理解为跟Vue/React类似。

事件的发布和订阅使用了tapable这个库,它更专注于nodeJs环境

再来看看里面的核心方法:run,它就是用来启动编译的

在run方法当中,核心是这个run箭头函数,里面调用了this.compile去编译代码

再来看看 this.compile 当中,真正干活的是Compilation

这个Compilation是核心模块,这里面的实现比较复杂,主要是负责每一个具体的构建过程,它主要的功能是:

  • 资源管理(根据配置从入口模块开始逐步解析处理所有的资源)、

  • 依赖解析(构建依赖图 moduleGraph)、

  • 打包输出(根据chunkGraph产出),

  • 还有错误处理以及插件系统(通过调用对应时机的hook)

可以说compiler是整个webpack的编译器实例,负责读取配置、管理插件,而compilation则是每次构建的具体过程,负责管理资源、解析依赖、生成输出文件。

关系示意图大概如此:

lua 复制代码
       +-------------------+
       |    Compiler      |
       |                   |
       |   - Compilation 1 |
       |   - Compilation 2 |
       |   - ...           |
       +-------------------+

搭建代码框架

让我们在项目中新建lib目录,里面是我们实现的webpack源码

里面的代码组织如下

lib/webpack.js

javascript 复制代码
const { Compiler } = require('./Compiler.js')
function webpack(webpackOptions) { 
    const compiler = new Compiler()
    return compiler; 
}
module.exports = webpack

lib/Compiler.js

javascript 复制代码
class Compiler {

    constructor(config) {

        this.entry = config.entry;

        this.output = config.output;

        this.module = config.module;

        this.plugins = config.plugins;
        // 利用tapable定义一些hook
        this.hooks = {
            run: new SyncHook(), //会在编译刚开始的时候触发此run钩子
            done: new SyncHook(), //会在编译结束的时候触发此done钩子 };
        }
    run(){
    // 这里创建Compilation实例
        const compilation = new Compilation();
    }
}
module.exports = {

    Compiler

}

lib/Compilation.js

javascript 复制代码
class Compilation {
    constructor({ module, output }) {
        this.loaders = module.rules;
        this.output = output;
        this.graph = [];
    }
    // 这里开始构建
    build(){
    
    }

接着在根目录下,新建packing.js,这个是用来启动启动手写的webpack

javascript 复制代码
const webpack = require("./lib/webpack"); //手写webpack
const webpackOptions = require("./webpack.config.js"); //这里一般会放配置信息
const compiler = webpack(webpackOptions);

compiler.run((err, stats) => {
  console.log(err);
});

此时项目目录结构如下:

最后是依赖介绍

虽然说是手写,但有些轮子不需要我们去造,着重了解webpack的实现即可

  • tapable:之前提到的,用来做事件流的管理,是插件系统的核心

  • @babel/parser:babel 提供的 Javascript 代码解析器。它可以将 Javascript 代码转换为 ast(抽象语法树),方便后续的代码处理和转换

  • @babel/traverse:用于对输入的抽象语法树(ast)进行遍历。在这里主要是定位到require,从而构建依赖图

  • @babel/core:babel 的核心库,它提供了 babel 的编译器和 API 接口,用于将源代码转换为目标代码

  • @babel/preset-env:可根据配置的目标浏览器或者运行环境来自动将 ES6 + 的代码转换为 ES5

  • ejs: 用于生成模板代码或者读取模板文件,上面的webpack运行时就需要这个依赖去读取

zsh 复制代码
npm install -D @babel/parser @babel/traverse @babel/core @babel/preset-env ejs tapable 

结尾

在本文当中,为了实现webpack,介绍了webpack的产物和源码思路,并且在最后准备了我们需要的代码框架

在下篇当中,会去逐步的实现一个迷你版本的webpack。

相关推荐
前端大卫30 分钟前
Vue3 + Element-Plus 自定义虚拟表格滚动实现方案【附源码】
前端
却尘1 小时前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare1 小时前
浅浅看一下设计模式
前端
Lee川1 小时前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix1 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人1 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl1 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅1 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人2 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼2 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端