用Vue和Typesense建立一个位置感知的应用程序教程

386 阅读12分钟

位置感知的应用程序,可以简化定位附近的企业和服务,在现代网络开发中越来越普遍。例如,想想Uber和Airbnb等流行的应用程序。Uber通过实现地理定位功能,将用户的位置与他们数据库中所有司机的位置进行比较,从而能够将用户与最近的司机联系起来。

在这些类型的应用程序中实现地理定位功能时,大部分的工作在于编码位置比较逻辑,根据一些预先定义的标准返回一组结果。例如,你可以返回一个离用户当前位置最多 100 米的餐厅列表。

为了避免为这种位置比较编写自定义逻辑,我们可以使用Typesense。在本教程中,我们将亲身学习如何使用Typesense并使其抽象出位置比较任务。我们将建立一个简单的附近酒店网络应用,列出离用户位置最近的酒店。让我们开始吧!

什么是Typesense?

Typesense是一个搜索解决方案,它允许你将搜索功能整合到你的项目中,而不需要编写你自己的自定义搜索逻辑。你基本上是把你的项目中的这一部分外包了。

例如,当你把你的代码部署到Heroku时,你允许第三方来处理你的基础设施问题,而你则专注于编写代码。同样,当你在你的项目中使用Firebase认证时,你也是让一个外部代理来处理你的整个认证逻辑,而你则专注于其他事情。

Typesense使用了一个类似的模型。当你把Typesense添加到你的项目中时,你就允许Typesense在你的项目中实现搜索。尽管用户提交查询的搜索栏或过滤器图标会在你的应用程序中存在,但Typesense会处理查询并返回所需的结果。

当第三方代表你实现你的项目的某些组件时,它主要被称为X作为服务模式,其中X是被实现的组件。例如,Heroku将计算作为一种服务模式运行,因为它代表你实现了基础设施。Typesense将搜索作为一种服务模式,提供一个开箱即用的搜索解决方案,你可以将其插入你的项目中。

其他搜索服务产品包括Algolia、Elasticsearch、Amazon Elasticsearch、Azure Search等等。然而,Typesense提供的功能组合是无可比拟的。它是开源的、轻量级的、无情的、快速的,而且容易集成。

构建附近酒店的网络应用

前提条件

      • 具备JavaScript的基本知识
      • 有Vue的经验
      • 本地安装了Node.js
      • 安装Vue和Vue CLI
      • 任何你选择的IDE。我将使用VS Code

你可以在GitHub上找到本教程的完整源代码。

启动Typesense服务器

使用Typesense有两个步骤。首先,我们将创建一个集群,这基本上是Typesense上我们可以存储和访问数据的空间。然后,我们将用户要搜索的数据添加到第一步配置的集群中,在我们的例子中,是一个酒店的列表。

我们将创建一个集群,存储我们平台上的所有酒店的列表。除了持久化我们的数据外,我们的集群还将允许我们通过REST API端点来检索和更新我们的数据。

我们可以使用Typesense的官方Docker镜像或通过他们的云服务启动我们的集群。在本指南中,我们将使用他们的云服务,因为在我看来,这更容易操作。

前往Typesense云,用GitHub登录。点击启动按钮,配置一个新的集群。使用集群的默认设置,如下图所示。启动集群可能需要一些时间,所以你可以放松,等待系统做它的事情。

New Typesense Cluster Created

一旦配置完成,你应该看到屏幕上显示的内容如下图所示。点击顶部的生成API密钥按钮,这将下载一个文本文件,你需要把它放在安全的地方。

Generate API Keys Cluster Ready

我们以后会需要生成的API密钥。这样,我们就完成了Typesense服务器的启动。

设置我们的Vue开发环境

让我们来初始化一个新的Vue项目。启动你的系统的终端窗口,导航到你想创建Vue项目的任何目录。在你的终端中运行命令vue create nearby-hotels 。在该命令中,nearby-hotels 是我们项目的名称,但你可以把它改成任何你想要的东西。

通过上面的命令,你会被提示配置某些东西。继续,选择默认值,并将Vue Router添加到项目中。一个具有下图所示结构的项目将为你生成。

Vue Hotels Project Folder

在你项目的根目录下运行命令vue run serve ,启动Vue的开发服务器并进行测试。

用我们的酒店列表填充我们的Typesense服务器

现在我们有了Typesense集群和我们的项目在本地设置,我们可以把我们需要的数据添加到我们的集群。在搜索引擎方面,这个步骤被称为索引。我们将我们的索引任务分解成三个子任务。

      • 连接到我们的Typesense服务器
      • 准备要上传的数据,在我们的案例中,就是我们的酒店列表。
      • 上传准备好的数据

连接到我们的Typesense服务器

为了与我们的Typesense服务器通信,我们需要创建一个Typesense客户端实例,并使用我们在前一步下载的文本文件中的秘密密钥。通过运行npm i typesense 命令来安装typesense 包。然后,在你的项目根目录下创建一个名为typesense.js 的文件,并在其中添加以下内容。

import 'dotenv/config';
import { Client } from 'typesense';

// Create a new client
const client = new Client({
        nodes: [
          {
                        host: "your typesense host here. starts with xxx.a1.typesense.net",
                        port: 443,
                        protocol: "https",
          },
        ],
        apiKey: "your typesense admin key here",
        connectionTimeoutSeconds: 2,
});
export default client;

我们已经使用我们的Typesense凭证来初始化Typesense客户端。因为我们导出了客户端,所以我们应该能够在项目的任何文件中访问它。我们将使用这个客户端来连接我们的Typesense服务器;它拥有所有API操作的方法。

准备要上传的数据

在你项目的根目录下创建一个hotels.jsonl 文件,我们将在其中存储我们想要上传到Typesense服务器的酒店列表。把这个文件的内容复制到你新创建的文件中。不要编辑内容,只需按原样复制和粘贴。

上传准备好的数据

为了将准备好的数据上传到我们的Typesense服务器,我们将使用一个脚本。进入你的项目根目录,创建一个名为indexer.js 的新文件,并添加以下代码。

import * as fs from "fs";
import * as readline from "readline";

import client from "./typesense.js";

const hotelsSchema = {
  name: "hotels",
  fields: [
    { name: "hotel_name", type: "string" },
    { name: "address", type: "string" },
    { name: "coordinates", type: "geopoint" },
  ],
};

在Typesense中,一组存储的数据被称为一个集合。集合中的每个数据都被称为一个文档。在我们的案例中,我们将有一个酒店的集合,该集合中的每个酒店都是一个文档。请注意,一个集合中的所有文档都遵循相同的结构,有相同数量的字段和字段类型。为了执行这个约束,我们需要预先定义我们集合的结构。

我们将根据我们用client.collections().create(hotelsSchema 定义的结构来创建一个集合。接下来,我们从我们的hotels.json 文件中读取每一行,并根据该行的内容在我们的Typesense集合中创建一个文档,代码为client.collections('hotels').documents().create(hotelDocument);

client
  .collections()
  .create(hotelsSchema)
  .then(function () {
    readline
      .createInterface({
        input: fs.createReadStream("hotels.jsonl"),
        terminal: false,
      })
      .on("line", function (line) {
        let hotelDocument = JSON.parse(line);

        client.collections("hotels").documents().create(hotelDocument);
      });
  });

在你的项目根目录下运行命令node indexer.js ,以执行你的脚本。如果你的CLI中没有显示错误信息,你的脚本就已经成功执行了。为了确认,请登录你的Typesense仪表盘,点击导航栏上的cluster。你应该看到下面的屏幕。

Typesense Dashboard Collection Schema

注意Typesense报告说我们的酒店集合中有20个文件,这是很准确的。
就这样,我们完成了数据的索引。

构建界面

为了得到一个完整的工作界面,我们将创建三个Vue组件。

      • HomePage:我们的索引页
      • Hotel:用于将每个酒店渲染到DOM中
      • NearbyHotelsPage:显示离用户最近的十家酒店的列表。Hotel 组件将被导入并用于该组件中

HomePage 组件

首先到public/index.html ,在head 标签之间添加下面的内容。不要覆盖head 标签的内容。

<!-- Bootstrap CSS -->
<link
      crossorigin="anonymous"
      href="<https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css>"
      integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB"
      rel="stylesheet"
 />

 <link
        rel="stylesheet"
        href="<https://use.fontawesome.com/3b7fb1f2e6.css>"
 />

我们从各自的CDN导入Bootstrap 4和Font Awesome库。接下来,删除src/views 中的所有文件,在该目录下创建一个HomePage.vue 文件,内容如下。

<template>
  <div class="container-fluid">
    <div class="row text-center mt-5 mb-5 px-5">
      <div class="col-12 col-md-6 mr-md-auto ml-md-auto">
        <h1>Welcome to Our Nearby Hotels Application</h1>
      </div>
    </div>

    <div class="row text-left mt-5 px-5">
      <div class="col-12 col-md-6 mr-md-auto ml-md-auto">
        <button class="btn-block shadow text-center">
          <div class="text">
            <i class="fa fa-map-marker" aria-hidden="true"></i>
            view nearby hotels
          </div>
        </button>
      </div>
    </div>
  </div>
</template>

<style scoped>
button {
  background-color: #1da462;
  color: white !important;
  font-weight: bold;
  font-size: 1.5rem;
  color: white;
  border: none;
  border-radius: 1rem;
  cursor: pointer;
}
</style>

<script>
export default {
  name: "home-page",
};
</script>

用以下内容覆盖src/router/index.js 的内容。

import Vue from "vue";
import VueRouter from "vue-router";
import HomeView from "../views/HomePage.vue";

Vue.use(VueRouter);

const routes = [
  {
    path: "/",
    name: "home",
    component: HomeView,
  },
];

const router = new VueRouter({
  mode: "history",
  base: process.env.BASE_URL,
  routes,
});

export default router;

用下面的内容覆盖src/App.vue 的内容。

<template>
  <div id="app">
    <router-view />
  </div>
</template>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  color: #2c3e50;
}
</style>

现在,运行你的开发服务器,将你的浏览器指向localhost:8080 。你应该看到如下图所示的页面。

Dev Server Hotels Application

这就是我们对HomePage 组件的全部需求。接下来,我们将对我们的Hotel 组件进行工作。

Hotel 组件

首先,删除src/components 中的所有文件,在该目录下创建一个Hotel.vue 文件,内容如下。

<template>
  <div class="m-3">
    <div id="HotelImageBackground">
      <img
        src="../assets/hotel.jpg"
        class="img-fluid"
        :alt="hotel.name + ' image'"
      />
    </div>

    <div class="text-left mt-2">
      <div>
        <span id="hotelName">{{ hotel.hotel_name }}</span>
      </div>
      <div>
        <span id="address" class="mt-0">
          <i class="fa fa-map-marker" aria-hidden="true"></i>
          {{ hotel.address }}
        </span>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "hotel-single",

  props: {
    hotel: {
      type: Object,
      required: true,
    },
  },
};
</script>

<style scoped>
#address {
  font-weight: 400;
  font-size: 0.9rem;
  color: #bfc1c2;
}

#hotelName {
  font-size: 1.2rem;
  font-weight: bolder;
}

#hotelImageBackground {
  height: 300px;
  overflow: hidden;
  display: flex;
  justify-content: center;
  flex-flow: column;
  background-color: #dcdcdc;
}

@media only screen and (max-width: 640px) {
  #hotelName {
    font-size: 1rem;
    font-weight: bold;
  }
  #address {
    font-size: 0.8rem;
    font-weight: 400;
    color: #bfc1c2;
  }
}
</style>

在这里下载图片,并把它移到src/assets 。我们在上面的代码中使用该图片。我们将在即将创建的NearbyHotelsPage 组件中使用这个组件。

NearbyHotelsPage 组件

src/views 中创建一个名为NearbyHotelsPage.vue 的组件,内容如下。

<template>
  <div class="container">
    <div class="row text-center mt-5 mb-5 px-5">
      <div class="col-12 col-md-6 mr-md-auto ml-md-auto">
        <h1>Hotels Around You</h1>
        <small>
          <router-link to="/">Back to Home</router-link>
        </small>
      </div>
    </div>

    <div class="row" id="card-wrapper">
      <div
        class="col-md-3 col-12 border shadow-sm m-2"
        v-for="hotel in hotels"
        :key="hotel.document.id"
        id="card"
      >
        <Hotel :hotel="hotel.document" />
      </div>
    </div>
  </div>
</template>

<script>
import Hotel from "@/components/Hotel.vue";
import client from "../../typesense.js";

export default {
  name: "nearby-hotels",

  data() {
    return {
      hotels: [],

      // Lagos, Nigeria Coordinates
      userLatitude: 6.465422,
      userLongitude: 3.406448,

      // New York Coordinates
      // userLatitude: 40.71427,
      // userLongitude: -74.00597,
    };
  },

  components: {
    Hotel,
  },

  methods: {
    getHotels() {
      const searchParams = {
        q: "*",
        query_by: "hotel_name",
        filter_by: `coordinates:(${this.userLatitude}, ${this.userLongitude}, 1000 km)`,
        sort_by: `coordinates(${this.userLatitude}, ${this.userLongitude}):asc`,
      };

      client
        .collections("hotels")
        .documents()
        .search(searchParams)
        .then((results) => {
          console.log(`server response: ${results}`);
          this.hotels = results["hits"];
          console.log(`hotels: ${this.hotels}`);
        })
        .catch((error) => {
          console.log(error);
        });
    },
  },

  mounted() {
    this.getHotels();
  },
};
</script>

<style>
#card-wrapper {
  min-height: 50vh;
  display: flex;
  justify-content: center;
  align-items: center;
}
</style>

让我们把上面的组件中的一些东西弄明白。

data() {
    return {
      hotels: [],

      // Lagos, Nigeria Coordinates
      userLatitude: 6.465422,
      userLongitude: 3.406448,

      // New York Coordinates
      // userLatitude: 40.71427,
      // userLongitude: -74.00597,
    };
  }

在我们组件的data() 部分,我们已经初始化了三个变量:userLatitude,UserLongitude, 和hotels

请记住,我们的目标是获得与用户所在地一定距离内的酒店列表。userLatitudeuserLongitude 变量指定了用户的当前位置。

在生产场景中,我们不会对其进行硬编码。hotels 数组是我们将存储从Typesense检索到的酒店列表的地方。

const searchParams = {
        q: "*",
        query_by: "hotel_name",
        filter_by: `coordinates:(${this.userLatitude}, ${this.userLongitude}, 1000 km)`,
        sort_by: `coordinates(${this.userLatitude}, ${this.userLongitude}):asc`,
      };

我们在上面的getHotels() 方法中声明了searchParams 这个变量。我们将向Typesense传递searchParams ,这是我们告诉Typesense我们的查询条件的方式。

例如,SearchParams 中的filter_by: coordinates:(${this.userLatitude}, ${this.userLongitude}, 1000 km) 告诉Typesense抓取距离所传递的坐标1000公里以内的酒店,this.userLatitude and this.userLongitude

在过滤完成后,searchParams 中传递的sort_by: coordinates(${this.userLatitude}, ${this.userLongitude}):asc 告诉Typesense将过滤后的酒店列表按照它们与用户传递给Typesense的坐标的距离排序。

client
        .collections("hotels")
        .documents()
        .search(searchParams)
        .then((results) => {
          console.log(`server response: ${results}`);
          this.hotels = results["hits"];
          console.log(`hotels: ${this.hotels}`);
        })
        .catch((error) => {
          console.log(error);
        });
    },

在上面的代码片段中,我们对Typesense服务器进行了实际调用,并将结果存储在我们最初声明的hotels

NearbyHotelsPage 组件中的其他东西都是样式和HTML。然而,我们需要注意的一件事是在下面的代码中。

// Lagos, Nigeria Coordinates
userLatitude: 6.465422,
userLongitude: 3.406448,

// New York Coordinates
// userLatitude: 40.71427,
// userLongitude: -74.00597,

我们目前正在传递Lagos坐标作为用户的位置。如果你用npm run serve 启动你的开发服务器,并导航到附近的酒店页面,你应该看到一个尼日利亚的酒店列表,从拉各斯的酒店开始,如下图所示。

Hotels Example Nigeria

如果你选择使用纽约的坐标,注释掉第一组坐标,那么你应该看到美国的酒店,从纽约的酒店开始,如下图所示。

Hotels Example New York

现在,我们有了一个可以工作的附近酒店应用程序。耶!

真实世界应用的注意事项

在我们的基本应用中,有一些事情是我们不敢在一个真正被人们使用的应用中做的。

我们肯定会改变的主要事情是我们如何处理用户的坐标。它目前是硬编码的,但理想情况下,我们会有一些逻辑来自动检测和更新用户的当前坐标。

接下来,我们将有一个用户界面,而不是使用脚本来索引预定义的酒店列表,酒店业主或系统管理员可以通过这个用户界面向我们的Typesense服务器添加新酒店。你可以选择直接向Typsense发布新酒店,也可以先将数据发送到你的后台,然后在Typsense上创建一个数据副本。

最后,如前所述,我们需要将我们的秘密密钥保存在一个.env 文件中,例如。

总结

在本指南中,我们解释了Typesense是如何从根本上成为一个搜索即服务的解决方案,以及搜索即服务的含义。然后,我们通过建立一个基本的附近酒店网络应用来探索Typesense的地理定位功能。

目的是向你展示你如何利用Typesense的地理定位功能来建立你的定制的、位置感知的应用程序,而不需要设置你自己的后台组件来处理所有与地理空间有关的逻辑。