关于koa.js:麒麟操作系统-kylinos-从入门到精通-办公环境-第六十篇-KTV相关文件MKV的处理

0.根底环境类别:笔记本型号:中国长城 NF14C硬件平台:飞腾处理器(ArmV8 指令集)零碎:河汉麒麟操作系统 V10 SP1(2303) 关键词:信创,麒麟零碎,linux,飞腾,arm,aarch,arm64,按键精灵 1.需要背景这一次理论适合需要背景是加入歌唱较量,从网上找到了相似KTV伴奏的文件。下载的MKV外面有有原唱和伴奏两条音轨,但因为较量选手较多,从流程管制上不容易治理。因些须要将原唱的音轨移除,就是本文要解决的问题。我在Windows 11下尝试了剪映(不反对)、Adobe Premiere(简单,没找到形式)以及尝试过间接用VLC Player间接提取音轨的形式,均没有解决我的问题。通过搜寻,所有的材料均指向了一个向MKVToolnix的软件,通过试用,简略好用,解决了MKV的问题,记录在此,心愿大家遇到的时候有用。 2.MKVToolNix装置一个好的习惯是,咱们先找找利用市场。好在利用市场中有这一款软件,咱们间接通过利用市场装置。截至发稿时,市场中的版本是45,而官网的版本曾经到了79(官网并没有提供arm64的最新安装包,如果要装须要本人编译)。关上的成果如下: 3.MKV解决实例图片的程序为操作程序,首先右击输出源空白区找到对应的MKV文件读取后,能够看到对应的MKV元素清单,有视频,多音轨,字幕以及其余根底信息等将不须要的音轨(或字幕)勾销勾选(具体是什么音轨,你须要在播放器中确认一下)本文中,确认第一条音轨是原唱,将其勾销勾选确认输入文件地位点击开始混流,保留在播放器中验证成果如下![上传中...]() 麒麟零碎专栏:https://segmentfault.com/blog/kylinos文章发表在SegmentFault分割邮箱:1179611323@qq.com群:662512340发行日志:20230922 首发

September 22, 2023 · 1 min · jiezi

关于koa.js:nodekoalogger

koa-logger源码解析以GET申请“/”为例子,koa-logger会打印以下日志: <-- GET /GET / - 790ms --> GET / 200 803ms 185b分为是申请与响应的日志 // koa-logger 主函数function dev (opts) { // print函数,默认参数为空时是调用console.log,如果有transporter选项,则调用transporter函数代替 const print = (function () { let transporter if (typeof opts === 'function') { transporter = opts } else if (opts && opts.transporter) { transporter = opts.transporter } return function printFunc (...args) { // 留神:这里做了格局说明符替换 const str = util.format(...args) if (transporter) { transporter(str, args) } else { console.log(...args) } } }()) return async function logger (ctx, next) { // request const start = ctx[Symbol.for('request-received.startTime')] ? ctx[Symbol.for('request-received.startTime')].getTime() : Date.now() // 打印申请日志 print(' ' + chalk.gray('<--') + ' ' + chalk.bold('%s') + ' ' + chalk.gray('%s'), ctx.method, ctx.originalUrl) try { await next() } catch (err) { // log uncaught downstream errors log(print, ctx, start, null, err) throw err } // calculate the length of a streaming response // by intercepting the stream with a counter. // only necessary if a content-length header is currently not set. // 通过应用计数器拦挡流来计算响应流的长度。只有在以后没有设置Content-Length的响应报头时才须要。 const length = ctx.response.length const body = ctx.body let counter if (length == null && body && body.readable) { ctx.body = body .pipe(counter = Counter()) .on('error', ctx.onerror) } // log when the response is finished or closed, // whichever happens first. // 记录响应实现或敞开的工夫,以先产生的工夫为准。个别都是触发finish事件 const res = ctx.res const onfinish = done.bind(null, 'finish') const onclose = done.bind(null, 'close') // 重点:判断一个流申请是否完结 res.once('finish', onfinish) res.once('close', onclose) function done (event) { res.removeListener('finish', onfinish) res.removeListener('close', onclose) // 打印 申请形式 门路 状态码 响应工夫 报文长度 log(print, ctx, start, counter ? counter.length : length, null, event) } }}还有个logger辅助函数: ...

August 30, 2022 · 3 min · jiezi

关于koa.js:KoaJS

前言koa致力于成为一个更小、更富裕表现力、更强壮的、更轻量的web开发框架。因为它所有性能都通过插件实现,这种插拔式的架构设计模式,很合乎unix哲学。一个简略的服务,如下:const Koa = require('koa') let app = new Koa() app.use((ctx, next) => { console.log(ctx) }) app.listen(4000)复制代码而后在浏览器端关上http://127.0.0.1:4000即可拜访若没有指定返回body,koa默认解决成了Not Found本文内容: 中间件原理(联合代码) 原理中间件实现思路了解上述洋葱模型 浏览源码 app.listen()ctx挂载内容 context.jsrequest.jsresponse.js挂载ctx next构建的洋葱模型 app.use((ctx, next) =< { ... })中间件含异步代码如何保障正确执行返回报文解决屡次调用next导致凌乱问题 基于事件驱动去解决异样 koa2, koa1 和 express区别 一、中间件原理(联合代码)原理 中间件执行就像穿梭洋葱一样,最早use的中间件,就放在最外层。解决程序横穿洋葱,从左到右,右边接管一个request,左边输入返回response;个别的中间件都会执行两次,调用next之前为第一次,调用next时把管制传递给上游的下一个中间件。当上游不再有中间件或者没有执行next函数时,就将顺次复原上游中间件的行为,让上游中间件执行next之后的代码; 如下代码:const Koa = require('koa')const app = new Koa()app.use((ctx, next) => { console.log(1)next()console.log(3)})app.use((ctx) => { console.log(2)})app.listen(9001) 执行后果是1=>2=>3复制代码中间件实现思路 留神其中的compose函数,这个函数是实现中间件洋葱模型的要害// 场景模仿// 异步 promise 模仿const delay = async () => { return new Promise((resolve, reject) => { setTimeout(() => { console.log('delay 2000ms') resolve();}, 2000);});}// 两头间模仿const fn1 = async (ctx, next) => { console.log(1); await next(); console.log(2);}const fn2 = async (ctx, next) => { console.log(3); await delay(); await next(); console.log(4);}const fn3 = async (ctx, next) => { console.log(5);} ...

July 14, 2022 · 3 min · jiezi

关于koa.js:适合-Kubernetes-初学者的一些实战练习-四

本系列前三局部: 适宜 Kubernetes 初学者的一些实战练习 (一)适宜 Kubernetes 初学者的一些实战练习 (二)适宜 Kubernetes 初学者的一些实战练习 (三)练习1 - Kubernetes pod 的主动 scale (程度主动伸缩)kubectl scale 命令用于程序在负载减轻或放大时进行 pod 扩容或放大,本练习咱们通过一个理论例子来察看 scale命令到底能达到什么成果。 命令行创立一个 deployment: kubectl run jerry-nginx --image=nginx:1.12.2 kubectl get deploy 查看刚刚创立的 deployment: 主动被 deployment 创立的 pod:kubectl get pod: 应用下列命令查看生成的 deployment 明细: kubectl get deployment jerry-nginx -o yaml 另一个有用的命令: kubectl describe deployment jerry-nginx 当初咱们应用上面的命令对 deployment 进行程度扩大: kubectl scale deployment jerry-nginx --replicas=3 kubectl get pods -l run=jerry-nginx下图这个 Age 为 15 分钟之前的是第一次创立 deployment 时生成的,其余两个 Age 为 1 分钟之前的是执行了 scale 命令后主动创立的。 ...

April 3, 2022 · 3 min · jiezi

在Koajs中实现文件上传的接口

文件上传是一个基本的功能,每个系统几乎都会有,比如上传图片、上传Excel等。那么在Node Koa应用中如何实现一个支持文件上传的接口呢?本文从环境准备开始、最后分别用 Postman 和一个HTML页面来测试。 环境准备首先当然是要初始化一个Koa项目了,安装 Koa、koa-router 即可。 npm install koa koa-router设置图片上传目录,把图片上传到指定的目录中,在 app 路径下新建 public 文件夹,目录结构如下: koa-upload/--app----public------uploads----index.js--package.json编写 index.js const koa = require('koa')const app = new koa()router.post('/upload', ctx => { ctx.body = 'koa upload demo'})app.use(router.routes());app.listen(3000, () => { console.log('启动成功') console.log('http://localhost:3000')});然后启动,确保这一步没有问题。 使用 koa-body 中间件获取上传的文件koa-body 支持文件、json、form格式的请求体,安装 koa-body npm install koa-body设置 koaBody 配置参数,index.js const koa = require('koa')const koaBody = require('koa-body')const path = require('path')const app = new koa()app.use(koaBody({ // 支持文件格式 multipart: true, formidable: { // 上传目录 uploadDir: path.join(__dirname, 'public/uploads'), // 保留文件扩展名 keepExtensions: true, }}));... ...接下来完善 /upload 路由,获取文件,然后直接返回文件路径 ...

October 7, 2019 · 1 min · jiezi

Koa源码浅析

Koa源码十分精简,只有不到2k行的代码,总共由4个模块文件组成,非常适合我们来学习。 参考代码: learn-koa2 我们先来看段原生Node实现Server服务器的代码: const http = require('http');const server = http.createServer((req, res) => { res.writeHead(200); res.end('hello world');});server.listen(3000, () => { console.log('server start at 3000');});非常简单的几行代码,就实现了一个服务器Server。createServer方法接收的callback回调函数,可以对每次请求的req res对象进行各种操作,最后返回结果。不过弊端也很明显,callback函数非常容易随着业务逻辑的复杂也变得臃肿,即使把callback函数拆分成各个小函数,也会在繁杂的异步回调中渐渐失去对整个流程的把控。 另外,Node原生提供的一些API,有时也会让开发者疑惑: res.statusCode = 200;res.writeHead(200);修改res的属性或者调用res的方法都可以改变http状态码,这在多人协作的项目中,很容易产生不同的代码风格。 我们再来看段Koa实现Server: const Koa = require('koa');const app = new Koa();app.use(async (ctx, next) => { console.log('1-start'); await next(); console.log('1-end');});app.use(async (ctx, next) => { console.log('2-start'); ctx.status = 200; ctx.body = 'Hello World'; console.log('2-end');});app.listen(3000);// 最后输出内容:// 1-start// 2-start// 2-end// 1-endKoa使用了中间件的概念来完成对一个http请求的处理,同时,Koa采用了async和await的语法使得异步流程可以更好的控制。ctx执行上下文代理了原生的res和req,这让开发者避免接触底层,而是通过代理访问和设置属性。 看完两者的对比后,我们应该会有几个疑惑: ctx.status为什么就可以直接设置状态码了,不是根本没看到res对象吗?中间件中的next到底是啥?为什么执行next就进入了下一个中间件?所有中间件执行完成后,为什么可以再次返回原来的中间件(洋葱模型)?现在让我们带着疑惑,进行源码解读,同时自己实现一个简易版的Koa吧! ...

June 28, 2019 · 7 min · jiezi

TKoa - 使用 TypeScript 重构的 Node.js Koa开发框架

????Tkoa是使用 typescript 编写的 koa 框架!尽管它是基于 typescript 编写,但是你依然还是可以使用一些 node.js 框架和基于 koa 的中间件。不仅如此,你还可以享受 typescript 的类型检查系统和方便地使用 typescript 进行测试!安装TKoa 需要 >= typescript v3.1.0 和 node v7.6.0 版本。$ npm install tkoaHello T-koaimport tKoa = require(’tkoa’);interface ctx { res: { end: Function }}const app = new tKoa();// responseapp.use((ctx: ctx) => { ctx.res.end(‘Hello T-koa!’);});app.listen(3000);MiddlewareTkoa 是一个中间件框架,拥有两种中间件:异步中间件普通中间件下面是一个日志记录中间件示例,其中使用了不同的中间件类型:async functions (node v7.6+):interface ctx { method: string, url: string}app.use(async (ctx: ctx, next: Function) => { const start = Date.now(); await next(); const ms = Date.now() - start; console.log(${ctx.method} ${ctx.url} - ${ms}ms);});Common function// Middleware normally takes two parameters (ctx, next), ctx is the context for one request,// next is a function that is invoked to execute the downstream middleware. It returns a Promise with a then function for running code after completion.interface ctx { method: string, url: string}app.use((ctx: ctx, next: Function) => { const start = Date.now(); return next().then(() => { const ms = Date.now() - start; console.log(${ctx.method} ${ctx.url} - ${ms}ms); });});Getting startedTkoa - 教程en - english readmeGitter - 聊天室SupportTypeScript大于等于 v3.1 版本Node.js大于等于 v7.6.0 版本LicenseMIT ...

April 17, 2019 · 1 min · jiezi

????????Koa编写的微信小程序码生成接口

wxqrcode-generater????????Koa编写的微信小程序码生成接口源码地址点我本地运行需要先建好数据库~ 在index.js同级目录新建文件config.js,内容如下(根据自己服务器数据库进行填写)module.exports.database = { host: ‘’, user: ‘’, password: ‘’, database: ‘’, port: 1234}之后就可以运行起来了(服务器可用pm2进程管理):node index.js接口注意:以下接口只列出后缀。接口:/addMp方法: POST说明: 增加小程序信息入参:参数必填类型说明name是String小程序名appId是String小程序appIdsecretKey是String小程序secretKeyavatar否String小程序头像接口:/getCode方法: POST说明: 下载二维码接口入参:参数必填类型说明appId是String小程序appIdpath是String小程序路径,例如: “pages/index/index"width是Number二维码宽度,单位为pxautoLineColor否Boolean自动线条颜色isHyaline否Boolean是否透明底色接口:/getMpList方法: GET说明: 获取小程序列表

March 10, 2019 · 1 min · jiezi

让我们来重新设计一下 koa-router

前言koa-router 是目前用的比较多的 Koa 的路由中间件之一,前段时间由于作者没有精力继续维护而将其公开售卖。我们有些项目也用到了这个库,但是目前很多我们想要的特性都没有,比如生成接口文档。本身这个库代码实现还比较简单,因此综合考虑打算重写一个。项目地址:https://github.com/d-band/koa…特性:支持几乎所有的 koa-router 特性支持 params 校验params 支持从 path, header, query, cookie 中获取支持 body parser支持 request body 校验支持参数类型自动转换支持自动生成 OpenAPI简单例子:index.jsimport Koa from ‘koa’;import Mapper from ‘koa-mapper’;import * as service from ‘./service’;const Mapper = new Mapper();mapper.get(’/users/:id/projects’, { params: { id: { type: ’number’ }, status: { type: ‘array<string>’, in: ‘query’ }, token: { type: ‘string’, in: ‘header’ } }}, service.getProjects);mapper.post(’/users/:id/projects’, { params: { id: { type: ’number’ } }, body: ‘Project’}, service.addProject);mapper.schema(‘Project’, { id: { type: ’number’, required: true }, name: { type: ‘string’, required: true }, status: { type: ‘array<Status>’, required: true }});mapper.schema(‘Status’, { id: { type: ‘integer’ }, name: { type: ‘string’ }}, { required: [‘id’, ’name’]});app.use(mapper.routes());app.use(mapper.allowedMethods());app.listen(3000);// open http://localhost:3000/openapi.jsonservice.jsexport async function getProjects(ctx) { const { id, status, token } = ctx.params; await checkToken(id, token); ctx.body = await Project.findAll({ where: { userId: id, status: { $in: status } } });}export async function addProject(ctx) { const { body } = ctx.request; ctx.body = await Project.create({ …body, userId: id });}路由定义:mapper.get(path, [options], …middlewares);mapper.post(path, [options], …middlewares);mapper.put(path, [options], …middlewares);mapper.del(path, [options], …middlewares);…options 为可选参数,包含:name: 路由名称params: 请求参数定义body: 请求 Body 定义其他 OpenAPI 中 Operation Object 的参数options.params 为请求参数定义,如:params = { id: { type: ’number’ }, name: { type: ‘string’, in: ‘query’ }, user: { type: ‘User’, in: ‘query’ }}type: 参数类型,包含基本类型(number、string、integer、date、time、datetime),数组类型(array<string>),自定义类型(如 User),自定义数组类型(array<User>),多个类型(number|string)in: 参数来源,包含 path,header,query,cookie其他 OpenAPI 中 Parameter Object 的参数自定义类型mapper.define(schemaName, properties, options);// ormapper.schema(schemaName, properties, options);支持类型组合,如:mapper.schema(‘Status’, { id: { type: ‘integer’ }, name: { type: ‘string’ }}, { required: [‘id’]});mapper.schema(‘Project’, { id: { type: ’number’, required: true }, name: { type: ‘string’, required: true }, status: { type: ‘array<Status>’, required: true }});支持继承,如:mapper.schema(‘Model’, { id: { type: ’number’ }, createdAt: { type: ‘datetime’ }, updatedAt: { type: ‘datetime’ }});mapper.schema(‘User: Model’, { name: { type: ‘string’ }});Body Parsermapper.post(’/users’, { body: ‘User’}, (ctx) => { const { id, name } = ctx.request.body;});支持文件上传,如:mapper.post(’/uploadImage’, { bodyparser: { multipart: true }, body: { user: { type: ’number’ }, image: { type: ‘file’ } }}, (ctx) => { const { user, image } = ctx.request.body;});结尾目前 koa-mapper 刚发布,测试覆盖率达到 100%,有哪些有兴趣的小伙伴欢迎一起维护。 ...

March 8, 2019 · 2 min · jiezi

Koa 系列 —— Koa 中间件机制解析

上一篇讲了如何编写属于自己的 Koa 中间件,本篇将根据原理实现一个简单的中间件处理函数,并对 Koa 中间件处理函数 compose 函数进行源码解析。1. compose 函数简单实现Koa 中间件采取的是中间件洋葱模型,具体原理可见如何编写属于自己的 Koa 中间件。本质就是将中间件嵌套执行:function middleware0(){ console.log(‘middleware0’)}function middleware1(){ console.log(‘middleware1’)}// 将两个中间件嵌套执行middleware0(middleware1())当然实际上更复杂,还要考虑中间件的异步执行和中间件如何进行嵌套。Koa 中异步处理在 Koa1 中使用的是 generator + co.js,在 Koa2 中使用的是 async/await,我们本次采用 async/await 来处理异步。中间件的嵌套可以通过将中间件当参数传递来实现嵌套。据此我们对上面的代码进行进一步加工:ps:Node7.6+ 支持 async/awaitasync function middleware0(next){ console.log(‘middleware0’) await next()}async function middleware1(next){ console.log(‘middleware1’)}// 将两个中间件嵌套执行middleware0(middleware1)Koa 中通过 compose 函数对中间件的进行处理。compose 函数参数为 middleware 的数组, middleware 数组成员是通过 use 方法添加的中间件。下面写个简单的 compose 函数,来实现多个中间件的处理:async function middleware0(next){ console.log(‘middleware0’) await next() console.log(‘middleware0 end’)}async function middleware1(next){ console.log(‘middleware1’) await next() console.log(‘middleware1 end’)}async function middleware2(next){ console.log(‘middleware2’) await next() console.log(‘middleware2 end’)}/** * @param {Array} 中间件数组 /function compose (middleware) { // 从第一个中间件开始执行 return dispatch(0) function dispatch(i){ // 获取第 i 个中间件 fn = middleware[i] // 获取不到中间件,则直接返回结束 if(!fn) return // 执行第 i 个中间件,并传入第 i + 1 个中间件 return fn(() => dispatch(i + 1)) }}// 执行compose([middleware0, middleware1, middleware2])2. 源码解析我们已经简单实现了一个 compose 函数,现在来看下 Koa 中源码的实现。Koa 中的 compose 函数已经提取到 koa-compose 包中,其中的核心代码如下:/* * @param {Array} 中间件数组 * @return {Function} /function compose (middleware) { // 判断是否为数组,不是则抛出异常 if (!Array.isArray(middleware)) throw new TypeError(‘Middleware stack must be an array!’) // 判断 middleware 数组中的中间件是否为函数,不是函数抛出异常 for (const fn of middleware) { if (typeof fn !== ‘function’) throw new TypeError(‘Middleware must be composed of functions!’) } /* * 此处先不执行中间件,直接返回函数 * 统一在外面进行异常判断,再开始执行中间件 / return function (context, next) { let index = -1 // 从第一个中间件开始执行 return dispatch(0) function dispatch (i) { // 同一个中间件多次调用 next 抛出异常 if (i <= index) return Promise.reject(new Error(’next() called multiple times’)) index = i // 获取第 i 个中间件 let fn = middleware[i] /* * 中间件执行结束,检查是否有传入 next 回调函数 * 此 next 并非中间件执行的 next 参数 / if (i === middleware.length) fn = next /* * 所有的返回都是Promise对象 * Promise对象可以保证中间件和返回请求对象之间的执行顺序 / if (!fn) return Promise.resolve() try { // 执行第 i 个中间件,并传入第 i + 1 个中间件 return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err) } } }}通过解析可以发现,源码相对于我们的实现更加健全:更完善的异常处理在执行前统一对传入参数进行检查多次执行 next 函数抛出异常处理等最终返回结果 Promise 化,保证中间件和整个处理函数在 Koa 中的执行顺序,具体可参考下面 Koa 源码片段:/* * application.js * fnMiddleware(ctx) 就是 compose 函数返回的函数,默认不传入 next 参数 * Promise 保证中间件,handleResponse 执行顺序。 */fnMiddleware(ctx).then(handleResponse).catch(onerror)3. 小结从最开始的编写 Koa 中间件,到现在阅读 compose 函数源码,Koa 中间件机制并不复杂,了解之后,我们可以运用、编写更合适的中间件,构建自己的 Koa 应用。本文首发于公众号,更多内容欢迎关注我的公众号: 阿夸漫谈 ...

January 19, 2019 · 2 min · jiezi

vue ssr 实现方式学习笔记

为什么要写本文呢,话说现在 vue-ssr 官网上对 vue 服务端渲染的介绍已经很全面了,包括各种服务端渲染框架比如 Nuxt.js 、 集成 Koa 和vue-server-renderer 的 node.js 框架 egg.js,都有自己的官网和团队在维护,文档真是面面俱到功能强大,但是,我个人在刚开始看这些资料的时候,总是忍不住发起灵魂三问:“我是谁?我在哪?我在干什么?”,提前没有相关知识的人开始学这些,肯定是要走一些弯路或者卡在某个点一段时间的,所以我想把我的学习经验做下总结,一方面方便自己以后查阅,一方面也会在文中加一些针对官网上没有细说的点的理解,希望能帮助你减少些学习成本,毕竟这是一个知识共享的时代嘛。本文不涉及到源码解析,主要讲解如何实现 vue 的服务端渲染,比较适合 vue-ssr 小白阅读,下面我们进入正文:先说下基本概念:ssr 的全称是 server side render,服务端渲染,vue ssr 的意思就是在服务端进行 vue 的渲染,直接对前端返回带有数据,并且是渲染好的HTML页面;而不是返回一个空的HTML页面,再由vue 通过异步请求来获取数据,再重新补充到页面中。这么做的最主要原因,就是搜索引擎优化,也就是SEO,这更利于网络爬虫去爬取和收集数据。为什么这样就有利于网络爬虫爬取呢?这里简单说一下爬虫的爬取方式,爬虫通过访问 URL 获取一个页面后,会获取当前HTML中已存在的数据,也可以理解为把拿到的 HTML 页面转为了字符串内容,然后解析、存储这些内容,但是如果页面中有些数据是通过异步请求获得的,那么爬虫是不会等待异步请求返回之后才结束对页面数据的解析的,这样就会没有爬取到这部分数据,很不利于其他搜索引擎的收录。这也就是为什么单页面网站是不具备良好的SEO效果的,因为单页面返回的就是一个基本为空的 HTML 文件,里面就一个带有ID的元素等待挂载而已,页面的内容都是通过 js 后续生成的,比如这样:<!DOCTYPE html><html lang=“en”> <head><title>Hello</title></head> <body><div id=“app”></div></body> <script src=“bundle.js”></script></html>但对于很多公司来说,公司的产品是希望能被百度、谷歌等搜索引擎收录之后,进行排名,进一步的被用户搜索到,能更利于品牌的推广、流量变现等操作,要实现这些,就必须保证产品的网页是能够被网络爬虫爬取到的,显然一个完整的带有全部数据的页面更利于爬虫的爬取,当然现在也有很多方法可以去实现针对页面异步数据的爬取,github 上也开源了很多的爬虫代码,但是这显然对于爬虫来说更加的不友好、成本更高。SSR 当然也是有着其他的好处的,比如首屏页面加载速度更快,用户等待时间更短等,其他更多概念可以查看官网 https://ssr.vuejs.org/zh/ ,这些官网上都有介绍。代码实现下面我们结合官网上的代码,做一下代码实操,来加深下理解:在官网中,提供了一个使用模块 vue-server-renderer 简单实现 vue 服务端渲染的示例: 新建一个文件夹vue-ssr-demo,进入其中执行如下命令:// 安装模块 npm install vue vue-server-renderer –save创建文件 server.js// vue-ssr-demo/server.js 示例代码//第一步,创建vue实例const Vue = require(‘vue’);const app = new Vue({ template: “<div>hello world</div>”});//第二步,创建一个rendererconst renderer = require(‘vue-server-renderer’).createRenderer();//第三步,将vue渲染为HTMLrenderer.renderToString(app, (err, html)=>{ if(err){ throw err; } console.log(html);});保存以上代码后,在 vue-ssr-demo 文件夹下打开命令行工具,执行 node server.js 命令,可得到如下 HTML 内容:➜ vue-ssr-demo node server.js <div data-server-rendered=“true”>hello world</div>好,上面的例子中我们已经让 vue 在服务端,也就是 node 环境下运行起来了,到这里其实已经实现了 vue 的服务端渲染了。可是,实际项目中使用哪有这么简单,起码数据还没渲染啊,那接下来我们看看如何渲染数据:vue-ssr 渲染数据的方式有两种,我们先看下第一种:// server.jsconst data_vue = { word: ‘Hello World!’};//第一步,创建vue实例const Vue = require(‘vue’);//vue 实例化过程中插入数据const app = new Vue({ data: data_vue, template: “<div>{{word}}</div>”});//第二步,创建一个rendererconst renderer = require(‘vue-server-renderer’).createRenderer();//第三步,将vue渲染为HTMLrenderer.renderToString(app, (err, html)=>{ if(err){ throw err; } console.log(html);}); 第一种方式,在创建 vue 实例时,将需要的数据传入 vue 的模板,使用方法与客户端 vue 一样;运行 server.js 结果如下,数据 data_vue 已经插入到 vue 模板里面了:➜ vue-ssr-demo node server.js<div data-server-rendered=“true”>Hello World!</div> 第二种,模板插值,这里我们也直接先放代码:const data_vue = { word: ‘Hello World!’};const data_tpl = { people: ‘Hello People!’};//第一步,创建vue实例const Vue = require(‘vue’);const app = new Vue({ data: data_vue, template: “<div>{{word}}</div>”});//第二步,创建一个 renderer 实例const renderer = require(‘vue-server-renderer’).createRenderer({ template: “<!–vue-ssr-outlet–><div>{{people}}</div>”});//第三步,将vue渲染为HTMLrenderer.renderToString(app, data_tpl, (err, html)=>{ if(err){ throw err; } console.log(html);});这里我们增加了数据 data_tpl,你会发现,在 renderToString 方法中传入了这个参数,那么这个参数作用在哪里呢?这就要看下官网中关于 createRenderer 和 renderToString 方法的介绍了,createRenderer: 使用(可选的)选项创建一个 Renderer 实例。 const { createRenderer } = require(‘vue-server-renderer’) const renderer = createRenderer({ / 选项 / })在选项中,就有一个参数叫 template,看官网怎么说的:template: 为整个页面的 HTML 提供一个模板。此模板应包含注释 <!–vue-ssr-outlet–>,作为渲染应用程序内容的占位符。为整个页面的 HTML 提供一个模板。此模板应包含注释 <!–vue-ssr-outlet–>,作为渲染应用程序内容的占位符。模板还支持使用渲染上下文 (render context) 进行基本插值:使用双花括号 (double-mustache) 进行 HTML 转义插值 (HTML-escaped interpolation);使用三花括号 (triple-mustache) 进行 HTML 不转义插值 (non-HTML-escaped interpolation)。根据介绍,在创建 renderer 实例时,可以通过 template 参数声明一个模板,这个模板用来干嘛呢?就用来挂载 vue 模板渲染完成之后生成的 HTML。这里要注意一下,当创建 renderer 实例时没有声明 template 参数,那么默认渲染完就是 vue 模板生成的 HTML;当创建 renderer 实例时声明了 template 参数,一定要在模板中增加一句注释 “<!–vue-ssr-outlet–>” 作为 vue 模板插入的占位符,否则会报找不到插入模板位置的错误。再次运行 server.js ,结果如下,vue 模板已成功插入,且 template 模板中的 {{people}} 变量也因在 renderToString 方法中第二位参数的传入,显示了数据:➜ vue-ssr-demo node server.js<div data-server-rendered=“true”>Hello World!</div><div>Hello People!</div>如果我们把 template 换成一个 HTML 页面的基本架构,来包裹 vue 模板,是不是就能得到一个完整页面了呢?我们来试一下:const data_vue = { word: ‘Hello World!’};const data_tpl = { people: ‘Hello People!’};//第一步,创建vue实例const Vue = require(‘vue’);const app = new Vue({ data: data_vue, template: “<div>{{word}}</div>”});//第二步,创建一个rendererconst renderer = require(‘vue-server-renderer’).createRenderer({ template: &lt;!DOCTYPE html&gt; &lt;html lang="en"&gt; &lt;head&gt;&lt;title&gt;Hello&lt;/title&gt;&lt;/head&gt; &lt;body&gt; &lt;!--vue-ssr-outlet--&gt;&lt;div&gt;{{people}}&lt;/div&gt; &lt;/body&gt; &lt;/html&gt;});//第三步,将vue渲染为HTMLrenderer.renderToString(app, data_tpl, (err, html)=>{ if(err){ throw err; } console.log(html);});运行 server.js ,结果如下,我们得到了一个完整的 HTML 页面,且成功插入了数据:➜ vue-ssr-demo node server.js<!DOCTYPE html><html lang=“en”> <head><title>Hello</title></head> <body> <div data-server-rendered=“true”>Hello World!</div><div>Hello People!</div> </body></html>好,现在页面生成了,该怎么显示呢?这里我们借助下框架 Koa 实现,先来安装:npm install koa -S然后修改 server.js ,如下:const data_vue = { word: ‘Hello World!’};const data_tpl = { people: ‘Hello People!’};const Koa = require(‘koa’);//创建 koa 实例const koa = new Koa();const Vue = require(‘vue’);//创建一个rendererconst renderer = require(‘vue-server-renderer’).createRenderer({ template: &lt;!DOCTYPE html&gt; &lt;html lang="en"&gt; &lt;head&gt;&lt;title&gt;Hello&lt;/title&gt;&lt;/head&gt; &lt;body&gt; &lt;!--vue-ssr-outlet--&gt;&lt;div&gt;{{people}}&lt;/div&gt; &lt;/body&gt; &lt;/html&gt;});// 对于任何请求,app将调用该异步函数处理请求:koa.use(async (ctx, next) => { // await next(); //创建vue实例 const app = new Vue({ data: data_vue, template: “<div>{{word}}</div>” }); //将vue渲染为HTML const body = await renderer.renderToString(app, data_tpl); ctx.body = body;});// 在端口3001监听:koa.listen(3001);console.log(‘app started at port 3001…’);运行 server.js :➜ vue-ssr-demo node server.jsapp started at port 3001…然后打开浏览器,输入网址 http://localhost:3001/ ,即可看到运行后的效果。这样就实现了一个简单的服务端渲染项目,但是我们在平常开发的时候,肯定不会这么简单的去构建一个项目,必然会用到一些,比如打包、压缩的工具,这篇就写到这里,下一篇我们尝试使用 webpack 来构建一个 vue 的服务端渲染项目。如有问题,欢迎指正!谢谢! ...

January 18, 2019 · 3 min · jiezi

vue + koa2 + webpack4 构建ssr项目

什么是服务器端渲染 (SSR)?为什么使用服务器端渲染 (SSR)?看这 Vue SSR 指南技术栈vue、vue-router、vuexkoa2webpack4axiosbabel、eslintcss、stylus、postcsspm2目录层次webpack4-ssr-config├── client # 项目代码目录│ ├── assets # css、images等静态资源目录│ ├── components # 项目自定义组件目录│ ├── plugins # 第三方插件(只能在客户端运行)目录,比如 编辑器│ ├── store # vuex数据存储目录│ ├── utils # 通用Mixins目录│ ├── views # 业务视图.vue和route路由目录│ ├── app.vue # │ ├── config.js # vue组件、mixins注册,http拦截器配置等等│ ├── entry-client.js # 仅运行于浏览器│ ├── entry-server.js # 仅运行于服务器│ ├── index.js # 通用 entry│ ├── router.js # 路由配置和相关钩子配置│ └── routes.js # 汇聚业务模块所有路由route配置├── config # 配置文件目录│ ├── http # axios封装的http请求│ ├── logger # .vue里this.[log,warn,info,error]和koa2里 logger日志输出│ ├── middle # koa2中间件目录│ │ ├── errorMiddleWare.js # 错误处理中间件│ │ ├── proxyMiddleWare.js # 接口代理中间件│ │ └── staticMiddleWare.js # 静态资源中间件│ ├── eslintrc.conf.js # eslint详细配置│ ├── index.js # server入口│ ├── koa.server.js # koa2服务详细配置│ ├── setup.dev.server.js # koa2开发模式实现hot热更新│ ├── vue.koa.ssr.js # vue ssr的koa2中间件。匹配路由、请求接口生成dom,实现SSR│ ├── webpack.base.config.js # 基本配置 (base config) │ ├── webpack.client.config.js # 客户端配置 (client config)│ └── webpack.server.config.js # 服务器配置 (server config)├── dist # 代码打包目录├── log # pm2日志输出目录├── node_modules # node包├── .babelrc # babel配置├── .eslintrc.js # eslint配置├── .gitignore # git配置├── app.config.js # 端口、代理配置、webpack配置等等├── constants.js # 存放常量├── favicon.ico # ico图标├── index.template.ejs # index模板├── package.json # ├── package-lock.json # ├── pm2.config.js # 项目pm2配置├── pm2.md # pm2的api文档├── postcss.config.js # postcss配置文件└── README.md # 文档源码结构构建使用 webpack 来打包我们的 Vue 应用程序,参考官方分成3个配置,这里使用的webpack4和官方的略有区别。├── webpack.base.config.js # 基本配置 (base config) ├── webpack.client.config.js # 客户端配置 (client config)├── webpack.server.config.js # 服务器配置 (server config)具体webpack配置代码这里省略…对于客户端应用程序和服务器应用程序,我们都要使用 webpack 打包 - 服务器需要「服务器 bundle」然后用于服务器端渲染(SSR),而「客户端 bundle」会发送给浏览器,用于混合静态标记。基本流程如下图:项目代码├── entry-client.js # 仅运行于浏览器├── entry-server.js # 仅运行于服务器├── index.js # 通用 entry├── router.js # 路由配置├── routes.js # 汇聚业务模块所有路由route配置index.jsindex.js 是我们应用程序的「通用 entry」,对外导出一个 createApp 函数。这里使用工厂模式为为每个请求创建一个新的根 Vue 实例,从而避免server端单例模式,如果我们在多个请求之间使用一个共享的实例,很容易导致交叉请求状态污染。entry-client.js:客户端 entry 只需创建应用程序,并且将其挂载到 DOM 中:import Vue from ‘vue’import { createApp } from ‘./index’// 引入http请求import http from ‘./../config/http/http’……const { app, router, store } = createApp()if (window.INITIAL_STATE) { store.replaceState(window.INITIAL_STATE) // 客户端和服务端保持一致 store.state.$http = http}router.onReady(() => { …… Promise.all(asyncDataHooks.map(hook => hook({ store, router, route: to }))) .then(() => { bar.finish() next() }) .catch(next) }) // 挂载 app.$mount(’#app’)})entry-server.js:服务器 entry 使用 default export 导出函数,并在每次渲染中重复调用此函数。此时,除了创建和返回应用程序实例之外,还在此执行服务器端路由匹配和数据预取逻辑。import { createApp } from ‘./index’// 引入http请求import http from ‘./../config/http/http’// 处理ssr期间cookies穿透import { setCookies } from ‘./../config/http/http’// 客户端特定引导逻辑……const { app } = createApp()// 这里假定 App.vue 模板中根元素具有 id="app"app.$mount(’#app’)export default context => { return new Promise((resolve, reject) => { const { app, router, store } = createApp() const { url } = context …… // 设置服务器端 router 的位置,路由配置里如果设置过base,url需要把url.replace(base,’’)掉,不然会404 router.push(url) // 等到 router 将可能的异步组件和钩子函数解析完 router.onReady(() => { …… // SSR期间同步cookies setCookies(context.cookies || {}) // http注入到rootState上,方便store里调用 store.state.$http = http // 使用Promise.all执行匹配到的Component的asyncData方法,即预取数据 Promise.all(matchedComponents.map(({ asyncData }) => asyncData && asyncData({ store, router, route: router.currentRoute, }))).then(() => { // 在所有预取钩子(preFetch hook) resolve 后, // 我们的 store 现在已经填充入渲染应用程序所需的状态。 // 当我们将状态附加到上下文, // 并且 template 选项用于 renderer 时, // 状态将自动序列化为 window.__INITIAL_STATE__,并注入 HTML。 context.state = store.state resolve(app) }).catch(reject) }, reject) })}router.js、routes.js、store.jsrouter和store也都是工厂模式,routes是业务模块路由配置的集合。routerimport Vue from ‘vue’import Router from ‘vue-router’import routes from ‘./routes’Vue.use(Router)export function createRouter() { const router = new Router({ mode: ‘history’, fallback: false, // base: ‘/ssr’, routes }) router.beforeEach((to, from, next) => { /todo * 做权限验证的时候,服务端和客户端状态同步的时候会执行一次 * 建议vuex里用一个状态值控制,默认false,同步时直接next,因为服务端已经执行过。 * / next() }) router.afterEach((route) => { /todo/ }) return router}routeimport testRoutes from ‘./views/test/routes’import entry from ‘./app.vue’const home = () => import(’./views/home.vue’)const routes = [ { path: ‘/’, component: home }, { path: ‘/test’, component: entry, children: testRoutes },]export default routesstoreimport Vue from ‘vue’import Vuex from ‘vuex’import test from ‘./modules/test’Vue.use(Vuex)export function createStore() { return new Vuex.Store({ modules: { test } })}Http请求http使用Axios库封装/ * Created by zdliuccit on 2019/1/14. * @file axios封装 * export default http 接口请求 * export addRequestInterceptor 请求前拦截器 * export addResponseInterceptor 请求后拦截器 * export setCookies 同步cookie */import axios from ‘axios’const currentIP = require(‘ip’).address()const appConfig = require(’./../../app.config’)const defaultHeaders = { Accept: ‘application/json, text/plain, /; charset=utf-8’, ‘Content-Type’: ‘application/json; charset=utf-8’, Pragma: ’no-cache’, ‘Cache-Control’: ’no-cache’,}Object.assign(axios.defaults.headers.common, defaultHeaders)if (!process.browser) { axios.defaults.baseURL = http://${currentIP}:${appConfig.appPort}}const methods = [‘get’, ‘post’, ‘put’, ‘delete’, ‘patch’, ‘options’, ‘request’, ‘head’]const http = {}methods.forEach(method => { http[method] = axios[method].bind(axios)})export const addRequestInterceptor = (resolve, reject) => { if (axios.interceptors.request.handlers.length === 0) axios.interceptors.request.use(resolve, reject)}export const addResponseInterceptor = (resolve, reject) => { if (axios.interceptors.response.handlers.length === 0) axios.interceptors.response.use(resolve, reject)}export const setCookies = Cookies => axios.defaults.headers.cookie = Cookiesexport default httpstore中已经注入到rootState,使用如下:loading({ commit, rootState: { $http } }) { return $http.get(‘path’).then(res => { … }) }在config.js中,把http注册到vue的原型链和配置request、response的拦截器import Vue from ‘vue’// 引入http请求插件import http from ‘./../config/http’// 引入log日志插件import { addRequestInterceptor, addResponseInterceptor } from ‘./../config/http/http’import titleMixin from ‘./utils/title’// 引入log日志插件import vueLogger from ‘./../config/logger/vue-logger’// 注册插件Vue.use(http)Vue.use(vueLogger)Vue.mixin(titleMixin)// request前自动添加api配置addRequestInterceptor( (config) => { /统一加/api前缀/ config.url = /api${config.url} return config }, (error) => { return Promise.reject(error) })// http 返回response前处理addResponseInterceptor( (response) => { /*todo 在这里统一前置处理请求响应 / return Promise.resolve(response.data) }, (error) => { / * todo 统一处理500、400等错误状态 * 这里reject下,交给entry-server.js的处理 */ const { response, request } = error return Promise.reject({ code: response.status, data: response.data, method: request.method, path: request.path }) })这样,.vue中间中直接调用this.$http.get()、this.$http.post()…cookies穿透在ssr期间我们需要截取客户端的cookie,保持用户会话唯一性。在entry-server.js中使用setCookies方法,传入的参数是从context上获取。…… // SSR期间同步cookies setCookies(context.cookies || {})……在vue.koa.ssr.js代码中往context注入cookie…… const context = { url: ctx.url, title: ‘Vue Koa2 SSR’, cookies: ctx.request.headers.cookie }……其他title处理参考官方用到全局变量的第三方插件、组件如何处理等等流式渲染预渲染……还有很多优化、深坑,看看官方文档、踩踩就知道了Koa官方使用express框架。express虽然现在也支持async、await,不过独爱koa。koa主文件// 引入相关包和中间件等等const Koa = require(‘koa’)…const appConfig = require(’./../app.config’)const uri = http://${currentIP}:${appConfig.appPort}// koa serverconst app = new Koa()// 定义中间件,const middleWares = [ ……]middleWares.forEach((middleware) => { if (!middleware) { return } app.use(middleware)})// vue ssr处理vueKoaSSR(app, uri)// http代理中间件app.use(proxyMiddleWare())console.log(\n&gt; Starting server... ${uri} \n)// 错误处理app.on(’error’, (err) => { // console.error(‘Server error: \n%s\n%s ‘, err.stack || ‘’)})app.listen(appConfig.appPort)vue.koa.ssr.jsvue koa2 ssr中间件开发模式直接使用setup.dev.server.jswebpack hot热更新生产模块直接读取dist目录的文件路由匹配匹配proxy代理配置,接口请求进入proxyMiddleWare.js接口代理中间件非接口进入render(),返回htmlconst fs = require(‘fs’)const path = require(‘path’)const LRU = require(’lru-cache’)const { createBundleRenderer } = require(‘vue-server-renderer’)const isProd = process.env.NODE_ENV === ‘production’const proxyConfig = require(’./../app.config’).proxyconst setUpDevServer = require(’./setup.dev.server’)module.exports = function (app, uri) { const renderData = (ctx, renderer) => { const context = { url: ctx.url, title: ‘Vue Koa2 SSR’, cookies: ctx.request.headers.cookie } return new Promise((resolve, reject) => { renderer.renderToString(context, (err, html) => { if (err) { return reject(err) } resolve(html) }) }) } function createRenderer(bundle, options) { return createBundleRenderer(bundle, Object.assign(options, { cache: LRU({ max: 1000, maxAge: 1000 * 60 * 15 }), runInNewContext: false })) } function resolve(dir) { return path.resolve(process.cwd(), dir) } let renderer if (isProd) { // prod mode const template = fs.readFileSync(resolve(‘dist/index.html’), ‘utf-8’) const bundle = require(resolve(‘dist/vue-ssr-server-bundle.json’)) const clientManifest = require(resolve(‘dist/vue-ssr-client-manifest.json’)) renderer = createRenderer(bundle, { template, clientManifest }) } else { // dev mode setUpDevServer(app, uri, (bundle, options) => { try { renderer = createRenderer(bundle, options) } catch (e) { console.log(’\nbundle error’, e) } } ) } app.use(async (ctx, next) => { if (!renderer) { ctx.type = ‘html’ return ctx.body = ‘waiting for compilation… refresh in a moment.’; } if (Object.keys(proxyConfig).findIndex(vl => ctx.url.startsWith(vl)) > -1) { return next() } let html, status try { status = 200 html = await renderData(ctx, renderer) } catch (e) { console.log(’\ne’, e) if (e.code === 404) { status = 404 html = ‘404 | Not Found’ } else { status = 500 html = ‘500 | Internal Server Error’ } } ctx.type = ‘html’ ctx.status = status ? status : ctx.status ctx.body = html })}setup.dev.server.jskoa2的webpack热更新配置和相关中间件的代码,这里就不贴出来了,和express略有区别。部署Pm2简介PM2是node进程管理工具,可以利用它来简化很多node应用管理的繁琐任务,如性能监控、自动重启、负载均衡等,而且使用非常简单。pm2.config.js配置如下module.exports = { apps: [{ name: ‘ml-app’, // app名称 script: ‘config/index.js’, // 要运行的脚本的路径。 args: ‘’, // 由传递给脚本的参数组成的字符串或字符串数组。 output: ‘./log/out.log’, error: ‘./log/error.log’, log: ‘./log/combined.outerr.log’, merge_logs: true, // 集群的所有实例的日志文件合并 log_date_format: “DD-MM-YYYY”, instances: 4, // 进程数 1、数字 2、‘max’根据cpu内核数 max_memory_restart: ‘1G’, // 当内存超过1024M时自动重启 watching: true, env_test: { NODE_ENV: ‘production’ }, env_production: { NODE_ENV: ‘production’ } }],}构建生产代码npm run build 构建生产代码pm2启动服务初次启动pm2 start pm2.config.js –env production # production 对应 env_productionorpm2 start ml-apppm2的用法和参数说明可以参考pm2.md,也可参考PM2实用入门指南Nginx在pm2基础上,Nginx配置upstream实现负载均衡在http节点下,加入upstream节点。upstream server_name { server 172.16.119.198:8018 max_fails=2 fail_timeout=30s; server 172.16.119.198:8019 max_fails=2 fail_timeout=30s; server 172.16.119.198:8020 max_fails=2 fail_timeout=30s; …..}将server节点下的location节点中的proxy_pass配置为:http:// + server_name,即“ http://server_name”.location / { proxy_pass http://server_name; proxy_set_header Host localhost; proxy_set_header X-Forwarded-For $remote_addr}详细配置参考文档如果应用服务是域名子路径ssr的话,需要注意如下location除了需要设置匹配/ssr规则之外,还需设置接口、资源的前缀比如(/api,/dist) location ~ /(ssr|api|dist) {…}vue的路由也该设置base:’/ssr’entry-server.js里router.push(url)这里,url应该把/ssr去掉,即router.push(url.replace(’/ssr’,’’’))参考文档vue官方文档koanginxpm2Demo地址 服务器带宽垃圾,将就看看。 git仓库地址还有很多不足,后续慢慢折腾….结束语:生命的价值在于瞎折腾 ...

January 17, 2019 · 6 min · jiezi

Koa 系列 — 如何编写属于自己的 Koa 中间件

Koa 是一个由 Express 原班人马打造的新的 web 框架,Koa 本身并没有捆绑任何中间件,只提供了应用(Application)、上下文(Context)、请求(Request)、响应(Response)四个模块。原本 Express 中的路由(Router)模块已经被移除,改为通过中间件的方式实现。相比较 Express,Koa 能让使用者更大程度上构建个性化的应用。1. 中间件简介Koa 是一个中间件框架,本身没有捆绑任何中间件。本身支持的功能并不多,功能都可以通过中间件拓展实现。通过添加不同的中间件,实现不同的需求,从而构建一个 Koa 应用。Koa 的中间件就是函数,可以是 async 函数,或是普通函数,以下是官网的示例:// async 函数app.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; console.log(${ctx.method} ${ctx.url} - ${ms}ms);});// 普通函数app.use((ctx, next) => { const start = Date.now(); return next().then(() => { const ms = Date.now() - start; console.log(${ctx.method} ${ctx.url} - ${ms}ms); });});中间件可以通过官方维护的仓库查找获取,也可以根据需求编写属于自己的中间件。2. 中间件原理2.1 示例下面是一个的 Koa 应用,简单演示了中间件的执行顺序:const Koa = require(‘Koa’);const app = new Koa();// 最外层的中间件app.use(async (ctx, next) => { await console.log(第 1 个执行); await next(); await console.log(第 8 个执行);});// 第二层中间件app.use(async (ctx, next) => { await console.log(第 2 个执行); await console.log(第 3 个执行); await next(); await console.log(第 6 个执行); await console.log(第 7 个执行);});// 最里层的中间件app.use(async (ctx, next) => { await console.log(第 4 个执行); ctx.body = “Hello world.”; await console.log(第 5 个执行);});app.listen(3000, () => { console.log(Server port is 3000.);})2.2 原理从上面的示例中可以看出,中间件的执行顺序并不是从头到尾,而是类似于前端的事件流。事件流是先进行事件捕获,到达目标,然后进行事件冒泡。中间件的实现过程也是一样的,先从最外面的中间件开始执行,next() 后进入下一个中间件,一路执行到最里面的中间件,然后再从最里面的中间件开始往外执行。Koa 中间件采用的是洋葱圈模型,每次执行下一个中间件传入两个参数 ctx 和 next,参数 ctx 是由 koa 传入的封装了 request 和 response 的变量,可以通过它访问 request 和 response,next 就是进入下一个要执行的中间件。3. 编写属于自己的中间件3.1 token 验证的 middleware前后端分离开发,我们常采用 JWT 来进行身份验证,其中 token 一般放在 HTTP 请求中的 Header Authorization 字段中,每次请求后端都要进行校验,如 Java 的 Spring 框架可以在过滤器中对 token 进行统一验证,而 Koa 则通过编写中间件来实现 token 验证。// token.js// token 中间件module.exports = (options) => async (ctx, next) { try { // 获取 token const token = ctx.header.authorization if (token) { try { // verify 函数验证 token,并获取用户相关信息 await verify(token) } catch (err) { console.log(err) } } // 进入下一个中间件 await next() } catch (err) { console.log(err) }}// app.js// 引入 token 中间件const Koa = require(‘Koa’);const app = new Koa();const token = require(’./token’)app.use(token())app.listen(3000, () => { console.log(Server port is 3000.);})3.2 log 的 middleware日志模块也是线上不可缺少的一部分,完善的日志系统可以帮助我们迅速地排查出线上的问题。通过 Koa 中间件,我们可以实现属于自己的日志模块// logger.js// logger 中间件const fs = require(‘fs’)module.exports = (options) => async (ctx, next) => { const startTime = Date.now() const requestTime = new Date() await next() const ms = Date.now() - startTime; let logout = ${ctx.request.ip} -- ${requestTime} -- ${ctx.method} -- ${ctx.url} -- ${ms}ms; // 输出日志文件 fs.appendFileSync(’./log.txt’, logout + ‘\n’)}// app.js// 引入 logger 中间件const Koa = require(‘Koa’);const app = new Koa();const logger = require(’./logger’)app.use(logger())app.listen(3000, () => { console.log(Server port is 3000.);})可以结合 log4js 等包来记录更详细的日志4. 总结至此,我们已经了解中间件的原理,以及如何实现一个自己的中间件。中间件的代码通常比较简单,我们可以通过阅读官方维护的仓库中优秀中间件的源码,来加深对中间件的理解和运用。本文首发于公众号,更多内容欢迎关注我的公众号:阿夸漫谈 ...

January 15, 2019 · 2 min · jiezi

搭建一个小型的koa后端开发环境

以免忘记,慢慢丰富初始化准备开始开发,依旧是常规的初始化// 初始化文件夹npm init// 安装koanpm i koa –save-devhelloworld还是先按照官方文档helloword一下,测试koa的正常使用。基本的文档,可以看这里koa。在根目录新建app.js文件,输入以下内容。// 必修的 hello world 应用:const Koa = require(‘koa’);const app = new Koa();app.use(async ctx => { ctx.body = ‘Hello World’;});app.listen(3000);输入命令运行node app.js则会在本地的3000端口生成一个内容为Hello World的页面。这样,我们一个基本的服务就生成并且启动成功了。正式搭建开发环境使用路由塑造接口有两种是很多人常用的,我最开始见到别人用的是koa-route,也用了,但是最近发现koa-router是被koa-route推荐的,也是用的人最多的,所以两种都尝试一下。koa-route安装封装好的koa-route模块npm i koa-route –save-dev更改app.js文件,这也是官方给的例子。const Koa = require(‘koa’);const app = new Koa();var _ = require(‘koa-route’);var db = { tobi: { name: ’tobi’, species: ‘ferret’ }, loki: { name: ’loki’, species: ‘ferret’ }, jane: { name: ‘jane’, species: ‘ferret’ }};var pets = { list: (ctx) => { var names = Object.keys(db); ctx.body = ‘pets: ’ + names.join(’, ‘); }, show: (ctx, name) => { var pet = db[name]; if (!pet) return ctx.throw(‘cannot find that pet’, 404); ctx.body = pet.name + ’ is a ’ + pet.species; }};app.use(_.get(’/pets’, pets.list));app.listen(3000);console.log(’listening on port 3000’);至此一个简单的接口完成,我们可以通过浏览器访问到数据。koa-router来自koa-router的官方的例子。修改app.js文件var Koa = require(‘koa’);var app = new Koa();var Router = require(‘koa-router’);var router = new Router();router.get(’/test’, (ctx, next) => { ctx.body = ‘Hello World!’;});app .use(router.routes()) .use(router.allowedMethods());app.listen(3001);console.log(’listening on port 3001’);跨域由于我模拟的是前后端分离的项目,即后端采用本地3001端口开启服务,前端采用8081访问页面,那么前端请求后端接口必定跨域,浏览器报错因此我们修改设置 Access-Control-Allow-Origin:,允许所有域名的脚本访问该资源。app.use(async (ctx, next) => { ctx.set(“Access-Control-Allow-Origin”, “”); await next();});重启服务即可拿取数据。nodemon热更新开发中,我们当然需要在修改文件后,项目能够自动重启,方便调用,nodemon是一个比较常见的解决方式。安装nodemonnpm install –save-dev nodemon使用nodemon来代替node启动服务即可nodemon app有大佬写了很详细的教程koa框架教程-阮一峰 ...

December 26, 2018 · 1 min · jiezi

koa2 一网打尽(基本使用,洋葱圈,中间件机制和模拟,源码分析(工程,核心模块,特殊处理),核心点,生态)

本文 github 地址: https://github.com/HCThink/h-blog/blob/master/source/koa2/readme.mdgithub 首页(star+watch,一手动态直达): https://github.com/HCThink/h-blog掘金 link , 掘金 专栏segmentfault 主页原创禁止私自转载koa2koa homepage优秀的下一代 web 开发框架。Koa 应用程序不是 HTTP 服务器的1对1展现。 可以将一个或多个 Koa 应用程序安装在一起以形成具有单个HTTP服务器的更大应用程序。基础使用快速搭建简易 koa server 服务koa 搭建一个服务还是很简单的, 主要代码如下, 完整代码如下. 切到主目录下,安装依赖: yarn执行入口: yarn startkoa demo 目录koa demo 主文件import Koa from ‘koa’;import https from ‘https’;import open from ‘open’;const Log = console.log;const App = new Koa();App.use(async (ctx, next) => { ctx.body = ‘Hello World’; Log(‘mid1 start…’); await next(); Log(‘mid1 end…’);});App.use(async (ctx, next) => { debugger; Log(‘mid2 start…’); await next(); Log(‘mid2 end…’);});App.use((ctx, next) => { Log(‘mid3…’);});// 服务监听: 两种方式。App.listen(3000); // 语法糖// http.createServer(app.callback()).listen(3000);https.createServer(App.callback()).listen(3001);open(‘http://localhost:3000’);// 如下为执行顺序, 实际上 http 会握手,所以输出多次// 如下执行特征也就是洋葱圈, 实际上熟悉 async、await 则不会比较意外。// mid1 start…// mid2 start…// mid3…// mid2 end…// mid1 end…koa2特性封装并增强 node http server[request, response],简单易容。洋葱圈处理模型。基于 async/await 的灵活强大的中间件机制。通过委托使得 api 在使用上更加便捷易用。api参考官网提供的基本 api ,不在赘述: https://koa.bootcss.com/部分 api 实现,参考: 源码分析常用 apiapp.listen: 服务端口监听app.callback: 返回适用于 http.createServer() 方法的回调函数来处理请求。你也可以使用此回调函数将 koa 应用程序挂载到 Connect/Express 应用程序中。app.use(function): 挂载中间件的主要方法。核心对象contextKoa Context 将 node 的 request 和 response 对象封装到单个对象中,为编写 Web 应用程序和 API 提供了许多有用的方法。 这些操作在 HTTP 服务器开发中频繁使用,它们被添加到此级别而不是更高级别的框架,这将强制中间件重新实现此通用功能。每个 请求都将创建一个 Context,并在中间件中作为接收器引用,或者 ctx 标识符。ctx.res requestctx.req: responsectx.request: koa request toolctx.response: koa response toolctx.cookiesctx.request.accepts(types): type 值可能是一个或多个 mime 类型的字符串,如 application/json,扩展名称如 json,或数组 [“json”, “html”, “text/plain”]。request.acceptsCharsets(charsets)…更多参考洋葱圈使用层面koa 洋葱圈执行机制图解koa 洋葱圈koa demo, koa demo 源码洋葱圈简易实现版执行方式: tsc onionRings.ts –lib ’es2015’ –sourceMap && node onionRings.js洋葱圈简易实现版 main, 洋葱圈简易实现版 源码简易实现 外部中间件参考: koa-bodyparsermain codepublic use(middleware: Function) { this.middList.push(middleware);}// 执行器private async deal(i: number = 0) { debugger; if (i >= this.middList.length) { return false; } await this.middList[i](this, this.deal.bind(this, i + 1));}实现思路use 方法注册 middleware。deal 模拟一个执行器: 大致思路就是将下一个 middleware 作为上一个 middleware 的 next 去 await,用以保证正确的执行顺序和中断。问题如果习惯了回调的思路, 你会不会有这种疑惑: 洋葱圈机制于在 一个中间件中调用另一个中间件,被调中间件执行成功,回到当前中间件继续往后执行,这样不断调用,中间件很多的话, 会不会形成一个很深的函数调用栈? 从而影响性能, 同时形成「xx 地狱」? – ps(此问题源于分享时原同事 小龙 的提问。)实际上这是个很好的问题,对函数执行机制比较了解才会产生的疑问。排除异步代码处理,我们很容易用同步方式模拟出这种调用层级。参考: 同步方式。 这种模式存在明显的调用栈问题。我可以负责任的回答: 不会的,下一个问题。 ???? ????不会的原因在 generator 中详细介绍,一两句说不清楚。实际上我认为这里是有语法门槛的。在 generator 之前,用任何方式处理这个问题,都显得怪异,而且难以解调用决层级带来的性能, 调试等带来问题。详细说明参考: generator 真.协程源码KOA 源码特别精简, 不像 Express 封装的功能那么多, git 源码: 【https://github.com/koajs/koa】工程koa2 的源码工程结构非常简洁,一目了然, 没有花里胡哨的东西。主文件├── History.md├── ….├── Readme.md├── benchmarks├── docs // doc│ ├── api ……├── lib // 源码│ ├── application.js // 入口文件,封装了context,request,response,核心的中间件处理流程。│ ├── context.js // context.js 处理应用上下文,里面直接封装部分request.js和response.js的方法│ ├── request.js // request.js 处理http请求│ └── response.js // response.js 处理http响应├── package.json└── test // 测试模块 ├── application ….package.jsonjest 做测试node 版本{ “engines”: { “node”: “^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4” }}主入口: “main”: “lib/application.js"koa 核心模块封装的 http server(node)核心对象 context, request、response中间件机制和剥洋葱模型的实现错误捕获和错误处理源码application.jsapplication.js 是 koa 的入口,继承了events , 所以框架有事件监听和事件触发的能力。application 还暴露了一些常用的api,比如toJSON、listen、use等等。context.jsrequest.jsresponse.js特殊处理委托摘自 context.js:context.jsconst proto = module.exports = { // …};delegate(proto, ‘response’) .method(‘attachment’) .method(‘redirect’) .method(‘remove’) .method(‘vary’) .method(‘set’) .method(‘append’) .method(‘flushHeaders’) .access(‘status’) .access(‘message’) .access(‘body’) .access(’length’) .access(’type’) .access(’lastModified’) .access(’etag’) .getter(‘headerSent’) .getter(‘writable’);delegate(proto, ‘request’) .method(‘acceptsLanguages’) .method(‘acceptsEncodings’) .method(‘acceptsCharsets’) .method(‘accepts’) .method(‘get’) .method(‘is’) .access(‘querystring’) .access(‘idempotent’) .access(‘socket’) .access(‘search’) .access(‘method’) .access(‘query’) .access(‘path’) .access(‘url’) .access(‘accept’) .getter(‘origin’) .getter(‘href’) .getter(‘subdomains’) .getter(‘protocol’) .getter(‘host’) .getter(‘hostname’) .getter(‘URL’) .getter(‘header’) .getter(‘headers’) .getter(‘secure’) .getter(‘stale’) .getter(‘fresh’) .getter(‘ips’) .getter(‘ip’);koa 为了方便串联中间件,提供了一个 context 对象,并且把核心的 response, request 对象挂载在上面, 但是这样往往就造成使用上写法冗余, eg: ctx.response.body, 而且某些对象还是经常使用的,这很不方便,所以产生了 delegates 库,用于委托操作, 委托之后,就可以在 ctx 上直接使用部分委托属性: ctx.body。源码分析如下delegates 源码解析delegates 库源码文件middleware 机制koa-composekoa 中 use 用来注册中间件,实际上是将多个中间件放入一个缓存队列中 this.middleware.push(fn);,然后通过koa-compose这个插件进行递归组合。因此严格来讲 middleware 的执行结构的组织并不在 koa 源码中完成,而是在依赖库 koa-compose 中。 koa 中使用: const fn = compose(this.middleware); 完成中间件的组合。koa-compose 核心逻辑如下, 主要思路大致是: 通过包装 middleware List 返回一个 组装好的执行器。组装思路是:将下一个 middleware 进行包装【执行器 + promise 化】作为上一个 middleware 的 next【dispatch.bind(null, i + 1)】。同时给中间件提供 context 对象。return function (context, next) { // last called middleware # let index = -1 return dispatch(0) function dispatch (i) { if (i <= index) return Promise.reject(new Error(’next() called multiple times’)) index = i let fn = middleware[i] if (i === middleware.length) fn = next // 函数洋葱的最后补上一个Promise.resolve(); if (!fn) return Promise.resolve() try { // middleware 是 async 函数, 返回 promise 。Promise.resolve 确保中间件执行完成 // 提供 ctx, next fn: dispatch.bind(null, i + 1) return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err) } }}koa-composekoa-compose 是一个非常精简的库,不做单独分析了, 他提供了一种主调型的递归: fn(context, dispatch.bind(null, i + 1)) , 这种方式可以认为是’懒递归’, 将递归的执行交给主调者控制,这样能够在更合适的时机执行后续处理, 但如果某个中间件不调用 next,那么其后的中间件就不被执行了。这和 js 协程【generator】有机制上的类似,都是使用者来控制 next 的执行时机, 可类比学习。generator易用性处理koa 非常易用, 原因是 koa 在源码层面做了大量的 委托 和针对复杂对象的封装,如 request, response 的 get/set. 用以提高工具的可用度,易用度。实际上我认为这一点是现代框架非常重要的东西,脱离用户的库都不是好库。koa 是好库。delegates 上面说过, 参考: delegates。get/setrequest, response 两个文件千行代码, 80% 左右的都是 get、set,参考:request.jsresponse.js另一方面,表现在 application.js 的 createContext 方法中,通过挂载引用和委托配合 get set 的实践配合提升易用度,单独不太好讲,分析注释在源码中。异常捕获中间件异常捕获, koa1 中间件基于 generator + co, koa2 中间件基于 async/await, async 函数返回 promise, 所以只要在组合中间件后 catch 即可捕获中间件异常fnMiddleware(ctx).then(handleResponse).catch(onerror);框架层发生错误的捕获机制, 这个通过继承 event 模块很容易实现监听。this.on(’error’, this.onerror);注册的 error 事件, 在 context.onerror 中被 emit this.app.emit(’error’, err, this);http 异常处理 : Execute a callback when a HTTP request closes, finishes, or errors.onFinished(res, onerror); // application.handleRequest中间件交互初用中间件可能会有一个疑问: 中间件如何通信?事实上这是个设计取舍逻辑, 中间件之间的数据交互并不是麻烦事, 特别是在 ECMAScript 推出 async await 之后,但问题是这样做的意义不大,原因是所有的中间件是可任意插拔组合的,这种不确定性,导致了中间件之间的数据交互就变得不稳定,最起码的数据格式就没办法固定,就更别谈处理了。灵活的插件机制导致中间件之间的交互难有统一层面的实现。另一方面从中间件的定位来看,其之间也没必要交互,中间件不能脱离 http 的请求响应而独立存在,他是服务于整个过程的,也因此所有的中间件第一个参数就是 ctx, 这个对象挂载了 request 和 response, 以及 koa 提供的封装和工具操作。核心点中断这是洋葱圈非常核心的支撑点, 我们稍微留意就能发现 koa 中间件执行机制于普通 js 的执行顺序很不一致, 我们看如下代码:app.use(async (cxt, next) => { Log(1); await next(); Log(2);});app.use(async (cxt, next) => { Log(3); await next(); Log(4);});上述代码执行顺序也就是洋葱圈: Log(1) -> await next (Log(3)) -> await next -> Log(4) -> Log(2).为了保证代码按照洋葱模型的执行顺序执行,程序需要在调用 next 的时候让代码等待,我称之为中断。实际上以前想要实现这种执行顺序,只能依赖 cb, promise.then 来模拟,而且即便实现了,在写法上也显得臃肿和别扭,要么是写出很胖的函数,要么是写出很长的函数。而且没法处理调用栈的问题。async/await 可以比较优雅的实现这种具有同步执行特征的前端代码来处理异步,代码执行到 await 这里,等待 await 表达式的执行,执行完成之后,接着往后执行。实际上这很类似于 generator 的 yield,特性。async 也就是 generator + 执行器的一个语法糖, 参考:async / awaitcogeneratorasync ? no , it’s generatorkoa.use 得确直接使用 async 函数处理中间件及其中可能存在的异步, 而 async/await 实现上是基于 generator 。async 在使用上可讲的点通常在他的 task 放在哪,以及执行时机 和 timeout ,promise 的执行顺序等。真正的中断特性得益于 generator。一位不愿透漏姓名的同事问了我一个问题,怎么证明 async 是 generator + 执行器 的语法糖?这是不得不讨论一个问题。相关的讨论参考: Async / Await > #generator 部分探讨生态koa 中间件并没有一个统一的 market 之类的地方,说实话找起来不是那么方便。如果你想找中间件的话,可以在 npm 上用 koa- 做关键字检索: https://www.npmjs.com/search?…官方 middleware源码使用的中间件koa-compose上面已有分析koa-is-jsonfunction isJSON(body) { if (!body) return false; if (‘string’ == typeof body) return false; if (‘function’ == typeof body.pipe) return false; if (Buffer.isBuffer(body)) return false; return true;}koa-convert用于兼容处理 generator 中间件,基本可以认为是 co + generator 中间件【也依赖 koa-compose 进行组织】other koa社区常用中间件合集: some middleware参考 & 鸣谢https://koa.bootcss.com/http://nodejs.cn/api/fs.html#…https://juejin.im/post/5ba786… ...

December 13, 2018 · 4 min · jiezi

koa,express,node 通用方法连接MySQL

这个教程不管node,express,koa都可以用下面方法连接,这里用koa做个参考新建文件目录,我是这样子的很多教程都没有涉及到版本,所以让很多初学者,拷贝他的代码,出现错误问题我的版本: “dependencies”: { “koa”: “^2.6.2”, “mysql”: “^2.16.0” }1.设置配置文件// default.js// 设置配置文件const config = { // 启动端口 port: 3000, // 数据库配置 database: { DATABASE: ‘ceshi’, USERNAME: ‘root’, PASSWORD: ‘1234’, PORT: ‘3306’, HOST: ’localhost’ } } module.exports = config2.连接数据库// mysql/index.jsvar mysql = require(‘mysql’);var config = require(’../config/default.js’)var pool = mysql.createPool({ host : config.database.HOST, user : config.database.USERNAME, password : config.database.PASSWORD, database : config.database.DATABASE});class Mysql { constructor () { } query () { return new Promise((resolve, reject) => { pool.query(‘SELECT * from ceshidata’, function (error, results, fields) { if (error) { throw error }; resolve(results) // console.log(‘The solution is: ‘, results[0].solution); }); }) }}module.exports = new Mysql()3.设置服务器// index.jsconst Koa = require(‘koa’)const config = require(’./config/default’)const mysql = require(’./mysql’)const app = new Koa()app.use(async (ctx) => { let data = await mysql.query() ctx.body = { “code”: 1, “data”: data, “mesg”: ‘ok’ } })app.listen(config.port)console.log(listening on port ${config.port})4.启动服务器,去浏览器访问先去数据库添加点数据node index.js打开浏览器localhost:3000, 然后你就会看到以下数据,自己添加的数据查询出来了然后其他相关操作,可以看mysql相关API,我下次也会分享出来首发于微信公众号:node前端不妨关注一下,我们一起学习回复:100有福利哦 ...

November 15, 2018 · 1 min · jiezi

node实现分片下载

本文基于http Range Requests协议,实现了分片下载的功能。使用场景包括基于浏览器的流文件片段传输、基于客户端的分片下载等。原理http通过Range Requests相关的header,可以与服务器进行协商,实现分部分的请求。这里就不细说具体协议内容了,具体可以参考这两篇文章,解释的非常详细:https://tools.ietf.org/html/rfc7233https://www.oschina.net/translate/http-partial-content-in-node-js下面贴一下实现过程,代码可以在git查看:https://github.com/keller35/partial。服务端代码服务端用node实现:const fs = require(‘fs’);const path = require(‘path’);const Koa = require(‘koa’);const app = new Koa();const PATH = ‘./resource’;app.use(async ctx => { const file = path.join(__dirname, ${PATH}${ctx.path}); // 1、404检查 try { fs.accessSync(file); } catch (e) { return ctx.response.status = 404; } const method = ctx.request.method; const { size } = fs.statSync(file); // 2、响应head请求,返回文件大小 if (‘HEAD’ == method) { return ctx.set(‘Content-Length’, size); } const range = ctx.headers[‘range’]; // 3、通知浏览器可以进行分部分请求 if (!range) { return ctx.set(‘Accept-Ranges’, ‘bytes’); } const { start, end } = getRange(range); // 4、检查请求范围 if (start >= size || end >= size) { ctx.response.status = 416; return ctx.set(‘Content-Range’, bytes */${size}); } // 5、206分部分响应 ctx.response.status = 206; ctx.set(‘Accept-Ranges’, ‘bytes’); ctx.set(‘Content-Range’, bytes ${start}-${end ? end : size - 1}/${size}); ctx.body = fs.createReadStream(file, { start, end });});app.listen(3000, () => console.log(‘partial content server start’));function getRange(range) { var match = /bytes=([0-9])-([0-9])/.exec(range); const requestRange = {}; if (match) { if (match[1]) requestRange.start = Number(match[1]); if (match[2]) requestRange.end = Number(match[2]); } return requestRange;}代码实现的功能逻辑大致是:对请求的资源做检查,不存在则响应404对于HEAD请求,返回资源大小如果GET请求没有告知range,返回Content-Length,告知浏览器可以进行分片请求如果请求设置了range,则检查range是否合法,不合法返回合法的rangge一切正常,获取文件range范围部分,做流响应代码很简单,把Range Requests协议对应实现一遍就ok了,当然这里没有完全实现协议的内容,但已经满足了这里演示的需求。服务端代码ok了,用一个浏览器的demo来检验一下。浏览器例子现代浏览器基本都实现了Range Requests,这里用audio标签作为例子。<html> <head> <title>分片流传输</title> <script type=“text/javascript”> function jump() { const player = document.getElementById(‘musicPlayer’); // 从30s开始播放 player.currentTime = 30; } </script> </head> <body> <audio id=“musicPlayer” src=“http:127.0.0.1:3000/source.mp3” controls></audio> <button onclick=“jump()">切到30s</button> </body></html>最终的效果是这样的:对比两张图,当html加载完成,浏览器自动请求资源,此时header有Range: bytes=0-,表示从第0 byte开始加载资源;当点击跳到30s处播放时,此时header变成了Range: bytes=3145728-。同样用这个服务端代码,还可以实现一个客户端,模拟一下分包下载。node分包下载这个例子演示了,对一个资源,并发的实现分部分的下载,然后再合并成一个文件。这里也是用node实现:import request from ‘request’;import path from ‘path’;import fs from ‘fs’;const SINGLE = 1024 * 1000;const SOURCE = ‘http://127.0.0.1:3000/source.mp3’;request({ method: ‘HEAD’, uri: SOURCE,}, (err, res) => { if (err) return console.error(err); const file = path.join(__dirname, ‘./download/source.mp3’); try { fs.closeSync(fs.openSync(file, ‘w’)); } catch (err) { return console.error(err); } const size = Number(res.headers[‘content-length’]); const length = parseInt(size / SINGLE); for (let i=0; i<length; i++) { let start = i * SINGLE; let end = i == length ? (i + 1) * SINGLE - 1 : size - 1; request({ method: ‘GET’, uri: SOURCE, headers: { ‘range’: bytes=${start}-${end} }, }).on(‘response’, (resp) => { const range = resp.headers[‘content-range’]; const match = /bytes ([0-9])-([0-9])/.exec(range); start = match[1]; end = match[2]; }).pipe(fs.createWriteStream(file, {start, end})); }});代码比较简单,就是开启多个http请求,并发的下载资源,然后根据响应的content-range,写到文件的对应位置。参考文章:https://www.oschina.net/translate/http-partial-content-in-node-jshttps://tools.ietf.org/html/rfc7233 ...

October 17, 2018 · 2 min · jiezi

async语法升级踩坑小记

从今年过完年回来,三月份开始,就一直在做重构相关的事情。 就在今天刚刚上线了最新一次的重构代码,希望高峰期安好,接近半年的Node.js代码重构。 包含从callback+async.waterfall到generator+co,统统升级为了async,还顺带推动了TypeScript在我司的使用。 这些日子也踩了不少坑,也总结了一些小小的优化方案,进行精简后将一些比较关键的点,拿出来分享给大家,希望有同样在做重构的小伙伴们可以绕过这些。为什么要升级首先还是要谈谈改代码的理由,毕竟重构肯定是要有合理的理由的。 如果单纯想看升级相关事项可以直接选择跳过这部分。Callback从最原始的开始说起,期间确实遇到了几个年代久远的项目,Node 0.x,使用的普通callback,也有一些会应用上async.waterfall这样在当年看起来很优秀的工具。// 普通的回调函数调用var fs = require(‘fs’)fs.readFile(’test1.txt’, function (err, data1) { if (err) return console.error(err) fs.readFile(’test2.txt’, function (err, data2) { if (err) return console.error(err) // 执行后续逻辑 console.log(data1.toString() + data2.toString()) // … })})// 使用了async以后的复杂逻辑var async = require(‘fs’)async.waterfall([ function (callback) { fs.readFile(’test1.txt’, function (err, data) { if (err) callback(err) callback(null, data.toString()) }) }, function (result, callback) { fs.readFile(’test2.txt’, function (err, data) { if (err) callback(err) callback(null, result + data.toString()) }) }], function (err, result) { if (err) return console.error(err) // 获取到正确的结果 console.log(result) // 输出两个文件拼接后的内容})虽说async.waterfall解决了callback hell的问题,不会出现一个函数前边有二三十个空格的缩进。 但是这样的流程控制在某些情况下会让代码变得很诡异,例如我很难在某个函数中选择下一个应该执行的函数,而是只能按照顺序执行,如果想要进行跳过,可能就要在中途的函数中进行额外处理:async.waterfall([ function (callback) { if (XXX) { callback(null, null, null, true) } else { callback(null, data1, data2) } }, function (data1, data2, isPass, callback) { if (isPass) { callback(null, null, null, isPass) } else { callback(null, data1 + data2) } }])所以很可能你的代码会变成这样,里边存在大量的不可读的函数调用,那满屏充斥的null占位符。 所以callback这种形式的,一定要进行修改, 这属于难以维护的代码。Generator实际上generator是依托于co以及类似的工具来实现的将其转换为Promise,从编辑器中看,这样的代码可读性已经没有什么问题了,但是问题在于他始终是需要额外引入co来帮忙实现的,generator本身并不具备帮你执行异步代码的功能。 不要再说什么async/await是generator的语法糖了 因为我司Node版本已经统一升级到了8.11.x,所以async/await语法已经可用。 这就像如果document.querySelectorAll、fetch已经可以满足需求了,为什么还要引入jQuery呢。 所以,将generator函数改造为async/await函数也是势在必行。期间遇到的坑将callback的升级为async/await其实并没有什么坑,反倒是在generator + co 那里遇到了一些问题:数组执行的问题在co的代码中,大家应该都见到过这样的:const results = yield list.map(function * (item) { return yield getData(item)})在循环中发起一些异步请求,有些人会告诉你,从yield改为async/await仅仅替换关键字就好了。 那么恭喜你得到的results实际上是一个由Promise实例组成的数组。const results = await list.map(async item => { return await getData(item)})console.log(results) // [Promise, Promise, Promise, …]因为async并不会判断你后边的是不是一个数组(这个是在co中有额外的处理)而仅仅检查表达式是否为一个Promise实例。 所以正确的做法是,添加一层Promise.all,或者说等新的语法await*,Node.js 10.x貌似还不支持。。// 关于这段代码的优化方案在下边的建议中有提到const results = await Promise.all(list.map(async item => { return await getData(item)}))console.log(results) // [1, 2, 3, …]await / yield 执行顺序的差异这个一般来说遇到的概率不大,但是如果真的遇到了而栽了进去就欲哭无泪了。 首先这样的代码在执行上是没有什么区别的:yield 123 // 123await 123 // 123这样的代码也是没有什么区别的:yield Promise.resolve(123) // 123await Promise.resolve(123) // 123但是这样的代码,问题就来了:yield true ? Promise.resolve(123) : Promise.resolve(233) // 123await true ? Promise.resolve(123) : Promise.resolve(233) // Promise<123>从字面上我们其实是想要得到yield那样的效果,结果却得到了一个Promise实例。 这个是因为yield、await两个关键字执行顺序不同所导致的。 在MDN的文档中可以找到对应的说明:MDN | Operator precedence 可以看到yield的权重非常低,仅高于return,所以从字面上看,这个执行的结果很符合我们想要的。 而await关键字的权重要高很多,甚至高于最普通的四则运算,所以必然也是高于三元运算符的。 也就是说await版本的实际执行是这样子的:(await true) ? Promise.resolve(123) : Promise.resolve(233) // Promise<123>那么我们想要获取预期的结果,就需要添加()来告知解释器我们想要的执行顺序了:await (true ? Promise.resolve(123) : Promise.resolve(233)) // 123一定不要漏写 await 关键字这个其实算不上升级时的坑,在使用co时也会遇到,但是这是一个很严重,而且很容易出现的问题。 如果有一个异步的操作用来返回一个布尔值,告诉我们他是否为管理员,我们可能会写这样的代码:async function isAdmin (id) { if (id === 123) return true return false}if (await isAdmin(1)) { // 管理员的操作} else { // 普通用户的操作}因为这种写法接近同步代码,所以遗漏关键字是很有可能出现的:if (isAdmin(1)) { // 管理员的操作} else { // 普通用户的操作}因为async函数的调用会返回一个Promise实例,得益于我强大的弱类型脚本语言,Promise实例是一个Object,那么就不为空,也就是说会转换为true,那么所有调用的情况都会进入if块。 那么解决这样的问题,有一个比较稳妥的方式,强制判断类型,而不是简单的使用if else,使用类似(a === 1)、(a === true)这样的操作。_eslint、ts 之类的都很难解决这个问题_一些建议何时应该用 async ,何时应该直接用 Promise首先,async函数的执行返回值就是一个Promise,所以可以简单地理解为async是一个基于Promise的包装:function fetchData () { return Promise().resolve(123)}// ==>async function fetchData () { return 123}所以可以认为说await后边是一个Promise的实例。 而针对一些非Promise实例则没有什么影响,直接返回数据。 在针对一些老旧的callback函数,当前版本的Node已经提供了官方的转换工具util.promisify,用来将符合Error-first callback规则的异步操作转换为Promise实例: 而一些没有遵守这样规则的,或者我们要自定义一些行为的,那么我们会尝试手动实现这样的封装。 在这种情况下一般会采用直接使用Promise,因为这样我们可以很方便的控制何时应该reject,何时应该resolve。 但是如果遇到了在回调执行的过程中需要发起其他异步请求,难道就因为这个Promise导致我们在内部也要使用.then来处理么?function getList () { return new Promise((resolve, reject) => { oldMethod((err, data) => { fetch(data.url).then(res => res.json()).then(data => { resolve(data) }) }) })}await getList()但上边的代码也太丑了,所以关于上述问题,肯定是有更清晰的写法的,不要限制自己的思维。 async也是一个普通函数,完全可以放在任何函数执行的地方。 所以关于上述的逻辑可以进行这样的修改:function getList () { return new Promise((resolve, reject) => { oldMethod(async (err, data) => { const res = await fetch(data.url) const data = await res.json() resolve(data) }) })}await getList()这完全是一个可行的方案,对于oldMethod来说,我按照约定调用了传入的回调函数,而对于async匿名函数来说,也正确的执行了自己的逻辑,并在其内部触发了外层的resolve,实现了完整的流程。 代码变得清晰很多,逻辑没有任何修改。合理的减少 await 关键字await只能在async函数中使用,await后边可以跟一个Promise实例,这个是大家都知道的。 但是同样的,有些await其实并没有存在的必要。 首先有一个我面试时候经常会问的题目:Promise.resolve(Promise.resolve(123)).then(console.log) // ?最终输出的结果是什么。 这就要说到resolve的执行方式了,如果传入的是一个Promise实例,亦或者是一个thenable对象(简单的理解为支持.then((resolve, reject) => {})调用的对象),那么resolve实际返回的结果是内部执行的结果。 也就是说上述示例代码直接输出123,哪怕再多嵌套几层都是一样的结果。 通过上边所说的,不知大家是否理解了 合理的减少 await 关键字 这句话的意思。 结合着前边提到的在async函数中返回数据是一个类似Promise.resolve/Promise.reject的过程。 而await就是类似监听then的动作。所以像类似这样的代码完全可以避免:const imgList = []async function getImage (url) { const res = await fetch(url) return await res.blob()}await Promise.all(imgList.map(async url => await getImage(url)))// ==>async function getImage (url) { const res = fetch(url) return res.blob()}await Promise.all(imgList.map(url => getImage(url)))上下两种方案效果完全相同。Express 与 koa 的升级首先,Express是通过调用response.send来完成请求返回数据的。 所以直接使用async关键字替换原有的普通回调函数即可。 而Koa也并不是说你必须要升级到2.x才能够使用async函数。 在Koa1.x中推荐的是generator函数,也就意味着其内部是调用了co来帮忙做转换的。 而看过co源码的小伙伴一定知道,里边同时存在对于Promise的处理。 也就是说传入一个async函数完全是没有问题的。 但是1.x的请求上下文使用的是this,而2.x则是使用的第一个参数context。 所以在升级中这里可能是唯一需要注意的地方,在1.x不要使用箭头函数来注册中间件。// expressexpress.get(’/’, async (req, res) => { res.send({ code: 200 })})// koa1.xrouter.get(’/’, async function (next) { this.body = { code: 200 }})// koa2.xrouter.get(’/’, async (ctx, next) => { ctx.body = { code: 200 }})小结重构项目是一件很有意思的事儿,但是对于一些注释文档都很缺失的项目来说,重构则是一件痛苦的事情,因为你需要从代码中获取逻辑,而作为动态脚本语言的JavaScript,其在大型项目中的可维护性并不是很高。 所以如果条件允许,还是建议选择TypeScript之类的工具来帮助更好的进行开发。 ...

September 28, 2018 · 3 min · jiezi

koa-send源码分析

通常我们在做静态文件服务的时候,首选CDN。当文件内容需要经常变动时,则可以采用nginx代理的方式。node本身也可以搭建静态服务,用koa static可以很容易实现这个功能。koa static是一个koa中间件,内部是对koa send的封装。koa static本身只做了一层简单的逻辑,所以这篇文章主要分析一下koa send的实现方式。如果让我们自己实现这个功能,也很简单,逻辑就是根据用户请求路径,找到文件,然后做一个文件流的响应。koa send的实现也大概是这个思路,另外多了一些基于http协议的处理,当然,阅读koa send的源码,还是有一些意外的收获。koa send源码很简洁,唯一暴露了一个工具函数send,send函数大致结构如下:async function send(ctx, path, opts = {}) { // 1、参数path校验 // 2、配置opts初始化 // 3、accept encoding处理 // 4、404、500处理 // 5、缓存头处理 // 6、流响应}第1步和第2步是koa send本身的一些配置的处理,代码比较啰嗦,我们可以忽略。第3步,主要是根据请求头Accept-Encoding进行处理,如果用户浏览器支持br或者gzip的压缩方式,koa send会判断是否存在br或者gz格式文件,如果存在会优先响应br或者gz文件。第4步,会做文件查找,如果不存在文件,或者文件查找异常,则进行404或者500的响应。具体代码如下:try { stats = await fs.stat(path) // 默认文件index if (stats.isDirectory()) { if (format && index) { path += ‘/’ + index stats = await fs.stat(path) } else { return } }} catch (err) { const notfound = [‘ENOENT’, ‘ENAMETOOLONG’, ‘ENOTDIR’] if (notfound.includes(err.code)) { throw createError(404, err) } err.status = 500 throw err}第5步,会设置协商缓存Last-Modified和强制缓存Cache-Control,代码很简单不解释,不过这里面有一个之前没遇到的知识点,koa send设置的Cache-Control会有类似max-age=10000,immutable的值,immutable表示永不改变,浏览器永不需要请求资源,这个感觉可以配合带hash或者版本号的资源使用。第6步最有意思,代码很简单,只有一行:ctx.body = fs.createReadStream(path)熟悉node文件流的同学都会知道,fs.createReadStream创建了一个自path的文件流,调用.pipe即可通过管道做文件流传输。比如使用node的http模块实现的http服务,如果要对res做文件流响应,那么只要调用strame.pipe(res)即可。但是这里有意思的是,koa send把stream赋值给ctx.body就完了,没有看到任何流处理。另外平时我们做json格式的响应,也是类似的调用ctx.body={a:1}。也就是说,ctx.body可以做不同类型的赋值操作。要解释这个操作方式,就要回到koa本身对koa.body的实现。先贴上koa body的实现代码:set body(val) { // no content if (null == val) { if (!statuses.empty[this.status]) this.status = 204; this.remove(‘Content-Type’); this.remove(‘Content-Length’); this.remove(‘Transfer-Encoding’); return; } // string if (‘string’ == typeof val) { if (setType) this.type = /^s*</.test(val) ? ‘html’ : ’text’; this.length = Buffer.byteLength(val); return; } // buffer if (Buffer.isBuffer(val)) { if (setType) this.type = ‘bin’; this.length = val.length; return; } // stream if (‘function’ == typeof val.pipe) { // 结束关闭流 stream.destroy stream.close // https://www.npmjs.com/package/on-finished // https://www.npmjs.com/package/destroy onFinish(this.res, destroy.bind(null, val)); ensureErrorHandler(val, err => this.ctx.onerror(err)); // overwriting if (null != original && original != val) this.remove(‘Content-Length’); if (setType) this.type = ‘bin’; return; } // json this.remove(‘Content-Length’); this.type = ‘json’;}从截取的源码可以看到,原来是ctx.body做了一层setter拦截,当我们赋值的时候,koa对于不同格式的body,统一在setter中做了分类处理。从源码上看,body依次支持了空值、字符串、字节、文件流和json几种响应类型。对于是否流的判断,就是通过是否对象存在pipe函数确定的。这个实现方式挺有意思,以后对于一些不同类型的复制操作,可以把类型判断和一些逻辑放到setter中来做,代码会清晰很多。 ...

September 5, 2018 · 1 min · jiezi