tnblog
首页
视频
资源
登录

.net6 Signalr+Vue3 的运用(上)

13175人阅读 2023/1/31 17:37 总访问:3658060 评论:0 收藏:1 手机
分类: .net后台框架

.netcore

.net6 Signalr+Vue3 的运用(上)

什么是 SignalR?


ASP.NET Core SignalR 是一个开放源代码库,可用于简化向应用添加实时 Web 功能。 实时 Web 功能使服务器端代码能够将内容推送到客户端。

什么是 Hubs?


SignalR 使用Hubs在客户端和服务器之间通信。
Hub 是一种高级管道,允许客户端和服务器相互调用方法。
SignalR 自动处理跨计算机边界的调度,并允许客户端调用服务器上的方法,反之亦然。
可以将强类型参数传递给方法,从而支持模型绑定。
SignalR 提供两种内置中心协议:基于 JSON 的文本协议和基于 MessagePack 的二进制协议。
也叫做集线器。

SignalR后端项目

创建项目

创建signalrtest项目,我在创建时选择了OpenAPI,没有选择Https。

创建强类型Hubs


这样做的好处是:方便客户端调用服务器的方法时,拼音不会报错。
接下来我们在HubClients目录下定义客户端的调用方法的接口IChatClient,并且只定义SendAll方法。

  1. public interface IChatClient
  2. {
  3. Task SendAll(object message);
  4. }


然后我们在Hubs的目录下创建ChatHub集线器。
并定义了一个SendMessage的方法向所有的用户发送消息,并对客户端连接和断开状态做了一个日志记录。

  1. public class ChatHub : Hub<IChatClient>
  2. {
  3. ILogger<ChatHub> _logger;
  4. public ChatHub(ILogger<ChatHub> logger, CommonService common)
  5. {
  6. _logger = logger;
  7. _common = common;
  8. }
  9. readonly CommonService _common;
  10. /// <summary>
  11. /// 客户端连接服务端
  12. /// </summary>
  13. /// <returns></returns>
  14. public override Task OnConnectedAsync()
  15. {
  16. var id = Context.ConnectionId;
  17. _logger.LogInformation($"Client ConnectionId=> [[{id}]] Already Connection Server!");
  18. return base.OnConnectedAsync();
  19. }
  20. /// <summary>
  21. /// 客户端断开连接
  22. /// </summary>
  23. /// <param name="exception"></param>
  24. /// <returns></returns>
  25. public override Task OnDisconnectedAsync(Exception exception)
  26. {
  27. var id = Context.ConnectionId;
  28. _logger.LogInformation($"Client ConnectionId=> [[{id}]] Already Close Connection Server!");
  29. return base.OnDisconnectedAsync(exception);
  30. }
  31. /**
  32. * 测试
  33. * */
  34. /// <summary>
  35. /// 给所有客户端发送消息
  36. /// </summary>
  37. /// <returns></returns>
  38. public async Task SendMessage(string data)
  39. {
  40. Console.WriteLine("Have one Data!");
  41. await Clients.All.SendAll(_common.SendAll(data));
  42. }
  43. }


这里有一个CommonService它定义在HubService的目录下面,里面只有一个SendAll方法,该方法只是在原有的消息基础上添加Hello和随机数。
内容如下所示:

  1. public class CommonService
  2. {
  3. internal object SendAll(string data)
  4. {
  5. return $"Hello {new Random().Next(0, 100)} {data} ";
  6. }
  7. }

配置SignalR


我们可以通过AddSignalR方法来注册SignalR相关服务,并通过AddJsonProtocol启用SignalR 的 JSON 协议。
PayloadSerializerOptions是一个System.Text.JsonJsonSerializerOptions对象,PropertyNamingPolicy属性为null表示保持属性名称不变(是否区分大小写,无所谓)。

  1. builder.Services
  2. .AddSignalR()
  3. .AddJsonProtocol(options => {
  4. options.PayloadSerializerOptions.PropertyNamingPolicy = null;
  5. })
  6. ;
  7. builder.Services.TryAddSingleton(typeof(CommonService));

如果想使用NewtonsoftJson,可以将AddJsonProtocol方法改为AddNewtonsoftJsonProtocol


然后我们通过MapHub方法,加载路由路径/ChatHubChatHub处理,并设置传输的方式可以使用WebSocketsLongPolling

  1. app.UseDefaultFiles();
  2. app.UseStaticFiles();
  3. app.MapHub<ChatHub>("/ChatHub", options =>
  4. {
  5. options.Transports =
  6. HttpTransportType.WebSockets |
  7. HttpTransportType.LongPolling;
  8. });

SignalR前端地址项目


首先我们创建一个Vue3.0的项目,并安装@aspnet/signalr包。

  1. vue create signalrtestvue
  2. cd signalrtestvue
  3. npm install @aspnet/signalr

目前aspnet/signalr包已经弃用了,推荐使用@microsoft/signalr包,更多请参考


然后我们在src/utils目录下编写signalR.js文件。
请修改你本地的signalr服务器的连接地址。

  1. import * as signalR from '@aspnet/signalr'
  2. const url = "http://localhost:5102/ChatHub"
  3. const signal = new signalR.HubConnectionBuilder()
  4. .withUrl(url, {
  5. skipNegotiation: true,
  6. transport: signalR.HttpTransportType.WebSockets
  7. })
  8. .configureLogging(signalR.LogLevel.Information)
  9. .build()
  10. signal.on('SendAll', (res) => {
  11. console.log(res, '收到消息')
  12. })
  13. signal.start().then(() => {
  14. if (window.Notification) {
  15. if (Notification.permission === 'granted') {
  16. console.log('允许通知')
  17. } else if (Notification.permission !== 'denied') {
  18. console.log('需要通知权限')
  19. Notification.requestPermission((permission) => { console.log("权限通知",permission) })
  20. } else if (Notification.permission === 'denied') {
  21. console.log('拒绝通知')
  22. }
  23. } else {
  24. console.error('浏览器不支持Notification')
  25. }
  26. console.log('连接成功')
  27. })
  28. signal.onclose((err)=>{
  29. console.log("连接已经断开 执行函数onclose",err)
  30. })
  31. export default {
  32. signal
  33. }


通过HubConnectionBuilder来连接到我们的Hubs。
withUrl设置连接地址。
configureLogging设置日志记录的级别。
接下来讲将相关的事件方法。

事件名 描述
on 接收服务器返回的方法进行处理。
start 启动Hubs的服务器连接。
onclose 服务器关闭触发的事件回调。
stop 关闭Hubs的服务器连接。
invoke 调用服务器的方法。


然后我们在main.js下进行全局启用signalr

  1. import { createApp } from 'vue'
  2. import App from './App.vue'
  3. import signalr from './utils/signalR'
  4. const app = createApp(App)
  5. app.config.globalProperties.$signalr = signalr.signal;
  6. app.mount('#app')


components目录下找到HellloWorld.vue并添加一个按钮向服务器端的SendMessage发送消息。

  1. <template>
  2. <div class="hello">
  3. <h1>{{ msg }}</h1>
  4. <button @click="onClickButton" >获取</button>
  5. </div>
  6. </template>
  7. <script>
  8. export default {
  9. name: 'HelloWorld',
  10. props: {
  11. msg: String
  12. },
  13. data() {
  14. return {
  15. };
  16. },
  17. methods: {
  18. onClickButton() {
  19. console.log(this.$signalr)
  20. this.$signalr
  21. .invoke('SendMessage', "hmy")
  22. .catch(function(err) {return console.error(err) })
  23. }
  24. }
  25. }
  26. </script>
  27. <!-- Add "scoped" attribute to limit CSS to this component only -->
  28. <style scoped>
  29. h3 {
  30. margin: 40px 0 0;
  31. }
  32. ul {
  33. list-style-type: none;
  34. padding: 0;
  35. }
  36. li {
  37. display: inline-block;
  38. margin: 0 10px;
  39. }
  40. a {
  41. color: #42b983;
  42. }
  43. </style>

运行测试

  1. npm run serve


通过点击获取按钮来触发事件。

由服务器端发向客户端


我们这里添加一个ClientHubController控制器并通过请求发送SendMessage消息。

  1. [ApiController]
  2. [Route("[controller]")]
  3. public class ClientHubController : ControllerBase
  4. {
  5. private readonly ILogger<ClientHubController> _logger;
  6. public ClientHubController(
  7. ILogger<ClientHubController> logger
  8. )
  9. {
  10. _logger = logger;
  11. }
  12. [HttpGet(Name = "SendMessage")]
  13. public async Task SendMessage(string date, [FromServices] IHubContext<ChatHub, IChatClient> hubContext)
  14. {
  15. await hubContext.Clients.All.SendAll(date);
  16. }
  17. }

这里我使用的是强类型IHubContext<,>,如果是弱类型直接IHubContext也是可以的。


运行测试。


如果我们希望在第一个页面点击获取按钮,并返回一个发送消息的结果,我们可以通过Clients.Caller来将消息返回给调用方。
首先在CommonService中创建一个发送给调用方的消息。

  1. internal object SendCaller() => "Send Successful!";


再在SendMessage中进行调用。

  1. public async Task SendMessage(string data)
  2. {
  3. Console.WriteLine("Have one Data!");
  4. await Clients.All.SendAll(_common.SendAll(data));
  5. await Clients.Caller.SendAll(_common.SendCaller(data));
  6. }

指定客户端发送消息


如果我们想给指定的客户端发送消息,首先我们需要获取所有连接服务器的ID,这里我做一个简易的集合进行存储,并且在OnDisconnectedAsyncOnConnectedAsync事件中进行增加与删除。

  1. public static class UserIdsStore
  2. {
  3. static HashSet<string> Ids = new HashSet<string>();
  4. }
  1. /// <summary>
  2. /// 客户端连接服务端
  3. /// </summary>
  4. /// <returns></returns>
  5. public override Task OnConnectedAsync()
  6. {
  7. var id = Context.ConnectionId;
  8. // 添加用户ID
  9. UserIdsStore.Ids.Add(id);
  10. _logger.LogInformation($"Client ConnectionId=> [[{id}]] Already Connection Server!");
  11. return base.OnConnectedAsync();
  12. }
  13. /// <summary>
  14. /// 客户端断开连接
  15. /// </summary>
  16. /// <param name="exception"></param>
  17. /// <returns></returns>
  18. public override Task OnDisconnectedAsync(Exception exception)
  19. {
  20. var id = Context.ConnectionId;
  21. // 删除用户ID
  22. UserIdsStore.Ids.Remove(id);
  23. _logger.LogInformation($"Client ConnectionId=> [[{id}]] Already Close Connection Server!");
  24. return base.OnDisconnectedAsync(exception);
  25. }


然后我们可以在IChatClient中添加SendCustomUserMessage接口方便客户端接收。

  1. Task SendCustomUserMessage(object message);


ClientHubController中添加两个接口。

  1. /// <summary>
  2. /// 获取所有的用户
  3. /// </summary>
  4. /// <returns></returns>
  5. [HttpGet("GetAllUserIds", Name = "GetAllUserIds")]
  6. public string[] GetAllUserIds()
  7. {
  8. return UserIdsStore.Ids.ToArray();
  9. }
  10. /// <summary>
  11. /// 发送指定的消息给指定的客户端
  12. /// </summary>
  13. /// <param name="userid"></param>
  14. /// <param name="date"></param>
  15. /// <param name="hubContext"></param>
  16. /// <returns></returns>
  17. [HttpGet("SendCustomUserMessage", Name = "SendCustomUserMessage")]
  18. public async Task<IActionResult> SendCustomUserMessage(
  19. string userid,
  20. string date,
  21. [FromServices] IHubContext<ChatHub, IChatClient> hubContext
  22. )
  23. {
  24. await hubContext.Clients.Client(userid).SendCustomUserMessage(date);
  25. return Ok("Send Successful!");
  26. }


最后我们需要在我们的前端客户端中的SignalR.js文件,创建SendCustomUserMessage事件的处理。

  1. signal.on('SendCustomUserMessage', (res) => {
  2. console.log(res, '收到消息')
  3. })


接下来运行项目,重新启动一下前端项目进行测试。
首先获取一下所有的ID。


然后我们只取其中的ID调用SendCustomUserMessage接口来进行发送消息。

授权访问服务器


在我们将SignalR的用户之前,首先需要授权,这里我们可以搞一个JWT的方便。
首先安装Microsoft.AspNetCore.Authentication.JwtBearer


添加MyJWTBearer类,自定义我们的JWT的Token生成。

  1. public static class MyJWTBearer
  2. {
  3. public static readonly SymmetricSecurityKey SecurityKey = new SymmetricSecurityKey(Guid.NewGuid().ToByteArray());
  4. public static readonly JwtSecurityTokenHandler JwtTokenHandler = new JwtSecurityTokenHandler();
  5. public static string GenerateToken(HttpContext httpContext)
  6. {
  7. // 请求时传入的用户参数为NameIdentifier claim的值
  8. var claims = new[] { new Claim(ClaimTypes.NameIdentifier, httpContext.Request.Query["user"]) };
  9. // 签名凭据
  10. var credentials = new SigningCredentials(SecurityKey, SecurityAlgorithms.HmacSha256);
  11. // 生成JWT Token
  12. var token = new JwtSecurityToken("SignalRTestServer", "SignalRTests", claims, expires: DateTime.UtcNow.AddSeconds(60), signingCredentials: credentials);
  13. return JwtTokenHandler.WriteToken(token);
  14. }
  15. public static void AddMyJWTBearerAuth(this IServiceCollection services)
  16. {
  17. // 添加自定义授权
  18. services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
  19. .AddJwtBearer(options =>
  20. {
  21. options.TokenValidationParameters =
  22. new TokenValidationParameters
  23. {
  24. LifetimeValidator = (before, expires, token, parameters) => expires > DateTime.UtcNow,
  25. ValidateAudience = false,
  26. ValidateIssuer = false,
  27. ValidateActor = false,
  28. ValidateLifetime = true,
  29. IssuerSigningKey = MyJWTBearer.SecurityKey
  30. };
  31. options.Events = new JwtBearerEvents
  32. {
  33. OnMessageReceived = context =>
  34. {
  35. // 当我们收到消息时,去获取请求中的access_token字段
  36. var accessToken = context.Request.Query["access_token"];
  37. // 如果没有就去头上找,找到了就放入我们context.token中
  38. if (!string.IsNullOrEmpty(accessToken))
  39. {
  40. context.Token = accessToken;
  41. }
  42. return Task.CompletedTask;
  43. }
  44. };
  45. });
  46. }
  47. }

这里在生成的时候多了一个NameIdentifierClaim Type的类型,这个类型是当我们发消息调用User()方法时需要验证的时候需要的,当然后续我们可以自定义其中的逻辑进行判断。(待会再讲)


接下来我们配置一下自定义的授权。

  1. // 添加授权服务
  2. builder.Services.AddMyJWTBearerAuth();
  3. ...
  4. app.UseAuthentication();
  5. app.UseAuthorization();
  6. // 授权路径
  7. app.MapGet("generatetoken", c => c.Response.WriteAsync(MyJWTBearer.GenerateToken(c)));


测试一下
http://localhost:5102/generatetoken?user=bob


接下来我们修改一下前端项目,我们想在连接Signalr调用前先调用Token,再使用Token进行连接我们的服务器。
所以首先安装一下axios

  1. npm install axios


然后我们需要修改一下main.js,删除以前自动连接的signalR.js的引用,并且的添加axios的引用。

  1. import { createApp } from 'vue'
  2. import App from './App.vue'
  3. import axios from 'axios'
  4. const app = createApp(App)
  5. axios.defaults.baseURL = "http://localhost:5102"
  6. app.config.globalProperties.$http = axios;
  7. app.mount('#app')


然后修改HelloWorld.vue页面,在填写好用户名后登录。

  1. <template>
  2. <div class="hello">
  3. <h1>{{ msg }}</h1>
  4. <div>
  5. UserName: <input type="text" v-model="username" >
  6. <button @click="onConnectionClickButton" >连接</button>
  7. </div>
  8. <div>
  9. Message: <input type="text" v-model="message" >
  10. <button @click="onClickButton" >发送</button>
  11. </div>
  12. </div>
  13. </template>
  14. <script>
  15. import * as signalR from '@aspnet/signalr'
  16. const url = "http://localhost:5102/ChatHub"
  17. const signal = new signalR.HubConnectionBuilder()
  18. .withUrl(url, {
  19. skipNegotiation: true,
  20. transport: signalR.HttpTransportType.WebSockets,
  21. accessTokenFactory: () => ""
  22. })
  23. .configureLogging(signalR.LogLevel.Information)
  24. .build()
  25. signal.on('SendCustomUserMessage', (res) => {
  26. console.log(res, '收到消息')
  27. })
  28. signal.on('SendAll', (res) => {
  29. console.log(res, '收到消息')
  30. })
  31. export default {
  32. name: 'HelloWorld',
  33. props: {
  34. msg: String
  35. },
  36. data() {
  37. return {
  38. username: "",
  39. message: ""
  40. }
  41. },
  42. methods: {
  43. onClickButton() {
  44. var e = this
  45. signal
  46. .invoke('SendMessage', e.message)
  47. .catch(function(err) {return console.error(err) })
  48. },
  49. async onConnectionClickButton() {
  50. // 首先我们去获取Token
  51. let name = this.username
  52. let result = await this.$http.get(`generatetoken?user=${name}`)
  53. if (result.status !== 200) {
  54. console.error("Token 请求失败")
  55. return
  56. }
  57. var token = result.data
  58. console.log("获得Token",token)
  59. // 放入token
  60. signal.connection.options.accessTokenFactory = () => token
  61. // 然后我们请求连接signalr
  62. signal.start().then(() => {
  63. console.log('连接成功')
  64. })
  65. },
  66. }
  67. }
  68. </script>
  69. <!-- Add "scoped" attribute to limit CSS to this component only -->
  70. <style scoped>
  71. h3 {
  72. margin: 40px 0 0;
  73. }
  74. ul {
  75. list-style-type: none;
  76. padding: 0;
  77. }
  78. li {
  79. display: inline-block;
  80. margin: 0 10px;
  81. }
  82. a {
  83. color: #42b983;
  84. }
  85. </style>

跨域问题


这里涉及到跨域问题。

  1. string MyAllowSpecificOrigins = "_signalrtestcores";
  2. builder.Services.AddCors(options =>
  3. {
  4. options.AddPolicy(MyAllowSpecificOrigins,
  5. builder => builder.AllowAnyOrigin()
  6. .AllowAnyHeader()
  7. .WithMethods("GET", "POST", "HEAD", "PUT", "DELETE", "OPTIONS")
  8. )
  9. ;
  10. });
  11. ...
  12. app.UseCors(MyAllowSpecificOrigins);


然后再为ChatHub添加授权特性Authorize,表示访问该资源需要进行授权。

  1. [Authorize]
  2. public class ChatHub : Hub<IChatClient>


接下来我们运行测试一下,输入用户名后,点击连接,同时也是可以发送消息的。


可以看到我们已经成功的进行了授权。

断开后重连


我们与Signalr服务器断开连接后,我们希望进行重新连接,不用每次都刷新页面。可以在onclose事件里面进行设置。

  1. <script>
  2. import * as signalR from '@aspnet/signalr'
  3. const url = "http://localhost:5102/ChatHub"
  4. const signal = new signalR.HubConnectionBuilder()
  5. .withUrl(url, {
  6. skipNegotiation: true,
  7. transport: signalR.HttpTransportType.WebSockets,
  8. accessTokenFactory: () => ""
  9. })
  10. .configureLogging(signalR.LogLevel.Information)
  11. .build()
  12. signal.on('SendCustomUserMessage', (res) => {
  13. console.log(res, '收到消息')
  14. })
  15. signal.on('SendAll', (res) => {
  16. console.log(res, '收到消息')
  17. })
  18. export default {
  19. name: 'HelloWorld',
  20. props: {
  21. msg: String
  22. },
  23. data() {
  24. return {
  25. username: "bob",
  26. message: "",
  27. timer: null,
  28. connectionstatus: "init"
  29. }
  30. },
  31. mouted() {
  32. this.timer = setInterval(()=>{},500)
  33. },
  34. destoryed() {
  35. this.clearInterval(this.timer)
  36. },
  37. methods: {
  38. onClickButton() {
  39. var e = this
  40. signal
  41. .invoke('SendMessage', e.message)
  42. .catch(function(err) {return console.error(err) })
  43. },
  44. async onConnectionClickButton() {
  45. try{
  46. // 首先我们去获取Token
  47. let name = this.username
  48. let result = await this.$http.get(`generatetoken?user=${name}`)
  49. if (result.status !== 200) {
  50. console.error("Token 请求失败")
  51. return
  52. }
  53. var token = result.data
  54. console.log("获得Token",token)
  55. var e = this
  56. // onClose的定义
  57. if (e.connectionstatus == "init") {
  58. signal.onclose(() => {
  59. e.connectionstatus = "close"
  60. signal.stop()
  61. console.log("连接已关闭")
  62. e.retryConnection()
  63. });
  64. }
  65. // 放入token
  66. signal.connection.options.accessTokenFactory = () => token
  67. // 然后我们请求连接signalr
  68. signal.start().then(() => {
  69. if (e.connectionstatus == "close") {
  70. clearInterval(e.timer)
  71. }
  72. e.connectionstatus = "start"
  73. console.log('连接成功')
  74. })
  75. }catch(e){
  76. if (e.code == "ERR_NETWORK") {
  77. console.log("Token 请求失败")
  78. }
  79. }
  80. },
  81. retryConnection() {
  82. var e = this
  83. if (this.connectionstatus == "init" || this.connectionstatus == "start") {
  84. return
  85. }else if(this.connectionstatus == "close"){
  86. console.log("正在重试连接...")
  87. this.timer = setInterval(()=>{
  88. e.onConnectionClickButton()
  89. },10000)
  90. return
  91. }
  92. }
  93. }
  94. }
  95. </script>


这样就不用担心服务器挂了还是没挂了。


欢迎加群讨论技术,1群:677373950(满了,可以加,但通过不了),2群:656732739

评价

一点flex布局的运用

水平分割:html:&lt;divclass=&quot;flex-container&quot;&gt; &lt;divclass=&quot;flex-item&quot;&gt;flexitem1&lt;/div...

c 锁的运用

锁的一般分类:乐观锁、悲观锁、共享锁、排它锁、互斥锁锁的对象为什么要是私有的只读的因为防止被修改C#中Monitor和Lock以...

.net json序列化匿名类型jobject的运用

在反序列化的时候我们可以不用每次都添加一次实体,如果只是简单临时的用一下可以使用jobject即可解析方法一般有两种方法一...

grpc 在vs中的运用与proto同步

思 考 问 题 grpc 服务器当对 proto 文件进行修改时每次都去 Copy 到本地 Client 是比较麻烦的,有没有快捷的方法解决?Gr...

Weave Scope 的运用

Weave Scope 的运用[TOC] Weave Scope 的介绍 简介 Weave Scope 用于监控、可视化和管理 Docker 以及 Kubernetes...

Kubernetes Velero 备份的运用

Velero 的运用[TOC] Velero简介Velero是一个开源工具,可以安全地备份,恢复和迁移Kubernetes集群和持久卷。它既可以在本...

Windows搭建sshd服务与.net的运用

Windows搭建sshd服务与.net的运用[TOC] 下载win sshd 当前最新包连接: https://github.com/PowerShell/Win32-OpenSSH/re...

.net6 Signalr+Vue3 的运用

.net6 Signalr+Vue3 的运用(下)[TOC] 上篇链接:https://www.tnblog.net/hb/article/details/7961SignalR 中的用户 Sig...

.net6 Signalr+Vue3 配合Ingress Nginx的运用

.net6 Signalr+Vue3 配合Ingress Nginx的运用[TOC] 结合上篇:https://www.tnblog.net/hb/article/details/7963 项目打...

C ?、?? 问号和2个问号的用法类型?、对象?

C# ?C# ???:单问号1.定义数据类型可为空。可用于对int,double,bool等无法直接赋值为null的数据类型进行null的赋值如这...

Python实例 1-日志抓取处理 补错附日志小技巧

有时候数据出了问题,可以从日志中恢复数据(如果你没记日志..没备份..→_→..)一、日志展示介绍个平常自己用的小方法,如...

C 数组拆分泛型

主要用到了泛型。泛型是c#2.0的一个新增加的特性,它为使用c#语言编写面向对象程序增加了极大的效力和灵活性。不会强行对值...

MySQL 视图的增删改 查

要显示视图的定义,需要在SHOWCREATEVIEW子句之后指定视图的名称, 我们先来创建几张表,完事后在进行演示:--用户信息表...

使用NPOI导出excel包括图片

Excl模板导出相信我们都会,那么模板上要导出图片呢?嗯~还是来个例子:准备工作:首先要引用NPOI包:然后获取数据集(我这...

ajaxSubmit异步传图片嘘,外面都是假的

引用代码&lt;scriptsrc=&quot;/Scripts/jquery.form.js&quot;&gt;&lt;/script&gt;js就在旁边img链接中,只不过大小为0x0,...
这一世以无限游戏为使命!
排名
2
文章
657
粉丝
44
评论
93
docker中Sware集群与service
尘叶心繁 : 想学呀!我教你呀
一个bug让程序员走上法庭 索赔金额达400亿日元
叼着奶瓶逛酒吧 : 所以说做程序员也要懂点法律知识
.net core 塑形资源
剑轩 : 收藏收藏
映射AutoMapper
剑轩 : 好是好,这个对效率影响大不大哇,效率高不高
ASP.NET Core 服务注册生命周期
剑轩 : http://www.tnblog.net/aojiancc2/article/details/167
ICP备案 :渝ICP备18016597号-1
网站信息:2018-2025TNBLOG.NET
技术交流:群号656732739
联系我们:contact@tnblog.net
公网安备:50010702506256
欢迎加群交流技术