[譯] 關於 SPA,你需要掌握的 4 層 (2)
此文已由作者張威授權網易雲社群釋出。
歡迎訪問網易雲社群,瞭解更多網易技術產品運營經驗。
檢視層
現在我們有了一個可執行且不依賴於框架的應用程式,React 已經準備投入使用。
檢視層由 presentational components 和 container components 組成。
presentational components 關注事物的外觀,而 container components 則關注事物的工作方式。更多細節解釋請關注 Dan Abramov 的文章。
讓我們使用 ArticleFormContainer 和 ArticleListContainer 開始構建 App 元件。
// @flowimport React, {Component} from 'react';import './App.css';import {ArticleFormContainer} from "./components/ArticleFormContainer";import {ArticleListContainer} from "./components/ArticleListContainer"; type Props = {};class App extends Component<Props> { render() { return ( <div className="App"> <ArticleFormContainer/> <ArticleListContainer/> </div> ); } }export default App;
接下來,我們來建立 ArticleFormContainer。React 或者 Angular 都不重要,表單有些複雜。
檢視 Ramda 庫以及如何增強我們程式碼的宣告性質的方法。
表單接受使用者輸入並將其傳遞給 articleService 處理。此服務根據該輸入建立一個 Article,並將其新增到 ArticleStore 中以供 interested 元件使用它。所有這些邏輯都儲存在 submitForm 方法中。
『ArticleFormContainer.js』
// @flowimport React, {Component} from 'react';import * as R from 'ramda';import type {ArticleService} from "../domain/ArticleService";import type {ArticleStore} from "../store/ArticleStore";import {articleService} from "../domain/ArticleService";import {articleStore} from "../store/ArticleStore";import {ArticleFormComponent} from "./ArticleFormComponent"; type Props = {}; type FormField = { value: string; valid: boolean; } export type FormData = { articleTitle: FormField; articleAuthor: FormField; }; export class ArticleFormContainer extends Component<Props, FormData> { articleStore: ArticleStore; articleService: ArticleService; constructor(props: Props) { super(props); this.state = { articleTitle: { value: '', valid: true }, articleAuthor: { value: '', valid: true } }; this.articleStore = articleStore; this.articleService = articleService; } changeArticleTitle(event: Event) { this.setState( R.assocPath( ['articleTitle', 'value'], R.path(['target', 'value'], event) ) ); } changeArticleAuthor(event: Event) { this.setState( R.assocPath( ['articleAuthor', 'value'], R.path(['target', 'value'], event) ) ); } submitForm(event: Event) { const articleTitle = R.path(['target', 'articleTitle', 'value'], event); const articleAuthor = R.path(['target', 'articleAuthor', 'value'], event); const isTitleValid = this.articleService.isTitleValid(articleTitle); const isAuthorValid = this.articleService.isAuthorValid(articleAuthor); if (isTitleValid && isAuthorValid) { const newArticle = this.articleService.createArticle({ title: articleTitle, author: articleAuthor }); if (newArticle) { this.articleStore.addArticle(newArticle); } this.clearForm(); } else { this.markInvalid(isTitleValid, isAuthorValid); } }; clearForm() { this.setState((state) => { return R.pipe( R.assocPath(['articleTitle', 'valid'], true), R.assocPath(['articleTitle', 'value'], ''), R.assocPath(['articleAuthor', 'valid'], true), R.assocPath(['articleAuthor', 'value'], '') )(state); }); } markInvalid(isTitleValid: boolean, isAuthorValid: boolean) { this.setState((state) => { return R.pipe( R.assocPath(['articleTitle', 'valid'], isTitleValid), R.assocPath(['articleAuthor', 'valid'], isAuthorValid) )(state); }); } render() { return ( <ArticleFormComponent formData={this.state} submitForm={this.submitForm.bind(this)} changeArticleTitle={(event) => this.changeArticleTitle(event)} changeArticleAuthor={(event) => this.changeArticleAuthor(event)} /> ) } }
這裡注意 ArticleFormContainer,presentational component,返回使用者看到的真實表單。該元件顯示容器傳遞的資料,並丟擲 changeArticleTitle、 changeArticleAuthor 和 submitForm 的方法。
// @flow import React from 'react'; import type {FormData} from './ArticleFormContainer'; type Props = { formData: FormData; changeArticleTitle: Function; changeArticleAuthor: Function; submitForm: Function; } export const ArticleFormComponent = (props: Props) => { const { formData, changeArticleTitle, changeArticleAuthor, submitForm } = props; const onSubmit = (submitHandler) => (event) => { event.preventDefault(); submitHandler(event); }; return ( <form noValidate onSubmit={onSubmit(submitForm)} > <div> <label htmlFor="article-title">Title</label> <input type="text" id="article-title" name="articleTitle" autoComplete="off" value={formData.articleTitle.value} onChange={changeArticleTitle} /> {!formData.articleTitle.valid && (<p>Please fill in the title</p>)} </div> <div> <label htmlFor="article-author">Author</label> <input type="text" id="article-author" name="articleAuthor" autoComplete="off" value={formData.articleAuthor.value} onChange={changeArticleAuthor} /> {!formData.articleAuthor.valid && (<p>Please fill in the author</p>)} </div> <button type="submit" value="Submit" > Create article </button> </form> ) };
現在我們有了建立文章的表單,下面就陳列他們吧。ArticleListContainer 訂閱了 ArticleStore,獲取所有的文章並展示在 ArticleListComponent 中。
『ArticleListContainer.js』
// @flowimport * as React from 'react'import type {Article} from "../domain/Article";import type {ArticleStore} from "../store/ArticleStore";import {articleStore} from "../store/ArticleStore";import {ArticleListComponent} from "./ArticleListComponent"; type State = { articles: Article[] } type Props = {}; export class ArticleListContainer extends React.Component<Props, State> { subscriber: Function; articleStore: ArticleStore; constructor(props: Props) { super(props); this.articleStore = articleStore; this.state = { articles: [] }; this.subscriber = this.articleStore.subscribe((articles: Article[]) => { this.setState({articles}); }); } componentWillUnmount() { this.articleStore.unsubscribe(this.subscriber); } render() { return <ArticleListComponent {...this.state}/>; } }
ArticleListComponent 是一個 presentational component,他通過 props 接收文章,並展示元件 ArticleContainer。
『ArticleListComponent.js』
// @flowimport React from 'react';import type {Article} from "../domain/Article";import {ArticleContainer} from "./ArticleContainer"; type Props = { articles: Article[] }export const ArticleListComponent = (props: Props) => { const {articles} = props; return ( <div> { articles.map((article: Article, index) => ( <ArticleContainer article={article} key={index} /> )) } </div> ) };
ArticleContainer 傳遞文章資料到表現層的 ArticleComponent,同時實現 likeArticle 和 removeArticle 這兩個方法。
likeArticle 方法負責更新文章的收藏數,通過將現存的文章替換成更新後的副本。
removeArticle 方法負責從 store 中刪除制定文章。
『ArticleContainer.js』
// @flowimport React, {Component} from 'react';import type {Article} from "../domain/Article";import type {ArticleService} from "../domain/ArticleService";import type {ArticleStore} from "../store/ArticleStore";import {articleService} from "../domain/ArticleService";import {articleStore} from "../store/ArticleStore";import {ArticleComponent} from "./ArticleComponent"; type Props = { article: Article; }; export class ArticleContainer extends Component<Props> { articleStore: ArticleStore; articleService: ArticleService; constructor(props: Props) { super(props); this.articleStore = articleStore; this.articleService = articleService; } likeArticle(article: Article) { const updatedArticle = this.articleService.updateLikes(article, article.likes + 1); this.articleStore.updateArticle(updatedArticle); } removeArticle(article: Article) { this.articleStore.removeArticle(article); } render() { return ( <div> <ArticleComponent article={this.props.article} likeArticle={(article: Article) => this.likeArticle(article)} deleteArticle={(article: Article) => this.removeArticle(article)} /> </div> ) } }
ArticleContainer 負責將文章的資料傳遞給負責展示的 ArticleComponent,同時負責當 「收藏」或「刪除」按鈕被點選時在響應的回撥中通知 container component。
還記得那個作者名要大寫的無厘頭需求嗎?
ArticleComponent 在應用程式層呼叫 ArticleUiService,將一個狀態從其原始值(沒有大寫規律的字串)轉換成一個所需的大寫字串。
『ArticleComponent.js』
// @flowimport React from 'react';import type {Article} from "../domain/Article";import * as articleUiService from "../services/ArticleUiService"; type Props = { article: Article; likeArticle: Function; deleteArticle: Function; }export const ArticleComponent = (props: Props) => { const { article, likeArticle, deleteArticle } = props; return ( <div> <h3>{article.title}</h3> <p>{articleUiService.displayAuthor(article.author)}</p> <p>{article.likes}</p> <button type="button" onClick={() => likeArticle(article)} > Like </button> <button type="button" onClick={() => deleteArticle(article)} > Delete </button> </div> ); };
幹得漂亮!
我們現在有一個功能完備的 React 應用程式和一個魯棒的、定義清晰的架構。任何新晉成員都可以通過閱讀這篇文章學會如何順利的進展我們的工作。:)
你可以在這裡檢視我們最終實現的應用程式,同時奉上 GitHub 倉庫地址。
更多網易技術、產品、運營經驗分享請點選。
相關文章:
【推薦】 分散式儲存系統可靠性系列三:設計模式
【推薦】 從golang的垃圾回收說起(上篇)
相關文章
- [譯] 關於 SPA,你需要掌握的 4 層
- 關於mysql,需要掌握的基礎(二):JDBC和DAO層MySqlJDBC
- 關於mybatis,需要掌握的基礎MyBatis
- [譯] 關於 Angular 動態元件你需要知道的Angular元件
- 在 2021 年你需要掌握的 7 種關於 JavaScript 的陣列方法JavaScript陣列
- [譯] 關於 `ExpressionChangedAfterItHasBeenCheckedError` 錯誤你所需要知道的事情ExpressError
- [譯] 關於 CSS 變數,你需要了解的一切CSS變數
- [譯] 關於 Yarn 和 npm 你所需要知道的一切YarnNPM
- [譯] 關於 Angular 的變化檢測,你需要知道的一切Angular
- 關於跨域你需要知道的跨域
- [譯] 關於Angular的變更檢測(Change Detection)你需要知道這些Angular
- 關於等保2.0,你需要了解的
- 關於redis,你需要了解的幾點!Redis
- 關於CSS Transition,你需要知道的事CSS
- 14.關於Dart中的Future你掌握的夠嗎?Dart
- 【Java8新特性】關於Java8中的日期時間API,你需要掌握這些!!JavaAPI
- 關於Android模組化你需要知道的Android
- [譯] 關於 Flutter 頁面路由過渡動畫,你所需要知道的一切Flutter路由動畫
- 2021年你需要掌握的前端小知識前端
- 關於webpack優化,你需要知道的事(上篇)Web優化
- 你需要知道的關於 Go 包的一切Go
- 關於GDPR,你需要了解的的5件事
- [譯] 為什麼你需要關注一下 FlutterFlutter
- 關於WSL2你應該知道的
- 關於深度學習編譯器,這些知識你需要了解一下深度學習編譯
- 【譯】關於JavaScript 陣列你應該知道的事情JavaScript陣列
- 幾你需要熟練掌握的辦公軟體
- [譯]隱式轉型,你值得掌握
- 關於 React19,你需要了解的前因後果React
- 【ASK_ORACLE】關於Oracle索引分裂你需要知道的Oracle索引
- 關於有效客戶關係管理,你需要了解的一切
- 關於Service你所需要知道內容(一)
- 關於 ThinkSNS+ 程式的 SPA(H5)安裝教程H5
- 【譯】關於 JavaScript 的原型你應該知道的所有事情JavaScript原型
- 掌握了開源框架還不夠,你更需要掌握原始碼框架原始碼
- 10個你在JavaScript面試前需要掌握的概念JavaScript面試
- [譯] 你不需要基於 CSS Grid 的柵格佈局系統CSS
- 關於進入遊戲行業你需要知道的事遊戲行業