react全家桶從0到1(react-router4、redux、redux-saga)

wupengyu發表於2019-01-18

本文從零開始,逐步講解如何用react全家桶搭建一個完整的react專案。文中針對react、webpack、babel、react-route、redux、redux-saga的核心配置會加以講解,希望通過這個專案,可以系統的瞭解react技術棧的主要知識,避免搭建一次後面就忘記的情況。

程式碼庫:https://github.com/teapot-py/react-demo
首先關於主要的npm包版本列一下:

  1. react@16.7.0
  2. webpack@4.28.4
  3. babel@7+
  4. react-router@4.3.1
  5. redux@4+

從webpack開始

思考一下webpack到底做了什麼事情?其實簡單來說,就是從入口檔案開始,不斷尋找依賴,同時為了解析各種不同的檔案載入相應的loader,最後生成我們希望的型別的目標檔案。

這個過程就像是在一個迷宮裡尋寶,我們從入口進入,同時我們也會不斷的接收到下一處寶藏的提示資訊,我們對資訊進行解碼,而解碼的時候可能需要一些工具,比如說鑰匙,而loader就像是這樣的鑰匙,然後得到我們可以識別的內容。

回到我們的專案,首先進行專案的初始化,分別執行如下命令

mkdir react-demo // 新建專案資料夾
cd react-demo // cd到專案目錄下
npm init // npm初始化
複製程式碼

引入webpack

npm i webpack --save
touch webpack.config.js
複製程式碼

對webpack進行簡單配置,更新webpack.config.js

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執行命令

"scripts": {
  "dev": "./node_modules/.bin/webpack --config webpack.config.js"
}
複製程式碼

如果有報錯請按提示安裝webpack-cli

npm i webpack-cli
複製程式碼

執行webpack

npm run dev
複製程式碼

如果在專案資料夾下生成了dist檔案,說明我們的配置是沒有問題的。

接入react

安裝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了,不過目前還有很多問題沒有解決

  1. 如何解析JS檔案的程式碼?
  2. 如何將js檔案加入模板檔案中?

Babel解析js檔案

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
複製程式碼
  1. babel-loader:使用Babel轉換JavaScript依賴關係的Webpack載入器, 簡單來講就是webpack和babel中間層,允許webpack在遇到js檔案時用bable來解析
  2. @babel/core:即babel-core,將ES6程式碼轉換為ES5。7.0之後,包名升級為@babel/core。@babel相當於一種官方標記,和以前大家隨便起名形成區別。
  3. @babel/preset-env:即babel-preset-env,根據您要支援的瀏覽器,決定使用哪些transformations / plugins 和 polyfills,例如為舊瀏覽器提供現代瀏覽器的新特性。
  4. @babel/preset-react:即 babel-preset-react,針對所有React外掛的Babel預設,例如將JSX轉換為函式.

更新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可以極大的提高我們的開發效率,通過監聽檔案變化,自動更新頁面
安裝 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是用於前端資料管理的包,避免因專案過大前端資料無法管理的問題,同時通過單項資料流管理前端的資料狀態。

建立多個目錄

  1. 新建src/actions目錄,用於建立action函式
  2. 新建src/reducers目錄,用於建立reducers
  3. 新建src/store目錄,用於建立store

下面我們來通過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

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-polyfill

Babel預設只轉換新的JavaScript語法,而不轉換新的API。例如,Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全域性物件,以及一些定義在全域性物件上的方法(比如Object.assign)都不會轉譯。如果想使用這些新的物件和方法,必須使用 babel-polyfill,為當前環境提供一個墊片。

babel-runtime

Babel轉譯後的程式碼要實現原始碼同樣的功能需要藉助一些幫助函式,而這些幫助函式可能會重複出現在一些模組裡,導致編譯後的程式碼體積變大。
Babel 為了解決這個問題,提供了單獨的包babel-runtime供編譯模組複用工具函式。
在沒有使用babel-runtime之前,庫和工具包一般不會直接引入 polyfill。否則像Promise這樣的全域性物件會汙染全域性名稱空間,這就要求庫的使用者自己提供 polyfill。這些 polyfill一般在庫和工具的使用說明中會提到,比如很多庫都會有要求提供 es5的polyfill。
在使用babel-runtime後,庫和工具只要在 package.json中增加依賴babel-runtime,交給babel-runtime去引入 polyfill 就行了;

詳細解釋可以參考

babel presets 和 plugins的區別

Babel外掛一般儘可能拆成小的力度,開發者可以按需引進。比如對ES6轉ES5的功能,Babel官方拆成了20+個外掛。
這樣的好處顯而易見,既提高了效能,也提高了擴充套件性。比如開發者想要體驗ES6的箭頭函式特性,那他只需要引入transform-es2015-arrow-functions外掛就可以,而不是載入ES6全家桶。
但很多時候,逐個外掛引入的效率比較低下。比如在專案開發中,開發者想要將所有ES6的程式碼轉成ES5,外掛逐個引入的方式令人抓狂,不單費力,而且容易出錯。
這個時候,可以採用Babel Preset。
可以簡單的把Babel Preset視為Babel Plugin的集合。比如babel-preset-es2015就包含了所有跟ES6轉換有關的外掛。

更新.babelrc檔案配置,支援genrator

{
  "presets": ["@babel/preset-env", "@babel/preset-react"],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": false,
        "helpers": true,
        "regenerator": true,
        "useESModules": false
      }
    ]
  ]
}
複製程式碼


點選按鈕會在2秒後執行+1操作。

引入react-router

在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專案的搭建,下面回顧一下我們做的工作

  1. 引入webpack
  2. 引入react
  3. 引入babel解析react
  4. 接入webpack-dev-server提高前端開發效率
  5. 引入redux實現一個increment功能
  6. 引入redux-saga實現非同步處理
  7. 引入react-router實現前端路由

麻雀雖小,五臟俱全,希望通過最簡單的程式碼快速的理解react工具鏈。其實這個小專案中還是很多不完善的地方,比如說樣式的解析、Eslint檢查、生產環境配置,雖然這幾項是一個完整專案不可缺少的部分,但是就demo專案來說,對我們理解react工具鏈可能會有些干擾,所以就不在專案中加了。
後面我會新建一個分支,把這些完整的功能都加上,同時也會對當前的目錄結構進行優化。

程式碼庫:https://github.com/teapot-py/react-demo


相關文章