boycot 搜索
avatar

boycot

浏览器导航首页设计

2021/11/01 更新: 以下内容为旧版网站的记录,站点地址已更新为 Howdz 起始页项目地址

一个浏览器首页站点, 包含可切换的常用搜索引擎搜索功能, 键盘布局添加快捷收藏网站, 并加入键盘按键监听可快速打开, 自定义背景图, 配置同步功能等功能

系统半成品已部署与线上,在线访问:https://howdz.xyz

目录

  1. 搜索引擎切换功能
  2. 键盘收藏夹功能
  3. 背景图切换功能
  4. 配置同步功能
  5. 关于优化

搜索引擎切换功能

该功能时为了便于让用户可快速切换不同的搜索引擎,可以涉及不同领域的搜索,例如常用引擎、视频、翻译等搜索。在搜索框聚焦状态下按 Tab 键就可按用户规定的顺序快速切换引擎(Shift + Tab 向上切换)。

handleInputKeyDown (e) {
  if (e.keyCode === 9) {
    if (e.shiftKey) {
      this.activeEngine = this.activeEngine <= 0 ? this.$store.state.engineList.length - 1 : --this.activeEngine
      e.preventDefault()
    } else {
      this.activeEngine = this.activeEngine >= this.$store.state.engineList.length - 1 ? 0 : ++this.activeEngine
      e.preventDefault()
    }
  }
  if (e.keyCode === 13) {
    window.open(this.$store.state.engineList[this.activeEngine].link + encodeURIComponent(this.searchKey))
  }
}

寻找目前主流搜索引擎关键字拼接规则记录列表和寻找 Icon 保存到 VUEX 中,目前设置了默认引擎为 Bing 国内、国外、百度,然后备用设置了 Google、搜狗、Bilibili、淘宝等。用户可以在设置页通过拖拽切换引擎顺序与添加备用搜索到当前。

拖拽功能使用 vuedragable 实现,将当前引擎与备用引擎设为同一个 group,即可让两者可以互相拖拽,并且通过 pull 设置实现当 engineList 长度为 1 是不可再向外拖出。

...
<div class="text">当前引擎组</div>
<draggable
  :list="engineList"
  :group="{ name: 'engine',pull: engineList.length > 1 }"
  @end="handleDragEnd"
>
  <transition-group
    type="transition"
    name="flip-list"
    class="now-engine-list engine-list"
  >
    <div class="engine-list-item" v-for="item in engineList" :key="item.name">
      <img :src="item.iconPath" alt="icon" width="24" height="24" />
      <div class="text">{{item.name}}</div>
    </div>
  </transition-group>
</draggable>
<div class="text">备用引擎组</div>
<draggable :list="backupEngineList" group="engine" @end="handleDragEnd">
  <transition-group
    type="transition"
    name="flip-list"
    class="backupEngineList engine-list"
  >
    <div
      class="engine-list-item"
      v-for="item in backupEngineList"
      :key="item.name"
    >
      <img :src="item.iconPath" alt="icon" width="24" height="24" />
      <div class="text">{{item.name}}</div>
    </div>
  </transition-group>
</draggable>
...

键盘收藏夹功能

用户可通过点击模拟键盘按键快速跳转到收藏好的网站,未设置时点击则弹窗让用户添加。

主要功能实现:

  1. 截取用户输入的 http 地址中的域名,然后通过“域名 + /favicon.ico”获取主流网站的 Icon,当获取不到时,使用截取 Title 的首字符作为 Icon。亦可使用谷歌的 Favicon 服务,通过“http://www.google.cn/s2/favicons?domain= + 域名”获取网站 Icon,但获取出来的都是固定 16px x 16px 大小。
  2. 使用 Flex 布局实现模拟键盘布局
  3. 监听按键添加事件,window.open 打开用户收藏的网站
  4. 使用个人组件Animation Dialog实现动画弹窗(Where open where close 交互)
<img
  class="icon"
  :src="`${userSettingKeyMap[key].url.match(/^(\w+:\/\/)?([^\/]+)/i) ? userSettingKeyMap[key].url.match(/^(\w+:\/\/)?([^\/]+)/i)[0] : ''}/favicon.ico`"
  alt="link"
  @load="hanldeImgLoad"
  @error="handleImgError"
/>
<div class="no-icon">{{userSettingKeyMap[key].remark.slice(0,1)}}</div>

添加展示

背景图切换功能

背景图使用的图片来自免费无版权图片壁纸网站Unplash,并使用其提供的API 服务获取 JSON 图片列表。其 Api 接口不可直接调用,需要注册获取到 accessKey 之后将其放在请求中才可使用接口服务,且普通用户每小时只可调用 50 次,因此不合适直接把获取 unsplash 图片的请求放在前端。

后端实现

后端使用 Nodejs 每天定时调用 1 次获取 Unsplash 最新图片的接口,并把返回数据保留为 json 文件,然后由 Nodejs 提供接口,即背景图片以天为单位更新。

// Nodejs后端服务
const { unsplashApiKey } = require('../config/config') // 调用UnsplashAPI的Access Key
const schedule = require('node-schedule') // nodejs定时器服务
...
// 获取Unsplash最新图片
const getUnsplashPhotos = async () => {
  const pageSize = 30
  const photosList = []
  try {
    for (let page = 1; page <= 4; page++) {
      const url = `https://api.unsplash.com/photos?page=${page}&per_page=${pageSize}&client_id=${unsplashApiKey}`
      const { data } = await axios.get(url)
      const result = data.filter(item => {
        return item.width > item.height
      }).map(item => {
        const { id, width, height, color, description, urls, links } = item
        return { id, width, height, color, description, urls, links }
      })
      photosList.push(...result)
    }
    const today = getToday()
    const info = {
      date: today,
      num: photosList.length,
      list: photosList
    }
    const data = JSON.stringify(info, null, '\t')
    fs.writeFileSync(`./unsplash/${today}.json`, data)
    logger('定时获取Unsplash图片')
  } catch (e) {
    logger('定时获取Unsplash图片', 0, e)
  }
}
...
// 获取今日图片
router.get('/photos', async ctx => {
  const fileList = fs.readdirSync('./unsplash').sort((a, b) => {
    const [date1] = a.split('.')
    const [date2] = b.split('.')
    return new Date(date2) - new Date(date1)
  })
  const latest = fileList[0]
  const txt = fs.readFileSync(`./unsplash/${latest}`, 'utf-8')
  try {
    const data = JSON.parse(txt)
    ctx.body = r.successData(data)
  } catch (e) {
    ctx.body = r.error(308, e)
  }
})
...
// 每天1点定时获取Unsplash图片保存JSON
const runUnsplashSchedule = () => {
  schedule.scheduleJob('0 1 1 * * *', () => {
    getUnsplashPhotos()
  })
}
runUnsplashSchedule()
...

前端处理

前端使用 Vuex 保留用户每次切换获取的图片缓存,在不刷新页面下,同一张图片不需要再次加载。并将最后一次获取到的图片转成 Base64 保存到 Localstorage 里面的,此时要注意多数浏览器 Localstorage 最大存储 5M,需要做下判断,图片过大就不进行缓存了。

关于获取图片资源,一开始是使用 new Image()方案然后监听 onload 事件用 canvas 将 Img 转成 Base64 来实现。但是后面发现 canvas 将 Unsplash 图片转成 base64 会有跨域问题,尽管将Img 的 crossOrigin 属性设成’anonymous’,在 Chrome 下没问题,但是用 Safari 依然报跨域。最后采用了另外一种方案,使用 Ajax 去加载图片资源。需要将 responseType 改为 arraybuffer 方式,然后读取二进制拼接成 base64。使用 Ajax 方式还有一个优点,就是可以获取到加载进度,直接用 img 的 src 去获取无法监听图片下载进度。

ajax 获取图片为 base64

// ajax读取图片为base64
// processFn为监听进度的回调
export const getBase64ByAjax = (url, formatter = "image/png", processFn) => {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open("GET", url, true);
    xhr.responseType = "arraybuffer";
    xhr.onload = (e) => {
      if (xhr.status === 200) {
        const uInt8Array = new Uint8Array(xhr.response);
        let i = uInt8Array.length;
        const binaryString = new Array(i);
        while (i--) {
          binaryString[i] = String.fromCharCode(uInt8Array[i]);
        }
        const data = binaryString.join("");
        const base64 = window.btoa(data);
        const dataURL =
          "data:" + (formatter || "image/png") + ";base64," + base64;
        resolve(dataURL);
      }
    };
    xhr.onerror = (e) => {
      reject(e);
    };
    xhr.onprogress = (e) => {
      processFn && processFn(e);
    };
    xhr.send();
  });
};

Vuex 记录图片加载及其缓存

export default new Vuex.Store({
  state: {
    // ... //
    unsplashImgList: [],
    downloadingImgInfo: null,
    downloadingImgBase64: "",
    downloadingProcess: 0,
    cachecover: {},
    // ... //
  },
  mutations: {
    // ... //
    setEngineList(state, engineList) {
      state.engineList = engineList;
    },
    setBackupEngineList(state, backupEngineList) {
      state.backupEngineList = backupEngineList;
    },
    setUnsplashImgList(state, unsplashImgList) {
      state.unsplashImgList = unsplashImgList;
    },
    setDownloadingImgInfo(state, downloadingImgInfo) {
      state.downloadingImgInfo = downloadingImgInfo;
    },
    setDownloadingProcess(state, downloadingProcess) {
      state.downloadingProcess = downloadingProcess;
    },
    setDownloadingImgBase64(state, base64) {
      document.body.style.setProperty(
        "--textColor",
        base64 ? "#f8f8f9" : "#262626"
      );
      document.body.style.setProperty(
        "--textShadowColor",
        base64 ? "#262626" : "transparent"
      );
      state.downloadingImgBase64 = base64;
      const userTodayImgCache = {
        date: getToday(),
        base64,
      };
      const toJson = JSON.stringify(userTodayImgCache);
      if (toJson.length < 3.5 * 1024 * 1024) {
        localStorage.setItem(
          "userTodayImgCache",
          JSON.stringify(userTodayImgCache)
        );
      }
    },
    setCacheImg(state, { imgId, base64 }) {
      state.cacheImg = {
        ...state.cacheImg,
        [imgId]: base64,
      };
    },
    // ... //
  },
  actions: {
    // ... //
    getDownloadingImg({ commit, state }, downloadingImg) {
      const imgId = downloadingImg.id;
      if (state.cacheImg[imgId]) {
        commit("setDownloadingImgBase64", state.cacheImg[imgId]);
      } else {
        let imgURL;
        if (document.body.clientWidth >= 1440) {
          imgURL = downloadingImg.urls.regular
            .replace("w=1080", "w=1920")
            .replace("q=80", "q=70");
        } else {
          imgURL = downloadingImg.urls.regular.replace("q=80", "q=70");
        }
        commit("setDownloadingImgInfo", downloadingImg);
        commit("setDownloadingProcess", 0);
        const processFn = (e) => {
          const process = ~~((e.loaded / e.total) * 100);
          commit("setDownloadingProcess", process);
        };
        getBase64ByAjax(imgURL, "image/png", processFn).then((data) => {
          const dataURL = data;
          commit("setDownloadingImgBase64", dataURL);
          commit("setCacheImg", { imgId, base64: dataURL });
          commit("setDownloadingImgInfo", null);
        });
      }
    },
    // ... //
  },
});

背景切换展示

当前并未实现自定义图片上传功能,后续进行优化

配置同步功能

该功能未在线上版本实现,但已有实现思路。

  1. 方案一:用户注册账号,登录后自动同步配置。该方案为传统方案,但是系统功能单一,用上账户功能对用户来说是过于麻烦,而且涉及到账号安全问题。(不推荐)
  2. 方案二:用户点击保存配置按钮后生成一串 AccessKey 随机字符串,在另一端设备用户输入该字符串发送请求,后端返回改字符串对应的配置信息。随机字符串生成后有效期为 24 小时,后端定时删除。(推荐)
  3. 方案三:导出 json 文件进行同步。(不推荐)

关于优化

打包优化

项目使用到的 vue、vuex 等资源使用线上 CDN 服务,可减少打包大小并减轻服务端带宽压力。使用 Vue-cli3 的项目在 vue.config.js 中加入 externals 配置,不打包 vue 相关资源,并在 index.html 加入 Vue CDN 资源。

// vue.config.js
module.exports = {
  // ...
  configureWebpack: (config) => {
    config.externals = {
      vue: "Vue",
      vuex: "Vuex",
      // 'vue-router': 'VueRouter',
      // axios: 'axios'
    };
  },
  // ...
};

因系统功能完全是单页面完成,删除了 vue-router 功能,涉及请求不多也将 axios 改为原生 ajax 实现

图片优化

  1. 由于 Unsplash 为境外站点,国内访问有可能速度很慢。可以考虑在 nodejs 进行获取图片请求后,再将每张图片保存到本地。或者为了减轻服务器带宽压力,可以将图片上传到七牛云或腾讯云的提供的图片资源服务。
  2. Unsplash 提供的图片 api 接口,可以判断当前用户的设备,例如区分手机端和 PC 端,然后更改请求部分参数使其返回不同大小的图片。
  3. 将图片缓存到浏览器中。

系统半成品已部署与线上,在线访问:https://howdz.xyz

以上内容未经授权请勿随意转载。

乳白
杏仁黄
茉莉黄
麦秆黄
油菜花黄
佛手黄
篾黄
葵扇黄
柠檬黄
金瓜黄
藤黄
酪黄
香水玫瑰黄
淡密黄
大豆黄
素馨黄
向日葵黄
雅梨黄
黄连黄
金盏黄
蛋壳黄
肉色
鹅掌黄
鸡蛋黄
鼬黄
榴萼黄
淡橘橙
枇杷黄
橙皮黄
北瓜黄
杏黄
雄黄
万寿菊黄
菊蕾白
秋葵黄
硫华黄
柚黄
芒果黄
蒿黄
姜黄
香蕉黄
草黄
新禾绿
月灰
淡灰绿
草灰绿
苔绿
碧螺春绿
燕羽灰
蟹壳灰
潭水绿
橄榄绿
蚌肉白
豆汁黄
淡茧黄
乳鸭黄
荔肉白
象牙黄
炒米黄
鹦鹉冠黄
木瓜黄
浅烙黄
莲子白
谷黄
栀子黄
芥黄
银鼠灰
尘灰
枯绿
鲛青
粽叶绿
灰绿
鹤灰
淡松烟
暗海水绿
棕榈绿
米色
淡肉色
麦芽糖黄
琥珀黄
甘草黄
初熟杏黄
浅驼色
沙石黄
虎皮黄
土黄
百灵鸟灰
山鸡黄
龟背黄
苍黄
莱阳梨黄
蜴蜊绿
松鼠灰
橄榄灰
蟹壳绿
古铜绿
焦茶绿
粉白
落英淡粉
瓜瓤粉
蜜黄
金叶黄
金莺黄
鹿角棕
凋叶棕
玳瑁黄
软木黄
风帆黄
桂皮淡棕
猴毛灰
山鸡褐
驼色
茶褐
古铜褐
荷花白
玫瑰粉
橘橙
美人焦橙
润红
淡桃红
海螺橙
桃红
颊红
淡罂粟红
晨曦红
蟹壳红
金莲花橙
草莓红
龙睛鱼红
蜻蜓红
大红
柿红
榴花红
银朱
朱红
鲑鱼红
金黄
鹿皮褐
醉瓜肉
麂棕
淡银灰
淡赭
槟榔综
银灰
海鸥灰
淡咖啡
岩石棕
芒果棕
石板灰
珠母灰
丁香棕
咖啡
筍皮棕
燕颔红
玉粉红
金驼
铁棕
蛛网灰
淡可可棕
中红灰
淡土黄
淡豆沙
椰壳棕
淡铁灰
中灰驼
淡栗棕
可可棕
柞叶棕
野蔷薇红
菠萝红
藕荷
陶瓷红
晓灰
余烬红
火砖红
火泥棕
绀红
橡树棕
海报灰
玫瑰灰
火山棕
豆沙
淡米粉
初桃粉红
介壳淡粉红
淡藏花红
瓜瓤红
芙蓉红
莓酱红
法螺红
落霞红
淡玫瑰灰
蟹蝥红
火岩棕
赭石
暗驼棕
酱棕
栗棕
洋水仙红
谷鞘红
苹果红
铁水红
桂红
极光红
粉红
舌红
曲红
红汞红
淡绯
无花果红
榴子红
胭脂红
合欢红
春梅红
香叶红
珊瑚红
萝卜红
淡茜红
艳红
淡菽红
鱼鳃红
樱桃红
淡蕊香红
石竹红
草茉莉红
茶花红
枸枢红
秋海棠红
丽春红
夕阳红
鹤顶红
鹅血石红
覆盆子红
貂紫
暗玉紫
栗紫
葡萄酱紫
牡丹粉红
山茶红
海棠红
玉红
高粱红
满江红
枣红
葡萄紫
酱紫
淡曙红
唐菖蒲红
鹅冠红
莓红
枫叶红
苋菜红
烟红
暗紫苑红
殷红
猪肝紫
金鱼紫
草珠红
淡绛红
品红
凤仙花红
粉团花红
夹竹桃红
榲桲红
姜红
莲瓣红
水红
报春红
月季红
豇豆红
霞光红
松叶牡丹红
喜蛋红
鼠鼻红
尖晶玉红
山黎豆红
锦葵红
鼠背灰
甘蔗紫
石竹紫
苍蝇灰
卵石紫
李紫
茄皮紫
吊钟花红
兔眼红
紫荆红
菜头紫
鹞冠紫
葡萄酒红
磨石紫
檀紫
火鹅紫
墨紫
晶红
扁豆花红
白芨红
嫩菱红
菠根红
酢酱草红
洋葱紫
海象紫
绀紫
古铜紫
石蕊红
芍药耕红
藏花红
初荷红
马鞭草紫
丁香淡紫
丹紫红
玫瑰红
淡牵牛紫
凤信紫
萝兰紫
玫瑰紫
藤萝紫
槿紫
蕈紫
桔梗紫
魏紫
芝兰紫
菱锰红
龙须红
蓟粉红
电气石红
樱草紫
芦穗灰
隐红灰
苋菜紫
芦灰
暮云灰
斑鸠灰
淡藤萝紫
淡青紫
青蛤壳紫
豆蔻紫
扁豆紫
芥花紫
青莲
芓紫
葛巾紫
牵牛紫
紫灰
龙睛鱼紫
荸荠紫
古鼎灰
乌梅紫
深牵牛紫
银白
芡食白
远山紫
淡蓝紫
山梗紫
螺甸紫
玛瑙灰
野菊紫
满天星紫
锌灰
野葡萄紫
剑锋紫
龙葵紫
暗龙胆紫
晶石紫
暗蓝紫
景泰蓝
尼罗蓝
远天蓝
星蓝
羽扇豆蓝
花青
睛蓝
虹蓝
湖水蓝
秋波蓝
涧石蓝
潮蓝
群青
霁青
碧青
宝石蓝
天蓝
柏林蓝
海青
钴蓝
鸢尾蓝
牵牛花蓝
飞燕草蓝
品蓝
银鱼白
安安蓝
鱼尾灰
鲸鱼灰
海参灰
沙鱼灰
钢蓝
云水蓝
晴山蓝
靛青
大理石灰
海涛蓝
蝶翅蓝
海军蓝
水牛灰
牛角灰
燕颔蓝
云峰白
井天蓝
云山蓝
釉蓝
鸥蓝
搪磁蓝
月影白
星灰
淡蓝灰
鷃蓝
嫩灰
战舰灰
瓦罐灰
青灰
鸽蓝
钢青
暗蓝
月白
海天蓝
清水蓝
瀑布蓝
蔚蓝
孔雀蓝
甸子蓝
石绿
竹篁绿
粉绿
美蝶绿
毛绿
蔻梢绿
麦苗绿
蛙绿
铜绿
竹绿
蓝绿
穹灰
翠蓝
胆矾蓝
樫鸟蓝
闪蓝
冰山蓝
虾壳青
晚波蓝
蜻蜓蓝
玉鈫蓝
垩灰
夏云灰
苍蓝
黄昏灰
灰蓝
深灰蓝
玉簪绿
青矾绿
草原远绿
梧枝绿
浪花绿
海王绿
亚丁绿
镍灰
明灰
淡绿灰
飞泉绿
狼烟灰
绿灰
苍绿
深海绿
长石灰
苷蓝绿
莽丛绿
淡翠绿
明绿
田园绿
翠绿
淡绿
葱绿
孔雀绿
艾绿
蟾绿
宫殿绿
松霜绿
蛋白石绿
薄荷绿
瓦松绿
荷叶绿
田螺绿
白屈菜绿
河豚灰
蒽油绿
槲寄生绿
云杉绿
嫩菊绿
艾背绿
嘉陵水绿
玉髓绿
鲜绿
宝石绿
海沬绿
姚黄
橄榄石绿
水绿
芦苇绿
槐花黄绿
苹果绿
芽绿
蝶黄
橄榄黄绿
鹦鹉绿
油绿
象牙白
汉白玉
雪白
鱼肚白
珍珠灰
浅灰
铅灰
中灰
瓦灰
夜灰
雁灰
深灰