前言
昨天看了社区小伙伴聊起来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.tsshared/logger/src/deferred.tsshared/logger/src/recording.tsshared/logger/src/sampled.tsshared/logger/src/void.tsshared/utils/src/index.tsshared/localization/src/index.tsshared/featurekit/src/index.tsshared/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-svelte2.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 handlersshared/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-utils→shared/utils
策略二: 创建占位符
- 如果依赖缺失但不影响编译,创建占位符
- 例如:
@amp/web-shared-styles→ 创建基础的 SCSS 文件
策略三: 创建 Mock 实现
- 如果依赖缺失且影响编译,创建最小实现
- 例如: Logger 模块的
deferred.ts、recording.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:
- 检查项目中是否有替代实现
- 创建最小 Mock 实现
- 如果无法模拟,建议仅用于代码研究
Q5: Node.js 版本不兼容怎么办?
A:
- 降级依赖到兼容版本
- 或升级 Node.js 到支持的版本
总结
关键要点
- 路径别名配置是关键 - 必须与代码中的导入路径匹配
- 模块导出文件很重要 - 为每个模块创建
index.ts - 版本兼容性要检查 - 确保依赖版本与 Node.js 版本匹配
- 私有依赖要有策略 - 根据实际情况选择合适的处理方式
项目状态
- ✅ 基础配置: 完成
- ✅ 路径别名: 完成
- ✅ 工具模块: 完成
- ✅ Logger 系统: 完成
- ✅ 样式占位符: 完成
- ⚠️ Jet 框架: 需要创建 mock(可选)
- ❌ API 类型: 需要创建类型定义(可选)
建议
对于从生产环境提取的项目:
- 优先用于代码研究 - 学习架构和最佳实践
- 创建最小配置 - 让项目可以编译和类型检查
- 不要强求完整运行 - 某些私有依赖无法模拟
- 关注核心功能 - 研究关键模块的实现
下一步
如果需要继续完善项目:
- 创建
@jet/environment的类型 stub - 创建
@jet-app/app-store/api的基础类型定义 - 完善样式占位符文件
- 添加更多的 Mock 实现