用webpack4從零開始構建react腳手架

lcc發表於2021-09-09

使用腳手架

git clone git@github.com:xiehaitao0229/react-wepack4-xht.git cd react-webpack4-xht

`npm run dev`  //  啟動本地
`npm run build`  //  打包線上環境
`npm run clean` //  清除線上環境打包出來的檔案
`npm run test` //  單元測試的工具庫
`npm run fix` //  修復eslint的寫法
`npm run format` //  格式化程式碼
`npm run precommit` //  commit 程式碼到git倉庫的檢查
複製程式碼

webpack今年推出的4這個版本就一直關注很學習它,webpack4這個版本借鑑了parcel的零配置,打包速度變得更快,值得大家去跟進學習。

既然我們已經迎接了webpack4的到來了,那麼就一起來使用一下,即使你沒用過之前的版本,沒關係,我們重新出發,將工作中常用到的配置寫給大家來看

安裝webpack

  • 需要先在專案中npm init初始化一下,生成package.json
  • 建議node版本安裝到8.2以上
// webpack4中除了正常安裝webpack之外,需要再單獨安一個webpack-cli
npm i webpack webpack-cli -D
複製程式碼

webpack是基於Node的

在專案下建立一個webpack.config.js(預設,可修改)檔案來配置webpack

module.exports = {
    entry: '',               // 入口檔案
    output: {},              // 出口檔案
    module: {},              // 處理對應模組
    plugins: [],             // 對應的外掛
    devServer: {},           // 開發伺服器配置
    mode: 'development'      // 模式配置
}
複製程式碼

以上就是webpack的正常配置模組 啟動devServer需要安裝一下webpack-dev-server

npm i webpack-dev-server -D
複製程式碼

image.png
接下來我們按照專案的結構,我們就從0開始去寫一下配置

// webpack.config.js

const path = require('path');

module.exports = {
    entry: './src/index.js',    // 入口檔案
    output: {
        filename: 'bundle.js',      // 打包後的檔名稱
        path: path.resolve('dist')  // 打包後的目錄,必須是絕對路徑
    }
}
複製程式碼

上面就可以說是實現了最簡單的webpack配置了,那接下來就打包一下看看

image.png

配置執行檔案

工作當中我們打包編譯的時候一般都執行npm run dev這樣的命令,既然是通過npm執行的命令,我們就應該找到package.json裡的執行指令碼去配置一下命令,這裡如下圖所示

image.png
npm run build就是我們打包後的檔案,這是生產環境下,上線需要的檔案

npm run dev是我們開發環境下打包的檔案,當然由於devServer幫我們把檔案放到記憶體中了,所以並不會輸出打包後的dist資料夾

配置Html模板

檔案都打包好了,但是我們在使用的時候不能在dist目錄下去建立一個html檔案,然後去引用打包後的js吧,這不合理,實際開發中也不會這樣 我們需要實現html打包功能,可以通過一個模板實現打包出引用好路徑的html來 這就需要用到一個常用的外掛了,< html-webpack-plugin >,用之前我們來安一下它

npm i html-webpack-plugin -D
複製程式碼
let path = require('path');
// 外掛都是一個類,所以我們命名的時候儘量用大寫開頭
let HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: './src/index.js',
    output: {
        // 新增hash可以防止檔案快取,每次都會生成4位的hash串
        filename: 'bundle.js',   
        path: path.resolve('dist')
    },
    plugins: [
        // 通過new一下這個類來使用外掛
        new HtmlWebpackPlugin({
            // 用哪個html作為模板
            // 在src目錄下建立一個index.html頁面當做模板來用
            template: './src/index.html',
            hash: true, // 會在打包好的bundle.js後面加上hash串
        })
    ]
}
複製程式碼

通過上面的配置後,我們再npm run build打包看一下現在是個什麼樣子了

image.png
多頁面開發,怎麼配置多頁面 如果開發的時候不只一個頁面,我們需要配置多頁面,那麼需要怎麼來搞呢?不用擔心,html-webpack-plugin外掛自有辦法,我們來觀望一下

let path = require('path');
let HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    // 多頁面開發,怎麼配置多頁面
    entry: {
        index: './src/index.js',
        login: './src/login.js'
    },
    // 出口檔案  
    output: {                       
        filename: '[name].js',
        path: path.resolve('dist')
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './src/index.html',   
            filename: 'index.html',
            chunks: ['index']   // 對應關係,index.js對應的是index.html
        }),
        new HtmlWebpackPlugin({
            template: './src/index2.html',
            filename: 'login.html',
            chunks: ['login']   // 對應關係,login.js對應的是login.html
        })
    ]
}
複製程式碼

image.png
上面基本介紹完了html和js的打包配置了,webpack對css的解析需要用到loader,所以我們先提前安裝好,待會好方便使用

引用CSS檔案

需要下載一些解析css樣式的loader

npm i style-loader css-loader -D
// 引入less檔案的話,也需要安裝對應的loader
npm i less less-loader -D
npm i node-sass sass-loader -D
複製程式碼

下面我們來看一下如何配置css檔案的解析

// index.js
import './css/style.css';   // 引入css
import './less/style.less'; // 引入less

console.log('這裡是打包檔案入口-index.js');

// webpack.config.js
module.exports = {
    entry: {
        index: './src/index.js'
    },
    output: {
        filename: 'bundle.js',
        path: path.resolve('dist')
    },
    module: {
        rules: [
            {
                test: /\.css$/,     // 解析css
                use: ['style-loader', 'css-loader'] // 從右向左解析
                /* 
                    也可以這樣寫,這種方式方便寫一些配置引數
                    use: [
                        {loader: 'style-loader'},
                        {loader: 'css-loader'}
                    ]
                */
            }
        ]
    }
}
複製程式碼
  • 此時打包後的css檔案是以行內樣式style的標籤寫進打包後的html頁面中,如果樣式很多的話,我們更希望直接用link的方式引入進去,這時候需要把css拆分出來
  • extract-text-webpack-plugin外掛它的功效就在於會將打包到js裡的css檔案進行一個拆分,單獨提取css

拆分CSS

// @next表示可以支援webpack4版本的外掛
npm i extract-text-webpack-plugin@next -D
複製程式碼
let path = require('path');
let HtmlWebpackPlugin = require('html-webpack-plugin');
// 拆分css樣式的外掛
let ExtractTextWebpackPlugin = require('extract-text-webpack-plugin');

module: {
        rules: [
            {
                test: /\.less$/,     // 解析less
                use: ExtractTextWebpackPlugin.extract({
                    // 將css用link的方式引入就不再需要style-loader了
                    fallback: "style-loader",
                    use: ['css-loader', 'less-loader'] // 從右向左解析
                })
            },
            {
                test: /\.scss$/,     // 解析scss
                use: ExtractTextWebpackPlugin.extract({
                    // 將css用link的方式引入就不再需要style-loader了
                    fallback: "style-loader",
                    use: ['css-loader', 'sass-loader'] // 從右向左解析
                })
            },
            {
                test: /\.css$/,     // 解析css
                use: ExtractTextWebpackPlugin.extract({
                    // 將css用link的方式引入就不再需要style-loader了
                    fallback: "style-loader",
                    use: ['css-loader']
                })
            }
        ]
    },
   plugins: [
        new HtmlWebpackPlugin({
            template: './src/index.html',
        }),
        // 拆分後會把css檔案放到dist目錄下的css/style.css
        new ExtractTextWebpackPlugin('css/style.css')  
    ]
複製程式碼

image.png
另一個外掛mini-css-extract-plugin也是可以辦到的,它可以說是為webpack4而生的, 在這裡就簡單的提一下

npm i mini-css-extract-plugin -D
複製程式碼
let MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [MiniCssExtractPlugin.loader, 'css-loader']
            }
        ]
    },
    plugins: [
        new MiniCssExtractPlugin({
            filename: 'css/a.css'   // 指定打包後的css
        })
    ]
}
複製程式碼

拆分成多個css

這裡要著重說一下上面兩個外掛的區別了,個人還是建議用extract-text-webpack-plugin的,畢竟從之前的版本承接下來的,雖然在安包的時候需要@next,但是還是值得信賴的

而且現在的extract-text-webpack-plugin也支援了拆分成多個css,而目前mini-css-extract-plugin還不支援此功能

// 正常寫入的less
let styleLess = new ExtractTextWebpackPlugin('css/style.css');
// reset
let resetCss = new ExtractTextWebpackPlugin('css/reset.css');

module.exports = {
    module: {
        rules: [
            {
                test: /\.css$/,
                use: resetCss.extract({
                    fallback: "style-loader",  
                    use: 'css-loader'
                })
            },
            {
                test: /\.less$/,
                use: styleLess.extract({
                    fallback: "style-loader",
                     use: ['css-loader', 'less-loader'] // 從右向左解析
                })
            }
        ]
    },
    plugins: [
        styleLess,
        resetCss
    ]
}
複製程式碼

通過這樣操作後可以打包成兩個不同的css檔案,如下圖

image.png

引用圖片

npm i file-loader url-loader -D
複製程式碼

如果是在css檔案裡引入的如背景圖之類的圖片,就需要指定一下相對路徑

module.exports = {
    module: {
        rules: [
            {
                test: /\.(jpe?g|png|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            limit: 8192,    // 小於8k的圖片自動轉成base64格式,並且不會存在實體圖片
                            outputPath: 'images/'   // 圖片打包後存放的目錄
                        }
                    }
                ]
            }
        ]
    }
}
複製程式碼

在css中指定了publicPath路徑這樣就可以根據相對路徑引用到圖片資源了,如下圖所示

image.png

頁面img引用圖片

頁面中經常會用到img標籤,img引用的圖片地址也需要一個loader來幫我們處理好

npm i html-withimg-loader -D
複製程式碼
  module.exports = {
    module: {
        rules: [
            {
                test: /\.(htm|html)$/,
                use: 'html-withimg-loader'
            }
        ]
    }
}
複製程式碼

這樣再打包後的html檔案下img就可以正常引用圖片路徑了

image.png

引用字型圖片和svg圖片

字型圖示和svg圖片都可以通過file-loader來解析

module.exports = {
    module: {
        rules: [
            {
                test: /\.(eot|ttf|woff|svg)$/,
                use: 'file-loader'
            }
        ]
    }
}
複製程式碼

這樣即使樣式中引入了這類格式的圖示或者圖片都沒有問題了,img如果也引用svg格式的話,配合上面寫好的html-withimg-loader就都沒有問題了

新增CSS3字首

通過postcss中的autoprefixer可以實現將CSS3中的一些需要相容寫法的屬性新增響應的字首,這樣省去我們不少的時間

由於也是一個loader載入器,我們也需要先安裝一下

npm i postcss-loader autoprefixer -D
複製程式碼

安裝後,我們還需要像webpack一樣寫一個config的配置檔案,在專案根目錄下建立一個postcss.config.js檔案,配置如下:

module.exports = {
    plugins: [
        require('autoprefixer')({
            "browsers": [
                "defaults",
                "not ie < 11",
                "last 2 versions",
                "> 1%",
                "iOS 7",
                "last 3 iOS versions"
            ]
        })
    ]
};
複製程式碼

然後在webpack裡配置postcss-loader

module.exports = {
    module: {
        rules: [
            {
                test: /\.less$/,     // 解析less
                use: ExtractTextWebpackPlugin.extract({
                    // 將css用link的方式引入就不再需要style-loader了
                    fallback: "style-loader",
                    use: ['css-loader', 'postcss-loader', 'less-loader'] // 從右向左解析
                })
            },
            {
                test: /\.scss$/,     // 解析scss
                use: ExtractTextWebpackPlugin.extract({
                    // 將css用link的方式引入就不再需要style-loader了
                    fallback: "style-loader",
                    use: ['css-loader', 'postcss-loader', 'sass-loader'] // 從右向左解析
                })
            },
            {
                test: /\.css$/,     // 解析css
                use: ExtractTextWebpackPlugin.extract({
                    // 將css用link的方式引入就不再需要style-loader了
                    fallback: "style-loader",
                    use: ['css-loader', 'postcss-loader']
                })
            },
        ]
    }
}
複製程式碼

轉義ES6和react

在實際開發中,我們在大量的使用著ES6及之後的api去寫程式碼,這樣會提高我們寫程式碼的速度,不過由於低版本瀏覽器的存在,不得不需要轉換成相容的程式碼,於是就有了常用的Babel了

Babel會將ES6的程式碼轉成ES5的程式碼

npm i babel-core babel-loader babel-preset-env babel-preset-stage-3  babel-preset-react babel-polyfill babel-plugin-import babel-loader babel-register -D
babel-preset-stage-3  使用這個外掛來編譯為了後面使用...state擴充套件運算子可以使用
複製程式碼

當把這些都安好後,我們就開始配置,由於要相容的程式碼不僅僅包含ES6還有之後的版本和那些僅僅是草案的內容,所以我們可以通過一個.babelrc檔案來配置一下,對這些版本的支援

// .babelrc
{
    "presets": [
        [
            "env",
            {
                "loose": true,  
                "modules": false 
            }
        ],
        "es2015",
        "react",
      "babel-preset-stage-3"
    ]
}
複製程式碼

我們再在webpack裡配置一下babel-loader既可以做到程式碼轉成ES5了

module.exports = {
    module: {
        rules: [
            {
                test:/\.js$/,
                use: 'babel-loader',
                include: /src/,          // 只轉化src目錄下的js
                exclude: /node_modules/  // 排除掉node_modules,優化打包速度
            }
        ]
    }
}
複製程式碼

加入 babel-plugin-transform-runtime 和 babel-polyfill

1.先來說說babel-plugin-transform-runtime

在轉換 ES2015 語法為 ECMAScript 5 的語法時,babel 會需要一些輔助函式,例如 _extend。babel 預設會將這些輔助函式內聯到每一個 js 檔案裡,這樣檔案多的時候,專案就會很大。

所以 babel 提供了 transform-runtime 來將這些輔助函式“搬”到一個單獨的模組 babel-runtime 中,這樣做能減小專案檔案的大小。

npm install --save-dev babel-plugin-transform-runtime
複製程式碼

修改.babelrc配置檔案,增加配置 .babelrc

"plugins": [
       "transform-runtime"
     ]
複製程式碼

2.再來看babel-polyfill 為什麼要整合babel-polyfill?

Babel預設只轉換新的JavaScript句法(syntax),而不轉換新的API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全域性物件,以及一些定義在全域性物件上的方法(比如Object.assign)都不會轉碼。 舉例來說,ES6在Array物件上新增了Array.from方法。Babel就不會轉碼這個方法。如果想讓這個方法執行,必須使用babel-polyfill,為當前環境提供一個墊片。

npm install --save-dev babel-polyfill
複製程式碼
//  修改入口檔案index.js
import 'babel-polyfill';
複製程式碼

呀呀呀,在我們每次npm run build的時候都會在dist目錄下建立很多打好的包,如果積累過多可能也會混亂

所以應該在每次打包之前將dist目錄下的檔案都清空,然後再把打好包的檔案放進去,主人們,接下來我們使用clean-webpack-plugin這個外掛吧

npm i clean-webpack-plugin -D
複製程式碼
let CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {
    plugins: [
        // 打包前先清空
        new CleanWebpackPlugin('dist')  
    ]
}
複製程式碼

resolve解析

 resolve: {
        // 別名
        alias: {
          pages:path.join(__dirname,'src/pages'),
          component:path.join(__dirname,'src/component'),
          actions:path.join(__dirname,'src/redux/actions'),
          reducers:path.join(__dirname,'src/redux/reducers'),
        },
        // 省略字尾
        extensions: ['.js', '.jsx', '.json', '.css', '.scss', '.less']
    }
複製程式碼

提取公共程式碼

在webpack4之前,提取公共程式碼都是通過一個叫CommonsChunkPlugin的外掛來辦到的。到了4以後,內建了一個一模一樣的功能 optimization

下面我們就來看看如何提取公共程式碼

optimization: {
        splitChunks: {
            cacheGroups: {
                vendor: {   // 抽離第三方外掛
                    test: /node_modules/,   // 指定是node_modules下的第三方包
                    chunks: 'initial',
                    name: 'vendor',  // 打包後的檔名,任意命名    
                    // 設定優先順序,防止和自定義的公共程式碼提取時被覆蓋,不進行打包
                    priority: 10
                },
                utils: {
                    // 抽離自己寫的公共程式碼,utils裡面是一個公共類庫
                    chunks: 'initial',
                    name: 'utils',  //  任意命名
                    minSize: 0    // 只要超出0位元組就生成一個新包
                }
            }
        }
    },
還要在plugins裡面引入需要單獨打包出來的chunk
      new HtmlWebpackPlugin({
            template: './src/index.html',
            chunks: ['vendor', 'index', 'utils']  //  引入需要的chunk   
        }),
複製程式碼

image.png

webpack-dev-server

簡單來說,webpack-dev-server就是一個小型的靜態檔案伺服器。使用它,可以為webpack打包生成的資原始檔提供Web服務。

npm install webpack-dev-server --save-dev
npm install webpack-dev-server -g
複製程式碼
 devServer: {
        port: 3000,             // 埠
        open: true,             // 自動開啟瀏覽器
        hot: true,               // 開啟熱更新
        overlay: true, // 瀏覽器頁面上顯示錯誤
        historyApiFallback: true
    },
複製程式碼

devtool優化

現在我們發現一個問題,程式碼哪裡寫錯了,瀏覽器報錯只報在build.js第幾行。這讓我們排查錯誤無從下手,傳送門。 在開發環境下配置

devtool: 'inline-source-map'
複製程式碼

同時,我們在srouce裡面能看到我們寫的程式碼,也能打斷點除錯程式碼

熱更新和自動重新整理的區別

在配置devServer的時候,如果hot為true,就代表開啟了熱更新,但是這並沒有那麼簡單,因為熱更新還需要配置一個webpack自帶的外掛並且還要在主要js檔案裡檢查是否有module.hot

// webpack.config.js
let webpack = require('webpack');
module.exports = {
    plugins: [
        // 熱更新,熱更新不是重新整理
        new webpack.HotModuleReplacementPlugin()
    ],
    devServer: {
        hot: true,  //  加上這一行
    }
}

//  在入口檔案index.js
// 還需要在主要的js檔案裡寫入下面這段程式碼
if (module.hot) {
  // 實現熱更新
  module.hot.accept();
}
複製程式碼

熱更新的好處在開發vue或者react的時候,其中某個元件修改的時候就會針對這個元件進行更新,超級好用,提升開發效率

整合react-router

`npm install --save react-router-dom`
複製程式碼

新建router資料夾和元件

`cd src`
`mkdir router && touch router/router.js`
複製程式碼

按照react-router文件編輯一個最基本的router.js。包含兩個頁面homepage1src/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 = () => (
    <Router>
        <div>
            <ul>
                <li><Link to="/">首頁</Link></li>
                <li><Link to="/page1">Page1</Link></li>
            </ul>
            <Switch>
                <Route exact path="/" component={Home}/>
                <Route path="/page1" component={Page1}/>
            </Switch>
        </div>
    </Router>
);
export default getRouter;
複製程式碼

新建頁面資料夾

cd src
mkdir pages
複製程式碼

新建兩個頁面 Home,Page1

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 (
            <div>
                this is home ~hi xht
            </div>
        )
    }
}
複製程式碼

Page1.js

import React, {Component} from 'react';

export default class Page1 extends Component {
    render() {
        return (
            <div>
                this is Page1~hi xht
            </div>
        )
    }
}
複製程式碼

現在路由和頁面建好了,我們在入口檔案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('root'));
複製程式碼

現在執行打包命令npm run dev。開啟index.html檢視效果啦!

整合react-redux

接下來,我們就要就要就要整合redux了。 要對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/couter.js
touch reducers/couter.js
複製程式碼

先來寫action建立函式。通過action建立函式,可以建立action src/redux/actions/counter.js

/*action*/

export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";
export const RESET = "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/couter.js

import { INCREMENT, DECREMENT, RESET } from '../actions/couters';

/*
* 初始化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/couter';

export default function combineReducers(state = {}, action) {
    return {
        counter: counter(state.counter, action)
    }
}
複製程式碼

reducer就是純函式,接收state 和 action,然後返回一個新的 state。 看上面的程式碼,無論是combineReducers函式也好,還是reducer函式也好,都是接收state和action, 返回更新後的state。區別就是combineReducers函式是處理整棵樹,reducer函式是處理樹的某一點。 接下來,我們要建立一個store。

前面我們可以使用 action 來描述“發生了什麼”,使用action建立函式來返回action。

還可以使用 reducers 來根據 action 更新 state 。

那我們如何提交action?提交的時候,怎麼才能觸發reducers呢?

store 就是把它們聯絡到一起的物件。store 有以下職責:

維持應用的 state;

  • 提供 getState() 方法獲取 state;
  • 提供 dispatch(action) 觸發reducers方法更新 state;
  • 通過subscribe(listener) 註冊監聽器;
  • 通過 subscribe(listener) 返回的函式登出監聽器。

src/redux/store.js

import {createStore} from 'redux';
import combineReducers from './reducers.js';
let store = createStore(combineReducers);
export default store;
複製程式碼

寫一個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 (
            <div>
                <div>當前計數為(顯示redux計數)</div>
                <button onClick={() => {
                    console.log('呼叫自增函式');
                }}>自增
                </button>
                <button onClick={() => {
                    console.log('呼叫自減函式');
                }}>自減
                </button>
                <button onClick={() => {
                    console.log('呼叫重置函式');
                }}>重置
                </button>
            </div>
        )
    }
}
複製程式碼

修改路由,增加Counter src/router/router.js

import React from 'react';
import { HashRouter 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 = () => (
  <Router>
    <div>
      <ul>
        <li>
          <Link to="/">首頁</Link>
        </li>
        <li>
          <Link to="/page1">Page1</Link>
        </li>
        <li>
          <Link to="/couter">Counter</Link>
        </li>
      </ul>
      <Switch>
        <Route exact path="/" component={Home} />
        <Route path="/page1" component={Page1} />
        <Route path="/couter" component={Counter} />
      </Switch>
    </div>
  </Router>
);

export default getRouter;
複製程式碼

npm run dev 看看效果。 下一步,我們讓Counter元件和react-redux聯合起來。使Counter能獲得到Redux的state,並且能發射action。 先來安裝react-redux

npm install --save react-redux
複製程式碼

src/pages/Counter/Counter.js

import React, { Component } from 'react';
import { increment, decrement, reset } from 'actions/couters';

import { connect } from 'react-redux';

class Counter extends Component {
  render() {
    const {
      counter: { count },
      increment,
      decrement,
      reset,
    } = this.props;
    return (
      <div>
        <div>
          當前計數為:
          {count}
        </div>
        <button onClick={() => increment()}>自增</button>
        <button onClick={() => decrement()}>自減</button>
        <button onClick={() => reset()}>重置</button>
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  return {
    counter: state.couter,
  };
};

const mapDispatchToProps = (dispatch) => {
  return {
    increment: () => {
      dispatch(increment());
    },
    decrement: () => {
      dispatch(decrement());
    },
    reset: () => {
      dispatch(reset());
    },
  };
};

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Counter);
複製程式碼

下面我們要傳入store

所有容器元件都可以訪問 Redux store,所以可以手動監聽它。一種方式是把它以 props 的形式傳入到所有容器元件中。但這太麻煩了,因為必須要用 store 把展示元件包裹一層,僅僅是因為恰好在元件樹中渲染了一個容器元件。

建議的方式是使用指定的 React Redux 元件 來 讓所有容器元件都可以訪問 store,而不必顯示地傳遞它。只需要在渲染根元件時使用即可。

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './css/index';
import { Provider } from 'react-redux';
import getRouter from './router/router';
import store from './redux/store';

const router = getRouter();

/* 初始化 */
renderWithHotReload(router);

function renderWithHotReload(RootElement) {
  ReactDOM.render(
    <Provider store={store}>{RootElement}</Provider>,
    document.getElementById('root')
  );
}

// 還需要在主要的js檔案裡寫入下面這段程式碼
if (module.hot) {
  // 實現熱更新
  module.hot.accept();
}
複製程式碼

我們在說清楚一下 1.Provider元件是讓所有的元件可以訪問到store。不用手動去傳。也不用手動去監聽。 2.connect函式作用是從 Redux state 樹中讀取部分資料,並通過 props 來把這些資料提供給要渲染的元件。也傳遞dispatch(action)函式到props。

引入非同步action,整合redux-thunk

下面,我們以向後臺請求使用者基本資訊為例。 1.我們先建立一個user.json,等會請求用,相當於後臺的API介面。

在根目錄
mkdir api
cd api
touch user.json
複製程式碼

user.json

{
  "name": "xiehaitao",
  "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 = "GET_USER_INFO_REQUEST";
export const GET_USER_INFO_SUCCESS = "GET_USER_INFO_SUCCESS";
export const GET_USER_INFO_FAIL = "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建立函式。 3.建立reducer

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;
  }
}
複製程式碼

組合reducer src/redux/reducers.js

import counter from 'reducers/counter';
import userInfo from 'reducers/userInfo';

export default function combineReducers(state = {}, action) {
    return {
        couter: couter(state.couter, action),
        userInfo: userInfo(state.userInfo, action)
    }
}
複製程式碼

4.現在有了action,有了reducer,我們就需要呼叫把action裡面的三個action函式和網路請求結合起來。 src/redux/actions/userInfo.js增加

export function getUserInfo() {
  return function (dispatch) {
    dispatch(getUserInfoRequest());

    return fetch('/api/user.json')
      .then((response) => {
        return response.json();
      })
      .then((json) => {
        dispatch(getUserInfoSuccess(json));
      })
      .catch(() => {
        dispatch(getUserInfoFail());
      });
  };
}
複製程式碼

我們這裡發現,別的action建立函式都是返回action物件: 但是我們現在的這個action建立函式 getUserInfo則是返回函式了。 為了讓action建立函式除了返回action物件外,還可以返回函式,我們需要引用redux-thunk。

npm install --save redux-thunk
複製程式碼

簡單的說,中介軟體就是action在到達reducer,先經過中介軟體處理。我們之前知道reducer能處理的action只有這樣的{type:xxx},所以我們使用中介軟體來處理 函式形式的action,把他們轉為標準的action給reducer。這是redux-thunk的作用。 使用redux-thunk中介軟體 src/redux/store.js

import { createStore, applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
import combineReducers from './reducers';

const store = createStore(combineReducers, applyMiddleware(thunkMiddleware));

export default store;

複製程式碼

到這裡,redux-thunk已經配置完成了,寫一個元件來驗證一下

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: { userInfo, isLoading, errorMsg },
      getUserInfo,
    } = this.props;
    return (
      <div>
        {isLoading
          ? '請求資訊中......'
          : errorMsg || (
          <div>
            <p>使用者資訊:</p>
            <p>
                  使用者名稱:
              {userInfo.name}
            </p>
            <p>
                  介紹:
              {userInfo.intro}
            </p>
          </div>
          )}
        <button onClick={() => getUserInfo()}>請求使用者資訊</button>
      </div>
    );
  }
}

export default connect(
  state => ({ userInfo: state.userInfo }),
  { getUserInfo }
)(UserInfo);

複製程式碼

增加路由 src/router/router.js

import React from 'react';
import { HashRouter 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 = () => (
  <Router>
    <div>
      <ul>
        <li>
          <Link to="/">首頁</Link>
        </li>
        <li>
          <Link to="/page1">Page1</Link>
        </li>
        <li>
          <Link to="/couter">Counter</Link>
        </li>
        <li>
          <Link to="/userinfo">UserInfo</Link>
        </li>
      </ul>
      <Switch>
        <Route exact path="/" component={Home} />
        <Route path="/page1" component={Page1} />
        <Route path="/couter" component={Counter} />
        <Route path="/userinfo" component={UserInfo} />
      </Switch>
    </div>
  </Router>
);

export default getRouter;

複製程式碼

image.png

combinReducers優化

redux提供了一個combineReducers函式來合併reducer,不用自己合併 src/redux/reducers.js

import { combineReducers } from 'redux';
import userInfo from 'reducers/userInfo';
import couter from './reducers/couters';

export default combineReducers({
  couter,
  userInfo,
});

複製程式碼

指定環境

在webpack4之前都是需要建三個檔案來需要環境的,webpack.base.js,webpack.dev.js,webpack.prod.js,現在在webpack4之後就不需要了,因為用--mode 就可以區分環境了 先安裝

npm install -D yargs-parser
複製程式碼
這個包可以拿到--mode 裡面的引數,這樣子就可以區別是本地環境還是線上環境了
    "dev": "cross-env webpack-dev-server --mode development",
    "build": "npm run lint && cross-env npm run clean && webpack --mode production",
複製程式碼
//  webpack.config.js
const argv = require('yargs-parser')(process.argv.slice(2));
const pro = argv.mode == 'production' ? true : false;  //  區別是生產環境和開發環境

let plu = [];
if (pro) {
    //  線上環境
    plu.push(
        new HtmlWebpackPlugin({
            template: './src/index.html',
            hash: true, // 會在打包好的bundle.js後面加上hash串
            chunks: ['vendor', 'index', 'utils']  //  引入需要的chunk
        }),
        // 拆分後會把css檔案放到dist目錄下的css/style.css
        new ExtractTextWebpackPlugin('css/style.[chunkhash].css'),
        new ExtractTextWebpackPlugin('css/reset.[chunkhash].css'),
        new CleanWebpackPlugin('dist'),
    )
} else {
    //  開發環境
    plu.push(
        new HtmlWebpackPlugin({
            template: './src/index.html',
            chunks: ['vendor', 'index', 'utils']  //  引入需要的chunk
        }),
        // 拆分後會把css檔案放到dist目錄下的css/style.css
        new ExtractTextWebpackPlugin('css/style.css'),
        new ExtractTextWebpackPlugin('css/reset.css'),
        new webpack.HotModuleReplacementPlugin(),  // 熱更新,熱更新不是重新整理
    )
}

devtool: pro ? '' : 'inline-source-map'  //  只有本地開發才需要除錯

複製程式碼

整合eslint

eslint目標是以可擴充套件,每條規則獨立,不內建編碼風格為理念的lint工具,使用者可以定製自己的規則做成公共包

eslint主要有以下特點:

1)預設規則包含所有的jslint,jshint中存在的規則易遷移

2)規則可配置性高,可設定警告,錯誤兩個error等級,也可以直接禁用

3)包含程式碼風格檢測的規則

4)支援外掛擴充套件,自定義規則

針對react開發者,eslint已經可以很好的支援jsx語法了。 先安裝外掛

npm install -D eslint eslint-config-airbnb eslint-loader  eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react
複製程式碼

配置.eslintrc檔案

// 直接繼承airbnb的配置規則,同時也可以寫入自己的特定規則,後面的內容會覆蓋預設的規則,
//  下面是我比較習慣的lint規則
{
  "extends": ["airbnb"],
  "env": {
    "browser": true,
    "node": true,
    "es6": true,
    "mocha": true,
    "jest": true,
    "jasmine": true
  },
  "rules": {
    "no-plusplus": [0],
    "eqeqeq": [0],
    "no-empty": [0],
    "no-param-reassign": [0],
    "generator-star-spacing": [0],
    "consistent-return": [0],
    "no-shadow": [0],
    "react/forbid-prop-types": [0],
    "react/jsx-filename-extension": [
      1,
      {
        "extensions": [".js"]
      }
    ],
    "react/button-has-type": [
      "<enabled>",
      {
        "button": false,
        "submit": false,
        "reset": false
      }
    ],
    "global-require": [1],
    "import/prefer-default-export": [0],
    "react/jsx-boolean-value": [0],
    "react/jsx-no-bind": [0],
    "react/prop-types": [0],
    "react/prefer-stateless-function": [0],
    "react/jsx-one-expression-per-line": [0],
    "react/jsx-wrap-multilines": [
      "error",
      {
        "no-empty": [0],
        "no-param-reassign": [0],
        "declaration": "parens-new-line",
        "assignment": "parens-new-line",
        "return": "parens-new-line",
        "arrow": "parens-new-line",
        "condition": "parens-new-line",
        "logical": "parens-new-line",
        "prop": "ignore"
      }
    ],
    "no-else-return": [0],
    "no-restricted-syntax": [0],
    "import/no-extraneous-dependencies": [0],
    "no-use-before-define": [0],
    "jsx-a11y/no-static-element-interactions": [0],
    "jsx-a11y/no-noninteractive-element-interactions": [0],
    "jsx-a11y/click-events-have-key-events": [0],
    "jsx-a11y/anchor-is-valid": [0],
    "no-nested-ternary": [0],
    "arrow-body-style": [0],
    "import/extensions": [0],
    "no-bitwise": [0],
    "no-cond-assign": [0],
    "import/no-unresolved": [0],
    "comma-dangle": [
      "error",
      {
        "arrays": "always-multiline",
        "objects": "always-multiline",
        "imports": "always-multiline",
        "exports": "always-multiline",
        "functions": "ignore"
      }
    ],
    "object-curly-newline": [0],
    "function-paren-newline": [0],
    "no-restricted-globals": [0],
    "require-yield": [1]
  },
  "globals": {
    "document": true,
    "localStorage": true,
    "window": true
  }
}

複製程式碼

除此之外還要在webpack.config.js檔案增加module的loader

module: {
        rules: [
            {
                enforce: "pre",  //  代表在解析loader之前就先解析eslint-loader
                test: /\.js$/,
                exclude: /node_modules/,
                include:/src/,
                loader: "eslint-loader",
              },
          ]
}
複製程式碼

pagekage.json檔案裡面script增加

    "lint": "npm run format && npm run fix &&  eslint --ext .js src",  //  檢測你寫的程式碼是否符合eslint的規則
    "fix": "npm run format && eslint --fix --ext .js src",  //  npm run fix 這個是可以修復你沒有按照lint規則的寫法
複製程式碼

自動格式化以及提交程式碼時的優化配置

###第一步 格式化所有程式碼 prettier

npm install -D prettier
複製程式碼

在package.json的script裡面新增如下配置

{
  "scripts": {
    "format": "prettier --single-quote --trailing-comma es5 --write \"src/**/*.js\""
  }
}
複製程式碼

你可以通過 npm run format試一下是否可以自動格式化你的程式碼 第二步 配置Eslint 上面我們已經配置好eslint了在package.json的scripts裡新增如下

"fix": "npm run format && eslint --fix --ext .js src",
複製程式碼

第三步 新增Git鉤子(Pre-commit Hook)

Git 鉤子(hooks)是在Git 倉庫中特定事件(certain points)觸發後被呼叫的指令碼。 詳情可瀏覽 git-scm.com/book/zh/v2/… 每次提交程式碼,執行 git commit之後進行自動格式化,免去每次人為手動格式化,使遠端倉庫程式碼保持風格統一。

npm install -D lint-staged husky 
複製程式碼

在package.json的scripts裡新增如下

   "precommit": "npm run lint",
複製程式碼

現在讓我們來看看在package.json的scripts的所有配置吧

  "scripts": {
    "dev": "cross-env webpack-dev-server --mode development",
    "build": "npm run lint && cross-env npm run clean && webpack --mode production",
    "precommit": "npm run lint",
    "clean": "cross-env rm -rf dist && mkdir dist",
    "test": "mocha --compilers js:babel-register --require ./test/test_helper.js --recursive",
    "test:watch": "npm run test --watch",
    "lint": "npm run format && npm run fix &&  eslint --ext .js src",
    "fix": "npm run format && eslint --fix --ext .js src",
    "format": "prettier --single-quote --trailing-comma es5 --write \"src/**/*.js\""
  },
複製程式碼

好了,接下來還會繼續維護這個腳手架,把react的SSR服務端渲染的腳手架也搭建起來

相關文章