作者: | 来源:互联网 | 2023-08-14 13:10
webpack插件入门-最近写了一个移动端项目,不过每次build的时候还需要手动上传服务器感觉很不方便,毕竟每次删除文件夹然后拖拽上传的过程太重复了,本着不重复造轮子的原则去G
最近写了一个移动端项目,不过每次 build 的时候还需要手动上传服务器感觉很不方便,毕竟每次删除文件夹然后拖拽上传的过程太重复了,本着不重复造轮子的原则去 Github 翻了一下,发现 Upload
上传插件还是蛮多的,不过距离自己的要求还是有些差异,很多插件只是只是单一职责,只负责上传这件事情。
而如果只负责上传文件不做删除会导致服务器文件越来越多,占用额外的储存成本,WebPack 在 build 过程中会检测相关依赖是否变更,如果变更相关文件的 hash
也是发生变更,这样就会导致新的文件上传到服务器,而旧资源却不会被覆盖替换掉。
基本概念
WebPack 的插件是基于 Tapable
实现的,它是一种发布订阅的实现,作用就是将插件的各个生命周期钩子广播出去,然后在合适的时机执行。同时只让插件关注自身的订阅,保证插件组合起来有序进行。
Tapable
暴露了三个方法:
- tap: 可以注册同步钩子和异步钩子
- tapAsync: 回调形式注册异步钩子
- tapPromise: Promise 形式注册异步钩子
在编写插件时 WebPack 显示要求我们有 apply
方法,这样做的原因是 WebPack 执行期间会执行 apply 方法,并且注入compiler
,之后在compiler
上订阅钩子事件,在合适时间触发已订阅的 apply 方法
再看一下官方给出的示例代码
class MyExampleWebpackPlugin {
apply(compiler) {
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
);
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 文件
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
和钩子,完成这个上传过程
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');
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
的连接信息加上自定义扩展的一些字段而来的
export interface Option {
src?: string;
to: string;
port?: number;
host: string;
username?: string;
password?: string;
privateKey?: string;
}
最后
完整代码已经上传了Github 仓库,如果你有兴趣可以具体看下更具体的一些信息,如果对你有帮助也欢迎 star
。