导航菜单
首页 >  在微前端qiankun中使用Vite你踩坑了吗  > qiankun微前端从搭建到部署大型踩坑记录片(一镜到底)

qiankun微前端从搭建到部署大型踩坑记录片(一镜到底)

前言近两年一直会有遇到需要微前端框架的需求,同时在招聘上,微前端的需求也是挺多的,最近整理了一下之前经手过的几个qiankun微前端项目,分享给大家。

项目结构预览前期准备工作

主应用的搭建、基座的配置。子应用template的搭建(react)。

搭建主应用在workspace建立mirc-project目录来存放主应用和微应用

mkdir mirc-project // 创建目录cd mirc-projectmkdir main // 创建主应用项目目录cd mainnpm init //初始化package.json

为主应用安装qiankun

yarn add qiankun

根目录下新建src目录,并新建index.html,根据结构预览划分html结构,同时,新建index.ts文件,并在index.html引用,如下:

安装一下ts+react开发环境

yarn add --dev typescript ts-node react react-dom @types/react @types/react-dom ejs jest @types/ejs @types/jest

下载babel

yarn add --dev babel-jest @babel/core @babel/preset-env @babel/preset-typescript

配置babel.config.json

{ "presets": [["@parcel/babel-preset-env", { "targets": { "node": "current" } }],"@babel/preset-typescript" ], "plugins": ["@parcel/babel-plugin-transform-runtime"]}

为主应用添加一个打包的库,这里选择Parcel, 以及微前端的一个库single-spa。

yarn add --dev parcel parcel-bundler parcel-plugin-custom-dist-structureyarn add single-spa @parcel/babel-preset-env @parcel/babel-plugin-transform-runtime

为主应用package.json添加执行脚本:

"start": "parcel src/index.html","build:dev": "parcel build src/index.html --no-cache",

至此,需要的依赖都已搞定。接下来是code环节。

注册子应用

子应用项目的搭建我们后面再做详细介绍,现在假如我们已经成功运行了一个子应用,本地访问localhost:3001。index.ts

import { registerMicroApps, start } from "qiankun";registerMicroApps([ {name: "react app", // app name registeredentry: "http://localhost:3001/",container: "#micro-app-slot",activeRule: "/", },]);start();

现在执行 npm run start 可以看到我们子应用的内容,如果你为loading图标添加了样式,loading图标还在转?我们还需要完善。

自定义注册微应用新建 microAppsConfig.ts。因为用了ts,这里我们先定义一下类型。src/core/interface.d.ts

export type ApplicationActiveRule = string | string[];export type ContainerSlot = | "#sidebar-slot" | "#navbar-slot" | "#micro-app-slot"export interface MicroApplication { name: string; entry: string; container: ContainerSlot; activeRule: ApplicationActiveRule; inactiveRule?: ApplicationActiveRule; basename: string; path?: string; noAuth?: boolean; critical?: boolean;}export interface MicroPages { loginApp: string; notFoundApp: string; notAllowAccessApp: string; apps: MicroApplication[];}

microAppsConfig.ts 内容如下:

import { MicroApplication } from "../core/interface";export const mainApps: MicroApplication[] = [{ name: "navbar", entry: "http://localhost:3001", container: "#navbar-slot" as const, activeRule: "/", inactiveRule: ["/login", "/404", "/forgot-password"], basename: "/",},{ name: "sidebar", entry: "http://localhost:3002", container: "#sidebar-slot" as const, activeRule: "/", inactiveRule: ["/login", "/404", "/401", "/forgot-password"], basename: "/", critical: true,},{ name: "login", entry: "http://localhost:3000", container: "#micro-app-slot" as const, activeRule: ["/login", "/forgot-password"], basename: "/", path: "/login", noAuth: true,},{ name: "404", entry: "/pages/404/index.html", container: "#micro-app-slot" as const, activeRule: "/404", basename: "/404/", path: "/404", noAuth: true,},{ name: "401", entry: "/pages/401/index.html", container: "#micro-app-slot" as const, activeRule: "/401", basename: "/401/", path: "/401", noAuth: true,}, ];export const microAppsConfig = {loginApp: "login",notFoundApp: "404",notAllowAccessApp: "401",apps: [ ...mainApps, {name: "dashboard",entry: "http://localhost:3003",container: "#micro-app-slot" as const,activeRule: "/dashboard",basename: "/dashboard/", },], };export default microAppsConfig;

对内容做一些解释

loginApp: "foo" # 用于登陆的app 名字notFoundApp: "404" # 当没有当前路径没有任何app匹配时跳转到该appdefaultApp: "foo" # 当访问根路径时,会跳转到该appapps: - name: "foo" # 应用名字,最好不要包含空格,还有各种奇怪的字符,全局唯一entry: "/subapps/foo/index.html" # 应用入口,可以为一个完整URL,只支持绝对路径container: "#sidebar-slot" # 应用挂载位置 "sidebar-slot" | "navbar-slot" | "micro-app-slot"activeRule: "/foo" # 支持string 或者 string[],当pathname 以rule开头时,就认为该app是active的inactiveRule: "/login" # 可选,支持string 或者 string[],当pathname 以rule开头时,就认为该app是inactive的basename: "/foo/" # 定义微应用的basename,一般与activeRule相同,需要以"/"结尾。对于需要使用根路径做跳转的应用,建议使用"/"作为basename。path: “/foo" # 选填 string,当应用作为loginApp / notFoundApp / defaultApp 时,会跳转到这个地址noAuth: true # 选填 boolean,为true的话则表示没有 token 依然能加载成功critical: true # 选填 boolean,为true时表示该应用在启动的时候就需要提前加载

主应用与子应用之间的通信

这里qiankun提供了initGlobalState方法在主应用注册定义全局状态,并返回通信方法,子应用通过props调用。当然,我们需要注意的是当路由和登录用户切换之后处理。

定义获取当前用户信息的方法getUser文件,主要用于获取用户token以及其他的用户信息。

export type User = {username: string;token: string }const getUser: () => Promise = async () => {// 可以在这里调用用户信息接口// todoreturn { username: "test", token: "test_token" }; };export default getUser;

定义项目全局的state

// 定义export interface GlobalState { user: User | null; refreshToken: () => Promise;}const initGlobalState = ( initialState: Partial, apps: MicroApplication[]) => { const actions = qiankunInitGlobalState({...initialState, }); return actions;};export default initGlobalState;

定义全局子应用状态action store

import {initGlobalState as qiankunInitGlobalState,MicroAppStateActions, } from "qiankun";let _state: Parameters[0] = {}; let _stateChangeFns: Parameters[0][] = [];export const setOnGlobalStateChange = (onGlobalStateChange: MicroAppStateActions["onGlobalStateChange"] | undefined ) => {_stateChangeFns = [];_state = {};onGlobalStateChange((state, prevState) => { _stateChangeFns.forEach((fn) => fn(state, prevState)); _state = state;}, true); };export const addStateChangeListener = (...[callback, fireImmediately]: Parameters< MicroAppStateActions["onGlobalStateChange"]> ) => {_stateChangeFns.push(callback);if (fireImmediately) { callback(_state, _state);} };

路由或者用户改变时,需要对重定向地址做处理,相应的demo,我们统一放在一个core包下面由于代码量的原因,完整代码放在gitee上,仅供参考。

在子应用中使用主应用注册的state和回调方法以react项目为例, 通过props传递给App组件

export async function bootstrap() {}export async function mount(props: SubAppProps) { ReactDOM.render(,props.container.querySelector(defaultRootSelector) );}export async function unmount(props: SubAppProps) { const ele = props.container.querySelector(defaultRootSelector); ele && ReactDOM.unmountComponentAtNode(ele);}

定义获取全局state的hook方法

interface UserInfo { token: string | null; username: string;}export interface GlobalState { user: UserInfo | null; refreshToken: (() => Promise) | undefined;}export interface GlobalStateContext { state: GlobalState; setToken: (token: string | null) => void; getToken: () => string | null;}const appGlobalContainer = createContainer< GlobalStateContext, Pick>((initialState) => { const [state, setState] = useState({user: null,refreshToken: undefined, }); const stateRef = useCurrent(state); const { onGlobalStateChange, setGlobalState } = initialState!; useEffect(() => {onGlobalStateChange((state) => { setState(state as GlobalState);}, true); }, [onGlobalStateChange]); const setInnerAndGlobalState = useCallback((newState: Partial) => { setGlobalState({ ...stateRef.current, ...newState }); setState({ ...stateRef.current, ...newState });},[setState, stateRef, setGlobalState] ); const setToken = useCallback((token: string | null) => { const { username } = stateRef.current?.user || {}; setInnerAndGlobalState({user: { token, username: username || "", // permissionList,}, });},[setInnerAndGlobalState, stateRef] ); const getToken = useCallback(() => {return stateRef.current.user?.token || null; }, [stateRef]); return {state,setToken,getToken, };});export const AppGlobalStateProvider = appGlobalContainer.Provider;export const useAppGlobalState = appGlobalContainer.useContainer;

在页面中使用

import { useAppGlobalState } from "context/appGlobalState";import { useIntl } from "react-intl";import { Line, PageWrap } from "components/styled.common";export default function Home() { const intl = useIntl(); const { state } = useAppGlobalState(); const routes = [{ path: "/", breadcrumbName: "首页",},{ path: "", breadcrumbName: "系统用户",}, ]; return ( App Global State: {state.user?.username} {/* # Page operation update */} );}

将在页面看到 App Global State: test

微前端子应用

这里以React项目来举例子,相关的搭建React项目的经验可以参考其他文章。这里我们默认以create-react-app生成了一个React项目,主要关注集成qiankun的部分。

因为qiankun+vite方式构建微应用还没有完善的解决办法,所以如果使用vue的话,暂时只有使用webpack构建的版本在配置上会简单一点。

子应用qiankun的配置src/qiankun.ts

declare global { interface Window {__webpack_public_path__?: string;__POWERED_BY_QIANKUN__?: boolean;__INJECTED_PUBLIC_PATH_BY_QIANKUN__?: string;__QIANKUN_DEVELOPMENT__?: boolean; }}export type OnGlobalStateChangeCallback = ( state: Record, prevState: Record) => void;export interface SubAppProps { name: string; basename: string; container: HTMLElement; onGlobalStateChange: (callback: OnGlobalStateChangeCallback,fireImmediately?: boolean ) => void; setGlobalState: (state: Record) => boolean;}

src/public-path.ts

declare global { interface Window {__webpack_public_path__?: string;__POWERED_BY_QIANKUN__?: boolean;__INJECTED_PUBLIC_PATH_BY_QIANKUN__?: string;__QIANKUN_DEVELOPMENT__?: boolean; }}if ( window.__POWERED_BY_QIANKUN__ && window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;}export {};

子应用单独运行时的的处理src/renderDev.ts

import { OnGlobalStateChangeCallback, SubAppProps } from "./qiankun";import { render as reactDomRender } from "react-dom";import packageJson from "../package.json";const createGlobalState = (initialGlobalState: Record) => { let globalState: Record = initialGlobalState; const callbacks: OnGlobalStateChangeCallback[] = []; const onGlobalStateChange = (callback: OnGlobalStateChangeCallback,fireImmediately?: boolean ) => {callbacks.push(callback);if (fireImmediately) { callback(globalState, globalState);} }; const setGlobalState = (newState: Record) => {const prevState = globalState;globalState = newState;callbacks.forEach((cb) => { cb(globalState, prevState);});return true; }; return {onGlobalStateChange,setGlobalState, };};const renderDev = async ( App: React.FC, rootSelector: string, initialGlobalState: Record) => { const basename = process.env.PUBLIC_URL || "/"; reactDomRender(,document.body.querySelector(rootSelector) );};export default renderDev;

接着,在src/index.ts文件,定义qiankun的挂载生命周期,以及子应用独立运行的判断。

import "./public-path";import ReactDOM from "react-dom";import { SubAppProps } from "./qiankun";import App from "./App";import "./index.less";const defaultRootSelector = "#root";if (process.env.NODE_ENV === "development" && !window.__POWERED_BY_QIANKUN__) { Promise.all([import("./renderDev")]).then(async ([{ default: render }]) => {// 可以在这里进行用户接口的请求。let user = { username: "子应用dev环境用户名", token: "子应用dev环境用户名token",};render(App, defaultRootSelector, { user: user,}); });}export async function bootstrap() {}export async function mount(props: SubAppProps) { ReactDOM.render(,props.container.querySelector(defaultRootSelector) );}export async function unmount(props: SubAppProps) { const ele = props.container.querySelector(defaultRootSelector); ele && ReactDOM.unmountComponentAtNode(ele);}

当我们运行npm run dev,我们在页面中得到的state.user?.username为子dev环境用户名

如何通过docker 部署。

qiankun微前端架构通过docker镜像部署方式:

docker 创建 bridge net:

docker network create -d bridge --subnet 172.19.0.0/24 --gateway 172.19.0.1 mirc-qiankun-net172.19.0.0 docker 创建的网卡ip,可根据部署环境更改mirc-woody-net 创建的网卡名称

主应用:在 Dockerfile 配置 docker 容器 nginx , 以便访问子应用。

example

FROM nginxVOLUME /tmpENV LANG en_US.UTF-8RUN echo "server { \listen80; \ #解决Router(mode: 'history')模式下,刷新路由地址不能找到页面的问题 \ location / { \ root/var/www/html/; \ index index.html index.htm; \ if (!-e \$request_filename) { \ rewrite ^(.*)\$ /index.html?s=\$1 last; \ break; \ } \ } \ location /system-login/ { \proxy_pass http://172.19.0.3;\proxy_set_header Host \$host; \ } \ location /system-sidebar/ { \proxy_pass http://172.19.0.4;\proxy_set_header Host \$host; \ } \ location /system-navbar/ { \proxy_pass http://172.19.0.5;\proxy_set_header Host \$host; \ } \ location /system-setting/ { \proxy_pass http://172.19.0.6;\proxy_set_header Host \$host; \ } \ access_log /var/log/nginx/access.log; \ }" > /etc/nginx/conf.d/default.conf \&& mkdir -p /var/www \&& mkdir -p /var/www/htmlADD dist/ /var/www/html/EXPOSE 80EXPOSE 443其中, location 配置的是微前端主应用注册子应用的 entry 入口。 ##### 注册子应用示例{ name: "login", entry: "/system-login/", container: "#micro-app-slot" as const, activeRule: "/login", basename: "/login", path: "/login", noAuth: true,},proxy_pass 配置的是子应用在 docker 创建的网关内指定的ip访问地址。

👇会讲如何在子应用挂载docker网关ip

 

子应用(以当前子应用模版为例):1: craco.config.js 的配置修改

webpack: {configure: {output: {publicPath:process.env.NODE_ENV === "production" ? `/system-navbar/` : "/",library: `${packageName}-[name]`,libraryTarget: "umd",jsonpFunction: `webpackJsonp_${packageName}`,},},}主要修改两个地方,publicPath 生产设置为主应用的 entry 路径, library 最好设置为 注册子应用时的name。

2: 确保 package.json 文件的 name 字段值唯一,不与其他子应用冲突   3: 子应用 Dcokerfile

FROM nginxVOLUME /tmpENV LANG en_US.UTF-8RUN echo "server { \listen80; \#解决Router(mode: 'history')模式下,刷新路由地址不能找到页面的问题 \location / { \root/var/www/html/; \index index.html index.htm; \if (!-e \$request_filename) { \rewrite ^(.*)\$ /index.html?s=\$1 last; \break; \} \} \access_log /var/log/nginx/access.log ; \} " > /etc/nginx/conf.d/default.conf \&& mkdir -p /var/www \&& mkdir -p /var/www/htmlCOPY ./build /var/www/html/system-navbarADD build/ /var/www/html/EXPOSE 80EXPOSE 443

docker 命令   正常构建镜像:  

docker build -f Dockerfile -t platform-end:v1.0 .docker build -f Dockerfile -t mirc-sidebar:v1.0 .docker build -f Dockerfile -t mirc-navbar:v1.0 .docker build -f Dockerfile -t mirc-system-setting:v1.0 .

运行容器时,需要制定docker网关,以及对应的ip, 指定的ip 即为主应用nginx代理的ip地址 

docker run -d -p 8099:80 --net mirc-qiankun-net --ip 172.19.0.2 --name mirc-main platform-end:v1.0docker run -d -p 9000:80 --net mirc-qiankun-net --ip 172.19.0.3 --name mirc-login mirc-woody-login:v1.0docker run -d -p 9001:80 --net mirc-qiankun-net --ip 172.19.0.4 --name mirc-sidebar mirc-sidebar:v1.0docker run -d -p 9002:80 --net mirc-qiankun-net --ip 172.19.0.5 --name mirc-navbar mirc-navbar:v1.0docker run -d -p 9003:80 --net mirc-qiankun-net --ip 172.19.0.6 --name mirc-system-setting mirc-system-setting:v1.0

服务器只需配置上述配置的主应用8099端口即可访问整个项目。demo仓库(主应用)如果你觉得有用的话,帮忙点个赞👍。

source: 微前端qiankun+docker+nginx配合gitlab-ci/cd的自动化部署的实现qiankuncreate-react-appantd

相关推荐: