跳到主要内容

full-bleed 布局

· 阅读需 1 分钟
TransonQ
Front-end Engineer

full-bleed图例​

full-bleed

代码​

<main class="wrapper">
<h1>Some Heading</h1>
<p>Some content and stuff</p>

<div class="full-bleed">
<img src="https://picsum.photos/1200/300"/>
</div>

<p>Some content and stuff</p>
</main>
.wrapper {
display: grid;
/** 定义 grid 为三列 */
grid-template-columns:
1fr
min(65ch, calc(100% - 64px))
1fr;
height: 100%;
}
.wrapper > * {
/** 每个子元素都放在第二列的格子中;
第一列和第三列的格子是空的;
这样就达到了整体居中的效果.
*/
grid-column: 2;
}

.full-bleed {
width: 100%;
/** 从第1 到第3 列,不包含第四列,因为这个写法是按照索引的,左闭右开 */
grid-column: 1 / 4;

display:grid;
place-items:center;
}
.full-bleed > img{
width:100%;
max-width:900px;
min-width:500px;
}

效果图

文章来源:Full-Bleed Layout Using CSS Grid

pnpm工作区实现monorepo+shadcn项目

· 阅读需 13 分钟
TransonQ
Front-end Engineer

创建工作区​

推荐使用 pnpm,本文章 pnpm 版本为:v8.15.7

mkdir growth-apps

cd growth-apps

pnpm init

可以得到如下的项目结构,然后修改 package.json,如下所示 init

创建 pnpm-workspace.yaml,并且更新文件内容如下:

image-20240419154935735

创建 .npmrc 文件,更新文件内容如下:

auto-install-peers = true
enable-pre-post-scripts=true

创建 .gitignore 文件,更新如下内容:

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

image-20240419155206736

初始化工作区 git:

git init

创建工作区文件夹:/apps /packages :

├───📁 apps/
├───📁 packages/
├───📄 .gitignore
├───📄 .npmrc
├───📄 package.json
└───📄 pnpm-workspace.yaml

先准备 packages 中的公共目录,参考设计如下:

  • /ui
  • /utils
  • /config-ts
  • /config-tailwind

里程碑:已经得到一个 monorepo 基本的架构了。

apps​

这里以常见的 vite-react 创建一个 SPA 项目为例:

cd apps

pnpx create-vite

---------------
pnpx create-vite
Packages: +1
+
Progress: resolved 1, reused 1, downloaded 0, added 1, done
✔ Project name: … vite-react
✔ Select a framework: › React
✔ Select a variant: › TypeScript
---------------

这里可以将这个 vite-react 项目作为模板项目,后续创建类似的项目以此为样板直接快速搭建就好了。所以为此我们需要做一点小改动:

  • 我们将 /apps/vite-react/package.json 中的 devDependencies 和 dependencies 剪切到顶层的 /package.json 中。
  • 执行 pnpm i。
  • 接下来在 vite-react 项目的终端中执行 pnpm run dev 可以看到已经运行了一个初始项目。
pnpm run dev
--------
> vite
Port 5173 is in use, trying another one...

VITE v5.2.9 ready in 130 ms

➜ Local: http://localhost:5174/
➜ Network: use --host to expose
➜ press h + enter to show help
--------

注意:因为这个案例需求是,可能会创建多个 SPA 项目,将基础依赖提取到顶层 package.json 是为了保证所有项目使用同一个依赖版本。方便管理。

packages​

pnpm init

需要修改一下 package.json,根据 monorepo 工作区的约定,我们约定公共 ui 库的名称前缀为 @mono/。

{
"name": "@mono/ui",
"version": "1.0.0",
"description": "",
"private": true,
"scripts": {},
"keywords": []
}

注意:记得每一次修改库名之后,执行一下:pnpm i ,你会在终端中看到类似于这样的提示:Scope: all 2 workspace projects 。这说明 pnpm 识别到了你的工作区中的项目。这一步是必须的,后续类似的操作,也遵循这一原则。

然后为 packages 中的项目都初始化,然后都给项目名字加上前缀:@mono/utils , @mono/config-ts ,@mono/config-tailwind 。文章后续默认已经设置好了各个项目的 package.json。

配置 @mono/config-ts​

monorepo-ts

一个最快速的实践方式是,直接将vite-react 项目中的配置文件复制到公共配置项目@mono/config-ts中。

config-ts

├───📄 package.json
├───📄 tsconfig.json
└───📄 tsconfig.node.json

config-ts/package.json

{
"name": "@mono/config-ts",
"version": "1.0.0",
"description": "",
"private": true,
"exports": {
".": "./tsconfig.json"
}
}

完成以上步骤后,需要在 apps/vite-react工作区中安装 @mono/config-ts

{
"name": "vite-react",
...
"devDependencies": {
"@mono/config-ts": "workspace:*"
}
}

然后可能需要重启一下 ts 服务器,才会有代码提示。接下来通过在apps/vite-react的 ts 配置中引入并继承公共的配置就好了:

{
"extends": "@mono/config-ts",
"include": [
"src"
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"~/*": [
"src/*"
]
},
}
}

里程碑:已经完成了复用 ts 配置的操作啦。

上面还配置了路径别名,根据自己习惯的配置即可,记得在 vite.config.ts 中配置路径别名,还需要安装node 的类型文件:

pnpm i -wD @types/node
// `vite.config.ts`
import react from "@vitejs/plugin-react"
import { resolve } from "path"
import { defineConfig } from "vite"

// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"~": resolve(__dirname, "src"),
},
},
})

配置 @mono/ui (shadcn)​

monorepo-ui

还记得前面配置的 @mono/ui 吗?现在准备开始配置项目应该如何巧妙的复用 shadcn 组件,由于 shadcn 具有极高的灵活性,同时它又是通过拷贝组件的到本地仓库的模式进行开发,如果 monorepo 多个项目每一个项目都去维护各自的 UI 组件,工作量会大大提高。所以这里主要分享一下如何在 monorepo 中复用组件。

经过前面的配置,monorepo项目目录大概如下:

.
├───📁 apps/
│ └───📁 vite-react/
│ └───...
├───📁 packages/
│ ├───📁 config-tailwind/
│ │ └───📄 package.json
│ ├───📁 config-ts/
│ │ ├───📄 package.json
│ │ ├───📄 tsconfig.json
│ │ └───📄 tsconfig.node.json
│ ├───📁 ui/
│ │ └───📄 package.json
│ └───📁 utils/
│ └───📄 package.json
├───📄 .gitignore
├───📄 .npmrc
├───📄 package.json
├───📄 pnpm-lock.yaml
└───📄 pnpm-workspace.yaml

找到 @mono/ui也就是 /packages/ui ,现在将会在这里通过 shadcn/ui 的官方教程,开始初始化组件的存放位置。

注意:这里我们不需要初始化为 vite 项目,其实只需要一个 ts 配置就好了。

首先需要在@mono/ui中安装共享 ts 配置的公共库:@mono/config-ts

cd /packages/ui

pnpm i @mono/config-ts @mono/utils

------
{
"name": "@mono/ui",
"version": "1.0.0",
"description": "",
"scripts": {},
"keywords": [],
"dependencies": {
"@mono/config-ts": "workspace:*",
"@mono/utils": "workspace:*"
}
}
------

创建一个 tsconfig.json 文件

{
"extends": "@mono/config-ts",
"include": [
"src"
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
]
},
}
}

紧接着开始下一步,开始初始化 shadcn 配置项

pnpm install -wD tailwindcss postcss autoprefixer

pnpx tailwindcss init -p

pnpx shadcn-ui@latest init
✔ Would you like to use TypeScript (recommended)? … no / yes
✔ Which style would you like to use? › Default
✔ Which color would you like to use as base color? › Slate
✔ Where is your global CSS file? … src/gloabal.css
✔ Would you like to use CSS variables for colors? … no / yes
✔ Are you using a custom tailwind prefix eg. tw-? (Leave blank if not) …
✔ Where is your tailwind.config.js located? … tailwind.config.js
✔ Configure the import alias for components: … src
✔ Configure the import alias for utils: … @mono/utils
✔ Are you using React Server Components? … no / yes
✔ Write configuration to components.json. Proceed? … yes

注意:上面有一些不同,分别是:

  1. Configure the import alias for components: src
    • 由于@mono/ui,仅作为存放 shadcn 组件库的工作区,所以直接放在 src 下足够了,避免文件夹层级过深。
  2. Configure the import alias for utils: @mono/utils
    • 是的,@mono/utils 用于存放整个项目的公共函数工具。
  3. 如果你打开项目会发现 shadcn 默认安装了一些必要的依赖,考虑到多个项目使用相同版本,将他们移动到顶层工作区的 package.json中去。

配置 @mono/utils​

monorepo-utils

├───📁 src/
│ ├───📄 cn.ts
│ └───📄 index.ts
├───📄 package.json
└───📄 tsconfig.json

cn.ts是 UI 必要的函数

import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

index.ts所有工具函数导出的入口,未来需要使用到的函数都可以在此处导出。

export { cn } from "./cn";

packages.json

{
"name": "@mono/utils",
"version": "1.0.0",
"description": "shared utils",
"scripts": {},
"devDependencies": {
"@mono/config-ts": "workspace:^"
},
"exports": {
".": "./src/index.ts"
}
}

里程碑:这样配置,既满足了公共函数的存放位置,有解答了上面关于:“Configure the import alias for utils: @mono/utils” 的问题。

现在来试试组件安装效果:

cd /packages/ui

pnpx shadcn-ui@latest add button

image-20240419204201124

可以看到,安装组件验证了上面把工具函数都放在@mono/utils中的做法是正确的。

接下来配置package.json,添加:exports相关的配置,以便我们在业务 app 中更直观的引入。

{
"name": "@mono/ui",
"version": "1.0.0",
"description": "",
"scripts": {},
"keywords": [],
"dependencies": {
"@mono/config-ts": "workspace:*",
"@mono/utils": "workspace:*",
"@radix-ui/react-slot": "1.0.2"
},
"exports": {
"./*": "./src/ui/*.tsx"
}
}

现在需要回到我们的第一个 vite-react 项目,看看上面引入的 button 组件是否有效。

pnpm -F vite-react i @mono/ui
import { Button } from "@mono/ui/button"

function App() {
return (
<>
<Button>click</Button>
</>
)
}

export default App
pnpm -F vite-react run dev

image-20240419204908546

是的,成功引入了共享组件库@mono/ui中的 button 组件,没有样式效果。请继续阅读下一个章节:关于共享tailwind的配置,@mono/config-tailwind。

配置 @mono/config-tailwind​

monorepo-tailwind

现在我们需要引入 tailwind 相关的配置

cd apps/vite-react

pnpx tailwindcss init -p

pnpm i @mono/config-tailwind

同样,这里需要对 tailwind.config.ts 做一点小改动,所有项目的默认配置都是通过继承 @mono/config-tailwind 来的。

# config-tailwind
├───📄 global.css
├───📄 package.json
└───📄 tailwind.config.js

/packages/config-tailwind/tailwind.config.ts 配置如下:

/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
'../../packages/ui/src/**/*.{ts,tsx}', // ++
'../../packages/lib/src/**/*.{ts,tsx}' // ++
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
}

global.css

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;

--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;

--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;

--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;

--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;

--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;

--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;

--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;

--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;

--radius: 0.5rem;

--sb-track-color: hsl(var(--muted));
--sb-thumb-color: hsl(var(--muted-foreground));
--sb-size: 6px;
}

.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;

--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;

--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;

--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;

--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;

--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;

--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;

--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 85.7% 97.3%;

--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;

--sb-track-color: hsl(var(--muted));
--sb-thumb-color: hsl(var(--muted-foreground));
--sb-size: 6px;
}
}

@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

body::-webkit-scrollbar {
width: var(--sb-size);
}

body::-webkit-scrollbar-track {
background: var(--sb-track-color);
border-radius: 3px;
}

body::-webkit-scrollbar-thumb {
background: var(--sb-thumb-color);
border-radius: 3px;
}

@supports not selector(::-webkit-scrollbar) {
body {
scrollbar-color: var(--sb-thumb-color) var(--sb-track-color);
}
}

package.json

{
"name": "@mono/config-tailwind",
"version": "1.0.0",
"description": "",
"scripts": {},
"keywords": [],
"exports": {
".": "./tailwind.config.ts",
"./global.css": "./global.css"
}
}

在需要 tailwind 解析的项目中的 tailwind.config.ts 可以直接引入并导出公共配置:

apps/vite-react/tailwind.config.ts

packages/ui/tailwind.config.ts

export * from "@mono/config-tailwind";

同时需要将 vite-react 项目中的 main.tsx里面的 import 'global.css'->import "@mono/config-tailwind/global.css"

配置 ui-add 指令​

image-20240419232127393

image-20240419232147741

现在测试一下命令是否生效:

pnpm run ui-add card

在 vite-react 项目中 引入案例:

import { Button } from "@mono/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@mono/ui/card"

function App() {
return (
<>
<Button>click</Button>
<Card>
<CardHeader>
<CardTitle>Card Title</CardTitle>
<CardDescription>Card Description</CardDescription>
</CardHeader>
<CardContent>
<p>Card Content</p>
</CardContent>
<CardFooter>
<p>Card Footer</p>
</CardFooter>
</Card>
</>
)
}

export default App

image-20240419232626210

配置@mono/hooks​

monorepo-hooks

用类似的方式可以尝试配置一个公共hooks 库:

├───📁 src/
│ ├───📄 index.ts
│ └───📄 useTest.ts
├───📄 package.json
└───📄 tsconfig.json
// package.json
{
"name": "@mono/hooks",
"version": "1.0.0",
"private": true,
"description": "",
"scripts": {},
"exports": {
".": "./src/index.ts"
},
"devDependencies": {
"@mono/config-ts": "workspace:*"
}
}

TL;DR​

Repository address: https://github.com/TransonQ/growth-apps

自定义useSearchState与查询字符串同步状态

· 阅读需 1 分钟
TransonQ
Front-end Engineer

code​

代码: useSearchState-TransonQ

import { useState, useEffect } from "react";

function useSearchState<T>(key: string, init?: T) {
const searchParams = new URLSearchParams(window.location.search);
let initValue = searchParams.get(key) || "";

try {
JSON.parse(initValue); // 尝试解析查询字符串中的值
} catch (e) {
if (init !== undefined) {
initValue = JSON.stringify(init); // 如果解析错误,使用init的格式化后的值
} else {
initValue = ""; // 如果init也没有,初始化为空字符串
}
}

const [params, setParams] = useState<T>(() => {
return JSON.parse(initValue) as T;
});

useEffect(() => {
const searchParams = new URLSearchParams(window.location.search);
searchParams.set(key, JSON.stringify(params));
const newUrl = `${window.location.pathname}?${searchParams.toString()}`;
window.history.replaceState({}, "", newUrl);
}, [params, key]);

return [params, setParams] as const;
}

export default useSearchState;

使用案例​

https://codesandbox.io/p/sandbox/usesearchstate-key-init-9krjzz

使用useContext封装一个全局useToast

· 阅读需 3 分钟
TransonQ
Front-end Engineer
ToastContext.ts
import { createContext } from 'react'

interface ToastContextProps {
toast: {
success: (message: string, duration?: number) => void
error: (message: string, duration?: number) => void
}
}

export const ToastContext = createContext<ToastContextProps | undefined>(
undefined
)
emitter.ts
import mitt from 'mitt'

export const emitter = mitt()
GlobalToastProvider.tsx
import { Toast } from '@shopify/polaris'
import { useEffect, useMemo, useState } from 'react'
import { uniqueCode } from '../../utils'
import { ToastContext } from './ToastContext'
import { emitter } from './emitter'

/**
* Toasts 传参
*/
type ToastsProps = {
/** 前端生成的唯一id */
id: string
/** toast 内容 */
message: string
/** toast 类型 */
type: 'success' | 'error'
/** toast 持续时间 */
duration: number
/** 插入队列的时间 */
createdAt: Date
}

export const GlobalToastProvider = ({
children,
}: {
children: React.ReactNode
}) => {
/** toasts 队列 */
const [toasts, setToasts] = useState<ToastsProps[]>([])

/** 唤起成功的 toast */
const toastSuccess = (message: string, duration = 5000) => {
const newToast: ToastsProps = {
id: uniqueCode('toast_success'),
type: 'success',
message,
duration,
createdAt: new Date(), // 记录插入队列的时间
}

setToasts((prevToasts) => [...prevToasts, newToast])
}

/** 唤起错误的 toast */
const toastError = (message: string, duration = 5000) => {
const newToast: ToastsProps = {
id: uniqueCode('toast_error'),
type: 'error',
message,
duration,
createdAt: new Date(), // 记录插入队列的时间
}

setToasts((prevToasts) => [...prevToasts, newToast])
}

/** toast 唤起函数 */
const toastHandler = useMemo(() => {
return {
success: toastSuccess,
error: toastError,
}
}, [])

/** 移除 toast */
const dismissToast = (id: string) => {
setToasts((prevToasts) => prevToasts.filter((toast) => toast.id !== id))
}

/** 定时器根据入队时间和 duration 比较确定 toast 存在时长 */
useEffect(() => {
let timer: NodeJS.Timeout
if (toasts.length === 0) return

timer = setInterval(() => {
const now = new Date()
const updatedToasts = toasts.filter((toast) => {
return now.getTime() - toast.createdAt.getTime() < toast.duration
})
setToasts(updatedToasts)
}, 1000)

return () => {
clearInterval(timer)
}
}, [toasts])

useEffect(() => {
emitter.on('toaster.error', (e: any) =>
toastHandler.error(e.message, e.duration)
)
emitter.on('toaster.success', (e: any) =>
toastHandler.success(e.message, e.duration)
)
return () => {
emitter.all.clear()
}
}, [toastHandler])

return (
<ToastContext.Provider value={{ toast: toastHandler }}>
{children}
{toasts.map((item) => (
<Toast
key={item.id}
content={item.message}
onDismiss={() => dismissToast(item.id)}
error={item.type === 'error'}
/>
))}
</ToastContext.Provider>
)
}
useToast
import { useContext } from 'react'
import { ToastContext } from './ToastContext'

type useToastValue = {
success: (message: string, duration?: number) => void
error: (message: string, duration?: number) => void
}

export const useToast = (): useToastValue => {
const context = useContext(ToastContext)

if (!context) {
throw new Error(
'useToast must be used within a <GlobalToastProvider> * </GlobalToastProvider>'
)
}

return context.toast
}
toaster.ts
import { emitter } from './emitter'

export const toaster = {
success: (message: string, duration?: number) => {
emitter.emit('toaster.success', { message, duration })
},
error: (message: string, duration?: number) => {
emitter.emit('toaster.error', { message, duration })
},
}

升级doc龙v2->v3

· 阅读需 2 分钟
TransonQ
Front-end Engineer

步骤​

官方升级指南

package.json​

 {
"dependencies": {
// upgrade to Docusaurus v3
- "@docusaurus/core": "2.4.3",
- "@docusaurus/preset-classic": "2.4.3",
+ "@docusaurus/core": "3.0.0",
+ "@docusaurus/preset-classic": "3.0.0",
// upgrade to MDX v2
- "@mdx-js/react": "^1.6.22",
+ "@mdx-js/react": "^3.0.0",
// upgrade to prism-react-renderer v2.0+
- "prism-react-renderer": "^1.3.5",
+ "prism-react-renderer": "^2.1.0",
// upgrade to React v18.0+
- "react": "^17.0.2",
- "react-dom": "^17.0.2"
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
},
"devDependencies": {
// upgrade Docusaurus dev dependencies to v3
- "@docusaurus/module-type-aliases": "2.4.3"
- "@docusaurus/types": "2.4.3"
+ "@docusaurus/module-type-aliases": "3.0.0"
+ "@docusaurus/types": "3.0.0"
}
"engines": {
// require Node.js 18.0+
- "node": ">=16.14"
+ "node": ">=18.0"
}
}

ts项目​

 {
...
"devDependencies": {
// swap the external TypeScript config package for the new official one
- "@tsconfig/docusaurus": "^1.0.7",
+ "@docusaurus/tsconfig": "3.0.0",
// upgrade React types to v18.0+
- "@types/react": "^17.0.69",
+ "@types/react": "^18.2.29",
// upgrade TypeScript to v5.0+
- "typescript": "~4.7.4"
+ "typescript": "~5.2.2"
}
}

检查是否存在 v3无法编译的文件​

npx docusaurus-mdx-checker

Prism-React-Renderer 配置​

- const lightTheme = require('prism-react-renderer/themes/github');
- const darkTheme = require('prism-react-renderer/themes/dracula');
+ const {themes} = require('prism-react-renderer');
+ const lightTheme = themes.github;
+ const darkTheme = themes.dracula;

个人补上了 mermaid 支持, 案例:

在Remix中使用styled-components

· 阅读需 2 分钟
TransonQ
Front-end Engineer

背景​

最近业务开始使用remix用于开发, 相对于之前纯客户端渲染开发, styled-components这一高效的css-in-js库一直想用在ssr上面, 前段时间卡壳在于对原生node请求响应这一方向接触较少, 较为生疏的情况下, 看着人家styled-components SSR文档看了, 原理了解了之后总结就是: 服务端渲染需要将生成的css和html从服务端发往客户端,css-in-js的库基本都有==避免双重渲染== 的手段. 如下就是styled-components收集样式后加入详情头解决方法.

代码案例​

import {
createReadableStreamFromReadable,
type EntryContext,
} from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import isbot from "isbot";
import { renderToPipeableStream } from "react-dom/server";
import { PassThrough } from "stream";
import { ServerStyleSheet } from "styled-components"; // 1.导入ServerStyleSheet
import { addDocumentResponseHeaders } from "./shopify.server";

const ABORT_DELAY = 5000;

export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
addDocumentResponseHeaders(request, responseHeaders);

const callbackName = isbot(request.headers.get("user-agent"))
? "onAllReady"
: "onShellReady";

return new Promise((resolve, reject) => {
const sheet = new ServerStyleSheet(); // 2.创建ServerStyleSheet实例

const { pipe, abort } = renderToPipeableStream(
// 3.使用collectStyles方法收集样式
sheet.collectStyles(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>
),
{
[callbackName]: () => {
const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);
const styleTags = sheet.getStyleTags(); // 4.获取样式标签

responseHeaders.set("Content-Type", "text/html");
responseHeaders.set("Link", styleTags); // 5.将样式标签添加到响应头中

resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
})
);
pipe(body);
},
onShellError(error) {
reject(error);
},
onError(error) {
responseStatusCode = 500;
console.error(error);
},
}
);

setTimeout(abort, ABORT_DELAY);
});
}

图解 RSC

· 阅读需 2 分钟
TransonQ
Front-end Engineer

一 . 最开始 : PHP 时代​

1

二 . 更多交互 : CSR 时代​

客户端渲染会收到渲染网站所需的js

2

客户端已经具备渲染代码, 从而不需要跟服务器通信

3

三 . 更好的 SEO: 服务端渲染(SSR)/静态站点生成(SSG)​

客户端可以短时间看到静态网页, 但是需要更多时间加载js和 hydrate

4

后续导航跟CSR一样, SSR拥有显示下一页所需要的所有渲染代码, 因此切换速度很快

5

问题 : 大多数 SSR/SSG 方法将用于生成页面的所有 JavaScript发送到客户端,然后客户端再次运行所有内容,并将该 HTML 与刚刚启动的 JavaScript 结合起来。我们真的需要发送并运行所有 JavaScript 吗?我们真的需要重复所有渲染工作只是为了水合吗?如果服务器端渲染需要很长时间怎么办?

四 . React Server Component​

  1. 可以此定义哪些组件代码需要在服务器运行

  2. 服务器组件可以直接从组件内部获取数据。获取完成后,服务器组件可以将该数据流式传输到客户端。

  3. 加载慢的服务器组件,可以有单独的加载, 其他并不影响

7

swr取消请求hook封装

· 阅读需 1 分钟
TransonQ
Front-end Engineer
function useCancellableSWR(key, swrOptions) {
const source = axios.CancelToken.source();

return [
useSWR(key, (url) => axios.get(url, { cancelToken: source.token }).then(res => res.data), {
...swrOptions,
}),
source,
];
}

// usage:
const [{ data }, cancelFn] = useCancellableSWR('/endpoint');

cancelFn.cancel()

issue-129-comment

流行框架中hydration和resumability对比

· 阅读需 5 分钟
TransonQ
Front-end Engineer

流行框架中hydration和resumability对比

hydration​

当 React、Vue、Svelte 或 Angular 等 SPA 框架添加了 SSR 支持时,水合作为一种算法首次出现。为了使这些框架在浏览器中变得交互式,它们必须从根 () 重新执行整个应用程序,以恢复序列化为 HTML (renderToString) 时丢失的状态和事件处理程序。

所有水合实现都将单个组件作为根(通常是 ),为了使其具有交互性,树中的每个组件都必须执行。只有在设置了事件处理程序之后,用户才能与其交互。

水合作用只能通过向先前在服务器中执行的组件树添加交互性的过程来理解。这是一个 O(n) 算法,其中 N 是要唤醒的组件数量。

hydration

partial hydration​

Astro 框架凸显了部分水合的概念,它源于这样的理解:并非树的所有部分都需要水合,特别是那些完全静态的部分。这种方法建立了“交互岛”,减少了浏览器的工作负载,从而实现更快的交互。

部分水合正是这样的:在不同时刻多次调用 HydroRoot(),所以这个名字很合适。

然而,必须强调的是,这些“交互岛”需要由开发人员手动创建。此外,这些岛会创建边界,这意味着每个岛都作为独立的子应用程序运行,这可能会阻碍应用程序不同部分之间的通信。

partial_hydration

React Server Component is hydration​

就像部分水合一样,RSC 是具有不同姓氏的水合,我们可以将其称为稀疏水合(Sparse Hydration)。

RSC 没有创建孤立的岛屿,而是维护单个根,允许组件相互通信。但是,根目录中的“服务器组件”不需要在浏览器中重新执行。相反,React 将这些组件的 vNode 作为数据序列化到 HTML 中。这是一种时空权衡(Space–time tradeoff)。

就像孤岛架构一样,开发人员必须努力使用“使用服务器”或“使用客户端”指令在 React 服务器组件中设置边界。

React Server Component is hydration

可恢复性(Resumability)本质上是一种不同的算法​

(Resumability is fundamentally a different algorithm) 最后,我们深入研究了可恢复性,这是一种提倡即时交互性的范例,无需水合或爬行组件。它完全脱离了树结构。

可恢复性的独特之处在于根(或入口点)是事件处理程序,而不是组件。

在传统的水合、部分水合和 React Server 组件中,组件充当根。对于部分水合,您可能有多个根,而对于 React Server 组件,您只有一个根。

另一方面,可恢复性将事件处理程序置于根位置。箭头从事件指向组件,表明不需要运行任何用户代码,即使对于可以重新渲染的组件也是如此。这种方法有效地将所有组件视为静态的,无论它们是否是静态的。

Resumability

参考原文

hydration-tree-resumability-map