为您的 Web 应用构建“可插入式”小部件 GenAI LIVE!| 2025 年 6 月 4 日

2025-06-10

为您的 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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

我们将修改应用程序的入口,将 React 应用加载为app,将 Widget.ts 文件加载为Widget。并在 HTMLPlugin 中告诉 Webpack 将Widget从 Chunk 中排除。

现在我们可以设置服务器了。我们将运行:

npm run dev
Enter fullscreen mode Exit fullscreen mode

现在,如果您访问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
 );
});
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

这里我刚刚创建了一个简单的待办事项列表应用。完整代码请参考这里。当前页面是一个状态变量,每当点击链接时都会发生变化。各个页面的组件会根据 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>
Enter fullscreen mode Exit fullscreen mode

设置完毕。只需使用以下命令运行此文件:

npm run start
Enter fullscreen mode Exit fullscreen mode

然后打开http://localhost:5000。现在,我们已将构建的整个 React 应用渲染到 iframe 中,可以使用上述脚本将其插入任何网站。

这是作为小部件制作的 React App 的演示。

替代文本

鏂囩珷鏉ユ簮锛�https://dev.to/soorajsnblaze333/build-a-pluggable-widget-for-your-web-app-3k2l
PREV
如何用 Javascript 创建一个简单的物理引擎 - 第一部分
NEXT
使用 React Query 异步获取数据⚛️