- 前端
【免费下载链接】loadable-components
The recommended Code Splitting library for React ✂️✨
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)给出的步骤如下:
- 克隆仓库
git clone https://github.com/gregberge/loadable-components.git安装 yarn(如果尚未安装)
安装库依赖并构建库(packages)
yarn yarn build- 进入示例目录
cd ./loadable-components/examples/server-side-rendering- 安装示例项目依赖
yarn- 本地开发运行,或构建后以生产模式启动
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 个关键脚本,逐条解析如下:
| 脚本 | 实际命令 | 作用 |
|---|---|---|
dev | nodemon src/server/main.js | 开发模式:监听src/server/main.js,配合nodemon.json使用babel-node直接运行 ES 模块 |
build | rm -Rf ./public && NODE_ENV=production yarn build:webpack && yarn build:lib | 清理旧产物后,依次执行 webpack 打包与 Babel 编译 |
build:webpack | webpack | 读取webpack.config.babel.js,一次性打出web与node两套 bundle |
build:lib | babel -d lib src | 把src下的服务端代码用 Babel 编译输出到lib,供生产模式node直接运行 |
start | NODE_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后,实际发生的是:
nodemon 以 babel-node 启动服务端。
nodemon.json(examples/server-side-rendering/nodemon.json)设置了"execMap": { "js": "babel-node" },并对client、public目录做了 ignore——也就是说服务端源码改动会自动重启,但客户端源码与构建产物改动不会触发重启(它们由 webpack 热编译接管)。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,因此必须选择性落盘。- 匹配
统一入口处理所有请求。服务器对
*路径统一走 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> `) })这里有两个关键动作:
- Node 侧提取入口组件:
nodeExtractor.requireEntrypoint()从 Node 侧统计文件中定位main-node.js打包出的入口 chunk 并 require,拿到默认导出的App。这正是 main-node.js 只做export { default } from './App'的原因——Node 侧入口不需要执行渲染/水合,只需要把组件树暴露给服务端。 - 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) })三个要点:
import 'core-js':webpack 的 web 侧配置把core-js@3作为 polyfill 引入(对应 Babel 配置中useBuiltIns: 'entry'、corejs: 'core-js@3'),保证低版本浏览器的语法与 API 兼容。hydrate而非render:服务端已输出完整的 HTML,客户端需要复用现有 DOM 并绑定事件。直接render会重建整棵 DOM 树,导致闪烁与性能浪费。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 产物。
- web 打包(babel-loader + target=web):启用
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 ✂️✨
相关推荐
loadable-components 的 webpack 5 服务端渲染示例深度解析:从零跑通 SSR 与按需加载
loadable components 的 webpack 5 服务端渲染示例深度解析:从零跑通 SSR 与按需加载 本篇技术指南以仓库中 examples/w
前端loadable-components 服务端渲染(async-node 模式)实战指南:从零运行 SSR 示例并理解其双构建架构
loadable components 服务端渲染(async node 模式)实战指南:从零运行 SSR 示例并理解其双构建架构 导读 本文以 example
前端loadable-components服务端渲染原理:从虚拟DOM到HTML
loadable components服务端渲染原理:从虚拟DOM到HTML 1. 服务端渲染 SSR 的核心痛点 传统React应用在客户端渲染时,首次加载会
前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考