Webpack前世今生

說故事的五公子發表於2020-07-22

在正式介紹Webpack之前,先給大家說明一下前端為什麼需要模組化

1.為什麼需要模組化

1.1JS原始功能

在網頁開發的早期,js製作作為一種指令碼語言,做一些簡單的表單驗證或動畫實現等,那個時候程式碼還是很少的。那個時候的程式碼是怎麼寫的呢?直接將程式碼寫在<script>標籤中即可。隨著ajax非同步請求的出現,慢慢形成了前後端的分離,客戶端需要完成的事情越來越多,程式碼量也是與日俱增。為了應對程式碼量的劇增,我們通常會將程式碼組織在多個js檔案中,進行維護。但是這種維護方式,依然不能避免一些災難性的問題。比如全域性變數同名問題,看下面的例子:

image-20200720090601237

小明後來發現程式碼不能正常執行,去檢查自己的變數,發現確實true,最後杯具發生了,小明加班到2點還是沒有找到問題出在哪裡(所以,某些加班真的是無意義的)

另外,這種程式碼的編寫方式對js檔案的依賴順序幾乎是強制性的,但是當js檔案過多,比如有幾十個的時候,弄清楚它們的順序是一件比較同時的事情。而且即使你弄清楚順序了,也不能避免上面出現的這種尷尬問題的發生。

1.2匿名函式解決方案

我們可以使用匿名函式來解決方面的重名問題在aaa.js檔案中,我們使用匿名函式

(function(){
    var flag=true
})()

但是如果我們希望在main.js檔案中,用到flag,應該如何處理呢?顯然,另外一個檔案中不容易使用,因為flag是一個區域性變數。

1.3使用模組作為出口

我們可以使用將需要暴露到外面的變數,使用一個模組作為出口,什麼意思呢?來看下對應的程式碼:

image-20200720095420223

我們做了什麼事情呢?非常簡單,在匿名函式內部,定義一個物件。給物件新增各種需要暴露到外面的屬性和方法(不需要暴露的直接定義即可)。最後將這個物件返回,並且在外面使用了一個MoudleA接受。接下來,我們在man.js中怎麼使用呢?我們只需要使用屬於自己模組的屬性和方法即可。這就是模組最基礎的封裝,事實上模組的封裝還有很多高階的話題,但是我們這裡就是要認識一下為什麼需要模組,以及模組的原始雛形。幸運的是,前端模組化開發已經有了很多既有的規範,以及對應的實現方案。常見的模組化規範CommonJS、AMD、CMD,也有ES6的Modules

1.4CommonJS(瞭解)

模組化有兩個核心:匯出和匯入

CommonJS的匯出:

image-20200720100108626

CommonJS的匯入:

image-20200720100139721

export基本使用:

export指令用於匯出變數,比如下面的程式碼:

export let name = 'wugongzi'
export let age = 19

上面的程式碼還有另外一種寫法:

let name = 'wugongzi'
let age = 19
export {name,age}

匯出函式或類:

上面的程式碼主要輸出變數,也可以輸出函式或者輸出類

export function test(content) {
    console.log(content)
}

export class Person{
    constructor(name,age){
        this.name=name;
        this.age=age;
    }
    
    run() {
        console.log(this.name + '在奔跑')
    }
}

或者是下面這種形式:

function test(content) {
    console.log(content)
}

class Person{
    constructor(name,age){
        this.name=name;
        this.age=age;
    }
    
    run() {
        console.log(this.name + '在奔跑')
    }
}

export {test,Person}

export default:

某些情況下,一個模組中包含某個的功能,我們並不希望給這個功能命名,而且讓匯入者可以自己來命名,這個時候就可以使用export default

//info.js
export default function() {
    console.log('default function')
}	

我們來到main.js中,這樣使用就可以了,這裡的myFunc是我自己命名的,你可以根據需要命名它對應的名字

import myFunc from './info.js'
myFunc()

注意:export default 在同一模組中不允許同時存在多個

import:

我們使用export指令匯出了模組對外提供的介面,下面我們就可以通過import命令來載入對應的這個模組了

首先,我們需要在HTML程式碼中引入兩個js檔案,並且型別需要設定為module

<script src="info.js" type="module"></script>
<script src="main.js" type="module"></script>

import指令用於匯入模組中的內容,比如main.js的程式碼

import {name,age} from './info.js'
console.log(name,age)

如果我們希望某個模組中所有的資訊都匯入,一個個匯入顯然有些麻煩:通過*可以匯入模組中所有的export變數

但是通常情況下我們需要給*起一個別名,方便後續的使用

import * as info from './info.js'
console.log(info.name,info.age)

2.什麼是Webpack

什麼是webpack?這個webpack還真不是一兩句話可以說清楚的。我們先看看官方的解釋:

At its core, webpack is a static module bundler for modern JavaScript applications.

從本質上來講,webpack是一個現代的JavaScript應用的靜態模組打包工具。但是它是什麼呢?用概念解釋概念,還是不清晰。我們從兩個點來解釋上面這句話:模組打包

2.1模組

在前面學習中,我已經用了大量的篇幅解釋了為什麼前端需要模組化。而且我也提到了目前使用前端模組化的一些方案:AMD、CMD、CommonJS、ES6。在ES6之前,我們要想進行模組化開發,就必須藉助於其他的工具,讓我們可以進行模組化開發。並且在通過模組化開發完成了專案後,還需要處理模組間的各種依賴,並且將其進行整合打包。而webpack其中一個核心就是讓我們可能進行模組化開發,並且會幫助我們處理模組間的依賴關係。而且不僅僅是JavaScript檔案,我們的CSS、圖片、json檔案等等在webpack中都可以被當做模組來使用(在後續我們會看到)。這就是webpack中模組化的概念。

2.2打包

打包如何理解呢?理解了webpack可以幫助我們進行模組化,並且處理模組間的各種複雜關係後,打包的概念就非常好理解了。就是將webpack中的各種資源模組進行打包合併成一個或多個包(Bundle)。並且在打包的過程中,還可以對資源進行處理,比如壓縮圖片,將scss轉成css,將ES6語法轉成ES5語法,將TypeScript轉成JavaScript等等操作。但是打包的操作似乎grunt/gulp也可以幫助我們完成,它們有什麼不同呢?

image-20200722054953713

3.和grunt/gulp的對比

grunt/gulp的核心是Task,我們可以配置一系列的task,並且定義task要處理的事務(例如ES6、ts轉化,圖片壓縮,scss轉成css),之後讓grunt/gulp來依次執行這些task,而且讓整個流程自動化。所以grunt/gulp也被稱為前端自動化任務管理工具。我們來看一個gulp的task:

image-20200720103933312

上面的task就是將src下面的所有js檔案轉成ES5的語法。並且最終輸出到dist資料夾中。什麼時候用grunt/gulp呢?如果你的工程模組依賴非常簡單,甚至是沒有用到模組化的概念。只需要進行簡單的合併、壓縮,就使用grunt/gulp即可。但是如果整個專案使用了模組化管理,而且相互依賴非常強,我們就可以使用更加強大的webpack了。所以,grunt/gulp和webpack有什麼不同呢?

  • grunt/gulp更加強調的是前端流程的自動化,模組化不是它的核心。

  • webpack更加強調模組化開發管理,而檔案壓縮合並、預處理等功能,是他附帶的功能。

4.webpack的安裝

在使用webpack之前我們需要先安裝webpack,安裝webpack首先需要安裝Node.js,Node.js自帶了軟體包管理工具npm。Node.js安裝比較簡單,從官網下載安裝包後一路next即可安裝成功,安裝好了以後記得配置環境變數(環境變數的具體配置不會的可以參考下網上的教程)。

安裝好了以後可以通過node -v檢視自己的Node版本

全域性安裝webpack:

npm install webpack -g

區域性安裝webpack:(後續專案中會用到)

cd 專案對應目錄
//@3.6.0是版本號 --save-dev是開發時依賴,專案打包後不需要繼續使用的。
npm install webpack@3.6.0 --save-dev

為什麼全域性安裝後,還需要區域性安裝呢?在終端直接執行webpack命令,使用的全域性安裝的webpack。當在package.json中定義了scripts時,其中包含了webpack命令,那麼使用的是區域性webpack

5.webpack起步

5.1建立檔案

我們建立如下檔案和資料夾:

image-20200720105651305

  • dist資料夾:用於存放之後打包的檔案(目前為空)

  • src資料夾:用於存放我們寫的原始檔

    • main.js:專案的入口檔案。具體內容檢視下面詳情。
    • mathUtils.js:定義了一些數學工具函式,可以在其他地方引用,並且使用。具體內容檢視下面的詳情。
  • index.html:瀏覽器開啟展示的首頁html

  • package.json:通過npm init生成的,npm包管理的檔案(暫時沒有用上,後面才會用上)

mathUtils.js中的程式碼:

function add(num1,num2){
	return num1+num2;
}

function mul(num1,num2){
	return num1*num2;
}

module.exports = {
	add,mul
}

main.js中的程式碼:

const math = require('./mathUtils.js')

console.log('Hello webpakc');
console.log(math.add(10,20));
console.log(math.mul(10,20));

5.2檔案打包

現在的js檔案中使用了模組化的方式進行開發,他們可以直接使用嗎?不可以。因為如果直接在index.html引入這兩個js檔案,瀏覽器並不識別其中的模組化程式碼。另外,在真實專案中當有許多這樣的js檔案時,我們一個個引用非常麻煩,並且後期非常不方便對它們進行管理。

我們應該怎麼做呢?使用webpack工具,對多個js檔案進行打包。我們知道,webpack就是一個模組化的打包工具,所以它支援我們程式碼中寫模組化,可以對模組化的程式碼進行處理。(如何處理的,待會兒在原理中,我會講解)另外,如果在處理完所有模組之間的關係後,將多個js打包到一個js檔案中,引入時就變得非常方便了。那麼該如何打包呢?

我們可以在終端使用

webpack .\src\main.js -o .\dist\bundle.js

進行打包,如果自己的webpack版本較低,可以使用webpack .\src\main.js .\dist\bundle.js這個命令

image-20200722055653033

打包後會在dist檔案下,生成一個bundle.js檔案。檔案內容有些複雜,這裡暫時先不看,後續再進行分析。bundle.js檔案,是webpack處理了專案直接檔案依賴後生成的一個js檔案,我們只需要將這個js檔案在index.html中引入即可,不需要再像之前那樣需要引入很多JS檔案

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title></title>
	</head>
	<body>
		<script src="dist/bundle.js" type="text/javascript" charset="utf-8"></script>
	</body>
</html>

在瀏覽器中執行index.html便可在控制檯看到輸出效果

image-20200722055849306

6.webpack配置

上面我們已經瞭解了webpack是什麼以及怎麼用,下面我們來學習該如何進行webpack的配置

6.1入口和出口

我們考慮一下,如果每次使用webpack的命令都需要寫上入口和出口作為引數,就非常麻煩,有沒有一種方法可以將這兩個引數寫到配置中,在執行時,直接讀取呢?當然可以,就是建立一個webpack.config.js檔案,內容如下

const path = require('path');

module.exports = {
	//入口:可以是字串、陣列、物件
	entry: './src/main.js',

	//出口:通常是一個物件,裡面至少包含兩個屬性,path和filename
	output: {
		path: path.resolve(__dirname, 'dist'), //注意 path通常是一個決定路徑
		filename: 'bundle.js'
	}
};

這時我們在終端中只需要輸入webpakc即可打包

image-20200722061120079

6.2區域性安裝webpack

目前,我們使用的webpack是全域性的webpack,如果我們想使用區域性來打包呢?因為一個專案往往依賴特定的webpack版本,全域性的版本可能很這個專案的webpack版本不一致,匯出打包出現問題。所以通常一個專案,都有自己區域性的webpack。第一步,專案中需要安裝自己區域性的webpack。這裡我們讓區域性安裝webpack3.6.0,Vue CLI3中已經升級到webpack4,但是它將配置檔案隱藏了起來,所以檢視起來不是很方便。

安裝命令:(注意,一定先進入你的專案路徑下,然後輸入命令)

npm install webpack@3.6.0 --save-dev

image-20200722061507298

6.3package.json中定義啟動

剛才在上一步我們已經安裝好區域性webpack,那麼我們該如何使用區域性webpack進行打包呢?如果你此時在命令列中輸入webpack命令,那麼依然是使用全域性的webpack,因此我們還需要對此進行配置

首先我們通過npm init生成package.json,

image-20200722062204032

{
  "name": "day04",
  "version": "1.0.0",
  "description": "package.json test",
  "main": "webpack.config.js",
  "dependencies": {
    "webpack": "^3.6.0"
  },
  "devDependencies": {},
  "scripts": {
	"build": "webpack"  //加上這一句,當我們執行npm run build時它會去我們區域性的webpack中去尋找命令,如果找不到再去全域性尋找
  },
  "author": "wugongzi",
  "license": "ISC"
}

生成好package.json後我們可以使用npm run build來打包我們的專案。當我們執行npm run build時它首先會去我們區域性的webpack中去尋找命令,如果找不到再去全域性尋找

7.loader

loader是webpack中一個非常核心的概念。webpack用來做什麼呢?在我們之前的例項中,我們主要是用webpack來處理我們寫的js程式碼,並且webpack會自動處理js之間相關的依賴。但是,在開發中我們不僅僅有基本的js程式碼處理,我們也需要載入css、圖片,也包括一些高階的將ES6轉成ES5程式碼,將TypeScript轉成ES5程式碼,將scss、less轉成css,將.jsx、.vue檔案轉成js檔案等等。對於webpack本身的能力來說,對於這些轉化是不支援的。那怎麼辦呢?給webpack擴充套件對應的loader就可以啦。

loader使用過程:

  • 步驟一:通過npm安裝需要使用的loader

  • 步驟二:在webpack.config.js中的modules關鍵字下進行配置

大部分loader我們都可以在webpack的官網中找到,並且學習對應的用法。

7.1CSS loader

專案開發過程中,我們必然需要新增很多的樣式,而樣式我們往往寫到一個單獨的檔案中。在src目錄中,建立一個css檔案,其中建立一個normal.css檔案。我們也可以重新組織檔案的目錄結構,將零散的js檔案放在一個js資料夾中。normal.css中的程式碼非常簡單,就是將body設定為red但是,這個時候normal.css中的樣式會生效嗎?當然不會,因為我們壓根就沒有引用它。webpack也不可能找到它,因為我們只有一個入口,webpack會從入口開始查詢其他依賴的檔案。

normal.css

body {
	background-color: red;
}

在main.js中引入

//引入CSS檔案
require('./css/normal.css')

image-20200722063401552

重新打包,出現如下錯誤

image-20200722064111412

這個錯誤告訴我們:載入normal.css檔案必須有對應的loader。

這時,我們開啟webpack的官網https://www.webpackjs.com/loaders/css-loader/,找到對應的css-loader的配置。loader的配置按照官網的要求來就可以,安裝好對應的loader後,我們需要在webpack.config.js中加入相應的配置

const path = require('path');

module.exports = {
	//入口:可以是字串、陣列、物件
	entry: './src/main.js',

	//出口:通常是一個物件,裡面至少包含兩個屬性,path和filename
	output: {
		path: path.resolve(__dirname, 'dist'), //注意 path通常是一個決定路徑
		filename: 'bundle.js'
	},
    
     
	module: {
        //引入 css配置 
		rules: [{
			test: /\.css$/,
			use: ['style-loader', 'css-loader']
		}]
	}
};

注意:使用css-loader前必須引入style-loader,不然在頁面看不到效果

7.2less loader

如果我們希望在專案中使用less、scss、stylus來寫樣式,webpack是否可以幫助我們處理呢?我們這裡以less為例,其他也是一樣的。我們還是先建立一個less檔案,依然放在css資料夾中

image-20200722064712866

image-20200722064744056

繼續在官方中查詢,我們會找到less-loader相關的使用說明。首先,還是需要安裝對應的loader。注意:我們這裡還安裝了less,因為webpack會使用less對less檔案進行編譯。其次,修改對應的配置檔案,新增一個rules選項,用於處理.less檔案

npm install --save-dev less-loader less

image-20200722065141726

7.3圖片檔案處理

首先,我們在專案中加入兩張圖片:一張較小的圖片test01.jpg(小於8kb),一張較大的圖片test02.jpeg(大於8kb),待會兒我們會針對這兩張圖片進行不同的處理

我們先考慮在css樣式中引用圖片的情況,所以我更改了normal.css中的樣式:

image-20200722065057716

如果我們現在直接打包,會出現如下問題

image-20200722065119797

圖片處理,我們使用url-loader來處理,依然先安裝url-loader

npm install --save-dev url-loader

修改webpack-config.js

image-20200722065306543

再次打包,執行index.html,就會發現我們的背景圖片選出了出來。而仔細觀察,你會發現背景圖是通過base64顯示出來的。OK,這也是limit屬性的作用,當圖片小於8kb時,對圖片進行base64編碼

image-20200722065354896

那麼問題來了,如果圖片大於8kb呢?我們將background的圖片改成test02.jpg,這次因為大於8kb的圖片,會通過file-loader進行處理,但是我們的專案中並沒有file-loader

image-20200722065456563

所以我們需要安裝file-loader

npm install --save-dev file-loader

再次打包,就會發現dist資料夾下多了一個圖片檔案

image-20200722065545125

7.4圖片檔案修改名稱

我們發現webpack自動幫助我們生成一個非常長的名字,這是一個32位hash值,目的是防止名字重複。但是,真實開發中,我們可能對打包的圖片名字有一定的要求,比如,將所有的圖片放在一個資料夾中,跟上圖片原來的名稱,同時也要防止重複。所以,我們可以在options中新增上如下選項:

  • img:檔案要打包到的資料夾

  • name:獲取圖片原來的名字,放在該位置

  • hash:8:為了防止圖片名稱衝突,依然使用hash,但是我們只保留8位

  • ext:使用圖片原來的副檔名

image-20200722065730458

但是,我們發現圖片並沒有顯示出來,這是因為圖片使用的路徑不正確,預設情況下,webpack會將生成的路徑直接返回給使用者。但是,我們整個程式是打包在dist資料夾下的,所以這裡我們需要在路徑下再新增一個dist/

image-20200722065746205

7.5ES6語法處理

如果你仔細閱讀webpack打包的js檔案,發現寫的ES6語法並沒有轉成ES5,那麼就意味著可能一些對ES6還不支援的瀏覽器沒有辦法很好的執行我們的程式碼。在前面我們說過,如果希望將ES6的語法轉成ES5,那麼就需要使用babel。而在webpack中,我們直接使用babel對應的loader就可以了。

npm install --save-dev babel-loader@7 babel-core babel-preset-es2015

配置webpack.config.js檔案

image-20200722065943614

重新打包,檢視bundle.js檔案,發現其中的內容變成了ES5的語法

相關文章