亚洲中字慕日产2020,大陆极品少妇内射AAAAAA,无码av大香线蕉伊人久久,久久精品国产亚洲av麻豆网站

資訊專欄INFORMATION COLUMN

打包工具的配置教程見的多了,但它們的運行原理你知道嗎?

MoAir / 2011人閱讀

摘要:前端模塊化成為了主流的今天,離不開各種打包工具的貢獻(xiàn)。與此同時,打包工具也會處理好模塊之間的依賴關(guān)系,最終這個大模塊將可以被運行在合適的平臺中。至此,整一個打包工具已經(jīng)完成。明白了當(dāng)中每一步的目的,便能夠明白一個打包工具的運行原理。

前端模塊化成為了主流的今天,離不開各種打包工具的貢獻(xiàn)。社區(qū)里面對于webpack,rollup以及后起之秀parcel的介紹層出不窮,對于它們各自的使用配置分析也是汗牛充棟。為了避免成為一位“配置工程師”,我們需要來了解一下打包工具的運行原理,只有把核心原理搞明白了,在工具的使用上才能更加得心應(yīng)手。

本文基于parcel核心開發(fā)者@ronami的開源項目minipack而來,在其非常詳盡的注釋之上加入更多的理解和說明,方便讀者更好地理解。

1、打包工具核心原理

顧名思義,打包工具就是負(fù)責(zé)把一些分散的小模塊,按照一定的規(guī)則整合成一個大模塊的工具。與此同時,打包工具也會處理好模塊之間的依賴關(guān)系,最終這個大模塊將可以被運行在合適的平臺中。

打包工具會從一個入口文件開始,分析它里面的依賴,并且再進(jìn)一步地分析依賴中的依賴,不斷重復(fù)這個過程,直到把這些依賴關(guān)系理清挑明為止。

從上面的描述可以看到,打包工具最核心的部分,其實就是處理好模塊之間的依賴關(guān)系,而minipack以及本文所要討論的,也是集中在模塊依賴關(guān)系的知識點當(dāng)中。

為了簡單起見,minipack項目直接使用ES modules規(guī)范,接下來我們新建三個文件,并且為它們之間建立依賴:

/* 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)

它們的依賴關(guān)系非常簡單:entry.jsmessage.jsname.js,其中entry.js將會成為打包工具的入口文件。

但是,這里面的依賴關(guān)系只是我們?nèi)祟愃斫獾?,如果要讓機器也能夠理解當(dāng)中的依賴關(guān)系,就需要借助一定的手段了。

2、依賴關(guān)系解析

新建一個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")

接下來,我們會撰寫一個函數(shù),這個函數(shù)接收一個文件作為模塊,然后讀取它里面的內(nèi)容,分析出其所有的依賴項。當(dāng)然,我們可以通過正則匹配模塊文件里面的import關(guān)鍵字,但這樣做非常不優(yōu)雅,所以我們可以使用babylon這個js解析器把文件內(nèi)容轉(zhuǎn)化成抽象語法樹(AST),直接從AST里面獲取我們需要的信息。

得到了AST之后,就可以使用babel-traverse去遍歷這棵AST,獲取當(dāng)中關(guān)鍵的“依賴聲明”,然后把這些依賴都保存在一個數(shù)組當(dāng)中。

最后使用babel-coretransformFromAst方法搭配babel-preset-env插件,把ES6語法轉(zhuǎn)化成瀏覽器可以識別的ES5語法,并且為該js模塊分配一個ID。

let ID = 0

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

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

  // 該文件的所有依賴
  const dependencies = []

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

  // 轉(zhuǎn)化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";

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

var _message2 = _interopRequireDefault(_message);

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

console.log(_message2.default);" }

可見entry.js文件已經(jīng)變成了一個典型的模塊,且依賴已經(jīng)被分析出來了。接下來我們就要遞歸這個過程,把“依賴中的依賴”也都分析出來,也就是下一節(jié)要討論的建立依賴關(guān)系圖集。

3、建立依賴關(guān)系圖集

新建一個名為createGragh()的函數(shù),傳入一個入口文件的路徑作為參數(shù),然后通過createAsset()解析這個文件使之定義成一個模塊。

接下來,為了能夠挨個挨個地對模塊進(jìn)行依賴分析,所以我們維護(hù)一個數(shù)組,首先把第一個模塊傳進(jìn)去并進(jìn)行分析。當(dāng)這個模塊被分析出還有其他依賴模塊的時候,就把這些依賴模塊也放進(jìn)數(shù)組中,然后繼續(xù)分析這些新加進(jìn)去的模塊,直到把所有的依賴以及“依賴中的依賴”都完全分析出來。

與此同時,我們有必要為模塊新建一個mapping屬性,用來儲存模塊、依賴、依賴ID之間的依賴關(guān)系,例如“ID為0的A模塊依賴于ID為2的B模塊和ID為3的C模塊”就可以表示成下面這個樣子:

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

搞清楚了個中道理,就可以開始編寫函數(shù)了。

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

  // 遍歷數(shù)組,分析每一個模塊是否還有其它依賴,若有則把依賴模塊推進(jìn)數(shù)組
  for (const asset of queue) {
    asset.mapping = {}
    // 由于依賴的路徑是相對于當(dāng)前模塊,所以要把相對路徑都處理為絕對路徑
    const dirname = path.dirname(asset.filename)
    // 遍歷當(dāng)前模塊的依賴項并繼續(xù)分析
    asset.dependencies.forEach(relativePath => {
      // 構(gòu)造絕對路徑
      const absolutePath = path.join(dirname, relativePath)
      // 生成依賴模塊
      const child = createAsset(absolutePath)
      // 把依賴關(guān)系寫入模塊的mapping當(dāng)中
      asset.mapping[relativePath] = child.id
      // 把這個依賴模塊也推入到queue數(shù)組中,以便繼續(xù)對其進(jìn)行以來分析
      queue.push(child)
    })
  }

  // 最后返回這個queue,也就是依賴關(guān)系圖集
  return queue
}

可能有讀者對其中的for...of ...循環(huán)當(dāng)中的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";

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

var _message2 = _interopRequireDefault(_message);

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

console.log(_message2.default);",
    mapping: { "./message.js": 1 } },
  { id: 1,
    filename: "example/message.js",
    dependencies: [ "./name.js" ],
    code: ""use strict";

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

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

exports.default = "Hello " + _name.name + "!";",
    mapping: { "./name.js": 2 } },
  { id: 2,
    filename: "example/name.js",
    dependencies: [],
    code: ""use strict";

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

現(xiàn)在依賴關(guān)系圖集已經(jīng)構(gòu)建完成了,接下來就是把它們打包成一個多帶帶的,可直接運行的文件啦!

4、進(jìn)行打包

上一步生成的依賴關(guān)系圖集,接下來將通過CommomJS規(guī)范來實現(xiàn)加載。由于篇幅關(guān)系,本文不對CommomJS規(guī)范進(jìn)行擴展,有興趣的讀者可以參考@阮一峰 老師的一篇文章《瀏覽器加載 CommonJS 模塊的原理與實現(xiàn)》,說得非常清晰。簡單來說,就是通過構(gòu)造一個立即執(zhí)行函數(shù)(function () {})(),手動定義module,exportsrequire變量,最后實現(xiàn)代碼在瀏覽器運行的目的。

接下來就是依據(jù)這個規(guī)范,通過字符串拼接去構(gòu)建代碼塊。

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!”。

至此,整一個打包工具已經(jīng)完成。

5、歸納總結(jié)

經(jīng)過上面幾個步驟,我們可以知道一個模塊打包工具,第一步會從入口文件開始,對其進(jìn)行依賴分析,第二步對其所有依賴再次遞歸進(jìn)行依賴分析,第三步構(gòu)建出模塊的依賴圖集,最后一步根據(jù)依賴圖集使用CommonJS規(guī)范構(gòu)建出最終的代碼。明白了當(dāng)中每一步的目的,便能夠明白一個打包工具的運行原理。

最后再次感謝@ronami的開源項目minipack,其源碼有著更為詳細(xì)的注釋,非常值得大家閱讀。

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://www.ezyhdfw.cn/yun/95449.html

相關(guān)文章

  • 校招社招必備核心前端面試問題與詳細(xì)解答

    摘要:本文總結(jié)了前端老司機經(jīng)常問題的一些問題并結(jié)合個人總結(jié)給出了比較詳盡的答案。網(wǎng)易阿里騰訊校招社招必備知識點。此外還有網(wǎng)絡(luò)線程,定時器任務(wù)線程,文件系統(tǒng)處理線程等等。線程核心是引擎。主線程和工作線程之間的通知機制叫做事件循環(huán)。 showImg(https://segmentfault.com/img/bVbu4aB?w=300&h=208); 本文總結(jié)了前端老司機經(jīng)常問題的一些問題并結(jié)合個...

    DevTalking 評論0 收藏0
  • 校招社招必備核心前端面試問題與詳細(xì)解答

    摘要:本文總結(jié)了前端老司機經(jīng)常問題的一些問題并結(jié)合個人總結(jié)給出了比較詳盡的答案。網(wǎng)易阿里騰訊校招社招必備知識點。此外還有網(wǎng)絡(luò)線程,定時器任務(wù)線程,文件系統(tǒng)處理線程等等。線程核心是引擎。主線程和工作線程之間的通知機制叫做事件循環(huán)。 showImg(https://segmentfault.com/img/bVbu4aB?w=300&h=208); 本文總結(jié)了前端老司機經(jīng)常問題的一些問題并結(jié)合個...

    jonh_felix 評論0 收藏0
  • 校招社招必備核心前端面試問題與詳細(xì)解答

    摘要:本文總結(jié)了前端老司機經(jīng)常問題的一些問題并結(jié)合個人總結(jié)給出了比較詳盡的答案。網(wǎng)易阿里騰訊校招社招必備知識點。此外還有網(wǎng)絡(luò)線程,定時器任務(wù)線程,文件系統(tǒng)處理線程等等。線程核心是引擎。主線程和工作線程之間的通知機制叫做事件循環(huán)。 showImg(https://segmentfault.com/img/bVbu4aB?w=300&h=208); 本文總結(jié)了前端老司機經(jīng)常問題的一些問題并結(jié)合個...

    Rango 評論0 收藏0
  • [譯] 用 Webpack 武裝自己

    摘要:現(xiàn)在來做一個的入口讓我們在文件里進(jìn)行的配置。如果想要顯示它們,我們可以在運行的時候使用你還可以使用,在改變代碼的時候自動進(jìn)行打包。新建文件,里面是一段,告訴使用進(jìn)行預(yù)處理。 本文譯自:Webpack your bags這篇文章由入門到深入的介紹了webpack的功能和使用技巧,真心值得一看。 由于我英語水平有限,而且很少翻譯文章,所以文中的一些語句在翻譯時做了類似語義的轉(zhuǎn)換,望諒解。...

    Tychio 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<