前言
本篇主要是講一些全家桶的優化與完善,基礎功能上一篇已經講得差不多了。直接開始:
Source Maps
當javaScript拋出異常時,我們會很想知道它發生在哪個文件的哪一行。但是webpack 總是將文件輸出為一個或多個bundle,我們對錯誤的追蹤很不方便。Source maps試圖解決這一個問題,我們只需要改變一下配置項即可。
在webpack.dev.config.js中加入:
devtool:"inline-source-map"
css編譯
- 這里以less-loader為例,先安裝
- less-loader 是組件中可以引入less后綴的文件
- css-loader 是使css文件可以用@import和url(...)的方法實現require;
- 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,效果如圖
圖片編譯
先進行一個測試,打開src/Pages/UserInfo/UserInfo.js
import imgSrc from '../../../public/image/react15.png'
...
<h2>個人資料</h2>
<img src={imgSrc}/>
運行后,頁面報錯
出現這個錯誤是因為打包后的文件找不到我們之前寫好的相對路徑。對此,我們可以用如下方式解決。
首先我們要安裝兩個依賴:
- 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:盜用大冪冪的照片_)
按需加載
我們打包后,頁面統一生成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 效果如圖
緩存
按需加載文件的進階優化則是文件緩存。緩存我們要解決以下兩個問題:
- 當用戶首次訪問Home.js時,進行文件的加載,第二次訪問時再進行同樣文件的加載嗎?
- 當文件做了緩存時,我們如果有改動代碼,重新打包,我們要如何更新緩存的文件?
問題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中,刪除一些和開發環境有關的幾點:
- webpack-dev-server相關內容
- devtool的值改成 cheap-module-source-map
- 輸出文件名增加字符改為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
- 清理邏輯
- 發起請求前 dispatch REQUEST;
- 請求成功后 dispatch SUCESS,再執行callback;
- 請求失敗后 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。