webpack4 基礎?

嘻嘻xixi發表於2019-05-04

假期沒有出去玩,但是我覺得很充實吼/(ㄒoㄒ)/~~

根據Sean在 frontend masters上的課程加上個人的理解 Sean 是 webpack 的核心貢獻者哦?課程程式碼

為什麼要使用webpack

傳統方法在瀏覽器中執行JS

  1. 一個功能載入一個script 標籤 e.g. Jquery , Swiper
  2. 載入一個巨大的JS檔案。

script標籤載入的弊端

  1. 擴充套件性太差,標籤很多的情況下維持標籤的順序很痛苦。
  2. 全域性變數汙染
  3. 載入過多的JS檔案有載入效能問題,因為瀏覽器的並行連線數有限制。可以參考html - Max parallel http connections in a browser? - Stack Overflow

單獨一個JS檔案的弊端

這裡應該是說所有程式碼寫在一起,不是按照模組組織最後打包出一個檔案來的。

  1. 不同功能的作用域問題
  2. 使用者需要載入的檔案過大。
  3. 維護性和可讀性很差。

立即執行函式IIFEs

Immediately invoked function expressions

var outerScope = 1;
const whatever = (function(dataNowUsedInside) {
  var outerScope = 4;
  return {
    someAttribute: "you want"
  };
})(1);
console.log(outerScope);  //1
複製程式碼

立即執行函式可以解決作用域衝突的問題,如上因為函式作用域的存在不會汙染外部作用域。 PS: 另外上面的這個暴露出一個模組的模式叫Revealing Module pattern 可以參考 Learning JavaScript Design Patterns

Make, Gulp, Grunt, Broccoli , Brunch 這些工具通過一個檔案當做一個立即執行函式,將這些立即執行函式連線起來打包成一個檔案。

這些工具的弊端:

  1. 每次更改一個檔案都要重新構建所有檔案
  2. 無法剔除沒有使用過的程式碼,比如引入Lodash, 就用了幾個函式,結果500Kb的檔案都被引入了。
  3. 立即執行函式過多的話可能會造成效能問題。 參考這篇文章The cost of small modules | Read the Tea Leaves。文章裡提到一個是函式內巢狀函式,還有在關聯陣列中檢視模組在模組越來越多的情況下會暴露出意外的效能問題。另外立即執行函式會導致引擎立刻解析函式 (eager parse),大量的立即執行函式也會導致應用解析變慢。
  4. 沒法做懶載入。

Javascript模組優缺點

CommonJS

node裡沒有<script>標籤,怎麼載入JS程式碼呢,於是出來了CommonJS.

  1. 但是 CommonJS 不支援瀏覽器
  2. 沒有動態繫結,什麼是動態繫結可以參考 深入es module
  3. CommonJS解析演算法慢,因為它是同步的
  4. 沒有靜態分析,沒法剔除無用程式碼

打包器

開發者不願意從某個庫的網上下載JS檔案然後放到專案中來,他們想通過NPM分享模組程式碼。但是NPM上的是CommonJs模組的,所以就出現了Browserify 之類的工具把CommonJS風格的程式碼解析require宣告,按照你引用的順序打包成瀏覽器可以執行的程式碼。

AMD

除了CommonJS還有很多模組載入模式。甚至還有AMD+CommonJS的模組載入模式。

沒有真正的模組系統,沒有node 瀏覽器都支援的。直到ESM的出現。

ESM

ES2015和模組是獨立的兩個部分。模組標準原名是harmony modules specification。你甚至可以用ES3的語法搭配模組使用。 ESM的問題:

  1. node 中的ESM尚未實現。
  2. 相容問題。

webpack 能做什麼

webpack 是一個模組打包器

  1. 庫的作者使用他們喜歡的模組系統,AMD, CommonJS, webpack 可以讓你使用任何的模組格式甚至混合,將它們最終打包成瀏覽器可以執行的程式碼。
  2. 在構建階段建立動態打包模組(懶載入的模組)
  3. 不僅打包JS資源還包括html, css甚至圖片

除錯webpack

  1. 按照除錯node的方法
    node --inspect --inspect-brk ./node_modules/webpack/bin/webpack.js
複製程式碼

之後在瀏覽器中開啟,可以參考debugger 2. 利用vscode

    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "webpack",
            "program": "${workspaceFolder}/node_modules/webpack/bin/webpack.js"
        }
    ]
複製程式碼

可以參考下debugging-webpack

webpack 基礎概念

什麼是依賴圖(dependency graph)

webpack根據模組之間的依賴關係遞迴構建而成 dependency-graph

entry

告訴webpack從那個檔案開始構建它的依賴圖。webpack4預設配置是src/index.js

output

告訴webpack打包好的檔案的位置名稱等。webpack4預設配置會打包到dist/main.js

loader

webpack開箱只理解js和json檔案。loader可以將其他模組載入進來轉換成js 模組,然後載入到依賴圖裡。webpack預設使用的是acorn 解析js檔案,acorn預設只會處理stage4的提案。所以比較新的特性webpack是處理不了的,這個時候可以藉助babel 這裡舉兩個loader

babel-loader

    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env']
                    }
                }
            }
        ]
    }
複製程式碼

babel-loader8.x需要配合使用的是@babel-core以及@babel/preset-env

    npm install -D babel-loader @babel/core @babel/preset-env 
複製程式碼

babel-loader: webpack和babel之間的橋樑 @babel/core: 解析檔案並且生成檔案 @babel/preset-env: 相應的轉換規則

css loader

    npm install -D css-loader style-loader
複製程式碼
    {
        test: /\.css$/,
        loaders: ['style-loader', 'css-loader']
    }
複製程式碼

loader解析順序是從右向左可以 可以理解成這樣styleLoader(cssLoader()) 一個方法的輸出是另一個方法的引數。 css-loader: 負責將css檔案轉換成js style-loader: 將css內容插入<style>標籤中 ps:我在安裝了相關loader之後, 在vscode裡去嘗試從node_modules目錄裡找發現怎麼也找不到相關loader o(╯□╰)o,最後我直接開啟了檔案目錄找到了。。。

如何寫一個loader

write-a-loader 總結下就是loader是一個函式,在webpack配置裡可以配置loader的別名,以及你自己寫的loader的解析位置。 函式的引數就是上一個loader處理好後的字串。loader也分同步和非同步的,babel-loader就是非同步的。

plugin

plugin可以在webpack的構建的任意時刻執行一些特定任務,比如打包優化,插入環境變數等。 webpack原始碼有很大部分都是外掛寫的,具體會再出一篇詳細講下webpack是如何工作的,這裡就簡單介紹下。

如何寫一個外掛

    class myFirstWebpackPlugin {
        apply(compiler){
            compiler.hooks.done.tapAsync('myFirstWebpackPlugin',(stats, cb)=>{
                cb()
            })
        }
    }
    module.exports = myFirstWebpackPlugin
複製程式碼

以上程式碼是基於webpack4的,3.x的版本是如下

    class myFirstWebpackPlugin {
        apply(compiler){
            compiler.plugin("done",(stats)=>{
                
            })
        }
    }
    module.exports = myFirstWebpackPlugin
複製程式碼

3.x版本的hook是字串形式,需要人為去記憶,在webpack4中將其作為變數的屬性,文件化後在編輯器中輸入會有提示,目前尚未文件化完成。

外掛可以在webpack構建的任意時刻執行,和鉤子函式一樣,提前在這些時間點註冊回撥函式,外掛的鉤子同樣可以是同步或者非同步的。上面的例項是在構建完成後執行,回撥函式的引數裡包括一系列統計資料,構建開始時間結束時間等。

webpack程式碼拆分(code splitting)

程式碼拆分的用途:

  1. 不需要在首屏載入的比較大的第三方庫比如three.js
  2. 模態框提示框等使用者可能不會去點選的
  3. 路由元件

程式碼拆分的好處:

  1. 提升首屏載入速度,提升使用者體驗特別是移動端
  2. 有助於提升SEO,google會降低載入慢的網站的權重

webpack會將import()引用的模組打包到單獨打包到輸出目錄下,webpack內建的acorn解析器預設是隻解析stage4的,import()仍然在stage3,proposals。這是因為webpack引用了acorn-dynamic-import可以支援在不使用babel的情況下解析import()語法。如果專案中使用了babel-loader需要再配置@babel/plugin-syntax-dynamic-import,否則babel會報錯,因為babel也是需要將程式碼解析轉換成抽象語法樹的。 webpack程式碼拆分分為靜態和動態程式碼拆分,但是無論哪種,都是將非同步請求的模組提前打包到輸出目錄中。

靜態程式碼拆分(static code splitting)

// main.js
    const getFooter = () => import('./footer');
    button.addEventListener('click',e=>{
        getFooter().then(m=>{
            document.body.appendChild(m.footer)
        })
    })
// footer.js
const footer = document.createElement("footer");
export { footer };
複製程式碼

import()返回的是一個promise

動態程式碼拆分(dynamic code splitting)

const setButtonStyle = color => import(`./button-style/${color}`);  
複製程式碼

上面的./button-style是部分地址,webpack會將這個地址下面的所有js模組進行單獨打包。這個可以用在當這個目錄下有很多檔案需要程式碼拆分的時候。

魔法註釋(magic comment)

內聯註釋控制webpack動態打包行為,為什麼要使用它?因為這樣不會增加新語法,不和動態import以及loader規範衝突。

import(
    /* webpackMode: "lazy-once" */
    `./button-style/${color}`); 
複製程式碼

比如上面這個webpackMode預設是lazy它會將./button-style下的所有模組都單獨打包;lazy-once則會將其下面的所有模組打成一個包常用於開發環境減少打包次數。 更多註釋選項請參考官網magic-comments

流行庫中的程式碼拆分

vue-code-splitting-pattern

react-loadable

相關文章