• 手写一个Webpack吧~


    前言

    Webpack是前端热门打包工具,是前端工程化的根基,在Webpack眼中,万物皆模块,因此Webpack打包的核心目的其实就是把所有模块都打包到一个文件中(bundle.js)

    在这里插入图片描述

    原理分析

    如果现在有这些模块:

    index.js

    import add from './add.js';
    console.log(add(1, 2));
    
    • 1
    • 2

    add.js

    export default function add(a, b) {
      return a + b;
    }
    
    • 1
    • 2
    • 3

    我们配置一个最基本的webpack.config.js文件:

    const path = require('path');
    
    module.exports = {
      entry: './index.js',
      output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
      },
      mode: 'none'
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    执行npx webpack,打包后结果:
    在这里插入图片描述

    可以看到,webpack将多个js文件打包到了一个文件中,这就是webpack的原理,要实现这个原理,需要这些步骤:

    • 分析entry文件;
    • 基于entry文件的导入依赖,递归查找出整个项目的依赖;
    • 将所有依赖转换成依赖图;
    • 写入bundle.js;

    分析entry文件

    由于需要es6转es5,以及ast语法树的生成,便于我们查找每个文件中的依赖关系,下载这些依赖包:

    npm i --save-dev @babel/parser @babel/traverse @babel/core
    
    • 1

    并且在项目中新建webpack.js文件,导入包:

    const fs = require('fs');
    const path = require('path');
    const parser = require('@babel/parser');
    const traverse = require('@babel/traverse').default;
    const babel = require('@babel/core');
    
    • 1
    • 2
    • 3
    • 4
    • 5

    接下来开始写第一个方法getModuleInfo,获取单个文件的信息,代码如下:

    //生成单文件依赖树
    function getModuleInfo(file) {
      //读取入口文件内容
      const body = fs.readFileSync(file, 'utf-8');
      //转为ast语法树
      const ast = parser.parse(body, {
        sourceType: 'module'
      })
      //收集所有模块依赖
      const deps = {};
      traverse(ast, {
        ImportDeclaration({ node }) {
          const dirname = path.dirname(file);
          //转换当前文件的绝对路径
          const absPath = path.join(dirname, node.source.value);
          deps[node.source.value] = absPath;
        }
      })
      //es6转es5代码
      const { code } = babel.transformFromAst(ast, null, {
        presets: ["@babel/preset-env"],
      });
      const moduleInfo = { file, deps, code };
      return moduleInfo;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    从代码可以看到,读取到文件信息后,将文件转为ast语法树,并开始收集依赖,这里遍历器用的是babel-traverse自带的遍历器,让我们快速收集到所有import导入依赖的语句,最后将文件代码转为es5代码。

    执行getModuleInfo('./index.js');出现结果:

    在这里插入图片描述

    这就是依赖树种一个文件的内容,接下来的思路很简单,基于entry文件的deps,递归查找所有deps文件中所用到的依赖,直到无依赖,并将收集到的信息全部保存在一个对象中。

    收集依赖

    这里我们创建两个函数,parseModules和getDeps,代码如下:

    //收集entry文件依赖,并整合所有依赖
    function parseModules(file) {
      const entry = getModuleInfo(file);
      let temps = [entry];
      const depsResult = {};          //整个项目最终所有文件 -> 代码块的映射表
      getDeps(entry, temps)
      temps.forEach(item => {
        depsResult[item.file] = {
          deps: item.deps,
          code: item.code
        }
      })
      return depsResult;
    }
    
    //收集entry文件所依赖的子文件其他依赖
    function getDeps({ deps }, temps) {
      //遍历入口文件的所有依赖,寻找更多依赖
      Object.keys(deps).forEach(d => {
        const child = getModuleInfo(deps[d]);
        temps.push(child);
        getDeps(child, temps)
      })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    在parseModules函数中,我们首先获取到了入口文件的依赖树,基于他的deps,进行更多deps的获取,并记录他们的依赖树,最后保存在depsResult中,结果如下:

    在这里插入图片描述
    接下来,我们将所有文件的代码合并起来即可。

    写入bundle

    这里,我们写一个自执行函数,里面包括了自定义require函数和exports对象,让浏览器识别到我们自定义的导入导出:

    function bundle(file) {
      const depsGraph = JSON.stringify(parseModules(file));
      return `(function (graph) {
            function require(file) {
                function absRequire(relPath) {
                    return require(graph[file].deps[relPath])
                }
                var exports = {};
                (function (require,exports,code) {
                    eval(code)
                })(absRequire,exports,graph[file].code)
                return exports
            }
            require('${file}')
        })(${depsGraph})`;
    }
    
    const content = bundle('./index.js');
    !fs.existsSync("./dist") && fs.mkdirSync("./dist");
    fs.writeFileSync("./dist/bundle.js", content);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    最后把这块代码写入dist/bundle.js即可。

    测试

    我们在index.html中引入dist/bundle.js:

    在这里插入图片描述
    打开浏览器:

    在这里插入图片描述

    源码

    const fs = require('fs');
    const path = require('path');
    const parser = require('@babel/parser');
    const traverse = require('@babel/traverse').default;
    const babel = require('@babel/core');
    
    //生成单文件依赖树
    function getModuleInfo(file) {
      //读取入口文件内容
      const body = fs.readFileSync(file, 'utf-8');
      //转为ast语法树
      const ast = parser.parse(body, {
        sourceType: 'module'
      })
      //收集所有模块依赖
      const deps = {};
      traverse(ast, {
        ImportDeclaration({ node }) {
          const dirname = path.dirname(file);
          //转换当前文件的绝对路径
          const absPath = path.join(dirname, node.source.value);
          deps[node.source.value] = absPath;
        }
      })
      //es6转es5代码
      const { code } = babel.transformFromAst(ast, null, {
        presets: ["@babel/preset-env"],
      });
      const moduleInfo = { file, deps, code };
      return moduleInfo;
    }
    
    //收集entry文件依赖,并整合所有依赖
    function parseModules(file) {
      const entry = getModuleInfo(file);
      let temps = [entry];
      const depsResult = {};          //整个项目最终所有文件 -> 代码块的映射表
      getDeps(entry, temps)
      temps.forEach(item => {
        depsResult[item.file] = {
          deps: item.deps,
          code: item.code
        }
      })
      return depsResult;
    }
    
    //收集entry文件所依赖的子文件其他依赖
    function getDeps({ deps }, temps) {
      //遍历入口文件的所有依赖,寻找更多依赖
      Object.keys(deps).forEach(d => {
        const child = getModuleInfo(deps[d]);
        temps.push(child);
        getDeps(child, temps)
      })
    }
    
    function bundle(file) {
      const depsGraph = JSON.stringify(parseModules(file));
      return `(function (graph) {
            function require(file) {
                function absRequire(relPath) {
                    return require(graph[file].deps[relPath])
                }
                var exports = {};
                (function (require,exports,code) {
                    eval(code)
                })(absRequire,exports,graph[file].code)
                return exports
            }
            require('${file}')
        })(${depsGraph})`;
    }
    
    const content = bundle('./index.js');
    !fs.existsSync("./dist") && fs.mkdirSync("./dist");
    fs.writeFileSync("./dist/bundle.js", content);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77

    ok,学费了~

  • 相关阅读:
    Android IO 框架 Okio 的实现原理,如何检测超时?
    OAuth 2.0一键登录那些事
    微服务中的鉴权该怎么做?
    Python-爬虫(基础概念、常见请求模块(urllib、requests))
    软件测试分析流程及输出项包括哪些内容?
    奇安信发布《2024人工智能安全报告》,AI深度伪造欺诈激增30倍
    带你玩转序列模型之Bleu得分&注意力模型&语音识别
    Vue挂载(mount)和继承(extend)
    【每日一题】535. TinyURL 的加密与解密
    企业数字化转型-数字技术创新对企业市场价值的影响研究(数据复现)
  • 原文地址:https://blog.csdn.net/m0_46995864/article/details/125568332