天天看點

手把手教你全家桶之React(三)--完結篇

前言

本篇主要是講一些全家桶的優化與完善,基礎功能上一篇已經講得差不多了。直接開始:

Source Maps

當javaScript抛出異常時,我們會很想知道它發生在哪個檔案的哪一行。但是webpack 總是将檔案輸出為一個或多個bundle,我們對錯誤的追蹤很不友善。Source maps試圖解決這一個問題,我們隻需要改變一下配置項即可。

在webpack.dev.config.js中加入:

devtool:"inline-source-map"           

複制

css編譯

  • 這裡以less-loader為例,先安裝
  1. less-loader 是元件中可以引入less字尾的檔案
  2. css-loader 是使css檔案可以用@import和url(...)的方法實作require;
  3. style-loader 使計算後的樣式加入到頁面中。

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

  • 配置webpack.dev.config.js檔案

    module:{ rules:[ { test:/\.js$/, use:['babel-loader?cacheDirectory=true'], include:path.join(__dirname,'src') },{ test:/\.less$/, use:[ 'style-loader', {loader:'css-loader',options:{importLoaders:1}}, 'less-loader' ] } ] },

    測試下

    cd src/pages/Home touch Home.less

    打開 Home.less

    .wrap{ width:300px; height:300px; background:red; & .content{ width:200px; height:200px; margin:auto; background:yellow; } }

    在Home.js中引入,并添加class

    import './Home.less' ... render(){ return( <div> <h1>目前共點選次數為:{this.state.count}</h1> <button onClick={()=> this._test()}>點選我!</button> <div className="wrap"> <div className="content"></div> </div> </div> ) }

    因為添加了新的依賴,我們重新跑一次npm run start,效果如圖

手把手教你全家桶之React(三)--完結篇

圖檔編譯

先進行一個測試,打開src/Pages/UserInfo/UserInfo.js

import imgSrc from '../../../public/image/react15.png'
    ...
    <h2>個人資料</h2>
    <img src={imgSrc}/>           

複制

運作後,頁面報錯

手把手教你全家桶之React(三)--完結篇

出現這個錯誤是因為打包後的檔案找不到我們之前寫好的相對路徑。對此,我們可以用如下方式解決。

首先我們要安裝兩個依賴:

  • file-loader 當我們寫樣式比如背景圖檔,我們的路徑是相對于目前檔案的,但webpack最終會打包成一個檔案。打包後的相對路徑會找不到對應檔案。這時,file-loader可以幫我們找到正确的檔案路徑。
  • url-loader 如果圖檔過多,會增加過多的http請求,url-loader提示圖檔base64編碼服務,設定limit參數,小于設定值的圖檔會被轉為一串字元,隻需将字元打包到檔案中,就能通路圖檔了。

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

    在webpack.dev.config.js增加配置

    module:{ rules:[ ... { test:/\.(png|jpg|gif)$/, use:[{ loader:'url-loader', options:{ // 設定為小于8K的大小 limit:8192 } }] } ] }

    配置成功後,我們重新運作npm run start(因為新加了依賴要重新跑一次服務),看下效果(PS:盜用大幂幂的照片^_^)

手把手教你全家桶之React(三)--完結篇

按需加載

我們打包後,頁面統一生成bundle.js,當我們進入Home頁面時,因為加載的檔案過多會導緻頁面慢。我們想要達到跳轉到對應頁面時按需加載檔案的效果,就需要用到bundle-loader。

  • 安裝

    npm install bundle-loader --save

  • 在router下建立Bundle.js

    cd src/router touch Bundle.js

    打開Bundle.js,根據示例

import React,{Component} from 'react'
class Bundle extends Component{
    state={
        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({
                mod:mod.default ? mod.default : mod
            })
        })
    }
    render(){
        return this.props.children(this.state.mod)
    }
}
export default Bundle;           

複制

  • 路由配置改造,src/router/router.js
import React from 'react';
import {BrowserRouter as Router,Route,Switch,Link} from 'react-router-dom';

import Home from 'bundle-loader?lazy&name=home!pages/Home/Home';
import About from 'bundle-loader?lazy&name=page1!pages/About/About';
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) => (props) => (
    <Bundle load={component}>
        {
            (Componet) => Component ? <Component {...props} /> : <Loading/>
        }
    </Bundle>
);
const getRouter=()=>(
    <Router>
        <div>
            <ul>
                <li><Link to="/">Home</Link></li>
                <li><Link to="/about">About</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="/about" component={createComponent(About)}/>
                <Route path="/counter" component={createComponent(Counter)}/>
                <Route path="/userinfo" component={createComponent(UserInfo)}/>
            </Switch>
        </div>
    </Router>

);
export default getRouter;           

複制

  • 修改webpack.dev.config.js配置,使打包輸出的檔案名對應
output:{
    path:path.join(__dirname,'./dist'),
    filename:'bundle.js',
    chunkFilename:'[name].js'
}           

複制

運作npm run start 效果如圖

手把手教你全家桶之React(三)--完結篇

緩存

按需加載檔案的進階優化則是檔案緩存。緩存我們要解決以下兩個問題:

  1. 當使用者首次通路Home.js時,進行檔案的加載,第二次通路時再進行同樣檔案的加載嗎?
  2. 當檔案做了緩存時,我們如果有改動代碼,重新打包,我們要如何更新緩存的檔案?

    問題1在浏覽器中已經對靜态資源檔案做了緩存,我們主要解決問題二。

    日常開發中,我們是通過打包修改檔案名(比如加hash),使用戶端能識别新的檔案,重新加載。

    打開webpack.dev.config.js

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

    我們可以看到編譯後的檔案名已經變了

手把手教你全家桶之React(三)--完結篇

由于我們在dist/index.html中引用的還是bundle.js,是以我們要改成每次編譯後自動插入到index.html中,可以用到HtmlWebpackPlugin。

  • 安裝

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

  • 建立入口模闆檔案index.html

    cd src touch index.html

  • 打開index.html

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <div id="app"></div> </body> </html>

  • 修改webpack.dev.config.js配置檔案

    var HtmlWebpackPlugin=require('html-webpack-plugin'); ... plugins:[new HtmlWebpackPlugin({ filename:'index.html', template:path.join(__dirname,'src/index.html') })],

    此時删掉之前的dist/index.html,運作npm run start通路正常。

    公共代碼提取

    我們打包生成的檔案js檔案中,都包含了react,redux,react-router這樣的代碼。然而這些依賴代碼我們在很多檔案都引用了,而不需要它自動更新。是以我們可以把這些公共代碼提取出來。

    我們根據教程配置。

  • 打開webpack.dev.config.js

    var webpack=require('webpack'); module.exports={ 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(三)--完結篇

可以發現app.[hash].js和vendor.[hash].js生成的hash是一樣的。也就意味着如果代碼有改動app.[hash].js與vendor.[hash].js都會同時改變。然後vendor裡的内容我們不希望它更新。根據文檔,我要在webpack裡還要配置

手把手教你全家桶之React(三)--完結篇

應用到我們項目應該

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

複制

再次運作,發現報錯,webpack-dev-server --hot 不相容chunkhash

手把手教你全家桶之React(三)--完結篇

解決這個問題,我們要先區分生産環境與開發環境的差別。是以,上面的問題先留一下,我們先來建構生産環境的配置。

生産環境建構

生産環境與開發環境的差別往往展現在目标差異大。開發環境我們要配置的東西很多,要求實時加裁,熱更新子產品等。但生産環境要求較小,更關注小的bundle,更輕量的Source map,更高效的加載時間等。

  • 首先建立配置檔案

    touch webpack.config.js

  • 将之前webpack.dev.config.js的内容複制到webpack.config.js中,删除一些和開發環境有關的幾點:
  1. webpack-dev-server相關内容
  2. devtool的值改成 cheap-module-source-map
  3. 輸出檔案名增加字元改為chunkhash,原本的webpack.dev.config.js改回為hash

    根據以上幾點,webpack.config.js内容如下:

    var path=require('path'); var HtmlWebpackPlugin=require('html-webpack-plugin'); var webpack=require('webpack'); module.exports={ // 入口檔案指向src/index.js entry:{ app:[ 'react-hot-loader/patch', path.join(__dirname,'src/index.js') ], vendor:['react','react-router-dom','redux','react-dom','react-redux'] }, //打包後的檔案到目前目錄下的dist檔案夾,名為bundle.js output:{ path:path.join(__dirname,'./dist'), filename:'[name].[chunkhash].js', chunkFilename:'[name].[chunkhash].js' }, module:{ rules:[ { test:/\.js$/, use:['babel-loader?cacheDirectory=true'], include:path.join(__dirname,'src') },{ test:/\.less$/, use:[ 'style-loader', {loader:'css-loader',options:{importLoaders:1}}, { loader: 'less-loader', options: { strictMath: true, noIeCompat: true } } ] }, { 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' }) ], devtool:"cheap-module-source-map", resolve:{ alias:{ pages:path.join(__dirname,'src/pages'), components:path.join(__dirname,'src/components'), router:path.join(__dirname,'src/router'), actions:path.join(__dirname,'src/redux/actions'), reducers:path.join(__dirname,'src/redux/reducers'), // redux:path.join(__dirname,'src/redux') 與子產品重名 } } };

  • 在package.json中增加build打包指令,指定配置檔案。

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

    運作一次打包指令 npm run build,檔案名支援了chunkhash.

手把手教你全家桶之React(三)--完結篇

雖然檔案名不同了,但是改變代碼重新打包會發現app.[hash].js和vendor.[chunkhash].js一樣都更新了名字,這不就和沒拆分是一樣的嗎?

别着急,看官網介紹

手把手教你全家桶之React(三)--完結篇

注意mainfest與vendor的順序不能錯哦

  • 打開webpack.config.js

js plugins:[ new HtmlWebpackPlugin({ filename:'index.html', template:path.join(__dirname,'src/index.html') }), new webpack.HashedModuleIdsPlugin(), new webpack.optimize.CommonsChunkPlugin({ name:'vendor' }), new webpack.optimize.CommonsChunkPlugin({ name:'mainfest' }) ]

當我們建構了基礎的生産環境配置後,我們可以增加指定環境配置,根據process.env.NODE_ENV環境變量關聯,讓library中應該引用哪些内容。例如,當不處于生産環境中時,library可能會添加額外的日志log和test。當使用 process.env.NODE_ENV === 'production' 時,一些 library 可能針對具體使用者的環境進行代碼優化,進而删除或添加一些重要代碼。

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

複制

打包優化

檔案壓縮

webpack使用UglifyJSPlugin來壓縮打包後生成的檔案。

  • 安裝

    npm install uglifyjs-webpack-plugin --save-dev

  • 打開webpack.config.js進行配置
const UglifyJSPlugin=require('uglifyjs-webpack-plugin')
module.exports={
    plugins:[
        ...
        new UglifyJSPlugin()
    ]
}           

複制

運作npm run build有沒有發現打包的檔案小了好多

手把手教你全家桶之React(三)--完結篇

清理dist檔案

每次打包dist都會多好多檔案混合在裡面,我們應該清掉之前打包的檔案,隻留下目前打包後的檔案。我們用到clean-webpack-plugin

  • 安裝

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

  • 打開webpack.config.js來配置
const CleanWebpackPlugin=require('clean-webpack-plugin');
...
plugins:[
    new CleanWebpackPlugin(['dist'])
]           

複制

現在試試打包一下,每次是不是都是直接覆寫整個檔案。雖然api檔案也被清掉了,但是沒關系,那隻是用來測試的。

靜态檔案的基本路徑

當我們打包後,靜态檔案沒辦法定位到靜态伺服器,我們需要在webpack.config.js中配置

output:{
    ...
    publicPath:'/'
}           

複制

css打包分離

如果我要要将打包到js的css内容抽出來單獨成css檔案,我們可以使用extract-text-webpack-plugin.

  • 安裝

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

  • 打開webpack.config.js進行配置
const ExtractTextPlugin=require("extract-text-webpack-plugin");
module.exports={
    module:{
        rules:[
            ...
            {
                test:/\.(css|less)$/,
                use:ExtractTextPlugin.extract({
                    fallback:"style-loader",
                    use:"css-loader"
                })
            }
        ]
    },
    plugins:[
        ...
        new ExtractTextPlugin({
            filename:'[name].[contenthash:5].css',
            allChunks:true
        })
    ]
}           

複制

我們可以增加一些css檔案引用,來測試下。由于我們之前的示例是用less來寫的樣式,那麼我們加上less的配置,使之生成獨立檔案。

修改剛剛的配置項:

module.exports={
    module:{
        rules:[
            ...
            {
                test:/\.(css|less)$/,
                use:ExtractTextPlugin.extract({
                    fallback:"style-loader",
                    use:["css-loader","less-loader"]
                })
            }
        ]
    },
}           

複制

重新打包,就能看到被生成的css檔案啦

手把手教你全家桶之React(三)--完結篇

axios

  • 安裝axios

    npm install --save axios

  • 然後簡化之前寫的userInfo的action,修改redux/actions/userInfo.js
export const GET_USERINFO_REQUEST="userInfo/GET_USERINFO_REQUEST";
export const GET_USERINFO_SUCCESS="userInfo/GET_USERINFO_SUCCESS";
export const GET_USERINFO_FAIL="userInfo/GET_USERINFO_FAIL";

export function getUserInfo(){
    return{
        types:[GET_USERINFO_REQUEST,GET_USERINFO_SUCCESS,GET_USERINFO_FAIL],
        promise:client => client.get('/api/userInfo.json')     
    }
}           

複制

其中dispath(getUserInfo())後,是通過redux的中間件來處理的。為了弄清楚,我們自己來寫一個。

自定義Middleware

  • 清理邏輯
  1. 發起請求前 dispatch REQUEST;
  2. 請求成功後 dispatch SUCESS,再執行callback;
  3. 請求失敗後 dispatch FAIL。
  • 建立基本檔案

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

  • 定義promiseMiddleware.js的内容
import axios from 'axios';
export default store => next =>action =>{
    const {dispatch,getState}=store;
    // 如果dispatch傳來的是一個function,則跳過
    if(typeof action === 'function'){
        action(dispatch,getState);
        return ;
    }
    // 解析action
    const {
        promise,
        types,
        afterSuccess,
        ...rest
    }=action;
    // 如果不是異步請求則直接跳轉下一步
    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 thunkMiddleware from 'redux-thunk';
// let store = createStore(combineReducers,applyMiddleware(thunkMiddleware));

import promiseMiddleware from './middleware/promiseMiddleware';
let store = createStore(combineReducers,applyMiddleware(promiseMiddleware));

export default store;           

複制

  • 最後修改src/redux/reducers/userInfo.js

    因為是當action請求成功,我們在中間件會自動加上一個result字段來存結果。

export default function reducer(state=initState,action){
    switch(action.type){
        ...
        case GET_USERINFO_SUCCESS: 
            return{
                ...state,
                isLoading:false,
                userInfo:action.result.data,
                errMsg:''
            }
    }
}           

複制

我們重新開機npm run start ,通路userInfo接口是不是成功啦!

好啦,先寫到這吧,如果還有細節完善會在源碼上更新。源碼位址,歡迎star和issues。