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(({/* ... */} ), 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中所有的相对路径改为绝对路径,比如:
重启服务器,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 (); } }Repos
{ this.props.children }
React Router - {/* 表单 */}
React
这里有两种解决方法,第一种比第二种更简洁。
第一种方法 是使用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(, domNode) // 在服务端渲染 const markup = renderToString( )
这不是火箭科学,也并非微不足道。你要知道,当一个 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 = ();
这样就可以直接在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(, document.getElementById('app') );
打开server.js,从 react-router 中导入Router, browserHistory这两个模块帮助我们在服务端渲染。
如果我们试图在服务端像客户端一样render一个
此外,大多数应用希望使用路由帮助加载数据,所以无关异步路由,你要想知道在实际渲染之前页面将渲染什么,你得在渲染前先加载路由异步操作完成后所返回的数据。
首先我们从 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(); // 虽然还有其他方式能将 HTML 存储在模版里,但还没一种能和 React-Router 完美协作 // 所以这里只使用了一个叫 `renderPage` 的函数 res.send(renderPage(appHtml)); }) }) function renderPage(appHtml) { // 将 HTML 放到 es6 模版字符串``中,${appHtml} 占位符将 `appHtml`的值插进来 return ` My First React Router App ${appHtml}` } 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() res.send(renderPage(appHtml)) } else { // 没有错误,也没有跳转,什么都匹配不到的情况下 res.status(404).send('Not Found') } }); });
React 服务器端渲染目前还是比较新的技术,还没有最佳的实践,尤其在数据加载方面。本教程到此也结束了,希望这对你来说是个崭新的开始。
本篇示例源码:
react-router-demo-part3
本文由 前端先生 原创,欢迎转载分享,但请注明出处。