[译] 关于 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构建 ...
随机推荐
- mysql ,show slave status详解
===想确认sql_thread线程是否应用完了io_thread接收到的了relay log,看 Master_Log_File=Relay_Master_Log_File , Read_Maste ...
- Linux学习笔记 -- 为 Shell 传递参数
我们可以在执行 Shell 脚本时,可以向脚本传递参数.脚本内获取参数的格式为:$n.(n 代表一个数字,0为所执行的shell脚本名称,1 为执行脚本的第一个参数,2 为执行脚本的第二个参数,以此类 ...
- python中常用模块详解一
1.time 模块 import time s = time.localtime() # 把时间转化成格式化的时间,通过. 取得里面的年月日等 struct_time 格式 time.struct_t ...
- Valid timeZone Values(转)
https://www.vmware.com/support/developer/vc-sdk/visdk400pubs/ReferenceGuide/timezone.html Valid time ...
- MySQL OSC(在线更改表结构)原理
1 OSC介绍 在我们的数据库操作中,更改表结构是一个常见的操作,而当我们的表数据量非常大时,我们更改表结构的时间是非 常的长,并且在跟改期间,会生成一个互斥锁,阻塞对整个表的所有操作,这样,对于我们 ...
- clock函数返回负值~ (转)
使用clock() 函数来进行计时,时不时的返回一个很大的负数,怎么检查也检查不出错误,现在找出错误原因,给大家分享一下. 来源网页:http://kebe-jea.blogbus.com/logs/ ...
- 【知识碎片】python 篇
领域:运维 网站 游戏 搜索 嵌入式 C/S软件 Openstack二次开发 绿色版:Portable Python 面向对象.解释型动态语言 env python 切换版也好使,自己寻找系统中pyt ...
- 在aspx页动态加载ascx页面内容,给GridView控件绑定数据
在aspx页动态加载ascx页面内容 //加载ascx页面内容Control c1 = this.Page.LoadControl("WebUserControl1.ascx"); ...
- 关于PHP的一个坑爹问题(页面刷新)
最近在用PHP做一个服务端和一个客户端,在快要完工的时候,出现了一个重大问题---- 当在客户端手动输入IP和端口的时候,一按连接,OK,连接成功,嘻嘻,就在我自以为大功告成的时候,来了个晴天霹雳,一 ...
- vim 的移动
越来也喜欢用linux的vim 来编程了,简单.高效.专业,最近拿着一本<vim的中文使用手册>在慢慢的看,看到现在就没有勇气继续看下去,我想先放一下,运用前面自己学的东西实际的去操作一下 ...