第1章 簡介
何為webpack:
Webpack是一個開元的JS模塊打包工具,其最核心的功能是解決模塊之間的依賴,把各個模塊按照特定的規則和順序組織在一起,最終合並為一個JS文件,這個過程就叫做模塊打包。
為什么需要webpack:
應用規模大了以后,必須借助一定的工具,否則人工維護代碼的成本將逐漸變得難以承受,學會使用工具可以讓開發效率成倍的提升。
何為模塊:
在設計程序結構時,把所有代碼都堆到一起是非常糟糕的做法。更好的組織方式按照特定的功能將其拆分為多個代碼段,每個代碼段實現一個特定的目的。你可以對其進行獨立的設計、開發和測試,最終通過接口來將它們組合到一起,這就是基本的模塊化思想。
引入多個js文件到頁面中的缺點:
①需要手動維護js的加載順序。頁面的多個script之間通常會有依賴關系,但由於這種依賴關系是隱式的,除了添加注釋以外很難清晰地指明誰依賴了誰,這樣當加載文件過多的時候就會出現問題。
②每一個script標簽,都意味着需要向服務器請求一次靜態資源,在HTTP2還沒出現的時期,建立連接的成本是很高的,過多的請求會嚴重拖慢網頁的渲染速度。
③每個script標簽中,全局作用域,如果沒有進行任何處理而直接在代碼中進行變量或者函數聲明,就會造成全局作用域污染。
模塊化則解決了上述的所有問題:
①通過導入和導出與我們可以清晰模塊之間的依賴關系。
②模塊可以借助工具來打包,在頁面上只需要加載整合后的資源文件,減少了網絡開銷。
③多個模塊之間的作用域是隔離的彼此不會有命名沖突。
09年開始js社區開始進行模塊化嘗試,並依次出現了AMD、CommonJS、CMD等解決方案。但這些都是社區提出的,並不能算語言本身的特性。而在2015年。ES6正式定義了模塊標准,這門語言在誕生20年之后終於有人模塊這一概念。
ES6模塊標目前已經得到了大多數現代瀏覽器的支持,但在實際應用方面還需要一段時間,有以下原因:
①無法使用 code splitting 和 tree shaking(webpack的兩個重要特性)
②大多數npm模塊還是CommonJS的形式,而瀏覽器並不支持其語法,因此這些包沒有辦法直接使用。
③仍需考慮個別瀏覽器及平台的兼容性問題。
模塊打包工具的兩種工作方式:
①將存在依賴關系的模塊按照特定規則合並為單個JS文件,一次全部加載進頁面中。
②在頁面初始時加載一個入口模塊,其他模塊異步地進行加載。
目前社區中比較流行的打包模塊有Webpack,Parcel,Rollup等。
為什么選擇webpack?對比同類模塊打包工具,webpack具備哪些優勢?
①Webpack默認支持多種模塊標准,包括AMD、CommonJS,以及最新的ES6模塊,而其他工具大多數只能支持一到兩種。這對於使用多種模塊標准的工程非常有用,Webpack會幫助我們處理不同類型模塊之間的依賴關系。
②Webpack有完備的代碼分割解決方案。它可以分割打包后的資源,首屏只加載必要的部分,不太重要的功能放到后面動態地加載。這對於資源體積較大的應用來說尤為重要,可以有效地減小資源體積,提升首頁渲染速度。
③Webpack可以處理各種類型的資源。除了js外,webpack還可以處理樣式,模板,圖片等,而開發者需要做的僅僅是導入它們。比如你可以從js文件導入一個CSS或者PNG,而這一切最終都可以由loader來處理。
④Webpack擁有龐大的社區支持。除了webpack核心庫以外,還有無數開發者為它編寫周邊的插件和工具,絕大多數需求都可以找到已有解決方案。
安裝:
webpack,對操作系統沒有要求,唯一的依賴就是Node.js
webpack對node版本是有一定要求的,推薦使用LTS版本。LTS版本是node在當前階段較為穩定的版本。該版本中不會包含太多激進的特性。
安裝好node,使用 Node.js 的包管理器 npm 來安裝 Webpack,安裝模塊方式有兩種:全局安裝,本地安裝。
兩種安裝方式利弊及其特點:
全局安裝的好處是,npm會幫我們綁定一個命令行環境變量,一次安裝處處運行;本地安裝則會添加其成為項目中的依賴,只能在項目內部使用。
建議本地安裝,有以下原因:
①如果采用全局安裝,那么在與他人進行項目協作的時候,由於每個人系統中webpack版本不同,可能會導致輸出結果不一樣。
②部分依賴於webpack的插件會調用項目中webpack的內部模塊,這種情況下仍需要進行本地安裝,而全局本地都有,則容易造成混淆。
npx webpack --entry=./index.js --output-filename=bundle.js -mode=development
第一個參數:entry 是資源打包的入口。webpack從這里開始進行模塊依賴的查找,的到項目中的兩個js模塊,並通過它們來生成最終產物。
第二個參數:output-filenam 是輸出資源名。打包后生成的dist目錄下,包含一個bundle.js就是webpack的打包結果。
最后的參數:mode 指的是打包模式。Webpack為開發者提供了 development、production、none三種模式,除了none模式都會自動添加適合當前模式的一系列配置,為了減少工作量,開發中選擇development模式即可。
scripts 是 npm 提供的腳本命令功能,在這里可以直接使用由模塊添加的命令。(比如 webpack 取代之前的 npx webpack)
使用默認的目錄配置:
工程源代碼放在 /src 中,
輸出資源放在 /dist 中。
對於資源輸出目錄來說 webpack ,默認是 /dist ,我們不需要做任何改動。
同時webpack默認源代碼入口就是 src/index.js , 因此按照此目錄順序,可以省略掉 entry 的配置了。
雖然目錄命名並不是強制的,但還是建議遵循統一命名規范,這樣會使得大體結構比較清晰,也利於多人協作。
1.4.4 使用配置文件
通過 module.exports 導出一個對象,也就是打包時被webpack接收的配置對象。先前在命令行中輸入的一大串參數就都要改為 key-value 的形式放在這個對象下。
目前該對象包含兩個關於資源輸入資源輸出的屬性——entry 和 output 。
entry就是我們的資源入口,output則是一個包含更多詳細配置的對象。
之前的參數 --output-filename 和 --output-path 現在都成為了 output 下面的屬性。filename,和先前一樣都是bundle.js,不需要改動,而path和之前是有所區別的,webpack 對於 output.path 的要求是使用絕對路徑(從系統根目錄開始的完整路徑),之前命令行中為了簡潔都是相對路徑。
而在webpack.config.js 中,我們通過調用node.js的路徑拼裝函數——path.join,將_dirname (Node.js 內置的全局變量,值為當前文件所在的絕對路徑)與dist(輸出目錄)連接起來,得到最終的輸出目錄。
1.4.5 webpack-dev-server
安裝指令中的--save-dev參數是將webpack-dev-server作為工程的devDependencies(開發環境依賴)記錄在package.json中。
這樣做是因為webpack-dev-server僅僅在本地開發環境中才用到。
假如工程上線時要進行依賴安裝,就可以通過 npm install --production 過濾掉 devDependencies 中的冗余模塊,從而加快安裝和發布的速度。
為了便捷地啟動 webpack-dev-server,我們再package.json中添加一個dev指令:
然后還需要對 " webpack-dev-server " 進行配置。編輯webpack.config.js 如下:
我們在配置中添加了一個 devServer 對象,它是專門用來放 webpack,dev-server 配置的。webpack-dev-server 可以看做是一個服務者,它的主要工作就是接收瀏覽器請求,然后將資源返回。當服務啟動時,會先讓 Webpack 進行模塊打包並將資源准備好(在示例中就是bundle.js)。
當 webpack-dev-server 接收到瀏覽器的資源請求時。它會首先進行 URL 地址校驗。如果該地址是資源服務地址(上面配置的publicPath),就會從 Webpack 的打包結果中尋找該資源並返回瀏覽器。反之,如果請求不屬於資源服務地址,則直接讀取硬盤中的源文件並將其返回。
綜上,總結出 webpack-dev-server 的兩大職能:
①令Webpack進行模塊打包,並處理打包結果的資源請求。
②作為普通的 Web Server ,處理靜態資源文件請求。
webpack-dev-server,不是像直接用webpack開發那樣每次都會生成bundle.js ,而 webpack-dev-server 只是將打包結果放在內存中,並不會寫入實際的bundle.js ,在每次 webpack-dev-server 接收到請求時都只是將內存中的打包結果返回給瀏覽器。
webpack-dev-server 還有一項很便捷的特性就是 live-reloading(自動刷新)。
當webpack-dev-server發現工程源文件進行了更新操作就會自動刷新瀏覽器,顯示更新后的內容。
之后會講到,hot-module-replacement(模塊熱替換),我們始終不需要刷新瀏覽器就能獲取到更新之后的內容。
1.5 小結
webpack的功能,它可以處理模塊之間的依賴,將它們串聯起來合並為單一的JS文件。
安裝webpack一般選擇本地安裝,這樣可以使團隊開發時共用一個版本,並且可以讓其他插件直接獲取webpack的內部模塊。
配置本地開發環境可以借助 npm scripts 來維護命令行腳本,當打包腳本參數過多時,我們需要將其轉換為 webpack.config.js ,用文件的方式維護復雜的 webpack 配置。
webpack-dev-server 的作用啟動一個本地服務,可以處理打包資源與靜態資源的請求。它的 live-reloading 功能可以監聽文件變化,自動刷新界面提高開發效率。
第2章 模塊打包
CommonJS
導出是一個模塊向外暴露自身的唯一方式。CommonJS中,通過 module.exports 進行導出
導入,CommonJS中使用 require 進行模塊導入
module 對象用來存放其信息,其 loaded 屬性用於記錄該模塊是否被加載過。第一次被加載和執行后會被置為 true ,后面再次加載時檢查到 module.loaded 為 true ,則不會再執行模塊代碼了。
ES6 Module
ES6 Module 會自動采取嚴格模式 (所以要將未開啟嚴格模式的代碼轉換為ESM要注意此點)
使用 export 命令導出模塊,命名導出 和 默認導出
命名導出可以用as關鍵字改變名字,導入導出的時候都可以。
默認導出就是 export default ,可以理解為 對外輸出了一個名為 default 的變量。
使用 import 語法導入模塊。
導入多個變量可以用 import * as <myMoudle> 把所有導入的變量作為屬性值添加到 <myModule>
默認導出 import 后面的名字可以自由指定,它指代了導出文件默認導出的值。
CommonJS 和 ES6 Module 的區別?
commonJS 對模塊依賴的解決是動態的,依賴建立在代碼發生階段。require 指定路徑可以動態指定。
ES6Module 對模塊依賴關系的建立是在代碼的編譯階段。聲明式的導入、導出語句,不支持導入路徑是表達式。
ESM相對於CommonJS的優點:
①死代碼檢測和排除。可以用靜態分析工具檢測出那些模塊沒有被調用過,打包的時候可以去除,減小資源體積。
②模塊變量類型檢查。js屬於動態類型語言,不會在代碼執行前檢查類型錯誤。ESM的靜態模塊結構有助於確保模塊之間傳遞的值和接口類型是正確的。
③編譯器優化。在CommonJS等動態模塊系統中,無論怎么導入都是一個對象,但是esm可以支持直接導入變量,減少了引用層級,程序效率更高。
值拷貝與動態映射
在導入模塊時,
CommonJS獲取的是一份導出值的拷貝;
ES6Module中則是動態映射。
在產生循環依賴的時候CommonJS會輸出{}空對象,ESModule會輸出undefined。
但是因為ESModule為動態映射,如果我們保證當導入的值被使用時已經設置好正確的導出值,就可以解決循環依賴產生的問題。
AMD標准:
define函數來定義,同步加載模塊標准語法更加冗長,異步加載方式比較混亂,容易造成回調地獄,已經很少使用了。
UMD:
是一組模塊形式的集合,UMD一般先判斷AMD環境,也就是檢查全局環境下是否有define函數。而通過AMD定義的模式是不支持CommonJS和ESM的,使用webpack的時候可以更改下UMD的判斷順序。
加載npm模塊:
與其它語言相比,js缺乏標准庫。
當開發者需要解析URL,日期解析等常見問題的時候,只能自己封裝,
npm包管理器為開發者帶來便捷,npm 可以讓開發者在其它平台上找到由他人開發和發布的庫。
很多語言都有包管理器,比如 JAVA 的 maven , Ruby 的 gem。
目前JavaScript的有兩個主流包管理器,npm 和 yarn 。
每一個npm模塊都有一個入口。當我們加載一個模塊時,實際上就是加載該模塊的入口文件。這個入口被維護在模塊內部 package.json 文件的 main 字段中。
當加載模塊時,實際上加載的是 node_module/lodash/lodash.js
除了直接加載外,也可以通過 <package_name>/<path> 的形式單獨加載模塊內部的某個JS文件。
import all from "lodash/fp/all.js"
這樣webpack在打包的時候,也只是打包這一個引入文件,不會打包全部的lodash庫,可以減小打包資源的體積。
模塊打包原理:
bundle在瀏覽器上運行:
①最外層匿名函數會初始化瀏覽器執行環境,包括定義 installedModules對象、_wepack_require_ 函數等,為模塊加載和執行做准備工作。
②加載入口模塊,每個bundle.js 都只有一個入口模塊
③執行模塊代碼,如果執行到了 module.exports 則記錄下模塊導出值;如果遇到 require 函數(_webpack_require_),則會暫時交出執行權。進入 _webpack_require_ 內加載其它模塊的邏輯。
④_webpack_require_ 內會判斷即將加載的模塊是或否存在於 installedModule 中。存在直接取值,否則回到第3步,執行該模塊的代碼獲取導出值。
⑤所有依賴加載完畢,執行權回到入口模塊。
第三步和第四部是一個遞歸的過程。webpack為每個模塊創造了一個可以導出和導入的環境,本質上沒有修改代碼的執行邏輯,因此代碼執行順序和模塊加載順序是完全一致的,這就是webpack打包的奧秘。
小結:
CommonJS 和 ESModule 主要區別在於:
前者建立模塊依賴關系是在運行時,后者是編譯時。
導入方面,CommonJS 是值拷貝,ESModule導入的是只讀的變量映射。
esm通過其靜態特性可以進行編譯過程中的優化,並且具備處理循環依賴的能力。
第3章 資源輸入輸出
entry 入口 -> 進入 各個module 形成一個 chunk ,由 chunk -> 打包得到 bundle
工程中可以定義多個入口,每個入口都會產生一個結果資源。所以,entry 與 bundle 存在着對應關系。
某些特殊情況,一個入口也可能產生多個chunk,並最終生成多個bundle。
配置資源入口:
webpack 通過 cotext 和 entry 這兩個配置項共同決定入口文件路徑。
配置入口實際做了兩件事:
確定入口模塊位置,告訴webpack從哪里開始進行打包。
定義 chunk name 。如果工程只有一個入口,那么默認其 chunk name 為 “main” ;如果工程有多個入口,需要為每個入口定義 chunk name , 來作為該 chunk 的唯一標識。
context 可以理解為資源入口的路徑前綴,在配置時要求必須使用絕對路徑的形式。
//二者效果相同 module.exports = { context:path.join(_dirname,'./src'), entry:'./scripts/index.js', } module.exports = { context:path.join(_dirname,'./src/scripts'), entry:'./index.js', }
配置 context 的主要目的是讓 entry 的編寫更加簡潔,尤其是在多入口的情況下。context 可以省略,默認值為當前工程的根目錄。
entry 與context只能為字符串不同,entry的配置可以是多種形式的:字符串、數組、對象、函數。可以根據不同的需求場景來選擇。
字符串類型: module.exports = { entry:'./src/index.js', output:{ filename:'dundle.js' }, mode:'development' , } 數組類型入口: 傳入一個數組的作用是將多個資源預先合並,在打包時 webpack 會將數組中的最后一個元素作為實際入口路徑。如: module.exports = { entry:['babel-polyfill','./src/idex.js'], } 上面的配置等同於: //webpack.config.js module.exports = { entry:'./src/index.js' } //index.js import 'babel-polyfill'; 對象類型入口: 如果想要定義多入口,則必須使用對象形式。對象的屬性名(key)是 chunk name ,屬性值(value)是入口路徑。如: module.exports = { entry:{ // chunk name 為 index,入口路徑為 ./src/index.js index:'./src/index.js', // chunk name 為 lib,入口路徑為 ./src/lib.js lib:'./src/lib.js' } } 函數類型入口: 用函數定義時,只需要返回上面任意形式即可。 module.exports = { entry:()=>'./src/index.js', } 傳入函數的優點在於我們可以在函數體內添加一些動態邏輯來獲取工程的入口。另外,函數也支持返回一個 Promise 對象來進行異步操作。
3.2.3 實例
單頁應用:對於SPA來說,一般定義單一入口即可
module.exports = { entry:'./src/app.js', }
配置資源出口:
module.export = { entry:'./src/app.js', output:{ filename:'./js/bundle.js',
path:'../'
} }
filename不僅是名字,還是webpack生成的相對路徑,即使沒有webpack也會生成。path,可以單獨配置導出的位置,默認dist。
filename模板變量配置:
[name]:指定chunk name
[hash]:指代此次打包所有資源的hash
[chunkhush]:指代當前chunk內容的hash
[id]:指代當前chunk的id
[query]:指代filename配置項中的query
實際工程中,使用的較多的是[name],它與chunk是一一對應的關系,且可讀性較高。如果要控制客戶端緩存,最好還是加上[chunkhash],每個chunk產生的[chunkhash]只與自身內容有關,單個chunk內容的改變不會影響其它資源,可以最精確的讓客戶端得到緩存。
output:{ filename:'[name]@[chunkhash].js' }
3.3.3 publicPath 指定資源的請求位置:
html、host、CDN
webpack-dev-serve 也有publicpath配置,指定的是靜態資源服務路徑。
module.export = { entry:'./src/app.js', output:{ filename:'./js/bundle.js', path:'../' }, devServer:{ publicPath:"./assets", port:3000 } }
本章小結:
本章介紹了資源輸入輸出流程,以及相關配置項context、entry、output。
在配置打包入口時,context相當於路徑前綴,entry是入口文件路徑。單入口的chunk name不可更改,多入口的話必須為每一個chunk指定chunk name。
當第三方依賴較多時,我們可以用提取 vendor 的方法將這些模塊打包到一個單獨的bundle中,以更有效地利用客戶端緩存,加快頁面渲染速度。
path和publicPath的區別在於path指定的資源是輸出位置,而publicPath指定的是間接資源的請求位置。
第4章 預處理器
loader :的字面意思是裝載器,在webpack中它的實際功能則更像是預處理器。webpack本身只識別js,對於其它資源必須先定義一個或者多個loader進行轉譯,輸出為webpack能接收的形式再繼續進行,因此loader實際做的是一個預處理的工作。
模塊具有高內聚性和可復用性結構,通過“webpack”一切皆“模塊”思想,我們可以將模塊的這些特征應用到每一種靜態資源上,從而設計和實驗出更加健壯的系統。
loader 本身就是一個函數,在該函數中對接收到的內容進行轉換,然后返回轉換后的結果(可能包含source map 和 AST對象)。
module.exports = function loader (content,map,meta){ var callback = this.async(); var result = handler(content,map,meta); callback( null, //error result.content, //轉化后的內容 result.map, //轉化后的 source-map result.meta //轉化后的AST ) }
loader 都是一些第三方npm模塊,webpack本身不包含任何loader,一次使用loader第一步就是從npm安裝它。 npm install XX-loader
將loader引入工程的具體配置:
module.exports = { //... module:{ rules:[{ test:/\.xx$/, use:['XX-loader'], }] } } //loader的相關配置都在對象module中, //其中module.rules代表模塊處理規則, //每條規則可以包含很多配置項,這里我們只使用了最重要的兩項。 //test:可接收一個正則表達式,或者元素為恆澤表達式的數組,只有正則配上的模塊才會使用這條規則。 //use:可接收一個數組,數組包含該規則所使用的loader。