共计 13685 个字符,预计需要花费 35 分钟才能阅读完成。
前言
近两年始终会有遇到须要微前端框架的需要,同时在招聘上,微前端的需要也是挺多的,最近整顿了一下之前经手过的几个 qiankun 微前端我的项目,分享给大家。
我的项目构造预览
后期筹备工作
- 主利用的搭建、基座的配置。
- 子利用 template 的搭建(react)。
搭建主利用
在 workspace 建设 mirc-project 目录来寄存主利用和微利用
mkdir mirc-project // 创立目录
cd mirc-project
mkdir main // 创立主利用我的项目目录
cd main
npm 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-structure
yarn 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 匹配时跳转到该 app
defaultApp: "foo" # 当拜访根门路时,会跳转到该 app
apps:
- 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 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; \ } \ } \ access_log /var/log/nginx/access.log ; \ } " > /etc/nginx/conf.d/default.conf \ && mkdir -p /var/www \ && mkdir -p /var/www/html COPY ./build /var/www/html/system-navbar ADD build/ /var/www/html/ EXPOSE 80 EXPOSE 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.0 docker run -d -p 9000:80 --net mirc-qiankun-net --ip 172.19.0.3 --name mirc-login mirc-woody-login:v1.0 docker run -d -p 9001:80 --net mirc-qiankun-net --ip 172.19.0.4 --name mirc-sidebar mirc-sidebar:v1.0 docker run -d -p 9002:80 --net mirc-qiankun-net --ip 172.19.0.5 --name mirc-navbar mirc-navbar:v1.0 docker 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