cli中的@/components/utils是怎么被替换的成对应的alias的?

前言

我们都知道在webpack,vite,rsbuild等cli中我们可以使用@xxx/xxx引入文件,也可以通过在config文件内部配置alias控制对应引入路径。甚至添加较多的匹配规则

bash 复制代码
 alias: {
      '@': './src/',
      '@common': './src/common',
      '@op': './src/operation',
    },

但是这个功能是怎么实现的呢?这里将带大家手挫一下简单的实现。

目标🎯

src/page:

javascript 复制代码
// src/demo.ts
import { foo } from '@common/foo';
const bar = require('@op/bar');

alias 配置:

perl 复制代码
{
  "@common": "./src/common",
  "@op": "./src/operation"
}

output:

javascript 复制代码
import { foo } from './src/common/foo';
const bar = require('./src/operation/bar');

实现

大家都知道当我们在打包编译过程中都是回去读取文件,第一反应会是怎么去读取呢?Reg匹配吗?这样可以但是效率低下,同时十分消耗性能。

在编译过程中,我们是会转换成AST,而我们就在这里面去做动作。

以一个import { add } from '@components/utils'引入为例,使用babel-parse编译得到AST🌲如下:

json 复制代码
{
  "type": "File",
  "start": 0,
  "end": 40,
  "loc": {
    "start": {
      "line": 1,
      "column": 0,
      "index": 0
    },
    "end": {
      "line": 1,
      "column": 40,
      "index": 40
    }
  },
  "errors": [],
  "program": {
    "type": "Program",
    "start": 0,
    "end": 40,
    "loc": {
      "start": {
        "line": 1,
        "column": 0,
        "index": 0
      },
      "end": {
        "line": 1,
        "column": 40,
        "index": 40
      }
    },
    "sourceType": "module",
    "interpreter": null,
    "body": [
      {
        "type": "ImportDeclaration",
        "start": 0,
        "end": 40,
        "loc": {
          "start": {
            "line": 1,
            "column": 0,
            "index": 0
          },
          "end": {
            "line": 1,
            "column": 40,
            "index": 40
          }
        },
        "specifiers": [
          {
            "type": "ImportSpecifier",
            "start": 9,
            "end": 12,
            "loc": {
              "start": {
                "line": 1,
                "column": 9,
                "index": 9
              },
              "end": {
                "line": 1,
                "column": 12,
                "index": 12
              }
            },
            "imported": {
              "type": "Identifier",
              "start": 9,
              "end": 12,
              "loc": {
                "start": {
                  "line": 1,
                  "column": 9,
                  "index": 9
                },
                "end": {
                  "line": 1,
                  "column": 12,
                  "index": 12
                },
                "identifierName": "add"
              },
              "name": "add"
            },
            "local": {
              "type": "Identifier",
              "start": 9,
              "end": 12,
              "loc": {
                "start": {
                  "line": 1,
                  "column": 9,
                  "index": 9
                },
                "end": {
                  "line": 1,
                  "column": 12,
                  "index": 12
                },
                "identifierName": "add"
              },
              "name": "add"
            }
          }
        ],
        "source": {
          "type": "StringLiteral",
          "start": 20,
          "end": 39,
          "loc": {
            "start": {
              "line": 1,
              "column": 20,
              "index": 20
            },
            "end": {
              "line": 1,
              "column": 39,
              "index": 39
            }
          },
          "extra": {
            "rawValue": "@components/utils",
            "raw": "'@components/utils'"
          },
          "value": "@components/utils"
        },
        "attributes": []
      }
    ],
    "directives": [],
    "extra": {
      "topLevelAwait": false
    }
  },
  "comments": []
}

我们重点关注属性ImportDeclaration,着是声明该语句为导入语句 带入审查一下: 导入函数名:

引用地址:

有了这些数据是不是便轻易就能替换了,这里还其他npm包能帮你加倍完成任务

文件目录🌲如下:

转换文件:

ini 复制代码
import fs from 'fs';
import { parse } from '@babel/parser';
import traverseModule from '@babel/traverse';
import generateModule from '@babel/generator';
import config from './config.js';
// 🔧 兼容 CommonJS 导出
const traverse = traverseModule.default;
const generate = generateModule.default;



function resolveAliasPath(value) {
  console.log('valueLLL', value);
  console.log('config.alias', config.alias);
  for (const key in config.alias) {
    console.log('key:', key);
    if (value.startsWith(key)) {
      console.log('::::', value, config.alias[key]);
      return value.replace(key, config.alias[key]);
    }
  }
  return value;
}

export function rewriteImportsWithAlias(filePath) {
  const rawCode = fs.readFileSync(filePath, 'utf-8');
  console
  const ast = parse(rawCode, {
    sourceType: 'module',
    plugins: ['jsx'], // 如有 ts 改成 ['jsx', 'typescript']
  });

  // 确保 dist 目录存在
  const distDir = './dist';
  if (!fs.existsSync(distDir)) {
    fs.mkdirSync(distDir); 
  }
  //将AST写入文件
  fs.writeFileSync('./dist/ast.json', JSON.stringify(ast, null, 2));

  traverse(ast, {
    ImportDeclaration(path) {
      const source = path.node.source.value;
      path.node.source.value = resolveAliasPath(source);
    },
    CallExpression(path) {
      const callee = path.node.callee;
      if (
        callee.type === 'Identifier' &&
        callee.name === 'require' &&
        path.node.arguments.length === 1 &&
        path.node.arguments[0].type === 'StringLiteral'
      ) {
        const arg = path.node.arguments[0];
        arg.value = resolveAliasPath(arg.value);
      }
    },
  });

  const output = generate(ast, {}, rawCode);
  return output.code;
}

config:

demo内的内容:

运行文件可以得到dist文件夹下的内容:

结尾

大部分工具(如 Vite、Webpack、Rspack)在实现时都采用 路径前缀匹配,也就是:

kotlin 复制代码
for (const aliasKey in aliasMap) {
  if (importPath.startsWith(aliasKey)) {
    // 替换成对应的路径
    return importPath.replace(aliasKey, aliasMap[aliasKey]);
  }
}

👉 所以 不用正则,效率不会差,甚至可以 O(1) 优化成哈希前缀匹配(如果实现上很讲究)。

比如 Webpack 源码中用的是 enhanced-resolve 里的 ResolverPlugin 插件机制,在 resolve 的过程中一层一层挂钩函数去改路径。而 vite 用的 esbuild(或 rollup)本身提供的 resolveId hook 中就有 alias 重写。

相关推荐
Nan_Shu_6149 分钟前
学习:uniapp全栈微信小程序vue3后台(28)
前端·学习·微信小程序·小程序·uni-app
珍宝商店19 分钟前
原生 JavaScript 方法实战指南
开发语言·前端·javascript
蓝莓味的口香糖29 分钟前
【企业微信】VUE项目在企微中自定义转发内容
前端·vue.js·企业微信
IT_陈寒29 分钟前
告别低效!用这5个Python技巧让你的数据处理速度提升300% 🚀
前端·人工智能·后端
—Qeyser31 分钟前
Laravel + UniApp AES加密/解密
前端·uni-app·laravel
C++chaofan34 分钟前
游标查询在对话历史场景下的独特优势
java·前端·javascript·数据库·spring boot
cg.family36 分钟前
Vue3 v-slot 详解与示例
前端·javascript·vue.js
FreeBuf_1 小时前
新型域名前置攻击利用Google Meet、YouTube、Chrome及GCP构建流量隧道
前端·chrome
c0detrend1 小时前
技术架构设计:如何打造一个高性能的Chrome截图插件
前端·chrome
幽络源小助理1 小时前
8、幽络源微服务项目实战:前端登录跨域同源策略处理+axios封装+权限的递归查询增删改+鉴权测试
前端·微服务·架构