H5移动应用的发布优化

H5移动应用常常是一个手机网页应用,或被包装成一个安卓或苹果的应用程序。 在开发完成后,将本地开发版本上线时常常需要优化,主要的优化思路是:

1 缓存优化

对一个典型的H5应用,如index.html,它引用了index.css, index.js等资源:

<!DOCTYPE html>
<html>
<link rel="stylesheet" href="index.css" />
<script src="index.js"></script>
H5应用内容
</html>

默认情况下,缓存一般是这样工作的:

以上称为"no-cache"缓存控制策略(Cache-Control: no-cache),是很多浏览器的默认策略。 浏览器为每个资源都会询问服务器,如果服务器有更新,则下载更新(服务器返回状态200),如果没有更新,则使用本地缓存(服务器返回状态304)。 这种策略保证了资源的正确性,缺点是访问服务器次数过多。

并不是所有浏览器默认都这么办,典型的如微信浏览器,它使用复杂的检查更新策略,尽可能少的访问网络。开发者经常遇到这样的问题: 尽管服务器已更新了文件,浏览器基本不去更新,导致应用不一致或出错,而且微信检查资源更新的时机不好预测,可能过一两天再访问应用就正常了。 大多数情况下,我们看到的策略是:

尽管效率高了,可它导致应用无法及时更新。那么问题来了,如何既能利用缓存策略减少对服务端的访问,又能及时更新应用?

建议方法是这样的:

上面对资源引用时,URL添加了v={HASH值}版本标识。当文件更新时,这个值也相应变化,浏览器就会被强制要求获取更新的文件,保证及时更新。

于是,在发布应用时,我们可以用一个工具来自动为URL添加HASH值。下面以筋斗云发布优化工具webcc为例讲解优化步骤。

注意:webcc默认与版本控制工具git配合使用,它将指定文件夹下在git库中的文件作为源文件编译。 webcc是php开发的工具,请安装好php环境。 假定H5应用目录为myweb,已添加到git版本库中;如果应用目录未使用git管理,先应设置设置环境变量WEBCC_LS_CMD来指定源文件列表,例如:

export WEBCC_LS_CMD='find . -type f'

如果项目用svn管理,应该把.svn目录过滤掉:

export WEBCC_LS_CMD='find . -type f | grep -v .svn'

或者如果已配置好svn命令行,最好直接用svn命令列出源文件:

export WEBCC_LS_CMD='svn ls -R'
  1. 在需要HASH的地方使用?__HASH__这样的格式:

    <!DOCTYPE html>
    <html>
    <link rel="stylesheet" href="index.css?__HASH__" />
    <script src="index.js?__HASH__"></script>
    H5应用内容
    </html>
  2. 在H5应用目录下,添加配置文件webcc.conf.php,配置规则如下:

    <?php
    
    $RULES = [
        'index.html' => 'HASH'
    ];

    如果有多个文件需要加HASH,可以这样指定:

    <?php
    
    $RULES = [
        '*.html' => 'HASH',
        'lib/app_fw.js' => 'HASH',
    ];

    注意webcc配置文件是php格式,别忘记有<?php文件头。

  3. 运行webcc(Windows环境下建议在git-bash中运行), 生成优化后的网站:

    ### 如果未使用git管理工程,先加上下一句命令:
    ### export WEBCC_LS_CMD='find . -type f'
    php webcc.php myweb -o myweb-online

    其中myweb是H5应用的开发版本文件夹,myweb-online是生成的优化版本。 webcc生成指纹的方法是取文件sha1值的后6位。

注意几种情况:

1.1 H5官方标准的应用缓存

熟悉H5的朋友可能知道,H5官方标准(W3C)中定义了使用manifest文件的方法用于缓存控制。 目前不建议使用,因为:

2 JS/CSS优化

在实际开发中,会引入很多的库,通过对JS/CSS文件进行最小化及合并,实现减少交互次数与交互数据量的目标。

例如有以下H5入口文件index.html

<!DOCTYPE html>
<html>

<link rel="stylesheet" href="lib1.css" />
<link rel="stylesheet" href="lib2.css" />
<link rel="stylesheet" href="module1.css" />
<link rel="stylesheet" href="module2.css" />
<link rel="stylesheet" href="index.css" />

<script src="lib1.js"></script>
<script src="lib2.js"></script>
<script src="module1.js"></script>
<script src="module2.js"></script>
<script src="index.js"></script>

H5应用内容
</html>

其中引入的文件可能有第三方库如lib1、lib2,以及应用自身通用模块如module1, module2,最后是该应用专用的index.js/index.css内容。 打开这个文件,浏览器需要访问11次服务器(H5主页及引用10个文件)。

2.1 极致优化 - 合并所有文件

最极致的优化,可以将所有文件最小化后合并到一起。以使用webcc为例:

<!DOCTYPE html>
<html>

<!-- WEBCC_BEGIN MERGE -->
<link rel="stylesheet" href="lib1.css" />
<link rel="stylesheet" href="lib2.css" />
<link rel="stylesheet" href="module1.css" />
<link rel="stylesheet" href="module2.css" />
<link rel="stylesheet" href="index.css" />

<script src="lib1.js"></script>
<script src="lib2.js"></script>
<script src="module1.js"></script>
<script src="module2.js"></script>
<script src="index.js"></script>
<!-- WEBCC_END -->

H5应用内容
</html>

将这些JS/CSS文件引用放到WEBCC_BEGIN MERGE/WEBCC_END标记之间,webcc会自动对它们进行合并和压缩。

webcc配置文件webcc.conf.php中与上例相同,指定index.html使用HASH规则即可:

<?php

$RULES = [
    'index.html' => 'HASH'
];

运行webcc后,生成的index.html内容如下:

<!DOCTYPE html>
<html>

<style>
lib1.css内容
lib2.css内容
...
</style>

<script>
// lib1.js内容
// lib2.js内容
...
</script>

H5应用内容
</html>

通过这样的处理,11个文件变成了1个文件,原先需要访问11次服务器,现在只要访问1次就可以了;而且经过文件最小化(minify),传输的内容也大小减少。

2.2 合理优化 - 分类合并文件

合理的优化一定是均衡考虑,而不是追求某一个场景的极致。
-- 天笑

上一节的优化合并并内嵌了所有文件,实现了应用的极致优化。 然而考虑一个频繁更新的应用,它带来一个问题:用户访问时,一遇到应用更新,就经常需要下载完整的应用。是否可以将更新影响到的内容最小化呢?

实践中,对于频繁更新的应用,我们采取折衷的方法,根据更新频度,可以将引用的文件分成几类分别合并,例如:

基于以上分类,对于上面的例子,我们可以使用这样的优化设置:

<!DOCTYPE html>
<html>

<!-- WEBCC_BEGIN MERGE=libs -->
<link rel="stylesheet" href="lib1.css" />
<link rel="stylesheet" href="lib2.css" />

<script src="lib1.js"></script>
<script src="lib2.js"></script>
<!-- WEBCC_END -->

<!-- WEBCC_BEGIN MERGE=modules-->
<link rel="stylesheet" href="module1.css" />
<link rel="stylesheet" href="module2.css" />

<script src="module1.js"></script>
<script src="module2.js"></script>
<!-- WEBCC_END -->

<!-- WEBCC_BEGIN MERGE -->
<link rel="stylesheet" href="index.css" />

<script src="index.js"></script>
<!-- WEBCC_END -->

H5应用内容
</html>

以上设置经webcc处理后,生成的index.html示例如下:

<!DOCTYPE html>
<html>

<link rel="stylesheet" href="libs.min.css?409dc2" />
<script src="libs.min.js?e4172c"></script>

<link rel="stylesheet" href="modules.min.css?81235a" />
<script src="modules.min.js?a34a56"></script>

<style>
// index.css内容
</style>

<script>
// index.js内容
</script>

H5应用内容
</html>

这样,在初次访问时,交互次数从未优化时的11次下降到5次;与上节的极致优化相比,下载数据量基本相同,交互次数多了4次稍差; 而在更新了index.js并上线后,用户再次访问只需要更新index.html文件,其它libs, modules中的内容都未变化,不必更新,与极致优化相比节省了很多流量。

以上讲解了优化的原则,实际策略可根据您的项目需要灵活配置。

3 逻辑页优化

逻辑页是H5移动应用中的重要概念。用户看到的每个页面并不是一个个独立的H5应用,而是一个H5应用中的一个逻辑页而已,这样实现出来的应用才能运行更平滑且省流量。

不支持逻辑页的H5多页应用都在耍流氓。
-- 天笑

一个H5多逻辑页应用(亦称“变脸式应用”)通常由基础架构层(框架)、通用逻辑层(多逻辑页共享部分)以及一个个逻辑页构成。下面是一个H5多页应用的结构:

<!DOCTYPE html>
<html>

<!-- 框架,所有H5应用共享 -->
<link rel="stylesheet" href="app_fw.css" />
<script src="lib/app_fw.js"></script>

<!-- 通用逻辑层,当前应用内所有逻辑页共享 -->
<link rel="stylesheet" href="index.css" />
<script src="index.js"></script>

H5应用框架内容

<!-- 逻辑页: page1 -->
<link rel="stylesheet" href="page1.css" />
<script src="page1.js"></script>
<div id="page1">逻辑页内容</div>

<!-- 逻辑页: page2 -->
<link rel="stylesheet" href="page2.css" />
<script src="page2.js"></script>
<div id="page2">逻辑页内容</div>
</div>

</html>

上例中,在文档的后面,是所有的逻辑页,每个逻辑页都有样式(css), 控制逻辑(js)和页面内容(div)部分。应用框架中会有支持逻辑页显示和切换的操作。 在开发时,每个页面可以分开开发,实现模块化。

由于逻辑页也包含JS/CSS这些文件,所以可以把它们当成模块,也采用上一节的对JS/CSS文件最小化再合并的方法进行优化,把所有的逻辑合并到主页里。

然而,这样做有以下问题:

3.1 支持动态逻辑页和模板加载

解决以上问题的方法是使用动态逻辑页和模板加载,即框架在显示一个逻辑页时,先查看内部有没有逻辑页模板,有则用之,没有则从外部动态加载逻辑页。

这样,在开发时,所有逻辑页都可放置在主页外部独立开发;在上线时优化时,把常用的逻辑页内嵌到主页中去。

以筋斗云框架为例,H5动态逻辑页应用文件结构如下:

index.html - H5应用
page/home.html - 逻辑页home
page/home.js - home的应用逻辑
page/login.html - 逻辑页login
page/login.js - login应用逻辑
...

开发时,在index.html中不包含任何逻辑页,而在上线时,将home.html, login.html等常用页合并到主页中去。 webcc优化工具支持mergePage命令合并和内嵌逻辑页,可设置策略如下:

<!DOCTYPE html>
<html>

...
H5应用框架内容

<!-- WEBCC_BEGIN 所有内嵌的逻辑页 -->
<!-- WEBCC_USE_THIS
WEBCC_CMD mergePage -minify yes page/home.html page/login.html
WEBCC_END -->

</html>

其中,逻辑页page/home.html内容示例如下,它指定了脚本为home.js

<div mui-script="home.js">
home页内容
</div>

webcc处理后,生成的内容如下:

<!DOCTYPE html>
<html>

...
H5应用框架内容

<!-- 内嵌逻辑页: home -->
<script type="text/template" id="tpl_home">
    逻辑页链接的JS文件: home.js
    逻辑页内容: home.html
</script>
其它内嵌逻辑页...

</html>

优化策略:

4 图片优化 - 制作精灵图

图片优化有以下思路:

本文主要介绍小图片合并的方法,也就是常说的精灵图(sprite)的制作。

定义一个icon.css文件,包含用到的小图标如下:

.icon-back {
    background-image: url(icon/16/back.png);
}
.icon-menu {
    background-image: url(icon/16/menu.png);
}
.icon-add {
    background-image: url(icon/16/add.png);
}

...

我们先将所用到的图标合并成一列竖图,每一行一个图标,假定生成的图片为icon-16.png。 根据icon.css生成icon.out.css文件,修改背景图的位置属性如下:

icon.out.css:

.icon-back {
    background-image: url(icon/icon-16.png); background-size: 16px !important; background-position: 0 0px !important;
}
.icon-menu {
    background-image: url(icon/icon-16.png); background-size: 16px !important; background-position: 0 -16px !important;
}
.icon-add {
    background-image: url(icon/icon-16.png); background-size: 16px !important; background-position: 0 -32px !important;
}
...

这时,在H5应用中直接使用icon.out.css即可。 上面过程可以使用筋斗云工具jdcloud-sprite完成。注意:使用前应下载安装imagemagick软件,用于图片合并。

php jdcloud-sprite.php icon.css

这样便可生成icon.out.css以及合并后的图片。此外,它可以支持生成2倍图、按图片宽度分组生成的选项,具体可参考筋斗云工具文档(doc/jdcloud-sprite.html)。

实际在使用筋斗云移动框架的项目中,不用直接手工调用上述命令,而是在tool/Makefile中设置好命令:

sprite: ../server/icon.out.css

../server/icon.out.css: ../server/icon.css
    php jdcloud-sprite.php $< -2x -group -sprite icon/icon@2x.png

clean-sprite:
    -rm -rf ../server/icon.out.css ../server/icon/icon@2x-*.png

设置好后,直接运行tool/make-sprite.sh即可生成精灵图和相应的css文件。