这个是组内一位同窗在平时开发中,发现调试不便,为团队开发的热更新工具。很厉害,文章中的技术实现内容也是我了解了他的具体实现思路后,整理出来的。css
工具源码EHU(esl-hot-update)html
热更新就是当你在开发环境修改代码后,不用刷新整个页面便可看到修改后的效果。node
若是你的项目中使用了webpack的话,你会很幸运,借助webpack-dev-server插件能够实现项目的热更新。webpack
对于大型的系统级别项目会有下面几个特色git
模块化(AMD)模式的普遍使用后,开发环境散文件特别多,很容易上百,一不当心还能上千github
初始化的内容特别多,各类底层库,ui库等等web
这两个特色直接致使每次调试后,刷新会很慢。若是初始化的js达到上千的数量级,每一次从新刷新都是5s,10s,甚至20s的等待。chrome
而热更新的目的就是为了在必定程度上减小这5s,10s,甚20s的浪费。express
咱们使用的是百度本身的开发环境工具edp,首先他不支持热更新npm
咱们使用的AMD实践也是百度本身的esl,并且即便是requirejs也暂时没有找到对应的热更新策略,假如requirejs有对应的,咱们也没法直接使用
因此最终的结论是咱们本身去实现一个基于咱们本身业务的。这样咱们考虑的面不用太广,而且解决方案的更有针对性,即面向咱们现有的业务框架。最重要的是能够尝试修改底层框架作配合。
等待路踩通了,咱们再去考虑普适性。
从ehu/package.json 这个文件中,咱们就能够看出一些具体的思路
须要一个watch功能,即可以监听到文件的修改
socket.io通知浏览器处理文件的改变
修改esl这个文件,达到可以实时更新的效果
当时最简单的考虑,就是文件改变了后,可以通知浏览器,浏览器去从新load这个文件而且执行一次。这个时候再从新去打开这个模块或者功能后,会发现新load的代码在执行后会覆盖上一次的。
因此当时的个人第一直觉是,esl重复require时,若是后面一次会覆盖前面的,那么能够经过简单的覆盖思路去尝试,结果发现覆盖不了。通过验证,发现是esl内部维护了一个map,即require过的模块会存起来。咱们若是但愿更新这个模块,只能将map中的对应模块名删除。(后面会详细讲述esl的改造)
对应这个工具,我当时也提出了几个要求
esl必然是须要修改的,可是如何对开发人员透明?首先是不能让你们都作这种修改。
页面中也必须加入socket.io支持,那么咱们如何在不影响其余人员开发的状况下加入?
咱们作的属于beta版本,如何选择性的使用?ehu工具和之前的开发模式随意切换?
安装方便,可否只是做为一个工具,即插即用,不须要繁琐的配置?
npm install -g ehu(mac下须要sudo,windows下须要管理员权限)
在原来执行edp webserver start命令的路径 执行 ehu(再也不须要执行 edp webserver start)
原来端口号8848修改成8844(原8848依旧可使用,但不支持热更新)
首先使用的方式很简单,为此特地将工具打包到npm上,之后就算有升级,仅仅须要你们update便可。
另外从使用角度,也尽可能集成化(一句命令行便可),避免为了这个工具的使用而作太多额外的事情。
"dependencies": { "async": "^1.5.0", "commander": "^2.9.0", "express": "^4.13.3", "express-http-proxy": "^0.6.0", "lodash": "^3.10.1", "socket.io": "^1.3.7", "watch": "^0.16.0" }
几个必要的 watch
——监听文件变化 socket.io
——和浏览器的实时通信 express
——搭建一个服务 express-http-proxy
——代理 commander
——便于本身写node命令
工具类: async
和 lodash
先看看昨天对于这个工具提出的几个要求
esl必然是须要修改的,可是如何对开发人员透明?首先是不能让你们都作这种修改。
页面中也必须加入socket.io支持,那么咱们如何在不影响其余人员开发的状况下加入?
咱们作的属于beta版本,如何选择性的使用?ehu工具和之前的开发模式随意切换?
安装方便,可否只是做为一个工具,即插即用,不须要繁琐的配置?
对于1和2,咱们实际上是须要修改/添加一些代码的,可是代码都不但愿提交到项目的开发环境,由于这些代码生成环境彻底不须要。
因此咱们的解决方案是:拦截,改写(偷梁换柱)
举个例子,当咱们须要对esl作一些改造时,咱们处理方式是当路由指向esl.js时,咱们换成另一个esl-ehu.js(esl-ehu.js是对esl.js改造后的)返回去,这样就对开发环境的代码透明了。
socket.io的支持也是同理,咱们能够在返回html时,改写html的代码,加入对于socket.io的引入。
上面的思路其实来源于以前项目构建打包。
对于3,咱们但愿在使用工具时,任然能很快切换到之前模式,这样作兼容的目的是但愿工具更有竞争力,能吸引你们使用。
咱们的解决方案是:内部实现一个子线程,端口号依然是之前的,并且访问这个端口,就绕过了这个工具。
对于子线程child_process,咱们还遇到一个问题,就是子线程跑系统的时候,常常挂掉,今天刚刚找到一个解决方案,后面会单开一个文章讲这个坑。
对于4,其实就是使用npm方式
第一步:搭建一个新服务做为底层,去托管住咱们如今edp服务,新服务上有一个路由配置,对于咱们须要处理的,拦截。对于不用处理的直接代理给edp
代码参考
var mid = express(); mid.all('*', httpProxy(config.defaultServer, { // 先走特殊规则,不然就代理到默认web server filter: function(req, res) { return !ruleRoute(req, res); }, forwardPath: function(req, res) { return URL.parse(req.url).path; } })); // 由express-http-proxy托管路由 app.use('/', mid);
ruleRoute
就是一些拦截处理
在此以前,启动下子进程
var child = require('child_process'); var cli = child.exec(defaultServerCLI); cli.stdout.on('data', function (log) { !isServerStarted && (cb(null, log)); isServerStarted && console.log(log); });
此处有坑,后面单开文章描述
第二步: 由于上面拦截后的返回的文件已经支持socket.io,esl等底层已经修改了,因此下面是须要去监听文件通知浏览器作对应处理。
// 启动socket.io服务 io = require('socket.io')(server); io.on('connection', function (socket) { socket.emit('hello'); }); // 监视文件改动 initWatch();
第三步: 作一些集成工做
program .version('0.0.6') .usage('[options]') .option('-p, --port <n>', 'Set the port', setPort) .option('-n, --noServerCLI', '...', noServerCLI) .parse(process.argv);
集成到node命令中
第四步: 默认配置
module.exports = { // 默认的服务器 defaultServer: 'http://127.0.0.1:8848', // 默认的服务器启动命令 defaultServerCLI: 'edp webserver start', // 从服务器根目录到须要监控的文件夹中间path baseDir: 'nirvana-workspace', // hot update 须要watch的文件夹(不包括baseDir) watchDirs: 'src', // 入口文件(不包括baseDir) indexHTML: 'main.html', // ehu启动端口号(不可与默认的服务器端口号冲突) port: 8844 };
源码中有不少逻辑是处理配置的
socket.io
——浏览器端仅仅依赖socket这个去和服务端通讯
通讯逻辑
// 创建链接 socket.on('hello', function () { log(getLogMsgPrefix(), 'HotUpdate已启动!'); }); // 检测到文件改动 socket.on('hotUpdate', function (file) { // log(getLogMsgPrefix(), '检测到文件改动', file); // ....处理文件修改后对应热更新逻辑 });
这个原理比较简单,页面监听到样式的修改,从新加载一次样式便可,简单的覆盖。
可是存在一个潜在问题,由于样式是简单的覆盖,因此,若是修改是删除了样式,是没法生效的。
举例:
修改前:
display: none; overflow: hidden; position: relative; background: #FFFFFF; border: 1px solid #E8E8E8; margin-top: 20px;
修改后:
display: none; overflow: hidden; position: relative; background: #FFFFFF;
删除的border
和margin-top
实际上是没有生效的
这个也是后期须要解决的一个问题。
目前项目中使用的是tpl的模板引擎。
如今就遇到一个问题,在热更新时,模板引擎实际上是重复加载模板的,那么就涉及到重复加载是否后面的会覆盖前面问题。
查看加载模板的源码后,发现根据配置有三个选择,覆盖
,忽略
和报错
, 咱们业务中使用的配置是遇到重复后会报错处理,因此咱们须要在不修改业务默认属性的状况下,添加一些逻辑。
// [esl-hot-update] 从新加载须要覆盖 window.EHU_HOT_UPDATE_OPTIONS && window.EHU_HOT_UPDATE_OPTIONS.etpl.isOverride && (namingConflict = 'override'); switch (namingConflict) { /* jshint ignore:start */ case 'override': engine.targets[name] = target; context.targets.push(name); case 'ignore': break; /* jshint ignore:end */ default: throw new Error('Target exists: ' + name); }
window.EHU_HOT_UPDATE_OPTIONS.etpl.isOverride
这个是修改后本身实现的控制配置修改的逻辑。
而后这个文件加入到服务端的路由中,请求时替换。
这里逻辑比较复杂,由于须要修改底层的AMD模块加载的逻辑。
js没有模板那么简单,不是直接覆盖,由于在AMD模式中,每个文件,都是被上一个文件调用执行的结果。
因此咱们处理的逻辑是不只须要从新加载修改的文件,而且递归全部直接或者间接调用他的文件,所有从新加载。
因此从上面的特色能够看出,这个工具目前阶段主要适用于业务模块的开发,由于业务的依赖不会特别深,对于dep中的核心文件修改,就不是很合适,一旦文件比较底层,热跟新是从新加载的模块也会很是多。
另外也有不少其余的坑,还在不断优化中。
此次实践其实就是业务中遇到的问题(系统太庞大,调试太麻烦),如何解决问题,如何把解决的思路变成一个解决方案,分享给团队。
由于本身解决了,和造成一个解决方案仍是有很是大的差异的,例如咱们在造成方案的过程当中,就尝试了不少新东西,踩了不少坑。
目前还有个坑就是chrome浏览器,调试的Source资源时,若是一个资源重复加载,内存中会更新,可是对应的资源没有更新,致使断点时,映射不对(断点失效),目前暂时的解决方案是,每次请求时添加时间戳,让Source映射的资源强制更新。这个能够正常断点,可是断点没有记忆功能(坑啊,由于文件变了)。