原文地址:Micro?Frontends extending the microservice idea to frontend development
由多個擁有獨立發(fā)布能力的團隊構(gòu)建現(xiàn)代Web應(yīng)用程序的技術(shù)谤辜,策略和方法蓄坏。
什么是微前端价捧?
"微前端"一詞最早于2016年底出現(xiàn)在ThoughtWorks Technology Radar上。它將微服務(wù)的概念擴展到了前端世界涡戳。當前的趨勢是構(gòu)建一個特性豐富且功能強大的瀏覽器應(yīng)用程序(又名單頁應(yīng)用程序)结蟋,該應(yīng)用程序未應(yīng)用微服務(wù)架構(gòu)。隨著時間的流逝妹蔽,通常由獨立團隊開發(fā)的前端層會不斷增長椎眯,并且變得越來越難以維護。這就是我們所謂的巨石應(yīng)用胳岂。
微前端背后的想法是將網(wǎng)站或Web應(yīng)用程序視為由每個獨立團隊擁有功能的合集编整。每個團隊都有自己關(guān)心和專長的不同業(yè)務(wù)或任務(wù)領(lǐng)域。單個團隊是跨職能的乳丰,從數(shù)據(jù)庫到用戶界面掌测,端到端地開發(fā)其功能。
但是产园,這個想法并不新奇汞斧。它與自包含系統(tǒng)概念有很多共同點。過去什燕,類似的方法被稱為垂直系統(tǒng)的前端集成粘勒。但是微前端顯然是一個更友好,更小巧的術(shù)語屎即。
巨石應(yīng)用
垂直組織
什么是現(xiàn)代web應(yīng)用庙睡?
在介紹中,我使用了"構(gòu)建現(xiàn)代web應(yīng)用程序"一詞技俐。讓我們定義與此術(shù)語相關(guān)的假設(shè)乘陪。
將這個東西放到更大的視野中,Aral Balkan寫過一篇博客文章雕擂,介紹了他的Documents-to-Applications Continuum啡邑。他提出了滑動比例尺的概念,其中在左側(cè)是一個由靜態(tài)文檔構(gòu)建且通過鏈接連接的站點井赌,而在右側(cè)是一個純行為驅(qū)動的谤逼,無內(nèi)容的應(yīng)用程序,例如在線照片編輯器仇穗。
如果您將項目放置在此范圍的左側(cè)流部,則非常適合在Web服務(wù)器級別進行集成。使用此模型仪缸,服務(wù)器從構(gòu)成用戶請求的頁面的所有組件中收集并連接HTML字符串贵涵。通過從服務(wù)器重新加載頁面或通過ajax替換頁面的一部分來完成更新。Gustaf Nilsson Kotte撰寫了有關(guān)此主題的綜合文章。
當您的用戶界面必須提供實時反饋(即使是在不可靠的連接上)時宾茂,僅由服務(wù)器渲染的網(wǎng)站已不再滿足要求瓷马。要實現(xiàn)Optimistic UI或Skeleton Screens,您還需要能夠在設(shè)備本身上更新用戶界面跨晴。Google的術(shù)語Progressive Web Apps恰如其分地描述了成為網(wǎng)絡(luò)良好公民(漸進式增強功能)的平衡行為欧聘,同時還提供了類似App的功能表現(xiàn)。這種應(yīng)用程序位于site-app-continuum中間的某個位置端盆。在這里怀骤,僅基于服務(wù)器的解決方案已經(jīng)不能滿足要求。我們必須將集成轉(zhuǎn)移到瀏覽器中焕妙,這是本文的重點蒋伦。
微前端背后的核心思想
技術(shù)獨立
每個團隊都應(yīng)該能夠選擇和升級其技術(shù)棧,而不必與其他團隊進行協(xié)調(diào)焚鹊。自定義元素是一種隱藏實現(xiàn)細節(jié)痕届,同時為其他人提供中立接口的好方法。團隊代碼隔離
即使所有團隊都使用相同的框架末患,也不要共享運行時間研叫。構(gòu)建獨立的獨立應(yīng)用程序。不要依賴共享狀態(tài)或全局變量璧针。建立團隊前綴
同意尚無法隔離的命名約定嚷炉。命名空間CSS,事件探橱,本地存儲和Cookies申屹,以避免沖突并闡明所有權(quán)。通過自定義API支持本機瀏覽器功能
使用瀏覽器事件進行通信而不是構(gòu)建全局的PubSub系統(tǒng)走搁。如果確實需要構(gòu)建跨團隊API独柑,請盡量簡單迈窟。建立彈性站點
即使JavaScript失敗或尚未執(zhí)行私植,您的功能也應(yīng)該很有用。使用通用渲染和漸進增強功能可改善感知性能车酣。
DOM是API
自定義元素曲稼,是Web組件規(guī)范中的互操作性,是集成到瀏覽器中的一個很好的原始方法湖员。每個團隊都使用自己選擇的網(wǎng)絡(luò)技術(shù)來構(gòu)建其組件贫悄,并將其包裝在一個自定義元素中(例如<order-minicart> </order-minicart\>)。該特定元素(標簽娘摔,屬性和事件)的DOM規(guī)范充當其他團隊的合約或公共API窄坦。優(yōu)點是他們可以使用組件及其功能,而無需了解實現(xiàn)。他們只需要能夠與DOM交互即可鸭津。
但是彤侍,僅自定義元素并不能滿足我們所有需求。為了解決漸進增強逆趋,通用渲染或路由問題盏阶,我們需要其他軟件。
該頁面分為兩個主要區(qū)域闻书。首先名斟,我們將討論頁面組成-如何根據(jù)不同團隊擁有的組件來組裝頁面。之后,我們將展示實現(xiàn)客戶端頁面轉(zhuǎn)換的示例打月。
頁面組成
除了使用不同框架本身編寫的客戶端和服務(wù)器端代碼集成之外谈撒,還應(yīng)該討論很多附帶主題:隔離js的機制,避免css沖突楞卡,按需加載資源,在團隊之間共享公共資源脾歇,處理數(shù)據(jù)獲取 并考慮為用戶提供良好的加載狀態(tài)蒋腮。 我們將一步一步地涉及這些主題。
基本原型
拖拉機商店的不同型號產(chǎn)品頁面將作為以下示例的基礎(chǔ)藕各。
它具有一個變體選擇器池摧,可以在三種不同的拖拉機型號之間切換。 更改產(chǎn)品圖片時激况,名稱作彤,價格和建議會更新。 還有一個購買按鈕乌逐,將選擇的變體添加到購物籃竭讳,并在頂部增加一個迷你購物籃,并相應(yīng)地進行更新浙踢。
try in browser & inspect the code
所有HTML都是使用純JavaScript和沒有依賴性的ES6模板字符串在客戶端生成的绢慢。該代碼使用簡單的狀態(tài)/標記分離,并在每次更改時重新呈現(xiàn)整個HTML客戶端-無需花哨的DOM區(qū)別洛波,也沒有通用渲染胰舆。也沒有團隊區(qū)分-代碼寫在一個js/css文件中。
客戶端集成
在此示例中蹬挤,頁面分為三個團隊擁有的單獨的組件/片段缚窿。Team Checkout(藍色)現(xiàn)在負責(zé)與購買過程有關(guān)的所有事情,即“購買”按鈕和迷你購物籃焰扳。Team Inspire(綠色)在此頁面上管理產(chǎn)品推薦倦零。該頁面本身歸團隊產(chǎn)品(紅色)所有误续。
try in browser & inspect the code
團隊產(chǎn)品決定要包括的功能以及在布局中的位置。該頁面包含團隊產(chǎn)品本身可以提供的信息扫茅,例如產(chǎn)品名稱女嘲,圖像和可用的變體。但是它也包括其他團隊的片段(自定義元素)诞帐。
如何創(chuàng)建自定義元素欣尼?
讓我們以“購買”按鈕為例。Team Product包含按鈕停蕉,只需將<blue-buy sku =“t_porsche”> </blue-buy>添加到標記中的所需位置即可愕鼓。為此,Team Checkout必須在頁面上注冊藍色購買元素慧起。
class BlueBuy extends HTMLElement {
connectedCallback() {
this.innerHTML = `<button type="button">buy for 66,00 €</button>`;
}
disconnectedCallback() { ... }
}
window.customElements.define('blue-buy'菇晃,BlueBuy);
現(xiàn)在,每次瀏覽器遇到一個新的blue-buy標簽時蚓挤,都會調(diào)用connectedCallback磺送。這是對自定義元素的根DOM節(jié)點的引用〔右猓可以使用標準DOM元素(例如innerHTML或getAttribute())的所有屬性和方法估灿。
命名元素時,規(guī)范定義的唯一要求是名稱必須包含短劃線(-)缤剧,以保持與即將到來的新HTML標簽的兼容性馅袁。在接下來的示例中,使用了命名約定[team_color]-[feature]荒辕。團隊命名空間可防止名稱沖突汗销,因此只需查看DOM,就可以清楚地了解功能的所有權(quán)抵窒。
父子通信 / DOM修改
當用戶在變量選擇器中選擇另一臺拖拉機時弛针,必須相應(yīng)地更新購買按鈕。要實現(xiàn)此團隊產(chǎn)品李皇,只需從DOM中刪除現(xiàn)有元素削茁,然后插入一個新元素即可。
container.innerHTML;
// => <blue-buy sku="t_porsche">...</blue-buy>
container.innerHTML = '<blue-buy sku="t_fendt"></blue-buy>';
同步調(diào)用舊元素的connectedCallback疙赠,以使該元素有機會清理諸如事件偵聽器之類的東西付材。之后朦拖,將調(diào)用新創(chuàng)建的t_fendt元素的connectedCallback圃阳。
另一個性能更高的選項是僅更新現(xiàn)有元素上的sku屬性。
document.querySelector('blue-buy').setAttribute('sku', 't_fendt');
如果Team Product使用具有DOM差異功能的模板引擎(如React)璧帝,則將由算法自動完成捍岳。
為了支持這一點,Custom元素可以實現(xiàn)attributeChangedCallback并指定觀察到的屬性列表,應(yīng)為其觸發(fā)此回調(diào)
const prices = {
t_porsche: '66,00 €',
t_fendt: '54,00 €',
t_eicher: '58,00 €',
};
class BlueBuy extends HTMLElement {
static get observedAttributes() {
return ['sku'];
}
connectedCallback() {
this.render();
}
render() {
const sku = this.getAttribute('sku');
const price = prices[sku];
this.innerHTML = `<button type="button">buy for ${price}</button>`;
}
attributeChangedCallback(attr, oldValue, newValue) {
this.render();
}
disconnectedCallback() {...}
}
window.customElements.define('blue-buy', BlueBuy);
為了避免重復(fù)锣夹,引入了render()方法页徐,該方法在connectedCallback和attributeChangedCallback中被調(diào)用。這種方法收集所需的數(shù)據(jù)银萍,而innerHTML就是新的標記变勇。當決定在Custom Element中使用更復(fù)雜的模板引擎或框架時,這里就是初始化代碼的地方贴唇。
瀏覽器支持
上面的示例使用了當前支持Chrome搀绣,Safari和Opera自定義元素V1規(guī)范。但是戳气,使用document-register-element可以使用輕量級且經(jīng)歷過戰(zhàn)斗考驗的polyfill來使其在所有瀏覽器中都能正常工作链患。在幕后,它使用廣泛支持的Mutation Observer API瓶您,因此在后臺不會出現(xiàn)駭人的DOM樹麻捻。
框架兼容性
由于自定義元素是一種網(wǎng)絡(luò)標準,因此所有主流的JavaScript框架(如Angular呀袱,React贸毕,Preact,Vue或Hyperapp)都支持它們夜赵。 但是當您進入細節(jié)時崖咨,某些框架中仍然存在一些實現(xiàn)問題。在無處不在的自定義元素Rob Dodson匯集了一個兼容性測試套件油吭,著重強調(diào)了尚未解決的問題击蹲。
父子或兄弟組件通信 / DOM事件
但是,屬性傳遞不足以進行所有交互婉宰。在我們的示例中歌豺,當用戶單擊“購買”按鈕時,迷你購物籃應(yīng)刷新心包。
這兩個片段均由Team Checkout擁有(藍色)类咧,因此它們可以構(gòu)建某種內(nèi)部JavaScript API,該API可使迷你購物籃知道何時按下按鈕蟹腾。但這將要求組件實例相互通信痕惋,并且沖突隔離。
較干凈的方法是使用PubSub機制娃殖,在該機制中值戳,一個組件可以發(fā)布消息,而其他組件可以訂閱特定主題炉爆。幸運的是堕虹,瀏覽器內(nèi)置了此功能卧晓。這正是單擊,選擇或鼠標懸停之類的瀏覽器事件的工作方式赴捞。除了本機事件逼裆,還可以使用新的CustomEvent(...)創(chuàng)建更高級別的事件。事件始終與創(chuàng)建/調(diào)度事件的DOM節(jié)點相關(guān)赦政。大多數(shù)本機事件還具有冒泡功能胜宇。這樣就可以偵聽DOM特定子樹上的所有事件。如果要偵聽頁面上的所有事件恢着,請將事件偵聽器附加到window元素掸屡。在示例中,blue:basket:changed-event的創(chuàng)建如下所示:
class BlueBuy extends HTMLElement {
[...]
connectedCallback() {
[...]
this.render();
this.firstChild.addEventListener('click', this.addToCart);
}
addToCart() {
// maybe talk to an api
this.dispatchEvent(new CustomEvent('blue:basket:changed', {
bubbles: true,
}));
}
render() {
this.innerHTML = `<button type="button">buy</button>`;
}
disconnectedCallback() {
this.firstChild.removeEventListener('click', this.addToCart);
}
}
現(xiàn)在然评,迷你購物籃可以在窗口上訂閱此事件仅财,并在刷新數(shù)據(jù)時得到通知。
class BlueBasket extends HTMLElement {
connectedCallback() {
[...]
window.addEventListener('blue:basket:changed', this.refresh);
}
refresh() {
// fetch new data and render it
}
disconnectedCallback() {
window.removeEventListener('blue:basket:changed', this.refresh);
}
}
通過這種方法碗淌,迷你籃片段向其范圍(窗口)之外的DOM元素添加了一個偵聽器盏求。對于大量的應(yīng)用程序這都沒問題,但是如果您對此不滿意亿眠,則還可以實現(xiàn)一種方法碎罚,其中頁面本身(團隊產(chǎn)品)偵聽事件并通過在DOM元素上調(diào)用refresh()通知迷你購物籃。
// page.js
const $ = document.getElementsByTagName;
$('blue-buy')[0].addEventListener('blue:basket:changed', function() {
$('blue-basket')[0].refresh();
});
強制性地調(diào)用DOM方法并不常見纳像,但是可以在video element api中找到荆烈。 如果可以,應(yīng)首選使用聲明式方法(更改屬性)竟趾。
服務(wù)器端渲染/通用渲染
自定義元素非常適合在瀏覽器內(nèi)部集成組件憔购。但是,當構(gòu)建一個可在Web上訪問的站點時岔帽,初始負載性能很可能會變得很重要玫鸟,并且在下載并執(zhí)行所有js框架之前,用戶將看到白屏犀勒。另外屎飘,最好考慮一下如果JavaScript失敗或被阻塞,網(wǎng)站會發(fā)生什么情況贾费。Jeremy Keith在其電子書/播客Resilient Web Design中解釋了其重要性钦购。因此,在服務(wù)器上呈現(xiàn)核心內(nèi)容的能力是關(guān)鍵褂萧。不幸的是押桃,Web組件規(guī)范根本沒有涉及服務(wù)器渲染。沒有JavaScript箱玷,沒有自定義元素:(
自定義元素+服務(wù)器端包含=??
為了使服務(wù)器渲染正常工作怨规,重構(gòu)了前面的示例陌宿。每個團隊都有自己的express服務(wù)器锡足,還可以通過url訪問Custom Element的render()方法波丰。
$ curl http://127.0.0.1:3000/blue-buy?sku=t_porsche
<button type="button">buy for 66,00 €</button>
“自定義元素”標記名稱用作路徑名-屬性成為查詢參數(shù)。現(xiàn)在舶得,有一種方法可以通過服務(wù)器渲染每個組件的內(nèi)容掰烟。結(jié)合<blue-buy >-Custom Elements,可以實現(xiàn)與通用Web組件非常接近的功能:
<blue-buy sku="t_porsche">
<!--#include virtual="/blue-buy?sku=t_porsche" -->
</blue-buy>
include注釋是服務(wù)器端包含的一部分沐批,該功能在大多數(shù)Web服務(wù)器中都可用纫骑。是的,將當前日期嵌入到我們網(wǎng)站上九孩,這是過去經(jīng)常使用的的一種技術(shù)先馆。還有一些替代技術(shù),例如ESI躺彬,nodesi煤墙,compoxure和tailor,但是對于我們的項目宪拥,SSI已被證明是一種簡單且難以置信的穩(wěn)定解決方案仿野。
在Web服務(wù)器將完整頁面發(fā)送到瀏覽器之前,#include注釋將替換為/ blue-buy?sku=t_porsche的響應(yīng)她君。nginx中的配置如下所示:
upstream team_blue {
server team_blue:3001;
}
upstream team_green {
server team_green:3002;
}
upstream team_red {
server team_red:3003;
}
server {
listen 3000;
ssi on;
location /blue {
proxy_pass http://team_blue;
}
location /green {
proxy_pass http://team_green;
}
location /red {
proxy_pass http://team_red;
}
location / {
proxy_pass http://team_red;
}
}
指令ssi:on; 啟用SSI功能脚作,并為每個團隊添加上游和位置塊,以確保將所有以/blue開頭的url路由到正確的應(yīng)用程序(team_blue:3001)缔刹。另外球涛,/路由映射到紅色隊,該紅色隊控制著主頁/產(chǎn)品頁面校镐。
此動畫在禁用了JavaScript的瀏覽器中顯示了拖拉機商店宾符。
現(xiàn)在,變體選擇按鈕是實際的鏈接灭翔,每次單擊都會導(dǎo)致頁面重新加載魏烫。右側(cè)的終端說明了如何將頁面請求路由到紅色團隊,該團隊控制產(chǎn)品頁面肝箱,然后用藍色和綠色團隊的片段補充標記哄褒。
重新打開JavaScript時,僅第一個請求的服務(wù)器日志消息可見煌张。像第一個示例一樣呐赡,所有后續(xù)的拖拉機更改都由客戶端處理。在后面的示例中骏融,產(chǎn)品數(shù)據(jù)將從JavaScript中提取链嘀,并根據(jù)需要通過REST API加載萌狂。
您可以在本地計算機上使用此示例代碼。 僅需要安裝Docker Compose怀泊。
git clone https://github.com/neuland/micro-frontends.git
cd micro-frontends/2-composition-universal
docker-compose up --build
然后茫藏,Docker在端口3000上啟動Nginx,并為每個團隊構(gòu)建node.js映像霹琼。在瀏覽器中打開http://127.0.0.1:3000/時务傲,應(yīng)該會看到一個紅色的拖拉機。docker-compose的組合日志可輕松查看網(wǎng)絡(luò)中正在發(fā)生的事情枣申。遺憾的是售葡,無法控制輸出顏色,因此您必須忍受以下事實:藍色團隊可能會以綠色突出顯示:)
src文件映射到各個容器中忠藤,并且在更改代碼后挟伙,節(jié)點應(yīng)用程序?qū)⒅匦聠印8膎ginx.conf要求重新啟動docker-compose才能生效模孩。因此尖阔,隨時隨意擺弄并提供反饋。
數(shù)據(jù)獲取和加載狀態(tài)
SSI/ESI方法的缺點是瓜贾,最慢的片段確定整個頁面的響應(yīng)時間诺祸。因此,最好是可以緩存片段的響應(yīng)祭芦。對于制作成本高且難以緩存的片段筷笨,通常最好將其從初始渲染中排除。 可以將它們異步加載到瀏覽器中龟劲。在我們的示例中胃夏,綠色個性化片段顯示了個性化推薦,這是一個備選方案昌跌。
一種可能的解決方案是仰禀,紅色團隊僅跳過SSI Include。
之前
<green-recos sku="t_porsche">
<!--#include virtual="/green-recos?sku=t_porsche" -->
</green-recos>
之后
<green-recos sku="t_porsche"></green-recos>
重要說明:自定義元素無法自動關(guān)閉蚕愤,因此編寫<green-recos sku="t_porsche" />無法正常工作答恶。
渲染僅在瀏覽器中進行。但是萍诱,從動畫中可以看出悬嗓,此更改現(xiàn)在導(dǎo)致頁面的大量重排。推薦區(qū)域最初是空白的裕坊。團隊果嶺JavaScript已加載并執(zhí)行包竹。進行了用于獲取個性化推薦的API調(diào)用。呈現(xiàn)推薦標記,并請求關(guān)聯(lián)的圖像≈芟梗現(xiàn)在苗缩,該片段需要更多空間并推動頁面布局。
有不同的選擇來避免像這樣令人討厭的重排声诸。建議控制頁面的紅色小組可以固定容器的高度酱讶。在響應(yīng)式網(wǎng)站上,確定高度通常很棘手双絮,因為不同的屏幕尺寸可能會有所不同浴麻。但是更重要的問題是得问,這種團隊間協(xié)議在紅色和綠色團隊之間建立了緊密的聯(lián)系囤攀。如果綠色團隊希望在reco元素中添加其他子標題,則必須與紅色團隊在新高度上進行協(xié)調(diào)宫纬。兩個團隊都必須同時推出他們的更改焚挠,以避免布局混亂。
更好的方法是使用一種稱為骨架屏漓骚。紅色小組將綠色recos SSI包括在標記中蝌衔。此外,團隊綠色更改了其片段的服務(wù)器端渲染方法蝌蹂,以便生成內(nèi)容的示意圖噩斟。骨架標記可以重用實際內(nèi)容的部分布局樣式。這樣孤个,它可以保留所需的空間剃允,并且實際內(nèi)容的填充不會導(dǎo)致跳轉(zhuǎn)。
骨架屏幕對于客戶端渲染也非常有用齐鲤。當您的自定義元素由于用戶操作而插入DOM時斥废,它可以立即渲染框架,直到從服務(wù)器需要的數(shù)據(jù)到達為止给郊。
即使在諸如變量選擇之類的屬性更改上牡肉,您也可以決定切換到框架視圖,直到新數(shù)據(jù)到達淆九。這樣统锤,用戶可以得到片段中正在發(fā)生某些事情的指示。但是炭庙,當端點快速響應(yīng)時饲窿,新舊數(shù)據(jù)之間的短暫骨架閃爍也可能很煩人。保留舊數(shù)據(jù)或使用智能超時可以有所幫助煤搜。因此免绿,請明智地使用此技術(shù),并嘗試獲取用戶反饋擦盾。
在頁面之間導(dǎo)航
即將繼續(xù)……(我保證)
關(guān)注Github Repo以獲得通知
其他資源
- Book: Micro Frontends in Action由我編寫嘲驾。
- Talk: Micro Frontends - MicroCPH, Copenhagen 2019(幻燈片)Nitty Gritty Details或Frontend淌哟,Backend,??Happyend
- Talk: Micro Frontends - Web Rebels, Oslo 2018([幻燈片](https://noti.st/naltatis/HxcUfZ/micro-frontends -認為更小辽故,避免整體式的愛后端))更小的思考徒仓,避免整體式的,??后端
- Slides: Micro Frontends - JSUnconf.eu 2017
- Talk: Break Up With Your Frontend Monolith - JS Kongress 2017Elisabeth Engel在gutefrage.net上討論了實現(xiàn)Micro Frontends的問題誊垢。
- Article:Micro FrontendsCam Jackson在Martin Fowlers博客上的文章
- Post: Micro frontends - a microservice approach to front-end web developmentTomS?derlund解釋了核心概念并提供了與此主題相關(guān)的鏈接
- Post: Microservices to Micro-FrontendsSandeep Jain總結(jié)了微服務(wù)和微前端背后的主要原理
- Link Collection: Micro Frontends by Elisabeth Engel大量有關(guān)此主題的帖子掉弛,講座,工具和其他資源
- Awesome Micro FrontendsChristian Ulbrich精心策劃的鏈接列表cur
- Custom Elements Everywhere確蔽棺撸框架和自定義元素可以是BFF
- 可在manufactum.com上購買拖拉機:)
這家商店是由兩個團隊使用此處介紹的技術(shù)開發(fā)的殃饿。
即將發(fā)生的事情……(很快)
- 用例
- 在頁面之間導(dǎo)航
- 軟導(dǎo)航與硬導(dǎo)航
- 通用路由器
- …
- 在頁面之間導(dǎo)航
- 主題
- 隔離的CSS /一致的用戶界面/樣式指南和樣式庫
- 初始負載下的性能
- 使用網(wǎng)站時的表現(xiàn)
- 加載CSS
- 加載JS
- 集成測試
- …
貢獻者
- Koike Takayuki將網(wǎng)站翻譯為日語。
- JorgeBeltrán芋肠,將網(wǎng)站翻譯為西班牙文乎芳。
- Bruno Carneiro將網(wǎng)站翻譯為葡萄牙語。
該站點由Github Pages生成帖池。 它的來源可以在neuland/micro-frontends上找到奈惑。