Quantcast
Channel: CNode:Node.js专业中文社区
Viewing all 14821 articles
Browse latest View live

如何选择正确的Node框架:Express,Koa还是Hapi?

$
0
0

摘要: Node三驾马车。

Fundebug经授权转载,版权归原作者所有。

简介

Node.js是10年前首次推出的,目前它已经成为世界上最大的开源项目,在GitHub上有+59,000颗星,下载次数超过10亿。流行度快速增长的部分原因是Node.js允许开发人员在应用程序的客户端和服务器端部分使用相同的语言:JavaScript。Node.js是一个开源和跨平台的JavaScript运行时环境,专为构建可扩展的服务器端WEB应用而设计,自身具有高并发、扩展性强等特点。由于社区其呈指数级增长和普及,因此创建了许多框架来提高生产力。在本文中,我们将探讨Node.js中三个最流行的框架之间的差异:Express,Koa和Hapi。在以后的文章中,我们将研究Next,Nuxt和Nest。

比较基于:

  • GitHub Stars和npm下载
  • 安装
  • 基本的Hello World应用程序
  • 好处
  • 缺点
  • 性能
  • 安全
  • 社区参与

Express

Express是一个最小且灵活的Web应用程序框架,为Web和移动应用程序提供了一组强大的功能,它的行为就像一个中间件,可以帮助管理服务器和路由

  • star

    • GitHub star:+43,000
    • npm每周下载 6,881,035
  • 安装

    确保你已经安装node和npm

    // 你可以将express安装到项目依赖
        npm install express --save
        
        // 如果要临时安装Express而不是将其添加到依赖项列表,则可以使用
        npm install express --no-save
    
  • Hello World

    这是关于如何创建一个侦听端口3000并响应“Hello World!”的快速应用程序的最基本示例

    // 这里只创建根目录 其他目录返回404
        const express = require('express')
        const app = express()
        const port = 3000
        
        app.get('/', (req, res) => res.send('Hello World!'))
        
        app.listen(port, () => console.log(`Example app listening on port ${port}!`))
    
  • 好处

    • 几乎是Node.js Web中间件的标准
    • 简单,简约,灵活和可扩展
    • 快速开发应用程序
    • 完全可定制
    • 学习曲线低
    • 轻松集成第三方服务和中间件
    • 主要关注浏览器,模板和渲染集成开箱即用
  • 缺点

    尽管Express.js是一个非常方便且易于使用的框架,但它有一些可能影响开发过程的小缺点。

    • 组织需要非常清楚,以避免在维护代码时出现问题
    • 随着代码库大小的增加,重构变得非常具有挑战性
    • 需要大量的手工劳动,因为您需要创建所有端点
  • 性能

    Express是对web应用的一层基本封装,继承了Node.js的特性

    当天也有一些express性能的最佳实践包括:

    • 使用gzip压缩
    • 不要使用同步功能
    • 正确记录(用于调试,使用特殊模块,如调试,应用程序活动使用winston或bunyan)
    • 使用try-catch或promises正确处理异常
    • 确保您的应用程序使用流程管理器自动重新启动,或使用systemd或upstartinit等系统
    • 在群集中运行您的应用。您可以通过启动进程集群来大大提高Node.js应用程序的性能
    • 缓存请求结果,以便您的应用不会重复操作以反复提供相同的请求
    • 使用负载均衡器运行它的多个实例并分配流量,如Nginx或HAProxy
    • 对静态资源使用反向代理。它可以处理错误页面,压缩,缓存,提供文件和负载平衡等
    • 更多性能最佳实践

一个简单的“Hello World”应用程序每秒具有以下性能请求:

  • 安全

    Node.js漏洞直接影响Express,因此确保使用最新的稳定版Node.js

  • 社区参与

    • 贡献者数量:220
    • Pull Requests:821
    • Express社区定期活动包括 Gitter,IRC channel, issues, Wiki等等

最后,express可能是Node.js最流行的框架,还有许多其他流行的框架都是基于Express构建的。

koa

Koa 是一个新的 web 框架,由 Express幕后的原班人马打造,致力于成为web应用和API开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa帮你丢弃回调函数,并有力地增强错误处理Koa并没有捆绑任何中间件而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序

  • star

    • GitHub star:+25,000
    • npm每周下载:+ 300K
  • 安装

    Koa需要nodev7.6.0以上版本支持,因为内部使用了ES6的特性

        npm i koa
        node my-koa-app.js
    
  • Hello World

    创建一个web服务,监听3000端口返回‘Hello World’

        const Koa = require('koa');
        const app = new Koa();
        
        app.use(async ctx => {
          ctx.body = 'Hello World';
        });
        
        app.listen(3000);
    
  • 好处

    • Koa提高了互操作性,健壮性,使编写中间件变得更加愉快。
    • 集成了大量的web API,但是没有绑定中间件
    • 非常轻量,核心的Koa模块只有大约2K行代码
    • 拥有非常好的用户体验
    • 通过try / catch更好地处理错误
    • 异步控制流,代码可读性更高
  • 缺点

    • Koa社区相对较小
    • 与Express风格的中间件不兼容(目前还有遇到与其他框架兼容的中间件)
  • 性能

    Koa本身是一个非常轻量级的框架,可以构建具有出色性能的Web应用程序。代码可读性和维护性都相对较高

    当然一些性能的最佳实践也是必不可少的,例如:

    • 集群
    • 并行运行
    • 在代码中使用异步API
    • 保持代码小而轻
    • 以及使用gzip压缩 等等

一个简单的“Hello World”应用程序每秒具有以下性能请求:

  • 安全

    Koa有大量的中间件,提供相应的功能 贴图一张

  • 社区

最后,Koa专注于核心中间件功能,设计显式地利用了async/ waiting使异步代码可读性更高

Hapi

Hapi是基础功能相对丰富的框架。开发人员更专注于业务,而不是花时间构建基础架构。配置驱动的模式,区别于传统的web服务器操作。他还有比一个独特功能,能够在特定的IP上创建服务器,具有类似的功能onPreHandler。再需要的时候你可以拦截特地的请求做一些必要的操作

  • star _ GitHub Stars: +11000

    • npm 周下载: +222,293
  • 安装

    确保你已经安装node

    npm install hapi
    
  • Hello World

    以下示例是使用hapi的最基本的hello world应用程序:

    'use strict';
        
        const Hapi=require('hapi');
        
        // 创建一个服务监听8000端口
        const server=Hapi.server({
           host:'localhost',
            port:8000
        });
        
        // 添加路由
        server.route({
            method:'GET',
            path:'/hello',
            handler:function(request,h) {
        
               return'hello world';
            }
        });
        
        // 启动服务
        const start = async function() {
           try {
               await server.start();
           }
           catch (err) {
                console.log(err);
                process.exit(1);
            }
        
           console.log('Server running at:', server.info.uri);
        };
        start();
    
  • 好处

    • 提供了一个强大的插件系统,允许您快速添加新功能和修复错误
    • 可扩展的API
    • 对请求处理有更深层次的控制。
    • 创建(REST)api的最佳选择,提供了路由、输入、输出验证和缓存
    • 一次编写适配各端
    • 详细的API参考和对文档生成的良好支持
    • 与任何前端框架(如React,Angular和Vue.js)一起使用来创建单页面应用程序
    • 基于配置的伪中间件
    • 提供缓存,身份验证和输入验证
    • 提供基于插件的扩展架构
    • 提供非常好的企业插件,如joi,yar,catbox,boom,tv和travelogue
  • 缺点

    • 代码结构复杂
    • 插件不兼容,只能使用指定的插件如:catbox joi boom tv good travelogue等
    • 端点是手动创建的,必须手动测试
    • 重构是手动的
  • 性能

    017年对Node框架的研究表明hapi相对于其他框架的表现最差

    一个简单的“Hello World”应用程序每秒具有以下性能请求:

  • 安全

    hapi安全性主要依赖于插件 插件选择

    • Crumb反(XCSRF)验证插件。它适用于常规请求和CORS请求
    • Joi:JavaScript对象的对象模式描述语言和验证器
    • Hapi-rbac用户的访问权限控制
    • Blankie足够灵活的白名单作机制
    • Cryptiles加密库
  • 社区

    • 贡献者数量:184
    • Pull Requests:1176

最后Express仍然是当下最为流行,koa因拥抱ES6正在崛起,hapi还是大型项目的第一选择

不管是Express,Koa还是Hapi目前都是非常成熟的框架。几乎都能满足你的需求,没有最好,只有最合适

Choosing the right Node.js Framework: Express, Koa, or Hapi?

关于Fundebug

Fundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了10亿+错误事件,付费客户有Google、360、金山软件、百姓网等众多品牌企业。欢迎大家免费试用


新手的 tail 命令实现

$
0
0

本人 iOSer, 最近不是太忙, 下决心扩展自己的能力范围, 于是乎开始学 node, 感觉极大的扩展了自己编程能力的应用范围, 学了 fs, stream 模块之后, 写了一个简易的 tail 命令

node test-ntail.js ../colors.js 13 // colors.js 是本观察的文件, test-ntail.js 是测试文件, 支持行数, 整数从头开始数, 负数从尾部开始数

ntail.js


const { Readable } = require('stream')
const fs = require('fs')

class NTail extends Readable {
    constructor(path, opts) {
        super(path, opts)
        const{ watchLineNum } = opts
        let watchLineNumInt = parseInt(watchLineNum, 10)
        if (fs.existsSync(path)) {
            this.fd = fs.openSync(path)
            this.position = this.gotoWatchLineNum(watchLineNumInt)
            process.on('SIGINT', () => {
                fs.closeSync(this.fd)
                process.exit(1)
            })
        } else {
            throw Error(`${path} 文件不存在`)
        }
    }

    _read(size) {
       this.pushTailData()
    }

    pushTailData() {
        let buffer = Buffer.alloc(4 * 1024)
        fs.read(this.fd, buffer, 0, buffer.length, this.position, (err, readSize, buffer) => {
            this.push(buffer.slice(0, readSize))
            this.position += readSize
        })
    }
	
    gotoWatchLineNum (watchLineNum) {
        let fileSize = fs.fstatSync(this.fd).size
        let char = Buffer.alloc(1)
        let lineNum = 0
        let readSize = 0
        do {
            if(watchLineNum  >= 0) {
                fs.readSync(this.fd, char, 0, 1, null)
                if (char.toString() === '\n') {
                    lineNum += 1
                }
            } else {
                fs.readSync(this.fd, char, 0, 1, fileSize - readSize)
                if (char.toString() === '\n') {
                    lineNum -= 1
                }
            }
            readSize += 1
        } while(lineNum !== watchLineNum)
        if (watchLineNum >= 0) {
            return readSize
        } else {
            return fileSize - (readSize - 2)
        }
    }
}

module.exports = NTail

test-ntail.js

const NTail = require('./ntail')

let file = process.argv[2]
let lineNum = process.argv[3]
let tail = new NTail(file, {watchLineNum: lineNum})
tail.pipe(process.stdout)

iOS 也算是前端的一种, 学习了 node 之后, 感觉有了一种轻松的方式跟系统本身打交道, 原来是框架的使用者, node 为我们提供了生产资料, 我们现在也可以变成框架的创造者, 非常开心, 祝 node 长青

egg 定时任务 内存泄漏?????

$
0
0

egg 自带定时任务 在Ubuntu系统上内存泄漏,正常运行应该有2g多内存,我只要把定时任务已开启,立马只剩下100M了,但我本地mac运行是正常的。哎… 11111.png

Node.js 是如何异步判断文件是否存在?

$
0
0

通常我们在讨论 Node.js 的时候都会涉及到异步这个特性。实际上 Node.js 在执行异步调用的时候,不同的场景下有着不同的处理方式。本文将通过 libuv 源码来分析 Node.js 是如何通过 libuv 的线程池完成异步调用。本文描述的 Node.js 版本为 v11.15.0,libuv 版本为 1.24.0

以下面的代码为例,它通过调用 fs.access来异步地判断文件是否存在并在回调中打印日志,在 Node.js 中这是一个典型的异步调用。

const fs = require('fs')
const cb = function (err) {
  console.log(`Is myfile exists: ${!err}`)
}
fs.access('myfile', cb)

在分析上面这段代码的调用过程之前,我们先来了解一些 libuv 概念。

什么类型的请求 libuv 会把它放到线程池去执行

主动通过 libuv 发起的操作被 libuv 称为请求( uv_req_t ),libuv 的线程池作用于以下 4 种枚举的异步请求:

其它的 UV_CONNECTUV_WRITEUDP_SEND等则并不会通过线程池去执行。

线程池请求分类

这 4 种枚举请求 libuv 内部把它们分为 3 种任务类型( uv__work_kind ):

  • UV__WORK_CPU:CPU 密集型,UV_WORK类型的请求被定义为这种类型。因此根据这个分类,不推荐在 uv_queue_work中做 I/O 密集的操作。
  • UV__WORK_FAST_IO:快 IO 型,UV_FS类型的请求被定义为这种类型。
  • UV__WORK_SLOW_IO:慢 IO 型,UV_GETADDRINFOUV_GETNAMEINFO类型的请求被定义为这种类型。

UV__WORK_SLOW_IO执行不同于 UV__WORK_CPUUV__WORK_FAST_IO,libuv 执行它的时候流程会有些差异,这个后面会提到。

线程池是如何初始化的

libuv 通过init_threads函数初始化线程池,初始化时会根据一个名为 UV_THREADPOOL_SIZE的环境变量来初始化内部线程池的大小,线程最大数量为 128,默认为 4。如果以单进程的架构去部署服务,可以根据服务器 CPU 的核心数量及业务情况来设置线程池大小,达到资源利用的最大化。uv loop 线程在创建 worker 线程时,会初始化以下变量:

  • 信号量 sem:在创建线程时与线程进行同步,每个线程创建好后将会通过这个信号量告知 uv loop 线程自己已经初始化完毕,可以开始处理请求了。当所有线程都初始化完成后这个信号量将被销毁,即完成线程池的初始化。
  • 条件变量 cond:线程创建完成后通过这个条件变量进入阻塞状态( uv_cond_wait ),直到其它线程通过 uv_cond_signal将其唤醒。
  • 互斥量 mutex:对下面 3 个临界资源进行互斥访问。
  • 请求队列 wq:线程池收到 UV__WORK_CPUUV__WORK_FAST_IO类型的请求后将其插到此队列的尾部,并通过 uv_cond_signal唤醒 worker 线程去处理,这是线程池请求的主队列。
  • 慢 I/O 队列 slow_io_pending_wq:线程池收到 UV__WORK_SLOW_IO类型的请求后将其插到此队列的尾部。
  • 慢 I/O 标志位节点 run_slow_work_message:当存在慢 I/O 请求时,用来作为一个标志位放在请求队列 wq 中,表示当前有慢 I/O 请求,worker 线程处理请求时需要关注慢 I/O 队列的请求;当慢 I/O 队列的请求都处理完毕后这个标志位将从请求队列 wq 中移除。

worker 线程的入口函数均为 worker函数,这个我们后面再说。 init_threads实现如下:

static void init_threads(void) {
  unsigned int i;
  const char* val;
  uv_sem_t sem;

  // 6-23 行初始化线程池大小
  nthreads = ARRAY_SIZE(default_threads);
  val = getenv("UV_THREADPOOL_SIZE"); // 根据环境变量设置线程池大小
  if (val != NULL)
    nthreads = atoi(val);
  if (nthreads == 0)
    nthreads = 1;
  if (nthreads > MAX_THREADPOOL_SIZE)
    nthreads = MAX_THREADPOOL_SIZE;

  threads = default_threads;
  if (nthreads > ARRAY_SIZE(default_threads)) {
    threads = uv__malloc(nthreads * sizeof(threads[0]));
    if (threads == NULL) {
      nthreads = ARRAY_SIZE(default_threads);
      threads = default_threads;
    }
  }
  // 初始化条件变量
  if (uv_cond_init(&cond))
    abort();

  // 初始化互斥量
  if (uv_mutex_init(&mutex))
    abort();

  // 初始化队列和节点
  QUEUE_INIT(&wq); // 工作队列
  QUEUE_INIT(&slow_io_pending_wq); // 慢 I/O 队列
  QUEUE_INIT(&run_slow_work_message); // 如果有慢 I/O 请求,将此节点作为标志位插入到 wq 中

  // 初始化信号量
  if (uv_sem_init(&sem, 0))
    abort(); // 后续线程同步需要依赖这个信号量,因此这个信号量创建失败了则终止进程

  // 创建 worker 线程
  for (i = 0; i < nthreads; i++)
    if (uv_thread_create(threads + i, worker, &sem)) // 初始化 worker 线程
      abort(); // woker 线程创建错误原因为 EAGAIN、EINVAL、EPERM 其中之一,具体请参考 man3
  
  // 等待 worker 创建完成
  for (i = 0; i < nthreads; i++)
    uv_sem_wait(&sem); // 等待 worker 线程创建完毕

  // 回收信号量资源
  uv_sem_destroy(&sem);
}

请求是如何放到线程池去执行的

libuv 有两个函数可以创建多线程请求:

uv__work_submit函数主要做 2 件事:

  1. 调用 init_threads初始化线程池,因为线程池的创建是惰性的,只有用到的时候才会创建。
  2. 调用内部的 post函数将请求插入到请求队列中。

实现如下:

void uv__work_submit(uv_loop_t* loop,
                     struct uv__work* w,
                     enum uv__work_kind kind,
                     void (*work)(struct uv__work* w),
                     void (*done)(struct uv__work* w, int status)) {
  // 在收到请求后才开始初始化线程池,但是只会初始化一次
  uv_once(&once, init_once);
  w->loop = loop;
  w->work = work;
  w->done = done;
  post(&w->wq, kind);
}

static void init_once(void) {
  // fork 后子进程的 mutex 、condition variables 等 pthread 变量的状态是父进程 fork 时的复制,所以子进程创建时需要重置状态
  // 具体请参考 http://man7.org/linux/man-pages/man2/fork.2.html
  if (pthread_atfork(NULL, NULL, &reset_once))
    abort();
  // 初始化线程池
  init_threads();
}

static void reset_once(void) {
  // 重置 once 变量
  uv_once_t child_once = UV_ONCE_INIT;
  memcpy(&once, &child_once, sizeof(child_once));
}

post函数主要做 2 件事:

  1. 判断请求的请求类型是否是 UV__WORK_SLOW_IO
    • 如果是,将这个请求插到慢 I/O 请求队列 slow_io_pending_wq的尾部,同时在请求队列 wq的尾部插入一个 run_slow_work_message节点作为标志位,告知请求队列 wq当前存在慢 I/O 请求。
    • 如果不是,将请求插到请求队列 wq尾部。
  2. 如果有空闲的线程,唤醒某一个去执行请求。

并发的慢 I/O 的请求数量不会超过线程池大小的一半,这样做的好处是避免多个慢 I/O 的请求在某段时间内把所有线程都占满,导致其它能够快速执行的请求需要排队。

post函数实现如下:

static void post(QUEUE* q, enum uv__work_kind kind) {
  // 加锁
  uv_mutex_lock(&mutex);
  if (kind == UV__WORK_SLOW_IO) {
    /* 插入到慢 I/O 队列中 */
    QUEUE_INSERT_TAIL(&slow_io_pending_wq, q);
    /* 如果 run_slow_work_message 节点不为空代表其已在 wq 队列中,无需再次插入 */
    if (!QUEUE_EMPTY(&run_slow_work_message)) {
      uv_mutex_unlock(&mutex);
      return;
    }
    // 不在 wq 队列中则将 run_slow_work_message 作为标志位插到 wq 尾部
    q = &run_slow_work_message;
  }
  // 将请求插到请求队列尾部
  QUEUE_INSERT_TAIL(&wq, q);
  // 如果有空闲的线程,唤醒某一个去执行请求
  if (idle_threads > 0)
    uv_cond_signal(&cond); // 唤醒一个 worker 线程
  uv_mutex_unlock(&mutex);
}

worker 线程的入口函数 worker在线程创建好并初始化完成后将按照下面的步骤不断的循环:

  1. 等待唤醒。
  2. 取出请求队列 wq 或者慢 I/O 请求队列的头部请求去执行。
  3. 通知 uv loop 线程完成了一个请求的处理。
  4. 回到 1 。
static void worker(void* arg) {
  struct uv__work* w;
  QUEUE* q;
  int is_slow_work;

  // 通知 uv loop 线程此 worker 线程已创建完毕
  uv_sem_post((uv_sem_t*) arg);
  arg = NULL;

  uv_mutex_lock(&mutex);
  // 通过这个死循环来不断的执行请求
  for (;;) {
    /*
    	这个 while 有2个判断
    	1. 在多核处理器下,pthread_cond_signal 可能会激活多于一个线程,通过一个 while 来避免这种情况导致的问题,具体请参考 https://linux.die.net/man/3/pthread_cond_signal
    	2. 限制慢 I/O 请求的数量小于线程数量的一半
    */
    while (QUEUE_EMPTY(&wq) ||
           (QUEUE_HEAD(&wq) == &run_slow_work_message &&
            QUEUE_NEXT(&run_slow_work_message) == &wq &&
            slow_io_work_running >= slow_work_thread_threshold())) {
      idle_threads += 1;
      // worker 线程初始化完成或没有请求执行时进入阻塞状态,直到被新的请求唤醒
      uv_cond_wait(&cond, &mutex);
      idle_threads -= 1;
    }
    // 唤醒并且达到执行请求的条件后取出队列头部的请求
    q = QUEUE_HEAD(&wq);
    // 如果头部请求是退出,则跳出循环,结束 worker 线程
    if (q == &exit_message) {
      // 继续唤醒其它 worker 去结束线程
      uv_cond_signal(&cond);
      uv_mutex_unlock(&mutex);
      break;
    }

    // 将这个请求节点从请求队列 wq 中移除
    QUEUE_REMOVE(q);
    QUEUE_INIT(q);

    is_slow_work = 0;
    // 如果这个请求是慢 I/O 的标志位
    if (q == &run_slow_work_message) {
      /* 控制慢 I/O 请求数量,超过则插到队列尾部,等待前面的请求执行完 */
      if (slow_io_work_running >= slow_work_thread_threshold()) {
        QUEUE_INSERT_TAIL(&wq, q);
        continue;
      }

      /* 判断慢 I/O 请求队列中是否有请求,请求有可能被取消 */
      if (QUEUE_EMPTY(&slow_io_pending_wq))
        continue;

      is_slow_work = 1;
      slow_io_work_running++;

      // 取出慢 I/O 请求队列中头部的请求
      q = QUEUE_HEAD(&slow_io_pending_wq);
      QUEUE_REMOVE(q);
      QUEUE_INIT(q);

      // 如果慢 I/O 请求队列中还有请求,则将 run_slow_work_message 这个标志位重新插到请求队列 wq 的尾部
      if (!QUEUE_EMPTY(&slow_io_pending_wq)) {
        QUEUE_INSERT_TAIL(&wq, &run_slow_work_message);
        if (idle_threads > 0)
          uv_cond_signal(&cond); // 唤醒一个线程继续执行
      }
    }

    uv_mutex_unlock(&mutex);

    w = QUEUE_DATA(q, struct uv__work, wq);
    // 上面处理了这多,终于在这里开始执行请求的函数了
    w->work(w);

    uv_mutex_lock(&w->loop->wq_mutex);
    w->work = NULL;
    
    // 为保证线程安全,请求执行完后不会立即回调请求,而是将完成的请求插到已完成的请求队列中,在uv loop 线程完成回调
    QUEUE_INSERT_TAIL(&w->loop->wq, &w->wq);
    // 通过 uv_async_send 同步 uv loop 线程:线程池完成了一个请求
    uv_async_send(&w->loop->wq_async);
    uv_mutex_unlock(&w->loop->wq_mutex);

    uv_mutex_lock(&mutex);
    if (is_slow_work) {
      slow_io_work_running--;
    }
  }
}

请求在 worker 执行完后是如何同步 uv loop 所在的线程

uv_loop_init时,线程池的 wq_async(uv_async_t) 句柄通过 uv_async_init初始化并插入到 uv loop 的 async_handles队列中,然后在 uv loop 线程中遍历 async_handles队列并完成回调。

worker 线程 和 uv loop 线程通过 uv_async_send进行同步,而uv_async_send只做了一件事:向 async_wfd句柄写了一个长度为 1 个字节的字符串(只有 \0这个字符)。

uv_async_send实现如下:

int uv_async_send(uv_async_t* handle) {
  if (ACCESS_ONCE(int, handle->pending) != 0)
    return 0;
  // cmpxchgi 函数设置标志位,如果已经设置过则不会重复调用 uv__async_send
  if (cmpxchgi(&handle->pending, 0, 1) == 0)
    uv__async_send(handle->loop);

  return 0;
}

static void uv__async_send(uv_loop_t* loop) {
  const void* buf;
  ssize_t len;
  int fd;
  int r;

  buf = "";
  len = 1;
  fd = loop->async_wfd;

#if defined(__linux__)
  if (fd == -1) {
    static const uint64_t val = 1;
    buf = &val;
    len = sizeof(val);
    fd = loop->async_io_watcher.fd;  /* eventfd */
  }
#endif

  do
    r = write(fd, buf, len); // 向 fd 写入内容
  while (r == -1 && errno == EINTR);

  if (r == len)
    return;

  if (r == -1)
    if (errno == EAGAIN || errno == EWOULDBLOCK)
      return;

  abort();
}

async_wfd写内容为什么能做到同步呢?实际上在 worker 线程对 async_wfd写入时,uv loop 线程同时也在不断的循环去接收处理各种各样的事件或请求,其中就包括对 async_wfd可读事件的监听。

uv loop 是在 uv_run函数中执行的,它在 Node.js 启动时被调用, uv_run实现如下:

int uv_run(uv_loop_t* loop, uv_run_mode mode) {
  int timeout;
  int r;
  int ran_pending;

  r = uv__loop_alive(loop);
  if (!r)
    uv__update_time(loop);

  while (r != 0 && loop->stop_flag == 0) {
    // 更新计时器时间
    uv__update_time(loop);
    // 回调超时的计时器,setTimeout、setInterval 都是由这个函数回调
    uv__run_timers(loop);
    // 处理某些没有在 uv__io_poll 完成的回调
    ran_pending = uv__run_pending(loop);
    // 官方解释:Idle handle is needed only to stop the event loop from blocking in poll.
    // 实际上 napi 中某些函数比如 napi_call_threadsafe_function 会往 idle 队列中插入回调,然后在这个阶段执行
    uv__run_idle(loop);
    // process._startProfilerIdleNotifier 的回调
    uv__run_prepare(loop);

    timeout = 0;
    if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
      timeout = uv_backend_timeout(loop); // 计算 uv__io_poll 超时时间,算法请参考 https://github.com/libuv/libuv/blob/v1.24.0/src/unix/core.c#L318

    // 对 async_wfd 可读的监听在 uv__io_poll 这个函数中
    // 第二个参数 timeout 为上面计算出来,用来设置 epoll_wait 等函数等待 I/O 事件的时间
    uv__io_poll(loop, timeout);
    // setImmediate 的回调
    // ps: 个人觉得从实现上讲 setImmediate 和 nextTick 应该互换名字 :-)
    uv__run_check(loop);
    // 关闭句柄是个异步操作
    // 一般结束 uv loop 时会先调用 uv_walk 遍历所有句柄并关闭它们,然后再执行一次 uv loop 通过这个函数来完成关闭,最后再调用 uv_loop_close,否则的话会出现内存泄露
    uv__run_closing_handles(loop);

    if (mode == UV_RUN_ONCE) {
      /* UV_RUN_ONCE implies forward progress: at least one callback must have
       * been invoked when it returns. uv__io_poll() can return without doing
       * I/O (meaning: no callbacks) when its timeout expires - which means we
       * have pending timers that satisfy the forward progress constraint.
       *
       * UV_RUN_NOWAIT makes no guarantees about progress so it's omitted from
       * the check.
       */
      uv__update_time(loop);
      uv__run_timers(loop);
    }

    r = uv__loop_alive(loop);
    if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
      break;
  }

  /* The if statement lets gcc compile it to a conditional store. Avoids
   * dirtying a cache line.
   */
  if (loop->stop_flag != 0)
    loop->stop_flag = 0;

  return r;
}

可以看到 uv loop 里面其实就是在不断的循环去更新计时器、处理各种类型的回调、轮询 I/O 事件,Node.js 的异步便是通过 uv loop 完成的。

libuv 的异步采用的是 Reactor模型进行多路复用,在 uv__io_poll中去处理 I/O 相关的事件, uv__io_poll在不同的平台下通过 epollkqueue等不同的方式实现。所以当往 async_wfd写入内容时,在 uv__io_poll中将会轮询到 async_wfd可读的事件,这个事件仅仅是用来通知 uv loop 线程: 非 uv loop 线程有回调需要在 uv loop 线程执行。

当轮询到 async_wfd可读后,uv__io_poll会回调对应的函数 uv__async_io,它主要做了下面 2 件事:

  1. 读取数据,确认是否有 uv_async_send调用,数据内容并不关心。
  2. 遍历 async_handles句柄队列 ,判断是否有事件,如果有的话执行它的回调。

实现如下:

static void uv__async_io(uv_loop_t* loop, uv__io_t* w, unsigned int events) {
  char buf[1024];
  ssize_t r;
  QUEUE queue;
  QUEUE* q;
  uv_async_t* h;

  assert(w == &loop->async_io_watcher);

  // 这个 for 循环用来确认是否有 uv_async_send 调用
  for (;;) {
    r = read(w->fd, buf, sizeof(buf));

    if (r == sizeof(buf))
      continue;

    if (r != -1)
      break;

    if (errno == EAGAIN || errno == EWOULDBLOCK)
      break;

    if (errno == EINTR)
      continue;

    abort();
  }
 
  // 交换 loop->async_handle 和 queue内容,避免在遍历 loop->async_handles 时有新的 async_handle 插入到队列
  // loop->async_handles 队列中除了线程池的句柄还有其它的
  QUEUE_MOVE(&loop->async_handles, &queue);
  while (!QUEUE_EMPTY(&queue)) {
    q = QUEUE_HEAD(&queue);
    h = QUEUE_DATA(q, uv_async_t, queue);

    QUEUE_REMOVE(q);
    // 将 uv_async_t 重新插入到 loop->async_handles 中,uv_async_t 需要手动调用 uv__async_stop 才会从队列中移除
    QUEUE_INSERT_TAIL(&loop->async_handles, q);

    // 确认这个 async_handle 是否需要回调
    if (cmpxchgi(&h->pending, 1, 0) == 0)
      continue;

    if (h->async_cb == NULL)
      continue;

    // 调用通过 uv_async_init 初始化 uv_async_t 时绑定的回调函数
    // 线程池的 uv_async_t 是在 uv_loop_init 时初始化的,它绑定的回调是 uv__work_done
    // 因此如果 h == loop->wq_async,这里 h->async_cb 实际是调用了 uv__work_done(h);
    // 详情请参考 https://github.com/libuv/libuv/blob/v1.24.0/src/unix/loop.c#L88
    h->async_cb(h);
  }
}

调用线程池的 h->async_cb后会回到线程池的 uv__work_done函数:

void uv__work_done(uv_async_t* handle) {
  struct uv__work* w;
  uv_loop_t* loop;
  QUEUE* q;
  QUEUE wq;
  int err;

  loop = container_of(handle, uv_loop_t, wq_async);
  uv_mutex_lock(&loop->wq_mutex);
  // 清空已完成的 loop->wq 队列
  QUEUE_MOVE(&loop->wq, &wq);
  uv_mutex_unlock(&loop->wq_mutex);

  while (!QUEUE_EMPTY(&wq)) {
    q = QUEUE_HEAD(&wq);
    QUEUE_REMOVE(q);

    w = container_of(q, struct uv__work, wq);
    // 如果在回调前调用了 uv_cancel 取消请求,则即使请求已经执行完,依旧算出错
    err = (w->work == uv__cancelled) ? UV_ECANCELED : 0;
    w->done(w, err);
  }
}

最后通过 w->done(w, err)回调 uv__fs_done,并由 uv__fs_done回调 JS 函数:

static void uv__fs_done(struct uv__work* w, int status) {
  uv_fs_t* req;

  req = container_of(w, uv_fs_t, work_req);
  uv__req_unregister(req->loop, req);

  // 如果取消了则抛出异常
  if (status == UV_ECANCELED) {
    assert(req->result == 0);
    req->result = UV_ECANCELED;
  }

  // 回调 JS
  req->cb(req);
}

以上就是 libuv 是线程池从创建到执行多线程请求的过程。

fs.access 调用过程分析

再回到文章开头提到的代码,我们来分析它的调用过程。

const fs = require('fs')
const cb = function (err) {
  console.log(`Is myfile exists: ${!err}`)
}
fs.access('myfile', cb)

假设线程池大小为 2 ,下面描述了执行 fs.access时 3 个线程的状态(略过了 Node.js 启动和 JavaScript 和 Native 函数调用过程),时间轴从上到下:

空白代表处于阻塞状态,-代表线程尚未启动

uv loop threadworker thread 1worker thread 2
fs.access(‘myfile’, cb)--
JavaScript 通过 v8 调用 Native 函数--
uv_fs_access--
uv__work_submit--
init_threadsworkerworker
uv_sem_waituv_sem_postuv_sem_post
uv_cond_waituv_cond_wait
uv_cond_signal
uv__io_pollaccess
uv__io_poll
uv__io_polluv_async_send
uv__io_polluv_cond_wait
uv__io_poll
uv__async_io
uv__work_done
uv__fs_done
Native 通过 v8 回调 JavaScript 函数
cb
console.log(`Is myfile exists: ${exists}`)

可以看到调用过程如下:

  1. 通过 Node.js 启动时对 JavaScript 函数与 Native 函数的绑定,fs.access最终会进入到 Native 函数中,而 Native 函数会调用 libuv 的 uv_fs_access函数来判断文件是否可以访问。(这里略过 JavaScript 如何通过 v8 调用 Native 函数)
  2. uv_fs_access在 uv loop 线程向线程池提交了一个多线程请求。
  3. 由于线程池是惰性的,在执行请求前,先进行了初始化线程池的操作。
  4. 线程池初始化完成后唤醒了 worker thread 1去执行请求,同时 uv loop 线程不断的轮询是否完成了请求。
  5. worker thread 1同步的调用 access函数判断目标文件是否可读。
  6. access函数完成后, worker thread 1通过 uv_async_send同步 uv loop 线程请求已完成,同时自身进入阻塞状态,等待新的请求将其唤醒。
  7. uv loop 线程发现请求执行完成后通过一系列回调回到 uv__fs_done
  8. uv__fs_done回调 JavaScript 函数打印日志。(这里略过 uv__fs_done是如何通过 v8 回调到 JavaScript)

整个过程由于没有新的请求进来, worker thread 2始终处于阻塞状态。

结束语

通过对 fs.access的调用过程分析,我们了解了 libuv 是如何通过线程池进行异步调用的。另外也可以看到针对不同的平台,libuv 对 uv__io_poll的实现是不同的,后面我们将介绍 uv__io_poll实现异步 I/O 的方式。

完全用typescript写了个grpc service框架

$
0
0

proto定义好接口即可一条命令生成controller及引用的类型的定义 (class & interface). 效仿egg的『约定优于配置』原则, config, midware, controller, 相关type定义等只要按约定放到相应的文件夹即可.

框架还未单独封装, 现在放在framework目录下. 代码生成器还未单独封装, 现在放在codegen目录下. 这些现在是我的业余兴趣, 会利用闲余时间阶段性添加功能. https://github.com/xiaozhongliu/ts-rpc-seed

代码生成

ts-node codegen

运行服务

# 本地使用vscode的话直接进F5调试typescript
# 或者:
npm run tsc
node dist/app.js

测试请求

ts-node tester
# 或者:
npm run tsc
node dist/tester.js

代码样例

入口app.ts
import App from './framework'
new App().start()
config
export default (appInfo: AppInfo): Config => {
    return {
        // basic
        PORT: 50051,

        // log
        COMMON_LOG_PATH: `${appInfo.rootPath}/log/common`,
        REQUEST_LOG_PATH: `${appInfo.rootPath}/log/request`,
    }
}
midware
import { Context } from '../framework'
import 'dayjs/locale/zh-cn'
import dayjs from 'dayjs'
dayjs.locale('zh-cn')

export default async (ctx: Context, req: object, next: Function) => {
    const start = dayjs()
    await next()
    const end = dayjs()

    ctx.logger.request({
        '@duration': end.diff(start, 'millisecond'),
        controller: `${ctx.controller}.${ctx.action}`,
        metedata: JSON.stringify(ctx.metadata),
        request: JSON.stringify(req),
        response: JSON.stringify(ctx.response),
    })
}
controller
import { Controller, Context } from '../framework'
import HelloReply from '../typings/greeter/HelloReply'

export default class GreeterController extends Controller {

    async sayHello(ctx: Context, req: HelloRequest): Promise<HelloReply> {
        return new HelloReply(
            `Hello ${req.name}`,
        )
    }

    async sayGoodbye(ctx: Context, req: HelloRequest): Promise<HelloReply> {
        return new HelloReply(
            `Goodbye ${req.name}`,
        )
    }
}

请求日志输出类似

image.png

下一项功能

把在这里用的参数校验中间件搬过来, 用class-validator和class-transformer实现这样的效果, 并大部分自动生成:

import { IsOptional, Length, Min, Max, IsBoolean } from 'class-validator'

export default class IndexRequest {
    @Length(4, 8)
    @IsOptional()
    foo: string

    @Min(5)
    @Max(10)
    @IsOptional()
    bar: number

    @IsBoolean()
    @IsOptional()
    baz: boolean
}

What's New in JavaScript

$
0
0

前几天 Google IO 上 V8 团队为我们分享了《What’s New in JavaScript》主题,分享的语速很慢推荐大家可以都去听听就当锻炼下听力了。看完之后我整理了一个文字版帮助大家快速了解分享内容,嘉宾主要是分享了以下几点:

  1. JS 解析快了 2 倍
  2. async 执行快了 11 倍
  3. 平均减少了 20% 的内存使用
  4. class fileds 可以直接在 class 中初始化变量不用写在 constructor 里
  5. 私有变量前缀
  6. string.matchAll 用来做正则多次匹配
  7. numeric seperator 允许我们在写数字的时候使用 _ 作为分隔符提高可读性
  8. bigint 新的大数字类型支持
  9. Intl.NumberFormat 本地化格式化数字显示
  10. Array.prototype.flat(), Array.prototype.flatMap() 多层数组打平方法
  11. Object.entries() 和 Object.fromEntries() 快速对对象进行数组操作
  12. globalThis 无环境依赖的全局 this 支持
  13. Array.prototype.sort() 的排序结果稳定输出
  14. Intl.RelativeTimeFormat(), Intl.DateTimeFormat() 本地化显示时间
  15. Intl.ListFormat() 本地化显示多个名词列表
  16. Intl.locale() 提供某一本地化语言的各种常量查询
  17. 顶级 await 无需写 async 的支持
  18. Promise.allSettled() 和 Promise.any() 的增加丰富 Promise 场景
  19. WeakRef 类型用来做部分变量弱引用减少内存泄露

<!–more–>

Async 执行比之前快了11倍

开场就用 11x faster数字把大家惊到了,也有很多同学好奇到底是怎么做到的。其实这个优化并不是最近做的,去年11月的时候 V8 团队就发了一篇文章 《Faster async functions and promises》,这里面就非常详尽的讲述了如何让 async/await 优化到这个速度的,其主要归功于以下三点:

  • TurboFan:新的 JS 编译器
  • Orinoco:新的 GC 引擎
  • Node.js 8 上的一个 await bug

2008年 Chrome 出世,10年 Chrome 引入了 Crankshaft 编译器,多年后的今天这员老将已经无法满足现有的优化需求,毕竟当时的作者也未曾料想到前端的世界会发展的这么快。关于为何使用 TurboFan 替换掉 Crankshaft,大家可以看看《Launching Ignition and TurboFan》,原文中是这么说的:

Crankshaft 仅支持优化 JavaScript 的一部分特性。它并没有通过结构化的异常处理来设计代码,即代码块并没有通过try、catch、finally等关键字划分。另外由于为每一个新的特性Cranksshaft都将要做九套不同的框架代码适应不同的平台,因此在适配新的Javascript语言特性也很困难。还有Crankshaft框架代码的设计也限制优化机器码的扩展。尽管V8引擎团队为每一套芯片架构维护超过一万行代码,Crankshaft也不过为Javascript挤出一点点性能。 via:《Javascript是如何工作的:V8引擎的内核Ignition和TurboFan》

而 TurboFan 则提供了更好的架构,能够在不修改架构的情况下添加新的优化特性,这为面向未来优化 JavaScript 语言特性提供了很好的架构支持,能让团队花费更少的时间在做处理不同平台的特性和编码上。从原文的数据对比中就可以看到,仅仅是换了个编译器优化就在 8 倍左右了…… 给 V8 的大佬们跪下了。

而 Orinoco 新的 GC 引擎则是使用单独线程异步去处理,让其不影响 JS 主线程的执行。至于最后说的 async/await 的 BUG 则是让 V8 团队引发思考,将 async/await 原本基于 3 个 Promise 的实现减少成 2 个,最终减少成 1 个!最后达到了写 async/await 比直接写 Promise 还要快的结果。

我们知道 await后面跟的是 Promise 对象,但是即使不是 Promise JS 也会帮我们将其包装成 Promise。而在 V8 引擎中,为了实现这个包装,至少需要一个 Promise,两个微任务过程。这个在本身已经是 Promise 的情况下就有点亏大发了。而为了实现 async/await 在 await 结束之后要重新原函数的上下文并提供 await之后的结果,规范定义此时还需要一个 Promise,这个在 V8 团队看来是没有必要的,所以他们建议规范去除这个特性

最后的最后,官方还建议我们:多使用 async/await 而不是手写 Promise 代码,多使用 JavaScript 引擎提供的 Promise 而不是自己去实现。

Numeric Seperator

随着 Babel 的出现,JS 的语法糖简直不要太多,Numeric Seperator 算是一个。简单的说来它为我们手写数字的时候提供给了分隔符的支持,让我们在写大数字的时候具有可读性。

其实是个很简单的语法糖,为什么我会单独列出来说呢,主要是因为它正好解决了我之前一个实现的痛点。我有一个需求是一堆文章数据,我要按照产品给的规则去插入广告。如图非红框是文章,红框处是广告。由于插入规则会根据产品的需(心)求(情)频繁变化,所以我们最开始使用了两个变量进行标记:

const news = [1, 3, 5, 6, 7, 9, 10, 11];
const ads = [2, 4, 8, 12];

当位置发生变化的时候我们就需要同时对两个变量进行修改,这样导致了维护上的成本。所以我想了一个办法,广告的位置标记为 1,文章的位置标记为 0,使用纯二进制的形式来表示个记录,这样子就变成了:

+---+---+---+
| 0 | 1 | 0 |
+---+---+---+
| 1 | 0 | 0 |
+---+---+---+
| 0 | 1 | 0 |
+---+---+---+
| 0 | 0 | 1 |
+---+---+---+

1 011 010 100 010 001
// 首位为常量 1
// 2-4 位记录一行多少条
// 后续按照新闻和广告的位置进行记录

最后我们使用一个变量 0b1011010100010001就完成了两种信息的记录。这样做将很多数据集成在了一起解决了我们之前的问题,但是它带来了新的问题,大家也可以看到注释中按照空格劈开的话大家看的还比较明白,但是在段头将空格去除之后在阅读程度上就造成了非常大的困难了。而数字分隔符这个语法糖正好就能解决这个问题,0b1_011_010_100_010_001这样阅读起来就好很多了。

Promise

虽然在大部分的场景 async/await 都可以了,但是不好意思 Promise 有些场景还是不可替代的。Promsie.all()Promise.race()就是这种特别的存在。而 Promise.allSettled()Promise.any()则是新增加的方法, 相较于它们的前辈,这俩拥有忽略错误达到目的的特性。

我们之前有一个需求,是需要将文件安全的存储在一个存储服务中,为了灾备我们其实有两个 S3,一个 HBase 还有本地存储。所以每次都需要诸如以下的逻辑:

for(const service of services) {
  const result = await service.upload(file);
  if(result) break;
}

但其实我并不关心错误,我的目的是只要保证有一个服务最终能执行成功即可,所以 Promise.any()其实就可以解决这个问题。

await Promise.any( services.map(service => service.upload(file)) );

Promise.allsettled()Promise.any()的引入丰富了 Promise 更多的可能性。说不定以后还会增加更多的特性,例如 Promise.try(), Promise.some(), Promise.reduce()

WeakRef

WeakRef 这个新类型我最开始是不太理解的,毕竟我总感觉 Chrome 已经长大了,肯定会自己处理垃圾了。然而事情并没有我想的那么简单,我们知道 JS 的垃圾回收主要有“标记清除”和“引用计数”两种方法。引用计数是只要变量多一次引用则计数加 1,少一次引用则计数减 1,当引用为 0 时则表示你已经没有利用价值了,去垃圾站吧!

在 WeakRef 之前其实已经有两个类似的类型存在了,那就是 WeakMap 和 WeakSet。以 WeakMap 为例,它规定了它的 Key 必须是对象,而且它的对象都是弱引用的。举个例子:

//map.js
function usageSize() {
  const used = process.memoryUsage().heapUsed;
  return Math.round(used / 1024 / 1024 * 100) / 100 + 'M';
}

global.gc();
console.log(usageSize()); // ≈ 3.23M

let arr = new Array(5 * 1024 * 1024);
const map = new WeakMap();

map.set(arr, 1);
global.gc();
console.log(usageSize()); // ≈ 43.22M

arr = null;
global.gc();
console.log(usageSize()); // ≈ 43.23M
//weakmap.js
function usageSize() {
  const used = process.memoryUsage().heapUsed;
  return Math.round(used / 1024 / 1024 * 100) / 100 + 'M';
}

global.gc();
console.log(usageSize()); // ≈ 3.23M

let arr = new Array(5 * 1024 * 1024);
const map = new WeakMap();

map.set(arr, 1);
global.gc();
console.log(usageSize()); // ≈ 43.22M

arr = null;
global.gc();
console.log(usageSize()); // ≈ 3.23M

分别执行 node --expose-gc map.jsnode --expose-gc weakmap.js就可以发现区别了。在 arr 和 Map 中都保留了数组的强引用,所以在 Map 中简单的清除 arr 变量内存并没有得到释放,因为 Map 还存在引用计数。而在 WeakMap 中,它的键是弱引用,不计入引用计数中,所以当 arr 被清除之后,数组会因为引用计数为0而被回收掉。

正如分享中所说,WeakMap 和 WeakSet 足够好,但是它要求键必须是对象,在某些场景上不太试用。所以他们暴露了更方便的 WeakRef 类型。在 Python 中也存在 WeakRef 类型,干的事情比较类似。其实我们主要注意 WeakRef 的引用是不计引用计数的,就好理解了。例如 MDN中所说的引用计数没办法清理的循环引用问题:

function f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o

  return "azerty";
}

f();

如果试用 WeakRef 来改写,由于 WeakRef 不计算引用计数,所以计数一直为 0,在下一次回收中就会被正常回收了。

function f() {
  var o = new WeakRef({});
  var o2 = o;
  o.a = o2;

  return "azerty";
}

f();

在之前的一个多进程需求中,我们需要将子进程中的数据发送到主进程中,我们使用的方式是这样写的:

const metric = 'event';
global.DATA[metric] = {};

process.on(metric, () => {
  const data = global.DATA[metric];
  delete global.DATA[metric];
  return data;
});

代码就看着比较怪,由于 global.DATA[metric]作为强引用,如果直接在事件中 return global.DATA[metric]的话,由于存在引用计数,那么这个全局变量是一直占用内存的。此时如果使用 WeakRef 改写一下就可以减少 delete的逻辑了。

const metric = 'event';
global.DATA[metric] = new WeakRef({});

process.on(metric, () => {
  const ref = global.DATA[metric];
  if(ref !== undefined) {
    return ref.deref();
  }
  return ref;
});

后记

除了我上面讲的几个特性之外,还有很多其他的特性也非常一颗赛艇。例如 String.matchAll()让我们做多次匹配的时候再也不用写 while 了!Intl 本地化类的支持,让我们可以早日抛弃 moment.js,特别是 RelativeTimeFormat类真是解放了我们的生产力,不过目前接口的配置似乎比较定制化,不知道后续的细粒度需求支持情况会如何。

参考资料:

使用 Node.js 写一个代码生成器

$
0
0

背景

第一次接触代码生成器用的是动软代码生成器,数据库设计好之后,一键生成后端 curd代码。之后也用过 CodeSmith , T4。目前市面上也有很多优秀的代码生成器,而且大部分都提供可视化界面操作。

自己写一个的原因是因为要集成到自己写的一个小工具中,而且使用 Node.js 这种动态脚本语言进行编写更加灵活。

原理

代码生成器的原理就是:数据 + 模板 => 文件

数据一般为数据库的表字段结构。

模板的语法与使用的模板引擎有关。

使用模板引擎将数据模板进行编译,编译后的内容输出到文件中就得到了一份代码文件。

功能

因为这个代码生成器是要集成到一个小工具 lazy-mock内,这个工具的主要功能是启动一个 mock server 服务,包含curd功能,并且支持数据的持久化,文件变化的时候自动重启服务以最新的代码提供 api mock 服务。

代码生成器的功能就是根据配置的数据和模板,编译后将内容输出到指定的目录文件中。因为添加了新的文件,mock server 服务会自动重启。

还要支持模板的定制与开发,以及使用 CLI 安装模板。

可以开发前端项目的模板,直接将编译后的内容输出到前端项目的相关目录下,webpack 的热更新功能也会起作用。

模板引擎

模板引擎使用的是 nunjucks

lazy-mock使用的构建工具是 gulp,使用 gulp-nodemon 实现 mock-server 服务的自动重启。所以这里使用 gulp-nunjucks-render 配合 gulp 的构建流程。

代码生成

编写一个 gulp task :

const rename = require('gulp-rename')
const nunjucksRender = require('gulp-nunjucks-render')
const codeGenerate = require('./templates/generate')
const ServerFullPath = require('./package.json').ServerFullPath; //mock -server项目的绝对路径
const FrontendFullPath = require('./package.json').FrontendFullPath; //前端项目的绝对路径
const nunjucksRenderConfig = {
  path: 'templates/server',
  envOptions: {
    tags: {
      blockStart: '<%',
      blockEnd: '%>',
      variableStart: '<$',
      variableEnd: '$>',
      commentStart: '<#',
      commentEnd: '#>'
    },
  },
  ext: '.js',
  //以上是 nunjucks 的配置
  ServerFullPath,
  FrontendFullPath
}
gulp.task('code', function () {
  require('events').EventEmitter.defaultMaxListeners = 0
  return codeGenerate(gulp, nunjucksRender, rename, nunjucksRenderConfig)
});

代码具体结构细节可以打开 lazy-mock进行参照

为了支持模板的开发,以及更灵活的配置,我将代码生成的逻辑全都放在模板目录中。

templates是存放模板以及数据配置的目录。结构如下:

只生成 lazy-mock 代码的模板中 :

generate.js的内容如下:

const path = require('path')
const CodeGenerateConfig = require('./config').default;
const Model = CodeGenerateConfig.model;

module.exports = function generate(gulp, nunjucksRender, rename, nunjucksRenderConfig) {
    nunjucksRenderConfig.data = {
        model: CodeGenerateConfig.model,
        config: CodeGenerateConfig.config
    }
    const ServerProjectRootPath = nunjucksRenderConfig.ServerFullPath;
    //server
    const serverTemplatePath = 'templates/server/'
    gulp.src(`${serverTemplatePath}controller.njk`)
        .pipe(nunjucksRender(nunjucksRenderConfig))
        .pipe(rename(Model.name + '.js'))
        .pipe(gulp.dest(ServerProjectRootPath + CodeGenerateConfig.config.ControllerRelativePath));

    gulp.src(`${serverTemplatePath}service.njk`)
        .pipe(nunjucksRender(nunjucksRenderConfig))
        .pipe(rename(Model.name + 'Service.js'))
        .pipe(gulp.dest(ServerProjectRootPath + CodeGenerateConfig.config.ServiceRelativePath));

    gulp.src(`${serverTemplatePath}model.njk`)
        .pipe(nunjucksRender(nunjucksRenderConfig))
        .pipe(rename(Model.name + 'Model.js'))
        .pipe(gulp.dest(ServerProjectRootPath + CodeGenerateConfig.config.ModelRelativePath));

    gulp.src(`${serverTemplatePath}db.njk`)
        .pipe(nunjucksRender(nunjucksRenderConfig))
        .pipe(rename(Model.name + '_db.json'))
        .pipe(gulp.dest(ServerProjectRootPath + CodeGenerateConfig.config.DBRelativePath));

    return gulp.src(`${serverTemplatePath}route.njk`)
        .pipe(nunjucksRender(nunjucksRenderConfig))
        .pipe(rename(Model.name + 'Route.js'))
        .pipe(gulp.dest(ServerProjectRootPath + CodeGenerateConfig.config.RouteRelativePath));
}

类似:

gulp.src(`${serverTemplatePath}controller.njk`)
        .pipe(nunjucksRender(nunjucksRenderConfig))
        .pipe(rename(Model.name + '.js'))
        .pipe(gulp.dest(ServerProjectRootPath + CodeGenerateConfig.config.ControllerRelativePath));

表示使用 controller.njk 作为模板,nunjucksRenderConfig作为数据(模板内可以获取到 nunjucksRenderConfig 属性 data 上的数据)。编译后进行文件重命名,并保存到指定目录下。

model.js的内容如下:

var shortid = require('shortid')
var Mock = require('mockjs')
var Random = Mock.Random

//必须包含字段id
export default {
    name: "book",
    Name: "Book",
    properties: [
        {
            key: "id",
            title: "id"
        },
        {
            key: "name",
            title: "书名"
        },
        {
            key: "author",
            title: "作者"
        },
        {
            key: "press",
            title: "出版社"
        }
    ],
    buildMockData: function () {//不需要生成设为false
        let data = []
        for (let i = 0; i < 100; i++) {
            data.push({
                id: shortid.generate(),
                name: Random.cword(5, 7),
                author: Random.cname(),
                press: Random.cword(5, 7)
            })
        }
        return data
    }
}

模板中使用最多的就是这个数据,也是生成新代码需要配置的地方,比如这里配置的是 book ,生成的就是关于 book 的curd 的 mock 服务。要生成别的,修改后执行生成命令即可。

buildMockData 函数的作用是生成 mock 服务需要的随机数据,在 db.njk 模板中会使用:

{
  "<$ model.name $>":<% if model.buildMockData %><$ model.buildMockData()|dump|safe $><% else %>[]<% endif %>
}

这也是 nunjucks 如何在模板中执行函数

config.js的内容如下:

export default {
    //server
    RouteRelativePath: '/src/routes/',
    ControllerRelativePath: '/src/controllers/',
    ServiceRelativePath: '/src/services/',
    ModelRelativePath: '/src/models/',
    DBRelativePath: '/src/db/'
}

配置相应的模板编译后保存的位置。

config/index.js的内容如下:

import model from './model';
import config from './config';
export default {
    model,
    config
}

针对 lazy-mock 的代码生成的功能就已经完成了,要实现模板的定制直接修改模板文件即可,比如要修改 mock server 服务 api 的接口定义,直接修改 route.njk 文件:

import KoaRouter from 'koa-router'
import controllers from '../controllers/index.js'
import PermissionCheck from '../middleware/PermissionCheck'

const router = new KoaRouter()
router
    .get('/<$ model.name $>/paged', controllers.<$model.name $>.get<$ model.Name $>PagedList)
    .get('/<$ model.name $>/:id', controllers.<$ model.name $>.get<$ model.Name $>)
    .del('/<$ model.name $>/del', controllers.<$ model.name $>.del<$ model.Name $>)
    .del('/<$ model.name $>/batchdel', controllers.<$ model.name $>.del<$ model.Name $>s)
    .post('/<$ model.name $>/save', controllers.<$ model.name $>.save<$ model.Name $>)

module.exports = router

模板开发与安装

不同的项目,代码结构是不一样的,每次直接修改模板文件会很麻烦。

需要提供这样的功能:针对不同的项目开发一套独立的模板,支持模板的安装。

代码生成的相关逻辑都在模板目录的文件中,模板开发没有什么规则限制,只要保证目录名为 templatesgenerate.js中导出generate函数即可。

模板的安装原理就是将模板目录中的文件全部覆盖掉即可。不过具体的安装分为本地安装与在线安装。

之前已经说了,这个代码生成器是集成在 lazy-mock中的,我的做法是在初始化一个新 lazy-mock项目的时候,指定使用相应的模板进行初始化,也就是安装相应的模板。

使用 Node.js 写了一个 CLI 工具 lazy-mock-cli,已发到 npm ,其功能包含下载指定的远程模板来初始化新的 lazy-mock 项目。代码参考( copy )了 vue-cli2。代码不难,说下某些关键点。

安装 CLI 工具:

npm install lazy-mock -g

使用模板初始化项目:

lazy-mock init d2-admin-pm my-project

d2-admin-pm是我为一个前端项目已经写好的一个模板。

init命令调用的是 lazy-mock-init.js中的逻辑:

#!/usr/bin/env node
const download = require('download-git-repo')
const program = require('commander')
const ora = require('ora')
const exists = require('fs').existsSync
const rm = require('rimraf').sync
const path = require('path')
const chalk = require('chalk')
const inquirer = require('inquirer')
const home = require('user-home')
const fse = require('fs-extra')
const tildify = require('tildify')
const cliSpinners = require('cli-spinners');
const logger = require('../lib/logger')
const localPath = require('../lib/local-path')

const isLocalPath = localPath.isLocalPath
const getTemplatePath = localPath.getTemplatePath

program.usage('<template-name> [project-name]')
    .option('-c, --clone', 'use git clone')
    .option('--offline', 'use cached template')

program.on('--help', () => {
    console.log('  Examples:')
    console.log()
    console.log(chalk.gray('    # create a new project with an official template'))
    console.log('    $ lazy-mock init d2-admin-pm my-project')
    console.log()
    console.log(chalk.gray('    # create a new project straight from a github template'))
    console.log('    $ vue init username/repo my-project')
    console.log()
})

function help() {
    program.parse(process.argv)
    if (program.args.length < 1) return program.help()
}
help()
//模板
let template = program.args[0]
//判断是否使用官方模板
const hasSlash = template.indexOf('/') > -1
//项目名称
const rawName = program.args[1]
//在当前文件下创建
const inPlace = !rawName || rawName === '.'
//项目名称
const name = inPlace ? path.relative('../', process.cwd()) : rawName
//创建项目完整目标位置
const to = path.resolve(rawName || '.')
const clone = program.clone || false

//缓存位置
const serverTmp = path.join(home, '.lazy-mock', 'sever')
const tmp = path.join(home, '.lazy-mock', 'templates', template.replace(/[\/:]/g, '-'))
if (program.offline) {
    console.log(`> Use cached template at ${chalk.yellow(tildify(tmp))}`)
    template = tmp
}

//判断是否当前目录下初始化或者覆盖已有目录
if (inPlace || exists(to)) {
    inquirer.prompt([{
        type: 'confirm',
        message: inPlace
            ? 'Generate project in current directory?'
            : 'Target directory exists. Continue?',
        name: 'ok'
    }]).then(answers => {
        if (answers.ok) {
            run()
        }
    }).catch(logger.fatal)
} else {
    run()
}

function run() {
    //使用本地缓存
    if (isLocalPath(template)) {
        const templatePath = getTemplatePath(template)
        if (exists(templatePath)) {
            generate(name, templatePath, to, err => {
                if (err) logger.fatal(err)
                console.log()
                logger.success('Generated "%s"', name)
            })
        } else {
            logger.fatal('Local template "%s" not found.', template)
        }
    } else {
        if (!hasSlash) {
            //使用官方模板
            const officialTemplate = 'lazy-mock-templates/' + template
            downloadAndGenerate(officialTemplate)
        } else {
            downloadAndGenerate(template)
        }
    }
}

function downloadAndGenerate(template) {
    downloadServer(() => {
        downloadTemplate(template)
    })
}

function downloadServer(done) {
    const spinner = ora('downloading server')
    spinner.spinner = cliSpinners.bouncingBall
    spinner.start()
    if (exists(serverTmp)) rm(serverTmp)
    download('wjkang/lazy-mock', serverTmp, { clone }, err => {
        spinner.stop()
        if (err) logger.fatal('Failed to download server ' + template + ': ' + err.message.trim())
        done()
    })
}

function downloadTemplate(template) {
    const spinner = ora('downloading template')
    spinner.spinner = cliSpinners.bouncingBall
    spinner.start()
    if (exists(tmp)) rm(tmp)
    download(template, tmp, { clone }, err => {
        spinner.stop()
        if (err) logger.fatal('Failed to download template ' + template + ': ' + err.message.trim())
        generate(name, tmp, to, err => {
            if (err) logger.fatal(err)
            console.log()
            logger.success('Generated "%s"', name)
        })
    })
}

function generate(name, src, dest, done) {
    try {
        fse.removeSync(path.join(serverTmp, 'templates'))
        const packageObj = fse.readJsonSync(path.join(serverTmp, 'package.json'))
        packageObj.name = name
        packageObj.author = ""
        packageObj.description = ""
        packageObj.ServerFullPath = path.join(dest)
        packageObj.FrontendFullPath = path.join(dest, "front-page")
        fse.writeJsonSync(path.join(serverTmp, 'package.json'), packageObj, { spaces: 2 })
        fse.copySync(serverTmp, dest)
        fse.copySync(path.join(src, 'templates'), path.join(dest, 'templates'))
    } catch (err) {
        done(err)
        return
    }
    done()
}

判断了是使用本地缓存的模板还是拉取最新的模板,拉取线上模板时是从官方仓库拉取还是从别的仓库拉取。

一些小问题

目前代码生成的相关数据并不是来源于数据库,而是在 model.js中简单配置的,原因是我认为一个 mock server 不需要数据库,lazy-mock 确实如此。

但是如果写一个正儿八经的代码生成器,那肯定是需要根据已经设计好的数据库表来生成代码的。那么就需要连接数据库,读取数据表的字段信息,比如字段名称,字段类型,字段描述等。而不同关系型数据库,读取表字段信息的 sql 是不一样的,所以还要写一堆balabala的判断。可以使用现成的工具 sequelize-auto , 把它读取的 model 数据转成我们需要的格式即可。

生成前端项目代码的时候,会遇到这种情况:

某个目录结构是这样的:

index.js的内容:

import layoutHeaderAside from '@/layout/header-aside'
export default {
    "layoutHeaderAside": layoutHeaderAside,
    "menu": () => import(/* webpackChunkName: "menu" */'@/pages/sys/menu'),
    "route": () => import(/* webpackChunkName: "route" */'@/pages/sys/route'),
    "role": () => import(/* webpackChunkName: "role" */'@/pages/sys/role'),
    "user": () => import(/* webpackChunkName: "user" */'@/pages/sys/user'),
    "interface": () => import(/* webpackChunkName: "interface" */'@/pages/sys/interface')
}

如果添加一个 book 就需要在这里加上"book": () => import(/* webpackChunkName: "book" */'@/pages/sys/book')

这一行内容也是可以通过配置模板来生成的,比如模板内容为:

"<$ model.name $>": () => import(/* webpackChunkName: "<$ model.name $>" */'@/pages<$ model.module $><$ model.name $>')

但是生成的内容怎么加到index.js中呢?

第一种方法:复制粘贴

第二种方法:

这部分的模板为 routerMapComponent.njk

export default {
    "<$ model.name $>": () => import(/* webpackChunkName: "<$ model.name $>" */'@/pages<$ model.module $><$ model.name $>')
}

编译后文件保存到 routerMapComponents 目录下,比如 book.js

修改 index.js :

const files = require.context('./', true, /\.js$/);
import layoutHeaderAside from '@/layout/header-aside'

let componentMaps = {
    "layoutHeaderAside": layoutHeaderAside,
    "menu": () => import(/* webpackChunkName: "menu" */'@/pages/sys/menu'),
    "route": () => import(/* webpackChunkName: "route" */'@/pages/sys/route'),
    "role": () => import(/* webpackChunkName: "role" */'@/pages/sys/role'),
    "user": () => import(/* webpackChunkName: "user" */'@/pages/sys/user'),
    "interface": () => import(/* webpackChunkName: "interface" */'@/pages/sys/interface'),
}
files.keys().forEach((key) => {
    if (key === './index.js') return
    Object.assign(componentMaps, files(key).default)
})
export default componentMaps

使用了 require.context

我目前也是使用了这种方法

第三种方法:

开发模板的时候,做特殊处理,读取原有 index.js 的内容,按行进行分割,在数组的最后一个元素之前插入新生成的内容,注意逗号的处理,将新数组内容重新写入 index.js 中,注意换行。

打个广告

如果你想要快速的创建一个 mock-server,同时还支持数据的持久化,又不需要安装数据库,还支持代码生成器的模板开发,欢迎试试 lazy-mock

有必要显式地设置content-length吗?

$
0
0

我之前写代码从来没有设置过content-length,最近看koa-send这个包,在index.js 130行的地方有这样一行代码 ctx.set(‘Content-Length’, stats.size) 但是我把这行取消后用postman请求,发现没有差别,代码也能跑,读到地content-length。 现在问问各位大佬,是否有必要显式地设置content-length吗?


表白利器 ball rotating

$
0
0

表白利器 ball rotating

动画展示

为了节约gif的大小,只录制了部分

在线浏览

3个动画组成,和2个过渡时期

1、开始部分,5个球从左边移动到右边,然后在距离80的地方停止

function animation(ball, marginLeft) {
  return new Promise(function (resolve, reject) {
    function repeat() {
      var ballLeft = parseInt(ball.style.left);
      if (ballLeft == marginLeft) {
        resolve()
      } else {
        if (ballLeft > marginLeft) {
          ballLeft--;
        } else {
          ballLeft++;
        }
        ball.style.left = ballLeft + 'px';
        setTimeout(function() {
          repeat(ball, marginLeft);
        }, 20)
      }
    }
    setTimeout(repeat, 20);
  })
}

;(async function () {
  await animation(ball1, 30)
  await animation(ball2, 130)
  await animation(ball3, 230)
  await animation(ball3, 80)
  await animation(ball2, 80)
  await animation(ball1, 80)
  await animation(ball4, 80)
  await animation(ball5, 80)
  $(".ballWrap").addClass("swiperRotate");
  allTextRoate();
  setTimeout(function(){
    fallDown(0, 0, 0);
  }, 3000);
})();

通过js,每次left+1,控制5个球在80位置停下

2、在水平方向平移

function fallDown(index,stopTop,startSpeed){
  if(index==1){
    stopTop=-159;
    startSpeed=-159;       
  }
  var nowSpeed=startSpeed;
  var ele=$(".ball").eq(index);
  $(".ball").removeClass("zIndex100")
  ele.addClass("zIndex100");
  var timer=setInterval(function(){
    if(getHeight(ele)<=160&&nowSpeed>=0){
      nowSpeed++;
      ele.css("top",nowSpeed+'px');
      if(nowSpeed==160){
        nowSpeed=-nowSpeed;
      }
    }else{
      //clearInterval(timer);
      nowSpeed++;
      ele.css("top",-nowSpeed+'px');
      //console.log(nowSpeed,stopTop);
      if(nowSpeed==stopTop){
        clearInterval(timer);
        if(index===1){
          ele.css("top",160+'px');
          returnBack();
          return
        }
        stopTop=stopTop-40;  
        if (index === 0) {
          index = 3
        } else if (index === 3) {
          index = 2
        } else if (index === 2) {
          index = 4
        } else if (index === 4) {
          index = 1
        }
        startSpeed = stopTop;
        fallDown(index, stopTop, startSpeed);
      }
    }
  }, 20);
}

水平移动时需要注意球的顺序是0,3,2,4,1。因为要与最后的rotating结合,球的顺序是打乱的,移动的时候zIndex也要改变

3、无限rotating,这个是我看一个gif运动之后想到自己怎么通过代码实现,就使用上了

其实是3个正方形的运动,1号球和5号球一组,2号球和4号球一组,3号球单独一组

github代码

表白利器,欢迎fork和star

异步编程总结-4

$
0
0

该文章阅读需要5分钟,更多文章请点击本人博客halu886

异步编程解决方案

Promise/Deferred模式

使用事件的方式时,执行流程都需要预先设定好,并且包括相关分支的逻辑,这是由于发布/订阅模式决定的。

$.get('/api',{
    success: onSuccess,
    error: onError,
    complete: onComplete
})

上面的方式,必须要提前设计好如何处理目标。但是如果使用Promise/Deferred模式,则可以实现先执行异步调用,延迟传递处理。

Promise/Deferred模式被JQuery1.5中广为人知。Ajax经过重写后,调用Ajax可以通过如下调用。

$.get('/api')
  .success(onSuccess)
  .error(onError)
  .complete(onComplete)

这个方式比预先传入一个对象的方式更加灵活,并且不调用success()error(),complete(),请求也会照常调用。而且原先一个事件只能处理一个回调,但是如下所示,可以对事件加入任意多个业务逻辑。

$.get('/api/)
  .success(onSuccess1)
  .success(onSuccess2);

Promise/Deferred在2009年被抽象Promise/A,Promise/B,Promise/D 这样典型的Promise/Deferred 异步模型作为草案发布在CommonJs规范中。使得异步调用更加优雅。

异步的广度使得回调的增加,一旦回调增加,编程体验将会变的很不愉快,但是Promise/Deferred 模式在一定程度上缓解了这个问题。

Promise/A

Promise/A提议对单个异步操作做出如下定义。

  • Promise只会存在以下三个状态:未完成态,完成态和失败态。
  • Promise的状态只会出现从未完成态转向完成态或失败态。不能逆转,完成态和失败态不能相互转化。
  • Promise的状态一旦改变,将不能改变。

1

Promise/A 模式的API的定义比较简单,Promise 对象存在then()方法即可。不过then()方法有如下要求。

  • 接受完成态或错误态的回调方法,当操作完成时或者错误时,调用相应的方法。
  • 可选的支持progress事件回调作为第三个方法。
  • then()只接受function对象,其他对象将会忽略。
  • then()返回Promise,支持链式调用。

定义如下 then(fulfilledHandler,errorHandler,progressHandler)

var Promise = function(){
    EventEmitter.call(this);
}
util.inherits(Promise,EventEmitter);

Promise.prototype.then = function(fulfillHandler,errorHandler,progressHandler){
    if(typeof fulfilledHandler === 'function'){
        this.once('success',fulfilledHandler);
    }
    if(typeof errorHandler === 'function'){
        this.once('error',errorHandler);
    }
    if(typeof progressHandler === 'function'){
        this.on('progress',progressHandler);
    }
    return this;
};

then()方法所做的工作就是存放这些回调函数。为了完成整个流程,还需要触发这些回调,我们把这些对象称为Deferred,即延时对象。

var Deferred = function(){
    this.state = 'nufulfilled';
    this.promise = new Promise();
}

Deferred.prototype.resolve = function(obj){
    this.state= 'fulfilled';
    this.promise.emit('success',obj);
}

Deferred.prototype.reject = function(err){
    this.state = 'failed';
    this.promise.emit('error',err);
}

Deferred.prototype.progress = function(data){
    this.promise.emit('progress',data);
}

这里的状态和方法的关系如图所示: 2

我们可以根据典型的响应对象进行封装。

res.setEncoding('utf8');
res.on('data',function(chunk){
    console.log('BODY:' + chunk);
});
res.on('end',function(){
    // Done
})
res.on('error',function(err){
    // Error
})

转换如下

res.then(function(){
    //Done
},function(err){
    //Error
},function(chunk){
    console.log('BODY:' + chunk);
})

改造代码如下

var promisify = function(res){
    var deferred = new Deferred();
    var result = '';
    res.on('data',function(chunk){
        result += chunk;
        deferred.progress(chunk);
    });
    res.on('end',function(){
        deferred.resolve(result);
    });
    res.on('error',function(err){
        deferred.reject(err);
    });
    return deferred.promise;
}

如此便得到了简单的结果,返回promise 的目的是为了不让外部使用reject 和resolve。更改内部状态由定义者处理。

promisify(res).then(function(){
    // Done
},function (err){
    // Error
},function (chunk){
    // progress
    console.log('BODY:' + chunk);
})

Deferred主要是用于处理内部状态变化,维护异步模型的状态。Promise用于外部,通过then()方法暴露给外部添加自定义方法。

Promise 和Deferred的整体关系如图所示。

3

与事件订阅/发布模式相比,Promise/Deferred 模式的API接口和抽象模型变得更加灵活和简单。将业务中不变的封装在Deferred中,变化的封装在Promise中,在不同的场景下封装和改造其Deferred,当场景不常用,则需要好好取舍封装的成本和带来的简介的关系了。

Promise是高级接口,事件是低级接口。低级接口可以构造更加复杂的业务场景,高级接口一旦定义不容易变化,但是对于解决复杂问题更加有效。

Promise中多异步协作

介绍Promise提到过,主要是用于解决单个异步操作时的问题,但是当多个异步操作时该怎么处理。

类似EventProxy,这里给出了相对简单的原型实现。

Deferred.prototype.all = function(promises){
    var count = promises.length;
    var that = this;
    var results = [];
    promises.forEach(function(promise,i){
        promise.then(function(data){
            count--;
            results[i] = data;
            if(count === 0){
                that.resole(results);
            }
        },function(err){
            that.reject(err);
        });
    });
    return this.promise;
}

如下,读取多个文件时,all 将多个Promise 重新抽象组合为一个Promise。

var promise1 = readFile("foo.txt","utf-8");
var promise2 = readFile("bar.txt","utf-8");
var deferred = new Deferred();
deferred.all([promise1,promise2]).then(function(results){
    // TODO
},function(err){
    // TODO
});

这里all()方法抽象为多个异步操作,只有当所有的异步操作成功时,才算成功,如果一个异步操作失败,整个异步操作就算失败。

以上知识点均来自<<深入浅出Node.js>>,更多细节建议阅读书籍:-)

eggjs 中如何设置响应超时?

$
0
0

就像expressjs 的 connect-timeout npm包

electron安装ffi模块失败,报 MSB4019错误,请有经验的大佬指点一下

$
0
0

基础环境: window10 64位 node: 12.1.0 python 2.7.15 node-gyp : 4.0.0

操作步骤: 在基于node: 12.1.0的背景下 1、npm install –global –production windows-build-tools – 安装过 检查python的系统环境,存在,自动写入了 python环境变量.png 2、npm config set python C:\Users\Administrator.windows-build-tools\python27\python.exe – 我自己python的安装路径 3、设置 npm config set msvs_version 2015 此时 phython 全局环境正常,可以 python -v 查看到 4、安装 node-gyp npm install -g node-gyp 这里自动安装的是 4.0.0版本 5、创建一个空目录 mkdir test cd test npm init -y npm install electron npm install electron-rebuild 6、修改 package.json文件 “scripts”: { “start”: “electron .”, “build”: “electron-rebuild build-app” }, 7、安装 ffi模块,报错 npm install ffi -D

错误如下

ffi-错误截图.png

ffi-error-2.png

不知道哪里出了问题,请问有人遇到同样的问题吗? 望不吝解答,谢谢

写了一个有(gao)颜值喜马拉雅 FM 桌面客户端,支持 Mac、Win 和 Linux

$
0
0

前言

最近一个月沉迷喜马拉雅无法自拔,听相声、段子、每日新闻,还有英语听力,摸鱼学习两不误。上班时候苦于没有桌面端,用网页版有些 bug,官方也不搞一个,只好自己动手了。

样式参考了一下 Moon FM /t/555343,颜值还过得去,自我感觉挺好

Current ReleaseLicenseBuild Status

基于 Electron, Umi, Dva, Antd构建

预览

截图

功能

  • 一个基本的音乐播放器
  • 每日必听
  • 推荐
  • 排行榜
  • 分类
  • 订阅
  • 听过
  • 下载声音
  • 搜索专辑

更多功能

  • 引入 [Himalaya podcast]( https://www.himalaya.com/]
  • 多语言
  • 自定义样式
  • 快捷键设置
  • 下载历史
  • 本地音乐
  • 播放记录
  • 专辑评论
  • 多个声音加入播放列表

安装

这里去下载最新版本,或者下面的指定系统版本。

Mac(10.9+)

下载.dmg或者使用 homebrew(需要 50 个星星才行 ):

brew cask install mob

Linux [待测试]

‘Debian / Ubuntu’ 使用 .deb下载:

$ sudo dpkg -i Mob-0.1.2-linux-amd64.deb

其他发行版本用 .Appimage下载:

$ chmod u+x Mob-0.1.2-linux-x86_64.AppImage
$ ./Mob-0.1.2-linux-x86_64.AppImage

Windows [待测试]

下载

快捷键

全局

描述按键
暂停 / 播放<kbd>Cmd / Ctrl</kbd> + <kbd>Option / Alt</kbd> + <kbd>S</kbd>
音量加<kbd>Cmd / Ctrl</kbd> + <kbd>Option / Alt</kbd> + <kbd>Up</kbd>
音量减<kbd>Cmd / Ctrl</kbd> + <kbd>Option / Alt</kbd> + <kbd>Down</kbd>
上一曲<kbd>Cmd / Ctrl</kbd> + <kbd>Option / Alt</kbd> + <kbd>Left</kbd>
下一曲<kbd>Cmd / Ctrl</kbd> + <kbd>Option / Alt</kbd> + <kbd>Right</kbd>

开发

$ yarn install
$ yarn run start:main
$ yarn run start:renderer

开发与建议

仓库地址:zenghongtu/Mob

欢迎提 issue,顺手点个赞也是极好的了

fong - 纯typescript的node gRPC微服务框架

$
0
0

简介

fong: A service framework of node gRPC. github: https://github.com/xiaozhongliu/fong fong是一个完全用typescript编写的node gRPC框架, 可以基于它很方便地编写gRPC微服务应用. 一般是用来编写service层应用, 以供bff层或前端层等调用.

优点

1.纯typescript编写, typescript的好处不用多说了. 并且用户使用这个框架框架时, 查看定义都是ts源码, 用户使用框架感受不到type definition文件. 2.效仿egg.js的『约定优于配置』原则, 按照统一的约定进行应用开发, 项目风格一致, 开发模式简单, 上手速度极快. 如果用过egg, 就会发现一切都是那么熟悉.

对比

目前能找到的开源node gRPC框架很少, 跟其中star稍微多点的mali简单对比一下:

对比方面malifong
项目风格约定
定义查看跳转definition源代码
编写语言javascripttypescript
proto文件加载仅能加载一个按目录加载多个
代码生成
中间件
配置
日志
controller加载
service加载即将支持, 目前可以自己import即可
util加载即将支持, 目前可以自己import即可
入参校验即将支持
插件机制打算支持
更多功能TBD

示例

示例项目

github: https://github.com/xiaozhongliu/ts-rpc-seed

运行服务

使用vscode的话直接进F5调试typescript. 或者:

npm start

测试请求

ts-node tester
# 或者:
npm run tsc
node dist/tester.js

使用

目录约定

不同类型文件只要按以下目录放到相应的文件夹即可自动加载.

root
├── proto
|  └── greeter.proto
├── config
|  ├── config.default.ts
|  ├── config.dev.ts
|  ├── config.test.ts
|  ├── config.stage.ts
|  └── config.prod.ts
├── midware
|  └── logger.ts
├── controller
|  └── greeter.ts
├── service
|  └── sample.ts
├── util
|  └── sample.ts
└── typings
|  ├── enum.ts
|  └── indexed.d.ts
├── log
|  ├── common.20190512.log
|  ├── common.20190513.log
|  ├── request.20190512.log
|  └── request.20190513.log
├── app
├── packagen
├── tsconfign
└── tslintn

入口文件

import App from 'fong'
new App().start()

配置示例

默认配置config.default.ts与环境配置config.<NODE_ENV>.ts是必须的, 运行时会合并. 配置可从ctx.config和app.config获取.

import { AppInfo, Config } from 'fong'

export default (appInfo: AppInfo): Config => {
    return {
        // basic
        PORT: 50051,

        // log
        COMMON_LOG_PATH: `${appInfo.rootPath}/log/common`,
        REQUEST_LOG_PATH: `${appInfo.rootPath}/log/request`,
    }
}

中间件示例

注: req没有放到ctx, 是为了方便在controller中支持强类型.

import { Context } from 'fong'
import 'dayjs/locale/zh-cn'
import dayjs from 'dayjs'
dayjs.locale('zh-cn')

export default async (ctx: Context, req: object, next: Function) => {
    const start = dayjs()
    await next()
    const end = dayjs()

    ctx.logger.request({
        '@duration': end.diff(start, 'millisecond'),
        controller: `${ctx.controller}.${ctx.action}`,
        metedata: JSON.stringify(ctx.metadata),
        request: JSON.stringify(req),
        response: JSON.stringify(ctx.response),
    })
}

controller示例

import { Controller, Context } from 'fong'
import HelloReply from '../typings/greeter/HelloReply'

export default class GreeterController extends Controller {

    async sayHello(ctx: Context, req: HelloRequest): Promise<HelloReply> {
        return new HelloReply(
            `Hello ${req.name}`,
        )
    }

    async sayGoodbye(ctx: Context, req: HelloRequest): Promise<HelloReply> {
        return new HelloReply(
            `Goodbye ${req.name}`,
        )
    }
}

日志

日志文件: 请求日志: ./log/request.<yyyyMMdd>.log 其他日志: ./log/common.<yyyyMMdd>.log

请求日志示例:

{
    "@env": "dev",
    "@region": "unknown",
    "@timestamp": "2019-05-12T22:23:53.181Z",
    "@duration": 5,
    "controller": "Greeter.sayHello",
    "metedata": "{\"user-agent\":\"grpc-node/1.20.3 grpc-c/7.0.0 (osx; chttp2; godric)\"}",
    "request": "{\"name\":\"world\"}",
    "response": "{\"message\":\"Hello world\"}"
}

代码生成

代码生成器还未单独封包, 现在放在示例应用的codegen目录下.

使用方法: 1.定义好契约proto, 确保格式化了内容. 2.运行代码生成逻辑:

ts-node codegen

这样就会生成controller及相关请求/响应的interface/class, 未来会支持更多类型的文件的生成. 3.从./codegen/dist目录将生成的controller文件移入./controller文件夹并开始编写方法内部逻辑.

定义查看跳转

Peek Definition直接指向源码.

近期计划

service加载

service文件放到service文件夹即可自动加载. 通过ctx.<service>使用.

util加载

util文件放到util文件夹即可自动加载. 通过ctx.util.<function>使用.

入参校验

把在这里用的参数校验中间件搬过来, 用class-validator和class-transformer实现校验, 支持自动生成. 应用内的request model将会类似:

import { IsOptional, Length, Min, Max, IsBoolean } from 'class-validator'

export default class IndexRequest {
    @Length(4, 8)
    @IsOptional()
    foo: string

    @Min(5)
    @Max(10)
    @IsOptional()
    bar: number

    @IsBoolean()
    @IsOptional()
    baz: boolean
}

框架内的validate midware将会类似:

import { Context } from 'egg'
import { validate } from 'class-validator'
import { plainToClass } from 'class-transformer'

import HomeIndexRequest from '../request/home/IndexRequest'
import HomeValidateRequest from '../request/home/ValidateRequest'
const typeMap = new Map([
    ['Home.index', HomeIndexRequest],
    ['Home.validate', HomeValidateRequest],
])

export default async (ctx: Context, next: Function) => {
    const type = typeMap.get(ctx.routerName)
    const target = plainToClass(type, ctx.query)
    const errors = await validate(target)

    if (!errors.length) return next()

    ctx.body = {
        success: false,
        message: errors.map(error => ({
            field: error.property,
            prompt: error.constraints,
        })),
    }
}

新手的流连接实现

$
0
0

周末逛 V2EX, 在 Node 板块看到一个问题, 把上千个小文件合并到一个大文件, 看到有小伙伴说用自定义可读流实现, 但是这样的话还需要自己建立缓冲区, 自己监听 writstream ‘drain’ 事件, 我考虑后觉得应该充分利用 pipe 给开发者的便利, 写了一版本简陋的合并实现, 由于是个新手, 多有不足, 抛砖引玉, 还请大家多多指教

const Readable = require('stream').Readable
const fs = require('fs')

Readable.prototype.cpipe = function(writableStream, opts) {
    let preReadAbleStream = this.preStream
    let curReadAbleStream = this
    while(preReadAbleStream) {
        (function(cur, pre){
            pre.on('end', () => {
                writableStream.write('\n----------')
                cur.pipe(writableStream)
            })
        })(curReadAbleStream, preReadAbleStream)
        
        curReadAbleStream = preReadAbleStream
        preReadAbleStream = curReadAbleStream.preStream
    }
    return curReadAbleStream.pipe(writableStream, opts)
}

Readable.prototype.concat = function(readableStream) {
    readableStream.preStream = this
    return readableStream
}

let colors = fs.createReadStream('../colors.js')
let playground = fs.createReadStream('../playground.js')
let tail = fs.createReadStream('../13-tail-node/ntail.js')
colors.concat(playground).concat(tail).cpipe(process.stdout)

学习 Node 一段时间啦, 感觉有了顺手工具后, 充分激发了创造能力, 祝 Node 长青


egg+vue配置了token,还要开启_csrf验证吗

$
0
0

egg+vue配置了token,还要开启_csrf验证吗

Node path里面为什么自带www

$
0
0

如图 ,本人在nodeStudy目录下启动了个node server2.js, 然后想访问localhost:8080/www1/1.html,但是找不到这个文件,打印了下err,发现路径多了个www 00R~TH3VH73XDFP4LM8WO.png

如图: a.png

如何说服后端,配合使用node服务?

$
0
0

目前已知的最大优点就是,nodejs可以完成视图紧密的逻辑,让后端更加专注业务模型,减轻他们的开发量,,,但是人家不太想把这部分内容让给前端搞,减轻开发量不就要减人嘛 还有没有其他点,可以证明比现有模式(前端直接与后端合作)更好

Node写的命令行脚本如何获取和linux下pwd一致的结果

$
0
0

今天写了个demo学习Node编写命令行工具,遇到这么一个问题:

首先我的文件结构是:

微信截图_20190513203009.png

这个命令行很简单,我期望执行命令时,获取当前执行命令行时的路径,也就是 pwd命令的效果。我写了好几种方式:

image.png

我现在是在当前目录下的 testDir 目录下执行命令行,按照我想的预期,结果应该是和 pwd命令一致:

pwd.PNG

但是上面的六种方式,甚至连使用Node执行 pwd 命令输出的都是:

log.PNG

为什么会出现这种结果呢?请问要怎么样才能达到我的目的呢?

express-session如何设置当浏览器关闭时过期,或者会话维持超过30分钟也过期?

$
0
0

初学node希望各位前辈有时间多多调教QAQ

Viewing all 14821 articles
Browse latest View live