Koa源码学习
前言

koa是一个非常流行的Node.js http框架。本文我们来学习下它的使用和相关源码
来自官网的介绍:
Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序
为什么使用koa
使用koa而不直接使用Node.js的http模块
-
高度可定制性:koa中实现了一套中间件机制以及在koa中万物皆中间件,我们通过中间件处理请求和响应并可以按需自由添加和修改中间件,并且koa的中间件生态非常丰富。而使用http需要自己编写全部的请求处理逻辑
-
异步编程:koa基于async/await语法,可以让异步编程变得更加简单和优雅。而直接使用http模块,则需要使用回调函数或事件监听的方式进行异步编程,不够直观
-
错误处理:koa内置的错误处理机制可以很好的捕获和处理错误,让代码更加健壮和可靠。而使用http模块,则需要自己编写错误处理逻辑,容易出现漏洞
-
扩展性:koa内置的扩展机制可以让开发者在不改变核心代码的情况下,轻松地扩展和定制koa的功能。而使用http模块,则需要自己编写全部的扩展逻辑,不够便捷
使用
koa的使用非常简单,引入koa后只需要6行代码即可访问3000端口的http服务返回一个Hello koa
const Koa = require('koa');
const app = new Koa();app.use(ctx => {ctx.body = 'Hello Koa';
});app.listen(3000);
中间件
koa本身几乎没有封装任何进一步处理http请求的能力,而是实现了一套中间件的机制,所有的逻辑均由相关的中间件进行实现,中间件可以说是koa的灵魂
koa的中间件本质是一个函数,接收一个上下文对象(context)和一个next函数作为参数,然后对请求和响应进行处理,并将控制权传递给下一个中间件。中间件可以实现各种功能,例如路由、请求处理、错误处理等
const myMiddleware = async (ctx, next) => {// 处理请求// ...// 调用下一个中间件await next();// 处理响应// ...
}
例如我们实现一个错误处理中间件,在服务端发生任何错误时给客户端返回一个500的状态码,可以以下实现即可
const errorHandler = async (ctx, next) => {try {// 处理请求// ...// 调用下一个中间件await next();// 处理响应// ...} catch (err) {// 处理错误ctx.status = 500;ctx.body = err.message;}
}app.use(errorHandler)
以两个最常用的中间件为例
- koa-router
koa默认也是没有封装对于特定的请求方法进行处理的功能,像很多http中处理路由相关的逻辑则需要引入koa-router 进行使用。koa router提供了基础的路由路径处理、嵌套路由等一些基础路由能力

var Koa = require('koa');
var Router = require('koa-router');var app = new Koa();
var router = new Router();router.get('/', (ctx, next) => {// ctx.router available
});app.use(router.routes()).use(router.allowedMethods());
koa-router的源码就不展开了,原理基本上在中间件中读取req.url 、 req.method 和相关req上的一些属性进行分发到相应的路由注册的回调返回中进行处理
- koa-body
另一个常用的功能就是将请求的请求体数据解析成js对象,方便代码进行消费
对于node原生的http服务,我们需要监听请求对象的data和end事件,在data 事件中接收二进制buffer数据,在end事件中将buffer转成字符串再序列化成js对象
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');const app = new Koa();
app.use(bodyParser());app.use(async ctx => {// the parsed body will store in ctx.request.body// if nothing was parsed, body will be an empty object {}ctx.body = ctx.request.body;
});
这样对于这类请求我们通过ctx.request.body就能获取到json请求的数据,无需关心从请求流关心如何获取请求体。koa-body不止处理json类型,它还会对form、text、xml等类型做相应的处理

源码实现
koa的源码非常简洁,一共只有4个文件

application
application.js定义了Koa类,用于创建koa app对象,下面是koa类的构造函数
// ...
const Emitter = require('events')
const compose = require('koa-compose');
const http = require('http');
const context = require('./context');
const request = require('./request');
const response = require('./response');
// ...class Koa extends Emitter {constructor() {super();this.middleware = [];this.context = Object.create(context);this.request = Object.create(request);this.response = Object.create(response);// constructor中其它的逻辑忽略}// ...
}
Koa类继承了Emitter类,用于实现事件的发布和订阅。还定义了一些属性,主要包括middleware、context、request和response。其中,middleware是中间件函数数组,用于存储所有的中间件函数;context是koa的请求上下文对象、request是请求对象实例、response是响应对象实例
koa实例上也暴露了几个对外使用的方法
app.listen
上面的使用demo,可以看到调用listen后就是监听指定端口运行起我们的http服务

通过查看app.listen 的实现本质是调用了app.callback获取到回调函数处理逻辑,再传给http.createSerever。所以也等价于以下调用
const http = require('http');
const Koa = require('koa');
const app = new Koa();
http.createServer(app.callback()).listen(3000);
app.callback
返回可以直接传递给 http.createServer() 方法的回调函数来处理请求
callback () {const fn = this.compose(this.middleware)if (!this.listenerCount('error')) this.on('error', this.onerror)const handleRequest = (req, res) => {const ctx = this.createContext(req, res)return this.handleRequest(ctx, fn)}return handleRequest}handleRequest (ctx, fnMiddleware) {const res = ctx.resres.statusCode = 404const onerror = err => ctx.onerror(err)const handleResponse = () => respond(ctx)onFinished(res, onerror)return fnMiddleware(ctx).then(handleResponse).catch(onerror)}
主要有以下几个逻辑
-
判断我们是否有监听错误事件进行处理(
this.listenerCount是继承的EventEmiter上用于获取某个事件监听次数的方法),如果没有则使用koa自带的默认错误处理 -
使用回调入参的
request对象和response对象构造请求上下文对象并传递给this.handleRequest函数进行处理 -
在
handleRequest中,就是调用了被compose完成后的中间件函数,在处理完成后调用respond进行结束整个请求的流程 -
在koa中我们无需像Node.js中http需要显式调用
res.end或者res.pipe进行响应的结束发送,因为在handleResponse的respond函数中处理了。它会根据我们在业务逻辑设置的不同的body的类型进行相关调用,例如如果是一个流则调用pipe进行流式返回、特定状态码不返回body、非buffer和string的body序列化成字符串等

- 洋葱模型
koa的洋葱模型是一种中间件处理机制其核心是将请求和响应对象传递给一系列中间件函数,每个中间件函数都可以对请求和响应进行处理,并将控制权传递给下一个中间件函数,最终将响应返回给客户端。中间件函数在请求处理过程中像是一个个套在一起的“洋葱”,请求从外层中间件函数开始处理,逐层深入,直到最内层中间件函数,然后逐层返回,最终响应从最外层中间件函数返回给客户端
在洋葱模型中,每个中间件函数都是一个异步async函数。在处理请求时,每个中间件函数都接收一个context对象和一个next函数作为参数,context对象包含了请求和响应的信息,next函数可以调用下一个中间件函数
处理顺序如下
-
请求从外层中间件函数开始处理,先经过第一个中间件函数
-
第一个中间件函数处理请求,然后调用
next函数,将控制权传递给下一个中间件函数 -
下一个中间件函数也处理请求,然后调用
next函数,将控制权传递给下一个中间件函数,直到最内层中间件函数 -
最内层中间件函数处理请求完成后逐层返回每个中间件函数在返回时可以对响应进行处理
-
最后,响应从最外层中间件函数返回给客户端
洋葱模型的优点是可以将请求和响应的处理逻辑分解成多个模块,每个模块只需关注自己的逻辑,提高了代码的可维护性。由于每个中间件函数都可以对请求和响应进行处理,因此可以实现一些复杂的功能例如身份验证、日志记录、错误处理等
主要是koa-compose包的实现将中间件函数组合在一起,compoose实现代码如下
function compose (middleware) {return function (context, next) {// last called middleware #let index = -1return dispatch(0)function dispatch (i) {if (i <= index) return Promise.reject(new Error('next() called multiple times'))index = ilet fn = middleware[i]if (i === middleware.length) fn = nextif (!fn) return Promise.resolve()try {return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))} catch (err) {return Promise.reject(err)}}}
}// 使用const fn = this.compose(this.middleware)
compose函数接收一个中间件函数数组作为参数,返回一个新的中间件。新的中间件函数接收context和next对应于常规中间件的入参
函数内部实现了dispatch,用于递归调用中间件数组中的每个函数。
dispatch函数接收一个参数i,表示当前调用的中间件函数在数组中的索引。如果i小于等于上一次调用的索引index,则表示next函数被多次调用,koa中间件中next只能被调用一次,调用多次会抛出一个错误。然后,dispatch将index赋值为i,表示当前调用的中间件函数已经被执行。然后dispatch函数会从中间件数组中取出当前索引对应的函数fn,如果当前索引i等于数组长度则说明已经到达中间件函数数组的末尾然后将fn设置为next函数。如果fn不存在则直接返回一个已经resolve的Promise。最后dispatch函数通过Promise.resolve调用当前中间件函数,并将dispatch.bind(null, i + 1)作为下一个中间件函数的next参数传入,以便递归调用下一个中间件函数。如果当前中间件函数抛出了一个错误则通过Promise.reject将错误传递给下一个中间件函数
总结原理是通过递归调用中间件函数数组中的每个函数,并将next函数作为参数传入,实现洋葱模型中间件的处理顺序。在递归调用的过程中,如果某个中间件函数抛出了错误则通过Promise.reject将错误逐层传递给下一个中间件函数,直到最终返回错误响应或者成功响应
context
请求上下文对象,对应中间件的ctx入参
context.js文件主要是对外导出了一个对象,以及执行了一系列delegate操作


-
导出的对象主要是封装了cookie的读取逻辑
-
delegate方法是从delegatesnpm包进行导入(这个包的解读见# 每天阅读一个 npm 模块(7)- delegates - 掘金)
简单来说就是将对context对象上的操作代理到koa封装的request和response对象中去
// proto这里是context
delegate(proto, 'response').method('append').access('body')
这个执行后的结果就是
-
context.append方法调用实际调用的是context.response.append -
context.body的读写实际调的是context.response.body的读写
而context.response则在下面的createContext时将koa的response对象设置在context对象中去
在application中通过createContext方法构造后传入请求处理回调函数
class Koa extends Emitter {constructor() {//....this.context = Object.create(context);//....}// ...callback () {const fn = this.compose(this.middleware)if (!this.listenerCount('error')) this.on('error', this.onerror)const handleRequest = (req, res) => {const ctx = this.createContext(req, res)return this.handleRequest(ctx, fn)}return handleRequest}createContext (req, res) {/** @type {Context} */const context = Object.create(this.context)/** @type {KoaRequest} */const request = context.request = Object.create(this.request)/** @type {KoaResponse} */const response = context.response = Object.create(this.response)// 挂载context.app = request.app = response.app = thiscontext.req = request.req = response.req = reqcontext.res = request.res = response.res = resrequest.ctx = response.ctx = contextrequest.response = responseresponse.request = requestcontext.originalUrl = request.originalUrl = req.urlcontext.state = {}return context}
}
主要是将我们koa中常用的几个对象挂载到相应的地方,经过createContext的操作,我们可以得到可以通过以下方式获取相关对象
-
koa
app实例app === context.app === context.request.app === context.response.app
-
koa 请求
context对象context === context.request.ctx === context.response.ctx
-
koa
request对象ctx.request === ctx.response.request
-
koa
response对象ctx.response === ctx.request.response
-
原生
req对象context.req === context.request.req === context.response.req
-
原生
res对象context.res === context.response.res === context.request.res
request
koa中的请求对象封装。基本上都是基于Node.js的http请求的request做一些便捷使用的二次封装的属性和方法,并挂载在ctx.request中
一个例子就是Node.js 的http server回调函数入参的req对象http.ImcomingMessage 是没有提供便捷的获取query参数信息,它只有一个url属性
而koa的request对象则实现了query的解析、获取、设置等
// request.js
get query () {const str = this.querystringconst c = this._querycache = this._querycache || {}return c[str] || (c[str] = qs.parse(str))}get querystring () {if (!this.req) return ''return parse(this.req).query || ''}
response
koa中的响应对象封装,基于Node.js的http请求的response做一些封装的属性和方法,挂载在ctx.response中
一个比较常用到的就是会有根据我们的ctx.body设置的值(会delegate到ctx.response.body中)帮我们去设置response的Content-Type的值,例如给ctx.body设置一个普通js对象的话,会将Content-Type设置为json类型并将js对象json序列化(序列化逻辑在上面提到的respond函数中)


最近更新
作为一个代码实现非常精简且已经非常稳定的广泛使用的框架,一般来说不会有什么更新了,2.x也已经稳定了很久。但是在1/2却更新了3.0.0-alpha.0版本,翻看更新记录这个大版本目前只更新了一个功能

可以直接使用app.currentContext来获取当前的请求上下文对象,这个功能可以方便不少我们的代码开发
通过上面我们知道,koa的contxt对象是每次请求维度的一个新对象,如果我们想在一些封装的方法中获拿到当前请求的context对象,必须层层传递context对象会比较麻烦
// fn.js
const fn = (ctx) => {console.log(ctx.url)
}
exports.fn = fn// app.js
const app = new Koa();
app.use(ctx => {ctx.body = 'Hello Koa';fn(ctx)
}).listen(3000);
而支持了app.currentContext后,我们在任意地方想获取当前的请求上下文对象直接app.currentContext即可,无需再多层透传context对象
// fn.js
const fn = () => {console.log(app.currentContext.url)
}
这个功能的实现利用了Node.js的async_hooks模块提供的AsyncLocalStorage。AsyncLocalStorage 是 Node.js 在v14.8.0 版本中引入的一个模块,是官方推荐的在异步代码中管理数据的方式之一,会将我们保存的数据与异步操作所在的上下文关联起来,确保在异步操作中访问到相应正确的数据
AsyncLocalStorage 有两个主要的方法
-
run():用于在异步操作中保存数据。接收一个回调函数作为参数,该回调函数会在异步操作执行期间被调用,并且在该回调函数中保存的数据会与异步操作所在的上下文关联起来 -
getStore():用于在异步操作中获取数据。它会返回与异步操作所在的上下文关联的数据
所以在koa中实现app.currentContext功能主要就是以下代码
// application.js
class Application extends Emitter {constructor (options) {//....if (options.asyncLocalStorage) {const { AsyncLocalStorage } = require('async_hooks')this.ctxStorage = new AsyncLocalStorage()this.use(this.createAsyncCtxStorageMiddleware())}}// ...createAsyncCtxStorageMiddleware () {const app = thisreturn async function asyncCtxStorage (ctx, next) {await app.ctxStorage.run(ctx, async () => {return await next()})}}// ....get currentContext () {if (this.ctxStorage) return this.ctxStorage.getStore()}
}
-
如果初始化时配置了
option.asyncLocalStorage,就注册一个放在第一位的koa中间件 -
在请求进入中间件时会执行
ctxStorage.run存入当前的context对象并马上在回调函数中执行next(即请求后续所有的操作) -
在后续获取即可通过
getStore()获取到当前请求的context对象
总结
通过本文的学习我们了解到了koa的一些使用和实现,koa的源码是非常精简的没有太多耦合功能,但是设计了巧妙的中间件机制设计来方便让我们开发各种功能
相关文章:
Koa源码学习
前言 koa是一个非常流行的Node.js http框架。本文我们来学习下它的使用和相关源码 来自官网的介绍: Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。…...
一种延迟加载自定义元素的方法
您可能实际上并不需要所有这些;通常有一个更简单的方法。如果有意使用,此处显示的技术可能仍然对您的工具集有用。 为了保持一致性,我们希望我们的自动加载器也成为一个自定义元素——这也意味着我们可以通过 HTML 轻松配置它。但首先&#…...
Pytho经典面试题荟萃:第一期
目录 一、面试题 二、参考答案 解释器和编译器的区别 解释器 编译器 Python 的解释过程 Python 内存管理 Python 内存分配 引用计数 垃圾回收 其他内存管理技术 多重继承 多重继承带来的问题 命名冲突 菱形继承问题 解决多重继承带来的问题 方法重写 调用 su…...
01背包问题(大彻大悟版)
背包问题身为一个非常经典的动态规划问题,理清思路很重要,在经过多次观看y总视频和b站解析,加上CSDN的文章辅助,我终于从很多不理解到大彻大悟,下面是我对于背包问题思路的总结,有问题的话欢迎指出。谈到背…...
【麒麟服务器操作系统忘记开机密码怎么办?---银河麒麟服务器操作系统更改用户密码】
银河麒麟服务器操作系统更改用户密码 1.启动主机进入 grub 菜单,如图 1.1 以最新版本 Kylin-Server-10-SP2-x86-Release-Build09-20210524 为例。 图 1.1 grub 菜单 2 编辑 kernel 2.1按下”e”输入,输入用户名和密码(root/Kylin123123&…...
华为OD机试(20222023)考点分类
字符串,数组,集合操作 题库分值序号题目考点 or 实现Old1001敏感字段加密字符串,数组,集合操作Old1002IPv4地址转换成整数字符串,数组,集合操作Old1006字符串分割字符串,数组,集合操作Old1007...
初级篇 3 - HTML 或 CSS 文件中不懂的标签属性详解
目录一、遇到的不懂的标签属性详解1、meta 标签的 http-equiv 属性(元标签)二、遇到的 CSS 不懂的属性详解vertical-align三、如何规避 HTML 自动换行 - 脱离文档流配置属性 display: inline-block理解 inline、inline-block、blockinline总结:四、导航栏自动弹出子…...
【C语言】每日刷题 —— 牛客语法篇(4)
🚀🚀前言 大家好,继续更新专栏 c_牛客,不出意外的话每天更新十道题,难度也是从易到难,自己复习的同时也希望能帮助到大家,题目答案会根据我所学到的知识提供最优解。 🏡个人主页&am…...
HashMap ConcurrentHashMap介绍
目录 HashMap 数据结构 重要成员变量 Jdk7-扩容死锁分析 单线程扩容 多线程扩容 Jdk8-扩容 ConcurrentHashMap 数据结构 并发安全控制 源码原理分析 重要成员变量 协助扩容helpTransfer 扩容transfer 总结 CopyOnWrite机制 源码原理 HashMap 数据结构 数组…...
C++语法规则3(C++面向对象)
多态 C多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数; 形成多态必须具备三个条件: 必须存在继承关系;继承关系必须有同名虚函数(其中虚函数是在基类中使用关键字 virtual 声明的函数&#…...
Python tkinter 如何实现网站下载工具?将所有数据一键获取
前言 铁汁们有没有想过,如何把几个代码的功能结合到一起呢? 有想过的话,有没有实现过呢? 其实很简单的啊,咱就写一个界面就好了,想要哪个代码运行,鼠标轻轻一点就行 开发环境 python 3.8: 解…...
第六章:C语言数据结构与算法初阶之栈
系列文章目录 文章目录系列文章目录前言一、栈二、栈的实现三、接口函数的实现1、初始化2、销毁栈3、压栈与出栈4、判空5、元素个数6、返回栈顶元素四、栈中元素的访问总结前言 栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。 一、…...
Android学习之WebView
什么是WebView WebView是Android中UI组件的一种,WebView基于webkit内核,不过由于兼容性的原因在Android5.0后改为了Chromium内核。 WebView可以用来展示网页,常用于我们不想打开浏览器但又想浏览网页的情况。 WebView的使用 WebVeiw的常用…...
3/11 考试总结
时间安排 7:30–7:50 读题,T1 是个利用随机性的题目,T2 dp,T3 不知道是啥。 7:50–8:30 T1,对于随机有个结论时最值突变不超过 log ,于是可以处理出所有 log 个区间然后统计答案,但这暴力做是个 3log 铁定过不去。 8:30–8:50 T2…...
Leetcode 141.环形链表 142环形链表II
141环形链表 文章目录快慢指针快慢指针 代码思路: slow 和fast 指向 head slow走一步,fast走两步 没有环: fast每次走2步 ,如果 fast 最终遇到NULL(链表中的元素是 偶数)或者fast->next(链表中的元素是 奇数)遇到NULL…...
hibernate学习(五)
hibernate学习(五) hibernate的一对多关联映射: 一、数据库表与表之间关系 一对多建表原则: 多对多的建表原则: 一对一建表原则: (1)唯一外键对应: (…...
STM32CubeIDE 快速开发入门指南
描述 STM32CubeIDE是一体式多操作系统开发工具,是STM32Cube软件生态系统的一部分。 STM32CubeIDE是一种高级C/C开发平台,具有STM32微控制器和微处理器的外设配置、代码生成、代码编译和调试功能。它基于Eclipse/CDT™框架和用于开发的GCC工具链…...
华为OD机试 - 火星文计算(C 语言解题)【独家】
最近更新的博客 华为od 2023 | 什么是华为od,od 薪资待遇,od机试题清单华为OD机试真题大全,用 Python 解华为机试题 | 机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南)华为od机试,独家整理 已参加机试人员的实战技巧文章目录 使用说明本期题目:火星文计…...
超超超超保姆式详解——字符函数和字符串函数(学不会打我)上
目录 长度不受限制的字符串函数 strlen部分 strlen函数的易错小知识 strlen函数的实现 strcpy部分 strcat部分 自己实现strcat strstr函数部分 简单例子: 分析 strcmp部分 长度受限制的字符串函数 strncpy 简单例子 strncat strncmp 简单例子 &…...
Data mesh 笔记
有用的网站 https://www.datamesh-architecture.com/ https://www.agilelab.it/data-mesh-in-action https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/scenarios/cloud-scale-analytics/well-architected-framework https://www.datamesh-architecture.com…...
shell脚本--常见案例
1、自动备份文件或目录 2、批量重命名文件 3、查找并删除指定名称的文件: 4、批量删除文件 5、查找并替换文件内容 6、批量创建文件 7、创建文件夹并移动文件 8、在文件夹中查找文件...
VTK如何让部分单位不可见
最近遇到一个需求,需要让一个vtkDataSet中的部分单元不可见,查阅了一些资料大概有以下几种方式 1.通过颜色映射表来进行,是最正规的做法 vtkNew<vtkLookupTable> lut; //值为0不显示,主要是最后一个参数,透明度…...
大学生职业发展与就业创业指导教学评价
这里是引用 作为软工2203/2204班的学生,我们非常感谢您在《大学生职业发展与就业创业指导》课程中的悉心教导。这门课程对我们即将面临实习和就业的工科学生来说至关重要,而您认真负责的教学态度,让课程的每一部分都充满了实用价值。 尤其让我…...
优选算法第十二讲:队列 + 宽搜 优先级队列
优选算法第十二讲:队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...
USB Over IP专用硬件的5个特点
USB over IP技术通过将USB协议数据封装在标准TCP/IP网络数据包中,从根本上改变了USB连接。这允许客户端通过局域网或广域网远程访问和控制物理连接到服务器的USB设备(如专用硬件设备),从而消除了直接物理连接的需要。USB over IP的…...
基于 TAPD 进行项目管理
起因 自己写了个小工具,仓库用的Github。之前在用markdown进行需求管理,现在随着功能的增加,感觉有点难以管理了,所以用TAPD这个工具进行需求、Bug管理。 操作流程 注册 TAPD,需要提供一个企业名新建一个项目&#…...
MFC 抛体运动模拟:常见问题解决与界面美化
在 MFC 中开发抛体运动模拟程序时,我们常遇到 轨迹残留、无效刷新、视觉单调、物理逻辑瑕疵 等问题。本文将针对这些痛点,详细解析原因并提供解决方案,同时兼顾界面美化,让模拟效果更专业、更高效。 问题一:历史轨迹与小球残影残留 现象 小球运动后,历史位置的 “残影”…...
[免费]微信小程序问卷调查系统(SpringBoot后端+Vue管理端)【论文+源码+SQL脚本】
大家好,我是java1234_小锋老师,看到一个不错的微信小程序问卷调查系统(SpringBoot后端Vue管理端)【论文源码SQL脚本】,分享下哈。 项目视频演示 【免费】微信小程序问卷调查系统(SpringBoot后端Vue管理端) Java毕业设计_哔哩哔哩_bilibili 项…...
STM32HAL库USART源代码解析及应用
STM32HAL库USART源代码解析 前言STM32CubeIDE配置串口USART和UART的选择使用模式参数设置GPIO配置DMA配置中断配置硬件流控制使能生成代码解析和使用方法串口初始化__UART_HandleTypeDef结构体浅析HAL库代码实际使用方法使用轮询方式发送使用轮询方式接收使用中断方式发送使用中…...
tomcat指定使用的jdk版本
说明 有时候需要对tomcat配置指定的jdk版本号,此时,我们可以通过以下方式进行配置 设置方式 找到tomcat的bin目录中的setclasspath.bat。如果是linux系统则是setclasspath.sh set JAVA_HOMEC:\Program Files\Java\jdk8 set JRE_HOMEC:\Program Files…...
