最近打算改進一下現有網站的架構,微前端這個詞屢次進入了個人視野。javascript
可是網上關於微前端文章老是說得似是而非,因而我找到這篇文章進行翻譯。並大概理解微前端的理念。目前尚未肯定是否使用微前端架構,由於看起來業界對最佳實踐並無達成一致。html
譯文開始,有刪節。原文連接前端
把前端作好很難,讓多個團隊同時開發大型前端應用,就更難了。目前有一種趨勢是將前端應用拆分紅更小、更易於管理的小應用。這種體系結構是如何提升前端團隊的效率的呢?java
本文將對這些問題進行闡述。除了討論利弊,咱們還將介紹一些可用的例子,並深刻研究一個完整的示例應用。node
近年來,微服務已迅速普及,許多組織都使用這種體系結構樣式來避免大型單體應用的侷限性。儘管有不少介紹微服務的文章,但仍是有許多公司侷限於單體式前端應用。react
假設你想構建一個漸進式的Web應用程序,可是你很難將新的功能實現於現有的總體應用中。好比你想開始使用新的 JS 語法(或TypeScript),可是你沒法在現有的構建過程當中使用對應的構建工具。又或者,你只想擴展你的開發團隊,以便多個團隊能夠同時處理一個產品,可是現有應用中的耦合和複雜度讓每一個開發者互相掣肘。這些都是真實存在的問題,這些問題極大地下降了大型團隊的開發效率。webpack
最近,咱們看到愈來愈多前端開始把注意力集中在複雜前端應用的架構上面。尤爲是如何將前端總體分解,每一塊能夠獨立開發、測試和部署,同時對用戶而言還是一個總體。這種技術就是微前端,咱們將其定義爲:nginx
一種將獨立的前端應用組成一個更大的總體的架構風格git
固然,在軟件體系結構方面沒有免費的午飯。一些微型前端實現可能致使依賴關係很是重複,從而增長用戶的下載量。並且,團隊自治可能會致使團隊分散。儘管如此,咱們認爲風險是可控的,收益是高於成本的。github
對於許多團隊而言,這是開始微前端之旅的首要緣由。技術債阻礙了項目的發展,只能重寫。爲了不徹底重寫的風險,咱們更但願 逐個替換舊的模塊。
每一個單獨的微型前端應用的源代碼都將比單個總體前端應用的源代碼少得多。這些較小的代碼庫對於開發人員來講更容易維護。尤爲是咱們避免了組件間耦合所致使的複雜性。
就像微服務同樣,微前端的獨立部署能力是關鍵。部署範圍的減少,帶來了風險的下降。每一個微前端應用都應具備本身的持續交付途徑,不停地構建、測試、部署。
每一個團隊須要圍繞業務功能垂直組建,而不是根據技術能力來組建。這爲團隊帶來了更高的凝聚力。
簡而言之,微前端就是將大而恐怖的東西切成更小、更易於管理的部分,而後明確地代表它們之間的依賴性。咱們的技術選擇,咱們的代碼庫,咱們的團隊以及咱們的發佈流程都應該可以彼此獨立地操做和發展,無需過多的協調。
想象一個訂餐網站。從表面上看,這是一個很是簡單的概念,可是若是你想作得好,會有不少使人驚訝的細節:
每一個頁面都足夠複雜,從微前端的角度咱們能夠把每一個頁面交給一個專門的團隊(譯註:這些團隊的人員能夠重疊),而且每一個團隊都應該可以獨立於其餘團隊工做。他們應該可以開發、測試、部署和維護其代碼,而沒必要擔憂與其餘團隊衝突。
鑑於上面的定義至關寬鬆,全部有許多方法實現微前端。在本節中,咱們將顯示一些示例並討論它們的取捨之道。每一個頁面都有一個容器應用,該容器能夠:
咱們用一個很是傳統的方式開始,將多個模板渲染到服務器上的HTML裏。咱們有一個index.html,其中包含全部常見的頁面元素,而後使用 include 來引入其餘模板:
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Feed me</title>
</head>
<body>
<h1>🍽 Feed me</h1>
<!--# include file="$PAGE.html" -->
</body>
</html>
複製代碼
而後配置 nginx
server {
listen 8080;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
ssi on;
# 將 / 重定向到 /browse
rewrite ^/$ http://localhost:8080/browse redirect;
# 根據路徑訪問 html
location /browse {
set $PAGE 'browse';
}
location /order {
set $PAGE 'order';
}
location /profile {
set $PAGE 'profile'
}
# 全部其餘路徑都渲染 /index.html
error_page 404 /index.html;
}
複製代碼
這是至關標準的服務器端應用。咱們之因此能夠稱其爲微前端,是由於咱們讓每一個頁面獨立,可由一個獨立的團隊交付。
爲了得到更大的獨立性,能夠有一個單獨的服務器負責渲染和服務每一個微型前端,其中一個服務器位於前端,向其餘服務器發出請求。經過緩存,能夠把延遲降到最低。
這個例子說明了微前端不必定是一種新技術,也沒必要太複雜。只要咱們保證代碼隔離和團隊自治,不管咱們採用何種技術棧,咱們均可以達到相同的效果。
有人會用到的一種方法是將每一個微前端發佈爲一個 node 包,並讓容器應用程序將全部微前端應用做爲依賴項。好比這個 package.json:
{
"name": "@feed-me/container",
"version": "1.0.0",
"description": "A food delivery web app",
"dependencies": {
"@feed-me/browse-restaurants": "^1.2.3",
"@feed-me/order-food": "^4.5.6",
"@feed-me/user-profile": "^7.8.9"
}
}
複製代碼
乍看彷佛沒什麼問題,這種作法會產生一個可部署的包,咱們能夠輕鬆管理依賴項。
可是,這種方法意味着咱們必須從新編譯併發布每一個微前端應用,才能發佈咱們對某個應用做出的更改。咱們強烈不建議使用這種微前端方案。
iframe 是集成的最簡單方式之一。本質上來講,iframe 裏的頁面是徹底獨立的,能夠輕鬆構建。並且 iframe 還提供了不少的隔離機制。
<html>
<head>
<title>Feed me!</title>
</head>
<body>
<h1>Welcome to Feed me!</h1>
<iframe id="micro-frontend-container"></iframe>
<script type="text/javascript"> const microFrontendsByRoute = { '/': 'https://browse.example.com/index.html', '/order-food': 'https://order.example.com/index.html', '/user-profile': 'https://profile.example.com/index.html', }; const iframe = document.getElementById('micro-frontend-container'); iframe.src = microFrontendsByRoute[window.location.pathname]; </script>
</body>
</html>
複製代碼
iframe 並非一項新技術,因此上面代碼也許看起來並不那麼使人興奮。
可是,若是咱們從新審視先前列出的微前端的主要優點,只要咱們謹慎地劃分微應用和組建團隊的方式,iframe便很適合。
咱們常常看到不少人不肯意選擇iframe。由於 iframe有點使人討厭,但 iframe 實際上仍是有它的優勢的。上面提到的容易隔離確實會使iframe不夠靈活。它會使路由、歷史記錄和深層連接變得更加複雜,而且很難作成響應式頁面。
這種方式多是最靈活的一種,也是被採用頻率最高的一種方法。每一個微前端都對應一個 <script>
標籤,而且在加載時導出一個全局變量。而後,容器應用程序肯定應該安裝哪些微應用,並調用相關函數以告知微應用什麼時候以及在何處進行渲染。
<html>
<head>
<title>Feed me!</title>
</head>
<body>
<h1>Welcome to Feed me!</h1>
<!-- 這些腳本不會立刻渲染應用 -->
<!-- 而是分別暴露全局變量 -->
<script src="https://browse.example.com/bundle.js"></script>
<script src="https://order.example.com/bundle.js"></script>
<script src="https://profile.example.com/bundle.js"></script>
<div id="micro-frontend-root"></div>
<script type="text/javascript"> // 這些全局函數是上面腳本暴露的 const microFrontendsByRoute = { '/': window.renderBrowseRestaurants, '/order-food': window.renderOrderFood, '/user-profile': window.renderUserProfile, }; const renderFunction = microFrontendsByRoute[window.location.pathname]; // 渲染第一個微應用 renderFunction('micro-frontend-root'); </script>
</body>
</html>
複製代碼
上面是一個很基本的例子,演示了 JS 集成的大致思路。
與 package 集成不一樣,咱們能夠用不一樣的bundle.js獨立部署每一個應用。
與 iframe 集成不一樣的是,咱們具備徹底的靈活性,你能夠用 JS 控制何時下載每一個應用,以及渲染應用時額外傳參數。
這種方法的靈活性和獨立性使其成爲最經常使用的方案。當咱們展現完整的示例時,會有更詳細的探討。
這是前一種方法的變體,每一個微應用對應一個 HTML 自定義元素,供容器實例化,而不是提供全局函數。
<html>
<head>
<title>Feed me!</title>
</head>
<body>
<h1>Welcome to Feed me!</h1>
<!-- 這些腳本不會立刻渲染應用 -->
<!-- 而是分別提供自定義標籤 -->
<script src="https://browse.example.com/bundle.js"></script>
<script src="https://order.example.com/bundle.js"></script>
<script src="https://profile.example.com/bundle.js"></script>
<div id="micro-frontend-root"></div>
<script type="text/javascript"> // 這些標籤名是上面代碼定義的 const webComponentsByRoute = { '/': 'micro-frontend-browse-restaurants', '/order-food': 'micro-frontend-order-food', '/user-profile': 'micro-frontend-user-profile', }; const webComponentType = webComponentsByRoute[window.location.pathname]; // 渲染第一個微應用(自定義標籤) const root = document.getElementById('micro-frontend-root'); const webComponent = document.createElement(webComponentType); root.appendChild(webComponent); </script>
</body>
</html>
複製代碼
主要區別在於使用 Web Component 代替全局變量。若是你喜歡 Web Component 規範,那麼這是一個不錯的選擇。若是你但願在容器應用程序和微應用之間定義本身的接口,那麼你可能更喜歡前面的示例。
CSS 沒有模塊系統、命名空間和封裝。就算有,也一般缺少瀏覽器支持。在微前端環境中,這些問題會更嚴重。
例如,若是一個團隊的微前端的樣式表爲 h2 { color: black; },而另外一個團隊的則爲 h2 { color: blue; },而這兩個選擇器都附加在同一頁面上,就會衝突!
這不是一個新問題,但因爲這些選擇器是由不一樣的團隊在不一樣的時間編寫的,而且代碼可能分散在不一樣的庫中,所以更難避免。
多年來,有許多方法可讓 CSS 變得更易於管理。有些人選擇使用嚴格的命名約定,例如 BEM,以確保選擇器的範圍是足夠小的。其餘一些人則使用預處理器,例如 SASS,其選擇器嵌套能夠用做命名空間。一種較新的方法是經過 CSS 模塊 或各類 CSS-in-JS 庫,以編程的方式寫 CSS。某些開發者還會使用 shadow DOM 來隔離樣式。
只要你選擇一種能確保開發人員的樣式互不影響的方案便可。
上面咱們提到,視覺一致性很重要,一種解決方法是應用間共享可重用的 UI 組件庫。
提及來容易,作起來難。建立這樣一個庫的主要好處是減小工做量。此外,你的組件庫能夠充當樣式指南,做爲開發人員和設計師之間進行協做的重要橋樑。
第一個容易出錯的點,就是過早地建立了太多組件。好比你試圖建立一個囊括全部常見 UI 組件的組件庫。可是,經驗告訴咱們,在實際使用組件以前,咱們很難猜想組件的 API 應該是什麼樣的,強行作組件會致使早期的混亂。所以,咱們寧願讓團隊根據需求建立本身的組件,即便這最初會致使某些重複。
讓 API 天然出現,一旦組件的 API 變得顯而易見,就能夠將重複的代碼整合到共享庫中。
與任何共享內部庫同樣,庫的全部權和治理權很難分配。一種人認爲,全部開發成員都擁有庫的全部權,實際上這意味着沒有人擁有庫的全部權。若是沒有明確的約定或技術遠見,共享組件庫很快就會成爲不一致代碼的大雜燴。若是取另外一個極端,即徹底集中式的開發共享庫,後果就是建立組件的人與使用這些組件的人之間將存在很大的脫節。
咱們見過的最好的合做方式是,任何人均可覺得庫貢獻代碼,可是有一個 託管者(一我的或一個團隊)負責確保這些代碼的質量、一致性和有效性。
維護共享庫的人須要技術很強,同時溝通能力差也很強。
關於微前端的最多見問題之一是如何讓應用彼此通訊。咱們建議應該儘量少地通訊,由於這一般會引入沒必要要的耦合。
不過跨應用通訊的需求仍是存在的。
若是你使用的是 Redux,那麼一般你會爲整個應用建立一個全局狀態。但若是每一個微應用是獨立的,那麼每一個微應用就都應該有本身的 Redux 和全局狀態。
不管選擇哪一種方法,咱們都但願咱們的微應用經過消息或事件進行通訊,並避免任何共享狀態,以免耦合。
你還應該考慮如何自動驗證集成沒有中斷。功能測試是解法之一,可是因爲實現和維護成本,咱們傾向於只作一部分功能測試。或者,你能夠實施消費者驅動接口,讓每一個微應用指定它對其餘微應用的要求,這樣你就不用實際將它們所有集成在一塊兒並在瀏覽器中測試。
若是咱們有獨立的團隊獨立處理前端應用,那麼後端開發又是怎樣的呢?
咱們堅信全棧團隊的價值,從界面代碼一直到後臺 API 開發,再到數據庫和網站架構。
咱們推薦的模式是 Backends For Frontends 模式,其中每一個前端應用程序都有一個相應的後端,後端的目的僅僅是爲了知足該前端的需求。BFF模式起初的粒度多是每一個前端平臺(PC頁面、手機頁面等)對應一個後端應用,但最終會變爲每一個微應用對應一個後端應用。
這裏要說明一下,一個後端應用可能有獨立業務邏輯和數據庫的,也可能只是下游服務的聚合器。 若是微前端應用只有一個與之通訊的API,而且該API至關穩定,那麼爲它單獨構建一個後臺可能根本沒有太大價值。指導原則是:構建微前端應用的團隊沒必要等待其餘團隊爲其構建什麼事物。
所以,若是一個微前端用到的新功能須要後端接口的變動,那麼這一前一後兩個地方就應該交給一個團隊來開發。
另外一個常見的問題是,如何作身份驗證和鑑權?
顯然用戶只須要進行一次身份驗證,所以身份驗證應該放在容器應用裏。容器可能具備某種登陸形式,經過該登陸形式咱們能夠得到某種令牌。該令牌將歸容器全部,並能夠在初始化時注入到每一個微前端中。最後,微前端能夠將令牌發送到服務器,而後服務器進行驗證。
在測試方面,咱們看不到單體式前端和微前端之間的太大差別。
顯而易見的差距是容器應用程序對各類微前端的集成測試。
接下來咱們來實現一個詳細的例子。
主要介紹容器應用和微應用如何用 JavaScript 集成在一塊兒,由於這多是最有趣和最複雜的部分。
你能夠在 demo.microfrontends.com 上查看最終部署的結果,完整的源代碼能夠在 Github 上看到。
該項目使用 React.js 實現,值得一提的是 React 不是惟一選擇。
咱們將從 容器 開始,由於它是咱們的切入點。package.json:
{
"name": "@micro-frontends-demo/container",
"description": "Entry point and container for a micro frontends demo",
"scripts": {
"start": "PORT=3000 react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test"
},
"dependencies": {
"react": "^16.4.0",
"react-dom": "^16.4.0",
"react-router-dom": "^4.2.2",
"react-scripts": "^2.1.8"
},
"devDependencies": {
"enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1",
"jest-enzyme": "^6.0.2",
"react-app-rewire-micro-frontends": "^0.0.1",
"react-app-rewired": "^2.1.1"
},
"config-overrides-path": "node_modules/react-app-rewire-micro-frontends"
}
複製代碼
能夠看出,這是一個用 create-react-app 建立的 React 應用。
要注意我並無把其餘微應用包含到 package.json 的依賴裏。
若是你想知道如何選擇和展現微應用,能夠看一下 App.js。咱們使用React Router 將當前URL與預約義的路由列表進行匹配,並渲染相應的組件:
<Switch>
<Route exact path="/" component={Browse} />
<Route exact path="/restaurant/:id" component={Restaurant} />
<Route exact path="/random" render={Random} />
</Switch>
複製代碼
Browser 和 Restaurant 組件是這樣的:
const Browse = ({ history }) => (
<MicroFrontend history={history} name="Browse" host={browseHost} />
);
const Restaurant = ({ history }) => (
<MicroFrontend history={history} name="Restaurant" host={restaurantHost} />
);
複製代碼
兩個組件都渲染了一個 MicroFrontend 組件。除了 history 對象(稍後將變得很重要)以外,咱們還指定應用程序的惟一名稱,以及對應的後端 host。host 的值多是 http://localhost:3001 或 browse.demo.microfrontends.com。
MicroFrontend 只是另外一個 React 組件:
class MicroFrontend extends React.Component {
render() {
return <main id={`${this.props.name}-container`} />; } } 複製代碼
渲染時,咱們要作的只是在頁面上放置一個容器元素,其ID對於微前端應用來講是惟一的。咱們使用React componentDidMount做爲下載和安裝微應用的觸發器:
// class MicroFrontend
componentDidMount() {
const { name, host } = this.props;
const scriptId = `micro-frontend-script-${name}`;
if (document.getElementById(scriptId)) {
this.renderMicroFrontend();
return;
}
fetch(`${host}/asset-manifest.json`)
.then(res => res.json())
.then(manifest => {
const script = document.createElement('script');
script.id = scriptId;
script.src = `${host}${manifest['main.js']}`;
script.onload = this.renderMicroFrontend;
document.head.appendChild(script);
});
}
複製代碼
必須從 manifest 文件中獲取腳本的 URL,由於 react-scripts 輸出的編譯的 JavaScript 文件的文件名中帶有哈希值以方便緩存。
設置腳本的URL後,剩下的就是將其添加到文檔並初始化:
// class MicroFrontend
renderMicroFrontend = () => {
const { name, history } = this.props;
window[`render${name}`](`${name}-container`, history);
// E.g.: window.renderBrowse('browse-container', history);
};
複製代碼
最後要作的是清理工做。當 MicroFrontend 從頁面中刪除組件時,咱們也應該卸載相關的微應用。
componentWillUnmount() {
const { name } = this.props;
window[`unmount${name}`](`${name}-container`);
}
複製代碼
接下來介紹 window.renderBrowse 方法是怎麼實現的:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
window.renderBrowse = (containerId, history) => {
ReactDOM.render(<App history={history} />, document.getElementById(containerId)); registerServiceWorker(); }; window.unmountBrowse = containerId => { ReactDOM.unmountComponentAtNode(document.getElementById(containerId)); }; 複製代碼
上面代碼用到了 ReactDOM.render 和 ReactDOM.unmountComponentAtNode。
爲了獨立開發和運行微前端。每一個微前端應用還具備額外的 index.html,以在容器外部獨立呈現:
<html lang="en">
<head>
<title>Restaurant order</title>
</head>
<body>
<main id="container"></main>
<script type="text/javascript">
window.onload = () => {
window.renderRestaurant('container');
};
</script>
</body>
</html>
複製代碼
從如今開始,微前端大多隻是普通的 React 應用程序。'browser' 應用餐廳列表,提供 <input>
用來搜索和過濾,並用 <Link>
把結果包裹起來,用戶點擊時導航到一個特定的餐廳。而後,咱們將切換到第二個 'order' 微應用,展現一個帶有菜單的餐廳頁面。
咱們以前提到過 ,應將跨應用通訊保持在最低限度。在此示例中,咱們惟一的通訊是 browser 頁面須要告訴 order 頁面要加載哪一個餐廳。咱們使用路由來解決此問題。
涉及到三個 React 應用,都用React Router進行路由,可是以兩種略有不一樣的方式進行初始化。
對於容器應用程序,咱們建立一個,它會在內部實例化一個history對象,咱們使用該對象來處理客戶端歷史記錄,也可使用它來將多個React Router 連接在一塊兒。初始化路由的方式爲
<Router history={this.props.history}>
複製代碼
這個 histroy 是由容器應用提供的,全部微應用共用這個 history 對象。這使得用 url 做爲消息傳遞方式變得十分簡便。例如,咱們有一個像這樣的連接:
<Link to={`/restaurant/${restaurant.id}`}>
複製代碼
單擊此連接後,該路徑將在容器中更新,該容器將看到新的URL並肯定應該安裝和呈現餐廳微應用。而後,該微應用本身的路由邏輯將從URL中提取餐廳ID。
我但願這個示例可以顯示 URL 的靈活性和強大功能。使用 URL 做爲消息傳遞應該知足一下條件:
當使用路由做爲微前端應用之間的通訊方式時,咱們選擇的路由即構成合同。合同一旦肯定,不能輕易修改,因此咱們應該進行自動化測試,以檢查合同是否獲得遵照。
雖然咱們但願每一個團隊和微應用盡量獨立,可是有些事情仍是會共享的。
上面提過共享組件庫,可是對於這個小型應用而言,組件庫會顯得過大。所以,咱們有一個小 的公共內容庫,其中包括圖像、JSON數據和CSS,這些內容被全部其餘微應用共享。
還有一個重要的東西須要共享:依賴庫。重複的依賴項是微前端的一個常見缺點。即便在應用程序之間共享這些依賴也很是困難,咱們來討論如何實現依賴庫的共享。
第一步是選擇要共享的依賴項。對咱們已編譯代碼的分析代表,大約50%的代碼是由 react 和 react-dom 貢獻。這兩個庫是咱們最核心的依賴項,所以若是把這兩個庫單獨提取出來做爲共享庫,會很是有效。最後,它們是很是穩定和成熟的庫,升級也很慎重,因此升級工做應該不會太困難。
至於如何提取,咱們須要作的就是在 webpack 配置中將庫標記爲外部庫(externals):
module.exports = (config, env) => {
config.externals = {
react: 'React',
'react-dom': 'ReactDOM'
}
return config;
};
複製代碼
而後,用 script 向每一個index.html 文件添加幾個標籤,以從共享內容服務器中獲取這兩個庫:
<body>
<div id="root"></div>
<script src="%REACT_APP_CONTENT_HOST%/react.prod-16.8.6.min.js"></script>
<script src="%REACT_APP_CONTENT_HOST%/react-dom.prod-16.8.6.min.js"></script>
</body>
複製代碼
與全部架構同樣,微前端架構中也存在一些折衷。咱們獲得好處的同時,也伴隨着成本。
獨立構建的 JavaScript 文件可能致使重複的公共依賴,從而增長用戶的下載量。例如,若是每一個微應用都包括本身的 React 副本,那麼用戶就得屢次下載 React。
這個問題不容易解決,那能夠獲得緩解。首先,即便咱們不作任何優化,每一個單獨頁面的加載速度也有可能比構建單個總體式前端要快。緣由是若是獨立地編譯每一個頁面,咱們就能夠有效地進行代碼拆分,頁面只加載當前頁面的依賴項。這可能會致使初始頁面加載很快,但隨後的導航速度變慢,由於用戶被迫在每一個頁面從新下載相同的依賴項。咱們能夠對用戶常去的頁面進行分析,而後單獨優化他們的依賴項。
每一個項目是不一樣的,你必須針對性地進行分析。
當微應用愈來愈多,你在本地開發時確定沒法把全部微應用和對應的後端都啓動起來,那麼你就不得不在本地進行環境的簡化。
若是開發環境和生成環境的環境是不一樣的,這每每會形成問題。因此你須要保證,若是開發者想要徹底模擬生成環境,也是能夠作到的。只不過會很是耗時。
微前端做爲一個更加分佈式的體系結構,將不可避免地要管理更多的東西:更多的代碼庫、更多的工具、更多的構建管道、更多的服務器、更多的域名等。所以在採用這樣的體系結構以前,您須要考慮幾個問題:
多年來,隨着前端不斷變複雜,咱們看到了對更可擴展的體系結構日益增加的需求。咱們應該可以經過獨立的自治團隊來開發軟件。
雖然微前端不是惟一的辦法,但咱們已經看到了許多微前端達到這些目標的實際案例,而且隨着時間的推移,咱們已經可以逐漸將這項技術應用於舊網站。不管微型前端對你和你的團隊是否是正確的方法,微前端都是一種趨勢,在這種趨勢下,前端工程化和前端體系結構都將變得愈來愈重要。
譯文完。
更多深刻閱讀: