適配 CRA 項目爲微前端應用

前期準備

  • 一個使用 CRA 建立的新項目或者舊項目
  • 瞭解 publicPath 是什麼
  • 瞭解 single-spa 中關於微前端應用的概念(及 qiankun 中 html-entry  的概念)

解決如何覆蓋 CRA 配置的問題

一般狀況下,覆蓋 CRA 配置的解決方案有兩種:css

  • 直接 npm run eject 
  • 使用 react-app-rewired 或者 rescripts 等第三方工具

這裏使用第二種方式,緣由也比較簡單,由於魔改 webpack 是須要很大的勇氣的,且 npm run eject 不可逆(雖然能夠經過其餘方式恢復,但太麻煩了),而且對於須要覆蓋的配置,咱們也是有針對性的,因此使用第三方工具會更好一些。html

我這裏使用 rescripts 這個庫,它與 create-react-rewired 大同小異。前端

覆蓋 webpack 的打包模式

首先,對於 single-spa 中要加載的微前端應用,咱們須要提供諸如 bootstrap 、 mount 以及 unmount 等若干生命週期鉤子,但 CRAwebpack 的默認打包方式不會將這些方法暴露出來,因此聲明配置以下:node

config.output.library = `${name}-[name]`;
config.output.libraryTarget = 'umd';
config.output.jsonpFunction = `webpackJsonp_${name}`;
config.output.globalObject = 'window';

讓咱們來挨個分析下每行配置的做用及意義:react

  • library 和 libraryTarget 是共同生效的,默認狀況下,libraryTarget 的值是 var ,即在 entry file 在執行後,會返回一個變量,咱們這裏使用 umd 的緣由是由於,在主應用中,咱們仍然會使用 module system,不管是 webpack 仍是 Systemjs,所以 umd 是一個最佳選擇,由於它適配全部的 module system
  • jsonpFunction 是用來按需加載 chunk 的工具函數,因爲微前端應用中,一個頁面中,會同時存在多個 webpack 運行時環境,因此可能會存在命名衝突,致使加載 chunk 時出現意想不到的後果,手動設置一個惟一的命名能夠解決這個衝突
  • globalObject 自己屬性的默認值便是 window ,但因爲 libraryTarget 咱們設置成了 umd ,對於 nodejs 環境,全局對象時 global 而非 window ,這裏顯示地聲明它是 window 證實微前端應用只是針對 browser 而言的

移除 CRA 中內置的 HMR 功能

HMR 功能通常是針對開發環境而言的,對於爲何微前端應用在開發環境要關閉 HMR,我尚未深刻研究,但關閉它是官方代碼庫示例中提供的最佳實踐。webpack

在 CRA 中,HMR 功能是分兩部分存在的,一個是 webpack.config.devServer 提供的,另外一個是 CRA 本身實現的 webpackHotDevClient,咱們需依次移除或者關閉它們。git

首先關閉 webpack.devServer 的 HMR 功能,很簡單,添加以下配置:github

config.hot = false;
config.watchContentBase = false;
config.liveReload = false;

再來移除 webpackHotDevClient,這個會稍微麻煩一些,由於它是直接聲明在 webpack.config.entry 中的,因此使用下面的代碼移除它:web

config.entry = config.entry.filter(
   (e) => !e.includes('webpackHotDevClient')
);

同時還有 HotModuleReplacementPlugin 插件,它提供 css 的 HMR 功能,利用相同的代碼移除它:npm

config.plugins = config.plugins.filter(
   (p) => !(p instanceof webpack.HotModuleReplacementPlugin)
);

這樣就徹底從 CRA 中移除了 HMR 的功能。

對 devServer 添加 CORS 配置以支持跨域訪問

html-entry 爲前提實現的微前端框架,構建前提既是微前端應用要支持跨域訪問,對於部署階段,咱們能夠在 web server 或代理層完成該步驟,對於開發階段,咱們則須要對 devServer 進行一些調整,由於它默認是不支持跨域訪問的。

解決跨域問題除了配置反向代理以外,還可使用 CORS 來解決,在 devServer 中,顯示使用後者更加快捷,添加以下代碼便可:

config.headers = {
   'Access-Control-Allow-Origin': '*',
};

這樣既實現了最簡單的 CORS 配置,但知足開發環境中對於跨域訪問的支持,足夠了。

對於使用 history 做爲路由模式的應用作適配

很簡單,聲明以下配置便可:

config.historyApiFallback = true;

推薦使用 history 做爲微前端子應用的路由模式,由於在全局路由解析中,針對 hash 的匹配並不像 url 那樣靈活,同時也存在一些微妙的 bug。

使 devServer 監聽微前端子應用相對應的端口

也十分簡單,使用以下代碼:

config.port = 7101;

這裏的 7101,是微前端子應用監聽的接口,建議不論在開發階段,仍是在部署階段,都使用相同的接口以減小分辨接口的心智負擔。

爲微前端應用指定單獨的 publicPath

須要單獨指定 publicPath 的緣由是由於,當前咱們的微前端架構依賴於 html entry,每一個路徑所對應的 entry 所加載的微前端應用,必然會有一些從 publicPath 加載資源的代碼。

但在項目中,除非將全部子應用項目中的靜態資源目錄集合到一塊兒,託管在主應用中,或者使用 CDN,否則在項目啓動時,會遇到不少 404 的錯誤,其根本緣由是由於,以前請求的靜態資源,並非託管在主應用的服務器上,而是子應用的,所以如何在主應用或者代理層中映射這些靜態資源的加載請求,是必需要解決的事情。

默認狀況下,CRA 的 publicPath/ ,即相對於當前服務器的域名,子應用在主應用中加載時,所相對的是主應用服務器的域名,因此這裏須要對每一個子應用聲明不一樣的 publicPath

在 CRA 中聲明 publicPath 有兩種,

  • 經過在 package.json 中添加 homepage 字段
  • 經過 PUBLIC_PATH  環境變量

當前我使用的方式是第一種,由於第二種在當前的 CRA 版本中不生效(感受像是一個 bug),以下:

{
  "name": "vcapp-login",
  "homepage": "/login",
  ...
}

除了針對靜態資源設置單獨的 publicPath 以外,還須要在應用中,針對動態使用 publicPath 的地方作出修改,這個在 qiankun 中已經有響應的解決方案,以下:

if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

簡單原理就是動態的注入了 __INJECTED_PUBLIC_PATH_BY_QIANKUN__ 這個全局變量,它是經過 html entry 導入 entry 時,動態解析出來的。

適配 CRA 支持多 entry 啓動模式

衆所周知,CRA 啓動的 entry 文件是 src/index.jsx ,若是一個項目須要適配爲微前端應用,勢必須要在 index.jsx 中實現各類微前端模塊的生命週期函數。

在微前端應用的架構中,很重要的一點既是解耦,若是咱們在開發子應用時,且沒有用到任何和主應用或者其餘子應用相關的模塊或者狀態時,仍然須要其餘它們顯示不是一種理想的開發模式。

咱們理想的模式應該是,咱們仍然能夠按照傳統 SPA 的啓動方式,開發子應用,當須要與主應用集成,或者與其餘子應用調試時,又能夠以微前端模塊的方式啓動它。這實際上是在說,咱們當前的子應用要支持多 entry 啓動模式。

在 CRA 中,雖然能夠經過覆蓋 webpack 的方式來解決這個問題,可是我認爲有更簡便的方法。考慮到不管是傳統啓動方式,仍是微前端模塊的啓動方式,這兩種啓動方式在同一時間,咱們只會使用一種,那咱們移花接木式的變動 index.jsx 的內容,在 CRA 加載 entry 以前欺騙它豈不是更好?這裏咱們能夠利用如下兩點來實現相似的效果:

  • 使用 npm scripts 中的 hook 前綴來截止啓動指令
  • 在 hook 中執行一些腳本,動態修改 index.jsx  的內容

因爲涉及到的代碼較多,這裏就簡單貼一個 npm scripts 的截圖好了,以下:

image.png

能夠發現,對於 start 、 build ,均支持兩種模式的指令,從而適配不一樣開發模式下的構建需求。

最後說一點,對於 index.jsx 內容的更改,最簡單的方式即時經過軟連接的方式來實現,提早提供兩份被連接的目標文件,好比:

image.png

micro.tsx 和 standalone.tsx 均對應不一樣的 entry 入口,使用 CRA 啓動應用前,動態地建立軟連接將它們和 index.tsx 文件連接起來便可(因爲項目中使用了 ts,後綴爲 .tsx ,js 項目同理)。

引入主應用

以後咱們就能夠愉快地在主應用中引入咱們的子應用了,主要配置有兩個,一是註冊子應用,以下:

registerMicroApps(
  [
   // 其餘子應用
   ...,
    {
      name: "vcapp-login",
      entry: "//localhost:7101/login",
      container: "#subapp-container",
      activeRule: "/trade-login/",
    },
  ],
)

二是增長對於子應用的 publicPath 的配置,這兒會分爲兩部分,一個是部署環境下的,一個是開發環境下的,這裏分享開發環境下的。我主應用項目使用的打包器是 parcel ,所以能夠直接對它內部的 web server 增長中間件來完成這部分工做,以下:

app.use(
  createProxyMiddleware("/login", {
    target: "http://localhost:7101",
  })
);

注意這裏的 7101,與上文中的 7101 對應,若是它們不一致,會形成子應用加載失敗。

其餘的坑

  • CRA 對於 svg 格式的圖片,沒有寫在 url-loader  的匹配規則中,若是子應用使用了 svg 圖片,須要覆蓋 url-loader 配置已適配 publicPath 變動形成的影響

最後

因爲倉庫代碼在公司內網,不太方面直接拷貝出來,往後有時間會單另在 github 建立一個示例項目。

同時因爲該微前端應用的架構基於 single-spaqiankun,對於 CRA 項目向微前端項目的遷移所作的一些工做並不具備通用性。

對於微前端這種架構,我更多地將它做爲一種可以漸進式地重構項目的手段在使用,對於大型複雜項目,並無太多的經驗,一是由於沒機會作相似的複雜度極高的中臺項目,二是由於不少巨石應用,大可能是舊項目,因此將它用做重構項目的一種手段也許更能發揮它的用處。

若有錯誤,還望指出。

相關文章
相關標籤/搜索