2020年即將到來,在衆多前端的招聘要求裏,webpack
、工程化
這些字眼頻率愈來愈高。平常開發者中,咱們經常在用諸如vue-cli
、create-react-app
的腳手架來構建咱們的項目。可是若是你想在團隊脫穎而出(鶴立雞羣)、拿到更好的offer(還房貸),那麼你必須去深入的認識下咱們常常打交道的webpack
css
本文共分爲三個部分帶你快速掌握webpack,閱讀本篇大概須要60分鐘。若有不足之處,懇請斧正
本文編寫基於html
webpack 4.41.2
版本node: 10.15.3
版本新建一個目錄,初始化npm前端
npm init
複製代碼
webpack是運行在node環境中的,咱們須要安裝如下兩個npm包vue
npm i -D webpack webpack-cli
複製代碼
新建一個文件夾src
,而後新建一個文件main.js
,寫一點代碼測試一下node
console.log('call me 老yuan')
複製代碼
配置package.json命令 react
執行npm run build
複製代碼
此時若是生成了一個dist文件夾,而且內部含有main.js說明已經打包成功了jquery
上面一個簡單的例子只是webpack本身默認的配置,下面咱們要實現更加豐富的自定義配置
新建一個build
文件夾,裏面新建一個webpack.config.js
webpack
// webpack.config.js
const path = require('path');
module.exports = {
mode:'development', // 開發模式
entry: path.resolve(__dirname,'../src/main.js'), // 入口文件
output: {
filename: 'output.js', // 打包後的文件名稱
path: path.resolve(__dirname,'../dist') // 打包後的目錄
}
}
複製代碼
更改咱們的打包命令 git
執行npm run build
會發現生成了如下目錄(圖片)
dist
文件夾中的
main.js
就是咱們須要在瀏覽器中實際運行的文件
js文件打包好了,可是咱們不可能每次在html
文件中手動引入打包好的jsgithub
這裏可能有的朋友會認爲咱們打包js文件名稱不是一直是固定的嘛(output.js)?這樣每次就不用改動引入文件名稱了呀?實際上咱們平常開發中每每會這樣配置:
module.exports = {
// 省略其餘配置
output: {
filename: '[name].[hash:8].js', // 打包後的文件名稱
path: path.resolve(__dirname,'../dist') // 打包後的目錄
}
}
複製代碼
這時候生成的dist
目錄文件以下
npm i -D html-webpack-plugin
複製代碼
新建一個build
同級的文件夾public
,裏面新建一個index.html
具體配置文件以下
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode:'development', // 開發模式
entry: path.resolve(__dirname,'../src/main.js'), // 入口文件
output: {
filename: '[name].[hash:8].js', // 打包後的文件名稱
path: path.resolve(__dirname,'../dist') // 打包後的目錄
},
plugins:[
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'../public/index.html')
})
]
}
複製代碼
生成目錄以下(圖片)
能夠發現打包生成的js文件已經被自動引入html文件中生成多個html-webpack-plugin實例來解決這個問題
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode:'development', // 開發模式
entry: {
main:path.resolve(__dirname,'../src/main.js'),
header:path.resolve(__dirname,'../src/header.js')
},
output: {
filename: '[name].[hash:8].js', // 打包後的文件名稱
path: path.resolve(__dirname,'../dist') // 打包後的目錄
},
plugins:[
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'../public/index.html'),
filename:'index.html',
chunks:['main'] // 與入口文件對應的模塊名
}),
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'../public/header.html'),
filename:'header.html',
chunks:['header'] // 與入口文件對應的模塊名
}),
]
}
複製代碼
每次執行npm run build 會發現dist文件夾裏會殘留上次打包的文件,這裏咱們推薦一個plugin來幫咱們在打包輸出前清空文件夾
clean-webpack-plugin
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
module.exports = {
// ...省略其餘配置
plugins:[new CleanWebpackPlugin()]
}
複製代碼
咱們的入口文件是js,因此咱們在入口js中引入咱們的css文件
同時咱們也須要一些loader來解析咱們的css文件npm i -D style-loader css-loader
複製代碼
若是咱們使用less來構建樣式,則須要多安裝兩個
npm i -D less less-loader
複製代碼
配置文件以下
// webpack.config.js
module.exports = {
// ...省略其餘配置
module:{
rules:[
{
test:/\.css$/,
use:['style-loader','css-loader'] // 從右向左解析原則
},
{
test:/\.less$/,
use:['style-loader','css-loader','less-loader'] // 從右向左解析原則
}
]
}
}
複製代碼
瀏覽器打開html
以下
npm i -D postcss-loader autoprefixer
複製代碼
配置以下
// webpack.config.js
module.exports = {
module:{
rules:[
test/\.less$/,
use:['style-loader','css-loader','postcss-loader','less-loader'] // 從右向左解析原則
]
}
}
複製代碼
接下來,咱們還須要引入autoprefixer
使其生效,這裏有兩種方式
1,在項目根目錄下建立一個postcss.config.js
文件,配置以下:
module.exports = {
plugins: [require('autoprefixer')] // 引用該插件便可了
}
複製代碼
2,直接在webpack.config.js
裏配置
// webpack.config.js
module.exports = {
//...省略其餘配置
module:{
rules:[{
test:/\.less$/,
use:['style-loader','css-loader',{
loader:'postcss-loader',
options:{
plugins:[require('autoprefixer')]
}
},'less-loader'] // 從右向左解析原則
}]
}
}
複製代碼
這時候咱們發現css經過style標籤的方式添加到了html文件中,可是若是樣式文件不少,所有添加到html中,不免顯得混亂。這時候咱們想用把css拆分出來用外鏈的形式引入css文件怎麼作呢?這時候咱們就須要藉助插件來幫助咱們
npm i -D mini-css-extract-plugin
複製代碼
webpack 4.0之前,咱們經過
extract-text-webpack-plugin
插件,把css樣式從js文件中提取到單獨的css文件中。webpack4.0之後,官方推薦使用mini-css-extract-plugin
插件來打包css文件
配置文件以下
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
//...省略其餘配置
module: {
rules: [
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader'
],
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].[hash].css",
chunkFilename: "[id].css",
})
]
}
複製代碼
這裏須要說的細一點,上面咱們所用到的
mini-css-extract-plugin
會將全部的css樣式合併爲一個css文件。若是你想拆分爲一一對應的多個css文件,咱們須要使用到extract-text-webpack-plugin
,而目前mini-css-extract-plugin
還不支持此功能。咱們須要安裝@next版本的extract-text-webpack-plugin
npm i -D extract-text-webpack-plugin@next
複製代碼
// webpack.config.js
const path = require('path');
const ExtractTextWebpackPlugin = require('extract-text-webpack-plugin')
let indexLess = new ExtractTextWebpackPlugin('index.less');
let indexCss = new ExtractTextWebpackPlugin('index.css');
module.exports = {
module:{
rules:[
{
test:/\.css$/,
use: indexCss.extract({
use: ['css-loader']
})
},
{
test:/\.less$/,
use: indexLess.extract({
use: ['css-loader','less-loader']
})
}
]
},
plugins:[
indexLess,
indexCss
]
}
複製代碼
file-loader
就是將文件在進行一些處理後(主要是處理文件名和路徑、解析文件url),並將文件移動到輸出的目錄中
url-loader
通常與file-loader
搭配使用,功能與 file-loader 相似,若是文件小於限制的大小。則會返回 base64 編碼,不然使用 file-loader 將文件移動到輸出的目錄中
// webpack.config.js
module.exports = {
// 省略其它配置 ...
module: {
rules: [
// ...
{
test: /\.(jpe?g|png|gif)$/i, //圖片文件
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'img/[name].[hash:8].[ext]'
}
}
}
}
]
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, //媒體文件
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'media/[name].[hash:8].[ext]'
}
}
}
}
]
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i, // 字體
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'fonts/[name].[hash:8].[ext]'
}
}
}
}
]
},
]
}
}
複製代碼
爲了使咱們的js代碼兼容更多的環境咱們須要安裝依賴
npm i babel-loader @babel/preset-env @babel/core
複製代碼
babel-loader
與babel-core
的版本對應關係babel-loader
8.x 對應babel-core
7.xbabel-loader
7.x 對應babel-core
6.x // webpack.config.js
module.exports = {
// 省略其它配置 ...
module:{
rules:[
{
test:/\.js$/,
use:{
loader:'babel-loader',
options:{
presets:['@babel/preset-env']
}
},
exclude:/node_modules/
},
]
}
}
複製代碼
上面的babel-loader
只會將 ES6/7/8語法轉換爲ES5語法,可是對新api並不會轉換 例如(promise、Generator、Set、Maps、Proxy等)
此時咱們須要藉助babel-polyfill來幫助咱們轉換
npm i @babel/polyfill
複製代碼
// webpack.config.js
const path = require('path')
module.exports = {
entry: ["@babel/polyfill,path.resolve(__dirname,'../src/index.js')"], // 入口文件
}
複製代碼
上面的實踐是咱們對webpack的功能有了一個初步的瞭解,可是要想熟練應用於開發中,咱們須要一個系統的實戰。讓咱們一塊兒擺脫腳手架嘗試本身搭建一個vue開發環境
上面的小例子已經幫助而咱們實現了打包css、圖片、js、html等文件。 可是咱們還須要如下幾種配置
npm i -D vue-loader vue-template-compiler vue-style-loader
npm i -S vue
複製代碼
vue-loader
用於解析.vue
文件
vue-template-compiler
用於編譯模板 配置以下
const vueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
module:{
rules:[{
test:/\.vue$/,
use:['vue-loader']
},]
},
resolve:{
alias:{
'vue$':'vue/dist/vue.runtime.esm.js',
' @':path.resolve(__dirname,'../src')
},
extensions:['*','.js','.json','.vue']
},
plugins:[
new vueLoaderPlugin()
]
}
複製代碼
npm i -D webpack-dev-server
複製代碼
配置以下
const Webpack = require('webpack')
module.exports = {
// ...省略其餘配置
devServer:{
port:3000,
hot:true,
contentBase:'../dist'
},
plugins:[
new Webpack.HotModuleReplacementPlugin()
]
}
複製代碼
完整配置以下
// webpack.config.js
const path = require('path');
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const ExtractTextWebpackPlugin = require('extract-text-webpack-plugin')
const vueLoaderPlugin = require('vue-loader/lib/plugin')
const Webpack = require('webpack')
module.exports = {
mode:'development', // 開發模式
entry: {
main:path.resolve(__dirname,'../src/main.js'),
},
output: {
filename: '[name].[hash:8].js', // 打包後的文件名稱
path: path.resolve(__dirname,'../dist') // 打包後的目錄
},
module:{
rules:[
{
test:/\.vue$/,
use:['vue-loader']
},
{
test:/\.js$/,
use:{
loader:'babel-loader',
options:{
presets:[
['@babel/preset-env']
]
}
}
},
{
test:/\.css$/,
use: ['vue-style-loader','css-loader',{
loader:'postcss-loader',
options:{
plugins:[require('autoprefixer')]
}
}]
},
{
test:/\.less$/,
use: ['vue-style-loader','css-loader',{
loader:'postcss-loader',
options:{
plugins:[require('autoprefixer')]
}
},'less-loader']
}
]
},
resolve:{
alias:{
'vue$':'vue/dist/vue.runtime.esm.js',
' @':path.resolve(__dirname,'../src')
},
extensions:['*','.js','.json','.vue']
},
devServer:{
port:3000,
hot:true,
contentBase:'../dist'
},
plugins:[
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'../public/index.html'),
filename:'index.html'
}),
new vueLoaderPlugin(),
new Webpack.HotModuleReplacementPlugin()
]
}
複製代碼
npm run dev
這時候若是瀏覽器出現Vue開發環境運行成功,那麼恭喜你,已經成功邁出了第一步
實際應用到項目中,咱們須要區分開發環境與生產環境,咱們在原來webpack.config.js的基礎上再新增兩個文件
webpack.dev.js
開發環境主要實現的是熱更新,不要壓縮代碼,完整的sourceMap
複製代碼
webpack.prod.js
生產環境主要實現的是壓縮代碼、提取css文件、合理的sourceMap、分割代碼
須要安裝如下模塊:
npm i -D webpack-merge copy-webpack-plugin optimize-css-assets-webpack-plugin uglifyjs-webpack-plugin
複製代碼
webpack-merge
合併配置copy-webpack-plugin
拷貝靜態資源optimize-css-assets-webpack-plugin
壓縮cssuglifyjs-webpack-plugin
壓縮js
webpack mode
設置production
的時候會自動壓縮js代碼。原則上不須要引入uglifyjs-webpack-plugin
進行重複工做。可是optimize-css-assets-webpack-plugin
壓縮css的同時會破壞原有的js壓縮,因此這裏咱們引入uglifyjs
進行壓縮
const path = require('path')
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const vueLoaderPlugin = require('vue-loader/lib/plugin')
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
const devMode = process.argv.indexOf('--mode=production') === -1;
module.exports = {
entry:{
main:path.resolve(__dirname,'../src/main.js')
},
output:{
path:path.resolve(__dirname,'../dist'),
filename:'js/[name].[hash:8].js',
chunkFilename:'js/[name].[hash:8].js'
},
module:{
rules:[
{
test:/\.js$/,
use:{
loader:'babel-loader',
options:{
presets:['@babel/preset-env']
}
},
exclude:/node_modules/
},
{
test:/\.vue$/,
use:['cache-loader','thread-loader',{
loader:'vue-loader',
options:{
compilerOptions:{
preserveWhitespace:false
}
}
}]
},
{
test:/\.css$/,
use:[{
loader: devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
options:{
publicPath:"../dist/css/",
hmr:devMode
}
},'css-loader',{
loader:'postcss-loader',
options:{
plugins:[require('autoprefixer')]
}
}]
},
{
test:/\.less$/,
use:[{
loader:devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
options:{
publicPath:"../dist/css/",
hmr:devMode
}
},'css-loader','less-loader',{
loader:'postcss-loader',
options:{
plugins:[require('autoprefixer')]
}
}]
},
{
test:/\.(jep?g|png|gif)$/,
use:{
loader:'url-loader',
options:{
limit:10240,
fallback:{
loader:'file-loader',
options:{
name:'img/[name].[hash:8].[ext]'
}
}
}
}
},
{
test:/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
use:{
loader:'url-loader',
options:{
limit:10240,
fallback:{
loader:'file-loader',
options:{
name:'media/[name].[hash:8].[ext]'
}
}
}
}
},
{
test:/\.(woff2?|eot|ttf|otf)(\?.*)?$/i,
use:{
loader:'url-loader',
options:{
limit:10240,
fallback:{
loader:'file-loader',
options:{
name:'media/[name].[hash:8].[ext]'
}
}
}
}
}
]
},
resolve:{
alias:{
'vue$':'vue/dist/vue.runtime.esm.js',
' @':path.resolve(__dirname,'../src')
},
extensions:['*','.js','.json','.vue']
},
plugins:[
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'../public/index.html')
}),
new vueLoaderPlugin(),
new MiniCssExtractPlugin({
filename: devMode ? '[name].css' : '[name].[hash].css',
chunkFilename: devMode ? '[id].css' : '[id].[hash].css'
})
]
}
複製代碼
const Webpack = require('webpack')
const webpackConfig = require('./webpack.config.js')
const WebpackMerge = require('webpack-merge')
module.exports = WebpackMerge(webpackConfig,{
mode:'development',
devtool:'cheap-module-eval-source-map',
devServer:{
port:3000,
hot:true,
contentBase:'../dist'
},
plugins:[
new Webpack.HotModuleReplacementPlugin()
]
})
複製代碼
const path = require('path')
const webpackConfig = require('./webpack.config.js')
const WebpackMerge = require('webpack-merge')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
module.exports = WebpackMerge(webpackConfig,{
mode:'production',
devtool:'cheap-module-source-map',
plugins:[
new CopyWebpackPlugin([{
from:path.resolve(__dirname,'../public'),
to:path.resolve(__dirname,'../dist')
}]),
],
optimization:{
minimizer:[
new UglifyJsPlugin({//壓縮js
cache:true,
parallel:true,
sourceMap:true
}),
new OptimizeCssAssetsPlugin({})
],
splitChunks:{
chunks:'all',
cacheGroups:{
libs: {
name: "chunk-libs",
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: "initial" // 只打包初始時依賴的第三方
}
}
}
}
})
複製代碼
看到這裏你或許有些累了,可是要想獲取更好的offer,更高的薪水,下面必須繼續深刻
優化配置對咱們來講很是有實際意義,這實際關係到你打包出來文件的大小,打包的速度等。 具體優化能夠分爲如下幾點:構建速度指的是咱們每次修改代碼後熱更新的速度以及發佈前打包文件的速度。
mode
可設置development
production
兩個參數
若是沒有設置,webpack4
會將 mode
的默認值設置爲 production
production
模式下會進行tree shaking
(去除無用代碼)和uglifyjs
(代碼壓縮混淆)
alias
: 當咱們代碼中出現 import 'vue'
時, webpack會採用向上遞歸搜索的方式去node_modules
目錄下找。爲了減小搜索範圍咱們能夠直接告訴webpack去哪一個路徑下查找。也就是別名(alias
)的配置。include exclude
一樣配置include exclude
也能夠減小webpack loader
的搜索轉換時間。noParse
當咱們代碼中使用到import jq from 'jquery'
時,webpack
會去解析jq這個庫是否有依賴其餘的包。可是咱們對相似jquery
這類依賴庫,通常會認爲不會引用其餘的包(特殊除外,自行判斷)。增長noParse
屬性,告訴webpack
沒必要解析,以此增長打包速度。extensions
webpack
會根據extensions
定義的後綴查找文件(頻率較高的文件類型優先寫在前面)
在webpack構建過程當中,實際上耗費時間大多數用在loader解析轉換以及代碼的壓縮中。平常開發中咱們須要使用Loader對js,css,圖片,字體等文件作轉換操做,而且轉換的文件數據量也是很是大。因爲js單線程的特性使得這些轉換操做不能併發處理文件,而是須要一個個文件進行處理。HappyPack的基本原理是將這部分任務分解到多個子進程中去並行處理,子進程處理完成後把結果發送到主進程中,從而減小總的構建時間
npm i -D happypack
複製代碼
上面對於loader轉換已經作優化,那麼下面還有另外一個難點就是優化代碼的壓縮時間。
npm i -D webpack-parallel-uglify-plugin
複製代碼
對於開發項目中不常常會變動的靜態依賴文件。相似於咱們的
elementUi、vue
全家桶等等。由於不多會變動,因此咱們不但願這些依賴要被集成到每一次的構建邏輯中去。 這樣作的好處是每次更改我本地代碼的文件的時候,webpack
只須要打包我項目自己的文件代碼,而不會再去編譯第三方庫。之後只要咱們不升級第三方包的時候,那麼webpack
就不會對這些庫去打包,這樣能夠快速的提升打包的速度。
這裏咱們使用webpack
內置的DllPlugin DllReferencePlugin
進行抽離
在與webpack
配置文件同級目錄下新建webpack.dll.config.js
代碼以下
// webpack.dll.config.js
const path = require("path");
const webpack = require("webpack");
module.exports = {
// 你想要打包的模塊的數組
entry: {
vendor: ['vue','element-ui']
},
output: {
path: path.resolve(__dirname, 'static/js'), // 打包後文件輸出的位置
filename: '[name].dll.js',
library: '[name]_library'
// 這裏須要和webpack.DllPlugin中的`name: '[name]_library',`保持一致。
},
plugins: [
new webpack.DllPlugin({
path: path.resolve(__dirname, '[name]-manifest.json'),
name: '[name]_library',
context: __dirname
})
]
};
複製代碼
在package.json
中配置以下命令
"dll": "webpack --config build/webpack.dll.config.js"
複製代碼
接下來在咱們的webpack.config.js
中增長如下代碼
module.exports = {
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./vendor-manifest.json')
}),
new CopyWebpackPlugin([ // 拷貝生成的文件到dist目錄 這樣每次沒必要手動去cv
{from: 'static', to:'static'}
]),
]
};
複製代碼
執行
npm run dll
複製代碼
會發現生成了咱們須要的集合第三地方 代碼的vendor.dll.js
咱們須要在html
文件中手動引入這個js
文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>老yuan</title>
<script src="static/js/vendor.dll.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
複製代碼
這樣若是咱們沒有更新第三方依賴包,就沒必要npm run dll
。直接執行npm run dev npm run build
的時候會發現咱們的打包速度明顯有所提高。由於咱們已經經過dllPlugin
將第三方依賴包抽離出來了。
咱們每次執行構建都會把全部的文件都重複編譯一遍,這樣的重複工做是否能夠被緩存下來呢,答案是能夠的,目前大部分
loader
都提供了cache
配置項。好比在babel-loader
中,能夠經過設置cacheDirectory
來開啓緩存,babel-loader?cacheDirectory=true
就會將每次的編譯結果寫進硬盤文件(默認是在項目根目錄下的node_modules/.cache/babel-loader
目錄內,固然你也能夠自定義)
但若是 loader
不支持緩存呢?咱們也有方法,咱們能夠經過cache-loader
,它所作的事情很簡單,就是 babel-loader
開啓 cache
後作的事情,將 loader
的編譯結果寫入硬盤緩存。再次構建會先比較一下,若是文件較以前的沒有發生變化則會直接使用緩存。使用方法如官方 demo 所示,在一些性能開銷較大的 loader 以前添加此 loader便可
npm i -D cache-loader
複製代碼
打包的速度咱們是進行了優化,可是打包後的文件體積倒是十分大,形成了頁面加載緩慢,浪費流量等,接下來讓咱們從文件體積上繼續優化
webpack-bundle-analyzer
將打包後的內容束展現爲方便交互的直觀樹狀圖,讓咱們知道咱們所構建包中真正引入的內容
npm i -D webpack-bundle-analyzer
複製代碼
接下來在
package.json
裏配置啓動命令
"analyz": "NODE_ENV=production npm_config_report=true npm run build"
複製代碼
windows請安裝npm i -D cross-env
"analyz": "cross-env NODE_ENV=production npm_config_report=true npm run build"
複製代碼
接下來npm run analyz
瀏覽器會自動打開文件依賴圖的網頁
按照官方文檔的解釋,若是咱們想引用一個庫,可是又不想讓
webpack
打包,而且又不影響咱們在程序中以CMD、AMD
或者window/global
全局等方式進行使用,那就能夠經過配置Externals
。這個功能主要是用在建立一個庫的時候用的,可是也能夠在咱們項目開發中充分使用Externals
的方式,咱們將這些不須要打包的靜態資源從構建邏輯中剔除出去,而使用CDN
的方式,去引用它們。
有時咱們但願咱們經過script
引入的庫,如用CDN的方式引入的jquery
,咱們在使用時,依舊用require
的方式來使用,可是卻不但願webpack
將它又編譯進文件中。這裏官網案例已經足夠清晰明瞭,你們有興趣能夠點擊瞭解
webpack 官網案例以下
<script
src="https://code.jquery.com/jquery-3.1.0.js"
integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
crossorigin="anonymous">
</script>
複製代碼
module.exports = {
//...
externals: {
jquery: 'jQuery'
}
};
複製代碼
import $ from 'jquery';
$('.my-element').animate(/* ... */);
複製代碼
這裏單獨提一下
tree-shaking
,是由於這裏有個坑。tree-shaking
的主要做用是用來清除代碼中無用的部分。目前在webpack4
咱們設置mode
爲production
的時候已經自動開啓了tree-shaking
。可是要想使其生效,生成的代碼必須是ES6模塊。不能使用其它類型的模塊如CommonJS
之流。若是使用Babel
的話,這裏有一個小問題,由於Babel
的預案(preset)默認會將任何模塊類型都轉譯成CommonJS
類型,這樣會致使tree-shaking
失效。修正這個問題也很簡單,在.babelrc
文件或在webpack.config.js
文件中設置modules: false
就行了
// .babelrc
{
"presets": [
["@babel/preset-env",
{
"modules": false
}
]
]
}
複製代碼
或者
// webpack.config.js
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', { modules: false }]
}
},
exclude: /(node_modules)/
}
]
}
複製代碼
經歷過上面兩個系列的洗禮,到如今咱們成爲了一名合格的webpack配置工程師。可是光擰螺絲,自身的可替代性仍是很高,下面咱們將深刻webpack的原理中去
經歷過上面兩個部分,咱們已經能夠熟練的運用相關的loader和plugin對咱們的代碼進行轉換、解析。接下來咱們本身手動實現loader與plugin,使其在平時的開發中得到更多的樂趣。
loader
從本質上來講其實就是一個node
模塊。至關於一臺榨汁機(loader)
將相關類型的文件代碼(code)
給它。根據咱們設置的規則,通過它的一系列加工後還給咱們加工好的果汁(code)
。
loader
編寫原則
Loader
只作一件事;Webpack
會按順序鏈式調用每一個 Loader
;Webpack
制定的設計規則和結構,輸入與輸出均爲字符串,各個 Loader
徹底獨立,即插即用;在平常開發環境中,爲了方便調試咱們每每會加入許多console
打印。可是咱們不但願在生產環境中存在打印的值。那麼這裏咱們本身實現一個loader
去除代碼中的console
知識點普及之
AST
。AST
通俗的來講,假設咱們有一個文件a.js
,咱們對a.js
裏面的1000行進行一些操做處理,好比爲全部的await
增長try catch
,以及其餘操做,可是a.js
裏面的代碼本質上來講就是一堆字符串。那咱們怎麼辦呢,那就是轉換爲帶標記信息的對象(抽象語法樹)咱們方便進行增刪改查。這個帶標記的對象(抽象語法樹)就是AST
。這裏推薦一篇不錯的AST文章 AST快速入門
npm i -D @babel/parser @babel/traverse @babel/generator @babel/types
複製代碼
@babel/parser
將源代碼解析成 AST
@babel/traverse
對AST
節點進行遞歸遍歷,生成一個便於操做、轉換的path
對象@babel/generator
將AST
解碼生成js
代碼@babel/types
經過該模塊對具體的AST
節點進行進行增、刪、改、查新建drop-console.js
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const t = require('@babel/types')
module.exports=function(source){
const ast = parser.parse(source,{ sourceType: 'module'})
traverse(ast,{
CallExpression(path){
if(t.isMemberExpression(path.node.callee) && t.isIdentifier(path.node.callee.object, {name: "console"})){
path.remove()
}
}
})
const output = generator(ast, {}, source);
return output.code
}
複製代碼
如何使用
const path = require('path')
module.exports = {
mode:'development',
entry:path.resolve(__dirname,'index.js'),
output:{
filename:'[name].[contenthash].js',
path:path.resolve(__dirname,'dist')
},
module:{
rules:[{
test:/\.js$/,
use:path.resolve(__dirname,'drop-console.js')
}
]
}
}
複製代碼
實際上在
webpack4
中已經集成了去除console
功能,在minimizer
中可配置 去除console
附上官網 如何編寫一個loader
在
Webpack
運行的生命週期中會廣播出許多事件,Plugin
能夠監聽這些事件,在合適的時機經過Webpack
提供的API
改變輸出結果。通俗來講:一盤美味的 鹽豆炒雞蛋 須要經歷燒油 炒制 調味到最後的裝盤等過程,而plugin
至關於能夠監控每一個環節並進行操做,好比能夠寫一個少放胡椒粉plugin
,監控webpack
暴露出的生命週期事件(調味),在調味的時候執行少放胡椒粉操做。那麼它與loader
的區別是什麼呢?上面咱們也提到了loader
的單一原則,loader
只能一件事,好比說less-loader
,只能解析less
文件,plugin
則是針對整個流程執行普遍的任務。
一個基本的plugin插件結構以下
class firstPlugin {
constructor (options) {
console.log('firstPlugin options', options)
}
apply (compiler) {
compiler.plugin('done', compilation => {
console.log('firstPlugin')
))
}
}
module.exports = firstPlugin
複製代碼
compiler 、compilation是什麼?
compiler
對象包含了Webpack
環境全部的的配置信息。這個對象在啓動 webpack
時被一次性創建,並配置好全部可操做的設置,包括 options
,loader
和 plugin
。當在 webpack
環境中應用一個插件時,插件將收到此 compiler
對象的引用。可使用它來訪問 webpack
的主環境。compilation
對象包含了當前的模塊資源、編譯生成資源、變化的文件等。當運行webpack
開發環境中間件時,每當檢測到一個文件變化,就會建立一個新的 compilation
,從而生成一組新的編譯資源。compilation
對象也提供了不少關鍵時機的回調,以供插件作自定義處理時選擇使用。compiler和 compilation的區別在於
compiler表明了整個webpack從啓動到關閉的生命週期,而compilation 只是表明了一次新的編譯過程
compiler和compilation暴露出許多鉤子,咱們能夠根據實際需求的場景進行自定義處理
下面咱們手動開發一個簡單的需求,在生成打包文件以前自動生成一個關於打包出文件的大小信息
新建一個webpack-firstPlugin.js
class firstPlugin{
constructor(options){
this.options = options
}
apply(compiler){
compiler.plugin('emit',(compilation,callback)=>{
let str = ''
for (let filename in compilation.assets){
str += `文件:${filename} 大小${compilation.assets[filename]['size']()}\n`
}
// 經過compilation.assets能夠獲取打包後靜態資源信息,一樣也能夠寫入資源
compilation.assets['fileSize.md'] = {
source:function(){
return str
},
size:function(){
return str.length
}
}
callback()
})
}
}
module.exports = firstPlugin
複製代碼
如何使用
const path = require('path')
const firstPlugin = require('webpack-firstPlugin.js')
module.exports = {
// 省略其餘代碼
plugins:[
new firstPlugin()
]
}
複製代碼
執行 npm run build
便可看到在dist
文件夾中生成了一個包含打包文件信息的fileSize.md
上面兩個
loader
與plugin
案例只是一個引導,實際開發需求中的loader
與plugin
要考慮的方面不少,建議你們本身多動手嘗試一下。
附上官網 如何編寫一個plugin
因爲篇幅過長,且原理深刻較多。鑑於本篇以快速上手應用於實際開發的原則,這裏決定另起一篇新的文章去詳細剖析
webpack
原理以及實現一個demo
版本。待格式校準後,將會貼出文章連接在下方
不管是前端框架仍是構建工具的更新速度遠遠超乎了咱們的想象,前幾年的jquery
一把梭的時代一去不復返。咱們要擁抱的是不斷更新迭代的vue、react、node、serverless、docker、k8s
....
webpack5.0
旨在減小配置的複雜度,使其更容易上手(
webpack4
的時候也說了這句話),以及一些性能上的提高
目前來看,維護者的更新很頻繁,相信用不了多久webpack5.0
將會擁抱大衆。感興趣的同窗能夠先安裝beta
版本嚐嚐鮮。不過在此以前建議你們先對webpack4
進行一番掌握,這樣後面的路纔會愈來愈好走。