7

介绍

最近业务上需要开发扩展来实现某些功能。在开发过程中,遇到每次修改完代码,都需要手动点击chrome://extensions页面的Reload,才能更新扩展的问题,十分影响开发体验。于是花了点时间,把开发扩展的构建过程的hot reload搞定了。
具体代码见:https://github.com/chenhao-ch...

原理/思路/过程

根据自己的习惯,本次还是选用gulp + webpack来构建,界面部分使用Vue.js作为技术栈。

构建结果输出到硬盘

根据页面开发的习惯,搭建好构建逻辑后,就遇到了第一个问题:

扩展调试需要一个本地目录,而webpack启用dev-server后,构建结果是输出到内存中的。

经过一段时间的调查,发现webpack-dev-server并没有提供构建到硬盘的功能!!!也就是说,我们要输出到硬盘,只能我们自己写逻辑来实现了。

当然我们也可以不启动webpack-dev-server。当时热加载的实现是需要用到socket的,这个在webpack-dev-server中已经封装好了。为了修改的尽量少,建议还是使用webpack-dev-server的好。

为了找到解决方法,在网上找了很久,试了一堆方法,都不是很理想。最后找到了一种相对简单的方法来解决,就是利用webpack plugin的运行时生命周期来解决。简单点说,就是当webpack的构建结束(包括增量构建)时,会触发一个emit事件,在emit中我们可以将构建结果拿到,然后通过fs模块输出到硬盘上。代码如下:

// gulp.js
// 构建过程
gulp.task('webpack-build-dev', ['clean'], function() {
  process.env.NODE_ENV = 'development';
  var port = 3007;

  // 对每一个入口都添加dev server。
  for (var e in webpackDevConfig.entry) { 
    webpackDevConfig.entry[e].push(`webpack-dev-server/client?http://localhost:${port}`, 'webpack/hot/dev-server');
  }

  // 根据dev配置开始构建
  var compiler = webpack(webpackDevConfig);
  // 在构建结束时,运行emit事件
  compiler.plugin('emit', (compilation, callback) => {
    // 每次构建结束,都会触发该方法。
    const assets = compilation.assets;
    let file, data, fileDir;
    Object.keys(assets).forEach(key => {
      file = path.resolve(__dirname, './build/' + key);
      fileDir = path.dirname(file);
      if (!fs.existsSync(fileDir)) {
        fs.mkdirSync(fileDir);
      }

      data = assets[key].source();
      fs.writeFileSync(file, data);   // 将构建结果同步的写到硬盘中
    });
    callback();
  });
  
  // 启动服务器
  var server = new devServer(compiler, {});
  server.listen(port, '0.0.0.0', function() {});
});

可以看出,我们主要通过了compiler.plugin('emit',() => {}) 这段代码来实现编译结果输出到硬盘,关于webpackemit详见https://webpack.github.io/doc...,这里不详细解释。

自动更新扩展

构建结果可以输出到硬盘后,就可以开始调试了。这个时候又遇到第二个问题:

修改代码后,会触发构建,但是Chrome中的扩展并没有自动更新

这个问题花了很多时间,最后把webpackhotModuleReplaceMentPlugin插件的原理搞明白后,才搞定的。

我们都知道,要是webpackhot module replace,需要引入hotModuleReplaceMentPlugin,并且启动webpack-dev-server。那么为什么要这样做呢?我画一个图简单说明下webpack hot module replace的原理。

webpackDevServer流程

这里说下整个流程:当启动webpack构建时,会对每一个入口都注入webpackDevServer的部分代码,我这里就叫webpackDevServer(client)好了。 这个代码中有一个socket,运行后会和本地服务器的socket接口进行链接。当本地服务器关闭时,在页面的DevTools中我们会看到页面有不断再尝试链接sockjs-node/info就是一个socket链接。

然后我们修改代码,webpack中会自动进行构建,然后通知到webpackDevServer,并通过socket通知到webpackDevServer(client)。然后,webpackDevServer(client)就会通过postMessage通知到页面。让hotModule进行去更新。这里的更新就有部分模块更新的逻辑了,这里不细讲。

回到我们的问题上,我们要实现代码修改后,自动更新扩展,涉及两步:自动触发构建 & 构建结束后,扩展自动更新。可以看出,第一步不需要做任何操作就可以实现。那么第二步,我们可以利用webpackDevServer过程中的postMessage

我的做法时,在background中多引入一个reload.js。 代码如下:

// reload.js
// 实现webpackHotUpdate消息的监听
window.addEventListener('message', (e) => {
  if (typeof event.data === 'string' && event.data.indexOf('webpackHotUpdate') === 0) {
    // 当监听到webpackHotUpdate事件时,扩展重新安装
    chrome.runtime.reload();
  }
});

其中chrome.runtime.reload();就是Chrome官方提供的更新扩展方法,会自动更新整个扩展,包括backgroundcontentscript

然后在构建过程中把reload.js引入到background中。和业务逻辑进行隔离。

// gulpfile.js
// 迁移dev阶段的reload.js文件,以实现自动更新
gulp.task('move-dev', ['clean'], function () {
  // 迁移自动刷新扩展功能代码
  gulp.src(path.resolve(__dirname, './config/reload.js'))
      .pipe(gulp.dest(buildPath));

  var manifest = require('./src/manifest.json');
  manifest.background.scripts.push('./reload.js');
  fs.writeFileSync(path.resolve(__dirname, './build/manifest.json'), JSON.stringify(manifest, null, 2));
});

这样子,热加载的过程就变成下图这样:
extensions reload

多contentScript问题

解决了上面的两个问题,其实已经解决了扩展的构建,调试,热加载问题。但是,一个扩展是可以有多个content script的,还需要在构建上做支持。我通过下面这种方法来解决。
将每一个contentscript作为一个业务,并约定一下的目录结构:

│  background.js
│  manifest.json
├─biz
│  └─count
│      background.js
│      contentscript.js
│      contentscript.vue
├─common
│   log.js
│   message.js
│   onMessage.js
└─_locales

其中biz中的子目录都是一个业务,比如count就是一个业务。如果业务目录中存在contentscript.js,就会在构建时作为一个入口,构建出一个独立的[业务].js作为注入代码。而background.js可以通过import把每一个业务的background.js都引入。如此这般,构建结果目录结构就是:

│  background.js
│  count.js
│  manifest.json
├─sourcemap
│   background.js.map
│   count.js.map
└─_locales

然后还实现了message.jsonMessage.js用于解决background只能注册message监听一次的问题。统一不同业务的message通信。

最后放上这个部分的构建代码:

// webpack.dev.config.js
module.exports = {
  entry: {
    background: [
      // 默认只有background.js一个entry,contentScript入口有构建运行时,根据biz目录确定
      path.resolve(__dirname, '../src/background.js')
    ]
  },
  ...
  plugins: [
    new webpack.HotModuleReplacementPlugin()   // 启用热加载
  ],
  devtool: '#source-map',  // sourcemap方便调试
  watch: true  // watch 文件变化
};

// gulp.js
var webpackDevConfig = require('./config/webpack.dev.config.js');
// 根据biz目录下的文件夹名字,生成对应的contentscript entry
gulp.task('createEntry', function() {
  var bizDir = path.resolve(__dirname, './src/biz/');
  var allBiz = fs.readdirSync(bizDir);
  var entrys = {};
  var entryName = [];
  // 根据biz目录下的文件夹名字,生成对应的contentscript entry
  allBiz.forEach(function(b) {
    var bp = path.resolve(bizDir, b);
    if (fs.statSync(bp).isDirectory()) {
      if (fs.statSync(path.resolve(bp, 'contentscript.js')).isFile()) {
        entryName.push(b);
        entrys[b] = [path.resolve(bp, 'contentscript.js')];   // 添加业务的contetscript.js为entry
      }
    }
  });
  console.log(`${getTime()} 添加入口: ${entryName}`);
  entrys['background'] = webpackDevConfig.entry.background;
  webpackDevConfig.entry = entrys;  // 更新entry
});

总结

webpack用很长时间,一直觉得掌握的不够,经过这一次的研究,不仅搞定了扩展的自动更新,而且因为解决构建问题所绕过的弯路,把webpack参见的功能也基本摸清了,收获颇多。都说在解决问题中成长才是最好的成长,的确是这样。


chenhao_ch
2.1k 声望100 粉丝