[譯] 關於 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元件
- [譯] 關於 `ExpressionChangedAfterItHasBeenCheckedError` 錯誤你所需要知道的事情ExpressError
- [譯] 關於 CSS 變數,你需要了解的一切CSS變數
- 在 2021 年你需要掌握的 7 種關於 JavaScript 的陣列方法JavaScript陣列
- [譯] 關於 Yarn 和 npm 你所需要知道的一切YarnNPM
- 關於Android Gradle你需要知道這些(4)AndroidGradle
- [翻譯]程式設計師需要掌握的6項相關技能程式設計師
- [譯] 關於 Angular 的變化檢測,你需要知道的一切Angular
- 關於跨域你需要知道的跨域
- 關於Android Gradle你需要知道這些(2)AndroidGradle
- [譯] 關於Angular的變更檢測(Change Detection)你需要知道這些Angular
- 關於redis,你需要了解的幾點!Redis
- 關於CSS Transition,你需要知道的事CSS
- 關於MongoDB你需要知道的幾件事MongoDB
- 14.關於Dart中的Future你掌握的夠嗎?Dart
- 【Java8新特性】關於Java8中的日期時間API,你需要掌握這些!!JavaAPI
- 你需要掌握的三種程式語言
- iOS-UITableView你需要掌握的屬性iOSUIView
- [譯]關於ReactRouter4的一切React
- [譯] 關於 React Router 4 的一切React
- 關於Android模組化你需要知道的Android
- 關於 Web Workers 你需要了解的 7 件事Web
- 關於字元編碼,你所需要知道的字元
- [譯] 關於 Flutter 頁面路由過渡動畫,你所需要知道的一切Flutter路由動畫
- [譯] 所有你需要知道的關於完全理解 Node.js 事件迴圈及其度量Node.js事件
- [譯]關於 Parcel 你所需要知道的一切:快速的 Web 應用打包工具Web
- 關於webpack優化,你需要知道的事(上篇)Web優化
- 關於 Git 你需要知道的一些事情Git
- 關於C++14:你需要知道的新特性C++
- 你需要知道的關於 Go 包的一切Go
- 你一定需要的關於CNN、RNN的講解CNNRNN
- 幾你需要熟練掌握的辦公軟體
- 10個你在JavaScript面試前需要掌握的概念JavaScript面試
- [譯] 為什麼你需要關注一下 FlutterFlutter
- [譯]隱式轉型,你值得掌握