前言
在學習前端的的過程中,多少一定都有聽過 Webpack 這個名詞,而什麼是 Webpack 呢?當我聽到這個問題的時候,第一時間會先愣住,不知從何回答起,經過幾秒鐘的思考後,最後回答會是「它是一個模組打包工具」,不知道大家有沒有和我一樣呢?這個回答沒有錯,不過可能會有更多疑問,那模組又是什麼?為什麼需要打包?
這篇文章,將從模組化切入,來帶大家了解 Webpack 究竟幫我們做了多少事?
模組化
為什麼需要模組化設計?
試著回想我們最開始學習 JavaScript 的時候,我們會把程式碼寫在 HTML 檔案中,如下
檔案:index.html
<script>
var a = 1
function double(x){
return 2 * x
}
double(a)
</script>
但隨著功能越來越多我們會把 script
裡面的內容獨立出來到一個 JS 檔中:
檔案:index.html
<script src="./main.js"></script>
檔案:main.js
var a = 1
function double(x) {
return 2 * x
}
double(a)
var b = 2
function square(x) {
return x * x
}
square(b)
var c = 4
function add(x, y) {
return x + y
}
add(c, b)
再來想像一下,當開發人員越來越多,功能也一直再新增,如果都繼續編寫同一個 JS 檔,當行數不再是 24 而是 1,000,將變得非常難以閱讀及維護。為了避免這樣的情況發生,我們可以拆成多個 JS 檔,如下:
檔案:index.html
<script src="./utils.js"></script>
<script src="./main.js"></script>
檔案:utils.js
function double(x) {
return 2 * x
}
function square(x) {
return x * x
}
function add(x, y) {
return x + y
}
檔案:main.js
var a = 1
var b = 2
var c = 4
double(a)
square(b)
add(c, b)
目前我們雖然把程式碼分開了,但實際上它還是在同一個作用域下,其中的變數是會互相影響。例如,我新增了一個 utils2.js 檔,並宣告一個 add
函式,且實作內容和 utils.js 中的 add
函式不一樣,如下
檔案:index.html
<script src="./utils.js"></script>
<script src="./utils2.js"></script>
<script src="./main.js"></script>
檔案:utils.js
function add(x, y){
return x + y
}
檔案:utils2.js
function add(x, y){
return x + y + 4
}
檔案:main.js
var b = 2
var c = 4
console.log(add(c, b)) // 10
如果我們期望在 main.js 使用 add
函式會得到兩數之和,會因為 utils2.js 在 utils.js 後面載入,而覆蓋掉 utils.js 的函式宣告,最後導致結果出現不如預期。所以我們需要將 utils.js 與 utils2.js 這兩個檔案的內容各自獨立出來,互不干擾,而想要達到這樣的效果,我們可以使用 IIFE 來完成。
IIFE(立即呼叫函式 Immediately Invoked Functions)
檔案:utils.js
var moduleUtils = (function () {
var author = "Mr.A"
return {
add: function (x, y) {
return x + y
},
getAuthor: function () {
return author
},
}
})()
檔案:utils2.js
var moduleUtils2 = (function () {
var author = "Mr.B"
return {
add: function (x, y) {
return x + y + 4
},
getAuthor: function () {
return author
},
}
})()
檔案:main.js
var b = 2
var c = 4
console.log(moduleUtils.getAuthor()) // 'Mr.A'
console.log(moduleUtils.add(c, b)) // 6
console.log(moduleUtils2.getAuthor()) // 'Mr.B'
console.log(moduleUtils2.add(c, b)) // 10
透過立即呼叫函式的方式,建立作用域幫我們達到三點效果:
1. 解決變數或是函式命名上的衝突
2. 能夠建立私有變數與方法
3. 自由選擇要將哪些變數、方法暴露出去
因此,我們打算在 main.js 檔案中,取得變數 author
的值,只能透過 moduleUtils.getAuthor()
的方法。最值得注意的地方是,這樣子規劃,只能讀取而無法直接修改。這種模式就是最一開始模組化的概念,稱為模組模式。
這個模式看起來好像很完美了,但其實還是有一個小問題。假設 utils2.js 這個模組需要依賴 utils.js 的方法或是變數,我們就一定要注意 script 引入的先後順序,如下:
檔案:utils.js
var moduleUtils = (function () {
var author = "Mr.A"
return {
getAuthor: function () {
return author
},
}
})()
檔案:utils2.js
var moduleUtils2 = (function (module) {
var author = "Mr.B"
var authorFromUtils = module.getAuthor()
return {
getTwoAuthors: function () {
return author + "_&_" + authorFromUtils
},
}
})(moduleUtils)
檔案:main.js
console.log(moduleUtils.getAuthor()) // 'Mr.A'
console.log(moduleUtils2.getTwoAuthors()) // 'Mr.B_&_Mr.A'
檔案:index.html
<script src="./utils.js"></script>
<script src="./utils2.js"></script> <!-- utils2 一定要在 utils 後面引入 -->
<script src="./main.js"></script>
隨著相依性的檔案越來越多,專案的維護難度也會隨之提高。
综合以上,隨著前端專案規模越來越大,就越需要模組化的開發,來提升專案的維護性。隨著趨勢的發展,就有些模組化的規範相繼而出,而其中最為有名的就是 CommonJS。
Node.js 與 CommonJS
Node.js 能夠讓 JavaScript 在 server 環境中執行,並採用了 CommonJS 模組規範。所以在 server 端來說,每個檔案都是獨立的作用域,且都有 require
函式和 module
物件。
module
物件 每個檔案都會有module
物件,這個物件底下有一個exports
的屬性,如果在這個檔案中有想要輸出的變數或是方法,可以透過賦值給exports
來達成,如下:
只有單一值:
var name = 'will'
module.exports = name
多個值 第一種寫法:
var name = 'will'
module.exports.name = name
module.exports.getName = function(){
return name
}
多個值 第二種寫法:
var name = 'will'
function getName(){
return name
}
module.exports = {
name,
getName
}
require
函式 當需要載入某檔案,取得module.exports
的值時,會使用require
函式尋找該檔案,如下:
檔案:a.js
var name = 'will'
module.exports.name = name
module.exports.getName = function(){
return name
}
檔案:b.js
var aModule = require('./a.js')
aModule.getName() // will
了解 CommonJS 規範之後,我們就可以很輕鬆的將之前的範例改寫,如下:
檔案:utils.js
var author = 'Mr.A'
function getAuthor(){
return author
}
module.exports.getAuthor = getAuthor
檔案:utils2.js
var moduleUtils = require('./utils')
var author = 'Mr.B'
var authorFromUtils = moduleUtils.getAuthor()
function getTwoAuthors(){
return author + '_&_' + authorFromUtils
}
module.exports.getTwoAuthors = getTwoAuthors
檔案:main.js
var moduleUtils = require('./utils')
var moduleUtils2 = require('./utils2')
console.log(moduleUtils.getAuthor()) // 'Mr.A'
console.log(moduleUtils2.getTwoAuthors()) // 'Mr.B_&_Mr.A'
這樣寫相較 IIFE 簡單多了,但別忘了改成這樣子,實際上是沒辦法在瀏覽器上執行的,只能在 Node.js 環境下執行。既然瀏覽器不支援,要怎麼樣才能在瀏覽器執行?我們可以借助其他工具,也就是 Webpack,它會將我們的模組翻譯成瀏覽器支援的寫法,並且打包成一個壓縮過的 JS 檔。
不過在介紹 Webpack 之前,相信有些人會有疑惑,在寫專案都沒有使用 require
或 module.exports
,都是用 ES6 的 import
和 export
,而且瀏覽器也有支援,那為什麼還需要 Webpack? 在回答這個問題之前,先來認識一下 ES6 Module。
ES6 Module
在 ES6 以前,沒有正式的模組化規範,所以有各種模組化的寫法,像是上面介紹的 CommonJS,或是 AMD (Asynchromous Module Definition 非同步模組定義)等等。而 ES6 出來之後,終於有了正式的規範,就是上面提到的 import
與 export
,我們可以再把上面的例子用 ES6 Module 改寫,如下
檔案:utils.js
let author = 'Mr.A'
export function getAuthor(){
return author
}
檔案:utils2.js
import { getAuthor } from './utils.js'
let author = 'Mr.B'
let authorFromUtils = getAuthor()
export function getTwoAuthors(){
return author + '_&_' + authorFromUtils
}
檔案:main.js
import { getAuthor, author } from './utils.js'
import { getTwoAuthors } from './utils2.js'
console.log(getAuthor()) // 'Mr.A'
console.log(getTwoAuthors()) // 'Mr.B_&_Mr.A'
檔案:index.html
<script src="./main.js" type="module"></script>
依照目前的使用情境,看起來都沒有什麼太大的問題,但假設我們想要使用別人的套件呢?或許我們可以使用 CDN 引入,但如果說這個套件也沒有 CDN,我們就只能透過 NPM 或 Yarn 這類型的套件管理工具來下載。
套件下載完畢後,會放到 nodemodules 這個資料夾裡,這樣子我們引入的路徑就可能要改寫成 `'./nodemodules/xxx/index.js'這樣子。假設哪天套件的入口點換了,整個專案有使用到這個套件的都需要改寫,這樣其實很不方便。除此之外,有些套件輸出是寫
module.exports,我們也沒辦法使用
import` 來引入,因此這個時候,我們就需要藉由 Webpack 的幫助。
Webpack
初步認識 Webpack
在前面我們就有提到,Webpack 是一模組打包工具,不管我們是寫 CommonJS 又或是 ES6 Module 最終它會將我們的模組以及使用 NPM 安裝他人的模組一併打包成一個 JS 檔。接著讓我們來實際試試看如何使用 Webpack 打包。
如果上面的有跟著練習的話,目前的專案目錄會是這樣子
1.開啟終端機輸入下面兩行指令
npm init -y
npm install webpack webpack-cli --save-dev
順利的話,就會看到我們的目錄新增了兩個檔案分別是 package.json 和 package-lock.json,以及 node_modules 的資料夾
2.打開 package.json 並在 scripts 底下新增指令
檔案:package.json
{
"scripts": {
"build": "webpack"
},
}
3.新增 webpack.config.js 檔案
檔案:webpack.config.js
module.exports = {
mode: 'development',
entry: './main.js', // webpack 打包的起始檔案
output: {
path: __dirname, // 在這個 config 檔同層的目錄
filename: 'bundle.js'// 打包輸出的檔名
}
}
mode
這個屬性不寫的話預設會是 production
,代表在生產環境下使用,所以會自動幫你壓縮以及優化。development
這個模式代表開發的時候,打包的速度較快。
最後在終端機下執行:
$ npm run build
執行之後順利的話就會出現建置的成功訊息
接著修改我們 index.html,將 main.js 換成 bundle.js
檔案:index.html
<script src="./bundle.js"></script>
打開瀏覽器 console 之後,就會順利看到我們在 main.js 檔 console.log
的結果
這樣我們就成功地使用 Webpack 來幫我們把模組打包,不過目前為止我們還沒嘗試安裝套件來使用,接下來我們來試著安裝 React:
$ npm install react react-dom
安裝成功後,就來試試看能不能使用 React,並在畫面上印出 Hello, Webpack and React,所以將檔案做一下修改
檔案:index.html
<body>
<div id="root"></div>
<script src="./bundle.js"></script>
</body>
檔案:App.js
import React from "react"
function App() {
return React.createElement('h1', {}, "Hello Webpack and React")
}
export default App
檔案:main.js
import { getAuthor, author } from './utils.js'
import { getTwoAuthors } from './utils2.js'
import App from './App.js'
import ReactDom from 'react-dom'
import React from 'react'
const appEl = React.createElement(App)
ReactDom.render(appEl, document.getElementById('root'))
console.log(getAuthor())
console.log(getTwoAuthors())
最後我們再重新 build 一次:
$ npm run build
成功看一下建置的訊息會發現,Webpack 的確有去 node_modules ,將我們使用的套件模組一併打包。
最後打開畫面,除了之前在 console 印出來的結果外,也可以看到 Hello, Webpack and React!
強大的 Webpack
Webpack 打包 JS 模組只是它功能的冰山一角,如果有看過 Webpack 官網 可以看到首頁的這張圖:
Webpack 把任何資源都當作模組,像是 CSS 或是圖片,我們都可以用 import
的方式把檔案給引入進來,如果不使用 Webpack 是沒辦法做到的。而為了支援這樣子的功能,Webpack 透過定義不同的 loader
來識別檔案類型並轉譯成瀏覽器看得懂的語法。例如 babe-loader
能夠幫助我們將 ES6 以上的新語法轉譯成 ES5。除此之外,還可以透過 plugins
來擴充 Webpack 的功能,像是壓縮 CSS 檔,或是打包過後自動產生 HTML 等等。
結語
到這邊相信大家對 Webpack 應該有個基礎的認識了,也瞭解到為什麼需要使用 Webpack。回過頭來看在使用 creat-react-app 以及 Vue CLI,我們能夠輕鬆的 import
CSS 或圖片,背後都是因為有 Webpack 在處理。
最後,如果想要更加瞭解 Webpack,可以參考 五倍學院 的 Webpack5 入門 線上課程。