{"id":222,"date":"2026-02-01T19:25:24","date_gmt":"2026-02-01T11:25:24","guid":{"rendered":"http:\/\/localhost:8080\/%e6%88%91%e5%a6%82%e4%bd%95%e4%bb%8e%e9%9b%b6%e6%9e%84%e5%bb%ba%e4%b8%80%e4%b8%aa%e7%94%9f%e4%ba%a7%e7%ba%a7-react-ssr-%e6%a1%86%e6%9e%b6\/"},"modified":"2026-02-01T19:25:24","modified_gmt":"2026-02-01T11:25:24","slug":"%e6%88%91%e5%a6%82%e4%bd%95%e4%bb%8e%e9%9b%b6%e6%9e%84%e5%bb%ba%e4%b8%80%e4%b8%aa%e7%94%9f%e4%ba%a7%e7%ba%a7-react-ssr-%e6%a1%86%e6%9e%b6","status":"publish","type":"post","link":"https:\/\/erishen.cn\/?p=222","title":{"rendered":"\u6211\u5982\u4f55\u4ece\u96f6\u6784\u5efa\u4e00\u4e2a\u751f\u4ea7\u7ea7 React SSR \u6846\u67b6"},"content":{"rendered":"<h2>\u524d\u8a00<\/h2>\n<p>\u5728 React \u751f\u6001\u7cfb\u7edf\u4e2d\uff0cNext.js \u4f5c\u4e3a SSR \u6846\u67b6\u7684\u4ee3\u8868\uff0c\u4ee5\u5176\u5f00\u7bb1\u5373\u7528\u7684\u4f53\u9a8c\u548c\u5f3a\u5927\u7684\u529f\u80fd\u6df1\u53d7\u5f00\u53d1\u8005\u559c\u7231\u3002\u4f46\u5bf9\u4e8e\u60f3\u8981<strong>\u5b8c\u5168\u638c\u63a7\u6784\u5efa\u6d41\u7a0b<\/strong>\u3001<strong>\u6df1\u5ea6\u5b9a\u5236\u90e8\u7f72\u65b9\u6848<\/strong>\u3001\u6216\u8005\u53ea\u662f\u60f3<strong>\u5b66\u4e60 SSR \u5e95\u5c42\u539f\u7406<\/strong>\u7684\u5f00\u53d1\u8005\u6765\u8bf4\uff0cNext.js \u7684&quot;\u9ed1\u76d2&quot;\u7279\u6027\u53cd\u800c\u6210\u4e3a\u4e86\u4e00\u79cd\u9650\u5236\u3002<\/p>\n<p>\u6700\u8fd1\uff0c\u6211\u82b1\u4e86\u51e0\u4e2a\u6708\u65f6\u95f4\uff0c\u4ece\u96f6\u5f00\u59cb\u6784\u5efa\u4e86\u4e00\u4e2a\u8f7b\u91cf\u7ea7\u4f46\u529f\u80fd\u5b8c\u6574\u7684 React SSR \u6846\u67b6\u2014\u2014<strong>NSBP (Node React SSR by Webpack)<\/strong>\u3002\u4eca\u5929\uff0c\u6211\u60f3\u5206\u4eab\u4e00\u4e0b\u8fd9\u4e2a\u9879\u76ee\u7684\u5f00\u53d1\u5386\u7a0b\u3001\u6280\u672f\u9009\u578b\u3001\u6838\u5fc3\u7279\u6027\u4ee5\u53ca\u4ece\u4e2d\u83b7\u5f97\u7684\u5b9e\u6218\u7ecf\u9a8c\u3002<\/p>\n<blockquote>\n<p><strong>\u5728\u7ebf\u6f14\u793a<\/strong>: <a href=\"https:\/\/nsbp.erishen.cn\/\">https:\/\/nsbp.erishen.cn\/<\/a><br \/>\n<strong>GitHub<\/strong>: <a href=\"https:\/\/github.com\/erishen\/nsbp\">https:\/\/github.com\/erishen\/nsbp<\/a><\/p>\n<\/blockquote><\/div>\n<hr>\n<h2>\u4e00\u3001\u4e3a\u4ec0\u4e48\u9009\u62e9\u81ea\u5efa\u800c\u4e0d\u662f Next.js\uff1f<\/h2>\n<h3>1.1 \u5b8c\u5168\u638c\u63a7\u6784\u5efa\u6d41\u7a0b<\/h3>\n<p>Next.js \u7684\u5f3a\u5927\u6765\u81ea\u4e8e\u5b83\u7684\u81ea\u52a8\u5316\uff0c\u4f46\u8fd9\u4e5f\u610f\u5473\u7740\u4f60\u5931\u53bb\u4e86\u4e00\u4e9b\u63a7\u5236\u6743\uff1a<\/p>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code class=\"language-bash\"># Next.js \u7684\u6784\u5efa\u8fc7\u7a0b\uff08\u9ed1\u76d2\uff09\nnext build\n# \u2192 \u81ea\u52a8\u8def\u7531\n# \u2192 \u81ea\u52a8\u4ee3\u7801\u5206\u5272\n# \u2192 \u81ea\u52a8\u56fe\u7247\u4f18\u5316\n# \u2192 ... \u4f60\u770b\u4e0d\u5230\u7684\u51e0\u767e\u4e2a\u6b65\u9aa4\n<\/code><\/pre><\/div>\n<p>\u800c\u5728 NSBP \u4e2d\uff0c\u4f60\u53ef\u4ee5\u7cbe\u786e\u63a7\u5236\u6bcf\u4e00\u4e2a\u6784\u5efa\u6b65\u9aa4\uff1a<\/p>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code class=\"language-javascript\">\/\/ config\/webpack.base.js\nmodule.exports = {\n  entry: {\n    client: &#39;.\/src\/client\/index.tsx&#39;,\n    server: &#39;.\/src\/server\/index.ts&#39;\n  },\n  plugins: [\n    new MiniCssExtractPlugin(),\n    new TerserPlugin(),\n    \/\/ ... \u4f60\u53ef\u4ee5\u6dfb\u52a0\u4efb\u4f55 Webpack \u63d2\u4ef6\n  ]\n}\n<\/code><\/pre><\/div>\n<h3>1.2 \u6df1\u5ea6\u5b9a\u5236\u90e8\u7f72\u65b9\u6848<\/h3>\n<p>Next.js \u81ea\u5e26\u7684 Vercel \u90e8\u7f72\u786e\u5b9e\u65b9\u4fbf\uff0c\u4f46\u5982\u679c\u4f60\u60f3\uff1a<\/p>\n<ul>\n<li>\u5728\u81ea\u5df1\u7684 Linux \u670d\u52a1\u5668\u4e0a\u90e8\u7f72<\/li>\n<li>\u4f7f\u7528 Docker Compose \u7f16\u6392\u591a\u4e2a\u670d\u52a1<\/li>\n<li>\u914d\u7f6e\u53cd\u5411\u4ee3\u7406\u548c\u8d1f\u8f7d\u5747\u8861<\/li>\n<li>\u5b9e\u73b0\u7279\u5b9a\u7684\u5b89\u5168\u7b56\u7565<\/li>\n<\/ul>\n<p>\u90a3\u4e48\u81ea\u5df1\u638c\u63a7 Express \u670d\u52a1\u5668\u4f1a\u66f4\u6709\u4f18\u52bf\u3002<\/p>\n<h3>1.3 \u5b66\u4e60 SSR \u7684\u5e95\u5c42\u539f\u7406<\/h3>\n<p>\u901a\u8fc7\u624b\u5199 SSR \u6e32\u67d3\u903b\u8f91\uff0c\u4f60\u5c06\u6df1\u5165\u7406\u89e3\uff1a<\/p>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code class=\"language-typescript\">\/\/ SSR \u6e32\u67d3\u6d41\u7a0b\nconst sheet = new ServerStyleSheet()\nconst jsx = sheet.collectStyles(\n  &lt;Provider store={store}&gt;\n    &lt;StaticRouter location={reqPath}&gt;\n      &lt;Routes&gt;{\/* ... *\/}&lt;\/Routes&gt;\n    &lt;\/StaticRouter&gt;\n  &lt;\/Provider&gt;\n)\nconst content = renderToString(jsx)\nconst styleTags = sheet.getStyleTags()\n<\/code><\/pre><\/div>\n<p>\u8fd9\u4e9b\u77e5\u8bc6\u5bf9\u4f60\u5728\u4efb\u4f55 React SSR \u9879\u76ee\u4e2d\u90fd\u975e\u5e38\u6709\u4ef7\u503c\u3002<\/p>\n<hr>\n<h2>\u4e8c\u3001\u6280\u672f\u9009\u578b\u4e0e\u67b6\u6784\u8bbe\u8ba1<\/h2>\n<h3>2.1 \u6838\u5fc3\u6280\u672f\u6808<\/h3>\n<table>\n<thead>\n<tr>\n<th>\u7c7b\u522b<\/th>\n<th>\u6280\u672f\u9009\u578b<\/th>\n<th>\u7248\u672c<\/th>\n<th>\u7406\u7531<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>React<\/strong><\/td>\n<td>React 19.2.3<\/td>\n<td>\u6700\u65b0\u7248\u672c\uff0c\u5e76\u53d1\u7279\u6027\uff0c\u66f4\u597d\u7684\u7c7b\u578b\u652f\u6301<\/td>\n<td><\/td>\n<\/tr>\n<tr>\n<td><strong>\u72b6\u6001\u7ba1\u7406<\/strong><\/td>\n<td>Redux Toolkit<\/td>\n<td>\u7b80\u5316 Redux \u5f00\u53d1\uff0c\u5185\u7f6e Thunk \u652f\u6301<\/td>\n<td><\/td>\n<\/tr>\n<tr>\n<td><strong>\u8def\u7531<\/strong><\/td>\n<td>React Router DOM 7.12.0<\/td>\n<td>\u7a33\u5b9a\u7684\u5ba2\u6237\u7aef\u8def\u7531<\/td>\n<td><\/td>\n<\/tr>\n<tr>\n<td><strong>\u4ee3\u7801\u5206\u5272<\/strong><\/td>\n<td>@loadable\/component<\/td>\n<td>5.15.0\uff0c\u6210\u719f\u7684\u4ee3\u7801\u5206\u5272\u65b9\u6848<\/td>\n<td><\/td>\n<\/tr>\n<tr>\n<td><strong>\u6784\u5efa\u5de5\u5177<\/strong><\/td>\n<td>Webpack 5.96.0<\/td>\n<td>\u6a21\u5757\u5316\u3001\u53ef\u6269\u5c55\u3001\u751f\u6001\u4e30\u5bcc<\/td>\n<td><\/td>\n<\/tr>\n<tr>\n<td><strong>\u670d\u52a1\u7aef<\/strong><\/td>\n<td>Express 5.2.1<\/td>\n<td>\u8f7b\u91cf\u7ea7\u3001\u4e2d\u95f4\u4ef6\u751f\u6001\u5b8c\u5584<\/td>\n<td><\/td>\n<\/tr>\n<tr>\n<td><strong>\u6837\u5f0f\u65b9\u6848<\/strong><\/td>\n<td>Styled-components + Sass\/Less<\/td>\n<td>\u591a\u79cd\u9009\u62e9\uff0c\u7075\u6d3b\u5207\u6362<\/td>\n<td><\/td>\n<\/tr>\n<tr>\n<td><strong>TypeScript<\/strong><\/td>\n<td>5.x<\/td>\n<td>\u7c7b\u578b\u5b89\u5168\uff0c\u5f00\u53d1\u4f53\u9a8c\u4f18\u79c0<\/td>\n<td><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h3>2.2 SSR \u6e32\u67d3\u67b6\u6784<\/h3>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code>\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502         HTTP \u8bf7\u6c42                         \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n              \u2193\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502       Express \u4e2d\u95f4\u4ef6\u5c42                \u2502\n\u2502  \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510       \u2502\n\u2502  \u2502 Helmet (\u5b89\u5168) \u2502 Rate Limit  \u2502       \u2502\n\u2502  \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518       \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n              \u2193\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502       SSR \u6e32\u67d3\u5c42                     \u2502\n\u2502  1. \u521b\u5efa Redux Store                 \u2502\n\u2502  2. \u9884\u53d6\u8def\u7531\u6570\u636e (loadData)         \u2502\n\u2502  3. renderToString \u6e32\u67d3 HTML        \u2502\n\u2502  4. \u63d0\u53d6 Styled-components \u6837\u5f0f    \u2502\n\u2502  5. \u5e8f\u5217\u5316\u72b6\u6001\u5230 window.context      \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n              \u2193\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502       \u8fd4\u56de HTML + \u521d\u59cb\u72b6\u6001            \u2502\n\u2502  &lt;html&gt;                            \u2502\n\u2502    &lt;head&gt;                          \u2502\n\u2502      ${styleTags}                    \u2502\n\u2502    &lt;\/head&gt;                          \u2502\n\u2502    &lt;body&gt;                          \u2502\n\u2502      &lt;div id=&quot;root&quot;&gt;${content}&lt;\/div&gt;   \u2502\n\u2502      &lt;script&gt;                        \u2502\n\u2502        window.context = { state: ... } \u2502\n\u2502      &lt;\/script&gt;                        \u2502\n\u2502    &lt;\/body&gt;                          \u2502\n\u2502  &lt;\/html&gt;                            \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n<\/code><\/pre><\/div>\n<h3>2.3 \u5ba2\u6237\u7aef Hydration \u6d41\u7a0b<\/h3>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code class=\"language-typescript\">\/\/ src\/client\/index.tsx\nconst App = () =&gt; {\n  const store = useMemo(() =&gt; {\n    \/\/ \u8bfb\u53d6\u670d\u52a1\u7aef\u4f20\u9012\u7684\u72b6\u6001\n    if (isSEO() &amp;&amp; window?.context?.state) {\n      return getStore(window.context.state)\n    }\n    return getStore()\n  }, [])\n\n  return (\n    &lt;Provider store={store}&gt;\n      &lt;BrowserRouter&gt;\n        &lt;Routes&gt;{\/* ... *\/}&lt;\/Routes&gt;\n      &lt;\/BrowserRouter&gt;\n    &lt;\/Provider&gt;\n  )\n}\n\n\/\/ \u7b49\u5f85\u4ee3\u7801\u5206\u5272\u7ec4\u4ef6\u52a0\u8f7d\u5b8c\u6210\nloadableReady(() =&gt; {\n  hydrateRoot(document.getElementById(&#39;root&#39;)!, &lt;App \/&gt;)\n})\n<\/code><\/pre><\/div>\n<hr>\n<h2>\u4e09\u3001\u6838\u5fc3\u7279\u6027\u8be6\u89e3<\/h2>\n<h3>3.1 \u4e09\u79cd\u6e32\u67d3\u6a21\u5f0f\uff08\u7075\u6d3b\u5207\u6362\uff09<\/h3>\n<p>NSBP \u652f\u6301\u901a\u8fc7 URL \u53c2\u6570\u63a7\u5236\u6e32\u67d3\u6a21\u5f0f\uff1a<\/p>\n<table>\n<thead>\n<tr>\n<th>\u6a21\u5f0f<\/th>\n<th>URL<\/th>\n<th>\u573a\u666f<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>SSR\uff08\u9ed8\u8ba4\uff09<\/strong><\/td>\n<td><code>\/<\/code> \u6216 <code>\/?nsbp=1<\/code><\/td>\n<td>SEO \u4f18\u5316\u3001\u9996\u5c4f\u52a0\u8f7d\u5feb<\/td>\n<\/tr>\n<tr>\n<td><strong>CSR\uff08\u7eaf\u5ba2\u6237\u7aef\uff09<\/strong><\/td>\n<td><code>\/?nsbp=0<\/code><\/td>\n<td>\u8c03\u8bd5\u3001\u5f00\u53d1\u4f53\u9a8c<\/td>\n<\/tr>\n<tr>\n<td><strong>SSR \u56de\u9000<\/strong><\/td>\n<td><code>\/?nsbp=1&amp;from=link<\/code><\/td>\n<td>\u5185\u90e8\u94fe\u63a5\uff0c\u5141\u8bb8\u5ba2\u6237\u7aef\u66f4\u65b0<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><strong>\u5b9e\u73b0\u903b\u8f91<\/strong>\uff1a<\/p>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code class=\"language-typescript\">\/\/ src\/utils\/index.ts\nexport const isSEO = () =&gt; {\n  if (typeof window !== &#39;undefined&#39;) {\n    const nsbp = getLocationParams(&#39;nsbp&#39;)\n    if (nsbp !== &#39;&#39;) {\n      return parseInt(nsbp, 10)\n    }\n    return 1  \/\/ \u9ed8\u8ba4 SSR\n  }\n  return 1\n}\n<\/code><\/pre><\/div>\n<h3>3.2 \u667a\u80fd SSR \u6570\u636e\u9884\u53d6<\/h3>\n<p>\u670d\u52a1\u7aef\u9884\u53d6\u6570\u636e\uff0c\u907f\u514d\u5ba2\u6237\u7aef\u4e8c\u6b21\u8bf7\u6c42\uff1a<\/p>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code class=\"language-typescript\">\/\/ src\/Routers.tsx\nexport default [\n  {\n    path: &#39;\/&#39;,\n    element: &lt;Home \/&gt;,\n    loadData: homeLoadData,  \/\/ \u2190 SSR \u65f6\u9884\u53d6\u6570\u636e\n    key: &#39;home&#39;\n  }\n]\n\n\/\/ src\/services\/home.ts\nexport const loadData = (resolve: any, query: any) =&gt; {\n  return async (dispatch: any) =&gt; {\n    try {\n      const response = await fetch(&#39;\/getPhotoMenu&#39;)\n      const data = await response.json()\n\n      \/\/ Redux Thunk action\n      dispatch({\n        type: FETCH_PHOTO_MENU_SUCCESS,\n        payload: data.data\n      })\n\n      resolve()\n    } catch (error) {\n      console.error(&#39;Data loading error:&#39;, error)\n      resolve()\n    }\n  }\n}\n<\/code><\/pre><\/div>\n<p><strong>\u6548\u679c\u5bf9\u6bd4<\/strong>\uff1a<\/p>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code>\u4f20\u7edf CSR \u65b9\u5f0f\uff1a\n  1. \u5ba2\u6237\u7aef\u8bf7\u6c42\u9875\u9762 \u2192 \u8fd4\u56de\u7a7a HTML\n  2. \u52a0\u8f7d JS bundle\n  3. \u6267\u884c JS\n  4. \u53d1\u8d77 API \u8bf7\u6c42\n  5. \u6e32\u67d3\u5185\u5bb9\n  \u23f1\ufe0f \u603b\u8017\u65f6: ~2-3 \u79d2\n\nNSBP SSR \u65b9\u5f0f\uff1a\n  1. \u670d\u52a1\u7aef\u9884\u53d6\u6570\u636e\n  2. \u6e32\u67d3\u5b8c\u6574 HTML\n  3. \u8fd4\u56de\u7ed9\u5ba2\u6237\u7aef\n  4. \u5ba2\u6237\u7aef\u76f4\u63a5 hydrate\n  \u23f1\ufe0f \u603b\u8017\u65f6: ~0.5-1 \u79d2\n<\/code><\/pre><\/div>\n<h3>3.3 \u4ee3\u7801\u5206\u5272\u4e0e\u61d2\u52a0\u8f7d<\/h3>\n<p>\u4f7f\u7528 <code>@loadable\/component<\/code> \u5b9e\u73b0\u8def\u7531\u7ea7\u522b\u7684\u4ee3\u7801\u5206\u5272\uff1a<\/p>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code class=\"language-typescript\">import loadable from &#39;@loadable\/component&#39;\n\nconst Home = loadable(() =&gt; import(&#39;@containers\/Home&#39;), {\n  fallback: &lt;Loading \/&gt;\n})\n\nconst Photo = loadable(() =&gt; import(&#39;@containers\/Photo&#39;), {\n  fallback: &lt;Loading \/&gt;\n})\n<\/code><\/pre><\/div>\n<p><strong>Webpack \u914d\u7f6e<\/strong>\uff1a<\/p>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code class=\"language-javascript\">\/\/ config\/webpack.client.js\nconst { ChunkExtractor } = require(&#39;@loadable\/webpack-plugin&#39;)\n\nmodule.exports = merge(baseConfig, {\n  mode: &#39;production&#39;,\n  plugins: [\n    new ChunkExtractor({\n      statsFile: path.resolve(__dirname, &#39;..\/public\/loadable-stats.json&#39;)\n    })\n  ]\n})\n<\/code><\/pre><\/div>\n<p><strong>\u6548\u679c<\/strong>\uff1a<\/p>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code>\u9996\u6b21\u52a0\u8f7d\uff1a\n  - \u4e3b bundle: ~200KB\n  - \u53ea\u52a0\u8f7d\u9996\u9875\u4ee3\u7801\n\n\u8bbf\u95ee \/photo \u9875\u9762\uff1a\n  - \u6309\u9700\u52a0\u8f7d Photo \u7ec4\u4ef6\n  - \u4e0d\u52a0\u8f7d Login \u7ec4\u4ef6\n<\/code><\/pre><\/div>\n<h3>3.4 \u591a\u5c42\u5b89\u5168\u9632\u62a4\u4f53\u7cfb<\/h3>\n<h4>Helmet \u5b89\u5168\u5934\u90e8<\/h4>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code class=\"language-typescript\">\/\/ src\/server\/index.ts\napp.use(helmet({\n  contentSecurityPolicy: {\n    directives: {\n      defaultSrc: [&quot;&#39;self&#39;&quot;],\n      scriptSrc: [&quot;&#39;self&#39;&quot;, &quot;&#39;unsafe-inline&#39;&quot;, &quot;&#39;unsafe-eval&#39;&quot;],\n      styleSrc: [&quot;&#39;self&#39;&quot;, &quot;&#39;unsafe-inline&#39;&quot;],\n      imgSrc: [&quot;&#39;self&#39;&quot;, &#39;data:&#39;, &#39;https:&#39;],\n      connectSrc: [&quot;&#39;self&#39;&quot;, &#39;https:&#39;],\n      fontSrc: [&quot;&#39;self&#39;&quot;, &#39;data:&#39;],\n      objectSrc: [&quot;&#39;none&#39;&quot;],\n      mediaSrc: [&quot;&#39;self&#39;&quot;],\n      frameSrc: [&quot;&#39;none&#39;&quot;]\n    }\n  },\n  crossOriginEmbedderPolicy: false,\n  crossOriginOpenerPolicy: false\n}))\n<\/code><\/pre><\/div>\n<p><strong>\u5b89\u5168\u6548\u679c<\/strong>\uff1a<\/p>\n<table>\n<thead>\n<tr>\n<th>\u653b\u51fb\u7c7b\u578b<\/th>\n<th>\u9632\u62a4\u63aa\u65bd<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>XSS \u653b\u51fb<\/td>\n<td>CSP script-src \u7b56\u7565<\/td>\n<\/tr>\n<tr>\n<td>\u70b9\u51fb\u52ab\u6301<\/td>\n<td>X-Frame-Options: SAMEORIGIN<\/td>\n<\/tr>\n<tr>\n<td>MIME \u55c5\u63a2<\/td>\n<td>X-Content-Type-Options: nosniff<\/td>\n<\/tr>\n<tr>\n<td>\u4e0d\u5b89\u5168\u7684 HTTPS<\/td>\n<td>Strict-Transport-Security<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h4>\u901f\u7387\u9650\u5236<\/h4>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code class=\"language-typescript\">if (process.env.ENABLE_RATE_LIMIT === &#39;1&#39;) {\n  const limiter = rateLimit({\n    windowMs: 15 * 60 * 1000,  \/\/ 15 \u5206\u949f\n    max: 100,                     \/\/ \u6700\u591a 100 \u6b21\u8bf7\u6c42\n    message: &#39;Too many requests from this IP&#39;\n  })\n  app.use(&#39;\/api&#39;, limiter)\n}\n<\/code><\/pre><\/div>\n<h3>3.5 Docker \u4e00\u952e\u90e8\u7f72<\/h3>\n<p><strong>\u751f\u4ea7\u73af\u5883<\/strong> (<code>docker-compose.yml<\/code>)\uff1a<\/p>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code class=\"language-yaml\">services:\n  app:\n    build:\n      context: .\n      dockerfile: docker\/Dockerfile\n    container_name: nsbp-app\n    ports:\n      - &quot;8081:3001&quot;\n    environment:\n      - NODE_ENV=production\n      - ENABLE_RATE_LIMIT=1\n    restart: unless-stopped\n    healthcheck:\n      test: [&quot;CMD&quot;, &quot;node&quot;, &quot;-e&quot;, &quot;require(&#39;http&#39;).get(&#39;http:\/\/localhost:3001&#39;, (r) =&gt; {process.exit(r.statusCode === 200 ? 0 : 1)})&quot;]\n      interval: 30s\n      timeout: 3s\n      retries: 3\n<\/code><\/pre><\/div>\n<p><strong>\u5f00\u53d1\u73af\u5883<\/strong> (<code>docker-compose.dev.yml<\/code>)\uff1a<\/p>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code class=\"language-yaml\">services:\n  app:\n    build:\n      context: .\n      dockerfile: docker\/Dockerfile.dev\n    container_name: nsbp-app-dev\n    ports:\n      - &quot;3001:3001&quot;\n      - &quot;9229:9229&quot;  # Node.js debug port\n    volumes:\n      # \u4ee3\u7801\u6302\u8f7d\uff08\u70ed\u91cd\u8f7d\uff09\n      - .\/src:\/app\/src\n      - .\/public:\/app\/public\n      - .\/config:\/app\/config\n    command: [&quot;dumb-init&quot;, &quot;--&quot;, &quot;pnpm&quot;, &quot;run&quot;, &quot;dev&quot;]\n<\/code><\/pre><\/div>\n<p><strong>\u4e00\u952e\u542f\u52a8<\/strong>\uff1a<\/p>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code class=\"language-bash\"># \u751f\u4ea7\u73af\u5883\nmake prod\n# \u6216\ndocker-compose up -d\n\n# \u5f00\u53d1\u73af\u5883\nmake dev\n# \u6216\ndocker-compose -f docker-compose.dev.yml up --build\n<\/code><\/pre><\/div>\n<hr>\n<h2>\u56db\u3001\u5f00\u53d1\u4f53\u9a8c\u4f18\u5316<\/h2>\n<h3>4.1 \u5b8c\u6574\u7684\u4ee3\u7801\u8d28\u91cf\u5de5\u5177\u94fe<\/h3>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code class=\"language-json\">\/\/ package.json scripts\n{\n  &quot;scripts&quot;: {\n    &quot;lint&quot;: &quot;eslint src --ext .ts,.tsx,.js,.jsx&quot;,\n    &quot;lint:fix&quot;: &quot;eslint src --ext .ts,.tsx,.js,.jsx --fix&quot;,\n    &quot;format&quot;: &quot;prettier --write **\/*.{js,css,less,scss,ts,tsx}&quot;\n  }\n}\n<\/code><\/pre><\/div>\n<p><strong>Git Hooks<\/strong> (Husky + lint-staged)\uff1a<\/p>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code class=\"language-javascript\">\/\/ .husky\/pre-commit\n#!\/usr\/bin\/env sh\n. &quot;$(dirname -- &quot;$0&quot;)\/_\/husky.sh&quot;\n\nlint-staged --partial-staged\n<\/code><\/pre><\/div>\n<p><strong>\u6548\u679c<\/strong>\uff1a<\/p>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code class=\"language-bash\">$ git add .\n$ git commit -m &quot;feat: add new component&quot;\n\u2714 Pre-commit checks\n  \u2192 ESLint auto-fixed 3 files\n  \u2192 Prettier formatted 5 files\n[master 8a3f9d] feat: add new component\n<\/code><\/pre><\/div>\n<h3>4.2 CLI \u5de5\u5177\u5feb\u901f\u521b\u5efa\u9879\u76ee<\/h3>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code class=\"language-bash\"># \u5b89\u88c5 CLI\nnpm install -g nsbp-cli\n\n# \u521b\u5efa\u65b0\u9879\u76ee\nnpx nsbp-cli create my-app\n\n# \u9879\u76ee\u7ed3\u6784\nmy-app\/\n\u251c\u2500\u2500 src\/\n\u2502   \u251c\u2500\u2500 client\/\n\u2502   \u251c\u2500\u2500 server\/\n\u2502   \u251c\u2500\u2500 containers\/\n\u2502   \u251c\u2500\u2500 component\/\n\u2502   \u251c\u2500\u2500 store\/\n\u2502   \u2514\u2500\u2500 utils\/\n\u251c\u2500\u2500 docker\/\n\u2502   \u251c\u2500\u2500 Dockerfile\n\u2502   \u251c\u2500\u2500 Dockerfile.dev\n\u2502   \u251c\u2500\u2500 docker-compose.yml\n\u2502   \u2514\u2500\u2500 docker-compose.dev.yml\n\u251c\u2500\u2500 config\/\n\u2502   \u251c\u2500\u2500 webpack.base.js\n\u2502   \u251c\u2500\u2500 webpack.client.js\n\u2502   \u2514\u2500\u2500 webpack.server.js\n\u251c\u2500\u2500 .env.example\n\u251c\u2500\u2500 Makefile\n\u2514\u2500\u2500 README.md\n<\/code><\/pre><\/div>\n<h3>4.3 Make \u547d\u4ee4\u7b80\u5316\u64cd\u4f5c<\/h3>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code class=\"language-bash\"># \u67e5\u770b\u6240\u6709\u547d\u4ee4\nmake help\n\nAvailable targets:\n  help            Show this help message\n  build           Build Docker images for production\n  dev             Start development environment\n  prod            Start production environment\n  down            Stop and remove containers\n  logs            View logs\n  restart         Restart production containers\n  rebuild         Rebuild and restart containers\n  shell           Open shell in container\n<\/code><\/pre><\/div>\n<hr>\n<h2>\u4e94\u3001\u6027\u80fd\u4f18\u5316\u5b9e\u8df5<\/h2>\n<h3>5.1 Tree Shaking<\/h3>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code class=\"language-javascript\">\/\/ config\/webpack.base.js\nmodule.exports = {\n  optimization: {\n    usedExports: true,  \/\/ \u53ea\u5bfc\u51fa\u4f7f\u7528\u7684\u5bfc\u51fa\n    sideEffects: false,  \/\/ \u6ca1\u6709\u526f\u4f5c\u7528\n  }\n}\n<\/code><\/pre><\/div>\n<h3>5.2 \u4ee3\u7801\u538b\u7f29\u4e0e\u6df7\u6dc6<\/h3>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code class=\"language-javascript\">\/\/ config\/webpack.client.js (production)\nconst TerserPlugin = require(&#39;terser-webpack-plugin&#39;)\n\nmodule.exports = {\n  optimization: {\n    minimize: true,\n    minimizer: [\n      new TerserPlugin({\n        parallel: true,  \/\/ \u591a\u8fdb\u7a0b\u538b\u7f29\n        terserOptions: {\n          compress: {\n            drop_console: true,  \/\/ \u79fb\u9664 console.log\n            pure_funcs: [&#39;console.log&#39;, &#39;console.info&#39;]\n          }\n        }\n      })\n    ]\n  }\n}\n<\/code><\/pre><\/div>\n<h3>5.3 \u9759\u6001\u8d44\u6e90\u7f13\u5b58\u4f18\u5316<\/h3>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code class=\"language-typescript\">\/\/ src\/server\/index.ts\napp.use(express.static(&#39;public&#39;, {\n  dotfiles: &#39;ignore&#39;,\n  setHeaders: (res, filePath) =&gt; {\n    \/\/ \u7f13\u5b58 1 \u5e74\n    if (filePath.match(\/\\.(js|css|png|jpg|jpeg|gif|svg|ico)$\/)) {\n      res.setHeader(&#39;Cache-Control&#39;, &#39;public, max-age=31536000, immutable&#39;)\n    }\n  }\n}))\n<\/code><\/pre><\/div>\n<h3>5.4 \u56fe\u7247\u5c3a\u5bf8\u63a2\u6d4b<\/h3>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code class=\"language-typescript\">\/\/ \u4f7f\u7528 probe-image-size \u5728 SSR \u9636\u6bb5\u83b7\u53d6\u56fe\u7247\u5c3a\u5bf8\nimport { getPhotoWH } from &#39;@server\/photo&#39;\n\n\/\/ \u5ba2\u6237\u7aef\u76f4\u63a5\u4f7f\u7528\uff0c\u907f\u514d\u4e8c\u6b21\u8bf7\u6c42\nconst [width, height, filename] = photos[0]\n<\/code><\/pre><\/div>\n<hr>\n<h2>\u516d\u3001\u9002\u7528\u573a\u666f\u5206\u6790<\/h2>\n<h3>\u2705 \u9002\u5408\u4f7f\u7528 NSBP \u7684\u573a\u666f<\/h3>\n<ol>\n<li>\n<p><strong>\u4e2a\u4eba\u9879\u76ee\u548c\u5feb\u901f\u539f\u578b<\/strong><\/p>\n<ul>\n<li>\u9700\u8981\u5feb\u901f\u642d\u5efa SSR \u5e94\u7528<\/li>\n<li>\u60f3\u8981\u5b66\u4e60 SSR \u539f\u7406<\/li>\n<li>\u5bf9\u6784\u5efa\u6d41\u7a0b\u6709\u7279\u6b8a\u9700\u6c42<\/li>\n<\/ul>\n<\/li>\n<li>\n<p><strong>\u6559\u80b2\u548c\u5b66\u4e60\u76ee\u7684<\/strong><\/p>\n<ul>\n<li>\u6df1\u5165\u7406\u89e3 React SSR<\/li>\n<li>\u5b66\u4e60 Webpack \u914d\u7f6e<\/li>\n<li>\u638c\u63e1 Redux \u72b6\u6001\u7ba1\u7406<\/li>\n<\/ul>\n<\/li>\n<li>\n<p><strong>\u9700\u8981\u9ad8\u5ea6\u5b9a\u5236\u7684\u9879\u76ee<\/strong><\/p>\n<ul>\n<li>\u81ea\u5b9a\u4e49\u6784\u5efa\u6d41\u7a0b<\/li>\n<li>\u7279\u6b8a\u7684\u5b89\u5168\u7b56\u7565<\/li>\n<li>\u81ea\u5b9a\u4e49\u90e8\u7f72\u65b9\u6848<\/li>\n<\/ul>\n<\/li>\n<li>\n<p><strong>\u4f4e\u8d44\u6e90\u73af\u5883\u90e8\u7f72<\/strong><\/p>\n<ul>\n<li>\u5c0f\u578b VPS \u670d\u52a1\u5668<\/li>\n<li>\u9700\u8981\u4f18\u5316\u955c\u50cf\u5927\u5c0f<\/li>\n<li>\u7cbe\u786e\u63a7\u5236\u8d44\u6e90\u5360\u7528<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n<h3>\u274c \u4e0d\u9002\u5408\u4f7f\u7528 NSBP \u7684\u573a\u666f<\/h3>\n<ol>\n<li>\n<p><strong>\u4f01\u4e1a\u7ea7\u5927\u578b\u5e94\u7528<\/strong><\/p>\n<ul>\n<li>Next.js \u7684\u81ea\u52a8\u4f18\u5316\u80fd\u8282\u7701\u5927\u91cf\u65f6\u95f4<\/li>\n<li>\u9700\u8981 Image Optimization\u3001API Routes \u7b49\u9ad8\u7ea7\u529f\u80fd<\/li>\n<li>\u56e2\u961f\u89c4\u6a21\u5927\uff0c\u9700\u8981\u7edf\u4e00\u7684\u5f00\u53d1\u89c4\u8303<\/li>\n<\/ul>\n<\/li>\n<li>\n<p><strong>\u5feb\u901f\u8fed\u4ee3\u7684\u9879\u76ee<\/strong><\/p>\n<ul>\n<li>\u5f00\u53d1\u901f\u5ea6 &gt; \u5b9a\u5236\u80fd\u529b<\/li>\n<li>\u4e0d\u60f3\u6df1\u5165\u7406\u89e3\u5e95\u5c42\u539f\u7406<\/li>\n<\/ul>\n<\/li>\n<li>\n<p><strong>\u9700\u8981 AI\/ML \u529f\u80fd<\/strong><\/p>\n<ul>\n<li>Next.js \u7684 AI SDK \u96c6\u6210<\/li>\n<li>Vercel AI Platform \u652f\u6301<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n<hr>\n<h2>\u4e03\u3001\u9047\u5230\u7684\u95ee\u9898\u4e0e\u89e3\u51b3\u65b9\u6848<\/h2>\n<h3>7.1 React 19 Hydration \u9519\u8bef #418<\/h3>\n<p><strong>\u95ee\u9898<\/strong>\uff1a\u5728 HTTPS \u73af\u5883\u4e0b\uff0cHome \u7ec4\u4ef6\u62a5\u9519\uff1a<\/p>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code>Minified React error #418\n<\/code><\/pre><\/div>\n<p><strong>\u539f\u56e0<\/strong>\uff1a\u7ec4\u4ef6\u4e2d\u4f7f\u7528\u4e86 <code>dangerouslySetInnerHTML<\/code> \u7684 <code>&lt;script&gt;<\/code> \u6807\u7b7e\uff0c\u5bfc\u81f4\u670d\u52a1\u7aef\u548c\u5ba2\u6237\u7aef HTML \u4e0d\u5339\u914d\u3002<\/p>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code class=\"language-tsx\">\/\/ \u274c \u9519\u8bef\u5199\u6cd5\n&lt;script\n  dangerouslySetInnerHTML={{\n    __html: `setTimeout(() =&gt; { ... }, 800)`\n  }}\n\/&gt;\n<\/code><\/pre><\/div>\n<p><strong>\u89e3\u51b3\u65b9\u6848<\/strong>\uff1a\u5c06\u811a\u672c\u903b\u8f91\u79fb\u5230 <code>useEffect<\/code> \u4e2d\u3002<\/p>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code class=\"language-tsx\">\/\/ \u2705 \u6b63\u786e\u5199\u6cd5\nuseEffect(() =&gt; {\n  const timer = setTimeout(() =&gt; {\n    \/\/ \u5ba2\u6237\u7aef\u6267\u884c\n  }, 300)\n  return () =&gt; clearTimeout(timer)\n}, [])\n<\/code><\/pre><\/div>\n<h3>7.2 Docker \u7aef\u53e3\u914d\u7f6e\u95ee\u9898<\/h3>\n<p><strong>\u95ee\u9898<\/strong>\uff1a<code>.env<\/code> \u6587\u4ef6\u5728\u6839\u76ee\u5f55\uff0c<code>docker-compose.yml<\/code> \u5728 <code>docker\/<\/code> \u76ee\u5f55\uff0c\u5bfc\u81f4\u73af\u5883\u53d8\u91cf\u65e0\u6cd5\u8bfb\u53d6\u3002<\/p>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code class=\"language-bash\"># \u76ee\u5f55\u7ed3\u6784\nnsbp\/\n\u251c\u2500\u2500 .env              # \u73af\u5883\u53d8\u91cf\n\u2514\u2500\u2500 docker\/\n    \u2514\u2500\u2500 docker-compose.yml  # \u914d\u7f6e\u6587\u4ef6\n<\/code><\/pre><\/div>\n<p><strong>\u89e3\u51b3\u65b9\u6848<\/strong>\uff1a<\/p>\n<ol>\n<li>\u5c06 <code>docker-compose.yml<\/code> \u79fb\u5230\u6839\u76ee\u5f55<\/li>\n<li>\u66f4\u65b0 Makefile \u79fb\u9664 <code>docker\/<\/code> \u8def\u5f84\u524d\u7f00<\/li>\n<li>\u66f4\u65b0 CLI \u6a21\u677f\u4fdd\u6301\u4e00\u81f4<\/li>\n<\/ol>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code class=\"language-yaml\">\/\/ docker-compose.yml (\u73b0\u5728\u5728\u6839\u76ee\u5f55\uff09\nservices:\n  app:\n    build:\n      context: .  \/\/ \u2190 \u76f4\u63a5\u4f7f\u7528\u6839\u76ee\u5f55\n      dockerfile: docker\/Dockerfile\n<\/code><\/pre><\/div>\n<h3>7.3 Webpack Dev Server \u70ed\u91cd\u8f7d\u5931\u6548<\/h3>\n<p><strong>\u95ee\u9898<\/strong>\uff1a\u4fee\u6539\u6837\u5f0f\u6587\u4ef6\u540e\uff0c\u9875\u9762\u6ca1\u6709\u81ea\u52a8\u5237\u65b0\u3002<\/p>\n<p><strong>\u539f\u56e0<\/strong>\uff1aBrowser Sync \u914d\u7f6e\u4e0d\u6b63\u786e\u3002<\/p>\n<p><strong>\u89e3\u51b3\u65b9\u6848<\/strong>\uff1a\u6b63\u786e\u914d\u7f6e Browser Sync Webpack Plugin\u3002<\/p>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code class=\"language-javascript\">const BrowserSyncPlugin = require(&#39;browser-sync-webpack-plugin&#39;)\n\nmodule.exports = {\n  plugins: [\n    new BrowserSyncPlugin({\n      host: &#39;localhost&#39;,\n      port: 3000,\n      proxy: {\n        target: &#39;http:\/\/localhost:3001&#39;,\n        ws: true\n      }\n    })\n  ]\n}\n<\/code><\/pre><\/div>\n<hr>\n<h2>\u516b\u3001\u672a\u6765\u89c4\u5212\u4e0e\u6539\u8fdb\u65b9\u5411<\/h2>\n<h3>8.1 \u6280\u672f\u5347\u7ea7<\/h3>\n<ul>\n<li><input disabled=\"\" type=\"checkbox\"> \u5347\u7ea7\u5230 React Server Components\uff08React 19 \u65b0\u7279\u6027\uff09<\/li>\n<li><input disabled=\"\" type=\"checkbox\"> \u96c6\u6210 tRPC \u66ff\u4ee3 Axios\uff08\u7c7b\u578b\u5b89\u5168\u7684 API\uff09<\/li>\n<li><input disabled=\"\" type=\"checkbox\"> \u4f7f\u7528 Vitest \u66ff\u4ee3 Jest\uff08\u66f4\u5feb\u7684\u6d4b\u8bd5\u6846\u67b6\uff09<\/li>\n<li><input disabled=\"\" type=\"checkbox\"> \u6dfb\u52a0 Storybook \u7ec4\u4ef6\u6587\u6863<\/li>\n<\/ul>\n<h3>8.2 \u6027\u80fd\u4f18\u5316<\/h3>\n<ul>\n<li><input disabled=\"\" type=\"checkbox\"> \u5b9e\u73b0 Service Worker \u7f13\u5b58<\/li>\n<li><input disabled=\"\" type=\"checkbox\"> \u6dfb\u52a0 CDN \u652f\u6301\u9759\u6001\u8d44\u6e90<\/li>\n<li><input disabled=\"\" type=\"checkbox\"> \u56fe\u7247\u61d2\u52a0\u8f7d + WebP \u683c\u5f0f\u8f6c\u6362<\/li>\n<li><input disabled=\"\" type=\"checkbox\"> \u5b9e\u73b0 ISR (Incremental Static Regeneration)<\/li>\n<\/ul>\n<h3>8.3 \u5f00\u53d1\u4f53\u9a8c<\/h3>\n<ul>\n<li><input disabled=\"\" type=\"checkbox\"> \u6dfb\u52a0 TypeScript \u8def\u5f84\u81ea\u52a8\u5bfc\u5165<\/li>\n<li><input disabled=\"\" type=\"checkbox\"> \u5b9e\u73b0\u4ee3\u7801\u751f\u6210\u5de5\u5177\uff08CRUD \u6a21\u677f\uff09<\/li>\n<li><input disabled=\"\" type=\"checkbox\"> \u96c6\u6210 e2e \u6d4b\u8bd5\uff08Playwright\uff09<\/li>\n<li><input disabled=\"\" type=\"checkbox\"> \u6dfb\u52a0\u6027\u80fd\u76d1\u63a7\u548c\u5206\u6790<\/li>\n<\/ul>\n<hr>\n<h2>\u4e5d\u3001\u603b\u7ed3<\/h2>\n<h3>\u6536\u83b7\u4e0e\u7ecf\u9a8c<\/h3>\n<p><strong>\u6280\u672f\u5c42\u9762<\/strong>\uff1a<\/p>\n<ul>\n<li>\u6df1\u5165\u7406\u89e3\u4e86 React SSR \u7684\u5de5\u4f5c\u539f\u7406<\/li>\n<li>\u638c\u63e1\u4e86 Webpack \u7684\u590d\u6742\u914d\u7f6e<\/li>\n<li>\u719f\u7ec3\u4f7f\u7528 Redux Toolkit \u72b6\u6001\u7ba1\u7406<\/li>\n<li>\u5b66\u4f1a\u4e86 Docker \u5bb9\u5668\u5316\u90e8\u7f72<\/li>\n<\/ul>\n<p><strong>\u5de5\u7a0b\u5c42\u9762<\/strong>\uff1a<\/p>\n<ul>\n<li>\u5efa\u7acb\u4e86\u5b8c\u6574\u7684\u4ee3\u7801\u8d28\u91cf\u5de5\u5177\u94fe<\/li>\n<li>\u5b9e\u73b0\u4e86\u81ea\u52a8\u5316\u90e8\u7f72\u6d41\u7a0b<\/li>\n<li>\u638c\u63e1\u4e86 CLI \u5de5\u5177\u5f00\u53d1<\/li>\n<li>\u7406\u89e3\u4e86\u8f6f\u4ef6\u67b6\u6784\u8bbe\u8ba1\u539f\u5219<\/li>\n<\/ul>\n<h3>\u5bf9\u6bd4 Next.js<\/h3>\n<table>\n<thead>\n<tr>\n<th>\u7279\u6027<\/th>\n<th>NSBP<\/th>\n<th>Next.js<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>\u5b66\u4e60\u66f2\u7ebf<\/strong><\/td>\n<td>\u9661\u5ced\uff08\u9700\u8981 Webpack \u77e5\u8bc6\uff09<\/td>\n<td>\u5e73\u7f13\uff08\u5f00\u7bb1\u5373\u7528\uff09<\/td>\n<\/tr>\n<tr>\n<td><strong>\u5b9a\u5236\u80fd\u529b<\/strong><\/td>\n<td>\u5b8c\u5168\u53ef\u63a7<\/td>\n<td>\u6709\u9650\uff08\u53d7\u6846\u67b6\u9650\u5236\uff09<\/td>\n<\/tr>\n<tr>\n<td><strong>\u90e8\u7f72\u7075\u6d3b\u6027<\/strong><\/td>\n<td>\u9ad8\uff08\u53ef\u81ea\u5b9a\u4e49\u4efb\u4f55\u65b9\u6848\uff09<\/td>\n<td>\u4e2d\uff08\u4f18\u5148 Vercel\uff09<\/td>\n<\/tr>\n<tr>\n<td><strong>\u5f00\u53d1\u4f53\u9a8c<\/strong><\/td>\n<td>\u9700\u8981\u914d\u7f6e<\/td>\n<td>\u4f18\u79c0\uff08\u96f6\u914d\u7f6e\uff09<\/td>\n<\/tr>\n<tr>\n<td><strong>\u6027\u80fd\u4f18\u5316<\/strong><\/td>\n<td>\u624b\u52a8\u63a7\u5236<\/td>\n<td>\u81ea\u52a8\u4f18\u5316<\/td>\n<\/tr>\n<tr>\n<td><strong>\u9002\u7528\u573a\u666f<\/strong><\/td>\n<td>\u5b66\u4e60\u3001\u4e2a\u4eba\u9879\u76ee\u3001\u9ad8\u5ea6\u5b9a\u5236<\/td>\n<td>\u4f01\u4e1a\u5e94\u7528\u3001\u5feb\u901f\u5f00\u53d1<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h3>\u7ed9\u65b0\u624b\u7684\u5efa\u8bae<\/h3>\n<ol>\n<li><strong>\u5148\u5b66 Next.js<\/strong>\uff1a\u7406\u89e3 SSR \u7684\u57fa\u672c\u6982\u5ff5\u548c\u6700\u4f73\u5b9e\u8df5<\/li>\n<li><strong>\u518d\u5b66 Webpack<\/strong>\uff1a\u638c\u63e1\u6784\u5efa\u5de5\u5177\u7684\u5e95\u5c42\u539f\u7406<\/li>\n<li><strong>\u5c1d\u8bd5 NSBP<\/strong>\uff1a\u7406\u89e3\u4e00\u4e2a\u5b8c\u6574\u7684 SSR \u6846\u67b6\u662f\u5982\u4f55\u5de5\u4f5c\u7684<\/li>\n<li><strong>\u52a8\u624b\u5b9e\u8df5<\/strong>\uff1a\u81ea\u5df1\u6784\u5efa\u4e00\u4e9b\u5c0f\u9879\u76ee\uff0c\u6df1\u5165\u7406\u89e3\u6bcf\u4e2a\u73af\u8282<\/li>\n<\/ol>\n<hr>\n<h2>\u5341\u3001\u5feb\u901f\u5f00\u59cb<\/h2>\n<div class=\"code-toolbar\"><pre data-toolbar-order=\"copy\" data-prismjs-copy><code class=\"language-bash\"># 1. \u5b89\u88c5\u4f9d\u8d56\npnpm install\n\n# 2. \u914d\u7f6e\u73af\u5883\ncp .env.example .env\n\n# 3. \u542f\u52a8\u5f00\u53d1\npnpm run dev\n\n# 4. \u8bbf\u95ee\u5e94\u7528\nopen http:\/\/localhost:3001\n\n# 5. Docker \u90e8\u7f72\nmake prod\n<\/code><\/pre><\/div>\n<hr>\n<h2>\u7ed3\u8bed<\/h2>\n<p>\u6784\u5efa NSBP \u7684\u8fc7\u7a0b\u662f\u4e00\u6b21\u975e\u5e38\u6709\u4ef7\u503c\u7684\u5b66\u4e60\u7ecf\u5386\u3002\u867d\u7136\u6700\u7ec8\u4ea7\u54c1\u53ef\u80fd\u4e0d\u4f1a\u6210\u4e3a Next.js \u7684\u66ff\u4ee3\u54c1\uff0c\u4f46\u901a\u8fc7\u8fd9\u4e2a\u9879\u76ee\uff0c\u6211\uff1a<\/p>\n<ul>\n<li><strong>\u6df1\u5165\u7406\u89e3\u4e86\u73b0\u4ee3\u524d\u7aef\u5de5\u7a0b\u5316\u7684\u6bcf\u4e2a\u73af\u8282<\/strong><\/li>\n<li><strong>\u638c\u63e1\u4e86\u4ece\u96f6\u6784\u5efa\u4e00\u4e2a\u5b8c\u6574\u5e94\u7528\u7684\u80fd\u529b<\/strong><\/li>\n<li><strong>\u57f9\u517b\u4e86\u89e3\u51b3\u590d\u6742\u95ee\u9898\u7684\u601d\u7ef4\u6a21\u5f0f<\/strong><\/li>\n<\/ul>\n<p>\u6280\u672f\u6846\u67b6\u7684\u9009\u62e9\u6ca1\u6709\u7edd\u5bf9\u7684\u5bf9\u9519\uff0c\u53ea\u6709\u9002\u4e0d\u9002\u5408\u3002Next.js \u9002\u5408\u5feb\u901f\u5f00\u53d1\u548c\u5927\u591a\u6570\u573a\u666f\uff0c\u4f46\u5982\u679c\u4f60\u60f3\u8981\u5b8c\u5168\u638c\u63a7\u3001\u6df1\u5165\u5b66\u4e60\u3001\u6216\u8005\u6709\u7279\u6b8a\u9700\u6c42\uff0c\u90a3\u4e48\u4ece\u96f6\u6784\u5efa\u4e5f\u662f\u4e00\u79cd\u9009\u62e9\u3002<\/p>\n<p>\u5e0c\u671b\u8fd9\u7bc7\u6587\u7ae0\u80fd\u5bf9\u6b63\u5728\u5b66\u4e60 React SSR \u7684\u4f60\u6709\u6240\u5e2e\u52a9\uff01<\/p>\n<hr>\n<p><strong>\u9879\u76ee\u94fe\u63a5<\/strong>\uff1a<\/p>\n<ul>\n<li>\u5728\u7ebf\u6f14\u793a: <a href=\"https:\/\/nsbp.erishen.cn\/\">https:\/\/nsbp.erishen.cn\/<\/a><\/li>\n<li>GitHub: <a href=\"https:\/\/github.com\/erishen\/nsbp\">https:\/\/github.com\/erishen\/nsbp<\/a><\/li>\n<li>CLI: <code>npx nsbp-cli create my-app<\/code><\/li>\n<\/ul>\n<p><strong>\u76f8\u5173\u6587\u7ae0<\/strong>\uff1a<\/p>\n<ul>\n<li>[\u5982\u4f55\u4ece\u96f6\u5f00\u59cb\u6784\u5efa\u4e00\u4e2a React SSR \u5e94\u7528\uff08\u5f85\u5199\uff09]<\/li>\n<li>[Webpack 5 \u6700\u4f73\u5b9e\u8df5\u6307\u5357\uff08\u5f85\u5199\uff09]<\/li>\n<li>[TypeScript + Redux Toolkit \u5b9e\u6218\uff08\u5f85\u5199\uff09]<\/li>\n<\/ul>\n<hr>\n<p><em>\u672c\u6587\u9996\u53d1\u4e8e <a href=\"https:\/\/erishen.cn\/\">Erishen \u7684\u6280\u672f\u535a\u5ba2<\/a><\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u524d\u8a00 \u5728 React \u751f\u6001\u7cfb\u7edf\u4e2d\uff0cNext.js \u4f5c\u4e3a SSR \u6846\u67b6\u7684\u4ee3\u8868\uff0c\u4ee5\u5176\u5f00\u7bb1\u5373\u7528\u7684\u4f53\u9a8c\u548c\u5f3a\u5927\u7684\u529f\u80fd\u6df1\u53d7 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-222","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/erishen.cn\/index.php?rest_route=\/wp\/v2\/posts\/222","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/erishen.cn\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/erishen.cn\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/erishen.cn\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/erishen.cn\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=222"}],"version-history":[{"count":0,"href":"https:\/\/erishen.cn\/index.php?rest_route=\/wp\/v2\/posts\/222\/revisions"}],"wp:attachment":[{"href":"https:\/\/erishen.cn\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=222"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/erishen.cn\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=222"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/erishen.cn\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=222"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}