资讯专栏INFORMATION COLUMN

如何在前端项目中实现热更新

antz / 1365人阅读

摘要:如果你的项目中使用了的话,你会很幸运,借助插件可以实现项目的热更新。对模板更新的处理目前项目中使用的是的模板引擎。


这个是组内一位同学在平时开发中,发现调试不便,为团队开发的热更新工具。很厉害,文章中的技术实现内容也是我了解了他的具体实现思路后,整理出来的。

工具源码EHU(esl-hot-update)

热更新是什么

热更新就是当你在开发环境修改代码后,不用刷新整个页面即可看到修改后的效果。

如果你的项目中使用了webpack的话,你会很幸运,借助webpack-dev-server插件可以实现项目的热更新。

解决的问题

对于大型的系统级别项目会有下面几个特点

模块化(AMD)模式的广泛使用后,开发环境散文件特别多,很容易上百,一不小心还能上千

初始化的内容特别多,各种底层库,ui库等等

这两个特点直接导致每次调试后,刷新会很慢。如果初始化的js达到上千的数量级,每一次重新刷新都是5s,10s,甚至20s的等待。

而热更新的目的就是为了在一定程度上减少这5s,10s,甚20s的浪费。

遇到的问题

我们使用的是百度自己的开发环境工具edp,首先他不支持热更新

我们使用的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命令

工具类:
asynclodash

框架的思想

先看看昨天对于这个工具提出的几个要求

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 ", "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);
    // ....处理文件修改后对应热更新逻辑
});
对css/less更新的处理

这个原理比较简单,页面监听到样式的修改,重新加载一次样式即可,简单的覆盖。

但是存在一个潜在问题,因为样式是简单的覆盖,所以,如果修改是删除了样式,是无法生效的。

举例:
修改前:

display: none;
overflow: hidden;
position: relative;
background: #FFFFFF;
border: 1px solid #E8E8E8;
margin-top: 20px;

修改后:

display: none;
overflow: hidden;
position: relative;
background: #FFFFFF;

删除的bordermargin-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这个是修改后自己实现的控制配置修改的逻辑。

然后这个文件加入到服务端的路由中,请求时替换。

对js更新的处理

这里逻辑比较复杂,因为需要修改底层的AMD模块加载的逻辑。

js没有模板那么简单,不是直接覆盖,因为在AMD模式中,每一个文件,都是被上一个文件调用执行的结果。

所以我们处理的逻辑是不仅需要重新加载修改的文件,并且递归所有直接或者间接调用他的文件,全部重新加载。

所以从上面的特点可以看出,这个工具目前阶段主要适用于业务模块的开发,因为业务的依赖不会特别深,对于dep中的核心文件修改,就不是很合适,一旦文件比较底层,热跟新是重新加载的模块也会非常多。

另外也有很多其他的坑,还在不断优化中。

总结

这次实践其实就是业务中遇到的问题(系统太庞大,调试太麻烦),如何解决问题,如何把解决的思路变成一个解决方案,分享给团队。

因为自己解决了,和形成一个解决方案还是有非常大的差别的,例如我们在形成方案的过程中,就尝试了很多新东西,踩了很多坑。

目前还有个坑就是chrome浏览器,调试的Source资源时,如果一个资源重复加载,内存中会更新,但是对应的资源没有更新,导致断点时,映射不对(断点失效),目前暂时的解决方案是,每次请求时添加时间戳,让Source映射的资源强制更新。这个可以正常断点,但是断点没有记忆功能(坑啊,因为文件变了)。

微信公众号

博客地址

http://tangguangyao.github.io/

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/78318.html

相关文章

  • 使用node子进程spawn,exec踩过的坑

    摘要:最后发现使用子进程打开还真的就是使用到一定程度就挂掉。上面的简单流程就是启动一个子进程。逻辑就是,记录子进程的大小,一旦超过就掉子进程。我们在使用时,不知道设置,默认的是当我们子进程日志达到时,自动掉了。 showImg(https://segmentfault.com/img/bVrCnh); 如何在项目中实现热更新中提到的一个坑child_process的exec使用问题,下面文章...

    cppprimer 评论0 收藏0
  • 2018年8月所遇知识点整理

    摘要:注本文章是在工作过程中所接触的知识点的整理,涉及的东西比价杂乱,如有错误之处,欢迎纠错与指导一,页面的锚链接定义锚点锚点链接。类似于我们阅读书籍时的目录页码或章回提示。 *注:本文章是在工作过程中所接触的知识点的整理,涉及的东西比价杂乱,如有错误之处,欢迎纠错与指导 一, 页面的锚链接 1,定义:锚点,锚点链接。常常用于那些内容庞大繁琐的网页,通过点击命名锚点,不仅让我们能指向文档,还...

    silenceboy 评论0 收藏0
  • 2018年8月所遇知识点整理

    摘要:注本文章是在工作过程中所接触的知识点的整理,涉及的东西比价杂乱,如有错误之处,欢迎纠错与指导一,页面的锚链接定义锚点锚点链接。类似于我们阅读书籍时的目录页码或章回提示。 *注:本文章是在工作过程中所接触的知识点的整理,涉及的东西比价杂乱,如有错误之处,欢迎纠错与指导 一, 页面的锚链接 1,定义:锚点,锚点链接。常常用于那些内容庞大繁琐的网页,通过点击命名锚点,不仅让我们能指向文档,还...

    guqiu 评论0 收藏0
  • React+Koa+MongoDB+Docker开发环境

    摘要:已经发布到,只要在环境下安装即可。下面通过来构建开发环境,提高开发体验。容器容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的命名空间。部署开发环境部署开发环境其实很简单,只需要配置和即可。 前言 本次博文依然是对 multi-spa-webpack-cli 的扩充和完善。 集成 mongoose。 集成 Docker 开发环境。 multi-spa-w...

    sarva 评论0 收藏0
  • WEEX系列 WEEX入门

    摘要:通过使用有限的类标签阉割的及基于语法来快速构建原生应用。高性能本身对加载时间和资源占用进行了优化。站在巨人的肩膀上,我们也很容易开发出高性能的。我们可以把部署到服务器上实现热更新。引擎运行这些实现与线程通信,达到和原生应用相同的体验效果。 和一步一起从前端视角聊一聊WEEX WEEX是一套构建高性能、可扩展的原生应用跨平台解决方案。就一个字吊。 通过使用有限的类HTML标签、阉割的CS...

    nicercode 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<