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架构 + +在旧架构中,动态内容由 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 组件应该就可以根据已经注册好的路由配置文件来找到正确的组件,并将其渲染出来了。 + +image-20210823154009498 + +### 切换路由 + +到目前为止,我们实现了根据对应地址访问到对应组件的功能。这是一个路由必不可少的功能,但它还不能称得上是一个简单的路由器,因为它还无法处理用户手动切换的路由,也就是点击标签前往对应的页面。 + +简单梳理一下我们需要实现的功能: + +- 一个 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 @@ +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 \ 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 @@ + + +
客户端(浏览器)
- HTML,CSS,JS
- 每个请求都刷新页面
客户端(浏览器)- HTML,CSS,JS...
服务器
服务器
数据库
数据库
模型
模型
控制器
控制器
视图
视图
从数据库中获取数据
从数据库中获取数据
分发路由到正确的模型
分发路由到正确的模型
用获取到的数据创建 HTML 模板或 JSON
用获取到的数据创建 HTML 模板或 JSON
客户端(浏览器)
- HTML,CSS,JS
- 每个请求都刷新页面
客户端(浏览器)- HTML,CSS,JS...
发送请求给服务器
发送请求给服务器
服务器响应完整的HTML页面
服务器响应完整的HTML页面
发送第一个请求到服务器
发送第一个请求到服务器
响应主要的JavaScript
响应主要的JavaScript
后续页面的Ajax请求
后续页面的Ajax请求
响应 JSON原始数据,客户端继续由 JavaScript 渲染
响应 JSON原始数据,客户端继续由 JavaScript 渲染
今后的请求只针对数据
今后的请求只针对数据
旧架构
旧架构
新架构
新架构
后续的请求都是服务器返回的完整页面
后续的请求都是服务器返回的完整页面
Viewer does not support full SVG 1.1
\ 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 @@ + + +
Router 组件
Router 组件
根据路由渲染的子组件
根据路由渲染的子组件
React.createElement(element)
React.createElement(element)
  • 状态
  • 自定义事件
状态自定义事件
重新渲染
重新渲染
Home
Home
About
About
RouterLink组件
RouterLink组件
点击导航时
点击导航时
通知变化
通知变化
https://demo.rua.plus/about
https://demo.rua.plus/about
修改地址栏
修改地址栏
监听变化,根据地址重新渲染子组件
监听变化,根据地址重新渲染子组件
地址栏
地址栏
Viewer does not support full SVG 1.1
\ 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; }