├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── README.md ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── next.svg └── vercel.svg ├── src ├── app │ ├── [slug] │ │ ├── layout.tsx │ │ ├── markdown.css │ │ └── page.tsx │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── components │ ├── blogList │ │ └── list.tsx │ ├── header │ │ └── header.tsx │ ├── homeLink │ │ └── homeLink.tsx │ └── link │ │ └── link.tsx ├── data │ └── post.ts ├── fonts │ └── fonts.ts ├── lib │ └── utils.ts └── posts │ ├── a-chain-reaction │ ├── components.js │ ├── greeting.js │ └── index.mdx │ ├── math │ └── index.mdx │ ├── npm-audit-broken-by-design │ └── index.mdx │ ├── readme │ └── index.mdx │ └── the-two-react │ ├── components.js │ ├── counter.js │ ├── index.mdx │ ├── post-list.js │ └── post-preview.js ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "prettier-plugin-tailwindcss" 4 | ] 5 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Nextjs + tailwindcss 实现 大神 dan 一模一样的博客 3 | date: "2024-02-23" 4 | spoiler: Nextjs + tailwindcss 实战 5 | --- 6 | 7 | 大神 [dan](https://overreacted.io/) 的博客相信大家都看过,博客质量那是不用多说,懂的都懂。 说到博客样式,我比较喜欢这种简约风。其中博客中还支持组件交互效果。 所以我决定用 Nextjs + tailwindcss 实现一模一样的博客学习下。 8 | 9 | 技术选型: 10 | 11 | - Nextjs 12 | - tailwindcss 13 | - MDXRemote 14 | 15 | 部署: 16 | 17 | - vercel 18 | 19 | ## 项目文件结构: 20 | 21 | ``` 22 | |-- dan-blog 23 | |-- app 24 | | |-- favicon.ico 25 | | |-- globals.css 26 | | |-- layout.tsx 27 | | |-- page.tsx 博客列表(首页) 28 | | |-- [slug] 博客详情 29 | | |-- layout.tsx 30 | | |-- markdown.css 31 | | |-- page.tsx 32 | |-- components 33 | | |-- blogList 34 | | | |-- list.tsx 博客列表 35 | | |-- header 36 | | | |-- header.tsx 顶部header 37 | | |-- homeLink 38 | | |-- homeLink.tsx 顶部导航 39 | |-- data 40 | | |-- post.ts 处理博客数据(博客列表、博客详情) 41 | |-- fonts 42 | | |-- fonts.ts 全局字体 43 | |-- lib 44 | | |-- utils.ts 45 | |-- posts 写博客的地方 46 | |-- 博客文件夹1 47 | | |-- index.mdx 48 | |-- 博客文件夹2 49 | | |-- index.mdx 50 | |-- 博客文件夹3 51 | | |-- index.mdx 52 | |-- 博客文件夹4 53 | | |-- index.mdx 54 | |-- 博客文件夹5 55 | |-- components.js 56 | |-- 你的组件.js 57 | |-- index.mdx 58 | |-- 你的组件.js 59 | |-- 你的组件.js 60 | 61 | 62 | ``` 63 | 64 | 我并不打算直接引入database来存储markdown文件,这样成本太大,你必须要选择一种数据库,还要编写数据库增删改查代码。对于一个本项目来说,甚至是博客这种小项目来说,得不偿失。 65 | 66 | 我规划将博客文章的markdown文件放在项目中,通过读取文件的方式来渲染博客文章。这样做的好处是,你可以直接在项目中编写markdown文件,push到github,vercel就会自动部署,你的博客就更新了。 67 | 68 | nextjs的服务端组件能够很好的支持这种需求。我们可以通过服务端组件来读取文件,然后渲染到页面上。 69 | 70 | 在编写代码之前,你需要了解下nextjs基本工作原理,app router工作原理,以及动态路由工作原理。这样你才能更好的理解下面的代码。 71 | 72 | ## 博客列表 73 | 74 | layout文件代码(src/app/layout.tsx) 75 | 76 | ```js 77 | import type { Metadata } from "next"; 78 | import "./globals.css"; 79 | import { serif } from "@/fonts/fonts"; 80 | import Header from "@/components/header/header"; 81 | 82 | export const metadata: Metadata = { 83 | title: "overreacted - A blog by Dan Abramov", 84 | description: "Generated by create next app", 85 | }; 86 | 87 | export default function RootLayout({ 88 | children, 89 | }: Readonly<{ 90 | children: React.ReactNode; 91 | }>) { 92 | return ( 93 | 94 | 95 |
96 |
{children}
97 | 98 | 99 | ); 100 | } 101 | 102 | ``` 103 | 104 | 字体你可以选择你喜欢的样式,这里保持跟dan一模一样的字体 105 | 106 | 字体文件代码(src/fonts/fonts.ts) 107 | 108 | ```js 109 | import { Montserrat, Merriweather } from "next/font/google"; 110 | 111 | export const sans = Montserrat({ 112 | subsets: ["latin"], 113 | display: "swap", 114 | weight: ["400", "700", "900"], 115 | style: ["normal"], 116 | }); 117 | 118 | export const serif = Merriweather({ 119 | subsets: ["latin"], 120 | display: "swap", 121 | weight: ["400", "700"], 122 | style: ["normal", "italic"], 123 | }); 124 | ``` 125 | 126 | page作为首页,要渲染博客列表。在这之前需要先拿到博客列表数据。这里我将所有的博客md文件放在项目中的posts文件夹中(src/app/posts)。每一篇博客创建一个文件夹,文件夹中包含一个index.md文件,index.md就是写博客的地方,如果你需要交互组件,在当前文件夹中编写的组件代码,最后通过公共的components文件统一导出所有组件。 127 | 128 | 20240223173442.jpg 133 | 134 | page文件代码(src/app/page.tsx) 135 | 136 | ```js 137 | import BlogList from "@/components/blogList/list"; 138 | 139 | export default function Home() { 140 | return ; 141 | } 142 | ``` 143 | 144 | 这里我将component单独抽离到app路径外,你可以更好的管理你的组件。app文件下的文件都是页面文件。这样划分,职责功能更加清晰。 145 | 146 | BlogList组件(src/components/blogList/list.tsx) 147 | 148 | ```js 149 | export default async function BlogList() { 150 | return
...
; 151 | } 152 | ``` 153 | 154 | 接下来就需要获取博客列表数据,新建data文件夹(src/app/data/post.ts),这里集中处理读取博客内容,获取到博客列表信息,和博客详情。可以理解为数据库操作。 155 | 156 | 读取全部文章数据: 157 | 158 | ```js 159 | const rootDirectory = path.join(process.cwd(), "src", "posts"); 160 | 161 | export const getAllPostsMeta = async () => { 162 | // 获取到src/posts/下所有文件 163 | const dirs = fs 164 | .readdirSync(rootDirectory, { withFileTypes: true }) 165 | .filter((entry) => entry.isDirectory()) 166 | .map((entry) => entry.name); 167 | 168 | // 解析文章数据,拿到标题、日期、简介 169 | let datas = await Promise.all( 170 | dirs.map(async (dir) => { 171 | const { meta, content } = await getPostBySlug(dir); 172 | return { meta, content }; 173 | }), 174 | ); 175 | 176 | // 文章日期排序,最新的在最前面 177 | datas.sort((a, b) => { 178 | return Date.parse(a.meta.date) < Date.parse(b.meta.date) ? 1 : -1; 179 | }); 180 | return datas; 181 | }; 182 | ``` 183 | 184 | ```js 185 | export const getPostBySlug = async (dir: string) => { 186 | const filePath = path.join(rootDirectory, dir, "/index.mdx"); 187 | 188 | const fileContent = fs.readFileSync(filePath, { encoding: "utf8" }); 189 | 190 | // gray-matter库是一个解析markdown内容,可以拿到markdown文件的meta信息和content内容 191 | const { data } = matter(fileContent); 192 | 193 | // 如果文件名是中文,转成拼音 194 | const id = isChinese(dir) 195 | ? pinyin(dir, { 196 | toneType: "none", 197 | separator: "-", 198 | }) 199 | : dir; 200 | 201 | return { 202 | meta: { ...data, slug: dir, id }, 203 | content: fileContent, 204 | } as PostDetail; 205 | }; 206 | ``` 207 | 208 | 补全BlogList组件逻辑 209 | 210 | ```js 211 | export default async function BlogList() { 212 | const posts = await getAllPostsMeta(); 213 | 214 | return ( 215 |
216 | {posts.map((item) => { 217 | return ( 218 | 223 |
224 | 225 |

226 | {new Date(item.meta.date).toLocaleDateString("cn", { 227 | day: "2-digit", 228 | month: "2-digit", 229 | year: "numeric", 230 | })} 231 |

232 |

{item.meta.spoiler}

233 |
234 | 235 | ); 236 | })} 237 |
238 | ); 239 | } 240 | ``` 241 | 242 | 20240223174210.jpg 247 | 248 | 你会发现博客标题颜色随着文章顺序变化,实现这个功能,单独抽离PostTitle组件:这里的逻辑并不是唯一,你可以根据自己的需求来实现你自己喜欢的颜色。主要逻辑就是`--lightLink` `--darkLink` 这两个css变量,你可以根据不同的逻辑来设置这两个变量的值。其中`--lightLink` `--darkLink`分别是日间/暗黑模式的变量,你可以根据你的需求来设置。 249 | 250 | ```js 251 | import Color from "colorjs.io"; 252 | 253 | function PostTitle({ post }: { post: PostDetail }) { 254 | let lightStart = new Color("lab(63 59.32 -1.47)"); 255 | let lightEnd = new Color("lab(33 42.09 -43.19)"); 256 | let lightRange = lightStart.range(lightEnd); 257 | let darkStart = new Color("lab(81 32.36 -7.02)"); 258 | let darkEnd = new Color("lab(78 19.97 -36.75)"); 259 | let darkRange = darkStart.range(darkEnd); 260 | let today = new Date(); 261 | let timeSinceFirstPost = ( 262 | today.valueOf() - new Date(2018, 10, 30).valueOf() 263 | ).valueOf(); 264 | let timeSinceThisPost = ( 265 | today.valueOf() - new Date(post.meta.date).valueOf() 266 | ).valueOf(); 267 | let staleness = timeSinceThisPost / timeSinceFirstPost; 268 | 269 | return ( 270 |

282 | {post.meta.title} 283 |

284 | ); 285 | } 286 | 287 | ``` 288 | 289 | ## 博客详情 290 | 291 | 首页博客列表实现完了,接下来就是博客详情页面,渲染mdx文件内容,首推nextjs官方的MDXRemote组件,最重要的就是它支持引入自己编写的组件,这样就可以实现博客中的组件交互效果。 292 | 293 | 博客详情是nextjs动态路由最好的应用场景,你可以通过动态路由来渲染不同的博客详情页面。创建动态路由文件(src/app/[slug]/layout.tsx)。博客详情作为新页面自然是必须layout包裹的,所以在这里引入layout组件。 294 | 295 | ```js 296 | import HomeLink from "@/components/homeLink/homeLink"; 297 | 298 | export default function DetailLayout({ 299 | children, 300 | }: { 301 | children: React.ReactNode; 302 | }) { 303 | return ( 304 | <> 305 | {children} 306 |
307 | 308 | 309 |
310 | 311 | ); 312 | } 313 | 314 | ``` 315 | 316 | HomeLink组件没有关键逻辑,不用关心。 317 | 318 | 博客详情页面代码(src/app/[slug]/page.tsx) 319 | 320 | ```js 321 | import { getAllPostsMeta, getPost } from "@/data/post"; 322 | import { MDXRemote } from "next-mdx-remote/rsc"; 323 | // 美化代码,支持代码颜色主题 324 | import rehypePrettyCode from "rehype-pretty-code"; 325 | // 支持数学公式 326 | import remarkMath from "remark-math"; 327 | import rehypeKatex from "rehype-katex"; 328 | import { sans } from "@/fonts/fonts"; 329 | import "./markdown.css"; 330 | import { getPostWords, readingTime } from "@/lib/utils"; 331 | 332 | export async function generateStaticParams() { 333 | const metas = await getAllPostsMeta(); 334 | return metas.map((post) => { 335 | return { slug: post.meta.slug }; 336 | }); 337 | } 338 | 339 | export default async function PostPage({ 340 | params, 341 | }: { 342 | params: { slug: string }; 343 | }) { 344 | // 获取文章详情 345 | const post = await getPost(params.slug); 346 | let postComponents = {}; 347 | 348 | // 提取自己编写的组件 349 | try { 350 | postComponents = await import( 351 | "../../posts/" + params.slug + "/components.js" 352 | ); 353 | } catch (e: any) { 354 | if (!e || e.code !== "MODULE_NOT_FOUND") { 355 | throw e; 356 | } 357 | } 358 | 359 | const words = getPostWords(post.content); 360 | const readTime = readingTime(words); 361 | 362 | return ( 363 |
364 |

370 | {post.meta.title} 371 |

372 |

373 | {new Date(post.meta.date).toLocaleDateString("cn", { 374 | day: "2-digit", 375 | month: "2-digit", 376 | year: "numeric", 377 | })} 378 |

379 | 380 |

381 | 字数:{words} 382 |

383 |

384 | 预计阅读时间:{readTime}分钟 385 |

386 |
387 | 412 |
413 |
414 | ); 415 | } 416 | 417 | ``` 418 | 419 | 在BlogList组件中,我们使用`Link`组件包裹所有内容,并设置`href`属性为`"/" + item.meta.slug + "/"`,这样就可以通过动态路由来渲染不同的博客详情页面。 420 | 421 | ```js 422 | 425 | ``` 426 | 427 | 函数generateStaticParams 函数可以与动态路由段结合使用,在构建时静态生成路由,而不是在请求时按需生成路由。这样可以提高页面加载速度。 428 | 429 | 获取文章详情方法(src/data/posts.ts) 430 | 431 | ```js 432 | export async function getPost(slug: string) { 433 | const posts = await getAllPostsMeta(); 434 | if (!slug) throw new Error("not found"); 435 | const post = posts.find((post) => post.meta.slug === slug); 436 | if (!post) { 437 | throw new Error("not found"); 438 | } 439 | return post; 440 | } 441 | ``` 442 | 443 | 提取自己编写的组件,丢给MDXRomote组件。 444 | 445 | ```js 446 | try { 447 | postComponents = await import( 448 | "../../posts/" + params.slug + "/components.js" 449 | ); 450 | } catch (e: any) { 451 | if (!e || e.code !== "MODULE_NOT_FOUND") { 452 | throw e; 453 | } 454 | } 455 | ``` 456 | 457 | 文章markdown渲染美化css 458 | 459 | ```css 460 | .markdown { 461 | line-height: 28px; 462 | --path: none; 463 | --radius-top: 12px; 464 | --radius-bottom: 12px; 465 | --padding-top: 1rem; 466 | --padding-bottom: 1rem; 467 | } 468 | 469 | .markdown p { 470 | @apply pb-8; 471 | } 472 | 473 | .markdown a { 474 | @apply border-b-[1px] border-[--link] text-[--link]; 475 | } 476 | 477 | .markdown hr { 478 | @apply pt-8 opacity-60 dark:opacity-10; 479 | } 480 | 481 | .markdown h2 { 482 | @apply mt-2 pb-8 text-3xl font-bold; 483 | } 484 | 485 | .markdown h3 { 486 | @apply mt-2 pb-8 text-2xl font-bold; 487 | } 488 | 489 | .markdown h4 { 490 | @apply mt-2 pb-8 text-xl font-bold; 491 | } 492 | 493 | .markdown :not(pre) > code { 494 | border-radius: 10px; 495 | background: var(--inlineCode-bg); 496 | color: var(--inlineCode-text); 497 | padding: 0.15em 0.2em 0.05em; 498 | white-space: normal; 499 | } 500 | 501 | .markdown pre { 502 | @apply -mx-4 mb-8 overflow-y-auto p-4 text-sm; 503 | clip-path: var(--path); 504 | border-top-right-radius: var(--radius-top); 505 | border-top-left-radius: var(--radius-top); 506 | border-bottom-right-radius: var(--radius-bottom); 507 | border-bottom-left-radius: var(--radius-bottom); 508 | padding-top: var(--padding-top); 509 | padding-bottom: var(--padding-bottom); 510 | } 511 | 512 | .markdown pre code { 513 | width: auto; 514 | } 515 | 516 | .markdown blockquote { 517 | @apply relative -left-2 -ml-4 mb-8 pl-4; 518 | font-style: italic; 519 | border-left: 3px solid hsla(0, 0%, 0%, 0.9); 520 | border-left-color: inherit; 521 | opacity: 0.8; 522 | } 523 | 524 | .markdown blockquote p { 525 | margin: 0; 526 | padding: 0; 527 | } 528 | 529 | .markdown p img { 530 | margin-bottom: 0; 531 | } 532 | 533 | .markdown ul { 534 | margin-top: 0; 535 | padding-bottom: 0; 536 | padding-left: 0; 537 | padding-right: 0; 538 | padding-top: 0; 539 | margin-bottom: 1.75rem; 540 | list-style-position: outside; 541 | list-style-image: none; 542 | list-style: disc; 543 | } 544 | 545 | .markdown li { 546 | margin-bottom: calc(1.75rem / 2); 547 | } 548 | 549 | .markdown img { 550 | @apply mb-8; 551 | max-width: 100%; 552 | } 553 | 554 | .markdown pre [data-highlighted-line] { 555 | margin-left: -16px; 556 | padding-left: 12px; 557 | border-left: 4px solid #ffa7c4; 558 | background-color: #022a4b; 559 | display: block; 560 | padding-right: 1em; 561 | } 562 | ``` 563 | 564 | 至此逻辑已经全部完成,这个博客项目还比较简答。 其核心逻辑就是 读取markdown文件-MDXRemote组件渲染mdx文件内容。MDXRemote组件还支持丰富的插件功能,数学公式、图标展示。感兴趣的同学可以扩展。 565 | 566 | 完整代码在: [blog](https://github.com/sunshineLixun/dan-blog) 567 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | 3 | const nextConfig = { 4 | images: { 5 | remotePatterns: [ 6 | { 7 | protocol: "https", 8 | hostname: "avatars.githubusercontent.com", 9 | }, 10 | ], 11 | }, 12 | pageExtensions: ["js", "jsx", "mdx", "ts", "tsx"], 13 | }; 14 | 15 | export default nextConfig; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dan-blog", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@mdx-js/loader": "^3.0.0", 13 | "@mdx-js/react": "^3.0.0", 14 | "@next/mdx": "^14.1.0", 15 | "@types/mdx": "^2.0.11", 16 | "classnames": "^2.5.1", 17 | "colorjs.io": "^0.4.5", 18 | "gray-matter": "^4.0.3", 19 | "next": "14.2.1", 20 | "next-mdx-remote": "^4.4.1", 21 | "next-view-transitions": "^0.1.0", 22 | "pinyin-pro": "^3.19.4", 23 | "react": "^18", 24 | "react-dom": "^18", 25 | "rehype-katex": "^6.0.3", 26 | "rehype-pretty-code": "^0.13.0", 27 | "remark": "^15.0.1", 28 | "remark-math": "^5.1.1", 29 | "shiki": "^1.1.3" 30 | }, 31 | "devDependencies": { 32 | "@types/node": "^20", 33 | "@types/react": "^18", 34 | "@types/react-dom": "^18", 35 | "autoprefixer": "^10.0.1", 36 | "eslint": "^8", 37 | "eslint-config-next": "14.1.0", 38 | "postcss": "^8", 39 | "prettier": "^3.2.4", 40 | "prettier-plugin-tailwindcss": "^0.5.11", 41 | "tailwindcss": "^3.3.0", 42 | "typescript": "^5" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/[slug]/layout.tsx: -------------------------------------------------------------------------------- 1 | import HomeLink from "@/components/homeLink/homeLink"; 2 | 3 | export default function DetailLayout({ 4 | children, 5 | }: { 6 | children: React.ReactNode; 7 | }) { 8 | return ( 9 | <> 10 | {children} 11 |
12 | 13 |
14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/app/[slug]/markdown.css: -------------------------------------------------------------------------------- 1 | .markdown { 2 | line-height: 28px; 3 | --path: none; 4 | --radius-top: 12px; 5 | --radius-bottom: 12px; 6 | --padding-top: 1rem; 7 | --padding-bottom: 1rem; 8 | } 9 | 10 | .markdown p { 11 | @apply pb-8; 12 | } 13 | 14 | .markdown a { 15 | @apply border-b-[1px] border-[--link] text-[--link]; 16 | } 17 | 18 | .markdown hr { 19 | @apply pt-8 opacity-60 dark:opacity-10; 20 | } 21 | 22 | .markdown h2 { 23 | @apply mt-2 pb-8 text-3xl font-bold; 24 | } 25 | 26 | .markdown h3 { 27 | @apply mt-2 pb-8 text-2xl font-bold; 28 | } 29 | 30 | .markdown h4 { 31 | @apply mt-2 pb-8 text-xl font-bold; 32 | } 33 | 34 | .markdown :not(pre) > code { 35 | border-radius: 10px; 36 | background: var(--inlineCode-bg); 37 | color: var(--inlineCode-text); 38 | padding: 0.15em 0.2em 0.05em; 39 | white-space: normal; 40 | } 41 | 42 | .markdown pre { 43 | @apply -mx-4 mb-8 overflow-y-auto p-4 text-sm; 44 | clip-path: var(--path); 45 | border-top-right-radius: var(--radius-top); 46 | border-top-left-radius: var(--radius-top); 47 | border-bottom-right-radius: var(--radius-bottom); 48 | border-bottom-left-radius: var(--radius-bottom); 49 | padding-top: var(--padding-top); 50 | padding-bottom: var(--padding-bottom); 51 | } 52 | 53 | .markdown pre code { 54 | width: auto; 55 | } 56 | 57 | .markdown blockquote { 58 | @apply relative -left-2 -ml-4 mb-8 pl-4; 59 | font-style: italic; 60 | border-left: 3px solid hsla(0, 0%, 0%, 0.9); 61 | border-left-color: inherit; 62 | opacity: 0.8; 63 | } 64 | 65 | .markdown blockquote p { 66 | margin: 0; 67 | padding: 0; 68 | } 69 | 70 | .markdown p img { 71 | margin-bottom: 0; 72 | } 73 | 74 | .markdown ul { 75 | margin-top: 0; 76 | padding-bottom: 0; 77 | padding-left: 0; 78 | padding-right: 0; 79 | padding-top: 0; 80 | margin-bottom: 1.75rem; 81 | list-style-position: outside; 82 | list-style-image: none; 83 | list-style: disc; 84 | } 85 | 86 | .markdown li { 87 | margin-bottom: calc(1.75rem / 2); 88 | } 89 | 90 | .markdown img { 91 | @apply mb-8; 92 | max-width: 100%; 93 | } 94 | 95 | .markdown pre [data-highlighted-line] { 96 | margin-left: -16px; 97 | padding-left: 12px; 98 | border-left: 4px solid #ffa7c4; 99 | background-color: #022a4b; 100 | display: block; 101 | padding-right: 1em; 102 | } 103 | -------------------------------------------------------------------------------- /src/app/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { getAllPostsMeta, getPost } from "@/data/post"; 2 | import { MDXRemote } from "next-mdx-remote/rsc"; 3 | import rehypePrettyCode from "rehype-pretty-code"; 4 | import remarkMath from "remark-math"; 5 | import rehypeKatex from "rehype-katex"; 6 | import { sans } from "@/fonts/fonts"; 7 | import "./markdown.css"; 8 | import { getPostWords, readingTime } from "@/lib/utils"; 9 | 10 | export async function generateStaticParams() { 11 | const metas = await getAllPostsMeta(); 12 | return metas.map((post) => { 13 | return { slug: post.meta.slug }; 14 | }); 15 | } 16 | 17 | export default async function PostPage({ 18 | params, 19 | }: { 20 | params: { slug: string }; 21 | }) { 22 | const post = await getPost(params.slug); 23 | let postComponents = {}; 24 | 25 | try { 26 | postComponents = await import( 27 | "../../posts/" + params.slug + "/components.js" 28 | ); 29 | } catch (e: any) { 30 | if (!e || e.code !== "MODULE_NOT_FOUND") { 31 | throw e; 32 | } 33 | } 34 | 35 | const words = getPostWords(post.content); 36 | const readTime = readingTime(words); 37 | 38 | return ( 39 |
40 |

46 | {post.meta.title} 47 |

48 |

49 | {new Date(post.meta.date).toLocaleDateString("cn", { 50 | day: "2-digit", 51 | month: "2-digit", 52 | year: "numeric", 53 | })} 54 |

55 | 56 |

57 | 字数:{words} 58 |

59 |

60 | 预计阅读时间:{readTime}分钟 61 |

62 |
63 | 87 |
88 |
89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunshineLixun/dan-blog/3ecddcac611593277c46f14e72aca41f90c15680/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --text: #222; 7 | --title: #222; 8 | --bg: white; 9 | --code-bg: #232936; 10 | --link: #d23669; 11 | --inlineCode-bg: rgba(255, 229, 100, 0.2); 12 | --inlineCode-text: #1a1a1a; 13 | --pink: lab(63 59.32 -1.47); 14 | --purple: lab(33 42.09 -43.19); 15 | } 16 | @media (prefers-color-scheme: dark) { 17 | :root { 18 | --text: rgba(255, 255, 255, 0.88); 19 | --title: white; 20 | --bg: rgb(40, 44, 53); 21 | --code-bg: #191d27; 22 | --link: #ffa7c4; 23 | --inlineCode-bg: rgba(115, 124, 153, 0.2); 24 | --inlineCode-text: #e6e6e6; 25 | --pink: lab(81 32.36 -7.02); 26 | --purple: lab(78 19.97 -36.75); 27 | } 28 | } 29 | @property --myColor1 { 30 | syntax: ""; 31 | initial-value: #222; 32 | inherits: false; 33 | } 34 | @property --myColor2 { 35 | syntax: ""; 36 | initial-value: #222; 37 | inherits: false; 38 | } 39 | @media (prefers-color-scheme: dark) { 40 | @property --myColor1 { 41 | syntax: ""; 42 | initial-value: rgba(255, 255, 255, 0.88); 43 | inherits: false; 44 | } 45 | @property --myColor2 { 46 | syntax: ""; 47 | initial-value: rgba(255, 255, 255, 0.88); 48 | inherits: false; 49 | } 50 | } 51 | 52 | @media (prefers-color-scheme: dark) { 53 | body { 54 | -webkit-font-smoothing: antialiased; 55 | -moz-osx-font-smoothing: grayscale; 56 | } 57 | } 58 | 59 | @media (prefers-reduced-motion) { 60 | * { 61 | transition: none !important; 62 | transform: none !important; 63 | } 64 | } 65 | @layer utilities { 66 | .text-balance { 67 | text-wrap: balance; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { ViewTransitions } from "next-view-transitions"; 3 | import "./globals.css"; 4 | import { serif } from "@/fonts/fonts"; 5 | import Header from "@/components/header/header"; 6 | 7 | export const metadata: Metadata = { 8 | title: "overreacted - A blog by Dan Abramov", 9 | description: "Generated by create next app", 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode; 16 | }>) { 17 | return ( 18 | 19 | 20 | 21 |
22 |
{children}
23 | 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import BlogList from "@/components/blogList/list"; 2 | 3 | export default function Home() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/blogList/list.tsx: -------------------------------------------------------------------------------- 1 | import Link from "@/components/link/link"; 2 | import Color from "colorjs.io"; 3 | import { PostDetail, getAllPostsMeta } from "@/data/post"; 4 | import { sans } from "@/fonts/fonts"; 5 | 6 | export default async function BlogList() { 7 | const posts = await getAllPostsMeta(); 8 | 9 | return ( 10 |
11 | {posts.map((item) => { 12 | return ( 13 | 18 |
19 | 20 |

21 | {new Date(item.meta.date).toLocaleDateString("cn", { 22 | day: "2-digit", 23 | month: "2-digit", 24 | year: "numeric", 25 | })} 26 |

27 |

{item.meta.spoiler}

28 |
29 | 30 | ); 31 | })} 32 |
33 | ); 34 | } 35 | 36 | function PostTitle({ post }: { post: PostDetail }) { 37 | let lightStart = new Color("lab(63 59.32 -1.47)"); 38 | let lightEnd = new Color("lab(33 42.09 -43.19)"); 39 | let lightRange = lightStart.range(lightEnd); 40 | let darkStart = new Color("lab(81 32.36 -7.02)"); 41 | let darkEnd = new Color("lab(78 19.97 -36.75)"); 42 | let darkRange = darkStart.range(darkEnd); 43 | let today = new Date(); 44 | let timeSinceFirstPost = ( 45 | today.valueOf() - new Date(2018, 10, 30).valueOf() 46 | ).valueOf(); 47 | let timeSinceThisPost = ( 48 | today.valueOf() - new Date(post.meta.date).valueOf() 49 | ).valueOf(); 50 | let staleness = timeSinceThisPost / timeSinceFirstPost; 51 | 52 | return ( 53 |

65 | {post.meta.title} 66 |

67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /src/components/header/header.tsx: -------------------------------------------------------------------------------- 1 | import Link from "@/components/link/link"; 2 | import Image from "next/image"; 3 | import HomeLink from "../homeLink/homeLink"; 4 | 5 | export default function Header() { 6 | return ( 7 |
8 | 9 | 10 | 11 | by 12 | 17 | sunshineLixun 24 | 25 | 26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/components/homeLink/homeLink.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "@/components/link/link"; 4 | import { sans } from "@/fonts/fonts"; 5 | import { usePathname } from "next/navigation"; 6 | 7 | export default function HomeLink() { 8 | const pathname = usePathname(); 9 | const isActive = pathname === "/"; 10 | return ( 11 | 19 | 32 | overreacted 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/components/link/link.tsx: -------------------------------------------------------------------------------- 1 | import NextLink from "next/link"; 2 | import { Link as NextViewLink } from "next-view-transitions"; 3 | 4 | export default function Link(props: React.ComponentProps) { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /src/data/post.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs"; 3 | import matter from "gray-matter"; 4 | import { pinyin } from "pinyin-pro"; 5 | import { isChinese } from "@/lib/utils"; 6 | 7 | export interface PostDetail { 8 | meta: { 9 | title: string; 10 | date: string; 11 | spoiler: string; 12 | slug: string; 13 | id: string; 14 | }; 15 | content: string; 16 | } 17 | 18 | const rootDirectory = path.join(process.cwd(), "src", "posts"); 19 | 20 | export const getPostBySlug = async (dir: string) => { 21 | const filePath = path.join(rootDirectory, dir, "/index.mdx"); 22 | 23 | const fileContent = fs.readFileSync(filePath, { encoding: "utf8" }); 24 | 25 | const { data } = matter(fileContent); 26 | 27 | const id = isChinese(dir) 28 | ? pinyin(dir, { 29 | toneType: "none", 30 | separator: "-", 31 | }) 32 | : dir; 33 | 34 | return { 35 | meta: { ...data, slug: dir, id }, 36 | content: fileContent, 37 | } as PostDetail; 38 | }; 39 | 40 | export const getAllPostsMeta = async () => { 41 | const dirs = fs 42 | .readdirSync(rootDirectory, { withFileTypes: true }) 43 | .filter((entry) => entry.isDirectory()) 44 | .map((entry) => entry.name); 45 | 46 | let datas = await Promise.all( 47 | dirs.map(async (dir) => { 48 | const { meta, content } = await getPostBySlug(dir); 49 | return { meta, content }; 50 | }), 51 | ); 52 | 53 | datas.sort((a, b) => { 54 | return Date.parse(a.meta.date) < Date.parse(b.meta.date) ? 1 : -1; 55 | }); 56 | return datas; 57 | }; 58 | 59 | export async function getPost(slug: string) { 60 | const posts = await getAllPostsMeta(); 61 | if (!slug) throw new Error("not found"); 62 | const post = posts.find((post) => post.meta.slug === slug); 63 | if (!post) { 64 | throw new Error("not found"); 65 | } 66 | return post; 67 | } 68 | -------------------------------------------------------------------------------- /src/fonts/fonts.ts: -------------------------------------------------------------------------------- 1 | import { Montserrat, Merriweather } from "next/font/google"; 2 | 3 | export const sans = Montserrat({ 4 | subsets: ["latin"], 5 | display: "swap", 6 | weight: ["400", "700", "900"], 7 | style: ["normal"], 8 | }); 9 | 10 | export const serif = Merriweather({ 11 | subsets: ["latin"], 12 | display: "swap", 13 | weight: ["400", "700"], 14 | style: ["normal", "italic"], 15 | }); 16 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | export function isChinese(value: string) { 2 | return /^[\u4e00-\u9fa5]*$/.test(value); 3 | } 4 | 5 | export function getPostWords(content: string) { 6 | return content.split(" ").filter(Boolean).length; 7 | } 8 | 9 | const WORDS_PER_MINUTE = 200; 10 | export function readingTime(wordsCount: number) { 11 | return Math.ceil(wordsCount / WORDS_PER_MINUTE); 12 | } 13 | -------------------------------------------------------------------------------- /src/posts/a-chain-reaction/components.js: -------------------------------------------------------------------------------- 1 | export { Greeting } from "./greeting"; 2 | -------------------------------------------------------------------------------- /src/posts/a-chain-reaction/greeting.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export function Greeting({ person }) { 4 | return ( 5 |

6 | Hello, {person.firstName}! 7 |

8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/posts/a-chain-reaction/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "A Chain Reaction" 3 | date: "2023-12-11" 4 | spoiler: "The limits of my language mean the limits of my world." 5 | --- 6 | 7 | I wrote a bit of JSX in my editor: 8 | 9 | ```js 10 |

11 | Hello, Alice! 12 |

13 | ``` 14 | 15 | Right now, this information only exists on _my_ device. But with a bit of luck, it will travel through time and space to _your_ device, and appear on _your_ screen. 16 | 17 |

18 | Hello, Alice! 19 |

20 | 21 | The fact that this works is a marvel of engineering. 22 | 23 | Deep inside of your browser, there are pieces of code that know how to display a paragraph or draw text in italics. These pieces of code are different between different browsers, and even between different versions of the same browser. Drawing to the screen is also done differently on different operating systems. 24 | 25 | However, because these concepts have been given agreed-upon _names_ ([`

`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/p) for a paragraph, [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/i) for italics), I can refer to them without worrying how they _really_ work on your device. I can't directly access their internal logic but I know which information I can pass to them (such as a CSS [`className`](https://developer.mozilla.org/en-US/docs/Web/API/Element/className)). Thanks to the web standards, I can be reasonably sure my greeting will appear as I intended. 26 | 27 | Tags like `

` and `` let us refer to the built-in browser concepts. However, names don't _have to_ refer to something built-in. For example, I'm using CSS classes like [`text-2xl`](https://tailwindcss.com/docs/font-size) and [`font-sans`](https://tailwindcss.com/docs/font-family) to style my greeting. I didn't come up with those names myself--they come from a CSS library called Tailwind. I've included it on this page which lets me use any of the CSS class names it defines. 28 | 29 | So why do we like giving names to things? 30 | 31 | --- 32 | 33 | I wrote `

` and ``, and my editor recognized those names. So did your browser. If you've done some web development, you probably recognized them too, and maybe even guessed what would appear on the screen by reading the markup. In that sense, names help us start with a bit of a shared understanding. 34 | 35 | Fundamentally, computers execute relatively basic kinds of instructions--like adding or multiplying numbers, writing stuff to memory and reading from it, or communicating with external devices like a display. Merely showing a `

` on your screen could involve running hundreds of thousands of such instructions. 36 | 37 | If you saw all the instructions your computer ran to display a `

` on the screen, you could hardly guess what they're doing. It's like trying to figure out which song is playing by analyzing all the atoms bouncing around the room. It would seem incomprehensible! You'd need to "zoom out" to see what's going on. 38 | 39 | To describe a complex system, or to instruct a complex system what to do, it helps to separate its behavior into layers that build on each other's concepts. 40 | 41 | This way, people working on screen drivers can focus on how to send the right colors to the right pixels. Then people working on text rendering can focus on how each character should turn into a bunch of pixels. And that lets people like me focus on picking just the right color for my "paragraphs" and "italics". 42 | 43 | We like names because they let us forget what's behind them. 44 | 45 | --- 46 | 47 | I've used many names that other people came up with. Some are built into the browsers, like `

` and ``. Some are built into the tools I'm using, like `text-2xl` and `font-sans`. These may be my building blocks, but what am _I_ building? 48 | 49 | For example, what is this? 50 | 51 | ```js 52 |

53 | Hello, Alice! 54 |

55 | ``` 56 | 57 |

58 | Hello, Alice! 59 |

60 | 61 | From your browser's perspective, this is a paragraph with certain CSS classes (which make it large and purple) and some text inside (part of it is in italics). 62 | 63 | But from _my_ perspective, it's _a greeting for Alice._ Although my greeting _happens_ to be a paragraph, most of the time I want to think about it this way instead: 64 | 65 | ```js 66 | 67 | ``` 68 | 69 | Giving this concept a name provides me with some newfound flexibility. I can now display multiple `Greeting`s without copying and pasting their markup. I can pass different data to them. If I wanted to change how all greetings look and behave, I could do it in a single place. Turning `Greeting` into its own concept lets me adjust _"which greetings to display"_ separately from _"what a greeting is"_. 70 | 71 | However, I have also introduced a problem. 72 | 73 | Now that I've given this concept a name, the "language" in my mind is different from the "language" that your browser speaks. Your browser knows about `

` and ``, but it has never heard of a ``--that's my own concept. If I wanted your browser to understand what I mean, I'd have to "translate" this piece of markup to only use the concepts that your browser already knows. 74 | 75 | I'd need to turn this: 76 | 77 | ```js 78 | 79 | ``` 80 | 81 | into this: 82 | 83 | ```js 84 |

85 | Hello, Alice! 86 |

87 | ``` 88 | 89 | How would I go about that? 90 | 91 | --- 92 | 93 | To name something, I need to define it. 94 | 95 | For example, `alice` does not mean anything until I define `alice`: 96 | 97 | ```js 98 | const alice = { 99 | firstName: "Alice", 100 | birthYear: 1970, 101 | }; 102 | ``` 103 | 104 | Now `alice` refers to that JavaScript object. 105 | 106 | Similarly, I need to actually _define_ what my concept of a `Greeting` means. 107 | 108 | I will define a `Greeting` for any `person` as a paragraph showing "Hello, " followed by _that_ person's first name in italics, plus an exclamation mark: 109 | 110 | ```js 111 | function Greeting({ person }) { 112 | return ( 113 |

114 | Hello, {person.firstName}! 115 |

116 | ); 117 | } 118 | ``` 119 | 120 | Unlike `alice`, I defined `Greeting` as a function. This is because _a greeting_ would have to be different for every person. `Greeting` is a piece of code--it performs a _transformation_ or a _translation_. It _turns_ some data into some UI. 121 | 122 | That gives me an idea for what to do with this: 123 | 124 | ```js 125 | 126 | ``` 127 | 128 | Your browser wouldn't know what a `Greeting` is--that's my own concept. But now that I wrote a definition for that concept, I can _apply_ this definition to "unpack" what I meant. You see, _a greeting for a person is actually a paragraph:_ 129 | 130 | ```js {3-5} 131 | function Greeting({ person }) { 132 | return ( 133 |

134 | Hello, {person.firstName}! 135 |

136 | ); 137 | } 138 | ``` 139 | 140 | Plugging the `alice`'s data into that definition, I end up with this final JSX: 141 | 142 | ```js 143 |

144 | Hello, Alice! 145 |

146 | ``` 147 | 148 | At this point I only refer to the browser's own concepts. By substituting the `Greeting` with what I defined it to be, I have "translated" it for your browser. 149 | 150 | 156 | 157 | Now let's teach a computer to do the same thing. 158 | 159 | --- 160 | 161 | Take a look at what JSX is made of. 162 | 163 | ```js 164 | const originalJSX = ; 165 | console.log(originalJSX.type); // Greeting 166 | console.log(originalJSX.props); // { person: { firstName: 'Alice', birthYear: 1970 } } 167 | ``` 168 | 169 | Under the hood, JSX constructs an object with the `type` property corresponding to the tag, and the `props` property corresponding to the JSX attributes. 170 | 171 | You can think of `type` as being the "code" and `props` as being the "data". To get the result, you need to plug that data _into_ that code like I've done earlier. 172 | 173 | Here is a little function I wrote that does exactly that: 174 | 175 | ```js 176 | function translateForBrowser(originalJSX) { 177 | const { type, props } = originalJSX; 178 | return type(props); 179 | } 180 | ``` 181 | 182 | In this case, `type` will be `Greeting` and `props` will be `{ person: alice }`, so `translateForBrowser()` will return the result of calling `Greeting` with `{ person: alice }` as the argument. 183 | 184 | Which, as you might recall from the previous section, would give me this: 185 | 186 | ```js 187 |

188 | Hello, Alice! 189 |

190 | ``` 191 | 192 | And that's exactly what I wanted! 193 | 194 | You can verify that feeding my original piece of JSX to `translateForBrowser` will produce the "browser JSX" that only refers to concepts like `

` and ``. 195 | 196 | ```js {5-7} 197 | const originalJSX = ; 198 | console.log(originalJSX.type); // Greeting 199 | console.log(originalJSX.props); // { person: { firstName: 'Alice', birthYear: 1970 } } 200 | 201 | const browserJSX = translateForBrowser(originalJSX); 202 | console.log(browserJSX.type); // 'p' 203 | console.log(browserJSX.props); // { className: 'text-2xl font-sans text-purple-400 dark:text-purple-500', children: ['Hello', { type: 'i', props: { children: 'Alice' }, '!'] } 204 | ``` 205 | 206 | There are many things I could do with that "browser JSX". For example, I could turn it into an HTML string to be sent to the browser. I could also convert it into a sequence of instructions that update an already existing DOM node. For now, I won't be focusing on the different ways to use it. All that matters right now is that by the time I have the "browser JSX", there is nothing left to "translate". 207 | 208 | It's as if my `` has dissolved, and `

` and `` are the residue. 209 | 210 | --- 211 | 212 | Let's try something a tiny bit more complex. Suppose I want to wrap my greeting inside a [`

`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details) tag so that it appears collapsed by default: 213 | 214 | ```js {1,3} 215 |
216 | 217 |
218 | ``` 219 | 220 | The browser should display it like this (click "Details" to expand it!) 221 | 222 |
223 | 229 |
230 | 231 | So now my task is to figure out how to turn this: 232 | 233 | ```js 234 |
235 | 236 |
237 | ``` 238 | 239 | into this: 240 | 241 | ```js 242 |
243 |

244 | Hello, Alice! 245 |

246 |
247 | ``` 248 | 249 | Let's see if `translateForBrowser` can already handle that. 250 | 251 | ```js {2-4,9} 252 | const originalJSX = ( 253 |
254 | 255 |
256 | ); 257 | console.log(originalJSX.type); // 'details' 258 | console.log(originalJSX.props); // { children: { type: Greeting, props: { person: alice } } } 259 | 260 | const browserJSX = translateForBrowser(originalJSX); 261 | ``` 262 | 263 | You will get an error inside of the `translateForBrowser` call: 264 | 265 | ```js {3} 266 | function translateForBrowser(originalJSX) { 267 | const { type, props } = originalJSX; 268 | return type(props); // 🔴 TypeError: type is not a function 269 | } 270 | ``` 271 | 272 | What happened here? My `translateForBrowser` implementation assumed that `type`--that is, `originalJSX.type`--is always a function like `Greeting`. 273 | 274 | However, notice that `originalJSX.type` is actually a _string_ this time: 275 | 276 | ```js {6} 277 | const originalJSX = ( 278 |
279 | 280 |
281 | ); 282 | console.log(originalJSX.type); // 'details' 283 | console.log(originalJSX.props); // { children: { type: Greeting, props: { person: alice } } } 284 | ``` 285 | 286 | When you start a JSX tag with a lower case (like `
`), by convention it's assumed that you _want_ a built-in tag rather than some function you defined. 287 | 288 | Since built-in tags don't have any code associated with them (that code is somewhere inside your browser!), the `type` will be a string like `'details'`. How `
` work is opaque to my code--all I really know is its name. 289 | 290 | Let's split the logic in two cases, and skip translating the built-ins for now: 291 | 292 | ```js {3,5-7} 293 | function translateForBrowser(originalJSX) { 294 | const { type, props } = originalJSX; 295 | if (typeof type === "function") { 296 | return type(props); 297 | } else if (typeof type === "string") { 298 | return originalJSX; 299 | } 300 | } 301 | ``` 302 | 303 | After this change, `translateForBrowser` will only attempt to call some function if the original JSX's `type` actually _is_ a function like `Greeting`. 304 | 305 | So that's the result I wanted, right?... 306 | 307 | ```js 308 |
309 | 310 |
311 | ``` 312 | 313 | Wait. What I wanted is this: 314 | 315 | ```js 316 |
317 |

318 | Hello, Alice! 319 |

320 |
321 | ``` 322 | 323 | In my translation process, I want to _skip over_ the `
` tag because its implementation is opaque to me. I can't do anything useful with it--it is fully up to the browser. However, anything _inside_ of it may still need to be translated! 324 | 325 | Let's fix `translateForBrowser` to translate any built-in tag's children: 326 | 327 | ```js {6-12} 328 | function translateForBrowser(originalJSX) { 329 | const { type, props } = originalJSX; 330 | if (typeof type === "function") { 331 | return type(props); 332 | } else if (typeof type === "string") { 333 | return { 334 | type, 335 | props: { 336 | ...props, 337 | children: translateForBrowser(props.children), 338 | }, 339 | }; 340 | } 341 | } 342 | ``` 343 | 344 | With this change, when it meets an element like `
...
`, it will return another `
...
` tag, but the stuff _inside_ of it would be translated with my function again--so the `Greeting` will be gone: 345 | 346 | ```js 347 |
348 |

349 | Hello, Alice! 350 |

351 |
352 | ``` 353 | 354 | And _now_ I am speaking the browser's "language" again: 355 | 356 |
357 |

358 | Hello, Alice! 359 |

360 |
361 | 362 | The `Greeting` has been dissolved. 363 | 364 | --- 365 | 366 | Now suppose that I try to define an `ExpandableGreeting`: 367 | 368 | ```js 369 | function ExpandableGreeting({ person }) { 370 | return ( 371 |
372 | 373 |
374 | ); 375 | } 376 | ``` 377 | 378 | Here is my new original JSX: 379 | 380 | ```js 381 | 382 | ``` 383 | 384 | If I run it through `translateForBrowser`, I'll get this JSX in return: 385 | 386 | ```js 387 |
388 | 389 |
390 | ``` 391 | 392 | But that's not what I wanted! It still has a `Greeting` in it, and we don't consider a piece of JSX "browser-ready" until _all_ of my own concepts are gone. 393 | 394 | This is a bug in my `translateForBrowser` function. When it calls a function like `ExpandableGreeting`, it will return its output, and not do anything else. But we need to keep on going! That returned JSX _also_ needs to be translated. 395 | 396 | Luckily, there is an easy way I can solve this. When I call a function like `ExpandableGreeting`, I can take the JSX it returned and translate _that_ next: 397 | 398 | ```js {4-5} 399 | function translateForBrowser(originalJSX) { 400 | const { type, props } = originalJSX; 401 | if (typeof type === "function") { 402 | const returnedJSX = type(props); 403 | return translateForBrowser(returnedJSX); 404 | } else if (typeof type === "string") { 405 | return { 406 | type, 407 | props: { 408 | ...props, 409 | children: translateForBrowser(props.children), 410 | }, 411 | }; 412 | } 413 | } 414 | ``` 415 | 416 | I also need to stop the process when there's nothing left to translate, like if it receives `null` or a string. If it receives an array of things, I need to translate each of them. With these two fixes, `translateForBrowser` is complete: 417 | 418 | ```js {2-7} 419 | function translateForBrowser(originalJSX) { 420 | if (originalJSX == null || typeof originalJSX !== "object") { 421 | return originalJSX; 422 | } 423 | if (Array.isArray(originalJSX)) { 424 | return originalJSX.map(translateForBrowser); 425 | } 426 | const { type, props } = originalJSX; 427 | if (typeof type === "function") { 428 | const returnedJSX = type(props); 429 | return translateForBrowser(returnedJSX); 430 | } else if (typeof type === "string") { 431 | return { 432 | type, 433 | props: { 434 | ...props, 435 | children: translateForBrowser(props.children), 436 | }, 437 | }; 438 | } 439 | } 440 | ``` 441 | 442 | Now, suppose that I start with this: 443 | 444 | ```js 445 | 446 | ``` 447 | 448 | It will turn into this: 449 | 450 | ```js 451 |
452 | 453 |
454 | ``` 455 | 456 | Which will turn into this: 457 | 458 | ```js 459 |
460 |

461 | Hello, Alice! 462 |

463 |
464 | ``` 465 | 466 | And at that point, the process will stop. 467 | 468 | --- 469 | 470 | Let's see how this works one more time, with a bit of extra depth. 471 | 472 | I'll define `WelcomePage` like this: 473 | 474 | ```js 475 | function WelcomePage() { 476 | return ( 477 |
478 |

Welcome

479 | 480 | 481 | 482 |
483 | ); 484 | } 485 | ``` 486 | 487 | Now let's say I start the process with this original JSX: 488 | 489 | ```js 490 | 491 | ``` 492 | 493 | Can you retrace the sequence of transformations in your head? 494 | 495 | Let's do it step by step together. 496 | 497 | First, imagine `WelcomePage` dissolving, leaving behind its output: 498 | 499 | ```js {1-6} 500 |
501 |

Welcome

502 | 503 | 504 | 505 |
506 | ``` 507 | 508 | Then imagine each `ExpandableGreeting` dissolving, leaving behind _its_ output: 509 | 510 | ```js {3-11} 511 |
512 |

Welcome

513 |
514 | 515 |
516 |
517 | 518 |
519 |
520 | 521 |
522 |
523 | ``` 524 | 525 | Then imagine each `Greeting` dissolving, leaving behind _its_ output: 526 | 527 | ```js {4-6,9-11,14-16} 528 |
529 |

Welcome

530 |
531 |

532 | Hello, Alice! 533 |

534 |
535 |
536 |

537 | Hello, Bob! 538 |

539 |
540 |
541 |

542 | Hello, Crystal! 543 |

544 |
545 |
546 | ``` 547 | 548 | And now there is nothing left to "translate". All _my_ concepts have dissolved. 549 | 550 |
551 |

Welcome

552 |
553 |

554 | Hello, Alice! 555 |

556 |
557 |
558 |

559 | Hello, Bob! 560 |

561 |
562 |
563 |

564 | Hello, Crystal! 565 |

566 |
567 |
568 | 569 | This feels like a chain reaction. You mix a bit of data and code, and it keeps transforming until there is no more code to run, and only the residue is left. 570 | 571 | It would be nice if there was a library that could do this for us. 572 | 573 | But wait, here's a question. These transformations have to happen _somewhere_ on the way between your computer and mine. So where _do_ they happen? 574 | 575 | Do they happen on your computer? 576 | 577 | Or do they happen on mine? 578 | -------------------------------------------------------------------------------- /src/posts/math/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "支持数学公式" 3 | date: "2024-01-12" 4 | spoiler: "katex" 5 | --- 6 | 7 | Trying out maths rendering via markdown. 8 | 9 | This is an inline _equation:_ $$V_{sphere} = \frac{4}{3}\pi r^3$$, followed by a display style equation after lots more lines of paragraph to test vertical alignment of inline expressions as well as the standalone expressions. 10 | Here is also some **styled text**. 11 | 12 | This is another inline expression $$\sum_{i=0}^n i^2 = \frac{(n^2+n)(2n+1)}{6}$$ followed by a normal expression, which align to the **_middle_** of the content: 13 | 14 | $$ 15 | \begin{array}{c} 16 | \nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} & 17 | = \frac{4\pi}{c}\vec{\mathbf{j}} \nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\ 18 | \nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\ 19 | \nabla \cdot \vec{\mathbf{B}} & = 0 20 | \end{array} 21 | $$ 22 | -------------------------------------------------------------------------------- /src/posts/npm-audit-broken-by-design/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "npm audit: Broken by Design" 3 | date: "2021-07-07" 4 | spoiler: "Found 99 vulnerabilities (84 moderately irrelevant, 15 highly irrelevant)" 5 | --- 6 | 7 | Security is important. Nobody wants to be the person advocating for less security. So nobody wants to say it. But somebody has to say it. 8 | 9 | So I guess I’ll say it. 10 | 11 | **The way `npm audit` works is broken. Its rollout as a default after every `npm install` was rushed, inconsiderate, and inadequate for the front-end tooling.** 12 | 13 | Have you heard the story about [the boy who cried wolf?](https://en.wikipedia.org/wiki/The_Boy_Who_Cried_Wolf) Spoiler alert: the wolf eats the sheep. If we don’t want our sheep to be eaten, we need better tools. 14 | 15 | As of today, `npm audit` is a stain on the entire npm ecosystem. The best time to fix it was before rolling it out as a default. The next best time to fix it is now. 16 | 17 | In this post, I will briefly outline how it works, why it’s broken, and what changes I’m hoping to see. 18 | 19 | --- 20 | 21 | _Note: this article is written with a critical and somewhat snarky tone. I understand it’s super hard to maintain massive projects like Node.js/npm, and that mistakes may take a while to become apparent. I am frustrated only at the situation, not at the people involved. I kept the snarky tone because the level of my frustration has increased over the years, and I don’t want to pretend that the situation isn’t as dire as it really is. Most of all I am frustrated to see all the people for whom this is the first programming experience, as well as all the people who are blocked from deploying their changes due to irrelevant warnings. I am excited that [this issue is being considered](https://twitter.com/bitandbang/status/1412803378279759872) and I will do my best to provide input on the proposed solutions! 💜_ 22 | 23 | --- 24 | 25 | ## How does npm audit work? 26 | 27 | _[Skip ahead](#why-is-npm-audit-broken) if you already know how it works._ 28 | 29 | Your Node.js application has a dependency tree. It might look like this: 30 | 31 | ``` 32 | your-app 33 | - view-library@1.0.0 34 | - design-system@1.0.0 35 | - model-layer@1.0.0 36 | - database-layer@1.0.0 37 | - network-utility@1.0.0 38 | ``` 39 | 40 | Most likely, it’s a lot deeper. 41 | 42 | Now say there’s a vulnerability discovered in `network-utility@1.0.0`: 43 | 44 | ``` 45 | your-app 46 | - view-library@1.0.0 47 | - design-system@1.0.0 48 | - model-layer@1.0.0 49 | - database-layer@1.0.0 50 | - network-utility@1.0.0 (Vulnerable!) 51 | ``` 52 | 53 | This gets published in a special registry that `npm` will access next time you run `npm audit`. Since npm v6+, you’ll learn about this after every `npm install`: 54 | 55 | ``` 56 | 1 vulnerabilities (0 moderate, 1 high) 57 | 58 | To address issues that do not require attention, run: 59 | npm audit fix 60 | 61 | To address all issues (including breaking changes), run: 62 | npm audit fix --force 63 | ``` 64 | 65 | You run `npm audit fix`, and npm tries to install the latest `network-utility@1.0.1` with the fix in it. As long as `database-layer` specifies that it depends not on _exactly_ on `network-utility@1.0.0` but some permissible range that includes `1.0.1`, the fix “just works” and you get a working application: 66 | 67 | ``` 68 | your-app 69 | - view-library@1.0.0 70 | - design-system@1.0.0 71 | - model-layer@1.0.0 72 | - database-layer@1.0.0 73 | - network-utility@1.0.1 (Fixed!) 74 | ``` 75 | 76 | Alternatively, maybe `database-layer@1.0.0` depends strictly on `network-utility@1.0.0`. In that case, the maintainer of `database-layer` needs to release a new version too, which would allow `network-utility@1.0.1` instead: 77 | 78 | ``` 79 | your-app 80 | - view-library@1.0.0 81 | - design-system@1.0.0 82 | - model-layer@1.0.0 83 | - database-layer@1.0.1 (Updated to allow the fix.) 84 | - network-utility@1.0.1 (Fixed!) 85 | ``` 86 | 87 | Finally, if there is no way to gracefully upgrade the tree, you could try `npm audit fix --force`. This is supposed to be used if `database-layer` doesn’t accept the new version of `network-utility` and _also_ doesn’t release an update to accept it. So you’re kind of taking matters in your own hands, potentially risking breaking changes. Seems like a reasonable option to have. 88 | 89 | **This is how `npm audit` is supposed to work in theory.** 90 | 91 | As someone wise said, in theory there is no difference between theory and practice. But in practice there is. And that’s where all the fun starts. 92 | 93 | ## Why is npm audit broken? 94 | 95 | Let’s see how this works in practice. I’ll use Create React App for my testing. 96 | 97 | If you’re not familiar with it, it’s an integration facade that combines multiple other tools, including Babel, webpack, TypeScript, ESLint, PostCSS, Terser, and others. Create React App takes your JavaScript source code and converts it into a static HTML+JS+CSS folder. **Notably, it does _not_ produce a Node.js app.** 98 | 99 | Let’s create a new project! 100 | 101 | ``` 102 | npx create-react-app myapp 103 | ``` 104 | 105 | Immediately upon creating a project, I see this: 106 | 107 | ``` 108 | found 5 vulnerabilities (3 moderate, 2 high) 109 | run `npm audit fix` to fix them, or `npm audit` for details 110 | ``` 111 | 112 | Oh no, that seems bad! My just-created app is already vulnerable! 113 | 114 | Or so npm tells me. 115 | 116 | Let’s run `npm audit` to see what’s up. 117 | 118 | ### First “vulnerability” 119 | 120 | Here is the first problem reported by `npm audit`: 121 | 122 | ``` 123 | ┌───────────────┬──────────────────────────────────────────────────────────────┐ 124 | │ Moderate │ Regular Expression Denial of Service │ 125 | ├───────────────┼──────────────────────────────────────────────────────────────┤ 126 | │ Package │ browserslist │ 127 | ├───────────────┼──────────────────────────────────────────────────────────────┤ 128 | │ Patched in │ >=4.16.5 │ 129 | ├───────────────┼──────────────────────────────────────────────────────────────┤ 130 | │ Dependency of │ react-scripts │ 131 | ├───────────────┼──────────────────────────────────────────────────────────────┤ 132 | │ Path │ react-scripts > react-dev-utils > browserslist │ 133 | ├───────────────┼──────────────────────────────────────────────────────────────┤ 134 | │ More info │ https://npmjs.com/advisories/1747 │ 135 | └───────────────┴──────────────────────────────────────────────────────────────┘ 136 | ``` 137 | 138 | Apparently, `browserslist` is vulnerable. What’s that and how is it used? Create React App generates CSS files optimized for the browsers you target. For example, you can say you only target modern browsers in your `package.json`: 139 | 140 | ```jsx 141 | "browserslist": { 142 | "production": [ 143 | ">0.2%", 144 | "not dead", 145 | "not op_mini all" 146 | ], 147 | "development": [ 148 | "last 1 chrome version", 149 | "last 1 firefox version", 150 | "last 1 safari version" 151 | ] 152 | } 153 | ``` 154 | 155 | Then it won’t include outdated flexbox hacks in the output. Since multiple tools rely on the same configuration format for the browsers you target, Create React App uses the shared `browserslist` package to parse the configuration file. 156 | 157 | So what’s the vulnerability here? [“Regular Expression Denial of Service”](https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS) means that there is a regex in `browserslist` that, with malicious input, could become very slow. So an attacker can craft a special configuration string that, when passed to `browserslist`, could slow it down exponentially. This sounds bad... 158 | 159 | Wait, what?! Let’s remember how your app works. You have a configuration file _on your machine_. You _build_ your project. You get static HTML+CSS+JS in a folder. You put it on static hosting. There is simply **no way** for your application user to affect your `package.json` configuration. **This doesn’t make any sense.** If the attacker already has access to your machine and can change your configuration files, you have a much bigger problem than slow regular expressions! 160 | 161 | Okay, so I guess this “Moderate” “vulnerability” was neither moderate nor a vulnerability in the context of a project. Let’s keep going. 162 | 163 | **Verdict: this “vulnerability” is absurd in this context.** 164 | 165 | ### Second “vulnerability” 166 | 167 | Here is the next issue `npm audit` has helpfully reported: 168 | 169 | ``` 170 | ┌───────────────┬──────────────────────────────────────────────────────────────┐ 171 | │ Moderate │ Regular expression denial of service │ 172 | ├───────────────┼──────────────────────────────────────────────────────────────┤ 173 | │ Package │ glob-parent │ 174 | ├───────────────┼──────────────────────────────────────────────────────────────┤ 175 | │ Patched in │ >=5.1.2 │ 176 | ├───────────────┼──────────────────────────────────────────────────────────────┤ 177 | │ Dependency of │ react-scripts │ 178 | ├───────────────┼──────────────────────────────────────────────────────────────┤ 179 | │ Path │ react-scripts > webpack-dev-server > chokidar > glob-parent │ 180 | ├───────────────┼──────────────────────────────────────────────────────────────┤ 181 | │ More info │ https://npmjs.com/advisories/1751 │ 182 | └───────────────┴──────────────────────────────────────────────────────────────┘ 183 | ``` 184 | 185 | Let’s look at the `webpack-dev-server > chokidar > glob-parent` dependency chain. Here, `webpack-dev-server` is a **development-only** server that’s used to quickly serve your app **locally**. It uses `chokidar` to watch your filesystem for changes (such as when you save a file in your editor). And it uses [`glob-parent`](https://www.npmjs.com/package/glob-parent) in order to extract a part of the filesystem path from a filesystem watch pattern. 186 | 187 | Unfortunately, `glob-parent` is vulnerable! If an attacker supplies a specially crafted filepath, it could make this function exponentially slow, which would... 188 | 189 | Wait, what?! The development server is on your computer. The files are on your computer. The file watcher is using the configuration that _you_ have specified. None of this logic leaves your computer. If your attacker is sophisticated enough to log into _your machine_ during local development, the last thing they’ll want to do is to craft special long filepaths to slow down your development. They’ll want to steal your secrets instead. **So this whole threat is absurd.** 190 | 191 | Looks like this “Moderate” “vulnerability” was neither moderate nor a vulnerability in the context of a project. 192 | 193 | **Verdict: this “vulnerability” is absurd in this context.** 194 | 195 | ### Third “vulnerability” 196 | 197 | Let’s have a look at this one: 198 | 199 | ``` 200 | ┌───────────────┬──────────────────────────────────────────────────────────────┐ 201 | │ Moderate │ Regular expression denial of service │ 202 | ├───────────────┼──────────────────────────────────────────────────────────────┤ 203 | │ Package │ glob-parent │ 204 | ├───────────────┼──────────────────────────────────────────────────────────────┤ 205 | │ Patched in │ >=5.1.2 │ 206 | ├───────────────┼──────────────────────────────────────────────────────────────┤ 207 | │ Dependency of │ react-scripts │ 208 | ├───────────────┼──────────────────────────────────────────────────────────────┤ 209 | │ Path │ react-scripts > webpack > watchpack > watchpack-chokidar2 > │ 210 | │ │ chokidar > glob-parent │ 211 | ├───────────────┼──────────────────────────────────────────────────────────────┤ 212 | │ More info │ https://npmjs.com/advisories/1751 │ 213 | └───────────────┴──────────────────────────────────────────────────────────────┘ 214 | ``` 215 | 216 | Wait, it’s the same thing as above, but through a different dependency path. 217 | 218 | **Verdict: this “vulnerability” is absurd in this context.** 219 | 220 | ### Fourth “vulnerability” 221 | 222 | Oof, this one looks really bad! **`npm audit` has the nerve to show it in red color:** 223 | 224 | ``` 225 | ┌───────────────┬──────────────────────────────────────────────────────────────┐ 226 | │ High │ Denial of Service │ 227 | ├───────────────┼──────────────────────────────────────────────────────────────┤ 228 | │ Package │ css-what │ 229 | ├───────────────┼──────────────────────────────────────────────────────────────┤ 230 | │ Patched in │ >=5.0.1 │ 231 | ├───────────────┼──────────────────────────────────────────────────────────────┤ 232 | │ Dependency of │ react-scripts │ 233 | ├───────────────┼──────────────────────────────────────────────────────────────┤ 234 | │ Path │ react-scripts > @svgr/webpack > @svgr/plugin-svgo > svgo > │ 235 | │ │ css-select > css-what │ 236 | ├───────────────┼──────────────────────────────────────────────────────────────┤ 237 | │ More info │ https://npmjs.com/advisories/1754 │ 238 | └───────────────┴──────────────────────────────────────────────────────────────┘ 239 | ``` 240 | 241 | What is this “high” severity issue? “Denial of service.” I don’t want service to be denied! That would be really bad... Unless... 242 | 243 | Let’s look at the [issue](https://www.npmjs.com/advisories/1754). Apparently [`css-what`](https://www.npmjs.com/package/css-what), which is a parser for CSS selectors, can be slow with specially crafted input. This parser is used by a plugin that generates React components from SVG files. 244 | 245 | So what this means is that if the attacker takes control of my development machine or my source code, they put a special SVG file that will have a specially crafted CSS selector in it, which will make my build slow. That checks out... 246 | 247 | Wait, what?! If the attacker can modify my app’s source code, they’ll probably just put a bitcoin miner in it. Why would they add SVG files into my app, unless you can mine bitcoins with SVG? Again, this doesn’t make _any_ sense. 248 | 249 | **Verdict: this “vulnerability” is absurd in this context.** 250 | 251 | So much for the “high” severity. 252 | 253 | ### Fifth “vulnerability” 254 | 255 | ``` 256 | ┌───────────────┬──────────────────────────────────────────────────────────────┐ 257 | │ High │ Denial of Service │ 258 | ├───────────────┼──────────────────────────────────────────────────────────────┤ 259 | │ Package │ css-what │ 260 | ├───────────────┼──────────────────────────────────────────────────────────────┤ 261 | │ Patched in │ >=5.0.1 │ 262 | ├───────────────┼──────────────────────────────────────────────────────────────┤ 263 | │ Dependency of │ react-scripts │ 264 | ├───────────────┼──────────────────────────────────────────────────────────────┤ 265 | │ Path │ react-scripts > optimize-css-assets-webpack-plugin > cssnano │ 266 | │ │ > cssnano-preset-default > postcss-svgo > svgo > css-select │ 267 | │ │ > css-what │ 268 | ├───────────────┼──────────────────────────────────────────────────────────────┤ 269 | │ More info │ https://npmjs.com/advisories/1754 │ 270 | 271 | └───────────────┴──────────────────────────────────────────────────────────────┘ 272 | ``` 273 | 274 | This is just the same exact thing as above. 275 | 276 | **Verdict: this “vulnerability” is absurd in this context.** 277 | 278 | ### Shall we keep going? 279 | 280 | So far the boy has cried wolf five times. Two of them are duplicates. The rest are absurd non-issues in the context of how these dependencies are used. 281 | 282 | Five false alarms wouldn’t be too bad. 283 | 284 | **Unfortunately, there are hundreds.** 285 | 286 | Here are a [few](https://github.com/facebook/create-react-app/issues/11053) [typical](https://github.com/facebook/create-react-app/issues/11092) threads, but there are many more [linked from here](https://github.com/facebook/create-react-app/issues/11174): 287 | 288 | Screenshot of many GH threads 289 | 290 | **I’ve spent several hours looking through every `npm audit` issue reported to us over the last several months, and they all appear to be false positives in the context of a build tool dependency like Create React App.** 291 | 292 | Of course, they are possible to fix. We could relax some of the top-level dependencies to not be exact (leading to bugs in patches slipping in more often). We could make more releases just to stay ahead of this security theater. 293 | 294 | But this is inadequate. Imagine if your tests failed 99% of the times for bogus reasons! This wastes person-decades of effort and makes everyone miserable: 295 | 296 | - **It makes beginners miserable** because they run into this as their first programming experience in the Node.js ecosystem. As if installing Node.js/npm was not confusing enough (good luck if you added `sudo` somewhere because a tutorial told you), this is what they’re greeted with when they try examples online or even when they create a project. A beginner doesn’t know what a RegExp _is_. Of course they don’t have the know-how to be able to tell whether a RegExp DDoS or prototype pollution is something to worry about when they’re using a build tool to produce a static HTML+CSS+JS site. 297 | - **It makes experienced app developers miserable** because they have to either waste time doing obviously unnecessary work, or fight with their security departments trying to explain how `npm audit` is a broken tool unsuitable for real security audits _by design_. Yeah, somehow it was made a default in this state. 298 | - **It makes maintainers miserable** because instead of working on bugfixes and improvements, they have to pull in bogus vulnerability fixes that can’t possibly affect their project because otherwise their users are frustrated, scared, or both. 299 | - **Someday, it will make our users miserable** because we have trained an entire generation of developers to either not understand the warnings due to being overwhelmed, or to simply _ignore_ them because they always show up but the experienced developers (correctly) tell them there is no real issue in each case. 300 | 301 | It doesn’t help that `npm audit fix` (which the tool suggests using) is buggy. I ran `npm audit fix --force` today and it **downgraded** the main dependency to a three-year-old version with actual _real_ vulnerabilities. Thanks, npm, great job. 302 | 303 | ## What next? 304 | 305 | I don’t know how to solve this. But I didn’t add this system in the first place, so I’m probably not the best person to solve it. All I know is it’s horribly broken. 306 | 307 | There are a few possible solutions that I have seen. 308 | 309 | - **Move dependency to `devDependencies` if it doesn’t run in production.** This offers a way to specify that some dependency isn’t used in production code paths, so there is no risk associated with it. However, this solution is flawed: 310 | - `npm audit` still warns for development dependencies by default. You have to _know_ to run `npm audit --production` to not see the warnings from development dependencies. People who know to do that probably already don’t trust it anyway. This also doesn’t help beginners or people working at companies whose security departments want to audit everything. 311 | - `npm install` still uses information from plain `npm audit`, so you will effectively still see all the false positives every time you install something. 312 | - As any security professional will tell you, development dependencies actually _are_ an attack vector, and perhaps one of the most dangerous ones because it’s so hard to detect and the code runs with high trust assumptions. **This is why the situation is so bad in particular: any real issue gets buried below dozens of non-issues that `npm audit` is training people and maintainers to ignore.** It’s only a matter of time until this happens. 313 | - **Inline all dependencies during publish.** This is what I’m increasingly seeing packages similar to Create React App do. For example, both [Vite](https://unpkg.com/browse/vite@2.4.1/dist/node/) and [Next.js](https://unpkg.com/browse/next@11.0.1/dist/) simply bundle their dependencies directly in the package instead of relying on the npm `node_modules` mechanism. From a maintainer’s point of view, [the upsides are clear](https://github.com/vitejs/vite/blob/main/CONTRIBUTING.md#notes-on-dependencies): you get faster boot time, smaller downloads, and — as a nice bonus — no bogus vulnerability reports from your users. It’s a neat way to game the system but I’m worried about the incentives npm is creating for the ecosystem. Inlining dependencies kind of goes against the whole point of npm. 314 | - **Offer some way to counter-claim vulnerability reports.** The problem is not entirely unknown to Node.js and npm, of course. Different people have worked on different suggestions to fix it. For example, there is a [proposal](https://github.com/npm/rfcs/pull/18) for a way to manually resolve audit warnings so that they don’t display again. However, this still places the burden on app users, which don’t necessarily have context on what vulnerabilities deeper in the tree are real or bogus. I also have a [proposal](https://twitter.com/dan_abramov/status/1412380714012594178): I need a way to mark for my users that a certain vulnerability can’t possibly affect them. If you don’t trust my judgement, why are you running my code on your computer? I’d be happy to discuss other options too. 315 | 316 | The root of the issue is that npm added a default behavior that, in many situations, leads to a 99%+ false positive rate, creates an incredibly confusing first programming experience, makes people fight with security departments, makes maintainers never want to deal with Node.js ecosystem ever again, and at some point will lead to actually bad vulnerabilities slipping in unnnoticed. 317 | 318 | Something has to be done. 319 | 320 | In the meantime, I am planning to close all GitHub issues from `npm audit` that I see going forward that don’t correspond to a _real_ vulnerability that can affect the project. I invite other maintainers to adopt the same policy. This will create frustration for our users, but the core of the issue is with npm. I am done with this security theater. Node.js/npm have all the power to fix the problem. I am in contact with them, and I hope to see this problem prioritized. 321 | 322 | Today, `npm audit` is broken by design. 323 | 324 | Beginners, experienced developers, maintainers, security departments, and, most importantly — our users — deserve better. 325 | -------------------------------------------------------------------------------- /src/posts/readme/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Nextjs + tailwindcss 实现 大神 dan 一模一样的博客 3 | date: "2024-02-23" 4 | spoiler: Nextjs + tailwindcss 实战 5 | --- 6 | 7 | 大神 [dan](https://overreacted.io/) 的博客相信大家都看过,博客质量那是不用多说,懂的都懂。 说到博客样式,我比较喜欢这种简约风。其中博客中还支持组件交互效果。 所以我决定用 Nextjs + tailwindcss 实现一模一样的博客学习下。 8 | 9 | 技术选型: 10 | 11 | - Nextjs 12 | - tailwindcss 13 | - MDXRemote 14 | 15 | 部署: 16 | 17 | - vercel 18 | 19 | ## 项目文件结构: 20 | 21 | ``` 22 | |-- dan-blog 23 | |-- app 24 | | |-- favicon.ico 25 | | |-- globals.css 26 | | |-- layout.tsx 27 | | |-- page.tsx 博客列表(首页) 28 | | |-- [slug] 博客详情 29 | | |-- layout.tsx 30 | | |-- markdown.css 31 | | |-- page.tsx 32 | |-- components 33 | | |-- blogList 34 | | | |-- list.tsx 博客列表 35 | | |-- header 36 | | | |-- header.tsx 顶部header 37 | | |-- homeLink 38 | | |-- homeLink.tsx 顶部导航 39 | |-- data 40 | | |-- post.ts 处理博客数据(博客列表、博客详情) 41 | |-- fonts 42 | | |-- fonts.ts 全局字体 43 | |-- lib 44 | | |-- utils.ts 45 | |-- posts 写博客的地方 46 | |-- 博客文件夹1 47 | | |-- index.mdx 48 | |-- 博客文件夹2 49 | | |-- index.mdx 50 | |-- 博客文件夹3 51 | | |-- index.mdx 52 | |-- 博客文件夹4 53 | | |-- index.mdx 54 | |-- 博客文件夹5 55 | |-- components.js 56 | |-- 你的组件.js 57 | |-- index.mdx 58 | |-- 你的组件.js 59 | |-- 你的组件.js 60 | 61 | 62 | ``` 63 | 64 | 我并不打算直接引入database来存储markdown文件,这样成本太大,你必须要选择一种数据库,还要编写数据库增删改查代码。对于一个本项目来说,甚至是博客这种小项目来说,得不偿失。 65 | 66 | 我规划将博客文章的markdown文件放在项目中,通过读取文件的方式来渲染博客文章。这样做的好处是,你可以直接在项目中编写markdown文件,push到github,vercel就会自动部署,你的博客就更新了。 67 | 68 | nextjs的服务端组件能够很好的支持这种需求。我们可以通过服务端组件来读取文件,然后渲染到页面上。 69 | 70 | 在编写代码之前,你需要了解下nextjs基本工作原理,app router工作原理,以及动态路由工作原理。这样你才能更好的理解下面的代码。 71 | 72 | ## 博客列表 73 | 74 | layout文件代码(src/app/layout.tsx) 75 | 76 | ```js 77 | import type { Metadata } from "next"; 78 | import "./globals.css"; 79 | import { serif } from "@/fonts/fonts"; 80 | import Header from "@/components/header/header"; 81 | 82 | export const metadata: Metadata = { 83 | title: "overreacted - A blog by Dan Abramov", 84 | description: "Generated by create next app", 85 | }; 86 | 87 | export default function RootLayout({ 88 | children, 89 | }: Readonly<{ 90 | children: React.ReactNode; 91 | }>) { 92 | return ( 93 | 94 | 95 |
96 |
{children}
97 | 98 | 99 | ); 100 | } 101 | 102 | ``` 103 | 104 | 字体你可以选择你喜欢的样式,这里保持跟dan一模一样的字体 105 | 106 | 字体文件代码(src/fonts/fonts.ts) 107 | 108 | ```js 109 | import { Montserrat, Merriweather } from "next/font/google"; 110 | 111 | export const sans = Montserrat({ 112 | subsets: ["latin"], 113 | display: "swap", 114 | weight: ["400", "700", "900"], 115 | style: ["normal"], 116 | }); 117 | 118 | export const serif = Merriweather({ 119 | subsets: ["latin"], 120 | display: "swap", 121 | weight: ["400", "700"], 122 | style: ["normal", "italic"], 123 | }); 124 | ``` 125 | 126 | page作为首页,要渲染博客列表。在这之前需要先拿到博客列表数据。这里我将所有的博客md文件放在项目中的posts文件夹中(src/app/posts)。每一篇博客创建一个文件夹,文件夹中包含一个index.md文件,index.md就是写博客的地方,如果你需要交互组件,在当前文件夹中编写的组件代码,最后通过公共的components文件统一导出所有组件。 127 | 128 | 20240223173442.jpg 133 | 134 | page文件代码(src/app/page.tsx) 135 | 136 | ```js 137 | import BlogList from "@/components/blogList/list"; 138 | 139 | export default function Home() { 140 | return ; 141 | } 142 | ``` 143 | 144 | 这里我将component单独抽离到app路径外,你可以更好的管理你的组件。app文件下的文件都是页面文件。这样划分,职责功能更加清晰。 145 | 146 | BlogList组件(src/components/blogList/list.tsx) 147 | 148 | ```js 149 | export default async function BlogList() { 150 | return
...
; 151 | } 152 | ``` 153 | 154 | 接下来就需要获取博客列表数据,新建data文件夹(src/app/data/post.ts),这里集中处理读取博客内容,获取到博客列表信息,和博客详情。可以理解为数据库操作。 155 | 156 | 读取全部文章数据: 157 | 158 | ```js 159 | const rootDirectory = path.join(process.cwd(), "src", "posts"); 160 | 161 | export const getAllPostsMeta = async () => { 162 | // 获取到src/posts/下所有文件 163 | const dirs = fs 164 | .readdirSync(rootDirectory, { withFileTypes: true }) 165 | .filter((entry) => entry.isDirectory()) 166 | .map((entry) => entry.name); 167 | 168 | // 解析文章数据,拿到标题、日期、简介 169 | let datas = await Promise.all( 170 | dirs.map(async (dir) => { 171 | const { meta, content } = await getPostBySlug(dir); 172 | return { meta, content }; 173 | }), 174 | ); 175 | 176 | // 文章日期排序,最新的在最前面 177 | datas.sort((a, b) => { 178 | return Date.parse(a.meta.date) < Date.parse(b.meta.date) ? 1 : -1; 179 | }); 180 | return datas; 181 | }; 182 | ``` 183 | 184 | ```js 185 | export const getPostBySlug = async (dir: string) => { 186 | const filePath = path.join(rootDirectory, dir, "/index.mdx"); 187 | 188 | const fileContent = fs.readFileSync(filePath, { encoding: "utf8" }); 189 | 190 | // gray-matter库是一个解析markdown内容,可以拿到markdown文件的meta信息和content内容 191 | const { data } = matter(fileContent); 192 | 193 | // 如果文件名是中文,转成拼音 194 | const id = isChinese(dir) 195 | ? pinyin(dir, { 196 | toneType: "none", 197 | separator: "-", 198 | }) 199 | : dir; 200 | 201 | return { 202 | meta: { ...data, slug: dir, id }, 203 | content: fileContent, 204 | } as PostDetail; 205 | }; 206 | ``` 207 | 208 | 补全BlogList组件逻辑 209 | 210 | ```js 211 | export default async function BlogList() { 212 | const posts = await getAllPostsMeta(); 213 | 214 | return ( 215 |
216 | {posts.map((item) => { 217 | return ( 218 | 223 |
224 | 225 |

226 | {new Date(item.meta.date).toLocaleDateString("cn", { 227 | day: "2-digit", 228 | month: "2-digit", 229 | year: "numeric", 230 | })} 231 |

232 |

{item.meta.spoiler}

233 |
234 | 235 | ); 236 | })} 237 |
238 | ); 239 | } 240 | ``` 241 | 242 | 20240223174210.jpg 247 | 248 | 你会发现博客标题颜色随着文章顺序变化,实现这个功能,单独抽离PostTitle组件:这里的逻辑并不是唯一,你可以根据自己的需求来实现你自己喜欢的颜色。主要逻辑就是`--lightLink` `--darkLink` 这两个css变量,你可以根据不同的逻辑来设置这两个变量的值。其中`--lightLink` `--darkLink`分别是日间/暗黑模式的变量,你可以根据你的需求来设置。 249 | 250 | ```js 251 | import Color from "colorjs.io"; 252 | 253 | function PostTitle({ post }: { post: PostDetail }) { 254 | let lightStart = new Color("lab(63 59.32 -1.47)"); 255 | let lightEnd = new Color("lab(33 42.09 -43.19)"); 256 | let lightRange = lightStart.range(lightEnd); 257 | let darkStart = new Color("lab(81 32.36 -7.02)"); 258 | let darkEnd = new Color("lab(78 19.97 -36.75)"); 259 | let darkRange = darkStart.range(darkEnd); 260 | let today = new Date(); 261 | let timeSinceFirstPost = ( 262 | today.valueOf() - new Date(2018, 10, 30).valueOf() 263 | ).valueOf(); 264 | let timeSinceThisPost = ( 265 | today.valueOf() - new Date(post.meta.date).valueOf() 266 | ).valueOf(); 267 | let staleness = timeSinceThisPost / timeSinceFirstPost; 268 | 269 | return ( 270 |

282 | {post.meta.title} 283 |

284 | ); 285 | } 286 | 287 | ``` 288 | 289 | ## 博客详情 290 | 291 | 首页博客列表实现完了,接下来就是博客详情页面,渲染mdx文件内容,首推nextjs官方的MDXRemote组件,最重要的就是它支持引入自己编写的组件,这样就可以实现博客中的组件交互效果。 292 | 293 | 博客详情是nextjs动态路由最好的应用场景,你可以通过动态路由来渲染不同的博客详情页面。创建动态路由文件(src/app/[slug]/layout.tsx)。博客详情作为新页面自然是必须layout包裹的,所以在这里引入layout组件。 294 | 295 | ```js 296 | import HomeLink from "@/components/homeLink/homeLink"; 297 | 298 | export default function DetailLayout({ 299 | children, 300 | }: { 301 | children: React.ReactNode; 302 | }) { 303 | return ( 304 | <> 305 | {children} 306 |
307 | 308 | 309 |
310 | 311 | ); 312 | } 313 | 314 | ``` 315 | 316 | HomeLink组件没有关键逻辑,不用关心。 317 | 318 | 博客详情页面代码(src/app/[slug]/page.tsx) 319 | 320 | ```js 321 | import { getAllPostsMeta, getPost } from "@/data/post"; 322 | import { MDXRemote } from "next-mdx-remote/rsc"; 323 | // 美化代码,支持代码颜色主题 324 | import rehypePrettyCode from "rehype-pretty-code"; 325 | // 支持数学公式 326 | import remarkMath from "remark-math"; 327 | import rehypeKatex from "rehype-katex"; 328 | import { sans } from "@/fonts/fonts"; 329 | import "./markdown.css"; 330 | import { getPostWords, readingTime } from "@/lib/utils"; 331 | 332 | export async function generateStaticParams() { 333 | const metas = await getAllPostsMeta(); 334 | return metas.map((post) => { 335 | return { slug: post.meta.slug }; 336 | }); 337 | } 338 | 339 | export default async function PostPage({ 340 | params, 341 | }: { 342 | params: { slug: string }; 343 | }) { 344 | // 获取文章详情 345 | const post = await getPost(params.slug); 346 | let postComponents = {}; 347 | 348 | // 提取自己编写的组件 349 | try { 350 | postComponents = await import( 351 | "../../posts/" + params.slug + "/components.js" 352 | ); 353 | } catch (e: any) { 354 | if (!e || e.code !== "MODULE_NOT_FOUND") { 355 | throw e; 356 | } 357 | } 358 | 359 | const words = getPostWords(post.content); 360 | const readTime = readingTime(words); 361 | 362 | return ( 363 |
364 |

370 | {post.meta.title} 371 |

372 |

373 | {new Date(post.meta.date).toLocaleDateString("cn", { 374 | day: "2-digit", 375 | month: "2-digit", 376 | year: "numeric", 377 | })} 378 |

379 | 380 |

381 | 字数:{words} 382 |

383 |

384 | 预计阅读时间:{readTime}分钟 385 |

386 |
387 | 412 |
413 |
414 | ); 415 | } 416 | 417 | ``` 418 | 419 | 在BlogList组件中,我们使用`Link`组件包裹所有内容,并设置`href`属性为`"/" + item.meta.slug + "/"`,这样就可以通过动态路由来渲染不同的博客详情页面。 420 | 421 | ```js 422 | 425 | ``` 426 | 427 | 函数generateStaticParams 函数可以与动态路由段结合使用,在构建时静态生成路由,而不是在请求时按需生成路由。这样可以提高页面加载速度。 428 | 429 | 获取文章详情方法(src/data/posts.ts) 430 | 431 | ```js 432 | export async function getPost(slug: string) { 433 | const posts = await getAllPostsMeta(); 434 | if (!slug) throw new Error("not found"); 435 | const post = posts.find((post) => post.meta.slug === slug); 436 | if (!post) { 437 | throw new Error("not found"); 438 | } 439 | return post; 440 | } 441 | ``` 442 | 443 | 提取自己编写的组件,丢给MDXRomote组件。 444 | 445 | ```js 446 | try { 447 | postComponents = await import( 448 | "../../posts/" + params.slug + "/components.js" 449 | ); 450 | } catch (e: any) { 451 | if (!e || e.code !== "MODULE_NOT_FOUND") { 452 | throw e; 453 | } 454 | } 455 | ``` 456 | 457 | 文章markdown渲染美化css 458 | 459 | ```css 460 | .markdown { 461 | line-height: 28px; 462 | --path: none; 463 | --radius-top: 12px; 464 | --radius-bottom: 12px; 465 | --padding-top: 1rem; 466 | --padding-bottom: 1rem; 467 | } 468 | 469 | .markdown p { 470 | @apply pb-8; 471 | } 472 | 473 | .markdown a { 474 | @apply border-b-[1px] border-[--link] text-[--link]; 475 | } 476 | 477 | .markdown hr { 478 | @apply pt-8 opacity-60 dark:opacity-10; 479 | } 480 | 481 | .markdown h2 { 482 | @apply mt-2 pb-8 text-3xl font-bold; 483 | } 484 | 485 | .markdown h3 { 486 | @apply mt-2 pb-8 text-2xl font-bold; 487 | } 488 | 489 | .markdown h4 { 490 | @apply mt-2 pb-8 text-xl font-bold; 491 | } 492 | 493 | .markdown :not(pre) > code { 494 | border-radius: 10px; 495 | background: var(--inlineCode-bg); 496 | color: var(--inlineCode-text); 497 | padding: 0.15em 0.2em 0.05em; 498 | white-space: normal; 499 | } 500 | 501 | .markdown pre { 502 | @apply -mx-4 mb-8 overflow-y-auto p-4 text-sm; 503 | clip-path: var(--path); 504 | border-top-right-radius: var(--radius-top); 505 | border-top-left-radius: var(--radius-top); 506 | border-bottom-right-radius: var(--radius-bottom); 507 | border-bottom-left-radius: var(--radius-bottom); 508 | padding-top: var(--padding-top); 509 | padding-bottom: var(--padding-bottom); 510 | } 511 | 512 | .markdown pre code { 513 | width: auto; 514 | } 515 | 516 | .markdown blockquote { 517 | @apply relative -left-2 -ml-4 mb-8 pl-4; 518 | font-style: italic; 519 | border-left: 3px solid hsla(0, 0%, 0%, 0.9); 520 | border-left-color: inherit; 521 | opacity: 0.8; 522 | } 523 | 524 | .markdown blockquote p { 525 | margin: 0; 526 | padding: 0; 527 | } 528 | 529 | .markdown p img { 530 | margin-bottom: 0; 531 | } 532 | 533 | .markdown ul { 534 | margin-top: 0; 535 | padding-bottom: 0; 536 | padding-left: 0; 537 | padding-right: 0; 538 | padding-top: 0; 539 | margin-bottom: 1.75rem; 540 | list-style-position: outside; 541 | list-style-image: none; 542 | list-style: disc; 543 | } 544 | 545 | .markdown li { 546 | margin-bottom: calc(1.75rem / 2); 547 | } 548 | 549 | .markdown img { 550 | @apply mb-8; 551 | max-width: 100%; 552 | } 553 | 554 | .markdown pre [data-highlighted-line] { 555 | margin-left: -16px; 556 | padding-left: 12px; 557 | border-left: 4px solid #ffa7c4; 558 | background-color: #022a4b; 559 | display: block; 560 | padding-right: 1em; 561 | } 562 | ``` 563 | 564 | 至此逻辑已经全部完成,这个博客项目还比较简答。 其核心逻辑就是 读取markdown文件-MDXRemote组件渲染mdx文件内容。MDXRemote组件还支持丰富的插件功能,数学公式、图标展示。感兴趣的同学可以扩展。 565 | 566 | 完整代码在: [blog](https://github.com/sunshineLixun/dan-blog) 567 | -------------------------------------------------------------------------------- /src/posts/the-two-react/components.js: -------------------------------------------------------------------------------- 1 | export { Counter } from "./counter"; 2 | export { PostList } from "./post-list"; 3 | export { PostPreview } from "./post-preview"; 4 | -------------------------------------------------------------------------------- /src/posts/the-two-react/counter.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | 5 | export function Counter() { 6 | const [count, setCount] = useState(0); 7 | return ( 8 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/posts/the-two-react/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "The Two Reacts" 3 | date: "2024-01-04" 4 | spoiler: "UI = f(data)(state)" 5 | --- 6 | 7 | Suppose I want to display something on your screen. Whether I want to display a web page like this blog post, an interactive web app, or even a native app that you might download from some app store, at least _two_ devices must be involved. 8 | 9 | Your device and mine. 10 | 11 | It starts with some code and data on _my_ device. For example, I am editing this blog post as a file on my laptop. If you see it on your screen, it must have already traveled from my device to yours. At some point, somewhere, my code and data turned into the HTML and JavaScript instructing _your_ device to display this. 12 | 13 | So how does that relate to React? React is a UI programming paradigm that lets me break down _what_ to display (a blog post, a signup form, or even a whole app) into independent pieces called _components_, and compose them like LEGO blocks. I'll assume you already know and like components; check [react.dev](https://react.dev) for an intro. 14 | 15 | Components are code, and that code has to run somewhere. But wait--_whose_ computer should they run on? Should they run on your computer? Or on mine? 16 | 17 | Let's make a case for each side. 18 | 19 | --- 20 | 21 | First, I'll argue that components should run on _your_ computer. 22 | 23 | Here's a little counter button to demonstrate interactivity. Click it a few times! 24 | 25 | ```js 26 | 27 | ``` 28 | 29 |

30 | 31 |

32 | 33 | Assuming the JavaScript code for this component has already loaded, the number will increase. Notice that it increases _instantly on press_. There is no delay. No need to wait for the server. No need to download any additional data. 34 | 35 | This is possible because this component's code is running on _your_ computer: 36 | 37 | ```js 38 | import { useState } from "react"; 39 | 40 | export function Counter() { 41 | const [count, setCount] = useState(0); 42 | return ( 43 | 49 | ); 50 | } 51 | ``` 52 | 53 | Here, `count` is a piece of _client state_--a bit of information in your computer's memory that updates every time you press that button. **I don't know how many times you're going to press the button** so I can't predict and prepare all of its possible outputs on _my_ computer. The most I'll dare to prepare on my computer is the _initial_ rendering output ("You clicked me 0 times") and send it as HTML. But from that point and on, _your computer had to take over_ running this code. 54 | 55 | You could argue that it's _still_ not necessary to run this code on your computer. Maybe I could have it running on my server instead? Whenever you press the button, your computer could ask my server for the next rendering output. Isn't that how websites worked before all of those client-side JavaScript frameworks? 56 | 57 | Asking the server for a fresh UI works well when the user _expects_ a little delay--for example, when clicking a link. When the user knows they're navigating to _some different place_ in your app, they'll wait. However, any direct manipulation (such as dragging a slider, switching a tab, typing into a post composer, clicking a like button, swiping a card, hovering a menu, dragging a chart, and so on) would feel broken if it didn't reliably provide at least _some_ instant feedback. 58 | 59 | This principle isn't strictly technical--it's an intuition from the everyday life. For example, you wouldn't expect an elevator button to take you to the next floor in an instant. But when you're pushing a door handle, you _do_ expect it to follow your hand's movement directly, or it will feel stuck. In fact, even with an elevator button you'd expect at least _some_ instant feedback: it should yield to the pressure of your hand. Then it should light up to acknowledge your press. 60 | 61 | **When you build a user interface, you need to be able to respond to at least some interactions with _guaranteed_ low latency and with _zero_ network roundtrips.** 62 | 63 | You might have seen the React mental model being described as a sort of an equation: _UI is a function of state_, or `UI = f(state)`. This doesn't mean that your UI code has to literally be a single function taking state as an argument; it only means that the current state determines the UI. When the state changes, the UI needs to be recomputed. Since the state "lives" on your computer, the code to compute the UI (your components) must also run on your computer. 64 | 65 | Or so this argument goes. 66 | 67 | --- 68 | 69 | Next, I'll argue the opposite--that components should run on _my_ computer. 70 | 71 | Here's a preview card for a different post from this blog: 72 | 73 | ```js 74 | 75 | ``` 76 | 77 |
78 | 79 |
80 | 81 | How does a component from _this_ page know the number of words on _that_ page? 82 | 83 | If you check the Network tab, you'll see no extra requests. I'm not downloading that entire blog post from GitHub just to count the number of words in it. I'm not embedding the contents of that blog post on this page either. I'm not calling any APIs to count the words. And I sure did not count all those words by myself. 84 | 85 | So how does this component work? 86 | 87 | ```js 88 | import { readFile } from "fs/promises"; 89 | import matter from "gray-matter"; 90 | 91 | export async function PostPreview({ slug }) { 92 | const fileContent = await readFile("./public/" + slug + "/index.md", "utf8"); 93 | const { data, content } = matter(fileContent); 94 | const wordCount = content.split(" ").filter(Boolean).length; 95 | 96 | return ( 97 |
98 |
99 | 100 | {data.title} 101 | 102 |
103 | {wordCount} words 104 |
105 | ); 106 | } 107 | ``` 108 | 109 | This component runs on _my_ computer. When I want to read a file, I read a file with `fs.readFile`. When I want to parse its Markdown header, I parse it with `gray-matter`. When I want to count the words, I split its text and count them. **There is nothing extra I need to do because my code runs _right where the data is_.** 110 | 111 | Suppose I wanted to list _all_ the posts on my blog along with their word counts. 112 | 113 | Easy: 114 | 115 | ```js 116 | 117 | ``` 118 | 119 | 120 | 121 | All I needed to do was to render a `` for every post folder: 122 | 123 | ```js 124 | import { readdir } from "fs/promises"; 125 | import { PostPreview } from "./post-preview"; 126 | 127 | export async function PostList() { 128 | const entries = await readdir("./public/", { withFileTypes: true }); 129 | const dirs = entries.filter((entry) => entry.isDirectory()); 130 | return ( 131 |
132 | {dirs.map((dir) => ( 133 | 134 | ))} 135 |
136 | ); 137 | } 138 | ``` 139 | 140 | None of this code needed to run on your computer--and indeed _it couldn't_ because your computer doesn't have my files. Let's check _when_ this code ran: 141 | 142 | ```js 143 |

{new Date().toString()}

144 | ``` 145 | 146 |

{new Date().toString()}

147 | 148 | Aha--that's exactly when I last deployed my blog to my static web hosting! My components ran during the build process so they had full access to my posts. 149 | 150 | **Running my components close to their data source lets them read their own data and preprocess it _before_ sending any of that information to your device.** 151 | 152 | By the time you loaded this page, there was no more `` and no more ``, no `fileContent` and no `dirs`, no `fs` and no `gray-matter`. Instead, there was only a `
` with a few `
`s with ``s and ``s inside each of them. Your device only received _the UI it actually needs to display_ (the rendered post titles, link URLs, and post word counts) rather than _the full raw data_ that your components used to compute that UI from (the actual posts). 153 | 154 | With this mental model, _the UI is a function of server data_, or `UI = f(data)`. That data only exists _my_ device, so that's where the components should run. 155 | 156 | Or so the argument goes. 157 | 158 | --- 159 | 160 | UI is made of components, but we argued for two very different visions: 161 | 162 | - `UI = f(state)` where `state` is client-side, and `f` runs on the client. This approach allows writing instantly interactive components like ``. (Here, `f` may _also_ run on the server with the initial state to generate HTML.) 163 | - `UI = f(data)` where `data` is server-side, and `f` runs on the server only. This approach allows writing data-processing components like ``. (Here, `f` runs categorically on the server only. Build-time counts as "server".) 164 | 165 | If we set aside the familiarity bias, both of these approaches are compelling at what they do best. Unfortunately, these visions _seem_ mutually incompatible. 166 | 167 | If we want to allow instant interactivity like needed by ``, we _have to_ run components on the client. But components like `` can't run on the client _in principle_ because they use server-only APIs like `readFile`. (That's their whole point! Otherwise we might as well run them on the client.) 168 | 169 | Okay, what if we run all components on the server instead? But on the server, components like `` can only render their _initial_ state. The server doesn't know their _current_ state, and passing that state between the server and the client is too slow (unless it's tiny like a URL) and not even always possible (e.g. my blog's server code only runs on deploy so you can't "pass" stuff to it). 170 | 171 | Again, it seems like we have to choose between two different Reacts: 172 | 173 | - The "client" `UI = f(state)` paradigm that lets us write ``. 174 | - The "server" `UI = f(data)` paradigm that lets us write ``. 175 | 176 | But in practice, the real "formula" is closer to `UI = f(data, state)`. If you had no `data` or no `state`, it would generalize to those cases. But ideally, I'd prefer my programming paradigm to be able to _handle both cases_ without having to pick another abstraction, and I know at least a few of you would like that too. 177 | 178 | The problem to solve, then, is how to split our “`f`” across two very different programming environments. Is that even possible? Recall we're not talking about some actual function called `f`--here, `f` represents all our components. 179 | 180 | Is there some way we could split components between your computer and mine in a way that preserves what's great about React? Could we combine and nest components from two different environments? How would that work? 181 | 182 | How _should_ that work? 183 | 184 | Give it some thought, and next time we'll compare our notes. 185 | -------------------------------------------------------------------------------- /src/posts/the-two-react/post-list.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { PostPreview } from "./post-preview"; 3 | import { readdir } from "fs/promises"; 4 | 5 | const rootDirectory = path.join(process.cwd(), "src", "posts"); 6 | 7 | export async function PostList() { 8 | const entries = await readdir(rootDirectory, { withFileTypes: true }); 9 | const dirs = entries.filter((entry) => entry.isDirectory()); 10 | return ( 11 |
12 | {dirs.map((dir) => ( 13 | 14 | ))} 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/posts/the-two-react/post-preview.js: -------------------------------------------------------------------------------- 1 | import { readFile } from "fs/promises"; 2 | import matter from "gray-matter"; 3 | import path from "path"; 4 | 5 | const rootDirectory = path.join(process.cwd(), "src", "posts"); 6 | 7 | export async function PostPreview({ slug }) { 8 | const fileContent = await readFile( 9 | rootDirectory + "/" + slug + "/index.mdx", 10 | "utf8", 11 | ); 12 | const { data, content } = matter(fileContent); 13 | const wordCount = content.split(" ").filter(Boolean).length; 14 | 15 | return ( 16 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | import defaultTheme from "tailwindcss/defaultTheme"; 3 | 4 | const config: Config = { 5 | content: ["./src/**/**/*.{js,ts,jsx,tsx,mdx}"], 6 | theme: { 7 | extend: { 8 | backgroundImage: { 9 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 10 | "gradient-conic": 11 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 12 | }, 13 | }, 14 | fontFamily: { 15 | sans: ["Merriweather", ...defaultTheme.fontFamily.sans], 16 | }, 17 | }, 18 | plugins: [], 19 | }; 20 | export default config; 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | --------------------------------------------------------------------------------
17 |
18 | 19 | {data.title} 20 | 21 |
22 | {wordCount} words 23 |