webx之bigpipe

如果对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应用的速度优化带来更多选择性。

猜你喜欢

转载自hill007299.iteye.com/blog/1595689
今日推荐