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

我的项目构造预览


后期筹备工作

  1. 主利用的搭建、基座的配置。
  2. 子利用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
    1. 172.19.0.0 docker 创立的网卡ip,可依据部署环境更改
    2. mirc-woody-net 创立的网卡名称
  • 主利用:在 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