diff --git a/components/mdx/Image.tsx b/components/mdx/Image.tsx
index fcf68c4..ec5a535 100644
--- a/components/mdx/Image.tsx
+++ b/components/mdx/Image.tsx
@@ -3,10 +3,15 @@ import NextImage, { ImageProps } from 'next/image';
interface Props extends ImageProps {}
const Image = ({ alt, ...rest }: Props) => {
+ const supportImg = ['jpeg', 'png', 'webp', 'avif'];
+ const placeholder = supportImg.includes((rest.src as { src: string }).src)
+ ? 'blur'
+ : 'empty';
+
return (
<>
-
+
{alt && {alt}}
>
diff --git a/next.config.mjs b/next.config.mjs
index 9713e69..c545a9e 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -14,7 +14,10 @@ const composedConfig = composePlugins([
extension: /\.mdx?$/,
options: {
remarkPlugins: [remarkFrontmatter, remarkGfm],
- rehypePlugins: [rehypePrism, rehypeSlug],
+ rehypePlugins: [
+ [rehypePrism, { alias: { vue: 'xml' }, ignoreMissing: true }],
+ rehypeSlug,
+ ],
providerImportSource: '@mdx-js/react',
},
}),
diff --git a/pages/p/create-a-mini-router-for-react.mdx b/pages/p/create-a-mini-router-for-react.mdx
new file mode 100644
index 0000000..2bbdb9e
--- /dev/null
+++ b/pages/p/create-a-mini-router-for-react.mdx
@@ -0,0 +1,328 @@
+---
+title: 现代前端的Web应用路由-为React打造一个迷你路由器
+date: '2022-08-03'
+tags: [JavaScript, React]
+---
+
+import Layout from 'layouts/MDXLayout';
+import Image from 'components/mdx/Image';
+import image1 from 'public/images/p/create-a-mini-router-for-react/router.webp';
+import image2 from 'public/images/p/create-a-mini-router-for-react/Web架构.svg';
+import image3 from 'public/images/p/create-a-mini-router-for-react/迷你路由器.svg';
+import image4 from 'public/images/p/create-a-mini-router-for-react/image-20210823154009498.webp';
+
+export const meta = {
+ title: '现代前端的Web应用路由-为React打造一个迷你路由器',
+ date: '2022-08-03',
+ tags: ['JavaScript', 'React'],
+};
+
+export default ({ children }) => {children};
+
+路由不仅仅只是网络的代名词,它更像是一种表达路径的概念。与网络中的路由相似,前端中的页面路由也是带领我们前往指定的地方。
+
+
+
+## 现代前端的 Web 应用路由
+
+时代在变迁,过去,Web 应用的基本架构使用了一种不同于现代路由的方法。曾经的架构通常都是由后端生成的 HTML 模板来发送给浏览器。当我们单击一个标签导航到另一个页面时,浏览器会发送一个新的请求给服务器,然后服务器再将对应的页面渲染好发过来。也就是说,每个请求都会刷新页面。
+
+自那时起,Web 服务器在设计和构造方面经历了很多发展(前端也是)。如今,JavaScript 框架和浏览器技术已经足够先进,允许我们利用 JavaScript 在客户端渲染 HTML 页面,以至于 Web 应用可以采用更独特的前后的分离机制。在第一次由服务端下发了对应的 JavaScript 代码后,后续的工作就全部交给客户端 JavaScript。而后端服务器负责发送原始数据,通常是 JSON 等。
+
+
+
+在旧架构中,动态内容由 Web 服务器生成,服务器会在数据库中获取数据,并利用数据渲染 HTML 模板发送给浏览器。每次切换页面都会获取新的由服务端渲染的页面发送给浏览器。
+
+在新架构中,服务端通常只下发主要的 JavaScript 和基本的 HTML 框架。之后页面的渲染就会由我们的 JavaScript 接管,后续的动态内容也还是由服务器在数据库中获取,但不同的是,后续数据由服务器发送原始格式(JSON 等)。前端 JavaScript 由 AJAX 等技术获取到了新数据,再在客户端完成新的 HTML 渲染。
+
+好像 SPA 的大概就是将原先服务端干的活交给了前端的 JavaScript 来做。事实上,确实如此。AJAX 技术的发展,带动了客户端 JavaScript 崛起,使得原先需要在服务端才能完成渲染的动态内容,现在交给 JavaScript 就可以了。
+
+这么做的好处有很多:
+
+- 主要渲染工作在客户端,减少服务器的压力。简单的场景甚至只需要静态服务器。
+- 新内容获取只需要少量交互,而不是服务端发送渲染好的 HTML 页面。
+- 可以利用 JavaScript 在客户端修改和渲染任意内容,同时无需刷新整个页面。
+- ……
+
+当然同时也有一些缺点,目前最主要的痛点:
+
+- 首屏/白屏时间:由于 HTML 内容需要客户端 JavaScript 完成渲染,前端架构以及多种因素会影响首次内容的出现时间。
+- 爬虫/SEO:由于 HTML 内容需要客户端 JavaScript 完成渲染,早期的爬虫可能不会在浏览器环境下执行 JavaScript,这就导致了根本爬取不到 HTML 内容。
+
+> Google 的爬虫貌似已经可以爬取 SPA。
+
+## React 的路由
+
+React 是现代众多成熟的 SPA 应用框架之一,它自然也需要使用路由来切换对应的组件。React Router 是一个成熟的 React 路由组件库,它实现了许多功能,同时也非常还用。
+
+首先来看下本次路由的基本工作原理,本质上很简单,我们需要一个可以渲染任意组件的父组件 `` 或者叫 `` 之类的。然后再根据浏览器地址的变化,渲染注册路由时对应的组件即可。
+
+
+
+### 配置文件
+
+这里选择类似 Vue Router 的配置文件风格,而不是使用类似 `` 这样的 DOM 结构来注册路由。我们的目的是为了实现一个非常简单的迷你路由器,所以配置文件自然也就很简单:
+
+```ts
+import { lazy } from 'react';
+
+export default [
+ {
+ path: '/',
+ component: lazy(() => import('../pages/Home')),
+ },
+ {
+ path: '/about',
+ component: lazy(() => import('../pages/About')),
+ },
+];
+```
+
+一个 `path` 属性,对应了浏览器的地址,或者说 `location.pathname`;一个 `component` 属性,对应了到该地址时所需要渲染的组件。
+
+甚至还可以使用 `lazy()` 配合 `` 来实现代码分割。
+
+### 展示路由
+
+一个非常简单的路由就这样注册好了,接下来就是将对应的组件展示出来。我们都知道,JSX 最终会被 babel 转义为渲染函数,而一个组件的 `` 写法,基本等同于 `React.createElement(Home)`。[元素渲染 – React (reactjs.org)](https://zh-hans.reactjs.org/docs/rendering-elements.html#updating-the-rendered-element)
+
+所以动态的渲染指定的组件基本上也就很容易解决,接下来的思路也就很简单了,我们需要:
+
+- 一个状态:记录当前地址 `location.pathname`;
+- 根据当前地址在配置文件中寻找对应注册的组件,并将它渲染出来;
+- 一个副作用:当用户手动切换路由时,该组件需要重新渲染为对应注册的路由;
+
+先不考虑切换路由的问题,前两个基本上已经就实现了:
+
+```tsx
+// Router.tsx
+import React, { useState, Suspense } from 'react';
+import routes from './routes';
+
+const Router: React.FC = () => {
+ // 获取地址,并保存到状态中
+ const [path, setPath] = useState(location.pathname);
+ // 根据地址,寻找对应的组件
+ const element = routes.find((route) => route.path === path)?.component;
+
+ return (
+ <>
+ loading...
}>
+ {/* 使用 React.createElement() 渲染组件 */}
+ {element ? React.createElement(element) : void 0}
+
+ >
+ );
+};
+
+export default Router;
+```
+
+看上去很简单,事实上,确实很简单。
+
+现在我们直接从地址栏访问对应的路由,我们的 Router 组件应该就可以根据已经注册好的路由配置文件来找到正确的组件,并将其渲染出来了。
+
+
+
+### 切换路由
+
+到目前为止,我们实现了根据对应地址访问到对应组件的功能。这是一个路由必不可少的功能,但它还不能称得上是一个简单的路由器,因为它还无法处理用户手动切换的路由,也就是点击标签前往对应的页面。
+
+简单梳理一下我们需要实现的功能:
+
+- 一个 Link 组件,用于点击后导航到指定的地址;
+- 导航到地址后,还要修改浏览器的地址栏,并不真正的发送请求;
+- 通知 Router 组件,地址已经改变,重新渲染对应路由的组件;
+
+```tsx
+import React from 'react';
+
+interface Props {
+ to: string;
+ children?: React.ReactNode;
+}
+
+const RouterLink: React.FC = ({ to, children }: Props) => {
+ /**
+ * 处理点击事件
+ * 创建自定义事件监听器
+ * 并将 path 发送给 router
+ * @param e
+ */
+ const handleClick = (e: React.MouseEvent) => {
+ e.preventDefault();
+ const nowPath = location.pathname;
+ if (nowPath === to) return; // 原地跳转
+
+ history.pushState(null, '', to);
+ document.dispatchEvent(
+ new CustomEvent('route', {
+ detail: to,
+ })
+ );
+ };
+
+ return (
+ <>
+
+ {children}
+
+ >
+ );
+};
+
+export default RouterLink;
+```
+
+我们将 Link 组件实际的渲染为一个 `` 标签,这样就能模拟跳转到指定的导航了。这个组件它接收两个参数:`{ to, children }`,分别是前往的路由地址和标签的内容。标签的内容 children 就是展示在 a 标签内的文本。
+
+我们需要解决的第一个问题就是点击标签后跳转到指定的导航,这里其实需要分成两个部分,第一个部分是悄悄的修改浏览器地址栏,第二个部分则是通知 Router 组件去渲染对应的组件。
+
+修改地址栏很简单,利用到浏览器的 history API,可以很方便的修改 `pathName` 而不发送实际请求,这里只需要修改到第一个参数 to 即可:`history.pushState(null, '', to);`。
+
+但使用 `pushState()` 并不会发出任何通知,我们需要自己实现去通知 Router 组件地址已经变化。本来像利用第三方库来实现一个 发布/订阅 的模型的,但这样这个路由器可能就没有那么迷你了。最后发现利用 HTML 的 CustomEvent 可以实现一个简单的消息订阅与发布模型。
+
+HTML 自定义事件也很简单,我们在对应的 DOM 上 `dispatchEvent` 即可触发一个事件,而触发的这个事件,就是 `CustomEvent` 的实例,甚至还能传递一些信息。在这里,我们将路由地址传递过去。
+
+```ts
+document.dispatchEvent(
+ new CustomEvent('route', {
+ detail: to,
+ })
+);
+```
+
+而在 Router 组件中,只需要和以前一样在对应的 DOM 上去监听一个事件,这个事件就是刚刚发布的 `CustomEvent` 的实例。
+
+```ts
+// Router.tsx
+const handleRoute = (e: CustomEvent) => {
+ console.log(e.detail);
+ setPath(e.detail);
+};
+
+useEffect(() => {
+ /**
+ * 监听自定义 route 事件
+ * 并根据 path 修改路由
+ */
+ document.addEventListener('route', handleRoute as EventListener);
+
+ return () => {
+ // 清除副作用
+ document.removeEventListener('route', handleRoute as EventListener);
+ };
+}, []);
+```
+
+在 Router 组件中,根据接收到的变化,将新的地址保存到状态中,并触发组件重新渲染。
+
+这样一个最简单的 React 路由就做好了。
+
+## Vue 的路由
+
+与 React 同理,二者的路由切换都差不多,其主要思路还是使用自定义事件来订阅路由切换的请求。但 Vue 的具体实现与 React 还是有点不同的。
+
+### 配置文件
+
+路由的配置文件还是同理,不同的是,Vue 的异步组件需要在引入时同时引入一个 Loading 组件来实现 Loading 的效果:
+
+```ts
+import { defineAsyncComponent } from 'vue';
+import Loading from '../components/common/Loading.vue';
+
+export default [
+ {
+ path: '/',
+ name: 'Home',
+ component: defineAsyncComponent({
+ loader: () => import('../views/Home.vue'),
+ loadingComponent: Loading,
+ }),
+ },
+ {
+ path: '/about',
+ name: 'About',
+ component: defineAsyncComponent({
+ loader: () => import('../views/About.vue'),
+ loadingComponent: Loading,
+ }),
+ },
+];
+```
+
+### 展示路由
+
+同理,Vue 也是利用根据条件来渲染对应路由的组件。不同的是,我们可以使用模板语法来实现,也可以利用 `render()` 方法来直接渲染组件。
+
+首先来看看和 React 类似的 `render()` 方法。在 Vue3 中,使用 `setup()` 方法后,可以直接返回一个 `createVNode()` 的函数,这就是 `render()` 方法。所以可以直接写 TypeScript 文件。
+
+与 React 不同的地方在于,React 每次调用 `setPath(e.detail)` 存储状态时都会重新渲染组件,从而重新执行组件的函数,获取到对应的路由组件。
+
+但 Vue 不同,如果我们仅仅将路由名称 `e.detail` 保存到状态,但没有实际在 VNode 中使用的话,更新状态时不会重新渲染组件的,也就是说,不会获取到对应的路由组件。所以最佳的办法就是将整个路由组件保存到状态,可保存整个组件无疑太过庞大。好在 Vue3 给了我们另一种解决方法:`shallowRef()`。它会创建一个跟踪自身 `.value` 变化的 ref,但不会使其值也变成响应式的。
+
+```ts
+import { createVNode, defineComponent, shallowRef } from 'vue';
+import routes from './routes';
+
+export default defineComponent({
+ name: 'RouterView',
+ setup() {
+ let currentPath = window.location.pathname;
+ const component = shallowRef(
+ routes.find((item) => item.path === currentPath)?.component ??
+ 'Note found'
+ );
+
+ const handleEvent = (e: CustomEvent) => {
+ console.log(e.detail);
+ currentPath = e.detail;
+ component.value =
+ routes.find((item) => item.path === currentPath)?.component ??
+ 'Note found';
+ };
+
+ document.addEventListener('route', handleEvent as EventListener);
+
+ return () => createVNode(component.value);
+ },
+});
+```
+
+而使用模板语法主要是利用到了全局的 `component` 组件,其他部分与 `render()` 方法相同:
+
+```vue
+
+
+
+
+
+```
+
+## 总结
+
+如今的 JavaScript 做能做到的比以前更加强大,配合多种 HTML API,可以将曾经不可能实现的事变为现实。这个简单的迷你路由,主要的思路就是利用 HTML API 来通知 Router 组件该渲染哪个组件了。配合上 `lazy()` 方法,甚至还能实现代码分割。
+
+## Demo
+
+[DefectingCat/react-tiny-router: A tiny react router. (github.com)](https://github.com/DefectingCat/react-tiny-router)
diff --git a/public/images/p/create-a-mini-router-for-react/Web架构.drawio b/public/images/p/create-a-mini-router-for-react/Web架构.drawio
new file mode 100644
index 0000000..3657574
--- /dev/null
+++ b/public/images/p/create-a-mini-router-for-react/Web架构.drawio
@@ -0,0 +1 @@
+7Vxbc5s4FP41egyDLiB4BMfeTqbd7Yx3ptt96VCb2Gwdk8WkcfbX7xFIGITs2A7YaZxMHkAS0uGcT+cqjOjgbv1bFt3PP6XTeIGIPV0jeo0IwYwQJP7t6VPZ4mGnbJhlyVQO2jSMk/9i2WjL1odkGq8aA/M0XeTJfbNxki6X8SRvtEVZlj42h92mi+aq99EsbjWMJ9Gi3folmeZz2Ypdf9PxIU5mc7m0R3jZcRepwfJNVvNomj7WmugQ0UGWpnl5dbcexAvBPMWX8rnRlt6KsCxe5vs84F9//fY0+8a+/Pi4unlKsq/LH39fyVl+RosH+cJo6KBgiAK4cJHnoZCjIUdBiIIRGo5QOBCN0BU6yIMWDwW8aHGQ76PAU2OAPe4C6Aq/Z3A1E1dXsNCHPz99VEMGg/G4ur4Zmx8QS8HKYimGQlgtKNYcFXQBFRh5QKmPPIrCa0GFJNlFPvTaoivAgli48K/Fa5XCyJ+UhLP0YTmNBZMwrPk4T/J4fB9NRO8jYBra5vndQnavfsT5ZC5vJO/iLI/XW4WCK1HDHonTuzjPnmCIesCW6JDbA1N5/1gDmyvb5jWc+bItkvieVVNvIAAXEgUHIIIYEAHMBBmV7A0EPytpv4iZs0W0EuTbOmOrrWJ3w2XKm1wmntvmMjFw2e2Ly9TMZaeArCt4DXsQuBwGyKevC7IueR6yvoGX1OmJl8zISwCnBOoQeeHrZmFl7c7FQsfIQm8olLvUqW5Hm753XvrkvLx0DbwsrWTJwhCFw9fGQtxgIeX+eVnotVmoMyxeTgPh3sHdRJiRZNLkUbxO8r+kBRHXX8W15ci763Wt6/pJ3SyB+L/qN7WnxO3mseJOPWeQRzxtuZXPSqPGbcfAbdWWxYsoT342pzeJQK7wOU1g4R26x7G4JshV+pBNYvlg3aV8Zi5M23PlUTaL89ZcILzoqTbsXgxYbSfb8XWyXYs4O6nTH8FUfwQuSjo2UK3kcTx6fYMCAN81FPp0h5GX/u210BYwMiw170j4se2n9O0Amz/XlESepT/iQbpIM2hZpksYGd4mi4XWFC2S2VLsIoBnDO2hUCUJBECB7LhLplOxjFErbfTWlo3wQsXEmxLkuKWYPMNWIX25bdjgt71rpp40E7Wx1ZFiAmr0qXrSS9QmByklQhrj+9FI2OAiv8O2J9gyiYAXg5YSbaKeIAsEW4dZUqDMOoEhxYaw5FDYNlIJvxSuOG45V8fhitt+U+XgflQhZ2aCt9LFWp7oCTBlCM8uCVNEsz6ap7Q3prAWMtraRJ1hykzwdkwRI139YopfNqY0KOhT7I8p3S5pE3WGKTPB2zGFjXT1iylTFFlk4so0kggMsQgVIU4UlRmOfCaqITJbV9ZNIFSkRfUGF5EmjAmQx/bIjl5GOEmcM4eTauKGkAtJikJaOw1QybYmyXY6Acb4YZFXCEVqQRbdZCmtlLsramHhSJb4YGpi34z/+P1CcUCVI3I2HBjKsC8yIFWwZvk+bwRslKpgbkvIBjef4yyBFxNi+/XMEfN8y2WaE8AtVfF7cVBme3sZpa7sgEJCd9BQMTk+LCZXkLqyLRsCijqmXM5Ogyl460JsO/glQ5tSKrv4ek6MOq2UwZFuuKMVtqmznxveGTpNdez38yPnOz/CVKrvfOdHug6GlOLBDZ1D9s08Wq7L6pqu0F/PKjtxdyqVpY61vWqVpR+hwTqA9lVZzNGyPl4/mQN9HexQDds9RG2kg8p1p9bathiuO4AF+umrsdZkT+if1aOkWl6C8COhT11tIj2g6Ar6elmenyCxSgwJi26gD8p+o7AFiLFlY+8YDV6ZEmUCKnOCBTPfclhEXNvifu3PafoNBFt+vf9IhPvMcvzWNFWczSx7+yIdoZ+2Xo30j35qyOR0FMEzQmtQBeg6bxyqDFOLuxpyOGxRu/o7MsNMNAXMGOtHAWNmXKdfCHaeRNooYF6qx7oC5id0oV+3a0xcrT7A6XHwZI6Wl3RIP/DU1lEE70sXoycogNDeEl8iKeo23WnPwy/xJ2ACn51HSXfoZJfJnNfiZbvH5sR0L9vZMyd28C7i2q7wTxBgUnPCTVYDfQTBtIdNGS4uqkHgcR3wLc/bLP7Qfb64sg047636Q80fsGwTk7igskucHGYy4+oNVDmQVQXCMl36bArzbUq6yiwpSXsGSdOTStr0nU1793IUDMR/mbGGxl2p61q9/7I3NtESflVRpS5udlJxmz4Fau/eQrignYVwXQmAYvfeRD+j8SRL7vNLEaGWZGZqivPtWEM1Q4jQLj7mKIyq+GKjpWGVCIN/orVh116IOPVw2zEUp067I82nqrQdKQ/DCEU6RP6osK9cHJMyHLeR5cedVc8SJryGF3VaC1aq7fFiXqELSvs/Mn36+0aRom18x6C7yUmdMgVLw1dcQUMDyINYRsM8Kmw26AQiS9xijH+xn3NhLcRzTK63KcTsT8qH5cwkj7ce2y1kpH4mhGgCQoROo9i7nbSkCT3uxIu/3/5qmdrWBysdheytiXo+xqIOjTdgUJ4muU0LQjd4cP99SFXH1ar4vRpxxhLT+/WmU51AKbY6L3S/K47ahq68AJUhFwCCyzXkE5ehCmhT4IS3PQN+UkVg+rCzMwS4KkB7R8A2BFDGz4wAUxam7envMPjVITKQb9G7K4njiePXZe7GDwUeNjO3szkXmsDBdrOM5hucQuZ0ghG43fwSV2lWNr9nRof/Aw==
\ No newline at end of file
diff --git a/public/images/p/create-a-mini-router-for-react/Web架构.svg b/public/images/p/create-a-mini-router-for-react/Web架构.svg
new file mode 100644
index 0000000..52397fa
--- /dev/null
+++ b/public/images/p/create-a-mini-router-for-react/Web架构.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/public/images/p/create-a-mini-router-for-react/image-20210823154009498.webp b/public/images/p/create-a-mini-router-for-react/image-20210823154009498.webp
new file mode 100644
index 0000000..fb95fb1
Binary files /dev/null and b/public/images/p/create-a-mini-router-for-react/image-20210823154009498.webp differ
diff --git a/public/images/p/create-a-mini-router-for-react/router.png b/public/images/p/create-a-mini-router-for-react/router.png
new file mode 100644
index 0000000..baead8e
Binary files /dev/null and b/public/images/p/create-a-mini-router-for-react/router.png differ
diff --git a/public/images/p/create-a-mini-router-for-react/router.psd b/public/images/p/create-a-mini-router-for-react/router.psd
new file mode 100644
index 0000000..1e08910
Binary files /dev/null and b/public/images/p/create-a-mini-router-for-react/router.psd differ
diff --git a/public/images/p/create-a-mini-router-for-react/router.webp b/public/images/p/create-a-mini-router-for-react/router.webp
new file mode 100644
index 0000000..851891a
Binary files /dev/null and b/public/images/p/create-a-mini-router-for-react/router.webp differ
diff --git a/public/images/p/create-a-mini-router-for-react/迷你路由器.drawio b/public/images/p/create-a-mini-router-for-react/迷你路由器.drawio
new file mode 100644
index 0000000..1963734
--- /dev/null
+++ b/public/images/p/create-a-mini-router-for-react/迷你路由器.drawio
@@ -0,0 +1 @@
+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
\ No newline at end of file
diff --git a/public/images/p/create-a-mini-router-for-react/迷你路由器.svg b/public/images/p/create-a-mini-router-for-react/迷你路由器.svg
new file mode 100644
index 0000000..d77da23
--- /dev/null
+++ b/public/images/p/create-a-mini-router-for-react/迷你路由器.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/styles/rua.css b/styles/rua.css
index 2b45049..90fda2c 100644
--- a/styles/rua.css
+++ b/styles/rua.css
@@ -1,118 +1,118 @@
#article {
- --color-prettylights-syntax-comment: #6e7781;
- --color-prettylights-syntax-constant: #0550ae;
- --color-prettylights-syntax-entity: #8250df;
- --color-prettylights-syntax-storage-modifier-import: #24292f;
- --color-prettylights-syntax-entity-tag: #116329;
- --color-prettylights-syntax-keyword: #cf222e;
- --color-prettylights-syntax-string: #0a3069;
- --color-prettylights-syntax-variable: #953800;
- --color-prettylights-syntax-brackethighlighter-unmatched: #82071e;
- --color-prettylights-syntax-invalid-illegal-text: #f6f8fa;
- --color-prettylights-syntax-invalid-illegal-bg: #82071e;
- --color-prettylights-syntax-carriage-return-text: #f6f8fa;
- --color-prettylights-syntax-carriage-return-bg: #cf222e;
- --color-prettylights-syntax-string-regexp: #116329;
- --color-prettylights-syntax-markup-list: #3b2300;
- --color-prettylights-syntax-markup-heading: #0550ae;
- --color-prettylights-syntax-markup-italic: #24292f;
- --color-prettylights-syntax-markup-bold: #24292f;
- --color-prettylights-syntax-markup-deleted-text: #82071e;
- --color-prettylights-syntax-markup-deleted-bg: #ffebe9;
- --color-prettylights-syntax-markup-inserted-text: #116329;
- --color-prettylights-syntax-markup-inserted-bg: #dafbe1;
- --color-prettylights-syntax-markup-changed-text: #953800;
- --color-prettylights-syntax-markup-changed-bg: #ffd8b5;
- --color-prettylights-syntax-markup-ignored-text: #eaeef2;
- --color-prettylights-syntax-markup-ignored-bg: #0550ae;
- --color-prettylights-syntax-meta-diff-range: #8250df;
- --color-prettylights-syntax-brackethighlighter-angle: #57606a;
- --color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f;
- --color-prettylights-syntax-constant-other-reference-link: #0a3069;
- --color-fg-default: #24292f;
- --color-fg-muted: #57606a;
- --color-fg-subtle: #6e7781;
- --color-canvas-default: #ffffff;
- --color-canvas-subtle: #f6f8fa;
- --color-border-default: #d0d7de;
- --color-border-muted: hsla(210, 18%, 87%, 1);
- --color-neutral-muted: rgba(175, 184, 193, 0.2);
- --color-accent-fg: #0969da;
- --color-accent-emphasis: #0969da;
- --color-attention-subtle: #fff8c5;
- --color-danger-fg: #cf222e;
+ --color-prettylights-syntax-comment: #6e7781;
+ --color-prettylights-syntax-constant: #0550ae;
+ --color-prettylights-syntax-entity: #8250df;
+ --color-prettylights-syntax-storage-modifier-import: #24292f;
+ --color-prettylights-syntax-entity-tag: #116329;
+ --color-prettylights-syntax-keyword: #cf222e;
+ --color-prettylights-syntax-string: #0a3069;
+ --color-prettylights-syntax-variable: #953800;
+ --color-prettylights-syntax-brackethighlighter-unmatched: #82071e;
+ --color-prettylights-syntax-invalid-illegal-text: #f6f8fa;
+ --color-prettylights-syntax-invalid-illegal-bg: #82071e;
+ --color-prettylights-syntax-carriage-return-text: #f6f8fa;
+ --color-prettylights-syntax-carriage-return-bg: #cf222e;
+ --color-prettylights-syntax-string-regexp: #116329;
+ --color-prettylights-syntax-markup-list: #3b2300;
+ --color-prettylights-syntax-markup-heading: #0550ae;
+ --color-prettylights-syntax-markup-italic: #24292f;
+ --color-prettylights-syntax-markup-bold: #24292f;
+ --color-prettylights-syntax-markup-deleted-text: #82071e;
+ --color-prettylights-syntax-markup-deleted-bg: #ffebe9;
+ --color-prettylights-syntax-markup-inserted-text: #116329;
+ --color-prettylights-syntax-markup-inserted-bg: #dafbe1;
+ --color-prettylights-syntax-markup-changed-text: #953800;
+ --color-prettylights-syntax-markup-changed-bg: #ffd8b5;
+ --color-prettylights-syntax-markup-ignored-text: #eaeef2;
+ --color-prettylights-syntax-markup-ignored-bg: #0550ae;
+ --color-prettylights-syntax-meta-diff-range: #8250df;
+ --color-prettylights-syntax-brackethighlighter-angle: #57606a;
+ --color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f;
+ --color-prettylights-syntax-constant-other-reference-link: #0a3069;
+ --color-fg-default: #24292f;
+ --color-fg-muted: #57606a;
+ --color-fg-subtle: #6e7781;
+ --color-canvas-default: #ffffff;
+ --color-canvas-subtle: #f6f8fa;
+ --color-border-default: #d0d7de;
+ --color-border-muted: hsla(210, 18%, 87%, 1);
+ --color-neutral-muted: rgba(175, 184, 193, 0.2);
+ --color-accent-fg: #0969da;
+ --color-accent-emphasis: #0969da;
+ --color-attention-subtle: #fff8c5;
+ --color-danger-fg: #cf222e;
}
.dark #article {
- --color-prettylights-syntax-comment: #8b949e;
- --color-prettylights-syntax-constant: #79c0ff;
- --color-prettylights-syntax-entity: #d2a8ff;
- --color-prettylights-syntax-storage-modifier-import: #c9d1d9;
- --color-prettylights-syntax-entity-tag: #7ee787;
- --color-prettylights-syntax-keyword: #ff7b72;
- --color-prettylights-syntax-string: #a5d6ff;
- --color-prettylights-syntax-variable: #ffa657;
- --color-prettylights-syntax-brackethighlighter-unmatched: #f85149;
- --color-prettylights-syntax-invalid-illegal-text: #f0f6fc;
- --color-prettylights-syntax-invalid-illegal-bg: #8e1519;
- --color-prettylights-syntax-carriage-return-text: #f0f6fc;
- --color-prettylights-syntax-carriage-return-bg: #b62324;
- --color-prettylights-syntax-string-regexp: #7ee787;
- --color-prettylights-syntax-markup-list: #f2cc60;
- --color-prettylights-syntax-markup-heading: #1f6feb;
- --color-prettylights-syntax-markup-italic: #c9d1d9;
- --color-prettylights-syntax-markup-bold: #c9d1d9;
- --color-prettylights-syntax-markup-deleted-text: #ffdcd7;
- --color-prettylights-syntax-markup-deleted-bg: #67060c;
- --color-prettylights-syntax-markup-inserted-text: #aff5b4;
- --color-prettylights-syntax-markup-inserted-bg: #033a16;
- --color-prettylights-syntax-markup-changed-text: #ffdfb6;
- --color-prettylights-syntax-markup-changed-bg: #5a1e02;
- --color-prettylights-syntax-markup-ignored-text: #c9d1d9;
- --color-prettylights-syntax-markup-ignored-bg: #1158c7;
- --color-prettylights-syntax-meta-diff-range: #d2a8ff;
- --color-prettylights-syntax-brackethighlighter-angle: #8b949e;
- --color-prettylights-syntax-sublimelinter-gutter-mark: #484f58;
- --color-prettylights-syntax-constant-other-reference-link: #a5d6ff;
- --color-fg-default: #c9d1d9;
- --color-fg-muted: #8b949e;
- --color-fg-subtle: #484f58;
- --color-canvas-default: #0d1117;
- --color-canvas-subtle: #161b22;
- --color-border-default: #30363d;
- --color-border-muted: #21262d;
- --color-neutral-muted: rgba(110, 118, 129, 0.4);
- --color-accent-fg: #58a6ff;
- --color-accent-emphasis: #1f6feb;
- --color-attention-subtle: rgba(187, 128, 9, 0.15);
- --color-danger-fg: #f85149;
+ --color-prettylights-syntax-comment: #8b949e;
+ --color-prettylights-syntax-constant: #79c0ff;
+ --color-prettylights-syntax-entity: #d2a8ff;
+ --color-prettylights-syntax-storage-modifier-import: #c9d1d9;
+ --color-prettylights-syntax-entity-tag: #7ee787;
+ --color-prettylights-syntax-keyword: #ff7b72;
+ --color-prettylights-syntax-string: #a5d6ff;
+ --color-prettylights-syntax-variable: #ffa657;
+ --color-prettylights-syntax-brackethighlighter-unmatched: #f85149;
+ --color-prettylights-syntax-invalid-illegal-text: #f0f6fc;
+ --color-prettylights-syntax-invalid-illegal-bg: #8e1519;
+ --color-prettylights-syntax-carriage-return-text: #f0f6fc;
+ --color-prettylights-syntax-carriage-return-bg: #b62324;
+ --color-prettylights-syntax-string-regexp: #7ee787;
+ --color-prettylights-syntax-markup-list: #f2cc60;
+ --color-prettylights-syntax-markup-heading: #1f6feb;
+ --color-prettylights-syntax-markup-italic: #c9d1d9;
+ --color-prettylights-syntax-markup-bold: #c9d1d9;
+ --color-prettylights-syntax-markup-deleted-text: #ffdcd7;
+ --color-prettylights-syntax-markup-deleted-bg: #67060c;
+ --color-prettylights-syntax-markup-inserted-text: #aff5b4;
+ --color-prettylights-syntax-markup-inserted-bg: #033a16;
+ --color-prettylights-syntax-markup-changed-text: #ffdfb6;
+ --color-prettylights-syntax-markup-changed-bg: #5a1e02;
+ --color-prettylights-syntax-markup-ignored-text: #c9d1d9;
+ --color-prettylights-syntax-markup-ignored-bg: #1158c7;
+ --color-prettylights-syntax-meta-diff-range: #d2a8ff;
+ --color-prettylights-syntax-brackethighlighter-angle: #8b949e;
+ --color-prettylights-syntax-sublimelinter-gutter-mark: #484f58;
+ --color-prettylights-syntax-constant-other-reference-link: #a5d6ff;
+ --color-fg-default: #c9d1d9;
+ --color-fg-muted: #8b949e;
+ --color-fg-subtle: #484f58;
+ --color-canvas-default: #0d1117;
+ --color-canvas-subtle: #161b22;
+ --color-border-default: #30363d;
+ --color-border-muted: #21262d;
+ --color-neutral-muted: rgba(110, 118, 129, 0.4);
+ --color-accent-fg: #58a6ff;
+ --color-accent-emphasis: #1f6feb;
+ --color-attention-subtle: rgba(187, 128, 9, 0.15);
+ --color-danger-fg: #f85149;
}
#article {
- @apply text-lg leading-10;
+ @apply text-lg leading-10;
}
#article .toc {
- padding-left: 0.8em;
- @apply my-4;
+ padding-left: 0.8em;
+ @apply my-4;
}
#article .toc li {
- list-style-type: none;
+ list-style-type: none;
}
#article h1 {
- font-weight: bold;
- text-align: center;
- @apply text-gray-800 dark:text-gray-200;
- @apply mt-8 text-5xl font-Barlow;
+ font-weight: bold;
+ text-align: center;
+ @apply text-gray-800 dark:text-gray-200;
+ @apply mt-8 text-5xl font-Barlow;
}
#article time {
- display: block;
- text-align: center;
- @apply text-gray-400 dark:text-gray-600;
- @apply mt-8 mb-20;
+ display: block;
+ text-align: center;
+ @apply text-gray-400 dark:text-gray-600;
+ @apply mt-8 mb-20;
}
#article h2,
@@ -120,7 +120,7 @@ h3,
h4,
h5,
h6 {
- font-weight: bold;
+ font-weight: bold;
}
#article h2:hover::before,
@@ -128,243 +128,243 @@ h3:hover::before,
h4:hover::before,
h5:hover::before,
h6:hover::before {
- content: '#';
- left: -1.7rem;
- @apply absolute text-gray-400;
+ content: '#';
+ left: -1.7rem;
+ @apply absolute text-gray-400;
}
#article h2 {
- @apply relative text-3xl;
- @apply text-gray-700 dark:text-gray-200;
- @apply mt-8 mb-2;
+ @apply relative text-3xl;
+ @apply text-gray-700 dark:text-gray-200;
+ @apply mt-8 mb-2;
}
#article h3 {
- @apply relative text-2xl;
- @apply mt-6 mb-2;
+ @apply relative text-2xl;
+ @apply mt-6 mb-2;
}
#article h4 {
- @apply relative text-xl;
- @apply mt-6 mb-2;
+ @apply relative text-xl;
+ @apply mt-6 mb-2;
}
#article h5 {
- @apply relative;
- @apply mt-4 mb-2;
+ @apply relative;
+ @apply mt-4 mb-2;
}
#article h6 {
- @apply relative;
- @apply mt-2 mb-2;
+ @apply relative;
+ @apply mt-2 mb-2;
}
#article table {
- border-spacing: 0;
- border-collapse: collapse;
- display: block;
- width: max-content;
- max-width: 100%;
- overflow: auto;
- margin-top: 0;
- margin-bottom: 16px;
+ border-spacing: 0;
+ border-collapse: collapse;
+ display: block;
+ width: max-content;
+ max-width: 100%;
+ overflow: auto;
+ margin-top: 0;
+ margin-bottom: 16px;
}
#article td,
#article th {
- padding: 0;
+ padding: 0;
}
#article details summary {
- cursor: pointer;
+ cursor: pointer;
}
#article table th {
- font-weight: 600;
+ font-weight: 600;
}
#article table th,
#article table td {
- padding: 6px 13px;
+ padding: 6px 13px;
}
#article thead tr:first-child {
- @apply border-t-0;
+ @apply border-t-0;
}
#article table tr {
- background-color: var(--color-canvas-default);
- border-top: 1px solid var(--color-border-muted);
+ background-color: var(--color-canvas-default);
+ border-top: 1px solid var(--color-border-muted);
}
#article table tr:nth-child(2n) {
- background-color: var(--color-canvas-subtle);
+ background-color: var(--color-canvas-subtle);
}
#article table img {
- background-color: transparent;
+ background-color: transparent;
}
#article blockquote {
- margin: 0;
- padding: 0 1em;
- color: var(--color-fg-muted);
- border-left: 0.25em solid var(--color-border-default);
+ margin: 0;
+ padding: 0 1em;
+ color: var(--color-fg-muted);
+ border-left: 0.25em solid var(--color-border-default);
}
#article kbd {
- display: inline-block;
- padding: 3px 5px;
- font: 11px ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
+ display: inline-block;
+ padding: 3px 5px;
+ font: 11px ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
Liberation Mono, monospace;
- line-height: 10px;
- color: var(--color-fg-default);
- vertical-align: middle;
- background-color: var(--color-canvas-subtle);
- border: solid 1px var(--color-neutral-muted);
- border-bottom-color: var(--color-neutral-muted);
- border-radius: 6px;
- box-shadow: inset 0 -1px 0 var(--color-neutral-muted);
+ line-height: 10px;
+ color: var(--color-fg-default);
+ vertical-align: middle;
+ background-color: var(--color-canvas-subtle);
+ border: solid 1px var(--color-neutral-muted);
+ border-bottom-color: var(--color-neutral-muted);
+ border-radius: 6px;
+ box-shadow: inset 0 -1px 0 var(--color-neutral-muted);
}
#article hr {
- box-sizing: content-box;
- overflow: hidden;
- background: transparent;
- border-bottom: 1px solid var(--color-border-muted);
- height: 0.25em;
- padding: 0;
- margin: 24px 0;
- background-color: var(--color-border-default);
- border: 0;
+ box-sizing: content-box;
+ overflow: hidden;
+ background: transparent;
+ border-bottom: 1px solid var(--color-border-muted);
+ height: 0.25em;
+ padding: 0;
+ margin: 24px 0;
+ background-color: var(--color-border-default);
+ border: 0;
}
#article hr::before {
- display: table;
- content: '';
+ display: table;
+ content: '';
}
#article hr::after {
- display: table;
- clear: both;
- content: '';
+ display: table;
+ clear: both;
+ content: '';
}
#article code,
#article tt {
- padding: 0.2em 0.4em;
- margin: 0;
- font-size: 85%;
- background-color: var(--color-neutral-muted);
- border-radius: 6px;
+ padding: 0.2em 0.4em;
+ margin: 0;
+ font-size: 85%;
+ background-color: var(--color-neutral-muted);
+ border-radius: 6px;
}
#article pre > code {
- padding: 0;
- margin: 0;
- word-break: normal;
- white-space: pre;
- background: transparent;
- border: 0;
+ padding: 0;
+ margin: 0;
+ word-break: normal;
+ white-space: pre;
+ background: transparent;
+ border: 0;
}
#article code,
#article kbd,
#article pre,
#article samp {
- /* font-family: monospace, monospace; */
- font-size: 16px;
+ /* font-family: monospace, monospace; */
+ font-size: 16px;
}
#article mark {
- background-color: var(--color-attention-subtle);
- color: var(--color-text-primary);
+ background-color: var(--color-attention-subtle);
+ color: var(--color-text-primary);
}
#article sub,
#article sup {
- font-size: 75%;
- line-height: 0;
- position: relative;
- vertical-align: baseline;
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
}
#article sub {
- bottom: -0.25em;
+ bottom: -0.25em;
}
#article sup {
- top: -0.5em;
+ top: -0.5em;
}
#article ol li {
- list-style-type: auto;
+ list-style-type: auto;
}
#article ul li {
- list-style-type: initial;
+ list-style-type: initial;
}
#article ul.no-list,
#article ol.no-list {
- padding: 0;
- list-style-type: none;
+ padding: 0;
+ list-style-type: none;
}
#article ol[type='1'] {
- list-style-type: decimal;
+ list-style-type: decimal;
}
#article ol[type='a'] {
- list-style-type: lower-alpha;
+ list-style-type: lower-alpha;
}
#article ol[type='i'] {
- list-style-type: lower-roman;
+ list-style-type: lower-roman;
}
#article div > ol:not([type]) {
- list-style-type: decimal;
+ list-style-type: decimal;
}
#article ul,
#article ol {
- margin-top: 0;
- margin-bottom: 0;
- padding-left: 2em;
+ margin-top: 0;
+ margin-bottom: 0;
+ padding-left: 2em;
}
#article ol ol,
#article ul ol {
- list-style-type: lower-roman;
+ list-style-type: lower-roman;
}
#article ul ul ol,
#article ul ol ol,
#article ol ul ol,
#article ol ol ol {
- list-style-type: lower-alpha;
+ list-style-type: lower-alpha;
}
#article dd {
- margin-left: 0;
+ margin-left: 0;
}
#article .sp-layout > .sp-stack {
- height: 400px;
+ height: 400px;
}
@media screen and (max-width: 768px) {
- #article .sp-layout > .sp-stack {
- height: auto;
- }
+ #article .sp-layout > .sp-stack {
+ height: auto;
+ }
}
#article img {
- border-radius: 6px;
+ border-radius: 6px;
}
#article .cm-editor .cm-line {
- font-size: 15px;
- font-family: 'JetBrains Mono', -apple-system, monospace;
+ font-size: 15px;
+ font-family: 'JetBrains Mono', -apple-system, monospace;
}