手寫簡易webpack


webpack的定位是一個bundler,最基本的要解決的是將多個JS模塊打包成可以在瀏覽器上運行的代碼。接下來我們將實現一個簡易的miniWebpack也就是一個bundler:由入口文件對代碼進行打包,打包成可以在瀏覽器運行的代碼。

被打包項目介紹

整個演示項目的目錄結構如下所示,其中src下的文件是bundler.js需要打包的代碼。

├── bundler.js
├── package-lock.json
└── src
    ├── index.js
    ├── msg.js
    └── word.js

src下各文件內容如下:
word.js

const word = 'miniWebpack';
export default word;

msg.js

import word from './word.js'
const msg = `hello ${word}`;
export default msg;

index.js

import msg from './msg.js'
console.log(msg)
export default index;

實現bundler.js

要實現bundler,我們需要實現3部分功能:
moduleAnalyser:模塊分析。分析模塊,得到模塊的依賴、代碼等信息。
makeDependenciesGraph:生成依賴圖譜。遍歷打包項目,得到所有需要的模塊的分析結果 。
generateCode:生成可執行代碼。提供require()函數和exports對象,生成可以在瀏覽器執行的代碼。

模塊分析

使用fs模塊讀取module的內容;使用@babel/parser將文件內容轉換成抽象語法樹AST;使用@babel/traverse遍歷了AST ,對每個ImportDeclaration節點(保存的相對於module的路徑信息)做映射,把依賴關系拼裝在 dependencies對象里;使用@babel/core結合@babel/preset-env預設,將AST轉換成了瀏覽器可以執行的代碼。

const moduleAnalyser = (fileName) => {
  // 1.fs模塊根據路徑讀取到了module的內容
  const content = fs.readFileSync(fileName, 'utf-8');
  // 2.使用@babel/parser將文件內容轉換成抽象語法樹AST
  const ast = parser.parse(content, {
    sourceType: 'module'
  })
  // 3.使用@babel/traverse遍歷了AST ,對每個ImportDeclaration節點做映射,把依賴關系拼裝在 dependencies對象里
  let dependencies = {};
  traverse(ast, {
    ImportDeclaration({ node }) {
      const dirName = path.dirname(fileName);
      const newFile = './' + path.join(dirName, node.source.value);
      // key是相對於當前模塊的路徑,value為相對於bundler.js的路徑。
      dependencies[node.source.value] = newFile;
    }
  })
  // 4.使用@babel/core結合@babel/preset-env預設,將AST轉換成了瀏覽器可以執行的代碼
  const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"]
  })
  return {
    fileName,
    dependencies,
    code
  }
}

模塊分析流程圖如下:

調用console.log(moduleAnalyser('./src/index.js')),可以在控制台打印出以下內容:

{ fileName: './src/index.js',
  dependencies: { './msg.js': './src/msg.js' },
  code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports["default"] = void 0;\n\nvar _msg = _interopRequireDefault(require("./msg.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nconsole.log(_msg["default"]);\nvar _default = index;\nexports["default"] = _default;' }

執行moduleAnalyser(module),可以返回module的fileName、dependencies、code信息。注意在code里,import語法已經變成一個require函數了,export語法,也變成了在給一個exports變量賦值。

依賴圖譜生成

調用moduleAnalyser('./src/index.js')拿到入口文件的dependencies映射,接下來再把入口文件的依賴路徑再一次做模塊分析,再把依賴模板的依賴路徑再一次做模塊分析...... 其實就是廣度優先遍歷,可以很輕松得到這次打包所有需要的模塊的分析結果。

//生成依賴圖譜
const makeDependenciesGraph = (entry) => {
  //entryModule:入口文件的dependencies映射
  const entryModule = moduleAnalyser(entry);
  //graphArray:圖譜動態數組,初始只有一個元素entryModule
  const graphArray = [entryModule];
  for (let i = 0; i < graphArray.length; i++) {
    const item = graphArray[i];
    //dependencies:當前模塊的dependencies映射
    const { dependencies } = item;
    //如果當前模塊有依賴文件,則遍歷dependencies,調用moduleAnalyser,對依賴文件進行模板分析
    if (dependencies) {
      for (let j in dependencies) {
        graphArray.push(moduleAnalyser(dependencies[j]))
      }
    }
  }
  //graph:遍歷graphArray生成更利於打包使用的graph。其中key為fileName,value為dependencies和code
  const graph = {};
  graphArray.forEach(item => {
    graph[item.fileName] = {
      dependencies: item.dependencies,
      code: item.code
    }

  })
  return graph;
}

生成依賴圖譜流程圖如下:

調用console.log(makeDependenciesGraph('./src/index.js')),可以在控制台打印出以下內容:

{ './src/index.js': 
   { dependencies: { './msg.js': './src/msg.js' },
     code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports["default"] = void 0;\n\nvar _msg = _interopRequireDefault(require("./msg.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nconsole.log(_msg["default"]);\nvar _default = index;\nexports["default"] = _default;' },
  './src/msg.js': 
   { dependencies: { './word.js': './src/word.js' },
     code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports["default"] = void 0;\n\nvar _word = _interopRequireDefault(require("./word.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nvar msg = "hello ".concat(_word["default"]);\nvar _default = msg;\nexports["default"] = _default;' },
  './src/word.js': 
   { dependencies: {},
     code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports["default"] = void 0;\nvar word = \'miniWebpack\';\nvar _default = word;\nexports["default"] = _default;' } }

生成代碼

我們需要開始生成最終可運行的代碼了。在上文”模塊分析“部分,我們知道利用@babel/core結合@babel/preset-env生成的瀏覽器可執行代碼里,import語法已經變成一個require函數了,export語法,也變成了在給一個exports變量賦值。所以我們的”生成代碼“部分,需要提供一個require函數了和exports對象。

//generateCode 根據依賴圖譜生成瀏覽器可執行代碼
const generateCode = (entry) => {
   //根據entry,調用makeDependenciesGraph生成依賴圖譜graph
  const graph = JSON.stringify(makeDependenciesGraph(entry))
  //根據依賴圖譜生成瀏覽器可執行代碼
  return `
  可運行代碼...
  `
}

為了不污染全局作用域,我們使用立即執行函數來包裝我們的代碼,將依賴圖譜graph作為參數傳入:

(function(graph){
  // todo
})(${graph})

在graph中找到入口文件的code,並運行它:

  return `
  (function(graph){
    function require(module){
      eval(graph[module].code)
    }
    require('${entry}')
  })(${graph})

在入口文件的code,我們同樣需要調用require去獲取依賴模塊模塊導出的對象exports,所以require函數必須有導出對象,還要支持內部調用require函數。但是注意!!此require並非現在聲明的require函數,定義code內部使用的require函數 -> localRequire。因為我們觀察之前編譯出的代碼,可以知道在code中,require函數傳的參數是相對於當前module的相對路徑,但是我們打包生成可運行代碼時,需要的是相對於bundler.js的相對路徑。這時候,我們之前給每個module存的dependencies映射再次派上了用場,localRequire() 傳入依賴相對於module的相對路徑,根據graph對象,返回依賴相對於bundler.js的相對路徑。

(function(graph){
  function require(module){
    function localRequire(relativePath){
      return require(graph[module].dependencies[relativePath])
    }
    var exports={};
    eval(graph[module].code)
    return exports;
  }
  require('${entry}')
})(${graph})

為了防止模塊內部變量污染其它模塊,我們在eval外面包一層立即執行函數,將localRequire、exports和code作為參數傳入。

  (function(graph){
    function require(module){
      function localRequire(relativePath){
        return require(graph[module].dependencies[relativePath])
      }
      var exports={};
      (function(require,exports,code){
        eval(code)
      })(localRequire,exports,graph[module].code)
      return exports;
    }
    require('${entry}')
  })(${graph})

由此一個bundler就寫完了,最終生成的代碼,也是可以直接在瀏覽器中運行的。
調用console.log(generateCode('./src/index.js')),可以在控制台打印出以下內容:

  (function(graph){
    function require(module){
      function localRequire(relativePath){
        return require(graph[module].dependencies[relativePath])
      }
      var exports={};
      (function(require,exports,code){
        eval(code)
      })(localRequire,exports,graph[module].code)
      return exports;
    }
    require('./src/index.js')
  })({"./src/index.js":{"dependencies":{"./msg.js":"./src/msg.js"},"code":"\"use strict\";\n\nvar _msg = _interopRequireDefault(require(\"./msg.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(_msg[\"default\"]); // export default index;"},"./src/msg.js":{"dependencies":{"./word.js":"./src/word.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports[\"default\"] = void 0;\n\nvar _word = _interopRequireDefault(require(\"./word.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nvar msg = \"hello \".concat(_word[\"default\"]);\nvar _default = msg;\nexports[\"default\"] = _default;"},"./src/word.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports[\"default\"] = void 0;\nvar word = 'miniWebpack';\nvar _default = word;\nexports[\"default\"] = _default;"}})  

將這段代碼賦值到瀏覽器控制台,可以看到代碼執行情況:

完整的bundle.js代碼如下:

const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')

//moduleAnalyser:分析一個模塊的文件依賴
const moduleAnalyser = (fileName) => {
  // 1.fs模塊根據路徑讀取到了入口文件的內容
  const content = fs.readFileSync(fileName, 'utf-8');
  // 2.使用@babel/parser將文件內容轉換成抽象語法樹AST
  const ast = parser.parse(content, {
    sourceType: 'module'
  })
  // 3.使用@babel/traverse遍歷了AST ,對每個ImportDeclaration節點(保存的相對於入口文件的路徑)做映射,把依賴關系拼裝在 dependencies對象里
  let dependencies = {};
  traverse(ast, {
    ImportDeclaration({ node }) {
      const dirName = path.dirname(fileName);
      //newFile 相對於bundler.js的相對路徑,打包的時候用這個。
      const newFile = './' + path.join(dirName, node.source.value);
      // key是相對於當前模塊的路徑,value為相對於bundler.js的路徑。
      dependencies[node.source.value] = newFile;
    }
  })
  // 4.使用@babel/core結合@babel/preset-env預設,將AST轉換成了瀏覽器可以執行的代碼
  const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"]
  })
  return {
    fileName,
    dependencies,
    code
  }
}
//生成依賴圖譜
const makeDependenciesGraph = (entry) => {
  //entryModule:入口文件的dependencies映射
  const entryModule = moduleAnalyser(entry);
  //graphArray:圖譜動態數組,初始只有一個元素entryModule
  const graphArray = [entryModule];
  for (let i = 0; i < graphArray.length; i++) {
    const item = graphArray[i];
    //dependencies:當前模塊的dependencies映射
    const { dependencies } = item;
    //如果當前模塊有依賴文件,則遍歷dependencies,調用moduleAnalyser,對依賴文件進行模板分析
    if (dependencies) {
      for (let j in dependencies) {
        graphArray.push(moduleAnalyser(dependencies[j]))
      }
    }
  }
  //graph:遍歷graphArray生成更利於打包使用的graph。其中key為fileName,value為dependencies和code
  const graph = {};
  graphArray.forEach(item => {
    graph[item.fileName] = {
      dependencies: item.dependencies,
      code: item.code
    }

  })
  return graph;
}
//generateCode 根據依賴圖譜生成瀏覽器可執行代碼
const generateCode = (entry) => {
  const graph = JSON.stringify(makeDependenciesGraph(entry))
  //大的閉包,防止打包生成的代碼污染全局環境
  //瀏覽器可執行的代碼里有require方法,有exports對象,bundler.js打包后的代碼需要提供一個require方法和exports對象。
  //小的閉包,防止模塊內部變量污染其它模塊
  //localRequire 傳入依賴相對於module的相對路徑,根據graph對象,返回依賴相對於bundler.js的相對路徑
  return `
  (function(graph){
    function require(module){
      function localRequire(relativePath){
        return require(graph[module].dependencies[relativePath])
      }
      var exports={};
      (function(require,exports,code){
        eval(code)
      })(localRequire,exports,graph[module].code)
      return exports;
    }
    require('${entry}')
  })(${graph})
  `
}

const code = generateCode('./src/index.js');
console.log(code)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM