一文詳解如何在Node中構建一個輕量級的位置分析報告服務API

2022-02-23 22:00:15
本篇文章給大家分享一個實戰,介紹一下在中構建一個位置分析報告API的方法,在本教學結束時,你也會對Node.js中的錯誤處理和良好的檔案結構有更好的理解,希望對大家有所幫助!

由經緯度定義的位置,可以與其他資料結合使用,為企業產生洞察力,這就是所謂的位置分析。

在全球範圍內經營的企業在整個價值鏈中使用位置分析,例如,用於定位使用者、提供服務和執行目標廣告。隨著社交媒體和移動裝置的興起,位置分析的使用在全球範圍內有所增加。

在本教學中,我們將學習如何在中構建一個輕量級的位置分析報告服務API。在本教學結束時,你將能夠為自己的專案構建這種型別的API。你也會對Node.js中的錯誤處理和良好的檔案結構有更好的理解

讓我們開始吧!

前提條件

要繼續學習本教學,你需要具備以下條件。

  • 熟悉Node.js、Express和Git
  • Visual Studio程式碼編輯器
  • Heroku賬戶
  • Postman賬戶

設定檔案結構

首先,我們需要設定我們的檔案結構。開啟你的終端,建立一個新的目錄,你將在其中儲存專案的所有檔案。在你的終端,鍵入以下命令,後面跟著資料夾的名稱,lars

mkdir lars

在VS程式碼編輯器中開啟lars 工作目錄。

  code .

你會看到你的VS Code視窗開啟。

1.gif

Visual Studio Code 視窗

通過在Visual Studio中開啟你的終端並執行npm init -y ,初始化工作目錄。

如果你想在VS Code之外的作業系統的終端中執行這個命令,請導航到lars 目錄並執行下面的命令。

npm init -y

上面的程式碼自動生成了package.json 檔案。

2.gif

VS Code顯示建立的package.json檔案

在本教學中,我們將使用Express作為一個依賴項。通過執行下面的命令來安裝Express。

npm install express --save

3.gif

將Express作為依賴關係安裝的命令

安裝完Express後,你會注意到一個node_modules 資料夾被建立了。為了確認你已經安裝了Express,請檢查你的package.json 檔案,你會看到Express作為一個依賴項被安裝。

4.gif

node_modules資料夾被建立,Express被新增到package.json中。

我們需要將Express匯入我們的應用程式,因為它是一個npm模組。在與你的package.json 檔案相同的目錄下建立一個名為app.js 的新檔案。

5.gif

VS程式碼視窗的截圖顯示app.js已經建立。

在你的app.js 檔案中,通過執行下面的程式碼requireExpress。

const express = require('express');

6.gif

匯入Express

現在,呼叫Express來建立你的應用、路由和你的應用要執行的埠。

const app = express();

Node.js實現了模組化,這意味著它將你的應用分成模組,或各種檔案,並匯出每個檔案。我們將使用export 關鍵字彙出app

module.exports = app;

7.gif

app.js檔案

接下來,在與app.js 檔案相同的目錄下建立另一個名為server.js 的檔案。Requireapp.js 檔案匯入server.js 檔案。

const app = require('./app');

在與server.js 相同的目錄中建立一個名為config.env 的檔案。config.env 檔案將包含我們的應用程式需要的所有 [process.env](https://nodejs.org/dist/latest-v8.x/docs/api/process.html)我們的應用程式需要的所有金鑰。在config.env 檔案中,建立一個PORT 變數,並將PORT 設定為監聽埠8000

PORT=8000

匯入應用程式後,在server.js 檔案中建立一個名為port 的常數。將其設定為你剛剛建立的PORT 變數和一個預設的埠3000

const port = process.env.PORT || 3000;

最後,我們將用.listen() 方法設定應用程式在該埠上監聽。

app.listen(port, () => {
    console.log(`App listening on ${port}`)
});

構建路由

每當你存取一個網頁或一個在網路上執行的應用程式時,你都在發出一個HTTP請求。伺服器用來自後臺或資料庫的資料進行響應,這就是所謂的HTTP響應。

當你在一個網路應用程式上建立一個資源時,你正在呼叫POST 請求。同樣地,如果你試圖刪除或更新一個Web應用上的資源,你正在呼叫DELETEPATCH 、或UPDATE 請求。讓我們建立路由來處理這些請求。

在你的工作目錄中建立一個名為routes 的資料夾,並在其中建立一個名為analyticsRoute.js 的檔案。RequireanalyticsRoute.js 檔案中表達,以設定API的路由。

      const express = require('express');

我們還需要從app.js 檔案中require 我們的應用程式模組。

        const app = require('../app');

然後,我們建立我們的路由。

        const router = express.Router();

最後,我們要匯出路由器。

        module.exports = router;

建立控制器

我們需要為控制器建立檔案,將其匯入我們的analyticsRoutes 檔案。首先,在你的工作目錄中建立一個名為controllers 的資料夾。

我們的API將使用使用者提供的IP地址和座標來計算距離和位置。我們的請求需要接受這些資訊和來自使用者的請求。

我們將使用一個POST 請求,因為使用者在req.body 。為了儲存這些資訊,我們需要在控制器中require 一個fs 模組(檔案系統)。

處理POST 的請求

controllers 資料夾中建立一個名為storeController.js 的檔案。在storeController.js 檔案中,我們需要匯入fs 模組和fsPromises.readFile() 方法來處理返回的promise ,也就是使用者的IP地址和座標。

要安裝fs 模組,在你的工作目錄中開啟你的終端,執行以下命令。

npm i fs --save

在你的檔案頂部輸入以下程式碼。

const fsp = require('fs').promises;
const fs = require('fs');

接下來,我們將建立一個控制器,處理我們的POST 請求的路由。我們將使用exports 關鍵字並建立一個接受三個引數的非同步中介軟體函數

  • req: 代表請求物件
  • res: 代表響應物件
  • next: 函數在中介軟體輸出後立即被呼叫。
postAnalytics = async(req, res, next) => {}

現在,我們將把req.body 中的資料物件的屬性儲存到reportAnalytics 陣列中。我們將設定一個Date() 物件,將任何資料的建立日期儲存在一個createdAt 關鍵中。

reportAnalytics.push({...req.body, createdAt: new Date()});

我們將建立一個名為storeAnalytics.json 的檔案,使用JSON.stringify() ,將我們的reportAnalytics 陣列的內容儲存為一個字串。

 await fsp.writeFile(`${__dirname}/storeAnalytics.json`, JSON.stringify(reportAnalytics));

當使用者提出POST 要求時,我們需要檢查storeAnalytics.json 檔案是否存在。如果該檔案存在,我們需要讀取該檔案並儲存輸出。

輸出包含一個名為reportFile 的常數,它儲存了被讀取的檔案內容。在reportFile ,使用JSON.parse ,將檔案的內容轉換為一個JavaScript物件。

// checks if file exists
if (fs.existsSync(`${__dirname}/storeAnalytics.json`)) {
// If the file exists, reads the file
  const reportFile = await fsp.readFile(`${__dirname}/storeAnalytics.json`, 'utf-8')
// converts the file to JavaScript Object
reportAnalytics = JSON.parse(reportFile)
} else {
  // if file does not exist
   return ('File does not exist');
}

[fs.existsSync()](https://www.geeksforgeeks.org/node-js-fs-existssync-method/)方法同步地檢查檔案是否存在。它接受${__dirname}/storeAnalytics.json 路徑作為其單一引數,並指向我們要檢查的檔案的位置。

我們將await 關鍵字與reportFile ,以等待用fsp.readFile() 方法讀取檔案的結果。接下來,我們用(${__dirname}/storeAnalytics.json 來指定我們要讀取的檔案的路徑。我們將編碼格式設定為utf-8 ,這將把從檔案中讀取的內容轉換為一個字串。

JSON.parse()reportFile 轉換為JavaScript物件,並將其儲存在reportAnalytics 陣列中。else 語句塊中的程式碼只有在檔案不存在時才會執行。最後,我們使用了return 語句,因為我們想在程式碼執行後停止函數的執行。

如果檔案被成功讀取、建立並儲存在storeAnalytics.json ,我們需要傳送一個響應。我們將使用響應物件(res) ,它是我們的非同步postAnalytics 函數的第二個引數。

    res.status(201).json({
        status: 'success',
        data: {
            message: 'IP and Coordinates successfully taken'
        }
    })

我們將用一個狀態success 和資料資訊IP and Coordinates successfully taken 來響應。

你的storeController.js 檔案應該看起來像下面的螢幕截圖。

8.gif

處理GET 的請求

我們需要建立另一個控制器檔案來處理我們的GET 請求。當使用者向API發出GET 請求時,我們將根據他們的IP地址和座標來計算他們的位置。

controllers 資料夾中建立一個名為fetchController.js 的檔案。fsstoreController.js 檔案中,我們需要require 模組和fsPromises.readFile() 方法來處理返回的promise

const fsp = require('fs').promises;
const fs = require('fs');

讓我們建立控制器來處理我們對GET 請求的路由。我們將使用類似的中介軟體函數和引數來處理上面的POST 請求。

   exports.getAnalytics = async(req, res, next) => {}

getAnalytics 中介軟體中,輸入以下程式碼,從請求的查詢中獲得IP地址。

     const { ip } = req.query;

現在,建立一個空陣列,用來儲存req.body 的內容。

     let reportAnalytics = [];

正如我們之前所做的,我們需要檢查storeAnalytics.json 檔案是否存在。如果檔案存在,我們將在reportFile 上使用JSON.parse ,將檔案內容轉換為一個JavaScript物件。

if (fs.existsSync(`${__dirname}/storeAnalytics.json`)) {
        const reportFile = await fsp.readFile(`${__dirname}/storeAnalytics.json`, 'utf-8')
        reportAnalytics = JSON.parse(reportFile)
    } else {
        return ('File does not exist');
    }

現在,我們可以在storeAnalytics.json 檔案中儲存使用者的IP地址和座標。任何時候使用者請求根據提供的座標計算地理位置,IP地址將以查詢的形式包含在請求中。

現在我們已經從req.query 物件中得到了IP地址,我們可以編寫程式碼來檢查req.query 物件中提供的IP地址是否與儲存在storeAnalytics.json 檔案中的IP地址相同。

   for (let i=0; i<reportAnalytics.length; i++) {
        if (reportAnalytics[i].ip !== ip) {
           return ('No Coordinates found with that IP');
        };
    }

在上面的程式碼中,我們使用forloop 來回圈瀏覽reportAnalytics 陣列。我們將變數i ,代表當前元素在reportAnalytics 陣列中的索引,初始化為0 。如果i小於reportAnalytics 陣列的長度,我們將其遞增。

接下來,我們檢查reportAnalytics 陣列的IP地址屬性是否等於req.query 中提供的IP地址。

讓我們計算一下只在最後一小時記憶體儲的IP地址的位置。

    const hourAgo = new Date();
    hourAgo.setHours(hourAgo.getHours()-1);
    const getReport = reportAnalytics.filter(el => 
        el.ip === ip && new Date(el.createdAt) > hourAgo
    )

在上面的程式碼塊中,我們建立了一個名為hourAgo 的常數,並將其設定為一個Date 物件。我們使用setHours() 方法將hourAgo 設定為最後一個小時的getHours()-1

reportAnalytics 檔案中的當前IP地址等同於或等於req.query 中傳遞的IP地址時,意味著資料是在最後一小時內建立的,getReport 建立一個常數,設定為一個新的陣列。

建立一個名為coordinatesArray 的常數,它將只儲存已經儲存在getReport 陣列中的座標。

const coordinatesArray = getReport.map(element => element.coordinates)

接下來,我們需要用座標計算出位置。我們需要遍歷coordinatesArray ,通過傳入儲存為座標的兩個值來計算位置。

    let totalLength = 0;
    for (let i=0; i<coordinatesArray.length; i++) {
        if (i == coordinatesArray.length - 1) {
            break;
        }
        let distance = calculateDistance(coordinatesArray[i], coordina         tesArray[i+1]);
        totalLength += distance;
    }

在上面的程式碼中,totalLength 代表從兩個座標計算出來的總距離。為了遍歷coordinatesArray ,我們需要初始化我們的計算結果。將totalLength 設定為零,初始化總距離。

第二行包含我們使用的迭代程式碼forloop 。我們用let i=0 來初始化i 變數。i 變數代表當前元素在coordinatesArray 的索引。

i<coordinatesArray.length 設定迭代的條件,只有噹噹前元素的索引小於coordinatesArray 的長度時才執行。接下來,我們在迭代中增加當前元素的索引,以移動到下一個元素,i++

接下來,我們將檢查當前元素的索引是否等於陣列中最後一個元素的編號。然後,我們暫停迭代程式碼的執行,用break 關鍵字移動到下一個。

最後,我們建立一個名為calculateDistance 的函數,接受兩個引數,即第一和第二座標值(經度和緯度)。我們將在另一個模組中建立calculateDistance ,並將其匯出到fetchController.js 檔案中,然後我們將最終結果儲存在我們初始化的totalLength 變數中。

注意,每個請求都需要一個響應。我們將用一個200statusCode 和一個包含我們將計算的距離值的JSON來響應。只有在程式碼成功的情況下才會顯示響應。

     res.status(200).json({distance: totalLength})

你的fetchController.js 檔案應該看起來像下面兩個程式碼塊。

9.gif

fetchController.js檔案

10.gif

fetchController.js檔案的續篇

建立calculateDistance 函數

在你的工作目錄中,建立一個名為utilities 的新資料夾,在裡面建立一個名為calculateDistance.js 的檔案。開啟calculateDistance.js 檔案,新增以下函數。

const calculateDistance = (coordinate1, coordinate2) => {
    const distance = Math.sqrt(Math.pow(Number(coordinate1.x) - Number(coordinate2.x), 2) + Math.pow(Number(coordinate1.y) - Number(coordinate2.y), 2));
    return distance;
} 
module.exports = calculateDistance;

在第一行,我們建立一個名為calculateDistance 的函數,它接受兩個引數:coordinate1coordinate2 。它使用下面的方程式。

  • Math.sqrt: 數學中的平方根
  • Math.pow :將一個數位提高到一個冪值
  • Number(): 將一個值轉換為一個數位
  • coordinate1.x :第一個座標(經度)的第二個值
  • coordinate2.x :第一個座標的第一個值(經度)。
  • coordinate1.y :第二個座標的第二個值(緯度)。
  • coordinate2.y :第二個座標的第一個值(緯度)。

現在我們已經建立了calculateDistance 函數,我們需要將該函數require 到我們fetchController.js 檔案的程式碼中。在fs 模組之後新增下面的程式碼。

const calculateDistance = require('../utilities/calculateDistance');

實現錯誤處理

實現錯誤處理是很重要的,以防止我們的程式碼失敗或某個特定的實現沒有按照設計的方式工作。我們將在開發和生產中新增錯誤處理。

開啟你的config.env 檔案,執行NODE_ENV=development ,將環境設定為開發。

在你的controllers 資料夾中,建立一個名為errorController.js 的新檔案。下面的程式碼片斷建立了一個名為sendErrorDev 的函數,以處理在開發環境中遇到的錯誤。

const sendErrorDev = (err, res) => {
    res.status(err.statusCode).json({
        status: err.status,
        error: err,
        message: err.message,
        stack: err.stack,
    });
}

我們將建立一個名為sendErrorDev 的函數,它接受兩個引數,err 表示錯誤,res 表示響應。response.status 接收錯誤的statusCode ,並以JSON資料進行響應。

此外,我們將建立一個名為sendErrorProd 的函數,它將處理API在生產環境中遇到的錯誤。

const sendErrorProd = (err, res) => {
    if(err.isOperational) {
        res.status(err.statusCode).json({
            status: err.status,
            message: err.message
        });    
    } else {
        console.error('Error', err);
        res.status(500).json({
            status: 'error',
            message: 'Something went wrong'
        })
    }
}

在你的utilities 資料夾中,建立一個名為appError.js 的檔案,並輸入以下程式碼。

class AppError extends Error {
    constructor(message, statusCode) {
        super(message);
        this.statusCode = statusCode;
        this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
        this.isOperational = true;
        Error.captureStackTrace(this, this.constructor);
    }
}
module.exports = AppError;

我們將建立一個名為AppError 的類,它擴充套件了Error 物件。

然後,我們將建立一個建構函式,它將初始化該類的物件。它接受兩個引數,叫做messagestatusCodesuper 方法用一個引數呼叫建構函式,將其傳入message ,並獲得對建構函式的屬性和方法的存取。

接下來,我們將建構函式的statusCode 屬性設定為statusCode 。我們將建構函式的status 屬性設定為任何以4 開始的statusCode ,例如,將404 statusCode 設定為failerror

建立另一個名為catchAsync.js 的檔案,並在其中新增以下程式碼。

module.exports = fn => {
    return (req, res, next) => {
        fn(req, res, next).catch(next);
    }
}

在控制器檔案中新增錯誤處理

Require appError.js 檔案和catchAsync.js 檔案在你的storeController.jsfetchController.js 檔案中。將這兩條匯入語句放在兩個檔案中的程式碼頂部。

const catchAsync = require('../utilities/catchAsync');
const AppError = require('../utilities/appError');

storeController.jsfetchController.js 檔案中,用catchAsync() 方法包裝你的函數,如下所示。

// For storeController.js file
exports.postAnalytics = catchAsync(async(req, res, next) => {...} 

// For fetchController.js file
exports.getAnalytics = catchAsync(async(req, res, next) => {...}

接下來,在你的fetchController.js 檔案中,執行AppError 類。

   for (let i=0; i<reportAnalytics.length; i++) {
        if (reportAnalytics[i].ip !== ip) {
           return next(new AppError('No Coordinates found with that IP', 404));
        };
    }

接下來,在你的storeController.js 檔案中執行AppError 類。

   if (fs.existsSync(`${__dirname}/storeAnalytics.json`)) {
        const reportFile = await fsp.readFile(`${__dirname}/storeAnalytics.json`, 'utf-8')
        reportAnalytics = JSON.parse(reportFile)
    } else {
        return next(new AppError('File does not exist', 404));
    }

你的storeController.jsfetchController.js 檔案中的程式碼應該看起來像下面的截圖。

11.gif

storeController.js檔案的螢幕截圖

12.gif

fetchController.js檔案第1-32行

13.gif

fetchController.js檔案第33-37行

設定驗證

我們需要驗證在req.body ,其中包括IP地址和座標的資料,是正確的,而且格式正確。座標應該至少有兩個值,代表經度和緯度。

utilities 資料夾中,建立一個名為Validation 的新資料夾。在Validation 資料夾中,建立一個名為schema.js 的檔案。schema.js 檔案將包含req.body 中提供的任何資料的所需格式。我們將使用 [joi](https://www.npmjs.com/package/joi)驗證器。

npm install joi

schema.js 檔案中輸入以下程式碼。

const Joi = require('joi');
const schema = Joi.object().keys({
    ip: Joi.string().ip().required(),
    coordinates: Joi.object({
        x: Joi.number().required(),
        y: Joi.number().required()
    }).required()
})
module.exports = schema;

joi 在上面的程式碼塊中,我們require 驗證器,用它來建立我們的模式。然後,我們將IP地址設定為總是一個字串,並通過在請求體中要求它來驗證IP地址。

我們將座標設定為object 。我們將代表經度和緯度值的xy 值都設定為數位,並將其require ,以便我們的程式碼執行。最後,我們匯出了模式。

在驗證器資料夾中,建立另一個名為validateIP.js 的檔案。在裡面,我們將編寫程式碼來驗證IP地址,使用 [is-ip](https://www.npmjs.com/package/is-ip)npm包。讓我們把這個包匯出到我們的程式碼中。

validateIP.js 檔案中,新增以下程式碼。

const isIp = require('is-ip');
const fsp = require('fs').promises;
const fs = require('fs');
exports.validateIP = (req, res, next) => {
    if(isIp(req.query.ip) !== true) {
        return res.status(404).json({
            status: 'fail',
            data: {
                message: 'Invalid IP, not found.'
            }
        })
    }
    next();
}

執行以下命令,為我們的API安裝必要的依賴項。

npm install body-parser cors dotenv express fs is-ip joi morgan ndb nodemon

你的app.js 檔案應該看起來像下面的螢幕截圖。

14.gif

app.js檔案

在你的package.json 檔案中的scripts 部分下,新增以下程式碼片段。

"start:dev": "node server.js",
    "debug": "ndb server.js"

你的package.json 檔案應該看起來像下面的截圖。

15.gif

package.json檔案

用以下程式碼更新你的analyticsRoute.js 檔案。

const express = require('express');
const app = require('../app');
const router = express.Router();
const validateIP = require('../utilities/Validation/validateIP');
const storeController = require('../controllers/storeController');
const fetchController = require('../controllers/fetchController');
router.route('/analytics').post(storeController.postAnalytics).get(validateIP.validateIP, fetchController.getAnalytics);
module.exports = router;

現在,我們已經完成了我們的位置分析API的構建!現在,讓我們測試一下我們的程式碼,以確保它的工作。

測試API

我們將使用Postman來測試我們的API。讓我們啟動我們的API以確保它在我們的終端中執行。

node server.js

你會在你的終端看到以下輸出。

16.gif

終端

我們的API託管在Heroku上,它的最終輸出應該看起來像下面的輸出。

17.gif

你可以自己在託管的檔案中測試這個API

https://documenter.getpostman.com/view/13856921/TzXumeXS

結論

位置分析是企業的一個偉大工具。位置資訊可以讓公司更好地服務於潛在客戶和現有客戶。

在本教學中,我們學會了建立一個工具,以IP地址和座標的形式獲取位置資訊並計算出距離。我們在Node.js中設定了我們的檔案結構,建立了處理GETPOST 請求的路由,新增了錯誤處理,最後測試了我們的應用程式。

你可以使用本教學中所學到的資訊來建立你自己的位置報告API,你可以根據自己的業務需求進行客製化。

The postBuild a location analytics reporting API in Node.jsappeared first onLogRocket Blog.

更多node相關知識,請存取:!

以上就是一文詳解如何在Node中構建一個輕量級的位置分析報告服務API的詳細內容,更多請關注TW511.COM其它相關文章!