談談minipack

不想成爲配置工程師的前端不是好架構,哈哈

我已經記不清這是第幾次刷webpack的文檔了,看了忘,忘了看,再看再忘…

這都不是重點。webpack官網文檔中給我們推薦了https://github.com/ronami/minipack,可以一看。挺有意思的。

我用我蹩腳的英文對部分註釋進行了翻譯

我嘗試總結一下思路吧:
從入口文件開始,使用babylon分析js代碼,找到import語句,以此來找到依賴文件。然後又對依賴文件如此操作,構建出整個的依賴圖(createGraph函數)。最後通過依賴圖生成目標文件(bundle函數)。

AST開啓了另一扇門,有時間要了解一下.

有用點贊哦:)



/**
 * Module bundlers compile small pieces of code into something larger and more
 * complex that can run in a web browser. These small pieces are just JavaScript
 * files, and dependencies between them are expressed by a module system
 * (https://webpack.js.org/concepts/modules).
 * 
 * 模塊打包器編譯小片段代碼到更到的更復雜的代碼中,它們可以運行在瀏覽器中。這些小片段只是
 * JavaScript文件,它們之間的依賴用模塊系統表示。
 *
 * Module bundlers have this concept of an entry file. Instead of adding a few
 * script tags in the browser and letting them run, we let the bundler know
 * which file is the main file of our application. This is the file that should
 * bootstrap our entire application.
 * 
 * 模塊打包器有入口文件的入口。我們讓打包器知道哪一個文件是我們程序的主文件,而不是在
 * 瀏覽器中添加script標籤,讓它們運行。這是啓動我們整個應用的文件。
 *
 * Our bundler will start from that entry file, and it will try to understand
 * which files it depends on. Then, it will try to understand which files its
 * dependencies depend on. It will keep doing that until it figures out about
 * every module in our application, and how they depend on one another.
 *
 * 我們的打包器會從入口文件開始,它會嘗試理解這個文件依賴的文件。然後,它會嘗試理解它
 * 的依賴依賴的文件。它一直做這件事直到指出了每一個模塊,和它們怎麼依賴的。
 * 
 * This understanding of a project is called the dependency graph.
 *
 * 項目的理解稱爲依賴圖。
 * 
 * In this example, we will create a dependency graph and use it to package
 * all of its modules in one bundle.
 *
 * 這個例子中,我們創建一個依賴圖,然後使用它把所有的模塊打包到一個文件中。
 * 
 * Let's begin :)
 * 讓我們開始:)
 * 
 * Please note: This is a very simplified example. Handling cases such as
 * circular dependencies, caching module exports, parsing each module just once
 * and others are skipped to make this example as simple as possible.
 * 
 * 注意:這是一個很簡單的例子。處理循環依賴、緩存模塊導出和解析每一個模塊一次,別的都跳過了,
 * 使得這個例子儘可能簡單。
 */

const fs = require('fs'); // 文件系統模塊
const path = require('path'); // 路徑模塊
const babylon = require('babylon'); // JavaScript解析器
const traverse = require('babel-traverse').default;
const {transformFromAst} = require('babel-core');

let ID = 0;

// We start by creating a function that will accept a path to a file, read
// its contents, and extract its dependencies.
// 我們通過創建一個接收文件路徑、讀取它的內容、提前它的依賴的函數開始。
function createAsset(filename) {
  // Read the content of the file as a string.
  // 讀取文件的內容爲一個字符串
  const content = fs.readFileSync(filename, 'utf-8');

  // Now we try to figure out which files this file depends on. We can do that
  // by looking at its content for import strings. However, this is a pretty
  // clunky approach, so instead, we will use a JavaScript parser.
  //
  // 現在我們嘗試指出這個文件依賴的文件。我們可以通過查看import字符串。然而這是一個
  // 本辦法,我們使用JavaScript解析器,也就是babylon.
  //
  // JavaScript parsers are tools that can read and understand JavaScript code.
  // They generate a more abstract model called an AST (abstract syntax tree).
  // JavaScript解析器是可以讀取並理解JavaScript代碼的工具。它們生成一個更抽象的模型
  // 稱爲AST 抽象語法樹。

  // I strongly suggest that you look at AST Explorer (https://astexplorer.net)
  // to see how an AST looks like.
  // 我強烈建議看一下AST Explorer瞭解AST像什麼。
  //
  // The AST contains a lot of information about our code. We can query it to
  // understand what our code is trying to do.
  // AST包含你的代碼的信息。我們能查詢它來理解我們的代碼要做什麼。
  const ast = babylon.parse(content, {
    sourceType: 'module',
  });

  // This array will hold the relative paths of modules this module depends on.
  // 這個數組用來存儲這個模塊依賴的模塊的相對路徑。
  const dependencies = [];

  // We traverse the AST to try and understand which modules this module depends
  // on. To do that, we check every import declaration in the AST.
  // 我們遍歷AST嘗試理解這個模塊依賴的模塊。爲此我們檢查每一個import聲明。
  traverse(ast, {
    // EcmaScript modules are fairly easy because they are static. This means
    // that you can't import a variable, or conditionally import another module.
    // Every time we see an import statement we can just count its value as a
    // dependency.
    // EcmaScript模塊相當簡單,因爲他們是靜態的。這意味着你不能導入變量,或者條件導入別
    // 的模塊。每次我們看到一個import語句,我們僅僅獲取它的值作爲依賴。
    ImportDeclaration: ({node}) => {
      // We push the value that we import into the dependencies array.
      // 我們把它的值添加到依賴數組中。
      dependencies.push(node.source.value);
    },
  });

  // We also assign a unique identifier to this module by incrementing a simple
  // counter.
  // 我們給這個模塊分配一個唯一的標識符,通過自增長一個簡單的計數器。
  const id = ID++;

  // We use EcmaScript modules and other JavaScript features that may not be
  // supported on all browsers. To make sure our bundle runs in all browsers we
  // will transpile it with Babel (see https://babeljs.io).
  // 我們使用EcmaScript模塊和別的JavaScript特性,這些在所有瀏覽器中不能被支持。爲了讓我們
  // 的bundle在所有的瀏覽器中運行,我們用Babel轉碼。
  //
  // The `presets` option is a set of rules that tell Babel how to transpile
  // our code. We use `babel-preset-env` to transpile our code to something
  // that most browsers can run.
  // presets選項是一系列規則,它告訴Babel怎麼轉碼我們的代碼。我們使用babel-preset-env來
  // 轉碼。
  const {code} = transformFromAst(ast, null, {
    presets: ['env'],
  });

  // Return all information about this module.
  // 返回這模塊的所有信息
  return {
    id,
    filename,
    dependencies,
    code,
  };
}

// Now that we can extract the dependencies of a single module, we are going to
// start by extracting the dependencies of the entry file.
//
// 既然我們能提取一個模塊的依賴,我們將要通過提取入口文件的依賴開始。
//
// Then, we are going to extract the dependencies of every one of its
// dependencies. We will keep that going until we figure out about every module
// in the application and how they depend on one another. This understanding of
// a project is called the dependency graph.
// 然後,我們將要提取它的每個依賴的依賴。我們將要持續這個,知道指出應用中的每個模塊已經它們怎麼相互依賴。
// 項目的理解稱爲依賴圖。
function createGraph(entry) {
  // Start by parsing the entry file.
  // 通過解析入口文件開始
  const mainAsset = createAsset(entry);

  // We're going to use a queue to parse the dependencies of every asset. To do
  // that we are defining an array with just the entry asset.
  // 我們將要使用一個隊列來解析每個資源的依賴。我們定義一個包含入口資源的數組來實現這個。
  const queue = [mainAsset];

  // We use a `for ... of` loop to iterate over the queue. Initially the queue
  // only has one asset but as we iterate it we will push additional new assets
  // into the queue. This loop will terminate when the queue is empty.
  // 我們使用for...of循環來迭代隊列。剛開始這個隊列只有一個資源,但是我們迭代它,我們
  // 會把新的資源加入到隊列中。當隊列空了,這個循環就結束了
  for (const asset of queue) {
    // Every one of our assets has a list of relative paths to the modules it
    // depends on. We are going to iterate over them, parse them with our
    // `createAsset()` function, and track the dependencies this module has in
    // this object.
    // 每一個資源有一個列表,保存了它依賴的相對路徑。我們將會迭代它們,用我們的crateAsset來
    // 解析它們。
    asset.mapping = {};

    // This is the directory this module is in.
    // 這個模塊所在的目錄
    const dirname = path.dirname(asset.filename);

    // We iterate over the list of relative paths to its dependencies.
    // 迭代依賴
    asset.dependencies.forEach(relativePath => {
      // Our `createAsset()` function expects an absolute filename. The
      // dependencies array is an array of relative paths. These paths are
      // relative to the file that imported them. We can turn the relative path
      // into an absolute one by joining it with the path to the directory of
      // the parent asset.
      // 我們的creatAsset函數期望一個絕對文件名。依賴數組是一個相對路徑的數組。這些路徑
      // 相對導入它們的文件。我們能夠把相對路徑轉換成絕對路徑,通過把它跟父資源的目錄拼接。
      const absolutePath = path.join(dirname, relativePath);

      // 在已經解析的模塊裏面尋找
      const c = queue.find(x => x.filename === absolutePath);
      if (c) {
        asset.mapping[relativePath] = c.id;
        return;
      }

      // Parse the asset, read its content, and extract its dependencies.
      const child = createAsset(absolutePath);

      // It's essential for us to know that `asset` depends on `child`. We
      // express that relationship by adding a new property to the `mapping`
      // object with the id of the child.
      asset.mapping[relativePath] = child.id;

      // Finally, we push the child asset into the queue so its dependencies
      // will also be iterated over and parsed.
      queue.push(child);
    });
  }

  // At this point the queue is just an array with every module in the target
  // application: This is how we represent our graph.
  return queue;
}

// Next, we define a function that will use our graph and return a bundle that
// 接下來,我們定義一個函數,它使用我們的依賴圖來返回一個可以在瀏覽器中運行的bundle
// we can run in the browser.
//
// Our bundle will have just one self-invoking function:
// 我們的bundle將會只有一個自調用的函數
// (function() {})()
//
// That function will receive just one parameter: An object with information
// about every module in our graph.
// 那個函數將會接收一個參數,它是一個對象,帶有依賴圖中關於每個模塊的信息。
function bundle(graph) {
  let modules = '';

  // Before we get to the body of that function, we'll construct the object that 
  // we'll pass to it as a parameter. Please note that this string that we're
  // building gets wrapped by two curly braces ({}) so for every module, we add
  // a string of this format: `key: value,`.
  // 在我們說的函數體之前,我們先構建傳給它的對象。請注意我們構建的字符串被花括號包圍,所以對於每一個模塊,我們增加一個key: value格式的字符串
  graph.forEach(mod => {
    // Every module in the graph has an entry in this object. We use the 
   // module's id as the key and an array for the value (we have 2 values for
    // every module).
    // 依賴圖中的每個模塊在對象中有一個記錄。我們使用模塊id作爲key,數組作爲值。

    // The first value is the code of each module wrapped with a function. This
    // is because modules should be scoped: Defining a variable in one module
    // shouldn't affect others or the global scope.
    // 第一個值是用函數包裹的每個模塊的內容,這是因爲每個模塊必須有單獨的作用域:在模塊中定義的變量不應該影響別的模塊或全局作用域。
    // Our modules, after we transpiled them, use the CommonJS module system:
    // They expect a `require`, a `module` and an `exports` objects to be
    // available. Those are not normally available in the browser so we'll
    // implement them and inject them into our function wrappers.
    // 在我們轉碼後,我們的模塊使用commonjs模塊系統:他們期待一個require、module和exports。這些在瀏覽器中是不可用的,因此我們將會實現並注入它們到我們的包裹函數中。
    // For the second value, we stringify the mapping between a module and its
    // dependencies. This is an object that looks like this:
    // { './relative/path': 1 }.
    // 第二個值,我們把模塊跟它的依賴的映射字符串化。
    // This is because the transpiled code of our modules has calls to
    // `require()` with relative paths. When this function is called, we should
    // be able to know which module in the graph corresponds to that relative
    // path for this module.
    // 這是因爲轉過碼的模塊使用相對路徑調用require函數。當這個函數被調用時,我麼應該能夠知道它是依賴圖中的哪個模塊。
    modules += `${mod.id}: [
      function (require, module, exports) {
        ${mod.code}
      },
      ${JSON.stringify(mod.mapping)},
    ],`;
  });

  // Finally, we implement the body of the self-invoking function.
  // 最後,我們實現自調用函數體。
  // We start by creating a `require()` function: It accepts a module id and
  // looks for it in the `modules` object we constructed previously. We
  // destructure over the two-value array to get our function wrapper and the
  // mapping object.
  // 我們通過創建require函數來開始:它接收一個模塊id,在我們前面構建的modules對象中尋找這個模塊。我們解構兩個元素的數組來獲取我們的包裝函數和影射對象。
  // The code of our modules has calls to `require()` with relative file paths
  // instead of module ids. Our require function expects module ids. Also, two
  // modules might `require()` the same relative path but mean two different
  // modules.
  // 我們模塊的代碼使用相對路徑而不是模塊id來調用require。我們的require函數希望是模塊id。並且,兩個模塊可能require兩個相同的相對路徑,但是卻是不同的模塊。
  // To handle that, when a module is required we create a new, dedicated
  // `require` function for it to use. It will be specific to that module and
  // will know to turn its relative paths into ids by using the module's
  // mapping object. The mapping object is exactly that, a mapping between
  // relative paths and module ids for that specific module.
  // 爲了處理這種情況,當一個模塊被require時,我們創建一個新的專用的require函數給它用。它將是特定給那個模塊的,並且知道怎麼通過模塊的映射對象把相對路徑轉換成id。映射對象是針對具體模塊的,相對路徑和模塊id的映射。
  // Lastly, with CommonJs, when a module is required, it can expose values by
  // mutating its `exports` object. The `exports` object, after it has been
  // changed by the module's code, is returned from the `require()` function.
  // 最後,在commonjs中,當一個模塊被require時,它會通過exports對象導出值。exports對象再被修改之後,從require函數調用返回。
  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}})
  `;

  // We simply return the result, hurray! :)
  return result;
}

const graph = createGraph('./example/entry.js');
const result = bundle(graph);

console.log(result);

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章