普通视图

发现新文章,点击刷新页面。
今天 — 2025年4月13日首页

后端1小时上手vue开发到上线

作者 dmy
2025年4月13日 16:46

做为一个后端,很多时候想实现一个完整的功能产品,奈何前端技术太菜,开发时总感觉力不从心;随着ai技术的快速发展,让我们有能力快速开发自己的产品,因此掌握一些前端技术显得非常必要。

一番了解后,大概知道vue入门非常简单,另外它同时也可以支持小程序app的开发,基本还是比较全面,今天我们一起开始学习它吧。

不同于常规的教程,从头开始一个个知识点讲解,我们要的是整体感知,知道它是怎么玩的。;因此我们主要从整体的角度的去做一个vue项目,结合ui框架(后端多半都不太会写css,非常有必要),然后部署上线去做,相信把这一切看完后,你会有一个感受——原来是这么玩的

我们是从整体角度出发,因此知识点都是带一下,不会涉及过分细节。好啦,说了这么多,让我们开始吧。

一、环境准备

首先,先确保您的本地电脑有安装node.js,它是一个javscript运行环境,前端开发都需要装它。 使用node -v,如果有输出,则安装好了。 另外它还自带一个包管理器npm,可以通过通过npm -v查看版本。

在包管理方面,pnpmnpm更好,因此我们采用pnpm管理包,通过npm install -g pnpm安装 pnmp

安装完后pnpm -v验证安装成功。

好啦,准备就绪,开始!!!

二、项目准备

切换到一个你自己的开发目录下,执行pnpm create vue@latest

pnpm create vue@latest
┌  Vue.js - The Progressive JavaScript Framework
│
◇  Project name (target directory):
│  learn-vue
│
◆  Select features to include in your project: (↑/↓ to navigate, space to select, a to toggle all, enter to confirm)
│  ◼ TypeScript
│  ◻ JSX Support
│  ◼ Router (SPA development)
│  ◻ Pinia (state management)
│  ◻ Vitest (unit testing)
│  ◻ End-to-End Testing
│  ◼ ESLint (error prevention)
│  ◻ Prettier (code formatting)
└

它会有些提示,

  • project name 项目名,我这里写的learn-vue,你随意。
  • 一些选择我选择,TypeScriptRouter(路由)、ESLint(更好的检查提示)。

选好后,enter

pnpm create vue@latest
┌  Vue.js - The Progressive JavaScript Framework
│
◇  Project name (target directory):
│  learn-vue
│
◇  Select features to include in your project: (↑/↓ to navigate, space to select, a to toggle all, enter to confirm)
│  TypeScript, Router (SPA development), ESLint (error prevention)
│
◆  Install Oxlint for faster linting? (experimental)
│  ○ Yes / ● No
└

然后问你要安装Oxlint不,直接回车就行。

然后你将看到

pnpm create vue@latest
┌  Vue.js - The Progressive JavaScript Framework
│
◇  Project name (target directory):
│  learn-vue
│
◇  Select features to include in your project: (↑/↓ to navigate, space to select, a to toggle all, enter to confirm)
│  TypeScript, Router (SPA development), ESLint (error prevention)
│
◇  Install Oxlint for faster linting? (experimental)
│  No

Scaffolding project in /Users/dongmingyan/vue/learn-vue...
│
└  Done. Now run:

   cd learn-vue
   pnpm install
   pnpm dev

| Optional: Initialize Git in your project directory with:
   
   git init && git add -A && git commit -m "initial commit"

说明这个vue自定义的脚手架项目都搭建起来了,照着上面的提示执行

cd learn-vue
pnpm install
pnpm dev

成功后您会看到

pnpm dev

> learn-vue@0.0.0 dev /Users/dongmingyan/vue/learn-vue
> vite


  VITE v6.2.6  ready in 1204 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  Vue DevTools: Open http://localhost:5173/__devtools__/ as a separate window
  ➜  Vue DevTools: Press Option(⌥)+Shift(⇧)+D in App to toggle the Vue DevTools
  ➜  press h + enter to show help

在浏览器访问http://localhost:5173/发现都运行起来了。 image.png 一切正常符合我们的预期。

三、了解目录结构

前面我们通过vue的脚手架,帮我们生成了一个vue的项目,但是我们完全不知道这个项目的结构,因此我们先来看下。

tree -L 2
.
├── README.md
├── env.d.ts
├── eslint.config.ts
├── index.html
├── node_modules
│   ├── @tsconfig
│   ├── @types
│   ├── @vitejs
│   ├── @vue
│   ├── eslint -> .pnpm/eslint@9.24.0_jiti@2.4.2/node_modules/eslint
│   ├── eslint-plugin-vue -> .pnpm/eslint-plugin-vue@10.0.0_eslint@9.24.0_jiti@2.4.2__vue-eslint-parser@10.1.3_eslint@9.24.0_jiti@2.4.2__/node_modules/eslint-plugin-vue
│   ├── jiti -> .pnpm/jiti@2.4.2/node_modules/jiti
│   ├── npm-run-all2 -> .pnpm/npm-run-all2@7.0.2/node_modules/npm-run-all2
│   ├── typescript -> .pnpm/typescript@5.8.3/node_modules/typescript
│   ├── vite -> .pnpm/vite@6.2.6_@types+node@22.14.1_jiti@2.4.2/node_modules/vite
│   ├── vite-plugin-vue-devtools -> .pnpm/vite-plugin-vue-devtools@7.7.2_rollup@4.40.0_vite@6.2.6_@types+node@22.14.1_jiti@2.4.2__vue@3.5.13_typescript@5.8.3_/node_modules/vite-plugin-vue-devtools
│   ├── vue -> .pnpm/vue@3.5.13_typescript@5.8.3/node_modules/vue
│   ├── vue-router -> .pnpm/vue-router@4.5.0_vue@3.5.13_typescript@5.8.3_/node_modules/vue-router
│   └── vue-tsc -> .pnpm/vue-tsc@2.2.8_typescript@5.8.3/node_modules/vue-tsc
├── package.json
├── pnpm-lock.yaml
├── public
│   └── favicon.ico
├── src
│   ├── App.vue
│   ├── assets
│   ├── components
│   ├── main.ts
│   ├── router
│   └── views
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts

下面大概说明下:

  • main.ts 入口文件,这和其它语言(比如go)一样,凡是main的都是一个入口。
  • node_modules 目录 是项目安装了包存放位置,我们不用管它(提交代码仓库时,也不要提交上去,它是根据package.json执行包安装pnpm install自动装好的。
  • index.html 是vue的主页面(home)文件
  • public 主要用于存放图片、logo等等
  • src 目录 是我们着重注意的目录,基本开发都是在这里
  • pnpm-lock.yaml主要用于存放包的具体版本文件,需要提交到git
  • tsconfig.json 主要是typescript的配置文件,可以先不用关注
  • vite.config.ts vite的配置文件(控制应用如何启动、支持热更新-修改代码立即生效)

四、细看如何运作

前面我们看了目录结构是啥,但是还没看一个vue的各个部分是如何协作的。

  • main.ts
import './assets/main.css'

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

const app = createApp(App)

app.use(router)

app.mount('#app')

从入口看,这个文件就是从vue中导入了createAppApp我们初始化了一个app,然后在app上安装了路由(router) 最后一行app.mount('#app') 是把这个vue app挂载到index.html页面的一个叫app的id属性上了。

顺着我们去看下App.vue,这里内容稍多,我们简化着看

<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
import HelloWorld from './components/HelloWorld.vue'
</script>

<template>
  <header>
    <img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" />

    <div class="wrapper">
      <HelloWorld msg="You did it!" />

      <nav>
        <RouterLink to="/">Home</RouterLink>
        <RouterLink to="/about">About</RouterLink>
      </nav>
    </div>
  </header>

  <RouterView />
</template>

<style scoped>
</sytle>

整体上三个部分

  • <script> —— typeScript,这里导入了路由、和HelloWorld组件
  • template 模版,这在vue中是一个特殊的部分,一个组件最多只能有一个template模版,它代表的是html部分
  • style 当然是样式部分,你看它内部带了一个scoped,它代表样式只在当前vue文件生效

<HelloWorld msg="You did it!" /> 这里是组件的使用,看起来和我们使用标准的html标签查不多,它是自闭合的。这里的msg属性,是向HelloWorld组件中传递的数据,先了解即可。

然后我们可以看到,

<nav>
  <RouterLink to="/">Home</RouterLink>
  <RouterLink to="/about">About</RouterLink>
</nav>

这里使用了vue的RouterLink组件,放置了两个路由HomeAbount

我看再看下路由router/index.ts

import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView,
    },
    {
      path: '/about',
      name: 'about',
      // route level code-splitting
      // this generates a separate chunk (About.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () => import('../views/AboutView.vue'),
    },
  ],
})

这里定义了两个路由 homeabount,也就是在App.vue中使用的路由

我们看到这里大概就可以知道main.ts 引用 App.vueApp.vue引用了HelloWorld.vue本质上vue引用就是由一些列组件组合而成页面构成。

vue给我们提供了一些特殊的组件,比如路由/状态管理等等,方便我们使用。

五、动动手

理论是枯燥的,我们还是要动手自己去练练。

1. login页面

假设我们要一个登录页面,首先,我们添加一个路由在router/index.ts中添加

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    // ...
    {
      path: '/login',
      name: 'login',
      component: () => import('../views/LoginView.vue'),
    },
  ],
})

然后我们去添加登录页面,views/LoginView.vue

<template>
  <div>
    <h1>Login</h1>
  </div>
</template>

<script setup lang="ts">
</script>

<style scoped>
</style>

然后在App.vue中把链接加上

<nav>
  <RouterLink to="/">Home</RouterLink>
  <RouterLink to="/about">About</RouterLink>
  <RouterLink to="/login">Login</RouterLink><!-- 登录 -->
</nav>

然后在首页就能看到登录按钮,可以点击,也能进入登录页面,说明我们成功了。

2. 美化完成

现在登录页虽然实现了,但是就一个标题太low了,让我们把输入框和按钮加上。

重写login.vue

<template>
  <div class="login-container">
    <div class="login-box">
      <h1>登录</h1>
      <div class="form-item">
        <label for="username">用户名</label>
        <input 
          type="text" 
          id="username" 
          v-model="username" 
          placeholder="请输入用户名"
        />
      </div>
      <div class="form-item">
        <label for="password">密码</label>
        <input 
          type="password" 
          id="password" 
          v-model="password" 
          placeholder="请输入密码"
        />
      </div>
      <button @click="handleLogin" class="login-button">登录</button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'

const router = useRouter()
const username = ref('')
const password = ref('')

const handleLogin = () => {
  if (!username.value || !password.value) {
    alert('请输入用户名和密码')
    return
  }
  
  // 这里应该加入真实的登录逻辑
  console.log('登录信息:', username.value, password.value)
  
  // 模拟登录成功
  alert('登录成功')
  // 登录成功后跳转到首页
  router.push('/')
}
</script>

<style scoped>
.login-container {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  background-color: #f5f5f5;
}

.login-box {
  width: 350px;
  padding: 30px;
  background-color: white;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

h1 {
  text-align: center;
  margin-bottom: 24px;
  color: #333;
}

.form-item {
  margin-bottom: 16px;
}

label {
  display: block;
  margin-bottom: 6px;
  font-weight: bold;
  color: #555;
}

input {
  width: 100%;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 14px;
  box-sizing: border-box;
}

input:focus {
  border-color: #40a9ff;
  outline: none;
  box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}

.login-button {
  width: 100%;
  padding: 12px;
  background-color: #1890ff;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 16px;
  cursor: pointer;
  margin-top: 10px;
}

.login-button:hover {
  background-color: #40a9ff;
}

.login-button:active {
  background-color: #096dd9;
}
</style>

然后看我们登录页面,如图:

image.png 高大上了不少。

下面重点说下这里的知识点,

  • 值初始化
import { ref } from 'vue'
// ref 是vue提供用于初始化值的工具 ref 可以初始化任意类型的值 使用是以.value方式获取值

const username = ref('') // 这里初始化username 为空字符串
const password = ref('') // 密码同理
  • 双向绑定
<input 
  type="text" 
  id="username" 
  v-model="username" 
  placeholder="请输入用户名"
/>

这里的v-model="username" 是vue中用于提供双向绑定(输入框中值和外部js中username同步)输入框值的方式,这里username相当于一个变量。

  • 事件触发
<button @click="handleLogin" class="login-button">登录</button>

这里@click="handleLogin" @click是vue的事件触发,里面的handleLogin点击时执行的函数。

const handleLogin = () => {
  if (!username.value || !password.value) {
    alert('请输入用户名和密码')
    return
  }
  
  // 这里应该加入真实的登录逻辑
  console.log('登录信息:', username.value, password.value)
  
  // 模拟登录成功
  alert('登录成功')
  // 登录成功后跳转到首页
  router.push('/')
}

上面我们直接通过username.value就能获取到输入框中username的值是多少,是不是很方便啊。

vue最大的好处就是把数据和html之间的绑定做的特别方便,如果我们手动去写javascript获取值,分配值会特别麻烦,这也是vue最大的好处。

当然,vue还有很多非常棒的特性(比如计算属性、循环、判断、状态管理...),限于篇幅我们不可能一一介绍,这里有一个感知就行。

3. 引入ui库

自己手动写很多html、css还是很麻烦的,我们直接用现成的ui框架就好何必自己写呢,对不对。

考虑到大部分时需要使用移动端,因此我们引入vant ui它对移动端很友好。

  • 安装
pnpm install vant 
  • 引入css(main.ts)中
import './assets/main.css'
  • 添加vantTest.vue页面和路由 vantTest页面内容
<template>
  <div class="van-test">
    <CellGroup title="分组1" inset>
      <Cell title="单元格" value="内容" />
      <Cell title="单元格" value="内容" label="描述信息1" />
    </CellGroup>

    <CellGroup title="分组2" inset>
      <Cell title="单元格" value="内容" />
      <Cell title="单元格" value="内容" label="描述信息2" />
    </CellGroup>
  </div>
</template>

<script setup lang="ts">
import { Cell, CellGroup } from 'vant'
</script>

<style scoped>
  .van-test {
  display: flex;
  height: 100vh;
  background-color: #f5f5f5;
  flex-direction: column;
}
</style>  

image.png 看到了吧 引入成功了 自带了cell样式。

六、部署

现在我们了解了基本的开发了,我们把代码发布到线上。

vercel 提供了很好的支持,用它发布也非常简单。

首先,把我们的代码推到我们的github仓库。

  1. vercel用github账号注册
  2. import github上项目,这里需要把github上的仓库开发给vercel

image.png 3. 部署(非常快,不到一分钟)

image.png

成功啦

image.png

它自动给你分配域名,都可以访问了哦。

总算完结啦。

七. 最后

我们已完成了整个项目的搭建到部署,后续就靠你啦,都是细节,哈哈哈。

  1. vue 官网 cn.vuejs.org/guide/quick…
  2. vant UI 官网 vant-ui.github.io/vant/#/zh-C…

解决【vite-plugin-top-level-await】 插件导致的 Bindings Not Found 错误

作者 coding随想
2025年4月13日 10:13

解决【vite-plugin-top-level-await】 插件导致的 Bindings Not Found 错误

环境设置

  • 操作系统: macOS
  • 硬件平台: M1 Pro
  • 前端框架: Vue 3
  • Node.js 版本: 20

在使用 Vue 项目时,我们尝试集成 vite-plugin-top-level-await 插件以支持顶层 await 语法。然而,在构建过程中遇到了一个错误提示:“Bindings not found”。

具体错误信息如下:

error during build:
Error: Bindings not found.
    at Compiler.<anonymous> (/Users/xxx/modules/@swc/core/index.js:125:23)

起初,此错误被误认为是与 @swc/core 包有关的问题。但实际上,问题的根源在于缺少了 vite-plugin-top-level-await 插件。

解决方案

要解决这个问题,只需安装 vite-plugin-top-level-await 插件作为开发依赖项即可:

npm install --save-dev vite-plugin-top-level-await

确保该插件已正确安装并添加到项目的依赖中后,重新运行构建过程。这样应该可以解决“Bindings not found”的错误,并允许你的 Vue 项目正常编译和运行。

请记得检查项目的 vite.config.js 文件,确保已经适当地配置了 vite-plugin-top-level-await 插件。如果需要进一步的帮助,请参考插件的官方文档或社区资源。

Vite模块联邦(vite-plugin-federation)实现去中心化微前端后台管理系统架构

作者 skyWang416
2025年4月12日 17:36

一、项目概述

1.1 前言

在现代大型前端项目开发中,多团队协作时往往面临代码隔离与集成的挑战。为了解决这一问题,我们需要一种能够让各个微前端模块既能独立开发部署,又能作为完整系统一部分的解决方案。基于Vite的模块联邦插件@originjs/vite-plugin-federation提供了一种去中心化的微前端架构实现方式,实现了组件、路由的跨应用共享和动态加载。本文将结合实际项目经验,详细介绍如何利用模块联邦技术构建真正去中心化的微前端架构。

1.2 依赖版本要求

node: 18.20.5

{
  "vite": "^6.0.2",
  "@originjs/vite-plugin-federation": "1.4.1",
}

二、架构设计

2.1 模块联邦基础概念

2.1.1 什么是模块联邦

@originjs/vite-plugin-federation是Vite生态中实现模块联邦功能的插件,它允许多个独立应用在运行时共享模块和代码,无需重复构建或打包依赖。这一特性在去中心化微前端架构中尤为关键。

2.1.2 基本配置示例

一个微前端模块的典型配置如下:

federation({
  name: "pageAModule",        // 微前端模块名称
  filename: "pageAEntry.js",  // 入口文件名
  exposes: {                  // 暴露的模块,此处为路由
    './routes': './src/routes/index.ts'
  },
  remote: {
    menuModule: ModuleUrl, // 引入的远程模块
  }
  shared: ['vue', 'vue-router', 'pinia']  // 共享依赖
})
// 使用远程模块
import 'menuModule/xxx'

2.2 去中心化微前端架构设计

2.2.1 传统微前端 vs 去中心化微前端

传统微前端架构通常采用"基座模式",即一个主应用作为基座控制其他子应用的加载和渲染。而去中心化微前端架构没有明确的主应用,各个子应用可以独立部署、独立运行,同时又能无缝协作。

去中心化微前端架构的主要特点:

  1. 无中心基座:没有固定的主应用控制全局状态
  2. 平等协作:每个应用都可作为入口,并动态加载其他应用模块
  3. 路由共享机制:所有微前端模块共享路由表,实现无缝导航
  4. 共享运行时:应用间共享关键依赖和状态

image.png

image.png

image.png

2.3 特殊架构设计:menuModule的角色

虽然我们采用去中心化的微前端架构,但在后台管理系统中,我们仍然需要一个统一的菜单管理模块(menuModule)。来提供系统的layout和管理一些公共文件。

2.3.1 为什么需要menuModule?

  • 统一的菜单管理:后台管理系统需要一个统一的菜单体系,确保用户体验的一致性
  • 权限控制中心:集中管理各个页面模块的访问权限
  • 导航状态维护:统一处理菜单的激活状态、展开状态等
  • 系统级功能集成:包含用户信息、全局设置等系统级功能

2.3.2 menuModule的特殊定位

menuModule模块需要在所有的页面项目中引入,且所有项目在vite的federation中都只需引入这一个menuModule模块就行。 其他剩余的模块引入方式,在下文有提到。

menuModule承担以下职责:

  • 提供统一的布局容器(Layout)
  • 管理全局导航菜单
  • 处理用户认证和权限
  • 提供公共组件和工具函数

三、技术实现

3.1 模块联邦配置详解

在实际项目中,每个页面的微前端模块的联邦配置通常包含以下部分:

federation({
  name: config.projectPrefix + 'Module',      // 微前端模块名称
  filename: config.projectPrefix + 'Entry.js', // 入口文件名
  exposes: {
    './routes': './src/routes/index.ts',      // 暴露路由配置(关键)
  },
  remotes: {
    // 后台管理系统中需要单独引入菜单模块
    menuModule: ModuleUrl,
  },
  shared: ['vue', 'vue-router', 'pinia', 'dayjs', 'axios', 'sass', 'element-plus'], // 共享依赖
})

3.1.1 关键配置参数解析

  1. name:微前端模块的唯一标识符,其他模块通过此名称引用
  2. filename:构建后的入口文件名,通常以Entry.js结尾
  3. exposes:暴露的模块,主要是路由配置,这是实现去中心化的关键
  4. remotes:需要引用的其他微前端模块,指定模块的URL或加载方式
  5. shared:所有微前端模块间共享的依赖,确保单一实例,避免重复加载

3.2 构建时处理

在构建微前端模块时,vite-plugin-federation会进行以下处理:

  1. 识别exposes配置中声明的路由模块
  2. 为每个模块生成独立的构建产物
  3. 创建容器模块,管理模块的导出和依赖关系
  4. 生成远程入口文件,处理模块的加载和解析

四、动态模块的导入与路由整合

4.1 传统导入方式的局限

传统的模块联邦使用方式是预先声明远程模块并直接导入:

// 常规的模块联邦导入方法
import Component from 'remoteModule/Component'

这种方式存在一个重要限制:无法使用动态字符串拼接模块名。例如,以下代码在模块联邦中不起作用:

// 这种写法在模块联邦中不支持
const moduleName = 'remoteModule'
import Component from `${moduleName}/Component` // 不支持!

这样就会导致一个问题就是 在去中心化微前端架构中,每个项目模块在开发时并不知道全局系统中到底有多少个联邦模块,也无法预先确定所有模块的名称和地址。为了支持新增模块的灵活扩展,需要一个动态的机制来发现和加载模块。通常,我们会通过一个配置文件(如 registry.json)来集中管理所有联邦模块的注册信息,允许新模块随时加入系统。然而,官方的模块联邦导入方式,它不支持动态拼接模块名称的字符串。

为了解决这一问题,我实现了一个支持动态拼接模块名称的加载函数 getRemoteEntries。通过该函数,我们可以在运行时根据配置文件动态获取模块的 URL 并加载模块,从而实现真正的动态模块发现和集成。这种方式不仅解决了官方导入方式的限制,还为系统的扩展性和灵活性提供了强有力的支持。

4.2 动态模块实现

为解决上述限制,我实现了getRemoteEntries函数,通过@originjs/vite-plugin-federationgetAPI实现,运行时动态的模块声明和加载:

// src/utils/federation.ts
export const getRemoteEntries = async (name?: string, moduleName?: string): Promise<any[]> => {
  try {
    // 从注册中心获取所有可用模块的信息
    const response = await axios.get(`${baseUrl}/federation/registry.json`)

    // 运行时根据条件筛选模块
    const filteredModules = response.data.filter(module => !name || module.name.includes(name))

    // 动态加载匹配的模块
    const loadedComponents = []
    for (const moduleInfo of filteredModules) {
      // 动态构建模块URL
      const moduleUrl = buildModuleUrl(moduleInfo)

      // 使用模块联邦API动态加载模块
      const remote = await import(/* @vite-ignore */ moduleUrl)
      const moduleFactory = await remote.get('./' + moduleName)
      const module = await moduleFactory()

      loadedComponents.push(module)
    }

    return loadedComponents
  } catch (error) {
    console.error('获取远程模块失败:', error)
    return []
  }
}

这种动态模块发现机制的优势在于:

  1. 运行时集成:应用可以在运行时发现并加载其他模块
  2. 可扩展性:新模块可以随时添加到系统中,无需修改全部现有模块
  3. 自动注册:新增的微前端模块自动成为整体系统的一部分
  4. 动态模块名:支持通过字符串拼接方式构建模块URL

4.3 路由表的共享与整合

共享路由表是去中心化架构的核心,使每个模块都能作为独立入口。在我们的实现中,路由整合分两步进行:

  1. 路由收集:收集所有微前端模块的路由配置
  2. 路由合并:将所有路由合并到主布局路由下
// src/routes/index.ts
export const createRouterInstance = async () => {
  // 存储布局路由
  let layoutRoute: RouteRecordRaw | undefined

  if (ENV_TYPE === 'federation') {
    // 获取远程入口配置
    const remoteRoutes = await getRemoteEntries('', 'routes')
    // 找到Layout路由并合并
    layoutRoute = mergeLayoutChildren(remoteRoutes || [])
  }

  // 创建路由实例
  const router = createRouter({
    history: createWebHashHistory(),
    routes: ENV_TYPE === 'federation' ? (layoutRoute as any).default : baseRoutes,
  })

  // 全局前置守卫
  router.beforeEach(async (to, from, next) => {
    // 路由守卫逻辑...
    next()
  })

  return router
}

4.4 路由合并核心逻辑

路由合并的核心实现在mergeLayoutChildren函数中,它实现了以下功能:

  1. 识别布局路由:找到包含主布局的路由模块
  2. 处理独立页面:将需要独立展示的页面(outLayout)提取出来
  3. 嵌套路由合并:将所有子模块的路由合并到主布局的children中
  4. 顶层路由保留:保留顶层路由如登录页、404页面等
// 合并路由
const mergeLayoutChildren = (data: RouteRecordRaw[]) => {
  let layoutIndex = -1 // 添加变量记录Layout的索引
  const outLayoutItems: { moduleIndex: number; route: any }[] = [] // 存储所有outLayoutItem及其位置

  // 首先处理 baseRoutes 中的 outLayout 路由
  const baseOutLayoutRoutes = baseRoutes.filter((route) => route.meta?.outLayout)
  baseOutLayoutRoutes.forEach((route) => {
    outLayoutItems.push({ moduleIndex: -1, route })
  })

  // 遍历远程数据处理布局路由和独立路由
  data.forEach((item, index) => {
    const defaultRoutes = (item as any).default
    const layoutItem = defaultRoutes.find((route: any) => route.name === 'Layout')
    const outLayoutItem = defaultRoutes.find((route: any) => route.meta?.outLayout)

    if (layoutItem) {
      layoutIndex = index // 记录Layout所在的索引
    }

    if (outLayoutItem) {
      outLayoutItems.push({ moduleIndex: index, route: outLayoutItem })
      // 从原数组中移除outLayoutItem避免重复
      const outLayoutIndex = defaultRoutes.findIndex((route: any) => route.meta?.outLayout)
      if (outLayoutIndex > -1) {
        defaultRoutes.splice(outLayoutIndex, 1)
      }
    }
  })

  // 获取Layout路由作为合并基础
  const layoutRoute = (data[layoutIndex] as any).default.find((route: any) => route.name === 'Layout')

  if (layoutRoute) {
    // 将所有outLayoutItem添加到data[layoutIndex].default中作为顶层路由
    outLayoutItems.forEach((item) => {
      ;(data[layoutIndex] as any).default.push(item.route)
    })

    if (layoutRoute.children) {
      // 将主应用中非outLayout的路由添加到Layout的children中
      const nonOutLayoutBaseRoutes = baseRoutes.filter((route) => !route.meta?.outLayout)
      layoutRoute.children.push(...nonOutLayoutBaseRoutes)

      // 遍历所有其他模块的路由添加到Layout的children中
      data.forEach((item, index) => {
        if (index !== layoutIndex) {
          const routes = (item as any).default
          if (Array.isArray(routes)) {
            layoutRoute.children.push(...routes)
          }
        }
      })
    }
  }

  return data[layoutIndex]
}

五、状态管理与模块通信

在去中心化微前端架构中,各模块间的状态管理和通信是一个关键问题。本项目采用了基于pinia持久化缓存的状态共享机制,主要通过src/stores/modules/cache.ts实现:

5.1 基于缓存的状态管理

我们使用了专门的缓存存储模块来管理跨模块的状态:

// stores/modules/cache.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
import config from '@/config'

interface CacheData {
  value: unknown
  expire?: number
}

export const useCacheStore = defineStore(
  'cache',
  () => {
    // 状态定义
    const prefix = ref<string>(config.cachePrefix)
    const cacheData = ref<Record<string, CacheData>>({})

    // 获取当前时间戳
    const getTime = (): number => {
      return Math.round(new Date().getTime() / 1000)
    }

    // 获取完整的键名
    const getKey = (key: string, customPrefix?: string): string => {
      return (customPrefix ?? prefix.value) + key
    }

    // 设置缓存数据
    const set = (key: string, value: unknown, expire?: number, customPrefix?: string) => {
      const fullKey = getKey(key, customPrefix)
      cacheData.value[fullKey] = {
        value,
        expire: expire ? getTime() + expire : undefined,
      }
    }

    // 获取缓存数据
    const get = (key: string, customPrefix?: string) => {
      const fullKey = getKey(key, customPrefix)
      const data = cacheData.value[fullKey]

      if (!data) return null

      if (data.expire && data.expire < getTime()) {
        remove(key, customPrefix)
        return null
      }

      return data.value
    }

    // 删除缓存数据
    const remove = (key: string, customPrefix?: string) => {
      const fullKey = getKey(key, customPrefix)
      delete cacheData.value[fullKey]
    }

    return {
      prefix,
      cacheData,
      set,
      get,
      remove,
      getKey,
      getTime,
    }
  },
  {
    persist: {
      storage: localStorage,
    },
  }
)

5.2 缓存模块的核心特性

缓存存储模块具有以下特点:

  1. 命名空间隔离:通过前缀机制,确保不同模块间缓存键名不冲突
  2. 过期时间控制:支持设置数据过期时间,自动清理失效数据
  3. 本地持久化:使用Pinia的持久化功能,确保页面刷新后状态不丢失
  4. 跨模块共享:所有微前端模块都可以访问同一缓存存储,实现数据共享

5.3 模块间通信方式

基于缓存存储的通信机制主要有以下几种模式:

  1. 直接状态共享:模块间通过相同的缓存键读写数据

    // 页面A中设置数据
    const cacheStore = useCacheStore()
    cacheStore.set('sharedData', { count: 1 })
    
    // 页面B中读取数据
    const cacheStore = useCacheStore()
    const data = cacheStore.get('sharedData')
    
  2. 基于过期时间的临时共享:对于临时性数据,设置合适的过期时间

    // 设置1小时过期的共享数据
    cacheStore.set('temporaryData', value, 3600)
    
  3. 跨应用的全局状态:通过特定前缀获取不同微前端的状态

    // 获取user微前端模块的状态
    cacheStore.set('userInfo', settings, undefined, 'user')
    
    // 读取user微前端模块的状态
    const settings = cacheStore.get('userInfo', 'user')
    

六、项目结构与配置

6.1 微前端模块文件架构


├── 菜单模块 (被当前模块引用)
│   ├── vite.config.ts (模块联邦配置)
│   ├── src/
│   │   ├── layout/
│   │   ├── routes/
│   │   │   └── index.ts (导出路由配置)
│   │   ├── views/ (页面组件)
│   │   ├── stores/ (状态管理)
│   │   └── utils/
│   │       └── federation.ts (模块加载器)
│   └── package.json
├── user模块(页面模块)
│   ├── vite.config.ts (模块联邦配置)
│   ├── src/
│   │   ├── routes/
│   │   │   └── index.ts (导出路由配置)
│   │   ├── login/ (登录组件)
│   │   ├── views/ (页面组件)
│   │   ├── stores/ (状态管理)
│   │   └── utils/
│   │       └── federation.ts (模块加载器)
│   └── package.json
├── xxx模块(页面模块)
│   ├── vite.config.ts (模块联邦配置)
│   ├── src/
│   │   ├── routes/
│   │   │   └── index.ts (导出路由配置)
│   │   ├── views/ (页面组件)
│   │   ├── stores/ (状态管理)
│   │   └── utils/
│   │       └── federation.ts (模块加载器)
│   └── package.json

6.2 完整的vite.config.ts示例

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { federation } from '@originjs/vite-plugin-federation'
import path from 'path'

import { config } from './config'

const baseUrl = process.env.VITE_APP_BASE_URL || ''

// 需要暴露的模块
const exposes: Record<string, string> = {
  // 暴露路由表,供其他应用使用
  './routes': './src/routes/index.ts',
}

export default defineConfig({
  // 基础配置
  base: baseUrl || './', // 通过构建脚本传入实际静态文件地址,所有联邦模块文件都会遵循这个地址
  // 插件配置
  plugins: [
    vue(),

    // 模块联邦配置
    federation({
      name: config.projectPrefix + 'Module', // 微前端模块名称
      filename: config.projectPrefix + 'Entry.js', // 入口文件名
      exposes, // 暴露的模块
      remotes: {
        // 只需单独引入menuModule,其他的模块走federation
        menuModule: ModuleUrl,
      },
      // 共享依赖配置
      shared: [
        'vue',
        'vue-router',
        'pinia',
        'dayjs',
        'axios',
        'sass',
      ],
    })
  ],
  // 解析配置
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  },
})

6.3 部署与注册机制

去中心化微前端架构采用集中式的模块注册机制,通过registry.json文件管理所有可用模块:

CDN/静态资源服务器
├── federation/
│   └── registry.json (模块注册信息)
│
├── front-pc-page/ (页面模块)
│   └── pageEntry.js (入口文件)
│
├── front-pc-menu/ (菜单模块)
    └── menuEntry.js (菜单模块入口)

registry.json示例:

[
  {
    "name": "pageModule",
    "path": "front-pc-page"
  },
  {
    "name": "menuModule",
    "path": "front-pc-menu"
  }
]

七、总结

@originjs/vite-plugin-federation为实现去中心化微前端架构提供了强大的技术基础。通过共享路由表机制,我们可以构建出真正去中心化、高度灵活且可扩展的微前端系统,使得不同团队可以独立开发、测试和部署自己的模块,同时保持系统整体的一致性和可用性。

❌
❌