手把手教你建立前端微服务【天地】
本文建立一个依据 Vue3.0 的微服务结构,并以 Angular、Vue3.0、React 作为微服务,接入主使用。文章比较具体,步骤较多,走完,一个基础的微服务结构就真的建立好了。咱们先将学习最小化,亲力亲为建立好之后,再去考虑怎样优化,有坑的话,记得留言哦~
1. 页面结构
下图是咱们建立的体系的页面结构,在主使用中,包括 header 和左边的 menu 部分,container 部分用来放置咱们的微服务,本文章会运用 Vue 3.0、Angular、React 三个前端流行结构作为示例。
2. 目录结构
咱们的项目目录结构如下,main 用来寄存主体系,sub 寄存子体系。别的,package.json 现在先不论,后面会揭秘。
├── main
└── sub
└── sub-react
└── sub-vue
└── sub-angular
3. 实操
3.1 创立一个 MSP 的项目
mkdir msp
cd msp
npm init msp -y
3.2 按照上面的目录,咱们开始建立
1、首先是建立一个以 Vue 3.0
为结构的主使用,vue-cli 传送门
主使用首要担任登录、注册、菜单的事务,其他的事务逻辑,咱就不要放在主使用了,主使用越简略越好。
vue create main
创立的时分咱们选用 Vue 3.0、Babel、Vuex、CSS Pre-processor(Less) 和 Linter/Formatter(Standard)。
主使用咱们不需求路由装备的,路由需求由咱们自己去掌控。
eslint 的装备咱们选择在放在独自的文件里。
为了减少咱们在 ESLint 上纠结,咱们先暂时装备 eslint 如下(main/eslintrc.js):
module.exports = {
root: true,
env: {
node: true,
},
extends: ['plugin:vue/vue3-essential', 'eslint:recommended'],
parserOptions: {
parser: 'babel-eslint',
},
rules: {},
globals: {
__webpack_public_path__: true, // 全局变量注入,不然 linter 会报错
},
};
接下来,咱们持续创立好三个子使用,然后咱们在一起更改主、子使用的装备。
2、建立一个以Vue 3.0
为结构的子使用sub-vue
,跟主使用一样的办法。
mkdir sub
cd sub
vue create sub-vue
3、建立一个以 React
为结构的子使用sub-react
,Create React App.
npx create-react-app sub-react
4、建立一个以Angular
为结构的子使用sub-angular
,Create Angular App
ng new sub-angular
一路选择 Yes,并选择 Scss 预编译。
通过上面四步之后,实践目录结构如下:
├── package.json
├── main
│ ├── public
│ └── src
│ ├── assets
│ ├── components
│ ├── qiankun
│ └── store
└── sub
├── sub-angular
│ └── src
│ ├── app
│ ├── assets
│ └── environments
├── sub-react
│ ├── public
│ └── src
└── sub-vue
├── public
└── src
├── assets
├── components
├── routes
├── store
└── views
3.3 改造主、子使用
3.3.1 改造子使用sub-vue
- 重命名
src/router
成src/routes
,这个文件夹改成直接导出一切的路由装备,而不直接导出路由实例,咱们把路由放到 main.js 里边装备
import Home from '../views/Home.vue';
const routes = [
{
path: '/',
name: 'Home',
component: Home,
},
{
path: '/about',
name: 'About',
component: () =>
import(/* webpackChunkName: "about" */ '../views/About.vue'),
},
];
export default routes;
- 批改
src/main.js
:包装出 render 函数,便利依据运转环境,来运转项目
import './public-path';
import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import App from './App.vue';
import routes from './routes';
import store from './store';
let router = null;
let instance = null;
// todo: 在天地调用 render 函数的时分,会附上一些信息,比方说容器的挂载节点等
function render(props = {}) {
const { container } = props;
router = createRouter({
// 假如是运转在天地的环境下,一切的路由路径会在开头加上/sub-vue
history: createWebHistory(window.__POWERED_BY_QIANKUN__ ? '/sub-vue' : '/'),
routes,
});
instance = createApp(App);
instance.use(router);
instance.use(store);
instance.mount(container ? container.querySelector('#app') : '#app');
}
// 假如不是天地环境,直接运转render,然后让子使用能够独立运转
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
- 批改
src/main.js
:增加三个生命周期函数(bootstrap、mount、unmount),这三个函数会被主使用调用,来完成子使用的发动、挂载、卸载
export async function bootstrap() {
// 这个函数能够学学怎样用的,加个颜色
console.log('%c ', 'color: green;', 'vue3.0 app bootstraped');
}
export async function mount(props) {
render(props);
}
export async function unmount() {
instance.unmount();
instance._container.innerHTML = '';
instance = null;
router = null;
}
- 在
src
目录下增加文件public-path.js
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
假如呈现 eslint 报错’webpack_public_path‘ is not defined ,那么批改一下 eslint 的装备文件,重启服务。
module.exports = {
root: true,
env: {
node: true,
},
extends: ['plugin:vue/vue3-essential', 'eslint:recommended', '@vue/prettier'],
parserOptions: {
parser: 'babel-eslint',
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
},
globals: {
__webpack_public_path__: true,
},
};
- 批改 webpack 装备
在 src 下新增vue.config.js
,首要意图是更改项意图打包方式,为了让主使用能正确识别微使用暴露出来的一些信息(生命周期钩子等),记住咱们的子使用端口是 8080 哦!
const path = require('path');
const { name } = require('./package');
function resolve(dir) {
return path.join(__dirname, dir);
}
module.exports = {
outputDir: 'dist',
assetsDir: 'static',
filenameHashing: true,
devServer: {
hot: true,
disableHostCheck: true,
port: '8080',
overlay: {
warnings: false,
errors: true,
},
clientLogLevel: 'warning',
compress: true,
headers: {
// 一定要加的,因为天地会用http的请求去获取子使用的代码,那么必然会呈现跨域的问题。
'Access-Control-Allow-Origin': '*',
},
historyApiFallback: true,
},
configureWebpack: {
resolve: {
alias: {
'@': resolve('src'),
},
},
output: {
// 把子使用打包成 umd 库格式,然后主使用能够获取到子使用导出的生命周期钩子函数
library: `${name}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${name}`,
},
},
};
榜首个子使用sub-vue
装备完毕,跑一跑?假如没啥问题,子使用应该现已运转起来,为了美观一些,咱们更改一下/src/views/Home.vue
的内容:
<template>
<div class="home">
<img alt="Vue logo" src="https://www.6hu.cc/storage/2023/05/1683672859-b675e7d8eeef563.png" />
<HelloWorld msg="Welcome to Your Sub Vue App" />
</div>
</template>
<script>
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue';
export default {
name: 'Home',
components: {
HelloWorld,
},
};
</script>
3.3.2 注册子使用sub-vue
- 在主使用中增加天地
咱们只需求在主使用中引进天地,子使用是不依赖天地的,大大减少了代码侵入性。
cd main
yarn add qiankun # perhaps npm i qiankun -S
- 在主使用中注册子使用
创立一个目录qiankun
,用来独自放置天地相关的文件,为了便利拓展,咱们把一切子使用放在一个数组中,这样假如有新注册的子使用,只需求往数组中增加装备即可。
mkdir src/qiankun
touch src/qiankun/index.js
index.js 内容如下:
import {
registerMicroApps,
runAfterFirstMounted,
setDefaultMountApp,
start,
} from 'qiankun';
/**
* Step1 注册子使用
*/
const microApps = [
{
name: 'sub-vue',
developer: 'vue3.x',
entry: '//localhost:8080',
activeRule: '/sub-vue',
},
];
const apps = microApps.map((item) => {
return {
...item,
container: '#subapp-container', // 子使用挂载的 div
props: {
developer: item.developer,
routerBase: item.activeRule,
},
};
});
// 子使用挂载的几个生命周期钩子
registerMicroApps(apps, {
beforeLoad: (app) => {
console.log('before load app.name====>>>>>', app.name);
},
beforeMount: [
(app) => {
console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name);
},
],
afterMount: [
(app) => {
console.log('[LifeCycle] after mount %c%s', 'color: green;', app.name);
},
],
afterUnmount: [
(app) => {
console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name);
},
],
});
/**
* Step2 设置挂载的子使用
*/
setDefaultMountApp('/sub-vue');
/**
* Step3 发动使用
*/
start();
runAfterFirstMounted(() => {
console.log('[MainApp] first app mounted');
});
export default apps;
- 批改
App.vue
,导入咱们的微使用数组,绘制菜单
<template>
<div class="layout-wrapper">
<header class="layout-header">
<div class="logo">MSP</div>
</header>
<main class="layout-main">
<aside class="layout-aside">
<ul>
<li v-for="item in microApps" :key="item.name" @click="goto(item)">
{{ item.name }}
</li>
</ul>
</aside>
<section class="layout-section" id="subapp-container"></section>
</main>
</div>
</template>
<script>
import microApps from './qiankun';
export default {
name: 'App',
data() {
return {
isLoading: true,
microApps,
};
},
methods: {
goto(item) {
history.pushState(null, item.activeRule, item.activeRule);
},
},
};
</script>
<style lang="less">
* {
margin: 0;
padding: 0;
}
html,
body,
.layout-wrapper {
height: 100%;
overflow: hidden;
}
.layout-wrapper {
.layout-header {
height: 50px;
width: 100%;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
line-height: 50px;
position: relative;
.logo {
float: left;
margin: 0 50px;
}
.userinfo {
position: absolute;
right: 100px;
top: 0;
}
}
.layout-main {
height: calc(100% - 50px);
overflow: hidden;
display: flex;
justify-content: space-evenly;
.layout-aside {
width: 190px;
ul {
margin: 50px 0 0 20px;
border-right: 2px solid #aaa;
li {
list-style: none;
display: inline-block;
padding: 0 20px;
color: #aaa;
margin: 20px 0;
font-size: 18px;
font-weight: 400;
cursor: pointer;
&.active {
color: #42b983;
text-decoration: underline;
}
&:hover {
color: #444;
}
}
}
}
.layout-section {
width: 100%;
height: 100%;
}
}
}
</style>
- 更改主使用运转端口
因为咱们现在一切的子使用都是在咱们自己的电脑是运转,所以,每个子使用的端口不能重复。榜首个子使用的端口是 8080,主使用咱们现在改成 8443。在主使用的根目录下创立vue.config.js
module.exports = {
devServer: {
port: '8443',
clientLogLevel: 'warning',
disableHostCheck: true,
compress: true,
historyApiFallback: true,
},
};
到这儿,咱们现已装备好主使用 main, 并注册了子使用 sub-vue。跑起来,不出啥意外,你应该能够看到这个样子的页面。
3.3.3 改造子使用sub-angular
- 首先在
src
下增加public-path.js
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
- 设置
history
形式路由的 base,假如是运转在天地环境下,就以/sub-angular/
开头,批改src/app/app-routing.module.ts
文件
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { APP_BASE_HREF } from '@angular/common';
const routes: Routes = [];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
providers: [
{
provide: APP_BASE_HREF,
// @ts-ignore
useValue: window.__POWERED_BY_QIANKUN__ ? '/sub-angular' : '/',
},
],
})
export class AppRoutingModule {}
- 批改进口文件,包装出
render
函数,使其在天地环境下,由生命钩子履行render
,在本地环境,直接render
,批改src/main.ts
文件
import './public-path';
import { enableProdMode, NgModuleRef } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
let app: void | NgModuleRef<AppModule>;
async function render() {
app = await platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch((err) => console.error(err));
}
if (!(window as any).__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap(props: Object) {
console.log(props);
}
export async function mount(props: Object) {
render();
}
export async function unmount(props: Object) {
console.log(props);
// @ts-ignore
app.destroy();
}
- 批改 webpack 打包装备
- 先装置 @angular-builders/custom-webpack 插件
npm i @angular-builders/custom-webpack -D
- 在根目录增加 custom-webpack.config.js ,内容为:
const appName = require('./package.json').name;
module.exports = {
devServer: {
headers: {
'Access-Control-Allow-Origin': '*',
},
},
output: {
library: `${appName}-[name]`,
libraryTarget: 'umd',
chunkLoadingGlobal: `webpackJsonp_${appName}`,
},
};
- 批改
angular.json
,将[packageName] > architect > build > builder
和[packageName] > architect > serve > builder
的值改为咱们装置的插件,将咱们的打包装备文件加入到[packageName] > architect > build > options
。
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"sub-angular": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
},
"@schematics/angular:application": {
"strict": true
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-builders/custom-webpack:browser",
"options": {
"customWebpackConfig": {
"path": "./custom-webpack.config.js"
},
"outputPath": "dist/sub-angular",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": ["src/favicon.ico", "src/assets"],
"styles": ["src/styles.scss"],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-builders/custom-webpack:dev-server",
"configurations": {
"production": {
"browserTarget": "sub-angular:build:production"
},
"development": {
"browserTarget": "sub-angular:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "sub-angular:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"inlineStyleLanguage": "scss",
"assets": ["src/favicon.ico", "src/assets"],
"styles": ["src/styles.scss"],
"scripts": []
}
}
}
}
},
"defaultProject": "sub-angular"
}
- 处理 zone.js 的问题
在父使用引进 zone.js,需求在 import qiankun 之前引进。
将微使用的 src/polyfills.ts 里边的引进 zone.js 代码删掉。
-
import 'zone.js/dist/zone';
在微使用的 src/index.html
里边的 标签加上下面内容,微使用独立访问时运用。
<script src="https://unpkg.com/zone.js" ignore></script>
- 批改 ng build 打包报错问题,批改 tsconfig.json 文件,参考 issues/431
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "es5",
"typeRoots": ["node_modules/@types"],
"module": "es2020",
"lib": ["es2018", "dom"]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}
- 为了防止主使用或其他微使用也运用 angular 时,
<app-root></app-root>
会抵触的问题,主张给<app-root>
加上一个仅有的 id,比方说当时使用称号
批改src/index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>SubAngular</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<script src="https://unpkg.com/zone.js" ignore></script>
</head>
<body>
<app-root id="sub-angular"></app-root>
</body>
</html>
批改`src/app/app.component.ts :
import { Component } from '@angular/core';
@Component({
selector: '#sub-angular app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
title = 'sub-angular';
}
- 老规矩,改改页面款式,美观一点
批改 app.component.html
:
<h1>This is an Angular APP!</h1>
<router-outlet></router-outlet>
到这儿咱们现已接入了一个 Angular 结构的前端微使用。
3.3.3 改造子使用sub-react
以 create react app
生成的 react 17 项目为例。
- 在 src 目录新增 public-path.js:
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
- 进口文件 index.js 批改,为了防止根 id #root 与其他的 DOM 抵触,需求约束查找规模。
import './public-path';
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
function render(props) {
const { container } = props;
ReactDOM.render(
<App />,
container
? container.querySelector('#root')
: document.querySelector('#root')
);
}
if (!window.__POWERED_BY_QIANKUN__) {
render({});
}
/**
* bootstrap 只会在微使用初始化的时分调用一次,下次微使用从头进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常咱们能够在这儿做一些全局变量的初始化,比方不会在 unmount 阶段被毁掉的使用等级的缓存等。
*/
export async function bootstrap() {
console.log('[react16] react app bootstraped');
}
/**
* 使用每次进入都会调用 mount 办法,通常咱们在这儿触发使用的烘托办法
*/
export async function mount(props) {
console.log('[react16] props from main framework', props);
render(props);
}
/**
* 使用每次 切出/卸载 会调用的办法,通常在这儿咱们会卸载微使用的使用实例
*/
export async function unmount(props) {
const { container } = props;
ReactDOM.unmountComponentAtNode(
container
? container.querySelector('#root')
: document.querySelector('#root')
);
}
/**
* 可选生命周期钩子,仅运用 loadMicroApp 方式加载微使用时生效
*/
export async function update(props) {
console.log('update props', props);
}
- 批改 webpack 装备
咱们运用react-app-rewired
来批改react-scripts
的 webpack 装备。
npm i react-app-rewired -D
在根目录下增加config-overrides.js
:
const { name } = require('./package.json');
const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const webpack = require('webpack');
module.exports = {
webpack: function override(config, env) {
config.output.library = `${name}-[name]`;
config.output.libraryTarget = 'umd';
config.output.jsonpFunction = `webpackJsonp_${name}`;
// Remove 'react-refresh' from the loaders.
for (const rule of config.module.rules) {
if (!rule.oneOf) continue;
for (const one of rule.oneOf) {
if (
one.loader &&
one.loader.includes('babel-loader') &&
one.options &&
one.options.plugins
) {
one.options.plugins = one.options.plugins.filter(
(plugin) =>
typeof plugin !== 'string' || !plugin.includes('react-refresh')
);
}
}
}
config.plugins = config.plugins.filter(
(plugin) =>
!(plugin instanceof webpack.HotModuleReplacementPlugin) &&
!(plugin instanceof ReactRefreshPlugin)
);
return config;
},
devServer: (configFunction) => {
return function (proxy, allowedHost) {
const config = configFunction(proxy, allowedHost);
config.open = false;
config.hot = false;
config.headers = {
'Access-Control-Allow-Origin': '*',
};
return config;
};
},
};
批改package.json
的 scripts
部分:
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-app-rewired eject"
}
- 批改端口,增加文件
.env
:
SKIP_PREFLIGHT_CHECK=true
BROWSER=none
PORT=3000
- 老规矩,改改咱们页面,让页面美观点
自己删掉哪些没用的 CSS 叭,回头再仔细加
批改 src/App.js
:
import './App.css';
function App() {
return <div className='App'>This is a React APP!</div>;
}
export default App;
- 最终一步了,子使用注册到主使用
批改 main/src/qiankun/index.js
的 变量 microApps
:
const microApps = [
{
name: 'sub-vue',
developer: 'vue3.x',
entry: '//localhost:8080',
activeRule: '/sub-vue',
},
{
name: 'sub-angular',
developer: 'angular13',
entry: '//localhost:4200',
activeRule: '/sub-angular',
},
{
name: 'sub-react',
developer: 'react16',
entry: '//localhost:3000',
activeRule: '/sub-react',
},
];
到这儿咱们现已把三个微使用接入主使用,接下来,咱们分别把四个使用运转起来。这个大家应该都会滴。
运转主使用:
cd main
npm run serve
运转子使用 sub-vue
:
cd sub/sub-vue
npm run serve
运转子使用 sub-react
:
cd sub/sub-react
npm run start
运转子使用 sub-angular
:
cd sub/sub-angular
npm run start
总算看到页面咯?
4. 本地开发装备优化
每次跑这么多命令,运转各个微使用,仍是蛮累的。下面咱们介绍一个利器npm-run-all
,来批量运转咱们的微使用。
npm i npm-run-all -D
批改 package.json 下的 scripts:
"scripts": {
"start": "npm-run-all --parallel start:*",
"start:sub-react": "cd sub/sub-react && npm run start",
"start:sub-vue": "cd sub/sub-vue && npm run serve",
"start:sub-angular": "cd sub/sub-angular && npm run start",
"start:main": "cd main && npm run serve"
},
履行 npm run start
,引发一切 APP。到这儿,咱们的 MSP 就现已建立完成咯!
5. 小结
子使用改造:
- 增加
public-path.js
,为什么需求加呢?2. 防止根 id 与其他的 DOM 抵触,需求约束查找规模。 - 完成四个钩子函数,并导出。
- 批改 webpack 装备,增加
'Access-Control-Allow-Origin': '*'
,答应跨域请求文件;批改打包方式,然后让天地拿到导出的钩子函数,获取掌控权。