Webpack 簡介 :zap:

對於像我這樣的人來說,第一次接觸到 webpack 是像是這些 repository:

雖然這些 repository 放在一起很棒,但它們不一定是最好的學習工具。 像我的情況,我試著了解發生了那些事情,但還是很困惑,所以我決定不從這些大量而且分散的資源來理解。

我希望這個教學課程可以讓 webpack 更容易的學習。

需求

至少希望你了解基本的 node.js 和 npm。

貢獻

我很樂意接受任何所有的貢獻或是修正。如果你有任何問題,可以將這些問題發成 issue。如果我有錯誤的話,請將問題指出。最後,如果你覺得我漏了些什麼,或者可以將某些部分解釋的更好,留下一個 issue 或者是發送 Pull Request。

目錄

為什麼要 Webpack?

因為每個 react 或 redux 教學課程都假設你知道什麼是 webpack。:cry:

以下這些是更現實的原因,你可能會需要使用 webpack。

你可以:

  • 將你的 js 檔案 Bundle 變成單一的檔案
  • 在你的前端程式碼中使用 npm packages
  • 撰寫 JavaScript ES6 或 ES7(需要透過 babel 來幫助)
  • Minify 或優化程式碼
  • 將 LESS 或 SCSS 轉換成 CSS
  • 使用 HMR(Hot Module Replacement)
  • 包含任何類型的檔案到你的 JavaScript
  • 更多進階的東西,暫時不介紹
為什麼我需要這些功能?
  • Bundle JS 檔案 - 讓你可以撰寫模組化的 JavaScript,但是你不需要 include 每個 JavaScript <script> 的檔案(如果你需要多個 JavaScript 檔案可以透過設定來完成)。

  • 在你的前端程式碼中使用 npm packages - npm 在 internet 上是一個大型的 open source 生態系統。可以儲存或發佈你的程式碼,你可以到 npm 看一看,可能包含你想要的前端套件。

  • ES6 和 ES7 - 加入一些 JavaScript 的新功能,讓撰寫程式碼可以更容易而且更強大,請看這裡的介紹

  • Minify 或優化程式碼 - 減少你的檔案大小,好處包括像是更快的將頁面載入。

  • 將 LESS 或 SCSS 轉換成 CSS - 使用更好的方式來撰寫 CSS, 如果你不熟悉的話,這裡有一些介紹

  • 使用 HMR - 增加開發速度。每當你儲存程式碼的時候,它可以注入到網頁,而不需將網頁刷新。如果當編輯你的程式碼,你需要維護頁面的狀態,這是非常方便的。

  • 包含任何類型的檔案到你的 JavaScript - 減少對其他 build 工具的需要,讓你可以透過程式的方式修改或使用這些檔案。

基礎

安裝

你需要全域安裝來使用 webpack 大部分的功能:

npm install -g webpack

然而 webpack 有些功能,像是優化的 plugins,需要你將它安裝在本機。像這種情況下你需要:

npm install --save-dev webpack

命令

如果要執行 webpack:

webpack

如果你想要 webpack 在你每次變更儲存檔案後自動執行 build:

webpack --watch

如果你想要使用自訂的 webpack 設定檔:

webpack --config myconfig.js

Bundling

範例一

Official Dependency Tree

Webpack 簡稱為模組整合工具。如果你想要深入的話,可以拜訪 JavaScript Modules: A Beginner’s GuideJavaScript Modules Part 2: Module Bundling 這兩篇優秀的解釋文章:

我們要保持它的簡單,webpack 運作的方式是透過指定一個單一檔案作為你的進入點。 這個檔案會是 tree 的 root。然後你每次 require 一個檔案從其他檔案並把它加入到 tree。當你執行 webpack,所有的檔案和 module 都會被 bundle 成一個檔案。

這裡是一個簡單的範例:

Dependency Tree

根據這樣的情況,你可以有這樣的目錄:

MyDirectory
|- index.js
|- UIStuff.js
|- APIStuff.js
|- styles.css
|- extraFile.js

這些可能是你檔案的內容:

// index.js
require('./styles.css')
require('./UIStuff.js')
require('./APIStuff.js')

// UIStuff.js
var React = require('React')
React.createClass({
  // stuff
})

// APIStuff.js
var fetch = require('fetch') // fetch polyfill
fetch('https://google.com')
/* styles.css */
body {
  background-color: rgb(200, 56, 97);
}

當你執行 webpack,你會得到一個這個 tree 的 bundle 內容,雖然 extraFile.js 也是在相同的目錄中,但它不是被 bundle 的一部份,因為它在 index.js 沒有被 require

bundle.js 看起來會像:

// contents of styles.css
// contents of UIStuff.js + React
// contents of APIStuff.js + fetch

被 bundle 的這些檔案是你明確所 require 進來的檔案。

Loaders

你可能會注意到,我在上方的範例做了一些奇怪的事情。我在 JavaScript 檔案中 require 一個 css 檔案。

關於 webpack 真的很酷,有趣的事情是,你可以 require 其他不只是 JavaScript 的檔案。

在 webpack 這些東西我們稱為 loader。使用這些 loader,你可以 require 任何 .css.png.html 檔。

例如在上圖我有:

// index.js
require('./styles.css')

如果在我的 webpack 設定檔中,inclue style-loadercss-loader,這是可行的,還可以實際應用 CSS 到我的網頁。

你可以在 webpack 使用多個 loader,這裡只是一個單一的例子。

Plugins

Plugin,顧名思義就是替 webpack 增加額外的功能。其中常使用到的一個 plugin 是 UglifyJsPlugin,它可以 minify 你的 JavaScript 程式碼。我們之後會介紹如何使用。

你的 webpack 設定檔案

Webpack 沒辦法直接使用,需要透過你的需求來做設定。為了做到這一點,你需要建立一個檔案叫做:

webpack.config.js

預設情況下,webpack 會去識別這個檔名。如果你選擇使用不同的檔名,你需要加入 --config 來指定你的檔案名稱。

一個簡單的範例

範例二

你的目錄結構像是這樣:

MyDirectory
|- dist
|- src
   |- index.js
|- webpack.config.js

然後這是一個非常簡易的 webpack 設定:

// webpack.config.js
var path = require('path')

module.exports = {
  entry: ['./src/index'], // 在 index 檔案後的 .js 副檔名是可選的
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js'
  }
}

我們一個一個複習這些屬性:

  • entry - 這是你的 bundle 的進入點,我們曾在前面 bundling 的部分討論過它。entry 是一個陣列,根據你的需求,webpack 允許可以有多個進入點,來產生多個 bundle 檔案。

  • output - 宣告 webpack 輸出的形式。

    • path - 存放 bundle 檔案的位置。
    • filename - bundle 檔案名稱。

根據上面的設定,會在你的 dist 資料夾建立一個叫做 bundle.js 的檔案。

介紹 Plugins

範例三

想像一下,你使用 webpack 將你的檔案 bundle 在一起,然後你發現到 bundle 後的結果是 900KB。這是個問題,但是你可以透過 minify 你的 bundle 檔案來做改善。要做到這一點,你需要使用一個我在前面稍早提到的 UglifyJsPlugin plugin。

此外,你需要在本機安裝 webpack 才能實際的去使用這個 plugin。

npm install --save-dev webpack

現在你可以 require webpack 並 minify 你的程式碼。

// webpack.config.js
var path = require('path')
var webpack = require('webpack')

module.exports = {
  entry: ['./src/index'],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js'
  },

  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      compressor: {
        warnings: false,
      },
    })
  ]
}

我們一個一個複習這些屬性:

如此一來,當我們執行 webpackUglifyJsPlugin 透過像是移除所有空白等處理,可以將你的檔案減少至 200KB。

你也可以加入 OccurrenceOrderPlugin

根據調用次數指定 module 和 chunk 的 id。越常用的 id 較小(短)的 id。這使得 id 可以預測,可以減少檔案的大小,並是推薦的方法。

老實說,我不太確定底層的機制是如何工作的,但在根據目前 webpack2 beta 的預設情況下,所以我將它包含在內。

// webpack.config.js
var path = require('path')
var webpack = require('webpack')

module.exports = {
  entry: ['./src/index'],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      compressor: {
        warnings: false,
      },
    }),
    new webpack.optimize.OccurrenceOrderPlugin()
  ]
}

所以現在我們寫了一個設定檔讓我們可以 minify 和 bundle 我們的 JavaScript。這個 bundle 檔案可以被複製並貼到其他的專案目錄中,放入 <script> 就可以使用。如果你只需要了解怎麼用 webpack 處理只有 JavaScript 的基本情況,你可以直接跳到結論

一個更完整的範例

此外,比起單單使用 JavaScript,使用 webpack 可以做的更多,你可以避免複製、貼上並透過 webpack 管理你的整個專案。

在下面的部份中,我們要使用 webpack 建立一個非常簡單的網站。如果你想要跟著這個範例,建立一個像下方的目錄結構:

MyDirectory
|- dist
|- src
   |- index.js
   |- index.html
   |- styles.css
|- package.json
|- webpack.config.js

內容

  1. 介紹 Loaders - 我們將會加入 loader,這可以讓我 bundle 加入的 CSS。
  2. 加入更多的 Plugins - 我們加入一個 plugin 來幫助我們建立和使用一個 HTML 檔案。
  3. 開發伺服器 - 我們會將 webpack 設定檔案分為 developmentproduction 兩種版本,然後使用 webpack-dev-server 來查看我們的網站並啟用 HMR。
  4. 開始撰寫程式 - 我們來實際寫一些 JavaScript。

介紹 Loaders

範例四

在稍早前面的教學課程中我提到了 loaders。這些程式碼來幫助我們 require 非 JavaScript 的檔案。在這種情況下,我們將需要 style-loadercss-loader。首先我們需要安裝這些 loader:

npm install --save-dev style-loader css-loader

現在安裝完後,我們可以調整我們的 webpack 設定來引入 css-loader

// webpack.config.js
var path = require('path')
var webpack = require('webpack')

module.exports = {
  entry: ['./src/index'],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      compressor: {
        warnings: false,
      },
    }),
    new webpack.optimize.OccurrenceOrderPlugin()
  ],
  module: {
    loaders: [{
      test: /\.css$/,
      loaders: ['style', 'css']
    }]
  }
}

我們一個一個查看這些新屬性

  • module - 設定你的檔案選項。
    • loaders - 我們為應用程式所指定的一個 loader 陣列。
      • test - 一個正規表達式,用來找出要套用 loader 的檔案。
      • loaders - 指定哪些 loader 是用於匹配前述 test (正規表達式)的檔案。

這個時候你執行 webpack,如果你 require 的檔案結尾是 .css,然會我們會使用 stylecss loader,將 CSS 加入到 bundle。

如果我們沒有 loaders,我們會得到像是這樣的錯誤:

ERROR in ./test.css
Module parse failed: /Users/Developer/workspace/tutorials/webpack/part1/example1/test.css
Line 1: Unexpected token {
You may need an appropriate loader to handle this file type.

可選項目

如果你想要使用 SCSS 而不是 CSS 你需要執行:

npm install --save-dev sass-loader node-sass webpack

然後你的 loader 必須修改成:

{
  test: /\.scss$/,
  loaders: ["style", "css", "sass"]
}

處理 LESS 也類似於這個方式。

要知道這些需要被指定的 loader 是有順序的,這是一個很重要部分。在上面的範例,sass loader 是第一個應用在你的 .scss 檔案,然後是 css loader,最後是 style loader。你可以看到,這些 loader 的應用模式是由右到左。

加入更多的 Plugins

範例五

現在我們的網站已經有了樣式的基本架構,我們還需要一個實際的頁面來套用這些樣式。

我們透過 html-webpack-plugin 來做,它讓我們可以產生一個 HTML 頁面,或是使用現有的頁面。我們將使用一個目前現有的 index.html

首先我們需要安裝 plugin:

npm install --save-dev html-webpack-plugin@2

然後在我們的 webpack 設定檔加入:

// webpack.config.js
var path = require('path')
var webpack = require('webpack')
var HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  entry: ['./src/index'],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      compressor: {
        warnings: false,
      },
    }),
    new webpack.optimize.OccurrenceOrderPlugin(),
    new HtmlWebpackPlugin({
      template: './src/index.html'
    })
  ],
  module: {
    loaders: [{
      test: /\.css$/,
      loaders: ['style', 'css']
    }]
  }
}

這個時候當你執行 webpack,因為我們指定了一個搭配 ./src/index.html template 的 HtmlWebpackPlugin,它會產生一個檔案叫做 index.html 在我們的 dist 資料夾,而網頁的內容是 ./src/index.html

index.html 作為 template 如果是空的就沒意義了,現在是個好時機我們可以填入一些元素進去。

<html>
<head>
  <title>Webpack Tutorial</title>
</head>
<body>
  <h1>Very Website</h1>
  <section id="color"></section>
  <button id="button">Such Button</button>
</body>
</html>

注意到我們沒有放入一個 bundle.js<script> 標籤到我們的 HTML。實際上 plugin 會自動的幫你處理。如果你放入 script,到頭來你會載入兩次相同的程式碼。

而讓我們加入一些基本的樣式在 styles.css

h1 {
  color: rgb(114, 191, 190);
  text-align: center;
}

#color {
  width: 300px;
  height: 300px;
  margin: 0 auto;
}

button {
  cursor: pointer;
  display: block;
  width: 100px;
  outline: 0;
  border: 0;
  margin: 20px auto;
}

開發伺服器

範例六

現在我們想要實際在瀏覽器看到我們的網站,這就需要一個 web 伺服器來跑我們的程式碼。webpack 自帶了方便的 webpack-dev-server,你需要在本機和全域安裝。

npm install -g webpack-dev-server
npm install --save-dev webpack-dev-server

dev server 可以在瀏覽器看到你的網站外觀以及可以更快速的開發,是一個相當有用的資源。預設情況下你可以拜訪 http://localhost:8080。不幸的是,像是 hot reloading 的功能並不是內建的,還需要一些其他的設定。

這裡是 webpack 設定檔一個很棒的分離點,你可以將它分成用於「development」以及用於「production」。因為我們在這個教學課程中將盡量保持簡單,所以兩個設定之間不會有非常大的不同,不過這是 webpack 極度可設置性的一個入門。我們將兩個設定檔命名為 webpack.config.dev.jswebpack.config.prod.js

// webpack.config.dev.js
var path = require('path')
var webpack = require('webpack')
var HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  devtool: 'cheap-eval-source-map',
  entry: [
    'webpack-dev-server/client?http://localhost:8080',
    'webpack/hot/dev-server',
    './src/index'
  ],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new HtmlWebpackPlugin({
      template: './src/index.html'
    })
  ],
  module: {
    loaders: [{
      test: /\.css$/,
      loaders: ['style', 'css']
    }]
  },
  devServer: {
    contentBase: './dist',
    hot: true
  }
}

改變

  1. dev 設定檔省略了優化,因為當你不斷的 rebuild 時,它們是不必要的。所以拿掉了 webpack.optimize plugins。

  2. dev 設定檔需要對 dev server 做必要的設定,你可以到這裡了解更多。

總結:

  • entry: 兩個新的進入點將伺服器連結到瀏覽器,方便 HMR。
  • devServer
    • contentBase: 服務的檔案來自哪裡。
    • hot: 啟用 HMR。

prod 設定檔不需要改變太多:

// webpack.config.prod.js
var path = require('path')
var webpack = require('webpack')
var HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  devtool: 'source-map',
  entry: ['./src/index'],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      compressor: {
        warnings: false,
      },
    }),
    new webpack.optimize.OccurrenceOrderPlugin(),
    new HtmlWebpackPlugin({
      template: './src/index.html'
    })
  ],
  module: {
    loaders: [{
      test: /\.css$/,
      loaders: ['style', 'css']
    }]
  }
}

我也加入一個全新的屬性在 dev 和 prod 設定檔:

  • devtool - 這是協助 debug 的工具。基本上,當你得到一個錯誤,它會幫助你找到哪裡發生了錯誤,像是 chrome developer console。source-mapcheap-eval-source-map 之間的差異從文件說明有點難解釋。我可以肯定的是,source-map 是用於 productioncheap-eval-source-map 是用於 development

如果要執行 dev server,我們可以執行:

webpack-dev-server --config webpack.config.dev.js

如果我們要 build production 的程式碼,我們可以執行:

webpack --config webpack.config.prod.js

如果想要讓這些指令使用的更容易,我們可以到 package.json 來設定簡單的 script。

我們加入 scripts 屬性到設定檔:

// package.json
{
  //...
  "scripts": {
    "build": "webpack --config webpack.config.prod.js",
    "dev"  : "webpack-dev-server --config webpack.config.dev.js"
  }
  //...
}

我們可以執行這些指令:

npm run build
npm run dev

你現在可以透過 npm run dev,並導到 http://localhost:8080 看到你的網站。

備註: 當我正在測試這個部份時,我了解到,當我修改 index.html 檔案時,伺服器不能 hot reload。解決這個問題的方法在 html-reload。這裡涵蓋了一些 webpack 設定檔選項的有用資訊,我推薦你可以看一下,但是我把它分開了,因為我覺得會因為這個不太重要的原因,這會延長這個教學課程。

開始撰寫程式

範例七

大多數的人似乎會慌亂的原因是因為:webpack 事實上需要通過這些取得的進入點來撰寫 JavaScript;然而我們現在已經到達了這個教學課程最高潮的部分。

如果你還沒準備好:執行 npm run dev,以及導到 http://localhost:8080。設定 dev server 是不是可以 hot reload。在你每次儲存你專案所編輯的任何一個檔案部份時,瀏覽器將會重新載入來顯示你的修改。

我們也需要 npm package,為了來示範如何在前端使用它們。

npm install --save pleasejs

PleaseJS 是一個隨機色彩的產生器,其中我們需要在按鈕中加入 hook 來改變我們的 div 顏色。

// index.js

// 接受 hot module reloading
if (module.hot) {
  module.hot.accept()
}

require('./styles.css') // 網頁現在有了樣式
var Please = require('pleasejs')
var div = document.getElementById('color')
var button = document.getElementById('button')

function changeColor() {
  div.style.backgroundColor = Please.make_color()
}

button.addEventListener('click', changeColor)

有趣的是,為了讓 Hot Module Replacement 可以執行,你需要加入下面的程式碼:

if (module.hot) {
  module.hot.accept()
}

在一個 module 或是它的父 module。

然後我們就完成了!

備註: 你可能已經注意到在你的 css 被使用之前有些 delay,或許事實上你討厭將你的 css 放入到 JavaScript 檔案中。我留了另一個範例:css-extract,描述如何將你的 CSS 放在不同的檔案。

結論

我希望這些是有幫助的。

首先 Webpack 最重要的它是一個模組整合工具。它是一個高度模組化的工具,事實上,它並不是被限於在 ES6 和 React。

現在考慮到:

  • Part 2 將解決使用 Webpack 透過 Babel 將 ES6 轉換到 ES5。
  • Part 3 將解決使用 Webpack 和 React + Babel。

因為這是這常見的例子。

反思

恭喜!你已經讓你個按鈕去改變你的 div 的顏色!webpack 是不是很棒?

沒錯是的!但是,如果你所做的事情只是讓按鈕去改變 div 的顏色,它可能不值得你去寫像是這樣的設定。如果你想這麼做的話,你可能會感到...疲累。:anguished:

results matching ""

    No results matching ""