То, что принесла нам Astro, — это мир без Javascript по умолчанию. Они используют эту философию, чтобы уменьшить размер пакета и ускорить первую отрисовку браузера, но это также ставит перед разработчиком множество новых задач, таких как переключение кнопки темной темы в блоге.
С Next.js
Я написал ThemeContext для переключения темного режима в своем блоге, созданном поверх Next.js. ThemeContext добавит class=”dark”
в корневой HTML-код при монтировании приложения. Таким образом, TailwindCSS может распознать текущую тему.¹
```ts // The code I am using in my blog built with Next.js import { useEffect, useState, createContext } from "react"; const defaultState = { theme: "light", toggleDark: () => {}, }; export const ThemeContext = createContext(defaultState); export const ThemeProvider = ({ initialTheme, children }) => { const [theme, setTheme] = useState("light"); const rawSetTheme = (rawTheme) => { const root = window.document.documentElement; const isDark = rawTheme === "dark"; root.classList.remove(isDark ? "light" : "dark"); root.classList.add(rawTheme); }; if (initialTheme) { rawSetTheme(initialTheme); } React.useEffect(() => { rawSetTheme(theme); }, [theme]); return ( <ThemeContext.Provider value={[ theme, setTheme ]}> {children} </ThemeContext.Provider> ); }; ```
С Astro.js
Но если мы хотим сохранить использование Context API, нам может понадобиться написать что-то вроде приведенного ниже.
// mainPage.astro --- import ContextWrapperedComponent from "./ContextWrapperedComponent" --- <ContextWrapperedComponent client:load /> // ContextWrapperedComponent export const ContextWrapperedComponent = () => { // logic for context and components return ( <div> // bunch of components that rely on context </div> ) }
Основываясь на идее Astro.js — Partial Hydration², нам нужно пометить весь `ContextWrapperedComponent` как `client:load` или `client:only`, чтобы сделать его гидратированным и внедрить его в HTML как Javascript. Но таким образом появляется множество ненужных скриптов, которые вводятся и увеличивают общий размер пакета, а это не то, что нам нужно.
Поэтому я решил переключить свое хранилище состояния темного режима с Context на localStorage.
- При первом монтировании я получу доступ к значению ключа (тема блога) в localStorage и определяю тему страницы в соответствии с этим значением.
- Как только пользователь переключит тему, я сброшу тему страницы и обновлю значение в localStorage.
На первый взгляд, эта реализация выглядит просто, но все же имеет много предостережений.
Предупреждение 1 – мигающие страницы
Процесс рендеринга Astro сначала отправляет HTML и CSS клиенту, монтирует расширенную строку шаблона и прослушивает событие для внедрения скрипта. Такое поведение может привести к некоторым нежелательным результатам.
// This code will update the page's theme upon the finish // of the first time painting, but that is not what we want. useEffect(() => { let theme: "light" | "dark"; if (typeof localStorage !== "undefined" && localStorage.getItem("theme")) { theme = localStorage.getItem("theme") as "light" | "dark"; } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) { theme = "dark"; } else { theme = "light"; } if (theme === "light") { setTheme("light"); } else { setTheme("dark"); } }, []);
Например, если вашей темой по умолчанию является темный режим, но в localStorage хранится светлый режим. HTML сначала будет отображать темный режим, как только клиентская сторона смонтирована, событие запускает внедрение гидратированного скрипта, ваша страница будет обновляться как светлый режим, и этот процесс будет продолжать работать каждый раз, когда пользователь меняет страницы. В результате ваши страницы будут продолжать мигать.
Чтобы решить проблему мерцания, мы должны обновить тему перед первой прорисовкой браузера.
К счастью, Astro.js уже позаботились о нас, в их API вы можете сделать скрипт, отправляемый клиенту с HTML и CSS как есть, со специальным атрибутом `is:inline`³. Astro не будет гидратировать этот скрипт и практиковать какую-либо оптимизацию. Если серьезно, это не приветствуется философией Astro, но необходимо для нашего приложения. Кроме того, этот подход также используется в официальном документе Astro.js.⁴
// With this code, we can update the page's theme before the first-time painting. // You should put the code into <script is:inline> const html = document.querySelector("html"); const theme = (() => { if ( typeof localStorage !== "undefined" && localStorage.getItem("theme") ) { return localStorage.getItem("theme"); } if (window.matchMedia("(prefers-color-scheme: dark)").matches) { return "dark"; } return "light"; })(); if (theme === "light") { html.classList.remove("dark"); } else { html.classList.add("dark"); }
Предостережение 2 — переход кнопки переключения
Первоначально моя кнопка переключения темного режима находится в темном режиме, она будет отображать значок солнца и отображать значок луны в светлом режиме. Проблема с этим дизайном близка к предостережению 1, у него будет проблема с мерцанием. На первый взгляд, я думаю, что могу обновить значок так же, как я сделал с приведенным выше решением, но позже я обнаружил, что это не работает, и причина довольно проста.
Поскольку нам нужно, чтобы кнопка-переключатель была интерактивной, она должна иметь префикс «client:load», чтобы указать, что Astro.js гидратирует этот скрипт. Но при первом рисовании гидратированный скрипт еще не внедрен, поэтому скрипт is:inline не может найти целевую кнопку для обновления значка. Кнопка появится только после того, как первая картина будет закончена.
Чтобы решить эту проблему, мне нужно изменить дизайн кнопки-переключателя. Теперь он отображает значок солнца и луны одновременно, но имеет мерцающий переход, указывающий, какая из них является текущей темой.
Заключение
Программирование с помощью Astro.js — это новое путешествие, у него есть много возможностей для открытия, я буду продолжать обновлять то, что я нашел во время исследования.