webpack4.0優化那些事兒

whynotgonow發表於2019-03-04

一 縮小檔案搜尋範圍

1 include & exclude

1) action

  • 限制編譯範圍

2) useage

module: {
        rules: [
            {
                test: /\.js$/,
                use: ['babel-loader?cacheDirectory'],
                include: path.resolve(__dirname, 'src'),
                exclude: /node_modules/
            }
        ]
    }
複製程式碼

'babel-loader?cacheDirectory'

You can also speed up babel-loader by as much as 2x by using the cacheDirectory option. This will cache transformations to the filesystem.

QA

命令列warning

[BABEL] Note: The code generator has deoptimised the styling of "/Users/xxx/Documents/xxx/webpack_test/test3/node_modules/lodash/lodash.js" as it exceeds the max of "500KB".

加上exclude限制範圍就不會報錯了

2 resolve.modules

1) action

  • Tell webpack what directories should be searched when resolving modules.

2) useage

resolve: {
    modules: [path.resolve('node_modules'), path.resolve('lib')]
}
複製程式碼

3) note

  • Absolute and relative paths can both be used, but be aware that they will behave a bit differently.

  • A relative path will be scanned similarly to how Node scans for node_modules, by looking through the current directory as well as it's ancestors (i.e. ./node_modules, ../node_modules, and on).

  • With an absolute path, it will only search in the given directory.

  • If you want to add a directory to search in that takes precedence over node_modules/:(即是有先後順序的)

modules: [path.resolve(__dirname, "src"), "node_modules"]
複製程式碼

4) QA

Module not found: Error: Can't resolve 'ajax' in '/Users/xxx/Documents/xxx/webpack_test/test3/src'

當你需要指定除node_modules之外的其它模組目錄的時候可以在陣列中新增屬性

3 resolve.mainFields

1) action

  • Webpack 配置中的 resolve.mainFields 用於配置第三方模組使用哪個入口檔案。

安裝的第三方模組中都會有一個 package.json檔案,用於描述這個模組的屬性,其中有些欄位用於描述入口檔案在哪裡,resolve.mainFields 用於配置採用哪個欄位作為入口檔案的描述。

可以存在多個欄位描述入口檔案的原因是因為有些模組可以同時用在多個環境中,針對不同的執行環境需要使用不同的程式碼。 以 isomorphic-fetch API 為例,它是 Promise的一個實現,但可同時用於瀏覽器和 Node.js 環境。

2) useage

為了減少搜尋步驟,在你明確第三方模組的入口檔案描述欄位時,你可以把它設定的儘量少。 由於大多數第三方模組都採用 main欄位去描述入口檔案的位置,可以這樣配置 Webpack:

module.exports = {
  resolve: {
    // 只採用 main 欄位作為入口檔案描述欄位,以減少搜尋步驟
    mainFields: ['main'],
  },
};
複製程式碼

4 resolve.alias

1) action

  • 配置項通過別名來把原匯入路徑對映成一個新的匯入路徑
  • 此優化方法會影響使用Tree-Shaking去除無效程式碼

2) useage

alias: {
    "bootstrap": "bootstrap/dist/css/bootstrap.css"
}
複製程式碼

5 resolve.extensions

1) action

  • 在匯入語句沒帶檔案字尾時,Webpack會自動帶上字尾後去嘗試詢問檔案是否存在 -
  • 預設字尾是 extensions: ['.js', '.json']

2) useage

3) note

  • 字尾列表儘可能小
  • 頻率最高的往前方
  • 匯出語句裡儘可能帶上字尾

6 module.noParse

1) action

  • module.noParse 配置項可以讓 Webpack 忽略對部分沒采用模組化的檔案的遞迴解析處理

2) useage

module: {
    noParse: [/react\.min\.js/]
}
複製程式碼

2) note

被忽略掉的檔案裡不應該包含 import 、 require 、 define 等模組化語句

二 DLL

1 action

dll 為字尾的檔案稱為動態連結庫,在一個動態連結庫中可以包含給其他模組呼叫的函式和資料

  • 把基礎模組獨立出來打包到單獨的動態連線庫裡
  • 當需要匯入的模組在動態連線庫裡的時候,模組不能再次被打包,而是去動態連線庫裡獲取

2 usage

定義外掛(DLLPlugin) ---> 引用外掛(DllReferencePlugin)

本次例子用jquery舉例

1) 定義DLL

  • 用於打包出一個個動態連線庫
  • 需要單獨build

webpack.jquery.config.js

module.exports = {
    entry: ["jquery"],
    output: {
        filename: "vendor.js",
        path: path.resolve(__dirname, "dist"),
        libraryTarget: 'var',// 打包的方式,hou
        library: "vendor_lib_vendor"// DLL的名字
    },
    plugins: [
        new webpack.DllPlugin({
            name: "vendor_lib_vendor",// 定義DLL
            path: path.resolve(__dirname, "dist/vendor-manifest.json")
        })
    ]
};
複製程式碼

package.json 的scripts新增

"dll": "webpack --config webpack.jquery.config.js --mode development"
複製程式碼

配置好上述的檔案後,在終端執行 npm run dll,時候會在dist目錄下生成兩個檔案,分別是vendor.jsvendor-manifest.jsonvendor.js包含的就是打包後的jquery檔案程式碼,vendor-manifest.json是用來做關聯的。DLL定義好了,接下來就是應用打包好的DLL了。

2) 引用DLL

webpack.config.js 配置檔案中引入DllPlugin外掛打包好的動態連線庫

plugins: [
        new webpack.DllReferencePlugin({
            context: __dirname,
            manifest: require("./dist/vendor-manifest.json")
        })
    ],
複製程式碼

3) 使用DLL

app.html 在app.html的底部新增

<script src="./vendor.js"></script>
複製程式碼

3 note

libraryTargetlibrary

當用 Webpack 去構建一個可以被其他模組匯入使用的庫時需要用到它們。

  • output.libraryTarget 配置以何種方式匯出庫。
  • output.library 配置匯出庫的名稱。 它們通常搭配在一起使用。

output.libraryTarget 是字串的列舉型別,支援以下配置。

1) var (預設)

編寫的庫將通過 var 被賦值給通過 library 指定名稱的變數。

假如配置了 output.library='LibraryName',則輸出和使用的程式碼如下:

// Webpack 輸出的程式碼
var LibraryName = lib_code; //其中 lib_code 代指匯出庫的程式碼內容,是有返回值的一個自執行函式。

// 使用庫的方法
LibraryName.doSomething();
複製程式碼

2) commonjs

編寫的庫將通過 CommonJS 規範匯出。

假如配置了 output.library='LibraryName',則輸出和使用的程式碼如下:

// Webpack 輸出的程式碼
exports['LibraryName'] = lib_code;

// 使用庫的方法
require('library-name-in-npm')['LibraryName'].doSomething();
// 其中 library-name-in-npm 是指模組釋出到 Npm 程式碼倉庫時的名稱。
複製程式碼

3) commonjs2

編寫的庫將通過 CommonJS2 規範匯出,輸出和使用的程式碼如下:

// Webpack 輸出的程式碼
module.exports = lib_code;

// 使用庫的方法
require('library-name-in-npm').doSomething();
複製程式碼

CommonJS2 和 CommonJS 規範很相似,差別在於 CommonJS 只能用 exports 匯出,而 CommonJS2 在 CommonJS 的基礎上增加了 module.exports 的匯出方式。 在 output.libraryTarget 為 commonjs2 時,配置 output.library 將沒有意義。

4) this

編寫的庫將通過 this 被賦值給通過 library 指定的名稱,輸出和使用的程式碼如下:

// Webpack 輸出的程式碼
this['LibraryName'] = lib_code;

// 使用庫的方法
this.LibraryName.doSomething();
複製程式碼

5) window

編寫的庫將通過 window 被賦值給通過 library 指定的名稱,即把庫掛載到 window 上,輸出和使用的程式碼如下:

// Webpack 輸出的程式碼
window['LibraryName'] = lib_code;

// 使用庫的方法
window.LibraryName.doSomething();
複製程式碼

6) global

編寫的庫將通過 global 被賦值給通過 library 指定的名稱,即把庫掛載到 global 上,輸出和使用的程式碼如下:

// Webpack 輸出的程式碼
global['LibraryName'] = lib_code;

// 使用庫的方法
global.LibraryName.doSomething();
複製程式碼

三 HappyPack

1 action

HappyPack就能讓Webpack把任務分解給多個子程式去併發的執行,子程式處理完後再把結果傳送給主程式。

2 usage

install

由於webpack 4.0 剛剛釋出,響應的外掛還沒有更新完,不過可以在後面加一個@next來安裝即將釋出的版本

npm i happypack@next -D
複製程式碼

webpack.config.js

module: {
        rules: [
            {
                test: /\.css$/,
                use: 'happypack/loader?id=css',
                //把對.js檔案的處理轉交給id為babel的HappyPack例項
                //用唯一的識別符號id來代表當前的HappyPack是用來處理一類特定檔案
                include: path.resolve('./src'),
                exclude: /node_modules/
            },
            {
                test: /\.js/,
                use: 'happypack/loader?id=babel',
                include: path.resolve('./src'),
                exclude: /node_modules/
            }
        ]
    },
    plugins: [
        new HtmlWebPackPlugin({
            template: './src/index.html'
        }),
        new HappyPack({
            id: 'babel',
            loaders: ['babel-loader']// 和rules裡的配置相同
        }),
        new HappyPack({
            id: 'css',
            loaders: ['style-loader', 'css-loader']// 和rules裡的配置相同
        }),
    ]
複製程式碼

四 ParallelUglifyPlugin

1.action

  • 可以把對JS檔案的序列壓縮變為開啟多個子程式並行執行

2 usage

insatll

npm install webpack-parallel-uglify-plugin -D
複製程式碼

webpackage.config.js

new ParallelUglifyPlugin({
            workerCount: os.cpus().length - 1,//開啟幾個子程式去併發的執行壓縮。預設是當前執行電腦的 CPU 核數減去1
            uglifyJS: {
                output: {
                    beautify: false, //不需要格式化
                    comments: true, //不保留註釋
                },
                compress: {
                    warnings: false, // 在UglifyJs刪除沒有用到的程式碼時不輸出警告
                    drop_console: true, // 刪除所有的 `console` 語句,可以相容ie瀏覽器
                    collapse_vars: true, // 內嵌定義了但是隻用到一次的變數
                    reduce_vars: true, // 提取出出現多次但是沒有定義成變數去引用的靜態值
                }
            }
        })
複製程式碼

五 伺服器自動重新整理

1 檔案監聽

1) action

  • 可以監聽檔案變化,當檔案發生變化的時候重新編譯

2) useage

watch: true, 
watchOptions: {
    ignored: /node_modules/,
    aggregateTimeout: 300, 
    poll: 1 
}
複製程式碼

3) note

watch

只有在開啟監聽模式時(watch為true),watchOptions才有意義

aggregateTimeout

監聽到變化發生後等300(ms)再去執行動作,防止檔案更新太快導致編譯頻率太高

poll

通過不停的詢問檔案是否改變來判斷檔案是否發生變化,預設每秒詢問1000次

檔案監聽流程

webpack定時獲取檔案的更新時間,並跟上次儲存的時間進行比對,不一致就表示發生了變化,poll就用來配置每秒問多少次。

當檢測檔案不再發生變化,會先快取起來,等等待一段時間後之後再通知監聽者,這個等待時間通過aggregateTimeout配置。

webpack只會監聽entry依賴的檔案 我們需要儘可能減少需要監聽的檔案數量和檢查頻率,當然頻率的降低會導致靈敏度下降。

2 自動重新整理瀏覽器

1) use

devServer: {
    inline: true
},
複製程式碼

2) note

webpack負責監聽檔案變化,webpack-dev-server負責重新整理瀏覽器。這些檔案會被打包到chunk中,它們會代理客戶端向伺服器發起WebSocket連線

3 模組熱替換

1) action

  • 模組熱替換(Hot Module Replacement)的技術可在不重新整理整個網頁的情況下只更新指定的模組.
  • 原理是當一個原始碼發生變化時,只重新編譯發生變化的模組,再用新輸出的模組替換掉瀏覽器中對應的老模組

2) 優點

  • 反應更快,時間更短
  • 不重新整理網頁可以保留網頁執行狀態
  • 監聽更少的檔案
  • 忽略掉 node_modules 目錄下的檔案

2) use

webpack.config.js

devServer: {
   hot:true//將hot設定為true
},

// 需要的外掛
plugins: [
    new webpack.NamedModulesPlugin(),//顯示模組的相對路徑
    new webpack.HotModuleReplacementPlugin()// 啟動熱載入功能
]
複製程式碼

code

if (module.hot) {
    module.hot.accept('./hot.js', () => {
        let hot = require('./hot');
        document.getElementById('app2').innerHTML = hot + '1';
    })
}
複製程式碼

3 note

需要熱載入的模組需要在初始化的時候引入到模組中,否則不會觸發HMR。

六 區分環境

1 action

在開發網頁的時候,一般都會有多套執行環境,例如,在開發過程中方便開發除錯的環境。釋出到線上給使用者使用的執行環境。

線上的環境和開發環境區別主要有以下不同:

  • 線上的程式碼被壓縮
  • 開發環境可能會列印只有開發者才能看到的日誌
  • 開發環境和線上環境後端資料介面可能不同

2 usage

package.json

  • cross-env跨平臺設定環境變數(後面沒有&&)
"scripts": {
    "build-dev": "cross-env NODE_ENV=development webpack --mode development",
    "build-prod": "cross-env NODE_ENV=production webpack --mode production"
}
複製程式碼

webpack.config.js

  • 根據環境變數區分生產環境還是開發環境,然後和webpack.base.config.js合併,生產環境(或者開發環境)的優先順序高於webpack.base.config.js的配置。
let merge = require('webpack-merge');
let base = require('./webpack.base.config');
let other = null;
if (process.env.NODE_ENV === 'development') {
    other = require('./webpack.dev.config');
} else {
    other = require('./webapack.prod.config');
}
module.exports = merge(base, other);
複製程式碼

webpack.base.config.js

  • 基本配置
  • webpack.DefinePlugin 定義環境變數
基本配置...
plugins: [
    new webpack.DefinePlugin({
        __isDevelopment__: JSON.stringify(process.env.NODE_ENV == 'development')
    })
]
複製程式碼

webpack.dev.config.js

  • output舉例,如果開發和生產環境的引數不同,就會覆蓋webpack.base.config.js裡面的配置
const path = require('path');
module.exports = {
    output: {
        path: path.resolve('./dist'),
        filename: "[name].dev.[hash:2].js"
    }
};
複製程式碼

webpack.prod.config.js

  • (以output舉例)
const path = require('path');
module.exports = {
    output: {
        path: path.resolve('./dist'),
        filename: "[name].prod.[hash:8].js"
    }
};
複製程式碼

base.js

  • 配置檔案中的webpack.DefinePlugin定義的變數(__isDevelopment__),在入口檔案和入口檔案引用的其他檔案中都可以獲取到__isDevelopment__的值
let env = null;
if (__isDevelopment__) {
    env = 'dev';
} else {
    env = 'prod';
}
module.exports = env;
複製程式碼

index.js

let env = require('./base.js');
if (__isDevelopment__) {
    console.log('dev');
} else {
    console.log('prod');
}
console.log('env', env);

/*
prod
env prod
*/
複製程式碼

3 note

webpack.DefinePlugin

定義環境變數的值時用 JSON.stringify 包裹字串的原因是環境變數的值需要是一個由雙引號包裹的字串,而 JSON.stringify('production')的值正好等於'"production"'

七 CDN

CDN 又叫內容分發網路,通過把資源部署到世界各地,使用者在訪問時按照就近原則從離使用者最近的伺服器獲取資源,從而加速資源的獲取速度。

  • HTML檔案不快取,放在自己的伺服器上,關閉自己伺服器的快取,靜態資源的URL變成指向CDN伺服器的地址
  • 靜態的JavaScript、CSS、圖片等檔案開啟CDN和快取,並且檔名帶上HASH值
  • 為了並行載入不阻塞,把不同的靜態資源分配到不同的CDN伺服器上

八 Tree Shaking

1 action

tree Shaking 可以用來剔除JavaScript中用不上的死程式碼。

2 useage

  • 它依賴靜態的ES6模組化語法,例如通過import和export匯入匯出
  • 不要編譯ES6模組
use: {
    loader: 'babel-loader',
    query: {
        presets: [
            [
                "env", {
                    modules: false //含義是關閉 Babel 的模組轉換功能,保留原本的 ES6 模組化語法
                }
            ],
            "react"
        ]
    }
},
複製程式碼

3 note

需要注意的是它依賴靜態的ES6模組化語法,例如通過import和export匯入匯出。也就是說如果專案程式碼執行在不支援es6語法的環境上,Tree Shaking也就沒有意義了。

九 提取公共程式碼

1 為什麼需要提取公共程式碼

大網站有多個頁面,每個頁面由於採用相同技術棧和樣式程式碼,會包含很多公共程式碼,如果都包含進來會有問題

相同的資源被重複的載入,浪費使用者的流量和伺服器的成本; 每個頁面需要載入的資源太大,導致網頁首屏載入緩慢,影響使用者體驗。 如果能把公共程式碼抽離成單獨檔案進行載入能進行優化,可以減少網路傳輸流量,降低伺服器成本

2 如何提取

1) 分類

不同型別的檔案,打包後的程式碼塊也不同:

  • 基礎類庫,方便長期快取
  • 頁面之間的公用程式碼
  • 各個頁面單獨生成檔案

2) usage

webpack.config.js

 optimization: {
        splitChunks: {
            cacheGroups: {
                commons: {// 頁面之間的公用程式碼
                    chunks: 'initial',
                    minChunks: 2,
                    maxInitialRequests: 5, // The default limit is too small to showcase the effect
                    minSize: 0 // This is example is too small to create commons chunks
                },
                vendor: {// 基礎類庫
                    chunks: 'initial',
                    test: /node_modules/,
                    name: "vendor",
                    priority: 10,
                    enforce: true
                }
            }
        }
    },
複製程式碼

./src/pageA.js

require('./utils/utility1.js');
require('./utils/utility2.js');
require('react');
複製程式碼

./src/pageB.js

require('./utils/utility2.js');
require('./utils/utility3.js');
複製程式碼

./src/pageC.js

require('./utils/utility2.js');
require('./utils/utility3.js');
複製程式碼

utils/utility1.js

module.exports = 1;
複製程式碼

utils/utility2.js

module.exports = 2;
複製程式碼

utils/utility3.js

module.exports = 3;
複製程式碼

打包後的結果

上述三種程式碼的生成的結果,如下圖:

提取公共程式碼

十 Scope Hoisting

1 action

Scope Hoisting 可以讓 Webpack 打包出來的程式碼檔案更小、執行的更快, 它又譯作 "作用域提升",是在 Webpack3 中新推出的功能。

  • 程式碼體積更小,因為函式申明語句會產生大量程式碼
  • 程式碼在執行時因為建立的函式作用域更少了,記憶體開銷也隨之變小 hello.js

2 useage

package.json

 "build": "webpack  --display-optimization-bailout --mode development",
複製程式碼

webpack.config.js

plugins: [
    new ModuleConcatenationPlugin()
    ],
複製程式碼

./h.js

export default 'scope hoist'
複製程式碼

./index.js

import str from './h.js'
console.log(str);
複製程式碼

3 note

必須使用ES6語法,否則不起作用(--display-optimization-bailout 引數會提示)

十一 程式碼分離

程式碼分離是 webpack 中最引人注目的特性之一。此特效能夠把程式碼分離到不同的 bundle 中,然後可以按需載入或並行載入這些檔案。 有三種常用的程式碼分離方法:

  • 入口起點:使用 entry 配置手動地分離程式碼。
  • 防止重複:使用 splitChunks 去重和分離 chunk。
  • 動態匯入:通過模組的行內函數呼叫來分離程式碼。

入口起點和防止重複上面已經提到了,下面我們重點講一下動態匯入

1 action

使用者當前需要用什麼功能就只載入這個功能對應的程式碼,也就是所謂的按需載入 在給單頁應用做按需載入優化時,一般採用以下原則:

  • 對網站功能進行劃分,每一類一個chunk
  • 對於首次開啟頁面需要的功能直接載入,儘快展示給使用者
  • 某些依賴大量程式碼的功能點可以按需載入
  • 被分割出去的程式碼需要一個按需載入的時機

2 usage

  • 使用import(module)的語法
  • import 非同步 載入 模組是一個es7的語法
  • 在webpack裡import是一個天然的分割點
document.getElementById('play').addEventListener('click',function(){
    import('./vedio.js').then(function(video){
        let name = video.getName();
        console.log(name);
    });
});
複製程式碼

參考文件

相關文章