webpack是目前最火的打包工具,本文以最新的webpack4爲背景,從概念方面介紹webpack是什麼、能給咱們帶來什麼好處以及如何配置。css
因爲web應用擴展地得極其迅猛,前端技術也是突飛猛進,前端的苦不是有多難學,而是我剛學完,這東西就被淘汰了(手動哭臉)。框架方面咱們有vue、react、angular,咱們須要寫vue單文件,也須要寫jsx語法;js方面咱們有Typescript、es6(七、八、9),如今是每一年一個小版本的迭代,語法也是在不斷更新和淘汰;模塊方面咱們有es6的modlue、CommonJS、AMD;css方面咱們有sass、less、postcss。新技術層出不窮,瀏覽器實現跟不上,還要考慮到瀏覽器兼容性問題,是一個使人頭大的問題。可是webpack能夠輕鬆幫咱們解決這些問題,咱們能夠在webpack的世界中放心地使用上述提到的技術,以更方便、舒服的方式完成咱們開發。html
At its core, webpack is a static module bundler for modern JavaScript applications.
這是官網的定義,webpack就是一個靜態模塊的打包工具。webpack能夠將工程中的靜態資源根據咱們聲明的依賴關係,打包出最後的正常運行的輸出文件。官網顯示的這幅圖很形象地描述了這個過程: 前端
在webpack中,全部的靜態資源均可以被處理爲一個模塊,包括js、圖片、css、字體。模塊化是前端開發的主要模式,模塊化的好處有不少,我想到的有如下3種:vue
在es6以前,原生js是不支持模塊化開發。由於js設計之初,只是爲了實現表單驗證等簡單的交互,並無考慮到要用js來寫大型的web應用。隨着js承擔地職責愈來愈大,模塊化開發的需求愈來愈急迫。因而,js社區誕生出了CommonJS、AMD這樣的js模塊化標準,隨後在es6中,也終於加入了module的原生支持。如今最新版本的各大瀏覽器均已實現了對es6的module語法支持,但也僅限於最新的幾個版本,詳情能夠查看一下can i ues。咱們能夠把webpack當成是模塊化標準的實現方案,但webpack的功能不只限於此。node
webpack支持多種模塊使用方式,包括es6的module、CommonJS、AMD。推薦使用es6的module語法,一方面是由於它是標準,是之後模塊化語法的主要使用方式;另外一方面,是由於它是靜態的,webpack能夠依靠靜態分析,作一些優化,好比Treeshaking。webpack自帶js模塊處理功能,其餘類型的靜態資源咱們須要經過配置相應的loader去處理。react
webpack中最重要的概念有如下幾個:jquery
webpack通常根據配置文件去執行打包任務,咱們建立一個webpack.config.js文件來編寫咱們的打包配置。webpack
Entry,顧名思義就是工程的入口文件,Entry的配置寫法有三種:git
module.exports = {
entry: {
app: './src/app.js',
vendors: './src/vendors.js'
}
};
複製代碼
module.exports = {
entry: './path/to/my/entry/file.js'
};
複製代碼
module.exports = {
entry: ['./path/to/my/entry/file.js', './path/to/my/entry/file1.js']
};
複製代碼
入口通常用對象寫法便可,其餘兩種寫法的可忽略。es6
Output用於配置打包輸出的文件,包括輸出文件的文件名、輸出路徑、靜態資源地址,這裏列出最經常使用的4種:
module.exports = {
entry: {
app: './src/app.js',
search: './src/search.js'
},
output: {
filename: 'js/[name].js',
chunkFilename: 'js/[name].js',
path: __dirname + '/dist',
publicPath: 'http://cdn.example.com/assets/[hash]/'
}
};
複製代碼
配置項以下:
filename: 配置輸出文件名,可添加路徑配置(例子中js/),可以使用佔位符,佔位符有如下5種:
?
後面的內容path: 文件的輸出路徑,必須是絕對地址
publicPath: 用於設置打包過程當中產生的靜態文件的最終引用地址,靜態文件的最終引用地址爲output.publicPath + output.filename
,不少時候,你的靜態文件放置在CDN上,經過publicPath就能夠很方便地設置。若是你的靜態引用地址在運行時才能肯定,能夠在入口文件中設置__webpack_public_path__
來決定publicPath的值:
__webpack_public_path__ = myRuntimePublicPath;
// rest of your application entry
複製代碼
chunkFilename: 用於設置非entry入口的chunk的輸出文件名,非entry入口的chunk通常在動態引入和CommonsChunkPlugin
中產生,這是一個Plugin,用於抽取公共代碼或者進行代碼分割等操做,該plugin已經在webpack4中廢除,由webpac4內置的optimization.splitChunks
替代,後面會講到
output還有其餘不少配置,這4個是經常使用配置。
Loaders能夠理解爲不一樣類型模塊的處理器,將這些類型的模塊處理爲瀏覽器可運行和識別的代碼。好比babel-loader將es6以上代碼轉換爲es5代碼;sass-loader將sass代碼解析爲css代碼;url-loader和file-loader能夠將圖片、字體等靜態文件解析爲base64碼或者靜態文件地址。Loaders給咱們提供了處理模塊的入口,在裏面可使用所有的js功能,從而使webpack具備了強大而靈活的能力。webpack及webpack社區提供了功能強大的loader供開發者使用,你也能夠本身編寫loader。下面介紹一下在工程中經常使用的loader。
使用babel將ES2015+的代碼轉碼爲ES5的代碼,babel的具體配置可參考babel官網
modlue: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: 'babel-loader
}
]
}
複製代碼
exclude表示不處理的目錄,通常node_modules
中的第三方js文件不在咱們的處理範圍內。
該loader使對應的js文件在全局環境中運行一次。好比咱們在工程中使用了jquery的插件,須要全局暴露掛載到全局變量中,效果和在瀏覽器中加script標籤同樣。
import 'jquery';
module: {
rules: [
{
test: /jquery$/,
use: [ 'script-loader' ]
}
]
}
複製代碼
style-loader:將css模塊以style標籤的形式加入到html中
css-loader:主要用來解析css中的靜態資源,將@import和url()解析爲import/require(),解析出的除css之外的靜態資源,通常交給url-loader和file-loader去處理
postcss-loader:能夠對css進行各類處理,功能強大,好比自動添加css前綴,也可自定義插件
sass-loader/less-loader:將sass/less代碼轉換爲css
解析一個sass文件,並不僅須要一個loader,它須要多個loader串行處理,webpack能夠配置多個loader串行處理:
module: {
rules: [
{
test: /\.sass$/,
use: [ 'style-loader', 'css-loader', 'postcss-loader', 'sass-loader' ]
}
]
}
複製代碼
咱們能夠將use配置爲一個數組,loader從右往左依次執行,且前一個loader的結果是下一個loader的輸入。最後一個loader的輸出就是咱們最終要的結果。一個sass文件首先通過sass-loader
處理,變成css文件,又通過postcss-loader
處理,添加瀏覽器前綴等功能,接着交給css-loader
去解析css文件引用的靜態變量,最後由style-loader
以script
標籤的形式加入到html中。
url-loader
和file-loader
是一對用來處理圖片、svg、視頻、字體等靜態資源文件的loader。通常體積比較小的資源交給url-loader
處理,編碼爲base64字符串,直接嵌入js文件中。體積較大的文件由file-loader處理,直接釋放爲了一個輸出文件。
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'img/[name].[hash:7].[ext]'
}
},
複製代碼
通常只配置url-loader
便可,在資源超過limit的時候,url-loader
會將資源自動交給file-loader
處理,並將options內容也傳遞給file-loader。
loaders用來轉換某種特定類型的module,plugins則用來在一些合適的時機執行一些特定的任務,好比代碼分割、靜態資源處理、環境變量的注入、將全部css的module抽取爲單個文件等。webpack自身也是用插件系統構建起來的。插件的目的是作任何loaders作不了的事情。
HtmlWebpackPlugin插件能夠用來生成包含你全部打包文件(js和css)的html文件,特別是你在打包文件名配置了hash,就不得不用這個插件了。
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'src/index.html'),
filename: 'static/index.[hash].html',
inject: true,
minify: false,
chunks: ['app', 'vendor']
})
]
};
複製代碼
MiniCssExtractPlugin
將一個chunk中的css抽取爲一個單獨的css文件,若是chunk中不包含css,則不生成文件。且支持按需加載和sourceMap。webpack4新增插件,在webpack4以前是使用ExtractTextWebpackPlugin
來作這件事。官方文檔總結了MiniCssExtractPlugin
相對ExtractTextWebpackPlugin
的四個優點:
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const devMode = process.env.NODE_ENV !== 'production'
module.exports = {
plugins: [
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: "[name].css",
chunkFilename: "[id].css"
})
],
module: {
rules: [
{
test: /\.css$/,
use: [
devMode ? 'style-loader' : { // MiniCssExtractPlugin目前尚未HMR功能,因此最好只在生成環境使用
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '../' // 可單獨配置publicPath,默認採用output中的publicPath
}
},
"css-loader"
]
}
]
}
}
複製代碼
CopyWebpackPlugin用來處理靜態文件,能夠將文件或者文件夾原封不動地移動到打包目錄。
const CopyWebpackPlugin = require('copy-webpack-plugin')
const config = {
plugins: [
new CopyWebpackPlugin([
{ from: 'source', to: 'dest' },
{ from: 'source', to: 'dest', toType: 'dir|file|template' }, // 手動設置to的類型,好比設置成dir,即便to設置爲a.js,最後也會生成a.js文件夾
{ from: 'source', to: 'dest', context: '/app' }, // 基準目錄,from相對於context解析
{ from: 'source', to: 'dest', ignore: ['*.js'] }, // ignore, 忽略匹配的文件
'source' // 只有from,to默認爲output的path
], {
context: '/app', // 同上
ignore: ['*.js'], // 同上
})
]
}
複製代碼
CleanWebpackPlugin用來清除打包目錄,主要用於每次從新生成的時候,清除殘留文件。在文件有hash值的狀況下,是必要的。
const CleanWebpackPlugin = require('clean-webpack-plugin');
const config = {
plugins: [
new CleanWebpackPlugin(['dist', 'bulid/*.js'], {
watch: false, // 是否在--watch模式下也清除files而後從新編譯,默認爲false
exclude: [ 'files', 'svg', 'css' ] // 不刪除的子目錄和文件
}),
]
}
複製代碼
DefinePlugin用來定義webpack編譯期間的全局變量。咱們能夠根據這些變量,來作不一樣的動做。最典型的就是能夠區分開發環境和生產環境,好比在開發環境打印各類警告、錯誤,在生產環境去掉這些跟業務無關的代碼。
DefinePlugin的參數是一個對象,鍵名是一個標識符,或者用.
隔開的多級標識符,參數遵循如下規則
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production'),
PRODUCTION: JSON.stringify(true),
VERSION: JSON.stringify('5fa3b9'),
BROWSER_SUPPORTS_HTML5: true,
TWO: '1+1',
'typeof window': JSON.stringify('object')
});
複製代碼
index.js
console.log('PRODUCTION', PRODUCTION);
console.log('VERSION', VERSION);
console.log('BROWSER_SUPPORTS_HTML5', BROWSER_SUPPORTS_HTML5);
console.log('TWO', TWO);
console.log('Object', typeof window);
複製代碼
被編譯爲
console.log('PRODUCTION', true);
console.log('VERSION', "5fa3b9");
console.log('BROWSER_SUPPORTS_HTML5', true);
console.log('TWO', 1+1);
console.log('Object', false ? undefined : _typeof(window)); // 不太明白
複製代碼
DefinePlugin的原理很簡單,只是在編譯過程當中遇到這些定義好的鍵名,就用鍵值作簡單的文本替換。因此,你若是想給全局變量賦一個字符串,須要這樣寫'"production"'
,通常使用JSON.stringify
來轉一下。
webpack4新增了mode
配置。webpack會根據mode
值自動幫你作一個不一樣的優化:
production(默認值)
DefinePlugin
中將process.env.NODE_ENV
設置爲production
FlagDependencyUsagePlugin
, FlagIncludedChunksPlugin
, ModuleConcatenationPlugin
, NoEmitOnErrorsPlugin
, OccurrenceOrderPlugin
, SideEffectsFlagPlugin
and UglifyJsPlugin
development:
DefinePlugin
中將process.env.NODE_ENV
設置爲development
NamedChunksPlugin
, NamedModulesPlugin
none: 什麼都不作
mode
的兩種使用方式
module.exports = {
mode: 'production'
};
複製代碼
webpack --mode=production
複製代碼
代碼分割能夠把代碼按照必定的邏輯分割,用來作按需加載或者並行加載,以減小加載時間。在webpack中,主要有如下3中方式,實現代碼分割:
entry
入口配置處配置多個入口import()
語法,webpack使用SplitChunks
將import()
加載的module分割爲一個單獨的chunk,並在函數執行時加載該chunk對應的js文件。第一種沒什麼好說的,主要說一下後兩種。
在webpack4以前的版本,都是使用CommonsChunkPlugin
來抽取公共chunk,在webpack4中廢棄了CommonsChunkPlugin
,轉而支持內置的optimization.splitChunks
。
splitChunks
默認只對按需加載的chunk起做用,會自動將import()引入的module分割爲單獨chunk,可是須要知足如下條件:
node_modules
在執行後兩個原則的時候,體積大的chunk被優先生成。
splitChunks
的默認配置以下,咱們能夠看出正好是和上面4個條件對應的:
module.exports = {
optimization: {
splitChunks: {
chunks: 'async',
minSize: 30000, // chunk只有超過這個大小纔會被分割
maxSize: 0, // 大於這個體積的chunk會被自動分割爲更小的chunk
minChunks: 1, // 一個模塊被共享的chunk數量大於minChunks時,纔會被分割出來
maxAsyncRequests: 5, // 按需加載最大的並行數
maxInitialRequests: 3, // 初始加載最大的並行數
automaticNameDelimiter: '~', // name爲true時,新chunk的文件名由cacheGroups的key加上chunks屬性的一些信息生成,automaticNameDelimiter是分隔符
name: true,
cacheGroups: { // 配置拆分規則,會繼承splitChunks全部的配置項,全部splitChunks配置項均可以在這裏重寫覆蓋,test、prioprity、reuseExistingChunk是cacheGroups獨有的屬性
vendors: {
test: /[\\/]node_modules[\\/]/, // 模塊匹配規則,能夠是正則表達式或者函數,不寫默認選擇全部模塊
priority: -10 // 優先級,當同一個模塊同時包含在不一樣cacheGroup中,該模塊將被劃分到優先級高的組中
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true // 若是該chunk包含的modules都已經另外一個被分割的chunk中存在,那麼直接引用已存在的chunk,不會再從新產生一個
}
}
}
}
};
複製代碼
splitChunks
的chunks屬性表示做用的chunk範圍。chunks能夠是一個函數,徹底由開發者控制;也能夠是一個字符串,字符串一共有3個值:
name屬性表示最後生成chunk的文件名,有如下3中類型取值:
true
,則會根據cacheGroup的key和chunks屬性的信息自動生成splitChunks
下,那全部chunk設置爲相同的名稱,這會形成不一樣的chunk合成爲一個chunk,固然你能夠設置在cacheGroup
下Tree Shaking
此次詞很形象,搖樹,把爛掉的樹葉搖下來。咱們工程中的爛樹葉
就是那些咱們導出了,可是沒有用到的代碼,Tree Shaking
能夠幫助咱們去除無效代碼,減少打包體積,有其在大工程中,效果明顯。
Tree Shaking
的使用三部曲:
使用Tree Shaking
的第一個前提條件就是必須使用ES2015的模塊語法,import
,export
。由於ES2015的模塊語法是靜態加載
的,而CommonJS和AMD都是動態加載
。靜態加載
是指在編譯階段你就能肯定導出和加載的內容。ES2015在語法上規定:你只能在頂層模塊做用域進行導入導出操做,不容許在條件語句中導入導出,也不容許導入和導出的內容有變量。這意味着你只分析源碼就能夠肯定導入和導出的內容,而不是等到運行時才能肯定。好比說下面CommonJS
的語法,在ES6中就是不能使用的:
var my_lib;
if (Math.random()) {
my_lib = require('foo');
} else {
my_lib = require('bar');
}
複製代碼
你只有在運行的時候,才知道到底加載是的foo
仍是bar
。ES6強制模塊語法靜態化,失去了必定的靈活性,可是帶來了更多的好處,其中之一就是咱們能夠經過靜態分析去實現Tree Shaking。
在徹底的Es6模塊世界中,代碼是沒有反作用的,可是如今咱們可能用到的各類地方庫會有反作用。反作用
是指代碼除了導入和導出,還作了一些其餘影響了其餘代碼的行爲,好比定義了全局變量。最典型的例子就是polyfills,好比Promise的polyfills就定義了Promise全局變量。這時候若是咱們分析到Promise有未使用的導出代碼,則不能刪除,不然可能會影響Promise的使用。哪些是有反作用
的代碼,須要你識別,而且告訴webpack,方式就是經過設置sideEffects,能夠設置在package.json文件中,也能夠設置的module.rules中。
{
"name": "your-project",
"sideEffects": [
"./src/some-side-effectful-file.js",
"*.css"
]
}
複製代碼
webpack會經過靜態分析找到冗餘代碼,並打上標記,咱們看一下官網例子:
math.js
export function square(x) {
return x * x;
}
export function cube(x) {
return x * x * x;
}
複製代碼
index.js
import { cube } from './math.js';
function component() {
var element = document.createElement('pre');
element.innerHTML = [
'Hello webpack!',
'5 cubed is equal to ' + cube(5)
].join('\n\n');
return element;
}
document.body.appendChild(component());
複製代碼
在development
模式下打包,內容以下:
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
'use strict';
/* unused harmony export square */
/* harmony export (immutable) */ __webpack_exports__['a'] = cube;
function square(x) {
return x * x;
}
function cube(x) {
return x * x * x;
}
});
複製代碼
咱們能夠看到代碼中並未用到math.js
中的square
方法,webpack輸出文件中表示它未被使用。若是想去掉未被使用的代碼,則須要用到UglifyJSPlugin
插件,它是用來壓縮js文件的,自動啓用了去除冗餘代碼的功能。咱們能夠在production
模式下打包,會發現square
的代碼已經被去除。