Metro 配置项

0 阅读11分钟

1. cacheStores[CacheStores]

metro 缓存。默认 FileStore 在 os.tmpdir() 目录下。

2. cacheVersion[string]

缓存的版本,默认 1.0.0 。

3. projectRoot[string]

项目的根文件夹,默认就是当前项目的绝对路径。如果项目依赖此项目外的其他文件,那么需要在 watchFolds 中列出这个文件的所在目录。

4. watchFolders[Array<string>]

如果项目使用了项目 (projectRoot) 之外的文件,那么需要使用这个包含文件所在的文件夹,如果不包含会出现找不到 module 的错误。

假如你的结构是这样的:

├── metroLearn
│   ├── App.tsx
│   ├── Gemfile
│   ├── README.md
│   ├── __tests__
│   ├── android
│   ├── app.json
│   ├── babel.config.js
│   ├── index.js
│   ├── ios
│   ├── jest.config.js
│   ├── metro.config.js
│   ├── node_modules
│   ├── package-lock.json
│   ├── package.json
│   ├── test.js
│   └── tsconfig.json
└── modules
    └── test-module

其中你的 App.tsx 依赖于项目外的 modules/test-module ,你的 App.tsx 导入是这样的:

import test from '../modules/test-module'

如果直接这样使用会出现报错:

Error: Unable to resolve module ../modules/test-module from D:\Learns\Qianduan\react-native\metroLearn\App.tsx:

None of these files exist:
  * ..\modules\test-module(.android.js|.native.js|.js|.android.jsx|.native.jsx|.jsx|.android.json|.native.json|.json|.android.ts|.native.ts|.ts|.android.tsx|.native.tsx|.tsx)
  * ..\modules\test-module
  1 | import { Pressable, Text, View } from 'react-native';
  2 | import { SafeAreaView } from 'react-native-safe-area-context';
> 3 | import test from '../modules/test-module'
    |                   ^
  4 |
  5 | const App = () => {
  6 |   return (
    at ModuleResolver.resolveDependency (D:\Learns\Qianduan\react-native\metroLearn\node_modules\metro\src\node-haste\DependencyGraph\ModuleResolution.js:142:15)
    at DependencyGraph.resolveDependency (D:\Learns\Qianduan\react-native\metroLearn\node_modules\metro\src\node-haste\DependencyGraph.js:252:43)

只有当你将这个目录添加到 watchFolders 中才不会出现报错。

const config = {
  watchFolders: [path.join(__dirname, "..", "modules")]
};

5. transformerPath[string]

转换器的绝对路径,默认使用 metro-transform-worker

6. reporter[{update: (event: ReportableEvent) => void}]

打包过程中报告打包器的一个状态,默认大部分都已经打印到终端了。

7. resetCache[boolean]

是否重置缓存。

8. stickyWorkers[boolean]

默认为 true 。如果为 true,Metro 将使用从文件到 transformer worker 的稳定映射,确保同一文件始终由同一个 worker 转换。如果 transformer 初始化成本高昂,这可以提升初始构建性能,但可能会降低使用不同配置的并发构建速度(例如多个 React Native 应用连接到同一个 Metro 服务器)。

9. maxWorkers[number]

并行处理工作的数量,默认为可用核心数的一半,由 os.availableParallelism()确定。

10. fileMapCacheDirectory[string]

缓存目录的路径,默认为 os.tmpdir()

11. unstable_perfLoggerFactory[PerfLoggerFactory]

12. resolver

解析器,用于构建依赖图。

12.1. assetExts[Array<string>]

静态资源的后缀集合。默认支持的静态资源后缀:

[
  'bmp',  'gif',  'jpg',  'jpeg',
  'png',  'psd',  'svg',  'webp',
  'xml',  'm4v',  'mov',  'mp4',
  'mpeg', 'mpg',  'webm', 'aac',
  'aiff', 'caf',  'm4a',  'mp3',
  'wav',  'html', 'pdf',  'yaml',
  'yml',  'otf',  'ttf',  'zip'
]

静态资源会被打包成下面的格式:

__d(function (global, _$$_REQUIRE, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, _dependencyMap) {
  module.exports = _$$_REQUIRE(_dependencyMap[0], "react-native/Libraries/Image/AssetRegistry").registerAsset({
    "__packager_asset": true,
    "httpServerLocation": "/assets",
    "width": 512,
    "height": 512,
    "scales": [1],
    "hash": "4923e65be82e0dc6fd9bb95957221415",
    "name": "songyu",
    "type": "jpg"
  });
},611,[353],"songyu.jpg");

这些只是实际图片的索引,在开发模式下直接请求图片资源,通过 http://localhost:8081/assets/[图片路径]就能拿到对应的文件。静态资源需要对应的组件支持才能正确解析,js 层只是负责建立索引,这些文件最终会放到原生的静态资源文件夹中。

我在项目中使用了图片和 pdf 文件,我通过下面的命令打包 bundle 。

npx @react-native-community/cli bundle --entry-file index.js --platform android --bundle-output index.android,bundle --assets-dest assets

生成的结果为:

├── assets
│   ├── drawable-mdpi
│   │   ├── node_modules_reactnative_libraries_logbox_ui_logboximages_alerttriangle.png
│   │   ├── node_modules_reactnative_libraries_logbox_ui_logboximages_chevronleft.png
│   │   ├── node_modules_reactnative_libraries_logbox_ui_logboximages_chevronright.png
│   │   ├── node_modules_reactnative_libraries_logbox_ui_logboximages_close.png
│   │   ├── node_modules_reactnative_libraries_logbox_ui_logboximages_loader.png
│   │   └── songyu.jpg
│   └── raw
│       ├── keep.xml
│       └── wujingjianli.pdf

其中 songyu.jpgwujingjianli.pdf就是我的静态资源文件。

12.2. sourceExts[Array<string>]

源代码文件扩展,比如项目要想支持 ts ,那么就需要添加到这个列表中,当然这个默认已经添加了的。默认: ['js', 'jsx', 'ts', 'tsx', 'json']

12.3. resolverMainFields[Array<string>]

resolver 解析器解析包的时候会寻找入口文件,这个就是定义找哪个字段的,比如包一般都是 main 字段,这个字段定义了哪个文件是这个包的入口文件,但是在 react-native中寻找的循序是这样的 ['react-native', 'browser', 'main']

12.4. disableHierarchicalLookup[boolean]

是否禁用在 node_modules 文件夹中的逐级向上查找。

这个选项只影响默认的“沿目录树向上查找模块”的行为,
不会影响其他 Metro 配置,比如 extraNodeModulesnodeModulesPaths

默认值:false

当我设置成 true 后会立马出现报错:

ERROR  Error: Unable to resolve module @babel/runtime/helpers/interopRequireDefault from D:\Learns\Qianduan\react-native\metroLearn\index.js: @babel/runtime/helpers/interopRequireDefault could not be found within the project.
> 1 | /**
  2 |  * @format
  3 |  */
  4 |
    at ModuleResolver.resolveDependency (D:\Learns\Qianduan\react-native\metroLearn\node_modules\metro\src\node-haste\DependencyGraph\ModuleResolution.js:178:15)
    at DependencyGraph.resolveDependency (D:\Learns\Qianduan\react-native\metroLearn\node_modules\metro\src\node-haste\DependencyGraph.js:252:43)
    at D:\Learns\Qianduan\react-native\metroLearn\node_modules\metro\src\lib\transformHelpers.js:165:21
    at resolveDependencies (D:\Learns\Qianduan\react-native\metroLearn\node_modules\metro\src\DeltaBundler\buildSubgraph.js:43:25)
    at visit (D:\Learns\Qianduan\react-native\metroLearn\node_modules\metro\src\DeltaBundler\buildSubgraph.js:81:30)

而且禁用其他包也找不到,如果 node_modules只存在当前项目下,那么可以设置 nodeModulesPath来解决,只不过一般不要禁用这个选项。

{
  nodeModulesPath: [path.join(__dirname, 'node_modules')]
}

12.5. emptyModulePath[string]

当需要一个“空模块”时,使用哪个模块作为标准实现。

默认使用 metro-runtime 内置的空模块。

只有当 Metro 安装在你的项目之外时,你才需要修改这个配置。

12.6. enableGlobalPackages[boolean]

是否自动解析项目中的“第一方包”(例如 workspace 包)。

任何位于 projectRootwatchFolders 中、且不在 node_modules 、并且包含合法 name 字段的 package.json,都会被视为一个 package。

默认值:false

假如你在项目中编写了一个 module ,但是这个 module 并没有在 node_modules 中,默认情况下 metro 并不会把这个当着 module,会报错。而如果你把这个选项设置成了 true ,那么只要发现你的某个文件夹下有 package.json 并且存在合法的 name 字段就会自动解析成 module 。

我们在开发过程中可能会自己开发一些包给自己的项目使用,然后通过 link 的方式加载项目中,默认情况下 metro 并不认识这些包,即便你将这个 module 添加到 watchFolders 选项中,这个只是监听这里面配置文件夹下的文件,并不会当着 module 处理。

12.7. extraNodeModules[{[string]: string}]

类型:{[string]: string}(对象,key 是包名,value 是路径)

一个“包名 → 目录路径”的映射表。

在完成标准的 node_modules 查找以及 nodeModulesPaths 查找之后,
会使用这个映射来解析模块。

假如你有一个 module 并没有在 node_modules 和你指定的 nodeModulesPaths 中,如果你没有配置这个选项,默认是找不到的,当然前提是你也没有设置 enableGlobalPackages 为 true 。这种情况下你可以配置这个来解决找不到 module 的情况。

12.8. nodeModulesPaths[Array<string>]

在完成所有 node_modules 目录查找之后,还会额外检查的一组路径列表。

当第三方依赖安装在源码路径之外的位置时,这个配置会很有用。

12.9. resolveRequest[?CustomResolver]

一个可选函数,用于覆盖默认的模块解析算法

在需要使用别名(alias)或自定义协议(custom protocols)时特别有用。

resolveRequest: (context, moduleName, platform) => {
  if (moduleName.startsWith('my-custom-resolver:')) {
    // 自定义逻辑,把模块名解析为文件路径
    // 注意:如果无法解析,必须抛出错误
    return {
      filePath: 'path/to/file',
      type: 'sourceFile',
    };
  }
  // 可选:调用默认的 Metro 解析逻辑
  return context.resolveRequest(context, moduleName, platform);
}

12.10. useWatchman[boolean]

如果设置为 false,即使系统中安装了 Watchman,也不会让 Metro 使用它。

12.11. blockList[RegExp | Array<RegExp>]

用于定义哪些路径需要从 Metro 的文件映射中排除的正则表达式(或正则数组)。

任何绝对路径匹配这些规则的文件,都会被 Metro 完全忽略,无法被解析或导入。

此外,这些被屏蔽的文件也无法通过 /assets/ 接口被访问。

12.12. hasteImpModulePath[?string]

当前项目所使用的 Haste 实现文件路径。

Haste 是一种可选机制,允许你在项目中通过全局唯一名称来导入模块,例如:

import Foo from 'Foo'

Metro 要求这个模块导出如下结构:

module.exports = {
  getHasteName(filePath: string): ?string {
    // ...
  },
};

getHasteName 需要根据文件路径返回一个全局唯一的短名称
或返回 null(表示该模块不参与 Haste 系统)。

如果编写的里面没有 getCacheKey函数,那么会出现下面的错误:

error require(...).getCacheKey is not a function.
TypeError: require(...).getCacheKey is not a function
    at HastePlugin.getCacheKey (D:\Learns\Qianduan\react-native\metroLearn\node_modules\metro-file-map\src\plugins\HastePlugin.js:378:46)

下面是一个简单的例子:

module.exports = {
  getHasteName(filePath) {
    console.log(filePath)
    if (filePath.includes('Foo.js')) {
      return 'Foo';
    }
    return null;
  },
  getCacheKey() {
    return 'v1'
  }
};
// 这样原本导入可能需要 ../../../../Foo 的地方,只需要 import Foo from 'Foo' 就可以了。

12.13. platforms[Array<string>]

需要解析的额外平台。默认值为 ['ios', 'android', 'windows', 'web']

如果一个文件包含特定平台,那么在打包的时候就只会打包指定平台的代码,比如有 A.android.tsxA.ios.tsx ,打包 android ,那么最后 bundle 包中就只包含 A.android.tsx 的代码。假如你并没有在这个列表中写 android ,那么就可能报错,提示找不到这个文件,即便有也是识别 A.tsx 这个文件。

12.14. requireCycleIgnorePatternsArray<RegExp>

开发模式下,抑制(隐藏)涉及匹配这些表达式的模块的 require 循环警告。这对第三方代码和预期的第一方循环很有用。

注意:如果你为此配置项指定了自己的值,它将替换(而非追加)Metro 的默认值。

默认值:[/(^|/|\)node_modules($|/|\)/]

要想了解循环引用,首先得明白 metro 模块加载规则,之所以这么说是因为 metro 加载规则跟 nodejs 是不一样的。

如果 A -> B -> C 那么如果采用的是:

// A.tsx
import './B'
console.log("a")

// B.tsx
import './C'
console.log("b")

// C.tsx
console.log("c")


// 当我们导入 A.tsx 后执行循序是这样的
// c b a

这种情况下 nodejs 和 metro 是相同的。但是当代码下面这样就不一样了:

// A.tsx
import './B'
console.log("a")

// B.tsx
import {cFun} from './C'
console.log("b")
cFun()

// C.tsx
console.log("c")
export function cFun() {
  console.log("cFun")
}

// metro 中的结果: b c cFun a
// nodejs 中的结果: c b cFun a

可以看出来此时就存在差异了,主要原因在于 metro 处理按需导入的时候并不会立即执行导入的模块,而是具体执行的时候才开始执行。如果是导入默认的,那么情况就又跟 nodejs 相同了,也就是修改 B C 这两个文件的代码:

// B.tsx
import cFun from './C'
console.log("b")
cFun()

// C.tsx
console.log("c")
export default function cFun() {
  console.log("cFun")
}

// 此时执行循序 metro 跟 nodejs 相同都是: c b cFun a

了解这个我们才知道怎样 metro 才会出现循环引用。

我的示例目录结构:

├── A
│   ├── index.android.tsx
│   ├── index.ios.tsx
│   ├── index.web.tsx
│   └── index.windows.tsx
├── App.tsx
├── B
│   └── B.tsx

编写下面的正则表达式就可以了:

requireCycleIgnorePatterns: [
  // 1. 保留默认
  /(^|/|\)node_modules($|/|\)/,

  // 2. 匹配 A 组件 - 支持多种路径格式
  // 匹配: A/index.android.tsx, src/A/index.android.tsx, ./A/index.android.tsx
  /(^|/|\)A[/\]index.(android|ios|web|windows).tsx?$/,

  // 3. 匹配 B 组件 - 支持多种路径格式  
  // 匹配: B/B.tsx, src/B/B.tsx, ./B/B.tsx
  /(^|/|\)B[/\]B.tsx?$/,
]

其中要以 (^|/|\)开头,目的是匹配各种路径,官方默认也是有这个的。这里我写上了两个文件,实际情况是只要忽略循环引用中的一个文件即可。

13. transformer

转换器负责将模块转换成 react-native 能理解的代码格式。

13.1. dynamicDepsInPackpages['throwAtRuntime' | 'reject']

控制 Metro 如何处理无法在构建时静态分析的依赖。例如,require('./' + someFunction() + '.js') 无法在不执行 someFunction() 的情况下确定要加载哪个模块。

  • 'throwAtRuntime' (默认):Metro 不会停止打包,但 require 调用会在运行时抛出错误
  • 'reject' :Metro 会停止打包并向用户报告错误。

我测试了一下都会报错,目前还没完全弄明白。

13.2. getTransformOptions[Function]

Metro 在构建 bundle 时会调用这个函数,根据当前 bundle 的情况,动态计算 transformer 和 serializer 的额外配置。

函数签名:

function getTransformOptions(entryPoints, options, getDependenciesOf): Promise<ExtraTransformOptions>

13.2.1. 参数说明:

  • entryPoints:入口文件绝对路径(通常只有一个)
  • options
    • dev:是否开发模式
    • hot:已废弃(总是 true)
    • platform:目标平台(ios / android)
  • getDependenciesOf(path)
    👉 给你一个模块路径
    👉 返回它所有“递归依赖”的路径(Promise)

13.2.2. 返回值:

{
  preloadedModules?,
  ramGroups?,
  transform: {
    inlineRequires?,
    nonInlinedRequires?
  }
}
  • preloadedModules:一个普通对象,其键表示一组绝对路径。在对 indexed RAM bundle 进行序列化时,该集合中的模块会被标记为在运行时进行预加载(eager evaluation)。
  • ramGroups:一个绝对路径数组。在对 indexed RAM bundle 进行序列化时,数组中列出的每个模块都会与其传递依赖(transitive dependencies)一起被序列化。在运行时,当其中任意一个模块被执行时,这些模块会被一起解析(parsed)。
  • transform:用于 transformer 的高级配置选项。
    • inlineRequires:如果 inlineRequires 是布尔值,则表示是否在该 bundle 中启用 inline requires;如果 inlineRequires 是对象,则表示对所有模块启用 inline requires,但其绝对路径出现在 inlineRequires.blockList 中的模块除外。
    • nonInlinedRequires:一个未解析的模块标识符数组(例如 react、react-native),这些模块在启用 inline requires 的情况下也始终不会被 inline。

13.3. minifierPath[string]

用于指定在代码经过转换(transform)之后,对代码进行压缩(minify)的压缩器(minifier)的路径,或一个可以从 metro-transform-worker 解析到的包名。

默认值:'metro-minify-terser'

13.4. minifierConfig[{[key: string]: mixed}]

将传递给压缩器(minifier)的配置对象(需要是可序列化的)。

13.5. optmizationSizeLimit[number]

用于定义一个阈值(以字节为单位),当文件体积超过该阈值时,会禁用一些开销较大的优化操作。

13.6. assetPluginsArray<string>

用于修改资源(Asset)数据的模块列表。

我们根据上面的 assetExts 知道当遇到静态资源时会生成对应的资源对象,类似于:

{
  "httpServerLocation": "/assets",     // 资源在服务器上的路径
  "width": 100,                               // 图片宽度
  "height": 100,                              // 图片高度
  "scales": [1, 2, 3],                       // 支持的缩放比例
  "hash": "abc123def456",                    // 文件哈希(用于缓存)
  "name": "logo",                            // 文件名(不含扩展名)
  "type": "png"                              // 文件类型
}

如果你想把指定图片转换成其他类型的图片,那么就可以编写插件来完成。比如我将 jpg 转换成 webp 格式的图片,我让 AI 使用 sharp 来帮我编写了这个插件:

// metro-jpg-to-webp-plugin.js
const fs = require('fs');
const path = require('path');

/**
 * Metro Asset Plugin: JPG to WebP Converter (使用 sharp)
 * 
 * 功能:
 * 1. 使用 sharp 库将 JPG/JPEG 转换为 WebP
 * 2. 支持自定义压缩质量
 * 3. 智能缓存机制
 * 4. 保留原始尺寸和 scale 信息
 */

// 延迟加载 sharp(避免 Metro 启动时未安装报错)
let sharp = null;
function getSharp() {
  if (!sharp) {
    try {
      sharp = require('sharp');
    } catch (e) {
      throw new Error(
        '[metro-jpg-to-webp-plugin] sharp not found. ' +
        'Please install: npm install --save-dev sharp'
      );
    }
  }
  return sharp;
}

// 缓存目录
const CACHE_DIR = path.join(process.cwd(), '.webp-cache');

/**
 * 确保缓存目录存在
 */
function ensureCacheDir() {
  if (!fs.existsSync(CACHE_DIR)) {
    fs.mkdirSync(CACHE_DIR, { recursive: true });
  }
}

/**
 * 生成缓存文件路径
 * 基于:原始路径 + 修改时间 + 质量参数
 */
function getCachePath(originalPath, quality) {
  const stat = fs.statSync(originalPath);
  const hash = require('crypto')
    .createHash('md5')
    .update(`${originalPath}:${stat.mtimeMs}:${quality}`)
    .digest('hex')
    .slice(0, 8);
  
  const basename = path.basename(originalPath, path.extname(originalPath));
  return path.join(CACHE_DIR, `${basename}-${hash}-q${quality}.webp`);
}

/**
 * 检查缓存是否有效
 */
function isCacheValid(cachePath, originalPath) {
  if (!fs.existsSync(cachePath)) return false;
  
  const originalStat = fs.statSync(originalPath);
  const cacheStat = fs.statSync(cachePath);
  
  // 缓存文件比原文件新则有效
  return cacheStat.mtime > originalStat.mtime;
}

/**
 * 使用 sharp 转换 JPG 到 WebP
 */
async function convertWithSharp(inputPath, outputPath, quality) {
  ensureCacheDir();
  
  try {
    const sh = getSharp();
    
    await sh(inputPath)
      .webp({ 
        quality: quality,
        effort: 4,           // 压缩努力程度 (0-6),4 是平衡点
        smartSubsample: true, // 优化色度子采样
        nearLossless: false,  // 不使用近无损模式
      })
      .toFile(outputPath);
    
    return true;
  } catch (error) {
    console.error(`[jpg-to-webp-plugin] sharp conversion failed:`, error.message);
    return false;
  }
}

/**
 * 获取图片尺寸(使用 sharp)
 */
async function getImageDimensions(imagePath) {
  try {
    const sh = getSharp();
    const metadata = await sh(imagePath).metadata();
    return {
      width: metadata.width,
      height: metadata.height,
    };
  } catch (e) {
    return { width: null, height: null };
  }
}

/**
 * 格式化文件大小
 */
function formatSize(bytes) {
  if (bytes < 1024) return `${bytes}B`;
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)}KB`;
  return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;
}

/**
 * Metro Asset Plugin 主函数
 */
module.exports = async function jpgToWebpPlugin(assetData) {
  // 只处理 JPG/JPEG 文件
  const ext = assetData.type?.toLowerCase();
  if (ext !== 'jpg' && ext !== 'jpeg') {
    return assetData;
  }

  // 配置参数(可从环境变量或配置读取)
  const quality = parseInt(process.env.WEBP_QUALITY, 10) || 80;

  // 获取原始文件列表
  const originalFiles = assetData.files || [];
  if (originalFiles.length === 0) {
    return assetData;
  }

  console.log(`\n[jpg-to-webp-plugin] Processing: ${assetData.name}.${ext}`);

  // 转换结果数组
  const webpFiles = [];
  const webpScales = [];
  let totalOriginalSize = 0;
  let totalWebpSize = 0;

  for (let i = 0; i < originalFiles.length; i++) {
    const originalPath = originalFiles[i];
    const scale = assetData.scales?.[i] || 1;

    // 跳过不存在的文件
    if (!fs.existsSync(originalPath)) {
      console.warn(`  ⚠️ File not found: ${originalPath}`);
      webpFiles.push(originalPath);
      webpScales.push(scale);
      continue;
    }

    const originalSize = fs.statSync(originalPath).size;
    totalOriginalSize += originalSize;

    // 生成缓存路径
    const cachePath = getCachePath(originalPath, quality);

    // 检查缓存或执行转换
    if (!isCacheValid(cachePath, originalPath)) {
      console.log(`  🔄 Converting scale@${scale}x: ${path.basename(originalPath)}`);
      
      const success = await convertWithSharp(originalPath, cachePath, quality);
      
      if (!success) {
        console.warn(`  ❌ Conversion failed, using original`);
        webpFiles.push(originalPath);
        webpScales.push(scale);
        totalWebpSize += originalSize;
        continue;
      }
    } else {
      console.log(`  ✅ Using cache scale@${scale}x: ${path.basename(cachePath)}`);
    }

    const webpSize = fs.statSync(cachePath).size;
    totalWebpSize += webpSize;

    const savings = ((originalSize - webpSize) / originalSize * 100).toFixed(1);
    console.log(`     ${formatSize(originalSize)}${formatSize(webpSize)} (${savings}% smaller)`);

    webpFiles.push(cachePath);
    webpScales.push(scale);
  }

  // 计算总体节省
  if (totalOriginalSize > 0) {
    const totalSavings = ((totalOriginalSize - totalWebpSize) / totalOriginalSize * 100).toFixed(1);
    console.log(`  📊 Total: ${formatSize(totalOriginalSize)}${formatSize(totalWebpSize)} (${totalSavings}% smaller)\n`);
  }

  // 如果没有成功转换,保留原数据
  if (webpFiles.length === 0 || webpFiles.every((f, i) => f === originalFiles[i])) {
    return assetData;
  }

  // 获取尺寸信息(使用第一个成功转换的文件)
  let dimensions = { width: assetData.width, height: assetData.height };
  if (!dimensions.width && webpFiles.length > 0) {
    dimensions = await getImageDimensions(webpFiles[0]);
  }

  // 返回修改后的 assetData
  return {
    ...assetData,
    type: 'webp',                    // 修改文件类型为 webp
    files: webpFiles,                // 指向转换后的缓存文件
    scales: webpScales,              // 保留 scale 信息
    
    // 保留或更新尺寸信息
    width: dimensions.width || assetData.width,
    height: dimensions.height || assetData.height,
    
    // 生成新的文件哈希(基于缓存文件内容)
    fileHashes: webpFiles.map(filePath => {
      const content = fs.readFileSync(filePath);
      return require('crypto')
        .createHash('md5')
        .update(content)
        .digest('hex');
    }),
    
    // 保留其他元数据
    name: assetData.name,
    httpServerLocation: assetData.httpServerLocation,
  };
};

通过这个插件项目中无需修改任何代码,包括原本的图片后缀也不需要修改,自动帮忙转换了。只不过我觉得这个虽然很好,但是存在一定的风险,毕竟开发环境明明导入的时候 jpg 但是实际上却是 webp ,不懂的人会觉得很懵逼,只不过整体来说还是很好的,对于老项目如果一张图片一张图片的操作费时费力,而且改的多页容易出现问题。

13.7. assetRegistryPath[string]

从何处获取资源注册表。

默认实现:

const assets /*: Array<PackagerAsset> */ = [];

function registerAsset(asset /*: PackagerAsset */) /*: number */ {
  // `push` returns new array length, so the first asset will
  // get id 1 (not 0) to make the value truthy
  return assets.push(asset);
}

function getAssetByID(assetId /*: number */) /*: PackagerAsset */ {
  return assets[assetId - 1];
}

// eslint-disable-next-line @react-native/monorepo/no-commonjs-exports
module.exports = {registerAsset, getAssetByID};

由于最终 Image 获取资源导入的是默认的这个资源,所以如果你想自定义的话最好是:

const assetRegistry = require('@react-native/assets-registry/registry');

function registerAsset(asset /*: PackagerAsset */) /*: number */ {
  // `push` returns new array length, so the first asset will
  // get id 1 (not 0) to make the value truthy
  return assetRegistry.registerAsset(asset)
}

function getAssetByID(assetId /*: number */) /*: PackagerAsset */ {
  return assetRegistry.getAssetByID(assetId)
}

module.exports = {
  registerAsset,
  getAssetByID,
}

这样就能正常的显示图片了。主要是因为在 resolveAssetSource 这里导入的是官方默认的 @react-native/assets-registry/registry

13.8. babelTransformerPath[string]

用于指定通过 Babel 编译代码的模块名称,该模块需返回 AST 和可选的元数据。默认为 metro-babel-transformer。

请参考 metro-babel-transformer@react-native/metro-babel-transformer 的源代码,了解如何实现自定义 Babel transformer。

注意

此选项仅在默认的 transformerPath 下生效。自定义 transformers 可能会忽略此选项。

13.9. enableBabelRCLookup[boolean]

是否启用搜索 Babel 配置文件。该值作为 babelrc 配置选项传递给 Babel。默认为 true。

注意

此选项仅在默认的 transformerPath 下生效。自定义 transformers 可能会忽略此选项。自定义 Babel transformers 应当遵守此选项。

也就是会读取项目下的 babelrc 相关的配置文件,比如别名就可以在这个配置文件中进行。

13.10. enableBabelRuntime[boolean | string]

transformer 是否使用 @babel/plugin-transform-runtime 插件。默认为 true。

如果值为字符串,则被视为运行时版本号,并作为 version 参数传递给 @babel/plugin-transform-runtime 的配置。这允许你基于项目中安装的 Babel runtime 版本优化生成的运行时调用。

注意

此选项仅在 React Native 默认设置下生效。如果项目使用了自定义的 transformerPath、自定义的 babelTransformerPath 或自定义的 Babel 配置文件,此选项可能无效。

13.11. hermesParser[boolean]

是否使用 hermes-parser 包来解析 JavaScript 源文件,替代 Babel。默认为 false。

注意

此选项仅在默认的 transformerPath 和 Metro 内置的 Babel transformers 下生效。自定义 transformers 和自定义 Babel transformers 可能会忽略此选项。

14. seriallizer

14.1. getRunModuleStatement[(moduleId: number | string, globalPrefix: string) => string]

指定在 bundle 末尾追加的初始 require 语句的格式。默认为 __r(${moduleId});

默认情况下生成的 bundle 后面有下面:

__r(109);
__r(0);

而如果你像下面这样配置了:

module.exports = {
  serializer: {
    getRunModuleStatement: (moduleId) => {
      return `
        console.log("🚀 App is starting, entry =", ${moduleId});
        __r(${moduleId});
      `;
    },
  },
};

那最后打包的结果就为:

        console.log("🚀 App is starting, entry =", 109);
        __r(109);
      

        console.log("🚀 App is starting, entry =", 0);
        __r(0);

其中打包出来的 bundle 文件中, __d 代表注册模块,而 __r 则是执行模块。

14.2. createModuleIdFactory[() => (path: string) => number]

用来自定义 模块路径 → 模块 ID 的生成规则,该 ID 会被用于 bundle 中的 require / __d 调用。

14.3. getPolyfills[({platform: ?string}) => string[]]

用于指定 在 bundle 中预先注入的 polyfill 脚本列表,这些 polyfill 会在应用代码执行前加载,用来补齐 JS 运行环境缺失的能力。

14.4. getModulesRunBeforeMainModule[(entryFilePath: string) => Array<string>]

用于指定一组 在入口模块执行之前,通过 require 提前执行的模块列表(必须是绝对路径)。
⚠️ 前提是:这些模块已经被打进 bundle,否则不会生效。

react-native 默认情况下这里是 InitializeCore 模块首先初始化:

__d(function (global, _$$_REQUIRE, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, _dependencyMap) {
  'use client';
  'use strict';

  var start = Date.now();
  _$$_REQUIRE(_dependencyMap[0], "../../src/private/setup/setUpDefaultReactNativeEnvironment").default();
  _$$_REQUIRE(_dependencyMap[1], "../Utilities/GlobalPerformanceLogger").default.markPoint('initializeCore_start', _$$_REQUIRE(_dependencyMap[1], "../Utilities/GlobalPerformanceLogger").default.currentTimestamp() - (Date.now() - start));
  _$$_REQUIRE(_dependencyMap[1], "../Utilities/GlobalPerformanceLogger").default.markPoint('initializeCore_end');
},110,[111,252],"node_modules/react-native/Libraries/Core/InitializeCore.js");

如果我们想修改这个或者添加新的模块在 index.js 模块之前执行,那么就可以通过这个函数进行。

getModulesRunBeforeMainModule: (moduleId, globalPrefix) => {
  const defModule = defConfig.serializer.getModulesRunBeforeMainModule(moduleId, globalPrefix)
  return [
    ...defModule,
    require.resolve('./pre'),
  ]
}

这里就是 pre.js 会被提前执行。

14.5. processModuleFilter[(module) => boolean]

用于在打包阶段对模块进行过滤,决定哪些模块被包含进最终 bundle,哪些被直接丢弃

14.6. isThirdPartyModule[(module: {path: string, ...}) => string]

一个函数,用于确定哪些模块会被添加到 source map 的 x_google_ignoreList 字段中。这支持 Chrome DevTools 和其他兼容调试器中的 "Just My Code" 调试功能。

默认情况下,对路径中包含 node_modules 目录的模块返回 true。

注意

除了被 isThirdPartyModule 标记为忽略的模块外,Metro 还会自动将 bundler 自身生成的模块添加到忽略列表中。

15. server

给 Metro servers 使用。

15.1. port[number]

服务的端口号。

15.2. useGlobalHotkey[boolean]

是否启用 CMD+R 快捷键用于刷新 bundle。默认为 true。

15.3. rewriteRequestUrl[string => string]

一个函数,每当 Metro 处理 "URL" 时会被调用(见注释),在使用 jsc-safe-url 对非标准查询字符串分隔符进行规范化之后。Metro 会将此函数的返回值视为客户端提供的原始 URL。这适用于所有传入的 HTTP 请求(在任何自定义中间件之后),以及 /symbolicate 请求中的 bundle URL 和热重载协议中的 URL。

注意

输入可能是绝对 URL(例如 example.com/foo/bar?baz… /foo/bar?baz=qux)。输出应与输入使用相同的形式——即,当且仅当输入是绝对 URL 时,返回值才应该是绝对 URL。

15.4. forwardClientLogs[boolean]

启用将 client_log 事件(当客户端日志配置启用时)转发到 reporter。默认为 true。

15.5. tls[false | object]

如果未提供或为 false,Metro 将启动带有 WS WebSocket 端点的 HTTP 服务器。

如果为对象,Metro 将使用传入的 TLS 选项启动带有 WSS WebSocket 端点的 HTTPS 服务器:

{
  ca?: string | Buffer,      // 证书颁发机构(内容,不是路径)
  cert?: string | Buffer,    // 服务器证书(内容,不是路径)
  key?: string | Buffer,     // 私钥(内容,不是路径)
  requestCert?: boolean,     // 是否通过请求证书来验证远程对等方
}

注意,当覆盖基础配置时,对象类型的 tls 配置会扩展基础 tls 配置,false 会覆盖基础 tls 配置,而 null 和 undefined 会被忽略。

当使用 Metro.runServer 的 secureServerOptions 属性运行 Metro 时,Metro 同样会启动 HTTPS 服务器,如果提供了 config.server.tls 对象,则会与之合并,并覆盖它。

16. watcher

文件监听。

16.1. additionalExts[Array<string>]

Metro 除了 sourceExts 之外,还会额外监听这些扩展名的文件变化,但在模块解析(import/require)时,并不会自动尝试这些扩展名。

resolver.sourceExts 相比,有两个关键区别:

  1. 必须写完整文件名(包含扩展名)才能 import
import a from './moduleA.mjs' // ✅
import a from './moduleA'     // ❌ 不会自动补 .mjs
  1. 不会做平台扩展解析(platform-specific resolution)

比如不会去尝试:

moduleA.ios.mjs
moduleA.android.mjs

默认值:['cjs', 'mjs']

16.2. healthCheck.enabled[boolean]

是否定期检查文件系统监听器(watcher)是否正常工作。

检查方式是:向项目中写入一个临时文件,然后等待 watcher 是否能感知到这个变化。

默认值: false

16.3. healthCheck.filePrefix[string]

当启用了 watcher 健康检查(healthCheck)时,这个配置用于控制写入到项目中的临时文件名前缀

默认值: '.metro-health-check'

16.4. healthCheck.interval[number]

当启用 watcher 健康检查时,这个配置用于控制检查的执行频率(单位:毫秒) 。 默认值: 30000

16.5. healthCheck.timeout[number]

当启用 watcher 健康检查时,这个配置用于控制:
Metro 在写入检测文件后,会等待多久(毫秒)来确认 watcher 是否捕获到该变更。超过这个时间仍未捕获,则认为本次检测失败。

默认值: 5000

16.6. healthCheck.deferStates[string[]]

仅在使用 Watchman 时生效。
当 Watchman 处于这些“状态(states)”时,Metro 会延迟处理文件系统变更事件

这些状态存在期间,Metro 不会立刻触发 rebuild,而是“等一等”。

用途说明:

当文件系统还没有稳定(例如执行大规模代码更新)时,这可以用于“防抖”(debounce)构建过程。

默认值: ['hg.update']