webpack從此不再是我們的痛點 — 核心基礎

ngaiwe發表於2018-04-14

webpack一直是前端工程師的痛點,因為他的複雜、分散、loader、plugin這些第三方,讓我們的學習成本陡然上升,使我們一直對他的配置模稜兩可,今天帶大家徹底明白他如何配置,擺脫困擾我們很久的痛點。本篇主要是webpack基礎配置詳解,關於webpack的模組chunk、編譯階段流程、輸出階段流程、loader的編寫和手寫plugin會在後續文章推出,為了避免錯過可以關注我或者收藏我的個人部落格www.ngaiwe.com

1.webpack是什麼?

WebPack可以看做是模組打包機:它做的事情是,分析你的專案結構,找到JavaScript模組以及其它的一些瀏覽器不能直接執行的擴充語言(Scss,TypeScript等),並將其打包為合適的格式以供瀏覽器使用。並且跟具你的在專案中的各種需求,實現自動化處理,解放我們的生產力

  • 程式碼轉換:TypeScript 編譯成 JavaScript、SCSS 編譯成 CSS 。
  • 檔案優化:壓縮 JavaScript、CSS、HTML 程式碼,壓縮合並圖片等。
  • 程式碼分割:提取多個頁面的公共程式碼、提取首屏不需要執行部分的程式碼讓其非同步載入。
  • 模組合併:在採用模組化的專案裡會有很多個模組和檔案,需要構建功能把模組分類合併成一個檔案。
  • 自動重新整理:監聽本地原始碼的變化,自動重新構建、重新整理瀏覽器。
  • 程式碼校驗:在程式碼被提交到倉庫前需要校驗程式碼是否符合規範,以及單元測試是否通過。
  • 自動釋出:更新完程式碼後,自動構建出線上釋出程式碼並傳輸給釋出系統。

2.專案初始化

mkdir webpack-start
cd webpack-start
npm init
複製程式碼

3.webpack核心概念

  • Entry:入口,webpack執行構建的第一步將從Entry開始,可抽象理解為輸入
  • Module:模組,在webpacl中一切皆為模組,一個模組對應一個檔案,webpack會從配置的Entry開始遞迴找出所有依賴的模組
  • Chunk:程式碼塊,一個chunk由多個模組組合而成,用於將程式碼合併和分割
  • Loader:模組轉換器,用於把模組原內容按照需求轉換為需要的新內容
  • Plugin:擴充套件外掛,在webpack構建流程中的特定時機注入擴充套件邏輯來改變構建結果和想要做的事情
  • Output:輸入結果,在webpack經過一系列處理並得到最終想要的程式碼然後輸出結果

Webpack 啟動後會從 Entry 裡配置的 Module 開始遞迴解析 Entry 依賴的所有 Module。 每找到一個 Module, 就會根據配置的Loader去找出對應的轉換規則,對 Module 進行轉換後,再解析出當前 Module 依賴的 Module。 這些模組會以 Entry 為單位進行分組,一個 Entry 和其所有依賴的 Module 被分到一個組也就是一個 Chunk。最後 Webpack 會把所有 Chunk 轉換成檔案輸出。 在整個流程中 Webpack 會在恰當的時機執行 Plugin 裡定義的邏輯。

1.Entry

context用來解決配置檔案和入口檔案不再同一層結構,列如我們配置檔案在config,入口檔案在根目錄,則如下配置

module.exports = {
  context: path.join(__dirname, '..'), // 找到根目錄
  entry: './main.js' //根目錄下的入口檔案
}
複製程式碼

最簡單的單頁面(SPA)Entry入口,將main.js引入,並根據main.js中引用和依賴的模組開始解析

module.exports = {
  entry: './main.js'
}
複製程式碼

多頁面(MPA)Entry入口,將多個檔案引入,當然一般是讀取指定資料夾內的入口檔案,然後引入

entry: {
  home: "./home.js",
  about: "./about.js",
  contact: "./contact.js"
}
複製程式碼

如果是單頁面(傳入的是字串或字串陣列),則chunk會被命名為main,如果是多頁面(傳入一個物件),則每個鍵(key)會是chunk的名稱,描述了chunk的入口起點

2.Output

Object 指示webpack如何去輸出,以及在哪裡輸出你的bundle、asset 和其他你所打包或使用 webpack 載入的任何內容

  • path:輸出目錄對應一個絕對路徑

    path: path.resolve(__dirname, 'dist')
    複製程式碼
  • pathinfo:boolean 預設false作用是告訴webpack在bundle中引入所包含模組資訊的相關注釋,不應用於生產環境(production),對開發環境(development)極其有用

  • publicPath:主要作用是針對打包後的檔案裡面的靜態檔案路徑處理

  • filename:定義每個輸出bundle的名稱,這些bundle將寫入output.path選項指定的目錄下,對於單入口Entry,filename是一個靜態名稱

    filename: "bundle.js"
    複製程式碼

    但是在webpack中我們會用到程式碼拆分、各種外掛plugin或多入口Entry建立多個bundle,這樣我們就應該給每個bundle一個唯一的名稱

    filename: "[name].bundle.js"
    複製程式碼

    使用內部chunk id

    filename: "[id].bundle.js"
    複製程式碼

    唯一hash生成

    filename: "[name].[hash].bundle.js"
    複製程式碼

    使用基於每個 chunk 內容的 hash

    filename: "[chunkhash].bundle.js"
    複製程式碼
3.Module模組

處理專案中應用的不同模組,主要配置皆在Rules中,匹配到請求的規則陣列,這些規則能夠對模組應用loader,或者修改解析器parser

  • Module.noParse: 防止webpack解析的時候,將規則匹配成功的檔案進行解析和忽略大型的library來對效能的優化,在被忽略的檔案中不應該含有import、require和define的呼叫

    module.exports = {
      module: {
        rules: [],
        noParse: function(content) {
          return /jquery|lodash/.test(content) // 忽略jquery檔案解析,直接編譯打包
        }
      }
    }
    複製程式碼
  • Rules:建立模組時,匹配請求的規則陣列

    • Rule條件:resource(請求檔案的絕對路徑)、issuer(被請求資源的模組檔案的絕對路徑,匯入時的位置),比如一個檔案A匯入檔案B,resource是/B,issuer是/A是匯入檔案時的位置,而不是真正的位置,在規則中,test/include/exclude/resource對resource匹配,而issuer只對issuer匹配

    • Test/include/exclude/resource/issuer的用法和區別

      module.exports = {
          modules: {
              rules: [
                {
                  test: /\.js?$/,
                  include: [
                    path.resolve(__dirname, "app")
                  ],
                  exclude: [
                    path.resolve(__dirname, "app/demo")
                  ],
                  resource:{
                    test: /\.js?$/,
                    include: path.resolve(__dirname, "app"),
                    exclude: path.resolve(__dirname, "app/demo")
                  },
                  issuer: {
                    test: /\.js?$/,
                    include: path.resolve(__dirname, "app"),
                    exclude: path.resolve(__dirname, "app/demo")
                  }
                }
              ]
        }
      }
      複製程式碼

      test:一般是提供一個正規表示式或正規表示式的陣列,絕對路徑符合這個正則的則意味著滿足這個條件

      include:是一個字串或者字串陣列,指定目錄中的檔案需要走這個規則

      exclude:同樣是一個字串或者字串陣列,指定目錄中的檔案不需要走這個規則

      resource:就是對text/include/exclude的一個物件包裝,和他們單獨寫沒有區別

      issuer:和resource有異曲同工的作用,不過區別在於它是將這個rule應用於哪個檔案以及這個檔案所匯入的所有依賴檔案

    • resourceQuery:和resource用法一樣,不過針對的是匹配結果'?'後面的路徑引數,可以呼叫resource中的text等

    • oneOf:表示對該資源只應用第一個匹配的規則,一般結合resourceQuery

      {
        test: /\.(png|jpe?g|gif|svg)$/,
        oneOf: [
          {
            resourceQuery: /inline/, 
            loader: 'url-loader'
          },
          {
            loader: 'file-loader'
          }
        ]
      }
      複製程式碼
      • path/to/foo.png?inline: 會匹配url-loader
      • path/to/foo.png?other:會匹配file-loader
      • path/to/foo.png: 會匹配file-loader
    • useEntry:object包含著每一個loader並且對應loader的配置檔案

      {
        loader: "css-loader",
        options: {
          modules: true
        }
      }
      複製程式碼

      options會傳入loader,可以理解為loader的選項

    • use:是對useEntry的集合,並且對每一個入口指定使用一個loader

      use: [
        'style-loader',
        {
          loader: 'css-loader',
          options: {
            importLoaders: 1
          }
        },
        {
          loader: 'less-loader',
          options: {
            noIeCompat: true
          }
        }
      ]
      複製程式碼
4.Resolve解析

主要用來模組如何被解析,給webpack提供預設值

  • alias:object主要用來讓import和require呼叫更方便,設定初始路徑

    module.exports = {
     alias: {
      Utilities: path.resolve(__dirname, 'src/utilities/'),
      Templates: path.resolve(__dirname, 'src/templates/')
     }   
    }
    // 最開始的import
    import Utility from '../../utilities/utility';
    // 配置完以後
    import Utility from 'Utilities/utility';
    複製程式碼
  • enforceExtension:Boolean 預設false,表示引用不需要副檔名,為true時,import、require中引用必須加副檔名

  • extensions:Array 自動解析不需要副檔名

    extensions: [".js", ".json"]  // .js、.json引入不需要副檔名
    複製程式碼
  • modules:Array webpack解析模組的時候需要搜尋的目錄,一般用於優先搜尋和非node_modules檔案中的自定義模組

    modules: [path.resolve(__dirname, "src"), "node_modules"] //優先搜尋src目錄
    複製程式碼
5.Loader

通過使用不同的Loader,Webpack可以要把不同的檔案都轉成JS檔案,比如CSS、ES6/7、JSX等,一般用於module的use中

module: {
  rules:[
      {
        test:/\.css$/,
        use:['style-loader','css-loader'],
        include:path.join(__dirname,'./src'),
        exclude:/node_modules/
      }
  ]      
}
複製程式碼

具體相關loader需要檢視你要引入的loader官方文件API,手寫Loader會在下一篇文章具體介紹

6.Plugin外掛

Array 擴充套件webpack,在webpack構建流程中的特定時機注入擴充套件邏輯來改變構建結果和想要做的事情,具體使用檢視你引入的plugin官方文件,手寫plugin會在後續文章中推出

7.webpack-dev-server

開發中的server,webpack-dev-server可以快速搭建起本地服務,具體使用檢視 webpack-dev-server

8.Devtool

此選項控制是否生成,以及如何生成,官方推薦 SourceMapDevToolPluginsource-map-loader 建議看官方文件 Devtool 主要用來控制打包品質和在dev環境的除錯便捷度和編譯的快慢

9.Watch

webpack 可以監聽檔案變化,當它們修改後會重新編譯和 HotModuleReplacementPlugin 有相似之處,監聽檔案變動熱啟動

4.配置webpack

webpack安裝命令

npm install webpack webpack-cli -D
複製程式碼

Webpack.config.js

具體用到的plugin外掛

  • clean-webpack-plugin:用於打包前清空輸出目錄 官方API
  • html-webpack-plugin:用於自動產出HTML和引用產出的資源 官方API
  • copy-webpack-plugin:用於拷貝靜態資源,包括未被引用的資源 官方API
  • uglifyjs-webpack-plugin:用於壓縮JS可以讓輸出的JS檔案體積更小、載入更快、流量更省,還有混淆程式碼的加密功能 官方API
  • extract-text-webpack-plugin:因為CSS的下載和JS可以並行,當一個HTML檔案很大的時候,我們可以把CSS單獨提取出來載入 官方API
const path = require('path');
const webpack = require('webpack');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const UglifyjsWebpackPlugin = require('uglifyjs-webpack-plugin')
// npm i extract-text-webpack-plugin@next // @next可以安裝下一個非正式版本
const ExtractTextWebpackPlugin = require('extract-text-webpack-plugin');
let cssExtract = new ExtractTextWebpackPlugin({
    filename: 'css/css.css',
    allChunks: true
});
let lessExtract = new ExtractTextWebpackPlugin('css/less.css');
let sassExtract = new ExtractTextWebpackPlugin('css/sass.css');
/**
 * 有些時候我們希望把頁面中的CSS檔案單獨拉出來儲存載入
 * extract-text-webpack-plugin
 */
//let pages = ['index', 'base'];
// pages = pages.map(page => new HtmlWebpackPlugin({
//     template: './src/index.html',//指定產的HTML模板
//     filename: `${page}.html`,//產出的HTML檔名
//     title: `${page}`,
//     chunks: ['common', `${page}`],//在產出的HTML檔案裡引入哪些程式碼塊
//     hash: true,// 會在引入的js里加入查詢字串避免快取,
//     minify: {
//         removeAttributeQuotes: true
//     }
// }));
module.exports = {
    //先找到每個入口(Entry),然後從各個入口分別出發,找到依賴的模組(Module),
    //然後生成一個Chunk(程式碼塊),最後會把Chunk寫到檔案系統中(Assets)   
    entry: './src/main.js',
    output: {
        path: path.join(__dirname, 'dist'),//輸出的資料夾,只能是絕對路徑 
        //name是entry名字main,hash根據打包後的檔案內容計算出來的一個hash值
        filename: '[name].[hash].js' //打包後的檔名
    },
    resolve: {
        //引入模組的時候,可以不用副檔名 
        extensions: [".js", ".less", ".json"],
        alias: {//別名
            "bootstrap": "bootstrap/dist/css/bootstrap.css"
        }
    },
    //表示監控原始檔的變化,當原始檔發生改變後,則重新打包
    watch: false,
    watchOptions: {
        ignored: /node_modules/,
        poll: 1000,//每秒鐘詢問的次數
        aggregateTimeout: 500//
    },
    //devtool: 'source-map',//單獨檔案,可以定位到哪一列出錯了
    // devtool: 'cheap-module-source-map',//單獨檔案,體積更小,但只能定位到哪一行出錯
    // devtool: 'eval-source-map',//不會生成單獨檔案,
    // devtool: 'cheap-module-eval-source-map',//不會生成單獨檔案 只定位到行,體積更小
    /*
    loader有三種寫法
    use
    loader
    use+loader
    * */
    module: {
        rules: [
            {
                test: require.resolve('jquery'),
                use: {
                    loader: 'expose-loader',
                    options: '$'
                }
            },
            {
                test: /\.js/,
                use: {
                    loader: 'babel-loader',
                    query: {
                        presets: ["env", "stage-0", "react"]
                    }
                }
            },
            {
                //file-loader是解析圖片地址,把圖片從源位置拷貝到目標位置並且修改原引用地址
                //可以處理任意的二進位制,bootstrap 裡字型
                //url-loader可以在檔案比較小的時候,直接變成base64字串內嵌到頁面中
                test: /\.(png|jpg|gif|svg|bmp|eot|woff|woff2|ttf)/,
                loader: {
                    loader: 'url-loader',
                    options: {
                        limit: 5 * 1024,
                        //指定拷貝檔案的輸出目錄 
                        outputPath: 'images/'
                    }
                }
            },
            {
                test: /\.css$/,//轉換檔案的匹配正則
                //css-loader用來解析處理CSS檔案中的url路徑,要把CSS檔案變成一個模組
                //style-loader 可以把CSS檔案變成style標籤插入head中
                //多個loader是有順序要求的,從右往左寫,因為轉換的時候是從右往左轉換
                //此外掛先用css-loader處理一下css檔案
                //如果壓縮
                loader: cssExtract.extract({
                    use: ["css-loader?minimize"]
                })
                //loader: ["style-loader", "css-loader", "postcss-loader"]
            },
            {
                test: /\.less$/,
                loader: lessExtract.extract({
                    use: ["css-loader?minimize", "less-loader"]
                })
                //use: ["style-loader", "css-loader", "less-loader"]
            },
            {
                test: /\.scss$/,
                loader: sassExtract.extract({
                    use: ["css-loader?minimize", "sass-loader"]
                })
                // use: ["style-loader", "css-loader", "sass-loader"]
            },
            {
                test: /\.(html|htm)/,
                loader: 'html-withimg-loader'
            }
        ]
    },
    plugins: [
        //用來自動向模組內部注入變數
        // new webpack.ProvidePlugin({
        //     $: 'jquery'
        // }),
        new UglifyjsWebpackPlugin(),
        new CleanWebpackPlugin([path.join(__dirname, 'dist')]),
        //此外掛可以自動產出html檔案
        new HtmlWebpackPlugin({
            template: './src/index.html',//指定產的HTML模板
            filename: `index.html`,//產出的HTML檔名
            title: 'index',
            hash: true,// 會在引入的js里加入查詢字串避免快取,
            minify: {
                removeAttributeQuotes: true
            }
        }),
        new CopyWebpackPlugin([{
            from: path.join(__dirname, 'public'),
            to: path.join(__dirname, 'dist', 'public')
        }]),
        cssExtract,
        lessExtract,
        sassExtract
    ],
    //配置此靜態檔案伺服器,可以用來預覽打包後專案
    devServer: {
        contentBase: './dist',
        host: 'localhost',
        port: 8000,
        compress: true,//伺服器返回給瀏覽器的時候是否啟動gzip壓縮
    }
}
複製程式碼

5.總結

本篇偏向基礎,能夠搭建起簡單的webpack配置,高階進階會在後續文章推出,並且希望大家多去看官方API然後自我總結輸出,只有將知識輸出出來,才能更好的記憶和學習

6.部落格

魏燃技術部落格

有任何問題可留言或者傳送本人郵箱ngaiwe@126.com

相關文章