如何搭建一個REACT全家桶框架

涔芒果?發表於2019-04-02

前端技術發展太快,有些庫的版本一直在升級,現在網上的搭建教程不是不全面,就是版本太低,本人綜合一些教程和自己的理解,整理了一下,方便大家快速入手react框架。本教程針對剛入門和技術棧轉型人員。注:(本教程寫於2019-3-29,請注意版本)!!!

大家閱讀的時候,如發現問題,可提出,我會及時更新(本人較懶,有些命令沒有打出來,請仔細閱讀,避免遺漏!!!)

前言

本人也是半路加入react大軍的一員,由於半路加入,對整體框架了解較少,使用現成DVA框架,始終是一知半解。平常遇到問題,總是需要找資料去解決,也有問題最後難以解決,為了方便自己理解整個react相關的技術,也避免後來像我一樣的人繼續踩坑,可以根據這個教程有個比較全面的瞭解。(高手勿拍)!!!

專案簡介

1.技術棧目前是最新的

  • node 8.11.1
  • react 16.8.6
  • react-router-dom 5.0.0
  • redux 4.0.1
  • webpack 4.28.2

2.包管理工具

常用的有npm yarn等,本人這裡使用yarn,使用npm的小夥伴注意下命令區別

直接開始

初始化專案

  1. 先建立一個目錄並進入
mkdir react-cli && cd react-cli
複製程式碼
  1. 初始化專案,填寫專案資訊(可一路回車)
npm init
複製程式碼

安裝webpack

yarn global add webpack -D 
yarn global add webpack-cli -D 
複製程式碼
  • yarn使用add新增包,-D等於--save-dev -S等於--save
  • -D和-S兩者區別:-D是你開發時候依賴的東西,--S 是你釋出之後還依賴的東西
  • -g是全域性安裝,方便我們後面使用webpack命令(全域性安裝後依然不能使用的小夥伴,檢查下自己的環境變數PATH)

安裝好後新建build目錄放一個webpack基礎的開發配置webpack.dev.config.js

mkdir build && cd build && echo. > webpack.dev.config.js
複製程式碼

配置內容很簡單,配置入口和輸出

const path = require('path');

module.exports = {
 
    /*入口*/
    entry: path.join(__dirname, '../src/index.js'),
    
    /*輸出到dist目錄,輸出檔名字為bundle.js*/
    output: {
        path: path.join(__dirname, '../dist'),
        filename: 'bundle.js'
    }
};
複製程式碼

然後根據我們配置的入口檔案的地址,建立../src/index.js檔案(請注意src目錄和build目錄同級)

mkdir src && cd src && echo. > index.js
複製程式碼

然後寫入一行內容

document.getElementById('app').innerHTML = "Hello React";
複製程式碼

現在在根目錄下執行webpack打包命令

webpack --config ./build/webpack.dev.config.js
複製程式碼

我們可以看到生成了dist目錄和bundle.js。(消除警告看後面mode配置) 接下來我們在dist目錄下新建一個index.html來引用這個打包好的檔案

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src="./bundle.js" charset="utf-8"></script>
</body>
</html>
複製程式碼

然後雙擊開啟index.html,我們就看到瀏覽器輸出

Hello React
複製程式碼

這樣我們一個基本的打包功能就做好了!!!

mode

剛才打包成功但是帶有一個警告,意思是webpack4需要我們指定mode的型別來區分開發環境和生產環境,他會幫我們自動執行相應的功能,mode可以寫到啟動命令裡--mode=production or development,也可以寫到配置檔案裡,這裡我們將 webpack.dev.config.js裡面新增mode屬性。

/*入口*/
    entry: path.join(__dirname, '../src/index.js'),
    mode:'development',
複製程式碼

在執行打包命令,警告就消失了。

babel

Babel 把用最新標準編寫的 JavaScript 程式碼向下編譯成可以在今天隨處可用的版本。 
這一過程叫做“原始碼到原始碼”編譯, 也被稱為轉換編譯。(本教程使用的babel版本是7,請注意包名和配置與6的不同)
複製程式碼
  • @babel/core 呼叫Babel的API進行轉碼
  • @babel/preset-env 用於解析 ES6
  • @babel/preset-react 用於解析 JSX
  • babel-loader 載入器
yarn add @babel/core @babel/preset-env @babel/preset-react babel-loader -D
複製程式碼

然後在根目錄下新建一個babel配置檔案

babel.config.js

const babelConfig = {
   presets: ["@babel/preset-react", "@babel/preset-env"],
    plugins: []
}

module.exports = babelConfig;
複製程式碼

修改webpack.dev.config.js,增加babel-loader!

/*src目錄下面的以.js結尾的檔案,要使用babel解析*/
/*cacheDirectory是用來快取編譯結果,下次編譯加速*/
module: {
    rules: [{
        test: /\.js$/,
        use: ['babel-loader?cacheDirectory=true'],
        include: path.join(__dirname, '../src')
    }]
}
複製程式碼

現在我們簡單測試下,是否能正確轉義ES6~

修改 src/index.js

 /*使用es6的箭頭函式*/
    var func = str => {
        document.getElementById('app').innerHTML = str;
    };
    func('我現在在使用Babel!');
複製程式碼

再執行打包命令

webpack --config ./build/webpack.dev.config.js
複製程式碼

現在重新整理dist下面的index.html就會看到瀏覽器輸出

我現在在使用Babel!
複製程式碼

有興趣的可以開啟打包好的bundle.js,最下面會發現ES6箭頭函式被轉換為普通的function函式

react

接下來是我們的重點內容,接入react

yarn add react react-dom -S
複製程式碼

注:這裡使用 -S 來保證生產環境的依賴

修改 src/index.js使用react

import React from 'react';
import ReactDom from 'react-dom';

ReactDom.render(
    <div>Hello React!</div>, document.getElementById('app'));
複製程式碼

執行打包命令

webpack --config ./build/webpack.dev.config.js
複製程式碼

重新整理index.html 看效果。

接下來我們使用react的元件化思想做一下封裝,src下新建components目錄,然後新建一個Hello目錄,裡面建立一個index.js,寫入:

import React, { PureComponent } from 'react';

export default class Hello extends PureComponent  {
    render() {
        return (
            <div>
                Hello,元件化-React!
            </div>
        )
    }
}
複製程式碼

然後讓我們修改src/index.js,引用Hello元件!

import React from 'react';
import ReactDom from 'react-dom';
import Hello from './components/Hello';

ReactDom.render(
    <Hello/>, document.getElementById('app'));
複製程式碼

注:import 模組化匯入會預設選擇目錄下的index檔案,所以直接寫成'./components/Hello'

在根目錄執行打包命令

webpack --config ./build/webpack.dev.config.js
複製程式碼

開啟index.html看效果咯~

命令優化

每次打包都輸入很長的打包命令,很麻煩,我們對此優化一下。

修改package.json裡面的script物件,增加build屬性,寫入我們的打包命令。

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --config ./build/webpack.dev.config.js"
  },
複製程式碼

現在我們打包只需要執行npm run build就可以啦!(除了start是內建命令,其他新增的命令都需要用run去執行)

react-router

現在我們接入react的路由react-router

yarn add react-router-dom -S
複製程式碼

接下來為了使用路由,我們建兩個頁面來做路由切換的內容。首先在src下新建一個pages目錄,然後pages目錄下分別建立home和page目錄,裡面分別建立一個index.js。

src/pages/home/index.js
複製程式碼
import React, {PureComponent} from 'react';

export default class Home extends PureComponent {
    render() {
        return (
            <div>
                this is home~
            </div>
        )
    }
}
複製程式碼
src/pages/page/index.js
複製程式碼
import React, {PureComponent} from 'react';

export default class Page extends PureComponent {
    render() {
        return (
            <div>
                this is Page~
            </div>
        )
    }
}
複製程式碼

兩個頁面就寫好了,然後建立我們的選單導航元件

components/Nav/index.js
複製程式碼
import React from 'react';
import { Link } from 'react-router-dom';

export default () => {
    return (
        <div>
            <ul>
                <li><Link to="/">首頁</Link></li>
                <li><Link to="/page">Page</Link></li>
            </ul>
        </div>
    )
}
複製程式碼

注:使用Link元件改變當前路由

然後我們在src下面新建router.js,寫入我們的路由,並把它們跟頁面關聯起來

import React from 'react';

import { Route, Switch } from 'react-router-dom';

// 引入頁面
import Home from './pages/home';
import Page from './pages/page';

// 路由
const getRouter = () => (
    <Switch>
        <Route exact path="/" component={Home}/>
        <Route path="/page" component={Page}/>
    </Switch>
);

export default getRouter;
複製程式碼

頁面和選單和路由都寫好了,我們把它們關聯起來。在src/index.js中

import React from 'react';
import ReactDom from 'react-dom';
import {BrowserRouter as Router} from 'react-router-dom';
import Nav from './components/Nav';
import getRouter from './router';

ReactDom.render(
    <Router>
        <Nav/>
        {getRouter()}
    </Router>,
    document.getElementById('app')
)

複製程式碼

現在執行npm run build打包後就可以看到內容了,但是點選選單並沒有反應,這是正常的。因為我們目前使用的依然是本地磁碟路徑,並不是ip+埠的形式,接下來我們引入webpack-dev-server來啟動一個簡單的伺服器。

yarn global add webpack-dev-server -D
複製程式碼

修改webpack.dev.config.js,增加webpack-dev-server的配置。

// webpack-dev-server
devServer: {
    contentBase: path.join(__dirname, '../dist'), 
    compress: true,  // gzip壓縮
    host: '0.0.0.0', // 允許ip訪問
    hot:true, // 熱更新
    historyApiFallback:true, // 解決啟動後重新整理404
    port: 8000 // 埠
},
複製程式碼

注:contentBase一般不配,主要是允許訪問指定目錄下面的檔案,這裡使用到了dist下面的index.html

然後在package.json裡新建啟動命令

"start": "webpack-dev-server --config ./build/webpack.dev.config.js",
複製程式碼

執行npm start命令後開啟 http://localhost:8000 即可看到內容,並可以切換路由了!

proxy代理

devServer下有個proxy屬性可以設定我們的代理

 devServer: {
       ...
        proxy: { // 配置服務代理
            '/api': {
                 target: 'http://localhost:8000',
                 pathRewrite: {'^/api' : ''},  //可轉換
                 changeOrigin:true
            }
        },
        port: 8000 // 埠
    },
複製程式碼

在 localhost:8000 上有後端服務的話,你可以這樣啟用代理。請求到 /api/users 現在會被代理到請求 http://localhost:8000/users。(注意這裡的第二個屬性,它將'/api'替換成了'')。changeOrigin: true可以幫我們解決跨域的問題。

devtool優化

當啟動報錯或者像打斷點的時候,會發現打包後的程式碼無從下手。我們在webpack裡面新增

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

然後就可以在srouce裡面能看到我們寫的程式碼,也能打斷點除錯哦~

檔案路徑優化

正常我們引用元件或者頁面的時候,一般都是已../的形式去使用。若是檔案層級過深,會導致../../../的情況,不好維護和讀懂,為此webpack提供了alias 別名配置。

看這裡:切記名稱不可宣告成你引入的其他包名。別名的會覆蓋你的包名,導致你無法引用其他包。栗子:redux、react等

首先在webpack.dev.config.js裡面加入

resolve: {
    alias: {
        pages: path.join(__dirname, '../src/pages'),
        components: path.join(__dirname, '../src/components'),
        router: path.join(__dirname, '../src/router')
    }
}
複製程式碼

然後我們的router.js裡面引入元件就可以改為

// 引入頁面
import Home from './pages/home';
import Page from './pages/page';

// 引入頁面
import Home from 'pages/home';
import Page from 'pages/page';
複製程式碼

此功能層級越複雜越好用。

redux

接下來我們要整合redux,我們先不講理論,直接用redux做一個最常見的例子,計數器。首先我們在src下建立一個redux目錄,裡面分別建立兩個目錄,actions和reducers,分別存放我們的action和reducer。

首先引入redux
yarn add redux -S
複製程式碼

目錄下actions下counter.js

/*action*/

export const INCREMENT = "counter/INCREMENT";
export const DECREMENT = "counter/DECREMENT";
export const RESET = "counter/RESET";

export function increment() {
    return {type: INCREMENT}
}

export function decrement() {
    return {type: DECREMENT}
}

export function reset() {
    return {type: RESET}
}
複製程式碼

目錄下reducers下counter.js

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

/*
* 初始化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
    }
}
複製程式碼

在webpack配置裡新增actions和reducers的別名。

actions: path.join(__dirname, '../src/redux/actions'),
reducers: path.join(__dirname, '../src/redux/reducers')
複製程式碼

到這裡要說一下,action建立函式,主要是返回一個action類,action類有個type屬性,來決定執行哪一個reducer。reducer是一個純函式(只接受和返回引數,不引入其他變數或做其他功能),主要接受舊的state和action,根據action的type來判斷執行,然後返回一個新的state。

特殊說明:你可能有很多reducer,type一定要是全域性唯一的,一般通過prefix來修飾實現。栗子:counter/INCREMENT裡的counter就是他所有type的字首。
複製程式碼

接下來我麼要在redux目錄下建立一個store.js。

import {createStore} from 'redux';
import counter  from 'reducers/counter';

let store = createStore(counter);

export default store;
複製程式碼

store的具體功能介紹:

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

接著我們建立一個counter頁面來使用redux資料。在pages目錄下建立一個counter目錄和index.js。 頁面中引用我們的actions來執行reducer改變資料。

import React, {PureComponent} from 'react';
import { connect } from 'react-redux';
import { increment, decrement, reset } from 'actions/counter';

class Counter extends PureComponent {
    render() {
        return (
            <div>
                <div>當前計數為{this.props.count}</div>
                <button onClick={() => this.props.increment()}>自增
                </button>
                <button onClick={() => this.props.decrement()}>自減
                </button>
                <button onClick={() => this.props.reset()}>重置
                </button>
            </div>
        )
    }
}
export default connect((state) => state, dispatch => ({
    increment: () => {
        dispatch(increment())
    },
    decrement: () => {
        dispatch(decrement())
    },
    reset: () => {
        dispatch(reset())
    }
}))(Counter);
複製程式碼

connect是什麼呢?react-redux提供了一個方法connect。connect主要有兩個引數,一個mapStateToProps,就是把redux的state,轉為元件的Props,還有一個引數是mapDispatchToprops,把發射actions的方法,轉為Props屬性函式。

然後我們引入react-redux:

yarn add react-redux  -S
複製程式碼

接著我們新增計數器的選單和路由來展示我們的計數器功能。

Nav元件

<li><Link to="/counter">Counter</Link></li>
複製程式碼
router.js
import Counter from 'pages/counter';
---
<Route path="/counter" component={Counter}/>
複製程式碼

最後在src/index.js中使用store功能

import {Provider} from 'react-redux';
import store from './redux/store';

ReactDom.render(
    <Provider store={store}>
        <Router>
            <Nav/>
            {getRouter()}
        </Router>
    </Provider>,
    document.getElementById('app')
)
複製程式碼

Provider元件是讓所有的元件可以訪問到store。不用手動去傳。也不用手動去監聽。 接著我們啟動一下,npm start,然後就可以再瀏覽器中看到我們的計數器功能了。

我們開發中會有很多的reducer,redux提供了一個combineReducers函式來合併reducer,使用起來非常簡單。在store.js中引入combineReducers並使用它。

import {combineReducers} from "redux";

let store = createStore(combineReducers({counter}));
複製程式碼

然後我們在counter頁面元件中,使用connect注入的state改為counter即可(state完整樹中選擇你需要的資料集合)。

export default connect(({counter}) => counter, dispatch => ({
    increment: () => {
        dispatch(increment())
    },
    decrement: () => {
        dispatch(decrement())
    },
    reset: () => {
        dispatch(reset())
    }
}))(Counter);
複製程式碼

梳理一下redux的工作流:

  1. 呼叫store.dispatch(action)提交action。
  2. redux store呼叫傳入的reducer函式。把當前的state和action傳進去。
  3. 根 reducer 應該把多個子 reducer 輸出合併成一個單一的 state 樹。
  4. Redux store 儲存了根 reducer 返回的完整 state 樹。

HtmlWebpackPlugin優化

之前我們一直通過webpack裡面的

contentBase: path.join(__dirname, '../dist'),
複製程式碼

配置獲取dist/index.html來訪問。需要寫死引入的JS,比較麻煩。這個外掛,每次會自動把js插入到你的模板index.html裡面去。

yarn add html-webpack-plugin -D
複製程式碼

然後註釋webpack的contentBase配置,並在根目錄下新建public目錄,將dist下的index.html移動到public下,然後刪除bundle.js的引用

接著在webpack.dev.config.js裡面加入html-webpack-plugin的配置。

const HtmlWebpackPlugin = require('html-webpack-plugin');

plugins: [
    new HtmlWebpackPlugin({
        filename: 'index.html',
        template: path.join(__dirname, '../public/index.html')
    })
]
複製程式碼

接下來,我們每次啟動都會使用這個html-webpack-plugin,webpack會自動將打包好的JS注入到這個index.html模板裡面。

編譯css優化

首先引入css的loader

yarn add css-loader style-loader -D
複製程式碼

然後在我們之前的pages/page目錄下新增index.css檔案,寫入一行css

.page-box {
    border: 1px solid red;
    display: flex;
}
複製程式碼

然後我們在page/index.js中引入並使用

import './index.css';

<div class="page-box">
    this is Page~
</div>
複製程式碼

最後我們讓webpack支援載入css,在webpack.dev.config.js rules增加

{
   test: /\.css$/,
   use: ['style-loader', 'css-loader']
}
複製程式碼

npm start 啟動後檢視page路由就可以看到樣式生效了。

  • css-loader使你能夠使用類似@import 和 url(...)的方法實現 require()的功能;

  • style-loader將所有的計算後的樣式加入頁面中; 二者組合在一起使你能夠把樣式表嵌入webpack打包後的JS檔案中。

整合PostCSS優化

剛才的樣式我們加了個display:flex;樣式,往往我們在寫CSS的時候需要加瀏覽器字首。可是手動新增太過於麻煩,PostCSS提供了Autoprefixer這個外掛來幫我們完成這個工作。

首先引入相關包

yarn add postcss-loader postcss-cssnext -D
複製程式碼

postcss-cssnext 允許你使用未來的 CSS 特性(包括 autoprefixer)。

然後配置webpack.dev.config.js

rules: [{
    test: /\.(css)$/,
    use: ["style-loader", "css-loader", "postcss-loader"]
}]
複製程式碼

然後在根目錄下新建postcss.config.js

module.exports = {
    plugins: {
        'postcss-cssnext': {}
    }
};
複製程式碼

現在你執行程式碼,然後寫個css,去瀏覽器審查元素,看看,屬性是不是生成了瀏覽器字首!。如下:

編譯前
.page-box {
    border: 1px solid red;
    display: flex;
}

編譯後
.page-box {
    border: 1px solid red;
    display: -webkit-box;
    display: -ms-flexbox;
    display: flex;
}
複製程式碼

CSS Modules優化

CSS的規則都是全域性的,任何一個元件的樣式規則,都對整個頁面有效。產生區域性作用域的唯一方法,就是使用一個獨一無二的class的名字,不會與其他選擇器重名。這就是 CSS Modules 的做法。

我們在webpack.dev.config.js中啟用modules

use: ['style-loader', 'css-loader?modules', 'postcss-loader']
複製程式碼

接著我們在引入css的時候,可以使用物件.屬性的形式。(這裡有中劃線,使用[屬性名]的方式)

import style from './index.css';

<div className={style["page-box"]}>
    this is Page~
</div>
複製程式碼

這個時候開啟控制檯,你會發現className變成了一個雜湊字串。然後我們可以美化一下,使用cssmodules的同時,也能看清楚原先是哪個樣式。修改css-loader

之前
css-loader?modules

之後
{
    loader:'css-loader',
    options: {
        modules: true,
        localIdentName: '[local]--[hash:base64:5]'
    }
}
複製程式碼

重啟webpack後開啟控制檯,發現class樣式變成了class="page-box--1wbxe",是不是很好用。

編譯圖片優化

首先引入圖片的載入器

yarn add url-loader file-loader -D
複製程式碼

然後在src下新建images目錄,並放一個圖片a.jpg。

接著在webpack.dev.config.js的rules中配置,同時新增images別名。

{
    test: /\.(png|jpg|gif)$/,
    use: [{
        loader: 'url-loader',
        options: {
            limit: 8192
        }
    }]
}

images: path.join(__dirname, '../src/images'),
複製程式碼

options limit 8192意思是,小於等於8K的圖片會被轉成base64編碼,直接插入HTML中,減少HTTP請求。

然後我們繼續在剛才的page頁面,引入圖片並使用它。

import pic from 'images/a.jpg'

<div className={style["page-box"]}>
    this is Page~
    <img src={pic}/>
</div>
複製程式碼

重啟webpack後檢視到圖片。

按需載入

我們現在啟動後看到他每次都載入一個bundle.js檔案。當我們首屏載入的時候,就會很慢。因為他也下載其他的東西,所以我們需要一個東西區分我們需要載入什麼。目前大致分為按路由和按元件。我們這裡使用常用的按路由載入。react-router4.0以上提供了react-loadable。

首先引入react-loadable

yarn add react-loadable -D
複製程式碼

然後改寫我們的router.js

之前
import Home from 'pages/home';
import Page from 'pages/page';
import Counter from 'pages/counter';

之後
import loadable from 'react-loadable';
import Loading from 'components/Loading';

const Home = loadable({
    loader: () => import('pages/Home'),
    loading: Loading,
    timeout: 10000, // 10 seconds
})
const Page = loadable({
    loader: () => import('pages/page'),
    loading: Loading,
    timeout: 10000, // 10 seconds
})
const Counter = loadable({
    loader: () => import('pages/Counter'),
    loading: Loading,
    timeout: 10000, // 10 seconds
})
複製程式碼

loadable需要一個loading元件,我們在components下新增一個Loading元件

import React from 'react';

export default () => {
    return <div>Loading...</div>
};
複製程式碼

這個時候啟動會發現報錯不支援動態匯入,那麼我們需要babel支援動態匯入。 首先引入

yarn add @babel/plugin-syntax-dynamic-import -D
複製程式碼

然後配置babel.config.js檔案

plugins: ["@babel/plugin-syntax-dynamic-import"]
複製程式碼

再啟動就會發現source下不只有bundle.js一個檔案了。而且每次點選路由選單,都會新載入該選單的檔案,真正的做到了按需載入。

新增404路由

pages目錄下新建一個notfound目錄和404頁面元件

import React, {PureComponent} from 'react';

class NotFound extends PureComponent {
    render() {
        return (
            <div>
                404
            </div>
        )
    }
}
export default NotFound;
複製程式碼

router.js中新增404路由

const NotFound = loadable({
    loader: () => import('pages/notfound'),
    loading: Loading,
    timeout: 10000, // 10 seconds
})

<Switch>
    <Route exact path="/" component={Home}/>
    <Route path="/page" component={Page}/>
    <Route path="/counter" component={Counter}/>
    <Route component={NotFound}/>
</Switch>
複製程式碼

這個時候輸入一個不存在的路由,就會發現頁面元件展現為404。

提取公共程式碼

我們打包的檔案裡面包含了react,redux,react-router等等這些程式碼,每次釋出都要重新載入,其實沒必要,我們可以將他們單獨提取出來。在webpack.dev.config.js中配置入口:

entry: {
    app:[
        path.join(__dirname, '../src/index.js')
    ],
    vendor: ['react', 'react-router-dom', 'redux', 'react-dom', 'react-redux']
},
output: {
    path: path.join(__dirname, '../dist'),
    filename: '[name].[hash].js',
    chunkFilename: '[name].[chunkhash].js'
},
複製程式碼

提取css檔案

我們看到source下只有js檔案,但是實際上我們是有一個css檔案的,它被打包進入了js檔案裡面,現在我們將它提取出來。 使用webpack的mini-css-extract-plugin外掛。

yarn add mini-css-extract-plugin -D
複製程式碼

然後在webpack中配置

const MiniCssExtractPlugin = require("mini-css-extract-plugin");

{
    test: /\.css$/,
    use: [{loader: MiniCssExtractPlugin.loader}, {
        loader:'css-loader',
        options: {
            modules: true,
            localIdentName: '[local]--[hash:base64:5]'
        }
    }, 'postcss-loader']
 }
 
 new MiniCssExtractPlugin({ // 壓縮css
    filename: "[name].[contenthash].css",
    chunkFilename: "[id].[contenthash].css"
})
複製程式碼

然後在重啟,會發現source中多了一個css檔案,那麼證明我們提取成功了

快取

剛才我們output輸出的時候寫入了hash、chunkhash和contenthash,那他們到底有什麼用呢?

  • hash是跟整個專案的構建相關,只要專案裡有檔案更改,整個專案構建的hash值都會更改,並且全部檔案都共用相同的hash值
  • chunkhash和hash不一樣,它根據不同的入口檔案(Entry)進行依賴檔案解析、構建對應的chunk,生成對應的雜湊值。
  • contenthash是針對檔案內容級別的,只有你自己模組的內容變了,那麼hash值才改變,所以我們可以通過contenthash解決上訴問題

生產壞境構建

開發環境(development)和生產環境(production)的構建目標差異很大。 在開發環境中,我們需要具有實時重新載入 或 熱模組替換能力的 source map 和 localhost server。 在生產環境中,我們的目標則轉向於關注更小的 bundle,更輕量的 source map,以及更優化的資源,以改善載入時間。

build目錄下新建webpack.prod.config.js,複製原有配置做修改。首先刪除webpack.dev.config.js中的MiniCssExtractPlugin,然後刪除webpack.prod.config.js中的devServer,然後修改打包命令。

"build": "webpack --config ./build/webpack.prod.config.js"
複製程式碼

再把devtool的值改成none。

devtool: 'none',
複製程式碼

接下來我們為打包多做一些優化。

檔案壓縮

以前webpack使用uglifyjs-webpack-plugin來壓縮檔案,使我們打包出來的檔案體積更小。

現在只需要配置mode即可自動使用開發環境的一些配置,包括JS壓縮等等

mode:'production',
複製程式碼

打包後體積大幅度變小。

公共塊提取

這表示將選擇哪些塊進行優化。當提供一個字串,有效值為all,async和initial。提供all可以特別強大,因為這意味著即使在非同步和非非同步塊之間也可以共享塊。

optimization: {
    splitChunks: {
      chunks: 'all'
    }
}
複製程式碼

重新打包,你會發現打包體積變小。

css壓縮

我們發現使用了生產環境的mode配置以後,JS是壓縮了,但是css並沒有壓縮。這裡我們使用optimize-css-assets-webpack-plugin外掛來壓縮css。以下是官網建議

雖然webpack 5可能內建了CSS minimizer,但是你需要攜帶自己的webpack 4。要縮小輸出,請使用像optimize-css-assets-webpack-plugin這樣的外掛。設定optimization.minimizer會覆蓋webpack提供的預設值,因此請務必同時指定JS minimalizer:
複製程式碼

首先引入

yarn add optimize-css-assets-webpack-plugin -D
複製程式碼

新增打包配置webpack.prod.config.js

const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');

plugins: [
    ...
    new OptimizeCssAssetsPlugin()
],
複製程式碼

重新打包,你會發現單獨提取出來的CSS也壓縮了。

打包清空

我們發現每次打包,只要改動後都會增加檔案,怎麼自動清空之前的打包內容呢?webpack提供了clean-webpack-plugin外掛。 首先引入

yarn add clean-webpack-plugin -D
複製程式碼

然後配置打包檔案

const CleanWebpackPlugin = require('clean-webpack-plugin');

new CleanWebpackPlugin(), // 每次打包前清空
複製程式碼

public path

publicPath 配置選項在各種場景中都非常有用。你可以通過它來指定應用程式中所有資源的基礎路徑。在打包配置中新增

output: {
    publicPath : '/'
}
複製程式碼

加入 @babel/polyfill、@babel/plugin-transform-runtime、core-js、@babel/runtime-corejs2、@babel/plugin-proposal-class-properties

yarn add @babel/polyfill -S
複製程式碼

將以下行新增到您的webpack配置檔案的入口中:

 /*入口*/
entry: {
    app:[
        "@babel/polyfill",
        path.join(__dirname, '../src/index.js')
    ],
    vendor: ['react', 'react-router-dom', 'redux', 'react-dom', 'react-redux']
},
複製程式碼

@babel/polyfill可以讓我們愉快的使用瀏覽器不相容的es6、es7的API。但是他有幾個缺點:

  • 一是我們只是用了幾個API,它卻整個的引入了
  • 二是會汙染全域性

接下來我們做一下優化,新增

yarn add @babel/plugin-transform-runtime -D
yarn add core-js@2.6.5 -D
yarn add @babel/plugin-proposal-class-properties -D

yarn add @babel/runtime-corejs2 -S
複製程式碼

新增完後配置page.json,新增browserslist,來宣告生效瀏覽器

"browserslist": [
    "> 1%",
    "last 2 versions"
  ],
複製程式碼

在修改我們的babel配置檔案

{
    presets: [["@babel/preset-env",{
        useBuiltIns: "entry",
        corejs: 2
    }], "@babel/preset-react"],
    plugins: ["@babel/plugin-syntax-dynamic-import",'@babel/plugin-transform-runtime','@babel/plugin-proposal-class-properties']
}
複製程式碼

useBuiltIns是關鍵屬性,它會根據 browserlist 是否轉換新語法與 polyfill 新 AP業務程式碼使用到的新 API 按需進行 polyfill

  • false : 不啟用polyfill, 如果 import '@babel/polyfill', 會無視 browserlist 將所有的 polyfill 載入進來
  • entry : 啟用,需要手動 import '@babel/polyfill', 這樣會根據 browserlist 過濾出 需要的 polyfill
  • usage : 不需要手動import '@babel/polyfill'(加上也無妨,構造時會去掉), 且會根據 browserlist +

注:經測試usage無法支援IE,推薦使用entry,雖然會大幾十K。

@babel/plugin-transform-runtime和@babel/runtime-corejs2,前者是開發時候使用,後者是生產環境使用。主要功能:避免多次編譯出helper函式:Babel轉移後的程式碼想要實現和原來程式碼一樣的功能需要藉助一些幫助函式。還可以解決@babel/polyfill提供的類或者例項方法汙染全域性作用域的情況。

@babel/plugin-proposal-class-properties是我之前漏掉了,如果你要在class裡面寫箭頭函式或者裝飾器什麼的,需要它的支援。

資料請求axios和Mock

我們現在做前後端完全分離的應用,前端寫前端的,服務端寫服務端的,他們通過API介面連線。 然而往往服務端介面寫的好慢,前端沒法除錯,只能等待。這個時候我們就需要我們的mock.js來自己提供資料。 Mock.js會自動攔截的我們的ajax請求,並且提供各種隨機生成資料。(一定要註釋開始配置的代理,否則無法請求到我們的mock資料)

首先安裝mockjs

yarn add mockjs -D
複製程式碼

然後在根目錄下新建mock目錄,建立mock.js

import Mock from 'mockjs';
 
Mock.mock('/api/user', {
    'name': '@cname',
    'intro': '@word(20)'
});
複製程式碼

上面程式碼的意思就是,攔截/api/user,返回隨機的一箇中文名字,一個20個字母的字串。 然後在我們的src/index.js中引入它。

import '../mock/mock.js';
複製程式碼

介面和資料都準備好了,接下來我們寫一個請求獲取資料並展示。

首先引入axios

yarn add axios -S
複製程式碼

然後分別建立userInfo的reducer、action和page

redux/actions/userInfo.js如下

import axios from 'axios';

export const GET_USER_INFO = "userInfo/GET_USER_INFO";

export function getUserInfo() {
    return dispatch=>{
        axios.post('/api/user').then((res)=>{
            let data = JSON.parse(res.request.responseText);
            dispatch({
                type: GET_USER_INFO,
                payload:data
            });
        })
    }
}
複製程式碼
redux/reducers/userInfo.js如下

import { GET_USER_INFO } from 'actions/userInfo';


const initState = {
    userInfo: {}
};

export default function reducer(state = initState, action) {
    switch (action.type) {
        case GET_USER_INFO:
            return {
                ...state,
                userInfo: action.payload,
            };
        default:
            return state;
    }
}
複製程式碼
pages/userInfo/index.js如下

import React, {PureComponent} from 'react';
import {connect} from 'react-redux';
import {getUserInfo} from "actions/userInfo";

class UserInfo extends PureComponent {

    render() {
        const { userInfo={} } = this.props.userInfo;
        return (
            <div>
                {
                    <div>
                        <p>使用者資訊:</p>
                        <p>使用者名稱:{userInfo.name}</p>
                        <p>介紹:{userInfo.intro}</p>
                    </div>
                }
                <button onClick={() => this.props.getUserInfo()}>請求使用者資訊</button>
            </div>
        )
    }
}

export default connect((userInfo) => userInfo, {getUserInfo})(UserInfo);
複製程式碼

然後將我們的userInfo新增到全域性唯一的state,store裡面去,

store.js

import userInfo  from 'reducers/userInfo';

let store = createStore(combineReducers({counter, userInfo}));
複製程式碼

最後在新增新的路由和選單即可

router.js

const UserInfo = loadable({
    loader: () => import('pages/UserInfo'),
    loading: Loading,
    timeout: 10000, // 10 seconds
})

<Route path="/userinfo" component={UserInfo}/>
複製程式碼
components/Nav/index.js

<li><Link to="/userinfo">UserInfo</Link></li>
複製程式碼

執行,點選請求獲取資訊按鈕,發現報錯:Actions must be plain objects. Use custom middleware for async actions.這句話標識actions必須是個action物件,如果想要使用非同步必須藉助中介軟體。

redux-thunk中介軟體

我們先引入它

yarn add redux-thunk -S
複製程式碼

然後我們使用redux提供的applyMiddleware方法來啟動redux-thunk中介軟體,使actions支援非同步函式。

import {createStore, applyMiddleware} from 'redux';
import thunkMiddleware from 'redux-thunk';

let store = createStore(combineReducers({counter, userInfo}), applyMiddleware(thunkMiddleware));
複製程式碼

然後我們在重新啟動一下,會發現獲取到了資料。

部署

為了測試我們打包出來的檔案是否可行,這裡簡單搭一個小型的express服務。首先根目錄下新建一個server目錄,在該目錄下執行以下命令。

npm init 

yarn add nodemon express -D
複製程式碼
  • express 是一個比較容易上手的node框架
  • nodemon 是一個node開發輔助工具,可以無需重啟更新nodejs的程式碼,非常好用。 安裝好依賴後,我們新增我們的express.js檔案來寫node服務
var express = require('express');
var path = require('path');
var app = express();

app.get('/dist*', function (req, res) {
   res.sendFile( path.join(__dirname , "../" + req.url));
})
app.use(function (req, res) {
	res.sendFile(path.join( __dirname , "../dist/" + "index.html" ));
}) 
 
var server = app.listen(8081, function () {
  var host = server.address().address
  var port = server.address().port
  console.log("應用例項,訪問地址為 http://%s:%s", host, port)
})
複製程式碼

node的程式碼我就不細說了,大家可以網上找找教程。這裡主要是啟動了一個埠為8081的服務,然後做了兩個攔截,第一個攔截是所有訪問dist*這個地址的,將它轉到我們的dist下面打包的檔案上。第二個攔截是攔截所有錯誤的地址,將它轉發到我們的index.html上,這個可以解決重新整理404的問題。

在server目錄package.json檔案中新增啟動命令並執行。

"test": "nodemon ./express.js"
複製程式碼
npm run test
複製程式碼

啟動後訪問http://localhost:8081會發現很多模組引入404,不用慌,這裡涉及到之前講到的一個知識點--publicPath。我們將它改為

publicPath : '/dist/',
複製程式碼

在打包一次,就會發現一切正常了,我們node服務好了,打包出來的程式碼也能正常使用。

結尾

到這裡,本搭建一個react全家桶的教程就結束了。第一次寫,有些地方總結的不太好。話不多說,放一些資料供大家參考。

特別說明

本人也是萬千前端業務仔的一員,有些問題問到了我的知識盲區或者沒時間回覆,請見諒,感謝!!!

另外本教程主要是針對新人和其他技術棧轉react的新朋友作參考,能夠對react框架有個相對全面的瞭解。其他的優化和支援就不在這裡新增了。

建議本教程只做參考學習,並不能作為一個優質的專案可用開發框架。

程式碼本人會再測試一遍,下週會上傳到github。

git地址(這麼大應該看得到吧)

相關文章