Vue3开发可自由配置的浏览器起始页

前言

Howdz是基于Vue3 + Typescript开发的一个完全自定义配置的浏览器导航起始页,支持按需添加物料组件,可自由编辑组件的位置、大小与功能。支持响应式设计,可自定义随机壁纸、动态壁纸等。项目提供网页在线访问、打包出浏览器插件、打包出桌面应用(Electron)等访问方式。

本文记录项目开发中使用的相关技术。

表单封装

项目中运行自由添加各种物料组件,而每一个物料组件都含有自己的配置项表单,而其中又有部分相同的配置项,所以可以实现一个JS数据驱动的表单封装。

当前使用了ElementPlus框架,封装了一个StandardForm组件,为其传入formDataformConf两个属性即可生成双向绑定的表单,支持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指令的方式使用,菜单插件可以接收任意参数进行回调,所以可以把点击的物料组件数据传到回调中进行各种操作。


物料组件布局

当前提供2中布局方式,一种是基于类文件流的栅格布局,这种布局会让组件一个接一个排列,另外一个是Fixed布局,可以让组件固定与页面任意位置。

栅格模式

栅格模式使用vue-grid-layout实现,该插件vue3版本处于Beta中。


使用v-model:layout双向绑定栅格模式物料组件列表数据,因为物料数组存在vuex中,这里用computed的setter进行更新。isLock是用于判断当前是否处于编辑模式,在锁定状态下禁用拖拽与大小更改。当前使用的栅格数为12,即将屏幕宽度分割为12份。

Fixed模式

Fixed模式使用笔者自己开源的@howdjs/to-control插件完成,可以让物料组件固定在页面的任何位置中,也支持拖拽右下角更改大小。


与栅格模式不同,这里是使用事件回调函数对组件的Vuex数据进行更新。也是使用isLock判断组件是否锁定。插件支持更改定位方向,记录在右上角、右下角等,这样对响应式布局很有效。更多用法可参考: @howdjs/to-control

交互弹窗Popover

系统提供一种配置交互行为的功能,可以配置点击一个组件时弹窗另外一个组件,并配置组件弹出的方向。经过调研后发现Element-plusPopover并不太适合用于这种情况,因为弹出的组件时动态的。于是就自己封装了一个组件,不仅支持配置Popover的各个方向,还另外扩展了一个ScreenCenter的弹出,让组件可以在屏幕中间弹出(类似dialog)。

通过传入点击的元素、目标弹窗的宽高和弹窗方向,返回出目标弹窗的xy。核心代码如下:

/**
 * 获取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

CollectionSearch组件中,都有用到一个功能,就是由用户输入网址后能自动获取到网站的Favicon。在初版实现是直接使用网址origin + /favicon.ico获取,但经过大量尝试后发现,当前很多网站的icon并不是以这种标准形式存储的。所以后面就自己实现了一个后端接口来获取。

后端接口原理:

  1. 从用户输入的网站中读取到origin
  2. 尝试从Redis中读取已缓存的图标路径,读取到则返回
  3. 若缓存中没有,这使用cheerio加载网站,使用$('link[rel*="icon"]').attr('href')读取图标路径
  4. 若上一步没有读取到,则继续尝试使用标准形式读取,即网站Origin + /favicon.ico
  5. 读取成功则写入Redis缓存,否则返回获取失败

同时接口接收type参数,可由后端直接返回图片流,以解决一些网站的ICON资源做了CORS限制。因为在Collection组件中,为了减少初次访问请求加载数,前端读取到图标后会将图标转成BASE64格式存到本地存储中。这种方式需要使用Ajax获取图标,让接口直接返回文件流可以解决跨域问题。

另读取图标时,前端会使用Canvas通道法将图标的白色部分扣成透明,代码可参考此处

总结

项目仍在持续优化开发中,欢迎各种建议。由于篇幅问题,部分使用到的技术会不定时更新记录。若感谢的可以持续关注、Star,谢谢。

相关链接

你可能感兴趣的