为静态站点添加搜索功能

静态网页,指的是全部由html、css、js等文件组成的页面,一经发布出来,就已经不会再发生变化了(除非有人手动修改),静态网页是完全运行在客户端的。

与之相对应的便是动态网页,这种网页即使在没有人修改过网页内容的情况下,也会随着用户、时间、地点等因素而返回不同的内容,因此被称为是“动态”的。常见的有jsp、asp、php等等,此外使用异步请求向服务器获取数据也属于动态网页的范畴。

相比于静态网页,动态网页可以实现更丰富的功能,如登陆、注册、内容管理、数据统计、数据查询等等,这些功能都有一个共同点,即它们都是以数据库为基础来实现的。

至此我们了解到,动、静态网页的最主要区别,其实是页面内容有没有受到数据库的影响,而不是页面有没有在“动”(如gif、视频、动画效果等等)。

看到这里,可能会有读者疑惑:

既然静态页面不经过数据库,那要怎么实现搜索功能?

确实,在日常的生活和开发当中,大多数的搜索功能,都是使用异步请求来向服务端获取数据的,都是要经过数据库的,百度、谷歌搜索、淘宝上搜索商品、搜索WebAPI等等,但实际上,这只是搜索功能实现的方式之一,它并不是搜索的唯一方式,我们姑且把这种方式称为“动态搜索”。

如果有接触过一些静态博客项目的话,如WordPress、Hexo等,应该都知道这些系统都是支持文章搜索功能的。

WordPress

image

Hexo

image

以上的搜索功能没有经过服务器数据库。

虽然很想这么说,但是有多少人会信?

举一个更直观的例子,如果使用Vue框架开发过项目的话,应该都接触过ElementUI这个经典的UI库,其中就包含了一些具有搜索功能的组件,如Select、Cascader、Input,虽然我们一般都是将这些组件配合API来异步获取&渲染数据的,但实际上,即使没有API的支持,它也能够完成搜索功能,下面以Select组件为例。

提供给组件的数据

image

搜索数据

image

这个组件的搜索功能,其实跟前面提到的WordPress、Hexo系统的文章搜索功能是类似的,都是通过匹配固定的数据来返回搜索结果,需要注意的是,这一过程中,并没有数据库的参与,所以,它们都属于“静态搜索”。

现在我们应该了解到,静态搜索,就是把数据和搜索逻辑都放在前端,由前端来完成从输入关键字到返回数据的全搜索过程,这就是它的原理

其实无论静态搜索,还是动态搜索,都是通过某些算法来匹配数据返回搜索结果的,只不过是两者搜索逻辑的执行位置和数据存放位置不同而已。

所以,要完成这个静态搜索功能,有两个地方需要我们去准备,它们分别是负责检索数据的逻辑和用于检索的数据。

实现搜索逻辑

根据上面所举的Select组件例子,我们很容易想到,这个功能应该要满足这样的需求:

从搜索input表单中获取出文本,并将其和数据的某些字段匹配,如果匹配成功就把该条数据返回,最后将返回的数据渲染在搜索结果面板上。

首先准备需要的HTML结构

样式就不贴了,可能每个人做出来的效果都不一样,能用就行。

image

以下是渲染出来的效果。

image

定义搜索方法

此处的代码实现了一个简单的逻辑,即如果数据的title字段包含有输入的文本value的话,就将其返回。

// 跟日常开发一样,我们可以先忽略后端的数据,随便编一些就好了。
let dataList = [
    {
        fileName: "http:///11.html",
        tag: "测试标签1,测试标签11",
        title: "测试标题1",
        categoryName:"测试分类1"
    },
    {
        fileName: "http:///22.html",
        tag: "测试标签2",
        title: "测试标题2",
        categoryName:"测试分类2"
    },
    {
        fileName: "http:///33.html",
        tag: "测试标签3",
        title: "测试标题3",
        categoryName:"测试分类3"
    }
];

function search(value) {
    let r = [];
    for (let item of dataList || []) {
        if (item.title.includes(value)) {
            r.push(item);
        }
    }
    return r;
}

定义渲染数据方法

在该方法中执行搜索方法,如果有数据返回,则将数据用字符串拼接为一个ul列表,并将其设置为dom节点ele的html内容,否则返回找不到数据的提示信息。

let render = function(val, ele, ) {
    let list = search(val);
    let html = "";
    if (list.length > 0) {
        html += "<ul>";
        for (let item of list) {
            let href = item.fileName;
            html += "<li class='item' title='"+ item.title +"'>";
            html += "<a class='hover-1' href='"+ href +"'>" + item.title + "</a>";
            html += "</li>"
        }
        html += "</ul>";
    }
    else {
        html += "<div>搜索不到数据:|</div>";
    }
    ele.innerHTML = html;
}

为input搜索框添加事件侦听器

监听input输入事件和失去焦点事件,同时,为了尽可能地优化性能,我们还可以用防抖函数对主要函数进行包装,来处理频繁执行的问题。

该侦听器主要的功能为,监听输入事件,并获取其中的文本,如果不为空,则执行渲染方法,并显示搜索结果面板,否则隐藏搜索结果面板。

let searchInput = document.querySelector("#search-input");
let searchResult = document.querySelector("#search-result");
let content = searchResult.querySelector('.content');

// 防抖
function debounce(fn, delay = 500) {
    let timer = null;
    return function (...args) {
        if (timer) clearTimeout(timer);
        timer = setTimeout(() => {
            fn.apply(this, args);
        }, delay);
    };
}

let wrap = debounce(function() {
    let value = searchInput.value;
    if (value) {
        render(value, content);
        searchResult.style.display = "block";
    }
    else {
        if (searchResult.style.display === 'block') {
            searchResult.style.display = "none";
        }
    }
});
searchInput.addEventListener('input', wrap);
searchInput.addEventListener('focus', wrap);

完成以上的代码后,基本的搜索功能已经实现了。

上述的代码是连贯的,都在同一个文件内,按顺序摆放就行,分开只是为了让它们看起来更方便

image

此外我们还可以为匹配命中的字符添加高亮效果,将搜索逻辑部分代码修改如下。

function search(value) {
    let r = [];
    for (let item of dataList || []) {
        let reg = new RegExp('(' + value + ')', 'i');
        if (reg.test(item.title)) {
            // 将匹配命中的字符用span标签包装,并添加样式
            let title = item.title.replace(reg, "<span style='color: var(--c-1);'>$1</span>");
            let _item = {
                fileName: item.fileName,
                tag: item.tag,
                title: title,
                categoryName: item.categoryName,
                original: item.title // 由于替换后的title并不是纯文本,所以把原数据备份,后续可能会使用到
            };
            r.push(_item);
        }
    }
    return r;
}

查看运行效果。

image

只差最后一步

完成以上的代码编写后,只差准备数据这一步了。

这一步是异常简单的,你甚至可以不用写逻辑代码,自己手动输入一份格式正确的数据(即包含标题等信息,上述的例子中只用到了title这一字段),或者也可以像我一样,把从数据库查询出来的记录按照正确的格式直接写进一个js文件里,然后再通过script标签加载,相当的简单粗暴。

数据库查询的记录如下。

image

这里使用的是java语言,其他语言也是类似的,其他语言请自行实现

private void writeDataListToJsFile(List<Article> articleList) {
        List<Map<String, Object>> dataList = new ArrayList<>();

        articleList.forEach(item -> {
            Map<String, Object> map = new HashMap<>();
            map.put("title", item.getTitle());
            map.put("fileName", FolderName.ARTICLE_FOLDER + File.separator + item.getId() + ".html");
            map.put("categoryName", item.getCategoryName());
            map.put("tag", item.getTag());
            dataList.add(map);
        });

        String outputPath = appConfig.getOutputPath();
        try {
            BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(outputPath + File.separator + FolderName.JS_FOLDER + File.separator + "dataList.js"));
            String str = "let dataList = " + JSON.toJSONString(dataList) + ";";
            bufferedWriter.write(str);
            IOUtil.close(bufferedWriter);
        } catch (IOException e) {
            e.printStackTrace();
            throw new CustomException("将数据写入文件失败");
        }
}

然后在html文件中,使用script标签加载该js文件(注意要在搜索逻辑之前加载)。

image

最终的效果如下。

image


至此,这个简单版的静态搜索功能就算完成了,如果需要更复杂的搜索逻辑和更多的匹配字段,比方说,同时根据标题、标签、分类进行匹配,可以在此基础上自行拓展。

当然,该功能缺点也是有的,比如说,当数据量很大的时候,查询速度会变得很慢,以及加载网页的速度也会变慢,会严重影响用户的网站体验,这也是为什么常用异步请求搜索数据的原因之一,但对于个人博客项目那点体量的数据而言,无需担心这个问题。

以上就是本篇文章的所有内容了,希望对你有所帮助。

返回