# 路由组件开发
我们进入到 app/renderer
文件夹下,会发现这里有搭建环境时写的 <Title />
组件,我们将其进行删除(已无用),我们用脚趾头都能知道,之后会存在诸多模块入口,所以我们在 renderer 下,创建一个路由文件 router.tsx
,管理所有的模块入口,先来编写一下 router.tsx
// 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> | |
{/* 精确路由匹配 */} | |
<Route path="/" exact> | |
<Root /> | |
</Route> | |
</Switch> | |
{/* 重定向到首页 */} | |
<Redirect to="/" /> | |
</HashRouter> | |
); | |
} | |
export default Router; |
创建一个文件夹 container
,该文件夹存放着所有模块的代码文件,此时我们添加一个新文件夹,取名为: root
,表明这是首页模块,并创建入口文件 index.tsx 和 index.less
// renderer/container/root/index.tsx | |
import React from 'react'; | |
import './index.less'; | |
function Root() { | |
return <div>我是首页</div>; | |
} | |
export default Root; |
回到根组件 app.tsx
,将路由组件 router.tsx
引入
// 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')); |
大功告成,运行一下,看看效果如何
npm run start:main | |
npm run start:render |
不出意外,渲染进程窗口很顺利的展示了我们想要的页面效果,此时看看我们的文件结构
# 首页开发
通过效果图,我们可以将首页拆分成:
- logo 图片
- title 应用名称
- tips 应用简介特性
- entry 模块入口
- copyright 应用版权
我们先将 logo 图引入,通过 CSS 实现布局效果.
// 首页模块的入口文件 | |
import React from 'react'; | |
import './index.less'; | |
import Logo from '../../../../assets/logo.jpg'; | |
function Root() { | |
return ( | |
<div styleName="root"> | |
<div styleName="container"> | |
<img src={Logo} alt="" /> | |
<div styleName="title">onlineResume</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; |
CSS 如下
.root { | |
height: 100vh; | |
width: 100vw; | |
text-align: center; | |
background-color: #27292c; | |
.container { | |
width: 100%; | |
color: #ffffff; | |
padding-top: calc(8vh + 60px); | |
img { | |
width: 112px; | |
height: 112px; | |
} | |
.title { | |
font-size: 24px; | |
line-height: 36px; | |
} | |
.tips { | |
font-size: 16px; | |
line-height: 24px; | |
margin-top: 24px; | |
} | |
.theme { | |
margin: 24px 0; | |
height: 30px; | |
} | |
.action { | |
width: 300px; | |
margin: 0 auto; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
margin-top: 24px; | |
.item { | |
width: 25%; | |
cursor: pointer; | |
} | |
} | |
} | |
} |
刷新一下页面,可以发现我们距离成功只剩一步之遥。接下来我们来实现一下基本点击跳转等功能。
# 模块入口跳转功能
在 React 中我们可以通过 react-router 这个强大路由库进行页面之间的跳转,它可以让你向应用中快速地添加视图和数据流,同时保持页面与 URL 间的同步。
在环境搭建篇中我们已经安装了 react-router,由于我们采用 Hooks 的写法,react-router 提供了一个 API 叫做 useHistory
,接下来我们就通过它,来实现我们的跳转功能吧~
回到我们上面的代码,我们为其添加一个 onClick
事件
// 首页模块的入口文件 | |
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,我们简单写一下简历入口代码。
import React, { Component } from 'react' | |
export default class index extends Component { | |
render() { | |
return ( | |
<div> | |
我是简历页面 | |
</div> | |
) | |
} | |
} |
同时修改 router.tsx 文件,将其引入
// renderer/router.tsx | |
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 模块,它模块提供与桌面集成相关的功能。并且此模块也能用于渲染进程中,下面我们通过此模块,实现此功能(👇 部分代码省略)
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'); | |
} | |
}; | |
} |
到目前为止,我们首页的基本功能已经开发完成。
# 🤔 思考代码优化
上面我们是以简单粗暴形式,将页面和逻辑撸了出来,但代码简直 “不堪入目”,作为一个有追求、有代码洁癖的工程师,简直无法容忍,接下来我们对它进行美化。
# 1. webpack alias 别名
我们回过头看,当我们引入图片时,路径要些一连串的 ../../../../
,有没有想骂 x 的冲动,好在 webpack 提供 alias 配置,让我们能够配置别名,接下来我们上手试试。
我们的图片都放在项目根路径下的 assets 中,我们给它加个别名,修改 webpack.base.js
文件
module.exports = { | |
resolve: { | |
// 👇 添加别名 | |
alias: { | |
'@assets': path.join(__dirname, '../', 'assets/'), | |
'@src': path.join(__dirname, '../', 'app/renderer'), | |
}, | |
}, | |
}; |
添加之后,我们将文件的引入改成下面这种形式
// 未修改前 | |
import Logo from '../../../../assets/logo.png'; | |
// 修改后 | |
import Logo from '@assets/logo.png'; | |
// 未修改前 | |
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)发现没啥问题,完美
# 2. 模块入口的常量定义与类型约束
上面我们写了一段 “粗暴” 代码,我们能否将其进行抽离,思考一下, 路由常量数据
是一个只会在首页用到的数据还是其他模块也会用到的数据呢?
其他模块是否也会通过 history.push
方式跳转到其他模块页面,如果是,我们将来在其他模块也要写一段 “粗暴” 代码?还可能出现的问题是:我们期望数据一致,当往往出于疏忽,两边数据不一致。
那么我们将其抽离成一个路由常量文件,进行统一维护,是不是更好呢?
我们在 app/renderer
文件夹下新增一个文件夹,取名为:common,顾名思义,这里存放的是项目中所有公共通用的代码文件,在里边我们创建一个 constants 文件夹,表示这里维护所有常量数据。
我们在 contants 下维护一份路由专用的文件,取名为 router.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 文件
// router.d.ts | |
// 路由类型约束 | |
declare namespace TSRouter { | |
export interface Item { | |
/** | |
* @description 路由跳转链接 | |
*/ | |
url: string; | |
/** | |
* @description 关键词 | |
*/ | |
key: string; | |
/** | |
* @description 文本 | |
*/ | |
text: string; | |
} | |
} |
紧接着我们在 webpack 中配置一下此文件夹的别名
alias: { | |
// ... | |
'@common': path.join(__dirname, '../', 'app/renderer/common'), | |
} |
我们进行改造,首先先来修改一下路由组件 router.tsx
//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 文件进行改造
// 首页入口 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,表示这是路由相关的工具处理函数,在里面实现我们的函数方法:
// 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()); | |
} |
接下来我们进行修改的条件判断
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 中,修改一下样式
<!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> |
至此,我们的首页终于开发完毕,并且经过思考,不断优化,将项目的整个文件结构进行丰富。一张图回顾一下我们现在的文件结构