【HarmonyOS】React Native实战项目+Redux Toolkit状态管理

在这里插入图片描述

🌸你好呀!我是 lbb小魔仙
🌟 感谢陪伴~ 小白博主在线求友
🌿 跟着小白学Linux/Java/Python
📖 专栏汇总:
《Linux》专栏 | 《Java》专栏 | 《Python》专栏

在这里插入图片描述

欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net

一、Redux Toolkit在OpenHarmony平台的适配价值

Redux Toolkit(简称RTK)是Redux官方推荐的现代化状态管理工具,通过切片(Slice)机制大幅简化了状态管理代码的编写。在OpenHarmony平台应用中,RTK不仅能提升开发效率,更能通过特定的平台优化策略改善应用性能。

1.1 传统Redux与RTK对比

┌─────────────────────────────────────────────────────────────┐
│           Redux演进:从传统到Toolkit                        │
├─────────────────────────────────────────────────────────────┤
│                                                           │
│  传统Redux开发流程          Redux Toolkit开发流程           │
│  ┌──────────────┐         ┌──────────────┐             │
│  │ 定义Action   │         │ 创建Slice    │             │
│  │ Types       │         │ (一步到位)   │             │
│  └──────┬───────┘         └──────┬───────┘             │
│         │                        │                       │
│  ┌──────▼───────┐         ┌──────▼───────┐             │
│  │ 编写Action   │         │ 自动生成     │             │
│  │ Creators    │         │ Actions      │             │
│  └──────┬───────┘         └──────┬───────┘             │
│         │                        │                       │
│  ┌──────▼───────┐         ┌──────▼───────┐             │
│  │ 定义Reducer  │         │ 集成Store    │             │
│  │ Switch Case  │         │ (完成)       │             │
│  └──────┬───────┘         └──────────────┘             │
│         │                                                 │
│  ┌──────▼───────┐                                       │
│  │ 手动组合     │         代码量减少 ~60%               │
│  │ combineReducers│        类型安全 自动推断             │
│  └──────────────┘                                       │
└─────────────────────────────────────────────────────────────┘

1.2 OpenHarmony平台适配优势

特性 传统Redux Redux Toolkit OpenHarmony收益
代码体积 ~500行基础代码 ~200行 减少应用包体积,适配鸿蒙存储限制
异步处理 需redux-thunk 内置createAsyncThunk 优化鸿蒙任务调度机制
类型定义 手动维护 自动推断 提升TS在鸿蒙环境开发体验
不可变更新 手动展开运算符 内置Immer 减少运行时计算负担
开发效率 中等 加速鸿蒙应用迭代周期

1.3 鸿蒙应用状态管理架构

┌─────────────────────────────────────────────────────────────┐
│              OpenHarmony应用状态管理架构                    │
├─────────────────────────────────────────────────────────────┤
│                                                           │
│  ┌─────────────────────────────────────────────┐         │
│  │          React Native UI组件                 │         │
│  │  • useSelector(state => selector)          │         │
│  │  • useDispatch(actionCreator)             │         │
│  └──────────────────┬──────────────────────────┘         │
│                     │                                     │
│  ┌──────────────────▼──────────────────────────┐         │
│  │         Redux Toolkit Slice层               │         │
│  │  ┌─────────┐ ┌─────────┐ ┌─────────┐   │         │
│  │  │UserSlice│ │AppSlice │ │APISlice │   │         │
│  │  └────┬────┘ └────┬────┘ └────┬────┘   │         │
│  └───────┼──────────┼──────────┼───────────┘         │
│          │          │          │                         │
│  ┌───────▼──────────▼──────────▼───────────┐         │
│  │         Redux Store (configureStore)      │         │
│  │  • Redux DevTools                      │         │
│  │  • Immer (不可变更新)                  │         │
│  │  • Redux Thunk (异步处理)              │         │
│  └───────┬──────────────────────────────────┘         │
│          │                                             │
│  ┌───────▼───────────────────────────────────┐         │
│  │      持久化层 (OpenHarmony适配)          │         │
│  │  ┌─────────────────────────────────┐      │         │
│  │  │ ohos.data.relationalStore      │      │         │
│  │  │ (鸿蒙关系型数据库)            │      │         │
│  │  └─────────────────────────────────┘      │         │
│  └────────────────────────────────────────────┘         │
└─────────────────────────────────────────────────────────────┘

二、Slice配置核心概念

2.1 Slice结构解析

Redux Toolkit的Slice是一个完整的状态管理单元,包含:

import { createSlice, PayloadAction } from '@reduxjs/toolkit';

/**
 * 用户配置Slice - OpenHarmony平台优化版
 */
interface UserSettings {
  theme: 'light' | 'dark';
  fontSize: number;
  language: string;
  lastSync: number; // 鸿蒙设备时间戳
}

interface UserState {
  settings: UserSettings;
  loading: boolean;
  error: string | null;
}

const initialState: UserState = {
  settings: {
    theme: 'light',
    fontSize: 14,
    language: 'zh-CN',
    lastSync: 0,
  },
  loading: false,
  error: null,
};

const userSlice = createSlice({
  // 命名空间:自动生成action类型前缀
  name: 'user',

  // 初始状态
  initialState,

  // 同步reducers - 自动生成action creators
  reducers: {
    /**
     * 切换主题
     * Immer自动处理不可变更新
     */
    toggleTheme(state) {
      state.settings.theme =
        state.settings.theme === 'light' ? 'dark' : 'light';
      state.settings.lastSync = Date.now();
    },

    /**
     * 设置字体大小
     */
    setFontSize(state, action: PayloadAction<number>) {
      const size = Math.max(12, Math.min(24, action.payload));
      state.settings.fontSize = size;
      state.settings.lastSync = Date.now();
    },

    /**
     * 设置语言
     */
    setLanguage(state, action: PayloadAction<string>) {
      state.settings.language = action.payload;
      state.settings.lastSync = Date.now();
    },

    /**
     * 重置设置
     */
    resetSettings(state) {
      state.settings = { ...initialState.settings };
    },
  },

  // 异步reducers - 使用extraReducers处理
  extraReducers: (builder) => {
    builder
      // 加载设置异步操作
      .addCase(loadSettings.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(loadSettings.fulfilled, (state, action) => {
        state.loading = false;
        state.settings = action.payload;
      })
      .addCase(loadSettings.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message || '加载失败';
      });
  },
});

// 导出自动生成的action creators
export const {
  toggleTheme,
  setFontSize,
  setLanguage,
  resetSettings,
} = userSlice.actions;

// 导出reducer
export default userSlice.reducer;

2.2 异步操作处理(createAsyncThunk)

import { createAsyncThunk } from '@reduxjs/toolkit';
import { ohosData } from '@ohos.data.relationalStore';

/**
 * 异步加载用户设置
 * 使用OpenHarmony关系型数据库
 */
export const loadSettings = createAsyncThunk(
  'user/loadSettings',
  async (userId: string, { rejectWithValue }) => {
    try {
      // 获取鸿蒙数据库存储实例
      const store = await ohosData.getRdbStore({
        name: 'UserSettings.db',
        securityLevel: ohosData.SecurityLevel.S1,
      });

      // 查询用户设置
      const predicates = new ohosData.RdbPredicates('UserSettings');
      predicates.equalTo('userId', userId);

      const resultSet = await store.query('UserSettings', predicates);

      if (resultSet.goToFirstRow()) {
        return {
          theme: resultSet.getString(resultSet.getColumnIndex('theme')),
          fontSize: resultSet.getLong(resultSet.getColumnIndex('fontSize')),
          language: resultSet.getString(resultSet.getColumnIndex('language')),
          lastSync: resultSet.getLong(resultSet.getColumnIndex('lastSync')),
        };
      }

      // 返回默认设置
      return initialState.settings;
    } catch (error) {
      return rejectWithValue('加载设置失败');
    }
  }
);

/**
 * 保存用户设置到OpenHarmony数据库
 */
export const saveSettings = createAsyncThunk(
  'user/saveSettings',
  async (settings: UserSettings, { rejectWithValue }) => {
    try {
      const store = await ohosData.getRdbStore({
        name: 'UserSettings.db',
        securityLevel: ohosData.SecurityLevel.S1,
      });

      const valueBucket = {
        'userId': 'default',
        'theme': settings.theme,
        'fontSize': settings.fontSize,
        'language': settings.language,
        'lastSync': Date.now(),
      };

      await store.insert('UserSettings', valueBucket);

      return settings;
    } catch (error) {
      return rejectWithValue('保存设置失败');
    }
  }
);

2.3 Store配置与持久化

import { configureStore } from '@reduxjs/toolkit';
import { combineReducers } from '@reduxjs/toolkit';
import userReducer from './slices/userSlice';
import appReducer from './slices/appSlice';

/**
 * Redux Store配置 - OpenHarmony平台优化版
 */
const rootReducer = combineReducers({
  user: userReducer,
  app: appReducer,
});

export const store = configureStore({
  reducer: rootReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: {
        // 忽略OpenHarmony API调用
        ignoredActions: ['user/loadSettings/fulfilled'],
      },
    }),
  devTools: __DEV__,
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

/**
 * OpenHarmony持久化中间件
 */
export const createPersistenceMiddleware = (store: any) => (
  next: any
) => (action: any) => {
  const result = next(action);

  // 防抖保存:300ms内只保存一次
  if (persistenceTimeout) {
    clearTimeout(persistenceTimeout);
  }

  persistenceTimeout = setTimeout(() => {
    const state = store.getState();
    saveToHarmonyStorage(state);
  }, 300);

  return result;
};

let persistenceTimeout: NodeJS.Timeout | null = null;

/**
 * 保存状态到OpenHarmony存储
 */
async function saveToHarmonyStorage(state: RootState) {
  try {
    const preferences = await ohosData.getPreferences(
      getContext(),
      'redux_state'
    );

    await preferences.put('user_settings', state.user.settings);
    await preferences.flush();

    console.log('[Persistence] State saved to OpenHarmony storage');
  } catch (error) {
    console.error('[Persistence] Failed to save state:', error);
  }
}

三、OpenHarmony平台专项优化

3.1 异步操作适配策略

┌─────────────────────────────────────────────────────────┐
│         OpenHarmony异步任务处理流程                      │
├─────────────────────────────────────────────────────────┤
│                                                          │
│  ┌────────────┐     ┌────────────┐     ┌──────────┐ │
│  │ UI交互    │     │ Dispatch   │     │ 鸿蒙API │ │
│  │ 触发Action │────▶│ AsyncThunk │────▶│ 调用    │ │
│  └────────────┘     └─────┬──────┘     └────┬─────┘ │
│                           │                 │          │
│                    ┌──────▼──────┐         │          │
│                    │ pending状态  │         │          │
│                    │ loading=true │         │          │
│                    └─────────────┘         │          │
│                           │                 │          │
│                    ┌──────▼──────┐         │          │
│                    │ 任务执行     │         │          │
│                    │ 鸿蒙线程池  │         │          │
│                    └──────┬──────┘         │          │
│                           │                 │          │
│              ┌────────────┴────────────┐     │          │
│              │                         │     │          │
│        ┌─────▼─────┐           ┌─────▼─────▼─────┐   │
│        │ fulfilled  │           │     rejected    │   │
│        │ 更新状态  │           │ 记录错误信息   │   │
│        └───────────┘           └────────────────┘   │
└─────────────────────────────────────────────────────────┘

3.2 持久化存储方案对比

存储方式 OpenHarmony API 适用场景 性能 容量限制
Preferences ohos.data.preferences 简单键值对 1MB
关系型数据库 ohos.data.relationalStore 结构化数据 最高 100MB
分布式数据 ohos.data.distributedData 跨设备同步 50MB

推荐配置

  • 用户设置 → Preferences(快速读写)
  • 应用数据 → RelationalStore(复杂查询)
  • 跨设备数据 → DistributedData(自动同步)

3.3 内存管理优化

/**
 * OpenHarmony内存优化Slice
 */
import { createSlice, createEntityAdapter } from '@reduxjs/toolkit';

/**
 * 实体适配器 - 高效管理列表数据
 * 自动生成ID映射和选择器
 */
const listEntityAdapter = createEntityAdapter<ListItem>({
  // 自定义ID选择
  selectId: (item) => item.id,
  // 排序比较函数
  sortComparer: (a, b) => a.timestamp - b.timestamp,
});

/**
 * 列表Slice - 内存优化版
 */
const listSlice = createSlice({
  name: 'list',
  initialState: listEntityAdapter.getInitialState({
    loading: false,
    error: null,
    // 分页元数据
    pagination: {
      page: 1,
      hasMore: true,
    },
  }),
  reducers: {
    /**
     * 添加单个项目(使用实体适配器)
     */
    addOne: listEntityAdapter.addOne,

    /**
     * 批量添加(去重)
     */
    addMany: listEntityAdapter.upsertMany,

    /**
     * 移除项目
     */
    removeOne: listEntityAdapter.removeOne,

    /**
     * 更新项目
     */
    updateOne: listEntityAdapter.updateOne,

    /**
     * 清空列表(保留分页信息)
     */
    clearList: (state) => {
      listEntityAdapter.removeAll(state);
      state.pagination.page = 1;
      state.pagination.hasMore = true;
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchList.pending, (state) => {
        state.loading = true;
      })
      .addCase(fetchList.fulfilled, (state, action) => {
        state.loading = false;
        // 使用实体适配器的智能合并
        listEntityAdapter.upsertMany(state, action.payload.items);
        state.pagination = action.payload.pagination;
      });
  },
});

// 导出选择器
export const {
  selectAll,
  selectById,
  selectIds,
} = listEntityAdapter.getSelectors();

export default listSlice.reducer;

四、完整应用示例

/**
 * ReduxToolkitDemo - Redux Toolkit完整演示
 */
import React, { useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ScrollView } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import {
  toggleTheme,
  setFontSize,
  resetSettings,
  loadSettings,
} from '../store/slices/userSlice';
import type { RootState } from '../store';

export function ReduxToolkitDemo({ onBack }: { onBack: () => void }) {
  const dispatch = useDispatch();
  const userState = useSelector((state: RootState) => state.user);
  const [fontSizeInput, setFontSizeInput] = useState('14');

  // 动态样式计算
  const isDark = userState.settings.theme === 'dark';
  const dynamicStyles = {
    container: {
      backgroundColor: isDark ? '#1a1a1a' : '#f5f5f5',
    },
    content: {
      backgroundColor: isDark ? '#1a1a1a' : '#f5f5f5',
    },
    card: {
      backgroundColor: isDark ? '#2a2a2a' : '#ffffff',
    },
    text: {
      color: isDark ? '#ffffff' : '#333333',
    },
    code: {
      color: isDark ? '#d4d4d4' : '#1e1e1e',
      backgroundColor: isDark ? '#1e1e1e' : '#f5f5f5',
    },
  };

  const handleFontSizeChange = () => {
    const size = parseInt(fontSizeInput, 10);
    if (!isNaN(size) && size >= 12 && size <= 24) {
      dispatch(setFontSize(size));
    }
  };

  return (
    <View style={[styles.container, dynamicStyles.container]}>
      <View style={[styles.header, isDark && styles.headerDark]}>
        <TouchableOpacity onPress={onBack}>
          <Text style={styles.backBtn}></Text>
        </TouchableOpacity>
        <View style={styles.headerContent}>
          <Text style={styles.headerTitle}>Redux Toolkit</Text>
          <Text style={styles.headerSubtitle}>切片配置演示</Text>
        </View>
      </View>

      <ScrollView style={[styles.content, dynamicStyles.content]}>
        {/* 当前状态 */}
        <View style={[styles.card, dynamicStyles.card]}>
          <Text style={[styles.cardTitle, dynamicStyles.text]}>📊 当前状态</Text>
          <View style={styles.stateDisplay}>
            <View style={styles.stateRow}>
              <Text style={styles.stateLabel}>theme:</Text>
              <Text style={[styles.code, dynamicStyles.code]}>
                "{userState.settings.theme}"
              </Text>
              <View
                style={[
                  styles.themeDot,
                  isDark && styles.themeDotDark,
                ]}
              />
            </View>
            <View style={styles.stateRow}>
              <Text style={styles.stateLabel}>fontSize:</Text>
              <Text style={[styles.code, dynamicStyles.code]}>
                {userState.settings.fontSize}px
              </Text>
            </View>
            <View style={styles.stateRow}>
              <Text style={styles.stateLabel}>loading:</Text>
              <Text style={[styles.code, dynamicStyles.code]}>
                {String(userState.loading)}
              </Text>
            </View>
          </View>
        </View>

        {/* Actions */}
        <View style={[styles.card, dynamicStyles.card]}>
          <Text style={[styles.cardTitle, dynamicStyles.text]}>⚡ Dispatch Actions</Text>

          <View style={styles.actionGroup}>
            <Text style={[styles.actionLabel, dynamicStyles.text]}>toggleTheme()</Text>
            <View style={styles.buttonRow}>
              <TouchableOpacity
                style={[
                  styles.themeBtn,
                  styles.lightBtn,
                  !isDark && styles.themeBtnActive,
                ]}
                onPress={() => dispatch(toggleTheme())}
              >
                <Text style={styles.lightBtnText}>☀️ Light</Text>
              </TouchableOpacity>
              <TouchableOpacity
                style={[
                  styles.themeBtn,
                  styles.darkBtn,
                  isDark && styles.themeBtnActive,
                ]}
                onPress={() => dispatch(toggleTheme())}
              >
                <Text style={styles.darkBtnText}>🌙 Dark</Text>
              </TouchableOpacity>
            </View>
          </View>

          <View style={styles.actionGroup}>
            <Text style={[styles.actionLabel, dynamicStyles.text]}>setFontSize(size)</Text>
            <View style={styles.inputRow}>
              <TextInput
                style={[styles.input, dynamicStyles.code]}
                value={fontSizeInput}
                onChangeText={setFontSizeInput}
                keyboardType="number-pad"
                placeholder="12-24"
              />
              <TouchableOpacity
                style={styles.applyBtn}
                onPress={handleFontSizeChange}
              >
                <Text style={styles.applyBtnText}>应用</Text>
              </TouchableOpacity>
            </View>
          </View>

          <View style={styles.actionGroup}>
            <Text style={[styles.actionLabel, dynamicStyles.text]}>loadSettings() - 异步</Text>
            <TouchableOpacity
              style={[styles.asyncBtn, userState.loading && styles.asyncBtnDisabled]}
              onPress={() => dispatch(loadSettings('default') as any)}
              disabled={userState.loading}
            >
              <Text style={styles.asyncBtnText}>
                {userState.loading ? '加载中...' : '🔄 加载设置'}
              </Text>
            </TouchableOpacity>
          </View>

          <TouchableOpacity
            style={styles.resetBtn}
            onPress={() => dispatch(resetSettings())}
          >
            <Text style={styles.resetBtnText}>🔄 重置状态</Text>
          </TouchableOpacity>
        </View>

        {/* 代码示例 */}
        <View style={[styles.card, dynamicStyles.card]}>
          <Text style={[styles.cardTitle, dynamicStyles.text]}>📄 Slice代码</Text>
          <View style={[styles.codeBlock, dynamicStyles.code]}>
            <Text style={[styles.codeText, { color: '#9cdcfe' }]}>
              {`const userSlice = createSlice({
  name: 'user',
  initialState,
  reducers: {
    toggleTheme(state) {
      state.theme = state.theme === 'light'
        ? 'dark' : 'light'
    }
  }
})`}
            </Text>
          </View>
        </View>
      </ScrollView>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#f5f5f5' },
  header: {
    flexDirection: 'row',
    alignItems: 'center',
    padding: 16,
    paddingTop: 48,
    backgroundColor: '#764ABC',
  },
  headerDark: { backgroundColor: '#5a3a9e' },
  backBtn: {
    width: 40, height: 40,
    color: '#fff', fontSize: 24,
    textAlign: 'center',
  },
  headerContent: { flex: 1 },
  headerTitle: {
    fontSize: 20, fontWeight: '700',
    color: '#fff',
  },
  headerSubtitle: {
    fontSize: 14, color: '#fff',
    opacity: 0.9, marginTop: 2,
  },
  content: { flex: 1, padding: 16 },
  card: {
    backgroundColor: '#fff',
    borderRadius: 12,
    padding: 16,
    marginBottom: 16,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
  },
  cardTitle: {
    fontSize: 18, fontWeight: '700',
    color: '#333', marginBottom: 16,
  },
  stateDisplay: {
    backgroundColor: '#1e1e1e',
    borderRadius: 8,
    padding: 16,
  },
  stateRow: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 8,
  },
  stateLabel: {
    fontSize: 13, color: '#9cdcfe',
    marginRight: 8, width: 80,
  },
  code: {
    fontSize: 13, fontFamily: 'monospace',
    backgroundColor: '#1e1e1e',
    color: '#d4d4d4',
    paddingHorizontal: 8,
    paddingVertical: 4,
    borderRadius: 4,
  },
  themeDot: {
    width: 12, height: 12,
    borderRadius: 6,
    backgroundColor: '#f0f0f0',
    borderWidth: 1,
    borderColor: '#666',
  },
  themeDotDark: {
    backgroundColor: '#333',
    borderColor: '#999',
  },
  actionGroup: {
    marginBottom: 16,
  },
  actionLabel: {
    fontSize: 14, fontWeight: '500',
    color: '#666', marginBottom: 10,
  },
  buttonRow: {
    flexDirection: 'row',
    gap: 12,
  },
  themeBtn: {
    flex: 1, padding: 14,
    borderRadius: 8,
    alignItems: 'center',
  },
  themeBtnActive: {
    borderWidth: 2,
    borderColor: '#764ABC',
  },
  lightBtn: { backgroundColor: '#f5f5f5' },
  lightBtnText: {
    fontSize: 14, fontWeight: '600',
    color: '#333',
  },
  darkBtn: { backgroundColor: '#333' },
  darkBtnText: {
    fontSize: 14, fontWeight: '600',
    color: '#fff',
  },
  inputRow: {
    flexDirection: 'row',
    gap: 10,
  },
  input: {
    flex: 1, height: 48,
    borderRadius: 8,
    paddingHorizontal: 16,
    fontSize: 14,
    borderWidth: 1,
    borderColor: '#e0e0e0',
  },
  applyBtn: {
    paddingHorizontal: 20,
    backgroundColor: '#764ABC',
    borderRadius: 8,
    justifyContent: 'center',
  },
  applyBtnText: {
    fontSize: 14, fontWeight: '600',
    color: '#fff',
  },
  asyncBtn: {
    padding: 14,
    backgroundColor: '#03A9F4',
    borderRadius: 8,
    alignItems: 'center',
  },
  asyncBtnDisabled: {
    backgroundColor: '#B0BEC5',
  },
  asyncBtnText: {
    fontSize: 14, fontWeight: '600',
    color: '#fff',
  },
  resetBtn: {
    padding: 14,
    backgroundColor: '#f5f5f5',
    borderWidth: 1,
    borderColor: '#e0e0e0',
    borderRadius: 8,
    alignItems: 'center',
  },
  resetBtnText: {
    fontSize: 14, fontWeight: '600',
    color: '#666',
  },
  codeBlock: {
    borderRadius: 8,
    padding: 16,
  },
  codeText: {
    fontSize: 12,
    fontFamily: 'monospace',
    lineHeight: 18,
  },
});

五、项目源码

完整项目Demo: AtomGitDemos

技术栈:

  • React Native 0.72.5
  • OpenHarmony 6.0.0 (API 20)
  • Redux Toolkit 1.9+
  • TypeScript 4.8.4

社区支持: 开源鸿蒙跨平台社区


📕个人领域 :Linux/C++/java/AI
🚀 个人主页有点流鼻涕 · CSDN
💬 座右铭“向光而行,沐光而生。”

在这里插入图片描述

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐