Bootstrap

解决大中型浏览器(Chrome)插件开发痛点:自定义热更新方案——1.原理分析及构建部署实现

Chrome扩展程序序列文章:

背景

作为一款版本快速更新迭代的扩展程序,立刻解决重大bug、功能发布,频繁发版影响用户体验,并且,用户无科学上网没法自动更新程序。基于此背景,设计了套基于webpack构建,IndexedDB缓存的热更新方案,支持"一键"部署,解决线上问题,同时大大提升开发效率。

一、模块热更新原理

var obj = {  
  a:1,
    init(){
        console.log('我是obj')
    }
}
eval(`
var obj = {
    a:1,
    init(){
        console.log('你被替换了!')
    }
}
`)
obj.init() //  你被替换了!

这是整个热更新方案的原理,对整个对象进行替换执行。

接下去介绍下基于此的实现方案,更确切的说是一种规范,例子仅介绍Content-Scripts脚本的热更新,涉及扩展程序的Popup页,background.js以及可能需要注入js资源文件的实现不做介绍,仅提供思路。

Chrome扩展程序开发文档可参考:

二、设计项目结构规范和模板代码

# 项目代码结构
/build                 // webpack构建脚本
	webpack.hotfix.js    // 热更新webpack脚本
	webpack.base.js			 // 基础配置
	webpack.dev.js			 // 开发配置
	webpack.prod.js			 // 生产配置
/hotfix                // 执行npm run hotfix 将产生此热修复代码文件
	content-scripts/
  	moduleName.js
/app
    background/        // 程序后台运行的代码文件
    popup/             // 浏览器右上角展示的pop页目录
    content-scripts/
        moduleName/    // 功能模块代码
            entity.js  // 模块实体类
            index.js   // 模块热更新代码
....
package.json
manifest.json          // chrome扩展程序的配置文件

这么设计代码结构是方便webpack打hotfix代码文件。

// entity.js

const obj = {
    Init(){
        //实现代码业务逻辑,包括创建模块功能视图
    }
}

/** 模块对象挂载到全局,方便进行模块替换
* 注意,此处扩展程序content-script注入到页面会创建扩展程序独立的环境,跟页面是不同域的,两者绝缘
*/
if(window.CRX){
    window.CRX.obj = obj
}else{
    window.CRX = {}
    window.CRX.obj = obj
}

export default obj

import hotFix from 'hotfix.js'
import obj from './entity.js'

//热修复方法,对obj模块进行热修复(下期介绍:基于双缓存获取热更新代码)
const moduleName = 'obj';
hotFix(moduleName).catch(err=>{
    console.warn(`${moduleName}线上代码解析失败`,err)
    obj.Init()
})

三、设计Webpack构建hotfix文件

// 配置文件 config.js

//配置打包目录
config.packPath = [
  './app/content-scripts/',
]

//获取命令行自定义热修复模块名参数 npm run hotfix --module=name1,name2
config.hotFixModule = (function () {
  let modules = process.argv[process.argv.length - 1]
  if (modules.indexOf('module=') > -1) {
    modules = modules.split('=')[1].split(',')
    return modules.map(function (key) {
      return moduleMap[key.trim()]
    })
  }
})()

export default config

// webpack.hotfix.js

utils.checkEntries(entries)  // 检验模块是否配置,防止误上传
const packPath = config.packPath // 配置的需要热更新文件的包路径
let hotFixEntries = {} // 配置热更新打包入口对象
packPath.forEach(function (path) {
  const entryKey = path.indexOf('content-scripts') > -1 ? '**/entity.js' : '**/index.js'
  glob.sync(path + entryKey).forEach(function (entry) {
    //匹配出模块名content-scripts/moduleName  
    const matches = /.*app\/(.*)\/\b(?:entity|index)\b\.js/.exec(entry)
    if (matches[1]) {
      hotFixEntries[matches[1]] = entry
    }
  })
})

// 根据命令行入参模块名过滤出需热修复的模块
if (config.hotFixModule) {
  const hotFixModules = Object.keys(hotFixEntries).filter(module => config.hotFixModule.indexOf(module) > -1)
  for (let p in hotFixEntries) {
    if (hotFixModules.indexOf(p) === -1) {
      delete hotFixEntries[p]
    }
  }
}

// 合并基本webpack配置文件
module.exports = merge(baseWebpack, {
  entry: hotFixEntries,
  output: {
		path: utils.resolvePath.base(config.dirHotFix), // 打包后的文件存放的地方
	}
})
{
    "scripts":{
        "clean": "rimraf -rf ./package",
        "hotfix": "npm run cleanHotfix && cross-env NODE_ENV=production webpack --config ./build/webpack.hotfix.js --hide-modules --mode production",
    }
}

自此完成模块热更新构建指令,支持可选参数配置模块名:

npm run hotfix --module=module1,module2

执行命令,生产hotfix/文件夹,存放各热修复模块文件。

四、设计热更新代码自动部署

version: 当前程序版本号
name:模块名
type:热更新模块所属类型
content:模块代码

接口收到数据,依次创建version/type/name.js文件,并写入content。

// deploy.js
const allUploadQueue = [] // 所有要上传文件队列

// ora:一款优雅的终端旋转器
const spinner = ora('uploading files... ')

function addDirToQueue (_path, cb) {
  // 递归获取hotfix下模块文件相关逻辑省略
  ......
	// 构建队列文件信息
  let obj = {
    data: _path,
    size: size,
    name: fileName,
    type,
    content: fs.readFileSync(_path).toString()
  }
  allUploadQueue.push(obj)
}

function startUpload(){
  .....
  let tmp = []
  // 截取5次数据
  if (allUploadQueue.length >= options.perTotal) {
    tmp = allUploadQueue.splice(0, options.perTotal)
  } else {
    tmp = allUploadQueue.splice(0, allUploadQueue.length)
  }
  ...
  //构造请求
  const promise = tmp.map(...)
  // 递归上传
  Promise.all(promise).then(data=>{ 
    if(allUploadQueue.length){ 
       startUpload(options)
    }
  })

}

function uploadScriptFile (options = {}) {
  spinner.start()
  options = Object.assign({}, defaultOptions, options)
  //invariant:一款开发中描述错误的插件
  invariant(options.file, 'upload file is required!')
  invariant(options.originUrl, 'upload originUrl is required!')
  invariant(options.data, 'data object is required!')
  //根据hotfix文件夹路径,递归模块信息到队列
  addDirToQueue(options.file, function () {
    //单次并发5次上传文件请求,去上传文件
    startUpload(options)
  })
}
"scripts":{
  ...
  "deploy":"node ./build/deploy.js"
}

执行npm run deploy,完成hotfix文件上传服务器

五、总结

自此便实现了Chrome扩展程序的热更新打包部署。

//1. 构建hotfix
npm run hotfix --module=name1,name2 
//2. 发布到线上
npm run deploy

--END--

作者:梁龙先森 WX:newBlob

原创作品,抄袭必究