打算正式将我之前一直的 11ty-book 再移植 astro 上来,本文记录了一直过程中的一些经验和技巧。

  • 💡 关于业务代码部分,尤其是和 JS 相关的部分,你一定要分清楚你的逻辑是发生在 build 阶段还是客户端阶段。

CMS 数据部分

后端用的 Ghost,官方的 Demo 中的数据是每次调用函数去查询,觉得不太优雅,想着有没有其他方式可以在 Astro 初始化阶段将所有数据缓存下来,作为全局共享。

最后使用了官方介绍的 nanostores 库来处理,为了方便管理,先在 utils 目录下新建 api.ts,用于提供 Ghost 数据获取的直接接口,如下

import type { Page, Post } from '@ts-ghost/content-api';
 
import { settingsSchema, TSGhostContentAPI } from '@ts-ghost/content-api';
 
const ghostUrl = 'https://cms.1900.live';
const ghostApiKey = '54bae25f075f027aba23d6f657';
 
export const getPosts = async () => {
    const api = new TSGhostContentAPI(ghostUrl, ghostApiKey, 'v5.0');
    const results = await api.posts
        .browse()
        .include({
            authors: true,
            tags: true
        })
        .fetch();
    if (!results.success) {
        throw new Error(results.errors.map((e) => e.message).join(', '));
    }
    return {
        posts: results.data,
        meta: results.meta
    };
};
// ...其他操作

在 src 下新建目录 store,目录内新建文件 ghost-store.ts,通过 atom 将数据进行缓存,并提供一个数据获取的外部对象,代码如下

import { getSettings, getPosts, getAllTags, getAllPages, getAllPosts } from '../utils/api';
import { atom } from 'nanostores';
 
const settingsStore = atom(await getSettings());
const postsStore = atom(await getPosts());
const postsAllStore = atom(await getAllPosts());
const tagsStore = atom(await getAllTags());
const pagesStore = atom(await getAllPages());
 
export const settings = settingsStore.get();
export const posts = postsStore.get();
export const tags = tagsStore.get();
export const pages = pagesStore.get();
export const postsAll = postsAllStore.get();

资源和 CSS 部分

由于之前的项目使用的是 SCSS,而 Astro 这边的常规做法是使用 TailwindCSS 这种原子化样式,这样打包出来的页面性能是最好的,页面不会加载多余的样式代码,但是把那么多样式转换过来太麻烦了,所以放弃了。

  1. 先安装 Scss 支持
yarn add -D sass
  1. 将所有样式复制到 src 中的 Styles 目录下,Astro 会自动管理这些文件,我们只需要在用的地方导入即可。
---
import '../styles/book.scss';
---
 
<!doctype html>
<html lang={settings?.lang ?? 'en'} dir="ltr">
    <head>
        <slot name="head" />
        {/* 这里可以引入其他头部内容 */}
        <ViewTransitions />
    </head>
 
    <body dir="ltr">
<!-- ...  -->

另外所有用到的资源统一放在项目根目录下内的 Public 目录内,在组件里以目录为起点即可以直接进行访问,如:public/svg/icon.svg 在组件内用 svg/icon.svg 即可。

---
const { page } = Astro.props;
---
 
<div class="flex flex-wrap justify-between book-pagination">
    {
        page.url.prev && (
            <a href={page.url.prev} aria-label="Read previous page" class="flex align-center float-left">
                <Fragment set:html={getSvg('backward')} />
                <span />
            </a>
        )
    }
</div>

布局

环境变量

Astro 自带了适配全平台的环境变量管理,通过在根目录增加 .env 文件即可,也可增加后缀区分是开发模式还是生产环境 .env.development

通过 import.meta.env.GHOST_API_URL; 即可在代码中使用变量

分页

ts-ghost/content-api 提供的含 cursor 获取所有文章函数不能通过 return 跳出,不知道为什么,目前通过修改 while 判断跳出。

export const getAllPosts = async () => {
    const api = new TSGhostContentAPI(ghostUrl, ghostApiKey, 'v5.0');
    const posts: Post[] = [];
    let cursor = await api.posts
        .browse()
        .include({
            authors: true,
            tags: true
        })
        .paginate();
    if (cursor.current.success) posts.push(...cursor.current.data);
    while (cursor.next && posts.length < postLimit) {
        cursor = await cursor.next.paginate();
        if (cursor.current.success) posts.push(...cursor.current.data);
    }
    return posts;
};

在处理多级嵌套分页的时候遇到错误,折腾了两天,总算是弄明白了,总结如下

  • [page][...page] : 如果想用 Astro 的分页就必须写成这种格式,Astro 的底层函数会用 page 变量作为动态参数
  • Astro 的路由和文件夹结构息息相关,文件夹名称如果是 [tag], 则 StaticPaths 函数返回的 params 中必须包含同名称的参数

客户端 JS

正在移植搜索功能,发现客户端交互这个部分真的好难呀,原有的 js 完全没法用,GPT 给的方案也不太管用,似乎要使用一些第三方的框架处理起来会简单一些。。

试了 Alpinejs,教程和相关的案例太少了,没法弄。试着看看了 Solidjs,倒是有不少集成方案,大概看了一下准备开始弄。

Client:load 指的是代码在客户端加载,在需要做客户端交互的时候能用的上。没有这个指令时会直接静态 html 代码。

主题切换功能

因为 Astro 默认是 SPA 模式,而我之前的主题设置代码是在页面加载的时候设置,所以正常情况下是无法起效的,只有再页面刷新加载的时候才会执行那部分代码,参考文档和其他主题后发现需要监听 Astro 的自定义事件 astro:page-load 才能正常执行。

    document.addEventListener('astro:page-load', () => {
        const theme = localStorage.theme;
        if (localStorage.theme !== 'auto') {
            document.documentElement.classList.add(theme);
            localStorage.theme = theme;
            localStorage.themetype = localStorage.themetype || 'light';
        } else {
            const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
            localStorage.themetype = prefersDarkScheme ? 'dark' : 'light';
        }
 
        localStorage.name = localStorage.name || '自适应';
        localStorage.theme = theme || 'auto';
    });