美文网首页饥人谷技术博客
【微前端】10分钟学会乾坤大挪移

【微前端】10分钟学会乾坤大挪移

作者: 写代码的海怪 | 来源:发表于2021-05-17 23:14 被阅读0次

    前言

    今天刚刚学习了一个微前端框架——乾坤,正着热乎劲,写一篇入门博客。这篇文章不会讨论太多的原理和实现,只是一个入门写 Hello World 的教程。

    文章的很多地方都参考官网,但是官网的教程太简洁了,个人觉得还是做不到无脑上手,希望这篇文章可以帮到正在入门微前端的伙伴。

    想直接看代码的,我写了个比官网更简单的例子,点击即可学会。

    什么是微前端

    首先,来了解一下微前端是个啥。

    当我们写了一个又一个的 SPA 应用。突然有一天,老板说要将这些应用合并,前端工程们就头大了——每个应用的代码都是一座摇摇欲坠的💩山,别说合并了,就算动都不敢动呀。

    虽然很麻烦,但是前端工程师还是把这个问题解决了,而这个将多个 SPA 合并成一个 Web App 的解决方案就是微前端

    为什么要微前端

    “多个 SPA 合并成一个 Web App?”,可能有人会想到用 <iframe/> 也可以实现一个网页里内嵌多个网页呀。原因有:

    • 不感知 url 状态,比如前进、后退没法玩
    • UI 不同步、DOM 不同享。<iframe/> 本质上是页面的硬隔离,所以如果你有个遮罩层,可能只能在那一小片区域才展示遮罩层
    • 页面之间的通信很麻烦
    • 每次都要加载子应用,速度很慢

    而微前端正好可以补足上面的缺点。

    微前端的优势

    除了解决了上面的问题,微前端还有如下的优点:

    • 子应用技术栈无关,即类似上页说的页面硬隔离,但是是以 sandbox 的方式实现的
    • 合并多个子应用,相对地,也可以将大应用拆解成多个应用,实现业务解耦
    • 子应用高度自治,发布、报错、测试流程仅限于子应用,不会受别的业务影响,同时也不影响别的业务

    乾坤由来

    最原始的微前端框架并不是乾坤,而是 single-spa。但是这个框架只提供最基本的功能,而且全是英文,文档写得也很繁琐,应该没人想去看。

    阿里的乾坤则是基于 single-spa 开发的又一个微前端框架,提供了更多的功能,也解决一些坑,官网也很简洁。

    不过,个人觉得有点太简洁了,写 Hello World 的时候还是遇到一些坑,只能看 Github 的 /examples 目录学习。

    主应用 VS 子应用

    首先,要知道现在项目并不是只有一个了,而是区分出 主应用子应用,关系如下:

    两者区别:

    • 主应用

      • 概念:就是要统治各个子应用的应用,也即合并结果页面
      • 负责子应用的注册、路由分发。可以简单理解为 React.js 和 Vue.js 里的 App 组件,主要做一些初始化、路由注册、全局状态注册、销毁时的动作
    • 子应用

      • 概念:各个 SPA 应用,可以理解为 SPA 里的页面组件
      • 负责暴露一些函数,以此对接主应用,让主应用知道:哦,原来你是子应用,要和我对接。常见的对应函数有:bootstrap, mount, unmount

    项目创建

    乾坤官网最推荐的做法是将主应用和子应用分成两个项目,各自管理。当然,也可以一个项目里分成不同的目录来存放。

    ├── main    # 主应用
    ├── baidu   # 子应用
    └── taobao # 子应用
    

    如果你觉得 官方的例子 太复杂,也可以看我自己建的 qiankun-bigass-app,子应用只有两个用 React.js 的项目。我把很多无关的代码都删了。

    实现主应用

    理清上面的关系后,我们直接干代码,先看主应用。

    首先,我们弄一个 .html 文件出来,作为主页面的 HTML 模板:

    <body>
      <div class="mainapp">
        <!-- 标题栏 -->
        <header class="mainapp-header">
          <h1>QianKun</h1>
        </header>
        <div class="mainapp-main">
          <!-- 侧边栏 -->
          <ul class="mainapp-sidemenu">
            <li onclick="push('/taobao')">淘宝</li>
            <li onclick="push('/baidu')">百度</li>
          </ul>
          <!-- 子应用  -->
          <main id="subapp-container"></main>
        </div>
      </div>
    
      <script>
        function push(subapp) { history.pushState(null, subapp, subapp) }
      </script>
    </body>
    

    然后,使用 Webpack,指定为 template HTML,并配置 dev server,注意一定要配置 headers,不然会有跨域的问题,子应用同理:

    // webpack.config.js
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    
    module.exports = {
      entry: './index.js',
      devtool: 'source-map',
      devServer: {
        open: true,
        port: '7099',
        clientLogLevel: 'warning',
        disableHostCheck: true,
        compress: true,
        headers: {
          'Access-Control-Allow-Origin': '*',
        },
        historyApiFallback: true,
        overlay: { warnings: false, errors: true },
      },
      output: {
        publicPath: '/',
      },
      mode: 'development',
      resolve: {
        extensions: ['.js', '.jsx', '.ts', '.tsx'],
      },
      module: {
        rules: [
          {
            test: /\.jsx?$/,
            exclude: /node_modules/,
            use: {
              loader: 'babel-loader',
              options: {
                presets: ['@babel/preset-env'],
                plugins: ['@babel/plugin-transform-react-jsx'],
              },
            },
          },
          {
            test: /\.(le|c)ss$/,
            use: ['style-loader', 'css-loader', 'less-loader'],
          },
        ],
      },
      plugins: [
        new HtmlWebpackPlugin({
          filename: 'index.html',
          template: process.env.MODE === 'multiple' ? './multiple.html' : './index.html',
          minify: {
            removeComments: true,
            collapseWhitespace: true,
          },
        }),
      ],
    };
    

    入口文件 index.js 就比较重要了,需要完成主应用的很多事情:

    import { registerMicroApps, runAfterFirstMounted, setDefaultMountApp, start, initGlobalState } from 'qiankun';
    import './index.less';
    
    /**
     * 主应用 **可以使用任意技术栈**
     * 以下分别是 React 和 Vue 的示例,可切换尝试
     */
    import render from './Render';
    // import render from './render/VueRender';
    
    /**
     * Step1 初始化应用(可选)
     */
    render({ loading: true });
    
    const loader = loading => render({ loading });
    
    /**
     * Step2 注册子应用
     */
    
    registerMicroApps(
      [
        {
          name: 'taobao',
          entry: '//localhost:7101',
          container: '#subapp-viewport',
          loader,
          activeRule: '/taobao',
        },
        {
          name: 'baidu',
          entry: '//localhost:7102',
          container: '#subapp-viewport',
          loader,
          activeRule: '/baidu',
        },
      ],
      {
        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), true);
    
    setGlobalState({
      ignore: 'master',
      user: {
        name: 'master',
      },
    });
    
    /**
     * Step3 设置默认进入的子应用
     */
    setDefaultMountApp('/taobao');
    
    /**
     * Step4 启动应用
     */
    start();
    
    runAfterFirstMounted(() => {
      console.log('[MainApp] first app mounted');
    });
    

    上面主要完成:初始化、注册子应用、设置配置全局状态、设置默认进入子应用、启动应用。

    至于初始渲染函数,可以这么写:

    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);
    }
    

    实现子应用

    子应用其实和官网的差不多,这里以 React.js 子应用举例。首先用 create-react-app 来创建子应用:

    create-react-app baidu
    

    src 目录下新增 public-path.js

    if (window.__POWERED_BY_QIANKUN__) {
      __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
    }
    

    设置 history 模式路由的 base:

    const RouteExample = () => {
      return (
        <Router basename={window.__POWERED_BY_QIANKUN__ ? '/baidu' : '/'}>
          <nav>
            <Link to="/">Home</Link>
            <Link to="/about">About</Link>
          </nav>
          <Suspense fallback={null}>
            <Switch>
              <Route path="/" exact component={Home} />
              <Route path="/about" component={About} />
            </Switch>
          </Suspense>
        </Router>
      );
    };
    
    export default function App() {
      return (
        <div className="app-main">
          <h1>淘宝Taobao</h1>
    
          <hr/>
    
          <RouteExample />
        </div>
      );
    }
    

    __POWERED_BY_QIANKUN__ 用于判断现在是否作为子应用被访问,其它地方与普通 React.js App 没差别。

    去掉一些无用的文件后,在入口配置子应用:

    function render(props) {
      const { container } = props;
      ReactDOM.render(<App />, container ? container.querySelector('#root') : document.querySelector('#root'));
    }
    
    if (!window.__POWERED_BY_QIANKUN__) {
      render({});
    }
    
    // 监听全局状态
    function storeTest(props) {
      props.onGlobalStateChange((value, prev) => console.log('淘宝', `[onGlobalStateChange - ${props.name}]:`, value, prev), true);
      props.setGlobalState({
        ignore: props.name,
        user: {
          name: props.name,
        },
      });
    }
    
    export async function bootstrap() {
      console.log('[淘宝] react app bootstraped');
    }
    
    export async function mount(props) {
      console.log('[淘宝] props from main framework', props);
      storeTest(props);
      render(props);
    }
    
    export async function unmount(props) {
      const { container } = props;
      ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.querySelector('#root'));
    }
    

    注意上面的 bootstrap, mountunmount 一定要 export 出去,不然没人知道这个是子应用。

    下一步,是修改 Webpack 的配置。但是 creat-react-app 造出来的 React App 不 eject 出来就改不了,这里官网推荐使用 @rescripts/cli 来修改:

    yarn add -D @rescript/cli
    

    在根目录添加 .rescriptsrc.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;
      },
    };
    

    Webpack 配置同样很重要,一个是配置 historyApiFallback 处理单页的 404 问题,另一个是通过 Access-Control-Allow-Origin 解决主应用访问子应用的跨域问题。

    在上面的主应用里看到我们是要访问不同的端口的,那端口要怎么配置呢?可以通过 .env 来配置:

    SKIP_PREFLIGHT_CHECK=true
    BROWSER=none
    PORT=7101
    WDS_SOCKET_PORT=7102
    

    更多框架的配置可见这里

    API 粗讲

    乾坤的 API 也不是很多,详见这里。简单讲一下用处:

    API 用处 类比
    registerMicroApps 主应用用来注册多个子应用的函数 类似于 Vue 和 React 的路由
    start 启动主应用 类似于 React.js 的 render 函数和 Vue.js 的 new Vue()
    loadMicroApp 手动加载子应用 也类似于 React.js 的 render 函数和 Vue.js 的 new Vue(),只不过更自由了
    prefetchApps 预加载子应用 类似于 Webpack 的 prefetch 功能
    addGlobalUncaughtErrorHandler 页面报错时可以用于上报和兜底 -
    removeGlobalUncaughtErrorHandler 都懂的 -
    initGlobalState 初始化全局状态 类似于 Redux 的 createStore 和 Vue 的 new Vue.Store()

    FAQ

    请看 常见问题,最佳实践和遇到的问题都能在这里解决。

    (完)

    相关文章

      网友评论

        本文标题:【微前端】10分钟学会乾坤大挪移

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