最近業務上須要開發擴展來實現某些功能。在開發過程當中,遇到每次修改完代碼,都須要手動點擊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',() => {})
這段代碼來實現編譯結果輸出到硬盤,關於webpack
的emit
詳見https://webpack.github.io/doc...,這裏不詳細解釋。chrome
構建結果能夠輸出到硬盤後,就能夠開始調試了。這個時候又遇到第二個問題:json
修改代碼後,會觸發構建,可是Chrome中的擴展並無自動更新gulp
這個問題花了不少時間,最後把webpack
的hotModuleReplaceMentPlugin
插件的原理搞明白後,才搞定的。
咱們都知道,要是webpack
的hot module replace
,須要引入hotModuleReplaceMentPlugin
,而且啓動webpack-dev-server
。那麼爲何要這樣作呢?我畫一個圖簡單說明下webpack hot module replace
的原理。
這裏說下整個流程:當啓動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官方提供的更新擴展方法,會自動更新整個擴展,包括background
和contentscript
。
而後在構建過程當中把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)); });
這樣子,熱加載的過程就變成下圖這樣:
解決了上面的兩個問題,其實已經解決了擴展的構建,調試,熱加載問題。可是,一個擴展是能夠有多個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.js
和onMessage.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
參見的功能也基本摸清了,收穫頗多。都說在解決問題中成長才是最好的成長,的確是這樣。