در این مقاله میخواهیم یک dark mode یا حالت تاریک کامل را با استفاده از Next.js پیاده سازی کنیم. اگر در وب سایتی که از حالت تاریک یا dark mode مانند mdxjs.com پشتیبانی میکند، آن را اعمال کرده و صفحه را رفرش کنید، متوجه چیزی خواهید شد.
Flicker (فلیکر) یا همان سوسو زدن حالت نور. اما چرا این اتفاق میافتد؟
این مشکلی است که فقط به وب سایتهای استاتیک / هیبریدی محدود نمیشود، بلکه تقریبا در هر وب سایتی که از جاوااسکریپت برای توسعه کامپوننتهای خود استفاده میکند، ممکن است اتفاق بیافتد. و دلیلش آن است که وقتی صفحه بارگیری میشود، موارد زیر اجرا میشوند:
- ابتدا HTML بارگذاری میشود که به نوبه خود JS و CSS را بارگیری میکند.
- به طور پیش فرض، یک صفحه وب دارای رنگ پس زمینه شفاف است. به این معنی که پس زمینه سفید خواهید گرفت مگر اینکه از افزونههای خاصی استفاده کنید.
- HTML میتواند شامل استایل درون خطی باشد تا رنگ پس زمینه را طوری تنظیم کند که فلیکر را نبینیم. اما در حال حاضر CSS درون خطی از نمایش دادههای media پشتیبانی نمیکند، بنابراین نمیتوانیم بفهمیم که کاربر حالت تاریک را ترجیح میدهد یا نه.
- قبل از رندر صفحه، جاوااسکریپت بارگذاری شده ابتدا باید تجزیه شود. اگر ترجیحی برای حالت تاریک ذخیره شده وجود داشته باشد (معمولا از حافظه محلی استفاده میکند)، توسط جاوااسکریپت بارگیری میشود. این بدان معنی است که تا وقتی همه این کارها انجام نشده است، کاربر ما فقط آنچه HTML توصیف کرده است را میبیند، و این شامل یک پس زمینه شفاف خواهد بود
راهحل
پس باید چه کار کرد؟ ما باید راهی پیدا کنیم تا بتوانیم برخی از کدها را اجرا کنیم و قبل از بارگذاری کل صفحه، رنگ زمینه مناسب را اعمال کنیم.
در اینجا لیستی از موارد لازم برای پیاده سازی وجود دارد:
- اگر کاربر قبلا از سایت ما بازدید کرده باشد، از اولویت ذخیره شده آن استفاده میکنیم.
- اگر کاربر قبلا از سایت ما بازدید نکرده باشد یا تنظیمی را ذخیره نکرده باشد، بررسی میکنیم که آیا سیستمعامل آن دارای اولویت است یا نه.
- اگر دو روش بالا هنوز اولویت را مشخص نمیکنند، ما یک تم روشن را به عنوان استایل پیش فرض قرار میدهیم.
- قبل از اینکه صفحه رندر شود و به کاربر نشان داده شود، همه موارد فوق باید اجرا شوند.
- به کاربر اجازه دهید حالت تاریک را تغییر دهد و اولویت خود را برای مراجعه در آینده ذخیره کند.
بیایید با یک صفحه Next.js ساده همراه با کلید حالت تاریک شروع کنیم:
// pages/index.js
import { useState } from "react";
const IndexPage = () => {
const [isDarkTheme, setIsDarkTheme] = useState(false);
const handleToggle = (event) => {
setIsDarkTheme(ev.target.checked);
};
return (
<div>
<label>
<input
type="checkbox"
checked={isDarkTheme}
onChange={handleToggle}
/>
Dark
</label>
<h1>Hello there</h1>
<p>General Kenobi!</p>
</div>
);
};
export default IndexPage;
ذخیره و بازیابی اولویتهای کاربر
اگر کاربر قبلا از وب سایت ما بازدید کرده باشد، با اضافه کردن توانایی ذخیره و بازیابی تنظیمات انجام شده شروع میکنیم. localStorage یک روش ساده برای دستیابی دقیق به این امر مهم است، حتی وقتی کاربر صفحه را رفرش میکند یا مرورگر را کاملا میبندد و بعدا دوباره آن را باز میکند. اگرچه نگرانیهایی در مورد ذخیرهسازی اطلاعات حساس در localStorage وجود دارد، اما برای ذخیره تنظیمات برگزیده حالت تاریک کاربر بسیار مناسب است.
در اینجا چگونگی ذخیره و بارگیری اولویتهای تم با استفاده از localStorage آورده شده است:
window.localStorage.setItem("theme", "dark"); // or "light"
const userPreference = window.localStorage.getItem("theme"); // "dark"
اولویت در کل سیستم
prefers-color-scheme یک خصوصیت media در CSS است که به ما اجازه میدهد تشخیص دهیم آیا کاربر تنظیمات دلخواه حالت تاریک در سیستم را تنظیم کرده است یا خیر. در صورتی که کاربر هنوز آن را تنظیم نکرده باشد، میتوانیم از آن استفاده کنیم.
تمام کاری که ما باید انجام دهیم اجرای یک کوئری media است و مرورگر ()matchMedia را برای انجام دقیق این کار در اختیار ما قرار میدهد.
در اینجا نحوه جستجوی media برای بررسی اینکه آیا کاربر اولویتی را تنظیم کرده، به این صورت است:
const mql = window.matchMedia("(prefers-color-scheme: dark)");
با خروجی زیر (هنگامی که کاربر اولویت خود را برای حالت تاریک تنظیم کرده است):
{
"matches": true,
"media": "(prefers-color-scheme: dark)"
}
بیایید اینها را به برنامه خود اضافه کنیم.
import { useState } from "react";
const IndexPage = () => {
const [isDarkTheme, setIsDarkTheme] = useState(false);
const handleToggle = (event) => {
setIsDarkTheme(ev.target.checked);
};
const getMediaQueryPreference = () => {
const mediaQuery = "(prefers-color-scheme: dark)";
const mql = window.matchMedia(mediaQuery);
const hasPreference = typeof mql.matches === "boolean";
if (hasPreference) {
return mql.matches ? "dark" : "light";
}
};
const storeUserSetPreference = (pref) => {
localStorage.setItem("theme", pref);
};
const getUserSetPreference = () => {
return localStorage.getItem("theme");
};
useEffect(() => {
const userSetPreference = getUserSetPreference();
if (userSetPreference !== null) {
setIsDarkTheme(userSetPreference === "dark");
} else {
const mediaQueryPreference = getMediaQueryPreference();
setIsDarkTheme(mediaQueryPreference === "dark");
}
}, []);
useEffect(() => {
if (isDarkTheme !== undefined) {
if (isDarkTheme) {
storeUserSetPreference("dark");
} else {
storeUserSetPreference("light");
}
}
}, [isDarkTheme]);
return (
<div>
<label>
<input
type="checkbox"
checked={isDarkTheme}
onChange={handleToggle}
/>
Dark
</label>
<h1>Hello there</h1>
<p>General Kenobi!</p>
</div>
);
};
export default IndexPage;
- هنگامی که صفحه بارگیری میشود و کامپوننت IndexPage نصب شده است، اولویت تنظیمات کاربر را بازیابی میکنیم اگر قبلا یکی از آنها را از بازدید قبلی خود تنظیم کرده باشد.
- اگر کاربر قبلا آن را تنظیم نکرده باشد، ()localStorage.getItem مقدار null را برمیگرداند و ما به سراغ بررسی اولویت سیستم آنها در حالت تاریک میرویم.
- حالت روشن را به عنوان پیش فرض قرار میدهیم.
- هر زمان که کاربر برای روشن یا خاموش کردن حالت تاریک کلید آن را فعال کرد، اولویتش را در localStorage برای استفاده در آینده ذخیره میکنیم.
بسیار خب، تا اینجا کلید تغییر حالت روشن و تاریک را طراحی کردهایم و همچنین میتوانیم حالت صحیح را در صفحه ذخیره و بازیابی کنیم.
بازگشت به اصول اولیه
بزرگترین چالش اجرای همه این موارد قبل از اینکه چیزی به کاربر نشان داده شود، بود. از آنجا که ما از Next.js با Static Generation آن استفاده میکنیم، هیچ راهی وجود ندارد که در زمان کدنویسی بدانیم که ترجیح کاربر چه خواهد بود.
مگر اینکه راهی برای اجرای برخی از کدها وجود داشته باشد، قبل از اینکه همه صفحه بارگیری شود و به کاربر ارائه شود.
نگاهی به کد زیر بیندازید:
<body>
<script>
alert("No UI for you!");
</script>
<h1>Page Title</h1>
</body>
آنچه در آن به نظر میرسد در تصویر زیر میبینید:
اگر میخواهید خودتان آن را امتحان کنید، این لینک را بررسی کنید.
وقتی ما قبل از <h1> یک <script> در body خود اضافه میکنیم، ارائه محتوای واقعی توسط اسکریپت مسدود میشود. این بدان معناست که میتوانیم کدی را اجرا کنیم که قبل از نمایش هر محتوایی به کاربر، به صورت تضمینی اجرا شود. دقیقا همان کاری که میخواهیم انجام دهیم.
سند Next.js
از مثال بالا اکنون میدانیم که باید قبل از محتوای واقعی، یک <script> در <body> صفحه خود اضافه کنیم.
Next.js با اضافه کردن یک فایل document.tsx_ (یا document.js_) راهی ساده برای اصلاح تگ <html> و <body> در برنامه فراهم میکند. Document فقط در سرور رندر میشود، بنابراین اسکریپت ما همانطور که آن را در مرورگر کلاینت توصیف میکنیم، بارگیری میشود.
با این توضیحات، در زیر چگونگی افزودن اسکریپت آورده شده است:
import Document, { Html, Head, Main, NextScript } from "next/document";
export default class MyDocument extends Document {
render() {
return (
<Html>
<Head />
<body>
<script
dangerouslySetInnerHTML={{
__html: customScript,
}}
></script>
<Main />
<NextScript />
</body>
</Html>
);
}
}
const customScript = `
console.log("Our custom script runs!");
`;
<Html>، <Head />، <Main /> و <NextScript /> برای رندر صحیح صفحه مورد نیاز است.
DOM مرورگرHTML داخلی را برای دریافت یا تنظیم HTML موجود در یک عنصر در اختیار ما قرار میدهد. معمولا تنظیم HTML از طریق کد کاری خطرناک است، زیرا به راحتی میتوان سهوا کاربران را در معرض حمله XSS قرار داد. ریاکت با تمیز کردن محتویات قبل از رندرینگ، به طور پیش فرض ما را از این امر محافظت میکند.
اگر کاربری بخواهد نام خود را <script>I'm dangerous!</script> بگذارد، ریاکت کاراکترهایی مانند < را به صورت < رمزگذاری میکند. بدین ترتیب این اسکریپت هیچ تاثیری ندارد.
ریاکت همچنین روشی را برای نادیده گرفتن این رفتار با استفاده از gerouslySetInnerHTML فراهم میکند و به ما میگوید که این خطرناک است. در این پروژه واقعا میخواهیم یک اسکریپت را تزریق و اجرا کنیم.
توجه داشته باشید که اسکریپت چگونه ما را مجبور به عبور از HTML داخلی میکند.
اکنون میدانیم که چگونه اسکریپت قبل از بقیه صفحه بارگذاری شده است (با کمک سند Next.js، قبل از هر صفحه)، اما هنوز به چند قطعه دیگر از این معما نیاز داریم:
- اسکریپت را به محض بارگیری اجرا کنید.
- رنگ پس زمینه و سایر خصوصیات CSS را بر اساس منطقی که اضافه میکنیم تغییر دهید.
IIFEها
قطعه بعدی پازل ما این است که بدانیم چگونه اسکریپت سفارشی خود را در اسرع وقت اجرا کنیم.
به عنوان یادآوری این کار را برای درک وضعیت صحیح حالت تاریک (فعال / غیرفعال یا به عبارتی true / false) انجام میدهیم تا در هنگام بارگذاری صفحه وب توسط کاربر، از سوسو زدنهای ناگهانی جلوگیری کنیم.
عبارات تابع فراخوانی شده را بلافاصله وارد کنید. (یا به اختصار IIFE)
IIFE به سادگی یک تابع جاوااسکریپت است که به محض تعریف اجرا میشود. همچنین هنگامی که بخواهیم از آلودگی فضای نام جهانی جلوگیری کنیم، بسیار عالی عمل میکند.
IIFE اینگونه به نظر میرسد:
(function () {
var name = "Sreetam Das";
console.log(name);
// "Sreetam Das"
})();
// Variable name is not accessible from the outside scope
console.log(name);
// throws "Uncaught ReferenceError: name is not defined"
بیایید این را به document.js_ خود اضافه کنیم.
import Document, { Html, Head, Main, NextScript } from "next/document";
function setInitialColorMode() {
function getInitialColorMode() {
const preference = window.localStorage.getItem("theme");
const hasPreference = typeof preference === "string";
/**
* If the user has explicitly chosen light or dark,
* use it. Otherwise, this value will be null.
*/
if (hasPreference) {
return preference;
}
// If there is no saved preference, use a media query
const mediaQuery = "(prefers-color-scheme: dark)";
const mql = window.matchMedia(mediaQuery);
const hasPreference = typeof mql.matches === "boolean";
if (hasPreference) {
return mql.matches ? "dark" : "light";
}
// default to 'light'.
return "light";
}
const colorMode = getInitialColorMode();
}
// our function needs to be a string
const blockingSetInitialColorMode = `(function() {
${setInitialColorMode.toString()}
setInitialColorMode();
})()
`;
export default class MyDocument extends Document {
render() {
return (
<Html>
<Head />
<body>
<script
dangerouslySetInnerHTML={{
__html: blockingSetInitialColorMode,
}}
></script>
<Main />
<NextScript />
</body>
</Html>
);
}
}
اکنون میتوانیم قبل از بارگیری کامل صفحه، وضعیت مناسب حالت تاریک خود را به درستی بازیابی کنیم. حال مانع نهایی ما این است که بتوانیم این مورد را به کامپوننت صفحه خود منتقل کنیم تا در واقع بتوانیم حالت تاریک یا dark mode ترجیحی را اعمال کنیم.
چالش اینجاست که باید بتوانیم این اطلاعات را از یک اسکریپت خالص جاوااسکریپت که قبل از بارگیری کامل صفحه و کامپوننتهای ریاکت آن در حال اجرا است، انتقال دهیم و آنها را به اصطلاح هیدراته کنیم.
متغیرهای CSS
آخرین مرحله به روزرسانی صفحه با تم دلخواه کاربر است.
روشهای مختلفی برای انجام این کار وجود دارد:
- میتوانیم از کلاسهای CSS برای تمهای مختلف استفاده کنیم و آنها را به صورت برنامهریزی شده تغییر دهیم.
- میتوانیم از state ریاکت استفاده کنیم و یک .class را به عنوان یک الگوی واقعی عبور دهیم.
- همچنین میتوانیم از کامپوننتهای استایل استفاده کنیم.
هرچند به نظر میرسد همه گزینهها راهحلهای ممکن هستند، اما هر کدام به بویلرپلیت (boilerplate) بیشتری نیاز دارند که باید اضافه شوند.
یا میتوانیم متغیرهای CSS را داشته باشیم.
خصوصیات سفارشی CSS (که به آن متغیرهای CSS نیز گفته میشود) به ما امکان استفاده مجدد از مقادیر خاص را در کل یک سند میدهد. این موارد را میتوان با استفاده از نشانگذاری سفارشی تنظیم کرد و با استفاده از تابع ()var به آنها دسترسی داشت:
:root {
--color-primary-accent: #5b34da;
}
بهترین روش معمول، تعریف خصوصیات سفارشی در شبه کلاس root: است تا بتوان آن را به صورت گلوبال در سند HTML اعمال کرد.
بهترین قسمت در مورد متغیرهای CSS این است که واکنشپذیر هستند، در کل صفحه باقی میمانند و با به روزرسانی آنها، HTML نیز که به آنها ارجاع شده، به روز میشود و میتوان آنها را با استفاده از جاوااسکریپت به روز کرد.
// setting
const root = document.documentElement;
root.style.setProperty("--initial-color-mode", "dark");
// getting
const root = window.document.documentElement;
const initial = root.style.getPropertyValue("--initial-color-mode");
// "dark"
وقتی میخواهید مقادیر مشخصی را در CSS خود دوباره استفاده کنید، متغیرهای CSS واقعا میدرخشند.
میتوانیم از ویژگیهای HTML استفاده کنیم و از آنجا که CSS به این ویژگیها دسترسی دارد، بسته به خصوصیت data-theme تنظیم شده، مقادیر مختلفی را به متغیرهای CSS اختصاص دهیم. مانند زیر:
:root {
--color-primary-accent: #5b34da;
--color-primary: #000;
--color-background: #fff;
--color-secondary-accent: #358ef1;
}
[data-theme="dark"] {
--color-primary-accent: #9d86e9;
--color-secondary-accent: #61dafb;
--color-primary: #fff;
--color-background: #000;
}
[data-theme="batman"] {
--color-primary-accent: #ffff00;
}
همچنین میتوانیم خصوصیت را خیلی راحت تنظیم و حذف کنیم:
if (userPreference === "dark")
document.documentElement.setAttribute("data-theme", "dark");
// and to remove, setting the "light" mode:
document.documentElement.removeAttribute("data-theme");
سرانجام میتوانیم state حالت تاریک را از اسکریپت مسدود کننده خود به کامپوننت ریاکت منتقل کنیم.
قبل از اینکه همه چیزهایی را که تاکنون داریم جمع بندی کنیم، بیایید یادآوری کنیم:
- به محض بارگیری صفحه وب، یک اسکریپت مسدود کننده را با استفاده از سندNext.js و IIFEها تزریق و اجرا کنید.
- با استفاده از localStorage، اولویت ذخیره شده کاربر از بازدید قبلی را بررسی کنید.
- بررسی کنید آیا کاربر با استفاده از یک کوئری media، تنظیمات حالت تاریک یا dark mode در کل صفحه دارد یا خیر.
- اگر هر دو بررسی بالا بی نتیجه باشند، یک تم روشن را به عنوان پیش فرض قرار دهید.
- این اولویت را به عنوان یک متغیر CSS منتقل کنید که میتوانیم آن را در کامپوننتtoggle خود بخوانیم.
- تم را میتوان به یک toggle تبدیل کرد و با آن اولویت را برای بازدیدهای بعدی ذخیره کنید.
- صفحه هرگز نباید بار اول سوسو بزند، حتی اگر کاربر برای تم غیر پیش فرض اولویت داشته باشد.
- همیشه باید وضعیت صحیح toggle خود را نشان دهید و اگر وضعیت صحیح ناشناخته باشد، رندرینگ را به تعویق بیندازید.
نتیجه نهایی به این صورت است:
import Document, { Html, Head, Main, NextScript } from "next/document";
function setInitialColorMode() {
function getInitialColorMode() {
const preference = window.localStorage.getItem("theme");
const hasPreference = typeof preference === "string";
/**
* If the user has explicitly chosen light or dark,
* use it. Otherwise, this value will be null.
*/
if (hasPreference) {
return preference;
}
// If there is no saved preference, use a media query
const mediaQuery = "(prefers-color-scheme: dark)";
const mql = window.matchMedia(mediaQuery);
const hasPreference = typeof mql.matches === "boolean";
if (hasPreference) {
return mql.matches ? "dark" : "light";
}
// default to 'light'.
return "light";
}
const colorMode = getInitialColorMode();
const root = document.documentElement;
root.style.setProperty("--initial-color-mode", colorMode);
// add HTML attribute if dark mode
if (colorMode === "dark")
document.documentElement.setAttribute("data-theme", "dark");
}
// our function needs to be a string
const blockingSetInitialColorMode = `(function() {
${setInitialColorMode.toString()}
setInitialColorMode();
})()
`;
export default class MyDocument extends Document {
render() {
return (
<Html>
<Head />
<body>
<script
dangerouslySetInnerHTML={{
__html: blockingSetInitialColorMode,
}}
></script>
<Main />
<NextScript />
</body>
</Html>
);
}
}
توجه داشته باشید که چگونه از ()style.setProperty و همچنین ()documentElement.setAttribute برای انتقال دادههای خود استفاده میکنیم.
بیایید CSS خود را اضافه کرده و مقادیر جداگانهای را برای متغیرهای CSS در نظر بگیریم تا حالت تاریک اعمال شود.
:root {
--color-primary-accent: #5b34da;
--color-primary: #000;
--color-background: #fff;
}
[data-theme="dark"] {
--color-primary-accent: #9d86e9;
--color-primary: #fff;
--color-background: #000;
}
body {
background-color: var(--color-background);
color: var(--color-primary);
}
اکنون باید این استایلها را در برنامه خود وارد کنیم.
از آنجا که میخواهیم این استایلها در کل وب سایت ما در دسترس باشد، باید از کامپوننت App که Next.js در اختیار ما قرار میدهد استفاده کنیم. این همان سندی است که قبلا دیدیم، این یک کامپوننت ویژه است که میتواند برای کنترل هر صفحه در برنامه Next.js همانطور که برای مقداردهی اولیه صفحات ما کاربرد دارد، مورد استفاده قرار گیرد.
همچنین میتواند مکان مناسبی برای افزودن CSS گلوبال نیز باشد.
import "../styles.css";
export default function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
و در آخر، صفحه کامپوننت ریاکت:
import { useEffect, useState } from "react";
const IndexPage = () => {
const [darkTheme, setDarkTheme] = useState(undefined);
const handleToggle = (event) => {
setDarkTheme(event.target.checked);
};
const storeUserSetPreference = (pref) => {
localStorage.setItem("theme", pref);
};
const root = document.documentElement;
useEffect(() => {
const initialColorValue = root.style.getPropertyValue(
"--initial-color-mode",
);
setDarkTheme(initialColorValue === "dark");
}, []);
useEffect(() => {
if (darkTheme !== undefined) {
if (darkTheme) {
root.setAttribute("data-theme", "dark");
storeUserSetPreference("dark");
} else {
root.removeAttribute("data-theme");
storeUserSetPreference("light");
}
}
}, [darkTheme]);
return (
<div>
{darkTheme !== undefined && (
<label>
<input
type="checkbox"
checked={darkTheme}
onChange={handleToggle}
/>
Dark
</label>
)}
<h1>Hello there</h1>
<p style={{ color: "var(--color-primary-accent)" }}>
General Kenobi!
</p>
</div>
);
};
export default IndexPage;
یک نکته مهم در مورد رویکرد ما با استفاده از CSS vanilla این است که در طول توسعه، هنوز سوسو زدن را تجربه میکنید، اما هنگامی که برنامه کامپایل میشود دیگر این اتفاق نمیافتد.
اگر از کامپوننتهای استایل استفاده کنید، دیگر این مشکل ساز نیست؛ چون میتوانیم از ()ServerStyleSheet استفاده کنیم که مطمئن میشود CSS در برنامه به درستی وارد شده است و در طول توسعه از سوسو زدن جلوگیری میکند.
پیاده سازی حالت isDarkTheme به صورت تعریف نشده به ما امکان میدهد تا رندر کردن حالت تاریک خود را به تعویق بیندازیم، بنابراین از نشان دادن وضعیت toggle اشتباه به کاربر جلوگیری میکند.
جمعبندی
در این مقاله آموختیم که چگونه یک حالت تاریک یا dark mode کامل بدون هیچ سوسو زدنی داشته باشیم و مطمئنا انتظار نداشتید با مواردی مانند متغیرهای CSS و IIFE کار کنیم.
در اینجا چند لینک برای بررسی برنامه تکمیل شده وجود دارد:
برنامه کامل: nextjs-perfect-dark-mode.netlify.app
ریپازیتوری: github.com/sreetamdas/nextjs-perfect-dark-mode-mode
سند باکس: codesandbox.io/s/> dreamy-nightingale-ikwks
البته پکیجهایی هم وجود دارند که میتوانند همه اینها را برای شما مدیریت کنند. از جمله "flash" که فقط در اجرا کمی متفاوت است.
روز به روز تعداد بیشتری از افراد dark mode یا حالت تاریک را به وب سایتهای خود اضافه می کنند و امیدواریم این مقاله بتواند به شما کمک کند تا یک وب سایت مناسب را طراحی کنید.
دیدگاه و پرسش
در حال دریافت نظرات از سرور، لطفا منتظر بمانید
در حال دریافت نظرات از سرور، لطفا منتظر بمانید