MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Qwik 与第三方库集成:Redux 在 Qwik 中的使用指南

2021-08-263.9k 阅读

理解 Redux 及其在前端开发中的角色

Redux 是 JavaScript 应用程序的可预测状态容器,主要用于管理应用程序的状态。在大型前端项目中,随着业务逻辑复杂度的增加,状态管理变得至关重要。Redux 遵循单向数据流的原则,使得应用程序的状态变化可追踪、可预测。

Redux 的核心概念

  1. Store:Store 是 Redux 应用的核心,它保存着整个应用的状态树。应用中只有一个单一的 Store,所有的状态都集中存储在这里。例如,在一个电商应用中,购物车的商品列表、用户登录状态等都可以是 Store 中状态的一部分。
import { createStore } from'redux';
// 定义一个简单的 reducer
function counterReducer(state = { value: 0 }, action) {
    switch (action.type) {
        case 'increment':
            return { value: state.value + 1 };
        case 'decrement':
            return { value: state.value - 1 };
        default:
            return state;
    }
}
// 创建 Store
const store = createStore(counterReducer);
  1. Reducer:Reducer 是一个纯函数,它接收当前的状态和一个 action,然后返回一个新的状态。Reducer 完全决定了状态如何根据 action 进行变化。它必须是纯函数,即相同的输入总会返回相同的输出,并且不能有副作用(如 API 调用、修改全局变量等)。上面代码中的 counterReducer 就是一个简单的 Reducer,根据不同的 action.type 来更新状态。
  2. Action:Action 是一个普通的 JavaScript 对象,它描述了发生的事情。Action 必须有一个 type 字段来表示动作的类型,其他字段可以用来携带数据。例如:
const incrementAction = { type: 'increment' };
const decrementAction = { type: 'decrement' };
  1. Dispatch:Dispatch 是 Store 的一个方法,用于将 action 发送到 Store 中。当调用 store.dispatch(action) 时,Store 会将 action 传递给 Reducer,Reducer 根据 action 更新状态。
store.dispatch(incrementAction);

Qwik 简介及其优势

Qwik 是一个现代的前端框架,专注于提供极致的性能和开发者体验。它的一些显著特点使其在前端开发领域脱颖而出。

Qwik 的关键特性

  1. 即时渲染:Qwik 采用了一种称为“即时渲染”的技术,这意味着页面的初始渲染速度极快。它不需要等待 JavaScript 完全加载和解析,就能呈现出页面的基本内容,大大提升了用户的感知加载速度。例如,在一个新闻资讯类应用中,用户打开页面就能立即看到文章标题等关键信息,而无需等待整个页面的 JavaScript 代码下载和执行。
  2. 按需注水(On - demand Hydration):与传统的全量注水(将所有 JavaScript 代码下载并运行在客户端)不同,Qwik 仅在需要交互的部分注入 JavaScript。比如,一个页面上有多个按钮,只有当用户点击某个按钮时,与该按钮相关的交互逻辑的 JavaScript 代码才会被注入并执行,这样可以显著减少初始加载的 JavaScript 体积,提高页面的响应速度。
  3. 轻量级:Qwik 的核心库体积非常小,这有助于快速加载和提升性能。在构建大型应用时,较小的依赖体积可以减少网络传输时间和内存占用。

在 Qwik 中集成 Redux 的准备工作

在开始集成 Redux 到 Qwik 项目之前,需要确保项目已经初始化并具备基本的开发环境。

创建 Qwik 项目

  1. 使用 Qwik CLI:可以通过 Qwik CLI 快速创建一个新的 Qwik 项目。首先确保已经安装了 Node.js 和 npm(或 yarn)。然后在命令行中执行以下命令:
npm create qwik@latest my - qwik - app
cd my - qwik - app

这将创建一个名为 my - qwik - app 的新 Qwik 项目,并进入项目目录。 2. 项目结构:创建完成后,项目结构大致如下:

my - qwik - app
├── src
│   ├── components
│   │   └── Layout.tsx
│   ├── entry.client.tsx
│   ├── entry.server.tsx
│   ├── main.tsx
│   └── routes
│       └── index.tsx
├── package.json
├── tsconfig.json
└── vite.config.ts

src/components 目录用于存放组件,src/routes 目录用于定义路由,entry.client.tsxentry.server.tsx 分别是客户端和服务器端的入口文件,main.tsx 是应用的主文件。

安装 Redux 及其相关依赖

在 Qwik 项目目录中,使用 npm 或 yarn 安装 Redux 和 React - Redux(因为 Qwik 基于 React):

npm install redux react - redux

这将安装 Redux 的核心库以及 React - Redux 绑定库,React - Redux 提供了在 React(Qwik 基于 React)应用中使用 Redux 的方法,如 Provider 组件和 useSelectoruseDispatch 钩子。

在 Qwik 项目中设置 Redux Store

设置 Redux Store 是在 Qwik 中使用 Redux 的重要一步。

创建 Reducer

  1. 定义 Reducer 文件:在 src 目录下创建一个 redux 目录,然后在该目录下创建 counterReducer.ts 文件(以一个简单的计数器为例):
import { Action } from'redux';

interface CounterState {
    value: number;
}

const initialState: CounterState = {
    value: 0
};

export function counterReducer(state = initialState, action: Action): CounterState {
    switch (action.type) {
        case 'increment':
            return { value: state.value + 1 };
        case 'decrement':
            return { value: state.value - 1 };
        default:
            return state;
    }
}

这里定义了一个 CounterState 接口来描述计数器的状态,initialState 给出了初始状态,counterReducer 函数根据不同的 action.type 更新状态。

创建 Store

  1. 创建 Store 文件:在 redux 目录下创建 store.ts 文件:
import { createStore } from'redux';
import { counterReducer } from './counterReducer';

export const store = createStore(counterReducer);

这里通过 createStore 方法,将 counterReducer 传入创建了 Redux Store。

在 Qwik 组件中使用 Redux

使用 Provider 组件提供 Store

  1. 修改 main.tsx:在 src/main.tsx 文件中,导入 Provider 组件并将整个应用包裹在 Provider 中,使其能够访问 Redux Store:
import { component$, useContext } from '@builder.io/qwik';
import { Provider } from'react - redux';
import { store } from './redux/store';
import { Layout } from './components/Layout';

export const App = component$(() => {
    return (
        <Provider store={store}>
            <Layout />
        </Provider>
    );
});

Provider 组件接收 store 属性,将 Redux Store 传递给其所有后代组件。

使用 useSelector 和 useDispatch 钩子

  1. 创建 Counter 组件:在 src/components 目录下创建 Counter.tsx 文件:
import { component$, useContext } from '@builder.io/qwik';
import { useSelector, useDispatch } from'react - redux';
import { incrementAction, decrementAction } from '../redux/actions';

export const Counter = component$(() => {
    const count = useSelector((state: any) => state.value);
    const dispatch = useDispatch();

    const increment = () => {
        dispatch(incrementAction);
    };

    const decrement = () => {
        dispatch(decrementAction);
    };

    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={increment}>Increment</button>
            <button onClick={decrement}>Decrement</button>
        </div>
    );
});

在这个组件中,使用 useSelector 钩子从 Redux Store 中选择 count 值,useDispatch 钩子获取 dispatch 函数。通过点击按钮调用 dispatch 函数来发送 incrementActiondecrementAction 以更新 Store 中的状态。

处理复杂状态和异步操作

在实际应用中,状态往往更加复杂,并且可能涉及异步操作,如 API 调用。

处理复杂状态结构

  1. 扩展 Reducer 和 State:假设在电商应用中,除了计数器,还有商品列表。在 redux 目录下创建 productReducer.ts 文件:
import { Action } from'redux';

interface Product {
    id: number;
    name: string;
    price: number;
}

interface ProductState {
    products: Product[];
}

const initialProductState: ProductState = {
    products: []
};

export function productReducer(state = initialProductState, action: Action): ProductState {
    switch (action.type) {
        case 'add_product':
            const newProduct = action as { type: string; product: Product };
            return { products: [...state.products, newProduct.product] };
        default:
            return state;
    }
}

然后在 store.ts 文件中使用 combineReducers 来合并多个 reducer:

import { createStore, combineReducers } from'redux';
import { counterReducer } from './counterReducer';
import { productReducer } from './productReducer';

const rootReducer = combineReducers({
    counter: counterReducer,
    products: productReducer
});

export const store = createStore(rootReducer);

这样,Redux Store 就包含了计数器和商品列表两种状态。在组件中可以通过 useSelector 选择不同部分的状态:

import { component$, useContext } from '@builder.io/qwik';
import { useSelector } from'react - redux';

export const ComplexStateComponent = component$(() => {
    const count = useSelector((state: any) => state.counter.value);
    const products = useSelector((state: any) => state.products.products);

    return (
        <div>
            <p>Count: {count}</p>
            <ul>
                {products.map(product => (
                    <li key={product.id}>{product.name} - ${product.price}</li>
                ))}
            </ul>
        </div>
    );
});

处理异步操作

  1. 使用 Redux - Thunk 中间件(以 API 调用为例):首先安装 redux - thunk
npm install redux - thunk

然后在 store.ts 文件中应用中间件:

import { createStore, combineReducers, applyMiddleware } from'redux';
import thunk from'redux - thunk';
import { counterReducer } from './counterReducer';
import { productReducer } from './productReducer';

const rootReducer = combineReducers({
    counter: counterReducer,
    products: productReducer
});

export const store = createStore(rootReducer, applyMiddleware(thunk));

假设要从 API 获取商品列表,在 redux 目录下创建 productActions.ts 文件:

import { ThunkDispatch } from'redux - thunk';
import { Action } from'redux';
import { Product } from './productReducer';

const FETCH_PRODUCTS = 'fetch_products';

export const fetchProducts = () => {
    return async (dispatch: ThunkDispatch<any, any, Action>) => {
        try {
            const response = await fetch('https://example.com/api/products');
            const data: Product[] = await response.json();
            dispatch({ type: FETCH_PRODUCTS, products: data });
        } catch (error) {
            console.error('Error fetching products:', error);
        }
    };
};

在组件中可以使用这个异步 action:

import { component$, useContext } from '@builder.io/qwik';
import { useSelector, useDispatch } from'react - redux';
import { fetchProducts } from '../redux/productActions';

export const AsyncProductComponent = component$(() => {
    const products = useSelector((state: any) => state.products.products);
    const dispatch = useDispatch();

    const loadProducts = () => {
        dispatch(fetchProducts());
    };

    return (
        <div>
            <button onClick={loadProducts}>Load Products</button>
            <ul>
                {products.map(product => (
                    <li key={product.id}>{product.name} - ${product.price}</li>
                ))}
            </ul>
        </div>
    );
});

这里通过 redux - thunk 中间件,使得 action 可以返回一个函数,在函数中进行异步操作,如 API 调用,然后再 dispatch 一个普通的 action 来更新状态。

优化 Redux 在 Qwik 中的使用

代码拆分与懒加载

  1. 组件级代码拆分:在 Qwik 中,可以使用 component$ 函数来实现组件的懒加载。结合 Redux,假设某个 Redux - 相关的组件比较复杂且不常使用,可以将其拆分出来并懒加载。例如,在 src/components 目录下创建 SpecialFeature.tsx 文件,该组件依赖 Redux:
import { component$, useContext } from '@builder.io/qwik';
import { useSelector } from'react - redux';

export const SpecialFeature = component$(() => {
    const count = useSelector((state: any) => state.counter.value);
    return (
        <div>
            <p>Special Feature: Count from Redux - {count}</p>
        </div>
    );
});

在主组件中懒加载该组件:

import { component$, useContext } from '@builder.io/qwik';
import { lazy, Suspense } from'react';

const SpecialFeature = lazy(() => import('./SpecialFeature'));

export const MainComponent = component$(() => {
    return (
        <Suspense fallback={<div>Loading...</div>}>
            <SpecialFeature />
        </Suspense>
    );
});

这样,只有当 SpecialFeature 组件需要渲染时,才会加载其代码,包括相关的 Redux 逻辑,从而提高应用的初始加载性能。

性能优化

  1. Memoization:在使用 useSelector 时,可以利用 react - redux 提供的 memoize 功能。例如,在 Counter.tsx 组件中,如果 count 值没有变化,useSelector 不应该重新计算:
import { component$, useContext } from '@builder.io/qwik';
import { useSelector, useDispatch } from'react - redux';
import { incrementAction, decrementAction } from '../redux/actions';
import { shallowEqual } from'react - redux';

export const Counter = component$(() => {
    const count = useSelector((state: any) => state.value, shallowEqual);
    const dispatch = useDispatch();

    const increment = () => {
        dispatch(incrementAction);
    };

    const decrement = () => {
        dispatch(decrementAction);
    };

    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={increment}>Increment</button>
            <button onClick={decrement}>Decrement</button>
        </div>
    );
});

这里通过 shallowEqual 作为 useSelector 的第二个参数,只有当 state.value 的浅层次结构发生变化时,useSelector 才会重新计算,从而避免不必要的重新渲染,提升性能。

处理 Redux 与 Qwik 的兼容性问题

处理 Qwik 的即时渲染与 Redux 的交互

  1. 初始状态同步:由于 Qwik 的即时渲染特性,在服务器端渲染(SSR)时,需要确保 Redux Store 的初始状态能够正确地传递到客户端。可以在 entry.server.tsx 文件中,在渲染之前将 Redux Store 的状态序列化到 HTML 中:
import { renderToString } from '@builder.io/qwik/server';
import { App } from './main';
import { store } from './redux/store';

export default async function handler() {
    const html = await renderToString(<App />, {
        documentProps: {
            initialState: JSON.stringify(store.getState())
        }
    });
    return {
        body: html
    };
}

然后在客户端的 entry.client.tsx 文件中,将初始状态还原到 Redux Store 中:

import { component$, hydrate, useContext } from '@builder.io/qwik';
import { Provider } from'react - redux';
import { store } from './redux/store';
import { App } from './main';

const initialState = JSON.parse(document.documentElement.dataset.initialState || '{}');
store.replaceState(initialState);

hydrate(() => (
    <Provider store={store}>
        <App />
    </Provider>
));

这样,无论是在服务器端渲染还是客户端注水后,Redux Store 的初始状态都是一致的,避免了状态不一致的问题。

处理 Qwik 的按需注水与 Redux 的关系

  1. 延迟加载 Redux 相关代码:在 Qwik 的按需注水场景下,对于一些不常用的 Redux - 相关功能,可以延迟加载其代码。例如,某个特定的 Redux action 只在用户点击某个高级设置按钮时才需要使用。可以将相关的 action 创建函数和 reducer 逻辑放在一个单独的文件中,并使用动态导入:
// advancedSettingsActions.ts
import { Action } from'redux';

const ADVANCED_SETTING_CHANGE = 'advanced_setting_change';

export const changeAdvancedSetting = (value: string) => {
    return { type: ADVANCED_SETTING_CHANGE, value };
};

// advancedSettingsReducer.ts
import { Action } from'redux';

interface AdvancedSettingsState {
    settingValue: string;
}

const initialAdvancedSettingsState: AdvancedSettingsState = {
    settingValue: ''
};

export function advancedSettingsReducer(state = initialAdvancedSettingsState, action: Action): AdvancedSettingsState {
    switch (action.type) {
        case ADVANCED_SETTING_CHANGE:
            const newAction = action as { type: string; value: string };
            return { settingValue: newAction.value };
        default:
            return state;
    }
}

在主组件中:

import { component$, useContext } from '@builder.io/qwik';
import { useSelector, useDispatch } from'react - redux';

export const AdvancedSettingsComponent = component$(() => {
    const dispatch = useDispatch();
    const settingValue = useSelector((state: any) => state.advancedSettings.settingValue);

    const handleClick = async () => {
        const { changeAdvancedSetting } = await import('./advancedSettingsActions');
        dispatch(changeAdvancedSetting('new value'));
    };

    return (
        <div>
            <p>Advanced Setting: {settingValue}</p>
            <button onClick={handleClick}>Change Advanced Setting</button>
        </div>
    );
});

这样,只有当用户点击按钮时,才会加载 advancedSettingsActions 和相关的 reducer 代码,配合 Qwik 的按需注水机制,进一步优化性能。

常见问题及解决方法

Redux 状态未更新或更新异常

  1. 检查 Action 类型:确保在 dispatch action 时,action 的 type 与 reducer 中处理的 type 完全一致。例如,在 counterReducer.ts 中,如果 action.type'increment',在 dispatch 时必须使用相同的字符串:
const incrementAction = { type: 'increment' };
store.dispatch(incrementAction);
  1. Reducer 纯函数性:Reducer 必须是纯函数,不能有副作用。如果在 reducer 中修改了外部变量或者进行了 API 调用等操作,可能导致状态更新异常。例如,以下是错误的写法:
let externalVariable = 0;
export function counterReducer(state = initialState, action: Action): CounterState {
    switch (action.type) {
        case 'increment':
            externalVariable++;
            return { value: state.value + 1 };
        default:
            return state;
    }
}

应该改为:

export function counterReducer(state = initialState, action: Action): CounterState {
    switch (action.type) {
        case 'increment':
            return { value: state.value + 1 };
        default:
            return state;
    }
}

Qwik 组件与 Redux 集成的渲染问题

  1. 确保 Provider 包裹:在 Qwik 应用中,所有需要访问 Redux Store 的组件必须在 Provider 组件的包裹范围内。如果组件没有被正确包裹,useSelectoruseDispatch 钩子将无法正常工作。例如,在 main.tsx 中:
import { component$, useContext } from '@builder.io/qwik';
import { Provider } from'react - redux';
import { store } from './redux/store';
import { Layout } from './components/Layout';

export const App = component$(() => {
    return (
        <Provider store={store}>
            <Layout />
        </Provider>
    );
});
  1. 处理异步数据加载与渲染:当在 Qwik 组件中使用 Redux 进行异步数据加载(如 API 调用)时,要注意组件的渲染时机。如果在数据未加载完成时就尝试渲染依赖该数据的部分,可能会导致错误。可以使用加载状态来控制渲染,例如:
import { component$, useContext } from '@builder.io/qwik';
import { useSelector, useDispatch } from'react - redux';
import { fetchProducts } from '../redux/productActions';

export const AsyncProductComponent = component$(() => {
    const products = useSelector((state: any) => state.products.products);
    const isLoading = useSelector((state: any) => state.products.isLoading);
    const dispatch = useDispatch();

    const loadProducts = () => {
        dispatch(fetchProducts());
    };

    return (
        <div>
            <button onClick={loadProducts}>Load Products</button>
            {isLoading? (
                <p>Loading...</p>
            ) : (
                <ul>
                    {products.map(product => (
                        <li key={product.id}>{product.name} - ${product.price}</li>
                    ))}
                </ul>
            )}
        </div>
    );
});

productReducer.ts 中添加 isLoading 状态:

import { Action } from'redux';

interface Product {
    id: number;
    name: string;
    price: number;
}

interface ProductState {
    products: Product[];
    isLoading: boolean;
}

const initialProductState: ProductState = {
    products: [],
    isLoading: false
};

export function productReducer(state = initialProductState, action: Action): ProductState {
    switch (action.type) {
        case 'fetch_products_start':
            return { ...state, isLoading: true };
        case 'fetch_products_success':
            const newAction = action as { type: string; products: Product[] };
            return { products: newAction.products, isLoading: false };
        default:
            return state;
    }
}

productActions.ts 中更新异步 action:

import { ThunkDispatch } from'redux - thunk';
import { Action } from'redux';
import { Product } from './productReducer';

const FETCH_PRODUCTS_START = 'fetch_products_start';
const FETCH_PRODUCTS_SUCCESS = 'fetch_products_success';

export const fetchProducts = () => {
    return async (dispatch: ThunkDispatch<any, any, Action>) => {
        dispatch({ type: FETCH_PRODUCTS_START });
        try {
            const response = await fetch('https://example.com/api/products');
            const data: Product[] = await response.json();
            dispatch({ type: FETCH_PRODUCTS_SUCCESS, products: data });
        } catch (error) {
            console.error('Error fetching products:', error);
        }
    };
};

通过这种方式,在数据加载过程中显示加载提示,避免渲染异常。