简介
首先想要介绍的是 React,看到这篇文章的朋友想必都有一些关于 React 的了解了,但对于刚接触的新人而言,在这就要简要地介绍一下了。然后就是关于使用 React 构建一个简单单页应用(下文用 SPA 代替,Single Page Application)的一些介绍和讲解。
关于 React
React 起源于 Facebook 的内部项目,因为该公司对市场上所有 JavaScript MVC 框架,都不满意,就决定自己写一套,用来架设Instagram 的网站。做出来以后,发现这套东西很好用,就在2013年5月开源了。(更多相关介绍请看这)
特点:
- 仅仅只是 UI
- 虚拟 DOM:最大限度减少与 DOM 的交互(类似于使用 jQuery 操作 DOM)
- 单向数据流:很大程度减少了重复代码的使用
组件化:
- 可组合(Composeable):一个组件易于和其它组件一起使用,或者嵌套在另一个组件内部。如果一个组件内部创建了另一个组件,那么说父组件拥有(own)它创建的子组件,通过这个特性,一个复杂的UI可以拆分成多个简单的UI组件
- 可重用(Reusable):每个组件都是具有独立功能的,它可以被使用在多个UI场景
- 可维护(Maintainable):每个小的组件仅仅包含自身的逻辑,更容易被理解和维护
生命周期:
- Mounting:已插入真实 DOM
- Updating:正在被重新渲染
- Unmounting:已移出真实 DOM
React 为每个状态都提供了两种处理函数,will 函数在进入状态之前调用,did 函数在进入状态之后调用,三种状态共计五种处理函数。
- componentWillMount()
- componentDidMount()
- componentWillUpdate(object nextProps,object nextState)
- componentDidUpdate(object prevProps,object prevState)
- componentWillUnmount()
此外,React 还提供两种特殊状态的处理函数。
- componentWillReceiveProps(object nextProps):已加载组件收到新的参数时调用
- shouldComponentUpdate(object nextProps,object nextState):组件判断是否重新渲染时调用
正题
那么进入正题,花了点时间去写一个简单的 SPA,也算是一个比较完整 React 骨架,但不包括测试(测试的教程可以看这个),相关源码可以查看 react-start-kit。
接下来看看我们这个项目的构建需要用到些什么:
- react
- redux
- webpack
- react-router
- ant design
- babel
...
还有一些没有列举出来,具体可以看仓库源码的 package.json
。其中的详细介绍会在文尾列出一些我所看过的文章或是官方介绍。
配置项
Webpack
说到 React 项目的构建就不得不提 Webpack 这个神器。构建工具有很多,例如 Grunt,Gulp,Brunch 等,相比这些构建工具,Webpack 感觉就是和 React 不谋而合,尤其是 react-hot-loader 这样的神器(热加载),让 Webpack 成为最主流的 React 构建工具。
关于 Webpack 的特性以及介绍这里就不赘述了,我们可以从下图看出 Webpack 的作用:
接着我们从项目代码中来看 Webpack。
entry@H_403_108@: { app: [__dirname + '/src/index'@H_403_108@],},output: { path: __dirname + '/_dist'@H_403_108@,filename: '[name]_[hash].js'@H_403_108@,}@H_403_108@@H_403_108@
这部分主要是指定入口和出口文件。entry
作为项目的入口文件;output
作为文件编译后的出口,其中 path
代表输出的路径,filename
代表文件名称,而 [name]_[hash]
保证了浏览器不会存在缓存(即修改文件后效果不生效)。
module@H_403_108@: {
loaders: [{
test: /\.js$/@H_403_108@,loaders: ['babel'@H_403_108@],exclude: /node_modules/@H_403_108@,{
test: /\.css$/@H_403_108@,loaders: ['style'@H_403_108@,'css'@H_403_108@],include: /components/@H_403_108@,{
test: /\.(jpe?g|png|gif|svg|ico)/i@H_403_108@,loader: 'file'@H_403_108@,{
test: /\.(ttf|eot|svg|woff|woff2)/@H_403_108@,{
test: /\.(pdf)/@H_403_108@,{
test: /\.(swf|xap)/@H_403_108@,}],}
而这部分会帮助我们去处理不同类型的文件,其中 test
就是文件的后缀,loaders
是“转译器”,include
是指定文件的目录,exclude
是排除某个目录。我们可以看出,所有的 .js
文件都会通过 babel 去转译,也就是我们在项目中使用 ES6+ 语法会通过 babel 转译成浏览器可以识别的 ES5 代码。
最后配置好的 config 是这样的:
var@H_403_108@ HtmlWebpackPlugin = require@H_403_108@('html-webpack-plugin'@H_403_108@);
module@H_403_108@.exports = {
entry: {
app: [__dirname + '/src/index'@H_403_108@],output: {
path: __dirname + '/_dist'@H_403_108@,resolve: {
root: [
__dirname + '/src'@H_403_108@,__dirname + '/node_modules'@H_403_108@,__dirname,],extensions: [''@H_403_108@,'.js'@H_403_108@],module@H_403_108@: {
loaders: [{
test: /\.js$/@H_403_108@,{
test: /\.css$/@H_403_108@,{
test: /\.(jpe?g|png|gif|svg|ico)/i@H_403_108@,{
test: /\.(ttf|eot|svg|woff|woff2)/@H_403_108@,{
test: /\.(pdf)/@H_403_108@,{
test: /\.(swf|xap)/@H_403_108@,plugins: [
new@H_403_108@ HtmlWebpackPlugin({
template: __dirname + '/src/index.html'@H_403_108@,favicon: __dirname + '/src/favicon.ico'@H_403_108@,inject: false@H_403_108@,}),};
Express 服务器启动
Node.js web 应用开发框架 Express 作为项目的 web 服务器,有 Node.js 开发经验的同学应该挺熟悉的了,这里也不多做赘述。
最终的启动代码是这样的:
var@H_403_108@ express = require@H_403_108@('express'@H_403_108@);
var@H_403_108@ webpack = require@H_403_108@('webpack'@H_403_108@);
var@H_403_108@ webpackConfig = require@H_403_108@('./webpack.development'@H_403_108@);
var@H_403_108@ app = express();
var@H_403_108@ compiler = webpack(webpackConfig);
app.use@H_403_108@(require@H_403_108@('webpack-dev-middleware'@H_403_108@)(compiler,{
stats: {
colors: true@H_403_108@,}));
app.use@H_403_108@(require@H_403_108@('webpack-hot-middleware'@H_403_108@)(compiler)); //热加载@H_403_108@
app.listen(process.env.PORT,function@H_403_108@(err)@H_403_108@ @H_403_108@{ //在没有端口的情况下,会自动给出一个随机端口@H_403_108@
if@H_403_108@ (err) {
console.log(err);
}
});
为了方便我们的访问,项目使用了 minihost
进行启动,方便快捷。值得一提的是,使用 h -- npm start
命令启动时,访问的是项目文件夹的名称作为链接,例如项目叫 myproject
,那么此时可以访问 myproject.t.t
。
Redux
对于复杂的 SPA,状态(state)管理非常重要。state 可能包括:服务端的响应数据、本地对响应数据的缓存、本地创建的数据(比如,表单数据)以及一些 UI 的状态信息(比如,路由、选中的 tab、是否显示下拉列表、页码控制等等)。如果 state 变化不可预测,就会难于调试(state 不易重现,很难复现一些 bug)和不易于扩展(比如,优化更新渲染、服务端渲染、路由切换时获取数据等等)。
state 为单一对象,使得 Redux 只需要维护一棵状态树,服务端很容易初始化状态,易于服务器渲染。state 只能通过 dispatch(action) 来触发更新,更新逻辑由 reducer 来执行。
在使用 Redux 后,state 就变得很容易维护,而且数据流非常清晰,容易解决遇到的 BUG。
我们可以看下图来简要地理解 Redux:
我们可以在项目中看到的结构是:
├─store@H_403_108@
├─actions@H_403_108@
├─reducers@H_403_108@
├─constants@H_403_108@
├─helpers@H_403_108@
├─components@H_403_108@
├─app@H_403_108@.js@H_403_108@
├─favicon@H_403_108@.ico@H_403_108@
├─index@H_403_108@.html@H_403_108@
├─index@H_403_108@.js@H_403_108@
└─routes@H_403_108@.js@H_403_108@
最终我们的 store 是:
import@H_403_108@ {createStore,applyMiddleware,combineReducers,compose} from@H_403_108@ 'redux'@H_403_108@;
import@H_403_108@ thunk from@H_403_108@ 'redux-thunk'@H_403_108@;
import@H_403_108@ {reduxReactRouter} from@H_403_108@ 'redux-router'@H_403_108@;
import@H_403_108@ createHistory from@H_403_108@ 'history/lib/createHashHistory'@H_403_108@;
import@H_403_108@ routes from@H_403_108@ '../routes'@H_403_108@;
import@H_403_108@ * as@H_403_108@ reducers from@H_403_108@ '../reducers'@H_403_108@;
let@H_403_108@ middlewares = [thunk];
if@H_403_108@ (process.env.NODE_ENV === 'development'@H_403_108@) { //在开发环境下可以看到 state 的 log@H_403_108@
const@H_403_108@ logger = require@H_403_108@('redux-logger'@H_403_108@);
middlewares = [...middlewares,logger];
}
const@H_403_108@ finalCreateStore = compose( //组合多个函数@H_403_108@
applyMiddleware(...middlewares),reduxReactRouter({routes,createHistory}),)(createStore); //创建 store 来管理所有的 state@H_403_108@
export@H_403_108@ default@H_403_108@ function@H_403_108@ configureStore@H_403_108@(initialState@H_403_108@) @H_403_108@{
const@H_403_108@ reducer = combineReducers(reducers); //把一个由多个不同 reducer 函数作为 value 的 object,合并成一个最终的 reducer 函数@H_403_108@
const@H_403_108@ store = finalCreateStore(reducer,initialState);
if@H_403_108@ (process.env.NODE_ENV === 'development'@H_403_108@ && module@H_403_108@.hot) { //开发环境下的热加载@H_403_108@
module@H_403_108@.hot.accept('../reducers'@H_403_108@,() => {
const@H_403_108@ nextReducers = require@H_403_108@('../reducers'@H_403_108@);
const@H_403_108@ nextReducer = combineReducers(nextReducers);
store.replaceReducer(nextReducer);
});
}
return@H_403_108@ store;
}
获取 state 需要在组件中调用 connect
函数,可以自行定义需要获取的 state。(这用于区分展示型和容器型组件)
...
@connect@H_403_108@(
state => ({
data: state.data
})
)
export default@H_403_108@ class@H_403_108@ ComponentOne@H_403_108@ extends@H_403_108@@H_403_108@ Component@H_403_108@ {@H_403_108@
...
}
注意:connect
必须紧跟 component 的定义,不然会报错。
Router
为项目添加路由系统,使用了 react-router 来管理路由。在开发项目的时候,比较推荐的做法是使用路由去@R_898_404@面,并且创建 store 的同时我们就把 router 加入其中,然后我们根据路由的变化去更新视图。
我们可以看看路由的源码:
import@H_403_108@ React from@H_403_108@ 'react'@H_403_108@;
import@H_403_108@ Route from@H_403_108@ 'react-router/lib/Route'@H_403_108@; //import {Route} from 'react-router';@H_403_108@
import@H_403_108@ Base from@H_403_108@ 'components/base/Base'@H_403_108@;
import@H_403_108@ Home from@H_403_108@ 'components/home/Home'@H_403_108@;
export@H_403_108@ default@H_403_108@ (
<Route@H_403_108@ component@H_403_108@={Base}@H_403_108@>@H_403_108@ <Route@H_403_108@ path@H_403_108@="/"@H_403_108@ component@H_403_108@={Home}@H_403_108@ />@H_403_108@ <Route@H_403_108@ path@H_403_108@="/home"@H_403_108@ component@H_403_108@={Home}@H_403_108@ />@H_403_108@ </Route@H_403_108@>@H_403_108@ )@H_403_108@;
path
是跳转路径,component
是与路径相匹配的组件。
Ant Design
由蚂蚁金服技术部出品的一个 UI 设计语言,也是项目中所用到的 UI 组件库。
特性:
- Designed as Ant Design,提炼和服务企业级中后台产品的交互语言和视觉风格
- React Component 上精心封装的高质量 UI 库
- 基于 npm + webpack + babel 的工作流,支持 ES2015
选择理由:
- 有很好的技术支持
- 简洁的样式
- 基本涵盖常用组件
...
简单的 Component
组件作为 React 渲染的一个基本组成,我们通常把它们分为两类,容器型和展示型。相较于容器型,展示型是通过容器型传递 props 来获取数据,而容器型可以直接从 store 中获取,处理并传递给下级组件。
在实际应用中会发现,定义一个容器型组件负责处理数据,然后分发给下级展示型组件,当需要更新数据时,那么容器型组件发生变化会引起下级展示型组件的变化,这样就对我们业务上造成了一定的困扰(在不需要更新的部分组件上也发生了更新)。因此,我们选择在需要获取数据的组件中使用 connect
,这样则会方便很多(感觉有些违反规则)。
在项目中我们会这么定义组件:
import@H_403_108@ React@H_403_108@,{Component@H_403_108@} from 'reac@H_403_108@t';
import@H_403_108@ {connect} from 'react@H_403_108@-redux';
import@H_403_108@ Presentational@H_403_108@ from 'components@H_403_108@/common/Presentational@H_403_108@';
@connect@H_403_108@(
state => ({
data: state.data
})
)
export default@H_403_108@ class@H_403_108@ Container@H_403_108@ extends@H_403_108@@H_403_108@ Component@H_403_108@ {@H_403_108@
render() {
const {data} = this@H_403_108@.props;
return@H_403_108@ (
<Presentational@H_403_108@ data={data} />
)
}
}
上面是可以从 store 获取数据的组件,并嵌套另一个组件,将数据传递给它。
import@H_403_108@ React@H_403_108@,{Component@H_403_108@,PropTypes@H_403_108@} from 'reac@H_403_108@t';
export default@H_403_108@ class@H_403_108@ Presentational@H_403_108@ extends@H_403_108@@H_403_108@ Component@H_403_108@ {@H_403_108@
static propTypes = {
data: PropTypes@H_403_108@.string,}
render() {
const {data} = this@H_403_108@.props;
return@H_403_108@ (
<div>
{data}
</div>
)
}
}
获取上一个组件传递过来的数据,并展示出来。
总结
这是一篇科普文(哈哈~囧),并没有深入去分析各项技术的具体内容,希望能帮助刚入手 React 的新手们。实践项目的源码可以在 react-start-kit 看到,你可以下载这个项目进行自己的一些探索和开发。还在努力探索中,文中有措辞不当或是疏漏,欢迎提出意见和建议。
参考
react 官网
Babel 官网
redux 介绍
redux 中文文档
Ant design 官网
React 入门实例教程
react-router 中文文档
Webpack 傻瓜式指南(一)
CSS Modules 详解及 React 中实践
一看就懂的 ReactJs 入门教程(精华版)
深入浅出React(二):React开发神器Webpack