介绍
最近业务上需要开发扩展来实现某些功能。在开发过程中,遇到每次修改完代码,都需要手动点击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',() => {})
这段代码来实现编译结果输出到硬盘,关于webpack
的emit
详见https://webpack.github.io/doc...,这里不详细解释。
自动更新扩展
构建结果可以输出到硬盘后,就可以开始调试了。这个时候又遇到第二个问题:
修改代码后,会触发构建,但是Chrome中的扩展并没有自动更新
这个问题花了很多时间,最后把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));
});
这样子,热加载的过程就变成下图这样:
多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.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
参见的功能也基本摸清了,收获颇多。都说在解决问题中成长才是最好的成长,的确是这样。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。