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

前言

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

Source Maps

當javaScript拋出異常時,咱們會很想知道它發生在哪一個文件的哪一行。可是webpack 老是將文件輸出爲一個或多個bundle,咱們對錯誤的追蹤很不方便。Source maps試圖解決這一個問題,咱們只須要改變一下配置項便可。
在webpack.dev.config.js中加入:html

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'
                ]
            }
        ]
    },

測試下java

cd src/pages/Home
touch Home.less

打開 Home.lessreact

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

在Home.js中引入,並添加classwebpack

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,效果如圖
ios

圖片編譯

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

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

運行後,頁面報錯
github

出現這個錯誤是由於打包後的文件找不到咱們以前寫好的相對路徑。對此,咱們能夠用以下方式解決。
首先咱們要安裝兩個依賴:web

  • file-loader 當咱們寫樣式好比背景圖片,咱們的路徑是相對於當前文件的,但webpack最終會打包成一個文件。打包後的相對路徑會找不到對應文件。這時,file-loader能夠幫咱們找到正確的文件路徑。
  • url-loader 若是圖片過多,會增長過多的http請求,url-loader提示圖片base64編碼服務,設定limit參數,小於設置值的圖片會被轉爲一串字符,只需將字符打包到文件中,就能訪問圖片了。
npm install --save-dev url-loader file-loader

在webpack.dev.config.js增長配置shell

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

配置成功後,咱們從新運行npm run start(由於新加了依賴要從新跑一次服務),看下效果(PS:盜用大冪冪的照片^_^)

按需加載

咱們打包後,頁面統一輩子成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 效果如圖

緩存

按需加載文件的進階優化則是文件緩存。緩存咱們要解決如下兩個問題:

  1. 當用戶首次訪問Home.js時,進行文件的加載,第二次訪問時再進行一樣文件的加載嗎?
  2. 當文件作了緩存時,咱們若是有改動代碼,從新打包,咱們要如何更新緩存的文件?
    問題1在瀏覽器中已經對靜態資源文件作了緩存,咱們主要解決問題二。
    平常開發中,咱們是經過打包修改文件名(好比加hash),使客戶端能識別新的文件,從新加載。
    打開webpack.dev.config.js
output:{
    path:path.join(__dirname,'./dist'),
    filename:'[name].[hash].js',
    chunkFilename:'[name].[chunkhash].js'
}

咱們能夠看到編譯後的文件名已經變了

因爲咱們在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'
        })
    ]
}

從新運行,打包文件以下

能夠發現app.[hash].js和vendor.[hash].js生成的hash是同樣的。也就意味着若是代碼有改動app.[hash].js與vendor.[hash].js都會同時改變。而後vendor裏的內容咱們不但願它更新。根據文檔,我要在webpack裏還要配置

應用到咱們項目應該

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

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

解決這個問題,咱們要先區分生產環境與開發環境的區別。因此,上面的問題先留一下,咱們先來構建生產環境的配置。

生產環境構建

生產環境與開發環境的區別每每體如今目標差別大。開發環境咱們要配置的東西不少,要求實時加裁,熱更新模塊等。但生產環境要求較小,更關注小的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.

雖然文件名不一樣了,可是改變代碼從新打包會發現app.[hash].js和vendor.[chunkhash].js同樣都更新了名字,這不就和沒拆分是同樣的嗎?
彆着急,看官網介紹

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

  • 打開webpack.config.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有沒有發現打包的文件小了好多

清理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文件啦

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。

相關文章
相關標籤/搜索