成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

窺探原理:實(shí)現(xiàn)一個簡單的前端代碼打包器 Roid

AlphaGooo / 1902人閱讀

摘要:是一個極其簡單的打包軟件,使用開發(fā)而成,看完本文,你可以實(shí)現(xiàn)一個非常簡單的,但是又有實(shí)際用途的前端代碼打包工具。誠然,你并不需要了解太多編譯原理之類的事情,如果你在此之前對極為熟悉,那么你對前端打包工具一定能非常好的理解。

roid

roid 是一個極其簡單的打包軟件,使用 node.js 開發(fā)而成,看完本文,你可以實(shí)現(xiàn)一個非常簡單的,但是又有實(shí)際用途的前端代碼打包工具。

如果不想看教程,直接看代碼的(全部注釋):點(diǎn)擊地址

為什么要寫 roid ?

我們每天都面對前端的這幾款編譯工具,但是在大量交談中我得知,并不是很多人知道這些打包軟件背后的工作原理,因此有了這個 project 出現(xiàn)。誠然,你并不需要了解太多編譯原理之類的事情,如果你在此之前對 node.js 極為熟悉,那么你對前端打包工具一定能非常好的理解。

弄清楚打包工具的背后原理,有利于我們實(shí)現(xiàn)各種神奇的自動化、工程化東西,比如表單的雙向綁定,自創(chuàng) JavaScript 語法,又如螞蟻金服 ant 中大名鼎鼎的 import 插件,甚至是前端文件自動掃描載入等,能夠極大的提升我們工作效率。

不廢話,我們直接開始。

從一個自增 id 開始
const { readFileSync, writeFileSync } = require("fs")
const path = require("path")
const traverse = require("babel-traverse").default
const { transformFromAst, transform } = require("babel-core")

let ID = 0

// 當(dāng)前用戶的操作的目錄
const currentPath = process.cwd()

id:全局的自增 id ,記錄每一個載入的模塊的 id ,我們將所有的模塊都用唯一標(biāo)識符進(jìn)行標(biāo)示,因此自增 id 是最有效也是最直觀的,有多少個模塊,一統(tǒng)計就出來了。

解析單個文件模塊
function parseDependecies(filename) {
  const rawCode = readFileSync(filename, "utf-8")

  const ast = transform(rawCode).ast

  const dependencies = []

  traverse(ast, {
    ImportDeclaration(path) {
      const sourcePath = path.node.source.value
      dependencies.push(sourcePath)
    }
  })

  // 當(dāng)我們完成依賴的收集以后,我們就可以把我們的代碼從 AST 轉(zhuǎn)換成 CommenJS 的代碼
  // 這樣子兼容性更高,更好
  const es5Code = transformFromAst(ast, null, {
    presets: ["env"]
  }).code

  // 還記得我們的 webpack-loader 系統(tǒng)嗎?
  // 具體實(shí)現(xiàn)就是在這里可以實(shí)現(xiàn)
  // 通過將文件名和代碼都傳入 loader 中,進(jìn)行判斷,甚至用戶定義行為再進(jìn)行轉(zhuǎn)換
  // 就可以實(shí)現(xiàn) loader 的機(jī)制,當(dāng)然,我們在這里,就做一個弱智版的 loader 就可以了
  // parcel 在這里的優(yōu)化技巧是很有意思的,在 webpack 中,我們每一個 loader 之間傳遞的是轉(zhuǎn)換好的代碼
  // 而不是 AST,那么我們必須要在每一個 loader 進(jìn)行 code -> AST 的轉(zhuǎn)換,這樣時非常耗時的
  // parcel 的做法其實(shí)就是將 AST 直接傳遞,而不是轉(zhuǎn)換好的代碼,這樣,速度就快起來了
  const customCode = loader(filename, es5Code)

  // 最后模塊導(dǎo)出
  return {
    id: ID++,
    code: customCode,
    dependencies,
    filename
  }
}

首先,我們對每一個文件進(jìn)行處理。因?yàn)檫@只是一個簡單版本的 bundler ,因此,我們并不考慮如何去解析 css 、md 、txt 等等之類的格式,我們專心處理好 js 文件的打包,因?yàn)閷τ谄渌募?,處理起來過程不太一樣,用文件后綴很容易將他們區(qū)分進(jìn)行不同的處理,在這個版本,我們還是專注 js

const rawCode = readFileSync(filename, "utf-8") 函數(shù)注入一個 filename 顧名思義,就是文件名,讀取其的文件文本內(nèi)容,然后對其進(jìn)行 AST 的解析。我們使用 babeltransform 方法去轉(zhuǎn)換我們的原始代碼,通過轉(zhuǎn)換以后,我們的代碼變成了抽象語法樹( AST ),你可以通過 https://astexplorer.net/, 這個可視化的網(wǎng)站,看看 AST 生成的是什么。

當(dāng)我們解析完以后,我們就可以提取當(dāng)前文件中的 dependencies,dependencies 翻譯為依賴,也就是我們文件中所有的 import xxxx from xxxx,我們將這些依賴都放在 dependencies 的數(shù)組里面,之后統(tǒng)一進(jìn)行導(dǎo)出。

然后通過 traverse 遍歷我們的代碼。traverse 函數(shù)是一個遍歷 AST 的方法,由 babel-traverse 提供,他的遍歷模式是經(jīng)典的 visitor 模式
visitor 模式就是定義一系列的 visitor ,當(dāng)碰到 ASTtype === visitor 名字時,就會進(jìn)入這個 visitor 的函數(shù)。類型為 ImportDeclaration 的 AST 節(jié)點(diǎn),其實(shí)就是我們的 import xxx from xxxx,最后將地址 push 到 dependencies 中.

最后導(dǎo)出的時候,不要忘記了,每導(dǎo)出一個文件模塊,我們都往全局自增 id+ 1,以保證每一個文件模塊的唯一性。

解析所有文件,生成依賴圖
function parseGraph(entry) {
  // 從 entry 出發(fā),首先收集 entry 文件的依賴
  const entryAsset = parseDependecies(path.resolve(currentPath, entry))

  // graph 其實(shí)是一個數(shù)組,我們將最開始的入口模塊放在最開頭
  const graph = [entryAsset]

  for (const asset of graph) {
    if (!asset.idMapping) asset.idMapping = {}

    // 獲取 asset 中文件對應(yīng)的文件夾
    const dir = path.dirname(asset.filename)

    // 每個文件都會被 parse 出一個 dependencise,他是一個數(shù)組,在之前的函數(shù)中已經(jīng)講到
    // 因此,我們要遍歷這個數(shù)組,將有用的信息全部取出來
    // 值得關(guān)注的是 asset.idMapping[dependencyPath] = denpendencyAsset.id 操作
    // 我們往下看
    asset.dependencies.forEach(dependencyPath => {
      // 獲取文件中模塊的絕對路徑,比如 import ABC from "./world"
      // 會轉(zhuǎn)換成 /User/xxxx/desktop/xproject/world 這樣的形式
      const absolutePath = path.resolve(dir, dependencyPath)

      // 解析這些依賴
      const denpendencyAsset = parseDependecies(absolutePath)

      // 獲取唯一 id
      const id = denpendencyAsset.id

      // 這里是重要的點(diǎn)了,我們解析每解析一個模塊,我們就將他記錄在這個文件模塊 asset 下的 idMapping 中
      // 之后我們 require 的時候,能夠通過這個 id 值,找到這個模塊對應(yīng)的代碼,并進(jìn)行運(yùn)行
      asset.idMapping[dependencyPath] = denpendencyAsset.id

      // 將解析的模塊推入 graph 中去
      graph.push(denpendencyAsset)
    })
  }

  // 返回這個 graph
  return graph
}

接下來,我們對模塊進(jìn)行更高級的處理。我們之前已經(jīng)寫了一個 parseDependecies 函數(shù),那么現(xiàn)在我們要來寫一個 parseGraph 函數(shù),我們將所有文件模塊組成的集合叫做 graph(依賴圖),用于描述我們這個項(xiàng)目的所有的依賴關(guān)系,parseGraphentry (入口) 出發(fā),一直手機(jī)完所有的以來文件為止.

在這里我們使用 for of 循環(huán)而不是 forEach ,原因是因?yàn)槲覀冊谘h(huán)之中會不斷的向 graph 中,push 進(jìn)東西,graph 會不斷增加,用 for of 會一直持續(xù)這個循環(huán)直到 graph 不會再被推進(jìn)去東西,這就意味著,所有的依賴已經(jīng)解析完畢,graph 數(shù)組數(shù)量不會繼續(xù)增加,但是用 forEach 是不行的,只會遍歷一次。

for of 循環(huán)中,asset 代表解析好的模塊,里面有 filename , code , dependencies 等東西 asset.idMapping 是一個不太好理解的概念,我們每一個文件都會進(jìn)行 import 操作,import 操作在之后會被轉(zhuǎn)換成 require 每一個文件中的 requirepath 其實(shí)會對應(yīng)一個數(shù)字自增 id,這個自增 id 其實(shí)就是我們一開始的時候設(shè)置的 id,我們通過將 path-id 利用鍵值對,對應(yīng)起來,之后我們在文件中 require 就能夠輕松的找到文件的代碼,解釋這么啰嗦的原因是往往模塊之間的引用是錯中復(fù)雜的,這恰巧是這個概念難以解釋的原因。

最后,生成 bundle
function build(graph) {
  // 我們的 modules 就是一個字符串
  let modules = ""

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

  const wrap = `
  (function(modules) {
    function require(id) {
      const [fn, idMapping] = modules[id];
      function childRequire(filename) {
        return require(idMapping[filename]);
      }
      const newModule = {exports: {}};
      fn(childRequire, newModule, newModule.exports);
      return newModule.exports
    }
    require(0);
  })({${modules}});` // 注意這里需要給 modules 加上一個 {}
  return wrap
}

// 這是一個 loader 的最簡單實(shí)現(xiàn)
function loader(filename, code) {
  if (/index/.test(filename)) {
    console.log("this is loader ")
  }
  return code
}

// 最后我們導(dǎo)出我們的 bundler
module.exports = entry => {
  const graph = parseGraph(entry)
  const bundle = build(graph)
  return bundle
}

我們完成了 graph 的收集,那么就到我們真正的代碼打包了,這個函數(shù)使用了大量的字符串處理,你們不要覺得奇怪,為什么代碼和字符串可以混起來寫,如果你跳出寫代碼的范疇,看我們的代碼,實(shí)際上,代碼就是字符串,只不過他通過特殊的語言形式組織起來而已,對于腳本語言 JS 來說,字符串拼接成代碼,然后跑起來,這種操作在前端非常的常見,我認(rèn)為,這種思維的轉(zhuǎn)換,是擁有自動化、工程化的第一步。

我們將 graph 中所有的 asset 取出來,然后使用 node.js 制造模塊的方法來將一份代碼包起來,我之前做過一個《庖丁解牛:教你如何實(shí)現(xiàn)》node.js 模塊的文章,不懂的可以去看看,https://zhuanlan.zhihu.com/p/...

在這里簡單講述,我們將轉(zhuǎn)換好的源碼,放進(jìn)一個 function(require,module,exports){} 函數(shù)中,這個函數(shù)的參數(shù)就是我們隨處可用的 require,module,以及 exports,這就是為什么我們可以隨處使用這三個玩意的原因,因?yàn)槲覀兠恳粋€文件的代碼終將被這樣一個函數(shù)包裹起來,不過這段代碼中比較奇怪的是,我們將代碼封裝成了 1:[...],2:[...]的形式,我們在最后導(dǎo)入模塊的時候,會為這個字符串加上一個 {},變成 {1:[...],2:[...]},你沒看錯,這是一個對象,這個對象里用數(shù)字作為 key,一個二維元組作為值:

[0] 第一個就是我們被包裹的代碼

[1] 第二個就是我們的 mapping

馬上要見到曙光了,這一段代碼實(shí)際上才是模塊引入的核心邏輯,我們制造一個頂層的 require 函數(shù),這個函數(shù)接收一個 id 作為值,并且返回一個全新的 module 對象,我們倒入我們剛剛制作好的模塊,給他加上 {},使其成為 {1:[...],2:[...]} 這樣一個完整的形式。

然后塞入我們的立即執(zhí)行函數(shù)中(function(modules) {...})(),在 (function(modules) {...})() 中,我們先調(diào)用 require(0),理由很簡單,因?yàn)槲覀兊闹髂K永遠(yuǎn)是排在第一位的,緊接著,在我們的 require 函數(shù)中,我們拿到外部傳進(jìn)來的 modules,利用我們一直在說的全局?jǐn)?shù)字 id 獲取我們的模塊,每個模塊獲取出來的就是一個二維元組。

然后,我們要制造一個 子require,這么做的原因是我們在文件中使用 require 時,我們一般 require 的是地址,而頂層的 require 函數(shù)參數(shù)時 id
不要擔(dān)心,我們之前的 idMapping 在這里就用上了,通過用戶 require 進(jìn)來的地址,在 idMapping 中找到 id

然后遞歸調(diào)用 require(id),就能夠?qū)崿F(xiàn)模塊的自動倒入了,接下來制造一個 const newModule = {exports: {}};,運(yùn)行我們的函數(shù) fn(childRequire, newModule, newModule.exports);,將應(yīng)該丟進(jìn)去的丟進(jìn)去,最后 return newModule.exports 這個模塊的 exports 對象。

這里的邏輯其實(shí)跟 node.js 差別不太大。

最后寫一點(diǎn)測試

測試的代碼,我已經(jīng)放在了倉庫里,想測試一下的同學(xué)可以去倉庫中自行提取。

打滿注釋的代碼也放在倉庫了,點(diǎn)擊地址

git clone https://github.com/Foveluy/roid.git
npm i
node ./src/_test.js ./example/index.js

輸出

this is loader

hello zheng Fang!
welcome to roid, I"m zheng Fang

if you love roid and learnt any thing, please give me a star
https://github.com/Foveluy/roid
參考

https://github.com/blackLearn...

https://github.com/ronami/min...

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

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

相關(guān)文章

  • 窺探 Script 標(biāo)簽(步入現(xiàn)代 Web 開發(fā)魔法世界)

    摘要:而且默認(rèn)帶有執(zhí)行的順序是,,即便是內(nèi)聯(lián)的,依然具有屬性。模塊腳本只會執(zhí)行一次必須符合同源策略模塊腳本在跨域的時候默認(rèn)是不帶的。通常被用作腳本被禁用的回退方案。最后標(biāo)簽真的令人感到興奮。 窺探 Script 標(biāo)簽 0x01 什么是 script 標(biāo)簽? script 標(biāo)簽允許你包含一些動態(tài)腳本或數(shù)據(jù)塊到文檔中,script 標(biāo)簽是非閉合的,你也可以將動態(tài)腳本或數(shù)據(jù)塊當(dāng)做 script 的...

    Terry_Tai 評論0 收藏0
  • 窺探 Script 標(biāo)簽(步入現(xiàn)代 Web 開發(fā)魔法世界)

    摘要:而且默認(rèn)帶有執(zhí)行的順序是,,即便是內(nèi)聯(lián)的,依然具有屬性。模塊腳本只會執(zhí)行一次必須符合同源策略模塊腳本在跨域的時候默認(rèn)是不帶的。通常被用作腳本被禁用的回退方案。最后標(biāo)簽真的令人感到興奮。 窺探 Script 標(biāo)簽 0x01 什么是 script 標(biāo)簽? script 標(biāo)簽允許你包含一些動態(tài)腳本或數(shù)據(jù)塊到文檔中,script 標(biāo)簽是非閉合的,你也可以將動態(tài)腳本或數(shù)據(jù)塊當(dāng)做 script 的...

    gaosboy 評論0 收藏0
  • 割裂前端工程師--- 2017年前端生態(tài)窺探

    摘要:主要兼容的微信的瀏覽器,因?yàn)橐谂笥讶頎I銷,總體來說,會偏設(shè)計以及動畫些。 有一天,我們組內(nèi)的一個小伙伴突然問我,你知道有一個叫重構(gòu)工程師的崗位?這是干什么的?重構(gòu)工程師 這個問題引發(fā)了我對前端領(lǐng)域發(fā)展的思考,所以我來梳理下前端領(lǐng)域的發(fā)展過程,順便小小的預(yù)測下2017年的趨勢。不想看回憶的,可以直接跳到后面看展望。 神說,要有光,就有了光 自1991年蒂姆·伯納斯-李公開提及HTML...

    duan199226 評論0 收藏0
  • 割裂前端工程師--- 2017年前端生態(tài)窺探

    摘要:主要兼容的微信的瀏覽器,因?yàn)橐谂笥讶頎I銷,總體來說,會偏設(shè)計以及動畫些。 有一天,我們組內(nèi)的一個小伙伴突然問我,你知道有一個叫重構(gòu)工程師的崗位?這是干什么的?重構(gòu)工程師 這個問題引發(fā)了我對前端領(lǐng)域發(fā)展的思考,所以我來梳理下前端領(lǐng)域的發(fā)展過程,順便小小的預(yù)測下2017年的趨勢。不想看回憶的,可以直接跳到后面看展望。 神說,要有光,就有了光 自1991年蒂姆·伯納斯-李公開提及HTML...

    miguel.jiang 評論0 收藏0

發(fā)表評論

0條評論

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