微前端主要是借鑑微服務的概念。隨着單個項目愈來愈大,業務愈來愈複雜,維護和開發會變的愈來愈困難。微前端的目的是將將一個大型前端工程拆分紅若干個小型的前端工程,各個前端工程以前開發維護相互獨立,共同組成一個 Monolith
。javascript
The term Micro Frontends first came up in ThoughtWorks Technology Radar at the end of 2016. It extends the concepts of micro services to the frontend world. The current trend is to build a feature-rich and powerful browser application, aka single page app, which sits on top of a micro service architecture. Over time the frontend layer, often developed by a separate team, grows and gets more difficult to maintain. That’s what we call a Frontend Monolith.css
The idea behind Micro Frontends is to think about a website or web app as a composition of features which are owned by independent teams. Each team has a distinct area of business or mission it cares about and specialises in. A team is cross functional and develops its features end-to-end, from database to user interface.html
微前端將傳統的前端工程按項目(業務)縱向展開:前端
Monolithic Frontends 傳統項目 java
Organisation in Verticals 縱向展開 react
隨着時間的推移,前端層(一般由一個單獨的團隊開發)會增加,變得更難維護。微前端背後的想法是將網站或web應用程序看做是由獨立團隊擁有的功能組成的。每一個團隊都有其所關心和專門從事的不一樣業務或任務領域。團隊是跨職能的,從數據庫到用戶界面,端到端地開發其功能。webpack
微前端的實現,意味着對前端應用的拆分。拆分應用的目的,並不僅是爲了架構上好看,還爲了提高開發效率。 爲此,微前端帶來這麼一系列的好處:git
除此,它也有一系列的缺點:github
目前市面上並無很是完美的實踐方案,考慮到咱們的前端技術棧,目前的技術方案爲基於react
的 ice-stark,拆分的項目爲 蜘蛛先生-用戶端
的 個人學習
模塊。web
簡單看一下主要的代碼片斷。
主框架內主要做用就是做爲全部子應用的承載和消息樞紐。
入口並無明顯改動。
import React from 'react';
import ReactDOM from 'react-dom';
import Router from './router';
import '@alifd/next/reset.scss';
import "./global.scss";
const ICE_CONTAINER = document.getElementById('ice-container');
if (!ICE_CONTAINER) {
throw new Error('當前頁面不存在 <div id="ice-container"></div> 節點.');
}
ReactDOM.render(
<Router />, ICE_CONTAINER ); 複製代碼
引入子應用的路由須要用到 ice-stark
的路由模塊 AppRouter
和 Approute
;
import React from 'react';
import { AppRouter, AppRoute } from '@ice/stark';
import NotFoundPage from '@/pages/Exception/NotFound';
import ServerErrorPage from '@/pages/Exception/ServerError';
export default function BasicPage({ setPathname }) {
return (
<AppRouter NotFoundComponent={NotFoundPage} ErrorComponent={ServerErrorPage} onRouteChange={pathname => { setPathname(pathname); }} > <AppRoute path={['/myLearn']} basename="/myLearn" title="個人學習| 知蛛先生" url={[ '//zhizhutest.xx.mocks.taobao.com:4445/js/index.js', '//zhizhutest.xx.mocks.taobao.com:4445/css/index.css', ]} /> </AppRouter> ); } 複製代碼
能夠看到,子應用的渲染根節點須要調用 getMountNode
函數得到;
import ReactDOM from 'react-dom';
import { getMountNode } from '@ice/stark';
import { Provider } from 'react-redux';
import store from '@/store';
import Router from '@/router';
import '@alifd/next/reset.scss';
import "./global.scss";
ReactDOM.render(
<Provider store={store}> <Router /> </Provider>
, getMountNode());
複製代碼
路由模塊和普通項目的路由同樣,使用 react-router-dom 的路由模塊;
import { BrowserRouter as Router, Switch, Route, Redirect } from 'react-router-dom';
import { getBasename } from '@ice/stark';
import React from 'react';
import Layout from '@/layouts';
export default class router extends React.Component {
render() {
return (
<Router basename={getBasename()}> <Switch> <Route path="/" component={Layout} /> </Switch> </Router> ); } }; 複製代碼
項目地址:ice-stark,如下看一些主要的點:
export const setCache = (key: string, value: any): void => {
if (!(window as any).ICESTARK) {
(window as any).ICESTARK = {};
}
(window as any).ICESTARK[key] = value;
};
export const getCache = (key: string): any => {
const icestark: any = (window as any).ICESTARK;
return icestark && icestark[key] ? icestark[key] : null;
};
複製代碼
暴漏兩個方法,在 window 上的掛載了一個全局的 namespace:ICESTARK
,看一下保存了哪些數據:
子應用的掛載處,render 方法返回容器 myRefBase
:
setCache('root', null);
this.renderChild();
複製代碼
renderChild
主要步驟(拋開異常處理和狀態處理等):
removeElementFromBase
方法,清空 myRefBase 下的子應用掛載節點;appendElementToBase
方法,建立子應用的容器節點(div),添加到 myRefBase 節點下,返回該節點,名爲 rootElement;setCache
方法,將 rootElement 添加至 ICESTARK.root;emptyAssets
方法,將 head 下的帶有特定屬性的 script 連接和 link 連接卸載;loadAssets
方法,將 url 下的連接裝載入 head 標籤下; renderChild
方法在 AppRoute 的 componentDidMount
和 componentDidUpdate
生命週期內被調用,子應用的切換會先卸載上一個子應用所添加的 script 和 link;子應用的渲染根節點和主框架並不同,子應用須要用 getMountNode
方法獲取節點:
import ReactDOM from 'react-dom';
import { getMountNode } from '@ice/stark';
import { Provider } from 'react-redux';
import store from '@/store';
import Router from '@/router';
import '@alifd/next/reset.scss';
import "./global.scss";
ReactDOM.render(
<Provider store={store}> <Router /> </Provider>
, getMountNode());
複製代碼
getMountNode 方法返回了 window.ICESTARK.root
,這個值在 AppRoute 組件中的 renderChild 方法中被初始化賦值 setCache('root', rootElement)
,rootElement
的值爲 AppRoute 組件的方法 appendElementToBase
的返回值 document.createElement('div')
,這個 element 的 id 被賦值爲 AppRoute 的 props.rootId,默認值爲 icestarkNode
,這個 element 被 append 進 AppRoute 的容器中 render = () => <div ref={el => this.myRefBase = el} />
;
主要暴露的方法爲:recordAssets
,loadAsset
,loadAssets
,emptyAssets
:
該方法在 AppRouter 實例化的時被調用:
style.setAttribute(PREFIX, STATIC);
複製代碼
PREFIX 和 STATIC 在 constant.ts 文件中給定默認值:
export const PREFIX = 'icestark';
export const DYNAMIC = 'dynamic';
export const STATIC = 'static';
export const ICESTSRK_NOT_FOUND = `/${PREFIX}_404`;
複製代碼
根據參數 isCss 來判斷 type 爲 script 或 link,以 if...else 的邏輯來判斷標籤類型。 id 爲 icestark-(js/css)-[index] 的形式,手動設置屬性 icestark 的值爲 dynamic,基本屬性以下:
const element: HTMLScriptElement | HTMLLinkElement | HTMLElement = document.createElement(type);
// ...
if (isCss) {
(element as HTMLLinkElement).rel = 'stylesheet';
(element as HTMLLinkElement).href = url;
} else {
(element as HTMLScriptElement).type = 'text/javascript';
(element as HTMLScriptElement).src = url;
(element as HTMLScriptElement).async = false;
}
複製代碼
最後直接添加到 root 下,root 爲 loadAsset 方法的參數:
root.appendChild(element);
複製代碼
該方法在 AppRoute 的 renderChild 中被調用,參數以下:
loadAssets(
bundleList: string[],
useShadow: boolean,
jsCallback: (err: any) => boolean,
cssCallBack: () => void,
)
複製代碼
bundleList 在 renderChild 中:
const bundleList: string[] = Array.isArray(url) ? url : [url];
複製代碼
loadAssets 首先獲取當前的 jsRoot 和 cssRoot,在不啓用 useShadow 的請框架均爲 head 標籤。bundleList 會被 forEach,根據 urlItem 的後綴 push 到 jsList 和 cssList,loadAssets 有兩個內置方法:loadJs 和 loadCss,這兩個方法會遍歷 jsList 和 cssList,調用 loadAsset 方法;
該方法會將 :
${PREFIX}=${DYNAMIC}
;recordAssets
方法標記的) style、link、script 標籤所有移除;private originalPush: OriginalStateFunction = window.history.pushState;
private originalReplace: OriginalStateFunction = window.history.replaceState;
複製代碼
componentDidMount 中調用 hijackHistory 方法,劫持 history 的 pushState 和 replaceState,調用 this.handleStateChange:
hijackHistory = (): void => {
window.history.pushState = (state: any, title: string, url?: string, ...rest) => {
this.originalPush.apply(window.history, [state, title, url, ...rest]);
this.handleStateChange(state, url, 'pushState');
};
window.history.replaceState = (state: any, title: string, url?: string, ...rest) => {
this.originalReplace.apply(window.history, [state, title, url, ...rest]);
this.handleStateChange(state, url, 'replaceState');
};
window.addEventListener('popstate', this.handlePopState, false);
};
複製代碼
this.handleStateChange 調用了 this.handleRouteChange,this.handleRouteChange 調用了 this.props.onRouteChange。 render
方法中,手動實現了一個路由匹配:
let match: any = null;
let element: any;
React.Children.forEach(children, child => {
if (match == null && React.isValidElement(child)) {
element = child;
const { path } = child.props as any;
match = path ? matchPath(pathname, { ...child.props }) : null;
}
});
複製代碼
matchPath 的 匹配邏輯,未匹配會返回null。render 的返回值:
if (match) {
const { path, basename } = element.props as any;
setCache('basename', basename || (Array.isArray(path) ? path[0] : path));
realComponent = React.cloneElement(element, extraProps);
} else {
realComponent = (
<AppRoute
path={ICESTSRK_NOT_FOUND}
url={ICESTSRK_NOT_FOUND}
NotFoundComponent={NotFoundComponent}
useShadow={useShadow}
/>
);
}
return realComponent;
複製代碼
緣由:查看異常,由於
var href = "css/" + ({}[chunkId]||chunkId) + ".css";
var fullhref = __webpack_require__.p + href; // __webpack_require__.p 就是 publicpath
複製代碼
解決:修改子應用的publicPath:
目前能提出的解決方案是:
其實這個方法就不少:
預編譯文件對 bundle 的體積其實沒有什麼影響,主要是同步問題,並且 sass-resources-loader
並不支持引入線上資源:
以上爲此次微前端實踐的一點總結,裏面還有不少不成熟和待完善的地方,但願能拋磚引玉,給你們帶來更多的思路和探索。
第一次寫文章,若是有不對或者很差的地方請你們指正!