手寫webpack

前言

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

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

[kaiche.gifhtml

](https://postimg.cc/68vMQxVm)前端

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


1.1 初始化項目

新建一個目錄,初始化npmvue

npm init
複製代碼

webpack是運行在node環境中的,咱們須要安裝如下兩個npm包node

npm i -D webpack webpack-cli
複製代碼
  • npm i -D 爲npm install --save-dev的縮寫
  • npm i -S 爲npm install --save的縮寫

新建一個文件夾src ,而後新建一個文件index.js,寫一點代碼測試一下react

console.log('call me 老yuan')
複製代碼

配置package.json命令[](https://postimg.cc/BtwWLsDS)jquery

[20191211104852887.pngwebpack

](https://postimg.cc/BtwWLsDS)git

執行github

npm run build
複製代碼

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

1.2 開始咱們本身的配置

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

// 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')  // 打包後的目錄
    }
}
複製代碼

更改咱們的打包命令[](https://postimg.cc/xX1bsv8y)

[p2.png

](https://postimg.cc/xX1bsv8y)

執行 npm run build 會發現生成了如下目錄(圖片)
其中dist文件夾中的main.js就是咱們須要在瀏覽器中實際運行的文件
固然實際運用中不會僅僅如此,下面讓咱們經過實際案例帶你快速入手webpack

1.3 配置html模板

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

這裏可能有的朋友會認爲咱們打包js文件名稱不是一直是固定的嘛(output.js)?這樣每次就不用改動引入文件名稱了呀?實際上咱們平常開發中每每會這樣配置:
module.exports = {
    // 省略其餘配置
    output: {
      filename: '[name].[hash:8].js',      // 打包後的文件名稱
      path: path.resolve(__dirname,'../dist')  // 打包後的目錄
    }
}
複製代碼

這時候生成的dist目錄文件以下[](https://postimg.cc/8sv9ZY0D)

[p3.png

](https://postimg.cc/8sv9ZY0D)

爲了緩存,你會發現打包好的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')
      })
    ]
}
複製代碼

生成目錄以下(圖片)[](https://postimg.cc/6yYkZvPG)

[p4.png

](https://postimg.cc/6yYkZvPG)

能夠發現打包生成的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'] // 與入口文件對應的模塊名
      }),
    ]
}

複製代碼

此時會發現生成如下目錄[](https://postimg.cc/4m9c76yW)

[p5.png

](https://postimg.cc/4m9c76yW)

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文件[](https://postimg.cc/Lnp9DLvC)

[p6.png

](https://postimg.cc/Lnp9DLvC)

同時咱們也須要一些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以下[](https://postimg.cc/06YRbB19)

[p7.png

](https://postimg.cc/06YRbB19)

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敲一遍對閱讀下面的文章更有益,建議入門的同窗敲三遍以上 [

    學習.png

    • *
上面的實踐是咱們對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 配置打包命令

[c4.png

](https://postimg.cc/BtLYNxDh)

打包文件已經配置完畢,接下來讓咱們測試一下
首先在src新建一個main.js[

c1.png

](https://postimg.cc/tYNkJDWM)...[

c2.png

](https://postimg.cc/N5C4RFC6)...,裏面新建一個index.html[

c3.png

](https://postimg.cc/ZCv9DqKF)執行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,更高的薪水,下面必須繼續深刻[](https://postimg.cc/ZCRcC9tS)

[qiong111.jpg

](https://postimg.cc/ZCRcC9tS)

優化配置對咱們來講很是有實際意義,這實際關係到你打包出來文件的大小,打包的速度等。 具體優化能夠分爲如下幾點:

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

    carbon-2.png

    ](https://postimg.cc/nX0FxL4W)

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

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

[carbon-3.png

](https://postimg.cc/3kgP5ykv)

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

上面對於loader轉換已經作優化,那麼下面還有另外一個難點就是優化代碼的壓縮時間。
npm i -D webpack-parallel-uglify-plugin
複製代碼

[carbon-4.png

](https://postimg.cc/mt6QLVHx)

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
複製代碼

[carbon-5.png

](https://postimg.cc/wyVw0Jb6)

2.5.2 優化打包文件體積

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

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

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

npm i -D webpack-bundle-analyzer
複製代碼

[carbon-6.png

](https://postimg.cc/D4WxQcmR)

接下來在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瀏覽器會自動打開文件依賴圖的網頁

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類型。修正這個問題也很簡單,在 .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的原理中去

[TEST3.png

](https://postimg.cc/T5pYLdw3)

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')
module.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進行一番掌握,這樣後面的路纔會愈來愈好走。

做者:黃蟲子
連接:https://juejin.im/post/5de874... 來源:掘金 著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。

相關文章
相關標籤/搜索