MoreRSS

site iconSunZhongWei | 孙仲维

博客名「大象笔记」,全干程序员一名,曾在金山,DNSPod,腾讯云,常驻烟台。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

SunZhongWei | 孙仲维 的 RSS 预览

React Quill 富文本编辑器与 Ant Design Modal 同时使用时,html 标签消失

2024-11-22 13:09:25

这是一个无比诡异的 Quill 富文本编辑器组件的 bug,浪费了我周四整整一个下午。

唯一的收获是,把早已忘光的 React 组件封装,及组件通信的机制复习了一遍。

bug 现象

在一个 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>

说明服务器的处理及存储是没有问题的。

Quill 内容渲染

同时打印了前端逻辑传递给 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 版本都是一致的。

  • 然后回退了版本跟旧项目一致,不行;
  • 升级到最新的 react quill 2.0 正式版,也不行。

搜索关键词

我尝试了不少关键词,都没有找到类似的讨论,好不容易在 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

这里有解决方案,太专业了。虽然,都没有测试成功,但是有了一个大概的方向。

就是某些条件下,不应该更新这个值。

  1. 关闭 modal 动画
  2. 不使用 destroyOnClose
  3. 判断是否是第一次渲染
  4. 判断是否是用户输入 (这个是不合理的,因为我还有图片插入,也不是用户输入,而是 api)

解决方案:封装组件

封装成组件,延迟加载。即便不是这个原因,封装一个组件也很有必要。重复代码太多了。

没想到,居然通过 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 也解决不了这些前端的历史粪坑问题。

Linux fish shell 中自动补全 go Cobra 创建的命令

2024-11-20 17:19:43

书接上回,自从基于 cobra 完成了 Golang AST 解析 struct 字段,自动生成 CRUD 代码,又添加了部分功能。这个自动帮我搬砖的代码生成器基本完成了。😊

但是,在项目中使用的时候,还是有点小瑕疵,就是不能在 fish shell 中自动补全命令,主要是我创建的命令,命令我自己都记不住🥲。每次靠输入 cobra 帮助参数来查看,也略显麻烦。于是,我想能否像 smug 一样,实现自动补全命令。查了一下,果然可以:

添加 fish 自动补全

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 就能看到所有命令:

linux fish shell 中自动补全 go cobra 创建的命令

生成的 fish 配置内容是啥

纯好奇,打开 ~/.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 补全配置,之前的配置支持列出所有的命令。

fish 的配置目录结构

> tree ~/.config/fish/
/home/zhongwei/.config/fish/
├── completions
│   └── go_snip.fish
├── config.fish
└── fish_variables

1 directory, 3 files

ant design pro 的统一配置管理

2024-11-18 16:45:23

作为一个 CRUD boy,经常要新建管理后台的项目。确切的说,应该是经常要 ctrl c / ctrl v 来 copy 老的项目。

但是 ant design pro 好多配置不在一个统一的配置文件中,需要去多个地方修改设置。例如:

  • 顶部标题
  • 登录页的标题
  • logo 顶部,及登录页

到处找配置,非常浪费脑细胞,本已稀疏的头发也经受不住这么折腾。所以还是能统一管理比较好。

统一配置文件在哪里

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/*"]
    }
  }
}

其他页面的修改,可以参考这个方式,集中到一处,下次复制黏贴时,效率就高了不少。

加载页的 title

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" />

Golang AST 解析 struct 字段,自动生成 CRUD 代码

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 文件,自动解析出每个字段的名称,及类型。 然后自动生成:

  • react ant design 前端字段编辑界面,及列表展示界面
  • MySQL 创建表的 SQL
  • 自动填充 gorm 的可更新字段列表

生成命令

使用 cobra cli 添加一个新命令:

> cobra-cli add parseStruct

当然,如果是要通过 go generate 集成到项目中,并不需要 cobra 这类命令行工具。

解析 go 源码文件

用 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())
				}
			}
		}
	}
}

示例 model 代码

假设,我在 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

这里用到了三个库:

  • go/parser:用于解析 Go 源代码并生成 AST。
  • go/token:用于管理源代码的位置和标记。
  • go/ast:用于表示和操作 AST。

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
}

参考

升级到 golang 1.23 版本后,gorm 的 count 统计总是返回 0

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)

问题现象

  • 这个 count 总是返回 0。
  • 而且 debug 日志中,只有 find 对应的查询语句,但是没有 count 对应的 sql 语句。也就是说,并没有执行。
  • 这个问题,在升级到 golang 1.23 版本之后才出现的,之前在 go 1.18 版本中是正常的。

所以,看起来是 gorm 在 golang 1.23 下执行出错了,但是按照 gorm 的尿性,出错都是屁都不放一个。 所以直接跳过了 count 的执行,返回的就是初始值 0。

进一步测试

  • 如果去掉 Preload 的部分,count 是能正常运行的。
  • 加上 Preload 的,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

升级 gorm 版本

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
  • v1.25.12 是今年(2024年) 8 月份的最新版本
  • v1.23.2 是 2022 年 3 月的版本

测试

升级后,问题就解决了。。。

看来升级 go 版本,也是非常危险的一件事情,并不是百分百的向下兼容。 特别是 gorm 这种玩意,一定要测试后再发布。

接下来,就得把之前写的一堆 go 项目的 gorm 库升级一遍了 🥲