react全家桶從0搭建一個(gè)完整的react項(xiàng)目(react-router4、redux、redux-saga)

2021-5-31    前端達(dá)人

react全家桶從0到1(最新)

本文從零開始,逐步講解如何用react全家桶搭建一個(gè)完整的react項(xiàng)目。文中針對(duì)react、webpack、babel、react-route、redux、redux-saga的核心配置會(huì)加以講解,通過這個(gè)項(xiàng)目,可以系統(tǒng)的了解react技術(shù)棧的主要知識(shí),避免搭建一次后面就忘記的情況。

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

首先關(guān)于主要的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到底做了什么事情?其實(shí)簡單來說,就是從入口文件開始,不斷尋找依賴,同時(shí)為了解析各種不同的文件加載相應(yīng)的loader,最后生成我們希望的類型的目標(biāo)文件。

這個(gè)過程就像是在一個(gè)迷宮里尋寶,我們從入口進(jìn)入,同時(shí)我們也會(huì)不斷的接收到下一處寶藏的提示信息,我們對(duì)信息進(jìn)行解碼,而解碼的時(shí)候可能需要一些工具,比如說鑰匙,而loader就像是這樣的鑰匙,然后得到我們可以識(shí)別的內(nèi)容。

回到我們的項(xiàng)目,首先進(jìn)行項(xiàng)目的初始化,分別執(zhí)行如下命令

mkdir react-demo // 新建項(xiàng)目文件夾
cd react-demo // cd到項(xiàng)目目錄下
npm init // npm初始化 
  • 1
  • 2
  • 3

引入webpack

npm i webpack --save
touch webpack.config.js 
  • 1
  • 2

對(duì)webpack進(jìn)行簡單配置,更新webpack.config.js

const path = require('path');

module.exports = {
  entry: './app.js', // 入口文件
  output: {
    path: path.resolve(__dirname, 'dist'), // 定義輸出目錄
    filename: 'my-first-webpack.bundle.js'  // 定義輸出文件名稱
  }
}; 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

更新package.json文件,在scripts中添加webpack執(zhí)行命令

"scripts": {
  "dev": "./node_modules/.bin/webpack --config webpack.config.js"
} 
  • 1
  • 2
  • 3

如果有報(bào)錯(cuò)請(qǐng)按提示安裝webpack-cli

npm i webpack-cli 
  • 1

執(zhí)行webpack

npm run dev 
  • 1

如果在項(xiàng)目文件夾下生成了dist文件,說明我們的配置是沒有問題的。

接入react

安裝react相關(guān)包

npm install react react-dom --save 
  • 1

更新app.js入口文件

import React from 'react
import ReactDom from 'react-dom';
import App from './src/views/App';

ReactDom.render(<App />, document.getElementById('root')); 
  • 1
  • 2
  • 3
  • 4
  • 5

創(chuàng)建目錄 src/views/App,在App目錄下,新建index.js文件作為App組件,index.js文件內(nèi)容如下:

import React from 'react';

class App extends React.Component {

    constructor(props) {
        super(props);
    }

    render() {
        return (<div>App Container</div>);
    }
}
export default App; 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

在根目錄下創(chuàng)建模板文件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> 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

到了這一步其實(shí)關(guān)于react的引入就OK了,不過目前還有很多問題沒有解決

  1. 如何解析JS文件的代碼?
  2. 如何將js文件加入模板文件中?

Babel解析js文件

Babel是一個(gè)工具鏈,主要用于在舊的瀏覽器或環(huán)境中將ECMAScript2015+的代碼轉(zhuǎn)換為向后兼容版本的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
  1. babel-loader:使用Babel轉(zhuǎn)換JavaScript依賴關(guān)系的Webpack加載器, 簡單來講就是webpack和babel中間層,允許webpack在遇到j(luò)s文件時(shí)用bable來解析
  2. @babel/core:即babel-core,將ES6代碼轉(zhuǎn)換為ES5。7.0之后,包名升級(jí)為@babel/core。@babel相當(dāng)于一種官方標(biāo)記,和以前大家隨便起名形成區(qū)別。
  3. @babel/preset-env:即babel-preset-env,根據(jù)您要支持的瀏覽器,決定使用哪些transformations / plugins 和 polyfills,例如為舊瀏覽器提供現(xiàn)代瀏覽器的新特性。
  4. @babel/preset-react:即 babel-preset-react,針對(duì)所有React插件的Babel預(yù)設(shè),例如將JSX轉(zhuǎn)換為函數(shù).

更新webpack.config.js

 module: {
    rules: [
      {
        test: /\.js$/, // 匹配.js文件
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader'
        }
      }
    ]
  } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

根目錄下創(chuàng)建并配置.babelrc文件

{
  "presets": ["@babel/preset-env", "@babel/preset-react"]
} 
  • 1
  • 2
  • 3

配置HtmlWebPackPlugin

這個(gè)插件最主要的作用是將js代碼通過

npm i html-webpack-plugin -D 
  • 1

webpack新增HtmlWebPackPlugin配置

至此,我們看一下webpack.config.js文件的完整結(jié)構(gòu)

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')
    })
  ]
}; 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

執(zhí)行 npm run start,生成 dist文件夾

當(dāng)前目錄結(jié)構(gòu)如下
目錄結(jié)構(gòu)

可以看到在dist文件加下生成了index.html文件,我們?cè)跒g覽器中打開文件即可看到App組件內(nèi)容。

配置 webpack-dev-server

webpack-dev-server可以極大的提高我們的開發(fā)效率,通過監(jiān)聽文件變化,自動(dòng)更新頁面

安裝 webpack-dev-server 作為 dev 依賴項(xiàng)

npm i webpack-dev-server -D 
  • 1

更新package.json的啟動(dòng)腳本

“dev": "webpack-dev-server --config webpack.config.js --open" 
  • 1

webpack.config.js新增devServer配置

devServer: {
  hot: true, // 熱替換
  contentBase: path.join(__dirname, 'dist'), // server文件的根目錄
  compress: true, // 開啟gzip
  port: 8080, // 端口
},
plugins: [
  new webpack.HotModuleReplacementPlugin(), // HMR允許在運(yùn)行時(shí)更新各種模塊,而無需進(jìn)行完全刷新
  new HtmlWebPackPlugin({
    template: './index.html',
    filename: path.resolve(__dirname, 'dist/index.html')
  })
] 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

引入redux

redux是用于前端數(shù)據(jù)管理的包,避免因項(xiàng)目過大前端數(shù)據(jù)無法管理的問題,同時(shí)通過單項(xiàng)數(shù)據(jù)流管理前端的數(shù)據(jù)狀態(tài)。

創(chuàng)建多個(gè)目錄

  1. 新建src/actions目錄,用于創(chuàng)建action函數(shù)
  2. 新建src/reducers目錄,用于創(chuàng)建reducers
  3. 新建src/store目錄,用于創(chuàng)建store

下面我們來通過redux實(shí)現(xiàn)一個(gè)計(jì)數(shù)器的功能

安裝依賴

npm i redux react-redux -D 
  • 1

在actions文件夾下創(chuàng)建index.js文件

export const increment = () => {
  return {
    type: 'INCREMENT',
  };
}; 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在reducers文件夾下創(chuàng)建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; 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

更新store.js

import { createStore } from 'redux';
import incrementReducer from './reducers/index';

const store = createStore(incrementReducer);

export default store; 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

更新入口文件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')); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

更新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()}>點(diǎn)擊+1</button></div>

            </div>
        );
    }
}
export default connect(
    state => ({
        number: state.number
    })
)(App); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

點(diǎn)擊旁邊的數(shù)字會(huì)不斷地+1

引入redux-saga

redux-saga通過監(jiān)聽action來執(zhí)行有副作用的task,以保持action的簡潔性。引入了sagas的機(jī)制和generator的特性,讓redux-saga非常方便地處理復(fù)雜異步問題。
redux-saga的原理其實(shí)說起來也很簡單,通過劫持異步action,在redux-saga中進(jìn)行異步操作,異步結(jié)束后將結(jié)果傳給另外的action。

下面就接著我們計(jì)數(shù)器的例子,來實(shí)現(xiàn)一個(gè)異步的+1操作。

安裝依賴包

npm i redux-saga -D 
  • 1

新建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)
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

解釋下所做的事情,將watchIncrementAsync理解為一個(gè)saga,在這個(gè)saga中監(jiān)聽了名為INCREMENT_ASYNC的action,當(dāng)INCREMENT_ASYNC被dispatch時(shí),會(huì)調(diào)用incrementAsync方法,在該方法中做了異步操作,然后將結(jié)果傳給名為INCREMENT的action進(jìn)而更新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; 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

更新App組件

在頁面中新增異步提交按鈕,觀察異步結(jié)果

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()}>點(diǎn)擊+1</button></div>
                <div>current number: {this.props.number} <button onClick={()=>this.onClick2()}>點(diǎn)擊2秒后+1</button></div>
            </div>
        );
    }
}
export default connect(
    state => ({
        number: state.number
    })
)(App); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

觀察結(jié)果我們會(huì)發(fā)現(xiàn)如下報(bào)錯(cuò):

這是因?yàn)樵趓edux-saga中用到了Generator函數(shù),以我們目前的babel配置來說并不支持解析generator,需要安裝@babel/plugin-transform-runtime

npm install --save-dev @babel/plugin-transform-runtime 
  • 1

這里關(guān)于babel-polyfill、和transfor-runtime做進(jìn)一步解釋

babel-polyfill

Babel默認(rèn)只轉(zhuǎn)換新的JavaScript語法,而不轉(zhuǎn)換新的API。例如,Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局對(duì)象,以及一些定義在全局對(duì)象上的方法(比如Object.assign)都不會(huì)轉(zhuǎn)譯。如果想使用這些新的對(duì)象和方法,必須使用 babel-polyfill,為當(dāng)前環(huán)境提供一個(gè)墊片。

babel-runtime

Babel轉(zhuǎn)譯后的代碼要實(shí)現(xiàn)源代碼同樣的功能需要借助一些幫助函數(shù),而這些幫助函數(shù)可能會(huì)重復(fù)出現(xiàn)在一些模塊里,導(dǎo)致編譯后的代碼體積變大。
Babel 為了解決這個(gè)問題,提供了單獨(dú)的包babel-runtime供編譯模塊復(fù)用工具函數(shù)。
在沒有使用babel-runtime之前,庫和工具包一般不會(huì)直接引入 polyfill。否則像Promise這樣的全局對(duì)象會(huì)污染全局命名空間,這就要求庫的使用者自己提供 polyfill。這些 polyfill一般在庫和工具的使用說明中會(huì)提到,比如很多庫都會(huì)有要求提供 es5的polyfill。
在使用babel-runtime后,庫和工具只要在 package.json中增加依賴babel-runtime,交給babel-runtime去引入 polyfill 就行了;
詳細(xì)解釋可以參考

babel presets 和 plugins的區(qū)別

Babel插件一般盡可能拆成小的力度,開發(fā)者可以按需引進(jìn)。比如對(duì)ES6轉(zhuǎn)ES5的功能,Babel官方拆成了20+個(gè)插件。
這樣的好處顯而易見,既提高了性能,也提高了擴(kuò)展性。比如開發(fā)者想要體驗(yàn)ES6的箭頭函數(shù)特性,那他只需要引入transform-es2015-arrow-functions插件就可以,而不是加載ES6全家桶。
但很多時(shí)候,逐個(gè)插件引入的效率比較低下。比如在項(xiàng)目開發(fā)中,開發(fā)者想要將所有ES6的代碼轉(zhuǎn)成ES5,插件逐個(gè)引入的方式令人抓狂,不單費(fèi)力,而且容易出錯(cuò)。
這個(gè)時(shí)候,可以采用Babel Preset。
可以簡單的把Babel Preset視為Babel Plugin的集合。比如babel-preset-es2015就包含了所有跟ES6轉(zhuǎn)換有關(guān)的插件。

更新.babelrc文件配置,支持genrator

{
  "presets": ["@babel/preset-env", "@babel/preset-react"],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": false,
        "helpers": true,
        "regenerator": true,
        "useESModules": false
      }
    ]
  ]
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14


點(diǎn)擊按鈕會(huì)在2秒后執(zhí)行+1操作。

引入react-router

在web應(yīng)用開發(fā)中,路由系統(tǒng)是不可或缺的一部分。在瀏覽器當(dāng)前的URL發(fā)生變化時(shí),路由系統(tǒng)會(huì)做出一些響應(yīng),用來保證用戶界面與URL的同步。隨著單頁應(yīng)用時(shí)代的到來,為之服務(wù)的前端路由系統(tǒng)也相繼出現(xiàn)了。而react-route則是與react相匹配的前端路由。

引入react-router-dom

npm install --save react-router-dom -D 
  • 1

更新app.js入口文件增加路由匹配規(guī)則

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')); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

更新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()}>點(diǎn)擊+1</button></div>
                <div>current number: {this.props.number} <button onClick={()=>this.onClick2()}>點(diǎn)擊2秒后+1</button></div>
            </div>
        );
    }
}
export default connect(
    state => ({
        number: state.number
    })
)(App); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48


點(diǎn)擊列表可以跳轉(zhuǎn)相關(guān)路由

總結(jié)

至此,我們已經(jīng)一步步的,完成了一個(gè)簡單但是功能齊全的react項(xiàng)目的搭建,下面回顧一下我們做的工作

  1. 引入webpack
  2. 引入react
  3. 引入babel解析react
  4. 接入webpack-dev-server提高前端開發(fā)效率
  5. 引入redux實(shí)現(xiàn)一個(gè)increment功能
  6. 引入redux-saga實(shí)現(xiàn)異步處理
  7. 引入react-router實(shí)現(xiàn)前端路由

麻雀雖小,五臟俱全,希望通過最簡單的代碼快速的理解react工具鏈。其實(shí)這個(gè)小項(xiàng)目中還是很多不完善的地方,比如說樣式的解析、Eslint檢查、生產(chǎn)環(huán)境配置,雖然這幾項(xiàng)是一個(gè)完整項(xiàng)目不可缺少的部分,但是就demo項(xiàng)目來說,對(duì)我們理解react工具鏈可能會(huì)有些干擾,所以就不在項(xiàng)目中加了。

后面會(huì)新建一個(gè)分支,把這些完整的功能都加上,同時(shí)也會(huì)對(duì)當(dāng)前的目錄結(jié)構(gòu)進(jìn)行優(yōu)化。



藍(lán)藍(lán)設(shè)計(jì)建立了UI設(shè)計(jì)分享群,每天會(huì)分享國內(nèi)外的一些優(yōu)秀設(shè)計(jì),如果有興趣的話,可以進(jìn)入一起成長學(xué)習(xí),請(qǐng)掃碼藍(lán)小助,報(bào)下信息,藍(lán)小助會(huì)請(qǐng)您入群。歡迎您加入噢~~希望得到建議咨詢、商務(wù)合作,也請(qǐng)與我們聯(lián)系。

截屏2021-05-13 上午11.41.03.png


部分借鑒自:csdn  作者:鄭清

原文鏈接:

分享此文一切功德,皆悉回向給文章原作者及眾讀者.
免責(zé)聲明:藍(lán)藍(lán)設(shè)計(jì)尊重原作者,文章的版權(quán)歸原作者。如涉及版權(quán)問題,請(qǐng)及時(shí)與我們?nèi)〉寐?lián)系,我們立即更正或刪除。

藍(lán)藍(lán)設(shè)計(jì)sillybuy.com )是一家專注而深入的界面設(shè)計(jì)公司,為期望卓越的國內(nèi)外企業(yè)提供卓越的UI界面設(shè)計(jì)、BS界面設(shè)計(jì) 、 cs界面設(shè)計(jì) 、 ipad界面設(shè)計(jì) 、 包裝設(shè)計(jì) 、 圖標(biāo)定制 、 用戶體驗(yàn) 、交互設(shè)計(jì)、 網(wǎng)站建設(shè) 、平面設(shè)計(jì)服務(wù)

分享本文至:

日歷

鏈接

個(gè)人資料

存檔