Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

webpack 源码系列之 bundler 实现 #24

Open
noneven opened this issue Apr 3, 2018 · 1 comment
Open

webpack 源码系列之 bundler 实现 #24

noneven opened this issue Apr 3, 2018 · 1 comment

Comments

@noneven
Copy link
Owner

noneven commented Apr 3, 2018

webpack bundler 实现

前言

Q: 为什么我们要做这件事?
A: 太多小伙伴对 webpack 的认识都在 webpack.config.js 上,对 webpack 的使用也都是黑盒的,对其打包、loader、plugin等实现原理知之甚少

Q: webpack 现在如此庞大,应该如何着手?
A: 我们可以从 webpack 的第一个提交版本着手研究,它主要实现了 bundler和 loader,代码量很小,并且可读性很高 webpack 的第一个 commit

bundler 主要功能

  • 将多个符合 CommonJS 规范的模块打包成一个 JS 文件,使其可以运行在浏览器中。
  • 显然,浏览器没法直接执行 CommonJS 规范的模块,怎么办呢?我们可以将 module, module.exports, require 等函数在运行模块前定义好,各个模块分别调用执行逻辑
  • 一个打包后的 bundle.js 如下
/******/(function(modules) {
/******/	var installedModules = {};
/******/	function require(moduleId) {
/******/		if(installedModules[moduleId])
/******/			return installedModules[moduleId].exports;
/******/		var module = installedModules[moduleId] = {
/******/			exports: {}
/******/		};
/******/		modules[moduleId](module, module.exports, require);
/******/		return module.exports;
/******/	}
/******/	return require(0);
/******/})
/******/({
/******/0: function(module, exports, require) {

var a = require(/* ./a.js */1);
var b = require(/* ./b.js */2);
var luna = require(/* @alipay/luna-core */3);
a();
b();


/******/},
/******/
/******/1: function(module, exports, require) {

// module a

module.exports = function () {
    console.log('a')
};

/******/},
/******/
/******/2: function(module, exports, require) {

// module b

module.exports = function () {
    console.log('b')
};

/******/},
/******/
/******/3: function(module, exports, require) {

module.exports = {
    // ...
};

/******/},
/******/
/******/})

bundler 实现思路

分析 bundle.js,我们能够发现:

  • 1、不管有多少个模块,头部那一块都是一样的,它实现了 commonJS 的 module、module.exports、require 等函数,所以可以写成一个模板,也就是 webpack 里面的 templateSingle.js

  • 2、需要分析出各个模块间的依赖关系。也就是说,bundler 需要知道 example 依赖于 a、b 和 模块 luna。

  • 3、luna 模块位于 node_modules 文件夹当中,但是我们调用的时候却可以直接 require('@alipay/luna-core'),所以 bundler 肯定是存在某种自动查找的功能。

  • 4、在生成的 bundle.js 中,每个模块的唯一标识是模块的 ID,所以在拼接bundle.js 的时候,需要将每个模块的名字替换成模块的 ID

    // 转换前
    var a = require('./a.js');
    var b = require('./b.js');
    var luna = require('@alipay/luna-core');
    
    // 转换后
    var a = require(/* ./a.js */1);
    var b = require(/* ./b.js */2);
    var luna = require(/* @alipay/luna-core */3);
    

下面我们逐一分析一下上面的 4 各部分

1、头部模板

  • templateSingle

Q: 为什么叫 templateSingle?
A: 因为 webpack 在打包其他比如代码切割等时,头部模板会不一样,这儿为了区分,就叫 templateSingle 了,算是将所有的模块都打包到一个 JS 文件里面

// templateSingle
/******/(function(modules) {
/******/	var installedModules = {};
/******/	function require(moduleId) {
/******/		if(installedModules[moduleId])
/******/			return installedModules[moduleId].exports;
/******/		var module = installedModules[moduleId] = {
/******/			exports: {}
/******/		};
/******/		modules[moduleId](module, module.exports, require);
/******/		return module.exports;
/******/	}
/******/	return require(0);
/******/})

2、分析模块依赖

CommonJS 不同于 AMD,不会在模板定义时将所有依赖的声明。CommonJS 最显著的特征就是用到的时候再 require,所以我们得在整个文件的范围内查找依赖了哪些模块。

Q: 怎么在整个文件里面查找出依赖?
A: 正则匹配?

Q: 如果 require 是写在注释里面了怎么办?性能如何?
A: 正则行不通,我们可以采用 babel 等语言编译器的原理,将 JS 代码解析转换成 抽象语法树(AST),再对 AST 进行遍历,找到所有的 require 依赖。

Q: 如果模块 a 依赖 b,b 又依赖 c,然后 c 又依赖 d 这又怎么办?
A: 当解析 a 模块时,如果模块 a 中又 require 了其他模块,那么将继续解析依赖的模块。也就是说,总体上遵循深度优先遍历

  • 解析依赖具体实现:(详见 parse.js
/**
 * @file 解析模块依赖
 * @author chunk.cj
 */

const esprima = require('esprima');

/**
 * 解析模块包含的依赖
 * @param {string} source 模块内容字符串
 * @returns {object} module 解析模块得出的依赖关系
 */
module.exports = source => {
  const ast = esprima.parse(source, {
    range: true
  });
  const module = {};
  walkStatements(module, ast.body);
  module.source = source;
  return module;
};

/**
 * 遍历块中的语句
 * @param {object} module 模块对象
 * @param {object} statements AST语法树
 */
function walkStatements(module, statements) {
  statements.forEach(statement => walkStatement(module, statement));
}

/**
 * 分析每一条语句
 * @param {object} module 模块对象
 * @param {object} statement AST语法树
 */
function walkStatement(module, statement) {
  switch (statement.type) {
  case 'VariableDeclaration':
    if (statement.declarations) {
      walkVariableDeclarators(module, statement.declarations);
    }
    break;
  }
}

/**
 * 处理定义变量的语句
 * @param {object} module 模块对象
 * @param {object} declarators
 */
function walkVariableDeclarators(module, declarators) {
  declarators.forEach(declarator => {
    switch (declarator.type) {
    case 'VariableDeclarator':
      if (declarator.init) {
        walkExpression(module, declarator.init);
      }
      break;
    }
  });
}

/**
 * 处理表达式
 * @param {object} module  模块对象
 * @param {object} expression 表达式
 */
function walkExpression(module, expression) {
  switch (expression.type) {
  case 'CallExpression':
    // 处理普通的require
    if (expression.callee && expression.callee.name === 'require' && expression.callee.type === 'Identifier' && expression.arguments && expression.arguments.length === 1) {
      // TODO 此处还需处理require的计算参数
      module.requires = module.requires || [];
      const param = Array.from(expression.arguments)[0];
      module.requires.push({
        name: param.value,
        nameRange: param.range
      })
    }
    break;
  }
}

3、深度优先遍历构建依赖树:(详见 buildDeep.js

const fs = require('fs');
const path = require('path');
const parse = require('./parse');
const resolve = require('./resolve');

module.exports = async(mainModule, options) => {

  let depTree = {
    // 递增模块 id
    nextModuleId: 0,
    // 用于存储各个模块对象
    modules: {},
    // 用于映射模块名到模块 id 之间的关系
    mapModuleNameToId: {},
  };

  depTree = await parseModule(depTree, mainModule, options.context, options);
  return depTree;
};

const parseModule = async(depTree, moduleName, context, options) => {
  // 查找模块
  const absoluteFileName = resolve(moduleName, context, options.resolve);
  // 用模块的绝对路径作为模块的键值,保证唯一性
  module = depTree.modules[absoluteFileName] = {
    id: depTree.nextModuleId++,
    filename: absoluteFileName,
    name: moduleName
  };

  if (!absoluteFileName) {
    throw `找不到文件${absoluteFileName}`;
  }
  const source = fs.readFileSync(absoluteFileName).toString();
  const parsedModule = parse(source);

  module.requires = parsedModule.requires || [];
  module.source = parsedModule.source;

  // 写入映射关系
  depTree.mapModuleNameToId[moduleName] = depTree.nextModuleId - 1;

  // 如果此模块有依赖的模块,采取深度遍历的原则,遍历解析其依赖的模块
  const requireModules = parsedModule.requires;
  if (requireModules && requireModules.length > 0) {
    for (let require of requireModules) {
      depTree = await parseModule(depTree, require.name, path.dirname(absoluteFileName), options);
    }
  }
  return depTree;
}

4、模块寻址:(详见 resolve.js

简单的寻址方法

  • 如果给出的是绝对路径/相对路径,只查找一次。找到?返回绝对路径。找不到?返回 false。
  • 如果给出的是模块的名字,先在入口 js(example.js)文件所在目录下寻找同名JS文件(可省略扩展名)。找到?返回绝对路径。找不到?走第3步。
  • 在入口js(example.js)同级的 node_modules 文件夹(如果存在的话)查找。找到?返回绝对路径。找不到?返回 false。

这儿可以再考虑实现逐层往上查找 node_modules,可以参考 nodejs 默认的模块查找算法

/**
 * 查找模块所在绝对路径
 * @author chunk.cj
 */

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

// 判断给出的文件是否存在
const isFile = path => {
  try {
    const stats = fs.statSync(path);
    return stats && stats.isFile()
  } catch(e) {
    return false;
  }
};
const isDir = path => {
  try {
    const stats = fs.statSync(path);
    return stats && stats.isDirectory()
  } catch(e) {
    return false;
  }
};

/**
 * 根据模块的标志查找到模块的绝对路径
 * @param {string} moduleIdentifier 模块的标志,可能是模块名/相对路径/绝对路径
 * @param {string} context 上下文,入口 js 所在目录
 * @returns {string} 返回模块绝对路径
 */
module.exports = (moduleIdentifier, context, options) => {
  // 模块是绝对路径,只查找一次
  if (path.isAbsolute(moduleIdentifier)) {
    if (!path.extname(moduleIdentifier)) {
      moduleIdentifier += '.js';
    }
    if (isFile(moduleIdentifier)) {
      return moduleIdentifier;
    };
  } else if (moduleIdentifier.startsWith('./') || moduleIdentifier.startsWith('../')) {
    if (!path.extname(moduleIdentifier)) {
      moduleIdentifier += '.js';
    }
    moduleIdentifier = path.resolve(context, moduleIdentifier);
    if (isFile(moduleIdentifier)) {
      return moduleIdentifier;
    };
  } else {
    // 如果上述的方式都找不到,那么尝试在当前目录的 node_modules 里面找
    
    // 1、直接是node_modules文件夹下的文件
    if (isFile(path.resolve(context, './node_modules', moduleIdentifier))) {
      return moduleIdentifier;
    };
    if (isFile(path.resolve(context, './node_modules', `${moduleIdentifier}.js`))) {
      return `${moduleIdentifier}.js`;
    }

    // 2、node_modules 文件夹下的文件夹
    if (isDir(path.resolve(context, './node_modules', moduleIdentifier))) {
      var pkg = fs.readFileSync(path.resolve(context, './node_modules', moduleIdentifier, 'package.json'));
      var pkgJSON = JSON.parse(pkg);
      var main = path.resolve(context, './node_modules', moduleIdentifier, pkgJSON.main);
      if (isFile(main)) {
        return main;
      }
    } else {
      // 逐层往根目录查找
      const dirList = context.split('/');
      dirList.shift();
      while (dirList.length > 0) {
        dirList.pop();
        const dir = `/${dirList.join('/')}`;
        const moduleDir = path.resolve(dir, './node_modules', moduleIdentifier);

        if (isFile(moduleDir)) {
          return moduleDir;
        };
        if (isFile(`${moduleDir}.js`)) {
          return `${moduleDir}.js`;
        }
        if (isDir(moduleDir)) {
          // 解析 package.json 的 main 字段获取入口
          const pkg = fs.readFileSync(path.resolve(moduleDir, 'package.json'), 'utf-8');
          const pkgJSON = JSON.parse(pkg);
          const main = path.resolve(moduleDir, pkgJSON.main);
          if (isFile(main)) {
            return main;
          }
        }
      }
    }

    return moduleIdentifier;
  }
};

拼接 bundle:(详见 webpack.js

生成的 deepTree 如下:

{
  "nextModuleId": 5,
  "modules": {
    "/Users/chunk/Alipay/github/webpack-bundler/example/bundle/entry.js": {
      "id": 0,
      "filename": "/Users/chunk/Alipay/github/webpack-bundler/example/bundle/entry.js",
      "name": "/Users/chunk/Alipay/github/webpack-bundler/example/bundle/entry.js",
      "requires": [
        {
          "name": "./a.js",
          "nameRange": [
            16,
            24
          ]
        },
        {
          "name": "./b.js",
          "nameRange": [
            43,
            51
          ]
        }
      ],
      "source": "var a = require('./a.js');\nvar b = require('./b.js');\n\na();\nb();"
    },
    "/Users/chunk/Alipay/github/webpack-bundler/example/bundle/a.js": {
      "id": 1,
      "filename": "/Users/chunk/Alipay/github/webpack-bundler/example/bundle/a.js",
      "name": "./a.js",
      "requires": [
        {
          "name": "./c.js",
          "nameRange": [
            16,
            24
          ]
        }
      ],
      "source": "var c = require('./c.js');\n\nmodule.exports = function() {\n  console.log('a');\n};"
    },
    "/Users/chunk/Alipay/github/webpack-bundler/example/bundle/c.js": {
      "id": 4,
      "filename": "/Users/chunk/Alipay/github/webpack-bundler/example/bundle/c.js",
      "name": "./c.js",
      "requires": [],
      "source": "module.exports = function() {\n  console.log('c');\n};"
    },
    "/Users/chunk/Alipay/github/webpack-bundler/example/bundle/b.js": {
      "id": 3,
      "filename": "/Users/chunk/Alipay/github/webpack-bundler/example/bundle/b.js",
      "name": "./b.js",
      "requires": [
        {
          "name": "./c.js",
          "nameRange": [
            16,
            24
          ]
        }
      ],
      "source": "var c = require('./c.js');\n\nmodule.exports = function() {\n  console.log('b');\n  c();\n};"
    }
  },
  "mapModuleNameToId": {
    "/Users/chunk/Alipay/github/webpack-bundler/example/bundle/entry.js": 0,
    "./a.js": 1,
    "./c.js": 4,
    "./b.js": 3
  }
}

循环遍历 modules,拼接 module 模块的 source,拼接结构如下:

/******/(function(modules) {
/******/  var installedModules = {};
/******/  function require(moduleId) {
/******/    if(installedModules[moduleId])
/******/      return installedModules[moduleId].exports;
/******/    var module = installedModules[moduleId] = {
/******/      exports: {}
/******/    };
/******/    modules[moduleId](module, module.exports, require);
/******/    return module.exports;
/******/  }
/******/  return require(0);
/******/})

// 上面是 templateSingle

({

// 入口
0: function(module, exports, require) {
  // 模块 source
},

// 模块
1: function(module, exports, require) {
  // 模块 source
},

// ...

// 结尾
});

具体实现如下:

const buffer = [];
const modules = deepTree.modules;
for(let moduleName in modules) {
  const module = modules[moduleName];
    
  buffer.push("/******/");
  buffer.push(module.id);
  buffer.push(": function(module, exports, require) {\n\n");

  buffer.push(writeSource(module, deepTree));
  buffer.push("\n\n/******/},\n/******/\n");
}

return buffer.join("");

我们发现模块 source 里面的模块名还需要替换成模块 id。具体实现如下:

module.exports = function(module, deepTree) {
  const source = module.source;
  if (!module.requires || !module.requires.length) {
    return source.split('\n').map(line => `  ${line}\n`).join('');
  }

  const replaces = [];
  module.requires.forEach(requireItem => {
    if(requireItem.nameRange && requireItem.name) {
      const prefix = `/* ${requireItem.name} */`;
      replaces.push({
        from: requireItem.nameRange[0],
        to: requireItem.nameRange[1],
        value: prefix + deepTree.mapModuleNameToId[requireItem.name]
      });
    }
  });

  const result = [source];
  //  模块替换算法: https://github.com/coderwin/__/issues/20
  replaces.sort((a, b) => b.from - a.from).forEach(replace => {
    const remSource = result.shift();
    result.unshift(
      remSource.substr(0, replace.from),
      replace.value,
      remSource.substr(replace.to)
    );
  });

  // 给每行加上两个空格
  return result.join('').split('\n').map(line => `  ${line}\n`).join('');
};
@noneven
Copy link
Owner Author

noneven commented Apr 3, 2018

  • 1、完善模块寻址
  • 2、完善依赖解析
    • 暂时只能解析 var a = require('./a'); 这种变量定义形式,怎么优化了可以解析 require('./a'),或者 import a from './a' 这些形式?
    • 怎么解析出 require('./a')() 这样的自执行模块?
  • 3、代码压缩改进?
  • 4、还有下面三点...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant