阅读 251

手摸手教你用qiankun开发微前端项目(一)

  在做中后台的前端时,经常会遇到这样的痛点:

(1)中后台的系统很多,功能上比较独立,但是运营人员在使用的时候还是希望统一入口。这样多个项目就不得不在一个仓库中来维护,久而久之,项目代码就会变得越来越庞大,难以管理。
(2)涉及到基础组建的升级,由于多个项目可能用到的都是同一个基础组件,所以不得不所有项目都对组件的升级做适配后,新组件才能被使用,不太灵活。
(3)有时我们想把不同技术栈的项目整合到一个前端入口页面中。

  所以,我们可以通过微前端的思想来解决,微前端可以把每个系统拆成独立的服务,有自己独立的仓库,最后通过一个基座项目来在各个独立的子项目之间切换,并且可以给用户类似于一个单页面应用的顺滑体验。

  效果如下:

截屏2021-06-13 下午2.05.20.png

  下面我们就通过从0到1的搭建和部署一个极简微前端架构的过程,手把手教大家如何使用微前端。本文将会分两个流程来讲解:1.开发流程。2.部署流程。每个流程分三个步骤来介绍:1.基座项目。2.react子系统。3.vue子系统。

开发流程

基座项目

  主应用(基座)不限技术栈,只需要提供一个容器 DOM,然后注册微应用并 start 即可。先安装 qiankun :

$ yarn add qiankun # 或者 npm i qiankun -S
复制代码

  然后写打包入口文件index.js: 文件开头我们需要引入qiankun的一些库函数,引入主应用的样式文件和render函数。

// index.js

import { registerMicroApps, runAfterFirstMounted, setDefaultMountApp, start, initGlobalState } from 'qiankun';
import './index.less';

/**
 * 主应用 **可以使用任意技术栈**
 * 以下分别是 React 和 Vue 的示例,可切换尝试
 */
import render from './render/ReactRender';
// import render from './render/VueRender';
复制代码

  插一小段来介绍一下render函数,这里render函数可以使用react也可以使用vue,两种写法如下:

// render/ReactRender.jsx 

import React from 'react';
import ReactDOM from 'react-dom';

/**
 * 渲染子应用
 */
function Render(props) {
  const { loading } = props;

  return (
    <>
      {loading && <h4 className="subapp-loading">Loading...</h4>}
      <div id="subapp-viewport" />
    </>
  );
}

export default function render({ loading }) {
  const container = document.getElementById('subapp-container');
  ReactDOM.render(<Render loading={loading} />, container);
}
复制代码
// render/VueRender.js

import Vue from 'vue/dist/vue.esm';

function vueRender({ loading }) {
  return new Vue({
    template: `
      <div id="subapp-container">
        <h4 v-if="loading" class="subapp-loading">Loading...</h4>
        <div id="subapp-viewport"></div>
      </div>
    `,
    el: '#subapp-container',
    data() {
      return {
        loading,
      };
    },
  });
}

let app = null;

export default function render({ loading }) {
  if (!app) {
    app = vueRender({ loading });
  } else {
    app.loading = loading;
  }
}
复制代码

  这里渲染函数的功能就是,定义出子系统挂载的元素:id='subapp-viewport'。并且在子系统真正挂载到目标元素之前,渲染loading状态。这里两个应用所挂载到的元素为id='subapp-container',是在html模版中定义的:

//index.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>全栈编程</title>
</head>

<body>
  <div class="mainapp">
    <!-- 标题栏 -->
    <header class="mainapp-header">
      <h1>全栈编程</h1>
    </header>
    <div class="mainapp-main">
      <!-- 侧边栏 -->
      <ul class="mainapp-sidemenu">
        <li onclick="push('/reactapp')">reactapp</li>
        <li onclick="push('/vue')">Vue</li>
      </ul>
      <!-- 子应用  -->
      <main id="subapp-container"></main>
    </div>
  </div>
  
  <script>
    function push(subapp) { history.pushState(null, subapp, subapp) }
  </script>
</body>
</html>
复制代码

  两个render函数中的app会挂载到“子应用”元素之中。样式如下:

// index.less

// 主应用慎用 reset 样式
body {
  margin: 0;
}

.mainapp {
  // 防止被子应用的样式覆盖
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
  line-height: 1;
}

.mainapp-header {
  >h1 {
    color: #333;
    font-size: 36px;
    font-weight: 700;
    margin: 0;
    padding: 36px;
  }
}

.mainapp-main {
  display: flex;

  .mainapp-sidemenu {
    width: 130px;
    list-style: none;
    margin: 0;
    margin-left: 40px;
    padding: 0;
    border-right: 2px solid #aaa;

    >li {
      color: #aaa;
      margin: 20px 0;
      font-size: 18px;
      font-weight: 400;
      cursor: pointer;

      &:hover {
        color: #444;
      }

      &:first-child {
        margin-top: 5px;
      }
    }
  }
}

// 子应用区域
#subapp-container {
  flex-grow: 1;
  position: relative;
  margin: 0 40px;

  .subapp-loading {
    color: #444;
    font-size: 28px;
    font-weight: 600;
    text-align: center;
  }
}

复制代码

  由于我们在主应用中使用的是react方式,所以还需要install相关的包:

npm install react react-dom -S
复制代码

  整个package.json文件如下,可以参考:

//package.json

{
  "name": "main",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "webpack-dev-server",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "devDependencies": {
    "@babel/core": "^7.7.2",
    "@babel/plugin-transform-react-jsx": "^7.7.0",
    "@babel/preset-env": "^7.7.1",
    "babel-loader": "^8.0.6",
    "css-loader": "^3.2.0",
    "html-webpack-plugin": "^3.2.0",
    "less-loader": "^6.2.0",
    "style-loader": "^1.0.0",
    "webpack": "^4.41.2",
    "webpack-cli": "^3.3.10",
    "webpack-dev-server": "^3.9.0",
    "cross-env": "^7.0.2"
  },
  "dependencies": {
    "qiankun": "^2.4.1",
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "vue": "^2.6.11"
  }
}
复制代码

  下面我们再回来说最重要的入口文件index.js,引入必要的包和函数后,第一步需要先加载loading状态和定义loader函数:

//index.js

/**
 * Step1 初始化应用(可选)
 */
render({ loading: true });

const loader = loading => render({ loading });
复制代码

  第二步通过registerMicroApps注册子应用,其中参数有子系统名称、入口,由于我们在开发流程中是分多个端口起的服务,所以开发阶段分别定为3000端口和9000端口。container即为上面render函数定义的渲染子系统的id。loader即为所定义的render函数,有一个参数为是否正在加载。activeRule即为激活子系统的路由。

//index.js

/**
 * Step2 注册子应用
 */
registerMicroApps(
  [
    {
      name: 'reactapp',
      // entry: '/child/reactapp/',
      entry: 'http://localhost:3000',
      container: '#subapp-viewport',
      loader,
      activeRule: '/reactapp',
    },
    {
      name: 'vue',
      // entry: '/child/vue/',
      entry: 'http://localhost:9000',
      container: '#subapp-viewport',
      loader,
      activeRule: '/vue',
    }
  ],
  {
    beforeLoad: [
      app => {
        console.log('[LifeCycle] before load %c%s', 'color: green;', app.name);
      },
    ],
    beforeMount: [
      app => {
        console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name);
      },
    ],
    afterUnmount: [
      app => {
        console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name);
      },
    ],
  },
);

const { onGlobalStateChange, setGlobalState } = initGlobalState({
  user: 'qiankun',
});

onGlobalStateChange((value, prev) => console.log('[onGlobalStateChange - master]:', value, prev));

setGlobalState({
  ignore: 'master',
  user: {
    name: 'master',
  },
});
复制代码

  第三步为设置默认激活的子系统:

//index.js

/**
 * Step3 设置默认进入的子应用
 */
setDefaultMountApp('reactapp');
复制代码

  第四步调用start(),并且设置runAfterFirstMounted钩子:

//index.js

/**
 * Step4 启动应用
 */
start();

runAfterFirstMounted(() => {
  console.log('[MainApp] first app mounted');
});
复制代码

下一篇,我们继续讲解如何配置react子系统。

文章分类
前端
文章标签