webpack4從零學習常用配置
webpack 的核心價值就是前端原始碼的打包,即將前端原始碼中每一個檔案(無論任何型別)都當做一個 pack ,然後分析依賴,將其最終打包出線上執行的程式碼。webpack 的四個核心部分
entry 規定入口檔案,一個或者多個
output 規定輸出檔案的位置
loader 各個型別的轉換工具
plugin 打包過程中各種自定義功能的外掛
webpack 如今已經進入 v4.x 版本,v5.x 估計也會很快釋出。不過看 v5 的變化相比於 v4 ,常用的配置沒有變,這是一個好訊息,說明基本穩定。
需要了解的 webpack
前端工程化是近幾年前端發展迅速的主要推手之一,webpack 無疑是前端工程化的核心工具。目前前端工程化工具還沒有到一鍵生成,或者重度繼承到某個 IDE 中(雖然有些 cli 工具可以直接建立),還是需要開發人員手動做一些配置。
因此,作為前端開發人員,熟練應用 webpack 的常用配置、常用最佳化方案是必備的技能 —— 這也正是本文的內容。另外,webpack 的實現原理算是一個加分項,不要求所有開發人員掌握,本文也沒有涉及。
基礎配置
初始化環境
npm init -y
初始化 npm 環境,然後安裝 webpack npm i webpack webpack-cli -D
新建 src
目錄並在其中新建 index.js
,隨便寫點 console.log('index js')
。然後根目錄建立 webpack.config.js
,內容如下
const path = require('path')module.exports = { // mode 可選 development 或 production ,預設為後者 // production 會預設壓縮程式碼並進行其他最佳化(如 tree shaking) mode: 'development', entry: path.join(__dirname, 'src', 'index'), output: { filename: 'bundle.js', path: path.join(__dirname, 'dist') }}
然後增加 package.json
的 scripts
"scripts": { "build": "webpack" },
然後執行 npm run build
即可打包檔案到 dist
目錄。
區分 dev 和 build
使用 webpack 需要兩個最基本的功能:第一,開發的程式碼執行一下看看是否有效;第二,開發完畢了將程式碼打包出來。這兩個操作的需求、配置都是完全不一樣的。例如,執行程式碼時不需要壓縮以便 debug ,而打包程式碼時就需要壓縮以減少檔案體積。因此,這裡我們還是先把兩者分開,方便接下來各個步驟的講解。
首先,安裝 npm i webpack-merge -D
,然後根目錄新建 build
目錄,其中新建如下三個檔案。
// webpack.common.js 公共的配置const path = require('path')const srcPath = path.join(__dirname, '..', 'src')const distPath = path.join(__dirname, '..', 'dist')module.exports = { entry: path.join(srcPath, 'index')}
// webpack.dev.js 執行程式碼的配置(該檔案暫時用不到,先建立了,下文會用到)const path = require('path')const webpackCommonConf = require('./webpack.common.js')const { smart } = require('webpack-merge')const srcPath = path.join(__dirname, '..', 'src')const distPath = path.join(__dirname, '..', 'dist')module.exports = smart(webpackCommonConf, { mode: 'development'})
// webpack.prod.js 打包程式碼的配置const path = require('path')const webpackCommonConf = require('./webpack.common.js')const { smart } = require('webpack-merge')const srcPath = path.join(__dirname, '..', 'src')const distPath = path.join(__dirname, '..', 'dist')module.exports = smart(webpackCommonConf, { mode: 'production', output: { filename: 'bundle.[contentHash:8].js', // 打包程式碼時,加上 hash 戳 path: distPath, // publicPath: '' // 修改所有靜態檔案 url 的字首(如 cdn 域名),這裡暫時用不到 }})
修改 package.json
中的 scripts
"scripts": { "build": "webpack --config build/webpack.prod.js" },
重新執行 npm run build
即可看到打包出來的程式碼。最後,別忘了將根目錄下的 webpack.config.js
刪除。
這將引發一個新的問題:js 程式碼中將如何判斷是什麼環境呢?需要藉助 webpack.DefinedPlugin
外掛來定義全域性變數。可以在 webpack.dev.js
和 webpack.prod.js
中做如下配置:
// 引入 webpackconst webpack = require('webpack')// 增加 webpack 配置 plugins: [ new webpack.DefinePlugin({ // 注意:此處 webpack.dev.js 中寫 'development' ,webpack.prod.js 中寫 'production' ENV: JSON.stringify('development') })
最後,修改 src/index.js
只需加入一行 console.log(ENV)
,然後重啟 npm run dev
即可看到效果。
JS 模組化
webpack 預設支援 js 各種模組化,如常見的 commonJS 和 ES6 Module 。但是推薦使用 ES6 Module ,因為 production 模式下,ES6 Module 會預設觸發 tree shaking ,而 commonJS 則沒有這個福利。究其原因,ES6 Module 是靜態引用,在編譯時即可確定依賴關係,而 commonJS 是動態引用。
不過使用 ES6 Module 時,ES6 的解構賦值語法這裡有一個坑,例如 index.js
中有一行 import {fn, name} from './a.js'
,此時 a.js
中有以下幾種寫法,大家要注意!
// 正確寫法一export function fn() { console.log('fn')}export const name = 'b'
// 正確寫法二function fn() { console.log('fn')}const name = 'b'export { fn, name}
// 錯誤寫法function fn() { console.log('fn')}export default { fn, name: 'b'}
該現象的具體原因可參考 。下文馬上要講解啟動本地服務,讀者可以馬上寫一個 demo 自己驗證一下這個現象。
啟動本地服務
上文建立的 webpack.dev.js
一直沒使用,下面就要用起來。
使用 html
啟動本地服務,肯定需要一個 html 頁面作為載體,新建一個 src/index.html
並初始化內容
<!DOCTYPE html><html><head><title>Document</title></head><body> <p>this is index html</p></body></html>
要使用這個 html 檔案,還需要安裝 npm i html-webpack-plugin -D
,然後配置 build/webpack.common.js
,因為無論 dev 還是 prod 都需要打包 html 檔案。
plugins: [ new HtmlWebpackPlugin({ template: path.join(srcPath, 'index.html'), filename: 'index.html' }) ]
重新執行 npm run build
會發現打包出來了 dist/index.html
,且內部已經自動插入了打包的 js 檔案。
webpack-dev-server
有了 html 和 js 檔案,就可以啟動服務了。首先安裝 npm i webpack-dev-server -D
,然後開啟 build/webpack.dev.js
配置。只有執行程式碼才需要本地 server ,打包程式碼時不需要。
devServer: { port: 3000, progress: true, // 顯示打包的進度條 contentBase: distPath, // 根目錄 open: true, // 自動開啟瀏覽器 compress: true // 啟動 gzip 壓縮}
開啟 package.json
修改 scripts
,增加 "dev": "webpack-dev-server --config build/webpack.dev.js",
。然後執行 npm run dev
,開啟瀏覽器訪問 localhost:3000
即可看到效果。
解決跨域
實際開發中,server 端提供的埠地址和前端可能不同,導致 ajax 收到跨域限制。使用 webpack-dev-server 可配置代理,解決跨域問題。如有需要,在 build/webpack.dev.js
中增加如下配置。
devServer: { // 設定代理 proxy: { // 將本地 /api/xxx 代理到 localhost:3000/api/xxx '/api': '', // 將本地 /api2/xxx 代理到 localhost:3000/xxx '/api2': { target: '', pathRewrite: { '/api2': '' } } }
處理 ES6
使用 babel
由於現在瀏覽器還不能保證完全支援 ES6 ,將 ES6 編譯為 ES5 ,需要藉助 babel 這個神器。安裝 babel npm i babel-loader @babel/core @babel/preset-env -D
,然後修改 build/webpack.common.js
配置
module: { rules: [ { test: /.js$/, loader: ['babel-loader'], include: srcPath, exclude: /node_modules/ }, ] },
還要根目錄下新建一個 .babelrc
json 檔案,內容下
{ "presets": ["@babel/preset-env"], "plugins": []}
在 src/index.js
中加入一行 ES6 程式碼,如箭頭函式 const fn = () => { console.log('this is fn') }
。然後重新執行 npm run dev
,可以看到瀏覽器中載入的 js 中,這個函式已經被編譯為 function
形式。
使用高階特性
babel 可以解析 ES6 大部分語法特性,但是無法解析 class 、靜態屬性、塊級作用域,還有很多大於 ES6 版本的語法特性,如裝飾器。因此,想要把日常開發中的 ES6 程式碼全部轉換為 ES5 ,還需要藉助很多 babel 外掛。
安裝 npm i @babel/plugin-proposal-class-properties @babel/plugin-transform-block-scoping @babel/plugin-transform-classes -D
,然後配置 .babelrc
{ "presets": ["@babel/preset-env"], "plugins": [ "@babel/plugin-proposal-class-properties", "@babel/plugin-transform-block-scoping", "@babel/plugin-transform-classes" ]}
在 src/index.js
中新增一段 class
程式碼,然後重新執行 npm run build
,打包出來的程式碼會將 class
轉換為 function
形式。
source map
source map 用於反解析壓縮程式碼中錯誤的行列資訊,dev 時程式碼沒有壓縮,用不到 source map ,因此要配置 build/webpack.prod.js
// webpack 中 source map 的可選項,是情況選擇一種:// devtool: 'source-map' // 1. 生成獨立的 source map 檔案// devtool: 'eval-source-map' // 2. 同 1 ,但不會產生獨立的檔案,整合到打包出來的 js 檔案中// devtool: 'cheap-module-source-map' // 3. 生成單獨的 source map 檔案,但沒有列資訊(因此檔案體積較小)devtool: 'cheap-module-eval-source-map' // 4. 同 3 ,但不會產生獨立的檔案,整合到打包出來的 js 檔案中
生產環境下推薦使用 1 或者 3 ,即生成獨立的 map 檔案。修改之後,重新執行 npm run build
,會看到打包出來了 map 檔案。
處理樣式
在 webpack 看來,不僅僅是 js ,其他的檔案也是一個一個的模組,透過相應的 loader 進行解析並最終產出。
處理 css
安裝必要外掛 npm i style-loader css-loader -D
,然後配置 build/webpack.common.js
module: { rules: [ { /* js loader */ }, { test: /.css$/, loader: ['style-loader', 'css-loader'] // loader 的執行順序是:從後往前 } ] },
新建一個 css 檔案,然後引入到 src/index.js
中 import './css/index.css'
,重新執行 npm run dev
即可看到效果。
處理 less
less sass 都是常用 css 預處理語言,以 less 為例講解。安裝必要外掛 npm i less less-loader -D
,然後配置 build/webpack.common.js
{ test: /.less$/, loader: ['style-loader', 'css-loader', 'less-loader'] // 增加 'less-loader' ,注意順序 }
新建一個 less 檔案,然後引入到 src/index.js
中 import './css/index.less'
,重新執行 npm run dev
即可看到效果。
自動新增字首
一些 css3 的語法,例如 transform: rotate(45deg);
為了瀏覽器相容性需要加一些字首,如 webkit-
,可以透過 webpack 來自動新增。安裝 npm i postcss-loader autoprefixer -D
,然後配置
{ test: /.css$/, loader: ['style-loader', 'css-loader', 'postcss-loader'] // 增加 'postcss-loader' , 注意順序 }
還要新建一個 postcss.config.js
檔案,內容是
module.exports = { plugins: [require('autoprefixer')]}
重新執行 npm run dev
即可看到效果,自動增加了必要的字首。
抽離 css 檔案
預設情況下,webpack 會將 css 程式碼全部寫入到 html 的 <style>
標籤中,但是打包程式碼時需要抽離到單獨的 css 檔案中。安裝 npm i mini-css-extract-plugin -D
然後配置 build/webpack.prod.js
(打包程式碼時才需要,執行時不需要)
// 引入外掛const MiniCssExtractPlugin = require('mini-css-extract-plugin')// 增加 webpack 配置 module: { rules: [ { test: /.css$/, loader: [ MiniCssExtractPlugin.loader, // 注意,這裡不再用 style-loader 'css-loader', 'postcss-loader' ] } ] }, plugins: [ new MiniCssExtractPlugin({ filename: 'css/main.[contentHash:8].css' }) ]
如需要壓縮 css ,需要安裝 npm i terser-webpack-plugin optimize-css-assets-webpack-plugin -D
,然後增加配置
// 引入外掛const TerserJSPlugin = require('terser-webpack-plugin')const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')// 增加 webpack 配置 optimization: { minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})], },
執行 npm run build
即可看到打包出來的 css 是獨立的檔案,並且是被壓縮過的。
處理圖片
要在 js 中 import
圖片,或者在 css 中設定背景圖片。安裝 npm i file-loader -D
然後配置 build/webpack.common.js
{ test: /.(png|jpg|gif)$/, use: 'file-loader' }
如果想要處理 html 程式碼中 <img class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="..."/>
的形式,則安裝 npm i html-withimg-loader -D
然後配置 build/webpack.common.js
{ test: /.html$/, use: 'html-withimg-loader' }
打包之後,dist 目錄下會生成一個類似 917bb63ba2e14fc4aa4170a8a702d9f8.jpg
的檔案,並被引入到打包出來的結果中。
如果想要將小圖片用 base64 格式產出,則安裝 npm i url-loader -D
,然後配置 build/webpack.common.js
{ test: /.(png|jpg|gif)$/, use: { loader: 'url-loader', options: { // 小於 5kb 的圖片用 base64 格式產出 // 否則,依然延用 file-loader 的形式,產出 url 格式 limit: 5 * 1024, // 打包到 img 目錄下 outputPath: '/img/', // 設定圖片的 cdn 地址(也可以統一在外面的 output 中設定,那將作用於所有靜態資源) // publicPath: '' } } },
多頁應用
src 下有 index.js
index.html
和 other.js
other.html
,要打包輸出兩個頁面,且分別引用各自的 js 檔案。
第一,配置輸入輸出
entry: { index: path.join(srcPath, 'index.js'), other: path.join(srcPath, 'other.js') }, output: { filename: '[name].[contentHash:8].js', // [name] 表示 chunk 的名稱,即上面的 index 和 other path: distPath },
第二,配置 html 外掛
plugins: [ // 生成 index.html new HtmlWebpackPlugin({ template: path.join(srcPath, 'index.html'), filename: 'index.html', // chunks 表示該頁面要引用哪些 chunk (即上面的 index 和 other),預設全部引用 chunks: ['index'] // 只引用 index.js }), // 生成 other.html new HtmlWebpackPlugin({ template: path.join(srcPath, 'other.html'), filename: 'other.html', chunks: ['other'] // 只引用 other.js }),
抽離公共程式碼
公共模組
多個頁面或者入口,如果引用了同一段程式碼,如上文的多頁面例子中,index.js
和 other.js
都引用了 import './common.js'
,則 common.js
應該被作為公共模組打包。webpack v4 開始棄用了 commonChunkPlugin 改用 splitChunks ,可修改 build/webpack.prod.js
中的配置
optimization: { // 分割程式碼塊 splitChunks: { // 快取分組 cacheGroups: { // 公共的模組 common: { chunks: 'initial', minSize: 0, // 公共模組的大小限制 minChunks: 2 // 公共模組最少複用過幾次 } } } },
重新執行 npm run build
,即可看到有 common 模組被單獨打包出來,就是 common.js
的內容。
第三方模組
同理,如果我們的程式碼中引用了 jquery lodash 等,也希望將第三方模組單獨打包,和自己開發的業務程式碼分開。這樣每次重新上線時,第三方模組的程式碼就可以藉助瀏覽器快取,提高使用者訪問網頁的效率。修改配置檔案,增加下面的 vendor: {...}
配置。
optimization: { // 分割程式碼塊 splitChunks: { // 快取分組 cacheGroups: { // 第三方模組 vendor: { priority: 1, // 許可權更高,優先抽離,重要!!! test: /node_modules/, chunks: 'initial', minSize: 0, // 大小限制 minChunks: 1 // 最少複用過幾次 }, // 公共的模組 common: { chunks: 'initial', minSize: 0, // 公共模組的大小限制 minChunks: 2 // 公共模組最少複用過幾次 } } } },
重啟 npm run build
,即可看到 vendor 模組被打包出來,裡面是 jquery 或者 lodash 等第三方模組的內容。
懶載入
webpack 支援使用 import(...)
語法進行資源懶載入。安裝 npm i @babel/plugin-syntax-dynamic-import -D
然後將外掛配置到 .babelrc
中。
新建 src/dynamic-data.js
用於測試,內容是 export default { message: 'this is dynamic' }
。然後在 src/index.js
中加入
setTimeout(() => { import('./dynamic-data.js').then(res => { console.log(res.default.message) // 注意這裡的 default })}, 1500)
重新執行 npm run dev
重新整理頁面,可以看到 1.5s 之後列印出 this is dynamic
。而且,dynamic-data.js
也是 1.5s 之後被載入進瀏覽器的 —— 懶載入,雖然檔名變了。
重新執行 npm run build
也可以看到 dynamic-data.js
的內容被打包一個單獨的檔案中。
常見效能最佳化
tree shaking
使用 import
引入,在 production
環境下,webpack 會自動觸發 tree shaking ,去掉無用程式碼。但是使用 require 引入時,則不會觸發 tree shaking。這是因為 require 是動態引入,無法在編譯時判斷哪些功能被使用。而 import 是靜態引入,編譯時即可判斷依賴關係。
noParse
不去解析某些 lib 其內部的依賴,即確定這些 lib 沒有其他依賴,提高解析速度。可配置到 build/wepback.common.js
中
module: { noParse: /jquery|lodash/, // 不解析 jquery 和 lodash 的內部依賴
ignorePlugin
以常用的 moment
為例。安裝 npm i moment -d
並且 import moment from 'moment'
之後,monent 預設將所有語言的 js 都載入進來,使得打包檔案過大。可以透過 ignorePlugin 外掛忽略 locale 下的語言檔案,不打包進來。
plugins: [ new webpack.IgnorePlugin(/./locale/, /moment/), // 忽略 moment 下的 /locale 目錄
這樣,使用時可以手動引入中文包,並設定語言
import moment from 'moment'import 'moment/locale/zh-cn' // 手動引入中文語言包moment.locale('zh-cn')const r = moment().endOf('day').fromNow()console.log(r)
happyPack
多程式打包,參考 。注意,小專案使用反而會變慢。只有專案較大,打包出現明顯瓶頸時,才考慮使用 happypack 。
常用外掛和配置
ProvidePlugin
如要給所有的 js 模組直接使用 $
,不用每次都 import $ from 'jquery'
,可做如下配置
plugins: [ new webpack.ProvidePlugin({ $: 'jquery' }),
externals
如果 jquery 已經在 html 中透過 cdn 引用了,無需再打包,可做如下配置
externals: { jquery: 'jQuery' },
alias
設定 alias 別名在實際開發中比較常用,尤其是專案較大,目錄較多時。可做如下配置
resolve: { alias: { Utilities: path.join(srcPath, 'utilities') } },
在該配置之前,可能需要 import Utility from '../../utilities/utility'
使用。配置之後就可以 import Utility from 'Utilities/utility'
使用,一來書寫簡潔,二來不用再考慮相對目錄的層級關係。
extensions
如果引用檔案時沒有寫字尾名,可以透過 extensions 來匹配。
resolve: { extensions: [".js", ".json"] },
clean-webpack-plugin
由於使用了 contentHash ,每次 build 時候都可能打包出不同的檔案,因此要及時清理 dist 目錄。安裝 npm i clean-webpack-plugin -D
,然後在 build/webpack.prod.js
中配置
// 引入外掛const CleanWebpackPlugin = require('clean-webpack-plugin')// 增加配置 plugins: [ new CleanWebpackPlugin(), // 預設清空 output.path 目錄
copy-webpack-plugin
build 時,將 src 目錄下某個檔案或者資料夾,無條件的複製到 dist 目錄下,例如 src/doc
目錄複製過去。安裝 npm i copy-webpack-plugin -D
,然後在 build/webpack.prod.js
中配置
// 引入外掛const CopyWebpackPlugin = require('copy-webpack-plugin')// 增加配置 plugins: [ new CopyWebpackPlugin([ { from: path.join(srcPath, 'doc'), // 將 src/doc 複製到 dist/doc to: path.join(distPath, 'doc') } ]),
bannerPlugin
程式碼的版權宣告,在 build/webpack.prod.js
中配置即可。
plugins: [ new webpack.BannerPlugin('by github.com/wangfupeng1988 r'),
總結
webpack 發展至今配置非常多,該影片中也沒有全部講解出來,只是一些實際開發中常用的。其他的配置可以去看官網文件。
大家可以關注一下課程:
【新課】
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/855/viewspace-2822951/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- webpack4配置學習(一)Web
- ?從零開始學習webpack系列二(配置檔案)Web
- 從零學習SpringSecuritySpringGse
- 從零開始學習KafkaKafka
- 從零開始學習機器學習機器學習
- 從零開始學習laravelLaravel
- eclipse學習從零開始Eclipse
- webpack4配置詳解之常用外掛分享Web
- 繼續學習webpack4的一些配置總結Web
- Webpack4學習筆記Web筆記
- 從零開始學習 Go ——安裝Go
- 如何從零學習PostgreSQL Page結構SQL
- 如何從零起步學習AIAI
- 30天從零開始學習SwiftSwift
- 從零開始學習C++(0)C++
- Webpack4 學習筆記六 多頁面配置和devtoolWeb筆記dev
- 從零開始學安全(六)●黑客常用的Dos命令黑客
- 從零開始學習OpenGL-14複習光照
- 《谷歌JAX深度學習從零開始學》簡介谷歌深度學習
- 《Python深度學習從零開始學》簡介Python深度學習
- 從零開始學習 React 高階元件React元件
- 從零開始學習邏輯迴歸邏輯迴歸
- 從零開始學習如何部署程式碼
- 從零開始內網滲透學習內網
- 如何從零開始學習一個框架框架
- Webpack4 學習筆記 - 01:webpack的安裝和簡單配置Web筆記
- 從零開始配置深度學習環境:CUDA+Anaconda+Pytorch+TensorFlow深度學習PyTorch
- 用webpack4從零開始構建react腳手架WebReact
- 從實踐中尋找webpack4最優配置Web
- 從零開始學Spring Boot系列-外部化配置Spring Boot
- 軟體測試如何從零開始學習
- 我是如何從零開始學習前端的前端
- VUE2.0從零開始 學習路線Vue
- 從零開始學習C++之遞推C++
- 從零開始學習C++(1-1)C++
- 《從零開始學Swift》學習筆記(Day 24)——列舉Swift筆記
- 《從零開始學Swift》學習筆記(Day 16)——字典集合Swift筆記
- 從零開始學習的朋友應該如何學習Linux技術?Linux