webpack源碼分析之一:文件打包

前言

自動化打包工具webpack,相信不少人和我同樣嘗試着研究下它,可是繁雜的功能以及高度抽象的代碼實在是很難理解,因此筆者只能經過github的webpack的第一次提交進行分析,實現,並將實現的一些心得分享一下。html

功能分析

對於node端來說,有commonjs來規範模塊的標識,定義,引用。而瀏覽器端因爲缺少原生對象支持就須要經過自我實現來模擬commonjs規範。
webpack是經過一個IIFE當即調用函數表達式去實現這個規範的。簡要的去註釋,去除內部運行的代碼,其格式以下:node

(function(module){})([function(){},function(){}]);

簡單點說就是各個模塊代碼以數組的形式傳遞給運行函數,在進行存儲。詳細分析能夠參考簡要分析webpack打包後代碼webpack

因此實現以上的功能需求點以下:git

  • 文件路徑分析與定位resolve
  • 文件編譯&解析,分析出依賴文件parse
  • 生成須要打包的文件樹depTree
  • 將依賴文件寫入輸出文件內writeChunk

文件分析與定位

本功能和node的require相似,故有參考node require源碼github

文件分析,將文件爲兩種類型web

  • 以 "./","../","/" 標識符開頭的路徑文件模塊json

    • 該類文件會經過path.join 轉化爲真實的路徑而定位。
  • 自定義的文件模塊segmentfault

    • 這類相對比較麻煩,他在當前目錄下面的node_modules,查找文件,未找到則一路向上查找,最終查找到或者拋出異常。如:
[ '/Users/zhujian/Documents/workspace/webpack/simple-webpack/node_modules',
  '/Users/zhujian/Documents/workspace/webpack/node_modules',
  '/Users/zhujian/Documents/workspace/node_modules',
  '/Users/zhujian/Documents/node_modules',
  '/Users/zhujian/node_modules',
  '/Users/node_modules',
  '/node_modules' ]

文件定位數組

  • 對於部分文件並無帶擴展名,此時有默認的擴展名依次以.js,.jsx爲後綴依次補充。固然咱們能夠用傳入extensions,修改默認的擴展名。
{extensions:['js','jsx','jpg']}
  • 當發現該路徑爲文件夾時則,則依次查找以下文件瀏覽器

    • package.json(main字段)
    • index+(擴展名)

文件解析

  • 文件能夠定位以後,則是解析定位下來的文件了,本文用的是exprima,文檔如parser規範文檔

    • esprima解析文件,返回一個語法樹。
    • 對語法樹進行遍歷,對遇到type 爲CallExpression,且其callee爲name爲require的節點,將該節點的value,以及下標包裝成對象儲存起來。

好比

const b = require('./b');

解析後

....
"init": {
        "type": "CallExpression",
        "callee": {
            "type": "Identifier",
            "name": "require",
            "range": [
                10,
                17
            ]
        },
        "arguments": [
            {
                "type": "Literal",
                "value": "./b",
                "raw": "'./b'",
                "range": [
                    18,
                    23
                ]
            }
        ],
        "range": [
            10,
            24
        ]
    },
 ....

咱們要作的就是提取value "./b",以及該字符串在文件所處的位置range。

文件樹生成

主要是從入口文件開始,將全部依賴的js,以及其內容,分配的id組成一個可操做的扁平化的對象和存儲着name與id對應的map對象。

實現手法上也是遞歸resolve函數,獲取到各個文件的依賴,文件,id的信息,最後獲得depTree對象

舉個例子:

{ modules: 
   { '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/a.js': 
      { filename: '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/a.js',
        id: 0,
        requires: [Array],
        rangeRequires: [Array],
        source: 'const b = require(\'./b\');\nconst c = require(\'c\');\nconst {e, f, g} = require(\'./m\');\n\n 
      },
     '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/b.js': 
      { filename: '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/b.js',
        id: 1,
        requires: [],
        rangeRequires: [],
        source: 'const b = \'b\';\n\nmodule.exports = b;\n' 
      },
     '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/node_modules/c.js': 
      { filename: '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/node_modules/c.js',
        id: 2,
        requires: [],
        rangeRequires: [],
        source: 'const c = \'c\';\n\nmodule.exports = c;\n' 
      },
     '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/m.js': 
      { filename: '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/m.js',
        id: 3,
        requires: [],
        rangeRequires: [],
        source: '// const core = require(\'./core\');\nconst a = 1;\n\n
      },
  nextModuleId: 4,
  mapNameToId: 
   { '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/a.js': 0,
     '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/b.js': 1,
     '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/node_modules/c.js': 2,
     '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/m.js': 3 
     } 
 }

文件寫入

寫入主函數,替換入口的執行函數。這塊會用到以前的path和id關係的map對象,經過路口文件的絕對路徑,找出入口文件的mainId,並進行替換。

寫入參數數組。遍歷文件樹,將文件節點的source內容替換掉

大體以下:

require('module') 替換爲__webpack_require__(0)
  • 這個地方要考慮的點是

    • 若是用replace替換的話,會影響source帶部分關鍵字的內容,不可取。
    • 用索引替換的字符串的話,一旦第一個替換成功,整個字符串長度發生變化,原先的索引下標就失效了。

官方實現

const result = [source];
    replaces.forEach(replace => {
        const {from, value, end} = replace;
        const source = result.shift();
        result.unshift(source.substr(0, from), value, source.substr(end))
    });

代碼實現

本人的簡易版webpack實現simple-webpack

(完)

參考資料

  1. webpack早期源碼
  2. require源碼解讀
  3. node require源碼
  4. parser規範文檔
  5. 簡要分析webpack打包後代碼
相關文章
相關標籤/搜索