擴展開發過程當中的自動更新實現

介紹

最近業務上須要開發擴展來實現某些功能。在開發過程當中,遇到每次修改完代碼,都須要手動點擊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參見的功能也基本摸清了,收穫頗多。都說在解決問題中成長才是最好的成長,的確是這樣。

相關文章
相關標籤/搜索