前端知識體系4.前端工程化1.Webpack專題

本文目錄:

  • 1.webpack的定義及基礎核心概念
  • 2.webpack構建原理
  • 3.webpack運行的基本流程
  • 4.webpack 動態(tài)加載的實現(xiàn)原理及使用方法
  • 5.loader的原理及手寫loader的思路
  • 6.plugin的原理及手寫plugin的思路
  • 7.loader和plugin的區(qū)別
  • 8.tree sharking是什么
  • 9.什么是webpack熱更新
  • 10.介紹下webpack5的新特性
  • 11.Webpack性能優(yōu)化
  • 12.在前端工程化涌現(xiàn)出眾多工具, 試說明webpack與grunt、gulp的不同?
  • 13.npm打包時需要注意哪些?如何利用webpack來更好的構建?

1.webpack的定義及基礎核心概念

webpack 是一個現(xiàn)代 JavaScript 應用程序的靜態(tài)模塊打包器(module bundler)。當 webpack 處理應用程序時,它會遞歸地構建一個依賴關系圖(dependency graph),其中包含應用程序需要的每個模塊,然后將所有這些模塊打包成一個或多個 bundle。
webpack有四個核心概念:
入口(entry),輸出(output),loader,插件(plugins)
webpack的入口文件模板結構:

module.exports = {
    //入口配置
    entry: '',
    //出口配置
    output: '',
    //模塊配置
    module: {
        rules: [
            {
                test: /\.css/,
                use: ["style-loader", "css-loader"]
            }
         ]
    },
    //插件配置
    plugins: {},
    //模式配置,開發(fā)模式還是生產(chǎn)模式
    mode:'',
    //開發(fā)服務器配置
    devServer: {},
    //解析配置
    resolve: {}
}

2.webpack構建原理

webpack.config.js導出一個Object對象(或者導出一個Function,或者導出一個Promise函數(shù),還可以導出一個數(shù)組包含多份配置)。Webpack從入口文件開始,識別出源碼中的模塊化導入語句,遞歸地找出所有依賴,然后把入口文件和所有依賴打包到一個單獨的文件中(即一個chunk),這就是所謂的模塊打包。

3.webpack運行的基本流程

webpack運行的基本流程分為初始化、編譯、輸出三個階段.
初始化:
從配置文件和shell文件讀取、合并參數(shù);
加載plugin
實例化compiler
編譯:
從entry發(fā)出,針對每個module串行調(diào)用對應loader編譯文件內(nèi)容
找到module依賴的module,遞歸進行編譯處理
輸出:
把編譯后module組合成chunk
把chunk轉換成文件,輸出到文件系統(tǒng)

Webpack 的運行流程是一個串行的過程,從啟動到結束會依次執(zhí)行以下流程:

初始化參數(shù):從配置文件和 Shell 語句中讀取與合并參數(shù),得出最終的參數(shù);
開始編譯:用上一步得到的參數(shù)初始化 Compiler 對象,加載所有配置的插件,執(zhí)行對象的 run 方法開始執(zhí)行編譯;
確定入口:根據(jù)配置中的 entry 找出所有的入口文件;
編譯模塊:從入口文件出發(fā),調(diào)用所有配置的 Loader 對模塊進行翻譯,再找出該模塊依賴的模塊,再遞歸本步驟直到所有入口依賴的文件都經(jīng)過了本步驟的處理;
完成模塊編譯:在經(jīng)過第4步使用 Loader 翻譯完所有模塊后,得到了每個模塊被翻譯后的最終內(nèi)容以及它們之間的依賴關系;
輸出資源:根據(jù)入口和模塊之間的依賴關系,組裝成一個個包含多個模塊的 Chunk,再把每個 Chunk 轉換成一個單獨的文件加入到輸出列表,這步是可以修改輸出內(nèi)容的最后機會;
輸出完成:在確定好輸出內(nèi)容后,根據(jù)配置確定輸出的路徑和文件名,把文件內(nèi)容寫入到文件系統(tǒng)。
在以上過程中,Webpack 會在特定的時間點廣播出特定的事件,插件在監(jiān)聽到感興趣的事件后會執(zhí)行特定的邏輯,并且插件可以調(diào)用 Webpack 提供的 API 改變 Webpack 的運行結果

4.webpack 動態(tài)加載的實現(xiàn)原理及使用方法

在代碼中所有被import()的模塊,都將打成一個單獨的包,放在chunk存儲的目錄下。在瀏覽器運行到這一行代碼時,就會自動請求這個資源,實現(xiàn)異步加載。
ES6的import語法告訴我們,模塊只能做靜態(tài)加載。
所謂靜態(tài)加載,就是你不能寫成如下形式:

let filename  = 'module.js';
import {mod} from './' + filename. 
//也不能寫成如下形式:
if(condition) {
  import {mod} from './path1'
} else {
  import {mod} from './path2'
}

先webpack4以后的版本的支持下,import可以進行動態(tài)加載,大致用法如下:import()接收一個路徑參數(shù),然后通過then的方式引入模塊

let filename = 'module.js'; 

import('./' + filename). then(module =>{
    console(module);
}).catch(err => {
    console(err.message); 
});

//如果你知道 export的函數(shù)名
import('./' + filename). then(({fnName}) =>{
    console(fnName);
}).catch(err => {
    console(err.message); 
});

這里有一點要注意的是:
import的加載是加載的模塊的引用。而import()加載的是模塊的拷貝,就是類似于require(),怎么來說明?看下面的例子:
module.js 文件:

export let counter = 3;
export function incCounter() {
  counter++;
}

main.js 文件:

let filename = 'module.js'; 
 import('./' + filename).then(({counter, incCounter})=>{
 console.log(counter); //3
 incCounter(); 
 console.log(counter); //3
 }); 

原本的import寫法:

import {counter, incCounter} from './module.js';
console.log(counter); //3
incCounter();
console.log(counter); //4

5.loader的原理及手寫loader的思路

loader是 webpack 用于在編譯過程中解析各類文件格式,并輸出;
loader(加載器)是一個代碼轉換器,它由 webpack 的 loader runner 執(zhí)行調(diào)用,接收原始資源數(shù)據(jù)作為參數(shù)(當多個加載器聯(lián)合使用時,上一個loader的結果會傳入下一個loader),最終輸出 javascript 代碼(和可選的 source map)給 webpack 做進一步編譯。
手寫loader的思路:
loader本質(zhì)上就是一個 node 模塊,通過寫一個函數(shù)來完成自動化的過程。
這里通過寫一個最簡單的loader來理解手寫loader的思路。
當只有一個 loader 應用于資源文件時,它接收源碼作為參數(shù),輸出轉換后的 js 代碼。文件路徑:loaders/simple-loader.js

module.exports = function loader (source) {
    console.log('simple-loader is working');
    return source;
}

這就是一個最簡單的 loader 了,這個 loader 啥也沒干,就是接收源碼,然后原樣返回,為了證明這個loader被調(diào)用了,我在里面打印了一句話‘simple-loader is working’。
測試這個 loader:
若是使用 npm 安裝的第三方 loader,直接寫 loader 的名字就可以了。但是現(xiàn)在用的是自己開發(fā)的本地 loader,需要我們手動配置路徑,告訴 webpack 這些 loader 在哪里。

// webpack.config.js
const path = require('path');
module.exports = {
  entry: {...},
  output: {...},
  module: {
    rules: [
      {
        test: /\.js$/,
        // 直接指明 loader 的絕對路徑
        use: path.resolve(__dirname, 'loaders/simple-loader')
      }
    ]
  }
}

執(zhí)行webpack編譯,可以看到,控制臺輸出 ‘simple-loader is working’。說明 loader 成功被調(diào)用。

6.plugin的原理及手寫plugin的思路

wenpack根據(jù)自己的工作機制提供了許多hooks,類似于Vue的生命周期。
例如:run(開始編譯階段),make( 從 entry 開始遞歸分析依賴,準備對每個模塊進行 build),done(完成所有的編譯過程)
plugin必須是一個函數(shù),或者是一個包含apply的對象。一般來說我們都會定義一個類型,然后在這個類型中定義apply方法,最后再通過這個類型來創(chuàng)建一個實例對象去使用這個插件。
例如下面這段代碼

const pluginName = 'myplugin'
module.exports =  class myplugin {
    apply(){}
}

這個apply方法接收一個叫compiler的參數(shù)對象,這個對象是webpack工作中最核心的對象,包含了此次打包構建的所有配置信息,我們就可以通過這個對象去注冊鉤子函數(shù)。

const pluginName = 'myplugin'
module.exports =  class myplugin {
    apply(compiler){
        compiler.hooks.run.tap(pluginName, () =>{
            {
                console.log('開始編譯');
            }
        })
    }
}

我們想在run階段輸出‘開始編譯’這句話,在webpack.config.js中引入并配置

const myplugin = require('./myplugin')
...
plugins:[
  new myplugin()
]
...

進行webpack編譯,在控制臺可以看到在開始階段輸出了內(nèi)容,說明plugin生效了。

7.loader和plugin的區(qū)別

對于loader,它是一個轉換器,將A文件進行編譯形成B文件,這里操作的是文件,比如將A.scss轉換為A.css,單純的文件轉換過程。
plugin是一個擴展器,它豐富了webpack本身,針對是loader結束后,webpack打包的整個過程,它并不直接操作文件,而是基于事件機制工作,會監(jiān)聽webpack打包過程中的某些節(jié)點,執(zhí)行廣泛的任務。

不同的作用

  • Loader直譯為"加載器"。Webpack將一切文件視為模塊,但是webpack原生是只能解析js文件,如果想將其他文件也打包的話,就會用到loader。 所以Loader的作用是讓webpack擁有了加載和解析非JavaScript文件的能力。
  • Plugin直譯為"插件"。Plugin可以擴展webpack的功能,讓webpack具有更多的靈活性。 在 Webpack 運行的生命周期中會廣播出許多事件,Plugin 可以監(jiān)聽這些事件,在合適的時機通過 Webpack 提供的 API 改變輸出結果。

不同的用法

  • Loader在module.rules中配置,也就是說他作為模塊的解析規(guī)則而存在。 類型為數(shù)組,每一項都是一個Object,里面描述了對于什么類型的文件(test),使用什么加載(loader)和使用的參數(shù)(options)
  • Plugin在plugins中單獨配置。 類型為數(shù)組,每一項是一個plugin的實例,參數(shù)都通過構造函數(shù)傳入。

8.tree sharking是什么

Tree shaking 是一種通過清除多余代碼方式來優(yōu)化項目打包體積的技術。
我們在項目中創(chuàng)建一個utils.js文件:

export function add(a, b) {
    console.log('add');
    return a + b;
}
export function minus(a, b) {
    console.log('minus');
    return a - b;
}
export function multiply(a, b) {
    console.log('multiply');
    return a * b;
}
export function divide(a, b) {
    console.log('divide');
    return a / b;
}

index.js文件中導入utils.js的add方法并調(diào)用:

import { add } from './utils';
add(10, 2);

運行npm run build后查看dist/bundle.js文件,可以發(fā)現(xiàn)utils.js中所有的代碼都打包了,并沒有像我們預期的那樣只打包add()函數(shù)。
CommonJS的動態(tài)特性模塊意味著tree shaking不適用。因為它是不可能確定哪些模塊實際運行之前是需要的或者是不需要的。在ES6中,進入了完全靜態(tài)的導入語法:import。ES6的import語法完美可以使用tree shaking,因為可以在代碼不運行的情況下就能分析出不需要的代碼。
webpack4以后的版本,只需要將mode設置為production即可開啟tree shaking。

9.什么是webpack熱更新

模塊熱替換(HMR - Hot Module Replacement)允許在運行時替換,添加,刪除各種模塊,而無需進行完全刷新重新加載整個頁面。
一個帶有熱替換功能的webpack.config.js 文件的配置如下,做了這么幾件事

  • 引入了webpack庫
  • 使用了new webpack.HotModuleReplacementPlugin()
  • 設置devServer選項中的hot字段為true

10.介紹下webpack5的新特性

1.通過
嵌套tree-shaking的實現(xiàn)
移除Node.js polyfills 自動加載功能
有效減少打包后的文件體積。
2.生成的代碼不再僅僅是ES5,也會生成 ES6 的代碼
3.optimization配置中優(yōu)化了minSize&maxSize的配置方式,對js和css有了區(qū)分,單位是kb

optimization: {
  runtimeChunks: {},
  splitChunks: {},
  // 在文件大小為0-30kb的情況下進行文件分割
  minSize: {
    javaScript: 0,
    style: 0
  },
  maxSize: {
    javaScript: 30,
    style: 30
  }
}

4.在配置文件中使用cache: {type: "filesystem"}配置實現(xiàn)持久化緩存,提高構建速度

11.Webpack性能優(yōu)化

優(yōu)化可以從兩個方面考慮,一個是優(yōu)化開發(fā)體驗,一個是優(yōu)化輸出質(zhì)量。

優(yōu)化開發(fā)體驗

①縮小文件搜索范圍
resolve字段告訴webpack怎么去搜索文件,所以首先要重視resolve字段的配置:
由于loader對文件轉換操作很耗時,應該盡量減少loader處理的文件,可以使用include命中需要處理的文件,縮小命中范圍。
②DllPlugin可以將特定的類庫提前打包然后引入
DllPlugin是webpack的內(nèi)置插件,這種方式可以極大的減少打包類庫的次數(shù),只有當類庫更新版本才有需要重新打包,并且也實現(xiàn)了將公共代碼抽離成單獨文件的優(yōu)化方案
③HappyPack
因為Node是單線程運行的,所以Webpack在打包的過程中也是單線程的,特別是在執(zhí)行Loader的時候,這樣會導致等待的情況,HappyPack可以將Loader的同步執(zhí)行轉換為并行的,HappyPack插件需要另外安裝。
④使用source-map優(yōu)化代碼調(diào)試
在webpack.config.js中加入devtool:'source-map'可以讓構建后代碼出錯,會通過映射關系追蹤源代碼錯誤。
實際開發(fā)中我們往往只需要在開發(fā)環(huán)境中開啟source-map

const isProd = process.env.NODE_ENV === 'production';
module.exports = {
    devtool: isProd
        ? false
        : '#cheap-module-source-map',
}

⑤熱更新HMR
利用webpack內(nèi)置插件HotModuleReplacementPlugin,無需在每次更改內(nèi)容時都重新加載整個頁面。

優(yōu)化輸出質(zhì)量

優(yōu)化輸出質(zhì)量最大的好處就是可以減少首屏的加載時間
①按需加載路由
如果我們把十幾個頁面甚至更多的路由頁面,把這些頁面全部打包進一個JS文件的話,雖然將多個請求合并了,但是同樣也加載了很多并不需要的代碼,耗費了更長的時間。那么為了首頁能更快地呈現(xiàn)給客戶,這時候我們就可以使用按需加載,將每個路由頁面單獨打包為一個文件
②使用Tree Shaking,刪除項目中未被引用的代碼。
③開啟Scope Hoisting
Scope Hoisting直譯就是作用域提升,Scope Hoisting會分析出模塊之間的依賴關系,盡可能的把打包出來的模塊合并到一個函數(shù)中,讓Webpack打包出來的代碼更小、運行更快。
Scope Hoisting 是webpack內(nèi)置的功能,只要配置一個插件即可

module.exports = {
  plugins: [
    // 開啟 Scope Hoisting 功能
    new webpack.optimize.ModuleConcatenationPlugin()
  ]
}

④區(qū)分環(huán)境--減小生產(chǎn)環(huán)境代碼體積
代碼運行環(huán)境分為開發(fā)環(huán)境和生產(chǎn)環(huán)境,代碼需要根據(jù)不同環(huán)境做不同的操作,許多第三方庫中也有大量的根據(jù)開發(fā)環(huán)境判斷的if else代碼,構建也需要根據(jù)不同環(huán)境輸出不同的代碼,所以需要一套機制可以在源碼中區(qū)分環(huán)境,區(qū)分環(huán)境之后可以使輸出的生產(chǎn)環(huán)境的代碼體積減小。Webpack中使用內(nèi)置DefinePlugin插件來定義配置文件適用的環(huán)境。

plugins:[
    new webpack.DefinePlugin({
        'process.env': {
            NODE_ENV: JSON.stringify('production')
        }
    })
]

注意,JSON.stringify('production') 的原因是,環(huán)境變量值需要一個雙引號包裹的字符串,而stringify后的值是'"production"'
然后就可以在源碼中使用定義的環(huán)境:

if(process.env.NODE_ENV === 'production'){
    console.log('你在生產(chǎn)環(huán)境')
    doSth();
}else{
    console.log('你在開發(fā)環(huán)境')
    doSthElse();
}

⑤使用terser-webpack-plugin插件壓縮JS代碼
如果使用的是 webpack v5 或以上版本,你不需要安裝這個插件。webpack v5 自帶最新的 terser-webpack-plugin。如果使用 webpack v4,則必須安裝 terser-webpack-plugin v4 的版本。

npm install terser-webpack-plugin --save-dev

然后將插件添加到你的 webpack 配置文件中

const TerserPlugin = require("terser-webpack-plugin");

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [new TerserPlugin()],
  },
};

⑥壓縮圖片資源
對于某些網(wǎng)站,圖像占據(jù)了頁面很大部分,雖然它們不會阻塞頁面渲染,但是它們?nèi)匀徽加昧撕艽笠徊糠謳挘趙ebpack中可以使用url-loader來優(yōu)化。
url-loader 可以將小型靜態(tài)文件內(nèi)聯(lián)到應用程序中。如果不進行配置,它將把接受一個傳遞的文件,將其放在已編譯的包旁邊,并返回該文件的url。但是,如果指定 limit 選項,它將把小于這個限制的文件編碼為Base64 數(shù)據(jù)的 url 并返回這個url,這會將圖像內(nèi)聯(lián)到 JavaScript 代碼中,從而可以減少一個HTTP請求。

module.exports = {
  module: {
    rules: [
      {
        test: /\.(jpe?g|png|gif)$/,
        loader: 'url-loader',
        options: {
          // Inline files smaller than 10 kB (10240 bytes)
          limit: 10 * 1024,
        },
      },
    ],
  }
};

12.在前端工程化涌現(xiàn)出眾多工具, 試說明webpack與grunt、gulp的不同?

三者都是前端構建工具,grunt和gulp在早期比較流行,現(xiàn)在webpack相對來說比較主流,不過一些輕量化的任務還是會用gulp來處理,比如單獨打包CSS文件等。
gruntgulp是基于任務和流(Task、Stream)的。類似jQuery,找到一個(或一類)文件,對其做一系列鏈式操作,更新流上的數(shù)據(jù), 整條鏈式操作構成了一個任務,多個任務就構成了整個web的構建流程。
webpack是基于入口的。webpack會自動地遞歸解析入口所需要加載的所有資源文件,然后用不同的Loader來處理不同的文件,用Plugin來擴展webpack功能。
所以總結一下:
從構建思路來說
gulp和grunt需要開發(fā)者將整個前端構建過程拆分成多個Task,并合理控制所有Task的調(diào)用關系 webpack需要開發(fā)者找到入口,并需要清楚對于不同的資源應該使用什么Loader做何種解析和加工復制代碼
對于知識背景來說
gulp更像后端開發(fā)者的思路,需要對于整個流程了如指掌 webpack更傾向于前端開發(fā)者的思路

13.npm打包時需要注意哪些?如何利用webpack來更好的構建?

Npm是目前最大的 JavaScript 模塊倉庫,里面有來自全世界開發(fā)者上傳的可復用模塊。你可能只是JS模塊的使用者,但是有些情況你也會去選擇上傳自己開發(fā)的模塊。 關于NPM模塊上傳的方法可以去官網(wǎng)上進行學習,這里只講解如何利用webpack來構建。

NPM模塊需要注意以下問題:

  1. 要支持CommonJS模塊化規(guī)范,所以要求打包后的最后結果也遵守該規(guī)則。
  2. Npm模塊使用者的環(huán)境是不確定的,很有可能并不支持ES6,所以打包的最后結果應該是采用ES5編寫的。并且如果ES5是經(jīng)過轉換的,請最好連同SourceMap一同上傳。
  3. Npm包大小應該是盡量?。ㄓ行﹤}庫會限制包大小)
  4. 發(fā)布的模塊不能將依賴的模塊也一同打包,應該讓用戶選擇性的去自行安裝。這樣可以避免模塊應用者再次打包時出現(xiàn)底層模塊被重復打包的情況。
  5. UI組件類的模塊應該將依賴的其它資源文件,例如.css文件也需要包含在發(fā)布的模塊里。

基于以上需要注意的問題,我們可以對于webpack配置做以下擴展和優(yōu)化:

1.CommonJS模塊化規(guī)范的解決方案: 設置output.libraryTarget='commonjs2'使輸出的代碼符合CommonJS2 模塊化規(guī)范,以供給其它模塊導入使用
輸出ES5代碼的解決方案:使用babel-loader把 ES6 代碼轉換成 2.ES5 的代碼。再通過開啟devtool: 'source-map'輸出SourceMap以發(fā)布調(diào)試。
3.Npm包大小盡量小的解決方案:Babel 在把 ES6 代碼轉換成 ES5 代碼時會注入一些輔助函數(shù),最終導致每個輸出的文件中都包含這段輔助函數(shù)的代碼,造成了代碼的冗余。解決方法是修改.babelrc文件,為其加入transform-runtime插件
4.不能將依賴模塊打包到NPM模塊中的解決方案:使用externals配置項來告訴webpack哪些模塊不需要打包。
5.對于依賴的資源文件打包的解決方案:通過css-loader和extract-text-webpack-plugin來實現(xiàn),配置如下:

const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
  module: {
    rules: [
      {
        // 增加對 CSS 文件的支持
        test: /\.css/,
        // 提取出 Chunk 中的 CSS 代碼到單獨的文件中
        use: ExtractTextPlugin.extract({
          use: ['css-loader']
        }),
      },
    ]
  },
  plugins: [
    new ExtractTextPlugin({
      // 輸出的 CSS 文件名稱
      filename: 'index.css',
    }),
  ],
};
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容