阐述 Vue 在大型企业级应用中的架构实践,例如微前端、组件库管理和跨团队协作。

各位老铁,早上好!我是老码,今天跟大家聊聊 Vue 在大型企业级应用里那些不得不说的架构实践。咱们争取把“高大上”的概念讲得“接地气”,让大家听完就能抄家伙上阵。

一、Vue 在大型企业级应用中的挑战

大型企业级应用,规模大,复杂度高,团队成员多,这三大特点决定了用 Vue 做项目,会遇到一些单打独斗时不会遇到的问题。

  • 代码膨胀: 功能模块越来越多,代码量迅速增长,导致项目启动慢、打包慢、维护难。
  • 依赖混乱: 各个模块之间的依赖关系复杂,容易出现循环依赖,甚至导致项目崩溃。
  • 多人协作: 多个团队同时开发,代码风格不统一,组件命名冲突,沟通成本高。
  • 技术栈不统一: 不同团队可能有不同的技术偏好,导致项目技术栈混乱,维护成本增加。
  • 升级困难: 升级 Vue 版本或者引入新的依赖库,可能会影响到整个应用,风险较高。

解决这些问题,需要一套完善的架构实践,包括微前端、组件库管理、跨团队协作等等。下面咱们一个一个来啃。

二、微前端:化整为零,各个击破

微前端,顾名思义,就是把一个大型前端应用拆分成多个小型应用,每个应用都可以独立开发、独立部署、独立运行。这样,每个团队只需要负责自己的一小块,降低了项目的复杂度,提高了开发效率。

1. 为什么要用微前端?

  • 团队自治: 每个团队可以独立选择技术栈,不用受限于整个项目的技术栈。
  • 增量升级: 可以逐步升级应用,不用一次性升级整个应用,降低了升级风险。
  • 独立部署: 每个应用可以独立部署,提高了应用的可用性。
  • 代码复用: 可以将公共组件提取出来,供多个应用使用,提高了代码复用率。

2. 微前端的常见方案

  • Iframe: 最简单的微前端方案,但存在一些问题,比如:页面刷新会丢失状态、通信复杂、用户体验差。
  • Web Components: 将每个应用封装成 Web Components,然后通过 JavaScript 动态加载,但存在一些兼容性问题。
  • Single-SPA: 一个 JavaScript 微前端框架,可以集成各种前端框架,比如:Vue、React、Angular。
  • Module Federation: Webpack 5 提供的一种微前端方案,可以将不同的应用打包成独立的模块,然后通过 JavaScript 动态加载。

3. Single-SPA 实战

Single-SPA 允许你将多个 SPA 应用整合到一个页面中。咱们举个例子,假设我们有两个 Vue 应用:app1app2

  • 创建 Single-SPA 应用

首先,我们需要创建一个 Single-SPA 应用作为入口,它负责加载和卸载其他微应用。

npm install -g create-single-spa
create-single-spa

选择如下选项:

? Directory for your project: my-org
? npm or yarn: npm
? Would you like to use TypeScript? No
? Would you like to use single-spa-layout for routing? No
? Which framework would you like to use for your root application? Vanilla JS
  • 创建 Vue 微应用 (app1 & app2)

使用 Vue CLI 创建两个 Vue 应用 app1app2

vue create app1
vue create app2

app1app2 中,我们需要修改 main.js 文件,使其成为 Single-SPA 的微应用。

// app1/src/main.js
import Vue from 'vue'
import App from './App.vue'
import singleSpaVue from 'single-spa-vue';

Vue.config.productionTip = false

const vueLifecycles = singleSpaVue({
  Vue,
  appOptions: {
    el: '#vue-app1', // 确保这个 ID 在主应用中存在
    render: h => h(App)
  }
});

export const bootstrap = vueLifecycles.bootstrap;
export const mount = vueLifecycles.mount;
export const unmount = vueLifecycles.unmount;

// 不要在这里实例化 Vue 应用!交给 Single-SPA 处理

app2main.js 类似,只需要修改 el#vue-app2

  • 配置 Webpack

为了让 Single-SPA 能够加载我们的微应用,我们需要配置 Webpack。

// vue.config.js (app1 和 app2 类似)
const { defineConfig } = require('@vue/cli-service')
const { name } = require('./package.json');

module.exports = defineConfig({
  transpileDependencies: true,
  configureWebpack: {
    output: {
      library: `${name}-[name]`,
      libraryTarget: 'umd',
      chunkLoadingGlobal: `webpackJsonp_${name}`,
    },
  },
  devServer: {
    port: 8081, // app1 端口
    headers: {
      "Access-Control-Allow-Origin": "*",
    },
  },
});

app2vue.config.js 类似,只需要修改 port8082name

  • 注册微应用

在 Single-SPA 主应用中,我们需要注册这两个微应用。

// my-org/src/index.js
import { registerApplication, start } from 'single-spa';

registerApplication({
  name: 'app1',
  app: () => System.import('app1'),
  activeWhen: location => location.pathname.startsWith('/app1'),
  customProps: { someProp: 'someValue' },
});

registerApplication({
  name: 'app2',
  app: () => System.import('app2'),
  activeWhen: location => location.pathname.startsWith('/app2'),
  customProps: { someProp: 'anotherValue' },
});

start();
  • 配置 SystemJS

Single-SPA 使用 SystemJS 来加载微应用,我们需要在主应用的 HTML 文件中配置 SystemJS。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Single-SPA Root Config</title>
    <meta name="importmap-type" content="systemjs-importmap" />
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>

    <!-- If you need to support IE11, uncomment the script tag below and adjust the path to systemjs/dist/system.js accordingly. -->
    <!-- <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/system.js"></script> -->

    <!-- Import map -->
    <system-importmap type="systemjs-importmap">
      {
        "imports": {
          "single-spa": "https://cdn.jsdelivr.net/npm/[email protected]/lib/system/single-spa.min.js",
          "app1": "http://localhost:8081/js/app.js",
          "app2": "http://localhost:8082/js/app.js"
        }
      }
    </system-importmap>
  </head>
  <body>
    <noscript>
      You need to enable JavaScript to run this app.
    </noscript>
    <div class="container">
        <nav class="navbar navbar-expand-lg navbar-light bg-light">
            <a class="navbar-brand" href="#">Microfrontends</a>
            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarNav">
                <ul class="navbar-nav">
                    <li class="nav-item">
                        <a class="nav-link" href="/app1">App 1</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/app2">App 2</a>
                    </li>
                </ul>
            </div>
        </nav>
        <div id="vue-app1"></div>
        <div id="vue-app2"></div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/system.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/import-map-overrides.js"></script>
    <import-map-overrides-full show-when-local-storage="devtools" dev-libs></import-map-overrides-full>
  </body>
</html>

注意:需要安装 import-map-overrides

  • 运行

启动 app1app2 和 Single-SPA 主应用,然后在浏览器中访问主应用,就可以看到两个微应用被加载进来了。

三、组件库管理:统一标准,提升效率

组件库是大型企业级应用中必不可少的一部分。它可以将常用的 UI 组件、业务组件封装起来,供多个团队使用,提高代码复用率,统一代码风格,降低开发成本。

1. 组件库的分类

  • UI 组件库: 包含常用的 UI 组件,比如:按钮、输入框、下拉框、表格等等。
  • 业务组件库: 包含特定的业务组件,比如:用户登录、商品展示、订单管理等等。

2. 组件库的设计原则

  • 通用性: 组件应该具有通用性,可以在不同的场景中使用。
  • 可配置性: 组件应该具有可配置性,可以通过配置来满足不同的需求。
  • 可扩展性: 组件应该具有可扩展性,可以通过扩展来添加新的功能。
  • 易用性: 组件应该易于使用,文档清晰,示例完整。
  • 一致性: 组件应该具有一致性,风格统一,交互一致。

3. 组件库的管理工具

  • Bit: 一个组件共享平台,可以将组件发布到 Bit 云端,供其他团队使用。
  • Storybook: 一个 UI 组件开发环境,可以用来开发、测试和文档化 UI 组件。
  • NPM: 可以将组件库发布到 NPM 上,供其他团队使用。

4. Storybook + NPM 实战

咱们以 Storybook + NPM 为例,演示如何管理 Vue 组件库。

  • 创建组件库项目
vue create my-component-library
  • 安装 Storybook
cd my-component-library
npx sb init
  • 创建组件

src/components 目录下创建两个组件:MyButton.vueMyInput.vue

// src/components/MyButton.vue
<template>
  <button class="my-button" :style="buttonStyle" @click="$emit('click')">
    {{ label }}
  </button>
</template>

<script>
export default {
  name: 'MyButton',
  props: {
    label: {
      type: String,
      default: 'Button'
    },
    backgroundColor: {
      type: String,
      default: 'blue'
    },
    textColor: {
      type: String,
      default: 'white'
    }
  },
  computed: {
    buttonStyle() {
      return {
        backgroundColor: this.backgroundColor,
        color: this.textColor,
        padding: '10px 20px',
        border: 'none',
        borderRadius: '5px',
        cursor: 'pointer'
      };
    }
  }
};
</script>

<style scoped>
.my-button {
  font-size: 16px;
}
</style>
// src/components/MyInput.vue
<template>
  <input class="my-input" type="text" :placeholder="placeholder" :value="value" @input="$emit('input', $event.target.value)">
</template>

<script>
export default {
  name: 'MyInput',
  props: {
    placeholder: {
      type: String,
      default: '请输入内容'
    },
    value: {
      type: String,
      default: ''
    }
  }
};
</script>

<style scoped>
.my-input {
  padding: 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
  font-size: 14px;
}
</style>
  • 编写 Storybook 故事

src/stories 目录下创建两个故事:MyButton.stories.jsMyInput.stories.js

// src/stories/MyButton.stories.js
import MyButton from '../components/MyButton.vue';

export default {
  title: 'Components/MyButton',
  component: MyButton,
  argTypes: {
    backgroundColor: { control: 'color' },
    onClick: { action: 'clicked' },
  },
};

const Template = (args) => ({
  components: { MyButton },
  setup() {
    return { args };
  },
  template: '<my-button v-bind="args" @click="args.onClick" />',
});

export const Primary = Template.bind({});
Primary.args = {
  label: 'Primary Button',
};

export const Secondary = Template.bind({});
Secondary.args = {
  label: 'Secondary Button',
  backgroundColor: 'green',
  textColor: 'yellow'
};
// src/stories/MyInput.stories.js
import MyInput from '../components/MyInput.vue';

export default {
  title: 'Components/MyInput',
  component: MyInput,
  argTypes: {
    onInput: { action: 'inputted' },
  },
};

const Template = (args) => ({
  components: { MyInput },
  setup() {
    return { args };
  },
  template: '<my-input v-bind="args" @input="args.onInput" />',
});

export const Default = Template.bind({});
Default.args = {
  placeholder: 'Enter text here',
};
  • 运行 Storybook
npm run storybook
  • 打包组件库

修改 vue.config.js 文件,使其打包成一个库。

// vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  configureWebpack: {
    output: {
      library: 'MyComponentLibrary',
      libraryTarget: 'umd'
    },
    externals: {
      vue: 'vue' // 不将 Vue 打包进组件库
    }
  }
})

修改 package.json 文件,添加 build:lib 命令。

{
  "name": "my-component-library",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "storybook": "start-storybook -p 6006",
    "build-storybook": "build-storybook",
    "build:lib": "vue-cli-service build --target lib --name my-component-library src/components/index.js"
  },
  "dependencies": {
    "core-js": "^3.8.3",
    "vue": "^2.6.14"
  },
  "devDependencies": {
    "@babel/core": "^7.12.16",
    "@babel/eslint-parser": "^7.12.16",
    "@storybook/addon-actions": "^6.5.16",
    "@storybook/addon-essentials": "^6.5.16",
    "@storybook/addon-interactions": "^6.5.16",
    "@storybook/addon-links": "^6.5.16",
    "@storybook/builder-webpack5": "^6.5.16",
    "@storybook/manager-webpack5": "^6.5.16",
    "@storybook/vue": "^6.5.16",
    "@storybook/vue2": "^6.5.16",
    "@storybook/testing-library": "^0.0.13",
    "@vue/cli-plugin-babel": "~5.0.0",
    "@vue/cli-plugin-eslint": "~5.0.0",
    "@vue/cli-service": "~5.0.0",
    "babel-loader": "^8.1.0",
    "eslint": "^7.32.0",
    "eslint-plugin-vue": "^8.0.3",
    "vue-template-compiler": "^2.6.14"
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/essential",
      "eslint:recommended"
    ],
    "parserOptions": {
      "parser": "@babel/eslint-parser"
    },
    "rules": {}
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ]
}

src/components/index.js 中导出所有组件。

// src/components/index.js
import MyButton from './MyButton.vue';
import MyInput from './MyInput.vue';

export {
  MyButton,
  MyInput
};
  • 发布组件库到 NPM

首先,需要在 NPM 上注册一个账号。然后,登录 NPM。

npm login

然后,发布组件库。

npm publish
  • 使用组件库

在其他 Vue 项目中,可以通过 NPM 安装组件库。

npm install my-component-library

然后在 Vue 项目中使用组件。

<template>
  <div>
    <my-button label="Click me" @click="handleClick" />
    <my-input placeholder="Enter your name" v-model="name" />
  </div>
</template>

<script>
import { MyButton, MyInput } from 'my-component-library';

export default {
  components: {
    MyButton,
    MyInput
  },
  data() {
    return {
      name: ''
    };
  },
  methods: {
    handleClick() {
      alert('Button clicked!');
    }
  }
};
</script>

四、跨团队协作:约定大于配置,规范化流程

大型企业级应用通常由多个团队共同开发,因此,跨团队协作至关重要。良好的协作可以提高开发效率,降低沟通成本,保证代码质量。

1. 代码规范

统一的代码规范是跨团队协作的基础。可以使用 ESLint、Prettier 等工具来强制执行代码规范。

  • ESLint: 用于检查 JavaScript 代码的语法和风格错误。
  • Prettier: 用于格式化 JavaScript 代码,使其符合统一的风格。

2. Git 工作流

统一的 Git 工作流可以避免代码冲突,提高代码质量。常用的 Git 工作流有:

  • Gitflow: 一种基于分支的 Git 工作流,适用于大型项目,可以有效地管理发布、修复和新功能开发。
  • GitHub Flow: 一种简单的 Git 工作流,适用于小型项目,可以快速地迭代和发布。

3. 代码审查

代码审查可以发现代码中的潜在问题,提高代码质量。可以使用 GitHub、GitLab 等平台提供的代码审查功能。

4. 文档

清晰的文档可以帮助团队成员理解代码,降低沟通成本。可以使用 JSDoc、VuePress 等工具来生成代码文档。

5. 沟通

及时的沟通可以避免误解,提高协作效率。可以使用 Slack、钉钉等工具进行团队沟通。

6. 协作流程示例

下面是一个简单的跨团队协作流程示例:

步骤 描述 负责人
1. 需求分析 产品经理负责收集用户需求,编写需求文档。 产品经理
2. 设计 设计师负责设计 UI 界面,编写设计文档。 设计师
3. 开发 开发团队根据需求文档和设计文档进行开发。每个团队负责自己模块的开发,遵循统一的代码规范和 Git 工作流。 开发团队
4. 代码审查 开发完成后,提交代码进行代码审查。其他团队成员负责审查代码,提出修改意见。 团队成员
5. 测试 代码审查通过后,提交代码进行测试。测试团队负责测试代码,发现 Bug。 测试团队
6. 修复 发现 Bug 后,开发团队负责修复 Bug。 开发团队
7. 发布 测试通过后,发布代码到生产环境。 运维团队

五、总结

Vue 在大型企业级应用中面临着许多挑战,但通过合理的架构实践,比如微前端、组件库管理、跨团队协作等等,可以有效地解决这些问题。希望今天的分享能帮助大家更好地使用 Vue 开发大型企业级应用。

记住,架构不是一成不变的,要根据实际情况进行调整。没有最好的架构,只有最适合的架构。咱们要灵活运用,不断学习,才能在大型企业级应用中游刃有余。

好了,今天的分享就到这里,大家有什么问题可以提出来,咱们一起讨论。散会!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注