1. 什么是tree-shaking?
在
webpack
中,tree-shaking
的作用是可以剔除js
中用不上的代碼宦棺,但是它依賴的是靜態(tài)的ES6
的模塊語(yǔ)法瓣距。
也就是說(shuō)沒(méi)有被引用到的模塊它是不會(huì)被打包進(jìn)來(lái)的,可以減少我們的包的大小代咸,減少文件的加載時(shí)間蹈丸,提高用戶體驗(yàn)。
webpack2版本中就開(kāi)始引入了 tree shaking的概念,它可以在打包時(shí)可以忽略哪些沒(méi)有被使用到的代碼逻杖。
注意:要讓 Tree Shaking 正常工作的前提是:提交給webpack的javascript代碼必須采用了 ES6的模塊化語(yǔ)法慨默,因?yàn)镋S6模塊化語(yǔ)法是靜態(tài)的(在導(dǎo)入,導(dǎo)出語(yǔ)句中的路徑必須是靜態(tài)的字符串)弧腥。
2. 在webpack中如何使用 tree-shaking 呢厦取?
在配置代碼前,我們來(lái)看看我們項(xiàng)目中的目錄結(jié)構(gòu)如下:
### 目錄結(jié)構(gòu)如下:
demo1 # 工程名
| |--- dist # 打包后生成的目錄文件
| |--- node_modules # 所有的依賴包
| |--- js # 存放所有js文件
| | |-- demo1.js
| | |-- main.js # js入口文件
| |--- common # js公用的文件
| | |-- util.js # 公用的util.js文件
| |--- webpack.config.js # webpack配置文件
| |--- index.html # html文件
| |--- styles # 存放所有的css樣式文件
| | |-- main.styl # main.styl文件
| | |-- index.styl
| |--- .gitignore
| |--- README.md
| |--- package.json
| |--- .babelrc # babel轉(zhuǎn)碼文件
webpack.config.js 代碼如下:
const path = require('path');
// 引入 mini-css-extract-plugin 插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
// 清除dist目錄下的文件
const ClearWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');
// 引入打包html文件
const HtmlWebpackPlugin = require('html-webpack-plugin');
// 引入HappyPack插件
const HappyPack = require('happypack');
module.exports = {
// 入口文件
entry: {
main: './js/main.js'
},
output: {
filename: '[name].[contenthash].js',
// 將輸出的文件都放在dist目錄下
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
// 使用正則去匹配
test: /\.styl$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {}
},
{
loader: 'postcss-loader',
options: {
ident: 'postcss',
plugins: [
require('postcss-cssnext')(),
require('cssnano')(),
require('postcss-pxtorem')({
rootValue: 16,
unitPrecision: 5,
propWhiteList: []
}),
require('postcss-sprites')()
]
}
},
{
loader: 'stylus-loader',
options: {}
}
]
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'happypack/loader?id=css-pack'
]
},
{
test: /\.(png|jpg)$/,
use: ['happypack/loader?id=image']
},
{
test: /\.js$/,
// 將對(duì).js文件的處理轉(zhuǎn)交給id為babel的HappyPack的實(shí)列
use: ['happypack/loader?id=babel'],
// loader: 'babel-loader',
exclude: path.resolve(__dirname, 'node_modules') // 排除文件
}
]
},
resolve: {
extensions: ['*', '.js', '.json']
},
devtool: 'cheap-module-eval-source-map',
devServer: {
port: 8081,
host: '0.0.0.0',
headers: {
'X-foo': '112233'
},
inline: true,
overlay: true,
stats: 'errors-only'
},
mode: 'development',
plugins: [
new HtmlWebpackPlugin({
template: './index.html' // 模版文件
}),
new ClearWebpackPlugin(['dist']),
new MiniCssExtractPlugin({
filename: '[name].[contenthash:8].css'
}),
/**** 使用HappyPack實(shí)例化 *****/
new HappyPack({
// 用唯一的標(biāo)識(shí)符id來(lái)代表當(dāng)前的HappyPack 處理一類(lèi)特定的文件
id: 'babel',
// 如何處理.js文件管搪,用法和Loader配置是一樣的
loaders: ['babel-loader']
}),
new HappyPack({
id: 'image',
loaders: [{
loader: require.resolve('url-loader'),
options: {
limit: 10000,
name: '[name].[ext]'
}
}]
}),
// 處理styl文件
new HappyPack({
id: 'css-pack',
loaders: ['css-loader']
})
]
};
common/util.js 代碼如下:
export function a() {
alert('aaaa');
}
export function b() {
alert('bbbbb');
}
export function c() {
alert('cccc');
}
common/util.js 代碼如下:
export function a() {
alert('aaaa');
}
export function b() {
alert('bbbbb');
}
export function c() {
alert('cccc');
}
js/main.js 代碼如下:
import { a } from '../common/util';
a();
執(zhí)行 webpack后虾攻,打包文件如下:
然后繼續(xù)查看 dist/main.xxx.js代碼如下:
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return a; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "b", function() { return b; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "c", function() { return c; });
function a() {
alert('aaaa');
}
function b() {
alert('bbbbb');
}
function c() {
alert('cccc');
}
/***/ }),
如上代碼,還是會(huì)包含 b,c 兩個(gè)函數(shù)代碼進(jìn)來(lái)更鲁,那是因?yàn)?webpack 想要使用tree-shaking功能的話霎箍,我們需要壓縮代碼,就能把沒(méi)有引用的代碼剔除掉澡为,因此我們需要在webpack中加上壓縮js代碼如下:
// 引入 ParallelUglifyPlugin 插件
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
module.exports = {
plugins: [
// 使用 ParallelUglifyPlugin 并行壓縮輸出JS代碼
new ParallelUglifyPlugin({
// 傳遞給 UglifyJS的參數(shù)如下:
uglifyJS: {
output: {
/*
是否輸出可讀性較強(qiáng)的代碼漂坏,即會(huì)保留空格和制表符,默認(rèn)為輸出媒至,為了達(dá)到更好的壓縮效果顶别,
可以設(shè)置為false
*/
beautify: false,
/*
是否保留代碼中的注釋?zhuān)J(rèn)為保留,為了達(dá)到更好的壓縮效果拒啰,可以設(shè)置為false
*/
comments: false
},
compress: {
/*
是否在UglifyJS刪除沒(méi)有用到的代碼時(shí)輸出警告信息驯绎,默認(rèn)為輸出,可以設(shè)置為false關(guān)閉這些作用
不大的警告
*/
warnings: false,
/*
是否刪除代碼中所有的console語(yǔ)句谋旦,默認(rèn)為不刪除剩失,開(kāi)啟后,會(huì)刪除所有的console語(yǔ)句
*/
drop_console: true,
/*
是否內(nèi)嵌雖然已經(jīng)定義了册着,但是只用到一次的變量拴孤,比如將 var x = 1; y = x, 轉(zhuǎn)換成 y = 5, 默認(rèn)為不
轉(zhuǎn)換,為了達(dá)到更好的壓縮效果甲捏,可以設(shè)置為false
*/
collapse_vars: true,
/*
是否提取出現(xiàn)了多次但是沒(méi)有定義成變量去引用的靜態(tài)值演熟,比如將 x = 'xxx'; y = 'xxx' 轉(zhuǎn)換成
var a = 'xxxx'; x = a; y = a; 默認(rèn)為不轉(zhuǎn)換,為了達(dá)到更好的壓縮效果摊鸡,可以設(shè)置為false
*/
reduce_vars: true
}
}
})
]
}
再運(yùn)行下打包命令后绽媒。我們繼續(xù)查看代碼蚕冬,如下所示:
可以看到還是會(huì)把無(wú)用的 b函數(shù) 和 c函數(shù)代碼打包進(jìn)去免猾。這是什么情況?那是因?yàn)槲覀冊(cè)趙ebpack中配置了 mode: 'development',我們現(xiàn)在把它改成 mode: 'production',后囤热,就可以看到只用 a函數(shù)了猎提,我們可以到dist目錄下的main.js代碼內(nèi)部搜索下 alert, 就可以看到了,只有一個(gè)alert('a')了。說(shuō)明b函數(shù)和c函數(shù)被剔除掉了锨苏。
tree-shaking 目前的缺陷:
tree-shaking 能夠利用ES6的靜態(tài)引入規(guī)范疙教,減少包的體積,避免不必要的代碼引入伞租,但是webpack只能做一點(diǎn)簡(jiǎn)單的事情贞谓。
比如 我現(xiàn)在在main.js代碼改成如下:
import { func2 } from '../common/util';
var a = func2(222);
alert(a);
common/util.js 代碼如下:
import lodash from 'lodash-es'
var func1 = function(v) {
alert('111');
return lodash.isArray(v);
}
var func2 = function(v) {
return v;
};
export {
func1,
func2
}
如上代碼,在main.js中引入了 func2, 但是并沒(méi)有引入func1, 但是func1引入了lodash-es葵诈。webpack在檢查的時(shí)候發(fā)現(xiàn)func1中確實(shí)用到了lodash-es裸弦,因此不會(huì)把lodash去掉,但是func1函數(shù)會(huì)去掉的作喘。但是我們?cè)趈s中也并沒(méi)有使用到lodash理疙。因此在這種情況下,webpack中的 tree-shaking 解決不了這種情況泞坦,因此 webpack-deep-scope-plugin 插件就可以解決這種問(wèn)題了窖贤,如下沒(méi)有使用 webpack-deep-scope-plugin 插件打包后的文件大小。如下:
如上main.js 打包壓縮后的js代碼大小有81.1kb贰锁。打開(kāi)dist/main.js代碼搜索 lodash后赃梧,可以搜索到,因此lodash插件被打包進(jìn)去main.js中了豌熄,但是實(shí)際上我們項(xiàng)目并沒(méi)有使用到lodash槽奕,因此lodash的庫(kù)我們按常理來(lái)講并不需要打包進(jìn)去的。
3. 使用webpack-deep-scope-plugin 優(yōu)化
1. 首先需要安裝 webpack-deep-scope-plugin, 安裝命令如下:
npm i -D webpack-deep-scope-plugin
在webpack.config.js 代碼引入如下:
// 引入 webpack-deep-scope-plugin 優(yōu)化
const WebpackDeepScopeAnalysisPlugin = require('webpack-deep-scope-plugin').default;
module.exports = {
plugins: [
new WebpackDeepScopeAnalysisPlugin()
]
}
然后我們繼續(xù)打包如下所示:
打包后發(fā)現(xiàn)如上房轿,只有969字節(jié)粤攒,1kb都不到,再打開(kāi)dist/main.js 查看代碼囱持,搜索下 lodash, 發(fā)現(xiàn)搜索不到夯接。
注意點(diǎn):
1. 要使用 tree-shaking,必須保證引用的插件的模塊是ES6模塊規(guī)范編寫(xiě)的纷妆,這也是我為什么引用了的是 lodash-es,而不是 'lodash'盔几, 如果引用的是lodash的話,是不能去掉的掩幢。
2. 在 .babelrc 中逊拍,babel設(shè)置 module: false, 避免babel將模塊轉(zhuǎn)換為成 CommonJS規(guī)范。引入模塊包也必須符合ES6規(guī)范的际邻。如下 babelrc代碼:
{
"plugins": [
[
"transform-runtime",
{
"polyfill": false
}
]
],
"presets": [
[
"env",
{
"modules": false // 關(guān)閉Babel的模塊轉(zhuǎn)換功能芯丧,保留ES6模塊化語(yǔ)法
}
],
"stage-2"
]
}
且需要在 package.json 中定義 sideEffect: false, 這也是為了避免出現(xiàn) import xxx 導(dǎo)致模塊內(nèi)部的一些函數(shù)執(zhí)行后影響全局環(huán)境, 卻被去除掉的情況.
3. webpack-deep-scope-plugin 插件依賴 node8.0+ 和 webpack 4.14.0 +