在用React来开发SPA(single page application)中,一般都会使用react router来做页面的路由,而如果应用很大的话,一般都需要使用按需加载,既当进入到某个url中时才去加载其所对应的组件,这样可以让打包后的js更小,增强初始化时的速度。那么如何来完成这种按需加载呢?

由于react router 更新到最新的v4后,和之前发生了很大的变化,因此,此篇文章我将分为两部分来分别介绍react router v4前版本和v4版本的按需加载。

首先我们来建一个项目

npm init

通过按照提示配置后,我们初始化了项目,开始安装依赖

先安装我们node运行环境需要express和body-parser

npm install express body-parser

然后安装开发需要的几个环境

npm install --save-dev babel-core babel-loader babel-preset-es2015 babel-preset-react jsx jsx-loader prop-types react react-dom reat-router@3.2.1 webpack webpack-cli

此处注意,我安装的是3.2.1的react router 这个是当前v4版本以下的最后一个版本

新建webpack打包的config配置文件

touch webpack.config.js

新建源码目录、生产版本目录

mkdir src dis

新建nodejs的服务器脚本

touch server.js

完成服务脚本

var express = require('express');
var bodyParser = require('body-parser');
var app = express();

app.use(express.static(__dirname + '/dist'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
    extended: false
}));

app.get('/articles.json', function (req, res) {
    letlist= [{
        title: 'articles 1',
        content: 'This is articles 1'
    }, {
        title: 'articles 2',
        content: 'This is articles 2'
    }, {
        title: 'articles 3',
        content: 'This is articles 3'
    }];
    res.send(JSON.stringify(list));
});

app.get('*', function (req, res) {
    res.sendFile(__dirname + '/index.html');
});

app.listen(3001, function () {
    console.log("["+ (newDate()).toLocaleString() +"]服务已经启动..")
});

server.js源码

新建一个静态页面并完成编码

<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Lesson 2</title>
        <link rel="stylesheet" href="/app.css" />
    </head>
    <body></body>
    <script type="text/javascript" src="/app.js"></script>
</html>

现在我们进入源码目录并创建组件文件夹

cd srcmkdir components

我们开始创建几个组件首先创建一个主页页面的组件components/Home.jsx

import React, { Component } from 'react';

export default class Home extends Component {
    render() {
        return (
            <div className="homepage">
                <h3>This is Home Page.</h3>
            </div>
        );
    }
}

Home.jsx源码

创建一个关于页面的组件components/About.jsx

import React, { Component } from 'react';

export default class About extends Component {
    render() {
        return (
            <div className="aboutpage">
                <h3>This is about page.</h3>
            </div>
        );
    }
}

About.jsx源码

创建一个文章组件components/Article.jsx

import React, { Component } from 'react';
import PropTypes from 'prop-types';

export default class Article extends Component {
    render() {
        return (
            <li>
                <h5>{this.props.art.title}</h5>
                <p>{this.props.art.content}</p>
            </li>
        );
    }
}

Article.propTypes = {
    art: PropTypes.shape({
        title: PropTypes.string.isRequired,
        content: PropTypes.string.isRequired
    }).isRequired
}

Article.jsx源码

创建一个文章列表页面的组件components/Articles.jsx,并在组件初始化时,通过ajax从后端获取数据

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Article from './Article';

export default class Articles extends Component {
    constructor(props) {
        super(props);
        this.state= {
            articles: []
        }
    }

    componentWillMount() {
        fetch('/articles.json', {
            method: 'GET',
            headers: {
                "Content-Type": "application/json"
            },
            credentials: "same-origin"
        }).then((response) => {
            return response.json();
        }).then((json) => {
            this.setState({
                articles: json
            });
        }).catch((err) => {
            console.error('载入文章失败');
        });
    }

    render() {
        return (
            <div className="articles">
                <h3>This is article page.</h3>
                <ul className="articlelist">
                    {this.state.articles.map((article, index) =><Article art={article} key={index}/>)}
                </ul>
            </div>
        );
    }
}
现在完成应用的组件components/App.jsx
import React, { Component } from 'react';
import { IndexLink, Link } from 'react-router';

export default class App extends Component {
    render() {
        return (
            <div className="app">
                <header>
                    <ul>
                        <li><IndexLink to="/" activeClassName="hover">Home</IndexLink></li>
                        <li><Link to="articles" activeClassName="hover">Articles</Link></li>
                        <li><Link to="about" activeClassName="hover">About Us</Link></li>
                        <div className="clearfix"></div>
                    </ul>
                </header>
                <div className="body">
                    {this.props.children}
                </div>
            </div>
        );
    }
}
最后我们来完成入口文件app.jsx
import React from 'react';
import { Router, browserHistory, Route, IndexRoute } from 'react-router';
import ReactDOM from 'react-dom';
import App from './components/App';
ReactDOM.render((
<Router history={browserHistory}>
<Route path="/"component={App}>
<IndexRoute getComponents={(location, cb) => {
require.ensure([], function (require) {
letm = require('./components/Home');
cb(null, m.default ? m.default : m);
});
}} />
<Route path="articles" getComponents={(location, cb) => {
require.ensure([], (require) => {
letm = require('./components/Articles');
cb(null, m.default ? m.default : m);
});
}} />
<Route path="about" getComponents={(location, cb) => {
require.ensure([], (require) => {
letm = require('./components/About');
cb(null, m.default ? m.default : m);
});
}} />
</Route>
</Router>)
, document.body);
此处使用了延迟加载。在React Router v4 之前,Route组件提供了一个异步加载组件的方法getComponents,在此方法内,通过require.ensure来延迟加载组件,webpack打包时,会讲延迟加载的组件做分割,在需要此组件时才载入分块的js
此时,我们来配置webpack的config文件
const webpack = require('webpack');
const modulePath = __dirname + '/node_modules/';
module.exports = {
entry: './src/app.jsx',
output: {
path: __dirname+'/dist',
publicPath: '/',
filename: 'app.js'
},
module: {
rules: [{
test: /\.jsx$/,
exclude: modulePath,
loader: [{
loader: 'jsx-loader'
}, {
loader: 'babel-loader',
query: {
presets: ['es2015', 'react']
}
}]
}]
},
plugins: [
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify('production')
}
})
],
resolve: {
extensions: ['.js', '.jsx']
}
};
现在可以使用webpack来打包文件
webpack -p
打包后可以看到,在dist下生成了多个文件
npm start
启动nodejs服务,现在可以用浏览器去查看location:3001
可以看到,访问首页时,载入了2.app.js,这是其中一个组件,也就是默认首页Home.jsx这个组件
然后访问下About Us可以看到,又载入了一个0.app.js
最后点击Articles可以看到载入了1.app.js,同时用访问了后端的articles.json来获取文章数据
这样可以看到,所有组件都是分割延迟载入的,只有需要时才会载入。
以上这些源码可以查看github去了解


下面来介绍在React Router v4下如何实现延迟载入

在v4中,安装依赖时,注意下react router需要改下版本号,当前最新为4.2.0,并且需要加入react-router-dom
npm install --save-dev babel-core babel-loader babel-preset-es2015 babel-preset-react jsx jsx-loader prop-types react react-dom reat-router@4.2.0react-router-dom webpack webpack-cli
其他组件,都没有变化,因为react router v4将不提供异步获取组件的getComponents方法接口,因此需要我们自己来写一个异步载入的组件
新建一个异步载入组件 components/AsyncLoad.jsx
import { Component } from 'react';
export default class AsyncLoad extends Component {
constructor(props) {
super(props);
this.state= {
component: null
};
}
componentWillMount() {
this.load(this.props);
}
componentWillReceiveProps(nextProps) {
if (nextProps.load!==this.props.load) {
this.load(nextProps);
}
}
load(props) {
this.setState({
component: null
});
props.load(component => {
this.setState({
component: component.default? component.default:component
});
})
}
render() {
return this.state.component?this.props.children(this.state.component) :null;
}
}
现在来说下这个组件吧,此组件将接收一个名为load的props,这个load,就是一个函数,用于异步载入需要组件的函数,而child则也是一个函数,是其渲染函数
其使用方法是这样的
let HomeAsync = (props) => (
<AsyncLoad load={(cb) => {
require.ensure([], require => {
cb(require('./Home'));
});
}}>
{(Home) =><Home {...props}/>}
</AsyncLoad>
);
当使用HomeAsync这个异步组件时,其将会通过异步组件AsyncLoadload来初始化,在渲染前的componentWillMount函数中做载入,当载入完成后,会将异步组件AsyncLoadload的state变更为刚刚通过异步载入的组件,渲染的时候则通过判断state.component是否已经载入完成,当载入完成后会渲染出来。
load方法中commpont.default ? commpont.default : commpont 是为了支持 export default 和module exports两种不同的模块导出方式
下面开始修改components/App.jsx
import React, { Component } from 'react';
import { BrowserRouter as Router, Switch, Route, Redirect, NavLink } from 'react-router-dom';
import AsyncLoad from './AsyncLoad';
let HomeAsync = (props) => (
<AsyncLoad load={(cb) => {
require.ensure([], require => {
cb(require('./Home'));
});
}}>
{(Home) =><Home {...props}/>}
</AsyncLoad>
);
let ArticlesAsync = (props) => (
<AsyncLoad load={(cb) => {
require.ensure([], require => {
cb(require('./Articles'));
});
}}>
{(Articles) =><Articles {...props}/>}
</AsyncLoad>
);
let AboutAsync = (props) => (
<AsyncLoad load={(cb) => {
require.ensure([], require => {
cb(require('./About'));
});
}}>
{(About) =><About {...props}/>}
</AsyncLoad>
);
export default class App extends Component {
render() {
return (
<Router>
<div className="app">
<header>
<ul>
<li><NavLink to="/" exact activeClassName="hover">Home</NavLink></li>
<li><NavLink to="/articles" activeClassName="hover">Articles</NavLink></li>
<li><NavLink to="/about" activeClassName="hover">About Us</NavLink></li>
<div className="clearfix"></div>
</ul>
</header>
<div className="body">
<Switch>
<Route path="/" exact component={HomeAsync}/>
<Route path="/about" component={AboutAsync}/>
<Route path="/articles" component={ArticlesAsync}/>
<Route>
<Redirect to="/"/>
</Route>
</Switch>
</div>
</div>
</Router>
);
}
}
这样将需要载入的组件都是用异步组件嵌套一下就可以实现按需载入了
修改入口app.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';
ReactDOM.render((
<App/>
), document.body);
这样在v4版本的react router 按需载入就完成了
react router v4 和之前版本变化还是很大的,强烈建议去阅读下react router v4官方文档
以上这些源码可以查看github去了解

这篇文章差不多就到这里了,如果有什么问题,可以留言给我。

react router 的按需载入方式还是很实用的,在一些小型、中型应用中都可以用到。希望这篇文章能帮到你