likes
comments
collection
share

丐版Webpack的实现(上)

作者站长头像
站长
· 阅读数 1

写在最前

关于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安装
npm init
npm install webpack webpack-cli --save-dev
  1. 根目录下创建webpack.config.js文件
// 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
{
    "scripts": { 
        "build:dev": "webpack --mode", 
        "build": "webpack --mode production" 
    },
}
  1. 编写一些js代码
// 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

此时的依赖关系为:

丐版Webpack的实现(上)

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

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

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

再来看看产出的结果:

丐版Webpack的实现(上)

bundle.js分析

分析一下产出结果:

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

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

这里对换行做了处理,方便阅读 丐版Webpack的实现(上) 可以看到这个对象是以路径作为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字段描述了它的入口

丐版Webpack的实现(上)

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

丐版Webpack的实现(上) 其核心是在lib/webpack.js,webpack是一个函数,返回的是一个compiler实例

丐版Webpack的实现(上)

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

丐版Webpack的实现(上)

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

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

丐版Webpack的实现(上)

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

丐版Webpack的实现(上) 在run方法当中,核心是这个run箭头函数,里面调用了this.compile去编译代码

丐版Webpack的实现(上) 再来看看 this.compile 当中,真正干活的是Compilation

丐版Webpack的实现(上)

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

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

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

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

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

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

关系示意图大概如此:

       +-------------------+
       |    Compiler      |
       |                   |
       |   - Compilation 1 |
       |   - Compilation 2 |
       |   - ...           |
       +-------------------+

搭建代码框架

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

里面的代码组织如下

lib/webpack.js

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

lib/Compiler.js

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(){
        this.compile()
    }
    compile(){
        // 这里创建Compilation实例
        const compilation = new Compilation();
    }
}
module.exports = {

    Compiler

}

lib/Compilation.js

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

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


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

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

此时项目目录结构如下:

丐版Webpack的实现(上)

最后是依赖介绍

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

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

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

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

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

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

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

npm install -D @babel/parser @babel/traverse @babel/core @babel/preset-env ejs tapable 

结尾

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

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