在正式介紹Webpack之前,先給大家說明一下前端為什麼需要模組化
1.為什麼需要模組化
1.1JS原始功能
在網頁開發的早期,js製作作為一種指令碼語言,做一些簡單的表單驗證或動畫實現等,那個時候程式碼還是很少的。那個時候的程式碼是怎麼寫的呢?直接將程式碼寫在<script>
標籤中即可。隨著ajax非同步請求的出現,慢慢形成了前後端的分離,客戶端需要完成的事情越來越多,程式碼量也是與日俱增。為了應對程式碼量的劇增,我們通常會將程式碼組織在多個js檔案中,進行維護。但是這種維護方式,依然不能避免一些災難性的問題。比如全域性變數同名問題,看下面的例子:
小明後來發現程式碼不能正常執行,去檢查自己的變數,發現確實true,最後杯具發生了,小明加班到2點還是沒有找到問題出在哪裡(所以,某些加班真的是無意義的)
另外,這種程式碼的編寫方式對js檔案的依賴順序幾乎是強制性的,但是當js檔案過多,比如有幾十個的時候,弄清楚它們的順序是一件比較同時的事情。而且即使你弄清楚順序了,也不能避免上面出現的這種尷尬問題的發生。
1.2匿名函式解決方案
我們可以使用匿名函式來解決方面的重名問題在aaa.js檔案中,我們使用匿名函式
(function(){
var flag=true
})()
但是如果我們希望在main.js檔案中,用到flag,應該如何處理呢?顯然,另外一個檔案中不容易使用,因為flag是一個區域性變數。
1.3使用模組作為出口
我們可以使用將需要暴露到外面的變數,使用一個模組作為出口,什麼意思呢?來看下對應的程式碼:
我們做了什麼事情呢?非常簡單,在匿名函式內部,定義一個物件。給物件新增各種需要暴露到外面的屬性和方法(不需要暴露的直接定義即可)。最後將這個物件返回,並且在外面使用了一個MoudleA接受。接下來,我們在man.js中怎麼使用呢?我們只需要使用屬於自己模組的屬性和方法即可。這就是模組最基礎的封裝,事實上模組的封裝還有很多高階的話題,但是我們這裡就是要認識一下為什麼需要模組,以及模組的原始雛形。幸運的是,前端模組化開發已經有了很多既有的規範,以及對應的實現方案。常見的模組化規範CommonJS、AMD、CMD,也有ES6的Modules
1.4CommonJS(瞭解)
模組化有兩個核心:匯出和匯入
CommonJS的匯出:
CommonJS的匯入:
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也可以幫助我們完成,它們有什麼不同呢?
3.和grunt/gulp的對比
grunt/gulp的核心是Task,我們可以配置一系列的task,並且定義task要處理的事務(例如ES6、ts轉化,圖片壓縮,scss轉成css),之後讓grunt/gulp來依次執行這些task,而且讓整個流程自動化。所以grunt/gulp也被稱為前端自動化任務管理工具。我們來看一個gulp的task:
上面的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建立檔案
我們建立如下檔案和資料夾:
-
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這個命令
打包後會在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便可在控制檯看到輸出效果
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即可打包
6.2區域性安裝webpack
目前,我們使用的webpack是全域性的webpack,如果我們想使用區域性來打包呢?因為一個專案往往依賴特定的webpack版本,全域性的版本可能很這個專案的webpack版本不一致,匯出打包出現問題。所以通常一個專案,都有自己區域性的webpack。第一步,專案中需要安裝自己區域性的webpack。這裡我們讓區域性安裝webpack3.6.0,Vue CLI3中已經升級到webpack4,但是它將配置檔案隱藏了起來,所以檢視起來不是很方便。
安裝命令:(注意,一定先進入你的專案路徑下,然後輸入命令)
npm install webpack@3.6.0 --save-dev
6.3package.json中定義啟動
剛才在上一步我們已經安裝好區域性webpack,那麼我們該如何使用區域性webpack進行打包呢?如果你此時在命令列中輸入webpack命令,那麼依然是使用全域性的webpack,因此我們還需要對此進行配置
首先我們通過npm init
生成package.json,
{
"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')
重新打包,出現如下錯誤
這個錯誤告訴我們:載入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資料夾中
繼續在官方中查詢,我們會找到less-loader相關的使用說明。首先,還是需要安裝對應的loader。注意:我們這裡還安裝了less,因為webpack會使用less對less檔案進行編譯。其次,修改對應的配置檔案,新增一個rules選項,用於處理.less檔案
npm install --save-dev less-loader less
7.3圖片檔案處理
首先,我們在專案中加入兩張圖片:一張較小的圖片test01.jpg(小於8kb),一張較大的圖片test02.jpeg(大於8kb),待會兒我們會針對這兩張圖片進行不同的處理
我們先考慮在css樣式中引用圖片的情況,所以我更改了normal.css中的樣式:
如果我們現在直接打包,會出現如下問題
圖片處理,我們使用url-loader來處理,依然先安裝url-loader
npm install --save-dev url-loader
修改webpack-config.js
再次打包,執行index.html,就會發現我們的背景圖片選出了出來。而仔細觀察,你會發現背景圖是通過base64顯示出來的。OK,這也是limit屬性的作用,當圖片小於8kb時,對圖片進行base64編碼
那麼問題來了,如果圖片大於8kb呢?我們將background的圖片改成test02.jpg,這次因為大於8kb的圖片,會通過file-loader進行處理,但是我們的專案中並沒有file-loader
所以我們需要安裝file-loader
npm install --save-dev file-loader
再次打包,就會發現dist資料夾下多了一個圖片檔案
7.4圖片檔案修改名稱
我們發現webpack自動幫助我們生成一個非常長的名字,這是一個32位hash值,目的是防止名字重複。但是,真實開發中,我們可能對打包的圖片名字有一定的要求,比如,將所有的圖片放在一個資料夾中,跟上圖片原來的名稱,同時也要防止重複。所以,我們可以在options中新增上如下選項:
-
img:檔案要打包到的資料夾
-
name:獲取圖片原來的名字,放在該位置
-
hash:8:為了防止圖片名稱衝突,依然使用hash,但是我們只保留8位
-
ext:使用圖片原來的副檔名
但是,我們發現圖片並沒有顯示出來,這是因為圖片使用的路徑不正確,預設情況下,webpack會將生成的路徑直接返回給使用者。但是,我們整個程式是打包在dist資料夾下的,所以這裡我們需要在路徑下再新增一個dist/
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檔案
重新打包,檢視bundle.js檔案,發現其中的內容變成了ES5的語法