前端Nodejs框架koa/egg和es6入门

news2024/11/26 19:31:00

故事从一个小需求开始,有一天我们美丽可爱的运营MM拿出来了一份Excel表格,希望在一些特定场景能让小蜜按照她定制的样子来交互,并且每一条问题或答案都带一些值来影响处理过程最终能对用户看到的结果起到一定作用。Excel里包含两个sheet页,一个sheet页描述了一颗交互流程树(她用xmind写好导出成excel的),另一页描述每个选项要带的值。所以LZ设计了一个json的格式来表达这颗树,并要做一个后台管理页面来维护每一个交互流程。

Implementation
我们的新版后台管理系统是基于egg框架的node应用,以前只听过没用过,这次终于有机会试一试了。我理解egg的本质上是koa,所以先从koa开始

关于koa
开场不得不贴上koa经典的洋葱图

screenshot

这个图可以展示koa对于req和res的处理。比koa传统一点的express框架是用比较典型的web框架思路,req和res来做参数调用中间件的,而koa的不同就是使用了ctx。做为koa的中间件来处理ctx的就是Generator函数。简单解释下,当一个request过来的时候,会依次经过各个中间件进行处理,而处理过程中肯定是需要跳转的,所以跳转需要一个信号或者标识,这个信号就是yield next,当到某个中间件后,该中间件处理完不执行yield next的时候,然后就会反向执行前面那些中间件剩下的逻辑。就像洋葱图里,所有层是被切片的,有点像AOP。举个官方的栗子:

var koa = require('koa');
var app = koa();
 
// response-time
app.use(function *(next){
  var start = new Date;
  yield next;
  var ms = new Date - start;
  this.set('X-Response-Time', ms + 'ms');
});
 
// logger
app.use(function *(next){
  var start = new Date;
  yield next;
  var ms = new Date - start;
  console.log('%s %s - %s', this.method, this.url, ms);
});
 
// response
app.use(function *(){
  this.body = 'Hello World';
});
 
app.listen(3000);

其中,我们可以把response-time,logger,response分别作为三个层。不错,这很洋葱。

那么问题来了,如果多层,执行顺序要怎么控制呢?稍微改一下输出,比较方便看结果,代码如下:

var koa = require('koa');
var app = koa();
 
app.use(function* function1(next) {
    console.log('function1 -> before next');
    yield next;
    console.log('function1 -> after next');
});
 
app.use(function* function2(next) {
    console.log('function2 -> before next');
    yield next;
    console.log('function2 -> after next');
});
 
app.use(function* function3(next) {
    console.log('function3 -> before next');
    this.body = 'hello world';
    console.log('function3 -> after next');
});
 
app.listen(3000);

关于这个执行顺序,画个图比较容易看明白:
在这里插入图片描述

yield

so 执行结果是这样的:

function1 -> before next
function2 -> before next
function3 -> before next
function3 -> after next
function2 -> after next
function1 -> after next
并不是想象中串行执行function1,2,3的。

关于ES6-Generator
其实这里讲述的顺序有点乱,因为上面算是结果,导致结果的过程是因为我最早光去看代码是不知道function后面的那个星号是什么意思的,去查了才开始了解Generator,于是才有了上面的执行顺序的探索。其实星号和yield可以理解成Generator的特征。看《ECMAScript 6 入门》里Generator这一章就足够详细了,里面有一句话,大概是讲Generator函数可以理解成一个状态机,它封装了多个内部状态。执行Generator函数时会返回一个遍历器对象,可以依次遍历Generator函数内部的每一个状态。回到刚才的栗子,作为状态机Generator对象一定会维护一个状态属性,就是GeneratorStatus。回到刚才的栗子,GeneratorStatus的值从suspended变为closed后,就不会再改变了。也就是说,Generator对象在一个环境中,只能执行一遍,上面的Generator的执行就有点类似于function1(function2(function3({})))

理解了这些,基本上就可以获得koa新手村玩家的荣誉称号了。作为混新手村的一员,进阶玩法还在不断探索中,共勉。

关于egg
再说egg,我理解其实egg是框架的框架,它最大的价值是给开发者定义了规范,做了MVC做的事情。正好这周猿来如此也听了不四大神的分享。他说,egg更像java的spring框架。新手村刚刚使用过egg框架的玩家听完由衷的感叹,对对对就是这样~

egg的文档很少,有疑惑的地方我一般是这样解决的。看API (基本无法解决问题)-> 读源码(可以解决一部分疑惑)-> 涉及到资料比较多的知识,可以看看koa和chain的相关资料,一言不合就google(也可以解决一部分问题)-> 还不行就问团队的egg先驱探险者@空承 -> 如果有了新的思路再循环整个过程,然后不断尝试。

Egg相关也可以去看下空承写的唯快不破:Egg及在Summer中的应用

回到需求本身
举个栗子,做需求的过程中遇到了一个上传的filestream问题。简单来讲,上传文件实际上拿到的是一个fs的read流。如果是文本其实很好办,node也像java一样有类似readline的方法能解决。但是我要解决的是一个多sheet的excel文件。第一反应就是去npm找合适的第三方包来解决。把流行的几个包试了一下,基本都是要读取路径的,没有直接可以对流处理的。其中最强大的xlsx可以处理流,但是直接用是不行的。首先,它读的一定是个正常的文件流,上传来的read流并不支持。又尝试了各种其他方案,一一否决了之后决定先把流生成临时文件,代码如下:

function read(rs) {
  return new Promise((resolve, reject) => {
    rs.on('data', (chunk) => {
      fs.writeFileSync(filePath, chunk, 'binary');
    }).on('end', () => {
      resolve();
    }).on('error', (err) => {
      reject(err);
    });
  });
}

Promise
Promise解决的是异步编程的问题,理念上他像一个容器,里面保存着某个异步操作的结果。从语法上来看,它是一个对象,构造函数包括resolve和rejected。
我理解他也是维护状态来实现回调的,包括三种状态的变化:pending(处理中),resolved(处理成功),reject(处理失败)。resolve是将Promise对象的状态从从Pending变为Resolved,在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。同理,reject将Promise对象的状态从从Pending变为Rejected,在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。Promise实例生成以后,可以用then方法分别指定Resolved状态和Reject状态的回调函数。更深入可以看http://liubin.org/promises-book/ 很详细,很强大。

node的fs
用法上API基本足矣,但是要理解的是写文件其实是有一个pipe概念在里面的。就好比两个桶,read流是一个桶(source),用一根管子(pipe)往另一个桶(target)也就是要生成的文件里倒水(data)。用pipe运输data是需要过程的,这个过程就用到了Promise的状态,来看是不是写完了。于是就有了上面的代码。

xlsx–解析Excel文件
有了真实存在的文件而不是一个read流就解决一切了么?当初天真的LZ也是这么一位的,然而真相使用xlsx是只转workbook对象的部分是可以的,主要是下面这一部分。

function parse_xlscfb(cfb, options) {
if(!options) options = {};
fix_read_opts(options);
reset_cp();
var CompObj, Summary, Workbook;
if(cfb.find) {
    CompObj = cfb.find('!CompObj');
    Summary = cfb.find('!SummaryInformation');
    Workbook = cfb.find('/Workbook');
} else {
    prep_blob(cfb, 0);
    Workbook = {content: cfb};
}
 
if(!Workbook) Workbook = cfb.find('/Book');
var CompObjP, SummaryP, WorkbookP;
 
if(CompObj) CompObjP = parse_compobj(CompObj);
if(options.bookProps && !options.bookSheets) WorkbookP = {};
else {
    if(Workbook) WorkbookP = parse_workbook(Workbook.content, options, !!Workbook.find);
    else throw new Error("Cannot find Workbook stream");
}

但是从workbook读取sheet页的对象的时候就抛异常了,把workbook对象打出来看了一下,再对比下解析的源码,大概是因为sheet页的类似于head的一个东西一般是[‘!range’],但是我手里的文件是[‘!ref’],不知道是不是因为背景中叙述的文件来源是xmind导出来的树所导致的。那其实现在是要兼容[‘!ref’]。此时一脸懵逼的LZ就开始google了,在xlsx的wiki里看到关于worksheet对象的解释如下:

Worksheet Object
Each key that does not start with ! maps to a cell (using A-1 notation)
worksheet[address] returns the cell object for the specified address.
Special worksheet keys (accessible as worksheet[key], each starting with !):
ws[‘!ref’]: A-1 based range representing the worksheet range. Functions that work with sheets should use this parameter to determine the range. Cells that are assigned outside of the range are not processed. In particular, when writing a worksheet by hand, be sure to update the range. For a longer discussion, see http://git.io/KIaNKQFunctions that handle worksheets should test for the presence of !ref field. If the !ref is omitted or is not a valid range, functions are free to treat the sheet as empty or attempt to guess the range. The standard utilities that ship with this library treat sheets as empty (for example, the CSV output is an empty string).
When reading a worksheet with the sheetRows property set, the ref parameter will use the restricted range. The original range is set at ws[‘!fullref’]

ws[‘!cols’]: array of column properties objects. Column widths are actually stored in files in a normalized manner, measured in terms of the “Maximum Digit Width” (the largest width of the rendered digits 0-9, in pixels). When parsed, the column objects store the pixel width in the wpx field, character width in the wch field, and the maximum digit width in the MDW field.
ws[‘!merges’]: array of range objects corresponding to the merged cells in the worksheet. Plaintext utilities are unaware of merge cells. CSV export will write all cells in the merge range if they exist, so be sure that only the first cell (upper-left) in the range is set.
其实我自己也懒得看英文,上面这段大概就是解释了一下ref和cols两种情况取sheet页里cell范围的相关事宜,具体在查+试并结合xlsx的规范,发现应该是这样把sheet页的cell对象读出来作为一个二维数组返回的:

function _decode_range(range) {
  let o = {s: {c: 0, r: 0}, e: {c: 0, r: 0}};
  let idx, i = 0, cc = 0;
  let len = range.length;
  for (idx = 0; i < len; ++i) {
    if ((cc = range.charCodeAt(i) - 64) < 1 || cc > 26) break;
    idx = 26 * idx + cc;
  }
  o.s.c = --idx;
 
  for (idx = 0; i < len; ++i) {
    if ((cc = range.charCodeAt(i) - 48) < 0 || cc > 9) break;
    idx = 10 * idx + cc;
  }
  o.s.r = --idx;
 
  if (i === len || range.charCodeAt(++i) === 58) {
    o.e.c = o.s.c;
    o.e.r = o.s.r;
    return o;
  }
 
  for (idx = 0; i !== len; ++i) {
    if ((cc = range.charCodeAt(i) - 64) < 1 || cc > 26) break;
    idx = 26 * idx + cc;
  }
  o.e.c = --idx;
 
  for (idx = 0; i !== len; ++i) {
    if ((cc = range.charCodeAt(i) - 48) < 0 || cc > 9) break;
    idx = 10 * idx + cc;
  }
  o.e.r = --idx;
  return o;
}

至此,我拿到了上传的Excel文件的每个sheet页的每个cell值。后面要做的就是一些业务逻辑处理了,就不粘代码了。整体来看上传大概就是做了下面这几件事:生成临时文件->读取文件->解析数据->得到数据后删除临时文件->对数据进行业务逻辑处理->返回处理后的结果

exports.upload = function*() {
  const rs = yield this.getFileStream();
  let ret = [];
  try {
    yield read(rs).then(function () {
      return _parse_xlsx(filePath);
    }, function (error) {
      return error;
    }).then(function () {
      _clean(filePath);
      ret = _build(data);
    });
    this.body = {
      data: ret,
    };
  } catch (e) {
    this.logger.error(`upload(${data})`, e);
    toResponse(this, Result.error(ResultCode.FILE_NOT_SUPPORT, e.message));
  }
};
Finally

LZ是个普通的业务开发,所以为了产品的迭代速度时间一向紧迫。这个引出来一大串学习过程的需求可能选择写在熟悉的java工程里很快就能交付使用。但是事实上,一系列的探索和实现大概花了一周的时间才完成。所以此处不得不感谢我们项目的产品和运营对于一个并不牛X的技术人员的任性和偏执的包容。(中间LZ也为了不影响整体进度做了一部分java版的实现,后来才改成了node版,所以有小伙伴看到这里不要像LZ一样任性,科学实验不拖team的后腿是前提~)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/68176.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【C++类型转换】4种类型转换:static_cast、reinterpret_cast、const_cast、dynamic_cast

目录 1. C语言中的类型转换 2.1.类型转换&#xff1a;static_cast 2.2.类型转换&#xff1a;reinterpret_cast 2.3.类型转换&#xff1a;const_cast 2.4.类型转换&#xff1a;dynamic_cast 1. C语言中的类型转换 隐式类型转换&#xff1a;编译器在编译阶段自动进行&#x…

第9章 无监督学习

系列文章目录 第1章 绪论 第2章 机器学习概述 第3章 线性模型 第4章 前馈神经网络 第5章 卷积神经网络 第6章 循环神经网络 第7章 网络优化与正则化 第8章 注意力机制与外部记忆 第9章 无监督学习 第10章 模型独立的学习方式 第11章 概率图模型 第12章 深度信念网络 第13章 深…

两种形式的import

两种形式 import … 后面只能是模块或包 from … import … from 后面只能是模块或包&#xff0c;import 后面可以是任何变量 总结 可以简单的记成&#xff1a;第一个空只能填模块或包&#xff0c;第二个空填啥都行。 看一个实例 通过from import可以正常都模块 import方式不…

安装head

版本 github地址&#xff1a;https://gitcode.net/mirrors/mobz/elasticsearch-head?utm_sourcecsdn_github_accelerator 下载zip 解压zip得到文件夹elasticsearch-head-master 打开head命令行&#xff0c;依次按命令下载 下载淘宝镜像&#xff1a; npm install -g cnpm -…

程序员需知的9个编程学习官网,建议收藏

毫无疑问&#xff0c;一项技术&#xff0c;最权威的文档一定是它的官方文档&#xff0c;所有的教程、书籍都是直接或者间接在官方相关文档的基础上完成。所以&#xff0c;编程学习&#xff0c;后面最好还是要看官网的。当然&#xff0c;官网大部分都是英文——其实配合一些翻译…

Ubuntu下Petalinux安装流程与常见问题解决

一. 简介 由于近期需要对ZYNQ ARM端做Linux开发测试&#xff0c;需要用到Petalinux工具进行定制&#xff0c;所以本文对Petalinux安装过程进行简要总结&#xff0c;并对安装过程中遇到的一些问题进行解释并解决。 Petalinux是Xilinx公司开发的嵌入式Linux定制软件&#xff0c;…

「C#」异步编程玩法笔记-async、await

C#5.0的时候引入了async和await两个修饰符&#xff0c;成为异步编程的核心关键字。 async 是修饰符&#xff0c;表明方法含有异步操作&#xff0c;但并不是说整个方法是异步的。async修饰的方法会先同步执行到第一处await的地方而后开始异步。 await可以理解为一异步特有的“re…

使用Python制作内马尔的胜利之舞代码版

不知道大家最近有没有被球星内马尔所吸引&#xff0c;反正我是对他的胜利之舞上瘾了。今天&#xff0c;我以程序猿的视角将他的胜利之舞做成代码版的视频。话不多说&#xff0c;先看看最终效果图&#xff1a; 哈哈哈哈&#xff0c;是不是看着还不错的样子。 之前我做过类似的教…

小程序游戏 vs h5游戏,技术优势分别有哪些

从“跳一跳”到“羊了个羊”微信小游戏上线4年时间&#xff0c;除了涌现出不少火爆全网的小游戏之外&#xff0c;也有类似于“动物餐厅”、“口袋奇兵”等游戏得以在此孵化繁荣&#xff0c;凭借着微信强大的社交属性小游戏成为游戏厂商在桌面端、App 端、H5 端之外争夺的另一个…

微软宣布 S2C2F 已被 OpenSSF 采用

开源供应链安全对大多数 IT 领导者来说是个日益严峻的挑战&#xff0c;围绕确保开发人员在构建软件时如何使用和管理开源软件 (OSS) 依赖项的稳健策略至关重要。Microsoft 发布安全供应链消费框架 (S2C2F) 是一个以消费为中心的框架&#xff0c;它使用基于威胁的风险降低方法来…

linux下安装部署es-head插件

es通过程序代码调用es 各种api接口。 es-head查看与显示es状态信息&#xff0c;数据量&#xff0c;具体数据。 1、elasticsearch-head介绍 官方地址: https://github.com/mobz/elasticsearch-head elasticsearch-head 是一款用来管理Elasticsearch集群的第三方插件工具。 e…

用二元泊松模型预测2022年世界杯淘汰赛结果

用二元泊松模型预测2022年世界杯淘汰赛结果 网上有很多文章用双泊松&#xff08;Double Poisson&#xff09;模型来预测世界杯比赛结果。但是双泊松模型有一个严重的缺陷&#xff0c;那就是它假设比赛中两队的比分是条件独立的。而我们都知道&#xff0c;在对抗性比赛中&…

国产API管理神器Eolink也太强了吧

一、研发痛点 什么是API研发管理 API研发管理是包含了API开发管理、开发团队协作、自动化测试、网关以及监控等等API管理全生命周期的一系列管理过程。可以帮助公司实现开发运维一体化&#xff0c;提升开发速度&#xff0c;达到降本增效的目标。 前端痛点 针对前端开发在使…

Metabase学习教程:权限-2

使用集合权限 设置具有权限的集合&#xff0c;以帮助用户组织和共享与其相关的工作。 集合保持问题,仪表板&#xff0c;和模型有条理&#xff0c;容易找到。将集合视为存储我们工作的文件夹是很有帮助的。集合权限授予一群人访问&#xff1a; 查看或编辑保存在集合中的问题、…

激活企业数字化采购价值,智慧采购管理系统助力半导体行业提升采购协同效率

如今&#xff0c;随着国内经济不断发展以及国家对半导体行业的政策扶持&#xff0c;我国半导体行业发展迅速&#xff0c;半导体技术含量与日俱增的同时&#xff0c;也对我国半导体企业的管理效能与管理工具提出了更高的要求。在海外对国内半导体产业发展日益严格的当下&#xf…

Netty篇之如何优雅的关服

强制关服的危害 linux中关服如果我们使用 kill -9 pid号或者在windows中使用 taskkill /f /pid pid号来关服的话&#xff0c;相当于是突然断电的方式&#xff0c;会导致如下几种情况。 缓存中的数据丢失正在进行文件的写操作&#xff0c;没有更新完成&#xff0c;突然退出会…

技术分享 | Redis 集群架构解析

作者&#xff1a;贲绍华 爱可生研发中心工程师&#xff0c;负责项目的需求与维护工作。其他身份&#xff1a;柯基铲屎官。 本文来源&#xff1a;原创投稿 *爱可生开源社区出品&#xff0c;原创内容未经授权不得随意使用&#xff0c;转载请联系小编并注明来源。 一、集群架构的一…

决策树算法中处理噪音点

目录 如何解决&#xff1f;——采用剪枝的方法。 预剪枝 后剪枝 如果训练集中存在噪音点&#xff0c;模型在学习的过程总会将噪音与标签的关系也学习进去&#xff0c;这样就会造成模型的过拟合化&#xff0c;也就是模型在训练集的分类效果很好&#xff0c;在未知数据上处理效…

python快速实现2048小游戏

《2048》是一款比较流行的数字游戏&#xff0c;最早于2014年3月20日发行。原版2048首先在GitHub上发布&#xff0c;原作者是Gabriele Cirulli&#xff0c;后被移植到各个平台。这款游戏是基于《1024》和《小3传奇》的玩法开发而成的新型数字游戏。 操作指南&#xff1a; 每次…

景区票务系统毕业设计,景区售票系统设计与实现,旅游售票系统毕业设计源码分析

项目背景和意义 目的&#xff1a;本课题主要目标是设计并能够实现一个基于java的景区景点预约购票系统&#xff0c;整体使用javaMySql的B/S架构&#xff0c;技术上采用了springboot框架&#xff1b;通过后台添加景区资讯、景点介绍&#xff0c;管理用户订单&#xff1b;用户通过…