從零搭建React全家桶框架教程

從零搭建React全家桶框架教程

源碼地址:https://github.com/brickspert/react-family 歡迎star
提問反饋:blogjavascript

原文地址:https://github.com/brickspert/blog/issues/1(github這裏我會不斷更新教程的)css

此處不更新,github上會一直更新html

寫在前面

當我第一次跟着項目作react項目的時候,因爲半截加入的,對框架了解甚少,只能跟着別人的樣板寫。對整個框架沒有一點了解。java

作項目,老是要解決各類問題的,因此每一個地方都須要去了解,可是對整個框架沒有一個總體的瞭解,實在是不行。node

期間,我也跟着別人的搭建框架的教程一步一步的走,可是常常由於本身太菜,走不下去。在通過各類蹂躪以後,對整個框架也有一個大概的瞭解,
我就想把他寫下來,讓後來的菜鳥能跟着個人教程對react全家桶有一個全面的認識。react

個人這個教程,重新建根文件夾開始,到成型的框架,每一個文件爲何要創建?創建了幹什麼?每一個依賴都是幹什麼的?一步一步寫下來,供你們學習。webpack

固然,這個框架我之後會一直維護的,也但願你們能一塊兒來完善這個框架,若是您有任何建議,歡迎留言,歡迎forkios

在完善本框架的同時,我準備再新建一個兼容ie8的框架react-family-ie8,固然是基於該框架改造的。git

說明

  1. 每一個命令行塊都是以根目錄爲基礎的。例以下面命令行塊,都是基於根目錄的。
cd src/pages
mkdir Home
  1. 技術棧均是目前最新的。
  • react 15.6.1
  • react-router-dom 4.2.2
  • redux 3.7.2
  • webpack 3.5.5
  1. 目錄說明
│  .babelrc                          #babel配置文件
│  package-lock.json
│  package.json
│  README.MD
│  webpack.config.js                 #webpack生產配置文件
│  webpack.dev.config.js             #webpack開發配置文件
│  
├─dist
├─public                             #公共資源文件
└─src                                #項目源碼
    │  index.html                    #index.html模板
    │  index.js                      #入口文件
    │  
    ├─component                      #組建庫
    │  └─Hello
    │          Hello.js
    │          
    ├─pages                          #頁面目錄
    │  ├─Counter
    │  │      Counter.js
    │  │      
    │  ├─Home
    │  │      Home.js
    │  │      
    │  ├─Page1
    │  │  │  Page1.css                #頁面樣式
    │  │  │  Page1.js
    │  │  │  
    │  │  └─images                    #頁面圖片
    │  │          brickpsert.jpg
    │  │          
    │  └─UserInfo
    │          UserInfo.js
    │          
    ├─redux
    │  │  reducers.js
    │  │  store.js
    │  │  
    │  ├─actions
    │  │      counter.js
    │  │      userInfo.js
    │  │      
    │  ├─middleware
    │  │      promiseMiddleware.js
    │  │      
    │  └─reducers
    │          counter.js
    │          userInfo.js
    │          
    └─router                        #路由文件
            Bundle.js
            router.js

init項目

  1. 建立文件夾並進入es6

    `mkdir react-family && cd react-family`
  2. init npm

    `npm init` 按照提示填寫項目基本信息

webpack

  1. 安裝 webpack

    npm install --save-dev webpack

    Q: 何時用--save-dev,何時用--save

    A: --save-dev 是你開發時候依賴的東西,--save 是你發佈以後還依賴的東西。看這裏

  2. 根據webpack文檔編寫最基礎的配置文件

    新建webpack開發配置文件 touch webpack.dev.config.js

    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'
        }
    };
  3. 學會使用webpack編譯文件

    新建入口文件

    mkdir src && touch ./src/index.js

    src/index.js 添加內容

    document.getElementById('app').innerHTML = "Webpack works"

    如今咱們執行命令 webpack --config webpack.dev.config.js

    咱們能夠看到生成了dist文件夾和bundle.js

  4. 如今咱們測試下~

    dist文件夾下面新建一個index.html

    touch ./dist/index.html

    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,能夠看到Webpack works!

    webpack

    如今回頭看下,咱們作了什麼或者說webpack作了什麼。

    把入口文件 index.js 通過處理以後,生成 bundle.js。就這麼簡單。

babel

Babel 把用最新標準編寫的 JavaScript 代碼向下編譯成能夠在今天隨處可用的版本。 這一過程叫作「源碼到源碼」編譯, 也被稱爲轉換編譯。

通俗的說,就是咱們能夠用ES6, ES7等來編寫代碼,Babel會把他們通通轉爲ES5。

npm install --save-dev babel-core babel-loader babel-preset-es2015 babel-preset-react babel-preset-stage-0

新建babel配置文件.babelrc

touch .babelrc

.babelrc

{
   "presets": [
     "es2015",
     "react",
     "stage-0"
   ],
   "plugins": []
 }

修改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 webpack.dev.config.js

瀏覽器打開index.html,咱們看到正確輸出了我如今在使用Babel!

babel

而後咱們打開打包後的bundle.js,翻頁到最下面,能夠看到箭頭函數被轉換成普通函數了!

babel-bundle.png

Q: babel-preset-state-0,babel-preset-state-1,babel-preset-state-2,babel-preset-state-3有什麼區別?

A: 每一級包含上一級的功能,好比 state-0包含state-1的功能,以此類推。state-0功能最全。具體能夠看這篇文章:babel配置-各階段的stage的區別

參考地址:

  1. https://segmentfault.com/a/11...
  2. http://www.ruanyifeng.com/blo...

react

npm install --save react react-dom

修改 src/index.js使用react

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

ReactDom.render(
    <div>Hello React!</div>, document.getElementById('app'));

執行打包命令webpack --config webpack.dev.config.js

打開index.html 看效果。

咱們簡單作下改進,把Hello React放到組件裏面。體現組件化~

cd src
mkdir component
cd component
mkdir Hello
cd Hello
touch Hello.js

按照React語法,寫一個Hello組件

import React, {Component} from 'react';

export default class Hello extends Component {
    render() {
        return (
            <div>
                Hello,React!
            </div>
        )
    }
}

而後讓咱們修改src/index.js,引用Hello組件!

src/index.js

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

ReactDom.render(
    <Hello/>, document.getElementById('app'));

根目錄執行打包命令

webpack --config webpack.dev.config.js

打開index.html看效果咯~

命令優化

Q:每次打包都得在根目錄執行這麼一長串命令webpack --config webpack.dev.config.js,能不打這麼長嗎?

A:修改package.json裏面的script,增長dev-build

package.json

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev-build": "webpack --config webpack.dev.config.js"
  }

如今咱們打包只須要執行npm start-build就能夠啦!

參考地址:

http://www.ruanyifeng.com/blo...

react-router

npm install --save react-router-dom

新建router文件夾和組件

cd src
mkdir router && touch router/router.js

按照react-router文檔編輯一個最基本的router.js。包含兩個頁面homepage1

src/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~
            </div>
        )
    }
}

Page1.js

import React, {Component} from 'react';

export default class Page1 extends Component {
    render() {
        return (
            <div>
                this is Page1~
            </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('app'));

如今執行打包命令npm start-build。打開index.html查看效果啦!

那麼問題來了~咱們發現點擊‘首頁’和‘Page1’沒有反應。不要驚慌,這是正常的。

咱們以前一直用這個路徑訪問index.html,相似這樣:file:///F:/react/react-family/dist/index.html
這種路徑了,不是咱們想象中的路由那樣的路徑http://localhost:3000~咱們須要配置一個簡單的WEB服務器,指向
index.html~有下面兩種方法來實現

  1. Nginx, Apache, IIS等配置啓動一個簡單的的WEB服務器。
  2. 使用webpack-dev-server來配置啓動WEB服務器。

下一節,咱們來使用第二種方法啓動服務器。這一節的DEMO,先放這裏。

參考地址

  1. http://www.jianshu.com/p/e3ad...
  2. http://reacttraining.cn/web/g...

webpack-dev-server

簡單來講,webpack-dev-server就是一個小型的靜態文件服務器。使用它,能夠爲webpack打包生成的資源文件提供Web服務。

npm install webpack-dev-server --save-dev

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

webpack.dev.config.js

devServer: {
        contentBase: path.join(__dirname, './dist')
    }

如今執行

webpack-dev-server --config webpack.dev.config.js

瀏覽器打開http://localhost:8080,OK,如今咱們能夠點擊首頁,Page1了,
看URL地址變化啦!咱們看到react-router已經成功了哦。

Q: --content-base是什麼?

A:URL的根目錄。若是不設定的話,默認指向項目根目錄。

**重要提示:webpack-dev-server編譯後的文件,都存儲在內存中,咱們並不能看見的。你能夠刪除以前遺留的文件dist/bundle.js
仍然能正常打開網站!**

每次執行webpack-dev-server --config webpack.dev.config.js,要打很長的命令,咱們修改package.json,增長script->start:

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev-build": "webpack --config webpack.dev.config.js",
    "start": "webpack-dev-server --config webpack.dev.config.js"
  }

下次執行npm start就能夠了。

既然用到了webpack-dev-server,咱們就看看它的其餘的配置項
看了以後,發現有幾個咱們能夠用的。

  • color(CLI only) console中打印彩色日誌
  • historyApiFallback 任意的404響應都被替代爲index.html。有什麼用呢?你如今運行
    npm start,而後打開瀏覽器,訪問http://localhost:8080,而後點擊Page1到連接http://localhost:8080/page1

而後刷新頁面試試。是否是發現刷新後404了。爲何?dist文件夾裏面並無page1.html,固然會404了,因此咱們須要配置
historyApiFallback,讓全部的404定位到index.html

  • host 指定一個host,默認是localhost。若是你但願服務器外部能夠訪問,指定以下:host: "0.0.0.0"。好比你用手機經過IP訪問。
  • hot 啓用Webpack的模塊熱替換特性。關於熱模塊替換,我下一小節專門講解一下。
  • port 配置要監聽的端口。默認就是咱們如今使用的8080端口。
  • proxy 代理。好比在 localhost:3000 上有後端服務的話,你能夠這樣啓用代理:
proxy: {
      "/api": "http://localhost:3000"
    }
  • progress(CLI only) 將編譯進度輸出到控制檯。

根據這幾個配置,修改下咱們的webpack-dev-server的配置~

webpack.dev.config.js

devServer: {
        contentBase: path.join(__dirname, './dist'),
        historyApiFallback: true,
        host: '0.0.0.0'
    }

CLI ONLY的須要在命令行中配置

package.json

"dev": "webpack-dev-server --config webpack.dev.config.js --color --progress"

如今咱們執行npm start 看看效果。是否是看到打包的時候有百分比進度?在http://localhost:8080/page1頁面刷新是否是沒問題了?
用手機經過局域網IP是否能夠訪問到網站?

參考地址:

  1. https://segmentfault.com/a/11...
  2. https://webpack.js.org/guides...

模塊熱替換(Hot Module Replacement)

到目前,當咱們修改代碼的時候,瀏覽器會自動刷新,不信你能夠去試試。(若是你的不會刷新,看看這個調整文本編輯器

我相信看這個教程的人,應該用過別人的框架。咱們在修改代碼的時候,瀏覽器不會刷新,只會更新本身修改的那一塊。咱們也要實現這個效果。

咱們看下webpack模塊熱替換教程。

咱們接下來要這麼修改

package.json 增長 --hot

"dev": "webpack-dev-server --config webpack.dev.config.js --color --progress --hot"

src/index.js 增長module.hot.accept(),以下。當模塊更新的時候,通知index.js

src/index.js

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

import getRouter from './router/router';

if (module.hot) {
    module.hot.accept();
}

ReactDom.render(
    getRouter(), document.getElementById('app'));

如今咱們執行npm start,打開瀏覽器,修改Home.js,看是否是不刷新頁面的狀況下,內容更新了?驚不驚喜?意不意外?

作模塊熱替換,咱們只改了幾行代碼,很是簡單的。紙老虎一個~

如今我須要說明下咱們命令行使用的--hot,能夠經過配置webpack.dev.config.js來替換,
向文檔上那樣,修改下面三處。但咱們仍是用--hot吧。下面的方式咱們知道一下就行,咱們不用。一樣的效果。

const webpack = require('webpack');

devServer: {
    hot: true
}

plugins:[
     new webpack.HotModuleReplacementPlugin()
]

HRM配置其實有兩種方式,一種CLI方式,一種Node.js API方式。咱們用到的就是CLI方式,比較簡單。
Node.js API方式,就是建一個server.js等等,網上大部分教程都是這種方式,這裏不作講解了。

你覺得模塊熱替換到這裏就結束了?no~no~no~

上面的配置對react模塊的支持不是很好哦。

例以下面的demo,當模塊熱替換的時候,state會重置,這不是咱們想要的。

修改Home.js,增長計數state

src/pages/Home/Home.js

import React, {Component} from 'react';

export default class Home extends Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        }
    }

    _handleClick() {
        this.setState({
            count: ++this.state.count
        });
    }

    render() {
        return (
            <div>
                this is home~<br/>
                當前計數:{this.state.count}<br/>
                <button onClick={() => this._handleClick()}>自增</button>
            </div>
        )
    }
}

你能夠測試一下,當咱們修改代碼的時候,webpack在更新頁面的時候,也把count初始爲0了。

爲了在react模塊更新的同時,能保留state等頁面中其餘狀態,咱們須要引入react-hot-loader~

Q: 請問webpack-dev-serverreact-hot-loader二者的熱替換有什麼區別?

A: 區別在於webpack-dev-server本身的--hot模式只能即時刷新頁面,但狀態保存不住。由於React有一些本身語法(JSX)是HotModuleReplacementPlugin搞不定的。
react-hot-loader--hot基礎上作了額外的處理,來保證狀態能夠存下來。(來自segmentfault

下面咱們來加入react-hot-loader v3,

安裝依賴

npm install react-hot-loader@next --save-dev

根據文檔
咱們要作以下幾個修改~

  1. .babelrc 增長 react-hot-loader/babel

.babelrc

{
  "presets": [
    "es2015",
    "react",
    "stage-0"
  ],
  "plugins": [
    "react-hot-loader/babel"
  ]
}
  1. webpack.dev.config.js入口增長react-hot-loader/patch

webpack.dev.config.js

entry: [
        'react-hot-loader/patch',
        path.join(__dirname, 'src/index.js')
    ]
  1. src/index.js修改以下

src/index.js

import React from 'react';
import ReactDom from 'react-dom';
import {AppContainer} from 'react-hot-loader';

import getRouter from './router/router';

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

/*熱更新*/
if (module.hot) {
    module.hot.accept('./router/router', () => {
        const getRouter = require('./router/router').default;
        renderWithHotReload(getRouter());
    });
}

function renderWithHotReload(RootElement) {
    ReactDom.render(
        <AppContainer>
            {RootElement}
        </AppContainer>,
        document.getElementById('app')
    )
}

如今,執行npm start,試試。是否是修改頁面的時候,state不更新了?

參考文章:

  1. https://github.com/gaearon/re...

文件路徑優化

作到這裏,咱們簡單休息下。作下優化~

在以前寫的代碼中,咱們引用組件,或者頁面時候,寫的是相對路徑~

好比src/router/router.js裏面,引用Home.js的時候就用的相對路徑

import Home from '../pages/Home/Home';

webpack提供了一個別名配置,就是咱們不管在哪一個路徑下,引用均可以這樣

import Home from 'pages/Home/Home';

下面咱們來配置下,修改webpack.dev.config.js,增長別名~

webpack.config.js

resolve: {
        alias: {
            pages: path.join(__dirname, 'src/pages'),
            component: path.join(__dirname, 'src/component'),
            router: path.join(__dirname, 'src/router')
        }
    }

而後咱們把以前使用的絕對路徑通通改掉。

src/router/router.js

import Home from 'pages/Home/Home';
import Page1 from 'pages/Page1/Page1';

src/index.js

import getRouter from 'router/router';

咱們這裏約定,下面,咱們會默認配置須要的別名路徑,再也不作重複的講述哦。

redux

接下來,咱們就要就要就要集成redux了。

要對redux有一個大概的認識,能夠閱讀阮一峯前輩的Redux 入門教程(一):基本用法

若是要對redux有一個很是詳細的認識,我推薦閱讀中文文檔,寫的很是好。讀了這個教程,有一個很是深入的感受,redux並無任何魔法。

不要被各類關於 reducers, middleware, store 的演講所矇蔽 ---- Redux 實際是很是簡單的。

固然,我這篇文章是寫給新手的,若是看不懂上面的文章,或者不想看,不要緊。先會用,多用用就知道原理了。

開始整代碼!咱們就作一個最簡單的計數器。自增,自減,重置。

先安裝redux npm install --save redux

初始化目錄結構

cd src
mkdir redux
cd redux
mkdir actions
mkdir reducers
touch reducers.js
touch store.js
touch actions/counter.js
touch reducers/counter.js

先來寫action建立函數。經過action建立函數,能夠建立action~
src/redux/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}
}

再來寫reducer,reducer是一個純函數,接收action和舊的state,生成新的state.

src/redux/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
    }
}

一個項目有不少的reducers,咱們要把他們整合到一塊兒

src/redux/reducers.js

import counter from './reducers/counter';

export default function combineReducers(state = {}, action) {
    return {
        counter: counter(state.counter, action)
    }
}

到這裏,咱們必須再理解下一句話。

reducer就是純函數,接收stateaction,而後返回一個新的 state

看看上面的代碼,不管是combineReducers函數也好,仍是reducer函數也好,都是接收stateaction
返回更新後的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;

到如今爲止,咱們已經可使用redux了~

下面咱們就簡單的測試下

cd src
cd redux
touch testRedux.js

src/redux/testRedux.js

import {increment, decrement, reset} from './actions/counter';

import store from './store';

// 打印初始狀態
console.log(store.getState());

// 每次 state 更新時,打印日誌
// 注意 subscribe() 返回一個函數用來註銷監聽器
let unsubscribe = store.subscribe(() =>
    console.log(store.getState())
);

// 發起一系列 action
store.dispatch(increment());
store.dispatch(decrement());
store.dispatch(reset());

// 中止監聽 state 更新
unsubscribe();

當前文件夾執行命令

webpack testRedux.js build.js

node build.js

是否是看到輸出了state變化?

{ counter: { count: 0 } }
{ counter: { count: 1 } }
{ counter: { count: 0 } }
{ counter: { count: 0 } }

作這個測試,就是爲了告訴你們,reduxreact不要緊,雖然說他倆能合做。

到這裏,我建議你再理下redux的數據流,看看這裏

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

就是醬紫~~

這會webpack.dev.config.js路徑別名增長一下,後面好寫了。

webpack.config.js

alias: {
            ...
            actions: path.join(__dirname, 'src/redux/actions'),
            reducers: path.join(__dirname, 'src/redux/reducers'),
            redux: path.join(__dirname, 'src/redux')
        }

把前面的相對路徑都改改。

下面咱們開始搭配react使用。

寫一個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 {BrowserRouter 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="/counter">Counter</Link></li>
            </ul>
            <Switch>
                <Route exact path="/" component={Home}/>
                <Route path="/page1" component={Page1}/>
                <Route path="/counter" component={Counter}/>
            </Switch>
        </div>
    </Router>
);

export default getRouter;

npm start看看效果。

下一步,咱們讓Counter組件和Redux聯合起來。使Counter能得到到Reduxstate,而且能發射action

固然咱們可使用剛纔測試testRedux的方法,手動監聽~手動引入store~可是這確定很麻煩哦。

react-redux提供了一個方法connect

容器組件就是使用 store.subscribe() 從 Redux state 樹中讀取部分數據,並經過 props 來把這些數據提供給要渲染的組件。你能夠手工來開發容器組件,但建議使用 React Redux 庫的 connect() 方法來生成,這個方法作了性能優化來避免不少沒必要要的重複渲染。

connect接收兩個參數,一個mapStateToProps,就是把reduxstate,轉爲組件的Props,還有一個參數是mapDispatchToprops,
就是把發射actions的方法,轉爲Props屬性函數。

先來安裝react-redux

npm install --save react-redux

src/pages/Counter/Counter.js

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

import {connect} from 'react-redux';

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

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

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 組件 <Provider> 來 魔法般的 讓全部容器組件均可以訪問 store,而沒必要顯示地傳遞它。只須要在渲染根組件時使用便可。

src/index.js

import React from 'react';
import ReactDom from 'react-dom';
import {AppContainer} from 'react-hot-loader';
import {Provider} from 'react-redux';
import store from './redux/store';

import getRouter from 'router/router';

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

/*熱更新*/
if (module.hot) {
    module.hot.accept('./router/router', () => {
        const getRouter = require('router/router').default;
        renderWithHotReload(getRouter());
    });
}

function renderWithHotReload(RootElement) {
    ReactDom.render(
        <AppContainer>
            <Provider store={store}>
                {RootElement}
            </Provider>
        </AppContainer>,
        document.getElementById('app')
    )
}

到這裏咱們就能夠執行npm start,打開localhost:8080/counter看效果了。

可是你發現npm start一直報錯

ERROR in ./node_modules/react-redux/es/connect/mapDispatchToProps.js
Module not found: Error: Can't resolve 'redux' in 'F:\Project\react\react-family\node_modules\react-redux\es\connect'

ERROR in ./src/redux/store.js
Module not found: Error: Can't resolve 'redux' in 'F:\Project\react\react-family\src\redux'

WTF?這個錯誤困擾了半天。我說下爲何形成這個錯誤。咱們引用redux的時候這樣用的

import {createStore} from 'redux'

然而,咱們在webapck.dev.config.js裏面這樣配置了

resolve: {
        alias: {
            ...
            redux: path.join(__dirname, 'src/redux')
        }
    }

而後webapck編譯的時候碰到redux都去src/redux去找了。可是找不到啊。因此咱們把webpack.dev.config.js裏面redux這一行刪除了,就行了。
而且把使用咱們本身使用redux文件夾的地方改爲相對路徑哦。

如今你能夠npm start去看效果了。

這裏咱們再縷下(能夠讀React 實踐心得:react-redux 之 connect 方法詳解

  1. Provider組件是讓全部的組件能夠訪問到store。不用手動去傳。也不用手動去監聽。
  2. connect函數做用是從 Redux state 樹中讀取部分數據,並經過 props 來把這些數據提供給要渲染的組件。也傳遞dispatch(action)函數到props

接下來,咱們要說異步action

參考地址: http://cn.redux.js.org/docs/a...

想象一下咱們調用一個異步get請求去後臺請求數據:

  1. 請求開始的時候,界面轉圈提示正在加載。isLoading置爲true
  2. 請求成功,顯示數據。isLoading置爲false,data填充數據。
  3. 請求失敗,顯示失敗。isLoading置爲false,顯示錯誤信息。

下面,咱們以向後臺請求用戶基本信息爲例。

  1. 咱們先建立一個user.json,等會請求用,至關於後臺的API接口。
cd dist
mkdir api
cd api
touch user.json

dist/api/user.json

{
  "name": "brickspert",
  "intro": "please give me a star"
}
  1. 建立必須的action建立函數。
cd src/redux/actions
touch userInfo.js

src/redux/actions/getUserInfo.js

export const GET_USER_INFO_REQUEST = "userInfo/GET_USER_INFO_REQUEST";
export const GET_USER_INFO_SUCCESS = "userInfo/GET_USER_INFO_SUCCESS";
export const GET_USER_INFO_FAIL = "userInfo/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建立函數。

  1. 建立reducer

再強調下,reducer是根據stateaction生成新state純函數

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;
    }
}

這裏的...state語法,是和別人的Object.assign()起同一個做用,合併新舊state。咱們這裏是沒效果的,可是我建議都寫上這個哦

組合reducer

src/redux/reducers.js

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

export default function combineReducers(state = {}, action) {
    return {
        counter: counter(state.counter, action),
        userInfo: userInfo(state.userInfo, action)
    }
}
  1. 如今有了action,有了reducer,咱們就須要調用把action裏面的三個action函數和網絡請求結合起來。

    • 請求中 dispatch getUserInfoRequest
    • 請求成功 dispatch getUserInfoSuccess
    • 請求失敗 dispatch getUserInfoFail

src/redux/actions/userInfo.js增長

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

        return fetch('http://localhost:8080/api/user.json')
            .then((response => {
                return response.json()
            }))
            .then((json) => {
                    dispatch(getUserInfoSuccess(json))
                }
            ).catch(
                () => {
                    dispatch(getUserInfoFail());
                }
            )
    }
}

咱們這裏發現,別的action建立函數都是返回action對象:

{type: xxxx}

可是咱們如今的這個action建立函數 getUserInfo則是返回函數了。

爲了讓action建立函數除了返回action對象外,還能夠返回函數,咱們須要引用redux-thunk

npm install --save redux-thunk

這裏涉及到redux中間件middleware,我後面會講到的。你也能夠讀這裏Middleware

簡單的說,中間件就是action在到達reducer,先通過中間件處理。咱們以前知道reducer能處理的action只有這樣的{type:xxx},因此咱們使用中間件來處理
函數形式的action,把他們轉爲標準的actionreducer。這是redux-thunk的做用。
使用redux-thunk中間件

咱們來引入redux-thunk中間件

src/redux/store.js

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

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

export default store;

到這裏,redux這邊OK了,咱們來寫個組件驗證下。

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

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

這裏你可能發現connect參數寫法不同了,mapStateToProps函數用了es6簡寫,mapDispatchToProps用了react-redux提供的簡單寫法。

增長路由
src/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';
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="/counter">Counter</Link></li>
                <li><Link to="/userinfo">UserInfo</Link></li>
            </ul>
            <Switch>
                <Route exact path="/" component={Home}/>
                <Route path="/page1" component={Page1}/>
                <Route path="/counter" component={Counter}/>
                <Route path="/userinfo" component={UserInfo}/>
            </Switch>
        </div>
    </Router>
);

export default getRouter;

如今你能夠執行npm start去看效果啦!

redux

到這裏redux集成基本告一段落了,後面咱們還會有一些優化。

combinReducers優化

redux提供了一個combineReducers函數來合併reducer,不用咱們本身合併哦。寫起來簡單,可是意思和咱們
本身寫的combinReducers也是同樣的。

src/redux/reducers.js

import {combineReducers} from "redux";

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


export default combineReducers({
    counter,
    userInfo
});

devtool優化

如今咱們發現一個問題,代碼哪裏寫錯了,瀏覽器報錯只報在build.js第幾行。

錯誤圖片

這讓咱們分析錯誤無從下手。看這裏

咱們增長webpack配置devtool

src/webpack.dev.config.js增長

devtool: 'inline-source-map'

此次看錯誤信息是否是提示的很詳細了?

錯誤圖片

同時,咱們在srouce裏面能看到咱們寫的代碼,也能打斷點調試哦~

錯誤圖片

編譯css

先說這裏爲何不用scss,由於Windows使用node-sass,須要先安裝 Microsoft Windows SDK for Windows 7 and .NET Framework 4
我怕有些人copy這份代碼後,沒注意,運行不起來。因此這裏不用scss了,若是須要,自行編譯哦。

npm install css-loader style-loader --save-dev

css-loader使你可以使用相似@importurl(...)的方法實現 require()的功能;

style-loader將全部的計算後的樣式加入頁面中; 兩者組合在一塊兒使你可以把樣式表嵌入webpack打包後的JS文件中。

webpack.dev.config.js rules增長

{
   test: /\.css$/,
   use: ['style-loader', 'css-loader']
}

咱們用Page1頁面來測試下

cd src/pages/Page1
touch Page1.css

src/pages/Page1/Page1.css

.page-box {
    border: 1px solid red;
}

src/pages/Page1/Page1.js

import React, {Component} from 'react';

import './Page1.css';

export default class Page1 extends Component {
    render() {
        return (
            <div className="page-box">
                this is page1~
            </div>
        )
    }
}

好了,如今npm start去看效果吧。

編譯圖片

npm install --save-dev url-loader file-loader

webpack.dev.config.js rules增長

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

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

咱們來用Page1 測試下

cd src/pages/Page1
mkdir images

images文件夾放一個圖片。

修改代碼,引用圖片

src/pages/Page1/Page1.js

import React, {Component} from 'react';

import './Page1.css';

import image from './images/brickpsert.jpg';

export default class Page1 extends Component {
    render() {
        return (
            <div className="page-box">
                this is page1~
                <img src={image}/>
            </div>
        )
    }
}

能夠去看看效果啦。

按需加載

爲何要實現按需加載?

咱們如今看到,打包完後,全部頁面只生成了一個build.js,當咱們首屏加載的時候,就會很慢。由於他也下載了別的頁面的js了哦。

若是每一個頁面都打包了本身單獨的JS,在進入本身頁面的時候才加載對應的js,那首屏加載就會快不少哦。

react-router 2.0時代, 按需加載須要用到的最關鍵的一個函數,就是require.ensure(),它是按需加載可以實現的核心。

在4.0版本,官方放棄了這種處理按需加載的方式,選擇了一個更加簡潔的處理方式。

傳送門

根據官方示例,咱們開搞

  1. npm install bundle-loader --save-dev
  2. 新建bundle.js
cd src/router
touch Bundle.js

src/router/Bundle.js

import React, {Component} from 'react'

class Bundle extends Component {
    state = {
        // short for "module" but that's a keyword in js, so "mod"
        mod: null
    };

    componentWillMount() {
        this.load(this.props)
    }

    componentWillReceiveProps(nextProps) {
        if (nextProps.load !== this.props.load) {
            this.load(nextProps)
        }
    }

    load(props) {
        this.setState({
            mod: null
        });
        props.load((mod) => {
            this.setState({
                // handle both es imports and cjs
                mod: mod.default ? mod.default : mod
            })
        })
    }

    render() {
        return this.props.children(this.state.mod)
    }
}

export default Bundle;
  1. 改造路由器

src/router/router.js

import React from 'react';

import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';

import Bundle from './Bundle';

import Home from 'bundle-loader?lazy&name=home!pages/Home/Home';
import Page1 from 'bundle-loader?lazy&name=page1!pages/Page1/Page1';
import Counter from 'bundle-loader?lazy&name=counter!pages/Counter/Counter';
import UserInfo from 'bundle-loader?lazy&name=userInfo!pages/UserInfo/UserInfo';

const Loading = function () {
    return <div>Loading...</div>
};

const createComponent = (component) => () => (
    <Bundle load={component}>
        {
            (Component) => Component ? <Component/> : <Loading/>
        }
    </Bundle>
);

const getRouter = () => (
    <Router>
        <div>
            <ul>
                <li><Link to="/">首頁</Link></li>
                <li><Link to="/page1">Page1</Link></li>
                <li><Link to="/counter">Counter</Link></li>
                <li><Link to="/userinfo">UserInfo</Link></li>
            </ul>
            <Switch>
                <Route exact path="/" component={createComponent(Home)}/>
                <Route path="/page1" component={createComponent(Page1)}/>
                <Route path="/counter" component={createComponent(Counter)}/>
                <Route path="/userinfo" component={createComponent(UserInfo)}/>
            </Switch>
        </div>
    </Router>
);

export default getRouter;

如今你能夠npm start,打開瀏覽器,看是否是進入新的頁面,都會加載本身的JS的~

可是你可能發現,名字都是0.bundle.js這樣子的,這分不清楚是哪一個頁面的js呀!

咱們修改下webpack.dev.config.js,加個chunkFilenamechunkFilename是除了entry定義的入口js以外的js~

output: {
        path: path.join(__dirname, './dist'),
        filename: 'bundle.js',
        chunkFilename: '[name].js'
    }

如今你運行發現名字變成home.js,這樣的了。棒棒噠!

那麼問題來了home是在哪裏設置的?webpack怎麼知道他叫home

其實在這裏咱們定義了,router.js裏面

import Home from 'bundle-loader?lazy&name=home!pages/Home/Home';

看到沒。這裏有個name=home。嘿嘿。

參考地址:

  1. http://www.jianshu.com/p/8dd9...
  2. https://github.com/ReactTrain...
  3. https://segmentfault.com/a/11...
  4. http://react-china.org/t/webp...
  5. https://juejin.im/post/58f971...

緩存

想象一下這個場景~

咱們網站上線了,用戶第一次訪問首頁,下載了home.js,第二次訪問又下載了home.js~

這確定不行呀,因此咱們通常都會作一個緩存,用戶下載一次home.js後,第二次就不下載了。

有一天,咱們更新了home.js,可是用戶不知道呀,用戶仍是使用本地舊的home.js。出問題了~

怎麼解決?每次代碼更新後,打包生成的名字不同。好比第一次叫home.a.js,第二次叫home.b.js

文檔看這裏

咱們照着文檔來

webpack.dev.config.js

output: {
        path: path.join(__dirname, './dist'),
        filename: '[name].[hash].js',
        chunkFilename: '[name].[chunkhash].js'
    }

每次打包都用增長hash~

如今咱們試試,是否是修改了文件,打包後相應的文件名字就變啦?

package

可是你可能發現了,網頁打開報錯了~由於你dist/index.html裏面引用js名字仍是bundle.js老名字啊,改爲新的名字就能夠啦。

啊~那豈不是我每次編譯打包,都得去改一下js名字?欲知後事如何,且看下節分享。

HtmlWebpackPlugin

這個插件,每次會自動把js插入到你的模板index.html裏面去。

npm install html-webpack-plugin --save-dev

新建模板index.html

cd src
touch index.html

src/index.html

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
<div id="app"></div>
</body>
</html>

修改webpack.dev.config.js,增長plugin

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

    plugins: [new HtmlWebpackPlugin({
        filename: 'index.html',
        template: path.join(__dirname, 'src/index.html')
    })],

npm start運行項目,看看是否是能正常訪問啦。~

說明一下:npm start打包後的文件存在內存中,你看不到的。~ 你能夠把遺留dist/index.html刪除掉了。

提取公共代碼

想象一下,咱們的主文件,原來的bundle.js裏面是否是包含了react,redux,react-router等等
這些代碼??這些代碼基本上不會改變的。可是,他們合併在bundle.js裏面,每次項目發佈,從新請求bundle.js的時候,至關於從新請求了
react等這些公共庫。浪費了~

咱們把react這些不會改變的公共庫提取出來,用戶緩存下來。今後之後,用戶不再用下載這些庫了,不管是否發佈項目。嘻嘻。

webpack文檔給了教程,看這裏

webpack.dev.config.js

var webpack = require('webpack');

    entry: {
        app: [
            'react-hot-loader/patch',
            path.join(__dirname, 'src/index.js')
        ],
        vendor: ['react', 'react-router-dom', 'redux', 'react-dom', 'react-redux']
    }
    
        /*plugins*/
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor'
        })

react等庫生成打包到vendor.hash.js裏面去。

可是你如今可能發現編譯生成的文件app.[hash].jsvendor.[hash].js生成的hash同樣的,這裏是個問題,由於呀,你每次修改代碼,都會致使vendor.[hash].js名字改變,那咱們提取出來的意義也就沒了。其實文檔上寫的很清楚,

output: {
        path: path.join(__dirname, './dist'),
        filename: '[name].[hash].js', //這裏應該用chunkhash替換hash
        chunkFilename: '[name].[chunkhash].js'
    }

可是無奈,若是用chunkhash,會報錯。和webpack-dev-server --hot不兼容,具體看這裏

如今咱們在配置開發版配置文件,就向webpack-dev-server妥協,由於咱們要用他。問題先放這裏,等會咱們配置正式版webpack.config.js的時候要解決這個問題。

生產壞境構建

開發環境(development)和生產環境(production)的構建目標差別很大。在開發環境中,咱們須要具備強大的、具備實時從新加載(live reloading)或熱模塊替換(hot module replacement)能力的 source map 和 localhost server。而在生產環境中,咱們的目標則轉向於關注更小的 bundle,更輕量的 source map,以及更優化的資源,以改善加載時間。因爲要遵循邏輯分離,咱們一般建議爲每一個環境編寫彼此獨立的 webpack 配置。

文檔看這裏

咱們要開始作了~

touch webpack.config.js

webpack.dev.config.js的基礎上先作如下幾個修改~

  1. 先刪除webpack-dev-server相關的東西~
  2. devtool的值改爲cheap-module-source-map
  3. 剛纔說的hash改爲chunkhash

webpack.config.js

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

module.exports = {
    devtool: 'cheap-module-source-map',
    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].[chunkhash].js',
        chunkFilename: '[name].[chunkhash].js'
    },
    module: {
        rules: [{
            test: /\.js$/,
            use: ['babel-loader'],
            include: path.join(__dirname, 'src')
        }, {
            test: /\.css$/,
            use: ['style-loader', 'css-loader']
        }, {
            test: /\.(png|jpg|gif)$/,
            use: [{
                loader: 'url-loader',
                options: {
                    limit: 8192
                }
            }]
        }]
    },
    plugins: [
        new HtmlWebpackPlugin({
            filename: 'index.html',
            template: path.join(__dirname, 'src/index.html')
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor'
        })
    ],

    resolve: {
        alias: {
            pages: path.join(__dirname, 'src/pages'),
            component: path.join(__dirname, 'src/component'),
            router: path.join(__dirname, 'src/router'),
            actions: path.join(__dirname, 'src/redux/actions'),
            reducers: path.join(__dirname, 'src/redux/reducers')
        }
    }
};

package.json增長打包腳本

"build":"webpack --config webpack.config.js"

而後執行npm run build~看看dist文件夾是否是生成了咱們發佈要用的全部文件哦?

接下來咱們仍是要優化正式版配置文件~

文件壓縮

webpack使用UglifyJSPlugin來壓縮生成的文件。

npm i --save-dev uglifyjs-webpack-plugin

webpack.config.js

const UglifyJSPlugin = require('uglifyjs-webpack-plugin')

module.exports = {
  plugins: [
    new UglifyJSPlugin()
  ]
}

npm run build發現打包文件大小減少了好多。

uglify

指定環境

許多 library 將經過與 process.env.NODE_ENV 環境變量關聯,以決定 library 中應該引用哪些內容。例如,當不處於生產環境中時,某些 library 爲了使調試變得容易,可能會添加額外的日誌記錄(log)和測試(test)。其實,當使用 process.env.NODE_ENV === 'production' 時,一些 library 可能針對具體用戶的環境進行代碼優化,從而刪除或添加一些重要代碼。咱們可使用 webpack 內置的 DefinePlugin 爲全部的依賴定義這個變量:

webpack.config.js

module.exports = {
  plugins: [
       new webpack.DefinePlugin({
          'process.env': {
              'NODE_ENV': JSON.stringify('production')
           }
       })
  ]
}

npm run build後發現vendor.[hash].js又變小了。

uglify

優化緩存

剛纔咱們把[name].[hash].js變成[name].[chunkhash].js後,npm run build後,
發現app.xxx.jsvendor.xxx.js不同了哦。

可是如今又有一個問題了。

你隨便修改代碼一處,例如Home.js,隨便改變個字,你發現home.xxx.js名字變化的同時,
vendor.xxx.js名字也變了。這不行啊。這和沒拆分不是同樣同樣了嗎?咱們本意是vendor.xxx.js
名字永久不變,一直緩存在用戶本地的。~

官方文檔推薦了一個插件HashedModuleIdsPlugin

plugins: [
        new webpack.HashedModuleIdsPlugin()
    ]

如今你打包,修改代碼再試試,是否是名字不變啦?錯了,如今打包,我發現名字仍是變了,通過比對文檔,我發現還要加一個runtime代碼抽取,

new webpack.optimize.CommonsChunkPlugin({
    name: 'runtime'
})

加上這句話就行了~爲何呢?看下解釋

注意,引入順序在這裏很重要。CommonsChunkPlugin 的 'vendor' 實例,必須在 'runtime' 實例以前引入。

public path

想象一個場景,咱們的靜態文件放在了單獨的靜態服務器上去了,那咱們打包的時候,如何讓靜態文件的連接定位到靜態服務器呢?

看文檔Public Path

webpack.config.js output 中增長一個publicPath,咱們當前用/,相對於當前路徑,若是你要改爲別的url,就改這裏就行了。

output: {
        publicPath : '/'
    }

打包優化

你如今打開dist,是否是發現好多好多文件,每次打包後的文件在這裏混合了?咱們但願每次打包前自動清理下dist文件。

npm install clean-webpack-plugin --save-dev

webpack.config.js

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


plugins: [
    new CleanWebpackPlugin(['dist'])
]

如今npm run bundle試試,是否是以前的都清空了。固然咱們以前的api文件夾也被清空了,不過不要緊哦~原本就是測試用的。

抽取css

目前咱們的css是直接打包進js裏面的,咱們但願能單獨生成css文件。

咱們使用extract-text-webpack-plugin來實現。

npm install --save-dev extract-text-webpack-plugin

webpack.config.js

const ExtractTextPlugin = require("extract-text-webpack-plugin");

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ExtractTextPlugin.extract({
          fallback: "style-loader",
          use: "css-loader"
        })
      }
    ]
  },
  plugins: [
     new ExtractTextPlugin({
         filename: '[name].[contenthash:5].css',
         allChunks: true
     })
  ]
}

npm run build後發現單獨生成了css文件哦

使用axiosmiddleware優化API請求

先安裝下axios

npm install --save axios

咱們以前項目的一次API請求是這樣寫的哦~

action建立函數是這樣的。比咱們如今寫的fetch簡單多了。

export function getUserInfo() {
    return {
        types: [GET_USER_INFO_REQUEST, GET_USER_INFO_SUCCESS, GET_USER_INFO_FAIL],
        promise: client => client.get(`http://localhost:8080/api/user.json`)
        afterSuccess:(dispatch,getState,response)=>{
            /*請求成功後執行的函數*/
        },
        otherData:otherData
    }
}

而後在dispatch(getUserInfo())後,經過redux中間件來處理請求邏輯。

中間件的教程看這裏

咱們想一想中間件的邏輯

  1. 請求前dispatch REQUEST請求。
  2. 成功後dispatch SUCCESS請求,若是定義了afterSuccess()函數,調用它。
  3. 失敗後dispatch FAIL請求。

來寫一個

cd src/redux
mkdir middleware
cd middleware
touch promiseMiddleware.js

src/redux/middleware/promiseMiddleware.js

import axios from 'axios';

export default  store => next => action => {
    const {dispatch, getState} = store;
    /*若是dispatch來的是一個function,此處不作處理,直接進入下一級*/
    if (typeof action === 'function') {
        action(dispatch, getState);
    }
    /*解析action*/
    const {
        promise,
        types,
        afterSuccess,
        ...rest
    } = action;

    /*沒有promise,證實不是想要發送ajax請求的,就直接進入下一步啦!*/
    if (!action.promise) {
        return next(action);
    }

    /*解析types*/
    const [REQUEST,
        SUCCESS,
        FAILURE] = types;

    /*開始請求的時候,發一個action*/
    next({
        ...rest,
        type: REQUEST
    });
    /*定義請求成功時的方法*/
    const onFulfilled = result => {
        next({
            ...rest,
            result,
            type: SUCCESS
        });
        if (afterSuccess) {
            afterSuccess(dispatch, getState, result);
        }
    };
    /*定義請求失敗時的方法*/
    const onRejected = error => {
        next({
            ...rest,
            error,
            type: FAILURE
        });
    };

    return promise(axios).then(onFulfilled, onRejected).catch(error => {
        console.error('MIDDLEWARE ERROR:', error);
        onRejected(error)
    })
}

修改src/redux/store.js來應用這個中間件

import {createStore, applyMiddleware} from 'redux';
import combineReducers from './reducers.js';

import promiseMiddleware from './middleware/promiseMiddleware'

let store = createStore(combineReducers, applyMiddleware(promiseMiddleware));

export default store;

修改src/redux/actions/userInfo.js

export const GET_USER_INFO_REQUEST = "userInfo/GET_USER_INFO_REQUEST";
export const GET_USER_INFO_SUCCESS = "userInfo/GET_USER_INFO_SUCCESS";
export const GET_USER_INFO_FAIL = "userInfo/GET_USER_INFO_FAIL";

export function getUserInfo() {
    return {
        types: [GET_USER_INFO_REQUEST, GET_USER_INFO_SUCCESS, GET_USER_INFO_FAIL],
        promise: client => client.get(`http://localhost:8080/api/user.json`)
    }
}

是否是簡單清新不少啦?

修改src/redux/reducers/userInfo.js

case GET_USER_INFO_SUCCESS:
            return {
                ...state,
                isLoading: false,
                userInfo: action.result.data,
                errorMsg: ''
            };

action.userInfo修改爲了action.result.data。你看中間件,請求成功,會給action增長一個result字段來存儲響應結果哦~不用手動傳了。

npm start看看咱們的網絡請求是否是正常哦。

調整文本編輯器

使用自動編譯代碼時,可能會在保存文件時遇到一些問題。某些編輯器具備「安全寫入」功能,可能會影響從新編譯。

要在一些常見的編輯器中禁用此功能,請查看如下列表:

  • Sublime Text 3 - 在用戶首選項(user preferences)中添加 atomic_save: "false"。
  • IntelliJ - 在首選項(preferences)中使用搜索,查找到 "safe write" 而且禁用它。
  • Vim - 在設置(settings)中增長 :set backupcopy=yes。
  • WebStorm - 在 Preferences > Appearance & Behavior > System Settings 中取消選中 Use "safe write"。
相關文章
相關標籤/搜索