模块别名(Module Alias)或者叫路径重映射(Remapping)是前端的模块化进程中很常见的一个需求,由来已久,无论是之于 AMD 规范的 RequireJS,还是之于 ES Module 规范的 Import Maps 提案,都在解决该问题。
RequireJS
RequireJS 的 paths
主要用于当某个依赖加载失败后进行回退。
requirejs.config({
//To get timely, correct error triggers in IE, force a define/shim exports check.
enforceDefine: true,
paths: {
jquery: [
'http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min',
//If the CDN location fails, load from this location
'lib/jquery'
]
}
});
//Later
require(['jquery'], function ($) {});
Sea.js
Sea.js 已经退出历史舞台,相比 RequireJS,它的配置更为丰富。
seajs.config({
// 设置路径,方便跨目录调用
paths: {
arale: 'https://a.alipayobjects.com/arale',
jquery: 'https://a.alipayobjects.com/jquery'
},
// 设置别名,方便调用
alias: {
class: 'arale/class/1.0.0/class',
jquery: 'jquery/jquery/1.10.1/jquery'
}
});
Node.js
除了客户端,服务端 JS 也有使用模块别名的需求,所以出现了 module-alias 之类的 npm 包,通过修改 Node 内部的 Module._resolveFilename()
、Module._nodeModulePaths()
方法实现模块别名。
// Node 项目入口
require('module-alias/register');
// package.json
{
"_moduleAliases": {
"@root": ".", // Application's root
"@deep": "src/some/very/deep/directory/or/file",
"@my_module": "lib/some-file.js",
"something": "src/foo" // Or without @. Actually, it could be any string
}
}
这样可以将:
require('../../../../some/very/deep/module');
简写成:
var module = require('@deep/module');
webpack resolve.alias
webpack 的模块解析也支持配置别名,参见 Resolve。
// webpack.config.js
const path = require('path');
module.exports = {
//...
resolve: {
alias: {
Utilities: path.resolve(__dirname, 'src/utilities/'),
Templates: path.resolve(__dirname, 'src/templates/')
}
}
};
import Utility from '../../utilities/utility';
可以被简化为:
import Utility from 'Utilities/utility';
Rollup @rollup/plugin-alias
Rollup 也支持通过插件 @rollup/plugin-alias 实现别名功能。
// rollup.config.js
import alias from '@rollup/plugin-alias';
module.exports = {
input: 'src/index.js',
output: {
dir: 'output',
format: 'cjs'
},
plugins: [
alias({
entries: [
{ find: 'utils', replacement: '../../../utils' },
{ find: 'batman-1.0.0', replacement: './joker-1.5.0' }
]
})
]
};
Babel
除了在模块加载器(Loader)和打包器(Bundler)上做文章,还可以在语法转译层面实现。
比如 babel-plugin-module-alias 插件(现已更名为 babel-plugin-module-resolver)。允许编写的时候使用路径别名,编译后处理为完整路径。
// .babelrc.json
{
"plugins": [
[
"module-resolver",
{
"root": ["./src"],
"alias": {
"test": "./test",
"underscore": "lodash"
}
}
]
]
}
或者使用 babel-plugin-root-import。
// .babelrc.json
{
"plugins": [
[
"babel-plugin-root-import",
{
"rootPathPrefix": "@"
}
]
]
}
TypeScript
TypeScript 支持 Path Mapping,通过配置 baseUrl 和 paths 实现模块别名。
以下是官方文档的一些例子:
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".", // This must be specified if "paths" is.
"paths": {
"jquery": ["node_modules/jquery/dist/jquery"] // This mapping is relative to "baseUrl"
}
}
}
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"*": ["*", "generated/*"]
}
}
}
// tsconfig.json
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"app/*": ["app/*"],
"config/*": ["app/_config/*"],
"environment/*": ["environments/*"],
"shared/*": ["app/_shared/*"],
"helpers/*": ["helpers/*"],
"tests/*": ["tests/*"]
},
}
需要注意 TypeScript 的编译器 tsc 编译产出的 JS 文件,依然保留原始的模块路径,并不会替换为映射后的路径。
对此,社区实现了自定义 transformer 来处理 paths
配置,只不过需要依赖 ts-loader 等工具,或者使用支持 transformer 的 TypeScript 版本 ttypescript。
ts-transform-paths
ts-transform-paths 是常见的 transformer,可以在 tsc 编译时替换映射路径,需要配合 webpack
/rollup
/ttypescript
/ts-node
使用。
// webpack.config.js
const pathsTransformer = require('ts-transform-paths').default;
module.exports = {
// ...
module: {
rules: [
{
test: /\.ts$/,
loader: 'ts-loader',
options: {
getCustomTransformers: program => pathsTransformer()
}
}
]
}
};
react-native-typescript-transformer
react-native-typescript-transformer 的使用场景是 React Native,多亏了 React Native CLI 底层的 Metro 打包器支持自定义 transformer。
// metro.config.js
module.exports = {
transformer: {
babelTransformerPath: require.resolve('react-native-typescript-transformer')
}
};
其他解决方案
Gulp gulp-ts-alias
在 Gulp 中使用 gulp-typescript 时,可以通过 gulp-ts-alias 实现别名。
// gulpfile.js
const typescript = require('gulp-typescript');
const sourcemaps = require('gulp-sourcemaps');
const alias = require('gulp-ts-alias');
const project = typescript.createProject('tsconfig.json');
function build() {
const compiled = src('./src/**/*.ts')
.pipe(alias({ configuration: project.config }))
.pipe(sourcemaps.init())
.pipe(project());
return compiled.js
.pipe(
sourcemaps.write({
sourceRoot: file =>
path.relative(path.join(file.cwd, file.path), file.base)
})
)
.pipe(dest('build/'));
}
tscpaths
tscpaths 可以在 tsc 编译时替换映射路径。
tsc --project tsconfig.json && tscpaths -p tsconfig.json -s ./src -o ./out
tsc-alias
tsc-alias Fork 自 tscpaths
,也是在 tsc 编译时替换映射路径。
tsc --project tsconfig.json && tsc-alias -p tsconfig.json
TypeScript + Node.js
tsconfig-paths
tsconfig-paths 可以让 Node(以及 ts-node
)执行代码时,识别 tsconfig.json
中的 paths
配置。原理同 module-alias
。
npm i -D tsconfig-paths
ts-node -r tsconfig-paths/register src/index.ts
tsconfig-paths-webpack-plugin
tsconfig-paths-webpack-plugin 集成 tsconfig-paths
功能的 webpack 插件,可以省去配置 resolve.alias
。
// webpack.config.js
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
module.exports = {
//...
resolve: {
plugins: [new TsconfigPathsPlugin({})]
}
};
ESLint
ESLint 默认不识别 webpack 或者 tsconfig.json
配置的别名,需要借助社区插件:
npm i -D eslint
npm i -D eslint-plugin-import
npm i -D eslint-import-resolver-typescript
eslint --ext .js,.ts src
// .eslintrc.js
/** @type {import('eslint').Linter.Config} */
module.exports = {
parser: '@typescript-eslint/parser',
env: {
browser: true,
node: true
},
extends: [
'eslint:recommended',
'plugin:import/recommended',
'plugin:import/typescript'
],
settings: {
'import/resolver': {
// 配置 `eslint-import-resolver-typescript`
typescript: {
project: '.'
}
}
}
};
Import maps
目前 ES Module 尚不支持模块别名,但已经有相关提案。
<script type="importmap">
{
"imports": {
"moment": "/node_modules/moment/src/moment.js",
"lodash": "/node_modules/lodash-es/lodash.js"
}
}
</script>
借助 Import maps 定义,如下代码:
import moment from 'moment';
import { partition } from 'lodash';
相当于:
import moment from '/node_modules/moment/src/moment.js';
import { partition } from '/node_modules/lodash-es/lodash.js';
补充
webpack CSS 相关的 loader 支持以 ~
开头表示 node_modules
,如 @import '~antd/dist/antd.css'
。新版已经废弃这种写法,不需要加 ~
,默认会尝试解析 node_modules
目录。
Bash 中,~
表示用户 HOME 目录。
相关链接:
- Webpack Resolver - less-loader
- Resolving
import
at-rules - sass-loader - Support absolute imports with
@
- Create React App - Using webpack aliases - VSCode
总结
使用哪种模块别名的实现,取决于实际使用场景,无论是单纯地使用 Babel 或者 tsc 做语法转译,还是使用 webpack、Rollup 打包,或者后期加载模块,都有对应的解决方案。
练习
思考一下以下几种路径别名的区别:
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@components": ["src/components"],
},
}
对应使用场景:
import Tabs from '@/components/tabs'; // 匹配 '@/*',映射为 'src/components/tabs'
import Button from '@components/button'; // 匹配 '@components/*',映射为 'src/components/button'
import { Table } from '@components'; // 匹配 '@components',映射为 'src/components'