在服务器端使用 Vue 动态过滤列表比你想象的要容易

Avatar of Dan Brellis
Dan Brellis

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费积分!

我最近参加了在德克萨斯州奥斯汀举行的 ARTIFACT 会议,并从一些关于通过网站性能角度进行可访问性的演讲中获得灵感。我发现,人们倾向于依赖大型 JavaScript 框架来处理工作,比如 React、Vue 和 Angular,但在某些情况下这可能过于复杂,从而对网站性能(以及可访问性)造成负面影响。同时,这些框架可以使开发人员更容易更高效地进行开发。从这次会议中,我最大的收获是了解如何平衡快速、高效的体验和我的开发流程。

在会议结束后几天,我正在为一个项目构建列表过滤功能时,一直在思考这个问题。这是一个非常标准的功能:我需要一个帖子列表和一些类别过滤功能。我当时使用 CraftCMS 来进行前端路由和模板,以及一些 Vue 组件来增强 JavaScript 功能。这不是一个完整的“单页面应用程序”,更像是少量 Vue 代码的加入。

通常的做法是:

  1. 使用 Craft/Twig 渲染一个带有空 div 的页面
  2. 将 Vue 组件挂载到该 div 上
  3. 从 Vue 组件发出 Ajax 请求到 API 获取帖子数据(JSON 格式)
  4. 渲染帖子并将过滤功能绑定到帖子。

由于帖子在 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 变量中。然后,当用户选择一个标签后组件更新时,我们再次获取计数并更新变量。