本文基于 vite 和 pnpm workspace 的 monorepo 架构跟大家一起重识 alias 的原理和用法!(ps:为方便大家自己研究下案例代码的执行,完整代码已经上传 github,有需要的朋友可以自行 clone 下来玩玩。
前言
说起 alias 大家肯定不陌生,毕竟几乎在任何一个现代前端工程的项目中都有它的影子。常用的打包工具 webpack 和 vite 都有 alias 的配置,并且当我们使用脚手架生成一些基础项目的时候,大家都会默契地把 @ 通过 alias 配置指向了工程中的 src 目录。
比如这样,我们配置了 @ 指向当前目录下的 src:
js
alias:[
{
find: '@',
replacement: path.resolve('src'),
},
]
我们在应用中便可以通过 @/xxx 来便捷的导入 src 下的任何模块。比如一个文件中的 import 如下:

当配置了 alias 后,我们可以改写成 import '@/App.css' 这样写法。从某种程度上来说,alias 别名代替一些相对、绝对路径的写法,在开发阶段给我们提供便利。
我举一个很常见的例子,我们开发过程中经常会因为一些调整来变更文件的位置 ,那此时如果我使用相对路径来写模块引用的话,当我的文件位置变更的时候,往往需要同步修改我的模块导入路径。比如当我的文件提升了一个层级的时候,我的相对路径 import '../App.css' 写法可能就要改成 import './App.css' 这样了。但是如果我是通过 '@/App.css' 的方式来引用,只要 App.css 的位置不变,导入其的文件无论换到哪里都不需要变更这个导入的路径。
好吧,相信这个用法大家都很熟悉了!但不知道大家有没有了解过它背后是怎么运作的呢?我们接着往下看。
alias 原理
我们看 vite 文档 对其的介绍可知,它的底层实现是一个 rollup 插件:@rollup/plugin-alias。当我们点进去这个插件的 源码 大概看了看,有写过 vite 或者 rollup 插件的同学可能就发现了两个熟悉的钩子:

buildStart 和 resolveId。这两个钩子我们在 rollup 的插件文档中可以直接看到:

当然我们也同样在 vite 的插件文档中找到相关钩子的说明:

只要我们简单看看 @rollup/plugin-alias 的源码,大概就能猜到 alias 的底层实现原理了,如下图:

dev 和 build
基于上述的一些文档说明和源码阅读,我们大概知道了 alias 的实现原理是基于两点:
- 别名规则的匹配
- 路径替换
基于此我们可以简单拓展下,大家都知道 vite 的 dev mode 下是不打包文件的,通过直接启动开发服务器并借助浏览器原生支持的 import 能力进行模块的按需导入。那我们大概可以猜测到 dev 和 build 模式下的 alias 表象是有区别的。
比如我有一个 AliasTest 的组件通过 @ 的方式在入口组件中导入:
js
const AliasTest = lazy(() => import('@/components/alias-test'));
当我运行 dev 的时候在浏览器下可以看到对应的路径截图如下:

有经验的都知道,此时我们的源文件的导入路径是不会变化的(也就是说不修改源文件),所以我们可以认为这是 dev 服务器的实现。紧接着我们对这个 react-demo 的单页应用进行打包,再看看 @ 这个别名的变化情况。(我特意使用了 lazy 引入,就为了分包出来看得清晰点)
产物结果如下,此时的 AliasTest 组件被打包成了 index-BsXGEvj6.js 文件:

此时我们再回到入口文件中看看我们源代码写的 '@/components/alias-test' 会变成什么?

确实已经正确转换为指向打包后的文件的相对路径了(当然源文件也随之被修改)。
巧妙的用途
除了我们项目中常见的通过配置 alias 实现 @ 指向 src 目录,这个配置还有没有其他妙用呢?答案是有的,比如我们常做 npm 包开发的同学应该就会用到。
这里我还是回到同一个 react 的 monorepo 工程中进行讲解。比如我要开发一个 npm 包:react-bundle,并且我通过 react-demo 这个单页应用的项目作为代码调试的工程,它提供一个 vite dev 的环境能让我们很方便的开发调试 react-bundle 的 js包:

回顾上一篇文章------从 Monorepo 重温 ESM 的模块化机制中提到的模块解析规则,package.json 中的入口字段决定着 node 的模块解析如何找到最终对应的文件,所以当我们需要在 react-demo 中启动 dev 能调试到 react-bundle 时,我们必须要正确地安装好依赖并且 react-bundle 的入口配置正确。
此时我们看看 react-bundle 中的 package.json 配置是怎么指向入口的:
json
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/react-bundle/index.d.ts"
}
},
它的入口配置为 dist/index.js,也就是说我们真正在 react-demo 中调试的文件是一个打包后的文件:

那这样会有一个怎么样的问题?是不是意味着我们每在 react-bundle 中该一行代码,都需要重新 build 一次才能生效。这种情况对于 npm 包的开发者来说是不能容忍的,因为效率真的太低了。并且我们可能会因为忘记 build 了而发现自己的改动迟迟没有生效从而浪费时间去排查。那此时怎么办呢?
聪明的朋友一定想到了,那直接改掉 package.json 中的入口指向不就好了吗?比如我修改了一下 react-bundle 的 pkj 的入口指向。比如这里我把指向从 ./dist/index.js 改为了项目源码的入口文件: ./index.ts
json
"exports": {
".": {
"import": "./index.ts",
"types": "./dist/react-bundle/index.d.ts"
}
},
此时在 react-bundle 的每一行修改都实时反馈到 react-demo 的 dev 调试 web 中了,让我们这些 npm 包开发者感受到了无比的愉悦。比如我修改了一下 react-bundle 中的代码:

实时调试效果: 
看似一切美好的解决方案,但当我们要把 npm包 发布到 npm 仓库并被用户安装到自己的项目时,问题就出现了。这里大家可以自己想一想问题是什么?但是如果说你的 npm 包本意就是提供源码文件给其他用户使用,那这样确实没毛病。
大多数情况下,npm 包的开发者都会提供"开箱即用"的代码包,他不需要要求用户的项目装什么 ts,用什么打包,反正他就提供一个 js 文件,只要是运行在有 js 引擎的环境中都能正常运行的产物。另外随着前端工程的不断发展,各种 npm包还会提供各式各样的产物模式,如 umd、iife、es module 规范、commonjs 规范等产物。
所以从大部分场景看,我们开发 npm包的 package.json 入口一般都是指向打包后的入口文件的,如果说我们开发的时候改了对应的入口指向,保不齐我们发包的时候没有改回来就会导致用这个 npm 包的同学出问题。当然有同学说可以多搞几个 package.json 不就好了?但此时我想说的是,我们可以配置 alias 来解决这个问题。
比如此时我们的 package.json 的指向依然是打包后的地址,但是我们给调试用的应用配置了 alias 指向我们的 react-bundle 的源码文件。此时我们依然可以实时调试我们的 react-bundle,但是又不用担心发包后的入口配置不对从而引用其他开发者了。

于是乎,alias 在这里的妙用可以极大地降低我们 npm 包开发时的心智负担,我们可以配置好 alias 并借助 monorepo 的代码组织方式很丝滑地实时开发调试我们的 npm 包,又不用担心会影响到使用该 npm包 的用户们。
总结
配置别名 alias 的场景不仅仅是我们每个单页应用中的 src 映射,还可以应用到 npm 包的开发调试场景。当然,alias 的玩法肯定不局限在这两种场景,本文更多是借 npm包 开发调试妙用 alias 的引子让大家更多地 get 到这个配置的作用。相信本文可以拓宽我们对 alias 的理解,并拓宽我们在一些业务场景中解决问题的解题思路!