博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
React-Router 中文简明教程(下)
阅读量:4085 次
发布时间:2019-05-25

本文共 10956 字,大约阅读时间需要 36 分钟。

更新于 2017-1-17

http://www.mrfront.com/2016/12/30/react-router-tutorial-part3/?utm_source=tuicool&utm_medium=referral

这篇为 React-Router 教程的最后一章,前两章为:

文章大纲

本篇示例源码:

( 打开源码边看教程 可以帮助你更好的理解 )

十. 使用 browserHistory 属性让 URL 更简洁

前面章节中,我们将Router组件的history属性设为hashHistoryhistory属性用于监听切换 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 替代 hashHistoryimport { 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.jsoutput: {    path: 'public',    filename: 'bundle.js',    publicPath: '/'},

现在我们需要用 Express 创建一个生产环境的 server,在根目录下 创建server.js

// server.jsvar express = require('express')var path = require('path')var app = express()// 通过 Express 托管静态资源,比如 index.css// 访问静态资源文件时,express.static 中间件会根据目录查找所需的文件app.use(express.static(__dirname))// 设置路由规则,将所有的路由请求发送至 index.htmlapp.get('*', function (req, res) {    res.sendFile(path.join(__dirname, 'index.html'))})// 启动服务器var PORT = process.env.PORT || 8080app.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.htmlindex.css放进public

修改 server.js,将静态文件指向正确的目录:

// server.js// ...// 添加 path.joinapp.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.jsimport 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

  • React Router
  • React
  • {/* 表单 */}
  • / {' '}
    {' '}
{ this.props.children }
); }}

这里有两种解决方法,第一种比第二种更简洁。

第一种方法 是使用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-part3-result1

本篇示例源码:

十三. 服务端渲染

好吧,首先你要明白服务器端渲染的核心 在 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.jsimport 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.jsimport 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一个<Router/>,我们将会得到一个空白页,原因在于服务端渲染是同步的 路由匹配却是异步的,而客户端由于没等到异步操作完成就渲染了一次,服务端返回的数据就被丢弃了。

此外,大多数应用希望使用路由帮助加载数据,所以无关异步路由,你要想知道在实际渲染之前页面将渲染什么,你得在渲染前先加载路由异步操作完成后所返回的数据。

首先我们从 react-router 中导入matchRouterContext,然后匹配路由到 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 服务器端渲染目前还是比较新的技术,还没有最佳的实践,尤其在数据加载方面。本教程到此也结束了,希望这对你来说是个崭新的开始。

本篇源码:

本文由  原创,欢迎转载分享,但请注明出处。

标签:
你可能感兴趣的文章
【机器学习】机器学习系统SysML 阅读表
查看>>
最小费用最大流 修改的dijkstra + Ford-Fulksonff算法
查看>>
最小费用流 Bellman-Ford与Dijkstra 模板
查看>>
实现高性能纠删码引擎 | 纠删码技术详解(下)
查看>>
RS(纠删码)技术浅析及Python实现
查看>>
二叉搜索树、完全二叉树、平衡二叉树 Python实现
查看>>
深度学习的技术在金融行业中的应用
查看>>
深度学习理论实践前言CNN/RNN理解Attention理解深度学习传统领域的应用
查看>>
attention 机制 源码篇
查看>>
介绍归纳、递归
查看>>
卷积神经网络
查看>>
介绍 Keras
查看>>
python利用动态变量实时绘图的问题-读取数据库数据
查看>>
python中plot实现即时数据动态显示方法
查看>>
Python 读取数据库数据,动态绘图
查看>>
python实战二:使用CSV数据绘制带数据标志的折线图(matplotlib)
查看>>
python数据可视化(二)读取csv数据绘制 区间可视化图
查看>>
Python-Matplotlib 动态图表绘制来达到实时监控
查看>>
电脑文件命名规范与目录规划
查看>>
从0到1:实现循环神经网络Vanilla RNN(序列分析)
查看>>