关注公众号: 微信搜索 前端工具人
; 收获更多的干货
一、介绍:
qiankun
项目实际搭建, 及各种微应用流行框架技术 (vue2 、vue3、react 、 umi2 、umi3
)的配置;
初衷
:自己当时摸索qiankun
构建项目时,问题百出, 特别是umi2
及 umi3
,百度了几天才把热门框架都集合完毕;
目的
:总结出的模板项目, 便于自己后期重构项目技术选型及项目快速搭建;也为其他有需要的朋友提供示例及参考;
项目源码:已上传到 github https://github.com/laijinxian/qiankun-template
如有对你有帮助,麻烦 star 下
末尾的常见问题多数为目前开发中遇到的疑难点, 特地整理出来;有其他问题欢迎留言交流
实际项目源码就没贴出来了,都是依据这个模板构建的;
后面看下好不好把实际项目源码抽离出来,上传到github
; 目前子项目使用的是 vite2.0 + vue3 + ts
以及 react + Umi3 + dva + ts
二、什么是微前端 qiankun 篇
推荐阅读 qiankun文档
其实我个人更喜欢叫成 前端微服务架构
, 感觉逼格更高点...
2.1 官方介绍:
- 微前端是一种多个团队通过独立发布功能的方式来共同构建现代化
web
应用的技术手段及方法策略 (有点高深...)
2.2 我的观点:
-
与技术栈无关、独立开发、独立部署、增量升级、独立运行;
-
拆分、细化、解耦
你的巨无霸项目; 提升开发及打包部署效率; -
不在局限于一个项目只能使用一种技术,一个项目可以使用
N
种技术,扩展自己技术知识面; -
对于比如 大型
erp、OA
之类的系统, 微前端可以让你更加的得心应手的开发; -
对于想
重构
公司辣眼睛的项目尤为合适;这也算我入手微前端的主要原因之一,下面会讲到; -
顺应时代潮流, 作为主流技术现在非常多的公司
招聘面试
基本都会问微前端,细化程度不一样;
三、 为什么用qiankun, 为什么选择qiankun
3.1 为什么用qiakun
自打进入公司,看到了现有的项目,我总结了几点
- 项目全都使用 Vue, 一直开发下去你会发现 React 忘得快差不多了;技术的局限性;
- 现有项目代码又臭又长,毫无规范;
eslint、css
预编译啥都没有; - 2-3层
for
循环,var
之类的,粘贴复制无用代码不删除到处可见;公共代码提取、接口统一处理、工具类编写不存在的; - 一个项目同时出现
vue、jQuery
两个大框架;运行项目、热编译、你可以先上趟厕所; - 每期功能迭代,先要花大半天时间去熟悉这个代码、还真不敢乱改,有毒、谁改谁后悔的那种
- 想重构,奈何刚接手的时候项目已经很大了,并且不怎么熟悉业务,且不断的加功能; 一时重构基本不可能;千万级别的用户量出问题了这锅背不动, 时间也不允许
后面需求排期不是很紧凑,正直qiankun
微前端很火,就想着使用qiankun
微前端方案重构;
思路如下:
目标
把一个项目按照菜单划分,一个大菜单分为一个子服务(子项目)- 刚开始原有项目全部划分为一个子服务,新加功能菜单划分为另一个子服务;这样既保证原有项目不变,新项目完全使用新的框架及开发风格规范;
- 时间充裕下情况下,慢慢把其他功能按照菜单划分成子服务,慢慢的最小粒度去重构项目
3.2 目前微前端方案有:
iframe
single-spa
qiankun
基于single-spa
方案实现, 更强大更易上手
推荐阅读 掘金大佬文章, 文章有详细介绍及常见问题
四、 构建步骤
项目结构:
├── main-service // 主应用
└── sub-service // 微应用
└── sub-react // react 子应用
└── sub-umi2 // umi2 子应用
└── sub-umi3 // umi3 子应用
└── sub-vue2 // vue2 子应用
└── sub-vue3 // vue3 子应用
推荐阅读:
-
详细结构代码已上传github 请前往 github 查看
-
qiankun官方文档
-
参考掘金文章
4.1 项目结构组成
主应用:
vue2.x + vuec-li3
主要业务功能就是登录注册及菜单;官方推荐主应用尽可能的简单,不要涉及其他的业务功能
微应用:
vue2.x + vue-cli3
vue3.x + vue-cli4 + typescript
react16
react16 + umi2 + dva
react16 + umi3 + dva
4.2 主应用配置
qiankun
只需要在主应用中引入,微应用不需要
yarn add qiankun # 或者 npm i qiankun -S
4.3 主应用 src 下 注册微应用
主应用 src
下新建 qiankun/index.js
import {
registerMicroApps,
runAfterFirstMounted,
setDefaultMountApp,
start
} from "qiankun";
import store from '../store/index'
import { instance } from "../main";
import 'nprogress/nprogress.css'
/**
* Step1 初始化应用(可选)
*/
function loader(loading) {
if (instance && instance.$children) {
// instance.$children[0] 是App.vue,此时直接改动App.vue的isLoading
instance.$children[0].isLoading = loading;
}
}
/**
* Step2 注册子应用
*/
const microApps = [
{
name: 'sub-vue2',
developer: 'vue2.x',
entry: '//localhost:7788',
activeRule: '/sub-vue2',
},
{
name: 'sub-vue3',
developer: 'vue3.x',
entry: '//localhost:7799',
activeRule: '/sub-vue3'
},
{
name: 'sub-react',
developer: 'react16',
entry: '//localhost:7755',
activeRule: '/sub-react'
},
{
name: 'sub-umi2',
developer: 'umi2.x',
entry: '//localhost:7766',
activeRule: '/sub-umi2'
},
{
name: 'sub-umi3',
developer: 'umi3.x',
entry: '//localhost:7733',
activeRule: '/sub-umi3'
}
]
const apps = microApps.map(item => {
return {
...item,
loader, // 给子应用配置加上loader方法
container: '#subapp-container', // 子应用挂载的div
props: {
developer: item.developer, // 下发基础路由
routerBase: item.activeRule, // 下发基础路由
getGlobalState: store.getGlobalState // 下发getGlobalState方法
}
}
})
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)
}
]
})
/**
* Step3 设置默认进入的子应用
*/
setDefaultMountApp('/sub-vue2')
/**
* Step4 启动应用
*/
start();
runAfterFirstMounted(() => {
console.log("[MainApp] first app mounted");
});
export default apps
4.4 微应用导出生命周期钩子
各种框架配置推荐阅读 官方文档
下面以 vue3.x
及 react umi3
为例; 其他微服务配置请前往 [github](https://github.com/laijinxian/qiankun-template)
源码查看
子应用的名称最好与父应用在 qiankun/index.js
中配置的名称一致(这样可以直接使用package.json
中的name
作为output
)
vue3.x
微应用
首先 vue create sub-vue3
创建项目
修改 main.js
导出生命周期函数
// @ts-nocheck
import "./public-path";
import { createApp } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import App from "./App.vue";
import routes from "./router";
import store from "./store";
let router = null;
let instance = null;
function render(props = {}) {
const { container } = props;
router = createRouter({
history: createWebHistory(window.__POWERED_BY_QIANKUN__ ? "/sub-vue3" : "/"),
routes
});
instance = createApp(App);
instance.use(router);
instance.use(store);
instance.mount(container ? container.querySelector("#app") : "#app");
}
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap() {
console.log("%c ", "color: green;", "vue3.0 app bootstraped");
}
function storeTest(props) {
props.onGlobalStateChange &&
props.onGlobalStateChange(
(value, prev) =>
console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev),
true
);
props.setGlobalState &&
props.setGlobalState({
ignore: props.name,
user: {
name: props.name
}
});
}
export async function mount(props) {
storeTest(props);
render(props);
instance.config.globalProperties.$onGlobalStateChange =
props.onGlobalStateChange;
instance.config.globalProperties.$setGlobalState = props.setGlobalState;
}
export async function unmount() {
instance.unmount();
instance._container.innerHTML = "";
instance = null;
router = null;
}
新建 vue.config.js
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: '7799',
overlay: {
warnings: false,
errors: true,
},
clientLogLevel: "warning",
disableHostCheck: true,
compress: true,
headers: {
'Access-Control-Allow-Origin': '*',
},
historyApiFallback: true,
overlay: { warnings: false, errors: true }
},
// 自定义webpack配置
configureWebpack: {
resolve: {
alias: {
'@': resolve('src'),
},
},
output: {
// 把子应用打包成 umd 库格式
library: `${name}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${name}`,
},
},
};
src
新建 public-path.js
并引入
/* eslint-disable @typescript-eslint/camelcase */
if ((window as any).__POWERED_BY_QIANKUN__) {
/* eslint-disable @typescript-eslint/camelcase */
__webpack_public_path__ = (window as any).__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
react umi3
微应用
创建项目
推荐阅读: umi 官方文档
$ mkdir myapp && cd myapp
$ yarn create umi
安装
$ npm install --save-dev @umijs/plugin-qiankun
$ yarn add @umijs/plugin-qiankun
修改 src/app.js 导出生命周期函数
import './public-path'
export const dva = {
config: {
onError(err) {
err.preventDefault();
console.error(err.message);
},
},
};
export const qiankun = {
// 应用加载之前
async bootstrap(props) {
console.log('app1 bootstrap', props);
},
// 应用 render 之前触发
async mount(props) {
console.log('app1 mount', props);
storeTest(props);
},
// 应用卸载之后触发
async unmount(props) {
console.log('app1 unmount', props);
},
};
function storeTest(props) {
props.onGlobalStateChange &&
props.onGlobalStateChange(
(value, prev) => console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev),
true,
);
props.setGlobalState &&
props.setGlobalState({
ignore: props.name,
user: {
name: props.name,
},
});
}
修改 .umirc.js 文件 引入 @umijs/plugin-qiankun 插件
// ref: https://umijs.org/config/
export default {
mountElementId: 'sub-umi3',
base: `sub-umi3`, // 子应用的 base,默认为 package.json 中的 name 字段
treeShaking: true,
routes: [
{ exact: false, path: '/', component: '../layouts/index',
routes: [
{ exact: false, path: '/', component: '../pages/index' },
{ component: './404.js' }
],
}
],
plugins: [
['@umijs/plugin-qiankun', {
keepOriginalRoutes: true
}],
// ref: https://umijs.org/plugin/umi-plugin-react.html
['umi-plugin-react', {
antd: true,
dva: true,
dynamicImport: { webpackChunkName: true },
title: 'react',
dll: false,
routes: {
exclude: [
/models\//,
/services\//,
/model\.(t|j)sx?$/,
/service\.(t|j)sx?$/,
/components\//,
],
},
}],
],
}
src
新建 public-path.js
并引入
/* eslint-disable @typescript-eslint/camelcase */
if ((window as any).__POWERED_BY_QIANKUN__) {
/* eslint-disable @typescript-eslint/camelcase */
__webpack_public_path__ = (window as any).__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
以上就是微前端的基本配置 demo
, 源码 [github](https://github.com/laijinxian/qiankun-template)
查看
接下来真正的项目重构实操及进阶
五、 项目重构实践、进阶中常见问题
5.1 qiankun 常见报错
推荐阅读: 官方文档总结 qiankun.umijs.org/zh/faq
5.2 状态管理, 主应用和微应用之间的通信
qiankun
通过 initGlobalState
: 定义全局状态,并返回通信方法,建议在主应用使用,微应用通过 props
获取通信方法;
onGlobalStateChange
: 在当前应用监听全局状态,有变更触发 callback
;
setGlobalState
: 按一级属性设置全局状态,微应用中只能修改已存在的一级属性; 换句话说只能修改主用于预先定义的属性,后面添加的属性无效
官方列子 发布-订阅的设计模式: 主应用
import { initGlobalState, MicroAppStateActions } from 'qiankun';
// 初始化 state
const actions: MicroAppStateActions = initGlobalState(state);
actions.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log(state, prev);
});
actions.setGlobalState(state);
actions.offGlobalStateChange();
微应用
// 从生命周期 mount 中获取通信方法,使用方式和 master 一致
export function mount(props) {
props.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log(state, prev);
});
props.setGlobalState(state);
}
5.3 各应用之间的独立仓库以及聚合管理
实际开发中项目存储在公司仓库中,以 gitLab
为例, 当子应用一多,全部放在一个仓库下面, 这时候就显得很臃肿了,也很庞大,大大的增加了维护成本,和开发效率;
我们可以通过 sh
脚本, 初始只需要克隆主仓库代码, 然后通过 sh
脚本去一键拉取所有子应用;
主仓库新建 script/clone-all.sh
文件 内容如下
# 子服务 gitLab 地址
SUB_SERVICE_GIT=('http://gitlab.qinlinkeji.com/xxxxxx/qiankun-sub-service-vue.git' 'http://gitlab.qinlinkeji.com/xxxxxx/qiankun-sub-service-react.git')
SUB_SERVICE_NAME=('qiankun-sub-service-vue' 'qiankun-sub-service-react')
# 子服务
if [ ! -d "sub-service" ]; then
echo '创建sub-service目录...'
mkdir sub-service
fi
echo '进入sub-service目录...'
cd sub-service
# 遍历克隆微服务
for i in ${!SUB_SERVICE_NAME[@]}
do
if [ ! -d ${SUB_SERVICE_NAME[$i]} ]; then
echo '克隆微服务项目'${SUB_SERVICE_NAME[$i]}
git clone ${SUB_SERVICE_GIT[$i]}
fi
done
echo '脚本结束...'
# 克隆完成
代码拉取完成后, 紧接着就是下载各个项目的依赖及运行
应用根目录安装 npm i npm-run-all -D
package.json
文件 scripts
命令如下
"scripts": {
"clone:all": "bash ./scripts/clone-all.sh",
"install": "npm-run-all --serial install:*",
"install:main": "cd main-service && cnpm i",
"install:sub-vue2": "cd sub-service/sub-vue2 && yarn install",
"install:sub-vue3": "cd sub-service/sub-vue3 && yarn install",
"install:sub-react": "cd sub-service/sub-react && cnpm i",
"install:sub-umi2": "cd sub-service/sub-umi2 && yarn install",
"install:sub-umi3": "cd sub-service/sub-umi3 && yarn install",
"start": "npm-run-all --parallel start:*",
"start:sub-react": "cd sub-service/sub-react && npm start",
"start:sub-vue2": "cd sub-service/sub-vue2 && npm start",
"start:sub-vue3": "cd sub-service/sub-vue3 && yarn start",
"start:sub-umi2": "cd sub-service/sub-umi2 && yarn start",
"start:sub-umi3": "cd sub-service/sub-umi3 && yarn start",
"start:main": "cd main-service && yarn start",
"test": "echo \"Error: no test specified\" && exit 1"
},
步骤: 第一步 clone
主应用, 然后依次执行 yarn clone:all --> yarn install --> yarn start
即可运行整个项目
5.4 子应用之间的独立开发
需求
: 每次项目的迭代,有可能只涉及其中某个应用功能,
期望
: 只需要单独打开这个子应用修改即可;并不是整个庞大项目一起启用
目标
: 应用解耦的同时也能高效撸代码
问题
: 整个项目中都需要一个登录态(登录凭证 token
), 上面说到登录token
是在主应用中维护的, 不启动主应用,子应用怎么拿到登录态token
呢;
解析
: 其实登录的主要作用都是获取到用户信息及 token
后, 保存在浏览器缓存中,比如 localStorage、sessionStorage、cookie、IndexedDB
, 需要的地方获取即可;
方法
: 子应用中通过 qiankun
提供的 window.__POWERED_BY_QIANKUN__
属性, 很直接的知道目前是否运行在 qiankun
的主应用的上下文中;全局维护一个变量,控制是否展示 iframe
的登录页
if (!window.__POWERED_BY_QIANKUN__) {
// 不在主应用的上下文中
}
当不在qiankun
主应用上下文环境中时, 通过 iframe
形式, 直接引入登录页面, 完成登录把用户数据及token
存入缓存中即可;
要注意的是:
- 浏览器默认不支持
iframe
文件的script
脚本执行; 需要设置sandbox="allow-scripts allow-same-origin"
两个属性即可 - 下面代码是通过本地的
html
文件 (登录页);在vue-cli3
中我们需要吧html
静态、文件放在public/static
下面 - 当然当你项目发布到服务器之后, 把上面步骤删了,直接在
iframe
里引用登录页面即可;iframe
的url
指向你的线上登录页; 这样下来子应用只需要加个iframe
一行代码,即可完成子应用的登录态获取
实例: vue
子应用 某页面
<template>
<!-- <iframe src="https://juejin.cn/" width="400" height="300" sandbox="allow-scripts allow-same-origin"></iframe> -->
<iframe ref="iframe" name="iframe" width="400" height="300" sandbox="allow-scripts allow-same-origin"></iframe>
</template>
<script>
export default {
data () {
return {
html: require("static/login.html")
}
},
mounted() {
this.$refs.iframe.srcdoc = this.html
console.log(localStorage.getItem('userInfo'))
}
}
</script>
5.5 如何提取出公共的依赖库
官方说法: 并不推荐这种做法, 因为微服务主要目标是解耦大型应用, 并且当你升级某个项目的公共依赖之后,意味着其他子应用也升级了, 很难保证不出问题;但你确实想那么做,那么也有方法:
方法1: 官方推荐你可以在微应用中将公共依赖配置成 external
,然后在主应用中导入这些公共依赖;
推荐阅读: 掘金文章
方法2: 我的做法是 通过 webpack
的 DllPlugin
动态链接库, 生成静态 json
在子应用中引
入; DllPlugin
是webpack
内置的插件,不需要额外安装; 这里就不贴代码了, 代码有点多, DllPlugin教程很多, 百度到处是, 跟着配置下webpack.dll.config.js
就行;
DllPlugin
也是项目优化的一个手段, 自己配置一遍印象更深
5.6 如何提取出公共方法
在这我个人也不怎么推荐, 因为子应用是不同框架 vue\react\umi-react
并不能保证方法能同时作用于这几个框架项目, 不能的话那何来公共方法一说;
当然你的子应用全是同一个框架那上面的话当我没说。。。
有需求就有方法: 推荐参考 掘金文章 更详细:
npm
指向本地file
地址:npm install file:../common
。直接在根目录新建一个common
目录,然后npm
直接依赖文件路径。npm
指向私有git
仓库:npm install git+ssh://xxx-common.git
。- 发布到
npm
私服
demo
中我用的是第一种方法,当然不嫌麻烦可以选用第三种发布到npm
私服,嫌私服难搭,可以用后台的manven
私服,把你的公共代码给后台让让后台发布; manven
私服按我的理解后台标配;
第一种方法 指向本地file
地址事例; vue
子应用 main.js
引入你的本地公共代码并注册
import globalRegister from '../../../main-service/src/store/global-register'
export async function mount(props) {
console.log('[vue] props from main framework', props);
storeTest(props);
render(props);
globalRegister(store, props)
}
5.7 微应用之间如何跳转
- 主应用和微应用都是
hash
模式,主应用根据hash
来判断微应用,则不用考虑这个问题。 - 主应用根据
path
来判断微应用
history
模式的微应用之间的跳转,或者微应用跳主应用页面,直接使用微应用的路由实例是不行的,原因是微应用的路由实例跳转都基于路由的 base
。有两种办法可以跳转:
history.pushState()
:mdn用法介绍- 将主应用的路由实例通过 props 传给微应用,微应用这个路由实例跳转。
// 用法 第二、第三参数分别是子应用名称及激活路由
history.pushState(null, 'sub-react', '/sub-react');
5.8 微应用文件更新之后,访问的还是旧版文
项目上线后由于是独立仓库独立开发独立部署, 微应用文件更新之后,访问的还是旧版文;
服务器需要给微应用的 index.html
配置一个响应头:Cache-Control no-cache
,意思就是每次请求都检查是否更新。
以 Nginx
为例:
location = /index.html {
add_header Cache-Control no-cache;
}
5.9 应用加载的资源会 404
原因是 webpack
加载资源时未使用正确的 publicPath
。
可以通过以下两个方式解决这个问题:
a. 使用 webpack
运行时 publicPath
配置
qiankun
将会在微应用 bootstrap
之前注入一个运行时的 publicPath
变量,你需要做的是在微应用的 entry js
的顶部添加如下代码:
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
关于运行时 publicPath
的技术细节,可以参考 webpack
文档。
runtime publicPath
主要解决的是微应用动态载入的 脚本、样式、图片 等地址不正确的问题。
b. 使用 webpack
静态 publicPath
配置
你需要将你的 webpack publicPath 配置设置成一个绝对地址的 url,比如在开发环境可能是:
{
output: {
publicPath: `//localhost:${port}`,
}
}
5.10 如何部署
推荐阅读 官方文档 更详细
主应用和微应用都是独立开发和部署,即它们都属于不同的仓库和服务
场景:主应用和微应用部署到同一个服务器(同一个IP和端口) 如果服务器数量有限,或不能跨域等原因需要把主应用和微应用部署到一起。
通常的做法是主应用部署在一级目录,微应用部署在二/三级目录。
微应用想部署在非根目录,在微应用打包之前需要做两件事:
-
必须配置
webpack
构建时的publicPath
为目录名称,更多信息请看webpack
官方说明 和vue-cli3
的官方说明 -
history
路由的微应用需要设置base
,值为目录名称,用于独立访问时使用。
部署之后注意三点:
activeRule
不能和微应用的真实访问路径一样,否则在主应用页面刷新会直接变成微应用页面。- 微应用的真实访问路径就是微应用的
entry
,entry
可以为相对路径。 - 微应用的
entry
路径最后面的/
不可省略,否则publicPath
会设置错误,例如子项的访问路径是http://localhost:8080/app1
,那么entry
就是http://localhost:8080/app1/
。
以上问题大多数可前端官方文档常见问题查看, 只不过不是很详细, 有的需要自己去百度完善
六、参考链接
掘金: juejin.cn/post/687546…
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!