Git Product home page Git Product logo

blog's Issues

vue+flask前后端信息传输非对称加密

一. 介绍

为了前后端传输数据的安全性,需要对数据进行加密。因此选定使用非对称加密,此处为RSA

在传输数据前,后端生成公钥和私钥,将公钥给前端,前端加密之后,将密文传给后端,后端使用私钥解密即可得到原始的数据。

二. 环境

  • 前端使用jsencrypt加密
  • 后端使用Crypto生成密钥和解密

三. 后端生成密钥

from Crypto.PublicKey import RSA

def generate_key():
    """
    生成公钥和私钥
    
    :return: 返回私钥和公钥
    """
    rsa = RSA.generate(1024)
    private_key = rsa.exportKey()
    publick_key = rsa.publickey().exportKey()
    return private_key.decode(), publick_key.decode()

四. 前端用公钥加密

import { JSEncrypt } from "jsencrypt";

function rsa_en(pubkey, target_str) {
  /** 
  分段加密信息
  
  :params target_str: 需要加密的信息,此处为很长的信息
  :pubkey: 公钥
  :return: 存储密文的数组
  **/
  let encrypt = new JSEncrypt();
  encrypt.setPublicKey(pubkey);
  let result = encrypt.encrypt(JSON.stringify(target_str));
}

五. 后端使用私钥解密

from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
from Crypto.Hash import SHA
from Crypto import Random
from base64 import b64decode

def rsa_decrypt(private_key, message):
    """
    rsa解密函数

    :prams private_key: 私钥
    :params message: 加密后的密文
    :return: 解密后原始信息
    """
    dsize = SHA.digest_size
    sentinel = Random.new().read(1024 + dsize)
    private_key = RSA.import_key(private_key)
    cipher_rsa = PKCS1_v1_5.new(private_key)
    return cipher_rsa.decrypt(b64decode(message), sentinel)
  • 这里使用base64先解密一遍是必要的,否则报错ValueError: Ciphertext with incorrect length.

六. 完整代码

前端

import { JSEncrypt } from "jsencrypt";

function rsa_en(pubkey, target_str) {
  /** 
  分段加密信息
  
  :params target_str: 需要加密的信息,此处为很长的信息
  :pubkey: 公钥
  :return: 存储密文的数组
  **/
  let encrypt = new JSEncrypt();
    encrypt.setPublicKey(pubkey);
    let result = encrypt.encrypt(JSON.stringify(target_str));
}

后端

from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
from Crypto.Hash import SHA
from Crypto import Random
from base64 import b64decode


def generate_key():
    """
    生成公钥和私钥

    :return: 返回私钥和公钥
    """
    rsa = RSA.generate(1024)
    private_key = rsa.exportKey()
    publick_key = rsa.publickey().exportKey()
    return private_key.decode(), publick_key.decode()


def rsa_encrypt(public_key, message):
    """
    rsa加密函数

    :params publick_key: 公钥
    :params message: 需要加密的信息
    :return: 加密后的密文
    """
    public_key = RSA.import_key(public_key)
    cipher_rsa = PKCS1_v1_5.new(public_key)
    return cipher_rsa.encrypt(str.encode(message))


def rsa_decrypt(private_key, message):
    """
    rsa解密函数

    :prams private_key: 私钥
    :params message: 加密后的密文
    :return: 解密后原始信息
    """
    dsize = SHA.digest_size
    sentinel = Random.new().read(1024 + dsize)
    private_key = RSA.import_key(private_key)
    cipher_rsa = PKCS1_v1_5.new(private_key)
    return cipher_rsa.decrypt(b64decode(message), sentinel)

七. 分段加密

RSA加密信息最长为128位,过长则会报错,因此,对于过长的信息需要分段加密,后端也要分段解密后拼装。

import { JSEncrypt } from "jsencrypt";


function en_str(target_str, pubkey) {
  /** 
  分段加密信息
  
  :params target_str: 需要加密的信息,此处为很长的信息
  :pubkey: 公钥
  :return: 存储密文的数组
  **/
  let encrypt = new JSEncrypt();
	encrypt.setPublicKey(pubkey);
  let en_array = [];
	let n = 100; // 每段信息的长度
	for (let i = 0, l = target_str.length; i < l / n; i++) {
	  let message = target_str.slice(n * i, n * (i + 1));
	  en_array.push(encrypt.encrypt(message));
	}
  return en_array;
}

Swift计算两个日期的天数差

一. 官方方法

DateComponents

A date or time specified in terms of units (such as year, month, day, hour, and minute) to be evaluated in a calendar system and time zone.

以要在日历系统和时区中计算的单位(例如年、月、日、小时和分钟)指定的日期或时间。

二. 实现

1. 计算两个字符串形式的日期的天数差

func dateDiff() -> Int {
  // 计算两个日期差,返回相差天数
  let formatter = DateFormatter()
  let calendar = Calendar.current
  formatter.dateFormat = "yyyy-MM-dd"
  let today = Date()
  
  // 开始日期
  let startDate = formatter.date(from: "2021-06-08")
  
  // 结束日期
  let endDate = formatter.date(from: "2021-06-09")
  let diff:DateComponents = calendar.dateComponents([.day], from: startDate!, to: endDate!)
  return diff.day!
}

2. 计算当天跟某一天的天数差

func checkDiff() -> Int {
  // 计算两个日期差,返回相差天数
  let formatter = DateFormatter()
  let calendar = Calendar.current
  formatter.dateFormat = "yyyy-MM-dd"

  // 当天
  let today = Date()
  let startDate = formatter.date(from: formatter.string(from: today))
  
  // 固定日期
  let endDate = formatter.date(from: "2021-06-09")
  
  let diff:DateComponents = calendar.dateComponents([.day], from: startDate!, to: endDate!)
  return diff.day!
}

Go http请求报错x509 certificate signed by unknown authority

报错

在Go中POST请求时报错

x509: certificate signed by unknown authority

即无法检验证书。

package main

import (
    "net/http"
)

func Handle() {
  
 ...
  
    _, err := http.Post(
        ...
    )
  
...
  
}

解决

跳过校验即可。此处引入"crypto/tls"

package main

import (
  "crypto/tls"
	"net/http"
)

func Handle() {
  
    ...
  
    tr := &http.Transport{
        TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
    }

    client := &http.Client{
        Timeout:   15 * time.Second,
        Transport: tr,
    }
  
    _, err := client.Post(
        ...
    )
  
    ...
}

tailwindcss基础

一. 简介

Rapidly build modern websites without ever leaving your HTML.

Tailwind CSS可以快速建立现代网站,而无需离开HTML。其特性是原子化,很像的BootStrap的css。

通俗点解释就是,其封装了很多独立的css样式,只需要在html中添加class即可调用,而不需要去从头写css样式。

二. 安装

1. 下载包

npm install tailwindcss@latest postcss@latest autoprefixer@latest

可能会遇到如下报错:

Error: PostCSS plugin tailwindcss requires PostCSS 8.

那就需要降低PostCSS的版本。如下,先卸载,再去安装。

npm uninstall tailwindcss postcss autoprefixer
npm install tailwindcss@npm:@tailwindcss/postcss7-compat @tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9

2. 添加Tailwind作为PostCSS插件

添加tailwindcssautoprefixerPostCSS配置。大部分情况下作为postcss.config.js文件放在项目的顶级路径下。其也能作为.postcssrc文件,或者使用postcss键放在package.json文件中。

// postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  }
}

3. 创建配置文件

如果想自定义安装,当使用npm安装tailwindcss时候需要使用tailwind命令行去生成一个配置文件。

npx tailwindcss init

这将会创建一个最小化的tailwind.config.js文件,其位于项目的顶级路径下。

// tailwind.config.js
module.exports = {
  purge: [],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {},
  plugins: [],
}

4. 在CSS中包含Tailwind

创建styles.css文件。

/* ./your-css-folder/styles.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

引入该文件。

import "./styles.css"

5. 构建CSS

为生产而构建时,确保配置清除选项以删除任何最小文件大小的未使用类。

// tailwind.config.js
module.exports = {
  purge: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"], // 修改此行
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {}
  },
  variants: {
    extend: {}
  },
  plugins: []
};

三. 简要说明

由于其样式属性巨多,此处只举几例作简要说明,讲解基础用法。在开始不熟悉的情况下,要开着其手册查询。

1. Width

Class 解释
w-0 width: 0px;
w-1 width: 0.25rem;
w-1/2 width: 50%;
w-full width: 100%;
... ...
<!--示例-->
<div>
  <div class="w-1/2"></div>
</div>

2. Padding

Class 解释
p-0 padding: 0px;
p-5 padding: 1.25rem;
pl-1 padding-left: 0.25rem;
... ...
<!--示例-->
<div class="p-5"></div>

3. Position

Class 解释
static position: static;
fixed position: fixed;
absolute position: absolute;
... ...
<!--示例-->
<div class="static">
  <p>Static parent</p>
  <div class="absolute bottom-0 left-0 ...">
    <p>Absolute child</p>
  </div>
</div>

4. Flex垂直居中

<div class="flex flex-row justify-center items-center">
  <div>1</div>
  <div>2</div>
  <div></div>
</div>

四. 简单案例

<div class="flex flex-col justify-center items-center p-20">
  <div v-for="item in 10"
       :key="item"
       class="flex flex-row justify-between items-center w-1/5 bg-gray-100 m-5 p-10 cursor-pointer shadow rounded hover:shadow-lg transition duration-300 ease-in-out">
    <img src="@/assets/message.png" alt="logo" height="50px" width="50px">
    <div class="flex flex-col ml-5">
      <div class="text-lg">今天晚上加{{ item }}个鸡腿</div>
      <div class="text-sm text-gray-500">2020.2.{{ item }}</div>
    </div>
  </div>
</div>

vue组件props双向绑定

在vue2中不允许子组件直接修改props,为单项数据流,所有若要修改只能通过额外的值,并监听props以改变额外的值。

一. 设置props

props: {
    dialog: {
      type: Boolean,
      default: false
    }
}

二. 创建额外的值

data中创建一个localDialog,其值为this.dialog

data() {
    return {
    	localDialog: this.dialog
    }
}

三. 监听

保持同步的关键在于需要在子组件内监听props,即此处的dialog

watch: {
    dialog(val) {
      this.localDialog = val
    }
}

四. 子组件向父组件传递

子组件使用this.$emit()即可向父组件传递变化的值。

methods: {
    sendToFather() {
        this.$emit('dialogchange', this.localDialog)
    }
}

五. 父组件调用

<your-component :dialog="dialog" @dialogchange="dialogchange" />
data() {
	return {
        dialog: false
    }
},
methods: {
    dialogchange(val) {
    	this.dialog = val
    }
}

六. 完整代码

1. 子组件

<template>
    <div :visible="localDialog">
        justmylife.cc
		<button @click="sendToFather" />
    </div>
</template>

<script>
export default {
	props: {
    	dialog: {
      	type: Boolean,
      	default: false
    	}
	},
	data() {
        return {
            localDialog: this.dialog
        }
    },
    watch: {
   		dialog(val) {
      		this.localDialog = val
    	}
	},
    methods: {
    	sendToFather() {
        	this.$emit('dialogchange', this.localDialog)
    	}
	}
}
</script>

2. 父组件

<template>
    <your-component :dialog="dialog" @dialogchange="dialogchange" />
</template>

<script>
import yourComponent from './yourComponent'

export default {
	components: {
        yourComponent
    },
    data() {
		return {
        	dialog: false
    	}
	},
	methods: {
    	dialogchange(val) {
    		this.dialog = val
    	}
	}
}
</script>

Docker修改时区

一. 前言

在使用Docker时,其默认时区并非使用者所在时区,需要进行修改。对于单个容器,当前修改有几种常见方式,比如直接映射宿主机时区到容器内,而本文介绍的为使用Dockerfile来直接修改镜像时区。此处仅以常见几个基础容器为例来介绍。

二. 常见容器

1. Alpine

FROM alpine:latest

# 安装tzdata
RUN apk add --no-cache tzdata

# 设置时区
ENV TZ="Asia/Shanghai"
  • 验证
docker build -t alpine:time .
docker run --rm -it alpine:time date

2. Ubuntu

FROM ubuntu

# 设置localtime
# 此处需要优先设置localtime,否则安装tzdata将会进入时区选择
RUN ln -fs /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

# 安装tzdata
RUN apt-get update \
		&& apt-get install tzdata -y \
		&& apt-get clean
  • 验证
docker build -t ubuntu:time .
docker run --rm -it ubuntu:time date

3. Debian

  • Debian中已经安装了tzdata,所以跟Ubuntu有所不同
FROM debian

# 修改设置dpkg为自动配置
ENV DEBIAN_FRONTEND=noninteractive

RUN ln -fs /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

RUN dpkg-reconfigure -f noninteractive tzdata

# 修改设置dpkg为手动输入选择操作
ENV DEBIAN_FRONTEND=dialog
  • 验证
docker build -t debian:time .
docker run --rm -it debian:time date

三. 结语

此处不再列举太多,主要解决方式为安装tzdata,然后修改时区。

SwiftUI项目调用生物识别(Touch ID / Face ID)

一. 设置权限

打开文件info.plist,在空白处右击,选择Add Row,输入选择Privacy - Face ID Usage Description,然后在value中写入我们需要验证您的身份以保护数据

swiftui_face_privacy_info_plist

二. 代码层面接入

打开ContentView.swift文件,开始如下操作。

1. 引入相关库

import LocalAuthentication

2. 创建lock变量

@State private var isUnlocked = false

isUnlocked为是否解锁,true表示验证完成,已解锁,false表示验证失败,未解锁。

3. 创建函数

func authenticate() {
    let context = LAContext()
    var error: NSError?

    // 检查生物特征认证是否可用
    if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
        // 可用,所以继续使用它
        let reason = "我们需要验证您的身份以保护数据"

        context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, authenticationError in
            // 身份验证现已完成
            DispatchQueue.main.async {
                if success {
                    // 认证成功,解锁
                    self.isUnlocked = true
                } else {
                    // 发生的异常
                }
            }
        }
    } else {
        // 没有生物识别
    }
}

4. 根据isUnlocked切换View

VStack {
    if self.isUnlocked {
        Text("Unlocked")
    } else {
        Text("Locked")
    }
}
.onAppear(perform: authenticate) // 该方法调用生物识别验证函数

三. 审核问题

最近提交了ios和macos两个产品到app store,我设置的强制使用生物识别才能进入应用。但是出现的问题是macos的审核过了,而ios的审核没有过,其反馈的问题即为开始的生物识别没有过,审核人员使用的模拟器,根本不存在生物识别。

所以跟可能会踩这个坑的小伙伴儿提个醒,目前我还没有好的解决方案,正在等待新的审核中......

SwiftUI MacOS项目alert弹出两次问题解决

问题

使用Alert时,将其用在list的循环视图元素中,弹出Alert时,一定时长不选择就会在点击后弹出第二次。

这里提一下就是之前在网上看到一个帖子说他将Alert放在NavigationView上也会出现该问题。

        VStask {
            ForEach(items, id: \.self) { item in
                ElementView(item: item) // 循环中的元素
                    .alert(isPresented: $showAlert) {
                        Alert(
                            title: Text("删除确认"),
                            message: Text("请问您确认删除该数据吗?"),
                            primaryButton: .default(
                                Text("取消"),
                                action: {
                                    showAlert = false
                                }
                            ),
                            secondaryButton: .destructive(
                                Text("删除"),
                                action: {
                                    deleteItems(offsets: [index])
                                })
                        )
                    }
            }
        }

解决

Alert放到循环之前的元素上,比如VStackList

参考

Promise inside request interceptor

问题

在使用axios的拦截器时候,需要在request中调用一个promise函数,因此需要等待其执行完成才能去进行下一步。

function getToken() {
  return new Promise(...)
}

// Request interceptors
service.interceptors.request.use(config => {
  getToken()
  ...
})

解决

function getToken() {
  return new Promise(...)
}

// Request interceptors
service.interceptors.request.use(async config => {
  await getToken()
  ...
})

sqlx基础教程

一. 前言

为什么要用sqlx而不是gorm呢?是因为orm学习成本比较高,当使用python时候需要使用sqlalchemy,遇到go就要换成gorm,换成别的语言就又有其他orm。而直接使用原生sql可以减少学习成本,适用于所有开发语言。其次,gorm本身支持软删除,但是其对软删除的支持上存在缺陷,在单条查询可以过滤软删除数据,但是在多条查询时无法有效过滤,就造成了有时候要手动过滤又有时候不要手动过滤,使用体验非常差。

因此此处考虑去使用sqlx来直接调用原生sql

二. 安装

go get github.com/jmoiron/sqlx

三. 连接数据库

package database

import (
	"fmt"
	"os"

	_ "github.com/go-sql-driver/mysql"
	"github.com/jmoiron/sqlx"
)

func DBConnect() *sqlx.DB {
	env := os.Getenv("NODE_ENV")

	var host, port, user, password, dbname string
  // 使用环境变量来切换生产和开发环境
	if env == "production" {
		host = os.Getenv("dbHost")
		port = os.Getenv("dbPort")
		user = os.Getenv("dbUser")
		password = os.Getenv("dbPassword")
		dbname = os.Getenv("dbname")
	} else {
		host = "<your-host>"
		port = "<your-port>"
		user = "<your-user>"
		password = "<your-password>"
		dbname = "<your-db-name>"
	}

	dbConfig := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true", user, password, host, port, dbname)
	db, err := sqlx.Connect("mysql", dbConfig)
	if err != nil {
		fmt.Println(err)
		panic("failed to connect database")
	}

	return db
}

四. 创建表和调用

1. 创建表

create table if not exists test_gin.todo
(
    todo_id    int auto_increment
        primary key,
    title      varchar(20)                          not null comment 'todo标题',
    content    varchar(200)                         null comment '内容',
    user_id    int                                  not null comment '用户id',
    created_at timestamp  default CURRENT_TIMESTAMP null comment '创建时间戳',
    updated_at timestamp  default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间戳',
    is_deleted tinyint(1) default 0                 not null comment '是否被删除,0:未删,1:已删'
);

2. 创建struct

type Todo struct {
	TodoID    int    `db:"todo_id" json:"todo_id,omitempty"`
	Title     string `db:"title" json:"title,omitempty"`
	Content   string `db:"content" json:"content,omitempty"`
	UserID    int    `db:"user_id" json:"user_id,omitempty"`
	CreatedAt string `db:"created_at" json:"created_at,omitempty"`
	UpdatedAt string `db:"updated_at" json:"updated_at,omitempty"`
	IsDeleted bool   `db:"is_deleted" json:"is_deleted,omitempty"`
}

3. 封装方法

此处以增删为例。封装常用方法是为了复用,封装时候使用害羞的代码。不需要为了封装而封装。

// 新增Todo
func (t *Todo) Add() (todoID int, err error) {

	// 连接数据库
	db := database.DBConnect()
	defer db.Close()

	// 执行添加sql
	tx := db.MustBegin()
	result := tx.MustExec("insert into todo (title, content, user_id) value (?, ?, ?)",
		t.Title, t.Content, t.UserID)
	lastTodoID, err := result.LastInsertId()
	if err != nil {
		return
	}
	_ = tx.Commit()

	todoID = int(lastTodoID)
	return
}

// 删除Todo
func (t *Todo) Del() {

	db := database.DBConnect()
	defer db.Close()

	tx := db.MustBegin()
	tx.MustExec("update todo set is_deleted = 0 where is_deleted = 0 and todo_id = ?", t.TodoID)
	_ = tx.Commit()
}

五. 视图中使用

此处以新增接口为例。

1. 调用封装的方法

package views

import (
	"github.com/gin-gonic/gin"
	"testGin/models"
)

// addTodo.go -- post

func AddTodo(c *gin.Context) {

	// {"title": "hello", "content": "world"}
	var requestBody struct {
		Title   string `json:"title"`
		Content string `json:"content"`
	}

	if c.ShouldBind(&requestBody) != nil {
		c.JSON(200, gin.H{
			"code":    40000,
			"message": "参数有误",
		})
		return
	}

	todo := models.Todo{
		Title:   requestBody.Title,
		Content: requestBody.Content,
		UserID:  1, // 此处的1为假数据,此处应当从上下文获取请求用户的user_id
	}
	todoID, err := todo.Add()
	if err != nil {
		c.JSON(200, gin.H{
			"code": 20001,
		})
		return
	}

	c.JSON(200, gin.H{
		"code":    20000,
		"todo_id": todoID,
	})
}

2. 直接使用

package views

import (
	"github.com/gin-gonic/gin"
	"testGin/database"
)

// addTodo.go -- post

func AddTodo(c *gin.Context) {

	// {"title": "hello", "content": "world"}
	var requestBody struct {
		Title   string `json:"title"`
		Content string `json:"content"`
	}

	if c.ShouldBind(&requestBody) != nil {
		c.JSON(200, gin.H{
			"code":    40000,
			"message": "参数有误",
		})
		return
	}

	// 连接数据库
	db := database.DBConnect()
	defer db.Close()

	// 执行添加sql
	tx := db.MustBegin()
	result := tx.MustExec("insert into todo (title, content, user_id) value (?, ?, ?)",
		requestBody.Title, requestBody.Content, 1)
	lastTodoID, err := result.LastInsertId()
	if err != nil {
		c.JSON(200, gin.H{
			"code": 20001,
		})
		return
	}
	_ = tx.Commit()

	c.JSON(200, gin.H{
		"code":    20000,
		"todo_id": int(lastTodoID),
	})
}

electron-builder踩坑系列---无边框窗口拖动

简述

采用无边框就自然导致拖动问题,因此需要手动去设置可拖动区域。

设置可拖动区域也会出现新的问题,如果可拖动区域上存在其他元素的事件问题。在mac上直接设置一块可拖动区域即可,而windows就会出现可拖动区域上其他元素的事件被拖动事件覆盖掉了。因此还需要对其他元素设置不可拖动。

官方文档

默认情况下, 无边框窗口是不可拖拽的。 应用程序需要在 CSS 中指定 -webkit-app-region: drag 来告诉 Electron 哪些区域是可拖拽的(如操作系统的标准标题栏),在可拖拽区域内部使用 -webkit-app-region: no-drag 则可以将其中部分区域排除。 请注意, 当前只支持矩形形状。

要使整个窗口可拖拽, 您可以添加 -webkit-app-region: drag 作为 body 的样式:

<body style="-webkit-app-region: drag">
</body>

请注意,如果您使整个窗口都可拖拽,则必须将其中的按钮标记为不可拖拽,否则用户将无法点击它们:

button {
  -webkit-app-region: no-drag;
}

实现

.menu {
  -webkit-app-region: drag;
}

.menu-button {
  -webkit-app-region: no-drag;
}

参考文档

vue2实现动态生成二维码和将网页合成图片并在微信内置浏览器长按保存

一. url转为二维码

1. 需要的库

qrcodejs2

2. 安装

npm install qrcodejs2 --save

3. 引入

import QRCode from "qrcodejs2"

4. 实现

<template>
	<div>
    <div id="qrcode"></div>
  </div>
</template>

<script>
import QRCode from "qrcodejs2";
 
export default {
  methods: {
    GenerateQRcode() {
      new QRCode("qrcode", { // 此处的qrcode为上面div的id
        text: 目标url,
        width: 200,
        height: 200,
        colorDark: "#000000",
        colorLight: "#ffffff",
        correctLevel: QRCode.CorrectLevel.H
      });
    }
  },
  mounted() {
    this.GenerateQRcode();
  }
}
</script>

二. 网页保存为图片

1. 需要的库

html2canvas

2. 安装

npm install html2canvas --save

3. 引入

import html2canvas from "html2canvas"

4. 实现

<template>
	<div>
  	<div id="container"></div>
	</div>
</template>

<script>
import html2canvas from "html2canvas";
import QRCode from "qrcodejs2";

export default {
  methods: {
    outputImg() {
    	const targetDom = document.getElementById("container");
    	html2canvas(targetDom).then(canvas => {
      	console.log(canvas);
      	console.log(canvas.toDataURL());
    	});
  	}
  },
  mounted() {
    this.outputImg();
  }
}
</script>

三. 整合

关于小程序内置浏览器的图片下载,需要一个用来生成图片的块,还需要一个img,先将其隐藏。实现步骤就是首先生成二维码,然后再将html生成图片,最后在html2canvas回调中替换imgsrc,并将生成图片的块隐藏,将img显示。

当然关于这个实现方式,我看到的技术分享文章中,还有两种不同的解决方式:

  • 不需要html来写生成图片的块,而是使用js直接创建;
  • 不需要替换隐藏,将生成的图片覆盖到html生成图片的块之前;

这里我只记录一下我使用的,后期会再去研究这两种实现方式。

<template>
	<div>
    <!--合成的图片,默认隐藏,合成之后显示-->
    <div v-show="imgUrl.length">
      <img :src="imgUrl" alt="生成的图片" class="image" />
  	</div>
    <!--合成图片需要的html块,默认显示,合成之后隐藏-->
  	<div id="container" v-show="!imgUrl.length">
    	<div id="qrcode"></div>
      <p>长按识别二维码</p>
  	</div>
  </div>
</template>

<script>
import html2canvas from "html2canvas";

export default {
  data() {
    return {
      imgUrl: ""
    }
  },
  methods: {
    outputImg() {
    	const targetDom = document.getElementById("container");
    	html2canvas(targetDom).then(canvas => {
        // 将图片src替换为canvas生成之后转换的url
      	this.imgUrl = canvas.toDataURL();
    	});
  	},
    GenerateQRcode() {
      new QRCode("qrcode", {
        text: 目标url,
        width: 200,
        height: 200,
        colorDark: "#000000",
        colorLight: "#ffffff",
        correctLevel: QRCode.CorrectLevel.H
      });
    }
  },
  mounted() {
    new Promise(resolve => {
      // 先生成二维码
      this.GenerateQRcode();
      resove();
    })
    .then(() => {
      // 再合成图片
      this.outputImg();
    })
  }
}
</script>

<style scoped>
  // 生成之后的图片有点放肆,可以设置宽度来适应手机屏幕
 .image {
    width: 100%;
 }
</style>

由此即可实现需要的功能了。

关于后续的优化,需要解决的图片清晰度问题、跨域图片问题等,可以参考这篇文章,这位大佬写得很详细。

vue菜单高亮

一. 需求

在使用vue写菜单时,需要实现菜单上高亮与当前路由匹配。

二. 原生支持

Vue原生支持菜单高亮,菜单中路由使用<router-link>

添加样式.router-link-class来实现菜单高亮的样式。

在菜单中某一元素的<router-link>添加exact属性来实现默认高亮。

<template>
  <div class="menu">
    <ul>
      <li>
        <!--默认高亮路由-->
        <router-link to="/" exact>首页</router-link>
      </li>
      <li>
        <router-link to="/news" exact>资讯</router-link>
      </li>
      <li>
        <router-link to="/about" exact>关于</router-link>
      </li>
    </ul>
  </div>
</template>

<style scoped>
  .router-link-active {
    background: lightblue;
    color: black;
  }
</style>

三. ElementUI的NavMenu组件

ElementUINavMenu组件封装比较完善,可以直接上手使用,但是需要设置router=true才可开启高亮。

高亮样式可以通过官方给出的属性设置。

 <el-menu
      default-active="/"
      :router="true">
     <el-menu-item index="/">
         <span slot="title">首页</span>
     </el-menu-item>
     <el-menu-item index="/news">
         <span slot="title">资讯</span>
     </el-menu-item>
     <el-menu-item index="/about">
         <span slot="title">关于</span>
     </el-menu-item>
</el-menu>

四. 参考文档

Interval计时器在tab页切换或者隐藏情况下停止运行

问题

开始是开发electron时遇到的问题,使用Interval计时器,在窗口最小化隐藏再打开,计时器在隐藏期间并没有工作。

后来网上查询相关问题,发现更多是在浏览器tab页隐藏/切换情况下,计时器就会停止。

解决

在后台选项卡上运行的计时器方法可能会耗尽资源。在后台选项卡中以非常短的时间间隔运行回调的应用程序可能会消耗大量内存,以至于当前活动选项卡的工作可能会受到影响。在最坏的情况下,浏览器可能会崩溃,或者设备的电池会很快耗尽。

此限制是浏览器限制的。

无法突破限制,但是可以使用折中的方式,当然我也觉得此方式相较于一直计时会更优,即监听visibilitychange事件。

visibilitychange事件可以监听tab页面的激活与失活事件,因此可以:

  • 在失活时,记录计时器计算的最后的值,清空计时器
  • 在激活时,计算失活期间应有的值,继续使用计时器计算

添加事件代码如下:

document.addEventListener('visibilitychange', function() {
    if(document.hidden) {
        // tab页失活
        
    }
    else {
        // tab页激活
        
    }
});

参考文档

SwiftUI+Reality开发AR项目解决全屏问题

环境

  • Swift 5.4
  • Xcode 12.5.1

问题

在使用Swift UIRealiy开发AR项目时,发现摄像头一直是居中的,无法全屏。

解决

1. 创建LaunchScreen.storyboard文件

在左侧文件列表中新建文件,名为LaunchScreen.storyboard

2. 设置Launch Screen File

点击左侧文件列表中你的项目文件(最顶级文件),进入文件[your-project].xcodeproj文件。

General中,找到App Icons and Launch Images,在其模块中有Launch Screen File选项,点击选择为LaunchScreen.storyboard

总结下就是:[yourTarget] -> General -> App Icons and Launch Images

参考文档

Flask_sqlalchemy报错MySQL server has gone away解决

报错

使用flask_sqlalchemy,服务端出现500错误,日志显示报错如下:

MySQL server has gone away

解决

添加配置:

SQLALCHEMY_POOL_RECYCLE = 280

该配置作用是设置多少秒后回收连接,如果不提供值,默认为 2 小时。此处将其设置为280秒。

该配置原文解释:

Number of seconds after which a connection is automatically recycled. This is required for MySQL, which removes connections after 8 hours idle by default. Note that Flask-SQLAlchemy automatically sets this to 2 hours if MySQL is used.

electron-builder踩坑系列---mac下窗口毛玻璃效果

简介

一直觉得毛玻璃样式很炫,而要在electron中实现,本来是需要自己去写样式的,我在开发之前也去了解了下,想看看有没有大佬已经实现了,不过确实发现了一个大佬的仓库分享了毛玻璃组件,但是其README也提到了官方仓库对于mac的毛玻璃效果的pr,然后我去找了官方文档,已经有相关属性了,就很妙啊!

但是为什么标题要写“mac下”下呢,因为这个属性只对mac有效。(打工人落泪...)

官方文档

文档地址

https://www.electronjs.org/docs/api/browser-window

相关属性

  • vibrancy String (可选) - 窗口是否使用 vibrancy 动态效果, 仅 macOS 中有效. Can be appearance-based, light, dark, titlebar, selection, menu, popover, sidebar, medium-light, ultra-dark, header, sheet, window, hud, fullscreen-ui, tooltip, content, under-window, or under-page. Please note that using frame: false in combination with a vibrancy value requires that you use a non-default titleBarStyle as well. Also note that appearance-based, light, dark, medium-light, and ultra-dark have been deprecated and will be removed in an upcoming version of macOS.

  • visualEffectStateString (optional) - Specify how the material appearance should reflect window activity state on macOS. Must be used with thevibrancyproperty. 可能的值有

    • followWindow - 当窗口处于激活状态时,后台应自动显示为激活状态,当窗口处于非激活状态时,后台应自动显示为非激活状态。 This is the default.
    • active - 后台应一直显示为激活状态。
    • inactive - 后台应一直显示为非激活状态。

实现

有了官方Buff加持,使起来就很方便了。

// background.js

let win = new BrowserWindow({
  width: 800,
  height: 600,
  vibrancy: 'dark',  // 'light', 'medium-light' etc
  visualEffectState: "active" // 这个参数不加的话,鼠标离开应用程序其背景就会变成白色
})

实现就是这么简单!

小伙伴儿们有兴趣的可以参考下我这个项目,使用的毛玻璃样式。

Flask设置全局错误捕获

一. 前言

代码运行过程中,意外情况会导致500错误,对于使用者来说体验很不好,对于开发者来说也无法及时获取错误,需要去查看日志。

并且有的插件在某些报错情况下会返回一些敏感信息,非常危险。因此需要去捕获全局错误,通知开发者,自定义错误消息等。

二. 实现

from server import app
from flask import request
from datetime import datetime
from werkzeug.exceptions import HTTPException


@app.errorhandler(Exception)
def all_exception_handler(e):
    if isinstance(e, HTTPException):
        if e.code == 404:
            return {
                       'code':    40004,
                       'message': '404'
                   }, 404

    # 通知开发者/写入日志
    handle(path = request.path, content = str(e))
    
    return {
        'code':    20001,
        'message': 'Error'
    }

SwiftUI项目Image点击事件

一. 前言

swiftui中的点击可以有两种情况:

  • Button
  • Gestures

根据不同情况可以去不同地使用。

二. 单纯的按钮

此处单纯的按钮即为有按钮样式和点击事件。

Button(action: {
  ... // 点击事件触发的代码
}, label: {
	Image(systemName: "plus")
})

三. 无样式的按钮

即为没有按钮样式的按钮,方便直接展示Image

Button(action: {
  ... // 点击事件触发的代码
}, label: {
	Image(systemName: "plus")
})
.buttonStyle(BorderlessButtonStyle())

四. TapGesture事件

Image(systemName: "plus")
  .onTapGesture {
    ... // 点击事件触发的代码
  }

Nginx配置Proxy Cache

一. 前言

当Ngnix的缓存被打开之后,Nginx会将符合规则的response存储到文件作为缓存,并且用其缓存去响应客户端,不需要每次都去服务端请求。

二. 开启缓存

1. 如何开启

要开启缓存,需要在最顶级的http {}下配置proxy_cache_path

http {
    ...
    proxy_cache_path /data/nginx/cache keys_zone=one:10m;
}

此处可以选择直接修改/etc/nginx/nginx.conf文件,如上代码所示添加。也可以直接在/etc/nginx/conf.d文件夹下创建新的conf文件,如settings.conf,内容如下:

proxy_cache_path /data/nginx/cache keys_zone=one:10m;

2. 参数详解

  • /path/to/cache : 本地路径,缓存文件存放地址;
  • levels : 默认所有缓存文件都放在同一个/path/to/cache下,从而影响缓存的性能,大部分场景推荐使用2级目录来存储缓存文件;
  • key_zone : 在共享内存中设置一块存储区域来存放缓存的key和metadata(类似使用次数),这样nginx可以快速判断一个request是否命中或者未命中缓存,1m可以存储8000个key,10m可以存储80000个key;
  • max_size : 最大cache空间,如果不指定,会使用掉所有disk space,当达到配额后,会删除最少使用的cache文件;
  • inactive : 未被访问文件在缓存中保留时间,本配置中如果60分钟未被访问则不论状态是否为expired,缓存控制程序会删掉文件,默认为10分钟。“需要注意的是,inactive和expired配置项的含义是不同的,expired只是缓存过期,但不会被删除,inactive是删除指定时间内未被访问的缓存文件”;
  • use_temp_path : 如果为off,则nginx会将缓存文件直接写入指定的cache文件中,而不是使用temp_path存储,official建议为off,避免文件在不同文件系统中不必要的拷贝;
  • proxy_cache : 启用proxy cache,指定key_zone;

3. nginx初始化读取缓存配置

Nginx在缓存中使用到两个进程:

  • cache manager:该进程周期性地检测缓存的状况,检测缓存的大小、数量等是否超过设置额度;
  • cache loeader:该进程只会在Nginx启动后运行一次,加载之前的缓存文件的元数据到共享内存中。在启动时候一次加载整个缓存会消耗Nginx启动时的性能,为了避免这种情况,可以在proxy_cache_path中直接配置如下参数:
    • loader_threshold:一个迭代的持续时间,单位为ms,默认值200;
    • loader_files:一个迭代中加载的最大项目数,默认为100;
    • loader_sleeps:迭代之间的推迟,单位为ms,默认值50;

在如下案例中,迭代持续300ms或者直到200个项目已经被加载:

proxy_cache_path /data/nginx/cache keys_zone=one:10m loader_threshold=300 loader_files=200;

三. 哪些请求会缓存

默认情况下,Nginx会在第一次缓存所有的GETHEAD方法的请求的响应,使用字符串作为缓存的key,如果不同的请求使用同一个key,则会直接返回其对应的缓存数据。

1. 修改缓存的key

默认的缓存key为:

proxy_cache_key $scheme$proxy_host$uri$is_args$args;

可以通过配置自定义,如下:

proxy_cache_key "$host$request_uri$cookie_user";

2. 定义在缓存响应之前必须发出具有相同密钥的请求的最小次数

proxy_cache_min_uses 5;

3. 定义缓存方法的请求

默认缓存请求方法为GETHEAD,通过如下添加POST方法。

proxy_cache_methods GET HEAD POST;

四. 限制或者禁止缓存

默认情况下,响应会无限期的保存下来。只有当缓存大小超过限制或者超出缓存时间的情况下,缓存将会被清除。

1. 根据响应码设定存储时间

proxy_cache_valid 200 302 10m;
proxy_cache_valid 404      1m;

如果是匹配所有可以使用any

proxy_cache_valid any 5m;

2. 忽略缓存

通过设置proxy_cache_bypass配置可以使Nginx不直接使用缓存返回客户端。配置中的每个参数都只是0和非0,只要一个参数不为0,都将不会使用缓存。

proxy_cache_bypass $cookie_nocache $arg_nocache$arg_comment;

3. 没有缓存

通过设置proxy_no_cache来使得响应完全不缓存,其参数和proxy_cache_bypass一样。

proxy_no_cache $http_pragma $http_authorization;

对于proxy_cache_bypass和proxy_no_cache的参数,如$arg_nocache的意思为请求的url上携带nocache参数。

五. 参考文档

Golang AES-256-CBC加密和解密

一. 前言

项目开发中遇到该问题,网上的文章太乱,为了节省下次踩坑时间,特此记录。

二. 加解密

1. 填充函数

该函数在加解密中都需要用到。

func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
	padding := blockSize - len(ciphertext)%blockSize
	padText := bytes.Repeat([]byte{byte(padding)}, padding)
	return append(ciphertext, padText...)
}

2. 加密

func Ase256Encrypt(plaintext string, key string, iv string, blockSize int) string {
	bKey := []byte(key)
	bIV := []byte(iv)
	bPlaintext := PKCS5Padding([]byte(plaintext), blockSize)
	block, _ := aes.NewCipher(bKey)
	ciphertext := make([]byte, len(bPlaintext))
	mode := cipher.NewCBCEncrypter(block, bIV)
	mode.CryptBlocks(ciphertext, bPlaintext)

	return base64.StdEncoding.EncodeToString(ciphertext)
}

3. 解密

func Aes256Decrypt(cryptData, key, iv string) ([]byte, error) {
	ciphertext, err := base64.StdEncoding.DecodeString(cryptData)
	if err != nil {
		return nil, err
	}

	block, err := aes.NewCipher([]byte(key))
	if err != nil {
		return nil, err
	}

	if len(ciphertext)%aes.BlockSize != 0 {
		err = errors.New("ciphertext is not a multiple of the block size")
		return nil, err
	}

	mode := cipher.NewCBCDecrypter(block, []byte(iv))
	mode.CryptBlocks(ciphertext, ciphertext)

	return ciphertext, err
}

4. 全部代码

// 填充
func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
	padding := blockSize - len(ciphertext)%blockSize
	padText := bytes.Repeat([]byte{byte(padding)}, padding)
	return append(ciphertext, padText...)
}

// 加密
func Ase256(plaintext string, key string, iv string, blockSize int) string {
	bKey := []byte(key)
	bIV := []byte(iv)
	bPlaintext := PKCS5Padding([]byte(plaintext), blockSize)
	block, _ := aes.NewCipher(bKey)
	ciphertext := make([]byte, len(bPlaintext))
	mode := cipher.NewCBCEncrypter(block, bIV)
	mode.CryptBlocks(ciphertext, bPlaintext)

	return base64.StdEncoding.EncodeToString(ciphertext)
}

// 解密
func Aes256Decrypt(cryptData, key, iv string) ([]byte, error) {
	ciphertext, err := base64.StdEncoding.DecodeString(cryptData)
	if err != nil {
		return nil, err
	}

	block, err := aes.NewCipher([]byte(key))
	if err != nil {
		return nil, err
	}

	if len(ciphertext)%aes.BlockSize != 0 {
		err = errors.New("ciphertext is not a multiple of the block size")
		return nil, err
	}

	mode := cipher.NewCBCDecrypter(block, []byte(iv))
	mode.CryptBlocks(ciphertext, ciphertext)

	return ciphertext, err
}

三. 调用

1. 加密

result := Ase256Encrypt("<需要加密的数据>", "<key>", "<iv>", aes.BlockSize)

2. 解密

result, err := Aes256Decrypt("<需要解密的数据>", "<key>", "<iv>")

Flask_sqlalchemy的增删改查

一. 增

article = Article(title='article1', content='heihei')
db.session.add(article)
db.session.commit()

若要在新增之后获取数据库中新增数据的信息,如id

article = Article(title='article1', content='heihei')
db.session.add(article)
db.session.flush() # 添加这一条,用于预提交
db.session.commit()

# 输出新增数据的信息
print(article.id)
print(article.title)

二. 删

# 1.把需要删除的数据查找出来
article = Article.query.filter_by(content = 'heihei').first()

# 2.把这条数据删除掉
db.session.delete(article)

# 3.提交
db.session.commit()

三. 改

# 1.先把你要更改的数据查找出来
article = Article.query.filter(Article.title == 'article1').first()

# 2.把这条数据需要修改的地方进行修改
article.title = 'article2'

# 3.提交
db.session.commit()

四. 查

1. 查询单个

article1 = Article.query.filter(Article.title == 'article').first()
article1 = Article.query.filter_by(title = 'article').first() # 或者

print(article1.title)
print(article1.content)

2. 查询所有

article1 = Article.query.filter_by(title = 'article').all()
for item in article1:
  print(item.title)
  print(item.content)

3. 倒叙

article1 = Article.query.filter_by(title = 'article').order_by(Article.id.desc()).all()

4. 限制数量

article1 = Article.query.filter_by(title = 'article').limit(10).all()

node spawn在windows下不生效问题记录

问题描述

使用electron开发的windows桌面应用程序,在调用目标文件夹底下的exe执行文件时,开发机子上没有问题,但是其他机子使用时一直调用失败,也抓取不到日志。

spawn(path.join(remote.app.getAppPath(), "../target.exe"), [], {
  shell: true,
  detached: false,
  windowsHide: true
});

原因

路径存在空格。

也是经过各种原因排查,然后一次偶然的成功才注意到了路径问题,排查之后发现确实是这问题......

解决

spawn按照如上我的代码一定条件下可以运行,其有一个参数cwd,用来表明运行目录。spawn第一个参数必须是命令的名字,不能是路径。

所以如上代码改成这样:

spawn("target.exe", [], { // 此处直接写目标exe文件
  cwd: path.join(remote.app.getAppPath(), "../"), // 注意这里,使用了cwd参数来写运行目录
  shell: true,
  detached: false,
  windowsHide: true
});

参考文档

vue实现纯前端导入与解析excel表格文件

一. 场景

前端导入excel表格,直接前端解析文件,将数据传给后端。

二. 需要的库

1. 安装

npm install xlsx

2. 使用

import XLSX from "xlsx";

三. 代码实现

1. html部分

    <div class="container">
      {{ upload_file || "导入" }}
      <input
        type="file"
        accept=".xls,.xlsx"
        class="upload_file"
        @change="readExcel($event)"
      />
    </div>
  </div>

2. JS部分

readExcel(e) {
      // 读取表格文件
      let that = this;
      const files = e.target.files;
      if (files.length <= 0) {
        return false;
      } else if (!/\.(xls|xlsx)$/.test(files[0].name.toLowerCase())) {
        this.$message({
          message: "上传格式不正确,请上传xls或者xlsx格式",
          type: "warning"
        });
        return false;
      } else {
        // 更新获取文件名
        that.upload_file = files[0].name;
      }

      const fileReader = new FileReader();
      fileReader.onload = ev => {
        try {
          const data = ev.target.result;
          const workbook = XLSX.read(data, {
            type: "binary"
          });
          const wsname = workbook.SheetNames[0]; //取第一张表
          const ws = XLSX.utils.sheet_to_json(workbook.Sheets[wsname]); //生成json表格内容
					console.log(ws);
        } catch (e) {
          return false;
        }
      };
      fileReader.readAsBinaryString(files[0]);
    }

3. 整体代码

<template>
  <div>
    <div class="container">
      {{ upload_file || "导入" }}
      <input
        type="file"
        accept=".xls,.xlsx"
        class="upload_file"
        @change="readExcel($event)"
      />
    </div>
  </div>
</template>

<script>
import XLSX from "xlsx";

export default {
  data() {
    return {
      upload_file: "",
      lists: []
    };
  },
  methods: {
    submit_form() {
      // 给后端发送请求,更新数据
      console.log("假装给后端发了个请求...");
    },
    readExcel(e) {
      // 读取表格文件
      let that = this;
      const files = e.target.files;
      if (files.length <= 0) {
        return false;
      } else if (!/\.(xls|xlsx)$/.test(files[0].name.toLowerCase())) {
        this.$message({
          message: "上传格式不正确,请上传xls或者xlsx格式",
          type: "warning"
        });
        return false;
      } else {
        // 更新获取文件名
        that.upload_file = files[0].name;
      }

      const fileReader = new FileReader();
      fileReader.onload = ev => {
        try {
          const data = ev.target.result;
          const workbook = XLSX.read(data, {
            type: "binary"
          });
          const wsname = workbook.SheetNames[0]; //取第一张表
          const ws = XLSX.utils.sheet_to_json(workbook.Sheets[wsname]); //生成json表格内容
          that.lists = [];
          // 从解析出来的数据中提取相应的数据
          ws.forEach(item => {
            that.lists.push({
              username: item["用户名"],
              phone_number: item["手机号"]
            });
          });
          // 给后端发请求
          this.submit_form();
        } catch (e) {
          return false;
        }
      };
      fileReader.readAsBinaryString(files[0]);
    }
  }
};
</script>

四. 样式

原本的文件上传样式可能会跟页面整体风格不搭,所以需要修改其样式。不过此处并不是直接修改其样式而是通过写一个div来覆盖原有的上传按钮。此处样式与element UI中的primary按钮样式相同。

实现该样式的关键在于.upload_fileopacityposition

.container {
  border: none;
  border-radius: 4px;
  background-color: #409eff;
  height: 40px;
  margin-top: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 15px;
  min-width: 80px;
  *zoom: 1;
}

.upload_file {
  font-size: 20px;
  opacity: 0;
  position: absolute;
  filter: alpha(opacity=0);
  width: 60px;
}

五. 最后

前端的日益强大导致很多功能都可以在前端去直接实现,并且可以减少服务器压力。

当然单纯地去实现这样的数据传输,尤其对于重要数据,是很不安全的,因此在前后端数据传输的时候,可以加上加密校验,这个后期会来写的。

六. 参考文章

为了实现该功能参考了如下大佬的文章:

Vue按需引入ElementUI报错Error/ Plugin/Preset files are not allowed to export objects, only functions

报错

按照ElementUI官方文档按需引入却报错,首先报错缺少babel-preset-es2015。安装该组件之后编译却报错。

Error: Plugin/Preset files are not allowed to export objects, only functions.

解决

该问题为babel版本冲突。

1. 安装插件

yarn add @babel/preset-env 

2. 编辑.babelrc

{  
    "presets": [["@babel/preset-env", { "modules": false }]],
    "plugins": [
      [
        "component",
        {
          "libraryName": "element-ui",
          "styleLibraryName": "theme-chalk"
        }
      ]
    ]
}

SwiftUI MacOS项目根据屏幕大小调整窗口大小

一. 代码实现

1. 获取屏幕对象

var window = NSScreen.main?.visibleFrame

2. 设置大小

HStack {
  
}
.frame(width: window!.width / 2.0, height: window!.height / 1.5)

二. 汇总

struct Home: View {

	var window = NSScreen.main?.visibleFrame
  
  var body: some View {
    HStack {
      Text("Hello, World!")
    }
    .frame(width: window!.width / 2.0, height: window!.height / 1.5)
  }
}

Flask_RESTful解析常见类型请求数据

一. 前言

Flask_RESTful是一个Flask 扩展,它添加了快速构建 REST APIs 的支持。其请求解析接口是模仿 argparse 接口。它设计成提供简单并且统一的访问 Flask 中 flask.request 对象里的任何变量的入口。


二. 常见类型解析

1. 基本参数

  • 请求
{
  "username": "kuari",
  "info": "heihei"
}
  • 解析
parse = reqparse.RequestParser()
parse.add_argument('username', type = str)
parse.add_argument('info', type = str)
args = parse.parse_args()

2. 必选参数

使用参数required

  • 请求
{
  "username": "kuari",
  "info": "heihei"
}
  • 解析
parse = reqparse.RequestParser()
parse.add_argument('username', type = str, required = True)
parse.add_argument('info', type = str, required = True)
args = parse.parse_args()

3. 列表[string]

使用参数action = 'append'

  • 请求
{
  "username": "kuari",
  "info": [
    "handsome", "cheerful", "optimism"
  ]
}
  • 解析
parse = reqparse.RequestParser()
parse.add_argument('username', type = str, required = True)
parse.add_argument('info', type = str, action = 'append', required = True)
args = parse.parse_args()

4. 列表[dict]

使用参数action = 'append'

  • 请求
{
  "username": "kuari",
  "friends": [
    {
      "username": "tom",
      "age": 20
    },
    {
      "username": "jerry",
      "age": 20
    }
  ]
}
  • 解析
parse = reqparse.RequestParser()
parse.add_argument('username', type = str, required = True)
parse.add_argument('info', type = dict, action = 'append', required = True)
args = parse.parse_args()

5. JSON

  • 请求
{
  "username": "kuari",
  "info": {
    "character": "optimism",
    "age": 20
  }
}
  • 解析
parse = reqparse.RequestParser()
parse.add_argument('username', type = str, required = True)
parse.add_argument('info', type = dict, required = True)
args = parse.parse_args()

electron-builder踩坑系列---tray系统托盘

简述

窗口最小化或者关闭的情况下,进程未退出,需要通过系统托盘来查看,当然还需要托盘菜单。这里就用最简单的菜单实现,加上点击事件触发。

官方文档

const { app, Menu, Tray } = require('electron')

let tray = null
app.whenReady().then(() => {
  tray = new Tray('/path/to/my/icon')
  const contextMenu = Menu.buildFromTemplate([
    { label: 'Item1', type: 'radio' },
    { label: 'Item2', type: 'radio' },
    { label: 'Item3', type: 'radio', checked: true },
    { label: 'Item4', type: 'radio' }
  ])
  tray.setToolTip('This is my application.')
  tray.setContextMenu(contextMenu)
})

实现

let tray = null;
app.whenReady().then(() => {
  const iconUrl =
        process.env.NODE_ENV === "development"
  ? path.join(__dirname, "../build/favicon.ico")
  : path.join(__dirname, "favicon.ico");
  tray = new Tray(nativeImage.createFromPath(iconUrl));

  let trayMenuTemplate = [
    {
      label: "显示/隐藏",
      click: function() {
        return win.isVisible() ? win.hide() : win.show();
      }
    },
    {
      label: "退出",
      click: function() {
        app.quit();
      }
    }
  ];
  const contextMenu = Menu.buildFromTemplate(trayMenuTemplate);
  tray.setToolTip("kuari");
  tray.setContextMenu(contextMenu);
});

参考文档

electron-builder踩坑系列---禁止用户调整窗口大小

简述

当想为程序设定一个固定的窗口大小时候,需要限制用户对程序窗口的大小进行拖动调整。但是使用官方的设定时候,在mac平台上可以禁用调整,当在windows平台上时候却依然可以。所以为了兼容两者,可以选择使用无边框来实现。

官方文档

  • resizable Boolean (optional) - Whether window is resizable. 默认值为 true

实现

mac

win = new BrowserWindow({
  width: 800,
  height: 600,
  resizable: false
});

windows

  • 为了兼容,可选择都加上无边框
win = new BrowserWindow({
  width: 800,
  height: 600,
  frame: false,
  resizable: false
});

参考文档

SFTP部署报错解决记录

一. 前言

部署SFTP服务器,数次遇到几个报错,特此记录

二. 环境

  • 路径

    home
    └── tom
        └── uploads
  • 用户为tom

三. 报错

报错一

permission denied

报错二

bad ownership or modes for chroot directory component "/home"

四. 解决

以上两个报错,此处为统一解决。

  • 创建用户组
groupadd ftp
  • 将用户加入用户组
usermod -a -G ftp tom
  • 设置权限
chown root:ftp -R /home/tom
chown tom:ftp -R /home/tom/uploads
chmod 755 -R /home

H5检测手机摇一摇

一. 简介

要实现h5检测手机摇一摇动作可以直接调用h5原生api。但是在我的实践中发现在ios中限制条件比较多,体验还是有些区别的。

二. 如何监听

调用Window: devicemotion event即可实现监听。devicemotion事件以固定的时间间隔触发,并指示设备当时在接收的加速物理力量。 它还提供有关旋转速率的信息(如果有)。

function handleMotionEvent(event) {

    var x = event.accelerationIncludingGravity.x;
    var y = event.accelerationIncludingGravity.y;
    var z = event.accelerationIncludingGravity.z;

    // Do something awesome.
}

window.addEventListener("devicemotion", handleMotionEvent, true);

三. 安卓机

安卓机上直接按照如上即可实现。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>测试摇一摇</title>
</head>
<body>
<div class="phone">
  <div id="show">摇一摇</div>
</div>
</body>

<script>
function handleMotionEvent(event) {
  document.getElementById('show').innerHTML = '摇动中'
}

if (window.DeviceMotionEvent) {
  window.addEventListener("devicemotion", handleMotionEvent, false);
} else {
  alert("该浏览器不支持摇一摇功能");
}
</script>
</html>

四. iPhone

1. 限制

ios上限制有两条:

  • h5必须是https协议的
  • 必须用户点击授权才可以调用devicemotion

2. 授权

function getPermission() {
  if (
    typeof window.DeviceMotionEvent !== 'undefined' &&
    typeof window.DeviceMotionEvent.requestPermission === 'function'
  ) {
    window.DeviceMotionEvent.requestPermission()
      .then(function(state) {
        if ('granted' === state) {
          //用户同意授权
          
        } else {
          //用户拒绝授权
          alert('摇一摇需要授权设备运动权限,请重启应用后,再次进行授权!')
        }
      })
      .catch(function(err) {
        alert('error: ' + err)
      })
  }
}

直接调用该函数请求授权会导致报错:

error: NotAllowedError: Requesting device orientation or motion access requires a user gesture to prompt

需要用户主动去请求授权,因此此处需要将调用放到比如一个按钮上,让用户去点击请求授权。

<button onclick="getPermission()">请求授权</button>

3. 全部代码

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>测试摇一摇</title>
</head>
<body>
<div class="phone">
  <button onclick="getPermission()">请求授权</button>
  <div id="show"></div>
</div>
</body>

<script>
function handleMotionEvent(event) {
  document.getElementById('show').innerHTML = '摇动中'
}


function startListen() {
  if (window.DeviceMotionEvent) {
    window.addEventListener("devicemotion", handleMotionEvent, false);
  } else {
    alert("该浏览器不支持摇一摇功能");
  }
}

function getPermission() {
  if (
    typeof window.DeviceMotionEvent !== 'undefined' &&
    typeof window.DeviceMotionEvent.requestPermission === 'function'
  ) {
    window.DeviceMotionEvent.requestPermission()
      .then(function(state) {
        if ('granted' === state) {
          //用户同意授权
          startListen()
        } else {
          //用户拒绝授权
          alert('摇一摇需要授权设备运动权限,请重启应用后,再次进行授权!')
        }
      })
      .catch(function(err) {
        alert('error: ' + err)
      })
  }
}
</script>
</html>

Golang实现农历转换阳历

一. 前言

因为项目需求,需要去检测用户的农历生日。虽然后来找到了合适的库,但是首先先解释下农历的定义,也是去了解才知道,原来农历不是阴历。

农历属于阴阳合历,其年份分为平年和闰年。平年为十二个月,闰年为十三个月。月份分为大月和小月,大月三十天,小月二十九天,其平均历月等于一个朔望月。

二. 环境

  • Go 1.16
  • github.com/nosixtools/solarlunar 0.0.0

三. 库

github.com/nosixtools/solarlunar

该库支持1900~2049年。所以项目要跑到2049年后的童鞋就要注意......

当然,该库还支持阳历转农历、节假日计算等,有兴趣大家可以自行去了解下。

四. 使用

1. 判断闰年

该库不支持闰年判断,所以需要自己去实现闰年的判断,其参数类型为Boolean

func IsALeapYear(year int) (result bool) {
	if year%4 == 0 && year%100 != 0 || year%400 == 0 {
		result = true
		return
	}
	return
}

2. 转换

需要转换的阳历日期格式是固定的,是2006-01-02。此处以农历2021-07-17为例。

func main() {
	lunarDate := "2021-07-17"
	fmt.Println(solarlunar.LunarToSolar(lunarDate, IsALeapYear(time.Now().Year())))
}

输出为:

2021-08-24

五. 全部代码

package main

import (
	"fmt"
	"time"

	"github.com/nosixtools/solarlunar"
)

func main() {
	lunarDate := "2021-07-17"
	fmt.Println(solarlunar.LunarToSolar(lunarDate, IsALeapYear(time.Now().Year())))
}

func IsALeapYear(year int) (result bool) {
	if year%4 == 0 && year%100 != 0 || year%400 == 0 {
		result = true
		return
	}
	return
}

IOS监听上下左右滑动手势

一. 前言

IOS监听手势使用的方法为UISwipeGestureRecognizer

二. 添加手势监听

let gesture = UISwipeGestureRecognizer()
gesture.addTarget(self, action: #selector(yourSelector(gesture:)))
gesture.direction = .left // .left左滑 .right右滑 .up上滑 .down下滑
self.addGestureRecognizer(gesture)

三. 添加响应事件

@objc private func leftPushEvent(){
  print("响应...")
}

四. 模板

把上面的整合起来,基本可以按照这个模板来写。

@objc private func leftPushEvent(){
  print("响应...")
}

let gesture = UISwipeGestureRecognizer()
gesture.addTarget(self, action: #selector(leftPushEvent(gesture:)))
gesture.direction = .left
self.addGestureRecognizer(gesture)

五. 参考文档

Linux分享文件?快速创建静态文件服务器

一. 需求

Linux对于开发者来说极其友好,但是由于国内主流办公产品相关的生态较为匮乏,因此如何使用Linux去分享文件是一件十分头疼的问题。

对于这个问题,可以直接使用静态文件服务器解决部分需求,如下介绍几个常见方法。

二. 语言类

1. Python

对于Python来说,可以直接使用内置的库来实现。

  • python2

    python -m SimpleHTTPServer 8000
  • Python3

    python -m http.server 8000

2. Node.js

node生态内有一个项目http-server,直接V8引擎带你飞。

  1. 安装
  • Npm
npm install --global http-server
  • Homebrew
brew install http-server
  1. 运行
http-server [path] [options]

例如:

cd exmaple/
http-server
  1. 项目仓库地址

https://github.com/http-party/http-server

三. 服务类

  1. Nginx/Apache

NginxApache本身可用于静态文件服务器,这就需要用户直接在本地安装。

当然,nginx需要注意配置一下,打开索引:

server {
	listen	80;
	...
	
	location / {
		root /usr/share/nginx/html;
		autoindex on;
	}
}
  1. Docker

使用Docker其实也是使用如Nginx来实现静态文件服务器,但是容器化在该场景存在几大优势:

  • 即开即用
  • 环境隔离

相对于直接安装Nginx或者Apache,更推荐使用Docker

SwiftUI项目实现搜索功能

一. 实现

1. 创建变量

@State var search: String = ""

2. 过滤

此处过滤条件为判断元素是否包含搜索的文本。

<Your-Array>.filter({"\($0)".contains(search.lowercased()) || search.isEmpty})

二. 汇总

struct DataList: View {

  @State var search: String = ""
  @Binding var dataList: [Item]

  var dataSearchFilterList: [Item] {
    dataList.filter({"\($0)".contains(search.lowercased()) || search.isEmpty})
  }

  var body: some View {
    if dataSearchFilterList.isEmpty {
      Text("搜索不到...")
    } else {
      ... // 展示搜索结果
    }
  }
  
}

go中json解析报错invalid character '\\b' after top-level value

报错

Golangjson解析时报错:

invalid character '\\b' after top-level value

代码如下:

json.Unmarshal([]byte(result), &response)

分析与排错

首先将result打印出来,发现并无异常,其标点符号也没有问题。

然后查看网上现有解决方案的帖子基本试了下,起码对于我来说并不适用,概括下方案:

  1. 遍历然后过滤,最后重组;
  2. 遍历,使用SetEscapeHTML(false)禁用转义符;
  3. 编码;
  4. ...

最后对比代码中获取到的字符产长度和手动复制所见的字符串的长度,发现确实代码中字符长度不同,其长度是80,而手动复制的字符串的长度是72。

解决

strings.ReplaceAll(result, "\b", "")

就挺简单的......

v-charts添加图表标题

一. 场景

使用 v-charts 做数据可视化,需要给图表添加标题。

二. 解决方法

v-charts本身并没有提供显示标题的配置,顾需要引入 echartstitle

三. 实现

1. 引入title

import "echarts/lib/component/title";

2. 添加标题配置

this.chartTitle = {
  text: "平台用户与创客数量对比图",
  textStyle: {
    fontWeight: 600,
    color: "white"
  }
};

3. 使用

<!--其中...代表其他配置-->
<ve-bar ... :title="chartTitle" />

4. 完整实现

<template>
  <ve-bar ... :title="chartTitle" />
</template>

<script>
import "echarts/lib/component/title";

export default {
  data() {
    this.chartTitle = {
      text: "平台用户与创客数量对比图",
      textStyle: {
        fontWeight: 600,
        color: "white"
      }
    };
  }
};
</script>

四. echarts配置手册

五. 参考文章

electron-builder踩坑系列---无边框

简述

虽然并非出于直接实现无边框的需求,但是为了实现窗口的固定大小(设置固定值不可调整情况下,mac是没问题的,但是windows一直可以调整大小)而设置了无边框。

官方文档

const { BrowserWindow } = require('electron')
let win = new BrowserWindow({ width: 800, height: 600, frame: false })
win.show()

实现

// background.js

win = new BrowserWindow({
  width: 800,
  height: 600,
  frame: false
});

参考文档

Flask_Restful视图函数模块化

Restful作为目前流行的api设计规范,在flask上也有较好的实践,即为flask_restful。我们在使用flask_restful的时候,当代码量达到一定程度,需要将视图函数模块化。然而在Flask之前一直使用Blueprint模块化,那么flask_restful如何模块化呢?底下就来瞅瞅!

一. 当前架构

server/
├── __init__.py
├── commands.py
├── config.py
└── views.py
  • __init__.py:将该文件夹标示为一个模块
  • commands.py:额外命令,如初始化数据库
  • config.py:配置文件
  • views.py:视图函数

此为一个简单的flask项目架构,我们可以将flask中的各个功能拆分出来,分给不同文件,最后在__init__.py导入。

from flask import Flask
from flask_cors import CORS
from flask_restful import Api

app = Flask('server')
app.config.from_pyfile('config.py')

CORS(app)

api = Api(app)

from server import commands, views

二. 初步模块化

现在将视图函数改为一个文件夹views,然后在该文件夹下放入拆分后的文件。这里以登录和获取用户信息接口为例,架构就变成了下面这样。

server/
├── __init__.py
├── commands.py
├── config.py
└── views
    ├── __init__.py
    ├── login.py
    └── info.py

此处的__init__.py文件功能如上,是将文件夹views标示为模块,但是此处的__init__py是个空白文件,但是server文件夹中的__init__.py的引入需要改变。

...
from server import commands
from server.views import login, info

三. 最终模块化

此时还不够,例如登录和获取用户信息接口是User模块下的,而获取文章标题接口是Article模块的下的,因此我们继续分。

server/
├── __init__.py
├── commands.py
├── config.py
└── views
    ├── Aricle
    │   ├── __init__.py
    │   └── title.py
    ├── User
    │   ├── __init__.py
    │   ├── info.py
    │   └── login.py
    └── __init__.py

server文件夹中的__init__.py的引入继续改变。

...
from server import commands
from server.views.User import login, info
from srever.views.Article import title

四. 结语

至此,关于flask_restful的视图函数模块化结束,若有后续改进会继续分享,若大家有更好的方式请务必分享一下。

当时为了解决这个问题查了很久,但是要么是介绍blueprint的,要么就是flask_restful插件入门,简直刺激...

SwiftUI项目判断是否为暗黑模式

一. 实现

@Environment(\.colorScheme) var colorScheme

var isLight: Bool {
  colorScheme == .light
}

二. 调用

Text("Hello, World !")
	.foregroundColor(isLight ? Color.red : Color.green)

三. 完整例子

import SwiftUI

struct CheckIsLight: View {
    
    @Environment(\.colorScheme) var colorScheme

    var isLight: Bool {
      colorScheme == .light
    }
    
    var body: some View {
        Text("Hello, World !")
            .foregroundColor(isLight ? Color.red : Color.green) // 此处使用isLght实现根据暗黑模式切换字体颜色
    }
}

struct CheckIsLight_Previews: PreviewProvider {
    static var previews: some View {
        CheckIsLight()
    }
}

基于python3和js的前后端aes加解密

一. 简述

在特定敏感数据的场景需要加密,一开始采用rsa加密,但是rsa加密对性能要求较高,在解密时候对于数据量限制较大,导致加密传输的数据量上限较低。而采用Base64虽然简单明了但是解密过于简单。因此采用折中的对称加密aes

aes加密需要前后端加密类型相同,因此此处采用CTR,其对加密文本没有长度限制。

二. 前端实现

let crypto = require("crypto")

export function aesEncrypted(key, text) {
  let iv = Buffer.concat([ crypto.randomBytes(12), Buffer.alloc(4, 0) ])
  let cipher = crypto.createCipheriv("aes-128-ctr", key, iv)
  return iv.toString('hex') + cipher.update(text, 'utf8', 'hex') + cipher.final('hex')
}

三. 后端实现

def aesDecryption(key_: str, de_text: str) -> str:
    """
    aes解密函数
    :param key_: aes的key
    :param de_text: aes加密的密文
    :return: 解密的文本
    """
    ct = codecs.decode(de_text.encode(), 'hex')
    counter = Counter.new(32, prefix = ct[:12], initial_value = 0)
    cipher = AES.new(key_.encode(), AES.MODE_CTR, counter = counter)
    return cipher.decrypt(ct[16:]).decode()

四. 示例代码

前端

let crypto = require("crypto")

export function aesEncrypted(key, text) {
  let iv = Buffer.concat([ crypto.randomBytes(12), Buffer.alloc(4, 0) ])
  let cipher = crypto.createCipheriv("aes-128-ctr", key, iv)
  return iv.toString('hex') + cipher.update(text, 'utf8', 'hex') + cipher.final('hex')
}

后端

from base64 import b64encode
from Crypto.Cipher import AES
from Crypto.Util import Counter
from random import randint
import codecs
import hashlib


def convert_to_md5(info: str) -> str:
    """
    md5加密
    :param info: 需要加密的内容
    :return: md5加密密文
    """
    md5 = hashlib.md5()
    md5.update(info.encode('utf-8'))
    return md5.hexdigest()


def aesCreateKey() -> str:
    """
    生成aes加密的key,key的长度必须16位
    :return: 返回key的base64密文
    """
    en_key = convert_to_md5(str(randint(100000, 999999)))[8:-8]
    return b64encode(en_key.encode()).decode()


def aesDecryption(key_: str, de_text: str) -> str:
    """
    aes解密函数
    :param key_: aes的key
    :param de_text: aes加密的密文
    :return: 解密的文本
    """
    ct = codecs.decode(de_text.encode(), 'hex')
    counter = Counter.new(32, prefix = ct[:12], initial_value = 0)
    cipher = AES.new(key_.encode(), AES.MODE_CTR, counter = counter)
    return cipher.decrypt(ct[16:]).decode()

五. 参考文档

gin中间件和鉴权

一. 前言

gin的中间件的使用场景非常广泛,此处主要介绍如何使用其来完成常见场景下的鉴权。

二. 官方文档

官方文档列出了如下几种使用方式:

三. 不同场景的鉴权实现

1. api key

对于api key的方式需要设置白名单,对白名单外的请求进行token检测。此中间件在处理请求被处理之前对请求进行拦截,验证token,因此可在此处利用gin.Context来设置上下文,如请求所属用户的用户信息等。

package middleware

import (
	"fmt"
	"net/url"
	"strings"

	"github.com/gin-gonic/gin"
)

func whiteList() map[string]string {
	return map[string]string{
		"/ping": "GET",
	}
}

func withinWhiteList(url *url.URL, method string) bool {
	target := whiteList()
	queryUrl := strings.Split(fmt.Sprint(url), "?")[0]
	if _, ok := target[queryUrl]; ok {
		if target[queryUrl] == method {
			return true
		}
		return false
	}
	return false
}

func Authorize() gin.HandlerFunc {
	return func(c *gin.Context) {

		type QueryToken struct {
			Token string `binding:"required,len=3" form:"token"`
		}

		// 当路由不在白名单内时进行token检测
		if !withinWhiteList(c.Request.URL, c.Request.Method) {
			var queryToken QueryToken
			if c.ShouldBindQuery(&queryToken) != nil {
				c.AbortWithStatusJSON(200, gin.H{
					"code": 40001,
				})
				return
			}

			c.Set("role", "user")
		}

		c.Next()
	}
}

2. 路由权限

1)说明

对于请求的处理,需要去验证是否对其请求的路径拥有访问权限。

首先看一下gin的路由设置:

func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes

其参数为...HandlerFunc,其解释为:

type HandlerFunc func(*Context)
HandlerFunc defines the handler used by gin middleware as return value.

所以此处可以通过定制中间件的方式实现一个路由权限处理。

当然此处的权限处理比较简单,使用角色直接去判断权限。如分为两个角色,管理员admin和普通用户user

不过此处实现有个前提条件,就是如何拿到用户的角色呢?此处需要在上一步(api key)的实现中加上利用gin.Context设置角色:

c.Set("role", "admin") // 可见上一步的代码,当然此处只是为了演示设置固定值

然后在中间件中拿到角色并进行判断。

2)路由权限中间件

package middleware

import (
	"errors"
	"fmt"

	"github.com/gin-gonic/gin"
)

func Permissions(roles []string) gin.HandlerFunc {
	return func(c *gin.Context) {

		permissionsErr := func() error {

			// 获取上下文中的用户角色
			roleValue, exists := c.Get("role")
			if !exists {
				return errors.New("获取用户信息失败")
			}
			role := fmt.Sprint(roleValue)

			// 判断请求的用户的角色是否属于设定角色
			noAccess := true
			for i := 0; i < len(roles); i++ {
				if role == roles[i] {
					noAccess = false
				}
			}
			if noAccess {
				return errors.New("权限不够")
			}

			return nil

		}()
		if permissionsErr != nil {
			c.AbortWithStatusJSON(200, gin.H{
				"code": 40001,
			})
			return
		}

		c.Next()
	}
}

3)使用

在设置路由时候,添加该中间件,并设置白名单。

r.POST("/todo", middleware.Permissions([]string{"admin"}), views.AddTodo) // 添加中间件将会验证角色
r.PUT("/todo", views.ModifyTodo) // 未添加中间件则不会验证角色

go-Redis的发布与订阅

一. 前言

在数据量较小的情况下,可以使用Redis来实现消息的发布与订阅,来代替KafkaKafka对于数据量大的场景下性能卓越,但是对于如此小场景时候,不仅运维成本提升,还用不上多少性能。

不过使用Redis的另一个弊端是消息不能堆积,一旦消费者节点没有消费消息,消息将会丢失。因此需要评估当下场景来选择适合的架构。

此处使用go-redis来实现Redis的发布与订阅。

二. 官方文档

官方文档有较为完整的例子:

pubsub := rdb.Subscribe(ctx, "mychannel1")

// Wait for confirmation that subscription is created before publishing anything.
_, err := pubsub.Receive(ctx)
if err != nil {
	panic(err)
}

// Go channel which receives messages.
ch := pubsub.Channel()

// Publish a message.
err = rdb.Publish(ctx, "mychannel1", "hello").Err()
if err != nil {
	panic(err)
}

time.AfterFunc(time.Second, func() {
	// When pubsub is closed channel is closed too.
	_ = pubsub.Close()
})

// Consume messages.
for msg := range ch {
	fmt.Println(msg.Channel, msg.Payload)
}

三. 代码实现

分步讲解下具体实现代码。

1. 连接redis

func redisConnect() (rdb *redis.Client) {

	var (
		redisServer string
		port        string
		password    string
	)

	redisServer = os.Getenv("RedisUrl")
	port = os.Getenv("RedisPort")
	password = os.Getenv("RedisPass")

	rdb = redis.NewClient(&redis.Options{
		Addr:     redisServer + ":" + port,
		Password: password,
		DB:       0, // use default DB
	})

	return
}

2. 发布消息

func pubMessage(channel, msg string) {
	rdb := redisConnect()
	rdb.Publish(context.Background(), channel, msg)
}

3. 订阅消息

func subMessage(channel string) {
	rdb := redisConnect()
	pubsub := rdb.Subscribe(context.Background(), channel)
	_, err := pubsub.Receive(context.Background())
	if err != nil {
		panic(err)
	}

	ch := pubsub.Channel()
	for msg := range ch {
		fmt.Println(msg.Channel, msg.Payload)
	}
}

四. 完整案例

此处分为一个发布节点和一个订阅节点来实现了简单的发布与订阅。

1. 消息发布节点

package main

import (
	"context"
	"fmt"
	"os"

	"github.com/go-redis/redis/v8"
)

func redisConnect() (rdb *redis.Client) {

	var (
		redisServer string
		port        string
		password    string
	)

	redisServer = os.Getenv("RedisUrl")
	port = os.Getenv("RedisPort")
	password = os.Getenv("RedisPass")

	rdb = redis.NewClient(&redis.Options{
		Addr:     redisServer + ":" + port,
		Password: password,
		DB:       0, // use default DB
	})

	return
}

func pubMessage(channel, msg string) {
	rdb := redisConnect()
	rdb.Publish(context.Background(), channel, msg)
}

func main() {
	channel := "hello"
	msgList := []string{"hello", "world"}

  // 此处发了两个消息
	for _, msg := range msgList {
		pubMessage(channel, msg)
		fmt.Printf("已经发送%s到%s\n", msg, channel)
	}
}

2. 消息订阅节点

package main

import (
	"context"
	"fmt"
	"os"

	"github.com/go-redis/redis/v8"
)

func redisConnect() (rdb *redis.Client) {

	var (
		redisServer string
		port        string
		password    string
	)

	redisServer = os.Getenv("RedisUrl")
	port = os.Getenv("RedisPort")
	password = os.Getenv("RedisPass")

	rdb = redis.NewClient(&redis.Options{
		Addr:     redisServer + ":" + port,
		Password: password,
		DB:       0, // use default DB
	})

	return
}

func subMessage(channel string) {
	rdb := redisConnect()
	pubsub := rdb.Subscribe(context.Background(), channel)
	_, err := pubsub.Receive(context.Background())
	if err != nil {
		panic(err)
	}

	ch := pubsub.Channel()
	for msg := range ch {
		fmt.Println(msg.Channel, msg.Payload)
	}
}

func main() {
    channel := "hello"
    subMessage(channel)
}

五. 运行结果

1. 消息发布节点输出

go_redis_pub

2. 消息订阅节点输出

go_redis_sub

SwiftUI项目复制字符串到剪切板

一. 前言

这是个比较坑的问题,我一开始开发的是macos项目,到网上搜的方案基本都是使用UIPasteboard方法,但是偏偏用不了。

后来开发ios项目,用macos的就不行,发现UIPasteboard的可行,所以这里需要清楚的是,ios和macos的复制方法是不同的......

二. MacOS

1. 实现

func copyToClipBoard(textToCopy: String) {
  let pasteBoard = NSPasteboard.general
  pasteBoard.clearContents()
  pasteBoard.setString(textToCopy, forType: .string)
}

2. 调用

copyToClipBoard(textToCopy: "Hello,World!")

三. IOS

1. 实现

UIPasteboard.general.setValue(<Your-String>, forPasteboardType: kUTTypePlainText as String)

2. 调用

UIPasteboard.general.setValue("Hello,World!", forPasteboardType: kUTTypePlainText as String)

前端文件花式直传OSS!后端:那我走?

一. 简介

前端还在传文件给后端吗?你们的服务器扛得住吗?什么......老板砸钱加机器?!告辞!/狗头

前后端文件传输涉及数据较大,往往会成为很多项目的性能瓶颈。常见的传输方式也有不少,相对来说,OSS直传能够减轻很大压力。

本文我们来列举下常见的和oss直传的几种传输方式,并列举其优劣。

二. 常见方式

1. 表单上传

表单上传文件是最常见的方式,前后端开发小伙伴都很轻松,前端哐哐传,后端哐哐收就成了。其过程如下图所示。

form上传

优势:

  • 简单方便,开发量小
  • 前后端原生支持,无需额外第三方库支持

2. Base64上传

Base64方式上传文件,多常见于小文件,如小图片等,前后端都可直接使用String类型发送和接收。不过在前端,需要将文件转成base64数据,不仅会增加些性能消耗,还会增加传输数据的体积。而对于后端,如果并不是想直接存储base64数据,也还需要将其转成文件再存储,也会增加后端的性能消耗。

该上传方式可适用于Resetful Api,也可适用于文件的加密、回调接口携带文件等等。其过程如下图所示。

base64上传

优势:

  • 适用于Resetful Api,可用于加密、回调等场景,较为灵活

劣势:

  • 前后端文件与base64数据转换需要消耗性能
  • 只适用于小文件

三. OSS直传

此处的OSS直传方案都是使用的阿里云的OSS产品,以下将介绍三个方案,可适用于不同的场景。

1. Browser.js SDK上传

该方案可在前端直接通过browser.js上传文件到OSS,可分成三步:

  1. 前端使用SDK直传OSS
  2. 前端上传完成后请求后端,通知上传完成
  3. 后端检测OSS上该文件是否存在(可选)

其流程如下图所示。

browser直传

Browser.js的方式需要前端安装阿里云的库ali-oss,然后在前端调用。直传还需要OSS账户的Key和Secret,因此为了安全考虑,需要建立RAM账户,然后前端向后端先请求一个STS临时访问凭证来完成直传,其流程如下图所示。

browser直传2

2. Javascript客户端签名直传

Javascript客户端签名直传,需要先从后端获取临时签名,其流程与上一步的browser.js方案大致相同,不同点在于:

  • 无需第三方库支持,直接表单上传
  • 原生支持后端上传回调(下一步骤讲述)

其流程如下图所示。

javascript签名直传

不过该方案做下来,我感觉最大的问题是权限配置有点麻烦....../泪眼

3. 服务端签名直传并设置上传回调

该方案其实是上面方案——Javascript客户端签名直传的升级版本,其加上了后端的上传回调功能。不错呦~

前端需要改动的很少,只需要在请求参数中加上callback参数即可,该参数为后端加密,在签名请求的响应中一起返回回来,内加密了后端回调接口。在前端直传完成后,后端回调接口将会接收到相关文件参数,包括文件路径、大小、类型等。最后OSS会将回调接口response转发给前端,响应直传OSS的请求。

其流程如下图所示。

后端签名直传且回调

四. 对比

传统方式相比直传OSS,相对来说有三个缺点:

  • 上传慢:用户数据需先上传到应用服务器,之后再上传到OSS。网络传输时间比直传到OSS多一倍。如果用户数据不通过应用服务器中转,而是直传到OSS,速度将大大提升。而且OSS采用BGP带宽,能保证各地各运营商之间的传输速度。
  • 扩展性差:如果后续用户多了,应用服务器会成为瓶颈。
  • 费用高:需要准备多台应用服务器。由于OSS上传流量是免费的,如果数据直传到OSS,不通过应用服务器,那么将能省下几台应用服务器。

当然,对于规模较小、成本较低的项目来说,常见的上传方式还是适合的,毕竟没有最好的,只有最适合的。

五. 参考文档

docker中php上传大小限制

问题

使用docker运行的wordpress,有报错The uploaded file exceeds the upload_max_filesize directive in php.ini

过程

按照一般的方式去修改文件php.ini

upload_max_filesize = 30M
post_max_size = 30M

容器中有两个php.ini文件:

  • php.ini-development
  • php.ini-production

都修改之后却并不生效。

解决

在文件夹conf.d中添加文件uploads.ini

file_uploads = On
memory_limit = 64M
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 600

文件夹conf.d位置需要看具体环境:

php -i | grep php.ini

然后在其目录下找到文件夹conf.d

也可以通过在运行容器时候直接将该文件映射进入。

electron-builder踩坑系列---lowdb本地存储

简述

electron应用在开发中,需要存储数据到本地,经历了两个版本,其方案都不太一样。

一开始考虑使用cookie,在开发过程中没有任何问题,但是编译之后去使用,发现无法操作cookie。原来在开发中直接js操作的的浏览器的cookie,而在electron中需要交由底层的nodejs去操作本地的cookie,官方说法是通过Sessioncookies属性来访问Cookies的实例。但是我在实践过程中确实没有成功,然后随着需求变化,数据量变大,就直接放弃了这个方案。

当时用的electron版本是9.0.0,之后才用的方案是直接文件存储,即直接fs读与写,毫无问题。就是注意配置文件的存放位置。

现在再去使用electron,版本已经到了11.0.0。当我去使用fs读写时直接给我报错fs.writeFile is not a function,经过一天多的排错和查找,最终放弃该方案,当然我并没有找到原因和解决方案。最后决定使用lowdb去实现存储。

官方文档

Github仓库

https://github.com/typicode/lowdb

官方介绍

Small JSON database for Node, Electron and the browser. Powered by Lodash. ⚡

实现

安装

npm install lowdb

增删改查实例

const low = require('lowdb')
const FileSync = require('lowdb/adapters/FileSync')

const adapter = new FileSync('db.json')
const db = low(adapter)

// 默认初始化配置文件中
db.defaults({ posts: [], user: {}, count: 0 })
  .write()

// 增
db.get('posts')
  .push({ id: 1, title: 'lowdb is awesome'})
  .write()

// 删
db.get('posts')
  .remove({ title: 'low!' })
  .write()

// 改
db.set('user.name', 'typicode')
  .write()

// 查
db.get('posts[0].title')
  .value()

加密

比较重要的是,lowdb本身支持对于配置文件的加密,但是需要自己去实现写加解密的函数。

const adapter = new FileSync('db.json', {
  serialize: (data) => encrypt(JSON.stringify(data)),
  deserialize: (data) => JSON.parse(decrypt(data))
})

如下加解密方式可以参考下:

const algorithm = "aes-256-ctr";
const ENCRYPTION_KEY = "<ENCRYPTION_KEY>"

const IV_LENGTH = 16;

// 加密
function encrypt(text) {
  let iv = crypto.randomBytes(IV_LENGTH);
  let cipher = crypto.createCipheriv(
    algorithm,
    Buffer.from(ENCRYPTION_KEY, "hex"),
    iv
  );
  cipher.setAutoPadding(true);
  let encrypted = cipher.update(text);
  encrypted = Buffer.concat([encrypted, cipher.final()]);
  return iv.toString("hex") + ":" + encrypted.toString("hex");
}

// 解密
function decrypt(text) {
  let textParts = text.split(":");
  let iv = Buffer.from(textParts.shift(), "hex");
  let encryptedText = Buffer.from(textParts.join(":"), "hex");
  let decipher = crypto.createDecipheriv(
    algorithm,
    Buffer.from(ENCRYPTION_KEY, "hex"),
    iv
  );
  let decrypted = decipher.update(encryptedText);
  decrypted = Buffer.concat([decrypted, decipher.final()]);
  return decrypted.toString();
}

Swift UI项目调用core data

一. 前言

这篇文章是我写的第一篇Swift UI相关的笔记吧。

这真的难搞哦,毕竟新出来的。国内文档真的是少之又少,国外的文档也真不多,基本搜出来的都是UIKit的。官方文档,也是一言难尽,基本就处于,我要实现一个功能,然后去搜一下各种帖子,筛选掉无用的帖子,找到有用的点,当然也是时常根本找不到有用的帖子,然后就要去油管上看各种教程,然后发现:哦!原来还有这个方法!接着去官方文档搜一下,看一下属性,自己调用调用......

光是这个core data我就折腾了近一周,最后发现,原来还是官方好呀.....

吐槽一下自学swift ui,现在进入正题了。

core data呢,是苹果官方的本地数据库,但是其存储的文件其实是sqlite文件。其可以通过icloud实现备份和同步,当然icloud我会额外写一篇文档来详细讲述的(又是一把辛酸泪...)。

二. 环境

如下是我当前的环境:

  • 系统:macOS Big Sur 11.4
  • Xcode:Version 12.5 (12E262)
  • Swift:5.4

三. 操作步骤

1. 创建项目

在创建项目的时候,可以直接选择Use Core Data选项,xcode会直接在ContentView.swift中生成一个相关demo。

create_swiftui_project

2. 查看相关文件

创建之后,查看文件目录,相对于不选择Use Core Data,会多出如下几个文件:

  • .xcdatamodeId
  • Persistence.swift

3. 创建core data表

点击<Your-Project-Name>.xcdatamodeId文件,进入页面。

可以看到页面内有默认的CONFIGURATIONSDefault,默认的ENITITIESItem,可以理解为分别对应sql中的库和表。

swiftui_core_data_page

然后点击Item,可以看到其字段,有默认的timestamp字段。

若要新增ENTITIES(表),点击底部的Add Entity按钮即可。

若要新增Attributes(字段),点击右侧Attributes中的+即可,注意字段要选择类型。

比如,此处以默认的Item为例,新增usernameage两个字段。

swiftui_core_data_create_attributes

4. 代码层面操作core data

1)查看

@Environment(\.managedObjectContext) private var viewContext

@FetchRequest(
  sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
  animation: .default)
private var items: FetchedResults<Item>

// body中便利items即可

2)新增

private func addItem() {
  withAnimation {
    let newItem = Item(context: viewContext)
    newItem.timestamp = Date()

    do {
      try viewContext.save()
    } catch {
      // Replace this implementation with code to handle the error appropriately.
      // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
      let nsError = error as NSError
      fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
    }
  }
}

如上是xcode自动生成的新增函数,若是要自定义添加字段可以这样改动下:

// 此处按照如上添加Attributes修改,具体修改按照项目具体情况
private func addItem(username: String, age: Int16) {
  withAnimation {
    let newItem = Item(context: viewContext)
    newItem.username = username
    newItem.age = age
    newItem.timestamp = Date()

    do {
      try viewContext.save()
    } catch {
      // Replace this implementation with code to handle the error appropriately.
      // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
      let nsError = error as NSError
      fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
    }
  }
}

3)删除

private func deleteItems(offsets: IndexSet) {
  withAnimation {
    offsets.map { items[$0] }.forEach(viewContext.delete)

    do {
      try viewContext.save()
    } catch {
      // Replace this implementation with code to handle the error appropriately.
      // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
      let nsError = error as NSError
      fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
    }
  }
}

4)汇总

在默认生成代码中,有一个toolbar,其在macos中可以生效,但是在ios中只有EditionButton()可以使用,为了方便演示,此处新增一个Button来添加数据。

其次有点要声明下,在xcode中写代码时,右侧的canvas会实时渲染,列表中出现的数据并不是core data中的数据,而是默认生成的Persistence.swift中生成的演示数据,只能看看,不能当真。只有在模拟器/实体机编译运行时才能操作core data

如下为修改过后的ContentView.swift文件:

//
//  ContentView.swift
//  HelloKuari
//
//  Created by Kuari on 2021/6/5.
//

import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext

    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
        animation: .default)
    private var items: FetchedResults<Item>

    var body: some View {
        
        VStack {
            List {
                ForEach(items) { item in
                    Text("Tom: \(item.username!) age: \(item.age) time :  \(item.timestamp!, formatter: itemFormatter)")
                }
                .onDelete(perform: deleteItems)
            }
            
            // 新增一个按钮来添加数据
            Button(action: {
                addItem(username: "tom", age: 12)
            }, label: {
                Text("Add Item")
            })
        }

    }

    private func addItem(username: String, age: Int16) {
        withAnimation {
            let newItem = Item(context: viewContext)
            newItem.username = username
            newItem.age = age
            newItem.timestamp = Date()

            do {
                try viewContext.save()
            } catch {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }

    private func deleteItems(offsets: IndexSet) {
        withAnimation {
            offsets.map { items[$0] }.forEach(viewContext.delete)

            do {
                try viewContext.save()
            } catch {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
}

private let itemFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateStyle = .short
    formatter.timeStyle = .medium
    return formatter
}()

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
    }
}

然后点击左侧顶部的运行按钮,编译运行。

一开始是空白一片,点击Add Item按钮之后,便会开始添加数据。

swiftui_core_data_add_item_demo

对于记录右滑即可删除,其为ListonDelete方法。

四. 结语

该文章是面向新手的,也是记录下我踩过的坑,因为目前文档匮乏,身边也没swift的开发小伙伴儿,只能靠自己摸索,若有大佬有更好的方法,真的还请不吝赐教。

后面持续记录踩坑中......

flask_restful限制request字段长度

一. 前言

当前产品遇到一个报错,就是接口收到请求没有限制请求字段长度,导致字段长度超过数据库对应字段长度,直接报了500。因此也对此有些新的需求,需要在后端限制请求字段最大长度。

二. 环境

  • 开发语言:Python 3.7
  • 后端框架:Flask 1.1.1
  • 插件:Flask-RESTful 0.3.8

三. 实现

def field_max_limit(max_length):
    def validate(s):
        if type(s) != str:
            raise ValidationError("The field must be String.")

        if len(s) <= max_length:
            return s
        raise ValidationError("The field cannot exceed %i characters." % max_length)

    return validate

# 解析请求参数时候验证长度
parse.add_argument('username', type = field_max_limit(5), required = True)

四. 示例

from flask import Flask
from flask_restful import Api, Resource, reqparse
from werkzeug.routing import ValidationError

app = Flask(__name__)
api = Api(app)


def field_max_limit(max_length):
    def validate(s):
        if type(s) != str:
            raise ValidationError("The field must be String.")

        if len(s) <= max_length:
            return s
        raise ValidationError("The field cannot exceed %i characters." % max_length)

    return validate


class Login(Resource):

    def post(self):
        parse = reqparse.RequestParser()
        parse.add_argument('username', type = field_max_limit(5), required = True)
        parse.add_argument('password', type = field_max_limit(20), required = True)
        args = parse.parse_args()

        print({
            'username': args.username,
            'password': args.password
        })

        return {
            'code': 20000
        }


api.add_resource(Login, '/login')

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.