8000 GitHub - Foveluy/roid: a simple bundler📦 build for fun
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Foveluy/roid

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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 ,因此,我们并ä¸è€ƒè™‘å¦‚ä½•åŽ»è§£æž css ã€md ã€txt 等等之类的格å¼ï¼Œæˆ‘们专心处ç†å¥½ js 文件的打包,因为对于其他文件而言,处ç†èµ·æ¥è¿‡ç¨‹ä¸å¤ªä¸€æ ·ï¼Œç”¨æ–‡ä»¶åŽç¼€å¾ˆå®¹æ˜“将他们区分进行ä¸åŒçš„处ç†ï¼Œåœ¨è¿™ä¸ªç‰ˆæœ¬ï¼Œæˆ‘们还是专注 js。

const rawCode = readFileSync(filename, 'utf-8') 函数注入一个 filename é¡¾åæ€ä¹‰ï¼Œå°±æ˜¯æ–‡ä»¶å,读å–其的文件文本内容,然åŽå¯¹å…¶è¿›è¡Œ AST 的解æžã€‚我们使用 babel çš„ transform æ–¹æ³•åŽ»è½¬æ¢æˆ‘们的原始代ç ï¼Œé€šè¿‡è½¬æ¢ä»¥åŽï¼Œæˆ‘们的代ç å˜æˆäº†æŠ½è±¡è¯­æ³•树( AST ),你å¯ä»¥é€šè¿‡ https://astexplorer.net/, 这个å¯è§†åŒ–的网站,看看 AST 生æˆçš„æ˜¯ä»€ä¹ˆã€‚

当我们解æžå®Œä»¥åŽï¼Œæˆ‘们就å¯ä»¥æå–当剿–‡ä»¶ä¸­çš„ dependencies,dependencies 翻译为ä¾èµ–,也就是我们文件中所有的 import xxxx from xxxx,我们将这些ä¾èµ–都放在 dependencies 的数组里é¢ï¼Œä¹‹åŽç»Ÿä¸€è¿›è¡Œå¯¼å‡ºã€‚

ç„¶åŽé€šè¿‡ traverse é历我们的代ç ã€‚traverse 函数是一个é历 AST 的方法,由 babel-traverse æä¾›ï¼Œä»–çš„éåŽ†æ¨¡å¼æ˜¯ç»å…¸çš„ visitor æ¨¡å¼ ï¼Œvisitor 模å¼å°±æ˜¯å®šä¹‰ä¸€ç³»åˆ—çš„ visitor ,当碰到 AST çš„ type === 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(ä¾èµ–图),用于æè¿°æˆ‘们这个项目的所有的ä¾èµ–关系,parseGraph 从 entry (入å£ï¼‰ 出å‘ï¼Œä¸€ç›´æ‰‹æœºå®Œæ‰€æœ‰çš„ä»¥æ¥æ–‡ä»¶ä¸ºæ­¢.

在这里我们使用 for of å¾ªçŽ¯è€Œä¸æ˜¯ forEach ï¼ŒåŽŸå› æ˜¯å› ä¸ºæˆ‘ä»¬åœ¨å¾ªçŽ¯ä¹‹ä¸­ä¼šä¸æ–­çš„å‘ graph 中,push 进东西,graph ä¼šä¸æ–­å¢žåŠ ï¼Œç”¨ for of 会一直æŒç»­è¿™ä¸ªå¾ªçŽ¯ç›´åˆ° graph ä¸ä¼šå†è¢«æŽ¨è¿›åŽ»ä¸œè¥¿ï¼Œè¿™å°±æ„味ç€ï¼Œæ‰€æœ‰çš„ä¾èµ–å·²ç»è§£æžå®Œæ¯•,graph 数组数é‡ä¸ä¼šç»§ç»­å¢žåŠ ï¼Œä½†æ˜¯ç”¨ forEach 是ä¸è¡Œçš„,åªä¼šé历一次。

在 for of 循环中,asset 代表解æžå¥½çš„æ¨¡å—ï¼Œé‡Œé¢æœ‰ filename , code , dependencies 等东西 asset.idMapping 是一个ä¸å¤ªå¥½ç†è§£çš„æ¦‚念,我们æ¯ä¸€ä¸ªæ–‡ä»¶éƒ½ä¼šè¿›è¡Œ import æ“作,import æ“ä½œåœ 8CCF ¨ä¹‹åŽä¼šè¢«è½¬æ¢æˆ require æ¯ä¸€ä¸ªæ–‡ä»¶ä¸­çš„ require çš„ path 其实会对应一个数字自增 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/34974579

在这里简å•讲述,我们将转æ¢å¥½çš„æºç ï¼Œæ”¾è¿›ä¸€ä¸ª function(require,module,exports){} å‡½æ•°ä¸­ï¼Œè¿™ä¸ªå‡½æ•°çš„å‚æ•°å°±æ˜¯æˆ‘们éšå¤„å¯ç”¨çš„ require,module,ä»¥åŠ 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. Fadingvision/blackLearning.github.io#23
  2. https://github.com/ronami/minipack

About

a simple bundler📦 build for fun

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published
0