当前位置: 首页 > news >正文

【Wbpack原理】基础流程解析,实现 mini-webpack

【Wbpack原理】基础流程解析,实现 mini-webpack

⛄:webpack 对前端同学来说并不陌生,它是我们学习前端工程化的第一站,在最开始的 vue-cli 中我们就可以发现它的身影。我们的 vue/react 项目是如何打包成 js 文件并在浏览器中运行的呢?本系列文章将会帮助你由浅入深理解 webpack 原理,了解其中的 loader/plugin 机制,熟悉 webpack 打包流程。实现简易 webpack 核心代码,run-loader 模块,示例 loaderplugin。Tip:在阅读本文前需要了解一些 webpack 的基础概念及常用配置。

本质上,webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容。

本系列全部代码与文章会在 LonelySnowman/mini-webpack 同步,如果能帮到你的话请帮我点个 star 吧 😀。

基础流程解析

webpack 打包流程可大致分为以下四部分。

  1. 初始化准备:
    • webpack 会读取 webpack.config.js 文件中的参数,并将 shell 命令中的参数合并形成最终参数。
    • 然后 webpack 根据最终参数初始化 compiler 对象,注册配置中的插件,执行 compiler.run() 开始编译。
  2. 模块编译:
    • 从打包入口开始,调用匹配文件的 loader 对文件进行处理,并分析模块间的依赖关系,递归对模块进行编译。
  3. 模块生成:
    • 模块递归编译结束后,得到模块之间的相互依赖关系。
  4. 输出文件:
    • 根据模块间的依赖关系及配置文件,将处理后的模块输出到 output 的目录下。

compiler 对象记录着构建过程中 webpack 环境与配置信息,整个 webpack 从开始到结束的生命周期。

目录结构

首先搭建一下我们项目结构的基础目录,讲解过程中会给出具体代码,使用 pnpm-workspace 构建一个 monorepo 仓库,除 mini-webpack 代码外,后续还会带大家实现其他 webpack 相关知识模块。

也可以直接去 LonelySnowman/mini-webpack 克隆本系列相关代码。

packages
├─core // mini-webpack 核心代码
│  │  compilation.js // compilation 对象实现
│  │  compiler.js // compiler 对象实现
│  │  index.js // 编译执行主文件
│  │  webpack.js // webpack 初始化相关代码
│  └─util
│    index.js // 工具函数
├─example // 被编译的案例代码
│  │ webpack.config.js
│  └─src
│    entry1.js
│    entry2.js
│    module.js
├─loaders // 简易 loader demo
│ loader-1.js
│ loader-2.js
└─plugins // 简易 plugin demoplugin-1.jsplugin-2.jsplugin-test.js
# pnpm-workspace.yaml
packages:- 'packages/*'

新建打包案例

在开始编写 mini-webpack 核心代码前,我们先编写一个用于我们编写完成后的测试用例。

新建一个 webpack 配置文件。

// packages/example/webpack.config.js
const path = require('path')
const Plugin1 = require('../plugins/plugin-1')
const Plugin2 = require('../plugins/plugin-2')module.exports = {mode: 'development',entry: {main: path.resolve(__dirname, './src/entry1.js'),second: path.resolve(__dirname, './src/entry2.js'),},devtool: false,context: process.cwd(),output: {path: path.resolve(__dirname, './build'),filename: '[name].js',},plugins: [new Plugin1(), new Plugin2()],resolve: {extensions: ['.js', '.ts'],},module: {rules: [{test: /\.js/,use: [path.resolve(__dirname, '../loaders/loader-1.js'),path.resolve(__dirname, '../loaders/loader-2.js'),],},],},
};

新建一下我们需要打包用的文件。

// packages/example/src/entry1.js
const depModule = require('./module');
console.log(depModule, 'Entry 1 dep');
console.log('This is entry 1 !');// packages/example/src/entry2.js
const depModule = require('./module');
console.log(depModule, 'Entry 2 dep');
console.log('This is entry 2 !');// packages/example/src/module.js
const name = 'This is module';
module.exports = {name,
};

新建我们用到的 pluginloader,如果你对这两个的实现原理都不太了解也不要担心,后续我们会详细讲解,这里只编写了一些简单的小案例。

// packages/plugins/plugin-1.js
class Plugin1 {apply(compiler) {compiler.hooks.run.tap('Plugin1', () => {console.log('Plugin1 Start');});}
}module.exports = Plugin1;// packages/plugins/plugin-2.js
class Plugin2 {apply(compiler) {compiler.hooks.done.tap('Plugin2', () => {console.log('Plugin2 Done');});}
}module.exports = Plugin2;
// packages/loaders/loader-1.js
function loader(source) {console.log('loader1: normal', source);return source + '\n// loader1';
}loader.pitch = function () {console.log('loader1 pitch');
};module.exports = loader;// packages/loaders/loader-2.js
function loader(source) {console.log('loader2: normal', source);return source + '\n// loader2';
}loader.pitch = function () {console.log('loader2 pitch');
};module.exports = loader;

初始化准备阶段

webpack cli 运行入口

打包打包时 webpack 会读取 webpack.config.js 的配置并与 shell 中的参数合并,生成 compiler 对象并调用 compiler.run() 方法进行打包。

我们新建 index.js 作为 webpack 运行的入口。

// packages/core/index.js
// 调用 webpack(config) 初始化 compiler 对象
const webpack = require('./webpack');
const config = require('../example/webpack.config');// webpack() 方法会返回一个 compiler 对象
const compiler = webpack(config);// 调用 run 方法进行打包
compiler.run((err, stats) => {if (err) {console.log(err, 'err');}
});

新建 webpack.js 去读取参数并返回 compiler 对象。

// packages/core/webpack.js
const Compiler = require('./compiler')function webpack(options) {// 初始化参数根据配置文件和 shell 参数得到合并后的参数const mergedOptions = mergeOptions(options);// 创建compiler对象const compiler = new Compiler(mergedOptions)return compiler
}module.exports = webpack;

补充 mergeOptions 方法。

  • 在运行 webpack 命令时我们可以使用 --mode=production 去覆盖 webpack.config.js 的参数
// packages/core/webpack.js
// webpack --mode=production
function mergeOptions(options) {const shellOptions = process.argv.slice(2).reduce((option, argv) => {// 根据 = 分割const [key, value] = argv.split('=')if (key && value) {// 去除 key 前面的 --const parseKey = key.slice(2)option[parseKey] = value}return option;}, {})// 用 shellOptions 覆盖配置文件的 optionsreturn { ...options, ...shellOptions }
}

实现 compiler 对象

新建 compiler.js 文件,实现 compiler 对象核心逻辑。

compiler 对象记录着构建过程中 webpack 环境与配置信息,整个 webpack 从开始到结束的生命周期。我们需要实现 plugin 插件机制与 loader 机制。下面是 compiler 对象的基础骨架。

// packages/core/compiler.js
class Compiler {constructor(options) {this.options = options;}// 实现 run 方法开始编译run(callback) {}
}module.exports = Compiler

实现基础插件钩子

插件是 webpack 生态的关键部分, 它为我们提供了一种强有力的方式来直接触及 webpack 的编译过程(compilation process)。 插件能够 hook 到每一个编译(compilation)中发出的关键事件中。 在编译的每个阶段中,插件都拥有对 compiler 对象的完全访问能力, 并且在合适的时机,还可以访问当前的 compilation 对象。

compilation 对象记录编译模块的信息,只要项目文件有改动,compilation 就会被重新创建。

webpack 插件可以简单理解为可以在 wepack 整个生命周期中触发的钩子,类似与 vue 中的 createdmounted 等生命周期。

这里简单讲解一下,后续有单独的章节详细讲解 plugin

我们实现一个简易的 webpack 插件,packages/plugins/plugin-test.js,插件就是一个 javascript 类,需要实现 apply 方法供 webpack 调用,webpack 会在 compilercompilation 对象上预设一系列钩子供我们调用。

// 这个插件的作用就是在 webpack 开始编译前输出 PluginTest Start
class PluginTest {// webpack 会调用 apply 函数并传入 compiler 对象apply(compiler) {// 在 compiler 对象上的 run hooks 下注册同步钩子compiler.hooks.run.tap('Plugin Test', () => {console.log('PluginTest Start');});}
}module.exports = PluginTest;

接下来我们在 compiler 实现一些基本的钩子,webpack 的插件借助 tapable 这个库去实现,我们可以使用 new SyncHook() 去初始化一个钩子对象,放在 compiler.hooks 下。

const { SyncHook } = require('tapable')class Compiler {constructor(options) {// ...// 创建 plugin hooksthis.hooks = {// 开始编译时的钩子run: new SyncHook(), // new AsyncSeriesHook(["compiler"])// 输出 asset 到 output 目录之前执行的钩子emit: new SyncHook(), // new AsyncSeriesHook(["compilation"])// 在 compilation 全部完成编译执行的钩子done: new SyncHook(), // new AsyncSeriesHook(["stats"])compilation: new SyncHook(["compilation", "params"]),};}run(callback) {// 调用 run 方法时触发 hooks.run 的钩子回调this.hooks.run.call()}
}

在初始化 compiler 对象时我们还需要去执行插件实例中的 apply 方法,用于注册插件中的钩子。

添加 loadPlugin 方法后的完整 webpack.js 如下。

// packages/core/webpack.js
const Compiler = require('./compiler')function webpack(options) {// 合并参数 得到合并后的参数 mergedOptionsconst mergedOptions = mergeOptions(options);// 创建compiler对象const compiler = new Compiler(mergedOptions)// 加载插件loadPlugin(options.plugins, compiler);return compiler
}// 合并参数
function mergeOptions(options) {const shellOptions = process.argv.slice(2).reduce((option, argv) => {// 根据 = 分割const [key, value] = argv.split('=')if (key && value) {// 去除 key 前面的 --const parseKey = key.slice(2)option[parseKey] = value}return option;}, {})return { ...options, ...shellOptions }
}// 加载插件函数
function loadPlugin(plugins, compiler) {if (plugins && Array.isArray(plugins)) {plugins.forEach((plugin) => {// webpack 插件都是一个类 需要有 apply 方法plugin.apply(compiler);});}
}module.exports = webpack;

webpack 插件本质上就是通过发布订阅者模式,在 compiler.hooks 上监听事件,通过 compiler.hooks.xxx.tap 去订阅事件,用 compiler.hooks.xxx.call 去触发事件,触发方法在后续会逐步添加。

至此已实现了初始化准备阶段的内容,我们实现了 webpack 配置的读取及初始化合并,注册 webpack 插件并调用 compiler.run() 方法开始编译。

模块编译阶段

寻找编译入口 entry

打包前我们需要根据合并后的配置找到打包入口文件,对 entry 文件进行编译处理。入口配置可以为字符串也可以为对象。

// 字符串配置形式
{entry: 'entry.js'
}
// 字符串形式最终也会被转为对象配置
{entry: {main: 'entry.js'}
}// 对象配置形式
{entry: {'entry1': './entry1.js','entry2': './entry2.js'}
}

我们在 compiler.js 中实现 getEntry 寻找打包入口的方法。

// packages/core/compiler.js
const path = require('path')
const { toUnixPath } = require('./util')class Compiler {constructor(options) {// 读取配置中的 根目录 路径默认值为 process.cwd()this.rootPath = this.options.context || toUnixPath(process.cwd())// ...}// run方法启动编译// 同时run方法接受外部传递的callbackrun(callback) {// ...const entry = this.getEntry();// ...}// 获取入口文件路径getEntry() {let entry = Object.create(null)const { entry: optionsEntry } = this.options// string 转为含 main 的对象 (支持 webpack entry 配置传入字符串的情况)if (typeof optionsEntry === 'string') entry['main'] = optionsEntryelse entry = optionsEntry// 将 entry 变成绝对路径Object.keys(entry).forEach((key) => {const value = entry[key]if (!path.isAbsolute(value)) {// 转化为绝对路径的同时统一路径分隔符为 /entry[key] = toUnixPath(path.join(this.rootPath, value))}})return entry}
}module.exports = Compiler

补充一下用到的工具函数。

// packages/core/util/index.js// 统一路径分隔符为 /
function toUnixPath(path) {return path.replace(/\\/g, '/');
}module.exports = {toUnixPath
}

这一步我们通过读取 webpack 配置中的 entry 获取打包入口文件转化为绝对路径并统一路径分分隔符。

从入口文件开始编译

🤔 编译阶段我们需要完成以下内容:

  1. 根据入口文件构建 compilation 对象,compilation 对象会负责模块编译过程的处理。
  2. 根据入口文件路径分析入口文件,使用 loader 处理匹配的文件。
  3. loader 处理完成的入口文件进行编译。
  4. 分析入口文件依赖,重复上边两个步骤编译对应依赖。
  5. 如果嵌套文件存在依赖文件,递归调用依赖模块进行编译。
  6. 递归编译完成后,组装一个个包含多个模块的chunk

新建 Compilation 类进行编译模块的处理,保存该次编译过程中的入口模块对象、依赖模块对象、

// packages/core/compilation.jsclass Compilation {constructor(compiler, params) {// 获取 compiler 上的 optionsthis.options = compiler.options;this.rootPath = compiler.rootPath;// 保存所有入口模块对象this.entries = new Set();// 保存所有依赖模块对象this.modules = new Set();// 所有的代码块对象this.chunks = new Set();// 存放本次产出的文件对象this.assets = new Set();// 存放本次编译所有产出的文件名this.files = [];}
}

根据配置中的入口文件,开始从入口文件开始进行编译,并创建入口文件对象。

// packages/core/compilation.jsclass Compilation {// ...// A.编译全部入口模块buildEntryModule(entry) {Object.entries(entry).forEach(([entryName, entryPath]) => {// 对入口文件进行编译 获取入口文件对象const entryObj = this.buildModule(entryName, entryPath);this.entries.add(entryObj);// 根据当前入口文件和模块的相互依赖关系,组装成为一个个包含当前入口所有依赖模块的chunkthis.buildUpChunk(entryName, entryObj);});}
}

模块编译

在编写模块编译的方法前,我们可以先使用原版的 webpack 对我们的案例进行打包,看一下打包后的结果。

// packages/core/index.js// 使用 webpack 替换我们的 mini-webpack
// const webpack = require('./webpack');
const webpack = require('webpack');
const config = require('../example/webpack.config');
const compiler = webpack(config);
compiler.run((err, stats) => {if (err) {console.log(err, 'err');}
});

然后在根目录执行 node .\packages\core\index.js 进行打包。

我们可以看到依据我们的 entry 打包出了两个文件,分别来自 entry1entry2 ,我们可以看一下 packages/example/build/main.js 文件。下面的代码是剔除了注释后的。

(() => {// entry 中引入的模块被存入了一个 __webpack_modules__ 对象// key 为模块的相对路径// value 为一个函数直接执行 module 中的代码var __webpack_modules__ = ({"./packages/example/src/module.js":((module) => {const name = 'This is module';module.exports = {name,};})});var __webpack_module_cache__ = {};// 自行封装一个 __webpack_require__ 方法执行 module 中的代码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__ = {};(() => {// 代码中的 require 均被替换为 __webpack_require__const depModule = __webpack_require__( "./packages/example/src/module.js");console.log(depModule, 'Entry 1 dep');console.log('This is entry 1 !');// loader2// loader1})();
})();

🤔 这样一看原理其实很简单,webpack 最终打包出的文件是一个立即执行函数,依次读取 entry 中引用的文件全部编译在 __webpack_modules__ 中的一个对象 ,key 为模块的相对路径(作为一个模块的唯一 id),value 为一个函数直接执行 module 中的代码。然后再封装一个 __webpack_require__ 方法从 __webpack_modules__ 获取 module 代码并执行。并将代码中的 require 全部替换为 __webpack_require__

那么再编译模块的方法主要进行两步操作,获取代码文件的源代码字符串,然后使用 loader 对代码进行处理,再对处理后的代码进行编译,就是将代码中的 require 全部替换为 __webpack_require__,最后我们输出模块的时候再将 module 中的代码打包进 __webpack_modules__ 就可以了。

// packages/core/compilation.js
buildModule(moduleName, modulePath) {// 1.读取文件原始代码const originSourceCode = fs.readFileSync(modulePath, 'utf-8')// originSourceCode 与 moduleCode// 记录当前处理模块的 源代码 与 编译后代码this.originSourceCode = originSourceCodethis.moduleCode = originSourceCode;// 2.调用 loader 进行处理// 这里先用一个简单的方法进行处理// 源码中封装了一个 loader-runner 模块进行处理this.handleLoader(modulePath);// 3.调用 webpack 进行模块编译获得最终的module对象// 处理 require 引用问题const module = this.handleWebpackCompiler(moduleName, modulePath);return module
}

首先我们需要用 loader 处理读取的源文件内容。loader 本质上就是一个函数,接收文件源代码并可以在 this 中调用 webpack 上下文对象,返回 loader 处理后的代码内容。

// packages/core/compilation.js
handleLoader(modulePath) {const matchLoaders = [];// 1. 获取所有传入的loader规则const rules = this.options.module.rules;// 读取 loader 路径rules.forEach((loader) => {const testRule = loader.test;if (testRule.test(modulePath)) {if (typeof loader.use === 'string') {matchLoaders.push(loader.use);} else {matchLoaders.push(...loader.use);}}// 2. 倒序执行loader传入源代码for (let i = matchLoaders.length - 1; i >= 0; i--) {// require 引入对应 loaderconst loaderFn = require(matchLoaders[i]);// 使用 call 绑定 thisthis.moduleCode = loaderFn.call(this, this.moduleCode);}});
}

🤔 loader 处理完毕后我们需要进行 webpack 编译阶段,也就是需要将源代码中的 require 全部替换为 __webpack_require__,并生成 module 对象。这个操作可以利用 bable 将代码转化为 ast 语法树,并直接操作语法树生成新的代码,非常方便。

并且在处理过程中我们要进行递归操作,一个模块依赖其他模块时,也需要对该模块的依赖模块进行编译处理。

// 引入相关工具库
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generator = require('@babel/generator').default;
const t = require('@babel/types');// packages/core/compilation.js
handleWebpackCompiler(moduleName, modulePath) {// 将当前模块相对于项目启动根目录计算出相对路径 作为模块IDconst moduleId = toUnixPath('./' + path.relative(this.rootPath, modulePath));// 创建模块对象const module = {id: moduleId,dependencies: new Set(), // 该模块所依赖模块绝对路径地址name: [moduleName], // 该模块所属的入口文件source: this.originSourceCode // 当前模块代码};// 调用 babel 分析我们的代码const ast = parser.parse(this.moduleCode, {sourceType: 'module',});// 利用 traverse 方法遍历语法树traverse(ast, {// 当遇到 require 语句时会触发该回调CallExpression:(nodePath) => {const node = nodePath.node;if (node.callee.name === 'require') {// 获得源代码中引入模块相对路径const requirePath = node.arguments[0].value;// 获取到 require 内部的绝对路径// tryExtensions 就是对路径进行后缀的匹配const moduleDirName = path.dirname(modulePath);const absolutePath = tryExtensions(path.join(moduleDirName, requirePath),this.options.resolve.extensions,requirePath,moduleDirName);// 绝对路径转化为相对 rootPath 的相对路径作为 moduleIdconst moduleId = toUnixPath('./' + path.relative(this.rootPath, absolutePath));// 将 require 替换为 __webpack_require__node.callee = t.identifier('__webpack_require__');// 修改源代码中 require 语句引入的模块// 全部修改变为相对于跟路径 moduleId 来处理node.arguments = [t.stringLiteral(moduleId)];// 将该模块 require 的模块全部添加进依赖中module.dependencies.add(moduleId);}},});// 遍历结束根据AST生成新的代码const { code } = generator(ast);// 为当前模块挂载新的生成的代码module._source = code;// 递归依赖深度遍历 存在依赖模块则加入// 添加前防止重复解析const alreadyModules = Array.from(this.modules).map((i) => i.id);module.dependencies.forEach((dependencyPath) => {if (!alreadyModules.includes(dependencyPath)) {const depModule = this.buildModule(moduleName, dependencyPath);// 将编译后的任何依赖模块对象加入到modules对象中去this.modules.add(depModule);} else {// 否则不需要解析 仅添加入口文件this.modules.forEach((value) => {if (value.id === dependencyPath) {value.name.push(moduleName);}})}});// 返回当前模块对象return module
}

补充一下匹配文件后缀的方法。

// packages/core/util/index.js
function tryExtensions(modulePath,extensions,originModulePath,moduleContext
) {// 用户传入后缀优先直接寻找extensions.unshift('');for (let extension of extensions) {if (fs.existsSync(modulePath + extension)) {return modulePath + extension;}}// 未匹配对应文件throw new Error(`No module, Error: Can't resolve ${originModulePath} in  ${moduleContext}`);
}

😀 到这里我们就完成了模块编译阶段,我们从打包入口开始,依次对入口文件以及引用的依赖模块进行 loader 处理以及 webpack 编译,构建出一个 依赖图(dependency graph),使用 entriesmodules 分别保存了入口对象和模块对象,我们可以根据这些信息去构建我们的 chunks,最后将打包后的模块输出。

模块生成阶段

组装 chunk

🤔 这一阶段比较简单,一个 entry 生成一个 chunk 根据相关 modules 生成对象即可。

// packages/core/compilation.js
buildUpChunk(entryName, entryObj) {const chunk = {name: entryName, // 每一个入口文件作为一个 chunkentryModule: entryObj, // 编译后的 entry 对象modules: Array.from(this.modules).filter((i) =>i.name.includes(entryName)), // 在该 entry 中引入的 module};// 将 chunk 添加到 this.chunks 中去this.chunks.add(chunk);
}

接下来补充一下在 compiler 对象中调用 compilation 进行编译的代码。

run(callback) {// ...// 获取入口配置对象const entry = this.getEntry();const compilation = this.newCompilation();// 编译入口文件compilation.buildEntryModule(entry);// ...
}

补充一下构建 compilation 对象的方法。

newCompilation(params) {// 源码这里还会传入 normalModuleFactory 等对象const compilation = new Compilation(this, {})// 调用 compilation 阶段触发的钩子this.hooks.compilation.call(compilation, params);return compilation;
}

输出文件阶段

最后我们需要根据我们生成的 chunks 去输出最终编译完成的文件即可,在模块编译阶段中已经讲解了 webpack 打包的原理,是在内部封装了一个 __webpack_require__ 方法去调用 __webpack_modules__ 中的方法,需要变更的地方只有 __webpack_modules__ 对象和处理后的源代码内容,这些在 entrysmoduleschunks 中我们都已经生成好了,其他地方直接使用原版 webpack 打包后的内容即可,这样我们就能生成我们的 assets 并输出文件。

编写一个根据 chunk 信息去生成最终代码的方法。

// packages/core/util/index.js
function getSourceCode(chunk) {const { name, entryModule, modules } = chunk;// 根据 moduleId 作为 key// 处理后的代码封装成一个方法作为 valuereturn `(() => {var __webpack_modules__ = {${modules.map((module) => {return `'${module.id}': (module) => {${module._source}}`;}).join(',')}};// The module cachevar __webpack_module_cache__ = {};// The require functionfunction __webpack_require__(moduleId) {// Check if module is in cachevar 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] = {// no module.id needed// no module.loaded neededexports: {},});// Execute the module function__webpack_modules__[moduleId](module, module.exports, __webpack_require__);// Return the exports of the modulereturn module.exports;}var __webpack_exports__ = {};// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.(() => {${entryModule._source}})();})();`;
}

最后我们根据 chunks 中的信息直接输出文件即可。

// packages/core/compilation.jsemitAssets(compilation) {const output = this.options.output;// 根据 chunks 生成 assets 内容compilation.chunks.forEach((chunk) => {const parseFileName = output.filename.replace('[name]', chunk.name);compilation.assets[parseFileName] = getSourceCode(chunk);});// 调用 Plugin emit 钩子this.hooks.emit.call();// 目录不存在需要先创建目录if (!fs.existsSync(output.path)) fs.mkdirSync(output.path);// files 中保存所有的生成文件名compilation.files = Object.keys(this.assets);// 将 assets 中的内容生成打包文件输出compilation.files.forEach((fileName) => {const filePath = path.join(output.path, fileName);fs.writeFileSync(filePath, this.assets[fileName]);});
}

还需要在 compiler.run 函数中调用一下并补充回调逻辑,触发钩子等。

run(callback) {// ...// 导出列表之后将每个 chunk 转化称为单独的文件// 加入到输出列表 assets 中this.emitAssets(compilation);// 结束之后触发钩子this.hooks.done.call();// 执行 compiler.run 结束后的回调并返回编译信息callback(null, {toJson: () => {return {entries: compilation.entries,modules: compilation.modules,files: compilation.files,chunks: compilation.chunks,assets: compilation.assets,};},});
}

到这里我们简易 webpack 的核心逻辑就全部实现了 😀,我们可以在项目根目录下执行 node .\packages\core\index.js 下我们的编译命令。发现 packages/example/build 目录下生成了 main.jssecond.js 两个文件,分别对应两个入口打包出的 chunk

结语

想写在自己实操一遍后你已经对 webpack 有了更深刻的理解,之后我们还会深挖 loader 机制去实现 loader-runner,理解 plugin 机制并实现简易的 tapable,并实现简易的 loaderpluin ,最终能通过 index.html 运行我们的 web 项目。

参考文章

Webpack - 19组清风的专栏 - 掘金 (juejin.cn)

Github地址

LonelySnowman/mini-webpack

如果对你有帮助的话记得帮我点个赞 👍。

文章内容有不正确的地方请指出,我会及时更改 😀。

相关文章:

【Wbpack原理】基础流程解析,实现 mini-webpack

【Wbpack原理】基础流程解析,实现 mini-webpack ⛄:webpack 对前端同学来说并不陌生,它是我们学习前端工程化的第一站,在最开始的 vue-cli 中我们就可以发现它的身影。我们的 vue/react 项目是如何打包成 js 文件并在浏览器中运行…...

Debian 安装 python 3.9.6

安装相关依赖 sudo apt update sudo apt install build-essential zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libsqlite3-dev libreadline-dev libffi-dev curl libbz2-dev 下载Python 源码 wget https://mirrors.aliyun.com/python-release/source/Py…...

搜索二维矩阵2 合并两个有序链表

240. 搜索二维矩阵 II - 力扣&#xff08;LeetCode&#xff09; class Solution { public:bool searchMatrix(vector<vector<int>>& matrix, int target) {int i matrix.size() - 1, j 0;while(i > 0 && j < matrix[0].size()){if(matrix[i][j…...

深入Tauri开发——从环境搭建到项目构建

深入Tauri开发——从环境搭建到项目构建 开启你的Tauri桌面应用开发之旅&#xff08;续&#xff09; 经过上一篇文章的基础介绍&#xff0c;现在让我们更进一步&#xff0c;详细阐述如何在Windows和macOS平台上顺利搭建Tauri应用所需的开发环境&#xff0c;并指导您从创建项目…...

Redis 和 Mysql 数据库数据如何保持一致性

Redis 和 Mysql 数据库数据如何保持一致性 保持Redis和MySQL数据库数据一致性是一个常见且重要的问题&#xff0c;特别是在使用Redis作为MySQL数据库的缓存层时。以下是几种常用的保证二者数据一致性的策略和方法&#xff1a; 双写一致性&#xff08;同步更新&#xff09;&…...

探索7个MAMP本地开发环境的高效替代软件

什么是本地开发环境 本地开发环境是Web开发环境中的一种类型&#xff0c;它是指开发者自己的计算机上配置的一套用于开发和测试网站或应用程序的软件集合。这套环境使得开发者可以在本地计算机上构建和测试网站&#xff0c;而无需实时部署到服务器。 创建本地开发环境有两种方…...

靡语IT:Bootstrap 简介

1.1 Bootstrap 简介&#xff1a;什么是 Bootstrap? Bootstrap 是一个用于快速开发 Web 应用程序和网站的前端框架。Bootstrap是前端开发中比较受欢迎的框架&#xff0c;简洁且灵活。它基于HTML、CSS和JavaScript&#xff0c;HTML定义页面元素&#xff0c;CSS定义页面布局&…...

亚马逊店铺引流:海外云手机的利用方法

在电商业务蓬勃发展的当下&#xff0c;亚马逊已经成为全球最大的电商平台之一&#xff0c;拥有庞大的用户群和交易量。在激烈的市场竞争中&#xff0c;如何有效地吸引流量成为亚马逊店铺经营者所关注的重点。海外云手机作为一项新兴技术工具&#xff0c;为亚马逊店铺的流量引导…...

10 Python进阶:MongoDB

MongoDb介绍 MongoDB是一个基于分布式架构的文档数据库&#xff0c;它使用JSON样式的数据存储&#xff0c;支持动态查询&#xff0c;完全索引。MongoDB是NoSQL数据库的一种&#xff0c;主要用于处理大型、半结构化或无结构化的数据。以下是MongoDB数据库的一些关键特点和优势&a…...

Leetcode 142. 环形链表 II和Leetcode 242. 有效的字母异位词

文章目录 Leetcode 142. 环形链表 II题目描述C语言题解解题思路 Leetcode 242. 有效的字母异位词题目描述C语言题解和思路解题思路 Leetcode 142. 环形链表 II 题目描述 给定一个链表的头节点 head &#xff0c;返回链表开始入环的第一个节点。 如果链表无环&#xff0c;则返…...

【嵌入式DIY实例】-MODBUS串行通信

MODBUS串行通信 文章目录 MODBUS串行通信1、什么是RS-4852、MAX485 TTL转RS485转换器3、硬件准备4、代码实现4.1 主机和从机之间简单通信4.2 主/从机之间LED控制在本文中,我们将介绍如何使用 MAX485 MODBUS 在Arduino之间进行串行通信。 我们将使用 Arduino nano 板和 MODBUS …...

入门用Hive构建数据仓库

在当今数据爆炸的时代&#xff0c;构建高效的数据仓库是企业实现数据驱动决策的关键。Apache Hive 是一个基于 Hadoop 的数据仓库工具&#xff0c;可以轻松地进行数据存储、查询和分析。本文将介绍什么是 Hive、为什么选择 Hive 构建数据仓库、如何搭建 Hive 环境以及如何在 Hi…...

【计算机网络】会话层

负责维护两个会话主机之间链接的建立、管理和终止&#xff0c;以及数据的交换。 会话控制&#xff1a;决策该由谁来传递数据 令牌管理&#xff1a;禁止双方同时执行一个关键动作 同步功能&#xff1a;在一个长的传输过程中设置一些断点&#xff0c;以便系统崩溃后能恢复至崩…...

springboot实现七牛云的文件上传下载

一&#xff1a;依赖包 <dependency><groupId>com.qiniu</groupId><artifactId>qiniu-java-sdk</artifactId><qiniu-java-sdk.version>7.7.0</qiniu-java-sdk.version></dependency>二:具体实现 RestController RequestMapping…...

【RISC-V 指令集】RISC-V 向量V扩展指令集介绍(六)- 向量内存一致性模型

1. 引言 以下是《riscv-v-spec-1.0.pdf》文档的关键内容&#xff1a; 这是一份关于向量扩展的详细技术文档&#xff0c;内容覆盖了向量指令集的多个关键方面&#xff0c;如向量寄存器状态映射、向量指令格式、向量加载和存储操作、向量内存对齐约束、向量内存一致性模型、向量…...

Lvgl9 WindowsSimulator Visual Studio2017

因为在操作过程中遇到了一些错误&#xff0c;所以将操作及解决问题的过程记录下来。 一、下载lv_port_pc_visual_studio github链接:GitHub - lvgl/lv_port_pc_visual_studio: Visual Studio projects for LVGL embedded graphics library. Recommended on Windows. Linux su…...

【STL】链表(list)

链表是一种物理存储单元上非连续、非顺序的存储结构&#xff0c;数据元素的逻辑顺序是通过链表中的指针链接次序实现的。 链表由一系列结点&#xff08;链表中每一个元素称为结点&#xff09;组成&#xff0c;结点可以在运行时动态生成。每个结点包括两个部分&#xff1a;一个…...

node.js常用指令

1、node&#xff1a;启动 Node.js REPL&#xff08;交互式解释器&#xff09;。 node 2、node [文件名]&#xff1a;执行指定的 JavaScript 文件。 node app.js 3、npm init&#xff1a;初始化一个新的 Node.js 项目&#xff0c;生成 package.json 文件。 此命令会创建一个…...

Flutter第六弹 基础列表ListView

目标&#xff1a; 1&#xff09;Flutter有哪些常用的列表组建 2&#xff09;怎么定制列表项Item&#xff1f; 一、ListView简介 使用标准的 ListView 构造方法非常适合只有少量数据的列表。我们还将使用内置的 ListTile widget 来给我们的条目提供可视化结构。ListView支持…...

【考研经验贴】24考研860软件工程佛系上岸经验分享【丰富简历、初复试攻略、导师志愿、资料汇总】

&#x1f60a;你好&#xff0c;我是小航&#xff0c;一个正在变秃、变强的文艺倾年。 &#x1f514;本文讲解24考研860软件工程佛系上岸经验分享【丰富简历、初复试攻略、导师志愿、资料汇总】&#xff0c;期待与你一同探索、学习、进步&#xff0c;一起卷起来叭&#xff01; 目…...

15-1-Flex布局

个人主页&#xff1a;学习前端的小z 个人专栏&#xff1a;HTML5和CSS3悦读 本专栏旨在分享记录每日学习的前端知识和学习笔记的归纳总结&#xff0c;欢迎大家在评论区交流讨论&#xff01; 文章目录 Flex布局1 Flex容器和Flex项目2 Flex 容器属性2.1 主轴的方向2.2 主轴对齐方式…...

深入浅出 -- 系统架构之负载均衡Nginx的性能优化

一、Nginx性能优化 到这里文章的篇幅较长了&#xff0c;最后再来聊一下关于Nginx的性能优化&#xff0c;主要就简单说说收益最高的几个优化项&#xff0c;在这块就不再展开叙述了&#xff0c;毕竟影响性能都有多方面原因导致的&#xff0c;比如网络、服务器硬件、操作系统、后端…...

AI大模型下的策略模式与模板方法模式对比解析

​&#x1f308; 个人主页&#xff1a;danci_ &#x1f525; 系列专栏&#xff1a;《设计模式》《MYSQL应用》 &#x1f4aa;&#x1f3fb; 制定明确可量化的目标&#xff0c;坚持默默的做事。 &#x1f680; 转载自热榜文章&#xff1a;设计模式深度解析&#xff1a;AI大模型下…...

前端| 富文本显示不全的解决方法

背景 前置条件&#xff1a;编辑器wangEditor vue项目 在pc端进行了富文本操作&#xff0c; 将word内容复制到编辑器中&#xff0c; 进行发布&#xff0c; pc端正常&#xff0c; 在手机端展示的时候 显示不全 分析 根据h5端编辑器内容的数据展示&#xff0c; 看到有一些样式造…...

数据结构——链表

目录 一、链表 1、单向链表 单向链表的遍历方式&#xff1a; 2、循环链表 3、双向链表 二、自行车停放&#xff08;双向链表&#xff09; 一、链表 链表是由许多相同数据类型的数据项按特定顺序排列而成的线性表特性&#xff1a;存放的位置是不连续且随机的&#xff0c;动…...

uniapp使用vuex

1、uniapp中使用vuex_uniapp使用vuex-CSDN博客 2、uniapp中使用vuex(store)模块的例子 - 简书 (jianshu.com) 3、vuex介绍及使用指南&#xff08;面向实战&#xff09;_vuex 实战应用-CSDN博客...

C++从入门到精通——this指针

this指针 前言一、this指针的引出问题 二、this指针的特性三、例题什么时候会出现编译报错什么时候会出现运行崩溃this指针存在哪里this指针可以为空吗 四、C语言和C实现Stack的对比C语言实现C实现 前言 this指针是一个特殊的指针&#xff0c;在C类的成员函数中使用。它指向调…...

Hive3.0.0建库表命令测试

Hive创建表格格式如下&#xff1a; create [external] table [if not exists] table_name [(col_name data_type [comment col_comment],)] [comment table_comment] [partitioned by(col_name data_type [comment col_comment],)] [clustered by (col_name,col_name,...)…...

一起学习python——基础篇(7)

今天讲一下python的函数。 函数是什么&#xff1f;函数是一段独立的代码块&#xff0c;这块代码是为了实现一些功能&#xff0c;而这个代码块只有在被调用时才能运行。 在 Python 中&#xff0c;使用 def 关键字定义函数&#xff1a; 函数的固定结构就是 def(关键字)函数名字…...

【LeetCode热题100】74. 搜索二维矩阵(二分)

一.题目要求 给你一个满足下述两条属性的 m x n 整数矩阵&#xff1a; 每行中的整数从左到右按非严格递增顺序排列。每行的第一个整数大于前一行的最后一个整数。 给你一个整数 target &#xff0c;如果 target 在矩阵中&#xff0c;返回 true &#xff1b;否则&#xff0c;…...

网站开发 图片储存/个人网站seo入门

Linux 基金会是一个通过开源实现大规模创新的非盈利性组织&#xff0c;已经发布了10本企业开源指南的中文翻译&#xff0c;旨在帮助高管&#xff0c;开源项目经理&#xff0c;开发人员&#xff0c;律师和决策者学习如何最佳利用开源。CNCF 首席技术官兼 TODO 集团联合创始人 Ch…...

国外中文网站排行/seo标题优化是什么意思

http://www.techpowerup.com/articles/overclocking/vidcard/43 显存不够用的时候&#xff0c;拿内存当显存。AGP Aperture size就是取多少内存 How big should I set AGP Aperture size in my BIOS? First of all, AGP Aperture memory will not be used until your video …...

专做眼镜批发的网站/爱站网工具

目录 工程资源 一、加载Excel配置表的txt文件&#xff08;读写&#xff09; 二、GameFramework之Procedure流程 工程资源 https://github.com/AMikeW/BStandShaderResources *** 资源使用注意事项 *** 一个是场景&#xff0c;一个是Log日志类所需的脚本定义标记启动才能看到…...

福州市建设工程造价管理网站/广州营销型网站

4.2调试控制台 调试控制台是整个调试器的工作中心&#xff0c;这个控制台将响应用户的一切输入&#xff0c;完成用户所需的调试功能。 4.2.1命令解析 4.2.1.1数据结构的设计 我将所有命令统一定义在一张表里&#xff0c;这个表是如下的数据结构&#xff1a; typedefstruct{ CHA…...

网站用户引导/宁波seo在线优化方案

请帮助我如何使用deviation_2DArray.java中的变量到NBC.java&#xff0c;在NBC.java我想平均b[i] d[i][j]和c[j]例&#xff1a;b[1]avg (d[1][1]d[1][2].....d[1][5])提前致谢。2DArray.javapublic class 2DArray {public static void main(String[] args) {double[][] d new …...

查询网站所有死链接/百度一下首页设为主页

为什么80%的码农都做不了架构师&#xff1f;>>> 今天,托管的服务器挂了情况如下: 好多命令 Input /Output error. 建立/删除 文件/文件夹: Read-Only filesystem shutdown half init 各种重启无效. 因为,已经出现过此种错误,网络中心管理老师预估,磁盘老化损坏,上次…...