Appearance
业务篇-首页开发,好的印象能加分
基础建设很重要
首页作为我们整个应用程序的门面担当,它是用户打开应用程序之后,第一眼直观看到的东西,做得复杂多样、花里胡哨的会给用户造成一定的视觉障碍,我们抓住整个应用的核心点:简洁、轻巧,下面是我们的效果图,接下来跟着阿宽一同开发首页效果吧~
本章节将一步步带你开发首页功能,这是一个循序渐进过程,从简单粗暴的功能实现到思考抽离,如果你对本章节内容兴趣不大,可以快速阅读或跳过。
路由组件开发
我们进入到 app/renderer
文件夹下,会发现这里有搭建环境时写的 <Title />
组件,我们将其进行删除(已无用),我们用脚趾头都能知道,之后会存在诸多模块入口,所以我们在 renderer 下,创建一个路由文件 router.tsx
,管理所有的模块入口,先来编写一下 router.tsx
ts
// renderer/router.tsx
import React from 'react';
import { HashRouter, Route, Switch, Redirect } from 'react-router-dom';
import Root from './container/root';
function Router() {
return (
<HashRouter>
<Switch>
{/* 👇 一定要添加 exact */}
<Route path="/" exact>
<Root />
</Route>
</Switch>
{/* 重定向到首页 */}
<Redirect to="/" />
</HashRouter>
);
}
export default Router;
眼精的小伙伴已经发现,上面我们引入了 import Root from './container/root'
,貌似我们现在还没有这个组件,我们来创建一下它。
创建一个文件夹 container
,该文件夹存放着所有模块的代码文件,此时我们添加一个新文件夹,取名为:root
,表明这是首页模块,并创建入口文件 index.tsx 和 index.less
ts
// renderer/container/root/index.tsx
import React from 'react';
import './index.less';
function Root() {
return <div>我是首页</div>;
}
export default Root;
回到根组件 app.tsx,将路由组件 router.tsx 引入
ts
// renderer/app.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import Router from './router';
function App() {
return <Router />;
}
ReactDOM.render(<App />, document.getElementById('root'));
大功告成,运行一下,看看效果如何
bash
npm run start:main
npm run start:render
不出意外,渲染进程窗口很顺利的展示了我们想要的页面效果,此时看看我们的文件结构
首页开发
通过效果图,我们可以将首页拆分成:
- logo 图片
- title 应用名称
- tips 应用简介特性
- entry 模块入口
- copyright 应用版权
接下来将代码实现首页效果,对于样式代码不会贴出,小伙伴们可移步 github 阅读:👉 chapter-06
我们先将logo图引入(图片可自行获取),通过 CSS 实现布局效果,CSS部分代码此处忽略
ts
// 首页模块的入口文件
import React from 'react';
import './index.less';
import Logo from '../../../../assets/logo.png';
function Root() {
return (
<div styleName="root">
<div styleName="container">
<img src={Logo} alt="" />
<div styleName="title">VisResumeMook</div>
<div styleName="tips">一个模板简历制作平台, 让你的简历更加出众 ~</div>
<div styleName="action">
{['介绍', '简历', '源码'].map((text, index) => {
return (
<div key={index} styleName="item">{text}</div>
);
})}
</div>
<div styleName="copyright">
<div styleName="footer">
<p styleName="copyright">
Copyright © 2018-{new Date().getFullYear()} All Rights Reserved. Copyright By pengdaokuan
</p>
</div>
</div>
</div>
</div>
);
}
export default Root;
刷新一下页面,可以发现我们距离成功只剩一步之遥。接下来我们来实现一下基本点击跳转等功能。
模块入口跳转功能
在 React 中我们可以通过 react-router 这个强大路由库进行页面之间的跳转,它可以让你向应用中快速地添加视图和数据流,同时保持页面与 URL 间的同步。
在环境搭建篇中我们已经安装了 react-router,由于我们采用 Hooks 的写法,react-router 提供了一个 API 叫做 useHistory
,接下来我们就通过它,来实现我们的跳转功能吧~
回到我们上面的代码,我们为其添加一个 onClick
事件
ts
// 首页模块的入口文件
import React from 'react';
import './index.less';
import { useHistory } from 'react-router';
import Logo from '../../../../assets/logo.png';
function Root() {
// 👇 通过 history.push 进行跳转
const history = useHistory();
const onRouterToLink = (text: string) => {
if (text === '简历') {
console.log('跳转到简历页面')
history.push('/resume')
} else {
console.log('进入到 github ')
}
}
return (
<div styleName="root">
...
<div styleName="action">
{['介绍', '简历', '源码'].map((text, index) => {
return (
<div key={index} styleName="item" onClick={() => onRouterToLink(text)} >
{text}
</div>
);
)}
</div>
...
</div>
);
}
export default Root;
解读一下上面代码,我们为每个模块 div 都添加 onClick
事件,点击模块后,进行条件判断,从而做对应的操作。 刷新一下页面,点击简历
,发现页面空白,为什么呢?回过头想想,我们上边的 router.tsx
路由组件,不就只写了一个首页模块的路由吗?我们回去添加一个新路由。
在 container
下添加 resume 文件夹,并新增入口 index.tsx,我们简单写一下简历入口代码。
ts
import React from 'react';
import './index.less';
function Resume() {
return <div>我是简历模块</div>;
}
export default Resume;
同时修改 router.tsx 文件,将其引入
ts
import React from 'react';
import { HashRouter, Route, Switch, Redirect } from 'react-router-dom';
import Root from './container/root';
import Resume from './container/resume';
function Router() {
return (
<HashRouter>
<Switch>
<Route path="/" exact>
<Root />
</Route>
{/* 👇 添加简历模块入口路由 */}
<Route path="/resume" exact>
<Resume />
</Route>
</Switch>
<Redirect to="/" />
</HashRouter>
);
}
export default Router;
再点击一下 简历
,此时可成功跳转。页面内的路由切换尚能解决,窗口外的页面跳转无从下手。我们期望点击介绍
、源码
处,能够脱离应用窗口,在我们默认浏览器中打开页面,进入到 github 中。
electron 提供一个 shell 模块,它模块提供与桌面集成相关的功能。并且此模块也能用于渲染进程中,下面我们通过此模块,实现此功能(👇 部分代码省略)
ts
import { shell } from 'electron';
function Root() {
const onRouterToLink = (text: string) => {
if (text !== '简历') {
// 通过 shell 模块,打开默认浏览器,进入 github
shell.openExternal('https://github.com/PDKSophia/visResumeMook');
} else {
history.push('/resume');
}
};
}
到目前为止,我们首页的基本功能已经开发完成。相关代码可访问:chapter-06
🤔 思考代码优化
上面我们是以简单粗暴形式,将页面和逻辑撸了出来,但代码简直“不堪入目”,作为一个有追求、有代码洁癖的工程师,简直无法容忍,接下来我们对它进行美化。
1. webpack alias 别名
我们回过头看,当我们引入图片时,路径要些一连串的 ../../../../
,有没有想骂 x 的冲动,好在 webpack 提供 alias 配置,让我们能够配置别名,接下来我们上手试试。
我们的图片都放在项目根路径下的 assets 中,我们给它加个别名,修改 webpack.base.js
文件
javascript
module.exports = {
resolve: {
// 👇 添加别名
alias: {
'@assets': path.join(__dirname, '../', 'assets/'),
'@src': path.join(__dirname, '../', 'app/renderer'),
},
},
};
添加之后,我们将文件的引入改成下面这种形式
ts
// 未修改前
import Logo from '../../../../assets/logo.png';
// 修改后
import Logo from '@assets/logo.png';
ts
// 未修改前
import Root from './container/root';
import Resume from './container/resume';
// 修改后
import Root from '@src/container/root';
import Resume from '@src/container/resume';
重跑一下项目(运行 npm run start:render)发现没啥问题,完美
如果发现文件报红,这是 ts 报错,你需要前往 tsconfig.json 中同样添加一下 paths 配置
添加之后如果还是有问题,可以重启一下 vscode
json
{
"compilerOptions": {
// 👇 如果ts报错,添加下面这段代码
"baseUrl": "./",
"paths": {
"@assets/*": ["./assets/*"],
"@src/*": ["./app/renderer/*"],
"@common/*": ["./app/renderer/common/*"]
}
}
}
2. 模块入口的常量定义与类型约束
上面我们写了一段“粗暴”代码,我们能否将其进行抽离,思考一下,路由常量数据
是一个只会在首页用到的数据还是其他模块也会用到的数据呢?
其他模块是否也会通过 history.push
方式跳转到其他模块页面,如果是,我们将来在其他模块也要写一段“粗暴”代码?还可能出现的问题是:我们期望数据一致,当往往出于疏忽,两边数据不一致。
那么我们将其抽离成一个路由常量文件,进行统一维护,是不是更好呢?
我们在 app/renderer
文件夹下新增一个文件夹,取名为:common,顾名思义,这里存放的是项目中所有公共通用的代码文件,在里边我们创建一个 constants 文件夹,表示这里维护所有常量数据。
我们在 contants 下维护一份路由专用的文件,取名为 router.ts
,我们来写一下该文件:
ts
// 模块路径
const ROUTER = {
root: '/',
resume: '/resume',
};
export default ROUTER;
export const ROUTER_KEY = {
root: 'root',
resume: 'resume',
};
// 入口模块,TS 定义类型必须为 TSRouter.Item
export const ROUTER_ENTRY: TSRouter.Item[] = [
{
url: 'https://github.com/PDKSophia/visResumeMook',
key: 'intro',
text: '介绍',
},
{
url: ROUTER.resume,
key: ROUTER_KEY.resume,
text: '简历',
},
{
url: 'https://github.com/PDKSophia/visResumeMook',
key: 'code',
text: '源码',
},
];
既然我们使用了 Typescript,那么我们先小试牛刀一下,上面定义的 ROUTER_ENTRY
我们将它的类型约束为 TSRouter.Item
,我们在 common 文件夹下新增一个名为 types 文件夹,表示此文件存放着应用中用到的类型定义。我们来新增一个用于路由的 router.d.ts 文件
ts
// router.d.ts
// 路由类型约束
declare namespace TSRouter {
export interface Item {
/**
* @description 路由跳转链接
*/
url: string;
/**
* @description 关键词
*/
key: string;
/**
* @description 文本
*/
text: string;
}
}
紧接着我们在 webpack 中配置一下此文件夹的别名
javascript
alias: {
// ...
'@common': path.join(__dirname, '../', 'app/renderer/common'),
}
我们进行改造,首先先来修改一下路由组件 router.tsx
ts
// router.tsx 路由组件
// 👇 引入路由常量
import ROUTER from '@common/constants/router';
function Router() {
return (
<HashRouter>
<Switch>
<Route path={ROUTER.root} exact>
<Root />
</Route>
<Route path={ROUTER.resume} exact>
<Resume />
</Route>
</Switch>
<Redirect to={ROUTER.root} />
</HashRouter>
);
}
export default Router;
我们在首页入口 index.tsx 文件进行改造
ts
// 首页入口 index.tsx
import { ROUTER_ENTRY, ROUTER_KEY } from '@common/constants/router';
// 在方法调用上
const onRouterToLink = (router: TSRouter.Item) => {
if (router.text !== '简历') {
shell.openExternal(router.url);
} else {
history.push(router.url)
}
};
// 在遍历上
<div styleName="action">
{ROUTER_ENTRY.map((router: TSRouter.Item) => {
return (
<div key={router.key} styleName="item" onClick={() => onRouterToLink(router)} >
{router.text}
</div>
);
})}
</div>
3. utils 方法抽离
虽然我们代码优化了一部分,但还是存在一些小问题的,比如 router.text !== '简历'
这个条件判断就有些突兀了,我们回到问题本质,这里进行判断原因是:如果这个 url 是外部可访问的链接,则通过 shell 模块打开浏览器,如果是页面之间跳转,则跳转到对应的路由页面。
所以问题聚焦在,如何判断 url 是不是可访问的外部链接?这很简单,我们写一个方法,判断 url 是不是 http 或 https 开头,该方法返回 boolean 值,下面我们来实现此方法。
首先在 common 下新增一个 utils 文件夹,并新增 router.ts,表示这是路由相关的工具处理函数,在里面实现我们的函数方法:
ts
// renderer/common/utils/router.ts
/**
* @desc 判断是否属于外部连接
* @param {string} url - 链接
*/
export function isHttpOrHttpsUrl(url: string): boolean {
let regRule = /(http|https):\/\/([\w.]+\/?)\S*/;
return regRule.test(url.toLowerCase());
}
接下来我们进行修改的条件判断
ts
import { isHttpOrHttpsUrl } from '@common/utils/router';
// 在方法调用上
const onRouterToLink = (router: TSRouter.Item) => {
if (isHttpOrHttpsUrl(router.url) {
shell.openExternal(router.url);
} else {
history.push(router.url);
}
};
4. 页面存在空白间隙
最懒惰的解决方式是,在 index.html 中,修改一下样式
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>VisResumeMook</title>
<style>
* {
margin: 0;
}
</style>
</head>
<body>
<div id="root"></div>
</body>
</html>
至此,我们的首页终于开发完毕,并且经过思考,不断优化,将项目的整个文件结构进行丰富。一张图回顾一下我们现在的文件结构
总结
本章节更像是手把手教程,从最简单的粗暴实现,到最后的思考以及抽离,中间穿插着 webpack、ts 等一些配置,希望这章节能让小伙伴们上手项目,这并不难,接下来的实战章节,阿宽也会通过这种方式,循序渐进,一步步引导大家去思考去优化。
- 简单粗暴型代码:chapter-06
- 思考优化型代码:chapter-06-op
如果您在边阅读边实践时,发现代码报错或者 TS 报错,那么小伙伴们可以根据报错信息,去线上看看相应的代码。
如果对本章节存在疑问,欢迎在评论区留言。