我的第一个全栈 Web 应用程序
在这篇文章,作者将使用 React.js 和 Redux.js 前端技术,并通过调用 Ruby on Rails API,手把手教会你如何创建一个完美的全栈 Web 应用程序。
以下为译文:
在学习了600多节教学课程,并实践4个项目之后,现如今我终于可以谈一谈我的最后一个项目了。在参加 Flatiron School 的这个在线软件开发自学课程时,我知道我必须完成五个项目才能毕业。最后一个项目似乎一直遥不可及,因为这个项目需要大量我还没有掌握的技能。但现在,我终于可以提交这个项目了。
最后这个项目的目标是要构建一个漂亮的单页应用,前端使用 React.js 和 Redux.js,调用 Ruby on Rails API。
单页应用
首先,什么是单页应用?单页应用就是一个网站或Web应用程序,根据用户的动作或行为,动态地改写当前页的内容,而不是从服务器加载全新的页面。实现途径有两种:
在一次页面加载中读取所有的页面内容。但考虑到应用程序的复杂性,这样做可能需要很长时间,因此会影响用户体验。
在某个用户事件后,向服务器请求相应的内容。常见的用户事件包括点击按钮、页面向下滚动、鼠标悬停在某个元素上、按下键盘上的某个键等。
对于复杂的应用程序,第二种方式更常见。毕竟,单页应用存在的原因就是它能提供更为平滑的用户体验,不会被全页重新加载打断。
从代码的角度来讲,单页应用意味着整个应用程序中只有一个HTML页面,通常这个页面名为index.html。
构建应用程序结构
应用程序分为两部分:前端和后端。前端是用户交互的部分,即用户界面。后端负责服务器与用户界面之间的连接。构建应用程序有两个选择:
第一个选择就是把前端和后端都放在同一个代码仓库中(比如GitHub上的代码仓库)。
第二个选择是建立两个代码仓库,一个用于后端,一个用于前端。这样做有几个好处,其中之一就是后端(比如本文中的API)可以被多个前端复用,另一个好处就是编辑器中管理的目录更小。
上述两种方法并没有对错之分。基于上面给出的两个理由。我在构建应用程序时选择了使用两个独立的代码仓库、
第一个代码仓库是后端的。我在终端中使用下述命令创建了一个Rails应用程序作为API,不过没有任何视图。这跟创建普通的Rails应用程序是一样的,只不过多了一个参数。
rails new my_app_backend --api
至于前端,我采用了create-react-app生成器。
npx create-react-app my_app_frontend
这两个命令可以帮我建好所需的一切文件。
关于组件的类型
该项目的技术要求是,至少需要写两个容器组件,以及5个无状态组件。React中的组件是界面的基本构成元素。它可以从父组件接受输入(通过props访问),还可以重用。
下面详细介绍一下容器组件和无状态组件。首先需要解释一下什么是状态(state)。状态就是可能会改变的数据。状态改变可能有多种原因,其中之一就是数据库更新导致状态变化,另一个原因就是用户修改了数据。
容器组件也称为有状态组件,而无状态组件也称为表现组件。容器组件和表现组件并没有严格的区分,每个开发者都可以按照自己的意愿来组织各个组件。但一般而言,容器组件是有状态的,可以通过其状态改变来跟踪,而表现组件没有状态,它可以显示传递过来的props,也可以永远显示固定的内容。
至于本文讨论的应用程序,我决定采用最基本的分割方法。我给API中的每个模型都建立了一个容器组件。随着项目的进行,我删掉了一些不再需要的组件,同时还添加了一些其他组件。有状态组件基本上都是表单。最好的例子就是注册表单和登录表单。在React的表单中,每次用户输入都会导致状态的变化,可能是局部状态变化,也可能是Redux存储状态变化(我们稍后讨论Redux)。无状态组件的例子就是 BicyclesList.js 中的自行车列表。这个组件通过 props 接受一个来自 CitiesContainer 组件的城市列表,它本身与状态没有任何关系。
React中的路由
由于单页应用中不会重新加载完整的页面,因此产生的问题之一就是路由如何进行。Web应用程序中路由的作用是,在用户访问特定网页时确定需要执行什么。我们的单页应用中只有一个视图,因此没办法像Rails应用程序那样在用户点击链接时跳转到另一个视图。
凡事都有解决的办法,对于这个问题,我们可以使用react-router库。它有许多功能,其中包括:
URL显示用户的当前位置,而不仅仅是显示根页面的URL
用户可以使用浏览器的前进和后退按钮
用户可以在地址栏中输入URL,跳转到指定页面
下面以 CitiesContainer 为例来介绍路由的工作方式:
App.jsimport React from 'react';
import { Route } from 'react-router-dom';
import CitiesContainer from './containers/CitiesContainer';class CitiesContainer extends React.Component {
render() {
return (
<Route path='/cities' component={CitiesContainer} />
)
}
}export default App;
这里使用了 path 而不是 exact path,因此所有包含 /cities 的路径都会被处理。CitiesContainer 容器的内部如下:
containers/CitiesContainer.js import React from 'react';
import { connect } from 'react-redux';
import { Route } from 'react-router-dom';
import { fetchCities } from '../actions/fetchCities';
import CitiesList from '../components/CitiesList';
import CityPage from '../components/CityPage';
import BicyclesList from '../components/BicyclesList';class CitiesContainer extends React.Component {
componentDidMount() {
this.props.fetchCities()
}
render() {
return (
<div>
<Route exact path='/cities' render={() => <CitiesList
cities={this.props.cities} />} />
<Route exact path='/cities/:id' render={(routerProps) =>
<CityPage {...routerProps} cities={this.props.cities}
/>}/>
<Route path='/cities/:id/bicycles' render={(routerProps) =>
<BicyclesList {...routerProps} cities={this.props.cities}
/>}/>
</div>
)
}
}const mapStateToProps = state => {
return {
cities: state.cities
}
}export default connect(mapStateToProps, { fetchCities })(CitiesContainer)
React-router-dom 包为开发者提供了 routerProps。该属性可以将URL的内容作为参数传递给props。在这里,我们可以通过 props 访问城市的id,进而可以对 cities(通过props访问)进行过滤,找到我们需要的那个城市。
使用Redux
终于讲到了Redux。Redux是什么呢?这个问题我也问过自己,也问过Google。我在Youtube上找到了这个视频(https://www.youtube.com/watch?v=np8A_aW7Pew),介绍得非常清楚。我希望对你也有帮助。Redux的文档告诉我们,Redux是一个供 JavaScript 应用程序使用的、“可预测的”状态容器。文档中强调了Redux的4个方面:
可预测:它有助于编写在所有环境中行为都很一致的应用程序,更容易测试。
中心化:应用程序的状态和逻辑中心化,可以实现强大的功能,如状态持久化等。
可调式:Redux DevTools 可以非常方便地跟踪应用程序状态变化发生的时间、位置、原因以及方式等。
灵活:Redux可以与任何UI层结合使用。
在我们的应用程序中,Redux有许多便利之处,但最重要的一点是你可以从任何地方访问当前用户的信息(如果存在当前登录用户的话)。我们将用户信息保存到Redux存储中,就可以从应用程序的任意位置访问,而不仅限于将当前用户通过props传递的那些组件。实际上,任何子组件都可以通过以下两种方式之一连接到Redux存储:
使用mapsStateToProps(),无需从组件中访问Redux存储,就能将状态内容放到props中
使用mapDispatchToProps(),无需从组件中访问Redux存储,就能分发actions。
这样就可以分离状态管理和状态显示。但这并非Redux的全部功能。我们在构建异步action creator的时候还是用了thunk中间件。
什么是中间件?维基百科的解释是“提供系统软件和应用软件之间连接的软件,以便于软件各部件之间的沟通”。你可以认为中间件就像胶水一样。因此 thunk 可以让我们做一些原本做不到的事情。thunk 函数的参数是 dispatch,因此可以在函数中使用,在本例中就是在action creator中使用。由于dispatch可以在函数内使用,我们可以利用这一点,仅在fetch请求结束时进行分发。如下例所示,我们仅在获取所有城市的fetch请求结束后进行分发:
actions/fetchCities.jsexport const fetchCities = () => {
return (dispatch) => {
fetch('http://localhost:3000/api/v1/cities')
.then(response => response.json())
.then(cities => {
dispatch({
type: 'FETCH_CITIES',
payload: cities
})
})
}
}
利用fetch实现数据持久化
我们可以通过fetch,用GET方式从服务器上获得数据,也可以用POST方式将数据发送到服务器。用GET获取数据的方式可以参照前面讨论路由的时候提到的 CitiesContainer 示例。我们在下面的action控制器中,利用fetch以GET方式获取所有城市:
actions/fetchCities.jsexport const fetchCities = () => {
return (dispatch) => {
fetch('http://localhost:3000/api/v1/cities')
.then(response => response.json())
.then(cities => {
dispatch({
type: 'FETCH_CITIES',
payload: cities
})
})
}
}
在认证部分,登录action creator是一个以POST方式向服务器发送数据的例子:
actions/auth.jsexport const login = credentials => {
return (dispatch) => {
fetch('http://localhost:3000/api/v1/login', {
credentials: 'include',
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(credentials)
})
.then(response => response.json())
.then(user => {
if (user.error) {
alert(user.error)
} else {
dispatch(setCurrentUser(user))
dispatch(resetLoginForm())
}
})
.catch(console.log)
}
}export const setCurrentUser = user => {
return {
type: 'SET_CURRENT_USER',
payload: user
}
}export const resetLoginForm = () => {
return {
type: 'RESET_LOGIN_FORM'
}
}
至于样式,我最初的计划是使用React版的Bootstrap,因为这是唯一一个我听说过的库。在样式方面我还是新手,而且我没找到导航条,因此搜索了一下React中有什么可以使用的框架。然后在这篇文章(https://medium.com/@zeolearn/6-best-reactjs-based-ui-frameworks-9c780b96236c)中看到了Semantic UI React的Menu组件,这正是我所需要的东西。我从来没有用过其他样式框架,因此没办法比较Semantic UI和其他组件哪个更容易使用,但我必须要说,Semantic UI非常容易上手。
样式并不是这个项目的重点,但我计划在这个应用程序中实现这个样式。我已经建立了基础结构和基本的功能,现在添加新功能已经比较容易了。我计划继续改进应用程序的功能。
原文:https://medium.com/@annelaure.developer/my-first-full-stack-web-application-8ac82db61b10
本文为 CSDN 翻译,转载请注明来源出处。
【End】
热 文 推 荐
☞漫话:如何给女朋友解释为什么一到年底,部分网站就会出现日期混乱的现象?