普通视图

发现新文章,点击刷新页面。
昨天 — 2025年11月6日首页

TypeScript泛型:让类型也"通用"的魔法

2025年11月6日 08:55

前言

大家好,我是小杨。还记得我刚学习TypeScript时,最让我头疼的就是泛型这个概念。什么TUK,看起来像密码一样神秘。但当我真正理解并开始使用泛型后,才发现它就像是TypeScript中的"瑞士军刀",能让我们的代码既灵活又类型安全。今天,我想和大家分享我对于TypeScript泛型的理解和实战经验。

什么是泛型?从函数参数到类型参数

想象一下,如果你要写一个函数,它既能处理数字,又能处理字符串,还能处理任何其他类型,你会怎么做?

在JavaScript中,我们可能会这样写:

// JavaScript方式 - 缺乏类型安全
function identity(value) {
    return value;
}

const num = identity(42);        // 返回42,但类型信息丢失了
const str = identity("hello");   // 返回"hello",类型信息丢失了

而在TypeScript中,泛型给了我们更好的解决方案:

typescript

// TypeScript泛型 - 保持类型安全
function identity<T>(value: T): T {
    return value;
}

const num = identity(42);        // 类型为 number
const str = identity("hello");   // 类型为 string
const bool = identity(true);     // 类型为 boolean

这里的<T>就是泛型参数,它像一个"类型变量",在函数被调用时确定具体的类型。

泛型基础:从简单到复杂

1. 泛型函数

让我们从一个实际的例子开始:

// 一个简单的栈实现
class Stack<T> {
    private items: T[] = [];
    
    push(item: T): void {
        this.items.push(item);
    }
    
    pop(): T | undefined {
        return this.items.pop();
    }
    
    peek(): T | undefined {
        return this.items[this.items.length - 1];
    }
    
    size(): number {
        return this.items.length;
    }
}

// 使用示例 - 类型安全!
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
// numberStack.push("hello"); // ❌ 编译错误:不能将字符串压入数字栈

const stringStack = new Stack<string>();
stringStack.push("hello");
stringStack.push("world");

2. 多个泛型参数

// 处理键值对的函数
function pair<K, V>(key: K, value: V): [K, V] {
    return [key, value];
}

// 使用示例
const stringNumberPair = pair("age", 25);      // [string, number]
const numberBooleanPair = pair(1, true);       // [number, boolean]
const complexPair = pair("config", { debug: true }); // [string, { debug: boolean }]

3. 泛型约束

有时候,我们需要对泛型参数做一些限制:

// 要求泛型参数必须有length属性
interface HasLength {
    length: number;
}

function getLength<T extends HasLength>(item: T): number {
    return item.length;
}

// 使用示例
getLength("hello");        // ✅ 字符串有length
getLength([1, 2, 3]);      // ✅ 数组有length  
getLength({ length: 5 });  // ✅ 对象有length属性
// getLength(42);          // ❌ 数字没有length属性

实战场景:泛型在项目中的应用

场景1:API响应处理

在我的实际项目中,泛型在API层发挥了巨大作用:

// 定义通用的API响应类型
interface ApiResponse<T> {
    success: boolean;
    data: T;
    message?: string;
    timestamp: number;
}

// 通用的API请求函数
async function apiRequest<T>(
    endpoint: string, 
    options?: RequestInit
): Promise<ApiResponse<T>> {
    const response = await fetch(`/api/${endpoint}`, options);
    const result: ApiResponse<T> = await response.json();
    return result;
}

// 定义具体的数据类型
interface User {
    id: number;
    name: string;
    email: string;
}

interface Product {
    id: number;
    title: string;
    price: number;
    category: string;
}

// 使用示例 - 完美的类型安全!
const userResponse = await apiRequest<User>("users/1");
console.log(userResponse.data.name);    // ✅ 正确的属性访问
// console.log(userResponse.data.invalid); // ❌ 编译错误

const productResponse = await apiRequest<Product>("products/123");
console.log(productResponse.data.price); // ✅ 正确的属性访问

场景2:工具函数库

泛型让工具函数变得更加通用和类型安全:

// 数组工具函数
function filterArray<T>(
    array: T[], 
    predicate: (item: T, index: number) => boolean
): T[] {
    return array.filter(predicate);
}

function mapArray<T, U>(
    array: T[], 
    mapper: (item: T, index: number) => U
): U[] {
    return array.map(mapper);
}

// 对象工具函数
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

function mergeObjects<T extends object, U extends object>(
    obj1: T, 
    obj2: U
): T & U {
    return { ...obj1, ...obj2 };
}

// 使用示例
const numbers = [1, 2, 3, 4, 5];
const evenNumbers = filterArray(numbers, n => n % 2 === 0); // number[]

const users = [
    { id: 1, name: "Alice" },
    { id: 2, name: "Bob" }
];
const userNames = mapArray(users, user => user.name); // string[]

const person = { name: "Alice", age: 30 };
const name = getProperty(person, "name"); // string
// const invalid = getProperty(person, "email"); // ❌ 编译错误

场景3:状态管理

在React项目中,泛型可以帮助我们创建类型安全的Hook:

import { useState, useCallback } from 'react';

// 通用的表单Hook
function useForm<T extends Record<string, any>>(initialValues: T) {
    const [values, setValues] = useState<T>(initialValues);
    
    const setValue = useCallback(<K extends keyof T>(key: K, value: T[K]) => {
        setValues(prev => ({ ...prev, [key]: value }));
    }, []);
    
    const reset = useCallback(() => {
        setValues(initialValues);
    }, [initialValues]);
    
    return {
        values,
        setValue,
        reset,
        setValues
    };
}

// 使用示例
interface LoginForm {
    email: string;
    password: string;
    rememberMe: boolean;
}

function LoginComponent() {
    const { values, setValue } = useForm<LoginForm>({
        email: "",
        password: "", 
        rememberMe: false
    });
    
    // 完全类型安全!
    const handleEmailChange = (email: string) => {
        setValue("email", email); // ✅ 正确
    };
    
    const handleRememberChange = (remember: boolean) => {
        setValue("rememberMe", remember); // ✅ 正确
    };
    
    // setValue("invalidKey", "value"); // ❌ 编译错误
    // setValue("email", 123);          // ❌ 编译错误
}

场景4:高阶组件和渲染Props

// 带加载状态的高阶组件
function withLoading<TProps extends object>(
    Component: React.ComponentType<TProps>
) {
    return function WithLoadingComponent(props: TProps & { isLoading?: boolean }) {
        const { isLoading, ...componentProps } = props;
        
        if (isLoading) {
            return <div>Loading...</div>;
        }
        
        return <Component {...componentProps as TProps} />;
    };
}

// 数据获取组件
interface DataRendererProps<T> {
    url: string;
    children: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode;
}

function DataRenderer<T>({ url, children }: DataRendererProps<T>) {
    const [data, setData] = useState<T | null>(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState<Error | null>(null);
    
    useEffect(() => {
        fetch(url)
            .then(response => response.json())
            .then((data: T) => {
                setData(data);
                setLoading(false);
            })
            .catch((err: Error) => {
                setError(err);
                setLoading(false);
            });
    }, [url]);
    
    return <>{children(data, loading, error)}</>;
}

// 使用示例
interface UserData {
    id: number;
    name: string;
    email: string;
}

function UserProfile() {
    return (
        <DataRenderer<UserData> url="/api/user/1">
            {(user, loading, error) => {
                if (loading) return <div>Loading user...</div>;
                if (error) return <div>Error: {error.message}</div>;
                if (user) return <div>Hello, {user.name}!</div>;
                return null;
            }}
        </DataRenderer>
    );
}

高级泛型技巧

1. 条件类型

// 根据条件选择类型
type IsString<T> = T extends string ? "yes" : "no";

type A = IsString<string>;    // "yes"
type B = IsString<number>;    // "no"

// 提取数组元素类型
type ArrayElement<T> = T extends (infer U)[] ? U : never;

type Numbers = ArrayElement<number[]>;      // number
type Strings = ArrayElement<string[]>;      // string
type Mixed = ArrayElement<(number | string)[]>; // number | string

2. 映射类型

// 让所有属性变为可选
type Partial<T> = {
    [P in keyof T]?: T[P];
};

// 让所有属性变为只读
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

// 实际应用
interface User {
    id: number;
    name: string;
    email: string;
}

type PartialUser = Partial<User>;
// 等价于 { id?: number; name?: string; email?: string; }

type ReadonlyUser = Readonly<User>;
// 等价于 { readonly id: number; readonly name: string; readonly email: string; }

3. 泛型工具类型实战

// 创建请求参数类型
interface ApiEndpoints {
    users: {
        GET: { id: number };
        POST: { name: string; email: string };
    };
    products: {
        GET: { category?: string };
        POST: { title: string; price: number };
    };
}

// 自动生成请求参数类型
type RequestParams<TEndpoint extends keyof ApiEndpoints, TMethod extends keyof ApiEndpoints[TEndpoint]> 
    = ApiEndpoints[TEndpoint][TMethod];

// 使用示例
type GetUserParams = RequestParams<"users", "GET">;     // { id: number }
type CreateUserParams = RequestParams<"users", "POST">; // { name: string; email: string }
type GetProductParams = RequestParams<"products", "GET">; // { category?: string }

常见陷阱和最佳实践

1. 不要过度使用泛型

// 不推荐:过度复杂的泛型
function overlyComplex<T extends Record<string, any>, K extends keyof T, U extends T[K]>(
    obj: T, 
    key: K, 
    transformer: (value: T[K]) => U
): U {
    return transformer(obj[key]);
}

// 推荐:保持简单
function getAndTransform<T, U>(
    obj: Record<string, T>,
    key: string,
    transformer: (value: T) => U
): U {
    return transformer(obj[key]);
}

2. 提供合理的默认值

// 为泛型参数提供默认值
interface PaginationOptions<T = any> {
    page: number;
    pageSize: number;
    filter?: (item: T) => boolean;
    sort?: (a: T, b: T) => number;
}

// 使用默认值
const defaultOptions: PaginationOptions = {
    page: 1,
    pageSize: 10
};

// 指定具体类型
const userOptions: PaginationOptions<User> = {
    page: 1,
    pageSize: 20,
    filter: user => user.active,
    sort: (a, b) => a.name.localeCompare(b.name)
};

3. 合理使用类型推断

// 让TypeScript自动推断类型
function createArray<T>(...items: T[]): T[] {
    return items;
}

// 自动推断为number[]
const numbers = createArray(1, 2, 3);
// 自动推断为string[] 
const strings = createArray("a", "b", "c");
// 自动推断为(string | number)[]
const mixed = createArray(1, "two", 3);

结语

泛型是TypeScript中最强大的特性之一,它让我们的代码在保持类型安全的同时,获得了极大的灵活性。从简单的工具函数到复杂的系统架构,泛型都能发挥重要作用。

记住学习泛型的关键:

  • 从简单的用例开始,逐步深入
  • 多实践,在真实项目中应用
  • 不要害怕犯错,TypeScript编译器会指导你

泛型就像编程中的"魔法",一旦掌握,你就会发现它能解决很多之前觉得棘手的问题。希望今天的分享能帮助你在TypeScript的道路上更进一步!

⭐  写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

TypeScript函数:给JavaScript函数加上"类型安全带"

2025年11月6日 08:42

前言

大家好,我是小杨。记得我刚从JavaScript转向TypeScript时,最让我惊喜的就是函数的类型安全特性。以前在JavaScript中,我经常遇到"undefined is not a function"这样的运行时错误,而TypeScript的函数特性就像给代码系上了安全带,让很多错误在编写阶段就能被发现。今天就来聊聊TypeScript中的函数,以及它和JavaScript函数的那些区别。

基础篇:TypeScript函数的基本用法

函数声明的类型化

在JavaScript中,我们这样写函数:

// JavaScript风格
function greet(name) {
    return `Hello, ${name}!`;
}

而在TypeScript中,我们可以为参数和返回值添加类型:

// TypeScript风格 - 为函数系上安全带
function greet(name: string): string {
    return `Hello, ${name}!`;
}

// 使用示例
const message = greet("Alice"); // ✅ 正确
// const errorMessage = greet(123); // ❌ 编译错误:参数类型不匹配

函数表达式与箭头函数

// 函数表达式
const add = function(x: number, y: number): number {
    return x + y;
};

// 箭头函数 - 我的最爱
const multiply = (x: number, y: number): number => x * y;

// 使用类型别名
type MathOperation = (a: number, b: number) => number;

const divide: MathOperation = (a, b) => a / b;

进阶特性:TypeScript函数的超能力

1. 可选参数和默认参数

// 可选参数
function createUser(
    name: string, 
    email: string, 
    age?: number  // 这个问号让参数变成可选的
): User {
    return {
        name,
        email,
        age: age || 0 // 处理可选参数
    };
}

// 默认参数
function sendMessage(
    content: string,
    priority: "low" | "medium" | "high" = "medium", // 默认值
    timeout: number = 5000
): void {
    console.log(`Sending: ${content}, Priority: ${priority}, Timeout: ${timeout}ms`);
}

// 使用示例
createUser("Alice", "alice@example.com"); // ✅ age是可选的
createUser("Bob", "bob@example.com", 25); // ✅ 也可以提供age

sendMessage("Hello"); // ✅ 使用默认参数
sendMessage("Urgent!", "high", 1000); // ✅ 自定义所有参数

2. 剩余参数

// 收集所有参数到一个数组中
function buildPath(...segments: string[]): string {
    return segments.join('/');
}

// 混合使用
function configureApp(
    name: string,
    ...settings: [string, any][]
): AppConfig {
    console.log(`Configuring app: ${name}`);
    settings.forEach(([key, value]) => {
        console.log(`Setting ${key} to ${value}`);
    });
    return { name, settings };
}

// 使用示例
const path = buildPath("usr", "local", "bin", "app"); // "usr/local/bin/app"
configureApp("MyApp", ["theme", "dark"], ["lang", "zh-CN"]);

3. 函数重载

这是TypeScript独有的强大特性:

// 重载签名
function processInput(input: string): string[];
function processInput(input: number): number[];
function processInput(input: boolean): boolean[];

// 实现签名
function processInput(input: any): any[] {
    if (typeof input === 'string') {
        return input.split('');
    } else if (typeof input === 'number') {
        return [input, input * 2, input * 3];
    } else {
        return [input, !input];
    }
}

// 使用示例 - 自动获得正确的类型提示!
const chars = processInput("hello"); // string[] 类型
const numbers = processInput(5);     // number[] 类型
const booleans = processInput(true); // boolean[] 类型

实战对比:TypeScript vs JavaScript函数

场景1:API请求函数

JavaScript版本:

// JavaScript - 容易出错
async function fetchUserData(userId) {
    const response = await fetch(`/api/users/${userId}`);
    const data = await response.json();
    return data; // 返回什么?我们不知道!
}

// 使用时可能会遇到问题
const userData = await fetchUserData(123);
console.log(userData.nonExistentProperty); // 运行时才报错!

TypeScript版本:

// TypeScript - 安全明确
interface User {
    id: number;
    name: string;
    email: string;
    avatar?: string;
}

async function fetchUserData(userId: number): Promise<User> {
    const response = await fetch(`/api/users/${userId}`);
    const data: User = await response.json();
    return data; // 明确的返回类型
}

// 使用时获得完整的类型安全
const userData = await fetchUserData(123);
console.log(userData.name); // ✅ 正确的属性
// console.log(userData.nonExistentProperty); // ❌ 编译时就报错!

场景2:回调函数处理

JavaScript版本:

// JavaScript - 容易出错
function processArray(arr, callback) {
    const result = [];
    for (let i = 0; i < arr.length; i++) {
        result.push(callback(arr[i]));
    }
    return result;
}

// 可能出现的错误
const numbers = [1, 2, 3];
const doubled = processArray(numbers, (num) => num * 2); // 正常工作
const problematic = processArray(numbers, "not a function"); // 运行时崩溃!

TypeScript版本:

// TypeScript - 类型安全
function processArray<T, U>(
    arr: T[],
    callback: (item: T, index: number) => U
): U[] {
    const result: U[] = [];
    for (let i = 0; i < arr.length; i++) {
        result.push(callback(arr[i], i));
    }
    return result;
}

// 使用示例 - 完全类型安全
const numbers = [1, 2, 3];
const doubled = processArray(numbers, (num) => num * 2); // number[] 类型
const strings = processArray(numbers, (num) => num.toString()); // string[] 类型

// const error = processArray(numbers, "not a function"); // ❌ 编译错误

场景3:配置对象处理

JavaScript版本:

// JavaScript - 配置容易出错
function createButton(config) {
    const defaultConfig = {
        text: "Button",
        color: "blue",
        size: "medium",
        disabled: false
    };
    
    return { ...defaultConfig, ...config };
}

// 可能的问题
const button1 = createButton({ text: "Click me", colour: "red" }); // 拼写错误,静默失败
const button2 = createButton({ size: "extra-large" }); // 无效的尺寸,运行时才可能发现

TypeScript版本:

// TypeScript - 配置安全
interface ButtonConfig {
    text?: string;
    color?: "blue" | "red" | "green" | "yellow";
    size?: "small" | "medium" | "large";
    disabled?: boolean;
}

function createButton(config: ButtonConfig) {
    const defaultConfig: Required<ButtonConfig> = {
        text: "Button",
        color: "blue",
        size: "medium",
        disabled: false
    };
    
    return { ...defaultConfig, ...config };
}

// 使用示例 - 即时错误检测
const button1 = createButton({ 
    text: "Click me", 
    color: "red",  // ✅ 有效颜色
    size: "large"  // ✅ 有效尺寸
});

// const button2 = createButton({ colour: "red" }); // ❌ 拼写错误,编译时报错
// const button3 = createButton({ size: "extra-large" }); // ❌ 无效尺寸,编译时报错

高级特性:TypeScript函数的独特能力

1. 泛型函数

// 泛型让函数更加灵活
function identity<T>(value: T): T {
    return value;
}

// 自动类型推断
const num = identity(42);        // number 类型
const str = identity("hello");   // string 类型
const bool = identity(true);     // boolean 类型

// 泛型约束
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

const user = { name: "Alice", age: 30 };
const userName = getProperty(user, "name"); // string 类型
const userAge = getProperty(user, "age");   // number 类型
// const invalid = getProperty(user, "email"); // ❌ 编译错误

2. 条件类型与函数

// 基于输入类型的条件返回
type ApiResponse<T> = T extends number 
    ? { data: number; type: "number" }
    : T extends string
    ? { data: string; type: "string" }
    : { data: T; type: "object" };

function createResponse<T>(data: T): ApiResponse<T> {
    if (typeof data === "number") {
        return { data, type: "number" } as ApiResponse<T>;
    } else if (typeof data === "string") {
        return { data, type: "string" } as ApiResponse<T>;
    } else {
        return { data, type: "object" } as ApiResponse<T>;
    }
}

// 自动推断正确的返回类型
const numResponse = createResponse(42);    // { data: number; type: "number" }
const strResponse = createResponse("hello"); // { data: string; type: "string" }

3. 函数类型的高级用法

// 函数组合
type FunctionType<T, R> = (arg: T) => R;

function compose<T, U, R>(
    f: FunctionType<U, R>,
    g: FunctionType<T, U>
): FunctionType<T, R> {
    return (x: T) => f(g(x));
}

// 使用组合
const addFive = (x: number) => x + 5;
const multiplyByTwo = (x: number) => x * 2;
const toString = (x: number) => x.toString();

const processNumber = compose(toString, compose(addFive, multiplyByTwo));
const result = processNumber(10); // "25"

最佳实践和注意事项

1. 合理使用any和unknown

// 不推荐:过度使用any
function dangerousFunction(data: any): any {
    // 这里可能发生任何事!
    return data.someProperty.someMethod();
}

// 推荐:使用unknown进行类型安全处理
function safeFunction(data: unknown): string {
    if (typeof data === 'string') {
        return data.toUpperCase();
    } else if (data && typeof data === 'object' && 'message' in data) {
        return String(data.message);
    }
    return "Unknown data";
}

2. 充分利用类型推断

// TypeScript可以推断返回类型,不需要总是显式声明
function calculateTotal(price: number, quantity: number) {
    return price * quantity; // 自动推断返回number类型
}

// 但对于复杂逻辑,显式声明更好
function processOrder(order: Order): ProcessResult {
    // 复杂的处理逻辑...
    return result;
}

3. 错误处理的最佳实践

// 使用Result模式而不是抛出错误
type Result<T, E = Error> = 
    | { success: true; data: T }
    | { success: false; error: E };

function safeDivide(a: number, b: number): Result<number> {
    if (b === 0) {
        return { success: false, error: new Error("Division by zero") };
    }
    return { success: true, data: a / b };
}

// 使用示例
const result = safeDivide(10, 2);
if (result.success) {
    console.log(result.data); // 5
} else {
    console.error(result.error.message);
}

总结:TypeScript函数的优势

通过上面的对比和实践,我们可以看到TypeScript函数相比JavaScript函数的主要优势:

  1. 类型安全:在编译时捕获类型错误
  2. 更好的智能提示:IDE可以提供准确的参数和返回类型提示
  3. 自文档化:函数签名本身就是很好的文档
  4. 重构友好:类型系统帮助安全地进行代码重构
  5. 团队协作:明确的接口约定减少沟通成本

结语

从JavaScript的"自由奔放"到TypeScript的"规范有序",函数的类型化可能是最有价值的改进之一。它就像给我们的代码加上了安全带,虽然一开始可能觉得有些束缚,但一旦习惯,就会发现它能避免很多潜在的事故。

记住,好的TypeScript代码不是一味地添加类型,而是找到类型安全和开发效率的最佳平衡点。希望今天的分享能帮助你在实际项目中更好地使用TypeScript函数!

⭐  写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

昨天以前首页

TypeScript枚举:让你的代码更有"选择权"

2025年11月5日 08:52

前言

大家好,我是小杨。在平时的开发中,我经常遇到需要定义一组相关常量的情况。比如订单状态、用户角色、颜色主题等等。最开始我都是用普通的常量来定义,直到我发现了TypeScript的枚举(Enum)特性,它让我的代码变得更加清晰和类型安全。今天就来和大家聊聊这个既实用又有趣的特性。

什么是枚举?从选择题到代码

想象一下你在做选择题:A、B、C、D四个选项,每个选项都有明确的含义。枚举就是代码世界中的"选择题",它让我们可以定义一组命名的常量,让代码更易读、更安全。

第一个枚举例子

让我从一个最简单的例子开始:

// 定义一个表示星期的枚举
enum DayOfWeek {
  Sunday,
  Monday,
  Tuesday,
  Wednesday,
  Thursday,
  Friday,
  Saturday
}

// 使用枚举
const today: DayOfWeek = DayOfWeek.Wednesday;
const isWeekend = (day: DayOfWeek): boolean => {
  return day === DayOfWeek.Saturday || day === DayOfWeek.Sunday;
};

console.log(isWeekend(today)); // 输出: false
console.log(isWeekend(DayOfWeek.Sunday)); // 输出: true

看到这里你可能会有疑问:这些枚举值对应的是什么?让我打印出来看看:

console.log(DayOfWeek.Sunday);    // 输出: 0
console.log(DayOfWeek.Monday);    // 输出: 1
console.log(DayOfWeek.Tuesday);   // 输出: 2

默认情况下,枚举成员从0开始自动编号。但我们可以自定义这些值。

枚举的多种用法

1. 数字枚举

// 自定义数字值
enum HttpStatus {
  OK = 200,
  Created = 201,
  BadRequest = 400,
  Unauthorized = 401,
  NotFound = 404,
  InternalServerError = 500
}

function handleResponse(status: HttpStatus) {
  switch (status) {
    case HttpStatus.OK:
      console.log("请求成功");
      break;
    case HttpStatus.NotFound:
      console.log("资源未找到");
      break;
    case HttpStatus.InternalServerError:
      console.log("服务器错误");
      break;
    default:
      console.log("其他状态码:", status);
  }
}

// 使用
handleResponse(HttpStatus.OK); // 输出: 请求成功

2. 字符串枚举

在实际项目中,我更喜欢使用字符串枚举,因为它们更容易调试:

enum UserRole {
  Admin = "ADMIN",
  Editor = "EDITOR", 
  Viewer = "VIEWER",
  Guest = "GUEST"
}

enum ApiEndpoints {
  Login = "/api/auth/login",
  Logout = "/api/auth/logout",
  Users = "/api/users",
  Products = "/api/products"
}

function checkPermission(role: UserRole): boolean {
  return role === UserRole.Admin || role === UserRole.Editor;
}

// 使用
const currentUserRole = UserRole.Admin;
console.log(checkPermission(currentUserRole)); // 输出: true
console.log(ApiEndpoints.Login); // 输出: "/api/auth/login"

3. 常量枚举

如果你关心性能,常量枚举是个不错的选择:

const enum Direction {
  Up = "UP",
  Down = "DOWN", 
  Left = "LEFT",
  Right = "RIGHT"
}

// 使用常量枚举
const move = (dir: Direction) => {
  console.log(`Moving ${dir}`);
};

move(Direction.Up); // 编译后:move("UP")

常量枚举在编译时会被完全删除,只保留具体的值,可以减少代码体积。

4. 异构枚举

虽然不常用,但TypeScript也支持数字和字符串混合的枚举:

enum MixedEnum {
  No = 0,
  Yes = "YES",
  Maybe = 2
}

实战场景:枚举在项目中的应用

场景1:状态管理

在我的电商项目中,枚举在订单状态管理中发挥了重要作用:

enum OrderStatus {
  Pending = "PENDING",
  Confirmed = "CONFIRMED", 
  Processing = "PROCESSING",
  Shipped = "SHIPPED",
  Delivered = "DELIVERED",
  Cancelled = "CANCELLED",
  Refunded = "REFUNDED"
}

class Order {
  constructor(
    public id: number,
    public status: OrderStatus,
    public createdAt: Date
  ) {}
  
  canBeCancelled(): boolean {
    const cancellableStatuses = [
      OrderStatus.Pending,
      OrderStatus.Confirmed,
      OrderStatus.Processing
    ];
    return cancellableStatuses.includes(this.status);
  }
  
  getStatusText(): string {
    const statusTexts = {
      [OrderStatus.Pending]: "待处理",
      [OrderStatus.Confirmed]: "已确认",
      [OrderStatus.Processing]: "处理中",
      [OrderStatus.Shipped]: "已发货", 
      [OrderStatus.Delivered]: "已送达",
      [OrderStatus.Cancelled]: "已取消",
      [OrderStatus.Refunded]: "已退款"
    };
    return statusTexts[this.status];
  }
}

// 使用示例
const order = new Order(1, OrderStatus.Pending, new Date());
console.log(order.canBeCancelled()); // 输出: true
console.log(order.getStatusText());  // 输出: 待处理

场景2:配置系统

在应用配置中,枚举让配置更加类型安全:

enum Environment {
  Development = "development",
  Staging = "staging",
  Production = "production"
}

enum LogLevel {
  Error = "error",
  Warn = "warn", 
  Info = "info",
  Debug = "debug"
}

enum Theme {
  Light = "light",
  Dark = "dark",
  Auto = "auto"
}

class AppConfig {
  constructor(
    public env: Environment,
    public logLevel: LogLevel,
    public theme: Theme,
    public features: {
      analytics: boolean;
      notifications: boolean;
    }
  ) {}
  
  isProduction(): boolean {
    return this.env === Environment.Production;
  }
  
  shouldLogDebug(): boolean {
    return this.logLevel === LogLevel.Debug;
  }
}

// 使用示例
const config = new AppConfig(
  Environment.Development,
  LogLevel.Debug,
  Theme.Auto,
  { analytics: true, notifications: false }
);

console.log(config.isProduction()); // 输出: false

场景3:权限系统

在用户权限管理中,枚举让权限检查更加清晰:

enum Permission {
  ReadUsers = "users:read",
  WriteUsers = "users:write", 
  DeleteUsers = "users:delete",
  ReadProducts = "products:read",
  WriteProducts = "products:write",
  DeleteProducts = "products:delete",
  ManageOrders = "orders:manage"
}

enum UserRole {
  SuperAdmin = "SUPER_ADMIN",
  Admin = "ADMIN",
  Manager = "MANAGER", 
  Customer = "CUSTOMER"
}

class PermissionManager {
  private rolePermissions: Record<UserRole, Permission[]> = {
    [UserRole.SuperAdmin]: Object.values(Permission),
    [UserRole.Admin]: [
      Permission.ReadUsers,
      Permission.WriteUsers,
      Permission.ReadProducts, 
      Permission.WriteProducts,
      Permission.ManageOrders
    ],
    [UserRole.Manager]: [
      Permission.ReadProducts,
      Permission.WriteProducts,
      Permission.ManageOrders
    ],
    [UserRole.Customer]: [
      Permission.ReadProducts
    ]
  };
  
  hasPermission(role: UserRole, permission: Permission): boolean {
    return this.rolePermissions[role]?.includes(permission) || false;
  }
  
  getPermissionsForRole(role: UserRole): Permission[] {
    return this.rolePermissions[role] || [];
  }
}

// 使用示例
const permissionManager = new PermissionManager();
const canDeleteUsers = permissionManager.hasPermission(
  UserRole.Admin, 
  Permission.DeleteUsers
);
console.log(canDeleteUsers); // 输出: false

场景4:国际化

在多语言项目中,枚举可以帮助管理语言选项:

enum Language {
  English = "en",
  Chinese = "zh",
  Japanese = "ja",
  Korean = "ko",
  French = "fr"
}

enum Currency {
  USD = "USD",
  CNY = "CNY", 
  JPY = "JPY",
  EUR = "EUR",
  KRW = "KRW"
}

class LocalizationService {
  private translations: Record<Language, Record<string, string>> = {
    [Language.English]: {
      welcome: "Welcome",
      goodbye: "Goodbye",
      error: "An error occurred"
    },
    [Language.Chinese]: {
      welcome: "欢迎",
      goodbye: "再见", 
      error: "发生错误"
    }
    // 其他语言...
  };
  
  constructor(private language: Language) {}
  
  setLanguage(language: Language): void {
    this.language = language;
  }
  
  translate(key: string): string {
    return this.translations[this.language]?.[key] || key;
  }
  
  formatCurrency(amount: number, currency: Currency): string {
    const formatters: Record<Currency, string> = {
      [Currency.USD]: `$${amount.toFixed(2)}`,
      [Currency.CNY]: ${amount.toFixed(2)}`,
      [Currency.JPY]: ${amount}`,
      [Currency.EUR]: `€${amount.toFixed(2)}`,
      [Currency.KRW]: `₩${amount.toLocaleString()}`
    };
    return formatters[currency];
  }
}

// 使用示例
const i18n = new LocalizationService(Language.Chinese);
console.log(i18n.translate("welcome")); // 输出: 欢迎
console.log(i18n.formatCurrency(100, Currency.CNY)); // 输出: ¥100.00

枚举的最佳实践

1. 使用字符串枚举提高可读性

// 推荐:字符串枚举
enum UserAction {
  Login = "USER_LOGIN",
  Logout = "USER_LOGOUT", 
  UpdateProfile = "USER_UPDATE_PROFILE"
}

// 不推荐:数字枚举(调试时难以理解)
enum UserActionOld {
  Login,    // 0
  Logout,   // 1
  UpdateProfile // 2
}

2. 使用常量枚举优化性能

// 在性能敏感的场景使用常量枚举
const enum ResponseCode {
  Success = 200,
  NotFound = 404,
  ServerError = 500
}

// 编译后,这些枚举引用会被替换为具体数值
const code = ResponseCode.Success; // 编译为: const code = 200

3. 避免异构枚举

// 不推荐:混合数字和字符串
enum BadExample {
  A = 1,
  B = "B",
  C = 2
}

// 推荐:保持一致性
enum GoodExample {
  A = "A",
  B = "B", 
  C = "C"
}

enum GoodExample2 {
  A = 1,
  B = 2,
  C = 3
}

4. 使用命名空间扩展枚举

enum NotificationType {
  Email = "EMAIL",
  SMS = "SMS",
  Push = "PUSH"
}

namespace NotificationType {
  export function isUrgent(type: NotificationType): boolean {
    return type === NotificationType.SMS || type === NotificationType.Push;
  }
  
  export function getChannels(): NotificationType[] {
    return Object.values(NotificationType);
  }
}

// 使用示例
console.log(NotificationType.isUrgent(NotificationType.Email)); // false
console.log(NotificationType.getChannels()); // ["EMAIL", "SMS", "PUSH"]

枚举的替代方案

虽然枚举很强大,但在某些情况下,使用联合类型可能更合适:

// 使用联合类型
type OrderStatus = "PENDING" | "CONFIRMED" | "SHIPPED" | "DELIVERED";

// 使用对象常量
const ORDER_STATUS = {
  PENDING: "PENDING",
  CONFIRMED: "CONFIRMED", 
  SHIPPED: "SHIPPED",
  DELIVERED: "DELIVERED"
} as const;

type OrderStatusType = typeof ORDER_STATUS[keyof typeof ORDER_STATUS];

选择建议:

  • 需要迭代枚举成员时:使用枚举
  • 只需要类型安全时:使用联合类型
  • 需要同时使用值和类型时:使用对象常量 + 类型

结语

枚举是TypeScript中一个非常实用的特性,它让我们的代码更加清晰、安全。通过合理使用枚举,我们可以更好地表达业务逻辑中的各种状态和选项。

记住,工具虽好,但也要用在合适的地方。希望今天的分享能帮助你在实际项目中更好地使用TypeScript枚举!

⭐  写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

TypeScript类:面向对象编程的超级武器

2025年11月5日 08:44

前言

大家好,我是小杨。还记得我刚接触编程时,面对"类"这个概念总是觉得有些抽象。直到在TypeScript中深入使用类之后,我才真正体会到面向对象编程的魅力。今天,我想和大家分享我在实际项目中运用TypeScript类的一些心得和技巧。

类的基础:从蓝图到实物

想象一下你要建房子,首先需要一张蓝图,这张蓝图定义了房子的结构:有几个房间、门窗的位置、水电布局等。然后根据这张蓝图,你可以建造出很多具体的房子。

TypeScript中的类就是这样的"蓝图",而根据类创建的对象就是一栋栋具体的"房子"。

最简单的类

让我从一个最简单的例子开始:

class Car { 
  brand: string;
  color: string;
  
  constructor(brand: string, color: string) {
    this.brand = brand;
    this.color = color;
  }
  
  drive() {
    console.log(`The ${this.color} ${this.brand} is driving.`);
  }
}

// 创建类的实例
const myCar = new Car("Toyota", "red");
myCar.drive(); // 输出: The red Toyota is driving.

这就是最基本的类:它有属性(brand、color)和方法(drive)。

类的进阶特性

1. 访问修饰符:控制可见性

访问修饰符就像房子的访问权限:有些区域对所有访客开放,有些只对家人开放,有些则是私密空间。

class BankAccount {
  public readonly accountNumber: string;  // 公开但只读
  private balance: number;                // 完全私有
  protected owner: string;                // 受保护的
  
  constructor(accountNumber: string, initialBalance: number, owner: string) {
    this.accountNumber = accountNumber;
    this.balance = initialBalance;
    this.owner = owner;
  }
  
  public deposit(amount: number): void {
    if (amount > 0) {
      this.balance += amount;
      console.log(`Deposited $${amount}. New balance: $${this.balance}`);
    }
  }
  
  public withdraw(amount: number): boolean {
    if (amount > 0 && amount <= this.balance) {
      this.balance -= amount;
      console.log(`Withdrew $${amount}. Remaining balance: $${this.balance}`);
      return true;
    }
    console.log("Insufficient funds");
    return false;
  }
  
  public getBalance(): number {
    return this.balance;
  }
}

// 使用示例
const myAccount = new BankAccount("123456", 1000, "Alice");
myAccount.deposit(500);    // ✅ 允许
console.log(myAccount.accountNumber); // ✅ 允许
// myAccount.balance = 9999; // ❌ 编译错误:balance是私有的
// console.log(myAccount.balance); // ❌ 编译错误

2. 继承:代码复用的艺术

继承让类可以建立在现有类的基础上,就像子承父业一样。

class Animal {
  constructor(public name: string, protected age: number) {}
  
  speak() {
    console.log(`${this.name} makes a sound.`);
  }
  
  sleep() {
    console.log(`${this.name} is sleeping.`);
  }
}

class Dog extends Animal {
  private breed: string;
  
  constructor(name: string, age: number, breed: string) {
    super(name, age); // 调用父类的constructor
    this.breed = breed;
  }
  
  // 重写父类方法
  speak() {
    console.log(`${this.name} barks!`);
  }
  
  // 子类特有方法
  fetch() {
    console.log(`${this.name} is fetching the ball.`);
  }
  
  getInfo() {
    return `${this.name} is a ${this.breed} and ${this.age} years old.`;
  }
}

// 使用示例
const myDog = new Dog("Buddy", 3, "Golden Retriever");
myDog.speak();  // 输出: Buddy barks!
myDog.sleep();  // 输出: Buddy is sleeping.
myDog.fetch();  // 输出: Buddy is fetching the ball.
console.log(myDog.getInfo());

3. 抽象类:定义框架

抽象类就像建筑的设计规范,它定义了结构但不提供完整实现。

abstract class Shape {
  constructor(public name: string) {}
  
  // 抽象方法,必须在子类中实现
  abstract calculateArea(): number;
  
  // 具体方法
  displayInfo() {
    console.log(`Shape: ${this.name}, Area: ${this.calculateArea()}`);
  }
}

class Circle extends Shape {
  constructor(private radius: number) {
    super("Circle");
  }
  
  calculateArea(): number {
    return Math.PI * this.radius * this.radius;
  }
}

class Rectangle extends Shape {
  constructor(private width: number, private height: number) {
    super("Rectangle");
  }
  
  calculateArea(): number {
    return this.width * this.height;
  }
}

// 使用示例
const circle = new Circle(5);
const rectangle = new Rectangle(4, 6);

circle.displayInfo();    // 输出: Shape: Circle, Area: 78.53981633974483
rectangle.displayInfo(); // 输出: Shape: Rectangle, Area: 24

实战场景:类在项目中的应用

场景1:数据模型层

在我的电商项目中,类帮助我们构建了清晰的数据模型:

class Product {
  constructor(
    public id: number,
    public name: string,
    public price: number,
    private stock: number
  ) {}
  
  // 业务方法
  reduceStock(quantity: number): boolean {
    if (this.stock >= quantity) {
      this.stock -= quantity;
      return true;
    }
    return false;
  }
  
  addStock(quantity: number): void {
    this.stock += quantity;
  }
  
  getStock(): number {
    return this.stock;
  }
  
  // 静态方法
  static createFromData(data: any): Product {
    return new Product(data.id, data.name, data.price, data.stock);
  }
}

class ShoppingCart {
  private items: Map<number, { product: Product; quantity: number }> = new Map();
  
  addItem(product: Product, quantity: number): void {
    if (product.reduceStock(quantity)) {
      const existingItem = this.items.get(product.id);
      if (existingItem) {
        existingItem.quantity += quantity;
      } else {
        this.items.set(product.id, { product, quantity });
      }
    }
  }
  
  removeItem(productId: number): void {
    const item = this.items.get(productId);
    if (item) {
      item.product.addStock(item.quantity);
      this.items.delete(productId);
    }
  }
  
  getTotal(): number {
    let total = 0;
    for (const [_, item] of this.items) {
      total += item.product.price * item.quantity;
    }
    return total;
  }
  
  checkout(): Order {
    return new Order(this);
  }
}

class Order {
  private orderId: string;
  private orderDate: Date;
  
  constructor(private cart: ShoppingCart) {
    this.orderId = this.generateOrderId();
    this.orderDate = new Date();
  }
  
  private generateOrderId(): string {
    return `ORD-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  }
}

场景2:UI组件系统

在React项目中,类组件虽然现在较少使用,但在某些场景下仍然很有价值:

abstract class BaseComponent<P = {}, S = {}> {
  protected state: S;
  protected props: P;
  
  constructor(props: P) {
    this.props = props;
    this.state = {} as S;
  }
  
  abstract render(): string;
  
  setState(newState: Partial<S>): void {
    this.state = { ...this.state, ...newState };
    this.onStateUpdate();
  }
  
  protected onStateUpdate(): void {
    // 触发重新渲染的逻辑
    console.log("State updated, triggering re-render");
  }
}

class CounterComponent extends BaseComponent<{ initialCount: number }, { count: number }> {
  constructor(props: { initialCount: number }) {
    super(props);
    this.state = { count: props.initialCount };
  }
  
  increment = (): void => {
    this.setState({ count: this.state.count + 1 });
  };
  
  decrement = (): void => {
    this.setState({ count: this.state.count - 1 });
  };
  
  render(): string {
    return `
      <div class="counter">
        <h2>Count: ${this.state.count}</h2>
        <button onclick="counter.increment()">+</button>
        <button onclick="counter.decrement()">-</button>
      </div>
    `;
  }
}

// 使用示例
const counter = new CounterComponent({ initialCount: 0 });
console.log(counter.render());

场景3:服务层和工具类

在服务层设计中,类提供了很好的封装:

class ApiService {
  private baseURL: string;
  private token: string | null = null;
  
  constructor(baseURL: string) {
    this.baseURL = baseURL;
  }
  
  setToken(token: string): void {
    this.token = token;
  }
  
  private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
    const url = `${this.baseURL}${endpoint}`;
    const headers: HeadersInit = {
      'Content-Type': 'application/json',
      ...options.headers,
    };
    
    if (this.token) {
      headers['Authorization'] = `Bearer ${this.token}`;
    }
    
    try {
      const response = await fetch(url, {
        ...options,
        headers,
      });
      
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      
      return await response.json();
    } catch (error) {
      console.error('API request failed:', error);
      throw error;
    }
  }
  
  async get<T>(endpoint: string): Promise<T> {
    return this.request<T>(endpoint);
  }
  
  async post<T>(endpoint: string, data: any): Promise<T> {
    return this.request<T>(endpoint, {
      method: 'POST',
      body: JSON.stringify(data),
    });
  }
}

// 单例模式的应用
class ConfigManager {
  private static instance: ConfigManager;
  private config: Map<string, any> = new Map();
  
  private constructor() {
    // 私有构造函数,防止外部实例化
  }
  
  static getInstance(): ConfigManager {
    if (!ConfigManager.instance) {
      ConfigManager.instance = new ConfigManager();
    }
    return ConfigManager.instance;
  }
  
  set(key: string, value: any): void {
    this.config.set(key, value);
  }
  
  get(key: string): any {
    return this.config.get(key);
  }
}

最佳实践和注意事项

1. 组合优于继承

// 不推荐:过度使用继承
class AnimalWithSwim extends Animal {
  swim() { /* ... */ }
}

class AnimalWithFly extends Animal {
  fly() { /* ... */ }
}

class AnimalWithBoth extends Animal {
  swim() { /* ... */ }
  fly() { /* ... */ }
}

// 推荐:使用组合
class Swimmer {
  swim() {
    console.log("Swimming...");
  }
}

class Flyer {
  fly() {
    console.log("Flying...");
  }
}

class Duck extends Animal {
  private swimmer = new Swimmer();
  private flyer = new Flyer();
  
  swim() {
    this.swimmer.swim();
  }
  
  fly() {
    this.flyer.fly();
  }
}

2. 合理使用Getter和Setter

class User {
  private _email: string = "";
  
  constructor(public name: string) {}
  
  get email(): string {
    return this._email;
  }
  
  set email(value: string) {
    if (this.isValidEmail(value)) {
      this._email = value;
    } else {
      throw new Error("Invalid email address");
    }
  }
  
  private isValidEmail(email: string): boolean {
    const emailRegex = /^[^\s@]+@[^\s@]+.[^\s@]+$/;
    return emailRegex.test(email);
  }
}

const user = new User("Alice");
user.email = "alice@example.com"; // ✅ 有效
// user.email = "invalid-email";   // ❌ 抛出错误

3. 接口与类的结合

interface IRepository<T> {
  findById(id: number): T | null;
  save(entity: T): void;
  delete(id: number): boolean;
}

class UserRepository implements IRepository<User> {
  private users: Map<number, User> = new Map();
  private nextId: number = 1;
  
  findById(id: number): User | null {
    return this.users.get(id) || null;
  }
  
  save(user: User): void {
    if (!user.id) {
      user.id = this.nextId++;
    }
    this.users.set(user.id, user);
  }
  
  delete(id: number): boolean {
    return this.users.delete(id);
  }
}

结语

TypeScript的类为我们提供了强大的面向对象编程能力,从简单的数据封装到复杂的系统架构,类都能发挥重要作用。通过合理的类设计,我们可以创建出更加健壮、可维护的应用程序。

记住,类的设计不是越多越好,而是要找到适合项目需求的平衡点。希望今天的分享能帮助你在实际项目中更好地运用TypeScript类!

⭐  写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

TypeScript接口:打造你的代码“契约”之道

2025年11月5日 08:41

前言

大家好,我是小杨。在日常的TypeScript开发中,接口(Interface)是我最得力的助手之一。它就像一份严谨的"契约",让我的代码更加可靠和可维护。今天就来和大家聊聊这个看似简单却威力强大的特性。

什么是接口?从生活到代码的类比

想象一下,你要购买一台笔记本电脑。你不需要知道内部每个零件的具体品牌,但你肯定关心:必须有键盘、屏幕、USB接口、电池等。这就是一种"接口"约定——定义了设备应该具备的能力,而不关心具体实现。

在TypeScript中,接口也是这样的存在:它定义了对象应该长什么样子,类应该具备什么方法,但不包含具体的实现细节。

基础用法:从简单对象开始

让我从一个最简单的例子开始:

// 定义一个用户接口
interface IUser {
  id: number;
  name: string;
  email: string;
  age?: number; // 可选属性
}

// 使用接口
const myUser: IUser = {
  id: 1,
  name: "Alice",
  email: "alice@example.com"
  // age是可选的,所以可以不写
};

// 如果缺少必需属性,TypeScript会报错
const invalidUser: IUser = {
  id: 2,
  name: "Bob"
  // 缺少email,编译时会报错
};

在这个例子中,我定义了一个用户接口,任何声称自己是IUser的对象都必须包含idnameemail这三个属性。

接口的进阶玩法

1. 函数类型的接口

接口不仅可以描述对象,还可以描述函数:

interface ISearchFunc {
  (source: string, keyword: string): boolean;
}

// 使用函数接口
const mySearch: ISearchFunc = function(src, kw) {
  return src.indexOf(kw) > -1;
};

// 测试
console.log(mySearch("hello world", "hello")); // true

2. 可索引类型的接口

当我们需要处理数组或字典时,索引接口就派上用场了:

interface IStringArray {
  [index: number]: string;
}

interface IUserDictionary {
  [key: string]: IUser;
}

const usersArray: IStringArray = ["Alice", "Bob", "Charlie"];
const usersDict: IUserDictionary = {
  "user1": { id: 1, name: "Alice", email: "alice@example.com" },
  "user2": { id: 2, name: "Bob", email: "bob@example.com" }
};

3. 接口继承

接口可以继承其他接口,这在构建复杂类型系统时特别有用:

interface IPerson {
  name: string;
  age: number;
}

interface IEmployee extends IPerson {
  employeeId: string;
  department: string;
}

interface IManager extends IEmployee {
  teamSize: number;
}

// 现在IManager必须包含所有继承链上的属性
const myManager: IManager = {
  name: "Carol",
  age: 35,
  employeeId: "E002",
  department: "Engineering",
  teamSize: 8
};

实战场景:接口在项目中的应用

场景1:API响应数据建模

在我最近的项目中,接口在处理API响应时发挥了巨大作用:

// 定义API响应接口
interface IApiResponse<T> {
  code: number;
  message: string;
  data: T;
  timestamp: number;
}

interface IProduct {
  id: number;
  title: string;
  price: number;
  inventory: number;
}

interface IOrder {
  orderId: string;
  products: IProduct[];
  totalAmount: number;
}

// 使用泛型接口
async function fetchOrder(orderId: string): Promise<IApiResponse<IOrder>> {
  const response = await fetch(`/api/orders/${orderId}`);
  const result: IApiResponse<IOrder> = await response.json();
  return result;
}

// 使用时获得完整的类型提示
const orderResponse = await fetchOrder("123");
console.log(orderResponse.data.products[0].title); // 完整的类型安全!

场景2:组件Props的类型定义

在React项目中,我用接口来定义组件Props:

interface IButtonProps {
  text: string;
  onClick: () => void;
  variant?: 'primary' | 'secondary' | 'danger';
  disabled?: boolean;
  size?: 'small' | 'medium' | 'large';
}

const MyButton: React.FC<IButtonProps> = ({ 
  text, 
  onClick, 
  variant = 'primary',
  disabled = false,
  size = 'medium'
}) => {
  return (
    <button
      className={`btn btn-${variant} btn-${size}`}
      onClick={onClick}
      disabled={disabled}
    >
      {text}
    </button>
  );
};

// 使用组件时获得自动补全和类型检查
<MyButton 
  text="点击我"
  onClick={() => console.log("clicked")}
  variant="primary"
  size="large"
/>

场景3:配置对象类型安全

在应用配置管理中,接口确保配置的正确性:

interface IAppConfig {
  api: {
    baseURL: string;
    timeout: number;
    retries: number;
  };
  features: {
    darkMode: boolean;
    analytics: boolean;
    notifications: boolean;
  };
  ui: {
    theme: 'light' | 'dark' | 'auto';
    language: string;
  };
}

const appConfig: IAppConfig = {
  api: {
    baseURL: "https://api.mysite.com",
    timeout: 5000,
    retries: 3
  },
  features: {
    darkMode: true,
    analytics: true,
    notifications: false
  },
  ui: {
    theme: 'auto',
    language: 'zh-CN'
  }
};

// 如果有人误写配置,TypeScript会立即提示
const wrongConfig: IAppConfig = {
  api: {
    baseURL: "https://api.mysite.com",
    timeout: "5000", // 错误:应该是number而不是string
    retries: 3
  },
  // ... 其他配置
};

接口 vs 类型别名:如何选择?

很多初学者会困惑于接口和类型别名的区别,这里是我的使用经验:

// 接口 - 适合对象类型,支持继承
interface IPoint {
  x: number;
  y: number;
}

interface I3DPoint extends IPoint {
  z: number;
}

// 类型别名 - 更适合联合类型、元组等
type ID = number | string;
type Coordinates = [number, number];
type Direction = 'up' | 'down' | 'left' | 'right';

// 我的经验法则:
// - 需要继承或实现时,用接口
// - 需要联合类型、元组或其他复杂类型时,用类型别名
// - 对象类型两者都可以,但在团队中保持一致性更重要

最佳实践和踩坑经验

1. 接口命名约定

在我的项目中,通常使用这样的命名规则:

// 普通接口
interface User {}
interface Product {}

// 带前缀的接口(在某些规范中使用)
interface IUser {}        // 匈牙利命名法
interface UserInterface {} // 后缀命名法

// 选择一种并保持团队一致

2. 避免过度使用可选属性

// 不推荐:太多可选属性会让接口失去意义
interface IWeakContract {
  name?: string;
  age?: number;
  email?: string;
  phone?: string;
  // ... 很多可选属性
}

// 推荐:明确区分必需和可选
interface IStrongContract {
  // 必需的核心属性
  id: number;
  name: string;
  
  // 可选的附加属性
  metadata?: {
    createdAt?: Date;
    updatedAt?: Date;
    tags?: string[];
  };
}

3. 使用只读属性保护数据

interface IReadonlyUser {
  readonly id: number;
  readonly createdAt: Date;
  name: string;
  email: string;
}

const user: IReadonlyUser = {
  id: 1,
  createdAt: new Date(),
  name: "Alice",
  email: "alice@example.com"
};

user.name = "Bob"; // ✅ 允许
user.id = 2;       // ❌ 编译错误:id是只读的

结语

TypeScript接口就像是我们代码世界的"法律条文",它规定了各个部分应该如何协作。通过合理使用接口,我大大减少了运行时错误,提高了代码的可读性和可维护性。

记住,好的接口设计不是一蹴而就的,它需要在实际项目中不断实践和调整。希望我的这些经验能够帮助你在TypeScript的道路上走得更顺畅!

⭐  写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

TS泛型:让类型也学会“套娃”,但这次很优雅

2025年11月3日 08:36

大家好,我是小杨。今天想和大家聊聊TypeScript中让我又爱又恨的一个特性——泛型。

记得我第一次接触泛型时,看着那一对尖括号<>,心里直犯嘀咕:这玩意儿到底有啥用?直到我在项目中写了这样的代码:

// 最初的我:使用any大法
function identity(value: any): any {
    return value;
}

const result1 = identity("hello"); // result1的类型是any
const result2 = identity(42);      // result2的类型也是any

看起来没问题,对吧?但用着用着就发现不对劲了——我完全失去了类型信息!result1result2都变成了any,TypeScript的所有优势荡然无存。

泛型来救场了!

泛型就像是给函数、类或接口的一个"类型参数",让我们在使用时才确定具体的类型:

// 现在的我:使用泛型
function identity<T>(value: T): T {
    return value;
}

const result1 = identity("hello"); // result1的类型是string!
const result2 = identity(42);      // result2的类型是number!

看到了吗?TypeScript自动推断出了返回值的具体类型,这就是泛型的魔力!

泛型的几种实用场景

1. 处理数组不再头疼

// 以前处理数组很麻烦
function getFirstElement(arr: any[]): any {
    return arr[0];
}

// 现在可以这样
function getFirstElement<T>(arr: T[]): T | undefined {
    return arr[0];
}

const stringArray = ["apple", "banana", "cherry"];
const numberArray = [1, 2, 3];

const firstString = getFirstElement(stringArray); // 类型是string
const firstNumber = getFirstElement(numberArray); // 类型是number

2. 让接口也"通用"起来

// 一个通用的API响应接口
interface ApiResponse<T> {
    code: number;
    message: string;
    data: T;
    timestamp: Date;
}

// 用户数据
interface User {
    id: number;
    name: string;
    email: string;
}

// 商品数据  
interface Product {
    id: number;
    title: string;
    price: number;
}

// 现在可以这样使用
const userResponse: ApiResponse<User> = {
    code: 200,
    message: "success",
    data: {
        id: 1,
        name: "张三",
        email: "zhangsan@example.com"
    },
    timestamp: new Date()
};

const productResponse: ApiResponse<Product> = {
    code: 200, 
    message: "success",
    data: {
        id: 101,
        title: "笔记本电脑",
        price: 5999
    },
    timestamp: new Date()
};

3. 约束泛型:给类型加个"边界"

有时候我们希望泛型有一定的限制,不是随便什么类型都可以:

// 要求传入的对象必须有length属性
interface HasLength {
    length: number;
}

function logLength<T extends HasLength>(item: T): void {
    console.log(`Length: ${item.length}`);
}

logLength("hello");     // ✅ 字符串有length
logLength([1, 2, 3]);   // ✅ 数组有length  
logLength({ length: 5 }); // ✅ 对象有length属性
// logLength(42);       // ❌ 数字没有length属性

4. 多个泛型参数

// 处理键值对
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

const person = {
    name: "李四",
    age: 30,
    email: "lisi@example.com"
};

const personName = getProperty(person, "name");    // string类型
const personAge = getProperty(person, "age");      // number类型
// getProperty(person, "height");                  // ❌ 没有height这个属性

我在实际项目中的泛型应用

让我分享一个在真实项目中很有用的例子:

// 分页查询的通用类型
interface PaginationParams {
    page: number;
    pageSize: number;
}

interface PaginationResult<T> {
    data: T[];
    total: number;
    currentPage: number;
    totalPages: number;
    hasNext: boolean;
}

async function paginateQuery<T>(
    params: PaginationParams,
    fetchData: (params: PaginationParams) => Promise<T[]>,
    countData: () => Promise<number>
): Promise<PaginationResult<T>> {
    const [data, total] = await Promise.all([
        fetchData(params),
        countData()
    ]);
    
    return {
        data,
        total,
        currentPage: params.page,
        totalPages: Math.ceil(total / params.pageSize),
        hasNext: params.page < Math.ceil(total / params.pageSize)
    };
}

// 使用示例
interface Article {
    id: number;
    title: string;
    content: string;
}

// 查询文章列表
const articleResult = await paginateQuery<Article>(
    { page: 1, pageSize: 10 },
    fetchArticles,
    countArticles
);
// articleResult.data 的类型是 Article[]

我的心得体会

泛型学习有个过程,我总结了几点经验:

  1. 从简单开始:先理解<T>的基本概念,不要一开始就追求复杂的高级用法
  2. 多用多练:在合适的场景刻意使用泛型,慢慢就会找到感觉
  3. 适度使用:不是所有地方都需要泛型,在真正需要通用性的地方使用它
  4. 善用约束:用extends给泛型加上合理的限制,避免滥用

泛型就像是TypeScript中的"万能钥匙",一旦掌握,就能写出既灵活又类型安全的代码。它让我们的类型系统从"死板"变得"智能",这才是TypeScript真正的威力所在!

希望这篇分享能帮你打开泛型的大门。如果你在学习过程中有什么心得体会,欢迎在评论区交流讨论!

⭐  写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

Webpack配置魔法书:从入门到高手的通关秘籍

2025年10月30日 08:41

朋友们,我是小杨!今天咱们来聊聊Webpack配置这个话题。很多人第一次看到webpack.config.js文件时,感觉就像在看天书一样。别担心,今天我就带你从零开始,一步步解锁Webpack配置的奥秘!

初识Webpack:先来个"Hello World"

让我们从一个最简单的配置开始,就像学编程先写Hello World一样:

// 我的第一个webpack配置
const path = require('path');

module.exports = {
  // 入口:告诉Webpack从哪开始打包
  entry: './src/index.js',
  
  // 输出:打包后的文件放哪里
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  
  // 模式:开发还是生产
  mode: 'development'
};

这个基础配置就像搭积木的第一步,虽然简单,但已经能完成基本的打包任务了!

核心配置详解:拆解Webpack的"五脏六腑"

1. Entry(入口):打包的起点

// 单入口(SPA应用)
entry: './src/index.js'

// 多入口(多页面应用)
entry: {
  home: './src/home.js',
  about: './src/about.js',
  contact: './src/contact.js'
}

// 动态入口
entry: () => new Promise((resolve) => {
  resolve('./src/dynamic-entry.js');
})

2. Output(输出):打包成果的归宿

output: {
  path: path.resolve(__dirname, 'dist'),
  // 使用占位符确保文件名唯一
  filename: '[name].[contenthash].js',
  // 清理输出目录
  clean: true,
  // 公共路径(CDN场景很有用)
  publicPath: 'https://cdn.example.com/'
}

3. Loader:文件转换的"翻译官"

Loader是Webpack最强大的功能之一,让我展示几个常用配置:

module: {
  rules: [
    // 处理CSS文件
    {
      test: /.css$/i,
      use: ['style-loader', 'css-loader']
    },
    
    // 处理SCSS文件
    {
      test: /.scss$/i,
      use: [
        'style-loader',
        'css-loader',
        'sass-loader'
      ]
    },
    
    // 处理图片
    {
      test: /.(png|jpg|jpeg|gif|svg)$/i,
      type: 'asset/resource',
      generator: {
        filename: 'images/[name].[hash][ext]'
      }
    },
    
    // 处理字体
    {
      test: /.(woff|woff2|eot|ttf|otf)$/i,
      type: 'asset/resource',
      generator: {
        filename: 'fonts/[name].[hash][ext]'
      }
    },
    
    // Babel转译JS
    {
      test: /.js$/,
      exclude: /node_modules/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env']
        }
      }
    }
  ]
}

4. Plugins:增强功能的"外挂"

插件让Webpack变得更强大,来看我的常用插件组合:

const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

plugins: [
  // 自动生成HTML文件
  new HtmlWebpackPlugin({
    template: './src/index.html',
    title: '我的应用',
    minify: true
  }),
  
  // 提取CSS到单独文件
  new MiniCssExtractPlugin({
    filename: '[name].[contenthash].css'
  }),
  
  // 清理输出目录
  new CleanWebpackPlugin(),
  
  // 定义环境变量
  new webpack.DefinePlugin({
    'process.env.NODE_ENV': JSON.stringify('production')
  })
]

实战配置:搭建完整的开发环境

让我分享一个我在实际项目中使用的完整配置:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = (env, argv) => {
  const isProduction = argv.mode === 'production';
  
  return {
    entry: {
      main: './src/index.js',
      vendor: './src/vendor.js'
    },
    
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: isProduction 
        ? '[name].[contenthash].js' 
        : '[name].js',
      publicPath: '/'
    },
    
    module: {
      rules: [
        {
          test: /.js$/,
          exclude: /node_modules/,
          use: 'babel-loader'
        },
        {
          test: /.css$/,
          use: [
            isProduction 
              ? MiniCssExtractPlugin.loader 
              : 'style-loader',
            'css-loader',
            'postcss-loader'
          ]
        },
        {
          test: /.(png|jpg|gif)$/,
          type: 'asset/resource'
        }
      ]
    },
    
    plugins: [
      new HtmlWebpackPlugin({
        template: './src/index.html',
        inject: true
      }),
      ...(isProduction 
        ? [new MiniCssExtractPlugin({
            filename: '[name].[contenthash].css'
          })]
        : []
      )
    ],
    
    optimization: {
      splitChunks: {
        chunks: 'all',
        cacheGroups: {
          vendor: {
            test: /[\/]node_modules[\/]/,
            name: 'vendors',
            priority: 10
          }
        }
      }
    },
    
    devServer: {
      static: './dist',
      hot: true,
      open: true,
      port: 3000
    },
    
    devtool: isProduction ? 'source-map' : 'eval-cheap-module-source-map'
  };
};

环境特定配置:开发vs生产

开发环境配置要点

// webpack.dev.js
module.exports = {
  mode: 'development',
  devtool: 'eval-source-map',
  devServer: {
    static: './dist',
    hot: true,
    open: true,
    historyApiFallback: true
  },
  module: {
    rules: [
      {
        test: /.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  }
};

生产环境配置要点

// webpack.prod.js
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
  mode: 'production',
  devtool: 'source-map',
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin(),
      new CssMinimizerPlugin()
    ],
    splitChunks: {
      chunks: 'all'
    }
  },
  module: {
    rules: [
      {
        test: /.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader']
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin()
  ]
};

高级配置技巧:我的独门秘籍

1. 动态配置

module.exports = (env, argv) => {
  const isProduction = argv.mode === 'production';
  const isAnalyze = env && env.analyze;
  
  const config = {
    // 基础配置...
  };
  
  if (isAnalyze) {
    const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
    config.plugins.push(new BundleAnalyzerPlugin());
  }
  
  return config;
};

2. 多目标构建

// 同时构建多个配置
module.exports = [
  {
    name: 'client',
    target: 'web',
    entry: './src/client.js',
    output: {
      filename: 'client.bundle.js'
    }
  },
  {
    name: 'server',
    target: 'node',
    entry: './src/server.js',
    output: {
      filename: 'server.bundle.js'
    }
  }
];

常见问题排查:我踩过的那些坑

问题1:文件找不到?
检查路径配置,记得用path.resolve

问题2:Loader不生效?
检查test正则和use数组顺序

问题3:打包文件太大?
合理配置splitChunks和压缩选项

问题4:热更新不工作?
检查devServer配置和HotModuleReplacementPlugin

性能优化配置

optimization: {
  // 代码分割
  splitChunks: {
    chunks: 'all',
    cacheGroups: {
      // 第三方库单独打包
      vendor: {
        test: /[\/]node_modules[\/]/,
        name: 'vendors',
        priority: 20
      },
      // 公共代码单独打包
      common: {
        name: 'common',
        minChunks: 2,
        priority: 10,
        reuseExistingChunk: true
      }
    }
  },
  // 运行时代码单独提取
  runtimeChunk: {
    name: 'runtime'
  }
}

总结

Webpack配置就像搭积木,从简单开始,逐步添加需要的功能。记住几个关键点:

  1. 理解核心概念:Entry、Output、Loader、Plugin
  2. 区分环境:开发环境要快,生产环境要小
  3. 渐进式配置:从简单开始,按需添加功能
  4. 善用优化:代码分割、压缩、缓存一个都不能少

配置Webpack不是一蹴而就的,需要在实际项目中不断实践和调整。希望这篇指南能帮你少走弯路,快速掌握Webpack配置的精髓!

⭐  写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

webpack了解吗,讲一讲原理,怎么压缩代码

2025年10月30日 08:36

大家好,我是小杨!今天我要带大家探索前端工程化的核心魔法——Webpack。很多人觉得Webpack很复杂,但其实掌握了它的原理,你就会发现它就像个智能的"代码料理机",能把各种原料加工成美味佳肴!

Webpack的核心概念:先认识这些"厨房工具"

想象一下,Webpack就是一个现代化的智能厨房:

// 这是我的webpack.config.js - 相当于"菜谱"
module.exports = {
  // 入口起点 - 告诉厨房从哪开始准备食材
  entry: './src/index.js',
  
  // 输出 - 成品要放在哪里
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  
  // 加载器 - 各种食材处理工具
  module: {
    rules: [
      {
        test: /.css$/,        // 遇到CSS食材
        use: ['style-loader', 'css-loader']  // 用这两个工具处理
      },
      {
        test: /.(png|jpg)$/,  // 遇到图片食材
        use: ['file-loader']   // 用文件处理工具
      }
    ]
  },
  
  // 插件 - 高级厨房设备
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    })
  ]
};

Webpack的工作原理:看看"厨房"是怎么运作的

让我用最通俗的方式解释Webpack的工作流程:

第一步:依赖收集(找食材)
Webpack从入口文件开始,像侦探一样找出所有的import和require语句,建立完整的依赖关系图。

// 假设这是我的项目结构
// src/
//   index.js (入口)
//   utils.js  
//   styles.css

// index.js
import { calculateTotal } from './utils.js';
import './styles.css';

const result = calculateTotal(100, 20);
console.log('总价:', result);

// utils.js
export function calculateTotal(price, tax) {
  return price + (price * tax);
}

第二步:模块转换(处理食材)
通过loader系统,把不同类型的文件都转换成JavaScript能理解的模块。

// 比如CSS文件,经过css-loader和style-loader处理
// 原始的styles.css
.body { color: red; }

// 被转换成JavaScript模块
const styles = ".body { color: red; }";
// 然后通过style-loader注入到页面

第三步:代码生成(装盘上菜)
把所有模块组合成一个或多个bundle文件。

代码压缩的魔法:让文件瘦身的秘密武器

说到代码压缩,这可是Webpack的拿手好戏!让我展示几种常见的压缩方式:

1. JavaScript压缩

// 压缩前的代码
function calculatePrice(originalPrice, discountRate) {
    const discountAmount = originalPrice * discountRate;
    const finalPrice = originalPrice - discountAmount;
    return finalPrice;
}

const result = calculatePrice(100, 0.2);
console.log("最终价格是:", result);

// 经过TerserWebpackPlugin压缩后
function n(n,r){return n-n*r}console.log("最终价格是:",n(100,.2));

在webpack中配置:

const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [new TerserPlugin({
      terserOptions: {
        compress: {
          drop_console: true, // 移除console.log
          pure_funcs: ['console.log'] // 移除特定的函数调用
        }
      }
    })]
  }
};

2. CSS压缩

// 压缩前
.container {
    margin: 10px 20px 10px 20px;
    padding: 15px;
    background-color: #ff0000;
}

.title {
    font-size: 16px;
    font-weight: bold;
}

// 压缩后
.container{margin:10px 20px;padding:15px;background-color:red}.title{font-size:16px;font-weight:700}

配置方法:

const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
  optimization: {
    minimizer: [
      new CssMinimizerPlugin(),
    ],
  },
};

3. 图片压缩

module.exports = {
  module: {
    rules: [
      {
        test: /.(png|jpg|jpeg|gif)$/i,
        use: [
          {
            loader: 'image-webpack-loader',
            options: {
              mozjpeg: {
                progressive: true,
                quality: 65
              },
              optipng: {
                enabled: true,
              },
              pngquant: {
                quality: [0.65, 0.90],
                speed: 4
              }
            }
          }
        ]
      }
    ]
  }
};

高级优化技巧:我的实战经验分享

1. 代码分割

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\/]node_modules[\/]/,
          name: 'vendors',
          chunks: 'all',
        },
        common: {
          name: 'common',
          minChunks: 2,
          chunks: 'all',
          minSize: 0
        }
      }
    }
  }
};

2. Tree Shaking

// utils.js - 原始代码
export function usedFunction() {
  return '这个函数会被使用';
}

export function unusedFunction() {
  return '这个函数不会被使用,会被tree shaking掉';
}

// index.js - 只导入usedFunction
import { usedFunction } from './utils';

// 打包后,unusedFunction会被自动移除

实战案例:看我如何优化一个真实项目

让我分享一个真实的优化经历:

// 优化前的配置
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js'
  }
};

// 优化后的配置
module.exports = {
  entry: {
    main: './src/index.js',
    admin: './src/admin.js'
  },
  output: {
    filename: '[name].[contenthash].js',
    path: path.resolve(__dirname, 'dist'),
    clean: true
  },
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\/]node_modules[\/]/,
          name: 'vendors',
          priority: 10
        }
      }
    },
    usedExports: true,
    minimize: true
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash].css'
    }),
    new CompressionPlugin({
      algorithm: 'gzip'
    })
  ]
};

优化效果:

  • 文件大小减少60%
  • 加载速度提升40%
  • 缓存命中率大幅提高

常见坑点和解决方案

坑1:Tree Shaking不生效
检查:package.json中要有"sideEffects": false

坑2:压缩后代码报错
可能是ES6+语法问题,确保terser配置正确

坑3:文件太大
合理使用代码分割和动态导入

总结

Webpack就像前端开发的"瑞士军刀",掌握它的原理和优化技巧,能让你在性能优化的道路上如鱼得水。记住:好的打包策略不是一蹴而就的,需要根据项目特点不断调整优化

希望这篇分享能帮你更好地理解Webpack!如果有任何问题,欢迎在评论区交流~

⭐  写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

玩转小程序生命周期:从入门到上瘾

2025年10月30日 08:30

作为前端老司机,我经常被问到:“小杨,小程序的生命周期到底怎么玩?”今天我就用最接地气的方式,带你解锁小程序生命周期的正确打开方式。准备好,我们要发车了!

先来认识下小程序的生命周期家族

想象一下,你的小程序就像一个有生命的个体,从诞生到离开,每个阶段都有专属的“人生时刻”。让我用一个简单的Page示例来演示:

// 我的第一个小程序页面
Page({
  data: {
    userInfo: {},
    isLoading: true
  },
  
  // 生命周期函数们要登场啦!
  onLoad(options) {
    console.log('页面加载啦!');
    // 我在这里初始化数据
    this.fetchUserData();
  },
  
  onShow() {
    console.log('页面显示啦!');
    // 每次进入页面都会执行
    this.updateBadgeCount();
  },
  
  onReady() {
    console.log('页面准备就绪!');
    // 页面渲染完成,可以操作DOM了
    this.initCanvas();
  },
  
  onHide() {
    console.log('页面隐藏了');
    // 页面被收起时清理一些资源
    this.clearTimer();
  },
  
  onUnload() {
    console.log('页面被卸载了');
    // 页面被关闭时彻底清理
    this.cleanup();
  }
})

全局生命周期:小程序的“大脑”

App级别的生命周期就像是整个小程序的大脑,掌控着全局的生死轮回:

App({
  onLaunch(options) {
    // 小程序启动时的初始化
    console.log('小程序诞生了!');
    this.initCloudService();
  },
  
  onShow(options) {
    // 小程序切换到前台
    console.log('小程序被唤醒了');
    this.syncData();
  },
  
  onHide() {
    // 小程序被切换到后台
    console.log('小程序去休息了');
    this.saveState();
  },
  
  onError(msg) {
    // 出错时的处理
    console.error('出问题啦:', msg);
    this.reportError(msg);
  }
})

组件生命周期:精致的“小部件”

组件的生命周期更加精细,让我用一个自定义组件来展示:

Component({
  lifetimes: {
    created() {
      // 组件实例刚被创建
      console.log('组件出生了!');
    },
    
    attached() {
      // 组件进入页面节点树
      console.log('组件安家落户了');
      this.initData();
    },
    
    ready() {
      // 组件渲染完成
      console.log('组件装修完毕');
      this.startAnimation();
    },
    
    detached() {
      // 组件被从页面移除
      console.log('组件搬家了');
      this.releaseResource();
    }
  },
  
  // 还有页面生命周期,专门为组件定制
  pageLifetimes: {
    show() {
      // 页面展示时的逻辑
      this.resumeMusic();
    },
    
    hide() {
      // 页面隐藏时的逻辑
      this.pauseMusic();
    }
  }
})

实战技巧:生命周期的最佳拍档

在我多年的开发经验中,发现这些生命周期组合使用效果最佳:

场景1:数据加载优化

Page({
  data: {
    listData: [],
    hasMore: true
  },
  
  onLoad() {
    // 初次加载数据
    this.loadInitialData();
  },
  
  onShow() {
    // 每次显示时检查更新
    if (this.shouldRefresh()) {
      this.refreshData();
    }
  },
  
  onHide() {
    // 离开时保存状态
    this.saveScrollPosition();
  }
})

场景2:资源管理

Page({
  timer: null,
  
  onShow() {
    // 开启定时器
    this.timer = setInterval(() => {
      this.updateRealTimeData();
    }, 5000);
  },
  
  onHide() {
    // 及时清理,避免内存泄漏
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = null;
    }
  },
  
  onUnload() {
    // 双重保险
    this.onHide();
    this.cleanupNetworkRequests();
  }
})

常见坑点与解决方案

坑1:onLoad vs onShow 傻傻分不清

  • onLoad:只在页面创建时执行一次,适合一次性初始化
  • onShow:每次页面显示都执行,适合状态更新

坑2:内存泄漏
记得在onHide或onUnload中清理定时器、事件监听等资源。

坑3:数据状态混乱
合理利用onHide保存状态,onShow恢复状态。

总结

掌握小程序生命周期,就像掌握了小程序的“呼吸节奏”。合理运用它们,能让你的小程序运行更加流畅,用户体验更加丝滑。记住:合适的生命周期做合适的事,这是写出优质小程序的秘诀!

希望这篇分享能帮你更好地理解小程序生命周期。如果有任何问题,欢迎在评论区交流讨论~

⭐  写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

❌
❌