手摸手学会node框架之一——koa 傻瓜式小白教程

news2024/11/29 6:26:10

一、Koa简介

基于 Node.js 平台的下一代 web 开发框架。 由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。

详细请参考Koa官网进行学习。

二、Koa基础入门

1.项目初始化

执行 npm init -y, 生成package.json

npm init -y

2.安装Koa

执行命令

npm i koa

ps:项目名称不能为koa,不然就噶了

image-20220725101707666

3.入门体验

1)回顾Express如何创建服务程序
//导入express
const express = require('express')
//创建web服务器
const app = express()
//编写中间件
app.use(function(req, res, next) {
  console.log('hello express');
  next()
})
//启动服务器并监听端口
app.listen(8080, () => [
  console.log('express server running at http://localhost:8080')
])
2)使用koa编写服务程序
// 一. 导入koa
const Koa = require('koa')
// 二. 实例化对象 (Koa首字母需要大写,此处实际上是类)
const app = new Koa()
// 三. 编写中间件
app.use((ctx) => {
    //ctx:content http 请求上下文
  ctx.body = 'hello Koa'
})
// 四. 启动服务
app.listen(3000, () => {
  console.log('Koa server is running on http://localhost:3000')
})

通过node +文件路径执行,可以看到终端输出了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G08JPgvm-1673836833805)(C:\Users\Leyuan\AppData\Roaming\Typora\typora-user-images\image-20220725103442187.png)]

同时,我们使用postman对http://localhost:3000网址发出请求,可以看到服务端发出的响应“hello koa”

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H8tZcAPB-1673836833805)(C:\Users\Leyuan\AppData\Roaming\Typora\typora-user-images\image-20220725103721709.png)]

nodemon插件的安装

安装插件,便于实时监听后缀为js、mis、json文件的修改保存,避免多次重启服务(使用nodemon +文件路径启动)

npm i nodemon -D //此为开发环境安装,全局安装不需要-D

ps:此处安装成功后,可能会出现命令执行失败的情况,可以使用如下命令安装。

npm i nodemon -g --verbose 

执行 nodemon src/test命令后,修改并保存文件内容,可以看到nodemon监听到文件被修改自行重启。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k7LsAOzS-1673836833806)(C:\Users\Leyuan\AppData\Roaming\Typora\typora-user-images\image-20220725105646578.png)]

三、走进中间件

1.基本概念

有时候从请求到响应的业务比较复杂, 将这些复杂的业务拆开成一个个功能独立的函数, 就是中间件每一个中间件就是一个函数,互不影响,但又彼此协作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QguD3XmL-1673836833806)(C:\Users\Leyuan\AppData\Roaming\Typora\typora-user-images\image-20220725135217537.png)]

2.基本使用

// 一. 导入koa
const Koa = require('koa')
// 二. 实例化对象
const app = new Koa()
// 三. 编写中间件
app.use((ctx, next) => {
  console.log('我来组成身体')
    //next() 可以将当前函数暂停并将控制传递给定义的下一个中间件。
  next()
})
app.use((ctx, next) => {
  console.log('我来组成头部')
  next()
})
app.use((ctx) => {
  console.log('---------')
  //如果此处不使用ctx.body会报错“not found”
  ctx.body = '组装完成'
})
// 四. 启动服务
app.listen(3000, () => {
  console.log('server is running on http://localhost:3000')
})


app.use 可以将给定的中间件方法添加到此应用程序需要注意的是,其一次只能接受一个函数做为参数。其返回 this, 因此可以链式表达,以上代码可以简写为

// 一. 导入koa
const Koa = require('koa')
// 二. 实例化对象
const app = new Koa()
// 三. 编写中间件
app
.use((ctx, next) => {
  console.log('我来组成身体')
  next()
})
.use((ctx, next) => {
  console.log('我来组成头部')
  next()
})
.use((ctx) => {
  console.log('---------')
  //如果此处不使用ctx.body会报错“not found”
  ctx.body = '组装完成'
})
// 四. 启动服务
.listen(3000, () => {
  console.log('server is running on http://localhost:3000')
})

思考题 下面的输出顺序是?

// 1. 导入koa包
const Koa = require('koa')
// 2. 实例化对象
const app = new Koa()
// 3. 编写中间件
app.use((ctx, next) => {
  console.log(1)
  next()
  console.log(2)
  console.log('---------------')
  ctx.body = 'hello world'
})

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

app.use((ctx)=>{
  console.log(5)
})
// 4. 监听端口, 启动服务
app.listen(3000)
console.log('server is running on http://localhost:3000')

洋葱圈模型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8cClobXS-1673836833806)(C:\Users\Leyuan\AppData\Roaming\Typora\typora-user-images\image-20220725143506178.png)]

  1. 中间件函数队列,会在最后一个中间件或一个没有调用next的中间件那里停止。
  2. koa官方文档上把外层的中间件称为"上游",内层的中间件为"下游"。
  3. 一般的中间件都会执行两次,调用next之前为第一次,调用next时把控制传递给下游的下一个中间件。当下游不再有中间件或者没有执行next函数时,就将依次恢复上游中间件的行为,让上游中间件执行next之后的代码
从源代码看use
  use(fn) {
    // 判断是否为函数
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    // 判断是否为generator函数,并转化为generator函数
    if (isGeneratorFunction(fn)) {
      deprecate('Support for generators will be removed in v3. ' +
                'See the documentation for examples of how to convert old middleware ' +
                'https://github.com/koajs/koa/blob/master/docs/migration.md');
      fn = convert(fn);
    }
// 调试  DEBUG=koa* node app.js
    debug('use %s', fn._name || fn.name || '-');
    // 把中间件push进middleware
    this.middleware.push(fn);
    return this;
  }
Genertor 函数

Genertor 函数是es6 新增的一种异步编程的解决方案,语法和传统的函数完全不同;Genertor 函数的最大的特点就是可以交出函数的执行权(即暂停执行)。

1)形式上: Generator函数是一个普通的函数,不过相对于普通函数多出了两个特征。一是在function关键字和函数明之间多了’*'号;二是函数内部使用了yield表达式,用于定义Generator函数中的每个状态。
2)语法上: Generator函数封装了多个内部状态(通过yield表达式定义内部状态)。执行Generator函数时会返回一个遍历器对象(Iterator(迭代器)对象)。也就是说,Generator是遍历器对象生成函数,函数内部封装了多个状态。通过返回的3)Iterator对象,可以依次遍历(调用next方法)Generator函数的每个内部状态。
3)调用上: 普通函数在调用之后会立即执行,而Generator函数调用之后不会立即执行,而是会返回遍历器对象(Iterator对象)。通过Iterator对象的next方法来遍历内部yield表达式定义的每一个状态。

function *myGenerator() {
	yield 'Hello'
	yield 'world'
	return 'ending'
}

let MG = myGenerator()

MG.next() // {value:'Hello',done:false}
MG.next() // {value:'world',done:false}
MG.next() // {value:'ending',done:true}
MG.next() // {value:'undefined',done:false}

上面代码一共调用了四次next方法。

第一次调用,Generator 函数开始执行,直到遇到第一个yield表达式为止。next方法返回一个对象,它的value属性就是当前yield表达式的值hello,done属性的值false,表示遍历还没有结束。

第二次调用,Generator 函数从上次yield表达式停下的地方,一直执行到下一个yield表达式。next方法返回的对象的value属性就是当前yield表达式的值world,done属性的值false,表示遍历还没有结束。

第三次调用,Generator 函数从上次yield表达式停下的地方,一直执行到return语句(如果没有return语句,就执行到函数结束)。next方法返回的对象的value属性,就是紧跟在return语句后面的表达式的值(如果没有return语句,则value属性的值为undefined),done属性的值true,表示遍历已经结束。

第四次调用,此时 Generator 函数已经运行完毕,next方法返回对象的value属性为undefined,done属性为true。以后再调用next方法,返回的都是这个值。

调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。
koa-compose源代码

从源代码观察洋葱模型的原理

'use strict'

/**
 * Expose compositor.
 */

module.exports = compose

/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */

function compose (middleware) {
  // 判断接收的中间件是否为数组
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    // 判断是否为函数
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */
// 返回匿名函数,该函数接收两个参数
  return function (context, next) {
    // last called middleware #
    // 初始下标为-1,记录执行的中间件的索引
    let index = -1
    // 从第一个中间件并开始递归执行
    return dispatch(0) 
    function dispatch (i) {
      // 这里是保证同个中间件中一个next()不被调用多次调用 
      // 当i<index,next()函数被调用至少两次的时候,抛出错误
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
       // 如果i>index,则该中间件并未执行,记录索引
      index = i
      // 根据下标取出中间件
      let fn = middleware[i]
      
      // 当i已经是数组的length了,说明中间件函数都执行结束,即已经到了洋葱最中心
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
         // 若数组下标并未到达最后一位,且存在当前中间件函数则执行当前函数并传入 dispatch(i + 1),就可看出是数组中的下一个中间件了,此时作为 next 传入了中间件函数中;
      // 也就是说我们写中间件时,已经默认注入了 ctx 与 下次执行的封装函数 next,也是因为如此我们在 koa 的中间件中才可以非常方便的判断什么时候进入下一个中间件去执行的洋葱结构,并且一定要执行 next() 否则数组将在此中断,因为这里是 Function.prototype.bind(),bind()方法会创建一个新函数,称为绑定函数.当调用这个绑定函数时,绑定函数会以创建它时传入 bind()方法的第一个参数作为 this,传入 bind()方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数.
      // 需注意的是 bind 时指向 null 也是为了以防在执行过程中你有什么骚操作改变了指向,那就不好了
      // 在不断的 Promise.resolve 中去实现递归 dispatch 函数,最终实现顺序控制执行所有中间件函数
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

四、路由

1、什么是路由

  • 建立URL和处理函数之间的对应关系
  • 主要作用: 根据不同的Method和URL返回不同的内容
app.use((ctx) => {
  if (ctx.url == '/') {
    ctx.body = '这是主页'
  } else if (ctx.url == '/users') {
    if (ctx.method == 'GET') {
      ctx.body = '这是用户列表页'
    } else if (ctx.method == 'POST') {
      ctx.body = '创建用户'
    } else {
      ctx.status = 405 // 不支持的请求方法
    }
  } else {
    ctx.status = 404
  }
})

2、使用koa-router

1)安装
npm i koa-router
或
npm i @koa/router
2)使用
// 一. 导入koa
const Koa = require('koa')
// 二. 实例化对象
const app = new Koa()
// 三. 导入koa-router, 实例化路由对象
const Router = require('koa-router')
const router = new Router()
router.get('/', (ctx) => {
  ctx.body = '这是主页'
})
router.get('/users', (ctx) => {
  ctx.body = '这是用户页'
})
router.post('/users', (ctx) => {
  ctx.body = '创建用户页'
})
// 四. 注册路由中间件
// userRouter.routes() 加载路由规则
// userRouter.allowedMethods() 对于没有实现和没有使用的请求方式做出正确的响应
app.use(router.routes())
app.use(router.allowedMethods())
// 五. 启动服务
app.listen(3000, () => {
  console.log('server is running on http://localhost:3000')
})


3) 优化

我们可以将一个模块放到一个单独的文件中. 分离出一个router路由层

创建src/router/user.route.js

// 导入koa-router, 实例化路由对象
const Router = require('koa-router')
const router = new Router()

router.get('/users', (ctx) => {
  ctx.body = '这是用户页'
})
router.post('/users', (ctx) => {
  ctx.body = '创建用户页'
})

module.exports = router

再导入

// 一. 导入koa
const Koa = require('koa')
// 二. 实例化对象
const app = new Koa()

const userRouter = require('./router/user.route')

// 四. 注册路由中间件
app.use(userRouter.routes()).use(userRouter.allowedMethods())

// 五. 启动服务
app.listen(3000, () => {
  console.log('server is running on http://localhost:3000')
})

可以给路由设置一个统一的前缀, 使代码更加简洁

// 导入koa-router, 实例化路由对象
const Router = require('koa-router')
const router = new Router({ prefix: '/users' })

router.get('/', (ctx) => {
  ctx.body = '这是用户页'
})
router.post('/', (ctx) => {
  ctx.body = '创建用户页'
})

module.exports = router

五、请求参数

在很多场景中, 后端都需要解析请求的参数, 做为数据库操作的条件

场景一

前端希望通过请求, 获取id=1的用户信息

image-20211009154415445

接口设计

GET /users/:id

场景二

前端希望查询年龄在18到20的用户信息

image-20211009161051380

场景三

前端注册, 填写了用户名, 年龄, 传递给后端, 后端需要解析这些数据, 保存到数据库

image-20211009161448091

对于不同的Http请求, 需要使用不同的方式携带参数

  • GET请求: 在URL中以键值对传递
  • POST/PUT/PATCH/DELET请求: 在请求体中传递
// 一. 导入koa
const Koa = require('koa')
// 二. 实例化对象
const app = new Koa()
// 三. 导入koa-router, 实例化路由对象
const Router = require('koa-router')
const router = new Router({ prefix: '/users' })
const db=[{id:1,name:'小明',age:21},
          {id:2,name:'小红',age:18},
          {id:3,name:'小兰',age:19},]

//GET /users 获取所有用户信息,返回数组
router.get('/', (ctx) => {
  ctx.body = db
})

//GET /users/:id  场景一:根据id,获取该用户信息,返回对象(通过路由传参, 可以通过params得到)
router.get('/:id', (ctx) => {
  const id = ctx.params.id
  const res = db.filter((item)=>item.id==id)
  if(!res[0]) ctx.throw(404)
  ctx.body=res[0]
})

// GET /users?start=18&end=20 ---- 获取所有的用户信息, 返回一个数组(以键值对的形式传参, 可以通过query得到)
router.get('/', (ctx) => {
  // 通过 ctx.query 是ctx.request.query的代理 解析键值对参数
  const { start = 0, end = 0 } = ctx.query
  const res = db.filter((item) => item.age >= start && item.age <= end)
  // 解析键值对
  res.length == 0 ? ctx.throw(404) : (ctx.body = res)
})
// 四. 注册路由中间件
app.use(router.routes())
app.use(router.allowedMethods())
// 五. 启动服务
app.listen(3000, () => {
  console.log('server is running on http://localhost:3000')
})

场景三、处理body参数

Koa原生支持body参数解析, 通常借助社区的中间件实现. 官方推荐的有

  • koa-bodyparser
  • koa-body (支持的方法更多,推荐使用)
1)安装
npm install koa-body
2)注册
// 注册KoaBody中间件, 解析请求体中的参数, 挂载到ctx.request.body
const KoaBody = require('koa-body')
app.use(KoaBody())
3)使用

通过ctx.request.body获取请求体中的数据

app.use(ctx=>{
ctx.body=`Request Body:${JSON.stringify(ctx.request.body)}`
})

需要注意的是,在使用koa-body获取post请求body参数时,一定要在注册路由前使用koa-body,否则ctx.request.body获取为空

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r2ag5ahP-1673836833807)(C:\Users\Leyuan\AppData\Roaming\Typora\typora-user-images\image-20220801203314620.png)]

六、错误处理

1、为什么使用错误处理

  • 防止程序 down 掉
  • 告诉用户错误信息
  • 便于开发者调试
  • 一般Koa中的错误分为三类
    • 404: 当请求的资源找不到, 或者没有通过ctx.body返回时, 由koa自动返回
    • 手动抛出: 通过ctx.throw手动抛出
    • 500: 运行时错误

2、错误处理

1)在 async 函数中错误捕获

async 是“异步”的简写,而 await 可以认为是 async wait 的简写。async
用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。async 会将其后的函数的返回值封装成一个 Promise 对象,而await 会等待这个 Promise 完成,并将其 resolve 的结果返回出来。

我们通常处理 Promise 异步操作中的错误可以使用 .catch(err=>{ … }) 来处理,如:

getAsyncData().then(() => {
  console.log("成功啦")
}).catch(() => {
  console.log("出错啦")
})

同理,如果我们在koa中也这么处理会怎么样呢?

router.get('/getDatas', (ctx, next)=>{
  getAsyncData().then((data) => {
    ctx.body = {
      ok: ture,
      data: data,
      msg: ""
    }
  }).catch((err) => {
    ctx.body = {
      ok: false,
      data: "",
      msg: err.message
    }
  })
})   

很显然,使用这种方式去处理响应数据是无效的。在异步函数中又创建了一个新的异步函数,新的异步函数的回调执行顺序肯定在当前异步函数的回调执行完毕之后。假如我们以这样的方式处理错误的话,当我们执行 ctx.body 赋值数据时,当前的请求已经发送完毕了,所以 ctx.body 是不能在内层的异步函数中调用的。如果我们需要通过异步获取数据,我们应该在当前的异步函数中使用 await 来阻塞数据获取的异步函数。

app.use(async (ctx,next) => {
  try {
    await next();
  } catch (err) {
    ctx.status = err.status || err.statusCode;
    ctx.body = {
      message: err.message
    }
  }
})

koa继承了emitter类,可以通过error监听错误,使用emit提交错误

app.use(async (ctx,next) => {
    try{
        await next()
    }catch(err){
        ctx.response.status = err.statusCode || err.status || 500;
        ctx.response.body = {
            message: err.message
        };
        // 如果在try...catch中已经捕捉到错误,error事件就不会发出,通过emit手动释放error事件
        ctx.app.emit('error', err, ctx);
    }
});
// 继续触发error事件
app.on('error',() => {
    console.error('server error', err.message);
    console.error(err);
});

3、插件的使用

#1) 安装

npm i koa-json-error

#2) 使用

基本使用

const error = require('koa-json-error')
app.use(error())

高级使用

const error = require('koa-json-error')
app.use(
  error({
    format: (err) => {
      return { code: err.status, message: err.message, result: err.stack }
    },
    postFormat: (err, obj) => {
      const { result, ...rest } = obj
      return process.env.NODE_ENV == 'production' ? rest : obj
    },
  })
)

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

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

相关文章

博客之星规则能否参照“金球奖”

文章目录课前小差粉丝对我的价值粉丝数量的提升KOL与粉丝链接粉丝影响收入博客之星规则设想博客之星新玩法&#xff1f;内部评审展望2023写在最后课前小差 哈喽&#xff0c;大家好&#xff0c;我是几何心凉&#xff0c;这是一份全新的专栏&#xff0c;唯一得倒CSDN王总的授权&…

小侃设计模式(廿一)-状态模式

1.概述 状态模式&#xff08;State Pattern&#xff09;是行为型设计模式的一种&#xff0c;它主要用来解决对象存在多种状态转换时&#xff0c;需要对外输出不同的行为。状态模式与策略模式有一定相似&#xff0c;区别在于策略模式行为彼此独立&#xff0c;可以进行相互替换&…

VueJs中的toRef与toRefs函数的一个比较

前言ref是处理基本数据类型响应式API函数,在setup中声明定义的变量,可以直接在模板中使用没有被响应式API包裹处理的变量数据,是不具备响应式能力的也就是往往在逻辑中修改了数据,但是页面不会更新,那怎么样将一个非响应式数据变成响应式数据就需要用到toRef()与toRefs()这两个…

【计算机网络】网络编程套接字

文章目录理解源IP地址和目的IP地址理解端口号和进程ID理解源端口号和目的端口号认识TCP协议认识UDP协议网络字节序socket编程接口socket网址查看socket常见APIUDP协议实现网络通信UDP创建socket文件描述符sockaddr结构UDP绑定端口号UDP接收发送网络数据简单的UDP网络程序TCP协议…

拉伯证券|大股东或易主,阿里巴巴换股入局

三大股指齐上扬&#xff0c;早盘主力埋伏这些股。 到午间收盘&#xff0c;“家居零售榜首股”之称的美凯龙一字涨停&#xff0c;港股红星美凯龙涨24%&#xff0c;此前一度涨超30%。 消息面上&#xff0c;1月13日晚间&#xff0c;美凯龙发布公告称&#xff0c;公司控股股东红星…

transformer算法解析

本文参考&#xff1a; 详解Transformer &#xff08;Attention Is All You Need&#xff09; - 知乎 Transformer 代码完全解读&#xff01;_AI科技大本营的博客-CSDN博客 Transformer学习笔记一&#xff1a;Positional Encoding&#xff08;位置编码&#xff09; - 知乎 1、…

【C语言】自定义类型

前言男孩子在外面要保护好自己~一、结构体为什么会有结构体呢&#xff1f;但要描述一个复杂对象时&#xff0c;仅用之前学过的基本数据类型表达不了&#xff08;如&#xff1a;我要描述一个人&#xff0c;仅靠基本数据类型只能说定义他的一种属性<如用 int 定义他的年龄>…

字符串的处理

一、字符数组 用来存放字符型数据的数组称为字符数组&#xff0c;其元素是一个个的字符。 char 字符数组名[常量表达式]&#xff1b; C语言规定字符串是以\0字符作为结束符的字符数组。 在程序中可以通过判断数组元素是否为空字符来判断字符串是否结束。 字符串的输…

介绍Java中的常/变量.各种数据类型以及类型转换和提升的用法

本文简单描述了什么是常量和变量,介绍了Java各种数据类型:基本数据类型(四类八种,大小和范围)和引用数据类型(种类),简单介绍了包装类字符串类型,以及不同数据类型之间的常量和变量,数据类型之间的转换和提升… Java常/变量和数据类型一.什么是常量?二.什么是变量?三.数据类型…

[审核]因为审核人员不了解苹果登录被拒

1.审核被拒信息 Guideline 2.1 - Information Needed We’re looking forward to completing the review of your app, but we need more information to continue. Next Steps Please provide detailed answers to the following questions in your reply to this message i…

寒假集训一期总结(一)–––思维训练

目录 思维训练 走方格 解题思路 参考代码 最短曼哈顿距离 ​编辑 解题思路 参考代码 酒厂选址 解题思路 参考代码 雪地足迹Tracks in the Snow 解题思路 参考代码 一个星期没有更博客了…这一个星期,去学校信竞集训的我收获颇丰,下面就是我的还加集训总结 思…

【小白向】让电脑成为热点WIFI

让电脑成为热点WIFI 本文针对下述情况&#xff0c;有一台电脑&#xff0c;一部手机&#xff0c;但是电脑通过网线连接。此时电脑可以上网&#xff0c;手机没有流量&#xff0c; 仅能通过WIFI上网&#xff0c;但此时没有WIFI。 其实你的电脑可能自己本身就能作为热点发布WIFI&…

绕任一向量旋转矩阵计算思考与实现

欢迎关注更多精彩 关注我&#xff0c;学习常用算法与数据结构&#xff0c;一题多解&#xff0c;降维打击。 问题提出 如图所示&#xff0c;在空间中有一向量A&#xff0c;问点O绕A方向逆时针旋转角度α的矩阵如何表示。 问题分析 问题化规 直接去构造一个矩阵是比较困难的。…

自从我学会了Jenkins的自动构建,我再也没有每次都打包上传到服务器然后发布Java服务了

上次我们讲了使用Jenkins部署maven项目 工作两年半&#xff0c;终于学会了Jenkins部署Maven项目 这次我们来讲一下每次提交代码的时候Jenkins自动构建 我们使用的代码仓库是gitee 文章目录&#x1f3c3;第一步&#xff0c;我们在Jenkins中安装gitee插件&#x1f3c3;第二步&am…

Go语言并发编程及依赖管理

目录&#x1f9e1;并发编程GoroutineCSP(Communicating Sequential Processes)&#x1f9e1;依赖管理依赖演变依赖管理三要素&#x1f49f;这里是CS大白话专场&#xff0c;让枯燥的学习变得有趣&#xff01; &#x1f49f;没有对象不要怕&#xff0c;我们new一个出来&#xff0…

Linux (open、write、read、close、lseek、chmod、sync)操作文件的函数详解

目录 一、文件操作方式 二、Linux底层文件操作 1. open 2. write 3. read 4. close 5. lseek 6. chmod 7. sync、syncfs、fsync、fdatasync 三、 Linux 系统调用 四、总结 linux中&#xff0c;一切皆文件&#xff08;网络设备除外&#xff09; 硬件设备也“是”文件&a…

力扣刷题记录——507.完美数、509. 斐波那契数、520. 检测大写字母

本专栏主要记录力扣的刷题记录&#xff0c;备战蓝桥杯&#xff0c;供复盘和优化算法使用&#xff0c;也希望给大家带来帮助&#xff0c;博主是算法小白&#xff0c;希望各位大佬不要见笑&#xff0c;今天要分享的是——《507.完美数、509. 斐波那契数、520. 检测大写字母》。 目…

InfluxDB + Grafana计算成功率

文章目录方式一 借助Grafana的Transfrom方式二 Influx子查询Transfrom介绍建议针对每类Metric&#xff0c;使用一个Metric&#xff0c;增加success的tag区分成功还是失败。 方式一 借助Grafana的Transfrom 第一步&#xff1a;新建2个Query Query Total: SELECT sum("coun…

安科瑞电气火灾监控系统在春晓161#地块人防工程的设计与应用

安科瑞 华楠摘要&#xff1a;本文简述了电气火灾监控系统的组成原理&#xff0c;分析了电气火灾监控系统在应用中的设计依据和相关规范。通过安科瑞剩余电流式电气火灾监控系统在春晓161#地块人防工程电气火灾监控系统项目的实例介绍&#xff0c;阐述了电气火灾监控系统功能的实…

c语言实现扫雷(详细讲解)

本篇介绍,讲解如何使用c语言实现扫雷小游戏. 金句分享: ✨✨✨爱你所爱,行你所行✨✨✨ 目录前言:一、游戏设计思路介绍:效果展示二、游戏的分步讲解2.1、主函数测试区&#xff08;test.c&#xff09;基本构成2.2、游戏中函数实现区(game.c) (重点)2.21、雷盘的创建与初始化函…