此文已由作者张威授权网易云社区发布。

欢迎访问网易云社区,了解更多网易技术产品运营经验。

我们从头来构建一个 React 的应用程序,探究领域、存储、应用服务和视图这四层

每个成功的项目都需要一个清晰的架构,这对于所有团队成员都是心照不宣的。

试想一下,作为团队的新人。技术负责人给你介绍了在项目进程中提出的新应用程序的架构。

然后告诉你需求:

我们的应用程序将显示一系列文章。用户能够创建、删除和收藏文章。

然后他说,去做吧!

Ok,没问题,我们来搭框架吧

我选择 FaceBook 开源的构建工具 Create React App,使用 Flow 来进行类型检查。简单起见,先忽略样式。

作为先决条件,让我们讨论一下现代框架的声明性本质,以及涉及到的 state 概念。

现在的框架多为声明式的

React, Angular, Vue 都是声明式的,并鼓励我们使用函数式编程的思想。

你有见过手翻书吗?

一本手翻书或电影书,里面有一系列逐页变化的图片,当页面快速翻页的时候,就形成了动态的画面。 [1]

现在让我们来看一下 React 中的定义:

在应用程序中为每个状态设计简单的视图, React 会在数据发生变化时高效地更新和渲染正确的组件。 [2]

Angular 中的定义:

使用简单、声明式的模板快速构建特性。使用您自己的组件扩展模板语言。 [3]

大同小异?

框架帮助我们构建包含视图的应用程序。视图是状态的表象。那状态又是什么?

状态

状态表示应用程序中会更改的所有数据。

你访问一个URL,这是状态,发出一个 Ajax 请求来获取电影列表,这是也状态,将信息持久化到本地存储,同上,也是状态。

状态由一系列不变对象组成

不可变结构有很多好处,其中一个就是在视图层。

下面是 React 指南对性能优化介绍的引言。

不变性使得跟踪更改变得更容易。更改总是会产生一个新对象,所以我们只需要检查对象的引用是否发生了更改。

领域层

域可以描述状态并保存业务逻辑。它是应用程序的核心,应该与视图层解耦。Angular, React 或者是 Vue,这些都不重要,重要的是不管选择什么框架,我们都能够使用自己的领。

因为我们处理的是不可变的结构,所以我们的领域层将包含实体和域服务。

在 OOP 中存在争议,特别是在大规模应用程序中,在使用不可变数据时,贫血模型是完全可以接受的。

对我来说,弗拉基米尔·克里科夫(Vladimir Khorikov)的这门课让我大开眼界。

要显示文章列表,我们首先要建模的是Article实体。

所有 Article 类型实体的未来对象都是不可变的。Flow 可以通过使所有属性只读(属性前面带 + 号)来强制将对象不可变。

// @flowexport type Article = {
  +id: string;
  +likes: number;
  +title: string;
  +author: string;
}

现在,让我们使用工厂函数模式创建 articleService。

查看 @mpjme 的这个视频,了解更多关于JS中的工厂函数知识。

由于在我们的应用程序中只需要一个articleService,我们将把它导出为一个单例。

createArticle 允许我们创建 Article 的冻结对象。每一篇新文章都会有一个唯一的自动生成的id和零收藏,我们仅需要提供作者和标题。

**Object.freeze()** 方法可冻结一个对象:即无法给它新增属性。 [5]

createArticle 方法返回的是一个 Article 的「Maybe」类型

Maybe 类型强制你在操作 Article 对象前先检查它是否存在。

如果创建文章所需要的任一字段校验失败,那么 createArticle 方法将返回null。这里可能有人会说,最好抛出一个用户定义的异常。如果我们这么做,但上层不实现catch块,那么程序将在运行时终止。 updateLikes 方法会帮我们更新现存文章的收藏数,将返回一个拥有新计数的副本。

最后,isTitleValid 和 isAuthorValid 方法能帮助 createArticle 隔离非法数据。

// @flowimport v1 from 'uuid';import * as R from 'ramda';import type {Article} from "./Article";import * as validators from "./Validators";export type ArticleFields = {
  +title: string;
  +author: string;
}export type ArticleService = {
  createArticle(articleFields: ArticleFields): ?Article;
  updateLikes(article: Article, likes: number): Article;
  isTitleValid(title: string): boolean;
  isAuthorValid(author: string): boolean;
}export const createArticle = (articleFields: ArticleFields): ?Article => {  const {title, author} = articleFields;  return isTitleValid(title) && isAuthorValid(author) ?
    Object.freeze({      id: v1(),      likes: 0,
      title,
      author
    }) :    null;
};export const updateLikes = (article: Article, likes: number) =>
  validators.isObject(article) ?
    Object.freeze({
      ...article,
      likes
    }) :
    article;export const isTitleValid = (title: string) =>
  R.allPass([
    validators.isString,
    validators.isLengthGreaterThen(0)
  ])(title);export const isAuthorValid = (author: string) =>
  R.allPass([
    validators.isString,
    validators.isLengthGreaterThen(0)
  ])(author);export const ArticleServiceFactory = () => ({
  createArticle,
  updateLikes,
  isTitleValid,
  isAuthorValid
});export const articleService = ArticleServiceFactory();

验证对于保持数据一致性非常重要,特别是在领域级别。我们可以用纯函数来编写 Validators 服务

// @flowexport const isObject = (toValidate: any) => !!(toValidate && typeof toValidate === 'object');export const isString = (toValidate: any) => typeof toValidate === 'string';export const isLengthGreaterThen = (length: number) => (toValidate: string) => toValidate.length > length;

请使用最小的工程来检验这些验证方法,仅用于演示。

事实上,在 JavaScript 中检验一个对象是否为对象并不容易。 :)

现在我们有了领域层的结构!

好在现在就可以使用我们的代码来,而无需考虑框架。

让我们来看一下如何使用 articleService 创建一篇关于我最喜欢的书的文章,并更新它的收藏数。

// @flowimport {articleService} from "../domain/ArticleService";const article = articleService.createArticle({
  title: '12 rules for life',
  author: 'Jordan Peterson'});const incrementedArticle = article ? articleService.updateLikes(article, 4) : null;console.log('article', article);/*
   const itWillPrint = {
     id: "92832a9a-ec55-46d7-a34d-870d50f191df",
     likes: 0,
     title: "12 rules for life",
     author: "Jordan Peterson"
   };
 */console.log('incrementedArticle', incrementedArticle);/*
   const itWillPrintUpdated = {
     id: "92832a9a-ec55-46d7-a34d-870d50f191df",
     likes: 4,
     title: "12 rules for life",
     author: "Jordan Peterson"
   };
 */

存储层

创建和更新文章所产生的数据代表了我们的应用程序的状态。

我们需要一个地方来储存这些数据,而 store 就是最佳人选

状态可以很容易地由一系列文章来建模。

// @flowimport type {Article} from "./Article";

export type ArticleState = Article[];

ArticleState.js

ArticleStoreFactory 实现了发布-订阅模式,并导出 articleStore 作为单例。

store 可保存文章并赋予他们添加、删除和更新的不可变操作。

记住,store 只对文章进行操作。只有 articleService 才能创建或更新它们。

感兴趣的人可以订阅和退订 articleStore。

articleStore 保存所有订阅者的列表,并将每个更改通知到他们。

// @flowimport {update} from "ramda";import type {Article} from "../domain/Article";import type {ArticleState} from "./ArticleState";export type ArticleStore = {
  addArticle(article: Article): void;
  removeArticle(article: Article): void;
  updateArticle(article: Article): void;
  subscribe(subscriber: Function): Function;
  unsubscribe(subscriber: Function): void;
}export const addArticle = (articleState: ArticleState, article: Article) => articleState.concat(article);export const removeArticle = (articleState: ArticleState, article: Article) =>
  articleState.filter((a: Article) => a.id !== article.id);export const updateArticle = (articleState: ArticleState, article: Article) => {  const index = articleState.findIndex((a: Article) => a.id === article.id);  return update(index, article, articleState);
};export const subscribe = (subscribers: Function[], subscriber: Function) =>
  subscribers.concat(subscriber);export const unsubscribe = (subscribers: Function[], subscriber: Function) =>
  subscribers.filter((s: Function) => s !== subscriber);export const notify = (articleState: ArticleState, subscribers: Function[]) =>
  subscribers.forEach((s: Function) => s(articleState));export const ArticleStoreFactory = (() => {  let articleState: ArticleState = Object.freeze([]);  let subscribers: Function[] = Object.freeze([]);  return {    addArticle: (article: Article) => {
      articleState = addArticle(articleState, article);
      notify(articleState, subscribers);
    },    removeArticle: (article: Article) => {
      articleState = removeArticle(articleState, article);
      notify(articleState, subscribers);
    },    updateArticle: (article: Article) => {
      articleState = updateArticle(articleState, article);
      notify(articleState, subscribers);
    },    subscribe: (subscriber: Function) => {
      subscribers = subscribe(subscribers, subscriber);      return subscriber;
    },    unsubscribe: (subscriber: Function) => {
      subscribers = unsubscribe(subscribers, subscriber);
    }
  }
});export const articleStore = ArticleStoreFactory();

ArticleStore.js

我们的 store 实现对于演示的目的是有意义的,它让我们理解背后的概念。在实际运作中,我推荐使用状态管理系统,像 ReduxngrxMobX, 或者是可监控的数据管理系统

好的,现在我们有了领域层和存储层的结构。

让我们为 store 创建两篇文章和两个订阅者,并观察订阅者如何获得更改通知。

// @flowimport type {ArticleState} from "../store/ArticleState";import {articleService} from "../domain/ArticleService";import {articleStore} from "../store/ArticleStore";const article1 = articleService.createArticle({  title: '12 rules for life',  author: 'Jordan Peterson'});const article2 = articleService.createArticle({  title: 'The Subtle Art of Not Giving a F.',  author: 'Mark Manson'});if (article1 && article2) {  const subscriber1 = (articleState: ArticleState) => {    console.log('subscriber1, articleState changed: ', articleState);
  };  const subscriber2 = (articleState: ArticleState) => {    console.log('subscriber2, articleState changed: ', articleState);
  };   articleStore.subscribe(subscriber1);
  articleStore.subscribe(subscriber2);   articleStore.addArticle(article1);
  articleStore.addArticle(article2);   articleStore.unsubscribe(subscriber2);  const likedArticle2 = articleService.updateLikes(article2, 1);
  articleStore.updateArticle(likedArticle2);   articleStore.removeArticle(article1);
}

应用服务层

这一层用于执行与状态流相关的各种操作,如Ajax从服务器或状态镜像中获取数据。

出于某种原因,设计师要求所有作者的名字都是大写的。

我们知道这种要求是比较无厘头的,而且我们并不想因此污化了我们的模块。

于是我们创建了 ArticleUiService 来处理这些特性。这个服务将取用一个状态,就是作者的名字,将其构建到项目中,可返回大写的版本给调用者。

// @flowexport const displayAuthor = (author: string) => author.toUpperCase();

让我们看一个如何使用这个服务的演示!

// @flowimport {articleService} from "../domain/ArticleService";import * as articleUiService from "../services/ArticleUiService";

const article = articleService.createArticle({  title: '12 rules for life',  author: 'Jordan Peterson'});

const authorName = article ?
  articleUiService.displayAuthor(article.author) :  null; console.log(authorName);// 将输出 JORDAN PETERSONif (article) {
  console.log(article.author);  // 将输出 Jordan Peterson}

app-service-demo.js

免费体验云安全(易盾)内容安全、验证码等服务

更多网易技术、产品、运营经验分享请点击

相关文章:
【推荐】 Docker容器的自动化监控实现
【推荐】 Innodb实践总结(一)
【推荐】 Android View部分消失效果实现

[译] 关于 SPA,你需要掌握的 4 层 (1)的更多相关文章

  1. [译] 关于 SPA,你需要掌握的 4 层 (2)

    此文已由作者张威授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 视图层 现在我们有了一个可执行且不依赖于框架的应用程序,React 已经准备投入使用. 视图层由 presen ...

  2. 【译】为什么BERT有3个嵌入层,它们都是如何实现的

    目录 引言 概览 Token Embeddings 作用 实现 Segment Embeddings 作用 实现 Position Embeddings 作用 实现 合成表示 结论 参考文献 本文翻译 ...

  3. 有道词典中的OCR功能:第三方库的变化

    之前有点好奇有道词典中的OCR功能,具体来说就是强力取词功能.我知道的最有名的OCR库是tesseract,这个库是惠普在早些年前开源的. 在用python做爬虫处理验证码的时候,就会用到这个库,对应 ...

  4. 第一章:关于Ehcache

    PDF官方文档:http://www.ehcache.org/generated/2.10.4/pdf/About_Ehcache.pdf 1.什么是Ehcache? Ehcache是一种开源的基于标 ...

  5. Ehcache概念篇

    前言 缓存用于提供性能和减轻数据库负荷,本文在缓存概念的基础上对Ehcache进行叙述,经实践发现3.x版本高可用性不好实现,所以我采用2.x版本. Ehcache是开源.基于缓存标准.基于java的 ...

  6. 《Effective Java》第9章 异常

    第58条:对可恢复的情况使用受检异常,对编程错误使用运行时异常 Java程序设计语言提供了三种可抛出结构(throwable) ;受检的异常(checked exception)运行时异常(run-t ...

  7. Java 异常规范

    1. 只针对异常情况使用异常,不要用异常来控制流程 try { int i = 0; while (true) { range[i++].doSomething(); } } catch (Array ...

  8. Effective.Java第67-77条(异常相关)

    67.  明智审慎地进行优化 有三条优化的格言是每个人都应该知道的: (1)比起其他任何单一的原因(包括盲目的愚钝),很多计算上的过失都被归咎于效率(不一定能实现) (2)不要去计算效率上的一些小小的 ...

  9. [译]ABP框架使用AngularJs,ASP.NET MVC,Web API和EntityFramework构建N层架构的SPA应用程序

    本文转自:http://www.skcode.cn/archives/281 本文演示ABP框架如何使用AngularJs,ASP.NET MVC,Web API 和EntityFramework构建 ...

随机推荐

  1. 装饰器1、无参数的装饰器 2、有参数的装饰器 3、装饰器本身带参数的以及如果函数带return结果的情况

     装饰器分成三种: 1.无参数的: 2.有参数的: 3.装饰器本身带参数的. 装饰器decorator又叫语法糖 定义:本质是函数,器就是函数的意思.装饰其他函数.就是为其他函数添加附加功能. 原则: ...

  2. mysql整数类型

    数值类型 1.整数类型 整型类型的后面的宽度,不是存储宽度,是显示宽度,不够位数用0添加,够位数使用原数据 整数类型:TINYINT SMALLINT MEDIUMINT INT BIGINT 作用: ...

  3. Delphi IOS 蓝牙锁屏后台运行

    Delphi IOS 后台运行 同样的程序,编译成android,锁屏后继续运行正常,蓝牙通讯正常,但在IOS下锁屏后程序的蓝牙就中断通讯了? IOS的机制就是这样,锁屏就关闭了. 音乐播放器是怎么做 ...

  4. PHP框架 Laravel

    PHP框架 CI(CodeIgniter) http://www.codeigniter.com/ http://codeigniter.org.cn/ Laravel PHP Laravel htt ...

  5. 10-EasyNetQ之控制队列名称

    EasyNetQ默认行为,当生成队列的名称时,使用消息类型名+subscription Id.例如:PartyInvitation 这个消息类型,命名空间为 EasyNetQ.Tests.Integr ...

  6. Objective-C入门 简介Cocoa框架

    Cocoa Framework简称Cocoa,它是Mac OS X上的快速应用程序开发(RAD, Rapid Application Development)框架,一个高度面向对象的(Object O ...

  7. Django模板语言之组合搜索

    url.py from django.conf.urls import url from django.contrib import admin from app01 import views url ...

  8. centos7使用frabric自动化部署LNMP

    1.创建lnmp.py文件 $ vim lnmp.py ------------------------> #!/usr/bin/env python from fabric.colors im ...

  9. quartz在web.xml的配置

    第一步:下载所需的Jar包 commons-beanutils.ja.commons-collections.jar.commons-logging.jar.commons-digester.jar. ...

  10. Python3.7安装PyQt5的方法

    一.系统环境 操作系统:Win7 64位 Python Version:3.7 二.安装参考 方法1:pip install PyQt5 方法2:下载whl安装包安装 a.下载网址:https://p ...