steedos-webapps

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Steedos Webapps | Steedos 软件包自定义 React 应用

Steedos Webapps | Steedos 软件包自定义 React 应用

Overview | 概述

概述

Steedos packages can contain React + Vite sub-projects in the
webapps/
directory. Each webapp is an independent Vite application that can be developed standalone and compiled as an IIFE script that self-registers as an amis Renderer component.
Steedos 软件包通过
webapps/
目录管理 React 子项目。每个子项目是独立的 Vite 应用,可独立开发调试,也可编译为 amis 自注册组件。
Steedos软件包可在
webapps/
目录中包含React + Vite子项目。每个Web应用都是独立的Vite应用,可独立开发并编译为自注册为amis Renderer组件的IIFE脚本。
Steedos 软件包通过
webapps/
目录管理 React 子项目。每个子项目是独立的 Vite 应用,可独立开发调试,也可编译为 amis 自注册组件。

Directory Structure | 目录结构

目录结构

my-package/                              # Steedos package root
├── webapps/                             # React sub-projects
│   ├── designer/                        # webapp 1
│   │   ├── src/
│   │   │   ├── components/              # React components
│   │   │   ├── amis-entry.ts            # amis registration entry
│   │   │   ├── amis-jsx-shim.ts         # JSX Runtime bridge
│   │   │   └── amis-renderer.css
│   │   ├── dist/amis-renderer/          # Build output
│   │   ├── vite.config.ts               # Standard dev config
│   │   ├── vite.amis.config.ts          # amis IIFE build config
│   │   ├── package.json
│   │   └── tailwind.config.js
│   │
│   └── dashboard/                       # webapp 2
│       ├── src/
│       │   ├── components/
│       │   ├── amis-entry.ts
│       │   └── amis-jsx-shim.ts
│       ├── vite.amis.config.ts
│       └── package.json
├── main/default/
│   ├── client/                          # Client loader files
│   │   ├── designer.client.js           # Loads designer amis renderer
│   │   └── dashboard.client.js          # Loads dashboard amis renderer
│   └── routes/                          # Express SPA routers
│       ├── designer.router.js           # SPA access for designer
│       └── dashboard.router.js          # SPA access for dashboard
├── public/                              # Deployed output (copied from each webapp)
│   ├── designer/
│   │   ├── amis-renderer.js
│   │   └── amis-renderer.css
│   └── dashboard/
│       ├── amis-renderer.js
│       └── amis-renderer.css
├── package.json                         # Package root package.json
└── package.service.js                   # Moleculer service definition
Key Concepts:
  • Each
    webapps/
    subdirectory is an independent React + Vite project
  • Two build modes: standard Vite build (dev) + IIFE build (amis component)
  • IIFE output is copied to
    public/<webapp-name>/
    , served as static files by Steedos
  • main/default/client/<webapp-name>.client.js
    triggers loading of the IIFE into the Steedos frontend
  • Each webapp is isolated — can use different dependencies and versions
my-package/                              # Steedos软件包根目录
├── webapps/                             # React子项目
│   ├── designer/                        # Web应用1
│   │   ├── src/
│   │   │   ├── components/              # React组件
│   │   │   ├── amis-entry.ts            # amis注册入口
│   │   │   ├── amis-jsx-shim.ts         # JSX运行时桥接文件
│   │   │   └── amis-renderer.css
│   │   ├── dist/amis-renderer/          # 构建输出
│   │   ├── vite.config.ts               # 标准开发配置
│   │   ├── vite.amis.config.ts          # amis IIFE构建配置
│   │   ├── package.json
│   │   └── tailwind.config.js
│   │
│   └── dashboard/                       # Web应用2
│       ├── src/
│       │   ├── components/
│       │   ├── amis-entry.ts
│       │   └── amis-jsx-shim.ts
│       ├── vite.amis.config.ts
│       └── package.json
├── main/default/
│   ├── client/                          # 客户端加载文件
│   │   ├── designer.client.js           # 加载designer amis渲染器
│   │   └── dashboard.client.js          # 加载dashboard amis渲染器
│   └── routes/                          # Express SPA路由
│       ├── designer.router.js           # designer的SPA访问路由
│       └── dashboard.router.js          # dashboard的SPA访问路由
├── public/                              # 部署输出(从各个Web应用复制而来)
│   ├── designer/
│   │   ├── amis-renderer.js
│   │   └── amis-renderer.css
│   └── dashboard/
│       ├── amis-renderer.js
│       └── amis-renderer.css
├── package.json                         # 软件包根目录package.json
└── package.service.js                   # Moleculer服务定义
关键概念:
  • webapps/
    下的每个子目录都是独立的React + Vite项目
  • 两种构建模式:标准Vite构建(开发)+ IIFE构建(amis组件)
  • IIFE输出复制到
    public/<webapp-name>/
    ,由Steedos作为静态文件提供服务
  • main/default/client/<webapp-name>.client.js
    触发将IIFE加载到Steedos前端
  • 每个Web应用相互隔离——可使用不同的依赖及版本

Scaffold Selection | 脚手架选择

脚手架选择

Before creating a webapp, choose a UI scaffold. This determines which component library and styling approach to use. You can use any React-compatible UI library — below are two recommended options.
创建 webapp 前,先选择 UI 脚手架,决定使用哪个组件库和样式方案。支持任意 React 兼容的 UI 库,以下是两个推荐选项。
ScaffoldDescriptionWhen to Use
antd (default)Ant Design component library. Reuses host page's antd via
external
, no extra bundle size.
Default choice — enterprise forms, tables, standard UI. Recommended when unsure.
shadcn/uiTailwind CSS + Radix UI primitives. Components are copied into project (not a dependency).Custom/modern UI, full design control, lightweight output.
OtherAny React UI library (MUI, Chakra, Mantine, headless, etc.). Follow the same IIFE build pattern.Specific design system requirements or team preference.
创建Web应用前,先选择UI脚手架,决定使用哪个组件库和样式方案。支持任意React兼容的UI库,以下是两个推荐选项。
创建 webapp 前,先选择 UI 脚手架,决定使用哪个组件库和样式方案。支持任意 React 兼容的 UI 库,以下是两个推荐选项。
脚手架描述使用场景
antd(默认)Ant Design组件库。通过
external
复用宿主页面的antd,无额外打包体积。
默认选择——企业级表单、表格、标准UI。不确定时推荐使用。
shadcn/uiTailwind CSS + Radix UI基础组件。组件被复制到项目中(非依赖)。定制/现代UI、完全设计控制、轻量化输出。
其他任意React UI库(MUI、Chakra、Mantine、无样式组件等)。遵循相同的IIFE构建模式。特定设计系统要求或团队偏好。

Key Differences | 关键区别

关键区别

antdshadcn/ui
StylingCSS-in-JS (host page antd)Tailwind CSS utility classes
Bundle
antd
marked as
external
— zero bundle cost
Components copied into
src/
, bundled into IIFE
TailwindOptionalRequired
waitForThing
target
window.antd
window.antd
(still needed — amis SDK depends on antd)
PostCSS plugins
postcss-prefix-selector
postcss-prefix-selector
+ Tailwind v4 workarounds (removeAtProperty, unwrapTwSupports, removeAtLayer)
antdshadcn/ui
样式方案CSS-in-JS(宿主页面antd)Tailwind CSS工具类
打包
antd
标记为
external
——零打包成本
组件复制到
src/
,打包进IIFE
Tailwind可选必填
waitForThing
目标
window.antd
window.antd
(仍需依赖——amis SDK依赖antd)
PostCSS插件
postcss-prefix-selector
postcss-prefix-selector
+ Tailwind v4兼容方案(removeAtProperty、unwrapTwSupports、removeAtLayer)

antd Scaffold Setup | antd 脚手架

antd脚手架设置

bash
cd my-package/webapps
npm create vite@latest my-widget -- --template react-ts
cd my-widget
npm install
npm install antd
npm install -D @tailwindcss/postcss autoprefixer postcss-prefix-selector terser
In
vite.amis.config.ts
, mark antd as external:
typescript
rollupOptions: {
  external: ['react', 'react-dom', 'antd'],
  output: {
    globals: {
      react: 'amisRequire("react")',
      'react-dom': 'amisRequire("react-dom")',
      'antd': 'antd',
    },
  },
},
bash
cd my-package/webapps
npm create vite@latest my-widget -- --template react-ts
cd my-widget
npm install
npm install antd
npm install -D @tailwindcss/postcss autoprefixer postcss-prefix-selector terser
vite.amis.config.ts
中,将antd标记为外部依赖:
typescript
rollupOptions: {
  external: ['react', 'react-dom', 'antd'],
  output: {
    globals: {
      react: 'amisRequire("react")',
      'react-dom': 'amisRequire("react-dom")',
      'antd': 'antd',
    },
  },
},

shadcn/ui Scaffold Setup | shadcn/ui 脚手架

shadcn/ui脚手架设置

bash
cd my-package/webapps
npm create vite@latest my-widget -- --template react-ts
cd my-widget
npm install
npx shadcn@latest init
npm install -D @tailwindcss/postcss autoprefixer postcss-prefix-selector terser
npm install -D tailwindcss
In
vite.amis.config.ts
, do NOT externalize antd (shadcn/ui doesn't use it):
typescript
rollupOptions: {
  external: ['react', 'react-dom'],
  output: {
    globals: {
      react: 'amisRequire("react")',
      'react-dom': 'amisRequire("react-dom")',
    },
  },
},
⚠️ shadcn/ui uses Tailwind v4 — you MUST add the 3 PostCSS workaround plugins (see Tailwind CSS v4 Workarounds section).
bash
cd my-package/webapps
npm create vite@latest my-widget -- --template react-ts
cd my-widget
npm install
npx shadcn@latest init
npm install -D @tailwindcss/postcss autoprefixer postcss-prefix-selector terser
npm install -D tailwindcss
vite.amis.config.ts
中,不要将antd标记为外部依赖(shadcn/ui不使用它):
typescript
rollupOptions: {
  external: ['react', 'react-dom'],
  output: {
    globals: {
      react: 'amisRequire("react")',
      'react-dom': 'amisRequire("react-dom")',
    },
  },
},
⚠️ shadcn/ui使用Tailwind v4——必须添加3个PostCSS兼容插件(见Tailwind CSS v4兼容方案章节)。

Other Scaffolds | 其他脚手架

其他脚手架

Any React-compatible UI library works. The core requirements are the same:
  1. Mark
    react
    and
    react-dom
    as
    external
    in rollup (use
    amisRequire
    )
  2. If the library is already on the host page (like antd), mark it as
    external
    too
  3. Use
    postcss-prefix-selector
    for CSS isolation
  4. If using Tailwind v4, add the 3 PostCSS workaround plugins
任意React兼容的UI库均可使用。核心要求如下:
  1. 在rollup中标记
    react
    react-dom
    external
    (使用
    amisRequire
  2. 如果库已在宿主页面加载(如antd),也将其标记为
    external
  3. 使用
    postcss-prefix-selector
    实现CSS隔离
  4. 如果使用Tailwind v4,添加3个PostCSS兼容插件

Creating a New Webapp | 创建新 webapp

创建新Web应用

Step 1: Initialize Vite Project | 初始化

步骤1:初始化Vite项目

bash
cd my-package/webapps
npm create vite@latest my-widget -- --template react-ts
cd my-widget
npm install
bash
cd my-package/webapps
npm create vite@latest my-widget -- --template react-ts
cd my-widget
npm install

Step 2: Add Build Dependencies | 添加构建依赖

步骤2:添加构建依赖

bash
undefined
bash
undefined

Common dependencies (both scaffolds)

通用依赖(两种脚手架)

npm install -D @tailwindcss/postcss autoprefixer postcss-prefix-selector terser
npm install -D @tailwindcss/postcss autoprefixer postcss-prefix-selector terser

antd scaffold

antd脚手架

npm install antd
npm install antd

shadcn/ui scaffold

shadcn/ui脚手架

npx shadcn@latest init npm install -D tailwindcss
undefined
npx shadcn@latest init npm install -D tailwindcss
undefined

Step 3: Create amis Integration Files | 创建 amis 集成文件

步骤3:创建amis集成文件

Each webapp needs 3 amis-specific files:
webapps/my-widget/src/
├── amis-entry.ts          # Registration entry
├── amis-jsx-shim.ts       # JSX bridge (copy from other webapp)
└── amis-renderer.css      # Style entry (imported by amis-entry.ts)
每个Web应用需要3个amis专属文件:
webapps/my-widget/src/
├── amis-entry.ts          # 注册入口
├── amis-jsx-shim.ts       # JSX桥接文件(从其他Web应用复制)
└── amis-renderer.css      # 样式入口(由amis-entry.ts导入)

Step 4: Configure Build Scripts | 配置构建脚本

步骤4:配置构建脚本

In the webapp's
package.json
:
json
{
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "build:amis": "vite build --config vite.amis.config.ts && rm -rf ../../public/my-widget && cp -R dist/amis-renderer ../../public/my-widget",
    "build:all": "tsc -b && vite build && vite build --config vite.amis.config.ts && rm -rf ../../public/my-widget && cp -R dist/amis-renderer ../../public/my-widget"
  }
}
在Web应用的
package.json
中:
json
{
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "build:amis": "vite build --config vite.amis.config.ts && rm -rf ../../public/my-widget && cp -R dist/amis-renderer ../../public/my-widget",
    "build:all": "tsc -b && vite build && vite build --config vite.amis.config.ts && rm -rf ../../public/my-widget && cp -R dist/amis-renderer ../../public/my-widget"
  }
}

Step 5: Create SPA Router | 创建 SPA 路由

步骤5:创建SPA路由

Create
main/default/routes/<webapp-name>.router.js
to serve the webapp as a standalone SPA:
创建
main/default/routes/<webapp-name>.router.js
,让 webapp 可以作为独立 SPA 访问:
javascript
// main/default/routes/my-widget.router.js
'use strict';
const express = require('express');
const router = express.Router();
const { requireAuthentication } = require("@steedos/auth");
const path = require('path');

const packageRoot = path.dirname(require.resolve('@steedos-labs/my-package/package.json'));
const webappDistPath = path.join(packageRoot, 'webapps', 'my-widget', 'dist');
const amisRendererDistPath = path.join(webappDistPath, 'amis-renderer');

// Main page entry (requires auth)
router.get('/api/my-package/my-widget', requireAuthentication, async (req, res) => {
  try {
    if (process.env.NODE_ENV === 'development') {
      return res.redirect('http://localhost:5173');
    }
    res.sendFile(path.join(webappDistPath, 'index.html'), { dotfiles: 'allow' });
  } catch (e) {
    res.status(500).send({ errors: [{ errorMessage: e.message }] });
  }
});

// Static assets
router.use('/api/my-package/my-widget', express.static(webappDistPath));

// SPA fallback (frontend routing support)
router.use('/api/my-package/my-widget', (req, res, next) => {
  if (req.method !== 'GET') return next();
  if (path.extname(req.path)) return next();
  if (req.path === '/' || req.path === '') return next();
  requireAuthentication(req, res, () => {
    try {
      if (process.env.NODE_ENV === 'development') {
        return res.redirect('http://localhost:5173');
      }
      res.sendFile(path.join(webappDistPath, 'index.html'), { dotfiles: 'allow' });
    } catch (e) {
      res.status(500).send({ errors: [{ errorMessage: e.message }] });
    }
  });
});

// amis renderer static assets
router.use('/api/my-package/my-widget-amis', express.static(amisRendererDistPath));

exports.default = router;
Also update the webapp's
vite.config.ts
to set
base
matching the router path:
同时更新 webapp 的
vite.config.ts
,设置
base
与路由路径一致:
typescript
// webapps/my-widget/vite.config.ts
export default defineConfig(({ command }) => ({
  base: command === 'build' ? '/api/my-package/my-widget/' : '/',
  // ... other config
}))
创建
main/default/routes/<webapp-name>.router.js
,让Web应用可以作为独立SPA访问:
创建
main/default/routes/<webapp-name>.router.js
,让 webapp 可以作为独立 SPA 访问:
javascript
// main/default/routes/my-widget.router.js
'use strict';
const express = require('express');
const router = express.Router();
const { requireAuthentication } = require("@steedos/auth");
const path = require('path');

const packageRoot = path.dirname(require.resolve('@steedos-labs/my-package/package.json'));
const webappDistPath = path.join(packageRoot, 'webapps', 'my-widget', 'dist');
const amisRendererDistPath = path.join(webappDistPath, 'amis-renderer');

// 主页面入口(需认证)
router.get('/api/my-package/my-widget', requireAuthentication, async (req, res) => {
  try {
    if (process.env.NODE_ENV === 'development') {
      return res.redirect('http://localhost:5173');
    }
    res.sendFile(path.join(webappDistPath, 'index.html'), { dotfiles: 'allow' });
  } catch (e) {
    res.status(500).send({ errors: [{ errorMessage: e.message }] });
  }
});

// 静态资源
router.use('/api/my-package/my-widget', express.static(webappDistPath));

// SPA fallback(支持前端路由)
router.use('/api/my-package/my-widget', (req, res, next) => {
  if (req.method !== 'GET') return next();
  if (path.extname(req.path)) return next();
  if (req.path === '/' || req.path === '') return next();
  requireAuthentication(req, res, () => {
    try {
      if (process.env.NODE_ENV === 'development') {
        return res.redirect('http://localhost:5173');
      }
      res.sendFile(path.join(webappDistPath, 'index.html'), { dotfiles: 'allow' });
    } catch (e) {
      res.status(500).send({ errors: [{ errorMessage: e.message }] });
    }
  });
});

// amis渲染器静态资源
router.use('/api/my-package/my-widget-amis', express.static(amisRendererDistPath));

exports.default = router;
同时更新Web应用的
vite.config.ts
,设置
base
与路由路径一致:
同时更新 webapp 的
vite.config.ts
,设置
base
与路由路径一致:
typescript
// webapps/my-widget/vite.config.ts
export default defineConfig(({ command }) => ({
  base: command === 'build' ? '/api/my-package/my-widget/' : '/',
  // ... 其他配置
}))

Step 6: Register in Package Root | 在软件包根目录注册

步骤6:在软件包根目录注册

json
{
  "name": "@steedos-labs/my-package",
  "files": [
    "main/default/client",
    "main/default/routes",
    "webapps/my-widget/dist",
    "public/my-widget",
    "package.service.js"
  ],
  "scripts": {
    "build:my-widget": "cd webapps/my-widget && npm run build:all",
    "build:webapps": "npm run build:my-widget"
  }
}
json
{
  "name": "@steedos-labs/my-package",
  "files": [
    "main/default/client",
    "main/default/routes",
    "webapps/my-widget/dist",
    "public/my-widget",
    "package.service.js"
  ],
  "scripts": {
    "build:my-widget": "cd webapps/my-widget && npm run build:all",
    "build:webapps": "npm run build:my-widget"
  }
}

Key Files | 关键文件

关键文件

vite.amis.config.ts — IIFE Build Config

vite.amis.config.ts — IIFE构建配置

Compiles React components into self-executing IIFE scripts loadable by amis.
typescript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/postcss'
import autoprefixer from 'autoprefixer'
import path from 'path'

import { createRequire } from 'module'
const require = createRequire(import.meta.url)
const prefixSelector = require('postcss-prefix-selector')

// CSS scope prefix — all styles scoped under this selector
const SCOPE = '.my-widget'

export default defineConfig({
  plugins: [react({ jsxRuntime: 'classic' })],

  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      'react/jsx-runtime': path.resolve(__dirname, './src/amis-jsx-shim.ts'),
      'react/jsx-dev-runtime': path.resolve(__dirname, './src/amis-jsx-shim.ts'),
    },
  },

  define: {
    'process.env.NODE_ENV': JSON.stringify('production'),
    'process.env': JSON.stringify({}),
  },

  build: {
    outDir: 'dist/amis-renderer',
    emptyOutDir: true,
    lib: {
      entry: path.resolve(__dirname, 'src/amis-entry.ts'),
      name: 'MyWidget',
      formats: ['iife'],
      fileName: () => 'amis-renderer.js',
    },
    rollupOptions: {
      external: ['react', 'react-dom', 'antd'],
      output: {
        globals: {
          react: 'amisRequire("react")',
          'react-dom': 'amisRequire("react-dom")',
          'antd': 'antd',
        },
        assetFileNames: 'amis-renderer.[ext]',
      },
    },
    assetsInlineLimit: 8192,
    cssCodeSplit: false,
    minify: 'terser',
  },

  css: {
    postcss: {
      plugins: [
        tailwindcss(),
        autoprefixer(),
        prefixSelector({
          prefix: SCOPE,
          transform(prefix, selector, prefixedSelector, _filePath, rule) {
            if (selector === ':root') return selector;
            if (/^(html|body)(\s|,|$)/.test(selector)) return selector;
            const parent = rule.parent;
            if (parent?.type === 'atrule' && /^keyframes/.test(parent.name)) return selector;
            return prefixedSelector;
          },
        }),
      ],
    },
  },
})
Critical configuration points:
ConfigPurpose
formats: ['iife']
Self-executing script, registers on load
external: ['react', 'react-dom']
React NOT bundled — reuse amis SDK's React
react/jsx-runtime
alias
Prevent 3rd-party libs from bundling their own jsx-runtime
postcss-prefix-selector
Scope all CSS under prefix to prevent style leaks
jsxRuntime: 'classic'
Use
React.createElement
instead of new JSX Transform
将React组件编译为可被amis加载的自执行IIFE脚本。
typescript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/postcss'
import autoprefixer from 'autoprefixer'
import path from 'path'

import { createRequire } from 'module'
const require = createRequire(import.meta.url)
const prefixSelector = require('postcss-prefix-selector')

// CSS作用域前缀——所有样式都在此选择器下生效
const SCOPE = '.my-widget'

export default defineConfig({
  plugins: [react({ jsxRuntime: 'classic' })],

  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      'react/jsx-runtime': path.resolve(__dirname, './src/amis-jsx-shim.ts'),
      'react/jsx-dev-runtime': path.resolve(__dirname, './src/amis-jsx-shim.ts'),
    },
  },

  define: {
    'process.env.NODE_ENV': JSON.stringify('production'),
    'process.env': JSON.stringify({}),
  },

  build: {
    outDir: 'dist/amis-renderer',
    emptyOutDir: true,
    lib: {
      entry: path.resolve(__dirname, 'src/amis-entry.ts'),
      name: 'MyWidget',
      formats: ['iife'],
      fileName: () => 'amis-renderer.js',
    },
    rollupOptions: {
      external: ['react', 'react-dom', 'antd'],
      output: {
        globals: {
          react: 'amisRequire("react")',
          'react-dom': 'amisRequire("react-dom")',
          'antd': 'antd',
        },
        assetFileNames: 'amis-renderer.[ext]',
      },
    },
    assetsInlineLimit: 8192,
    cssCodeSplit: false,
    minify: 'terser',
  },

  css: {
    postcss: {
      plugins: [
        tailwindcss(),
        autoprefixer(),
        prefixSelector({
          prefix: SCOPE,
          transform(prefix, selector, prefixedSelector, _filePath, rule) {
            if (selector === ':root') return selector;
            if (/^(html|body)(\s|,|$)/.test(selector)) return selector;
            const parent = rule.parent;
            if (parent?.type === 'atrule' && /^keyframes/.test(parent.name)) return selector;
            return prefixedSelector;
          },
        }),
      ],
    },
  },
})
关键配置点:
配置项用途
formats: ['iife']
自执行脚本,加载时自动注册
external: ['react', 'react-dom']
React不打包——复用amis SDK的React
react/jsx-runtime
别名
防止第三方库打包自己的jsx-runtime
postcss-prefix-selector
将所有CSS限定在指定前缀下,防止样式泄露
jsxRuntime: 'classic'
使用
React.createElement
而非新的JSX转换

amis-jsx-shim.ts — JSX Runtime Bridge

amis-jsx-shim.ts — JSX运行时桥接文件

Rollup
external
can only externalize
react
, not the
react/jsx-runtime
sub-path. Third-party dependencies (e.g.
@tiptap/react
) import from
react/jsx-runtime
, which would bundle an incompatible React copy. This shim delegates
jsx()
/
jsxs()
to the externalized
React.createElement()
.
This file is identical across all webapps — copy directly without modification.
typescript
import React from 'react';

export function jsx(
  type: React.ElementType,
  props: Record<string, unknown>,
  key?: string,
): React.ReactElement {
  const { children, ...rest } = props;
  if (key !== undefined) (rest as Record<string, unknown>).key = key;
  return children !== undefined
    ? React.createElement(type, rest as React.Attributes, children as React.ReactNode)
    : React.createElement(type, rest as React.Attributes);
}

export function jsxs(
  type: React.ElementType,
  props: Record<string, unknown>,
  key?: string,
): React.ReactElement {
  const { children, ...rest } = props;
  if (key !== undefined) (rest as Record<string, unknown>).key = key;
  if (Array.isArray(children)) {
    return React.createElement(type, rest as React.Attributes, ...children);
  }
  return children !== undefined
    ? React.createElement(type, rest as React.Attributes, children as React.ReactNode)
    : React.createElement(type, rest as React.Attributes);
}

export const jsxDEV = jsx;
export const Fragment = React.Fragment;
Rollup的
external
只能外部化
react
,无法外部化
react/jsx-runtime
子路径。第三方依赖(如
@tiptap/react
)会从
react/jsx-runtime
导入,这会打包一个不兼容的React副本。此垫片将
jsx()
/
jsxs()
委托给外部化的
React.createElement()
所有Web应用的此文件完全相同——直接复制无需修改。
typescript
import React from 'react';

export function jsx(
  type: React.ElementType,
  props: Record<string, unknown>,
  key?: string,
): React.ReactElement {
  const { children, ...rest } = props;
  if (key !== undefined) (rest as Record<string, unknown>).key = key;
  return children !== undefined
    ? React.createElement(type, rest as React.Attributes, children as React.ReactNode)
    : React.createElement(type, rest as React.Attributes);
}

export function jsxs(
  type: React.ElementType,
  props: Record<string, unknown>,
  key?: string,
): React.ReactElement {
  const { children, ...rest } = props;
  if (key !== undefined) (rest as Record<string, unknown>).key = key;
  if (Array.isArray(children)) {
    return React.createElement(type, rest as React.Attributes, ...children);
  }
  return children !== undefined
    ? React.createElement(type, rest as React.Attributes, children as React.ReactNode)
    : React.createElement(type, rest as React.Attributes);
}

export const jsxDEV = jsx;
export const Fragment = React.Fragment;

amis-entry.ts — Registration Entry

amis-entry.ts — 注册入口

IIFE build entry point. Imports styles, defines a bridge component, registers via
amisLib.Renderer()
.
typescript
import './amis-renderer.css';
import { MyReactComponent } from './components/MyReactComponent';

declare global {
  function amisRequire(mod: string): any;
}

function register() {
  if (typeof amisRequire === 'undefined') {
    console.error('[my-widget] amisRequire is not defined. Load amis SDK first.');
    return;
  }

  const React = amisRequire('react');
  const amisLib = amisRequire('amis');

  if (!amisLib?.Renderer) {
    console.error('[my-widget] amis.Renderer not found.');
    return;
  }

  // Bridge component: amis props → React component props
  function MyWidget(props: any) {
    const { $schema, data, dispatchEvent } = props;

    // Register to amis ScopedContext (makes getComponentById work)
    const ScopedContext = amisLib.ScopedContext;
    const scoped = React.useContext(ScopedContext);
    const compRef = React.useRef(null);
    const scopedRef = React.useRef(null);

    const componentMethods = {
      getValue: () => compRef.current?.getValue(),
      validate: async () => compRef.current?.validate(),
    };

    if (scopedRef.current === null) {
      scopedRef.current = { ...componentMethods };
    } else {
      Object.assign(scopedRef.current, componentMethods);
    }

    Object.defineProperty(scopedRef.current, 'props', {
      get: () => props,
      configurable: true,
    });

    React.useEffect(() => {
      if (!scoped || !($schema.id || props.id)) return;
      scoped.registerComponent(scopedRef.current);
      return () => scoped.unRegisterComponent(scopedRef.current);
    }, [$schema.id || props.id]);

    // Read custom properties from $schema (configured in amis JSON schema)
    const title = $schema.title || '';
    const config = $schema.config || {};

    // Read from amis data scope
    const contextValue = data?.someKey || '';

    // Event callback — dispatch events to amis
    const handleChange = (val: any) => {
      dispatchEvent?.('change', { value: val });
    };

    // Render the actual React component
    return React.createElement(MyReactComponent, {
      ref: compRef,
      title,
      config,
      value: contextValue,
      onChange: handleChange,
      className: 'my-widget',  // MUST match CSS scope prefix
    });
  }

  // Register as amis Renderer
  amisLib.Renderer({
    type: 'my-widget',    // type name used in amis JSON schema
    autoVar: true,
  })(MyWidget);

  console.log('[my-widget] amis Renderer registered.');
}

register();
IIFE构建的入口点。导入样式,定义桥接组件,通过
amisLib.Renderer()
注册。
typescript
import './amis-renderer.css';
import { MyReactComponent } from './components/MyReactComponent';

declare global {
  function amisRequire(mod: string): any;
}

function register() {
  if (typeof amisRequire === 'undefined') {
    console.error('[my-widget] amisRequire未定义。请先加载amis SDK。');
    return;
  }

  const React = amisRequire('react');
  const amisLib = amisRequire('amis');

  if (!amisLib?.Renderer) {
    console.error('[my-widget] 未找到amis.Renderer。');
    return;
  }

  // 桥接组件:amis props → React组件props
  function MyWidget(props: any) {
    const { $schema, data, dispatchEvent } = props;

    // 注册到amis ScopedContext(使getComponentById生效)
    const ScopedContext = amisLib.ScopedContext;
    const scoped = React.useContext(ScopedContext);
    const compRef = React.useRef(null);
    const scopedRef = React.useRef(null);

    const componentMethods = {
      getValue: () => compRef.current?.getValue(),
      validate: async () => compRef.current?.validate(),
    };

    if (scopedRef.current === null) {
      scopedRef.current = { ...componentMethods };
    } else {
      Object.assign(scopedRef.current, componentMethods);
    }

    Object.defineProperty(scopedRef.current, 'props', {
      get: () => props,
      configurable: true,
    });

    React.useEffect(() => {
      if (!scoped || !($schema.id || props.id)) return;
      scoped.registerComponent(scopedRef.current);
      return () => scoped.unRegisterComponent(scopedRef.current);
    }, [$schema.id || props.id]);

    // 从$schema读取自定义属性(在amis JSON schema中配置)
    const title = $schema.title || '';
    const config = $schema.config || {};

    // 从amis数据作用域读取
    const contextValue = data?.someKey || '';

    // 事件回调——向amis分发事件
    const handleChange = (val: any) => {
      dispatchEvent?.('change', { value: val });
    };

    // 渲染实际的React组件
    return React.createElement(MyReactComponent, {
      ref: compRef,
      title,
      config,
      value: contextValue,
      onChange: handleChange,
      className: 'my-widget',  // 必须匹配CSS作用域前缀
    });
  }

  // 注册为amis Renderer
  amisLib.Renderer({
    type: 'my-widget',    // 在amis JSON schema中使用的类型名称
    autoVar: true,
  })(MyWidget);

  console.log('[my-widget] amis Renderer已注册。');
}

register();

amis Props Reference | amis 传入的 Props

amis传入的Props参考

PropDescription
$schema
Full JSON schema node — includes all custom properties you defined
data
amis current data scope (context variables)
dispatchEvent
Dispatch events to amis (
change
,
submit
, etc.)
onBulkChange
Batch-write values back to amis data scope
env
amis environment config (fetcher, notify, etc.)
Prop描述
$schema
完整的JSON schema节点——包含所有你定义的自定义属性
data
amis当前的数据作用域(上下文变量)
dispatchEvent
向amis分发事件(
change
submit
等)
onBulkChange
批量将值写回amis数据作用域
env
amis环境配置(fetcher、notify等)

Using in Amis Schema | 在 amis Schema 中使用

在amis Schema中使用

After registration, reference via
type
in amis JSON schema:
json
{
  "type": "my-widget",
  "id": "widget1",
  "title": "Hello World",
  "config": { "theme": "dark" },
  "onEvent": {
    "change": {
      "actions": [
        {
          "actionType": "setValue",
          "args": { "value": "${event.data.value}" }
        }
      ]
    }
  }
}
注册完成后,在amis JSON schema中通过
type
引用:
json
{
  "type": "my-widget",
  "id": "widget1",
  "title": "Hello World",
  "config": { "theme": "dark" },
  "onEvent": {
    "change": {
      "actions": [
        {
          "actionType": "setValue",
          "args": { "value": "${event.data.value}" }
        }
      ]
    }
  }
}

API v6 Response Structures | API v6 响应数据结构

API v6响应数据结构

When calling Steedos API v6 endpoints from webapp code (fetch/axios), use the correct response format:
EndpointResponse Format
GET /api/v6/data/:obj?skip=0&top=20
(list)
{ "data": [...], "totalCount": 42 }
GET /api/v6/data/:obj/:id
(single)
{ "_id": "...", "name": "...", ... }
— Raw document, NOT wrapped
POST /api/v6/data/:obj
(create)
{ "_id": "...", ... }
— Raw created document, NOT wrapped
PATCH /api/v6/data/:obj/:id
(update)
{ "_id": "...", ... }
— Raw updated document, NOT wrapped
DELETE /api/v6/data/:obj/:id
(delete)
{ "deleted": true, "_id": "..." }
POST /api/v6/functions/:obj/:fn
(function)
Whatever the function returns — NO wrapping, raw return value
typescript
// List records — response has { data, totalCount }
const res = await fetch('/api/v6/data/orders?skip=0&top=20');
const { data: orders, totalCount } = await res.json();

// Single record — response IS the record
const res = await fetch(`/api/v6/data/orders/${id}`);
const order = await res.json(); // { _id, name, status, ... }

// Create record — response IS the created record
const res = await fetch('/api/v6/data/orders', { method: 'POST', body: JSON.stringify(record) });
const created = await res.json(); // { _id, name, created, ... }

// Call function — response IS whatever the function returns
const res = await fetch('/api/v6/functions/orders/approve', {
  method: 'POST',
  body: JSON.stringify({ id: orderId })
});
const result = await res.json(); // e.g. { message: "Approved", success: true }
⚠️
skip
and
top
are REQUIRED for all list endpoints
(
/api/v6/data/
,
/api/v6/tables/
,
/api/v6/direct/
).
📖 For complete API v6 documentation (all endpoints, filter operators, complex filters, authentication), load the steedos-server-api skill.
📖 如需 API v6 完整文档(所有端点、筛选运算符、复合筛选、认证方式),请加载 steedos-server-api 技能
从Web应用代码(fetch/axios)调用Steedos API v6端点时,需使用正确的响应格式:
端点响应格式
GET /api/v6/data/:obj?skip=0&top=20
(列表)
{ "data": [...], "totalCount": 42 }
GET /api/v6/data/:obj/:id
(单条)
{ "_id": "...", "name": "...", ... }
— 原始文档,被包裹
POST /api/v6/data/:obj
(创建)
{ "_id": "...", ... }
— 原始创建文档,被包裹
PATCH /api/v6/data/:obj/:id
(更新)
{ "_id": "...", ... }
— 原始更新文档,被包裹
DELETE /api/v6/data/:obj/:id
(删除)
{ "deleted": true, "_id": "..." }
POST /api/v6/functions/:obj/:fn
(函数)
函数返回的任意内容——无包裹,原始返回值
typescript
// 获取列表——响应包含{ data, totalCount }
const res = await fetch('/api/v6/data/orders?skip=0&top=20');
const { data: orders, totalCount } = await res.json();

// 获取单条记录——响应即为记录本身
const res = await fetch(`/api/v6/data/orders/${id}`);
const order = await res.json(); // { _id, name, status, ... }

// 创建记录——响应即为创建的记录
const res = await fetch('/api/v6/data/orders', { method: 'POST', body: JSON.stringify(record) });
const created = await res.json(); // { _id, name, created, ... }

// 调用函数——响应即为函数返回值
const res = await fetch('/api/v6/functions/orders/approve', {
  method: 'POST',
  body: JSON.stringify({ id: orderId })
});
const result = await res.json(); // 例如:{ message: "Approved", success: true }
⚠️ 所有列表端点
/api/v6/data/
/api/v6/tables/
/api/v6/direct/
必须包含
skip
top
参数
📖 如需API v6完整文档(所有端点、筛选运算符、复合筛选、认证方式),请加载 steedos-server-api 技能
📖 如需 API v6 完整文档(所有端点、筛选运算符、复合筛选、认证方式),请加载 steedos-server-api 技能

Express Router for SPA Access | 通过 Router 提供 SPA 访问

通过Express路由提供SPA访问

Note: The SPA router is created by default in Step 5 of "Creating a New Webapp". This section provides detailed reference for the router implementation.
注意: SPA 路由已在「创建新 webapp」Step 5 中默认创建。本节提供路由实现的详细参考。
Webapps serve as standalone SPA applications via Express routes in
main/default/routes/
:
javascript
'use strict';
const express = require('express');
const router = express.Router();
const { requireAuthentication } = require("@steedos/auth");
const path = require('path');

const packageRoot = path.dirname(require.resolve('@steedos-labs/my-package/package.json'));
const webappDistPath = path.join(packageRoot, 'webapps', 'my-widget', 'dist');
const amisRendererDistPath = path.join(webappDistPath, 'amis-renderer');

// Main page entry (requires auth)
router.get('/api/my-package/my-widget', requireAuthentication, async (req, res) => {
  try {
    if (process.env.NODE_ENV === 'development') {
      return res.redirect('http://localhost:5173');
    }
    res.sendFile(path.join(webappDistPath, 'index.html'), { dotfiles: 'allow' });
  } catch (e) {
    res.status(500).send({ errors: [{ errorMessage: e.message }] });
  }
});

// Static assets
router.use('/api/my-package/my-widget', express.static(webappDistPath));

// SPA fallback (frontend routing support)
router.use('/api/my-package/my-widget', (req, res, next) => {
  if (req.method !== 'GET') return next();
  if (path.extname(req.path)) return next();
  if (req.path === '/' || req.path === '') return next();
  requireAuthentication(req, res, () => {
    try {
      if (process.env.NODE_ENV === 'development') {
        return res.redirect('http://localhost:5173');
      }
      res.sendFile(path.join(webappDistPath, 'index.html'), { dotfiles: 'allow' });
    } catch (e) {
      res.status(500).send({ errors: [{ errorMessage: e.message }] });
    }
  });
});

// amis renderer static assets
router.use('/api/my-package/my-widget-amis', express.static(amisRendererDistPath));

exports.default = router;
Important: The webapp's
vite.config.ts
base
must match the router path:
typescript
base: command === 'build' ? '/api/my-package/my-widget/' : '/',
注意: SPA路由已在「创建新Web应用」步骤5中默认创建。本节提供路由实现的详细参考。
注意: SPA 路由已在「创建新 webapp」Step 5 中默认创建。本节提供路由实现的详细参考。
Web应用通过
main/default/routes/
中的Express路由作为独立SPA应用提供服务:
javascript
'use strict';
const express = require('express');
const router = express.Router();
const { requireAuthentication } = require("@steedos/auth");
const path = require('path');

const packageRoot = path.dirname(require.resolve('@steedos-labs/my-package/package.json'));
const webappDistPath = path.join(packageRoot, 'webapps', 'my-widget', 'dist');
const amisRendererDistPath = path.join(webappDistPath, 'amis-renderer');

// 主页面入口(需认证)
router.get('/api/my-package/my-widget', requireAuthentication, async (req, res) => {
  try {
    if (process.env.NODE_ENV === 'development') {
      return res.redirect('http://localhost:5173');
    }
    res.sendFile(path.join(webappDistPath, 'index.html'), { dotfiles: 'allow' });
  } catch (e) {
    res.status(500).send({ errors: [{ errorMessage: e.message }] });
  }
});

// 静态资源
router.use('/api/my-package/my-widget', express.static(webappDistPath));

// SPA fallback(支持前端路由)
router.use('/api/my-package/my-widget', (req, res, next) => {
  if (req.method !== 'GET') return next();
  if (path.extname(req.path)) return next();
  if (req.path === '/' || req.path === '') return next();
  requireAuthentication(req, res, () => {
    try {
      if (process.env.NODE_ENV === 'development') {
        return res.redirect('http://localhost:5173');
      }
      res.sendFile(path.join(webappDistPath, 'index.html'), { dotfiles: 'allow' });
    } catch (e) {
      res.status(500).send({ errors: [{ errorMessage: e.message }] });
    }
  });
});

// amis渲染器静态资源
router.use('/api/my-package/my-widget-amis', express.static(amisRendererDistPath));

exports.default = router;
重要提示: Web应用的
vite.config.ts
中的
base
必须与路由路径匹配:
typescript
base: command === 'build' ? '/api/my-package/my-widget/' : '/',

Client Loader File | 客户端加载文件

客户端加载文件

⚠️ Required: Each webapp MUST have a client loader file at
main/default/client/<webapp-name>.client.js
. This file tells Steedos to load the compiled amis renderer JS and CSS into the frontend. Without it, the amis component will NOT be available in pages.
⚠️ 必须:每个 webapp 都必须有
main/default/client/<webapp-name>.client.js
加载文件。没有此文件,amis 自定义组件不会被加载到前端。
⚠️ 必须:每个Web应用都必须有
main/default/client/<webapp-name>.client.js
加载文件。此文件告知Steedos将编译后的amis渲染器JS和CSS加载到前端。没有此文件,amis自定义组件将无法在页面中使用。
⚠️ 必须:每个 webapp 都必须有
main/default/client/<webapp-name>.client.js
加载文件。没有此文件,amis 自定义组件不会被加载到前端。

File Naming | 命名规则

命名规则

The file name MUST match the webapp/public folder name:
Webapp DirectoryPublic OutputClient Loader File
webapps/designer/
public/designer/
main/default/client/designer.client.js
webapps/dashboard/
public/dashboard/
main/default/client/dashboard.client.js
webapps/workstation/
public/workstation/
main/default/client/workstation.client.js
文件名必须与webapp/public文件夹名称匹配:
Web应用目录公共输出目录客户端加载文件
webapps/designer/
public/designer/
main/default/client/designer.client.js
webapps/dashboard/
public/dashboard/
main/default/client/dashboard.client.js
webapps/workstation/
public/workstation/
main/default/client/workstation.client.js

File Content | 文件内容

文件内容

javascript
// main/default/client/my-widget.client.js
waitForThing(window, 'antd').then(function(){
    loadJs('/my-widget/amis-renderer.js');
    loadCss('/my-widget/amis-renderer.css')
})
How it works:
  1. waitForThing(window, 'antd')
    — Waits until
    window.antd
    is available. The IIFE uses
    amisRequire("react")
    and
    amisRequire("amis")
    which depend on antd being loaded first.
  2. loadJs('/my-widget/amis-renderer.js')
    — Loads the compiled IIFE script from
    public/my-widget/
    . The script self-executes and registers the amis Renderer.
  3. loadCss('/my-widget/amis-renderer.css')
    — Loads the scoped CSS from
    public/my-widget/
    .
The paths (
/my-widget/...
) correspond to the
public/my-widget/
directory, which Steedos serves as static files.
waitForThing
loadJs
loadCss
是 Steedos 前端内置的全局工具函数,无需额外引入。
javascript
// main/default/client/my-widget.client.js
waitForThing(window, 'antd').then(function(){
    loadJs('/my-widget/amis-renderer.js');
    loadCss('/my-widget/amis-renderer.css')
})
工作原理:
  1. waitForThing(window, 'antd')
    — 等待
    window.antd
    可用。IIFE使用
    amisRequire("react")
    amisRequire("amis")
    ,它们依赖antd先加载。
  2. loadJs('/my-widget/amis-renderer.js')
    — 从
    public/my-widget/
    加载编译后的IIFE脚本。脚本自执行并注册amis Renderer。
  3. loadCss('/my-widget/amis-renderer.css')
    — 从
    public/my-widget/
    加载作用域化的CSS。
路径(
/my-widget/...
)对应
public/my-widget/
目录,Steedos将其作为静态文件提供服务。
waitForThing
loadJs
loadCss
是 Steedos 前端内置的全局工具函数,无需额外引入。

CSS Isolation | CSS 隔离

CSS隔离

Scope Prefix | 作用域前缀

作用域前缀

postcss-prefix-selector
adds a scope class to all CSS rules:
css
/* Before */
.btn { color: red; }

/* After */
.my-widget .btn { color: red; }
The component root element MUST have the matching class name (passed via
className
in
amis-entry.ts
).
postcss-prefix-selector
为所有CSS规则添加作用域类:
css
/* 之前 */
.btn { color: red; }

/* 之后 */
.my-widget .btn { color: red; }
组件根元素必须有匹配的类名(在
amis-entry.ts
中通过
className
传递)。

Tailwind CSS v4 Workarounds

Tailwind CSS v4兼容方案

Tailwind v4 introduces
@property
,
@supports
,
@layer
— global CSS features that conflict with the host page. Add these 3 PostCSS plugins after
prefixSelector
in
vite.amis.config.ts
:
typescript
// 1. Remove @property — global, pollutes host page CSS property registry
function removeAtProperty() {
  return {
    postcssPlugin: 'remove-at-property',
    AtRule: { property(rule) { rule.remove() } },
  }
}
removeAtProperty.postcss = true

// 2. Unwrap @supports fallbacks — @property removed, so variable defaults must apply unconditionally
function unwrapTwSupports() {
  return {
    postcssPlugin: 'unwrap-tw-supports',
    AtRule: {
      supports(atRule) {
        if (atRule.params.includes('-webkit-hyphens') || atRule.params.includes('-moz-orient')) {
          atRule.nodes?.length ? atRule.replaceWith(atRule.nodes) : atRule.remove()
        }
      },
    },
  }
}
unwrapTwSupports.postcss = true

// 3. Remove @layer — host page CSS not in layers, layer styles can never override
function removeAtLayer() {
  return {
    postcssPlugin: 'remove-at-layer',
    AtRule: {
      layer(atRule) {
        atRule.nodes?.length ? atRule.replaceWith(atRule.nodes) : atRule.remove()
      },
    },
  }
}
removeAtLayer.postcss = true
Tailwind v4引入了
@property
@supports
@layer
——这些全局CSS特性会与宿主页面冲突。在
vite.amis.config.ts
prefixSelector
之后添加这3个PostCSS插件:
typescript
// 1. 移除@property——全局特性,会污染宿主页面CSS属性注册表
function removeAtProperty() {
  return {
    postcssPlugin: 'remove-at-property',
    AtRule: { property(rule) { rule.remove() } },
  }
}
removeAtProperty.postcss = true

// 2. 展开@supports降级——@property已移除,变量默认值必须无条件生效
function unwrapTwSupports() {
  return {
    postcssPlugin: 'unwrap-tw-supports',
    AtRule: {
      supports(atRule) {
        if (atRule.params.includes('-webkit-hyphens') || atRule.params.includes('-moz-orient')) {
          atRule.nodes?.length ? atRule.replaceWith(atRule.nodes) : atRule.remove()
        }
      },
    },
  }
}
unwrapTwSupports.postcss = true

// 3. 移除@layer——宿主页面CSS不在层中,层样式永远无法覆盖
function removeAtLayer() {
  return {
    postcssPlugin: 'remove-at-layer',
    AtRule: {
      layer(atRule) {
        atRule.nodes?.length ? atRule.replaceWith(atRule.nodes) : atRule.remove()
      },
    },
  }
}
removeAtLayer.postcss = true

Multi-Webapp Management | 多 webapp 管理

多Web应用管理

Package Root Configuration

软件包根目录配置

json
{
  "name": "@steedos-labs/my-package",
  "files": [
    "main/default/client",
    "main/default/routes",
    "webapps/designer/dist",
    "webapps/dashboard/dist",
    "public/designer",
    "public/dashboard",
    "package.service.js"
  ],
  "scripts": {
    "build:designer": "cd webapps/designer && npm run build:all",
    "build:dashboard": "cd webapps/dashboard && npm run build:all",
    "build:webapps": "npm run build:designer && npm run build:dashboard",
    "release": "npm run build:webapps && npm publish"
  }
}
json
{
  "name": "@steedos-labs/my-package",
  "files": [
    "main/default/client",
    "main/default/routes",
    "webapps/designer/dist",
    "webapps/dashboard/dist",
    "public/designer",
    "public/dashboard",
    "package.service.js"
  ],
  "scripts": {
    "build:designer": "cd webapps/designer && npm run build:all",
    "build:dashboard": "cd webapps/dashboard && npm run build:all",
    "build:webapps": "npm run build:designer && npm run build:dashboard",
    "release": "npm run build:webapps && npm publish"
  }
}

Webapp Independence | webapp 独立性

Web应用独立性

  • Each webapp is a fully independent Vite project with its own
    node_modules
  • Different webapps can use different dependency versions
  • CSS scope prefixes are unique per webapp — no style conflicts
  • amis
    type
    names must be globally unique (e.g.
    workflow-form-v2
    ,
    dashboard-chart
    )
  • amis-jsx-shim.ts
    is identical across all webapps — copy directly
  • 每个Web应用都是完全独立的Vite项目,拥有自己的
    node_modules
  • 不同Web应用可使用不同的依赖版本
  • CSS作用域前缀每个Web应用唯一——无样式冲突
  • amis的
    type
    名称必须全局唯一(如
    workflow-form-v2
    dashboard-chart
  • amis-jsx-shim.ts
    在所有Web应用中完全相同——直接复制

Development Workflow | 开发工作流

开发工作流

bash
undefined
bash
undefined

---- Development ----

---- 开发阶段 ----

cd webapps/my-widget && npm install npm run dev # http://localhost:5173
cd webapps/my-widget && npm install npm run dev # 访问http://localhost:5173

---- Build ----

---- 构建阶段 ----

npm run build:amis # IIFE only → dist/amis-renderer/ → public/my-widget/ npm run build:all # Standard + IIFE
npm run build:amis # 仅构建IIFE → dist/amis-renderer/ → public/my-widget/ npm run build:all # 标准构建 + IIFE构建

---- Test in Steedos ----

---- 在Steedos中测试 ----

Start Steedos — public/my-widget/ auto-served as static files

启动Steedos——public/my-widget/自动作为静态文件提供服务

amis pages load amis-renderer.js and auto-register the component

amis页面加载amis-renderer.js并自动注册组件

---- Publish ----

---- 发布阶段 ----

cd ../.. && npm publish # files field ensures public/my-widget is included
undefined
cd ../.. && npm publish # files字段确保public/my-widget被包含在内
undefined

Minimum Checklist | 最小化清单

最小化清单

#FileDescription
1
webapps/xxx/src/components/
React business components
2
webapps/xxx/src/amis-jsx-shim.ts
JSX bridge (copy directly)
3
webapps/xxx/src/amis-entry.ts
amis registration entry (modify component import and type)
4
webapps/xxx/vite.amis.config.ts
IIFE build config (modify entry, global name, CSS scope)
5
webapps/xxx/package.json
Add
build:amis
script
6Root
package.json
Include
public/xxx
,
main/default/client
,
main/default/routes
in
files
, add build command
7
main/default/client/xxx.client.js
⚠️ Client loader — triggers loading of amis renderer into frontend
8
main/default/routes/xxx.router.js
⚠️ SPA router — enables standalone SPA access via
/api/<pkg>/xxx
序号文件描述
1
webapps/xxx/src/components/
React业务组件
2
webapps/xxx/src/amis-jsx-shim.ts
JSX桥接文件(直接复制)
3
webapps/xxx/src/amis-entry.ts
amis注册入口(修改组件导入和类型)
4
webapps/xxx/vite.amis.config.ts
IIFE构建配置(修改入口、全局名称、CSS作用域)
5
webapps/xxx/package.json
添加
build:amis
脚本
6根目录
package.json
files
中包含
public/xxx
main/default/client
main/default/routes
,添加构建命令
7
main/default/client/xxx.client.js
⚠️ 客户端加载文件——触发将amis渲染器加载到前端
8
main/default/routes/xxx.router.js
⚠️ SPA路由——允许通过
/api/<pkg>/xxx
访问独立SPA

Post-Development Prompt | 开发完成后提示

开发完成后提示

After completing webapp development and configuration, always inform the user about the available access methods:
webapp 开发和配置完成后,务必告知用户可用的访问方式
✅ Webapp
{webapp-name}
开发完成!你可以通过以下方式访问:
  • amis 组件方式:在 amis Schema 中使用
    "type": "{webapp-name}"
    嵌入到任意页面
  • 独立 SPA 方式:通过浏览器访问
    {ROOT_URL}/api/{package-name}/{webapp-name}
  • 开发模式:运行
    cd webapps/{webapp-name} && npm run dev
    ,访问
    http://localhost:5173
Web应用开发和配置完成后,务必告知用户可用的访问方式
webapp 开发和配置完成后,务必告知用户可用的访问方式
✅ Web应用
{webapp-name}
开发完成!你可以通过以下方式访问:
  • amis组件方式:在amis Schema中使用
    "type": "{webapp-name}"
    嵌入到任意页面
  • 独立SPA方式:通过浏览器访问
    {ROOT_URL}/api/{package-name}/{webapp-name}
  • 开发模式:运行
    cd webapps/{webapp-name} && npm run dev
    ,访问
    http://localhost:5173

FAQ | 常见问题

常见问题

Q: Why can't React be bundled into the IIFE? A: amis SDK ships its own React. Bundling two copies causes Hooks errors and broken Context sharing. Must use
amisRequire("react")
to reuse amis's React.
Q: Third-party lib imports
react/jsx-runtime
?
A:
amis-jsx-shim.ts
delegates
jsx()
/
jsxs()
to
React.createElement()
. Map both
react/jsx-runtime
and
react/jsx-dev-runtime
in Vite aliases.
Q:
process is not defined
error?
A: Some dependencies reference
process.env.NODE_ENV
. Add to Vite
define
:
typescript
define: {
  'process.env.NODE_ENV': JSON.stringify('production'),
  'process.env': JSON.stringify({}),
}
Q: Style conflicts with host page? A:
postcss-prefix-selector
+ root element class name. Exclude
:root
,
html
,
body
,
@keyframes
from prefixing.
Q: How to expose component methods to amis
getComponentById
?
A: Use
amisLib.ScopedContext
in the bridge component, register via
scoped.registerComponent()
, and mount methods like
getValue()
and
validate()
.
Q: How to handle antd? A: When host page loads antd via CDN, mark
antd
in rollup
external
. For dayjs locale (antd DatePicker dependency), handle it in
amis-entry.ts
.
问:为什么不能将React打包到IIFE中? 答:amis SDK自带React。打包两个副本会导致Hooks错误和Context共享失效。必须使用
amisRequire("react")
复用amis的React。
问:第三方库导入
react/jsx-runtime
怎么办?
答:
amis-jsx-shim.ts
jsx()
/
jsxs()
委托给
React.createElement()
。在Vite别名中映射
react/jsx-runtime
react/jsx-dev-runtime
问:出现
process is not defined
错误?
答:某些依赖引用
process.env.NODE_ENV
。在Vite的
define
中添加:
typescript
define: {
  'process.env.NODE_ENV': JSON.stringify('production'),
  'process.env': JSON.stringify({}),
}
问:与宿主页面样式冲突? 答:使用
postcss-prefix-selector
+ 根元素类名。排除
:root
html
body
@keyframes
添加前缀。
问:如何向amis的
getComponentById
暴露组件方法?
答:在桥接组件中使用
amisLib.ScopedContext
,通过
scoped.registerComponent()
注册,并挂载
getValue()
validate()
等方法。
问:如何处理antd? 答:当宿主页面通过CDN加载antd时,在rollup的
external
中标记
antd
。对于dayjs本地化(antd DatePicker依赖),在
amis-entry.ts
中处理。