likes
comments
collection
share

如何在TS Compiler中使用 alias

作者站长头像
站长
· 阅读数 43

问题的开始

在项目开发中使用alias是很常见的需求,当你使用tsc编译的时候就会发现并不是那么简单。

前提:使用 tsc 编译

在使用别名的时候首先需要在tsconfig.json中配置paths字段

paths字段是用来设置模块名和文件映射关系的。[paths是基于baseUrl来进行加载的,所以也必须设置baseUrl]

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@src/*": ["src/*"],
    },
  }
}

设置完成后并且使用别名,是可以正常导入的,路径解析也没有什么问题。

如何在TS Compiler中使用 alias 接下来使用tsc编译,运行:会发现 cannot find pacakge 报错

如何在TS Compiler中使用 alias

问题出现的原因

我们去看一下编译产物 如何在TS Compiler中使用 alias

tsc在编译的时候,并没有将 @src/编译成我们想要的路径。

tsc 只会对代码进行 transformer,并不会对代码进行除此之外的任何转换。

如何解决?

发现了两种主流的解决思路

通过AST修改源代码

ts compile 整个流程中并没有直接暴露 api 钩子函数等可以让我们直接修改语法树,需要借助第三方的编译方式

ttypescript

使用 ttypescript + typescript-transform-paths

通过修改node的require解析策略

通过修改 Module 的解析方式,将路径替换为正确alias后的路径

tsconfig-pathmodule-alias这类库

tsconfig-path:

  // Patch node's module loading
  const Module = require("module");
  const originalResolveFilename = Module._resolveFilename;
  const coreModules = getCoreModules(Module.builtinModules);
  Module._resolveFilename = function (request: string, _parent: any): string {
    const isCoreModule = coreModules.hasOwnProperty(request);
    if (!isCoreModule) {
      // 获取自定义的 tsconfig 中 paths 的内容
      const found = matchPath(request);
      if (found) {
        const modifiedArguments = [found, ...[].slice.call(arguments, 1)]; // Passes all arguments. Even those that is not specified above.
        return originalResolveFilename.apply(this, modifiedArguments);
      }
    }
    return originalResolveFilename.apply(this, arguments);
  };

module-alias

var oldResolveFilename = Module._resolveFilename
Module._resolveFilename = function (request, parentModule, isMain, options) {
  for (var i = moduleAliasNames.length; i-- > 0;) {
    var alias = moduleAliasNames[i]
    if (isPathMatchesAlias(request, alias)) {
      var aliasTarget = moduleAliases[alias]
      // Custom function handler
      if (typeof moduleAliases[alias] === 'function') {
        var fromPath = parentModule.filename
        aliasTarget = moduleAliases[alias](fromPath, request, alias)
        if (!aliasTarget || typeof aliasTarget !== 'string') {
          throw new Error('[module-alias] Expecting custom handler function to return path.')
        }
      }
      request = nodePath.join(aliasTarget, request.substr(alias.length))
      // Only use the first match
      break
    }
  }

  return oldResolveFilename.call(this, request, parentModule, isMain, options)
}

追根溯源

为什么 ts comiple 中不做类似 alias 这类转换

Module path maps are not resolved in emitted code

ts 核心成员说 paths 和 baseUrl 只是在运行时告诉 complier 如何去解析,compiler 不会去重写模块名。

如何在TS Compiler中使用 alias

在 paths 的解释中也有提到:github.com/dividab/tsc…

如何在TS Compiler中使用 alias

Path mapping

如何在TS Compiler中使用 alias

ts paths 只是为了解决在运行时的路径映射问题,它不会改变compile之后的问题。

最后讨论总结:你说的对,但是我们不改。(作者的立场是:你们说的也不对)

如何在TS Compiler中使用 alias

一个讨论者的话,没啥含义,就是觉得说的挺对

Lots of people feeling a behaviour is correct, doesn't mean it is the right thing to do.

总体来说,TS 是为了解决开发环境时的开发体验的,而不是做编译器。这不是 ts 所擅长的,专业的人做专业的事,打包就该交给专业的构建工具去做。

所以这个issuse一开始ts的核心成员就在问有没有用构建工具

Do you use some other bundling tool like browserify or webpack on the generated output? or do you expect this to run directly on node?

延伸探究

Module 的解析方式是什么?

Commonjs

require 一个模块的流程

  1. 如果cache里面存了,直接返回exports对象

  2. 如果是一个内部的模块,调用 BuiltinModule.prototype.compileForPublicLoader() 并且返回export对象

  3. 否则创建一个Module,存入cache。加载文件返回 export 对象

lib/internal/modules/run_main.js
// Module._load is the monkey-patchable CJS module loader.
Module._load(main, null, true);
Module._load = function(request, parent, isMain) {
  let relResolveCacheIdentifier;
  // 如果有父级 Module,A中有BCD模块,B中有EF模块,A和B都属于父级模块
  if (parent) {
    debug('Module._load REQUEST %s parent: %s', request, parent.id);
    // Fast path for (lazy loaded) modules in the same directory. The indirect
    // caching is required to allow cache invalidation without changing the old
    // cache key names.
    // \x00 是为了避免混淆做的特殊字符拼接,表示空字符,不可见也不会被打印
    relResolveCacheIdentifier = `${parent.path}\x00${request}`;
    // relativeResolveCache 初始值是空对象
    const filename = relativeResolveCache[relResolveCacheIdentifier];
    reportModuleToWatchMode(filename);
    if (filename !== undefined) {
      const cachedModule = Module._cache[filename];
      if (cachedModule !== undefined) {
      // 更下 paraent children ?
        updateChildren(parent, cachedModule, true);
        // 如果模块在cache中了,但是并没有被加载完成,说明发生了循环依赖
        // 此时函数会返回已缓存的模块导出,而不继续执行,并且抛出warning
        if (!cachedModule.loaded)
          return getExportsForCircularRequire(cachedModule);
          // 返回cache中的模块
        return cachedModule.exports;
      }
      // 之前的缓存出错 删除缓存的key
      delete relativeResolveCache[relResolveCacheIdentifier];
    }
  }
  
  // 如果是内部模块,直接返回 module.exports
  if (StringPrototypeStartsWith(request, 'node:')) {
    // Slice 'node:' prefix
    const id = StringPrototypeSlice(request, 5);
    // 提供了所有内部模块的list 以及对应的映射关系
    const module = loadBuiltinModule(id, request);
    if (!module?.canBeRequiredByUsers) {
      throw new ERR_UNKNOWN_BUILTIN_MODULE(request);
    }

    return module.exports;
  }
  
  // 读取文件内容以及查看是否有缓存
  // 1. 如果有缓存
  // 1.1 查看缓存是否加载完成,加载完成直接返回缓存的export
  // 1.2 如果缓存没有加载完成,判断是否是循环依赖,是则返回,否则加载
  const filename = Module._resolveFilename(request, parent, isMain);
  const cachedModule = Module._cache[filename];
  if (cachedModule !== undefined) {
    updateChildren(parent, cachedModule, true);
    if (!cachedModule.loaded) {
    // 没看懂,去 cjsParseCache 是在 esm的translators方法中set的
      const parseCachedModule = cjsParseCache.get(cachedModule);
      if (!parseCachedModule || parseCachedModule.loaded)
        return getExportsForCircularRequire(cachedModule);
      parseCachedModule.loaded = true;
    } else {
      return cachedModule.exports;
    }
  }
  // 如果是内部模块(不带 node: )
  const mod = loadBuiltinModule(filename, request);
  if (mod?.canBeRequiredByUsers &&
      BuiltinModule.canBeRequiredWithoutScheme(filename)) {
    return mod.exports;
  }
  // 之前没有缓存过,新建一个module
  // Don't call updateChildren(), Module constructor already does.
  const module = cachedModule || new Module(filename, parent);

  if (isMain) {
    process.mainModule = module;
    setOwnProperty(module.require, 'main', process.mainModule);
    module.id = '.';
  }

  reportModuleToWatchMode(filename);
  // 在加载之前先把他加入缓存,避免循环依赖
  Module._cache[filename] = module;
  // 如果有父级Module则把当前导入加到相对引用缓存中,提前处理
  if (parent !== undefined) {
    relativeResolveCache[relResolveCacheIdentifier] = filename;
  }

  let threw = true;
  try {
    // 加载模块
    module.load(filename);
    threw = false;
  } finally {
    if (threw) {
      // 加载失败。删除之前存入的Module缓存 
      delete Module._cache[filename];
      if (parent !== undefined) {
          // 加载失败。删除之前存入父级模块缓存的缓存 
        delete relativeResolveCache[relResolveCacheIdentifier];
        // 删除父级模块当前加入的该模块的
        const children = parent?.children;
        if (ArrayIsArray(children)) {
          const index = ArrayPrototypeIndexOf(children, module);
          if (index !== -1) {
            ArrayPrototypeSplice(children, index, 1);
          }
        }
      }
    } else if (module.exports &&
               !isProxy(module.exports) &&
               ObjectGetPrototypeOf(module.exports) ===
                 CircularRequirePrototypeWarningProxy) {
      ObjectSetPrototypeOf(module.exports, ObjectPrototype);
    }
  }

  return module.exports;
};
// 解析文件 ‘index.js’ null true
Module._resolveFilename = function(request, parent, isMain, options) {
  // 如果是内部模块直接返回
  if (
    (
      StringPrototypeStartsWith(request, 'node:') &&
      BuiltinModule.canBeRequiredByUsers(StringPrototypeSlice(request, 5))
    ) || (
      BuiltinModule.canBeRequiredByUsers(request) &&
      BuiltinModule.canBeRequiredWithoutScheme(request)
    )
  ) {
    return request;
  }

  let paths;
  // 这一块都是在处理路径 node_module、./ ../ 等相对路径
  if (typeof options === 'object' && options !== null) {
    if (ArrayIsArray(options.paths)) {
      const isRelative = StringPrototypeStartsWith(request, './') ||
          StringPrototypeStartsWith(request, '../') ||
          ((isWindows && StringPrototypeStartsWith(request, '.\')) ||
          StringPrototypeStartsWith(request, '..\'));

      if (isRelative) {
        paths = options.paths;
      } else {
        const fakeParent = new Module('', null);

        paths = [];

        for (let i = 0; i < options.paths.length; i++) {
          const path = options.paths[i];
          fakeParent.paths = Module._nodeModulePaths(path);
          // 查找路径 node_module、绝对路径等
          const lookupPaths = Module._resolveLookupPaths(request, fakeParent);

          for (let j = 0; j < lookupPaths.length; j++) {
            if (!ArrayPrototypeIncludes(paths, lookupPaths[j]))
              ArrayPrototypePush(paths, lookupPaths[j]);
          }
        }
      }
    } else if (options.paths === undefined) {
      paths = Module._resolveLookupPaths(request, parent);
    } else {
      throw new ERR_INVALID_ARG_VALUE('options.paths', options.paths);
    }
  } else {
    // 查找路径 
    paths = Module._resolveLookupPaths(request, parent);
  }

  if (request[0] === '#' && (parent?.filename || parent?.id === '<repl>')) {
    const parentPath = parent?.filename ?? process.cwd() + path.sep;
    const pkg = readPackageScope(parentPath) || {};
    if (pkg.data?.imports != null) {
      try {
        return finalizeEsmResolution(
          packageImportsResolve(request, pathToFileURL(parentPath),
                                cjsConditions), parentPath, request,
          pkg.path);
      } catch (e) {
        if (e.code === 'ERR_MODULE_NOT_FOUND')
          throw createEsmNotFoundErr(request);
        throw e;
      }
    }
  }

  // Try module self resolution first 
  // 处理package.json中的exports字段(可能是引用库的package.json)
  const parentPath = trySelfParentPath(parent);
  const selfResolved = trySelf(parentPath, request);
  if (selfResolved) {
    const cacheKey = request + '\x00' +
         (paths.length === 1 ? paths[0] : ArrayPrototypeJoin(paths, '\x00'));
    Module._pathCache[cacheKey] = selfResolved;
    return selfResolved;
  }

  // Look up the filename first, since that's the cache key.
  const filename = Module._findPath(request, paths, isMain, false);
  if (filename) return filename;
  const requireStack = [];
  for (let cursor = parent;
    cursor;
    cursor = moduleParentCache.get(cursor)) {
    ArrayPrototypePush(requireStack, cursor.filename || cursor.id);
  }
  let message = `Cannot find module '${request}'`;
  if (requireStack.length > 0) {
    message = message + '\nRequire stack:\n- ' +
              ArrayPrototypeJoin(requireStack, '\n- ');
  }
  // eslint-disable-next-line no-restricted-syntax
  const err = new Error(message);
  err.code = 'MODULE_NOT_FOUND';
  err.requireStack = requireStack;
  throw err;
};

relativeResolveCache 和 Module._cache有什么区别?

relativeResolveCache 是一个局部变量,作用范围仅限于当前的模块解析过程,用于提高模块解析的性能和效率。

Module._cache 是全局的缓存对象,存储已加载的模块缓存。

循环依赖如何解决的?

通过先读cache,以及只要创建了模块就先将模块加到cache里,即使这个模块还没有完全加载完成,自然而然就解决了循环依赖

转载自:https://juejin.cn/post/7282890157254459426
评论
请登录