github:github.com/fenivana/we…
webpack 更新到了 4.0,官網還沒有更新文件。因此把教程更新一下,方便大家用起 webpack 4。
寫在開頭
先說說為什麼要寫這篇文章,最初的原因是組裡的小朋友們看了 webpack 文件後,表情都是這樣的:摘自 webpack 一篇文件的評論區)
和這樣的:
是的,即使是外國佬也在吐槽這文件不是人能看的。回想起當年自己啃 webpack 文件的血與淚的往事,覺得有必要整一個教程,可以讓大家看完後愉悅地搭建起一個 webpack 打包方案的專案。
官網新的 webpack 文件現在寫的很詳細了,能看英文的小夥伴可以直接去看官網。
可能會有人問 webpack 到底有什麼用,你不能上來就糊我一臉程式碼讓我馬上搞,我照著搞了一遍結果根本沒什麼用,都是騙人的。所以,在說 webpack 之前,我想先談一下前端打包方案這幾年的演進歷程,在什麼場景下,我們遇到了什麼問題,催生出了應對這些問題的工具。瞭解了需求和目的之後,你就知道什麼時候 webpack 可以幫到你。我希望我用完之後很爽,你們用完之後也是。
先說說前端打包方案的黑暗歷史
在很長的一段前端歷史裡,是不存在打包這個說法的。那個時候頁面基本是純靜態的或者服務端輸出的,沒有 AJAX,也沒有 jQuery。那個時候的 JavaScript 就像個玩具,用處大概就是在側欄弄個時鐘,用 media player 放個 mp3 之類的指令碼,程式碼量不是很多,直接放在 <script>
標籤裡或者弄個 js 檔案引一下就行,日子過得很輕鬆愉快。
隨後的幾年,人們開始嘗試在一個頁面裡做更多的事情。容器的顯示,隱藏,切換。用 css 寫的彈層,圖片輪播等等。但如果一個頁面內不能向伺服器請求資料,能做的事情畢竟有限的,程式碼的量也能維持在頁面互動邏輯範圍內。這時候很多人開始突破一個頁面能做的事情的範圍,使用隱藏的 iframe 和 flash 等作為和伺服器通訊的橋樑,新世界的大門慢慢地被開啟,在一個頁面內和伺服器進行資料互動,意味著以前需要跳轉多個頁面的事情現在可以用一個頁面搞定。但由於 iframe 和 flash 技術過於 tricky 和複雜,並沒能得到廣泛的推廣。
直到 Google 推出 Gmail 的時候(2004 年),人們意識到了一個被忽略的介面,XMLHttpRequest, 也就是我們俗稱的 AJAX, 這是一個使用方便的,相容性良好的伺服器通訊介面。從此開始,我們的頁面開始玩出各種花來了,前端一下子出現了各種各樣的庫,Prototype、Dojo、MooTools、Ext JS、jQuery…… 我們開始往頁面裡插入各種庫和外掛,我們的 js 檔案也就爆炸了。
隨著 js 能做的事情越來越多,引用越來越多,檔案越來越大,加上當時大約只有 2Mbps 左右的網速,下載速度還不如 3G 網路,對 js 檔案的壓縮和合並的需求越來越強烈,當然這裡面也有把程式碼混淆了不容易被盜用等其他因素在裡面。JSMin、YUI Compressor、Closure Compiler、UglifyJS 等 js 檔案壓縮合並工具陸陸續續誕生了。壓縮工具是有了,但我們得要執行它,最簡單的辦法呢,就是 windows 上搞個 bat 指令碼,mac / linux 上搞個 bash 指令碼,哪幾個檔案要合併在一塊的,哪幾個要壓縮的,釋出的時候執行一下指令碼,生成壓縮後的檔案。
基於合併壓縮技術,專案越做越大,問題也越來越多,大概就是以下這些問題:
- 庫和外掛為了要給他人呼叫,肯定要找個地方註冊,一般就是在 window 下申明一個全域性的函式或物件。難保哪天用的兩個庫在全域性用同樣的名字,那就衝突了。
- 庫和外掛如果還依賴其他的庫和外掛,就要告知使用人,需要先引哪些依賴庫,那些依賴庫也有自己的依賴庫的話,就要先引依賴庫的依賴庫,以此類推。
恰好就在這個時候(2009 年),隨著後端 JavaScript 技術的發展,人們提出了 CommonJS 的模組化規範,大概的語法是: 如果 a.js
依賴 b.js
和 c.js
, 那麼就在 a.js
的頭部,引入這些依賴檔案:
var b = require('./b')
var c = require('./c')
複製程式碼
那麼變數 b
和 c
會是什麼呢?那就是 b.js 和 c.js 匯出的東西,比如 b.js 可以這樣匯出:
exports.square = function(num) {
return num * num
}
複製程式碼
然後就可以在 a.js 使用這個 square
方法:
var n = b.square(2)
複製程式碼
如果 c.js 依賴 d.js, 匯出的是一個 Number
, 那麼可以這樣寫:
var d = require('./d')
module.exports = d.PI // 假設 d.PI 的值是 3.14159
複製程式碼
那麼 a.js 中的變數 c
就是數字 3.14159
,具體的語法規範可以檢視 Node.js 的 文件。
但是 CommonJS 在瀏覽器內並不適用。因為 require()
的返回是同步的,意味著有多個依賴的話需要一個一個依次下載,堵塞了 js 指令碼的執行。所以人們就在 CommonJS 的基礎上定義了 Asynchronous Module Definition (AMD) 規範(2011 年),使用了非同步回撥的語法來並行下載多個依賴項,比如作為入口的 a.js 可以這樣寫:
require(['./b', './c'], function(b, c) {
var n = b.square(2)
console.log(c)
})
複製程式碼
相應的匯出語法也是非同步回撥方式,比如 c.js
依賴 d.js
, 就寫成這樣:
define(['./d'], function(d) {
return d.PI
})
複製程式碼
可以看到,定義一個模組是使用 define()
函式,define()
和 require()
的區別是,define()
必須要在回撥函式中返回一個值作為匯出的東西,require()
不需要匯出東西,因此回撥函式中不需要返回值,也無法作為被依賴項被其他檔案匯入,因此一般用於入口檔案,比如頁面中這樣載入 a.js
:
<script src="js/require.js" data-main="js/a"></script>
複製程式碼
以上是 AMD 規範的基本用法,更詳細的就不多說了(反正也淘汰了~),有興趣的可以看 這裡。
js 模組化問題基本解決了,css 和 html 也沒閒著。什麼 less,sass,stylus 的 css 前處理器橫空出世,說能幫我們簡化 css 的寫法,自動給你加 vendor prefix。html 在這期間也出現了一堆模板語言,什麼 handlebars,ejs,jade,可以把 ajax 拿到的資料插入到模板中,然後用 innerHTML 顯示到頁面上。
託 AMD 和 CSS 預處理和模板語言的福,我們的編譯指令碼也洋洋灑灑寫了百來行。命令列指令碼有個不好的地方,就是 windows 和 mac/linux 是不通用的,如果有跨平臺需求的話,windows 要裝個可以執行 bash 指令碼的命令列工具,比如 msys(目前最新的是 msys2),或者使用 php 或 python 等其他語言的指令碼來編寫,對於非全棧型的前端程式設計師來說,寫 bash / php / python 還是很生澀的。因此我們需要一個簡單的打包工具,可以利用各種編譯工具,編譯 / 壓縮 js、css、html、圖片等資源。然後 Grunt 產生了(2012 年),配置檔案格式是我們最愛的 js,寫法也很簡單,社群有非常多的外掛支援各種編譯、lint、測試工具。一年多後另一個打包工具 gulp 誕生了,擴充套件性更強,採用流式處理效率更高。
依託 AMD 模組化程式設計,SPA(Single-page application) 的實現方式更為簡單清晰,一個網頁不再是傳統的類似 word 文件的頁面,而是一個完整的應用程式。SPA 應用有一個總的入口頁面,我們通常把它命名為 index.html、app.html、main.html,這個 html 的 <body>
一般是空的,或者只有總的佈局(layout),比如下圖:
佈局會把 header、nav、footer 的內容填上,但 main 區域是個空的容器。這個作為入口的 html 最主要的工作是載入啟動 SPA 的 js 檔案,然後由 js 驅動,根據當前瀏覽器地址進行路由分發,載入對應的 AMD 模組,然後該 AMD 模組執行,渲染對應的 html 到頁面指定的容器內(比如圖中的 main)。在點選連結等互動時,頁面不會跳轉,而是由 js 路由載入對應的 AMD 模組,然後該 AMD 模組渲染對應的 html 到容器內。
雖然 AMD 模組讓 SPA 更容易地實現,但小問題還是很多的:
- 不是所有的第三方庫都是 AMD 規範的,這時候要配置
shim
,很麻煩。 - 雖然 RequireJS 支援通過外掛把 html 作為依賴載入,但 html 裡面的
<img>
的路徑是個問題,需要使用絕對路徑並且保持打包後的圖片路徑和打包前的路徑不變,或者使用 html 模板語言把src
寫成變數,在執行時生成。 - 不支援動態載入 css,變通的方法是把所有的 css 檔案合併壓縮成一個檔案,在入口的 html 頁面一次性載入。
- SPA 專案越做越大,一個應用打包後的 js 檔案到了幾 MB 的大小。雖然 r.js 支援分模組打包,但配置很麻煩,因為模組之間會互相依賴,在配置的時候需要 exclude 那些通用的依賴項,而依賴項要在檔案裡一個個檢查。
- 所有的第三方庫都要自己一個個的下載,解壓,放到某個目錄下,更別提更新有多麻煩了。雖然可以用 npm 包管理工具,但 npm 的包都是 CommonJS 規範的,給後端 Node.js 用的,只有部分支援 AMD 規範,而且在 npm 3 之前,這些包有依賴項的話也是不能用的。後來有個 bower 包管理工具是專門的 web 前端倉庫,這裡的包一般都支援 AMD 規範。
- AMD 規範定義和引用模組的語法太麻煩,上面介紹的 AMD 語法僅是最簡單通用的語法,API 文件裡面還有很多變異的寫法,特別是當發生迴圈引用的時候(a 依賴 b,b 依賴 a),需要使用其他的 語法 解決這個問題。而且 npm 上很多前後端通用的庫都是 CommonJS 的語法。後來很多人又開始嘗試使用 ES6 模組規範,如何引用 ES6 模組又是一個大問題。
- 專案的檔案結構不合理,因為 grunt/gulp 是按照檔案格式批量處理的,所以一般會把 js、html、css、圖片分別放在不同的目錄下,所以同一個模組的檔案會散落在不同的目錄下,開發的時候找檔案是個麻煩的事情。code review 時想知道一個檔案是哪個模組的也很麻煩,解決辦法比如又要在 imgs 目錄下建立按模組命名的資料夾,裡面再放圖片。
到了這裡,我們的主角 webpack 登場了(2012 年)(此處應有掌聲)。
和 webpack 差不多同期登場的還有 Browserify。這裡簡單介紹一下 Browserify。Browserify 的目的是讓前端也能用 CommonJS 的語法 require('module')
來載入 js。它會從入口 js 檔案開始,把所有的 require()
呼叫的檔案打包合併到一個檔案,這樣就解決了非同步載入的問題。那麼 Browserify 有什麼不足之處導致我不推薦使用它呢? 主要原因有下面幾點:
- 最主要的一點,Browserify 不支援把程式碼打包成多個檔案,在有需要的時候載入。這就意味著訪問任何一個頁面都會全量載入所有檔案。
- Browserify 對其他非 js 檔案的載入不夠完善,因為它主要解決的是
require()
js 模組的問題,其他檔案不是它關心的部分。比如 html 檔案裡的 img 標籤,它只能轉成 Data URI 的形式,而不能替換為打包後的路徑。 - 因為上面一點 Browserify 對資原始檔的載入支援不夠完善,導致打包時一般都要配合 gulp 或 grunt 一塊使用,無謂地增加了打包的難度。
- Browserify 只支援 CommonJS 模組規範,不支援 AMD 和 ES6 模組規範,這意味舊的 AMD 模組和將來的 ES6 模組不能使用。
基於以上幾點,Browserify 並不是一個理想的選擇。那麼 webpack 是否解決了以上的幾個問題呢? 廢話,不然介紹它幹嘛。那麼下面章節我們用實戰的方式來說明 webpack 是怎麼解決上述的問題的。
上手先搞一個簡單的 SPA 應用
一上來步子太大容易扯到蛋,讓我們先弄個最簡單的 webpack 配置來熱一下身。
安裝 Node.js
webpack 是基於我大 Node.js 的打包工具,上來第一件事自然是先安裝 Node.js 了,傳送門 ->。
初始化一個專案
我們先隨便找個地方,建一個資料夾叫 simple
, 然後在這裡面搭專案。完成品在 examples/simple 目錄,大家搞的時候可以參照一下。我們先看一下目錄結構:
├── dist 打包輸出目錄,只需部署這個目錄到生產環境
├── package.json 專案配置資訊
├── node_modules npm 安裝的依賴包都在這裡面
├── src 我們的原始碼
│ ├── components 可以複用的模組放在這裡面
│ ├── index.html 入口 html
│ ├── index.js 入口 js
│ ├── shared 公共函式庫
│ └── views 頁面放這裡
└── webpack.config.js webpack 配置檔案
複製程式碼
開啟命令列視窗,cd
到剛才建的 simple 目錄。然後執行這個命令初始化專案:
npm init
複製程式碼
命令列會要你輸入一些配置資訊,我們這裡一路按回車下去,生成一個預設的專案配置檔案 package.json
。
給專案加上語法報錯和程式碼規範檢查
我們安裝 eslint, 用來檢查語法報錯,當我們書寫 js 時,有錯誤的地方會出現提示。
npm install eslint eslint-config-enough eslint-loader --save-dev
複製程式碼
npm install
可以一條命令同時安裝多個包,包之間用空格分隔。包會被安裝進 node_modules
目錄中。
--save-dev
會把安裝的包和版本號記錄到 package.json
中的 devDependencies
物件中,還有一個 --save
, 會記錄到 dependencies
物件中,它們的區別,我們可以先簡單的理解為打包工具和測試工具用到的包使用 --save-dev
存到 devDependencies
, 比如 eslint、webpack。瀏覽器中執行的 js 用到的包存到 dependencies
, 比如 jQuery 等。那麼它們用來幹嘛的?
因為有些 npm 包安裝是需要編譯的,那麼導致 windows / mac /linux 上編譯出的可執行檔案是不同的,也就是無法通用,因此我們在提交程式碼到 git 上去的時候,一般都會在 .gitignore
裡指定忽略 node_modules 目錄和裡面的檔案,這樣其他人從 git 上拉下來的專案是沒有 node_modules 目錄的,這時我們需要執行
npm install
複製程式碼
它會讀取 package.json
中的 devDependencies
和 dependencies
欄位,把記錄的包的相應版本下載下來。
這裡 eslint-config-enough 是配置檔案,它規定了程式碼規範,要使它生效,我們要在 package.json
中新增內容:
{
"eslintConfig": {
"extends": "enough",
"env": {
"browser": true,
"node": true
}
}
}
複製程式碼
業界最有名的語法規範是 airbnb 出品的,但它規定的太死板了,比如不允許使用 for-of
和 for-in
等。感興趣的同學可以參照 這裡 安裝使用。
eslint-loader 用於在 webpack 編譯的時候檢查程式碼,如果有錯誤,webpack 會報錯。
專案裡安裝了 eslint 還沒用,我們的 IDE 和編輯器也得要裝 eslint 外掛支援它。
Visual Studio Code 需要安裝 ESLint 擴充套件
atom 需要安裝 linter 和 linter-eslint 這兩個外掛,裝好後重啟生效。
WebStorm 需要在設定中開啟 eslint 開關:
寫幾個頁面
我們寫一個最簡單的 SPA 應用來介紹 SPA 應用的內部工作原理。首先,建立 src/index.html 檔案,內容如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
</body>
</html>
複製程式碼
它是一個空白頁面,注意這裡我們不需要自己寫 <script src="index.js"></script>
, 因為打包後的檔名和路徑可能會變,所以我們用 webpack 外掛幫我們自動加上。
src/index.js:
// 引入 router
import router from './router'
// 啟動 router
router.start()
複製程式碼
src/router.js:
// 引入頁面檔案
import foo from './views/foo'
import bar from './views/bar'
const routes = {
'/foo': foo,
'/bar': bar
}
// Router 類,用來控制頁面根據當前 URL 切換
class Router {
start() {
// 點選瀏覽器後退 / 前進按鈕時會觸發 window.onpopstate 事件,我們在這時切換到相應頁面
// https://developer.mozilla.org/en-US/docs/Web/Events/popstate
window.addEventListener('popstate', () => {
this.load(location.pathname)
})
// 開啟頁面時載入當前頁面
this.load(location.pathname)
}
// 前往 path,變更位址列 URL,並載入相應頁面
go(path) {
// 變更位址列 URL
history.pushState({}, '', path)
// 載入頁面
this.load(path)
}
// 載入 path 路徑的頁面
load(path) {
// 首頁
if (path === '/') path = '/foo'
// 建立頁面例項
const view = new routes[path]()
// 呼叫頁面方法,把頁面載入到 document.body 中
view.mount(document.body)
}
}
// 匯出 router 例項
export default new Router()
複製程式碼
src/views/foo/index.js:
// 引入 router
import router from '../../router'
// 引入 html 模板,會被作為字串引入
import template from './index.html'
// 引入 css, 會生成 <style> 塊插入到 <head> 頭中
import './style.css'
// 匯出類
export default class {
mount(container) {
document.title = 'foo'
container.innerHTML = template
container.querySelector('.foo__gobar').addEventListener('click', () => {
// 呼叫 router.go 方法載入 /bar 頁面
router.go('/bar')
})
}
}
複製程式碼
src/views/bar/index.js:
// 引入 router
import router from '../../router'
// 引入 html 模板,會被作為字串引入
import template from './index.html'
// 引入 css, 會生成 <style> 塊插入到 <head> 頭中
import './style.css'
// 匯出類
export default class {
mount(container) {
document.title = 'bar'
container.innerHTML = template
container.querySelector('.bar__gofoo').addEventListener('click', () => {
// 呼叫 router.go 方法載入 /foo 頁面
router.go('/foo')
})
}
}
複製程式碼
藉助 webpack 外掛,我們可以 import
html, css 等其他格式的檔案,文字類的檔案會被儲存為變數打包進 js 檔案,其他二進位制類的檔案,比如圖片,可以自己配置,小圖片作為 Data URI 打包進 js 檔案,大檔案打包為單獨檔案,我們稍後再講這塊。
其他的 src 目錄下的檔案大家自己瀏覽,拷貝一份到自己的工作目錄,等會打包時會用到。
頁面程式碼這樣就差不多搞定了,接下來我們進入 webpack 的安裝和配置階段。現在我們還沒有講 webpack 配置所以頁面還無法訪問,等會弄好 webpack 配置後再看頁面實際效果。
安裝 webpack 和 Babel
我們把 webpack 和它的外掛安裝到專案:
npm install webpack webpack-cli webpack-serve html-webpack-plugin html-loader css-loader style-loader file-loader url-loader --save-dev
複製程式碼
webpack 即 webpack 核心庫。它提供了很多 API, 通過 Node.js 指令碼中 require('webpack')
的方式來使用 webpack。
webpack-cli 是 webpack 的命令列工具。讓我們可以不用寫打包指令碼,只需配置打包配置檔案,然後在命令列輸入 webpack-cli --config webpack.config.js
來使用 webpack, 簡單很多。webpack 4 之前命令列工具是整合在 webpack 包中的,4.0 開始 webpack 包本身不再整合 cli。
webpack-serve 是 webpack 提供的用來開發除錯的伺服器,讓你可以用 http://127.0.0.1:8080/ 這樣的 url 開啟頁面來除錯,有了它就不用配置 nginx 了,方便很多。
html-webpack-plugin, html-loader, css-loader, style-loader 等看名字就知道是打包 html 檔案,css 檔案的外掛,大家在這裡可能會有疑問,html-webpack-plugin
和 html-loader
有什麼區別,css-loader
和 style-loader
有什麼區別,我們等會看配置檔案的時候再講。
file-loader 和 url-loader 是打包二進位制檔案的外掛,具體也在配置檔案章節講解。
接下來,為了能讓不支援 ES6 的瀏覽器 (比如 IE) 也能照常執行,我們需要安裝 babel, 它會把我們寫的 ES6 原始碼轉化成 ES5,這樣我們原始碼寫 ES6,打包時生成 ES5。
npm install babel-core babel-preset-env babel-loader --save-dev
複製程式碼
這裡 babel-core
顧名思義是 babel 的核心編譯器。babel-preset-env 是一個配置檔案,我們可以使用這個配置檔案轉換 ES2015/ES2016/ES2017 到 ES5,是的,不只 ES6 哦。babel 還有 其他配置檔案。
光安裝了 babel-preset-env
,在打包時是不會生效的,需要在 package.json
加入 babel
配置:
{
"babel": {
"presets": ["env"]
}
}
複製程式碼
打包時 babel 會讀取 package.json
中 babel
欄位的內容,然後執行相應的轉換。
babel-loader 是 webpack 的外掛,我們下面章節再說。
配置 webpack
包都裝好了,接下來總算可以進入正題了。我們來建立 webpack 配置檔案 webpack.config.js
,注意這個檔案是在 node.js 中執行的,因此不支援 ES6 的 import
語法。我們來看檔案內容:
const { resolve } = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const history = require('connect-history-api-fallback')
const convert = require('koa-connect')
// 使用 WEBPACK_SERVE 環境變數檢測當前是否是在 webpack-server 啟動的開發環境中
const dev = Boolean(process.env.WEBPACK_SERVE)
module.exports = {
/*
webpack 執行模式
development:開發環境,它會在配置檔案中插入除錯相關的選項,比如 moduleId 使用檔案路徑方便除錯
production:生產環境,webpack 會將程式碼做壓縮等優化
*/
mode: dev ? 'development' : 'production',
/*
配置 source map
開發模式下使用 cheap-module-eval-source-map, 生成的 source map 能和原始碼每行對應,方便打斷點除錯
生產模式下使用 hidden-source-map, 生成獨立的 source map 檔案,並且不在 js 檔案中插入 source map 路徑,用於在 error report 工具中檢視 (比如 Sentry)
*/
devtool: dev ? 'cheap-module-eval-source-map' : 'hidden-source-map',
// 配置頁面入口 js 檔案
entry: './src/index.js',
// 配置打包輸出相關
output: {
// 打包輸出目錄
path: resolve(__dirname, 'dist'),
// 入口 js 的打包輸出檔名
filename: 'index.js'
},
module: {
/*
配置各種型別檔案的載入器,稱之為 loader
webpack 當遇到 import ... 時,會呼叫這裡配置的 loader 對引用的檔案進行編譯
*/
rules: [
{
/*
使用 babel 編譯 ES6 / ES7 / ES8 為 ES5 程式碼
使用正規表示式匹配字尾名為 .js 的檔案
*/
test: /\.js$/,
// 排除 node_modules 目錄下的檔案,npm 安裝的包不需要編譯
exclude: /node_modules/,
/*
use 指定該檔案的 loader, 值可以是字串或者陣列。
這裡先使用 eslint-loader 處理,返回的結果交給 babel-loader 處理。loader 的處理順序是從最後一個到第一個。
eslint-loader 用來檢查程式碼,如果有錯誤,編譯的時候會報錯。
babel-loader 用來編譯 js 檔案。
*/
use: ['babel-loader', 'eslint-loader']
},
{
// 匹配 html 檔案
test: /\.html$/,
/*
使用 html-loader, 將 html 內容存為 js 字串,比如當遇到
import htmlString from './template.html';
template.html 的檔案內容會被轉成一個 js 字串,合併到 js 檔案裡。
*/
use: 'html-loader'
},
{
// 匹配 css 檔案
test: /\.css$/,
/*
先使用 css-loader 處理,返回的結果交給 style-loader 處理。
css-loader 將 css 內容存為 js 字串,並且會把 background, @font-face 等引用的圖片,
字型檔案交給指定的 loader 打包,類似上面的 html-loader, 用什麼 loader 同樣在 loaders 物件中定義,等會下面就會看到。
*/
use: ['style-loader', 'css-loader']
},
{
/*
匹配各種格式的圖片和字型檔案
上面 html-loader 會把 html 中 <img> 標籤的圖片解析出來,檔名匹配到這裡的 test 的正規表示式,
css-loader 引用的圖片和字型同樣會匹配到這裡的 test 條件
*/
test: /\.(png|jpg|jpeg|gif|eot|ttf|woff|woff2|svg|svgz)(\?.+)?$/,
/*
使用 url-loader, 它接受一個 limit 引數,單位為位元組(byte)
當檔案體積小於 limit 時,url-loader 把檔案轉為 Data URI 的格式內聯到引用的地方
當檔案大於 limit 時,url-loader 會呼叫 file-loader, 把檔案儲存到輸出目錄,並把引用的檔案路徑改寫成輸出後的路徑
比如 views/foo/index.html 中
<img src="smallpic.png">
會被編譯成
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAA...">
而
<img src="largepic.png">
會被編譯成
<img src="/f78661bef717cf2cc2c2e5158f196384.png">
*/
use: [
{
loader: 'url-loader',
options: {
limit: 10000
}
}
]
}
]
},
/*
配置 webpack 外掛
plugin 和 loader 的區別是,loader 是在 import 時根據不同的檔名,匹配不同的 loader 對這個檔案做處理,
而 plugin, 關注的不是檔案的格式,而是在編譯的各個階段,會觸發不同的事件,讓你可以干預每個編譯階段。
*/
plugins: [
/*
html-webpack-plugin 用來打包入口 html 檔案
entry 配置的入口是 js 檔案,webpack 以 js 檔案為入口,遇到 import, 用配置的 loader 載入引入檔案
但作為瀏覽器開啟的入口 html, 是引用入口 js 的檔案,它在整個編譯過程的外面,
所以,我們需要 html-webpack-plugin 來打包作為入口的 html 檔案
*/
new HtmlWebpackPlugin({
/*
template 引數指定入口 html 檔案路徑,外掛會把這個檔案交給 webpack 去編譯,
webpack 按照正常流程,找到 loaders 中 test 條件匹配的 loader 來編譯,那麼這裡 html-loader 就是匹配的 loader
html-loader 編譯後產生的字串,會由 html-webpack-plugin 儲存為 html 檔案到輸出目錄,預設檔名為 index.html
可以通過 filename 引數指定輸出的檔名
html-webpack-plugin 也可以不指定 template 引數,它會使用預設的 html 模板。
*/
template: './src/index.html',
/*
因為和 webpack 4 的相容性問題,chunksSortMode 引數需要設定為 none
https://github.com/jantimon/html-webpack-plugin/issues/870
*/
chunksSortMode: 'none'
})
]
}
/*
配置開發時用的伺服器,讓你可以用 http://127.0.0.1:8080/ 這樣的 url 開啟頁面來除錯
並且帶有熱更新的功能,打程式碼時儲存一下檔案,瀏覽器會自動重新整理。比 nginx 方便很多
如果是修改 css, 甚至不需要重新整理頁面,直接生效。這讓像彈框這種需要點選互動後才會出來的東西除錯起來方便很多。
因為 webpack-cli 無法正確識別 serve 選項,使用 webpack-cli 執行打包時會報錯。
因此我們在這裡判斷一下,僅當使用 webpack-serve 時插入 serve 選項。
issue:https://github.com/webpack-contrib/webpack-serve/issues/19
*/
if (dev) {
module.exports.serve = {
// 配置監聽埠,預設值 8080
port: 8080,
// add: 用來給伺服器的 koa 例項注入 middleware 增加功能
add: app => {
/*
配置 SPA 入口
SPA 的入口是一個統一的 html 檔案,比如
http://localhost:8080/foo
我們要返回給它
http://localhost:8080/index.html
這個檔案
*/
app.use(convert(history()))
}
}
}
複製程式碼
走一個
配置 OK 了,接下來我們就執行一下吧。我們先試一下開發環境用的 webpack-serve
:
./node_modules/.bin/webpack-serve webpack.config.js
複製程式碼
執行時需要指定配置檔案。
上面的命令適用於 Mac / Linux 等 * nix 系統,也適用於 Windows 上的 PowerShell 和 bash/zsh 環境(Windows Subsystem for Linux, Git Bash、Babun、MSYS2 等)。安利一下 Windows 同學使用 Ubuntu on Windows,可以避免很多跨平臺的問題,比如設定環境變數。
如果使用 Windows 的 cmd.exe,請執行:
node_modules\.bin\webpack-serve webpack.config.js
複製程式碼
npm 會把包的可執行檔案安裝到 ./node_modules/.bin/
目錄下,所以我們要在這個目錄下執行命令。
命令執行後,控制檯顯示:
「wdm」: Compiled successfully。
複製程式碼
這就代表編譯成功了,我們可以在瀏覽器開啟 http://localhost:8080/
看看效果。如果有報錯,那可能是什麼地方沒弄對?請自己仔細檢查一下~
我們可以隨意更改一下 src 目錄下的原始碼,儲存後,瀏覽器裡的頁面應該很快會有相應變化。
要退出編譯,按 ctrl+c
。
開發環境編譯試過之後,我們試試看編譯生產環境的程式碼,命令是:
./node_modules/.bin/webpack-cli
複製程式碼
不需要指定配置檔案,預設讀取 webpack.config.js
執行指令碼的命令有點麻煩,因此,我們可以利用 npm,把命令寫在 package.json
中:
{
"scripts": {
"dev": "webpack-serve webpack.config.js",
"build": "webpack-cli"
}
}
複製程式碼
package.json
中的 scripts
物件,可以用來寫一些指令碼命令,命令不需要字首目錄 ./node_modules/.bin/
,npm 會自動尋找該目錄下的命令。我們可以執行:
npm run dev
複製程式碼
來啟動開發環境。
執行
npm run build
複製程式碼
來打包生產環境的程式碼。
進階配置
上面的專案雖然可以跑起來了,但有幾個點我們還沒有考慮到:
- 設定靜態資源的 url 路徑字首
- 各個頁面分開打包
- 第三方庫和業務程式碼分開打包
- 輸出的 entry 檔案加上 hash
- 開發環境關閉 performance.hints
- 配置 favicon
- 開發環境允許其他電腦訪問
- 打包時自定義部分引數
- webpack-serve 處理路徑帶字尾名的檔案的特殊規則
- 程式碼中插入環境變數
- 簡化 import 路徑
- 優化 babel 編譯後的程式碼效能
- 使用 webpack 自帶的 ES6 模組處理功能
- 使用 autoprefixer 自動建立 css 的 vendor prefixes
那麼,讓我們在上面的配置的基礎上繼續完善,下面的程式碼我們只寫出改變的部分。程式碼在 examples/advanced 目錄。
設定靜態資源的 url 路徑字首
現在我們的資原始檔的 url 直接在根目錄,比如 http://127.0.0.1:8080/index.js
, 這樣做快取控制和 CDN 不是很方便,因此我們給資原始檔的 url 加一個字首,比如 http://127.0.0.1:8080/assets/index.js
. 我們來修改一下 webpack 配置:
{
output: {
publicPath: '/assets/'
}
}
複製程式碼
webpack-serve
也需要修改:
if (dev) {
module.exports.serve = {
port: 8080,
host: '0.0.0.0',
dev: {
/*
指定 webpack-dev-middleware 的 publicpath
一般情況下與 output.publicPath 保持一致(除非 output.publicPath 使用的是相對路徑)
https://github.com/webpack/webpack-dev-middleware#publicpath
*/
publicPath: '/assets/'
},
add: app => {
app.use(convert(history({
index: '/assets/' // index.html 檔案在 /assets/ 路徑下
})))
}
}
}
複製程式碼
各個頁面分開打包
這樣瀏覽器只需載入當前頁面所需的程式碼。
webpack 可以使用非同步載入檔案的方式引用模組,我們使用 async/ await 和 dynamic import 來實現:
src/router.js:
// 將 async/await 轉換成 ES5 程式碼後需要這個執行時庫來支援
import 'regenerator-runtime/runtime'
const routes = {
// import() 返回 promise
'/foo': () => import('./views/foo'),
'/bar.do': () => import('./views/bar.do')
}
class Router {
// ...
// 載入 path 路徑的頁面
// 使用 async/await 語法
async load(path) {
// 首頁
if (path === '/') path = '/foo'
// 動態載入頁面
const View = (await routes[path]()).default
// 建立頁面例項
const view = new View()
// 呼叫頁面方法,把頁面載入到 document.body 中
view.mount(document.body)
}
}
複製程式碼
這樣我們就不需要在開頭把所有頁面檔案都 import 進來了。
因為 import()
還沒有正式進入標準,需要使用 babel-preset-stage-2 來支援:
npm install babel-preset-stage-2 --save-dev
複製程式碼
package.json
改一下:
{
"babel": {
"presets": [
"env",
"stage-2"
]
}
}
複製程式碼
然後修改 webpack 配置:
{
output: {
/*
程式碼中引用的檔案(js、css、圖片等)會根據配置合併為一個或多個包,我們稱一個包為 chunk。
每個 chunk 包含多個 modules。無論是否是 js,webpack 都將引入的檔案視為一個 module。
chunkFilename 用來配置這個 chunk 輸出的檔名。
[chunkhash]:這個 chunk 的 hash 值,檔案發生變化時該值也會變。使用 [chunkhash] 作為檔名可以防止瀏覽器讀取舊的快取檔案。
還有一個佔位符 [id],編譯時每個 chunk 會有一個id。
我們在這裡不使用它,因為這個 id 是個遞增的數字,增加或減少一個chunk,都可能導致其他 chunk 的 id 發生改變,導致快取失效。
*/
chunkFilename: '[chunkhash].js',
}
}
複製程式碼
第三方庫和業務程式碼分開打包
這樣更新業務程式碼時可以藉助瀏覽器快取,使用者不需要重新下載沒有發生變化的第三方庫。 Webpack 4 最大的改進便是自動拆分 chunk, 如果同時滿足下列條件,chunk 就會被拆分:
- 新的 chunk 能被複用,或者模組是來自 node_modules 目錄
- 新的 chunk 大於 30Kb(min+gz 壓縮前)
- 按需載入 chunk 的併發請求數量小於等於 5 個
- 頁面初始載入時的併發請求數量小於等於 3 個
一般情況只需配置這幾個引數即可:
{
plugins: [
// ...
/*
使用檔案路徑的 hash 作為 moduleId。
雖然我們使用 [chunkhash] 作為 chunk 的輸出名,但仍然不夠。
因為 chunk 內部的每個 module 都有一個 id,webpack 預設使用遞增的數字作為 moduleId。
如果引入了一個新檔案或刪掉一個檔案,可能會導致其他檔案的 moduleId 也發生改變,
那麼受影響的 module 所在的 chunk 的 [chunkhash] 就會發生改變,導致快取失效。
因此使用檔案路徑的 hash 作為 moduleId 來避免這個問題。
*/
new webpack.HashedModuleIdsPlugin()
],
optimization: {
/*
上面提到 chunkFilename 指定了 chunk 打包輸出的名字,那麼檔名存在哪裡了呢?
它就存在引用它的檔案中。這意味著一個 chunk 檔名發生改變,會導致引用這個 chunk 檔案也發生改變。
runtimeChunk 設定為 true, webpack 就會把 chunk 檔名全部存到一個單獨的 chunk 中,
這樣更新一個檔案只會影響到它所在的 chunk 和 runtimeChunk,避免了引用這個 chunk 的檔案也發生改變。
*/
runtimeChunk: true,
splitChunks: {
/*
預設 entry 的 chunk 不會被拆分
因為我們使用了 html-webpack-plugin 來動態插入 <script> 標籤,entry 被拆成多個 chunk 也能自動被插入到 html 中,
所以我們可以配置成 all, 把 entry chunk 也拆分了
*/
chunks: 'all'
}
}
}
複製程式碼
webpack 4 支援更多的手動優化,詳見: https://gist.github.com/sokra/1522d586b8e5c0f5072d7565c2bee693
但正如 webpack 文件中所說,預設配置已經足夠優化,在沒有測試的情況下不要盲目手動優化。
輸出的 entry 檔案加上 hash
上面我們提到了 chunkFilename
使用 [chunkhash]
防止瀏覽器讀取錯誤快取,那麼 entry 同樣需要加上 hash。
但使用 webpack-serve
啟動開發環境時,entry 檔案是沒有 [chunkhash]
的,用了會報錯。
因此我們只在執行 webpack-cli
時使用 [chunkhash]
。
{
output: {
filename: dev ? '[name].js' : '[chunkhash].js'
}
}
複製程式碼
這裡我們使用了 [name]
佔位符。解釋它之前我們先了解一下 entry
的完整定義:
{
entry: {
NAME: [FILE1, FILE2, ...]
}
}
複製程式碼
我們可以定義多個 entry 檔案,比如你的專案有多個 html 入口檔案,每個 html 對應一個或多個 entry 檔案。 然後每個 entry 可以定義由多個 module 組成,這些 module 會依次執行。 在 webpack 4 之前,這是很有用的功能,比如之前提到的第三方庫和業務程式碼分開打包,在以前,我們需要這麼配置:
{
entry {
main: './src/index.js',
vendor: ['jquery', 'lodash']
}
}
複製程式碼
entry 引用檔案的規則和 import
是一樣的,會尋找 node_modules
裡的包。然後結合 CommonsChunkPlugin
把 vendor 定義的 module 從業務程式碼分離出來打包成一個單獨的 chunk。
如果 entry 是一個 module,我們可以不使用陣列的形式。
在 simple 專案中,我們配置了 entry: './src/index.js'
,這是最簡單的形式,轉換成完整的寫法就是:
{
entry: {
main: ['./src/index.js']
}
}
複製程式碼
webpack 會給這個 entry 指定名字為 main
。
看到這應該知道 [name]
的意思了吧?它就是 entry 的名字。
有人可能注意到官網文件中還有一個 [hash]
佔位符,這個 hash 是整個編譯過程產生的一個總的 hash 值,而不是單個檔案的 hash 值,專案中任何一個檔案的改動,都會造成這個 hash 值的改變。[hash]
佔位符是始終存在的,但我們不希望修改一個檔案導致所有輸出的檔案 hash 都改變,這樣就無法利用瀏覽器快取了。因此這個 [hash]
意義不大。
開發環境關閉 performance.hints
我們注意到執行開發環境是命令列會報一段 warning:
WARNING in asset size limit: The following asset(s) exceed the recommended size limit (250 kB).
This can impact web performance.
複製程式碼
這是說建議每個輸出的 js 檔案的大小不要超過 250k。但開發環境因為包含了 sourcemap 並且程式碼未壓縮所以一般都會超過這個大小,所以我們可以在開發環境把這個 warning 關閉。
webpack 配置中加入:
{
performance: {
hints: dev ? false : 'warning'
}
}
複製程式碼
配置 favicon
在 src 目錄中放一張 favicon.png,然後 src/index.html
的 <head>
中插入:
<link rel="icon" type="image/png" href="favicon.png">
複製程式碼
修改 webpack 配置:
{
module: {
rules: [
{
test: /\.html$/,
use: [
{
loader: 'html-loader',
options: {
/*
html-loader 接受 attrs 引數,表示什麼標籤的什麼屬性需要呼叫 webpack 的 loader 進行打包。
比如 <img> 標籤的 src 屬性,webpack 會把 <img> 引用的圖片打包,然後 src 的屬性值替換為打包後的路徑。
使用什麼 loader 程式碼,同樣是在 module.rules 定義中使用匹配的規則。
如果 html-loader 不指定 attrs 引數,預設值是 img:src, 意味著會預設打包 <img> 標籤的圖片。
這裡我們加上 <link> 標籤的 href 屬性,用來打包入口 index.html 引入的 favicon.png 檔案。
*/
attrs: ['img:src', 'link:href']
}
}
]
},
{
/*
匹配 favicon.png
上面的 html-loader 會把入口 index.html 引用的 favicon.png 圖示檔案解析出來進行打包
打包規則就按照這裡指定的 loader 執行
*/
test: /favicon\.png$/,
use: [
{
// 使用 file-loader
loader: 'file-loader',
options: {
/*
name:指定檔案輸出名
[hash] 為原始檔的hash值,[ext] 為字尾。
*/
name: '[hash].[ext]'
}
}
]
},
// 圖片檔案的載入配置增加一個 exclude 引數
{
test: /\.(png|jpg|jpeg|gif|eot|ttf|woff|woff2|svg|svgz)(\?.+)?$/,
// 排除 favicon.png, 因為它已經由上面的 loader 處理了。如果不排除掉,它會被這個 loader 再處理一遍
exclude: /favicon\.png$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10000
}
}
]
}
]
}
}
複製程式碼
其實 html-webpack-plugin 接受一個 favicon
引數,可以指定 favicon 檔案路徑,會自動打包插入到 html 檔案中。但它有個 bug,打包後的檔名路徑不帶 hash,就算有 hash,它也是 [hash],而不是 [chunkhash]。導致修改程式碼也會改變 favicon 打包輸出的檔名。issue 中提到的 favicons-webpack-plugin 倒是可以用,但它依賴 PhantomJS, 非常大。
開發環境允許其他電腦訪問
const internalIp = require('internal-ip')
module.exports.serve = {
host: '0.0.0.0',
hot: {
host: {
client: internalIp.v4.sync(),
server: '0.0.0.0'
}
},
// ...
}
複製程式碼
打包時自定義部分引數
在多人開發時,每個人可能需要有自己的配置,比如說 webpack-serve 監聽的埠號,如果寫死在 webpack 配置裡,而那個埠號在某個同學的電腦上被其他程式佔用了,簡單粗暴的修改 webpack.config.js
會導致提交程式碼後其他同學的埠也被改掉。
還有一點就是開發環境、測試環境、生產環境的部分 webpack 配置是不同的,比如 publicPath
在生產環境可能要配置一個 CDN 地址。
我們在根目錄建立一個資料夾 config
,裡面建立 3 個配置檔案:
- default.js: 生產環境
module.exports = {
publicPath: 'http://cdn.example.com/assets/'
}
複製程式碼
- dev.js: 預設開發環境
module.exports = {
publicPath: '/assets/',
serve: {
port: 8090
}
}
複製程式碼
- local.js: 個人本地環境,在 dev.js 基礎上修改部分引數。
const config = require('./dev')
config.serve.port = 8070
module.exports = config
複製程式碼
package.json
修改 scripts
:
{
"scripts": {
"local": "npm run webpack-serve --config=local",
"dev": "npm run webpack-serve --config=dev",
"webpack-serve": "webpack-serve webpack.config.js",
"build": "webpack-cli"
}
}
複製程式碼
webpack 配置修改:
// ...
const url = require('url')
const config = require('./config/' + (process.env.npm_config_config || 'default'))
module.exports = {
// ...
output: {
// ...
publicPath: config.publicPath
}
// ...
}
if (dev) {
module.exports.serve = {
host: '0.0.0.0',
port: config.serve.port,
dev: {
publicPath: config.publicPath
},
add: app => {
app.use(convert(history({
index: url.parse(config.publicPath).pathname
})))
}
}
}
複製程式碼
這裡的關鍵是 npm run
傳進來的自定義引數可以通過 process.env.npm_config_*
獲得。引數中如果有 -
會被轉成 _
。
還有一點,我們不需要把自己個人用的配置檔案提交到 git,所以我們在 .gitignore
中加入:
config/*
!config/default.js
!config/dev.js
複製程式碼
把 config
目錄排除掉,但是保留生產環境和 dev 預設配置檔案。
可能有同學注意到了 webpack-cli
可以通過 --env 的方式從命令列傳參給指令碼,遺憾的是 webpack-cli
不支援。
webpack-serve 處理帶字尾名的檔案的特殊規則
當處理帶字尾名的請求時,比如 http://localhost:8080/bar.do ,connect-history-api-fallback
會認為它應該是一個實際存在的檔案,就算找不到該檔案,也不會 fallback 到 index.html,而是返回 404。但在 SPA 應用中這不是我們希望的。
幸好有一個配置選項 disableDotRule: true
可以禁用這個規則,使帶字尾的檔案當不存在時也能 fallback 到 index.html
module.exports.serve = {
// ...
add: app => {
app.use(convert(history({
// ...
disableDotRule: true,
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'] // 需要配合 disableDotRule 一起使用
})))
}
}
複製程式碼
程式碼中插入環境變數
在業務程式碼中,有些變數在開發環境和生產環境是不同的,比如域名、後臺 API 地址等。還有開發環境可能需要列印除錯資訊等。
我們可以使用 DefinePlugin 外掛在打包時往程式碼中插入需要的環境變數。
// ...
const pkgInfo = require('./package.json')
module.exports = {
// ...
plugins: [
new webpack.DefinePlugin({
DEBUG: dev,
VERSION: JSON.stringify(pkgInfo.version),
CONFIG: JSON.stringify(config.runtimeConfig)
}),
// ...
]
}
複製程式碼
DefinePlugin 外掛的原理很簡單,如果我們在程式碼中寫:
console.log(DEBUG)
複製程式碼
它會做類似這樣的處理:
'console.log(DEBUG)'.replace('DEBUG', true)
複製程式碼
最後生成:
console.log(true)
複製程式碼
這裡有一點需要注意,像這裡的 VERSION
, 如果我們不對 pkgInfo.version
做 JSON.stringify()
,
console.log(VERSION)
複製程式碼
然後做替換操作:
'console.log(VERSION)'.replace('VERSION', '1.0.0')
複製程式碼
最後生成:
console.log(1.0.0)
複製程式碼
這樣語法就錯誤了。所以,我們需要 JSON.stringify(pkgInfo.version)
轉一下變成 '"1.0.0"'
,替換的時候才會帶引號。
還有一點,webpack 打包壓縮的時候,會把程式碼進行優化,比如:
if (DEBUG) {
console.log('debug mode')
} else {
console.log('production mode')
}
複製程式碼
會被編譯成:
if (false) {
console.log('debug mode')
} else {
console.log('production mode')
}
複製程式碼
然後壓縮優化為:
console.log('production mode')
複製程式碼
簡化 import 路徑
檔案 a 引入檔案 b 時,b 的路徑是相對於 a 檔案所在目錄的。如果 a 和 b 在不同的目錄,藏得又深,寫起來就會很麻煩:
import b from '../../../components/b'
複製程式碼
為了方便,我們可以定義一個路徑別名(alias):
resolve: {
alias: {
'~': resolve(__dirname, 'src')
}
}
複製程式碼
這樣,我們可以以 src
目錄為基礎路徑來 import
檔案:
import b from '~/components/b'
複製程式碼
html 中的 <img>
標籤沒法使用這個別名功能,但 html-loader
有一個 root
引數,可以使 /
開頭的檔案相對於 root
目錄解析。
{
test: /\.html$/,
use: [
{
loader: 'html-loader',
options: {
root: resolve(__dirname, 'src'),
attrs: ['img:src', 'link:href']
}
}
]
}
複製程式碼
那麼,<img src="/favicon.png">
就能順利指向到 src 目錄下的 favicon.png 檔案,不需要關心當前檔案和目標檔案的相對路徑。
PS: 在除錯 <img>
標籤的時候遇到一個坑,html-loader
會解析 <!-- -->
註釋中的內容,之前在註釋中寫的
<!--
大於 10kb 的圖片,圖片會被儲存到輸出目錄,src 會被替換為打包後的路徑
<img src="/assets/f78661bef717cf2cc2c2e5158f196384.png">
-->
複製程式碼
之前因為沒有加 root
引數,所以 /
開頭的檔名不會被解析,加了 root
導致編譯時報錯,找不到該檔案。大家記住這一點。
優化 babel 編譯後的程式碼效能
babel 編譯後的程式碼一般會造成效能損失,babel 提供了一個 loose 選項,使編譯後的程式碼不需要完全遵循 ES6 規定,簡化編譯後的程式碼,提高程式碼執行效率:
package.json:
{
"babel": {
"presets": [
[
"env",
{
"loose": true
}
],
"stage-2"
]
}
}
複製程式碼
但這麼做會有相容性的風險,可能會導致 ES6 原始碼理應的執行結果和編譯後的 ES5 程式碼的實際結果並不一致。如果程式碼沒有遇到實際的效率瓶頸,官方 不建議 使用 loose
模式。
使用 webpack 自帶的 ES6 模組處理功能
我們目前的配置,babel 會把 ES6 模組定義轉為 CommonJS 定義,但 webpack 自己可以處理 import
和 export
, 而且 webpack 處理 import
時會做程式碼優化,把沒用到的部分程式碼刪除掉。因此我們通過 babel 提供的 modules: false
選項把 ES6 模組轉為 CommonJS 模組的功能給關閉掉。
package.json:
{
"babel": {
"presets": [
[
"env",
{
"loose": true,
"modules": false
}
],
"stage-2"
]
}
}
複製程式碼
使用 autoprefixer 自動建立 css 的 vendor prefixes
css 有一個很麻煩的問題就是比較新的 css 屬性在各個瀏覽器裡是要加字首的,我們可以使用 autoprefixer 工具自動建立這些瀏覽器規則,那麼我們的 css 中只需要寫:
:fullscreen a {
display: flex
}
複製程式碼
autoprefixer 會編譯成:
:-webkit-full-screen a {
display: -webkit-box;
display: flex
}
:-moz-full-screen a {
display: flex
}
:-ms-fullscreen a {
display: -ms-flexbox;
display: flex
}
:fullscreen a {
display: -webkit-box;
display: -ms-flexbox;
display: flex
}
複製程式碼
首先,我們用 npm 安裝它:
npm install postcss-loader autoprefixer --save-dev
複製程式碼
autoprefixer 是 postcss 的一個外掛,所以我們也要安裝 postcss 的 webpack loader。
修改一下 webpack 的 css rule:
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader']
}
複製程式碼
然後建立檔案 postcss.config.js
:
module.exports = {
plugins: [
require('autoprefixer')()
]
}
複製程式碼
使用 webpack 打包多頁面應用(Multiple-Page Application)
多頁面網站同樣可以用 webpack 來打包,以便使用 npm 包,import()
,code splitting
等好處。
MPA 意味著並沒不是一個單一的 html 入口和 js 入口,而是每個頁面對應一個 html 和多個 js。那麼我們可以把專案結構設計為:
├── dist
├── package.json
├── node_modules
├── src
│ ├── components
│ ├── shared
| ├── favicon.png
│ └── pages 頁面放這裡
| ├── foo 編譯後生成 http://localhost:8080/foo.html
| | ├── index.html
| | ├── index.js
| | ├── style.css
| | └── pic.png
| └── bar http://localhost:8080/bar.html
| ├── index.html
| ├── index.js
| ├── style.css
| └── baz http://localhost:8080/bar/baz.html
| ├── index.html
| ├── index.js
| └── style.css
└── webpack.config.js
複製程式碼
這裡每個頁面的 index.html
是個完整的從 <!DOCTYPE html>
開頭到 </html>
結束的頁面,這些檔案都要用 html-webpack-plugin
處理。index.js
是每個頁面的業務邏輯,作為每個頁面的入口 js 配置到 entry
中。這裡我們需要用 glob
庫來把這些檔案都篩選出來批量操作。為了使用 webpack 4 的 optimization.splitChunks
和 optimization.runtimeChunk
功能,我寫了 html-webpack-include-sibling-chunks-plugin 外掛來配合使用。還要裝幾個外掛把 css 壓縮並放到 <head>
中。
npm install glob html-webpack-include-sibling-chunks-plugin uglifyjs-webpack-plugin mini-css-extract-plugin optimize-css-assets-webpack-plugin --save-dev
複製程式碼
webpack.config.js
修改的地方:
// ...
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const HtmlWebpackIncludeSiblingChunksPlugin = require('html-webpack-include-sibling-chunks-plugin')
const glob = require('glob')
const dev = Boolean(process.env.WEBPACK_SERVE)
const config = require('./config/' + (process.env.npm_config_config || 'default'))
const entries = glob.sync('./src/**/index.js')
const entry = {}
const htmlPlugins = []
for (const path of entries) {
const template = path.replace('index.js', 'index.html')
const chunkName = path.slice('./src/pages/'.length, -'/index.js'.length)
entry[chunkName] = dev ? [path, template] : path
htmlPlugins.push(new HtmlWebpackPlugin({
template,
filename: chunkName + '.html',
chunksSortMode: 'none',
chunks: [chunkName]
}))
}
module.exports = {
entry,
output: {
path: resolve(__dirname, 'dist'),
// 我們不定義 publicPath,否則訪問 html 時需要帶上 publicPath 字首
filename: dev ? '[name].js' : '[chunkhash].js',
chunkFilename: '[chunkhash].js'
},
optimization: {
runtimeChunk: true,
splitChunks: {
chunks: 'all'
},
minimizer: dev ? [] : [
new UglifyJsPlugin({
cache: true,
parallel: true,
sourceMap: true
}),
new OptimizeCSSAssetsPlugin()
]
},
module: {
rules: [
// ...
{
test: /\.css$/,
use: [dev ? 'style-loader' : MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
},
// ...
]
},
plugins: [
// ...
/*
這裡不使用 [chunkhash]
因為從同一個 chunk 抽離出來的 css 共享同一個 [chunkhash]
[contenthash] 你可以簡單理解為 moduleId + content 生成的 hash
因此一個 chunk 中的多個 module 有自己的 [contenthash]
*/
new MiniCssExtractPlugin({
filename: '[contenthash].css',
chunkFilename: '[contenthash].css'
}),
// 必須放在html-webpack-plugin前面
new HtmlWebpackIncludeSiblingChunksPlugin(),
...htmlPlugins
],
// ...
}
複製程式碼
entry
和 htmlPlugins
會通過遍歷 pages 目錄生成,比如:
entry:
{
'bar/baz': './src/pages/bar/baz/index.js',
bar: './src/pages/bar/index.js',
foo: './src/pages/foo/index.js'
}
複製程式碼
在開發環境中,為了能夠修改 html 檔案後網頁能夠自動重新整理,我們還需要把 html 檔案也加入 entry 中,比如:
{
foo: ['./src/pages/foo/index.js', './src/pages/foo/index.html']
}
複製程式碼
這樣,當 foo 頁面的 index.js 或 index.html 檔案改動時,都會觸發瀏覽器重新整理該頁面。雖然把 html 加入 entry 很奇怪,但放心,不會導致錯誤。記得不要在生產環境這麼做,不然導致 chunk 檔案包含了無用的 html 片段。
htmlPlugins:
[
new HtmlWebpackPlugin({
template: './src/pages/bar/baz/index.html',
filename: 'bar/baz.html',
chunksSortMode: 'none',
chunks: ['bar/baz']
},
new HtmlWebpackPlugin({
template: './src/pages/bar/index.html',
filename: 'bar.html',
chunksSortMode: 'none',
chunks: ['bar']
},
new HtmlWebpackPlugin({
template: './src/pages/foo/index.html',
filename: 'foo.html',
chunksSortMode: 'none',
chunks: ['foo']
}
]
複製程式碼
程式碼在 examples/mpa 目錄。
總結
通過這篇文章,我想大家應該學會了 webpack 的正確開啟姿勢。雖然我沒有提及如何用 webpack 來編譯 React 和 vue.js, 但大家可以想到,無非是安裝一些 loader 和 plugin 來處理 jsx 和 vue 格式的檔案,那時難度就不在於 webpack 了,而是程式碼架構組織的問題了。具體的大家自己去摸索一下。
版權許可
本作品採用 知識共享署名 - 非商業性使用 4.0 國際許可協議 進行許可。