命名って難しい

変数、関数、クラスなどなど実装より命名に毎回悩むタイプの人間による技術や趣味についてのメモ。

弊社現場レベルで使えるであろう簡単な社内FAQページを作った

経緯

新しい施策を社内で行うと問い合わせが増える。 その問い合わせがある程度蓄積してきたのでFAQを立てようと思い作りました。 あくまで弊社現場レベルなのでレベルは高くありません。

環境

  • Windows Server + IIS
  • 社内イントラのサーバーに間借りして設置
  • 社内イントラ関連ファイル(html/css/jsなど)のフォルダは共有されており、許可された者のみ編集可能
  • なんでもExcelで管理しているからメンテはExcelでできるとよい。
    • 弊社現場レベルで使えるのはExcelくらい

作ったもの

  1. FAQページ(Bootstrap4 + jQuery 3.3 + Vue.js)
  2. ExcelのFAQ台帳(CSVを吐くマクロ付き)
  3. PowerShellスクリプト(CSVjson形式に変換)

まずはFAQページを作り、Vue.jsでうまいことデータを表示し、
メンテに気を使いつつ、Vue.jsにわたすjsonを作れるExcelマクロとPowerShellを組みました。

全体像

こんな感じのExcelにFAQを追加して更新すると。。。 f:id:NotShown:20180820221646p:plain

FAQページがこうなります。 f:id:NotShown:20180820222047p:plain

こんな環境を作りました。

ExcelのFAQ台帳

  • FAQの源流
  • 保存前タイミングでCSVを出力、そのCSVjsonに変換するPowerShell起動
    • マクロボタンとかを用意するとメンテする人が押し忘れる可能性があるので保存前
Private Sub Workbook_BeforeSave(ByVal SaveAsUI As Boolean, Cancel As Boolean)

    Dim sheet As Worksheet
    Dim name As String

    Dim csv As String: csv = ThisWorkbook.Path & "\qna.csv"
    Dim ps As String: ps = ThisWorkbook.Path & "\convertQnACSV2JSON.ps1"
    
    
    Set sheet = Worksheets("Q&A")
        
        ' ファイル名
        name = ThisWorkbook.Path & "\qna.csv"

        ' シートを別のワークブックにコピーする
        sheet.Copy

        ' コピーしたワークブックを上書き保存
        Application.DisplayAlerts = False
        ActiveWorkbook.SaveAs Filename:=name, FileFormat:=xlCSV
        Application.DisplayAlerts = True

        ' コピーしたワークブックを閉じる
        ActiveWorkbook.Close SaveChanges:=False

    strCommand = "Powershell -File """ & ps & """"
    Set WshShell = CreateObject("WScript.Shell")
    WshShell.Exec (strCommand)
End Sub

PowerShellスクリプト

Excelブックマクロから出力されたCSVjson形式に変換します。

FAQデータは以下の構造として捉えています(わかりにくい)

  • ジャンル別Q&Aのリスト
    • 単一Q&A
      • Question
      • Answer
      • 参考リンクのリスト
        • タイトル
        • URL
<#
 # Q&AコンテンツのCSVをJson形式に変換する
 
 CSVレイアウトは以下
 | genre | id | q | a | title1 | url1 | title2 | url2 | ~ | url5 |
 
 #> 
Push-Location -Path (Split-Path $MyInvocation.MyCommand.Path)

$csv = Import-Csv ".\qna.csv" -Encoding Default

# スコープはjson全体
$json = @{content=(New-Object System.Collections.ArrayList)}

# スコープはジャンル別のグループ
foreach($gen in ($csv | group "genre")){

    # スコープは各Q&A
    $jgen=@{genre=$gen.Name;qnas=(New-Object System.Collections.ArrayList)}
    foreach($qna in $gen.Group){

        # Q&A参考リンクは列でタイトルとURLを表現している。
        # 5つまでのタイトルとURLの組み合わせを配列に変換する。
        $tmpLinks = @(
            @{title=$qna.title1; url=$qna.url1},
            @{title=$qna.title2; url=$qna.url2},
            @{title=$qna.title3; url=$qna.url3},
            @{title=$qna.title4; url=$qna.url4},
            @{title=$qna.title5; url=$qna.url5}
            )

        # リンクは自由入力欄のため、ある場合のみリストに加える
        $jqna = @{id=$qna.id;q=$qna.q;a=$qna.a;keywords=$qna.keywords;links=(New-Object System.Collections.ArrayList)}
        foreach($link in $tmpLinks){
            if($link.url -ne ""){
                # タイトルなしの場合URLにする
                if($link.title -eq ""){
                    $link.title = $link.url
                }
                $jqna.links.Add($link)
            }
        }
        $jgen.qnas.Add($jqna)
    }
    $json.content.Add($jgen)
}

# 出力する
Set-Content qna.json ( ConvertTo-Json $json -Depth 6 ) -Encoding UTF8

FAQページ

見た目は全体像の通り、FAQはアコーディオンで開きます。

なお、こちらはBootstrapのフリーのFAQテンプレートを流用して作っており、参考URLと検索欄の他はほぼそのまま。 www.prepbootstrap.com

このページに変換機能もあったので気まぐれでBootstrap4にアップデートしています。

テンプレからの変更点1:検索機能

検索欄に入力すると、FAQが絞れます。検索というよりは絞り込みですね。 既存の環境はいじれないのでバックエンドは考えずフロントでどうにかしています。

function search() {
    // 検索条件を取得、スペースで区切られた各条件で検索する。
    var input = document.querySelector("#search-criteria");
    var conditions = input.value.split(/ | /).filter(function(elem) {
        return elem != "";
    });

    // 正規表現の作成
    var tmpExp = "";
    conditions.forEach(function(c, i, a) {
        tmpExp += ("(?=.*" + c + ")");
    });
    var exp = "^" + tmpExp;
    var regexp = new RegExp(exp, "i");

    // Q&Aの各要素を取得。条件にヒットしないものを非表示にする。
    var divTmp = document.querySelectorAll(".qna")
    divs = Array.prototype.slice.call(divTmp, 0)
    divs.forEach(function(div, index, ar) {
        div.style.display = regexp.test(div.innerHTML) ? "" : "none";
    });
}

テンプレからの変更点2:Vue.js

以下の理由からVue.jsに触れることにしました。

  • 最近流行っている技術に触れたい
  • メンテナンスを楽にしたい
    • jsonを更新するだけで反映される構造にしたい
  • とはいえシンプルなものでさっさと作りたい
    • Reactも気になっていたんですが、環境作るのが難しそうで今回は断念

Web開発の業務経験がないので、こんな理由で使っていいのかアレですが、いろいろ興味のあるものは触れてみると楽しいので。

活用したのは「FAQのjsonデータを元にDOMを作る」目的です(言葉の使い方が正しいか不安) 以前にも riotjsを使ったことがあったので、スムーズに取り入れることができました。

具体的には以下のようなコードを書きました。

<div class="container">

    <br />
    <div class="form-group">
        <label for="usr">検索</label>
        <input type="text" class="form-control" id="search-criteria" placeholder="検索" onkeyup="search()">
    </div>
    <div class="" id="accordion">
        <div id="qnaAll">
            <div v-for="qnaByGenre in content">
                <div class="faqHeader">{{ qnaByGenre.genre }}</div>
                <div class="card qna" v-for="qna in qnaByGenre.qnas">
                    <div class="card-header">
                        <h4 class=""><a class="accordion-toggle collapsed" data-toggle="collapse" data-parent="#accordion" v-bind:href="'#' + qna.id">{{ qna.q }}</a></h4>
                    </div>
                    <div v-bind:id="qna.id" class="panel-collapse collapse">
                        <div class="card-block">
                            <div class="answer">{{ qna.a }}</div>
                            <div style="display:none">検索用キーワード {{ qna.keywords }}</div>
                            <div class="links" v-if="qna.links.length > 0">
                                <hr />
                                <h5>参考リンク</h5>
                                <ul v-for="link in qna.links">
                                    <li><a v-bind:href="link.url" target="_blank"> {{ link.title }}</a></li>
                                </ul>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>