管理資源&管理輸出
管理資源
webpack
最出色的功能之一就是,除了JavaScript
擂仍,還可以通過loader
引入任何其他類型的文件。webpack.config.js
配置如下:
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.(png|svg|jpg|gif)$/,
use: [
'file-loader'
]
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [
'file-loader'
]
},
{
test: /\.(csv|tsv)$/,
use: [
'csv-loader'
]
},
{
test: /\.xml$/,
use: [
'xml-loader'
]
}
]
}
};
管理輸出
配置
HtmlWebpackPlugin
插件自動生成index.html
;配置CleanWebpackPlugin
插件自動清理dist
目錄轧拄;WebpackManifestPlugin
插件可以提取manifest
挥下。webpack.config.js
配置如下:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
entry: {
app: './src/index.js',
print: './src/print.js'
},
plugins: [
new CleanWebpackPlugin(['dist']),
new HtmlWebpackPlugin({
title: 'Output Management'
})
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
開發(fā)環(huán)境&生產(chǎn)環(huán)境
- 開發(fā)環(huán)境(
development
)和生產(chǎn)環(huán)境(production
)的構(gòu)建目標(biāo)差異很大揍魂。 - 在開發(fā)環(huán)境中,我們需要具有強大的棚瘟、具有實時重新加載(
live reloading
)或熱模塊替換(hot module replacement
)能力的source map
和localhost server
现斋。 - 而在生產(chǎn)環(huán)境中,我們的目標(biāo)則轉(zhuǎn)向于關(guān)注更小的
bundle
偎蘸,更輕量的source map
庄蹋,以及更優(yōu)化的資源,以改善加載時間迷雪。 - 由于要遵循邏輯分離限书,我們通常建議為每個環(huán)境編寫彼此獨立的
webpack
配置。 - 但是章咧,我們會遵循不重復(fù)原則倦西,保留一個“通用”配置。我們將使用一個名為
webpack-merge
的工具合并配置
npm install --save-dev webpack-merge
開發(fā)模式&模塊熱替換
生產(chǎn)環(huán)境
- 代碼壓縮:
UglifyJSPlugin
或其他工具 -
source map
用還是不用赁严? - 許多
library
將通過與process.env.NODE_ENV
環(huán)境變量關(guān)聯(lián)扰柠,以決定library
中應(yīng)該引用哪些內(nèi)容
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
})
-
ExtractTextPlugin
:將CSS
分離成單獨的文件 -
CLI
替代選項:原來用的一些插件現(xiàn)在可以替換成一些優(yōu)化配置項:例如,--optimize-minimize
標(biāo)記將在后臺引用UglifyJSPlugin
疼约。和以上描述的DefinePlugin
實例相同卤档,--define process.env.NODE_ENV="'production'"
也會做同樣的事情。并且程剥,webpack -p
將自動地調(diào)用上述這些標(biāo)記劝枣,從而調(diào)用需要引入的插件。
構(gòu)建性能
- 保持版本最新:
webpack/node/npm
- 盡量少使用不同的工具
loader/plugins
;必要的話也用在盡量最少數(shù)的必要模塊中哨免;可以將非常消耗資源的 loaders 轉(zhuǎn)存到worker pool
中
//使用 include 字段僅將 loader 模塊應(yīng)用在實際需要用其轉(zhuǎn)換的位置中:
{
test: /\.js$/,
include: path.resolve(__dirname, "src"),
loader: "babel-loader"
}
-
Dlls
:使用DllPlugin
將更改不頻繁的代碼進行單獨編譯茎活。這將改善引用程序的編譯速度,即使它增加了構(gòu)建過程的復(fù)雜性琢唾。 -
Smaller = Faster
:減少編譯的整體大小载荔,以提高構(gòu)建性能。盡量保持chunks
小巧采桃。 - 使用
cache-loader
啟用持久化緩存懒熙。使用package.json
中的postinstall
清除緩存目錄。thread-loader
可以將非常消耗資源的loaders
轉(zhuǎn)存到worker pool
中 - 提高解析速度:
- 盡量減少
resolve.modules, resolve.extensions, resolve.mainFiles, resolve.descriptionFiles
中類目的數(shù)量普办,因為他們會增加文件系統(tǒng)調(diào)用的次數(shù)工扎。 - 如果你不使用
symlinks
,可以設(shè)置resolve.symlinks: false
衔蹲。 - 如果你使用自定義解析
plugins
肢娘,并且沒有指定context
信息,可以設(shè)置resolve.cacheWithContext: false
舆驶。
- 盡量減少
- 開發(fā)環(huán)境中
- 硬避免在生產(chǎn)環(huán)境下才會用到的工具:
UglifyJsPlugin橱健、ExtractTextPlugin、[hash]/[chunkhash]沙廉、AggressiveSplittingPlugin拘荡、AggressiveMergingPlugin、ModuleConcatenationPlugin
- 不同的
devtool
的設(shè)置撬陵,會導(dǎo)致不同的性能差異 - 在內(nèi)存中進行代碼的編譯和資源的提供珊皿,但并不寫入磁盤來提高性能:
webpack-dev-server
- 盡量減少入口
chunk
的體積,以提高性能
- 硬避免在生產(chǎn)環(huán)境下才會用到的工具:
- 其他
tree shaking
tree shaking
是一個術(shù)語巨税,通常用于描述移除JavaScript
上下文中的未引用代碼(dead-code
)蟋定。新的webpack 4
正式版本,擴展了這個檢測能力垢夹,通過package.json
的sideEffects
屬性作為標(biāo)記溢吻,向compiler
提供提示,表明項目中的哪些文件是pure
(純的ES2015
模塊)"果元,由此可以安全地刪除文件中未使用的部分促王。
-
sideEffects
:將文件標(biāo)記為無副作用
如同上面提到的,如果所有代碼都不包含副作用而晒,我們就可以簡單地將該屬性標(biāo)記為 false蝇狼,來告知 webpack,它可以安全地刪除未用到的 export 導(dǎo)出倡怎。
{
"name": "your-project",
"sideEffects": false
}
如果你的代碼確實有一些副作用迅耘,那么可以改為提供一個數(shù)組:
{
"name": "your-project",
"sideEffects": [
"./src/some-side-effectful-file.js"
]
}
- 壓縮輸出:通過如上方式贱枣,我們可以,找出那些需要刪除的“未使用代碼”颤专,然而纽哥,我們不只是要找出,還需要在
bundle
中刪除它們栖秕。為此春塌,我們將使用-p(production)
這個webpack
編譯標(biāo)記,來啟用uglifyjs
壓縮插件簇捍。- 注意只壳,
--optimize-minimize
標(biāo)記也會在webpack
內(nèi)部調(diào)用UglifyJsPlugin
。) - 從
webpack 4
開始暑塑,也可以通過mode
配置選項輕松切換到壓縮輸出吼句,只需設(shè)置為production
。
- 注意只壳,
懶加載(動態(tài)導(dǎo)入/按需加載)
在
vue
單頁應(yīng)用中事格,當(dāng)項目不斷完善豐富時惕艳,即使使用webpack
打包,文件依然是非常大的分蓖,影響頁面的加載尔艇。如果我們能把不同路由對應(yīng)的組件分割成不同的代碼塊尔许,當(dāng)路由被訪問時才加載對應(yīng)的組件(也就是按需加載)么鹤,這樣就更加高效了∥独龋——引自vue-router
官方文檔
非動態(tài)加載時的打包情況如下圖:
如下案例中
hello
組件的加載方式改為路由懶加載[import()語法],在進行打包
// import Hello from '@/components/Hello'
export default new Router({
routes: [
{
path: '/',
name: 'Hello',
// component: Hello,
component: () => import('@/components/Hello')
}
]
})
- 很明顯的看到蒸甜,打包后有
4個js
文件,仔細的同學(xué)還發(fā)現(xiàn)余佛,app.js
文件的大小加上新多出文件的大小柠新,約等于沒有分割打包的app
的大小。 - 這樣等于異步加載的組件辉巡,是單獨打包成了一個
js
恨憎,在頁面首次加載的時候不需要加載他,等到請求相應(yīng)的頁面的時候在去服務(wù)器請求它郊楣,減小了頁面首屏加載的時間憔恳。 -
webpack.prod.conf.js
中配置output.chunkFilename
規(guī)定了打包異步文件的格式
//webpack.prod.conf.js:生產(chǎn)打包js
//utils.assetsPath是utils.js中封裝的一個路徑相關(guān)的方法
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
/**
本人在自己的工程中刪除或修改chunkFilename的配置,
最后還是都按這個規(guī)則生產(chǎn)js文件了净蚤,
Why?
*/
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},
-
不需要刻意懶加載钥组。下面的案例中,代碼確實會在腳本運行的時候產(chǎn)生一個分離的代碼塊
lodash.bundle.js
今瀑,在技術(shù)概念上“懶加載”它程梦。問題是加載這個包并不需要用戶的交互 -- 意思是每次加載頁面的時候都會請求它点把。這樣做并沒有對我們有很多幫助,還會對性能產(chǎn)生負面影響屿附。
//src/index.js
- import _ from 'lodash';
-
- function component() {
+ function getComponent() {
- var element = document.createElement('div');
-
- // Lodash, now imported by this script
- element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+ return import(/* webpackChunkName: "lodash" */ 'lodash').then(_ => {
+ var element = document.createElement('div');
+
+ element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+
+ return element;
+
+ }).catch(error => 'An error occurred while loading the component');
}
- document.body.appendChild(component());
+ getComponent().then(component => {
+ document.body.appendChild(component);
+ })
- 當(dāng)頁面有個按鈕點擊事件時郎逃,需要加載某個組件,這種用戶交互的情景下挺份,我們可以使用懶加載衣厘;當(dāng)然,路由也是交互的一種压恒。
緩存
客戶端(通常是瀏覽器)獲取資源是比較耗費時間的影暴。可以通過命中緩存探赫,以降低網(wǎng)絡(luò)流量型宙,使網(wǎng)站加載速度更快;然而伦吠,緩存的存在可能會使你獲取新代碼時比較棘手(文件名不變的話)妆兑。如何通過必要的配置,以確保
webpack
編譯生成的文件能夠被客戶端緩存毛仪,而在文件內(nèi)容變化后搁嗓,能夠請求到新的文件。
- 通過使用
output.filename
進行文件名替換
- 如果我們不做修改箱靴,然后再次運行構(gòu)建腺逛,我們以為文件名會保持不變。然而衡怀,如果我們真的運行棍矛,可能會發(fā)現(xiàn)情況并非如此(譯注:如果不做修改,文件名可能會變抛杨,也可能不會够委。)
- 這也是因為
webpack
在入口chunk
中,包含了某些樣板(boilerplate
)怖现,特別是runtime
和manifest
茁帽。(譯注:樣板(boilerplate
)指webpack
運行時的引導(dǎo)代碼) - 即:如果
webpack
生成的hash
發(fā)生改變,manifest
文件也會發(fā)生改變屈嗤。因此潘拨,vendor bundle
的內(nèi)容也會發(fā)生改變,并且失效恢共。所以战秋,我們需要將manifest
文件提取出來。 - 用
CommonsChunkPlugin
將manifest
提取出來
-
webpack
里每個模塊都有一個module id
讨韭,module id
是該模塊在模塊依賴關(guān)系圖里按順序分配的序號脂信,如果這個module id
發(fā)生了變化癣蟋,那么他的chunkhash
也會發(fā)生變化。 - 這樣會導(dǎo)致:如果你引入一個新的模塊狰闪,會導(dǎo)致
module id
整體發(fā)生改變疯搅,可能會導(dǎo)致所有文件的chunkhash
發(fā)生變化,這顯然不是我們想要的 - 這里需要用
HashedModuleIdsPlugin
埋泵,根據(jù)模塊的相對路徑生成一個四位數(shù)的hash作為模塊id幔欧,這樣就算引入了新的模塊,也不會影響module id
的值丽声,只要模塊的路徑不改變的話礁蔗。
new webpack.HashedModuleIdsPlugin()
創(chuàng)建library
除了打包應(yīng)用程序代碼,
webpack
還可以用于打包JavaScript library
雁社;即我們平時npm install
下來的那種依賴包
最后浴井,
package.json
中配置"main": "dist/ginna-form.js"
;我們從node_modules
引入時就靠這個屬性來找到對應(yīng)的文件的哦。
shimming
webpack
編譯器(compiler
)能夠識別遵循ES2015
模塊語法霉撵、CommonJS
或AMD
規(guī)范編寫的模塊磺浙。然而,一些第三方的庫(library
)可能會引用一些全局依賴(例如jQuery
中的$
)徒坡。這些庫也可能創(chuàng)建一些需要被導(dǎo)出的全局變量撕氧。 這些“不符合規(guī)范的模塊”就是shimming
發(fā)揮作用的地方
shim
:一種庫(library
)的抽象,這種庫能將一個新的API
引入到一個舊的環(huán)境中喇完,而且僅靠舊的環(huán)境中已有的手段實現(xiàn)伦泥。polyfill
就是一個用在瀏覽器API
上的shim
。
1. 讓jQuery
作為全局變量何暮,可以被別的組件引用
2. 將模塊中的this
置為window
當(dāng)模塊運行在 CommonJS
環(huán)境下this
會變成一個問題奄喂,也就是說此時的 this
指向的是 module.exports
。在這個例子中海洼,你可以通過使用 imports-loader
覆寫 this
:
3. 某個庫(library)創(chuàng)建出一個全局變量,它期望用戶使用這個變量
- 你可能從來沒有在自己的源碼中做過這些事情富腊,但是你也許遇到過一個老舊的庫(
library
)坏逢,和下面所展示的代碼類似。 - 在下圖用例中赘被,我們可以使用
exports-loader
是整,將一個全局變量作為一個普通的模塊來導(dǎo)出。例如民假,為了將file
導(dǎo)出為file
以及將helpers.parse
導(dǎo)出為parse
4. babel-polyfill
按需加載
-
Babel
是一個廣泛使用的轉(zhuǎn)碼器浮入,可以將ES6
代碼轉(zhuǎn)為ES5
代碼,從而可以在現(xiàn)有環(huán)境執(zhí)行羊异,所以我們可以用ES6
編寫事秀,而不用考慮環(huán)境支持的問題彤断。 -
Babel
默認(rèn)只轉(zhuǎn)換新的JavaScript
語法(syntax
),如箭頭函數(shù)等易迹,而不轉(zhuǎn)換新的API
宰衙,比如Iterator、Generator睹欲、Set供炼、Maps、Proxy、Reflect策精、Symbol膏燕、Promise
等全局對象,以及一些定義在全局對象上的方法(比如Object.assign
)都不會轉(zhuǎn)碼先嬉;因此我們需要polyfill
;
5. 深度優(yōu)化包babel-preset-env
-
babel-preset-env
是一個新的preset
楚堤,可以根據(jù)配置的目標(biāo)運行環(huán)境(environment
)自動啟用需要的babel
插件 - 之前我們寫
javascript
代碼時疫蔓,需要使用N
個preset
,比如:babel-preset-es2015身冬、babel-preset-es2016
衅胀。es2015
可以把ES6
代碼編譯為ES5
,es2016
可以把ES2016
代碼編譯為ES6
酥筝。babel-preset-latest
可以編譯stage 4
進度的ECMAScript
代碼滚躯。 - 問題是我們幾乎每個項目中都使用了非常多的
preset
,包括不必要的嘿歌。 -
babel-preset-env
的工作方式類似babel-preset-latest
掸掏,唯一不同的就是它會根據(jù)配置的env
只編譯那些還不支持的特性。 - 使用這個插件宙帝,你講再也不需要使用
es20xx presets
了丧凤。
6. 其他工具
- 還有一些其他的工具能夠幫助我們處理這些老舊的模塊。
-
script-loader
會在全局上下文中對代碼進行取值步脓,類似于通過一個script
標(biāo)簽引入腳本愿待。在這種模式下,每一個標(biāo)準(zhǔn)的庫(library
)都應(yīng)該能正常運行 - 這些老舊的模塊如果沒有
AMD/CommonJS
規(guī)范版本靴患,但你也想將他們加入dist
文件仍侥,你可以使用noParse
來標(biāo)識出這個模塊
noParse:這是module中的一個屬性,
作用:不去解析屬性值代表的庫的依賴
舉例:
我們一般引用jquery鸳君,可以如下引用:
import jq from 'jquery'
對于上面的解析規(guī)則:
當(dāng)解析jq的時候农渊,會去解析jq這個庫是否有依賴其他的包
我們對類似jq這類依賴庫,一般會認(rèn)為不會引用其他的包(特殊除外,自行判斷)或颊。
所以砸紊,對于這類不引用其他的包的庫传于,我們在打包的時候就沒有必要去解析,
這樣能夠增加打包速率批糟。
所以格了,可以在webpack的配置中增加noParse屬性
(以下代碼只需要看module的noParse屬性)
module.exports = {
mode:'development',
entry:'./src/index.js',
output:{
filename:'bundle.js',
path:path.resolve(__dirname,'dist')
},
module:{
noParse:/jquery/,//不去解析jquery中的依賴庫
rules:[ ]
}
}
漸進式網(wǎng)絡(luò)應(yīng)用程序PWA
漸進式網(wǎng)絡(luò)應(yīng)用程序(
Progressive Web Application - PWA
),是一種可以提供類似于原生應(yīng)用程序(native app
)體驗的網(wǎng)絡(luò)應(yīng)用程序(web app
)徽鼎。PWA
可以用來做很多事盛末。其中最重要的是,在離線(offline
)時應(yīng)用程序能夠繼續(xù)運行功能否淤。這是通過使用名為Service Workers
的網(wǎng)絡(luò)技術(shù)來實現(xiàn)的悄但。
//print.js
export default function printMe() {
console.log('I get called from print.js!');
}
//index.js
import printMe from './print.js';
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('./service-worker.js').then(registration => {
console.log('SW registered: ', registration);
}).catch(registrationError => {
console.log('SW registration failed: ', registrationError);
});
});
}
function component() {
var element = document.createElement('div');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
var btn = document.createElement('button');
btn.innerHTML = 'Click me and check the console!';
btn.onclick = printMe;
element.appendChild(btn);
return element;
}
document.body.appendChild(component());
//webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const WorkboxPlugin = require('workbox-webpack-plugin');
module.exports = {
entry: {
app: './src/index.js',
print: './src/print.js'
},
//3個插件都需要npm install --save-dev
plugins: [
new CleanWebpackPlugin(['dist']),
new HtmlWebpackPlugin({
title: 'Output Management'
title: 'Progressive Web Application'
}),
//添加 Workbox
new WorkboxPlugin.GenerateSW({
// 這些選項幫助 ServiceWorkers 快速啟用
// 不允許遺留任何“舊的” ServiceWorkers
clientsClaim: true,
skipWaiting: true
})
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
//package.json配置腳本
"scripts": {
"build": "webpack",
//使用一個簡易服務(wù)器,搭建出我們所需的離線體驗
//npm install http-server --save-dev
"start": "http-server dist"
},
- 有了
Workbox
石抡,我們再看下執(zhí)行npm run build
時會發(fā)生什么
clean-webpack-plugin: /mnt/c/Source/webpack-follow-along/dist has been removed.
Hash: 6588e31715d9be04be25
Version: webpack 3.10.0
Time: 782ms
Asset Size Chunks Chunk Names
app.bundle.js 545 kB 0, 1 [emitted] [big] app
print.bundle.js 2.74 kB 1 [emitted] print
index.html 254 bytes [emitted]
precache-manifest.b5ca1c555e832d6fbf9462efd29d27eb.js 268 bytes [emitted]
service-worker.js 1 kB [emitted]
[0] ./src/print.js 87 bytes {0} {1} [built]
[1] ./src/index.js 477 bytes {0} [built]
[3] (webpack)/buildin/global.js 509 bytes {0} [built]
[4] (webpack)/buildin/module.js 517 bytes {0} [built]
+ 1 hidden module
Child html-webpack-plugin for "index.html":
1 asset
[2] (webpack)/buildin/global.js 509 bytes {0} [built]
[3] (webpack)/buildin/module.js 517 bytes {0} [built]
+ 2 hidden modules
- 現(xiàn)在你可以看到檐嚣,生成了 2 個額外的文件:
service-worker.js
(或sw.js
) 和體積很大的precache-manifest.b5ca1c555e832d6fbf9462efd29d27eb.js
。sw.js
是Service Worker
文件啰扛,precache-manifest.b5ca1c555e832d6fbf9462efd29d27eb.js
是sw.js
引用的文件嚎京,所以它也可以運行 - 然后用
npm start
啟動服務(wù)。訪問http://localhost:8080/index.html
并查看console
控制臺隐解。在那里你應(yīng)該看到:
SW registered
- 現(xiàn)在來進行測試鞍帝。停止服務(wù)器并刷新頁面。如果瀏覽器能夠支持
Service Worker
煞茫,你應(yīng)該可以看到你的應(yīng)用程序還在正常運行帕涌。然而,服務(wù)器已經(jīng)停止了服務(wù)续徽,此刻是Service Worker
在提供服務(wù)蚓曼。
TypeScript
準(zhǔn)備工作:
ts-loader
插件、tsconfig.json
配置文件(和package.json
同級)钦扭、ts
文件中如何使用第三方庫(ts
聲明文件*.d.ts
)
-
webpack.config.js
配置
-
tsconfig.json
案例:
- 使用第三方庫
當(dāng)從npm
安裝第三方庫時纫版,一定要牢記同時安裝這個庫的類型聲明文件。你可以從TypeSearch
中找到并安裝這些第三方庫的類型聲明文件土全。舉個例子捎琐,如果想安裝lodash
這個庫的類型聲明文件,我們可以運行下面的命令:npm install --save-dev @types/lodash
TypeScript
學(xué)習(xí)