前言
之前习惯使用的是百度的作为浏览器的首页,百度登录账号后可以在首页上收藏很多书签,但是近期因为使用不同电脑协同办公,导致一台电脑登录百度账号后,其它的电脑的百度会被提下线,这就很烦,还不如自己做个起始页;
效果图
项目下载
项目代码已上传,可以前往下载相关代,下载地址如下:下载地址,如果不能下载且有需要的小伙伴及时联系博主或评论留下邮箱,我看到后会第一时间发送代码;
技术栈
本项目前台的主要技术栈使用的是:Vue3+VueX+VueRouter+Axios+TypeScript,后台主要是用的Node+TypeScript,当然本文主要讲述的是前台相关,后台部分代码有兴趣的可以留言;
分析
在开始码代码前简单的分析一下,理一下逻辑,同时看看有可能会涉及到哪些知识点,这样在准备阶段大致心里就有了一个底,对于重复度高的部分可以提前预计到并封装成组件,公共样式也尽量提前抽离;
思维导图
功能点概述
首页
主要用途是搜索,有搜索框组件和时钟组件组成,并且搜索框组件聚焦时可以看到历史记录以及热门搜索,同时聚焦时背景图上有一个放大以及毛玻璃的效果;
快捷页
快捷入口的集合,打开时背景图同样有一个放大以及毛玻璃的效果;
另外,在整个项目里,使用到了lowdb来对数据进行缓存和读取(其实就是localstage),使用到了loadsh作为函数库
初始化项目
既然是基于Vue,那么开始必须是基于Vue3的脚手架搭建项目,具体命令如下:
vue create start
注意的是,在配置项目的时候需要选择安装TypeScript,以及使用到了CSS的预处理器:less,实际上我觉得Sass更符合我个人的使用习惯,但是公司的项目都是用的less,那么这里也就继续使用less作为预处理器,两者其实差不多,没什么根本性的不同;
功能点
感觉没有必要将每一行代码都放在博客里,那样的话感觉整篇博客都是长篇代码而且效果也不好,博客里又不能调试,要看整体代码直接下载项目就是了,还能一边看一边调试,本文主要记录一下涉及到的功能点的代码,方便理解Vue3+Typescript(由于代码写的比较早,现在回过头来看还是又一些可以优化的地方的,写的不好的地方多多见谅);
布局组件
这个组件的主要目的是用来做布局,因为新的界面交付到我们开发手里时,不能直接开始写代码,肯定要写一些布局组件,用来对页面进行合理的布局,布局组件写完后,只要在对应的位置填入对应的组件就行了,这样就不用在每一个页面都写一遍布局,这样不科学也太累,一旦需求发生变化,如果每个页面都是进行布局那工作了就成倍的增加了;
布局组件部分代码如下,在本项目中这个布局组件除了用来布局,还用来做各个子组件的数据的一个交互的链接点,因为不管是快捷入口还是搜索栏组件都会对背景组件的状态产生修改,一旦状态发生变化,那么这个组件就可以用来临时做数据的中转:
<template>
<base-layout :shortcuts="shortcuts">
<template #backgourndImage>
<base-background
:glass="glass"
@changeGlass="changeGlass"
:shortcuts="shortcuts"
@changeShortcuts="changeShortcuts"
>
<template #clock>
<clock-component
:glass="glass"
:search="searchState"
v-show="!shortcuts"
/>
</template>
</base-background>
</template>
<template #search>
<search-component
@changeFocus="changeFocus"
:glass="glass"
v-show="!shortcuts"
/>
</template>
<template #copyright>
<copyright-component />
</template>
<template #shortcuts>
<shortcutsComponent v-show="shortcuts" />
</template>
</base-layout>
</template>
// 组件、库、接口等引入
<script lang="ts">
// ...组件注册
setup() {
const store: Store<any> = useStore();
const glass: Ref<boolean> = ref(false);
const searchState: Ref<boolean> = ref(false);
const shortcuts: Ref<boolean> = ref(false);
function changeGlass(value: boolean) {
changeComponentState(value);
}
function changeFocus(value: boolean) {
glass.value = value;
searchState.value = value;
}
function changeShortcuts(value: boolean) {
changeComponentState(value);
}
function changeComponentState(value: boolean) {
glass.value = value;
shortcuts.value = value;
}
/**
* 基本信息
* @returns {Object} 基本信息
*/
async function getBasicInfo(): Promise<any> {
const basicInfo: any = await GetBasicInfo();
return new Promise((resolve) => {
resolve(basicInfo);
});
}
/**
* 初始化基本信息
*/
async function initBasic() {
// 初始化本地信息
const info = await store.dispatch("basic/basic/load");
// 本地无数据,启用默认参数
if (loadsh.isEmpty(info)) {
const data: dataApiInterface = await getBasicInfo();
const saveInfo = await store.dispatch(
"basic/basic/saveBasicInfo",
data
);
if (loadsh.isEmpty(saveInfo))
console.error("初始化异常,请联系管理员");
}
}
/**
* 初始化
*/
async function init(): Promise<void> {
// 初始化基本信息
await initBasic();
}
init();
return {
glass,
searchState,
shortcuts,
changeGlass,
changeFocus,
changeShortcuts,
};
},
</script>
我们写了一个base-layout的组件用来做布局,并且在组件中预留了4个插槽,分别用来放置背景组件,主界面,快捷入口,版权信息,这样整体的结构不会凌乱,而base-layout部分代码如下:
<template>
<div class="threeS-home-container">
<div class="threeS-home-bg">
<slot name="backgourndImage"></slot>
</div>
<div class="threeS-home-search">
<slot name="search"></slot>
</div>
<div class="threeS-home-copyright">
<slot name="copyright"></slot>
</div>
<div class="threeS-home-shortcuts" v-if="shortcuts">
<slot name="shortcuts"></slot>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
props: {
shortcuts: {
type: Boolean,
default: false,
},
},
setup() {},
});
</script>
<style scoped lang="less">
// ... 具体的样式
</style>
就是做了一个布局,对每一块内容都进行圈定范围,并且预留了插槽用来内容填充;
背景组件
这个组件主要的功能就是背景图的放大以及毛玻璃,这两块效果其实都是CSS实现的,JS实现的主要是在什么时机触发这个效果,在本项目中,触发的时机一共有两个:
- 处于首页的时候,输入框聚焦,此时的背景组件会触发放大和毛玻璃效果;
- 右击首页,打开快捷入口的时候,也会触发放大和毛玻璃效果;
部分代码如下:
<script lang="ts">
import { defineComponent, computed, ref, nextTick, watch } from "vue";
import { useStore, Store } from "vuex";
export default defineComponent({
props: {
glass: {
type: Boolean,
default: false,
},
shortcuts: {
type: Boolean,
default: false,
},
},
setup(props, context) {
// 表单DOM
const threeSBgRef: any = ref(null);
// 获取vuex信息
const store: Store<any> = useStore();
// 背景图路径
const imgSrcPath = ref("");
// 背景图状态,false-隐藏,true-显示
const showImg = ref(false);
// 背景图信息
const bagImg = computed(() => store.getters["basic/basic/getBg"]);
/**
* 背景图DOM
* @returns {HTMLElement} 背景图DOM
*/
async function getImgDom(): Promise<any> {
return new Promise((resolve) => {
nextTick(() => {
resolve(threeSBgRef.value);
});
});
}
watch(
() => bagImg.value,
async (value) => {
if (Array.isArray(value)) {
return false;
}
const img: HTMLElement = await getImgDom();
img.onload = () => {
showImg.value = true;
};
imgSrcPath.value = value.path;
}
);
/**
* 右击
*/
function handleRightClick(): void | boolean {
if (imgSrcPath.value === "") return false;
context.emit("changeShortcuts", !props.shortcuts);
}
/**
* 左击
*/
function handleLeftClick(): void | boolean {
if (!props.glass) return false;
context.emit("changeGlass", false);
}
return {
imgSrc: computed(() =>
imgSrcPath.value === ""
? null
: require(`../../../../../src/assets/bg/${imgSrcPath.value}`)
),
showImg,
threeSBgRef,
handleRightClick,
handleLeftClick,
};
},
});
</script>
另外,这里还有一个骨架屏的效果,就是当背景图还没有被加载的时候,整个背景图处于灰色,并且有一个从左往右的动效,代表正在加载中,骨架屏效果代码如下:
.threeS-loading {
background-color: #f2f2f2;
background: linear-gradient(
100deg,
rgba(255, 255, 255, 0) 40%,
rgba(255, 255, 255, 0.5) 50%,
rgba(255, 255, 255, 0) 60%
)
#f6f6f6;
background-size: 200% 100%;
background-position-x: 120%;
animation: 1s loading ease-in-out infinite;
}
@keyframes loading {
to {
background-position-x: -20%;
}
}
时钟组件
这个组件的难点在于实现电子数字的格式,这个是最复杂的,而时间的获取就是基于浏览器Date对象,这个很简单,获取时间的方法部分如下:
/**
* 获得时间
* @returns {String} 当前时间
*/
function getTime(): string {
const date = new Date();
const hour = date.getHours();
const minute = date.getMinutes();
return (
(hour >= 10 ? hour : "0" + hour) +
":" +
(minute >= 10 ? minute : "0" + minute)
);
}
当获取到时间以后,需要将转成电子格式的,这部分的原理就是先拼出一个电子格式的8,然后,不同的数字只是去掉8中的某一个笔划,这样就达到了0-9的数字,这里我挑一部分CSS代码
.digits div span {
opacity: 0;
position: absolute;
-webkit-transition: 0.25s;
-moz-transition: 0.25s;
transition: 0.25s;
}
.digits div span:before,
.digits div span:after {
content: "";
position: absolute;
width: 0;
height: 0;
border: @fontSize solid transparent;
}
.digits .d1 {
height: @fontSize;
width: 16px;
top: 0;
left: 6px;
}
.digits .d1:before {
border-width: 0 @fontSize @fontSize 0;
border-right-color: inherit;
left: -@fontSize;
}
.digits .d1:after {
border-width: 0 0 @fontSize @fontSize;
border-left-color: inherit;
right: -@fontSize;
}
.digits .d2 {
height: @fontSize;
width: 16px;
top: 24px;
left: 6px;
}
.digits .d2:before {
border-width: 3px 4px 2px;
border-right-color: inherit;
left: -8px;
}
.digits .d2:after {
border-width: 3px 4px 2px;
border-left-color: inherit;
right: -8px;
}
这里是1和2的组成,通过类似的方法将0-9的CSS都写完,之后获取到时间,在填入的时候判断需要填入的是哪个数字就行:
/**
* 填入时间
* @param {String} time - 时间
* @param {HTMLElement} timerClock - 填入时间的DOM
* @returns 无
*/
function clocknum(time: string, timerClock: HTMLElement): void {
timerClock.innerHTML = "";
var html = "";
var strarr: any[] = time.toString().split("");
var digit_to_name = "zero one two three four five six seven eight nine".split(
" "
);
for (var i = 0; i < strarr.length; i++) {
if (strarr[i] == ":") {
html += '<div class="dot"></div>';
} else {
var clasname = digit_to_name[strarr[i]];
html +=
'<div class="' +
clasname +
'">' +
'<span class="d1"></span>' +
'<span class="d2"></span>' +
'<span class="d3"></span>' +
'<span class="d4"></span>' +
'<span class="d5"></span>' +
'<span class="d6"></span>' +
'<span class="d7"></span>' +
"</div>";
}
}
timerClock.innerHTML = html;
}
这样时间组件就算基本完成了,使用的时候只需要引入主页,放在对应的位置就可以了;
搜索组件
搜索组件一共分为两部分,需要单独实现,分别为:搜索框,搜索历史(热门搜索);
搜索框
这个其实简单,其实就是一个输入框,并且在其上附加了一个聚焦事件,用于和背景组件的联动,确认搜索的时候根据搜索类型打开对应的页签就可以了,并且确认搜索的时候,将该条搜索条件作为记录存储到本地缓存中,所谓历史记录的一部分,部分实现代码如下:
<!-- 输入框 -->
<div
:class="[
'threeS-search-container',
{ 'threeS-search-hover': mouseEnter },
{ 'threeS-search-active': glass },
]"
@mouseenter="handleMouseEvent"
@mouseleave="handleMouseEvent"
>
<div class="threeS-search-body">
<input
class="threeS-search-input"
type="text"
:placeholder="glass ? '' : '搜索内容'"
@focus="handleFocus"
v-model="searchValue"
@keyup.enter="handleSearch"
/>
<button
v-if="glass"
class="icon iconfont icon-magnifier search-icon"
@click="handleSearch"
></button>
</div>
</div>
<!-- 搜索类型 -->
<div
:class="[
'threeS-search-type-container',
{ 'threeS-type-focus': glass },
]"
>
<div
class="threeS-search-type"
:class="[{ 'is-active': item.id === searchType }]"
v-for="item in searchTypeList"
:key="item.id"
@click="handleSearchType(item)"
>
{{ item.name }}
</div>
</div>
这个是HTML部分,从结构上看没有什么复杂的,只是搜索的按钮需要融入搜索框的背景之中,不显突兀;
const searchTypeList: searchType[] = [
{
name: "百度",
id: "baidu",
url: "https://www.baidu.com/s?wd=",
},
{
name: "谷歌",
id: "google",
url: "https://www.google.com/search?q=",
},
{
name: "必应",
id: "bing",
url: "https://cn.bing.com/search?q=",
},
{
name: "淘宝",
id: "taobao",
url: "https://s.taobao.com/search?q=",
},
];
const searchType: Ref<string> = ref(searchTypeList[0].id);
const searchUrl: Ref<string> = ref(searchTypeList[0].url);
function handleSearchType(item: searchType): void {
searchType.value = item.id;
searchUrl.value = item.url;
}
这个一部分搜索类型时的代码,这部分也不复杂,但是这部分又很关键,因为每个平台的搜索地址前缀到底是什么样的,网络上查了很久都没查到合理的答案,只能自己试,试了好久才发现;
历史记录
历史记录这一块主要的知识点在Vue部分其实就是一个for循环,通过对数据的便利将其展示出来,对了,还有有一个,就是数据的来源,这里的一部分是来自于后台接口,一部分是来自于本地的localstage,这里存在一个数据合并的问题以及清除历史记录时需要对本地的localstage进行同步清理,下面是部分代码
// ---------- 历史信息 start ----------
const getHistoryInfo: any = computed(
() => store.getters["basic/basic/getHistory"]
);
watch(
() => getHistoryInfo.value,
(value) => {
historyList.value = value;
}
);
const updateHistory: ActionMethod = (history: urlInterface[]) =>
store.dispatch("basic/basic/updateHistoryInfo", history);
/**
* 清除历史记录
* @param {Object} item 待清除的项
*/
function handleClearHistory(item: urlInterface): void {
const newHistoryInfo: urlInterface[] = getHistoryInfo.value.filter(
(el: urlInterface) => {
return el.url !== item.url;
}
);
// 更新
updateHistory(newHistoryInfo);
}
function goHistoryUrl(item: urlInterface): void {
window.open(item.url);
}
/**
* 清除全部历史记录
*/
function clearHistory() {
// 更新
updateHistory([]);
}
// ---------- 历史信息 end ----------
快捷入口
快捷入口的难点其实在于ICON的展示,至于关于Vue的知识点其实也同样跟上面一样是一个v-for的循环,将数据遍历并展示出来,并为其增加点击事件,点击后直接打开新窗口;
至于ICON的难点在于一开始使用的是iconfont的Font class模式,但是后来发现Font class模式有一个最大的缺点:无法展示多色,因此整个icon都是一个个单色的方块,后来通过仔细了解字体模式,发现了它SVG的用法,使用SVG可以将其展示成多色,当然这个模式也有一个缺点,就是对浏览器的兼容没那么好,但是,IE什么的本来就不再考虑了,以下是部分代码:
<template>
<div class="threeS-stortucts-container">
<ul>
<li v-for="(item, index) in threeSStortucts" :key="index">
<div class="threeS-container" @click="handleGoUrl(item.url)">
<svg class="icon svg-icon" aria-hidden="true">
<use :xlink:href="item.icon"></use>
</svg>
</div>
<div class="threeS-stortucts-title">{{ item.name }}</div>
</li>
</ul>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, Ref, ref } from "vue";
import { useStore, Store } from "vuex";
export default defineComponent({
setup() {
const store: Store<any> = useStore();
function handleGoUrl(url: string): void {
window.open(url);
}
return {
handleGoUrl,
threeSStortucts: computed(
() => store.getters["basic/basic/getStortucts"]
),
};
},
});
</script>
小结
通过本项目,目的是为了更好的了解Vue3,毕竟Vue3已经正式发布,虽然项目中不会立即使用到,但是对于Vue3的理解已经是必须提上行程了,同时也必须去调研日常所用的插件,工具函数等是否能在Vue3+TypeScript中得到良好的运用;
好了,本文差不多就到这里,如果有任何问题欢迎留言提问,相互学习,一起成长!