手机百度前端工程化之路

天蝎系统介绍

背景介绍

  • 手机百度是一款hybrid APP
  • 处于发展期,开发迭代快,主线版本并行开发
  • 琐碎事情
    • 接入方多
    • 运营活动需求
    • cms需求

发现问题,选择最合适产品特点的解决方案

成熟的团队不应该还停留在炫技和跟风阶段

问题

  • FE开发、联调和代码部署成本高
  • 重复事情特别多,对团队和个人发展都不利
  • 一刀切的解决方案太多
    • 静态资源内嵌
    • localstorage存储方案
  • 没有自己产品的js库
  • 页面太多,不方便统一管理

立足现状,布局未来

第一步:将前端common从template中抽离

svn模块太多,要做统一的前端解决方案,只能将公共的代码单独维护,通过svn:externals属性

前端common svn

将前端common从template的svn中脱离

common
  ├─demo # demo示例目录,不上线
  ├─docs # 文档目录,jsdoc生成,svn忽略
  ├─plugin # smarty插件和解决方案
  ├─static # 静态资源
  │  ├─bdbox # js模块化库
  │  ├─css
  │  ├─img
  │  └─js
  ├─test # 放fis的调试json文件,用于本地开发,不上线
  │  └─page
  │      └─xxx
  ├─widget # widget目录
  └─nodejsLib # nodejs工具

第二步:根据团队实际情况做好前端js库

重复代码抽离,模块间依赖关系明确,方便静态资源管理;用不用第三方库?

关于用不用第三方库的思考

  • 团队成员是否都能自己手写原生js,并且保证质量和工作效率
  • 选择的第三方库,社区是否活跃,团队是否可以cover住bug
  • 扩展性,模块间耦合程度,模块可拔插
  • 有些模块要”私人订制“

zepto + 私人订制的AMD库Bdbox

  • zepto
    • dom操作,事件,ajax(单独)
    • 去掉不常用的模块
  • Bdbox
    • 手机百度js模块库
    • 跟zepto互补
    • 做前端静态资源管理的基石

Bdbox模块库

说明 包含模块
端能力 跟客户端相关 ios,android,apad,invoke,moplus,lbs等
通用&方案 通信、事件、跨域、SPA io,event,app,xDomain,Deffered等
工具 粒度小,用途广泛 cookie,detect,dateFormat等
性能&监控 用户行为统计、速度监控和错误收集 monitor,S,cache(ls/接口/静态资源)
交互 页面交互相关,zepto扩展 Dialog,mask,swipe,fastclick
运营 抽奖游戏,pv和行为统计,跨域通信,游戏类 刮刮乐,摇一摇,跑马灯,游戏舞台/精灵/对象池/easing

18个大类,50+个模块,覆盖手机百度所有应用场景

对模块的管理

  • 利用FIS-PLUS自动包裹AMD规范(有别于官方)
  • 生成静态资源配置文件: common-map.json
    • 扩展FIS-PLUS,包含高频源码和文件hash
  • 对smarty require/widget 标签语法进行改造
    • 支持资源多种动态合并
  • 通过注释,使用jsdoc产出Bdbox文档

模块编写:像写node模块

/**
 * 简单模板
 * @memberOf Bdbox.utils
 * @name template
 * @param  {String} html 模板String内容
 * @param  {Object} data 模板data对象
 * @return {String}      返回处理后的模板
 * @author wangyongqing01
 * @version $Id: template.js 175996 2014-05-16 00:48:03Z wangyongqing01 $
 * @example
 * var t = Bdbox.utils.template('I am <%=name%>', {name:'Theo Wang'});
 * // I am Theo Wang
 * console.log(t);
 */
module.exports = function(html, data) {
    for (var i in data) {
        html = html.replace(new RegExp('<%=\\s*' + i + '\\s*%>', 'g'), data[i]);
    }
    return html;
};

编译后:AMD模块

define('common:bdbox/utils/template', function(require, exports, module, $){
    module.exports = function(html, data) {
        for (var i in data) {
            html = html.replace(new RegExp('<%=\\s*' + i + '\\s*%>', 'g'), data[i]);
        }
        return html;
    };
});

第三步:模板根据action拆分成父子模板

父模板做解决方案,子模板专注于业务,父子继承关系

模板拆分方案

  • 修改odp Router和Template类
  • 父模板按照action来划分,放在 tpl/layout
  • 子模板按照具体页面来分,放在 tpl/page
  • 跟php约定父子模板路径smarty变量
{%extends file=$tplData.parentTplPath %}
{%block name="head"%}...{%/block%}
{%block name="body"%}...{%/block%}

第四步:父模板做解决方案

利用Smarty标签扩展机制,做好解决方案;解决代码调试,速度性能,抽样和静态资源管理

页面渲染模式

解决联调成本和静态资源管理混乱等问题

{%html framework="common:bdbox" rendermode="inline|tag|combo"%}

页面渲染模式介绍

  • inline
    • 即所有的静态资源都内嵌到页面,最古老的一刀切方案
    • http请求少,省电
  • tag
    • 即使用script和link标签,引入外链的js和css
    • PC常见方式,利用http cache
  • combo
    • 将页面多个请求合并成一个
    • 减少http请求,可结合CDN和http cache优势

inline模式

线上环境,适合慢速网络

inline模式

tag模式

线下开发调试环境,适合debug

tag模式

combo模式

线上环境,适合3G+网络

combo模式

nginx combo服务:box.bdimg.com

combo.php是线下测试combo的文件,实际使用场景中文件url都带有hash值

根据网速智能切换渲染模式

  • 客户端知道用户现在所处网络环境,而且页面公共参数含有该信息
  • wise有ip测速库
<!DOCTYPE html>
{%if $network == 'fast' %}
    {%html rendermode="combo"%}
{%else%}
    {%html rendermode="inline" localstorage="true" lscookiepath="/xxx" %}
{%/if%}
{%head%}
.....
{%/head%}
....

inline+localstorage存储

解决慢速网络cache问题

粒度细/多维度,存储更多,降低更新频率

localstorage存储和更新方案

  • 细粒度
    • 代码根据更新频率分层:基础、通用和业务
    • 业务代码更新不会下发基础和通用代码
  • 利用cookie记录版本号,避免二次请求
  • 多维度
    • 原理:cookie可根据domain和path两个维度设置
    • 条件:不同频道页面path不同,但是domain相同
    • 效果:访问A频道,再访问B频道,B频道跟A相同的代码将不再下发
  • 打包自动更新版本配置文件:localstorage.json和lsconfig.php
  • 打包工具自动包裹版本逻辑

localstorage.json举例

{
    "jA": {
        "hash": "2c79d70",
        "files": [
            "common:widget/localstorage/zepto-ajax.tpl"
        ],
        "version": 1
    },
    "jZ": {
        "hash": "5358395",
        "files": [
            "common:widget/localstorage/zepto.tpl"
        ],
        "version": 1
    }
}

文件版本逻辑自动生成

<script data-lsid="jZ">
    __inline('/static/js/zepto.js');
</script>

编译后:

{%if ($_ls_nonsupport) || ($_parsedLSCookies.jZ.isUpdate ) %}
    <script data-lsv="{%$_parsedLSCookies.jZ.version|escape:html%}"  data-lsid="jZ">
    var Zepto=xxx
    </script>
{%else%}
    <script>LS.exec("jZ","js");</script>
{%/if%}
  • 有效避免新手出错,解放双手!
  • php判断变量都是通过解析localstorage.json和对比cookie得到的

cookie存储版本号效果

cookie存储版本号效果 cookie存储版本号效果2

版本号是36进制,如果超过36则从1开始,以保证始终长度为1。

cookie过期时间一周,不需要考虑版本号重叠问题

小结

  • 通过客户端公共参数和wise测速ip库智能选择页面渲染模式
  • 2G等网络延时长使用inline和localstorage存储
  • 3G以上使用CDN+combo渲染模式
  • 开发中使用tag渲染模式,方便快速定位bug所在模块
  • localstorage细粒度多维度和自动化更新

哪天4G普及了,只要去掉慢速的逻辑分支,既可以全部更换到最优方案

第五步:前端模板组件化

js模板编译成Bdbox模块,方便管理、跨域拉取和模块化存储

模板选型:artTemplate

  • 基本不跟smarty语法冲突
  • 性能
  • 扩展能力
  • 编译支持
  • 国人开发,情怀~

模板选型

synax eJS doT artTempalte
是否支持定界符自定义 Y Y Y
是否支持include Y Y Y
是否支持Node Y Y Y
是否支持简洁语法 Y Y Y
是否支持格式化输出 filters方式 N filters
是否支持浏览器端 Y Y Y

模板编译:编译之前

{{include '../public/header'}}
<div id="main">
    <h3>{{title}}</h3>
    <ul>
        {{each list}}
        <li><a href="{{$value.url}}">{{$value.title}}</a></li>
        {{/each}}
    </ul>
</div>

{{include '../public/footer'}}

模板编译:编译之后为Bdbox模块

define('baiduboxapp:tmpl/test/test2', function(require, exports, module, $) {
    $.template('baiduboxapp:tmpl/test/test2', function($data, $filename) {
        //忽略部分代码
        include('../public/header');
        $out += '\n\n<div id="main">\n    <h3>';
        $out += $escape(title);
        $out += '</h3>\n    <ul>\n        ';
        $each(list, function($value, $index) {
            $out += '\n        <li><a href="';
            $out += $escape($value.url);
            $out += '">';
            $out += $escape($value.title);
            $out += '</a></li>\n        ';
        });
        $out += '\n    </ul>\n</div>\n\n';
        include('../public/footer');
        $out += '\n';
        return new String($out);
    });
    module.exports = function(id, data) {
        var html = $.template("baiduboxapp:tmpl/test/test2", data);
        $.byId(id).innerHTML = html;
    }
});

编译之后:依赖自动生成

"baiduboxapp:tmpl/test/test2": {
    "uri": "baiduboxapp/tmpl/test/test2.js",
    "type": "js",
    "deps": [
        "baiduboxapp:tmpl/public/header",
        "baiduboxapp:tmpl/public/footer"
    ]
}

模板使用

{%require name="common:bdbox/template"%}
<div id="content"></div>
{%script%}
require('baiduboxapp:tmpl/test/test2');
var data = {
    title:'加载模板演示',
    time: +new Date(),
    list: [...]
};
var html = Bdbox.template('baiduboxapp:tmpl/test/test2', data);
document.getElementById('content').innerHTML = html;
//or
Bdbox.tmpl.test.test2('content', data);
{%/script%}

rendermode="tag"的渲染模式下模板输出

js模板解决方案

js模板解决方案

  • 将js的string模板编译成可执行js代码片段
  • 编译成Bdbox模块
  • 依赖关系自动维护
  • 减少js模板编译时间
  • 解决跨域拉取string模板的问题
  • 看不到js模板的存在!

第六步:webapp的组件化开发

组件化之后,做页面更像再玩积木游戏

组件化

  • 一个UI组件由:tmpl、js和css构成
  • js文件是组件的核心
  • 组件内css/js/tmpl 产生依赖关系

组件化目录结构

# component举例:发现频道
components/discovery
    ├─header # 头部大banner
    │      header.css
    │      header.js
    │      header.tmpl
    │
    └─comment # 评论
            comment.css
            comment.js
            comment.tmpl

组件化举例:discovery/foo

模板 foo.tmpl

{{include './public/header'}}

<div id="main">
    <h3>{{title}}</h3>
    <ul>
        {{each list}}
        <li><a href="{{$value.url}}">{{$value.title}}</a></li>
        {{/each}}
    </ul>
</div>

{{include './public/footer'}}

组件化举例:discovery/foo

交互 foo.js

function bindEvent(id){
    $.byId(id).addEventListener(xxx);
    //....
}
module.exports = function(id, data){
    template(id, data);
    bindEvent(id);
}

编译后:

define('baiduboxapp:c_discovery/foo', function(require, exports, module, $){
    var template=require("baiduboxapp:c_tmpl/discovery/foo");
    function bindEvent(id){
        $.byId(id).addEventListener(xxx);
        //....
    }
    module.exports = function(id, data){
        template(id, data);
        bindEvent(id);
    }
});

编译产生依赖关系表

"baiduboxapp:c_discovery/foo": {
    "deps": [
        "baiduboxapp:c_css/discovery/foo",
        "baiduboxapp:c_tmpl/discovery/foo" ]
},
"baiduboxapp:c_tmpl/discovery/foo": {
    "deps": [
        "baiduboxapp:c_tmpl/discovery/public/header",
        "baiduboxapp:c_tmpl/discovery/public/footer",
        "baiduboxapp:c_css/discovery/foo"    ]
},
"baiduboxapp:c_css/discovery/foo": {
    "uri": "baiduboxapp/components/css/discovery/foo.css",
},
"baiduboxapp:c_tmpl/discovery/public/footer": {
    "deps": [ "baiduboxapp:c_tmpl/discovery/public/logo" ]
},
"baiduboxapp:c_tmpl/discovery/public/header": {
    "deps": [ "baiduboxapp:c_tmpl/discovery/public/logo" ]
},
"baiduboxapp:c_tmpl/discovery/public/logo": {
    "uri": "baiduboxapp/components/tmpl/discovery/public/logo.js",
}

调用一个组件

require('baiduboxapp:c_discovery/foo');
Bdbox.c_discovery.foo('content', data);

实际输出:

<!--先在head输出css-->
<link rel="stylesheet" type="text/css" href="baiduboxapp/components/css/discovery/foo.css" />
<!--在body依次输出模板依赖和js模块依赖-->
<script type="text/javascript" src="baiduboxapp/components/tmpl/discovery/public/logo.js"></script>
<script type="text/javascript" src="baiduboxapp/components/tmpl/discovery/public/header.js"></script>
<script type="text/javascript" src="baiduboxapp/components/tmpl/discovery/public/footer.js"></script>
<script type="text/javascript" src="baiduboxapp/components/tmpl/discovery/foo.js"></script>
<script type="text/javascript" src="baiduboxapp/components/discovery/foo.js"></script>
<div id="content"></div>
<script>
    Bdbox.c_discovery.foo('content', data);
</script>

结合之前的解决方案,webapp的组件化开发更加得心应手!《详细文档

第七步:自己开发工具,减少等待联调成本

使用工具,对接口进行模拟,提前跑通流程,减少联调时间

ServerRD

FE

ClientRD

  • 手机百度是一款hybrid APP
  • 增加了客户端RD的角色,需要跟其配合
  • 制定接口/接口联调/webview页面联调

利用FIS减少和PHPer联调成本

  • 可以支持本地接口数据模拟
  • ORP线上第一台可以调试数据
  • 模拟数据放在 tpl/test 中,供他人复用

实际开发中,FE先自己指定数据格式,并且本地调试数据,等PHPer开发完,让其按照调试数据格式输出data即可~

利用chrome扩展减少和CRD联调过成本

chrome扩展

实际开发中,FE利用chrome扩展,在document_start时注入js,模拟webview的js接口;等CRD开发完成,直接调用真是js接口即可~

手机百度chrome扩展功能

  • 支持FIS调试
  • 客户端接口模拟
  • 手机百度userAgent模拟
  • 快速生成二维码
  • CRD客户端js接口自助测试
  • 内置JSON-handler
  • URL映射和框URL生成

手机百度前端工程化体系

手机百度前端工程化体系