[譯] 關於 SPA,你需要掌握的 4 層 (2)

weixin_33890499發表於2018-11-20

此文已由作者張威授權網易雲社群釋出。

歡迎訪問網易雲社群,瞭解更多網易技術產品運營經驗。


檢視層

現在我們有了一個可執行且不依賴於框架的應用程式,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 的方法。


ArticleFormComponent.js

// @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的垃圾回收說起(上篇)

相關文章