Сверхценный гималайский клиент на основе Electron + React - The Birth of Mob

React.js

предисловие

В прошлом месяце я пристрастился к Гималаям и не мог выбраться. Слушая разговоры, шутки, ежедневные новости и прослушивание английского языка, я мог учиться у рыбы. Когда я был на работе, я страдал от отсутствия десктопного терминала, а в веб-версии были какие-то баги. Стиль отсылает к Moon FM /t/555343, внешний вид достойный, и я себя хорошо чувствую 😜😜😜

Введение

Моб (モブ),Супер сила 100Мужчина номер один.

GitHub: zenghongtu/Mob

на основеElectron, Umi, Dva, AntdПостроить

Функции и пользовательский интерфейс

На данный момент реализованы следующие функции:

  • Базовый музыкальный проигрыватель
  • Каждый день
  • рекомендовать
  • Таблица лидеров
  • Классификация
  • подписка
  • слышал
  • скачать звук
  • поиск по альбому

Технический отбор

Стек технологий:

Причина, по которой я выбрал Umi, заключается в том, что я изучил часть исходного кода в предыдущих проектах, у меня хороший опыт разработки и мало ошибок. Другая причина в том, что когда я искал шаблон, я увидел шаблон этого большого парняwangtianlun/umi-electron-typescript, я использовал его напрямую, что значительно сократило мне время на построение среды разработки.Я хотел бы выразить свою благодарность здесь ~

Если вы не знакомы с Уми и Два, я предлагаю вам изучить его, вы можете начать работу за считанные минуты, и эффективность разработки не должна быть слишком высокой.

Разработка

Проблемы с хуками React

В процессе разработки все компоненты и страницы разрабатываются с использованием React Hooks. И один из самых неуловимых крючков для меня - этоuseEffectНикто.

// ...
useEffect(() => {
  ipcRenderer.on("HOTKEY", handleGlobalShortcut);
  ipcRenderer.on("DOWNLOAD", handleDownloadStatus);
  return () => {
    ipcRenderer.removeListener("HOTKEY", handleGlobalShortcut);
    ipcRenderer.removeListener("DOWNLOAD", handleDownloadStatus);
  };
}, [volume]);
// ...
const handleGlobalShortcut = (e, hotkey) => {
  switch (hotkey) {
    case "nextTrack":
      handleNext();
      break;
    case "prevTrack":
      handlePrev();
      break;
    case "volumeUp":
      const volumeUp = volume > 0.95 ? 1 : volume + 0.05;
      handleVolume(volumeUp * 100);
      break;
    case "volumeDown":
      const volumeDown = volume < 0.05 ? 0 : volume - 0.05;
      handleVolume(volumeDown * 100);
      break;
    case "changePlayState":
      handlePlayPause();
      break;
    default:
      break;
  }
};
// ...

Чтобы уменьшить количество рендеров, я установлю второму параметру значение[volume], но это приводит к некоторым неожиданным ситуациям, например, я запускаюchangePlayState, но не получил ожидаемого значения, на этот раз установлено значение[volume, playState]Это нормально.

Причина проста, потому чтоplayStateНе зависимо, не запускать рендеринг

Итак, этот опыт заключается в том, что когда вы сталкиваетесь с проблемами с хуками, вы можете попробовать добавить их в `useEffect (если этот хук полезен)

Повторное использование компонентов

Сначала взгляните на превью:

Можно обнаружить, что многие компоненты похожи, как улучшить их повторное использование, это способ повысить эффективность разработки.

В этом проекте я не использую компоненты более высокого порядка, но в любом случае контролирую или, скорее,render propsДля повторного использования вызовите указанный жизненный цикл компонента.

Есть три компонента, которые повторно используются во многих других компонентах и ​​страницах:

  • компонент загрузки содержимого страницы
  • компонент обложки альбома
  • Компонент списка альбомов

Компоненты загрузки содержимого страницы следующие:

export interface Content<T, R> {
  render: (result: Result) => React.ReactNode;
  genRequestList: (params?: R[]) => Array<Promise<T>>;
  rspHandler: (rspArr: any, lastResult?: any) => Result;
  params?: R[];
}

export default function({
  params, // api 的请求参数
  genRequestList, // 负责返回 api 请求列表,返回值会被`Content`调用请求数据,返回值给`rspHandler`
  rspHandler, // 处理请求返回的`Response`值,返回值给`render`
  render //  负责渲染结果,将值传递给`render`函数中的组件
}: Content<any, any>) {
  const [loading, setLoading] = useState(true);
  const [hasError, setError] = useState(false);
  const [result, setResult] = useState(null);
  useEffect(() => {
    (async () => {
      try {
        setLoading(true);
        setError(false);
        const rspArr = await Promise.all(genRequestList(params));
        setResult(rspHandler(rspArr, result));
      } catch (e) {
        setError(true);
      } finally {
        setLoading(false);
      }
    })();
  }, [params]);
  return (
    <div className={styles.contentWrap}>
      {loading && !result ? (
        <div className={styles.loading}>
          <Loading />
        </div>
      ) : hasError ? (
        <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
      ) : (
        render(result)
      )}
    </div>
  );
}

Используйте кеш для улучшения опыта

Инкционируйте запрос на получение AXIOS и генерируйте уникальное значение для каждого URL-адреса. Если он находится в белом, он будет храниться в хранилище сеанса. Время истечения по умолчанию составляет 3600, а значение будет возвращено непосредственно в следующем посещении. Отказ

Одной из проблем.

const request = ({ whitelist = [], expiry = DEFAULT_EXPIRY }) => ({
  ...instance,
  get: async (url: string, config?: AxiosRequestConfig) => {
    if (config) {
      config.url = url;
    }
    const fingerprint = JSON.stringify(config || url);
    // 判断是否需要缓存
    const isNeedCache = !whitelist.length || whitelist.includes(url);
    // 生成唯一值
    const hashKey = hash
      .sha256()
      .update(fingerprint)
      .digest("hex");

    if (expiry !== 0) {
      const cached = sessionStorage.getItem(hashKey);
      const lastCachedTS: number = +sessionStorage.getItem(`${hashKey}:TS`);
      if (cached !== null && lastCachedTS !== null) {
        const age = (Date.now() - lastCachedTS) / 1000;
        // 如果没有过期,就直接返回该值
        if (age < expiry) {
          return JSON.parse(cached);
        }
        // 否则清除之前的旧值
        sessionStorage.removeItem(hashKey);
        sessionStorage.removeItem(`${hashKey}:TS`);
      }
    }

    const rsp = await instance.get(url, config);

    if (isNeedCache) {
      cacheRsp(rsp, hashKey);
    }
    return rsp;
  }
});

export default request({ whitelist: [] });

Flex justify-content: space-betweenпроблема с последней строкой

установить во флексеjustify-content: space-between, в последней строчке что-то неприятное.

Для этой проблемы мой подход заключается в вычислении, а затем заполнении пустогоdivвходить.

const DEFAULT_WIDTH = 130;
const DEFAULT_PAGE_COUNT = 130;
const DEFAULT_WINDOW_WIDTH = 1040;
export default function({
  siderWidth = SIDE_BAR_WIDTH,
  pageCount = DEFAULT_PAGE_COUNT,
  divWidth = DEFAULT_WIDTH
}) {
  const [fillCount, setFillCount] = useState(0);
  const handleResize = debounce(e => {
    let innerWidth: number;
    if (e) {
      innerWidth = e.target.innerWidth;
    }
    // 当前容器的宽度
    const containerWidth = innerWidth || DEFAULT_WINDOW_WIDTH - siderWidth;
    // 每一行可以放的个数
    const rowDivCount = Math.floor(containerWidth / divWidth);
    // 需要填充的个数
    const count = rowDivCount - (pageCount % rowDivCount);
    setFillCount(count);
  }, 100);

  useEffect(() => {
    handleResize();
    window.addEventListener("resize", handleResize);
    return () => {
      window.removeEventListener("resize", handleResize);
    };
  }, []);
  return (
    <>
      {fillCount
        ? // 按照填充个数填进去
          Array.from({ length: fillCount }).map((_, idx) => {
            return (
              <div
                key={idx}
                style={{ width: divWidth, height: 0 }}
                className={styles.filler}
              />
            );
          })
        : null}
    </>
  );
}

Маршрутизация вперед и назад

существуетumiИли скорееreact-router, и толькоmemory-routerМожно судить, может ли он двигаться вперед или назад.

Вы можете только записать индекс самостоятельно, а затем сделать суждение.

let lastHistoryLen = 0;
const NavBar = ({ history, isLogin }) => {
  const { length, action } = history;

  const [curIndx, setCurIndx] = useState(0);
  const [suggests, setSuggests] = useState(null);
  const [text, setText] = useState('');
  const [visible, setVisible] = useState(false);

  useEffect(() => {
   // 判断最后历史记录的长度是否大于当前历史记录长度,如果是的话,把 index 归零
    if (lastHistoryLen > length) {
      setCurIndx(0);
    }
    lastHistoryLen = length;
  });

  const fetchSuggests = debounce(async (kw) => {
    if (!kw) {
      setSuggests(null);
      return;
    }
    const {
      data: { result },
    }: { data: SuggestRspData } = await getSuggest({ kw });
    let suggests = [...result.albumResultList, ...result.queryResultList];
    if (suggests.length < 1) {
      suggests = null;
    }
    // todo (only support albumResult now)
    setSuggests(suggests);
  }, 200);

// ...

  const handleArrowClick = (n) => {
    return () => {
      setCurIndx(curIndx + n);
      router.go(n);
    };
  };

Как войти

Изначально я хотел проанализировать интерфейс входа в систему, но если бы я это сделал, добавление скан-кода для входа заняло бы много времени.

Поэтому я подумал об использованииwebviewВстроить страницу входа.После входа в систему, если личная страница открыта, вход выполнен успешно.

const TARGET_URL = "www.ximalaya.com/passport/sync_set";
const COOKIE_URL = "https://www.ximalaya.com";
const WebView = ({ onLoadedSession }) => {
  const [isLoading, setLoading] = useState(true);
  useEffect(() => {
    const webview = document.querySelector("#xmlyWebView") as HTMLElement;
    const handleDOMReady = e => {
      if (webview.getURL().includes(TARGET_URL)) {
        // todo fix prevent redirect
        e.preventDefault();
        const { session } = webview.getWebContents();
        onLoadedSession(session, COOKIE_URL);
        webview.reload();
      }
    };
    const handleLoadCommit = () => {
      setLoading(true);
    };
    const handleDidFinishLoad = () => {
      setLoading(false);
    };
    webview.addEventListener("dom-ready", handleDOMReady);
    webview.addEventListener("load-commit", handleLoadCommit);
    webview.addEventListener("did-finish-load", handleDidFinishLoad);
    return () => {
      webview.removeEventListener("dom-ready", handleDOMReady);
      webview.removeEventListener("load-commit", handleLoadCommit);
      webview.removeEventListener("did-finish-load", handleDidFinishLoad);
    };
  }, []);

  const props = {
    id: "xmlyWebView",
    useragent:
      // tslint:disable-next-line:max-line-length
      "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36",
    src: `https://${TARGET_URL}`,
    style: { widht: "750px", height: "600px" }
  };
  return (
    <div>
      <Spin tip="Loading..." spinning={isLoading}>
        <webview {...props} />
      </Spin>
    </div>
  );
};

наконец

Надеюсь, эта статья поможет вам.

Скачайте и испытайте