告别广告侵扰:Pi-hole移动管理客户端开发指南
你是否厌倦了在家庭网络中频繁配置Pi-hole(广告拦截器)?每次调整过滤规则都必须登录网页界面,无法在外出时快速响应家人的网络需求?本文将带你从零构建一个功能完备的React Native移动客户端,通过直观的界面实现Pi-hole全功能管理,让网络广告拦截尽在掌控。## 读完本文你将获得- 完整的Pi-hole API交互方案- React Native应用架构设计模板- 实时统计数...
·
告别广告侵扰:Pi-hole移动管理客户端开发指南
你是否厌倦了在家庭网络中频繁配置Pi-hole(广告拦截器)?每次调整过滤规则都必须登录网页界面,无法在外出时快速响应家人的网络需求?本文将带你从零构建一个功能完备的React Native移动客户端,通过直观的界面实现Pi-hole全功能管理,让网络广告拦截尽在掌控。
读完本文你将获得
- 完整的Pi-hole API交互方案
- React Native应用架构设计模板
- 实时统计数据可视化实现
- 设备级网络控制权限管理
- 跨平台部署最佳实践
项目背景与架构设计
Pi-hole工作原理简析
Pi-hole作为网络级广告拦截器,通过以下流程实现全网广告过滤:
移动客户端架构设计
采用分层架构确保代码可维护性和扩展性:
开发环境搭建
技术栈选择
| 模块 | 技术选择 | 选择理由 |
|---|---|---|
| 前端框架 | React Native 0.74+ | 跨平台支持,热重载,原生性能 |
| 状态管理 | Redux Toolkit | 简化状态管理,内置不可变更新逻辑 |
| 网络请求 | Axios + React Query | 请求缓存,重试机制,类型安全 |
| UI组件库 | React Native Paper | Material Design风格,组件丰富 |
| 图表展示 | Victory Native | 高性能SVG图表,动画支持 |
| 本地存储 | AsyncStorage + Realm | 轻量级键值存储+复杂查询支持 |
环境配置步骤
# 1. 创建新项目
npx react-native init PiholeManager --template react-native-template-typescript
# 2. 安装核心依赖
cd PiholeManager
npm install @reduxjs/toolkit react-redux axios react-query react-native-paper victory-native @react-navigation/native @react-navigation/stack realm
# 3. 安装原生依赖
npx pod-install ios # iOS环境
# Android环境无需额外操作
# 4. 启动开发服务器
npm start
Pi-hole API交互实现
API认证流程解析
通过分析Pi-hole的api.sh脚本,我们梳理出完整的认证流程:
认证实现代码
// services/apiClient.ts
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
export class PiholeApiClient {
private api: AxiosInstance;
private sid: string | null = null;
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
this.api = axios.create({
timeout: 5000,
headers: {
'Content-Type': 'application/json',
'User-Agent': 'PiholeManager/1.0.0'
}
});
}
async testAvailability(): Promise<boolean> {
try {
const response = await this.api.get('/auth');
return response.status === 200 || response.status === 401;
} catch (error) {
console.error('API可用性检测失败:', error);
return false;
}
}
async login(password: string, totp?: string): Promise<boolean> {
try {
const response = await this.api.post('/auth', {
password,
totp: totp || null
});
if (response.data.session?.valid) {
this.sid = response.data.session.sid;
this.api.defaults.headers.common['sid'] = this.sid;
return true;
}
return false;
} catch (error) {
console.error('登录失败:', error);
return false;
}
}
async logout(): Promise<void> {
if (this.sid) {
try {
await this.api.delete('/auth');
} finally {
this.sid = null;
delete this.api.defaults.headers.common['sid'];
}
}
}
// 通用请求方法
async request<T>(config: AxiosRequestConfig): Promise<T> {
try {
const response = await this.api(config);
return response.data;
} catch (error) {
console.error(`请求失败: ${config.method} ${config.url}`, error);
throw error;
}
}
}
核心API端点封装
// services/piholeService.ts
import { PiholeApiClient } from './apiClient';
export interface PiholeStatus {
status: 'enabled' | 'disabled';
gravityLastUpdated: string;
domainsBeingBlocked: number;
dnsQueriesToday: number;
adsBlockedToday: number;
adsPercentageToday: number;
}
export interface QueryLogEntry {
timestamp: number;
type: string;
domain: string;
client: string;
status: string;
}
export class PiholeService {
private apiClient: PiholeApiClient;
constructor(apiClient: PiholeApiClient) {
this.apiClient = apiClient;
}
// 获取Pi-hole状态概览
async getStatus(): Promise<PiholeStatus> {
return this.apiClient.request<PiholeStatus>({
method: 'GET',
url: 'stats/summary'
});
}
// 切换Pi-hole状态
async toggleStatus(enable: boolean): Promise<boolean> {
const response = await this.apiClient.request<{ status: string }>({
method: 'POST',
url: `enable${enable ? '' : '?disable=300'}`, // 禁用默认5分钟
});
return response.status === 'enabled';
}
// 获取查询日志
async getQueryLog(limit = 100): Promise<QueryLogEntry[]> {
return this.apiClient.request<QueryLogEntry[]>({
method: 'GET',
url: `logs?limit=${limit}`
});
}
// 更多API方法实现...
}
应用核心功能开发
仪表盘界面实现
仪表盘作为应用入口,需要直观展示关键指标和快速操作:
// screens/DashboardScreen.tsx
import React, { useEffect, useState } from 'react';
import { View, StyleSheet, RefreshControl } from 'react-native';
import { Text, Card, Button, ActivityIndicator } from 'react-native-paper';
import { useDispatch, useSelector } from 'react-redux';
import { fetchPiholeStatus, togglePiholeStatus } from '../store/piholeSlice';
import { VictoryPie, VictoryBar } from 'victory-native';
const DashboardScreen: React.FC = () => {
const dispatch = useDispatch();
const { status, stats, loading, error } = useSelector((state) => state.pihole);
const [refreshing, setRefreshing] = useState(false);
useEffect(() => {
dispatch(fetchPiholeStatus());
const interval = setInterval(() => {
dispatch(fetchPiholeStatus());
}, 30000); // 每30秒刷新一次
return () => clearInterval(interval);
}, [dispatch]);
const handleRefresh = async () => {
setRefreshing(true);
await dispatch(fetchPiholeStatus());
setRefreshing(false);
};
const handleToggleStatus = () => {
const newStatus = status === 'enabled';
dispatch(togglePiholeStatus(!newStatus));
};
if (loading && !stats) {
return <ActivityIndicator style={styles.loader} size="large" />;
}
return (
<View style={styles.container} refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
}>
<View style={styles.statusContainer}>
<Text style={styles.title}>Pi-hole状态</Text>
<Button
mode={status === 'enabled' ? "contained" : "outlined"}
onPress={handleToggleStatus}
style={styles.statusButton}
>
{status === 'enabled' ? '已启用' : '已禁用'}
</Button>
</View>
<View style={styles.statsGrid}>
<Card style={styles.statCard}>
<Card.Content>
<Text style={styles.statLabel}>今日查询</Text>
<Text style={styles.statValue}>{stats?.dnsQueriesToday.toLocaleString()}</Text>
</Card.Content>
</Card>
<Card style={styles.statCard}>
<Card.Content>
<Text style={styles.statLabel}>已拦截广告</Text>
<Text style={styles.statValue}>{stats?.adsBlockedToday.toLocaleString()}</Text>
</Card.Content>
</Card>
<Card style={styles.statCard}>
<Card.Content>
<Text style={styles.statLabel}>拦截率</Text>
<Text style={styles.statValue}>{stats?.adsPercentageToday.toFixed(1)}%</Text>
</Card.Content>
</Card>
</View>
<Card style={styles.chartCard}>
<Card.Content>
<Text style={styles.chartTitle}>流量分布</Text>
<View style={styles.chartContainer}>
<VictoryPie
data={[
{ x: '已拦截', y: stats?.adsBlockedToday || 0 },
{ x: '已允许', y: (stats?.dnsQueriesToday || 0) - (stats?.adsBlockedToday || 0) }
]}
colors={['#ef5350', '#4caf50']}
innerRadius={50}
labelRadius={70}
style={{ labels: { fontSize: 12 } }}
/>
</View>
</Card.Content>
</Card>
{/* 更多仪表盘内容... */}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
backgroundColor: '#f5f5f5',
},
statusContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 20,
},
title: {
fontSize: 24,
fontWeight: 'bold',
},
statusButton: {
paddingHorizontal: 20,
},
statsGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
marginBottom: 20,
},
statCard: {
width: '48%',
marginBottom: 16,
elevation: 2,
},
statLabel: {
fontSize: 14,
color: '#666',
},
statValue: {
fontSize: 24,
fontWeight: 'bold',
marginTop: 4,
},
chartCard: {
elevation: 2,
marginBottom: 20,
},
chartTitle: {
fontSize: 18,
marginBottom: 16,
fontWeight: 'bold',
},
chartContainer: {
height: 200,
alignItems: 'center',
},
loader: {
flex: 1,
justifyContent: 'center',
},
});
export default DashboardScreen;
实时数据同步实现
使用React Query实现数据获取和缓存管理:
// hooks/usePiholeData.ts
import { useQuery, useMutation, useQueryClient } from 'react-query';
import { PiholeService } from '../services/piholeService';
export function usePiholeStatus() {
return useQuery('piholeStatus',
async () => {
const service = new PiholeService(/* apiClient实例 */);
return service.getStatus();
},
{
refetchInterval: 5000, // 每5秒刷新一次
staleTime: 1000,
}
);
}
export function useTogglePihole() {
const queryClient = useQueryClient();
return useMutation(
async (enable: boolean) => {
const service = new PiholeService(/* apiClient实例 */);
return service.toggleStatus(enable);
},
{
onSuccess: () => {
// 切换成功后刷新状态数据
queryClient.invalidateQueries('piholeStatus');
}
}
);
}
设备管理功能
实现网络中设备的识别和单独控制:
// screens/DevicesScreen.tsx
import React, { useEffect } from 'react';
import { View, StyleSheet, FlatList } from 'react-native';
import { Text, Card, Switch, List } from 'react-native-paper';
import { useQuery } from 'react-query';
import { PiholeService } from '../services/piholeService';
interface Device {
id: string;
name: string;
ip: string;
mac: string;
blocked: boolean;
queryCount: number;
}
const DevicesScreen: React.FC = () => {
const { data: devices, isLoading, refetch } = useQuery<Device[]>(
'piholeDevices',
async () => {
const service = new PiholeService(/* apiClient实例 */);
return service.getClients();
}
);
const handleDeviceToggle = async (deviceId: string, blocked: boolean) => {
const service = new PiholeService(/* apiClient实例 */);
await service.setClientBlockStatus(deviceId, !blocked);
refetch();
};
const renderDeviceItem = ({ item }: { item: Device }) => (
<List.Item
title={item.name || item.ip}
description={`IP: ${item.ip} | 查询: ${item.queryCount}`}
right={() => (
<Switch
value={item.blocked}
onValueChange={() => handleDeviceToggle(item.id, item.blocked)}
/>
)}
/>
);
if (isLoading) {
return <Text>加载中...</Text>;
}
return (
<View style={styles.container}>
<FlatList
data={devices}
renderItem={renderDeviceItem}
keyExtractor={item => item.id}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
});
export default DevicesScreen;
高级功能与优化
离线数据同步
使用Realm数据库实现查询日志的本地存储和离线访问:
// services/databaseService.ts
import Realm from 'realm';
// 定义数据模型
class QueryLogSchema extends Realm.Object {
static schema = {
name: 'QueryLog',
primaryKey: 'id',
properties: {
id: 'string',
timestamp: 'date',
domain: 'string',
client: 'string',
type: 'string',
status: 'string',
},
};
}
export class DatabaseService {
private realm: Realm | null = null;
async init() {
this.realm = await Realm.open({
schema: [QueryLogSchema],
schemaVersion: 1,
});
}
async saveQueryLogs(logs: any[]) {
if (!this.realm) return;
this.realm.write(() => {
logs.forEach(log => {
this.realm!.create('QueryLog', {
id: `${log.timestamp}-${log.domain}`,
timestamp: new Date(log.timestamp * 1000),
domain: log.domain,
client: log.client,
type: log.type,
status: log.status,
}, 'modified');
});
});
}
getLocalQueryLogs(days = 7): any[] {
if (!this.realm) return [];
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - days);
return this.realm.objects('QueryLog')
.filtered('timestamp >= $0', cutoffDate)
.sorted('timestamp', true);
}
// 更多数据库操作方法...
}
应用主题与设置
实现深色/浅色主题切换和个性化设置:
// components/SettingsScreen.tsx
import React, { useState } from 'react';
import { View, StyleSheet } from 'react-native';
import {
Text, Switch, List,
DarkTheme, DefaultTheme,
Provider as PaperProvider
} from 'react-native-paper';
import AsyncStorage from '@react-native-async-storage/async-storage';
const SettingsScreen: React.FC = () => {
const [darkMode, setDarkMode] = useState(false);
const [notifications, setNotifications] = useState(true);
useEffect(() => {
// 从存储加载设置
const loadSettings = async () => {
const savedDarkMode = await AsyncStorage.getItem('darkMode');
if (savedDarkMode !== null) {
setDarkMode(savedDarkMode === 'true');
}
};
loadSettings();
}, []);
const handleDarkModeToggle = async (value: boolean) => {
setDarkMode(value);
await AsyncStorage.setItem('darkMode', value.toString());
// 主题切换逻辑...
};
return (
<View style={styles.container}>
<List.Section title="外观设置">
<List.Item
title="深色模式"
right={() => (
<Switch
value={darkMode}
onValueChange={handleDarkModeToggle}
/>
)}
/>
</List.Section>
<List.Section title="通知设置">
<List.Item
title="拦截通知"
right={() => (
<Switch
value={notifications}
onValueChange={setNotifications}
/>
)}
/>
</List.Section>
{/* 更多设置项... */}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
});
export default SettingsScreen;
应用测试与部署
调试与测试策略
| 测试类型 | 工具/方法 | 重点测试场景 |
|---|---|---|
| 单元测试 | Jest + React Native Testing Library | API服务、状态管理、工具函数 |
| 集成测试 | Detox | 用户流程、API交互 |
| 性能测试 | React Native Performance Monitor | 列表滚动、图表渲染、网络请求 |
| UI测试 | Storybook | 组件样式、主题一致性 |
生产构建与发布
# Android构建
cd android
./gradlew assembleRelease
# iOS构建
cd ios
xcodebuild -workspace PiholeManager.xcworkspace -scheme PiholeManager -configuration Release -archivePath PiholeManager.xcarchive archive
xcodebuild -exportArchive -archivePath PiholeManager.xcarchive -exportPath . -exportOptionsPlist ExportOptions.plist
扩展功能与未来方向
潜在功能扩展
- 高级统计分析:实现周/月/年流量趋势分析,异常检测
- 自定义过滤规则:允许用户添加/管理自定义域名过滤规则
- 家庭共享:多用户权限管理,不同成员不同控制权限
- 自动化规则:基于时间、网络条件自动调整过滤策略
技术演进路线
总结与资源
通过本文介绍的方法,你已掌握使用React Native开发Pi-hole移动客户端的核心技术。这个项目不仅解决了网络广告拦截的移动管理痛点,也展示了如何将复杂后端服务封装为直观的移动应用。
关键知识点回顾
- Pi-hole API认证与会话管理
- React Native状态管理最佳实践
- 实时数据同步与缓存策略
- 移动端数据可视化实现
- 跨平台应用构建与部署
学习资源推荐
立即动手构建属于你的Pi-hole移动客户端,告别广告侵扰,掌控家庭网络!
更多推荐


所有评论(0)