前言篇--笔者心得
刚从大学校园里跑出来,大学里做过的各种东西看似很多感觉很牛批,完了出来接触才发现自己是个小菜鸡...近段时间一直在学习项目需要的react方面的知识,入过的坑简直是不能太多(欲哭无泪)。在学习react的道路上一直在摸索,除了阅读了阮一峰老师写的关于react的知识点之外,还学习搭建了一个简单的框架,作为巩固近端时间学习的一个简单的方法。(学习搭建框架的这篇教程并非属于笔者所创,仅仅是用于笔者实践过后的一个分享,不喜勿喷=_= 更过更详细的介绍请移步猛戳 这里啦)
项目须知
根据我们平常所见的项目,一般都会有一个较为完整的路径框架(大体框架的目录),便于文件的存放以及查找和管理,如下图所示(教程中的图)
此外,在需要的跟目录下新建文件是只需要右键安装好的git Bush here进行文件夹的新建即可好啦,现在让我们愉快的开始前端框架的搭建吧哈哈哈~~
1.创建文件夹并进入(在此之前你需要保证已有git账号并且已经配置好需要的运行环境)
$ mkdir my-react-family && cd my-react-family
2. npm init
$ npm init
(输完整条指令时需要一直按enter直到下图出现)
webpack
1.安装webpack
$ npm install webpack@3 --save-dev
(可能有很多初学者跟笔者一样不大理解什么时候该用--save-dev
,什么时候该用--save
。笔者经过学习教程中了解到--save-dev
是开发的时候依赖的东西,而--save
是发布之后还一直依赖的东西,够清楚了吧~~)
2.根据webpack文档编写最基础的配置文件
新建webpack
开发的配置文件 touch webpack.dev.config.js
接着在编译器的webpack dev.config.js
文件中编写一下代码
const path = require('path');
moudle.export = {
/*这里是入口*/
entry: path.join(__dirname, 'src/index.js'),
/*这里是输出文件的出口,输出文件到dist文件夹,输出文件名为bundle.js*/
output: {
path: path.join(__dirname,'./dist'),
filename: 'main.js'
}
};
复制代码
3.接下来你要学会的是如何编译webpack入口文件(讲真指令就这么几条,请用心记)~_~
$ mkdir src && touch ./src/index.js
在编译器中的src/index.js
文件下输入以下代码
document.getElementById('app').innerHTML = "Nice to see you~~";
复制代码
现在我们可以执行 webpack --config -g webpack.dev.config.js
(Tips:这里的webpack需要全局安装,如果没有全局安装会报错 全局安装webpack的指令为: npm install --save-dev -g webpack)
现在可以看到在文件夹下面生成了dist
和main.js
文件(但是教程生成的是bundle.js
文件,我也不知道为啥不过好像不影响,所以没有深究)
4.现在可以来测试一下下啦
在dist
文件夹下新建一个index.html
紧接着在index.html
下输入一下代码:
"en">
"UTF-8">
Document
"app">
复制代码
接下来就可以直接看到编译的成果啦哈哈哈,亲们请看图:
babel
(关于babel
,哇~~说法有很多,但是简单的说它就是一个转码器(通俗易懂吧ahhh),可以将ES6的代码转换成ES5的代码,从而在现有的环境下执行。详细请猛戳这里)
好啦接下来要执行以下指令便于代码的编译以及转换:
npm install --save-dev babel-core babel-loader babel-preset-es2015 babel-preset-react babel-preset-stage-0
复制代码
(Tips:
babel-core
调用Babel的API进行转码babel-loader
babel-preset-es2015
用于解析 ES6babel-preset-react
用于解析 JSXbabel-preset-stage-0
用于解析 ES7 提案
)
接下来请执行以下指令:
touch .babelrc
(亲们注意一下呀,这条指令是在根目录,也就是在my-react-family下新建)
接着在.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')
}]
}
复制代码
现在我们来简单的测试一下是否能转换义ES5
呀
我们先要修改index.js
/*使用es6的箭头函数*/
var func = str => {
document.getElementById('app').innerHTML = str;
};
func('我现在在使用Babel!');
复制代码
然后在编译器里面执行打包命令:webpack --config webpack.dev.config.js
,再运行就可以看到以下的效果图啦~~~!
总结一下刚才我们做了啥??
我们刚才通过安装babel
的依赖,在通过配置webpack
,更改入口文件index.js
内部的代码,将通过webpack --config webpack.dev.config.js
指令打包,从而生成main.js
。这时main.js
文件后面的代码会跟index.js
的意思一致,就只是es6
和es5
的区别呀...神奇吧ahh
react
执行以下指令
$ npm install --save react react-dom
修改src/index.js
,使用react
import React from 'react';
import ReactDom from 'react-dom';
ReactDom.render(
Hello React!, document.getElementById('app'));
复制代码
接着执行打包命令webpack --config webpack.dev.config.js
打开index.html
看看效果吧~~
紧接着就是制作我们的组件啦。我们把Hello React
放到我们的组件里面,实现react
的组件化~~
cd src
mkdir component
cd component
mkdir Hello
cd Hello
touch Hello.js
复制代码
接着在Hello.js
当中键入以下代码:
import React, {Component} from 'react';
export default class Hello extends Component {
render() {
return (
Hello,React!
)
}
}
复制代码
在这之前,要是小伙伴还不熟悉react
组件化的写法,或者是不熟悉ES6
的语法,笔者有个小小的建议,可以去学习一下阮一峰老师写的 ECMAScript6入门
这能帮助比较全面的了解react方面的语法。
接着修改src/index.js
,引用Hello
组件
import React from 'react';
import ReactDom from 'react-dom';
import Hello from './component/Hello/Hello';
ReactDom.render(
, document.getElementById('app'));
复制代码
最后在根目录里面执行打包命令webpack --config webpack.dev.config.js
好啦,此刻有没有很激动呀,尝试着打开index.html
看看效果吧(react
道路不好走呀,得坚持才能成功)
命令优化
有的小伙伴是不是觉得每次执行打包命令(webpack --config webpack.dev.config.js
)的时候很麻烦??哈哈哈没事,我们现在可以此条命令进行优化,节省一定的时间哟~具体实现的操作如下:
修改package.json
里面的script
,增加dev-build
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev-build": "webpack --config webpack.dev.config.js"
}
复制代码
现在,我们每次执行打包命令的时候,不必要每次都执行那么鬼长鬼长的一串命令啦,很nice有木有~~
react-router
npm install react-router-dom --save
新建一个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 = () => (
- "/">首页
- "/page1">Page1
"/" component={Home}/>
"/page1" component={Page1}/>
);
export default getRouter;
复制代码
新建一个页面文件夹
cd src
mkdir pages
复制代码
新建两个页面用于存放组件
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 (
this is home~
)
}
}
复制代码
src/pages/Page1/Page1.js
import React, {Component} from 'react';
export default class Page1 extends Component {
render() {
return (
this is Page1~
)
}
}
复制代码
现在基本的路由和页面都已经做好了,接下来我们要在入口文件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'));
复制代码
由于在命令优化中我们已经把webpack.dev.config.js
打包命令进行优化,现在我们只需执行npm run dev-build
即可。(注意此时要查看相应的页面不能只在index.html
中点击浏览器了,我们需要在
参考地址(摘自原教程):
1.参考资料一
2.参考资料二
webpack-dev-server
webpack-dev-server
,简单的来说,是一个小型的静态文件服务器。使用它可以为webapck打包生成的资源文件提供web服务。
一般来说,,这里安装webpack-dev-config的时候,需要全局安装,全局安装的代码:`npm install webpack-dev-server@2 -g`
复制代码
webpack.dev.config.js
devServer: {
contentBase: path.join(__dirname, './dist')
}
复制代码
现在执行
webpack-dev-server --config webpack.dev.config.js
这时候就可以使用httpa://localhost:8080,就可以看到我们所建好的页面啦
Tips:webpack-dev-server
编译之后的文件,都会存储在内存之中,但是这是我们是看不到的。你可以删除之前的dist/main.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
就ok啦
***题外话:在原教程当中,提到了其他的配置项,在此我把他大概的在这儿再复述一遍哈(可能会略显多余,可不看)
- 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.config.js
devServer: {
port: 8080,
contentBase: path.join(__dirname, './dist'),
historyApiFallback: true,
host: '0.0.0.0'
}
复制代码
现在CLI ONLY
的需要在命令行中配置
package.json
"start": "webpack-dev-server --config webpack.dev.config.js --color --progress"
复制代码
现在我们可以执行npm start
看看效果啦~~可以看到在http://localhost:8080/page1
是没有啥大问题的了。
模块热替换 (Hot Modle Replacement)
这儿有一个神奇的现象,小伙伴们不妨尝试一下再组件中修改一下内容,浏览器会自动刷新哟啊哈哈哈,是不是超级神奇超级nice,感觉离成功又进了一大步。
接下来我们来研究一下webpack模块热替换教程(摘自原教程)
package.json
增加--hot
"start": "webpack-dev-server --config webpack.dev.config.js --color --progress --hot"
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
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 (
this is home~
当前计数:{this.state.count}
)
}
}
复制代码
此刻可以刷新一下浏览器,可以看到webpack更新之后的页面,count
的值变为了0。
这有一点需要注意一下的就是,在react
模块更新的同时,为了能保留state
等页面中的其他状态,我们需要引入react-hot-loader
呀
或许到这儿会有一些小伙伴不大理解webpack-dev-server
和react-hot-loader
的区别。
其实区别在于webpack-dev-server
自己的--hot
模式只能即时刷新页面,但状态保存不住。因为React有一些自己语法(JSX)是HotModuleReplacementPlugin
搞不定的,而react-hot-loader
在--hot
基础上做了额外的处理,来保证状态可以存下来。
好啦,接下来我们需要加入react-hot-loader v3
执行以下程序安装依赖
npm install react-hot-loader@next --save-dev
接下来我们需要做一下修改
1..babelrc
{
"presets": [
"es2015",
"react",
"stage-0"
],
"plugins": [
"react-hot-loader/babel"
]
}
复制代码
2.webpack.dev.config.js
入口增加react-hot-loader/patch
webpack.dev.config.js
entry: [
'react-hot-loader/patch',
path.join(__dirname, 'src/index.js')
]
复制代码
3.修改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(
{RootElement}
,
document.getElementById('app')
)
}
复制代码
好啦,现在执行npm start
,可以发现state就没有再更新啦~~又完成了一小步,甚是开心啊哈哈
redux
接下来我们就要开始集成redux
咯~~
其实吧redux
也没有我们想的那么难,只要把流程理清就差不多了,在这儿我还是建议大家伙儿去仔细的月度一下阮一峰老师的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
创建函数,通过创建antion
函数,可以创建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)
}
}
复制代码
到这儿咋们实践了很多,我看着原本的教程到这儿的时候其实是有点懵逼的,感觉一直跟着操作比较机械,虽然代码是一点一点跟着敲的,但是有些逻辑以及概念方面的东西还不是很能理解。所以咋们先缓缓,推荐去看一下react-redux
的官方文档,请猛戳这里,也可以选择强戳这里....中文版的可能更加便于如我一般的小菜鸟们理解啊哈哈哈
其实只要理解最核心的一点大体上就ok了
reducer
就是纯函数,接收state
和 action
,然后返回一个新的 state
。
接下来在src/redux/store.js
添加一下代码
import {createStore} from 'redux';
import combineReducers from './reducers.js';
let store = createStore(combineReducers);
export default store;
复制代码
到这里应该可以使用redux
啦~~是不是在nice的同时心里还稍稍着些许的成就感呀
接下来我们增加一下路径别名(简单的说这就是为了方便哈,也可以不用,只要自己引入组件的之后路径不错就行哈)
webpack.dev.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 (
当前计数为(显示redux计数)
)
}
}
复制代码
到这儿我们需要修改一下路由,因为每写一个页面我们都要把它添加至路由当中,这样才能生效。
修改路由,添加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 = () => (
- "/">首页
- "/page1">Page1
- "/counter">Counter
"/" component={Home}/>
"/page1" component={Page1}/>
"/counter" component={Counter}/>
);
export default getRouter;
复制代码
(小小的提示一下,当时我跟着原教程走的时候,有些引入组件的路径跟原教程的不大一样,但是不影响,只要稍稍调一下就行,希望小伙伴们在看的时候也能注意一下这一点,这里插入的代码跟原教程的一样。诶呀~~其实bug何其多,即使是跟着原教程来做可能还是会有的,但是办法总比困难多不是?坑嘛,多踩踩还是极好的)
现在可以npm start
看看效果啦~~
一般情况下,要是不安装任何依赖,只能依靠手动监听以及手动引入store
,但是这样会较为麻烦
react-redux
提供了一个办法,connect
。超级实用(具体怎么个实用法前面的材料有讲到哈,如我一般的小菜鸟们请仔细阅读)
到这里,我们先来安装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 (
当前计数为{this.props.counter.count}
)
}
}
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
找到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(
{RootElement}
,
document.getElementById('app')
)
}
复制代码
现在我们尝试着打开npm start
,查看一下效果
我们可以看到会报错,别担心,我们只要把下面的一行代码删掉就ok啦
resolve: {
alias: {
...
redux: path.join(__dirname, 'src/redux')
}
}
复制代码
呐,上面alias
里面的那行,删掉之后妥妥的就能看到显示的界面咯。
缕一下缕一下:
1.Provider
组件是让所有的组件可以访问到store
。不用手动去传。也不用手动去监听。
2.connect
函数作用是从Redux state
树中读取部分数据,并通过props
来把这些数据提供给要渲染的组件。也传递dispatch(action)
函数到props
。
接下来我们要做的是如何发送异步请求!!!
做之前我们不妨先构思一下请求的步骤:
1.请求开始的时候,界面转圈提示正在加载。isLoadin
g置为true
。
2.请求成功,显示数据。isLoading
置为false
,data
填充数据。
3.请求失败,显示失败。isLoading
置为false
,显示错误信息。
接下来,我们来尝试写一个比较简单的后台请求用户信息的例子。
1.首先我们得先创建一个user.json
,用于请求数据,相当于后台API接口
cd dist
mkdir api
cd api
touch user.json
复制代码
dist/api/user.json
{
"msg": "phoebe",
"name": "brickspert",
"intro": "please give me a star"
}
复制代码
2.创建一个必要的action
函数
cd src/redux/actions
touch userInfo.js
复制代码
找到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";
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
创建函数。
- 接下来我们要创建一个
render
(前面已经提到render
是根据stat
和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
语法,和objiect.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)
}
}
复制代码
4.在有了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());
}
)
}
}
复制代码
在这里我们需要引入redux-thunk
中间件。
npm install --save redux-thunk
中间件的使用,作用和基本概念可以阅读一下Middleware
接下来我们需要引入react-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;
复制代码
好啦,接下来我么就可以尝试着写一个组件来验证一下啦,内心有点小激动呀有木有~~
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 (
{
isLoading ? '请求信息中......' :
(
errorMsg ? errorMsg :
用户信息:
用户名:{userInfo.name}
介绍:{userInfo.intro}
)
}
)
}
}
export default connect((state) => ({userInfo: state.userInfo}), {getUserInfo})(UserInfo);
复制代码
紧接着就是添加路由
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 = () => (
- "/">首页
- "/page1">Page1
- "/counter">Counter
- "/userinfo">UserInfo
"/" component={Home}/>
"/page1" component={Page1}/>
"/counter" component={Counter}/>
"/userinfo" component={UserInfo}/>
);
export default getRouter;
复制代码
到现在可以去运行一下啦(中途要是出现什么小bug,请尝试着努力的去修改哈,有bug才是正常的,多改改是好事儿)看看效果:
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优化
不多说,为了便于调试,我们可以在webpack
配置devtool
!
找到webpack.dev.config.js
devtool: 'inline-source-map'
编译CSS
首先我们需要安装 Microsoft Windows SDK for Windows 7 and .NET Framework 4。
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']
}
复制代码
找到src/pages/Page1/Page1.js
import React, {Component} from 'react';
import './Page1.css';
export default class Page1 extends Component {
render() {
return (
"page-box">
this is page1~
)
}
}
复制代码
ok啦,现在可以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/bg.jpg';
export default class Page1 extends Component {
render() {
return (
"page-box">
this is page1~
)
}
}
复制代码
效果图如下:
按需加载
一般情况下我们打包完之后,所有的页面只生成了一个build.js,当我们首屏加载的时候,就会很慢。因为他也下载了别的界面的Js。这极大程度上拖缓了加载进程。但是如果每个网页都打包了自己单独的js,在进入自己的页面时才会加载对应的js,那么首屏加载就会快很多。
在 react-router 2.0
时代,按需加载需要用到的最关键的一个函数,就是require.ensure()
,它是按需加载能够实现的核心。
但是在4.0
版本,官方放弃了这种处理方式,采用了一个更加简洁的办法,如下:
首先添加依赖:
1.npm install bundle-loader --save-dev
2.新建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;
复制代码
3.改造路由器
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 Loading...
};
const createComponent = (component) => (props) => (
{
(Component) => Component ? :
}
);
const getRouter = () => (
- "/">首页
- "/page1">Page1
- "/counter">Counter
- "/userinfo">UserInfo
"/" component={createComponent(Home)}/>
"/page1" component={createComponent(Page1)}/>
"/counter" component={createComponent(Counter)}/>
"/userinfo" component={createComponent(UserInfo)}/>
);
export default getRouter;
复制代码
现在我们可以打开npm start
看一下具体的效果啦~~打开浏览器,看是不是进入新的页面,都会加载自己的JS的~
接下来你很快就会发现,打开浏览器的时候分不清是哪个页面的js
。不要慌!!!我们只要修改webpack.dev.config.js
,加个chunkFilename
就ok啦
output: {
path: path.join(__dirname, './dist'),
filename: 'bundle.js',
chunkFilename: '[name].js'
}
复制代码
ok了接下来你会看到运行是的名字变成了home.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.j
s,第二次叫home.b.js
。
ok,接下来我们可以照着文档来操作:
webpack.dev.config.js
output: {
path: path.join(__dirname, './dist'),
filename: '[name].[hash].js',
chunkFilename: '[name].[chunkhash].js'
}
复制代码
现在我们每次打包的时候都会增加hash哟
打包之后相应的名字就会改变啦~~请看下图
HtmlWebpackPlugin
使用这个插件,它会自动的把js
插入到模板的index.html
中:
现在开始执行命令:
npm install html-webpack-plugin --save-dev
这时候我们需要在src路径下新建一个index.html
模板:
cd src
touch index.html
复制代码
src/index.html
"en">
"UTF-8">
Document
"app">
复制代码
修改webpack.dev.config.js
,添加pligin
var HtmlWebpackPlugin = require('html-webpack-plugin');
plugins: [new HtmlWebpackPlugin({
filename: 'index.html',
template: path.join(__dirname, 'src/index.html')
})],
复制代码
现在咋们npm install
就可以正常的浏览网站啦~~(很激动有木有~~)现在我们可以把dist/index.html
给删掉哟,因为打包后的文件是存在在内存之中的,咋们看不到。
提取公共代码
看到这个标题千万别懵,也别疑惑为啥要提取公共代码。。。。这肯定是为了减少资源消耗呀啊哈哈
话不多说,请往下看
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
等库生成打包到verdor.hash.js
里面去~~
但是到这里你可能会发现,编译生成的文件app.[hash].js和vendor.[hash].js生成的hash是一样的,这是因为我们每次修改代码的时候都会导致vendor.[hash].js名字改变,那这样的话,我们提取出来的意义也就没有啦。(但是我们这里不做改变,因为存在chunkhash和webpack-dev-server --hot版本不兼容的问题....其实现在我们也可以不深究,到我们有了一定的阅历之后自然而然的就懂了哈)
构建生产环境
概念:开发环境(development)和生产环境(production)的构建目标差异很大。在开发环境中,我们需要具有强大的、具有实时重新加载(live reloading)或热模块替换(hot module replacement)能力的 source map 和 localhost server。而在生产环境中,我们的目标则转向于关注更小的 bundle,更轻量的 source map,以及更优化的资源,以改善加载时间。由于要遵循逻辑分离,我们通常建议为每个环境编写彼此独立的 webpack 配置。
开始执行操作:
touch webpack.config.js
根据原教程,在webpack.dev.config.js
的基础上做了几个小小的修改:
1.先删除webpack-dev-server
相关的东西~
2.devtool
的值改成cheap-module-source-map
3.刚才说的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
文件夹里面是不是生成了我们发布的文件啦~~~
文件压缩
咋们使用UglifyJSPliginL
来压缩生成的文件。
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
,发现运行之后的app.xxx.js
和vendor.xxx.js
不一样了,但是当随意修改一个组件的内容时发现组件的名字变化的同时,vendor.xxx.js
名字也变了。但是这不是我们想要的哈,我们最初想要的是vendor.xxx.js
的名字永久的不变。
那么接下来我们需要这么做:
webpack.config.js
plugins: [
new webpack.HashedModuleIdsPlugin(),
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 build
看看是不是dist
文件夹下的多余的文件都被自动清理了??神奇吧~~
抽取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
})
]
复制代码
好啦到这里你可以试着npm run build
,看看是不是在dist
文件夹下生成了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中间件来处理请求逻辑。
在这之前我们可以先来理一下:
1.请求前dispatch REQUEST请求。
2.成功后dispatch SUCCESS请求,如果定义了afterSuccess()函数,调用它。
3.失败后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);
return;
}
/*解析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
看看,我们发现了一个问题他会报一个错
没错,这是因为我们之前在清dist的时候把它清掉啦啊哈哈,现在只需按照着了路径手动新建即可,步骤跟之前是一样的哈~~
合并提取webpack
公共配置
webpack.common.config.js
主要是用来存放webpack.dev.config.js
和webpack.config.js
共同存放的东西,你也不想每次都要在这两个文件里边敲一样的东西的吧ahhh
在这之前我们先要配置一下webpack-merge
npm install --save-dev webpack-merge
touch webpack.common.config.js
webpack.common.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
commonConfig = {
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',
publicPath: "/"
},
module: {
rules: [{
test: /\.js$/,
use: ['babel-loader?cacheDirectory=true'],
include: path.join(__dirname, 'src')
}, {
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.HashedModuleIdsPlugin(),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor'
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime'
})
],
resolve: {
alias: {
pages: path.join(__dirname, 'src/pages'),
components: path.join(__dirname, 'src/components'),
router: path.join(__dirname, 'src/router'),
actions: path.join(__dirname, 'src/redux/actions'),
reducers: path.join(__dirname, 'src/redux/reducers')
}
}
};
module.exports = commonConfig;
复制代码
webpack.dev.config.js
const merge = require('webpack-merge');
const path = require('path');
const commonConfig = require('./webpack.common.config.js');
const devConfig = {
devtool: 'inline-source-map',
entry: {
app: [
'react-hot-loader/patch',
path.join(__dirname, 'src/index.js')
]
},
output: {
/*这里本来应该是[chunkhash]的,但是由于[chunkhash]和react-hot-loader不兼容。只能妥协*/
filename: '[name].[hash].js'
},
module: {
rules: [{
test: /\.css$/,
use: ["style-loader", "css-loader"]
}]
},
devServer: {
contentBase: path.join(__dirname, './dist'),
historyApiFallback: true,
host: '0.0.0.0',
}
};
module.exports = merge({
customizeArray(a, b, key) {
/*entry.app不合并,全替换*/
if (key === 'entry.app') {
return b;
}
return undefined;
}
})(commonConfig, devConfig);
复制代码
webpack.config.js
const merge = require('webpack-merge');
const webpack = require('webpack');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const commonConfig = require('./webpack.common.config.js');
const publicConfig = {
devtool: 'cheap-module-source-map',
module: {
rules: [{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: "css-loader"
})
}]
},
plugins: [
new CleanWebpackPlugin(['dist/*.*']),
new UglifyJSPlugin(),
new webpack.DefinePlugin({
'process.env': {
'NODE_ENV': JSON.stringify('production')
}
}),
new ExtractTextPlugin({
filename: '[name].[contenthash:5].css',
allChunks: true
})
]
};
module.exports = merge(commonConfig, publicConfig);
复制代码
好啦,到这里基本上算是大功告成啦,很开心有木有!!!其实只要认真的照着教程敲一遍(不懂的再看着原教程),真的是蛮不错的。笔者跟着教程巧了一遍,这次写这篇博客主要也是想巩固一下之前学到的知识点,因为在之前很多概念性的东西还没很能理解,但是走第二遍的时候发现都懂了很多新的东西ahhh甚是开心呐本教程最主要的就是为了笔者巩固所学以及分享而用,纯属分享不带任何商业目的哈有意向的可以去看看原教程,猛戳这里。同时也希望这篇文章对刚入门的你们有所帮助。