本文從零開始,逐步講解如何用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工具鏈可能會有些干擾,因此就不在項目中加了。
後面我會新建一個分支,把這些完整的功能都加上,同時也會對當前的目錄結構進行優化。