8.1 项目结构设计
8.1.1 标准项目结构
一个典型的 TypeScript 项目结构应该清晰、可维护且易于扩展:
my-typescript-project/
├── src/ # 源代码目录
│ ├── components/ # 组件
│ ├── services/ # 服务层
│ ├── utils/ # 工具函数
│ ├── types/ # 类型定义
│ ├── constants/ # 常量
│ ├── hooks/ # 自定义钩子(React项目)
│ ├── stores/ # 状态管理
│ ├── assets/ # 静态资源
│ ├── styles/ # 样式文件
│ ├── tests/ # 测试文件
│ └── index.ts # 入口文件
├── dist/ # 编译输出目录
├── docs/ # 文档
├── scripts/ # 构建脚本
├── public/ # 公共资源
├── .vscode/ # VS Code 配置
├── node_modules/ # 依赖包
├── package.json # 项目配置
├── tsconfig.json # TypeScript 配置
├── tsconfig.build.json # 构建配置
├── jest.config.js # 测试配置
├── .eslintrc.js # ESLint 配置
├── .prettierrc # Prettier 配置
├── .gitignore # Git 忽略文件
├── README.md # 项目说明
└── CHANGELOG.md # 变更日志
8.1.2 模块化组织
// src/types/index.ts - 统一类型导出
export * from './user';
export * from './api';
export * from './common';
// src/types/user.ts
export interface User {
id: string;
name: string;
email: string;
role: UserRole;
createdAt: Date;
updatedAt: Date;
}
export enum UserRole {
ADMIN = 'admin',
USER = 'user',
MODERATOR = 'moderator'
}
export interface CreateUserRequest {
name: string;
email: string;
role?: UserRole;
}
export interface UpdateUserRequest extends Partial<CreateUserRequest> {
id: string;
}
// src/types/api.ts
export interface ApiResponse<T = any> {
success: boolean;
data?: T;
error?: string;
message?: string;
}
export interface PaginatedResponse<T> extends ApiResponse<T[]> {
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}
export interface ApiError {
code: string;
message: string;
details?: Record<string, any>;
}
// src/types/common.ts
export type ID = string | number;
export interface BaseEntity {
id: ID;
createdAt: Date;
updatedAt: Date;
}
export type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
export type RequiredFields<T, K extends keyof T> = T & Required<Pick<T, K>>;
// src/constants/index.ts - 常量管理
export const API_ENDPOINTS = {
USERS: '/api/users',
AUTH: '/api/auth',
POSTS: '/api/posts'
} as const;
export const HTTP_STATUS = {
OK: 200,
CREATED: 201,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
INTERNAL_SERVER_ERROR: 500
} as const;
export const VALIDATION_RULES = {
EMAIL_REGEX: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
PASSWORD_MIN_LENGTH: 8,
USERNAME_MIN_LENGTH: 3,
USERNAME_MAX_LENGTH: 20
} as const;
// src/utils/index.ts - 工具函数
export * from './validation';
export * from './formatting';
export * from './api';
export * from './storage';
// src/utils/validation.ts
import { VALIDATION_RULES } from '../constants';
export class ValidationUtils {
static isValidEmail(email: string): boolean {
return VALIDATION_RULES.EMAIL_REGEX.test(email);
}
static isValidPassword(password: string): boolean {
return password.length >= VALIDATION_RULES.PASSWORD_MIN_LENGTH;
}
static isValidUsername(username: string): boolean {
return username.length >= VALIDATION_RULES.USERNAME_MIN_LENGTH &&
username.length <= VALIDATION_RULES.USERNAME_MAX_LENGTH;
}
static validateRequired<T>(value: T, fieldName: string): T {
if (value === null || value === undefined || value === '') {
throw new Error(`${fieldName} is required`);
}
return value;
}
}
// src/utils/formatting.ts
export class FormattingUtils {
static formatDate(date: Date, locale = 'en-US'): string {
return new Intl.DateTimeFormat(locale).format(date);
}
static formatCurrency(amount: number, currency = 'USD', locale = 'en-US'): string {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency
}).format(amount);
}
static truncateText(text: string, maxLength: number): string {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength - 3) + '...';
}
static capitalizeFirst(text: string): string {
return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
}
}
// src/services/base.service.ts - 基础服务类
import { ApiResponse, ApiError } from '../types';
export abstract class BaseService {
protected baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
protected async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<ApiResponse<T>> {
try {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || 'Request failed');
}
return data;
} catch (error) {
throw this.handleError(error);
}
}
protected handleError(error: unknown): ApiError {
if (error instanceof Error) {
return {
code: 'REQUEST_ERROR',
message: error.message
};
}
return {
code: 'UNKNOWN_ERROR',
message: 'An unknown error occurred'
};
}
}
8.2 TypeScript 配置详解
8.2.1 tsconfig.json 完整配置
{
"compilerOptions": {
// 基本选项
"target": "ES2020", // 编译目标
"module": "ESNext", // 模块系统
"lib": ["ES2020", "DOM", "DOM.Iterable"], // 包含的库
"allowJs": true, // 允许编译 JS 文件
"checkJs": false, // 检查 JS 文件
"jsx": "react-jsx", // JSX 处理方式
"declaration": true, // 生成声明文件
"declarationMap": true, // 生成声明文件的 source map
"sourceMap": true, // 生成 source map
"outDir": "./dist", // 输出目录
"rootDir": "./src", // 根目录
"removeComments": false, // 移除注释
"noEmit": false, // 不生成输出文件
"incremental": true, // 增量编译
"tsBuildInfoFile": "./.tsbuildinfo", // 增量编译信息文件
// 模块解析选项
"moduleResolution": "node", // 模块解析策略
"baseUrl": "./", // 基础路径
"paths": { // 路径映射
"@/*": ["src/*"],
"@/components/*": ["src/components/*"],
"@/services/*": ["src/services/*"],
"@/utils/*": ["src/utils/*"],
"@/types/*": ["src/types/*"]
},
"typeRoots": ["./node_modules/@types", "./src/types"], // 类型根目录
"types": ["node", "jest"], // 包含的类型包
"allowSyntheticDefaultImports": true, // 允许合成默认导入
"esModuleInterop": true, // ES 模块互操作
"preserveSymlinks": true, // 保留符号链接
"allowUmdGlobalAccess": false, // 允许 UMD 全局访问
// 严格检查选项
"strict": true, // 启用所有严格检查
"noImplicitAny": true, // 不允许隐式 any
"strictNullChecks": true, // 严格空值检查
"strictFunctionTypes": true, // 严格函数类型检查
"strictBindCallApply": true, // 严格 bind/call/apply 检查
"strictPropertyInitialization": true, // 严格属性初始化检查
"noImplicitThis": true, // 不允许隐式 this
"alwaysStrict": true, // 总是以严格模式解析
// 额外检查选项
"noUnusedLocals": true, // 检查未使用的局部变量
"noUnusedParameters": true, // 检查未使用的参数
"exactOptionalPropertyTypes": true, // 精确可选属性类型
"noImplicitReturns": true, // 检查函数返回值
"noFallthroughCasesInSwitch": true, // 检查 switch 语句的 fallthrough
"noUncheckedIndexedAccess": true, // 检查索引访问
"noImplicitOverride": true, // 检查隐式重写
"noPropertyAccessFromIndexSignature": true, // 禁止从索引签名访问属性
// 高级选项
"skipLibCheck": true, // 跳过库文件检查
"forceConsistentCasingInFileNames": true, // 强制文件名大小写一致
"resolveJsonModule": true, // 解析 JSON 模块
"isolatedModules": true, // 隔离模块
"useDefineForClassFields": true, // 使用 define 定义类字段
"experimentalDecorators": true, // 实验性装饰器
"emitDecoratorMetadata": true, // 发出装饰器元数据
"downlevelIteration": true, // 降级迭代
"importHelpers": true, // 导入辅助函数
"newLine": "lf" // 换行符
},
"include": [
"src/**/*",
"tests/**/*",
"scripts/**/*"
],
"exclude": [
"node_modules",
"dist",
"build",
"coverage",
"**/*.spec.ts",
"**/*.test.ts"
],
"references": [
{ "path": "./tsconfig.build.json" }
]
}
8.2.2 多配置文件管理
// tsconfig.build.json - 构建配置
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false,
"declaration": true,
"declarationMap": true,
"sourceMap": false,
"removeComments": true
},
"exclude": [
"**/*.test.ts",
"**/*.spec.ts",
"tests/**/*",
"src/**/*.stories.ts"
]
}
// tsconfig.test.json - 测试配置
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": true,
"types": ["node", "jest", "@testing-library/jest-dom"]
},
"include": [
"src/**/*",
"tests/**/*",
"**/*.test.ts",
"**/*.spec.ts"
]
}
// tsconfig.node.json - Node.js 配置
{
"extends": "./tsconfig.json",
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"lib": ["ES2020"],
"types": ["node"]
},
"include": [
"scripts/**/*",
"server/**/*"
]
}
8.2.3 路径映射和模块解析
// 配置路径映射后的导入示例
// 传统导入方式
// import { UserService } from '../../../services/user.service';
// import { ValidationUtils } from '../../../utils/validation';
// import { User } from '../../../types/user';
// 使用路径映射后
import { UserService } from '@/services/user.service';
import { ValidationUtils } from '@/utils/validation';
import { User } from '@/types/user';
// 创建路径映射配置工具
class PathMappingConfig {
static generatePaths(srcDir: string = 'src'): Record<string, string[]> {
return {
'@/*': [`${srcDir}/*`],
'@/components/*': [`${srcDir}/components/*`],
'@/services/*': [`${srcDir}/services/*`],
'@/utils/*': [`${srcDir}/utils/*`],
'@/types/*': [`${srcDir}/types/*`],
'@/constants/*': [`${srcDir}/constants/*`],
'@/hooks/*': [`${srcDir}/hooks/*`],
'@/stores/*': [`${srcDir}/stores/*`],
'@/assets/*': [`${srcDir}/assets/*`],
'@/styles/*': [`${srcDir}/styles/*`]
};
}
static generateWebpackAlias(srcDir: string = 'src'): Record<string, string> {
const path = require('path');
const basePath = path.resolve(__dirname, srcDir);
return {
'@': basePath,
'@/components': path.resolve(basePath, 'components'),
'@/services': path.resolve(basePath, 'services'),
'@/utils': path.resolve(basePath, 'utils'),
'@/types': path.resolve(basePath, 'types'),
'@/constants': path.resolve(basePath, 'constants'),
'@/hooks': path.resolve(basePath, 'hooks'),
'@/stores': path.resolve(basePath, 'stores'),
'@/assets': path.resolve(basePath, 'assets'),
'@/styles': path.resolve(basePath, 'styles')
};
}
}
8.3 代码质量工具
8.3.1 ESLint 配置
// .eslintrc.js
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
ecmaFeatures: {
jsx: true
},
project: './tsconfig.json'
},
plugins: [
'@typescript-eslint',
'react',
'react-hooks',
'import',
'jsx-a11y',
'prettier'
],
extends: [
'eslint:recommended',
'@typescript-eslint/recommended',
'@typescript-eslint/recommended-requiring-type-checking',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:import/typescript',
'plugin:jsx-a11y/recommended',
'prettier'
],
env: {
browser: true,
es6: true,
node: true,
jest: true
},
settings: {
react: {
version: 'detect'
},
'import/resolver': {
typescript: {
alwaysTryTypes: true,
project: './tsconfig.json'
}
}
},
rules: {
// TypeScript 规则
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/explicit-function-return-type': 'warn',
'@typescript-eslint/explicit-module-boundary-types': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-non-null-assertion': 'warn',
'@typescript-eslint/prefer-const': 'error',
'@typescript-eslint/prefer-readonly': 'warn',
'@typescript-eslint/prefer-nullish-coalescing': 'error',
'@typescript-eslint/prefer-optional-chain': 'error',
'@typescript-eslint/strict-boolean-expressions': 'warn',
'@typescript-eslint/switch-exhaustiveness-check': 'error',
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
'@typescript-eslint/consistent-type-imports': [
'error',
{ prefer: 'type-imports' }
],
// 导入规则
'import/order': [
'error',
{
groups: [
'builtin',
'external',
'internal',
'parent',
'sibling',
'index'
],
'newlines-between': 'always',
alphabetize: {
order: 'asc',
caseInsensitive: true
}
}
],
'import/no-unresolved': 'error',
'import/no-cycle': 'error',
'import/no-self-import': 'error',
'import/no-duplicate-imports': 'error',
// React 规则
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
'react/jsx-uses-react': 'off',
'react/jsx-uses-vars': 'error',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
// 通用规则
'no-console': ['warn', { allow: ['warn', 'error'] }],
'no-debugger': 'error',
'no-alert': 'error',
'no-var': 'error',
'prefer-const': 'error',
'prefer-arrow-callback': 'error',
'arrow-spacing': 'error',
'object-shorthand': 'error',
'prefer-template': 'error',
'template-curly-spacing': 'error',
'yield-star-spacing': 'error',
'yoda': 'error',
// Prettier 集成
'prettier/prettier': 'error'
},
overrides: [
{
files: ['**/*.test.ts', '**/*.test.tsx', '**/*.spec.ts', '**/*.spec.tsx'],
env: {
jest: true
},
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-non-null-assertion': 'off'
}
},
{
files: ['scripts/**/*'],
rules: {
'no-console': 'off'
}
}
]
};
8.3.2 Prettier 配置
// .prettierrc
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"quoteProps": "as-needed",
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "avoid",
"endOfLine": "lf",
"embeddedLanguageFormatting": "auto",
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"jsxSingleQuote": true,
"proseWrap": "preserve",
"requirePragma": false,
"vueIndentScriptAndStyle": false
}
// .prettierignore
dist/
build/
coverage/
node_modules/
*.min.js
*.min.css
package-lock.json
yarn.lock
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
8.3.3 Husky 和 lint-staged 配置
// package.json
{
"scripts": {
"prepare": "husky install",
"lint": "eslint src --ext .ts,.tsx --fix",
"lint:check": "eslint src --ext .ts,.tsx",
"format": "prettier --write src/**/*.{ts,tsx,json,css,md}",
"format:check": "prettier --check src/**/*.{ts,tsx,json,css,md}",
"type-check": "tsc --noEmit",
"test": "jest",
"test:coverage": "jest --coverage"
},
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write",
"git add"
],
"*.{json,css,md}": [
"prettier --write",
"git add"
]
},
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"pre-push": "npm run type-check && npm run test",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
}
}
# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
# .husky/pre-push
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run type-check
npm run test
# .husky/commit-msg
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no -- commitlint --edit "$1"
8.4 构建和打包
8.4.1 Webpack 配置
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const webpack = require('webpack');
module.exports = (env, argv) => {
const isProduction = argv.mode === 'production';
const isDevelopment = !isProduction;
return {
entry: './src/index.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: isProduction
? '[name].[contenthash].js'
: '[name].js',
chunkFilename: isProduction
? '[name].[contenthash].chunk.js'
: '[name].chunk.js',
publicPath: '/',
clean: true
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
alias: {
'@': path.resolve(__dirname, 'src'),
'@/components': path.resolve(__dirname, 'src/components'),
'@/services': path.resolve(__dirname, 'src/services'),
'@/utils': path.resolve(__dirname, 'src/utils'),
'@/types': path.resolve(__dirname, 'src/types'),
'@/constants': path.resolve(__dirname, 'src/constants'),
'@/hooks': path.resolve(__dirname, 'src/hooks'),
'@/stores': path.resolve(__dirname, 'src/stores'),
'@/assets': path.resolve(__dirname, 'src/assets'),
'@/styles': path.resolve(__dirname, 'src/styles')
}
},
module: {
rules: [
{
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true,
configFile: 'tsconfig.json'
}
}
]
},
{
test: /\.(css|scss|sass)$/,
use: [
isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
{
loader: 'css-loader',
options: {
modules: {
auto: true,
localIdentName: isProduction
? '[hash:base64:8]'
: '[name]__[local]--[hash:base64:5]'
},
sourceMap: isDevelopment
}
},
{
loader: 'postcss-loader',
options: {
sourceMap: isDevelopment
}
},
{
loader: 'sass-loader',
options: {
sourceMap: isDevelopment
}
}
]
},
{
test: /\.(png|jpe?g|gif|svg|webp)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024 // 8kb
}
},
generator: {
filename: 'assets/images/[name].[hash][ext]'
}
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
generator: {
filename: 'assets/fonts/[name].[hash][ext]'
}
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './public/index.html',
filename: 'index.html',
inject: true,
minify: isProduction ? {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true
} : false
}),
new ForkTsCheckerWebpackPlugin({
typescript: {
configFile: 'tsconfig.json'
},
eslint: {
files: './src/**/*.{ts,tsx}'
}
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(argv.mode),
'process.env.VERSION': JSON.stringify(process.env.npm_package_version)
}),
...(isProduction ? [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
chunkFilename: '[name].[contenthash].chunk.css'
})
] : [])
],
optimization: {
minimize: isProduction,
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
},
common: {
name: 'common',
minChunks: 2,
chunks: 'all',
enforce: true
}
}
},
runtimeChunk: {
name: 'runtime'
}
},
devServer: {
static: {
directory: path.join(__dirname, 'public')
},
compress: true,
port: 3000,
hot: true,
open: true,
historyApiFallback: true,
client: {
overlay: {
errors: true,
warnings: false
}
}
},
devtool: isDevelopment ? 'eval-source-map' : 'source-map',
performance: {
hints: isProduction ? 'warning' : false,
maxEntrypointSize: 512000,
maxAssetSize: 512000
}
};
};
8.4.2 Vite 配置
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
import { visualizer } from 'rollup-plugin-visualizer';
import { createHtmlPlugin } from 'vite-plugin-html';
export default defineConfig(({ command, mode }) => {
const isProduction = mode === 'production';
return {
plugins: [
react(),
createHtmlPlugin({
inject: {
data: {
title: 'TypeScript App',
description: 'A modern TypeScript application'
}
}
}),
...(isProduction ? [
visualizer({
filename: 'dist/stats.html',
open: true,
gzipSize: true,
brotliSize: true
})
] : [])
],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@/components': resolve(__dirname, 'src/components'),
'@/services': resolve(__dirname, 'src/services'),
'@/utils': resolve(__dirname, 'src/utils'),
'@/types': resolve(__dirname, 'src/types'),
'@/constants': resolve(__dirname, 'src/constants'),
'@/hooks': resolve(__dirname, 'src/hooks'),
'@/stores': resolve(__dirname, 'src/stores'),
'@/assets': resolve(__dirname, 'src/assets'),
'@/styles': resolve(__dirname, 'src/styles')
}
},
css: {
modules: {
localsConvention: 'camelCase',
generateScopedName: isProduction
? '[hash:base64:8]'
: '[name]__[local]--[hash:base64:5]'
},
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/variables.scss";`
}
}
},
build: {
target: 'es2020',
outDir: 'dist',
assetsDir: 'assets',
sourcemap: true,
minify: 'terser',
terserOptions: {
compress: {
drop_console: isProduction,
drop_debugger: isProduction
}
},
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
utils: ['lodash', 'date-fns']
}
}
},
chunkSizeWarningLimit: 1000
},
server: {
port: 3000,
open: true,
cors: true,
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
preview: {
port: 4173,
open: true
},
define: {
__APP_VERSION__: JSON.stringify(process.env.npm_package_version),
__BUILD_TIME__: JSON.stringify(new Date().toISOString())
},
optimizeDeps: {
include: ['react', 'react-dom'],
exclude: ['@vite/client', '@vite/env']
}
};
});
8.4.3 构建脚本
// scripts/build.ts
import { execSync } from 'child_process';
import { rmSync, existsSync } from 'fs';
import { resolve } from 'path';
import chalk from 'chalk';
class BuildScript {
private readonly distDir = resolve(__dirname, '../dist');
async run(): Promise<void> {
console.log(chalk.blue('🚀 Starting build process...'));
try {
// 清理输出目录
this.cleanDist();
// 类型检查
await this.typeCheck();
// 代码检查
await this.lint();
// 运行测试
await this.test();
// 构建项目
await this.build();
// 分析包大小
await this.analyzeBundleSize();
console.log(chalk.green('✅ Build completed successfully!'));
} catch (error) {
console.error(chalk.red('❌ Build failed:'), error);
process.exit(1);
}
}
private cleanDist(): void {
console.log(chalk.yellow('🧹 Cleaning dist directory...'));
if (existsSync(this.distDir)) {
rmSync(this.distDir, { recursive: true, force: true });
}
}
private async typeCheck(): Promise<void> {
console.log(chalk.yellow('🔍 Running type check...'));
try {
execSync('npx tsc --noEmit', { stdio: 'inherit' });
console.log(chalk.green('✅ Type check passed'));
} catch (error) {
throw new Error('Type check failed');
}
}
private async lint(): Promise<void> {
console.log(chalk.yellow('🔍 Running ESLint...'));
try {
execSync('npx eslint src --ext .ts,.tsx', { stdio: 'inherit' });
console.log(chalk.green('✅ Linting passed'));
} catch (error) {
throw new Error('Linting failed');
}
}
private async test(): Promise<void> {
console.log(chalk.yellow('🧪 Running tests...'));
try {
execSync('npm test -- --coverage --watchAll=false', { stdio: 'inherit' });
console.log(chalk.green('✅ Tests passed'));
} catch (error) {
throw new Error('Tests failed');
}
}
private async build(): Promise<void> {
console.log(chalk.yellow('📦 Building project...'));
try {
execSync('npx webpack --mode production', { stdio: 'inherit' });
console.log(chalk.green('✅ Build completed'));
} catch (error) {
throw new Error('Build failed');
}
}
private async analyzeBundleSize(): Promise<void> {
console.log(chalk.yellow('📊 Analyzing bundle size...'));
try {
execSync('npx webpack-bundle-analyzer dist/stats.json --mode static --report dist/bundle-report.html --no-open', {
stdio: 'inherit'
});
console.log(chalk.green('✅ Bundle analysis completed'));
} catch (error) {
console.warn(chalk.yellow('⚠️ Bundle analysis failed, but build continues'));
}
}
}
// 运行构建脚本
if (require.main === module) {
const buildScript = new BuildScript();
buildScript.run();
}
export { BuildScript };
// package.json scripts
{
"scripts": {
"dev": "vite",
"build": "ts-node scripts/build.ts",
"build:prod": "NODE_ENV=production npm run build",
"build:analyze": "npm run build && npx webpack-bundle-analyzer dist/stats.json",
"preview": "vite preview",
"type-check": "tsc --noEmit",
"type-check:watch": "tsc --noEmit --watch",
"lint": "eslint src --ext .ts,.tsx --fix",
"lint:check": "eslint src --ext .ts,.tsx",
"format": "prettier --write src/**/*.{ts,tsx,json,css,md}",
"format:check": "prettier --check src/**/*.{ts,tsx,json,css,md}",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"clean": "rimraf dist coverage .tsbuildinfo",
"clean:all": "npm run clean && rimraf node_modules package-lock.json"
}
}
8.5 环境配置管理
8.5.1 环境变量管理
// src/config/env.ts
interface EnvironmentConfig {
NODE_ENV: 'development' | 'production' | 'test';
API_BASE_URL: string;
API_TIMEOUT: number;
ENABLE_ANALYTICS: boolean;
LOG_LEVEL: 'debug' | 'info' | 'warn' | 'error';
VERSION: string;
BUILD_TIME: string;
}
class EnvironmentManager {
private static instance: EnvironmentManager;
private config: EnvironmentConfig;
private constructor() {
this.config = this.loadConfig();
this.validateConfig();
}
static getInstance(): EnvironmentManager {
if (!this.instance) {
this.instance = new EnvironmentManager();
}
return this.instance;
}
private loadConfig(): EnvironmentConfig {
return {
NODE_ENV: (process.env.NODE_ENV as any) || 'development',
API_BASE_URL: process.env.REACT_APP_API_BASE_URL || 'http://localhost:8000',
API_TIMEOUT: parseInt(process.env.REACT_APP_API_TIMEOUT || '5000', 10),
ENABLE_ANALYTICS: process.env.REACT_APP_ENABLE_ANALYTICS === 'true',
LOG_LEVEL: (process.env.REACT_APP_LOG_LEVEL as any) || 'info',
VERSION: process.env.REACT_APP_VERSION || '1.0.0',
BUILD_TIME: process.env.REACT_APP_BUILD_TIME || new Date().toISOString()
};
}
private validateConfig(): void {
const requiredVars = ['API_BASE_URL'];
const missingVars = requiredVars.filter(varName => !this.config[varName as keyof EnvironmentConfig]);
if (missingVars.length > 0) {
throw new Error(`Missing required environment variables: ${missingVars.join(', ')}`);
}
// 验证 URL 格式
try {
new URL(this.config.API_BASE_URL);
} catch {
throw new Error('Invalid API_BASE_URL format');
}
// 验证超时时间
if (this.config.API_TIMEOUT <= 0) {
throw new Error('API_TIMEOUT must be a positive number');
}
}
get<K extends keyof EnvironmentConfig>(key: K): EnvironmentConfig[K] {
return this.config[key];
}
getAll(): Readonly<EnvironmentConfig> {
return { ...this.config };
}
isDevelopment(): boolean {
return this.config.NODE_ENV === 'development';
}
isProduction(): boolean {
return this.config.NODE_ENV === 'production';
}
isTest(): boolean {
return this.config.NODE_ENV === 'test';
}
}
// 导出单例实例
export const env = EnvironmentManager.getInstance();
// 类型安全的环境变量访问
export const config = {
api: {
baseUrl: env.get('API_BASE_URL'),
timeout: env.get('API_TIMEOUT')
},
app: {
version: env.get('VERSION'),
buildTime: env.get('BUILD_TIME'),
enableAnalytics: env.get('ENABLE_ANALYTICS')
},
logging: {
level: env.get('LOG_LEVEL')
}
} as const;
8.5.2 多环境配置文件
# .env.development
REACT_APP_API_BASE_URL=http://localhost:8000
REACT_APP_API_TIMEOUT=10000
REACT_APP_ENABLE_ANALYTICS=false
REACT_APP_LOG_LEVEL=debug
# .env.production
REACT_APP_API_BASE_URL=https://api.example.com
REACT_APP_API_TIMEOUT=5000
REACT_APP_ENABLE_ANALYTICS=true
REACT_APP_LOG_LEVEL=error
# .env.test
REACT_APP_API_BASE_URL=http://localhost:3001
REACT_APP_API_TIMEOUT=3000
REACT_APP_ENABLE_ANALYTICS=false
REACT_APP_LOG_LEVEL=warn
# .env.local (本地开发覆盖)
REACT_APP_API_BASE_URL=http://localhost:9000
REACT_APP_ENABLE_ANALYTICS=false
// src/config/feature-flags.ts
interface FeatureFlags {
enableNewUI: boolean;
enableBetaFeatures: boolean;
enableExperimentalAPI: boolean;
maxFileUploadSize: number;
enableRealTimeUpdates: boolean;
}
class FeatureFlagManager {
private flags: FeatureFlags;
constructor() {
this.flags = this.loadFlags();
}
private loadFlags(): FeatureFlags {
// 可以从环境变量、远程配置或本地存储加载
return {
enableNewUI: process.env.REACT_APP_ENABLE_NEW_UI === 'true',
enableBetaFeatures: process.env.REACT_APP_ENABLE_BETA === 'true',
enableExperimentalAPI: process.env.REACT_APP_ENABLE_EXPERIMENTAL_API === 'true',
maxFileUploadSize: parseInt(process.env.REACT_APP_MAX_FILE_SIZE || '10485760', 10), // 10MB
enableRealTimeUpdates: process.env.REACT_APP_ENABLE_REALTIME === 'true'
};
}
isEnabled(flag: keyof FeatureFlags): boolean {
return Boolean(this.flags[flag]);
}
getValue<K extends keyof FeatureFlags>(flag: K): FeatureFlags[K] {
return this.flags[flag];
}
// 运行时更新功能标志(用于A/B测试等)
updateFlag<K extends keyof FeatureFlags>(flag: K, value: FeatureFlags[K]): void {
this.flags[flag] = value;
// 可以持久化到本地存储
localStorage.setItem(`feature_flag_${flag}`, JSON.stringify(value));
}
}
export const featureFlags = new FeatureFlagManager();
// 使用示例
export const useFeatureFlag = (flag: keyof FeatureFlags) => {
return featureFlags.isEnabled(flag);
};
8.6 本章练习
练习 1:创建项目脚手架
// 创建一个 TypeScript 项目脚手架生成器
// 支持以下功能:
// 1. 选择项目类型(React、Node.js、Library)
// 2. 选择包管理器(npm、yarn、pnpm)
// 3. 选择代码质量工具(ESLint、Prettier、Husky)
// 4. 选择测试框架(Jest、Vitest)
// 5. 自动生成配置文件
interface ScaffoldOptions {
projectName: string;
projectType: 'react' | 'node' | 'library';
packageManager: 'npm' | 'yarn' | 'pnpm';
includeESLint: boolean;
includePrettier: boolean;
includeHusky: boolean;
testFramework: 'jest' | 'vitest' | 'none';
includeCI: boolean;
}
class ProjectScaffold {
// 你的实现
}
// 使用示例
const scaffold = new ProjectScaffold();
await scaffold.generate({
projectName: 'my-awesome-project',
projectType: 'react',
packageManager: 'npm',
includeESLint: true,
includePrettier: true,
includeHusky: true,
testFramework: 'jest',
includeCI: true
});
练习 2:构建性能优化器
// 实现一个构建性能优化器
// 支持以下功能:
// 1. 分析构建时间
// 2. 检测大文件和重复依赖
// 3. 建议优化方案
// 4. 自动应用优化
interface BuildAnalysis {
buildTime: number;
bundleSize: number;
chunkSizes: Record<string, number>;
duplicateDependencies: string[];
largeAssets: Array<{ name: string; size: number }>;
suggestions: string[];
}
class BuildOptimizer {
// 你的实现
}
// 使用示例
const optimizer = new BuildOptimizer();
const analysis = await optimizer.analyze('./dist');
console.log('Build analysis:', analysis);
const optimizations = optimizer.getSuggestions(analysis);
const applied = await optimizer.applyOptimizations(optimizations);
console.log('Applied optimizations:', applied);
练习 3:配置管理系统
// 实现一个配置管理系统
// 支持以下功能:
// 1. 多环境配置
// 2. 配置验证
// 3. 配置热更新
// 4. 配置加密
// 5. 配置版本管理
interface ConfigSchema {
[key: string]: {
type: 'string' | 'number' | 'boolean' | 'object' | 'array';
required?: boolean;
default?: any;
validator?: (value: any) => boolean;
encrypted?: boolean;
};
}
class ConfigManager {
// 你的实现
}
// 使用示例
const configManager = new ConfigManager();
configManager.defineSchema({
apiUrl: {
type: 'string',
required: true,
validator: (value) => value.startsWith('https://')
},
apiKey: {
type: 'string',
required: true,
encrypted: true
},
timeout: {
type: 'number',
default: 5000,
validator: (value) => value > 0
}
});
const config = await configManager.load('production');
console.log('Loaded config:', config);
// 监听配置变化
configManager.onConfigChange((newConfig) => {
console.log('Config updated:', newConfig);
});
8.7 本章总结
本章详细介绍了 TypeScript 项目的工程化配置,包括:
- 项目结构设计:学习了标准的项目结构和模块化组织方式
- TypeScript 配置详解:掌握了 tsconfig.json 的完整配置和多配置文件管理
- 代码质量工具:配置了 ESLint、Prettier 和 Git hooks
- 构建和打包:学习了 Webpack 和 Vite 的配置和优化
- 环境配置管理:实现了环境变量管理和功能标志系统
良好的工程化配置是项目成功的基础,它能够提高开发效率、保证代码质量、简化部署流程。通过本章的学习,你应该能够搭建一个完整、可维护的 TypeScript 项目。
下一章我们将学习测试策略,了解如何为 TypeScript 项目编写全面的测试。