手寫一個 astro-icon

日期:
分類: Changelog 2 頁面仔的自我修養 9
標籤: Astro 4

一直以來我的部落格的開發體驗都不是很好:即使不是依賴變更後的冷啟動,僅是在有快取的情況下,Astro 的 dev 在 ready 前和渲染出第一個頁面這兩個過程之間,都需要十幾秒以上的時間。最近再次心血來潮,想看看到底是誰幹的好事。

照例 dev,然後發現,在 [astro-icon] 的輸出後整個卡住了。遂以 --verbose 模式啟動,然後看到下面兩行 +23s 和 +22s:

dev --verbose 的輸出
dev --verbose 的輸出
---
import type { HTMLAttributes } from "astro/types";
import { readFile } from "node:fs/promises";
import type { ExtendedIconifyIcon } from "@iconify/types";
import { iconToSVG, iconToHTML, getIconData } from "@iconify/utils";
import { locate } from "@iconify/json";

export type Props = HTMLAttributes<"svg"> &
  (
    | {
        name: string;
      }
    | {
        set: string;
        icon: string;
      }
  ) & {
    "is:inline"?: boolean;
    title?: string;
    desc?: string;
    size?: number | string;
    width?: number | string;
    height?: number | string;
    class?: string;
  };

const fsCache = new Map<string, string>();
async function readFileCached(path: string) {
  let res = fsCache.get(path);
  if (res) return res;
  res = await readFile(path, "utf8");
  fsCache.set(path, res);
  return res;
}

async function generateSVG(
  prefix: string,
  name: string,
  customisations: Record<string, unknown> = {}
) {
  let data: ExtendedIconifyIcon | null = null;
  if (prefix) {
    const filename = locate(prefix);

    /** @type {import("@iconify/types").IconifyJSON} */
    const iconSet = JSON.parse(await readFileCached(filename));

    data = getIconData(iconSet, name);
  } else {
    const body = await readFileCached(`./src/icons/${name}.svg`);
    // extract width and height from the SVG: <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"
    data = {
      body,
      width: Number.parseInt(body.match(/ width="(\d+)(px)?"/)?.[1] ?? "32"),
      height: Number.parseInt(body.match(/ height="(\d+)(px)?"/)?.[1] ?? "32"),
    };
  }
  if (!data) {
    throw new Error(`Icon not found: ${prefix}:${name}`);
  }

  /**
   * Default dimensions of generated SVG:
   * - '1em' -> 1em, easy to resize icons with font-size.
   * - 'auto' -> same as icon's viewBox.
   * - 'unset' -> no width/height in generated icons. You'll need to assign width and height in CSS.
   */
  const height = (customisations?.height as string | undefined) ?? "1em";
  const { attributes, body } = iconToSVG(data, {
    height,
  });
  return iconToHTML(body, attributes);
}

function generateAPIimg(prefix: string, name: string) {
  if (prefix === "") {
    return `/icons/${name}.svg`;
  }
  return `https://api.iconify.design/${prefix}:${name}.svg`;
}

let prefix = "";
let name = "";
if (Astro.props.name) {
  const namen = Astro.props.name;
  if (namen.includes(":")) [prefix, name] = namen.split(":");
  else [prefix, name] = ["", namen];
} else if ("set" in Astro.props) {
  prefix = Astro.props.set;
  name = Astro.props.icon;
} else {
  console.log(Astro.props);
  throw new Error("Icon name or set and icon must be provided.");
}
---

{
  Astro.locals.renderer === "rss" ? (
    <img
      src={generateAPIimg(prefix, name)}
      width={Astro.props.width ?? Astro.props.size ?? "1em"}
      height={Astro.props.height ?? Astro.props.size ?? "1em"}
      alt={name}
      class={Astro.props.class}
    />
  ) : (
    <Fragment
      set:html={await generateSVG(prefix, name, {
        height: Astro.props.height ?? "1em",
      })}
    />
  )
}

評論

評論將在稽覈後顯示,閣下可以在本部落格的 Github 倉庫的 拉取請求列表 中檢視。提交成功後會自動跳轉。

本站不支持 Dark Reader 的暗色模式,请对本站关闭后再访问,亮色模式的对比度、亮度等选项不受影响。部分页面右上角提供暗色模式切换按钮,如果你没看到,说明你的浏览器尚不支持此特性。本提示不依赖于 JavaScript,你可自行查找其他用户在本站发表的关于如何关闭此提示的评论。