предисловие
В прошлом месяце я пристрастился к Гималаям и не мог выбраться. Слушая разговоры, шутки, ежедневные новости и прослушивание английского языка, я мог учиться у рыбы. Когда я был на работе, я страдал от отсутствия десктопного терминала, а в веб-версии были какие-то баги. Стиль отсылает к Moon FM /t/555343, внешний вид достойный, и я себя хорошо чувствую 😜😜😜
Введение
Моб (モブ),Супер сила 100Мужчина номер один.
GitHub: zenghongtu/Mob
Функции и пользовательский интерфейс
На данный момент реализованы следующие функции:
- Базовый музыкальный проигрыватель
- Каждый день
- рекомендовать
- Таблица лидеров
- Классификация
- подписка
- слышал
- скачать звук
- поиск по альбому
Технический отбор
Стек технологий:
Причина, по которой я выбрал 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>
);
};
наконец
Надеюсь, эта статья поможет вам.