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

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

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

前言

本人也是半路加入react大軍的一員,因爲半路加入,對總體框架了解較少,使用現成DVA框架,始終是隻知其一;不知其二。日常遇到問題,老是須要找資料去解決,也有問題最後難以解決,爲了方便本身理解整個react相關的技術,也避免後來像我同樣的人繼續踩坑,能夠根據這個教程有個比較全面的瞭解。(高手勿拍)!!!html

項目簡介

1.技術棧目前是最新的前端

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

2.包管理工具java

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

直接開始

初始化項目

  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.jsreact

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

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

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目錄同級)ios

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

而後寫入一行內容git

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-corejs二、@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可讓咱們愉快的使用瀏覽器不兼容的es六、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地址(這麼大應該看獲得吧)

相關文章
相關標籤/搜索