【微前端】10分鐘學會乾坤大挪移

前言

今天剛剛學習了一個微前端框架——乾坤,正著熱乎勁刁俭,寫一篇入門博客。這篇文章不會討論太多的原理和實現(xiàn)畴蒲,只是一個入門寫 Hello World 的教程虹统。

文章的很多地方都參考官網(wǎng),但是官網(wǎng)的教程太簡潔了吨岭,個人覺得還是做不到無腦上手拉宗,希望這篇文章可以幫到正在入門微前端的伙伴。

想直接看代碼的辣辫,我寫了個比官網(wǎng)更簡單的例子旦事,點擊即可學會。

什么是微前端

首先急灭,來了解一下微前端是個啥姐浮。

當我們寫了一個又一個的 SPA 應用。突然有一天化戳,老板說要將這些應用合并单料,前端工程們就頭大了——每個應用的代碼都是一座搖搖欲墜的??山,別說合并了点楼,就算動都不敢動呀扫尖。

雖然很麻煩,但是前端工程師還是把這個問題解決了掠廓,而這個將多個 SPA 合并成一個 Web App 的解決方案就是微前端换怖。

為什么要微前端

“多個 SPA 合并成一個 Web App?”蟀瞧,可能有人會想到用 <iframe/> 也可以實現(xiàn)一個網(wǎng)頁里內(nèi)嵌多個網(wǎng)頁呀沉颂。原因有:

  • 不感知 url 狀態(tài)条摸,比如前進、后退沒法玩
  • UI 不同步铸屉、DOM 不同享钉蒲。<iframe/> 本質(zhì)上是頁面的硬隔離,所以如果你有個遮罩層彻坛,可能只能在那一小片區(qū)域才展示遮罩層
  • 頁面之間的通信很麻煩
  • 每次都要加載子應用顷啼,速度很慢

而微前端正好可以補足上面的缺點。

微前端的優(yōu)勢

除了解決了上面的問題昌屉,微前端還有如下的優(yōu)點:

  • 子應用技術棧無關钙蒙,即類似上頁說的頁面硬隔離,但是是以 sandbox 的方式實現(xiàn)的
  • 合并多個子應用间驮,相對地躬厌,也可以將大應用拆解成多個應用,實現(xiàn)業(yè)務解耦
  • 子應用高度自治竞帽,發(fā)布扛施、報錯、測試流程僅限于子應用抢呆,不會受別的業(yè)務影響煮嫌,同時也不影響別的業(yè)務

乾坤由來

最原始的微前端框架并不是乾坤,而是 single-spa抱虐。但是這個框架只提供最基本的功能昌阿,而且全是英文,文檔寫得也很繁瑣恳邀,應該沒人想去看懦冰。

阿里的乾坤則是基于 single-spa 開發(fā)的又一個微前端框架,提供了更多的功能谣沸,也解決一些坑刷钢,官網(wǎng)也很簡潔。

不過乳附,個人覺得有點太簡潔了内地,寫 Hello World 的時候還是遇到一些坑,只能看 Github 的 /examples 目錄學習赋除。

主應用 VS 子應用

首先阱缓,要知道現(xiàn)在項目并不是只有一個了,而是區(qū)分出 主應用子應用举农,關系如下:

兩者區(qū)別:

  • 主應用

    • 概念:就是要統(tǒng)治各個子應用的應用荆针,也即合并結(jié)果頁面
    • 負責子應用的注冊、路由分發(fā)『奖常可以簡單理解為 React.js 和 Vue.js 里的 App 組件喉悴,主要做一些初始化、路由注冊玖媚、全局狀態(tài)注冊箕肃、銷毀時的動作
  • 子應用

    • 概念:各個 SPA 應用,可以理解為 SPA 里的頁面組件
    • 負責暴露一些函數(shù)最盅,以此對接主應用突雪,讓主應用知道:哦,原來你是子應用涡贱,要和我對接。常見的對應函數(shù)有:bootstrap, mount, unmount

項目創(chuàng)建

乾坤官網(wǎng)最推薦的做法是將主應用和子應用分成兩個項目惹想,各自管理问词。當然,也可以一個項目里分成不同的目錄來存放嘀粱。

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

如果你覺得 官方的例子 太復雜激挪,也可以看我自己建的 qiankun-bigass-app,子應用只有兩個用 React.js 的項目锋叨。我把很多無關的代碼都刪了垄分。

實現(xiàn)主應用

理清上面的關系后,我們直接干代碼娃磺,先看主應用薄湿。

首先,我們弄一個 .html 文件出來偷卧,作為主頁面的 HTML 模板:

<body>
  <div class="mainapp">
    <!-- 標題欄 -->
    <header class="mainapp-header">
      <h1>QianKun</h1>
    </header>
    <div class="mainapp-main">
      <!-- 側(cè)邊欄 -->
      <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');
});

上面主要完成:初始化仔蝌、注冊子應用泛领、設置配置全局狀態(tài)、設置默認進入子應用掌逛、啟動應用师逸。

至于初始渲染函數(shù),可以這么寫:

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

實現(xiàn)子應用

子應用其實和官網(wǎng)的差不多,這里以 React.js 子應用舉例篓像。首先用 create-react-app 來創(chuàng)建子應用:

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__ 用于判斷現(xiàn)在是否作為子應用被訪問动知,其它地方與普通 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({});
}

// 監(jiān)聽全局狀態(tài)
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 出來就改不了,這里官網(wǎng)推薦使用 @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 主應用用來注冊多個子應用的函數(shù) 類似于 Vue 和 React 的路由
start 啟動主應用 類似于 React.js 的 render 函數(shù)和 Vue.js 的 new Vue()
loadMicroApp 手動加載子應用 也類似于 React.js 的 render 函數(shù)和 Vue.js 的 new Vue()辰晕,只不過更自由了
prefetchApps 預加載子應用 類似于 Webpack 的 prefetch 功能
addGlobalUncaughtErrorHandler 頁面報錯時可以用于上報和兜底 -
removeGlobalUncaughtErrorHandler 都懂的 -
initGlobalState 初始化全局狀態(tài) 類似于 Redux 的 createStore 和 Vue 的 new Vue.Store()

FAQ

請看 常見問題蛤迎,最佳實踐和遇到的問題都能在這里解決。

(完)

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末含友,一起剝皮案震驚了整個濱河市替裆,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌唱较,老刑警劉巖扎唾,帶你破解...
    沈念sama閱讀 212,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異南缓,居然都是意外死亡胸遇,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評論 3 385
  • 文/潘曉璐 我一進店門汉形,熙熙樓的掌柜王于貴愁眉苦臉地迎上來纸镊,“玉大人,你說我怎么就攤上這事概疆《和” “怎么了?”我有些...
    開封第一講書人閱讀 158,369評論 0 348
  • 文/不壞的土叔 我叫張陵岔冀,是天一觀的道長凯旭。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么罐呼? 我笑而不...
    開封第一講書人閱讀 56,799評論 1 285
  • 正文 為了忘掉前任鞠柄,我火速辦了婚禮,結(jié)果婚禮上嫉柴,老公的妹妹穿的比我還像新娘厌杜。我一直安慰自己,他們只是感情好计螺,可當我...
    茶點故事閱讀 65,910評論 6 386
  • 文/花漫 我一把揭開白布夯尽。 她就那樣靜靜地躺著,像睡著了一般登馒。 火紅的嫁衣襯著肌膚如雪匙握。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,096評論 1 291
  • 那天谊娇,我揣著相機與錄音肺孤,去河邊找鬼。 笑死济欢,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的小渊。 我是一名探鬼主播法褥,決...
    沈念sama閱讀 39,159評論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼酬屉!你這毒婦竟也來了半等?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,917評論 0 268
  • 序言:老撾萬榮一對情侶失蹤呐萨,失蹤者是張志新(化名)和其女友劉穎杀饵,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谬擦,經(jīng)...
    沈念sama閱讀 44,360評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡切距,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,673評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了惨远。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片谜悟。...
    茶點故事閱讀 38,814評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖北秽,靈堂內(nèi)的尸體忽然破棺而出葡幸,到底是詐尸還是另有隱情,我是刑警寧澤贺氓,帶...
    沈念sama閱讀 34,509評論 4 334
  • 正文 年R本政府宣布蔚叨,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏蔑水。R本人自食惡果不足惜邢锯,卻給世界環(huán)境...
    茶點故事閱讀 40,156評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望肤粱。 院中可真熱鬧弹囚,春花似錦、人聲如沸领曼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽庶骄。三九已至毁渗,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間单刁,已是汗流浹背灸异。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留羔飞,地道東北人肺樟。 一個月前我還...
    沈念sama閱讀 46,641評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像逻淌,于是被迫代替她去往敵國和親么伯。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,728評論 2 351

推薦閱讀更多精彩內(nèi)容

  • 背景 近幾年前端技術發(fā)展迅猛卡儒,框架組件層出不窮田柔,導致企業(yè)中慢慢沉淀了很多不同技術棧的前端應用。同時隨著前端業(yè)務復雜...
    cd2001cjm閱讀 924評論 1 2
  • 微前端是一種類似于后端微服務的架構(gòu)骨望,它將微服務的理念應用于瀏覽器端硬爆,即將單頁面前端應用由單一的單體應用轉(zhuǎn)變?yōu)槎鄠€小...
    空無一碼閱讀 514評論 0 0
  • 一、什么是微前端 Techniques, strategies and recipes for building ...
    梅西愛騎車閱讀 3,172評論 0 15
  • 什么是微前端 微前端是一種多個團隊通過獨立發(fā)布功能的方式來共同構(gòu)建現(xiàn)代化 web 應用的技術手段及方法策略. 微前...
    lean_閱讀 2,543評論 0 7
  • 我是黑夜里大雨紛飛的人啊 1 “又到一年六月擎鸠,有人笑有人哭缀磕,有人歡樂有人憂愁,有人驚喜有人失落糠亩,有的覺得收獲滿滿有...
    陌忘宇閱讀 8,531評論 28 53