模塊打包器的實現(xiàn)(一)

什么是打包器

一個完整的 JavaScript 項目(比如各種前端SPA)由各種各樣的資源模塊(module)組成换吧,包括 JavaScript 代碼寞焙,CSS 樣式以及圖片等各種文件。打包器(module bundler)可以分析入口文件(entry)引用了哪些模塊,找到對應的文件债蜜,將其合并到一起。這樣執(zhí)行輸出文件(output)的時候究反,一個完整的項目會呈現(xiàn)出來寻定。

單頁面應用包含大量 JavaScript 代碼,為了合理地管理代碼精耐,開發(fā)時會將代碼拆分到不同文件里面特姐。在各個模塊的代碼編寫完成之后,bundler 可以幫我們把各個分散的 JS 文件合并起來黍氮,輸出一個完整的 JS 文件唐含。

對于樣式文件和圖片等資源,我們也可以指定如何處理它們沫浆。一般的處理方式是直接插入到 HTML 或者 JS 文件中捷枯,或者通過指定文件網(wǎng)絡地址(public path),在需要該文件的時候瀏覽器會通過網(wǎng)絡請求獲取到這些資源专执。

功能完備的打包器可以把各種資源模塊聚集到一起淮捆,生成完整的 web app。但本文作為開篇只討論如何實現(xiàn)一個 JavaSript bundler本股。

過程分析

一個最簡單的 JS bundler 可以幫助我們:

  • 找到 entry JavaScript 文件引用到的所有其他 JS 文件攀痊,并將其合并到目標 JS 文件(output)中。
  • 保證各個模塊的 JavaScript 代碼都在自己的作用域中執(zhí)行拄显,避免命名沖突苟径。

為了保證每個模塊的 JS 代碼都在自己的作用域中執(zhí)行,可以參考 Node 執(zhí)行 JS 代碼的方式躬审〖郑可以概括為 5 步:

  • resolve:通過 require 中的 string 定位到文件的真實地址。
  • load:加載這個文件承边。
  • wrap:將引入的代碼包含在一個函數(shù)中遭殉,保證定義的變量只作用在本文件中。
  • execute:執(zhí)行代碼博助。
  • cache:緩存執(zhí)行結果险污。

而打包的流程可以概括為:

  • 找到起始文件的依賴文件,將其加載并描述為一個資源模塊(asset)富岳,其包含的信息包括:
    • id:唯一 id
    • filename:絕對文件路徑
    • code:模塊代碼蛔糯,將其包含在一個函數(shù)里面拯腮。并且需要把 ESM 的 import 和 export 改成 require 和 exports,這樣可以執(zhí)行函數(shù)參數(shù)里面的 require 和 exports渤闷,函數(shù)參數(shù)的 require 可以幫我們通過相對路徑找到實際文件疾瓮。
    • dependencies:引入的模塊。
    • mapping:記錄以來模塊的相對路徑和其模塊 id 的對應關系飒箭。
  • 當依賴的文件有其他依賴的時候狼电,繼續(xù)加載依賴文件。最終生成一個依賴圖(dependency graph)弦蹂,包含所有模塊之間的依賴關系肩碟。
  • 拼湊一個完整 string,包含所有模塊信息凸椿,并且執(zhí)行起始文件削祈。輸出這個 string。

代碼實現(xiàn)

轉換 JS 文件為資源模塊(asset):

const path = require('path');
const fs = require('fs');

const parser = require('babel-parser');
const { transformFromAst } = require('@babel/core');
const traverse = require('@babel/traverse').default;

let ID = 0;

function createAsset(filename) {
  const content = fs.readFileSync(filename, 'utf-8');
  const ast = parser.parse(content, {
    sourceType: 'module',
  });
  
  const dependencies = [];
  traverse(ast, {
    importDelcaration: ({ node }) => {
      dependencies.push(node.source.value);
    }
  });
  
  const { code } = tranformFromAst(ast, null, {
    presets: ['@babel/preset-env'],
  });
  
  return {
    id: ID++,
    filename,
    dependencies,
    code
  };
}

生成依賴圖:

function createGraph(entry) {
  const mainAsset = createAsset(entry);
  
  const queue = [mainAsset];
  
  queue.forEach(asset => {
    asset.mapping = {};
    const dirname = path.dirname(asset.filename);
    
    asset.dependencies.forEach((relativePath) => {
      const filename = path.join(dirname, relativePath);
      const child = createAsset(filename);
     
      asset.mapping[relativePath] = child.id;
      queue.push(child);
    });
  });
  
  return queue;
}

合并 string:

function createBundle(entry) {
  const graph = createGraph(entry);
  
  let modules = '{';
  
  graph.forEach((asset) => {
    modules +=
      `${asset.id}: [function (requre, module, exports) { ${asset.code} }, ${JSON.stringify(asset.mapping)}],`;
  });
  
  modules += '}';
  
  const result = `function(modules) {
    function require(id) {
      const [fn, mapping] = modules[id];
      
      function localRequire(relativePath) {
        require(mapping[relativePath]));
      }
      
      const module = { exports: {} };
      
      fn(localRequire, module, exports);
      
      return moduele.exports;
    }
    
    require(0);
  }(${modules)`;
}

備注項目依賴

"dependencies": {
    "@babel/core": "7.9.6",
    "@babel/parser": "7.9.6",
    "@babel/preset-env": "7.9.6",
    "@babel/traverse": "7.9.6"
  }

附錄

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末脑漫,一起剝皮案震驚了整個濱河市髓抑,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌优幸,老刑警劉巖吨拍,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異网杆,居然都是意外死亡羹饰,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門碳却,熙熙樓的掌柜王于貴愁眉苦臉地迎上來队秩,“玉大人,你說我怎么就攤上這事昼浦♀勺剩” “怎么了?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵座柱,是天一觀的道長迷帜。 經(jīng)常有香客問我,道長色洞,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任冠胯,我火速辦了婚禮火诸,結果婚禮上,老公的妹妹穿的比我還像新娘荠察。我一直安慰自己置蜀,他們只是感情好奈搜,可當我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著盯荤,像睡著了一般馋吗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上秋秤,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天宏粤,我揣著相機與錄音,去河邊找鬼灼卢。 笑死绍哎,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的鞋真。 我是一名探鬼主播崇堰,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼涩咖!你這毒婦竟也來了海诲?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤檩互,失蹤者是張志新(化名)和其女友劉穎特幔,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體盾似,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡敬辣,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了零院。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片溉跃。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖告抄,靈堂內(nèi)的尸體忽然破棺而出撰茎,到底是詐尸還是另有隱情,我是刑警寧澤打洼,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布龄糊,位于F島的核電站,受9級特大地震影響募疮,放射性物質發(fā)生泄漏炫惩。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一阿浓、第九天 我趴在偏房一處隱蔽的房頂上張望他嚷。 院中可真熱鬧,春花似錦、人聲如沸筋蓖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽粘咖。三九已至蚣抗,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間瓮下,已是汗流浹背翰铡。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留唱捣,地道東北人两蟀。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像震缭,于是被迫代替她去往敵國和親赂毯。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,512評論 2 359