我最近参加了在德克萨斯州奥斯汀举行的 ARTIFACT 会议,并从一些关于通过网站性能角度进行可访问性的演讲中获得灵感。我发现,人们倾向于依赖大型 JavaScript 框架来处理工作,比如 React、Vue 和 Angular,但在某些情况下这可能过于复杂,从而对网站性能(以及可访问性)造成负面影响。同时,这些框架可以使开发人员更容易更高效地进行开发。从这次会议中,我最大的收获是了解如何平衡快速、高效的体验和我的开发流程。
在会议结束后几天,我正在为一个项目构建列表过滤功能时,一直在思考这个问题。这是一个非常标准的功能:我需要一个帖子列表和一些类别过滤功能。我当时使用 CraftCMS 来进行前端路由和模板,以及一些 Vue 组件来增强 JavaScript 功能。这不是一个完整的“单页面应用程序”,更像是少量 Vue 代码的加入。
通常的做法是:
- 使用 Craft/Twig 渲染一个带有空 div 的页面
- 将 Vue 组件挂载到该 div 上
- 从 Vue 组件发出 Ajax 请求到 API 获取帖子数据(JSON 格式)
- 渲染帖子并将过滤功能绑定到帖子。
由于帖子在 Vue 中以数组形式保存,因此 动态列表渲染 是一项非常简单直接的任务。
简单,完成了吧?嗯…… 额外的 Ajax 请求意味着用户在初始加载时不会看到任何内容,具体取决于用户的网络状况,这可能需要一些时间。我们可以添加一个 加载指示器,但也许我们可以做得更好?
最好是,帖子在 CMS 的初始页面请求时被渲染。
但如何将静态 HTML 与 Vue 联系起来以实现过滤功能呢?
在后退一步重新思考问题后,我意识到可以使用 v-if
指令以及来自 Twig 的内联 JavaScript(“循环内”)来实现相同的效果。下面,我将展示我的操作步骤。
我的原始项目使用 CraftCMS,但我将在下面的演示中使用 WordPress。这只是一个概念,可以应用于 CraftCMS/Twig 或任何其他 CMS/模板引擎组合。
首先我们需要一个过滤 UI,这通常会放在归档模板中循环的开头。
<?php $terms = get_terms( [
'taxonomy' => 'post_tag', // I used tags in this example, but any taxonomy would do
'hide_empty' => true,
'fields' => 'names'
] );
if(!empty($terms)): ?>
<div>
Filter:
<ul class="filters">
<li class="filters__item"><button class="filters__button" :class="{'filters__button--active': tag === ''}" @click="tag = ''">All</button></li>
<?php foreach($terms as $term): ?>
<li class="filters__item">
<button class="filters__button" :class="{'filters__button--active': tag === '<?php echo $term; ?>'}" @click="tag = '<?php echo $term; ?>'"><?php echo $term; ?></button>
</li>
<?php endforeach; ?>
</ul>
<p aria-live="polite">Showing posts tagged {{ tag ? tag : 'all' }}.</p>
</div>
<?php endif; ?>
根据代码,我们使用 get_terms()
从 WordPress 获取一些标签,并在 foreach
循环中输出。你会注意到每个标签的按钮都有一些我们稍后会用到的 Vue 指令。
我们已经有了循环!
<div class="posts">
<?php
// Start the Loop.
while ( have_posts() ) : the_post();
<article id="post-<?php the_ID(); ?>"
<?php post_class(); ?>
v-if='<?php echo json_encode(wp_get_post_tags(get_the_ID(), ['fields' => 'names'])); ?>.includes(tag) || tag === ""'
>
<header class="entry-header">
<h2><?php the_title(); ?></h2>
</header>
<div class="entry-content">
<?php the_excerpt(); ?>
</div>
</article>
// End the loop.
endwhile; ?>
</div>
这是一个非常标准的 WordPress 帖子循环。你会注意到一些 Vue 指令,它们使用了 PHP 输出 CMS 内容。
除了某些样式外,剩下的就是 Vue “应用程序”。你准备好了吗?它就在这里
new Vue({
el: '#filterablePosts',
data: {
'tag': ''
}
});
是的,真的,这在 JavaScript 文件中就足够让它正常工作了!
所以,这里发生了什么?
好吧,我们没有将 JSON 格式的帖子数组传递给 Vue,而是在初始页面加载时使用 WordPress 输出帖子。诀窍是使用 PHP 在 Vue 指令中输出所需内容:v-if
和 :class
。
过滤按钮上发生的是一个 onclick
事件处理程序(@click
),它将 Vue 变量“tag”设置为 WordPress 帖子标签的值。
@click="tag = '<?php echo $term; ?>'"
此外,如果该 Vue 变量等于按钮的值(在 :class
指令中),则会为按钮添加一个 active 类。这只是为了样式。
:class="{'filters__button--active': tag === '<?php echo $term; ?>'}"
对于文章列表,我们根据 Vue “tag” 变量的值来有条件地显示它们。
v-if='<?php echo json_encode(wp_get_post_tags(get_the_ID(), ['fields' => 'names'])); ?>.includes(tag) || tag === ""'
PHP 函数 json_encode
允许我们将帖子标签数组作为 JavaScript 输出,这意味着我们可以使用 .includes()
来查看 Vue “tag” 变量是否在该数组中。如果未选择任何标签,我们也希望显示该文章。
这里我们将其整合在一起,使用 Twenty Nineteen 主题 模板 archive.php
作为基础
<?php get_header(); ?>
<section id="primary" class="content-area">
<main id="main" class="site-main">
<?php if ( have_posts() ) : ?>
<header class="page-header">
<?php the_archive_title( '<h1 class="page-title">', '</h1>' ); ?>
</header>
<div class="postArchive" id="filterablePosts">
<?php $terms = get_terms( [
'taxonomy' => 'post_tag',
'hide_empty' => true,
'fields' => 'names'
] );
if(!empty($terms)): ?>
<div class="postArchive__filters">
Filter:
<ul class="postArchive__filterList filters">
<li class="filters__item"><button class="filters__button" :class="{'filters__button--active': tag === ''}" @click="tag = ''" aria-controls="postArchive__posts">All</button></li>
<?php foreach($terms as $term): ?>
<li class="filters__item">
<button class="filters__button" :class="{'filters__button--active': tag === '<?php echo $term; ?>'}" @click="tag = '<?php echo $term; ?>'" aria-controls="postArchive__posts"><?php echo $term; ?></button>
</li>
<?php endforeach; ?>
</ul>
<p aria-live="polite">Showing {{ postCount }} posts tagged {{ tag ? tag : 'all' }}.</p>
</div>
<?php endif; ?>
<div class="postArchive__posts">
<?php
// Start the Loop.
while ( have_posts() ) : the_post(); ?>
<article
id="post-<?php the_ID(); ?>"
<?php post_class(); ?>
v-if='<?php echo json_encode(wp_get_post_tags(get_the_ID(), ['fields' => 'names'])); ?>.includes(tag) || tag === ""'
>
<header class="entry-header">
<h2><?php the_title(); ?></h2>
</header>
<div class="entry-content">
<?php the_excerpt(); ?>
</div>
</article>
<?php endwhile; // End the loop. ?>
</div>
</div>
<?php
// If no content, include the "No posts found" template.
else :
get_template_part( 'template-parts/content/content', 'none' );
endif; ?>
</main>
</section>
<?php
get_footer();
这是一个在 CodePen 上的示例
查看 CodePen 上的
使用服务器端数据获取的 Vue 动态列表过滤 by Dan Brellis (@danbrellis)
在 CodePen 上。
奖励时间!
你可能已经注意到,我在过滤按钮列表下方添加了一个 aria-live="polite"
通知器,以 让辅助技术用户知道 内容已发生变化。
<p aria-live="polite">Showing {{ postCount }} posts tagged {{ tag ? tag : 'all' }}.</p>
要获得 postCount
Vue 变量,我们需要在 Vue 组件中添加一些额外的 JavaScript 代码
new Vue({
el: '#filterablePosts',
data: {
'tag': '',
'postCount': ''
},
methods: {
getCount: function(){
let posts = this.$el.getElementsByTagName('article');
return posts.length;
}
},
beforeMount: function(){
this.postCount = this.getCount();
},
updated: function(){
this.postCount = this.getCount();
}
});</p>
新方法 getCount
用于选择组件 div 中的文章元素并返回长度。在 Vue 组件挂载之前,我们获取该计数并将其添加到新的 Vue postCount
变量中。然后,当用户选择一个标签后组件更新时,我们再次获取计数并更新变量。