webpack篇之依赖分析

190 阅读5分钟

webpack篇之依赖分析

前置知识 babel & ast

babel的原理

  1. parse: 把代码code 变为 ast

  2. traverse: 遍历ast并修改代码

  3. generate: 将ast变为新的代码 newcode

经过这三步,就可以对代码进行转换

转换的例子

  1. 下面是一个把代码中的let转化为var的例子

    import { writeFileSync } from 'fs';
    import { parse } from '@babel/parser';
    import _traverse from '@babel/traverse';
    const traverse = _traverse.default;
    import _generator from '@babel/generator';
    const generator = _generator.default;
    
    const code = `let a = 'let'; let b = 2;const c = false`;
    const ast = parse(code, { sourceType: 'module' }); // 第一步
    // 以下底第二步
    traverse(ast, {
      enter: (item) => {
        if (item.node.type === 'VariableDeclaration') {
          if (item.node.kind === 'let') {
            item.node.kind = 'var';
          }
        }
      },
    });
    // 第三步
    const result = generator(ast, {}, code);
    writeFileSync('./file_let_to_var.js', result.code);
    

    效果如下

    image.png

    image.png

  2. 将es6的代码转为es5

    import { parse } from '@babel/parser';
    import * as babel from '@babel/core';
    import * as fs from 'fs'
    
    const code =  fs.readFileSync('./test.js').toString()
    const ast = parse(code, { sourceType: 'module' });
    const result = babel.transformFromAstSync(ast, code, { 
      presets: ['@babel/preset-env'],
    });
    fs.writeFileSync('./test.es5.js',result.code)
    

    效果如下:

    image.png

    image.png

    image.png

开始 什么是打包器?

最简单的打包 则是把多个文件整合成一个文件

下面就来研究如何通过ast写一个简易的打包器

打包器的构建

假设有如下的场景

  • 有一个index.js[入口文件]
  • index中有 import a from './a.js'
  • index中又有 import b from './b.js

如何把这三个文件的代码打包?

想要打包 首先得知道index.js依赖了谁

收集依赖的思路

  • 先从入口文件开始,即调用collectDepsAndCode(index.js)
  • 初始化depRelation[入口] = {deps: [],code: `# 入口文件的代码`}
  • 把入口文件的代码code变为ast
  • 遍历ast找到依赖
  • 把依赖写入depRelation[入口].deps数组中
  • 最终得到的就是index.js的依赖

代码

import { parse } from '@babel/parser';
import _traverse from '@babel/traverse';
const traverse = _traverse.default;
import { readFileSync } from 'fs';
import { resolve, relative, dirname } from 'path';


// 设置根目录
const projectRoot = './project_1'
// 类型声明
// type DepRelation = { [key: string]: { deps: string[], code: string } }
// 初始化一个空的 depRelation,用于收集依赖
const depRelation = {};

// 将入口文件的绝对路径传入函数,如 D:\demo\fixture_1\index.js
collectCodeAndDeps(resolve(projectRoot, 'index.js'));

console.log(depRelation);
console.log('done');

function collectCodeAndDeps(filepath) {
  const key = getProjectPath(filepath); // 文件的项目路径,如 index.js
  // 获取文件内容,将内容放至 depRelation
  const code = readFileSync(filepath).toString();
  // 初始化 depRelation[key]
  depRelation[key] = { deps: [], code: code };
  // 将代码转为 AST
  const ast = parse(code, { sourceType: 'module' });
  // 分析文件依赖,将内容放至 depRelation
  traverse(ast, {
    enter: (path) => {
      if (path.node.type === 'ImportDeclaration') {
        // path.node.source.value 往往是一个相对路径,如 ./a.js,需要先把它转为一个绝对路径
        const depAbsolutePath = resolve(
          dirname(filepath),
          path.node.source.value
        );
        // 然后转为项目路径
        const depProjectPath = getProjectPath(depAbsolutePath);
        // 把依赖写进 depRelation
        depRelation[key].deps.push(depProjectPath);
      }
    },
  });
}
// 获取文件相对于根目录的相对路径
function getProjectPath(path) {
  return relative(projectRoot, path).replace(/\\/g, '/');
}

效果如下

image.png

image.png

image.png

image.png

上述代码可以完成依赖收集 但是又有其他的问题 问题如下

问题1?

假设依赖中还有依赖 上述代码就没办法分析到全部的依赖了

  • 有一个index.js[入口文件]
  • index中有 import a from './a.js'
  • index中又有 import b from './b.js
  • a.js中依赖了其他文件
  • b.js中也依赖了其他文件

如何解决这个问题?

问题1解决思路

  • 依然使用collectDepsAndCode(index.js)
  • 发现a.js 调用collectDepsAndCode(a.js)
  • 又发现了a.js依赖了其他依赖 调用collectDepsAndCode(a的依赖.js),直到没有依赖
  • 重复a.js的逻辑 去处理b.js
  • 其实这就是递归

问题1解决代码

import { parse } from '@babel/parser';
import _traverse from '@babel/traverse';
const traverse = _traverse.default;
import { readFileSync } from 'fs';
import { resolve, relative, dirname } from 'path';

// 设置根目录
const projectRoot = 'project_2';
// 类型声明
// type DepRelation = { [key: string]: { deps: string[], code: string } }
// 初始化一个空的 depRelation,用于收集依赖
const depRelation = {};

// 将入口文件的绝对路径传入函数,如 D:\demo\fixture_1\index.js
collectCodeAndDeps(resolve(projectRoot, 'index.js'));

console.log(depRelation);
console.log('done');

function collectCodeAndDeps(filepath) {
  const key = getProjectPath(filepath); // 文件的项目路径,如 index.js
  // 获取文件内容,将内容放至 depRelation
  const code = readFileSync(filepath).toString();
  // 初始化 depRelation[key]
  depRelation[key] = { deps: [], code: code };
  // 将代码转为 AST
  const ast = parse(code, { sourceType: 'module' });
  // 分析文件依赖,将内容放至 depRelation
  traverse(ast, {
    enter: (path) => {
      if (path.node.type === 'ImportDeclaration') {
        // path.node.source.value 往往是一个相对路径,如 ./a.js,需要先把它转为一个绝对路径
        const depAbsolutePath = resolve(
          dirname(filepath),
          path.node.source.value
        );
        // 然后转为项目路径
        const depProjectPath = getProjectPath(depAbsolutePath);
        // 把依赖写进 depRelation
        depRelation[key].deps.push(depProjectPath);
        collectCodeAndDeps(depAbsolutePath);  // 重点 递归的处理依赖
      }
    },
  });
}
// 获取文件相对于根目录的相对路径
function getProjectPath(path) {
  return relative(projectRoot, path).replace(/\\/g, '/');
}

代码效果图

image.png

上述代码实现了对深层依赖情况的处理 but 还有问题

问题2

假设存在循环依赖 上述代码就会报错 因为递归没有出口 如何解决这个问题?

例如:

  • 有index.js
  • index.js 依赖了a.js和b.js
  • a.js 依赖了b.js
  • b.js 又依赖了a.js

问题2解决思路

  • 如果发现这个依赖已经被分析过,则退出递归 这样递归就有出口了 也就不会死循环了

问题2解决代码

import { parse } from '@babel/parser';
import _traverse from '@babel/traverse';
const traverse = _traverse.default;
import { readFileSync } from 'fs';
import { resolve, relative, dirname } from 'path';

// 设置根目录
const projectRoot = 'project_3';
// 类型声明
// type DepRelation = { [key: string]: { deps: string[], code: string } }
// 初始化一个空的 depRelation,用于收集依赖
const depRelation = {};

// 将入口文件的绝对路径传入函数,如 D:\demo\fixture_1\index.js
collectCodeAndDeps(resolve(projectRoot, 'index.js'));

console.log(depRelation);
console.log('done');

function collectCodeAndDeps(filepath) {
  const key = getProjectPath(filepath); // 文件的项目路径,如 index.js
  // 获取文件内容,将内容放至 depRelation
  if (Object.keys(depRelation).indexOf(key) >= 0) return; // 如果这个文件被分析过 则return 相当于递归的出口
  const code = readFileSync(filepath).toString();
  // 初始化 depRelation[key]
  depRelation[key] = { deps: [], code: code };
  // 将代码转为 AST
  const ast = parse(code, { sourceType: 'module' });
  // 分析文件依赖,将内容放至 depRelation
  traverse(ast, {
    enter: (path) => {
      if (path.node.type === 'ImportDeclaration') {
        // path.node.source.value 往往是一个相对路径,如 ./a.js,需要先把它转为一个绝对路径
        console.log(dirname(filepath), path.node.source.value);
        const depAbsolutePath = resolve(
          dirname(filepath),
          path.node.source.value
        );
        // 然后转为项目路径
        const depProjectPath = getProjectPath(depAbsolutePath);
        // 把依赖写进 depRelation
        depRelation[key].deps.push(depProjectPath);
        collectCodeAndDeps(depAbsolutePath);
      }
    },
  });
}
// 获取文件相对于根目录的相对路径
function getProjectPath(path) {
  return relative(projectRoot, path).replace(/\\/g, '/');
}

问题2代码效果

image.png

结论

本文探索了babel、ast 以及如何使用ast去分析文件的依赖,为接下来的打包器打下了基础