前端使用 Konva 实现可视化设计器(12)- 连接线 - 直线
这一章实现的连接线,目前仅支持直线连接,为了能够不影响原有的其它功能,尝试了2、3个实现思路,最终实测这个实现方式目前来说最为合适了。
请大家动动小手,给我一个免费的 Star 吧~
大家如果发现了 Bug,欢迎来提 Issue 哟~

相关定义
- 连接点
  
记录了连接点相关信息,并不作为素材而存在,仅记录信息,即导出导入的时候,并不会出现所谓的连接点节点。
它存放在节点身上,因此导出、导入自然而然就可以持久化了。
src/Render/draws/LinkDraw.ts
// 连接点
export interface LinkDrawPoint {
  id: string
  groupId: string
  visible: boolean
  pairs: LinkDrawPair[]
  x: number
  y: number
}
- 连接对
  
一个连接点,记录从该点出发的多条连接线信息,作为连接对信息存在。
src/Render/draws/LinkDraw.ts
// 连接对
export interface LinkDrawPair {
  id: string
  from: {
    groupId: string
    pointId: string
  }
  to: {
    groupId: string
    pointId: string
  }
}
- 连接点(锚点)
  
它是拖入素材的时候生成的真实节点,归属于所在的节点中,存在却不可见,关键作用是同步连接点真实坐标,尤其是节点发生 transform 时候,必须依赖它获得 transform 后连接点变化。
src/Render/handlers/DragOutsideHandlers.ts
// 略
		drop: (e: GlobalEventHandlersEventMap['drop']) => {
// 略
              const points = [
                // 左
                { x: 0, y: group.height() / 2 },
                // 右
                {
                  x: group.width(),
                  y: group.height() / 2
                },
                // 上
                { x: group.width() / 2, y: 0 },
                // 下
                {
                  x: group.width() / 2,
                  y: group.height()
                }
              ]
              // 连接点信息
              group.setAttrs({
                points: points.map(
                  (o) =>
                    ({
                      ...o,
                      id: nanoid(),
                      groupId: group.id(),
                      visible: true,
                      pairs: []
                    }) as LinkDrawPoint
                )
              })
              // 连接点(锚点)
              for (const point of group.getAttr('points') ?? []) {
                group.add(
                  new Konva.Circle({
                    name: 'link-anchor',
                    id: point.id,
                    x: point.x,
                    y: point.y,
                    radius: this.render.toStageValue(1),
                    stroke: 'rgba(0,0,255,1)',
                    strokeWidth: this.render.toStageValue(2),
                    visible: false
                  })
                )
              }
              group.on('mouseenter', () => {
                // 显示 连接点
                this.render.linkTool.pointsVisible(true, group)
              })
              // hover 框(多选时才显示)
              group.add(
                new Konva.Rect({
                  id: 'hoverRect',
                  width: image.width(),
                  height: image.height(),
                  fill: 'rgba(0,255,0,0.3)',
                  visible: false
                })
              )
              group.on('mouseleave', () => {
                // 隐藏 连接点
                this.render.linkTool.pointsVisible(false, group)
                // 隐藏 hover 框
                group.findOne('#hoverRect')?.visible(false)
              })
// 略
}
// 略
- 连接线
  
根据连接点信息,绘制的线条,也不作为素材而存在,导出导入的时候,也不会出现所谓的连接点节点。不过,在导出图片、SVG和用于预览框的时候,会直接利用线条节点导出、显示。
src/Render/tools/ImportExportTool.ts
// 略
  /**
   * 获得显示内容
   * @param withLink 是否包含线条
   * @returns
   */
  getView(withLink: boolean = false) {
  	// 复制画布
    const copy = this.render.stage.clone()
    // 提取 main layer 备用
    const main = copy.find('#main')[0] as Konva.Layer
    const cover = copy.find('#cover')[0] as Konva.Layer
    // 暂时清空所有 layer
    copy.removeChildren()
    // 提取节点
    let nodes = main.getChildren((node) => {
      return !this.render.ignore(node)
    })
    if (withLink) {
      nodes = nodes.concat(
        cover.getChildren((node) => {
          return node.name() === Draws.LinkDraw.name
        })
      )
    }
  	// 略
  }
// 略
src/Render/draws/PreviewDraw.ts
  override draw() {
      // 略
      const main = this.render.stage.find('#main')[0] as Konva.Layer
      const cover = this.render.stage.find('#cover')[0] as Konva.Layer
      // 提取节点
      const nodes = [
        ...main.getChildren((node) => {
          return !this.render.ignore(node)
        }),
        // 补充连线
        ...cover.getChildren((node) => {
          return node.name() === Draws.LinkDraw.name
        })
      ]
      // 略
  }
- 连接线(临时)
  
起点鼠标按下 -> 拖动显示线条 -> 终点鼠标释放 -> 产生连接信息 LinkDrawPoint. LinkDrawPair
// 连接线(临时)
export interface LinkDrawState {
  linkingLine: {
    group: Konva.Group
    circle: Konva.Circle
    line: Konva.Line
  } | null
}
代码文件
新增几个关键的代码文件:
src/Render/draws/LinkDraw.ts
根据 连接点.链接对 绘制 连接点、连接线,及其相关的事件处理
它的绘制顺序,应该放在绘制 比例尺、预览框之前。
src/Render/handlers/LinkHandlers.ts
根据 连接线(临时)信息,绘制/移除 连接线(临时)
src/Render/tools/LinkTool.ts
移除连接线,控制 连接点 的显示/隐藏
移除连接线,实际上就是移除其 连接对 信息
// 略
export class LinkTool {
  // 略
  pointsVisible(visible: boolean, group?: Konva.Group) {
    if (group) {
      this.pointsVisibleEach(visible, group)
    } else {
      const groups = this.render.layer.find('.asset') as Konva.Group[]
      for (const group of groups) {
        this.pointsVisibleEach(visible, group)
      }
    }
    // 更新连线
    this.render.draws[Draws.LinkDraw.name].draw()
    // 更新预览
    this.render.draws[Draws.PreviewDraw.name].draw()
  }
  remove(line: Konva.Line) {
    const { groupId, pointId, pairId } = line.getAttrs()
    if (groupId && pointId && pairId) {
      const group = this.render.layer.findOne(`#${groupId}`) as Konva.Group
      if (group) {
        const points = (group.getAttr('points') ?? []) as LinkDrawPoint[]
        const point = points.find((o) => o.id === pointId)
        if (point) {
          const pairIndex = (point.pairs ?? ([] as LinkDrawPair[])).findIndex(
            (o) => o.id === pairId
          )
          if (pairIndex > -1) {
            point.pairs.splice(pairIndex, 1)
            group.setAttr('points', points)
            // 更新连线
            this.render.draws[Draws.LinkDraw.name].draw()
            // 更新预览
            this.render.draws[Draws.PreviewDraw.name].draw()
          }
        }
      }
    }
  }
}
关键逻辑
- 绘制 连接线(临时)
  
src/Render/draws/LinkDraw.ts
起点鼠标按下 'mousedown' -> 略 -> 终点鼠标释放 'mouseup'
// 略
export class LinkDraw extends Types.BaseDraw implements Types.Draw {
  // 略
  override draw() {
    this.clear()
    // stage 状态
    const stageState = this.render.getStageState()
    const groups = this.render.layer.find('.asset') as Konva.Group[]
    const points = groups.reduce((ps, group) => {
      return ps.concat(Array.isArray(group.getAttr('points')) ? group.getAttr('points') : [])
    }, [] as LinkDrawPoint[])
    const pairs = points.reduce((ps, point) => {
      return ps.concat(point.pairs ? point.pairs : [])
    }, [] as LinkDrawPair[])
    // 略
    // 连接点
    for (const point of points) {
      const group = groups.find((o) => o.id() === point.groupId)
      // 非 选择中
      if (group && !group.getAttr('selected')) {
        const anchor = this.render.layer.findOne(`#${point.id}`)
        if (anchor) {
          const circle = new Konva.Circle({
            id: point.id,
            groupId: group.id(),
            x: this.render.toStageValue(anchor.absolutePosition().x - stageState.x),
            y: this.render.toStageValue(anchor.absolutePosition().y - stageState.y),
            radius: this.render.toStageValue(this.option.size),
            stroke: 'rgba(255,0,0,0.2)',
            strokeWidth: this.render.toStageValue(1),
            name: 'link-point',
            opacity: point.visible ? 1 : 0
          })
          // 略
          circle.on('mousedown', () => {
            this.render.selectionTool.selectingClear()
            const pos = this.render.stage.getPointerPosition()
            if (pos) {
              // 临时 连接线 画
              this.state.linkingLine = {
                group: group,
                circle: circle,
                line: new Konva.Line({
                  name: 'linking-line',
                  points: _.flatten([
                    [circle.x(), circle.y()],
                    [
                      this.render.toStageValue(pos.x - stageState.x),
                      this.render.toStageValue(pos.y - stageState.y)
                    ]
                  ]),
                  stroke: 'blue',
                  strokeWidth: 1
                })
              }
              this.layer.add(this.state.linkingLine.line)
            }
          })
          // 略
      }
    }
  }
}
src/Render/handlers/LinkHandlers.ts
拖动显示线条、移除 连接线(临时)
从起点到鼠标当前位置
  handlers = {
    stage: {
      mouseup: () => {
        const linkDrawState = (this.render.draws[Draws.LinkDraw.name] as Draws.LinkDraw).state
        // 临时 连接线 移除
        linkDrawState.linkingLine?.line.remove()
        linkDrawState.linkingLine = null
      },
      mousemove: () => {
        const linkDrawState = (this.render.draws[Draws.LinkDraw.name] as Draws.LinkDraw).state
        const pos = this.render.stage.getPointerPosition()
        if (pos) {
          // stage 状态
          const stageState = this.render.getStageState()
          // 临时 连接线 画
          if (linkDrawState.linkingLine) {
            const { circle, line } = linkDrawState.linkingLine
            line.points(
              _.flatten([
                [circle.x(), circle.y()],
                [
                  this.render.toStageValue(pos.x - stageState.x),
                  this.render.toStageValue(pos.y - stageState.y)
                ]
              ])
            )
          }
        }
      }
    }
  }
- 产生连接信息
src/Render/draws/LinkDraw.ts
// 略
export class LinkDraw extends Types.BaseDraw implements Types.Draw {
  // 略
  override draw() {
    this.clear()
    // stage 状态
    const stageState = this.render.getStageState()
    const groups = this.render.layer.find('.asset') as Konva.Group[]
    const points = groups.reduce((ps, group) => {
      return ps.concat(Array.isArray(group.getAttr('points')) ? group.getAttr('points') : [])
    }, [] as LinkDrawPoint[])
    const pairs = points.reduce((ps, point) => {
      return ps.concat(point.pairs ? point.pairs : [])
    }, [] as LinkDrawPair[])
    // 略
    // 连接点
    for (const point of points) {
      const group = groups.find((o) => o.id() === point.groupId)
      // 非 选择中
      if (group && !group.getAttr('selected')) {
        const anchor = this.render.layer.findOne(`#${point.id}`)
        if (anchor) {
          const circle = new Konva.Circle({
            id: point.id,
            groupId: group.id(),
            x: this.render.toStageValue(anchor.absolutePosition().x - stageState.x),
            y: this.render.toStageValue(anchor.absolutePosition().y - stageState.y),
            radius: this.render.toStageValue(this.option.size),
            stroke: 'rgba(255,0,0,0.2)',
            strokeWidth: this.render.toStageValue(1),
            name: 'link-point',
            opacity: point.visible ? 1 : 0
          })
          // 略
          circle.on('mouseup', () => {
            if (this.state.linkingLine) {
              const line = this.state.linkingLine
              // 不同连接点
              if (line.circle.id() !== circle.id()) {
                const toGroup = groups.find((o) => o.id() === circle.getAttr('groupId'))
                if (toGroup) {
                  const fromPoints = (
                    Array.isArray(line.group.getAttr('points')) ? line.group.getAttr('points') : []
                  ) as LinkDrawPoint[]
                  const fromPoint = fromPoints.find((o) => o.id === line.circle.id())
                  if (fromPoint) {
                    const toPoints = (
                      Array.isArray(toGroup.getAttr('points')) ? toGroup.getAttr('points') : []
                    ) as LinkDrawPoint[]
                    const toPoint = toPoints.find((o) => o.id === circle.id())
                    if (toPoint) {
                      if (Array.isArray(fromPoint.pairs)) {
                        fromPoint.pairs = [
                          ...fromPoint.pairs,
                          {
                            id: nanoid(),
                            from: {
                              groupId: line.group.id(),
                              pointId: line.circle.id()
                            },
                            to: {
                              groupId: circle.getAttr('groupId'),
                              pointId: circle.id()
                            }
                          }
                        ]
                      }
                      // 更新历史
                      this.render.updateHistory()
                      this.draw()
                      // 更新预览
                      this.render.draws[Draws.PreviewDraw.name].draw()
                    }
                  }
                }
              }
              // 临时 连接线 移除
              this.state.linkingLine?.line.remove()
              this.state.linkingLine = null
            }
          })
          this.group.add(circle)
        }
        // 略
      }
    }
  }
}
- 绘制 连接线
  
src/Render/draws/LinkDraw.ts
这里就是利用了上面提到的 连接点(锚点),通过它的 absolutePosition 获得真实位置。
// 略
export class LinkDraw extends Types.BaseDraw implements Types.Draw {
  // 略
  override draw() {
    this.clear()
    // stage 状态
    const stageState = this.render.getStageState()
    const groups = this.render.layer.find('.asset') as Konva.Group[]
    const points = groups.reduce((ps, group) => {
      return ps.concat(Array.isArray(group.getAttr('points')) ? group.getAttr('points') : [])
    }, [] as LinkDrawPoint[])
    const pairs = points.reduce((ps, point) => {
      return ps.concat(point.pairs ? point.pairs : [])
    }, [] as LinkDrawPair[])
    // 连接线
    for (const pair of pairs) {
      const fromGroup = groups.find((o) => o.id() === pair.from.groupId)
      const fromPoint = points.find((o) => o.id === pair.from.pointId)
      const toGroup = groups.find((o) => o.id() === pair.to.groupId)
      const toPoint = points.find((o) => o.id === pair.to.pointId)
      if (fromGroup && toGroup && fromPoint && toPoint) {
        const fromAnchor = this.render.layer.findOne(`#${fromPoint.id}`)
        const toAnchor = this.render.layer.findOne(`#${toPoint.id}`)
        if (fromAnchor && toAnchor) {
          const line = new Konva.Line({
            name: 'link-line',
            // 用于删除连接线
            groupId: fromGroup.id(),
            pointId: fromPoint.id,
            pairId: pair.id,
            //
            points: _.flatten([
              [
                this.render.toStageValue(fromAnchor.absolutePosition().x - stageState.x),
                this.render.toStageValue(fromAnchor.absolutePosition().y - stageState.y)
              ],
              [
                this.render.toStageValue(toAnchor.absolutePosition().x - stageState.x),
                this.render.toStageValue(toAnchor.absolutePosition().y - stageState.y)
              ]
            ]),
            stroke: 'red',
            strokeWidth: 2
          })
          this.group.add(line)
          // 连接线 hover 效果
          line.on('mouseenter', () => {
            line.stroke('rgba(255,0,0,0.6)')
            document.body.style.cursor = 'pointer'
          })
          line.on('mouseleave', () => {
            line.stroke('red')
            document.body.style.cursor = 'default'
          })
        }
      }
    }
    // 略
  }
}
- 绘制 连接点
src/Render/draws/LinkDraw.ts
// 略
export class LinkDraw extends Types.BaseDraw implements Types.Draw {
 // 略
  override draw() {
    this.clear()
    // stage 状态
    const stageState = this.render.getStageState()
    const groups = this.render.layer.find('.asset') as Konva.Group[]
    const points = groups.reduce((ps, group) => {
      return ps.concat(Array.isArray(group.getAttr('points')) ? group.getAttr('points') : [])
    }, [] as LinkDrawPoint[])
    const pairs = points.reduce((ps, point) => {
      return ps.concat(point.pairs ? point.pairs : [])
    }, [] as LinkDrawPair[])
    // 略
    // 连接点
    for (const point of points) {
      const group = groups.find((o) => o.id() === point.groupId)
      // 非 选择中
      if (group && !group.getAttr('selected')) {
        const anchor = this.render.layer.findOne(`#${point.id}`)
        if (anchor) {
          const circle = new Konva.Circle({
            id: point.id,
            groupId: group.id(),
            x: this.render.toStageValue(anchor.absolutePosition().x - stageState.x),
            y: this.render.toStageValue(anchor.absolutePosition().y - stageState.y),
            radius: this.render.toStageValue(this.option.size),
            stroke: 'rgba(255,0,0,0.2)',
            strokeWidth: this.render.toStageValue(1),
            name: 'link-point',
            opacity: point.visible ? 1 : 0
          })
          // hover 效果
          circle.on('mouseenter', () => {
            circle.stroke('rgba(255,0,0,0.5)')
            circle.opacity(1)
            document.body.style.cursor = 'pointer'
          })
          circle.on('mouseleave', () => {
            circle.stroke('rgba(255,0,0,0.2)')
            circle.opacity(0)
            document.body.style.cursor = 'default'
          })
          // 略
      }
    }
  }
}
- 复制
有几个关键:
- 更新 id,包括:节点、连接点、锚点、连接对
- 重新绑定相关事件
src/Render/tools/CopyTool.ts
// 略
export class CopyTool {
  // 略
  /**
   * 复制粘贴
   * @param nodes 节点数组
   * @param skip 跳过检查
   * @returns 复制的元素
   */
  copy(nodes: Konva.Node[]) {
    const clones: Konva.Group[] = []
    for (const node of nodes) {
      if (node instanceof Konva.Transformer) {
        // 复制已选择
        const backup = [...this.render.selectionTool.selectingNodes]
        this.render.selectionTool.selectingClear()
        this.copy(backup)
        return
      } else {
        // 复制未选择(先记录,后处理)
        clones.push(node.clone())
      }
    }
    // 处理克隆节点
    // 新旧 id 映射
    const groupIdChanges: { [index: string]: string } = {}
    const pointIdChanges: { [index: string]: string } = {}
    // 新 id、新事件
    for (const copy of clones) {
      const gid = nanoid()
      groupIdChanges[copy.id()] = gid
      copy.id(gid)
      const pointsClone = _.cloneDeep(copy.getAttr('points') ?? [])
      copy.setAttr('points', pointsClone)
      for (const point of pointsClone) {
        const pid = nanoid()
        pointIdChanges[point.id] = pid
        const anchor = copy.findOne(`#${point.id}`)
        anchor?.id(pid)
        point.id = pid
        point.groupId = copy.id()
        point.visible = false
      }
      copy.off('mouseenter')
      copy.on('mouseenter', () => {
        // 显示 连接点
        this.render.linkTool.pointsVisible(true, copy)
      })
      copy.off('mouseleave')
      copy.on('mouseleave', () => {
        // 隐藏 连接点
        this.render.linkTool.pointsVisible(false, copy)
        // 隐藏 hover 框
        copy.findOne('#hoverRect')?.visible(false)
      })
      // 使新节点产生偏移
      copy.setAttrs({
        x: copy.x() + this.render.toStageValue(this.render.bgSize) * this.pasteCount,
        y: copy.y() + this.render.toStageValue(this.render.bgSize) * this.pasteCount
      })
    }
    // pairs 新 id
    for (const copy of clones) {
      const points = copy.getAttr('points') ?? []
      for (const point of points) {
        for (const pair of point.pairs) {
          // id 换新
          pair.id = nanoid()
          pair.from.groupId = groupIdChanges[pair.from.groupId]
          pair.from.pointId = pointIdChanges[pair.from.pointId]
          pair.to.groupId = groupIdChanges[pair.to.groupId]
          pair.to.pointId = pointIdChanges[pair.to.pointId]
        }
      }
    }
    // 略
  }
}
接下来,计划实现下面这些功能:
- 连接线 - 折线(头疼)
- 等等。。。
More Stars please!勾勾手指~
前端使用 Konva 实现可视化设计器(12)- 连接线 - 直线的更多相关文章
- 惊闻企业Web应用生成平台 活字格 V4.0 免费了,不单可视化设计器免费,服务器也免费!
		官网消息: 针对活字格开发者,新版本完全免费!您可下载活字格 Web 应用生成平台 V4.0 Updated 1,方便的创建各类 Web 应用系统,任意部署,永不过期. 我之前学习过活字格,也曾经向用 ... 
- (原创)【B4A】一步一步入门02:可视化界面设计器、控件的使用
		一.前言 上篇 (原创)[B4A]一步一步入门01:简介.开发环境搭建.HelloWorld 中我们创建了默认的项目,现在我们来看一下B4A项目的构成,以及如何所见即所得的设计界面,并添加和使用自带的 ... 
- Windows Phone 十二、设计器同步
		在设计阶段为页面添加数据源 Blend或者VS的可视化设计器会跑我们的代码,然后来显示出来,当我们Build之后,设计器会进入页面的构造函数,调用InitializeComponent();方法来将U ... 
- WinForms项目升级.Net Core 3.0之后,没有WinForm设计器?
		目录 .NET Conf 2019 Window Forms 设计器 .NET Conf 2019 2019 9.23-9.25召开了 .NET Conf 2019 大会,大会宣布了 .Net Cor ... 
- ActiveReports 9 新功能:可视化查询设计器(VQD)介绍
		在最新发布的ActiveReports 9报表控件中添加了多项新功能,以帮助你在更短的时间里创建外观绚丽.功能强大的报表系统,本文将重点介绍可视化数据查询设计器,无需手动编写任何SQL语句,主要内容如 ... 
- VS2015 android 设计器不能可视化问题解决。
		近期安装了VS2015,体验了一下android 的开发,按模板创建执行了个,试下效果非常不错.也能够可视化设计.但昨天再次打开或创建一个android程序后,设计界面直接不能显示,显示错误:(可能是 ... 
- 可视化流程设计——流程设计器演示(基于Silverlight)
		上一篇文章<通用流程设计>对鄙人写的通用流程做了一定的介绍,并奉上了相关源码.但一个好的流程设计必少不了流程设计器的支持,本文将针对<通用流程设计>中的流程的设计器做一个简单的 ... 
- 解析大型.NET ERP系统核心组件 查询设计器 报表设计器 窗体设计器 工作流设计器 任务计划设计器
		企业管理软件包含一些公共的组件,这些基础的组件在每个新项目立项阶段就必须考虑.核心的稳定不变功能,方便系统开发与维护,也为系统二次开发提供了诸多便利.比如通用权限管理系统,通用附件管理,通用查询等组件 ... 
- F2工作流引擎之-纯JS Web在线可拖拽的流程设计器(八)
		Web纯JS流程设计器无需编程,完全是通过鼠标拖.拉.拽的方式来完成,支持串行.并行.分支.异或分支.M取N路分支.会签.聚合.多重聚合.退回.传阅.转交,都可以非常方便快捷地实现,管理员 ... 
- 纯JS Web在线可拖拽的流程设计器
		F2工作流引擎之-纯JS Web在线可拖拽的流程设计器 Web纯JS流程设计器无需编程,完全是通过鼠标拖.拉.拽的方式来完成,支持串行.并行.分支.异或分支.M取N路分支.会签.聚合.多重聚合.退回. ... 
随机推荐
- 《Effective C#》系列之(零)——概要
			把全书的内容讲述完整可能需要很长时间,我可以先回答主要目录和核心的内容.如果您有任何特定问题或需要更详细的解释,请告诉我. <Effective C#>一书共包含50条C#编程建议,以下是 ... 
- 力扣73(java)-矩阵置零(中等)
			题目: 给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 .请使用 原地 算法. 提示: m == matrix.length n == matrix[0].l ... 
- EMAS移动测试-远程真机篇
			简介: 导读:本文将介绍申请远程真机以及在远程真机上执行测试任务的详细操作,包括申请远程真机.安装应用.扫码.定位.性能测试等. 一.移动测试概览 移动测试服务(Mobile Testing)是为企业 ... 
- 如果千百年前有视觉AI算法,世界将会是什么样的光景呢?
			视觉AI算法在近些年取得了一定的突破,被应用在了越来越多的地方,我相信距离真正的AI普及这个大目标也越来越近了.我时常在想假如古代也有视觉AI算法,那是不是很多故事的结局都将被改写?<伯乐相马& ... 
- 云原生DevOps的5步升级路径
			简介: 究竟什么是云原生DevOps呢?我们认为:云原生DevOps是充分利用云原生基础设施,基于微服务/无服务架构体系和开源标准,语言和框架无关,具备持续交付和智能自运维能力,从而做到比传统DevO ... 
- 深度解析数据湖存储方案Lakehouse架构
			简介:从数据仓库.数据湖的优劣势,湖仓一体架构的应用和优势等多方面深度解析Lakehouse架构. 作者:张泊 Databricks 软件工程师  Lakehouse由lake和house两个词组 ... 
- 如何避免出现SQL注入漏洞
			简介: 本文将针对开发过程中依旧经常出现的SQL编码缺陷,讲解其背后原理及形成原因.并以几个常见漏洞存在形式,提醒技术同学注意相关问题.最后会根据原理,提供解决或缓解方案.  作者 | 阿里云安全 ... 
- [Nova] KeyValue Field 设置默认 key 的方式
			1. 使用 withMeta: KeyValue::make('options') ->withMeta([ 'value' => $this->options ?? [ 'A' = ... 
- cs61a回顾
			从1月25开始到2.20,完成第一个项目hog. 总结让自己进度慢的主观因素: 妄图一次阅读掌握所有知识:违反了<为什么学生不喜欢上学>中大脑不是用来思考的,它的真正作用在于使你避免思考的 ... 
- XAML 给资源起个好名字 用 StaticResource 起一个别名
			本文来和大家聊一下关于 XAML 资源的定义的事情,和开发技术关系不大,更多的是开发的思路 在稍微大一点的项目里,肯定 XAML 资源是少不了的.对于 XAML 资源,行业里讨论多(非小白讨论)的是关 ... 
