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服务+阿里云企业服务+腾讯智能服务,麻溜的干业务,之因此没有成功,就是特么这个操蛋的创业环境。

各位看官仔细想一想,在云服务商竞争激烈的今天,给了不少小而美的企业生机。之前要想作到的事情须要各类各样的人才,耗时耗力,关键还未必作的好。如今大部分都是调用现有的云服务,咱们小企业就作好本身的业务,方便又快捷。并且我不用讨论什么压力,什么稳定。我把安全稳定的问题所有用分润的方式,利用云服务上的能力解决了。是!我啥底层都不懂,但不妨碍我们成为一家企业的顶梁柱。未必比一些人差,不少专一于技术的人员,不但没有解决企业的问题,还增长了企业的负担,最后仍是说企业这没有那没有。想一想问题在哪?牢骚两句,看官自便。

相关文章
相关标签/搜索