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

利用AST解决项目webpack alias泛滥问题:2019-1-24 #16

Open
jsonz1993 opened this issue Jul 27, 2021 · 0 comments
Open

利用AST解决项目webpack alias泛滥问题:2019-1-24 #16

jsonz1993 opened this issue Jul 27, 2021 · 0 comments

Comments

@jsonz1993
Copy link
Owner

本文为博客迁移过来,原文链接: 利用AST解决项目webpack alias泛滥问题:2019-1-24

文章代码的源码仓库

AST 简单介绍

AST(Abstract Syntax Tree)既抽象语法树,或称语法树,简单来说就是代码语法结构的一种抽象表示。比如 var answer = 6 * 7; 会被解析为这么一棵树

那么代码怎样才能解析成这一棵 AST, AST在前端领域一般又可以干嘛?

编译器

ast是由编译器解析生成的,简单的编译器可以由以下几部分组成:

  • tokenizer 分词器 把代码或文本按类型分开,返回: tokens
  • parser 语法剖析器 对分词后的文本按照语法分析转换成 抽象语法树 返回:ast
  • transformer 语法转换器 把语法剖析后的抽象语法树转换成我们想要的: newAst
  • codeGenerator 代码生成器 把AST 转换为 目标代码

我们前端构建中很常用的babel就是这种原理

babel 初始阶段并没有做任何事,基本上等于 const babel = code=> code; 先 tokenizer, parser 解析代码,再 transformer 的时候,完全不改动原来的 ast

对编译器原理有兴趣的,可以看我以前写的小demo,500行简单易懂 min-compiler,看完会有个整体概念。

而生成的AST我们可以用来做什么?
AST你都拿到了,剩下的事情就是对这棵树做你想要的操作,比如代码转换(babel),代码压缩等。

这里我用他来处理webpack的alias泛滥问题。

webpack alias问题

webpack alias 在很多情况下可以提供便利,但是如果项目参加的人太多,又没有什么约束,大家贪图方便什么都加到alias....就会变成这样子

  • 很多你不知道他到底是node_modules里面的包还是自己封装过的
  • 很多写二级路径就可以获取到的,没必要多加个alias
  • 现在比较推崇不是很远的路径都写成相对路径,编辑器可以直接跳过去方便(虽然这个可以通过jsconfig来解决,但是太多看着也很烦)
    所以我决定把项目里的alias从23个缩减为7个。

利用 estools 解决webpack Alias

我们先来整理一下思路

  • 先用解析器把代码解析成 AST
  • 再找出我们需要去除的alias,把他改为其他值,生成新的AST
  • 把新的AST转为代码,重新写入文件

我们这里的把alias改为其他值,指的是这种情况

目录结构:
- src
  - components
    - btn

alias: {
  btn: path.resolve(basepath, 'src/components/btn'),
  btn: path.resolve(basepath, 'src/components'),
}

原来的引入 import Btn from 'btn';
改为 import Btn from 'components/btn';

这里我们用 esprima 来做代码分析生成ast,用 estraverse 来转换代码,用 escodegen 生成代码。直接上代码

const aliasConfig = { /* webpack alias 配置*/}

function translateAlias(filePath) {

  // 解析ast
  const codeStr = fs.readFileSync(filePath).toString();
  const ast = esprima.parseModule(codeStr);

  // 转换ast
  estraverse.traverse(ast, {
    // 对于每个node节点都会进入这个函数
    enter(node, parent) {
      // 判断是否是我们的目标文件
      const isAliasDec = isRequireDeclaration(node, parent);
      if (isAliasDec) {
        // 替换掉alias => newAlias
        const newVal = getModulePath(node.value, filePath);
        node.value = newVal;
      }
    },
  });

  // 重新生成代码
  const newCodeStr = escodegen.generate(ast);
  fs.writeFileSync(filePath, newCodeStr, {});
}

// 工具函数: 判断是否是 require
function isRequireDeclaration(node, parent) {
  const { type, value } = node;
  const { callee } = parent || {};
  // 类型一致 && 该key在aliasKey中 && 是 require引入的
  return (
    type === 'Literal' &&
    aliasKey.includes(value) &&
    !allowAliasKey.includes(value) &&
    isRequest(callee)
  );
}

// 工具函数:获取路径
function getModulePath(aliasKey, filePath) {
  const firstDir = /\w*/.exec(aliasKey)[0];

  const modulePath = aliasKey.replace(firstDir, aliasConfig[firstDir]);
  const aliasPath = aliasKey.replace(firstDir, aliasMap[firstDir]);

  if (!aliasConfig[firstDir] || !aliasMap[firstDir] || allowAliasKey.includes(firstDir)) return false;

  // 获取引入的模块与当前模块相对路径,判断是否太长,是就返回alias,否则就返回相对路径就完事了
  const relativePath = path.relative(filePath, modulePath);
  const relativeTime = relativePath.split('../').length - 1;
  return (relativeTime < MAX_RELATIVE)? relativePath: aliasPath;
}

translateAlias(filePath);

estool_ast
试跑了一下,发现说虽然代码引用确实有被替换了,但是代码里面的所有空行和注释都丢了,而且一些规范格式也和原来不一样。
这显然是不行的,先不说格式的问题,一个文件连换行和注释都没有,那他就是没有灵魂的js~

看了下这是因为 esprima 在解析的时候,遇到空行和注释会直接跳过不解析生成AST,所以会导致后面生成的代码没有空行和注释。

babel解决空行和注释等问题、prettier保持代码风格一致

我们平时项目上用的最多的转换代码的工具就是babel,那么我们也可以把 esTool 那一套换成 babel生态,用babel来帮我们做这些转换。

原理和思路基本上是一样的,用 babylon 解析,babel-traverse 转换,再用babel-generator生成代码。
生成之后,先不写进去,而是用 prettier 格式化一遍再重写到本地,以保持和原来的风格一致。

function translateAlias(filePath) {
  console.log(`开始处理第${i++}个: ${filePath}`)
  const code = fs.readFileSync(filePath).toString();

  // 获取ast
  const ast = babylon.parse(code, {
    sourceType: 'module',
    plugins: ['jsx', 'objectRestSpread']
  });

  traverse(ast, {
    enter(path) {
      // 转换 CommonJs 的情况
      translateRequireModulePath(path, filePath);
      // 转换 ESM 的情况
      translateImportModulePath(path, filePath);
    }
  });

  const newCode = generate(ast, {});
  // 重新用项目的prettier配置格式化多一次再写入
  const prettierCode = prettier.format(newCode.code, prettierConfig);
  fs.writeFileSync(filePath, prettierCode);
  console.log(`处理结束${filePath}`)
}

https://raw.githubusercontent.com/jsonz1993/pic-repo/master/assets/images/AST-practice/babel__1627367242656.png

到此减少webpack-alias的功能处理完成,最后总结一下

  1. glob读取所有要转的js文件
  2. babylon 将js文件解析成AST
  3. babel-traverse 处理AST,判断如果是 require('xxx')或者import xxx from 'xxx' 替换掉这些路径
  4. babel-generator 将新生成的AST转化为代码
  5. prettier 格式化新生成的代码,保持与原项目风格一致
  6. 重新写入本地文件

告辞!

https://raw.githubusercontent.com/jsonz1993/pic-repo/master/assets/images/AST-practice/kaiji__1627367242656.jpg
最后写的时候参考到的链接,大部分是类库的文档
迷你编译器
estools代码生成escodegen
estools代码转换estraverse
代码解析esprima
babel plugin book
babel-generator
babel-traverse
babylon
在线ast生成
在线ast生成

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

No branches or pull requests

1 participant