- 原文地址:A Guide to Custom Elements for React Developers
- 原文作者:CHARLES PETERS
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:子非
- 校對者:Xcco, Ivocin
最近我需要構建 UI 介面,雖然現在 React.js 是我更為青睞的 UI 解決方案,不過長時間以來我第一次沒有選擇用它。然後我看了瀏覽器內建的 API 發現使用自定義元素(也就是 Web 元件)可能正是 React 開發者需要的方案。
自定義元素可以具有與 React 元件大致相同的優點,而且實現起來無需繫結特定的框架。自定義元素能提供新的 HTML 標籤,我們可以使用原生瀏覽器的 API,用程式設計的方式操控它。
讓我們說說基於元件的 UI 優點:
- 封裝 — 把專注點放在元件的內部實現上
- 複用 — 當把 UI 分割成更通用的小塊時,它們更容易分解為你可以複用的形態
- 隔離 — 因為元件是被封裝過的,你能獲得隔離帶來的額外好處,即讓你更輕鬆地定位錯誤和更易修改應用中的特定部分
用例
你可能想知道有誰在生產環境中使用自定義元素。比較出名的有:
和元件 API 的相似點
當試圖比較 React 元件和自定義元件時,我發現它們的 API 非常相似:
- 它們都是類,而類已經不是新的概念了,並且都能擴充套件自基類
- 它們都繼承掛載或渲染生命週期
- 它們都需要通過 props 或 attributes 來靜態或動態傳入資料
演示
那麼,讓我們來構建一個小型應用,提供 GitHub 倉庫的詳細資訊列表。
如果我要用 React 來實現,我會定義一個如下的簡單元件:
<Repository name="charliewilco/obsidian" />
複製程式碼
這個元件需要一個 prop —— 倉庫名,我們要這麼實現它:
class Repository extends React.Component {
state = {
repo: null
};
async getDetails(name) {
return await fetch(`https://api.github.com/repos/${name}`, {
mode: 'cors'
}).then(res => res.json());
}
async componentDidMount() {
const { name } = this.props;
const repo = await this.getDetails(name);
this.setState({ repo });
}
render() {
const { repo } = this.state;
if (!repo) {
return <h1>Loading</h1>;
}
if (repo.message) {
return <div className="Card Card--error">Error: {repo.message}</div>;
}
return (
<div class="Card">
<aside>
<img
width="48"
height="48"
class="Avatar"
src={repo.owner.avatar_url}
alt="Profile picture for ${repo.owner.login}"
/>
</aside>
<header>
<h2 class="Card__title">{repo.full_name}</h2>
<span class="Card__meta">{repo.description}</span>
</header>
</div>
);
}
}
複製程式碼
請看 Charles (@charliewilco) 在 CodePen 上的 React 演示 — GitHub。
來深入看一下,我們有一個元件,這個元件有它自己的狀態,即倉庫的詳細資訊。開始時,我們把它設為 null
,因為此時還沒有任何資料,所以在載入資料時會有一個載入提示。
在 React 的生命週期中,我們使用 fetch 從 GitHub 獲得資料,建立選項卡,然後在我們拿到返回資料後使用 setState()
觸發一次重新渲染。所有 UI 使用的不同狀態都會在 render()
方法裡表現出來。
定義/使用自定義元素
使用自定義元素實現起來稍有不同。和 React 元件一樣,我們的自定義元素也需要一個屬性 —— 倉庫名,它的狀態也是自己管理的。
如下就是我們的元素:
<github-repo name="charliewilco/obsidian"></github-repo>
<github-repo name="charliewilco/level.css"></github-repo>
<github-repo name="charliewilco/react-branches"></github-repo>
<github-repo name="charliewilco/react-gluejar"></github-repo>
<github-repo name="charliewilco/dotfiles"></github-repo>
複製程式碼
請看 Charles (@charliewilco) 在 CodePen 上的自定義元素演示 — GitHub。
現在,我們所需要做的就是定義和註冊自定義元素,建立一個類,它繼承自 HTMLElement
類,然後用 customElements.define()
註冊元素的名字。
class OurCustomElement extends HTMLElement {}
window.customElements.define('our-element', OurCustomElement);
複製程式碼
它是這樣呼叫的:
<our-element></our-element>
複製程式碼
這個新元素現在還不是很有用,但是有它之後,我們能用三個方法來擴充套件這個元素的功能。這些方法類似於 React 元件的 生命週期 API。兩個和我們最相關的類生命週期函式是 disconnectedCallBack
和 connectedCallback
,而且由於自定義元素是一個類,它自然會有一個構造器。
名字 | 何時呼叫 |
---|---|
constructor |
用來建立或更新元素的例項。常用來初始化狀態、設定事件監聽或建立 Shadow DOM。如果你想知道在 constructor 可以做什麼,請檢視設計規範。 |
connectedCallback |
在元素被插入 DOM 後呼叫。用來執行建立任務的程式碼,例如獲取資源或渲染 UI。總體上說,你應該在這裡嘗試非同步任務。 |
disconnectedCallback |
在元素被移出 DOM 後呼叫。用來執行做清理任務的程式碼。 |
為了實現我們的自定義元素,我們建立瞭如下類並設定了和 UI 相關的屬性:
class Repository extends HTMLElement {
constructor() {
super();
this.repoDetails = null;
this.name = this.getAttribute("name");
this.endpoint = `https://api.github.com/repos/${this.name}`
this.innerHTML = `<h1>Loading</h1>`
}
}
複製程式碼
通過在我們的構造器中呼叫 super()
,元素自己的上下文和 DOM 操作 API 就可以使用了。目前,我們已經設定了預設的倉庫詳情為 null
,從元素屬性取得倉庫名,建立一個用來呼叫的 endpoint,這樣我們不用在後面定義,最重要的是,將初始的 HTML 設定成了載入提示。
為了獲取關於元素倉庫的詳情,我們將需要向 GitHub 的 API 傳送請求。我們使用 fetch
,由於它是基於 Promise 的,我們使用 async
和 await
來使我們的程式碼更易閱讀。你可以在這裡瞭解更多關於 async
/await
關鍵字,並且可以在這裡瞭解更多瀏覽器的 fetch API 的內容。你還可以在 Twitter 上和我討論,瞭解我是否更喜歡 Axios 庫。(提示,這取決於我早餐時喝了茶還是咖啡。)
現在,讓我們給這個類新增方一個方法來向 GitHub 查詢倉庫詳情。
class Repository extends HTMLElement {
constructor() {
// ...
}
async getDetails() {
return await fetch(this.endpoint, { mode: "cors" }).then(res => res.json());
}
}
複製程式碼
下面,讓我們使用 connectedCallback
方法和 Shadow DOM 來使用 getDetails
方法的返回值。使用這個方法的效果和我們在 React 示例中呼叫 Repository.componentDidMount()
類似。我們將開始時賦給this.repoDetails
的 null
替換掉 —— 並將在後面呼叫模板建立 HTML 時使用它。
class Repository extends HTMLElement {
constructor() {
// ...
}
async getDetails() {
// ...
}
async connectedCallback() {
let repo = await this.getDetails();
this.repoDetails = repo;
this.initShadowDOM();
}
initShadowDOM() {
let shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot.innerHTML = this.template;
}
}
複製程式碼
你會注意到我們正在呼叫與 Shadow DOM 相關的方法。除了作為被漫威電影拒絕的標題之外,Shadow DOM 還有自己豐富的 API 值得研究。為了我們的目標,它將抽象出一種將 innerHTML
新增到元素的實現。
現在我們將 this.template
賦值給 innerHTML
。現在來定義 template
:
class Repository extends HTMLElement {
get template() {
const repo = this.repoDetails;
// 如果獲取錯誤資訊,向使用者顯示提示資訊
if (repo.message) {
return `<div class="Card Card--error">Error: ${repo.message}</div>`
} else {
return `
<div class="Card">
<aside>
<img width="48" height="48" class="Avatar" src="${repo.owner.avatar_url}" alt="Profile picture for ${repo.owner.login}" />
</aside>
<header>
<h2 class="Card__title">${repo.full_name}</h2>
<span class="Card__meta">${repo.description}</span>
</header>
</div>
`
}
}
}
複製程式碼
自定義元素差不多就是這樣。自定義元素可以管理自身狀態、獲取自身資料及將狀態體現給使用者,同時提供了可以在應用程式裡使用的 HTML 元素。
在完成本次練習之後,我發現自定義元素唯一需要的依賴是瀏覽器的原生 API 而不是另外需要解析和執行的框架。這是一個更具可移植性和可複用性的解決方案,而且這個方案和你喜歡並用之謀生的框架的 API 很相似。
當然,這種方法也有缺點,我們說的是不同瀏覽器的支援問題和缺乏一致性。此外,DOM 操作 API 可能會十分混亂。有時它們是賦值。有時它們是函式。有時這些方法需要回撥函式而有時又不需要。如果你不相信,那就去看一下使用 document.createElement()
將類新增進 HTML 元素的方法,這是使用 React 的五大理由之一。基本實現其實並不複雜,但它與其他類似的 document
方法不一致。
現實的問題是:它是否會被淘汰?也許會。React 仍然在它該擅長的東西上表現良好:虛擬 DOM、管理應用狀態、封裝和在樹中向下傳遞資料。現在幾乎沒有在該框架中使用自定義元素的動力。另一方面,自定義元素在製作瀏覽器應用上非常簡單實用。
瞭解更多
如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。