Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Webpack 插件入门 #69

Open
bosens-China opened this issue Jun 4, 2021 · 0 comments
Open

Webpack 插件入门 #69

bosens-China opened this issue Jun 4, 2021 · 0 comments
Labels
工具相关 工程化相关的东西

Comments

@bosens-China
Copy link
Owner

webpack 插件入门

bg

最近写了一个移动端项目,不过每次 build 的时候还需要手动上传服务器感觉很不方便,毕竟每次删除文件夹然后拖拽上传的过程太重复了,本着不重复造轮子的原则去 Github 翻了一下,发现 Upload上传插件还是蛮多的,不过距离自己的要求还是有些差异,很多插件只是只是单一职责,只负责上传这件事情。

而如果只负责上传文件不做删除会导致服务器文件越来越多,占用额外的储存成本,WebPack 在 build 过程中会检测相关依赖是否变更,如果变更相关文件的 hash 也是发生变更,这样就会导致新的文件上传到服务器,而旧资源却不会被覆盖替换掉。

基本概念

WebPack 的插件是基于 Tapable 实现的,它是一种发布订阅的实现,作用就是将插件的各个生命周期钩子广播出去,然后在合适的时机执行。同时只让插件关注自身的订阅,保证插件组合起来有序进行。

Tapable暴露了三个方法:

  • tap: 可以注册同步钩子和异步钩子
  • tapAsync: 回调形式注册异步钩子
  • tapPromise: Promise 形式注册异步钩子

在编写插件时 WebPack 显示要求我们有 apply 方法,这样做的原因是 WebPack 执行期间会执行 apply 方法,并且注入compiler,之后在compiler上订阅钩子事件,在合适时间触发已订阅的 apply 方法

再看一下官方给出的示例代码

// A JavaScript class.
class MyExampleWebpackPlugin {
  // Define `apply` as its prototype method which is supplied with compiler as its argument
  apply(compiler) {
    // Specify the event hook to attach to
    compiler.hooks.emit.tapAsync('MyExampleWebpackPlugin', (compilation, callback) => {
      console.log('This is an example plugin!');
      console.log(
        'Here’s the `compilation` object which represents a single build of assets:',
        compilation
      );

      // Manipulate the build using the plugin API provided by webpack
      compilation.addModule(/* ... */);

      callback();
    });
  }
}

上面插件在compiler中订阅了 emit 的异步钩子,然后做了一些操作之后,执行 callback() 回调

这里稍微说下,对于 tapAsync 的钩子,callback 必须执行,否则程序会一致在等待,而 callback 左侧的 compilation 是用来访问这一次的资源构建信息,例如一些输出的资源,相互依赖的关系等。

了解了上面的信息,我们找一下 compiler 钩子 有没有我们需要的,文档中列举的钩子很多:

  • environment
  • afterEnvironment
  • entryOption
  • ...

翻到最后会看到一个 done 的钩子,它在 compilation 完成时执行。

这里我们需要的前置基本准备齐全了,下面要做的就是在 done 触发时

  • 连接 ssh 服务器,执行rm-rf xx的操作
  • 上传 build 后的资源到 xx 目录下

插件开发准备

之后的内容采用 TypeScript 作为开发,如果你没有相关经验直接跳过类型注释即可

为了方便解耦和复用文件,我们创建了一个 utils.ts 文件

// utils.ts
import { NodeSSH } from 'node-ssh';

import { Option } from './typings';

export const isObject = (obj: any): obj is Object => typeof obj === 'object' && obj;

// 删除文件夹
export const removeDir = async (option: Option) => {
  const ssh = new NodeSSH();
  await ssh.connect(option);
  await ssh.execCommand(`rm -rf ${option.to}`);
  await ssh.dispose();
};

// 上传文件夹
export const uploadDir = async (option: Option) => {
  const ssh = new NodeSSH();
  await ssh.connect(option);
  await ssh.putDirectory(option.src!, option.to, {
    recursive: true,
  });
  await ssh.dispose();
};

它暴露三个方法,删除文件夹和上传文件夹还有一个判断 object 的方法,上面的删除和上传文件夹基于 node-ssh 封装而来,如果你有兴趣了解可以去阅读一下文档

插件开发

剩下的插件开发,就是获取用户填写一些必要字段,例如密码、上传的服务器路径、host 等信息,结合上面的 utils 和钩子,完成这个上传过程

// upload-plugin.ts
import { Compiler, Stats } from 'webpack';
import { isObject, uploadDir, removeDir } from './utils';
import { Option } from './typings';

class UploadPlugin {
  public stats: Stats;

  public option: Option & Record<string, any>;

  public removeDir: boolean;

  constructor(option: Option, remove = true) {
    this.stats = null as unknown as Stats;
    this.option = option;
    this.removeDir = remove;
    this.init();
  }

  init() {
    this.checkOption();
    this.setOption();
  }

  // 检验参数
  checkOption(option = this.option) {
    if (!isObject(option)) {
      throw new Error('option Must be an object!');
    }
    const result = ['to', 'host'].filter((f) => !option[f]);
    if (result.length) {
      throw new Error(`The ${result.join(',')} parameter is required!`);
    }
    if (!option.password && !option.privateKey) {
      throw new Error('password and privateKey must have one entry!');
    }
  }

  // 初始化默认值
  setOption() {
    const option = {
      port: 22,
      username: 'root',
    };
    this.option = {
      ...option,
      ...this.option,
    };
  }

  apply(compiler: Compiler) {
    compiler.hooks.done.tap('upload-plugin', async (stats) => {
      console.time('time');
      // 获取默认的信息,如果src不存在直接使用webpack的配置
      const src = stats.compilation.outputOptions.path;
      this.option.src = this.option.src ?? src;
      if (this.removeDir) {
        await removeDir(this.option);
      }
      await uploadDir(this.option);
      console.timeEnd('time');
    });
  }
}

export default UploadPlugin;

整体代码还是很简洁的,去除参数校验部分还有赋值默认值参数,剩下的就是根据参数来是否删除远程文件夹,之后执行上传方法。

你可能很好奇 Option 的定义是啥,这个是结合 node-ssh 的连接信息加上自定义扩展的一些字段而来的

// typings.d.ts
export interface Option {
  src?: string;
  to: string;
  port?: number;
  host: string;
  username?: string;
  password?: string;
  privateKey?: string;
}

最后

完整代码已经上传了Github 仓库,如果你有兴趣可以具体看下更具体的一些信息,如果对你有帮助也欢迎 star

@bosens-China bosens-China added the 工具相关 工程化相关的东西 label Jun 4, 2021
@bosens-China bosens-China changed the title webpack 插件入门 WebPack 插件入门 Dec 21, 2021
@bosens-China bosens-China changed the title WebPack 插件入门 Webpack 插件入门 Dec 21, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
工具相关 工程化相关的东西
Projects
None yet
Development

No branches or pull requests

1 participant