学习 App Store 代码

219 阅读4分钟

前言

昨天看了社区小伙伴聊起来App Store事件,就去github找了下前端代码 rxliuli 大佬用魔法手段找到代码并且上传了一份。让我们一起学习记录下!地址如下: github.com/rxliuli/app…

项目背景

项目特点

  • 技术栈: Svelte 4 + TypeScript + Vite
  • 框架: Jet Framework (Apple 内部框架)
  • 构建工具: Vite 4.x
  • 依赖管理: 包含大量 @amp/*@jet/* 私有包

项目结构

apps.apple.com.-main/
├── src/              # 主应用代码
├── shared/           # 共享模块
│   ├── components/   # 共享组件
│   ├── logger/       # 日志系统
│   ├── localization/ # 国际化
│   ├── utils/        # 工具函数
│   └── ...
├── assets/           # 静态资源
└── config/           # 配置文件

问题分析

问题一:构建工具配置缺失

症状:

  • 缺少 package.json
  • 缺少 vite.config.ts
  • 缺少 tsconfig.json
  • 缺少 index.html 入口文件

根本原因: 项目是从生产环境提取的源码,不包含完整的开发环境配置。

问题二:Node.js 版本不兼容

症状:

Error [ERR_MODULE_NOT_FOUND]: Cannot find module 'svelte/compiler'
crypto$2.getRandomValues is not a function

根本原因:

  • 当前 Node.js 版本: v16.20.2
  • Vite 5 需要: Node.js 18+ 或 20+
  • 依赖版本与 Node.js 版本不匹配

解决方案: 降级 Vite 和相关依赖到支持 Node.js 16 的版本:

{
  "dependencies": {
    "svelte": "^4.2.0"
  },
  "devDependencies": {
    "@sveltejs/vite-plugin-svelte": "^2.5.3",
    "vite": "^4.5.0"
  }
}

问题三:路径别名配置错误

症状:

Failed to load url /shared/apps-common/src/src/jet/prefetched-intents
EISDIR: illegal operation on a directory, read /shared/logger/src

根本原因: Vite 配置中的路径别名指向了错误的目录层级,导致路径重复(如 /src/src/)。

原始配置(错误):

resolve: {
  alias: {
    '@amp/web-apps-common': './shared/apps-common/src',
  }
}

正确配置:

resolve: {
  alias: {
    // 注意:代码中使用 @amp/web-apps-common/src/... 
    // 所以别名应该指向根目录,而不是 src 目录
    '@amp/web-apps-common': './shared/apps-common',
  }
}

问题四:私有依赖包缺失

症状:

Cannot find module '@amp/web-apps-utils'
Cannot find module '@amp/web-shared-styles'
Cannot find module '@jet/environment'

根本原因: 这些是 Apple 内部的私有包,无法从公共 npm 仓库获取。

依赖分类:

依赖包状态解决方案
@amp/web-apps-utils⚠️ 部分存在映射到 shared/utils
@amp/web-apps-logger✅ 已存在创建缺失的模块文件
@amp/web-apps-localization✅ 已存在创建 index.ts 导出
@amp/web-shared-styles❌ 缺失创建样式占位符
@jet/environment❌ 缺失需要创建类型 stub
@jet-app/app-store/api❌ 缺失需要创建类型定义

问题五:模块文件缺失

症状:

Failed to resolve import "./deferred" from "shared/logger/src/index.ts"
Cannot find module 'shared/logger/src/errorkit'

根本原因: 虽然目录结构存在,但缺少必要的模块导出文件。

缺失的文件:

  • shared/logger/src/types.ts
  • shared/logger/src/deferred.ts
  • shared/logger/src/recording.ts
  • shared/logger/src/sampled.ts
  • shared/logger/src/void.ts
  • shared/utils/src/index.ts
  • shared/localization/src/index.ts
  • shared/featurekit/src/index.ts
  • shared/logger/src/errorkit/index.ts

解决方案

方案一:创建完整的项目配置文件

1. package.json

{
  "name": "app-store-frontend",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "type-check": "svelte-check --tsconfig ./tsconfig.json"
  },
  "dependencies": {
    "svelte": "^4.2.0"
  },
  "devDependencies": {
    "@sveltejs/vite-plugin-svelte": "^2.5.3",
    "@tsconfig/svelte": "^5.0.0",
    "sass": "^1.69.0",
    "svelte-check": "^3.6.0",
    "typescript": "^5.3.0",
    "vite": "^4.5.0"
  }
}

关键点:

  • 使用 Vite 4.x 以兼容 Node.js 16
  • 使用 @sveltejs/vite-plugin-svelte 2.x 版本
  • 添加必要的开发依赖

2. vite.config.ts

import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
import path from 'path';

export default defineConfig({
  plugins: [svelte()],
  resolve: {
    alias: {
      '~': path.resolve(__dirname, './src'),
      // 路径别名配置:注意代码中使用 @amp/web-apps-common/src/...
      // 所以别名应该指向根目录,而不是 src 目录
      '@amp/web-app-components': path.resolve(__dirname, './shared/components'),
      '@amp/web-apps-common': path.resolve(__dirname, './shared/apps-common'),
      '@amp/web-apps-logger': path.resolve(__dirname, './shared/logger'),
      '@amp/web-apps-localization': path.resolve(__dirname, './shared/localization'),
      '@amp/web-apps-featurekit': path.resolve(__dirname, './shared/featurekit'),
      '@amp/web-apps-utils': path.resolve(__dirname, './shared/utils'),
      '@amp/web-shared-styles': path.resolve(__dirname, './shared/styles'),
      '@jet/environment': path.resolve(__dirname, './src/jet'),
      '@jet-app/app-store': path.resolve(__dirname, './src'),
    },
  },
  server: {
    port: 5173,
    open: true,
  },
  build: {
    outDir: 'dist',
    sourcemap: true,
  },
  define: {
    'process.env.VERSION': JSON.stringify('1.0.0-dev'),
    'import.meta.env.VITEST': 'false',
    'import.meta.env.DEV': 'true',
    'import.meta.env.APP_SCOPE': JSON.stringify('dev'),
  },
});

关键点:

  • 路径别名必须与代码中的导入路径匹配
  • 使用 path.resolve 确保路径正确
  • 定义环境变量以支持开发模式

3. tsconfig.json

{
  "extends": "@tsconfig/svelte/tsconfig.json",
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",
    "strict": true,
    "baseUrl": ".",
    "paths": {
      "~/*": ["./src/*"],
      "@amp/web-app-components/*": ["./shared/components/src/*"],
      "@amp/web-apps-common/*": ["./shared/apps-common/src/*"],
      "@amp/web-apps-logger/*": ["./shared/logger/src/*"],
      "@amp/web-apps-localization/*": ["./shared/localization/src/*"],
      "@amp/web-apps-featurekit/*": ["./shared/featurekit/src/*"],
      "@amp/web-apps-utils/*": ["./shared/utils/src/*"],
      "@jet/environment/*": ["./src/jet/*"],
      "@jet-app/app-store/*": ["./src/*"]
    }
  },
  "include": ["src/**/*.ts", "src/**/*.svelte", "shared/**/*.ts"],
  "exclude": ["node_modules"]
}

方案二:创建缺失的模块文件

1. Logger 模块

shared/logger/src/types.ts:

export type Level = 'debug' | 'info' | 'warn' | 'error';

export interface Logger {
    debug(...args: unknown[]): string;
    info(...args: unknown[]): string;
    warn(...args: unknown[]): string;
    error(...args: unknown[]): string;
}

export interface LoggerFactory {
    loggerFor(name: string): Logger;
}

shared/logger/src/deferred.ts:

import type { LoggerFactory, Logger, Level } from './types';
import { BaseLogger } from './base';

export class DeferredLoggerFactory implements LoggerFactory {
    private readonly factoryGetter: () => LoggerFactory;

    constructor(factoryGetter: () => LoggerFactory) {
        this.factoryGetter = factoryGetter;
    }

    loggerFor(name: string): Logger {
        return new DeferredLogger(name, this.factoryGetter);
    }
}

class DeferredLogger extends BaseLogger {
    private readonly factoryGetter: () => LoggerFactory;
    private logger: Logger | null = null;

    constructor(name: string, factoryGetter: () => LoggerFactory) {
        super(name);
        this.factoryGetter = factoryGetter;
    }

    private getLogger(): Logger {
        if (!this.logger) {
            this.logger = this.factoryGetter().loggerFor(this.name);
        }
        return this.logger;
    }

    protected log(method: Level, ...args: unknown[]): string {
        return this.getLogger()[method](...args);
    }
}

shared/logger/src/index.ts:

export * from './composite';
export * from './console';
export * from './deferred';
export * from './recording';
export * from './sampled';
export * from './types';
export * from './void';
export * from './errorkit';

export function setContext(
    context: Map<string, unknown>,
    factory: LoggerFactory,
): void {
    context.set('loggerFactory', factory);
}

export function loggerFor(subject: string): Logger {
    const factory = getContext('loggerFactory') as LoggerFactory | undefined;
    if (!factory) {
        throw new Error('loggerFor called before setContext');
    }
    return factory.loggerFor(subject);
}

2. Utils 模块

shared/utils/src/index.ts:

// Re-export all utility functions
export * from './is-pojo';
export * from './uuid';
export * from './history';
export * from './platform';
export * from './optional';
export * from './url';
export * from './try-scroll';
export * from './lru-map';
export * from './object-from-entries';
export * from './get-pwa-display-mode';
export * from './launch/launch-client';
export * from './launch/scheme';

3. Localization 模块

shared/localization/src/index.ts:

// Re-export main types and functions
export { I18N } from './i18n';
export type { default as I18N } from './i18n';
export * from './i18n';
export * from './getPageDir';
export * from './getLocAttributes';
export * from './setHTMLAttributes';
export * from './translator';

方案三:创建样式占位符

由于 @amp/web-shared-styles 是 Apple 内部的样式系统,我们创建了占位符文件:

shared/styles/sasskit-stylekit/ac-sasskit-config.scss:

// Mock styles for @amp/web-shared-styles
// This is a placeholder for the actual Apple internal styles

$global-sidebar-width-large: 260px !default;

@mixin viewport($name) {
  // Mock viewport mixin
}

shared/styles/app/core/viewports.scss:

// Mock viewports styles
@mixin viewport-small {
  @media (max-width: 767px) {
    @content;
  }
}

@mixin viewport-medium {
  @media (min-width: 768px) and (max-width: 1023px) {
    @content;
  }
}

@mixin viewport-large {
  @media (min-width: 1024px) {
    @content;
  }
}

shared/styles/app/core/globalvars.scss:

// Mock global variables
$global-sidebar-width-large: 260px !default;
$global-sidebar-width: 240px !default;

方案四:处理目录导入问题

问题: 代码中导入目录而不是文件,导致 EISDIR 错误。

解决方案: 为目录创建 index.ts 文件:

  • src/jet/action-handlers/index.ts - 导出 action handlers
  • shared/logger/src/errorkit/index.ts - 导出 errorkit 模块
  • shared/featurekit/src/index.ts - 导出 featurekit 模块

示例:

// src/jet/action-handlers/index.ts
export * from './browser';

最佳实践

1. 路径别名配置原则

原则: 路径别名应该指向模块的根目录,而不是 src 目录。

原因: 代码中使用 @amp/web-apps-common/src/jet/... 这样的导入路径,如果别名指向 src,会导致路径重复。

正确做法:

'@amp/web-apps-common': './shared/apps-common'
// 代码中使用: @amp/web-apps-common/src/jet/...
// 实际解析为: ./shared/apps-common/src/jet/...

错误做法:

'@amp/web-apps-common': './shared/apps-common/src'
// 代码中使用: @amp/web-apps-common/src/jet/...
// 实际解析为: ./shared/apps-common/src/src/jet/... ❌

2. 模块导出文件

原则: 为每个模块目录创建 index.ts 文件,统一导出。

好处:

  • 简化导入路径
  • 隐藏内部实现细节
  • 便于重构

示例:

// shared/utils/src/index.ts
export * from './is-pojo';
export * from './uuid';
export * from './history';
// ... 其他导出

3. 版本兼容性处理

原则: 根据项目环境和 Node.js 版本选择合适的依赖版本。

检查清单:

  • Node.js 版本
  • Vite 版本兼容性
  • Svelte 插件版本
  • TypeScript 版本

示例:

  • Node.js 16 → Vite 4.x
  • Node.js 18+ → Vite 5.x

4. 私有依赖处理策略

策略一: 映射到现有代码

  • 如果项目中有对应的实现,使用路径别名映射
  • 例如: @amp/web-apps-utilsshared/utils

策略二: 创建占位符

  • 如果依赖缺失但不影响编译,创建占位符
  • 例如: @amp/web-shared-styles → 创建基础的 SCSS 文件

策略三: 创建 Mock 实现

  • 如果依赖缺失且影响编译,创建最小实现
  • 例如: Logger 模块的 deferred.tsrecording.ts

策略四: 仅用于代码研究

  • 如果依赖无法模拟,建议仅用于代码研究
  • 例如: @jet/environment 框架依赖

常见问题及解决方案

Q1: 如何判断路径别名配置是否正确?

A: 检查导入路径是否解析正确:

# 代码中使用
import { something } from '@amp/web-apps-utils';

# 应该解析为
./shared/utils/src/index.ts

Q2: EISDIR 错误如何解决?

A: 为目录创建 index.ts 文件,或者修正导入路径指向具体文件。

Q3: 样式文件缺失怎么办?

A: 创建基础的 SCSS 占位符文件,包含必要的变量和 mixin。

Q4: 如何处理无法获取的私有依赖?

A:

  1. 检查项目中是否有替代实现
  2. 创建最小 Mock 实现
  3. 如果无法模拟,建议仅用于代码研究

Q5: Node.js 版本不兼容怎么办?

A:

  • 降级依赖到兼容版本
  • 或升级 Node.js 到支持的版本

总结

关键要点

  1. 路径别名配置是关键 - 必须与代码中的导入路径匹配
  2. 模块导出文件很重要 - 为每个模块创建 index.ts
  3. 版本兼容性要检查 - 确保依赖版本与 Node.js 版本匹配
  4. 私有依赖要有策略 - 根据实际情况选择合适的处理方式

项目状态

  • 基础配置: 完成
  • 路径别名: 完成
  • 工具模块: 完成
  • Logger 系统: 完成
  • 样式占位符: 完成
  • ⚠️ Jet 框架: 需要创建 mock(可选)
  • API 类型: 需要创建类型定义(可选)

建议

对于从生产环境提取的项目:

  1. 优先用于代码研究 - 学习架构和最佳实践
  2. 创建最小配置 - 让项目可以编译和类型检查
  3. 不要强求完整运行 - 某些私有依赖无法模拟
  4. 关注核心功能 - 研究关键模块的实现

下一步

如果需要继续完善项目:

  1. 创建 @jet/environment 的类型 stub
  2. 创建 @jet-app/app-store/api 的基础类型定义
  3. 完善样式占位符文件
  4. 添加更多的 Mock 实现

参考资料