美文网首页js css html
微前端解决方案-qiankun实战及部署(持续更新中。。。)

微前端解决方案-qiankun实战及部署(持续更新中。。。)

作者: e只咸鱼 | 来源:发表于2022-05-29 16:23 被阅读0次

    一.导读

    1.什么是微前端
    • 微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。
    • 微前端架构具备以下几个核心价值:
      技术栈无关 : 主框架不限制接入应用的技术栈,微应用具备完全自主权
      独立开发、独立部署 : 微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
      增量升级:在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
      独立运行时 : 每个微应用之间状态隔离,运行时状态不共享
    • 微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用(Frontend Monolith)后,随之而来的应用不可维护的问题。这类问题在企业级 Web 应用中尤其常见。
    2.qiankun是什么
    • qiankun 是一个基于 single-spa微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。
      官网: https://qiankun.umijs.org/zh
    • qiankun特性
      基于 single-spa 封装,提供了更加开箱即用的 API。
      技术栈无关,任意技术栈的应用均可 使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。
      HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
      样式隔离,确保微应用之间样式互相不干扰。
      JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
      ⚡️ 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
      umi 插件,提供了 @umijs/plugin-qiankun 供 umi 应用一键切换成微前端架构系统。
    • 了解完理论基础,让我们动手实践一下···

    二.建立项目

    image.png
    如图: 我建立了一个主应用和三个子应用
    主应用 main vue3搭建 "vue": "^3.0.0",
    子应用 micro-react react18搭建 "react": "^18.1.0",
    子应用 micro-vue2 vue2搭建 "vue": "^2.6.11",
    子应用 micro-vue3 vue3搭建 "vue": "^3.0.0",

    注意 : vue3技术选型我使用的是vue3 + webpack ,vite目前对于qiankun还不是太友好 ,硬要搞vite代价会很大,后续等官网优化后我们在去使用vite

    由于搭建项目太简单我就不说明了 ~ ovo

    三.主应用

    注意: qiankun 需要一个主应用 来注入所有的子应用
    先安装乾坤的依赖包

     yarn add qiankun # 或者 npm i qiankun -S
    

    目前乾坤是2.0版本 安装后package.json 是2.72版本

    image.png

    在安装 element-plus 把项目的布局简单做一下

    npm install element-plus --save
    

    注意: vue3 安装element-plus, vue2安装element-ui

    src下新建micro-app.js 用于存放所有子应用
    const microApps = [
      {
        name: 'micro-react', //应用名 项目名最好也是这个
        entry: '//localhost:20000', //默认会加载这个html 解析里面的js 动态的执行 (子应用必须支持跨域)内部用的fetch
        activeRule: '/react', // 激活的路径
        container: '#micro-react', // 容器名
        props: {}, //父子应用通信
      },
      {
        name: 'micro-vue2',
        entry: '//localhost:30000',
        activeRule: '/vue2',
        container: '#micro-vue2',
        props: {},
      },
      {
        name: 'micro-vue3',
        entry: '//localhost:40000',
        activeRule: '/vue3',
        container: '#micro-vue3',
        props: {},
      },
    ];
    
    export default microApps;
    
    新建vue.config.js
    module.exports = {
      devServer: {
        port: 8000,
        headers: {
          // 重点1: 允许跨域访问子应用页面
          'Access-Control-Allow-Origin': '*',
        },
      },
    };
    
    Main页面
    import { createApp } from 'vue'
    import App from './App.vue'
    import router from './router'
    import store from './store'
    // createApp(App).use(store).use(router).mount('#app')
    
    //-----------------------上面是原先的,下面是新增的-----------------------------
    
    import ElementPlus from 'element-plus'; //element-plus
    import 'element-plus/dist/index.css'; //element-plus
    import { registerMicroApps, start } from 'qiankun';
    import microApps from './micro-app';
    
    let app = createApp(App);
    app.use(store);
    app.use(router);
    app.use(ElementPlus);
    app.mount('#app');
    
    registerMicroApps(microApps, {
      //还有一些生命周期 如果需要可以根据官网文档按需加入
      beforeMount(app) {
        console.log('挂载前', app);
      },
      afterMount(app) {
        console.log('卸载后', app);
      },
    });
    
    start({
      prefetch: false, //取消预加载
    }); 
    
    进入App页面简单调下布局
    <template>
      <el-menu :default-active="activeIndex" :router="true" mode="horizontal">
        <!-- 基座中可以放自己的路由 -->
        <el-menu-item index="/">主应用 main</el-menu-item>
        <!-- 引用其他子应用 -->
        <el-menu-item index="/react">子应用 react18</el-menu-item>
        <el-menu-item index="/vue2">子应用 vue2</el-menu-item>
        <el-menu-item index="/vue3">子应用 vue3</el-menu-item>
      </el-menu>
    
      <router-view />
      <!-- 子应用的容器 -->
      <div id="micro-react"></div>
      <div id="micro-vue2"></div>
      <div id="micro-vue3"></div>
    </template>
    
    <script setup>
    import { ref } from 'vue';
    const activeIndex = ref('/');
    </script>
    
    <style lang="less">
    html,
    body,
    #app {
      width: 100%;
      height: 100%;
      margin: 0;
    }
    #app {
      font-family: Avenir, Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      text-align: center;
      color: #2c3e50;
    }
    </style>
    

    需要注意: app里的容器名和跳转路径都不是随便起的 需要和micro-app.js 定义好的子应用一一对应

    image.png

    到此主应用搭建完毕~~~ovo

    四.子应用

    1.react

    安装npm install react-app-rewired 重写默认的react配置文件
    npm install react-app-rewired --save
    

    修改package.json,原本的react-script 改为react-app-rewired

    "scripts": {
       "start": "react-app-rewired start",
       "build": "react-app-rewired build",
       "test": "react-app-rewired test",
       "eject": "react-app-rewired eject"
     },
    
    安装npm i react-router-dom 我安装的是最新版本 "react-router-dom": "^6.3.0"
    npm i react-router-dom --save
    
    根目录下新建.env文件
    PORT=20000
    #  防止热更新出错
    WDS_SOCKET_PORT=20000 
    
    src下新建public-path.js (用于修改运行时的 publicPath)
    //判断是否是qiankun加载
    if (window.__POWERED_BY_QIANKUN__) {
      __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
    }
    
    src下新建 config-overrides.js
    const { name } = require('./package');
    
    module.exports = {
      webpack: config => {
        config.output.library = `${name}-[name]`; 
        config.output.libraryTarget = 'umd';
        // config.output.jsonpFunction = `webpackJsonp_${name}`;
        config.output.globalObject = 'window';
        return config;
      },
      devServer: _ => {
        const config = _;
    
        config.headers = {
          'Access-Control-Allow-Origin': '*',
        };
        config.historyApiFallback = true;
        config.hot = false;
        config.watchContentBase = false;
        config.liveReload = false;
    
        return config;
      },
    };
    
    
    进入src下index.js
    // import logo from './logo.svg';
    // import './App.css';
    
    // function App() {
    //   return (
    //     <div className="App">
    //       <header className="App-header">
    //         <img src={logo} className="App-logo" alt="logo" />
    //         <p>
    //           Edit <code>src/App.js</code> and save to reload.
    //         </p>
    //         <a
    //           className="App-link"
    //           href="https://reactjs.org"
    //           target="_blank"
    //           rel="noopener noreferrer"
    //         >
    //           Learn React
    //         </a>
    //       </header>
    //     </div>
    //   );
    // }
    
    // export default App;
    
    // ------------------------上面原先的,下面最新的------------------------------------
    
    import logo from './logo.svg';
    import './App.css';
    import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
    
    function App() {
      return (
        <>
          {/* basename 判断如果是qiankun加载 basename为react 相当于加个标识*/}
          <Router basename={window.__POWERED_BY_QIANKUN__ ? '/react' : '/'}>
            {/*  */}
            <Link to="/">首页</Link>
            <Link to="/about">关于页面</Link>
            <Routes>
              <Route path="/" element={<Home />}></Route>
              <Route path="/about" element={<About />}></Route>
            </Routes>
          </Router>
        </>
      );
    }
    
    function About() {
      return <div>about</div>;
    }
    
    function Home() {
      return (
        <div>
          <header className="App-header">
            <img src={logo} className="App-logo" alt="logo" />
            <p>
              Edit <code>src/App.js</code> and save to reload.
            </p>
            <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer">
              Learn React
            </a>
          </header>
        </div>
      );
    }
    export default App;
    
    

    2.vue2

    src下新建public-path.js 用于修改运行时的 publicPath
    // eslint-disable-next-line no-undef
    if (window.__POWERED_BY_QIANKUN__) {
      __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
    }
    
    在main页面 引入public-path.js文件
    // import Vue from 'vue';
    // import App from './App.vue';
    // import router from './router';
    
    // Vue.config.productionTip = false
    
    // new Vue({
    //   router,
    //   render: h => h(App)
    // }).$mount('#app')
    
    // ·················上面原先的 下面新增的·····················
    import './public-path';
    import Vue from 'vue';
    import App from './App.vue';
    import router from './router';
    
    // Vue.config.productionTip = false
    
    let instance = null;
    function render(props = {}) {
      const { container } = props;
      instance = new Vue({
        router,
        render: (h) => h(App),
      }).$mount(container ? container.querySelector('#app') : '#app');
    }
    
    // 如何独立运行微应用?
    if (!window.__POWERED_BY_QIANKUN__) {
      render();
    }
    
    export async function bootstrap(props) {
      // 启动
    }
    export async function mount(props) {
      // 挂载  onGlobalStateChange 可通过这个属性来进行父子应用通信 发布订阅机制
      render(props);
    }
    export async function unmount(props) {
      // 卸载
      instance.$destroy();
    }
    
    
    新增vue.config.js文件
    const { name } = require('./package');
    
    module.exports = {
      devServer: {
        port: 30000,
        headers: {
          'Access-Control-Allow-Origin': '*', //开发时增加跨域 表示所有人都可以访问我的服务器
        },
      },
      configureWebpack: {
        output: {
          library: `${name}-[name]`,
          libraryTarget: 'umd', // 把子应用打包成 umd 库格式
          jsonpFunction: `webpackJsonp_${name}`,
        },
      },
    };
    
    router.js文件
    const router = new VueRouter({
      mode: 'history',
      // base: process.env.BASE_URL,
      base: '/vue2',
      routes,
    });
    

    3.vue3

    src下新建public-path.js 用于修改运行时的 publicPath
    // eslint-disable-next-line no-undef
    if (window.__POWERED_BY_QIANKUN__) {
      __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
    }
    
    在main页面 引入public-path.js文件
    import './public-path'; // 注意需要引入public-path
    import { createApp } from 'vue';
    import App from './App.vue';
    import router from './router';
    import store from './store';
    
    let instance = null;
    
    function render({ container } = {}) {
      instance = createApp(App);
      instance.use(router);
      instance.use(store);
      instance.mount(container ? container.querySelector('#app') : '#app');
    }
    
    
    // 如何独立运行微应用?
    if (!window.__POWERED_BY_QIANKUN__) {
      render();
    }
    
    export async function bootstrap(props) {
      // 启动
    }
    export async function mount(props) {
      // 挂载
      render(props);
    }
    export async function unmount(props) {
      // 卸载
      instance.unmount();
      instance = null;
    }
    
    新增vue.config.js文件
    const { name } = require('./package');
    
    module.exports = {
      devServer: {
        port: 40000,
        headers: {
          'Access-Control-Allow-Origin': '*', //开发时增加跨域 表示所有人都可以访问我的服务器
        },
      },
      configureWebpack: {
        output: {
          library: `${name}-[name]`,
          libraryTarget: 'umd', // 把子应用打包成 umd 库格式
          jsonpFunction: `webpackJsonp_${name}`,
        },
      },
    };
    
    

    到这里项目搭建完毕,基础跳转没有问题 ,可以在主应用和子应用跳转
    bug :主应用和子应用使用不同版本的vue后路由切换报错 ?
    bug :主应用样式与子应用样式冲突 ?
    需求 :父子组件传参如何实现 ?
    需求 :如何部署 ?
    别担心 下面我一一解答

    5.bug

    [Bug]主应用和子应用使用不同版本的vue后路由切换报错

    问题的原因 : vue-router 3.x与vue-router 4.x设置的history.state的数据结构不同
    低版本的 vue-router 在 pushState 的时候,会覆盖丢失主路由的 history.state,导致主路由跳转异常
    解决办法 : 主应用监听router.beforEach 手动修改history.state数据结构

    import _ from "lodash"
    
    router.beforeEach((to, from, next) => {
      if (_.isEmpty(history.state.current)) {
        _.assign(history.state, { current: from.fullPath });
      }
      next();
    });
    
    [Bug]主应用样式与子应用样式冲突

    可以通过给css样式名加前缀来实现隔离
    https://blog.csdn.net/zjscy666/article/details/107864891
    https://blog.csdn.net/m0_54854484/article/details/123442168

    6.需求

    [需求] 父子组件传参如何实现

    qiankun通过initGlobalState, onGlobalStateChange, setGlobalState实现主应用的全局状态管理,然后默认会通过props将通信方法传递给子应用。先看下官方的示例用法:

    主应用

    // main/src/main.js
    import { initGlobalState } from 'qiankun';
    // 初始化 state
    const initialState = {
      user: {} // 用户信息
    };
    const actions = initGlobalState(initialState);
    actions.onGlobalStateChange((state, prev) => {
      // state: 变更后的状态; prev 变更前的状态
      console.log(state, prev);
    });
    actions.setGlobalState(state);
    actions.offGlobalStateChange();
    

    子应用

    // 从生命周期 mount 中获取通信方法,props默认会有onGlobalStateChange和setGlobalState两个api
    export function mount(props) {
      props.onGlobalStateChange((state, prev) => {
        // state: 变更后的状态; prev 变更前的状态
        console.log(state, prev);
      });
      props.setGlobalState(state);
    }
    

    这两段代码不难理解,父子应用通过onGlobalStateChange这个方法进行通信,这其实是一个发布-订阅的设计模式。
    ok,官方的示例用法很简单也完全够用,纯JavaScript的语法,不涉及任何的vue或react的东西,开发者可自由定制。

    如果我们直接使用官方的这个示例,那么数据会比较松散且调用复杂,所有子应用都得声明onGlobalStateChange对状态进行监听,再通过setGlobalState进行更新数据。

    因此,我们很有必要对数据状态做进一步的封装设计

    主应用src下新建actions.js
    //src/actions.js
    // 父子应用通信
    import { initGlobalState } from 'qiankun';
    import store from './store';
    
    const state = {
      //这里写初始化数据
      name: 'wang',
      age: 123,
      count: 0,
    };
    
    const actions = initGlobalState(state);
    
    actions.onGlobalStateChange((state, prev) => {
      console.log('主应用变更前:', state);
      console.log('主应用变更后:', prev);
      store.commit('setGlobalData', state);
    });
    
    store.commit('setGlobalData', state);
    
    export default actions;
    

    将初始化的数据存到vuex中 如果数据变更了 在将变更后的数据存到vuex

    主应用main store文件夹下index.js中
    //store/index.js 
    import { createStore } from 'vuex';
    
    export default createStore({
      state: {
        GlobalData: {},
      },
      mutations: {
        setGlobalData(state, value) {
          state.GlobalData = value;
        },
      },
      actions: {},
      modules: {},
    });
    

    最后在main.js 中导入

    //main.js
    import './actions.js'
    
    子应用 (vue3)

    核心:通过将主应用的onGlobalStateChange,setGlobalState方法挂载到全局就可以使用了

    import './public-path'; // 注意需要引入public-path
    import { createApp } from 'vue';
    import App from './App.vue';
    import router from './router';
    import store from './store';
    
    let instance = null;
    
    //核心
    function render(props) {
      const { container, onGlobalStateChange, setGlobalState } = props;
      console.log(props);
      instance = createApp(App);
      instance.config.globalProperties.$onGlobalStateChange = onGlobalStateChange;
      instance.config.globalProperties.$setGlobalState = setGlobalState;
      instance.use(router);
      instance.use(store);
      instance.mount(container ? container.querySelector('#app') : '#app');
    }
    
    // 如何独立运行微应用?
    if (!window.__POWERED_BY_QIANKUN__) {
      render();
    }
    
    export async function bootstrap(props) {
      // 启动
    }
    export async function mount(props) {
      // 挂载
      render(props);
    }
    export async function unmount(props) {
      // 卸载
      instance.unmount();
      instance = null;
    }
    
    使用

    主应用

    <template>
      <div>
        <h1>主应用/vue3子应用 的全局数据</h1>
        <div>姓名 : {{ $store.state.GlobalData.name }}</div>
        <div>年龄 : {{ $store.state.GlobalData.age }}</div>
        <div>数量 : {{ $store.state.GlobalData.count }}</div>
    
        <el-button type="primary" @click="revampData">修改全局数据</el-button>
      </div>
    </template>
    
    <script setup>
    import actions from '../actions';
    const revampData = () => {
      actions.setGlobalState({ name: '主应用' });
    };
    </script>
    
    

    子应用(vue3)

    <template>
        <div>我是vue3项目</div>
        <button @click="revampData">修改全局数据</button>
    </template>
    
    <script>
    import { getCurrentInstance } from 'vue';
    export default {
      name: 'Home',
      components: {
        HelloWorld,
      },
      setup() {
        const { proxy } = getCurrentInstance();
        const revampData = () => {
          proxy.$setGlobalState({ name: 'vue3子应用应用' });
        };
    
        return {
          revampData,
        };
      },
    };
    </script>
    
    

    [需求] 如何部署

    未完待续 。。。

    相关文章

      网友评论

        本文标题:微前端解决方案-qiankun实战及部署(持续更新中。。。)

        本文链接:https://www.haomeiwen.com/subject/dxggprtx.html