资讯动态

Shopify退回原生:Liquid直出加原生JS,告别写两遍的维护噩梦

发布时间:2026/9/18 3:12:00 来源:尧图企业网站定制
上个月接了个Shopify店铺优化需求客户想做商品列表页的分类筛选和排序要求首屏快、Google能收录到每一个筛选结果移动端还不能卡。我按老思路在Liquid模板里写了一遍服务端渲染又在客户端用React重新实现了一遍交互逻辑——干到一半我就觉得不对劲等于同样一件事写了两遍而后面的维护还得同步改两套代码。就在我纠结要不要继续堆框架的时候圈子里的风向变了越来越多人开始讨论“退回原生”用Liquid直出加原生JavaScript来构建店铺交互。我试着用这套思路把项目重做了一遍发现“写两遍”这件事的性价比和成本结构跟我原来想的完全不一样。这篇文章想聊聊我观察到的Shopify生态信号、为什么回归原生后“写两遍”不再等于两倍工作量以及我在实操中踩过的坑和总结出的方法。适合被店铺逻辑折磨的开发者、想减少维护成本的建站团队以及正在纠结要不要跟风上框架的人。1. 事情的起点Shopify 店铺开发里那个让人崩溃的“写两遍”1.1 为什么模板渲染和交互脚本天生就是两套逻辑电商页面的要求很拧巴一边要服务端直接输出HTML确保爬虫能抓到内容、首屏不用等JS跑完另一边又要有流畅的交互筛选、排序、加购这些操作不能整个页面刷新。这两个诉求天然会把开发逼成两段式——Liquid负责服务端吐页面JavaScript框架负责客户端做交互。问题是很多团队在第二步时习惯性上重量级方案。我在之前一个项目里就是典型做法Liquid先渲染出初始商品列表然后引入React把商品数据JSON序列化塞进>!-- sections/products.liquid -- div idfilter-root >// filter-react.js import { createRoot } from react-dom/client; import { useMemo, useState } from react; const rootEl document.getElementById(filter-root); const rawProducts JSON.parse(rootEl.dataset.products); function ProductList({ items }) { const [sort, setSort] useState(manual); const sorted useMemo(() sortItems(items, sort), [items, sort]); return ( ul classNameproduct-list {sorted.map((product) ( li key{product.id} classNameproduct-card img src{product.image} alt{product.title} / h3{product.title}/h3 p{product.price}/p /li ))} /ul ); } createRoot(rootEl).render(ProductList items{rawProducts} /);这个模式的问题很明显商品数据先被Liquid序列化成JSON再被React读取解析然后又重新生成一遍DOM。模板逻辑在Liquid里写了一遍在JSX里又写了一遍。以后改卡片结构两处都要动数据有差异还会出现两边渲染结果不一致。再看原生路线的实现。Liquid模板照常输出完整HTMLJS只负责增强!-- sections/products.liquid -- {% paginate collection.products by 24 %} form classfilter-bar idfilter-form select idsort-select namesort_by {% for option in collection.sort_options %} option value{{ option.value }} {% if option.value collection.sort_by %}selected{% endif %} {{ option.name }} /option {% endfor %} /select /form ul classproduct-list idproduct-list {% for product in collection.products %} li classproduct-card>// filter-native.js const select document.getElementById(sort-select); const list document.getElementById(product-list); // 用一个 token 做请求竞态控制防止快速切换时旧请求覆盖新结果 let requestToken 0; select.addEventListener(change, async () { const url new URL(window.location.href); url.searchParams.set(sort_by, select.value); const currentToken requestToken; const res await fetch(url.pathname url.search, { headers: { X-Requested-With: XMLHttpRequest } }); if (currentToken ! requestToken) return; const html await res.text(); const doc new DOMParser().parseFromString(html, text/html); const newList doc.getElementById(product-list); if (newList) { list.replaceWith(newList.cloneNode(true)); } history.replaceState({}, , url); });这段原生JS做的事情很清楚拿到新URL、请求HTML、解析出新的列表节点、替换旧节点、同步地址栏。没有虚拟DOM没有组件树没有独立的数据层。连续的HTML片段请求靠一个递增token就能避免最常见UI竞态问题。在原方案里两种技术都要会Liquid一套、React一套新方案基本只需要你理解HTML在服务端如何生成、DOM在浏览器里如何操作。从这个角度看“写两遍”还写不写写。服务端写一遍客户端写一遍但两遍操作的对象是同一个页面、同一套HTML语义维护成本选项卡然下降。3.3 从“两遍”变成“一层模板加一层增强”的成本账有人会问原生方案里Liquid模板和JS脚本还是有各自的代码遇到复杂交互是不是还是得写两遍我实测下来的体会是只要把Liquid当唯一的数据和结构来源JS只做DOM增强就不存在“两遍”的扯皮。用表格对比一下两条路线的综合成本对比维度Liquid 重量级框架Liquid 原生JS首屏渲染需等待JS执行后才完善服务端直出立即展示SEO友好度依赖JS渲染配置有风险天然友好状态同步需要维护两套状态URL和DOM就是唯一状态团队技能要求需要熟悉框架体系熟悉浏览器API即可排查问题成本要跨框架与模板排查原生代码直接可调试长期维护成本随业务迭代指数上升稳定接近线性构建部署需要打包器、依赖管理静态JS文件即可我在实际项目里做了一个粗略统计同样一个店铺筛选功能旧方案涉及改动16个文件包括React组件、工具函数、主题配置原生方案只改了8个文件其中还包含大量Liquid模板本身的结构调整。后续两个月迭代中旧方案几乎每次改版都要同时动两处原生方案大部分时候只改一个Liquid模板或者只调一段JS两边同时改的机会少了很多。这大概就是“写两遍不再等于两倍工作量”最直观的体现。4. 原生化改造的正确姿势与常见坑4.1 从现有主题改到原生工作流的五个步骤如果你已经有一个跑在框架方案上的Shopify主题别一口气全推翻按下面这套流程渐进式改造会更稳妥。第一步盘点和划界。先列出当前主题里的关键交互点哪些是纯展示、哪些需要客户端逻辑。把Liquid输出和JS增强的边界画出来明确“服务端负责数据输出客户端负责交互增强”的原则避免再出现两套渲染。第二步去掉中间层依赖。看看当前主题里引用了哪些框架和工具库逐步用原生API替换。比如jQuery的$.ajax换成Fetch$(selector)换成querySelector动画用CSS Transition和IntersectionObserver配合实现。一次替换一个模块每次替换后测试一遍主要流程。第三步用组件化思想组织原生代码但别急着引入组件框架。可以先按页面区块拆成多个独立的JS模块使用>// mini-store.js const store new Proxy({ sort_by: , tags: [] }, { set(target, key, value) { target[key] value; document.dispatchEvent(new CustomEvent(store:change, { detail: { key, value } })); return true; } });第三个坑是直接替换DOM导致列表闪烁。用innerHTML整块替换商品列表时图片会重新加载页面高度还会跳一下移动端感受特别糟。解决方法是先用DocumentFragment组装新节点一次性替换同时给图片容器设置固定宽高比并在请求期间保留旧列表占位这样视觉上几乎无感知。4.3 常见问题速查表问题常见原因解决思路筛选后地址栏没变漏掉history.replaceState统一在更新DOM后同步URL参数快速切换时列表被旧请求覆盖缺少请求竞态控制用递增token忽略过期响应Liquid模板和JS插值符号冲突模板字符串里混用{{ }}JS侧只使用${}数据用>

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价