手把手教你做一个自己的导航首页

拥有无限潜能的前端导航项目


前端三件套,首先当然是——

搞什么开发,当然是先需求分析

需求分析

前端导航一般都有两个主要功能

  1. 搜索
  2. 快捷标签页

通过墨刀作图,可以得到如图原型图

产品原型图

页面结构

得到产品原型图以后,分析一下页面的结构

首先能够确定的是两边留白,中间三行

大致就是 3x3 表格模样,特别适合 grid 布局

什么?兼容?想那么多干啥,先自己写出来,跑起来再说,2333

在 grid 布局中,用到中间一列,上部放置搜索框,中部放置标签页,下部放置授权标志

那大致可以得到如下 HTML

1
2
3
4
5
6
7
<body>
<div class="app">
<header></header>
<main></main>
<footer></footer>
</div>
</body>

header 中包含两个组件,所以 header 应该写成如下模样

1
2
3
4
5
6
<header>
<div class="searchBox">
<input type="text" id="keyword">
<button onclick="search()" id="submit">搜索</button>
</div>
</header>

main 中包含多个标签页,数量不确定,行数也不确定,所以标签页决定通过 js 动态插入

然后新增功能需要一个新增窗口,先画原型图

弹出页原型图

可见弹出窗口也需要一些组件,且需要用户通过”添加”按钮提供控制

所以可得如下 main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<main>
<div id="tab">
<div class="delete" onclick="cancel()">×</div>
<div>
<span>链接:</span>
<input type="text" id="url" placeholder="请输入链接">
</div>
<div>
<span>描述:</span>
<input type="text" id="desp" placeholder="请输入描述">
</div>
<button onclick="insertCard()">确定</button>
</div>
<div class="container"></div>
</main>

而 footer 的内容不是动态的,所以可以直接写死

1
2
3
<footer>
<div>© 2017 — 2020 ringoer.com 版权所有</div>
</footer>

最后再引入对全文操作的 js 文件,可得全页面的 HTML 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<!DOCTYPE html>
<html lang="zh-CN">

<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>导航</title>
<link rel="stylesheet" href="style.css">
</head>

<body>
<div class="app">
<header>
<div class="searchBox">
<input type="text" id="keyword">
<button onclick="search()" id="submit">搜索</button>
</div>
</header>
<main>
<div id="tab">
<div class="delete" onclick="cancel()">×</div>
<div>
<span>链接:</span>
<input type="text" id="url" placeholder="请输入链接">
</div>
<div>
<span>描述:</span>
<input type="text" id="desp" placeholder="请输入描述">
</div>
<button onclick="insertCard()">确定</button>
</div>
<div class="container"></div>
</main>
<footer>
<div>© 2017 — 2020 ringoer.com 版权所有</div>
</footer>
</div>
<script src="main.js"></script>
</body>

</html>

接下来开始写CSS

CSS

全局初始化

先 fix 一下默认的样式

1
2
3
4
5
*{
margin: 0;
padding: 0;
box-sizing: border-box;
}

然后为 body 选择一个合适的灰色

1
2
3
body{
background-color: #f0f0f0
}

定位

然后就是定位了

  1. 大小,显然要占满全屏,100vh 和 100vw 安排上
  2. 滚动条肯定不给左右给上下,overflow 安排
  3. 说好的 3x3 布局,grid 安排

可以得到如下 CSS

1
2
3
4
5
6
7
8
9
10
11
12
.app{
height: 100vh;
width: 100vw;
overflow: hidden auto;
display: grid;
grid-template-columns: 1fr 8fr 1fr;
grid-template-rows: 100px 1fr 100px;
grid-template-areas:
". header ."
". main ."
". footer .";
}

然后 app 里的 header main footer 都是要做居中的,3 个flex 安排上

1
2
3
4
5
.app>*{
display: flex;
justify-content: center;
align-items: center;
}

接下来给三个块分配 grid 空间

1
2
3
4
5
6
7
8
9
header{
grid-area: header;
}
main{
grid-area: main;
}
footer{
grid-area: footer;
}

然后先来做 header 吧

搜索

header 包含两个框,输入和按钮,并且还要居中

所以决定用一个 div 包住这两个,然后把 div 居中,里面一左一右摆放

先做个定宽居中 div

1
2
3
4
.searchBox{
width: 80%;
max-width: 800px;
}

之所以这里不用写居中,是因为容器 header 已经定义了 flex 居中了

然后写搜索框宽度,显然这个是自适应的

那么反过来,先决定 button 的样式

首先让 button 宽 80px 吧

然后为了 button 好看,一般会给 padding

接下来背景色给个浅蓝色吧,同时字给白色(为了看得见)

这边框好丑,我要给白边加圆角

现在就还挺好看的

然后给一下上下 margin,以及为了同行显示的 inline-block,可以得到如下 CSS

1
2
3
4
5
6
7
8
9
10
#submit{
width: 80px;
padding: 8px 12px;
margin: 8px 0;
display: inline-block;
border: 1px solid #ccc;
border-radius: 4px;
color: white;
background-color: cornflowerblue;
}

再来做一个搜索框

首先给个 padding 让字显示位置不要那么贴边

然后再和 button 一样弄好看点

最后再弄个计算宽度来自适应

可以得到如下 CSS

1
2
3
4
5
6
7
8
#keyword{
width: calc(100% - 100px);
padding: 8px 12px;
margin: 8px 0;
display: inline-block;
border: 1px solid #ccc;
border-radius: 4px;
}

搜索就做好了!

主容器

首先要让弹窗不能影响我们的布局,于是把弹窗显示去掉

display: none; 即可去除

之所以选用 display none,是因为它不会渲染到页面上

而 visibility hidden 虽然不保留事件,但还是会渲染到页面上占据空间

opacity 更离谱,事件都还留着

然后开始做主容器内容

显然主容器的每列是平均布局,有多列

多列适合 flex column,但是平均布局不能用 space-between,否则空间分配会失常

所以先得到容器样式

1
2
3
4
5
6
7
8
9
.container{
width: 100%;
min-width: 300px;
max-width: 1200px;
height: 100%;
display: flex;
justify-content: center;
flex-wrap: wrap;
}

接下来每一个标签页,都有自己的一张卡片一样的样式

在卡片中,也有 logo、description、叉叉等三部分内容

其中叉叉是绝对定位在右上角的,前两个则是纵向居中排列在卡片中

可以得到卡片样式

1
2
3
4
5
6
7
8
9
10
11
12
13
.card{
width: 200px;
height: 160px;
margin: 20px;
box-shadow: 0 0 1px black;
background-color: white;
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
cursor: pointer;
}

这里给 margin 预留了足够位置,所以不需要使用 负 margin 法 来平衡

然后给叉叉定位

1
2
3
4
5
6
7
.delete{
position: absolute;
font-size: 30px;
top: 2px;
right: 10px;
visibility: hidden;
}

因为平时没选中的时候是看不见叉叉的,所以 visibility: hidden;

然后定义 logo 样式,此处我使用文本而不是矢量图

1
2
3
.logo{
font-size: 80px;
}

慢着,描述变长的时候,描述怎么换行了?

给描述上一个不许换行的样式

1
2
3
.desp{
white-space: nowrap;
}

接下来定义选中卡片的时候的行为

显然选中的行为一般都是变大或者上浮,此处选择上浮

1
2
3
.card:hover{
transform: translateY(-10px);
}

上浮不能太突兀,给卡片加个过渡动画吧

1
2
3
.card{
transition: all 0.3s;
}

当选中的时候,叉叉也要能看到才对

1
2
3
.card:hover .delete{
visibility: visible;
}

但是这时候,点叉叉还是点不到,怎么回事呢?因为叉叉浮起不够高,点到别的了!

让叉叉浮起来

1
2
3
.delete{
z-index: 1;
}

OK,主容器部分做好了

弹窗

弹窗有几个要点

  1. 绝对定位在视口中央
  2. 三列内容,纵向居中排列,加一个叉
  3. 当它显示时,按键等级(层叠上下文)要比主页高

于是可以得到如下 CSS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#tab{
width: 400px;
height: 300px;
background-color: white;
/* display: flex; */
display: none;
justify-content: center;
align-items: center;
flex-direction: column;
position: fixed;
box-shadow: 0 0 1px black;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
z-index: 1;
}

至于 z-index 为什么是 1 呢?

因为我们刚才给 container 里的 card 里的 delete 赋值了 z-index=1,所以导致 container 的层级也升高了,所以这时候 tab 的 z-index 要不低于 container,才能浮在 container 上方

接下来为了方便布局,让所有内部子元素都以块级元素形式显示,并设置上下边距

1
2
3
4
#tab>*{
display: block;
margin: 20px 0;
}

tab 里的 delete 和 card 里的不一样,默认是显示的

1
2
3
4
#tab>.delete{
visibility: visible;
margin: 0;
}

再修整一下 tab 里元素的样式,弄好看点

1
2
3
4
5
6
7
8
9
10
11
#url,#desp{
padding: 10px;
}

#tab>button{
border: 1px solid #0381aa;
color: #fff;
background-color: #00a7de;
padding: 10px;
width: 100px;
}

响应式

我们刚才都是以 PC 端为基准开发的,现在要支持移动端,怎么办呢?

答案就是一个媒体查询就完事了

分析可得在移动端,容器的 3x3 布局变为 1x3 布局,card 也最多只能排列下两个

所以可得 CSS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@media (max-width: 600px){
.app{
grid-template-areas:
"header header header"
"main main main"
"footer footer footer";
}
.card{
width: 35vw;
height: 28vw;
margin: 3vw;
}
.logo{
font-size: 60px;
}
}

响应式这样就完成了,不错吧?

JS

基本的骨架弄好了,现在要赋予这个项目血肉

搜索

从上到下,第一个就是搜索

很简单,只要研究一下百度的搜索格式就可以了

1
2
3
4
5
6
function search() {
let baseUrl = 'https://www.baidu.com/s?ie=UTF-8&wd='
let keyword = document.querySelector('#keyword').value
keyword = keyword.replaceAll(' ', '%20')
window.open(baseUrl + keyword)
}

注意搜索中要改一下默认的 http 字符转义问题,一般是空格转换成 %20 就行了

标签页渲染

然后是最重要的标签页渲染的功能

为了体现效果,此处先多搞几个重复的标签页

1
2
3
4
5
6
7
8
9
10
11
12
13
let base_navs = [{ 'href': 'https://ringoer.com', 'desp': 'Ringoer\'s Site', 'logo': 'R' },
{ 'href': 'https://baidu.com', 'desp': '百度', 'logo': 'B' },
{ 'href': 'https://baidu.com', 'desp': '百度', 'logo': 'B' },
{ 'href': 'https://baidu.com', 'desp': '百度', 'logo': 'B' },
{ 'href': 'https://baidu.com', 'desp': '百度', 'logo': 'B' },
{ 'href': 'https://baidu.com', 'desp': '百度', 'logo': 'B' },
{ 'href': 'https://baidu.com', 'desp': '百度', 'logo': 'B' },
{ 'href': 'https://baidu.com', 'desp': '百度', 'logo': 'B' },
{ 'href': 'https://baidu.com', 'desp': '百度', 'logo': 'B' },
{ 'href': 'https://baidu.com', 'desp': '百度', 'logo': 'B' },
{ 'href': 'https://baidu.com', 'desp': '百度', 'logo': 'B' },
{ 'href': 'https://baidu.com', 'desp': '百度', 'logo': 'B' },
{ 'href': 'https://baidu.com', 'desp': '百度', 'logo': 'B' },]

接下来,渲染到页面上

首先读取列表里的每个项目

1
2
3
4
5
6
let container = document.querySelector('.container')
container.innerHTML = ''
for (let i = 0; i < navList.length; i++) {
let nav = navList[i]
container.appendChild(createCard(i, nav.href, nav.desp, nav.logo))
}

然后在最后加上”添加”标签页

1
container.appendChild(createCard(navList.length, '/', '添加', '+'))

逻辑就完成了

那么来看看 createCard 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function createCard(i, href, desp, logo) {
let card = document.createElement('div')
card.className = 'card'
card.addEventListener('click', () => {
if (logo === '+') {
let tab = document.querySelector('#tab')
tab.style.display = 'flex';
return
}
window.open(href)
}, false)
if (logo === '+') {
card.innerHTML = '<div class="logo">' + logo + '</div>' +
'<div class="desp">' + desp + '</div>'
}
else {
card.innerHTML = '<div class="delete" onclick="deleteCard(' + i.toString() + ',event)">×</div>' +
'<div class="logo">' + logo + '</div>' +
'<div class="desp">' + desp + '</div>'
}
return card
}

先动态创建 card,然后根据是不是”添加”标签页,来动态绑定事件并插入子元素,最后返回 card

为了可以正常打开 tab 页,”添加”标签页的 click 事件让 tab 页的 display 从 none 变成了 flex,就可以显示了

可以看到里面提及了 delete 按钮拥有的 delete 事件

1
2
3
4
5
6
7
8
9
function deleteCard(i, event) {
let ans = confirm('确定要删除这个标签吗?')
if (ans) {
navList.splice(i, 1)
draw()
}

event.stopPropagation()
}

这里一定要阻止冒泡,否则事件一旦冒泡,就会触发 card 的点击事件,导致虽然删除了 card,但是发生了跳转

这时候发现,我刷新一下怎么就重置了??

所以要保存用户使用数据

为了方便起见,这里保存到 localStorage

在初始化的时候先读取内容

1
2
3
4
5
let navList = JSON.parse(localStorage.getItem('navList'))
if (!navList) {
navList = base_navs
}
draw()

然后在每次渲染之后都存数据

1
localStorage.setItem('navList', JSON.stringify(navList))

这样就可以得到完整的 draw 方法

1
2
3
4
5
6
7
8
9
10
function draw() {
let container = document.querySelector('.container')
container.innerHTML = ''
for (let i = 0; i < navList.length; i++) {
let nav = navList[i]
container.appendChild(createCard(i, nav.href, nav.desp, nav.logo))
}
container.appendChild(createCard(navList.length, '/', '添加', '+'))
localStorage.setItem('navList', JSON.stringify(navList))
}

这样渲染功能也大功告成了!

新增

最后是 tab 页提供的新增功能

分析需求可得

  1. 只允许 http 或 https 开头的 url
  2. 取 url 去掉协议后的第一个字符作为 logo
  3. 渲染到最后一个,但还在”添加”标签页之前

于是可得如下 js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function insertCard() {
let url = document.querySelector('#url').value
let desp = document.querySelector('#desp').value
if (!url.match('[http|https]://*')) {
alert('请输入包含http或https的链接格式!')
return
} else {
let tab = document.querySelector('#tab')
tab.style.display = 'none';
}
let logo = (url.split('//')[1])[0].toUpperCase()

navList.push(JSON.parse('{ "href": "' + url + '", "desp": "' + desp + '", "logo": "' + logo + '" }'))

draw()
}

这是主要功能,但是这个 tab 页还得支持关闭呀

1
2
3
4
function cancel() {
let tab = document.querySelector('#tab')
tab.style.display = 'none';
}

重新 none 就好了

ok,运行试试吧,这就是你的前端导航项目

改进

其实简单的项目还有很多值得改进的地方

比如我这个写法,并没有完美复刻设计图

大致有以下改进点

  1. 完美复刻设计图
  2. 改用 vue 或 react 做这个项目
  3. 支持用户在线保存设置
  4. 改善 logo,支持图片

以上点就够喝一壶了2333

那就到这里了


感谢阅读

--It's the end.Thanks for your read.--