如何使用Meteor、TypeScript、React和外部集成构建生产应用程序
在半小时内使用Meteor 3.0和ChatGPT构建一个功能齐全的博客平台。
流星3.0的闪电般快速Web应用开发书籍:第1章
您将学习: Meteor架构;界面-模式-智能方法层次结构;使用(类似)MVC方法的简单多用户博客平台的架构;实时博客平台应用的第一个版本;使用ChatGPT来处理枯燥的编码任务。
本文概述:
- 介绍
- 流星架构
- 安装和博客应用介绍
- 模型:具有集合的数据层
- 控制器:Meteor.Methods
- 模式、验证、最小化样板代码
- 更好的控制器:已验证的方法
- 用户,认证,授权
- 终审控制器:保护POST业务方法
- 发布/订阅:进入视图
- 查看:未样式化的React和React Router
- 最终视图:客户端的PubSub和反应性
应用程序完整源代码可在此处获取:
介绍
我从一开始就参与了 Meteor 项目 - 当时他们已经拥有“发布/订阅”功能,但还没有身份验证或授权。我在 Veeam Software 中负责所有运营和业务基础架构团队,以及独立平台 - 例如 innmind.com 和 anasaea.com,以及我们现在正在开发的平台。我们在构建这些平台的同时学到了很多东西,因为我喜欢分享我的知识,而且 Meteor 3.0 的重要版本即将推出,所以我决定开始另一系列文章,最终成为一本书。
如果您对稍微不同寻常的方法学习类型理论和 Haskell 感兴趣,我的另一本(即将完成)书籍已经在这里了。
在本系列课程中,我们将学习并了解:
- 为什么Meteor非常酷和它的架构
- 设计强大的数据驱动型应用程序的最佳实践
- 如何通过使用库、(轻量级)代码生成器和ChatGPT来最小化样板代码 (是的,它可以很有帮助)。
- 如何使用实用、务实的授权方法
- 如何构建与企业系统如Microsoft Azure或Google集成的独户和多租户应用程序。
- 如何通过OpenAI的API利用OpenAI
- 如何通过Stripe处理销售和订阅。
- 如何部署和监控生产应用
在阅读完本书后,你应该能够在最短的时间内掌握构建几乎任何类型的实用数据驱动的 web 应用程序的能力。本书假定读者对 TypeScript 和 React 有一定的了解 — 我们不会介绍这些语言和编程的基础知识。
由于我们的重点是实用性,因此我们不会进行过度扩展,而是会深入探讨所选技术栈。
- 流星(尽可能使用3.0 - 在撰写本文时尚未发布,但我们需要做好准备)
- React 和 react-bootstrap 用于前端。
- Typescript作为语言(因为JavaScript仍然是php之后宇宙中最差的语言)
- 选择了一些有用的外部集成:Microsoft Azure、OpenAI、Stripe,以及可能根据需求包括 Google 和 Facebook Graph。
为什么选择 Meteor 及 Meteor 架构。
你可以阅读无数的意见和比较,将Meteor与其他技术相比较。对于我们而言,使用Meteor开发了大约10个相当大规模的项目后,它的美妙和便利在于:
- 编写一次,多端运行(几乎可以)— 在客户端和服务器端使用相同的API
- 响应性(在 React 之前就已经被发明了)自然融入了前端的 React。
- 绝对无缝升级,无需麻烦数据库迁移。
- 极快的上市时间,从想法到即可使用的解决方案,而不会妥协性能(与无数低代码/无代码解决方案不同)。
- 也许对于喜欢微服务和分布式架构的人来说最重要的是:Meteor将作为“核心应用程序”忠实地为您服务,处理用户管理、数据实体以实现“单一真相源”等,同时您仍然可以集成任意数量的外部API,无论是微服务还是其他类型的API。
- 非常好的文档和包生态系统
现在,让我们来深入了解一些关键的架构细节。为了尽可能地从一开始就实践,我们将从构建一个简单的多用户博客平台开始本系列,同时途中学习关键的Meteor概念,这些概念将在我们进入更复杂的应用程序开发时变得非常有用。我们将基于三个关键的架构构建块来构建美丽、稳健、可伸缩的应用程序:
- 藏品
- 发布/订阅
- 方法
他们将与客户端的React结合使用,以为任何复杂度的应用程序提供一个漂亮的MVC结构:
博客平台:Type Foundation
我们将采用数据驱动的类型化方法,这意味着我们必须从定义需要的持久化数据类型开始任何项目。其余所有内容都将围绕它们展开。对于博客平台,我们将有以下内容:
- 用户:表示是系统的用户,可以创作帖子或者评论。
- 帖子:表示博客文章或文章。
- 评论:代表对博客文章的评论。
从功能方面来看,我们将需要提供对典型博客平台功能的支持:
- 在系统中注册
- 编写和编辑博客文章
- 阅读并订阅其他用户
- 撰写并编辑博客文章的评论
让我们看看我们能多快地建造它 :)
如果你还没有安装Meteor,请安装。或者简单地运行:
npm install -g meteor
在撰写本文时,Meteor 3.0 尚未发布,因此我们将使用 2.12 版本构建博客应用程序,但将在未来的文章中升级至 Meteor 3.0。关键概念保持不变,Meteor 3.0 的主要区别在于使用异步和 promise 的标准 JavaScript 支持,而不是 Meteor 2 及以下版本中使用的 fibers 包。
创建我们的博客应用程序,请运行:
meteor create --typescript blogplatform
这将使用Typescript和React创建一个基本的应用程序。进入imports / api文件夹并删除生成的links.ts文件。然后进入imports / ui并删除Hello.tsx和Info.tsx,然后打开App.tsx并进行一些编辑,使其大致如下:
import React from 'react';
export const App = () => (
<div>
<h1>Welcome to Meteor!</h1>
</div>
);
最后,前往 server/main.ts 并删除除了 meteor 导入语句以外的所有内容,确保它看起来像这样:
import { Meteor } from 'meteor/meteor';
现在,如果在项目文件夹中执行meteor并导航到localhost:3000,您应该可以看到“欢迎来到Meteor”标题。虽然没有什么激动人心的,但我们已经设置好了环境,从现在开始会变得很棒:)
带有集合的数据层:模型
流星是数据驱动的。经过多年类型理论和 Haskell 研究和开发的探讨,我相信通过设计正确的类型,我们可以自动化大部分乏味的开发任务,以此快速地开发出各种商业应用,并使维护变得轻而易举。在实践中,Meteor 与 TypeScript 更加接近这个目标,甚至可以说是比其他大多数框架都更接近。
它使用Mongo Collections进行持久化数据存储,通过发布-订阅机制和客户端的mini-mongo包,在服务器和客户端都提供几乎相同的API。让我们为上文提到的帖子和评论类型定义数据层。我们稍后会讨论用户,因为Meteor已经提供了它。
数据层的最佳实践是:先定义一个TypeScript接口,然后定义一个相应的架构,再基于这两者创建一个Meteor集合。
以上内容将帮助您避免以后或与“纯”JavaScript相比所遇到的许多麻烦,您甚至无法想象。让我们从Post数据类型开始。在api文件夹内创建Post文件夹(这是Meteor约定),创建Post.ts文件并定义我们的接口:
import { Mongo } from "meteor/mongo";
export interface IPost {
_id?: string,
title: string,
content: string,
createdAt: Date,
updatedAt: Date,
authorId: string
}
一切都相当自我解释。_id是Mongo为每个持久化文档使用的ID(在未来的章节中,我们将抽象掉很多这些内容),authorId是指创建此帖子的用户的引用。我们使用export关键字,以便在代码的任何地方都可以使用此接口。如果您使用的是VS Code或类似的IDE,则typescript语法检查器将通过输入大大帮助您捕捉愚蠢的错误。
一旦我们定义好界面,我们就可以立即创建一个集合来容纳我们的帖子:
export const CollectionPosts = new Mongo.Collection<IPost>("posts");
将我们的接口传递给构造函数将在以后让我们的生活更轻松-所有集合方法将自动知道它们只能使用IPost接口类型进行操作。“ posts”的名称将是数据库中实际的Mongo集合名称。我们正在导出它以便能够在我们的代码中任何其他地方使用-无论是在服务器上还是客户端上。您很快就会感受到这一点的好处。
现在,Meteor Collections拥有广泛的API,但由于我们正在尝试使用适当的、可扩展的结构和职责分离构建应用程序,因此让我们从一开始就考虑业务逻辑,而不是低级别的集合操作。我们至少需要能够创建新帖子并编辑现有帖子——让我们定义这些内容!
你会如何在“普通”网络应用程序中操纵数据库?通常是通过一些不同程度的丑陋的 REST-ful api。幸运的是,Meteor在底层基于WebSocket协议构建,支持与服务器的实时双向通信,从而提供了一种更优雅的解决方案。
流星方法:控制器
Meteor方法是调用服务器方法并通过DDP协议从客户端接收结果的机制。DDP是由Meteor发明的基于websocket的协议,我们将在稍后的时间深入介绍其细节,因为从高级应用程序开发人员的角度来看,没有必要直接接触DDP。
我们需要创建两种方法——添加新帖子和编辑现有帖子。以下是如何做到非常简单:
Meteor.methods({
"posts.addNewPost"(p:IPost) {
p.createdAt = new Date()
p.updatedAt = p.createdAt
return CollectionPosts.insert(p);
},
"posts.editPost"(id:string, title:string, content:string) {
return CollectionPosts.update({_id:id},
{
$set: {
title: title,
content: content,
updatedAt: new Date()
}
})
}
})
上面的代码定义了两种方法,其中一种将新文章插入到我们的集合中,而第二种方法给定现有文章ID、新标题和内容,然后使用标准mongo选择器和修改器更新文章。我们还在服务器上设置了创建和更新日期。
一旦这些方法被定义,你可以在服务器和客户端上同时使用相同的API进行调用!
Meteor.callAsync("posts.addNewPost", post)
// or, more extensively and without the callback hell of life before:
const res = await Meteor.callAsync("posts.addNewPost", post)
// if you need to catch errors either use try / catch, or chained catch(callback)
我们使用callAsync,因为这将成为Meteor 3.0未来的发展方向。我们将更详细地解释如何使用这种新方法来处理错误等。
我们稍后会大大改进这个基本解决方案,但现在您可以通过进行以下更改来检查它是否有效-将以下行添加到server/main.ts中:
import "/imports/api/Post/Post"
使用meteor命令再次启动您的应用程序,导航到localhost:3000,然后在开发控制台中输入类似以下的内容:
const res = await Meteor.callAsync("posts.addNewPost",
{title:"new post", content: {"hello world"})
你应该在res变量中收到新创建的帖子的id,并且为了确保其已经插入到本地数据库中,可以在应用程序运行时,打开另一个在应用文件夹中的终端窗口,并运行meteor mongo命令。你应该会得到一个mongo shell,通过运行db.posts.find({}),你应该可以看到新插入的帖子。它成功了!
格式、验证与最小化模板代码
这已经比调用 REST API 更好了,但是我们在这种方法中缺少一些强大应用的关键部分。我们想在运行时验证从客户端发送到我们方法中的任何内容,并且我们希望编写 await Meteor.callAsync 每次都能得到更好的 API。当然,我们可以在每个方法中显式编写验证代码,但这太多余了。我们需要的是基于我们已经定义的 IPost 接口的验证模式。
由于我们的目标是尽可能地实用,因此我们将使用优秀的验证包simpl-schema。请使用以下命令将其安装到您的项目中:
meteor npm install --save simpl-schema
现在我们可以在 Post.ts 文件中根据我们的 IPost 接口定义一个 SPost 模式:
import SimpleSchema from "simpl-schema";
// ...
export const SPost = new SimpleSchema({
_id: {
type: String,
optional: true,
},
title: {
type: String,
},
content: {
type: String,
},
createdAt: {
type: Date,
},
updatedAt: {
type: Date,
},
authorId: {
type: String,
},
});
现在,这一点很重要。我们的IPost接口已经定义了此模式。不幸的是,我们不得不手动编写实际的SimplSchema定义,以便在运行时使用。在这本书的后面,我们将创建一个简单的代码生成器来自动化这个任务-样板是不好的,不好-但在此期间,您可以使用类似以下请求的ChatGPT:
create a simpl-schema definition based on the following interface:
interface IPost {
_id?: string,
title: string,
content: string,
createdAt: Date,
updatedAt: Date,
authorId: string
}
Produce code only, no explanation
工作像魔法一样,即使是更复杂的界面(包括数组、联合类型等)都可以。
当我们进入UI部分时,我们将欣赏到拥有模式的全部功能(验证表格可能是您面临的最乏味和无聊的任务),但即使在服务器上,它也非常有用 - 运行一个
SPost.validate(post)
如果您的帖子对象在运行时与IPost接口不对应,则会有一堆格式良好的错误消息。SimplSchema有非常完善的文档,我们会在进程中了解它的不同方面。
现在,既然我们有了一个接口、对应的模式、一个集合和一种构建方法的方式,让我们为我们的帖子创建一个漂亮而不易出错的业务逻辑接口!
验证的方法:更好的业务逻辑控制器
使用Meteor Collection API,无论是在客户端还是服务器端,您已经可以创建功能齐全的Web应用程序,但它们可能会有一些错误并且相当复杂。因为我们想要一个可扩展且易于维护的MVC架构,需要使用另一个非常方便的包ValidatedMethod(或更精确地说是mdg:validated-method)来创建适当的业务逻辑/控制器层。它是一个Meteor包,而不是npm包 - 这一点很重要。虽然其安装与npm的安装相同,只需在应用程序文件夹中运行以下命令:meteor add mdg:validated-method,就能完成安装。
ValidatedMethod是一个方便的包装器,它在Meteor.methods和Meteor.call的内部隐藏机制,并在其上面添加了灵活的验证选项。让我们将我们已经定义的两个业务逻辑方法--addNewPost和editPost--打包起来,使它们成为“控制器”对象的一部分:
export const PostController = {
// add a new post
addNewPost: new ValidatedMethod({
name: "posts.addNewPost",
validate: SPost.validator(),
run(p:IPost) {
p.createdAt = new Date()
p.updatedAt = p.createdAt
return CollectionPosts.insert(p);
}
}),
// edit existing post
editPost: new ValidatedMethod({
name: "posts.editPost",
validate: null,
run(props: {id:string, title:string, content:string}) {
const {id, title, content} = props
return CollectionPosts.update({_id:id},
{
$set: {
title: title,
content: content,
updatedAt: new Date()
}
})
}
})
}
正如您所见,这只是从我们之前定义的Meteor.methods调用中进行的简单更改。方法名称进入ValidatedMethod的name属性,我们要执行的实际代码进入run定义,而validate定义了验证函数。我们可以手写它,但由于我们为我们的IPost接口 - SPost定义了模式,事实证明它已经具有内置的验证器函数,这节省了我们很多努力。
为了调用这些方法,我们可以简单地运行PostController.addNewPost(p),这同样适用于服务器和客户端!Meteor在幕后处理一切——在客户端上运行存根,抛出客户端错误,省去了向服务器发送请求的回合,如果一切顺利,会在服务器上正常运行代码并返回结果。一旦我们到达UI部分,我们将看到关于这个的实际用法。
对于 editPost 代码,我们不提供验证程序,因为我们只使用 id、title 和 content 调用它,而不是 IPost 的所有字段。然而,这两种方法显然都非常原始,缺乏非常重要的功能——拥有它们的用户。我们需要定义其他身份验证检查,以便只有已登录的用户可以创建新的帖子,而且只有帖子的所有者才能编辑它。幸运的是,Meteor 提供了一个非常简单的机制来实现这一点。
让我们看一下,然后为评论实体定义额外的逻辑,接着使用react和react-bootstrap编写快速前端。
用户,身份验证,授权
请再次记住,这本书的目的是尽可能实用。在此情况下,我们不会探讨解决用户管理和身份验证问题的各种不同选项,而是会感激地使用Meteor为我们提供的工具。
即,有一个内置的集合来处理用户:Meteor.users
我们将把它作为我们后续各种高级用户管理方法的基础,现在,只需要知道它存在以及通过密码进行身份验证的方法以及最终使用外部系统(如Facebook或Google)通过OAuth进行身份验证就足够了。
为了开始使用它,我们需要添加另一个Meteor包:meteor add accounts-password。
这将使我们能够注册和登录用户,以及许多有用的方法来处理授权:
// id of the currently logged in user:
Meteor.userId()
// User collection object for the currently logged in user:
Meteor.user()
您可以在服务器和客户端上都调用它们,如果没有用户登录,它们将返回null。
由于我们也在使用TypeScript并想要扩展一个已经存在的用户对象,我们不能为其定义一个新的接口,而是必须扩展现有的接口。创建一个imports/api/User文件夹和其中的User.ts文件,将以下代码放入其中:
declare module "meteor/meteor" {
namespace Meteor {
interface User {
/**
* Extending the User type
*/
firstName: string,
lastName: string
}
}
}
目前,我们只是为已有的字段添加了名字和姓氏,这可以在下面的示例对象中看到:
{
_id: 'QwkSmTCZiw5KDx3L6', // Meteor.userId()
username: 'cool_kid_13', // Unique name
emails: [
// Each email address can only belong to one user.
{ address: 'cool@example.com', verified: true },
{ address: 'another@different.com', verified: false }
],
createdAt: new Date('Wed Aug 21 2013 15:16:52 GMT-0700 (PDT)'),
profile: {
// The profile is writable by the user by default.
name: 'Joe Schmoe'
},
services: {
facebook: {
id: '709050', // Facebook ID
accessToken: 'AAACCgdX7G2...AbV9AZDZD'
},
resume: {
loginTokens: [
{ token: '97e8c205-c7e4-47c9-9bea-8e2ccc0694cd',
when: 1349761684048 }
]
}
}
}
有一个用户对象上的“默认可写”属性,但由于安全原因,不建议使用它,所以我们将忽略它 - 我们将在顶层添加所有必要的字段。
从业务逻辑上来说,我们需要注册新用户和登录已有用户的方法。对于后者,我们将使用标准API,对于前者,我们可能会考虑创建一个类似于上面定义的PostController的UserController,但实际上,在这种情况下,最好还是与Meteor标准API一起使用onCreateUser回调来适当设置名称。原因是Meteor标准用户认证方法会在客户端自动加密密码,以提高安全性 - 我们绝对希望利用这一点。
将我们的User.ts文件按以下方式扩展:
import {Accounts} from 'meteor/accounts-base'
// ...
Accounts.onCreateUser((options, user) => {
const customizedUser = Object.assign({
firstName: options.firstName,
lastName: options.lastName
}, user);
return customizedUser;
})
然后在客户端上,我们只需调用Accounts.createUser({email: "...", password: "...", firstName: "...", lastName: "..."}),一切都会被照料好。当然,由于选项对象来自客户端,您可能还想额外检查firstName和lastName是否是合理的字符串,而不是json格式的完整维基百科。
现在我们知道如何与用户进行一些基本工作,让我们调整我们的Post方法以正确检查用户所有权,并定义评论功能以完成我们的后端。
终极控制器:保护帖子业务逻辑
返回Post.ts,并让我们修复我们的控制器方法:
addNewPost: new ValidatedMethod({
name: "posts.addNewPost",
validate: SPost.validator(),
run(p:IPost) {
const uid = Meteor.userId()
if (!uid) {
throw new Meteor.Error("not-authorized",
"only logged in users can create new posts")
}
p.createdAt = new Date()
p.updatedAt = p.createdAt
p.authorId = uid
return CollectionPosts.insert(p);
}
}),
我们已经添加了对当前登录用户的检查(调用Meteor.userId()来获取当前在服务器和客户端上始终返回当前登录用户的ID),并在用户未登录时抛出异常。将此类检查放在验证方法中而不是实际的业务逻辑调用中是一个风格问题,我们认为它实际上属于validate方法,但为了简单起见,现在让我们将其保留在主要运行代码中。我们在服务器上设置authorId属性为正确的userId(因为我们不信任客户端)。
为了文章编辑,我们需要进行一些额外的检查:
editPost: new ValidatedMethod({
name: "posts.editPost",
validate: null,
run(props: {id:string, title:string, content:string}) {
const {id, title, content} = props
// is the user logged in check?
const uid = Meteor.userId()
if (!uid) {
throw new Meteor.Error("not-authorized",
"only logged in users can create new posts")
}
// is the user owner of the post check?
const post = CollectionPosts.findOne({_id: id});
if (!post || (post?.authorId != uid)) {
throw new Meteor.Error("not-authorized",
"only owners may edit posts!")
}
return CollectionPosts.update({_id:id},
{
$set: {
title: title,
content: content,
updatedAt: new Date()
}
})
}
})
首先,我们进行“用户是否已登录”检查。然后,我们尝试获取从客户端传递的帖子id,并检查是否存在这样的帖子,然后检查帖子的所有者是否与当前登录的用户相同。如果不是,则抛出另一个异常。
就这样,我们已经定义了以下业务逻辑函数,并且它们足以让我们创建一个可工作的博客平台。
- 创建新用户
- 登录用户
- 创建新帖子
- 编辑帖子
在我们开始用React和Bootstrap创建界面之前,我们的应用还缺少一个至关重要的部分-确实,我们有这四个业务逻辑方法,但我们如何实际查看平台上现有的帖子呢?我们应该创建另一种方法来搜索所需的帖子,并通过请求将它们发送到客户端吗?
当然,我们可以这样做,但Meteor在这方面有更好的解决方案 —— 一种响应式发布/订阅机制。如果我们通过方法实现这个功能,那么每次想要更新时都需要在客户端显式地调用这些方法,这是很丑的。Meteor发布/订阅通过将最新和最棒的数据始终推送到客户端来解决了这个问题,包括在其他地方进行的任何更新!
让我们加上这个缺失的部分。
发布和订阅
流星pubsub机制允许您在服务器和客户端上使用相同的Mongo Collection api,同时确保客户端始终具有所需的数据并具有响应性。这是一个非常强大和多样化的机制,我们将在本书的进一步部分中查看其不同的高级方面,但是现在,让我们从可能的最简单的案例开始:将所有帖子发布到客户端。显然,这对于大型生产应用程序来说是一个不好的解决方案-因此我们将在接下来的章节中引入分页,但我们希望快速达到某些工作,因此让我们进行迭代。
创建一个api / Post / publications.ts文件,将以下代码放入其中:
import { Meteor } from "meteor/meteor";
import { CollectionPosts } from "./Post";
Meteor.publish("posts.allPosts",
()=> CollectionPosts.find({}))
那就是这样!我们在这里定义了一个名为“posts.allPosts”的新“刊物”,它返回一个反应式光标,其中包含来自我们收集的所有帖子。然后在客户端上,我们可以调用Meteor.subscribe(“posts.allPosts”),神奇地,我们将通过客户端上的同一个CollectionPosts对象访问所有帖子!惊人,不是吗?
再次提到,这是使用Meteor pub-sub的最基本的方式,但是我们将深入探讨其强大的功能。
现在,最后,让我们构建我们的用户界面。
未定义样式的React:路由和主组件
让我们先从未样式的 React 开始,将前端与后端连接起来,并在我们手中得到一个功能齐全的应用程序。我们将在下一节中添加 Bootstrap 和一些更漂亮的样式。
我们需要以下前端页面/组件:
- 用户注册
- 用户登录
- 查看所有帖子
- 创建新帖子
- 编辑现有文章
我们将使用 react-router v6 来处理我们应用程序中的路由,因此让我们立即添加它:npm add --save react-router react-router-dom
咱们还得添加Bootstrap和react-bootstrap:
npm 添加 --保存 react-bootstrap bootstrap
我们稍后将查看如何将Bootstrap样式与SCSS编译器集成,现在我们将只使用未设置样式的React-Bootstrap组件,并在下一次会话中美化它们。是的,有许多不同的方法可以为Meteor应用程序创建前端,但React仍然是最受欢迎的前端JavaScript框架,所以让我们跟随它。
由于这是我们学习基本原则的第一个应用程序,我们将采取一些简短快捷的方式来设计我们的组件,以节省时间。让我们从注册和登录开始。进入imports/ui文件夹,在那里创建views子文件夹,并在其中创建一个LoginRegistration.tsx文件。
现在,就我个人而言,我绝对讨厌编写React表单代码。这是单调乏味、无聊、毫无用处的样板文件。如果你喜欢手工完成它,那么请随便,但如果你像我一样,幸运的是,我们有ChatGPT可以完美地完成这个任务。使用类似这样的提示:
Write a typescript code for React dynamic form with validation
and using react-bootstrap components that contains two fields:
login and password. Produce code only, no explanation.
它会为您创建精美格式化的代码,提高您的生产力,具备状态追踪、验证等功能。请随意编写更好的验证程序或进行其他更改。在下一步中,我们将调整此表单以处理注册和登录,并将其连接到 Meteor 后端。
import React, { useState } from 'react';
import { Button, Form } from 'react-bootstrap';
type FormData = {
login: string;
password: string;
};
export const LoginRegistration: React.FC = () => {
const [login, setLogin] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState<FormData>({ login: '', password: '' });
const validateForm = () => {
let formErrors = { login: '', password: '' };
if (!login) formErrors.login = "Login is required";
if (!password || password.length < 6) formErrors.password = "Password must have at least 6 characters";
setErrors(formErrors);
// return true if no errors
return Object.values(formErrors).every(x => !x);
};
const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
if (validateForm()) {
console.log({ login, password });
}
};
return (
<Form onSubmit={handleSubmit}>
<Form.Group controlId="formLogin">
<Form.Label>Login:</Form.Label>
<Form.Control
type="text"
value={login}
isInvalid={!!errors.login}
onChange={(e) => setLogin(e.target.value)}
/>
<Form.Control.Feedback type='invalid'>
{errors.login}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="formPassword">
<Form.Label>Password:</Form.Label>
<Form.Control
type="password"
value={password}
isInvalid={!!errors.password}
onChange={(e) => setPassword(e.target.value)}
/>
<Form.Control.Feedback type='invalid'>
{errors.password}
</Form.Control.Feedback>
</Form.Group>
<Button variant="primary" type="submit">
Submit
</Button>
</Form>
);
}
在我们进行这些更改之前,让我们设置基本的路由以便与UI一起使用。将路由定义分开是最佳实践,但我们正在采取捷径,因此将在client/main.tsx文件中定义它们。将其内容更改为以下内容:
import React from 'react';
import { createRoot } from 'react-dom/client';
import { Meteor } from 'meteor/meteor';
import {
createBrowserRouter,
RouterProvider,
} from "react-router-dom";
import LoginRegistration from '/imports/ui/Views/LoginRegistration';
const router = createBrowserRouter([
{
path: "login",
element: <LoginRegistration />,
}
])
Meteor.startup(() => {
const container = document.getElementById('react-target');
const root = createRoot(container!);
root.render(<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>);
});
在这里定义一个单一路由,用于显示我们的登录注册组件以测试一切是否正常。我们还稍微调整了主要呈现代码,以确保我们在将来支持高级路由。最后,在服务器的 main.ts 文件中调整导入我们的用户逻辑和发布自帖子的信息。
import { Meteor } from 'meteor/meteor';
import "/imports/api/Post/Post"
import "/imports/api/Post/publications"
import "/imports/api/User/User"
现在在你的应用文件夹中运行Meteor(你可以让它保持运行状态,它会自动重新加载你做出的任何更改)。导航到localhost:3000/login,你应该会看到类似下面这样的内容:
耶!点击“提交”以检查验证是否有效:
不错。我的意思是,它很丑,但是我们会逐步改进-但是它起作用了!现在,让我们调整这个表单,使其处理登录和注册功能(记住,这在真实应用程序中必须是两个不同的表单,因为很明显)。
将表单按钮更改为这些按钮,同时编辑表单元素以停止处理提交事件:
<Form>
{/* ... */}
<Button variant="primary" onClick={()=>handleSubmit(true)}>
Register
</Button>
<Button variant="primary" onClick={()=>handleSubmit(false)}>
Login
</Button>
我们还将调整handleSubmit函数,以带有一个可区分注册或登录的单个布尔参数。现在让我们将Meteor后端连接到它:
const handleSubmit = (registration: boolean) => {
if (validateForm()) {
//console.log({ login, password, registration });
if (registration) {
Accounts.createUser({
email: login,
username: login,
password: password
}, (err)=> {
console.log(err)
})
}
else {
Meteor.loginWithPassword(login,password,(err)=> {
console.log(err);
})
}
}
};
非常直接,如果是注册 - 我们调用createUser,如果是登录,我们登录用户。
练习:调整代码以向用户询问其名字和姓氏,并根据我们之前编写的代码将其传递到服务器。
让我们通过调整React渲染代码,为已登录用户添加欢迎信息,以确保其可用。
<>
{Meteor.userId() &&
<h3>Welcome, {Meteor.user()?.username}</h3>}
<Form>
{/* ... */}
</Form>
</>
现在注册一些用户,然后点击登录。你应该会看到类似以下的内容:
很好,它有效!
练习:为React组件添加良好的错误处理消息。
现在,让我们让已登录的用户有能力创建博客文章(我们稍后会添加编辑功能)。我们需要另一个react-bootstrap表单,其中有两个字段:标题和内容。去问ChatGPT或自己写,你应该会得到像下面这样的东西,将其放到Views / EditPost.tsx文件中:
import React, { useState } from 'react';
import { Button, Form } from 'react-bootstrap';
type FormData = {
title: string;
content: string;
};
export const EditPost: React.FC = () => {
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [errors, setErrors] = useState<FormData>({ title: '', content: '' });
const validateForm = () => {
let formErrors = { title: '', content: '' };
if (!title) formErrors.title = "Title is required";
if (!content) formErrors.content = "Content is required";
setErrors(formErrors);
return Object.values(formErrors).every(x => !x);
};
const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
if (validateForm()) {
console.log({ title, content });
}
};
return (
<Form onSubmit={handleSubmit}>
<Form.Group controlId="formTitle">
<Form.Label>Title:</Form.Label>
<Form.Control
type="text"
value={title}
isInvalid={!!errors.title}
onChange={(e) => setTitle(e.target.value)}
/>
<Form.Control.Feedback type='invalid'>
{errors.title}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="formContent">
<Form.Label>Content:</Form.Label>
<Form.Control
as="textarea"
value={content}
isInvalid={!!errors.content}
onChange={(e) => setContent(e.target.value)}
/>
<Form.Control.Feedback type='invalid'>
{errors.content}
</Form.Control.Feedback>
</Form.Group>
<Button variant="primary" type="submit">
Submit
</Button>
</Form>
);
}
让我们也在 main.tsx 中添加一个路由:
import {LoginRegistration} from '/imports/ui/Views/LoginRegistration';
import { EditPost } from '/imports/ui/Views/EditPost';
const router = createBrowserRouter([
{
path: "login",
element: <LoginRegistration />,
},
{
path: "editpost",
element: <EditPost />,
}
])
现在,如果您导航到localhost:3000/editpost,您应该会看到您的帖子表单:
现在让我们通过更改handleSubmit函数来使用PostController API,确保它实际上创建了一个新的帖子:
import { PostController } from '/imports/api/Post/Post';
// ...
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
if (validateForm()) {
console.log({ title, content });
const pid = await PostController.addNewPost.call({
title: title,
content: content,
createdAt: new Date,
updatedAt: new Date,
authorId: Meteor.userId()
})
console.log("New post id:", pid)
}
};
转到localhost:3000/editpost并创建您的第一篇文章:
你应该能在控制台中看到它的ID。为了确保它已被创建,在另一个终端选项卡中运行meteor mongo,同时在第一个选项卡中运行meteor,然后输入db.posts.find() - 你应该能看到类似这样的内容:
太好了,现在让我们再创建几篇文章,确保我们可以在单独的组件中实际显示它们给读者。
最终视角:客户端的PubSub和反应性
现在我们数据库中有几篇文章了,让我们创建一个主页面来动态显示所有文章。这个部分很重要,因为我们将学习如何在最小化重新渲染的同时与Meteor发布关联。为此,我们将使用另一个小巧方便的包——react-meteor-data。它应该已经在您的应用程序中了,因为我们使用了--typescript标记来创建它,这也自动安装了React和与之配合使用的关键包。所以让我们继续。
创建ui/MainPage.tsx文件,并使用以下代码进行分析。
import React from 'react'
import { useSubscribe, useFind } from "meteor/react-meteor-data";
import { CollectionPosts, IPost } from '../api/Post/Post';
import Container from 'react-bootstrap/esm/Container';
import Row from 'react-bootstrap/esm/Row';
import Col from 'react-bootstrap/esm/Col';
export const MainPage = () => {
const loading = useSubscribe("posts.allPosts")
const posts = useFind(()=> CollectionPosts.find({}), [])
return (
<Container>
<Row>
<Col>
<h3>Welcome to our Blogs!</h3>
</Col>
</Row>
{posts.map((p:IPost,i:number)=> {
return (
<Row key={i}>
<Col>
<h4>{p.title}</h4>
<p className="text-muted">
{p.createdAt.toString()}
</p>
<p>
{p.content}
</p>
</Col>
</Row>
)
})}
</Container>
)
}
同时不要忘记在 main.tsx 中更新路由表-
import { MainPage } from '/imports/ui/MainPage';
const router = createBrowserRouter([
{
path: "login",
element: <LoginRegistration />,
},
{
path: "editpost",
element: <EditPost />,
},
{
path: "/",
element: <MainPage />,
}
])
现在导航到localhost:3000,您应该在那里看到所有帖子:
现在,检查响应性——在另一个标签页中打开localhost:3000/editpost,添加一个帖子,则可以在您的主页面上自动显示出来!
魔法在于使用 useSubscribe 和 useFind 方法,它们在 React hooks 中包装了一些基本的 Meteor API,使一切正确地响应。useSubscribe 隐藏了对 Meteor.subscribe 的调用,该订阅连接了我们到上面定义的发布,并返回一个处理函数,检查订阅准备就绪情况——也是响应式的。这样,您可以调用 loading() 并返回一个漂亮的加载屏幕,同时您的数据正在准备中——我们将在未来的章节中展示如何优雅地完成,但您现在可以自己尝试。
useFind 接受一个函数作为参数以及一个反应性应该依赖的参数数组。在我们的情况下,它不依赖于任何东西,所以我们只需要传递一个空数组。但是再次强调——你可以欣赏到我们在客户端和服务器上都使用了同样的业务逻辑控制器的美。
const posts = useFind(()=> CollectionPosts.find({}), [])
我们传递的函数应该返回一个游标。Meteor 在内部确保我们所有的查询都经过了优化,并最小化了不必要的重新渲染。
Pubsub是一种非常强大的机制,可以进行精细控制。在未来的章节中,我们将查看其高级功能。现在,这只是为了让您沉迷于使用Meteor和React创建正确的MVC架构有多么容易。代码简洁、优雅且易于维护——通常情况下,最乏味的部分实际上是前端,但在这里ChatGPT可以给我们带来很多帮助。
在下一章节中,我们将向您展示如何使用Bootstrap和SCSS编译器应用样式 - 其中一些设置并不直观,我们希望我们的指南能够为您节省几个小时的谷歌搜索时间。
敬请关注并欢迎您的评论、问题和改进建议!