Appearance
业务篇-思考并补全遗漏的功能细节,整体优化代码,让应用更健壮
前言
前边文章以功能需求开发为主,但回过头看,貌似每个功能点都相对“割裂”,接下来让我们补全所遗漏的功能交互点,让整个应用更加健壮可用。如果你对本章节内容兴趣不大,可以快速阅读或跳过。
功能一:空模版/模版不可预览异常处理
在第十五章我们实现了简历模版功能,并且添加了侧边栏的拉伸收起效果,但对于空模版或模版不可预览的异常并未进行处理。废话不多说,整起!
1.1 点击模版,存入 Redux
前往 /renderer/container/templateList/Navigation
,我们修改 index.tsx
代码
下面是伪代码,部分代码进行省略,建议大家直接看线上代码
ts
// /renderer/container/templateList/Navigation/index.tsx
import { useDispatch, useSelector } from 'react-redux';
function Navigation() {
const dispatch = useDispatch();
// 👇 选中模版,存入Redux
const onSelect = (template: TSTemplate.Item) => {
dispatch({
type: 'templateModel/setStore',
payload: {
key: 'selectTemplate',
values: template,
},
});
};
return (
<div styleName="navigation">
<MyScrollBox maxHeight={height - HEADER_HEIGHT}>
{templateList &&
templateList.length > 0 &&
templateList.map((template: TSTemplate.Item) => {
return (
<div styleName="template" key={template?.templateId}>
{/* 👇 部分代码忽略,为预览模版按钮注册点击事件 */}
{selectTemplate?.templateId !== template?.templateId && (
<MyButton size="middle" className="view-btn" onClick={() => onSelect(template)}>
预览模版
</MyButton>
)}
</div>
);
})}
</MyScrollBox>
</div>
);
}
export default Navigation;
1.2 添加空缺省组件
我们切换模版,将当前模版存入 Redux 中,接下来需要在“内容”区域,根据选中的模版进行条件判断,存在模版则展示出来,不存在则显示缺省组件。在此之前,我们先简单实现一下空缺省组件。
前往 renderer/common/components
,我们在公共组件下,新增一个名为 MyEmpty
的文件夹,主要是空数据状态时的说明,效果为一张图 + 一句话
,让我们来写一下代码
ts
// renderer/common/components/MyEmpty/index.tsx
import React from 'react';
import './index.less';
export type SizeType = 'small' | 'big';
interface IEmptyProps {
imgSrc: string;
size?: SizeType;
label?: string;
style?: React.CSSProperties;
}
function MyEmpty({ imgSrc, size = 'small', style, label }: IEmptyProps) {
return (
<div styleName="empty">
<img src={imgSrc} style={style} styleName={`img-${size}`} />
{label && <p styleName="label">{label}</p>}
</div>
);
}
export default MyEmpty;
1.3 遇到问题
这里有一个很蛋疼的问题:如何保证模版组件和模版封面是一一对应关系?举个例子:
- 在
assets
静态资源文件夹下,有一个模版封面template1.png
- 在
renderer/container/templates
模版仓库中,有一个模版组件<TemplateOne />
这时候如何保证 template1.png
对应的就是 <TemplateOne />
? 我们能在主观意识上,清楚知道,模版封面 1 对应模版组件 1,但计算机并不知道,代码该如何写?
假设我将 template1.png
改成 template2.png
,是否意味着,这张模版封面对应<TemplateTwo />
呢?答案很显然:并不是!
这个问题貌似很难被解决,举个场景,现在存在 10 张模版封面,对应 10 个模版组件,在应用加载时,我们通过读取静态资源文件,假设以有序的形式展示我们的封面,伪代码为:
javascript
fileAction.readDir(assetPath).then((files) => {
console.log(files);
// ['template1.png', 'template2.png', 'template3.png', 'template4.png', ...]
});
我们可以通过“投机取巧”的方式,将读到的模版封面的文件索引存入到 redux 中
javascript
fileAction.readDir(assetPath).then((files) => {
if (files.length > 0) {
let templateList = files.map((fileName, index) => {
const base64URL = await fileAction.read(`${appPath}assets/template/${fileName}`, 'base64');
return {
templateName: fileName,
templateId: createUID(),
// 👇 这里记录索引,将其顺带存入到 Redux 中
templateIndex: index + 1,
templateCover: `data:image/png;base64,${base64URL}`,
};
});
// 👇 存入 Redux 的逻辑,这里就不展示了
}
});
之后我们可以通过索引对应到模版组件
ts
{selectTemplate?.templateIndex === 1 && <TemplateOne />}
{selectTemplate?.templateIndex === 2 && <TemplateTwo />}
{selectTemplate?.templateIndex === 3 && <TemplateThree />}
{selectTemplate?.templateIndex === 4 && <TemplateFour />}
这种方式纯属于投机取巧,这要求我们的模版封面必须是按照顺序进行命名,并且每次新增一个模版封面,还需要去添加对应的模版组件,以及渲染的条件语句
如果中途我们将模版封面的命名打乱,显而易见的后果就是:模版与封面对应不上。
老实说,该问题我还未有较好的解决方案,目前有个小思路,但还需要经过验证可行性。为此我们先将其放一放,继续往下开发。
1.4 暂时缓缓,继续开发
如何实现封面与模版的对应关系?暂时以上边所说的索引进行简单实现,前往 renderer/hooks
找到 useReadDirAssetsTemplateHooks.ts
文件,进行修改,不要忘记了,对于类型声明也需要对应修改
ts
// renderer/hooks/useReadDirAssetsTemplateHooks.ts
// 找到这段代码
if (files.length > 0) {
let templateList: TSTemplate.Item[] = [];
for (let idx = 0; idx < files.length; idx++) {
const base64URL = await fileAction.read(`${appPath}assets/template/${files[idx]}`, 'base64');
templateList.push({
templateName: files[idx],
// 👇 添加索引
templateIndex: idx,
templateId: createUID(),
templateCover: `data:image/png;base64,${base64URL}`,
});
}
}
ts
// renderer/common/types/template.d.ts
declare namespace TSTemplate {
export interface Item {
// 👇 添加此类型说明
/**
* @description 模版下标
*/
templateIndex: number;
}
}
紧接着,前往 renderer/container/templateList/StaticResume
组件,我们在这里需要做模版不存在或无法预览时的异常处理了。我们来修改一下代码,大家记得看注释
ts
import React from 'react';
import './index.less';
import { shell } from 'electron';
import * as TemplateList from '@src/container/templates';
import Footer from '../Footer';
import MyScrollBox from '@common/components/MyScrollBox';
import { useSelector } from 'react-redux';
import MyEmpty from '@common/components/MyEmpty';
import EmptyPng from '@assets/icon/empty.png';
import MyButton from '@common/components/MyButton';
// 👇 1. 合法且存在的简历模版,因为我们存在两个模版封面,但只有一个模版组件
const VALID_TEMPLATE = [0];
function StaticResume() {
const HEADER_HEIGHT = 76; // 距离头部距离
const height = document.body.clientHeight;
const selectTemplate: TSTemplate.Item = useSelector((state: any) => state.templateModel.selectTemplate);
// 👇 2. 下面判断该模版是否合法且存在组件模版
const isIncludeTemplate = VALID_TEMPLATE.includes(selectTemplate.templateIndex);
const isValidTemplate = selectTemplate.templateId && selectTemplate.templateIndex !== -1;
return (
<div styleName="container">
<MyScrollBox maxHeight={height - HEADER_HEIGHT}>
{isValidTemplate && isIncludeTemplate && (
<>
{selectTemplate.templateIndex === 0 && <TemplateList.TemplateOne />}
<Footer />
</>
)}
{/* 👇 3. 缺省页说明 */}
{isValidTemplate && !isIncludeTemplate && (
<LackDesc label="暂未开发此模版,欢迎点击下方按钮进行模版贡献" />
)}
{!isValidTemplate && (
<LackDesc label="暂无模版数据,欢迎点击下方按钮进行模版贡献" />
)}
</MyScrollBox>
</div>
);
}
export default StaticResume;
const LackDesc = React.memo(({ label }: { label: string }) => {
return (
<div styleName="empty">
<MyEmpty imgSrc={EmptyPng} label={label} />
<div styleName="footer">
<MyButton
size="middle"
className="use-btn"
onClick={() => {
shell.openExternal('https://github.com/PDKSophia/visResumeMook/issues/4');
}}
>
贡献模版
</MyButton>
</div>
</div>
);
});
最终效果如图所示,👉 点击查看功能一 commit 代码
功能二:从模版列表跳转至简历制作
上边实现了预览模版功能,但也仅仅只是预览而已,接下来我们实现从预览模版到简历制作。
我们先来看一下,简历制作的入口有哪些?
- 从首页进入
- 从模版列表页面点击模版制作进入
会有不同的入口,并且会以不同的模版进行制作,那么我们在简历制作页面,就得知道:当前的入口来源、当前的模版信息
2.1 路由改写
如何让简历制作页面获取到上述的两个关键信息呢?
- Redux 响应式
在跳转前,将所需要的信息存到 Redux 中,进入到最终页面时再将数据取出
- Router 路由式
我们可以将一些关键信息携带到路由中,进入到最终页面时解析路由,获取数据。
经过对比,第一种方案不太推荐,它需要我们在 Redux 中维护一些数据,为此我们采用第二种方案解决。但写路由又是一门学问,比如说写成 /resume?id=1&idx=1
还是写成 /resume/:id/:idx
前者需要我们自行实现路由参数的拼接与解析,后者有现成的库使用,想必大家都写过这样的路由 /resume/:id/:idx
,本质是通过 path-to-regexp 库实现,接下来我将在项目中引入该库,修改我们的路由。我们先来安装
npm install path-to-regexp --save-dev
前往 renderer/common/utils
中修改 router.ts
文件,我们新增一个方法,用于合并参数
ts
// renderer/common/utils/router.ts
import { compile } from 'path-to-regexp';
export function compilePath(route: string, params?: { [key: string]: any }) {
const toPath = compile(route, { encode: encodeURIComponent });
return toPath(params);
}
接着我们将所有的路由都进行修改,在 renderer
文件夹下全局查找所有通过 history.push(route)
都改成 history.push(compilePath(route))
所有的改写就不一一展示,大家可自行修改。
2.2 首页跳转处修改
前往 app/renderer/common/constants
修改路由参数定义
ts
// renderer/common/constants/router.ts
const ROUTER = {
root: '/',
// 👇 这里我们改一下简历制作的路由,规则:/来源/模版ID/模版索引
resume: '/resume/:fromPath/:templateId/:templateIndex',
template: '/template',
};
接着进入 renderer/container/root
首页,找到入口文件 index.tsx
,我们修改跳转逻辑代码
ts
const onRouterToLink = (router: TSRouter.Item) => {
if (isHttpOrHttpsUrl(router.url)) {
shell.openExternal(router.url);
} else {
if (router.key !== ROUTER_KEY.resume) {
history.push(compilePath(router.url));
} else {
// 👇 跳转路由的修改
history.push(
compilePath(router.url, {
fromPath: ROUTER_KEY.root,
templateId: selectTemplate?.templateId,
templateIndex: selectTemplate?.templateIndex,
})
);
}
}
};
2.3 简历模版跳转处修改
前往 renderer/container/templateList/Footer
首页模块,找到入口文件 index.tsx
,我们修改一下跳转逻辑代码
ts
// app/renderer/container/templateList/Footer/index.tsx
import React from 'react';
import './index.less';
import { useHistory } from 'react-router';
import { useSelector } from 'react-redux';
import { ROUTER_KEY } from '@common/constants/router';
import { compilePath } from '@common/utils/router';
import MyButton from '@common/components/MyButton';
import ROUTER from '@common/constants/router';
function Footer() {
const history = useHistory();
const selectTemplate = useSelector((state: any) => state.templateModel.selectTemplate);
const onMadeResume = (router: TSRouter.Item) => {
// 👇 跳转路由的修改
history.push(
compilePath(ROUTER.resume, {
fromPath: ROUTER_KEY.templateList,
templateId: selectTemplate?.templateId,
templateIndex: selectTemplate?.templateIndex,
})
);
};
return (
<div styleName="footer">
<MyButton size="middle" className="use-btn" onClick={onMadeResume}>
以此模版前往制作简历
</MyButton>
</div>
);
}
export default Footer;
2.4 简历制作处的路由参数获取,正确返回
由于我们的入口来源不同,所以在“返回”时,也需做对应的处理。我们可以通过 useParams
这个 hooks,得到路由中的数据,根据数据的 fromPath
不同,进行对应的处理。
前往 /renderer/container/resume/components/ResumeActions
,找到入口文件 index.tsx
,进行修改,下面是伪代码
ts
// renderer/container/resume/components/ResumeActions/index.tsx
import { useHistory, useParams } from 'react-router';
import { compilePath } from '@common/utils/router';
import ROUTER, { ROUTER_KEY } from '@common/constants/router';
function ResumeAction() {
const history = useHistory();
// 👇 定义参数类型
const routerParams = useParams<{
fromPath: string;
templateId: string;
templateIndex: string;
}>();
// 返回首页
const onBack = () => {
if (routerParams?.fromPath === ROUTER_KEY.root) {
history.push(compilePath(ROUTER.root));
} else if (routerParams?.fromPath === ROUTER_KEY.templateList) {
history.push(compilePath(ROUTER.templateList));
} else {
console.log('here');
}
};
return (
<div styleName="actions">
<div styleName="back" onClick={onBack}>返回</div>
</div>
);
}
export default ResumeAction;
2.5 简历制作处的路由参数获取,正确显示模版
如上所示,我们从 useParams
这个 hooks,得到路由中的数据,根据模版 ID 和模版索引,进而加载展示正确的模版,如果你认真看了上边的功能一,想必此问题对你来说也并非难事,这里就不进行多余讲解。
也算是一个小测试,小伙伴们可自行实现。
功能三:实现 KeepAlive 效果
想必小伙伴们现在会有一个很蛋疼的事情,那就是没有页面缓存的效果,记得以前写 Vue 时,页面缓存就很方便,只需要这么写,就能实现效果
javascript
// 这里以 vue2 为例
<!-- 失活的组件将会被缓存!-->
<keep-alive>
<component v-bind:is="currentTabComponent"></component>
</keep-alive>
页面缓存的原理,我猜想应该是通过 display:none 的方式实现,感兴趣的可以私下去了解,或者评论区发表你的观点
那我们在 React 中,如何实现页面缓存呢?有一个 react-router-cache-route 库,可以替我们实现,使用也极其简单,让我们先来安装一下
npm install react-router-cache-route --save-dev
前往 /renderer/router.tsx
修改一下路由组件
ts
// renderer/router.tsx
import React, { useEffect } from 'react';
// 👇 引入,实现页面缓存
import CacheRoute, { CacheSwitch } from 'react-router-cache-route';
import { HashRouter, Redirect } from 'react-router-dom';
import Root from '@src/container/root';
import Resume from '@src/container/resume';
import TemplateList from '@src/container/templateList';
import ROUTER from '@common/constants/router';
import useThemeActionHooks from './hooks/useThemeActionHooks';
import useReadDirAssetsTemplateHooks from './hooks/useReadDirAssetsTemplateHooks';
function Router() {
const readDirAssetsTemplateHooks = useReadDirAssetsTemplateHooks();
const initThemeConfig = useThemeActionHooks.useInitThemeConfig();
useEffect(() => {
initThemeConfig();
readDirAssetsTemplateHooks();
}, []);
return (
<HashRouter>
<CacheSwitch>
<CacheRoute path={ROUTER.root} exact component={Root} />
<CacheRoute path={ROUTER.resume} exact component={Resume} />
<CacheRoute path={ROUTER.templateList} exact component={TemplateList} />
<Redirect from={ROUTER.root} exact to={ROUTER.root} />
</CacheSwitch>
</HashRouter>
);
}
export default Router;
这样就实现了页面缓存,你可以做个尝试,任意进入一个路由,在该路由下刷新页面,看看是否会停留在此页面下(之前是会回到首页)
功能四:简历制作内容高度有误
当我们实现了上述的三个功能之后,就会引出此问题,如何复现呢?
复现步骤:首页 -> 进入简历制作页 -> 在简历制作页下刷新 -> 高度错误,下面是一张异常图片
不对劲,为什么简历的显示高度会出现异常,这与我们期望不符。我们来排查一下,进入到 renderer/container/resume/ResumeContent
中,打印一下我们的高度
ts
// renderer/container/resume/ResumeContent/index.tsx
function ResumeContent() {
const HEADER_ACTION_HEIGHT = 92;
const height = document.body.clientHeight;
// 👇 打印一下,看看高度是多少
console.log('body: ', document.body);
console.log('clientHeight: ', document.body.clientHeight);
return (
<MyScrollBox maxHeight={height - HEADER_ACTION_HEIGHT}>
{/* 组件内容 */}
</MyScrollBox>
);
}
从上边代码可以看到,我们简历的显示高度是与 document.body.clientHeight
密切相关,想要搞清楚为什么高度错误,就得打印 clientHeight
,看看是否与我们的所期望的一致
clientHeight
居然是 0 !我们再看看,打印的 body 元素的高度是多少
为什么会这样呢?小伙伴们可以思考一下~
那如何解决呢?最简单粗暴的方式,把 height 放在 state 中
ts
function ResumeContent() {
const HEADER_ACTION_HEIGHT = 92;
const [height, setHeight] = useState(0);
useEffect(() => {
if (document.body && document.body.clientHeight > 0)
setHeight(document.body.clientHeight);
}, [document.body]);
return (
<MyScrollBox maxHeight={height - HEADER_ACTION_HEIGHT}>
{/* 组件内容 */}
</MyScrollBox>
);
}
此问题最重要的不在于如何解决,最重要的是知道为什么会出现这种情况。这里留个小提问,小伙伴们动动小奶袋瓜,想一想~然后在评论区留言。
总结
本章主要是对细节方面的做了些小小优化,虽说使用的都是第三方库,但我希望小伙伴们能不给自己设限,不要停留在使用状态,而是能去了解该库背后的实现,不要求精通读完源码,但能对背后的实现原理略知一二,我想,这一定比此章节更重要。如果有疑问,可以在评论区指出。