uber-zap_如何构建自己的Uber-for-X应用程序(PART 2)

uber-zap

Featured in Mybridge’s Top Ten NodeJS articles from Jan-Feb 2017 and Top 50 NodeJS articles of the year (v.2018)

2017年1月2日 发布的Mybridge十大 NodeJS 文章年度最佳50篇NodeJS文章中精选(v.2018)



Update: Read the updated version of this article on my tech blog

更新:在我的技术博客上阅读本文的更新版本



Welcome to part 2 of this series Building your own Uber-for-X App. In part 1, you used an example of a citizen-cop app and learned how to fetch cops located near a given pair of latitude and longitude coordinates. In this part, you’ll continue building the same app and learn to implement these features:

欢迎阅读本系列的 2部分, 构建自己的X专用Uber应用 。 在第1部分中,您使用了一个公民警察应用程序的示例,并学习了如何获取位于给定纬度和经度对附近的警察。 在这一部分中,您将继续构建相同的应用程序并学习实现以下功能:

  • Exchanging data between cops and citizens in real time using web sockets

    使用网络套接字在警察与公民之间实时交换数据
  • Using maps to show location details of the citizen and the cop

    使用地图显示市民和警察的位置详细信息
  • Visualizing crime data

    可视化犯罪数据

Be sure to read part 1 thoroughly and try out the examples before proceeding with the rest of this tutorial.

在继续本教程的其余部分之前,请务必通读第1部分并尝试使用示例。

项目设置和文件夹组织 (Project Set-up and folder organization)

Let’s analyze the project files that we currently have, from the previous part:

让我们从上一部分开始分析当前拥有的项目文件:

  • app.js contains your server set-up and database configs. Every time you need to start the server you’ll use this file by typing node app.js in your terminal.

    app.js包含您的服务器设置和数据库配置。 每次需要启动服务器时,都需要在终端中键入node app.js来使用此文件。

  • routes.js — you’ll use this file to write end-points and handlers

    routes.js —您将使用此文件来编写端点和处理程序

  • db-operations — where you’ll write database operations

    db-operations —您将在其中编写数据库操作

  • views will contain your HTML pages

    视图将包含您HTML页面

  • public will contain sub-folders for storing JavaScripts, stylesheets and images

    public将包含用于存储JavaScript,样式表和图像的子文件夹

If you’ve used Uber before, you’re aware that there’s the driver-facing app, and a rider facing app. Let’s try implementing the same — citizen.html will show the citizen facing side of the app and cop.html will show the cop facing app. You’ll save these files inside the views folder. Open citizen.html in your text editor and add this:

如果您以前使用过Uber,那么您会知道有一个面向驾驶员的应用程序和一个面向驾驶员的应用程序。 让我们尝试实现相同的- citizen.html将显示应用程序的公民的侧面与cop.html将展示面向应用的警察。 您将这些文件保存在views文件夹中。 在文本编辑器中打开citizen.html ,并添加以下内容:

<!DOCTYPE html>
<html lang = "en">
<head>
    <meta charset="utf-8"/>
    <title>Citizen <%= userId %> </title>
</head>
<body data-userId="<%= userId %>">
    <h1>Hello Citizen <%= userId %></h1>
    <h4 id="notification"> 
        <!-- Some info will be displayed here-->
    </h4>
    <div id="map"> 
        <!-- We will load a map here later-->
    </div>
    <!--Load JavaScripts -->
</body>
</html>

Repeat this step for cop.html as well, but replace the word Citizen with Cop.

同样,对cop.html重复此步骤,但用Cop代替单词Citizen

The data-userId is an attribute that begins with the prefix data-, which you can use to store some information as strings, that doesn’t necessarily need to have a visual representation. <%= userId %> would appear to be a strange looking syntax, but don’t worry —y our template engine understands that anything that’s between <%= and %> is a variable, and it will substitute the variable userId for actual value on the server side before the page is served. You’ll understand this better as you progress.

data-userId是一个以前缀data-开头的属性,您可以使用该属性将某些信息存储为字符串,而不必具有直观的表示形式。 <%= userId %> 看起来似乎是一种奇怪的语法,但是不用担心-我们的模板引擎了解<%=%>之间的任何内容都是变量,它将在服务器端用变量userId代替实际值,然后页面已投放。 随着您的进步,您会更好地理解这一点。

If you recall in the earlier part, you had these lines in app.js :

如果您回想起前面的部分,则app.js中包含以下

app.set('views', 'views'); 
app.use(express.static('./public'));
app.set('view engine','html');
app.engine('html',consolidate.underscore);

The first line tells your app to look for HTML files inside the views folder whenever it gets a request for a particular page. The second line sets the folder from which static assets like stylesheets and JavaScripts will be served when a page loads on the browser. The next two lines tell our application to use the underscore template engine to parse our html files.

第一行告诉您的应用程序每当收到对特定页面的请求时,便在views文件夹中查找HTML文件。 第二行设置文件夹,当在浏览器上加载页面时,将从该文件夹提供静态资源,例如样式表和JavaScript。 接下来的两行告诉我们的应用程序使用下划线模板引擎来解析我们的html文件。

Now that the directory structure is set-up and the views are ready, it’s time to start implementing features! Before continuing, it’ll be helpful to keep the following points in mind:

现在目录结构已经设置好并且视图已经准备好了,是时候开始实现功能了! 在继续之前,请记住以下几点会有所帮助:

  • Write JS code inside the script tag in the HTML document. You may choose to write it inside a .js file, in which case you should save the JS file(s) inside /public/js folder and load it in your page. Make sure that you load the libraries and other dependencies first!

    在HTML文档的script标签内编写JS代码。 您可以选择将其写入.js文件中,在这种情况下,应将JS文件保存在/ public / js文件夹中,并将其加载到页面中。 确保首先加载库和其他依赖项!

  • It’ll be helpful if you keep the developer console open in your browser to check for error messages in case something doesn’t seem to be working. Keep a watch on the terminal output too.

    如果在浏览器中保持打开开发人员控制台的状态,以检查是否有错误消息,这将很有帮助。 还要注意终端输出。
  • The words event and signal will be used interchangeably in this tutorial — both mean the same thing.

    事件信号这两个词在本教程中将互换使用-两者含义相同。

Let’s start hacking!

让我们开始骇客吧!

服务公民和警察页面 (Serving Citizen and Cop Pages)

Let’s render the citizen page on going to http://localhost:8000/citizen.html, and the cop page on going to http://localhost:8000/cop.html. To do this, open app.js and add these lines inside the callback function of mongoClient.connect:

让我们在转到http:// localhost:8000 / citizen.html时呈现公民页面 并在cop页面上转到http:// localhost:8000 / cop.html 。 为此,请打开app.js并将这些行添加到mongoClient.connect的回调函数中:

app.get('/citizen.html', function(req, res){
    res.render('citizen.html',{
        userId: req.query.userId
    });
});

app.get('/cop.html', function(req, res){
    res.render('cop.html', {
        userId: req.query.userId
    });
});

Save your files, re-start your server and load the citizen and cop pages. You should see Hello Citizen on the page. If you pass userId as query parameters in the URL, for example — http://localhost:8000/citizen.html?userId=YOURNAME then you’ll see Hello Citizen YOURNAME. That’s because your template engine substituted the variable userId with the value that you passed from the query parameters, and served the page back.

保存文件,重新启动服务器并加载“公民”页面和“警察”页面 您应该在页面上看到Hello Citizen 。 如果在URL中将userId作为查询参数传递,例如-http:// localhost:8000 / citizen.html?userId = YOURNAME 然后您将看到Hello Citizen YOURNAME 。 这是因为您的模板引擎将变量userId替换为您从查询参数传递的值,并返回了页面。

为什么需要Web套接字,它们如何工作? (Why do you need web sockets, and how do they work?)

Event or signal based communication has always been an intuitive way to pass messages ever since historic times. The earliest techniques were quite rudimentary — like using fire signals for various purposes, mostly to warn of danger to people.

自历史以来,基于事件或信号的通信一直是传递消息的直观方式。 最早的技术还很初级,例如将火警信号用于各种目的,主要是警告人们危险。

Over the centuries, newer and better forms of communication have emerged. The advent of computers and the internet sparked something really innovative — and with the development of the OSI model, socket programming and the smart-phone revolution, one-on-one communication has become quite sophisticated. The basic principles remain the same, but now much more interesting than setting something on fire and throwing it.

几个世纪以来,出现了更新更好的沟通形式。 计算机和互联网的出现激发了一些真正的创新-随着OSI模型,套接字编程和智能电话革命的发展,一对一通信变得相当复杂。 基本原理保持不变,但是现在比起放火扔东西要有趣得多。

Using Sockets, you can send and receive information via events, or in other words signals. There can be different types of such signals, and if the parties involved know what kind of signal to ‘listen’ to, then there can be an exchange of information.

使用套接字,您可以通过事件(信号)发送和接收信息。 此类信号可能有不同类型,如果相关各方知道要“监听”哪种信号,则可以进行信息交换。

但是为什么不简单地使用HTTP请求呢? (But why not simply use HTTP requests?)

I read a very nice article on the difference between HTTP requests and web-sockets. It’s a short one, so you can read it to understand the concept of web-sockets better.

我读了一篇非常不错的文章,介绍HTTP请求和Web套接字之间区别 。 这是一个简短的文章,因此您可以阅读以更好地了解网络套接字的概念。

But briefly put, traditional HTTP requests like GET and POST initiate a new connection request and later close the connection after the server sends back the response. If you were to attempt building a real time app using HTTP, the client would have to initiate requests at regular intervals to check for new information (which may or may not be available). This is because of the fact that the server itself is unable to push information on its own.

但简单地说,传统的HTTP请求(例如GET和POST)会发起一个新的连接请求,然后在服务器发回响应后关闭连接。 如果要尝试使用HTTP构建实时应用程序,则客户端必须定期发起请求以检查新信息(可能可用或可能不可用)。 这是因为服务器本身无法自行推送信息。

And this is highly inefficient — the client would waste resources in constantly interrupting the server and saying “Hi, I’m XYZ - let’s shake hands. Do you have something new for me?”, and the server will be like — “Hi (shaking hands). No I don’t. Good-bye!” over and over again, which means even the server is wasting resources!

这是非常低效的-客户端会浪费资源,不断中断服务器并说:“ 嗨,我是XYZ-让我们握手吧。 你有什么新鲜的东西给我吗? ”,服务器将像“嗨(握手)。 不,我不知道。 再见!” 一遍又一遍,这意味着甚至服务器也在浪费资源!

Web-sockets however, create a persistent connection between a client and the server. So this way the client need not keep asking the server, the server can push information when it needs to. This method is much more efficient for building real time applications.

但是,Web套接字在客户端和服务器之间创建了持久连接。 因此,客户端不必继续询问服务器,服务器可以在需要时推送信息。 这种方法对于构建实时应用程序更为有效。

Web-sockets have support in all major browsers, but for few browsers that don’t — there are other fallback options/techniques to rely on, like Long Polling. These fallback techniques and the Web Sockets APIs are bundled together within Socket.IO, so you wouldn’t have to worry about browser compatibility. Here is an excellent answer on Stack Overflow that compares lots of those options.

Web套接字在所有主要浏览器中均受支持,但对于少数没有的浏览器,则需要依赖其他后备选项/技术,例如Long Polling。 这些后备技术和Web套接字API在Socket.IO中捆绑在一起,因此您不必担心浏览器的兼容性。 这是堆栈溢出的一个很好的答案 ,它比较了很多这些选项。

集成Socket.IO (Integrating Socket.IO)

Let’s start by integrating Socket.io with the express server and also load socket.io’s client-side library in the html pages. You’ll also use jQuery — it isn’t needed for socket.io to work, but your app will need it for making AJAX requests and tons of other stuff. So go ahead, write this in both the pages:

让我们首先将Socket.io与快速服务器集成在一起,然后在html页面中加载socket.io的客户端库。 您还将使用jQuery-不需要socket.io即可正常工作,但您的应用将需要它来发出AJAX请求和大量其他内容。 因此,请继续在两个页面中编写以下代码:

<!-- Load socket.io client library -->
<script src="/socket.io/socket.io.js"></script>

<!-- Load JQuery from a CDN -->
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>

<!-- load libraries before your JS code
Write rest of your JS code here -->

<script type="text/javascript">
    var socket = io();
    
    //Fetch userId from the data-atribute of the body tag
    var userId = document.body.getAttribute("data-userId");
    
    /*Fire a 'join' event and send your userId to the server, to join a room - room-name will be the userId itself!
*/ 
    socket.emit('join', {userId: userId});
    
//Declare variables, this will be used later
    var requestDetails = {};
    var copDetails = {};
    var map, marker;
    
</script>

The first script tag loads Socket.IO’s client library (once we serve the page using socket.io server), which exposes a global io object. Your app will make use of this object to emit events/signals to the server and listen to events from the server.

第一个脚本标记加载Socket.IO的客户端库(一旦我们使用socket.io服务器为页面提供服务),该客户端库将公开全局io对象。 您的应用程序将利用此对象向服务器发出事件/信号,并监听来自服务器的事件。

Now you have to change app.js to use socket.io:

现在,您必须将app.js更改为使用socket.io:

var http = require("http");
var express = require("express");
var consolidate = require("consolidate"); //1
var _ = require("underscore");
var bodyParser = require('body-parser');

var routes = require('./routes'); //File that contains our endpoints
var mongoClient = require("mongodb").MongoClient;

var app = express();
app.use(bodyParser.urlencoded({
    extended: true,
}));

app.use(bodyParser.json({
    limit: '5mb'
}));

app.set('views', 'views'); //Set the folder-name from where you serve the html page. 
app.use(express.static('./public')); //setting the folder name (public) where all the static files like css, js, images etc are made available

app.set('view engine', 'html');
app.engine('html', consolidate.underscore); //Use underscore to parse templates when we do res.render

var server = http.Server(app);
var portNumber = 8000; //for locahost:8000

var io = require('socket.io')(server); //Creating a new socket.io instance by passing the HTTP server object

server.listen(portNumber, function() { //Runs the server on port 8000
    console.log('Server listening at port ' + portNumber);

    var url = 'mongodb://localhost:27017/myUberApp'; //Db name
    mongoClient.connect(url, function(err, db) { //a connection with the mongodb is established here.
        console.log("Connected to Database");

        app.get('/citizen.html', function(req, res) { //a request to /citizen.html will render our citizen.html page
            //Substitute the variable userId in citizen.html with the userId value extracted from query params of the request.
            res.render('citizen.html', {
                userId: req.query.userId
            });
        });

        app.get('/cop.html', function(req, res) {
            res.render('cop.html', {
                userId: req.query.userId
            });
        });

        io.on('connection', function(socket) { //Listen on the 'connection' event for incoming sockets
            console.log('A user just connected');

            socket.on('join', function(data) { //Listen to any join event from connected users
                socket.join(data.userId); //User joins a unique room/channel that's named after the userId 
                console.log("User joined room: " + data.userId);
            });

            routes.initialize(app, db, socket, io); //Pass socket and io objects that we could use at different parts of our app
        });
    });
});

/* 1. Not all the template engines work uniformly with express, hence this library in js, (consolidate), is used to make the template engines work uniformly. Altough it doesn't have any 
modules of its own and any template engine to be used should be seprately installed!*/

Make sure to change the initialize function in routes.js to accept four parameters instead of two, like this — function initialize(app, db, socket, io).

确保更改初始化 routes.js中的函数可以接受四个参数,而不是两个,就像这样— 函数initialize(app,db, socketio )

If you restart the server and refresh your pages, you’ll see the message A user just connected in your terminal. The server will also create a new room once it receives a join event from the connected clients, so you’ll see another message printed — User joined room. Try it with http://localhost:8000/cop.html?userId=02, you should get a similar output.

如果重新启动服务器并刷新页面,则会看到消息“用户刚连接到终端”。 一旦服务器接收到来自连接的客户端的加入事件,服务器还将创建一个新房间,因此您将看到另一条消息- 用户加入房间。 尝试使用http:// localhost:8000 / cop.html?userId = 02 ,您应该会得到类似的输出。

Perfect — now that you have integrated socket.io, you can begin building the rest of your application.

完美-既然您已经集成了socket.io,就可以开始构建其余的应用程序。

公民警察沟通: (Citizen-cop communication:)

The entire process can be broadly divided into two sets of features:

整个过程可以大致分为两组功能:

  1. Requesting for help and notifying nearby cops

    寻求帮助并通知附近的警察
  2. Accepting the request and notifying the citizen

    接受请求并通知市民

Let’s try to understand how to implement each of these features in detail.

让我们尝试了解如何详细实现每个功能。

寻求帮助并通知附近的警察: (Requesting for help and notifying nearby cops:)
  • First create an end-point /cops/info inside routes.js, that will call a function to fetch a cop’s profile info, and return the results in the form of JSON to the client —

    首先,在routes.js中创建一个端点/ cops / info 端点将调用一个函数以获取警察的个人资料,并将结果以JSON的形式返回给客户端-

// GET request to '/cops/info?userId=02'
app.get('/cops/info', function(req, res){
    var userId = req.query.userId //extract userId from query params
    dbOperations.fetchCopDetails(db, userId, function(results){
        res.json({
            copDetails: results //return results to client
        });
    });
});
  • Next, you’ll write the function fetchCopDetails in db-operations.js, that accepts an instance of db, the cop’s userId and a callback function. This function will use MongoDB’s findOne query to fetch a cop’s info with a given userId from the database, and then return the result to the callback:

    接下来,您将在db-operations.js中编写函数fetchCopDetails ,该函数接受db的实例 cop的userId和回调函数。 此功能将使用MongoDB的findOne 查询以从数据库中获取具有给定userId的警察信息,然后将结果返回给回调:

function fetchCopDetails(db, userId, callback) {
    db.collection("policeData").findOne({
        userId: userId
    }, function(err, results) {
        if (err) {
            console.log(err);
        } else {
            callback({
                copId: results.userId,
                displayName: results.displayName,
                phone: results.phone,
                location: results.location
            });
        }
    });
}
exports.fetchCopDetails = fetchCopDetails;
  • Inside cop.html :

    内部cop.html

Now that you’ve created the endpoint, you can call it using JQuery’s AJAX function to fetch the cop’s profile info and display it inside an empty div id=”copDetails”. You’ll also configure the cop page to begin listening to any help requests:

现在,您已经创建了端点,可以使用JQuery的AJAX函数调用该端点,以获取cop的配置文件信息并将其显示在一个空的div id =“ copDetails”中 。 您还将配置“警察”页面以开始收听所有帮助请求:

//First send a GET request using JQuery AJAX and get the cop's details and save it
$.ajax({
    url: "/cops/info?userId="+userId,
    type: "GET",
    dataType: "json",
    success: function(data){ //Once response is successful
        copDetails = data.copDetails; //Save the cop details
        copDetails.location = {
            address: copDetails.location.address,
            longitude: copDetails.location.coordinates[0],
            latitude: copDetails.location.coordinates[1] 
        };
        document.getElementById("copDetails").innerHTML = JSON.stringify(data.copDetails);
    },
    error: function(httpRequest, status, error){
        console.log(error);
    }
});

//Listen for a "request-for-help" event
socket.on("request-for-help", function(eventData){
    //Once request is received, do this:
    
    //Save request details
    requestDetails = eventData; //Contains info of citizen
    
    //display the data received from the event
    document.getElementById("notification").innerHTML = "Someone's being attacked by a wildling! \n" + JSON.stringify(requestDetails);
});

If you restart the server and go to http://localhost:8000/cop.html?userId=02, (passing userId of a saved cop in the query params) you’ll find the cop’s info displayed on the page. Your cop page has also begun to listen to any request-for-help events.

如果重新启动服务器并转到http:// localhost:8000 / cop.html?userId = 02 (在查询参数中传递已保存警察的用户ID ),则会在页面上找到警察的信息。 您的警察页面也已开始收听所有请求帮助事件。

内部citizen.html (Inside citizen.html)

The next step is to create a button for the citizen that can be clicked in case of emergency. Once clicked, it will fire a request-for-help signal and the signal can carry back information of the citizen back to the server:

下一步是为市民创建一个按钮,在紧急情况下可以单击该按钮。 一旦单击,它将发出一个请求帮助信号,该信号可以将市民的信息带回服务器:

<button onclick="requestForHelp()">
    Request for help
</button>

Write the handler for generating the event in the script tag:

script标签中编写用于生成事件的处理程序:

//Citizen's info
requestDetails = {
    citizenId: userId,
    location: {
        address: "Indiranagar, Bengaluru, Karnataka 560038, India",
        latitude: 12.9718915,
        longitude: 77.64115449999997
    }
}

//When button is clicked, fire request-for-help and send citizen's userId and location
function requestForHelp(){
    socket.emit("request-for-help", requestDetails);
}
  • Finally, the server needs to handle this event, as shown in the illustration. Go to db-operations.js and create a new function that can be used to save the request details in a new table requestsData:

    最后,服务器需要处理此事件,如图所示。 转到db-operations.js并创建一个新函数,该函数可用于将请求详细信息保存在新表requestData中

//Saves details like citizen’s location, time
function saveRequest(db, issueId, requestTime, location, citizenId, status, callback){

    db.collection('requestsData').insert({
        "_id": issueId,
        "requestTime": requestTime,
        "location": location,
        "citizenId": citizenId,
        "status": status
    }, function(err, results){
           if(err) {
               console.log(err);
           }else{
               callback(results);
           }
    });
}
exports.saveRequest = saveRequest;

The status field will tell whether a cop has responded to the request or not. Finally, in routes.js, add this inside the initialize function:

状态字段将告诉警察是否已响应请求。 最后,在routes.js中,将其添加到initialize函数中:

//Listen to a 'request-for-help' event from connected citizens
socket.on('request-for-help', function(eventData) {
    /*
        eventData contains userId and location
        1. First save the request details inside a table requestsData
        2. AFTER saving, fetch nearby cops from citizen’s location
        3. Fire a request-for-help event to each of the cop’s room
    */

    var requestTime = new Date(); //Time of the request

    var ObjectID = require('mongodb').ObjectID;
    var requestId = new ObjectID; //Generate unique ID for the request

    //1. First save the request details inside a table requestsData.
    //Convert latitude and longitude to [longitude, latitude]
    var location = {
        coordinates: [
            eventData.location.longitude,
            eventData.location.latitude
        ],
        address: eventData.location.address
    };
    dbOperations.saveRequest(db, requestId, requestTime, location, eventData.citizenId, 'waiting', function(results) {

        //2. AFTER saving, fetch nearby cops from citizen’s location
        dbOperations.fetchNearestCops(db, location.coordinates, function(results) {
            eventData.requestId = requestId;
            //3. After fetching nearest cops, fire a 'request-for-help' event to each of them
            for (var i = 0; i < results.length; i++) {
                io.sockets.in(results[i].userId).emit('request-for-help', eventData);
            }
        });
    });
});

That’s it, you’ve built the first set of features! Re-start the server and test this out by opening 4 tabs, one for a citizen and cop pages 01, 02 and 03.

就是这样,您已经构建了第一套功能! 重新启动服务器并测试了这一点,通过打开4个标签,一个是公民和警察页010203

Once you press the help button, you’ll notice that cop 01 does not get the request because that cop is far away from the citizen’s location. However cop 02 and cop 03 pages show the help request.

按下帮助按钮后,您会注意到cop 01没有收到请求,因为该cop离市民所在地很远。 但是, cop 02cop 03页面显示了帮助请求。

Awesome, you managed to send a request from a citizen and notify all nearby cops! Now, for the second set of features — this involves notifying the citizen once a cop accepts the request.

太好了,您设法向一个市民发送了一个请求,并通知了附近所有的警察! 现在,对于第二组功能-这涉及到警察接受请求后通知公民。

接受请求并通知市民 (Accepting the request and notifying the citizen)
内部cop.html (Inside cop.html)

The cop should be able to click a button to inform the citizen that the request has been accepted. When clicked, this button will fire a request-accepted event and also send back the cop’s info to the server:

警察应该能够单击一个按钮,以通知市民该请求已被接受。 单击后,此按钮将触发请求接受的事件,还将警察的信息发送回服务器:

<button onclick="helpCitizen()">
    Help Citizen
</button>

and the event handler will look like this:

事件处理程序将如下所示:

function helpCitizen(){
    //Fire a "request-accepted" event/signal and send relevant info back to server
    socket.emit("request-accepted", {
        requestDetails: requestDetails,
        copDetails: copDetails
    });
 }
内部citizen.html (Inside citizen.html)

The citizen page will start listening to any request-accepted events from the server. Once it receives the signal, you can display the cop info inside an empty div:

公民页面将开始侦听来自服务器的任何请求接受的事件。 收到信号后,您可以在一个空的div中显示警察信息:

//Listen for a "request-accepted" event
socket.on("request-accepted", function(eventData){
    copDetails = data; //Save cop details

   //Display Cop details
    document.getElementById("notification").innerHTML = "A cop is coming to your rescue! \n" + JSON.stringify(copDetails);
});

Now the server needs to handle the request-accepted event as shown in the illustration. First you’ll write a function in db-operations.js that will update the request in the database with the cop’s userId and change the status field from waiting to engaged:

现在,服务器需要处理请求接受的事件,如图所示。 首先,您将在db-operations.js中编写一个函数,该函数将使用cop的userId更新数据库中的请求,并将状态字段从waiting更改为engaged

function updateRequest(db, requestId, copId, status, callback) {
    db.collection('requestsData').update({
        "_id": requestId //Perform update for the given requestId
    }, {
        $set: {
            "status": status, //Update status to 'engaged'
            "copId": copId  //save cop's userId
        }
    }, function(err, results) {
        if (err) {
            console.log(err);
        } else {
            callback("Issue updated")
        }
    });
}
exports.updateRequest = updateRequest;

When the server listens to a request-accepted event, it’ll use the above function to save the request details and then emit a request-accepted event to the citizen. So go ahead, write this in your routes.js file:

当服务器侦听请求接受的事件时,它将使用上面的功能保存请求详细信息,然后向市民发出请求接受的事件。 因此,将其写入您的routes.js文件:

//Listen to a 'request-accepted' event from connected cops
socket.on('request-accepted', function(eventData){

    //Convert string to MongoDb's ObjectId data-type
    var ObjectID = require('mongodb').ObjectID;
    var requestId = new ObjectID(eventData.requestDetails.requestId);
    //For the request with requestId, update request details
    dbOperations.updateRequest(db, requestId, eventData.copDetails.copId, 'engaged’, function(results){
                               
       //Fire a 'request-accepted' event to the citizen and send cop details
    io.sockets.in(eventData.requestDetails.citizenId).emit('request-accepted', eventData.copDetails);
       });
 
 });

Great, you’ve built finished building the second set of features! Re-start your server, refresh your pages, and try it out!

太好了,您已经完成了第二套功能的构建! 重新启动服务器,刷新页面,然后尝试!

下一步是什么? (What’s next?)

By now it might have become obvious to you — the citizen page sends a hard-coded value of location every-time the button for help is clicked. Similarly the location info for all your sample cops have already been fed into the database earlier and are fixed values.

到现在为止,这对您可能已经变得显而易见了-每次单击帮助按钮时,“公民”页面都会发送一个硬编码的location值。 同样,所有样本警察的位置信息都已被更早地输入数据库,并且是固定值。

However in the real world, both the citizen and the cop don’t have a fixed location because they keep moving around — and therefore you’ll need a way to test this behavior out!

但是,在现实世界中,市民和警察都没有固定的位置,因为他们一直在走动-因此,您将需要一种方法来检验这种行为!

输入地图 (Enter Maps)

There are lot of mapping options out there. Google Maps API are very robust and feature rich. I personally love Mapbox too, it uses OpenStreetMap protocols under the hood, and here is the best part — it’s open source and hugely customizable! So let’s use that for building the rest of your app.

那里有很多映射选项。 Google Maps API非常强大且功能丰富。 我个人也喜欢Mapbox,它在幕后使用了OpenStreetMap协议,这是最好的部分-它是开源的,可进行高度自定义! 因此,让我们将其用于构建应用程序的其余部分。

使用Mapbox API (Using Mapbox API)
  • In order to begin using these APIs, you need to first create an account on MapBox and get the authentication key here.

    为了开始使用这些API,您需要首先在MapBox上创建一个帐户,然后在此处获取身份验证密钥

    Depending on your needs, Mapbox offers different

    根据您的需求,Mapbox提供不同的

    pricing plans to use these APIs in your apps — for now the free starter plan is sufficient.

    定价计划以在您的应用中使用这些API-目前免费的入门计划已足够。

  • Next, you’ll load mapbox.js library (current version 2.4.0) in both the pages using a script tag. It’s built on top of Leaflet (another JavaScript library).

    接下来,您将加载mapbox.js 两个页面中使用script标签的库(当前版本2.4.0)。 它基于Leaflet (另一个JavaScript库)构建。

<script src="https://api.mapbox.com/mapbox.js/v2.4.0/mapbox.js"></script>

You’ll also load the stylesheet used by mapbox.js inside the head tag of your HTML:

您还将在HTML的head标签内加载mapbox.js使用的样式表:

<link href="https://api.mapbox.com/mapbox.js/v2.4.0/mapbox.css" rel="stylesheet" />

Once you’ve done this, it’s time for you to start writing the logic —

完成此操作后,就该开始编写逻辑了-

  • First, load the map and set it to show some location as default

    首先,加载地图并将其设置为默认显示某些位置
  • Display a marker on the map

    在地图上显示标记
  • Use the autocomplete feature offered by Mapbox geocoder api. This allows you to input for a place and choose from the autocomplete suggestions.

    使用Mapbox Geocoder API提供的自动完成功能。 这使您可以输入地点并从自动完成建议中选择。

    After choosing the place, you can extract the place information and do whatever you want with it.

    选择地点后,您可以提取地点信息并对其进行任何操作。

Leaflet exposes all it’s APIs inside a global variable L. Since mapbox.js is built on top of Leaflet, the APIs that you’re gonna use will also be exposed in a global L variable.

Leaflet在全局变量L中公开其所有API 由于mapbox.js是在Leaflet之上构建的,因此您要使用的API也将在全局L变量中公开。

  • In citizen.html write this in your JavaScript

    citizen.html中 在JavaScript中编写

L.mapbox.accessToken = "YOUR_API_KEY";

//Load the map and give it a default style
map = L.mapbox.map("map", "mapbox.streets");

//set it to a given lat-lng and zoom level
map.setView([12.9718915, 77.64115449999997], 9);

//Display a default marker
marker = L.marker([12.9718915, 77.64115449999997]).addTo(map);

//This will display an input box
map.addControl(L.mapbox.geocoderControl("mapbox.places", {
    autocomplete: true, //will suggest for places as you type
}).on("select", function(data){
    //This function runs when a place is selected

    //data contains the geocoding results
    console.log(data);

    //Do something with the results
    //Extract address and coordinates from the results and save it
    requestDetails.location = {
        address: data.feature["place_name"],
        latitude: data.feature.center[1],
        longitude: data.feature.center[0]
    };

    //Set the marker to new location
    marker.setLatLng( [data.feature.center[1], data.feature.center[0]]);
}));

The above code extracts the place information once you select a place and updates the location details, so the next time you click the help button, you’ll send the new location along with your request.

上面的代码在选择位置后会提取位置信息并更新位置详细信息,因此,下次单击帮助按钮时,您将随请求一起发送新位置。

Once a cop accepts the request, you can show the location of the cop using a custom marker. First save this image inside /public/images, then write this code inside the event-handler of the request-accepted event:

警察接受请求后,您可以使用自定义标记显示警察的位置。 首先将此图像保存在/ public / images内 ,然后将此代码写入请求接受的事件的事件处理程序内:

//Show cop location on the map
L.marker([
    copDetails.location.latitude,
    copDetails.location.longitude
],{
    icon: L.icon({
        iconUrl: "/images/police.png", //image path
        iconSize: [60, 28] //in pixels
    })
}).addTo(map);

That’s it! Now let’s repeat the same for the cop page as well inside cop.html.

而已! 现在,在cop.html内对cop页面重复相同的操作

Your cop’s page fetches the cop’s location info from the server using AJAX, so all you need to do is set the map and the marker to point to it. Let’s write this code inside the success callback of your AJAX function:

您的警察页面会使用AJAX从服务器获取警察的位置信息,因此您要做的就是设置地图和指向它的标记。 让我们在AJAX函数的成功回调中编写以下代码:

L.mapbox.accessToken = "YOUR_API_KEY";

//Load the map and give it a default style
map = L.mapbox.map("map", "mapbox.streets");

//set it to a cop's lat-lng and zoom level
map.setView( [copDetails.location.latitude, copDetails.location.longitude ], 9);

//Display a default marker
marker = L.marker([copDetails.location.latitude, copDetails.location.longitude]).addTo(map);

//This will display an input box
map.addControl(L.mapbox.geocoderControl("mapbox.places", {
    autocomplete: true, //will suggest for places as you type
}).on("select", function(data){
    //This function runs when a place is selected
    
    //data contains the geocoding results
    console.log(data);
    
    //Do something with the results
    
    //Set the marker to new location
    marker.setLatLng([
        data.feature.center[1],
        data.feature.center[0]
    ]);
}));

Once a cop gets a request, you can use a custom marker to display the citizen’s location. Download the marker image and save it in /public/images. Next, let’s write the logic inside the event handler of your request-for-help event:

警察收到请求后,您可以使用自定义标记显示市民的位置。 下载标记图像并将其保存在/ public / images中。 接下来,让我们在请求帮助事件的事件处理程序中编写逻辑:

//Show citizen location on the map
L.marker([
    requestDetails.location.latitude,
    requestDetails.location.longitude
],{
    icon: L.icon({
       iconUrl: "/images/citizen.png",
       iconSize: [50,50]
    })
}).addTo(map);

Cool, let’s try this out — open cop pages 04, 05 and 06. In the citizen page, type “the forum bengaluru”, select the first result and watch the app in action when you ask for help!

酷,让我们尝试了这一点-打开网页警察040506 。 在“公民”页面中,键入“ forum bengaluru”,选择第一个结果,并在寻求帮助时观看正在运行的应用程序!

数据可视化 (Data Visualization)

A Picture is worth a thousand words
一张图片胜过千言万语

People love visualizing data. It helps you understand a certain topic better. For example in the metric system, I didn’t quite realize just how large a Gigameter really is, but I understood it better after I saw this picture:

人们喜欢可视化数据。 它可以帮助您更好地理解某个主题。 例如,在公制中,我并没有完全意识到千兆级表的真正大小,但是在看到以下图片后,我对它有了更好的了解:

Unlike computers, humans don’t understand numbers laid out on spreadsheets very easily — the larger the data-set, the harder it becomes for us to identify any meaningful patterns in it. Lot’s of meaningful information could go undetected, simply because the human brain is not trained to pour over large number of tables filled with text and numbers.

与计算机不同,人类不太容易理解电子表格上的数字-数据集越大,我们越难识别其中的任何有意义的模式。 许多有意义的信息可能不会被发现,这仅仅是因为人的大脑没有受过训练,无法倾倒大量装有文本和数字的桌子。

It’s much easier to process information and identify patterns if the data can be visualized. There are many ways to do that, in the form of graphs, charts etc. and there are several libraries that allows you to do those things in a screen.

如果数据可以可视化,则处理信息和识别模式要容易得多。 有很多方法可以做到这一点,例如以图形,图表等形式,并且有几个库可让您在屏幕上执行这些操作。

At this point, I’m assuming that you probably have played around with your app a little bit, and saved help requests in MongoDB. If not, you can download the data-set and then import it to your database by typing this in your terminal:

在这一点上,我假设您可能已经玩了一些应用程序,并将帮助请求保存在MongoDB中。 如果没有,您可以下载数据集,然后通过在终端中键入以下内容将其导入数据库:

mongoimport --db myUberApp --collection requestsData --drop --file ./path/to/jsonfile.json

As you already know, the saved requests contain useful information like the location details, the status field which indicates whether a citizen has received help or not, and so forth. Perfect for using this information to visualize crime data on a heat-map! Here’s an example from Mapbox.

如您所知,已保存的请求包含有用的信息,例如位置详细信息,指示公民是否已获得帮助的状态字段,等等。 非常适合使用此信息在热图上可视化犯罪数据! 这是Mapbox的示例

I’m gonna use MapBox GL JS — it’s a library that uses WebGL to help visualize data inside maps and make them very interactive. It’s extremely customizable — with features like colors, transitions and lighting. Feel free to try your own styles later!

我将使用MapBox GL JS-这是一个使用WebGL来帮助可视化地图中的数据并使它们具有高度交互性的库。 它是高度可定制的-具有颜色,过渡和照明等功能。 以后随时尝试自己的样式!

For the heat-map feature, the library accepts data-sets in the GeoJSON format, and then plots data-points on the map. GeoJSON is a format for encoding a variety of geographic data structures. Hence you need to convert your saved data to adhere to this format.

对于热图功能,该库接受GeoJSON格式的数据集,然后在地图上绘制数据点。 GeoJSON是一种用于编码各种地理数据结构的格式。 因此,您需要转换保存的数据以遵守此格式。

So, here are the following steps:

因此,以下是以下步骤:

  1. An endpoint to serve your visualization page data.html.

    提供可视化页面data.html的端点

  2. Next, have an endpoint — /requests/info that fetches your requests from MongoDB, converts them to the GeoJSON format and returns them to the client.

    接下来,有一个端点- / requests / info ,该端点从MongoDB中获取您的请求,将其转换为GeoJSON格式,然后将其返回给客户端。

  3. Create a page data.html that loads the visualization library and stylesheet.

    创建一个页面data.html来加载可视化库和样式表。

  4. Using AJAX, fetch the data-set from MongoDB and create a heatmap!

    使用AJAX,从MongoDB获取数据集并创建热图!
第1步: (Step 1:)

Open app.js, and write this code to serve the visualization page:

打开app.js,并编写以下代码以提供可视化页面:

app.get('/data.html', function(req, res) {
    res.render('data.html');
});
第2步: (Step 2:)

Let’s write a function in db-operations.js that fetches all results from your requestsData table:

让我们在db-operations.js中编写一个函数,该函数从您的requestsData表中获取所有结果:

function fetchRequests(db, callback) {
    var collection = db.collection('requestsData');
    //Using stream to process potentially huge records
    var stream = collection.find({}, {
        requestTime: true,
        status: true,
        location: true
    }).stream();
    
    var requestsData = [];
    
    stream.on('data', function(request) {
        requestsData.push(request);
    });
    
    //Runs after results are fetched
    stream.on('end', function() {
        callback(requestsData);
    });
}
exports.fetchRequests = fetchRequests;

In the above code, you query the requestsData table to return all documents. You can specify which fields to include and exclude from the results using boolean values — true to include the field and false to exclude the field. The results are then returned back to a callback function.

在上面的代码中,查询querysData表以返回所有文档。 您可以指定要包括和使用布尔值结果中排除哪些字段- 真正包含的字段和虚假排除领域。 然后将结果返回到回调函数。

How does GeoJSON look like?

GeoJSON的外观如何?

Information stored in GeoJSON has the following format:

GeoJSON中存储的信息具有以下格式:

{
    type: "FeatureCollection",
    features: [
        {
             type: "Feature",
             geometry: {
                 type: "Point",
                 coordinates: [<longitude>, <latitude>]
             },
             properties: {
                 <field1>: <value1>,
                 <field2>: <value2>,
                        ...
             }
        }
        ...
    ]
}

You’ll need to convert each object returned by your function into feature objects. The properties field can hold optional meta-data like status, requestTime, address etc. You’ll write the handle in routes.js that will call the function, convert it to GeoJSON and then return it back:

您需要将函数返回的每个对象转换为功能对象。 属性字段可以保存可选的元数据,例如状态,requestTime,地址等。您将在handle.js中编写句柄,该句柄将调用该函数,将其转换为GeoJSON,然后将其返回:

app.get('/requests/info', function(req, res){
    dbOperations.fetchRequests(db, function(results){
        var features = [];
        
        for(var i=0; i<results.length; i++){
            features.push({
                type: 'Feature',
                geometry: {
                    type: 'Point',
                    coordinates: results[i].location.coordinates
                },
                properties: {
                    status: results[i].status,
                    requestTime: results[i].requestTime,
                    address: results[i].location.address
                }
            });
        }
        var geoJsonData = {
            type: 'FeatureCollection',
            features: features
        }
        
        res.json(geoJsonData);
    });
});
第三步: (Step 3:)

Create a page data.html in your views folder, and load the stylesheet and library for the visualization:

在您的views文件夹中创建一个页面data.html ,并为可视化加载样式表和库:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <title>Visualize Data</title>
    <link href="https://api.tiles.mapbox.com/mapbox-gl-js/v0.26.0/mapbox-gl.css" rel="stylesheet" />
</head>

<body>

    <div id="map" style="width: 800px; height: 500px"> 
        <!--Load the map here -->
    </div>
    
    <!-- Load socket.io client library -->
    <script src="/socket.io/socket.io.js"></script>
    
    <!-- Load JQuery from a CDN -->
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
    
    <!-- Load Mapbox GL Library -->
    <script src="https://api.tiles.mapbox.com/mapbox-gl-js/v0.26.0/mapbox-gl.js"></script>
    
    <!-- load libraries before your JS code
    Write rest of your JS code here -->
    
    <script type="text/javascript">
        var socket = io();
        var map, marker;
        mapboxgl.accessToken = "YOUR_ACCESS_TOKEN";
    </script>
</body>
</html>

Now you’ll use AJAX to call your endpoint and fetch the GeoJSON data:

现在,您将使用AJAX调用端点并获取GeoJSON数据:

$.ajax({
    url: "/requests/info",
    type: "GET",
    dataType: "json",
    
    success: function(data) {
        console.log(data);
    }
    error: function(httpRequest, status, error) {
        console.log(error);
    }
});

Cool — save your code, re-start your server and point your browser to http://localhost:8000/data.html . You’ll see the results of your AJAX call in the console.

酷—保存代码,重新启动服务器,然后将浏览器指向http:// localhost:8000 / data.html 您将在控制台中看到AJAX调用的结果。

Now, let’s use it to generate a heat-map. Write this inside the success callback of your AJAX call:

现在,让我们用它来生成热图。 在AJAX调用的成功回调中编写以下代码:

var map = new mapboxgl.Map({
    container: "map",
    style: "mapbox://styles/mapbox/dark-v9",
    center: [77.64115449999997, 12.9718915],
    zoom: 10
});

map.on("load", function() {
    
    //Add a new source from our GeoJSON data
    map.addSource("help-requests", {
       type: "geojson",
       data: data
    });
    
//we can specify different color and styling formats by adding different layers
    
    map.addLayer({
        "id": "help-requests",
        "type": "circle",
        "source": "help-requests",
        "paint": {
        //Apply a different color to different status fields
            "circle-color": {
                property: "status",
                type: "categorical",
                stops: [
                    //For waiting, show in red
                    ["waiting", "rgba(255,0,0,0.5)"],
                    
                    //For engaged, show in green
                    ["engaged", "rgba(0,255,0,0.5)"]
                ]
            },
            "circle-radius": 20, //Radius of the circle
            "circle-blur": 1 //Amount of blur
        }
    });
});

Refresh your page to see a cool looking heatmap generated from your data-set!

刷新页面以查看从数据集生成的漂亮的热图!

结论 (Conclusion)

If you made it this far, congratulations! Hopefully this tutorial series gave you an insight on how to build a real time web application with ease — all you now need is the next big idea!

如果您做到了这一点,那么恭喜! 希望本系列教程能使您对如何轻松构建实时Web应用程序有一个深刻的了解-现在您所需要的只是下一个大创意!

I’m sure you’re aware that there are still plenty of places to improve upon in the app that you just built. You can try adding more features to it and make it more ‘intelligent’, for example:

我确定您知道,您刚刚构建的应用程序中仍有许多地方需要改进。 您可以尝试向其添加更多功能,使其更具“智能性”,例如:

  • Mimic a moving cop and a moving citizen that continuously send location updates to each other in real time, and update the marker icons on the map.

    模仿一个不断移动的警察和一个不断移动的市民,他们不断实时地彼此发送位置更新,并更新地图上的标记图标。
  • Set the status field to closed once the cop has helped the citizen out. Then, you can assign a different color to visualize closed issues on a heat-map. That way you’ll have an understanding of how efficient cops are in a given area.

    一旦警察帮助了市民,请将状态字段设置为关闭 。 然后,您可以分配其他颜色以在热图上可视化已关闭的问题。 这样,您将了解给定区域内警察的效率。

  • Build a rating system with which a citizen and a cop can rate each other. This way, neither citizen nor cop will misuse the system, and cops can get performance reports.

    建立一个评分系统,公民和警察可以互相评分。 这样,市民和警察都不会滥用该系统,警察可以获取绩效报告。
  • Have a cool looking user interface, like Material UI.

    拥有一个漂亮的用户界面,例如Material UI。
  • Lastly, have a sign-up and login mechanism!

    最后,有一个注册和登录机制!

Using a library like React or a framework like Angular might help you implement features in a robust and scalable manner. You could also experiment with charting libraries like D3.js to visualize information in the forma of bar-charts, pie-charts, line-charts etc.

使用React之类的库或Angular之类的框架可能会帮助您以健壮和可扩展的方式实现功能。 您还可以尝试使用D3.js之类的图表库来可视化条形图,饼形图,折线图等格式中的信息。

At some point you could deploy your app on a cloud hosting service provider — like Amazon Web Services or Google Cloud Platform, to show people what you made and have them test out features. It’ll be a nice way to get feedback and ideas, and who knows — your app might turn out to be a life saver some day!

有时,您可以在Amazon Web Services或Google Cloud Platform等云托管服务提供商上部署您的应用程序,以向人们展示您的所作所为,并让他们测试功能。 这将是获取反馈和想法的好方法,而且谁知道-您的应用可能有一天会拯救生命!

感谢您的阅读。 (Thank you for reading.)

Do recommend this if it helped you. In-case you have questions on any aspect of this tutorial series or need my help in understanding something, feel free to tweet or leave a comment here. I’d love to hear about your Uber-for-X ideas! You can read more such articles in my tech blog too.

如果对您有帮助,请推荐这个。 如果您对本教程系列的任何方面有疑问,或者需要我的帮助以了解某些内容,请随时在此处 发消息或发表评论。 我很想听听您关于Uber-for-X的想法! 您也可以在我的技术博客中阅读更多此类文章。

这就是您一直在等待的完整源代码(And here’s what you’ve been waiting for, the full source code!)

Liked what you read? You should subscribe. I won't waste your time.

喜欢您阅读的内容吗? 您应该订阅 。 我不会浪费你的时间。

Check out my Patreon page! Become a Patron!

查看我的Patreon页面! 成为赞助人!

翻译自: https://www.freecodecamp.org/news/how-to-build-your-own-uber-for-x-app-part-2-8ba6ffa2573d/

uber-zap

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值