前端开发
在 Hyvä/Magento 2 中编写 CSP 兼容…
当需要为 Hyvä 主题创建 Alpine 交互组件、且代码要在标准 Alpine 与 Alpine CSP 构建中都能运行并避免 unsafe-eval 时使用。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 59/100 ×0.15
适用场景
在 Hyvä/Magento 2 中编写特定
解决思路
- 使用 CSP 兼容模式编写所有 Alpine 代码:即便当前使用标准 Alpine,也按 Alpine CSP 约束写,使其同时适用于标准构建和无 unsafe-eval 的 Alpine CSP 构建。
- 了解 Alpine CSP 是为去除 unsafe-eval 的专用构建,支付相关页面为满足 PCI-DSS 4.0 需要它(2025 年 4 月 1 日起强制)。
- 在 CSP 下属性读取保持原样:可以继续用 x-show 读取布尔属性等只读表达式。
- 在 CSP 下否定不要写 !open;改为定义方法,如 isNotOpen,并在模板中调用。
- 在 CSP 下状态变更不要直接赋值;改为方法,如模板绑定 close,方法内执行 this.open = false。
- 在 CSP 下方法参数不要内联写 setTab('info');改为模板绑定 setTab 并用 data-tab 传值,方法内读取 dataset。
- 在 CSP 下不要用 x-model;改用 :value 加 @input 的两向绑定替代模式。
- 在 CSP 下不要用范围迭代 x-for=“i in 10”;只迭代数组。
- 组件模板根元素用 x-data 指向构造函数名,如 x-data=initComponentName。
- 在 script 中定义命名全局函数 initComponentName,返回对象,包含 properties、init() 生命周期、状态读取方法、状态变更方法。
- 在 window.addEventListener('alpine:init', ...) 内调用 Alpine.data('initComponentName', initComponentName),把构造函数注册为 Alpine 组件。
- 注册时传 {once: true},防止重复注册。
- 每个 script 块后调用 $hyvaCsp->registerInlineScript()。
- PHP 值放入 JavaScript 字符串时用 $escaper->escapeJs()。
- PHP 值放入 data 属性时用 $escaper->escapeHtmlAttr(),不要用 escapeJs。
- 构造函数写成命名全局函数,不要内联在 Alpine.data() 回调中,以便其他模块或子主题代理和扩展。
- 组合多个对象时在构造函数内用展开语法,如 ...hyva.modal.call(this)、...hyva.formValidation(this.$el)。
- 组合 hyva 工具函数时用 .call(this) 把 Alpine 上下文传入被组合函数。
- 嵌套值属性用点号访问,如 item.is_visible、item.title。
- 需要在模板中做否定、条件或转换时,不在内联表达式里做,创建方法,如 isItemNotDeleted() 返回 !this.item.deleted,itemLabel() 返回 this.item.title || this.item.value。
- 简单布尔否定可用计算属性名简写:在返回对象中定义 ['!deleted']() { return !this.deleted; },模板用 !deleted 读取。
- 所有状态变更都提取到方法:模板写 @click=toggle,方法内写 this.open = !this.open,不写 @click=open = !open。
- 传方法参数用 dataset:模板写 @click=selectItem 和 data-item-id=...,方法内用 this.$el.dataset.itemId 读取。
- data 属性中的 PHP 值用 $escaper->escapeHtmlAttr() 转义。
- 方法内可访问 this.$event 读取事件,如 this.$event.target.value。
- 方法内可访问 x-for 循环变量,如 this.item、this.index。
- 替代 x-model 的文本输入:input 用 :value=username 和 @input=setUsername;方法 setUsername() 中 this.username = this.$event.target.value。
- 替代 x-model 的数字输入:方法内用 hyva.safeParseNumber(this.$event.target.value) 解析并赋给数值属性。
- 替代 x-model 的 textarea:textarea 用 @input=setComment 和 x-text=comment;方法内 this.comment = this.$event.target.value。
- 替代 x-model 的 checkbox:input type=checkbox 用 :checked=isSubscribed 和 @change=toggleSubscribed;方法内 this.isSubscribed = this.$event.target.checked。
- 替代 x-model 的 checkbox 数组:在 x-for 中渲染 checkbox,绑定 :value、:checked、@change、:data-option-id。
- checkbox 数组的选中判断方法 isOptionSelected() 返回 this.selectedOptions.includes(this.option.id)。
- checkbox 数组的切换方法 toggleOption() 读取 this.$el.dataset.optionId,若不存在则 push 到 this.selectedOptions,若存在则用 splice 移除。
- 替代 x-model 的 select:select 用 @change=setCountry;option 在 x-for 中绑定 :value、:selected、x-text。
- select 的选中判断方法 isCountrySelected() 返回 this.selectedCountry === this.country.code。
- select 的设置方法 setCountry() 执行 this.selectedCountry = this.$event.target.value。
- 基本 x-for 迭代用 template x-for=(product, index) in products 和 :key=index,内部用 x-text 等读取循环变量。
- 在 x-for 循环中使用方法:模板绑定 :class=getItemClasses、@click=goToProduct;方法内通过 this.index、this.product 访问循环变量并返回类对象或执行跳转。
- 可将方法作为 x-for 的值提供者:写 x-for=(item, index) in getFilteredItems,方法名不带括号;方法返回过滤后的数组,如 this.items.filter(...)。
- 记住 Alpine CSP 不支持 x-for=“i in 10” 这种范围迭代。
- 使用 hyva.getFormKey() 获取或生成 POST 请求的 form key。
- 使用 hyva.getUenc() 将当前 URL 做 base64 编码以便重定向。
- 使用 hyva.postForm({action, data, skipUenc}) 以编程方式提交 POST 表单。
- 使用 hyva.getCookie(name) 读取 cookie,注意其尊重 consent。
- 使用 hyva.setCookie(name, value, days, skipSetDomain) 设置 cookie。
- 使用 hyva.setSessionCookie(name, value, skipSetDomain) 设置 session cookie。
- 使用 hyva.formatPrice(value, showSign, options) 格式化货币。
- 使用 hyva.str(template, ...args) 做 %1、%2 占位符插值。
- 使用 hyva.strf(template, ...args) 做从 %0 开始的零基字符串插值。
- 使用 hyva.safeParseNumber(rawValue) 安全解析数字,替代 x-model.number。
- 使用 hyva.replaceDomElement(selector, content) 用 HTML 内容替换 DOM 元素。
- 使用 hyva.trapFocus(rootElement) 在元素内锁定焦点,常用于模态框。
- 使用 hyva.releaseFocus(rootElement) 释放焦点锁定。
- 使用 hyva.getBrowserStorage() 安全获取 localStorage/sessionStorage。
- 切换组件可用 hyva.createBooleanObject('open', false),会生成 open()、notOpen()、toggleOpen()、setOpenTrue()、setOpenFalse()。
- 使用 hyva.alpineInitialized(fn) 在 Alpine 初始化后运行回调。
- 监听自定义事件:根元素绑定 @private-content-loaded.window=onPrivateContentLoaded、@update-gallery.window=onGalleryUpdate。
- 在事件处理方法中用 this.$event.detail.data 或 this.$event.detail 获取事件数据。
- 派发事件:方法内用 this.$dispatch('update-qty-' + this.productId, this.qty) 派发自定义事件。
- 常见 Hyvä 事件包括 private-content-loaded、reload-customer-section-data、update-gallery、reset-gallery。
- 多个 window 或 document 事件监听器用 x-bind=eventListeners 模式:根元素写 x-bind=eventListeners。
- 构造函数返回 eventListeners 对象,键写成 ['@keydown.window.escape']、['@update-gallery.window']、['@keyup.arrow-right.window'] 等,值为处理函数。
- 动态类用 :class=containerClasses,方法返回类对象,如 { 'w-full h-full fixed top-0 left-0 bg-white z-50': this.fullscreen, 'relative': !this.fullscreen }。
- 通过 data 属性传 PHP 数据:根元素写 data-products=$escaper->escapeHtmlAttr(json_encode($products))、data-config=...。
- 在 init() 中解析 data 属性:this.products = JSON.parse(this.$root.dataset.products || '[]'),this.config = JSON.parse(this.$root.dataset.config || '{}')。
- 通过内联 JavaScript 传 PHP 数据:整数用 (int) 输出,字符串用 $escaper->escapeJs(),JSON 用 /* @noEscape */ json_encode($config)。
- 完整数量选择器示例:PHP 中声明 strict_types,引入 HyvaCsp、Escaper,设置 productId、minQty、maxQty、defaultQty。
- 数量选择器模板根元素 x-data=initQtySelector,label 用 sr-only,按钮绑定 @click=decrement 和 @click=increment,input 用 :value=qty、@input=onInput、min、max。
- 数量选择器构造函数返回 qty、minQty、maxQty、productId。
- onInput() 用 hyva.safeParseNumber 解析输入,按 minQty/maxQty 夹取,赋给 this.qty,并 this.$dispatch('update-qty-' + this.productId, this.qty)。
- increment() 在未超过 maxQty 时自增并派发事件;decrement() 在未低于 minQty 时自减并派发事件。
- isMinQty() 返回 this.qty <= this.minQty;isMaxQty() 返回 this.qty >= this.maxQty。
- decrementClasses() 和 incrementClasses() 返回类对象,如 { 'opacity-50 cursor-not-allowed': this.isMinQty() }。
- 最后在 alpine:init 中注册 Alpine.data('initQtySelector', initQtySelector),并调用 $hyvaCsp->registerInlineScript()。
- 参考 Hyvä CSP 文档 https://docs.hyva.io/hyva-themes/writing-code/csp/alpine-csp.html、Alpine.js 文档 https://alpinejs.dev/、示例组件 vendor/hyva-themes/magento2-default-theme-csp/、核心工具 vendor/hyva-themes/magento2-theme-module/src/view/frontend/templates/page/js/hyva.phtml。
适用边界 · 注意事项
- 项目不是 Magento 2 + Hyvä 主题,或没有 HyväCsp ViewModel / Alpine CSP 支持时。
- 无法修改模板并调用 $hyvaCsp->registerInlineScript(),或不能使用 Alpine.data() 注册组件时。
- 必须依赖 x-model 或 x-for 范围迭代且无法改写为 :value + @input 或数组迭代时;这些语法在 Alpine CSP 中不受支持。