窺伺道理:完成一個簡樸的前端代碼打包器 Roid

roid

roid 是一個極為簡樸的打包軟件,運用 node.js 開闢而成,看完本文,你可以完成一個異常簡樸的,然則又有現實用處的前端代碼打包東西。

假如不想看教程,直接看代碼的(悉數詮釋):點擊地點

為何要寫 roid ?

我們天天都面臨前端的這幾款編譯東西,然則在大批攀談中我得知,並非很多人曉得這些打包軟件背地的事變道理,因而有了這個 project 湧現。固然,你並不需要相識太多編譯道理之類的事變,假如你在此之前對 node.js 極為熟習,那末你對前端打包東西一定能異常好的明白。

弄清楚打包東西的背地道理,有利於我們完成種種奇異的自動化、工程化東西,比方表單的雙向綁定,自創 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

// 當前用戶的操縱的目次
const currentPath = process.cwd()

id:全局的自增 id ,紀錄每一個載入的模塊的 id ,我們將一切的模塊都用唯一標識符舉行標示,因而自增 id 是最有效也是最直觀的,有多少個模塊,一統計就出來了。

剖析單個文件模塊

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)
    }
  })

  // 當我們完成依靠的網絡今後,我們就可以把我們的代碼從 AST 轉換成 CommenJS 的代碼
  // 這模樣兼容性更高,更好
  const es5Code = transformFromAst(ast, null, {
    presets: ['env']
  }).code

  // 還記得我們的 webpack-loader 體系嗎?
  // 詳細完成就是在這裏可以完成
  // 經由歷程將文件名和代碼都傳入 loader 中,舉行推斷,以至用戶定義行動再舉行轉換
  // 就可以完成 loader 的機制,固然,我們在這裏,就做一個弱智版的 loader 就可以了
  // parcel 在這裏的優化技能是很有意義的,在 webpack 中,我們每一個 loader 之間通報的是轉換好的代碼
  // 而不是 AST,那末我們必需要在每一個 loader 舉行 code -> AST 的轉換,如許時異常耗時的
  // parcel 的做法實在就是將 AST 直接通報,而不是轉換好的代碼,如許,速率就快起來了
  const customCode = loader(filename, es5Code)

  // 末了模塊導出
  return {
    id: ID++,
    code: customCode,
    dependencies,
    filename
  }
}

起首,我們對每一個文件舉行處置懲罰。由於這隻是一個簡樸版本的 bundler ,因而,我們並不斟酌怎樣去剖析 cssmdtxt 等等之類的花樣,我們用心處置懲罰好 js 文件的打包,由於關於其他文件而言,處置懲罰起來歷程不太一樣,用文件後綴很輕易將他們辨別舉行差異的處置懲罰,在這個版本,我們照樣專註 js

const rawCode = readFileSync(filename, 'utf-8') 函數注入一個 filename 望文生義,就是文件名,讀取其的文件文本內容,然後對其舉行 AST 的剖析。我們運用 babeltransform 要領去轉換我們的原始代碼,經由歷程轉換今後,我們的代碼變成了籠統語法樹( AST ),你可以經由歷程 https://astexplorer.net/, 這個可視化的網站,看看 AST 天生的是什麼。

當我們剖析完今後,我們就可以提取當前文件中的 dependenciesdependencies 翻譯為依靠,也就是我們文件中一切的 import xxxx from xxxx,我們將這些依靠都放在 dependencies 的數組內里,以後一致舉行導出。

然後經由歷程 traverse 遍歷我們的代碼。traverse 函數是一個遍歷 AST 的要領,由 babel-traverse 供應,他的遍歷情勢是典範的 visitor 情勢
visitor 情勢就是定義一系列的 visitor ,當遇到 ASTtype === visitor 名字時,就會進入這個 visitor 的函數。範例為 ImportDeclaration 的 AST 節點,實在就是我們的 import xxx from xxxx,末了將地點 push 到 dependencies 中.

末了導出的時刻,不要忘記了,每導出一個文件模塊,我們都往全局自增 id+ 1,以保證每一個文件模塊的唯一性。

剖析一切文件,天生依靠圖

function parseGraph(entry) {
  // 從 entry 動身,起首網絡 entry 文件的依靠
  const entryAsset = parseDependecies(path.resolve(currentPath, entry))

  // graph 現實上是一個數組,我們將最最先的進口模塊放在最開首
  const graph = [entryAsset]

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

    // 獵取 asset 中文件對應的文件夾
    const dir = path.dirname(asset.filename)

    // 每一個文件都邑被 parse 出一個 dependencise,他是一個數組,在之前的函數中已講到
    // 因而,我們要遍歷這個數組,將有效的信息悉數取出來
    // 值得關注的是 asset.idMapping[dependencyPath] = denpendencyAsset.id 操縱
    // 我們往下看
    asset.dependencies.forEach(dependencyPath => {
      // 獵取文件中模塊的絕對路徑,比方 import ABC from './world'
      // 會轉換成 /User/xxxx/desktop/xproject/world 如許的情勢
      const absolutePath = path.resolve(dir, dependencyPath)

      // 剖析這些依靠
      const denpendencyAsset = parseDependecies(absolutePath)

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

      // 這裡是主要的點了,我們剖析每剖析一個模塊,我們就將他紀錄在這個文件模塊 asset 下的 idMapping 中
      // 以後我們 require 的時刻,可以經由歷程這個 id 值,找到這個模塊對應的代碼,並舉行運轉
      asset.idMapping[dependencyPath] = denpendencyAsset.id

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

  // 返回這個 graph
  return graph
}

接下來,我們對模塊舉行更高等的處置懲罰。我們之前已寫了一個 parseDependecies 函數,那末如今我們要來寫一個 parseGraph 函數,我們將一切文件模塊構成的鳩合叫做 graph(依靠圖),用於形貌我們這個項目的一切的依靠關聯,parseGraphentry (進口) 動身,一向手機完一切的以來文件為止.

在這裏我們運用 for of 輪迴而不是 forEach ,緣由是由於我們在輪迴之中會不停的向 graph 中,push 進東西,graph 會不停增添,用 for of 會一向延續這個輪迴直到 graph 不會再被推進去東西,這就意味着,一切的依靠已剖析終了,graph 數組數目不會繼承增添,然則用 forEach 是不可的,只會遍歷一次。

for of 輪迴中,asset 代表剖析好的模塊,內里有 filename , code , dependencies 等東西 asset.idMapping 是一個不太好明白的觀點,我們每一個文件都邑舉行 import 操縱,import 操縱在以後會被轉換成 require 每一個文件中的 requirepath 實在會對應一個数字自增 id,這個自增 id 實在就是我們一最先的時刻設置的 id,我們經由歷程將 path-id 應用鍵值對,對應起來,以後我們在文件中 require 就可以輕鬆的找到文件的代碼,詮釋這麼煩瑣的緣由是每每模塊之間的引用是錯中龐雜的,這恰巧是這個觀點難以詮釋的緣由。

末了,天生 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 的最簡樸完成
function loader(filename, code) {
  if (/index/.test(filename)) {
    console.log('this is loader ')
  }
  return code
}

// 末了我們導出我們的 bundler
module.exports = entry => {
  const graph = parseGraph(entry)
  const bundle = build(graph)
  return bundle
}

我們完成了 graph 的網絡,那末就到我們真正的代碼打包了,這個函數運用了大批的字符串處置懲罰,你們不要以為新鮮,為何代碼和字符串可以混起來寫,假如你跳出寫代碼的領域,看我們的代碼,現實上,代碼就是字符串,只不過他經由歷程特別的言語情勢組織起來罷了,關於腳本言語 JS 來講,字符串拼接成代碼,然後跑起來,這類操縱在前端異常的罕見,我以為,這類頭腦的轉換,是具有自動化、工程化的第一步。

我們將 graph 中一切的 asset 取出來,然後運用 node.js 製作模塊的要領來將一份代碼包起來,我之前做過一個《庖丁解牛:教你怎樣完成》node.js 模塊的文章,不懂的可以去看看,https://zhuanlan.zhihu.com/p/…

在這裏簡樸報告,我們將轉換好的源碼,放進一個 function(require,module,exports){} 函數中,這個函數的參數就是我們到處可用的 requiremodule,以及 exports,這就是為何我們可以到處運用這三個玩意的緣由,由於我們每一個文件的代碼終將被如許一個函數包裹起來,不過這段代碼中比較新鮮的是,我們將代碼封裝成了 1:[...],2:[...]的情勢,我們在末了導入模塊的時刻,會為這個字符串加上一個 {},變成 {1:[...],2:[...]},你沒看錯,這是一個對象,這個對象里用数字作為 key,一個二維元組作為值:

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

立時要見到曙光了,這一段代碼現實上才是模塊引入的中心邏輯,我們製作一個頂層的 require 函數,這個函數吸收一個 id 作為值,而且返回一個全新的 module 對象,我們倒入我們方才製作好的模塊,給他加上 {},使其成為 {1:[...],2:[...]} 如許一個完全的情勢。

然後塞入我們的馬上實行函數中(function(modules) {...})(),在 (function(modules) {...})() 中,我們先挪用 require(0),來由很簡樸,由於我們的主模塊永遠是排在第一位的,緊接着,在我們的 require 函數中,我們拿到外部傳進來的 modules,應用我們一向在說的全局数字 id 獵取我們的模塊,每一個模塊獵取出來的就是一個二維元組。

然後,我們要製作一個 子require,這麼做的緣由是我們在文件中運用 require 時,我們平常 require 的是地點,而頂層的 require 函數參數時 id
不要憂鬱,我們之前的 idMapping 在這裏就用上了,經由歷程用戶 require 進來的地點,在 idMapping 中找到 id

然後遞歸挪用 require(id),就可以完成模塊的自動倒入了,接下來製作一個 const newModule = {exports: {}};,運轉我們的函數 fn(childRequire, newModule, newModule.exports);,將應當丟進去的丟進去,末了 return newModule.exports 這個模塊的 exports 對象。

這裏的邏輯實在跟 node.js 差異不太大。

末了寫一點測試

測試的代碼,我已放在了堆棧里,想測試一下的同硯可以去堆棧中自行提取。

打滿詮釋的代碼也放在堆棧了,點擊地點

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

參考

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