打包東西的設置教程見的多了,但它們的運轉道理你知道嗎?

《打包東西的設置教程見的多了,但它們的運轉道理你知道嗎?》

前端模塊化成為了主流的本日,離不開種種打包東西的孝敬。社區內里關於webpack,rollup以及後起之秀parcel的引見屢見不鮮,關於它們各自的運用設置剖析也是汗牛充棟。為了防止成為一名“設置工程師”,我們須要來相識一下打包東西的運轉原理,只需把中心原理搞邃曉了,在東西的運用上才越發隨心所欲。

本文基於parcel中心開發者@ronami的開源項目minipack而來,在其異常詳實的解釋之上到場更多的明白和申明,輕易讀者更好地明白。

1、打包東西中心原理

望文生義,打包東西就是擔任把一些疏散的小模塊,依據肯定的劃定規矩整合成一個大模塊的東西。與此同時,打包東西也會處置懲罰好模塊之間的依靠關聯,終究這個大模塊將可以被運轉在適宜的平台中。

打包東西會從一個進口文件最先,剖析它內里的依靠,而且再進一步地剖析依靠中的依靠,不停反覆這個歷程,直到把這些依靠關聯理清挑明為止。

從上面的形貌可以看到,打包東西最中心的部份,實在就是處置懲罰好模塊之間的依靠關聯,而minipack以及本文所要議論的,也是集合在模塊依靠關聯的知識點當中。

為了簡樸起見,minipack項目直接運用ES modules範例,接下來我們新建三個文件,而且為它們之間豎立依靠:

/* name.js */

export const name = 'World'
/* message.js */

import { name } from './name.js'

export default `Hello ${name}!`
/* entry.js */

import message from './message.js'

console.log(message)

它們的依靠關聯異常簡樸:entry.jsmessage.jsname.js,其中entry.js將會成為打包東西的進口文件。

然則,這內里的依靠關聯只是我們人類所明白的,假如要讓机械也可以明白當中的依靠關聯,就須要藉助肯定的手腕了。

2、依靠關聯剖析

新建一個js文件,命名為minipack.js,起首引入必要的東西。

/* minipack.js */

const fs = require('fs')
const path = require('path')
const babylon = require('babylon')
const traverse = require('babel-traverse').default
const { transformFromAst } = require('babel-core')

接下來,我們會撰寫一個函數,這個函數吸收一個文件作為模塊,然後讀取它內里的內容,剖析出其一切的依靠項。固然,我們可以經由過程正則婚配模塊文件內里的import癥結字,但如許做異常不文雅,所以我們可以運用babylon這個js剖析器把文件內容轉化成籠統語法樹(AST),直接從AST內里獵取我們須要的信息。

得到了AST以後,就可以運用babel-traverse去遍歷這棵AST,獵取當中癥結的“依靠聲明”,然後把這些依靠都保存在一個數組當中。

末了運用babel-coretransformFromAst要領搭配babel-preset-env插件,把ES6語法轉化成瀏覽器可以辨認的ES5語法,而且為該js模塊分派一個ID。

let ID = 0

function createAsset (filename) {
  // 讀取文件內容
  const content = fs.readFileSync(filename, 'utf-8')

  // 轉化成AST
  const ast = babylon.parse(content, {
    sourceType: 'module',
  });

  // 該文件的一切依靠
  const dependencies = []

  // 獵取依靠聲明
  traverse(ast, {
    ImportDeclaration: ({ node }) => {
      dependencies.push(node.source.value);
    }
  })

  // 轉化ES6語法到ES5
  const {code} = transformFromAst(ast, null, {
    presets: ['env'],
  })

  // 分派ID
  const id = ID++

  // 返回這個模塊
  return {
    id,
    filename,
    dependencies,
    code,
  }
}

運轉createAsset('./example/entry.js'),輸出以下:

{ id: 0,
  filename: './example/entry.js',
  dependencies: [ './message.js' ],
  code: '"use strict";\n\nvar _message = require("./message.js");\n\nvar _message2 = _interopRequireDefault(_message);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nconsole.log(_message2.default);' }

可見entry.js文件已變成了一個典範的模塊,且依靠已被剖析出來了。接下來我們就要遞歸這個歷程,把“依靠中的依靠”也都剖析出來,也就是下一節要議論的豎立依靠關聯圖集。

3、豎立依靠關聯圖集

新建一個名為createGragh()的函數,傳入一個進口文件的途徑作為參數,然後經由過程createAsset()剖析這個文件使之定義成一個模塊。

接下來,為了可以挨個挨個地對模塊舉行依靠剖析,所以我們保護一個數組,起首把第一個模塊傳進去並舉行剖析。當這個模塊被剖析出另有其他依靠模塊的時刻,就把這些依靠模塊也放進數組中,然後繼承剖析這些新加進去的模塊,直到把一切的依靠以及“依靠中的依靠”都完整剖析出來。

與此同時,我們有必要為模塊新建一個mapping屬性,用來貯存模塊、依靠、依靠ID之間的依靠關聯,比方“ID為0的A模塊依靠於ID為2的B模塊和ID為3的C模塊”就可以示意成下面這個模樣:

{
  0: [function A () {}, { 'B.js': 2, 'C.js': 3 }]
}

搞清楚了其中原理,就可以最先編寫函數了。

function createGragh (entry) {
  // 剖析傳入的文件為模塊
  const mainAsset = createAsset(entry)
  
  // 保護一個數組,傳入第一個模塊
  const queue = [mainAsset]

  // 遍曆數組,剖析每個模塊是不是另有別的依靠,如有則把依靠模塊推動數組
  for (const asset of queue) {
    asset.mapping = {}
    // 因為依靠的途徑是相關於當前模塊,所以要把相對途徑都處置懲罰為絕對途徑
    const dirname = path.dirname(asset.filename)
    // 遍歷當前模塊的依靠項並繼承剖析
    asset.dependencies.forEach(relativePath => {
      // 組織絕對途徑
      const absolutePath = path.join(dirname, relativePath)
      // 天生依靠模塊
      const child = createAsset(absolutePath)
      // 把依靠關聯寫入模塊的mapping當中
      asset.mapping[relativePath] = child.id
      // 把這個依靠模塊也推入到queue數組中,以便繼承對其舉行以來剖析
      queue.push(child)
    })
  }

  // 末了返回這個queue,也就是依靠關聯圖集
  return queue
}

可能有讀者對其中的for...of ...輪迴當中的queue.push有點迷,然則只需嘗試過下面這段代碼就可以搞邃曉了:

var numArr = ['1', '2', '3']

for (num of numArr) {
  console.log(num)
  if (num === '3') {
    arr.push('Done!')
  }
}

嘗試運轉一下createGraph('./example/entry.js'),就可以看到以下的輸出:

[ { id: 0,
    filename: './example/entry.js',
    dependencies: [ './message.js' ],
    code: '"use strict";\n\nvar _message = require("./message.js");\n\nvar _message2 = _interopRequireDefault(_message);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nconsole.log(_message2.default);',
    mapping: { './message.js': 1 } },
  { id: 1,
    filename: 'example/message.js',
    dependencies: [ './name.js' ],
    code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\n\nvar _name = require("./name.js");\n\nexports.default = "Hello " + _name.name + "!";',
    mapping: { './name.js': 2 } },
  { id: 2,
    filename: 'example/name.js',
    dependencies: [],
    code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nvar name = exports.name = \'world\';',
    mapping: {} } ]

如今依靠關聯圖集已構建完成了,接下來就是把它們打包成一個零丁的,可直接運轉的文件啦!

4、舉行打包

上一步天生的依靠關聯圖集,接下來將經由過程CommomJS範例來完成加載。因為篇幅關聯,本文不對CommomJS範例舉行擴大,有興緻的讀者可以參考@阮一峰 先生的一篇文章《瀏覽器加載 CommonJS 模塊的原理與完成》,說得異常清楚。簡樸來講,就是經由過程組織一個馬上實行函數(function () {})(),手動定義moduleexportsrequire變量,末了完成代碼在瀏覽器運轉的目標。

接下來就是依據這個範例,經由過程字符串拼接去構建代碼塊。

function bundle (graph) {
  let modules = ''

  graph.forEach(mod => {
    modules += `${mod.id}: [
      function (require, module, exports) { ${mod.code} },
      ${JSON.stringify(mod.mapping)},
    ],`
  })

  const result = `
    (function(modules) {
      function require(id) {
        const [fn, mapping] = modules[id];

        function localRequire(name) {
          return require(mapping[name]);
        }

        const module = { exports : {} };

        fn(localRequire, module, module.exports);

        return module.exports;
      }

      require(0);
    })({${modules}})
  `
  return result
}

末了運轉bundle(createGraph('./example/entry.js')),輸出以下:

(function (modules) {
  function require(id) {
    const [fn, mapping] = modules[id];

    function localRequire(name) {
      return require(mapping[name]);
    }

    const module = { exports: {} };

    fn(localRequire, module, module.exports);

    return module.exports;
  }

  require(0);
})({
  0: [
    function (require, module, exports) {
      "use strict";

      var _message = require("./message.js");

      var _message2 = _interopRequireDefault(_message);

      function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

      console.log(_message2.default);
    },
    { "./message.js": 1 },
  ], 1: [
    function (require, module, exports) {
      "use strict";

      Object.defineProperty(exports, "__esModule", {
        value: true
      });

      var _name = require("./name.js");

      exports.default = "Hello " + _name.name + "!";
    },
    { "./name.js": 2 },
  ], 2: [
    function (require, module, exports) {
      "use strict";

      Object.defineProperty(exports, "__esModule", {
        value: true
      });
      var name = exports.name = 'world';
    },
    {},
  ],
})

這段代碼將可以直接在瀏覽器運轉,輸出“Hello world!”。

至此,整一個打包東西已完成。

5、歸結總結

經由上面幾個步驟,我們可以曉得一個模塊打包東西,第一步會從進口文件最先,對其舉行依靠剖析,第二步對其一切依靠再次遞歸舉行依靠剖析,第三步構建出模塊的依靠圖集,末了一步依據依靠圖集運用CommonJS範例構建出終究的代碼。邃曉了當中每一步的目標,便可以邃曉一個打包東西的運轉原理。

末了再次謝謝@ronami的開源項目minipack,其源碼有着更加細緻的解釋,異常值得人人瀏覽。

    原文作者:jrainlau
    原文地址: https://segmentfault.com/a/1190000015291911
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞