本文从零开始,逐步讲解如何用react全家桶搭建一个完整的react项目。文中针对react、webpack、babel、react-route、redux、redux-saga的核心配置会加以讲解,但愿经过这个项目,能够系统的了解react技术栈的主要知识,避免搭建一次后面就忘记的状况。html
代码库:https://github.com/teapot-py/react-demo
首先关于主要的npm包版本列一下:前端
思考一下webpack到底作了什么事情?其实简单来讲,就是从入口文件开始,不断寻找依赖,同时为了解析各类不一样的文件加载相应的loader,最后生成咱们但愿的类型的目标文件。node
这个过程就像是在一个迷宫里寻宝,咱们从入口进入,同时咱们也会不断的接收到下一处宝藏的提示信息,咱们对信息进行解码,而解码的时候可能须要一些工具,好比说钥匙,而loader就像是这样的钥匙,而后获得咱们能够识别的内容。react
回到咱们的项目,首先进行项目的初始化,分别执行以下命令webpack
mkdir react-demo // 新建项目文件夹
cd react-demo // cd到项目目录下
npm init // npm初始化
复制代码
引入webpackgit
npm i webpack --save
touch webpack.config.js
复制代码
对webpack进行简单配置,更新webpack.config.jsgithub
const path = require('path');
module.exports = {
entry: './app.js', // 入口文件
output: {
path: path.resolve(__dirname, 'dist'), // 定义输出目录
filename: 'my-first-webpack.bundle.js' // 定义输出文件名称
}
};
复制代码
更新package.json文件,在scripts中添加webpack执行命令web
"scripts": {
"dev": "./node_modules/.bin/webpack --config webpack.config.js"
}
复制代码
若是有报错请按提示安装webpack-clinpm
npm i webpack-cli
复制代码
执行webpackjson
npm run dev
复制代码
若是在项目文件夹下生成了dist文件,说明咱们的配置是没有问题的。
安装react相关包
npm install react react-dom --save
复制代码
更新app.js入口文件
import React from 'react import ReactDom from 'react-dom'; import App from './src/views/App'; ReactDom.render(<App />, document.getElementById('root')); 复制代码
建立目录 src/views/App,在App目录下,新建index.js文件做为App组件,index.js文件内容以下:
import React from 'react';
class App extends React.Component {
constructor(props) {
super(props);
}
render() {
return (<div>App Container</div>);
}
}
export default App;
复制代码
在根目录下建立模板文件index.html
<!DOCTYPE html>
<html>
<head>
<title>index</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
</head>
<body>
<div id="root"></div>
</body>
</html>
复制代码
到了这一步其实关于react的引入就OK了,不过目前还有不少问题没有解决
Babel是一个工具链,主要用于在旧的浏览器或环境中将ECMAScript2015+的代码转换为向后兼容版本的JavaScript代码。
安装babel-loader,@babel/core,@babel/preset-env,@babel/preset-react
npm i babel-loader@8 @babel/core @babel/preset-env @babel/preset-react -D
复制代码
更新webpack.config.js
module: {
rules: [
{
test: /\.js$/, // 匹配.js文件
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
}
]
}
复制代码
根目录下建立并配置.babelrc文件
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}
复制代码
配置HtmlWebPackPlugin
这个插件最主要的做用是将js代码经过<script>标签注入到 HTML 文件中
npm i html-webpack-plugin -D
复制代码
webpack新增HtmlWebPackPlugin配置
至此,咱们看一下webpack.config.js文件的完整结构
const path = require('path');
const HtmlWebPackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './app.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-first-webpack.bundle.js'
},
mode: 'development',
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
}
]
},
plugins: [
new HtmlWebPackPlugin({
template: './index.html',
filename: path.resolve(__dirname, 'dist/index.html')
})
]
};
复制代码
执行 npm run start,生成 dist文件夹
当前目录结构以下
能够看到在dist文件加下生成了index.html文件,咱们在浏览器中打开文件便可看到App组件内容。
webpack-dev-server能够极大的提升咱们的开发效率,经过监听文件变化,自动更新页面
安装 webpack-dev-server 做为 dev 依赖项
npm i webpack-dev-server -D
复制代码
更新package.json的启动脚本
“dev": "webpack-dev-server --config webpack.config.js --open" 复制代码
webpack.config.js新增devServer配置
devServer: {
hot: true, // 热替换
contentBase: path.join(__dirname, 'dist'), // server文件的根目录
compress: true, // 开启gzip
port: 8080, // 端口
},
plugins: [
new webpack.HotModuleReplacementPlugin(), // HMR容许在运行时更新各类模块,而无需进行彻底刷新
new HtmlWebPackPlugin({
template: './index.html',
filename: path.resolve(__dirname, 'dist/index.html')
})
]
复制代码
redux是用于前端数据管理的包,避免因项目过大前端数据没法管理的问题,同时经过单项数据流管理前端的数据状态。
建立多个目录
下面咱们来经过redux实现一个计数器的功能
安装依赖
npm i redux react-redux -D
复制代码
在actions文件夹下建立index.js文件
export const increment = () => {
return {
type: 'INCREMENT',
};
};
复制代码
在reducers文件夹下建立index.js文件
const initialState = {
number: 0
};
const incrementReducer = (state = initialState, action) => {
switch(action.type) {
case 'INCREMENT': {
state.number += 1
return { ...state }
break
};
default: return state;
}
};
export default incrementReducer;
复制代码
更新store.js
import { createStore } from 'redux';
import incrementReducer from './reducers/index';
const store = createStore(incrementReducer);
export default store;
复制代码
更新入口文件app.js
import App from './src/views/App';
import ReactDom from 'react-dom';
import React from 'react';
import store from './src/store';
import { Provider } from 'react-redux';
ReactDom.render(
<Provider store={store}>
<App />
</Provider>
, document.getElementById('root'));
复制代码
更新App组件
import React from 'react';
import { connect } from 'react-redux';
import { increment } from '../../actions/index';
class App extends React.Component {
constructor(props) {
super(props);
}
onClick() {
this.props.dispatch(increment())
}
render() {
return (
<div>
<div>current number: {this.props.number} <button onClick={()=>this.onClick()}>点击+1</button></div>
</div>
);
}
}
export default connect(
state => ({
number: state.number
})
)(App);
复制代码
点击旁边的数字会不断地+1
redux-saga经过监听action来执行有反作用的task,以保持action的简洁性。引入了sagas的机制和generator的特性,让redux-saga很是方便地处理复杂异步问题。
redux-saga的原理其实提及来也很简单,经过劫持异步action,在redux-saga中进行异步操做,异步结束后将结果传给另外的action。
下面就接着咱们计数器的例子,来实现一个异步的+1操做。
安装依赖包
npm i redux-saga -D
复制代码
新建src/sagas/index.js文件
import { delay } from 'redux-saga'
import { put, takeEvery } from 'redux-saga/effects'
export function* incrementAsync() {
yield delay(2000)
yield put({ type: 'INCREMENT' })
}
export function* watchIncrementAsync() {
yield takeEvery('INCREMENT_ASYNC', incrementAsync)
}
复制代码
解释下所作的事情,将watchIncrementAsync理解为一个saga,在这个saga中监听了名为INCREMENT_ASYNC的action,当INCREMENT_ASYNC被dispatch时,会调用incrementAsync方法,在该方法中作了异步操做,而后将结果传给名为INCREMENT的action进而更新store。
更新store.js
在store中加入redux-saga中间件
import { createStore, applyMiddleware } from 'redux';
import incrementReducer from './reducers/index';
import createSagaMiddleware from 'redux-saga'
import { watchIncrementAsync } from './sagas/index'
const sagaMiddleware = createSagaMiddleware()
const store = createStore(incrementReducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(watchIncrementAsync)
export default store;
复制代码
更新App组件
在页面中新增异步提交按钮,观察异步结果
import React from 'react';
import { connect } from 'react-redux';
import { increment } from '../../actions/index';
class App extends React.Component {
constructor(props) {
super(props);
}
onClick() {
this.props.dispatch(increment())
}
onClick2() {
this.props.dispatch({ type: 'INCREMENT_ASYNC' })
}
render() {
return (
<div>
<div>current number: {this.props.number} <button onClick={()=>this.onClick()}>点击+1</button></div>
<div>current number: {this.props.number} <button onClick={()=>this.onClick2()}>点击2秒后+1</button></div>
</div>
);
}
}
export default connect(
state => ({
number: state.number
})
)(App);
复制代码
观察结果咱们会发现以下报错:
这是由于在redux-saga中用到了Generator函数,以咱们目前的babel配置来讲并不支持解析generator,须要安装@babel/plugin-transform-runtime
npm install --save-dev @babel/plugin-transform-runtime
复制代码
这里关于babel-polyfill、和transfor-runtime作进一步解释
Babel默认只转换新的JavaScript语法,而不转换新的API。例如,Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(好比Object.assign)都不会转译。若是想使用这些新的对象和方法,必须使用 babel-polyfill,为当前环境提供一个垫片。
Babel转译后的代码要实现源代码一样的功能须要借助一些帮助函数,而这些帮助函数可能会重复出如今一些模块里,致使编译后的代码体积变大。
Babel 为了解决这个问题,提供了单独的包babel-runtime供编译模块复用工具函数。
在没有使用babel-runtime以前,库和工具包通常不会直接引入 polyfill。不然像Promise这样的全局对象会污染全局命名空间,这就要求库的使用者本身提供 polyfill。这些 polyfill通常在库和工具的使用说明中会提到,好比不少库都会有要求提供 es5的polyfill。
在使用babel-runtime后,库和工具只要在 package.json中增长依赖babel-runtime,交给babel-runtime去引入 polyfill 就好了;
Babel插件通常尽量拆成小的力度,开发者能够按需引进。好比对ES6转ES5的功能,Babel官方拆成了20+个插件。
这样的好处显而易见,既提升了性能,也提升了扩展性。好比开发者想要体验ES6的箭头函数特性,那他只须要引入transform-es2015-arrow-functions插件就能够,而不是加载ES6全家桶。
但不少时候,逐个插件引入的效率比较低下。好比在项目开发中,开发者想要将全部ES6的代码转成ES5,插件逐个引入的方式使人抓狂,不单费力,并且容易出错。
这个时候,能够采用Babel Preset。
能够简单的把Babel Preset视为Babel Plugin的集合。好比babel-preset-es2015就包含了全部跟ES6转换有关的插件。
{
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": false,
"helpers": true,
"regenerator": true,
"useESModules": false
}
]
]
}
复制代码
点击按钮会在2秒后执行+1操做。
在web应用开发中,路由系统是不可或缺的一部分。在浏览器当前的URL发生变化时,路由系统会作出一些响应,用来保证用户界面与URL的同步。随着单页应用时代的到来,为之服务的前端路由系统也相继出现了。而react-route则是与react相匹配的前端路由。
引入react-router-dom
npm install --save react-router-dom -D
复制代码
更新app.js入口文件增长路由匹配规则
import App from './src/views/App';
import ReactDom from 'react-dom';
import React from 'react';
import store from './src/store';
import { Provider } from 'react-redux';
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
const About = () => <h2>页面一</h2>;
const Users = () => <h2>页面二</h2>;
ReactDom.render(
<Provider store={store}>
<Router>
<Switch>
<Route path="/" exact component={App} />
<Route path="/about/" component={About} />
<Route path="/users/" component={Users} />
</Switch>
</Router>
</Provider>
, document.getElementById('root'));
复制代码
更新App组件,展现路由效果
import React from 'react';
import { connect } from 'react-redux';
import { increment } from '../../actions/index';
import { Link } from "react-router-dom";
class App extends React.Component {
constructor(props) {
super(props);
}
onClick() {
this.props.dispatch(increment())
}
onClick2() {
this.props.dispatch({ type: 'INCREMENT_ASYNC' })
}
render() {
return (
<div>
<div>react-router 测试</div>
<nav>
<ul>
<li>
<Link to="/about/">页面一</Link>
</li>
<li>
<Link to="/users/">页面二</Link>
</li>
</ul>
</nav>
<br/>
<div>redux & redux-saga测试</div>
<div>current number: {this.props.number} <button onClick={()=>this.onClick()}>点击+1</button></div>
<div>current number: {this.props.number} <button onClick={()=>this.onClick2()}>点击2秒后+1</button></div>
</div>
);
}
}
export default connect(
state => ({
number: state.number
})
)(App);
复制代码
点击列表能够跳转相关路由
至此,咱们已经一步步的,完成了一个简单可是功能齐全的react项目的搭建,下面回顾一下咱们作的工做
麻雀虽小,五脏俱全,但愿经过最简单的代码快速的理解react工具链。其实这个小项目中仍是不少不完善的地方,好比说样式的解析、Eslint检查、生产环境配置,虽然这几项是一个完整项目不可缺乏的部分,可是就demo项目来讲,对咱们理解react工具链可能会有些干扰,因此就不在项目中加了。
后面我会新建一个分支,把这些完整的功能都加上,同时也会对当前的目录结构进行优化。