hunter-ji / blog Goto Github PK
View Code? Open in Web Editor NEWMy Blog.
My Blog.
为了前后端传输数据的安全性,需要对数据进行加密。因此选定使用非对称加密,此处为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;
}
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.
以要在日历系统和时区中计算的单位(例如年、月、日、小时和分钟)指定的日期或时间。
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!
}
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中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(
...
)
...
}
Rapidly build modern websites without ever leaving your HTML.
Tailwind CSS可以快速建立现代网站,而无需离开HTML。其特性是原子化,很像的BootStrap
的css。
通俗点解释就是,其封装了很多独立的css样式,只需要在html中添加class
即可调用,而不需要去从头写css样式。
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
添加tailwindcss
和autoprefixer
到PostCSS
配置。大部分情况下作为postcss.config.js
文件放在项目的顶级路径下。其也能作为.postcssrc
文件,或者使用postcss
键放在package.json
文件中。
// postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
}
}
如果想自定义安装,当使用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: [],
}
创建styles.css
文件。
/* ./your-css-folder/styles.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
引入该文件。
import "./styles.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: []
};
由于其样式属性巨多,此处只举几例作简要说明,讲解基础用法。在开始不熟悉的情况下,要开着其手册查询。
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>
Class | 解释 |
---|---|
p-0 | padding: 0px; |
p-5 | padding: 1.25rem; |
pl-1 | padding-left: 0.25rem; |
... | ... |
<!--示例-->
<div class="p-5"></div>
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>
<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>
在vue2中不允许子组件直接修改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
}
}
<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>
<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
时,其默认时区并非使用者所在时区,需要进行修改。对于单个容器,当前修改有几种常见方式,比如直接映射宿主机时区到容器内,而本文介绍的为使用Dockerfile
来直接修改镜像时区。此处仅以常见几个基础容器为例来介绍。
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
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
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
,然后修改时区。
打开文件info.plist
,在空白处右击,选择Add Row
,输入选择Privacy - Face ID Usage Description
,然后在value
中写入我们需要验证您的身份以保护数据
。
打开ContentView.swift
文件,开始如下操作。
import LocalAuthentication
@State private var isUnlocked = false
isUnlocked
为是否解锁,true
表示验证完成,已解锁,false
表示验证失败,未解锁。
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 {
// 没有生物识别
}
}
VStack {
if self.isUnlocked {
Text("Unlocked")
} else {
Text("Locked")
}
}
.onAppear(perform: authenticate) // 该方法调用生物识别验证函数
最近提交了ios和macos两个产品到app store,我设置的强制使用生物识别才能进入应用。但是出现的问题是macos的审核过了,而ios的审核没有过,其反馈的问题即为开始的生物识别没有过,审核人员使用的模拟器,根本不存在生物识别。
所以跟可能会踩这个坑的小伙伴儿提个醒,目前我还没有好的解决方案,正在等待新的审核中......
使用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
放到循环之前的元素上,比如VStack
、List
。
在使用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
而不是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
}
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:已删'
);
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"`
}
此处以增删为例。封装常用方法是为了复用,封装时候使用害羞的代码。不需要为了封装而封装。
// 新增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()
}
此处以新增接口为例。
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,
})
}
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),
})
}
采用无边框就自然导致拖动问题,因此需要手动去设置可拖动区域。
设置可拖动区域也会出现新的问题,如果可拖动区域上存在其他元素的事件问题。在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;
}
npm install qrcodejs2 --save
import QRCode from "qrcodejs2"
<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>
npm install html2canvas --save
import html2canvas from "html2canvas"
<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回调中替换img
的src
,并将生成图片的块隐藏,将img
显示。
当然关于这个实现方式,我看到的技术分享文章中,还有两种不同的解决方式:
这里我只记录一下我使用的,后期会再去研究这两种实现方式。
<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原生支持菜单高亮,菜单中路由使用<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
组件封装比较完善,可以直接上手使用,但是需要设置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>
开始是开发electron
时遇到的问题,使用Interval
计时器,在窗口最小化隐藏再打开,计时器在隐藏期间并没有工作。
后来网上查询相关问题,发现更多是在浏览器tab页隐藏/切换情况下,计时器就会停止。
在后台选项卡上运行的计时器方法可能会耗尽资源。在后台选项卡中以非常短的时间间隔运行回调的应用程序可能会消耗大量内存,以至于当前活动选项卡的工作可能会受到影响。在最坏的情况下,浏览器可能会崩溃,或者设备的电池会很快耗尽。
此限制是浏览器限制的。
无法突破限制,但是可以使用折中的方式,当然我也觉得此方式相较于一直计时会更优,即监听visibilitychange
事件。
visibilitychange
事件可以监听tab页面的激活与失活事件,因此可以:
document.addEventListener('visibilitychange', function() {
if(document.hidden) {
// tab页失活
}
else {
// tab页激活
}
});
在使用Swift UI
和Realiy
开发AR项目时,发现摄像头一直是居中的,无法全屏。
LaunchScreen.storyboard
文件在左侧文件列表中新建文件,名为LaunchScreen.storyboard
。
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
,服务端出现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
中实现,本来是需要自己去写样式的,我在开发之前也去了解了下,想看看有没有大佬已经实现了,不过确实发现了一个大佬的仓库分享了毛玻璃组件,但是其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.
visualEffectState
String (optional) - Specify how the material appearance should reflect window activity state on macOS. Must be used with thevibrancy
property. 可能的值有
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" // 这个参数不加的话,鼠标离开应用程序其背景就会变成白色
})
实现就是这么简单!
小伙伴儿们有兴趣的可以参考下我这个项目,使用的毛玻璃样式。
an upstream response is buffered to a temporary file
Nginx
配置加上如下配置
proxy_max_temp_file_size 0;
client_max_body_size 50m;
代码运行过程中,意外情况会导致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中的点击可以有两种情况:
根据不同情况可以去不同地使用。
此处单纯的按钮即为有按钮样式和点击事件。
Button(action: {
... // 点击事件触发的代码
}, label: {
Image(systemName: "plus")
})
即为没有按钮样式的按钮,方便直接展示Image
。
Button(action: {
... // 点击事件触发的代码
}, label: {
Image(systemName: "plus")
})
.buttonStyle(BorderlessButtonStyle())
Image(systemName: "plus")
.onTapGesture {
... // 点击事件触发的代码
}
当Ngnix的缓存被打开之后,Nginx会将符合规则的response存储到文件作为缓存,并且用其缓存去响应客户端,不需要每次都去服务端请求。
要开启缓存,需要在最顶级的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;
Nginx
在缓存中使用到两个进程:
cache manager
:该进程周期性地检测缓存的状况,检测缓存的大小、数量等是否超过设置额度;cache loeader
:该进程只会在Nginx
启动后运行一次,加载之前的缓存文件的元数据到共享内存中。在启动时候一次加载整个缓存会消耗Nginx
启动时的性能,为了避免这种情况,可以在proxy_cache_path
中直接配置如下参数:
在如下案例中,迭代持续300ms或者直到200个项目已经被加载:
proxy_cache_path /data/nginx/cache keys_zone=one:10m loader_threshold=300 loader_files=200;
默认情况下,Nginx
会在第一次缓存所有的GET
和HEAD
方法的请求的响应,使用字符串作为缓存的key,如果不同的请求使用同一个key,则会直接返回其对应的缓存数据。
默认的缓存key为:
proxy_cache_key $scheme$proxy_host$uri$is_args$args;
可以通过配置自定义,如下:
proxy_cache_key "$host$request_uri$cookie_user";
proxy_cache_min_uses 5;
默认缓存请求方法为GET
和HEAD
,通过如下添加POST
方法。
proxy_cache_methods GET HEAD POST;
默认情况下,响应会无限期的保存下来。只有当缓存大小超过限制或者超出缓存时间的情况下,缓存将会被清除。
proxy_cache_valid 200 302 10m;
proxy_cache_valid 404 1m;
如果是匹配所有可以使用any
:
proxy_cache_valid any 5m;
通过设置proxy_cache_bypass
配置可以使Nginx
不直接使用缓存返回客户端。配置中的每个参数都只是0
和非0
,只要一个参数不为0
,都将不会使用缓存。
proxy_cache_bypass $cookie_nocache $arg_nocache$arg_comment;
通过设置proxy_no_cache
来使得响应完全不缓存,其参数和proxy_cache_bypass
一样。
proxy_no_cache $http_pragma $http_authorization;
对于proxy_cache_bypass和proxy_no_cache的参数,如$arg_nocache的意思为请求的url上携带nocache参数。
项目开发中遇到该问题,网上的文章太乱,为了节省下次踩坑时间,特此记录。
该函数在加解密中都需要用到。
func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padText := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padText...)
}
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)
}
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
}
// 填充
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
}
result := Ase256Encrypt("<需要加密的数据>", "<key>", "<iv>", aes.BlockSize)
result, err := Aes256Decrypt("<需要解密的数据>", "<key>", "<iv>")
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()
article1 = Article.query.filter(Article.title == 'article').first()
article1 = Article.query.filter_by(title = 'article').first() # 或者
print(article1.title)
print(article1.content)
article1 = Article.query.filter_by(title = 'article').all()
for item in article1:
print(item.title)
print(item.content)
article1 = Article.query.filter_by(title = 'article').order_by(Article.id.desc()).all()
article1 = Article.query.filter_by(title = 'article').limit(10).all()
使用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
});
前端导入excel表格,直接前端解析文件,将数据传给后端。
npm install xlsx
import XLSX from "xlsx";
<div class="container">
{{ upload_file || "导入" }}
<input
type="file"
accept=".xls,.xlsx"
class="upload_file"
@change="readExcel($event)"
/>
</div>
</div>
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]);
}
<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_file
的opacity
和position
。
.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;
}
前端的日益强大导致很多功能都可以在前端去直接实现,并且可以减少服务器压力。
当然单纯地去实现这样的数据传输,尤其对于重要数据,是很不安全的,因此在前后端数据传输的时候,可以加上加密校验,这个后期会来写的。
为了实现该功能参考了如下大佬的文章:
按照ElementUI官方文档按需引入却报错,首先报错缺少babel-preset-es2015
。安装该组件之后编译却报错。
Error: Plugin/Preset files are not allowed to export objects, only functions.
该问题为babel
版本冲突。
yarn add @babel/preset-env
.babelrc
{
"presets": [["@babel/preset-env", { "modules": false }]],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
var window = NSScreen.main?.visibleFrame
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 扩展,它添加了快速构建 REST APIs 的支持。其请求解析接口是模仿 argparse
接口。它设计成提供简单并且统一的访问 Flask 中 flask.request
对象里的任何变量的入口。
{
"username": "kuari",
"info": "heihei"
}
parse = reqparse.RequestParser()
parse.add_argument('username', type = str)
parse.add_argument('info', type = str)
args = parse.parse_args()
使用参数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()
使用参数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()
使用参数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()
{
"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()
窗口最小化或者关闭的情况下,进程未退出,需要通过系统托盘来查看,当然还需要托盘菜单。这里就用最简单的菜单实现,加上点击事件触发。
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);
});
当想为程序设定一个固定的窗口大小时候,需要限制用户对程序窗口的大小进行拖动调整。但是使用官方的设定时候,在mac平台上可以禁用调整,当在windows平台上时候却依然可以。所以为了兼容两者,可以选择使用无边框来实现。
resizable
Boolean (optional) - Whether window is resizable. 默认值为 true
。win = new BrowserWindow({
width: 800,
height: 600,
resizable: false
});
win = new BrowserWindow({
width: 800,
height: 600,
frame: false,
resizable: false
});
部署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原生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>
在ios
上限制有两条:
https
协议的devicemotion
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>
<!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>
因为项目需求,需要去检测用户的农历生日。虽然后来找到了合适的库,但是首先先解释下农历
的定义,也是去了解才知道,原来农历不是阴历。
农历属于阴阳合历,其年份分为平年和闰年。平年为十二个月,闰年为十三个月。月份分为大月和小月,大月三十天,小月二十九天,其平均历月等于一个朔望月。
github.com/nosixtools/solarlunar
该库支持1900~2049年。所以项目要跑到2049年后的童鞋就要注意......
当然,该库还支持阳历转农历、节假日计算等,有兴趣大家可以自行去了解下。
该库不支持闰年判断,所以需要自己去实现闰年的判断,其参数类型为Boolean
。
func IsALeapYear(year int) (result bool) {
if year%4 == 0 && year%100 != 0 || year%400 == 0 {
result = true
return
}
return
}
需要转换的阳历日期格式是固定的,是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监听手势使用的方法为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去分享文件是一件十分头疼的问题。
对于这个问题,可以直接使用静态文件服务器解决部分需求,如下介绍几个常见方法。
对于Python
来说,可以直接使用内置的库来实现。
python2
python -m SimpleHTTPServer 8000
Python3
python -m http.server 8000
node
生态内有一个项目http-server
,直接V8
引擎带你飞。
npm install --global http-server
brew install http-server
http-server [path] [options]
例如:
cd exmaple/
http-server
https://github.com/http-party/http-server
Nginx
和Apache
本身可用于静态文件服务器,这就需要用户直接在本地安装。
当然,nginx
需要注意配置一下,打开索引:
server {
listen 80;
...
location / {
root /usr/share/nginx/html;
autoindex on;
}
}
使用Docker
其实也是使用如Nginx
来实现静态文件服务器,但是容器化在该场景存在几大优势:
相对于直接安装Nginx
或者Apache
,更推荐使用Docker
。
@State var search: String = ""
此处过滤条件为判断元素是否包含搜索的文本。
<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 {
... // 展示搜索结果
}
}
}
在Golang
中json
解析时报错:
invalid character '\\b' after top-level value
代码如下:
json.Unmarshal([]byte(result), &response)
首先将result
打印出来,发现并无异常,其标点符号也没有问题。
然后查看网上现有解决方案的帖子基本试了下,起码对于我来说并不适用,概括下方案:
最后对比代码中获取到的字符产长度和手动复制所见的字符串的长度,发现确实代码中字符长度不同,其长度是80,而手动复制的字符串的长度是72。
strings.ReplaceAll(result, "\b", "")
就挺简单的......
使用 v-charts
做数据可视化,需要给图表添加标题。
v-charts本身并没有提供显示标题的配置,顾需要引入 echarts
的 title
。
import "echarts/lib/component/title";
this.chartTitle = {
text: "平台用户与创客数量对比图",
textStyle: {
fontWeight: 600,
color: "white"
}
};
<!--其中...代表其他配置-->
<ve-bar ... :title="chartTitle" />
<template>
<ve-bar ... :title="chartTitle" />
</template>
<script>
import "echarts/lib/component/title";
export default {
data() {
this.chartTitle = {
text: "平台用户与创客数量对比图",
textStyle: {
fontWeight: 600,
color: "white"
}
};
}
};
</script>
虽然并非出于直接实现无边框的需求,但是为了实现窗口的固定大小(设置固定值不可调整情况下,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
});
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插件入门,简直刺激...
@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()
}
}
在特定敏感数据的场景需要加密,一开始采用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
的中间件的使用场景非常广泛,此处主要介绍如何使用其来完成常见场景下的鉴权。
官方文档列出了如下几种使用方式:
对于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()
}
}
对于请求的处理,需要去验证是否对其请求的路径拥有访问权限。
首先看一下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") // 可见上一步的代码,当然此处只是为了演示设置固定值
然后在中间件中拿到角色并进行判断。
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()
}
}
在设置路由时候,添加该中间件,并设置白名单。
r.POST("/todo", middleware.Permissions([]string{"admin"}), views.AddTodo) // 添加中间件将会验证角色
r.PUT("/todo", views.ModifyTodo) // 未添加中间件则不会验证角色
在数据量较小的情况下,可以使用Redis
来实现消息的发布与订阅,来代替Kafka
。Kafka
对于数据量大的场景下性能卓越,但是对于如此小场景时候,不仅运维成本提升,还用不上多少性能。
不过使用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)
}
分步讲解下具体实现代码。
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 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)
}
}
此处分为一个发布节点和一个订阅节点来实现了简单的发布与订阅。
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)
}
}
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)
}
这是个比较坑的问题,我一开始开发的是macos项目,到网上搜的方案基本都是使用UIPasteboard
方法,但是偏偏用不了。
后来开发ios项目,用macos的就不行,发现UIPasteboard
的可行,所以这里需要清楚的是,ios和macos的复制方法是不同的......
func copyToClipBoard(textToCopy: String) {
let pasteBoard = NSPasteboard.general
pasteBoard.clearContents()
pasteBoard.setString(textToCopy, forType: .string)
}
copyToClipBoard(textToCopy: "Hello,World!")
UIPasteboard.general.setValue(<Your-String>, forPasteboardType: kUTTypePlainText as String)
UIPasteboard.general.setValue("Hello,World!", forPasteboardType: kUTTypePlainText as String)
前端还在传文件给后端吗?你们的服务器扛得住吗?什么......老板砸钱加机器?!告辞!/狗头
前后端文件传输涉及数据较大,往往会成为很多项目的性能瓶颈。常见的传输方式也有不少,相对来说,OSS直传能够减轻很大压力。
本文我们来列举下常见的和oss直传的几种传输方式,并列举其优劣。
表单上传文件是最常见的方式,前后端开发小伙伴都很轻松,前端哐哐传,后端哐哐收就成了。其过程如下图所示。
优势:
Base64方式上传文件,多常见于小文件,如小图片等,前后端都可直接使用String
类型发送和接收。不过在前端,需要将文件转成base64数据,不仅会增加些性能消耗,还会增加传输数据的体积。而对于后端,如果并不是想直接存储base64数据,也还需要将其转成文件再存储,也会增加后端的性能消耗。
该上传方式可适用于Resetful Api
,也可适用于文件的加密、回调接口携带文件等等。其过程如下图所示。
优势:
Resetful Api
,可用于加密、回调等场景,较为灵活劣势:
此处的OSS直传方案都是使用的阿里云的OSS产品,以下将介绍三个方案,可适用于不同的场景。
该方案可在前端直接通过browser.js
上传文件到OSS,可分成三步:
其流程如下图所示。
Browser.js
的方式需要前端安装阿里云的库ali-oss
,然后在前端调用。直传还需要OSS账户的Key和Secret,因此为了安全考虑,需要建立RAM账户,然后前端向后端先请求一个STS
临时访问凭证来完成直传,其流程如下图所示。
Javascript客户端签名直传,需要先从后端获取临时签名,其流程与上一步的browser.js
方案大致相同,不同点在于:
其流程如下图所示。
不过该方案做下来,我感觉最大的问题是权限配置有点麻烦....../泪眼
该方案其实是上面方案——Javascript客户端签名直传的升级版本,其加上了后端的上传回调功能。不错呦~
前端需要改动的很少,只需要在请求参数中加上callback
参数即可,该参数为后端加密,在签名请求的响应中一起返回回来,内加密了后端回调接口。在前端直传完成后,后端回调接口将会接收到相关文件参数,包括文件路径、大小、类型等。最后OSS会将回调接口response转发给前端,响应直传OSS的请求。
其流程如下图所示。
传统方式相比直传OSS,相对来说有三个缺点:
当然,对于规模较小、成本较低的项目来说,常见的上传方式还是适合的,毕竟没有最好的,只有最适合的。
使用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
文件:
都修改之后却并不生效。
在文件夹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
应用在开发中,需要存储数据到本地,经历了两个版本,其方案都不太一样。
一开始考虑使用cookie,在开发过程中没有任何问题,但是编译之后去使用,发现无法操作cookie。原来在开发中直接js操作的的浏览器的cookie,而在electron中需要交由底层的nodejs去操作本地的cookie,官方说法是通过Session
的cookies
属性来访问Cookies
的实例。但是我在实践过程中确实没有成功,然后随着需求变化,数据量变大,就直接放弃了这个方案。
当时用的electron
版本是9.0.0
,之后才用的方案是直接文件存储,即直接fs
读与写,毫无问题。就是注意配置文件的存放位置。
现在再去使用electron
,版本已经到了11.0.0
。当我去使用fs
读写时直接给我报错fs.writeFile is not a function
,经过一天多的排错和查找,最终放弃该方案,当然我并没有找到原因和解决方案。最后决定使用lowdb
去实现存储。
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相关的笔记吧。
这真的难搞哦,毕竟新出来的。国内文档真的是少之又少,国外的文档也真不多,基本搜出来的都是UIKit的。官方文档,也是一言难尽,基本就处于,我要实现一个功能,然后去搜一下各种帖子,筛选掉无用的帖子,找到有用的点,当然也是时常根本找不到有用的帖子,然后就要去油管上看各种教程,然后发现:哦!原来还有这个方法!接着去官方文档搜一下,看一下属性,自己调用调用......
光是这个core data我就折腾了近一周,最后发现,原来还是官方好呀.....
吐槽一下自学swift ui,现在进入正题了。
core data呢,是苹果官方的本地数据库,但是其存储的文件其实是sqlite
文件。其可以通过icloud实现备份和同步,当然icloud我会额外写一篇文档来详细讲述的(又是一把辛酸泪...)。
如下是我当前的环境:
在创建项目的时候,可以直接选择Use Core Data
选项,xcode会直接在ContentView.swift
中生成一个相关demo。
创建之后,查看文件目录,相对于不选择Use Core Data
,会多出如下几个文件:
点击<Your-Project-Name>.xcdatamodeId
文件,进入页面。
可以看到页面内有默认的CONFIGURATIONS
为Default
,默认的ENITITIES
为Item
,可以理解为分别对应sql中的库和表。
然后点击Item
,可以看到其字段,有默认的timestamp
字段。
若要新增ENTITIES
(表),点击底部的Add Entity
按钮即可。
若要新增Attributes
(字段),点击右侧Attributes
中的+
即可,注意字段要选择类型。
比如,此处以默认的Item
为例,新增username
和age
两个字段。
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
// body中便利items即可
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)")
}
}
}
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)")
}
}
}
在默认生成代码中,有一个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
按钮之后,便会开始添加数据。
对于记录右滑即可删除,其为List
的onDelete
方法。
该文章是面向新手的,也是记录下我踩过的坑,因为目前文档匮乏,身边也没swift的开发小伙伴儿,只能靠自己摸索,若有大佬有更好的方法,真的还请不吝赐教。
后面持续记录踩坑中......
当前产品遇到一个报错,就是接口收到请求没有限制请求字段长度,导致字段长度超过数据库对应字段长度,直接报了500。因此也对此有些新的需求,需要在后端限制请求字段最大长度。
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')
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.