Bricks 里的查询循环(query loop)确实强大又方便,但你也肯定不想把自定义的 WooCommerce 产品循环复制粘贴到每个页面里吧?有时候你用 Bricks 把 WooCommerce 设计得挺漂亮,可产品元素(Products element)还是套用 WooCommerce 默认样式,又得花时间去重新设计。
为什么不干脆做一个标准的产品循环模板,让它覆盖 WooCommerce 默认的产品循环呢?这篇教程就带你这么做。和往常一样,我不只给你最终能跑的代码,还会解释过程中可能遇到的各种问题——先泡杯咖啡,希望你喜欢。
⚠️ 注意:一旦你决定覆盖 WooCommerce 产品循环,部分 WooCommerce 钩子(hooks)就不能再用了。不过没关系——你只需要用 Bricks 模板来控制一切。
别吐槽我简陋的模板设计 :)
教程环境
- Bricks theme v1.5 beta(bricksbuilder.io)
- WordPress 6.0.1
- PHP 7.4
- Open LiteSpeed Server
- 教程里的所有自定义代码都放在子主题的
functions.php里,另外还有子主题的 woocommerce/content-product.php(后面会讲到)
第 1 步:创建自定义产品循环模板
这一步很简单——新建一个 section 类型的模板,按你的喜好设计。当然数据必须是动态的。比如你想显示产品价格,就用一个基础文本元素把内容设为 {woo_product_price};想显示图片就用图片元素、数据源选特色图片,等等(在小闪电图标里找合适的动态数据)。
我还把 div 设成了可点击链接,这样访客点击整个区块就能进入对应的产品详情页。
在构建器(builder)模式下内容没有填充也别担心,这是正常的——因为 Bricks 暂时还不知道怎么填充这些动态数据。
第 2 步:在子主题创建新的 PHP 文件
要替换 WooCommerce 默认的产品循环,直接用 WooCommerce 的模板覆盖机制就行(参考文档):
- 在子主题里新建一个 woocommerce 文件夹(你应该用子主题,可以从 Bricks Account 页面下载)
- 在 woocommerce 文件夹里新建文件 content-product.php
把下面的代码复制进新建的 content-product.php,记得保存:
<?php
defined( 'ABSPATH' ) || exit;
global $product;
// Ensure visibility.
if ( empty( $product ) || ! $product->is_visible() ) {
return;
}
// Replace 856 with your product loop template ID.
$template_id = 856;
// Just to check if the template exists
$template_data = \Bricks\Database::get_data( $template_id, 'content' );
$template_post_status = get_post_status( $template_id );
// Only use our template if the template is published and exists.
if ( $template_data && $template_post_status === 'publish' ) :
$ori['post_id'] = \Bricks\Database::$page_data['post_id'];
$ori['original_post_id']= \Bricks\Database::$page_data['original_post_id'];
$ori['preview_or_post_id'] = \Bricks\Database::$page_data['preview_or_post_id'];
do_action( 'itchy/template_output/before', $template_id, $product->ID, $ori );
// This is the product loop template.
echo do_shortcode( '[bricks_template id="' . $template_id . '"]' );
do_action( 'itchy/template_output/after', $template_id, $product->ID, $ori );
else:
// Below code is from WooCommerce default template
?>
<li <?php wc_product_class( '', $product ); ?>>
<?php
/**
* Hook: woocommerce_before_shop_loop_item.
*
* @hooked woocommerce_template_loop_product_link_open - 10
*/
do_action( 'woocommerce_before_shop_loop_item' );
/**
* Hook: woocommerce_before_shop_loop_item_title.
*
* @hooked woocommerce_show_product_loop_sale_flash - 10
* @hooked woocommerce_template_loop_product_thumbnail - 10
*/
do_action( 'woocommerce_before_shop_loop_item_title' );
/**
* Hook: woocommerce_shop_loop_item_title.
*
* @hooked woocommerce_template_loop_product_title - 10
*/
do_action( 'woocommerce_shop_loop_item_title' );
/**
* Hook: woocommerce_after_shop_loop_item_title.
*
* @hooked woocommerce_template_loop_rating - 5
* @hooked woocommerce_template_loop_price - 10
*/
do_action( 'woocommerce_after_shop_loop_item_title' );
/**
* Hook: woocommerce_after_shop_loop_item.
*
* @hooked woocommerce_template_loop_product_link_close - 5
* @hooked woocommerce_template_loop_add_to_cart - 10
*/
do_action( 'woocommerce_after_shop_loop_item' );
?>
</li>
<?php
endif;
?>
记得把 $template_id 换成你自己的模板 ID。现在先忽略那些 $ori 数组和 do_action,后面会解释。上面代码里的「主角」是 do_shortcode( '[bricks_template id="' . $template_id . '"]' )——这行代码让 Bricks 通过短代码渲染我们的模板。
去 WooCommerce 商城页(Shop page),或者任何用到产品循环的地方(如相关产品 Related Products、追加销售 Upsell 等)看看效果。
但是等等——为什么显示成这样?!Jenn,你在逗我吗?
Bricks 模板确实生效了,但图片、产品标题和价格呢?
我们来修好它!
第 3 步:理解问题所在
到底发生了什么?动态数据无法正确填充,是因为模板里的 Bricks 元素在执行动态函数时用错了 post ID。证据就是:标题元素渲染出来的是「Shop」而不是每个产品的标题——它把商城页当成了当前文章。
为了验证这一点,可以用 bricks/element/settings 过滤器钩子来测试。把下面的代码复制到 functions.php,把 $element->id 换成你自己的元素 ID:
add_filter( 'bricks/element/settings', function( $settings, $element ) {
// My heading element Id is cafvof, only target this element
if ( $element->id !== 'cafvof' ) return $settings;
// Let's check what $post_id is used for populating dynamic data here
var_dump( $element->post_id );
// Let's check what is the global $product ID too
global $product;
var_dump( $product->get_id() );
return $settings;
}, 10, 2);
看到了吧——产品循环里每个元素的 $element->post_id 都是 171,也就是我的商城页的 post ID。现在明白为什么不出产品特色图、不出产品价格了吧?因为商城页压根没有特色图,也没有产品价格!
这是 bug 吗?不是,Bricks 的行为是正确的。想象一下:你正在一个 post_id = 100 的页面里设计和用 Bricks 元素,那么后台执行动态数据时,这个页面里的每个元素当然都应该用 100 作为 post_id。当然,在查询循环、AJAX 或 REST 调用等场景下,Bricks 会自动动态改变 $element->post_id。
所以,我们需要写一段代码,让产品循环里的 $element->post_id 也动态改变。
第 4 步:动态改变元素的 Post ID
如果你读过我博客上的 Bricks Builder Useful Functions & Tips 一文,应该记得我提到过可以用 bricks/builder/data_post_id 来改变 Bricks 元素的 post ID,那篇文章里也介绍了其他改 post ID 的方法。
\Bricks\Database:$page_data 里关键的数组键值对是 post_id、original_post_id 和 preview_or_post_id。
我的计划是这样的:
- 渲染产品循环模板前,把原始数据存进临时变量
- 把
\Bricks\Database:$page_data里 post_id 相关的值改成$product->get_id() - 渲染产品循环模板——现在它会用 WooCommerce 产品 ID 作为元素的 post ID
- 单个循环渲染完后,把原始数据恢复到
\Bricks\Database:$page_data
你可能会问:为什么还要恢复原始数据?反正下一个产品循环也会覆盖它啊。我的回答很简单:每次离开前我都喜欢把自己的烂摊子收拾干净——而且我希望我的代码不会引发其他未知的副作用。
现在明白 content-product.php 里为什么有 $ori 数组和 do_action 了吧?
// Previous codes from child-theme/woocommerce/content-product.php
// Only use our template if the template is published and exists.
if ( $template_data && $template_post_status === 'publish' ) :
// Save some original data into $ori array
$ori['post_id'] = \Bricks\Database::$page_data['post_id'];
$ori['original_post_id']= \Bricks\Database::$page_data['original_post_id'];
$ori['preview_or_post_id'] = \Bricks\Database::$page_data['preview_or_post_id'];
// Create a custom action hook and so I can use in functions.php
// I may use this same action hook in other woocommerce template in future too!
do_action( 'itchy/template_output/before', $template_id, $product->ID, $ori );
// This is the product loop template.
echo do_shortcode( '[bricks_template id="' . $template_id . '"]' );
// Another custom action hook to some cleanup
do_action( 'itchy/template_output/after', $template_id, $product->ID, $ori );
else:
对,加了些注释,希望你能看懂。
好了,现在在 functions.php 里挂上我们的自定义动作钩子:
// 2) Change the \Bricks\Database:$page_data post_id related values to $product->get_id()
add_action( 'itchy/template_output/before', 'itchy_change_data_post_id', 10, 3 );
function itchy_change_data_post_id( $template_id, $id, $ori ) {
// Only target my product loop template, in future you might have more different templates to be hooked into
if ( $template_id !== 856 ) return;
// Use filter hook to change the $post_id used for populating dynamic data
// I use anonymous function to do this as it is easier to pass the $id and $ori to the function
// I use $id from content-product.php, which is the product ID
// I use priority 34 to make sure it is unique, this is a little tricky, I will use 34 priority to remove my filter hook later
add_filter( 'bricks/builder/data_post_id', function( $element_post_id ) use ( $id ) {
return $id;
}, 34 );
// Different elements use different way to set the post_id
// I feel more safe to manually set product ID here too, sorry if I am wrong, because there are many different logic in Bricks codebase
// Without doing this, some dynamic data will not be populated correctly too
\Bricks\Database::$page_data['original_post_id'] = $id;
\Bricks\Database::$page_data['preview_or_post_id'] = $id;
\Bricks\Database::$page_data['post_id'] = $id;
}
// 4) Once finished 1 single loop, restore the original data back to \Bricks\Database:$page_data
add_action( 'itchy/template_output/after', 'itchy_revert_data_post_id', 10, 3 );
function itchy_revert_data_post_id( $template_id, $id, $ori ) {
// Only target my product loop template, in future you might have more different templates to be hooked into
if ( $template_id !== 856 ) return;
// I want to remove my anonymous filter function
global $wp_filter;
// callbacks[34] is the anonymous function I added above, use unset to remove the filter
if ( isset( $wp_filter[ 'bricks/builder/data_post_id' ]->callbacks[34] ) ) {
unset( $wp_filter[ 'bricks/builder/data_post_id' ]->callbacks[34] );
}
// I feel more safe to manually set original values here
\Bricks\Database::$page_data['original_post_id'] = $ori['original_post_id'];
\Bricks\Database::$page_data['preview_or_post_id'] = $ori['preview_or_post_id'];
\Bricks\Database::$page_data['post_id'] = $ori['post_id'];
}
请仔细读一遍我写的注释。
去前端看看吧。铛铛铛~!
不过,如果你再看 $element->post_id,会发现它显示的是空字符串。老实说,我也不明白为什么显示空字符串,希望有人能给我解释一下 :)
如何禁用我的产品循环模板?
把模板状态改成 draft(草稿)即可,这样就会重新用回 WooCommerce 默认的产品循环。或者直接把 content-product.php 删除或改名,比如改成 backup_content-product.php。
为什么 Bricks Products 元素不用我的自定义产品循环模板?
如果你发现 products 元素没有应用自定义产品循环模板,请检查并确保 Bricks products 元素里的字段都是空的。这样它才会回退到 WooCommerce 模板,从而触发我们的模板覆盖机制。
结论
Bricks 是一个极其灵活强大的主题,只要你会一些 tweak 和自定义代码,绝大多数功能都能实现!感谢 Bricks 团队的努力,以后我也很想好好读读、理解他们的代码,写出更多教程。
⚠️ 生产环境务必删除第 3 步里的调试代码!
⚠️ 记得把产品循环模板做成响应式的——建议用 div,并设置 align-self: stretch。