Vue3开发可自由配置的浏览器起始页
前言
Howdz是基于Vue3
+ Typescript
开发的一个完全自定义配置的浏览器导航起始页,支持按需添加物料组件,可自由编辑组件的位置、大小与功能。支持响应式设计,可自定义随机壁纸、动态壁纸等。项目提供网页在线访问、打包出浏览器插件、打包出桌面应用(Electron)等访问方式。
本文记录项目开发中使用的相关技术。
表单封装
项目中运行自由添加各种物料组件,而每一个物料组件都含有自己的配置项表单,而其中又有部分相同的配置项,所以可以实现一个JS数据驱动的表单封装。
当前使用了ElementPlus
框架,封装了一个StandardForm组件,为其传入formData
与formConf
两个属性即可生成双向绑定的表单,支持JSX
插入其他自定义组件。因篇幅问题,组件封装代码可参考此处: standard-form.vue
然后可以使用类似JSON的格式,实现各个物料组件的配置表单,例如Weather
组件的setting.tsx
如下:
// @/materials/Weather/setting.tsx
import pick from '../base' // pick可以自由选取公用的配置
export default {
formData: {
weatherMode: 1,
cityName: '',
animationIcon: true,
duration: 15,
position: 5,
baseFontSize: 16,
textColor: '#262626',
textShadow: '0 0 1px #464646',
iconShadow: '0 0 1px #464646',
fontFamily: '',
padding: 10
},
formConf (formData: Record) { // 传入formData以实现双向绑定
return {
weatherMode: {
label: '天气城市',
type: 'radio-group',
radio: {
list: [{ name: '自动获取(IP)', value: 1 }, { name: '手动输入', value: 2 }],
label: 'name',
value: 'value'
}
},
cityName: {
when: (formData: Record) => formData.weatherMode === 2, // 类似v-if
type: 'input',
attrs: { placeholder: '请输入城市名(目前仅支持中国城市名)', clearable: true },
rules: [{
required: true,
validator: (rule: unknown, value: string, callback: (e?:Error) => void) => {
formData.weatherMode === 2 && !value ? callback(new Error('请输入城市名')) : callback()
}
}] // 支持el-form原生rule
},
animationIcon: {
label: '动画图标',
type: 'switch',
tips: '默认使用含动画的ICON,若想提高性能可关闭使用静态ICON'
},
duration: {
label: '自动刷新频率',
type: 'input-number',
attrs: { 'controls-position': 'right', min: 5, max: 12 * 60 },
tips: '刷新频率,单位为分钟'
},
...pick(formData, [ // 选取公用的配置
'position',
'baseFontSize',
'textColor',
'textShadow',
'iconShadow',
'fontFamily',
'padding'
])
}
}
}
右键菜单 物料组件添加后,在编辑模式下可以右键弹出菜单更改配置或删除等。右键菜单的实现来源与笔者开源的
@howdjs/mouse-menu
。同时在本项目中,为了兼容移动端,对插件进行了二次封装,为其添加了长按弹出菜单的功能。二次封装代码参考此处。项目中采用的是
vue指令
的方式使用,菜单插件可以接收任意参数进行回调,所以可以把点击的物料组件数据传到回调中进行各种操作。isLock, params: element, menuList }">
物料组件布局 当前提供2中布局方式,一种是基于类文件流的栅格布局,这种布局会让组件一个接一个排列,另外一个是Fixed布局,可以让组件固定与页面任意位置。
栅格模式
栅格模式使用vue-grid-layout实现,该插件vue3版本处于Beta中。
【Vue3开发可自由配置的浏览器起始页】使用
v-model:layout
双向绑定栅格模式物料组件列表数据,因为物料数组存在vuex中,这里用computed
的setter进行更新。isLock
是用于判断当前是否处于编辑模式,在锁定状态下禁用拖拽与大小更改。当前使用的栅格数为12,即将屏幕宽度分割为12份。Fixed模式
Fixed模式使用笔者自己开源的@howdjs/to-control插件完成,可以让物料组件固定在页面的任何位置中,也支持拖拽右下角更改大小。
isLock,
arrowOptions: { lineColor: '#9a98c3', size: 12, padding: 8 }
}"
:key="element.id"
@todragend="handleAffixDragend($event, element)"
@tocontrolend="handleAffixDragend($event, element)"
>
与栅格模式不同,这里是使用事件回调函数对组件的Vuex数据进行更新。也是使用
isLock
判断组件是否锁定。插件支持更改定位方向,记录在右上角、右下角等,这样对响应式布局很有效。更多用法可参考: @howdjs/to-control交互弹窗Popover 系统提供一种配置交互行为的功能,可以配置点击一个组件时弹窗另外一个组件,并配置组件弹出的方向。经过调研后发现
Element-plus
的Popover
并不太适合用于这种情况,因为弹出的组件时动态的。于是就自己封装了一个组件,不仅支持配置Popover
的各个方向,还另外扩展了一个ScreenCenter
的弹出,让组件可以在屏幕中间弹出(类似dialog
)。通过传入点击的元素、目标弹窗的宽高和弹窗方向,返回出目标弹窗的
x
和y
。核心代码如下:/**
* 获取Popover目标信息
* @param element 来源DOM
* @param popoverRect popover信息
* @param direction popover方向
* @returns [endX, endY, fromX, fromY]
*/
export function getPopoverActivePointByDirection(
element: HTMLElement,
popoverRect: PopoverOption,
direction = DirectionEnum.BOTTOM_CENTER
) {
const { width, height, top, left } = element.getBoundingClientRect()
const { width: popoverWidth, height: popoverHeight, offset = 10 } = popoverRect
const activePointMap = {
[DirectionEnum.SCREEN_CENTER]: [window.innerWidth / 2 - popoverWidth / 2, window.innerHeight / 2 - popoverHeight / 2],
[DirectionEnum.TOP_START]: [left, top - popoverHeight - offset],
[DirectionEnum.TOP_CENTER]: [left + width / 2 - popoverWidth / 2, top - popoverHeight - offset],
[DirectionEnum.TOP_END]: [left + width - popoverWidth, top - popoverHeight - offset],
[DirectionEnum.RIGHT_START]: [left + width + offset, top],
[DirectionEnum.RIGHT_CENTER]: [left + width + offset, top + height / 2 - popoverHeight / 2],
[DirectionEnum.RIGHT_END]: [left + width + offset, top + height - popoverHeight],
[DirectionEnum.BOTTOM_END]: [left + width - popoverWidth, top + height + offset],
[DirectionEnum.BOTTOM_CENTER]: [left + width / 2 - popoverWidth / 2, top + height + offset],
[DirectionEnum.BOTTOM_START]: [left, top + height + offset],
[DirectionEnum.LEFT_END]: [left - popoverWidth - offset, top + height - popoverHeight],
[DirectionEnum.LEFT_CENTER]: [left - popoverWidth - offset, top + height / 2 - popoverHeight / 2],
[DirectionEnum.LEFT_START]: [left - popoverWidth - offset, top]
}
const fromPoint = [left + width / 2, top + height / 2]
return [...activePointMap[direction], ...fromPoint] || [0, 0, ...fromPoint]
}
另外,使用
transform-origin
这个属性可以实现弹窗从点击元素过渡展开的动画。最后配置弹窗的方向与弹出的组件类型即可。代码参考:ActionPopover.vue获取任意网站Favicon 在
Collection
与Search
组件中,都有用到一个功能,就是由用户输入网址后能自动获取到网站的Favicon。在初版实现是直接使用网址origin + /favicon.ico获取,但经过大量尝试后发现,当前很多网站的icon并不是以这种标准形式存储的。所以后面就自己实现了一个后端接口来获取。后端接口原理:
- 从用户输入的网站中读取到origin
- 尝试从
Redis
中读取已缓存的图标路径,读取到则返回 - 若缓存中没有,这使用
cheerio
加载网站,使用$('link[rel*="icon"]').attr('href')
读取图标路径 - 若上一步没有读取到,则继续尝试使用标准形式读取,即网站Origin + /favicon.ico
- 读取成功则写入
Redis
缓存,否则返回获取失败
type
参数,可由后端直接返回图片流,以解决一些网站的ICON资源做了CORS限制。因为在Collection
组件中,为了减少初次访问请求加载数,前端读取到图标后会将图标转成BASE64格式存到本地存储中。这种方式需要使用Ajax获取图标,让接口直接返回文件流可以解决跨域问题。另读取图标时,前端会使用Canvas通道法将图标的白色部分扣成透明,代码可参考此处
总结 项目仍在持续优化开发中,欢迎各种建议。由于篇幅问题,部分使用到的技术会不定时更新记录。若感谢的可以持续关注、Star,谢谢。
相关链接
- Howdz介绍文档
- Github
- 在线网页版地址
推荐阅读
- 深入理解Go之generate
- 标签、语法规范、内联框架、超链接、CSS的编写位置、CSS语法、开发工具、块和内联、常用选择器、后代元素选择器、伪类、伪元素。
- 2019-1-14
- 抑郁症(可怕吗?)
- 松软可口易消化,无需烤箱超简单,新手麻麻也能轻松成功~
- 你不可不知的真相系列之科学
- 关于自我为中心的一点感想
- 为什么孩子一定要学会可视化思维!
- 唐嫣可真会穿,西装搭牛仔裤都能穿出高级感,一双大长腿太抢镜
- 我怀孕了可是我失业了,孕期生活,到底该何去何从()