外观
前端快速入门
2026-04-01
本文以新建一个业务子应用为目标,从零创建 IceStark 微前端子应用,完成页面开发,并接入 nebula-web-main 主应用。
全程约 30 分钟,读完即可独立开发新子应用。
目录
- 一、环境准备
- 二、monorepo 结构说明
- 三、创建子应用
- 四、配置 vite.config.ts
- 五、IceStark 生命周期入口
- 六、创建 API 请求实例
- 七、编写第一个页面
- 八、注册路由
- 九、接入主应用
- 十、本地联调
一、环境准备
| 工具 | 版本要求 |
|---|---|
| Node.js | 22+ |
| pnpm | 9+ |
| 浏览器 | Chrome / Edge(开发调试) |
# 确认版本
node -v # v22.x.x
pnpm -v # 9.x.x二、monorepo 结构说明
nebula-web 是一个 pnpm monorepo,所有子应用和公共包统一在一个仓库中管理:
nebula-web/
├── package.json # 根 workspace 配置
├── pnpm-workspace.yaml # 声明 apps/* 和 packages/* 为工作区
│
├── apps/
│ ├── nebula-web-main/ # 主应用(壳):登录、布局、菜单、微前端容器
│ ├── nebula-web-auth/ # 认证管理子应用(端口 3001)
│ ├── nebula-web-system/ # 系统管理子应用(端口 3002)
│ └── my-subapp/ # ← 你的新子应用
│
└── packages/
├── nebula-ui-types/ # 共享 TypeScript 类型(R<T>、PageResult<T> 等)
├── nebula-ui-utils/ # 共享工具(createRequest、Token 工具等)
└── nebula-ui-components/ # 共享业务组件库pnpm-workspace.yaml 内容:
packages:
- 'apps/*'
- 'packages/*'三、创建子应用
3.1 复制 nebula-web-system 作为模板
在 apps/ 目录下复制 nebula-web-system 文件夹,重命名为你的子应用名(如 my-subapp):
cp -r apps/nebula-web-system apps/my-subapp3.2 修改 package.json
编辑 apps/my-subapp/package.json:
{
"name": "my-subapp",
"version": "0.0.1",
"scripts": {
"dev": "vite --host 127.0.0.1 --port 3003",
"dev:remote": "vite --host 127.0.0.1 --port 3003 --mode remote",
"build": "vue-tsc -b && vite build",
"preview": "vite preview",
"typecheck": "vue-tsc --build --force"
}
}端口约定: 主应用
3000,auth 子应用3001,system 子应用3002,新子应用从3003开始递增。
3.3 在根 workspace 安装依赖
# 在 nebula-web 根目录执行
pnpm install四、配置 vite.config.ts
apps/my-subapp/vite.config.ts 的关键配置项:
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig(({ mode }) => {
const isProd = mode === 'production'
return {
// 生产环境 base 路径与主应用中注册的 activePath 保持一致(加 / 后缀)
base: isProd ? '/myapp/' : '/',
plugins: [
vue(),
AutoImport({
imports: ['vue', 'vue-router', 'pinia'],
resolvers: [ElementPlusResolver()],
dts: 'src/types/auto-imports.d.ts',
}),
Components({
resolvers: [ElementPlusResolver()],
dts: 'src/types/components.d.ts',
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
port: 3003,
host: '127.0.0.1',
cors: true,
headers: {
'Access-Control-Allow-Origin': '*',
},
proxy: {
// 开发时代理后端接口,去掉 /my-service 前缀后转发
'/my-service': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: path => path.replace(/^\/my-service/, ''),
},
},
},
build: {
target: 'es2015',
rollupOptions: {
// 同时声明 src/main.ts 为 Rollup 入口,确保 IceStark import 模式下 mount/unmount 能被导出
input: {
index: fileURLToPath(new URL('./index.html', import.meta.url)),
main: fileURLToPath(new URL('./src/main.ts', import.meta.url)),
},
preserveEntrySignatures: 'exports-only',
},
},
}
})五、IceStark 生命周期入口
apps/my-subapp/src/main.ts 是微前端的核心文件,必须导出 mount 和 unmount:
import { createApp, type App as VueApp } from 'vue'
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import 'element-plus/dist/index.css'
import isInIcestark from '@ice/stark-app/lib/isInIcestark'
import getBasename from '@ice/stark-app/lib/getBasename'
import setLibraryName from '@ice/stark-app/lib/setLibraryName'
import { createRouter, createWebHistory } from 'vue-router'
import { createPinia } from 'pinia'
import AppComponent from './App.vue'
import routes from './router/routes'
// 全局唯一库名,必须与主应用 microAppConfig 中的配置一致
setLibraryName('mySubapp')
let vueApp: VueApp | null = null
function createVueApp(basename: string) {
const router = createRouter({
// 在 IceStark 环境中以 activePath 为 base,独立开发时为 /
history: createWebHistory(basename),
routes,
})
const app = createApp(AppComponent)
app.use(createPinia())
app.use(router)
app.use(ElementPlus, { locale: zhCn })
return { app, router }
}
// 供 IceStark 调用:挂载子应用
export function mount({ container }: { container: HTMLElement }) {
if (vueApp) {
vueApp.unmount()
vueApp = null
}
const { app } = createVueApp(getBasename())
vueApp = app
app.mount(container)
}
// 供 IceStark 调用:卸载子应用
export function unmount() {
if (vueApp) {
vueApp.unmount()
vueApp = null
}
}
// 生产环境 import 模式兜底:将生命周期挂到 window 上
;(window as any).mySubapp = { mount, unmount }
// 独立开发模式(不在 IceStark 环境中)
if (!isInIcestark()) {
const { app } = createVueApp('/')
app.mount('#app')
}六、创建 API 请求实例
apps/my-subapp/src/api/index.ts:
import { createRequest } from '@nebula-web/utils'
// VITE_MY_API_BASE 为空时回退到 /my-service 前缀
// Vite proxy 会将 /my-service/** 转发到后端(去掉前缀)
export const myRequest = createRequest(
import.meta.env.VITE_MY_API_BASE || '/my-service'
)createRequest 内置能力(无需业务层重复处理):
- 请求拦截:自动注入
Authorization: Bearer {token} - 响应拦截:
code === 200时自动解包,直接拿到data(不是R<T>) - 401 处理:自动静默刷新 Token,失败后触发跳转登录
七、编写第一个页面
在 apps/my-subapp/src/pages/demo/ 下创建 index.vue:
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { myRequest } from '@/api/index'
import type { PageResult } from '@nebula-web/types'
// 类型定义
interface DemoItem {
id: number
demoName: string
demoCode: string
status: number
createTime: string
}
// 查询参数
const queryForm = ref({ pageNo: 1, pageSize: 10, demoName: '' })
// 分页数据
const pageData = ref<PageResult<DemoItem>>({
records: [], total: 0, pageNo: 1, pageSize: 10, pages: 0,
})
const loading = ref(false)
// 加载数据
async function loadPage() {
loading.value = true
try {
pageData.value = await myRequest.post('/my/demo/page', queryForm.value)
} catch (err: any) {
ElMessage.error(err.message)
} finally {
loading.value = false
}
}
// 删除
async function handleDelete(ids: number[]) {
await ElMessageBox.confirm('确认删除所选数据?', '提示', { type: 'warning' })
await myRequest.delete('/my/demo', { data: ids })
ElMessage.success('删除成功')
loadPage()
}
onMounted(loadPage)
</script>
<template>
<div class="demo-page">
<el-card>
<!-- 查询表单 -->
<el-form :model="queryForm" inline>
<el-form-item label="名称">
<el-input v-model="queryForm.demoName" placeholder="请输入名称" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadPage">查询</el-button>
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table :data="pageData.records" v-loading="loading" stripe>
<el-table-column prop="demoName" label="名称" />
<el-table-column prop="demoCode" label="编码" />
<el-table-column prop="createTime" label="创建时间" />
<el-table-column label="操作" width="120">
<template #default="{ row }">
<el-button type="danger" size="small" @click="handleDelete([row.id])">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryForm.pageNo"
v-model:page-size="queryForm.pageSize"
:total="pageData.total"
@change="loadPage"
layout="total, sizes, prev, pager, next"
/>
</el-card>
</div>
</template>八、注册路由
apps/my-subapp/src/router/routes.ts:
import type { RouteRecordRaw } from 'vue-router'
/**
* 路径不含 /myapp 前缀。
* IceStark 通过 getBasename() 返回 activePath(/myapp)作为 createWebHistory 的 base,
* Vue Router 自动将 base 从 URL 中剥除后再匹配路由。
*
* 浏览器 URL: /myapp/demo → Vue Router 匹配: /demo
*/
const routes: RouteRecordRaw[] = [
{ path: '/', redirect: '/demo' },
{
path: '/demo',
name: 'Demo',
component: () => import('@/pages/demo/index.vue'),
meta: { title: '示例管理' },
},
]
export default routes九、接入主应用
9.1 在 nebula-system 数据库中添加菜单
在 nebula-system-server/src/main/resources/db/migration/ 中新增脚本:
-- V1.x.x__init_my_service_menu.sql
-- 一级目录(示例)
INSERT INTO sys_menu (id, parent_id, menu_type, menu_name, perms, path, component, sort_order, visible, status)
VALUES (50000, 0, 'M', '我的业务', '', '/myapp', '', 20, 1, 1);
-- 功能菜单
INSERT INTO sys_menu (id, parent_id, menu_type, menu_name, perms, path, component, sort_order, visible, status)
VALUES (50001, 50000, 'C', '示例管理', 'my:demo:query', '/myapp/demo', 'myapp/demo', 1, 1, 1);路径规则:
path为浏览器完整路径(/myapp/demo),component为子应用中的相对路由(myapp/demo,主应用识别myapp前缀来判断由哪个子应用渲染)。
9.2 在主应用配置子应用信息
编辑 apps/nebula-web-main/src/config/microApps.ts(或对应的微应用配置文件),新增:
{
name: 'mySubapp', // 与 setLibraryName 一致
activePath: '/myapp', // 路径前缀,菜单 path 以此开头时激活子应用
entry: import.meta.env.VITE_MY_APP_ENTRY ?? 'http://127.0.0.1:3003',
libraryName: 'mySubapp',
loadScriptMode: 'import',
}十、本地联调
10.1 启动后端服务
# 启动 nebula-system(端口 8081)
# 启动 my-service(端口 8080)10.2 独立运行子应用(不依赖主应用)
# 在 nebula-web 根目录
pnpm --filter my-subapp dev
# 或进入子应用目录
cd apps/my-subapp
pnpm dev访问 http://127.0.0.1:3003/demo 验证页面是否正常渲染和接口是否通。
10.3 配置主应用环境变量
在 apps/nebula-web-main/.env.development 中新增:
# 子应用入口地址(开发时指向本地)
VITE_MY_APP_ENTRY=http://127.0.0.1:3003
# 如果主应用需要代理到 my-service 后端
VITE_MY_BACKEND=http://localhost:808010.4 启动主应用
pnpm --filter nebula-web-main dev访问 http://127.0.0.1:3000,登录后通过菜单导航到「示例管理」,验证子应用在微前端容器中正常加载。
下一步
- 阅读 前端开发规范,了解 API 函数写法、TypeScript 类型定义规范
- 阅读 完整 CRUD 代码示例,获取前后端完整对照模板
- 阅读 nebula-web 微前端架构说明,了解 IceStark 主子应用通信机制
