AICon议程上新60%,阿里国际、360智脑、科大讯飞、蔚来汽车分享大模型探索与实践 了解详情
写点什么

我们从 Vue 到 Alpine.js 的旅程

作者: Tim Kleyersburg

  • 2022-12-05
    北京
  • 本文字数:5111 字

    阅读完需:约 17 分钟

我们从Vue到Alpine.js的旅程

问题


在 2019 年底,我们为一位客户重发布了电子商务网站。这次重新发布的变动很大,不仅影响了整体设计和模板,还涉及了前端的架构。唯一没什么改动的就是后端。


客户的主要需求是:


  • 优化 PageSpeed 指标

  • 提高可用性,从而提高转化率


在数月的实施之后,客户和我们都对最终成果感到满意。我们在 Lighthouse 的全部四个类别中都达到了绿色评级,转化率也有了显著的提升。直到谷歌在 Lighthouse 6.0 更新中更改了性能评分的计算模式,让我们的评分从绿色降级为红色。


顺带一提,Lighthouse 在新标准中将重点转移到了前端内容上,在首字节时间(TTFB)以及如文件大小、CSS 优化、网页字体等会对总体网页性能有影响的内容之外,还囊括了“可交互时间(TTI)”以及“最大内容绘制(LCP)”指标。随着网页可交互性越来越强,其对性能的感知也越发重要。理论上来说,我们是支持谷歌将这些新指标纳入评分标准的,尽管谷歌在展示“优秀范例”时用的是几乎没有任何交互性的博客站点,这完全是在拿苹果和橘子在作比较。


在与客户的一次会议后,我们延后了针对 Lighthouse 新指标的优化工作。在分析了网站访客的常用设备后,我们很难再说服自己将大量时间花费在我们和所有竞争对手都要面临的问题上。


然而,随着在 2020 年底、2021 年初谷歌公布部分新指标将对搜索结果排名有影响后(是时候将页面体验引入谷歌搜索了),显然我们并不能再继续将这个问题推延了。在与客户的又一次商讨后,我们确定了我们所能提供显著竞争优势,并让最终用户感受到速度的提升。


分析过程


我们需要更多的数据。坦白来说,在这之前我们从来没怎么重视过更深层次的性能指标,而现在我们要开始赶进度了。我们通过谷歌 Chrome 浏览器和其内置的 Lighthouse 应用,外加开发者工具中的性能标签,三管齐下分析网站性能。


我们当前的设置


在重发布后,我们将前端架构完全推翻重写,用 Vue 2 作为 JavaScript 框架,TailwindCSS 为 CSS 框架。所有内容都由 Symfony Encore(Webpack)进行打包。


我们的站点没有用 SPA,而是将根实例捆绑到一个 div 元素 #app 上。借助无渲染组件(Vue.js 中的无渲染组件)让我们可以使用服务器端变量或是用 Twig 轻松编写大部分模板,而不需要编写任何 API。


<notepad-star  :product-id="{{ product_id }}"  :initial-star="{{ is_stared(product_id) ? 'true' : 'false' }}">  <div>    <button @click.prevent="toggle">Toggle</button>  </div></notepad-star>
复制代码


product_id 是服务器端变量,is_stared(product_id)是 Twig 函数,二者都是作为 props 传入 Vue 组件的。


问题分析


目前我们的流程大致是这样子的:


  1. 在 chrome 里生成性能报告

  2. 研究报告结果

  3. 改点东西

  4. 重新生成新报告以确定或者推翻我们假设


性能报告中最有用的部分是“评估脚本”,似乎浏览器在评估我们 JavaScript 包的时候要做不少事。



生产环境


我们的第一步是注释掉脚本标签,看看对指标会有什么影响。结果发现,效果相当显著。



注:这份报告是我们在开发环境中生成的,与实际生产环境大约有 10%-15% 的差异。


需要做什么


我们确定了以下几点亟需关注:


  1. 优化关键资源的预加载

  2. 最大限度地缩减阻断时间

  3. 优化交互时间

  4. 最大限度地减少主线程工作


Part 1:优化预加载


为追求简单快速简单的部署,我们没有对谷歌标签管理和我们的 CCM 进行完善的性能测试,这也导致了一些渲染阻塞。我们测试了预加载和预连接的各种不同组合,并最终得出了以下结果:


  • 预加载关键资源,如 CCM 脚本

  • 预连接 GTM

  • 预加载我们自己的关键资源,如网页字体或我们自己的主要 css、js 文件


这些是我们用到的工具:


  • Lighthouse:直观展示哪些资源应该被预加载

  • Firefox:通过开发者工具可以找到被预加载,但在最初几秒内未被使用的字体


Part 2-4:优化其余部分


在优化预加载后,剩下可能对我们关键指标有影响的就只有我们 JavaScript 包中自己的资源了,其余指标也都或多或少跟这些资源挂钩。


找到问题


在开始优化之前,我们需要先从更深层次分析问题。如前文所述,我们对所有的 Vue 组件都应用了无渲染组件,并用 Vue 实例打包了整个网站。这种方式让我们可以很方便地进行全局状态管理,我们还可以通过添加额外的混合器来为网站增加交互性,比如:


export const searchOverlay = {  data() {    return {      showSearchOverlay: false,    }  },}
复制代码


全局状态示例 / 混合器提供功能性


Vue 的不同版本


Vue 有两个不同的版本:运行时构建,以及包含模板编译器的版本。运行时构建的文件大小相比来说要小很多,但只能用于单一文件的组件,因为这类组件会被包含在捆绑包中,因此不需要模板编译器。另一方面,模板编译器让我们可以从模板引擎(Twig)中生成模板,并插入到无渲染组件的默认槽中。


另外,由于我们需要将网站整体打包,Vue 需要对所有可见的 DOM 节点进行评估,而光是在主页上就有大约 4500 个节点。这也是为什么我们的脚本评估时间会是如此的长。


既然对根因有了更好的理解,我们可以开始着手评估问题缓解的方法了。


很可惜我们最终并没有找到能显著提升当前架构性能的方法,我们的模板架构和后端结构并不允许我们优雅地切换到运行时构建。


评估需求


下一步,我们开始整理当前网页上所提供的组件和交互功能,以从我们全新的解决方案中获得新的视角。


这些是我们目前已有组件的例子:


  • 实时搜索

  • 动态侧边栏导航

  • 弹出框菜单

  • 模态框


我们还有一些之前由混合器提供的小型函数。这些函数因为没有状态且可以简单直接地在任何地方触发,主要用于不需要单独组件即可实现的功能,如:


  • 动态更新产品类别

  • 打开发货模式

  • 展示或隐藏全局信息轮播图


这些功能都有一个共同点:需要组件间的交流。这些组件都不算复杂,主要用于提供互动性或防止网页重新加载。


我们希望且需要从新框架中获得的有:


  • 反应性,在数据发生变化后模板会重新渲染

  • 事件系统以方便组件间交流

  • 占用空间小


引入 Alpine.js


我们曾在其他项目中用 Alpine.js 来提供交互性,最终效果也很好。既然我们已经在项目中使用 TailwindCSS 了,Alpine.js 所声称的“类似 JavaScript 中的 TailwindCSS”说法很得我们心。我们并不确定 Alpine.js 是否能胜任如此大型的电子商务站点,因此我们需要建立一个概念验证,以测试它是否最难处理的部分。我们重新构建了如滑动导航、动态购物车以及主菜单等包含前文所提到需求的重要组件,如果我们能重新整合这些组件,那我们可以肯定地认为其他组件都没问题。在经过了大约一天左右的工作,我们收获了满意的成果。虽然重构过程并不是一帆风顺,但既然我们的大部分逻辑都是用 JavaScript 写的,从 Vue 到 Alpine.js 的转换都是很直接的。我们最终确定了以下的架构形式:


js/├── components/│   ├── cart.js│   ├── mobileMenu.js│   └── ...├── enums/│   ├── events.js│   └── ...├── helper/│   └── customEvent.js├── providers/│   ├── cart.js│   ├── googleTagManager.js│   └── ...└── stores/    ├── cart.js    ├── global.js    └── ...
复制代码


组件


组件是以窗口范围的函数所定义的,可以返回用于在 Alpines 的 x-data 属性中用于初始化组件的对象。


下面是一个简化的模态组件示例,请注意我们是怎么使用 customEvent 函数和“枚举(enums )”的。


import customEvent from '@/helper/customEvent'import { MODAL_OPEN, MODAL_OPENED, MODAL_CLOSE } from '@/enums/events'
window.modal = () => ({ open: false, init() { if (this.instantDisplay !== undefined) { this.open = true } }, close() { this.open = false customEvent(MODAL_CLOSE, this.name) }, wrapper: { async [`@${MODAL_OPEN}.window`](e) { if (modalToOpen !== e.payload.name) { return }
customEvent(MODAL_OPENED, this.name) this.open = true }, },})
复制代码


enum


并不是指实际意义上的枚举,只是个用来保存常量的辅助文件,方便我们在整体代码库中使用这些常量,而不用担心事件在重命名时会连锁搞崩掉别的东西。


const MODAL_OPEN = 'modal-open'const MODAL_OPENED = 'modal-opened'const MODAL_CLOSE = 'modal-close'
复制代码


helper


我们可以在任何地方导入 helper 函数且不会保留任何状态。这个是我们的 customEvent helper 函数:


export default function (name, payload = null, originalEvent = null) {  // 入参对象应包含以下:  // name: 'string',  // payload: 'object'  // originalEvent: 'this',或者其他你需要点击的目标
const customEvent = new CustomEvent(name, { detail: { payload: payload, originalEvent: originalEvent, }, })
window.dispatchEvent(customEvent)}
复制代码


这个简单的 helper 给我们带来极大的灵活性,让我们摆脱了定义无数个 Alpine 组件的烦恼,在包括 HTML 中等任何地方直接调用。其本质也不过是标准 CustomEvent API 的一部分,改造成可在窗口范围内使用的函数且能接收 onclick 属性入参。


<button type="button" onclick="customEvent('name', 'payload')"></button>
复制代码


内容提供器


内容提供器通过可复用功能提供数据,可以把它看作是客户端的 API 层。和 helper 函数一样,这些函数不应包含任何状态,且可被组件消耗的。


下面是实时搜索的内容提供器大致代码:


import customEvent from '@/helper/customEvent'import { SEARCH_GET } from '@/enums/events'
async function getResultFor(searchTerm) { let result = undefined
await fetch(`/search?q=${searchTerm}`) .then((response) => response.json()) .then((data) => { result = data })
customEvent(SEARCH_GET, result)
return result}
export { getResultFor }
复制代码


store


既然我们 JavaScript 框架选择依赖 Alpine.js 2.8,那么选择 Spruce 做全局状态管理也很合理。网站的每个部分都有一个 store,以下几行代码是我们用于管理大型菜单状态的:


Spruce.store('megamenu', {  activeId: null,  toggle(id) {    if (id === this.activeId) {      this.activeId = null      return    }    this.activeId = id  },})
复制代码


新旧指标的对比


在确定架构并顺利实施最复杂的组件后,我们很自信我们前进的道路一定是正确的。性能标准测试结果也很好,大部分的性能分类都有了 15-20 百分点的提升。


我们迫不及待地想实现所有组件以获得完整的指标结果,每次点击 Lighthouse 标签中“生成报告”按钮,都会让我们的心跳加速。如果不包含脚本的话,预计我们的网站是不可能达到 56 的评分,但这是我们现在的结果:



再次声明,这只是我们的开发环境,因此很多图中的“机会”并不适用于实际生产环境。


这次的结果让我们颇为满意,在最后的几项测试,并对代码进行清理后,我们开始准备下周一的版本发布。


上午 8 点 24 分,我们点下了“合并”按钮。在这之前我们进行了发布前的最后一次 Lighthouse 测试,性能评分当时下降到了 28,具体是什么原因造成的这次 10 分左右的下降我们并不清楚。部署工作顺利进行,网站运行正常,于是我们又进行了一次 Lighthouse 测试。这是测试结果:



在上线之后我们发现了一些小问题,在及时修复后我们成功将评分打上 62 分,真是太刺激了!当然,这并不会是我们旅途的终点,我们仅仅是为后续进一步改善用户界面体验打下了良好的基础。


荣誉提名:Debugbear


在这次重新部署中,我们需要一个能对指标进行监控的工具。在研究通过 CI/CD 管道、手动测试或是通过 Lighthouse 节点 CLI 运行脚本时,我们偶然发现了 Debugbear。


Debugbear 的服务可以检测网站的核心状态、运行 Lighthouse 测试并将测试结果与竞争对手或历史结果进行对比,从而提供对两次测试结果的深层次解读。它不仅帮我们更好地了解问题根因,还提升了我们对优化工作的信心。


可以说,Debugbear 的性价比非常高,再加上 Matt 人真的很好,当时我们的信用卡除了问题,他非常慷慨地延长了我们的试用期,让我们安心测试而不用担心最后期限。


写在最后


以上基本就是我们旅程的第一阶段了。


最终的成果让我们对自己的决策充满了信心。Vue 并不适合我们的项目,老实说,当初选择 Vue 或许是因为它看起来不错,但它从来不是我们最好的选择。错处不在 Vue,Vue 是个很强的框架,我们也还在继续使用它,但现在我们有了另一个比 Vue 更合适的工具。


希望这篇文章能帮上你!如果有任何问题,欢迎在推特上联系我😊。


原文链接:

https://www.tim-kleyersburg.de/articles/from-vue-to-alpinejs


相关阅读:

Vue涉及国家安全漏洞?尤雨溪回应:前端框架没有渗透功能

尤雨溪:Vue 3 将成为新的默认版本

2022-12-05 19:1914658

评论

发布
暂无评论
发现更多内容

架构师第 7 课作业及学习总结

小诗

「架构师训练营第 1 期」

接私活必备的 6 个开源项目

GitHub指北

第二周作业

Geek_mewu4t

依赖倒置与接口隔离原则

玄月

大数据计算引擎Spark

积极&丧

人人都在谈的数字化转型,区块链技术能扮演何种角色?

CECBC

区块链

大作业:知识点图谱

paul

JVM 垃圾回收机制分析

Andy

数据应用总结(一)

Mars

架构师第 9 课作业及学习总结

小诗

「架构师训练营第 1 期」

架构师第 12 课作业及学习总结

小诗

Dubbo微服务调用时序图

Andy

「架构师训练营 4 期」 第二周 - 0201

凯迪

福田区实现数字人民币六个100%,农行推出ATM机存取现功能

CECBC

数字红包

架构师第 8 课作业及学习总结

小诗

「架构师训练营第 1 期」

重学JS | this的指向问题

梁龙先森

大前端 编程语言 28天写作

架构师第 10 课作业及学习总结

小诗

大作业一

饭桶

第 12 周作业

Steven

生命唯愿,爱与自由

废材姑娘

个人感悟

Prometheus官方文档【查询篇-运算符】

卓丁

Prometheus Monitor 监控告警 普罗米修斯 PromQL

架构师第 11 课作业及学习总结

小诗

「架构师训练营第 1 期」

【HTML】全局属性:accesskey

德育处主任

html html5 大前端 快捷键 28天写作

Windows安装Mysql

千泷

Python 100 天从新手到大师

GitHub指北

大作业二

饭桶

架构师训练营大作业

Cheer

MapReduce函数分析

Mars

数字人民币为何频频入榜金融机构“工作单”

CECBC

数字人民币

大作业二

「架构师训练营第 1 期」

架构师第 13 课作业及学习总结

小诗

「架构师训练营第 1 期」

我们从Vue到Alpine.js的旅程_大前端_InfoQ精选文章