Meteor+MongoDB 實現簡單的即時搜尋

假裝我是程式猿發表於2017-11-09

使用 Meteor 和 MongoDB 實現一個簡單的即時搜尋服務。Meteor 是一個 Node.js 實現的快速開發平臺,可以快速開發 Web 和 APP。同時選用 MongoDB 資料庫來儲存資料,MongoDB 也是 Meteor 預設使用的資料庫。

目標

即時搜尋其實我們基本每天都在用,像百度、Google 都是這方面的典型例子,它最大的特點就是在使用者輸入關鍵字的同時返回搜尋結果給使用者,使用者體驗很棒。

知道了即時搜尋的初步概念,我們首先明確一下本次實驗的幾個實現目標:

  • 1.在使用者輸入關鍵字時進行即時搜尋
  • 2.對關鍵字進行條件匹配
  • 3.對關鍵字進行正則匹配

知道我們的實現目標之後,我們需要來整理一下整個即時搜尋的思路:

使用者輸入關鍵字 -->  通過監聽使用者輸入獲取關鍵字 --> 將關鍵字傳送到伺服器端 --> 伺服器端根據關鍵字進行查詢 --> 將查到的資料返回給客戶端 --> 將資料展示給使用者複製程式碼

以上基本上就是整個即時搜尋的實現思路,雖然步驟上可能看著有點多,但是這些都可以在很短的時間內完成。不過如果是自己單純地從零開始進行編寫程式碼,實現起來可能就有點繁瑣了。所以,這個時候 Meteor 就可以派上用場了。關於 Meteor 的更多資料和介紹,可以直接到官網檢視,Meteor 是一個用 Node.js 快速開發 Web App 的平臺。

資料說明和準備

我們這次做的是對一些軟體開發資料的簡單收集和統計。最基礎且最重要的資料是軟體專案的程式碼庫,其中包含了專案的所有程式碼檔案、程式碼的所有版本、程式碼提交者的想關資訊。我們會將這些資料儲存在 MongoDB 中,對於每一個資料我們儲存了軟體專案的以下資訊:

  • 名稱 (prj)
  • 所託管的網站 (repo)
  • 程式碼庫所在位置 (src_loc)
  • 提交日誌所在位置 (log_loc)
  • 提交人數 (n_peo)
  • 提交的版本數 (n_cmt)
  • 所使用的版本控制系統 (vcs)
  • 專案的起止時間 (btime/etime)
  • 時間跨度等資訊 (span)

以 Hadoop 為例, 它在 MongoDB 中的一條記錄大概是這樣的:

{ 
    "_id" : "1430277742.791925", 
    "prj" : "hadoop-common.git",   
    "repo" : "git.apache.org",    
    "src_loc" : "/path/to/datastore/git/git.apache.org_hadoop-common.git", 
    "log_loc" : "/path/to/datastore/git/git.apache.org/hadoop-common.git", 
    "n_peo" : 36, 
    "n_cmt" : 5825,
    "vcs" : "git"
    "b_time" : 200601, 
    "e_time" : 201005, 
    "span" : 52,
    "script" : "", 
}複製程式碼

這裡簡要說明一下,MongoDB 中,每一條記錄你可以簡單地看成一個 JSON,這得受益於 MongoDB 的 NoSQL(Not Only SQL) 特性。

準備資料:

獲取我們需要的資料:

Cd Meteor
Wget http://labfile.oss.aliyuncs.com/courses/386/Search.zip
unzip Search.zip
Cd Search複製程式碼

到這裡,你會得到一個 Search 資料夾,你可以直接將這個資料夾拖到 Brackets 中進行編寫程式碼,我們主要編寫的就是 SRSearch.js 和 SRSearch.html。不過在開始之前我們需要一些資料準備,所以先準備好資料再編寫程式碼:

啟動 MongoDB,在命令列執行:

sudo service mongodb start複製程式碼

啟動 MongoDB 之後,就可以插入相應的資料,在專案目錄之下 Search/ 命令列執行:

mongoimport -h localhost:27017 --db meteor --collection log_info --type json --file mongo.json --jsonArray複製程式碼

這裡需要注意的地方是 --collection log_info,我們在後面主要就是使用到 log_info 這個collection.

之後你大概會看到下面的資訊

connected to: localhost:27017
imported 10 objects複製程式碼

這裡我們便於小專案,直接使用十條資料,如果有更多的資料,也是可以跑起來的。資料準備完畢之後,我麼就可以開始編寫程式碼了。

操作步驟

首先,需要說明的是,這裡你完全不用擔心程式碼的編寫,因為程式碼不會超過120行。所以讓我們簡簡單單地就開始吧。為了方便起見,我們可以將客戶端、伺服器端的程式碼放在同一個 javascript 檔案中,也就是我們就只需要引入一個 js 檔案就可以了。如果你覺得客戶端和服務端的程式碼在一個檔案中有點奇怪,沒有關係,先繼續往下看一點點,Meteor 會幫你解決這個問題。

既然是做搜尋,其實就是搜尋我們存在資料庫裡面的內容,所以我們首先來獲取 Mongodb 中儲存後設資料的 collection:

Items = new Mongo.Collection("log_info");複製程式碼

沒錯,就是這樣簡單的一行程式碼,我們就可以獲取到log_info(就是上面插入資料時候制定的collection)這個collection裡面的所有資料記錄了。這裡需要說明的是,這個物件既可在客戶端程式碼中使用,也可在伺服器端程式碼使用(這就是對同構開發的簡單理解), Meteor 會基於 DDP 協議幫我們搞定資料在伺服器、客戶端之間的傳輸問題。獲取到這些資料 Items 之後,我們首先實現的是:在伺服器端根據使用者輸入的關鍵字對資料 Items 進行查詢

if (Meteor.isServer) {
    Meteor.publish('items', function (queryString) {
        return query(Items, queryString)
    })
}複製程式碼

程式碼說明:首先 if (Meteor.isServer) 就是限定這裡面的一段程式碼只在服務端執行,所以回頭看看剛剛叫你不用擔心的問題客戶端和服務端的程式碼在一個檔案...。然後,我們再看裡面的程式碼,我們使用 Meteor 的 publish (釋出)方法將我們的資料釋出出去,但是我們不能總是將所有的Items釋出給客戶端,我們需要根據使用者的輸入 (queryString) 進行篩選,所以最後在publish方法的回撥函式中,我們執行一個自定義的query方法對 Items 的所有資料進行查詢和篩選。

服務端程式碼寫好之後,我們就可以編寫客戶端的程式碼了:

if (Meteor.isClient) {
    Session.set('queryString', '')
    Meteor.subscribe('items', '')

    Template.body.events({
        "keyup #search-box": _.throttle(function(event) {
            Session.set('queryString', event.target.value)
            Meteor.subscribe('items', event.target.value)
        }, 50)
    })
}複製程式碼

這裡的程式碼需要說明的幾個點就是:

首先我們通過if (Meteor.isClient)來限定裡面的程式碼段就只在客戶端執行。緊接著Session.set('queryString', '')用Session宣告queryString為空,通過Meteor.subscribe('items', '')宣告items為空。就像簡單的宣告變數一樣。

Template.body 代表的就是HTML中的 標籤,在 Meteor 的HTML模版檔案中,它會自動繫結整個 標籤,並將它儲存在Template的body屬性當中。所以你可以簡單地理解為:Template.body.events就是可以對整個 進行時間監聽,不過我們通過"keyup #search-box"來指明更具體的事件和更具體的件套元素,這裡的前半段就是代表事件型別,後半段就是簡單的元素選擇器。對#search-box的keyup事件,我們使用 _.throttle 方法防治每一次的使用者輸入都馬上提交給服務端,我們將提交時間間隔設定為50毫秒。然後,Session.set('queryString', event.target.value)這一段就是將查詢字串直接儲存到Session當中。在這裡的event.target.value就是使用者輸入的關鍵字的值,你可以用console.log(event.target.value)來檢視一下。最後,也就是很關鍵的一步了,我們使用subscribe(訂閱)方法來訂閱伺服器釋出(publish)的資料,subscribe方法跟publish方法一起使用,你可能也會注意到,我們就是通過subscribe('items')和publish('items')中的items來指名釋出和訂閱的匹配。最後我們將使用者的輸入值(event.target.value)傳給服務端。

這時候,其實在客戶端與服務端的通道已將打通了。不過這時候我們還不能看到資料,因為我們並沒有使用模板將他們展示出來。所以,實現之,下面的程式碼需要寫在 if (Meteor.isClient) {} 裡面,Template 只能在客戶端執行:

Template.body.helpers({
    items: function () {
        return query(Items, Session.get('queryString'))
    }
});複製程式碼

這個可以簡單地理解為Template.body的一個輔助函式。這是就是將符合查詢條件的items返回給模板。這個items我們後面在寫模板的時候會用到的。

以上的程式碼都實現之後,我們其實就差一個query函式還沒實現了,它需要做的是以下幾件事:

1.根據使用者的輸入構造查詢資料庫的條件
2.對資料庫進行查詢
3.返回查詢結果

function query(collections, queryString){
    var limit = 40 //限制搜尋結果返回最多40個
    var query = queryString.split(' ') // split將使用者輸入的queryString分成一個個字母

    var andArray = [] // 儲存條件搜尋的陣列
    for (var i = query.length - 1; i >= 0; i--) {
        if (query[i] == '') {
            continue
        }

        var testSpecial  = isSpecial(query[i])
        // 通過isSpecial函式判斷是否存在 > = <
        // console.log(testSpecial); 可以檢視
        if (testSpecial != null) {
            andArray.push(testSpecial)
        } else {
            var regEx = new RegExp(query[i], 'ig')
            andArray.push({prj: regEx})
        }
    }

    if (andArray.length != 0){
        //如果andArray存在特殊字元 < = > ,將條件判斷也作為條件發起查詢
        return collections.find({$and: andArray}, {limit:limit})
    } else {
        // 如果不存在特殊字元 < = > 直接查詢
        return collections.find({},{limit:limit})
    }
}複製程式碼

query函式接受兩個引數,一個是collections,一個是queryString,我們在前面通過 query(Items, Session.get('queryString'))傳入了對應的引數。具體的詳解可以參考程式碼的註釋,接下來就是實現一下isSpecial函式了,就是判斷使用者輸入的關鍵字是否存在< = >等字元

function isSpecial(str){
    var relation
    var result = {}

    if (str.indexOf('<') != -1) {
        //是否存在 < 號
        relation = str.split('<')
        // 存在的話,將其作為分割符把字串分成兩個部分 如 n_cmt<20 就會分成n_cmt 和 20兩個部分
        result[relation[0]] = { $lt: Number(relation[1])}
        //這裡的relation[0]和relation[1]分別代表上例中的 n_cmt和 20
        return result
    } else if (str.indexOf('>') != -1){
        relation = str.split('>')
        result[relation[0]] = { $gt: Number(relation[1])}
        return result
    } else if (str.indexOf('=') != -1){
        relation = str.split('=')
        result[relation[0]] = relation[1]
        return result
    } else if (str.indexOf(':') != -1){
        relation = str.split(':')
        result[relation[0]] = new RegExp(relation[1], 'ig')
        return result
    } 
    return null
}複製程式碼

isSpecial主要是通過條件判斷語句來確認使用者輸入的關鍵字是否存在< = >等特殊字元,這些都是用於條件查詢,而最後的:判斷則是為了滿足正規表示式的查詢。詳細的程式碼思路可以看註釋部分,其他的觸類旁通。

到這裡,其實js程式碼基本上就可以說是寫完了。然後這個時候,我們需要回頭看一下,在上面的Template.body.helpers({})我們就提到過模板用來展示內容,但是直到現在,我們還是沒有模板檔案(HTML檔案)來展示內容,所以我們現在來寫一寫:

SRsearch.html的程式碼內容:

<head>
    <title>SRSearch</title>
    <link rel="stylesheet" href="bootstrap.min.css">
    <link rel="stylesheet" href="bootstrap-theme.min.css">
</head>

<body>
</body>複製程式碼

在上面的程式碼中,我們直接就去掉了標籤,因為Meteor會幫我們處理這件事。然後我們直接使用bootstrap,就是為了節省時間。

基本架構有了,我門需要一個表單來提交使用者輸入的內容:

<div class="container">
    <h1>Source Repo Search</h1>
    <form class="form-horizontal" onsubmit="return false;">
        <div class="form-group">
            <input type="text" class="form-control" id="search-box" 
        placeholder="Type project keyword to search">
        </div>
    </form>
    <div id='help'>
        <small>Possible options: repo | b_time | e_time | span | vcs | n_cmt | n_peo </small><br>
        <small>Possible operators:  &gt; | = | &lt; | : </small><br>
        <small>RegEx supported.</small>
    </div>
</div>複製程式碼

在上面的form中,我們首先指定onsubmit="return false;"不讓form執行預設提交。然後在form裡面,我們給使用者一個input輸入框,這裡需要注意的是,這個input的id必須和Template.body.events({"keyup #search-box"})中的選擇器一致,不然就觸發不了事件啦。而id為help的div裡面,我們的目的只是給使用者一些提示。表單寫完之後,我們需要將搜尋結果實時反饋出來:

<small>RegEx supported.</small>
<div>
    {{#each items}}
        {{> item}}
    {{/each}}
</div>複製程式碼

在上面的程式碼中,我們需要關注的是下面這個部分:

{{#each items}}
    {{> item}}
{{/each}}複製程式碼

還記得我們在Template.body.helpers({})指定的變數items麼,這裡的{{#each items}}就是迴圈輸出那個items裡面的每一個搜尋結果。然後{{> item}}指定輸出的模板,這裡的item為模板的名字,我們需要建立一個name="item"的模板:

<template name="item">
    <dl class="dl-horizontal">
    <dt>Project</dt>            <dd>{{prj}}</dd>
    <dt>Repository</dt>         <dd>{{repo}}</dd>
    <dt>Time Span</dt>          <dd>{{b_time}} - {{e_time}}</dd>
    <dt>Version Control</dt>    <dd>{{vcs}}</dd>
    <dt># Commit</dt>           <dd>{{n_cmt}}</dd>
    <dt># People</dt>           <dd>{{n_peo}}</dd>
    <dt>Source Location</dt>    <dd>{{src_loc}}</dd>
    <dt>Log Location</dt>       <dd>{{log_loc}}</dd>
    <hr>
    </dl>
</template>複製程式碼

我們使用
到這裡,全部的程式碼就編寫完成了。現在就可以在專案的根目錄執行meteor run,

=> Started proxy.
=> Started MongoDB.
=> Started your app.

=> App running at: http://localhost:3000/複製程式碼

之後瀏覽器訪問 http://localhost:3000/ 就可以見證奇蹟了。

覺得我分享的文章對你有幫助或者對內容有什麼異議,請聯絡微信公眾號:範小二

相關文章