本篇笔记将记录如何模仿编写一个Webpack工具。

  1. bundler
  2. ├─bundler.js
  3. ├─package-lock.json
  4. ├─src
  5. | ├─index.js
  6. | ├─message.js
  7. | word.js

模块分析

写上我们的业务逻辑:

  1. import message from './message.js'
  2. console.log(message);
  1. import { word } from './word.js'
  2. const message = `say ${word}`;
  3. export default message;
  1. export const word = "hello";

上面三个文件依次暴露出去一个变量随后被引入在其他js文件中,我们如何才能看到index.js文件的打印效果呢?

fs 模块

引入node自带的fs模块用来帮我们读取index.js文件的内容。

  1. const fs = require("fs");
  2. const moduleAnalyser = (filename) => {
  3. // 读取文件
  4. const content = fs.readFileSync(filename, "utf-8");
  5. }
  6. moduleAnalyser("./src/index.js")

当我们运行node bundler.js的时候就去执行bundler文件里面的代码,这样index文件的源代码就被打印了出来。
image.png

@babel/parser

从源代码中我们可以清晰的看到index文件引入了其他文件的模块,那么如何继续去分析其他模块文件的代码呢?

我们可以借助babel@babel/parser来帮我们完成这个操作。

  1. $ npm install @babel/parser -S
  1. const fs = require("fs");
  2. const parser = require("@babel/parser");
  3. const moduleAnalyser = (filename) => {
  4. // 读取文件
  5. const content = fs.readFileSync(filename, "utf-8");
  6. // 提取文件依赖(代码分析)
  7. let parseContent = parser.parse(content, {
  8. sourceType: "module"
  9. })
  10. console.log(parseContent)
  11. }
  12. moduleAnalyser("./src/index.js")

然后运行node bundler.js去支持bundler文件的时候就看到一个抽象的AST树,里面分析了代码文件之间的关系。

  1. xiechen@xiechendeMacBook-Pro bundler % node bundler.js
  2. Node {
  3. type: 'File',
  4. start: 0,
  5. end: 57,
  6. loc: SourceLocation {
  7. start: Position { line: 1, column: 0 },
  8. end: Position { line: 3, column: 21 },
  9. filename: undefined,
  10. identifierName: undefined
  11. },
  12. range: undefined,
  13. leadingComments: undefined,
  14. trailingComments: undefined,
  15. innerComments: undefined,
  16. extra: undefined,
  17. errors: [],
  18. program: Node {
  19. type: 'Program',
  20. start: 0,
  21. end: 57,
  22. loc: SourceLocation {
  23. start: [Position],
  24. end: [Position],
  25. filename: undefined,
  26. identifierName: undefined
  27. },
  28. range: undefined,
  29. leadingComments: undefined,
  30. trailingComments: undefined,
  31. innerComments: undefined,
  32. extra: undefined,
  33. sourceType: 'module',
  34. interpreter: null,
  35. body: [ [Node], [Node] ],
  36. directives: []
  37. },
  38. comments: []
  39. }

打印parseContent.program.body就能看到我们当前程序使用的模块的依赖关系。

  1. xiechen@xiechendeMacBook-Pro bundler % node bundler.js
  2. [
  3. Node {
  4. type: 'ImportDeclaration',
  5. start: 0,
  6. end: 34,
  7. loc: SourceLocation {
  8. start: [Position],
  9. end: [Position],
  10. filename: undefined,
  11. identifierName: undefined
  12. },
  13. range: undefined,
  14. leadingComments: undefined,
  15. trailingComments: undefined,
  16. innerComments: undefined,
  17. extra: undefined,
  18. specifiers: [ [Node] ],
  19. source: Node {
  20. type: 'StringLiteral',
  21. start: 20,
  22. end: 34,
  23. loc: [SourceLocation],
  24. range: undefined,
  25. leadingComments: undefined,
  26. trailingComments: undefined,
  27. innerComments: undefined,
  28. extra: [Object],
  29. value: './message.js'
  30. }
  31. },
  32. Node {
  33. type: 'ExpressionStatement',
  34. start: 36,
  35. end: 57,
  36. loc: SourceLocation {
  37. start: [Position],
  38. end: [Position],
  39. filename: undefined,
  40. identifierName: undefined
  41. },
  42. range: undefined,
  43. leadingComments: undefined,
  44. trailingComments: undefined,
  45. innerComments: undefined,
  46. extra: undefined,
  47. expression: Node {
  48. type: 'CallExpression',
  49. start: 36,
  50. end: 56,
  51. loc: [SourceLocation],
  52. range: undefined,
  53. leadingComments: undefined,
  54. trailingComments: undefined,
  55. innerComments: undefined,
  56. extra: undefined,
  57. callee: [Node],
  58. arguments: [Array]
  59. }
  60. }
  61. ]

@babel/traverse

现在得到了所有的import模块依赖关系,那么如何解析出来呢?
安装@babel/traverse

  1. $ npm install @babel/traverse -S

引入traversepath模块,执行traverse方法:

  1. const fs = require("fs");
  2. // 引入 path 模块
  3. const path = require("path");
  4. const parser = require("@babel/parser");
  5. // 默认导出为 ESModule 的方式
  6. const traverse = require("@babel/traverse").default;
  7. const moduleAnalyser = (filename) => {
  8. // 读取文件
  9. const content = fs.readFileSync(filename, "utf-8");
  10. // 提取文件依赖(代码分析)
  11. let parseContent = parser.parse(content, {
  12. sourceType: "module"
  13. })
  14. // 遍历 import 节点
  15. const depencies = {};
  16. traverse(parseContent, {
  17. // 到遇到引入模块的时候
  18. ImportDeclaration({ node }){
  19. const dirname = path.dirname(filename);
  20. const newFile = path.join(dirname, node.source.value)
  21. depencies[node.source.value] = newFile;
  22. }
  23. })
  24. console.log(depencies)
  25. }
  26. moduleAnalyser("./src/index.js")

然后运行node bundler.js我们就能得到入口文件的相对路径和绝对路径啦

  1. $ node bundler.js
  2. # { './message.js': 'src/message.js' }

接着将bundler.js文件分析的结果返回出去,写代码之前还需要安装两个模块:

  1. $ npm install @babel/core -D
  2. $ npm install @babel/preset-env

traverse转换后的AST树转为浏览器可运行的代码,然后返回出去:

  1. const fs = require("fs");
  2. // 引入path模块
  3. const path = require("path");
  4. const parser = require("@babel/parser");
  5. // 默认导出为 ESModule 的方式
  6. const traverse = require("@babel/traverse").default;
  7. const core = require("@babel/core");
  8. const moduleAnalyser = (filename) => {
  9. // 读取文件
  10. const content = fs.readFileSync(filename, "utf-8");
  11. // 提取文件依赖(代码分析)
  12. let parseContent = parser.parse(content, {
  13. sourceType: "module"
  14. })
  15. // 遍历 import 节点
  16. const depencies = {};
  17. traverse(parseContent, {
  18. // 到遇到引入模块的时候
  19. ImportDeclaration({ node }) {
  20. const dirname = path.dirname(filename);
  21. const newFile = path.join(dirname, node.source.value)
  22. depencies[node.source.value] = newFile;
  23. }
  24. })
  25. // 把 AST 抽象语法树转换为一个对象,可以在浏览器中运行的 Code
  26. const { code } = core.transformFromAst(parseContent, null, {
  27. presets: ["@babel/preset-env"]
  28. })
  29. return {
  30. filename,
  31. depencies,
  32. code
  33. }
  34. }
  35. const moduleInfo = moduleAnalyser("./src/index.js")
  36. console.log(moduleInfo);

运行命令node bundler.js

  1. xiechen@xiechendeMacBook-Pro bundler % node bundler.js
  2. {
  3. filename: './src/index.js',
  4. depencies: { './message.js': 'src/message.js' },
  5. code: '"use strict";\n' +
  6. '\n' +
  7. 'var _message = _interopRequireDefault(require("./message.js"));\n' +
  8. '\n' +
  9. 'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
  10. '\n' +
  11. 'console.log(_message["default"]);'
  12. }

现在我们就把bundler.js分析入口文件index.js后的代码返回了出去。

Dependencies Graph

到目前为止,我们可以把入口文件代码的依赖关系梳理清楚了,那么如何梳理整个项目的文件依赖关系呢?
1、先分析入口文件的依赖关系
2、拿到入口文件的依赖关系后,根据入口文件的依赖关系对被引入文件进行依赖分析(也就是depenciesimport关系)
3、使用循环遍历的方法进行递归分析。

重点代码在 43 行到 58 行:

  1. const fs = require("fs");
  2. // 引入path模块
  3. const path = require("path");
  4. const parser = require("@babel/parser");
  5. // 默认导出为 ESModule 的方式
  6. const traverse = require("@babel/traverse").default;
  7. const core = require("@babel/core");
  8. // 分析代码依赖
  9. const moduleAnalyser = (filename) => {
  10. // 读取文件
  11. const content = fs.readFileSync(filename, "utf-8");
  12. // 提取文件依赖(代码分析)
  13. let parseContent = parser.parse(content, {
  14. sourceType: "module"
  15. })
  16. // 遍历 import 节点
  17. const depencies = {};
  18. traverse(parseContent, {
  19. // 到遇到引入模块的时候
  20. ImportDeclaration({ node }) {
  21. const dirname = path.dirname(filename);
  22. const newFile = path.join(dirname, node.source.value)
  23. depencies[node.source.value] = newFile;
  24. }
  25. })
  26. // 把 AST 抽象语法树转换为一个对象,可以在浏览器中运行的 Code
  27. const { code } = core.transformFromAst(parseContent, null, {
  28. presets: ["@babel/preset-env"]
  29. })
  30. return {
  31. filename,
  32. depencies,
  33. code
  34. }
  35. }
  36. // 遍历所有模块,分析整个项目的依赖关系
  37. const makeDependenciesGraph = (entry) => {
  38. const entryModule = moduleAnalyser(entry);
  39. const graphArray = [entryModule];
  40. for (let i = 0; i < graphArray.length; i++) {
  41. const item = graphArray[i];
  42. const { depencies } = item;
  43. if (depencies) {
  44. for (const j in depencies) {
  45. graphArray.push(moduleAnalyser(depencies[j]))
  46. }
  47. }
  48. }
  49. console.log(graphArray)
  50. }
  51. const graphInfo = makeDependenciesGraph("./src/index.js");

运行node bundler.js后就得到了一个完整的项目依赖数组,index.js文件引入了message.js,message.js文件引入了word.jsword.js文件没有引入任何文件。

  1. [
  2. {
  3. filename: './src/index.js',
  4. depencies: { './message.js': 'src/message.js' },
  5. code: '"use strict";\n' +
  6. '\n' +
  7. 'var _message = _interopRequireDefault(require("./message.js"));\n' +
  8. '\n' +
  9. 'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
  10. '\n' +
  11. 'console.log(_message["default"]);'
  12. },
  13. {
  14. filename: 'src/message.js',
  15. depencies: { './word.js': 'src/word.js' },
  16. code: '"use strict";\n' +
  17. '\n' +
  18. 'Object.defineProperty(exports, "__esModule", {\n' +
  19. ' value: true\n' +
  20. '});\n' +
  21. 'exports["default"] = void 0;\n' +
  22. '\n' +
  23. 'var _word = require("./word.js");\n' +
  24. '\n' +
  25. 'var message = "say ".concat(_word.word);\n' +
  26. 'var _default = message;\n' +
  27. 'exports["default"] = _default;'
  28. },
  29. {
  30. filename: 'src/word.js',
  31. depencies: {},
  32. code: '"use strict";\n' +
  33. '\n' +
  34. 'Object.defineProperty(exports, "__esModule", {\n' +
  35. ' value: true\n' +
  36. '});\n' +
  37. 'exports.word = void 0;\n' +
  38. 'var word = "hello";\n' +
  39. 'exports.word = word;'
  40. }
  41. ]

为了方便我们后面进行代码打包我们进行一个格式优化,新增 58 行和 65 行,并且在 69 行打印结果。

  1. const fs = require("fs");
  2. // 引入path模块
  3. const path = require("path");
  4. const parser = require("@babel/parser");
  5. // 默认导出为 ESModule 的方式
  6. const traverse = require("@babel/traverse").default;
  7. const core = require("@babel/core");
  8. // 分析代码依赖
  9. const moduleAnalyser = (filename) => {
  10. // 读取文件
  11. const content = fs.readFileSync(filename, "utf-8");
  12. // 提取文件依赖(代码分析)
  13. let parseContent = parser.parse(content, {
  14. sourceType: "module"
  15. })
  16. // 遍历 import 节点
  17. const depencies = {};
  18. traverse(parseContent, {
  19. // 到遇到引入模块的时候
  20. ImportDeclaration({ node }) {
  21. const dirname = path.dirname(filename);
  22. const newFile = path.join(dirname, node.source.value)
  23. depencies[node.source.value] = newFile;
  24. }
  25. })
  26. // 把 AST 抽象语法树转换为一个对象,可以在浏览器中运行的 Code
  27. const { code } = core.transformFromAst(parseContent, null, {
  28. presets: ["@babel/preset-env"]
  29. })
  30. return {
  31. filename,
  32. depencies,
  33. code
  34. }
  35. }
  36. // 遍历所有模块,分析整个项目的依赖关系
  37. const makeDependenciesGraph = (entry) => {
  38. const entryModule = moduleAnalyser(entry);
  39. const graphArray = [entryModule];
  40. for (let i = 0; i < graphArray.length; i++) {
  41. const item = graphArray[i];
  42. const { depencies } = item;
  43. if (depencies) {
  44. for (const j in depencies) {
  45. graphArray.push(moduleAnalyser(depencies[j]))
  46. }
  47. }
  48. }
  49. const graph = {};
  50. graphArray.forEach(item => {
  51. graph[item.filename] = {
  52. depencies: item.depencies,
  53. code: item.code
  54. }
  55. })
  56. return graph;
  57. }
  58. const graphInfo = makeDependenciesGraph("./src/index.js");
  59. console.log(graphInfo)

node bundler.js的最终结果:

  1. {
  2. './src/index.js': {
  3. depencies: { './message.js': 'src/message.js' },
  4. code: '"use strict";\n' +
  5. '\n' +
  6. 'var _message = _interopRequireDefault(require("./message.js"));\n' +
  7. '\n' +
  8. 'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
  9. '\n' +
  10. 'console.log(_message["default"]);'
  11. },
  12. 'src/message.js': {
  13. depencies: { './word.js': 'src/word.js' },
  14. code: '"use strict";\n' +
  15. '\n' +
  16. 'Object.defineProperty(exports, "__esModule", {\n' +
  17. ' value: true\n' +
  18. '});\n' +
  19. 'exports["default"] = void 0;\n' +
  20. '\n' +
  21. 'var _word = require("./word.js");\n' +
  22. '\n' +
  23. 'var message = "say ".concat(_word.word);\n' +
  24. 'var _default = message;\n' +
  25. 'exports["default"] = _default;'
  26. },
  27. 'src/word.js': {
  28. depencies: {},
  29. code: '"use strict";\n' +
  30. '\n' +
  31. 'Object.defineProperty(exports, "__esModule", {\n' +
  32. ' value: true\n' +
  33. '});\n' +
  34. 'exports.word = void 0;\n' +
  35. 'var word = "hello";\n' +
  36. 'exports.word = word;'
  37. }
  38. }

生成代码

:::warning ⚠️ 注意
一定要对比这上面的依赖关系JSON对象来理解。 :::

重点在 66 行的generateCode方法:

  1. const fs = require("fs");
  2. // 引入path模块
  3. const path = require("path");
  4. const parser = require("@babel/parser");
  5. // 默认导出为 ESModule 的方式
  6. const traverse = require("@babel/traverse").default;
  7. const core = require("@babel/core");
  8. // 分析代码依赖
  9. const moduleAnalyser = (filename) => {
  10. // 读取文件
  11. const content = fs.readFileSync(filename, "utf-8");
  12. // 提取文件依赖(代码分析)
  13. let parseContent = parser.parse(content, {
  14. sourceType: "module"
  15. })
  16. // 遍历 import 节点
  17. const depencies = {};
  18. traverse(parseContent, {
  19. // 到遇到引入模块的时候
  20. ImportDeclaration({ node }) {
  21. const dirname = path.dirname(filename);
  22. const newFile = path.join(dirname, node.source.value)
  23. depencies[node.source.value] = newFile;
  24. }
  25. })
  26. // 把 AST 抽象语法树转换为一个对象,可以在浏览器中运行的 Code
  27. const { code } = core.transformFromAst(parseContent, null, {
  28. presets: ["@babel/preset-env"]
  29. })
  30. return {
  31. filename,
  32. depencies,
  33. code
  34. }
  35. }
  36. // 遍历所有模块,分析整个项目的依赖关系
  37. const makeDependenciesGraph = (entry) => {
  38. const entryModule = moduleAnalyser(entry);
  39. const graphArray = [entryModule];
  40. for (let i = 0; i < graphArray.length; i++) {
  41. const item = graphArray[i];
  42. const { depencies } = item;
  43. if (depencies) {
  44. for (const j in depencies) {
  45. graphArray.push(moduleAnalyser(depencies[j]))
  46. }
  47. }
  48. }
  49. const graph = {};
  50. graphArray.forEach(item => {
  51. graph[item.filename] = {
  52. depencies: item.depencies,
  53. code: item.code
  54. }
  55. })
  56. return graph;
  57. }
  58. const generateCode = (entry) => {
  59. // 如果不转为字符串得到的结果就是 [Object Object]
  60. const graph = JSON.stringify(makeDependenciesGraph(entry));
  61. // 返回一个立即执行函数防止全局变量污染
  62. return `
  63. (function(graph){
  64. // 因为我们生成的依赖对象中的Code存在require方法
  65. function require(module){
  66. // 将相对路径转换为绝对路径例如 ./message/js = ./src/message.js
  67. function localRequire(relativePath){
  68. return require(graph[module].depencies[relativePath])
  69. }
  70. // 因为我们生成的依赖对象中的Code存在exports对象
  71. var exports = {};
  72. // 执行每个模块的代码 index/message/word
  73. // 当执行到require的时候实际执行的是localRequire
  74. (function(require,exports,code){
  75. eval(code)
  76. })(localRequire,exports,graph[module].code);
  77. return exports;
  78. }
  79. require('${entry}');
  80. })(${graph})`;
  81. }
  82. const code = generateCode("./src/index.js");
  83. console.log(code);

node bundler.js后得到的代码复制然后到浏览器的控制台中输入就能看到打印的结果啦。
image.png