最近业务上须要开发扩展来实现某些功能。在开发过程当中,遇到每次修改完代码,都须要手动点击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
参见的功能也基本摸清了,收获颇多。都说在解决问题中成长才是最好的成长,的确是这样。