前言
近两年始终会有遇到须要微前端框架的需要,同时在招聘上,微前端的需要也是挺多的,最近整顿了一下之前经手过的几个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援用,如下:
<body> <div id="wrapper"> <div id="sidebar-slot"></div> <div id="container"> <div id="navbar-slot"></div> <div id="micro-app-wrapper"> <!-- loading icon --> <div id="loading-wrapper"> <div class="sc-bdnxRM cCKQJl"> <div class="sc-gtsrHT kzzTWM"></div> <div class="sc-gtsrHT kzzTWM"></div> <div class="sc-gtsrHT kzzTWM"></div> <div class="sc-gtsrHT kzzTWM"></div> </div> </div> <div id="micro-app-slot"></div> </div> </div> </div> <script type="module" src="./index.ts"></script></body>
装置一下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 registered entry: "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<User> = async () => { // 能够在这里调用用户信息接口 // todo return { username: "test", token: "test_token" }; }; export default getUser;
定义我的项目全局的state
// 定义export interface GlobalState { user: User | null; refreshToken: () => Promise<string>;}const initGlobalState = ( initialState: Partial<GlobalState>, apps: MicroApplication[]) => { const actions = qiankunInitGlobalState({ ...initialState, }); return actions;};export default initGlobalState;
定义全局子利用状态action store
import { initGlobalState as qiankunInitGlobalState, MicroAppStateActions, } from "qiankun"; let _state: Parameters<typeof qiankunInitGlobalState>[0] = {}; let _stateChangeFns: Parameters< MicroAppStateActions["onGlobalStateChange"] >[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( <App basename={props.basename} subAppProps={props} styledTarget={props.container} />, 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<string>) | undefined;}export interface GlobalStateContext { state: GlobalState; setToken: (token: string | null) => void; getToken: () => string | null;}const appGlobalContainer = createContainer< GlobalStateContext, Pick<SubAppProps, "onGlobalStateChange" | "setGlobalState">>((initialState) => { const [state, setState] = useState<GlobalState>({ 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<GlobalState>) => { 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 ( <PageWrap> <h4>App Global State: {state.user?.username}</h4> {/* <h3># Page operation update</h3> */} </PageWrap> );}
将在页面看到 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<string, any>, prevState: Record<string, any>) => void;export interface SubAppProps { name: string; basename: string; container: HTMLElement; onGlobalStateChange: ( callback: OnGlobalStateChangeCallback, fireImmediately?: boolean ) => void; setGlobalState: (state: Record<string, any>) => 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<string, any>) => { let globalState: Record<string, any> = initialGlobalState; const callbacks: OnGlobalStateChangeCallback[] = []; const onGlobalStateChange = ( callback: OnGlobalStateChangeCallback, fireImmediately?: boolean ) => { callbacks.push(callback); if (fireImmediately) { callback(globalState, globalState); } }; const setGlobalState = (newState: Record<string, any>) => { const prevState = globalState; globalState = newState; callbacks.forEach((cb) => { cb(globalState, prevState); }); return true; }; return { onGlobalStateChange, setGlobalState, };};const renderDev = async ( App: React.FC<{ basename: string; subAppProps: SubAppProps }>, rootSelector: string, initialGlobalState: Record<string, any>) => { const basename = process.env.PUBLIC_URL || "/"; reactDomRender( <App basename={basename} subAppProps={{ container: document.body, name: packageJson.name, basename: basename, ...createGlobalState(initialGlobalState), }} />, 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( <App basename={props.basename} subAppProps={props} styledTarget={props.container} />, 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-net
- 172.19.0.0
docker
创立的网卡ip,可依据部署环境更改 - mirc-woody-net 创立的网卡名称
- 172.19.0.0
主利用:在
Dockerfile
配置docker
容器nginx
, 以便拜访子利用。example
FROM nginx VOLUME /tmp ENV LANG en_US.UTF-8 RUN echo "server { \ listen 80; \ #解决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/html ADD dist/ /var/www/html/ EXPOSE 80 EXPOSE 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 { \ listen 80; \ #解决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的自动化部署的实现
qiankun
create-react-app
antd