【我只看到了第三層】從新聊聊webpack

前言

針對技術而言,越學越感受不會的太多。多是沒到達必定的境界。在網上衝浪學習的時候,看到有些大佬文筆從容,思路清晰。對某個技術點的深度刨析。真產生了一種發自心裏的尊重和佩服和尊重和佩服。有些時候,一段文字就能點醒你,原來是這樣!!!有時候感受到這東西被嚼碎了往你嘴裏塞你都嚼不動的感受(無奈)。本人對於webpack的學習也有一段時間了。想借這篇博文梳理webpack相關的知識體系。也是對於本身學習的一段總結。css

webpack

# 像解析Tapable事件流和實現,分析webpack-cli源碼,解析構建流程,實現xx帶有難度的loader或plugin均都不在本文章之列(都不會)
複製代碼

image-20200910133615897

  • webpack 只是一個模塊打包器

::經過webpack將零散的模塊代碼打包到同一個JavaScript文件中,對於代碼中有環境兼容的問題,經過模塊加載器(loader)將其轉化,webpack還能夠進行代碼拆分。對應用中的代碼根據須要打包(按需打包),實現了漸進式加載。這樣就解決了文件太碎或太大的問題。webpack會以模塊化的方式去載入任意類型的文件(import './index.css')。html

webpack解決了前端總體的模塊化,而不是單指JavaScript的模塊化。全部的資源均可以看作一個模塊。前端

import j1 from './index.js'
import c1 from './index.css'
import h1 from './index.html'
import p1 from './index.png'
....
複製代碼

webpack的打包流程

  1. webpack-cli 會解析cli參數,與你配置的參數進行合併,獲取到最終的配置項。
  2. 經過配置項建立Compiler對象,添加構建過程須要的方法,完成註冊插件等功能,這個對象將貫穿整個構建過程。
  3. 經過AST引擎庫(ACORN) + entry所對應的入口文件找到全部依賴,生成ast抽象語法樹,在這裏也能夠看做依賴關係圖。
  4. 解析ast語法樹,對每一個模塊進行根據配置項進行不一樣的loader處理。
  5. 構建完全部模塊進行寫入,寫入到output對應的輸出目錄中。

下面來看一下vue

webpack是如何知道這是一個模塊,我要對他進行打包的呢?java

webpack觸發打包

webpack並不會對入口文件中全部的數據進行無腦打包,而是須要觸發方式。react

  • 衆所周知,ESModule 的 import 語法會被webpack看成一個模塊進行打包。那麼還有啥方式?jquery

    • javaScript代碼:Commonjs規範的require語法、AMD的require語法webpack

    • 非javaScript代碼:衆所周知,非javaScript代碼是須要經過loader處理的web

      • css: 在loader處理css的過程當中,像樣式代碼中的@import/url 也會觸發打包機制。vue-cli

        處理css文件時,咱們使用css-loader,css-loader若是發現了@import語法或者url語法會將其引入的路徑做爲一個新的模塊進行打包。好比:background-image: url(background.png); webpack發現了.png文件,會將該文件交給url-loader進行處理。好比@import url(reset.css); webpack發現了.css文件,會出發資源加載,而後將文件交給css-loader處理。

  • html: 在loader處理html的過程當中,像src也會觸發打包機制。若是須要更多的觸發機制,須要看loader有沒有暴露接口,若是提供,須要本身配置。

webpack 與 gulp對比

只討論一點:關於對import/require語法的支持。

瀏覽器是不支持import/require語法的,那麼這些語法是怎麼被轉換了呢?由於webpack提供了基礎代碼"替換了"import這些語法。 而gulp就是一個純粹的自動化構建工具流。沒有提供這些基礎代碼讓用戶輕鬆的使用模塊化語法。

下面來看一下

webpack的引導代碼

基礎代碼或引導代碼webpack是如何實現的? 或者說webpack是對import/require等語法的實現?

ps: 如下是在mode: none的時候的打包方式。在mode: development的時候有所不一樣。

/******/ (function (modules) { // webpackBootstrap
    .....
})
    ([
/* 0 - 入口文件*/
/***/ (function (module, __webpack_exports__, __webpack_require__) {
 "use strict";
            __webpack_require__.r(__webpack_exports__);
/* harmony import */ var _testA_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); // 也就是import testA from './testA.js'
/* harmony import */ var _testB_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2); // 也就是import testB from './testB.js'
            console.log(_testA_js__WEBPACK_IMPORTED_MODULE_0__["default"], _testB_js__WEBPACK_IMPORTED_MODULE_1__["default"]);
        }),
/* 1 第一個引入的模塊testA*/
/***/ (function (module, __webpack_exports__, __webpack_require__) {
 "use strict";
            __webpack_require__.r(__webpack_exports__);
            var t1 = 'hello1';
/* harmony default export */ __webpack_exports__["default"] = (t1);
        }),
/* 2 第二個引入的模塊testB*/
/***/ (function (module, __webpack_exports__, __webpack_require__) {
 "use strict";
            __webpack_require__.r(__webpack_exports__);
            var t2 = 'hello2';
/* harmony default export */ __webpack_exports__["default"] = (t2);
        })
    ]);
複製代碼

從上面能夠看到webpack是經過__webpack_require__方法實現了 import x from './x.xx' 的語法。default的意思是默認導出。這個匿名函數的參數是一個數組,每個模塊都做爲了這個數組中的成員。模塊被解析成了一個個函數,從而產生了獨立的做用域, 模塊與模塊之間不會產生變量衝突的問題。 在mode: development的時候,會有一些差別,但大致上是相同的。webpack打包事後提供的引導代碼讓模塊與模塊之間的關係清晰獨立,並且更容易進行拆分和組合。

總結:

把全部的模塊放到同一個文件中,提供基礎代碼讓模塊與模塊之間的關係保持原有的關係。 將全部的模塊都做爲巨大匿名函數的數組參數中的成員,數組中的每一個成員都是一個函數,也就是說webpack將每個模塊都做爲一個函數。以維持模塊的私有化。從第一個入口參數開始執行, 只會先執行下標爲0的函數。 每個模塊都對應一個下標,webpack將模塊與模塊之間的關係在編譯階段就作好了處理。好比a.js import b.js。 b.js的下標爲3, 那麼就會在a.js這個函數中,webpack.require(3) , 這樣編譯好,webpack就是這樣維護模塊與模塊之間的關係的。

loader的工做原理

  • webpack能夠零配置,默認 是src/index.js --> dist/main.js

  • webpack會將遇到的全部模塊都看成JavaScript去處理

    • 每一個loader 都須要導出一個函數
      • 函數的返回結果會做爲JavaScript代碼(這些代碼放到了每一個模塊所表明函數中做爲函數體)去執行,因此要求這個函數返回的必須是一個標準的可執行的JavaScript代碼。
    • 輸入就是須要解析的內容
    • 輸出就是處理後的結果
    1. 對於同一個資源能夠依次使用多個loader
    2. 在模塊加載的時候工做
    複製代碼

plugin的工做原理

Loader 專一實現資源模塊加載,去實現總體模塊的打包

webpack 加強了webpack自動化能力

e g. plugin 能夠清除打包的目錄、能夠拷貝一些資源文件、壓縮輸出的代碼等等等的能力。

webpack的插件機制是由鉤子機制實現的。相似於web中的事件,插件的工做過程當中會有不少的環節,爲了便於插件的擴展,webpack給每個環節掛載鉤子,這樣插件的運行和開發就是在鉤子中擴展能力。

plugin必須是一個函數或者是一個包含apply方法的對象。

實現plugin是經過在生命週期的鉤子中關在函數實現擴展,達到插件的目的。

SoureMap

  • sourcemap解決了運行時代碼和開發時代碼不一致, 致使沒法調試和錯誤信息沒法定位的問題。

線上文件引入jquery.min.js, 若是須要須要調試jquery.js的話, 則須要在引入的jquery.min.js最後一行加上

//# sourceMappingURL=jquery-3.4.1.min.map

告訴此文件去尋找jquery-3.4.1.min.map。該文件記錄了轉換以後的代碼和轉換以前的代碼的映射關係。

目前,webpack對sourcemap的風格支持 有 12種實現方式,每種方式的效率和效果各不相同。

  • eval模式

    eval('console.log(123)')  //VM122:1 運行在一個臨時的虛擬機上。
    eval('console.log(123) //# sourceURL=./foo/bar.js') // ./foo/bar.js:1 運行環境就是./foo/bar.js
    // 經過sourceURL 就能夠指定該運行環境名稱或者說是路徑。
    複製代碼

    devtools: eval , 設置成eval模式,能夠看到打包後的模塊化代碼,在打包後的bundle.js中,每一個模塊的執行都使用eval包裹執行,在eval的最後能夠看到//# sourceURL=webpack:///./src/main.js? 這樣的信息來標註文件路徑。表明該模塊只想源文件的路徑。構建的速度最快,可是效果也很通常。只能定位到是哪一個文件有問題。不會生成source map

    devtools: eval-source-map, 一樣也是使用eval函數執行模塊代碼,除了能夠幫咱們定位到出現問題的文件,還能夠肯定行列信息。這種相比較eval,在生成的js內部去生成了以dataURL的形式引入生成的source map。這個sourcemap是通過babel轉換的,而不是最原始的。

    devtools: cheap-eval-source-map , 在上一個eval-source-map的基礎上加了一個cheap,就是廉價的,便宜的,用計算機術語來講就是閹割版。相比較上一個只能定位到行,可是不能定位到列的信息。可是構建的速度加快了。source map原理同上

    cheap-module-eval-source-map, 相比較cheap-eval-source-map, source map映射的真正的源代碼,而不是編譯後的。會產生 xxx.js.map文件。其餘的痛cheap-eval-source-map

    inline-source-map, 和 source map 是效果是相同的,可是的.js.map文件是以dataURL的形式放到編譯後文件的最後一行。用#sourceMappingURL引入。普通的就是生成 .map.js文件

    hidden-source-map, 構建過程當中生成了source map文件, 在代碼中沒有使用註釋的方式引入sourcemap,通常咱們在開發第三方包的時候會使用這個sourcemap風格。

    nosources-source-map, 能看到錯誤出現的位置,可是看不到源代碼,只提供錯誤出現的位置,可是不給你顯示,這是在生產環境中防止暴露源代碼的一種方案。

  • 選擇最佳實踐的sourcemap

    • 開發環境下:cheap-module-eval-source-map。

    • 生產環境下:none

    • 生產環境下: nosources-source-map

      • 這是對代碼自己沒多少信息的前提下,選擇使用nosource-source-map可以定位錯誤,並且不會暴露源代碼。

實際開發中關於路徑問題

  • 在搭建腳手架的過程當中,路徑的問題非常頭疼,一直也沒法找到一個好的解決方案,主要仍是對webpack中可配置路徑的一些屬性不夠了解。好比publicPath,filename, html打包到的位置。

publicPath

webpack打包的模塊會默認放到output的目錄中。

  • publicPath是在運行時瀏覽器中所引用的資源的url將publicPath做爲其前綴。
  • 默認值是空字符串"", 表示網站的根目錄
  • publicPath的值 最後面有一個 / ,該 / 不能省略, 由於是路徑拼接的形式
import icon from './icon.png'
// 若是沒有publicPath,則icon爲 './[hash].png'
// 若是有publicPath,則icon爲 publicPath+ './[hahs].png'
複製代碼
  • webpack-dev-server 也會默認從 publicPath 爲基準,使用它來決定在哪一個目錄下啓用服務,來訪問 webpack 輸出的文件。
html頁面中資源路徑,被自動注入了output中filename的值。
output中filename的值。/backend/js/app.11c8b942e5dd4b3a51b5.js?2c61c09c3e5bff69e658, 自動注入到index.html中做爲src/href,可是打包的位置是由path和filename一塊兒決定的,也就是須要爲filename的結果設置爲服務器上做爲此項目的根路徑。由於只有根路徑才能夠找到。  
若是設置了publicPath,那麼全部的靜態資源的路徑前面都會加上公共路徑 publicPath的值。(不會產生目錄,只是在引入的路徑前加上publicPath的值)
// 設置publicPath: '/abc'
<script src=/abc/backend/js/app.57e28a966ab9582ff286.js?1dcc397cc95f5934ef81></script>

index.html的路徑是由filename決定的
也就是說實際在磁盤上產生的路徑是由path+filename決定的,可是 代碼中的資源地址的路徑是filename+publicPath決定的。

對於loader而言,有些loader有本身的publiPath,可是也能夠經過設置filename來替換掉publicPath,一勞永逸
複製代碼
HMR

webpack中最強大的功能之一

  • 模塊熱替換

    應用運行過程當中實時替換某個模塊,應用的運行狀態不受影響。熱替換隻將修改的模塊實時替換至應用中,沒必要去徹底刷新應用。

    • 熱更新不只能夠熱更新文本文件,並且還能夠更新其餘類型的文件。
  • 開啓HMR

  • HMR已經集成到了webpack-dev-Serve了,使用 webpack-dev-Serve --hot 開啓熱更新。

    • ~~咱們發現開啓熱更新以後,只有css是開箱即用的,而js改變仍是會刷新頁面。這是由於 不一樣的模塊具備不一樣的邏輯,不一樣的邏輯又致使處理過程也是不一樣的。每一個js文件都須要單獨爲這個js文件進行處理這個js文件的熱更新(根據這個頁面的邏輯)。 webpack沒有辦法提供一個通用的替換方案,可是vue-cli或者create-react-app腳手架是能夠進行HMR的,由於他們是框架,他們每一個文件都知足必定的規律,框架內部繼承了HMR熱模替換。
  • 那麼怎樣爲每一個js單獨提供hmr呢? 使用 webpack提供的module.hot.accept(文件路徑, () => { // 熱替換邏輯 }) , 下面簡單的展現下圖片的HMR

if (module.hot) {
	// 圖片的hmr
  module.hot.accept('./test.png', () => {
    img.src = background
    console.log(background)
  })
}
複製代碼
  • 問題: 若是咱們使用hot: true 開啓熱模替換的話,若是替換失敗,好比代碼出現錯誤,那麼就會回退到使用自動刷新的功能進行hmr,設置hotOnly: true // 只使用 HMR,不會 fallback 到 live reloading.

webpack.DefinePlugin

  • 這個是爲項目注入全局變量。key: value 這個value必須是一個js代碼片斷。
new webpack.DefinePlugin({
  // 值要求的是一個代碼片斷
  API_BASE_URL: JSON.stringify('https://api.example.com')
})
複製代碼

tree-shaking

搖樹 , 就是搖掉代碼中未被引用的部分(dead-code)。mode:production。會自動開啓搖樹優化。

tree-shaking的實現:

optimization: {
  usedExports: true, // 標記枯樹葉
  minimize: true	// 負責搖掉標記的枯樹葉
}
複製代碼
  • 測試tree-shaking
let foo = () => {
    let x = 1
    if (false) {
        console.log('never readched')
    }
    let a = 3
    return a
}

let baz = () => {
    var x = 'baz is running'
    console.log(x)
    function unused () {
        return 5
    }
    return x
    let c = x + 3
    return c
}

module.exports = {  // commonjs模塊規範導出
    baz
}
export { // ESM 模塊規範導出
	baz 
} 
複製代碼
  • 打包後的結果
esModule 規範 打包後的模塊
([
    function (e, n, r) {
 "use strict";
        r.r(n);
        var t;
        t = "baz is running",
            console.log(t),
            console.log("main.js running")
    }
]);

commonjs 規範 打包後的模塊
([
    function (e, n, t) {
        (0, t(1).baz)(), console.log("main.js running")
    },
    function (e, n, t) {
        e.exports = {
            baz: function () {
                var e = "baz is running";
                return console.log(e),
                    e
            }
        }
    }
]);
複製代碼
  • 爲何須要esModule規範才能進行tree-shaking
1. ES6的模塊引入是靜態分析的,故而能夠在編譯時正確判斷到底加載了什麼代碼。
2. 分析程序流,判斷哪些變量未被使用、引用,進而刪除此代碼。
複製代碼

Scope Hoisting 做用域提高

合併模塊函數

  • 開啓了合併模塊函數,再也不是一個模塊一個函數了,而是將全部的模塊放到了一個函數中。
    • 儘量的將全部模塊合併輸出到一個函數中。
    • 既提高了運行效率,又減小了輸出代碼的體積。
optimization: {
  usedExports: true, // 標記枯樹葉
  minimize: true,	// 負責搖掉標記的枯樹葉
  concatenateModules: true. // 開啓 Scope Hoisting
}
複製代碼

tree-shaking 和 babel

  • 不少資料上說,若是咱們使用了babel-loader就會致使tree-shaking失效。
  • Tree-shaking的前提是模塊必須是ESModule標準。由webpack打包的代碼必須使用ESM標準。webpack在打包以前,將模塊交給配置交由不一樣的loader處理,將loader處理後的結果打包到一塊兒。babel-loader處理js的過程當中,可能處理掉esModule轉換爲commonjs規範。可是通過咱們實驗,即時使用了babel-loader處理事後,仍是會進行搖樹優化,是由於在新版本的babel-loader中已經幫咱們自動關閉了esModule的轉換。
loader: 'babel-loader',
options: {
  presets: [
    ['@babel/preset-env', { modules: 'commonjs' }] // 開啓esModule的轉換爲commonjs, 這樣將不會進行commonjs轉換。
  ]
}
複製代碼
  • 仍需探索~~~~~~~~~~~~~~~~~~~~~~

sideEffects

反作用

  • 容許咱們經過配置的方式去標識咱們的代碼是否有反作用,從而爲tree-shaking提供更大的壓縮空間。

  • 反作用:模塊執行時除了導出成員以外所作的事情。好比給xxx原型上添加了原型方法,別人引入的時候會污染到xxx原型。

  • 通常用於npm包標記是否有反作用

  • 如何使用:

    • 在webpack.config.js中 使用sideEffects開啓這個功能
    • 在packjson中配置sideEffects: false 標識 這個項目中的代碼沒有任何反作用。
  • 使用以前請確保你的代碼沒有反作用。不然會誤刪掉反作用代碼。

webpack代碼分包/割

  • bundle的體積過大,須要解決。由於並非每一個模塊在啓動時都是必要的。
  • 分包、按需加載
    • 多入口打包
    • 動態導入、按需加載

多入口打包

  • 通常適用於多頁應用程序
  • 一個頁面對應一個打包入口
  • 公共部分單獨提取
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'
    }
  },
  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']
    })
  ]
}
複製代碼

動態導入、按需加載

  • 全部動態導入的模塊會被自動分包

  • 經過代碼的邏輯控制何時須要動態導入,或者說何時加載這個模塊。

  • 魔法註釋:能夠對動態打包出來的文件進行從新命名, 並且能夠對文件進行靈活組合。

  • import('路徑').then(data => {}) import() 是ESModule規範的語法,而這個方法返回的是一個promise。

  • 按需加載webpack是如何打包的呢?這是webpack爲import() 語法提供的引導代碼。

__webpack_require__.e = function requireEnsure(chunkId) {
    var promises = [];
    var installedChunkData = installedChunks[chunkId];
    if(installedChunkData !== 0) { // 0 means "already installed".
        // a Promise means "currently loading".
        if(installedChunkData) {
            promises.push(installedChunkData[2]);
        } else {
            // setup Promise in chunk cache
            var promise = new Promise(function(resolve, reject) {
                installedChunkData = installedChunks[chunkId] = [resolve, reject];
            });
            promises.push(installedChunkData[2] = promise);

            // start chunk loading
            var script = document.createElement('script');  // 建立一個script標籤
            var onScriptComplete;
            script.charset = 'utf-8';
            script.timeout = 120; // 設置script的超時時間
           
            script.src = jsonpScriptSrc(chunkId);  // 設置src
            // create error before stack unwound to get useful stacktrace later
           
            var timeout = setTimeout(function(){
                onScriptComplete({ type: 'timeout', target: script }); // 完成後的邏輯
            }, 120000);
            script.onerror = script.onload = onScriptComplete;
            document.head.appendChild(script);  // 插入到頁面上
        }
    }
    return Promise.all(promises);  // 返回一個promise
};
複製代碼

能夠看到,動態引入就是建立script,而後獲得到script標籤的src,將建立好的script標籤插入到head裏面。

  • 實現一個hash路由的按需加載
const render = () => {
  const hash = window.location.hash || '#posts'

  const mainElement = document.querySelector('.main')

  mainElement.innerHTML = ''

  if (hash === '#posts') {
    // mainElement.appendChild(posts())
    import(/* webpackChunkName: 'components' */'./posts/posts').then(({ default: posts }) => {
      mainElement.appendChild(posts())
    })
  } else if (hash === '#album') {
    // mainElement.appendChild(album())
    import(/* webpackChunkName: 'components' */'./album/album').then(({ default: album }) => {
      mainElement.appendChild(album())
    })
  }
}

render()

window.addEventListener('hashchange', render)
複製代碼

輸出文件名hash

  • 生產環境下,文件名使用hash,對客戶端而言,全新的文件名,就是全新的請求,就不會有緩存的問題。
  • hash
    • 整個項目的級別的,只要項目中任何一個地方發生改動,那麼這個hash值都會發生變化,
  • chunkhash
    • chunk級別的,同一路的文件hash值相同,而同一路中的文件發生了變化,這路的文件的hash值都會變化。
    • 好比a.js引入了a.css, a.js和a.css就是一路的。
  • contenthash
    • 文件級別的hash,根據文件生成的hash,文件發生修改,hash就會改變。

webpack是大前端發展到如今不能否認的居功至偉的功臣,如今框架開發通常狀況都會使用高度開箱即用的腳手架工具,可是對於webpack的瞭解也是很必要的。理解咱們的程序是如何一步步的從一隻滿身雞毛的雞變成一隻香噴噴的奧爾良口味的乾坤烤雞(drf烤雞名)

相關文章
相關標籤/搜索