CSS仅列出过滤或滥用平台
#javascript #css #showdev #astro

我做了一些不好的事情,我很高兴分享它。

自我犯规的问题(许多人)

我曾经将我的个人网站运送到具有慷慨的免费计划的Vercel。但是,我一直想使用Cloudflare,特别是如果我打算在那里购买该域。

由于我的网站只是一堆静态内容,只有很少的交互式组件,因此我选择了Astro。而且,如果我需要添加一些花哨的东西,我就不会添加麻烦。例如,“共享”按钮是一个预先组件(即使它不一定是)。

但是,有时您真的希望该页面被渲染为服务器端。就我而言,/feed页面是我发表的所有文章的列表,列表可能会变得很漫长(如果我实际上保证写更多书)。为了使读者过滤他们感兴趣的主题,我在每个帖子中添加了一个标签列表,在/feed页面上列出了所有主题:

A list of clickable tags, with some selected as filters for the list of articles

每个过滤器芯片可能是一个链接,添加了用于过滤的查询参数,例如?topic=web。然后,在服务器上,我将读取参数并过滤feed:

/* Server only */
const topic = Astro.url.searchParams.get("topic");

// returns ALL articles
const feed = await getFeed();

const filteredFeed = topic
  ? feed.filter((post) => post.data.tags.includes(topic))
  : feed;

// create a flat list of all unique tags used
const topics = [...new Set(feed.flatMap((post) => post.data.tags))];

没有客户端JavaScript,因此,由于查看过渡,访问者几乎没有注意到该页面是服务器端渲染的。

为不存在的问题带来解决方案;还有两个问题

当我从Vercel切换到Cloudflare时,我立即注意到了主要问题:我的图像未进行优化。 astro:assets使用Sharp提供了图像优化,并且CloudFlare适配器不支持它(maybe only for now?)。因此,我选择了:

  • 每次手动优化图像;
  • 请勿优化图像;
  • 请勿使用SSR,在所有内容的构建时间构建页面,应用过滤客户端;

由于前两个选项不喜欢我,我想我可以用最后一个选择。

显然,我只能删除标签过滤,没有人会注意到。但是,嘿,我们在这里进行工程师,我们喜欢挑战。

第一个快速解决方案是通过JavaScript添加过滤,我们都做到了:

  • 从服务器中获取文章和标签列表;
  • 将其传递给跟踪选定芯片并渲染列表的组件;
  • 对芯片选择反应以过滤列表;

对于任何网络框架来说,这似乎是一项琐碎的任务。对于Svelte,我什至可以避免运送大量JavaScript。但是,嘿,我们在这里进行工程师,我们喜欢挑战。

解决方案

我知道我只能渲染所有文章,然后隐藏那些与display: none不匹配的过滤的文章。我也知道我可以使用:has CSS选择器进行切换。所以,我需要:

  • 与每个标签相对应的value的复选框列表
<form>
  <input type="checkbox" value="react" />
  <input type="checkbox" value="tailwindcss" />
</form>
  • 检查一个复选框时隐藏所有文章(应用过滤)
/* if any of the checkboxes is selected, hide all articles */
.feed:has(> form input:checked) .feed-entry {
  display: none;
}
/* TODO: put display: block to those matching selected checkboxes */
  • 显示匹配过滤器的文章

显示与过滤器相匹配的文章

这一点是最复杂的。由于标签和文章的列表是动态的,因此我不仅可以在CSS中列出所有标签,但我知道我需要这样的东西:

.feed:has(> form input:where(
  /* list of all article tags */
  [value=react]:checked,
  [value=tailwindcss]:checked
 ) matching-article  { display: block; }

我决定采用一种直接的方法:生成<style>标签,每篇文章都呈现:

/* for each post, take it's slug as id */
const id = `entry-${post.slug}`;
/* form a CSS selector */
const css = /* CSS */ `
.feed:has( > form input:where(
    ${post.data.tags.map((tag) => `[value="${tag}"]:checked`).join(",")})
) #${id} { display: block; }`;

return (
  <>
    <style set:html={css} />
    <li>
      <a href={post.slug}>{post.title}</a>
    </li>
  </>
);

现在,每篇文章都包括UI和<style>标签,当选择正确的复选框时,它可以看到它。

Browser Dev Panel with DOM tree, with list items having both, style and list item tags

P.S。同样,任何网络框架都可以更好地解决这个问题。通常,您只需将代码较少,因为您只能为每篇文章重复使用HTML,而只需运送JS在运行时生成的模板即可。特别是对于此功能,可以完全忽略JS被禁用或无法加载的风险。

重置状态

当访问者选择多个芯片时,我们希望他们能够重置选择以再次查看所有文章。解决方案很简单:reset input/button

<form>
  /* style same way as the rest of the chips */
  <input type="reset" />
  <input type="checkbox" value="react" />
</form>

An animation showing how the filter chips can be reset with input type reset

小雨

可以优化使用每篇文章的CSS运输,以减少代码。由于列表在构建时间充分渲染,我利用闪电快速lightningcss

import { transform } from "lightningcss";

// in each article
const { code } = transform({
  code: Buffer.from(/* CSS */ `
    .feed:has( > form
    input:where(${post.data.tags
      .map((tag) => `[value="${tag}"]:checked`)
      .join(",")}
    )) #${id} { display: block; }
  `),
  minify: true,
});
return (
  <>
    <style set:html={code} />

也很高兴添加视图过渡,以平稳地出现和消失的列表项目。类似:

<form
  onchange="document.startViewTransition()"
  onreset="document.startViewTransition()"
>

这不起作用,因为在拍摄快照之前更改了列表项目?不确定。

概括

您可以在此部署上进行最终结果:

https://1f1e705a.alvechy.pages.dev/feed

正如我所说,这是一个挑战,而不是最终解决方案。你应该做什么?

  • 船JavaScript;
  • 利用服务器。例如,只有几点javaScript,您就可以作为用户滚动来实现feed的懒惰加载,而不是按照;
  • ; 来运送巨大的HTML
  • 船更圆角;