☰
Build-Time Targeting 示例:基于自定义特征的静态变体生成与边缘渲染
2026/10/8 19:17:12 网站建设 项目流程
  • 低代码
  • 前端
  • 后端

【免费下载链接】plasmic

Visual builder for React. Build apps, websites, and content. Integrate with your codebase.

项目地址:https://gitcode.com/gh_mirrors/pl/plasmic
点击查看免费下载

本示例基于 Next.js Pages Router 与@plasmicapp/loader-nextjs,演示如何将 UTM 来源、浏览器类型等自定义特征(custom traits)编码进 URL 路径,在构建期(build time)生成全部可能的变体页面,并通过 Edge Middleware 将请求重写到对应的静态变体。文章会同时对照仓库中同类的 custom-targeting 示例与 loader-edge 源码,帮助你理解这一模式从路径编码到边缘重写的完整链路。


一、示例概览与目标效果

仓库中的 build-time-targeting 是官方提供的「构建期目标定位」参考工程。它与运行时动态渲染变体不同,采用构建期穷举 + 边缘重写策略:

  • 构建期:利用generateAllPathsWithTraits枚举出所有可能的特征组合路径,生成对应静态页面;
  • 请求期:Edge Middleware 读取请求特征(utm_source、browser),用getMiddlewareResponse计算出目标路径并重写,使不同特征的访问者落到不同静态变体上。

示例定义的命中规则非常简单:

  • browser=chrome
  • utm_source=google

验证方式:

  • 访问https://build-time-targeting.vercel.app/?utm_source=google,会展示utm_source = google的变体;
  • 使用不同浏览器访问,会展示browser = chrome的变体。

工程根目录结构如下:

build-time-targeting/ ├── plasmic-init.ts # 初始化 Loader 并注册自定义特征 ├── middleware.ts # Edge Middleware,解析特征并重写路径 ├── next.config.js # Next.js 配置 ├── package.json # 依赖与脚本 └── pages/ ├── [[...catchall]].tsx # 全量捕获页:静态路径生成 + SSR 变体提取 ├── plasmic-host.tsx # App Host 画布宿主页 └── api/hello.ts

阅读本示例时,可以对照同仓库的 custom-targeting(运行时动态变体)与 custom-targeting-codegen(代码生成模式)来理解不同变体方案的差异。


二、注册自定义特征:plasmic-init.ts

自定义特征(custom traits)是决定页面变体的输入维度,需要在 Loader 初始化时注册。打开 plasmic-init.ts:

import { initPlasmicLoader } from "@plasmicapp/loader-nextjs"; export const PLASMIC = initPlasmicLoader({ projects: [ { id: "qSU617xDVJeD8V18Bsr4AA", token: "9cZbD1xYkMbOkRInGNShESoR5f8hD5YU6HstGlYDEiFsZpVmxG4WsGPLTIZA9KDTS3jYSRjYAwrIZfxSdE6ow", }, ], // 默认使用项目最后一次发布(published)的版本; // 开发阶段可设为 true 使用未发布版本,但性能显著更慢 preview: false, });

接着注册两个自定义特征:

PLASMIC.registerTrait("browser", { type: "choice", label: "Browser", options: ["Chrome", "Safari", "Other"], }); PLASMIC.registerTrait("utm_source", { type: "text", label: "UTM Source", });

要点说明:

  • registerTrait由 Loader 提供,用于声明特征的类型与取值空间。type为"choice"时需给出options枚举;为"text"时则接受任意字符串;
  • 这里注册的browser与utm_source是自定义特征,与之相对的是 Plasmic 内置的pageUrl等特征(见 variation.ts 中getActiveVariation对pageUrl的注入);
  • 同一个特征会被三个环节使用:Edge Middleware 读取请求值、getActiveVariation在 SSR 时挑选变体、generateAllPathsWithTraits在构建期穷举路径。

对应地,页面需要提供 Plasmic 画布宿主。参见 pages/plasmic-host.tsx:

import { PLASMIC } from "@/plasmic-init"; import { PlasmicCanvasHost } from "@plasmicapp/loader-nextjs"; export default function PlasmicHost() { return PLASMIC && <PlasmicCanvasHost />; }

该页面用于在 Plasmic Studio 中将本项目设置为 App Host,从而在设计器中实时预览代码组件与变体。


三、请求期特征解析与路径重写:middleware.ts

Edge Middleware 是本模式的核心枢纽:它负责读取请求特征 → 计算目标路径 → 重写响应。

import { getMiddlewareResponse } from "@plasmicapp/loader-nextjs/edge"; import { NextRequest, NextResponse, userAgent } from "next/server"; // 排除确定不是 Plasmic 变体页面的路径 export const config = { matcher: ["/:path((?!_next/|api/|favicon\\.ico|plasmic-host).*)"], }; export async function middleware(req: NextRequest) { // 只为 GET 请求挑选动态变体 if (req.method !== "GET") { return; } // Next.js 基于 ua-parser-js 解析 User-Agent const ua = userAgent(req); const browser = ua.browser.name?.includes("Chrome") ? "Chrome" : ua.browser.name?.includes("Safari") ? "Safari" : "Other"; const newUrl = req.nextUrl.clone(); const PLASMIC_SEED = req.cookies.get("plasmic_seed"); // 将请求重写到编码了自定义特征(以及 A/B 测试随机种子)的新路径 const { pathname, cookies } = getMiddlewareResponse({ path: newUrl.pathname, traits: { // 为正在使用的自定义特征提供取值;通常来自 cookie 或查询参数 ...(req.nextUrl.searchParams.get("utm_source") ? { utm_source: req.nextUrl.searchParams.get("utm_source") ?? "" } : {}), browser, }, cookies: { ...(PLASMIC_SEED ? { plasmic_seed: PLASMIC_SEED.value } : {}), }, }); // 用新 pathname 重写响应 newUrl.pathname = pathname; const res = NextResponse.rewrite(newUrl); // 保存需要写入 cookie 的内容——即随机种子对应的自定义特征。 // 每次访问使用同一个随机种子挑选 A/B 测试桶, // 确保同一访问者始终看到同一个 A/B 测试桶。 cookies.forEach((cookie) => { res.cookies.set(cookie.key, cookie.value); }); return res; }

关键点拆解:

1. matcher 排除非变体路径。_next/(静态资源)、api/(API 路由)、favicon.ico、plasmic-host(画布宿主)都不会经过本中间件,避免不必要的重写开销。

2. 特征读取策略的差异。browser由userAgent(req)解析而来(归类为Chrome/Safari/Other);utm_source则从查询参数读取。值得对比的是仓库中 custom-targeting 的写法:它直接无条件读取utm_source(req.nextUrl.searchParams.get("utm_source") ?? ""),而本示例用展开运算符做条件判断,无该参数时不携带该特征。两种写法的取舍在于:缺省时是回退到默认变体,还是作为特征参与变体匹配。

3.getMiddlewareResponse的作用。它返回{ pathname, cookies }:

  • pathname是把特征按固定格式编码进 URL 的结果;
  • cookies是在访问者尚无plasmic_seedcookie 时,新生成的种子(用于 A/B 测试分桶)。

从 loader-edge 源码 可以确认其内部逻辑:

export const getMiddlewareResponse = (opts: { path: string; traits: Traits; cookies: Record<string, string>; seedRange?: number; }) => { const newCookies: { key: string; value: string }[] = []; const seedRange = Number.isInteger(opts.seedRange) ? opts.seedRange : DEFAULT_PLASMIC_SEED_RANGE; // 默认 16 const seed = opts.cookies[PLASMIC_SEED] || getSeed(seedRange); let traits = opts.traits; if (seedRange && seedRange > 0) { traits = { ...traits, [PLASMIC_SEED]: seed }; if (!opts.cookies[PLASMIC_SEED]) { newCookies.push({ key: PLASMIC_SEED, value: seed }); } } return { pathname: rewriteWithTraits(opts.path, traits), cookies: newCookies, }; };

也就是说:如果访问者 cookie 中已有plasmic_seed,则复用;否则随机生成 0~15 之间的种子并写入 cookie。这保证了同一访问者跨多次请求始终落在同一个 A/B 测试桶。

4. 特征编码格式。rewriteWithTraits把特征编码为__pm__key=value形式拼在路径尾部(见 variation.ts):

export const rewriteWithTraits = (path: string, traits: Traits) => { if (Object.keys(traits).length === 0) { return path; } return `${path}${path.endsWith("/") ? "" : "/"}${expandTraits(traits)}`; };

而expandTraits会把多个特征按键名排序后逐一拼接。对应的解码逻辑是rewriteWithoutTraits(variation.ts),在服务端解析路径时把特征剥离出来。


四、构建期路径生成与变体提取:pages/[[...catchall]].tsx

全量捕获页是变体方案的消费端,它承担三个职责:构建期生成全部变体路径、SSR 时解析当前变体、渲染对应页面。

import { ComponentRenderData, extractPlasmicQueryData, PlasmicComponent, PlasmicRootProvider, } from "@plasmicapp/loader-nextjs"; import type { GetStaticPaths, GetStaticProps } from "next"; import { PLASMIC } from "@/plasmic-init"; import { generateAllPathsWithTraits, getActiveVariation, rewriteWithoutTraits, } from "@plasmicapp/loader-nextjs/edge";

4.1 构建期:getStaticPaths穷举所有变体路径

export const getStaticPaths: GetStaticPaths = async () => { const pageModules = await PLASMIC.fetchPages(); function* gen() { for (const page of pageModules) { // 为当前页面生成包含全部变体的所有可能路径 const allPaths = generateAllPathsWithTraits(page.path, { browser: ["Chrome", "Safari", "Other"], utm_source: ["google", "facebook"], }); for (const path of allPaths) { yield { params: { catchall: path.substring(1).split("/"), }, }; } } } return { paths: Array.from(gen()), fallback: false, }; };

注意generateAllPathsWithTraits的第二参数传入了每个特征的全部取值:

  • browser:Chrome、Safari、Other(与 plasmic-init.ts 中注册的options一致);
  • utm_source:google、facebook。

从 loader-edge 源码 看其穷举逻辑:默认会生成 16 个随机种子(0~15)对应的plasmic_seed组合,再与各特征的取值做笛卡尔积,最终对每种组合调用rewriteWithTraits编码成路径。因此路径总数 = 特征组合数 × 16(种子数)。

fallback: false表示只允许已枚举的路径命中该页面,这保证了构建产物完全可预测,但也意味着所有特征取值必须在构建期已知——这正是「构建期目标定位」与运行时方案的根本区别。

4.2 SSR 期:getStaticProps解析变体并预取数据

export const getStaticProps: GetStaticProps = async (context) => { const { catchall } = context.params ?? {}; const rawPlasmicPath = typeof catchall === "string" ? catchall : Array.isArray(catchall) ? `/${catchall.join("/")}` : "/"; // 解析路径并剥离出特征 const { path: plasmicPath, traits } = rewriteWithoutTraits(rawPlasmicPath); const plasmicData = await PLASMIC.maybeFetchComponentData(plasmicPath); if (!plasmicData) { // 非 Plasmic 捕获页 return { props: {} }; } // 获取当前页面的活跃变体 const variation = getActiveVariation({ splits: PLASMIC.getActiveSplits(), traits, path: plasmicPath, }); const pageMeta = plasmicData.entryCompMetas[0]; // 缓存页面所需的数据 const queryCache = await extractPlasmicQueryData( <PlasmicRootProvider loader={PLASMIC} prefetchedData={plasmicData} pageParams={pageMeta.params} variation={variation} > <PlasmicComponent component={pageMeta.displayName} /> </PlasmicRootProvider> ); // 如需增量静态再生成(ISR),可设置 revalidate return { props: { plasmicData, queryCache, variation }, revalidate: 60 }; };

流程拆解:

  1. rewriteWithoutTraits把路径中的__pm__特征段解析为{ path, traits }(对应 variation.ts);
  2. PLASMIC.maybeFetchComponentData(plasmicPath)拉取该路径对应的组件渲染数据;
  3. getActiveVariation结合当前 splits 与 traits 计算活跃变体。其底层实现(variation.ts)会注入pageUrl特征,并用plasmic_seed作为随机源,通过种子化随机函数保证分桶稳定;
  4. extractPlasmicQueryData在服务端预取并缓存页面查询数据,避免客户端重复请求;
  5. 返回revalidate: 60启用 ISR,让页面在 60 秒后按需重新生成。

4.3 渲染:变体透传

export default function PlasmicLoaderPage(props: { plasmicData?: ComponentRenderData; queryCache?: Record<string, any>; variation?: Record<string, string>; }) { const { plasmicData, queryCache, variation } = props; const router = useRouter(); if (!plasmicData || plasmicData.entryCompMetas.length === 0) { return <Error statusCode={404} />; } const pageMeta = plasmicData.entryCompMetas[0]; return ( <PlasmicRootProvider loader={PLASMIC} prefetchedData={plasmicData} prefetchedQueryData={queryCache} pageParams={pageMeta.params} pageQuery={router.query} variation={variation} > <PlasmicComponent component={pageMeta.displayName} /> </PlasmicRootProvider> ); }

variation通过PlasmicRootProvider下发给渲染树,Plasmic 会据此决定对每个 split 使用哪个变体分支,从而在客户端也保持与服务端一致的变体选择。


五、完整请求链路串讲

把三个环节串起来,一次访问的完整链路如下:

浏览器发起 GET 请求(携带 UTM 查询参数 / UA 头 / plasmic_seed cookie) │ ▼ Edge Middleware(middleware.ts) ├─ 解析 browser(userAgent)与 utm_source(查询参数) ├─ getMiddlewareResponse 计算目标路径:原路径 + __pm__ 特征段 ├─ 若无 plasmic_seed,随机生成种子并写入 Set-Cookie └─ NextResponse.rewrite 重写到该路径 │ ▼ Catch All 页面([[...catchall]].tsx) ├─ 构建期:getStaticPaths 已生成该特征组合对应的静态页面 ├─ getStaticProps:rewriteWithoutTraits 剥离特征 │ ├─ maybeFetchComponentData 获取渲染数据 │ ├─ getActiveVariation 依据 splits + traits 选出变体 │ └─ extractPlasmicQueryData 预取查询数据(ISR revalidate=60) └─ 渲染:PlasmicRootProvider 以 variation 驱动组件变体

由于重写发生在边缘网络、页面在构建期已静态化,因此变体判定几乎没有额外延迟,代价是特征取值必须在构建期预知并枚举进getStaticPaths。


六、变体方案选型参考

仓库中还有其他目标定位示例,可用于对比选型:

方案路径生成变体判定时机适用场景
build-time-targeting(本文)generateAllPathsWithTraits构建期穷举边缘 + SSR特征取值有限且预先可知,追求极致首屏性能
custom-targeting不枚举,运行时动态边缘重写特征取值无法穷举(如 UTM 任意值)
custom-targeting-codegen代码生成模式构建期/运行时使用 Plasmic Codegen 而非 Loader 的工程

若特征取值空间很大(例如utm_source可能是任意字符串),穷举所有组合会指数级膨胀路径数量,此时更适合 custom-targeting 的运行时方案;而本例取值受限(browser三选一、utm_source二选一),构建期穷举是高效且可预测的选择。


七、本地运行与验证

# 安装依赖并启动开发服务器 yarn install yarn dev

项目脚本见 package.json:dev(next dev)、build(next build)、start(next start)、lint(next lint)。依赖方面,核心是@plasmicapp/loader-nextjs(^1.0.333)与 Next.js 16.2.1。

本地验证变体效果的步骤:

  1. yarn dev启动后在浏览器访问根路径,默认看到无特征版本;
  2. 在 URL 后追加?utm_source=google,中间件会把utm_source=google编码进重写路径,页面展示utm_source = google变体;
  3. 在 DevTools 中切换 User-Agent 为 Chrome/Safari 或其他浏览器,观察browser特征对应的变体;
  4. 首次访问后检查 cookie,确认plasmic_seed已写入且保持不变,多次刷新应始终看到同一个 A/B 测试桶。

如需在 Plasmic Studio 中查看/克隆对应项目,其项目 ID 为qSU617xDVJeD8V18Bsr4AA(见 plasmic-init.ts),可在设计器中调整两个变体的内容后发布,前端重新构建即可生效。


八、小结

构建期目标定位示例展示了一条完整可落地的变体链路:

  • 特征声明:PLASMIC.registerTrait注册browser(choice 枚举)与utm_source(text 自由文本);
  • 边缘重写:getMiddlewareResponse将请求特征与随机种子编码进路径,并维护plasmic_seedcookie 保证分桶稳定;
  • 构建期穷举:generateAllPathsWithTraits结合已知特征取值与 16 个种子生成全部静态路径;
  • 服务端解析:rewriteWithoutTraits剥离特征、getActiveVariation依据 splits 与 traits 选出变体、extractPlasmicQueryData预取数据;
  • 渲染透传:variation经PlasmicRootProvider驱动组件分支。

这套模式适合特征取值有限、预先可知且对首屏性能敏感的场景;若特征取值不可穷举,可参考仓库中的 custom-targeting 运行时方案。两者的底层路径编码、种子分桶逻辑都统一实现在 packages/loader-edge/src/variation.ts,值得通读以理解 Plasmic 变体体系的完整设计。

  • 低代码
  • 前端
  • 后端

【免费下载链接】plasmic

Visual builder for React. Build apps, websites, and content. Integrate with your codebase.

项目地址:https://gitcode.com/gh_mirrors/pl/plasmic
点击查看免费下载

相关推荐

上一篇:Orca 渲染进程 Agent 状态高频路径的性能优化:从 9,279 个监听者到单次发布的事务化折叠
下一篇:终极MasterPortfolio安全指南:保护你的个人数据和GitHub信息的7个关键步骤

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询