从零搭建React全家桶框架教程
源码地址:https://github.com/brickspert/react-family 欢迎star
提问反馈:blog
原文地址:https://github.com/brickspert/blog/issues/1(github这里我会不断更新教程的)
此处不更新,github上会一直更新
写在前面
当我第一次跟着项目做react
项目的时候,由于半截加入的,对框架了解甚少,只能跟着别人的样板写。对整个框架没有一点了解。
做项目,总是要解决各种问题的,所以每个地方都需要去了解,但是对整个框架没有一个整体的了解,实在是不行。
期间,我也跟着别人的搭建框架的教程一步一步的走,但是经常因为自己太菜,走不下去。在经过各种蹂躏之后,对整个框架也有一个大概的了解,
我就想把他写下来,让后来的菜鸟能跟着我的教程对react
全家桶有一个全面的认识。
我的这个教程,从新建根文件夹开始,到成型的框架,每个文件为什么要建立?建立了干什么?每个依赖都是干什么的?一步一步写下来,供大家学习。
当然,这个框架我以后会一直维护的,也希望大家能一起来完善这个框架,如果您有任何建议,欢迎留言,欢迎fork
。
在完善本框架的同时,我准备再新建一个兼容ie8
的框架react-family-ie8
,当然是基于该框架改造的。
说明
- 每个命令行块都是以根目录为基础的。例如下面命令行块,都是基于根目录的。
- cd src/pages
- mkdir Home
- 技术栈均是目前最新的。
- react 15.6.1
- react-router-dom 4.2.2
- redux 3.7.2
- webpack 3.5.5
- 目录说明
- │ .babelrc #babel配置文件
- │ package-lock.json
- │ package.json
- │ README.MD
- │ webpack.config.js #webpack生产配置文件
- │ webpack.dev.config.js #webpack开发配置文件
- │
- ├─dist
- ├─public #公共资源文件
- └─src #项目源码
- │ index.html #index.html模板
- │ index.js #入口文件
- │
- ├─component #组建库
- │ └─Hello
- │ Hello.js
- │
- ├─pages #页面目录
- │ ├─Counter
- │ │ Counter.js
- │ │
- │ ├─Home
- │ │ Home.js
- │ │
- │ ├─Page1
- │ │ │ Page1.css #页面样式
- │ │ │ Page1.js
- │ │ │
- │ │ └─images #页面图片
- │ │ brickpsert.jpg
- │ │
- │ └─UserInfo
- │ UserInfo.js
- │
- ├─redux
- │ │ reducers.js
- │ │ store.js
- │ │
- │ ├─actions
- │ │ counter.js
- │ │ userInfo.js
- │ │
- │ ├─middleware
- │ │ promiseMiddleware.js
- │ │
- │ └─reducers
- │ counter.js
- │ userInfo.js
- │
- └─router #路由文件
- Bundle.js
- router.js
init项目
webpack
- 安装
webpack
npm install --save-dev webpack
Q: 什么时候用
--save-dev
,什么时候用--save
?A:
--save-dev
是你开发时候依赖的东西,--save
是你发布之后还依赖的东西。看这里 -
新建
webpack
开发配置文件touch webpack.dev.config.js
webpack.dev.config.js
- 学会使用
webpack
编译文件新建入口文件
mkdir src && touch ./src/index.js
document.getElementById('app').innerHTML = "Webpack works"
现在我们执行命令
webpack --config webpack.dev.config.js
-
现在我们测试下~
dist
文件夹下面新建一个index.html
touch ./dist/index.html
dist/index.html
填写内容- <!doctype html>
- <html lang="en">
- <head>
- <Meta charset="UTF-8">
- <title>Document</title>
- </head>
- <body>
- <div id="app"></div>
- <script type="text/javascript" src="./bundle.js" charset="utf-8"></script>
- </body>
- </html>
用浏览器打开
index.html
,可以看到Webpack works
!现在回头看下,我们做了什么或者说
webpack
做了什么。
babel
Babel 把用最新标准编写的 JavaScript 代码向下编译成可以在今天随处可用的版本。 这一过程叫做“源码到源码”编译, 也被称为转换编译。
通俗的说,就是我们可以用ES6,ES7等来编写代码,Babel会把他们统统转为ES5。
- babel-core 调用Babel的API进行转码
- babel-loader
- babel-preset-es2015 用于解析 ES6
- babel-preset-react 用于解析 JSX
- babel-preset-stage-0 用于解析 ES7 提案
npm install --save-dev babel-core babel-loader babel-preset-es2015 babel-preset-react babel-preset-stage-0
新建babel
配置文件.babelrc
touch .babelrc
.babelrc
- {
- "presets": [
- "es2015","react","stage-0"
- ],"plugins": []
- }
修改webpack.dev.config.js
,增加babel-loader
!
现在我们简单测试下,是否能正确转义ES6~
修改 src/index.js
- /*使用es6的箭头函数*/
- var func = str => {
- document.getElementById('app').innerHTML = str;
- };
- func('我现在在使用Babel!');
执行打包命令webpack --config webpack.dev.config.js
浏览器打开index.html
,我们看到正确输出了我现在在使用Babel!
然后我们打开打包后的bundle.js
,翻页到最下面,可以看到箭头函数被转换成普通函数了!
Q: babel-preset-state-0
,babel-preset-state-1
,babel-preset-state-2
,babel-preset-state-3
有什么区别?
A: 每一级包含上一级的功能,比如 state-0
包含state-1
的功能,以此类推。state-0
功能最全。具体可以看这篇文章:babel配置-各阶段的stage的区别
参考地址:
react
npm install --save react react-dom
修改 src/index.js
使用react
- import React from 'react';
- import ReactDom from 'react-dom';
- ReactDom.render(
- <div>Hello React!</div>,document.getElementById('app'));
执行打包命令webpack --config webpack.dev.config.js
打开index.html
看效果。
我们简单做下改进,把Hello React
放到组件里面。体现组件化~
- cd src
- mkdir component
- cd component
- mkdir Hello
- cd Hello
- touch Hello.js
按照React语法,写一个Hello组件
- import React,{Component} from 'react';
- export default class Hello extends Component {
- render() {
- return (
- <div>
- Hello,React!
- </div>
- )
- }
- }
然后让我们修改src/index.js
,引用Hello
组件!
src/index.js
- import React from 'react';
- import ReactDom from 'react-dom';
- import Hello from './component/Hello/Hello';
- ReactDom.render(
- <Hello/>,document.getElementById('app'));
在根目录执行打包命令
webpack --config webpack.dev.config.js
打开index.html
看效果咯~
命令优化
Q:每次打包都得在根目录执行这么一长串命令webpack --config webpack.dev.config.js
,能不打这么长吗?
A:修改package.json
里面的script
,增加dev-build
。
package.json
- "scripts": {
- "test": "echo \"Error: no test specified\" && exit 1","dev-build": "webpack --config webpack.dev.config.js"
- }
现在我们打包只需要执行npm start-build
就可以啦!
参考地址:
http://www.ruanyifeng.com/blo...
react-router
npm install --save react-router-dom
新建router
文件夹和组件
- cd src
- mkdir router && touch router/router.js
按照react-router
文档编辑一个最基本的router.js
。包含两个页面home
和page1
。
src/router/router.js
- import React from 'react';
- import {BrowserRouter as Router,Route,Switch,Link} from 'react-router-dom';
- import Home from '../pages/Home/Home';
- import Page1 from '../pages/Page1/Page1';
- const getRouter = () => (
- <Router>
- <div>
- <ul>
- <li><Link to="/">首页</Link></li>
- <li><Link to="/page1">Page1</Link></li>
- </ul>
- <Switch>
- <Route exact path="/" component={Home}/>
- <Route path="/page1" component={Page1}/>
- </Switch>
- </div>
- </Router>
- );
- export default getRouter;
- cd src
- mkdir pages
新建两个页面 Home
,Page1
- cd src/pages
- mkdir Home && touch Home/Home.js
- mkdir Page1 && touch Page1/Page1.js
填充内容:
src/pages/Home/Home.js
- import React,{Component} from 'react';
- export default class Home extends Component {
- render() {
- return (
- <div>
- this is home~
- </div>
- )
- }
- }
Page1.js
- import React,{Component} from 'react';
- export default class Page1 extends Component {
- render() {
- return (
- <div>
- this is Page1~
- </div>
- )
- }
- }
现在路由和页面建好了,我们在入口文件src/index.js
引用Router。
修改src/index.js
- import React from 'react';
- import ReactDom from 'react-dom';
- import getRouter from './router/router';
- ReactDom.render(
- getRouter(),document.getElementById('app'));
现在执行打包命令npm start-build
。打开index.html
查看效果啦!
那么问题来了~我们发现点击‘首页’和‘Page1’没有反应。不要惊慌,这是正常的。
我们之前一直用这个路径访问index.html
,类似这样:file:///F:/react/react-family/dist/index.html
。
这种路径了,不是我们想象中的路由那样的路径http://localhost:3000
~我们需要配置一个简单的WEB服务器,指向index.html
~有下面两种方法来实现
-
Nginx
,Apache
,IIS
等配置启动一个简单的的WEB服务器。 - 使用
webpack-dev-server
来配置启动WEB服务器。
下一节,我们来使用第二种方法启动服务器。这一节的DEMO,先放这里。
参考地址
webpack-dev-server
简单来说,webpack-dev-server
就是一个小型的静态文件服务器。使用它,可以为webpack
打包生成的资源文件提供Web服务。
npm install webpack-dev-server --save-dev
修改webpack.dev.config.js
,增加webpack-dev-server
的配置。
webpack.dev.config.js
- devServer: {
- contentBase: path.join(__dirname,'./dist')
- }
现在执行
webpack-dev-server --config webpack.dev.config.js
浏览器打开http://localhost:8080,OK,现在我们可以点击首页
,Page1
了,
看URL地址变化啦!我们看到react-router
已经成功了哦。
Q: --content-base
是什么?
A:URL的根目录。如果不设定的话,默认指向项目根目录。
**重要提示:webpack-dev-server编译后的文件,都存储在内存中,我们并不能看见的。你可以删除之前遗留的文件dist/bundle.js
,
仍然能正常打开网站!**
每次执行webpack-dev-server --config webpack.dev.config.js
,要打很长的命令,我们修改package.json
,增加script->start
:
- "scripts": {
- "test": "echo \"Error: no test specified\" && exit 1","dev-build": "webpack --config webpack.dev.config.js","start": "webpack-dev-server --config webpack.dev.config.js"
- }
下次执行npm start
就可以了。
既然用到了webpack-dev-server
,我们就看看它的其他的配置项。
看了之后,发现有几个我们可以用的。
- color(CLI only)
console
中打印彩色日志 - historyApiFallback 任意的
404
响应都被替代为index.html
。有什么用呢?你现在运行npm start
,然后打开浏览器,访问http://localhost:8080
,然后点击Page1
到链接http://localhost:8080/page1
,
然后刷新页面试试。是不是发现刷新后404
了。为什么?dist
文件夹里面并没有page1.html
,当然会404
了,所以我们需要配置historyApiFallback
,让所有的404
定位到index.html
。
- host 指定一个
host
,默认是localhost
。如果你希望服务器外部可以访问,指定如下:host: "0.0.0.0"
。比如你用手机通过IP访问。 - hot 启用
Webpack
的模块热替换特性。关于热模块替换,我下一小节专门讲解一下。 - port 配置要监听的端口。默认就是我们现在使用的
8080
端口。 - proxy 代理。比如在
localhost:3000
上有后端服务的话,你可以这样启用代理:
- proxy: {
- "/api": "http://localhost:3000"
- }
- progress(CLI only) 将编译进度输出到控制台。
根据这几个配置,修改下我们的webpack-dev-server
的配置~
webpack.dev.config.js
- devServer: {
- contentBase: path.join(__dirname,historyApiFallback: true,host: '0.0.0.0'
- }
CLI ONLY
的需要在命令行中配置
package.json
- "dev": "webpack-dev-server --config webpack.dev.config.js --color --progress"
现在我们执行npm start
看看效果。是不是看到打包的时候有百分比进度?在http://localhost:8080/page1
页面刷新是不是没问题了?
用手机通过局域网IP是否可以访问到网站?
参考地址:
模块热替换(Hot Module Replacement)
到目前,当我们修改代码的时候,浏览器会自动刷新,不信你可以去试试。(如果你的不会刷新,看看这个调整文本编辑器)
我相信看这个教程的人,应该用过别人的框架。我们在修改代码的时候,浏览器不会刷新,只会更新自己修改的那一块。我们也要实现这个效果。
我们看下webpack模块热替换教程。
我们接下来要这么修改
package.json
增加 --hot
- "dev": "webpack-dev-server --config webpack.dev.config.js --color --progress --hot"
src/index.js
增加module.hot.accept()
,如下。当模块更新的时候,通知index.js
。
src/index.js
- import React from 'react';
- import ReactDom from 'react-dom';
- import getRouter from './router/router';
- if (module.hot) {
- module.hot.accept();
- }
- ReactDom.render(
- getRouter(),document.getElementById('app'));
现在我们执行npm start
,打开浏览器,修改Home.js
,看是不是不刷新页面的情况下,内容更新了?惊不惊喜?意不意外?
做模块热替换,我们只改了几行代码,非常简单的。纸老虎一个~
现在我需要说明下我们命令行使用的--hot
,可以通过配置webpack.dev.config.js
来替换,
向文档上那样,修改下面三处。但我们还是用--hot
吧。下面的方式我们知道一下就行,我们不用。同样的效果。
- const webpack = require('webpack');
- devServer: {
- hot: true
- }
- plugins:[
- new webpack.HotModuleReplacementPlugin()
- ]
HRM
配置其实有两种方式,一种CLI
方式,一种Node.js API
方式。我们用到的就是CLI
方式,比较简单。Node.js API
方式,就是建一个server.js
等等,网上大部分教程都是这种方式,这里不做讲解了。
你以为模块热替换到这里就结束了?no~no~no~
上面的配置对react
模块的支持不是很好哦。
例如下面的demo
,当模块热替换的时候,state
会重置,这不是我们想要的。
src/pages/Home/Home.js
- import React,{Component} from 'react';
- export default class Home extends Component {
- constructor(props) {
- super(props);
- this.state = {
- count: 0
- }
- }
- _handleClick() {
- this.setState({
- count: ++this.state.count
- });
- }
- render() {
- return (
- <div>
- this is home~<br/>
- 当前计数:{this.state.count}<br/>
- <button onClick={() => this._handleClick()}>自增</button>
- </div>
- )
- }
- }
你可以测试一下,当我们修改代码的时候,webpack
在更新页面的时候,也把count
初始为0了。
为了在react
模块更新的同时,能保留state
等页面中其他状态,我们需要引入react-hot-loader~
Q: 请问webpack-dev-server
与react-hot-loader
两者的热替换有什么区别?
A: 区别在于webpack-dev-serve
r自己的--hot
模式只能即时刷新页面,但状态保存不住。因为React
有一些自己语法(JSX)是HotModuleReplacementPlugin
搞不定的。
而react-hot-loader
在--hot
基础上做了额外的处理,来保证状态可以存下来。(来自segmentfault)
下面我们来加入react-hot-loader v3
,
安装依赖
npm install react-hot-loader@next --save-dev
-
.babelrc
增加react-hot-loader/babel
.babelrc
- {
- "presets": [
- "es2015","stage-0"
- ],"plugins": [
- "react-hot-loader/babel"
- ]
- }
-
webpack.dev.config.js
入口增加react-hot-loader/patch
webpack.dev.config.js
- entry: [
- 'react-hot-loader/patch',path.join(__dirname,'src/index.js')
- ]
-
src/index.js
修改如下
src/index.js
- import React from 'react';
- import ReactDom from 'react-dom';
- import {AppContainer} from 'react-hot-loader';
- import getRouter from './router/router';
- /*初始化*/
- renderWithHotReload(getRouter());
- /*热更新*/
- if (module.hot) {
- module.hot.accept('./router/router',() => {
- const getRouter = require('./router/router').default;
- renderWithHotReload(getRouter());
- });
- }
- function renderWithHotReload(RootElement) {
- ReactDom.render(
- <AppContainer>
- {RootElement}
- </AppContainer>,document.getElementById('app')
- )
- }
现在,执行npm start
,试试。是不是修改页面的时候,state
不更新了?
参考文章:
文件路径优化
做到这里,我们简单休息下。做下优化~
在之前写的代码中,我们引用组件,或者页面时候,写的是相对路径~
比如src/router/router.js
里面,引用Home.js
的时候就用的相对路径
- import Home from '../pages/Home/Home';
webpack提供了一个别名配置,就是我们无论在哪个路径下,引用都可以这样
- import Home from 'pages/Home/Home';
下面我们来配置下,修改webpack.dev.config.js
,增加别名~
webpack.config.js
- resolve: {
- alias: {
- pages: path.join(__dirname,'src/pages'),component: path.join(__dirname,'src/component'),router: path.join(__dirname,'src/router')
- }
- }
然后我们把之前使用的绝对路径统统改掉。
src/router/router.js
- import Home from 'pages/Home/Home';
- import Page1 from 'pages/Page1/Page1';
src/index.js
- import getRouter from 'router/router';
我们这里约定,下面,我们会默认配置需要的别名路径,不再做重复的讲述哦。
redux
接下来,我们就要就要就要集成redux
了。
要对redux
有一个大概的认识,可以阅读阮一峰前辈的Redux 入门教程(一):基本用法
如果要对redux
有一个非常详细的认识,我推荐阅读中文文档,写的非常好。读了这个教程,有一个非常深刻的感觉,redux
并没有任何魔法。
不要被各种关于 reducers,middleware,store 的演讲所蒙蔽 ---- Redux 实际是非常简单的。
当然,我这篇文章是写给新手的,如果看不懂上面的文章,或者不想看,没关系。先会用,多用用就知道原理了。
开始整代码!我们就做一个最简单的计数器。自增,自减,重置。
先安装redux
npm install --save redux
初始化目录结构
- cd src
- mkdir redux
- cd redux
- mkdir actions
- mkdir reducers
- touch reducers.js
- touch store.js
- touch actions/counter.js
- touch reducers/counter.js
先来写action
创建函数。通过action
创建函数,可以创建action
~src/redux/actions/counter.js
- /*action*/
- export const INCREMENT = "counter/INCREMENT";
- export const DECREMENT = "counter/DECREMENT";
- export const RESET = "counter/RESET";
- export function increment() {
- return {type: INCREMENT}
- }
- export function decrement() {
- return {type: DECREMENT}
- }
- export function reset() {
- return {type: RESET}
- }
再来写reducer
,reducer
是一个纯函数,接收action
和旧的state
,生成新的state
.
src/redux/reducers/counter.js
- import {INCREMENT,DECREMENT,RESET} from '../actions/counter';
- /*
- * 初始化state
- */
- const initState = {
- count: 0
- };
- /*
- * reducer
- */
- export default function reducer(state = initState,action) {
- switch (action.type) {
- case INCREMENT:
- return {
- count: state.count + 1
- };
- case DECREMENT:
- return {
- count: state.count - 1
- };
- case RESET:
- return {count: 0};
- default:
- return state
- }
- }
一个项目有很多的reducers
,我们要把他们整合到一起
src/redux/reducers.js
- import counter from './reducers/counter';
- export default function combineReducers(state = {},action) {
- return {
- counter: counter(state.counter,action)
- }
- }
到这里,我们必须再理解下一句话。
reducer
就是纯函数,接收state
和 action
,然后返回一个新的 state
。
看看上面的代码,无论是combineReducers
函数也好,还是reducer
函数也好,都是接收state
和action
,
返回更新后的state
。区别就是combineReducers
函数是处理整棵树,reducer
函数是处理树的某一点。
接下来,我们要创建一个store
。
前面我们可以使用 action
来描述“发生了什么”,使用action
创建函数来返回action
。
还可以使用 reducers
来根据 action
更新 state
。
那我们如何提交action
?提交的时候,怎么才能触发reducers
呢?
store
就是把它们联系到一起的对象。store
有以下职责:
- 维持应用的
state
; - 提供
getState()
方法获取state
; - 提供
dispatch(action)
触发reducers
方法更新state
; - 通过
subscribe(listener)
注册监听器; - 通过
subscribe(listener)
返回的函数注销监听器。
src/redux/store.js
- import {createStore} from 'redux';
- import combineReducers from './reducers.js';
- let store = createStore(combineReducers);
- export default store;
到现在为止,我们已经可以使用redux
了~
下面我们就简单的测试下
- cd src
- cd redux
- touch testRedux.js
src/redux/testRedux.js
- import {increment,decrement,reset} from './actions/counter';
- import store from './store';
- // 打印初始状态
- console.log(store.getState());
- // 每次 state 更新时,打印日志
- // 注意 subscribe() 返回一个函数用来注销监听器
- let unsubscribe = store.subscribe(() =>
- console.log(store.getState())
- );
- // 发起一系列 action
- store.dispatch(increment());
- store.dispatch(decrement());
- store.dispatch(reset());
- // 停止监听 state 更新
- unsubscribe();
当前文件夹执行命令
- webpack testRedux.js build.js
- node build.js
是不是看到输出了state
变化?
- { counter: { count: 0 } }
- { counter: { count: 1 } }
- { counter: { count: 0 } }
- { counter: { count: 0 } }
做这个测试,就是为了告诉大家,redux
和react
没关系,虽说他俩能合作。
到这里,我建议你再理下redux
的数据流,看看这里。
- 调用
store.dispatch(action)
提交action
。 -
redux store
调用传入的reducer
函数。把当前的state
和action
传进去。 - 根
reducer
应该把多个子reducer
输出合并成一个单一的state
树。 -
Redux store
保存了根reducer
返回的完整state
树。
就是酱紫~~
这会webpack.dev.config.js
路径别名增加一下,后面好写了。
webpack.config.js
- alias: {
- ...
- actions: path.join(__dirname,'src/redux/actions'),reducers: path.join(__dirname,'src/redux/reducers'),redux: path.join(__dirname,'src/redux')
- }
把前面的相对路径都改改。
下面我们开始搭配react
使用。
写一个Counter
页面
- cd src/pages
- mkdir Counter
- touch Counter/Counter.js
src/pages/Counter/Counter.js
- import React,{Component} from 'react';
- export default class Counter extends Component {
- render() {
- return (
- <div>
- <div>当前计数为(显示redux计数)</div>
- <button onClick={() => {
- console.log('调用自增函数');
- }}>自增
- </button>
- <button onClick={() => {
- console.log('调用自减函数');
- }}>自减
- </button>
- <button onClick={() => {
- console.log('调用重置函数');
- }}>重置
- </button>
- </div>
- )
- }
- }
src/router/router.js
- import React from 'react';
- import {BrowserRouter as Router,Link} from 'react-router-dom';
- import Home from 'pages/Home/Home';
- import Page1 from 'pages/Page1/Page1';
- import Counter from 'pages/Counter/Counter';
- const getRouter = () => (
- <Router>
- <div>
- <ul>
- <li><Link to="/">首页</Link></li>
- <li><Link to="/page1">Page1</Link></li>
- <li><Link to="/counter">Counter</Link></li>
- </ul>
- <Switch>
- <Route exact path="/" component={Home}/>
- <Route path="/page1" component={Page1}/>
- <Route path="/counter" component={Counter}/>
- </Switch>
- </div>
- </Router>
- );
- export default getRouter;
npm start
看看效果。
下一步,我们让Counter
组件和Redux
联合起来。使Counter
能获得到Redux
的state
,并且能发射action
。
当然我们可以使用刚才测试testRedux
的方法,手动监听~手动引入store
~但是这肯定很麻烦哦。
react-redux
提供了一个方法connect
。
容器组件就是使用 store.subscribe() 从 Redux state 树中读取部分数据,并通过 props 来把这些数据提供给要渲染的组件。你可以手工来开发容器组件,但建议使用 React Redux 库的 connect() 方法来生成,这个方法做了性能优化来避免很多不必要的重复渲染。
connect
接收两个参数,一个mapStateToProps
,就是把redux
的state
,转为组件的Props
,还有一个参数是mapDispatchToprops
,
就是把发射actions
的方法,转为Props
属性函数。
先来安装react-redux
npm install --save react-redux
src/pages/Counter/Counter.js
- import React,{Component} from 'react';
- import {increment,reset} from 'actions/counter';
- import {connect} from 'react-redux';
- class Counter extends Component {
- render() {
- return (
- <div>
- <div>当前计数为{this.props.counter.count}</div>
- <button onClick={() => this.props.increment()}>自增
- </button>
- <button onClick={() => this.props.decrement()}>自减
- </button>
- <button onClick={() => this.props.reset()}>重置
- </button>
- </div>
- )
- }
- }
- const mapStateToProps = (state) => {
- return {
- counter: state.counter
- }
- };
- const mapDispatchToProps = (dispatch) => {
- return {
- increment: () => {
- dispatch(increment())
- },decrement: () => {
- dispatch(decrement())
- },reset: () => {
- dispatch(reset())
- }
- }
- };
- export default connect(mapStateToProps,mapDispatchToProps)(Counter);
下面我们要传入store
所有容器组件都可以访问 Redux store,所以可以手动监听它。一种方式是把它以 props 的形式传入到所有容器组件中。但这太麻烦了,因为必须要用 store 把展示组件包裹一层,仅仅是因为恰好在组件树中渲染了一个容器组件。
建议的方式是使用指定的 React Redux 组件 <Provider> 来 魔法般的 让所有容器组件都可以访问 store,而不必显示地传递它。只需要在渲染根组件时使用即可。
src/index.js
- import React from 'react';
- import ReactDom from 'react-dom';
- import {AppContainer} from 'react-hot-loader';
- import {Provider} from 'react-redux';
- import store from './redux/store';
- import getRouter from 'router/router';
- /*初始化*/
- renderWithHotReload(getRouter());
- /*热更新*/
- if (module.hot) {
- module.hot.accept('./router/router',() => {
- const getRouter = require('router/router').default;
- renderWithHotReload(getRouter());
- });
- }
- function renderWithHotReload(RootElement) {
- ReactDom.render(
- <AppContainer>
- <Provider store={store}>
- {RootElement}
- </Provider>
- </AppContainer>,document.getElementById('app')
- )
- }
到这里我们就可以执行npm start
,打开localhost:8080/counter看效果了。
但是你发现npm start
一直报错
- ERROR in ./node_modules/react-redux/es/connect/mapDispatchToProps.js
- Module not found: Error: Can't resolve 'redux' in 'F:\Project\react\react-family\node_modules\react-redux\es\connect'
- ERROR in ./src/redux/store.js
- Module not found: Error: Can't resolve 'redux' in 'F:\Project\react\react-family\src\redux'
WTF?这个错误困扰了半天。我说下为什么造成这个错误。我们引用redux
的时候这样用的
import {createStore} from 'redux'
然而,我们在webapck.dev.config.js
里面这样配置了
- resolve: {
- alias: {
- ...
- redux: path.join(__dirname,'src/redux')
- }
- }
然后webapck
编译的时候碰到redux
都去src/redux
去找了。但是找不到啊。所以我们把webpack.dev.config.js
里面redux
这一行删除了,就好了。
并且把使用我们自己使用redux
文件夹的地方改成相对路径哦。
现在你可以npm start
去看效果了。
这里我们再缕下(可以读React 实践心得:react-redux 之 connect 方法详解)
-
Provider
组件是让所有的组件可以访问到store
。不用手动去传。也不用手动去监听。 -
connect
函数作用是从Redux state
树中读取部分数据,并通过 props 来把这些数据提供给要渲染的组件。也传递dispatch(action)
函数到props
。
接下来,我们要说异步action
参考地址: @L_502_32@
- 请求开始的时候,界面转圈提示正在加载。
isLoading
置为true
。 - 请求成功,显示数据。
isLoading
置为false
,data
填充数据。 - 请求失败,显示失败。
isLoading
置为false
,显示错误信息。
- 我们先创建一个
user.json
,等会请求用,相当于后台的API接口。
- cd dist
- mkdir api
- cd api
- touch user.json
dist/api/user.json
- {
- "name": "brickspert","intro": "please give me a star"
- }
- 创建必须的
action
创建函数。
- cd src/redux/actions
- touch userInfo.js
src/redux/actions/getUserInfo.js
- export const GET_USER_INFO_REQUEST = "userInfo/GET_USER_INFO_REQUEST";
- export const GET_USER_INFO_SUCCESS = "userInfo/GET_USER_INFO_SUCCESS";
- export const GET_USER_INFO_FAIL = "userInfo/GET_USER_INFO_FAIL";
- function getUserInfoRequest() {
- return {
- type: GET_USER_INFO_REQUEST
- }
- }
- function getUserInfoSuccess(userInfo) {
- return {
- type: GET_USER_INFO_SUCCESS,userInfo: userInfo
- }
- }
- function getUserInfoFail() {
- return {
- type: GET_USER_INFO_FAIL
- }
- }
我们创建了请求中,请求成功,请求失败三个action
创建函数。
- 创建
reducer
再强调下,reducer
是根据state
和action
生成新state
的纯函数。
- cd src/redux/reducers
- touch userInfo.js
src/redux/reducers/userInfo.js
- import {GET_USER_INFO_REQUEST,GET_USER_INFO_SUCCESS,GET_USER_INFO_FAIL} from 'actions/userInfo';
- const initState = {
- isLoading: false,userInfo: {},errorMsg: ''
- };
- export default function reducer(state = initState,action) {
- switch (action.type) {
- case GET_USER_INFO_REQUEST:
- return {
- ...state,isLoading: true,errorMsg: ''
- };
- case GET_USER_INFO_SUCCESS:
- return {
- ...state,isLoading: false,userInfo: action.userInfo,errorMsg: ''
- };
- case GET_USER_INFO_FAIL:
- return {
- ...state,errorMsg: '请求错误'
- };
- default:
- return state;
- }
- }
这里的...state
语法,是和别人的object.assign()
起同一个作用,合并新旧state。我们这里是没效果的,但是我建议都写上这个哦
组合reducer
src/redux/reducers.js
- import counter from 'reducers/counter';
- import userInfo from 'reducers/userInfo';
- export default function combineReducers(state = {},action),userInfo: userInfo(state.userInfo,action)
- }
- }
-
现在有了
action
,有了reducer
,我们就需要调用把action
里面的三个action
函数和网络请求结合起来。- 请求中
dispatch getUserInfoRequest
- 请求成功
dispatch getUserInfoSuccess
- 请求失败
dispatch getUserInfoFail
- 请求中
src/redux/actions/userInfo.js
增加
- export function getUserInfo() {
- return function (dispatch) {
- dispatch(getUserInfoRequest());
- return fetch('http://localhost:8080/api/user.json')
- .then((response => {
- return response.json()
- }))
- .then((json) => {
- dispatch(getUserInfoSuccess(json))
- }
- ).catch(
- () => {
- dispatch(getUserInfoFail());
- }
- )
- }
- }
我们这里发现,别的action
创建函数都是返回action
对象:
- {type: xxxx}
但是我们现在的这个action
创建函数 getUserInfo
则是返回函数了。
为了让action
创建函数除了返回action
对象外,还可以返回函数,我们需要引用redux-thunk
。
npm install --save redux-thunk
这里涉及到redux
中间件middleware
,我后面会讲到的。你也可以读这里Middleware。
简单的说,中间件就是action
在到达reducer
,先经过中间件处理。我们之前知道reducer
能处理的action
只有这样的{type:xxx}
,所以我们使用中间件来处理
函数形式的action
,把他们转为标准的action
给reducer
。这是redux-thunk
的作用。
使用redux-thunk
中间件
我们来引入redux-thunk
中间件
src/redux/store.js
- import {createStore,applyMiddleware} from 'redux';
- import thunkMiddleware from 'redux-thunk';
- import combineReducers from './reducers.js';
- let store = createStore(combineReducers,applyMiddleware(thunkMiddleware));
- export default store;
到这里,redux
这边OK了,我们来写个组件验证下。
- cd src/pages
- mkdir UserInfo
- cd UserInfo
- touch UserInfo.js
src/pages/UserInfo/UserInfo.js
- import React,{Component} from 'react';
- import {connect} from 'react-redux';
- import {getUserInfo} from "actions/userInfo";
- class UserInfo extends Component {
- render() {
- const {userInfo,isLoading,errorMsg} = this.props.userInfo;
- return (
- <div>
- {
- isLoading ? '请求信息中......' :
- (
- errorMsg ? errorMsg :
- <div>
- <p>用户信息:</p>
- <p>用户名:{userInfo.name}</p>
- <p>介绍:{userInfo.intro}</p>
- </div>
- )
- }
- <button onClick={() => this.props.getUserInfo()}>请求用户信息</button>
- </div>
- )
- }
- }
- export default connect((state) => ({userInfo: state.userInfo}),{getUserInfo})(UserInfo);
这里你可能发现connect
参数写法不一样了,mapStateToProps
函数用了es6
简写,mapDispatchToProps
用了react-redux
提供的简单写法。
增加路由src/router/router.js
- import React from 'react';
- import {BrowserRouter as Router,Link} from 'react-router-dom';
- import Home from 'pages/Home/Home';
- import Page1 from 'pages/Page1/Page1';
- import Counter from 'pages/Counter/Counter';
- import UserInfo from 'pages/UserInfo/UserInfo';
- const getRouter = () => (
- <Router>
- <div>
- <ul>
- <li><Link to="/">首页</Link></li>
- <li><Link to="/page1">Page1</Link></li>
- <li><Link to="/counter">Counter</Link></li>
- <li><Link to="/userinfo">UserInfo</Link></li>
- </ul>
- <Switch>
- <Route exact path="/" component={Home}/>
- <Route path="/page1" component={Page1}/>
- <Route path="/counter" component={Counter}/>
- <Route path="/userinfo" component={UserInfo}/>
- </Switch>
- </div>
- </Router>
- );
- export default getRouter;
现在你可以执行npm start
去看效果啦!
到这里redux
集成基本告一段落了,后面我们还会有一些优化。
combinReducers优化
redux
提供了一个combineReducers
函数来合并reducer
,不用我们自己合并哦。写起来简单,但是意思和我们
自己写的combinReducers
也是一样的。
src/redux/reducers.js
- import {combineReducers} from "redux";
- import counter from 'reducers/counter';
- import userInfo from 'reducers/userInfo';
- export default combineReducers({
- counter,userInfo
- });
devtool优化
现在我们发现一个问题,代码哪里写错了,浏览器报错只报在build.js
第几行。
我们增加webpack
配置devtool
!
src/webpack.dev.config.js
增加
- devtool: 'inline-source-map'
同时,我们在srouce
里面能看到我们写的代码,也能打断点调试哦~
编译css
先说这里为什么不用scss
,因为Windows
使用node-sass
,需要先安装 Microsoft Windows SDK for Windows 7 and .NET Framework 4。
我怕有些人copy这份代码后,没注意,运行不起来。所以这里不用scss
了,如果需要,自行编译哦。
npm install css-loader style-loader --save-dev
css-loader
使你能够使用类似@import
和 url(...)
的方法实现 require()
的功能;
style-loader
将所有的计算后的样式加入页面中; 二者组合在一起使你能够把样式表嵌入webpack
打包后的JS文件中。
webpack.dev.config.js
rules
增加
- {
- test: /\.css$/,use: ['style-loader','css-loader']
- }
我们用Page1
页面来测试下
- cd src/pages/Page1
- touch Page1.css
src/pages/Page1/Page1.css
- .page-Box {
- border: 1px solid red;
- }
src/pages/Page1/Page1.js
- import React,{Component} from 'react';
- import './Page1.css';
- export default class Page1 extends Component {
- render() {
- return (
- <div className="page-Box">
- this is page1~
- </div>
- )
- }
- }
好了,现在npm start
去看效果吧。
编译图片
npm install --save-dev url-loader file-loader
webpack.dev.config.js
rules
增加
- {
- test: /\.(png|jpg|gif)$/,use: [{
- loader: 'url-loader',options: {
- limit: 8192
- }
- }]
- }
options limit 8192
意思是,小于等于8K的图片会被转成base64
编码,直接插入HTML中,减少HTTP
请求。
我们来用Page1
测试下
- cd src/pages/Page1
- mkdir images
src/pages/Page1/Page1.js
- import React,{Component} from 'react';
- import './Page1.css';
- import image from './images/brickpsert.jpg';
- export default class Page1 extends Component {
- render() {
- return (
- <div className="page-Box">
- this is page1~
- <img src={image}/>
- </div>
- )
- }
- }
可以去看看效果啦。
按需加载
为什么要实现按需加载?
我们现在看到,打包完后,所有页面只生成了一个build.js
,当我们首屏加载的时候,就会很慢。因为他也下载了别的页面的js
了哦。
如果每个页面都打包了自己单独的JS,在进入自己页面的时候才加载对应的js,那首屏加载就会快很多哦。
在 react-router 2.0
时代, 按需加载需要用到的最关键的一个函数,就是require.ensure()
,它是按需加载能够实现的核心。
在4.0版本,官方放弃了这种处理按需加载的方式,选择了一个更加简洁的处理方式。
根据官方示例,我们开搞
npm install bundle-loader --save-dev
- 新建
bundle.js
- cd src/router
- touch Bundle.js
src/router/Bundle.js
- import React,{Component} from 'react'
- class Bundle extends Component {
- state = {
- // short for "module" but that's a keyword in js,so "mod"
- mod: null
- };
- componentWillMount() {
- this.load(this.props)
- }
- componentWillReceiveProps(nextProps) {
- if (nextProps.load !== this.props.load) {
- this.load(nextProps)
- }
- }
- load(props) {
- this.setState({
- mod: null
- });
- props.load((mod) => {
- this.setState({
- // handle both es imports and cjs
- mod: mod.default ? mod.default : mod
- })
- })
- }
- render() {
- return this.props.children(this.state.mod)
- }
- }
- export default Bundle;
- 改造路由器
src/router/router.js
- import React from 'react';
- import {BrowserRouter as Router,Link} from 'react-router-dom';
- import Bundle from './Bundle';
- import Home from 'bundle-loader?lazy&name=home!pages/Home/Home';
- import Page1 from 'bundle-loader?lazy&name=page1!pages/Page1/Page1';
- import Counter from 'bundle-loader?lazy&name=counter!pages/Counter/Counter';
- import UserInfo from 'bundle-loader?lazy&name=userInfo!pages/UserInfo/UserInfo';
- const Loading = function () {
- return <div>Loading...</div>
- };
- const createComponent = (component) => () => (
- <Bundle load={component}>
- {
- (Component) => Component ? <Component/> : <Loading/>
- }
- </Bundle>
- );
- const getRouter = () => (
- <Router>
- <div>
- <ul>
- <li><Link to="/">首页</Link></li>
- <li><Link to="/page1">Page1</Link></li>
- <li><Link to="/counter">Counter</Link></li>
- <li><Link to="/userinfo">UserInfo</Link></li>
- </ul>
- <Switch>
- <Route exact path="/" component={createComponent(Home)}/>
- <Route path="/page1" component={createComponent(Page1)}/>
- <Route path="/counter" component={createComponent(Counter)}/>
- <Route path="/userinfo" component={createComponent(UserInfo)}/>
- </Switch>
- </div>
- </Router>
- );
- export default getRouter;
现在你可以npm start
,打开浏览器,看是不是进入新的页面,都会加载自己的JS的~
但是你可能发现,名字都是0.bundle.js
这样子的,这分不清楚是哪个页面的js
呀!
我们修改下webpack.dev.config.js
,加个chunkFilename
。chunkFilename
是除了entry
定义的入口js
之外的js
~
- output: {
- path: path.join(__dirname,filename: 'bundle.js',chunkFilename: '[name].js'
- }
现在你运行发现名字变成home.js
,这样的了。棒棒哒!
那么问题来了home
是在哪里设置的?webpack
怎么知道他叫home
?
其实在这里我们定义了,router.js
里面
import Home from 'bundle-loader?lazy&name=home!pages/Home/Home';
看到没。这里有个name=home
。嘿嘿。
参考地址:
- http://www.jianshu.com/p/8dd9...
- https://github.com/ReactTrain...
- https://segmentfault.com/a/11...
- http://react-china.org/t/webp...
- https://juejin.im/post/58f971...
缓存
想象一下这个场景~
我们网站上线了,用户第一次访问首页,下载了home.js
,第二次访问又下载了home.js
~
这肯定不行呀,所以我们一般都会做一个缓存,用户下载一次home.js
后,第二次就不下载了。
有一天,我们更新了home.js
,但是用户不知道呀,用户还是使用本地旧的home.js
。出问题了~
怎么解决?每次代码更新后,打包生成的名字不一样。比如第一次叫home.a.js
,第二次叫home.b.js
。
文档看这里
我们照着文档来
webpack.dev.config.js
- output: {
- path: path.join(__dirname,filename: '[name].[hash].js',chunkFilename: '[name].[chunkhash].js'
- }
每次打包都用增加hash
~
现在我们试试,是不是修改了文件,打包后相应的文件名字就变啦?
但是你可能发现了,网页打开报错了~因为你dist/index.html
里面引用js
名字还是bundle.js
老名字啊,改成新的名字就可以啦。
啊~那岂不是我每次编译打包,都得去改一下js名字?欲知后事如何,且看下节分享。
HtmlWebpackPlugin
这个插件,每次会自动把js插入到你的模板index.html
里面去。
npm install html-webpack-plugin --save-dev
新建模板index.html
- cd src
- touch index.html
src/index.html
- <!doctype html>
- <html lang="en">
- <head>
- <Meta charset="UTF-8">
- <title>Document</title>
- </head>
- <body>
- <div id="app"></div>
- </body>
- </html>
修改webpack.dev.config.js
,增加plugin
- var HtmlWebpackPlugin = require('html-webpack-plugin');
- plugins: [new HtmlWebpackPlugin({
- filename: 'index.html',template: path.join(__dirname,'src/index.html')
- })],
npm start
运行项目,看看是不是能正常访问啦。~
说明一下:npm start
打包后的文件存在内存中,你看不到的。~ 你可以把遗留dist/index.html
删除掉了。
提取公共代码
想象一下,我们的主文件,原来的bundle.js
里面是不是包含了react
,redux
,react-router
等等
这些代码??这些代码基本上不会改变的。但是,他们合并在bundle.js
里面,每次项目发布,重新请求bundle.js
的时候,相当于重新请求了react
等这些公共库。浪费了~
我们把react
这些不会改变的公共库提取出来,用户缓存下来。从此以后,用户再也不用下载这些库了,无论是否发布项目。嘻嘻。
webpack
文档给了教程,看这里
webpack.dev.config.js
- var webpack = require('webpack');
- entry: {
- app: [
- 'react-hot-loader/patch','src/index.js')
- ],vendor: ['react','react-router-dom','redux','react-dom','react-redux']
- }
- /*plugins*/
- new webpack.optimize.CommonsChunkPlugin({
- name: 'vendor'
- })
把react
等库生成打包到vendor.hash.js
里面去。
但是你现在可能发现编译生成的文件app.[hash].js
和vendor.[hash].js
生成的hash
一样的,这里是个问题,因为呀,你每次修改代码,都会导致vendor.[hash].js
名字改变,那我们提取出来的意义也就没了。其实文档上写的很清楚,
- output: {
- path: path.join(__dirname,//这里应该用chunkhash替换hash
- chunkFilename: '[name].[chunkhash].js'
- }
但是无奈,如果用chunkhash
,会报错。和webpack-dev-server --hot
不兼容,具体看这里。
现在我们在配置开发版配置文件,就向webpack-dev-server
妥协,因为我们要用他。问题先放这里,等会我们配置正式版webpack.config.js
的时候要解决这个问题。
生产坏境构建
开发环境(development)和生产环境(production)的构建目标差异很大。在开发环境中,我们需要具有强大的、具有实时重新加载(live reloading)或热模块替换(hot module replacement)能力的 source map 和 localhost server。而在生产环境中,我们的目标则转向于关注更小的 bundle,更轻量的 source map,以及更优化的资源,以改善加载时间。由于要遵循逻辑分离,我们通常建议为每个环境编写彼此独立的 webpack 配置。
文档看这里
我们要开始做了~
- touch webpack.config.js
在webpack.dev.config.js
的基础上先做以下几个修改~
- 先删除
webpack-dev-server
相关的东西~ -
devtool
的值改成cheap-module-source-map
- 刚才说的
hash
改成chunkhash
webpack.config.js
- const path = require('path');
- var HtmlWebpackPlugin = require('html-webpack-plugin');
- var webpack = require('webpack');
- module.exports = {
- devtool: 'cheap-module-source-map',entry: {
- app: [
- path.join(__dirname,'react-redux']
- },output: {
- path: path.join(__dirname,filename: '[name].[chunkhash].js',chunkFilename: '[name].[chunkhash].js'
- },module: {
- rules: [{
- test: /\.js$/,use: ['babel-loader'],'src')
- },{
- test: /\.css$/,'css-loader']
- },{
- test: /\.(png|jpg|gif)$/,use: [{
- loader: 'url-loader',options: {
- limit: 8192
- }
- }]
- }]
- },plugins: [
- new HtmlWebpackPlugin({
- filename: 'index.html','src/index.html')
- }),new webpack.optimize.CommonsChunkPlugin({
- name: 'vendor'
- })
- ],resolve: {
- alias: {
- pages: path.join(__dirname,'src/router'),actions: path.join(__dirname,'src/redux/reducers')
- }
- }
- };
在package.json
增加打包脚本
"build":"webpack --config webpack.config.js"
然后执行npm run build
~看看dist
文件夹是不是生成了我们发布要用的所有文件哦?
接下来我们还是要优化正式版配置文件~
文件压缩
webpack
使用UglifyJSPlugin
来压缩生成的文件。
npm i --save-dev uglifyjs-webpack-plugin
webpack.config.js
- const UglifyJSPlugin = require('uglifyjs-webpack-plugin')
- module.exports = {
- plugins: [
- new UglifyJSPlugin()
- ]
- }
npm run build
发现打包文件大小减小了好多。
指定环境
许多 library 将通过与 process.env.NODE_ENV 环境变量关联,以决定 library 中应该引用哪些内容。例如,当不处于生产环境中时,某些 library 为了使调试变得容易,可能会添加额外的日志记录(log)和测试(test)。其实,当使用 process.env.NODE_ENV === 'production' 时,一些 library 可能针对具体用户的环境进行代码优化,从而删除或添加一些重要代码。我们可以使用 webpack 内置的 DefinePlugin 为所有的依赖定义这个变量:
webpack.config.js
- module.exports = {
- plugins: [
- new webpack.DefinePlugin({
- 'process.env': {
- 'NODE_ENV': JSON.stringify('production')
- }
- })
- ]
- }
npm run build
后发现vendor.[hash].js
又变小了。
优化缓存
刚才我们把[name].[hash].js
变成[name].[chunkhash].js
后,npm run build
后,
发现app.xxx.js
和vendor.xxx.js
不一样了哦。
但是现在又有一个问题了。
你随便修改代码一处,例如Home.js
,随便改变个字,你发现home.xxx.js
名字变化的同时,vendor.xxx.js
名字也变了。这不行啊。这和没拆分不是一样一样了吗?我们本意是vendor.xxx.js
名字永久不变,一直缓存在用户本地的。~
@L_502_46@推荐了一个插件HashedModuleIdsPlugin
- plugins: [
- new webpack.HashedModuleIdsPlugin()
- ]
现在你打包,修改代码再试试,是不是名字不变啦?错了,现在打包,我发现名字还是变了,经过比对文档,我发现还要加一个runtime
代码抽取,
- new webpack.optimize.CommonsChunkPlugin({
- name: 'runtime'
- })
注意,引入顺序在这里很重要。CommonsChunkPlugin 的 'vendor' 实例,必须在 'runtime' 实例之前引入。
public path
想象一个场景,我们的静态文件放在了单独的静态服务器上去了,那我们打包的时候,如何让静态文件的链接定位到静态服务器呢?
看文档Public Path
webpack.config.js
output
中增加一个publicPath
,我们当前用/
,相对于当前路径,如果你要改成别的url
,就改这里就好了。
- output: {
- publicPath : '/'
- }
打包优化
你现在打开dist
,是不是发现好多好多文件,每次打包后的文件在这里混合了?我们希望每次打包前自动清理下dist
文件。
npm install clean-webpack-plugin --save-dev
webpack.config.js
- const CleanWebpackPlugin = require('clean-webpack-plugin');
- plugins: [
- new CleanWebpackPlugin(['dist'])
- ]
现在npm run bundle
试试,是不是之前的都清空了。当然我们之前的api
文件夹也被清空了,不过没关系哦~本来就是测试用的。
抽取css
目前我们的css
是直接打包进js
里面的,我们希望能单独生成css
文件。
我们使用extract-text-webpack-plugin来实现。
npm install --save-dev extract-text-webpack-plugin
webpack.config.js
- const ExtractTextPlugin = require("extract-text-webpack-plugin");
- module.exports = {
- module: {
- rules: [
- {
- test: /\.css$/,use: ExtractTextPlugin.extract({
- fallback: "style-loader",use: "css-loader"
- })
- }
- ]
- },plugins: [
- new ExtractTextPlugin({
- filename: '[name].[contenthash:5].css',allChunks: true
- })
- ]
- }
使用axios
和middleware
优化API请求
先安装下axios
npm install --save axios
我们之前项目的一次API请求是这样写的哦~
action
创建函数是这样的。比我们现在写的fetch
简单多了。
然后在dispatch(getUserInfo())后,通过redux
中间件来处理请求逻辑。
中间件的教程看这里
我们想想中间件的逻辑
来写一个
- cd src/redux
- mkdir middleware
- cd middleware
- touch promiseMiddleware.js
src/redux/middleware/promiseMiddleware.js
- import axios from 'axios';
- export default store => next => action => {
- const {dispatch,getState} = store;
- /*如果dispatch来的是一个function,此处不做处理,直接进入下一级*/
- if (typeof action === 'function') {
- action(dispatch,getState);
- }
- /*解析action*/
- const {
- promise,types,afterSuccess,...rest
- } = action;
- /*没有promise,证明不是想要发送ajax请求的,就直接进入下一步啦!*/
- if (!action.promise) {
- return next(action);
- }
- /*解析types*/
- const [REQUEST,SUCCESS,FAILURE] = types;
- /*开始请求的时候,发一个action*/
- next({
- ...rest,type: REQUEST
- });
- /*定义请求成功时的方法*/
- const onFulfilled = result => {
- next({
- ...rest,result,type: SUCCESS
- });
- if (afterSuccess) {
- afterSuccess(dispatch,result);
- }
- };
- /*定义请求失败时的方法*/
- const onRejected = error => {
- next({
- ...rest,error,type: FAILURE
- });
- };
- return promise(axios).then(onFulfilled,onRejected).catch(error => {
- console.error('MIDDLEWARE ERROR:',error);
- onRejected(error)
- })
- }
修改src/redux/store.js
来应用这个中间件
- import {createStore,applyMiddleware} from 'redux';
- import combineReducers from './reducers.js';
- import promiseMiddleware from './middleware/promiseMiddleware'
- let store = createStore(combineReducers,applyMiddleware(promiseMiddleware));
- export default store;
修改src/redux/actions/userInfo.js
- export const GET_USER_INFO_REQUEST = "userInfo/GET_USER_INFO_REQUEST";
- export const GET_USER_INFO_SUCCESS = "userInfo/GET_USER_INFO_SUCCESS";
- export const GET_USER_INFO_FAIL = "userInfo/GET_USER_INFO_FAIL";
- export function getUserInfo() {
- return {
- types: [GET_USER_INFO_REQUEST,promise: client => client.get(`http://localhost:8080/api/user.json`)
- }
- }
是不是简单清新很多啦?
修改src/redux/reducers/userInfo.js
- case GET_USER_INFO_SUCCESS:
- return {
- ...state,userInfo: action.result.data,errorMsg: ''
- };
action.userInfo
修改成了action.result.data
。你看中间件,请求成功,会给action
增加一个result
字段来存储响应结果哦~不用手动传了。
npm start
看看我们的网络请求是不是正常哦。
调整文本编辑器
使用自动编译代码时,可能会在保存文件时遇到一些问题。某些编辑器具有“安全写入”功能,可能会影响重新编译。
要在一些常见的编辑器中禁用此功能,请查看以下列表: