之前开发过一款chrome插件,当时使用了一个vue-cli-plugin-browser-extension,配合vue开发chrome插件非常的丝滑,尤其是可以auto reload这块非常的感兴趣。
今天有时间就仔细研究了下
vue-cli-plugin-browser-extension
先说一下使用,在vue.config.js中配置如下:
pluginOptions: {
browserExtension: {
components: {
background: true,
contentScripts: true,
},
componentOptions: {
contentScripts: {
entries: {
content: "src/content.ts",
inject: "src/inject/index.ts",
},
},
background: {
entry: "src/background.ts",
}
}
}
}
readme.md中写的也非常的详细。
具体是怎么实现的呢?
查看package.json的声明,发现入口在index.js
{
"main":"index.js"
}
非常熟悉的入口函数,直到后来看了vue-cli的源码,才知道这里面的api, options到底是怎么回事,有兴趣的可以阅读下@vue/cli-serve的源码。
因为对auto reload非常感兴趣,所以只关注这块的逻辑
// index.js
const ExtensionReloader = require('webpack-extension-reloader')
module.exports = (api, options) => {
api.chainWebpack();
// 重点在这里,使用了一个plugin
webpackConfig.plugin('extension-reloader').use(ExtensionReloader, ...);
}
再次查看下package.json的devDependenci,还真的有依赖这个
"dependencies": {
"@vue/cli-shared-utils": "^3.0.0-rc.3",
"copy-webpack-plugin": "^5.1.2",
"imports-loader": "^0.8.0",
"webextension-polyfill": "^0.4.0",
"webpack": "^4.16.0",
"webpack-extension-reloader": "^1.1.0",
"zip-webpack-plugin": "^3.0.0"
}
那很明显auto reload就是使用了webpack-extension-reloader插件。
webpack-extension-reloader
还是首先看下package.json的main
{
"main": "dist/webpack-extension-reloader.js",
"repository": {
"type": "git",
"url": "git://github.com/rubenspgcavalcante/webpack-extension-reloader.git"
}
}
发现npm install后,只有dist代码,当然我是拒绝看这种build之后的代码,一般来说都是有GitHub地址的。
我们clone下来直接查看GitHub的源代码。
那这个main入口代码是如何生成的呢?发现有个webpack.config.js,里面有这个配置
module.exports={
entry: test({ tests: "./specs/index.ts" }) || {
[packName]: "./src/index.ts",
[`${packName}-cli`]: "./client/index.ts"
},
output: {
publicPath: ".",
path: path.resolve(__dirname, "./dist"),
filename: "[name].js",
libraryTarget: "umd"
},
}
源码指向了src/index.ts
import ExtensionReloaderImpl from "./ExtensionReloader";
export = ExtensionReloaderImpl;
又指向了ExtensionReloader,因为是个webpack插件,所以直接看apply函数即可(不懂的亲自写个webpack plugin就明白了)
import { middlewareInjector } from "./middleware";
export default class ExtensionReloaderImpl extends AbstractPluginReloader{
apply(compiler:Compiler){
this._registerPlugin(compiler);
}
_registerPlugin(){
this._injector = middlewareInjector(parsedEntries, { port, reloadPage });
}
}
这里的重点又指向了src/middleware
import _middlewareInjector from "./middleware-injector";
export const middlewareInjector = _middlewareInjector;
再看src/middleware/middleware-injector
import middleWareSourceBuilder from "./middleware-source-builder";
const middlewareInjector: MiddlewareInjector = (
{ background, contentScript, extensionPage },
{ port, reloadPage },
) => {
const source: Source = middleWareSourceBuilder({ port, reloadPage });
// ...
};
export default middlewareInjector;
再看middleware-source-builder
import { template } from "lodash";
// 这里就是模板的源文件,关于这个写法,得参考webpack
import rawSource from "raw-loader!./wer-middleware.raw";
import polyfillSource from "raw-loader!webextension-polyfill";
import { RawSource, Source } from "webpack-sources";
import {
RECONNECT_INTERVAL,
SOCKET_ERR_CODE_REF,
} from "../constants/middleware-config.constants";
import * as signals from "../utils/signals";
export default function middleWareSourceBuilder({
port,
reloadPage,
}: IMiddlewareTemplateParams): Source {
const tmpl = template(rawSource);
return new RawSource(
// 这里都是一些参数
tmpl({
WSHost: `ws://localhost:${port}`,
config: JSON.stringify({ RECONNECT_INTERVAL, SOCKET_ERR_CODE_REF }),
polyfillSource: `"||${polyfillSource}"`,
reloadPage: `${reloadPage}`,
signals: JSON.stringify(signals),
}),
);
}
这里有个小发现,就是lodash原来也有个template方法,和ejs非常像。
再看wer-middleware.raw
/* -------------------------------------------------- */
/* Start of Webpack Hot Extension Middleware */
/* ================================================== */
/* This will be converted into a lodash templ., any */
/* external argument must be provided using it */
/* -------------------------------------------------- */
(function(window) {
function contentScriptWorker() {
runtime.sendMessage({ type: SIGN_CONNECT }).then(msg => console.info(msg));
runtime.onMessage.addListener(({ type, payload }: IAction) => {
switch (type) {
case SIGN_RELOAD:
logger("Detected Changes. Reloading ...");
// 重点:当收到消息后,调用了reload函数,至此原理就非常清楚了
reloadPage && window.location.reload();
break;
case SIGN_LOG:
console.info(payload);
break;
}
});
}
function backgroundWorker(socket: WebSocket) {
// 通过socket和background建立链接
socket.addEventListener("message", ({ data }: MessageEvent) => {
const { type, payload } = JSON.parse(data);
if (type === SIGN_CHANGE && (!payload || !payload.onlyPageChanged)) {
// 重点:当background收到重载消息是,会重新加载这个chrome extension
// http://www.kkh86.com/it/chrome-extension-doc/extensions/runtime.html#method-reload
runtime.reload();
});
}
});
socket.addEventListener("close", ({ code }: CloseEvent) => {
const intId = setInterval(() => {
const ws = new WebSocket(wsHost);
ws.addEventListener("open", () => {
clearInterval(intId);
runtime.reload();
});
}, RECONNECT_INTERVAL);
});
}
// ======================== Called only on extension pages that are not the background ============================= //
function extensionPageWorker() {
runtime.sendMessage({ type: SIGN_CONNECT }).then(msg => console.info(msg));
runtime.onMessage.addListener(({ type, payload }: IAction) => {
switch (type) {
case SIGN_CHANGE:
reloadPage && window.location.reload();
break;
}
});
}
// ======================= Bootstraps the middleware =========================== //
// 这里应该是多环境复用这份代码
runtime.reload
? extension.getBackgroundPage() === window ? backgroundWorker(new WebSocket(wsHost)) : extensionPageWorker()
: contentScriptWorker();
})(window);
再看下package.json中"ws": "^7.2.0",果然安装了ws,用来创建一个websocket服务器,
在src/hot-reload/HotReloaderServer.ts中就有创建这个server的逻辑。
当webpack重新build并生成文件后,就会触发
this._compiler.hooks.afterEmit.tap
具体的实现细节还是非常多的,原理大致也就明白了,就是最终会触发server去通知调用reload接口,实现auto reload,再深入的就没有具体分析了