Sunday, May 16, 2021

V2EX - 技术

V2EX - 技术


大佬们,有没有类似 PHP 框架的数据库链式查询的 Python 模块

Posted: 16 May 2021 03:42 AM PDT

如题,用了 php 框架的数据库链式查询感觉很好用,想问问有没有类似的 python 模块,不想写 sql 语句啊。

求助!有哪位大佬知道 flutter 的 dart-sdk 源码修改后如何能生效?

Posted: 16 May 2021 03:41 AM PDT

例如 flutter/bin/cache/dart-sdk/lib/_http/websocket_impl.dart

照猫画虎,仿照 VuePress 2 的默认主题自己写了一个

Posted: 16 May 2021 03:37 AM PDT

VuePress 2 正式版发布在即,就参照它的默认主题自己写了一个 :vuepress-theme-mix

预览地址: https://vuepress-theme-mix.vercel.app/

代码地址: https://github.com/gavinliu6/vuepress-theme-mix

说明:欢迎 Star,非专业前端开发,开发此主题纯属个人兴趣,顺便练手。如遇槽点,希望轻喷😬 有关技术和 UI 的批评建议虚心接受。

安卓开发 如何优雅的做 code review ?

Posted: 16 May 2021 03:26 AM PDT

你们公司是 code review 怎么做的的?

腾讯云要笑死我,抽奖送的颈枕是他们用过的��

Posted: 16 May 2021 03:21 AM PDT

前段时间他们有个活动,我抽中唯一的颈枕。腾讯定制鸡头颈枕,一看就知道是生肖年定制,我想应该是库存。

收到货我惊了,确实是 2017 鸡年定制的,但不是库存,看起来是用过的,鸡头轻微掉色染到了其他部分,扣子有明显的磨损,扣不太紧了。

我倒不在意有没有用过,这些东西我拿到手都会洗一遍再用,但确实给我整笑了。

我现在就想知道腾讯云是怎么定礼品的,该不会是团队里每人出一份吧。😂

最近需要用到阿里的全套服务,有些问题请教下大佬们

Posted: 16 May 2021 03:19 AM PDT

  1. 接口老大说直接用阿里的函数计算来写
  2. 数据库也是直接买阿里的云数据库,目前打算是用 mysql

想请教下,这种方式和传统的该买云服务器在上面部署 application 有什么明显的优势吗?比如费用和可用性方面。 之前没怎么接触过这方面,一直都是个标准的 CURD 业务 boy,懂得大佬麻烦不吝赐教, 谢谢。

怎么正确禁用 ROOT 用户

Posted: 16 May 2021 03:10 AM PDT

ubuntu/suse 都默认禁用 root 账户

然后可以给 root 设置密码来启用。

不启用 root 账户时,sudo 的密码是当前用户的密码。

启用后密码就必须是 root 账户的密码。

想再禁用 root 该怎么操作?设置 /etc/shadow,替换 root 的密码为!不好使。passwd -dl root 也不好使

不好使指的是:sudo xxx的时候提示输入 root 密码。无论怎么尝试(旧密码)或当前用户密码都不正确。

JS 如何获取多层嵌套的 Class 元素?

Posted: 16 May 2021 02:59 AM PDT

求帮助,Wordpress 添加文章目录到侧边栏遇到的问题:
文章是用构建器构建的,文章容器被多层嵌套..?
<div class="entry-inner">
<div class="sektion-wrapper nb-loc " data-sek-level="location" data-sek-id="before_content" data-sek-is-global-location="false">
<div data-sek-level="section" data-sek-id="__nimble__fdfe8afc5daa" class="sek-section sek-has-modules ">
<div class="sek-container-fluid"><div class="sek-row sek-sektion-inner">
<div data-sek-level="column" data-sek-id="__nimble__a24b01e49d25" class="sek-column sek-col-base sek-col-100 ">
<div class="sek-column-inner "><div data-sek-level="module" data-sek-id="__nimble__376e3143b538" data-sek-module-type="czr_tiny_mce_editor_module" class="sek-module ">
<div class="sek-module-inner">
<p>111</p>
<p>2222</p>
<h2><2333</h2>
<h2>2344</h2>
....

我换成了自己的目录容器(custom_html-7)和文章容器(sek-module-inner),但获取不到构建器构建的内容。

原代码如下
来源:https://www.rainng.com/js-wordpress-catalog/#h2-2

<(忽略)script>
/*
Author: Azure99
WebSite: https://www.rainng.com/
GitHub: https://github.com/Azure99
*/

let catalogData = getArticleCatalog();
// 自适应文章目录
if (catalogData != null) {
// dynamic-wrapper 换成你的目录容器
let wrapper = document.getElementById('dynamic-wrapper');
wrapper.innerHTML = generateCatalog(catalogData);
}

// 获取本页面的文章目录信息
function getArticleCatalog() {
// entry-inner 换成你使用主题的文章容器
let articleContent = document.getElementsByClassName('entry-inner');
if (articleContent.length !== 1) {
return null;
}

let catalog = [];
let header = {};

let elements = articleContent[0].childNodes;
// 遍历所有元素
for (let i = 0; i < elements.length; i++) {
if (elements[i].nodeName === 'H2') {
// 为二级标题分配 ID 以供锚点跳转,下同
elements[i].id = 'h2-' + catalog.length;
// 记录此二级标题和其所有的三级子标题
header = {
name: elements[i].innerText,
childHeaders: []
};
catalog.push(header);

} else if (elements[i].nodeName === 'H3') {
elements[i].id = 'h2-' + (catalog.length - 1) + '-h3-' + header.childHeaders.length;
// 记录此三级标题到二级标题下
header.childHeaders.push(elements[i].innerText);
}
}

return catalog;
}

// 根据目录信息生成文章目录代码
function generateCatalog(catalogData) {
let catalog = '<div style="text-align: center; margin-top: 10px;">文章目录</div>';

for (let i = 0; i < catalogData.length; i++) {
let target = '#h2-' + i; // 跳转目标
let index = (i + 1) + '. '; // 标题索引
let name = catalogData[i].name; // 标题
catalog += '<a href="' + target + '">' + index + name + '</a><br/>';

for (let i2 = 0; i2 < catalogData[i].childHeaders.length; i2++) {
target = '#h2-' + i + '-h3-' + i2;
index = (i + 1) + '.' + (i2 + 1) + '. ';
name = catalogData[i].childHeaders[i2];
catalog += ' <a href="' + target + '">' + index + name + '</a><br/>'
}
}

return catalog;
}
<(忽略)/script>

群晖上有推荐可以把视频分享到外网在线播放的方法吗?

Posted: 16 May 2021 02:45 AM PDT

想到一个应用场景,把一个剧集分享给你朋友看,不用登陆,不用注册,浏览器端点开即看,若是能生成剧集播放列表更好。

目前觉得群晖自带的 Video Station 分享功能还不错,可是视频播放上有些太羸弱了,比如有些视频格式要么播放不了,要么要硬解之类的。不过对 Video Station 了解不深,没怎么折腾。

一直使用 Plex,Plex 可以分享。优点是 Plex 视频界面比较漂亮,网页端也不错,各方面功能也很 OK 。但是接收分享内容的朋友需要注册 Plex 账户,通过在 Plex 账户之间分享内容,想要在手机端观看还要收费,略显麻烦了一些。、

Plex 还有个分享功能,访客模式,挺好,点进去即可观看,可惜只能内网。

各位在座的彦祖还有更好的实现方式推荐吗?

Python 连接 mysql 失败 TypeError: __init__() takes 1 positional argument but 4 were given

Posted: 16 May 2021 02:30 AM PDT

首先我非常确定各个参数是没错的
然后我试过给各个参数加上名称 例如 host=self.host 也是不行

CODE:

def __init__(self, host=None, username=None, password=None, database=None, config_file=None, dbms="mysql"):
"""
Class constractor
:param host:
:param username:
:param password:
:param database:
"""
self._host = host
self._username = username
self._password = password
self._database = database
self._run_transaction = None
if config_file:
self.get_mysql_credentials(config_file, dbms)

def get_mysql_credentials(self, configfile, dbms):
"""
Retrieve credentials from configuration file
:param configfile: path to
:param dbms: default is mysql
:return: VOID
"""
try:
if configfile:
config = configparser.ConfigParser()
config.read(configfile)
self._host = config[dbms]['host']
self._username = config[dbms]['username']
self._password = config[dbms]['password']
self._database = config[dbms]['database']
self._run_transaction = config[dbms]['transactions']
except:
print("Error: couldn't read config file")

def connect(self, database=None):
"""
Connect to a existing database
:param database: name
:return: the connection handler
"""
try:
if database: # if is not Null
self._database = database
connection = pymysql.connect(self._host, self._username, self._password, self._database)
return connection
except pymysql.InternalError as error:
print(error.args[1])

config_file:::
[mysql]
host = localhost
username = root
password = passwordForCSC675
database = PharmacyManagementSystemDB
transactions = 1

.net 的 entity framework 里实体新加字段如何让数据库自动同步

Posted: 16 May 2021 02:07 AM PDT

表里新加字段,实体代码改动后,如何让数据库保持一致。搜到资料是用命令在文件夹里生成迁移脚本,这样脚本岂不是越来越多。这个数据库只是在用户客户端用的,这办法太繁琐也容易出错。

在表结构只做增量更新的需求下,有解决办法吗,EnsureCreated 方法只能创建新的数据库和实体一致

如果是 nosql 数据库就不用关心这个问题了,不过调研之后没有发现稳定成熟的嵌入式数据库,仍然用的 sqlite

求杭漂租房建议

Posted: 16 May 2021 01:55 AM PDT

RT, 下个月大概率去蚂蚁实习了。新人没有租房经验,还请网友支支招。有没有靠谱点的平台,或者周边还算不错的公寓房。个人有点接受不了合租,优先考虑整租。

一加 9pro+氧 os 有什么能用 nfc 门禁卡的方案?

Posted: 16 May 2021 01:37 AM PDT

昨天刚到的手机就刷了氧 os,整体观感不错,但是不能用 nfc 有点难受。 看官方论坛也没有一个靠谱的方案。 google play 上的 nfc 软件也不太敢用。

特意来这里咨询一下老哥们

粤语编程语言还能机器学习和撸 App?

Posted: 16 May 2021 01:13 AM PDT

还记得几个月前一位 16 岁高中生发布的粤语编程语言吗?
https://www.v2ex.com/t/752132#reply20
作为广东方言编程语言,没几个强大的功能怎能行?
使用粤语开发的第一个 App:
```
使下 kivy
咩系 HelloApp?
佢个老豆叫 App
佢识得 |HelloWorld| -> {
|同我 show| 下 -> "Hello World" @ |做嘢|
返转头 |做嘢|
}
明白未啊?

|App 运行| 下 -> |HelloApp, HelloApp().HelloWorld|
```
实现 KNN 算法:
```
使下 math
|[5, 1], [4, 0], [1, 3], [0, 4]| 拍住上 -> |数据|
|'动作片', '动作片', '科幻片', '科幻片'| 拍住上 -> |标签|
讲嘢: |K| 系 3
嗌 KNN 过嚟估下 -> |[3, 0]|
```
还支持 web,游戏等开发,更多例子见 github.
空闲时偶尔玩一下粤语编程,装装逼,难道不快乐吗?
项目地址
https://github.com/StepfenShawn/Cantonese.git
求 star

钉钉微应用开发(SpringBoot + VUE + Docker)~

Posted: 16 May 2021 12:36 AM PDT

求各位大佬 start 呀


Prod Status

目标与期望

基于钉钉微应用开发的实验室绩效管理系统,将实验室的绩效、学分、论文评审管理与钉钉对接。
主要功能有:绩效、学分申请与审核,论文评审投票及学分管理,实验室助研金计算等。

注意事项

  • sdk 使用代码:com.softeng.dingtalk.api
  • 前端代码:dingtalk-vue (👈预览)
  • 使用了 lombok 插件简化代码,idea 需要安装 lombok 插件,否则编译过不去
  • 由于目前钉钉小程序只支持 GET/POST, 考虑到兼容性这里的接口全部为 GET/POST 方式
  • 系统启动时,初始化操作会调用钉钉 SDK,拉取钉钉组织的所有用户, 请先在开发平台设置出口 IP

系统部署

本项目使用 GitHub Actions 实现 CI,受外网网速限制,没有采用在 GitHub 机器上构件镜像,再拉取到服务器上运行的方式。而是在每次 CI 触发后,GitHub 机器 ssh 登陆服务器,执行脚本来拉取最新代码,构建镜像,并运行容器,具体如下:

  1. 从 GitHub 仓库中拉去最新代码到服务器本地仓库
  2. 使用 mvn 构建项目
  3. docker-compose build 构建镜像
  4. docker-compose up -d 在后台启动容器
  5. docker image prune -f 清理无用的镜像

GitHub Actions 的 CI 脚本如下

  • 生产环境 CI 脚本:.github/workflows/prod.yml
  • 测试环境 CI 脚本:.github/workflows/test.yml
  • 与 CI 脚本对应的项目结构如下
    . |__ dingtalk     |__ dingtalk-springboot  // 后端代码     |__ dingtalk-vue         // 前端代码     |__ docker-compose.yml   // docker-compose 配置文件 

docker-compose 编排配置如下:

系统运维

前端预览

01

02

03·

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

从 AOT 编译的二进制文件中提取 Java 类信息

Posted: 15 May 2021 11:46 PM PDT

从 AOT 编译的二进制文件中提取 Java 类信息,详细查看下面文章

https://www.jianshu.com/p/504c68ebf22e

gRPC-Go 源码阅读 - gRPC 拦截器

Posted: 15 May 2021 09:17 PM PDT

在 gRPC 中有拦截器,拦截器可以对 RPC 的请求和响应做拦截处理,并且在客户端、服务端都可以进行拦截处理, 使用拦截器可以在 handler 执行前后做一些处理,更灵活的处理 gRPC 流程中的业务逻辑

在 gRPC-Go 的源码的 NewServer 方法中,我们可以看到如下代码

 func NewServer(opt ...ServerOption) *Server { 	opts := defaultServerOptions 	for _, o := range opt { 		o.apply(&opts) 	} 	s := &Server{ 		lis:      make(map[net.Listener]bool), 		opts:     opts, 		conns:    make(map[transport.ServerTransport]bool), 		services: make(map[string]*serviceInfo), 		quit:     grpcsync.NewEvent(), 		done:     grpcsync.NewEvent(), 		czData:   new(channelzData), 	} 	chainUnaryServerInterceptors(s) 	chainStreamServerInterceptors(s) 	s.cv = sync.NewCond(&s.mu) 	if EnableTracing { 		_, file, line, _ := runtime.Caller(1) 		s.events = trace.NewEventLog("grpc.Server", fmt.Sprintf("%s:%d", file, line)) 	}  	if s.opts.numServerWorkers > 0 { 		s.initServerWorkers() 	}  	if channelz.IsOn() { 		s.channelzID = channelz.RegisterServer(&channelzServer{s}, "") 	} 	return s } 

其中 chainUnaryServerInterceptors(s)chainStreamServerInterceptors(s) 就是将用户实现的拦截器注册到服务端拦截器链中,服务端拦截器有 unary interceptor 和 stream interceptor 两种, 在我目前看的 gRPC 版本(1.37.1)中, 服务端两种拦截器都是可以支持多个拦截器的,在更早的 gRPC 版本中服务端、可客户端只支持单个拦截器

gRPC 的拦截器实现特别简单

unary interceptor 拦截器只要实现 UnaryServerInterceptor 方法

type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error) 

stream interceptor 拦截器只要实现 StreamServerInterceptor 方法

type StreamServerInterceptor func(srv interface{}, ss ServerStream, info *StreamServerInfo, handler StreamHandler) error 

下面我看可以通过一个简单的例子看看如何添加一个拦截器

服务端拦截器

package main  import ( 	"context" 	"fmt" 	"google.golang.org/grpc" 	interceptor "grpcinterceptor/proto" 	"grpcinterceptor/services" 	"log" 	"net" )  func main() { 	rpcService := grpc.NewServer(grpc.UnaryInterceptor(unaryServerInterceptorFirst), 		grpc.StreamInterceptor(streamServerInterceptorFirst))  	interceptor.RegisterSayHiServiceServer(rpcService, new(services.SayHiService))  	lis, err := net.Listen("tcp", "localhost:11088") 	if err != nil { 		log.Fatalf("net listen err: %v", err) 	}  	err = rpcService.Serve(lis) 	if err != nil { 		log.Fatalf("serve err: %v", err) 	} }  // type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error) func unaryServerInterceptorFirst(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { 	fmt.Println("unaryServerInterceptor -- 01 before handler") 	resp, err := handler(ctx, req)  	fmt.Println("unaryServerInterceptor -- 01 after handler")  	return resp, err }  // type StreamServerInterceptor func(srv interface{}, ss ServerStream, info *StreamServerInfo, handler StreamHandler) error func streamServerInterceptorFirst(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { 	fmt.Println("streamServerInterceptor -- 01 before handler")  	err := handler(srv, ss)  	fmt.Println("streamServerInterceptor -- 01 after handler")  	return err } 

上面我实现了一个 unary interceptor unaryServerInterceptorFirst,和一个 stream interceptor streamServerInterceptorFirst

然后使用 grpc.UnaryInterceptorgrpc.StreamInterceptor 做一次转换后,以参数的形式传入 NewServer 即可

rpcService := grpc.NewServer(grpc.UnaryInterceptor(unaryServerInterceptorFirst), grpc.StreamInterceptor(streamServerInterceptorFirst)) 

此时使用当客户端发起请求后,可以看到如下输出

go run main.go unaryServerInterceptor -- 01 before handler unaryServerInterceptor -- 01 after handler 

这里的例子比较简单,在实战中,如果你有用户权限认证这种需求,使用拦截器实现是一个不错的主意

上面例子中我们定义的是单个拦截器, 下面是一个定义多个拦截器的例子

package main  import ( 	"context" 	"fmt" 	"google.golang.org/grpc" 	interceptor "grpcinterceptor/proto" 	"grpcinterceptor/services" 	"log" 	"net" )  func main() { 	var serverInterceptors = ServerInterceptors{ 		UnaryInterceptors:  []grpc.UnaryServerInterceptor{unaryServerInterceptorFirst, unaryServerInterceptorSecond}, 		StreamInterceptors: []grpc.StreamServerInterceptor{streamServerInterceptorFirst}, 	}  	rpcService := grpc.NewServer(grpc.ChainUnaryInterceptor(serverInterceptors.UnaryInterceptors...), 		grpc.ChainStreamInterceptor(serverInterceptors.StreamInterceptors...))  	interceptor.RegisterSayHiServiceServer(rpcService, new(services.SayHiService))  	lis, err := net.Listen("tcp", "localhost:11088") 	if err != nil { 		log.Fatalf("net listen err: %v", err) 	}   	err = rpcService.Serve(lis)  	if err != nil {  		log.Fatalf("serve err: %v", err) 	} }  type ServerInterceptors struct { 	// 服务端 Unary interceptor 	UnaryInterceptors []grpc.UnaryServerInterceptor 	// 服务端 Stream interceptor 	StreamInterceptors []grpc.StreamServerInterceptor }  // type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error) func unaryServerInterceptorFirst(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { 	fmt.Println("unaryServerInterceptor -- 01 before handler") 	resp, err := handler(ctx, req)  	fmt.Println("unaryServerInterceptor -- 01 after handler")  	return resp, err }  func unaryServerInterceptorSecond(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { 	fmt.Println("unaryServerInterceptor -- 02 before handler")  	resp, err := handler(ctx, req)  	fmt.Println("unaryServerInterceptor -- 02 after handler")  	return resp, err }  // type StreamServerInterceptor func(srv interface{}, ss ServerStream, info *StreamServerInfo, handler StreamHandler) error func streamServerInterceptorFirst(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { 	fmt.Println("streamServerInterceptor -- 01 before handler")  	err := handler(srv, ss)  	fmt.Println("streamServerInterceptor -- 01 after handler")  	return err } 

和单拦截器不同点是在传给 NewServer 之前,调用的是 grpc.ChainUnaryInterceptor 和 grpc.ChainStreamInterceptor 做了一次转化

此时 handler 执行时候,输出如下

 go run main.go unaryServerInterceptor -- 01 before handler unaryServerInterceptor -- 02 before handler unaryServerInterceptor -- 02 after handler unaryServerInterceptor -- 01 after handler 

此时的执行顺序是值得注意的,先执行 before,后执行 after, before 按照拦截器的顺序执行,after 则是逆序执行的。

在我们上面的例子中,定义有 unary 、stream 两种拦截器, 从运行结果可以看到只有 unary interceptor 执行了,stream interceptor 没有执行,我相信大家已经知道原因了,因为我们定义的 handler 只有 unary 的

客户端的拦截器的定义和使用和服务端类似,大家去尝试一下即可;另外 Github 上也有很多开源拦截器,如 做身份认证的拦截器 可以拿来就用,如果你有好的想法,也可以实现拦截器开源后供大家使用。

例子完整代码 https://gitee.com/wewin11235/grpcinterceptor 运行代码:

cd grpcinterceptor go run server/main.go go run client/main.go 

2 + 2

Posted: 15 May 2021 09:16 PM PDT

Console
> 2 + 2
> 4
> "2" + "2"
> "22"
> 2 + 2 - 2
> 2
> "2" + "2" - "2"
> 20

xray 中 vmess 中使用的 kdf 秘钥导出函数用的是什么算法

Posted: 15 May 2021 04:31 PM PDT

看半天也不知道是怎么实现原理.
源码 https://github.com/XTLS/Xray-core/blob/main/proxy/vmess/aead/kdf.go
再问一下我找了半天没有找到最新的 vmess 的协议文档 只找到了下面这个 https://www.v2ray.com/developer/protocols/vmess.html 但是这个我看了一下 v2ray 的实现好像和这个文档写的不一样.

leetcode 240 二维二分查找的解法速度好像还不如直接左下或者右上线性查找的解法 kotlin 时间 43% 内存 22%

Posted: 15 May 2021 04:01 PM PDT

而且代码量多了好多倍,二维矩阵的对角线上二分查找那边界条件 debug 了半天才写对
思路是在对角线上找到 a < target < b 的相邻值 a 和 b,a 的左上角所有元素都小于 target,b 的右下角所有元素都大于 target,然后递归的在右上和左下找

实际就是官方解法 3 的拓展提到的解法,翻了下题解区好像都没找到实现了这种解法的
我一开始的思路就是这个,写到一半感觉太麻烦就想翻答案看看是不是想错了,结果发现官方也只把这个作为拓展

class Solution {      fun searchMatrix(matrix: Array<IntArray>, target: Int): Boolean {         if (matrix.isEmpty() || matrix[0].isEmpty()) {             return false         }         return searchSubMatrix(matrix, target, 0, matrix[0].size - 1, 0, matrix.size - 1)     }      private fun searchSubMatrix(         matrix: Array<IntArray>,         target: Int,         xmin: Int,         xmax: Int,         ymin: Int,         ymax: Int     ): Boolean {         if (xmin == xmax && ymin == ymax) {             return retrieveValAtPos(matrix, Pair(xmin, ymin)) == target         }         val (upLeftBound, downRightBound) = findRange(matrix, target, xmin, xmax, ymin, ymax) ?: return true         return searchSubMatrix(             matrix,             target,             if (upLeftBound.first + 1 > xmax) xmax else upLeftBound.first + 1,             xmax,             ymin,             if (downRightBound.second - 1 < ymin) ymin else downRightBound.second - 1         ) || searchSubMatrix(             matrix,             target,             xmin,             if (downRightBound.first - 1 < xmin) xmin else downRightBound.first - 1,             if (upLeftBound.second + 1 > ymax) ymax else upLeftBound.second + 1,             ymax,         )     }      private fun retrieveValAtPos(matrix: Array<IntArray>, pos: Pair<Int, Int>): Int {         return matrix[pos.second][pos.first]     }      private fun findRange(         matrix: Array<IntArray>,         target: Int,         xmin: Int,         xmax: Int,         ymin: Int,         ymax: Int     ): Pair<Pair<Int, Int>, Pair<Int, Int>>? {         var lower = Pair(xmin, ymin)         var upper = Pair(xmax, ymax)         var center: Pair<Int, Int>         var centerVal: Int         while (lower.first < upper.first || lower.second < upper.second) {             center = Pair(                 lower.first + (upper.first - lower.first) / 2,                 lower.second + (upper.second - lower.second) / 2             )             centerVal = retrieveValAtPos(matrix, center)             when {                 centerVal > target -> {                     upper = Pair(                         if (upper.first == xmin) xmin else upper.first - 1,                         if (upper.second == ymin) ymin else upper.second - 1                     )                 }                 centerVal < target -> {                     lower = Pair(                         if (lower.first == xmax) xmax else lower.first + 1,                         if (lower.second == ymax) ymax else lower.second + 1                     )                 }                 else -> {                     return null                 }             }         }         when {             retrieveValAtPos(matrix, lower) > target -> {                 return Pair(                     Pair(                         if (upper.first == xmin) xmin else upper.first - 1,                         if (upper.second == ymin) ymin else upper.second - 1                     ),                     lower                 )             }             retrieveValAtPos(matrix, lower) < target -> {                 return Pair(                     lower,                     Pair(                         if (lower.first == xmax) xmax else lower.first + 1,                         if (lower.second == ymax) ymax else lower.second + 1                     )                 )             }             else -> return null         }     }   }  

基于 Ant Design Pro 页面标签化展示的研究与实现

Posted: 15 May 2021 11:31 AM PDT

效果预览 🚀

snapshot

摘要

Ant Design 作为一个昔日世界第一的 UI 库,影响力自是足够深远。而由官方推出的「开箱即用的中台前端 /设计解决方案」—— Ant Design Pro 也日趋成熟。较为遗憾的是 Ant Design Pro 官方并没有提供页面标签化展示的功能,因为当时环境的需要,我走上了这条页面标签化的不归路……

关键词:Ant Design ProUmi标签页页面标签化

绪论

从 19 年偶然发现了 Ant Design Pro (以下简称 Pro )以来,对我的技术发展有着不容忽视的影响,当然本文着重讨论我在页面标签化展示的研究与实现。刚接触 Pro 的时候自己还是一个工作中用 Java,业余学习 React 而转型前端的毕业一年的菜鸟。突然面对 Pro 这样一个庞杂的脚手架(当时的版本 Pro v2 ),而且还要做一个自己本来毫无头绪的功能时,我是拒绝的,奈何没人顶上只能自己硬着头皮搞了。

道路是崎岖的

一切的起点都要从一个不得不提的官方仓库关于此讨论甚多的 issue ——能否提供 tab 切换模式说起。感谢其中相关的仓库提供的思路。当时研究了好几个仓库的源码,主要有两个思路:

  • 读取路由配置生成一个扁平的路由映射,再为其他激活页面的组件注入点击事件回调;
  • 拦截 children,子组件缓存不同路由下的 children,再根据当前的 location 渲染对应的组件。

第一个思路是通过点击事件来更新标签页的渲染,对其他组件还存在一定的侵入性,实现不够优雅,更为尴尬的是不支持页面的嵌套路由渲染(后来知道了路由配置中的组件其实是 Switch 组件时,应该也能实现嵌套路由渲染)。

第二个思路就没有了对于其他组件的侵入性,只需要监听 childrenlocation 的变化即可。印象中由于彼时 hooks 还未发布,通过类组件实现颇为繁琐。hooks 正式发布后又重构成了函数式组件实现。

前途是光明的

snapshot

近两年的时间里随着各方面的不断成熟,当前实现 Ant Design Pro Plus 已经支持了足够丰富的功能:

  • 支持页面的嵌套路由渲染
  • 两种标签页模式可选
    • 基于路由,每个路由只渲染一个标签页
    • 基于路由参数,计算出每个路由的所有参数的哈希值,不同的哈希值渲染不同的标签页
  • 可固定标签栏
  • 快捷操作
    • 刷新标签页 - window.reloadTab()
    • 关闭标签页 - window.closeTab()
    • 返回之前标签页 - window.goBackTab()
    • 关闭并返回之前标签页 - window.closeAndGoBackTab()
  • reloadable,支持在头部操作栏刷新当前标签页
  • follow,路由定义中新增配置,默认打开方式是添加到所有标签页最后面,可通过配置该属性,使得一个标签页在 follow 指定的标签页后面打开(可参考查询页 Demo )
  • persistent,支持页面刷新之后恢复上次的标签页状态

得益于 hooks 功能的加持,封装了 useTabs 的 hook,核心功能一目了然。如此,只需要根据状态渲染标签页即可。

核心逻辑

作为一个不那么简单的功能,需要注意的细节自然不少,这里重点介绍两个核心函数。

getOriginalRenderRoute

根据 location 和原始的路由定义解析出待渲染的路由定义对象 RenderRoute,**核心是算出正确的 renderKey**,标签页的唯一性主要由其决定(基于路由参数的标签页还需要结合哈希值)。

getOriginalRenderRoute

注释应该还算清晰,尽力覆盖了一些我所能考虑到的各种情况。特别的是做了一个缓存,避免反复计算 renderKey

withRouteTab

页面性能优化高阶函数。默认情况下,每次切换都会触发所有标签页的渲染,当打开标签页太多且页面较为复杂时,由于没有必要的渲染可能会造成操作标签页时有明显的反馈延迟,可通过此高阶函数包裹页面组件以优化渲染性能。

一个难题

近两年的发展并不是一帆风顺,很多问题都算是不痛不痒,一个萝卜一个坑都能解决,但是一个关于 Umi 的难题折磨了我很长的时间。

Umi 升级到 3.0 的时候也尝试升级项目的 Umi 版本,不升不知道,一升吓一跳,切换时所有标签页都会渲染成当前 location 对应的页面内容,当时我就震惊了,不禁陷入了哲学三问:我是谁?我在哪儿?我要干嘛?

相当长的一段时间都毫无头绪,也提了 issue —— 「想了解一下 umi 2 与 3 对路由组件处理的异同」,没有得到反馈 _(:3J∠)_ 直到感觉被 Umi 抛下了好远好远,无奈再次硬着头皮研究了 Umi 两个版本之间关于路由渲染的源码,功夫不负有心人,最后终于找到了病根,成功升级 Umi 的版本,这也是该功能仅支持 ^umi@2.0.0 | ^umi@3.3.8 的原因。

总结

对比已知的其他实现要么断更,要么功能不够完善,要么二者兼备。一个功能维护了近两年,之所以开篇提到「我走上了这条页面标签化的不归路」也正是这个原因,好在现在思路越来越清晰了。

正是在输出这篇文章的时候,突然想到可以将前文提到的两种思路整合,貌似也是个不错的方案,即只监听 location 并移除 children 的依赖。不过后续的重点可能还是侧重于将此功能插件化集成到 Umi 中。

前端一出道就碰到了 Pro,应该算是一大幸事了。以此为基础,对于前端开发的技术栈有了一系列较为成熟的认识,同时培养了较好的开发习惯,也为后续的自身技术上的可持续发展提供了源源不断的动力。当然,由于自身能力所限,过程中可能会有不足之处,对于 Pro 页面标签化展示这一问题重点是抛砖引玉,如果有任何意见或建议,欢迎批评指正。

小弟刚学后端和 MySql, 请教一下事物的知识点. NodeJs+MySql

Posted: 15 May 2021 10:57 AM PDT

看了很多帖子,开启事务,又是锁的,有点懵逼.

能不能直观的给我解释一个例子, 比如现在这个商品就一件,怎么防止两个人同时下单. 可以理解为在执行一个方法的时候,让另外一个方法等待. 具体该如何 弄这个事物.

真的没完明白,求求大佬帮忙解释一下. 或者直接告诉我该怎么写,我一看就明白了. 感谢感谢🙏

关于抓包的问题

Posted: 15 May 2021 09:54 AM PDT

使用 Charles 抓 app 接口

手机已经设置了证书,但是在 https 请求下,依然 unknown

SSL handshake with client failed: An unknown issue occurred processing the certificate (certificate_unknown )  

手机是红米、miui12 、对应安卓版本是 10

网上说是因为安卓 10 支持的凭证必须是系统凭证,但是我安装的凭证是用户凭证

所以想问下有什么解决办法吗

求教,为什么脚本运行是总会提示 grep 命令没有参数

Posted: 14 May 2021 11:51 PM PDT

学着写了一下 shell 脚本,用来备份数据库, 运行正常
但是每次运行都会先提示 Usage: grep [OPTION]... PATTERN [FILE],之后才是"MySQL backup Starting!"

脚本贴上
#!/bin/bash
# 数据库登录参数
user="root"
password="*******!"
host="127.0.0.1"
port="3306"
db_name="www"
# 备份文件存放地址
backup_path="/root/backups/mysql"
# 当天年月日
date=$(date +"%Y-%m-%d")
# 是否删除过期数据
expire_backup_delete="OFF"
expire_days=7
backup_time=$date
backup_dir=$backup_location
welcome_msg="MySQL backup Starting!"
# 判断 mysql 实例是否正常运行
mysql_ps=`ps -ef |grep mysql |wc -l`
mysql_listen=`netstat -an |grep LISTEN |grep $mysql_port|wc -l`
if [ [$mysql_ps == 0] -o [$mysql_listen == 0] ]; then
echo "ERROR:MySQL is not running! backup stop!"
exit
else
echo $welcome_msg
fi
# 数据库到 SQL 文件
mysqldump -u$user -p$password -h$host -P$port $db_name > $backup_path/$db_name-$date.sql
flag=`echo $?`
if [ $flag == "0" ];then
echo "database mysql_backup_$db_name success backup to $backup_dir/$db_name-$date.sql.gz"
else
echo "database mysql_backup_$db_name backup fail!"
fi
# 备份写入日志
echo "$backup_path/$db_name-$date" >> $backup_path/log.txt
# 删除过期数据
if [ "$expire_backup_delete" == "ON" -a "$backup_location" != "" ];then
`find $backup_location/ -type f -mtime +$expire_days | xargs rm -rf`
echo "Expired backup data delete complete!"
fi

No comments:

Post a Comment