Webpack前世今生
在正式介紹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 [email protected] --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 [email protected] --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的官網//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的語法