news 2026/10/13 6:08:10

react-snap 深度解析:零配置 SPA 静态预渲染的原理、defaultOptions 配置全解与生产级注意事项

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
react-snap 深度解析:零配置 SPA 静态预渲染的原理、defaultOptions 配置全解与生产级注意事项

【免费下载链接】react-snap

👻 Zero-configuration framework-agnostic static prerendering for SPAs

项目地址:https://gitcode.com/gh_mirrors/re/react-snap
点击查看免费下载

react-snap 是一个「零配置、框架无关」的 SPA 静态预渲染工具:它用 Headless Chrome 从根路由出发爬取站内所有链接,把渲染完成的页面保存为静态 HTML,从而为单页应用解决 SEO(Google、DuckDuckGo)与 SMO(Twitter、Facebook 分享卡片)问题。本文基于项目 README 与源码(index.js、run.js、src/puppeteer_utils.js)展开,覆盖从 create-react-app / Preact / Vue.js 三套接入方式、defaultOptions全部配置项、inlineCss关键 CSS 内联机制,到异步组件闪烁、Redux 状态注入、AJAX 缓存等生产环境注意事项的完整实操路径。读完你应能独立完成接入、按项目构建工具定制配置,并规避 README 中列出的全部已知坑。

什么是 react-snap:零配置的静态预渲染

项目定位为「Zero-configuration framework-agnostic static prerendering for SPAs」(见 package.json 的description字段)。其官方功能特性如下(继承自 Readme.md):

  • 为 SPA 启用SEO(Google、DuckDuckGo 等)与SMO(Twitter、Facebook 等);
  • 与 create-react-app开箱即用,无需任何代码改动;
  • 背后使用真实浏览器(Headless Chrome),因此不受 WebGL、Blob 等 HTML5 特性支持问题的困扰;
  • 内置大量加载性能优化(详见 doc/load-performance-optimizations.md);
  • 不依赖 React:名字只是致敬react-snapshot,实际可用于任何技术栈(如 Vue、Preact);
  • npm 包没有编译步骤,因此你可以 fork 它、按需修改后通过 GitHub URL 直接安装。

零配置是其核心卖点:你不需要理解或配置它如何工作。如果对内部机制感兴趣,可参阅 doc/behind-the-scenes.md,本文第六节将直接基于源码给出完整的流水线解读。

适用前提:运行环境要求 Node>= 8.6.0(package.json 的engines字段);核心依赖为puppeteer ^1.8.0,即首次安装时会下载 Chromium 可执行文件,这也是后续容器环境配置章节的背景。

基础接入

方式一:create-react-app(默认场景)

第一步,安装:

yarn add --dev react-snap

第二步,修改package.json,让构建完成后自动执行预渲染:

"scripts": { "postbuild": "react-snap" }

这里的react-snap命令对应 package.json 中的"bin": { "react-snap": "./run.js" },即安装后会自动提供 CLI 入口 run.js。

第三步,修改src/index.js(React 16+):

import { hydrate, render } from "react-dom"; const rootElement = document.getElementById("root"); if (rootElement.hasChildNodes()) { hydrate(<App />, rootElement); } else { render(<App />, rootElement); }

逻辑非常直接:如果#root下已有预渲染的节点(说明命中了 react-snap 生成的静态 HTML),就用hydrate在既有 DOM 上挂载事件与状态;否则回退到普通的render。到这一步接入就完成了。

方式二:Preact

Preact 的 hydration 需要借助一个特殊技巧(参考 Preact 官方 issue #1060 的讨论结论):

const rootElement = document.getElementById("root"); if (rootElement.hasChildNodes()) { preact.render(<App />, rootElement, rootElement.firstElementChild); } else { preact.render(<App />, rootElement); }

关键在于第三个参数rootElement.firstElementChild:Preact 通过它知道应当复用已有的子节点而不是从空容器开始渲染。

方式三:Vue.js

安装:

yarn add --dev react-snap

修改package.json:

"scripts": { "postbuild": "react-snap" }, "reactSnap": { "source": "dist", "minifyHtml": { "collapseWhitespace": false, "removeComments": false } }

两个关键配置点:

  • source指向 webpack(或任意 bundler)的输出目录。Vue 项目通常是dist,而 create-react-app 项目默认就是build(与 index.js 中defaultOptions.source: "build"的默认值一致),所以 CRA 场景无需配置。
  • Vue 模板编译会保留模板中的空白文本节点,默认 HTML 压缩选项会破坏它,因此需要关闭collapseWhitespace与removeComments;README 同时给出替代方案:在vue-loader中使用preserveWhitespace: false。minifyHtml各参数的注意事项可参考项目 issue #142。
Vue 的注意事项(Caveats)
  1. 仅支持 HTML5 history API 的路由策略,不支持 hash(bang)URL 路由;
  2. Vue 用data-server-rendered属性标记根元素来标识「SSR 生成的标记」,该属性存在时 VDOM 会走 rehydrate 而非全量渲染,可能引起页面闪烁。官方给出的小 hack 是利用window.snapSaveState回调改写 DOM:
window.snapSaveState = () => { document.querySelector("#app").setAttribute("data-server-rendered", "true"); };

window.snapSaveState的语义是「渲染结束时的应用状态保存回调」,本可用于 Redux 或异步组件;此处的示例把它挪用来直接改 DOM,所以作者称其为 hack。从源码看,这个回调确实会在每个页面保存 HTML 前被调用:index.js 中afterFetch钩子在页面内执行window.snapSaveState(),并把返回对象序列化为window["key"]=...脚本注入 HTML。

  1. Vue 1.x:确保根组件使用replace: false。

工作原理:一条 14 步的预渲染流水线

doc/behind-the-scenes.md 给出了完整的执行步骤,下面逐条结合源码印证:

  1. 把index.html复制为200.html。对应 index.js:fs.createReadStream(path.join(sourceDir, "index.html")).pipe(fs.createWriteStream(path.join(sourceDir, "200.html")))。这份未被预渲染的index.html后续作为 service worker fallback 与 SPA 回退页使用。
  2. 启动本地 Web 服务器。index.js 用express+serve-static挂载source目录,并用express-history-api-fallback以200.html作为 history 回退页,监听默认端口45678(defaultOptions.port)。这样爬虫访问任何深层路由都能拿到应用外壳,路由交给前端处理。
  3. 访问/或include配置的任意页面。入口队列由options.include填充(src/puppeteer_utils.js)。
  4. 收集同域链接入队。getLinks(src/puppeteer_utils.js)抓取页面内所有a与link[rel='alternate']锚点(含 SVG 内<a xlink:href>的baseVal)以及iframe的src。addToQueue(src/puppeteer_utils.js)会剥掉 query 与 hash,且只入队与当前端口一致的 localhost 链接——这解释了为什么爬虫期间你必须请求具体 host 而不是相对路径。
  5. 队列超过 1 个 URL 时自动追加/404.html(src/puppeteer_utils.js),为多页应用预渲染 404 页。
  6. 用 Puppeteer 渲染页面。page.goto(pageUrl, { waitUntil: "networkidle0" })(src/puppeteer_utils.js)会一直等待直到网络空闲。
  7. 等待网络空闲(即上一步networkidle0的语义:没有活跃网络请求)。若导航超时,src/tracker.js 的createTracker会记录「已发起但从未结束或失败」的请求 URL,并附加到错误信息中(augmentTimeoutError),让超时原因可诊断。
  8. 按需移除 webpack chunk script 标签。由fixWebpackChunksIssue驱动,实现见 index.js:针对 CRA v1/v2 与 Parcel 三种产物命名规则,把main.*.js之外的 chunk<script>替换为<link rel="preload">,让首屏只加载主 bundle。
  9. 按需移除 blob: URL 的样式(removeBlobs,index.js),因为blob:资源无法落盘,保留在静态 HTML 中会失效。
  10. 为 CSS-in-JS 重建<style>文本(fixInsertRule,index.js):对通过CSSStyleSheet.insertRule动态写入的规则,把style.sheet.rules回写到<style>的 innerHTML,否则序列化 HTML 时规则会丢失。
  11. 按需内联关键 CSS(inlineCss,详见下文专节)。
  12. 按需收集 HTTP/2 push manifest(http2PushManifest,index.js:结束时把每个路由预加载到的 js/css/image 资源写成http2-push-manifest.json)。
  13. 压缩 HTML 并写入磁盘。saveAsHtml(index.js)先调用html-minifier的minify,再落盘;若路由以.html结尾则按原路径保存,否则写入route/index.html。它还会做两处健康检查:404 页 title 未包含 "404" 字符串时告警、普通页 title 包含 "404" 时提示 page not found。
  14. 路由保存规则:.html后缀原样使用,其余路由保存为route/index.html。

除了流水线,还有两个值得注意的并发与容错设计:

  • 并发爬取:爬虫基于 Highland 流实现,queue.map(...).mergeWithLimit(options.concurrency)(src/puppeteer_utils.js)默认 4 个标签页并行(defaultOptions.concurrency: 4),可用concurrency选项调整。
  • 优雅退出:crawl注册了SIGINT处理器,第一次^C进入shuttingDown状态、停止处理新 URL,第二次才强制退出(src/puppeteer_utils.js)。

测试侧,tests/run.test.js 用 mock fs 验证了「index.html被复制为200.html」「/保存为同目录index.html」等契约,tests/snapshots/defaultOptions.test.js.snap 则用快照锁定了全部默认配置,可作为配置参考的权威来源。

配置系统:reactSnap与defaultOptions全解

在 package.json 中传参

如需传参,在package.json中添加reactSnap块即可:

"reactSnap": { "inlineCss": true }

run.js 的调用方式是run({ publicPath, fixWebpackChunksIssue, ...reactSnap })——用户配置展开在末尾,覆盖同名默认值。README 坦言「并非所有选项都已写文档,可查阅index.js中的defaultOptions」。下面这张表即整理自 index.js(与快照文件交叉核对一致):

选项默认值说明
port45678本地 Express 服务器监听端口
source"build"构建产物输入目录(CRA 默认)
destinationnull(回退为source)输出目录,默认写回源目录
concurrency4并行渲染的浏览器标签页数
include["/"]起始爬取路由,必须为非空数组
userAgent"ReactSnap"爬虫 UA,可用于应用代码做分支判断
headlesstrue无头模式
puppeteer{ cache: true }Puppeteer 选项(目前支持cache)
puppeteerArgs[]传给puppeteer.launch的 Chromium 参数
puppeteerExecutablePathundefined自定义 Chromium 可执行文件路径(Docker/Alpine 常用)
puppeteerIgnoreHTTPSErrorsfalse忽略 HTTPS 错误
publicPath"/"应用部署的公共路径前缀
minifyCss{}传给 clean-css 的选项(inlineCss时用于压缩全量 CSS)
minifyHtml见下传给 html-minifier 的选项
viewport{ width: 480, height: 850 }移动端优先的视口尺寸
sourceMapstrue报错时把堆栈映射回源码
fixWebpackChunksIssue"CRA1"可选CRA1/CRA2/Parcel/false
removeBlobstrue移除 blob: 样式的<link>
fixInsertRuletrue重建 insertRule 写入的<style>文本
skipThirdPartyRequestsfalse拦截所有第三方域名请求
cacheAjaxRequestsfalse抓取并内联 JSON AJAX 响应
http2PushManifestfalse生成http2-push-manifest.json
ignoreForPreload["service-worker.js"]push manifest 中忽略的文件名
preconnectThirdPartytrue为第三方域名注入<link rel="preconnect">
inlineCssfalse关键 CSS 内联(实验性)
saveAs"html"可选"png"/"jpeg",输出截图
crawltrue是否跟随页面内链接
waitForfalse额外等待的选择器
externalServerfalse使用外部服务器而非内置 Express
removeStyleTagsfalse保存前移除全部<style>
preloadImagesfalse为图片注入<link rel="preload">
asyncScriptTagsfalse给<script src>加async
removeScriptTagsfalse保存前移除全部<script>(Server Side Only 场景)

minifyHtml的默认值(index.js):

minifyHtml: { collapseBooleanAttributes: true, collapseWhitespace: true, decodeEntities: true, keepClosingSlash: true, sortAttributes: true, sortClassName: false }

注意sortClassName自1.17.0起默认即为false(原因见下文 Semantic UI 小节)。

配置校验、弃用迁移与归一化

defaults() 函数集中处理了兼容与校验逻辑,直接调用 API(而非 CLI)时需要特别注意:

  • include为空数组或非数组 → 打印include option should be an non-empty array并直接抛错;
  • preloadResources已弃用 → 提示改用preloadImages或cacheAjaxRequests,抛错终止;
  • minifyOptions已更名为minifyHtml、asyncJs已更名为asyncScriptTags,旧名会自动迁移并提示;
  • fixWebpackChunksIssue: true不再合法 → 回退为"CRA1",合法取值提示为CRA1, CRA2, Parcel, false;
  • saveAs仅接受html、png、jpeg,否则抛错(index.js);
  • publicPath自动补前导/并去除尾部/;include的每个路由会被自动加上publicPath前缀(index.js)。

构建工具的自动探测

CLI 入口 run.js 还做了两件「零配置」魔法,直接调用run()时你需要自行补齐:

  1. publicPath:读取当前目录package.json的homepage字段或环境变量PUBLIC_URL,取其 pathname 作为publicPath;
  2. fixWebpackChunksIssue:解析react-scripts的版本号——1.x 映射为"CRA1",2.x 映射为"CRA2";若检测到parcel-bundler依赖则设为"Parcel"。三个修复函数依据各自的文件名正则识别主 bundle(如 CRA v2 的main\.[\w]{8}\.chunk\.js),把按需 chunk 的<script>降级为<link rel="preload" as="script">,避免首屏加载用户根本不会访问的页面 chunk。

inlineCss:关键 CSS 内联(实验性功能)

README 对inlineCss的说明是:

Experimental feature - requires improvements.react-snapcan inline critical CSS with the help of minimalcss and full CSS will be loaded in a non-blocking manner with the help of loadCss. UseinlineCss: trueto enable this feature.

并标注 TODO:等该功能稳定后应默认开启。源码层面(index.js)实际实现了两种自动选择的策略:

  1. 用 minimalcss 计算该页面真正用到的关键 CSS(minimalcss.minimize,支持按skipThirdPartyRequests跳过第三方请求);
  2. 再用页面内fetch拉取全部<link rel="stylesheet">文本,经 clean-css 压缩得到全量 CSS;
  3. 若关键CSS体积 × 2 >= 全量CSS体积,说明省不出什么,走inline 策略:把全量 CSS 直接内联进<style>并删除原<link>;否则走critical 策略:内联关键 CSS,原<link>改为rel="preload" as="style"加react-snap-onload="this.rel='stylesheet'"(保存 HTML 时react-snap-onload会被还原为onload,见 index.js),非阻塞加载完整样式;无 JS 环境的<noscript>中保留原样式表;
  4. 内联体积超过20KB时打印警告(twentyKb = 20 * 1024,index.js);
  5. critical 策略还会注入随包发布的 preload polyfill(index.js 从vendor/preload_polyfill.min.js读取,对应 npm 包 fg-loadcss)。

生产环境注意事项(Caveats)

这一节是 README 中信息密度最高的部分,逐条完整保留并补充源码依据。

异步组件(Async components / Code splitting)

异步组件(又称 code splitting、dynamic import、chunks、layers、rollups、fragments)指用动态import按需加载组件,常见方案有react.lazy、loadable-components、react-loadable、react-async-component等。

用 react-snap 渲染异步组件本身没有问题,棘手的时机在预渲染后的应用启动时:异步 chunk 尚未加载,React 先画出组件的「loading」态,chunk 到达后再画出真实组件——用户会看到一次闪烁(先 100% 进度瞬间回落到 loading 再爬升)。SSR 场景下 code splitting 库通常提供状态 API 处理该问题,但 react-snap 并非「真正的 SSR」,因此没有简单解法。README 给出三条方案:

方案 1:react-prerendered-component(推荐)。该库会「抱住」预渲染的 HTML,直到动态导入的代码就绪:

import loadable from "@loadable/component"; import { PrerenderedComponent } from "react-prerendered-component"; const prerenderedLoadable = dynamicImport => { const LoadableComponent = loadable(dynamicImport); return React.memo(props => ( // you can use the `.preload()` method from react-loadable or react-imported-component` <PrerenderedComponent live={LoadableComponent.load()}> <LoadableComponent {...props} /> </PrerenderedComponent> )); }; const MyComponent = prerenderedLoadable(() => import("./MyComponent"));

MyComponent会利用PrerenderedComponent生成的id属性定位到预渲染 HTML 的对应片段,并通过dangerouslySetInnerHTML注入,从而避免内容闪烁。

方案 2:React.lazy+ 自制 prefetch。React.lazy不提供load/preload方法,需要自行实现(作者注明这可能是脆弱方案):

const prefetchMap = new WeakMap(); const prefetchLazy = LazyComponent => { if (!prefetchMap.has(LazyComponent)) { prefetchMap.set(LazyComponent, LazyComponent._ctor()); } return prefetchMap.get(LazyComponent); }; const prerenderedLazy = dynamicImport => { const LazyComponent = React.lazy(dynamicImport); return React.memo(props => ( <PrerenderedComponent live={prefetchLazy(LazyComponent)}> <LazyComponent {...props} /> </PrerenderedComponent> )); }; const MyComponent = prerenderedLazy(() => import("./MyComponent"));

方案 3:loadable-components2.2.3 旧版(当前版本已 >5)配合window.snapSaveState:

import { loadComponents, getState } from "loadable-components"; window.snapSaveState = () => getState(); loadComponents() .then(() => hydrate(AppWithRouter, rootElement)) .catch(() => render(AppWithRouter, rootElement));

若未使用 babel 插件,别忘了显式提供 modules(参考 loadable-components issue #114):

const NotFoundPage = loadable(() => import("src/pages/NotFoundPage"), { modules: ["NotFoundPage"] });

loadable-components已被标记弃用、由@loadable/component取代,但后者移除了getState。因此想用这条路线只能停留在旧版(2.2.3),或等待 React 用异步渲染 +React.lazy原生的方式处理此场景。

从源码看,方案 3 之所以成立,是因为 index.js 在保存 HTML 前会调用window.snapSaveState(),将返回对象的每个 key 序列化为window["key"]=...;注入到页面第一个<script>之前,并对 HTML 危险字符做 Unicode 转义(作者注明:目前仅防 XSS、仅能安全序列化基础数据类型)。

Redux

参考 Redux 官方「Server Rendering」文档的 client-side 章节,与 react-snap 的结合方式:

// Grab the state from a global variable injected into the server-generated HTML const preloadedState = window.__PRELOADED_STATE__; // Allow the passed state to be garbage-collected delete window.__PRELOADED_STATE__; // Create Redux store with initial state const store = createStore(counterApp, preloadedState || initialState); // Tell react-snap how to save Redux state window.snapSaveState = () => ({ __PRELOADED_STATE__: store.getState() });

注意:目前仅支持基础「JSON」数据类型——Date、Set、Map、NaN等不会被正确处理(issue #54)。这与 index.js 中的注释一致:snapStringify本质是snapEscape(JSON.stringify(obj)),只做了字符转义。

第三方请求:Google Analytics、Mapbox 等

可用如下配置拦截所有第三方请求:

"skipThirdPartyRequests": true

实现见 src/puppeteer_utils.js:开启请求拦截后,URL 以basePath(http://localhost:<port>)开头的继续放行,其余一律request.abort()。这能显著加速爬虫,并避免统计脚本在预渲染期间产生污染数据。

AJAX

react-snap可以捕获所有 AJAX 请求:json类型的响应会被存入window.snapStore[<path>],其中<path>是请求路径。启用方式:

"cacheAjaxRequests": true

源码链路:preloadResources(index.js)在监听response事件时把content-type含json的响应体存入ajaxCache[route];保存 HTML 时序列化为window.snapStore={...};(index.js)。这样客户端首屏即可同步读到爬虫阶段缓存的数据,无需重新发请求。

注意:该功能可能与浏览器缓存冲突(issue #197),此时可关闭 Puppeteer 缓存:

"puppeteer": { "cache": false }

对应 src/puppeteer_utils.js 的page.setCacheEnabled(options.puppeteer.cache)。

Service Workers

create-react-app 默认以index.html作为 SW fallback:

navigateFallback: publicUrl + '/index.html',

必须把它改为未预渲染版本的index.html——即200.html(这正是流水线第 1 步复制出来的那份),否则在其它子页面会看到首页index.html一闪而过。具体做法(不 eject 的情况下配置 sw-precache)见 doc/recipes.md 的 "Configure sw-precache without ejecting" 小节。

容器与其它受限环境

Puppeteer(Headless Chrome)可能因 sandbox 问题失败。通用解法:

"puppeteerArgs": ["--no-sandbox", "--disable-setuid-sandbox"]

仓库自身的集成测试也使用了同样的参数(tests/run.test.js,注释标明是「for Travis CI」)。更多排查思路见 Puppeteer 官方 troubleshooting 文档。另注意"inlineCss": true在容器中偶发问题。

Docker + Alpine:在 Alpine 版 Docker 内运行react-snap时,可能需要自定义 Chromium 可执行文件(即puppeteerExecutablePath选项),参见 issue #93 与 #132 的讨论。

Heroku:

heroku buildpacks:add https://github.com/jontewks/puppeteer-heroku-buildpack.git heroku buildpacks:add heroku/nodejs heroku buildpacks:add https://github.com/heroku/heroku-buildpack-static.git

参考 an-almost-static-stack 项目的 PR #7。README 同时指出:撰写本文时 Heroku 尚不支持 HTTP/2。

Semantic UI

Semantic UI 基于含空格的类名子串定义样式(如three column)。对类名排序会破坏这种约定,因此使用时应配置:

"minifyHtml": { "sortClassName": false }

自1.17.0起sortClassName的默认值已是false(见 index.js 的默认选项),新版本可直接省略。

JSS(CSS-in-JS)

Once JS on the client is loaded, components initialized and your JSS styles are regenerated, it's a good time to remove server-side generated style tag in order to avoid side-effects

这基本意味着JSS 不支持 rehydration:客户端重新生成的样式会与预渲染时注入的<style>冲突,需要按 JSS 官方 SSR 文档的思路在客户端就绪后移除服务端 style tag。可能的解法讨论见 issue #99。注意这与fixInsertRule互补:后者只解决「规则序列化丢失」,不解决「双份样式」问题。

react-router v3

见 issue #135(v3 的路由实现与 history 爬取不兼容)。

用 userAgent 区分爬虫环境

应用代码中可以用navigator.userAgent == "ReactSnap"做分支判断(默认 UA 即ReactSnap,爬虫通过page.setUserAgent(options.userAgent)注入,见 src/puppeteer_utils.js)——典型场景是:爬虫阶段 AJAX 必须请求具体 host,而线上用户走相对路径。例如 API 基地址:

const BASE_URL = process.env.NODE_ENV == "production" && navigator.userAgent != "ReactSnap" ? "/" : "http://xxx.yy/rest-api";

截图模式与进阶玩法

defaultOptions中的saveAs支持"png"/"jpeg":此时 saveAsPng / saveAsJpeg 会调用page.screenshot,按「.html后缀同名换扩展 / 根路由保存为index.png/ 其余路由保存为<route>.png」的规则落盘,配合crawl: true可以批量生成整站截图。README 的 Examples 中链接的 doc/recipes.md 还收录了 "Use to render screenshots" 等更多实操配方(sw-precache 配置、GA 集成、S3 + Cloudflare 部署等)。

案例、替代方案与生态

README 指出的相关案例文档(均以仓库内相对路径给出):

  • Emotion 官网加载性能优化案例
  • an-almost-static-stack 加载性能优化案例
  • recipes 配方集
  • 外部演示项目 an-almost-static-stack(stereobooster/an-almost-static-stack)

同类方案横向对比见 doc/alternatives.md。

谁在使用:README 的 "Who uses it" 一节列出了 cloud.gov.au、blacklane、reformma 三个站点(对应 doc/who-uses-it/ 下的站点标识图)。

贡献:README 的 Contributing 部分提出,报 bug 请提供可复现的 demo 与步骤;此外minimalcss(inlineCss的关键 CSS 提取引擎,package.json 中依赖版本0.8.2)本身是 react-snap 的组成部分,也欢迎参与。

小结

react-snap 把「SPA 静态预渲染」压缩成一条postbuild命令:真实浏览器爬取 + 14 步 DOM 后处理 + 零配置默认值,构成了它对 SEO/SMO 问题的最小实现;而defaultOptions中三十余项配置、window.snapSaveState/window.snapStore两个全局钩子、以及 README 罗列的异步组件闪烁、Redux 序列化限制、SW fallback、容器 sandbox 等注意事项,则是把它用到生产时真正需要掌握的边界。以上全部内容均可在当前仓库中逐条对照源码验证:入口 index.js、CLI run.js、爬虫核心 src/puppeteer_utils.js、超时诊断 src/tracker.js,以及 tests/run.test.js 与 tests/snapshots/defaultOptions.test.js.snap 中的行为契约。

【免费下载链接】react-snap

👻 Zero-configuration framework-agnostic static prerendering for SPAs

项目地址:https://gitcode.com/gh_mirrors/re/react-snap
点击查看免费下载
上一篇:Go-Koans控制流完全手册:if、for、switch的10个实用案例
下一篇:Simply主题评论系统集成:Disqus、Facebook与Hyvor Talk对比测评

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

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

用 LinuxKit 构建 Kubernetes 系统镜像并在 Mac 上部署最小集群

云原生教程 【免费下载链接】kubernetes-handbook Kubernetes Handbook &#xff08;Kubernetes指南&#xff09; https://kubernetes.feisky.xyz 项目地址&#xff1a; https://gitcode.com/gh_mirrors/kub/kubernetes-handbook 点击查看 免费下载 LinuxKit 是 Docker 生态中…

作者头像 李华
网站建设 2026/10/13 6:04:39

uni-app验证码输入框实现与优化实践

1. 项目背景与需求分析在移动端应用开发中&#xff0c;验证码输入是一个高频出现的交互场景。传统的单个输入框粘贴方式虽然简单&#xff0c;但存在安全隐患且不符合部分业务场景需求。而多输入框分段验证的方式既能提升安全性&#xff0c;又能给用户更直观的反馈。在uni-app框…

作者头像 李华