准備了挺久,一直想要好好深入了解一下Webpack,之前一直嫌棄Webpack麻煩,偏向於Parcel這種零配置的模塊打包工具一些,但是實際上還是Webpack比較靠譜,並且Webpack功能更加強大。由於上一次學習Webpack的時候並沒有了解過Node.js,所以很多時候真的感覺無能為力,連個__dirname
都覺得好復雜,學習過Node.js之后再來學習Webpack,就會好理解很多,這一次算是比較深入的了解一下Webpack,爭取以后能夠脫離create-react-app
或者Vue-Cli
這種腳手架工具,或者自己也能夠寫一套腳本自動配置開發環境。
由於寫這篇筆記的時候,Webpack已經發行了最新的Webpack 4.0,所以這篇筆記就算是學習Webpack 4.0的筆記吧,筆者所用版本是webpack 4.8.3,另外使用Webpack 4.x的命令行需要安裝單獨的命令行工具,筆者所使用的Webpack命令行工具是webpack-cli 2.1.3,學習的時候可以按照這個要求部署開發環境。
此外,在學習webpack之前,你最好對ES6、Node.js有一定的了解,最好使用過一個腳手架。
一、核心概念
Webpack具有四個核心的概念,想要入門Webpack就得先好好了解這四個核心概念。它們分別是Entry(入口)
、Output(輸出)
、loader
和Plugins(插件)
。接下來詳細介紹這四個核心概念。
1.Entry
Entry是Webpack的入口起點指示,它指示webpack應該從哪個模塊開始着手,來作為其構建內部依賴圖的開始。可以在配置文件(webpack.config.js)中配置entry屬性來指定一個或多個入口點,默認為./src
(webpack 4開始引入默認值)。
具體配置方法:
entry: string | Array<string>
前者一個單獨的string是配置單獨的入口文件,配置為后者(一個數組)時,是多文件入口。
另外還可以通過對象語法進行配置:
entry: { [entryChunkName]: string | Array<string> }
比如:
//webpack.config.js module.exports = { entry: { app: './app.js', vendors: './vendors.js' } };
以上配置表示從app和vendors屬性開始打包構建依賴樹,這樣做的好處在於分離自己開發的業務邏輯代碼和第三方庫的源碼,因為第三方庫安裝后,源碼基本就不再變化,這樣分開打包有利於提升打包速度,減少了打包文件的個數,Vue-Cli
采取的就是這種分開打包的模式。但是為了支持拆分代碼更好的DllPlugin插件,以上語法可能會被拋棄。
2.Output
Output屬性告訴webpack在哪里輸出它所創建的bundles,也可指定bundles的名稱,默認位置為./dist
。整個應用結構都會被編譯到指定的輸出文件夾中去,最基本的屬性包括filename
(文件名)和path
(輸出路徑)。
值得注意的是,即是你配置了多個入口文件,你也只能有一個輸出點。
具體配置方法:
output: { filename: 'bundle.js', path: '/home/proj/public/dist' }
值得注意的是,output.filename
必須是絕對路徑,如果是一個相對路徑,打包時webpack會拋出異常。
多個入口時,使用下面的語法輸出多個bundle:
// webpack.config.js module.exports = { entry: { app: './src/app.js', vendors: './src/vendors.js' }, output: { filename: '[name].js', path: __dirname + '/dist' } }
以上配置將會輸出打包后文件app.js和vendors.js到__dirname + '/dist'
下。
3.Loaders
loader可以理解為webpack的編譯器,它使得webpack可以處理一些非JavaScript文件,比如png、csv、xml、css、json等各種類型的文件,使用合適的loader可以讓JavaScript的import導入非JavaScript模塊。JavaScript只認為JavaScript文件是模塊,而webpack的設計思想即萬物皆模塊,為了使得webpack能夠認識其他“模塊”,所以需要loader這個“編譯器”。
webpack中配置loader有兩個目標:
- (1)test屬性:標志有哪些后綴的文件應該被處理,是一個正則表達式。
- (2)use屬性:指定test類型的文件應該使用哪個loader進行預處理。
比如webpack.config.js:
module.exports = { entry: '...', output: '...', module: { rules: [ { test: /\.css$/, use: 'css-loader' } ] } };
該配置文件指示了所有的css文件在import
時都應該經過css-loader處理,經過css-loader處理后,可以在JavaScript模塊中直接使用import
語句導入css模塊。但是使用css-loader
的前提是先使用npm安裝css-loader
。
此處需要注意的是定義loaders規則時,不是定義在對象的rules屬性上,而是定義在module屬性的rules屬性中。
配置多個loader:
有時候,導入一個模塊可能要先使用多個loader進行預處理,這時就要對指定類型的文件配置多個loader進行預處理,配置多個loader,把use屬性賦值為數組即可,webpack會按照數組中loader的先后順序,使用對應的loader依次對模塊文件進行預處理。
{ module: { rules: [ { test: /\.css$/, use: [ { loader: 'style-loader' }, { loader: 'css-loader' } ] } ] } }
此外,還可以使用內聯方式進行loader配置:
import Styles from 'style-loader!css-loader?modules!./style.css'
但是這不是推薦的方法,請盡量使用module.rules
進行配置。
4.Plugins
loader用於轉換非JavaScript類型的文件,而插件可以用於執行范圍更廣的任務,包括打包、優化、壓縮、搭建服務器等等,功能十分強大。要是用一個插件,一般是先使用npm包管理器進行安裝,然后在配置文件中引入,最后將其實例化后傳遞給plugins數組屬性。
插件是webpack的支柱功能,目前主要是解決loader無法實現的其他許多復雜功能,通過plugins
屬性使用插件:
// webpack.config.js const webpack = require('webpack'); module.exports = { plugins: [ new webpack.optimize.UglifyJsPlugin() ] }
向plugins屬性傳遞實例數組即可。
5.Mode
模式(Mode)可以通過配置對象的mode
屬性進行配置,主要值為production
或者development
。兩種模式的區別在於一個是為生產環境編譯打包,一個是為了開發環境編譯打包。生產環境模式下,webpack會自動對代碼進行壓縮等優化,省去了配置的麻煩。
學習完以上基本概念之后,基本也就入門webpack了,因為webpack的強大就是建立在這些基本概念之上,利用webpack多樣的loaders和plugins,可以實現強大的打包功能。
二、基本配置
按照以下步驟實現webpack簡單的打包功能:
-
(1)建立工程文件夾,位置和名稱隨意,並將cmd或者git bash的當前路徑切換到工程文件夾。
-
(2)安裝webpack和webpack-cli到開發環境:
npm install webpack webpack-cli --save-dev
-
(3)在工程文件夾下建立以下文件和目錄:
- /src
- index.js
- index.css
- /dist
- index.html
- webpack.config.js
- /src
-
(4)安裝
css-loader
:npm install css-loader --save-dev
-
(5)配置
webpack.config.js
:module.exports = { mode: 'development', entry: './src/index.js', output: { path: __dirname + '/dist', filename: 'bundle.js' }, module: { rules: [ { test: /\.css$/, use: 'css-loader' } ] } };
-
(6)在
index.html
中引入bundle.js
:<!--index.html--> <html> <head> <title>Test</title> <meta charset='utf-8'/> </head> <body> <h1>Hello World!</h1> </body> <script src='./bundle.js'></script> </html>
-
(7)在
index.js
中添加:import './index.css'; console.log('Success!');
-
(8)在工程目錄下,使用以下命令打包:
webpack
查看輸出結果,可以雙擊
/dist/index.html
查看有沒有報錯以及控制台的輸出內容。
三、如何通過Node腳本使用webpack?
webpack提供Node API,方便我們在Node腳本中使用webpack。
基本代碼如下:
// 引入webpack模塊。 const webpack = require('webpack'); // 引入配置信息。 const config = require('./webpack.config'); // 通過webpack函數直接傳入config配置信息。 const compiler = webpack(config); // 通過compiler對象的apply方法應用插件,也可在配置信息中配置插件。 compiler.apply(new webpack.ProgressPlugin()); // 使用compiler對象的run方法運行webpack,開始打包。 compiler.run((err, stats) => { if(err) { // 回調中接收錯誤信息。 console.error(err); } else { // 回調中接收打包成功的具體反饋信息。 console.log(stats); } });
動態生成index.html和bundle.js
動態生成是啥?動態生成就是指在打包后的模塊名稱內插入hash值,使得每一次生成的模塊具有不同的名稱,而index.html之所以要動態生成是因為每次打包生成的模塊名稱不同,所以在HTML文件內引用時也要更改script標簽,這樣才能保證每次都能引用到正確的JavaScript文件。
為什么要添加hash值?
之所以要動態生態生成bundle文件,是為了防止瀏覽器緩存機制阻礙文件的更新,在每次修改代碼之后,文件名中的hash都會發生改變,強制瀏覽器進行刷新,獲取當前最新的文件。
如何添加hash到bundle文件中?
只需要在設置output時,在output.filename
中添加[hash]
到文件名中即可,比如:
// webpack.config.js module.exports = { output: { path: __dirname + '/dist', filename: '[name].[hash].js' } };
現在可以動態生成bundle文件了,那么如何動態添加bundle到HTML文件呢?
每次打包bundle文件之后,其名稱都會發生更改,每次人為地修改對應的HTML文件以添加JavaScript文件引用實在是令人煩躁,這時需要使用到強大的webpack插件了,有一個叫html-webpack-plugin
的插件,可以自動生成HTML文件。安裝到開發環境:
npm install html-webpack-plugin --save-dev
安裝之后,在webpack.config.js
中引入,並添加其實例到插件屬性(plugins)中去:
// webpack.config.js const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { // other configs ... plugins: [ new HtmlWebpackPlugin({ // options配置 }) ] };
這時就可以看到每次生成bundle文件之后,都會被動態生成對應的html文件。
在上面的代碼中還可以看到HtmlWebpackPlugin
插件的構造函數還可以傳遞一個配置對象作為參數。比較有用的配置屬性有title
(指定HTML中title標簽的內容,及網頁標題)、template
(指定模板HTML文件)等等,其他更多具體參考信息請訪問:Html-Webpack-Plugin
四、清理/dist文件夾
由於每次生成的JavaScript文件都不同名,所以新的文件不會覆蓋舊的文件,而舊的文件一只會存在於/dist
文件夾中,隨着編譯次數的增加,這個文件夾會越來越膨脹,所以應該想辦法每次生成新的bundle文件之前清理/dist
文件夾,以確保文件夾的干凈整潔,有以下兩個較好的處理辦法:
如果你是Node腳本調用webpack打包:
如果通過Node API調用webpack進行打包,可以在打包之前直接使用Node的fs模塊刪除/dist
文件夾中的所有文件:
const webpack = require('webpack'); const config = require('./webpack.config'); const fs = require('fs'); const compiler = webpack(config); var deleteFolderRecursive = function(path) { if (fs.existsSync(path)) { fs.readdirSync(path).forEach(function(file, index){ var curPath = path + "/" + file; if (fs.lstatSync(curPath).isDirectory()) { // recurse deleteFolderRecursive(curPath); } else { // delete file fs.unlinkSync(curPath); } }); fs.rmdirSync(path); } }; deleteFolderRecursive(__dirname + '/dist'); compiler.run((err, stats) => { if(err) { console.error(err); } else { console.log(stats.hash); } });
可以看到在調用compiler.run
打包之前,先使用自定義的deleteFolderRecursive
方法刪除了/dist
目錄下的所有文件。
如果你使用webpack-cli進行打包
這時候就得通過webpack的插件完成這個任務了,用到的插件是clean-webpack-plugin
。
安裝:
npm install clean-webpack-plugin --save-dev
然后在webpack.config.js
文件中添加插件:
// webpack.config.js const CleanWebpackPlugin = require('clean-webpack-plugin'); module.exports = { plugins: [ new CleanWebpackPlugin(['dist']) ] };
之后再次打包,你會發現之前的打包文件全部被刪除了。
五、搭建開發環境
開發環境與生產環境存在許多的差異,生產環境更講究生產效率,因此代碼必須壓縮、精簡,必須去除一些生產環境並不需要用到的調試工具,只需要提高應用的效率和性能即可。開發環境更講究調試、測試,為了方便開發,我們需要搭建一個合適的開發環境。
(一)使用source maps進行調試
為何要使用source maps?
因為webpack對源代碼進行打包后,會對源代碼進行壓縮、精簡、甚至變量名替換,在瀏覽器中,無法對代碼逐行打斷點進行調試,所有需要使用source maps進行調試,它使得我們在瀏覽器中可以看到源代碼,進而逐行打斷點調試。
如何使用source maps?
在配置中添加devtool
屬性,賦值為source-map
或者inline-source-map
即可,后者報錯信息更加具體,會指示源代碼中的具體錯誤位置,而source-map
選項無法指示到源代碼中的具體位置。
(二)使用開發工具
每次寫完代碼保存之后還需要手動輸入命令或啟動Node腳本進行編譯是一件令人不勝其煩的事情,選擇一下工具可以簡化開發過程中的工作:
- 啟用watch模式
- 使用webpack-dev-server
- 使用webpack-dev-middleware
(1)使用watch模式
在使用webpack-cli
進行打包時,通過命令webpack --watch
即可開啟watch模式,進入watch模式之后,一旦依賴樹中的某一個模塊發生了變化,webpack就會重新進行編譯。
(2)使用webpack-dev-server
使用過create-react-app
或者Vue-Cli
這種腳手架的童鞋都知道,通過命令npm run start
即可建立一個本地服務器,並且webpack會自動打開瀏覽器打開你正在開發的頁面,並且一旦你修改了文件,瀏覽器會自動進行刷新,基本做到了所見即所得的效果,比webpack的watch模式更加方便給力。
使用方法:
-
① 安裝webpack-dev-server:
npm install --save-dev webpack-dev-server
-
② 修改配置文件,添加devServer屬性:
// webpack.config.js module.exports = { devServer: { contentBase: './dist' } };
-
③ 添加命令屬性到
package.json
:// package.json { "scripts": { "start": "webpack-dev-server --open" } }
-
④ 運行命令
npm run start
可以看到瀏覽器打開后的實際效果,嘗試修改文件,查看瀏覽器是否實時更新。
此外還可以再devServer屬性下指定更多的配置信息,比如開發服務器的端口、熱更新模式、是否壓縮等等,具體查詢:Webpack
通過Node API使用webpack-dev-server
:
'use strict'; const Webpack = require('webpack'); const WebpackDevServer = require('../../../lib/Server'); const webpackConfig = require('./webpack.config'); const compiler = Webpack(webpackConfig); const devServerOptions = Object.assign({}, webpackConfig.devServer, { stats: { colors: true } }); const server = new WebpackDevServer(compiler, devServerOptions); server.listen(8080, '127.0.0.1', () => { console.log('Starting server on http://localhost:8080'); });
(3)使用webpack-dev-middleware
webpack-dev-middleware
是一個比webpack-dev-server
更加基礎的插件,webpack-dev-server
也使用了這個插件,所以可以理解為webpack-dev-middleware
的封裝層次更低,使用起來更加復雜,但是低封裝性意味着較高的自定義性,使用webpack-dev-middleware
可以定義更多的設置來滿足更多的開發需求,它基於express模塊。
這一塊不做過多介紹,因為webpack-dev-server
已經能夠應付大多數開發場景,不用再設置更多的express屬性了,想要詳細了解的童鞋可以了解:使用 webpack-dev-middleware
(4)設置IDE
某些IDE具有安全寫入功能,導致開發服務器運行時IDE無法保存文件,此時需要進行對應的設置。
具體參考:調整文本編輯器
(三)熱模塊替換
熱模塊替換(Hot Module Replacement,HMR),代表在應用程序運行過程中替換、添加、刪除模塊,瀏覽器無需刷新頁面即可呈現出相應的變化。
使用方法:
-
(1)在devServer屬性中添加hot屬性並賦值為true:
// webpack.config.js module.exports = { devServer: { hot: true } }
-
(2)引入兩個插件到webpack配置文件:
// webpack.config.js const webpack = require('webpack'); module.exports = { devServer: { hot: true }, plugins: [ new webpack.NamedModulesPlugin(), new webpack.HotModuleReplacementPlugin() ] };
-
(3)在入口文件底部添加代碼,使得在所有代碼發生變化時,都能夠通知webpack:
if (module.hot) { module.hot.accept('./print.js', function() { console.log('Accepting the updated intMe module!'); printMe(); }) }
熱模塊替換比較難以掌控,容易報錯,推薦在不同的開發配置下使用不同的loader簡化HMR過程。具體參考:其他代碼和框架
六、搭建生產環境
生產環境要求代碼精簡、性能優異,而開發要求開發快速、測試方便,代碼不要求簡潔,所以兩種環境下webpack打包的目的也不相同,所以最好將兩種環境下的配置文件分開來。對於分開的配置文件,在使用webpack時還是要對其中的配置信息進行整合,webpack-merge
是一個不錯的整合工具(Vue-Cli也有使用到)。
使用方法:
-
(1)安裝webpack-merge:
npm install webpack-merge --save-dev
-
(2)建立三個配置文件:
- webpack.base.conf.js
- webpack.dev.conf.js
- webpack.prod.conf.js
其中,
webpack.base.conf.js
表示最基礎的配置信息,開發環境和生產環境都需要設置的信息,比如entry
、output
、module
等。在另外兩個文件中配置一些對應環境下特有的信息,然后通過webpack-merge
模塊與webpack.base.conf.js
整合。 -
(3)添加npm scripts:
// package.json { "scripts": { "start": "webpack-dev-server --open --config webpack.dev.conf.js", "build": "webpack --config webpack.prod.conf.js" } }
此外,建議設置mode屬性,因為生產環境下會自動開啟代碼壓縮,免去了配置的麻煩。
七、性能優化
TreeShaking
TreeShaking表示移除JavaScript文件中的未使用到的代碼,webpack 4增強了這一部分的功能。通過配置package.json的sideEffects屬性,可以指定哪些文件可以移除多余代碼。如果sideEffects設置為false,那么表示文件中的未使用代碼可以放心移除,沒有副作用。如果有些文件中的冗余代碼不能被移除,那么可以設置sideEffects屬性為一個數組,數組內容為文件的路徑字符串。
指定無副作用的文件之后,設置mode為"production",再次構建代碼,可以發現未使用到的代碼已經被移除。
Tips
- 在
module.rules
屬性中,設置include屬性以指定哪些文件需要被loader處理。 - 只使用必要的loader。
- 保持最新版本。
- 減少項目文件數。
八、通過webpack構建PWA應用
漸進式網絡應用程序(Progressive Web Application - PWA),是一種可以提供類似於原生應用程序(native app)體驗的網絡應用程序(web app),在離線(offline)時應用程序能夠繼續運行功能,這是通過 Service Workers 技術來實現的。PWA是最近幾年比較火的概念,它的核心是由service worker技術實現的在客戶瀏覽器與服務器之間搭建的一個代理服務器,在網絡暢通時,客戶瀏覽器會通過service worker訪問服務器,並且緩存注冊的文件;在網絡斷開時,瀏覽器會訪問service worker這個代理服務器,使得在網絡斷開的情況下,頁面還是能夠訪問,實現了類似原生應用的網站開發。create-react-app
已經實現了PWA開發的配置。
下面介紹如何通過webpack快速開發PWA。
-
(1)安裝插件
workbox-webpack-plugin
:npm install workbox-webpack-plugin --save-dev
-
(2)在配置文件中引入該插件:
// webpack.config.js const WorkboxPlugin = require('workbox-webpack-plugin'); module.exports = { plugins: [ new WorkboxPlugin.GenerateSW({ clientsClaim: true, skipWaiting: true }) ] };
-
(3)使用webpack進行編譯,打包出service-worker.js
-
(4)在入口文件底部注冊service worker:
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); }); }); }
-
(5)打開頁面,進行調試:
npm run start
-
(6)打開瀏覽器調試工具,查看控制台的輸出,如果輸出“SW registered: ... ...”,表示注冊service worker成功,接下來可以斷開網絡,或者關閉服務器,再次刷新,可以看到頁面仍然可以顯示。
九、參考文章
十、總結
webpack確實是一個功能強大的模塊打包工具,豐富的loader和plugin使得其功能多而強。學習webpack使得我們可以自定義自己的開發環境,無需依賴create-react-app
和Vue-Cli
這類腳手架,也可以針對不同的需求對代碼進行不同方案的處理。這篇筆記還只是一篇入門的筆記,如果要真正的構建較為復雜的開發環境和生產環境,還需要了解許多的loader和plugin,好在webpack官網提供了所有的說明,可以給用戶提供使用指南:
閱讀腳手架的源碼也有助於學習webpack,今后應該還有進行這方面的學習,但是答辯即將到來,不知道畢業之前還有沒有機會^_^。