框架主要功能
每個主要的 JavaScript 框架都有不同的方法來更新 DOM,處理瀏覽器事件,並提供愉快的開發體驗。本文將探討“四大”框架的主要功能,從高層次瞭解框架的工作方式,以及它們之間的區別。
| 先決條件 | 熟悉核心 HTML、CSS 和 JavaScript 語言。 |
|---|---|
| 目標 | 瞭解框架的主要程式碼功能。 |
領域特定語言
本模組中討論的所有框架都由 JavaScript 提供支援,並且都允許您使用特定領域語言 (DSL) 來構建您的應用程式。特別是,React 推廣了使用 **JSX** 來編寫其元件,而 Ember 使用 **Handlebars**。與 HTML 不同,這些語言知道如何讀取資料變數,並且這些資料可用於簡化編寫 UI 的過程。
Angular 應用程式通常大量使用 **TypeScript**。TypeScript 不關心使用者介面的編寫,但它是一種特定領域語言,並且與普通 JavaScript 存在顯著差異。
DSL 不能被瀏覽器直接讀取;它們必須先轉換為 JavaScript 或 HTML。 轉換是開發過程中的一個額外步驟,但框架工具通常包含處理此步驟所需的工具,或者可以調整以包含此步驟。雖然可以在不使用這些特定領域語言的情況下構建框架應用程式,但使用它們將簡化您的開發過程,並使您更容易從這些框架周圍的社群獲得幫助。
JSX
JSX 代表 JavaScript 和 XML,是 JavaScript 的擴充套件,它將 HTML 類似語法引入 JavaScript 環境。它由 React 團隊發明,用於 React 應用程式,但可用於開發其他應用程式,例如 Vue 應用程式。
以下是簡單的 JSX 示例
const subject = "World";
const header = (
<header>
<h1>Hello, {subject}!</h1>
</header>
);
此表示式表示一個 HTML <header> 元素,其中包含一個 <h1> 元素。{subject} 周圍的花括號告訴應用程式讀取 subject 常量的值並將其插入我們的 <h1> 中。
與 React 一起使用時,上一段中的 JSX 將編譯成以下內容
const subject = "World";
const header = React.createElement(
"header",
null,
React.createElement("h1", null, "Hello, ", subject, "!"),
);
最終由瀏覽器渲染時,上面的程式碼段將生成看起來像這樣的 HTML
<header>
<h1>Hello, World!</h1>
</header>
Handlebars
The Handlebars 模板語言不特定於 Ember 應用程式,但在 Ember 應用程式中被大量使用。Handlebars 程式碼類似於 HTML,但可以選擇從其他地方提取資料。此資料可用於影響應用程式最終構建的 HTML。
與 JSX 一樣,Handlebars 使用花括號來插入變數的值。Handlebars 使用一對雙花括號,而不是一對單花括號。
給出以下 Handlebars 模板
<header>
<h1>Hello, {{subject}}!</h1>
</header>
以及以下資料
{
subject: "World";
}
Handlebars 將構建這樣的 HTML
<header>
<h1>Hello, World!</h1>
</header>
TypeScript
TypeScript 是 JavaScript 的超集,這意味著它擴充套件了 JavaScript — 所有 JavaScript 程式碼都是有效的 TypeScript,反之則不然。TypeScript 對於開發人員能夠在其程式碼上實施的嚴格性很有用。例如,考慮一個函式 add(),它接受整數 a 和 b 並返回它們的總和。
在 JavaScript 中,該函式可以這樣編寫
function add(a, b) {
return a + b;
}
這段程式碼對於習慣了 JavaScript 的人來說可能很簡單,但它仍然可以更清晰。JavaScript 允許我們使用 + 運算子將字串連線在一起,因此此函式在技術上仍然可以工作,即使 a 和 b 是字串,它可能不會給你預期的結果。如果我們只想允許將數字傳遞給此函式怎麼辦?TypeScript 使這成為可能
function add(a: number, b: number) {
return a + b;
}
每個引數後面寫的 : number 告訴 TypeScript a 和 b 都必須是數字。如果我們要使用此函式並將 '2' 作為引數傳遞給它,TypeScript 將在編譯期間引發錯誤,我們必須修復我們的錯誤。我們可以編寫我們自己的 JavaScript 來為我們引發這些錯誤,但這會使我們的原始碼變得冗長。也許讓 TypeScript 為我們處理此類檢查更有意義。
編寫元件
如上一章所述,大多數框架都具有一定的元件模型。React 元件可以使用 JSX 編寫,Ember 元件可以使用 Handlebars 編寫,Angular 和 Vue 元件可以使用稍微擴充套件了 HTML 的模板語法編寫。
無論他們對如何編寫元件有什麼意見,每個框架的元件都提供了一種方法來描述它們可能需要的外部屬性,元件應該管理的內部狀態,以及使用者可以在元件標記上觸發的事件。
本節其餘部分的程式碼片段將以 React 為例,並使用 JSX 編寫。
屬性
屬性或 **props** 是元件為了渲染而需要的外部資料。假設您正在為線上雜誌構建一個網站,並且您需要確保每位投稿作者都獲得了他們作品的署名。您可能會為每篇文章建立一個 AuthorCredit 元件。此元件需要顯示作者的肖像和關於他們的簡短簡介。為了知道要渲染哪個影像以及要列印哪個簡介,AuthorCredit 需要接受一些 props。
AuthorCredit 元件的 React 表示可能看起來像這樣
function AuthorCredit(props) {
return (
<figure>
<img src={props.src} alt={props.alt} />
<figcaption>{props.byline}</figcaption>
</figure>
);
}
{props.src}、{props.alt} 和 {props.byline} 代表 props 將插入元件的位置。要渲染此元件,我們將在我們要渲染它的地方編寫這樣的程式碼(這很可能在另一個元件內部)
<AuthorCredit
src="./assets/zelda.png"
alt="Portrait of Zelda Schiff"
byline="Zelda Schiff is editor-in-chief of the Library Times."
/>
這將最終在瀏覽器中渲染以下 <figure> 元素,其結構如 AuthorCredit 元件中定義的那樣,其內容如 AuthorCredit 元件呼叫中包含的 props 中定義的那樣
<figure>
<img src="assets/zelda.png" alt="Portrait of Zelda Schiff" />
<figcaption>Zelda Schiff is editor-in-chief of the Library Times.</figcaption>
</figure>
狀態
我們在上一章討論過 **狀態** 的概念 — 強大的狀態處理機制是有效框架的關鍵,每個元件可能都有一些需要控制其狀態的資料。只要元件在使用,此狀態就會以某種方式持續存在。與 props 一樣,狀態可用於影響元件的渲染方式。
例如,考慮一個按鈕,它計算它被點選了多少次。此元件應該負責跟蹤它自己的計數狀態,並且可以這樣編寫
function CounterButton() {
const [count] = useState(0);
return <button>Clicked {count} times</button>;
}
useState() 是一個 **React hook**,它在給定初始資料值的情況下,會跟蹤該值在更新時的變化。程式碼將最初像這樣在瀏覽器中渲染
<button>Clicked 0 times</button>
useState() 呼叫以一種穩健的方式在整個應用程式中跟蹤 count 值,而無需您自己編寫程式碼來執行此操作。
事件
為了具有互動性,元件需要能夠響應瀏覽器事件,以便我們的應用程式能夠響應我們的使用者。每個框架都提供了自己的語法來監聽瀏覽器事件,這些事件引用了等效的本機瀏覽器事件的名稱。
在 React 中,監聽 click 事件需要一個特殊的屬性 onClick。讓我們更新上面的 CounterButton 程式碼,使其能夠計算點選次數
function CounterButton() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>Clicked {count} times</button>
);
}
在此版本中,我們使用額外的 useState() 功能來建立一個特殊的 setCount() 函式,我們可以呼叫它來更新 count 的值。我們在 onClick 事件處理程式內呼叫此函式,將 count 設定為其當前值加上一。
樣式化元件
處理依賴項
所有主要框架都提供了用於處理依賴項的機制 — 在其他元件內部使用元件,有時使用多個層次結構級別。與其他功能一樣,確切的機制因框架而異,但最終結果相同。元件往往使用標準的 JavaScript 模組語法 將元件匯入其他元件,或者至少使用類似的東西。
元件中的元件
基於元件的 UI 架構的一個關鍵優勢是元件可以組合在一起。就像您可以將 HTML 標籤彼此巢狀以構建網站一樣,您也可以在其他元件內使用元件來構建 Web 應用程式。每個框架都允許您編寫利用(因此依賴於)其他元件的元件。
例如,我們的 AuthorCredit React 元件可能在 Article 元件內使用。這意味著 Article 需要匯入 AuthorCredit。
import AuthorCredit from "./components/AuthorCredit";
完成後,AuthorCredit 可以在 Article 元件內像這樣使用
<Article>
<AuthorCredit />
</Article>
依賴注入
現實世界中的應用程式通常涉及具有多個巢狀級別的元件結構。AuthorCredit 元件在許多級別深度巢狀可能出於某種原因需要來自我們應用程式最頂層的根級別的資料。
假設我們正在構建的雜誌網站結構如下
<App>
<Home>
<Article>
<AuthorCredit {/* props */} />
</Article>
</Home>
</App>
我們的 App 元件具有一些我們的 AuthorCredit 元件需要的資料。我們可以重寫 Home 和 Article,以便它們知道如何將 props 傳遞下去,但這如果我們的資料起源和目的地之間存在很多級別,可能會變得很繁瑣。這也是多餘的:Home 和 Article 實際上並不使用作者的肖像或簡介,但如果我們想將該資訊放入 AuthorCredit,我們將需要更改 Home 和 Article 來適應它。
透過多層元件傳遞資料的這個問題稱為 props 鑽取,對於大型應用程式來說並不理想。
為了避免屬性傳遞(prop drilling),框架提供了依賴注入的功能,這是一種將特定資料直接傳遞給需要它的元件的方法,無需透過中間層傳遞。每個框架都以不同的名稱和方式實現依賴注入,但最終的效果是一樣的。
Angular 將此過程稱為 依賴注入;Vue 有 provide() 和 inject() 元件方法;React 有 Context API;Ember 透過 服務 共享狀態。
生命週期
在框架的上下文中,元件的 **生命週期** 是元件從被附加到 DOM 並由瀏覽器渲染(通常稱為 *掛載*)到從 DOM 中刪除(通常稱為 *解除安裝*)所經歷的一系列階段。每個框架對這些生命週期階段的命名不同,並非所有框架都允許開發人員訪問相同的階段。所有框架都遵循相同的通用模型:它們允許開發人員在元件 *掛載*、*渲染*、*解除安裝* 以及這些階段之間進行許多操作時執行特定操作。
渲染 階段是最重要的,因為當用戶與應用程式互動時,它會被重複執行最多次。每當瀏覽器需要渲染新的內容時,它都會執行,無論新資訊是新增到瀏覽器中的內容、刪除的內容還是對現有內容的編輯。
這個 React 元件生命週期的圖表 提供了對該概念的概覽。
渲染元素
與生命週期一樣,框架對如何渲染應用程式採用了不同但類似的方法。它們都跟蹤瀏覽器 DOM 的當前渲染版本,並根據元件在應用程式中重新渲染的方式做出略微不同的決定來改變 DOM。由於框架為您做出了這些決定,因此您通常不會自己與 DOM 互動。這種對 DOM 的抽象比自己更新 DOM 更復雜,也更消耗記憶體,但沒有它,框架將無法讓您以它們所知的方式進行宣告式程式設計。
**虛擬 DOM** 是一種方法,透過這種方法,關於瀏覽器 DOM 的資訊儲存在 JavaScript 記憶體中。應用程式更新此 DOM 副本,然後將其與“真實” DOM(為使用者實際渲染的 DOM)進行比較,以決定要渲染的內容。應用程式構建一個“差異”來比較更新後的虛擬 DOM 與當前渲染的 DOM 之間的差異,並使用該差異將更新應用於真實 DOM。React 和 Vue 都使用虛擬 DOM 模型,但它們在進行差異比較或渲染時不使用完全相同的邏輯。
您可以 在 React 文件中閱讀有關虛擬 DOM 的更多資訊。
**增量 DOM** 與虛擬 DOM 類似,它構建了一個 DOM 差異來決定要渲染的內容,但不同之處在於它沒有在 JavaScript 記憶體中建立完整的 DOM 副本。它忽略了不需要更改的 DOM 部分。Angular 是本模組中迄今為止討論過的唯一使用增量 DOM 的框架。
您可以 在 Auth0 部落格中閱讀有關增量 DOM 的更多資訊。
**Glimmer VM** 是 Ember 獨有的。它不是虛擬 DOM 也不是增量 DOM;它是一個獨立的過程,透過它,Ember 的模板被轉譯成一種類似於“位元組碼”的東西,這種位元組碼比 JavaScript 更易於讀取和更快速。
路由
如 前一章所述,路由 是 Web 體驗的重要組成部分。為了避免在具有大量檢視的足夠複雜的應用程式中出現故障體驗,本模組中介紹的每個框架都提供了一個庫(或多個庫)來幫助開發人員在其應用程式中實現客戶端路由。
測試
所有應用程式都受益於測試覆蓋範圍,這確保了您的軟體能夠繼續按預期的方式執行,Web 應用程式也不例外。每個框架的生態系統都提供了有助於編寫測試的工具。測試工具本身並非內建在框架中,但用於生成框架應用程式的命令列介面工具允許您訪問適當的測試工具。
每個框架在其生態系統中都有廣泛的工具,具有執行單元測試和整合測試的功能。
Testing Library 是一套測試實用程式,它包含許多 JavaScript 環境(包括 React、Vue 和 Angular)的工具。Ember 文件涵蓋了 Ember 應用程式的測試。
以下是對我們的 CounterButton 的快速測試,它是藉助 React Testing Library 編寫的 - 它測試了多方面內容,例如按鈕的存在,以及在被點選 0 次、1 次和 2 次後按鈕是否顯示了正確的文字。
import { fireEvent, render, screen } from "@testing-library/react";
import CounterButton from "./CounterButton";
it("Renders a semantic button with an initial state of 0", () => {
render(<CounterButton />);
const btn = screen.getByRole("button");
expect(btn).toBeInTheDocument();
expect(btn).toHaveTextContent("Clicked 0 times");
});
it("Increments the count when clicked", () => {
render(<CounterButton />);
const btn = screen.getByRole("button");
fireEvent.click(btn);
expect(btn).toHaveTextContent("Clicked 1 times");
fireEvent.click(btn);
expect(btn).toHaveTextContent("Clicked 2 times");
});