在用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() +"]服务已经启动..")
});
新建一个静态页面并完成编码
<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>
);
}
}
创建一个关于页面的组件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>
);
}
}
创建一个文章组件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
}
创建一个文章列表页面的组件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>
);