Skip to content

S09-04 Webpack5-源码、自定义Loader、自定义Plugin

[TOC]

源码阅读

测试代码

1、在webpack源码中,编写测试文件夹why

image-20240312142028066

2、直接在build.js中,调用webpack()函数,实现打包

image-20240719163258195

3、使用node运行build.js

image-20240312142136367

调试代码

1、添加断点

image-20240312142420534

2、运行调试,点击Javascript调试终端运行和调试

image-20240312142556433

3、调试控制

image-20240312143338786

编译入口文件

image-20240312145548723

image-20240312145602286

思维导图

插件

  • Bookmarks
  • CodeTour

自定义Loader

Loader API

自定义loader

  • xxx-loader()(content, map, meta),自定义的loader

    • content:``,资源文件的内容

    • map:``,sourcemap 相关的数据

    • meta:``,一些元数据

    • 返回:

    • return:``,

    • this.callback()(err, content),回调函数

      • errError | null,错误信息。如果没错,则传入null
      • contentstring | buffer,传递给下个loader的内容
    • js
      module.exports = function (content, map, meta) {
        console.log('xxx-loader: ', content)
        // console.log('xxx-loader: ', map)
        // console.log('xxx-loader: ', meta)
      
        return 'xxx-loader'
      }
  • xxx-loader-pitch()(remainingRequest, precedingRequest, data),一个函数,在加载过程中,Webpack 会调用它来处理文件。

    • remainingRequeststring,剩下的请求

    • precedingRequeststring,之前处理过的请求

    • dataobject,loader 共享的数据

    • 返回:

    • 不返回:``,继续执行后续的loader

    • 返回值:``,终止执行后续的loader

    • js
      module.exports.pitch = function pitchLoader(remainingRequest, precedingRequest, data) {
        // `remainingRequest` 是从当前 loader 到最后一个 loader 的请求路径
        // `precedingRequest` 是当前 loader 之前的所有 loader 的请求路径
        + // `data` 是 loader 之间共享的数据
      
        // 可以根据 remainingRequest 来决定是否继续处理
        if (remainingRequest.includes('specificFile.js')) {
          // 如果特定文件存在,则忽略后续的 loader
          return `module.exports = 'This module is handled by pitch loader only.';`;
        }
      
        // 继续使用后续 loader
        // 返回 null 或者不返回任何值
      };

loaderContext

每个 loader 都有一个 loaderContext 对象,loader函数内部的this指向该对象

  • this.async()(),允许 loader 异步执行。调用 async() 方法可以使 loader 进入异步模式,并返回一个异步回调函数,用于处理异步操作。

    • 返回:

    • callback(err, content),返回一个异步回调函数,用于处理异步操作。

      • errError | null,错误信息。如果没错,则传入null

      • contentstring | buffer,传递给下个loader的内容

    • js
      module.exports = function(source) {
          const callback = this.async();
      	setTimeout(() => {
        		callback(null, 'some processed code');
      	}, 1000);
      }
  • this.getOptions()(),用于获取传递给 loader 的选项。

    • 返回:

    • optionsobject,在 Webpack 配置中为 loader 指定的选项。

    • js
      // webpack.config.js
      module.exports = {
        module: {
          rules: [
            {
              test: /\.js$/,
              use: {
                loader: path.resolve(__dirname, 'my-loader.js'),
                options: {
                  key1: 'value1',
                  key2: 'value2'
                }
              }
            }
          ]
        }
      };
      
      // loader函数
      module.exports = function(source) {
        const options = this.getOptions();
        console.log(options.key1); // 输出: 'value1'
        console.log(options.key2); // 输出: 'value2'
        
        // 处理源代码
        return source;
      };
  • this.data,用来在不同 loader 之间共享数据:状态或配置信息。

    • js
      // first-loader.js
      module.exports = function(source) {
        // 设置数据
        this.data = {
          myData: 'Hello from first-loader'
        };
        return source;
      };
      
      // second-loader.js
      module.exports = function(source) {
        // 读取数据
        const data = this.data.myData;
        console.log(data); // 输出: 'Hello from first-loader'
        
        // 继续处理源代码
        return source;
      };

schema-utils

schema-utils:是一个用于验证和处理配置选项的库,通常与 Webpack 的 loader 和插件一起使用。它通过 JSON Schema 定义和验证选项,以确保它们符合预期的格式和类型。

  • validate()(schema, options, context?),验证选项对象是否符合指定的 JSON Schema。

    • schemaobject,定义选项格式的 JSON Schema 对象。

    • optionsobject,需要验证的选项对象。

    • context?object,包含额外信息的上下文对象,例如 namebaseDataPath

    • 返回:

    • 成功:不返回任何值。

    • 失败:抛出错误。

    • js
      const { validate } = require('schema-utils');
      
      const schema = {
        type: 'object',
        properties: {
          option1: { type: 'string' },
          option2: { type: 'number' }
        },
        required: ['option1'],
        additionalProperties: false
      };
      
      const options = {
        option1: 'value',
        option2: 42
      };
      
      try {
        validate(schema, options, { name: 'my-loader' });
        console.log('Options are valid!');
      } catch (error) {
        console.error('Invalid options:', error);
      }

babel

babel: 是一个广泛使用的 JavaScript 编译器,它提供了一些核心 API 用于代码转换。

注意: 以下API属于 @babel/core 。每个API都有3种模式,如:

  • transform(): callbak模式,babel@8中将被删除。
  • transformSync(): 同步模式
  • transformAsync(): 异步Promise模式

API:

  • babel.transformSync()(code, options),用于将源代码转换为不同版本的JS代码。

    • codestring,需要转换的 JS 源代码

    • options{presets, plugins},用于配置 Babel 转换过程的选项。默认已经配置了presets

    • 返回值

    • result{code, map, ast},包含转换结果的对象,主要包括 code(转换后的代码)和 map(源映射)。

      • codestring,转换后的JS代码。
      • mapobject,生成的源映射(source map),如果启用了源映射的话。
      • astobject,转换后的抽象语法树(AST),如果请求了 ast 选项的话。
    • js
      const babel = require('@babel/core');
      
      const result = babel.transform('const a = 1;', {
        presets: ['@babel/preset-env']
      });
      
      console.log(result.code); // 编译后的代码
  • babel.parseSync()(code, options),用于解析源代码并生成抽象语法树(AST)。

    • codestring,要解析的源代码。

    • options{sourceType, plugins},解析的配置选项。

    • 返回:

    • astobject,生成的 AST 对象。

    • js
      const parser = require('@babel/parser');
      
      const ast = parser.parse('const a = 1;', {
        sourceType: 'module'
      });
      
      console.log(ast);
  • transformFromAstSync()(ast, code?, options),用于从 AST 进行转换为代码。

    • astobject,要转换的抽象语法树。

    • code?string,原始源代码(用于错误定位)。

    • optionsobject,Babel 编译选项。

    • 返回: Promise

    • result{code, map},返回一个对象,包含 code(编译后的代码)、map(源映射)等信息。

    • js
      const babel = require('@babel/core');
      const parser = require('@babel/parser');
      const generate = require('@babel/generator').default;
      
      const ast = parser.parse('const a = 1;');
      const result = babel.transformFromAstSync(ast, 'const a = 1;', {
        presets: ['@babel/preset-env']
      });
      
      console.log(result.code); // 编译后的代码
  • loadPartialConfig()(config?),用于加载更新 Babel 的部分配置文件。

    • config?object,包含了你想要更新或加载的部分配置选项。

    • 返回:

    • partialConfigPartialConfig,包含配置的对象,其中包括 optionsfile

    • js
      const babel = require('@babel/core');
      
      // 加载部分配置
      const partialConfig = babel.loadPartialConfig({
        presets: ['@babel/preset-env'],
        plugins: ['@babel/plugin-transform-arrow-functions']
      });
      
      // 访问配置和其他信息
      console.log(partialConfig.config);

marked

marked: 是一个流行的 Markdown 解析器,将 Markdown 转换为 HTML

API:

  • new Marked()(...markedExtension[]),用于创建 Marked 实例的构造函数。

    • markedExtensionMarkedExtension,扩展的插件接口。主要用于在 marked 的解析过程中插入自定义逻辑。

    • 返回:

    • markedobject,返回一个Marked实例。

    • js
      // 使用自定义渲染器
      const renderer = new marked.Renderer();
      renderer.heading = (text, level) => `<h${level} class="custom-heading">${text}</h${level}>`;
      
      const markdown = '# Custom Heading';
      const html = marked(markdown, { renderer });
      
      console.log(html); // <h1 class="custom-heading">Custom Heading</h1>
  • marked.parse()(markdown, options?),用于将 Markdown 文本转换为 HTML。

    • markdownstring,要解析的 Markdown 字符串。

    • optionsobject,配置选项对象,用于调整解析和渲染行为。

      • rendererboolean,自定义渲染器对象,用于修改 HTML 输出。
      • gfmboolean默认:true,是否启用 GitHub 风格的 Markdown 语法。
      • breaksboolean默认:false,是否将换行符转换为<br>标签。
      • pedanticboolean默认:false,是否宽容解析 Markdown 语法。
      • sanitizeboolean默认:false,是否移除 HTML 标签。
      • smartListsboolean默认:false,是否优化列表输出。
      • smartypantsboolean默认:false,是否使用智能引号。
    • 返回:

    • htmlstring,返回HTML字符串。

    • js
      const marked = require('marked');
      
      const markdown = '# Hello, World!\n\nThis is a paragraph with **bold** text.';
      const html = marked.parse(markdown);
      
      console.log(html);
      // Output:
      // <h1>Hello, World!</h1>
      // <p>This is a paragraph with <strong>bold</strong> text.</p>

marked-highlight

marked-highlight:用于在Markdown中高亮代码的库。它将 marked 和 highlight.js结合。

API:

  • markedHighlight()({highlight}),高亮代码块。

    • highlight(code, lang) => void,转换代码为html

    • langPrefix?string默认:,class前缀

    • async?boolean默认:false,如果highlight方法返回一个Promise,就设置该选项为true

    • 返回:

    • markedExtension MarkedExtension ,返回一个扩展的插件接口。主要用于在 marked 的解析过程中插入自定义逻辑。

    • js
        const marked = new Marked(
          markedHighlight({
            langPrefix: 'hljs language-',
            highlight: function (code, lang, info) {
              const language = hljs.getLanguage(lang) ? lang : 'plaintext'
              return hljs.highlight(code, { language }).value
            }
          })
        )
        const html = marked.parse(content)

highlight.js

highlight.js:用于高亮代码的库,支持多种编程语言。它的 API 允许你自定义代码高亮的行为。

API:

  • highlight()(code, {language?}),用于高亮代码,支持指定语言。

    • codestring,要高亮的代码字符串。

    • language?string,要使用的编程语言。如果未指定,highlight.js 会尝试自动检测语言。

    • 返回:

    • result{value},返回一个对象,包含 value 属性,其中存储了高亮后的 HTML 字符串。

    • js
      const hljs = require('highlight.js');
      
      const code = 'const x = 42;';
      const result = hljs.highlight(code, { language: 'javascript' }).value;
      
      console.log(result);
      // Output: <span class="hljs-keyword">const</span> x = <span class="hljs-number">42</span>;
  • getLanguage()(lang),用于获取特定语言的定义。

    • langstring,语言名称字符串,如 'javascript''python' 等。

    • 返回:

    • languageobject,返回一个包含语言定义的对象,或者如果语言未定义,则返回 undefined

    • js
      const hljs = require('highlight.js');
      
      // 用于检查 highlight.js 是否支持某种语言
      const languageExists = hljs.getLanguage('javascript') !== undefined;
      
      console.log(languageExists);
      // Output: true

自定义Loader

Loader 是用于对模块的源代码进行转换(处理),之前我们已经使用过很多 Loader,比如 css-loader、style-loader、babel-loader 等。

这里我们来学习如何自定义自己的 Loader:

  • Loader 本质上是一个导出为函数的 JavaScript 模块
  • 注意: loader导出时,建议使用commonjs语法导出:module.exportsexports
  • loader-runner 库会调用这个函数,然后将上一个 loader 产生的结果或者资源文件传入进去;
  • 注意: loader最终返回的结果必须是模块化的内容

自定义loader

编写一个 xxx-loader01.js 模块这个函数会接收三个参数

  • xxx-loader()(content, map, meta),自定义的loader

    • content:``,资源文件的内容

    • map:``,sourcemap 相关的数据

    • meta:``,一些元数据

    • 返回:

    • return:``,

    • this.callback()(err, content),回调函数

      • errError | null,错误信息。如果没错,则传入null
      • contentstring | buffer,传递给下个loader的内容
    • js
      module.exports = function (content, map, meta) {
        console.log('xxx-loader: ', content)
        // console.log('xxx-loader: ', map)
        // console.log('xxx-loader: ', meta)
      
        return 'xxx-loader'
      }

1、自定义一个loader

image-20240229143157726

2、使用loader

image-20240906113926764

3、使用多个loader

image-20240906114241641

4、打包效果

image-20240906114432644

resolveLoader

  • resolveLoader:这组选项与 resolve 对象的属性集合相同, 但仅用于解析 webpack 的 loader 包
    • modulesstring[],告诉 webpack 解析模块时应该搜索的目录,默认值:['node_modules']

如果我们依然希望可以直接去加载自己的 loader 文件夹,有没有更加简洁的办法呢?

1、配置 resolveLoader 属性

注意: 传入的路径和 context 是有关系的,在前面我们讲入口的相对路径时有讲过。context会影响到entryloader中的路径起始点

image-20240906115312059

2、此时loader就可以这样写了,也可以找到

image-20240313180800481

loader执行顺序

loader执行顺序

创建多个 Loader 使用,它的执行顺序是什么呢?

  • 从后向前、从右向左

image-20240229143229852

image-20240229143236115

pitch-loader

事实上还有另一种 Loader,称之为 PitchLoader

pitch loader: 允许你在真正的 loader 之前插入逻辑,并可以决定是否继续处理后续的 loader。

语法:

pitch loader 是一个函数,在加载过程中,Webpack 会调用它来处理文件。

  • xxx-loader-pitch()(remainingRequest, precedingRequest, data),一个函数,在加载过程中,Webpack 会调用它来处理文件。

    • remainingRequeststring,剩下的请求

      • precedingRequeststring,之前处理过的请求

      • dataobject,loader 共享的数据

    • 返回:

    • 不返回:``,继续执行后续的loader

    • 返回值:``,终止执行后续的loader

    • js
      module.exports.pitch = function pitchLoader(remainingRequest, precedingRequest, data) {
        // `remainingRequest` 是从当前 loader 到最后一个 loader 的请求路径
        // `precedingRequest` 是当前 loader 之前的所有 loader 的请求路径
        + // `data` 是 loader 之间共享的数据
      
        // 可以根据 remainingRequest 来决定是否继续处理
        if (remainingRequest.includes('specificFile.js')) {
          // 如果特定文件存在,则忽略后续的 loader
          return `module.exports = 'This module is handled by pitch loader only.';`;
        }
      
        // 继续使用后续 loader
        // 返回 null 或者不返回任何值
      };

image-20240229143245287

image-20240912124936205

enforce控制执行顺序

loader执行顺序的内部实现:

其实这也是为什么 loader 的执行顺序是相反的:

  • run-loader 优先执行 PitchLoader,在执行 PitchLoader 时进行 loaderIndex++

  • run-loader 之后执行 NormalLoader,在执行 NormalLoader 时进行 loaderIndex--

修改loader执行顺序:

  • rules[{test, use, loader, type, exclude, include, parser, generator, enforce},...],规则集合
    • testreg,匹配文件资源

    • use[{loader, options, query},...],设置对匹配到的资源使用的loader及配置

      • loaderstring,使用的loader。示例:use: [{loader: 'css-loader'}],简写:use: ['css-loader']
      • optionsobject,loader的配置项。示例:use: [{loader: 'css-loader', options: {importLoaders: 1}}]
      • query:``,已被options替代
      • 注意: use中多个loader的使用顺序是从后往前
    • loaderRule.use[{loader}]的简写

    • enforcepre | post | normal | inline,用于控制 loader 执行顺序的选项。

      • pre:指定该 loader 在所有其他 loader 之前执行。常用于进行某些预处理,例如代码风格检查。

      • post:指定该 loader 在所有其他 loader 之后执行。常用于进行某些后处理,例如添加某些特性或优化。

      • normal默认,loader 将按照 Webpack 的默认顺序执行。

      • inline:在行内设置的 loader。如:import 'loader1!loader2!./test.js'

      • js
            rules: [
              {
                test: /\.js$/,
                use: [{ loader: 'xxx-loader' }],
                enforce: 'pre' // 1
              },
              { // 2
                test: /\.js$/,
                use: [{ loader: 'yyy-loader' }]
              },
              {
                test: /\.js$/,
                use: [{ loader: 'zzz-loader' }],
                enforce: 'post' // 3
              }
            ]

那么,能不能改变它们的执行顺序呢?

  • 我们可以拆分成多个 Rule 对象,通过 enforce 来改变它们的顺序;

在 Pitching 和 Normal 它们的执行顺序分别是:

  • Pitching: post, inline, normal, pre;

  • Normal: pre, normal, inline, post;

image-20240906122137941

image-20240906122037106

同步、异步Loader

同步Loader

什么是同步的 Loader 呢?

  • 默认创建的 Loader 就是同步的 Loader

  • 这个 Loader 必须通过 return 或者 this.callback 来返回结果,交给下一个 loader 来处理

  • 通常在有错误的情况下,我们会使用 this.callback;

this.callback 的用法如下:

  • this.callback()(err, content),回调函数
    • errError | null,错误信息。如果没错,则传入null
    • contentstring | buffer,传递给下个loader的内容

image-20240229143302990

异步Loader

什么是异步的 Loader 呢?

  • 有时候我们使用 Loader 时会进行一些异步的操作;

  • 我们希望在异步操作完成后,再返回这个 loader 处理的结果;

  • 这个时候我们就要使用异步的 Loader 了;

loader-runner 已经在执行 loader 时给我们提供了方法,让 loader 变成一个异步的 loader:

js
// 通过调用this.async(),告诉loader不要在函数末尾直接return undefined,而是返回异步操作返回的结果
const callback = this.async()

image-20240229143312512

参数

传入、获取参数

1、传递参数

image-20240229143329205

2、获取参数

  • 方式一:(废弃),早期需要使用 loader-utils 库来获取参数,目前已经不再需要
  • 方式二:目前可以直接通过 this.getOptions() 方法获取参数

image-20240906152734030

image-20240906152749090

校验参数

1、我们可以通过一个 webpack 官方提供的校验库 schema-utils 安装对应的库:

sh
npm install schema-utils -D

2、传递参数

image-20240906153538400

3、校验规则

image-20240229143354683

4、校验参数是否符合规则

image-20240906153435074

5、校验失败

image-20240906153440166

案例

mr-babel-loader

我们知道 babel-loader 可以帮助我们对 JavaScript 的代码进行转换,这里我们定义一个自己的 babel-loader:

一、依赖包: @babel/core

二、实现过程

1、使用 babel.transform() 方法转换js代码

image-20240906155816017

此时打包会发现babel并没有转换ES6的语法为ES5

2、在使用babel-loader时,传递plugins、presets打包参数

image-20240906160109293

image-20240906160432941

3、在自定义的babel-loader中获取传递的options参数

image-20240906160250464

image-20240906160253696

image-20240906160519762

4、添加参数校验

image-20240906160635095

image-20240906160655652

5、在babel.config.js文件中配置babel参数

  • 配置参数

    image-20240906160823042

  • 获取参数

    image-20240906160947170

    image-20240906161007911

mr-md-loader

作用: hymd-loader用来解析markdown文件

依赖包:

  • marked:marked 是一个基于JS的 Markdown 解析器和编译器。
    • 安装:pnpm i marked -D
  • highlight.js:代码高亮插件。
    • 安装:pnpm i highlight.js -D
  • marked-highlight:用于在Markdown中高亮代码的库。它将 marked 和 highlight.js结合。
    • 安装:pnpm i marked-highlight -D

实现过程:

1、使用mr-md-loader解析md文件

image-20240906162310979

2、mr-md-loader基本实现

由于loader返回的结果必须是一个模块化的内容,此处在得到html文本后需要保存到code变量并导出出去。

image-20240906163243224

3、在main.js中导入md文件,并显示到页面中

image-20240906164127291

4、显示效果

问题:此时的样式优点丑,需要优化样式

image-20240906164021823

5、优化: 添加自定义的CSS样式

  • css样式

    image-20240906164555717

  • 导入样式

    image-20240906164422040

  • 配置css-loader

    image-20240906164503487

  • 效果

    image-20240906164610042

6、优化: 高亮关键字

  • 使用highlight.js插件标识出md内容的关键字

    image-20240906165359123

    image-20240906165422503

  • 自定义关键字的样式

  • image-20240906165800164

    image-20240906165558259

    image-20240906165601357

  • 使用highlight.js库默认的样式

    image-20240906165659925

    image-20240906165811243

自定义Plugin

Plugin API

tapable

tapable: 是一个用于处理插件系统的 JS 库,通常用于构建和扩展系统中的钩子(hooks)和事件。这是一个被广泛使用的库,尤其是在 Webpack 和其他构建工具中。

API:

  • 实例方法

  • hook.tap()(pluginName, fn),用于同步钩子的注册方法。它用于注册一个钩子函数,并指定一个插件名称。

    • pluginNamestring,插件名称。

    • fn(...args) => void,钩子函数。

    • js
      hook.tap('SecondPlugin', (name, age) => {
        console.log('SecondPlugin:', name, age);
      });
  • hook.tapAsync()(pluginName, fn),用于注册异步钩子的方法,适用于 AsyncSeriesHookAsyncParallelHook 等异步钩子类型。

    • pluginNamestring,插件名称。

    • fn(...args, callback) => void,钩子函数。在操作完成后调用 callback()。

    • js
      hookAsync.tapAsync('SecondPlugin', (name, callback) => {
        setTimeout(() => {
          console.log('SecondPlugin:', name);
          callback(); // 完成异步操作
        }, 500);
      });
  • hook.call()(...args),用于同步地触发所有注册的钩子函数。按注册顺序执行。

    • ...argsstring[],指定钩子所需的参数。

    • js
      const { SyncHook } = require('tapable');
      
      // 创建一个 SyncHook 实例
      const hook = new SyncHook(['name', 'age']);
      
      // 注册钩子函数
      hook.tap('PrintName', (name, age) => {
        console.log(`Name: ${name},Age: ${age}`);
      });
      
      // 触发钩子
      hook.call('Alice', 30);
  • hook.callAsync()(...args, callback),用于触发异步钩子的一个方法,并在所有钩子完成后执行一个最终的回调。

    • ...argsstring[],传递给钩子函数的参数。

    • callback(err?) => void,所有钩子函数执行完成后的回调函数,接受一个可选的错误参数。

    • js
      const { AsyncSeriesHook } = require('tapable');
      
      // 创建 AsyncSeriesHook 实例
      const hook = new AsyncSeriesHook(['arg1', 'arg2']);
      
      // 注册异步钩子
      hook.tapAsync('Plugin1', (arg1, arg2, callback) => {
        setTimeout(() => {
          console.log('Plugin1:', arg1, arg2);
          callback(); // 异步操作完成后调用 callback
        }, 1000);
      });
      
      hook.tapAsync('Plugin2', (arg1, arg2, callback) => {
        setTimeout(() => {
          console.log('Plugin2:', arg1, arg2);
          callback(); // 异步操作完成后调用 callback
        }, 500);
      });
      
      // 触发钩子
      hook.callAsync('value1', 'value2', (err) => {
        if (err) {
          console.error('Error:', err);
        } else {
          console.log('All hooks executed');
        }
      });
  • 创建实例

  • new SyncHook()([arg1, arg2, ...]),用于创建同步钩子的类。

    • [arg1, arg2, ...]string[],定义了钩子函数所接受的参数列表。

    • 返回:

    • hookSyncHook,一个 SyncHook 实例。

    • js
      const { SyncHook } = require('tapable');
      
      // 创建一个 SyncHook 实例
      const hook = new SyncHook(['name', 'age']);
      
      // 注册钩子函数
      hook.tap('PrintName', (name, age) => {
        console.log(`Name: ${name},Age: ${age}`);
      });
      
      // 触发钩子
      hook.call('Alice', 30);
  • new SyncBailHook()([arg1, arg2, ...]),用于创建同步钩子的类,与 SyncHook 不同的是,它提供了一个“中断”机制。一旦一个钩子函数返回非 undefined 的值,后续的钩子函数将不会被执行。

    • [arg1, arg2, ...]string[],定义了钩子函数所接受的参数列表。

    • 返回:

    • hookSyncBailHook,一个 SyncBailHook 实例。

    • js
      const { SyncBailHook } = require('tapable');
      
      // 创建一个 SyncBailHook 实例
      const hook = new SyncBailHook(['data']);
      
      // 注册钩子函数
      hook.tap('FirstPlugin', (data) => {
        console.log('FirstPlugin:', data);
        // 返回一个值,后续钩子将不会被调用
        return 'Early exit';
      });
      
      hook.tap('SecondPlugin', (data) => {
        console.log('SecondPlugin:', data);
      });
      
      // 触发钩子
      hook.call('Hello, World!');
  • new SyncLoopHook()([arg1, arg2, ...]),用于创建同步钩子的类,允许注册的钩子函数在特定条件下重复执行。每个钩子函数在被调用时会持续执行直到它返回 undefined,然后停止循环

    • [arg1, arg2, ...]string[],定义了钩子函数所接受的参数列表。

    • 返回:

    • hookSyncLoopHook,一个 SyncLoopHook 实例。

    • js
      const { SyncLoopHook } = require('tapable');
      
      // 创建一个 SyncLoopHook 实例
      const hook = new SyncLoopHook(['count']);
      
      // 注册钩子函数
      hook.tap('LoopPlugin', (count) => {
        console.log('Processing:', count);
        // 返回非 undefined 的值,继续循环
        if (count > 0) {
          return count - 1;
        }
        // 返回 undefined,停止循环
        return undefined;
      });
      
      // 触发钩子
      hook.call(5);
  • new SyncWaterfallHook()([arg1, arg2, ...]),用于创建同步钩子的类,它的特点是钩子函数的返回值会传递给下一个钩子函数

    • [arg1, arg2, ...]string[],定义了钩子函数所接受的参数列表。

    • 返回:

    • hookSyncWaterfallHook,一个 SyncWaterfallHook 实例。

    • js
      const { SyncWaterfallHook } = require('tapable');
      
      // 创建一个 SyncWaterfallHook 实例
      const hook = new SyncWaterfallHook(['data']);
      
      // 注册钩子函数
      hook.tap('FirstPlugin', (data) => {
        console.log('FirstPlugin:', data);
        // 修改数据并传递给下一个钩子函数
        return data + 1;
      });
      
      hook.tap('SecondPlugin', (data) => {
        console.log('SecondPlugin:', data);
        // 修改数据并传递给下一个钩子函数
        return data * 2;
      });
      
      hook.tap('ThirdPlugin', (data) => {
        console.log('ThirdPlugin:', data);
        // 最后一个钩子函数的返回值将不会被传递给其他函数
        return data - 3;
      });
      
      // 触发钩子
      hook.call(5);
  • new AsyncParallelHook()([arg1, arg2, ...]),用于创建异步并行执行的钩子。与同步钩子不同,异步并行钩子允许钩子函数并行执行而不是依次执行。

    • [arg1, arg2, ...]string[],定义了钩子函数所接受的参数列表。

    • 返回:

    • hookAsyncAsyncParallelHook,一个 AsyncParallelHook 实例。

    • js
      const { AsyncParallelHook } = require('tapable');
      
      // 创建一个 AsyncParallelHook 实例
      const hookAsync = new AsyncParallelHook(['name']);
      
      // 注册钩子函数
      hookAsync.tapAsync('FirstPlugin', (name, callback) => {
        setTimeout(() => {
          console.log('FirstPlugin:', name);
          callback(); // 完成异步操作
        }, 1000);
      });
      
      hookAsync.tapAsync('SecondPlugin', (name, callback) => {
        setTimeout(() => {
          console.log('SecondPlugin:', name);
          callback(); // 完成异步操作
        }, 500);
      });
      
      // 触发钩子
      hookAsync.callAsync('John', (err) => {
        if (err) {
          console.error('Error:', err);
        } else {
          console.log('All plugins have finished processing');
        }
      });
  • new AsyncSeriesHook()([arg1, arg2, ...]),用于处理异步操作。会等待上一个异步的 Hook 执行完毕

    • [arg1, arg2, ...]string[],定义了钩子函数所接受的参数列表。

    • 返回:

    • hookAsyncAsyncSeriesHook,一个 AsyncSeriesHook 实例。

    • js
      const { AsyncSeriesHook } = require('tapable');
      
      // 创建 AsyncSeriesHook 实例
      const hook = new AsyncSeriesHook(['arg1', 'arg2']);
      
      // 注册钩子
      hook.tapAsync('MyPlugin1', (arg1, arg2, callback) => {
        // 异步操作
        setTimeout(() => {
          console.log('Async operation complete');
          callback(); // 需要调用 callback 来表示操作完成
        }, 1000);
      });
      
      // 会等MyPlugin1执行完毕才执行
      hook.tapAsync('MyPlugin2', (arg1, arg2, callback) => {
        // 异步操作
        setTimeout(() => {
          console.log('Async operation complete');
          callback(); // 需要调用 callback 来表示操作完成
        }, 1000);
      });
      
      // 触发钩子
      hook.callAsync('value1', 'value2', (err) => {
        if (err) {
          console.error('Error:', err);
        } else {
          console.log('All hooks executed');
        }
      });

node-ssh

node-ssh:是一个用于在 Node.js 中简化 SSH 连接和命令执行的库。它提供了一种方便的方式来进行远程管理和自动化操作。

API:

  • new NodeSSH()(),创建SSH实例。

    • 返回

    • sshNodeSSH,SSH实例

    • js
      const { NodeSSH } = require('node-ssh');
      const ssh = new NodeSSH();
  • ssh.connect()({host, port, username, password, privateKey}),连接到远程服务器。

    • hoststring,远程服务器的主机名或 IP 地址。

    • portnumber默认:22,SSH 端口。

    • usernamestring,登录用户名。

    • passwordstring,登录密码(如果你使用密码认证)。

    • privateKeystring,私钥文件路径(如果你使用密钥认证)。

    • 返回: Promise

    • promise() => void

    • js
      await ssh.connect({
        host: 'example.com',
        username: 'your-username',
        password: 'your-password'
      });
  • ssh.execCommand()(command,options?),在远程主机上执行linux命令。

    • commandstring,要执行的linux命令。

    • options?{cwd, stdin},命令执行的选项,包括 cwd (当前工作目录) 和 stdin (标准输入数据)。

    • 返回: Promise

    • result({stdout, stderr}) => void,返回一个包含 stdoutstderr 的Promise对象。

    • js
      const result = await ssh.execCommand('ls -la');
      console.log('STDOUT:', result.stdout);
      console.log('STDERR:', result.stderr);
  • ssh.putFile()(localPath, remotePath),将本地文件上传到远程主机。

    • localPathstring,本地文件的路径。

    • remotePathstring,远程主机上的目标路径。

    • 返回: Promise

    • js
      await ssh.putFile('local/file/path', 'remote/file/path');
  • ssh.getFile()(localPath, remotePath),从远程主机下载文件到本地。

    • localPathstring,本地目标路径。

    • remotePathstring,远程文件的路径。

    • 返回: Promise

    • js
      await ssh.getFile('local/file/path', 'remote/file/path');
  • ssh.putDirectory()(localDir, remoteDir,options?),将本地目录递归地上传到远程服务器。

    • localDirstring,本地目录的路径,指定要上传的目录。

    • remoteDirstring,远程服务器上的目标目录路径。

    • options?{recursive?, concurrency?, overwrite?, tick?},配置上传的选项。

      • recursive?boolean默认:true,是否递归地上传子目录
      • concurrency?number默认:10,并发上传的并发数。
      • overwrite?boolean默认:true,是否覆盖远程目录中已存在的文件。
      • tick?(localPath, remotePath, error) => void,回调函数,接收每个文件的上传进度。
    • 返回: Promise

    • js
          const localDir = path.resolve(__dirname, 'local-directory');
          const remoteDir = '/path/on/remote/server';
      
          await ssh.putDirectory(localDir, remoteDir, {
            recursive: true,
            overwrite: true,
            tick: (localPath, remotePath, error) => {
              if (error) {
                console.error(`Failed to upload ${localPath}:`, error);
              } else {
                console.log(`Uploaded ${localPath} to ${remotePath}`);
              }
            }
          });
  • ssh.getDirectory()(localDir, remoteDir,options?),从远程服务器递归地下载目录到本地。

    • localDirstring,本地目标目录的路径。

    • remoteDirstring,远程服务器上的源目录路径。

    • options?{recursive?, concurrency?, overwrite?, tick?},配置下载的选项。

      • recursive?boolean默认:true,是否递归地下载子目录
      • concurrency?number默认:10,并发下载的并发数。
      • overwrite?boolean默认:true,是否覆盖远程目录中已存在的文件。
      • tick?(localPath, remotePath, error) => void,回调函数,接收每个文件的下载进度。
    • js
          const remoteDir = '/path/on/remote/server';
          const localDir = path.resolve(__dirname, 'local-directory');
      
          await ssh.getDirectory(remoteDir, localDir, {
            recursive: true,
            overwrite: true,
            tick: (remotePath, localPath, error) => {
              if (error) {
                console.error(`Failed to download ${remotePath}:`, error);
              } else {
                console.log(`Downloaded ${remotePath} to ${localPath}`);
              }
            }
          });
  • ssh.dispose()(),关闭与远程主机的连接并清理资源。

    • js
      ssh.dispose();

Tapable

Tapable概述

我们知道 webpack 有两个非常重要的类:Compiler 和 Compilation

  • 他们通过注入插件的方式,来监听 webpack 的所有生命周期;

  • 插件的注入离不开各种各样的 Hook,而他们的 Hook 是如何得到的呢?

  • 其实是创建了 Tapable 库中的各种 Hook 的实例;

所以,如果我们想要学习自定义插件,最好先了解一个库:Tapable

  • Tapable 是官方编写和维护的一个库;

  • Tapable 管理着需要的Hook,这些Hook可以被应用到我们的插件中

Tapable的Hook

image-20240906170845919

同步和异步的:

  • sync 开头的,是同步的Hook。

  • async 开头的,是异步的Hook,两个事件处理回调,不会等待上一次处理回调结束后再执行下一次回调。

其他的类别:

  • Bail:当有返回值时,就不会执行后续的事件触发了;

  • Loop:当返回值为 true,就会反复执行该事件,当返回值为 undefined 或者不返回内容,就退出事件,执行下一个事件;

  • Waterfall:当返回值不为 undefined 时,会将这次返回的结果作为下次事件的第一个参数

  • Parallel并行,不会等到上一个事件回调执行结束,才执行下一次事件处理回调;

  • Series串行,会等待上一个异步的 Hook;

Hook的使用

依赖包:

  • tapable:通过提供 Hooks 系统,使得你可以在 Webpack 的构建流程中插入自定义的逻辑。
    • 安装:pnpm i tapable
sync-基本使用

1、创建Hook对象

image-20240906172034455

2、监听Hook中的事件

注意: 自定义的插件就是写在这个位置

image-20240906172352328

3、触发事件

image-20240906172510113

sync-bail使用

Bail:当有返回值时,就不会执行后续的事件触发了。

1、创建bailHook

image-20240906173515157

2、监听Hook中的事件

image-20240906173339089

3、触发事件

image-20240906173132909

sync-loop使用

Loop:当返回值为 true,就会反复执行该事件,当返回值为 undefined 或者不返回内容,就退出事件,执行下一个事件;

1、创建loopHook

image-20240906174022679

2、监听Hook中的事件

image-20240906173928329

image-20240906173745108

3、触发事件

image-20240906173810514

sync-waterfall使用

Waterfall:当返回值不为 undefined 时,会将这次返回的结果作为下次事件的第一个参数

1、创建waterfallHook

image-20240906174230575

2、监听Hook中的事件

image-20240906174539881

image-20240906174418334

3、触发事件

image-20240906174210585

async-parallel使用

Parallel:并行,不会等到上一个事件回调执行结束,才执行下一次事件处理回调;

1、创建parallelHook

image-20240906174947332

2、监听Hook中的事件

image-20240906175412393

image-20240906175343282

3、触发事件

image-20240906175242742

async-series使用

Series:串行,会等待上一个异步的 Hook;

1、创建seriesHook

image-20240906175548918

2、监听Hook中的事件

image-20240906175952486

image-20240906180023979

3、触发事件

image-20240906175929080

自定义Plugin

在之前的学习中,我们已经使用了非常多的 Plugin:

  • CleanWebpackPlugin

  • HTMLWebpackPlugin

  • MiniCSSExtractPlugin

  • CompressionPlugin

  • 等等。。。

这些 Plugin 是如何被注册到 webpack 的生命周期中的呢?

  • 第一:在 webpack 函数的 createCompiler 方法中,注册了所有的插件;

  • 第二:在注册插件时,会调用插件函数或者插件对象的 apply 方法;

  • 第三:插件方法会接收 compiler 对象,我们可以通过 compiler 对象来监听 Hook 的事件;

  • 第四:某些插件也会传入一个 compilation 的对象,我们也可以监听 compilation 的 Hook 事件;

auto-upload-webpack-plugin

如何开发自己的插件呢?

  • 目前大部分插件都可以在社区中找到,但是推荐尽量使用在维护,并且经过社区验证的;

  • 这里我们开发一个自己的插件:将静态文件自动上传服务器中

依赖包:

  • node-ssh:在node中通过ssh连接远程服务器
    • 安装:pnpm i node-ssh -D

自定义插件:

1、创建 AutoUploadWebpackPlugin 类;

image-20240906210918565

2、编写 apply 方法:

  • 获取输出文件夹路径

  • 通过 ssh 连接服务器;

  • 删除服务器原来的文件夹;

  • 上传文件夹中的内容;

js
const { NodeSSH } = require('node-ssh')
const { PASSWORD } = require('./config')

class AutoUploadWebpackPlugin {
  constructor(options) {
    this.ssh = new NodeSSH()
    this.options = options
  }

  apply(compiler) {
    // console.log("AutoUploadWebpackPlugin被注册:")
    // 完成的事情: 注册hooks监听事件
    // 等到assets已经输出到output目录上时, 完成自动上传的功能
    compiler.hooks.afterEmit.tapAsync("AutoPlugin", async (compilation, callback) => {
      // 1.获取输出文件夹路径(其中资源)
      const outputPath = compilation.outputOptions.path

      // 2.连接远程服务器 SSH
      await this.connectServer()

      // 3.删除原有的文件夹中内容
      const remotePath = this.options.remotePath
      this.ssh.execCommand(`rm -rf ${remotePath}/*`)

      // 4.将文件夹中资源上传到服务器中
      await this.uploadFiles(outputPath, remotePath)

      // 5.关闭ssh连接
      this.ssh.dispose()

      // 完成所有的操作后, 调用callback()
      callback()
    })
  }

  async connectServer() {
    await this.ssh.connect({
      host: this.options.host,
      username: this.options.username,
      password: this.options.password
    })
    console.log('服务器连接成功')
  }

  async uploadFiles(localPath, remotePath) {
    const status = await this.ssh.putDirectory(localPath, remotePath, {
      recursive: true,
      concurrency: 10
    })
    if (status) {
      console.log("文件上传服务器成功~")
    }
  }
}

module.exports = AutoUploadWebpackPlugin
module.exports.AutoUploadWebpackPlugin = AutoUploadWebpackPlugin

3、在 webpack 的 plugins 中,使用 AutoUploadWebpackPlugin 类;

js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const AutoUploadWebpackPlugin = require('./plugins/AutoUploadWebpackPlugin')
const { PASSWORD } = require('./plugins/config')

module.exports = {
  entry: "./src/main.js",
  output: {
    path: path.resolve(__dirname, "./build"),
    filename: "bundle.js"
  },
  plugins: [
    new HtmlWebpackPlugin(),
    new AutoUploadWebpackPlugin({
      host: "123.207.32.32",
      username: "root",
      password: PASSWORD,
      remotePath: "/root/test"
    })
  ]
}