なんとなくやってみた
こちらのGist(eddiewebb/readme.md)を参考にしてやってみた。
仕組みとしては、
- ビルドの際に、すべての記事のデータを含んだJSONファイルを作成し、
/index.json
というパスでホスティングする。 /search
という独自のページを作成して、その画面で検索文字列を入力しsubmitすると、画面の再読み込みが起こり、Javascriptが再実行され検索する- Javascriptでは以下の処理を行っている
/index.json
にアクセスし、すべての記事のデータを取得するFuse.js
にデータを入れて検索- 検索して取得した結果から、検索文字列を
mark.js
でハイライトさせ、画面に表示させる
- Javascriptでは以下の処理を行っている
上記に従えばだいたいうまくいったけど、検索ごとに再読み込みが発生してしまい、/index.json
やその他jsやcssを無駄にリソースを食ってしまう
なので再読み込みさせずjsの中だけで検索機能を完結させてみた。
layouts/_default/search.html
// LICENCE: https://gist.github.com/eddiewebb/735feb48f50f0ddd65ae5606a1cb41ae#mit-license
summaryInclude = 60;
var fuseOptions = {
shouldSort: true,
includeMatches: true,
threshold: 0.3,
tokenize: true,
keys: [
{ name: "title", weight: 0.8 },
{ name: "contents", weight: 0.5 },
{ name: "tags", weight: 0.3 },
{ name: "categories", weight: 0.3 }
]
};
let fuse = null
initialize();
function initialize() {
$.getJSON("/index.json", function (data) {
var pages = data;
fuse = new Fuse(pages, fuseOptions);
/** 初期化 */
onSearch(param("s"));
// hook onSearch function on input string change
$("#search-query").on("input", debounce(function () {
const queryString = $("#search-query").val()
history.replaceState(null, null, "?s="+queryString);
onSearch(queryString);
}, 500));
});
}
// http://davidwalsh.name/javascript-debounce-function
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};
function onSearch(queryString) {
$('#search-results').empty();
if (!queryString) {
$('#search-results').append("<p>検索したい文字を入力してください</p>");
return
}
// reset content in search-results
var result = fuse.search(queryString);
if (result.length > 0) {
populateResults(result, queryString);
} else {
$('#search-results').append("<p>合致する記事がありませんでした</p>");
}
};
function populateResults(result, queryString) {
$.each(result, function (key, value) {
var contents = value.item.contents;
var snippet = "";
var snippetHighlights = [];
var tags = [];
if (fuseOptions.tokenize) {
snippetHighlights.push(queryString);
} else {
$.each(value.matches, function (matchKey, mvalue) {
console.log(mvalue.indices)
if (mvalue.key == "tags" || mvalue.key == "categories") {
snippetHighlights.push(mvalue.value);
} else if (mvalue.key == "contents") {
start = mvalue.indices[0][0] - summaryInclude > 0 ? mvalue.indices[0][0] - summaryInclude : 0;
end = mvalue.indices[0][1] + summaryInclude < contents.length ? mvalue.indices[0][1] + summaryInclude : contents.length;
snippet += contents.substring(start, end);
snippetHighlights.push(mvalue.value.substring(mvalue.indices[0][0], mvalue.indices[0][1] - mvalue.indices[0][0] + 1));
}
});
}
if (snippet.length < 1) {
snippet += contents.substring(0, summaryInclude * 2);
}
//pull template from hugo templarte definition
var templateDefinition = $('#search-result-template').html();
//replace values
var output = render(templateDefinition, { key: key, title: value.item.title, link: value.item.permalink, tags: value.item.tags, categories: value.item.categories, snippet: snippet });
$('#search-results').append(output);
$.each(snippetHighlights, function (snipkey, snipvalue) {
$("#summary-" + key).mark(snipvalue);
});
});
}
function param(name) {
return decodeURIComponent((location.search.split(name + '=')[1] || '').split('&')[0]).replace(/\+/g, ' ');
}
function render(templateString, data) {
var conditionalMatches, conditionalPattern, copy;
conditionalPattern = /\$\{\s*isset ([a-zA-Z]*) \s*\}(.*)\$\{\s*end\s*}/g;
//since loop below depends on re.lastInxdex, we use a copy to capture any manipulations whilst inside the loop
copy = templateString;
while ((conditionalMatches = conditionalPattern.exec(templateString)) !== null) {
if (data[conditionalMatches[1]]) {
//valid key, remove conditionals, leave contents.
copy = copy.replace(conditionalMatches[0], conditionalMatches[2]);
} else {
//not valid, remove entire section
copy = copy.replace(conditionalMatches[0], '');
}
}
templateString = copy;
//now any conditionals removed we can do simple substitution
var key, find, re;
for (key in data) {
find = '\\$\\{\\s*' + key + '\\s*\\}';
re = new RegExp(find, 'g');
templateString = templateString.replace(re, data[key]);
}
return templateString;
}
layouts/_default/search.html
{{ define "main" }}
<style>
mark{
background: orange;
}
</style>
<div role="main" class="container">
<div class="row">
<div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
<section class="resume-section p-3 p-lg-5 d-flex flex-column">
<div class="my-auto" >
🔎 <input id="search-query" placeholder="Typescript" name="s"/>
<div id="search-results">
<h3>Matching pages</h3>
</div>
</div>
</section>
<!-- this template is sucked in by search.js and appended to the search-results div above. So editing here will adjust style -->
<script id="search-result-template" type="text/x-js-template">
<article id="summary-${key}" class="post-preview">
<a href="${link}">
<h2 class="post-title">${title}</h2>
</a>
<p class="post-entry">
${snippet}
<a href="${link}" class="post-read-more">[{{ i18n "readMore" }}]</a>
</p>
</article>
</script>
</div>
</div>
</div>
{{ end }}
他のファイルは特に変化ない。fuse.jsやjqueryなどのjsは管理しにくかったので別ファイルに移した
所感
- 生のjs書くの久々、綺麗にする気があまり起こらなかった。個人のブログやし動くだけでええやろ。
- jQueryでロジック組むの大変だなぁと改めて思った