记录博客搜索想法功能实现过程

全篇共 2847 字。按500字/分钟阅读,预计用时 5.7 分钟。总访问 235 次,日访问 2 次。

随着写过的想法数量增多,想要快速锁定到过去写的某一篇想法变得越发困难。好在我在每一篇文章在创建之初都给他们设置了多个关键词。这篇文章记录我为博客实现搜索功能的过程。日后再对搜索功能做重构或增加新功能也都记录在这篇文章中。

SQL语句

技术预研。要实现搜索查询功能,前端UI和后端接口逻辑部分对我来说都很好解决。唯一需要我研究的是如何从MySQL数据库中通过设定合适的查询条件查出需要的内容。虽然我在自己博客最早的一版中实现过相似的功能,记得有模糊匹配这个MySQL的技术名词,但还是通过一些途径搜索并甄别出适合当前我的需要的SQL语句。

LIKE 模糊匹配与 REGEXP 正则表达式两种方案中,我选择了对我更适合的前者。将关键词用两个百分号 % 前后夹击包裹起来,实现模糊匹配。实现模糊匹配的SQL语句看起来像这样:

SELECT * FROM my_table WHERE tags LIKE '%airglass%';

在SQL语句中使用正则表达式。REGEXP 关键字后的字符串内写正则表达式。这条语句看起来像这样:

SELECT * FROM my_table WHERE tags REGEXP 'airglass';

支撑我博客后端的语言是NodeJS。我一直在使用的操作数据库的npm依赖模块是mysql。后端不应相信前端即用户传过来的任何数据。mysql.escape() 方法能对用户传入的作为查询字段值使用的字符进行“消毒”,即做转义处理,防止破坏性SQL注入的发生。上方代码片段中,显然需要对 LIKE 后的部分做转义。

Async/Await

await/async 语法非常有用,解决了JS长期被诟病的回调黑洞问题。在定义函数前使用 async 关键词,让函数内的逻辑按语句出现的顺序执行。

按需返回JSON格式的数据。查询到符合要求的所有想法后,将结果通过JSON格式响应给前端。前端接收到查询结果后,渲染出文章列表,点击文章标题跳转到该文章内容页面。所以必须要返回的信息只有文章标题和文章链接。

module.exports = utils => {
  const express = require('express');
  const router = express.Router();
  const mysql = require('mysql');
  router.get('/:keywords', async (req, res) => {
    const keywords = req.params.keywords;
    const searchLike = mysql.escape(`%${keywords}%`);
    const result = await utils.query('autumn', `select title, alias, tags from article where tags like ${searchLike} order by pv desc limit 0,10;`);
    res.json({
      keywords,
      result
    })
  })
  return router;
}

utils 对象类似 angular 中注入的依赖。我需要用装饰器重构这里的代码。

来到前端UI部分。我采用先写HTML结构,其次写JS交互逻辑,最后写CSS样式的顺序。越是靠前的步骤,越是需要照顾到后续的步骤。比如在写HTML结构时,是否为元素设置 id 属性和 class 属性以及如何命名防止和网页其他部分冲突,这些考虑都需要在这一步大致地考虑到,这是为了方便在写JS逻辑部分时获取到元素,以及写CSS样式时更优雅。

为避免版权纠纷,模版引擎Jade更名为Pug。Pug语法对缩进格式异常敏感。如果把 Tab 和 Space 两种缩进搞混,那么Pug渲染引擎在解析模版时会把错误提示渲染成最终的HTML,告诉你要么统一成Tab缩进,要么统一成Sapce缩进,但不能在一个Pug模版文件中同时使用两者。

#search(style="margin-top: 15px;")
  input#searchInput(spellcheck="false",placeholder="输入关键词搜索想法")
  ul#searchResult

延迟请求

来到JS部分。初始化搜索组件,给输入框元素绑定 oninput 输入事件和 onfocus 获取焦点事件。

function initSearch() {
  const searchInputEl = document.querySelector('#searchInput');
  searchInputEl.onfocus = function () {
    eventEmit.call(this);
  }
  searchInputEl.oninput = function () {
    eventEmit.call(this);
  }
  function eventEmit() {
    let value = this.value.trim()
    if (!value || value.length < 2) {
      clearSearchResult();
      return
    }
    bounceSearch(value);
  }
}

延迟400毫秒向后端发送搜索想法的请求。当监听到输入事件的400毫秒内再次输入,中断请求。

let searchBounceTimer;
function bounceSearch(query) {
  clearTimeout(searchBounceTimer);
  searchBounceTimer = setTimeout(() => {
    ajax(`/search/${query}`, 'GET', (res) => {
      repaintSearchResult(res);
    })
  }, 400)
}

负责重新渲染搜索结果和清除已有搜索结果的函数。

function clearSearchResult() {
  const searchResultEl = document.querySelector('#searchResult');
  searchResultEl.innerHTML = '';
}

function repaintSearchResult(res) {
  const searchResultEl = document.querySelector('#searchResult');
  let html = '';
  for (let i of res.result) {
    html += `<a href="/article/${i.alias}/"><li><h1>${i.title}</h1><strong>${i.tags}</strong></li></a>`;
  }
  searchResultEl.innerHTML = html;
}

来到UI样式部分。红绿蓝三色是我博客的主要颜色,也设想过四季概念。搜索组件目前放置在主页右侧,可视化想法频率组件的上方。我用绿色作为搜索组件的主色。