智聯招聘的微前端落地實踐——Widget

ThoughtWorks在幾年前提出了微前端的概念,其核心理念是將前端單體應用在開發階段拆分紅多個獨立的工程,並在運行階段組合成完整的應用。不只解耦了視圖和代碼,使得應用能夠容納多種技術棧,還進一步解耦了流程和團隊,極大地提升了團隊的自主性和協做效率。javascript

智聯招聘的大前端架構Ada自己就能夠看做一個基於路由的微前端架構,圍繞URL的研發方式可以靈活地實現頁面級別的解耦。而在在視圖區域級別,Ada也引入了專門的微前端實現機制——Widget。html

什麼是 Widget

Widget是一種能夠獨立開發和發佈的視圖區域,它運行於宿主頁面中,而且可以和宿主頁面進行雙向通訊。前端

在設計Widget架構時,咱們考慮到Ada的多框架支持能力,應當讓Widget在使用時不受框架的限制,也就是說,使用Knockout.js開發的Widget能夠運行在Vue.js的頁面中,反之亦然,這就決定了Widget的最終形態必須是框架無關的Plain JavaScript。出於一樣的考量,通訊機制也不該該受框架所限,而應該制定屬於Widget的通訊API規範。vue

總結一下,Widget的設計目標是:java

  • 框架獨立,不受宿主頁面技術棧限制;
  • 流程獨立,可以獨立開發和發佈,集成後無需宿主頁面再次配合發佈;
  • 執行獨立,運行邏輯不直接操控宿主頁面,而是經過API來交換信息;
  • 展示獨立,內容和樣式均限定在Widget視圖區域以內,不直接影響宿主頁面;

總體架構

爲了統一開發體驗,Ada從開發、調試、發佈和運行都爲Widget進行了專門的支持。web

image.png

在開發階段,理論上任何可以編譯成Plain JavaScript的框架均可以使用,Ada在Vue.js等腳手架中內置了對Widget的支持,開發者可使用熟悉的技術開發Widget,也能夠像調試頁面同樣預覽和調試Widget。segmentfault

咱們在腳手架內核中爲Widget單獨設計了Webpack配置,使得基於各類框架開發的Widget都能輸出成一個獨立的JavaScript Bundle(樣式也會構建到同一個Bundle中),藉此來保證輸出的文件符合Widget規範。安全

就像Ada體系裏的其餘工件同樣,每一個Widget都有一個惟一URN,宿主頁面經過該URN來引用Widget,從而和Widget的JavaScript Bundle解耦。在發佈階段,Ada會爲URN關聯最新的輸出清單,這樣一來,Widget不但能夠脫離宿主頁面獨立發佈,還能進一步實現灰度發佈和A/B實驗。架構

運行時

Widget的生命週期包括四個階段:註冊、初始化、運行和銷燬,各階段之間的轉換是由Widget SDK來負責調度的。app

宿主頁面使用<script>標籤加載Widget URN時,Ada Server會負責調度並返回正確的JavaScript Bundle,加載完畢後,就會向Widget SDK註冊本身。

註冊完畢以後,宿主頁面就能夠調用Widget SDK的init方法,並傳遞Widget名稱和父元素DOM,來決定Widget的初始化時機和位置。宿主頁面還能夠初始化同一個Widget的多個實例,並和它們分別進行通訊。

Widget的消息通訊機制借用了Web Worker的API規範,宿主頁面能夠經過Widget SDK的postMessage方法向指定Widget發送消息,同時經過onmessage方法監聽Widget發來的消息。反過來,Widget也能夠用一樣的方式和宿主頁面通訊。

當宿主頁面須要銷燬組件時,能夠調用Widget SDK的destory方法,後者會指示Widge銷燬視圖、清理存儲,而後再將Widget移出事件中心。

image.png

開發 Widget

Widget本質上是一個規範化的Class,可使用Plain JavaScript,也能夠融入任何框架,好比藉助Vue.js來開發Widget的代碼多是這樣的:

import Vue from 'vue'
import Widget from './Widget.vue' // 具體業務代碼

class Widget {
  constructor ({ el }) {
    this.el = el // 當前 Widget 的父 DOM
    this.mount()
  }

  mount () {
    const app = new Vue({
      render: h => h(Widget)
    })

    app.$mount(this.el)
  }
}

export default Widget

使用 Widget

宿主頁面經過<script>標籤導入Widget URN,而後初始化便可:

const scriptElement = document.createElement('script')

scriptElement.type = 'text/javascript'
scriptElement.async = true
scriptElement.src = YOUR_WIDGET_URN
scriptElement.onload = () => {
  window.zpWidget.init(this.widgetName, {
    el: YOUR_WIDGET_PARENT_DOM
  })
}

document.body.appendChild(scriptElement)

爲了貼合現代Web框架組件化的研發習慣,咱們爲Vue.js、Knockout.js和Weex提供了Widget組件,藉此來簡化Widget加載和初始化步驟。例如在Vue.js中,能夠這樣加載一個Widget:

<template>
  <!--
    參數解釋:urn 能夠是上線後的 widget 地址也能夠當前項目的相對路徑,
  -->
  <widget url="/widgets/YOUR_WIDGET_NAME" />
</template>

<script> import Widget from '@zpfe/widget-components/web'

export default {
  name: 'YOUR_COMPONENT_NAME',
  components: {
    Widget
  }
} </script>

初始化Widget以後,就能夠與宿主頁面進行雙向通訊了,例如:

// Widget 內部
window.zpWidget.postMessage({
  widgetName: 'timeNow',
  eventName: 'click'
}, new Date())

// 宿主頁面
window.zpWidget.onmessage({
  widgetName: 'timeNow',
  eventName: 'click'
}, (time) => {
  console.log(`當前時間是:${time}`)
})

實際應用

目前集團內已累計發佈了100餘個Widget,各條產品線都招到了Widget能發揮做用的場景,好比:

  • Passport藉由Widget統一了集團內的全部登陸邏輯,可以更好地調整安全策略,業務方經過接入 Widget 便可實現登陸功能;
  • 限時推廣的Banner或隱私通知,經常同時在多個產品中同時展現,且變化頻率較高,Widget可以有效地將其和業務代碼解耦;
  • 內部系統在升級時,能夠藉助Widget在視圖層面一小塊一小塊地逐步遷移,從而在用戶無感的狀況下漸進式地完成總體升級工做;

微前端不止步於Widget

發佈於2019年初的Widget機制,是咱們在微前端領域的第一次嘗試,成效使人滿意。集團內對Widget的普遍應用帶來了更多的訴求和靈感激發,將來,咱們還會結合業務特色去探索微前端的其餘可能性,讓架構賦能業務,爲用戶帶來價值。

相關文章
相關標籤/搜索