“360 AI 音箱”即将发布,移动应用也在紧张有序地开发中。本文将介绍 “360 AI 音箱” 移动应用 H5 部分的实践,主要包括:
- 项目环境搭建
- 与 Native 交互
- 自定义中文字体
- 表单输入
- Docker 部署
360 AI 音箱应用 H5 部分简介
应用主要分 4 大版块:
- 内容:音箱可以播放的音乐、故事、有声书等等
- 技能:运营预配置的音箱指令
- 场景:用户自定义的音箱指令
- 我的:用户的智能设备、账号等
其中,“技能”和 “场景” 版块由 H5 制作。如下图所示,技能部分主要包括运营后端预配置的指令列表和详情两个页面。
注意:本文图片为 “360 AI 音箱” 版权所有。另外,因为是设计稿截图,应用实际发布后的外观可能会有所不同。
技能列表和详情主要涉及使用自定义字体:Adobe 思源宋体(source.typekit.com/source-han-…)。
场景,其实是用户自定义的技能。场景相对复杂,不仅涉及类似技能的展示,还涉及增、删、改,甚至还有与原生配合的闹钟场景。
项目环境搭建
前端 H5 的技术方案有单页 SPA、传统 B/S 架构。考虑到项目涉及使用自定义字体和保存自定义场景的中间结果,所以采用了传统 B/S 架构,以最大限度避免加载 Web 字体的 FOIT/FOUT(www.zachleat.com/web/fout-vs…),同时利用服务端缓存。
开发框架:
- ThinkJS:360 优秀的开源服务端 Node.js 框架(thinkjs.org/)
- Webpack:前端编译打包(webpack.js.org/)
- Vue.js:SPA 组件式开发框架(vuejs.org/)
项目代码结构如下:
其中,
- deploy:是部署脚本和 Docker 构建脚本所在目录
- frontend:是前端资源目录,主要是 Webpack 编译入口文件、模板文件及 JS 和 CSS 资源
- runtime:是 ThinkJS 运行时存放配置等信息目录
- src:是服务端源代码目录
- view:是服务端模板目录,模板文件由 Webpack 编译保存过来
- www:是服务端静态资源目录,比如 Webpack 打包后的 JS、CSS、图片、字体等
实际项目中的静态资源都使用 Webpack 插件直接上传 CDN,图片也直接引用 CDN 图片,因此服务端并不保存任何静态文件。
与 Native 交互
H5 与 Native 共同定义了两个接口,用于双方互相调用。
1. JS 调 Native
// JS调用传参,通过参数把数据传给Native
SpkNativeBridge.callNative({action, params, whoCare})
复制代码
接口说明:
SpkNativeBridge
是 iOS 和 Android 实现并注入到 WebView 中的接口对象callNative
是SpkNativeBridge
的方法
参数说明:
-
action
:字符串,希望 Native 执行的操作 -
params
:JSON 对象,要传给 Native 的数据 -
whoCare
:数值,表示 JS 希望哪个端响应- 0:iOS 和 Android 都响应(默认值)
- 1:iOS 响应
- 2:Android 响应
返回值:具体商定
2. Native 调 JS
// Native调用传参,通过参数把数据传给JS
SpkJSBridage.callJS({action, params, whoAmI})
复制代码
接口说明:
SpkJSBridage
是 JS 在 WebView 中实现并暴露的接口对象callJS
是SpkJSBridage
的方法
参数说明:
-
action
:字符串,希望 JS 执行的操作 -
params
:JSON 对象,要传给 JS 的数据 -
whoAmI
:数值,表示哪个端调用的- 1:iOS 调用的
- 2:Android 调用的
返回值:具体商定
3. 调用示例
下面通过两个例子来说明 H5 与 Native 如何使用上述接口交互。下面这张图是用户创建场景期间退出时原生要弹出确认框的情景:
如图所示,导航条是 Native,下面是 WebView。导航条上的返回和保存按钮需要 H5 根据场景内容控制。比如,如上图所示,用户有未保存的内容时点击了返回按钮,H5 要告诉 Native 是否可以返回,还是需要提示。交互过程如下:
Native 调用
window.SpkJSBridge.callJS({
action: "can_back",
params: {},
whoAmI: 1/2
})
复制代码
H5 返回值
{
can: false,
target: "prev"
}
复制代码
返回值说明:
- can: 布尔值,true 表示可以返回,false 表示需要弹确认框
- target: 字符串,"prev" 返回上一级, "top" 返回顶级,"closeweb" 关闭之前的 webview
换句话说,用户点击 Native 的 “返回” 按钮,Native 调用 JS 的can_back()
方法,JS 判断是否有未保存内容,如果有则返回上述值,通知 Native 弹确认框。
除了 “返回”,还有“保存” 按钮。H5 负责控制 “保存” 按钮是否启用,以及启用之后用户点击调用的方法。具体来说,每次 WebView 加载后,JS 判断是否可以保存,比如上图中场景只有 “对音箱说” 的部分,没有“音箱的回应”,不能保存,因此 H5 会调用 Native 的displayRightButton()
方法,告诉 Native 按钮文字、是否启用,以及启用后用户点击的回调函数:
window.SpkNativeBridge.callNative({
action: "displayRightButton",
params: {
name: "下一步",
enable: false,
callbackName: "scene_topic_save"
},
whoCare: 0
})
复制代码
4. 注意事项
H5 调 Native 时传参,如果调 Andoid 则只能传基本数据类型(字符串或数值),不能传 JSON 对象;iOS 则没问题。为此,H5 需要判断 WebView 环境是 Android 还是 iOS,如果是前者,则将 JSON 对象转换为字符串:
// 包装方法,对Android将JSON转换为字符串
window.callNative = (param) => {
if (speakerWebviewHost === 2) param = JSON.stringify(param)
window.SpkNativeBridge.callNative(param)
}
复制代码
另外,JS 方法向 Native 返回值必须同步返回,虽然 Native 调用 JS 是异步调用,但 JS 如果返回 Promise,Native 是无法处理的。因此,需要在使用XMLHttpRequest
对象时将async
参数设置为false
:
const scene = $.ajax({
url,
async: false
}).responseJSON
复制代码
最后,还有一个 “坑”:Android 如果未开启 WebView 的 localStorage 特性,使用 localStorage 的 H5 页面就会 “冻结”!
自定义中文字体
如前所述,“技能” 列表和详情都需要用到 Adobe 开源的 “思源宋体”,而且原生闹钟等也会用到该特殊字体:
图中的 “玩法介绍”“功能介绍” 的标题以及前者的内容都需要使用自定义中文字体。然而,设计师给的开源字体文件有 23 MB 这么大,包含 65000 多字符。考虑到技能和闹钟用到这个字体的字符有限,我们决定使用字体截取技术。
经过调研,并且考虑到技能列表需要动态截取,最终我们自建了一个字体服务:奇字库。“奇字库” 提供中文字体在线动态截取服务,让字体文件从十几 MB 瞬间变成十几 KB、几 KB;基于 Adobe 和 Google 共同开发的 Web Font Loader(github.com/typekit/web…)定制了加载脚本,实现了字体加载与应用完全自动化。
目前,“奇字库” 囊括了公司所有付费的版权字体,可供公司内部各业务线的各类 Web 或客户端项目使用:
为获得最佳用户体验,“奇字库”提供了丰富的接口,可满足灵活定制的需求。服务端或浏览器可以通过 API 调用,动态截取字体。共有两大类共 8 个API:第一类是获取字体 “URL” 的,包括上传到 CDN 的 URL 和 base64 格式的 Data URL;第二类是获取 CSS @font-face
规则文本的,包括获取 CDN URL 和 Data URL 内容的 CSS。
比如,获取字体的 CDN URL,API 返回结果示例如下:
{
"ttf": "//s3.ssl.qhres.com/static/b73305c8dde4d68e.ttf",
"woff": "//s1.ssl.qhres.com/static/e702cca6e68ab80a.woff",
"woff2": "//s1.ssl.qhres.com/static/e27f2a98e5baf04d.woff2",
"eot": "//s2.ssl.qhres.com/static/590b2e87fb74c9d6.eot"
}
复制代码
再比如,获取字体 Data URL 的 CSS @font-face 规则,API 返回的结果示例如下:
@font-face {
font-family: myWebFont;
src: url('data:font/opentype;base64,Fg8AA...8AAw==');
src: url('data:font/opentype;base64,Fg8AA...8AAw==?#iefix') format('embedded-opentype'),
url('data:font/opentype;base64,d09GM...AAA==') format('woff2'),
url('data:font/opentype;base64,d09GR...Ssw==') format('woff'),
url('data:font/opentype;base64,AAEAA...//wAD') format('truetype');
}
复制代码
最简单的方式就是在网页里直接复制粘贴代码:
不过因为我们的项目有服务端,所以可以将截取到的字体文件与网页及 CSS 一起下发到浏览器,从而完全避免 FOUT,实现与使用本地字体一样的用户体验。
“奇字库” 目前只是 360 内部的项目,仅对公司内部提供服务,外部无法使用。如果读者对字体截取有兴趣,可以参考笔者之前的文章 “前端字体截取:实战篇”(mp.weixin.qq.com/s/pq9hXz_iG…)。
表单输入
表单输入的重点,一是组件化输入框,便于添加和删除;二是对用户输入的计数,涉及composition*
事件;三是使用debounce
,避免过早对用户输入进行计数。
首先,为满足用户输入多条 “音箱回应” 及自定义占位符文本的需求,输入框使用了contenteditable
值为true
的div
元素:
<div class="fieldset">
<div class="placeholder">输入想让音箱说的话<small>/最多{{lengthLimit}}字</small></div>
<div class="input" contenteditable="true"></div>
<img class="clearReplyInput" src="//p0.ssl.qhimg.com/d/lisongfeng/icon_close_s.png">
<span class="countDown">0</span>
</div>
复制代码
而且,基于这个元素构建了前端组件:
import inputComponent from './_replyInputComponent'
复制代码
每次创建这个组件的新实例,就会自动在 DOM 上添加新的输入框:
// +继续添加
new inputComponent({formsSelector, formTemplate, lengthLimit})
复制代码
其次,是对用度输入的字符数进行计数。此时,要用到三个事件:
keyup
:用户触摸软键盘按键后触发compositionstart
:用户调用输入法开始输入一段文字时触发,类似keydown
compositionend
:用户选取了最终要输入的文本结束一次输入时触发,比如用户使用拼音或五笔输入法,之前的输入比如 “jiang ge gu shi” 只是 “中间输入”,不会触发这个事件,只有当用户最终选取了“讲个故事” 之后才会触发
但是,compositionend
不能识别英文的 “组词输入”,所以最终还是绑定了keyup
事件:
// 开始输入隐藏占位符提示
this.input.bind('compositionstart', e => {
this.placeholder.hide()
})
// 组词结束后,处理内容并绑定input事件
this.input.bind('keyup compositionend', inputHandler)
复制代码
最后,就是使用debounce
对用户输入事件做延迟处理:
import debounce from 'lodash.debounce'
// 输入检查
const inputHandler = debounce(e=>{
// ...
}, 300)
复制代码
很多人不清楚debounce
和throttle
的区别。
debounce
是在某事件至少停止触发多长时间后执行,比如上面的inputHandler
会在我们注册的事件(keyup compositionend
)至少间隔 300 毫秒才会触发throttle
是针对连续密集触发的一系列事件,比如scroll
或resize
,将它们 “节流” 为均匀地每过多长时间才触发一次。
这里使用debounce
包装实际的处理程序,是为了避免过早地在用户输入期间对输入进行计数。
Docker 部署
容器部署的优点是多机房灾备,某机房因切割或服务下架而停服,都不会影响线上服务。容器部署用到的是 360 HULK 云平台的容器相关服务 Stark:
Docke 部署流程如下:
- 本地构建 Docker 镜像
- 上传到 Stark
- 修改容器镜像
- 重启服务
关于使用 Docker,主要看看官方的 Get Started 和 Dockerfile 相关文档即可
- Get Started:docs.docker.com/get-started…
- Dockerfile reference:docs.docker.com/engine/refe…
小结
本文又是一篇 “急就章”,大略介绍了 360 AI 音箱 H5 开发过程中的一些基本实践,希望可以为同行提供一些参考和借鉴,也欢迎大家批评指正。另外,开发过程中还有一些涉及算法的有意思的技术点,同样值得分享,等项目上线之后有时间了再分享吧。
暂无评论