各位老铁,早上好!我是老码,今天跟大家聊聊 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 应用:app1
和 app2
。
- 创建 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 应用 app1
和 app2
。
vue create app1
vue create app2
在 app1
和 app2
中,我们需要修改 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 处理
app2
的 main.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": "*",
},
},
});
app2
的 vue.config.js
类似,只需要修改 port
为 8082
和 name
。
- 注册微应用
在 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
。
- 运行
启动 app1
、app2
和 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.vue
和 MyInput.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.js
和 MyInput.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 开发大型企业级应用。
记住,架构不是一成不变的,要根据实际情况进行调整。没有最好的架构,只有最适合的架构。咱们要灵活运用,不断学习,才能在大型企业级应用中游刃有余。
好了,今天的分享就到这里,大家有什么问题可以提出来,咱们一起讨论。散会!