react 使用web component导出静态html报告

1、安装esbuild插件

powershell 复制代码
npm i --save-dev esbuild
# 或
yarn add -D esbuild

2、编写client.tsx,生成web component入口文件

typescript 复制代码
import React from "react";
import { hydrateRoot } from "react-dom/client";
import { XXXXExportApp } from "./XXXXExportApp";
import type { XXXXExportData } from "../XXXX";

declare global {
  interface Window {
    __XXXX_INITIAL_DATA__?: XXXXExportData;
  }
}

const root = document.getElementById("XXXX-root");
const initialData = window.__XXXX_INITIAL_DATA__;

if (root && initialData) {
  hydrateRoot(root, <XXXXExportApp data={initialData} />);
}

3、编写XXXXExportApp.tsx,该文件为导入内容文件,尽量导入原本页面组件,避免维护两套代码

其中 PageContent 使用和页面内容共用,有些代码需要处理,避免导出报错

typescript 复制代码
import React from "react";
import type { XXXXExportData } from "../XXXX";
import PageContent from "../PageContent/PageContent";

interface Props {
  data: XXXXExportData;
}

export const XXXXExportApp: React.FC<Props> = ({ data }) => {
  
  return (
    <div className="page">
      <div className="muted" style={{ marginBottom: 12 }}>
        <h1>{data.title}</h1>
        {data.exportTimeLabel}: {data.generatedAt}
      </div>
      <PageContent {...data.pageContent} />
    </div>
  );
};

4、编写esbuild配置脚本,用于导出web component

将页面组件打包成 iife 格式的 web component

typescript 复制代码
import { build } from "esbuild";
import { lessLoader } from 'esbuild-plugin-less';
import path from "node:path";
import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const entry = path.resolve(__dirname, "xxxx/export/client.tsx");
const outfile = path.resolve(__dirname, "xxxx.js");

await build({
  entryPoints: [entry],
  outfile,
  bundle: true,
  format: "iife",
  platform: "browser",
  target: ["es2019"],
  minify: true,
  jsx: "automatic",
  sourcemap: false,
  // 关键:不要 external react/react-dom/echarts
  external: [],
  plugins: [
    lessLoader(), // 直接启用插件,less解析插件
  ],
  loader: { // 图片转base64配置
    '.png': 'dataurl',   // 将所有 .png 导入转为 Base64
    '.jpg': 'dataurl',
    '.svg': 'dataurl',
  },

  define: {
    "process.env.NODE_ENV": '"production"',
    "process.env.__NEXT_IMAGE_OPTS": '"null"',
    "process.env": "{}",
    "process": "{}",
  },

  // 可选:让全局 this/window 语义更稳
  globalName: "XXXXExportClient",
});

console.log(outfile);

5、在package.json 添加构建脚本,执行构建命令

javascript 复制代码
{
  ...
  "scripts": {
    ...
    "build:xx-client": "node scripts/xxxx-export-client.mjs"
  },
  ...
}
powershell 复制代码
npm run build:xx-client
# 或
yarn build:xx-client

6、编写导出函数

typescript 复制代码
import React from "react";
import { renderToString } from "react-dom/server";
import { XXXXExportApp } from "./export/XXXXExportApp";
import { PageContentProps } from "./PageContent/PageContent";

export interface XXXXExportData {
  locale: "zh" | "en";
  title: string;
  exportTimeLabel: string;
  generatedAt: string;
  messages: Record<string, string>;
  pageContent: PageContentProps;
}

const serializeSafe = (value: unknown): string => {
  // 防止 </script> 提前闭合 + 常见 XSS 字符
  return JSON.stringify(value)
    .replace(/</g, "\\u003c")
    .replace(/>/g, "\\u003e")
    .replace(/&/g, "\\u0026")
    .replace(/\u2028/g, "\\u2028")
    .replace(/\u2029/g, "\\u2029");
};

export const exportXXXXHtml = async (data: XXXXExportData): Promise<string> => {
  async function getInlineClientJsFromRemote(url: string): Promise<string> {
    const res = await fetch(url, { method: "GET" });
    if (!res.ok) {
      throw new Error(`Fetch client js failed: ${res.status} ${res.statusText}`);
    }

    const jsText = await res.text();

    // 防止内联 script 被意外闭合
    return jsText.replace(/<\/script>/gi, "<\\/script>");
  };

  async function getInlineClientCssFromRemote(url: string): Promise<string> {
    const res = await fetch(url, { method: "GET" });
    if (!res.ok) throw new Error(`load css failed: ${res.status}`);

    const cssText = await res.text();

    const globalCssText = Array.from(document.styleSheets).map((s) => {
      try {
        return Array.from((s as CSSStyleSheet).cssRules).map((r) => r.cssText).join("\n");
      } catch {
        return ""; // 跨域样式表会抛错
      }
    }).join("\n");

    return cssText.replace(/<\/style>/gi, "<\\/style>") + globalCssText;
  }

  // 获取页面脚本及页面/全局样式
  const inlineClientJs = await getInlineClientJsFromRemote(`${window.location.origin}/xxxx-export-client.js`);
  const inlineClientCss = await getInlineClientCssFromRemote(`${window.location.origin}/xxxx-export-client.css`);

  const appHtml = renderToString(React.createElement(MemoryAnalysisExportApp, { data,  }));

  const initialData = serializeSafe(data);

  // 有些ui组件需要导入cdn插件才能上线功能保留
  return `<!doctype html>
  <html lang="${data.locale}" data-theme="dark" theme-mode="dark" style="color-scheme: dark;">
    <head>
      <meta charset="UTF-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1" />
      <title>${data.title}</title>
      <script src="https://cdn.jsdelivr.net/npm/tdesign-react@1.15.8/dist/tdesign.min.js"></script>
      <style>
        .muted { color: #ffffffd9; margin-bottom: 12px; margin: 12px; }
        .muted h1 { margin-bottom: 8px; font-size: 24px; }
      </style>
      <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tdesign-react@1.15.8/dist/tdesign.min.css">
      <style>${inlineClientCss}</style>
    </head>
    <body>
      <div id="memory-analysis-root">${appHtml}</div>
      <script>window.__XXXX_INITIAL_DATA__ = ${initialData};</script>
      <!-- 这里替换成你真实构建产物 -->
      <script type="module">${inlineClientJs}</script>
    </body>
  </html>`;
};
相关推荐
weixin_457763081 小时前
展示youtube的视频
前端·javascript·html
雨翼轻尘1 小时前
03_HTML进阶标签与CSS入门
前端·css·html·入门·进阶标签
云水一下1 小时前
Vue.js从零到精通系列(六):组合式函数与逻辑复用——打造自己的 Hooks 工具箱
前端·javascript·vue.js
IT_陈寒1 小时前
Java的ArrayList扩容把我坑惨了,原来是这样搞的
前端·人工智能·后端
snow@li1 小时前
Charles:软件能力深度解析 / 跨平台 HTTP/HTTPS 代理调试工具 / 客户端与互联网之间的中间人代理 / 拦截、查看、篡改所有网络流量
前端
UXbot1 小时前
移动端UI设计工具选型指南:iOS与Android设计标准支持对比
android·前端·低代码·ios·交互·团队开发·ui设计
程序员黑豆1 小时前
AI全栈开发 - Java:数据类型
java·前端
江华森1 小时前
Tomcat 10 实战部署指南:从零到生产级 Web 容器
java·前端·tomcat