前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >NODEJS开发经验

NODEJS开发经验

作者头像
epoos
发布2022-06-06 16:04:36
9850
发布2022-06-06 16:04:36
举报
文章被收录于专栏:epoos.comepoos.com

前段时间做了一个 nodejs 应用,项目架构是 前端 vue 单页应用,后端 nodejs 其实有考虑 ssr,但是因开发时间比较紧张,就没能使用。 下面是开发过程中的一些经验以及遇到的一些问题。

一、技术架构

koa洋葱模型
koa洋葱模型

具体项目技术栈如下: client端: vue 全家桶、history-router server端: koa、koa-router、redis+sentinel、msyql、java (java后端组同学开发)

二、项目目录

代码语言:javascript
复制
client/ # 所有的前端文件
- node_module/ # 前端文件依赖包
- src/ # 前端代码源码
- webpack/ # 构建工具
- package.json # 前端依赖包文件
mock/ # mock数据,
- api/ #对后端的mock(接口数据)
- index.js #mock入口文件
node_modules/ # 项目启动开发工具依赖包
server/ # 服务端代码
- channel/ # 数据渠道、来源(java http、java dubbo、数据库、redis)
- config/ # 网站配置文件(环境配置、数据库、redis 配置等)
- middleware/ # 中间件
- model # 数据库数据模型层
- node_modules/ # 服务端依赖包
- router/ # 路由(controler层)
- tools/ # 一些常用工具函数
- app.js # 服务入口
- autoRouter.js #路由入口
- package.json # 后端依赖包文件
package.json # 公共项目依赖包文件

三、技术要点

promise、async await

promise、async、await都属于javascript基础,这里略过。

client 端的请求

请求类型大概分为如下几类,以及各个类别对应的 koa 处理中间件模块

1.页面请求 —— history-router

2.静态资源请求 —— koa-static

3.favicon请求 —— koa-favicon

4.接口请求 —— koa-router

NODEJS 请求过程
请求流程
请求流程
请求接口模型
请求接口模型
koa 中间件、node端路由

中间件:中间件在请求和响应的过程中给我们一个修改数据的机会

中间件的功能包括: 1.执行任何代码。 2.修改请求和响应对象。 3.终结请求 - 响应循环。 4.调用堆栈中的下一个中间件

中间件是koa的核心,中间件return一个中间件函数,最好是用一个函数给封装起来,以便于传参和可扩展性。 本项目几乎所有路由处理都是通过中间件完成的。

中间件操作分为同步操作和异步操作。 同步操作很简单,处理完事务之后直接 await next() 到下一个中间件即可。

代码语言:javascript
复制
function middleFunction(param1, param2) {
  return async function middle1(ctx, next) {
    if ('/middle1' == ctx.path) {
      ctx.body = { data: param1 + param2 }
    } else {
      await next();
    }
  }
}

异步中间件,也很好理解,就是在中间件内部进行处理的是一个异步流程。 我们可以借助 async 和 await 来处理异步事务。

代码语言:javascript
复制
function middleFunction(param1, param2) {
  return async function middle3(ctx, next) {
    if ('/middle3' == ctx.path) {
      // 对于异步操作,await 必须等待一个promise对象
	  ctx.body = await new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve(param1 + param2)
        }, 3000)
      })
    } else {
      await next()
    }
  }
}

koa 中中间件是最核心的操作,因此往往会有很多中间件,中间件多意味着管理上需要花费更多的精力。 因此,koa 也提供了一些很方便的管理工具,如:用 koa-compose 组合中间件

代码语言:javascript
复制
const compose = require('koa-compose')
async function middle1(ctx, next) {
  if ('/middle1' == ctx.path) {
    ctx.body = { data: 'middle1' }
  } else {
    await next();
  }
}
async function middle2(ctx, next) {
  if ('/middle2' == ctx.path) {
    ctx.body = { data: 'middle2' }
  } else {
    await next()
  }
}
// ...
const middles = compose([middle1, middle2, /*...*/])
app.use(middles)

多个中间件如何执行?执行顺序如何? koa 中间件执行过程是一层一层的执行的,由外而内,再由内向外。 网上流传着很广泛的“洋葱模型”很好的诠释了这顺序,如下图所示:

koa洋葱模型
koa洋葱模型

等同于下面的这张图。

koa洋葱模型2
koa洋葱模型2
代码语言:javascript
复制
const Koa = require('koa')
const app = new Koa()
app.use(async (ctx, next) => {
  console.log(1)
  await next()
  console.log(2)
})

app.use(async (ctx, next) => {
  console.log(3)
  await next()
  console.log(4)
})

app.use(async (ctx, next) => {
  console.log(5)
  ctx.body = 'Hello World'
  console.log(6)
})
app.listen(3000)

// curl localhost:3000 输出:
// 1
// 3
// 5
// 6
// 4
// 2

其执行顺序等同于下面的:

代码语言:javascript
复制
function func1() {
  return new Promise((resolve, reject) => {
    console.log(1)
    func2()
    console.log(2)
  })
}

function func2() {
  return new Promise((resolve, reject) => {
    console.log(3)
    func3()
    console.log(4)
  })
}

function func3() {
  console.log(5)
  return new Promise((resolve, reject) => {
    console.log(6)
  })
}

func1()

// node index.js 执行结果如下:
// 1
// 3
// 5
// 6
// 4
// 2

理解了上面两段代码也就大概理解了 koa 的中间件的执行了。

整个系统执行中间件过程如下

koa-compress > koa-bodyparser > koa2-connect-history-api-fallback > koa-favicon > koa-static > commonRouter -> koa-router

其中 commonRouter 为自定义的中间件,内部路由过程如下: 记录开始时间 > 判断登录态 > 执行后续路由 > 回来执行记录结束时间 > 打日志(日志需要有请求时间)

容错、错误码

容错是程序的必要操作,尤其是后端项目,尤其重要,因为一旦报错很可能导致整个系统崩溃。 影响范围极大,为了更好的管理错误,我们最好能做到统一出口、入口,以便能够对错误进行更好的监控,以及异常处理。 可以借助于中间件来完成。

日志(引入log4 -> 日志埋点上报 -> logsearch|kibana查看)

日志也是后端项目必不可少的,nodejs 项目目前比较流行的日志框架有很多 log4js 是目前用的比较多的,其格式也跟其它语言的日志类似。(如 java 的log4j) log4js:可以做日志收集、写入文件,在服务器直接指定固定目录/data/nodejs/log

代码语言:javascript
复制
data/nodejs/access.log
data/nodejs/other.log
data/nodejs/server.log
本地调试

断点调试是一个很好的习惯,nodejs 最简单快捷的方式就是 console.log 直接控制台查看。 但是,对于复杂的情形,我们也会有需要用到断点调试的时候。 使用 vscode开发,并启动nodejs服务,可以很方便的进行断点 debug。

数据 mock

对于 nodejs 数据 mock 可以有很多方式: 方式一:是用第三方 mock 服务,启动一个mock数据端口static-mock 方式二:利用 webpack 的插件webpack-api-mocker

开发此项目的时候用的是方法二,好处是可以少启动一个端口,mock 可以和 client 的 webpack-dev-server 共享端口。

用到的主要第三方中间件

koa-static:将静态目录映射为路由可访问的路径 koa-favicon:将favicon.ico路径映射为可访问路径并设置max-age缓存头 koa-compress:对请求进行开启gzip压缩,效果很明显(nginx也可以做压缩),压缩之后 response-headers会有这个属性 Content-Encoding:gzip koa-bodyparser:对于POST请求的处理,koa-bodyparser中间件可以把 koa2 上下文的 formData 数据解析到 ctx.request.body中 koa2-connect-history-api-fallback:对vue history路由做处理,默认将非.xxx后缀请求跳到默认index.html页面

安全 xss、csrf、sql注入

koa-helmet:9个安全中间件的集合、帮助app抵御常见的一些web安全隐患 koa-limit:防止DOS攻击 koa-csrf:防止CSRF攻击 sql注入:对参数进行过滤(见后面附录1)

除此之外,还用到了如下工具:

启动工具 pm2、nodemon、配置、部署、健康检查
redis、sentinels、Medis图形化工具
mysql、mysql连接池、navicat图形化工具
四、踩过的坑

1.favicon.ico 不出来:

代码语言:javascript
复制
app.use(favicon(path.join(__dirname, 'favicon.ico')))
代码语言:javascript
复制
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">

2.直接用浏览器打开接口失败

原因:koa2-connect-history-api-fallback 中间件做了强制跳转

代码语言:javascript
复制
// /server/node_modules/koa2-connect-history-api-fallback/lib/connect-history-api-fallback.js
// koa2-connect-history-api-fallback 中间件在此处做了强制跳转
if (parsedUrl.pathname.indexOf('.') !== -1 && options.disableDotRule !== true) {
  logger('Not rewriting', ctx.method, ctx.url, 'because the path includes a dot (.) character.');
  return next();
}

解决办法:设置白名单

代码语言:javascript
复制
app.use(historyApiFallback({ whiteList: ['/api/'] }))

3.ndp环境变量首次设置之后生效,后面修改不生效,不生效

代码语言:javascript
复制
ndp -> 配置 -> 发布配置 -> NODE_ENV 

原因:怀疑是ndp本身的bug,未确定。

解决办法:手动杀掉服务器上pm2进程,重新启动。

4.发布之后进程没有杀死,有一个错误的进程将服务器cpu跑满了。

原因:可能是早期服务代码不完善,报错导致pm2管理失败,后续未重现

解决办法:手动杀掉服务器进程

5.日志打印报错,log4js 本地能写日志文件,服务器上写不了。

原因:

本地开发启动NODE服务的时候只启动一个进程。(需理解进程的概念)

而通过ndp发布之后,自动通过pm2启动,用的是cluster模式,启动了多个进程。

log4js,对于单进程和多进程需要做不同的配置。

解决办法:

代码语言:javascript
复制
// 文档地址: https://log4js-node.github.io/log4js-node/api.html
log4js.configure({
  disableClustering: true, // 不启动日志的集群模式
  // pm2: true, // 或者使用pm2,此模式需要服务端安装 pm2 install pm2-intercom
  // ...
})

6.测试、后端登录我们的项目的时候登录偶尔登录不上,切接口数据更新不及时

原因:配置nginx的时候配置了缓存6min

代码语言:javascript
复制
 location / {
  proxy_pass http://node_server;
  expires 10m; # 这个不需要
}

解决办法:

去掉nginx缓存配置 expires选项。

7.每次到一个新的环境,第一次构建都会报模块找不到的错误,重试N次之后正常。

可能原因:

执行build.sh的时候执行的是npm install client && npm install server 安装的总命令

总命令下的子命令 npm install client 等才是真正的安装npm依赖模块

而执行build.sh的时候脚本是同步的,但是只针对脚本内的总命令,不包括子命令

导致npm安装变成异步执行了,在npm未安装完成的情况下执行npm run build导致报错

解决办法:将总命令拆开分别执行安装

代码语言:javascript
复制
registry=https://registry.npm.taobao.org
npm install --prefix  ./client --registry=$registry
npm install --prefix ./server --registry=$registry
npm run build

8.经过 Nginx 的静态资源和接口返回的数据被截掉了一部分,返回的数据不完整。

问题原因:

新的预发环境nginx配置了缓冲,缓冲过小的时候nginx会将数据写入硬盘,而此时如果没有硬盘文件夹的读取权限,就会出现请求数据被截断的情况。

解决办法:增大缓冲

代码语言:javascript
复制
# 在预发环境 和 线上环境的location / 下面配置 proxy_buffers 缓存大小
location / {
  proxy_buffer_size 64k; # 请求头缓冲大小
  proxy_buffers 4 512k; # 请求内容缓冲大小 4 * 512kb
}

node-mysql中防止SQL注入四种常用方法:

方法一:使用 escape 方法对参数进行编码,如:
代码语言:javascript
复制
mysql.escape(param); 
connection.escape(param);
poll.escape(param)

escape()方法编码规则:

代码语言:javascript
复制
Numbers不进行转换;

Booleans转换为true/false;

Date对象转换为’YYYY-mm-dd HH:ii:ss’字符串;

Buffers转换为hex字符串,如X’0fa5’;

Strings进行安全转义;

Arrays转换为列表,如[‘a’, ‘b’]会转换为’a’, ‘b’;

多维数组转换为组列表,如[[‘a’, ‘b’], [‘c’, ‘d’]]会转换为’a’, ‘b’), (‘c’, ‘d’);

Objects会转换为key=value键值对的形式。嵌套的对象转换为字符串;

undefined/null会转换为NULL;

MySQL不支持NaN/Infinity,并且会触发MySQL错误。
方法二:使用connection.query()的查询参数占位符

使用”?”作为查询参数占位符。 在使用查询参数占位符的时候,在其内部自动调用 connection.escape() 方法对其传入的参数进行编码,如:

代码语言:javascript
复制
let post  = { name: 'namestring' }
let query = connection.query('SELECT * FROM users WHERE ?', post, (err, results) => {
});
console.log(query.sql); // SELECT * FROM users WHERE name = 'namestring'
方法三:使用escapedId()编码SQL查询标识符。
代码语言:javascript
复制
mysql.escapedId(identifier)
connection.escapeId(identifier)
pool.escapeId(identifier)

// 多用于排序,如:
let sorter = 'date'
let sql = 'SELECT * FROM posts ORDER BY ' + connection.escapeId(sorter)
方法四:使用mysql.format()转义参数。

准备查询,此方法用于准备查询语句,该函数会自动选择合适的转义参数。

相关链接:

mac 靠谱的安装mysql教程地址: Redis 命令 Redis Sentinel 介绍与部署 koa安全中间件简介

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019-01-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、技术架构
  • 二、项目目录
  • 三、技术要点
    • 四、踩过的坑
    • node-mysql中防止SQL注入四种常用方法:
      • 方法一:使用 escape 方法对参数进行编码,如:
        • 方法二:使用connection.query()的查询参数占位符
          • 方法三:使用escapedId()编码SQL查询标识符。
            • 方法四:使用mysql.format()转义参数。
            • 相关链接:
            相关产品与服务
            消息队列 TDMQ
            消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档