第一部分:使用 react.js、express.js、node.js 和 mongodb 构建 Web 应用程序






4.93/5 (21投票s)
这是一个关于使用 react.js、node.js、express.js 和 mongodb 构建 Web 应用程序的多部分系列。
引言
有一段时间以来,我一直在听到关于 react.js 的一些非常有趣的事情,比如它有多快,以及选择 react.js 来构建下一个 Web 项目会多么酷。这一切听起来都很好,但是,除非我们自己尝试,否则我们将无法欣赏它。所以,我决定花点时间玩玩 react.js,看看这些说法有多准确。现在只有一个问题,react.js 的文档假设你对现代 JavaScript 开发工作流程有一定程度的经验,总之,如果你是第一次使用它,它并不是很有帮助。这个系列文章是我的尝试,将所有碎片整合在一起,展示构建一个功能齐全的 React 应用程序需要什么。
在这个系列文章中,我们将从头开始创建一个全栈单页 JavaScript 应用程序。在本系列的第 1 部分,我们将完全专注于 react.js 工作流程,并在没有任何后端 API 或数据库的情况下创建应用程序的前端部分。在第 2 部分,我们将使用 express.js 为我们的应用程序创建后端,并将其数据持久化到 mongodb 中。
必备组件
为了最大限度地利用本文,我建议您跟着操作。需要对 node.js 有一些熟悉,但不需要提前了解 react.js 或 express.js。您需要在机器上安装 node,其他一切我们都会在进展过程中进行安装。
所以,不要再浪费时间了,打开您最喜欢的代码编辑器和命令行终端,直接进入代码。
项目设置
就像所有出色的 node 应用程序一样,故事总是以 npm 开始,它是 node.js 平台的包管理器。所以,快速在您的终端中输入以下命令:
npm install -g gulp bower nodemon
此命令将安装三个非常有用的 node 实用程序。Gulp 是一个任务运行器,我们将使用它来简化我们使用一些自动化来完成繁琐工作流任务的生活,Bower 是一个用于各种前端库(如 bootstrap、jquery 等)的包管理器。与手动下载这些库相比,这是一种更方便的安装方式。我们还将使用 "nodemon",在代码发生更改时自动重启我们的服务器应用程序。
接下来,让我们创建一个新的干净目录并使用以下命令进行初始化。
mkdir school-finder
npm init
bower init
我将我的目录命名为“school-finder”,在本文中,我将一直称之为根目录。“npm”和“bower”会提示您输入一些问题,只需一直按回车键,它们就会很高兴。此时,您的根目录中将拥有“package.json”和“bower.json”文件。现在,让我们安装以下我们将使用的 node 模块。
npm install --save browserify reactify vinyl-source-stream gulp react react-dom express guid
如果上面的列表看起来很长且令人困惑,那么请不要担心,我们正在使用 browserify,这样我们就可以使用类似 CommonJs 模块的模式来引用各种库,而不是在 html 页面中添加 script 标签。reactify 用于将 JSX 转换为 JavaScript。JSX 是一种类似 XML 的 JavaScript 语法,React 使用它,一旦您编写了一些 JSX 代码,您就会确切地理解我的意思。您可以在 npm 网站上找到有关这些模块的更多信息,就我们的目的而言,让我们假设它们是必需的。
现在,在根目录中创建另外两个目录,“app”和“server”。在“app”目录中创建“actions”、“components”、“dist”和“stores”目录。不要太担心目录名称,一旦到达那里,我将解释每个目录将包含什么。接下来,在应用程序的根目录中添加以下两个文件:“.bowerrc”和“gulpfile.js”。编辑 .bowerrc 文件并在其中添加以下代码。
{
"directory":"app/lib"
}
上面的代码本质上配置了 bower 将安装库的目录。我们还需要做的最后一件事是添加对 bootstrap 库的引用,我们需要一些基本的样式来让我们的应用程序看起来不错,所以让我们运行以下命令。
bower install --save bootstrap-css
好吧,这似乎做了很多工作,即使没有编写一行 React 代码,我们的项目也已经有很多了,但请相信我,做所有这些设置都是值得的。现在,让我们开始编写一些代码。
第一行代码
让我们在根文件夹的“server”目录中添加一个新文件,名为“server.js”。您可能已经猜到,它将包含我们应用程序的后端。但是等等!我们不是说在这里只专注于前端代码吗?是的,我们仍然坚持同一个计划,我们只需要足够的后端代码来让我们开始。所以,让我们在 server.js 文件中添加以下代码。
var express = require("express");
var path = require("path");
var app = express();
app.use(express.static(path.join(__dirname,"../app/dist")));
app.listen(7777,function(){
console.log("Started listening on port", 7777);
})
所以上面的代码创建了一个 express 应用程序,它监听端口 7777 上的 http 请求。它还配置 express 来从school-finder/app/dist目录提供静态内容,如 html、css、图像等。接下来,在“app”文件夹中添加“index.html”、“style.css”和“main.jsx”文件,并按如下方式编辑 index.html。
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>School Finder</title>
<link href="bootstrap.min.css" rel="stylesheet" />
<link href="style.css" rel="stylesheet" />
</head>
<body>
<div class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="#">School Finder</a>
</div>
</div>
</div>
<div id="container" class="container"></div>
<script src="main.js"></script>
</body>
</html>
现在,使用以下命令启动您的应用程序,并浏览 https://:7777/index.html。
nodemon .\server\server.js
此时,如果您检查浏览器的开发者工具,您会收到几个资源的 404 错误。您预料到了吗?让我们快速修复一下,编辑我们之前添加的 gulpfile.js,如下所示。
var gulp = require("gulp");
var browserify = require("browserify");
var reactify = require("reactify");
var source = require("vinyl-source-stream");
gulp.task("bundle", function () {
return browserify({
entries: "./app/main.jsx",
debug: true
}).transform(reactify)
.bundle()
.pipe(source("main.js"))
.pipe(gulp.dest("app/dist"))
});
gulp.task("copy", ["bundle"], function () {
return gulp.src(["app/index.html","app/lib/bootstrap-css/css/bootstrap.min.css","app/style.css"])
.pipe(gulp.dest("app/dist"));
});
gulp.task("default",["copy"],function(){
console.log("Gulp completed...");
});
在我们的 gulpfile 中,我们创建了两个任务:
a) 任务bundle,接受“main.jsx”文件,该文件当前为空,使用“reactify”将 jsx 代码转译为纯 JavaScript,然后使用“vinyl-source-stream”流式传输源,进一步在“dist”目录中即时创建“main.js”文件,其中包含转译后的 JS 代码。
b) 任务copy,接受 index.html、bootstrap.min.css 和 style.css 文件,并将它们复制到“dist”文件夹。
现在转到命令行终端并运行“gulp”命令,然后刷新浏览器中的页面。检查浏览器的开发者工具,如果到目前为止一切顺利,您应该看不到任何 404 错误。
来自 React 的问候
我知道您现在可能很着急,心想做了这么多工作,却还没有写一行 React 代码,但请记住,我们正在尝试获得真实的 React.js 开发体验,所以请继续前进。让我们在“components”目录中添加两个空文件“SchoolInfo.jsx”和“SchoolsList.jsx”,并按如下方式编辑它们。
SchoolInfo.jsx
var React = require("react");
module.exports = React.createClass({
render:function(){
return(
<div className="panel panel-default">
<div className="panel-heading">
{this.props.info.name}
</div>
<div className="panel-body">
{this.props.info.tagline}
</div>
</div>
)
}
})
SchoolsList.jsx
var React = require("react");
var SchoolInfo = require("./SchoolInfo.jsx")
module.exports = React.createClass({
render:function(){
return(
<div className="row">
<div className="col-md-6">
//We will add addSchool functionality here
</div>
<div className="col-md-6">
{
this.props.schools.map(function(s,index){
return(
<SchoolInfo info={s} key={"school"+index} />
)
})
}
</div>
</div>
)
}
});
有几个重要的点需要强调:
a) 我们能够在这里使用“require”构造来添加引用,即使浏览器不支持该构造,因为我们不会直接将这些文件的引用添加到我们的 html 文件中,而是“browserify”库,我们已将其配置为 gulp 任务,它将解析所有这些依赖项并将它们打包到“main.js”文件中。
b) React 应用程序是使用 React 组件构建的,您可以将组件视为自包含的模板,其功能内置于模板本身中,尽管 React 的纯粹主义者可能不同意这个定义,但我为了简单起见提出它。我们通过使用React.createClass方法来创建一个 React 组件。
c) render 函数中奇怪的语法被称为 JSX,“reactify”,我们已在 bundle 任务中配置,它会将 JSX 代码转译为浏览器理解的 JS 代码。您可以在花括号“{}”内编写 JavaScript 表达式,JSX 转译器在发出 JavaScript 代码时会对其进行内插。
d) 一旦您创建了一个 React 组件,您就可以在另一个 React 组件中使用熟悉的标签语法来渲染该组件,例如,我们在SchoolList组件中使用<SchoolInfo />标签渲染了SchoolInfo组件。
e) 当您需要将某些数据传递给组件时,可以通过在渲染组件时将对象作为值传递给组件属性来实现。您传递给组件属性的任何内容都可以在组件中通过this.props对象的属性来访问。例如,在SchoolList中渲染SchoolInfo组件时,您将school对象传递给“info”属性,您可以在SchoolInfo组件中通过this.props.info来访问它。
现在打开“main.jsx”文件,该文件到目前为止是空的,并按如下方式编辑。
var React = require("react");
var ReactDOM = require("react-dom");
var SchoolsList = require("./components/SchoolsList.jsx");
var _schools = [{name:"Lovedale",tagline:"this is a wonderful school"},
{name:"Bishop",tagline:"Another great school"}];
function render(){
ReactDOM.render(<SchoolsList schools={_schools} />, document.getElementById("container"));
}
render();
注意 ReactDOM.render 函数,它实际上将我们的 React 组件“SchoolsList”渲染到 Index.html 文件的ID 为 container 的 div 元素中。ReactDOM.render函数的第二个参数接受一个 html 元素,该元素充当我们整个 React 应用程序的挂载点。我们还向SchoolList组件的schools属性传递了一个school对象的数组。现在,再次从命令行终端运行“gulp”命令并刷新页面,您就可以看到一个 hello world React 应用程序了。
React Flux 架构
您一定在想,如果我们要做一个非琐碎的、包含所有这些组件的完整应用程序,我将走多远?我将在哪里放置事件处理程序、所有 REST API 调用?我的应用程序的架构应该是什么样的?好吧,您所有问题的答案是React Flux 架构。
React Flux 是 Facebook 内部使用的 React 应用程序的编程模式。Flux 确保在整个应用程序生命周期中只有单向数据流。Flux 具有以下代理,它们使 Flux 流保持正常运行:
1. Actions:它们是包含一些数据和一些上下文(类型)的有效负载,简而言之就是对象。它们由某些辅助函数创建,作为视图中活动的(主要是)结果。例如,当用户单击添加按钮时,我们将创建一个包含要添加的信息和上下文的操作。所有操作都发送到调度程序。
2. Dispatcher:调度程序工作就像一个全局集线器,当任何操作发送到它时,它会触发所有注册到它的侦听器。
3. Stores:Stores 将自己注册到调度程序。当调度程序广播一个操作的到达时,Stores 会更新自己(如果该操作与这些 Stores 相关),并发出一个更改事件,该事件会导致 UI 更新。
4. Views:Views 是渲染的 html 组件。
让我们在我们的应用程序中实现 FLUX,这样会更清楚一些。
创建调度程序
在您的生产应用程序中,您可能选择使用健壮的 flux 库,但对于本文,让我们创建自己的简单调度程序,以便我们可以确切地看到它是如何工作的。所以,在“app”目录中添加一个新文件“dispatcher.js”,并在其中添加以下代码,这段代码基本上是自解释的。
var Guid = require("guid");
var listeners = {};
function dispatch(payload) {
for (var id in listeners) {
listeners[id](payload);
}
}
function register(cb) {
var id = Guid.create();
listeners[id] = cb;
}
module.exports = {
register: register,
dispatch: dispatch
}
我们将把所有注册的侦听器保存在listeners对象中。任何有兴趣注册自己到调度程序的都可以使用register方法,另一方面,操作助手将调用调度程序的dispatch方法来广播操作。
创建操作助手
接下来,让我们在“actions”文件夹中添加一个新文件“SchoolActions.js”,并在其中添加以下代码。
var dispatcher = require("../dispatcher");
module.exports = {
addSchool:function(school){
dispatcher.dispatch({
school:school,
type:"school:addSchool"
});
},
deleteSchool:function(school){
dispatcher.dispatch({
school:school,
type:"school:deleteSchool"
});
}
}
如您所见,我们的 Action Helper 中有两个操作:addSchool和deleteSchool。这两个操作都以school对象的形式接收信息,然后添加一个上下文,即type属性,该属性告诉将对哪个项目执行哪个操作。每当调用操作方法时(将来自视图),它都会调用调度程序的dispatch方法来处理有效负载。
创建 Store
现在是时候实现我们的 Store 了,所以让我们在“stores”目录中添加“schoolsStore.js”文件,并按如下方式更改其代码。
var dispatcher = require("../dispatcher");
function SchoolStore() {
var listeners = [];
var schools = [{ name: "Lovedale", tagline:"A wonderful school" },
{ name: "Bishop",tagline:"An awesome school" },
{ name: "Daffodils", tagline:"An excellent school" }];
function getSchools() {
return schools;
}
function onChange(listener) {
listeners.push(listener);
}
function addSchool(school) {
schools.push(school)
triggerListeners();
}
function deleteSchool(school) {
var _index;
schools.map(function (s, index) {
if (s.name === school.name) {
_index = index;
}
});
schools.splice(_index, 1);
triggerListeners();
}
function triggerListeners() {
listeners.forEach(function (listener) {
listener(schools);
});
}
dispatcher.register(function (payload) {
var split = payload.type.split(":");
if (split[0] === "school") {
switch (split[1]) {
case "addSchool":
addSchool(payload.school);
break;
case "deleteSchool":
deleteSchool(payload.school);
break;
}
}
});
return {
getSchools: getSchools,
onChange: onChange
}
}
module.exports = SchoolStore();
在我们的 Store 实现中,请查看dispatcher.register方法调用,这就是 Store 将自己注册到调度程序的地方。当调度程序广播一个操作时,Store 的注册回调将被调用,其中它会检查有效负载的类型信息并决定一个适当的操作,例如addSchool或deleteSchool,就像在我们的例子中一样。
另请注意,在响应调度程序的调用采取适当的操作后,Store 会调用triggerListeners,这是将 UI 渲染器提供更新 UI 机会的最后一块拼图,通过调用 Store 的 onChange 事件的所有订阅者。
现在,让我们更新“main.jsx”文件,使其连接到我们的 Store,而不是显示虚拟数据。
//main.jsx
var React = require("react");
var ReactDOM = require("react-dom");
var SchoolsList = require("./components/SchoolsList.jsx");
var schoolsStore = require("./stores/schoolsStore");
var _schools = schoolsStore.getSchools();
schoolsStore.onChange(function(schools){
_schools = schools;
render();
});
function render(){
ReactDOM.render(<SchoolsList schools={_schools} />, document.getElementById("container"));
}
render();
您可以再次运行 gulp,刷新页面后,您将在屏幕上看到新数据出现。
为组件添加行为
让我们通过添加添加和删除功能使我们的应用程序更有用。我们将创建另一个 React 组件,所以让我们在“components”目录中添加“AddSchool.jsx”文件,并在其中添加以下代码。
var React = require("react");
var actions = require("../actions/SchoolActions");
module.exports = React.createClass({
getInitialState:function(){
return {
name:"",
tagline:""
}
},
addSchool:function(e){
e.preventDefault();
actions.addSchool(this.state);
},
handleInputChange:function(e){
e.preventDefault();
var name = e.target.name;
var state = this.state;
state[name] = e.target.value;
this.setState(state);
},
render:function(){
return(
<form className="form" onSubmit={this.addSchool}>
<div className="form-group">
<label className="control-label" htmlFor="name">School Name:</label>
<input type="text" className="form-control" id="name" name="name" value={this.state.name} onChange={this.handleInputChange} placeholder="School Name" />
</div>
<div className="form-group">
<label className="control-label" htmlFor="tagline">Tagline:</label>
<input type="text" className="form-control" id="tagline" name="tagline" value={this.state.address} onChange={this.handleInputChange} placeholder="Tagline" />
</div>
<div className="form-group">
<button className="btn" type="submit">Add School</button>
</div>
</form>
)
}
})
此组件包含一些新语法,所以让我们快速回顾一下。
1)我们添加了一个表单 onSubmit 处理程序“addSchool”。您可以在 createClass 参数对象中添加任意数量的函数。
2)就像我们通过属性将外部数据传递给 React 组件并通过this.props对象访问这些数据一样,我们可以通过this.state对象访问组件的内部状态。所有 React 组件都有自己的内部状态,在我们使用它之前,我们需要通过getInitialState函数来初始化它。这是一个特殊函数,它返回的对象将成为我们组件的初始状态。
3)正如我之前提到的,React 不支持双向绑定,所以每当我们在相关时就需要自己更改状态,例如,在本例中,每当用户在表单控件中输入值时,我们都会使用handleInputChange函数来更新状态,该函数从 onChange 事件处理程序触发。请注意,我们在事件处理程序中使用了e.preventDefault()以避免页面刷新。
4)当用户单击“添加学校”按钮时,在 submit 事件处理程序中,我们按照下图所示的流程启动 FLUX。
5)从事件处理程序,我们调用操作助手,它创建一个操作并调用调度程序。调度程序广播操作,由于 Store 订阅了该操作,它会更新自身并渲染 UI。这正是 FLUX 的流程。
现在,让我们更新“SchoolsList.jsx”文件,使其在其中渲染“AddSchool”组件:
var React = require("react");
var SchoolInfo = require("./SchoolInfo.jsx")
var AddSchool = require("./AddSchool.jsx");
module.exports = React.createClass({
render:function(){
return(
<div className="row">
<div className="col-md-6">
<AddSchool />
</div>
<div className="col-md-6">
{
this.props.schools.map(function(s,index){
return(
<SchoolInfo info={s} key={"school"+index} />
)
})
}
</div>
</div>
)
}
});
我们还来更新“SchoolInfo.jsx”以添加删除功能,如下所示。
var React = require("react");
var actions = require("../actions/SchoolActions");
module.exports = React.createClass({
deleteSchool: function(e){
e.preventDefault();
actions.deleteSchool(this.props.info);
},
render:function(){
return(
<div className="panel panel-default">
<div className="panel-heading">
{this.props.info.name}
<span className="pull-right text-uppercase delete-button" onClick={this.deleteSchool}>×</span>
</div>
<div className="panel-body">{this.props.info.tagline}</div>
</div>
)
}
})
现在再次运行gulp命令并刷新页面,我们就拥有了一个功能齐全的 React 应用程序。尝试添加/删除学校,它应该可以正常工作。
所以,现在我们完成了应用程序的前端部分,但是我们的应用程序有一个问题,如果您刷新页面,所有您添加或删除的新学校都会消失。这是因为我们还没有持久化任何信息。
在本次系列的下一部分,我们将为这个应用程序创建后端,并且我们还将回顾我们的前端代码以实现 REST API 调用。
摘要
如果您能忍受到这里,那就意味着您想学习 React。我附上了本篇文章的示例代码,如果您在跟进过程中遇到任何问题,我鼓励您下载并参考它。请按照以下步骤运行示例。
1. 下载并解压
2. 在您的命令行终端中导航到提取的应用程序的根目录
3. 运行npm install
4. 运行bower install
5. 运行gulp
6. 运行 nodemon .\server\server.js