Part2 · 前端工程化實戰 · 模塊化開發與規范化標準
一、模塊化演變程序
模塊化概述:
模塊化開發為當前最重要的前端開發范式之一,隨著前端代碼的日益復雜,的前端專案代碼出現了不得不花費大量時間去整理,而模塊化就是最主流的代碼組織方式,它通過把復雜的代碼通過功能不同劃分為不同的模塊,以單獨維護的方式,提高開發效率,降低維護成本,【模塊化】僅僅為一個思想,并沒有提供具體的實作,
1.stage1 基于檔案劃分
將每一個模塊獨立為一個檔案,在頁面中引入這些檔案(web中最原始的模塊化系統),
具體做法就是將每個功能及其相關狀態資料各自單獨放到不同的檔案中,約定每個檔案就是一個獨立的模塊,使用某個模塊就是將這個模塊引入頁面,然后直接呼叫模塊的中的成員(成員/函式)
特點:
- 所有的模塊都直接在全域作業,并沒有私有空間,所有的成員都可以在模塊外部被訪問或者修改;
- 檔案模塊過多時,容易產生命名沖突;
- 無法管理模塊與模塊之間的依賴關系
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Modular evolution stage 1</title>
</head>
<body>
<h1>模塊化演變(第一階段)</h1>
<h2>基于檔案的劃分模塊的方式</h2>
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
// 命名沖突,此處命名沖突問題由于引入模塊順序問題而不同,name值最后指向module-b.js中的name值
method1()
// 模塊成員可以被修改,外部可以任意修改模塊內部成員
name = 'foo'
</script>
</body>
</html>
module-a.js
// module a 相關狀態資料和功能函式
var name = 'module-a'
function method1 () {
console.log(name + '#method1')
}
function method2 () {
console.log(name + '#method2')
}
module-b.js
// module b 相關狀態資料和功能函式
var name = 'module-b'
function method1 () {
console.log(name + '#method1')
}
function method2 () {
console.log(name + '#method2')
}
2.stage2 命名空間方式
命名空間方式每個模塊只暴露一個全域物件,所有模塊成員都掛載到這個物件中,具體做法就是在第一階段的額基礎上,通過將每個模塊【包裹】為一個全域物件的形式實作,有點類似于為模塊內的成員添加了【命名空間】的感覺,
特點:
- 通過【命名空間】的方式減少了命名沖的可能
- 同樣沒有私有空間,所有模塊成員也可以在模塊外部被訪問或者修改
- 同樣無法管理模塊之間的依賴關系
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Modular evolution stage 2</title>
</head>
<body>
<h1>模塊化演變(第二階段)</h1>
<h2>每個模塊只暴露一個全域物件,所有模塊成員都掛載到這個物件中</h2>
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
moduleA.method1()
moduleB.method1()
// 模塊成員可以被修改
moduleA.name = 'foo'
</script>
</body>
</html>
module-a.js
// module a 相關狀態資料和功能函式
var moduleA = {
name: 'module-a',
method1: function () {
console.log(this.name + '#method1')
},
method2: function () {
console.log(this.name + '#method2')
}
}
module-b.js
// module b 相關狀態資料和功能函式
var moduleB = {
name: 'module-b',
method1: function () {
console.log(this.name + '#method1')
},
method2: function () {
console.log(this.name + '#method2')
}
}
3.stage3 IIFE
使用立即執行函式運算式IIFE(Immediately-Invoked Function Expression)為模塊提供私有空間,具體做法就是將每個模塊成員都放在一個函式提供的私有作用域中,對于需要寶庫給外部的成員,通過掛載全域物件的方式實作,
特點:
有了私有成員的概念,私有成員只能在模塊成員內通過必報的形式訪問,
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Modular evolution stage 3</title>
</head>
<body>
<h1>模塊化演變(第三階段)</h1>
<h2>使用立即執行函式運算式(IIFE:Immediately-Invoked Function Expression)為模塊提供私有空間</h2>
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
moduleA.method1()
moduleB.method1()
// 模塊私有成員無法訪問
console.log(moduleA.name) // => undefined
</script>
</body>
</html>
module-a.js
// module a 相關狀態資料和功能函式
;(function () {
var name = 'module-a'
function method1 () {
console.log(name + '#method1')
}
function method2 () {
console.log(name + '#method2')
}
window.moduleA = {
method1: method1,
method2: method2
}
})()
module-b.js
// module b 相關狀態資料和功能函式
;(function () {
var name = 'module-b'
function method1 () {
console.log(name + '#method1')
}
function method2 () {
console.log(name + '#method2')
}
window.moduleB = {
method1: method1,
method2: method2
}
})()
4.stage4 IIFE 引數
利用IIFE引數作為依賴宣告使用,具體做法是在第三階段的基礎上,利用立即執行函式的引數傳遞模塊依賴項
特點:
每個模塊之間的關系變得更加明顯
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Modular evolution stage 4</title>
</head>
<body>
<h1>模塊化演變(第四階段)</h1>
<h2>利用 IIFE 引數作為依賴宣告使用</h2>
<p>
具體做法就是在第三階段的基礎上,利用立即執行函式的引數傳遞模塊依賴項,
</p>
<p>
這使得每一個模塊之間的關系變得更加明顯,
</p>
<script src="https://unpkg.com/jquery"></script>
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
moduleA.method1()
moduleB.method1()
</script>
</body>
</html>
module-a.js
// module a 相關狀態資料和功能函式
;(function ($) {
var name = 'module-a'
function method1 () {
console.log(name + '#method1')
$('body').animate({ margin: '200px' })
}
function method2 () {
console.log(name + '#method2')
}
window.moduleA = {
method1: method1,
method2: method2
}
})(jQuery)
module-b.js
// module b 相關狀態資料和功能函式
;(function () {
var name = 'module-b'
function method1 () {
console.log(name + '#method1')
}
function method2 () {
console.log(name + '#method2')
}
window.moduleB = {
method1: method1,
method2: method2
}
})()
5.stage5 模塊化規范的出現
require.js提供AMD模塊化規范以及一個自動模塊化加載器
目錄結構:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Stce4OGs-1610550317435)(https://i.loli.net/2021/01/05/6SVzEnaC8mi2f4Q.png)]
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Modular evolution stage 5</title>
</head>
<body>
<h1>模塊化規范的出現</h1>
<h2>Require.js 提供了 AMD 模塊化規范,以及一個自動化模塊加載器</h2>
<script src="lib/require.js" data-main="main"></script>
</body>
</html>
module1.js
// 因為 jQuery 中定義的是一個名為 jquery 的 AMD 模塊
// 所以使用時必須通過 'jquery' 這個名稱獲取這個模塊
// 但是 jQuery.js 并不在同級目錄下,所以需要指定路徑
define('module1', ['jquery', './module2'], function ($, module2) {
return {
start: function () {
$('body').animate({ margin: '200px' })
module2()
}
}
})
module2.js
// 兼容 CMD 規范(類似 CommonJS 規范)
define(function (require, exports, module) {
// 通過 require 引入依賴
var $ = require('jquery')
// 通過 exports 或者 module.exports 對外暴露成員
module.exports = function () {
console.log('module 2~')
$('body').append('<p>module2</p>')
}
})
main.js
require.config({
paths: {
// 因為 jQuery 中定義的是一個名為 jquery 的 AMD 模塊
// 所以使用時必須通過 'jquery' 這個名稱獲取這個模塊
// 但是 jQuery.js 并不一定在同級目錄下,所以需要指定路徑
jquery: './lib/jquery'
}
})
require(['./modules/module1'], function (module1) {
module1.start()
})
二、模塊化規范
1.CommonJS
在之前的每個階段中,模塊加載的方式都是通過script標簽手動引入,也就是說,之前的方法中,模塊的加載并不受代碼的控制,一旦模塊過多,會出現各種問題,比如HTML忘記引入模塊等,
基于node.js的commonJS規范
- 一個檔案就是一個模塊
- 每個模塊都有單獨的作用域
- 通過module.exports匯出成員
- 通過require函式載入模塊
commonJS是以同步模式加載模塊,node執行機制是在啟動時加載模塊,在執行程序中不需要加載,只會使用,在瀏覽器端使用commonJS規范的話,必然導致效率低下,每次頁面加載都會導致大量的同步模式請求出現,
2.AMD
所以在早期并沒有選擇commonJS規范,而是專門為瀏覽器端且結合瀏覽器特點,重新設計了一個模塊加載規范:AMD(Asynchronous Module Definition)異步模塊定義規范,同時也除了一個require.js庫,其實作了AMD規范,同時本身又是一個強大的模塊加載器,
AMD規范中,require.js庫規定每個模塊使用define關鍵字去定義,可以傳遞2-3個引數,傳遞三個引數的話,第一個引數為該模塊的名字;第二個引數為陣列,宣告該模塊的依賴項,陣列中每個陣列的元素為具體依賴的其它模塊;第三個模塊為一個函式,該函式的引數與第二個引數中的依賴項一一對應,每一項分別為依賴項匯出的成員,函式的作用是為當前的模塊提供一個私有的空間,如果需要在本模塊中向外部模塊匯出一些成員,通過return的方式去實作,
define('module1', ['jquery', './module2'], function () {
return {
start: function () {
$('body').animate({margin: '200px'})
module2()
}
}
})
除此之外,require.js還提供了一個require()函式,該函式用來自動加載模塊,用法與define類似,區別是require只是用來加載模塊,而define是用來定義模塊,require函式去加載一個模塊時,其內部會自動創建一個script標簽,發送對應腳本檔案的請求,并且執行相應的模塊代碼,
require(['module1'], function(module1) {
module1.start()
})
目前絕大多數第三方庫都支持AMD規范,其生態相對較好,但是AMD使用起來比較復雜,除了業務代碼,需要使用define定義模塊以及require()函式去加載模塊,導致代碼復雜程度較高,如果專案中模塊的劃分較為細致時,模塊JS檔案請求頻繁,從而呆滯頁面效率比較低下,所以AMD也只能為前端模塊化規范前進的一步,只是一種妥協的手段,并不是最終的解決方案,
同期淘寶出現了Sea.js + CMD標準,類似commonJS且用法上與require.js大致相同,這種方式在后來也被Require.js兼容,
// CMD規范(類似CommonJS)
define(function (require, exports, module) {
// 通過require引入依賴
var $ = require('jquery');
// 通過exports或者module.exports對外暴露成員
module.exports = function () {
console.log('module-2');
$('body').append('<p>module2</p>')
}
})
3.模塊化標準規范
隨著技術的發展,模塊化 技術實作方式相對以往有了很大的變化,大家在前端模塊化的方式也基本統一,在node.js中遵循CommonJS,在Browser中采用ES Module,
在node.js中,CommonJS為其內置的模塊,正常使用require去匯入模塊,module.exports去匯出模塊,但是在ES Module在browser中就比較復雜一些,ES Module時ECMAScript2015(ES6)中定義的一個最新的模塊系統,它是最近幾年才定義的標準,定義初期,幾乎所有主流瀏覽器都不支持這個特性,隨著webpack等一系列打包工具的流行,這一規范才逐漸開始普及,
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-IkHynp7m-1610550317441)(https://i.loli.net/2021/01/05/kDBAe8RYy2lWS9C.png)]
4.ES Module
4.1基本特性
- 自動采用嚴格模式,忽略’use strict’
- 每個ESM模塊都是單獨的私有作用域
- ESM是通過CORS去請求外部JS模塊的
- ESM的script標簽會延遲執行腳本
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ES Module - 模塊的特性</title>
</head>
<body>
<!-- 通過給 script 添加 type = module 的屬性,就可以以 ES Module 的標準執行其中的 JS 代碼了 -->
<script type="module">
console.log('this is es module')
</script>
<!-- 1. ESM 自動采用嚴格模式,忽略 'use strict' -->
<script type="module">
console.log(this) // undifined
</script>
<!-- 2. 每個 ES Module 都是運行在單獨的私有作用域中 -->
<script type="module">
var foo = 100
console.log(foo)
</script>
<script type="module">
console.log(foo) // foo is not defined
</script>
<!-- 3. ESM 是通過 CORS 的方式請求外部 JS 模塊的 -->
<!-- 所以應用外部js問價需要其CDn支持CORS -->
<!-- <script type="module" src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script> -->
<!-- CORS不支持檔案的方式去訪問,只能通過http的方式去訪問 -->
<!-- 4. ESM 的 script 標簽會延遲執行腳本 defer延遲執行 type="module"與defer屬性功能相同 -->
<!-- <script defer src="demo.js"></script> -->
<script type="module" src="demo.js"></script>
<p>需要顯示的內容</p>
</body>
</html>
4.2匯入和匯出
-
export用法
1.單獨匯出每個成員export var name
module.js
export var name = 'foo module' export function hello () { console.log('hello') } export class Person {}app.js
import { name, hello, Person } from ‘./module.js’
console.log(name) // foo module
hello() // hello
2.模塊尾部統一匯出
module.js
```js
var name = 'foo module'
function hello () {
console.log('hello')
}
class Person {}
export { name, hello, Person }
app.js
import { name, hello, Person } from './module.js'
console.log(name) // foo module
hello() // hello
3.匯出別名
module.js
var name = 'foo module'
function hello () {
console.log('hello')
}
export {
name as fooName,
hello as fooHello
}
app.js
```js
import { fooName, fooHello } from ‘./module.js’
console.log(fooName) // foo module
fooHello() // hello
```
4.匯出default
module.js
var name = 'foo module'
export {
name as default
}
app.js
// 由于default為關鍵字,所以引入該變數時需要重命名
import { default as fooName } from './module.js'
console.log(fooName) // foo module
5.模塊默認匯出,引入時可取任意變數名
module.js
var name = 'foo module'
export default name
app.js
import abc from './module.js'
console.log(abc) // foo module
-
注意事項
- export固定寫法 { }
- import匯入成員并不是復制一個副本,而是直接匯入模塊成員的參考地址,也就是說import得到的變數與export匯出的變數在記憶體中是同一塊空間,一旦模塊中的成員被修改,引入的變數也會同時修改,
- import匯入的變數是只讀變數,但物件的讀寫屬性不受影響
module.js
var name = 'jack' var age = 18 // var obj = { name, age } // export default { name, age } // 這里的 `{ name, hello }` 不是一個物件字面量, // 它只是語法上的規則而已 export { name, age } // export name // 錯誤的用法 // export 'foo' // 同樣錯誤的用法 setTimeout(function () { name = 'ben' }, 1000)
app.js
// CommonJS 中是先將模塊整體匯入為一個物件,然后從物件中結構出需要的成員
// const { name, age } = require('./module.js')
// ES Module 中 { } 是固定語法,就是直接提取模塊匯出成員
import { name, age } from './module.js'
console.log(name, age)
// 匯入成員并不是復制一個副本,
// 而是直接匯入模塊成員的參考地址,
// 也就是說 import 得到的變數與 export 匯入的變數在記憶體中是同一塊空間,
// 一旦模塊中成員修改了,這里也會同時修改,
setTimeout(function () {
console.log(name, age)
}, 1500)
// 匯入模塊成員變數是只讀的
// name = 'tom' // 報錯
// 但是需要注意如果匯入的是一個物件,物件的屬性讀寫不受影響
// name.xxx = 'xxx' // 正常
-
import用法
app.js
// 1.匯入規則 // import { name } from './module' // 不可以省略.js擴展名以及./,在commonJS中可以省略擴展名及./ import { name } from './module.js' console.log(name) // 'jack' // commonJS中可以直接匯入模塊,例如:import { lowercase } from './utils',但是在原生ESM中需要填寫完整路徑 // 后期使用打包工具后,可以省略擴展名以及省略index.js默認檔案 import { lowercase } from './utils/index.js' console.log(lowercase("HHH")) // 匯入模塊時必須使用/開頭,否則ESM認為是需要加載一個第三方模塊 // import { name } from './module.js' // 或者從網站根目錄開始 // import { name } from '/04-import/module.js' // 或者使用完整的url加載模塊 import { name } from 'http://localhost:3000/04-import/module.js' // 意味著可以直接飲用CDN的模塊檔案 console.log(name) // 2.只是需要執行某個模塊,并不需要提取模塊中的成員 import {} from './module.js' // 或者import './module.js' ,在并不需要外界控制的子功能模塊式使用此種匯入方式 // 3.匯入多個模塊 import * as mod from './module.js' // 將所有的匯出的成員全部匯入并使用as重命名,全部放入一個物件中,每個成員都會作為物件的屬性 console.log(mod) // 4.動態匯入模塊 // var modulePath = './module.js' // import { name } from modulePath // console.log(name) // 報錯 // if (true) { // import { name } from './module.js' // } // 報錯 // ESM提供全域函式import(),專門用于動態匯入模塊,該函式回傳一個promise物件,當模塊的異步加載完成后,會自動執行then中的回呼函式,模塊的物件可以通過引數獲取 import('./module.js').then(function (module) { console.log(module) }) // 5.匯入命名成員以及默認成員 import { name, age, default as title } from './module.js' // 或者 console.log(name, age, title) // 匯入命名成員以及默認成員簡寫 import abc, {name, age} from './module.js' // abc可以使用任意變數名 console.log(abc, name, age)module.js
var name = 'jack' var age = 18 export { name, age } console.log('module action') export default 'default export'utils/index.js
export function lowercase (input) { return input.toLowerCase() } -
直接匯出所匯入的成員
除了匯入模塊,import還可以配合export使用,效果是將匯入的結果直接作為當前模塊的匯出成員,匯出后,當前作用域不再可以訪問匯入的成員了,一般在index.js中使用,在index.js中把某些目錄中散落的一些模塊通過export組織到一起再進行匯出
app.js
// export {name, age} from './module.js' // console.log(name) // name is not defined // 繁瑣的方法 import {Button} from './components/button.js' import {Avatar} from './components/avatar.js' console.log(Button, Avatar) // 簡單的方法,compotents中新增index.js,匯入再匯出組件 import { Button, Avatar } from './components/index.js' console.log(Button) console.log(Avatar)components/button.js
var Button = 'Button Components' export default Buttoncomponents/avatar.js
export var Avatar = 'Avatar Components'components/index.js
// import {Button} from './components/button.js' // import {Avatar} from './components/avatar.js' // export {Button, Avatar} export { default as Button } from './button.js' export { Avatar } from './avatar.js' -
polyfill兼容方案
ESM2014年提出,早期的瀏覽器它不可能支持這個特性,另外,在IE還有一些國產的瀏覽器上,截止到目前為止都還沒有支持,所以說在使用的時候還是需要去考慮將信所帶來的一個問題,
可以借助一些編譯工具在開發階段將這些ES6的代碼,編譯成ES5的方式,然后,再到瀏覽器當中去執行,這里介紹一個模塊browser-es-module-loader,將檔案引入到網頁中,網頁就可以運行ESM了,
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>ES Module 瀏覽器環境 Polyfill</title> </head> <body> <script nomodule src="https://unpkg.com/promise-polyfill@8.1.3/dist/polyfill.min.js"></script> <script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script> <script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script> <script type="module"> import { foo } from './module.js' console.log(foo) </script> <!-- 以上代碼在支持ESm中的瀏覽器會運行兩次,所以使用nomoudle確保在不支持ESM的瀏覽器作業 --> <script nomodule> alert('hello') </script> </body> </html>這種兼容ESm的方式,它只適合于本地區測驗,也就是開發階段去玩一玩,但是,在生產階段千萬不要去用它,因為它的原理是在運行階段動態的去決議腳本,效率非常的差,在生產階段,還是應該預先去把這些代碼編譯出來,讓它可以直接在瀏覽器當中去作業,
4.3.ESM in Node.js
-
與CommonJS互動
ESM作為JavaScript的語言層面的一個模塊化標準,逐漸的會去統一所有JS應用領域的模塊化需求,Node.js作為JavaScript的一個非常重要的一個應用領域,目前,已經開始逐步支持這樣一個特性,從Node.js的8.5版本過后,內部就已經以實驗特性的方式去支持ESM了,也就是說在Node.js當中可以直接原生的去使用ESM去編代碼了,但是,考慮到原來的這個comment規范與現在的ESM它們之間的差距還是比較大的,所以說目前,這樣一個特性一直還是處于一個過渡的狀態,那接下來,就一起來嘗試一下,直接在Node環境當中使用ESM撰寫代碼,
需要在Node.js中使用ESM:
- 首先將檔案擴展名改為.mjs
- 然后在命令列使用–experimental-modules引數,這個引數代表去啟用ESM的實驗特性,
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-RurBpEdi-1610550317443)(https://i.loli.net/2021/01/05/8eO1cBri7sbhIGw.png)]
提取第三方模塊
import _ from 'lodash' console.log(_.camelCase('ES Module')) // esModule提取node內置模塊成員
// 也可以直接提取模塊內的成員,內置模塊兼容了 ESM 的提取成員方式 import {writeFileSync} from 'fs' writeFileSync('./bar.txt', 'es module working~') // 不支持,因為第三方模塊都是匯出默認成員 // import { camelCase } from 'lodash' // console.log(camelCase('ES Module')) -
與CommonJS的差異
1.在ESM中使用CommonJS模塊
es-module.mjs
// ES Module 中可以匯入 CommonJS 模塊 // 只能使用import載入默認成員的方式去使用commonJS模塊 import mod from './commonjs.js' console.log(mod)common.js
// CommonJS 模塊始終只會匯出一個默認成員 // module.exports = { // foo: 'commonjs exports value' // } // 使用commonJS的匯出的別名exports exports.foo = 'commonjs exports value'在命令列運行
D:\DeskTop\02-interoperability>node --experimental-modules es-module.mjs { foo: 'commonjs exports value' }2.通過commonJS載入ESM(Node原生的環境中不能在 CommonJS 模塊中通過 require 載入 ES Module)
es-module.js
export const foo = 'es module export value'common.js
const mod = require('./es-module.js') console.log(mod) // !!!報錯總結:
- ESM中可以匯入CommonJS模塊
- CommonJS中不能匯入ESM模塊
- CommonJS始終只會匯出一個默認成員
- 注意import不是解構匯出物件
-
ES Modules in Node.js - 與 CommonJS 的差異
esm.mjs
// ESM 中沒有模塊全域成員了 // // 加載模塊函式 // console.log(require) // // 模塊物件 // console.log(module) // // 匯出物件別名 // console.log(exports) // // 當前檔案的絕對路徑 // console.log(__filename) // // 當前檔案所在目錄 // console.log(__dirname) // -------------以上成員無法列印 // require, module, exports 自然是通過 import 和 export 代替 // __filename 和 __dirname 通過 import 物件的 meta 屬性獲取 // const currentUrl = import.meta.url // console.log(currentUrl) // 通過 url 模塊的 fileURLToPath 方法轉換為路徑 import { fileURLToPath } from 'url' import { dirname } from 'path' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) console.log(__filename) console.log(__dirname)cjs.js
// 加載模塊函式 console.log(require) // 模塊物件 console.log(module) // 匯出物件別名 console.log(exports) // 當前檔案的絕對路徑 console.log(__filename) // 當前檔案所在目錄 console.log(__dirname) -
新版本進一步支持ESM
在Node.js的最新版本當中,它進一步的支持了ESM,這里可以來嘗試一下,可以通過Node剛剛experimental去執行一這個js檔案,那此時執行的效果,跟之前所看到的也是一樣的,不過,在這個新版本當中,可以通過給專案的package點當中去添加一個type欄位斷,將這個type欄位的設定為module,這個時候這個專案向所有的js檔案默認就會以ESM去作業了,也就是說不用再將擴展名改成mjs了,直接讓它們改回來為js,再回到檔案當中,將檔案當中的路徑也給它修改回來,
此時,就可以回到命令列當中,再次重新運行一下index.js,這個js檔案就會按照ESM的形式去作業了,如果這個時候你還想去使用commonJS的話,例如再去新建一個common.js這樣一個檔案,這個時候需要單獨對于commonJS這種方式,做一個偶爾的處理,那就是將這個common的檔案修改為點cjs的這樣一個擴展名,那此時再次去執行的話,就可以正常的去使用common js規范了,
index.js
// Node v12 之后的版本,可以通過 package.json 中添加 type 欄位為 module, // 將默認模塊系統修改為 ES Module // 此時就不需要修改檔案擴展名為 .mjs 了 import { foo, bar } from './module.js' console.log(foo, bar)module.js
export const foo = 'hello' export const bar = 'world'common.cjs
// 如果需要在 type=module 的情況下繼續使用 CommonJS, // 需要將檔案擴展名修改為 .cjs const path = require('path') console.log(path.join(__dirname, 'foo'))package.json
{ "type": "module" } -
Babel兼容方案
如果你使用的是早期的Node,可以使用babel去實作ESM的兼容問題,babel是目前最主流的一塊JavaScript的編譯器,它可以用來幫助將一些使用了新特性的代碼,編譯成當前環境的代碼,之后可以放心的去使用新特性,下面使用babel去實作低版本Node(這里使用8.0.0),
index.js
// 對于早期的 Node.js 版本,可以使用 Babel 實作 ES Module 的兼容 import { foo, bar } from './module.js' console.log(foo, bar)module.js
export const foo = 'hello' export const bar = 'world'-
安裝babel及其它插件
yarn add @babel/node @babel/core @babel/preset-env --dev -
直接運行yarn babel-node index.js時會報錯,不支持import,原因非常簡單,因為babel,它是基于插件機制去實作的,它的核心模塊,并不會去轉換我們的代碼,那具體要去轉換代碼當中的每一個特性,它是通過插件來去實作的,也就是說需要一個插件去轉換代碼當中的一個特性,那之前所安裝的這個preset-env,它實際上是一個插件的集合,在這個插件集合當中去包含了最新的JS標準當中的所有的新特性,可以借助于這個preset直接去把我們當前這個代碼當中所使用到的ESM就給它轉換過來,
-
使用新命令
yarn babel-node index.js --presets=@babel/preset-env # hello world -
如果說覺得每次手動的去傳輸這樣一個引數會比較麻煩的話,那你也可以選擇把它放到組態檔當中,專案中新建.babelrc檔案,該檔案為json格式的檔案
{ "presets": ["@babel/preset-env"] }此時可以直接使用yarn babel-node index.js命令直接運行,不需要額外加引數,
-
preset是一個插件集合,我們移除preset,直接使用插件
yarn remove @babel/preset-env yarn add @babel/plugin-transform-modules-commonjs --dev這時修改組態檔
{ "plugins": [ "@babel/plugin-transform-modules-commonjs" ] }繼續運行命令yarn babel-node index.js,這樣也是可以的,
-
三、Webpack打包
1. 模塊打包工具的由來及概要
模塊化確實很好的解決了在復雜應用開發程序當中的代碼組織問題,但是隨著引入模塊化,Web應用又會產生一些新的問題:
第一個,ESM模塊系統,它本身就存在環境兼容問題,盡管現如今主流瀏覽器的最新版本都已經支持這樣一個特性,但是目前還沒有辦法做到統一所有用戶瀏覽器的使用情況,所以還需要去解決兼容問題;
第二個,通過模塊化的方式劃分出來的模塊檔案會比較多,而前端應用又是運行在瀏覽器當中的,因此每一個在應用當中所需要的檔案,都需要從服務器當中請求回來,這些零散的模塊檔案必將會導致瀏覽器頻繁發出請求,從而影回應用的作業效率;
第三個,在前端應用開發程序當中,不僅僅只有JavaScript的代碼需要模塊化,隨著應用的日益復雜,HTML、CSS等資源檔案同樣也會面臨相同的問題,而且從宏觀角度來看的話,這些檔案也都可以看作為前端應用當中的一個模塊,只不過這些模塊的種類和用途跟JavaScript是不同的,
對于整個程序而言模塊化肯定是有必要的,不過需要在原有的基礎之上去引入更好的方案或者工具去解決上面這樣幾個問題或者是需求,讓開發者在應用的開發階段,可以繼續享受模塊化帶來的優勢,又不必擔心模塊化對生產環境所產生的一些影響,我們就先對這個所謂的更好的方案或者工具去提出一些設想,我們希望它們能夠滿足我們的這些設想:
第一點,需要這樣一個工具能夠編譯代碼,**開發階段撰寫的包含新特性的代碼,直接去轉換為能夠兼容絕大多數環境的代碼,**這樣一來面臨的環境問題也就不存在了;
第二點,**能夠將散落的模塊檔案再次打包到一起,解決了瀏覽器當中頻繁對模塊檔案發出請求的問題,**至于模塊化檔案劃分,只是在開發階段需要它,因為它能夠更好的代碼,但是對于運行環境實際上是沒有必要的,所以說可以選擇在開發階段通過模塊化的方式去撰寫,生產階段還是把它們打包到同一個檔案當中,
第三點,需要去支持不同種類的前端資源型別,可以把前端開發程序當中所涉及到的樣式,圖片,字體等等所有資源檔案都當作模塊去使用,對于整個前端應用來講,就有了一個統一的模塊化方案,之前介紹的那些模塊化方案,實際上只是針對現有JavaScript的模塊化方案,現在強調,對于整個前端應用來講,它的一個模塊化的方案,這些資源,有了模塊化方案后就可以通過代碼去控制,那它就可以與業務代碼統一去維護,這樣對于整個來講的話會更加合理一些,
針對前兩個需求,完全可以借助于之前所了解過的一些構建系統,去配合一些編譯工具就可以實作,但是,對于最后一個需求,很難通過這種方式去解決了,所以說就有了接下來所介紹的一個主題,也就是前端模塊打包工具,
前端領域目前有一些工具,就很好的解決了以上這幾個問題,其中最為主流的就是Webpack、Parcel、Rollup,以Webpack為例,它的一些核心特性,就很好的滿足了上面的那些需求,

-
首先Webpack作為一個模塊打包工具(Module Bundler),它本身就可以解決模塊化JavaScript代碼打包的問題,通過Webpack就可以將零散的模塊代碼打包到同一個JS檔案當中,對于代碼中那些有環境兼容問題的代碼,可以在打包的程序當中,通過模塊加載器(Loader)對其進行編譯轉換,
-
其次,Webpack還具備代碼拆分(Code splitting)的能力,能夠將應用當中所有的代碼,都按照需要去打包,這樣就不會產生把所有的代碼全部打包到一起,產生的這個檔案會比較大的這樣一個問題,可以把應用加載程序當中初次運行的時候所必須的那些模塊打包到一起,那對于其它的那些模塊再單獨存放,等到應用作業程序當中實際需要的某個模塊,再去加載這個模塊,從而實作增量加載或者叫漸進式加載,這樣就不用擔心檔案太碎或者是檔案太大這兩個極端的問題,
-
最后對于前端模塊型別的問題,**Webpack支持在JavaScript當中以模塊化的方式去載入任意型別的資源檔案,**例如在Webpack中就可以通過JavaScript去直接import一個css的檔案,最侄訓通過style標簽的形式去作業,其它型別的檔案,也可以有類似的這種方式去實作,
總之來說,所有的打包工具它們都是以模塊化為目標,這里所說的模塊化是對整個前端專案的模塊化,也就是比之前所說的JavaScript模塊化要更為宏觀一些,它可以讓我們在開發階段更好的去享受模塊化所帶來的優勢,同時,又不必擔心模塊化對生產環境所產生的一些影響,那這就是模塊化工具的一個作用,
2. Webpack快速上手、組態檔
-
Webpack快速上手
Webpack作為目前最主流的前端模塊打包器,提供了一整套的前端專案模塊化方案,而不僅僅是局限于只對JavaScript的模塊化,通過提供的前端模塊化方案,我們就可以很輕松的對前端專案開發程序當中,涉及到的所有的資源進行模塊化,因為Webpack的想法比較先進,而且它的檔案也比較晦澀難懂,所以說在最開始的時候,它顯得對開發者不是十分友好,但是隨著它版本的迭代,官方的檔案也在不斷的更新,目前Webpack已經非常受歡迎了,基本上可以說是覆寫了絕大多數現代化的外部應用專案開發程序,

使用***yarn init***初始化專案目錄,安裝webpack以及webpack-cli,使用yarn webpack,webpack會自動從src中的index.js開始打包(尋找import)
yarn init --yes yarn add webpack webpack-cli --dev yarn webpack運行yarn webpack后,webpack自動將index.js、heading.js打包在dist目錄下的main.js中,將index.html中的資源參考變為dist/main.js,并將script中的type="module"取消,再次使用serve .命令運行,發現專案依然可以運行,

如果每次都需要yarn去運行webpack命令,會比較麻煩,可以在package.json中添加scripts欄位,并將其設定為“build”:“webpack”,這樣就可以直接運行
***yarn build***命令,

-
Webpack組態檔
Webpack4.0以后支持零組態檔打包,也就是說不需要組態檔,直接按照約定的內容區打包,它的約定是默認入口檔案為src下的index.js,默認輸出為dist下的main.js,我們如果需要按照自定約定去打包,Webpack也支持組態檔打包,
在專案根目錄下添加webpack.config.js檔案,該檔案在node環境下運行,其內容如下:
const path = require('path') module.exports = { entry: './src/main.js', // 打包檔案的默認入口,不能省略./ output: { filename: 'bundle.js', // 輸出打包檔案的檔案名稱 path: path.join(__dirname, 'output') // 必須為絕對路徑,所以需要path模塊配合 } }

3. Webpack作業模式、打包結果運行原理
Webpack新增了一個作業模式的用法,這種用法大大簡化了Webpack配置的復雜程度,可以把它理解成針對于不同環境的基礎預設的配置,我們使用yarn webpack打包專案,命令列會出現一個配置警告,大致意思是我們沒有設定一個mode的屬性,可能會使用默認的production模式去作業,在這個模式下,webpack內部會自動啟動一些優化插件,比如自動壓縮代碼,這對實際生產環境非常友好,但是對于開發環境中,我們無法閱讀這些打包結果,
Webpack作業模式:
- production(默認)
- development(開發模式)
- none
可以通過cli引數去指定打包的模式,具體用法就是給webpack傳遞一個引數:–mode,引數有三種取值,默認就是production,production模式它會自動啟動優化,去優化打包結果,第二種引數是development,開發模式會自動優化打包的速度,它會添加一些開發程序中需要的輔助到代碼中(后面介紹除錯的會詳細介紹),第三種引數是none模式,none模式下,webpack運行最原始狀態的打包,不會做任何額外的處理,
yarn webpack --mode production # production為默認的作業模式,可以不用去顯式指定
yarn webpack --mode development
yarn webpack --mode none
目前作業模式只有這三種,具體的三種模式的差異可以從官網(https://webpack.js.org/configuration/mode)中找到,當然除了cli引數指定模式,還可以通過組態檔方式指定作業模式,
在webpack.config.js中添加node欄位,并指定模式,這樣就可以通過yarn webpack直接以配置的方式去打包,
webpack.config.js
const path = require('path')
module.exports = {
// 這個屬性有三種取值,分別是 production、development 和 none,
// 1. 生產模式下,Webpack 會自動優化打包結果;
// 2. 開發模式下,Webpack 會自動優化打包速度,添加一些除錯程序中的輔助;
// 3. None 模式下,Webpack 就是運行最原始的打包,不做任何額外處理;
mode: 'development',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist')
}
}
我們解讀一下webpack打包過后的結果,為了可以更好的理解打包過后的代碼,這里先將webpack的作業模式是為none,這樣是以最原始的狀態去打包代碼,
ps:此處使用的版本:
"webpack": "4.40.2",
"webpack-cli": "3.3.9"
webpack.config.js
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist')
}
}
yarn webpack
生成的bundle.js檔案
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _heading_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
const heading = Object(_heading_js__WEBPACK_IMPORTED_MODULE_0__["default"])()
document.body.append(heading)
/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = (() => {
const element = document.createElement('h2')
element.textContent = 'Hello world'
element.addEventListener('click', () => {
alert('Hello webpack')
})
return element
});
/***/ })
/******/ ]);
整個函式是一個立即執行函式,引數為modules,函式呼叫時傳入一個陣列,陣列中的每個元素是引數串列相同的函式,這里的函式對應的就是源代碼中的模塊,也就是說每一個模塊最終都會被包裹到這樣一個函式當中,從而去實作模塊的私有作用域,
進入webpack作業入口函式,這個函式內部并不復雜,而且注釋也非常清晰,最開始先定義了一個物件,用于存放或者叫快取加載過的模塊,緊接著,定義了一個__webpack_require函式,該函式專門用來加載模塊,再往后就是在require函式上面掛載了一些其它的資料和一些工具函式,入口函式執行到最后,呼叫了require函式,引數傳入0,來加載模塊,這個地方的模塊ID,實際上就是上面的模塊數當中的元素下標,也就是說這里才開始去加載在源代碼當中所謂的入口模塊,
4. Webpack資源模塊加載
正如一開始提到,webpack并不只是JavaScript的模塊化打包工具,它應該是整個前端專案或前端工程的模塊打包工具,也就是說可以通過webpack引入任意型別的靜態資源檔案,接下來通過webpack引入css檔案,
首先在專案目錄中添加一個main.css檔案,內容如下:
body {
margin: 0 auto;
padding: 0 20px;
max-width: 800px;
background: #186fb1;
}
然后回到webpack.config.js下,將入口檔案的路徑指向新創建的css檔案,隨后配置loader組件,test值為正則運算式/.css$/,use配置一個陣列,分別為style-loader以及style-loader
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/main.css',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist')
},
module: {
// rules陣列針對于其它資源模塊的加載規則,每個規則物件的都需要設定兩個屬性,
rules: [
{
test: /.css$/, // test用來去匹配在打包程序當中所遇到的檔案路徑
// use用來去指定我們匹配到的檔案,需要去使用的loader
use: [
'style-loader',
'css-loader'
]
}
]
}
}
命令列啟動,yarn webpack,通過serve . 運行,在瀏覽器中訪問就可以看到我們的css生效了,
ps:use中,如果配置了多個loader,其執行順序是從陣列最后一個元素往前執行,所以這里一定要把css-loader放到最后,因為我們必須要先通過css-loader把css代碼轉換為模塊才可以正常打包,
style-loader作業代碼在bundle.js中,部分代碼如下:

loader是webpack實作整個前端模塊化的核心,通過不同的loader就可以實作加載任何型別的資源,
5. Webpack匯入資源模塊
通過上面的探索,webpack確實可以把css檔案作為打包的入口檔案,不過webpack的打包入口檔案一般是JavaScript,因為打包入口檔案從某種程度來說,算是應用的運行入口,前端應用當中的業務是由JavaScript去驅動的,上面只是嘗試一下,正確的做法還是將JavaScript檔案作為打包的入口檔案,
webpack.config.js
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist')
},
module: {
rules: [
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
}
}

分別新建main.css以及heading.css檔案,代碼如圖所示,分別在heading.js和main.js中通過import引入css檔案,因為heading.css使用了類選擇器,所以需要在黃色框中為element添加類名,
構建完成后,命令列運行yarn webpack,隨后運行serve .命令,在瀏覽器打開,發現css檔案生效,
傳統的做法當中是將樣式和行為分離開,單獨去維護,單獨去引入,而webpack中建議我們在JavaScript中引入css,原因是webpack不僅僅建議我們在JavaScript中引入css,而是建議我們撰寫代碼程序中,去引入任何當前模塊所需要的資源檔案,因為真正需要資源的不是應用,而是此刻正在撰寫的代碼,它想要作業,就必須加載對應的資源,
通過JavaScript代碼去引入資源檔案,或者叫建立我們JavaScript和資源檔案之間的依賴關系,它有一個很明顯的優勢,JavaScript代碼本身是負責完成整個應用的業務功能,那放大來看,它就是驅動了我們整個前端應用,而在實作業務功能的程序當中,可能需要用到樣式、圖片等等一系列的資源檔案,如果建立了這種依賴關系,一來邏輯上比較合理,因為我們的JavaScript確實需要這些資源檔案的配合,才能去實作對應的功能,二來保證上線時資源檔案不會缺失,而且每一個上線的檔案都是必要的,
6. Webpack檔案資源加載器
目前webpack社區提供了非常多的資源加載器,基本上能想到的所有合理的需求都會有對應的loader,接下來嘗試一個非常有代表性的資源加載器,
大多數的加載器都類似于css-loader,都是將資源模塊轉換為js代碼的實作方式去作業,但是,還有一些經常用到的資源檔案,例如專案當中的圖片或者字體,這些檔案是沒有辦法通過js的方式去表示的,對于這一類的資源檔案,需要用到檔案資源加載器,也就是file-loader,
專案src目錄中添加一張圖片icon.png,在main.js中通過import的方式匯入圖片,并且創建圖片標簽,將其src設定為匯入的接收值,
初始目錄:

main.js
import createHeading from './heading.js'
import './main.css'
import icon from './icon.png'
const heading = createHeading()
document.body.append(heading)
const img = new Image()
img.src = icon
document.body.append(img)
隨后在webpack.config.js中設定一個新的規則,當遇到.png結尾的檔案時,使用file-loader加載器,
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
publicPath: 'dist/' // 網站的根目錄
},
module: {
rules: [
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /.png$/,
use: 'file-loader'
}
]
}
}
安裝file-loader,命令列使用yarn webpack打包資源,serve .運行,通過瀏覽器可以觀察到,img資源被加載成功,
打包后的目錄:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-4kTdX5HR-1610550317461)(https://i.loli.net/2021/01/05/FtikhS9xqPB5I8a.png)]

總結檔案加載器的作業程序,webpack打包程序中遇到圖片檔案,根據webpack.config.js中的配置規則對應到file-loader加載器,file-loader首先將匯入的(圖片)檔案復制到輸出目錄dist,然后將檔案拷貝到輸出目錄的路徑作為這個模塊的回傳值,對于這個應用來說,這個資源就被發布了,同時,也可以通過模塊的匯出成員拿到這個資源的訪問路徑,
7. Webpack URL 加載器
除了file-loader這種通過拷貝物理檔案的形式去處理檔案資源以外,還有一種通過data-url去表示檔案,這種方式也非常常見,Data URL是一種非常特殊的協議,它可以直接用來表示一個檔案,傳統URL一般要求服務器上有一個對應的檔案,然后通過請求這個地址,得到服務器上對應的檔案,

Data URL是一種當前URL就可以直接去表示這種檔案內容的方式,也就是說這種Data URL中的文本就已經包含了檔案的內容,在使用這種URl時,我們就不會再發送任何的HTTP請求,例如下面的URL:
data:text/html;charset=UTF-8,<h1>html content!</h1>
瀏覽器就能根據url決議出來這是一個HTML型別的檔案內容,它的編碼是UTF-8,內容是h1標簽,復制該url到瀏覽器地址欄,可以看到瀏覽器將它正常渲染出來了,
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-UJAsm8G4-1610550317466)(https://i.loli.net/2021/01/05/y8fJzoeGNAvkwTj.png)]
但是如果是圖片或者字體這一類的,這些無法通過文本表示的二進制檔案,我們可以將這些檔案內容編譯為base64編碼,以base64編碼也就是字串表示檔案的內容,
webpack中有一種加載器專門處理這種檔案:url-loader,安裝該加載器并在webpack.config.js中配置如下:
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
publicPath: 'dist/'
},
module: {
rules: [
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /.png$/,
use: 'url-loader'
}
]
}
}
在遇到png檔案時,通過url-loader編譯后打包,運行yarn webpack,查看bundle.js檔案,發現它將該圖片轉為base64編碼,并匯出,

通過serve .啟動服務器,發現該圖片被正常顯示在頁面上,通過F12開發者工具打開發現該圖片的src為上面base64的編碼字串,并且復制該url到地址欄,瀏覽器也可以正常渲染出該圖片,


這種方式其實非常適合專案當中體積比較小的資源,因為體積過大的話,就會造成打包結果非常大,從而影響運行速度,最佳的實踐方式,應該是對于專案當中的小檔案,通過url-loader的去轉換為URL代碼,從而減少應用發送資訊,對于較大的檔案,應該傳統的方式,單個檔案方式去存放,從而提高我們應用的加載速度,它支持通過配置選項的方式來去實作剛剛所說的這種最佳實踐方式,回到組態檔當中,具體做法就是將url-loader的這樣一個字串,這種簡化的配置方式修改為一個物件,那物件當中的loader的屬性,還是這個字串url-loader,為它添加一些其它配置選項,option選項中可以配置其它引數,具體引數如下:
{
test: /.png$/,
loader: 'url-loader',
options: {
limit: 10 * 1024 // 10 KB
}
}
}
- 小檔案使用Data URL,減少請求次數
- 大檔案單獨提取存放,提高加載速度
8. Webpack 常用加載器分類
webpack中的資源加載器有點像是生活中工廠里的生產車間,??它是用來去處理和加工打包程序當中所遇到的資源檔案,??設計當中還有很多其它的加載器,?,
目前個人分為三類:
-
編譯轉換型別加載器
它會把加載到的資源模塊轉換為JavaScript的代碼,??例如之前所用的css-loader,?它就是將css代碼轉換為bundle當中的一個JavaScript的模塊,??從而去實作通過JavaScript去運行css

-
檔案操作型別加載器
檔案操作型別加載器都會把加載到的資源模塊??拷貝輸出的目錄,同時將檔案的訪問路徑向外匯出,例如之前用到的file-loader,它就是一個非常典型的檔案操作型別加載器

-
代碼檢查類
針對于代碼質量檢查的加載器,??就是對所加的資源檔案,一般是代碼,??去進行校驗的一種加載器,??這種加載器,它的目的是為了統一代碼的風格,從而去提高代碼質量,??這種型別加載器一般不會去修改生產環境的代碼,

9. Webpack 與ES2015
由于webpack默認就能處理代碼當中的import和export,??所以很自然都會有人認為webpack會自動編譯的ES6代碼,實則不然,??那是webpack的僅僅是對模塊去完成打包作業,??所以說它才會對代碼當中的import和export做一些相應的轉換,??它并不能去轉換我們代碼當中其它的es6特性,
如果需要將ES6的代碼打包并編譯為ES5的代碼,需要一些其它的編譯形加載器,這里安裝一些額外的插件,
yarn add babel-loader @babel/core @babel/preset-env --dev
安裝完成后,撰寫webpack.config.js組態檔,針對js代碼指定babel-loader
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
publicPath: 'dist/'
},
module: {
rules: [
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /.png$/,
use: {
loader: 'url-loader',
options: {
limit: 10 * 1024 // 10 KB
}
}
}
]
}
}
再次重新打包,代碼中的ES2015中的新特性都被轉換,

總結:
- webpack只是打包工具,它不會處理一些ES6或者更高版本的新特性
- 如果需要處理新特性,可以通過為js代碼單獨配置加載器來實作
10. Webpack 加載資源的方式
除了代碼中的import能夠觸發模塊的加載,webpack還提供幾種方式,具體如下:
-
遵循ESM標準的import宣告
-
遵循commonJS標準的require函式
如果通過commonJS標準的require函式載入一個ESM的話,需要require一個函式的default屬性去獲取
const heading = require('./heading.js').default -
遵循AMD標準的define函式和require函式
webpack遵循多種模塊化標準,不過除非必要,建議不要在一個專案中混用多種標準,這樣會造成專案可維護性差,每個專案使用一種標準就行,
除了JavaScript代碼中的這三種方式之外,還有一些獨立的加載器,它在作業時也會去處理所加載到的資源當中的一些匯入的模塊,例如:
-
css-loader加載的css檔案,import指令以及部分屬性當中的URL函式,也會去觸發相應的資源模塊加載
樣式代碼中的@import指令和url函式
-
html-loader加載html檔案中的一些src屬性也會觸發

webpack.config.js
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
publicPath: 'dist/'
},
module: {
rules: [
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /.png$/,
use: {
loader: 'url-loader',
options: {
limit: 10 * 1024 // 10 KB
}
}
},
{
test: /.html$/,
use: {
loader: 'html-loader',
options: {
attrs: ['img:src', 'a:href'] // HTML中針對不同的屬性采用不同的加載器
}
}
}
]
}
}
main.js入口檔案
import './main.css'
import footerHtml from './footer.html'
document.write(footerHtml)
總結webpack資源加載方式:
- 遵循ESM標準的import宣告
- 遵循CommonJS標準的require函式
- 遵循AMD標準的define函式和require函式
- 樣式代碼中的@import指令和url函式
- HTML代碼中的img標簽的src屬性以及a標簽的href屬性
11. Webpack 核心作業原理
其實webpack官網首屏的內容就已經很清楚的描述了它的作業原理,

這里以一個普通的前端專案為例,在專案中一般會散落著各種各樣的代碼及資源檔案,webpack會根據webpack.config.js配置,找到其中一個檔案作為打包入口,一般情況下入口檔案是一個JavaScript檔案,

然后,它會順著入口檔案中的代碼,根據代碼中出現的import或者require之類的陳述句,決議推斷出來這個檔案所依賴的資源,其次就形成了整個專案中所有用到檔案之間的一個依賴關系的一個依賴樹,有了這個依賴樹之后,webpack會遍歷或者叫遞回這個依賴樹,然后找到每個節點所對應的資源檔案,
其次根據配置檔案中rules屬性去找到這個模塊所對應的加載器,然后交給對應的loader加載器去加載這個模塊,最后會將加載到的結果放到bundle.js也就是打包結果中,從而實作整個專案的打包,

整個程序中loader的機制氣到了一個很重要的作用,因為如果沒有loader的話,webpack就沒辦法實作各種資源檔案的加載,那對于webpack來說,它也只能算是一個用來打包或者合并js模塊代碼的一個工具了,
12. webpack 開發一個Loader
loader作為webpack的核心機制,內部的作業原理非常簡單,接下來一起開一發一個自己的loader,需求是一個markdown檔案的加載器,希望有了一個加載器之后,可以直接在代碼中直接匯入這個markdown檔案,

webpack內部的一個作業原理其實非常簡單,就是一個從輸入到輸出之間的一個轉換,?除此之外,還了解了loader,它實際上是一種管道的概念,可以將此次的這個loader的結果交給下一個loader去處理,??然后通過多個loader去完成一個功能,?例如之前所使用的css-loader和style-loader的之間的一個配合,包括后面還會使用到的sass或者less這種loader,它們也需要去??配合剛剛所說的這兩種loader,?這個就是作業管道一個特性?,
13. Webpack 插件機制介紹
插件機制是webpack當中另外一個核心特性??,它目的是為了增強webpack專案自動化方面的能力??,loader就是負責實作各種各樣的資源模塊的加載??,從而實作整體專案打包??,plugin則是用來去解決專案中除了資源以外,其它的一些自動化作業?,例如:
- plugin可以幫我們去實作自動在打包之前去清除dist目錄??,也就是上一次打包的結果??;
- 又或是它可以用來去幫我們拷貝那些不需要參與打包的資源檔案到輸出目錄??;
- 又或是它可以用來去幫我們壓縮我們打包結果輸出的代碼??,
總之??,有了plugin的webpack,幾乎無所不能的實作了前端工程化當中絕大多數經常用到的部分??,這也正是很多初學者會有webpack就是前端工程化的這種理解的原因??,
14. Webpack 自動清除輸出目錄插件
了解了插件的基本作用過后,接下來先來體驗幾個最常見的插件,??通過這個程序去了解如何使用插件,
??第一個就是用來自動清除輸出目錄的插件(??clean-webpack-plugin),通過之前的演示你可能已經發現,??webpack每次打包的結果都是覆寫到dist目錄,??而在打包之前,dist中就可能已經存在一些之前的遺留檔案,?再次打包,它只能覆寫掉那些同名的檔案,??對于其它那些已經移除的資源檔案就會一直積累在里面,非常不合理,那更為合理的做法就是在每次打包之前,??自動去清理dist目錄,??這樣就只會存在那些需要的檔案,clean-webpack-plugin??就很好的實作了這樣一個需求,
??那它是一個第三方的插件,先來通過yarn去安裝,??安裝過后能回到webpack.config.js的組態檔當中,然后去匯入這個??那這個插件模塊,??然后,使用插件我們需要去為配置物件添加一個plugins屬性,??這個屬性就是專門用來去配置插件的地方,??它是一個陣列,??添加一個插件就是在這個陣列當中去添加一個元素,
絕大多數插件模塊匯出的都是一個型別,?這里的clean-webpack-plugin也不例外,??所以使用它就是通過這個型別去創建一個實體,然后將這個實體放到這個陣列當中,完成之后再次嘗試yarn webpack進行打包,??此時,之前的那些打包結果就不會存在了,??dist目錄中都是本次打包的結果,??非常干凈,
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
publicPath: 'dist/'
},
module: {
rules: [
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /.png$/,
use: {
loader: 'url-loader',
options: {
limit: 10 * 1024 // 10 KB
}
}
}
]
},
plugins: [
new CleanWebpackPlugin(), // 通過這個型別去創建一個實體
]
}
15. Webpack 自動生成HTML插件
-
生成基本HTML檔案
除了清理dist的目錄以外,??還有一個非常常見的需求就是自動去生成使用打包結果的HTML,在這之前HTML??都是通過硬編碼的方式單獨去存放在專案根目錄下的,??
但這種方式有兩個問題,??第一就是在專案發布時,需要同時去發布的HTML檔案和所有的打包結果,這樣的話會比較麻煩,??而且上線過后,還需要去確保HTML的代碼路徑參考都是正確的,??第二個問題是,如果說輸出的目錄或者是輸出的檔案名,也就是??打包結果的配置發生了變化,??HTML代碼中script標簽所參考的路徑就需要手動的去修改,??
這是硬編碼的方式存在的兩個問題,??要解決這兩個問題,最好的辦法就是通過webpack??自動去生成HTML檔案,??也就是讓HTML也去參與到webpack構建程序??中,??構建程序中,webpack知道它生成了多少個bundle,??它會自動將這些打包的bundle添加到的頁面當中,?這樣的話,??一來HTML它也輸出到了dist目錄,上線的時候就只需要把dist目錄發布出去就可以了,??二來,HTML當中對于bundle的參考,它是動態的注入進來的,它不需要手動的去硬編碼,??所以說它可以確保路徑的參考是正常的,
??具體的實作方式,需要去借助一個叫html-webpack-plugin的一個插件去實作,這個插件同樣也是一個第三方的模塊,同樣需要去單獨安裝這個模塊,
yarn add html-webpack-plugin --dev??之后回到組態檔當中,載入這個模塊,??但這里不同于clean-webpack-plugin,??html-webpack-plugin默認匯出的就是一個插件的型別,我們不需要去解構它內部的成員,??有了這個型別過后,回到配置物件的plugins屬性當中,去添加一個這個型別的實體物件,??這樣就完成了這個插件的一個配置,
??那最后我們回到命令列終端,??再次運行打包命令,??index.html出現在了dist目錄當中,對于bundle的參考的路徑也是正常了,這樣就不再去需要根目錄下的index.html檔案了,之后HTML檔案都是通過webpack自動生成出來的?,
const path = require('path') const { CleanWebpackPlugin } = require('clean-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { mode: 'none', entry: './src/main.js', output: { filename: 'bundle.js', path: path.join(__dirname, 'dist'), }, module: { rules: [ { test: /.css$/, use: [ 'style-loader', 'css-loader' ] }, { test: /.png$/, use: { loader: 'url-loader', options: { limit: 10 * 1024 // 10 KB } } } ] }, plugins: [ new CleanWebpackPlugin(), // 用于生成 index.html new HtmlWebpackPlugin(), }) ] } -
生成HTML基本標簽以及使用模板生成HTML
有了html-webpack-plugin之后,就可以動態生成應用所需要的的HTML檔案,但是這里仍然存在一些需要改進的地方,
首先是HTML中的標題必須要修改,其次是很多時候需要自定義頁面當中的一些元資料標簽和一些基本的DOM結構,對于簡單的自定義,可以使用修改webpack.config.js檔案中的html-webpack-plugin屬性,如下:
plugins: [ new CleanWebpackPlugin(), // 用于生成 index.html new HtmlWebpackPlugin({ title: 'Webpack Plugin Sample', // 生成html檔案的標題 meta: { viewport: 'width=device-width' // 生成一些自定義的dom元素 }, }), ]如果需要對HTML檔案進行大量的自定義的話,需要在源代碼中添加一個用于生成HTML檔案的模板檔案,讓html-webpack-plugin根據模板生成頁面,在src目錄中添加index.html檔案,根據需要在模板中添加一些回應的元素,模板中希望動態輸出一些內容,采用lodash模板語法的方式:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Webpack</title> </head> <body> <div class="container"> <!--loadsh模板語法,訪問插件中的options屬性中的title值--> <h1><%= htmlWebpackPlugin.options.title %></h1> </div> </body> </html>htmlWebpackPlugin.options實際是html-webpack-plugin內部提供的一個變數,也可以通過另外一個屬性去添加一些自定義變數,然后通過template屬性去指定模板檔案,再次使用yarn webpack指令去打包專案,發現dist中的index.html出現了自定義的內容,
plugins: [ new CleanWebpackPlugin(), // 用于生成 index.html new HtmlWebpackPlugin({ title: 'Webpack Plugin Sample', meta: { viewport: 'width=device-width' }, template: './src/index.html' // 用于指向模板檔案的路徑 }), ] -
輸出多個頁面檔案
除了自定義輸出檔案的內容,同時輸出多個頁面檔案也是一個非常常見的需求,其實配置非常簡單,組態檔中添加一個新的HtmlWebpackPlugin物件,配置如下:
plugins: [ new CleanWebpackPlugin(), // 用于生成 index.html new HtmlWebpackPlugin({ title: 'Webpack Plugin Sample', meta: { viewport: 'width=device-width' }, template: './src/index.html' }), // 用于生成 about.html new HtmlWebpackPlugin({ filename: 'about.html', // 用于指定生成的檔案名稱,默認值是index.html title: 'About html' }) ]
16. Webpack 插件使用總結
在專案中,一般還有一些不需要參與構建的靜態檔案,??它們最終也需要發布到線上,??例如我們網站的favicon.icon,??一般會把這一類的檔案統一放在專案的public目錄當中,??希望webpack在打包時,可以一并將它們復制到輸出目錄,
??對于這種需求,可以借助于copy_webpack_plugin,??先安裝一下這個插件??,然后再去匯入這個插件的型別,??最后同樣在這個plugin屬性當中去添加一個這個型別的實體,??這型別的建構式它要求傳入一個陣列,??用于去指定需要去拷貝的檔案路徑,它可以是一個通配符,也可以是一個目錄或者是檔案的相對路徑,??這里使用plugin,??它表示在打包時會將所有的檔案全部拷貝到輸出目錄,??再次運行webpack指令,??打包完成過后,public目錄下所有的檔案就會同時拷貝到輸出目錄,
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
// publicPath: 'dist/'
},
module: {
rules: [
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /.png$/,
use: {
loader: 'url-loader',
options: {
limit: 10 * 1024 // 10 KB
}
}
}
]
},
plugins: [
new CleanWebpackPlugin(),
// 用于生成 index.html
new HtmlWebpackPlugin({
title: 'Webpack Plugin Sample',
meta: {
viewport: 'width=device-width'
},
template: './src/index.html'
}),
// 用于生成 about.html
new HtmlWebpackPlugin({
filename: 'about.html'
}),
new CopyWebpackPlugin([
'public'
])
]
}
17. Webpack 開發一個插件
通過前面的了解,我們知道相比于loader,plugin的能力范圍相對更廣一些,loader只是在加載模塊的環節去作業,?而plugin?范圍幾乎觸及到作業的每一個環節,那么plugin的作業機制到底是怎么實作的?其實原理很簡單,webpack的插件機制其實就是在軟體開發中最常見的鉤子機制,
鉤子機制也很容易理解,有點類似于web中的事件,在webpack作業程序中有很多環節,為了便于插件的擴展,webpack幾乎給每一個環節都埋下了一個鉤子,這樣的話,在開發插件的程序中,就可以通過鉤子在不同節點上掛載不同的任務,這樣可以輕松的擴展webpack能力,

接下來自定義一個插件,了解具體如何往鉤子上掛在任務,webpack要求插件必須是一個函式或者是一個包含apply方法的物件,一般會把插件定義為一個型別,然后在型別匯總定義一個方法,使用的時候就是通過這個型別構建一個實體,
插件需求:
-
清除webpack打包生成中的bundle.js中的每行首位的注釋字符

webpack.config.js
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
class MyPlugin {
apply (compiler) {
console.log('MyPlugin 啟動')
// 通過compiler的hooks方法訪問emit,并通過tap方法注冊一個鉤子函式,引數一:插件名稱;引數二:掛載到鉤子上的函式
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation => 可以理解為此次打包的背景關系
// compilation.assets打包程序中所有的資源資訊
for (const name in compilation.assets) {
// console.log(name) // 每個打包成功后的檔案名
// console.log(compilation.assets[name].source()) // 通過鍵值訪問對應檔案名的資源
if (name.endsWith('.js')) {
// 獲取后綴為.js的檔案資源
const contents = compilation.assets[name].source()
// 使用全域正則替換注釋
const withoutComments = contents.replace(/\/\*\*+\*\//g, '')
// 將替換后的結果覆寫到原檔案中
compilation.assets[name] = {
source: () => withoutComments, // 暴露最新的內容
size: () => withoutComments.length // 暴露最新內容的長度
}
}
}
})
}
}
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
// publicPath: 'dist/'
},
module: {
rules: [
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /.png$/,
use: {
loader: 'url-loader',
options: {
limit: 10 * 1024 // 10 KB
}
}
}
]
},
plugins: [
new CleanWebpackPlugin(),
// 用于生成 index.html
new HtmlWebpackPlugin({
title: 'Webpack Plugin Sample',
meta: {
viewport: 'width=device-width'
},
template: './src/index.html'
}),
// 用于生成 about.html
new HtmlWebpackPlugin({
filename: 'about.html'
}),
new CopyWebpackPlugin([
// 'public/**'
'public'
]),
new MyPlugin()
]
}
隨后運行yarn webpack重新打包,發現每行前面的注釋已經被去除掉了,
18. Webpack 開發體驗的問題
在此之前,我們已經了解了一些webpack的相關概念和一些基本的用法,??但是如果以目前的狀態去應對日常的開發作業還遠遠不夠,??那是因為撰寫源代碼再通過webpack打包,然后,運行應用最后重繪瀏覽器這種周而復始的方式,過于原始,如果說在實際的開發中還按照這種方式去使用,那必然會大大降低我們的開發效率,??應該如何去提高我們的開發效率???
在這里對理想的開發環境做一個設想,??首先希望這樣一個環境,它??必須使用HTTP服務區運行而不是以檔案的形式去預覽,??這樣的話我們一來,??更加接近生產環境的狀態,??二來可能會需要去使用ajax之類的一些API??,這些API使用檔案的形式??去訪問是不被支持的,其次??我們希望這樣一個環境當中,我們去修改源代碼過后,webpack就可以自動幫我們完成構建,然后瀏覽器可以即時顯示最新的結果,??這樣的話就可以大大減少在開發程序中額外的重復操作,??最后,還需要這樣一個環境,它能夠去提供Source Map支持,這樣的話,我們運行程序當中一旦出現錯誤,就可以根據錯誤的堆疊資訊,快速定位到源代碼當中對應的位置,??便于除錯應用,?,那對于以上這些需求,??webpack都已經有相對應的功能去實作,??接下來讓重點了解具體如何增強使用webpack的開發體驗,
19. Webpack 自動編譯及自動重繪瀏覽器
自動編譯
-
目每次修改完源代碼都是??通過命令列手動重復運行webpack命令,從而得到最新的打包結果,??那這種辦法,??我們也可以使用webpack cli提供的watch的作業模式,如果之前了解過其它的構建工具,那應該對這種模式并不陌生,
在這種模式下,專案下的源檔案會被監視,一旦這些檔案發生變化,它就會??自動重新去運用打包任務,具體的用法也非常簡單,就是在啟動webpack命令時添加 –watch命令引數,????這樣的話,webpack就會以監視模式去運行,在打包完成過后,cli不會立即退出,??而是會等待檔案的變化,然后再次作業,??一直到手動結束這個cli,
這種模式下,??我們就只需要專注編碼,??不必再去手動完成這些重復的作業了,這里,可以再開啟一個新的命令列終端,??同時以http的形式去運行應用,??然后,我們打開瀏覽器去預覽,??嘗試修改源代碼,??以觀察模式作業的webpack就會自動重新打包,?重繪頁面,查看最新的頁面結果,?
自動重繪瀏覽器
-
使用browser-sync去監聽目錄,并自動重繪瀏覽器
browser-sync dist --files "**/*"
20. Webpack Dev Server
Webpack Dev Server是webpack官方推出的一個開發工具,根據名字,就應該知道它提供了一個開發服務器,并且,它將自動編譯和自動重繪瀏覽器等一系列對開發非常友好的功能全部集成在了一起,這個工具可以直接解決我們之前的問題,
因為這是一個高度集成的工具,所以它使用起來也非常的簡單,
-
打開命令列,以開發依賴安裝
yarn add webpack-dev-server --dev
它提供了一個webpack-dev-server的cli程式,那我們同樣可以直接通過yarn去運行這個cli,或者,可以把它定義到npm script中,運行這個命令 yarn webpack-dev-server,它內部會自動去使用webpack去打包應用,并且會啟動一個HTTP server去運行打包結果,在運行過后,它還會去監聽我們的代碼變化,一旦語言檔案發生變化,它就會自動立即重新打包,這一點,與watch模式是一樣的,不過這里也需要注意webpack-dev-serverr為了提高作業效率,它并沒有將打包結果寫入到磁盤當中,它是將打包結果,暫時存放在記憶體當中,而內部的HTTP server從記憶體當中把這些檔案讀出來,然后發送給瀏覽器,這樣一來的話它就會減少很多不必要的磁盤讀寫操作,從而大大提高我們的構建效率,
這里,我們還可以為這個命令傳入一個**–open**的引數,它可以用于去自動喚起瀏覽器,去打開我們的運行地址,打開瀏覽器過后(如果說你有兩塊螢屏的話),你就可以把瀏覽器放到另外一塊螢屏當中,然后,我們去體驗這種一邊編碼,一邊即時預覽的開發環境了,
yarn webpack-dev-server --open
21. Webpack Dev Server靜態資源訪問
web-dev-server默認會將構建結果輸出的檔案,全部作為開發服務器的資源檔案,也就是說,只要是通過webpack的打包能夠輸出的檔案,都可以正常被訪問到,但是如果說還有一些靜態資源也需要作為開發服務器的資源被訪問的,那就需要額外的去告訴webpack-dev-server,
它具體的方法就是在我們webpack.config.js的組態檔當中去添加一個對應的配置,在配置物件當中去添加一個devServer的屬性,這個屬性是專門用來為webpack制定相關的配置選項,可以通過這個配置物件的contentBase屬性來去制定額外的靜態資源路徑,這個屬性可以是一個字串或者是一個陣列,也就是說可以配置一個或者是多個路徑,這里將這個路徑設定為專案根目錄當中的public目錄,
devServer: {
contentBase: './public',
},
那可能有人會有疑問,因為之前(1.16 Webpack 插件使用總結),已經通過插件將這個目錄輸出了,那按照剛剛的說法,我們所有輸出的檔案都可以直接被server,也就是直接可以在瀏覽器端訪問到,那按道理來講的話,這里這些檔案就不需要再作為開發服務器的額外的資源路徑了,事實情況確實如此,如果說你能這么想的話,那也就證明你確實理解了這樣一個點,但是,我們在實際去使用webpack的時候,我們一般都會把copyWebpackPlugin這樣的插件留在上線前的那一次打包中使用,那在平時的開發程序當中,我們一般不會去使用它,這是因為在開發程序中我們會頻繁、重復執行打包任務,那假設我們需要拷貝的檔案比較多或者是比較大,如果說我們每次都去執行這個插件的話,我們打包程序當中的開銷就會比較大,速度自然也就會降低了,由于這是額外的話題,所以說具體的操作方式,就是具體怎么樣去讓我們在開發階段,不去使用copyWebpackPlugin,然后在上線前那一刻我們再去使用這種插件,那這種操作方式我們在后續再來介紹,
那這里先注釋掉copyWebpackPlugin,這樣確保在打包程序當中不會再去輸出public目錄當中的靜態資源檔案,然后回到命令,再次執行webpack-dev-server,啟動過后,此次public目錄當中并沒有被拷貝到輸出目錄,如果說webpack只去加載那些打包生成的檔案,那public目錄檔案應該是訪問不到的,但是通過剛才的contentBase已經將它指定為了額外的資源路徑,所以說應該可以訪問到,打開瀏覽器,去訪問頁面檔案以及bundle.js,都是來源于打包結果當中,然后再去嘗試訪問一下favicon.ico,這個檔案就是來源于contentBase當中所配置的public目錄了,除此之外,例如這個other.html檔案,它也是這個目錄當中所指定的檔案,以上,就是contentBase,它可以用來去為webpack額外去指定一個靜態資源目錄的操作方式,
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist')
},
devServer: {
contentBase: './public',
},
module: {
rules: [
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /.png$/,
use: {
loader: 'url-loader',
options: {
limit: 10 * 1024 // 10 KB
}
}
}
]
},
plugins: [
new CleanWebpackPlugin(),
// 用于生成 index.html
new HtmlWebpackPlugin({
title: 'Webpack Tutorials',
meta: {
viewport: 'width=device-width'
},
template: './src/index.html'
}),
// // 開發階段最好不要使用這個插件
// new CopyWebpackPlugin(['public'])
]
}
22. Webpack Dev Server 代理API
由于開發服務器的緣故,這里會將應用運行在localhost:8080,而最終上線過后,應用一般又和API會部署到同源地址下面,這樣就會有一個非常常見的問題,那就是在實際生產環境當中,可以直接去訪問API,但是回到開發環境當中就會產生跨域請求問題,
可能有人會說可以使用跨域資源共享的方式去解決這個問題,事實確實如此,如果請求的這個API支持CORS,這個問題就不成立了,但是,并不是每種情況下,服務端的API都一定要支持CORS的,如果說前后端同源部署,也就是我們的域名、協議、埠是一致,這種情況下根本沒有必要去開啟CORS,所以以上這個問題還是經常會出現,那解決這個問題最好的辦法就是在開發服務器當中去配置**代理(proxy)**服務,也就是把介面服務代理到本地的這個開發服務地址,webpack-dev-server它支持直接通過配置的方式去添加代理服務,


具體的用法如下,目標就是將github的API代理到本地的開發服務器當中,先在瀏覽器當中嘗試去訪問一下其中的一個介面https://api.github.com/users

Github的介面的Endpoint(可以理解為介面端點/入口),它一般都是在根目錄下,例如這里所使用的這個users這個Endpoint,知道了介面的地址過后,回到組態檔當中,在devServer當中去添加一個proxy屬性,這個屬性專門用來去添加代理服務配置的,這個屬性是一個物件,其中每一個屬性的就是一個代理規則的配置,那屬性的名稱,就是需要被代理的請求路徑前綴,也就是請求以哪個地址開始,它就會走代理請求,但一般為了容易辨別,都會將其設定為"/api",也就是請求開發服務器當中的"/api"開頭的這種地址,都會讓它代理到介面當中,
它的值是為這個前綴所匹配到的這個代理規則配置,將代理目標設定為"https://api.github.com",也就是說當請求以斜線開頭,代理目標就是https://api.github.com,此時如果去請求"http://localhost:8080/api/users",就相當于請求了"https://api.github.com/api/users",意思是請求的路徑是什么,它最終代理的這個地址、路徑是會完全一致的,
而實際需要請求的這個介面地址,實際上是在"https://api.github.com/users",也就是跟路徑下面的"users",所以說對于代理路徑當中的"/api",需要通過重寫的方式把它去掉,可以在這兒再去添加一個pathRewrite屬性,來去實作代理路徑的重寫,重寫規則就是把路徑當中以"/api"開頭的這個開頭的這段字串給它替換為空,pathRewrite屬性,它最侄訓以正則的方式來去替換請求的路徑,所以在這兒,以**"^"**表示開頭,
除此之外,還需要設定changeOrigin屬性為true,這是因為默認代理服務器的會以實際在瀏覽器當中請求的主機名,在這里就是localhost:8080作為代理請求的主機名,也就是在瀏覽器端對這個代理過后的這個地址發起請求,這個請求背后,它肯定還需要去請求到github服務器,請求的程序當中會帶一個主機名,這個主機名默認情況下使用的是用戶在瀏覽器端發起請求的主機名,也就是localhost:8080,而一般情況下服務器需要根據主機名去判斷這一臺主機名,因為一個請求請到服務器過后,服務器一般會有多個網站,它會根據主機名去判斷這個請求是屬于哪個網站,然后把這個請求指派到對應的網站,localhost:8080對于github來說肯定是不認識,所以說這里需要去修改,**“changeOrigin=true”**的這種情況下就會以實際代理請求發生的程序當中的主機名去請求,請求github的地址,真正請求的應該是https://api.github.com,所以說主機名就會保持原有狀態,這個時候,就不用再關心最終把它代理成什么樣,只需要去正常的請求就可以了,
webpack.config.js
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
...
devServer: {
contentBase: './public',
proxy: {
'/api': {
// http://localhost:8080/api/users -> https://api.github.com/api/users
target: 'https://api.github.com',
// http://localhost:8080/api/users -> https://api.github.com/users
pathRewrite: {
'^/api': ''
},
// 不能使用 localhost:8080 作為請求 GitHub 的主機名
changeOrigin: true
}
}
},
...
}

main.js中添加代理過后的地址:
// 跨域請求,雖然 GitHub 支持 CORS,但是不是每個服務端都應該支持,
// fetch('https://api.github.com/users')
fetch('/api/users') // http://localhost:8080/api/users
.then(res => res.json())
.then(data => {
data.forEach(item => {
const li = document.createElement('li')
li.textContent = item.login
ul.append(li)
})
})

23. Source Map介紹
通過構建編譯之類的操作,可以將開發階段的源代碼轉換為能夠在生產環境當中運行的代碼,這是一種進步,但是這種進步的同時,也就意味著在實際生產環境當中運行的代碼,與開發階段所撰寫的代碼之間會有很大的差異,在這種情況下,如果需要去除錯應用,又或是運行應用的程序當中出現了意料之外的錯誤,我們將無從下手,這是因為無論是除錯還是報錯,它都是基于轉換過后的代碼來進行的,Source map就是解決這一類問題最好的一個辦法,
其名字就已經表述了它的作用,叫做源代碼地圖,它是用來映射轉換過后的代碼與原代碼之間的一個關系,一段轉換過后的代碼,通過轉換程序當中生成的這個Source map檔案,就可以逆向得到源代碼,


目前,很多第三方的庫在去發布的檔案當中,都會有一個**".map"后綴的檔案,例如這里,可以打開jquery-3.4.1.min.map**檔案看一下,這是一個json格式的檔案,這個檔案里面記錄的就是轉換過后的代碼與轉換之前代碼之間的映射關系,
主要有這幾個屬性,簡單來看一下:
-
首先是version,它指的是當前這個檔案所使用的source map的標準的版本,
-
然后是**“sources”**屬性,這個屬性中記錄的是轉換之前源檔案的名稱,因為很有可能是多個檔案合并轉換為了一個檔案,所以說這里這個屬性是一個陣列,
-
再然后是**“name”**屬性,這個指的是源代碼當中使用的一些成員名稱,在壓縮代碼時,會將開發階段所撰寫的那些有意義的變數名替換為一些簡短的字符,從而去壓縮整體代碼的體積,這個屬性中記錄的是原始對應的那些名稱,
-
最后是**"mappings"的屬性,這個屬性其實是整個source map檔案的核心屬性,它是一個Base64-VLQ**編碼的一個字串,該字串記錄的資訊,就是轉換過后代碼當中的字符與轉換之前所對應的映射關系,
有這樣一個檔案后,一般會在轉換過后的代碼當中,通過添加一行注釋的方式來引入這個source map檔案,不過這個特性它只是用來幫助開發者更容易去除錯和定位錯誤的,所以說它對生產環境其實沒有什么太大的意義,在最新版的jquery中,已經去除了參考source map的注釋,這里想要去嘗試的話,需要手動的添加回來,

這里在jquery.min.js檔案當中,最后一行去添加一個注釋**"//# sourceMappingURL=jquery-3.4.1.min.map",這樣在瀏覽器當中如果打開了開發人員工具的,開發人員工具加載到的這個js檔案最后有這么一行注釋,它就會自動去請求這個source map**檔案,然后根據這個檔案的內容,逆向決議出來對應的源代碼,以便于除錯,同時因為有了映射的關系,當源代碼當中出現了錯誤,也就很容易能定位到源代碼當中對應的位置,
這里簡單總結一下,source map的它解決的就是在前端方向引入了構建編譯之類的概念過后,導致前端撰寫的源代碼與運行的代碼之間,不一樣所產生的那些除錯的問題,
24. Webpack 配置 Source Map
webpack打包程序同樣支持為打包結果生成對應的source map檔案,用法上也非常簡單,不過它提供了很多不同的模式,這就導致大部分的初學者可能會比較懵,將來一起去研究webpack中如何去配置使用source map以及它幾種不同模式之間的一些差異,
回到webpack.config.js組態檔當中,這里需要使用的一個配置屬性——devtool,這個屬性是用來去配置開發程序中的輔助工具,也就是與source map相關的一些功能配置,這里可以直接將這個屬性設定為source map,然后打開命令列終端,運行yarn webpack,打包完成過后,打開所生成的dist的目錄,此時在這個目錄當中就會生成bundle.js和它對應的map檔案,而且打開bundle.js,找到這個檔案的最后,這個檔案的最后也通過注釋的方式去引入了這個soft map檔案,


如果只是這么去使用,實際的效果就會差的比較遠,為什么這么說,因為截止到目前,webpack對source map的風格支持很多種,也就是說它有很多實作方式,那每種方式所生成的source map的效果,以及生成source map的速度都是不一樣的,很簡單也很明顯的一個道理就是效果最好的,一般它的生成速度也就會最慢,而速度最快的一般生成出來的這個source map檔案也就沒有什么效果,具體哪種方式才是最好或者說最適合的,后續還需要繼續去探索,
25. Websocket eval 模式的 Source Map
webpack.config.js中的devtool,它除了可以使用source-map,還支持很多其它的模式,具體的可以參考檔案當中有一個不同模式之間的一個對比表,
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ciIONFaG-1610550317517)(https://i.loli.net/2021/01/05/aoX4hPuYTcI1jmS.png)]
這張表中分別從初次構建速度、重新打包速度、是否適合在生產環境中使用以及所生成的方式、source map的質量這四個維度,去對比了這些不同方式之間的一些差異,表格當中對比可能不夠明顯,所以接下來配合表格中的介紹,通過具體的嘗試來去體會這些不同模式之間的差異,從而找到適合自己的最佳實踐,
首先來看一個叫做eval的模式,eval是js當中的一個函式,它可以用來去運行我們字串當中的js代碼,這里可以嘗試一下,通過一位去執行一個"console.log(123)",默認情況下這段代碼會運行在一個臨時的虛擬機當中,可以通過source URL來去宣告這段代碼所屬的檔案路徑,這里再來嘗試執行一下,在這段js代碼字串當中去添加一個注釋內容,就是**"#sourceURL=’./foo/bar.js’",回車執行,此時這段代碼它所運行的這個環境就是"./foo/bar.js",這也就意味著可以通過sourceURL**來去改變我們通過eval執行的這段代碼所屬的這種環境的一個名稱,其實它還是運行在這個虛擬環境當中,只不過它告訴了執行引擎我這段代碼所屬的這個檔案路徑,這只是一個標識而已,

了解了這樣一個特點過后,回到組態檔中,這里將devtool屬性設定為**“eval”**,也就是使用eval模式,然后回到命令列終端,再次運行打包,打包完成過后去運行一下這個應用,然后回到瀏覽器,重繪一下頁面,此時根據控制臺的提示,就能找到這個錯誤所出現的檔案,但是當打開這個檔案,看到的卻是打包過后的模塊代碼,那這是為什么?因為在這種模式下,它會將每一個模塊所轉換過后的代碼都放在eval函式當中去執行,并且在這個一位函式執行的字串的最后通過sourceURL的方式去說明所對應的檔案路徑,這樣的話瀏覽器再通過eval去執行這段代碼的時候就知道這段代碼所對應的源代碼是哪一個檔案,從而實作定位錯誤所出現的檔案,但只能去定位檔案,這種模式下它不會去生成source map檔案,也就是說實際上,跟source map沒有什么太大關系,所以說它的構建速度也就是最快的,但是它的效果也很簡單,它只能定位源代碼檔案的名稱,而不知道具體的行列資訊,
26. Webpack devtool模式對比
為了可以更好的對比不同模式的source map之間的差異,這里使用一個新的專案來同時創建出不同模式下的打包結果,然后通過具體的實驗來去橫向對比它們之間的差異,
目錄結構:

打開webpack.config.js的組態檔,在這個檔案當中已經提前定好了一個陣列,陣列中的每一個成員就是配置取值的一種,
const allModes = [
'eval',
'cheap-eval-source-map',
'cheap-module-eval-source-map',
'eval-source-map',
'cheap-source-map',
'cheap-module-source-map',
'inline-cheap-source-map',
'inline-cheap-module-source-map',
'source-map',
'inline-source-map',
'hidden-source-map',
'nosources-source-map'
]
回圈遍歷這個陣列,撰寫webpack.config.js內容,具體內容如下:
const HtmlWebpackPlugin = require('html-webpack-plugin')
const allModes = [
'eval',
'cheap-eval-source-map',
'cheap-module-eval-source-map',
'eval-source-map',
'cheap-source-map',
'cheap-module-source-map',
'inline-cheap-source-map',
'inline-cheap-module-source-map',
'source-map',
'inline-source-map',
'hidden-source-map',
'nosources-source-map'
]
module.exports = allModes.map(item => {
return {
devtool: item,
mode: 'none',
entry: './src/main.js',
output: {
filename: `js/${item}.js`
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
filename: `${item}.html`
})
]
}
})
配置一個html- webpack-plugin,也就是為每一個打包任務去生成一個HTML檔案,通過前面的了解,應該知道,html可以用來生成一個使用打包結果的html,待會兒,就是通過這些HTML在瀏覽器當中去嘗試這些不同的打包結果,
這樣的配置可以一次生成多個打包任務,對應js目錄中生成以陣列allModes中每個元素命名的js檔案,命令列通過yarn webpack運行結果如下:

命令列運行serve dist:

有了這些不同模式下的打包結果過后,接下來就可以一個一個仔細去對比了,這里先看幾個比較典型的模式,然后找出它們之間的一些關系,
-
eval模式
它就是將模塊代碼放到eval函式當中去執行,并且通過sourceURL標注這個模塊檔案的路徑,這種模式下它并沒有生成對應的source map,它只能定位是哪一個檔案出了錯誤;
-
eval-source-map模式
這個模式同樣也是使用eval函式去執行模塊代碼,不過這里有所不同的是,它除了可以幫找到錯誤出現的檔案,還可以定位到具體的行和列的資訊,因為在這種模式下相比于eval,它生成了source map;
-
cheap-eval-source-map模式
這個模式的名字差不多就可以推斷出來一些資訊,它其實就是在上面的的eval-source-map基礎之上加了一個cheap,用我們計算機行業經常說的一個詞就是閹割版的source map,為什么這么說,因為它雖然也生成了source map,但是這種模式下的source map,它只能幫我們定位導航而沒有列的資訊,也就是少了一點效果,它的生成速度自然也就會快很多;
-
cheap-module-eval-source-map模式
根據這個名字慢慢的就發現webpack的這些模式的名字好像不是亂起的,它好像有某種規律,這里其實就是cheap-eval-source-map的這個模式基礎之上多了一個module,在這種模式下的特點,可能乍一看不會那么明顯,因為它也就只能定位導航,這里再來把剛剛cheap-eval-source-map的這個模式也找出來,然后,仔細做一個對比,通過仔細對比你會發現,cheap-module-eval-source-map定位源代碼跟我們撰寫的源代碼是一模一樣的,而cheap-eval-source-map它顯示的是經過ES6轉換過后的結果,這樣的話這兩者之間的差異也就出來了,這也是為什么之前在配置的時候會給js檔案單獨配一個loader的原因,因為帶有module的這種模式下,它決議出來的源代碼是沒有經過loader的加工,也就是真正手寫的那些源代碼,而不帶module,它是加工過后的一個結果,如果說想要真正跟手寫代碼一樣的源代碼的話,就需要選擇cheap module這種模式;

了解了以上這些模式過后,基本上就可以算是通盤了解了所有的模式,因為其它的模式無外乎就是把這幾個特點再次排列組合罷了,例如,cheap-source-map,它沒有eval,也就意味著它沒有用eval的方式去執行模塊代碼,沒有module的話也就意味著它反過來的這個源代碼,是處理過后的代碼,
-
inline-source-map模式
它跟普通的其實效果上是一樣的,只不過source map的這個模式下,它的這個source map檔案是以物理檔案的方式存在,它使用的是data URL的方式去將我們的source map嵌入到的代碼當中,之前遇到的eval,它其實也是使用這種行內的這種方式把source map嵌入進來,那這種方式實際上我個人覺得是最不可能用到的,因為它把source map嵌入到源代碼當中過后,這個時候就導致這個代碼體積會變大很多,
-
hidden-source-map模式
這個模式下在開發工具當中是看不到效果的,但是回到開發工具當中,去找一下這個檔案,會發現它確實生成了source map檔案,這就跟jquery是一樣的,在構建程序當中生成的檔案,但是,它在代碼當中并沒有通過注冊的方式去引入這個檔案,所以說在開發工具當中看不到效果,這個模式實際上是在開發一些第三方包的時候會比較有用,我們需要去生成source map,但是不想在的這個代碼當中直接去應用它們,一旦當使用這個包的開發者出現了問題,它可以再把這個source map手動引入回來或者通過其它的方式去使用source map,Source map還有很多其它的使用方式,通過http_header也可以去使用,這些就不在這兒擴展了,
-
nosource-source-map模式
這個模式下能看到錯誤出現的位置,但是點擊這個錯誤資訊,點進去過后是看不到源代碼的,這個nosource指的就是沒有源代碼,但是它同樣提供了行列資訊,這樣的話對于我們來講,還是結合自己撰寫的源代碼找到錯誤出現的位置,只是在開發工具當中看不到源代碼,這是為了在生產環境當中去保護源代碼不會被暴露,
以上,介紹了很多種的source map,也做了一些具體的對比,通過這些對比,大家要能總結出來這個source map里面這幾個核心關鍵詞,它們的一些特點,然后,對于其它幾個模式沒有介紹到的,就很容易能知道它們一些特點了,可能了解很多的這些模式過后,對大家來講的,最痛苦的一件事情就是選擇一個合適的source map模式,這個問題,下面接著來看,
27. Webpack 選擇Source Map模式
雖然webpack可支持各種各樣的source map模式,但是其實掌握它們的特點過后,發現一般在應用開發時,只會用到其中的幾種,根本就沒必要在選擇上糾結,這里介紹一下個人在開發時的一些選擇,
首先,在開發模式下會選擇cheap-module-eval-source-map,原因有三點:

-
第一是撰寫代碼的風格一般會要求每一行代碼不會超過80個字符,source map能夠定位到行就夠了,因為每一行里面最多也就80字符,很容易找到對應的位置;
-
第二是使用框架的情況會比較多,以react和vue來說,無論是使用jsx還是vue的單檔案組件,loader轉換過后的代碼和轉換之前都會有很大的差別,這里需要去除錯轉換之前的源代碼,所以要選擇有module的方式;
-
第三是雖然cheap-module-eval-source-map的初次啟動就是打包啟動速度會慢一些,但是大多數時間都是在使用webpack-dev-server,以監視模式去重新打包,而不是每次都啟動打包,所以說這種模式下它重新打包速度比較快,
其次在生產模式下會選擇none,原因很簡單,因為source map會暴露源代碼到生產環境,這樣的話,但凡是有一點技術的人,都可以很容易去復原專案當中絕大多數的源代碼,這一點,其實被很多開發者可能都忽略掉了,它們就光認為source map能夠帶來便利,但是帶來這個便利的同時也會有一些隱患,其次,個人認為除錯和報錯找錯誤這些都應該是開發階段的事情,應該在開發階段就盡可能把所有的問題和隱患都找出來,而不是到了生產環境讓全民去幫忙公測,所以這種情況就盡量避免不在生產環境區域使用source map,如果說對你的代碼實在是沒有信心的話,那我建議你選擇nosource-map模式,這樣當出現錯誤時,在控臺當中就可以找到源代碼對應的位置,但是不至于去向外暴露的源代碼內容,
當然這個程序當中的選擇實際上也沒有絕對,去理解這些模式之間的差異的目的,就是為了可以在不同環境當中,快速去選擇一個合適的模式,而不是去尋求一個通用的法則,在開發行業沒有絕對的通用法則,
28. Webpack 自動重繪問題
在此之前已經簡單了解了webpack dev serve的一些基本用法和特性,但它主要就是為使用webpack構建的專案,提供了一個比較友好的開發環境和一個可以用來除錯的開發服務器,使用webpack就可以讓開發程序更加專注于編碼,因為它可以監視到代碼的變化,然后自動進行打包,最后再通過自動重繪的方式同步到瀏覽器,以便于即時預覽,但是當實際去使用這樣一個特性去完成一些具體的開發任務時,會發現這里還是會有一些不舒服的地方,例如在編輯器的應用,想在編輯其中輸入一些文字,然后手動調整css,希望試試更新輸入的文字樣式,但是這個時候編輯器當中的內容卻沒有了,這里不得不再來編輯器當中再去添加一些文本,那久而久之的話就會發現,自動重繪這樣一個功能還是很雞肋,它并沒有想象的那么好用,這是因為每次修改完代碼,webpack監視到檔案的變化過后就會自動打包,然后自動重繪到瀏覽器,一旦頁面整體重繪,那頁面中之前的任何操作狀態都會丟失,所以就會出現剛剛所看到的這樣一個情況,但是,聰明的人一般都會有一些小辦法,例如可以在代碼當中先去寫死一個文本到編輯器當中,這樣即便頁面重繪,也不會有丟失的這種情況出現,這些方法都需要去撰寫一些跟業務本身無關的一些代碼,更好的辦法自然是能夠在頁面不重繪的這種情況下,代碼也可以及時的更新進去,針對這樣的需求webpack同樣也可以滿足,接下來了解一下webpack當中如何去在頁面不重繪的情況下,及時的去更新代碼模塊,

29. Webpack HMR 體驗
HMR全稱是Hot Module Replacement,叫做模塊熱替換或者叫做模塊熱更新,計算機行業經常聽到一個叫做熱拔插的名詞,那指的就是可以在一個正在運行的機器上隨時去插拔設備,而機器的運行狀態是不會受插設備的影響,而且插上的設備可以立即開始作業,例如電腦上的USB埠就是可以熱拔插的,
模塊熱替換當中的這個熱,跟剛剛提到的熱拔插實際上是一個道理,它們都是在運行程序中的即時變化,那**webpack中的模塊熱替換指的就是可以在應用程式運行的程序中實時的去替換掉應用中的某個模塊,而應用的運行狀態不會因此而改變,**
例如在應用程式的運行程序中,修改了某個模塊,通過自動重繪就會導致整個應用整體的重繪,頁面中的狀態資訊都會丟失掉,而如果這個地方使用的是熱替換的話,就可以實作只將剛剛修改的這個模塊實時的去替換到應用當中,不必去完全重繪應用,
30. Webpack 開啟 HMR
對于熱更新這種強大的功能而言,操作并不算特別復雜,了解一下具體如何去使用,HMR已經集成到webpack-dev-server中,所以就不需要再去單獨安裝什么模塊,使用這個特性需要再去運行引數**–hot**開啟這個特性,
目錄結構:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-mkWmpMhK-1610550317539)(https://i.loli.net/2021/01/06/2Q8vErk6zp3BZU7.png)]
yarn webpack-dev-server --hot
也可以使用配置的方式去打開HMR熱更新
webpack.config.js
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'development',
entry: './src/main.js',
output: {
filename: 'js/bundle.js'
},
devtool: 'source-map',
devServer: {
hot: true
// hotOnly: true // 只使用 HMR,不會 fallback 到 live reloading
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.(png|jpe?g|gif)$/,
use: 'file-loader'
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'Webpack Tutorial',
template: './src/index.html'
}),
new webpack.HotModuleReplacementPlugin() // 必須開啟插件
]
}
修改editor.css檔案,發現瀏覽器并沒有重繪頁面,而且修改的內容也自動更新到瀏覽器上了,
31. Webpack 處理JS模塊熱替換
但是js檔案好像有問題,修改js檔案后,瀏覽器依然進行重繪,這是因為webpack-dev-server不知道如何去重新構建js模塊,這是需要手動進行配置,
進入webpack打包的主入口檔案main.js,添加如下代碼(只針對當前編輯器案例)
import createEditor from './editor'
import background from './better.png'
import './global.css'
const editor = createEditor()
document.body.appendChild(editor)
const img = new Image()
img.src = background
document.body.appendChild(img)
if (module.hot) { // 判斷hot是否開啟,防止js出現錯誤后頁面重繪后錯誤資訊不被保留
let hotEditor = editor // 預先保存editor用于下次熱更新使用
module.hot.accept('./editor.js', () => {
const value = hotEditor.innerHTML // 預先保存頁面狀態資訊(這里為編輯器輸入的文本資訊)
document.body.removeChild(hotEditor) // 移除原先的editor
hotEditor = createEditor() // 使用createEditor創建新的editor
hotEditor.innerHTML = value // 在新的editor中寫入上面保留的頁面狀態資訊
document.body.appendChild(hotEditor) // 將新的editor更新到頁面中
})
}
32. Webpack 處理圖片模塊熱替換
import createEditor from './editor'
import background from './better.png'
import './global.css'
const editor = createEditor()
document.body.appendChild(editor)
const img = new Image()
img.src = background
document.body.appendChild(img)
// ============ 以下用于處理 HMR,與業務代碼無關 ============
// console.log(createEditor)
if (module.hot) {
let lastEditor = editor
module.hot.accept('./editor', () => {
// console.log('editor 模塊更新了,需要這里手動處理熱替換邏輯')
// console.log(createEditor)
const value = lastEditor.innerHTML
document.body.removeChild(lastEditor)
const newEditor = createEditor()
newEditor.innerHTML = value
document.body.appendChild(newEditor)
lastEditor = newEditor
})
module.hot.accept('./better.png', () => {
img.src = background // 重新賦值圖片src路徑
})
}
33. Webpack 生產環境優化
前面了解到的一些用法和特性都是為了可以在開發階段,擁有更好的開發體驗,而這些體驗提升的同時,webpack打包結果也會隨之變得越來越臃腫,這是因為在這個程序中webpack為了實作這些特性,它會自動往打包結果中添加一些額外的內容,例如之前所使用到的source map和HMR,它們都會往輸出結果當中去添加額外的代碼來去實作各自的功能,但是這些額外的代碼對于生產環境來講是容易的,因為生產環境跟開發環境有了很大的差異,在生產環境中強調的是以更少量,更高效的代碼去完成業務功能,也就是說會更注重運行效率,而在開放環境中,只注重開發效率,那針對這個問題,webpack當中就推出了mode的用法,那它提供了不同模式下的一些預設配置,其中生產模式中就已經包括了很多在生產環境當中所需要的優化配置,同時webpack也建議我們為不同的作業環境去創建不同的配置,以便于讓打包結果可以適用于不同的環境,接下來一起來去探索一下生產環境中有哪些值得優化的地方以及一些注意事項,
34. Webpack 不同環境下的配置
嘗試為不同的作業環境以創建不同的webpack配置,創建不同的環境配置的方式主要有兩種:
-
第一種是在組態檔中添加相應的配置判斷條件,根據環境的判斷條件的不同匯出不同的配置,
webpack組態檔支持匯出函式,函式中回傳所需要的的配置物件,函式接受兩個引數,第一個是env(cli傳遞的環境名引數),第二個是argv(運行cli程序中傳遞的所有引數),可以借助這樣一個特點來去實作不同的開發環境和生產環境分別回傳不同的配置,
const webpack = require('webpack') const { CleanWebpackPlugin } = require('clean-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') const CopyWebpackPlugin = require('copy-webpack-plugin') module.exports = (env, argv) => { const config = { mode: 'development', entry: './src/main.js', output: { filename: 'js/bundle.js' }, devtool: 'cheap-eval-module-source-map', devServer: { hot: true, contentBase: 'public' }, module: { rules: [ { test: /\.css$/, use: [ 'style-loader', 'css-loader' ] }, { test: /\.(png|jpe?g|gif)$/, use: { loader: 'file-loader', options: { outputPath: 'img', name: '[name].[ext]' } } } ] }, plugins: [ new HtmlWebpackPlugin({ title: 'Webpack Tutorial', template: './src/index.html' }), new webpack.HotModuleReplacementPlugin() ] } if (env === 'production') { config.mode = 'production' config.devtool = false config.plugins = [ ...config.plugins, // ES6將幾個陣列組合起來,生產環境下需要clean-webpack-plugin和copy-webpack-plugin new CleanWebpackPlugin(), new CopyWebpackPlugin(['public']) ] } return config }命令列運行:yarn webpack,當沒有傳遞env引數時,webpack會默認mode為開發階段(development),對應的public下的檔案不會被復制,
命令列運行:yarn webpack --env production,傳遞env引數后,webpack以生產環境(production)進行打包,額外的插件會作業,public目錄下的檔案會被復制,
這就是通過在匯出函式中對環境進行判斷,從而去實作為不同的環境倒出不同的配置,當然也可以直接在全域去判斷環境變數,然后直接匯出不同的配置,這樣也是可以的,
-
第二種是為不同的環境單獨添加一個組態檔,確保每一個環境下面都會有一個對應的組態檔,
通過判斷環境引數資料回傳不同的配物件,這種方式只適用于中小型專案,因為一旦專案變得復雜,組態檔也會一起變得復雜起來,所以說對于大型的專案,還是建議大家使用不同環境去對應不同組態檔的方式來實作,一般在這種方式下面,專案當中至少會有三個webpack組態檔,其中兩個(webpack.dev.js/webpack.prod.js)是用來適配不同的環境的,那另外一個是一個公共的配置(webpack.common.js),因為開發環境和生產環境并不是所有的配置都完全不同,所以說需要一個公共的檔案來去抽象兩者之間相同的配置,
專案目錄:

webpack.common.js
const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { entry: './src/main.js', output: { filename: 'js/bundle.js' }, module: { rules: [ { test: /\.css$/, use: [ 'style-loader', 'css-loader' ] }, { test: /\.(png|jpe?g|gif)$/, use: { loader: 'file-loader', options: { outputPath: 'img', name: '[name].[ext]' } } } ] }, plugins: [ new HtmlWebpackPlugin({ title: 'Webpack Tutorial', template: './src/index.html' }) ] }webpack.dev.js
const webpack = require('webpack') const merge = require('webpack-merge') const common = require('./webpack.common') module.exports = merge(common, { mode: 'development', devtool: 'cheap-eval-module-source-map', devServer: { hot: true, contentBase: 'public' }, plugins: [ new webpack.HotModuleReplacementPlugin() ] })webpack.prod.js
const merge = require('webpack-merge') const { CleanWebpackPlugin } = require('clean-webpack-plugin') const CopyWebpackPlugin = require('copy-webpack-plugin') const common = require('./webpack.common') module.exports = merge(common, { mode: 'production', plugins: [ new CleanWebpackPlugin(), new CopyWebpackPlugin(['public']) ] })webpack-merge提供了更加智能的配置合并,使用yarn add webpack-merge --dev安裝到生產環境中,將common中的配置分別于dev和prod組合,生產新的配置,
命令列運行
yarn webpack --config webpack.prod.js # --config用于指定組態檔 # 或者 yarn webpack --config webpack.dev.js如果覺得使用命令列太過麻煩,也可以在package.json進行配置
"scripts": { "prod": "webpack --config webpack.prod.js", "dev": "webpack --config webpack.dev.js" },隨后命令列運行
yarn prod # 或者yarn dev
35. Webpack DefinePlugin
webpack4中新增的production模式下,內部新增了很多通用的優化功能,對于使用者而言,這種開箱即用的體驗是非常棒的,但是對于學習者而言這種開箱即用會導致學習者忽略很多需要了解的東西,以至于出現問題后無從下手,如果需要深入了解webpack的使用,建議可以單獨研究一下每個配置背后的作用,這里先學習幾個主要的優化配置,順便去了解webpack是如何優化打包結果的,
-
DefinePlugin
為代碼注入全域成員,production模式下DefinePlugin會被啟用,并且往代碼中注入了一個常量:process.ev.NODE_ENV,很多第三方模塊都是通過這個成員去判斷當前的運行環境,從而去決定是否執行例如列印日志的這些操作,下面單獨使用這個插件,
webpack.config.js
const webpack = require('webpack') // DefinePlugin為webpack內置插件 module.exports = { mode: 'none', entry: './src/main.js', output: { filename: 'bundle.js' }, plugins: [ new webpack.DefinePlugin({ // 值要求的是一個代碼片段,該物件中每一個鍵值都會被注入到代碼中 // API_BASE_URL: 'https://api.example.com' // 錯誤寫法 第一步寫法 // API_BASE_URL: '"https://api.example.com"' // 正確寫法 API_BASE_URL: JSON.stringify('https://api.example.com') }) ] }main.js
console.log(API_BASE_URL)bundle.js
// 錯誤寫法 /***/ (function(module, exports) { console.log(https://api.example.com) // 按照第一步寫法,報錯,非JavaScript代碼段 /***/ }) /******/ ]); // 正確寫法 /***/ (function(module, exports) { console.log("https://api.example.com") // 按照第二部或第三部寫法,正常 /***/ }) /******/ ]);
36. Webpack Tree Shaking
Tree-shaking字面意義是“搖樹”,伴隨著搖樹,樹上的枯葉就會掉落下來,這里的Tree-shaking【搖掉】的是代碼中未參考的部分,這部分代碼叫做未參考代碼(dead code),Webpack生產模式優化中就有這樣一個有用的功能,它可以檢測出代碼中未參考的代碼,然后移除掉它們,
compontents.js
export const Button = () => {
return document.createElement('button')
console.log('dead-code') // 未參考代碼
}
// 未參考代碼,index.js中沒有匯入
export const Link = () => {
return document.createElement('a')
}
// 未參考代碼,index.js中沒有匯入
export const Heading = level => {
return document.createElement('h' + level)
}
index.js
import { Button } from './components'
document.body.appendChild(Button())
通過yarn webpack --mode production打包后發現,console.log(‘dead code’)以及其它兩個組件壓根沒有輸出到bundle.js,這是因為Tree-shaking在生產模式下自動開啟,
37. Webpack 使用Tree Shaking
需要注意的hiTree-shaking并不是webpack中的某一個配置選項,它是一組功能搭配使用過后的使用效果,這種功能會在生產模式下自動啟用,但是由于目前官方檔案中對Tree-shaking的介紹有點混亂,所以這里再來介紹一下它在其它模式下如何一步一步的手動的開啟,順便通過這個程序,了解Tree-shaking的作業程序以及它的優化功能,
之前在沒有啟用production作業模式時,生成的bundle.js部分代碼如下,其中未參考到的Link及Heading都被輸出到bundle.js中,
document.body.appendChild(Object(_components__WEBPACK_IMPORTED_MODULE_0__["Button"])())
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Button", function() { return Button; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Link", function() { return Link; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Heading", function() { return Heading; });
const Button = () => {
return document.createElement('button')
console.log('dead-code')
}
const Link = () => {
return document.createElement('a')
}
const Heading = level => {
return document.createElement('h' + level)
}
/***/ }
然后在webpack.config.js中添加如下配置optimization,該物件集中配置webpack優化功能,
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
optimization: { // 集中配置webpack優化功能
// 模塊只匯出被使用的成員
usedExports: true,
// 盡可能合并每一個模塊到一個函式中
concatenateModules: true,
// 壓縮輸出結果
// minimize: true
}
}
隨后繼續運行yarn webpack打包命令,輸出bundle.js部分如下:
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
// ESM COMPAT FLAG
__webpack_require__.r(__webpack_exports__);
// CONCATENATED MODULE: ./src/components.js
const Button = () => {
return document.createElement('button')
console.log('dead-code')
}
const Link = () => {
return document.createElement('a')
}
const Heading = level => {
return document.createElement('h' + level)
}
// CONCATENATED MODULE: ./src/index.js
document.body.appendChild(Button())
/***/ }
通過對比代碼發現,未開啟優化時,bundle.js將三個組件全部匯入,然后使用**document.body.appendChild(Object(_componentsWEBPACK_IMPORTED_MODULE_0[“Button”])())創建組件,而開啟優化后(為方便觀察,先關閉minimize),bundle.js直接使用document.body.appendChild(Button())**創建組件,實際上打開minimize后,在壓縮代碼中完全找不到Link以及Heading組件,
如果把代碼看做【大樹】,可以理解為usedExports將枯葉標記起來,而minimize負責把【枯葉】搖下來,
其中concatenateModules負責盡可能合并每一個模塊到一個函式中,未開啟時一個模塊為一個函式,這個作用又被稱之為Scope Hoisting(作用域提升),它是webpack3中添加的特性,此時再配合minimize,這樣代碼體積又會減小很多,
38. Webpack Tree Shaking 于Babel
由于早期webpack發展非常快,變化比較多,所有找資料時得到的結果并不一定適用于當前所使用的版本,對于Tree-shaking的資料更是如此,很多資料中表示如果使用babel-loader就會導致Tree-shaking失效,這里說明一下,首先需要明確的是Tree-shaking的實作,前提是必須使用ES Module組織代碼,也就是說,交給webpack打包的代碼必須使用ESM的方式來去實作的模塊化,原因是webpack在打包所有模塊之前,先將模塊根據不同的配置交給不同的loader去處理,最后再將所有loader處理過后的結果打包在一起,為了轉換ECMAScript新特性,很多時候會選擇babel-loader去處理JavaScript,babel-loader轉換代碼時有可能會將ES Module處理成CommonJS,取決于是否使用轉換ES Module的插件,例如之前使用的**"@babel/preset-env"**,其中就有這樣一個插件去將ESM轉換為CommonJS,這樣webpack打包時,拿到的代碼就是以commonJS組織的代碼,所以說Tree-shaking會失效,
-
實驗一:開啟bebel-loader,驗證Tree-shaking是否會失效
webpack.config.js
module.exports = { mode: 'none', entry: './src/index.js', output: { filename: 'bundle.js' }, module: { rules: [ { test: /\.js$/, use: { loader: 'babel-loader', options: { presets: [ "@babel/preset-env" // 插件集合 ] } } } ] }, optimization: { // 模塊只匯出被使用的成員 usedExports: true, // 盡可能合并每一個模塊到一個函式中 // concatenateModules: true, // 壓縮輸出結果 // minimize: true } }yarn webpackbundle.js
/***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return Button; }); /* unused harmony export Link */ /* unused harmony export Heading */ var Button = function Button() { return document.createElement('button'); console.log('dead-code'); }; var Link = function Link() { return document.createElement('a'); }; var Heading = function Heading(level) { return document.createElement('h' + level); }; /***/ })結論:當開啟bebel-loader時,Tree-shaking正常作業,當使用minimize后,未參考的代碼將被洗掉掉,與上面的描述不符,這是因為最新版本中babel-loader中自動關閉了ES Module轉換插件,
探索原始碼:
node_modules\babel-loader\lib\injectCaller.js部分代碼
module.exports = function injectCaller(opts, target) { if (!supportsCallerOption()) return opts; return Object.assign({}, opts, { caller: Object.assign({ name: "babel-loader", // Provide plugins with insight into webpack target. // https://github.com/babel/babel-loader/issues/787 target, // Webpack >= 2 supports ESM and dynamic import. supportsStaticESM: true, // 當前環境支持ES Module supportsDynamicImport: true, // Webpack 5 supports TLA behind a flag. We enable it by default // for Babel, and then webpack will throw an error if the experimental // flag isn't enabled. supportsTopLevelAwait: true }, opts.caller) }); };node_modules@babel\preset-env\lib\index.js部分代碼
const modulesPluginNames = getModulesPluginNames({ modules, transformations: _moduleTransformations.default, shouldTransformESM: modules !== "auto" || !(api.caller == null ? void 0 : api.caller(supportsStaticESM)), //禁用ESM的轉換 shouldTransformDynamicImport: modules !== "auto" || !(api.caller == null ? void 0 : api.caller(supportsDynamicImport)), shouldTransformExportNamespaceFrom: !shouldSkipExportNamespaceFrom, shouldParseTopLevelAwait: !api.caller || api.caller(supportsTopLevelAwait) });所以webpack最終打包時得到的依然是ES Module的代碼,所以Tree-shaking還是作業的,
-
實驗二:配置babel-loader,強制開啟ES Module轉換,驗證Tree-shaking是否會失效
webpack.config.js
module.exports = { mode: 'none', entry: './src/index.js', output: { filename: 'bundle.js' }, module: { rules: [ { test: /\.js$/, use: { loader: 'babel-loader', options: { presets: [ // 如果 Babel 加載模塊時已經轉換了 ESM,則會導致 Tree Shaking 失效 ['@babel/preset-env', { modules: 'commonjs' }] // 強制使用babel esm插件,將代碼中的esm轉換為CommonJs // ['@babel/preset-env', { modules: false }] // 也可以使用默認配置,也就是 auto,這樣 babel-loader 會自動關閉 ESM 轉換 // ['@babel/preset-env', { modules: 'auto' }] // 根據環境判斷是否開啟ES Module插件 ] } } } ] }, optimization: { // 模塊只匯出被使用的成員 usedExports: true, // 盡可能合并每一個模塊到一個函式中 // concatenateModules: true, // 壓縮輸出結果 // minimize: true } }bundle.js
exports.Heading = exports.Link = exports.Button = void 0; var Button = function Button() { return document.createElement('button'); console.log('dead-code'); }; exports.Button = Button; var Link = function Link() { return document.createElement('a'); }; exports.Link = Link; var Heading = function Heading(level) { return document.createElement('h' + level); }; exports.Heading = Heading;發現usedExport未生效,其匯出所有成員,包含未參考的成員,開啟壓縮袋嗎Tree-shaking也沒辦法作業,
**總結:**最新版本的bebel-loader并不會導致Tree-shaking失效,如果不確定,最簡單的辦法是將preset-env中的module改為false,確保preset-env不會開啟ES Module轉換插件,這樣也確保了Tree-shaking作業的前提,另外,上述實驗程序也值得琢磨,通過這樣會的探索可以了解到很多知其所以然的內容,
['@babel/preset-env', { modules: false }]
39. Webpack sideEffects及注意
webpack4中還新增了一個sideEffects的新特性,它允許通過配置的方式去標識代碼是否有副作用,從而為Tree-shaking提供更大的壓縮空間,副作用是指模塊執行的時候,除了匯出成員是否還做了一些其它的事情,這個特性一般只有在去開發一個NPM模塊時才會用到,但是因為官網當中把sideEffects的介紹跟Tree-shaking混到了一起,所以很多人誤認為它倆是因果關系,其實它倆真的沒有那么大的關系,
這里把sideEffects弄明白,你就能理解為什么了,這里先設計一個能夠讓sideEffects發揮效果的一個場景:
目錄結構:

基于剛剛的這個案例基礎之上,把components拆分出了多個組件檔案(button.js/heading.js/link.js),然后在index.js當中集中匯出,便于外界匯入,這是一種非常常見的同類檔案組織方式,回到入口檔案當中去匯入components中的組件,
index.js打包入口檔案
import { Button } from './components'
// 樣式檔案屬于副作用模塊
import './global.css'
// 副作用模塊
import './extend'
console.log((8).pad(3))
document.body.appendChild(Button())
這樣就會出現一個問題,因為在這載入的是components這個目錄下的index.js,index.js入口檔案中又載入了所有的組件模塊,這就會導致只想匯入button組件,但是所有的組件模塊都會被加載執行,打開命令列終端,然后嘗試運行打包,打包完成過后找到打包結果,你會發現所有組件的模塊確實都被打包進了bundle.js,

sideEffects特性就可以用來解決此類問題,打開webpack.config.js的組態檔,在optimization屬性當中去開啟sideEffects屬性,注意這個特性在production模式下同樣也會自動開啟,開啟這個特性過后,webpack在打包時就會先檢查當前代碼,當前專案所屬的這個package.json中有沒有sideEffects的標識,以此來判斷這個模塊是否有副作用,如果說這個模塊沒有副作用,那這些沒有用到的模塊就不再會打包,可以打開package.json,然后嘗試去添加一個sideEffects欄位,把它設定false,這樣的話就標識當前這個package.json所影響的這個專案,它當中所有的代碼都沒有副作用,一旦這些沒有用的模塊它沒有副作用了,它就會被移除掉,


完了以后再打開命令列終端,然后再次運行打包,打包過后同樣找到打包輸出的bundle.js,此時那些沒有用到的模塊就不再會被打包進來了,那這就是的sideEffects作用,注意這里設定了兩個地方,先在webpack.config.js的配置當中去開啟的sideEffects,它是用來去開啟這個功能,而在package.json們添加的sideEffects它是用來標識專案代碼是沒有副作用的,它倆不是一個意思,不要弄混了
sideEffects注意事項:
使用sideEffects這個功能的前提就是確定你的代碼沒有副作用,否則的話,在webpack打包時就會誤刪掉那些有副作用的代碼,例如這里準備了一個extend.js一個檔案,在這個當中并沒有向外匯出任何成員,它僅僅是在number這個物件的原型上掛載了一個方法,用來為數字去添加前面的倒零,這是一種非常常見的基于原型的擴展方法,
// 為 Number 的原型添加一個擴展方法
Number.prototype.pad = function (size) {
// 將數字轉為字串 => '8'
let result = this + ''
// 在數字前補指定個數的 0 => '008'
while (result.length < size) {
result = '0' + result
}
return result
}
回到index.js當中去匯入這個extend.js,但因為這個模塊確實沒有匯出任何成員,所以說這里也就不需要去提取任何成員,只不過在匯入這個模塊過后就可以使用,它為number所提供的擴展方法了,這里為number做擴展的這樣一個操作,就屬于extend.js這個模塊的副作用,因為在匯入的這個模塊過后,number的原型上就會多一個方法,這就是副作用,
import { Button } from './components'
// 樣式檔案屬于副作用模塊
import './global.css'
// 副作用模塊
import './extend'
console.log((8).pad(3))
document.body.appendChild(Button())
此時如果說還表示專案當中所有的代碼都沒有副作用的話,再次回到命令列運行打包,打包過后,找到打包結果,這個時候就會發現剛剛的這個擴展的操作,它是不會被打包進來的,因為它是副作用代碼,但是在的配置當中已經宣告了沒有副作用,所以它們就被移除掉了,除此之外,還有在代碼當中載入的css模塊,都屬于副作用模塊,同樣會面臨剛剛這樣一種問題,
解決的辦法就是在package.json當中,關掉副作用或者是標識一下當前這個專案當中哪些檔案是有副作用的,這樣的話webpack就不會去忽略這些有副作用的模塊,打開package.json,把false改成一個陣列,然后再去添加一下extend.js的路徑,還有這個global.css的檔案路徑,這里也可以使用路徑通配符的方式來去配置,
{
"name": "31-side-effects",
"version": "0.1.0",
"main": "index.js",
"author": "leo ",
"license": "MIT",
"scripts": {
"build": "webpack"
},
"devDependencies": {
"css-loader": "^3.2.0",
"style-loader": "^1.0.0",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.9"
},
"sideEffects": [
"./src/extend.js",
"*.css"
]
}
完成以后再次打開命令列終端運行打包,此時在找bundle.js中發現,這個有副作用的兩個模塊也會被同時打包進來了,以上就是對webpack和內置的一些優化屬性的一些介紹,總之,這些特性,它都是為了彌補webpack的早期在設計上的一些遺留問題,這一類的技術的發展確實越來越好,
40. Webpack 代碼分割
通過webpack實作前端專案整體模塊化的優勢很明顯,但是它同樣存在一些弊端,那就是專案當中所有的代碼最終都會被打包到一起,試想一下,如果說應用非常復雜,模塊非常多的話,那打包結果就會特別的大,很多時候超過兩三兆也是非常常見的事情,而事實情況是,大多數時候在應用開始作業時,并不是所有的模塊都是必須要加載進來的,但是,這些模塊又被全部打包到一起,需要任何一個模塊,都必須得把整體加載下來過后才能使用,而應用一般又是運行在瀏覽器端,這就意味著會浪費掉很多的流量和帶寬,
更為合理的方案就是把的打包結果按照一定的規則去分離到多個bundle.js當中,然后根據應用的運行需要,按需加載這些模塊,這樣的話就可以大大提高應用的回應速度以及它的運行效率,可能有人會想起來在一開始的時候說過webpack就是把專案中散落的那些模塊合并到一起,從而去提高運行效率,那這里又在說它應該把它分離開,這兩個說法是不是自相矛盾?其實這并不是矛盾,只是物極必反而已,資源太大了也不行,太碎了更不行,專案中劃分的這種模塊的顆粒度一般都會非常的細,很多時候一個模塊只是提供了一個小小的工具函式,它并不能形成一個完整的功能單元,如果不把這些散落的模塊合并到一起,就有可能再去運行一個小小的功能時,會加載非常多的模塊,而目前所主流的這種HTTP1.1協議,它本身就有很多缺陷,例如并不能同時對同一個域名下發起很多次的并行請求,而且每一次請求都會有一定的延遲,另外每次請求除了傳輸具體的內容以外,還會有額外的header請求頭和回應頭,當大量的這種請求的情況下,這些請求頭加在一起,也是很大的浪費,
綜上所述,模塊打包肯定是有必要的,不過像應用越來越大過后,要開始慢慢的學會變通,為了解決這樣的問題,webpack支持一種分包的功能,也可以把這種功能稱之為代碼分割,它通過把模塊,按照所設計的一個規則打包到不同的bundle.js當中,從而去提高應用的回應速度,目前的webpack去實作分包的方式主要有兩種:
-
第一種就是根據業務去配置不同的打包入口,也就是會有同時多個打包入口同時打包,這時候就會輸出多個打包結果;
-
第二種就是采用ES Module的動態匯入的功能,去實作模塊的按需加載,這個時候webpack會自動的把動態匯入的這個模塊單獨輸出的一個bundle.js當中,
41. Webpack 多入口打包
多入口打包一般適用于傳統的“多頁”應用程式,最常見的劃分規則是一個頁面對應一個打包入口,對于不同頁面之間公共的部分再去提取到公共的結果中,
目錄結構

一般webpack.config.js組態檔中的entry屬性只會一個檔案路徑(打包入口),如果需要配置多個打包入口,則需要將entry屬性定義成為一個物件(注意不是陣列,如果是陣列的話,那就是將多個檔案打包到一起,對于整個應用來講依然是一個入口),一旦配置為多入口,輸出的檔案名也需要修改**"[name].bundle.js**",[name]最侄訓被替換成入口的名稱,也就是index和album,
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'none',
entry: {
index: './src/index.js', // 多入口
album: './src/album.js'
},
output: {
filename: '[name].bundle.js' // [name]占位符,最終被替換為入口名稱index和album
},
optimization: {
splitChunks: {
// 自動提取所有公共模塊到單獨 bundle
chunks: 'all'
}
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/index.html',
filename: 'index.html',
}),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/album.html',
filename: 'album.html',
})
]
}
命令列運行yarn webpack命令,打開dist目錄發現已經有兩個js檔案,打開html檔案,發現兩個html檔案都引入了兩個js檔案,但需求是各自引入各自的js/css檔案,所以這里需要進一步處理,在html-webpack-plugin插件中增加chunks屬性,其值為對應需要引入的js檔案入口名稱,

...
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/index.html',
filename: 'index.html',
chunks: ['index']
}),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/album.html',
filename: 'album.html',
chunks: ['album']
})
]
42. Webpack 提取公共模塊
多入口打包本身非常容易理解,也非常容易使用,但是它也存在一個小小的問題,就是在不同的打包入口當中,它一定會有那么一些公共的部分,按照目前這種多入口的打包方式,不同的打包結果當中就會出現相同的模塊,例如在我們這里index入口和album入口當中就共同使用了global.css和fetch.js這兩個公共的模塊,因為實體比較簡單,所以說重復的影響不會有那么大,但是如果共同使用的是jQuery或者Vue這種體積比較大的模塊,那影響的話就會特別的大,所以說需要把這些公共的模塊去,提取到一個單獨的bundle.js當中,webpack中實作公共模塊提取的方式也非常簡單,只需要在優化配置當中去開啟一個叫splitChunks的一個功能就可以了,回到組態檔當中,配置如下:
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'none',
entry: {
index: './src/index.js',
album: './src/album.js'
},
output: {
filename: '[name].bundle.js'
},
optimization: {
splitChunks: {
// 自動提取所有公共模塊到單獨 bundle
chunks: 'all' // 表示會把所有的公共模塊都提取到單獨的bundle.js當中
}
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/index.html',
filename: 'index.html',
chunks: ['index']
}),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/album.html',
filename: 'album.html',
chunks: ['album']
})
]
}
打開命令列運行yarn webpack后發現,公共模塊的部分被打包進album~index.bundle.js中去了,
43. Webpack 動態匯入
按需加載是開發瀏覽器應用當中一個非常常見的需求,一般常說的按需加載指的是加載資料,這里所說的按需加載指的是在應用運行程序中需要某個模塊時才去加載這個模塊,這種方式可以極大的節省帶寬和流量,webpack支持使用動態匯入的這種方式來去實作模塊的按需加載,而且所有動態匯入的模塊都會被自動提取到單獨的bundle.js當中,從而實作分包,相比于多入口的方式,動態匯入更為靈活,因為通過代碼的邏輯去控制,需不需要加載某個模塊,或者是時候加的某個模塊,而分包的目的中就有很重要的一點就是:讓模塊實作按需加載,從而去提高應用的回應速度,
具體來看如何使用,這里已經提前設計好了一個可以發揮按需加載作用的場景,在這個頁面的主體區域,如果訪問的是文章頁的話,得到的就是一個文章串列,如果訪問的是相冊頁,顯示的就是相冊串列,
專案目錄:

動態匯入使用的就是ESM標準當中的動態匯入,在需要動態匯入組件的地方,通過這個函式匯入指定的路徑,這個方法回傳的就是一個promise,promise的方法當中就可以拿到模塊物件,由于網站是使用的默認匯出,所以說這里需要去解構模塊物件當中的default,然后把它放到post的這個變數當中,拿到這個成員過后,使用mainElement.appendChild(posts())創建頁面元素,album組件也是如此,完成以后再次回到瀏覽器,此時頁面仍然可以正常作業的,
// import posts from './posts/posts'
// import album from './album/album'
const render = () => {
const hash = window.location.hash || '#posts'
console.log(hash)
const mainElement = document.querySelector('.main')
mainElement.innerHTML = ''
if (hash === '#posts') {
// mainElement.appendChild(posts())
// 這個方法回傳的就是一個promise,promise的方法當中就可以拿到模塊物件,由于網站是使用的默認匯出,所以說這里需要去解構模塊物件當中的default,然后把它放到post的這個變數當中
import('./posts/posts').then(({ default: posts }) => {
mainElement.appendChild(posts())
})
} else if (hash === '#album') {
// mainElement.appendChild(album())
import('./album/album').then(({ default: album }) => {
mainElement.appendChild(album())
})
}
}
render()
window.addEventListener('hashchange', render)
這時再回到開發工具當中,然后重新去運行打包,然后去看看此時打包的結果是什么樣子的,打包結束,打開dist目錄,此時dist目錄下就會多出3個js檔案,那這三個js檔案,實際上就是由動態匯入自動分包所產生的,這3個檔案的分別是剛付訓入的兩個模塊index.js/album.js,以及這兩個模塊當中公共模塊fetch.js,

動態匯入整個程序無需配置任何一個地方,只需要按照ESM動態匯入成員的方式去匯入模塊就可以,內部會自動處理分包和按需加載,如果說你使用的是單頁應用開發框架,比如react或者Vue的話,在你專案當中的路由映射組件,就可以通過這種動態匯入的方式實作按需加載,
44. Webpack 魔法注釋
默認通過動態匯入產生的bundle.js檔案,它的名稱只是一個序號,但這并沒有什么不好的,因為在生產環境當中,大多數時候是根本不用關心資源檔案的名稱是什么,但是如果說還是需要給這些bundle.js命名的話,可以使用webpack所特有的魔法注釋是來去實作,
// import posts from './posts/posts'
// import album from './album/album'
const render = () => {
const hash = window.location.hash || '#posts'
console.log(hash)
const mainElement = document.querySelector('.main')
mainElement.innerHTML = ''
if (hash === '#posts') {
// mainElement.appendChild(posts())
// /* webpackChunkName: 'components' */'魔法注釋,特定格式
import(/* webpackChunkName: 'posts' */'./posts/posts').then(({ default: posts }) => {
mainElement.appendChild(posts())
})
} else if (hash === '#album') {
// mainElement.appendChild(album())
import(/* webpackChunkName: 'album' */'./album/album').then(({ default: album }) => {
mainElement.appendChild(album())
})
}
}
render()
window.addEventListener('hashchange', render)
特定格式:webpackChunkName:‘components’,這樣就可以給分包所產生的幫的起上名字了,再次打開命令列終端運行webpack打包,此時生成的bundle.js檔案它的name會使用剛剛注釋當中所提供的名稱了,

如果webpackChunkName是相同的,最終就會被打包到一起,例如這里可以把這兩個webpackChunkName設定為components,然后再次運行打包,此時,這兩個模塊它都會被打包進components.bundle.js檔案,借助于這樣一個特點,就可以根據自己的實際情況,靈活組織動態加載的模塊所輸出的檔案了,

45. Webpack MiniCssExtractPlugin
MiniCssExtractPlugin是一個可以將css代碼從打包結果當中提取出來的插件,通過這個插件就可以實作css模塊的按需加載,它的使用也非常簡單,回到專案當中,先執行yarn add mini-css-extract-plugin,打開webpack的組態檔,首先需要先匯入這個插件的模塊,匯入過后就可以將這個插件添加到配置物件的plugins陣列當中,這樣的話,它在作業時就會自動提取代碼當中的css到一個單獨的檔案當中,除此以外,目前所使用的樣式模塊,它是先交給css-loader去決議,然后交給style-loader的去處理,它的作用就是將樣式代碼通過style標簽的方式注入到頁面當中,從而使樣式可以作業,MiniCssExtractPlugin的話,樣式就會單獨存放到檔案當中,也就不需要加style標簽,而是直接通過link的方式去引入,所以這里就不再需要style-loader,取而代之使用的是MiniCssExtractPlugin所提供的一個MiniCssExtractPlugin.loader,來實作樣式檔案,通過link標簽的方式去引入,
webpack.config.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = {
mode: 'none',
entry: {
main: './src/index.js'
},
output: {
filename: '[name].bundle.js'
},
optimization: {
minimizer: [
new TerserWebpackPlugin(),
new OptimizeCssAssetsWebpackPlugin()
]
},
module: {
rules: [
{
test: /\.css$/,
use: [
// 'style-loader', // 將樣式通過 style 標簽注入
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Dynamic import',
template: './src/index.html',
filename: 'index.html'
}),
new MiniCssExtractPlugin()
]
}
完成以后回到命令列終端,再次運行webpack打包過后,就可以在目錄當中看到提取出來的檔案了,不過這里需要注意一點,如果說樣式檔案體積不是很大的話,提取到單個檔案當中,效果可能適得其反,個人的經驗是:如果css超過了150KB左右,才需要考慮是否將它提取到單獨檔案當中,否則的話其實css嵌入到代碼當中,它減少一次請求效果可能會更好,
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-NpdmczdM-1610550317556)(C:\Users\24561\AppData\Roaming\Typora\typora-user-images\image-20210110001523687.png)]
46. Webpack OptimizeCssAssetsWebpackPlugin
使用了MiniCssExtractPlugin過后,樣式檔案就可以被提取到單獨的css檔案當中了,但是這里同樣有一個小問題,回到命令列,嘗試以生產模式去運行打包(yarn webpack --mode production),照之前的了解,在生產模式下webpack會自動去壓縮輸出的結果,這里打開輸出的樣式檔案,發現樣式檔案根本沒有任何的變化,這是因為webpack內置的壓縮插件僅僅針對于js檔案的壓縮,對于其它的資源檔案壓縮都需要額外的插件來去支持,
webpack官方推薦了一個OptimizeCssAssetsWebpackPlugin插件,可以使用這個插件來去壓縮樣式檔案,首先安裝一下這個插件,yarn add optimize-css-assets-webpack-plugin,安裝完成后回到組態檔當中,先匯入這個插件,完成過后去把這個插件添加到配置物件的plugins當中,此時再次回到命令列終端,重新運行打包,這次打包完成過后,樣式檔案就可以以壓縮檔案的格式去輸出了,
不過這里還有一個額外的小問題,可能大家在官方檔案當中會發現,檔案當中這個插件它并不是配置在plugins陣列當中的,而是添加到了optimization屬性當中的minimizer屬性當中,這是為什么,其實也非常簡單,如果說把這個插件配置到plugin陣列當中,這個插件它在任何情況下都會正常作業,而配置到minimizer當中的話,那只會在minimizer特性開啟時才會作業,所以說webpack的建議,像這種壓縮類的插件,應該配置到minimizer陣列當中,以便于可以通過這個選項去統一控制,這里嘗試把這個插件移植到的optimization屬性的陣列當中,然后再次運行打包,此時如果說沒有開啟壓縮這個功能的話,這個插件就不會作業,反之如果說以生產模式打包,minimizer的屬性就會自動開啟,這個壓縮插件就會自動作業,樣式檔案也就會被壓縮,但是這么配置也有一個小小的缺點,可以來看一眼輸出的js檔案,這時候發現原本可以自動壓縮的js,這次卻不能自動壓縮了,這是因為設定了minimizer這個陣列,webpack就認為如果配置了這個陣列,就是要去自定義所使用的壓縮器插件,內部的js壓縮器就會被覆寫掉,所以這里需要手動再去把它添加回來,內置的js壓縮插件叫做terser-webpack-plugin,回到命令列,然后來手動安裝一下這個模塊,安裝完成過后這里再來把這個插件手動的去添加到這個陣列當中,這樣的話,如果再以生產模式運行打包,js檔案、css檔案都可以正常被壓縮了,如果說以普通模式打包也就是不開啟壓縮(minimizer)的話,它也就不會以壓縮的形式輸出了,
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin') // css壓縮插件
const TerserWebpackPlugin = require('terser-webpack-plugin') // webpack內置的js壓縮插件
module.exports = {
mode: 'none',
entry: {
main: './src/index.js'
},
output: {
filename: '[name].bundle.js'
},
optimization: {
minimizer: [
new TerserWebpackPlugin(),
new OptimizeCssAssetsWebpackPlugin()
]
},
module: {
rules: [
{
test: /\.css$/,
use: [
// 'style-loader', // 將樣式通過 style 標簽注入
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Dynamic import',
template: './src/index.html',
filename: 'index.html'
}),
new MiniCssExtractPlugin()
]
}
47. Webpack 輸出檔案名 Hash
一般部署前端的資源檔案時,都會啟用服務器的靜態資源快取,這樣的話對于用戶的瀏覽器而言,就可以快取住應用當中的靜態資源,后續就不再需要請求服務器得到這些檔案,整體應用的回應速度就有一個大幅度的提升,不過開啟靜態資源的客戶端快取,也會有一些小小的問題,如果說在快取策略當中的快取失效時間設定的過短的,效果就不是特別明顯,如果說把過期時間設定的比較長,一旦在這個程序當中應用發生了更新,重新部署過后,又沒有辦法及時更新到客戶端,
為了解決這個問題,建議在生產模式下需要給輸出的檔案名當中加哈希值,這樣的話一旦的資源檔案發生改變,檔案名稱也可以跟著一起去變化,對于客戶端而言,全新的檔案名就是全新的請求,也就沒有快取的問題,這樣的話就可以把服務端的快取策略當中的時間設定得非常長,也就不用擔心檔案更新過后的問題,
webpack中的filename屬性和絕大多數插件中的filename的屬性,都支持通過占位符的方式來去為檔案名設定hash,不過它們支持三種hash,效果各不相同,
-
hash
const { CleanWebpackPlugin } = require('clean-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') const MiniCssExtractPlugin = require('mini-css-extract-plugin') const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin') const TerserWebpackPlugin = require('terser-webpack-plugin') module.exports = { mode: 'none', entry: { main: './src/index.js' }, output: { // 這個hash是整個專案級別的,也就是說一旦專案當中有任何一個地方發生改動,這一次打包程序當中的哈希值全部都會發生變化, filename: '[name]-[hash].bundle.js' // 最普通的hash,專案級別 }, optimization: { minimizer: [ new TerserWebpackPlugin(), new OptimizeCssAssetsWebpackPlugin() ] }, module: { rules: [ { test: /\.css$/, use: [ // 'style-loader', // 將樣式通過 style 標簽注入 MiniCssExtractPlugin.loader, 'css-loader' ] } ] }, plugins: [ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ title: 'Dynamic import', template: './src/index.html', filename: 'index.html' }), new MiniCssExtractPlugin({ filename: '[name]-[hash].bundle.css' }) ] }
-
chunkhash
const { CleanWebpackPlugin } = require('clean-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') const MiniCssExtractPlugin = require('mini-css-extract-plugin') const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin') const TerserWebpackPlugin = require('terser-webpack-plugin') module.exports = { mode: 'none', entry: { main: './src/index.js' }, output: { // 這個哈希chunk級別的,也就是在打包程序當中,只要是同一路的打包,chunkhash都是相同的,一個打包入口算一路 filename: '[name]-[chunkhash].bundle.js' }, optimization: { minimizer: [ new TerserWebpackPlugin(), new OptimizeCssAssetsWebpackPlugin() ] }, module: { rules: [ { test: /\.css$/, use: [ // 'style-loader', // 將樣式通過 style 標簽注入 MiniCssExtractPlugin.loader, 'css-loader' ] } ] }, plugins: [ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ title: 'Dynamic import', template: './src/index.html', filename: 'index.html' }), new MiniCssExtractPlugin({ filename: '[name]-[chunkhash].bundle.css' }) ] } -
contenthash
const { CleanWebpackPlugin } = require('clean-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') const MiniCssExtractPlugin = require('mini-css-extract-plugin') const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin') const TerserWebpackPlugin = require('terser-webpack-plugin') module.exports = { mode: 'none', entry: { main: './src/index.js' }, output: { // 它實際上是檔案級別的hash,是根據輸出檔案的內容生成的哈希值,也就是說只要是不同的檔案,它就有不同的哈希值 filename: '[name]-[contenthash].bundle.js' }, optimization: { minimizer: [ new TerserWebpackPlugin(), new OptimizeCssAssetsWebpackPlugin() ] }, module: { rules: [ { test: /\.css$/, use: [ // 'style-loader', // 將樣式通過 style 標簽注入 MiniCssExtractPlugin.loader, 'css-loader' ] } ] }, plugins: [ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ title: 'Dynamic import', template: './src/index.html', filename: 'index.html' }), new MiniCssExtractPlugin({ filename: '[name]-[contenthash].bundle.css' }) ] }
相比于hash和chunkhash,contenthash它應該算是解決快取問題最好的方式,因為它精確的定位到了檔案級別的hash,只有當這個檔案發生了變化,才有可能去更新檔案名,這個實際上是最適合去解決快取問題的,另外,如果覺得這個20位長度的hash太長的話,webpack還允許指定hash的長度,可以在占位符里面通過":8"來去指定hash的長度,個人覺得如果說是控制快取的話,八位的contenthash應該是最好的選擇了,
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = {
mode: 'none',
entry: {
main: './src/index.js'
},
output: {
filename: '[name]-[contenthash:8].bundle.js' // 控制快取最好的選擇
},
optimization: {
minimizer: [
new TerserWebpackPlugin(),
new OptimizeCssAssetsWebpackPlugin()
]
},
module: {
rules: [
{
test: /\.css$/,
use: [
// 'style-loader', // 將樣式通過 style 標簽注入
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Dynamic import',
template: './src/index.html',
filename: 'index.html'
}),
new MiniCssExtractPlugin({
filename: '[name]-[contenthash:8].bundle.css' // 控制快取最好的選擇
})
]
}
四、其它打包工具
1. Rollup 概述
Rollup也是一款ESM的打包器,它也可以將專案當中散落的細小模塊打包為整塊的代碼,從而使得劃分的模塊可以更好的運行在瀏覽器環境或者是node環境,從作用上來看,與webpack非常類似,不過相比于webpack,Rollup要小巧的多,因為webpack在去配合一些插件的使用下,幾乎可以完成開發程序中前端工程化的絕大多數作業,而Rollup僅僅可以說是一個ESM的打包器,并沒有任何其它額外的功能,例如webpack中有對開發者十分友好的HMR(模塊熱替換),在Rollup當中就沒有辦法完全支持,Rollup誕生的目的并不是要與webpack之類的一些工具去全面競爭,它的初衷只是希望能夠提供一個高效的ESM打包器,充分利用ESM的各項特性,構建出結構比較扁平,性能比較出眾的類別庫,至于它其它的一些特點和優勢,需要上手過后才能了解,
2. Rollup 快速上手
這里準備一個簡單的實體,示例當中使用ESM的方式組織的代碼模塊化,
示例如下:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-N4gAM0l1-1610550317560)(https://i.loli.net/2021/01/10/WqgmyYtLz6GuAIR.png)]
嘗試使用Rollup完成對這個示例應用的打包,首先安裝一下Rollup模塊,它同樣也應該作為專案的開發依賴全裝,所以使用yarn add rollup --dev,安裝這個模塊它就會在node_module當中的bin目錄里面提供一個cli程式,可以通過這個cli去使用Rollup打包,
回到命令列,仍然是通過yarn rollup去運行,因為yarn可以自動找到node_module中的cli程式,避免手動通過路徑去查找對應的cli,執行過后會發現,在不傳遞任何引數的情況下,rollup會自動列印出來它的幫助資訊,資訊一開始的位置就已經告訴我們的正確用法,我們這兒,應該通過引數去指定一個打包入口檔案,
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ZIRTPFvz-1610550317561)(https://i.loli.net/2021/01/10/ISMHUoymtaAF19G.png)]
回來再次執行這個命令,這里打包入口是"./src/index.js",此時命令列出了一個錯誤,意思是在說應該去指定一個代碼輸出的格式,輸出格式的概念應該并不陌生意思就是你希望把ESM的代碼轉換過后,以什么樣的格式去輸出,這里可以使用–format的引數去指定輸出的格式,這里先選擇最適合瀏覽器端的iife(自呼叫函式),然后回車,此時打包結果就被列印到控制臺當中了,
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-NgVrpu06-1610550317563)(https://i.loli.net/2021/01/10/b5TvdYzSZa8Ft1M.png)]
還可以通過–file函式去指定一個輸出檔案的路徑,打包結果就會輸出到檔案當中,
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-SNgzE05r-1610550317564)(https://i.loli.net/2021/01/10/7YEBx1hikH8OgAo.png)]
打開bundle.js檔案第一印象就是它的打包結果驚人的簡潔,基本上就跟以前手寫的代碼是一樣的,相比于webpack當中大量的引導代碼還有一堆的模塊函式,這里的輸出結果幾乎沒有任何多余的代碼,就是把打包程序當中各個模塊按照模塊的依賴順序先后的拼接到一起,而且此時仔細去觀察打包結果,你會發現在輸出結果當中,它只會去保留用到的部分,對于未參考的部分都沒有輸出,這是因為rollup默認會自動Tree-shaking優化輸出的結果,Tree-shaking這個概念最早也就是在Rollup工具當中提出的,
3. Rollup 組態檔
Rollup同樣支持以組態檔的方式去配置打包程序中的各項引數,可以在專案中新建一個組態檔(rollup.config.js),這個檔案運行在node環境當中,不過,它自身會額外處理這個組態檔,所以說這里可以直接使用ESM,它需要匯出一個配置物件,這個物件中可以去通過input屬性去指定打包的入口檔案路徑,然后通過output屬性去指定輸出的相關配置,output屬性要求是一個物件,在output的物件當中,可以使用file屬性去指定輸出檔案名,然后可以用format來去指定輸出格式,
rollup.config.js
export.default = {
input: 'src/index.js', // 打包入口
optput: {
file:'dist/bundle.js', // 打包輸出檔案名
format: 'iife' // 立即執行函式
}
}
完成以后回到命令列再次執行,–config引數來去表明這里使用專案中的組態檔,默認的話rollup是不會去讀取組態檔,必須使用這個引數,也可以使用引數指定不同組態檔的名稱, 比如可以用rollup.production.js或者是rollup.development.js分別對于開發和生產不同的組態檔,
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-bakKoxd6-1610550317565)(https://i.loli.net/2021/01/10/MdcuePODJXVkisF.png)]
4. Rollup 使用插件
Rollup自身的功能就只是ESM模塊的合并打包,如果專案有更高級的需求,例如想要去加載其它型別的資源檔案,或者是要在代碼當中去匯入CommonJS模塊,又或是想要它去編譯ECMAscript的新特性,這些額外的需求,Rollup同樣支持使用插件的方式去擴展實作,而且插件是Rollup唯一的擴展方式,它不像webpack中劃分了loader、 plugin、minimizer等三種擴展方式,
這里先嘗試使用一個可以讓在代碼當中匯入json檔案的插件,通過這樣一個程序去了解,如何在Rollup當中使用插件,這里使用的這個插件的名字叫做rollup-json-plugin,打開命令列終端,然后將rollup-plugin-json作為專案的開發依賴安裝進來,安裝完成后,打開組態檔,由于Rollup的組態檔可以直接使用ESM,所以這里直接使用import的方式去匯入這個插件模塊,這個插件模塊它默認匯出的是一個插件函式,可以將這個函式的呼叫結果添加到配置物件的plugin陣列當中,需要注意的是,這里是將呼叫的結果放到陣列當中,而不是直接將這個函式放進去,
rollup.config.js
import json from 'rollup-plugin-json'
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'iife'
},
plugins: [
json() // 直接放入函式的回傳結果
]
}
配好這個插件后,就可以在代碼當中通過import的方式去匯入json檔案,回到index.js檔案當中,這里嘗試通過import匯入專案根目錄下的這個package.json檔案,這個這份檔案當中的每一個屬性就會作為一個單獨的匯出成員,這里提取一下json當中的name和version,然后通過log函式把它們列印出來,
index.js
// 匯入模塊成員
import { log } from './logger'
import messages from './messages'
import { name, version } from '../package.json'
// 使用模塊成員
const msg = messages.hi
log(msg)
log(name)
log(version)
完成以后回到命令列終端,再次運行rollup打包,打包完了后找到輸出的bundle.js,此時能看到,json中的name和version正常被打包進來了,而json當中那些沒有用到的屬性也都會Tree-shaking移除掉,這就是在Rollup當中如何去使用插件,
打包結果bundle.js
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-8v71lsE1-1610550317567)(https://i.loli.net/2021/01/10/9aUoySAqntiTmO3.png)]
5. Rollup 加載NPM模塊
Rollup默認只能夠按照檔案路徑方式去加載本地的檔案模塊,對于node_modules當中那些第三方的模塊,它并不能夠像webpack一樣直接去通過模塊的名稱匯入對應的模塊,為了抹平這樣一個差異,Rollup官方給出了一個rollup-plugin-node-resolve插件,通過使用這個插件,就可以在代碼當中直接去使用模塊名稱匯入對應的模塊,
回到命令列,安裝一下這個插件(yarn add rollup-plugin-node-resolve --dev),安裝成功后打開rollup.config.js組態檔,同樣需要先將這個插件匯入進來,然后將這個插件函式的呼叫結果配置到plugins組當中,
import json from 'rollup-plugin-json'
import resolve from 'rollup-plugin-node-resolve'
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'iife'
},
plugins: [ // 切記使用函式呼叫結果
json(),
resolve()
]
}
回到index.js代碼當中,直接node_modules當中的第三方的PM模塊,這里匯入的是提前安裝好的叫做lodash_es的模塊,這個模塊就是非常常見的lodash模塊的ES版本,匯入過后可以使用這個模塊所提供的一些工具方法了,
index.js
// 匯入模塊成員
import _ from 'lodash-es'
import { log } from './logger'
import messages from './messages'
import { name, version } from '../package.json'
// 使用模塊成員
const msg = messages.hi
log(msg)
log(name)
log(version)
log(_.camelCase('hello world'))
完成以后再次打開命令列終端,然后運行優化打包,此時lodash當中對應的代碼就能夠被打包到bundle.js當中,這里去使用的lodash ES版本,而不是使用lodash普通版本的原因是rollup默認只能夠去處理ES模型模塊,如果說需要去使用普通版本,需要欄位外的處理,
6. Rollup 加載 CommonJS 模塊
正如上面所說,Rollup設計的就是只處理ESM的模塊打包,如果在代碼當中去匯入commonJS模塊,默認是不支持的,但是目前還是會有大量的NPM模塊使用CommonJS方式去匯出成員,為了兼容這些模塊,Rollup官方給出了一個插件(rollup-plugin-commonjs),打開命令列安裝這個插件,安裝過后同樣打開rollup.config.js組態檔,這里匯入rollup-plugin-commonjs插件,然后把它配置到plugin陣列當中,過后就可以回到index.js代碼當中去直接匯入commonJS模塊,
yarn add rollup-plugin-commonjs --dev
rollup.config.js
import json from 'rollup-plugin-json'
import resolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'iife'
},
plugins: [
json(),
resolve(),
commonjs()
]
}
首先添加一個CommonJS模塊的示例檔案,新建一個叫做cjs.common.js的檔案,通過export也就是commonJS的方式去匯出了一個物件,有了這個模塊過后,回到index.js當中,嘗試通過import直接去匯入這個commonJS模塊,把它整體提取出來,common模塊的匯出會作為一個默認匯出,這里把這個默認匯出物件列印出來,
cjs-module.js
module.exports = {
foo: 'bar'
}
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-lkgGxinA-1610550317568)(https://i.loli.net/2021/01/10/FokrGygc8f9UTAW.png)]
完成以后再次打開命令列終端,然后運行rollup打包,此時的這個commonJS模塊也就可以被打包進bundle.js,這里你會發現匯入的這個commonJS的默認匯出,它以一個物件的形式出現在打包結果當中了,
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-wOgRpEMP-1610550317569)(https://i.loli.net/2021/01/10/RDo5fpjXBwyzLSt.png)]
7. Rollup 代碼拆分
在Rollup最新的版本中已經開始支持代碼拆分了,同樣可以使用符合ESM標準的動態匯入的方式,去實作模塊的按需加載,Rollup內部也會自動去處理代碼的拆分,也就是分包,回到打包入口檔案當中,這里一起來嘗試一下,先注釋掉這些使用靜態匯入的代碼,然后使用動態匯入的方式,去匯入一下log對應的這個模塊,那這里import的方法同樣回傳的是一個promise物件,在這個物件當中,它的then方法里面可以拿到一個模塊匯入過后的物件,因為模塊匯出的成員都會放在這個物件當中,所以這里就可以使用解構的方式去提取出來里面的那個方法,然后使用這個log方法去列印一個日志訊息,
index.js
// // 匯入模塊成員
// import { log } from './logger'
// import messages from './messages'
// // 使用模塊成員
// const msg = messages.hi
// log(msg)
import('./logger').then(({ log }) => {
log('code splitting~')
})
完了以后打開命令列終端嘗試運行rollup二打包,不過直接去運行rollup打包,它會報出一個錯誤,說的是使用code-splitting,也就是代碼拆分這種方式去打包,它要求的format(輸出格式),不能是iife(自執行函式)這種形式,原因也很簡單,因為自執行函式會把所有的模塊都放到同一個函式當中,它并沒有像webpack一樣有引導代碼,所以說它沒有辦法實作代碼拆分,
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-mgJSjbtt-1610550317573)(https://i.loli.net/2021/01/10/Kv3OrCIsiP6cTBn.png)]
如果想使用代碼拆分的,必須要使用AMD或者是commonjs等其它的標準,在瀏覽器環境當中只能使用AMD的標準,所以說這里需要使用AMD的格式去輸出打包結果,這里再次運行這個打包命令,這一次通過–format這個引數去覆寫的組態檔當中的format設定,把它設定為AMD,再次運行打包,
yarn rollup --config --format amd
這里同樣報出了一個錯誤,說的是code-splitting方式,它需要輸出多個檔案,因為需要輸出多個檔案,這里就不能再使用file的這種配置方式,因為file指定一個單個檔案輸出的檔案的檔案名,如果需要輸出多個檔案的話,可以使用dir的引數,
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-N2HUPYlw-1610550317574)(https://i.loli.net/2021/01/10/CxWS4y89ZPYBKun.png)]
回到組態檔當中,設定format以及dir引數,
rollup.config.js
export default {
input: 'src/index.js',
output: {
dir: 'dist',
format: 'amd'
}
}
完成之后回到命令列終端,再次運行rollup,打包完成過后,回到dist目錄下,這里它就會根據剛剛的動態匯入,生成一個入口的bundle以及動態匯入所對應的那個bundle,它們都是采用AMD的標準去輸出的,這就是在Rollup當中如何去實作代碼拆分,它使用的是動態匯入去實作的,
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Pq84RKwT-1610550317575)(https://i.loli.net/2021/01/10/GAWzf6PeOi4ImaX.png)]
8. Rollup 多入口打包
Rollup同樣支持多入口打包,而且對于不同入口當中公共的部分,Rollup也會自動提取到單個檔案當中作為獨立的版本,具體來看如何去配置,在這里的示例當中有兩個入口分別是index和album,它們兩個公用了fetch.js和log.js這兩個模塊,回到rollup.config.js組態檔當中,配置多入口打包的方式非常簡單,只需要將input屬性修改為一個陣列就可以了
input: ['src/index.js', 'src/album.js'],
也可以使用與webpack當中相同的物件的配置形式去配置,
input: {
foo: 'src/index.js',
bar: 'src/album.js'
},
不過這里需要注意,因為多入口打包內部會自動提取公共模塊,也就是說內部會使用代碼拆分code-splitting,這里就不能再去使用iife(自執行函式),需要將輸出格式修改為amd,完成后,打開命令列終端,然后運行rollup打包,打包過后,dist目錄下就會多出三個檔案,分別是兩個不同打包入口的打包結果,與公共提取出來的一個公共模塊,另外需要注意一點的是,對于amd這種輸出格式的js檔案,不能直接去參考到頁面上,而必須要去通過實作amd標準的庫(require.js)去加載,這里在dist目錄下手動去創建一個index.html檔案,然后嘗試在這個html當中去使用打包生成的結果,這里采用的就是require.js個庫來去加載以amd標準輸出的版本,使用CDN地址,然后把它引入到頁面當中,它可以通過data-main這樣一個引數來去指定require加載的這個模塊的入口模塊路徑,這里就是"data-main=‘foo.js’",
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<!-- AMD 標準格式的輸出 bundle 不能直接參考 -->
<!-- <script src="foo.js"></script> -->
<!-- 需要 Require.js 這樣的庫 -->
<script src="https://unpkg.com/requirejs@2.3.6/require.js" data-main="foo.js"></script>
</body>
</html>
完成后,再次回到命令列終端,然后server .當前目錄運行到HTTP服務之上,然后打開瀏覽器,再打開開發人員工具,此時你就可以看到的打包結果正常的加載起來也正常的作業了,
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-zyQeQGgO-1610550317576)(https://i.loli.net/2021/01/10/eWGoiFqp2wzNAlV.png)]
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Gvzbh3d3-1610550317577)(https://i.loli.net/2021/01/10/9miV1nbqsMBvOKC.png)]
9. Rollup 選用原則
通過以上探索和嘗試,發現Rollup確實有它的優勢:
- 輸出結果更加扁平
- 首先就是它輸出的結果會更加扁平一些,執行效率自然就會更高
- 自動移除未參考代碼
- 其次就是它會自動移除,未參考的那些代碼也就是Tree-shaking
- 打包結果依然完全可讀
- 就是它的打包結果基本上跟手寫的代碼是一致的,也就是說打包結果對于開發者而言還是可以正常的閱讀的
但是它的缺點同樣也很明顯:
- 加載非ESM的第三方模塊比較復雜
- 模塊最終都被打包到一個函式中,無法實作HMR
- 瀏覽器環境中,代碼拆分功能依賴AMD庫
綜合以上的這些特點,發現如果是正在開發一個應用程式,肯定會面臨要去大量引入第三方模塊這樣的需求,同時又需要HMR這樣的功能去提升的開發體驗,而且應用一旦大了過后,還涉及到必須要去分包,這些需求,Rollup在滿足上都會有一些欠缺,而如果正在開發的是一個JavaScript的框架或者是一個類別庫,這些優點就特別有必要,缺點幾乎都可以忽略,就拿加載第三方模塊來說,在開發類別庫的時候,在代碼當中會很少依賴一些第三方的模塊,所以說很多像React或者Vue之類的一些框架中,都是使用Rollup作為模塊打包器,而并非是webpack,
但是到目前為止,開源社區中,大多數人還是希望這兩個工具可以共同存在共同發展,并且能夠相互支持和借鑒,原因也很簡單,就是希望能夠讓更專業的工具去做更專業的事情,總結一下就是webpack的感覺是大而全而Rollup是小而美,在對它們兩者之間的選擇上,基本的原則就是如果正在開發應用程式,建議大家使用webpack,如果說你是正在開發類別庫或者是開發框架的話,建議選擇Rollup,不過這也并不是一個絕對的標準,只是一個經驗法則,因為Rollup它同樣也可以去構建絕大多數的應用程式,webpack也同樣可以去構建內庫或者框架,只不過相對來講,術業有專攻,另外一點就是隨著近幾年webpack的發展,Rollup中的很多優勢幾乎已經被抹平了,例如像這個Rollup當中的這種扁平化輸出,在webpack當中就可以使用concatenateModules的這樣一個插件去完成,也可以實作類似的這樣一個輸出,
10. Parcel
Parcel是一款完全零配置的前端打包器,它提供了近乎傻瓜式的使用體驗,只需要去了解它所提供的幾個簡單的命令,就可以直接使用它去構建前端應用程式了,
下面直接去看具體如何去使用Parcel,首先新建一個空專案,先通過yarn init的方式去初始化一個專案中的package.json檔案,完成后就可以去安裝parcel所對應的模塊,不過這里注意,parcel的NPM模塊叫做parcel-bundler,這里同樣將它安裝到專案的開發依賴當中,安裝完成后,parcel-bundler模塊它在node_modules當中的bin目錄里面,就提供了parcel的cli程式,后續就可以使用這個cli去執行對整個應用的打包,既然是打包應用代碼,這里就得先有代碼,
回到專案的根目錄下去新建一個src目錄,用于存放開發階段所撰寫的源代碼,然后同時去創建一個index.html檔案,待會html是parcel打包的入口檔案,Parcel跟webpack一樣,都支持以任意型別的檔案作為打包入口,不過Parcel的官方建議使用HTML檔案作為打包入口,官方所給出的理由是因為HTML是應用運行在瀏覽器端時的入口,所以應該使用HTML檔案作為打包入口,在這個入口檔案當中可以正常像平時一樣去撰寫,也可以在這里正常的去參考一些資源檔案,在這里被參考的資源最終都會被打包到一起,最終輸出到輸出目錄,
這里先來引入一個main.js的腳本檔案,然后緊接著就來去新建一個對應的main.js檔案,除此以外再去額外新建一個foo.js檔案,然后在這個當中以ESM的方式去默認匯出一個物件,而且在這個物件當中去訂一個bar方法,然后回到main.js當中,這里通過import匯入foo模塊,并且使用它倒物件中的bar方法,Parcel同樣支持對ESM模塊的打包,完成后再次回到命令列終端,使用yarn parcel去運行一下node_modules的parcel命令,這個命令它需要傳入打包入口檔案的路徑,在這里就是src下的index.html檔案,此時執行這個命令,parcel就會根據所傳入的引數,先去找到index.html檔案,然后根據index.js當中script標簽去找到它所引入的這個main.js檔案,再順著import陳述句,找到所對應的foo模塊,從而去完成整體專案的打包,
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-jUxuk10S-1610550317579)(https://i.loli.net/2021/01/10/teks9nj34I5L1HY.png)]
命令列執行后發現,parcel不僅僅打包了應用,而且它同時還開啟了一個開發服務器,這個開發服務器就跟webpack當中的webpack-dev-server一樣,打開這個地址,啟動瀏覽器,然后在瀏覽器當中打開開發人員工具,此時就可以使用自動重繪這樣的功能,這里可以先去嘗試修改一下源代碼當中的console.log()里面的內容,然后保存過后就會發現螢屏右側的瀏覽器會自動重繪一下,從而去執行最新的打包結果,
如果說需要的是HMR模塊熱替換的體驗,parcel當中同樣也支持,回到main.js當中,這里同樣需要去使用HMR所提供的API,先去判斷module.hot物件是否存在,如果存在這個物件就證明當前這個環境可以使用HMR的API,然后就可以使用module.hot.accept方法去處理模塊熱替換的邏輯了,不過這里的accept跟webpack所提供的API有一點不太一樣,webpack中的API支持接收兩個引數,用來去處理指定模塊更新過后的邏輯,而parcel這里所提供的accept,它只可以接受一個引數,也就是這個回呼函式,作用就是當這個模塊更新或者是當前模塊依賴的模塊更新過后,它會自動執行,可以嘗試先在這里列印一下HMR,然后main.js當中,嘗試再次修改源代碼,此時就可以看到,代碼就會自動被熱替換到瀏覽器當中自動執行了,
main.js
import foo from './foo'
foo.bar()
if (module.hot) {
module.hot.accept(() => {
console.log('hmr')
})
}
除了熱替換,parcel還支持一個非常友好的功能,就是自動安裝依賴,試想一下你正在開發一個應用的程序中,突然間你想要去使用某個第三方的模塊,你此時就需要去先停止正在運行的dev-server,然后去安裝這個模塊,安裝完成過后再去重新啟動dev-server,有了自動安裝依賴這樣的功能過后,就不必要再這樣麻煩了,
回到main.js當中,假設這里想要去使用一下jQuery,雖然之前并沒有安裝jQuery模塊,但是因為有了自動安裝依賴這樣的功能的緣故,這里只管正常匯入就可以了,匯入完成過后正常去使用一下jQuery所提供的API,在檔案保存過后,它會自動去安裝剛剛所匯入的這個模塊,極大程度的避免了額外的一些手動操作,
import $ from 'jquery'
import foo from './foo'
foo.bar()
$(document.body).append('<h1>Hello Parcel</h1>')
if (module.hot) {
module.hot.accept(() => {
console.log('hmr')
})
}
除此之外,它同樣支持加載其他型別的資源模塊,而且相比于其他的模塊打包器,在parcel當中去加載任意型別的資源模塊,同樣還是零配置的,例如這里再來添加一個style.css樣式檔案,然后在這個檔案當中去添加一些簡單的樣式,完成過后,回到main.js當中,通過import匯入這個樣式檔案,這個樣式就可以立即生效了,整個程序并沒有去安裝額外的插件、loader,還可以隨意去添加一個圖片到專案當中,然后回到代碼當中去匯入這張圖片,最后再通過jQuery將它顯示到頁面之中,你會發現同樣也是可以正常顯示出來的,而且整個程序并沒有停下來欄位外的事情,總之,Parcel希望給開發者的體驗就是你想要做什么,你就只管去做,額外的事情就由工具去負責處理,
import $ from 'jquery'
import foo from './foo'
import './style.css'
import logo from './zce.png'
foo.bar()
$(document.body).append('<h1>Hello Parcel</h1>')
$(document.body).append(`<img src="${logo}" />`)
if (module.hot) {
module.hot.accept(() => {
console.log('hmr')
})
}
另外parcel同樣支持使用動態匯入,內部如果使用了動態匯入的話,它也會自動拆分代碼,嘗試一下,這里先將靜態匯入的jQuery注釋,然后使用動態匯入的方式去匯入這塊的模塊,在這個import方法所回傳的promise物件的then當中,就可以拿到所匯入的jQuery物件了,然后把使用jQuery的代碼移到then方法里面,保存過后,回到瀏覽器,重繪一下頁面,然后再找到network面板當中,就能夠看到剛剛所拆分出來的jquery所對應的bundle檔案請求,
// import $ from 'jquery'
import foo from './foo'
import './style.css'
import logo from './zce.png'
foo.bar()
import('jquery').then($ => {
$(document.body).append('<h1>Hello Parcel</h1>')
$(document.body).append(`<img src="${logo}" />`)
})
if (module.hot) {
module.hot.accept(() => {
console.log('hmr')
})
}
以上基本上就是Parcel當中最常用的一些特性了,在使用上parcel幾乎沒有任何的難度,從頭到尾只是執行了一個parcel命令,所有的事情都是parcel內部自動完成的,
回到命令列結束parcel命令,然后看一下parcel如何以生產模式運行打包,需要去執行parcel-cli所提供的一個build命令,然后跟上打包入口檔案的路徑,就可以以生產模式運行打包了,
yarn parcel build src/index.html
這里額外補充一點,對于相同體量的專案打包,parcel構建速度會比webpack快很多,因為在parcel的內部使用的是多行程同時去作業,充分發揮了多核CPU的性能,webpack也可以使用一個叫做happypack的一個插件來去實作這一點,看一眼輸出的結果,這里所說出的這些檔案都會被壓縮,而且樣式代碼也都單獨提取到單個檔案當中了,這就是parcel的一個體驗,整體下來就是一個感覺——舒服,因為它在使用上真的太簡單了,試想一下之前用的webpack,需要做很多額外的配置,安裝很多的插件,在parcel當中其實也有這樣的一些插件,只不過它是自動安裝的,我們整體是不需要去關心這些東西的,所以它在使用上就給了一種非常舒服的感覺,
Parcel2017年發布的,出現的原因也就是因為當時webpack在使用上過于繁瑣,而且官方的檔案也不是很清晰明了,所以說parcel它一經推出,就迅速被推上了風口浪尖,其核心特點就是真正意義上做到了完全零配置,對專案沒有任何的侵入,而且整個程序有自動安裝依賴的這樣一個體驗,讓開發程序可以更加專注于編碼,除此之外,還有一個就是parcel一開始提供的這種構建速度就非常快,因為它內部使用了多行程同時作業,所以相比于webpack的打包速度,parcel要更快一些,但是這個剛剛也說了,webpack也可以借助于插件去解決這樣的問題,
parcel的優點固然很明顯,但是目前實際上你去觀察使用情況,你會發現,絕大多數的專案打包還是會選擇使用webpack,個人認為原因可能有兩點:
- 第一點就是webpack它的生態會更好一些,擴展更豐富,而且出現問題也很容易去解決;
- 第二點就是隨著這兩年的發展,webpack也越來越好用,開發者隨著不斷的去使用也越來越熟悉,所以說個人選擇的話,可能也會選擇webpack,parcel這樣的工具對于開發者而言,去了解它其實也就是為了保持對新鮮技術和工具的敏感度,從而更好的把握技術的趨勢和走向,
五、規范化標準
1. 規范化介紹
規范化:規范化是踐行前端工程化程序中的重要的組成部分,在這里會通過以下幾個方面來進行介紹和說明,
- 為什么要有規范標準
- 軟體開發需要多人協同
- 不同開發者具有不同的編碼習慣和喜好
- 不同的喜好增加專案維護成本
- 每個專案或者團隊需要明確統一的標準
- 哪里需要規范化標準
- 代碼、檔案、甚至提交日志
- 開發程序中認為撰寫的成果物
- 代碼標準化規范最為重要
- 實施規范化的方法
- 編碼前認為的標準約定
- 通過工具實作ESLint
- 常見的規范化實作方式
- ESLint 工具使用
- 定制 ESLint 校驗規則
- ESLint 對 TypeScript的支持
- ESLint 結合自動化工具或者 webpack
- 基于 ESLint 的衍生工具
- Stylelint 工具的使用
具體有哪些方法可以去完成這些規范化的操作,最初在落實規范化的操作時,其實非常的簡單,只需要提前約定好一個可以執行的標準,然后按照這個標準進行各自的開發作業,最終在code review環節,就會按照之前約定的標準去進行檢查相應的代碼,但是如果單靠人為約束的方式落實規范化會有很多的問題,首先,人類約束不可靠,其次開發者很難記住每個規則,所以就需要有專門的工具加以保障,相比于人類檢查工具的檢查更為嚴謹更為可靠,同時還可以去配合自動化的工具實作自動化檢查,這樣的規范化就更加容易得到質量上的保證,
一般把通過工具去找到專案中不合規范的地方這樣的一個程序稱之為Lint,之所以稱之為叫Lint的原因,是因為在剛有c語言的時候有一些常見的代碼問題是不能被編譯器捕獲到的,所以有人就開發了一個叫做Lint工具用于在編譯之前檢查出這些問題,避免編譯之后,帶來一些不必要的問題,所以后續這種類似功能的工具就都可以稱之為Lint,或者說Linter,例如現在前端最常見的也是ESLint、Stylelint等,
2. ESLint 介紹
- 最為主流的JavaScript Lint工具 檢測JavaScript代碼質量
- ESLint 很容易統一開發者的編碼風格
- ESLint 可以幫助開發者提升編碼能力
這里看一下關于ESLint的基本介紹,之前已經知道當下采用工具去完成專案代碼校驗作業是更加高效和合理的,在這里使用的是ESLint,它是目前最為主流的JavaScript Lint工具,專門用于監測JavaScript代碼的質量,通過ESLint就可以很容易的去統一不通開發者的編碼風格,例如縮進,換行、分號以及空格之類的使用,不僅如此,ESLint還可以找出代碼中不合理的地方,比如定義了從未使用的變數,或者一個變數使用之后才去對它進行宣告,再或者說去進行比較的時候,往往總是選擇"=="的符號等,以及其他的一些不合理的操作,這些不合理的操作一般就是代碼中些潛在問題,通過ESLint就能夠去有效的避免這些問題,從而提高代碼的質量,
另一方面,個人認為ESLint也可以去幫助開發者提升編碼能力,為什么這么說,試想一下,如果你撰寫的代碼每次在執行Lint操作的時候都能夠去找出一堆的問題,而這些問題,大都是以往編碼時候的壞習慣,慢慢的就應該記住了這些問題,正常來說,當下次再次遇到的時候,你自己就會去主動的避免他們,那么久而久之,你的編碼能力自然而然的也就得到了一個隱性的提升,
總結一下,就是想去表達:無論出于提升專案代碼質量的原因,還是說要去提高自身編碼水平的原因,ESLint都有很大的價值,接下來,會去通過一些嘗試,具體的去體會ESLint的這些優勢,順便去掌握這一類Lint工具的使用規律,以便后面去接觸到其他Lint的工具的時候,可以去做到觸類旁通,最后做到以不變應萬變,這一塊就是關于ESLint簡單的介紹,
3. ESLint 安裝
這里來看一下使用ESLint之前的一些準備作業,其實,就是ESLint的安裝和校驗,動手實操之前,先快速的梳理一下具體的操作步驟,首先使用ESLint就是為了校驗專案的代碼,因此,需要先有一個專案,在這個專案中該如何使用ESLint,它其實就是一款基于node.js開發的NPM模塊,所以想要使用ESLint也就需要先通過NPM或者yarn來安裝這一模塊,最后完成安裝之后,就可以通過簡單的命令來校驗安裝操作是否成功,
這里打開了一個空的示例專案,里面沒有任何的檔案,先打開命令列終端,然后通過npm init --yes來初始化專案的package.json檔案,用于管理專案的npm依賴,有了package.json之后,就可以安裝ESLint模塊,在這里,使用的是npm工具安裝,就是npm install eslint --save-dev,把ESLint作為專案的開發依賴安裝到專案本地,
npm init --yes
npm install eslint --save-dev
額外補充一個小的話題,就現階段來說,已經很少需要去全域安裝某個模塊,因為大多數情況下都是具體專案依賴某個模塊,把模塊安裝到專案本地,讓它跟著專案一起管理會更加的合理,而且別人在拿到你的專案過后,不需要單獨的去關心這個專案依賴了哪些全域模塊,直接通過npm install就可以安裝必要的工具模塊,這也就從側面提高專案的可維護性,
由于ESLint模塊提供了一個cli程式,所以安裝完成過后,在專案的node_modules的.bin目錄下,就會多出一個ESLint的可執行檔案,后續就可以直接通過這個cli程式去檢測產生的問題,這里回到命令列終端,通過路徑找到這個ESLint可執行檔案,然后添加一個–version引數,表示查看當前所安裝的ESLint版本,當然通過前面的介紹,你應該了解過,對于node_modules下的.bin目錄里的可執行檔案來說,可以去通過npx或者說yarn命令來找到之后快速的去執行它們,不必使用完整的路徑去進行訪問,如果你使用的是yarn,你就可以直接去執行yarn ESLint,這樣的話yarn會自動的找到bin目錄下的ESLint可執行檔案,這里使用的是npm工具,所以這里使用的是npx,而不是npm,npm是npm最新版本當中集成的一個工具,也就是說你只需要安裝最新版本的npm工具,那么就可以直接去使用npx命令,那這里同樣跟上一個–version引數,
npx eslint --version
# yarn eslint --version
在這里,最后再補充一句,不需要去糾結到底該選npm還是使yarn,他們兩者之間沒有絕對的好壞之分,各有各的優勢,你就按照你所在團隊或者說專案的具體要求,使用其中的任何一款即可,
4. ESLint 快速上手
這里來看一下ESLint快速上手的相關內容,當可以執行ESLint模塊的安裝操作之后,就在通過一個案例來具體的看一下,ESLint在專案代碼檢查方面的具體表現,首先,還是先快速的對后續的操作做一個步驟上的說明:
-
**撰寫“問題代碼”,**在最開始的時候需要先去新建一個專案,并且完成相應的初始化操作,同時也安裝好對應的ESLint模塊;
-
**使用ESLint執行檢測,**然后在這個專案當中去新建一個檔案,同時,在這個js檔案中撰寫一些所謂的問題代碼;
-
**完成eslint使用配置,**在這之后,就可以去執行相關的命令來進行代碼的檢查,但是,在第1次使用的操作之前,必須要先完成相應的配置,然后才能去進行正常的使用,而這些配置接下來會具體的講解
專案說明:在這之前,已經完成了NPM相關的初始化操作,同時在專案的目錄當中新建了一個檔案叫做01-prepare.js,緊接著在這個檔案當中去撰寫一些簡單的代碼,
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-F1GUgWPW-1610550317580)(https://i.loli.net/2021/01/10/UtJEQ5KO2kLs3Hl.png)]
接下來就先回到命令列終端這里,通過npx eslint ./01-prepare.js去執行(引數可以是路徑通配符,因為這樣就可以去實作批量的檢查),回車過后在終端當中卻列印出了一串錯誤的資訊,大體意思就是沒有找到一個組態檔,同時它也給出了一個解決的辦法,就是去執行eslint --init,然后再去初始化一個eslint組態檔,
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-W3yOgI9j-1610550317582)(https://i.loli.net/2021/01/10/ZAiCHPaJgpLKN8s.png)]
這里執行一下npx eslint --init,eslint會給出一些互動性的問題,如下:
? How would you like to use ESLint? ...
To check syntax only # 只檢查語法
> To check syntax and find problems # 檢查語法和問題
To check syntax, find problems, and enforce code style # 檢查語法、問題以及代碼風格
? What type of modules does your project use? ... # 專案中使用哪種模塊化型別
> JavaScript modules (import/export) # ESM
CommonJS (require/exports) # CommonJS
None of these
? Which framework does your project use? ... # 專案使用哪種框架
> React
Vue.js
None of these
? Does your project use TypeScript? ? No / Yes # 專案是否使用了TypeScript
? Where does your code run? ... (Press <space> to select, <a> to toggle all, <i> to invert selection) # 專案運行在哪里
√ Browser
√ Node
? How would you like to define a style for your project? ... # 使用哪種代碼風格
> Use a popular style guide # 目前最受歡迎的
Answer questions about your style # 通過回答問題自定義風格
Inspect your JavaScript file(s) # 檢查js檔案
? Which style guide do you want to follow? ... # 選擇哪種最受歡迎的代碼風格
> Airbnb: https://github.com/airbnb/javascript
Standard: https://github.com/standard/standard # 標準
Google: https://github.com/google/eslint-config-google
? What format do you want your config file to be in? ... # 以什么格式定義組態檔
> JavaScript
YAML
JSON
Checking peerDependencies of eslint-config-standard@latest
The config that you've selected requires the following dependencies:
eslint-config-standard@latest eslint@^7.12.1 eslint-plugin-import@^2.22.1 eslint-plugin-node@^11.1.0 eslint-plugin-promise@^4.2.1
? Would you like to install them now with npm? ? No / Yes # 是否安裝風格化代碼依賴項
最終這里選擇的答案如下:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-oxY0WB1c-1610550317584)(https://i.loli.net/2021/01/10/VEPuDY29rk5GRtX.png)]
一切OK過后專案的根目錄下就會多出一個.eslintrc.js的組態檔,有了這個組態檔過后,再次來執行命令npx eslint 01-prepare去校驗檔案,根據這次的執行結果,它首先檢查到的就是一個語法錯誤,回到代碼當中,先修正這個錯誤,它是由foo函式錯誤呼叫引起的,
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-1MIBjpX0-1610550317585)(https://i.loli.net/2021/01/10/GITfKZX5Yvc93Fn.png)]
再次校驗,就看到了更多的錯誤,可能你會好奇,為什么剛剛沒有找出這些錯誤?其實原因非常簡單,因為剛才的代碼當中存在著語法錯誤,eslint是沒有辦法去檢查問題代碼和代碼風格,這個時候,就可以自己根據提示找到具體的問題代碼,然后去進行解決,也可以去通過**–fix**引數來自動的修正絕大多數代碼風格上的問題,
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Pkp8an9a-1610550317587)(https://i.loli.net/2021/01/10/UaNGuq9oO52cLwd.png)]
在這里,就先通過npx eslint 01-prepare --fix來完成一次修正,當再次去執行的時候,問題的數量一下就少了很多,那些風格上的問題就都已經被自動修正了,非常的方便,不過如果你自己還沒有養成良好的代碼習慣,我建議在開始的時候還是手動的去修改每一個不好的地方,因為這樣就可以去加深的印象,作為一個優秀的開發人員,寫出了代碼,它本身就應該是格式良好的,而不是后來的去依賴這些工具進行格式化,這些工具,它只是在最后用于確保代碼的質量,
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-uFOmiKak-1610550317588)(https://i.loli.net/2021/01/10/hjVqoY1FB5M6Ox9.png)]
在最后,會看到還有幾個沒有被修復的問題,需要回到代碼當中,自己手動的去進行處理,移除未參考的foo變數,移除未定義的函式,再次保存,然后回到那個命令列,再次去運行檢查,此時代碼本身的問題就全部解決了,
以上這些,就是eslint的基本作用,簡單的總結一下其實就是兩點:
- 第一可以去找出代碼當中的問題,問題包括語法錯誤、代碼不合理、風格不統一;
- 第二可以去自動修復代碼風格上的絕大多數的問題
5.ESLint 組態檔決議
這里深入了解一下ESLint組態檔,之前通過npx eslint --init在專案根目錄下創建了一個eslintrc.js組態檔,在這個檔案當中寫入的配置就會去影響當前目錄以及所有子目錄檔案,正常情況下是不會手動的修改配置,但說如果需要去開啟或者關閉某些校驗規則的時候,這個組態檔就會非常重要,
下面回到專案當中,具體看一下里面的配置內容,先簡單的梳理一下,這里創建02-configuration.js檔案用于撰寫示例代碼,然后演示組態檔修改之后的運行結果,同時也提前做好了eslint的初始化,生成組態檔,打開組態檔內容如下:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-2vZKmOA6-1610550317590)(https://i.loli.net/2021/01/11/pnJMq2cwSBvXZjk.png)]
因為這個組態檔最終也是運行在node.js環境當中,可以看到這里以CommonJS的方式匯出了一個物件,在這個專案當中,目前是有4個配置選項,
-
env
JavaScript在不同的運行環境中是否不同的API可以被呼叫,這些API很多時候都是以全域成員的方式去提供出來,例如在瀏覽器環境中可以直接去使用window和document物件,而在node.js中不存在這些物件,env選項的作用就是標記當前代碼最終的運行環境,ESLint根據env判斷一些全域成員是否可用,從而從而避免代碼中使用到那些不存在的成員,
例如browser:“true”,代表代碼運行在瀏覽器環境中,意味著可以直接在代碼當中使用document或window這樣的全域物件,換個角度,這里的每一組環境對應的全域變數,一旦開啟某個環境,這個環境中的所有的全域成員都可以被使用,
修改env中的browser為false,在02-configuration.js中使用document.getElementById("#abc"),運行npx eslint ./02-configuration.js,發現并沒有報document未定義的錯誤,那是因為在專案初始化的時候,使用了standard風格,最終配置繼承了standard配置,而在standard中做了一些具體的配置,所以這時候document和window在任何環節中都可以運行,
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-4EUqwQ9j-1610550317593)(https://i.loli.net/2021/01/11/qBjhyfN8Lkb7vmc.png)]
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-KT3HjQH0-1610550317594)(https://i.loli.net/2021/01/11/7ucT4SehUf5AiFm.png)]
換一個browser中的全域成員alert使用,運行后發現提示alert未定義,
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-3b9VoVud-1610550317595)(https://i.loli.net/2021/01/11/qvl4LX93c1EwAZO.png)]
env選項的作用確實是根據環境來判斷全域成員是否可用,env具體可用的環境如下圖:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-veDgt3Io-1610550317596)(https://i.loli.net/2021/01/11/Qwjrg3sbq5CefEH.png)]
這張表里面給出了目前所有能用到的環境以及所對應的說明,不過需要注意的是這些環境,它們并不是互斥的,也就是說你可以同時開啟多個不同環境,
-
extends
繼承一些共享配置,例如這里使用的為standard,這就是設計中常見的配置,很多時候在多個專案之間共享eslint配置,可以定制一個公共的組態檔或模塊,然后在這里繼承,該屬性值為陣列,也就是說可以同時繼承多個共享配置,
-
parserOptions
這個選項的作用就是用來設定語法決議器的相關配置,ESMAScript近幾年發布了很多新的語法,如let const這些關鍵字,這個配置的主要作用就是控制是否允許使用某個ES版本的語法
-
rules
配置eslint校驗規則的開啟或者關閉,例如開啟no_alert,其屬性值可以為off、warn、error,
6.ESLint 配置注釋
這里看一下ESLint配置注釋相關內容,簡單說一下配置注釋,其實就可以理解為是將配置直接通過注釋的方式寫在腳本檔案當中,然后再去執行代碼的校驗,那為什么要這么做的原因也很簡單,在實際的開發程序中如果使用eslint,難免就會遇到一兩個必須要違反配置規則的情況,這種情況下肯定不能因為這一兩個點就去推翻校驗規則的配置,所以在這個時候,就可以去使用eslint的配置去解決這個問題,
例如在這里去定義一個普通的字串,但是在這個基礎上當中,因為業務的需求需要去使用一個${}占位符,但是所使用的standard風格不允許這樣去使用,可以直接先回到命令列來運行的eslint,發現它報了這樣一個錯誤出來,
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-QhOvsVwa-1610550317597)(https://i.loli.net/2021/01/11/TL9l2Pa6iSBjJuH.png)]
這種時候可以去通過注釋的方式,去臨時禁用一下當前的規則,這種注釋的語法有很多種,具體可以去參考官方給出了一個檔案,在這里,就可以直接去使用eslint-display-line, 這樣在作業的時候就會選擇性的去忽略這一行代碼,再次回到命令列的終端運行eslint,這個時候就不會再有錯誤出現了,
這樣去使用雖然能夠解決所面臨的問題,但是同樣也會帶來一個新問題,這個是一行中如果說有多個問題存在的時候,所有問題就都不會被檢測了,因此更好的做法就應該是在注釋的后邊再去跟上一個具體要禁用的規則名稱,可以在這里面去把需要禁用的規則給它寫上,這里需要禁用的就是"no-template-curly-in-string", 這個時候eslint在作業的時候就會忽略掉當前制定的這個規則,而其他的問題,仍然是可以被正常的發現,
當然注釋的方式不僅可以去禁用某個規則,還能夠去宣告全域變數修改某個規則的配置,臨時開啟某個環境等,這些功能,如果有需要的話,可以訪問地址:
http://eslint.cn/docs/user-guide/configuring#using-configuration-comments
7.ESLint 結合自動化工具
這里看一下,eslint結合自動化工具的使用,eslint本身是一個獨立的工具,但如果現在是在一個有自動化構建作業流的專案中,還是建議去把eslint集成到自動化構建的作業流當中,這樣去做有兩個優點,首先肯定是需要執行專案構建的,而把eslint集成在構建的程序當中,就可以去確保它一定會去作業,其次整合在一起去管理也會更加的方便,與專案相關的命令也會更加的統一,不需要一會執行gulp,一會去執行eslint,
提前準備一個gulp構建任務的專案,專案目錄如下:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-gGIyvqda-1610550317599)(https://i.loli.net/2021/01/11/wCItkGda2pZ3FrA.png)]
首先安裝依賴,在執行之前,通過插件先去檢查代碼,簡單的說一下,由于這里使用的是gulp-load-plugins自動加載插件的,所以就不需要再手動載入的模塊,可以直接找到這個插件去進行使用,這里找到script構建任務來,具體的去完成一下這個操作,當前的這個任務就會執行代碼的eslint的操作,然后再去執行,同時匯出script任務,就直接去執行這個任務,完成之后再次回到命令列終端,然后使用npx script找到eslint命令執行任務,發現這里報出了一個錯誤,分析一下可以發現它說的就是沒有找到eslint的組態檔,這就跟最開始的時候直接使用遇到的問題是一樣的,那么就應該先去創建一個組態檔,快速初始化eslint,重新的去運行任務,這次可以正常的執行,
完成這些以后,接下來就可以去找到專案當中的main.js入口檔案,在檔案中加入一些明顯的問題代碼,例如:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-rM0oArnZ-1610550317601)(https://i.loli.net/2021/01/11/MRl742UqxCwchY6.png)]
再次回到命令列終端,重新運行任務,你會發現這個任務仍然是可以成功執行的,這就顯得有些不合理了,因為在最初的設想就是當eslint發現問題過后,就可以直接的去體現出來,同時,也能夠去終止后續的編譯任務,這里為什么可以依然去成功的執行?其實這個問題的原因也很簡單,eslint只會去檢查代碼當中的問題,他并不會去根據檢查的結果來作出任何的反饋,
所以正確的做法就應該是在eslint插件處理完成之后,先去使用format方法,然后在動態當中去列印出具體錯誤資訊,之后再去使用eslint當中的failAfterError方法,錯誤之后可以直接去終止任務管道,這里使用相應的方法來完成這樣的兩個操作步驟,完成之后,再次回到命令列終端,重新的去運行script個任務,這個時候在控臺當中就說出了一些錯誤資訊,這時候就算是集成到編譯js的任務當中了,也就是融入到了所設計的作業流當中,
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-2ddc9mtf-1610550317603)(https://i.loli.net/2021/01/11/xIzdapRNEmJCK2Y.png)]
之后根據報錯資訊手動修改問題代碼,
8.ESLint 結合 Webpack
如果你現在正在開發是一個使用webpack打包的專案,ESLint也同樣可以集成進去,只不過webpack集成ESlint并不是以插件(plugins)的方式完成,而是通過loader機制,webpack在打包之前會將遇到的模塊交給對應的loader進行處理,所以ESLint就可以通過loader的形式集成到webpack中,這樣就可以實作在打包JavaScript代碼之前,先通過eslint校驗JavaScript代碼,
前置作業:
-
克隆一下地址專案
git clone https://github.com/zce/zce-react-app.git -
安裝對應模塊
yarn -
安裝eslint模塊及eslint-loader
yarn add eslint eslint-loader --dev -
初始化.eslintrc.js組態檔
yarn eslint --init
webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'production',
entry: './src/main.js',
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
'babel-loader',
// 'eslint-loader' // 首先執行
]
},
{
test: /\.js$/,
exclude: /node_modules/,
use: 'eslint-loader',
enforce: 'pre' // 該條loader優先執行
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html'
})
]
}
如果想使用eslint校驗代碼,需要在babel-loader之前使用eslint處理(注意:loader的使用順序是從陣列中的最后一位開始),或者使用常見的配置方法,對js新增一個loader配置,添加屬性enforce,將其值賦為pre,代表該條配置執行順序優先于其他loader,
最后命令列執行yarn webpack后發現控制臺報出很多錯誤,這也就意味著eslint-loader已經開始生效了,關于Webpack后續配置在下面講到,
9.ESLint 結合 Webpack 后續配置
這里看一下eslint結合到webpack的后續配置相關內容,接續上一節內容,
前置作業中初始化好的.eslintrc.js
module.exports = {
"env": {
"browser": true,
"es2021": true
},
"extends": [
"standard"
],
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"rules": {
}
};
在命令列運行yarn webpack后發現,eslint報出了很多錯誤,其原因是react框架特殊的語法,其從jsx編譯為js代碼,其中react定義卻未被使用,eslint官方社區提供了一個插件用于解決此問題,安裝eslint-plugin-react,
yarn add eslint-plugin-react --dev
在.eslintrc.js中配置插件
module.exports = {
"env": {
"browser": true,
"es2021": true
},
"extends": [
"standard"
],
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"rules": {
'react/jsx-uses-react': 2, // rules中使用數字2代替前面用到的error,二者效果相同
'react/jsx-uses-vars': 2
},
plugins: [
'react' // 使用安裝的eslint-plugin-react插件
]
};
修改過后,再次運行命令yarn webpack,可以發現已經沒有任何報錯了,這就是eslint-plugin-react的作用以及基本使用,不過對于大多數的eslint插件來說,一般都會提供一個共享的配置,從而降低使用的成本,這里使用的eslint-plugin-react中就匯出了兩個共享的配置,分別是:recommended和all,上面需要使用的就是recommended,插件提供共享配置,可以通過繼承extends進行使用,繼承的語法規則是:‘plugin:[要繼承的插件名稱]/[具體的配置名字]’,具體修改如下:
.eslintrc.js
module.exports = {
"env": {
"browser": true,
"es2021": true
},
"extends": [
"standard",
"plugin:react/recommended"
],
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"rules": {
// 'react/jsx-uses-react': 2, // rules中使用數字2代替前面用到的error,二者效果相同
// 'react/jsx-uses-vars': 2
},
// plugins: [
// 'react' // 使用安裝的eslint-plugin-react插件
// ]
};
再次進入命令列運行webpack打包工具,發現使用該共享配置于單獨配置效果是一樣的,不過第二種共享配置更加的方便,
10.現代化專案集成 ESLint
這里看一下現代化專案對ESLint支持,隨著react、vue等框架的逐漸普及,這些框架的周邊生態也都相當完善了,最明顯的感覺就是現階段再開發一個react或者vue.js的專案,基本上不需要自己配置webpack或者eslint等這些工程化的工具了,而在官方的cli程式中直接集成了這些工具,這里使用vue-cli創建vue專案作為演示,
前置作業:
-
準備一個空專案
-
安裝vue
yarn global add @vue/cli -
通過vue命令創建vue.js專案
vue create 5coder-vue-app互動方式詢問一些特性:
? Please pick a preset: Default ([Vue 2] babel, eslint) Default (Vue 3 Preview) ([Vue 3] babel, eslint) > Manually select features # 手動選擇 ? Check the features needed for your project: ( ) Choose Vue version >(*) Babel # 選擇babel運行環境 ( ) TypeScript ( ) Progressive Web App (PWA) Support ( ) Router ( ) Vuex ( ) CSS Pre-processors (*) Linter / Formatter ( ) Unit Testing ( ) E2E Testing ? Pick a linter / formatter config: ESLint with error prevention only ESLint + Airbnb config > ESLint + Standard config # 代碼風格選擇standard于eslint配合 ESLint + Prettier ? Pick additional lint features: ( ) Lint on save # webpack構建時自動校驗 >(*) Lint and fix on commit # 利用git鉤子,在git commit之前自動校驗代碼,確保提交到倉庫中的代碼是被校驗過的 ? Where do you prefer placing config for Babel, ESLint, etc.? (Use arrow keys) > In dedicated config files # 獨立的組態檔 In package.json ? Save this as a preset for future projects? (y/N) n # 是否在后續的專案保存該配置根據提示運行當前專案:
cd 5coder-vue-app npm run serve同時也可以在編譯器中修改代碼示例,通過這種方式就不需要在工具的配置和使用上花費太多的時間,開發者可以更加專注于業務功能的開發,
11.ESLint 檢查 TypeScript
現階段前端專案中使用TypeScript開發的情況越來越多,所以這里看一下eslint如何校驗TypeScript代碼的,對于TypeScript代碼的lint來說,以前使用tslint工具,但是后面tslint官方放棄維護,推薦使用eslint配合TypeScript插件進行代碼校驗,
初始化eslint組態檔,本次初始化時需要注意專案中需要使用到TypeScript(√ Does your project use TypeScript? · No / Yes)
yarn eslint --init
index.ts
function foo(ms: string): void{
console.log(msg);
}
foo('hello typescript~')
.eslintrc.js
module.exports = {
env: {
browser: true,
es2021: true
},
extends: [
'standard'
],
parser: '@typescript-eslint/parser', // 指定語法決議器TypeScript
parserOptions: {
ecmaVersion: 11,
},
plugins: [
'@typescript-eslint'
],
rules: {
}
}
命令列執行yarn eslint index.ts進行代碼校驗,結果如下:
=============
WARNING: You are currently running a version of TypeScript which is not officially supported by @typescript-eslint/typescript-estree.
You may find that it works just fine, or you may not.
SUPPORTED TYPESCRIPT VERSIONS: >=3.3.1 <4.1.0
YOUR TYPESCRIPT VERSION: 4.1.3
Please only submit bug reports when using the officially supported version.
=============
E:\2021\lagou\02-02-study-materials\codes\02-02-04-01-eslint\eslint-typescript\index.ts
1:13 error Missing space before function parentheses space-before-function-paren
1:31 error Missing space before opening brace space-before-blocks
2:15 error 'msg' is not defined no-undef
2:19 error Extra semicolon semi
2:20 error Block must not be padded by blank lines padded-blocks
6:25 error Newline required at end of file but not found eol-last
? 6 problems (6 errors, 0 warnings)
5 errors and 0 warnings potentially fixable with the `--fix` option.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
12.Stylelint 認識
眾所周知,前端專案中出了JavaScript代碼需要被lint之外,css代碼也同樣需要被lint,對于css代碼的lint操作,一般會使用stylelint的工具進行完成,Stylelint的使用與ESLint基本一致,
Stylelint使用介紹
-
提供默認的代碼檢查規則
可以在組態檔中選擇性的開啟或者關閉某一些規則
-
提供CLI工具,快速呼叫
-
通過插件支持Sass Less PostCSS
-
支持Gulp或Webpack集成
關于css代碼的檢查
安裝stylelint,“yarn add stylelint -D”,在命令列使用如下命令,發現提示沒有組態檔(Error: No configuration provided for E:\2021\lagou\02-02-study-materials\codes\02-02-04-01-eslint\11-stylelint\index.css),所以需要先在專案中添加組態檔.stylelintrc.js
yarn stylelint index.css # 或者使用yarn stylelint *.css通配符的方式
由于stylelint中并未提供共享配置,所以需要先安裝stylelint-config-standard
yarn add stylelint-config-standard
.stylelintrc.js
module.exports = {
extends: "stylelint-config-standard"
}
通過以上配置,stylelint可以正常檢查css代碼,
關于Sass代碼的檢查
如果需要使用stylelint校驗專案中的Sass代碼,需要安裝另外的模塊"stylelint-config-sass-guidelines",
yarn add stylelint-config-sass-guidelines -D
安裝完成后,再次回到組態檔中,將extends設定為陣列,并將stylelint-config-sass-guidelines添加進去
.stylelintrc.js
module.exports = {
extends: [
"stylelint-config-standard",
"stylelint-config-sass-guidelines"
]
}
再次執行yarn stylelint index.sass,可以正常進行sass代碼的校驗,
其余樣式代碼的校驗也是相同的,如果想講stylelint集成到gulp或者webpack中,參考eslint配置即可,
13.Prettier 的使用
Prettier是近兩年使用頻率較高的一款通用的前端代碼格式化工具,幾乎能完成所有型別代碼檔案的格式化作業,在日常使用中也可以使用它完成代碼格式化,或者說完成markdown檔案格式化作業,通過prettier,很容易落實前端專案中的規范化標準,而且它的使用也是非常簡單的,
目錄結構
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-2Me81NyD-1610550317604)(https://i.loli.net/2021/01/13/L2bBE7ONUDrcSd9.png)]
需要首先安裝prettier工具,命令列執行以下命令:
yarn add prettier --dev
安裝完成后,在命令列使用如下命令后,prettier默認將格式化后的代碼列印輸出到控制臺中,如果需要將格式化后的代碼直接覆寫進源檔案中,需要添加引數:
–write
yarn prettier style.css
yarn prettier style.css --write
yarn prettier . --write # 執行后將當前目錄下所有檔案進行格式化并寫入原檔案中
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-GGIheuox-1610550317606)(https://i.loli.net/2021/01/13/8z9hfYJ7cwmpLBt.png)]
14.Git Hooks 作業機制
這里來看一下關于git hooks的一些使用內容的介紹,因為在后期當中需要去使用到eslint和git hooks的一個結合,目前來說,已經了解了代碼規范化的重要性,同時也知道了如何去通過使用一些工具來確保的代碼規范是如何落地的,但是,在這個程序當中還是有一些遺漏的問題,比如說團隊中如果某一個成員沒有按照要求去使用lint工具,或者說壓根兒就忘記了去使用lint工具,最后直接把有問題的代碼去提交到了遠程倉庫,這種情況下在后期去進行專案集成時候導致整個專案的代碼就有可能不被檢測通過,這個時候lint工具就喪失了它的意義,而本身來說使用lint目的就是為了去確保提交到倉庫中的代碼都是沒有問題的,而且格式也是良好的,那么該怎樣去解決這個問題,如果說只是單純的靠口頭的約束去要求團隊當中的成員,在提交代碼之前都必須要去執行lint,這樣的結果必然是流于形式,所以更好的辦法就應該是通過某種方式強制的要求代碼在提交之前,必須先要去通過lint檢查,
Git Hooks介紹
- Git Hooks也稱之為git鉤子,每個鉤子都對應一個任務
- 通過shell腳本可以撰寫鉤子任務觸發是要具體執行的操作
下面演示一下git hooks的使用,首先創建一個空的git專案
git init
其次打開專案目錄找到隱藏檔案.git,打開后找到pre_commit.sample檔案,復制并重命名為pre_commit(無后綴),通過編譯器打開該檔案并清除所有內容(第一行的#!/bin/sh不能被洗掉),
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-xZ13fu3M-1610550317608)(https://i.loli.net/2021/01/13/TvCJ3kdz9iuXDpm.png)]
在檔案pre_commit檔案簡單撰寫如下腳本:
#!/bin/sh
echo "before commit"
隨后在命令列運行一下git add/git commit等命令,測驗撰寫的pre_commit腳本是否運行,
touch demo.txt
vim demo.txt
git add .
git commit -m "test"
運行后發現before commit被列印在控制臺上,

具體的概念操作是這個操作發生的時候,就可以去自動的執行鉤子里邊所定義的一些任務,那明白了這個hooks之后,就可以在將來去想辦法,如何在commit之前去強制執行lint的操作,不過這里面會有一些配置上的東西,需要單獨去進行講解,
15.ESLint 結合 Git Hooks
這里來看一下,關于ESlint結合Git Hooks的一個具體的使用,之前已經知道了Git Hooks是如何來完成作業的,而現在,想要的就是希望去通過Git的鉤子,可以在代碼提交之前強制的去實作對代碼的一個lint操作,但是,這里就遇到了一個很現實的問題,比如說當下的很多的開發者,其實并不是很擅長的去使用shell腳本來撰寫一些功能,而當前的這個功能又是必須要去使用的,
基于以上問題,所以就有人開發了一個npm的工具模塊,直接將Git Hooks操作進行一個簡單化的實作,這個模塊就是Husky,有了這個模塊就可以去實作在不撰寫shell腳本的情況下,也能夠去直接使用的鉤子所帶來的一些功能,不過這里有一件事不能忘記了,因為在上一節中做測驗的時候,手動去修改了一下.git/hooks目錄下的一些內容pre-commit,所以在這里進行洗掉,否則的話就會去影響模塊的作業,
專案目錄
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-iOpSRtFg-1610550317611)(https://i.loli.net/2021/01/13/HxtAsuBY9oc2gyz.png)]
-
首先需要安裝husky模塊
yarn add husky --dev -
安裝完成后去package.json檔案中,撰寫如下內容:
{ "name": "git_hooks", "version": "1.0.0", "main": "index.js", "author": "Leo <19924519007@163.com>", "license": "MIT", "scripts": { "test": "eslint" }, "devDependencies": { "eslint": "^7.17.0", "eslint-config-standard": "^16.0.2", "eslint-plugin-import": "^2.22.1", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^4.2.1" }, "husky": { "hooks": { "pre-commit": "npm run test" } } } -
修改index.js檔案內容
const a=1; 222 -
命令列執行add操作及commit操作
git add . git commit -m "3333"出現如下提示:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-9NJEhrM0-1610550317612)(https://i.loli.net/2021/01/13/Aotdfvl6cgiZPRs.png)]
這樣git commit就不會執行了,如果需要在eslint后繼續執行其他操作,就需要一個新的工具lint-staged模塊,
-
yarn add lint-staged --dev -
修改package.json
{ "name": "git_hooks", "version": "1.0.0", "main": "index.js", "author": "Leo <19924519007@163.com>", "license": "MIT", "scripts": { "test": "eslint index.js", "precommit": "lint-staged" }, "devDependencies": { "eslint": "^7.17.0", "eslint-config-standard": "^16.0.2", "eslint-plugin-import": "^2.22.1", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^4.2.1" }, "husky": { "hooks": { "pre-commit": "npm run precommit" } }, "lint-staged": { "*.js": [ "eslint", "git add" ] } } -
再次進行git add以及git commit操作
提示如圖:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-SIIpR9Vx-1610550317613)(C:\Users\24561\AppData\Roaming\Typora\typora-user-images\image-20210113224920187.png)]
nt基本一致,
Stylelint使用介紹
-
提供默認的代碼檢查規則
可以在組態檔中選擇性的開啟或者關閉某一些規則
-
提供CLI工具,快速呼叫
-
通過插件支持Sass Less PostCSS
-
支持Gulp或Webpack集成
關于css代碼的檢查
安裝stylelint,“yarn add stylelint -D”,在命令列使用如下命令,發現提示沒有組態檔(Error: No configuration provided for E:\2021\lagou\02-02-study-materials\codes\02-02-04-01-eslint\11-stylelint\index.css),所以需要先在專案中添加組態檔.stylelintrc.js
yarn stylelint index.css # 或者使用yarn stylelint *.css通配符的方式
由于stylelint中并未提供共享配置,所以需要先安裝stylelint-config-standard
yarn add stylelint-config-standard
.stylelintrc.js
module.exports = {
extends: "stylelint-config-standard"
}
通過以上配置,stylelint可以正常檢查css代碼,
關于Sass代碼的檢查
如果需要使用stylelint校驗專案中的Sass代碼,需要安裝另外的模塊"stylelint-config-sass-guidelines",
yarn add stylelint-config-sass-guidelines -D
安裝完成后,再次回到組態檔中,將extends設定為陣列,并將stylelint-config-sass-guidelines添加進去
.stylelintrc.js
module.exports = {
extends: [
"stylelint-config-standard",
"stylelint-config-sass-guidelines"
]
}
再次執行yarn stylelint index.sass,可以正常進行sass代碼的校驗,
其余樣式代碼的校驗也是相同的,如果想講stylelint集成到gulp或者webpack中,參考eslint配置即可,
13.Prettier 的使用
Prettier是近兩年使用頻率較高的一款通用的前端代碼格式化工具,幾乎能完成所有型別代碼檔案的格式化作業,在日常使用中也可以使用它完成代碼格式化,或者說完成markdown檔案格式化作業,通過prettier,很容易落實前端專案中的規范化標準,而且它的使用也是非常簡單的,
目錄結構
[外鏈圖片轉存中…(img-2Me81NyD-1610550317604)]
需要首先安裝prettier工具,命令列執行以下命令:
yarn add prettier --dev
安裝完成后,在命令列使用如下命令后,prettier默認將格式化后的代碼列印輸出到控制臺中,如果需要將格式化后的代碼直接覆寫進源檔案中,需要添加引數:
–write
yarn prettier style.css
yarn prettier style.css --write
yarn prettier . --write # 執行后將當前目錄下所有檔案進行格式化并寫入原檔案中
[外鏈圖片轉存中…(img-GGIheuox-1610550317606)]
14.Git Hooks 作業機制
這里來看一下關于git hooks的一些使用內容的介紹,因為在后期當中需要去使用到eslint和git hooks的一個結合,目前來說,已經了解了代碼規范化的重要性,同時也知道了如何去通過使用一些工具來確保的代碼規范是如何落地的,但是,在這個程序當中還是有一些遺漏的問題,比如說團隊中如果某一個成員沒有按照要求去使用lint工具,或者說壓根兒就忘記了去使用lint工具,最后直接把有問題的代碼去提交到了遠程倉庫,這種情況下在后期去進行專案集成時候導致整個專案的代碼就有可能不被檢測通過,這個時候lint工具就喪失了它的意義,而本身來說使用lint目的就是為了去確保提交到倉庫中的代碼都是沒有問題的,而且格式也是良好的,那么該怎樣去解決這個問題,如果說只是單純的靠口頭的約束去要求團隊當中的成員,在提交代碼之前都必須要去執行lint,這樣的結果必然是流于形式,所以更好的辦法就應該是通過某種方式強制的要求代碼在提交之前,必須先要去通過lint檢查,
Git Hooks介紹
- Git Hooks也稱之為git鉤子,每個鉤子都對應一個任務
- 通過shell腳本可以撰寫鉤子任務觸發是要具體執行的操作
下面演示一下git hooks的使用,首先創建一個空的git專案
git init
其次打開專案目錄找到隱藏檔案.git,打開后找到pre_commit.sample檔案,復制并重命名為pre_commit(無后綴),通過編譯器打開該檔案并清除所有內容(第一行的#!/bin/sh不能被洗掉),
[外鏈圖片轉存中…(img-xZ13fu3M-1610550317608)]
在檔案pre_commit檔案簡單撰寫如下腳本:
#!/bin/sh
echo "before commit"
隨后在命令列運行一下git add/git commit等命令,測驗撰寫的pre_commit腳本是否運行,
touch demo.txt
vim demo.txt
git add .
git commit -m "test"
運行后發現before commit被列印在控制臺上,
[外鏈圖片轉存中…(img-AyIw3OCE-1610550317609)]
具體的概念操作是這個操作發生的時候,就可以去自動的執行鉤子里邊所定義的一些任務,那明白了這個hooks之后,就可以在將來去想辦法,如何在commit之前去強制執行lint的操作,不過這里面會有一些配置上的東西,需要單獨去進行講解,
15.ESLint 結合 Git Hooks
這里來看一下,關于ESlint結合Git Hooks的一個具體的使用,之前已經知道了Git Hooks是如何來完成作業的,而現在,想要的就是希望去通過Git的鉤子,可以在代碼提交之前強制的去實作對代碼的一個lint操作,但是,這里就遇到了一個很現實的問題,比如說當下的很多的開發者,其實并不是很擅長的去使用shell腳本來撰寫一些功能,而當前的這個功能又是必須要去使用的,
基于以上問題,所以就有人開發了一個npm的工具模塊,直接將Git Hooks操作進行一個簡單化的實作,這個模塊就是Husky,有了這個模塊就可以去實作在不撰寫shell腳本的情況下,也能夠去直接使用的鉤子所帶來的一些功能,不過這里有一件事不能忘記了,因為在上一節中做測驗的時候,手動去修改了一下.git/hooks目錄下的一些內容pre-commit,所以在這里進行洗掉,否則的話就會去影響模塊的作業,
專案目錄
[外鏈圖片轉存中…(img-iOpSRtFg-1610550317611)]
-
首先需要安裝husky模塊
yarn add husky --dev -
安裝完成后去package.json檔案中,撰寫如下內容:
{ "name": "git_hooks", "version": "1.0.0", "main": "index.js", "author": "Leo <19924519007@163.com>", "license": "MIT", "scripts": { "test": "eslint" }, "devDependencies": { "eslint": "^7.17.0", "eslint-config-standard": "^16.0.2", "eslint-plugin-import": "^2.22.1", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^4.2.1" }, "husky": { "hooks": { "pre-commit": "npm run test" } } } -
修改index.js檔案內容
const a=1; 222 -
命令列執行add操作及commit操作
git add . git commit -m "3333"出現如下提示:
[外鏈圖片轉存中…(img-9NJEhrM0-1610550317612)]
這樣git commit就不會執行了,如果需要在eslint后繼續執行其他操作,就需要一個新的工具lint-staged模塊,
-
yarn add lint-staged --dev -
修改package.json
{ "name": "git_hooks", "version": "1.0.0", "main": "index.js", "author": "Leo <19924519007@163.com>", "license": "MIT", "scripts": { "test": "eslint index.js", "precommit": "lint-staged" }, "devDependencies": { "eslint": "^7.17.0", "eslint-config-standard": "^16.0.2", "eslint-plugin-import": "^2.22.1", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^4.2.1" }, "husky": { "hooks": { "pre-commit": "npm run precommit" } }, "lint-staged": { "*.js": [ "eslint", "git add" ] } } -
再次進行git add以及git commit操作
提示如圖:
[外鏈圖片轉存中…(img-SIIpR9Vx-1610550317613)]
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/248608.html
標籤:其他
上一篇:JS學習筆記(一)



