从0梳理 react
单页项目搭建。
懒惰的官网内容搬运工,若有不解,请访问官网原文。
参考资源:
webpack webpack.js.org/
webpack 中文 webpack.docschina.org/
babel babeljs.io/
babel 中文 www.babeljs.cn/
从零搭建React全家桶框架教程 github.com/brickspert/…
其他参考资源在相应章节中指出
文章中各个 npm
包版本请参考 package.json
。
内容结构
一、认识 webpack
1.1 在项目中安装使用 webpack
1.2 使用配置文件控制 webpack
复制代码
二、认识 babel
通过使用 babel 转换 ES2015+ 语法,来学习它的基本知识
复制代码
三、管理资源
3.1 趁热打铁,使用 webpack + babel 编译、打包 react 。
3.2 其他资源管理(比较简单,后续章节介绍)
复制代码
四、管理输出
4.1 使用插件 CleanWebpackPlugin (打包时清除 dist 目录下旧版本文件)
4.2 使用插件 HtmlWebpackPlugin (html 自动引入打包后的 js 文件)
复制代码
五、分离配置文件
分离开发环境与生产环境的配置文件。
复制代码
六、开发环境配置
6.1 使用 source-map
6.2 使用 WebpackDevServer (本地 server)
6.3 使用 HotModuleReplacement (热模块替换)
复制代码
七、管理资源后续
7.1 sass-loader
7.2 postcss-loader
7.3 使用插件 MiniCssExtractPlugin ,抽取 css 文件
7.4 font 处理
7.5 image 处理
复制代码
八、引入 react-router
8.1 安装使用 react-router
8.2 react-router 代码分割/按需加载
复制代码
九、引入 redux
9.1 安装使用 redux
9.2 使用 redux-sage 处理异步 action
复制代码
十、生产环境部署与配置
创建项目
新建 build-react
作为项目根目录,创建 src
目录。
执行命令:
# 初始化项目,生成 package.json ( -y 指默认参数,不用在命令行输入那些信息)
npm init -y
复制代码
一、认识 webpack
1.1 在项目中安装使用 webpack
1. 在项目中安装 webpack
cnpm i webpack --save-dev
cnpm i webpack-cli --save-dev # 用于在命令行中运行 webpack
复制代码
你可能想知道 --save-dev
,--save
的区别,自行查找。
2. 在 src
下创建 index.js
// index.js
console.log('Hello webpack !');
复制代码
3. 使用 webpack
打包
./node_modules/.bin/webpack --mode production
复制代码
执行之后,我们的到了 /dist
目录,以及它下面的 main.js
文件。通过观察可以看到 main.js
尾部就是我们的 index.js
。
你可能想知道:
a. 为什么不直接使用 webpack
而是使用 ./node_modules/.bin/webpack
?
因为如果你全局安装了 `webpack` ,那么直接使用 `webpack` 执行的是你全局安装的,而不是项目下的。
复制代码
b. --mode
是什么?
webpack 4 新增的一项配置,可以用来表示当前打包环境,它会在内部根据这个参数做一些插件的默认使用配置等。
这里只是不想看到控制台打印 Warning。
复制代码
4. 在 package.json
中添加指令"build": "webpack --mode production"
,方便我们执行打包
{
"name": "build-react",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --mode production"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^4.29.6",
"webpack-cli": "^3.3.0"
}
}
复制代码
接下来,我们执行 npm run build
就相当于 ./node_modules/.bin/webpack --mode production
。
5. 从上述过程中可以看出,webpack
默认将 /src/index.js
打包到 ./dist/main.js
。现在,我们创建 loading.js
,并在 index.js
中导入它,再次执行打包。
// loading.js
export default () => {
console.log('loading');
};
复制代码
// index.js
import loading from './loading';
loading();
console.log('Hello webpack !');
复制代码
查看打包过程及结果,可以看到它们都被打包到 main.js
。
1.2 使用配置文件控制 webpack
在 webpack 4 中,可以无须任何配置使用,然而大多数项目会需要很复杂的设置,这就是为什么 webpack 仍然要支持 配置文件。这比在终端(terminal)中手动输入大量命令要高效的多,所以让我们创建一个取代以上使用 CLI 选项方式的配置文件。
1. 在项目目录下创建 webpack.config.js
:
// webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
复制代码
2. 在 package.json
下修改指令
"build": "webpack --config webpack.config.js --mode production"
复制代码
如果 webpack.config.js
存在,则 webpack
命令将默认选择使用它。我们在这里使用 --config
选项只是向你表明,可以传递任何名称的配置文件。这对于需要拆分成多个文件的复杂配置是非常有用,对于后面将要提到的分离开发/生产环境配置也十分有用。
3. 执行打包 npm run build
我们可以看到 ./dist
下产生了我们在配置文件中指定的输出文件 bundle.js
。
比起 CLI 这种简单直接的使用方式,配置文件具有更多的灵活性。我们可以通过配置方式指定 loader 规则(loader rules)、插件(plugins)、解析选项(resolve options),以及许多其他增强功能。了解更多详细信息,请查看配置文档。
上面所说的这些配置就是 webpack
的一些核心概念,在官网都有详细的解释。
二、认识 babel
我们通过使用 babel
编译 ES2015 +
jsx
等等代码,总有人认为它是 webpack
的一部分,实际上它和 webpack
并无关系,接下来我们跟着使用指南,通过编译 ES2015+
来了解它。
1. 安装核心库,使用 babel
的基石
cnpm install --save-dev @babel/core
复制代码
2. 安装使用 CLI 命令行工具
cnpm install --save-dev @babel/core @babel/cli
./node_modules/.bin/babel src --out-dir lib
复制代码
这将解析 src 目录下的所有 JavaScript 文件,并应用我们所指定的代码转换功能,然后把每个文件输出到 lib 目录下。由于我们还没有指定任何代码转换功能,所以输出的代码将与输入的代码相同(不保留原代码格式)。我们可以将我们所需要的代码转换功能作为参数传递进去。
上面的示例中我们使用了 --out-dir 参数。你可以通过 --help 参数来查看命令行工具所能接受的所有参数列表。但是现在对我们来说最重要的是 --plugins 和 --presets 这两个参数。
接下来我们讨论 --plugins 和 --presets ,及插件和预设。
3. 插件和预设(preset)
代码转换功能以插件的形式出现,插件是小型的 JavaScript 程序,用于指导 Babel 如何对代码进行转换。你甚至可以编写自己的插件将你所需要的任何代码转换功能应用到你的代码上。例如将 ES2015+ 语法转换为 ES5 语法,我们可以使用诸如 @babel/plugin-transform-arrow-functions 之类的官方插件:
cnpm install --save-dev @babel/plugin-transform-arrow-functions
./node_modules/.bin/babel src --out-dir lib --plugins=@babel/plugin-transform-arrow-functions
复制代码
我们来试试:
将 /src/index.js
内容改为
const fn = () => 1;
复制代码
执行上述命令 ./node_modules/.bin/babel src --out-dir lib --plugins=@babel/plugin-transform-arrow-functions
现在,我们代码中的所有箭头函数(arrow functions)都将被转换为 ES5 兼容的函数表达式了。
这是个好的开始!但是我们的代码中仍然残留了其他 ES2015+ 的特性,我们希望对它们也进行转换。我们不需要一个接一个地添加所有需要的插件,我们可以使用一个 "preset" (即一组预先设定的插件)。
就像插件一样,你也可以根据自己所需要的插件组合创建一个自己的 preset 并将其分享出去。J对于当前的用例而言,我们可以使用一个名称为 env 的 preset。
cnpm install --save-dev @babel/preset-env
./node_modules/.bin/babel src --out-dir lib --presets=@babel/env
复制代码
如果不进行任何配置,上述 preset 所包含的插件将支持所有最新的 JavaScript (ES2015、ES2016 等)特性。但是 preset 也是支持参数的。我们来看看另一种传递参数的方法:配置文件,而不是通过终端控制台同时传递 cli 和 preset 的参数。
4. 配置
后面我们都将使用 .babelrc
进行配置,其他方式自行了解。
5. Polyfill
@babel/polyfill 模块包括 core-js 和一个自定义的 regenerator runtime 模块用于模拟完整的 ES2015+ 环境。
这意味着你可以使用诸如 Promise 和 WeakMap 之类的新的内置组件、 Array.from 或 Object.assign 之类的静态方法、 Array.prototype.includes 之类的实例方法以及生成器函数(generator functions)(前提是你使用了 regenerator 插件)。为了添加这些功能,polyfill 将添加到全局范围(global scope)和类似 String 这样的内置原型(native prototypes)中。
对于软件库/工具的作者来说,这可能太多了。如果你不需要类似 Array.prototype.includes 的实例方法,可以使用 transform runtime 插件而不是对全局范围(global scope)造成污染的 @babel/polyfill。
更进一步,如哦你确切地指导你所需要的 polyfills 功能,你可以直接从 core-js 获取它们。
由于我们构建的是一个应用程序,因此我们只需安装 @babel/polyfill 即可:
cnpm install --save @babel/polyfill
复制代码
三、管理资源
假设你已经看了官网相关基础内容,接下来我们来结合 webpack
babel
来编译打包 jsx
文件。
3.1 趁热打铁,使用 webpack + babel 编译、打包 react 。
1. 安装 react
,创建 jsx
文件
cnpm install --save react react-dom
复制代码
// /src/app.jsx
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(
<div>Hello react!</div>,
document.getElementById('root')
);
复制代码
2. 安装 babel
用来编译 react
的预设,配置 babel
cnpm install --save-dev @babel/preset-react
复制代码
// .babelrc
{
"presets": [
"@babel/preset-react"
],
"plugins": []
}
复制代码
执行 babel
编译
./node_modules/.bin/babel src --out-dir lib
复制代码
3. 结合 webpack
编译并打包 react
假设你已经看了 webpack
官方的基础内容,你应该知道 webpack
通过各种 loader
来对不同资源进行处理,接下来我们就要使用 babel-loader
来处理 jsx
。
安装 babel-loader
cnpm install -D babel-loader
复制代码
修改 webpack
配置文件
// webpack.config.js
const path = require('path');
module.exports = {
entry: './src/app.jsx',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.jsx$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
};
复制代码
为了直观查看结果,我们在 /dist
下创建 index.html
,引入打包后的 bundle.js
。
<!DOCTYPE html>
<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>Document</title>
</head>
<body>
<div id="root"></div>
<script src="./bundle.js"></script>
</body>
</html>
复制代码
执行打包命令 npm run build
,打包结束后,在浏览器打开 index.html
,大功告成。
3.2 其他资源管理(比较简单,后续章节介绍)
关于 css
font
image
等文件处理,我们会在第七章进行介绍,相信如果你已经理解了 loader
,并成功编译打包了 react
,那这些都不在话下。
四、管理输出
webpack
使用插件,来做那些 loader
做不到的事。
4.1 使用插件 CleanWebpackPlugin (打包时清除 dist 目录下旧版本文件)
你可能已经注意到,由于遗留了之前代码,我们的 /dist
文件夹显得相当杂乱。webpack 将生成文件并放置在 /dist
文件夹中,但是它不会追踪哪些文件是实际在项目中用到的。
通常比较推荐的做法是,在每次构建前清理 /dist
文件夹,这样只会生成用到的文件。让我们实现这个需求。
clean-webpack-plugin
是一个流行的清理插件,安装和配置它。
cnpm install --save-dev clean-webpack-plugin
复制代码
// webpack.config.js
const CleanWebpackPlugin = require('clean-webpack-plugin');
plugins: [
new CleanWebpackPlugin()
]
复制代码
接下来每次执行打包都会先清除 /dist
下的文件。
4.2 使用插件 HtmlWebpackPlugin (html 自动引入打包后的 js 文件)
细心的小伙伴已经发现,经过刚才的操作,index.html
也被清除了。
我们将使用 HtmlWebpackPlugin
插件,来生成 html
并将每次打包的js自动插入到你的 index.html
里面去,而且它还可以基于你的某个 html
模板来创建最终的 index.html
。
在 src
下创建一个模板 tmpl.html
:
<!DOCTYPE html>
<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>build react</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
复制代码
安装 & 配置:
cnpm install html-webpack-plugin --save-dev
复制代码
const HtmlWebpackPlugin = require('html-webpack-plugin');
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.join(__dirname, 'src/tmpl.html')
})
]
复制代码
执行打包,我们发现,生成的 index.html
自动包含了打包后的js文件,而且也拥有 tmpl.html
模板文件中的内容。
五、分离配置文件
整理删除 lib
等无用的文件目录。
接下来我们来分离开发环境与生产环境的配置文件。
往往我们在开发过程中需要一些配置在生产环境中并不需要,反之同理,所以我们要分开配置它们。
-
创建
webpack.common.js
,我们在它其中写通用的配置; -
创建
webpack.dev.js
,开发环境的配置; -
创建
webpack.prod.js
,生产环境的配置。
为了将这些配置合并在一起,我们将使用一个名为 webpack-merge
的工具。此工具会引用 "common" 配置,因此我们不必再在环境特定(environment-specific)的配置中编写重复代码。
cnpm install --save-dev webpack-merge
复制代码
// webpack.common.js
const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/app.jsx',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.jsx$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.join(__dirname, 'src/tmpl.html')
})
]
};
复制代码
// webpack.dev.js
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'development',
});
复制代码
// webpack.prod.js
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'production',
});
复制代码
细心的同学已经发现,我们在 dev
prod
中配置了不同的 mode
,接下来我们在 package.json
中配置指令。
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --config webpack.prod.js",
"start": "webpack --config webpack.dev.js"
},
复制代码
接下来我们分别执行 开发环境 npm start
和 生产环境 npm run build
:
比较打包出来的文件体积,我想你已经有点感受到 mode
的神奇了。
六、开发环境配置
6.1 使用 source-map
当 webpack 打包源代码时,可能会很难追踪到 error(错误) 和 warning(警告) 在源代码中的原始位置。例如,如果将三个源文件(a.js
, b.js
和 c.js
)打包到一个 bundle(bundle.js
)中,而其中一个源文件包含一个错误,那么堆栈跟踪就会直接指向到 bundle.js
。你可能需要准确地知道错误来自于哪个源文件,所以这种提示这通常不会提供太多帮助。
为了更容易地追踪 error 和 warning,JavaScript 提供了 source map 功能,可以将编译后的代码映射回原始源代码。如果一个错误来自于 b.js
,source map 就会明确的告诉你。
source map 有许多 可用选项,请务必仔细阅读它们,以便可以根据需要进行配置。
在这里,我们将使用 inline-source-map
选项:
// webpack.dev.js
devtool: 'inline-source-map',
复制代码
6.2 使用 WebpackDevServer (本地 server)
次编译代码时,手动运行
npm run build
会显得很麻烦。webpack 提供几种可选方式,帮助你在代码发生变化后自动编译代码:
- webpack watch mode(webpack 观察模式)
- webpack-dev-server
- webpack-dev-middleware
多数场景中,你可能需要使用
webpack-dev-server
,但是不妨探讨一下以上的所有选项。
在这里我们只用 webpack-dev-server
, 其他两个推荐看官方文档,尤其是 webpack-dev-middleware
,为带有 node
中间层的前端应用提供了本地基石。
简单来说,webpack-dev-server
就是一个小型的静态文件服务器。使用它,可以为webpack
打包生成的资源文件提供Web服务,并且具有 live reloading(实时重新加载) 功能。
cnpm install --save-dev webpack-dev-server
复制代码
// webpack.dev.js
const path = require('path');
devServer: {
port: 8080,
contentBase: path.join(__dirname, './dist'),
historyApiFallback: true,
host: '0.0.0.0'
}
复制代码
"start": "webpack-dev-server --config webpack.config.dev.js --color --progress"
复制代码
相关配置:webpack.docschina.org/configurati…
现在我们来执行 npm start
,访问 http://0.0.0.0:8080/ ,我们拥有了本地的服务器,如果你想要同一网段下的其他机器访问你的页面,那还等什么,去查查 api
怎么配置吧!
更高级的用法,通过它代理解决开发环境访问接口跨域问题,自行查找。
6.3 使用 HotModuleReplacement (热模块替换)
通过 6.2 的内容,我们建立了开发环境本地服务器,细心的同学已经发现,当你修改 app.jsx
内容时,控制台会重新构建,网页也会同步刷新。然而,我们仅仅修改了一处文本,整个页面也会刷新。本节,我们来使用 HotModuleReplacement
来实现热更新,也就是局部内容更新,而不是刷新整个页面。
模块热替换(hot module replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新所有类型的模块,而无需完全刷新。
此功能可以很大程度提高生产效率。我们要做的就是更新 webpack-dev-server 配置,然后使用 webpack 内置的 HMR 插件。
// webpack.dev.js
const webpack = require('webpack');
devServer: {
hot: true
},
plugins: [
new webpack.HotModuleReplacementPlugin()
],
复制代码
在 app.jsx
添加代码,让它支持热模块替换:
// app.jsx
if (module.hot) {
module.hot.accept();
}
复制代码
大功告成!不不不,还太早了,还有个问题我们需要解决。。
我们先来改写 app.jsx
,并创建一个 home.jsx
文件,实现一个计数功能:
// home.jsx
import React from 'react';
export default class Home extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
}
}
render() {
return (
<div className="home">
<p>count : {this.state.count}</p>
<button onClick={() => this.setState({ count: this.state.count += 1 })}>+</button>
<button onClick={() => this.setState({ count: this.state.count -= 1 })}>-</button>
<button onClick={() => this.setState({ count: 0 })}>reset</button>
</div>
);
}
}
复制代码
// app.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import Home from './home.jsx';
ReactDOM.render(
<Home />,
document.getElementById('root')
);
if (module.hot) {
module.hot.accept();
}
复制代码
打开页面,我们通过按钮改变 state
,让它不再为 0 ,接下来我们修改 home.jsx
代码,发现 state
被初始化了。
当页面上的 state
不再是初始值,而代码内容改动,热更新会重置 state
,而不会保留,这显然不好。
为了在react
模块更新的同时,能保留state
等页面中其他状态,我们需要引入react-hot-loader~
1. 安装 react-hot-loader
cnpm install react-hot-loader --save-dev
复制代码
2. .babelrc
增加 react-hot-loader/babel
{
"presets": [
"@babel/preset-react", // 编译 react
"@babel/preset-env" // 编译 ES2015+
],
"plugins": [
"react-hot-loader/babel" // react-hot-loader
]
}
复制代码
3. webpack.dev.js
入口增加 react-hot-loader/patch
entry: [
'react-hot-loader/patch',
path.join(__dirname, './src/app.jsx')
],
复制代码
4. 修改 app.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import Home from './home.jsx';
import {AppContainer} from 'react-hot-loader';
/*初始化*/
renderWithHotReload(Home);
/*热更新*/
if (module.hot) {
module.hot.accept('./home.jsx', () => {
const Home = require('./home.jsx').default;
renderWithHotReload(Home);
});
}
function renderWithHotReload(Home) {
ReactDom.render(
<AppContainer>
<Home />
</AppContainer>,
document.getElementById('root')
)
}
复制代码
大功告成! :)
七、管理资源后续
7.1 sass-loader
cnpm install css-loader style-loader --save-dev
cnpm install sass-loader node-sass webpack --save-dev
复制代码
// webpack.common.js
{
test: /\.scss$/,
use: [
"style-loader", // creates style nodes from JS strings
"css-loader", // translates CSS into CommonJS
"sass-loader" // compiles Sass to CSS, using Node Sass by default
]
}
复制代码
7.2 postcss-loader
不知道为什么要用请去这里:github.com/brickspert/…
cnpm install --save-dev postcss-loader postcss-cssnext
复制代码
// webpack.common.js
{
test: /\.scss$/,
use: [
"style-loader",
"css-loader",
"postcss-loader",
"sass-loader"
]
}
复制代码
根目录增加postcss
配置文件。
// postcss.config.js
module.exports = {
plugins: {
'postcss-cssnext': {}
}
};
复制代码
// home.scss
.home {
p {
color: red;
font-size: 24px;
transform: scale(1.1);
}
}
复制代码
7.3 使用插件 MiniCssExtractPlugin ,抽取 css 文件
目前我们的css
是直接打包进js
里面的,我们希望能单独生成css
文件。
Webpack 4 以前,是用github.com/webpack-con…来实现的,然而:
Since webpack v4 the
extract-text-webpack-plugin
should not be used for css. Use mini-css-extract-plugininstead.
还是一样简单,但这次我们只在生产环境中使用,安装 & 使用:
(开发环境仍然使用 style-loader
,记得移除 webpack.common.js
中的配置)
cnpm install --save-dev mini-css-extract-plugin
复制代码
// webpack.prod.js
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = merge(common, {
mode: 'production',
module: {
rules: [
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader",
"postcss-loader",
"sass-loader"
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].css",
chunkFilename: "[id].css"
})
]
});
复制代码
7.4 font 处理
创建 /public/fonts
文件夹,下载字体并使用:
@font-face {
font-family: "ledbdrev";
src: url("../public/fonts/ledbdrev.ttf");
}
.home {
p {
font-family: 'ledbdrev';
color: red;
font-size: 24px;
transform: scale(1.1);
transform-origin: 0 0;
}
}
复制代码
7.5 image 处理
同字体:
// home.jsx
import React from 'react';
import './home.scss';
import codeImg from '../public/images/code.png'; // 引入图片
export default class Home extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
}
}
render() {
return (
<div className="home">
<img src={codeImg} alt="!" />
<p>count : {this.state.count}</p>
<button onClick={() => this.setState({ count: this.state.count += 1 })}>+</button>
<button onClick={() => this.setState({ count: this.state.count -= 1 })}>-</button>
<button onClick={() => this.setState({ count: 0 })}>reset</button>
</div>
);
}
}
复制代码
八、引入 react-router
react-router
是个极简单的东西,通过点击这里,你可以完全掌握它。
我们先来调整一下项目结构:
-
在
src
下创建pages
目录,在pages
下创建home
目录并把我们的home.jsx
home.scss
移入其中。pages
就作为所有页面的容器。 -
在
pages
下创建article
文章页目录及article.jsx
。 -
修复相关图片、字体引用路径问题。
8.1 安装使用 react-router
1. 安装 react-router
cnpm install --save react-router-dom
复制代码
2. 在 /src
下创建 router.js
;添加 js
文件编译;在入口 app.jsx
中引入 router.js
(替换 Home
为 Router
)。
import React, { Component } from 'react';
import { Route, Switch } from 'react-router-dom';
import Home from './pages/home/home';
import Article from './pages/home/article';
export default class Router extends Component {
render() {
return (
<div>
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/article" component={Article} />
</Switch>
</div>
)
}
}
复制代码
// webpack.common.js
{
test: /\.(js|jsx)$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
复制代码
// /src/app.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import { BrowserRouter as Router } from 'react-router-dom'
import Router from './router';
/*初始化*/
renderWithHotReload(Router);
/*热更新*/
if (module.hot) {
module.hot.accept('./router.js', () => {
const Router = require('./router.js').default;
renderWithHotReload(Router);
});
}
function renderWithHotReload(Router) {
ReactDOM.render(
<AppContainer>
<BrowserRouter>
<Router />
</BrowserRouter>
</AppContainer>,
document.getElementById('root')
)
}
复制代码
3. npm start
,在浏览器输入 url
可访问 article
文章页。
8.2 react-router
代码分割/按需加载
我们项目的所有 js
都被打包到了 bundle.js
,打开首页我们可以发现,它加载了 bundle.js
,这其中也包含 article
页面的内容。我们的项目文件越来越大时,打开首屏将会非常缓慢。
接下来我们来尝试按需加载(代码分割)。
从 webpack
官网可以看到,代码分割/按需加载基于动态导入,react-router
也提供了相关方案。
reacttraining.com/react-route…
blog.youkuaiyun.com/mjzhang1993…
1. 安装 babel
插件支持动态引入语法,修改 .babelrc
;安装 @loadable/component
cnpm i --save @babel/plugin-syntax-dynamic-import
cnpm i --save @loadable/component
复制代码
{
"presets": [
"@babel/preset-react", // 编译 react
"@babel/preset-env" // 编译 ES2015+
],
"plugins": [
"react-hot-loader/babel", // react-hot-loader
"@babel/plugin-syntax-dynamic-import"
]
}
复制代码
2. 新建 /components/loading
目录及组件,使用 @loadable/component
按需加载组件
// /components/loading.jsx
import React from 'react';
export default () => <div>Loading...</div>
复制代码
// router.js
import React, { Component } from 'react';
import { Route, Switch } from 'react-router-dom';
import loadable from '@loadable/component'
import Loading from "./components/loading.jsx";
import Home from './pages/home/home.jsx';
const Article = loadable(() => import(/* webpackChunkName: "article" */'./pages/article/article.jsx'), { fallback: <Loading /> });
export default class Router extends Component {
render() {
return (
<div>
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/article" component={Article} />
</Switch>
</div>
)
}
}
复制代码
3. 修改 webpack
配置,让输出的 js
有对应的模块名称,这里的 name
实际上是我们在动态导入时,注释传入的
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
},
复制代码
4. 执行打包,再次进入浏览器访问首页,然后只有进入文章页时才会加载 article.js
本节有个问题:react-hot-loader
热更新失效。当修改动态引入的组件时,虽然触发了热更新,但 dom
并无变化。查了 github 相同 issues 。
在解决上述问题时,搜到了另一个按需加载方案 react-loadable
,又遇到了渲染的状态是前一次的问题,原来还要自己在组件里面写 hot
。
import React, { Component } from 'react';
import { hot } from 'react-hot-loader';
class Article extends Component {
render() {
return (
<div>
<h1>文章页111</h1>
</div>
)
}
}
export default hot(module)(Article);
复制代码
然后这两种方法都能用了。。。
九、引入 redux
redux
看上去十分复杂,当你慢慢尝试使用它,并不断使用,你完全不会觉得它是个复杂的技术。
9.1 安装使用 redux
接下来我们来使用 redux
来管理应用中的所有状态。
1. 安装 redux
cnpm install --save redux react-redux
复制代码
2. 在首页下创建 action.js
reducer.js
,修改 home.jsx
代码从 redux store
中获取状态以及 dispatch action
// action.js
export const add = () => ({
type: 'HOME_ADD'
});
export const cut = () => ({
type: 'HOME_CUT'
});
export const reset = () => ({
type: 'HOME_RESET'
});
复制代码
// reducer.js
const initState = {
count: 0
};
export default (state = initState, aciton) => {
switch(aciton.type){
case 'HOME_ADD':
return {
count: state.count + 1
};
case 'HOME_CUT':
return {
count: state.count - 1
};
case 'HOME_RESET':
return {
count: 0
};
default:
return state;
}
}
复制代码
// home.jsx
import React from 'react';
import { connect } from 'react-redux';
import { add, cut, reset } from './action';
import './home.scss';
class Home extends React.Component {
constructor(props) {
super(props);
}
render() {
const { dispatch, count } = this.props;
return (
<div className="home">
<h1>这是首页!</h1>
<p>count: {count}</p>
<button onClick={() => dispatch(add())}>+</button>
<button onClick={() => dispatch(cut())}>-</button>
<button onClick={() => dispatch(reset())}>reset</button>
</div>
);
}
}
export default connect(state => state.home)(Home);
复制代码
(还记得吗? article
页面为了解决热更新问题,导出了 hot
包裹的高阶组件,而 reudx
又需要包裹 connect
,所以需要导出 export default hot(module)(connect()(Article));
)
3. 在 src
目录下创建 redux
目录,创建 redux/reducers.js
用来合并所有页面的 reducer
,创建 redux/store.js
生成 store
,最后修改入口 app.jsx
从 redux store
获取状态
// reducers.js
import {combineReducers} from "redux";
import home from '../pages/home/reducer';
export default combineReducers({
home,
});
复制代码
// store.js
import { createStore } from 'redux';
import combineReducers from './reducers.js';
let store = createStore(
combineReducers,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
export default store;
复制代码
// app.jsx
// /src/app.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import { BrowserRouter } from 'react-router-dom'
import { Provider } from 'react-redux';
import store from './redux/store';
import Router from './router';
/*初始化*/
renderWithHotReload(Router);
/*热更新*/
if (module.hot) {
module.hot.accept('./router.js', () => {
const Router = require('./router.js').default;
renderWithHotReload(Router);
});
}
function renderWithHotReload(Router) {
ReactDOM.render(
<AppContainer>
<Provider store={store}>
<BrowserRouter>
<Router />
</BrowserRouter>
</Provider>
</AppContainer>,
document.getElementById('root')
)
}
复制代码
9.2 使用 redux-saga
中间件处理异步 action
许多场景下,我们需要异步的 action
,比如服务器请求。我们以此为例,使用一个 redux
中间件 redux-saga
来处理异步 action
。
1. 安装 redux-saga
cnpm install --save redux-saga
复制代码
2. 根目录创建一个 test-server.js
的 node
文件,用来模拟服务器返回数据
// test-server.js
// 加载 HTTP 模块
const http = require("http");
const hostname = '127.0.0.1';
const port = 5000;
// 创建 HTTP 服务器
const server = http.createServer((req, res) => {
// 用 HTTP 状态码和内容类型(Content-Type)设置 HTTP 响应头
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.setHeader('Access-Control-Allow-Origin', '*');
// 发送响应体
res.end(JSON.stringify({
code: 1,
content: `
静夜思
床前看月光,疑是地上霜。
抬头望山月,低头思故乡。
`
}));
});
// 监听 5000 端口的请求,注册一个回调函数记录监听开始
server.listen(port, hostname, () => {
console.log(`服务器运行于 http://${hostname}:${port}/`);
});
复制代码
3. /src
下新建一个 service
目录,用来存放各个页面的数据请求,创建 service/article.js
4. 引入 axios
发送请求
cnpm install --save axios
复制代码
// service/article.js
import axios from 'axios';
export const getArticleData = (id) => axios.get(`http://127.0.0.1:5000/?id=${id}`);
复制代码
5. 在 article
页面创建请求用的 action
reducer
以及 saga.js
,并在 reducers
中添加 article
的 reducer
// action.js
export const reqArticleData = id => ({
type: 'ARTICLE_GET_DATA',
id
});
export const setArticleData = content => ({
type: 'ARTICLE_SET_DATA',
content
});
export const reqFail = () => ({
type: 'ARTICLE_REQ_FAIL'
});
复制代码
// reducer.js
const initState = {
loading: true,
content: ''
};
export default (state = initState, action) => {
switch(action.type){
case 'ARTICLE_GET_DATA':
return {
...state,
loading: true
}
case 'ARTICLE_SET_DATA':
return {
...state,
content: action.content,
loading: false
}
case 'ARTICLE_REQ_FAIL':
return {
...state,
loading: false
}
default:
return state;
}
}
复制代码
// saga.js
import { put, takeLatest } from 'redux-saga/effects';
import { getArticleData } from '../../service/article';
import { setArticleData, reqFail } from './action';
function* reqArticleData(action) {
try {
const data = yield getArticleData(action.id);
if (data.data.code === 1) {
yield put(setArticleData(data.data.content));
} else {
yield put(reqFail());
}
} catch (e) {
yield put(reqFail());
}
}
function* articleSaga() {
yield takeLatest("ARTICLE_GET_DATA", reqArticleData);
}
export default articleSaga;
复制代码
// article.jsx
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { hot } from 'react-hot-loader';
import { reqArticleData } from './action';
import Loading from '../../components/loading.jsx';
class Article extends Component {
componentDidMount() {
this.props.dispatch(reqArticleData());
}
render() {
return (
<div>
<h1>文章页</h1>
{
this.props.loading
?
<Loading />
:
<pre>{this.props.content}</pre>
}
</div>
)
}
}
export default hot(module)(connect(state => state.article)(Article));
复制代码
6. 在 store.js
中使用 article/saga.js
,添加 babel-runtime
// store.js
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga'
import combineReducers from './reducers';
import articleSage from '../pages/article/saga';
const sagaMiddleware = createSagaMiddleware();
let store = createStore(
combineReducers,
applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(articleSage)
export default store;
复制代码
node test-server.js # 启动模拟返回数据
npm start
复制代码
打开文章页:
十、生产环境部署与配置
执行 npm run build
将打包出的 /dist
中的静态资源部署到服务器,配置 nginx
访问即可,若遇到二级页面404的问题,参考如下:
server {
server_name xxx.xxxxxx.com;
location / {
root /xxx/xxx/xxx/www/build;
try_files $uri /index.html;
}
}
复制代码