【轉】Webpack 快速上手(中)

因爲文章篇幅較長,爲了更好的閱讀體驗,本文分爲上、中、下三篇:javascript

  • 上篇介紹了什麼是 webpack,爲何須要 webpack,webpack 的文件輸入和輸出css

  • 中篇介紹了 webpack 在輸入和輸出這段中間所作的事情,也就是 loader 和 pluginshtml

  • 下篇介紹了 webpack 的優化,以及在開發環境和生產環境的不一樣用法前端

在上一篇中,介紹了經過設置 entry(入口文件)和output(出口文件),來對源代碼進行處理,可是在處理過程當中,webpack 是如何針對不一樣的文件進行打包的呢?這就是 loader 和 plugins 的要作的事情了。vue

loader

我的認爲 loader 是 webpack 中最厲害的一個功能了,它讓咱們能夠在項目隨意 import 各類類型的文件,css scss html img 等等都不在話下,若是有相關的 loader 支持,甚至能夠 import 其它語言的代碼。java

簡單的說 loader 就是一個處理器,在 webpack 中配置好相應的 loader 以後,就能夠在代碼中像加載 JavaScript 模塊同樣使用 import 把其它類型的代碼當作 JavaScript 模塊加載。node

loader 的用法有三種

  1. webpack.config.js 中配置,這種方式是最經常使用的,下面會着重介紹。
  2. 在代碼中顯示的指定 loader ,下面的代碼表示從 styles.css 加載樣式文件,用 style-loadercss-loader 來處理 css 文件。
import styles from 'style-loader!css-loader?modules!./styles.css';
  1. 在命令行中爲某些類型文件執行 loader 。下面的命令表示在打包過程當中,對 .css 文件使用 style-loadercss-loader 來處理。
webpack --module-bind 'css=style-loader!css-loader'

loader 的配置

loader 的配置其實比較簡單,只是提供了太多簡寫,讓新手有點摸不着頭腦,首先用 JavaScript、TypeScript、css、scss 來展現經常使用的幾種配置方式:react

// webpack.config.js
module.exports = {
  entry,
  output,
  module: {
    rules: [{
      test: /\.js$/,
      loader: 'babel-loader',
      options: { presets: ['env'] },
      include: __dirname + '/src'
    },
    { test: /\.tsx?$/, use: 'ts-loader' },
    {
      test: /\.css$/,
      use: ['style-loader', 'css-loader']
    },
    {
      test: /\.scss$/,
      use: [
        { loader: 'style-loader' },
        {
          loader: 'css-loader',
          options: { modules: true }
        },
        { loader: 'postcss-loader' },
        { loader: 'sass-loader' }
      ]
    }]
  }
}

從上面的代碼中咱們能夠發現,use 這個選項的配置是最沒節操了,它能夠是字符串、數組、甚至被 loader 這個選項代替,其實這都是簡寫。rules.loaderloader.optionsrules.use: [ {loader, options} ] 的簡寫。這些配置項的含義分別是:webpack

  • test: 正則表達式,用來匹配文件的擴展名。
  • use: 對匹配出的文件使用的 loader 配置,如上面所說,該選項配置靈活,能夠簡寫
  • loader: loader 名稱
  • options: loader 的額外配置選項
  • include / exclude: 包括 或 排除 的文件夾,兩個選項只能同時出現一個,上面的例子中 include: __dirname + '/src' 表示 babel-loader 只編譯 /src 文件下的文件,其它的不作處理;相反的,exclude: __dirname + '/src' 表示不編譯 /src 下的文件。

下面就來詳細的介紹一下經常使用的 loader 和其配置。git

(題外話,原本是先將 babel-loader 放到第一個介紹的,可是因爲篇幅較長,且有些難懂,因此將其放到了後面)

樣式處理

npm i style-loader css-loader less less-loader node-sass sass-loader postcss-loader autoprefixer -D

對於樣式文件的處理,咱們(我)一般會用到如下這些 loader :

  • style-loader
  • css-loader
  • postcss-loader
  • less-loader
  • sass-loader

那麼這些 loader 的使用場景和區別是什麼呢?

  1. 首先介紹 less-loader 和 sass-loader 。less 和 sass 都是 css 預處理器,可讓 css 編寫起來更爽,可是不能直接在瀏覽器中運行,因此須要先將 .less.scss 文件先轉換成 css 。這就是 less-loader 和 sass-loader 的做用。

  2. 不管是直接編寫的 css ,仍是由 less 或 sass 轉換而來的 css 都不是 JavaScript 模塊,這時候就要用到 css-loader ,它的做用就是把 css 轉成 JavaScript 模塊插入到代碼中。

  3. 樣式文件已經轉換好了,但並不會產生任何效果。由於這些樣式尚未添加到頁面中,這時候就該輪到 style-loader 出場了,它的做用就是把轉換後的樣式添加到頁面中,就像下面這樣。

style-loader

  1. 最後還有 postcss-loader 它的做用也很強大,最經常使用的功能就是幫助咱們自動爲一些樣式屬性名添加私有前戳(-moz、-ms、-webkit)。寫過 vue 的同窗都知道,當咱們給 style 標籤添加 scope 屬性的時候,打包後的類名會自動添加自定義屬性(例如 .panel[_v-72f4cef2]),這個功能就是基於 postcss-loader 實現的。

postcss 須要一份配置文件,這份配置文件以寫在單獨的文件中 (postcss.config.js),也能夠寫在 package.jsonpostcss 屬性中:

{
  "postcss": {
    "plugins": {
      "autoprefixer":{}
    }
  }
}

對 postcss 感興趣的同窗能夠看看這篇文章: PostCSS真的太好用了! (https://segmentfault.com/a/1190000014782560)

這些 loader 的執行順序是 :

sass-loader or less-loaderpostcss-loadercss-loaderstyle-loader

經過對這些 loader 的配置,咱們就能夠把樣式文件當作 js 文件同樣引入了。

// styles.css
.red { color: red; }
// index.js
import './styles.css';

這裏須要在額外提一下 css module ,這也是一個很好的特性,寫 react 的朋友對它應該很熟悉:

// index.js
import styles from './styles.css';

export default () => (
  <h2 className={styles.red}>css module</h2>
);

從上面的代碼中能夠看出,咱們將樣式當作 對象 styles 導入 jsx 中,那麼該樣式下的全部類名就是 styles 的屬性名了。

這樣的寫法也一樣適用於 ES6 的模板字符串:

// index.js
import styles from './styles.css';

const html = `<h2 class="${styles.title}">css module</h2>`;

document.body.innerHTML = html;

只要在 css-loader 的 options 中設置 { modules: true } 既能夠開啓此功能。

上面的這些配置,能夠幫咱們將 css 封裝成 js對象 ,打包在 .js 文件中,而後運行的時候,以 <style></style> 的方式動態插入到頁面中,但咱們更但願能夠將這些樣式從 js 文件中抽取出來放到 css 文件,一來這樣顯得更優雅一些,二來能夠減小 js 爲文件體積,避免動態建立 style 標籤所帶來的性能損耗。這個功能須要在 plugins 中進行設置,下面也會講到。

file-loader、url-loader

npm i file-loader url-loader -D

若是咱們在頁面中經過相對路徑來引入圖片、音頻、視頻、字體等文件資源時,在 webpack 環境中可能出現路徑錯誤404的問題。主要緣由是 開發時的目錄結構打包後的目錄結構 通常都是不同的,所以致使路徑失效,而 file-loader 就是爲了解決這個問題的。

  • file-loader 能夠解析頁面中引入的資源的路徑,而後根據配置,將這些資源拷貝到打包後的目錄中。

  • url-loader 則是對 file-loader 進行了一次封裝,若是解析的資源是圖片,則能夠將改圖片轉成 base64 從而減小 http 請求一提高性能,同時也能夠設置 limit。 只對指定大小的圖片進行轉換。

一樣的也能夠在 js 中引入資源

// index.js
import logo from './images/logo.png';

const img = new Image();
img.addEventListener('load', () => document.body.appendChild(img));
img.src = logo;

下面是 url-loader 的簡單配置參考:

// webpack.config.js
module.exports = {
  entry,
  output,
  module: {
    rules: [{
      test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
      use: [{
        loader: 'url-loader',
        options: {
          limit: 10000, // 10KB 轉換爲base64
          name: 'images/[name].[ext]' // 拷貝到 images 目錄下
        }
      }]
    }]
  }
}

html-loader

npm i html-loader -D

在 Web 開發中,一般會用到不少 html 模板,傳統的方式是將模板存在服務端,前端經過 http 請求加載模板,或者在 JavaScript 中拼接字符串,或者在頁面中將模板內容寫在 <script type="text/template"></script> 內。

而在 webpack 環境下,咱們也能夠把 html模板 當作 JavaScript 的模塊來加載,以 Vue 爲例:

<!-- template.html -->
<h2>{{ title }}</h2>
// index.js
import tpl from './template.html'

new Vue({
  el: '#app',
  template: tpl,
  data: {
    title: 'Hello Webpack'
  }
});

在上面代碼中,咱們將 template.html 的內容以字符串方式導出,這正是 html-loader 的功能,也能夠在配置只啓用壓縮功能。

// webpack.config.js
module.exports = {
  entry,
  output,
  module: {
    rules: [{
      test: /\.html$/,
      use: [{
        loader: 'html-loader',
        options: {
          minimize: true // 開啓壓縮
        }
      }]
    }]
  }
}

babel-loader(重點)

npm i @babel/core babel-loader @babel/preset-env @babel/runtime @babel/plugin-transform-runtime -D

Babel is a compiler for writing next generation JavaScript.

從官方的簡短介紹中能夠知道, babel 屬於編譯器,輸入 JavaScript 源碼,輸出 JavaScript 源碼(source to source),其做用就是將目前部分瀏覽器目前還不支持的 ES2015+ 語法轉換爲 ES5 語法。

babel-loader 則是讓 babel 能夠在 webpack 中使用的工具,同理若是你使用的是 gulp ,則須要用到 gulp-babel 這個包。

實際上,若是隻是用 babel 的話,輸入的代碼和編譯後輸出的代碼是相同的(被 webpack 混淆打包的代碼與 babel 無關)。由於 babel 的轉換工做全都是由 babel 的插件來完成的

關於 babel 的介紹和使用,僅僅一個小節的篇幅是徹底不夠,因此這裏貼一個連接,有興趣的讀者一點要點進去看一下 一口(很長的)氣了解 babel

babel 也是須要進行配置的,通常有兩種方式:

  1. 在根目錄建立 .babelrc
  2. package.jsonbabel 屬性中進行配置

我更傾向於在 package.json 進行配置,由於根目錄放置太多文件,強迫症實在沒法接受。不管是在 .babelrc 仍是 package.json 中配置,配置的內容都是同樣的,下面以在 package.json 中配置爲例:

{
  "babel": {
    "presets": [
      [
        "@babel/preset-env",
        {
          "modules": false,
          "targets": {
            "browsers": [
              "> 1%",
              "last 2 versions",
              "not ie <= 8"
            ]
          }
        }
      ]
    ],
    "plugins": [
      "@babel/plugin-transform-runtime",
      "@babel/plugin-syntax-dynamic-import"
    ]
  }
}

在該配置中,presetsplugins 對應的值都是數組,同時數組的每一項能夠是 string (只指定名字),也能夠是 array (指定名字,並進行更具體的配置)

plugins 表示用到的插件,好比咱們在代碼中使用到了 import() 動態加載模塊這個語法,那麼就要在 plugins 添加 @babel/plugin-syntax-dynamic-import 這個插件了;咱們須要對 babel 編譯後的代碼進行去重,就須要用到 @babel/plugin-transform-runtime 。 固然,這兩個插件也是須要單獨安裝的 npm i @babel/runtime @babel/plugin-transform-runtime @babel/plugin-syntax-dynamic-import -D

presets 一組 plugins 的集合。好比咱們能夠把 @babel/plugin-transform-runtime 和 @babel/plugin-syntax-dynamic-import 打包到一塊兒,叫 preset-my ,這樣咱們只須要在 presets 中添加 preset-my 就能夠了,省去了對 plugins 的配置 。上面的配置文件只配置一個 @babel/preset-env ,這是最經常使用的配置,@babel/preset-env 後面的對象是對 @babel/preset-env 具體配置。咱們注意到,其中有一個 targets.browsers 屬性,指定了瀏覽器版本,這個屬性也能夠放在 package.jsonbrowserslist 中。

爲何配置了 presets 還須要配置 plugins 呢?很簡單,如上面所說, presets 是一組 plugins 的集合,也就說 babel 對不一樣階段的語法作了整合,方便咱們使用。可是在上面的配置中,咱們只使用了 @babel/preset-env 這個集合裏的插件,而 import() 處於 stage-3 階段(記不太清了,也多是 stage-2),不包含於 @babel/preset-env ,因此就須要在 plugins 單獨添加 @babel/plugin-syntax-dynamic-import 插件來對 import() 語法進行轉換了。

社區中也提供了一些 presets ,好比 react 的 @babel/preset-react , vue 的 @vue/babel-preset-app

babel 的執行順序是:

讀取plugins數組按正序執行plugins內插件讀取presets數組按倒序執行presets內容

簡單的介紹了 babel 後,開始配置 babel-loader :

// webpack.config.js
module.exports = {
  entry,
  output,
  module: {
    rules: [{
      test: /\.js$/,
      loader: 'babel-loader',
      // options: { presets: ['env'] }, 該項的配置和上面babel的配置徹底相同,已經在package.json配置過,這裏不須要再配置
      include: __dirname + '/src' // 只對 ./src 目錄下的代碼進行編譯
    }]
  }
}

ts-loader

npm i typescript ts-loader -D

若是你的項目是用 typescript 開發的,這時候就要樣到 ts-loader 了。

ts-loader 的配置比較簡單,可是有許多須要注意的細節,詳情能夠參照這裏:https://github.com/TypeStrong/ts-loader/blob/master/README.md#configuration

plugins

講完了 entry、output 和 loader,下面開始講講 plugins 。細心的讀者應該已經發現,尚未提到代碼的壓縮,並且按照上面的方式打包會把 .css.js 文件打包在一塊兒,而且打包後的文件體積很大,可能還會存在冗餘的代碼等等一些問題,plugins 就是爲了解決這類問題而產生的。

這裏不要把 loaderplugins 搞混了,laoder 只是把特定的文件類型轉換成 JavaScript 模塊plugins 是在打包過程當中對全部模塊進行特定的操做plugins 的值是一個數組,全部的 webpack 都須要手動經過關鍵字 new 來實例化。 下面就介紹一些常見的插件。

html-webpack-plugin

npm i html-webpack-plugin -D

webpack 是對 JavaScript 進行打包的,打包出的只能是 .js 文件。 而 JavaScript 要想在瀏覽器中運行,那就必須在 html 中經過 script 的方式引入。在沒有其餘工具幫助的狀況下,咱們只能手動建立 html 文件,而後再把打包後的 .js 文件和 .css 文件寫到這個文件中,這樣作很麻煩。這時候能夠用 html-webpack-plugin 這個插件來自動完成上面的工做。

html-webpack-plugin 提供了一些配置項,若是不行配置,它會自動幫我建立一個空的 html 文件,而後將打包後的資源插入到這個頁面內:

// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry,
  output,
  plugins: [
    new HtmlWebpackPlugin() // 建立 /dist/index.html 文件,並將 index_bundle.js 插入到這個頁面中。
  ]
}

一樣,咱們也能夠爲其指定一個模板頁:

// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry,
  output,
  plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html', // 生成的文件名稱,默認爲 index.html
      template: 'src/index.html', // 以 src/index.html 爲模板文件
      inject: 'body', // 將打包後的文件注入到 body 區域內
      title: 'Hello webpack', // 生成文件的標題
      minify: {       // 對生成的文件進行壓縮,能夠設置爲 true ,也能夠是對向,進行更具體的配置
        collapseWhitespace: true,   // 刪除空格
        minifyCSS: true,
        minifyJS: true,
        removeAttributeQuotes: true,
        removeComments: true,
        removeTagWhitespace: true,
      }
    })
  ]
}

插件也能夠經過屢次實例化來重複使用:

// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry,
  output,
  plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'src/index.html',
      chunks: ['index', 'vendor'] // 只注入 index.bundle.js 和 vendor.bundle.js 
    }),
    new HtmlWebpackPlugin({
      filename: 'about.html',
      template: 'src/about.html',
      excludeChunks: ['index'] // 將 index.bundle.js 排除,其他的都注入
    })
  ]
}

分離css和js

  • webpack v4
npm i mini-css-extract-plugin -D

前面在介紹用 loader 處理樣式的時候說到,這些樣式最終會被混入到打包後的 .js 文件中,在頁面運行的時候,在以 <style></style> 的方式動態的插入到 DOM 節點中,這種作法有兩個很明顯的缺點:

  1. js 和 css 糅雜在一塊兒,增長了單個文件的體積。
  2. 在頁面運行時動態的去建立 style 標籤,多多少少會有些性能影響

若是能把這些 css 從打包後的 js 中抽取出來,就能夠解決上面的兩個問題,這時候就要用到 mini-css-extract-plugin 這個插件了。

// webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  entry,
  output,
  module: {
    rules: [{
      test: /\.css$/,
      use: [MiniCssExtractPlugin.loader, 'css-loader']
    }]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash].css',
      chunkFilename: '[id].[contenthash].css',
    })
  ]
}

從上面的配置中能夠看出,mini-css-extract-plugin 並非單獨做爲一個 plugin 來使用的,它還充當了 loader 的做用,代替了 style-loader 。前面在介紹 style-loader 的時候提到,它的做用是將轉換後的樣式插入到頁面中,既然咱們如今須要將 css 和 js 分離開,因此也就不須要再用到 style-loader 了。

看成爲插件使用的時候, mini-css-extract-plugin 能夠接受兩個可選參數:

  • filename :分離出的css文件名稱,寫法和 output 的 filename 選項相同,惟一區別是當你想使用緩存的時候,填寫的是 contenthash 而不是 chunkhash
  • chunkFilename :切割出的css文件塊名稱,寫法和 filename 相同

最近發現 extract-text-webpack-plugin 也支持 webpack4 用法了 mini-css-extract-plugin 徹底相同,並且相較於 mini-css-extract-plugin 還多了一些可選的配置

npm i extract-text-webpack-plugin -D
// webpack.config.js
const ExtractCssChunksPlugin = require('extract-css-chunks-webpack-plugin');

module.exports = {
  entry,
  output,
  module: {
    rules: [{
      test: /\.css$/,
      use: [ExtractCssChunksPlugin.loader, 'style-loader']
    }]
  },
  plugins: [
    new ExtractCssChunksPlugin({
      filename: '[name].[contenthash].css',
      chunkFilename: '[id].[contenthash].css',
      hot: true, //HMR 下面會着重介紹
      orderWarning: true, // Disable to remove warnings about conflicting order between imports
      reloadAll: true, //當啓用HMR時,強制從新加載全部css
      cssModules: true //若是啓用了 cssModules 此選項設置爲 true
    })
  ]
}

壓縮css

npm i optimize-css-assets-webpack-plugin -D

在將 css 從 js 中分離出來不以前,咱們是不須要考慮壓縮 css 的,由於樣式都被打包進了 js 文件中,當咱們設置 mode 爲 production 時,webpack 會自動壓縮 js 文件。可是咱們如今將 css 從 js 中分離出來了,webpack 目前還不能自動壓縮 css 文件。幹!真是麻煩!這時候又要用到插件來幫我壓縮分離出來的 css 文件了。

// webpack.config.js
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');

module.exports = {
  entry,
  output,
  plugins: [
    new OptimizeCssAssetsPlugin()
  ]
}

這裏講一個坑,在 webpack4 以前,壓縮都是經過 webpack.optimize.UglifyJsPlugin 這個插件來完成。webpack4 新增了 mode 和 optimization 兩個選項,當 mode 設置爲 production 時會自動壓縮 js 文件(這個已經提過屢次了),其實將 mode 設置爲 production 時, optimization.minimize 便會默認設置爲 true ,意思就是在打包的時候對 js 進行壓縮。而若是你想用第三方壓縮插件,你能夠將插件寫在 plugins 中,也能夠寫在 optimization.minimizer 中。可是如你將壓縮插件寫在 optimization.minimizer 中時,webpack 就會默認讀取 ptimizatio.minimizer 這個選項了,這也就意味着,這時候若是你不手動的配置 js 壓縮插件,js 文件是不會被壓縮,這時候又須要尋找壓縮 js 的插件,好比 uglifyjs-webpack-plugin ,而後再配置一下,說實話這樣真的很煩,因此我直接將壓縮的插件配置在了 plugins 中,這樣就省去了對 js 壓縮插件的配置。webpack 的文檔中描述了相關說明 Minimizing For Production

複製靜態資源

npm i copy-webpack-plugin -D

有時候咱們的項目中會有一些靜態資源,好比網站的favicon、你從不知道的地方找來的不知名的js插件等等,這些靜態資源並不會在項目中經過 import 的方式顯式的加載進來,而是在直接寫在頁面中

...
<link rel="shortcut icon" href="static/favicon.ico">
...
<script src="static/xxx.js"></script>

對於這些靜態資源,webpack 在打包過程當中不會對它們進行處理,全部須要咱們 copy 到打包後的目錄中,從而保證項目不會由於缺乏這些靜態文件而報錯, copy-webpack-plugin 的做用即是 copy 這些靜態資源到指定的目錄中的。

// webpack.config.js
const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
  entry,
  output,
  plugins: [
    new CopyWebpackPlugin([
      { from:'static/**',to: 'dist/static' }
    ])
  ]
}

上面的配置表示將 static 文件夾下全部的文件都複製到 dist/static 下面,若是你熟悉 gulp 的話,你會發現這其實就是一個移除了 pipe 的 gulp。

其實對於copy文件這種髒活累活你也能夠用你熟悉的方式來完成,好比 gulp、fs-extra 等。

clean-webpack-plugin

npm i clean-webpack-plugin -D

若是咱們打包輸出的文件使用了 chunkhash 、 hash 等來命名的話,隨着文件的變動和打包次數的增長,dist 目錄會淤積不少無用的打包文件,這時候即可以藉助 clean-webpack-plugin 幫咱們清除一些這些無用的文件

// webpack.config.js
const CleanWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
  entry,
  output,
  plugins: [
    new CleanWebpackPlugin('dist', {
      root: __dirname,
      verbose: true,
      dry: false
    })
  ]
}

和 copy 文件同樣,刪除文件這種話不必定非得讓 webpack 來作,咱們也能夠藉助其餘的方式來完成,好比我要再提一遍的的 gulp ,又或者 rimrafdel 等。可是區別是你須要手動的控制一下任務的流程,總不能在打包完成才刪除問吧,因此用 webpack 提供的插件是不須要考慮任務流程的問題。

上面介紹了5個 webpack 的 plugin ,主要目的是讓你們體會 webpack plugin 的做用基本用法。 實際上 webpack 的 plugin 還有不少不少,幾乎能夠知足你在項目構建中的各類需求,webpack 官網了列舉不少官方推薦的 plugin https://webpack.js.org/plugins/ ,有興趣的同窗能夠前往查看。

相關文章
相關標籤/搜索