扩展开发过程当中的自动更新实现

介绍

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

原理/思路/过程

根据本身的习惯,本次仍是选用gulp + webpack来构建,界面部分使用Vue.js做为技术栈。vue

构建结果输出到硬盘

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

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

通过一段时间的调查,发现webpack-dev-server并无提供构建到硬盘的功能!!!也就是说,咱们要输出到硬盘,只能咱们本身写逻辑来实现了。 git

固然咱们也能够不启动webpack-dev-server。当时热加载的实现是须要用到socket的,这个在webpack-dev-server中已经封装好了。为了修改的尽可能少,建议仍是使用webpack-dev-server的好。 github

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

// 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

自动更新扩展

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

修改代码后,会触发构建,可是Chrome中的扩展并无自动更新gulp

这个问题花了不少时间,最后把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参见的功能也基本摸清了,收获颇多。都说在解决问题中成长才是最好的成长,的确是这样。

相关文章
相关标签/搜索