webpack 的核心價值就是前端源碼的打包,即將前端源碼中每個文件(不管任何類型)都當作一個 pack ,而後分析依賴,將其最終打包出線上運行的代碼。webpack 的四個核心部分css
entry 規定入口文件,一個或者多個html
output 規定輸出文件的位置前端
loader 各個類型的轉換工具vue
plugin 打包過程當中各類自定義功能的插件node
webpack 現在已經進入 v4.x 版本,v5.x 估計也會很快發佈。不過看 v5 的變化相比於 v4 ,經常使用的配置沒有變,這是一個好消息,說明基本穩定。jquery
前端工程師須要瞭解的 webpackwebpack
前端工程化是近幾年前端發展迅速的主要推手之一,webpack 無疑是前端工程化的核心工具。目前前端工程化工具尚未到一鍵生成,或者重度繼承到某個 IDE 中(雖然有些 cli 工具能夠直接建立),仍是須要開發人員手動作一些配置。css3
所以,做爲前端開發人員,熟練應用 webpack 的經常使用配置、經常使用優化方案是必備的技能 ―― 這也正是本文的內容。另外,webpack 的實現原理算是一個加分項,不要求全部開發人員掌握,本文也沒有涉及。git
基礎配置github
初始化環境
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: 'http://cdn.abc.com' // 修改全部靜態文件 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 中作以下配置:
// 引入 webpack const 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' }
該現象的具體緣由可參考https://www.jb51.net/article/162079.htm 。下文立刻要講解啓動本地服務,讀者能夠立刻寫一個 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': 'http://localhost:3000', // 將本地 /api2/xxx 代理到 localhost:3000/xxx '/api2': { target: 'http://localhost:3000', 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 src="..."/> 的形式,則安裝 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: 'http://cdn.abc.com' } } },
多頁應用
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
多進程打包,參考 https://www.npmjs.com/package/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 發展至今配置很是多,該視頻中也沒有所有講解出來,只是一些實際開發中經常使用的。其餘的配置能夠去看官網文檔。