本文作者:任家乐
原创声明:本文为阅文前端团队 YFE 成员出品,请尊重原创,转载请联系公众号 ( id: yuewen_YFE ) 获取授权,并注明作者、出处和链接。
难用 or 姿势不对?
「用户体验 > 开发体验」Google AMP 的设计准则毫不掩饰地标记了它的核心观念,从一开始就承认了它对开发者的不友好。
「Webnovel m 站」作为国内首批使用 AMP 技术的产品「关于 AMP,Webnovel 都做了什么?」之一,结论上创造了显著提升的性能数据,过程中正如谷歌核心观点所说,开发体验非常差。最近得来一次机会,我们 Webnovel 面向非洲用户的新产品「Ficool m 站」实践了全站 AMP,从中找到了使用 AMP 的新姿势 - 「Next.js + AMP + Preact」,开发体验提升了不止一个档次,在此分享给大家。
开启洗白之路 - 从新项目入手
Ficool m站
作为 Webnovel 面向非洲地区的站点,它不仅覆盖 web 渠道,其借助 Google TWA 技术打包而成的轻量 APP 还将预装到 Android Go 系统的手机上,此系统对 APP 内存、CPU 占用有着严格的要求,现已成功入库。目前「Ficool m 站」大多数页面还暂未被 Google 搜索引擎收录,后续可以跟大家分享一下实际的效果以及我们在服务端做的进阶优化。
Why AMP?
比起「Ficool m 站是什么?」,对本文来说更重要的是「Again,Why AMP ?」
一则演讲的启发
今年 AMP conf 中一位伊拉克小哥的演讲令人印象深刻。
「No poser, no internet, no support: How AMP bridges the app gap in Iraq and other war-impacked region」
身处于战乱、网络条件落后的国家,他们选择使用 AMP,并借助 AMP 提供的一系列组件及优化方案一步步地填补了 APP 在体验、兼容性方面的落差,从而建立起了 APP 和曾经他们主动放弃的一批落后用户之间的桥梁。
国内的我们每天享受着 4G 可能某一天享受的就是 5G,很难想象遥远的非洲兄弟们忍受着怎样的网络条件... 我们的产品也终于有机会去了一趟非洲「肯尼亚」,并带回了一些「前线」消息 “ 那里的网速虽然没有想象中差,但普遍使用 3G 网络且速度有限,同时移动网络套餐相比国内要贵很多。”
- 「带宽贵」
- 「网速慢」
- 「 Webnovel m 站」在 AMP 上尝鲜成功
这 3 点已经足够推进「Ficool m 站」全站 AMP 的想法了,不仅如此,我们还走了一条 AMP 的小众路线 - 「Next.js + AMP + Preact」。
洗白的最佳姿势
Next.js + AMP + Preact
有了 「Webnovel m 站」的经验教训,我们预留时间做了大量的前期调研,并设定了一些目标:
- 开发过程效率要提升
- 代码后期可维护性要高
- 关注开发者身心:尽量使用一些现有的主流技术,摆脱旧的开发模式
最终 get 了 Next.js + AMP + Preact 这样的组合方式,听起来好像比 AMP + HTML 洋气了一些?这其实是一个极少人尝试过的 AMP 开发形式,在开发时间的压迫下一步棋走错全盘皆输!其过程采坑无数,大家不妨看看我们都经历了哪些历程。
2个历程的采坑
1、从 HTML「硬写 」 到 React
回顾「Webnovel m 站」 AMP 的旧开发方式:直接在 HTML 中引入 AMP 组件,以开发 HTML 的方式编写 AMP 页面 - 不可忍!
实际上,Next.js 在 8.1 版本以上就已经支持了 AMP 的开发,这意味着我们完全可以直接用 React 开发 AMP 页面。
抽组件
旧方式「图 1」对于组件的提取较为困难,借助 EJS 模板固然可以抽离 template 的部分并使用 include 语法将其引入,但 amp-list
的部分很难提取成组件(template 的部分是不确定的),而在 React AMP 中,我们可以将其提取成组件,并通过增加失败「fallback」、加载中「placeholder」等功能「图3」,使 amp-list
用起来更灵活。
「图 1:列表页 - HTML 旧方式开发」
「图 2:列表页 - React 方式开发」
「图 3:列表页 amp-list
组件 - React」
同样的,旧开发模式下能难做到服务端渲染(直出)部分、异步渲染部分的模板统一,这是由于 HTML 直出模板「 EJS 模板」语法和 AMP 的 「mustache template 模板」语法是截然不同的,然而在 React AMP 中我们却可以:
例如「图 2」中的 <BookItemTtoB>
组件,直出部分自然是正常传递 props,而 amp-list
的模板部分只需要用 mustache 模板语法 {{ bookId }}
传递值即可 ,<BookItemTtoB>
也就达到了两种情景下的复用。
编写样式
继续回顾之前的旧开发方式:样式必须内联放于 HTML 中的 <style amp-custom>
标签内,就当时的本地开发流程来说,要实现直接编译 scss 文件并将生成的 css 内联到 <style amp-custom>
标签内,改造起来远没有添加一条 webpack 配置那么简单,最终我们粗暴地将 css 文件用 EJS 引入模板的方式 include 到了 <style>
标签内,这显然不太优雅。
新的开发流程下,我们只需在 next.config.js
中拓展 webpack 的配置即可:引入 sass-loader
以及 Next.js 内置的 styled-jsx
「图4」,Next.js 会直接将编译好的 css 内联到 <style amp-custom>
标签内「图 5」,就是这么简单。
「图 4:next.config.js
中编译 css 相关配置」
「图 5:组件中引入样式」
总结:
1、用 React 方式开发 AMP 页面,组件的提取更简单。
2、页面的逻辑相比冗长的 HTML 代码更易维护且风格统一「单一 React 风格 VS HTML + EJS + mustache 混用」。
3、样式的编写更加容易、开发形式也吻合当前趋势。
2、AMP 页面的羽翼 - amp-script
第一阶段我们实现了用 React 愉快地进行 AMP 的开发,但局限是:React 的生命周期函数不能用、一切自定义的 JS 交互依然不能实现(AMP 不允许引入我们自己的 JavaScript )。
好消息!在今年 4 月中旬 AMP 发布了 amp-script
,借助它就可以写我们自己的脚本了!当然,它有一定局限性:大小上限( uncompressed 150KB),数量限制(每个页面只允许引入 1 个 amp-script
)以及 API 限制 ... 不管有多少限制,为了实现更多的复杂交互,我们需要使用这个能力。
amp-script
非常规用法 - 使用 Preact
AMP 官方提供的 amp-script
demo 基本都是原生 Javascript 的写法,而在我们的 React AMP 项目中很不希望维护 2 种风格的代码,因此我们探索了用 React 开发 amp-script
的可能性。
首先是解决大小限制问题:
「各框架源码 uncompressed 大小」
size ( uncompressed ) | amp-script 剩余空间 | |
---|---|---|
React | 110kb | 150 - 110 = 40kb |
Preact | 8.2kb | 150 - 8.2 = 141.8kb |
「图 6:React vs Preact in amp-script」
React 源码足足有 110kb,如果使用 React,那我们自己的脚本只能写 40kb,这显然不够,因此我们选择了既能保持 React 开发风格、又能控制 amp-script
大小的 「Preact,React 的轻量版」。
webpack 实现 amp-script
的编译&打包
amp-script
的引入方法不同于普通的 React 组件「图 7、图 8」,其中 amp-script
组件必须有 src 属性,其值为你引入的脚本文件 url(绝对地址),由于每个页面只能引 1 个脚本,我们需要保证这个脚本已经是打包后的最终脚本,此时毫无疑问要使用 webpack 进行依赖分析及打包了。
「图 8:amp-script 的引入脚本的方式」
我们重新为 amp-script
的打包写了个独立的 webpack 配置「webpack.amp.config」,使其编译打包过程更加单一简单,尽量不和 next.config.js
的配置项搞混。
此 webpack.amp.config
的目标:对目标脚本进行依赖分析,最终经过 babel 编译打包成我们所需的 es5 脚本(此脚本就是 amp-script 最终引用的脚本)。
这个功能算是 webpack 比较基本的功能了,按 webpack 官网 demo 使用即可,重点配置项可参考「图 9」,其中 getAllAmpScriptEntries() 会获取所有需要编译的 amp-script
脚本路径,最终生成的脚本存放于 Next.js 托管的静态资源文件夹下 ./static
。
「图 9:amp-script
webpack 配置项重点部分」
amp-script
开发方式尘埃落定
至此,我们已经可以正常享用 Preact 版 amp-script
了。如果只是简单的 DOM 操作,可以将需要被操作的 DOM 放在 amp-script
标签内、通过 ID 选择器获取元素即可,但我们既然使用了 Preact, 更推荐的做法是:需要操作的 DOM 全权由 amp-script
引用的 Preact 脚本渲染出来。
例如「图 10」中蓝色吸底栏,它涉及到的交互有:点击后展示 confirm 弹窗、请求后端进行相关操作(加入/移除书架)、接口返回成功后进行前端回显,普通页面的交互不外乎如此,具体实现思路是:React AMP 页面准备一个空的具有 ID 标识的 div 容器,借助 Preact 的 render 方法渲染出吸底栏组件,直接在 Preact 中进行事件的绑定、setState 更新 UI 等逻辑。
「图 10:详情页吸底栏使用 amp-script
脚本渲染」
amp-script
脚本 CDN 化
借助 CDN 可以减轻服务器的压力、最快地响应用户,因此所有的 JavaScript 资源都应该放于 CDN 上,amp-script
当然不能例外。
amp-script
的打包编译流程是独立于 Next.js 的,但当我们打包 Next 脚本时,两个独立流程的融合不可避免,我们希望执行 build 命令后,amp-script
的 src 地址将会被替换为 CDN 域名,同时 amp-script
脚本文件名也会加上 md5 码... 来继续倒腾 next.config.js
吧!
amp-script
的 src 路径替换 + 文件名 md5 化,这个需求和绝大多数图片的引入基本一样,因此用 webpack 的 file-loader 就可以实现「图 11」。
「图 11:file-loader
替换 amp-script
路径」
为避免报错,test 对应的正则建议只匹配 amp-script
的脚本,同时 publicPath 也需进行「本地/线上」的区分用以保证本地能够正常引用 amp-script 脚本(本地依旧访问 /static/
路径下的脚本)。为配合 file-loader
的使用,AMP 页面中也需把 amp-script
脚本的引用方式替换为 require()
方法引用。
一套流程下来,完全解决了 amp-script
脚本路径替换 + 文件名加 md5 的需求。
amp-script
脚本跨域问题完美解决
到了这一步,眼看着 amp-script
享受到了 CDN 的待遇、Next 服务减轻了一些压力,然而却爆出了跨域错误「图 12」。
此跨域问题是 amp-script
自身的一套安全策略抛出的异常,官方说明可参考「图 13」,大致意思是,如果我们引用的 amp-script
脚本和页面 URL 域名不同,需要在各页面 meta 标签内,添加 amp-script-src 对应的 hash code ,此 code 根据脚本内容、基于 Content Security
Policy 「CSP」生成,是每个脚本独一无二的编码。
「图 12:amp-script
跨域报错」
「图 13:amp-script
安全策略」
幸好 AMP 官方也提供了 CSP hash code
的生成工具@ampproject/toolbox-script-csp
,我们初步的思路定了下来:
- 开发 webpack 自定义 plugin,具体功能:获取编译后的
amp-script
脚本,同目录下生成对应的 hash code 文件。 - 每个 AMP 页面添加 meta 标签,其 content 为引入的 hash code 文件。
- Next.js 配置
raw-loader
,实现 meta 标签内 content 内容替换为 hash code 文件内容。
Step 1: webpack 自定义 plugin
此 plugin 功能单一还算好写,可以参考如下「图 14」,重点是获取到编译后的 amp-script
脚本内容、生成 hash code 、存放于同一路径下的 hash.txt 文件内。最终在 webpack 配置文件中 plugins 中引入此插件并实例化即可。
「图 14:webpack 自定义 plugin」
Step 2 & Step 3: next.config.js
配置 raw-loader
,AMP 页面引入 hash code 文件
页面中的改动如下所示:
「图 15:AMP 页面中引入 hash code 文件,添加 meta 标签」
next.config.js
中也需要配置 raw-loader
,实现 meta 标签内 content 值的替换。
「图 16:next.config.js
中配置 raw-loader」
跨域问题得以解决!
总结:实现了 amp-script
React 方式开发的全过程。有了自定义的脚本能力,AMP 页面的开发更加灵活。
最后一层加持
至此,开发过程中难免还会面临一些 amp-script
都无法实现的交互形式,这样的交互着实挑战了 AMP 的设计原则,换句话说,如果能够做到交互形式上完全遵循 AMP 的规范,开发体验则会畅通无阻!
AMP 设计原则
AMP 设计原则中,没有规定你具体要怎样设计、怎样实施,但核心原则必须是:只做对用户体验有利的事情!其中有2条原则对我们开发者来说会有一定的启发「图 17」
「图 17:AMP 设计原则 2 条」
4.涉及到开发的各个层面应职责分明,在正确的层解决问题。
5.只做可以让网页变快的事情。
逻辑后置
「涉及到开发的各个层面应职责分明,在正确的层解决问题」- 如果逻辑放于后端来说对用户体验较为友好,请不要仅仅因为前端实现起来简单,而把所有的逻辑都放在前端。
这里有一些场景可以参考:
- 需要使用
amp-mustache
组件(弱逻辑的模板语言,不支持运算、正则以及一些复杂判断逻辑)时,很多逻辑应该尽量让后端同学实现,例如:搜索页关键词高亮逻辑「图 18」,后端可以直接返回带有高亮样式的 HTML 元素。 - 对于长列表页面,下拉加载功能可以使用
amp-list
组件,但这需要后端协助加字段返回下一页的请求 URL 并携带参数、同时需要判断是否是最后一页而做逻辑的变更。当然前端也可以用amp-script
实现下拉加载,但是其效果远没有直接引用amp-list
好,此时逻辑后置很有必要。
「图 18:搜索页关键词蓝色高亮」
AMP 实现不了的窘迫场景
只做可以让网页变快的事情 - AMP 不推荐引入任何会导致动画帧率变慢、页面加载速度受限的组件、特性。
我对它的理解是,如果不能保证自己写的组件完全符合 AMP 标准,那就尽量用 AMP 提供的组件吧!这里不得不再泼一下冷水了,amp-script
真的有很多限制,它并不能实现所有交互逻辑,只是给开发者开了个不大不小的窗户而已。这种情况只有 2 条路可选了:
- 调整需求和设计稿。
- 妥协、不使用 AMP。
项目组各部门 follow 一致的规范
如果你提前看到了此文,那么情况可能并没有那么糟,因为你可以防范于未然,以下就是良方。
AMP 的设计原则有个很好的初衷:
「 These design principles are meant to guide the ongoing design and development of AMP. They should help us make internally consistent decisions. 」
(让我们做出一致的决定)
其中 “我们”,指的不仅是 AMP 页面前端开发者,而是参与这个项目的所有人,这个出发点非常重要,这也是 AMP 2019 CONF 「AMP core mindset」提到的主要观念。
产品需要了解 AMP 的限制,避免天马行空的需求设计;设计也需要了解 AMP 的组件规范,进而节约设计成本、产出可以用 AMP 技术实现出来的组件,最终避免开发成本的增加。例如, 图片轮播组件 AMP 自身已经实现的很好了「图 19」,设计如果没有特殊要求,不需要再设计新的风格。
「图 19:amp 轮播图组件 amp-carousel」
从开发层面来讲,前端在拿到设计稿后,就可以清楚的看出页面可以用到的 AMP 组件,对于后端同学来说,开发前期就知道该补什么字段、做哪些逻辑调整。
结束语
经过踩坑无数的坎坷历程,「Next.js + AMP + Preact 」这个 AMP 的新尝试在「Ficool m站」完美收工。尽管「Next.js + AMP + Preact」的组合在其他非 AMP 项目中几乎不会用到,但文中提到的每一个坑想必其他团队也很难回避,希望本文能让大家少走点弯路。
最后,团队的共识很重要,除了能节约沟通成本,更多的是大家都知道要做什么、为什么去做,我们是在实现同一个目标 - 更好的用户体验。