Nodejs寫一個web服務

前言:Nodejs很火,可是好像工做中不多見到大面積的招聘崗位。我也用了nodejs寫web服務不少年。分享一套我用了好久的代碼,我以爲挺好。有須要的朋友拿去看看,咱們不去糾結到底nodejs好很差,適不適合作Server,這些問題,吵翻天有何意義。能解決問題就行了。html

開發快也是一種優點,如今中小型創業公司,底子差,業務要求快。加上雲服務商的api加持,爲什麼咱們還要維護一個笨重的服務體系。當我須要AI服務的時候,我就接入AI服務商的接口就行了。我須要cdn服務的時候我接雲服務就行了,我須要支付的時候我調用支付服務API就行了。那麼一個小巧,快速的業務方案就會讓創業型企業快速鋪開市場,2-3個月就能夠快速上線。尤爲是在小程序盛行的今天,都不用笨重的APP了,雲開發也是不錯的選擇,可是企業總要有必定的數據和業務能力,否則也會被雲開發商掣肘。node

今天分享的就是一套代碼,會放到github上,github.com/vincent-li/… 你們下載還須要根據項目調整配置文件。不敢說是很是牛逼,我歷來都是土狗一個,看看各位若是沒有思路的朋友,是否是能夠拿去直接先把業務幹起來。廢話很少說。代碼走起。git

image.png

思路比較簡單待我一步一步拆解。我把nodejs定位就是web服務程序,不要跟我討論底層穩定性的問題,由於我利用雲服務商所有解決了。服務框架用的是koa,數據庫是mongodb,那麼咱們來說講一個web服務到底須要什麼。github

  • web服務最基本的是接收請求,查詢數據庫,返回結果給客戶端。此類功能在controllers下面完成。
  • 上傳文件,利用一個插件multer,www.npmjs.com/package/mul…
  • 靜態文件服務,我會提到,可是仍是不要寫在服務器上面,講過了,nodejs仍是能力有限。利用cdn接口。
  • 登陸驗證邏輯,利用session來作。有人說咋不用token,一個意思,只是放的位置不同。
  • log分析,利用守護進程pm2的log能力,具體後面講。

講了一堆估計沒作過的人看懵了,不急,看看代碼就明白了。入口index.js文件web

const Koa = require('koa')
const koaBodyParser = require('koa-bodyparser')
const koaViews = require('koa-views')
const koaStatic = require('koa-static')
const koaSession = require('koa-session')
const mongoStore = require('koa-session-mongo2')
const koaLogger = require('koa-logger')

const _config = require('../config')
const router = require('./controllers')

global._ = require('lodash')
global.model = require('./models')

const app = new Koa()

// 服務器運行log
app.use(koaLogger())

// 指定靜態目錄
app.use(koaStatic(_config.static))

// 指定views路徑
app.use(
    koaViews(_config.views, {
        map: {
            html: 'lodash',
        },
    })
)

// 解析報文的body
app.use(
    koaBodyParser({
        enableTypes: ['json', 'form', 'text'],
    })
)

// session初始化
app.use(
    koaSession(
        {
            key: 'makefuture_sess',
            store: new mongoStore(_config.sessionURL),
            signed: false,
            // cookie過時時間,由瀏覽器負責到時清除,單位毫秒
            maxAge: 5 * 24 * 60 * 60 * 1000,
        },
        app
    )
)

// 使用router
app.use(router.routes(), router.allowedMethods())

console.log('啓動端口:', _config.port)

app.listen(_config.port)

複製代碼

入口的js就是引入koa和一堆koa的插件,而後根據api裝配起來就OK了。算法

因爲項目都有各類環境,因此要根據啓動環境把不一樣的配置項加載進來。const _config = require('../config'),配置項中包括一些敏感的數據庫鏈接帳號和密碼,我都替換掉了。因此各位在開發的時候,不建議把production的配置上傳到gitlab,最好是運維持有並配置。sql

const port = Number.parseInt(process.env.PORT) || 6060
const mongoUri =
    'mongodb://abcd:************@0.0.0.0:3717/?authSource=admin'
const root = '/Users/liwenqiang/codespace/git.ichoice.cc/makefuture/make'
const host = 'http://localhost:6060'
module.exports = {
    host,
    port,
    cdn: 'https://cdn.izelas.run',
    hostName: 'makefuture.app',
    home: root,
    static: `${root}/static`,
    views: `${root}/src/views`,
    mongoUri,
    dbName: 'makefuture',
    // 存放session的庫和表配置
    sessionURL: {
        url: `${mongoUri}&poolSize=5&useUnifiedTopology=true&useNewUrlParser=true`,
        db: 'makefuture',
        collection: 'session',
        // 這裏設置的是數據庫session按期清除的時間,與cookie的過時時間應保持一致,
        // cookie由瀏覽器負責定時清除,須要注意的是索引一旦創建修改的時候須要刪除舊的索引。
        // 此處的時間是秒爲單位,cookie的maxAge是毫秒爲單位
        maxAge: 24 * 60 * 60,
    }
}
複製代碼

利用global把一些通用的庫加入到開發體系,就不用一遍一遍的引用了。這裏把lodash引入,我覺lodash的方法足夠咱們用了。還有加密算法crypto,其實作的久了,基於業務的全部開發都是各類成熟組件的裝配。解決方案都很成熟,因此我不認爲有難作的業務,至少難不在技術,而是產品形態。mongodb

爲啥把models做爲global全局,由於這塊是能夠獨立於koa初始化的,也沒有必要歸入整個koa的請求上下文,可是業務處理的時候又要頻繁使用,那麼幹脆初始化到global裏面,隨時取用。數據庫

const mongoose = require('mongoose')
const { mongoUri, dbName } = require('../../config')

mongoose.Promise = global.Promise

const conn = mongoose.createConnection(mongoUri, {
    dbName,
    useNewUrlParser: true,
    useUnifiedTopology: true,
})

const db = {}

const sysinfo = require('./_sysinfo')
const user = require('./_user')
const project = require('./_project')
const page = require('./_page')
const component = require('./_component')
const file = require('./_file')
const models = [sysinfo, user, project, page, component, file]

models.forEach((item) => {
    let newSchema = new mongoose.Schema(
        (typeof item.schema === 'function' && item.schema(mongoose.Schema)) ||
            item.schema,
        { collection: item.name }
    )
    db[item.name] = conn.model(item.name, newSchema)
})

module.exports = db
複製代碼

簡單的不要不要的,就是把config裏面配置的鏈接字符串,放到組件的方法裏面,mongoose.createConnection,mongoose是用的比較好的mongodb鏈接組件,每中數據庫都有相應的js幫助作操做。我比較喜歡mongodb是由於跟json結構完美契合,理解上比較一致,操做也基本符合js的操做規範。不用寫sql真的舒服。隨便找一個數據對象看下你們就明白了。npm

const model = {
    name: 'user',
    schema: {
        nick_name: String, // 暱稱
        phone: String, // 手機
        pass: String, // 密碼
        avatar_url: String, // 頭像
        sms_code: Number, // 短信登陸碼
        expire_at: Number, // 驗證碼過時時間
        create_at: Number, // 建立時間
    },
}

module.exports = model
複製代碼

name就是數據庫中對應的表名,也是調用的時候的名字。跟文件名能夠不一致。文件名我叫_user,name:user,調用的時候是model.user,很是方便。裏面的字段名稱和類型對應mongo的collection,也就是表。也能夠跟對象和function,具體操做能夠去看www.npmjs.com/package/mon… mongo的官方操做方法都支持,具體要看mongoose的版本和mongodb的版本,官方有的均可以用。建立完model就能夠在方法中使用 await model.* 的方式調用了。方法是異步的,前面要加await,不喜歡能夠直接調用同步方法。不建議,畢竟有語法支持,又不難理解。

回到index.js,後面的設置靜態目錄,設置模板,就比較簡單,模板就是js的入口頁面,爲啥把js的入口放到服務,而不是作成靜態放cdn,nodejs就是作靜態渲染的,目的就是能夠很好的把數據注入頁面,因此js入口本身作方便不少。session這塊要講一下,這個圖省事,你們都session概念不瞭解的麻煩自行閱讀相關資料。這裏的session模塊幹了幾件事。

  • 在koa ctx上下文中注入一個session對象,ctx.session = {},能夠把登陸信息所有放進去。好比
bo = {
    userid: u._id.toString(),
    nick_name: u.nick_name,
    avatar_url: u.avatar_url,
  }
  ctx.session = bo
複製代碼
  • 自動把ctx.session中的數據存儲到指定的數據庫,這裏就用的mongodb,這樣作就不會由於服務宕機形成登陸信息丟失。
  • 根據配置,在session超時以後清除session信息。

因此咱們只要作一個全局的過濾器,每次檢測ctx.session中有沒有登陸的信息就行了。好比userid,userid爲空就直接redirect到login頁面就行了。

下面就剩一個就是處理請求了,這纔是web服務的重頭戲。咱們利用bodyparse插件,將全部請求過濾一遍,把請求參數整理成json對象。

// 解析報文的body
app.use(
    koaBodyParser({
        enableTypes: ['json', 'form', 'text'],
    })
)
複製代碼

而後就能夠在ctx.request.query或者ctx.request.body中取用。ctx.request.query表明的是get請求的參數。ctx.request.body存post請求參數。固然也能夠處理head,put,delete等請求。其實我以爲用不到。各位本身看。

/**
 * 路由定義
 */
const Router = require('@koa/router')
const router = Router({
  prefix: '/api/user',
})

/**
 * 用戶短信登陸,朝用戶手機中發送登陸短息
 * @path - /api/user/smscode
 * @method - POST
 * @params
 *  phone - 用戶手機號
 * @returns
 *  data - string | object 返回數據
 *  code - 0 || 500,
 *  success - true | false,
 *  message - ''
 */

router.post('/smscode', sendSmsCode)
複製代碼

以上就是一個路由定義的方法,prefix表示統一的前綴。post就是方法名,意思就是初始化了一個接口,訪問路徑就是/api/user/smscode,具體看代碼,對應的執行方法就是sendSmsCode,這就是一個向用戶端發送短信驗證碼的接口。固然短息我不用寫,直接用雲服務就好。我用阿里雲的短信接口,發的66的。

const sendSmsCode = async (ctx) => {
  const { phone } = ctx.request.body
  if (!phone) {
    ctx.body = getResponse(false, 'e501')
    return
  }
  if (!checkPhone(phone)) {
    ctx.body = getResponse(false, 'e552')
    return
  }
  let u = await model.user.findOne({ phone }).lean()
  // 是否已經發過
  if (u && u.sms_code && u.expire_at && u.expire_at > Date.now()) {
    ctx.body = getResponse(false, 'e553')
    return
  }
  // 獲取阿里雲access信息
  let aliyun = await model.sysinfo.findOne({ key: 'aliyun' }).lean()
  if (aliyun && aliyun.val.accessKeyId && aliyun.val.accessSecret) {
    const client = new Alicloud({
      accessKeyId: aliyun.val.accessKeyId,
      accessKeySecret: aliyun.val.accessSecret,
      endpoint: 'https://dysmsapi.aliyuncs.com',
      apiVersion: '2017-05-25',
    })
    // 獲取隨機的6個數字
    const code = +`${_.random(9)}${_.random(9)}${_.random(9)}${_.random(
      9
    )}${_.random(9)}${_.random(9)}`
    const params = {
      RegionId: 'cn-hangzhou',
      PhoneNumbers: phone,
      SignName: '碼客將來',
      TemplateCode: 'SMS_203670207',
      TemplateParam: JSON.stringify({ code }),
    }
    const res = await client.request('SendSms', params, { method: 'POST' })
    if (res && res.Code === 'OK') {
      if (u) {
        await model.user.updateOne(
          { phone },
          { sms_code: code, expire_at: Date.now() + 15 * 60 * 1000 }
        )
      } else {
        await model.user.create({
          nick_name: '碼客',
          phone,
          create_at: Date.now(),
          sms_code: code,
          expire_at: Date.now() + 15 * 60 * 1000,
        })
      }
      ctx.body = getResponse(true, '操做成功')
    } else {
      // fmtp('阿里雲短息發送失敗,緣由:', res.Message)
      console.log('阿里雲短息發送失敗,緣由:', res.Message)
      ctx.body = getResponse(false, 'e500')
    }
  } else {
    fmtp('aliyun --->', aliyun.val)
    ctx.body = getResponse(false, 'e500')
  }
  return
}
複製代碼

你們自行看代碼吧,懶得講太多,不少東西上面提到過。這個代碼基本上是能夠跑起來的,可是呢,我上傳的代碼是修改了關鍵信息的,若是但願接數據庫就把本身的數據庫參數輸入。具體的看配置文件。固然但願你很會玩代碼,自行排查一些問題。沒那個閒功夫的就看看文章就行了,其實業務層,根據不少公司的業務發展不同,須要不同的代碼,仍是那句話,產品形態決定代碼形態。

精髓就是,站着雲廠商的肩膀上,快速把本身業務落地,AI類的我喜歡接百度,語音分析接科大訊飛,服務器、存儲、雲數據庫、短信接阿里雲,IM通常接騰訊,畢竟小程序場景仍是微信生態穩。個人創業經驗就是,小程序(微信)+Nodejs服務+阿里雲企業服務+騰訊智能服務,麻溜的幹業務,之因此沒有成功,就是特麼這個操蛋的創業環境。

各位看官仔細想一想,在雲服務商競爭激烈的今天,給了不少小而美的企業生機。之前要想作到的事情須要各類各樣的人才,耗時耗力,關鍵還未必作的好。如今大部分都是調用現有的雲服務,咱們小企業就作好本身的業務,方便又快捷。並且我不用討論什麼壓力,什麼穩定。我把安全穩定的問題所有用分潤的方式,利用雲服務上的能力解決了。是!我啥底層都不懂,但不妨礙我們成爲一家企業的頂樑柱。未必比一些人差,不少專一於技術的人員,不但沒有解決企業的問題,還增長了企業的負擔,最後仍是說企業這沒有那沒有。想一想問題在哪?牢騷兩句,看官自便。

相關文章
相關標籤/搜索