news 2026/9/27 21:17:33

loadable-components 服务端渲染(SSR)示例全解析:从克隆仓库到跑通并理解其原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
loadable-components 服务端渲染(SSR)示例全解析:从克隆仓库到跑通并理解其原理
  • 前端

【免费下载链接】loadable-components

The recommended Code Splitting library for React ✂️✨

项目地址:https://gitcode.com/gh_mirrors/loa/loadable-components
点击查看免费下载

loadable-components是 React 生态中主流的代码分割(Code Splitting)方案之一,而服务端渲染(SSR)正是它的核心应用场景:只有把按需加载的组件在服务端正确收集、注入 HTML,并在客户端精确水合(hydrate),代码分割才能真正落地。本篇文章以仓库中的examples/server-side-rendering示例为骨架,完整走通「克隆 → 构建 → 本地开发 → 生产部署」的全流程,并结合该示例的 webpack 配置、Babel 配置、Express 服务器代码与客户端入口,拆解ChunkExtractor、loadableReady、ssr: false等关键机制。读完你将具备独立跑通并二次开发一个 React SSR + 代码分割项目的能力。

一、示例定位:这个 SSR 示例到底演示了什么

examples/server-side-rendering是一个完整的、可独立运行的 Express + React 服务端渲染项目。与仓库中其他示例相比,它专门演示了@loadable/*全家桶在 SSR 场景下的完整协作:

  • @loadable/server的ChunkExtractor:在服务端收集渲染过程中被加载的 chunk,并生成对应的<link>、<style>、<script>标签;
  • @loadable/component的loadableReady():在客户端等待所有异步 chunk 就绪后再执行hydrate,避免水合失配;
  • @loadable/babel-plugin与@loadable/webpack-plugin:分别在编译期与打包期记录模块与 chunk 的映射关系,产出loadable-stats.json。

示例的客户端代码(App.js)刻意覆盖了多种用法:普通loadable()、动态模板路径加载、webpackPreload/webpackPrefetch注释、ssr: false、同一个模块在 SSR 内外的双份加载对比,以及用loadable.lib()加载非 React 库(moment),是学习 API 边角场景的最佳现场。

二、官方文档给出的运行步骤(原样保留)

原 README(examples/server-side-rendering/README.md)给出的步骤如下:

  1. 克隆仓库
git clone https://github.com/gregberge/loadable-components.git
  1. 安装 yarn(如果尚未安装)

  2. 安装库依赖并构建库(packages)

yarn yarn build
  1. 进入示例目录
cd ./loadable-components/examples/server-side-rendering
  1. 安装示例项目依赖
yarn
  1. 本地开发运行,或构建后以生产模式启动
yarn dev # 或者 yarn build yarn start

说明:原文步骤编号存在两处 "5",此处按执行顺序重新编号;原文档内部链接为外部站点的 yarn 安装指引,属于环境安装信息,实际操作时按对应平台的官方方式安装即可。

这套流程的核心在于第 3 步:仓库采用 Lerna + yarn workspace 结构(见根目录 lerna.json),示例的package.json通过file:./../../packages/*直接引用本地包源码,因此必须先对packages目录执行一次构建,才能保证@loadable/babel-plugin、@loadable/server、@loadable/component、@loadable/webpack-plugin可被示例引用到编译产物。

三、脚本层面拆解:dev/build/start各自做了什么

示例的 package.json 定义了 5 个关键脚本,逐条解析如下:

脚本实际命令作用
devnodemon src/server/main.js开发模式:监听src/server/main.js,配合nodemon.json使用babel-node直接运行 ES 模块
buildrm -Rf ./public && NODE_ENV=production yarn build:webpack && yarn build:lib清理旧产物后,依次执行 webpack 打包与 Babel 编译
build:webpackwebpack读取webpack.config.babel.js,一次性打出web与node两套 bundle
build:libbabel -d lib src把src下的服务端代码用 Babel 编译输出到lib,供生产模式node直接运行
startNODE_ENV=production node lib/server/main.js以生产模式启动编译后的服务端入口

依赖信息同样值得注意:示例直接以file:协议引用本地包(@loadable/babel-plugin、@loadable/component、@loadable/server、@loadable/webpack-plugin),同时提供link:all脚本(yarn link三个包),便于在调试本地包改动时快速建立软链接。生产运行所需的最小依赖为express、react、react-dom、core-js、moment;构建期使用webpack@4、webpack-cli、babel-loader、css-loader、mini-css-extract-plugin、webpack-node-externals与nodemon。

四、开发模式深入:nodemon + babel-node + webpack-dev-middleware 三件套

执行yarn dev后,实际发生的是:

  1. nodemon 以 babel-node 启动服务端。nodemon.json(examples/server-side-rendering/nodemon.json)设置了"execMap": { "js": "babel-node" },并对client、public目录做了 ignore——也就是说服务端源码改动会自动重启,但客户端源码与构建产物改动不会触发重启(它们由 webpack 热编译接管)。

  2. Express 服务器在非生产模式下挂载 webpack-dev-middleware。见 src/server/main.js:开发模式下通过require('../../webpack.config.babel')读取 webpack 配置并创建 compiler,然后以webpackDevMiddleware(compiler, ...)挂载中间件。其中writeToDisk回调只把两类文件写入磁盘:

    • 匹配/dist\/node\//的 Node 侧产物;
    • 包含loadable-stats的文件。

    这一设计是 SSR 场景的关键:内存中的 web 产物可以直接由中间件对外提供,但服务端渲染必须从磁盘同步读取 chunk 统计文件(loadable-stats.json)与 Node 侧 bundle,因此必须选择性落盘。

  3. 统一入口处理所有请求。服务器对*路径统一走 SSR 渲染逻辑(见下文第五节),开发模式下由中间件负责 web 资源的实时编译,express.static(src/server/main.js)负责静态资源服务,两者叠加即可做到改代码即时生效。

五、生产模式构建与启动:webpack 双配置 + Babel 编译服务端

yarn build分两步:

第一步build:webpack:读取 webpack.config.babel.js,该文件用getConfig(target)工厂函数导出web与node两套配置:

  • 公共部分:mode由NODE_ENV决定(开发或生产);入口分别是./src/client/main-web.js与./src/client/main-node.js;输出目录为public/dist/web与public/dist/node,publicPath为/dist/${target}/;optimization.moduleIds/chunkIds使用named,便于在loadable-stats.json中对照 chunk 名;插件统一挂载new LoadablePlugin()(@loadable/webpack-plugin)与MiniCssExtractPlugin。
  • Node 侧特有:target: 'node',且externals配置为['@loadable/component', nodeExternals()]——把@loadable/component和所有node_modules依赖排除出 bundle,交给 Node 运行时直接 require,从而显著减小产物体积;同时libraryTarget: 'commonjs2',保证main-node.js的默认导出(App 组件)可以被服务端代码requireEntrypoint()取到。
  • Web 侧特有:libraryTarget未设置(浏览器全局),CSS 通过MiniCssExtractPlugin抽取为独立样式文件。

第二步build:lib:babel -d lib src把src(含server/main.js)编译到lib。为什么服务端代码还要单独 Babel 一遍?因为start脚本使用原生node执行,而src中大量使用 ES Module 与 JSX,必须预先编译为 CommonJS。

启动yarn start:NODE_ENV=production node lib/server/main.js,此时开发中间件分支被跳过,服务器直接读取public/dist/node/loadable-stats.json与public/dist/web/loadable-stats.json两份统计文件进行渲染,并监听http://localhost:9000(见 src/server/main.js)。

六、SSR 核心原理:双ChunkExtractor的收集与注入

示例服务端渲染的核心代码集中在 src/server/main.js:

const nodeStats = path.resolve(__dirname, '../../public/dist/node/loadable-stats.json') const webStats = path.resolve(__dirname, '../../public/dist/web/loadable-stats.json') app.get('*', (req, res) => { const nodeExtractor = new ChunkExtractor({ statsFile: nodeStats }) const { default: App } = nodeExtractor.requireEntrypoint() const webExtractor = new ChunkExtractor({ statsFile: webStats }) const jsx = webExtractor.collectChunks(<App />) const html = renderToString(jsx) res.set('content-type', 'text/html') res.send(` <!DOCTYPE html> <html> <head> ${webExtractor.getLinkTags()} ${webExtractor.getStyleTags()} </head> <body> <div id="main">${html}</div> ${webExtractor.getScriptTags()} </body> </html> `) })

这里有两个关键动作:

  1. Node 侧提取入口组件:nodeExtractor.requireEntrypoint()从 Node 侧统计文件中定位main-node.js打包出的入口 chunk 并 require,拿到默认导出的App。这正是 main-node.js 只做export { default } from './App'的原因——Node 侧入口不需要执行渲染/水合,只需要把组件树暴露给服务端。
  2. Web 侧收集与注入:webExtractor.collectChunks(<App />)包裹组件树,在renderToString遍历过程中记录实际加载了哪些 loadable chunk;随后getLinkTags()/getStyleTags()/getScriptTags()分别输出<link rel="preload">、CSS 样式标签与<script>标签,按顺序注入<head>与<body>,确保浏览器拿到的是「刚好够用」的脚本与样式集合。

ChunkExtractor的实现位于 packages/server/src/ChunkExtractor.js,getLinkTags/getStyleTags/getScriptTags等方法的完整 API 可参考 api-loadable-server.mdx。

一个值得注意的细节:示例中App.js内同时存在ssr: false的E、GClient等组件,它们在服务端渲染时不会触发对应 chunk 的加载,因此不会出现在服务端收集结果中——这正是ssr选项控制「是否在服务端同步渲染并预加载」的直观体现。

七、客户端水合:loadableReady为什么必不可少

客户端入口 main-web.js 只有十余行:

import 'core-js' import React from 'react' import { hydrate } from 'react-dom' import { loadableReady } from '@loadable/component' import App from './App' loadableReady(() => { const root = document.getElementById('main') hydrate(<App />, root) })

三个要点:

  1. import 'core-js':webpack 的 web 侧配置把core-js@3作为 polyfill 引入(对应 Babel 配置中useBuiltIns: 'entry'、corejs: 'core-js@3'),保证低版本浏览器的语法与 API 兼容。
  2. hydrate而非render:服务端已输出完整的 HTML,客户端需要复用现有 DOM 并绑定事件。直接render会重建整棵 DOM 树,导致闪烁与性能浪费。
  3. loadableReady包裹水合:服务端注入的<script>标签在 HTML 解析时可能尚未完成 chunk 加载,若直接 hydrate,异步组件在首屏可能短暂处于未加载状态,造成 React 水合失配警告甚至错误。loadableReady(实现见 packages/component/src/loadableReady.js)会等待所有服务端渲染阶段标记过的 chunk 全部加载完成后,再执行hydrate,从而保证水合时组件状态与服务端完全一致。

八、示例中的 loadable 用法全景:一网打尽常见 API

App.js 集中演示了@loadable/component的多种形态,是理解 API 边角行为的现成教材:

const A = loadable(() => import('./letters/A')) const B = loadable(() => import('./letters/B')) const C = loadable(() => import(/* webpackPreload: true */ './letters/C')) const D = loadable(() => import(/* webpackPrefetch: true */ './letters/D')) const E = loadable(() => import('./letters/E?param'), { ssr: false }) const X = loadable(props => import(`./letters/${props.letter}`)) const Sub = loadable(props => import(`./letters/${props.letter}/file`)) const RootSub = loadable(props => import(`./${props.letter}/file`)) // 同一个 'G' 模块加载两次:一次完全客户端加载,一次参与 SSR const GClient = loadable(() => import('./letters/G'), { ssr: false, fallback: <span className="loading-state">ssr: false - Loading...</span>, }) const GServer = loadable(() => import('./letters/G'), { ssr: true, fallback: <span className="loading-state">ssr: true - Loading...</span>, }) const Moment = loadable.lib(() => import('moment'), { resolveComponent: moment => moment.default || moment, })
  • 静态导入(A、B):最常见的loadable(() => import(...))写法,对应 babel 插件将动态导入改写为带 chunk 标记的模块记录。
  • 预加载提示(C、D):/* webpackPreload: true */与/* webpackPrefetch: true */是 webpack 内置注释,让浏览器提前拉取对应 chunk,配合 SSR 场景可进一步优化首屏体验。
  • ssr: false(E、GClient):告知组件「不要参与服务端渲染」,服务端只渲染 fallback,chunk 完全在客户端加载。这在组件依赖浏览器 API(如window、document)时是标准解法。
  • 动态模板路径(X、Sub、RootSub):import(./letters/${props.letter})这类写法会被打包为按目录分组的动态 chunk,允许运行时根据 props 决定加载哪个模块;Sub演示子目录./letters/${letter}/file,RootSub演示任意根目录层级./${letter}/file。对应的模块文件如 letters/Z/file.js 与 Y/file.js,均以默认导出形式被消费。
  • 同一模块双份加载(GClientvsGServer):示例刻意用两个 loadable 实例加载同一个G组件,一个ssr: false一个ssr: true,用来观察同一个 chunk 在 SSR 与纯客户端两种路径下的加载时机差异——服务端渲染时GServer的 chunk 会被收集注入,而GClient不会。
  • loadable.lib(Moment):加载非 React 库(moment),通过resolveComponent处理模块默认导出与命名导出的差异(moment.default || moment),渲染时以 render prop 形式使用moment().format('HH:mm')。

各模块文件也很精简但各有用途:A.js 引入了moment与自己的A.css以模拟「依赖共享库 + 样式」的典型异步模块;G.js 是一个带 props 的纯函数组件,配合prefix属性区分两种加载路径。

九、Babel 配置:同一份源码,如何区分 web / node / webpack 三种环境

babel.config.js 是理解这套示例「一份源码多环境编译」的关键:

function isWebTarget(caller) { return Boolean(caller && caller.target === 'web') } function isWebpack(caller) { return Boolean(caller && caller.name === 'babel-loader') } module.exports = api => { const web = api.caller(isWebTarget) const webpack = api.caller(isWebpack) return { presets: [ '@babel/preset-react', [ '@babel/preset-env', { useBuiltIns: web ? 'entry' : undefined, corejs: web ? 'core-js@3' : false, targets: !web ? { node: 'current' } : undefined, modules: webpack ? false : 'commonjs', }, ], ], plugins: ['@babel/plugin-syntax-dynamic-import', '@loadable/babel-plugin'], } }
  • api.caller()是 Babel 7 提供的「向编译方询问上下文」机制。webpack 配置中给babel-loader传了caller: { target }(见 webpack.config.babel.js),因此 web 打包时isWebTarget命中,而 Node 侧打包时caller.target为node。
  • 三种编译环境的差异一目了然:
    • web 打包(babel-loader + target=web):启用useBuiltIns: 'entry'+core-js@3按入口注入 polyfill,modules: false保留 ES Module 交给 webpack 做 tree-shaking;
    • node 打包(babel-loader + target=node):targets: { node: 'current' }面向当前 Node 版本转译,不注入 core-js;
    • build:lib的 Babel CLI 编译(无 babel-loader caller):modules: 'commonjs',把src转成 Node 可直接 require 的 CommonJS 产物。
  • plugins中的@loadable/babel-plugin是关键:它会把loadable(() => import(...))的导入改写为带 chunkName 的loadable()调用(实现见 packages/babel-plugin/src/index.js),使运行时能够记录「哪个组件对应哪个 chunk」;@babel/plugin-syntax-dynamic-import则让 Babel 在转译时保留动态导入语法。

十、常见问题与排查思路

结合脚本配置与源码,运行中可能遇到的情况大致可分为以下几类:

  • yarn dev报错找不到@loadable/*包:通常是第 3 步的库构建未执行或产物过期。回到仓库根目录重新执行yarn && yarn build,或在示例目录执行yarn link:all建立本地软链接后再运行。
  • loadable-stats.json不存在 /ENOENT:该文件由@loadable/webpack-plugin在打包时生成到public/dist/{web,node}/。开发模式下需确保writeToDisk逻辑生效(Node 侧产物与 stats 文件必须落盘),生产模式下需完整执行yarn build(rm -Rf ./public清理旧目录后重新生成)。
  • 水合失配(hydration mismatch)警告:优先检查是否所有参与 SSR 的组件都已由loadableReady包裹后再hydrate;对依赖浏览器 API 的组件,应使用{ ssr: false }明确排除出服务端渲染。
  • 端口占用:示例固定监听9000端口(src/server/main.js),如需更换可修改app.listen的参数。
  • 依赖版本提示:示例使用webpack@4与react@16.8(见 package.json),其配置写法(如optimization.moduleIds、chunkIds)与 webpack 5 存在差异;若在较新环境运行遇到兼容性问题,可参考仓库中配套的 webpack5 示例。

十一、进一步探索

  • 查看服务端渲染 API 的完整文档:api-loadable-server.mdx;
  • 阅读ChunkExtractor与ChunkExtractorManager的实现:packages/server/src/ChunkExtractor.js、packages/server/src/ChunkExtractorManager.js;
  • 对照loadableReady与loadable()的实现:packages/component/src/loadableReady.js、packages/component/src/loadable.js;
  • 对比更复杂的场景示例:Razzle 集成的 examples/razzle、异步 Node 版本的 examples/server-side-rendering-async-node、TypeScript 版本的 examples/typescript 以及 webpack 5 版本的 examples/webpack/webpack5。

从一条简短的「Get the SSR example running」到完整的双配置打包、双ChunkExtractor收集注入、loadableReady水合闭环,这个示例把loadable-components在 SSR 场景下的每一个环节都落到了可运行的代码上。按本文顺序跑通一遍,再对照源码逐行阅读,你就能把这个示例改造成自己项目里的 SSR 代码分割基础设施。

  • 前端

【免费下载链接】loadable-components

The recommended Code Splitting library for React ✂️✨

项目地址:https://gitcode.com/gh_mirrors/loa/loadable-components
点击查看免费下载
上一篇:PaddleNLP 中文阅读理解鲁棒性实战:DuReader-robust 数据集微调全流程
下一篇:Windows10Debloater未来Roadmap:三大维度重构Windows系统优化体验

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/27 21:17:33

建设营销网站时以什么为导向?安全速查手册

建设营销网站时以什么为导向?安全速查手册 自己不会代码想做网站,最头疼的不是界面好不好看,而是上线后数据丢没丢、后台被没被黑。很多老板觉得营销网站就是放个画册、接个表单,把预算全砸在UI和SEO上,结果上线两周,服务器被挂满挖矿脚本,或者用户信息被拖库。这时候你才反应过来,…

作者头像 李华
网站建设 2026/9/27 21:17:19

有做soho网站的吗?这份避坑指南专治备案一头雾水

有做soho网站的吗?这份避坑指南专治备案一头雾水 你是不是也在问“有做soho网站的吗”,结果一查发现备案流程一头雾水,根本不知道从哪下手?别急,这篇避坑指南就是专门给你准备的,不绕弯子,直接讲怎么把SOHO网站从想法变成线上生意。很多外贸SOHO、自由职业者卡在第一步,不是不会写代码,而是搞不懂…

作者头像 李华
网站建设 2026/9/27 21:16:38

下载手机商城app下载安装避坑指南:选哪家好?

下载手机商城app下载安装避坑指南:选哪家好? 很多老板盯着“下载手机商城app下载安装”这几个字,心里其实慌得很。不是怕开发难,而是怕域名、服务器、备案这些底层逻辑搞不懂,钱花出去了,网站却像个孤岛,搜不到也打不开。这种“地基没打牢”的焦虑,比选哪家开发公司哪家好更让人头疼。…

作者头像 李华