源码地址:https://github.com/brickspert/react-family 欢迎star
提问反馈:blogjavascript
原文地址:https://github.com/brickspert/blog/issues/1(github这里我会不断更新教程的)css
此处不更新,github上会一直更新html
当我第一次跟着项目作react
项目的时候,因为半截加入的,对框架了解甚少,只能跟着别人的样板写。对整个框架没有一点了解。java
作项目,老是要解决各类问题的,因此每一个地方都须要去了解,可是对整个框架没有一个总体的了解,实在是不行。node
期间,我也跟着别人的搭建框架的教程一步一步的走,可是常常由于本身太菜,走不下去。在通过各类蹂躏以后,对整个框架也有一个大概的了解,
我就想把他写下来,让后来的菜鸟能跟着个人教程对react
全家桶有一个全面的认识。react
个人这个教程,重新建根文件夹开始,到成型的框架,每一个文件为何要创建?创建了干什么?每一个依赖都是干什么的?一步一步写下来,供你们学习。webpack
固然,这个框架我之后会一直维护的,也但愿你们能一块儿来完善这个框架,若是您有任何建议,欢迎留言,欢迎fork
。ios
在完善本框架的同时,我准备再新建一个兼容ie8
的框架react-family-ie8
,固然是基于该框架改造的。git
cd src/pages mkdir Home
│ .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
建立文件夹并进入es6
`mkdir react-family && cd react-family`
init npm
`npm init` 按照提示填写项目基本信息
webpack
npm install --save-dev webpack
Q: 何时用--save-dev
,何时用--save
?
A: --save-dev
是你开发时候依赖的东西,--save
是你发布以后还依赖的东西。看这里
根据webpack文档编写最基础的配置文件
新建webpack
开发配置文件 touch webpack.dev.config.js
webpack.dev.config.js
const path = require('path'); module.exports = { /*入口*/ entry: path.join(__dirname, 'src/index.js'), /*输出到dist文件夹,输出文件名字为bundle.js*/ output: { path: path.join(__dirname, './dist'), filename: 'bundle.js' } };
webpack
编译文件新建入口文件
mkdir src && touch ./src/index.js
src/index.js
添加内容
document.getElementById('app').innerHTML = "Webpack works"
如今咱们执行命令 webpack --config webpack.dev.config.js
咱们能够看到生成了dist
文件夹和bundle.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
作了什么。
把入口文件 index.js
通过处理以后,生成 bundle.js
。就这么简单。
Babel 把用最新标准编写的 JavaScript 代码向下编译成能够在今天随处可用的版本。 这一过程叫作“源码到源码”编译, 也被称为转换编译。
通俗的说,就是咱们能够用ES6, ES7等来编写代码,Babel会把他们通通转为ES5。
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
!
/*src文件夹下面的以.js结尾的文件,要使用babel解析*/ /*cacheDirectory是用来缓存编译结果,下次编译加速*/ module: { rules: [{ test: /\.js$/, use: ['babel-loader?cacheDirectory=true'], include: path.join(__dirname, 'src') }] }
如今咱们简单测试下,是否能正确转义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的区别
参考地址:
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...
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
打包生成的资源文件提供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
,咱们就看看它的其余的配置项。
看了以后,发现有几个咱们能够用的。
console
中打印彩色日志404
响应都被替代为index.html
。有什么用呢?你如今运行npm start
,而后打开浏览器,访问http://localhost:8080
,而后点击Page1
到连接http://localhost:8080/page1
,而后刷新页面试试。是否是发现刷新后404
了。为何?dist
文件夹里面并无page1.html
,固然会404
了,因此咱们须要配置historyApiFallback
,让全部的404
定位到index.html
。
host
,默认是localhost
。若是你但愿服务器外部能够访问,指定以下:host: "0.0.0.0"
。好比你用手机经过IP访问。Webpack
的模块热替换特性。关于热模块替换,我下一小节专门讲解一下。8080
端口。localhost:3000
上有后端服务的话,你能够这样启用代理:proxy: { "/api": "http://localhost:3000" }
根据这几个配置,修改下咱们的webpack-dev-server
的配置~
webpack.dev.config.js
devServer: { contentBase: path.join(__dirname, './dist'), 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是否能够访问到网站?
参考地址:
到目前,当咱们修改代码的时候,浏览器会自动刷新,不信你能够去试试。(若是你的不会刷新,看看这个调整文本编辑器)
我相信看这个教程的人,应该用过别人的框架。咱们在修改代码的时候,浏览器不会刷新,只会更新本身修改的那一块。咱们也要实现这个效果。
咱们看下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
会重置,这不是咱们想要的。
修改Home.js
,增长计数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", "react", "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
并无任何魔法。
不要被各类关于 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> ) } }
修改路由,增长Counter
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'; 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, decrement, 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
参考地址: http://cn.redux.js.org/docs/a...
想象一下咱们调用一个异步get
请求去后台请求数据:
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, userInfo: {}, errorMsg: '' }; case GET_USER_INFO_SUCCESS: return { ...state, isLoading: false, userInfo: action.userInfo, errorMsg: '' }; case GET_USER_INFO_FAIL: return { ...state, isLoading: false, userInfo: {}, 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) { return { counter: counter(state.counter, 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, Route, Switch, 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
集成基本告一段落了,后面咱们还会有一些优化。
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 });
如今咱们发现一个问题,代码哪里写错了,浏览器报错只报在build.js
第几行。
这让咱们分析错误无从下手。看这里。
咱们增长webpack
配置devtool
!
src/webpack.dev.config.js
增长
devtool: 'inline-source-map'
此次看错误信息是否是提示的很详细了?
同时,咱们在srouce
里面能看到咱们写的代码,也能打断点调试哦~
先说这里为何不用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
给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, Route, Switch, 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, './dist'), 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
。嘿嘿。
参考地址:
想象一下这个场景~
咱们网站上线了,用户第一次访问首页,下载了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, './dist'), filename: '[name].[hash].js', chunkFilename: '[name].[chunkhash].js' }
每次打包都用增长hash
~
如今咱们试试,是否是修改了文件,打包后相应的文件名字就变啦?
可是你可能发现了,网页打开报错了~由于你dist/index.html
里面引用js
名字仍是bundle.js
老名字啊,改为新的名字就能够啦。
啊~那岂不是我每次编译打包,都得去改一下js名字?欲知后事如何,且看下节分享。
这个插件,每次会自动把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', path.join(__dirname, '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, './dist'), filename: '[name].[hash].js', //这里应该用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, 'src/index.js') ], vendor: ['react', 'react-router-dom', 'redux', 'react-dom', 'react-redux'] }, output: { path: path.join(__dirname, './dist'), filename: '[name].[chunkhash].js', chunkFilename: '[name].[chunkhash].js' }, module: { rules: [{ test: /\.js$/, use: ['babel-loader'], include: path.join(__dirname, 'src') }, { test: /\.css$/, use: ['style-loader', 'css-loader'] }, { test: /\.(png|jpg|gif)$/, use: [{ loader: 'url-loader', options: { limit: 8192 } }] }] }, plugins: [ new HtmlWebpackPlugin({ filename: 'index.html', template: path.join(__dirname, 'src/index.html') }), new webpack.optimize.CommonsChunkPlugin({ name: 'vendor' }) ], resolve: { alias: { pages: path.join(__dirname, 'src/pages'), component: path.join(__dirname, 'src/component'), router: path.join(__dirname, 'src/router'), actions: path.join(__dirname, 'src/redux/actions'), reducers: 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
名字永久不变,一直缓存在用户本地的。~
官方文档推荐了一个插件HashedModuleIdsPlugin
plugins: [ new webpack.HashedModuleIdsPlugin() ]
如今你打包,修改代码再试试,是否是名字不变啦?错了,如今打包,我发现名字仍是变了,通过比对文档,我发现还要加一个runtime
代码抽取,
new webpack.optimize.CommonsChunkPlugin({ name: 'runtime' })
加上这句话就行了~为何呢?看下解释。
注意,引入顺序在这里很重要。CommonsChunkPlugin 的 'vendor' 实例,必须在 'runtime' 实例以前引入。
想象一个场景,咱们的静态文件放在了单独的静态服务器上去了,那咱们打包的时候,如何让静态文件的连接定位到静态服务器呢?
看文档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
是直接打包进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 }) ] }
npm run build
后发现单独生成了css
文件哦
axios
和middleware
优化API请求先安装下axios
npm install --save axios
咱们以前项目的一次API请求是这样写的哦~
action
建立函数是这样的。比咱们如今写的fetch
简单多了。
export function getUserInfo() { return { types: [GET_USER_INFO_REQUEST, GET_USER_INFO_SUCCESS, GET_USER_INFO_FAIL], promise: client => client.get(`http://localhost:8080/api/user.json`) afterSuccess:(dispatch,getState,response)=>{ /*请求成功后执行的函数*/ }, otherData:otherData } }
而后在dispatch(getUserInfo())后,经过redux
中间件来处理请求逻辑。
中间件的教程看这里
咱们想一想中间件的逻辑
dispatch
REQUEST
请求。dispatch
SUCCESS
请求,若是定义了afterSuccess()
函数,调用它。dispatch
FAIL
请求。来写一个
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, getState, 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, GET_USER_INFO_SUCCESS, GET_USER_INFO_FAIL], promise: client => client.get(`http://localhost:8080/api/user.json`) } }
是否是简单清新不少啦?
修改src/redux/reducers/userInfo.js
case GET_USER_INFO_SUCCESS: return { ...state, isLoading: false, userInfo: action.result.data, errorMsg: '' };
action.userInfo
修改为了action.result.data
。你看中间件,请求成功,会给action
增长一个result
字段来存储响应结果哦~不用手动传了。
npm start
看看咱们的网络请求是否是正常哦。
使用自动编译代码时,可能会在保存文件时遇到一些问题。某些编辑器具备“安全写入”功能,可能会影响从新编译。
要在一些常见的编辑器中禁用此功能,请查看如下列表: