必会!
参考文档:

作者:Sunshine_Lin

采用面向对象的方式,对上一篇【从零开始实现webpack核心打包过程】(面向过程)进行了封装,零散的方法封装成类调用,更具有webpack的形状,通过config.js配置
github代码仓库: https://github.com/all-smile/webpack-easy

原图理解

  1. 首先肯定是要先解析入口文件entry,将其转为AST(抽象语法树),使用@babel/parser
  2. 然后使用@babel/traverse去找出入口文件所有依赖模块
  3. 然后使用@babel/core+@babel/preset-env将入口文件的AST转为Code
  4. 将2中找到的入口文件的依赖模块,进行遍历递归,重复执行1,2,3
  5. 重写require函数,并与4中生成的递归关系图一起,输出到bundle中

857cf7c7368d730b170e58860c04d7fc.png

代码实现

webpack具体实现原理是很复杂的,这里只是简单实现一下,了解一下webpack是怎么运作的。在代码实现过程中,大家可以自己console.log一下,看看ast,dependcies,code这些具体长什么样,我这里就不展示了,自己去看会比较有成就感,嘿嘿!!

项目目录

image.png

config.js

这个文件中模拟webpack的配置

  1. const path = require('path')
  2. module.exports = {
  3. entry: './src/index.js',
  4. output: {
  5. path: path.resolve(__dirname, './dist'),
  6. filename: 'main.js'
  7. }
  8. }

入口文件

src/index.js是入口文件

  1. // src/index
  2. import add from "./add.js";
  3. console.log(add(1 , 2))
  4. // src/add.js
  5. export default (a, b) => a + b

1. 定义Compiler类

  1. // index.js
  2. class Compiler {
  3. constructor(options) {
  4. // webpack 配置
  5. const { entry, output } = options
  6. // 入口
  7. this.entry = entry
  8. // 出口
  9. this.output = output
  10. // 模块
  11. this.modules = []
  12. }
  13. // 构建启动
  14. run() {}
  15. // 重写 require函数,输出bundle
  16. generate() {}
  17. }

2. 解析入口文件,获取 AST

我们这里使用@babel/parser,这是babel7的工具,来帮助我们分析内部的语法,包括 es6,返回一个 AST 抽象语法树

  1. const fs = require('fs')
  2. const parser = require('@babel/parser')
  3. const options = require('./webpack.config')
  4. const Parser = {
  5. getAst: path => {
  6. // 读取入口文件
  7. const content = fs.readFileSync(path, 'utf-8')
  8. // 将文件内容转为AST抽象语法树
  9. return parser.parse(content, {
  10. sourceType: 'module'
  11. })
  12. }
  13. }
  14. class Compiler {
  15. constructor(options) {
  16. // webpack 配置
  17. const { entry, output } = options
  18. // 入口
  19. this.entry = entry
  20. // 出口
  21. this.output = output
  22. // 模块
  23. this.modules = []
  24. }
  25. // 构建启动
  26. run() {
  27. const ast = Parser.getAst(this.entry)
  28. }
  29. // 重写 require函数,输出bundle
  30. generate() {}
  31. }
  32. new Compiler(options).run()

3. 找出所有依赖模块

Babel 提供了@babel/traverse(遍历)方法维护这 AST 树的整体状态,我们这里使用它来帮我们找出依赖模块。

  1. const fs = require('fs')
  2. const path = require('path')
  3. const options = require('./webpack.config')
  4. const parser = require('@babel/parser')
  5. const traverse = require('@babel/traverse').default
  6. const Parser = {
  7. getAst: path => {
  8. // 读取入口文件
  9. const content = fs.readFileSync(path, 'utf-8')
  10. // 将文件内容转为AST抽象语法树
  11. return parser.parse(content, {
  12. sourceType: 'module'
  13. })
  14. },
  15. getDependecies: (ast, filename) => {
  16. const dependecies = {}
  17. // 遍历所有的 import 模块,存入dependecies
  18. traverse(ast, {
  19. // 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
  20. ImportDeclaration({ node }) {
  21. const dirname = path.dirname(filename)
  22. // 保存依赖模块路径,之后生成依赖关系图需要用到
  23. const filepath = './' + path.join(dirname, node.source.value)
  24. dependecies[node.source.value] = filepath
  25. }
  26. })
  27. return dependecies
  28. }
  29. }
  30. class Compiler {
  31. constructor(options) {
  32. // webpack 配置
  33. const { entry, output } = options
  34. // 入口
  35. this.entry = entry
  36. // 出口
  37. this.output = output
  38. // 模块
  39. this.modules = []
  40. }
  41. // 构建启动
  42. run() {
  43. const { getAst, getDependecies } = Parser
  44. const ast = getAst(this.entry)
  45. const dependecies = getDependecies(ast, this.entry)
  46. }
  47. // 重写 require函数,输出bundle
  48. generate() {}
  49. }
  50. new Compiler(options).run()

4. AST 转换为 code

将 AST 语法树转换为浏览器可执行代码,我们这里使用@babel/core 和 @babel/preset-env。

  1. const fs = require('fs')
  2. const path = require('path')
  3. const options = require('./webpack.config')
  4. const parser = require('@babel/parser')
  5. const traverse = require('@babel/traverse').default
  6. const { transformFromAst } = require('@babel/core')
  7. const Parser = {
  8. getAst: path => {
  9. // 读取入口文件
  10. const content = fs.readFileSync(path, 'utf-8')
  11. // 将文件内容转为AST抽象语法树
  12. return parser.parse(content, {
  13. sourceType: 'module'
  14. })
  15. },
  16. getDependecies: (ast, filename) => {
  17. const dependecies = {}
  18. // 遍历所有的 import 模块,存入dependecies
  19. traverse(ast, {
  20. // 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
  21. ImportDeclaration({ node }) {
  22. const dirname = path.dirname(filename)
  23. // 保存依赖模块路径,之后生成依赖关系图需要用到
  24. const filepath = './' + path.join(dirname, node.source.value)
  25. dependecies[node.source.value] = filepath
  26. }
  27. })
  28. return dependecies
  29. },
  30. getCode: ast => {
  31. // AST转换为code
  32. const { code } = transformFromAst(ast, null, {
  33. presets: ['@babel/preset-env']
  34. })
  35. return code
  36. }
  37. }
  38. class Compiler {
  39. constructor(options) {
  40. // webpack 配置
  41. const { entry, output } = options
  42. // 入口
  43. this.entry = entry
  44. // 出口
  45. this.output = output
  46. // 模块
  47. this.modules = []
  48. }
  49. // 构建启动
  50. run() {
  51. const { getAst, getDependecies, getCode } = Parser
  52. const ast = getAst(this.entry)
  53. const dependecies = getDependecies(ast, this.entry)
  54. const code = getCode(ast)
  55. }
  56. // 重写 require函数,输出bundle
  57. generate() {}
  58. }
  59. new Compiler(options).run()

image.png

5. 递归解析所有依赖项,生成依赖关系图

  1. const fs = require('fs')
  2. const path = require('path')
  3. const options = require('./webpack.config')
  4. const parser = require('@babel/parser')
  5. const traverse = require('@babel/traverse').default
  6. const { transformFromAst } = require('@babel/core')
  7. const Parser = {
  8. getAst: path => {
  9. // 读取入口文件
  10. const content = fs.readFileSync(path, 'utf-8')
  11. // 将文件内容转为AST抽象语法树
  12. return parser.parse(content, {
  13. sourceType: 'module'
  14. })
  15. },
  16. getDependecies: (ast, filename) => {
  17. const dependecies = {}
  18. // 遍历所有的 import 模块,存入dependecies
  19. traverse(ast, {
  20. // 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
  21. ImportDeclaration({ node }) {
  22. const dirname = path.dirname(filename)
  23. // 保存依赖模块路径,之后生成依赖关系图需要用到
  24. const filepath = './' + path.join(dirname, node.source.value)
  25. dependecies[node.source.value] = filepath
  26. }
  27. })
  28. return dependecies
  29. },
  30. getCode: ast => {
  31. // AST转换为code
  32. const { code } = transformFromAst(ast, null, {
  33. presets: ['@babel/preset-env']
  34. })
  35. return code
  36. }
  37. }
  38. class Compiler {
  39. constructor(options) {
  40. // webpack 配置
  41. const { entry, output } = options
  42. // 入口
  43. this.entry = entry
  44. // 出口
  45. this.output = output
  46. // 模块
  47. this.modules = []
  48. }
  49. // 构建启动
  50. run() {
  51. // 解析入口文件
  52. const info = this.build(this.entry)
  53. this.modules.push(info)
  54. this.modules.forEach(({ dependecies }) => {
  55. // 判断有依赖对象,递归解析所有依赖项
  56. if (dependecies) {
  57. for (const dependency in dependecies) {
  58. this.modules.push(this.build(dependecies[dependency]))
  59. }
  60. }
  61. })
  62. // 生成依赖关系图
  63. const dependencyGraph = this.modules.reduce(
  64. (graph, item) => ({
  65. ...graph,
  66. // 使用文件路径作为每个模块的唯一标识符,保存对应模块的依赖对象和文件内容
  67. [item.filename]: {
  68. dependecies: item.dependecies,
  69. code: item.code
  70. }
  71. }),
  72. {}
  73. )
  74. }
  75. build(filename) {
  76. const { getAst, getDependecies, getCode } = Parser
  77. const ast = getAst(filename)
  78. const dependecies = getDependecies(ast, filename)
  79. const code = getCode(ast)
  80. return {
  81. // 文件路径,可以作为每个模块的唯一标识符
  82. filename,
  83. // 依赖对象,保存着依赖模块路径
  84. dependecies,
  85. // 文件内容
  86. code
  87. }
  88. }
  89. // 重写 require函数,输出bundle
  90. generate() {}
  91. }
  92. new Compiler(options).run()

image.png

6. 重写 require 函数,输出 bundle

  1. const fs = require('fs')
  2. const path = require('path')
  3. const options = require('./webpack.config')
  4. const parser = require('@babel/parser')
  5. const traverse = require('@babel/traverse').default
  6. const { transformFromAst } = require('@babel/core')
  7. const Parser = {
  8. getAst: path => {
  9. // 读取入口文件
  10. const content = fs.readFileSync(path, 'utf-8')
  11. // 将文件内容转为AST抽象语法树
  12. return parser.parse(content, {
  13. sourceType: 'module'
  14. })
  15. },
  16. getDependecies: (ast, filename) => {
  17. const dependecies = {}
  18. // 遍历所有的 import 模块,存入dependecies
  19. traverse(ast, {
  20. // 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
  21. ImportDeclaration({ node }) {
  22. const dirname = path.dirname(filename)
  23. // 保存依赖模块路径,之后生成依赖关系图需要用到
  24. const filepath = './' + path.join(dirname, node.source.value)
  25. dependecies[node.source.value] = filepath
  26. }
  27. })
  28. return dependecies
  29. },
  30. getCode: ast => {
  31. // AST转换为code
  32. const { code } = transformFromAst(ast, null, {
  33. presets: ['@babel/preset-env']
  34. })
  35. return code
  36. }
  37. }
  38. class Compiler {
  39. constructor(options) {
  40. // webpack 配置
  41. const { entry, output } = options
  42. // 入口
  43. this.entry = entry
  44. // 出口
  45. this.output = output
  46. // 模块
  47. this.modules = []
  48. }
  49. // 构建启动
  50. run() {
  51. // 解析入口文件
  52. const info = this.build(this.entry)
  53. this.modules.push(info)
  54. this.modules.forEach(({ dependecies }) => {
  55. // 判断有依赖对象,递归解析所有依赖项
  56. if (dependecies) {
  57. for (const dependency in dependecies) {
  58. this.modules.push(this.build(dependecies[dependency]))
  59. }
  60. }
  61. })
  62. // 生成依赖关系图
  63. const dependencyGraph = this.modules.reduce(
  64. (graph, item) => ({
  65. ...graph,
  66. // 使用文件路径作为每个模块的唯一标识符,保存对应模块的依赖对象和文件内容
  67. [item.filename]: {
  68. dependecies: item.dependecies,
  69. code: item.code
  70. }
  71. }),
  72. {}
  73. )
  74. this.generate(dependencyGraph)
  75. }
  76. build(filename) {
  77. const { getAst, getDependecies, getCode } = Parser
  78. const ast = getAst(filename)
  79. const dependecies = getDependecies(ast, filename)
  80. const code = getCode(ast)
  81. return {
  82. // 文件路径,可以作为每个模块的唯一标识符
  83. filename,
  84. // 依赖对象,保存着依赖模块路径
  85. dependecies,
  86. // 文件内容
  87. code
  88. }
  89. }
  90. // 重写 require函数 (浏览器不能识别commonjs语法),输出bundle
  91. generate(code) {
  92. // 输出文件路径
  93. const filePath = path.join(this.output.path, this.output.filename)
  94. // 懵逼了吗? 没事,下一节我们捋一捋
  95. const bundle = `(function(graph){
  96. function require(module){
  97. function localRequire(relativePath){
  98. return require(graph[module].dependecies[relativePath])
  99. }
  100. var exports = {};
  101. (function(require,exports,code){
  102. eval(code)
  103. })(localRequire,exports,graph[module].code);
  104. return exports;
  105. }
  106. require('${this.entry}')
  107. })(${JSON.stringify(code)})`
  108. // 把文件内容写入到文件系统
  109. fs.writeFileSync(filePath, bundle, 'utf-8')
  110. }
  111. }
  112. new Compiler(options).run()

7. 看看main里的代码

实现了上面的代码,也就实现了把打包后的代码写到main.js文件里,咱们来看看那main.js文件里的代码吧:

  1. (function (graph) {
  2. function require(module) {
  3. if (!module) {
  4. return;
  5. }
  6. function localRequire(relativePath) {
  7. return require(graph[module].dependecies[relativePath]);
  8. }
  9. var exports = {};
  10. (function (require, exports, code) {
  11. eval(code);
  12. })(localRequire, exports, graph[module].code);
  13. return exports;
  14. }
  15. require("./src/index.js");
  16. })({
  17. "./src/index.js": {
  18. dependecies: { "./add.js": "./src\\add.js" },
  19. code: '"use strict";\n\nvar _add = _interopRequireDefault(require("./add.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nconsole.log((0, _add["default"])(1, 2));',
  20. },
  21. "./src\\add.js": {
  22. dependecies: {},
  23. code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports["default"] = void 0;\n\nvar _default = function _default(a, b) {\n return a + b;\n};\n\nexports["default"] = _default;',
  24. },
  25. });

大家可以执行一下main.js的代码,输出结果是:

  1. 3