为您的 Web 应用构建“可插入”小部件
GenAI LIVE! | 2025年6月4日
首先,什么是 Widget?Widget 指的是任何应用程序的缩略版本。你可以将任何应用程序制作成 Widget。
您可能在移动设备和电脑上见过它们,它们就像小型浮动窗口一样。例如,您有一个应用程序,其中包含您最喜欢的音乐应用程序的浮动小部件。这个小部件不仅可以为其他小部件腾出空间,还能让您访问完整应用程序的精简版本。
小部件基本上可以减少与应用程序交互的工作量。其中一个用例是“仅查看小部件”,其中所有“只读”数据显示在小部件上,而修改或写入操作则在应用程序上执行。这样,您可以为用户提供一个更易于使用的精简版本。
让我们创建一个简单的小部件应用,其中包含 3 个页面,分别用于创建、列出和更新操作。我们将使用以下内容
- React作为 UI 框架。
- 使用 Typescript作为编码语言。
- 用于设计的引导程序。
- Webpack用于配置和构建应用程序。
- 浏览器的本地存储用于数据存储。
首先,我们来创建一个 React 应用。本教程将使用这个模板代码。想了解这个模板是如何创建的,可以查看这里。
我们只需克隆此模板并修改小部件的代码即可。由于我们的小部件将在 iframe 中运行,因此无法使用 React-routers。因此,在本例中,我们将使用 switch case 条件渲染,根据页面变量来渲染组件。
克隆模板并安装软件包后,让我们开始为小部件创建一个初始化入口点。在 src 文件夹下创建一个名为Widget.ts的文件。此文件将包含设置和渲染 iframe 的所有配置。
所以,它基本上就是两件事的结合。你有一个普通的 React 应用,它将由 iframe 中的 Widget.ts 运行,并且可以插入到任何地方。由于我们知道无法在窗口和 iframe 之间直接传递 props,因此我们需要使用 postMessage 函数在 iframe 和窗口之间进行通信,并交换 props 或值。
所有这些一开始可能听起来很令人困惑,但只要我们一步一步来,就会变得更容易。
现在我们可以开始向widget.ts文件添加代码了。首先,我们将创建一个widget对象,用于在即将使用该widget的网页上进行配置和初始化。我们先来简单操作一下。
小部件.ts
const defaultStyles: any = {
'border': 'none',
'z-index': 2147483647,
'height': '650px',
'width': '350px',
'display': 'block !important',
'visibility': 'visible',
'background': 'none transparent',
'opacity': 1,
'pointer-events': 'auto',
'touch-action': 'auto',
'position': 'fixed',
'right': '20px',
'bottom': '20px',
}
interface IConfig {
readonly email: string;
}
interface IWidget {
config: IConfig | null;
iframe: HTMLIFrameElement | null;
init: (config: IConfig) => void;
setupListeners: () => void;
createIframe: () => void;
handleMessage: (event: MessageEvent) => void;
}
const Widget: IWidget = {
iframe: null,
config: null,
init: function(config: IConfig) {
this.config = config;
this.createIframe()
},
createIframe: function() {
this.iframe = document.createElement('iframe');
let styles = '';
for (let key in defaultStyles) { styles += key + ': ' + defaultStyles[key] + ';' }
this.iframe.setAttribute('style', styles)
this.iframe.src = 'http://localhost:9000';
this.iframe.referrerPolicy = 'origin';
document.body.appendChild(this.iframe);
this.setupListeners();
},
setupListeners: function() {
window.addEventListener('message', this.handleMessage.bind(this));
},
handleMessage: function(e) {
e.preventDefault();
if (!e.data || (typeof e.data !== 'string')) return;
let data = JSON.parse(e.data);
switch (data.action) {
case 'init': {
if (this.iframe) {
this.iframe.contentWindow.postMessage(JSON.stringify(this.config), '*');
}
break;
}
default:
break;
}
}
};
export default Widget;
init 函数将在 script 标签中使用,其余函数用于构建和设置小部件。handleMessage 函数将用于与 React 应用程序通信,以便在 iframe 和父级之间传递数据。因此,在这里我们只需获取使用小部件的网页中 script 标签传递的配置,并将其作为 config 变量传递给 React 应用。这里我们看到 iframe 的 src 是http://localhost:9000。这将是我们的 React 应用服务器。现在,为了将小部件加载到页面上,我们需要先以不同的方式配置 webpack 文件。
webpack.config.js
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const TerserWebpackPlugin = require('terser-webpack-plugin');
const isProd = process.env.NODE_ENV === 'production';
const config = {
mode: isProd ? 'production' : 'development',
entry: {
app: [
'webpack-dev-server/client?http://0.0.0.0:9000/',
'webpack/hot/only-dev-server',
'./src/index.tsx'
],
Widget: ['./src/widget.ts']
},
output: {
filename: '[name].js',
path: resolve(__dirname, 'dist'),
library: '[name]',
libraryTarget: 'umd',
libraryExport: 'default'
},
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'babel-loader',
exclude: /node_modules/,
},
{
test: /\.css?$/,
use: [
'style-loader',
{ loader: 'css-loader', options: { importLoaders: 1 } },
'postcss-loader'
]
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
hash: true,
filename: 'index.html',
inject: 'body',
excludeChunks: ['widget']
}),
],
};
if (isProd) {
config.optimization = {
minimizer: [new TerserWebpackPlugin(),],
};
} else {
config.devServer = {
port: 9000,
open: true,
hot: true,
compress: true,
stats: 'errors-only',
overlay: true,
};
}
module.exports = config;
我们将修改应用程序的入口,将 React 应用加载为app,将 Widget.ts 文件加载为Widget。并在 HTMLPlugin 中告诉 Webpack 将Widget从 Chunk 中排除。
现在我们可以设置服务器了。我们将运行:
npm run dev
现在,如果您访问http://localhost:9000/Widget.js,我们将在那里看到我们的Widget.ts编译代码。如果没有显示任何错误,那么一切就绪了。现在,我们已准备好转到 React App 的设置。
由于我们只需要在收到配置值时加载小部件,因此我们需要监听 postMessage。
索引.tsx
import React from 'react';
import { render } from 'react-dom';
import App from './App';
import { IConfig } from './config/interfaces';
import { Context } from './context/context';
import './stylesheets/index.css';
window.addEventListener('DOMContentLoaded', (event) => {
window.parent.postMessage(JSON.stringify({ action: 'init' }), '*');
window.removeEventListener('DOMContentLoaded', () => null);
});
window.addEventListener('message', (event) => {
event.preventDefault();
if (!event.data || (typeof event.data !== 'string')) return;
const config: IConfig = JSON.parse(event.data);
return render(
<Context.Provider value={JSON.stringify(config)}>
<App />
</Context.Provider>,
document.body
);
});
DOM 加载完成后,我们将向 iframe 发送一条带有init操作的消息,告知 iframe React 应用已加载到 DOM 中。iframe 检查Widget.ts中使用的 handleMessage 函数中的操作,并返回一条包含配置数据的消息。React 应用将监听此消息,并在配置存在的情况下调用 render 方法。这将确保小部件始终仅在配置存在后才加载。
现在我们的 React 应用程序已加载,我们将在 App.tsx 中创建条件路由。
应用程序.tsx
import React, { useContext, useState } from 'react';
import { IConfig } from './config/interfaces';
import { Context } from './context/context';
import Active from './components/Active';
import Completed from './components/Completed';
import NewTask from './components/NewTask';
const App: React.FC = (props) => {
const config: IConfig = JSON.parse(useContext(Context));
const [page, setPage] = useState<Number>(1);
const renderHeader = () => {
return (<h3 className="bg-dark p-3 m-0 text-white">Todo-List</h3>);
}
const renderLinks = () => {
return (<div className="nav row m-0 bg-light">
<a className="nav-link col-4 text-center" href="#" onClick={() => setPage(1)}>Active</a>
<a className="nav-link col-4 text-center" href="#" onClick={() => setPage(2)}>New</a>
<a className="nav-link col-4 text-center" href="#" onClick={() => setPage(3)}>Completed</a>
</div>)
}
const renderComponent = () => {
switch(page) {
case 1: return <Active config={config}/>
case 2: return <NewTask setPage={setPage}/>
case 3: return <Completed config={config}/>
default: return <Active config={config}/>
}
}
return (<div className="h-100 w-100 border rounded">
{renderHeader()}
{renderLinks()}
{renderComponent()}
</div>);
}
export default App;
这里我刚刚创建了一个简单的待办事项列表应用。完整代码请参考这里。当前页面是一个状态变量,每当点击链接时都会发生变化。各个页面的组件会根据 switch 语句加载。设置好所有页面后,我们将在 html 页面中调用 Widget 方法。
为了测试,我在 dist 文件夹中创建了一个名为index.html的文件,其中包含以下代码。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Webpack with React TS</title>
</head>
<body>
<script src="http://localhost:9000/Widget.js"></script>
<script>
const config = { email: 'sooraj@skcript.com' };
Widget.init(config);
</script>
</body>
</html>
设置完毕。只需使用以下命令运行此文件:
npm run start
然后打开http://localhost:5000。现在,我们已将构建的整个 React 应用渲染到 iframe 中,可以使用上述脚本将其插入任何网站。
这是作为小部件制作的 React App 的演示。
