2020年了,再不會webpack敲得代碼就不香了(萬字實戰)

前言

2020年即將到來,在衆多前端的招聘要求裏,webpack工程化這些字眼頻率越來越高。日常開發者中,我們常常在用諸如vue-clicreate-react-app的腳手架來構建我們的項目。但是如果你想在團隊脫穎而出(鶴立雞羣)、拿到更好的offer(還房貸),那麼你必須去深刻的認識下我們經常打交道的webpack

本文共分爲三個部分帶你快速掌握webpack,閱讀本篇大概需要60分鐘。如有不足之處,懇請斧正

本文編寫基於

  • webpack 4.41.2版本
  • node: 10.15.3 版本

1 入門(一起來用這些小例子讓你熟悉webpack的配置)


1.1 初始化項目

新建一個目錄,初始化npm

npm init

webpack是運行在node環境中的,我們需要安裝以下兩個npm包

npm i -D webpack webpack-cli
  • npm i -D 爲npm install --save-dev的縮寫
  • npm i -S 爲npm install --save的縮寫

新建一個文件夾src ,然後新建一個文件main.js,寫一點代碼測試一下

console.log('test 1...')

配置package.json命令

  "scripts": {
    "build": "webpack src/main.js"
  },

執行

npm run build

此時如果生成了一個dist文件夾,並且內部含有main.js說明已經打包成功了

1.2 開始我們自己的配置

上面一個簡單的例子只是webpack自己默認的配置,下面我們要實現更加豐富的自定義配置
新建一個build文件夾,裏面新建一個webpack.config.js

// 自定義配置文件:webpack.config.js

const path = require('path')
module.exports = {
    mode: 'development',    // 開發模式
    // mode: 'production',
    entry: path.resolve(__dirname, '../src/main.js'), // 項目入口文件
    output: {
        filename: 'output.js',  // 打包後的文件名稱
        path: path.resolve(__dirname, '../dist') // 打包後的目錄
    }
}

更改我們的打包命令

  "scripts": {
    "build": "webpack --config build/webpack.config.js"
  },

執行 npm run build
會發現生成了以下目錄(圖片)

其中dist文件夾中的main.js就是我們需要在瀏覽器中實際運行的文件

當然實際運用中不會僅僅如此,下面讓我們通過實際案例帶你快速入手webpack

1.3 配置html模板

js文件打包好了,但是我們不可能每次在html文件中手動引入打包好的js

這裏可能有的朋友會認爲我們打包js文件名稱不是一直是固定的嘛(output.js)?這樣每次就不用改動引入文件名稱了呀?實際上我們日常開發中往往會這樣配置:

module.exports = {
    mode: 'development',    // 開發模式
    // mode: 'production',
    entry: path.resolve(__dirname, '../src/main.js'), // 項目入口文件
    output: {
        // filename: 'output.js',  // 打包後的文件名稱
        filename: '[name].[hash:8].js',  // 日常開發中的配置
        path: path.resolve(__dirname, '../dist') // 打包後的目錄
    }
}

這時候生成的dist目錄文件如:

dist/main.9fle44ba.js

爲了緩存,你會發現打包好的js文件的名稱每次都不一樣。webpack打包出來的js文件我們需要引入到html中,但是每次我們都手動修改js文件名顯得很麻煩,因此我們需要一個插件來幫我們完成這件事情

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文件中

1.3.1 多入口文件如何開發

生成多個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'] // 與入口文件對應的模塊名
      }),
    ]
}

1.3.2 clean-webpack-plugin

每次執行npm run build 會發現dist文件夾裏會殘留上次打包的文件,這裏我們推薦一個plugin來幫我們在打包輸出前清空文件夾clean-webpack-plugin

const {CleanWebpackPlugin} = require('clean-webpack-plugin')
module.exports = {
    // ...省略其他配置
    plugins:[new CleanWebpackPlugin()]
}

1.4 引用CSS

我們的入口文件是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如下

1.4.1 爲css添加瀏覽器前綴

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文件怎麼做呢?這時候我們就需要藉助插件來幫助我們

1.4.2 拆分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",
    })
  ]
}

1.4.3 拆分多個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
    ]
}

1.5 打包 圖片、字體、媒體、等文件

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]'
                }
              }
            }
          }
        ]
      },
    ]
  }
}

1.6 用babel轉義js文件

爲了使我們的js代碼兼容更多的環境我們需要安裝依賴

npm i babel-loader @babel/preset-env @babel/core
  • 注意 babel-loaderbabel-core的版本對應關係
  1. babel-loader 8.x 對應babel-core 7.x
  2. babel-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')"],    // 入口文件
}

  • 手動把上面的demo敲一遍對閱讀下面的文章更有益,建議入門的同學敲三遍以上

上面的實踐是我們對webpack的功能有了一個初步的瞭解,但是要想熟練應用於開發中,我們需要一個系統的實戰。讓我們一起擺脫腳手架嘗試自己搭建一個vue開發環境

2 搭建vue開發環境

上面的小例子已經幫助而我們實現了打包css、圖片、js、html等文件。 但是我們還需要以下幾種配置

2.1 解析.vue文件

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()
   ]
}

2.2 配置webpack-dev-server進行熱更新

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()
    ]
}

2.3 配置打包命令

"scripts": {
	"dev":"webpack-dev-server --config build/webpack.config.js --open",
	"build":"webpack --config build/webpack.config.js",
}

打包文件已經配置完畢,接下來讓我們測試一下

首先在src新建一個main.js

import Vue form 'vue'
import App from './app'

new Vue({
	render: h => h(App)
}).$mount('#app')

新建一個App.vue
新建一個public文件夾,裏面新建一個index.html

執行npm run dev這時候如果瀏覽器出現Vue開發環境運行成功,那麼恭喜你,已經成功邁出了第一步

2.4 區分開發環境與生產環境

實際應用到項目中,我們需要區分開發環境與生產環境,我們在原來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 壓縮css
  • uglifyjs-webpack-plugin 壓縮js

webpack mode設置production的時候會自動壓縮js代碼。原則上不需要引入uglifyjs-webpack-plugin進行重複工作。但是optimize-css-assets-webpack-plugin壓縮css的同時會破壞原有的js壓縮,所以這裏我們引入uglifyjs進行壓縮

2.4.1 webpack.config.js

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'
    })
  ]
}

2.4.2 webpack.dev.js

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()
  ]
})

2.4.3 webpack.prod.js

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" // 只打包初始時依賴的第三方
        }
      }
    }
  }
})

2.5 優化webpack配置

看到這裏你或許有些累了,但是要想獲取更好的offer,更高的薪水,下面必須繼續深入

優化配置對我們來說非常有實際意義,這實際關係到你打包出來文件的大小,打包的速度等。 具體優化可以分爲以下幾點:

2.5.1 優化打包速度

構建速度指的是我們每次修改代碼後熱更新的速度以及發佈前打包文件的速度。

2.5.1.1 合理的配置mode參數與devtool參數

mode可設置development production兩個參數
如果沒有設置,webpack4 會將 mode 的默認值設置爲 production
production模式下會進行tree shaking(去除無用代碼)和uglifyjs(代碼壓縮混淆)

2.5.1.2 縮小文件的搜索範圍(配置include exclude alias noParse extensions)

  • 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定義的後綴查找文件(頻率較高的文件類型優先寫在前面)

2.5.1.3 使用HappyPack開啓多進程Loader轉換

在webpack構建過程中,實際上耗費時間大多數用在loader解析轉換以及代碼的壓縮中。日常開發中我們需要使用Loader對js,css,圖片,字體等文件做轉換操作,並且轉換的文件數據量也是非常大。由於js單線程的特性使得這些轉換操作不能併發處理文件,而是需要一個個文件進行處理。HappyPack的基本原理是將這部分任務分解到多個子進程中去並行處理,子進程處理完成後把結果發送到主進程中,從而減少總的構建時間

npm i -D happypack

2.5.1.4 使用webpack-parallel-uglify-plugin 增強代碼壓縮

上面對於loader轉換已經做優化,那麼下面還有另一個難點就是優化代碼的壓縮時間。

npm i -D webpack-parallel-iuglify-plugin

2.5.1.5 抽離第三方模塊

對於開發項目中不經常會變更的靜態依賴文件。類似於我們的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將第三方依賴包抽離出來了。

2.5.1.6 配置緩存

我們每次執行構建都會把所有的文件都重複編譯一遍,這樣的重複工作是否可以被緩存下來呢,答案是可以的,目前大部分 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

2.5.2 優化打包文件體積

打包的速度我們是進行了優化,但是打包後的文件體積卻是十分大,造成了頁面加載緩慢,浪費流量等,接下來讓我們從文件體積上繼續優化

2.5.2.1 引入webpack-bundle-analyzer分析打包後的文件

webpack-bundle-analyzer將打包後的內容束展示爲方便交互的直觀樹狀圖,讓我們知道我們所構建包中真正引入的內容

npm i -D webpack-bundle-analyzer

接下來在package.json裏配置啓動命令

"analyz": "MODE_ENV=production npm_config_report=true npm run build"

windows請安裝npm i -D cross-env

"analyz": "cross-env MODE_ENV=production npm_config_report=true npm run build"

接下來npm run analyz瀏覽器會自動打開文件依賴圖的網頁

2.5.2.3 externals

按照官方文檔的解釋,如果我們想引用一個庫,但是又不想讓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(/* ... */);

2.5.2.3 Tree-shaking

這裏單獨提一下tree-shaking,是因爲這裏有個坑。tree-shaking的主要作用是用來清除代碼中無用的部分。目前在webpack4 我們設置modeproduction的時候已經自動開啓了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的原理中去

3 手寫webpack系列

經歷過上面兩個部分,我們已經可以熟練的運用相關的loader和plugin對我們的代碼進行轉換、解析。接下來我們自己手動實現loader與plugin,使其在平時的開發中獲得更多的樂趣。

3.1 手寫webpack loader

loader從本質上來說其實就是一個node模塊。相當於一臺榨汁機(loader)將相關類型的文件代碼(code)給它。根據我們設置的規則,經過它的一系列加工後還給我們加工好的果汁(code)

loader編寫原則

  • 單一原則: 每個 Loader 只做一件事;
  • 鏈式調用: Webpack 會按順序鏈式調用每個 Loader
  • 統一原則: 遵循 Webpack 制定的設計規則和結構,輸入與輸出均爲字符串,各個 Loader 完全獨立,即插即用;

在日常開發環境中,爲了方便調試我們往往會加入許多console打印。但是我們不希望在生產環境中存在打印的值。那麼這裏我們自己實現一個loader去除代碼中的console

知識點普及之ASTAST通俗的來說,假設我們有一個文件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/traverseAST節點進行遞歸遍歷,生成一個便於操作、轉換的path對象
  • @babel/generatorAST解碼生成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

3.2 手寫webpack plugin

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 時被一次性建立,並配置好所有可操作的設置,包括 optionsloaderplugin。當在 webpack 環境中應用一個插件時,插件將收到此 compiler 對象的引用。可以使用它來訪問 webpack 的主環境。
  • compilation對象包含了當前的模塊資源、編譯生成資源、變化的文件等。當運行webpack 開發環境中間件時,每當檢測到一個文件變化,就會創建一個新的 compilation,從而生成一組新的編譯資源。compilation 對象也提供了很多關鍵時機的回調,以供插件做自定義處理時選擇使用。

compiler和 compilation的區別在於

  • compiler代表了整個webpack從啓動到關閉的生命週期,而compilation 只是代表了一次新的編譯過程

  • compiler和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')

moudle.exports = {
	//...
	plugins: [
		new firstPlugin
	]
}

執行 npm run build即可看到在dist文件夾中生成了一個包含打包文件信息的fileSize.md

上面兩個loaderplugin案例只是一個引導,實際開發需求中的loaderplugin要考慮的方面很多,建議大家自己多動手嘗試一下。

附上官網 如何編寫一個plugin

3.3 手寫webpack

由於篇幅過長,且原理深入較多。鑑於本篇以快速上手應用於實際開發的原則,這裏決定另起一篇新的文章去詳細剖析webpack原理以及實現一個demo版本。待格式校準後,將會貼出文章鏈接在下方

4 webpack5.0的時代

無論是前端框架還是構建工具的更新速度遠遠超乎了我們的想象,前幾年的jquery一把梭的時代一去不復返。我們要擁抱的是不斷更新迭代的vue、react、node、serverless、docker、k8s....

不甘落後的webpack也已經在近日發佈了 webpack 5.0.0 beta 10 版本。在之前作者也曾提過webpack5.0旨在減少配置的複雜度,使其更容易上手(webpack4的時候也說了這句話),以及一些性能上的提升

  • 使用持久化緩存提高構建性能;
  • 使用更好的算法和默認值改進長期緩存(long-term caching);
  • 清理內部結構而不引入任何破壞性的變化;
  • 引入一些breaking changes,以便儘可能長的使用v5版本。

目前來看,維護者的更新很頻繁,相信用不了多久webpack5.0將會擁抱大衆。感興趣的同學可以先安裝beta版本嚐嚐鮮。不過在此之前建議大家先對webpack4進行一番掌握,這樣後面的路纔會越來越好走。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章