开发chrome插件auto reload原理分析

877 阅读2分钟

之前开发过一款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.jsondevDependenci,还真的有依赖这个

"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,再深入的就没有具体分析了