2024-11-22 13:09:25
这是一个无比诡异的 Quill 富文本编辑器组件的 bug,浪费了我周四整整一个下午。
唯一的收获是,把早已忘光的 React 组件封装,及组件通信的机制复习了一遍。
在一个 Ant Design Pro 写的后台操作界面中,在弹出的 Modal 组件中,内嵌一个 React Quill 的富文本编辑器组件。 进行内容编辑,输入 hello 换行,再换行,输入 world。保存。
再次打开 Modal,里面 Quill 展示的内容,会看到中间的换行不见了。。。 再次重复上面操作,hello world 直接变为了一行。 继续测试,会看到不单是换行,连列表样式也会消失。
debug 一下, 保存时,会看到向服务器 POST 了数据:
<p>hello</p><p><br></p><p>world</p>
再次从服务器 GET 拉取
<p>hello</p><p><br></p><p>world</p>
说明服务器的处理及存储是没有问题的。
同时打印了前端逻辑传递给 Modal 内 Quill 的内容,也是没问题的。
只有渲染时,才有问题:
<div class="ql-editor" data-gramm="false" contenteditable="true">
<p>hello</p>
<p>world</p>
</div>
会看到换行丢失。
再次保存,连 p 标签都会消失,最后只剩下
<p>helloworld</p>
之前的项目中,测试了一下,没有这个问题。
但是版本有差异:
旧项目
"react-quill": "^2.0.0-beta.2",
新项目
"react-quill": "^2.0.0-beta.4",
其他依赖,Quill 及 Ant Design 版本都是一致的。
我尝试了不少关键词,都没有找到类似的讨论,好不容易在 stack overflow 上找到一个 angular 中使用 Quill 的讨论。 现象非常像。
I recently encountered this problem, which was mostly apparent when the quill editor is rendered within tabs or steppers. The solution I've used is to use *ngIf to display the quill editor only when it is in view (An alternative is to trigger it on afterViewInit if what I've done doesn't exactly fit your project). Possibly, it strips all the tags and simply wraps the content with p tags.
看来正确的搜索关键词应该是:
quill strip all tags
终于找到了这个问题的讨论:
https://github.com/zenoamaro/react-quill/issues/814
这里有解决方案,太专业了。虽然,都没有测试成功,但是有了一个大概的方向。
就是某些条件下,不应该更新这个值。
封装成组件,延迟加载。即便不是这个原因,封装一个组件也很有必要。重复代码太多了。
没想到,居然通过 ReactQuill 再次封装解决了。同时,在代码中加上了 github 那个讨论中的是否是第一次渲染的判断。
然后通过 react ant design Form.Item 的 props.value / props.onChange 的数据传递机制将结果返回。
增加样式内置:
yarn add styled-components --save
组件代码:
import React from 'react';
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';
import { uploadImage } from '@/services/ant-design-pro/api';
import styled from 'styled-components';
const Wrapper = styled.div`
.ql-editor {
min-height: ${(props) => props.minHeight || 200}px;
max-height: ${(props) => props.maxHeight || 500}px;
}
`;
function QuillFucker(props: any) {
const quill = React.useRef(null);
const [content, setContent] = React.useState(props.value || '');
const [isInitialRender, setIsInitialRender] = React.useState(true);
React.useEffect(() => {
setIsInitialRender(false);
}, []);
const toolbarContainer = [
// ...
];
const imageHandler = () => {
// ...
};
const modules = React.useMemo(
() => ({
toolbar: {
container: toolbarContainer,
handlers: {
image: imageHandler,
},
},
}),
[],
);
return (
<Wrapper minHeight={props.minHeight} maxHeight={props.maxHeight}>
{!isInitialRender && (
<ReactQuill
ref={quill}
theme="snow"
value={content}
onChange={(value, _delta, source) => {
setContent(value);
props.onChange(value);
}}
modules={modules}
/>
)}
</Wrapper>
);
}
export default QuillFucker;
写前端代码,就如同在粪坑里游泳一般,首先你需要勇敢地跳进去。
然后,冷静下来,遇到💩💩💩不要慌,要坚信,会遇到更多的💩。
同时,在粪坑里继续制造你的💩,这样才能不至于窒息。
即便是 AI 也解决不了这些前端的历史粪坑问题。
2024-11-20 17:19:43
书接上回,自从基于 cobra 完成了 Golang AST 解析 struct 字段,自动生成 CRUD 代码,又添加了部分功能。这个自动帮我搬砖的代码生成器基本完成了。😊
但是,在项目中使用的时候,还是有点小瑕疵,就是不能在 fish shell 中自动补全命令,主要是我创建的命令,命令我自己都记不住🥲。每次靠输入 cobra 帮助参数来查看,也略显麻烦。于是,我想能否像 smug 一样,实现自动补全命令。查了一下,果然可以:
cobra 内置了针对各种 shell 的自动补全功能。诸如,fish,bash,zsh,powershell。
例如,我的搬砖工具名为 go_snip,要查看如何生成 fish 自动补全配置,可以输入命令查看
> go_snip completion fish --help
Generate the autocompletion script for the fish shell.
To load completions in your current shell session:
go_snip completion fish | source
To load completions for every new session, execute once:
go_snip completion fish > ~/.config/fish/completions/go_snip.fish
You will need to start a new shell for this setup to take effect.
Usage:
go_snip completion fish [flags]
Flags:
-h, --help help for fish
--no-descriptions disable completion descriptions
如果,之前 fish shell 没有配置过自动补全,需要新建一个目录:
mkdir ~/.config/fish/completions/
然后执行导入配置操作:
> go_snip completion fish > ~/.config/fish/completions/go_snip.fish
然后,就可以正常使用自动补全了。
按一次 tab 就会出现命令提示,按两次 tab 就能看到所有命令:
纯好奇,打开 ~/.config/fish/completions/go_snip.fish
略去了前面的函数定义。完全如同画鬼符一般,看不懂。。。
# Remove any pre-existing completions for the program since we will be handling all of them.
complete -c go_snip -e
# this will get called after the two calls below and clear the $__go_snip_perform_completion_once_result global
complete -c go_snip -n '__go_snip_clear_perform_completion_once_result'
# The call to __go_snip_prepare_completions will setup __go_snip_comp_results
# which provides the program's completion choices.
# If this doesn't require order preservation, we don't use the -k flag
complete -c go_snip -n 'not __go_snip_requires_order_preservation && __go_snip_prepare_completions' -f -a '$__go_snip_comp_results'
# otherwise we use the -k flag
complete -k -c go_snip -n '__go_snip_requires_order_preservation && __go_snip_prepare_completions' -f -a '$__go_snip_comp_results'
例如,我再新添加一个子命令
> cobra-cli add test1
> go install
并不需要再次导入 fish 补全配置,之前的配置支持列出所有的命令。
> tree ~/.config/fish/
/home/zhongwei/.config/fish/
├── completions
│ └── go_snip.fish
├── config.fish
└── fish_variables
1 directory, 3 files
2024-11-18 16:45:23
作为一个 CRUD boy,经常要新建管理后台的项目。确切的说,应该是经常要 ctrl c / ctrl v 来 copy 老的项目。
但是 ant design pro 好多配置不在一个统一的配置文件中,需要去多个地方修改设置。例如:
到处找配置,非常浪费脑细胞,本已稀疏的头发也经受不住这么折腾。所以还是能统一管理比较好。
config/defaultSettings.ts
虽然这里定义了一些配置。但是像登录页的标题就不受这里控制。
import defaultSettings from '@/../config/defaultSettings';
const { title } = defaultSettings;
<span className={styles.title}>{title}</span>
这里的 @ 符号对应的路径,定义在文件 jsconfig.json:
{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
其他页面的修改,可以参考这个方式,集中到一处,下次复制黏贴时,效率就高了不少。
config/config.ts 中,有个 layout 配置,读取了 defaultSettings 中的 title:
import defaultSettings from './defaultSettings';
layout: {
// https://umijs.org/zh-CN/plugins/plugin-layout
//locale: true,
siderWidth: 208,
...defaultSettings,
},
所以,直接修改:
src/pages/document.ejs
<title><%= context.config.layout.title%></title>
<link rel="icon" href="<%= context.config.publicPath +'favicon.ico'%>" type="image/x-icon" />
2024-11-18 13:51:58
上周基于 cobra 实现了一个 golang 的命令行工具, 参考:golang 快速开发命令行工具的神器 cobra & cobra cli,实现了一键生成 go gin 后台,及 react ant design 前端的 CRUD 工具。 大大提升了枯燥的 CRUD 劳作效率。并在两个项目上试水成功。 但是,还有一点不够完美,就是目前的 ant design 前端部分,只是个界面架子。 具体的编辑字段,还得手动一个个添加。这周又接到了一个无数 CRUD 的搬砖项目,我觉得有必要把这部分功能加上了。 这样才能无愧于我的“搬砖之王”的称号。
即,使用 golang 解析一个 golang 的包含 struct 的 model 文件,自动解析出每个字段的名称,及类型。 然后自动生成:
使用 cobra cli 添加一个新命令:
> cobra-cli add parseStruct
当然,如果是要通过 go generate 集成到项目中,并不需要 cobra 这类命令行工具。
用 AI 生成了一个代码架子,并稍作修改:
package cmd
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"github.com/spf13/cobra"
)
// parseStructCmd represents the parseStruct command
var parseStructCmd = &cobra.Command{
Use: "parseStruct",
Short: "解析 model 文件中的 struct 字段,生成建表 SQL 及 antd pro 字段, 及可 update 字段列表",
Args: cobra.ExactArgs(1), // 参数为 model 文件路径
Example: "go_snip parseStruct models/device.go",
Run: parseStruct,
}
func init() {
rootCmd.AddCommand(parseStructCmd)
}
func parseStruct(cmd *cobra.Command, args []string) {
filePath := args[0]
fset := token.NewFileSet()
// 解析Go文件
file, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)
if err != nil {
fmt.Printf("解析文件失败:%v\n", err)
return
}
// 遍历文件中的所有声明
for _, decl := range file.Decls {
// 检查声明是否是结构体类型声明
genDecl, ok := decl.(*ast.GenDecl)
if ok && genDecl.Tok == token.TYPE && len(genDecl.Specs) > 0 {
typeSpec := genDecl.Specs[0].(*ast.TypeSpec)
structType, ok := typeSpec.Type.(*ast.StructType)
if ok {
// 输出结构体名称
fmt.Printf("结构体名称:%s\n", typeSpec.Name.Name)
// 遍历结构体的字段
for _, field := range structType.Fields.List {
fmt.Printf("名称:%s, 类型:%s, tag: %v, 注释: %s \n",
field.Names[0].Name, field.Type, field.Tag, field.Comment.Text())
}
}
}
}
}
假设,我在 models 目录下有个 device.go 的 go 文件。 定义了设备信息的结构体,用于 gorm 的数据库操作:
package models
import "time"
type Device struct {
ID uint
CreatedAt time.Time
UpdatedAt time.Time
Name string // 设备名称
Model string // 型号
Manufacturer string // 生产厂家
Address string // 地址
Admin string // 负责人姓名
Tel string // 联系电话
Images string // 设备照片。多张,地址使用英文逗号分隔
Attachments string // 附件。支持多个附近,地址使用英文逗号分隔
TotalCollect int `json:"total"` // 收藏总数
}
func (Device) TableName() string {
return "device"
}
运行命令,得到的解析结果如下:
> go run main.go parseStruct <some_project>/models/device.go
结构体名称:Device
名称:ID, 类型:uint, tag: <nil>, 注释:
名称:CreatedAt, 类型:&{time Time}, tag: <nil>, 注释:
名称:UpdatedAt, 类型:&{time Time}, tag: <nil>, 注释:
名称:Name, 类型:string, tag: <nil>, 注释: 设备名称
名称:Model, 类型:string, tag: <nil>, 注释: 型号
名称:Manufacturer, 类型:string, tag: <nil>, 注释: 生产厂家
名称:Address, 类型:string, tag: <nil>, 注释: 地址
名称:Admin, 类型:string, tag: <nil>, 注释: 负责人姓名
名称:Tel, 类型:string, tag: <nil>, 注释: 联系电话
名称:Images, 类型:string, tag: <nil>, 注释: 设备照片。多张,地址使用英文逗号分隔
名称:Attachments, 类型:string, tag: <nil>, 注释: 附件。支持多个附近,地址使用英文逗号分隔
名称:TotalCollect, 类型:int, tag: &{518 STRING `json:"total"`}, 注释: 收藏总数
可以看到,字段名称,类型,及 tag 和注释都能正确的解析出来了。
后面,就可以逐一处理每个字段,针对不同类型,生成不同的前端 ant design 组件代码了。
这里用到了三个库:
AST, 英文全程是 Abstract Syntax Tree, 即抽象语法树。
抽象语法树是源代码的一种抽象表示形式,它以树状结构来展现程序的语法结构,将代码中的各种语法元素(如语句、表达式、类型定义等)转化为节点,节点之间通过父子关系等连接,能够更清晰地体现代码的逻辑和语法构成,而忽略掉诸如空格、括号等具体的语法细节(即词法细节)。
查看 go 的 ast.go 实现代码,可以看到,其定义了一些常见的语法元素:
// All node types implement the Node interface.
type Node interface {
Pos() token.Pos // position of first character belonging to the node
End() token.Pos // position of first character immediately after the node
}
// All expression nodes implement the Expr interface.
type Expr interface {
Node
exprNode()
}
// All statement nodes implement the Stmt interface.
type Stmt interface {
Node
stmtNode()
}
// All declaration nodes implement the Decl interface.
type Decl interface {
Node
declNode()
}
// Comments
type Comment struct {
Slash token.Pos // position of "/" starting the comment
Text string // comment text (excluding '\n' for //-style comments)
}
例如,常用的 swagger 库 swaggo 就是基于 AST 解析结果,然后再分析注释代码,生成 swagger 操作界面:
https://github.com/swaggo/swag/blob/master/parser.go
import goparser "go/parser"
// ParseGeneralAPIInfo parses general api info for given mainAPIFile path.
func (parser *Parser) ParseGeneralAPIInfo(mainAPIFile string) error {
fileTree, err := goparser.ParseFile(token.NewFileSet(), mainAPIFile, nil, goparser.ParseComments)
if err != nil {
return fmt.Errorf("cannot parse source files %s: %s", mainAPIFile, err)
}
parser.swagger.Swagger = "2.0"
for _, comment := range fileTree.Comments {
comments := strings.Split(comment.Text(), "\n")
if !isGeneralAPIComment(comments) {
continue
}
err = parseGeneralAPIInfo(parser, comments)
if err != nil {
return err
}
}
return nil
}
2024-11-15 17:10:58
之前为了使用 excelize 的一个新功能, golang excelize 自动解析 excel 单元格的字体颜色, 将 golang 由 1.18 升级到了 1.23。但是遇到了 gorm 的一个 bug。
var items []models.Article
db := models.DB.Model(&models.Article{}).
Preload("Category")
db = db.Session(&gorm.Session{})
db.Order("id desc").
Limit(limit).
Offset((page - 1) * limit).
Find(&items)
var count int64 = 0
db.Count(&count)
所以,看起来是 gorm 在 golang 1.23 下执行出错了,但是按照 gorm 的尿性,出错都是屁都不放一个。 所以直接跳过了 count 的执行,返回的就是初始值 0。
> go version
go version go1.23.2 linux/amd64
> grep gorm go.mod
gorm.io/datatypes v1.0.6
gorm.io/driver/mysql v1.3.2
gorm.io/gorm v1.23.2
google 了半天,也看了 github 上 gorm 的相关 issue,有同样的问题反馈,但是没有人答复。
于是想先试试升级 gorm 版本到最新,看看是否可以解决
> go get -u gorm.io/gorm
go: downloading gorm.io/gorm v1.25.12
go: downloading golang.org/x/text v0.20.0
go: downloading golang.org/x/sys v0.5.0
go: downloading golang.org/x/sync v0.9.0
go: upgraded github.com/jinzhu/now v1.1.4 => v1.1.5
go: upgraded golang.org/x/sync v0.0.0-20201207232520-09787c993a3a => v0.9.0
go: upgraded golang.org/x/sys v0.0.0-20211020174200-9d6173849985 => v0.5.0
go: upgraded golang.org/x/text v0.3.7 => v0.20.0
go: upgraded gorm.io/gorm v1.23.2 => v1.25.12
升级后,问题就解决了。。。
看来升级 go 版本,也是非常危险的一件事情,并不是百分百的向下兼容。 特别是 gorm 这种玩意,一定要测试后再发布。
接下来,就得把之前写的一堆 go 项目的 gorm 库升级一遍了 🥲