[译] 关于 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 仓库地址。
更多网易技术、产品、运营经验分享请点击。
相关文章:
【推荐】 Docker 的优势
[译] 关于 SPA,你需要掌握的 4 层 (2)的更多相关文章
- [译] 关于 SPA,你需要掌握的 4 层 (1)
此文已由作者张威授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 我们从头来构建一个 React 的应用程序,探究领域.存储.应用服务和视图这四层 每个成功的项目都需要一个清晰 ...
- 【译】为什么BERT有3个嵌入层,它们都是如何实现的
目录 引言 概览 Token Embeddings 作用 实现 Segment Embeddings 作用 实现 Position Embeddings 作用 实现 合成表示 结论 参考文献 本文翻译 ...
- 有道词典中的OCR功能:第三方库的变化
之前有点好奇有道词典中的OCR功能,具体来说就是强力取词功能.我知道的最有名的OCR库是tesseract,这个库是惠普在早些年前开源的. 在用python做爬虫处理验证码的时候,就会用到这个库,对应 ...
- 第一章:关于Ehcache
PDF官方文档:http://www.ehcache.org/generated/2.10.4/pdf/About_Ehcache.pdf 1.什么是Ehcache? Ehcache是一种开源的基于标 ...
- Ehcache概念篇
前言 缓存用于提供性能和减轻数据库负荷,本文在缓存概念的基础上对Ehcache进行叙述,经实践发现3.x版本高可用性不好实现,所以我采用2.x版本. Ehcache是开源.基于缓存标准.基于java的 ...
- 《Effective Java》第9章 异常
第58条:对可恢复的情况使用受检异常,对编程错误使用运行时异常 Java程序设计语言提供了三种可抛出结构(throwable) ;受检的异常(checked exception)运行时异常(run-t ...
- Java 异常规范
1. 只针对异常情况使用异常,不要用异常来控制流程 try { int i = 0; while (true) { range[i++].doSomething(); } } catch (Array ...
- Effective.Java第67-77条(异常相关)
67. 明智审慎地进行优化 有三条优化的格言是每个人都应该知道的: (1)比起其他任何单一的原因(包括盲目的愚钝),很多计算上的过失都被归咎于效率(不一定能实现) (2)不要去计算效率上的一些小小的 ...
- [译]ABP框架使用AngularJs,ASP.NET MVC,Web API和EntityFramework构建N层架构的SPA应用程序
本文转自:http://www.skcode.cn/archives/281 本文演示ABP框架如何使用AngularJs,ASP.NET MVC,Web API 和EntityFramework构建 ...
随机推荐
- 分布式锁之三:Redlock实现分布式锁
之前写过一篇文章<如何在springcloud分布式系统中实现分布式锁?>,由于自己仅仅是阅读了相关的书籍,和查阅了相关的资料,就认为那样的是可行的.那篇文章实现的大概思路是用setNx命 ...
- Spring Cloud与分布式系统
本文不是讲解如何使用spring Cloud的教程,而是探讨Spring Cloud是什么,以及它诞生的背景和意义. 背景 2008年以后,国内互联网行业飞速发展,我们对软件系统的需求已经不再是过去” ...
- oracle查看表空间和物理文件大小
查看各表空间的使用情况 select a.tablespace_name,a.bytes/1024/1024 "Sum MB",(a.bytes-b.bytes)/1024/102 ...
- 【linux】查看进程使用的端口和端口使用情况
netstat -a 查看所有服务端口 netstat -tln 查看当前使用的端口 ps命令查看进程的id: ps aux | grep ftp 或者 pidof Name netstat命 ...
- Python Twisted系列教程2:异步编程初探与reactor模式
作者:dave@http://krondo.com/slow-poetry-and-the-apocalypse/ 译者:杨晓伟(采用意译) 这个系列是从这里开始的,欢迎你再次来到这里来.现在我们可 ...
- Java面向对象-String类
1,实例化String对象 有两种方式,我们直接看代码: package com.java1234.chap03.sec08; public class Demo1 { public static v ...
- SerialPort缓冲区
SerialPort缓冲区中有:接收缓冲区,发送缓冲区,输入缓冲区,输出缓冲区,传输缓冲区. 例如: 串口属性:BytesToRead(获取接收缓冲区中数据的字节数)--这里提到的是“接收缓冲区” 串 ...
- Struts2 result type(结果类型)
转自:http://www.cnblogs.com/liaojie970/p/7151103.html 在struts2框架中,当action处理完之后,就应该向用户返回结果信息,该任务被分为两部分: ...
- ubuntu安装Docky 3.0
添加PPA并在命令行安装,执行以下命令: sudo add-apt-repository ppa:ricotz/docky sudo apt-get update sudo apt-get insta ...
- 设备控制接口ioctl详解
[转]Linux设备控制接口 序言设备驱动程序的一个基本功能就是管理和控制设备,同时为用户应用程序提供管理和控制设备的接口.我们前面的“Hello World”驱动程序已经可以提供读写功能了,在这里我 ...