Webpack 构建原理

97 阅读4分钟

前端构建工具的诞生

-- images/
-- html/
-- css/
-- js/

早期的前端开发项目结构通常像上面这样,最终在 html 按适当的顺序引入 css、js 文件,随着项目的功能越来越多,会导致整个项目复杂度高、维护性差、项目极其不稳定,如代码复用性差、命名冲突等,也会导致开发效率很低。

于是诞生了模块化这个东西,将复杂的模块解耦为更小的模块,分离我们的关注点,大幅提高了我们的开发效率以及编程体验,同时也提高了代码复用性,降低了命名冲突的可能。

(function(module) {
   ...code
})(module)

在浏览器还不支持 ESModule 之前,浏览器是没法单独的加载某一个模块的,所以产生了像 webpack、rollup 这样的模块打包工具,用于将我们所有分散的模块进行聚合,最终生成 js、css、image 等静态资源。

随着浏览器迭代更新,又诞生了像 Vite 这样通过利用浏览器 ESMoudle 的能力,在 dev 环境下帮助我们更高效的进行开发

Webpack

image.png

image.png

从 webpack4 开始,可以零配置运行 webpack

Install

npm install webpack webpack-cli --save-dev

基本概念及使用

Entry

一次 build 可以有一个或者多个 entry,每个 entry 都对应生成一个 js 文件,entry 通常可以指定入口路径、输出文件名、依赖 chunk

const path = require('path');

module.exports = {
  mode: "development",
  entry: {
    mian: './src/main.js',
    index: '.src/index.js'
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
  },
};

Output

  • 指定 entry、异步 chunk 的输出文件名及其策略
  • 输出一个 library
  • 指定 publicPath

Loader

Loader 用于 module 的源代码转换,它们允许你在导入或者加载文件时对文件进行预处理,可以将不同语言转换为 javascript,例如 typescript、css、less、image、vue、react

Plugin

Plugin 是 webapck 的支柱,它自身就是由许多插件组合而成,我们可以订阅 webpack 各个阶段抛出的事件,来完成一些 loader 无法完成的事情

HMR

热更新,即不用刷新页面即可对更改的模块进行更新

 devServer: {
   hot: true
 }

Code Spliting

将 js 文件拆分为更小的模块,降低初始化加载的 bundle 体积,提高缓存利用率

// 动态导入某个文件或者组件
import('./xxx.js').then(_ => {
   console.log(_)
})

// context
const file = ['xxx.js', 'xxx1.js']
file.forEach(file => import(`./src/${file}`))

Module Federation

能轻易实现在两个使用 webpack 构建的项目之间共享代码,甚至组合不同的应用为一个应用。

module-federation-examples

内部原理

Runtime

我们现在的项目中会有很多不同的模块,通过 import 来导入不同的模块,形成一颗树的结构

// index.js
import a from './a.js'
import b from './b.js'
import c from './c.js'

function test(p) {
   return p
}

test(1)

打包后的代码其实是通过一个对象将所有的模块代码存储下来

var __webpack_modules__ = ({
  './a.js': function (module, __webpack_exports__, __webpack_require__) {
    // ...code
  },
  './b.js': function (module, __webpack_exports__, __webpack_require__) {
    // ...code
  },
  './c.js': function (module, __webpack_exports__, __webpack_require__) {
    // ...code
  },
  './index.js': function (module, __webpack_exports__, __webpack_require__) {
    const a = __webpack_require__('./a.js')
    const b = __webpack_require__('./b.js')
    const c = __webpack_require__('./c.js')
    function test(p) {
       return p
    }
    
    test(1)
    module.exports = {}
    __webpack_exports__.a = 1
  }
})

webpack_require 的定义如下

var __webpack_module_cache__ = {};
/******/  
/******/  // The require function
/******/  function __webpack_require__(moduleId) {
/******/    // Check if module is in cache
/******/    var cachedModule = __webpack_module_cache__[moduleId];
/******/    if (cachedModule !== undefined) {
/******/      return cachedModule.exports;
/******/    }
/******/    // Create a new module (and put it into the cache)
/******/    var module = __webpack_module_cache__[moduleId] = {
/******/      id: moduleId,
/******/      // no module.loaded needed
/******/      exports: {}
/******/    };
/******/  
/******/    // Execute the module function
/******/    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/  
/******/    // Return the exports of the module
/******/    return module.exports;
/******/  }

todo:异步组件如何加载

Tapable

Webpack 插件机制是基于发布订阅者模式,具体的实现是通过 Tapable 完成的。 playground

const {
        SyncHook, // 同步并安顺序调用钩子
        SyncBailHook, // 同步并安顺序调用钩子,出现熔断则停止后续执行
        SyncWaterfallHook, // 同步 waterfall 钩子
        SyncLoopHook, // 同步循环钩子
        AsyncParallelHook, // 异步并行调用钩子
        AsyncSeriesHook, // 异步串行调用钩子
 } = require("tapable");
 
//SyncHook
const hook = new SyncHook(['arg1', 'arg2', 'arg3']);
hook.tap('flag1', (arg1, arg2, arg3) => {
  console.log('flag1:', arg1, arg2, arg3);
});
hook.tap('flag2', (arg1, arg2, arg3) => {
  console.log('flag2:', arg1, arg2, arg3);
});
hook.call('19Qingfeng', 'wang', 'haoyu');

//SyncBailHook
const hook = new SyncBailHook(['arg1', 'arg2', 'arg3']);
hook.tap('flag1', (arg1, arg2, arg3) => {
  console.log('flag1:', arg1, arg2, arg3);
  return true
});
hook.tap('flag2', (arg1, arg2, arg3) => {
  console.log('flag2:', arg1, arg2, arg3);
});
hook.call('19Qingfeng', 'wang', 'haoyu');

//SyncWaterfallHook
const hook = new SyncWaterfallHook(['arg1']);
hook.tap('flag1', (arg1) => {
  console.log('flag1:', arg1);
});
hook.tap('flag2', (arg1) => {
  console.log('flag2:', arg1);
});
hook.tap('flag3', (arg1) => {
  console.log('flag3:', arg1);
});
hook.call('19Qingfeng');

// SyncLoopHook
let flag1 = 2;
let flag2 = 1;
const hook = new SyncLoopHook(['arg1', 'arg2', 'arg3']);
hook.tap('flag1', (arg1, arg2, arg3) => {
  console.log('flag1');
  if (flag1 !== 3) {
    return flag1++;
  }
});
hook.tap('flag2', (arg1, arg2, arg3) => {
  console.log('flag2');
  if (flag2 !== 3) {
    return flag2++;
  }
});
hook.tap('flag3', (arg1, arg2, arg3) => {
  console.log('flag3');
});
hook.call('19Qingfeng', 'wang', 'haoyu');

// AsyncSeriesHook
const hook = new AsyncSeriesHook(['arg1', 'arg2', 'arg3']);
console.time('timer');
hook.tapAsync('flag1', (arg1, arg2, arg3, callback) => {
  console.log('flag1:', arg1, arg2, arg3);
  setTimeout(() => {
    callback();
  }, 1000);
});
hook.tapPromise('flag2', (arg1, arg2, arg3) => {
  console.log('flag2:', arg1, arg2, arg3);
    return new Promise((resolve) => {
        setTimeout(() => {
          resolve();
        }, 1000);
    });
});
hook.callAsync('19Qingfeng', 'wang', 'haoyu', () => {
  console.log('全部执行完毕 done');
  console.timeEnd('timer');
});

// AsyncParallelHook
const hook = new AsyncParallelHook(['arg1', 'arg2', 'arg3']);
console.time('timer');
hook.tapPromise('flag1', (arg1, arg2, arg3) => {
  console.log('flag2:', arg1, arg2, arg3);
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(true);
    }, 1000);
  });
});
hook.tapAsync('flag2', (arg1, arg2, arg3, callback) => {
  console.log('flag1:', arg1, arg2, arg3);
  setTimeout(() => {
    callback();
  }, 1000);
});
hook.callAsync('19Qingfeng', 'wang', 'haoyu', () => {
  console.log('全部执行完毕 done');
  console.timeEnd('timer');
});

通过这种发布订阅的方式,可以 hack 到 webpack 构建的各个流程中,灵活性和扩展性极强,同时也将构建流程进行解耦,放到不同的插件中去执行

juejin.cn/post/704098…

Loader

如何编写一个 loader

export default function (source) {
  return newSource || { ast, sourcemap }
} 
  1. Vue sfc : github.com/vuejs/core/…
  2. babel:babeljs.io/docs/en/bab…
  3. sass:sass-lang.com/documentati…
  4. less:lesscss.org/usage/#deve…
  5. ts:github.com/microsoft/T…

juejin.cn/post/699275…

loader-runner 会负责 Loader 的执行

Module

在 webpack 中 Module 分为两种,一种是 NormalModule,一种是 ContextModule

// context
const file = ['xxx.js', 'xxx1.js']
file.forEach(file => import(`./src/${file}`))

对于上面这种方式产生的 Module 叫做 ContextModule,否则都为 NormalModule,module 其实就是对应到某一个具体的文件

class NormalModuleFactory extends ModuleFactory {
// ...
}

class NormalModule extends Module {
    constructor({
        ...
        type,
        request,
        loaders,
        resource,
        context,
        parser,
        parserOptions,
        generator,
        generatorOptions,
        ...
    }) {
        super(type, context || getContext(resource), layer);
        ...
        // Info from Factory
        /** @type {string} */
        this.request = request;
        this.parser = parser;
        this.parserOptions = parserOptions;
        /** @type {Generator} */
        this.generator = generator;
        this.generatorOptions = generatorOptions;
        /** @type {string} */
        this.resource = resource;
        /** @type {LoaderItem[]} */
        this.loaders = loaders;
        /** @type {Dependency[]} */
        this.dependencies = []
        ...
    }
    ...
    // 调用 loader 链,处理当前 module 对应的文件,分析出 dependencies
    _doBuild() {...}
}


// 用于记录模块之间的依赖关系
class ModuleGraph {
    constructor() {
        /** @type {WeakMap<Dependency, ModuleGraphConnection>} */
        this._dependencyMap = new WeakMap();
        /** @type {Map<Module, ModuleGraphModule>} */
        this._moduleMap = new Map();
        /** @type {WeakMap<any, Object>} */
        this._metaMap = new WeakMap();

        /** @type {WeakTupleMap<any[], any>} */
        this._cache = undefined;

        /** @type {Map<Module, WeakTupleMap<any, any>>} */
        this._moduleMemCaches = undefined;
    }
}

image.png

Chunk

一个 chunk 对应一个输出的文件,例如一个 entry 其实就是一个 chunk,一个 Chunk 包含多个 module,表示当前 Chunk 输出的内容包含了哪些模块

seal 阶段,会根据 module 及其之间的依赖关系,生成多个 chunk

image.png

image.png

Compiler

编译器,主要负责一些编译前的前置工作,和编译完成后的文件输出工作,具体编译流程在 Compilation 中

class Compiler {
    constructor(context, options = /** @type {WebpackOptions} */ ({})) {
        this.hooks = Object.freeze({
            ...
        });
        ...
        this.webpack = webpack;

        /** @type {string=} */
        this.name = undefined;
        /** @type {Compiler} */
        this.root = this;
        /** @type {string} */
        this.outputPath = "";
        /** @type {Watching} */
        this.watching = undefined;

        this.options = options;

        this.context = context;

        /** @type {boolean} */
        this.running = false;

        /** @type {boolean} */
        this.idle = false;

        /** @type {Compilation} */
        this._lastCompilation = undefined;
        ...
    }
    run() {
     // ...
    }
    
}

调用 run 方法启动构建

Compilation

每次编译都会创建一个 Compilation,来存储此次编译过程中所有的相关信息,还包含编译过程中的相关处理流程

class Compilation {
  constructor(compiler, params){
    /** @type {string=} */
        this.name = undefined;
        this.startTime = undefined;
        this.endTime = undefined;
        /** @type {Compiler} */
        this.compiler = compiler;
        this.options = options;
        this.moduleGraph = new ModuleGraph();
        /** @type {ChunkGraph} */
        this.chunkGraph = undefined;
        
        this.addModuleQueue = new AsyncQueue({
            name: "addModule",
            parent: this.processDependenciesQueue,
            getKey: module => module.identifier(),
            processor: this._addModule.bind(this)
        });
        /** @type {AsyncQueue<FactorizeModuleOptions, string, Module | ModuleFactoryResult>} */
        this.factorizeQueue = new AsyncQueue({
            name: "factorize",
            parent: this.addModuleQueue,
            processor: this._factorizeModule.bind(this)
        });
        /** @type {AsyncQueue<Module, Module, Module>} */
        this.buildQueue = new AsyncQueue({
            name: "build",
            parent: this.factorizeQueue,
            processor: this._buildModule.bind(this)
        });
        this.entries = new Map();
        this.modules = new Set();
        this.assets = {};
  }
  _addModule() {
    // ...
  }
  _factorizeModule() {
    // ...
  }
  _buildModule() {
    // ...
  }
  addEntry() {
    // ...
  }
  seal() {
   // ...
  }
}

Plugin

class MyExampleWebpackPlugin {
  apply(compiler) {
    compiler
        .hooks
        .emit
        .tapAsync('MyExampleWebpackPlugin',(compilation, callback) => {
          // ...
        });
  }
}

整体流程

image.png

Vite

image.png

特点是快,体现在以下方面:

  1. Dev 环境

    1. 使用 ESbuild 对项目源代码中,使用到的 node_module 中的依赖进行分别单独打包
    2. 项目自身源代码无需进行构建,利用浏览器支持 ESMoudule 的能力,直接加载模块,并且只会加载用到的模块
    3. 热更新也很快
  2. Production 环境

    1. 使用 rollup

storybook.js.org/blog/storyb…

Rollup

image.png

build process

image.png

Output process

image.png

ESbuild

image.png esbuild-loader:github.com/privatenumb…