更新于 2017-1-17
http://www.mrfront.com/2016/12/30/react-router-tutorial-part3/?utm_source=tuicool&utm_medium=referral
这篇为 React-Router 教程的最后一章,前两章为:
React-Router 中文简明教程(上)
React-Router 中文简明教程(中)
文章大纲:
本篇示例源码:
react-router-demo-part3
( 打开源码边看教程 可以帮助你更好的理解 )
十. 使用 browserHistory 属性让 URL 更简洁
前面章节中,我们将Router组件的history属性设为hashHistory,history属性用于监听切换 URL,URL 地址 默认 被解析成一个hash值(即#后面的部分,比如http://localhost:8080/#/about?_k=hvsqla),所以你也可以省略不写history属性。
现代浏览器可以直接使用 JavaScript 操作 URL 而不发起 HTTP 请求,所以就不再需要依赖 hash 来实现路由,将history设为browserHistory可以直接显示路径(比如http://localhost:8080/about)。
修改index.js,导入browserHistory替代hashHistory:
// index.js
// ...
// 导入 browserHistory 替代 hashHistory
import { Router, Route, browserHistory, IndexRoute } from 'react-router'
render((
<Router history={browserHistory}>
{/* ... */}
</Router>
), document.getElementById('app'))
npm start启动服务器,打开http://localhost:8080点击导航链接 About 一切正常~ 浏览器 URL 地址变简洁了 显示为http://localhost:8080/about,但刷新浏览器后 你会看到页面显示“Cannot GET /about”这是个404错误,表示找不到网页!
出现这个问题的原因在于:无论你传递了什么 URL,服务器都需要传递给你的 app,因为你的应用直在操纵浏览器中的 URL,但是当前的服务器却不知道如何处理这些 URL。
如何解决这个问题?可以在 webpack-dev-server 中使用–history-api-fallback选项,打开package.json,在“start”字段后添加–history-api-fallback参数:
"start": "webpack-dev-server --inline --content-base . --history-api-fallback"
接着,将index.html中所有的相对路径改为绝对路径,比如:
<!-- index.html -->
<!-- index.css -> /index.css -->
<link rel="stylesheet" href="/index.css">
<!-- bundle.js -> /bundle.js -->
<script src="/bundle.js"></script>
重启服务器,npm start,打开http://localhost:8080/about,再次刷新也一切正常~
十一. 搭建生产环境的 server
之前我们使用的 webpack-dev-server 并不是用于真正生产环境的 server,本节我们将体验下如何搭建一个生产环境的 server,首先需要安装三个模块:express(基于 Node.js 的 web 应用开发框架),if-env(用于切换开发和生产环境运行 npm start),compression(服务端 gzip 压缩)
npm install express if-env compression --save
修改package.json,使用if-env在“start”中进行判断,这样很方便,当我们运行npm start命令,如果检测到环境变量NODE_ENV值为production就执行npm run start:prod(生产环境),否则执行npm run start:dev(开发环境),具体如下:
"scripts": {
"start": "if-env NODE_ENV=production && npm run start:prod || npm run start:dev",
"start:dev": "webpack-dev-server --inline --content-base . --history-api-fallback",
"start:prod": "webpack && node server.js"
},
打开 webpack.config.js,修改output选项:
// webpack.config.js
output: {
path: 'public',
filename: 'bundle.js',
publicPath: '/'
},
现在我们需要用 Express 创建一个生产环境的 server,在根目录下 创建server.js:
// server.js
var express = require('express')
var path = require('path')
var app = express()
// 通过 Express 托管静态资源,比如 index.css
// 访问静态资源文件时,express.static 中间件会根据目录查找所需的文件
app.use(express.static(__dirname))
// 设置路由规则,将所有的路由请求发送至 index.html
app.get('*', function (req, res) {
res.sendFile(path.join(__dirname, 'index.html'))
})
// 启动服务器
var PORT = process.env.PORT || 8080
app.listen(PORT, function() {
console.log('Production Express server running at localhost:' + PORT)
})
现在运行:
NODE_ENV=production npm start
恭喜!现在我们已经成功搭建了一个生产环境的 server,可以随意点击链接进行测试。
尝试打开http://localhost:8080/package.json,哎哟!页面显示了package.json的源码,这样的文件我们当然不希望被访问到,所以还需要配置下哪些目录能被访问:
1. 在根目录下创建public文件夹
2. 将index.html和index.css放进public
修改 server.js,将静态文件指向正确的目录:
// server.js
// ...
// 添加 path.join
app.use(express.static(path.join(__dirname, 'public')))
// ...
app.get('*', function (req, res) {
// 在中间添加 'public' 路径
res.sendFile(path.join(__dirname, 'public', 'index.html'))
})
还需要在webpack.config.js中修改输出选项的path为‘public’:
// webpack.config.js
// ...
output: {
path: 'public',
// ...
}
最后,在启动文件中添加–content-base参数:
"start:dev": "webpack-dev-server --inline --content-base public --history-api-fallback",
Okay,现在我们就不会再从根目录启动公共文件了,我们在webpack.config.js中添加一些用于压缩优化的代码:
// webpack.config.js
// 首先导入 webpack 模块
var webpack = require('webpack')
module.exports = {
// ...
// 判断如果环境变量值为生产环境 就使用以下插件:
// `DedupePlugin` —— 打包的时候删除重复或者相似的文件
// `OccurrenceOrderPlugin` —— 根据模块调用次数,给模块分配合适的ids,减少文件大小
// `UglifyJsPlugin` —— 用于压缩js
plugins: process.env.NODE_ENV === 'production' ? [
new webpack.optimize.DedupePlugin(),
new webpack.optimize.OccurrenceOrderPlugin(),
new webpack.optimize.UglifyJsPlugin()
] : [],
// ...
}
在 express 中开启 gzip 压缩,修改server.js:
// server.js
// ...
var compression = require('compression')
var app = express()
// 必须写在最前面(放在 var app = express() 语句后面就行)
app.use(compression())
重启服务器,运行:
NODE_ENV=production npm start
现在你会发现 命令行中打印出 UglifyJS 日志,bundle.js也被压缩了。
十二. 表单处理
大多数导航使用Link组件用于用户点击跳转,但对于表单提交、点击按钮响应等情况,如何和 React-Router 结合呢?
我们在modules/Repos.js中构建一个简单的表单:
// modules/Repos.js
import React from 'react';
import NavLink from './NavLink';
import { browserHistory } from 'react-router';
export default class Repos extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit(event) {
event.preventDefault();
const userName = event.target.elements[0].value;
const repo = event.target.elements[1].value;
const path = `/repos/${userName}/${repo}`;
browserHistory.push(path);
}
render() {
return (
<div>
<h2>Repos</h2>
<ul>
<li><NavLink to="/repos/reactjs/react-router">React Router</NavLink></li>
<li><NavLink to="/repos/facebook/react">React</NavLink></li>
{/* 表单 */}
<li>
<form onSubmit={this.handleSubmit}>
<input type="text" placeholder="userName"/> / {' '}
<input type="text" placeholder="repo"/>{' '}
<button type="submit">Go</button>
</form>
</li>
</ul>
{ this.props.children }
</div>
);
}
}
这里有两种解决方法,第一种比第二种更简洁。
第一种方法 是使用browserHistory.push:
// modules/Repos.js
// ...
import { browserHistory } from 'react-router';
export default class Repos extends React.Component {
// ...
handleSubmit(event) {
// ...
const path = `/repos/${userName}/${repo}`;
browserHistory.push(path);
}
// ...
}
第二种方法 可以使用context对象:
// modules/Repos.js
// ...
export default class Repos extends React.Component {
// ...
handleSubmit(event) {
// ...
const path = `/repos/${userName}/${repo}`;
this.context.router.push(path);
}
// ...
}
Repos.contextTypes = {
router: React.PropTypes.object
};
打开http://localhost:8080/repos/,在表单中输入字段后 点击按钮 “Go” 进行测试,两种方法的结果是一样的:
本篇示例源码:
react-router-demo-part3
十三. 服务端渲染
好吧,首先你要明白服务器端渲染的核心 在 React 中是个比较容易理解的概念,就是利用renderToString返回组件渲染结果的 HTML 字符串,然后再将这个 HTML 字符串拼接到页面中 并在浏览器显示。
render(<App/>, domNode)
// 在服务端渲染
const markup = renderToString(<App/>)
这不是火箭科学,也并非微不足道。你要知道,当一个 React 项目变得复杂时,代码也随之膨胀,这会导致页面加载的速度变慢,尤其表现在流量珍贵的移动端。我们如何在享受 React 组件式开发便利的同时 提高页面加载性能呢?答案就是想方设法在服务端渲染 React 组件。
在你还没明白前,我会先抛出一堆 webpack 的”诡计”,然后我们再来聊 Router。
众所周知 node 是无法理解和直接运行 JSX 的,我们需要先编译它。像babel/register这样的编译器显然不适合直接用在服务端生产环境,那么就可以使用 webpack 在服务器端对 JSX 进行编译打包,就像在客户端所做的一样。
创建 新文件webpack.server.config.js,将下面的东西放进去:
var fs = require('fs');
var path = require('path');
module.exports = {
entry: path.resolve(__dirname, 'server.js'),
output: {
filename: 'server.bundle.js'
},
target: 'node',
// keep node_module paths out of the bundle
externals: fs.readdirSync(path.resolve(__dirname, 'node_modules')).concat([
'react-dom/server', 'react/addons',
]).reduce(function (ext, mod) {
ext[mod] = 'commonjs ' + mod;
return ext;
}, {}),
node: {
__filename: true,
__dirname: true
},
module: {
loaders: [
{
test: /\.js$/, exclude: /node_modules/,
loader: 'babel-loader?presets[]=es2015&presets[]=react'
}
]
}
}
这里不会细说上面这些代码具体做了什么,但你肯定能看出我们将通过 webpack 来运行server.js。在跑应用之前,我们需要在package.json的“scripts”字段中添加一些内容来构建服务端打包命令:
"scripts": {
"start": "if-env NODE_ENV=production && npm run start:prod || npm run start:dev",
"start:dev": "webpack-dev-server --inline --content-base public/ --history-api-fallback",
"start:prod": "npm run build && node server.bundle.js",
"build:client": "webpack",
"build:server": "webpack --config webpack.server.config.js",
"build": "npm run build:client && npm run build:server"
},
现在,当我们运行NODE_ENV=production npm start,客户端和服务端会同时使用 webpack 进行打包。
Ok,下面可以来说说有关 Router 的内容了,我们需要将路由的内容单独提取为一个模块,这样方便客户端和服务端都能导入它。创建 新文件./modules/routes.js,将你的路由和组件内容都移进去:
// modules/routes.js
import React from 'react';
import { Route, IndexRoute } from 'react-router';
import App from './App';
import About from './About';
import Repos from './Repos';
import Repo from './Repo';
import Home from './Home';
module.exports = (
<Route path="/" component={App}>
<IndexRoute component={Home}/>
<Route path="/repos" component={Repos}>
<Route path="/repos/:userName/:repoName" component={Repo}/>
</Route>
<Route path="/about" component={About}/>
</Route>
);
这样就可以直接在index.js中导入routes模块:
// index.js
import React from 'react';
import { render } from 'react-dom';
import { Router, browserHistory } from 'react-router';
// 导入 routes 模块,并放入 Router 组件中
import routes from './modules/routes';
render(
<Router routes={routes} history={browserHistory}/>,
document.getElementById('app')
);
打开server.js,从 react-router 中导入Router, browserHistory这两个模块帮助我们在服务端渲染。
如果我们试图在服务端像客户端一样render一个<Router/>,我们将会得到一个空白页,原因在于服务端渲染是同步的 路由匹配却是异步的,而客户端由于没等到异步操作完成就渲染了一次,服务端返回的数据就被丢弃了。
此外,大多数应用希望使用路由帮助加载数据,所以无关异步路由,你要想知道在实际渲染之前页面将渲染什么,你得在渲染前先加载路由异步操作完成后所返回的数据。
首先我们从 react-router 中导入match和RouterContext,然后匹配路由到 URL 并最终渲染。macth方法可以确保在路由异步操作完成后执行回调函数。
修改server.js:
// ...
import React from 'react';
// 使用 `renderToString` 将组件渲染的结果转为 HTML 字符串
import { renderToString } from 'react-dom/server';
// `match` 可以确保在路由异步操作完成后执行回调函数
import { match, RouterContext } from 'react-router';
import routes from './modules/routes';
// ...
// 将所有请求发送给 index.html,这样 `browserHistory` 可以工作
app.get('*', (req, res) => {
// 匹配路由到 URL
match({ routes: routes, location: req.url }, (err, redirect, props) => {
// `RouterContext` 为 `Router` 所 render 的内容,
// 当 `Router` 监听 `browserHistory` 的变化时,将它的 `props` 保存在 state(状态)中
// 但 app 在服务器端是无状态的,所以需要使用 `match` 在渲染前得到这些 `props`
const appHtml = renderToString(<RouterContext {...props}/>);
// 虽然还有其他方式能将 HTML 存储在模版里,但还没一种能和 React-Router 完美协作
// 所以这里只使用了一个叫 `renderPage` 的函数
res.send(renderPage(appHtml));
})
})
function renderPage(appHtml) {
// 将 HTML 放到 es6 模版字符串``中,${appHtml} 占位符将 `appHtml`的值插进来
return `
<!doctype html public="storage">
<html>
<meta charset="utf-8"/>
<title>My First React Router App</title>
<link rel="stylesheet" href="/index.css">
<div id="app">${appHtml}</div>
<script src="/bundle.js"></script>
`
}
var PORT = process.env.PORT || 8080;
app.listen(PORT, function() {
console.log('Production Express server running at localhost:' + PORT);
})
现在你可以运行NODE_ENV=production npm start并在浏览器访问应用,你可以看到页面内容并且服务器也将我们的应用发送到浏览器中,但当你点击界面上链接时,你会注意到客户端会响应但却并没向服务端请求用户界面,很酷是吧?!
原因很简单,之前match回调函数中的代码过于简单了 并没考虑到生产环境下的各种情况,应该像下面这样在代码中加一些判断:
app.get('*', (req, res) => {
match({ routes: routes, location: req.url }, (err, redirect, props) => {
if (err) {
// 路由匹配过程中发生错误时,发送错误信息
res.status(500).send(err.message)
} else if (redirect) {
// 我们还没说到路由钩子 `onEnter`,但在用户进入路由前可以进行跳转操作
// 这里我们跳转到服务器进行处理
res.redirect(redirect.pathname + redirect.search)
} else if (props) {
// 如果我们获取到 props 然后匹配到一条路由,说明可以进行 render 了
const appHtml = renderToString(<RouterContext {...props}/>)
res.send(renderPage(appHtml))
} else {
// 没有错误,也没有跳转,什么都匹配不到的情况下
res.status(404).send('Not Found')
}
});
});
React 服务器端渲染目前还是比较新的技术,还没有最佳的实践,尤其在数据加载方面。本教程到此也结束了,希望这对你来说是个崭新的开始。
本文由 前端先生 原创,欢迎转载分享,但请注明出处。