从0梳理 react
单页项目搭建。css
懒惰的官网内容搬运工,如有不解,请访问官网原文。html
源码地址:github.com/zzyper/buil…前端
参考资源:node
webpack webpack.js.org/react
webpack 中文 webpack.docschina.org/webpack
babel babeljs.io/ios
babel 中文 www.babeljs.cn/nginx
从零搭建React全家桶框架教程 github.com/brickspert/…git
其余参考资源在相应章节中指出github
文章中各个 npm
包版本请参考 package.json
。
1、认识 webpack
1.1 在项目中安装使用 webpack
1.2 使用配置文件控制 webpack
复制代码
2、认识 babel
经过使用 babel 转换 ES2015+ 语法,来学习它的基本知识
复制代码
3、管理资源
3.1 趁热打铁,使用 webpack + babel 编译、打包 react 。
3.2 其余资源管理(比较简单,后续章节介绍)
复制代码
4、管理输出
4.1 使用插件 CleanWebpackPlugin (打包时清除 dist 目录下旧版本文件)
4.2 使用插件 HtmlWebpackPlugin (html 自动引入打包后的 js 文件)
复制代码
5、分离配置文件
分离开发环境与生产环境的配置文件。
复制代码
6、开发环境配置
6.1 使用 source-map
6.2 使用 WebpackDevServer (本地 server)
6.3 使用 HotModuleReplacement (热模块替换)
复制代码
7、管理资源后续
7.1 sass-loader
7.2 postcss-loader
7.3 使用插件 MiniCssExtractPlugin ,抽取 css 文件
7.4 font 处理
7.5 image 处理
复制代码
8、引入 react-router
8.1 安装使用 react-router
8.2 react-router 代码分割/按需加载
复制代码
9、引入 redux
9.1 安装使用 redux
9.2 使用 redux-sage 处理异步 action
复制代码
10、生产环境部署与配置
新建 build-react
做为项目根目录,建立 src
目录。
执行命令:
# 初始化项目,生成 package.json ( -y 指默认参数,不用在命令行输入那些信息)
npm init -y
复制代码
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
。
在 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 (ES201五、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
文件。
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
,大功告成。
关于 css
font
image
等文件处理,咱们会在第七章进行介绍,相信若是你已经理解了 loader
,并成功编译打包了 react
,那这些都不在话下。
webpack
使用插件,来作那些 loader
作不到的事。
你可能已经注意到,因为遗留了以前代码,咱们的 /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
下的文件。
细心的小伙伴已经发现,通过刚才的操做,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
的神奇了。
当 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',
复制代码
次编译代码时,手动运行
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.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')
)
}
复制代码
大功告成! :)
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
]
}
复制代码
不知道为何要用请去这里: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);
}
}
复制代码
目前咱们的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"
})
]
});
复制代码
建立 /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;
}
}
复制代码
同字体:
// 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
。
修复相关图片、字体引用路径问题。
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
文章页。
react-router
代码分割/按需加载咱们项目的全部 js
都被打包到了 bundle.js
,打开首页咱们能够发现,它加载了 bundle.js
,这其中也包含 article
页面的内容。咱们的项目文件愈来愈大时,打开首屏将会很是缓慢。
接下来咱们来尝试按需加载(代码分割)。
从 webpack
官网能够看到,代码分割/按需加载基于动态导入,react-router
也提供了相关方案。
reacttraining.com/react-route…
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
看上去十分复杂,当你慢慢尝试使用它,并不断使用,你彻底不会以为它是个复杂的技术。
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')
)
}
复制代码
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;
}
}
复制代码