iKcamp|基于Koa2搭建Node.js实战(含视频)☞ 规范与部署

沪江CCtalk视频地址:https://www.cctalk.com/v/15114923889450

5.png

规范与部署

懒人推动社会进步。

本篇中,我们会讲述三个知识点

  • 定制书写规范
  • 开发环境运行
  • 如何部署运行

定制书写规范

文中所说的书写规范,仅供参考,非项目必需。

随着 Node 流行,JavaScript 编码规范已经相当成熟,社区也产生了各种各样的编码规范。但是在这里,我们要做的不是『限制空格的数量』,也不是『要不要加分号』。我们想要说的规范,是项目结构的规范。

目前我们的项目结构如下:

├─ controller/          // 用于解析用户的输入,处理后返回相应的结果
├─ service/             // 用于编写业务逻辑层,比如连接数据库,调用第三方接口等
├─ errorPage/           // http 请求错误时候,对应的错误响应页面
├─ logs/                // 项目运用中产生的日志数据
├─ middleware/          // 中间件集中地,用于编写中间件,并集中调用
│  ├─ mi-http-error/
│  ├─ mi-log/
│  ├─ mi-send/
│  └── index.js
├─ public/              // 用于放置静态资源
├─ views/               // 用于放置模板文件,返回客户端的视图层
├─ router.js            // 配置 URL 路由规则
└─ app.js               // 用于自定义启动时的初始化工作,比如启动 https,调用中间件,启动路由等

当架构师准备好项目结构后,开发人员只需要修改业务层面的代码即可,比如当我们增加一个业务场景时候,我们大概需要修改三个地方:

  1. service/ 目录下新建文件,处理逻辑层的业务代码,并返回给 controller
  2. controller/ 目录下新建文件,简单处理下请求数据后,传递给 service
  3. 修改路由文件 router.js,增加路由对应的处理器

随着业务量的增大,我们就会发现有一个重复性的操作——『不断的 require 文件,不断的解析文件中的函数』。当业务量达到一定程度时候,可能一个文件里面要额外引入十几个外部文件:

const controller1 = require('...')
const controller2 = require('...')
const controller3 = require('...')
const controller4 = require('...')
...
app.get('/fn1', controller1.fn1() )
app.get('/fn2', controller2.fn2() )
app.get('/fn3', controller3.fn3() )
app.get('/fn4', controller4.fn4() )

单是起名字就已经够头疼的!

所以,我们要做的事情就是,约定代码结构规范,省去这些头疼的事情,比如 router.js

// const router = require('koa-router')()
// const HomeController = require('./controller/home')
// module.exports = (app) => {
//   router.get( '/', HomeController.index )
//   router.get('/home', HomeController.home)
//   router.get('/home/:id/:name', HomeController.homeParams)
//   router.get('/user', HomeController.login)
//   router.post('/user/register', HomeController.register)
//   app.use(router.routes())
//      .use(router.allowedMethods())
// }
const router = require('koa-router')()
module.exports = (app) => {
  router.get( '/', app.controller.home.index )
  router.get('/home', app.controller.home.home)
  router.get('/home/:id/:name', app.controller.home.homeParams)
  router.get('/user', app.controller.home.login)
  router.post('/user/register', app.controller.home.register)
  app.use(router.routes())
     .use(router.allowedMethods())
}

聪明的同学可能已经发现了,app.controller.home.index 其实就是 cotroller/home.js 中的 index 函数。

设计思路

实现思路很简单,当应用程序启动时候,读取指定目录下的 js 文件,以文件名作为属性名,挂载在实例 app 上,然后把文件中的接口函数,扩展到文件对象上。

一般有两种方式入手,一种是程序启动时候去执行,另外一种是请求过来时候再去读取。

而在传统书写方式中,项目启动时候会根据 require 加载指定目录文件,然后缓存起来,其思路与第一种方式一致。如果以中间件的方式,在请求过来时候再去读取,则第一次读取肯定会相对慢一起。综合考虑,我们采用了第一种方式:程序启动时候读取。

代码实现

新建目录文件 middleware/mi-rule/index.js, 实现代码如下:

const Path = require("path");
const fs = require('fs');
module.exports = function (opts) {
  let { app, rules = []} = opts

  // 如果参数缺少实例 app,则抛出错误
  if (!app) {
    throw new Error("the app params is necessary!")
  }
  // 提取出 app 实例对象中的属性名
  const appKeys = Object.keys(app)
  rules.forEach((item) => {
    let { path, name} = item
    // 如果 app 实例中已经存在了传入过来的属性名,则抛出错误
    if (appKeys.includes(name)) {
      throw new Error(`the name of ${name} already exists!`)
    }
    let content = {};
    //读取指定文件夹下(dir)的所有文件并遍历
    fs.readdirSync(path).forEach(filename => {
      //取出文件的后缀
      let extname = Path.extname(filename);
      //只处理js文件
      if (extname === '.js') {
        //将文件名中去掉后缀
        let name = Path.basename(filename, extname);
        //读取文件中的内容并赋值绑定
        content[name] = require(Path.join(path, filename));
      }
    });
    app[name] = content
  })
}

opts 是参数对象,里面包含了实例 app,用来挂载指定的目录文件。rules 是我们指定的目录规则。

用法如下,修改 middleware/index.js

// 引入规则中件间
const miRule = require('./mi-rule')

module.exports = (app) => {
  /**
   * 在接口的开头调用
   * 指定 controller 文件夹下的 js 文件,挂载在 app.controller 属性
   * 指定 service 文件夹下的 js 文件,挂载在 app.service 属性
   */ 
  miRule({
    app,
    rules: [
      {
        path: path.join(__dirname, '../controller'),
        name: 'controller'
      },
      {
        path: path.join(__dirname, '../service'),
        name: 'service'
      }
    ]
  })

  // 以下代码省略
}

业务代码应用

1. 修改 router.js

const router = require('koa-router')()
module.exports = (app) => {
  router.get( '/', app.controller.home.index )
  router.get('/home', app.controller.home.home)
  router.get('/home/:id/:name', app.controller.home.homeParams)
  router.get('/user', app.controller.home.login)
  router.post('/user/register', app.controller.home.register)
  app.use(router.routes()).use(router.allowedMethods())
}

2. 修改 controller/home.js

module.exports = {
  index: async(ctx, next) => {
    await ctx.render("home/index", {title: "iKcamp欢迎您"})
  },
  home: async(ctx, next) => {
    ctx.response.body = '<h1>HOME page</h1>'
  },
  homeParams: async(ctx, next) => {
    ctx.response.body = '<h1>HOME page /:id/:name</h1>'
  },
  login: async(ctx, next) => {
    await ctx.render('home/login', {
      btnName: 'GoGoGo'
    })
  },
  register: async(ctx, next) => {
    // 解构出 app 实例对象
    const { app } = ctx

    let params = ctx.request.body
    let name = params.name
    let password = params.password

    // 留意 service 层的调用方式
    let res = await app.service.home.register(name,password)
    if(res.status == "-1"){
      await ctx.render("home/login", res.data)
    }else{
      ctx.state.title = "个人中心"
      await ctx.render("home/success", res.data)
    }
  }
}

项目中引入这个结构规范,并不是必须的,毕竟大家的想法不一样。iKcamp 团队在提出此想法时候,也是有不少分歧。提出这样一个思路,仅供大家参考。

开发环境运行

作为后端代码语言,开发环境中每次修改文件,都需要手动的重启应用,不能像前端浏览器那样清爽。为了减轻手工重启的成本,我们建议采用 nodemon 来代替 node 以启动应用。当代码发生变化时候,nodemon 会帮我们自动重启。

全局安装 nodemon

npm i nodemon -g

本地项目中也需要安装:

npm i nodemon -S

更多细节用法,请查阅官方文档

部署运行

线上部署运行的话,方法也有很多,我们推荐使用 pm2

pm2 是一个带有负载均衡功能的Node应用的进程管理器。

安装方法与 nodemon 相似,需要全局安装:

npm i pm2 -g

运行方法:

pm2 start app.js

更多细节用法,请查阅官方文档

移动Web前端高效开发实战.png

推荐: 翻译项目Master的自述:

1. 干货|人人都是翻译项目的Master

2. iKcamp出品微信小程序教学共5章16小节汇总(含视频)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,042评论 25 707
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,493评论 18 399
  • 一,tuple,可以装任何对象,没有顺序,取值用[]; 二,RACSequenceb遍历数组和字典 1,遍历数组 ...
    张不二01阅读 570评论 0 0
  • 001进度:今天没看书,烦其他事(借口) 002还是那句,能用钱解决的问题不是我能解决的问题,果然经济是决定一切的...
    K少628阅读 138评论 0 0
  • 在你努力奋斗,百折不挠的时候,记得照看好你心中的那个小女人。
    ShayleeLi阅读 229评论 2 8