如果对facebook的前端技术有所耳闻的话,那么对bigpipe应该不会陌生。
使用这个技术的好处是,可以缩短用户感知的响应时间,可以让浏览器和服务端并行工作。
举个例子,假如一个页面有5个区块,使用传统的方式是,服务端把这包括5个区块的完整页面html全部生成后,再响应给浏览器,浏览器解析html响应,构建dom树,下载引用的css、js文件,解析css应用到dom树,解析执行js。
传统方式的特点是服务端和浏览器是完全串行执行,服务端工作时,客户端闲着,客户端忙着时,服务端闲着,不能充分利用服务端和客户端的时间片。
而bigpipe则是采用了分步输出的做法,使得服务器和浏览器能够同时进行工作,接到http请求后,服务端先渲染骨架html,包含主体页面结构和区块页面(pagelet)占位符,然后马上输出给浏览器,浏览器即可对已接收到的html进行解析、渲染,同一时间服务端继续生成pagelet的html,每完成一个pagelet输出一个,让浏览器和服务端始终有大部分重叠工作时间,这样就可以大大缩短一个请求从发出到用户感知到的页面响应时间。
一个比较广为流传的比喻是,去餐厅点10个菜,传统模式是厨师把10道菜都做完了,才给端过来,顾客才能开吃。bigpipe是厨师每做完一道菜,都会端过来,顾客可以第一时间开吃。
虽然bigpipe出来了一段时间,但是java 的mvc框架对此的支持还比较少。所以自己利用业余时间尝试了一下,把bigpipe集成到webx中。要在webx框架中实现bigpipe,需要做到提前输出骨架页面和pagelet占位符,然后分别渲染pagelet,分步输出。
骨架页面要包括一段js脚本onHtmlArrived,用于分步接收并渲染html,还有pagelet占位符。
服务端每完成一个pagelet就对浏览器输出
<script>onHtmlArrived("id "," body");</script>(其中body是pagelet渲染结果,id是pagelet占位符id)
浏览器执行onHtmlArrived脚本,在之前的pagelet占位的地方填充html。
<script type="text/javascript">function onHtmlArrived(id,text) { var b=document.getElementById(id); b.innerHTML = text; }</script> <div>Progressive Loading <div class="pagelet" id="content1">-</div> <div class="pagelet" id="content2">-</div> <div class="pagelet" id="content3">-</div> <div class="pagelet" id="content4">-</div> </div>
bigpipe集成到webx的方式,我选择以pipeline作为扩展点,自定义一个BipPipeValve,这个valve的作用是提前输出骨架页面,解析骨架页面的pagelet,然后分别渲染pagelet(每个pagelet对应一个screen,采用screen的渲染方式),分别输出。
package com.alibaba.webx.tutorial1.common; public class BigPipeValve extends AbstractValve { @Autowired private BufferedRequestContext bufferedRequestContext; @Autowired private BasicRequestContext basicRequestContext; @Autowired private HttpServletRequest request; @Autowired private TemplateService templateService; @Autowired private MappingRuleService mappingRuleService; @Autowired private ModuleLoaderService moduleLoaderService; public void invoke(PipelineContext pipelineContext) throws Exception { // 拿出request,解析出下一批要执行的screen,flush当前渲染的。 // 遍历执行PerformScreenValve和RenderTemplateValve TurbineRunDataInternal rundata = (TurbineRunDataInternal) getTurbineRunData(request); System.out.println("valve started."); RequestContext tempRC = basicRequestContext.getWrappedRequestContext(); String body = bufferedRequestContext.popCharBuffer(); // 解析pagelet List<String> pageletList = parsePagelet(body); // 提前输出骨架页面 tempRC.getResponse().getWriter().write(body); tempRC.getResponse().getWriter().flush(); // 分别渲染每个pagelet,并输出 for (String id : pageletList) { rundata.setTarget("//" + id); rundata.setLayoutEnabled(false); performScreenModule(rundata); render(rundata); body = bufferedRequestContext.popCharBuffer(); body = renderPagelet(id, body); tempRC.getResponse().getWriter().write(body); tempRC.getResponse().getWriter().flush(); } pipelineContext.invokeNext(); // 调用后序valves System.out.println("valve ended."); } private String renderPagelet(String id, String body) { body = body.replace("\r\n", "").replace("\n", "").replace("\"", "\'"); String pagelet = "<script>onHtmlArrived(\"" + id + "\",\"" + body + "\");</script>"; return pagelet; } private List<String> parsePagelet(String body) throws ParserException { Parser parser = new Parser(); parser.setInputHTML(body); NodeIterator iterator = parser.elements(); Node node = iterator.nextNode(); node = iterator.nextNode(); node = iterator.nextNode(); List<String> pageletList = new ArrayList<String>(); travel(node, pageletList); return pageletList; } private void travel(Node node, List<String> pageletList) { if (node == null) { return; } else { if (node instanceof Div) { String cls = ((Div) node).getAttribute("class"); if (cls != null && cls.contains("pagelet")) { System.out.println(((Div) node).getAttribute("id")); pageletList.add(((Div) node).getAttribute("id")); } } } NodeList childrens = node.getChildren(); if (childrens == null || childrens.size() == 0) { return; } for (int i = 0; i < childrens.size(); i++) { Node temp = childrens.elementAt(i); travel(temp, pageletList); } } private void render(TurbineRunDataInternal rundata) throws TemplateException, IOException { String target = assertNotNull(rundata.getTarget(), "Target was not specified"); if (!rundata.isRedirected()) { Context context = rundata.getContext(); renderTemplate(getScreenTemplate(target), context, rundata); if (rundata.isLayoutEnabled()) { String layoutTemplateOverride = rundata.getLayoutTemplateOverride(); if (layoutTemplateOverride != null) { target = layoutTemplateOverride; } String layoutTemplate = getLayoutTemplate(target); if (templateService.exists(layoutTemplate)) { String screenContent = defaultIfNull(bufferedRequestContext.popCharBuffer(), EMPTY_STRING); context.put(SCREEN_PLACEHOLDER_KEY, screenContent); renderTemplate(layoutTemplate, context, rundata); } } } } protected void setContentType(TurbineRunData rundata) { if (StringUtil.isEmpty(rundata.getResponse().getContentType())) { rundata.getResponse().setContentType("text/html"); } } protected void performScreenModule(TurbineRunData rundata) { String target = assertNotNull(rundata.getTarget(), "Target was not specified"); String moduleName = getModuleName(target); try { Module module = moduleLoaderService.getModuleQuiet(TurbineConstant.SCREEN_MODULE, moduleName); if (module != null) { module.execute(); } else { if (isScreenModuleRequired()) { throw new ModuleNotFoundException("Could not find screen module: " + moduleName); } } } catch (ModuleLoaderException e) { throw new WebxException("Failed to load screen module: " + moduleName, e); } catch (Exception e) { throw new WebxException("Failed to execute screen: " + moduleName, e); } } protected boolean isScreenModuleRequired() { return false; } protected String getModuleName(String target) { return mappingRuleService.getMappedName(SCREEN_MODULE_NO_TEMPLATE, target); } protected String getScreenTemplate(String target) { return mappingRuleService.getMappedName(SCREEN_TEMPLATE, target); } protected String getLayoutTemplate(String target) { return mappingRuleService.getMappedName(LAYOUT_TEMPLATE, target); } protected void renderTemplate(String templateName, Context context, TurbineRunDataInternal rundata) throws TemplateException, IOException { rundata.pushContext(context); try { templateService.writeTo(templateName, new ContextAdapter(context), rundata.getResponse().getWriter()); } finally { rundata.popContext(); } } public static class DefinitionParser extends AbstractValveDefinitionParser<BigPipeValve> { } }
pipeline中的配置为
<?xml version="1.0" encoding="UTF-8" ?> <beans:beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:services="http://www.alibaba.com/schema/services" xmlns:pl-conditions="http://www.alibaba.com/schema/services/pipeline/conditions" xmlns:pl-valves="http://www.alibaba.com/schema/services/pipeline/valves" xmlns:beans="http://www.springframework.org/schema/beans" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation=" http://www.alibaba.com/schema/services http://localhost:8080/schema/services.xsd http://www.alibaba.com/schema/services/pipeline/conditions http://localhost:8080/schema/services-pipeline-conditions.xsd http://www.alibaba.com/schema/services/pipeline/valves http://localhost:8080/schema/services-pipeline-valves.xsd http://www.springframework.org/schema/beans http://localhost:8080/schema/www.springframework.org/schema/beans/spring-beans.xsd "> <services:pipeline xmlns="http://www.alibaba.com/schema/services/pipeline/valves"> <!-- 初始化turbine rundata,并在pipelineContext中设置可能会用到的对象(如rundata、utils),以便valve取得。 --> <prepareForTurbine /> <!-- 设置日志系统的上下文,支持把当前请求的详情打印在日志中。 --> <setLoggingContext /> <!-- 分析URL,取得target。 --> <analyzeURL /> <!-- 检查csrf token,防止csrf攻击和重复提交。假如request和session中的token不匹配,则出错,或显示expired页面。 --> <checkCsrfToken /> <loop> <choose> <when> <!-- 执行带模板的screen,默认有layout。 --> <pl-conditions:target-extension-condition extension="null, vm, jsp, jspx" /> <performAction /> <performTemplateScreen /> <renderTemplate /> </when> <when> <!-- 执行不带模板的screen,默认无layout。 --> <pl-conditions:target-extension-condition extension="do" /> <performAction /> <performScreen /> </when> <otherwise> <!-- 将控制交还给servlet engine。 --> <exit /> </otherwise> </choose> <!-- 假如rundata.setRedirectTarget()被设置,则循环,否则退出循环。 --> <breakUnlessTargetRedirected /> </loop> <!-- bigpipe配置 --> <valve class="com.alibaba.webx.tutorial1.common.BigPipeValve" /> </services:pipeline> </beans:beans>
当服务端执行时间较长时,bigpipe的体验优化效果很明显。例如,下面这个页面分为5个区块,如果每个区块的服务端响应时间是500ms,采用传统方式,用户要等待2.5秒才能感受到页面响应;而采用bigpipe,用户只要0.5-1秒钟,就能感知到页面响应了,速度提升了一倍。启动附件中的应用分别访问http://localhost:8080/bigindex.htm (bigpipe模式)和http://localhost:8080/index.htm(传统模式),会有明显的区别。
这个demo是在业余时间写出来的,性能不佳,估计也有不少的bug,本分享仅为抛砖引玉,望有webx达人能把bigpipe特性集成进去,相信会给web应用的速度优化带来更多选择性。