之前开发过一款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,再深入的就没有具体分析了