Webpack配置全解析(基礎篇)

  Webpack憑藉強大的功能,成爲最流行和最活躍的打包工具,也是面試時高級程序員必須掌握的「軟技能」;筆者結合在項目中的使用經驗,介紹webpack的使用;本文是入門篇,主要介紹webpack的入口、輸出和各類loader、plugins的使用以及開發環境的搭建,技術大佬請ctrl+w。css

  本文全部的demo代碼均在WebpackDemohtml

概念

  來看一下官網對webpack的定義:前端

本質上,webpack 是一個現代 JavaScript 應用程序的靜態模塊打包器(module bundler)。當 webpack 處理應用程序時,它會遞歸地構建一個依賴關係圖(dependency graph),其中包含應用程序須要的每一個模塊,而後將全部這些模塊打包成一個或多個bundle。vue

  首先webpack是一個靜態模塊打包器,所謂的靜態模塊,包括腳本、樣式表和圖片等等;webpack打包時首先遍歷全部的靜態資源,根據資源的引用,構建出一個依賴關係圖,而後再將模塊劃分,打包出一個或多個bundle。再次白piao一下官網的圖,生動的描述了這個過程:node

logo.png

  提到webpack,就不得不提webpack的四個核心概念jquery

  • 入口(entry):指示 webpack 應該使用哪一個模塊,來做爲構建其內部依賴圖的開始
  • 輸出(output):在哪裏輸出它所建立的 bundles
  • loader:讓 webpack 可以去處理那些非 JavaScript 文件
  • 插件(plugins):用於執行範圍更廣的任務

你的第一個打包器

  咱們首先在全局安裝webpack:webpack

npm install webpack webpack-cli –g
複製代碼

  webpack能夠不使用配置文件,直接經過命令行構建,用法以下:git

webpack <entry> [<entry>] -o <output>
複製代碼

  這裏的entry和output就對應了上述概念中的入口和輸入,咱們來新建一個入口文件:程序員

//demo1/index.js
var a = 1
console.log(a)
document.write('hello webpack')
複製代碼

  有了入口文件咱們還須要經過命令行定義一下輸入路徑dist/bundle.js:es6

webpack index.js -o dist/bundle.js
複製代碼

  這樣webpack就會在dist目錄生成打包後的文件。

demo1-result.png

  咱們也能夠在項目目錄新建一個html引入打包後的bundle.js文件查看效果。

配置文件

  命令行的打包方式僅限於簡單的項目,若是咱們的項目較爲複雜,有多個入口,咱們不可能每次打包都把入口記下來;所以通常項目中都使用配置文件來進行打包;配置文件的命令方式以下:

webpack [--config webpack.config.js]
複製代碼

  配置文件默認的名稱就是webpack.config.js,一個項目中常常會有多套配置文件,咱們能夠針對不一樣環境配置不一樣的文件,經過--config來進行切換:

//生產環境配置
webpack --config webpack.prod.config.js
//開發環境配置
webpack --config webpack.dev.config.js
複製代碼

多種配置類型

  config配置文件經過module.exports導出一個配置對象:

//webpack.config.js
var path = require('path');
module.exports = {
  mode: 'development',
  //入口文件
  entry: './index.js',
  //輸出目錄
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  }
};
複製代碼

  除了導出爲對象,還能夠導出爲一個函數,函數中會帶入命令行中傳入的環境變量等參數,這樣能夠更方便的對環境變量進行配置;好比咱們在打包線上正式環境和線上開發環境能夠經過env進行區分:

var path = require('path');
//env:環境對象
module.exports = function(env, argv){
  return {
    //其餘配置
    entry: './index.js',
    output: {}
  }
};
複製代碼

  另外還能夠導出爲一個Promise,用於異步加載配置,好比能夠動態加載入口文件:

module.exports = () => {
    return new Promise((resolve, reject)=>{
        setTimeout(()=>{
            resolve({
                entry: './index.js',
                output: {}
            })
        }, 5000)
    })
}
複製代碼

入口

  正如在上面提到的,入口是整個依賴關係的起點入口;咱們經常使用的單入口配置是一個頁面的入口:

module.exports = {
    entry: './index.js',
}
複製代碼

  它是下面的簡寫:

module.exports = {
    entry: {
        main: './index.js'
    },
}
複製代碼

  可是咱們一個頁面可能不止一個模塊,所以須要將多個依賴文件一塊兒注入,這時就須要用到數組了,代碼在demo2中:

module.exports = {
    entry: [
        //輪播圖模塊
        './src/banner.js',
        //主模塊
        './src/index.js', 
        //底部模塊
        './src/foot.js'
    ],
}
複製代碼

  有時候咱們一個項目可能有不止一個頁面,須要將多個頁面分開打包,entry支持傳入對象的形式,代碼在demo3中:

//demo3
module.exports = {
    entry: {
        home: './src/home.js',
        list: './src/list.js',
        detail: ['./src/detail.js', './src/common.js'],
    },
}
複製代碼

  這樣webpack就會構建三個不一樣的依賴關係。

輸出

  output選項用來控制webpack如何輸入編譯後的文件模塊;雖然能夠有多個entry,可是隻能配置一個output

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js',
        //CDN地址
        publicPath: '/',
    },
}
複製代碼

  這裏咱們配置了一個單入口,輸出也就是bundle.js;可是若是存在多入口的模式就行不通了,webpack會提示Conflict: Multiple chunks emit assets to the same filename,即多個文件資源有相同的文件名稱;webpack提供了佔位符來確保每個輸出的文件都有惟一的名稱:

module.exports = {
    entry: {
        home: './src/home.js',
        list: './src/list.js',
        detail: ['./src/detail.js', './src/common.js'],
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].bundle.js',
    },
}
複製代碼

  這樣webpack打包出來的文件就會按照入口文件的名稱來進行分別打包生成三個不一樣的bundle文件;還有如下不一樣的佔位符字符串:

佔位符 描述
[hash] 模塊標識符(module identifier)的 hash
[chunkhash] chunk 內容的 hash
[name] 模塊名稱
[id] 模塊標識符
[query] 模塊的 query,例如,文件名 ? 後面的字符串

  在這裏引入Module、Chunk和Bundle的概念,上面代碼中也常常會看到有這兩個名詞的出現,那麼他們三者到底有什麼區別呢?首先咱們發現module是常常出如今咱們的代碼中,好比module.exports;而Chunk常常和entry一塊兒出現,Bundle老是和output一塊兒出現。

  • module:咱們寫的源碼,不管是commonjs仍是amdjs,均可以理解爲一個個的module
  • chunk:當咱們寫的module源文件傳到webpack進行打包時,webpack會根據文件引用關係生成chunk文件,webpack 會對這些chunk文件進行一些操做
  • bundle:webpack處理好chunk文件後,最後會輸出bundle文件,這個bundle文件包含了通過加載和編譯的最終源文件,因此它能夠直接在瀏覽器中運行。

  咱們經過下面一張圖更深刻的理解這三個概念:

chunk-bundle.png

總結:

  module,chunk 和 bundle 其實就是同一份邏輯代碼在不一樣轉換場景下的取了三個名字:咱們直接寫出來的是module,webpack處理時是chunk,最後生成瀏覽器能夠直接運行的bundle。

hash、chunkhash、contenthash

  理解了chunk的概念,相信上面表中chunkhash和hash的區別也很容易理解了;

  • hash:是跟整個項目的構建相關,只要項目裏有文件更改,整個項目構建的hash值都會更改,而且所有文件都共用相同的hash值。
  • chunkhash:跟入口文件的構建有關,根據入口文件構建對應的chunk,生成每一個chunk對應的hash;入口文件更改,對應chunk的hash值會更改。
  • contenthash:跟文件內容自己相關,根據文件內容建立出惟一hash,也就是說文件內容更改,hash就更改。

模式

  在webpack2和webpack3中咱們須要手動加入插件來進行代碼的壓縮、環境變量的定義,還須要注意環境的判斷,十分的繁瑣;在webpack4中直接提供了模式這一配置,開箱便可用;若是忽略配置,webpack還會發出警告。

module.exports = {
  mode: 'development',
};
//至關於
module.exports = {
   devtool:'eval',
   plugins: [
      new webpack.NamedModulesPlugin(),
      new webpack.NamedChunksPlugin(),
      new webpack.DefinePlugin({ 
        "process.env.NODE_ENV": JSON.stringify("development") 
      })
   ]
}
複製代碼

  開發模式是告訴webpack,我如今是開發狀態,也就是打包出來的內容要對開發友好,便於代碼調試以及實現瀏覽器實時更新。

module.exports = {
  mode: 'production',
};
//至關於
module.exports = {
   plugins: [
      new UglifyJsPlugin(/*...*/),
      new webpack.DefinePlugin({ 
        "process.env.NODE_ENV": JSON.stringify("production") 
      }),
      new webpack.optimize.ModuleConcatenationPlugin(),
      new webpack.NoEmitOnErrorsPlugin()
   ]
}
複製代碼

  生產模式不用對開發友好,只須要關注打包的性能和生成更小體積的bundle。看到這裏用到了不少Plugin,不用慌,下面咱們會一一解釋他們的做用。

  相信不少童鞋都曾有過疑問,爲何這邊DefinePlugin定義環境變量的時候要用JSON.stringify("production"),直接用"production"不是更簡單嗎?

  咱們首先來看下JSON.stringify("production")生成了什麼;運行結果是""production"",注意這裏,並非你眼睛花了或者屏幕上有小黑點,結果確實比"production"多嵌套了一層引號。

  咱們能夠簡單的把DefinePlugin這個插件理解爲將代碼裏的全部process.env.NODE_ENV替換爲字符串中的內容。假如咱們在代碼中有以下判斷環境的代碼:

//webpack.config.js
module.exports = {
  plugins: [
    new webpack.DefinePlugin({ 
      "process.env.NODE_ENV": "production"
    }),
  ]
}
//src/index.js
if (process.env.NODE_ENV === 'production') {
    console.log('production');
}
複製代碼

  這樣生成出來的代碼就會編譯成這樣:

//dist/bundle.js
//代碼中並無定義production變量
if (production === 'production') {
    console.log('production');
}
複製代碼

  可是咱們代碼中可能並無定義production變量,所以會致使代碼直接報錯,因此咱們須要經過JSON.stringify來包裹一層:

//webpack.config.js
module.exports = {
  plugins: [
    new webpack.DefinePlugin({ 
      //"process.env.NODE_ENV": JSON.stringify("production")
      //至關於
      "process.env.NODE_ENV": '"production"'
    }),
  ]
}
//dist/bundle.js
if ("production" === 'production') {
    console.log('production');
}
複製代碼

  這樣編譯出來的代碼就沒有問題了。

自動生成頁面

  在上面的代碼中咱們發現都是手動來生成index.html,而後引入打包後的bundle文件,可是這樣太過繁瑣,並且若是生成的bundle文件引入了hash值,每次生成的文件名稱不同,所以咱們須要一個自動生成html的插件;首先咱們須要安裝這個插件:

npm install --save-dev html-webpack-plugin
複製代碼

  在demo3中,咱們生成了三個不一樣的bundle.js,咱們但願在三個不一樣的頁面能分別引入這三個文件,以下修改config文件:

module.exports = {
    //省略其餘代碼
    plugins: [
        new HtmlWebpackPlugin({
            //引用的模板文件
            template: './index.html',
            //生成的html名稱
            filename: 'home.html',
            chunks: ['home']
        }),
        new HtmlWebpackPlugin({
            template: './index.html',
            filename: 'list.html',
            chunks: ['list']
        }),
        new HtmlWebpackPlugin({
            template: './index.html',
            filename: 'detail.html',
            chunks: ['detail']
        }),
    ]
}
複製代碼

  咱們以index.html做爲模板文件,生成home、list、detail三個不一樣的頁面,而且經過chunks分別引入不一樣的bundle;若是這裏不寫chunks,每一個頁面就會引入全部生成出來的bundle。

  html-webpack-plugin還支持如下字段:

new HtmlWebpackPlugin({
    template: './index.html',
    filename: 'all.html',
    //頁面注入title
    title: 'html webpack plugin title',
    //默認引入全部的chunks連接
    chunks: 'all',
    //注入頁面位置
    inject: true,
    //啓用hash
    hash: true,
    favicon: '',
    //插入meta標籤
    meta: {
        'viewport': 'width=device-width, initial-scale=1.0'
    },
    minify: {
        //清除script標籤引號
        removeAttributeQuotes: true,
        //清除html中的註釋
        removeComments: true,
        //清除html中的空格、換行符
        //將html壓縮成一行
        collapseWhitespace: false,
        //壓縮html的行內樣式成一行
        minifyCSS: true,
        //清除內容爲空的元素(慎用)
        removeEmptyElements: false,
        //清除style和link標籤的type屬性
        removeStyleLinkTypeAttributes: false
    }
}),
複製代碼

  上面設置title後須要在模板文件中設置模板字符串:

<title><%= htmlWebpackPlugin.options.title %></title>
複製代碼

loader

  loader用於對模塊module的源碼進行轉換,默認webpack只能識別commonjs代碼,可是咱們在代碼中會引入好比vue、ts、less等文件,webpack就處理不過來了;loader拓展了webpack處理多種文件類型的能力,將這些文件轉換成瀏覽器可以渲染的js、css。

  module.rules容許咱們配置多個loader,可以很清晰的看出當前文件類型應用了哪些loader,loader的代碼均在demo4中。

{
    module: {
        rules: [
            {
                test: /\.js$/,
                use: {
                    loader: 'babel-loader',
                    options: {}
                }
            },
            {
                test: /\.css$/,
                use: [
                    { loader: 'style-loader' },
                    { loader: 'css-loader' }
                ]
            },
        ]
    }
}
複製代碼

  咱們能夠看到rules屬性值是一個數組,每一個數組對象表示了不一樣的匹配規則;test屬性時一個正則表達式,匹配不一樣的文件後綴;use表示匹配了這個文件後調用什麼loader來處理,當有多個loader的時候,use就須要用到數組。

  多個loader支持鏈式傳遞,可以對資源進行流水線處理,上一個loader處理的返回值傳遞給下一個loader;loader處理有一個優先級,從右到左,從下到上;在上面demo中對css的處理就聽從了這個優先級,css-loader先處理,處理好了再給style-loader;所以咱們寫loader的時候也要注意先後順序。

css-loader和style-loader

  css-loader和style-loader從名稱看起來功能很類似,然而二者的功能有着很大的區別,可是他們常常會成對使用;安裝方法:

npm i -D css-loader style-loader
複製代碼

  css-loader用來解釋@import和url();style-loader用來將css-loader生成的樣式表經過<style>標籤,插入到頁面中去。

/* /src/head.css */
.head{
    display: flex;
    background: #666;
}
/* /src/foot.css */
.foot{
    background: #ccc;
}
/* /src/index.css */
@import './head.css';
@import './foot.css';
.wrap {
    background: #999;
}
複製代碼

  而後在入口文件中將index.css引入,就能看到打包的效果,頁面中插入了三個style標籤,代碼在demo4:

css-loader.png

sass-loader和less-loader

  這兩個loader看名字你們也能猜到了,就是用來處理sass和less樣式的。安裝方法:

npm i -D sass-loader less-loader node-sass
複製代碼

  在config中進行配置,代碼在demo4:

{
    //其餘配置
    rules: {
        test: /\.scss$/,
        use: [{
            loader: 'style-loader'
        }, {
            loader: 'css-loader'
        },{
            loader: 'sass-loader'
        }]
    },{
        test: /\.less$/,
        use: [{
            loader: 'style-loader'
        }, {
            loader: 'css-loader'
        },{
            loader: 'less-loader'
        }]
    }
}

複製代碼

postcss-loader

  都0202年了,小夥伴確定不想一個一個的手動添加-moz、-ms、-webkit等瀏覽器私有前綴;postcss提供了不少對樣式的擴展功能;啥都不說,先安裝起來:

npm i -D postcss-loader
複製代碼

  老規矩,仍是在config中進行配置:

rules: [{
    test: /\.scss$/,
    use: [{
        loader: 'style-loader'
    }, {
        loader: 'css-loader'
    }, {
        loader: 'postcss-loader'
    },{
        loader: 'sass-loader'
    }]
},{
    test: /\.less$/,
    use: [{
        loader: 'style-loader'
    }, {
        loader: 'css-loader'
    }, {
        loader: 'postcss-loader'
    },{
        loader: 'less-loader'
    }]
}]
複製代碼

  正當咱們興沖沖的打包看效果時,發現樣式仍是老樣子,並無什麼改變。

why

  這是由於postcss主要功能只有兩個:第一就是把css解析成JS能夠操做的抽象語法樹AST,第二就是調用插件來處理AST並獲得結果;因此postcss通常都是經過插件來處理css,並不會直接處理,因此咱們須要先安裝一些插件:

npm i -D autoprefixer postcss-plugins-px2rem cssnano
複製代碼

  在項目根目錄新建一個.browserslistrc文件。

> 0.25%
last 2 versions
複製代碼

  咱們將postcss的配置單獨提取到項目根目錄下的postcss.config.js

module.exports = {
    plugins: [
        //自動添加前綴
        require('autoprefixer'),
        //px轉爲rem,應用於移動端
        require('postcss-plugins-px2rem')({ remUnit: 75 }),
        //優化合並css
        require('cssnano'),
    ]
}
複製代碼

  有了autoprefixer插件,咱們打包後的css就自動加上了前綴。

babel-loader

  兼容低版本瀏覽器的痛相信不少童鞋都經歷過,寫完代碼發現本身的js代碼不能運行在IE10或者IE11上,而後嘗試着引入各類polyfill;babel的出現給咱們提供了便利,將高版本的ES6甚至ES7轉爲ES5;咱們首先安裝babel所須要的依賴:

npm i -D babel-loader @babel/core @babel/preset-env @babel/plugin-transform-runtime
npm i -S @babel/runtime
複製代碼

  而後在config添加loader對js進行處理:

{
    //省略其餘配置
    rules: [{
        test: /\.js/,
        use: {
            loader: 'babel-loader'
        }
    }]
}
複製代碼

  一樣的,咱們把babel的配置提取到根目錄,新建一個.babelrc文件:

{
    "presets": [
        "@babel/preset-env"
    ],
    "plugins": [
        "@babel/plugin-transform-runtime"
    ]
}
複製代碼

  咱們能夠在index.js中嘗試寫一些es6的語法,看到代碼會被轉譯成es5,代碼在demo4中。因爲babel-loader的轉譯速度很慢,在後面咱們加入了時間插件後能夠看到每一個loader的耗時,babel-loader是最耗時間;所以咱們要儘量少的使用babel來轉譯文件,咱們對config進行改進,

{
    //省略其餘配置
    rules: [{
        test: /\.js$/,
        use: {
            loader: 'babel-loader'
        },
        // exclude: /node_modules/,
        include: [path.resolve(__dirname, 'src')]
    }]
}
複製代碼

  正則上使用$來進行精確匹配,經過exclude將node_modules中的文件進行排除,include將只匹配src中的文件;能夠看出來include的範圍比exclude更縮小更精確,所以也是推薦使用include。

file-loader和url-loader

  file-loader和url-loader都是用來處理圖片、字體圖標等文件;url-loader工做時分兩種狀況:當文件大小小於limit參數,url-loader將文件轉爲base-64編碼,用於減小http請求;當文件大小大於limit參數時,調用file-loader進行處理;所以咱們優先使用url-loader,首先仍是進行安裝,安裝url-loader以前還須要把file-loader先安裝:

npm i file-loader url-loader -D
複製代碼

  接下來仍是修改config:

{
    //省略其餘配置
    rules: [{
        test: /\.(png|jpg|gif|jpeg|webp|svg|eot|ttf|woff|woff2)$/,
        use: {
            loader: 'url-loader',
            options: {
                //10k
                limit: 10240,
                //生成資源名稱
                name: '[name].[hash:8].[ext]',
                //生成資源的路徑
                outputPath: 'imgs/'
            },
            exclude: /node_modules/,
        }
    }]
}
複製代碼

  咱們在css中給body添加一個小於10k的居中背景圖片:

body{
    width: 100vw;
    height: 100vh;
    background: url(./bg.png) no-repeat;
    background-size: 400px 400px;
    background-position: center center;
}
複製代碼

  打包後查看body的樣式能夠發現圖片已經被替換成base64格式的url了,代碼在demo4。

url-loader.png

html-withimg-loader

  若是咱們在頁面上引用一個圖片,會發現打包後的html仍是引用了src目錄下的圖片,這樣明顯是錯誤的,所以咱們還須要一個插件對html引用的圖片進行處理:

npm i -D html-withimg-loader
複製代碼

  老樣子仍是在config中對html進行配置:

{
    //省略其餘配置
    rules: [{
        test: /\.(htm|html)$/,
        use: {
            loader: 'html-withimg-loader'
        }
    }]
}
複製代碼

  然鵝,打開頁面發現倒是這樣的:

html-withimg-loader.png

  這是由於在url-loader中把每一個圖片做爲一個模塊來處理了,咱們還須要去url-loader中修改:

use: {
    loader: 'url-loader',
    options: {
        //10k
        limit: 10240,
        esModule: false
    }
}
複製代碼

  這樣咱們在頁面上的圖片引用也被修改了,代碼在demo4中。

  html-withimg-loader會致使html-webpack-plugin插件注入title的模板字符串<%= htmlWebpackPlugin.options.title %>失效,原封不動的展現在頁面上;所以,若是咱們想保留二者的功能須要在配置config中把html-withimg-loader刪除而且經過下面的方式來引用圖片:

<img src="<%=require('./src/bg1.png') %>" alt="" srcset="">
複製代碼

vue-loader

  最後說一下一個比較特殊的vue-loader,看名字就知道是用來處理vue文件的。

npm i -D vue-loader vue-template-compiler
npm i -S vue
複製代碼

  咱們首先來建立一個vue文件,具體代碼在demo5中:

//src/App.vue
<template>
    <div id="app">
        <div class="box" @click="tap">{{title}}</div>
    </div>
</template>
<script>
export default {
    name: 'app',
    data(){
        return {
            title: 'app實例'
        }
    },
    methods: {
        tap(){
            this.title = this.title.split('').reverse().join('')
        }
    }
}
</script>
<style>
#app{
    font-size: 16px;
    background: #ccc;
}
</style>
複製代碼

  而後在webpack的入口文件中引用它:

//src/main.js
import Vue from 'vue'
import App from './App.vue'

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

  不過vue-loader和其餘loader不太同樣,除了將它和.vue文件綁定以外,還須要引入它的一個插件:

const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
    module: {
        rules: [
        //省略其餘loader
        {
            test: /\.vue$/,
            loader: 'vue-loader'
        }]
    },
    plugins: [
        new VueLoaderPlugin(),
    ]
}
複製代碼

  這樣咱們就能愉快的在代碼中寫vue了。

搭建開發環境

  在上面的demo中咱們都是經過命令行打包生成dist文件,而後直接打開html或者經過static-server來查看頁面的;可是開發中咱們寫完代碼每次都來打包會嚴重影響開發的效率,咱們指望的是寫完代碼後當即就可以看到頁面的效果;webpack-dev-server就很好的提供了一個簡單的web服務器,可以實時從新加載。

  首先在咱們的項目中安裝依賴:

npm i -D webpack webpack-dev-server
複製代碼

  webpack-dev-server的用法和wepack同樣,只不過他會額外啓動一個express的服務器。咱們在項目中新建一個webpack.dev.config.js配置文件,單獨對開發環境進行一個配置,相關代碼在demo6中:

module.exports = {
    //省略其餘配置
    devServer: {
        //啓動服務器端口
        port: 9000,
        //默認是localhost,只能本地訪問
        host: "0.0.0.0",
        //自動打開瀏覽器
        open: false,
        //啓用模塊熱替換
        hot: true,
        //啓用gzip壓縮
        compress: true
    },
    plugins: [
        //熱更新插件
        new webpack.HotModuleReplacementPlugin({
        })
    ]
}
複製代碼

  經過命令行webpack-dev-server來啓動服務器,啓動後咱們發現根目錄並無生成任何文件,由於webpack打包到了內存中,不生成文件的緣由在於訪問內存中的代碼比訪問文件中的代碼更快。

  咱們在public/index.html的頁面上有時候會引用一些本地的靜態文件,直接打開頁面的會發現這些靜態文件的引用失效了,咱們能夠修改server的工做目錄,同時指定多個靜態資源的目錄:

contentBase: [
  path.join(__dirname, "public"),
  path.join(__dirname, "assets")
]
複製代碼

  熱更新(Hot Module Replacemen簡稱HMR)是在對代碼進行修改並保存以後,webpack對代碼從新打包,而且將新的模塊發送到瀏覽器端,瀏覽器經過新的模塊替換老的模塊,這樣就能在不刷新瀏覽器的前提下實現頁面的更新。

dev-server.png

  能夠看出瀏覽器和webpack-dev-server之間經過一個websock進行鏈接,初始化的時候client端保存了一個打包後的hash值;每次更新時server監聽文件改動,生成一個最新的hash值再次經過websocket推送給client端,client端對比兩次hash值後向服務器發起請求返回更新後的模塊文件進行替換。

  咱們點擊源碼旁的行數看一下編譯後的源碼是什麼樣的:

source-map.png

  發現跟咱們的源碼差距仍是挺大的,原本是一個簡單add函數,經過webpack的模塊封裝,已經很難理解原來代碼的含義了,所以,咱們須要將編譯後的代碼映射回源碼;devtool中不一樣的配置有不一樣的效果和速度,綜合性能和品質後,咱們通常在開發環境使用cheap-module-eval-source-map,在生產環境使用source-map

module.exports = {
    devtool: 'cheap-module-eval-source-map',
    //其餘配置
}
複製代碼

  其餘各模式的對比:

devtool.png

plugins

  在上面咱們也介紹了DefinePlugin、HtmlWebpackPlugin等不少插件,咱們發現這些插件都可以不一樣程度的影響着webpack的構建過程,下面還有一些經常使用的插件,plugins相關代碼在demo7中。

clean-webpack-plugin

  clean-webpack-plugin用於在打包前清理上一次項目生成的bundle文件,它會根據output.path自動清理文件夾;這個插件在生產環境用的頻率很是高,由於生產環境常常會經過hash生成不少bundle文件,若是不進行清理的話每次都會生成新的,致使文件夾很是龐大;這個插件安裝使用很是方便:

npm i -D clean-webpack-plugin
複製代碼

  安裝後咱們在config中配置一下就能夠了:

const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
    //其餘配置
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            template: './public/index.html',
            filename: 'index.html',
        })
    ]
}
複製代碼

mini-css-extract-plugin

  咱們以前的樣式都是經過style-loader插入到頁面中去,可是生產環境須要單獨抽離樣式文件,mini-css-extract-plugin就能夠幫我從js中剝離樣式:

npm i -D mini-css-extract-plugin
複製代碼

  咱們在開發環境使用style-loader,生產環境使用mini-css-extract-plugin:

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
    //其餘配置
    module: {
        rules: [
            {
                test: /\.less/,
                use: [{
                    loader: isDev ? 'style-loader' : MiniCssExtractPlugin.loader
                },{
                    loader: 'css-loader'
                },{
                    loader: 'less-loader'
                }]
            }
        ]
    },
    plugins: [
        new MiniCssExtractPlugin({
            filename: "[name].[hash:8].css",
        })
    ]
}
複製代碼

  引入loader後,咱們還須要配置plugin,提取的css一樣支持output.filename中的佔位符字符串。

optimize-css-assets-webpack-plugin

  咱們能夠發現雖然配置了production模式,打包出來的js壓縮了,可是打包出來的css確沒有壓縮;在生產環境咱們須要對css進行一下壓縮:

npm i optimize-css-assets-webpack-plugin -D
複製代碼

  而後也是引入插件:

const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
    //其餘配置
    plugins: [
        new OptimizeCSSAssetsPlugin(),
    ]
}
複製代碼

copy-webpack-plugin

  和demo6中同樣,咱們在public/index.html中引入了靜態資源,可是打包的時候webpack並不會幫咱們拷貝到dist目錄,所以copy-webpack-plugin就能夠很好地幫我作拷貝的工做了

npm i -D copy-webpack-plugin
複製代碼

  在config中配置咱們須要拷貝的源路徑和目標路徑:

const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
    plugins: [
        new CopyWebpackPlugin({
            patterns: [
                {
                    from: 'public/js/*.js',
                    to: path.resolve(__dirname, 'dist', 'js'),
                    flatten: true,
                }
            ]
        }),
    ]
}
複製代碼

ProvidePlugin

  ProvidePlugin能夠很快的幫咱們加載想要引入的模塊,而不用require。通常咱們加載jQuery須要先把它import:

import $ from 'jquery'
$('.box').html('box')
複製代碼

  可是咱們在config中配置ProvidePlugin插件後可以不用import,直接使用$

module.exports = {
    plugins: [
        new webpack.ProvidePlugin({
            $: 'jquery',
            jQuery: 'jquery'
        }),
    ]
}
複製代碼

  可是若是在項目中引入了太多模塊而且沒有require會讓人摸不着頭腦,所以建議加載一些常見的好比jQuery、vue、lodash等。

no-idea.png

loader和plugin的區別

  介紹了這麼多loader和plugin,咱們來回顧一下他們二者的區別:

loader:因爲webpack只能識別js,loader至關於翻譯官的角色,幫助webpack對其餘類型的資源進行轉譯的預處理工做。 plugins:plugins擴展了webpack的功能,在webpack運行時會廣播不少事件,plugin能夠監聽這些事件,而後經過webpack提供的API來改變輸出結果。

總結

  最後,介紹了這麼多,本文是webpack基礎篇,還有不少生產環境的優化尚未寫到;所以各位看官敬請期待優化篇。

參考:

再來一打Webpack面試題 Webpack HMR 原理解析

更多前端資料請關注公衆號【前端壹讀】

若是以爲寫得還不錯,請關注個人掘金主頁。更多文章請訪問謝小飛的博客

相關文章
相關標籤/搜索