Vite 工程源码子应用接入自定义页面示例
INFO
vite工程本地开发(dev / start)和最终运行(build)产物不同,只有最终运行(build)之后的产物能够以 es module 方式嵌入。
示例工程(github)
<bookmark name="GitHub - Lovrabet/sub-app-vite-demo: 在lovrabet体系下使用源码页面的例子(vite工程)" href="https://github.com/Lovrabet/sub-app-vite-demo/tree/main"></bookmark>
开发:执行
pnpm run start,可以完全以独立应用方式开发- 由于vite工程开发/构建方式不同,源码嵌入无法直接使用开发状态的产物,建议先以独立应用方式完成开发,再调试嵌入
构建:执行
pnpm run build,构建产物可同时用于独立使用和微应用嵌入(即“资源加载列表”)调试源码嵌入:执行
pnpm run build产出构建产物后,再执行pnpm run preview启动本地服务,即可直接访问本地的构建产物。将本地产物填入“资源加载列表”或使用代理工具,都可以实现调试
主要改造点说明
【重要】vite 构建配置修改以及平台生成接口的调用 代码示例
本地开发需要解决解决跨域问题:
TypeScript
本地浏览器 (https://dev.yuntooai.com:5173)
↓
直接发送fetch请求(带CORS headers)
↓
API服务器 (https://api.yuntooai.com)
↓
返回响应(允许跨域)
↓
浏览器接收数据具体配置如下:
JavaScript
import { defineConfig, loadEnv } from "vite";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import react from "@vitejs/plugin-react";
import htmlPlugin from "vite-plugin-index-html";
import pluginExternal from "vite-plugin-external";
import pkgJson from "./package.json";
const version = pkgJson.version;
const appName = pkgJson.name.split("/").pop();
const __dirname = dirname(fileURLToPath(import.meta.url));
// https://vitejs.dev/config/
export default defineConfig(async ({ mode }) => {
const env = loadEnv(mode, __dirname);
const PORT = Number(env.VITE_APP_PORT) || 5173;
const isCdn = Boolean(process.env.CDN_DOMAIN);
const outDir = isCdn ? `dist/${appName}/${version}` : "dist";
const base = isCdn ? `${process.env.CDN_DOMAIN}${outDir}/` : "/";
return {
base,
envDir: __dirname,
plugins: [
react(),
// 关键配置:提供 vite lib 打包 + html plugin 能力
htmlPlugin({
input: "src/main.tsx",
preserveEntrySignatures: "exports-only",
}),
pluginExternal({
externals: {
react: "React",
"react-dom": "ReactDOM",
antd: "antd",
dayjs: "dayjs",
},
}),
],
resolve: {
alias: {
"@": "/src",
},
},
// 可选配置:提供https自签名证书及跨域访问能力
// 因为接口域名为 api.yuntooai.com 存在跨域,服务端配置了允许 dev.yuntooai.com 的跨域请求,从而实现本地开发能够正常请求接口
// 这些配置不是必须的,你也可以使用 proxy 等任意手段自行处理跨域问题
server: {
port: PORT,
open: `https://dev.yuntooai.com:${PORT}`,
strictPort: true,
host: "dev.yuntooai.com",
https: await (await fetch("https://g.yuntooai.com/cert/dev.json")).json(),
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods":
"GET, POST, PUT, DELETE, PATCH, OPTIONS",
"Access-Control-Allow-Headers":
"X-Requested-With, Content-Type, Authorization",
},
},
build: {
outDir,
target: "esnext",
rollupOptions: {
output: {
format: "es",
entryFileNames: `assets/[name].js`,
assetFileNames: `assets/[name].css`,
},
},
},
optimizeDeps: {
include: ["react", "react-dom", "antd", "dayjs"],
},
};
});【重要】入口改造 代码示例
- 暴露
mountunmount,供微应用加载和销毁时使用 - 根据
isInIcestark()判断当前的运行环境,可同时兼容独立使用和嵌入使用
TypeScript
import React from "react";
import { createRoot } from "react-dom/client";
import { isInIcestark } from "@ice/stark-app";
import { ConfigProvider } from "antd";
import zhCN from "antd/locale/zh_CN";
import App from "./router";
import "./style.css";
// 可选:根据 isInIcestark() 判断当前的运行环境,可同时兼容独立使用和嵌入使用
if (!isInIcestark()) {
const container = document.getElementById("root");
if (container) {
const root = createRoot(container);
root.render(
<ConfigProvider locale={zhCN}>
<App />
</ConfigProvider>,
);
}
}
// 关键:暴露 mount 供主应用加载时调用
export function mount({
container,
customProps,
}: {
container: HTMLElement;
customProps: object;
}) {
const root = createRoot(container);
root.render(
<React.StrictMode>
<ConfigProvider locale={zhCN}>
<App {...customProps} />
</ConfigProvider>
</React.StrictMode>,
);
return root;
}
// 关键:暴露 unmount 供主应用卸载时调用
export function unmount({ container }: { container: HTMLElement }) {
// React 18 中不再需要手动卸载,但为了兼容性保留
const root = (container as any)._reactRoot;
if (root) {
root.unmount();
}
}【可选】前端路由适配 代码示例
- 通过
getBasename()获取到微应用运行时的basename并传入
JavaScript
const router = createBrowserRouter(
[
{
path: "/",
element: <MainLayout />,
children: [
{
index: true,
element: <Home />,
},
{
path: "about",
element: <About />,
},
{
path: "settings",
element: <Settings />,
},
],
},
],
{
basename: getBasename() || "/",
}
);【可选】布局适配 代码示例
- 根据
isInIcestark()判断当前运行环境,被嵌入时,不渲染layout布局
TypeScript
import React from "react";
import { isInIcestark } from "@ice/stark-app";
import { Outlet, useNavigate, useLocation } from "react-router";
import { Layout, Menu } from "antd";
const MainLayout: React.FC = () => {
// 可选:根据isInIcestark()判断当前运行环境,被嵌入时,不渲染layout布局
if (isInIcestark()) {
return (
<div style={{ padding: "16px 20px" }}>
<Outlet />
</div>
);
}
return (
<Layout>
.....
</Layout>
);
};
export default MainLayout;【可选】请求 lovrabet 平台已有接口
- 页面中使用 fetch 请求时,会自动带上 CORS headers,从而允许跨域访问
TypeScript
// 简单封装 apiRequest
const apiRequest = async (path, options = {}) => {
const response = await fetch(`https://api.yuntooai.com${path}`, {
credentials: 'include', // credentials: 'include' - 关键配置:跨域请求携带Cookie
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
});
return response.json();
};
// 页面中实际使用文生应用的接口
const data = await apiRequest('/smartapi/runtime/yuntoo/app-f4c03acb/9c1bfbd319174461b6b58d19d7bf040f/getList', {
method: 'POST',
body: {"pageSize":10,"currentPage":1}
});
// 页面中实际使用DB生成应用的接口
const data = await apiRequest('/dbapi/runtime/yuntoo/app-f4c03acb/b0a8936ffd2542499da5a165a09cc078/getList', {
method: 'POST',
body: {"pageSize":10,"currentPage":1}
});查看更多页面示例 DEMO
将 github 工程切换到 crm-demo 分支,即可查看 dashboard、客户360 等更丰富的示例
<bookmark name="GitHub - Lovrabet/sub-app-vite-demo at crm-demo" href="https://github.com/Lovrabet/sub-app-vite-demo/tree/crm-demo"></bookmark>
<grid> <column width-ratio="0.500000">
</column> <column width-ratio="0.500000">
</column> </grid>