• 2

  • 450

360 AI音箱H5开发实践

3星期前

“360 AI 音箱”即将发布,移动应用也在紧张有序地开发中。本文将介绍 “360 AI 音箱” 移动应用 H5 部分的实践,主要包括:

  • 项目环境搭建
  • 与 Native 交互
  • 自定义中文字体
  • 表单输入
  • Docker 部署

360 AI 音箱应用 H5 部分简介

应用主要分 4 大版块:

  1. 内容:音箱可以播放的音乐、故事、有声书等等
  2. 技能:运营预配置的音箱指令
  3. 场景:用户自定义的音箱指令
  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…),同时利用服务端缓存。

开发框架:

项目代码结构如下:

其中,

  • 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 中的接口对象
  • callNativeSpkNativeBridge的方法

参数说明:

  • 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 中实现并暴露的接口对象
  • callJSSpkJSBridage的方法

参数说明:

  • 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值为truediv元素:

<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)

复制代码

很多人不清楚debouncethrottle的区别。

  • debounce是在某事件至少停止触发多长时间后执行,比如上面的inputHandler会在我们注册的事件(keyup compositionend)至少间隔 300 毫秒才会触发
  • throttle是针对连续密集触发的一系列事件,比如scrollresize,将它们 “节流” 为均匀地每过多长时间才触发一次。

这里使用debounce包装实际的处理程序,是为了避免过早地在用户输入期间对输入进行计数。

Docker 部署

容器部署的优点是多机房灾备,某机房因切割或服务下架而停服,都不会影响线上服务。容器部署用到的是 360 HULK 云平台的容器相关服务 Stark:

Docke 部署流程如下:

  1. 本地构建 Docker 镜像
  2. 上传到 Stark
  3. 修改容器镜像
  4. 重启服务

关于使用 Docker,主要看看官方的 Get Started 和 Dockerfile 相关文档即可

小结

本文又是一篇 “急就章”,大略介绍了 360 AI 音箱 H5 开发过程中的一些基本实践,希望可以为同行提供一些参考和借鉴,也欢迎大家批评指正。另外,开发过程中还有一些涉及算法的有意思的技术点,同样值得分享,等项目上线之后有时间了再分享吧。

免责声明:文章版权归原作者所有,其内容与观点不代表Unitimes立场,亦不构成任何投资意见或建议。

程序员

450

相关文章推荐

未登录头像

暂无评论