Vue使用axios----ReferenceError: axios is not defined问题解决-CSDN博客

mikel阅读(493)

来源: Vue使用axios—-ReferenceError: axios is not defined问题解决-CSDN博客

问题1:ReferenceError: axios is not defined

问题代码:

const requrl = ‘/user/find/1’
axios.get(requrl).then(response => {
const user = response.data
this.username = user.username
this.age = user.age
}).catch(
function (error) {
// 请求失败处理
alert(‘请求失败!’)
})

解决方案1:
main.js加上Vue.prototype.$axios = axios
axios.get改为this.$axios.get调用,修改后不再报错

import axios from ‘axios’
//其他vue组件中就可以this.$axios调用使用
Vue.prototype.$axios = axios

const requrl = ‘/user/find/1’
this.$axios.get(requrl).then(response => {
const user = response.data
this.username = user.username
this.age = user.age
}).catch(
function (error) {
// 请求失败处理
alert(‘请求失败!’)
})

解决方案2:结合vue-axios插件,安装插件后,就不需要将axios绑定到Vue原型链上了,组件内通过this.axios调用
npm install axios vue-axios –save

import axios from ‘axios’
import VueAxios from “vue-axios”;
Vue.use(VueAxios,axios)
//其他vue组件中就可以this.$axios调用使用
//Vue.prototype.$axios = axios

const requrl = ‘/user/find/1’
this.axios.get(requrl).then(response => {
const user = response.data
this.username = user.username
this.age = user.age
}).catch(
function (error) {
// 请求失败处理
alert(‘请求失败!’)
})

问题2,并发测试时,TypeError: Cannot read property ‘$axios’ of undefined

报错代码如下:

function getUserget() {
return this.$axios.get(‘/user/find/1’)
}
function getUserpost() {
return this.$axios.post(‘/user/find’,{id:2})
}
this.$axios.all([getUserget(), getUserpost()])
.then(this.$axios.spread(function (res1, res2) {
// 两个请求现在都执行完成
const user1 = res1.data
const user2 = res2.data
console.log(user1.username)
console.log(user2.username)
}));

原因未知,有知道的大佬还望不吝赐教
无奈只得在app.vue(我是在这里做的测试)中再次引入
import axios from ‘axios’
再次引入,直接用axios就行

function getUserget() {
return axios.get(‘/user/find/1’);
}
function getUserpost() {
return axios.post(‘/user/find’,{id:2});
}

————————————————
版权声明:本文为CSDN博主「纷纷四季」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Laputa219/article/details/106802230

Vue入门项目——WebApi-CSDN博客

mikel阅读(650)

来源: Vue入门项目——WebApi-CSDN博客

Vue入门——WebApi
vue3项目搭建
组合式API
响应式API
reactive()
ref()
生命周期钩子
computed计算属性函数
watch监听函数
父子通信
模板引用
组合选项
vue3项目搭建
简单看下Vue3的优势吧

下载安装npm及node.js16.0以上版本(确保安装成功可用如下代码检查版本)

npm -v
node -v

下一步创建vue3项目,输入项目名字后并进行选择插件(可以直接回车全部no)

npm init vue@latest

然后找到项目用vsCode打开,并进行初始化

npm install

初始化完成后,把项目跑起来,可点击链接进行查看,看到如下画面就完成咯

npm run dev

组合式API
响应式API

reactive()
接收一个普通对象然后返回该普通对象的响应式代理(众所周知括号里的内容一般才是最重要的,没错,reactive只能封装对象类型的参数)
🍊个🌰:

<script setup>
//1、导入函数
import { reactive } from ‘vue’;
//2、执行函数,传入一个对象类型参数,用变量接收
const state = reactive({
count: 0 //参数是常规对象,不会引起视图更新
})
//使用reactive函数包装后,会变成响应式对象
const addCount = () => {
state.count++
}

const state2 = {
count2: 0 //参数是常规对象,不会引起视图更新
}
const addCount2 = () => {
state2.count2++
}
</script>
<template>
<div>
<button @click=”addCount” id=”btn1″>{{ state.count }}</button>
<!–这个按钮中的参数被reactive包裹,会被视图响应–>
<button @click=”addCount2″ id=”btn2″>{{state2.count2}}</button>
<!–这个参数没有被包裹不会响应–>
<!–但是会累积点击,在下一次点击第一个按钮时会被响应,一次性累计添加–>
</div>
</template>

ref()
接受一个参数值并返回一个响应式且可改变的 ref 对象。(找不同咯,对没错,ref()支持任何类型的参数对其进行封装)

<script setup>
//1、导入函数
import { ref } from ‘vue’;
//2、执行函数,传入一个参数(简单类型与对象类型均可),用变量接收
const count = ref(0)
const addCount = () => {
//脚本区域修改ref产生的响应式对象,必须通过.value属性修改
count.value++
}

const person = ref({
name: ‘芋头’
})
const setPerson = () => {
person.value.name = ‘一哑7’
}
</script>
<template>
<div>
<button @click=”addCount”>{{ count }}</button>
<!–点击button数字添加–>
</div>
<div>
<button @click=”setPerson”>{{ “这个人是” + person.name }}</button>
<!–点击button修改人名–>
</div>
</template>

冷知识:ref函数内部实现时依赖reactive函数,一般使用ref更多

生命周期钩子
先来看一张图熟悉一下api

详细的生命周期API使用请参考Vue3官网API介绍

生命周期函数:引入函数后执行并传入回调,会自动执行回调 多次执行生命周期函数时,回调会依次执行

<script setup>
//引入函数
import { onMounted } from ‘vue’

//执行函数并传入回调
onMounted( () => {
console.log(‘执行回调~’)
console.log(‘执行回调~’)
})

//多次执行函数时依次执行
onMounted( () => {
console.log(‘执行回调2~’)
console.log(‘执行回调2~’)
})

</script>

computed计算属性函数
<script setup>
//1、导入函数
import { ref , computed } from ‘vue’;
let list = ref([1 , 2 , 3 , 4 , 5 , 6 , 7 , 8])
//2、执行函数;return经过计算的值;用变量接受这个值
const computedList = computed(() => {
return list.value.filter(item => item > 2)
})
//计时器观察实时更新
setTimeout(() => {
list.value.push(-1 , 10 , 12 , 0)
} , 3000)
</script>
<template>
<div>
{{`原始数组是:[${list}]`}}
</div>
<div>
{{`新数组是:[${computedList}]`}}
</div>
</template>

watch监听函数
监听单个数据:
watch需要两个参数,第一个参数是需要监听的数据,第二个参数是在所需要监听的数据发生变化时所需要执行的回调函数。
<script setup>
//1、导入函数
import { ref , watch } from ‘vue’;
//2、设置需要监听的数据和回调函数
const count = ref(0)
const setCount = () => {
count.value++
}
//3、watch监听单个数据
watch(count , () => {
alert(‘哦豁,变了哦’)
})
</script>
<template>
<div>
<button @click=”setCount”>{{count}}</button>
</div>
</template>

监听多个数据:
这时只需要用数组把需要监听的数据封装起来就好了
<script setup>
//1、导入函数
import { ref , watch } from ‘vue’;
//2、设置需要监听的数据和回调函数
const count = ref(0)
const setCount = () => {
count.value++
}

const name = ref(‘芋头’)
const setName = () => {
name.value = ‘一哑7’
}
//3、watch监听多个数据,用数组把需要监听的数据封装起来就好了
watch([count , name] , ([newCount , newName] , [odlCount , oldName]) => {
alert(‘哦豁,变了哦’ + [odlCount , oldName] + ‘变成了’ + [newCount , newName] )
})
</script>
<template>
<div>
<button @click=”setCount”>{{count}}</button>
<button @click=”setName”>{{name}}</button>
</div>
</template>

不要忘了关于watch还有两个参数哦,immediate:立即执行;deep:深度监听

immediate : 在第一次刚刚监听到的时候就先执行一次
<script setup>
watch(count , () => {
alert(‘变了哦’)
} , {
immediate: true}
)
</script>

deep :通过watch监听的ref对象默认浅层监听,直接修改嵌套对象都不会出发回调函数,此时便需要开启deep
没理解?把下面代码复制下来删掉deep对比一下就知道咯
<script setup>
watch(name , () => {
alert(‘变了哦’)
} , {
deep: true
}
)
</script>

精确监听:监听多个属性中的一个属性(deep会有性能损耗建议不开启deep而是使用精确监听)
只需要把watch函数的参数变成两个回调函数,第一个是需要监听的东西,第二个是监听到之后需要执行的回调函数
🍊个🌰:

<script setup>
import { ref, watch } from ‘vue’
const state = ref({
name: ‘芋头’,
age: 22,
sex: ‘男’

})
const chanheName = () => {
//修改姓名
state.value.name = ‘一哑7’
}
const changeAge = () => {
//修改年龄~
state.value.age = 23
}
const changeSex = () => {
//去一趟泰国
state.value.sex = ‘女’
}
//精确侦听具体属性
watch(
() => state.value.age,
() => {
console.log(‘长大一岁了哦’)
}
)
watch(
() => state.value.name,
() => {
console.log(‘还顺路改了个名字’)
}
)
</script>
<template>
<div>
<div>这个人现在的名字:{{state.name}}</div>
<div>这个人现在的年龄:{{state.age}}</div>
</div>
<div>
<button @click=”chanheName”>修改姓名</button>
<button @click=”changeAge”>修改年领</button>
</div>
</template>

父子通信
父传子:在子组件中用defineProps接收父组件中绑定的数据就好了

//父亲页
<template>
<div>
I am dadTest
</div>
<div>
<sonTest />
</div>
<div>
父传子
<!– 父亲上绑定属性 –>
<sonTest message=”father Data” />
</div>
</template>
<script setup>
import sonTest from ‘./views/sonTest.vue’;
</script>

//儿子页
<template>
<div>
I am sonTest {{message}}
</div>
</template>
<script setup>
//用defineProps接收数据就好了
defineProps({
message: String
})
</script>

子传父:父组件通过@绑定事件,子组件内部通过defindEmits函数生成emit方法触发事件
defindEmits 传递参数是数组哦~,因为可传递所有绑定事件

//儿子页
<template>
<div>
<button @click=”sendMsg”>点击按钮查看来自子组件的信息</button>
</div>
</template>
<script setup>
//利用defindEmits函数生成emit方法
const emit = defineEmits([‘giveMeMessqge’])
const sendMsg = () => {
//出发绑定事件
emit(‘giveMeMessqge’ , ‘儿子消息’)
}
</script>

//父亲页
<template>
<div>
<sonTest @giveMeMessqge=”getMessage”/>
</div>
</template>
<script setup>
import sonTest from ‘./views/sonTest.vue’;
const getMessage = (msg) => {
alert(msg)
}
</script>

模板引用
默认情况下<script setup>语法糖下组件内部的属性与方法不对父组件开放,可以通过defineExpose编译宏指定哪些属性和方法允许访问

//子组件
<template>
<div>
Test 组件
</div>
</template>
<script setup>
import { ref } from ‘vue’;
const name = ref(‘nameTest’)
const setName = () => {
name.value = ‘newNameTest’
}

// defineExpose({
// name,
// setName
// })
</script>

//父组件
<template>
<div>
<!– 用ref标识绑定ref对象 –>
<h1 ref=”h1Ref”>dom标签h1</h1>
<sonTest ref=”sonRef”/>
</div>
</template>
<script setup>
import sonTest from ‘./views/sonTest.vue’;
import { ref , onMounted } from ‘vue’

//调用ref函数生成ref对象当然组件也是可以的
const h1Ref = ref(null)
const sonRef = ref(null)

//不过要注意在获取对象时要等组件挂在完毕哦,可以使用
onMounted(() => {
alert(h1Ref.value)
alert(sonRef.value)
})
</script>

组合选项
顶层组件向任意底层组件传递参数和方法,实现跨层组件通信顶层组件通过provide函数提供数据,底层组件通过inject函数获取数据

 

 

跨层传递普通数据:

 

跨层传递响应式数据:

 

跨层传递方法:顶层组件可以向底层组件传递方法,底层组件调用方法修改顶层组件中的数据

 

————————————————
版权声明:本文为CSDN博主「一哑7」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_45696320/article/details/131812520

前后端分离:WebAPI+Vue开发——远程数据请求axios_前后端分离前端axios远程访问-CSDN博客

mikel阅读(605)

来源: 前后端分离:WebAPI+Vue开发——远程数据请求axios_前后端分离前端axios远程访问-CSDN博客

前后端分离:WebAPI+Vue开发——远程数据请求axios

前后端分离:WebAPI+Vue开发——跨域设置

前后端分离:WebAPI+Vue开发——身份认证

本文没有Vue语法内容(Vue中文文档),只记录本人开发中遇到的难点

远程请求采用axios(axios中文文档,注意:IE11以下不支持axios)

ajax、axios、fetch之间的详细区别以及优缺点(https://blog.csdn.net/twodogya/article/details/80223508)

Get请求:

axios.get(‘http://api.abc.com/api/user’,{
param:{Id:132}
}).then(function(response){
console.log(response.data);
}).catch(function(error){
console.log(error);
});
Post请求:

axios.post(‘http://api.abc.com/api/user’,{
name:’frank’,
sex:’1′
}).then(function (response){
console.log(response.data)
}).catch(function(error){
console.log(error);
});
原始请求:

axios({
url:’http://api.abc.com/api/user’,
method:’post’,
responseType:’json’,
data:{
name:’frank’,
sex:’1′
}
}).then(function(response){
console.log(response.data);
}).catch(function(error){
console.log(error);
})
response的结构如下:

{
// 返回的数据
data: {},
// http状态码
status: 200,
//状态
statusText: ‘OK’,
// 返回结果的header头
headers: {},
//axios请求的配置内容
config: {}
}
全局默认值设置

axios.defaults.baseURL = ‘http://api.abc.com’;
设置之后,axios的远程请求,不用再具体到域名,直接用 url:’/api/user’即可,实际项目中建议把POST、GET方法进行封装,统一调用,如有更换其他远程请求方式的需求(如ajax)会很方便,封装如下:

//axios的Post方法封装
function POST(url, data, method) {
var tk = getCookie(‘token’);
if (tk) {
axios({
url: url,
method: ‘post’,
data: data,
headers: { ‘token’: tk }
}).then(function (response) {
if (response.data.ret == -2)//没有访问权限
{
location.href = ‘/’;
}
else if (response.data.ret == -1) {//程序错误
console.log(response.data.msg);
}
else {
method(response.data);
}
}).catch(function (error) {
console.log(error);
})
}
}
//axios的Get方法封装
function GET(url, data, method) {
var tk = getCookie(‘token’);
if (tk) {
axios({
url: url,
method: ‘get’,
data: data,
headers: { ‘token’: tk }
}).then(function (response) {
if (response.data.ret == -2)//没有访问权限
{
location.href = ‘/’;
}
else if (response.data.ret == -1) {//程序错误
console.log(response.data.msg);
}
else {
method(response.data);
}
}).catch(function (error) {
console.log(error);
})
}
}
GET和POST方法也可以封装到一个里边,method是一个回调函数,处理得到的数据;getCookie是自己写的cookie获取方法,此处的token类似sessionid,放在了请求头中,作为一个用户身份识别标识使用,用户身份识别后续再写;response.data.ret和response.msg是API接口中自定义的请求状态和提示信息
————————————————
版权声明:本文为CSDN博主「峰frank」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/fengkang511/article/details/82837701

.net Core3.1 WebAPI+Vue 前后端调用的一些问题 - wjbych - 博客园

mikel阅读(543)

来源: .net Core3.1 WebAPI+Vue 前后端调用的一些问题 – wjbych – 博客园

本人小白,如果你对此没啥了解,可以先搜索某个大神的教程看看:

Asp.netCore3.0 WebApi从0到1手摸手教你写【1】简单的webapi接口

说实话讲的真不错,只是我水平有限,看到实体类就晕了,只好将就着做个自己能用的就行了,至于怎么保存数据,以后再说。

需要的开发工具:

1、VS 2019 用以开发后端webapi

2、VS Code 用以前端开发,使用vue来做为前端框架,不懂的可以看看技术胖的教程,讲的很不错还免费,地址为 https://space.bilibili.com/165659472/channel/detail?cid=76803

3、Postman,用于调试你开发的接口,现在只有安装版了,可以在你没有使用vue时调试下所做的接口

 


 

一、控制器的路由设置问题

1、在项目中新建控制器,注释掉控制器中的路由

 

然后在startup.cs文件的Configureg事件中增加动态路由

 

 

复制代码
          app.UseEndpoints(endpoints =>
            {
                //endpoints.MapControllers();
                endpoints.MapControllerRoute(
                    name: "defalut",
                    pattern: "api/{controller=Home}/{action=Index}/{id?}"
                 );
            });
复制代码

 

二、项目中依赖项中包的安装问题

使用NuGet包管理器来安装所需要的包,如果你不会EF,不会LinQ,那么就不需要安装那些包了,我们可以使用以前的方法来保存数据到数据库的,我安装了三个

 

 

安装的时候比较慢,system.data.SQLClinet就是以前.net 4.0中我们用来保存数据到数据库时引用的包,现在需要单独引入。

 

三、Post数据传输时接口接收的问题

通过 Post来向接口传递数据后端接收一共有四种方法,可以参考下面这位大神写的

.NET Core WebAPI post参数传递时后端的接收方式

我们使用第一种方法:实体类,个人感觉容易理解。

在项目中新建一个类,用于存放定义的实体类,你也可以定义到控制器中。

 

在控制器中使用定义好的实体类

 

 四、使用postman测试接口

按F5启动你的项目,然后打开postman

 

就算你body中的json数据和后端定义的实体类不一致,后端也能接收到, 但是后端实体类中有的在json中一定要有。

 

五、使用Vue调用后端的接口

需要使用vue-cli来创建前端项目,如果你是单页面引用的话,那么在使用方法上会有所不同。

1、安装axios包

2、在main.js文件中引用axios

 

 

3、在vue文件中使用axios,为了方便使用,需要做个简单的封装。

 

 

注释掉的部分起的作用是把json转成字符串,因为所要调用的post接口参数就是json的,所以不需转换了

复制代码
 axiosPost:function(url,params){
          return new Promise((resolve, reject) => {
                  this.$axios({
                  url: url,
                  method: 'post',
                  data: params,
                  // transformRequest: [function(data) {
                  //     let ret = ''
                  //     for(let it in data) {
                  //         ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&'
                  //     }
                  //     console.log(ret)
                  //     return ret
                  // }],
                  headers: {
                      'Content-Type':'application/json'
                  }
              })
              .then(res=>{
                  resolve(res.data);
              })
          });
      },
复制代码

4、在vue文件中使用封装好的axios

 

 

 

复制代码
login(){
          var params={
              user:this.yhm,
              pwd:this.pwd,
          };
          var url='http://192.168.3.111:5000/api/user/userlogin';
          this.axiosPost(url,params)
          .then(res=>{
              if (res===401){
                  //window.location.href='login.htm';
                  console.log(res);
              }else{
                  console.log(res);
              }
          })
          //return ''
      },
复制代码

res中就是返回的数据,在这里接收到以后可以自已处理一下。

5、跨域请求错误的处理

调用后发现出错了,

跨域请求错误: Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’

可以参照下面的这个贴子来进行处理

https://www.cnblogs.com/jidanfan/p/11177509.html

在后端webapi项目中的startup.cs文件的Configure事件中增加以下的代码

app.UseCors(cfg =>
{
cfg.AllowAnyOrigin(); //对应跨域请求的地址
cfg.AllowAnyMethod(); //对应请求方法的Method
cfg.AllowAnyHeader(); //对应请求方法的Headers
//cfg.AllowCredentials(); //对应请求的withCredentials 值
});

好了,到此一个最基本的前端调用后端的例子就做好了。

水平很差,不会写贴子,写的东西自己公司内部将就着能用用就行了

2023版:深度比较几种.NET Excel导出库的性能差异 - 百宝门园地 - 博客园

mikel阅读(482)

来源: 2023版:深度比较几种.NET Excel导出库的性能差异 – 百宝门园地 – 博客园

引言

背景和目的

本文介绍了几个常用的电子表格处理库,包括EPPlus、NPOI、Aspose.Cells和DocumentFormat.OpenXml,我们将对这些库进行性能测评,以便为开发人员提供实际的性能指标和数据。

下表将功能/特点、开源/许可证这两列分开,以满足需求:

功能 / 特点 EPPlus NPOI Aspose.Cells DocumentFormat.OpenXml
开源
许可证 MIT Apache 商业 MIT
支持的 Excel 版本 Excel 2007 及更高版本 Excel 97-2003 Excel 2003 及更高版本 Excel 2007 及更高版本

测评电脑配置

组件 规格
CPU 11th Gen Intel(R) Core(TM) i5-11320H @ 3.20GHz,2496 Mhz,4 个内核,8 个逻辑处理器
内存 40 GB DDR4 3200MHz
操作系统 Microsoft Windows 10 专业版
电源选项 已设置为高性能
软件 LINQPad 7.8.5 Beta
运行时 .NET 6.0.21

准备工作

使用Bogus库生成6万条标准化的测试数据。

void Main()
{
	string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "test-data.json");
	using var file = File.Create(path);
	using var writer = new Utf8JsonWriter(file, new JsonWriterOptions { Indented = true });
	var data = new Bogus.Faker<Data>()
		.RuleFor(x => x.Id, x => x.IndexFaker + 1)
		.RuleFor(x => x.Gender, x => x.Person.Gender)
		.RuleFor(x => x.FirstName, (x, u) => x.Name.FirstName(u.Gender))
		.RuleFor(x => x.LastName, (x, u) => x.Name.LastName(u.Gender))
		.RuleFor(x => x.Email, (x, u) => x.Internet.Email(u.FirstName, u.LastName))
		.RuleFor(x => x.BirthDate, x => x.Person.DateOfBirth)
		.RuleFor(x => x.Company, x => x.Person.Company.Name)
		.RuleFor(x => x.Phone, x => x.Person.Phone)
		.RuleFor(x => x.Website, x => x.Person.Website)
		.RuleFor(x => x.SSN, x => x.Person.Ssn())
		.GenerateForever().Take(6_0000)
		.Dump();
	JsonSerializer.Serialize(writer, data);
	Process.Start("explorer", @$"/select, ""{path}""".Dump());
}

Bogus输出结果

Id Gender FirstName LastName Email BirthDate Company Phone Website SSN
1 Male Antonio Paucek Antonio.Paucek@gmail.com 1987/10/31 5:46:50 Moen, Willms and Maggio (898) 283-1583 x88626 pamela.name 850-06-4706
2 Male Kurt Gerhold Kurt.Gerhold40@yahoo.com 1985/11/1 18:41:01 Wilkinson and Sons (698) 637-0181 x49124 cordelia.net 014-86-1757
3 Male Howard Hegmann Howard2@hotmail.com 1979/7/20 22:35:40 Kassulke, Murphy and Volkman (544) 464-9818 x98381 kari.com 360-23-1669
4 Female Rosemarie Powlowski Rosemarie.Powlowski48@hotmail.com 1964/5/18 1:35:45 Will Group 1-740-705-6482 laurence.net 236-10-9925
5 Female Eunice Rogahn Eunice84@gmail.com 1979/11/25 11:53:14 Rippin – Rowe (691) 491-2282 x3466 yvette.net 219-75-6886
……

创建公共类方便正式测评使用

void Main()
{
    string path = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\test-data.json";
    LoadUsers(path).Dump();
}

List<User> LoadUsers(string jsonfile)
{
    string path = jsonfile;
    byte[] bytes = File.ReadAllBytes(path);
    return JsonSerializer.Deserialize<List<User>>(bytes);
}

IObservable<object> Measure(Action action, int times = 5)
{
    return Enumerable.Range(1, times).Select(i =>
    {
        var sw = Stopwatch.StartNew();

        long memory1 = GC.GetTotalMemory(true);
        long allocate1 = GC.GetTotalAllocatedBytes(true);
        {
            action();
        }
        long allocate2 = GC.GetTotalAllocatedBytes(true);
        long memory2 = GC.GetTotalMemory(true);

        sw.Stop();
        return new
        {
            次数 = i, 
            分配内存 = (allocate2 - allocate1).ToString("N0"),
            内存提高 = (memory2 - memory1).ToString("N0"), 
            耗时 = sw.ElapsedMilliseconds,
        };
    }).ToObservable();
}

class User
{
    public int Id { get; set; }
    public int Gender { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public DateTime BirthDate { get; set; }
    public string Company { get; set; }
    public string Phone { get; set; }
    public string Website { get; set; }
    public string SSN { get; set; }
}

代码解释

1、上面的代码单位是字节 (bytes)

2 、其中IObservable(System.IObservable)是用于处理事件流的接口,它实现了观察者模式。它表示一个可观察的序列,可以产生一系列的事件,并允许其他对象(观察者)来订阅和接收这些事件。IObservable 适用于动态的、实时的事件流处理,允许观察者以异步方式接收事件,可以用于响应式编程、事件驱动的编程模型等。

3、GC.GetTotalAllocatedBytes(true) 获取分配内存大小
GC.GetTotalMemory(true) 获取占用内存大小

性能测评

EPPlus

string path = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\test-data.json";
List<User> users = LoadUsers(path);

Measure(() =>
{
    Export(users, Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\export.epplus.xlsx");
}).Dump("EPPlus");

void Export<T>(List<T> data, string path)
{
    using var stream = File.Create(path);
    using var excel = new ExcelPackage(stream);
    ExcelWorksheet sheet = excel.Workbook.Worksheets.Add("Sheet1");
    PropertyInfo[] props = typeof(User).GetProperties();
    for (var i = 0; i < props.Length; ++i)
    {
        sheet.Cells[1, i + 1].Value = props[i].Name;
    }
    for (var i = 0; i < data.Count; ++i)
    {
        for (var j = 0; j < props.Length; ++j)
        {
            sheet.Cells[i + 2, j + 1].Value = props[j].GetValue(data[i]);
        }
    }
    excel.Save();
}

输出结果

EPPlus (6.2.8) (2023/8/15)输出结果

次数ΞΞ 分配内存ΞΞ 内存提高ΞΞ 耗时ΞΞ
1 454,869,176 970,160 2447
2 440,353,488 176 1776
3 440,062,264 0 1716
4 440,283,584 0 1750
5 440,653,264 0 1813

EPPlus (4.5.3.2)(2019/6/16)输出结果

次数ΞΞ 分配内存ΞΞ 内存提高ΞΞ 耗时ΞΞ
1 963,850,944 192,048 2765
2 509,450,792 600 1897
3 509,872,160 424 1920
4 509,858,576 424 1989
5 509,651,512 424 2076

由此看出 相比2019,到了2023年EPPlus的性能得到了略微的提升

NPOI

示例代码一:XSSFWorkbook

List<User> users = LoadUsers(Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\test-data.json");

Measure(() =>
{
    Export(users, Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\export.npoi.xlsx");
}).Dump("NPOI");

void Export<T>(List<T> data, string path)
{
    IWorkbook workbook = new XSSFWorkbook();
    ISheet sheet = workbook.CreateSheet("Sheet1");

    var headRow = sheet.CreateRow(0);
    PropertyInfo[] props = typeof(User).GetProperties();
    for (var i = 0; i < props.Length; ++i)
    {
        headRow.CreateCell(i).SetCellValue(props[i].Name);
    }
    for (var i = 0; i < data.Count; ++i)
    {
        var row = sheet.CreateRow(i + 1);
        for (var j = 0; j < props.Length; ++j)
        {
            row.CreateCell(j).SetCellValue(props[j].GetValue(data[i]).ToString());
        }
    }

    using var file = File.Create(path);
    workbook.Write(file);
	workbook.Close();
}

输出结果

NPOI (2.6.1)(2023/7/12)输出结果

次数ΞΞ 分配内存 内存提高 耗时ΞΞ
1 1,589,285,792 567,272 5549
2 1,577,028,664 96 7043
3 1,577,398,488 48 8107
4 1,576,360,696 -90,512 9336
5 1,576,226,688 -3,120 8289

NPOI (2.4.1)(2018/12/18)输出结果

次数ΞΞ 分配内存 内存提高 耗时ΞΞ
1 1,648,548,696 526,824 6947
2 1,633,685,136 120 7921
3 1,634,033,296 24 8864
4 1,634,660,176 -90,200 8945
5 1,634,205,368 -2,584 8078

示例代码二:SXSSFWorkbook

List<User> users = LoadUsers(Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\test-data.json");

Measure(() =>
{
    Export(users, Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\export.npoi.xlsx");
}).Dump("NPOI");

void Export<T>(List<T> data, string path)
{
    IWorkbook workbook = new SXSSFWorkbook();
    ISheet sheet = workbook.CreateSheet("Sheet1");

    var headRow = sheet.CreateRow(0);
    PropertyInfo[] props = typeof(User).GetProperties();
    for (var i = 0; i < props.Length; ++i)
    {
        headRow.CreateCell(i).SetCellValue(props[i].Name);
    }
    for (var i = 0; i < data.Count; ++i)
    {
        var row = sheet.CreateRow(i + 1);
        for (var j = 0; j < props.Length; ++j)
        {
            row.CreateCell(j).SetCellValue(props[j].GetValue(data[i]).ToString());
        }
    }

    using var file = File.Create(path);
    workbook.Write(file);
	workbook.Close();
}

输出结果

NPOI (2.6.1)(2023/7/12)输出结果

次数 分配内存 内存提高 耗时
1 571,769,144 11,495,488 2542
2 482,573,584 96 5106
3 481,139,296 24 1463
4 481,524,384 48 1510
5 481,466,616 48 1493

NPOI (2.4.1)(2018/12/18)输出结果

次数 分配内存 内存提高 耗时
1 660,709,472 537,512 7808
2 650,060,376 8,128 8649
3 649,006,952 4,136 7064
4 649,267,920 -89,776 6973
5 649,955,024 48 6538

经过测试 发现SXSSFWorkbook 确实比XSSFWorkbook 性能好,有显著提升
由此看出 相比2018,到了2023年NPOI的性能得到了略微的提升

Aspose.Cells

Util.NewProcess = true;
List<User> users = LoadUsers(Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\test-data.json");

SetLicense();

Measure(() =>
{
    Export(users, Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\export.aspose2.xlsx");
}, 5).Dump("Aspose");

void Export<T>(List<T> data, string path)
{
    using var excel = new Workbook();
    excel.Settings.MemorySetting = MemorySetting.Normal;
    excel.Settings.CheckExcelRestriction = false;
    Worksheet sheet = excel.Worksheets["Sheet1"];
    sheet.Cells.ImportCustomObjects(data, 0, 0, new ImportTableOptions
    {
        IsFieldNameShown = true, 
        DateFormat = "MM/DD/YYYY hh:mm:ss AM/PM", 
        ConvertNumericData = false, 
    });
    excel.Save(path);
}

void SetLicense()
{
    Stream stream = new MemoryStream(Convert.FromBase64String(@"密钥"));
    stream.Seek(0, SeekOrigin.Begin);
    new Aspose.Cells.License().SetLicense(stream);
}

输出结果

Aspose.Cells (23.8.0)(2023/8/9)输出结果

次数 分配内存 内存提高 耗时
1 443,025,112 3,471,984 2889
2 392,090,304 30,208 1863
3 391,419,072 -8 1716
4 392,041,144 24 1797
5 392,078,992 24 1689

Aspose.Cells (19.8.0)(2019/8/20)输出结果

次数 分配内存 内存提高 耗时
1 552,862,056 2,987,000 2913
2 508,337,872 49,776 1750
3 507,922,728 24 1933
4 507,949,584 24 1781
5 508,368,208 24 1773

由此看出 相比2019,到了2023年Aspose.Cells的性能还是一样差不多,只是内存占用减少了

DocumentFormat.OpenXml

List<User> users = LoadUsers(Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\test-data.json");

Measure(() =>
{
    Export(users, Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\export.openXml.xlsx");
}).Dump("OpenXML");

void Export<T>(List<T> data, string path)
{
    using SpreadsheetDocument excel = SpreadsheetDocument.Create(path, SpreadsheetDocumentType.Workbook);

    WorkbookPart workbookPart = excel.AddWorkbookPart();
    workbookPart.Workbook = new Workbook();

    WorksheetPart worksheetPart = workbookPart.AddNewPart<WorksheetPart>();
    worksheetPart.Worksheet = new Worksheet(new SheetData());

    Sheets sheets = excel.WorkbookPart.Workbook.AppendChild<Sheets>(new Sheets());
    Sheet sheet = new Sheet
    {
        Id = excel.WorkbookPart.GetIdOfPart(worksheetPart),
        SheetId = 1,
        Name = "Sheet1"
    };
    sheets.Append(sheet);
    
    SheetData sheetData = worksheetPart.Worksheet.GetFirstChild<SheetData>();

    PropertyInfo[] props = typeof(User).GetProperties();
    {    // header
        var row = new Row() { RowIndex = 1 };
        sheetData.Append(row);
        row.Append(props.Select((prop, i) => new Cell
        {
            CellReference = ('A' + i - 1) + row.RowIndex.Value.ToString(),
            CellValue = new CellValue(props[i].Name),
            DataType = new EnumValue<CellValues>(CellValues.String),
        }));
    }
    sheetData.Append(data.Select((item, i) => 
    {
        var row = new Row { RowIndex = (uint)(i + 2) };
        row.Append(props.Select((prop, j) => new Cell
        {
            CellReference = ('A' + j - 1) + row.RowIndex.Value.ToString(),
            CellValue = new CellValue(props[j].GetValue(data[i]).ToString()),
            DataType = new EnumValue<CellValues>(CellValues.String),
        }));
        return row;
    }));
    excel.Save();
}

输出结果

DocumentFormat.OpenXml (2.20.0)(2023/4/7)输出结果

次数ΞΞ 分配内存 内存提高 耗时ΞΞ
1 614,013,080 421,552 3909
2 613,007,112 96 3487
3 613,831,672 104 3465
4 613,058,344 24 3650
5 613,161,096 24 3521

DocumentFormat.OpenXml (2.9.1)(2019/3/14)输出结果

次数ΞΞ 分配内存 内存提高 耗时ΞΞ
1 542,724,752 139,080 3504
2 542,478,208 96 2897
3 543,030,904 24 2826
4 542,247,544 24 2957
5 542,763,312 24 2941

由此看出 相比2019,到了2023年DocumentFormat.OpenXml的性能反而越差啦

结论和总结

结论一:如果你想找开源,(旧版本免费),(最新版收费)EPPlus 依旧是最佳选择

次数ΞΞ 分配内存ΞΞ 内存提高ΞΞ 耗时ΞΞ
1 454,869,176 970,160 2447
2 440,353,488 176 1776
3 440,062,264 0 1716
4 440,283,584 0 1750
5 440,653,264 0 1813

结论二:如果你想找速度快,很稳定,但收费的,Aspose.Cells 依旧是最佳选择

次数 分配内存 内存提高 耗时
1 443,025,112 3,471,984 2889
2 392,090,304 30,208 1863
3 391,419,072 -8 1716
4 392,041,144 24 1797
5 392,078,992 24 1689

总结:
1、EPPlus表现不错,内存和耗时在开源组中表现最佳
2、收费的Aspose.Cells表现最佳,内存占用最低,用时也最短

作者 => 百宝门瞿佑明

此文章是对此前《.NET骚操作》2019年写的文章的更新和扩展
https://www.cnblogs.com/sdflysha/p/20190824-dotnet-excel-compare.html

原文地址:https://blog.baibaomen.com/2023版:深度比较几种-net-excel导出库的性能差异/

vue3 + mark.js | 实现文字标注功能 - 杨芋可可 - 博客园

mikel阅读(509)

来源: vue3 + mark.js | 实现文字标注功能 – 杨芋可可 – 博客园

具体实现

新增

  • 1、监听鼠标抬起事件,通过window.getSelection()方法获取鼠标用户选择的文本范围或光标的当前位置。
  • 2、通过 选中的文字长度是否大于0window.getSelection().isCollapsed (返回一个布尔值用于描述选区的起始点和终止点是否位于一个位置,即是否框选了)来判断是否展示标签选择的弹窗。
  • 3、标签选择的弹窗采用 子绝父相 的定位方式,通过鼠标抬起的位置确认弹窗的 top 与 left 值。
const TAG_WIDTH = 280 //自定义最大范围,以保证不超过内容的最大宽度 const tagInfo = ref({ visible: false, top: 0, left: 0, }) const el = document.getElementById(‘text-container’) //鼠标抬起 el?.addEventListener(‘mouseup’, (e) => { const text = window?.getSelection()?.toString() || if (text.length > 0) { const left = e.offsetX < TAG_WIDTH ? 0 : e.offsetX300 tagInfo.value = { visible: true, top: e.offsetY + 40, left: left, } getSelectedTextData() } else { tagInfo.value.visible = false } //清空重选/取消数据 resetEditTag()
const selectedText = reactive({ start: 0, end: 0, content: , }) //获取选取的文字数据 const getSelectedTextData = () => { const select = window?.getSelection() as any console.log(‘selectselectselectselect’, select) const nodeValue = select.focusNode?.nodeValue const anchorOffset = select.anchorOffset const focusOffset = select.focusOffset const nodeValueSatrtIndex = markContent.value?.indexOf(nodeValue) selectedText.content = select.toString() if (anchorOffset < focusOffset) { //从左到右标注 selectedText.start = nodeValueSatrtIndex + anchorOffset selectedText.end = nodeValueSatrtIndex + focusOffset } else { //从右到左 selectedText.start = nodeValueSatrtIndex + focusOffset selectedText.end = nodeValueSatrtIndex + anchorOffset } }

JavaScript操作光标和选区详情可参考文档:https://blog.51cto.com/u_14524391/3712814

  • 4、选中标签后,采用markjs的markRanges()方式去创建一个选中的元素并为其添加样式和绑定事件。
  • 5、定义一个响应式的文字列表,专门记录标记的内容,添加完元素后可追加一条已标记的数据。
import Mark from ‘mark.js’ import {ref} from ‘vue import { nanoid } from ‘nanoid‘ const selectedTextList = ref([]) const handleSelectLabel = (t) => { const marker = new Mark(document.getElementById(‘text-container‘)) const { tag_color, tag_name, tag_id } = t const markId = nanoid(10) marker.markRanges( [ { start: selectedText.start, //必填 length: selectedText.content.length, //必填 }, ], { className: ‘text-selected‘, element: ‘span‘, each: (element: any) => { //为元素添加样式和属性 element.setAttribute(‘id‘, markId) element.style.borderBottom = `2px solid ${t.tag_color}` //添加下划线 element.style.color = t.tag_color //绑定事件 element.onclick = function (e: any) { // } }, } ) selectedTextList.value.push({ tag_color, tag_name, tag_id, start: selectedText.start, end: selectedText.end, mark_content:selectedText.content, mark_id: markId, }) }

删除

image.png

点击已进行标记的文字————>重选/取消弹窗显示————>点击取消

如何判断点击的文字是否已标记,通过在创建的标记元素中绑定点击事件,触发则表示已标记。

  1. 在点击事件中记录该标记的相关内容,如颜色,文字,起始位置,以及唯一标识id(新建时给元素添加一个id属性,点击时即可通过e.target.id获取)
import { nanoid } from ‘nanoid’ //选择标签后 const markId = nanoid(10) marker.markRanges( [ { start: isReset ? editTag.value.start : selectedText.start, length: isReset ? editTag.value.content.length : selectedText.content.length, }, ], { className: ‘text-selected’, element: ‘span’, each: (element: any) => { element.setAttribute(‘id’, markId) //绑定事件 element.onclick = function (e: any) { e.preventDefault() if (!e.target.id) return const left = e.offsetX < TAG_WIDTH ? 0 : e.offsetX300 const item = selectedTextList.value?.find?.((t) => t.mark_id == e.target.id) as any const { mark_content, tag_id, start, end } = item || {} editTag.value = { visible: true, top: e.offsetY + 40, left: e.offsetX, mark_id: e.target.id, content: mark_content || , tag_id: tag_id || , start: start, end: end, } tagInfo.value = { visible: false, top: e.offsetY + 40, left: left, } } }, } )
  1. 点击取消后,获取在此前记录的id,根据id查询相关的标记元素
  • 使用markjs.unmark()方法即可删除此元素。
  • 绑定的响应式数据,可使用findIndexsplice()删除
  1. 编辑弹窗隐藏
const handleCancel = () => { if (!editTag.value.mark_id) return const markEl = new Mark(document.getElementById(editTag.value.mark_id)) markEl.unmark() selectedTextList.value.splice( selectedTextList.value?.findIndex((t) => t.mark_id == editTag.value.mark_id), 1 ) tagInfo.value = { visible: false, top: 0, left: 0, } resetEditTag() } const resetEditTag = () => { editTag.value = { visible: false, top: 0, left: 0, mark_id: , content: , tag_id: , start: 0, end: 0, } }

重选

image.png

和取消的步骤一样,只不过在点击重选后,先弹出标签弹窗,选择标签后,需要先删除选中的元素,然后再新增一个标记元素。由于在标签选择,在标签选择中判断一下是否是重选,是重选的话就需删除后再创建元素,不是的话就代表是新增,直接新增标记元素(综上所述)。

const handleSelectLabel = (t: TTag) => { tagInfo.value.visible = false const { tag_color, tag_name, tag_id } = t const marker = new Mark(document.getElementById(‘text-container’)) const markId = nanoid(10) const isReset = selectedTextList.value?.map((j) => j.mark_id).includes(editTag.value.mark_id) ? 1 : 0 // 1:重选 0:新增 if (isReset) { //如若重选,则删除后再新增标签 const markEl = new Mark(document.getElementById(editTag.value.mark_id)) markEl.unmark() selectedTextList.value.splice( selectedTextList.value?.findIndex((t) => t.mark_id == editTag.value.mark_id), 1 ) } marker.markRanges( [ { start: isReset ? editTag.value.start : selectedText.start, length: isReset ? editTag.value.content.length : selectedText.content.length, }, ], { className: ‘text-selected’, element: ‘span’, each: (element: any) => { element.setAttribute(‘id’, markId) element.style.borderBottom = `2px solid ${t.tag_color}` element.style.color = t.tag_color element.style.userSelect = ‘none’ element.style.paddingBottom = ‘6px’ element.onclick = function (e: any) { e.preventDefault() if (!e.target.id) return const left = e.offsetX < TAG_WIDTH ? 0 : e.offsetX300 const item = selectedTextList.value?.find?.((t) => t.mark_id == e.target.id) as any const { mark_content, tag_id, start, end } = item || {} editTag.value = { visible: true, top: e.offsetY + 40, left: e.offsetX, mark_id: e.target.id, content: mark_content || , tag_id: tag_id || , start: start, end: end, } tagInfo.value = { visible: false, top: e.offsetY + 40, left: left, } } }, } ) selectedTextList.value.push({ tag_color, tag_name, tag_id, start: isReset ? editTag.value.start : selectedText.start, end: isReset ? editTag.value.end : selectedText.end, mark_content: isReset ? editTag.value.content : selectedText.content, mark_id: markId, }) }

清空标记

image.png

const handleAllDelete = () => { selectedTextList.value = [] const marker = new Mark(document.getElementById(‘text-container’)) marker.unmark() }

完整代码

<script setup lang=“ts”> import { ref, onMounted, reactive } from ‘vue’ import Mark from ‘mark.js’ import { nanoid } from ‘nanoid’ type TTag = { tag_name: string tag_id: string tag_color: string } type TSelectText = { tag_id: string tag_name: string tag_color: string start: number end: number mark_content: string mark_id: string } const TAG_WIDTH = 280 const selectedTextList = ref<TSelectText[]>([]) const selectedText = reactive({ start: 0, end: 0, content: , }) const markContent = ref( ‘这是标注的内容有业绩还是我我很快就很快就开完如突然好几个地方各级很大功夫数据库二极管捍卫国家和我回家很晚十九世纪俄国激活工具和丈母娘环境和颠覆国家的高房价奥苏爱哦因为i以太网图的还是觉得好看啊空间函数调用加快速度还是饥渴的发货可是磕碰日俄和那那么会就开始开会的数据库和也会觉得讲故事的而黄金九二额呵呵三角函数的吧合乎实际的和尽快核实当升科技看交互的接口和送二ui为人开朗少女都被你们进货金额麦当娜表面上的’ ) const tagInfo = ref({ visible: false, top: 0, left: 0, }) const editTag = ref({ visible: false, top: 0, left: 0, mark_id: , content: , tag_id: , start: 0, end: 0, }) const tagList: TTag[] = [ { tag_name: ‘标签一’, tag_color: `#DE050CFF`, tag_id: ‘tag_id1’, }, { tag_name: ‘标签二’, tag_color: `#6ADE05FF`, tag_id: ‘tag_id2’, }, { tag_name: ‘标签三’, tag_color: `#DE058BFF`, tag_id: ‘tag_id3’, }, { tag_name: ‘标签四’, tag_color: `#9205DEFF`, tag_id: ‘tag_id4’, }, { tag_name: ‘标签五’, tag_color: `#DE5F05FF`, tag_id: ‘tag_id5’, }, ] const handleAllDelete = () => { selectedTextList.value = [] const marker = new Mark(document.getElementById(‘text-container’)) marker.unmark() } const handleCancel = () => { if (!editTag.value.mark_id) return const markEl = new Mark(document.getElementById(editTag.value.mark_id)) markEl.unmark() selectedTextList.value.splice( selectedTextList.value?.findIndex((t) => t.mark_id == editTag.value.mark_id), 1 ) tagInfo.value = { visible: false, top: 0, left: 0, } resetEditTag() } const handleReset = () => { editTag.value.visible = false tagInfo.value.visible = true } const handleSave = () => { console.log(‘标注的数据’, selectedTextList.value) } const handleSelectLabel = (t: TTag) => { const { tag_color, tag_name, tag_id } = t tagInfo.value.visible = false const marker = new Mark(document.getElementById(‘text-container’)) const markId = nanoid(10) const isReset = selectedTextList.value?.map((j) => j.mark_id).includes(editTag.value.mark_id) ? 1 : 0 // 1:重选 0:新增 if (isReset) { //如若重选,则删除后再新增标签 const markEl = new Mark(document.getElementById(editTag.value.mark_id)) markEl.unmark() selectedTextList.value.splice( selectedTextList.value?.findIndex((t) => t.mark_id == editTag.value.mark_id), 1 ) } marker.markRanges( [ { start: isReset ? editTag.value.start : selectedText.start, length: isReset ? editTag.value.content.length : selectedText.content.length, }, ], { className: ‘text-selected’, element: ‘span’, each: (element: any) => { element.setAttribute(‘id’, markId) element.style.borderBottom = `2px solid ${t.tag_color}` element.style.color = t.tag_color element.style.userSelect = ‘none’ element.style.paddingBottom = ‘6px’ element.onclick = function (e: any) { e.preventDefault() if (!e.target.id) return const left = e.offsetX < TAG_WIDTH ? 0 : e.offsetX300 const item = selectedTextList.value?.find?.((t) => t.mark_id == e.target.id) as any const { mark_content, tag_id, start, end } = item || {} editTag.value = { visible: true, top: e.offsetY + 40, left: e.offsetX, mark_id: e.target.id, content: mark_content || , tag_id: tag_id || , start: start, end: end, } tagInfo.value = { visible: false, top: e.offsetY + 40, left: left, } } }, } ) selectedTextList.value.push({ tag_color, tag_name, tag_id, start: isReset ? editTag.value.start : selectedText.start, end: isReset ? editTag.value.end : selectedText.end, mark_content: isReset ? editTag.value.content : selectedText.content, mark_id: markId, }) } /** * 获取选取的文字数据 */ const getSelectedTextData = () => { const select = window?.getSelection() as any const nodeValue = select.focusNode?.nodeValue const anchorOffset = select.anchorOffset const focusOffset = select.focusOffset const nodeValueSatrtIndex = markContent.value?.indexOf(nodeValue) selectedText.content = select.toString() if (anchorOffset < focusOffset) { //从左到右标注 selectedText.start = nodeValueSatrtIndex + anchorOffset selectedText.end = nodeValueSatrtIndex + focusOffset } else { //从右到左 selectedText.start = nodeValueSatrtIndex + focusOffset selectedText.end = nodeValueSatrtIndex + anchorOffset } } const resetEditTag = () => { editTag.value = { visible: false, top: 0, left: 0, mark_id: , content: , tag_id: , start: 0, end: 0, } } const drawMark = () => { //模拟后端返回的数据 const res = [ { start: 2, //必备 end: 6, tag_color: ‘#DE050CFF’, tag_id: ‘tag_id1’, tag_name: ‘标签一’, mark_content: ‘标注的内容’, mark_id: ‘mark_id1’, }, { start: 39, end: 41, tag_color: ‘#6ADE05FF’, tag_id: ‘tag_id2’, tag_name: ‘标签二’, mark_content: ‘二极管’, mark_id: ‘mark_id2’, }, { start: 58, end: 61, tag_color: ‘#DE058BFF’, tag_id: ‘tag_id3’, tag_name: ‘标签三’, mark_content: ‘激活工具’, mark_id: ‘mark_id3’, }, ] selectedTextList.value = res?.map((t) => ({ tag_id: t.tag_id, tag_name: t.tag_name, tag_color: t.tag_color, start: t.start, end: t.end, mark_content: t.mark_content, mark_id: t.mark_id, })) const markList = selectedTextList.value?.map((j) => ({ …j, start: j.start, //必备 length: j.end – j.start + 1, //必备 })) || [] const marker = new Mark(document.getElementById(‘text-container’)) markList?.forEach?.(function (m: any) { marker.markRanges([m], { element: ‘span’, className: ‘text-selected’, each: (element: any) => { element.setAttribute(‘id’, m.mark_id) element.style.borderBottom = `2px solid ${m.tag_color}` element.style.color = m.tag_color element.style.userSelect = ‘none’ element.style.paddingBottom = ‘6px’ element.onclick = function (e: any) { console.log(‘cccccc’, m) const left = e.offsetX < TAG_WIDTH ? 0 : e.offsetX300 editTag.value = { visible: true, top: e.offsetY + 40, left: e.offsetX, mark_id: m.mark_id, content: m.mark_content, tag_id: m.tag_id, start: m.start, end: m.end, } tagInfo.value = { visible: false, top: e.offsetY + 40, left: left, } } }, }) }) } //页面初始化 onMounted(() => { const el = document.getElementById(‘text-container’) //鼠标抬起 el?.addEventListener(‘mouseup’, (e) => { const text = window?.getSelection()?.toString() || if (text.length > 0) { const left = e.offsetX < TAG_WIDTH ? 0 : e.offsetX300 tagInfo.value = { visible: true, top: e.offsetY + 40, left: left, } getSelectedTextData() } else { tagInfo.value.visible = false } //清空重选/取消数据 resetEditTag() }) //从后端获取标注数据,进行初始化标注 drawMark() }) </script> <template> <header> <n-button type=“primary” :disabled=“selectedTextList.length == 0 ? true : false” ghost @click=“handleAllDelete” > 清空标记 </n-button> <n-button type=“primary” :disabled=“selectedTextList.length == 0 ? true : false” @click=“handleSave” > 保存 </n-button> </header> <main> <div id=“text-container” class=“text”> {{ markContent }} </div> <!– 标签选择 –> <div v-if=“tagInfo.visible && tagList.length > 0” :class=“[‘tag-box p-4 ‘]” :style=“{ top: tagInfo.top + ‘px’, left: tagInfo.left + ‘px’ }” > <div v-for=“i in tagList” :key=“i.tag_id” class=“tag-name” @click=“handleSelectLabel(i)”> <n-space> <p>{{ i.tag_name }}</p> <n-button v-if=“i.tag_id == editTag.tag_id” text type=“primary”></n-button> </n-space> <div :class=“[‘w-4 h-4’]” :style=“{ background: i.tag_color, }” ></div> </div> </div> <!– 重选/取消 –> <div v-if=“editTag.visible” class=“edit-tag” :style=“{ top: editTag.top + ‘px’, left: editTag.left + ‘px’ }” > <div class=“py-1 bg-gray-100 text-center” @click=“handleCancel”>取 消</div> <div class=“py-1 bg-gray-100 mt-2 text-center” @click=“handleReset”>重 选</div> </div> </main> </template> <style lang=“less” scoped> header { display: flex; justify-content: space-between; align-items: center; padding: 0 24px; height: 80px; border-bottom: 1px solid #e5e7eb; user-select: none; background: #fff; } main { background: #fff; margin: 24px; height: 80vh; padding: 24px; overflow-y: auto; position: relative; box-shadow: 0 3px 8px 0 rgb(0 0 0 / 13%); .text { color: #333; font-weight: 500; font-size: 16px; line-height: 50px; } .tag-box { position: absolute; z-index: 10; width: 280px; max-height: 40vh; overflow-y: auto; background: #fff; border-radius: 4px; box-shadow: 0 9px 28px 8px rgb(0 0 0 / 3%), 0 6px 16px 4px rgb(0 0 0 / 9%), 0 3px 6px2px rgb(0 0 0 / 20%); user-select: none; .tag-name { width: 100%; background: rgba(243, 244, 246, var(–tw-bg-opacity)); font-size: 14px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; padding: 4px 8px; margin-top: 8px; } .tag-name:nth-of-type(1) { margin-top: 0; } } .edit-tag { position: absolute; z-index: 20; padding: 16px; cursor: pointer; width: 100px; background: #fff; border-radius: 4px; box-shadow: 0 9px 28px 8px rgb(0 0 0 / 3%), 0 6px 16px 4px rgb(0 0 0 / 9%), 0 3px 6px2px rgb(0 0 0 / 20%); user-select: none; } ::selection { background: rgb(51 51 51 / 20%); } } </style>

结束语

目前功能实现比较简单,还有很多发挥的空间,先小小的记录一下,最后~,预祝大家,双节快乐!!

markjs

使用Vue轻松搞定表单,不在为表单烦恼 - 知乎

mikel阅读(585)

来源: 使用Vue轻松搞定表单,不在为表单烦恼 – 知乎

介绍

form-create 是一个可以通过 JSON 生成具有动态渲染、数据收集、验证和提交功能的表单生成器。并且支持生成任何 Vue 组件。结合内置17种常用表单组件和自定义组件,再复杂的表单都可以轻松搞定。

文档 | github | 在线示例

功能

  • 自定义组件
    • 可生成任何Vue组件
    • 自带数据验证
    • 轻松转换为表单组件
  • 通过 JSON 生成表单
  • 通过 Maker 生成表单
  • 强大的API,可快速操作表单
  • 双向数据绑定
  • 事件扩展
  • 局部更新
  • 数据验证
  • 栅格布局
  • 内置组件17种常用表单组件

对比 1.x

  • 速度更快
  • 体积更小
  • 更强大的全局配置
  • 自定义组件更容易扩展
  • 更容易支持第三方 UI 库
  • 更少的 bug

示例

通过 JSON 创建表单

动图封面

通过 API 操作表单

动图封面

@form-create包说明

使用

以element-ui版本为例介绍如何在项目中使用 form-create

安装

npm i @form-create/element-ui

挂载

全局注册

import formCreate form '@form-create/element-ui';

Vue.use(formCreate);

局部挂载

import formCreate form '@form-create/element-ui';

export default {
    components:{
        formCreate:formCreaet.$form()
    }
}

生成表单

<template>
    <form-create v-model="$f" :rule="rule" @on-submit="onSubmit"></form-create>
</template>
export default {
  data () {
    return {
     //表单实例对象
     $f:{},
     //表单生成规则
     rule:[
       {
          type:'input',
          field:'goods_name',
          title:'商品名称'
        },
        {
          type:'datePicker',
          field:'created_at',
          title:'创建时间'
        }
     ]
    };
  },
  methods:{
      onSubmit(formData){
          //TODO 提交表单
      }
  }
};

效果

实例对象 $f

可以通过 $f 快速操作表单,例如:

  • $f.hidden:隐藏指定组件
  • $f.validate:验证表单
  • $f.setValue:修改表单组件的值
  • $f.append:追加表单组件

自定义组件

生成

通过标签生成

{
    type:'el-button',
    name: 'btn',
    props:{
        type:'primary',
        field:'btn',
        loading:true
    },
    children:['加载中']
}

通过模板生成

{
    type:'template',
    name:'btn'
    template:'<el-button :loading="loading">{{text}}<el-button>',
    vm: new Vue({
      data:{
        loading:true,
        text:'加载中'
      }
    })
}

转换为表单组件

自定义组件转换为表单组件后,可通过$f.formData,$f.getValue,$f.setValue,$f.disabled等方法快速操作组件,达到和内置组件相同的效果

预定义

props

在自定义组件内部通过props接收一下属性

  • value 表单的值
  • disabled 组件的禁用状态

例如:

vm = Vue({
  props:{
   value:String,
   disabled:Boolean      
  }
})

input 事件

通过input事件更新组件内部的值

当组件值发生变化后,通过 input 事件更新值.例如:

vm.$emit('input',newValue);

挂载自定义组件

要生成的自定义组件必须通过Vue.component方法挂载到全局,或者通过formCreate.component方法挂载

例如:

formCreate.component('TestComponent',component);

或者

Vue.component('TestComponent',component);

生成

表单组件必须定义field属性

JSON

{
    type:'TestComponent',
    value:'test',
    field:'testField',
    title:'自定义组件'
}

Maker

formCreate.maker.create('TestComponent','testField','自定义组件').value('test')

示例

自定义计数器按钮组件,获取按钮点击数.该组件的功能和内置组件相同

formCreate.maker.template('<el-button @click="onClick" long :disabled="disabled">计数器-{{num}}</el-button>', new Vue({
  props:{
    //预定义
    disabled:Boolean,
    value:Number,
  },
  data: function () {
    return {
        num: this.value,
    }
  },
  watch:{
    value(n){
        this.num = n;
    }
  },
  methods: {
    onClick: function () {
        this.num++;
        //更新组件内部的值
        this.$emit('input',this.num);
    },
  },
}), 'tmp', '自定义 title').value(100).props('disabled',false)

完整示例

高拍仪拍照SDK开发(良田影像S300L|S500L) - 磊哥|www.javacn.site - 博客园

mikel阅读(486)

来源: 高拍仪拍照SDK开发(良田影像S300L|S500L) – 磊哥|www.javacn.site – 博客园

高拍仪拍照SDK开发下载地址:点击下载

本SDK适用于:良田影像S300L|S500L

高拍仪如图:

SDN开发包安装之后找到安装目录,如图:

大家找到各自需要的版本即可,需要注意的是如果需要上传图片到服务器的话,我已经写好了C#版的接收程序,代码如下:

 

复制代码
<%@ WebHandler Language="C#" Class="FileUpload" %>

using System;
using System.Web;

public class FileUpload : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        context.Response.ContentType = "text/plain";
        HttpPostedFile uploadFile = context.Request.Files[0];
        string fileName = System.IO.Path.GetFileName(uploadFile.FileName);
        int fileSize = uploadFile.ContentLength;
        string fileExt = System.IO.Path.GetExtension(fileName).ToLower();
        string message = "";
        if (!(fileExt == ".png" || fileExt == ".gif" || fileExt == ".jpg" || fileExt == ".jpeg"))
        {
            message = "图片类型只能为gif,png,jpg,jpeg";
            return;
        }
        else
        {
            if (fileSize > (int)(500 * 1024))
            {
                message = "图片大小不能超过500KB";
                return;
            }
            else
            {
                try
                {
                    string directoryPath = context.Server.MapPath("~/TmpFile/");
                    if (!System.IO.Directory.Exists(directoryPath))//不存在这个文件夹就创建这个文件夹 
                    {
                        System.IO.Directory.CreateDirectory(context.Server.MapPath("~/TmpFile/"));
                    }
                    uploadFile.SaveAs(context.Server.MapPath("~/TmpFile/") + fileName);
                    message = fileName;
                }
                catch (Exception ex)
                {
                    message = ex.Message;
                    return;
                }
            }
        }
    }

    public bool IsReusable
    {
        get
        {
            return false;
        }
    }

}
复制代码

 

JavaScript调用上传服务器代码:

复制代码
function SetSelImgs() {
    var selImgs = ""; // 获取选中的图片
    for (var i = 0; i < EThumbnails.GetDisplayCount(); i++) {
        if (1 == EThumbnails.IsChecked(i)) {
            var fileName = EThumbnails.GetFilePath(i).replace("D:\\", "");
            ScanCtrl.UploadFileOfHTTP("http://xxx/FileUpload.ashx",
                EThumbnails.GetFilePath(i), fileName);
            selImgs += fileName + ",";
        }
    } document.getElementById("selImgs1").value = selImgs;
    if ("" == selImgs) {
        alert('请先选中上传图片!');
        return false;
    }
    return true;
}
复制代码

 

百度云下载地址:链接:https://pan.baidu.com/s/1wUkJow9Xn2VL2FXMz9itHQ 密码:nn0v

良田高拍仪集成vue项目 - 蝈民党 - 博客园

mikel阅读(843)

来源: 良田高拍仪集成vue项目 – 蝈民党 – 博客园

一、硬件及开发包说明:

产品型号为良田高拍仪S1800A3,集成b/s系统,适用现代浏览器,图片使用BASE64数据。开发包的bin文件下的video.flt文件需要和高拍仪型号的硬件id对应,这个可以找厂家要然后替换.

开发包地址:https://github.com/xufeikko/webIm/blob/master/%E8%B7%A8%E6%B5%8F%E8%A7%88%E5%99%A8%E6%8E%A7%E4%BB%B6SDK%20V2.3.0.rar

开发包文件目录:

demo文件:samples->JavaScript

qwebchannel.js文件的改写

 

二、控件运行原理

控件大部分功能在后台运行,前端通过websocket传输所选操作的信号或者获取来自后台程序的信息,并展示。控件必须在有网条件下才能正常运行,通过websocket可以访问同一局域网的高拍仪设备(前提是连接高拍仪的电脑有运行后台程序)。

(开包中也有夸浏览器控件sdk使用说明)

后台运行程序:

 

三、vue中使用

a.导入js文件

b.mounted内初始化函数,baseUrl路径不能更改(详情请看跨浏览器sdk使用文档)

c.创建websocket连接

四、测试

连接并拍照成功

注意:本开发需要更新替换bin目录下video.flt配置文件     github:https://github.com/xufeikko/webIm/blob/master/video.flt

RabbitMQ保姆级教程最佳实践 - 佛祖让我来巡山 - 博客园

mikel阅读(490)

来源: RabbitMQ保姆级教程最佳实践 – 佛祖让我来巡山 – 博客园

一、消息队列介绍

1、消息队列概念

1、MQ全称为Message Queue,消息队列(MQ)是⼀种应⽤程序对应⽤程序的通信⽅法。
应⽤程序通过读写出⼊队列的消息(针对应⽤程序的数据)来通信,⽽⽆需专⽤连接来
链接它们。
2、消息传递指的是程序之间通过在消息中发送数据进⾏通信,⽽不是通过直接调⽤彼此来
通信,直接调⽤通常是⽤于诸如远程过程调⽤的技术。

2、常⽤的消息队列产品

1、RabbitMQ 稳定可靠,数据⼀致,⽀持多协议,有消息确认,基于erlang语⾔
2、Kafka ⾼吞吐,⾼性能,快速持久化,⽆消息确认,⽆消息遗漏,可能会有有重复消息,依赖于zookeeper,成本⾼.
3、ActiveMQ 不够灵活轻巧,对队列较多情况⽀持不好.
4、RocketMQ 性能好,⾼吞吐,⾼可⽤性,⽀持⼤规模分布式,协议⽀持单⼀

⼆、RabbitMQ

1、RabbitMQ介绍

1、RabbitMQ是⼀个在AMQP基础上完成的,可复⽤的企业消息系统。他遵循MozillaPublic License开源协议。
2、AMQP,即Advanced Message Queuing Protocol, ⼀个提供统⼀消息服务的应⽤层标准
     ⾼级消息队列协议,是应⽤层协议的⼀个开放标准,为⾯向消息的中间件设计。基于此协议
     的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语⾔等
     条件的限制。Erlang中的实现有 RabbitMQ等。
3、主要特性:
  • 保证可靠性 :使⽤⼀些机制来保证可靠性,如持久化、传输确认、发布确认
  • 灵活的路由功能
  • 可伸缩性:⽀持消息集群,多台RabbitMQ服务器可以组成⼀个集群
  • ⾼可⽤性 :RabbitMQ集群中的某个节点出现问题时队列仍然可⽤
  • ⽀持多种协议
  • ⽀持多语⾔客户端
  • 提供良好的管理界⾯
  • 提供跟踪机制:如果消息出现异常,可以通过跟踪机制分析异常原因
  • 提供插件机制:可通过插件进⾏多⽅⾯扩展

2、RabbitMQ安装和配置

具体参考:https://note.youdao.com/s/MKn2Jr8c

3、RabbitMQ逻辑结构

三、RabbitMQ⽤户管理

RabbitMQ默认提供了⼀个guests账号,但是此账号不能⽤作远程登录,也就是不能在管理系统的登录;我们可以创建⼀个新的账号并授予响应的管理权限来实现远程登录

1、逻辑结构

⽤户
虚拟主机
队列

2、⽤户管理

2.1、命令⾏⽤户管理

1、在linux中使⽤命令⾏创建⽤户

## 进⼊到rabbit_mq的sbin⽬录
cd /usr/local/rabbitmq_server-3.7.0/sbin
## 新增⽤户
./rabbitmqctl add_user ytao admin123

2、设置⽤户级别

## ⽤户级别:
## 1.administrator 可以登录控制台、查看所有信息、可以对RabbitMQ进⾏管理
## 2.monitoring 监控者 登录控制台、查看所有信息
## 3.policymaker 策略制定者 登录控制台、指定策略
## 4.managment 普通管理员 登录控制台
./rabbitmqctl set_user_tags ytao administrator

2.2、管理系统进⾏⽤户管理

管理系统登录:访问http://localhost:15672/

四、RabbitMQ⼯作⽅式

RabbitMQ提供了多种消息的通信⽅式—⼯作模式  https://www.rabbitmq.com/getstarted.html
消息通信是由两个⻆⾊完成:消息⽣产者(producer)和 消息消费者(Consumer)

1、简单模式

⼀个队列只有⼀个消费者

2、⼯作模式

多个消费者监听同⼀个队列

3、订阅模式

⼀个交换机绑定多个消息队列,每个消息队列有⼀个消费者监听

4、路由模式

⼀个交换机绑定多个消息队列,每个消息队列都由⾃⼰唯⼀的key,每个消息队列有⼀个消费者监听

五、RabbitMQ交换机和队列管理

1、创建队列

2、创建交换机

3、交换机绑定队列

六、在普通的Maven应⽤中使⽤MQ

1、简单模式

1.1、消息⽣产者

1、创建Maven项⽬

2、添加RabbitMQ连接所需要的依赖

复制代码
<!-- https://mvnrepository.com/artifact/com.rabbitmq/amqp-client -->
<dependency>
 <groupId>com.rabbitmq</groupId>
 <artifactId>amqp-client</artifactId>
 <version>4.10.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-log4j12 -->
<dependency>
 <groupId>org.slf4j</groupId>
 <artifactId>slf4j-log4j12</artifactId>
 <version>1.7.25</version>
 <scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commonslang3 -->
<dependency>
 <groupId>org.apache.commons</groupId>
 <artifactId>commons-lang3</artifactId>
 <version>3.9</version>
</dependency>
复制代码

3、在resources⽬录下创建log4j.properties

log4j.rootLogger=DEBUG,A1 log4j.logger.com.taotao = DEBUG
log4j.logger.org.mybatis = DEBUG
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss,SSS} [%t] [%c]-[%p] %m%n

4、创建MQ连接工具类

复制代码
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ConnectionUtil {
    public static Connection getConnection() throws IOException,
    TimeoutException {
        //1.创建连接⼯⼚
        ConnectionFactory factory = new ConnectionFactory();
        //2.在⼯⼚对象中设置MQ的连接信息
        (ip,port,virtualhost,username,password)
         factory.setHost("47.96.11.185");
        factory.setPort(5672);
        factory.setVirtualHost("host1");
        factory.setUsername("ytao");
        factory.setPassword("admin123");
        //3.通过⼯⼚对象获取与MQ的链接
        Connection connection = factory.newConnection();
        return connection;
    }
}
复制代码

5、消息⽣产者发送消息

复制代码
import com.qfedu.mq.utils.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
public class SendMsg {
    public static void main(String[] args) throws Exception{
        String msg = "Hello HuangDaoJun!";
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        //定义队列(使⽤Java代码在MQ中新建⼀个队列)
        //参数1:定义的队列名称
        //参数2:队列中的数据是否持久化(如果选择了持久化)
        //参数3: 是否排外(当前队列是否为当前连接私有)
        //参数4:⾃动删除(当此队列的连接数为0时,此队列会销毁(⽆论队列中是否还有数据))
        //参数5:设置当前队列的参数
        //channel.queueDeclare("queue7",false,false,false,null);
        //参数1:交换机名称,如果直接发送信息到队列,则交换机名称为""
        //参数2:⽬标队列名称
        //参数3:设置当前这条消息的属性(设置过期时间 10)
        //参数4:消息的内容
        channel.basicPublish("","queue1",null,msg.getBytes());
        System.out.println("发送:" + msg);
        channel.close();
        connection.close();
    }
}
复制代码

1.2、消息消费者

1、创建Maven项⽬
2、添加依赖
3、log4j.properties
4、ConnetionUtil.java
5、消费者消费消息
复制代码
import com.qfedu.mq.utils.ConnectionUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ReceiveMsg {
    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        Consumer consumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                //body就是从队列中获取的数据
                String msg = new String(body);
                System.out.println("接收:"+msg);
            }
        };
        channel.basicConsume("queue1",true,consumer);
    }
}
复制代码

2、⼯作模式

⼀个发送者多个消费者

2.1、发送者

复制代码
public class SendMsg {
    public static void main(String[] args) throws Exception{
        System.out.println("请输⼊消息:");
        Scanner scanner = new Scanner(System.in);
        String msg = null;
        while(!"quit".equals(msg = scanner.nextLine())){
            Connection connection = ConnectionUtil.getConnection();
            Channel channel = connection.createChannel();
            channel.basicPublish("","queue2",null,msg.getBytes());
            System.out.println("发送:" + msg);
            channel.close();
            connection.close();
        }
    }
}
复制代码

2.2、消费者1

复制代码
public class ReceiveMsg {
    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        Consumer consumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                //body就是从队列中获取的数据
                String msg = new String(body);
                System.out.println("Consumer1接收:"+msg);
                if("wait".equals(msg)){
                    try {
                        Thread.sleep(10000);
                    }
                    catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        channel.basicConsume("queue2",true,consumer);
    }
}
复制代码

2.3、消费者2

复制代码
public class ReceiveMsg {
    public static void main(String[] args) throws IOException,
    TimeoutException {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        Consumer consumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                //body就是从队列中获取的数据
                String msg = new String(body);
                System.out.println("Consumer2接收:"+msg);
            }
        };
        channel.basicConsume("queue2",true,consumer);
    }
}
复制代码

3、订阅模式

1、发送者 发送消息到交换机

复制代码
public class SendMsg {
    public static void main(String[] args) throws Exception{
        System.out.println("请输⼊消息:");
        Scanner scanner = new Scanner(System.in);
        String msg = null;
        while(!"quit".equals(msg = scanner.nextLine())){
            Connection connection = ConnectionUtil.getConnection();
            Channel channel = connection.createChannel();
            channel.basicPublish("ex1","",null,msg.getBytes());
            System.out.println("发送:" + msg);
            channel.close();
            connection.close();
        }
    }
}
复制代码

2、消费者1

复制代码
public class ReceiveMsg1 {
    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        Consumer consumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                //body就是从队列中获取的数据
                String msg = new String(body);
                System.out.println("Consumer1接收:"+msg);
                if("wait".equals(msg)){
                    try {
                        Thread.sleep(10000);
                    }
                    catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        channel.basicConsume("queue3",true,consumer);
    }
}
复制代码

3、消费者2

复制代码
public class ReceiveMsg2 {
    public static void main(String[] args) throws IOException,
    TimeoutException {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        Consumer consumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                //body就是从队列中获取的数据
                String msg = new String(body);
                System.out.println("Consumer2接收:"+msg);
            }
        }
        ;
        channel.basicConsume("queue4",true,consumer);
    }
}
复制代码

4、路由模式

1、发送者 发送消息到交换机

复制代码
public class SendMsg {
    public static void main(String[] args) throws Exception{
        System.out.println("请输⼊消息:");
        Scanner scanner = new Scanner(System.in);
        String msg = null;
        while(!"quit".equals(msg = scanner.nextLine())){
            Connection connection = ConnectionUtil.getConnection();
            Channel channel = connection.createChannel();
            if(msg.startsWith("a")){
                channel.basicPublish("ex2","a",null,msg.getBytes());
            } else if(msg.startsWith("b")){
                channel.basicPublish("ex2","b",null,msg.getBytes());
            }
            System.out.println("发送:" + msg);
            channel.close();
            connection.close();
        }
    }
}
复制代码

2、消费者1

复制代码
public class ReceiveMsg1 {
    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        Consumer consumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                //body就是从队列中获取的数据
                String msg = new String(body);
                System.out.println("Consumer1接收:"+msg);
                if("wait".equals(msg)){
                    try {
                        Thread.sleep(10000);
                    }
                    catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        channel.basicConsume("queue5",true,consumer);
    }
}
复制代码

3、消费者2

复制代码
public class ReceiveMsg2 {
    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        Consumer consumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                //body就是从队列中获取的数据
                String msg = new String(body);
                System.out.println("Consumer2接收:"+msg);
            }
        };
        channel.basicConsume("queue6",true,consumer);
    }
}
复制代码

七、在SpringBoot应⽤中使⽤MQ

SpringBoot应⽤可以完成⾃动配置及依赖注⼊——可以通过Spring直接提供与MQ的连接对象

1、消息⽣产者

1、创建SpringBoot应⽤,添加依赖

2、配置application.yml

复制代码
server:
  port: 9001
spring:
  application:
    name: producer
  rabbitmq:
    host: 47.96.11.185
    port: 5672
    virtual-host: host1
    username: ytao
    password: admin123
复制代码

3、发送消息

复制代码
@Service
public class TestService {
    @Resource
    private AmqpTemplate amqpTemplate;
    public void sendMsg(String msg){
        //1. 发送消息到队列
        amqpTemplate.convertAndSend("queue1",msg);
        //2. 发送消息到交换机(订阅交换机)
        amqpTemplate.convertAndSend("ex1","",msg);
        //3. 发送消息到交换机(路由交换机)
        amqpTemplate.convertAndSend("ex2","a",msg);
    }
}
复制代码

2、消息消费者

1、创建项⽬添加依赖
2、配置yml
3、接收消息
复制代码
@Service
//@RabbitListener(queues = {"queue1","queue2"})
@RabbitListener(queues = "queue1")
public class ReceiveMsgService {
    @RabbitHandler
    public void receiveMsg(String msg){
        System.out.println("接收MSG:"+msg);
    }
}
复制代码

⼋、使⽤RabbitMQ传递对象

RabbitMQ是消息队列,发送和接收的都是字符串/字节数组类型的消息

1、使⽤序列化对象

要求:
传递的对象实现序列化接⼝
传递的对象的包名、类名、属性名必须⼀致

 

1、消息提供者

复制代码
@Service
public class MQService {
    @Resource
    private AmqpTemplate amqpTemplate;
    public void sendGoodsToMq(Goods goods){
        //消息队列可以发送 字符串、字节数组、序列化对象
        amqpTemplate.convertAndSend("","queue1",goods);
    }
}
复制代码

2、消息消费者

复制代码
@Component
@RabbitListener(queues = "queue1")
public class ReceiveService {
    @RabbitHandler
    public void receiveMsg(Goods goods){
        System.out.println("Goods---"+goods);
    }
}
复制代码

2、使⽤序列化字节数组

要求:
  传递的对象实现序列化接⼝
  传递的对象的包名、类名、属性名必须⼀致

 

1、消息提供者

复制代码
@Service
public class MQService {
    @Resource
    private AmqpTemplate amqpTemplate;
    public void sendGoodsToMq(Goods goods){
        //消息队列可以发送 字符串、字节数组、序列化对象
        byte[] bytes = SerializationUtils.serialize(goods);
        amqpTemplate.convertAndSend("","queue1",bytes);
    }
}
复制代码

2、消息消费者

复制代码
@Component
@RabbitListener(queues = "queue1")
public class ReceiveService {
    @RabbitHandler
    public void receiveMsg(byte[] bs){
        Goods goods = (Goods) SerializationUtils.deserialize(bs);
        System.out.println("byte[]---"+goods);
    }
}
复制代码

3、使⽤JSON字符串传递

要求:对象的属性名⼀直

 

1、消息提供者

复制代码
@Service
public class MQService {
    @Resource
    private AmqpTemplate amqpTemplate;
    public void sendGoodsToMq(Goods goods) throws JsonProcessingException {
        //消息队列可以发送 字符串、字节数组、序列化对象
        ObjectMapper objectMapper = new ObjectMapper();
        String msg = objectMapper.writeValueAsString(goods);
        amqpTemplate.convertAndSend("","queue1",msg);
    }
}
复制代码

2、消息消费者

复制代码
@Component
@RabbitListener(queues = "queue1")
public class ReceiveService {
    @RabbitHandler
    public void receiveMsg(String msg) throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        Goods goods = objectMapper.readValue(msg,Goods.class);
        System.out.println("String---"+msg);
    }
}
复制代码

九、基于Java的交换机与队列创建

我们使⽤消息队列,消息队列和交换机可以通过管理系统完成创建,也可以在应⽤程序中通过Java代码来完成创建

1、普通Maven项⽬交换机及队列创建

1、使⽤Java代码新建队列

复制代码
//1.定义队列 (使⽤Java代码在MQ中新建⼀个队列)
//参数1:定义的队列名称
//参数2:队列中的数据是否持久化(如果选择了持久化)
//参数3: 是否排外(当前队列是否为当前连接私有)
//参数4:⾃动删除(当此队列的连接数为0时,此队列会销毁(⽆论队列中是否还有数据))
//参数5:设置当前队列的参数
channel.queueDeclare("queue7",false,false,false,null);
复制代码

2、新建交换机

//定义⼀个“订阅交换机”
channel.exchangeDeclare("ex3", BuiltinExchangeType.FANOUT);
//定义⼀个“路由交换机”
channel.exchangeDeclare("ex4", BuiltinExchangeType.DIRECT);

3、绑定队列到交换机

//绑定队列
//参数1:队列名称
//参数2:⽬标交换机
//参数3:如果绑定订阅交换机参数为"",如果绑定路由交换机则表示设置队列的key
channel.queueBind("queue7","ex4","k1");
channel.queueBind("queue8","ex4","k2");

2、SpringBoot应⽤中通过配置完成队列的创建

复制代码
@Configuration
public class RabbitMQConfiguration {
    //声明队列
    @Bean
    public Queue queue9(){
        Queue queue9 = new Queue("queue9");
        //设置队列属性
        return queue9;
    }
    @Bean
    public Queue queue10(){
        Queue queue10 = new Queue("queue10");
        //设置队列属性
        return queue10;
    }
    //声明订阅模式交换机
    @Bean
    public FanoutExchange ex5(){
        return new FanoutExchange("ex5");
    }
    //声明路由模式交换机
    @Bean
    public DirectExchange ex6(){
        return new DirectExchange("ex6");
    }
    //绑定队列
    @Bean
    public Binding bindingQueue9(Queue queue9, DirectExchange ex6){
        return BindingBuilder.bind(queue9).to(ex6).with("k1");
    }
    @Bean
    public Binding bindingQueue10(Queue queue10, DirectExchange ex6){
        return BindingBuilder.bind(queue10).to(ex6).with("k2");
    }
}
复制代码

⼗、消息的可靠性

消息的可靠性:从 ⽣产者发送消息 —— 消息队列存储消息 —— 消费者消费消息 的整个过程中消息的安全性及可控性。
  • ⽣产者
  • 消息队列
  • 消费者

1、RabbitMQ事务

RabbitMQ事务指的是基于客户端实现的事务管理,当在消息发送过程中添加了事务,处理效率降低⼏⼗倍甚⾄上百倍
复制代码
Connection connection = RabbitMQUtil.getConnection(); //connection 表示与 host1的连接
Channel channel = connection.createChannel();
channel.txSelect();//开启事务
try{
    channel.basicPublish("ex4", "k1", null, msg.getBytes());
    channel.txCommit();//提交事务
}
catch (Exception e){
    channel.txRollback();//事务回滚
}
finally{
    channel.close();
    connection.close();
}
复制代码

2、RabbitMQ消息确认和return机制

1、消息确认机制:确认消息提供者是否成功发送消息到交换机
2、return机制:确认消息是否成功的从交换机分发到队列

2.1、普通Maven项⽬的消息确认

1、普通confirm⽅式

//1.发送消息之前开启消息确认
channel.confirmSelect();
channel.basicPublish("ex1", "a", null, msg.getBytes());
//2.接收消息确认
Boolean b = channel.waitForConfirms();
System.out.println("发送:" +(b?"成功":"失败"));

2、批量confirm⽅式

复制代码
//1.发送消息之前开启消息确认
channel.confirmSelect();
//2.批量发送消息
for (int i=0 ; i<10 ; i++){
    channel.basicPublish("ex1", "a", null, msg.getBytes());
}
//3.接收批量消息确认:发送的所有消息中,如果有⼀条是失败的,则所有消息发送直接失败,抛出IO异常
Boolean b = channel.waitForConfirms();
复制代码

3、异步confirm⽅式

复制代码
//发送消息之前开启消息确认
channel.confirmSelect();
//批量发送消息
for (int i=0 ; i<10 ; i++){
    channel.basicPublish("ex1", "a", null, msg.getBytes());
}
//假如发送消息需要10s,waitForConfirms会进⼊阻塞状态
//boolean b = channel.waitForConfirms();
//使⽤监听器异步confirm
channel.addConfirmListener(new ConfirmListener() {
    //参数1: long l 返回消息的表示
    //参数2: boolean b 是否为批量confirm
    public void handleAck(long l, Boolean b) throws IOException {
        System.out.println("~~~~~消息成功发送到交换机");
    }
    public void handleNack(long l, Boolean b) throws IOException {
        System.out.println("~~~~~消息发送到交换机失败");
    }
}
);
复制代码

2.2、普通Maven项⽬的return机制

1、添加return监听器
2、发送消息是指定第三个参数为true
3、由于监听器监听是异步处理,所以在消息发送之后不能关闭channel
复制代码
String msg = "Hello HuangDaoJun!";
Connection connection = ConnectionUtil.getConnection();
//相当于JDBC操作的数据库连接
Channel channel = connection.createChannel();
//相当于JDBC操作的statement
//return机制:监控交换机是否将消息分发到队列
channel.addReturnListener(new ReturnListener() {
    public void handleReturn(int i, String s, String s1, String s2,AMQP.BasicProperties basicProperties,byte[] bytes) throws IOException {
        //如果交换机分发消息到队列失败,则会执⾏此⽅法(⽤来处理交换机分发消息到队列失败的情况)
        System.out.println("*****"+i);//标识
        System.out.println("*****"+s);//
        System.out.println("*****"+s1);//交换机名
        System.out.println("*****"+s2);//交换机对应的队列的key
        System.out.println("*****"+new String(bytes));//发送的消息
    }
}
);
//发送消息
//channel.basicPublish("ex2", "c", null, msg.getBytes());
channel.basicPublish("ex2", "c", true, null, msg.getBytes());
复制代码

2.3、在SpringBoot应⽤实现消息确认与return监听

1、配置application.yml,开启消息确认和return监听

spring:
 rabbitmq:
    publisher-confirm-type: simple ## 开启消息确认模式
    publisher-returns: true ##使⽤return监听机制

2、创建confirm和return监听

2.1、消息确认

复制代码
@Component
public class MyConfirmListener implements
RabbitTemplate.ConfirmCallback {
    @Autowired
    private AmqpTemplate amqpTemplate;
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @PostConstruct
    public void init(){
        rabbitTemplate.setConfirmCallback(this);
    }
    @Override
    public void confirm(CorrelationData correlationData, Boolean b, String s) {
        //参数b 表示消息确认结果
        //参数s 表示发送的消息
        if(b){
            System.out.println("消息发送到交换机成功!");
        } else{
            System.out.println("消息发送到交换机失败!");
            amqpTemplate.convertAndSend("ex4","",s);
        }
    }
}
复制代码

2.2、return机制

复制代码
@Component
public class MyReturnListener implements RabbitTemplate.ReturnsCallback
{
    @Autowired
    private AmqpTemplate amqpTemplate;
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @PostConstruct
    public void init(){
        rabbitTemplate.setReturnsCallback(this);
    }
    @Override
    public void returnedMessage(ReturnedMessage returnedMessage) {
        System.out.println("消息从交换机分发到队列失败");
        String exchange = returnedMessage.getExchange();
        String routingKey = returnedMessage.getRoutingKey();
        String msg = returnedMessage.getMessage().toString();
        amqpTemplate.convertAndSend(exchange,routingKey,msg);
    }
}
复制代码

3、RabbitMQ消费者⼿动应答

复制代码
@Component
@RabbitListener(queues="queue01")
public class Consumer1 {
    @RabbitHandler
    public void process(String msg,Channel channel, Message message) throws IOException {
        try {
            System.out.println("get msg1 success msg = "+msg);
            /**
         * 确认⼀条消息:<br>
         * channel.basicAck(deliveryTag, false); <br>
         * deliveryTag:该消息的index <br>
         * multiple:是否批量.true:将⼀次性ack所有⼩于deliveryTag的消息 <br>
       */
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            //消费者处理出了问题,需要告诉队列信息消费失败
            /**
         * 拒绝确认消息:<br>
         * channel.basicNack(long deliveryTag, boolean multiple, boolean requeue) ; <br>
         * deliveryTag:该消息的index<br>
         * multiple:是否批量.true:将⼀次性拒绝所有⼩于deliveryTag的消息。<br>
         * requeue:被拒绝的是否重新⼊队列 <br>
       */
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
            System.err.println("get msg1 failed msg = "+msg);
        }
    }
}
复制代码

4、消息消费的幂等性问题

消息消费的幂等性——多次消费的执⾏结果时相同的 (避免重复消费)
解决⽅案:处理成功的消息setnx到redis

⼗⼀、延迟机制

1、延迟队列

1、延迟队列——消息进⼊到队列之后,延迟指定的时间才能被消费者消费
2、AMQP协议和RabbitMQ队列本身是不⽀持延迟队列功能的,但是可以通过TTL(Time To Live)特性模拟延迟队列的功能
3、TTL就是消息的存活时间。RabbitMQ可以分别对队列和消息设置存活时间

1、在创建队列的时候可以设置队列的存活时间,当消息进⼊到队列并且在存活时间内没有消费者消费,则此消息就会从当前队列被移除;
2、创建消息队列没有设置TTL,但是消息设置了TTL,那么当消息的存活时间结束,也会被移除;
3、当TTL结束之后,我们可以指定将当前队列的消息转存到其他指定的队列

2、使⽤延迟队列实现订单⽀付监控

1、实现流程图

2、创建交换机和队列

⼗⼆、消息队列作⽤/使⽤场景总结

1、解耦

场景说明:⽤户下单之后,订单系统要通知库存系统

2、异步

场景说明:⽤户注册成功之后,需要发送注册邮件及注册短信提醒

3、消息通信

场景说明:应⽤系统之间的通信,例如聊天室

4、流量削峰

场景说明:秒杀业务

5、⽇志处理

场景说明:系统中⼤量的⽇志处理