React Nextjs App Route Export 生成的静态页嵌入到指定html的解决方案

202 阅读4分钟

背景

客户网站由VUE开发,并已经上线,需要嵌入在线客服,支持实时消息提醒 文字图片交互功能。大概功能如下图所示:

解决思路:

因为React. 使用Nextjs 框架开发,会编译成静态HTML,所以将编译后的HTML直接嵌入到原系统即可。还有VUE和React 结合的第三方框架,开发和客户的接入成本较高,暂不考虑。

客户接入要简单并不能影响现有功能,而且需要和现有页面通信,需要嵌入到同一页,不能用iframe。最好是一键安装。假设以下页面为客户现有页面,最终安装效果如下:客户只需要在原网页中加入一句js 代码即可:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>动态加载示例</title>
    <meta charset="UTF-8">
    <script src="./imInstaller.js"></script>
</head>
<body>
<h1>点右下游客服图标!</h1>
<div id="root">
    <div id="__next">
        <div id="content-container">
        </div>
    </div>
</div>
</body>
</html>

具体方案步骤

  1. 首先用Nextjs将React 组件生成HTML,next.config.mjs 配置如下:HTML文件会生成至dist/pc 目录下
 /** @type { import("next").NextConfig } */
const nextConfig = {
  ...
    assetPrefix: process.env.NEXT_PUBLIC_ASSET_PREFIX,
    reactStrictMode: false,
    // 编译文件的输出目录
    distDir: "dist/pc",
    output: "export"
   ...
};
export default nextConfig;
export default function Page() {
  return (
    <div id="content-container">
      <MyComponent /> //任意组件,演示效果
    </div>
  );
}

直接生成后部署到nginx,nginx 的配置如下:

server {
    listen        80;
    server_name   www.sparrowzoo.com sparrowzoo.com upload.sparrowzoo.com
    charset       utf_8;

  location /react {
          try_files $uri $uri.html $uri/ =404;//自动访问.html结尾的文件 比如/a会自动访问至/a.html
                alias   /workspace/sparrow-js/react-next-14/dist/pc;
                expires      1h;
     }
}

imInstaller.js

function loadjQuery(callback) {
  if (window.jQuery) {
    callback();
    return;
  }
  const script = document.createElement("script");
  script.src = "https://code.jquery.com/jquery-3.7.1.min.js";
  script.onload = callback;
  script.onerror = function () {
    console.error("加载失败,请检查网络或 CDN 状态");
  };
  document.head.appendChild(script);
}

// 加载 jQuery 并初始化功能
loadjQuery(function () {
  var root = "./";
  var htmlUrl = root + "/12talk";

  function loadHTML(url, containerId) {
    const container = $(`#${containerId}`);
    container.css({ position: "absolute", right: "4rem", bottom: "4rem" });
    // 显示加载状态
    $.ajax({
      url: url,
      method: "GET",
      cache: false, // 禁用缓存
      dataType: "html",
    })
      .done(function (html) {
        html = html.replace("</body>", "");
        html = html.replace("</html>", "");
        container.append(html); // 外部样式不
        reloadAssets(container);
      })
      .fail(function (jqXHR, textStatus) {
        console.error("加载失败:", textStatus);
        container.html('<p class="error">内容加载失败,请刷新重试</p>');
      });
  }

  // 资源重载处理
  function reloadAssets(container) {
    //处理样式表;
    container.find('link[rel="stylesheet"]').each(function () {
      $(this).clone().appendTo("head");
    });
  }

  $(document).ready(() => {
    loadHTML(htmlUrl, "content-container");
  });
});

部署上线后发现问题:

IM可以正常访问,但将原页面效果直接覆盖,这是万万不可接受的

分析原因

  1. Nextjs 生成的HTML默认为完整的HTML结构,包括 标签

通过jquery

        container.append(html); 

之后会直接覆盖原页面。

进行如下调整,将React 页面生成页面中的html body 去掉,只保留待嵌入的html代码片断,需要将项目的路由改为Route Groups 详见nextjs.org/docs/app/bu…

调整后的project structure 如下

Im 下的layout 不会受root layout. 影响,即im 目录下只生成html代码片断,不会生成完整html 代码,chat 目录下会生成完整html不受该配置影响。

修改后im目录下的layout.tsx

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <>{children}</>
  );
}

注意:本地运行可能会报错,提示

Missing Required HTML Tag

The following tags are missing in the Root Layout: , . Read more at nextjs.org/docs/messag… 不用理会,部署线上可保证运行正常

经过以上修改之后依然会覆盖原页面内容,但以上修改依然有效。进一步分析

在不知道问题的时侯,最没有办法的办法 就是排除法:将MyComponet 中的代码删至最简

export default function Demo() {
  return (
    <div>
      <h1>Welcome to 12talk</h1>
      <p>This is a demo page for 12talk.</p>
    </div>
  );
}

依然会覆盖,说明不是自定义组件的问题,也不是jquery加载的问题,可能是nextjs 生成机制html的机制,会默认将生成的html片断嵌到html. body下的div 下。这是nextjs 和react 的生成机制导致的,所以不管原页长什么样。最终都会变成

<html>
<body>
<div id="__next">
你的内容 
</div>
</body>
</html

所以原页面内容被覆盖。

知道原因后做如下调整

"use client";
import React, {useEffect} from "react";
import dynamic from "next/dynamic";
import {createRoot} from "react-dom/client";

const JQueryComponent = dynamic(() => import("./back"), {
  ssr: false, // 仅客户端渲染
});
export default function Page() {
  
  useEffect(() => {
    // 手动挂载到指定容器
    //修改默认挂载方式,挂载到指定的html标签上,不会覆盖原内容
    const container = document.getElementById("content-container");
    if (container) {
      const root = createRoot(container);
      root.render(<JQueryComponent />);
    }
  }, []);
  return null; // 返回空值,避免自动生成默认 div,
  //注意这句话,next js 如果发现有内容就会默认加到body下相当于document.body.innerHTML="<div>你的内容</div>"
  //这里返回null非常 重要
}

将生成好的html 片断部署至nginx.并将public index.html一并部署(为了演示使用,假设为客户现存页面)

并按以上imInstall.js 安装代码执行即可。

至此完美解决

最终总结

  1. IM侧实现较为复杂,需要了解react 和nextjs 的核心机制,可能遇到一些线上线下的部署的坑,但理论上是讲得通的
  2. 对于客户端使用非常简单,只需要一行代码即可安装!
<!DOCTYPE html>
<html lang="en">
<head>
    <title>动态加载示例</title>
    <meta charset="UTF-8">
    <script src="./imInstaller.js"></script>
</head>
<body>
<h1>点右下游客服图标!</h1>
<div id="root">
    <div id="__next">
        <div id="content-container">
        </div>
    </div>
</div>
</body>
</html>