首页
关于
翻译
留言
统计
搜索
1
以太坊简介
742 阅读
2
搭建 OpenAI 代理
663 阅读
3
第 4 章 创建您的第一个 React 组件
550 阅读
4
如何读懂编译后的 JavaScript 代码
526 阅读
5
第 9 章 使用 Jest 测试 React 应用
477 阅读
JavaScript
TypeScript
后端
Web
移动
运维
杂项
登录
Search
标签搜索
React
翻译
Vue
组件
Angular
工程化
库
Hook
框架
优化
路由
Node.js
Flash
部署
算法
可视化
Debug
测试
兼容
Web3
Flying
累计撰写
267
篇文章
累计收到
2
条评论
首页
栏目
JavaScript
TypeScript
后端
Web
移动
运维
杂项
页面
关于
翻译
留言
统计
搜索到
19
篇
框架
相关的结果
2024-04-10
Prisma 游标分页
Prisma ORM 是一个 Node.js 和T ypeScript 的 ORM,具有直观的数据模型、自动迁移、类型安全和自动补全功能。Prisma ORM一大亮点就是支持游标分页。游标分页使用 cursor 和 take 在给定游标之前或之后返回一组有限的结果,因此相对于传统的偏移分页具有更高的性能。游标标记你在结果集中的位置,必须是唯一且连续的列 - 例如 ID 或时间戳。
2024年04月10日
23 阅读
0 评论
1 点赞
2023-06-18
Mern 身份验证 API
在本文中,我们将从零开始创建一个 MERN 栈应用程序。此项目的视频可以在此处找到。前端将使用 React、Redux、React Router 和 React Bootstrap 构建单页面应用程序。后端将使用 Express、MongoDB 和 Mongoose 作为数据库,并使用 JWT(JSON Web Tokens)和 HTTP-only Cookie 来实现身份验证。我们还将使用 Redux 进行状态管理,并使用 Redux Toolkit 使事情变得更加简单。本文将分为两个部分。第一部分我们将创建后端,第二部分我们将创建前端。
2023年06月18日
112 阅读
0 评论
1 点赞
2020-01-23
Express 中的 Request 对象
req对象代表了一个 HTTP 请求,其具有一些属性来保存请求中的一些数据,比如query string,parameters,body,HTTP headers等等。按照惯例,这个对象总是简称为req(http 响应简称为res),但是它们实际的名字由这个回调方法在那里使用时的参数决定。如下例子:app.get('/user/:id', function (req, res) { res.send('user' + req.params.id); });其实你也可以这样写:app.get('/user/:id', function (request, response) { response.send('user' + request.params.id); });属性在Express 4中,req.files默认在req对象中不再是可用的。为了通过req.files对象来获得上传的文件,你可以使用一个multipart-handling(多种处理的工具集)中间件,比如busboy,multer,formidable,multipraty,connect-multiparty或者pez。req.app这个属性持有express程序实例的一个引用,其可以作为中间件使用。如果你按照这个模式,你创建一个模块导出一个中间件,这个中间件只在你的主文件中require()它,那么这个中间件可以通过req.app来获取 express 的实例。例如:// index.js app.get('/viewdirectory', require('./mymiddleware.js'));// mymiddleware.js module.exports = function (req, res) { res.send('The views directory is ' + req.app.get('views')); };req.baseUrl一个路由实例挂载的 Url 路径。var greet = express.Router(); greet.get('/jp', function (req, res) { console.log(req.baseUrl); // greet res.send('Konichiwa!'); }); app.use('/greet', greet);即使你使用的路径模式或者一系列路径模式来加载路由,baseUrl属性返回匹配的字符串,而不是路由模式。下面的例子,greet路由被加载在两个路径模式上。app.use(['/gre+t', 'hel{2}o'], greet); // load the on router on '/gre+t' and '/hel{2}o'当一个请求路径是/greet/jp,baseUrl是/greet,当一个请求路径是/hello/jp,req.baseUrl是/hello。req.baseUrl和app对象的mountpath属性相似,除了app.mountpath返回的是路径匹配模式。req.body在请求的 body 中保存的是提交的一对对键值数据。默认情况下,它是undefined,当你使用比如body-parser和multer这类解析body数据的中间件时,它是填充的。下面的例子,给你展示了怎么使用body-parser中间件来填充req.body。var app = require('express'); var bodyParser = require('body-parser'); var multer = require('multer');// v1.0.5 var upload = multer(); // for parsing multipart/form-data app.use(bodyParser.json()); // for parsing application/json app.use(bodyParser.urlencoded({extended:true})); // for parsing application/x-www-form-urlencoded app.post('/profile', upload.array(), function(req, res, next) { console.log(req.body); res.json(req.body); });req.cookies当使用cookie-parser中间件的时候,这个属性是一个对象,其包含了请求发送过来的cookies。如果请求没有带cookies,那么其值为{}。// Cookie:name=tj req.cookies.name; // => "tj"获取更多信息,问题,或者关注,可以查阅cookie-parser。req.fresh指示这个请求是否是新鲜的。其和req.stale是相反的。当cache-control请求头没有no-cache指示和下面中的任一一个条件为true,那么其就为true:if-modified-since请求头被指定,和last-modified请求头等于或者早于modified响应头。if-none-match请求头是*。if-none-match请求头在被解析进它的指令之后,和etag响应头的值不相等ps:If-None-Match 作用:If-None-Match 和 ETag 一起工作,工作原理是在 HTTP Response 中添加 ETag 信息。 当用户再次请求该资源时,将在 HTTP Request 中加入 If-None-Match 信息(ETag 的值)。如果服务器验证资源的 ETag 没有改变(该资源没有更新),将返回一个 304 状态告诉客户端使用本地缓存文件。否则将返回 200 状态和新的资源和 Etag. 使用这样的机制将提高网站的性能req.fresh; // => truereq.hostname包含了源自HostHTTP 头部的hostname。当trust proxy设置项被设置为启用值,X-Forwarded-Host头部被使用来代替Host。这个头部可以被客户端或者代理设置。// Host:"example.com" req.hostname; // => "example.com"req.ips当trust proxy设置项被设置为启用值,这个属性包含了一组在X-Forwarded-For请求头中指定的 IP 地址。不然,其就包含一个空的数组。这个头部可以被客户端或者代理设置。例如,如果X-Forwarded-For是client,proxy1,proxy2,req.ips就是["clinet", "proxy1", "proxy2"],这里proxy2就是最远的下游。req.originalUrlreq.url不是一个原生的Express属性,它继承自Node's http module。这个属性很像req.url;然而,其保留了原版的请求链接,允许你自由地重定向req.url到内部路由。比如,app.use()的mounting特点可以重定向req.url跳转到挂载点。// GET /search?q=something req.originalUrl; // => "/search?q=something"req.params一个对象,其包含了一系列的属性,这些属性和在路由中命名的参数名是一一对应的。例如,如果你有/user/:name路由,name属性可作为req.params.name。这个对象默认值为{}。// GET /user/tj req.params.name; // => "tj"当你使用正则表达式来定义路由规则,捕获组的组合一般使用req.params[n],这里的n是第几个捕获租。这个规则被施加在无名通配符匹配,比如/file/*的路由:// GET /file/javascripts/jquery.js req.params[0]; // => "javascripts/jquery.js"req.path包含请求 URL 的部分路径。// example.com/users?sort=desc req.path; // => "/users"当在一个中间件中被调用,挂载点不包含在req.path中。你可以查阅app.use()获得跟多的信息。req.protocol请求的协议,一般为http,当启用 TLS 加密,则为https。当trust proxy设置一个启用的参数,如果存在X-Forwarded-Proto头部的话,其将被信赖和使用。这个头部可以被客户端或者代理设置。req.ptotocol; // => "http"req.query一个对象,为每一个路由中的query string参数都分配一个属性。如果没有query string,它就是一个空对象,{}。// GET /search?q=tobi+ferret req.query.q; // => "tobi ferret" // GET /shoes?order=desc&shoe[color]=blue&shoe[type]=converse req.query.order; // => "desc" req.query.shoe.color; // => "blue" req.query.shoe.type; // => "converse"req.route当前匹配的路由,其为一串字符。比如:app.get('/user/:id?', function userIdHandler(req, res) { console.log(req.route); res.send('GET'); });前面片段的输出为:{ path:"/user/:id?" stack: [ { handle:[Function:userIdHandler], name:"userIdHandler", params:undefined, path:undefined, keys:[], regexp:/^\/?$/i, method:'get' } ] methods:{get:true} }req.secure一个布尔值,如果建立的是 TLS 的连接,那么就为true。等价与:'https' == req.protocol;req.signedCookies当使用cookie-parser中间件的时候,这个属性包含的是请求发过来的签名cookies,这个属性取得的是不含签名,可以直接使用的值。签名的cookies保存在不同的对象中来体现开发者的意图;不然,一个恶意攻击可以被施加在req.cookie值上(它是很容易被欺骗的)。记住,签名一个cookie不是把它藏起来或者加密;而是简单的防止篡改(因为签名使用的加密是私人的)。如果没有发送签名的cookie,那么这个属性默认为{}。// Cookie: user=tobi.CP7AWaXDfAKIRfH49dQzKJx7sKzzSoPq7/AcBBRVwlI3 req.signedCookies.user; // => "tobi"为了获取更多的信息,问题或者关注,可以参阅cookie-parser。req.stale指示这个请求是否是stale(陈旧的),它与req.fresh是相反的。更多信息,可以查看req.fresh。req.stale; // => truereq.subdomains请求中域名的子域名数组。// Host:"tobi.ferrets.example.com" req.subdomains; // => ["ferrets", "tobi"]req.xhr一个布尔值,如果X-Requested-With的值为XMLHttpRequest,那么其为true,其指示这个请求是被一个客服端库发送,比如jQuery。req.xhr; // => true方法req.accepts(types)检查这个指定的内容类型是否被接受,基于请求的Accept HTTP 头部。这个方法返回最佳匹配,如果没有一个匹配,那么其返回undefined(在这个 case 下,服务器端应该返回 406 和"Not Acceptable")。type值可以是一个单的MIME type字符串(比如application/json),一个扩展名比如json,一个逗号分隔的列表,或者一个数组。对于一个列表或者数组,这个方法返回最佳项(如果有的话)。// Accept:text/html req.accepts('html'); // => "html" // Accept:text/*, application/json req.accepts('html'); // => "html" req.accepts('text/html'); // => "text/html" req.accepts(['json', 'text']); // => "json" req.accepts('application/json'); // => "application/json" // Accept:text/*, application/json req.accepts('image/png'); req.accepts('png'); // => undefined // Accept:text/*;q=.5, application/json req.accepts(['html', 'json']); // => "json"获取更多信息,或者如果你有问题或关注,可以参阅accepts。req.acceptsCharsets(charset[, ...])返回指定的字符集集合中第一个的配置的字符集,基于请求的Accept-CharsetHTTP 头。如果指定的字符集没有匹配的,那么就返回 false。获取更多信息,或者如果你有问题或关注,可以参阅accepts。req.acceptsEncodings(encoding[, ...])返回指定的编码集合中第一个的配置的编码,基于请求的Accept-EncodingHTTP 头。如果指定的编码集没有匹配的,那么就返回 false。获取更多信息,或者如果你有问题或关注,可以参阅accepts。req.acceptsLanguages(lang [, ...])返回指定的语言集合中第一个的配置的语言,基于请求的Accept-LanguageHTTP 头。如果指定的语言集没有匹配的,那么就返回 false。获取更多信息,或者如果你有问题或关注,可以参阅accepts。req.get(field)返回指定的请求 HTTP 头部的域内容(不区分大小写)。Referrer和Referer的域内容可互换。req.get('Content-type'); // => "text/plain" req.get('content-type'); // => "text/plain" req.get('Something'); // => undefined其是req.header(field)的别名。req.is(type)如果进来的请求的Content-type头部域匹配参数type给定的MIME type,那么其返回true。否则返回false。// With Content-Type:text/html; charset=utf-8 req.is('html'); req.is('text/html'); req.is('text/*'); // => true // When Content-Type is application/json req.is('json'); req.is('application/json'); req.is('application/*'); // => true req.is('html'); // => false获取更多信息,或者如果你有问题或关注,可以参阅type-is。req.param(naem, [, defaultValue])过时的。可以在适合的情况下,使用req.params,req.body或者req.query。返回当前参数name的值。// ?name=tobi req.param('name'); // => "tobi" // POST name=tobi req.param('name'); // => "tobi" // /user/tobi for /user/:name req.param('name'); // => "tobi"按下面给出的顺序查找:req.paramsreq.bodyreq.query可选的,你可以指定一个defaultValue来设置一个默认值,如果这个参数在任何一个请求的对象中都不能找到。直接通过req.params,req.body,req.query取得应该更加的清晰-除非你确定每一个对象的输入。body-parser中间件必须加载,如果你使用req.param()。详细请看req.body。
2020年01月23日
135 阅读
0 评论
1 点赞
2018-12-25
第 12 章 使用 Redux 完善 Flux 应用
上一章带你实现了一个基于 Flux 架构的完整的 React 应用。在本章中,你将对这个应用进行一些修改,以便它使用 Redux 库来实现 Flux 架构。本章的组织结构如下:
2018年12月25日
279 阅读
0 评论
11 点赞
2018-12-10
第 11 章 准备使用 Flux 轻松维护 React 应用
我们决定在我们的 React 应用中实现 Flux 架构的原因是我们希望有一个更容易维护的数据流。上一章实现了 AppDispatcher、TweetActionCreators 和 TweetStore。让我们快速记住它们的用途:TweetActionCreators:创建和调度 actionAppDispatcher:将所有的 action 调度到所有 storesTweetStore:用于存储和管理应用数据数据流中唯一缺失的部分如下:使用 TweetActionCreators 创建 action 并启动数据流使用 TweetStore 获取数据这里有几个重要的问题要问:数据流在我们应用的哪里开始?我们的数据是什么?如果我们回答了这些问题,我们将知道从哪里开始重构我们的应用以适应 Flux 架构。Snapterest 允许用户接收和收集最新的推文。我们的应用唯一关心的数据是 tweets。因此,我们的数据流从接收新推文开始。应用的哪个部分负责接收新推文?你可能还记得我们的 Stream 组件有以下 componentDidMount()方法:componentDidMount() { SnapkiteStreamClient.initializeStream(this.handleNewTweet); }是的,目前,我们在渲染 Stream 组件后初始化了一个新的推文流。等等,你可能会问:“我们不是知道 React 组件应该只关心渲染用户界面吗?”你是正确的。不幸的是,目前,Stream 组件负责两件不同的事情:渲染 StreamTweet 组件初始化数据流显然,这是未来潜在的维护问题。让我们借助 Flux 来解耦这两个不同的关注点。解耦 Flux 关注点首先,我们将创建一个名为 WebAPIUtils 的实用工具模块。在 ~/snapterest/source/utils/ 目录下新建 WebAPIUtils.js 文件:import SnapkiteStreamClient from 'snapkite-stream-client'; import { receiveTweet } from '../actions/TweetActionCreators'; function initializeStreamOfTweets() { SnapkiteStreamClient.initializeStream(receiveTweet); } export { initializeStreamOfTweets };在这个工具模块中,我们首先导入 SnapkiteStreamClient 库和 TweetActionCreators。然后,我们创建 initializeStreamOfTweets() 函数来初始化一个新的推文流,就像在 Stream 组件的 componentDidMount() 方法中一样。除了一个关键的区别:每当 SnapkiteStreamClient 收到一条新消息时,它都会调用 TweetActionCreators.receiveTweet 方法将一条新消息作为参数传递给它:SnapkiteStreamClient.initializeStream(receiveTweet);记住,receiveTweet 函数接受的参数是 tweet:function receiveTweet(tweet) { // ... create and dispatch 'receive_tweet' action }然后,这条消息将作为 receiveTweet() 函数创建的新 action 对象的属性分发。然后,WebAPIUtils 模块导出了 initializeStreamOfTweets() 函数。现在我们有了一个模块,它有一个方法可以在 Flux 架构中发起数据流。我们应该在哪里导入并调用它?由于它与 Stream 组件解耦,事实上,它完全不依赖于任何 React 组件,我们甚至可以在 React 渲染任何内容之前使用它。我们在 app.js 文件中使用它:import React from 'react'; import ReactDOM from 'react-dom'; import Application from './components/Application'; import { initializeStreamOfTweets } from './utils/WebAPIUtils'; initializeStreamOfTweets(); ReactDOM.render(<Application />, document.getElementById('react-application'));如你所见,我们需要做的就是导入并调用 initializeStreamOfTweets()方法:import { initializeStreamOfTweets } from './utils/WebAPIUtils'; initializeStreamOfTweets();我们在调用 React 的 render() 方法之前执行此操作:ReactDOM.render(<Application />, document.getElementById('react-application'));事实上,作为一个实验,你可以完全删除 ReactDOM.render() 的代码行,并在 TweetActionCreators.receiveTweet 函数中放入一个日志语句。例如,运行以下代码:function receiveTweet(tweet) { console.log( "I've received a new tweet and now will dispatch it together with a new action." ); const action = { type: 'receive_tweet', tweet, }; AppDispatcher.dispatch(action); }现在运行npm start命令。然后,在浏览器中打开 ~/snapterest/build/index.html ,你会看到下面的文本呈现在页面上:I am about to learn the essentials of React.js.现在打开 JavaScript 控制台,你会看到如下输出:[Snapkite Stream Client] Socket connectedI've received a new tweet and now will dispatch it together with a new action.应用收到的每一条新消息都会打印出这条日志消息。即使我们没有渲染任何 React 组件,我们的 Flux 架构仍然存在:我们的应用收到一条新的推文。它创建并调度一个新的 action。没有 store 注册到 dispatcher,所以没有人接收新的 action;因此,什么也没有发生。现在你可以清楚地看到 React 和 Flux 是两个完全不依赖的东西。然而,我们确实想渲染我们的 React 组件。毕竟,我们在前 10 章中已经花了很多精力来创建它们!要做到这一点,我们需要将 TweetStore 存储到 action。你能猜到我们应该在哪里使用它吗?这里有一个提示:在一个 React 组件中,它需要一条 tweet 来渲染自己——我们的旧 Stream 组件。重构 Stream 组件现在有了 Flux 架构,我们将重新思考我们的 React 组件如何获取它们需要渲染的数据。如你所知,一个 React 组件通常有两个数据源:调用另一个库,例如,调用 jQuery.ajax() 方法,或者在我们的例子中,SnapkiteStreamClient.initializeStream()通过 props 对象从父组件接收数据我们希望 React 组件不使用任何外部库来接收数据。相反,从现在开始,他们将从 store 获取相同的数据。记住这个计划,让我们重构我们的 Stream 组件。现在看起来是这样的:import React from 'react'; import SnapkiteStreamClient from 'snapkite-stream-client'; import StreamTweet from './StreamTweet'; import Header from './Header'; class Stream extends React.Component { constructor() { super(); this.state = { tweet: null, }; } componentDidMount() { SnapkiteStreamClient.initializeStream(this.handleNewTweet); } componentWillUnmount() { SnapkiteStreamClient.destroyStream(); } handleNewTweet = (tweet) => { this.setState({ tweet, }); }; render() { const { tweet } = this.state; const { onAddTweetToCollection } = this.props; const headerText = 'Waiting for public photos from Twitter...'; if (tweet) { return ( <StreamTweet tweet={tweet} onAddTweetToCollection={onAddTweetToCollection} /> ); } return <Header text={headerText} />; } } export default Stream;首先,让我们删除 componentDidMount(), componentWillUnmount() 和 handleNewTweet() 方法,并导入 TweetStore 存储:import React from 'react'; import SnapkiteStreamClient from 'snapkite-stream-client'; import StreamTweet from './StreamTweet'; import Header from './Header'; import TweetStore from '../stores/TweetStore'; class Stream extends React.Component { state = { tweet: null, }; render() { const { tweet } = this.state; const { onAddTweetToCollection } = this.props; const headerText = 'Waiting for public photos from Twitter...'; if (tweet) { return ( <StreamTweet tweet={tweet} onAddTweetToCollection={onAddTweetToCollection} /> ); } return <Header text={headerText} />; } } export default Stream;再也不需要导入 snapkite-stream-client 模块。接下来,我们需要改变 Stream 组件获取初始消息的方式。让我们更新它的初始状态:state = { tweet: TweetStore.getTweet(), };代码方面,这可能看起来只是一个小变化,但却是一个重大的架构改进。我们现在使用 getTweet() 方法从 TweetStore 中获取数据。在上一章中,我们讨论了 store 如何在 Flux 中暴露公共方法,以便应用的其他部分从它们中获取数据。getTweet() 方法是这些公共方法中的一个例子,它们被称为 _getter_。你可以 store 中获取数据,但你不能像那样直接在 store 中设置数据。store 没有公共的 setter 方法。它们在设计时特意考虑了这种限制,以便当你使用 Flux 编写应用时,数据只能朝一个方向流动。当你需要维护你的 Flux 应用时,这将给你带来很多好处。现在我们知道了如何获取初始的推文,但如何获取后面所有的新推文呢?我们可以创建一个定时器,并反复调用 TweetStore.getTweet(); 然而,这并不是最好的解决方案,因为它假设我们不知道 TweetStore 何时更新了一条新的推文。然而,我们确实知道这一点。怎么做?在上一章中,我们实现了 TweetStore 对象的 addChangeListener() 方法,如下所示:addChangeListener(callback) { this.on('change', callback); }我们也实现了 removeChangeListener() 方法:removeChangeListener(callback) { this.removeListener('change', callback); }没错,我们可以让 TweetStore 告诉我们它什么时候改变了数据。为此,我们需要调用它的 addChangeListener()方法,并传入一个回调函数,TweetStore 会在收到每条新消息时调用该函数。问题是在 Stream 组件中,我们应该在哪里调用 TweetStore.addChangeListener() 方法呢?由于我们只需要为每个组件的生命周期添加一次 change 事件监听器到 TweetStore,因此 componentDidMount() 是一个完美的候选。将下面的 componentDidMount() 方法添加到 Stream 组件中:componentDidMount() { TweetStore.addChangeListener(this.onTweetChange); }我们在这里将 change 事件监听器 this.tweetchange 添加到 TweetStore。现在当 TweetStore 改变它的数据时,它会触发 this.onTweetChange 方法。我们很快就会创建这个方法。不要忘记,在卸载 React 组件之前,我们需要删除所有事件监听器。为此,将以下 componentWillUnmount() 方法添加到 Stream 组件:componentWillUnmount() { TweetStore.removeChangeListener(this.onTweetChange); }删除事件监听器与添加它非常相似。只需调用 TweetStore.removeChangeListener() 方法并传入 this.onTweetChange 方法作为参数。现在是时候在 Stream 组件中创建 onTweetChange 方法了:onTweetChange = () => { this.setState({ tweet: TweetStore.getTweet(), }); };如你所见,它使用 TweetStore.gettweet() 方法更新了组件的状态,并将一条新消息存储在 TweetStore 中。我们需要在 Stream 组件中做最后一次更改。在本章后面,你会了解到 StreamTweet 组件不再需要 handleAddTweetToCollection() 回调函数了。因此,在这个组件中,我们将更改以下代码片段:return ( <StreamTweet tweet={tweet} onAddTweetToCollection={onAddTweetToCollection} /> );用下面的代码替换它:return <StreamTweet tweet={tweet} />;现在让我们来看看新重构的 Stream 组件:import React from 'react'; import StreamTweet from './StreamTweet'; import Header from './Header'; import TweetStore from '../stores/TweetStore'; class Stream extends React.Component { state = { tweet: TweetStore.getTweet(), }; componentDidMount() { TweetStore.addChangeListener(this.onTweetChange); } componentWillUnmount() { TweetStore.removeChangeListener(this.onTweetChange); } onTweetChange = () => { this.setState({ tweet: TweetStore.getTweet(), }); }; render() { const { tweet } = this.state; const { onAddTweetToCollection } = this.props; const headerText = 'Waiting for public photos from Twitter...'; if (tweet) { return <StreamTweet tweet={tweet} />; } return <Header text={headerText} />; } } export default Stream;让我们回顾一下我们的 Stream 组件是如何始终保存最新消息的:我们将组件的初始推文设置为使用 getTweet() 方法从 TweetStore 获取的最新推文。然后,我们在 TweetStore 中监听变化。当 TweetStore 改变它的推文时,我们使用 getTweet() 方法将组件的状态更新为从 TweetStore 获取的最新推文。当组件即将卸载时,我们停止监听 TweetStore 中的变化。这就是 React 组件与 Flux store 交互的方式。在继续使我们的应用的其余部分 Flux 增强之前,让我们看看当前的数据流:app.js:接收新的推文并为每条推文调用 TweetActionCreatorsTweetActionCreators:创建并派发一个带有新推文的新 actionAppDispatcher:它将所有的 action 调度到所有的 storesTweetStore:它注册 dispatcher,并在 dispatcher 接收到每个新 action 时派发 change 事件Stream:监听 TweetStore 中的变化,从 TweetStore 获取一条新的推文,用一条新的推文 更新状态,并重新渲染你能看到我们现在如何扩展 React 组件、action 创建者和 store 的数量,同时仍然能够维护 Snapterest 吗?使用 Flux,它将永远是单向的数据流。不管我们要实现多少新特性,心智模型都是一样的。从长远来看,当我们需要维护我们的应用时,我们将受益匪浅。我是否提到过我们将在应用中更多地使用 Flux 呢?接下来,让我们来做这个。创建 CollectionStoreSnapterest 不仅存储最新的推文,还存储用户创建的推文集合。让我们用 Flux 重构这个特性。首先,让我们创建一个集合存储。进入 ~/snapterest/source/stores/ 目录,新建 CollectionStore.js 文件:import AppDispatcher from '../dispatcher/AppDispatcher'; import { EventEmitter } from 'events'; const CHANGE_EVENT = 'change'; let collectionTweets = {}; let collectionName = 'new'; function addTweetToCollection(tweet) { collectionTweets[tweet.id] = tweet; } function removeTweetFromCollection(tweetId) { delete collectionTweets[tweetId]; } function removeAllTweetsFromCollection() { collectionTweets = {}; } function setCollectionName(name) { collectionName = name; } function emitChange() { CollectionStore.emit(CHANGE_EVENT); } const CollectionStore = Object.assign({}, EventEmitter.prototype, { addChangeListener(callback) { this.on(CHANGE_EVENT, callback); }, removeChangeListener(callback) { this.removeListener(CHANGE_EVENT, callback); }, getCollectionTweets() { return collectionTweets; }, getCollectionName() { return collectionName; }, }); function handleAction(action) { switch (action.type) { case 'add_tweet_to_collection': addTweetToCollection(action.tweet); emitChange(); break; case 'remove_tweet_from_collection': removeTweetFromCollection(action.tweetId); emitChange(); break; case 'remove_all_tweets_from_collection': removeAllTweetsFromCollection(); emitChange(); break; case 'set_collection_name': setCollectionName(action.collectionName); emitChange(); break; default: // ... do nothing } } CollectionStore.dispatchToken = AppDispatcher.register(handleAction); export default CollectionStore;CollectionStore 是一个更大的 store,但它具有与 TweetStore 相同的结构。首先,我们导入依赖项并给 CHANGE_EVENT 变量指定一个 change 事件名称:import AppDispatcher from '../dispatcher/AppDispatcher'; import { EventEmitter } from 'events'; const CHANGE_EVENT = 'change';然后,定义数据和 4 个修改这些数据的私有方法:let collectionTweets = {}; let collectionName = 'new'; function addTweetToCollection(tweet) { collectionTweets[tweet.id] = tweet; } function removeTweetFromCollection(tweetId) { delete collectionTweets[tweetId]; } function removeAllTweetsFromCollection() { collectionTweets = {}; } function setCollectionName(name) { collectionName = name; }如你所见,我们将推文集合存储在一个对象中,该对象最初为空,我们还将集合名称最初设置为 new。然后,创建三个私有函数来修改 collectionTweets:addTweetToCollection():顾名思义,它将 tweet 对象添加到 collectionTweets 对象中removeTweetFromCollection():从 collectionTweets 对象中删除 tweet 对象removeAllTweetsFromCollection():通过将 collectionTweets 设置为空对象,删除其所有 tweet 对象然后,我们定义了一个私有函数 setCollectionName,它会修改 collectionName,把现有的集合名改成新的集合名。这些函数被认为是私有的,因为它们在 CollectionStore 模块之外不可访问。例如,你不能在任何其他模块中这样访问它们:CollectionStore.setCollectionName('impossible');如前文所述,这样做是为了在应用中强制执行单向数据流。我们创建了 emitChange() 方法来触发 change 事件。然后创建 CollectionStore 对象:const CollectionStore = Object.assign({}, EventEmitter.prototype, { addChangeListener(callback) { this.on(CHANGE_EVENT, callback); }, removeChangeListener(callback) { this.removeListener(CHANGE_EVENT, callback); }, getCollectionTweets() { return collectionTweets; }, getCollectionName() { return collectionName; }, });这和 TweetStore 对象非常相似,除了两个方法:getCollectionTweets():返回推文集合getCollectionName():返回集合名称这些方法可以在 CollectionStore.js 文件之外访问,并且应该在 React 组件中使用它们来从 CollectionStore 获取数据。接下来,我们创建 handleAction() 函数:function handleAction(action) { switch (action.type) { case 'add_tweet_to_collection': addTweetToCollection(action.tweet); emitChange(); break; case 'remove_tweet_from_collection': removeTweetFromCollection(action.tweetId); emitChange(); break; case 'remove_all_tweets_from_collection': removeAllTweetsFromCollection(); emitChange(); break; case 'set_collection_name': setCollectionName(action.collectionName); emitChange(); break; default: // ... do nothing } }这个函数处理 AppDispatcher 调度 action,但与 CollectionStore 模块中的 TweetStore 不同,我们可以处理多个 actions。实际上,我们可以处理与消息集合相关的 4 个 actions。add_tweet_to_collection:向集合中添加一条推文remove_tweet_from_collection:从集合中删除一条推文remove_all_tweets_from_collection:从集合中删除所有推文set_collection_name:设置集合名称请记住,所有的数据存储都会接收到所有的 actions,所以 CollectionStore 也会接收到 receive_tweet action,但我们在这个数据存储中忽略了它,就像 TweetStore 会忽略 add_tweet_to_collection、remove_tweet_from_collection、remove_all_tweets_from_collection 和 set_collection_name 一样。然后,我们用 AppDispatcher 注册 handleAction 回调函数,并将 dispatchToken 保存在 CollectionStore 对象中:CollectionStore.dispatchToken = AppDispatcher.register(handleAction);最后,我们将 CollectionStore 导出为一个模块:export default CollectionStore;现在集合存储已经准备好了,接下来让我们创建 action 创建器函数。创建 CollectionActionCreators转到 ~/snapterest/source/actions/ 并新建 CollectionActionCreators.js 文件:import AppDispatcher from '../dispatcher/AppDispatcher'; function addTweetToCollection(tweet) { const action = { type: 'add_tweet_to_collection', tweet, }; AppDispatcher.dispatch(action); } function removeTweetFromCollection(tweetId) { const action = { type: 'remove_tweet_from_collection', tweetId, }; AppDispatcher.dispatch(action); } function removeAllTweetsFromCollection() { const action = { type: 'remove_all_tweets_from_collection', }; AppDispatcher.dispatch(action); } function setCollectionName(collectionName) { const action = { type: 'set_collection_name', collectionName, }; AppDispatcher.dispatch(action); } export default { addTweetToCollection, removeTweetFromCollection, removeAllTweetsFromCollection, setCollectionName, };对于 CollectionStore 中处理的每个 action,我们都有一个 action 创建器函数:addTweetToCollection():创建并调度 add_tweet_to_ collection action,包含一条新的推文removeTweetFromCollection():创建并调度 remove_ tweet_from_collection action,该 action 的参数是必须从集合中删除的消息 IDremoveAllTweetsFromCollection():创建和调度 remove_all_tweets_from_collection actionsetCollectionName():创建并调度 add_tweet_to_ collection action,并使用新的集合名称现在当我们创建了 CollectionStore 和 CollectionActionCreators 模块后,我们可以采用 Flux 架构开始重构 React 组件。重构 Application 组件从哪里开始重构 React 组件呢?让我们从组件层次结构中最顶层的 React 组件——应用开始。目前,我们的 Application 组件存储和管理消息集合。让我们删除此功能,因为它现在由集合存储管理。删除 constructor()、addTweetToCollection()、removeTweetFromCollection () 以及 Application 组件的 removeAllTweetsFromCollection() 方法:import React from 'react'; import Stream from './Stream'; import Collection from './Collection'; class Application extends React.Component { render() { const { collectionTweets } = this.state; return ( <div className="container-fluid"> <div className="row"> <div className="col-md-4 text-center"> <Stream onAddTweetToCollection={this.addTweetToCollection} /> </div> <div className="col-md-8"> <Collection tweets={collectionTweets} onRemoveTweetFromCollection={this.removeTweetFromCollection} onRemoveAllTweetsFromCollection={ this.removeAllTweetsFromCollection } /> </div> </div> </div> ); } } export default Application;现在, Application 组件只有 render()方法,用于渲染流和 Collection 组件。由于它不再管理推文的集合,我们也不需要向流和 Collection 组件传递任何属性。更新 Application 组件的 render() 函数,如下所示:render() { return ( <div className="container-fluid"> <div className="row"> <div className="col-md-4 text-center"> <Stream /> </div> <div className="col-md-8"> <Collection /> </div> </div> </div> ); }使用 Flux 架构允许 Stream 组件管理最新的推文,Collection 组件管理推文集合,而 Application 组件不再需要管理任何东西,所以它变成了一个容器组件,用额外的 HTML 标记封装了 Stream 和 Collection 组件。事实上,你可能已经注意到,我们当前版本的 Application 组件很适合作为一个函数式 React 组件:import React from 'react'; import Stream from './Stream'; import Collection from './Collection'; const Application = () => ( <div className="container-fluid"> <div className="row"> <div className="col-md-4 text-center"> <Stream /> </div> <div className="col-md-8"> <Collection /> </div> </div> </div> ); export default Application;我们的 Application 组件现在更简单了,它的标记看起来更干净,这提高了组件的可维护性。做得好!重构 Collection 组件接下来,让我们重构 Collection 组件。用下面的代码替换现有的 Collection 组件:import React, { Component } from 'react'; import ReactDOMServer from 'react-dom/server'; import CollectionControls from './CollectionControls'; import TweetList from './TweetList'; import Header from './Header'; import CollectionUtils from '../utils/CollectionUtils'; import CollectionStore from '../stores/CollectionStore'; class Collection extends Component { state = { collectionTweets: CollectionStore.getCollectionTweets(), }; componentDidMount() { CollectionStore.addChangeListener(this.onCollectionChange); } componentWillUnmount() { CollectionStore.removeChangeListener(this.onCollectionChange); } onCollectionChange = () => { this.setState({ collectionTweets: CollectionStore.getCollectionTweets(), }); }; createHtmlMarkupStringOfTweetList() { const htmlString = ReactDOMServer.renderToStaticMarkup( <TweetList tweets={this.state.collectionTweets} /> ); const htmlMarkup = { html: htmlString, }; return JSON.stringify(htmlMarkup); } render() { const { collectionTweets } = this.state; const numberOfTweetsInCollection = CollectionUtils.getNumberOfTweetsInCollection(collectionTweets); let htmlMarkup; if (numberOfTweetsInCollection > 0) { htmlMarkup = this.createHtmlMarkupStringOfTweetList(); return ( <div> <CollectionControls numberOfTweetsInCollection={numberOfTweetsInCollection} htmlMarkup={htmlMarkup} /> <TweetList tweets={collectionTweets} /> </div> ); } return <Header text="Your collection is empty" />; } } export default Collection;我们改变了什么?几个地方。首先,导入两个新模块:import CollectionUtils from '../utils/CollectionUtils'; import CollectionStore from '../stores/CollectionStore';我们在第 9 章使用 Jest 测试 React 应用时创建了 CollectionUtils 模块,本章我们将使用它。CollectionStore 是我们获取数据的地方。接下来,你应该能够发现这四个方法的熟悉模式:在初始状态下,我们将推文集合设置为当时存储在 CollectionStore 中的内容。你可能还记得,CollectionStore 提供了 getCollectionTweets() 方法来从中获取数据。在 componentDidMount() 方法中,我们添加 change 事件监听器。onCollectionChange 转换为 CollectionStore。每当消息集合更新时,CollectionStore 就会调用 this.onCollectionChange 回调函数,通知 Collection 组件有更改。在 componentWillUnmount() 方法中,我们删除添加到 componentDidMount() 方法中的 change 事件监听器。在 onCollectionChange() 方法中,我们将组件的状态设置为 CollectionStore 中当前存储的状态。更新组件的状态会触发重新渲染。Collection 组件的 render() 方法现在更简单明了:render() { const { collectionTweets } = this.state; const numberOfTweetsInCollection = CollectionUtils .getNumberOfTweetsInCollection(collectionTweets); let htmlMarkup; if (numberOfTweetsInCollection > 0) { htmlMarkup = this.createHtmlMarkupStringOfTweetList(); return ( <div> <CollectionControls numberOfTweetsInCollection={numberOfTweetsInCollection} htmlMarkup={htmlMarkup} /> <TweetList tweets={collectionTweets} /> </div> ); } return (<Header text="Your collection is empty" />); }我们使用 CollectionUtils 模块获取集合中的一些推文,传递给子组件 CollectionControls 和 TweetList 的属性更少。重构 CollectionControls 组件CollectionControls 组件也有一些重大改进。让我们先看一下重构后的版本,然后讨论更新了什么以及为什么这么做:import React, { Component } from 'react'; import Header from './Header'; import Button from './Button'; import CollectionRenameForm from './CollectionRenameForm'; import CollectionExportForm from './CollectionExportForm'; import CollectionActionCreators from '../actions/CollectionActionCreators'; import CollectionStore from '../stores/CollectionStore'; class CollectionControls extends Component { state = { isEditingName: false, }; getHeaderText = () => { const { numberOfTweetsInCollection } = this.props; let text = numberOfTweetsInCollection; const name = CollectionStore.getCollectionName(); if (numberOfTweetsInCollection === 1) { text = `${text} tweet in your`; } else { text = `${text} tweets in your`; } return ( <span> {text} <strong> {name}</strong> collection </span> ); }; toggleEditCollectionName = () => { this.setState((prevState) => ({ isEditingName: !prevState.isEditingName, })); }; removeAllTweetsFromCollection = () => { CollectionActionCreators.removeAllTweetsFromCollection(); }; render() { const { name, isEditingName } = this.state; const onRemoveAllTweetsFromCollection = this.removeAllTweetsFromCollection; const { htmlMarkup } = this.props; if (isEditingName) { return ( <CollectionRenameForm name={name} onCancelCollectionNameChange={this.toggleEditCollectionName} /> ); } return ( <div> <Header text={this.getHeaderText()} /> <Button label="Rename collection" handleClick={this.toggleEditCollectionName} /> <Button label="Empty collection" handleClick={onRemoveAllTweetsFromCollection} /> <CollectionExportForm htmlMarkup={htmlMarkup} /> </div> ); } } export default CollectionControls;首先,导入两个额外的模块:import CollectionActionCreators from '../actions/CollectionActionCreators'; import CollectionStore from '../stores/CollectionStore';请注意,我们不再管理这个组件中的集合名称。相反,我们从 CollectionStore 模块中获取:const name = CollectionStore.getCollectionName();然后,我们做一个关键的改动。我们替换 setCollectionName() 方法,添加一个新集合 removeAllTweetsFromCollection():removeAllTweetsFromCollection = () => { CollectionActionCreators.removeAllTweetsFromCollection(); };当用户单击 Empty Collection 按钮时,会调用 removeAllTweetsFromCollection() 方法。这个用户操作触发了 removeAllTweetsFromCollection() action 创建函数,该函数创建并调度该 action 到 store。接着,CollectionStore 从集合中删除所有消息并派发 change 事件。接下来,让我们重构 CollectionRenameForm 组件。重构 CollectionRenameForm 组件CollectionRenameForm 是一个受控表单组件。这意味着它的输入值存储在组件的状态中,更新该值的唯一方法是更新组件的状态。它有应该从 CollectionStore 获取的初始值,让我们来实现它。首先,导入 CollectionActionCreators 和 CollectionStore 模块:import CollectionActionCreators from '../actions/CollectionActionCreators'; import CollectionStore from '../stores/CollectionStore';现在,我们需要删除它现有的 constructor() 方法:constructor(props) { super(props); const { name } = props; this.state = { inputValue: name }; }用下面的代码替换上面的代码:state = { inputValue: CollectionStore.getCollectionName(), };如你所见,唯一的区别是现在我们从 CollectionStore 中获取了初始的 inputValue。接下来,更新 handleFormSubmit() 方法:handleFormSubmit = (event) => { event.preventDefault(); const { onChangeCollectionName } = this.props; const { inputValue: collectionName } = this.state; onChangeCollectionName(collectionName); };用下面的代码替换上面的代码:handleFormSubmit = (event) => { event.preventDefault(); const { onCancelCollectionNameChange } = this.props; const { inputValue: collectionName } = this.state; CollectionActionCreators.setCollectionName(collectionName); onCancelCollectionNameChange(); };这里的重要区别在于,当用户提交表单时,我们将创建一个新的 action,在集合存储中设置一个新的名称:CollectionActionCreators.setCollectionName(collectionName);最后,我们需要调用 handleFormCancel() 方法修改集合名称的来源。handleFormCancel = (event) => { event.preventDefault(); const { name: collectionName, onCancelCollectionNameChange } = this.props; this.setInputValue(collectionName); onCancelCollectionNameChange(); };用下面的代码替换先前的代码:handleFormCancel = (event) => { event.preventDefault(); const { onCancelCollectionNameChange } = this.props; const collectionName = CollectionStore.getCollectionName(); this.setInputValue(collectionName); onCancelCollectionNameChange(); };同样,我们从集合存储中获取集合名称:const collectionName = CollectionStore.getCollectionName();这就是我们需要在 CollectionRenameForm 组件中更改的全部内容。接下来让我们重构 TweetList 组件。重构 TweetList 组件TweetList 组件渲染一个推文列表。每条推文都是一个 Tweet 组件,用户可以单击它将其从集合中删除。你觉得它可以使用 CollectionActionCreators 吗?是可以的。让我们把 CollectionActionCreators 模块添加进去:import CollectionActionCreators from '../actions/CollectionActionCreators';然后,创建 removeTweetFromCollection() 回调函数,当用户点击推文图片时调用该函数:removeTweetFromCollection = (tweet) => { CollectionActionCreators.removeTweetFromCollection(tweet.id); };如你所见,它使用 removeTweetFromCollection() 函数创建了一个新 action,将消息 ID 作为参数传递给它。最后,我们需要确保 removeTweetFromCollection()真的被调用了。在 getTweetElement()方法中,找到下面这行代码:const { tweets, onRemoveTweetFromCollection } = this.props;现在用下面的代码替换它:const { tweets } = this.props; const onRemoveTweetFromCollection = this.removeTweetFromCollection;我们已经完成了这个组件。重构之旅的下一站是 StreamTweet 组件。重构 StreamTweet 组件StreamTweet 渲染一个推文图像,用户可以点击它将其添加到推文集合中。你可能已经猜到了,我们将在用户单击推文图像时创建并调度一个新的 action。首先,将 CollectionActionCreators 模块导入 StreamTweet 组件:import CollectionActionCreators from '../actions/CollectionActionCreators';然后,新增 addTweetToCollection() 方法:addTweetToCollection = (tweet) => { CollectionActionCreators.addTweetToCollection(tweet); };addTweetToCollection() 回调函数应该在用户单击推文图像时调用。让我们看一下 render() 方法中的这行代码:<Tweet tweet="{tweet}" onImageClick="{onAddTweetToCollection}" />将上面的代码替换为下面的代码:<Tweet tweet="{tweet}" onImageClick="{this.addTweetToCollection}" />最后,需要替换下面这行代码:const { tweet, onAddTweetToCollection } = this.props;用下面代码代替:const { tweet } = this.props;StreamTweet 组件现在完成了。再接再厉这就是将 Flux 架构集成到我们的 React 应用中所需要做的所有工作。如果你比较一下没有使用 Flux 和使用 Flux 的 React 应用,你会很快发现使用 Flux 时,理解你的应用如何工作的是多么容易。你可以访问 https://facebook.github.io/flux了解更多关于 Flux 的信息。我认为现在是检查一切是否正常的好时机。让我们构建并运行 Snapterest!转到 ~/snapterest,在终端窗口中执行以下命令:npm start请确保运行的是我们在第 2 章中为你的项目安装强大的工具的 Snapkite 引擎应用。现在在浏览器中打开 ~/snapterest/build/index.html 文件。你将看到新推文出现在左侧,每次一条。点击一条推文,将其添加到右侧的集合中。它有效果吗?检查 JavaScript 控制台是否有错误。没有错误吗?恭喜你已经将 Flux 架构集成到我们的 React 应用中!总结在本章中,我们使用 Flux 架构完成了应用的重构。你了解了如何将 React 与 Flux 结合,以及 Flux 所提供的优势。下一章,我们将使用 Redux 库进一步简化应用的架构。
2018年12月10日
322 阅读
0 评论
9 点赞
1
2
...
4