1. 程式人生 > 實用技巧 >【30分鐘】吃透webpack,也許這一篇就夠了

【30分鐘】吃透webpack,也許這一篇就夠了

使用webpack前的準備

1、初始化一個前端專案

為了方便之後自己更好的使用這個webpack_starter,引入git的支援,一是可以把一些通用的東西放在主分支,二是可以把後面不同的配置支援可以通過branch或者tag的方式分門別類。

  • 在github初始化一個webpack_starter專案,如下圖所示,初始化.gitignore支援Node語言

#clone專案到本地
git clone https://github.com/mpandar/webpack_starter.git
cd webpack_starter
  • 利用yarn初始化專案(當然同樣可以使用npm,無太大差異,此處不再補充npm使用方法)
初始化過程按照提示完成即可,唯一注意的是entry point,這是webpack進行打包時的入口檔案,預設是根目錄下的index.js,不過通常情況下,我們的原始碼都是在src目錄下,所以修改為src/index.js
> yarn init
yarn init v1.3.2
question name (webpack_starter): 
question version (1.0.0): 0.1.0
question description: a webpack start project
question entry point (index.js): src/index.js
question repository url (https://github.com/mpandar/webpack_starter.git): 
question author (mpandar <mshp_****@126.com>): 
question license (MIT): 
question private: 
success Saved package.json
✨  Done in 53.55s.

2、安裝webpack

單獨安裝與全域性安裝對於webpack的使用並無太大差異,但推薦即使全域性安裝以後,仍要在專案中進行單獨的安裝,方便專案移植。否則可能會導致全域性安裝的webpack版本與專案中的配置檔案可能存在不匹配。當然單獨安裝後,使用一些npm或yarn命令,它們會優先使用本地安裝的webpack
#全域性安裝
yarn global add webpack
#單專案使用
yarn add webpack

3、初始化專案目錄

 |---
    |--dist    //存放webpack打包後相關檔案
    |--src     //存放專案原始碼
       |--index.js 
    |--config  //專案相關的配置檔案
       |--webpack.config.js   //webpack預設讀取專案根目錄下的webpack.config.js檔案作為配置資訊,為了規範化移入到config目錄下
    |--package.json

瞭解webpack配置檔案webpack.config.js

當然,即使沒有配置檔案,直接執行webpack命令,同樣可以直接對js檔案完成打包工作,這裡是一篇分析webpack打包後的程式碼的文章:簡要分析webpack打包後代碼,其中用到的一個新命令npx,很簡單,介紹點這裡

#直接打包
npx webpack src/index.js dist/bundle.js

為了應對更靈活的使用場景,webpack支援配置檔案,並且預設情況下,在專案根目錄下,如果存在webpack.config.js檔案,那麼webpack會主動讀取該檔案作為配置內容,不過Demo下,為了更加符合我們的目錄規範,我們將config檔案移到了config目錄下

//當配置檔案內容為空時,執行該命令會提示`Configuration file found but no entry configured`
npx webpack src/index.js --config config/webpack.config.js

接下來,讓我們看一下一個webpack配置檔案,最簡單隻需要包含entry(定義入口檔案)和output(定義打包輸出檔案)這兩個部分:

const path = require('path');
const base = path.join(__dirname, '..')

module.exports = {
  entry: path.resolve(base, 'src', 'index.js'),
  output: {
    filename: 'bundle.js',
    path: path.resolve(base, 'dist')
  }
};
注:使用path模組只是為了程式碼清晰,你完全可以不用,直接用__dirname+'/../src'類似程式碼拼接
//這樣就不需要在命令列定義輸入輸出檔案啦
npx webpack --config config/webpack.config.js

除了entryoutput,webpack中最常見的就是moduleresolveplugins,大致結構如下:

module.exports = {
  entry: path.resolve(base, 'src', 'index.js'),
  output: {
    filename: 'bundle.js',
    path: path.resolve(base, 'dist')
  },
  devtool: 'eval-source-map',
  devServer: {
    contentBase: path.resolve(base, 'dist'),
    historyApiFallback: true,
    inline: true,
    proxy: {
      "/api": "http://localhost:8000"
    }
  },
  module: {
    rules: [
    ]
  },
  resolve: {
  },
  plugins: [
  ]
};

當然瞭解webpack最好的地方永遠是官方文件,傳送門,接下來,自然是在目前配置的基礎上,增添更多令人興奮的特性

為開發增加更多利器

生成Source Map,為除錯助力

開發離不開除錯,但經過編碼後的程式碼並不利於除錯,很找到出錯的地方對應的你寫的程式碼,而Source Maps就是來幫我們解決這個問題的。
而webpack支援Source Maps僅僅是增加一行devtool配置選項,具體配置選項可以看這裡的官方文件,其中兩個選項eval-source-mapsource-map是比較常用的選項,前一個選項推薦僅僅用在開發環境,而後一個通常在一些第三方庫中,提供給開發者除錯使用。當然對於任何上線專案,實際上都推薦使用*.min.js並不使用Source Map以加快網路載入。

module.exports = {
  entry: path.resolve(base, 'src', 'index.js'),
  output: {
    filename: 'bundle.js',
    path: path.resolve(base, 'dist')
  },
  devtool: 'eval-source-map'
}

自動監控程式碼更新,自動編譯,自動瀏覽器重新整理

作為開發者,總不希望把時間浪費在執行命令和重新整理頁面上,webpack提供一個單獨的元件webpack-dev-server為我們提供一個基於nodejs的本地伺服器、檔案修改監控及編譯以及瀏覽器自動重新整理等特性。

首先是安裝:

yarn add webpack-dev-server--dev

詳細配置引數可查閱官方文件,常使用引數如下:

  • contentBase 指定專案根目錄
  • historyApiFallback 主要應用在單頁應用的開發場景,它依賴於HTML5 history API,如果設定為true,所有的跳轉將指向index.html
  • inline 主要是解決了自動瀏覽器自動重新整理問題
  • lazy 預設為false,若開啟後,則webpack-dev-server不再監測檔案變化自動編譯,只有等到我們手動重新整理瀏覽器的時候才會編譯檔案
  • proxy 可以有效的解決開發階段的跨域問題,畢竟不是所有的前端專案,都有獨立的nodejs作為中介軟體去請求服務,更多的還是把前端專案編譯後與後端服務部署在一起。而開發階段又相對獨立,這時候就可以利用proxy將前端請求轉發到後端測試伺服器,不影響開發
module.exports = {
  entry: path.resolve(base, 'src', 'index.js'),
  output: {
    filename: 'bundle.js',
    path: path.resolve(base, 'dist')
  },
  devtool: 'eval-source-map',
  devServer: {
    contentBase: path.resolve(base, 'dist'),
    historyApiFallback: true,
    inline: true,
    proxy: {
      "/api": "http://localhost:8000"
    }
  }
}
小插曲,本來測試proxy的時候,自己利用php -S localhost:8000快速起了一個監聽程序,但是訪問前端的時候卻提示轉發去請求被拒絕,後來發現webpack-dev-server去轉發請求的時候是把localhost轉化為了地址,即127.0.0.1:8000,所以在php啟動監聽程序時候,需要使用php -S 127.0.0.1:8000或者php -S 0.0.0.0:8000監聽所有網絡卡地址

webpack-dev-server的使用同webpack基本執行一樣,只是webpack-dev-server是一個不會退出的程序,並自動監控檔案變化等(Ctrl+C退出)

npx webpack-dev-server--config config/webpack.config.js

更方便的打包命令

使用npx webpack --config config/webpack.config.js進行打包實際上已經很方便了,但是當我們需要又有開發環境的配置,又有生產環境的配置,甚至還要為命令增加其他的環境變數的時候,這個命令簡直是又臭又長,我們總不能每次都輸入這個繁瑣的命令,其實我們可以利用npm scripts,這裡是一篇阮一峰大神對npm指令碼的介紹

//package.json
{  
  "scripts": {
    "build": "webpack --config config/webpack.config.js",
    "dev": "webpack-dev-server --config config/webpack.config.js"
  }
}
npm run build
npm run dev
需要注意的是在配置build&dev指令碼的時候,我們並沒有用npx命令,實際上,在npm指令碼中的命令,npm預設都是優先查詢本專案下node_modules下是否存在對應的模組及命令,如果沒找到,才會查詢全域性的命令

多檔案入口

通常一個專案中,並不是只有一個js檔案,而entry預設支援多檔案入口,修改output跟隨入口檔名字命名即可,簡單做如下修改:

module.exports = {
  entry: {
    index: path.resolve(base, 'src', 'index.js'),
    main: path.resolve(base, 'src', 'main.js')
  },
  output: {
    filename: 'js/[name].js',
    path: path.resolve(base, 'dist')
  }
}

同時建立一個簡單的main.js進行測試,再次編譯會發現在dist/js目錄下存在編譯後的index.js和main.js檔案

一切皆為Module

webpack令人興奮的一個特性就是模組化,易於擴充套件其功能。webpack支援大量的模組匯入方法,比如ES6(import)、CommonJS(require)、AMD等規範,並且加入了一些自由方法,具體的可以看官方說明文件。

用通俗的語言描述就是,webpack通過某個入口,去匹配各種模組匯入規範,發現一個模組就根據對應配置尋找對應的loader去處理,如此往復,直到處理完畢所有依賴。

使用起來就是在modules欄位中的rules(老版本名字為loaders,為保證相容性,還是支援這個欄位的)中配置test,去匹配檔案(js、css、圖片資源等等),然後把這個檔案交給合適的loader處理即可,所以但凡新出的框架,如果用到了獨特的語法功能,都會配套提供對應的loader工具

使用ES6等新特性

目前雖然瀏覽器對ES6新特性的支援度都非常高,但仍是有部分場景下,我們只能執行ES5的程式碼,這時候就需要利用到js轉碼屆的特斯拉Bebel及其外掛了

yarn add babel-loader babel-core babel-preset-env
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['env']
          }
        }
      }
    ]
  },

顯然test中匹配了所有的js檔案,exclude欄位去除了專案中依賴庫裡的檔案,use則是配置對應的loader。其中對於options,是作為引數傳遞給babel-loader的,babel的相關引數可以參考babel的官方網站,其中presets作為最主要的引數,告訴babel按照那種規則去解析程式碼,當然env是一個組合,包含es2015、es2016等,當presets引數包含多個值時,babel的處理規則是倒序的,"es2017","es2016",babel會先去匹配es2016的規則。另外,對於babel的配置,也可以通過在根目錄建立.babelrc方式去配置:

//.babelrc
{
  "presets": [
    "env"
  ]
}

當然除了擴充套件js的語法,有時候我們還需要擴充套件js的功能,比如在某些低版本的瀏覽器上執行'Hello World'.includes('Hello'),這可以使用babel-polyfill這個元件,點選這裡可以瞭解其使用方法。

CSS

當然,我們可以只讓webpack處理js檔案,繼續在html檔案中通過link標籤引入css檔案。但顯然webpack希望前端攻城獅們也能像模組化編寫js一樣,進行css的編寫。我們在src下建立css目錄,並建立main.css檔案

/* src/css/main.css */
body{
  background-color: deepskyblue; /* 我喜歡填空的藍色 */
}
//src/index.js
import style from './css/main.css'

這時候使用npm run build會發現編譯報錯,這是因為webpack無法處理載入main.css後的程式碼

ERROR in ./src/css/main.css
Module parse failed: Unexpected token (1:4)
You may need an appropriate loader to handle this file type.
| body{
|   background-color: red;
| }
 @ ./src/index.js 3:12-37

webpack官方提供css-loader專門處理匯入的css模組,當然css-loader也僅僅是完成了模組的匯入處理,使webpack在編譯時候不再報錯,實際上,還需要style-loader處理匯入後的css資料,自動新增到html的style標籤中。如果你在測試過程中,僅僅新增css-loaderloader,你會發現body實際上並沒有變成背景藍

yarn add style-loader css-loader
//config webpack.config.js module->rules
      {
        test: /\.css$/,
        use: [
          {
            loader: "style-loader"
          }, {
            loader: "css-loader",
            options: {
              modules: true,
              localIdentName: '[path][name]__[local]--[hash:base64:5]'
              }
          }
        ]
      }

當然css-loader還有一個重要配置選項,modules引數,它支援匯入的css中的類名自動重新命名,這樣即使在不同元件使用了相同的css類命名,經處理後互相之間也不會出現影響。看下效果:

CSS預編譯工具

比較常見的預編譯工具也就是Sass、Less、Stylus。在使用這三個預編譯工具前,需要安裝其對應的專屬處理程式,比如Sass的處理工具node-sass。為了配合與webpack使用,還要安裝對應的loader,比如sass-loader

yarn add sass-loader node-sass--dev

在webpack.config.js中新增對scss(Sass 3引入的新的語法格式,推薦新專案都用此)檔案的支援,再起強調,webpack中loader執行順序是從右往左,從下往上。

// webpack.config.js
module.exports = {
    ...
    module: {
        rules: [{
            test: /\.scss$/,
            use: [{
                loader: "style-loader"
            }, {
                loader: "css-loader",
                options: {
                  modules: true,
                  localIdentName: '[path][name]__[local]--[hash:base64:5]'
              }
            }, {
                loader: "sass-loader"
                }
            }]
        }]
    }
};

測試一下:

/* src/sass/main.scss  */
body {
  background-color: blue;
}
.main {
  background-color: grey
}
//src/index.js
// import style from './css/main.css'
import style from './sass/main.scss'
let es6 = () => {
  console.log('run in es6')
  console.log(style)
}
es6();

其他兩種預編譯器可以參考各自官方文件:less-loaderstylus-loader

像處理JS一樣處理CSS -- PostCSS

PostCSS官網介紹是,利用js轉譯css的一個工具。對PostCSS的介紹,我比較認可這篇文章,PostCSS提供了一個解析器,把css轉換為抽象語法樹(AST),當然這個AST是能夠被js處理的,然後交給各種外掛處理後,再將AST轉為css程式碼,所以關鍵是這些外掛能完成哪些功能。

Autoprefixer

Autoprefixer 是一個流行的 PostCSS 外掛,其作用是為 CSS 中的屬性新增瀏覽器特定的字首。

cssnext

cssnext 外掛允許開發人員在當前的專案中使用 CSS 將來版本中可能會加入的新特性。需要注意這個外掛本身包含了Autoprefixer功能,所以如果使用了這個外掛,則不需要Autoprefixer外掛。
當然PostCSS中其實也包含支援Sass、Less等的外掛,這個看個人喜好,不過我本人倒是蠻期待嘗試下cssnext,畢竟大部分特性可能就是未來css支援的特性,提前熟悉下也算是預習。
更多外掛可以讀這篇文章,不再敘述

安裝postcss-loader及其外掛autoprefixer

yarn add postcss-loader autoprefixer --dev

webpack.config.js中新增postcss支援,注意postcss處理css檔案的位置

// webpack.config.js
module.exports = {
    ...
    module: {
        rules: [{
            test: /\.scss$/,
            use: [{
              loader: "style-loader"
            }, {
              loader: "css-loader",
              options: {
                modules: true,
                localIdentName: '[path][name]__[local]--[hash:base64:5]',
                minimize: false
              }
            }, {
              loader: 'postcss-loader',
              options: {
                config: {
                  path: path.resolve(base, 'config', 'postcss.config.js')
                }
              }
            }, {
              loader: "sass-loader"
            }]
        }]
    }
};

如上面配置,postcss需要單獨的配置檔案,建立config/postcss.config.js,新增如下配置:

//config/postcss.config.js
module.exports = {
  plugins: {
    'autoprefixer': {}
  }
}

圖片資源

file-loader提供了對圖片資源的loader功能,並且利用publicPath選項還能很好的支援cdn,配合url-loader還能對小圖片直接進行數字序列化(DataURL),減少網路請求,提高載入速度。

yarn add url-loader file-loader--dev

增加相關配置;url-loader的預設fallbackloader就是file-loader為了更直觀就寫了出來,傳遞給file-loader的引數也只需要寫在options中即可。這樣
background-image: url('../image/logo.jpg')當我們在css檔案中使用這種方式引入圖片時,就會觸發url-loader去處理

//config/webpack.config.js
module.exports = {
    ...
    module: {
      rules: [    
      {
        test: /\.(png|jpg|gif)$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 8192,
              fallback: 'file-loader',
              name: '[hash:5].[ext]',
              // publicPath: 'https://cdn.j2do.com/',
              outputPath: 'images/'
            }
          }
        ]
      }
    ]
  }
}

雖不是萬能,但卻異常強大的外掛(Plugins)系統

Loader是專注於處理Webpack匯入的資源模組,而外掛是對Webpack功能的擴充套件。除了Webpack內建的外掛,開發社群提供了大量優秀的外掛。當然外掛也是解決問題的,我們還是以問題為導向,去介紹幾款外掛。

利用extract-text-webpack-plugin分離css到獨立檔案

yarn add extract-text-webpack-plugin --dev

其中ExtractTextPlugin中fallback是指定了如果不需要提取到獨立css檔案中的樣式檔案,則交給style-loade處理,其他loader配置,跟之前沒有差異。同樣如果是預編譯檔案(Sass、Less等)的話,也只需要增加對應的loader即可

module.exports = {
  ...
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ExtractTextPlugin.extract({
          fallback: "style-loader",
          use: [
            {
              loader: "css-loader",
              options: {
                modules: true,
                localIdentName: '[path][name]__[local]--[hash:base64:5]'
              }
            }, {
              loader: 'postcss-loader',
              options: {
                config: {
                  path: path.resolve(base, 'config', 'postcss.config.js')
                }
              }
            }
          ]
        })
      }
    ]
  }
  ...
  plugins: [
    new ExtractTextPlugin("css/[name].css")
  ]
};

再次執行yarn run build會發現生成了獨立的css檔案

利用html-webpack-plugin自動生成index.html等入口檔案

隨著多入口以及css的分離,手動去寫html的入口檔案也是一件麻煩事,這時候html-webpack-plugin就排上用途了,html-webpack-plugin能夠根據某個模板檔案自動生成入口的html,包括多個入口,安裝及配置如下:

yarn add html-webpack-plugin --dev
module.exports = {
  ...
  plugins: [
    new ExtractTextPlugin("[name].css"),
    new HtmlWebpackPlugin({
      title: 'Index Page',
      template: path.resolve(base, 'src', 'template/index.html.tmpl'),
      filename: "index.html",
      chunks: ['index']
    }),
    new HtmlWebpackPlugin({
      title: 'Main Page',
      template: path.resolve(base, 'src', 'template/index.html.tmpl'),
      filename: "main.html",
      chunks: ['main']
    }),
  ]
};

建立模板檔案,注意之所以命名為.tmpl是為了防止 .html 可能會被loader解析,<%= ... %>將不會被外掛識別,完成變數替換。如下圖,我們的title是以變數形式在webpack.config.js中配置。該外掛還支援多種模板檔案,具體可見官方文件

<!-- src/template/index.html.tmpl -->
<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <title>
    <%= htmlWebpackPlugin.options.title %>
  </title>
</head>

<body>
</body>

</html>

程式碼優化壓縮

前面已經將js、css等分離到單獨檔案,接下來就是優化壓縮這些程式碼。

對於css而言,只需要配置css-loaderminimize引數為true即可;當然還可以利用postcss的cssnano外掛進行程式碼的壓縮和優化,據說cssnano是目前css壓縮優化中效果最好的工具。

對於js的壓縮,我們藉助於uglifyjs-webpack-plugin外掛

yarn add uglifyjs-webpack-plugin--dev
module.exports = {
  ...
  plugins: [
    new ExtractTextPlugin("css/[name].css"),
    new HtmlWebpackPlugin({
      title: 'Index Page',
      template: path.resolve(base, 'src', 'template/index.html.tmpl'),
      filename: "index.html",
      chunks: ['index']
    }),
    new UglifyJsPlugin()
  ]
};

藉助Eslint自動程式碼規範檢測及格式化

Eslint不再介紹,在webpack下使用Eslint需要如下依賴包:

yarn add eslint eslint-loader babel-eslint eslint-config-standard --dev

其中eslint是必須的,eslint-loader是連線eslint與webpack的loader,babel-eslint是一個eslint解析器,使其能支援es6等語法檢測,eslint-config-standard是Airbnb的規範配置,目前最流行的js規範。

安裝過程中如果有提示[email protected]" has unmet peer dependency "eslint-plugin-import@>=2.2.0"等等,直接安裝對應的包即可,比如:yarn add eslint-plugin-import --dev即可
eslint是規範js語法,所以他需要處理的是js檔案,而且應該是先於所有loader去處理js檔案,如果出錯或者不規範則糾正之,這裡可以利用webpack的enforce屬性,設定eslint檢查,先於其他loader
module.exports = {
  ...
  module: {
    rules: [
      {
        enforce: "pre",
        test: /\.js$/,
        exclude: /node_modules/,//注意不要檢測node_modules裡面的程式碼
        loader: "eslint-loader",
        options: {
          fix: true //自動修復不規範的程式碼,並不是所有程式碼都能自動修復的,一些縮排,引號等能直接處理
        }
      }
      ...
    ]
  }
}

同時還要為eslint增加對應的配置檔案.eslintrc

{
  "parser": "babel-eslint",
  "extends": "standard",
  "rules": {}
}

這時候再嘗試build吧,會發現一些不規範的程式碼被自動修復,當然有些不規範的程式碼,無法自動修復的,會直接導致錯誤;比如,聲明瞭一個函式,卻沒有使用!