从零开始手撸一个阅读器--换源功能的实现(3)

149 阅读3分钟

概述

上一篇介绍了阅读器书源数据解析功能的实现。书源切换功能的实现依赖于网站站点的搜索功能。如果没有搜书和搜索不到对应小说,则无法定位小说。

实现

  1. 主要是请求所配置网站的搜书功能。如果得到的搜索结果中书名和作者相同,那么就判定这本小说和当前正在阅读的小说是同一本小说。其书源地址为小说目录页的地址(${item.origin}${item.pathname}),在切换书源的时候重新解析目录页数据,重新渲染。
  2. 错误处理:api,searchBook({ fuzzy: props.bookInfo.bookName, limit: 10, ...pagination, origins })中会定向请求origins所需要的书源,在getOriginList中处理需要请求的书源
    • 过滤书源管理中所禁用的书源source.filter((i) => !disable.includes(i.origin))
    • 过滤已经请求过的书源source.filter((i) => loaded.value.findIndex((j) => i.origin === j.origin) === -1)
    • 设置最大请求数为3:防止重复的异常请求导致ip被封禁
    • 如果该超最大请求数,可能是书源站点未收录当前小说,忽略该书源const maxLoadNumber = 3;const errorLoaded = ref<{ origin: string; loadNumber: number }[]>([]);
  3. 每次页面打开的时候会去重新加载未存在的书源,主要目的是因为当前网络异常或者第三方站点服务异常亦或者一些其他原因导致服务不通。
<template>
  <g-page>
    <g-popup v-model:showDialog="showDialog" type="center" :showCancel="false">
      <view class="container">
        <scroll-view :style="{ height: `60vh` }" scroll-y="true">
          <view
            :class="['item', bookInfo.origin === item.origin ? 'active' : '']"
            v-for="item in loaded"
            @click="setOrigin(item)"
          >
            <view class="title">{{ bookInfo.bookName }}</view>
            <view class="author">{{ bookInfo.author }}</view>
            <view class="link">{{ `${item.origin}${item.pathname}` }}</view>
          </view>

          <view class="loading" v-if="loading">
            <view class="tip">加载中</view>
            <view :class="store.appOption.theme === 'dark' ? 'loader' : 'loader-dark'"></view>
          </view>
        </scroll-view>
        <view class="btn">
          <view class="hz-button-primary" @click="showDialog = false">取消</view>
        </view>
      </view>
      >
    </g-popup>
  </g-page>
</template>

<script setup lang="ts">
import store from "@/store";
import sourceJson from "@/parser/source";
import { ref, reactive } from "vue";
import { onLoad, onUnload } from "@dcloudio/uni-app";
import * as api from "@/api/common";
import parser from "@/parser/index";

interface SourceItem {
  name: string;
  origin: string;
}

const emit = defineEmits<{
  (event: "handleOriginsChange", origins: Origins): void;
  (event: "setOrigin", origin: OriginItem): void;
}>();

const props = defineProps<{ bookInfo: bookInfo }>();
const pagination = reactive({
  pageSize: 3,
  pageNo: 1,
});
const showDialog = ref(false);
const loading = ref(false);
const breakRequest = ref(false);
const source: SourceItem[] = reactive(sourceJson.local);
const loaded = ref(props.bookInfo.origins);
const maxLoadNumber = 3;
const errorLoaded = ref<{ origin: string; loadNumber: number }[]>([]);

function load() {
  let loadOrigins = getOriginList();

  if (loadOrigins.length === 0) {
    loading.value = false;
    return;
  }

  let origins: Array<string> = [];
  loadOrigins.forEach((i) => {
    const isInError = errorLoaded.value.find((e) => e.origin === i);
    if (isInError) {
      // 最后处理加载失败的书源
      origins.push(i);
    } else {
      // 优先处理未加载过的书源
      origins.unshift(i);
    }
  });
  origins = origins.slice(0, 3);

  loading.value = true;
  // #ifdef H5
  api
    .searchBook({ fuzzy: props.bookInfo.bookName, limit: 10, ...pagination, origins })
    .then((res) => {
      const data = res.data.data as searchBookList[];
      handlerResult(data, origins);
    })
    .catch((e) => {
      handlerErr(origins);
    })
    .finally(() => {
      uni.hideLoading();
    });
  // #endif

  // #ifdef APP-PLUS
  parser
    .searchBook({ fuzzy: props.bookInfo.bookName, limit: 10, ...pagination, origins })
    .then((res) => {
      const data = res.data as searchBookList[];
      handlerResult(data, origins);
    })
    .catch((e) => {
      handlerErr(origins);
    })
    .finally(() => {
      uni.hideLoading();
    });
  // #endif
}
function handlerErr(origins: Array<string>) {
  origins.forEach((i) => {
    const inError = errorLoaded.value.findIndex((e) => e.origin === i);
    if (inError > -1) {
      errorLoaded.value[inError].loadNumber++;
    } else {
      errorLoaded.value.push({
        origin: i,
        loadNumber: 1,
      });
    }
  });

  if (!breakRequest.value) {
    load();
  }
}
function handlerResult(data: searchBookList[], origins: string[]) {
  const result = data.filter((i) => i.bookName === props.bookInfo.bookName && i.author === props.bookInfo.author);

  const realRequest = origins.slice(0, pagination.pageSize);
  realRequest.forEach((i) => {
    const value = result.find((j) => j.origin === i);

    if (!value) {
      const inError = errorLoaded.value.findIndex((e) => e.origin === i);
      if (inError > -1) {
        errorLoaded.value[inError].loadNumber++;
      } else {
        errorLoaded.value.push({
          origin: i,
          loadNumber: 1,
        });
      }
    }
  });

  result.forEach((i) => {
    if (!loaded.value.find((l) => l.origin === i.origin)) {
      loaded.value.push({
        origin: `${i.origin}`,
        pathname: `${i.pathname}`,
      });
    }
  });
  saveOrgins();

  if (!breakRequest.value) {
    load();
  }
}

function saveOrgins() {
  emit("handleOriginsChange", loaded.value);
}

function setOrigin(item: OriginItem) {
  emit("setOrigin", item);
}

function open() {
  showDialog.value = true;
}

function close() {
  showDialog.value = false;
}

defineExpose({
  open,
  close,
});

function getOriginList() {
  const disable = store.sourceMap.value.filter((i) => !i.enable).map((i) => i.origin);

  const enable = source
    .filter((i) => !disable.includes(i.origin))
    .filter((i) => loaded.value.findIndex((j) => i.origin === j.origin) === -1)
    .map((i) => i.origin);

  // 如果超最大请求数,忽略
  errorLoaded.value.forEach((i) => {
    if (i.loadNumber > maxLoadNumber) {
      const idx = enable.findIndex((j) => j === i.origin);
      if (idx > -1) {
        enable.splice(idx, 1);
      }
    }
  });
  return enable;
}

onLoad(() => {
  load();
});
onUnload(() => {
  breakRequest.value = true;
});
</script>

效果

1.gif

相关