最近一個月沉迷喜馬拉雅沒法自拔,聽相聲、段子、每日新聞,還有英語聽力,摸魚學習兩不誤。上班時候苦於沒有桌面端,用網頁版有些 bug,官方也不搞一個,只好本身動手了。樣式參考了一下 Moon FM /t/555343,顏值還過得去,自我感受挺好 😜😜😜javascript
Mob(モブ), 異能超能 100的男一號。java
GitHub: zenghongtu/Mob
目前實現的功能有這些:ios
技術棧:git
之因此選擇 Umi 是由於在以前項目中研究過其部分源碼,開發體驗感不錯,並且 bug 也少。
還有一個緣由是我在找模板的過程當中,看到這個大佬的模板wangtianlun/umi-electron-typescript,就直接拿來用了,大大減小了我搭建開發環境的時間,在此表示感謝~github
若是你對 Umi 和 Dva 不熟,牆裂建議去學一下,分分鐘就能夠上手,並且開發效率要提升的不要太多。
在開發中,全部組件、頁面都是使用 React Hooks 進行開發的。而讓我以爲最難以琢磨的一個 hooks 非 useEffect
莫屬。web
// ... 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]
就正常了。typescript
緣由很簡單,由於playState
不在依賴中,不會觸發從新渲染axios
因此這條經驗就是在使用 hook 遇到問題時,能夠先試一下添加到`useEffect·中(若是有用到這個 hook 的話)api
先來看一下預覽:
能夠發現不少組件是類似的,如何提升他們的複用,這一個提升開發效率的途徑。
在這個項目中我沒有使用高階組件,而是經過反正控制或者說是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 的 get 請求進行封裝,對每一個請求 url 生成惟一值,若是在白名單內,存入 session storage 中,默認過時時間是 3600s,在下次訪問時,直接返回該值。
這樣作的一個問題是沒法得到最新數據,但對比體驗感來講並不那麼嚴重。
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: [] });
justify-content: space-between
最後一行問題在 flex 中設置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
能夠判斷是否能夠前進或者後退。
只能本身記錄一下 index,而後進行判斷。
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> ); };
但願這篇文章能對你有所幫助。