最近文章

JPA JSON字符串与List以及对象的转换

有时为了方便,我们会吧对象或者List以JSON字符串的形式存放在数据库中。使用JPA就可以通过@Converte的方式实现。JSON字符与对象的转换,我们比较常用的时jackson,maven依赖如下: com.fasterxml.jackson.core jackson-databind 2.8.0 JSONObject字
标签:

单链表的实现

最近学习了RxJs的一些知识,发现RxJs中的Observable其实是一种链表结构,因此想试着温习一下链表的基础知识。require("@fatso83/mini-mocha").install(); const sinon = require("sinon"); const { expect } = require('chai'); class LinkNode { constructo
标签:

BST的简单实现

最基础的数据结构往往会有最关键的作用,理解这些基础的数据结构能做什么,以及它们不能做什么对于我们理解数据结构的本质有关键的作用。const Random = require("random-js").Random; const MersenneTwister19937 = require("random-js").MersenneTwister19937; const random = new R
标签:

子章节

z最早出现在操作
标签:

x现在

/都是/
标签:

为什么要写笔记

 一、写笔记是一个设计,思考和记录的过程。对于技术笔记来说,它展示的是作者分析和分解需求,设计架构,优秀编码以及高效输出的一个过程。这些过程的思考,如果没有及时记录下来,后续就会花更多的时间来回顾,这是不高效的。书写是为了更好的思考,记录是为了更好的反思。 二、想法只是昙花,文字才能永恒。任何时候只有写出来的才算输出。记得看过一段话,一个会坚持记笔记的人,肯定是个学习能力强,做
标签:

JPA实体映射——多对一关系映射进一步学习

我们先看看第一种情况,如何持计划多对一的关系,以及如何删除这样的关系?首先上代码:public class ManyToOneTest extends AbstractTest { @Override protected Class[] entities() { return new Class[]{ Post.class,
标签:

JPA实体映射——多对一关系映射

前几节我们介绍了一对多的关系和一对一关系,今天我们学习多对一关系以及这种映射方式的最佳实践,先上业务实例图。在我们的业务关系图中,部门和研究所实体是多对一的关系,同时我们还是采用双向关联来说明问题Bidirectional @ManyToOne部门实体import javax.persistence.*; import java.io.Serializable; import java.util.
标签:

深入理解关系数据表之间的关系

首先我们需要理解的是,关系是怎么产生的?在关系表中,关系是属于不同表的相应的行来构建形成的。当子表定义了一个外键引用了父表的主键的时候,表关系就形成。所以理解表关系,需要理解上述四个关键因素以及它们之间的关系。表关系建立在外键的基础上,因此,每一类表关系有三类关系类型: 一对多是最普通的关系,它通过将父表的一行与子表的多行关联起来形成这种关系。 一对一关系需要子表的主键通过外键
标签:

BTree的一种简单实现

最近在学习和理解红黑树的过程中,发现想要深入理解红黑树的本质离不开一种重要的数据结构,这种数据结构就是平衡树(BTree),今天开始学习BTree的一些基本操作。require("@fatso83/mini-mocha").install(); const { expect } = require('chai'); class BTreeNode { constructor(isLeaf){
标签:

Graphs 简单学习

最近学习数据结构的时候,发现一个有意思的规律,那就是复杂的数据结构一定是对简单数据结构的封装和抽象。因此,今天学习一个稍微复杂的数据结构,那就是Graphs。require("@fatso83/mini-mocha").install(); const { expect } = require('chai'); class Node { constructor(data) { this
标签:

Union Find 数据结构和算法理解

最近开始学习一些不那么简单的算法,比如上一篇中的Graph就是一种不那么容易理解和学习的数据结构,今天来学习另一种数据结构和算法。Union-Find这种算法的有哪些特点呢?1、用最少的代码解决相对复杂的问题,而且时间和空间复杂度还相当优异。2、这种算法能解决类似问题,比如:给定一组顶点和边,找出哪些组件属于同一个组,及存在多少个这样的组?3、这种算法的另一个名称:"Disjoint Set"。U
标签:

为什么React的render方法会执行两次?

最近在写React组件的时候,发现了一个奇怪的问题,就是组件的render方法总是执行两次。import Card from './src/Card'; import FriendProfile from './src/FriendProfile'; export default function App() { console.log('render App'); return (
标签:

numpy.tile()扩展函数

理解tile的意思,就容易理解tile的用法:n.瓦片; (贴墙或铺地用的)瓷砖; 地砖; 小方地毯; 片状材料; (铺屋顶的)瓦; (棋盘游戏的)棋子;vt.铺瓦; 铺地砖; 贴瓷砖; 平铺显示; 瓦片式显示定义numpy.tile(A, reps)通过重复数组A来构造新的数组,从而达到扩展A的作用。重复次数有reps指定。tile原意是铺瓷砖,形象理解就把A作为瓷砖,平铺显示。参数A:arra
标签:

Queue BFS 初步学习

最近在学习数据结构的时候,学到了树的广度优先搜索可以使用队列来实现,今天尝试着写一下:定义一棵树:let tree = { "10": { value: "10", left: "4", right: "17", }, "4": { value: "4", left: "1", right: "9", }, "17": { value: "17", lef
标签:

RxJs基础

Rxjs基础我们知道在Rxjs中有几类基础组件,它们分别是:1、Observables2、Observers3、Operators4、Subjects5、Schedulers以上这些组件是Rxjs的核心构建块。我们需要理解它们如何协同工作以便提供强大的功能。一旦对它们有了更高层次的理解,使用起来将会得心应手。那么,让我们一一来看看它们。先来看一个例子://import { from } from
标签:

AVLTree简单实现

今天突然感觉AVL树在Tree这种数据结构中有比较重要的作用,特别是理解如何将不平衡的树转化为平衡树的左旋和右旋操作是理解和学习AVL树的基础。先上代码://定义树的基本节点 const Node = function(item){ //基本元素 this.item = item; //节点的高度 this.height = 1; this.left = null; this.ri
标签:

理解CKEditor5的schema

我们知道,CKEditor5是一个用MVC架构设计的富文本编辑器。如上图所示,三层分别是:Model,Controller, View首先,第一个问题是schema属于那一层?经过官方文档的初步学习,我们可以看到:editor.model.schema; // -> The model's schema.因此,我们可以得出结论:schema属于模型层:其次我们需

kubernetes服务的版本回退

下面简单说一下kubernetes的版本回退,因为已经在线上使用挺久的时间了,是利用kubernetes deployment的rollout histrory回退到指定版本。 先给deployment打个样,webservice服务配置如下:kind: Deployment apiVersion: apps/v1beta2 metadata: name: pre-webservic
标签:

Go语言:标识符命名最佳实践

在 Go 语言中,标识符的命名非常重要,它们使你的代码易于阅读、易于理解和易于维护。编写Go代码时,遵循以下标识符命名的最佳实践:1、使用有意义的名称使用能够准确反映其作用的名称,让代码易于理解和维护。func calculateArea(width float64, height float64) float64 { return width * height } 2、避免使用缩写除非使用缩
标签:

Go语言获取对象的类型

Go reflect包含有检查变量类型的方法,示例代码如下:package main import ( "fmt" "reflect" ) func main() { v1:= "string" v2 := 100 v3 := 11.1 fmt.Println(reflect.TypeOf(v1)) fmt.Println(refle
标签:

Gradle手动安装说明

环境要求Gradle依赖于JDK8或以上版本。在安装之前,先执行java -version检查JDK版本:>java -version java version "11.0.13" 2021-10-19 LTS Java(TM) SE Runtime Environment 18.9 (build 11.0.13+10-LTS-370) Java HotSpot(TM) 64-Bit Ser
标签:

Git查看,修改用户名和邮箱(local和global)

git config命令查看用户名和邮箱#查看用户名 git config user.name #查看邮箱 git config user.emailgit config命令设置用户名和邮箱1. --local对当前仓库设置(仅对当前仓库有效)#设置用户名 git config --local user.name '用户名' #设置邮箱 git config --local user.email
标签:

JPA批量更新多个id的实体对象

在 Spring Data JPA 中,可以使用 @Query 注解和 HQL 或者 SQL 语句来执行自定义的更新操作。如果要根据多个 ID 更新实体对象,可以使用 IN 关键字。基于HQL的示例以下是一个示例,假设我们有一个名为 User 的实体类,其中包含 id 和 name 属性。现在我们想要根据多个 ID 更新这些用户的姓名: @Repository public interface U
标签:

Injector框架理解(二)

今天我们继续学习didi这个依赖注入框架,上一节我们知道了怎么定义组件,怎么定义模块,怎么通过Injector这个类来载入模块和获取组件。今天我们来学习如何重用模块:我们首先定义好组件和模块:const {Injector} = require('didi'); class FooType { constructor() { this.name = 'foo'; } }
标签:

Injector框架理解(一)

在可测试js的框架学习过程中,我们不断提到了依赖注入,依赖注入使用最多的就是在java中的spring,为了在js中使用依赖注入,我们今天学习另一个不那么常用的框架:didi首先看看安装命令npm install didi这个时候,我看到项目下的package.json{ "name": "babelwebpack", "version": "1.0.0", "description"
标签:

webpack初步理解

Webpack 是一个非常强大且有趣的工具,它被视为当今许多 Web 开发人员用来构建其应用程序的基础组件。然而,许多人会认为使用它是一个相当大的挑战,主要是因为它的复杂性。 webpack bundle过程图表modules模块是一个文件的升级版本。一个模块,一旦创建并且构建之后,除了包含原始的文件代码之外,还有一些其他有意义的信息:比如模块使用的加载器,模块的依赖项,模块的导出(如果
标签:

Hibernate映射计算的属性

在一般的实体映射中,一般都是一个属性对应数据库的某一列。今天我们来看看如何映射通过其他属性计算出来的属性。好了先看看具体的两个领域类:新建一个Account.java@Entity(name = "Account") @Table(name = "account") public class Account { @Id private Long id; @ManyToO
标签:

JPA——persistence.xml深入理解

在本文中,我们会解释 JPA persistence.xml 配置文件的用途,以及如何使用可用的 XML 标记或属性设置 Java Persistence 应用程序。虽然 Spring 应用程序可以在不需要 XML JPA 配置文件的情况下进行引导,但理解每个配置选项的含义仍然很重要,因为 Spring 在构建 Java Persistence LocalContainerEntityManage
标签:

CSS——Flexbox布局另类理解

Flexbox是一种非常强大的布局模式。当我们真正理解它们如何工作时,我们可以构建自动响应的动态布局,并且根据需要重新排列元素。看下面一个例子:新建一个dynamic-layout.html文件<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta nam
标签:

CSS——理解布局算法

大家在学习CSS的时候,是不是常常有一些灵感浮现的时刻,或者是看到别人只是用简单的CSS就能做出如此炫酷的效果。其实我也有类似的经历,我那时候总是关注折写出来的CSS的属性和值有哪些情况,它们分别的效果是什么?比如说z-index:10肯定在z-index:5之上; justify-content:center就是在flex布局的时候,让元素框居中;我琢磨着如果能够将CSS的属性和值学习得越多,那
标签:

CSS-Layout Ribbon

最近在设计页面的时候,用到了css布局中的一个Ribbon布局,样子如下图所示:刚开始打算用这个样式作为文章的标题,后来暂时没有使用,但是在学习这个布局的时候,积累了很多css的知识,今天在此记录一下,方便以后查找温习。首先,我贴出这部分代码的dom结构:<!DOCTYPE html> <html> <head> <meta charset="UT
标签:

javascript——嵌套函数作用域

javascript作用域我们知道,js中有三个作用域,分别是block scope(块作用域),function scope(函数作用域),globle scope(全局作用域);今天我们来看看什么是嵌套函数作用域嵌套函数作用域let a = 10; function outer() { let b = 20; function inner() { let c = 30; conso
标签:

新建章节

标签:

JPA实体状态学习-(瞬时态:Transient)

为了学习实体的状态,我们还是贴出这张实体状态转换迁移图:Transient(瞬时态)按照上图的描述,java对象在内存中被赋值后,没有调用entityManager.persist()方法之前实体对象所处的状态就是瞬時態举个例子:Teacher teacher = new Teacher("email@dot.com");此时,实例teacher就处于new/transient态(备注:这里的ne
标签:

JPA实体状态深入理解

我们在学习JPA实体状态的时候,常常会问,JPA的实体有多少状态呢?相信这个问题不难回答:瞬时态(transient)托管态(persistent)游离态(detached)移除态(removed)注意:这里最后一个移除态,有的时候也叫删除态(deleted),至于它和移除态有啥区别,暂时没有想到,如果您对此有更加深刻的理解,请留言回复。为什么会有这四种状态呢?啥,这个也有为啥,网上不是都这么说的
标签:

如何集成多个限界上下文?

在进行领域驱动设计时,发现有两个情境很容易产生误解?他们是:多个限界上下文集成和聚合设计。在本文中,主要介绍多个限界上下文集成。通用语言任何研究过 DDD 的人都已经了解到与其一起使用的许多概念:限界上下文、实体、聚合、值对象等。其中一些概念(实体,聚合,值对象)与代码直接关联,很容易理解和使用,而另一些(限界上下文)则由于其抽象性而难以吸收应用。通用语言就是这样一种情况,尽管它简单而强大,但很容
标签:

JUnit——Ignore测试

有时我们的代码在运行测试用例时没有完全准备好。结果,测试用例失败。 @Ignore 注释在这种情况下会有所帮助。带有@Ignore 注解的测试方法将不会被执行。如果一个测试类被@Ignore注解,那么它的任何测试方法都不会被执行。创建测试用例类创建一个 java 测试类,例如 TestJunit.java。将测试方法 testPrintMessage() 或 testSalutationMessa
标签:

SVM应用

from sklearn.model_selection import train_test_splitfrom sklearn.model_selection import GridSearchCVlinear_svc = LinearSVC()X_train, X_test, y_train, y_test = train_test_split(X,y)#define a list of pa
标签:

JUnit——Suite测试

Test Suite用于捆绑一些单元测试用例并将它们一起运行。在 JUnit 中,@RunWith 和@Suite 注释都用于运行suite test。本文以具有两个测试类 TestJunit1 和 TestJunit2 的示例为例,它们使用测试套件一起运行。创建一个要测试的 java 类,比如 MessageUtil.java/* * This class prints the given m
标签:

JPA JSON字符串与List以及对象的转换

更新于 2023.11.01 8分钟阅读 0 评论 15 推荐

    作者:

有时为了方便,我们会吧对象或者List以JSON字符串的形式存放在数据库中。使用JPA就可以通过@Converte的方式实现。

JSON字符与对象的转换,我们比较常用的时jackson,maven依赖如下:


     
    
      com.fasterxml.jackson.core
      jackson-databind
      2.8.0
    
  

JSONObject字符串转换为对象

这里以Order和Metadata为例。

1、首先创建用于转换Metadata对象的转换器。

@Converter(autoApply = true)
public class JSONConverter implements AttributeConverter {
    private final static Logger LOGGER = LoggerFactory.getLogger(JSONConverter .class);
    private final static ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public String convertToDatabaseColumn(Metadata meta) {
        try {
            return objectMapper.writeValueAsString(meta);
        } catch (JsonProcessingException ex) {
            LOGGER.error("", ex);
            return null;
        }
    }

    @Override
    public Metadata convertToEntityAttribute(String dbData) {
        try {
            return objectMapper.readValue(dbData, Metadata.class);
        } catch (IOException ex) {
            LOGGER.error("", ex);
            return null;
        }
    }
}

代码实现了JPA转换器接口AttributeConverter,这个接口提供了两个方法:

  • convertToDatabaseColumn:用于把对象转换为数据库字段存储的值。
  • convertToEntityAttribute:用于把数据库里的字段存储的值转换为对象。

在convertToDatabaseColumn方法中,使用了Jackson的ObjectMapper的writeValuAsString,把对象转换为JSONObject的字符串。

在convertToEntityAttribute方法中,也是用了Jackson的ObjectMapperreadValue方法,把字符串转换为对象。

hhzzz

 

2、在Order对象中添加注解@Convert

@Entity
@Table(name = "order")
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Convert(converter = JSONConverter.class)
    private Metadata metadata;

    //…get 和set方法
}

JSONArray字符串转换为List

JSONArray与List之间的转换类似于对象的转换,但因为Jackson对List范类型的转换实现不同,所以在字符串转换List实现方式不同。

1、定义转换器

@Converter(autoApply = true)
public class OrderItemsJSONConverter implements AttributeConverter, String> {

    private final static Logger LOGGER = LoggerFactory.getLogger(ChaptersJSONConverter.class);

    private final static ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public String convertToDatabaseColumn(List meta) {
        try {
            return objectMapper.writeValueAsString(meta);
        } catch (JsonProcessingException ex) {
            LOGGER.error("", ex);
            return null;
        }
    }

    @Override
    public List convertToEntityAttribute(String dbData) {
        try {
            if(dbData == null){  //如果dbData为null会导致转换报错,可以根据业务需求对null做处理
                return new ArrayList<>();
            }
            return objectMapper.readValue(dbData, new TypeReference>(){});
        } catch (IOException ex) {
            LOGGER.error("", ex);
            return null;
        }
    }

}

在字符串转换List,objectMapper.readValue的类型时TypeReference,这个是与对象转换不同的地方。

2、Order的OrderItem列表添加@Convert注释

import javax.persistence.*;
import java.util.List;

@Entity
@Table(name = "order")
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Convert(converter = JSONConverter.class)
    private Metadata metadata;

    @Convert(converter = ItemsJSONArrayConverter.class)
    private List item;

    //get set方法
}

单链表的实现

更新于 2023.06.30 1分钟阅读 1 评论 15 推荐

    作者:

最近学习了RxJs的一些知识,发现RxJs中的Observable其实是一种链表结构,因此想试着温习一下链表的基础知识。

require("@fatso83/mini-mocha").install();
const sinon = require("sinon");
const { expect } = require('chai');

class LinkNode {
  constructor(data){
    this.data = data;
    this.next = null;
  }
}

class SingleLinkList {
  constructor(head = null){
    this.head = head;
    this.tail = null;
  }

  size() {
    let count = 0;
    let node = this.head;
    while(node) {
      count++;
      node = node.next;
    }
    return count;
  }
  print() {
    let result = [];
    let cur = this.head;
    while(cur) {
      result.push(cur.data);
      cur = cur.next;    
    }
    return result;
  }

  delete(data) {
    if ( this.head == null) {
      //链表为空什么都不错
      return;
    }
    let pre = this.head;
    let cur = this.head;
    if (cur.data == data) {
      //第一个节点就是要删除的节点
      this.head = cur.next;
      //删除数据
      cur = null;
      return;
    }
    while(cur) {
      pre = cur;
      cur = cur.next;
      if (cur.data == data) {
        pre.next = cur.next;
        cur = null;
        return;
      }
    }
    
  }

  add(data) {
    const node = new LinkNode(data);
    if (this.head == null) {
      this.head = node;
    }
    
    const temp = this.tail;
    if (temp!=null) {
      temp.next = node;
    } 
    this.tail = node;
  }

  reverse() {
    let beg = null;
    let mid = this.head;
    let end = this.head.next;
    while(true) {
      mid.next = beg; 
      //beg = mid;
      if (end === null) {
        break;
      }
      //说明一下,这行的位置可以在判断之前,也可以在判断之后,一般来说在之后,
      //因为会少赋值一次
      beg = mid; 
      mid = end;
      end = end.next;
    }
    this.head = mid;
  }

  reverseRecursive(node) {
    if (node === null|| node.next===null) {
      return node;
    }
    const newNode = this.reverseRecursive(node.next);
    //注意这里是后序位置处理逻辑
    node.next.next = node;
    node.next = null;
    return newNode;
  }

  reverseN(node, n) {
    if (n === 1) {
      //记录下第n个元素的后继,然后每次在后序需要链接到后继元素
      //同时这里有个特点就是每次递归的返回都是第n个元素
      this.successor = node.next;
      return node;
    }
    const last = this.reverseN(node.next, n-1);
    node.next.next = node;
    node.next = this.successor;
    return last;
  }

  reverseBetweenMN(node, m, n ) {
    if (m ===1) {
      return this.reverseN(node,n);
    }
    //从1到m之间的元素没有啥修改,只是不断的更新头结点
    node.next = this.reverseBetweenMN(node.next, m-1, n-1);
    return node;
  }

  rotateRight(head, k) {
    let numberOfNodes = 0;
    let ptr1 = head;
    while(ptr1!=null) {
      ptr1 = ptr1.next;
      numberOfNodes++;
    }
    if(numberOfNodes!=0){
        k = k%numberOfNodes;
    }else{
        return head;
    }
    let count = 0;
    ptr1 = head;
    while(count < numberOfNodes-k-1) {
      ptr1 = ptr1.next;
      count++;
    }
    let tempNode = ptr1;
    while(ptr1.next!=null){
        ptr1 = ptr1.next;
    }
    
    ptr1.next = head;
    head = tempNode.next;
    tempNode.next = null;     
    return head;
  }

  insertSort(head) {
    let result = null;
    let current = head;
    let next;
    while(current !=null) {
      next = current.next;
      //Sort the linked list till the current element and store it
      result = this.sortedInsert(result, current);
      current = next;
    }
    return result;
  }

  sortedInsert(sorted, newNode) {
    //Temporary node to swap the elements 
    // 这里的temp可以认为是dumy节点
    let dumy = new LinkNode();
    let current = dumy;
    dumy.next = sorted;
    //Sort the list based on the specified order
    while(current.next !== null && current.next.data < newNode.data){
      current = current.next;
    }
    
    //Swap the elements
    newNode.next = current.next;
    current.next = newNode;
    //Return the sorted list.
    return dumy.next;
  }
}

const singleLinkList = new SingleLinkList();
for (let i = 0 ; i < 10 ; i++) {
  singleLinkList.add(i);
}

describe('SingleLinkList test',()=>{

  it('SingleLinkList traverse ordered linkedlist test',()=>{
    let ll = new SingleLinkList();
    ll.add(10);
    ll.add(5);
    ll.add(22);
    ll.add(3);
    ll.add(17);
    ll.add(10);
    
    //Get the head
    let toSort = ll.head;
    
    //Sort the list
    let sorted = ll.insertSort(toSort);
    ll.head = sorted;
    const result = ll.print();
    const expected = [3,5,10,10,17,22];
    expect(expected).to.eql(result);
    
  })
  
  it('SingleLinkList.add test',()=>{
    //测试新增节点
    const singleLinkList = new SingleLinkList();
    for (let i = 0 ; i < 10 ; i++) {
      singleLinkList.add(i);
    }
    const arrays = [0,1,2,3,4,5,6,7,8,9];
    const spy = sinon.spy(arrays);
    const result = singleLinkList.print();
    expect(result.length).to.equal(10);
    sinon.assert.match(result,spy);
  });
  
  it('SingleLinkList.delete middle node test',()=>{
    //测试删除链表中间节点
    const singleLinkList = new SingleLinkList();
    for (let i = 0 ; i < 10 ; i++) {
      singleLinkList.add(i);
    }
    singleLinkList.delete(3);
    const arrays = [0,1,2,4,5,6,7,8,9];
    const spy = sinon.spy(arrays);
    const result = singleLinkList.print();
    expect(result.length).to.equal(9);
    sinon.assert.match(result,spy);
  });
  it('SingleLinkList.delete head node test',()=>{
    //测试删除链表头节点
    const singleLinkList = new SingleLinkList();
    for (let i = 0 ; i < 10 ; i++) {
      singleLinkList.add(i);
    }
    singleLinkList.delete(0);
    const arrays = [1,2,3,4,5,6,7,8,9];
    const spy = sinon.spy(arrays);
    const result = singleLinkList.print();
    expect(result.length).to.equal(9);
    sinon.assert.match(result,spy);
  });
  it('SingleLinkList.delete tail node test',()=>{
    //测试删除链表尾节点
    const singleLinkList = new SingleLinkList();
    for (let i = 0 ; i < 10 ; i++) {
      singleLinkList.add(i);
    }
    singleLinkList.delete(9);
    const arrays = [0,1,2,3,4,5,6,7,8];
    const spy = sinon.spy(arrays);
    const result = singleLinkList.print();
    expect(result.length).to.equal(9);
    sinon.assert.match(result,spy);
  });

  it('SingleLinkList rotateRight test',()=>{
    const singleLinkList = new SingleLinkList();
    for (let i=1; i <6 ;i++) {
      singleLinkList.add(i);
    }
    const head = singleLinkList.rotateRight(singleLinkList.head,7);
    expect(head.data).to.equal(4);
  });
  it('linkedlist reverse test',()=> {
    const singleLinkList = new SingleLinkList();
    for (let i=1; i <6 ;i++) {
      singleLinkList.add(i);
    }
    singleLinkList.reverse();
    const result = singleLinkList.print();
    const expected = [5,4,3,2,1];
    expect(expected).to.eql(result);
  });
  it('linkedlist reverseRecursive test',()=> {
    const singleLinkList = new SingleLinkList();
    for (let i=1; i <6 ;i++) {
      singleLinkList.add(i);
    }
    const newHead = singleLinkList.reverseRecursive(singleLinkList.head);
    singleLinkList.head = newHead;
    const result = singleLinkList.print();
    const expected = [5,4,3,2,1];
    expect(expected).to.eql(result);
  });
  it('linkedlist reverseN test',()=> {
    const singleLinkList = new SingleLinkList();
    for (let i=1; i <6 ;i++) {
      singleLinkList.add(i);
    }
    const newHead = singleLinkList.reverseN(singleLinkList.head,3);
    singleLinkList.head = newHead;
    const result = singleLinkList.print();
    const expected = [3,2,1,4,5];
    expect(expected).to.eql(result);
  });
  it('linkedlist reverseBetweenMN test',()=> {
    const singleLinkList = new SingleLinkList();
    for (let i=1; i <6 ;i++) {
      singleLinkList.add(i);
    }
    const newHead = singleLinkList.reverseBetweenMN(singleLinkList.head,2,4);
    singleLinkList.head = newHead;
    const result = singleLinkList.print();
    const expected = [1,4,3,2,5];
    expect(expected).to.eql(result);
  });
  
});

在上一个版本中,主要通过控制台打印来验证链表是否正确,但是在实际的产品中,代码没有通过正式的单元测试始终不那么靠谱。因此这部分增加了单元测试。

如果代码在运行过程中出现出现了错误,实际上不是代码的问题,而是运行测试框架的时候,需要选择更高版本的nodejs运行环境,这里的测试框架代码在node10版本运行有问题,因此需要切换到node14或者更高。

在这里,提出一个疑问?首先,这里的链表是单链表,因此,如果找到链表中间的那个元素呢?

欢迎在评论区写下您的思路即可。

BST的简单实现

更新于 2023.06.21 0分钟阅读 3 评论 15 推荐

    作者:

最基础的数据结构往往会有最关键的作用,理解这些基础的数据结构能做什么,以及它们不能做什么对于我们理解数据结构的本质有关键的作用。

const Random = require("random-js").Random;
const MersenneTwister19937 = require("random-js").MersenneTwister19937;
const random = new Random(MersenneTwister19937.autoSeed());
class Node {
    constructor(data){
		this.data = data;
 		this.left = null;
		this.right = null;
	}
}

class BinarySearchTree {
	constructor(){
		this.root = null;
	}
	// function to be implemented
    // insert(data)
	insert(data) {
		let newNode = new Node(data);
		if (this.root === null) {
			this.root = newNode;
 		} else {
 			this.insertNode(this.root,newNode);
 		}
	}
	insertNode(node, newNode) {
		if (newNode.data < node.data) {//insert left tree
			if (node.left === null){
				node.left = newNode;
 			} else {
 				this.insertNode(node.left, newNode);
 			}
 		} else {//greater than value, insert right tree
 			if (node.right === null) {
				node.right = newNode;
 			} else {
 				this.insertNode(node.right, newNode);
 			}
 		}
	}
    // remove(data)
    remove(data){
		this.root = this.removeNode(this.root,data);
	}   
	removeNode(node,data){
		if (node === null){
			return null;
 		}else if (data < node.data) {
			node.left = this.removeNode(node.left, data);
			return node;
 		}else if (data > node.data){
			node.right = this.removeNode(node.right, data);
			return node;
 		} else {
 			//没有左右节点
			if (node.left === null && node.right === null){
				node = null;
				return null;
 			}
			//has right no left
			if (node.left === null){
				node = node.right;
				return node;
 			}
 			// has left no right
 			if (node.right === null){
				node = node.left;
				return node;
 			}
 			// both left and right
 			let aux = findMinNode(node.right);
			node.data = aux.data;
			node.right = this.removeNode(node.right,aux.data);
			return node;
 		}
	}
 	//recursive to find minimum node
    findMinNode(node){
        // if left of a node is null
        // then it must be minimum node
        if(node.left === null) {
            return node;
        } else {
            return this.findMinNode(node.left);
	    }  
	}  

    inOrder(node) {
      const res = []
      if (node === null) {
        return res;
      }
      const leftRes = this.inOrder(node.left);
      for (let i = 0 ; i< leftRes.length; i++) {
        res.push(leftRes[i]);
      }
      res.push(node.data);
      const rightRes = this.inOrder(node.right);
      for (let i = 0 ; i< rightRes.length; i++) {
        res.push(rightRes[i]);
      }
      return res;
    }

	inorder(node){
		if (node!== null){
			this.inorder(node.left);
			console.log(node.data);
			this.inorder(node.right);
 		}
	}
    inorderiterator(node) {
      const stack = [];
      const reversed = [];
      let curr = node;
      while(stack.length || curr) {
        while(curr) {
          stack.push(curr);
          curr = curr.left;
        }
        curr = stack.pop();
        reversed.push(curr.data);
        curr = curr.right;
      }
      return reversed;
    }

    preOrder(node) {
      const res = [];
      if (node === null) {
        return res;
      }
      res.push(node.data);
      const leftRes = this.preOrder(node.left);
      for (let i = 0 ; i< leftRes.length; i++) {
        res.push(leftRes[i]);
      }
      const rightRes = this.preOrder(node.right);
      for (let i = 0 ; i< rightRes.length; i++) {
        res.push(rightRes[i]);
      }
      return res;
    }

	preorder(node){
    	if(node !== null){
        	console.log(node.data);
        	this.preorder(node.left);
        	this.preorder(node.right);
    	}
	}
    preorderiterator(node) {
      const stack = [node];
      const traversed = [];
      let curr;
      while(stack.length) {
        curr = stack.pop();
        traversed.push(curr.data);
        if (curr.right) {
          stack.push(curr.right);
        }
        if (curr.left) {
          stack.push(curr.left);
        }
      }
      return traversed;
    }

    preorderiterator1(node) {
      const stack = [];
      const traversed = [];
      let curr = node;
      while(stack.length || curr) {
        while(curr){
          traversed.push(curr.data);
          stack.push(curr);
          curr = curr.left;
        }
        curr = stack.pop();
        curr = curr.right;
      }
      return traversed;
    }

    postOrder(node) {
      const res = [];
      if (node === null) {
        return res;
      }
      const leftRes = this.postOrder(node.left);
      for (let i = 0 ; i < leftRes.length; i++ ) {
        res.push(leftRes[i]);
      }
      const rightRes = this.postOrder(node.right);
      for (let i = 0 ; i < rightRes.length; i++ ) {
        res.push(rightRes[i]);
      }
      res.push(node.data);
      return res;
    }
  
	postorder(node){
      if(node !== null){
        this.postorder(node.left);
        this.postorder(node.right);
        console.log(node.data);
      }
	}

    postorderiterator(node) {
      const s1 = [node];
      const s2 = [];
      const reversed = [];
      let curr;
      while(s1.length) {
        curr = s1.pop();
        if (curr.left) {
          s1.push(curr.left);
        }
        if (curr.right) {
          s1.push(curr.right);
        }
        s2.push(curr);
      }
      while(s2.length) {
        curr = s2.pop();
        reversed.push(curr.data);
      }
      return reversed;
    }
  
	getRootNode(){
    	return this.root;
	} 
	search(node, data){
   		// if trees is empty return null
    	if(node === null)
        	return null;
 
    	// if data is less than node's data
    	// move left
    	else if(data < node.data)
        	return this.search(node.left, data);
 
    	// if data is more than node's data
    	// move right
    	else if(data > node.data)
        	return this.search(node.right, data);
 
    	// if data is equal to the node data
    	// return node
    	else
        	return node;
	}

    /**
     * 这个方法采用的是遍历
     */
    maxDepth(root) {
      this.res = 0;
      this.depth = 0;
      this.leaves = 0;
      this.travel(root);
      return this.res;
    }

    /**
     * 这个方法采用的是分治
     */
    maxDepthRecursive(root) {
      if (root===null) {
        return 0;
      }
      const leftDepth = this.maxDepthRecursive(root.left);
      const rightDepth = this.maxDepthRecursive(root.right);
      //由子树的深度推测出当前树的深度
      return Math.max(leftDepth,rightDepth) + 1;
    }

    travel(root) {
      if (root === null) {
        return;
      }
      this.depth++;
      if (root.left === null && root.right === null) {
        this.leaves++;
        this.res = Math.max(this.depth, this.res);
      }
      this.travel(root.left);
      this.travel(root.right);
      this.depth--;
    }

    printLevel(root, level) {
      if (root === null) {
        return;
      }
      console.log('current data:'+ root.data + ', it\'s level is:'+level);
      this.printLevel(root.left, level+1);
      this.printLevel(root.right, level+1);
    }

    calculateNodeCount(root) {
      if (root === null) {
        return 0;
      }
      const ltCount = this.calculateNodeCount(root.left);
      const rtCount = this.calculateNodeCount(root.right);
      //这部分计算需要知道左右子节点的信息后才能计算,因此放在最后
      return ltCount+rtCount+1;
    }

    /**
     * 反转左右子树节点 遍历递归思维
     */
    reverseLR(root) {
      if (root === null) {
        return;
      }
      const temp = root.left;
      root.left = root.right;
      root.right = temp;
      //注意,以上三行代码放在当前节点的前序位置,也可以放在后序位置
      //如果放在中序位置会怎么样呢?
      this.reverseLR(root.left);
      this.reverseLR(root.right);
    }

    /**
     * 反转左右子树节点 分治递归思维
     */
    invertTree(root) {
      if (root === null) {
        return null;
      }
      const left = this.invertTree(root.left);
      const right = this.invertTree(root.right);
      root.left = right;
      root.right = left;
      return root;
    }

    connect(root) {
      //这个算法需要完全二叉树
      if (root === null) {
        return null;
      }
      this.connectTravel(root.left, root.right);
    }

    connectTravel(node1, node2) {
      //这里需要把node1和node2,抽象看做一个整体
      if (node1 === null || node2 === null) {
        return;
      }
      node1.next = node2;
      this.connectTravel(node1.left, node1.right);
      this.connectTravel(node1.right, node1.right);
      this.connectTravel(node1.right, node2.left);
    }

    flatten(root) {
      if (root === null) {
        return;
      }
      this.flatten(root.left);
      this.flatten(root.right);
      //注意,后序中有三步操作
      //左右子树已经展平
      const left = root.left;
      const right = root.right;
      //左子树接上当前节点的右节点
      root.left = null;
      root.right = left;
      //找到当前节点的有段
      const p = root;
      while(p.right !==null) {
        p = p.right;
      }
      p.next = right;
    }

    isValidBst(root) {
      return this.isValidBstHelper(root, null,null);
    }

    isValidBstHelper(root, min, max) {
      if (root === null) {
        return true;
      }
      //判断左子树
      if (min!==null && min.data >= root.data) {
        return false;
      }
      //判断左子树
      if (max!==null && max.data <= root.data) {
        return false;
      }
      return this.isValidBstHelper(root.left,min,root) &&
        this.isValidBstHelper(root.right,root,max);
    }

    isValidBst1(root) {
      if (root === null) {
        return true;
      }
      const leftValid = this.isValidBst1(root.left);
      const rightValid = this.isValidBst1(root.right);
      //判断当前节点是否合法
      if (root.left!== null && root.data<= root.left.data) {
        return false;
      }
      if (root.right!== null && root.data>= root.right.data) {
        return false;
      }
      return leftValid && rightValid;
    }

    static buildTree(arrays) {
      return BinarySearchTree.build(arrays,0, arrays.length-1);
    }

    static build(arrays,start, end) {
      //如果只有一个节点即start===end,那么需要继续递归,
      //因此base case is below
      if (start > end) {
        return null;
      }
      //获取当前迭代的根节点值和索引,分成两半分别迭代,返回值是当前的根节点
      let index = -1,maxVal = Number.MIN_VALUE;
      for (let i = start; i <= end; i++) {
        if (arrays[i] > maxVal ) {
          maxVal = arrays[i];
          index = i;
        }
      }
      const root = new Node(maxVal);
      const left = BinarySearchTree.build(arrays, start, index-1);
      const right = BinarySearchTree.build(arrays, index+1, end);
      root.left = left;
      root.right = right;
      return root;
    }

    static buildTree1(preOrders, inOrders){
      return BinarySearchTree.build1(preOrders,0, preOrders.length-1,
                                   inOrders,0,inOrders.length-1);
    }

    static build1(preOrders, preStart,preEnd,inOrders, inStart,inEnd){
      if (preStart>preEnd) {
        return null;
      }
      const rootVal = preOrders[preStart];
      let rootIndex = -1;
      for (let i = inStart ;i <= inEnd; i++) {
        if (rootVal === inOrders[i]) {
          rootIndex = i;
          break;
        }
      }
      // 通过中序计算左边子树的个数
      const leftSize = rootIndex-inStart;
      const rootNode = new Node(rootVal);
      const left = BinarySearchTree.build1(preOrders,preStart+1,preStart+leftSize,
                                         inOrders,inStart, rootIndex-1);
      const right = BinarySearchTree.build1(preOrders,preStart+leftSize+1,preEnd,
                                         inOrders,rootIndex+1, inEnd);
      rootNode.left = left;
      rootNode.right = right;
      return rootNode;
    }

    static buildTree2(inOrders,postOrders) {
      return BinarySearchTree.build2(inOrders,0, inOrders.length-1,
                                   postOrders,0,postOrders.length-1);
    }

    static build2(inOrders, inStart,inEnd ,
                     postOrders, postStart,postEnd) {
      if (postStart > postEnd) {
        return null;
      }
      const rootVal = postOrders[postEnd];
      let rootIndex = -1;
      for (let i = inStart ;i <= inEnd; i++) {
        if (rootVal === inOrders[i]) {
          rootIndex = i;
          break;
        }
      }
      // 通过中序计算左边子树的个数
      const leftSize = rootIndex-inStart;
      const rootNode = new Node(rootVal);
      const left = BinarySearchTree.build2(inOrders,inStart,rootIndex-1,
                                         postOrders,postStart, postStart+ leftSize-1);
      const right = BinarySearchTree.build2(inOrders,rootIndex+1,inEnd,
                                         postOrders,postStart+ leftSize, postEnd-1);
      rootNode.left = left;
      rootNode.right = right;
      return rootNode;
    }
    // Helper function
    // findMinNode()
    // getRootNode()
    // inorder(node)
    // preorder(node)              
    // postorder(node)
    // search(node, data)
}
const buildedBst = new BinarySearchTree();
const root = BinarySearchTree.buildTree([4,9,15,8,6,2]);
buildedBst.root = root;
console.log(buildedBst.preorder(buildedBst.root));

const newBst = new BinarySearchTree();
const rootNew = BinarySearchTree.buildTree1([3,9,20,15,7],[9,3,15,20,7]);
newBst.root = rootNew;
console.log(newBst.preorder(newBst.root));

const newBst1 = new BinarySearchTree();
const rootNew1 = BinarySearchTree.buildTree2([5,2,6,4,7,1,8,3,9],[5,6,7,4,2,8,9,3,1]);
newBst1.root = rootNew1;
console.log(newBst1.inorder(newBst1.root));

const bst = new BinarySearchTree();
const newBet = new BinarySearchTree();
for (let i = 0 ; i< 20; i++){
	const value = random.integer(1, 100);
	newBet.insert(value);
}
console.log('inorder newBet');
//console.log(newBet.inorder(newBet.root));
bst.insert(10);
bst.insert(12);
bst.insert(8);
bst.insert(15);
bst.insert(9);
bst.insert(14);
// console.log('reverse left and right');
// bst.reverseLR(bst.root);

console.log('bst is valid or not:' + bst.isValidBst1(bst.root));

console.log('print level is:');
bst.printLevel(bst.root,1);
const bstCount = bst.calculateNodeCount(bst.root);
console.log('bst calculate count is:'+bstCount);
console.log('maxDepth:'+bst.maxDepth(bst.root));
console.log('leaves count:'+bst.leaves);
console.log('maxDepth1:'+bst.maxDepthRecursive(bst.root));
console.log('inorder');
console.log(bst.inorder(bst.root));
console.log('inOrder');
console.log(bst.inOrder(bst.root));
//console.log('inorderiterator');
//console.log(bst.inorderiterator(bst.root));
console.log('preorder');
console.log(bst.preorder(bst.root));
console.log('preOrder');
console.log(bst.preOrder(bst.root));
//console.log('preorderiterator1');
//console.log(bst.preorderiterator1(bst.root));
console.log('postorder');
console.log(bst.postorder(bst.root));
console.log('postOrder');
console.log(bst.postOrder(bst.root));
console.log('postorderiterator');
console.log(bst.postorderiterator(bst.root));


bst.remove(12);

 

 

子章节

发布于 2023.06.12 1分钟阅读 0 评论 10 推荐

    作者:

z最早出现在操作

x现在

发布于 2023.06.12 1分钟阅读 0 评论 11 推荐

    作者:

/都是/

为什么要写笔记

发布于 2023.06.12 1分钟阅读 0 评论 15 推荐

    作者:

 

一、写笔记是一个设计,思考和记录的过程。

对于技术笔记来说,它展示的是作者分析和分解需求,设计架构,优秀编码以及高效输出的一个过程。这些过程的思考,如果没有及时记录下来,后续就会花更多的时间来回顾,这是不高效的。

书写是为了更好的思考,记录是为了更好的反思。

 

二、想法只是昙花,文字才能永恒。

任何时候只有写出来的才算输出。记得看过一段话,一个会坚持记笔记的人,肯定是个学习能力强,做事踏实又有耐心的人。

 

三、经验只有经过复盘,才能成为您的能力。

记笔记是实现这一过程最有效的捷径。

 

四、热心分享,方便他人学习和交流。

知识的分享是一个快乐的过程。

JPA实体映射——多对一关系映射进一步学习

发布于 2023.05.19 22分钟阅读 0 评论 5 推荐

    作者:

我们先看看第一种情况,如何持计划多对一的关系,以及如何删除这样的关系?

首先上代码:

public class ManyToOneTest extends AbstractTest {


    @Override
    protected Class[] entities() {
        return new Class[]{
                Post.class,
                PostComment.class,
        };
    }

    @Test
    public void testLifecycle() {
        //第一步先持久化一个Post实体
        doInJPA(entityManager -> {
            Post post = new Post()
                    .setId(1L)
                    .setTitle("First post");
            entityManager.persist(post);
        });
        //通过查询出主表post,然后持计划子表post_comment
        doInJPA(entityManager -> {
            Post post = entityManager.find(Post.class, 1L);

            entityManager.persist(
                    new PostComment()
                            .setId(1L)
                            .setReview("My first review")
                            .setPost(post)
            );
        });
        //解除与post的关系
        doInJPA(entityManager -> {
            PostComment comment = entityManager.find(PostComment.class, 1L);

            comment.setPost(null);
            entityManager.remove(comment);
        });
        //删除post_comment表的数据
        doInJPA(entityManager -> {
            PostComment comment = entityManager.getReference(PostComment.class, 1L);

            entityManager.remove(comment);
        });
    }

    @Entity(name = "Post")
    @Table(name = "post")
    public static class Post {

        @Id
        private Long id;

        private String title;

        public Long getId() {
            return id;
        }

        public Post setId(Long id) {
            this.id = id;
            return this;
        }

        public String getTitle() {
            return title;
        }

        public Post setTitle(String title) {
            this.title = title;
            return this;
        }
    }

    @Entity(name = "PostComment")
    @Table(name = "post_comment")
    public static class PostComment {

        @Id
        private Long id;

        private String review;

        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "post_id")
        private Post post;

        public Long getId() {
            return id;
        }

        public PostComment setId(Long id) {
            this.id = id;
            return this;
        }

        public String getReview() {
            return review;
        }

        public PostComment setReview(String review) {
            this.review = review;
            return this;
        }

        public Post getPost() {
            return post;
        }

        public PostComment setPost(Post post) {
            this.post = post;
            return this;
        }
    }
}

首先分析一下上面的代码:主表显然是post,主键是post表的id;子表是post_detail,外键是post_id;

有了以上分析,我们可以看出,持久化主表post是最简单的,直接调用entityManager.persist()就可以了,而持久化子表就不那么容易,需要建立表外键关系,就是查询出post,然后设置到子表的post属性中,然后才持计划。

同理,在删除的时候,你不能直接删除post表,因为它的主键被post_detail的外键引用;需要先删除post_detail后才能删除;而删除post_detail的时候,需要先解除外键关系,然后再删除。

我们看看以上测试执行的SQL:

["insert into post (title, id) values (?, ?)"], Params:[(First post,1)]
["select manytoonet0_.id as id1_0_0_, manytoonet0_.title as title2_0_0_ from post manytoonet0_ where manytoonet0_.id=?"], Params:[(1)]
["insert into post_comment (post_id, review, id) values (?, ?, ?)"], Params:[(1,My first review,1)]
["select manytoonet0_.id as id1_1_0_, manytoonet0_.post_id as post_id3_1_0_, manytoonet0_.review as review2_1_0_ from post_comment manytoonet0_ where manytoonet0_.id=?"], Params:[(1)]
["update post_comment set post_id=?, review=? where id=?"], Params:[(NULL(BIGINT),My first review,1)]
["select manytoonet0_.id as id1_1_0_, manytoonet0_.post_id as post_id3_1_0_, manytoonet0_.review as review2_1_0_ from post_comment manytoonet0_ where manytoonet0_.id=?"], Params:[(1)]
["delete from post_comment where id=?"], Params:[(1)]

好了,从以上代码可以看出在这样的多对一的映射下,该如何操作了吧?同时您也可以看到,是不是有三个不必要的查询呢?这里是否可以优化呢?

 

我们看看下面的代码:

@Test
public void testThreePostComments() {
    doInJPA(entityManager -> {
        Post post = new Post()
            .setId(1L)
            .setTitle("First post");
        entityManager.persist(post);
    });
    doInJPA(entityManager -> {
        Post post = entityManager.getReference(Post.class, 1L);

        entityManager.persist(
            new PostComment()
                .setId(1L)
                
                .setReview("My first review")
                .setPost(post)
        );

        entityManager.persist(
            new PostComment()
                .setId(2L)
                .setReview("My second review")
                .setPost(post)
        );

        entityManager.persist(
            new PostComment()
                .setId(3L)
                .setReview("My third review")
                .setPost(post)
        );
    });

    doInJPA(entityManager -> {
        PostComment comment1 = entityManager.getReference(PostComment.class, 2L);

        entityManager.remove(comment1);
    });

    doInJPA(entityManager -> {
        List comments = entityManager.createQuery(
            "select pc " +
            "from PostComment pc " +
            "where pc.post.id = :postId", PostComment.class)
            .setParameter("postId", 1L)
            .getResultList();

        assertEquals(2, comments.size());
    });
}

这个例子和上一个例子最大的区别是,在持久化子表post_detail的时候,查询的时候使用了entityManager.getReference(),使用这个方法的好处是避免了对主表post的查询,从而少了一次查询。我们看看日志:

["insert into post (title, id) values (?, ?)"], Params:[(First post,1)]
["insert into post_comment (post_id, review, id) values (?, ?, ?)"], Params:[(1,My first review,1)]
["insert into post_comment (post_id, review, id) values (?, ?, ?)"], Params:[(1,My second review,2)]
["insert into post_comment (post_id, review, id) values (?, ?, ?)"], Params:[(1,My third review,3)]
["select manytoonet0_.id as id1_1_0_, manytoonet0_.post_id as post_id3_1_0_, manytoonet0_.review as review2_1_0_ from post_comment manytoonet0_ where manytoonet0_.id=?"], Params:[(2)]
["delete from post_comment where id=?"], Params:[(2)]
["select manytoonet0_.id as id1_1_, manytoonet0_.post_id as post_id3_1_, manytoonet0_.review as review2_1_ from post_comment manytoonet0_ where manytoonet0_.post_id=?"], Params:[(1)]

从日志我们可以看出,在插入子表部分进行了优化,当然,如果这里可以批量插入,那么这个优化就太棒了,不知道有没有啥好的办法做到批量插入子表;同时这里的删除如果能直接删除不用查询就更好了。

 

 

JPA实体映射——多对一关系映射

更新于 2023.05.19 21分钟阅读 0 评论 12 推荐

    作者:

前几节我们介绍了一对多的关系和一对一关系,今天我们学习多对一关系以及这种映射方式的最佳实践,先上业务实例图。

在我们的业务关系图中,部门和研究所实体是多对一的关系,同时我们还是采用双向关联来说明问题

Bidirectional @ManyToOne

部门实体

import javax.persistence.*;
import java.io.Serializable;
import java.util.Objects;
@Entity(name = "Department")
@Table(name = "departments")
public class Department implements Serializable {
    @Id
    @GeneratedValue
    private Long id = 0L;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    private Institute institute;

    public Department(){}

    public void setId(Long id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setInstitute(Institute institute) {
        this.institute = institute;
    }

    public Department(String name){
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public Institute getInstitute() {
        return institute;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Department)) return false;
        Department that = (Department) o;
        return name.equals(that.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }
}

DepartmentDAO

import com.jpa.demo.model.bidirectional.Department;
import com.jpa.demo.model.bidirectional.Institute;
import com.jpa.demo.utils.JPAUtil;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;

public class DepartmentDAO {

    private EntityManagerFactory entityManagerFactory = JPAUtil.getEntityManagerFactory();


    public void saveDepartment(Long instituteId) {
        EntityManager entityManager = null;
        Long id = null;
        try {
            entityManager = this.entityManagerFactory.createEntityManager();
            EntityTransaction tx = entityManager.getTransaction();
            tx.begin();
            Institute institute = entityManager.find(Institute.class, instituteId);
            Department department = new Department("深圳研究所-第一部门");
            department.setInstitute(institute);
            entityManager.persist(department);
            tx.commit();
        } finally {
            entityManager.close();
        }
    }
}

测试代码

@Test
public void testSaveDepartment() {
    Institute institute = new Institute("深圳研究所");
    institute.addDepartment(
        new Department("深圳研究所1部")
    );
    InstituteDAO dao = new InstituteDAO();
    dao.save(institute);
    Long instituteId = institute.getId();

    DepartmentDAO departmentDAO = new DepartmentDAO();
    departmentDAO.saveDepartment(instituteId);
}

日志信息

Hibernate: 
    select
        institute0_.id as id1_1_0_,
        institute0_.name as name2_1_0_ 
    from
        institutes institute0_ 
    where
        institute0_.id=?
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    insert 
    into
        departments
        (institute_id, name, id) 
    values
        (?, ?, ?)

可以看出,我在保存部门信息的时候,同时还查询除了研究所信息,这里可以优化为直接保存部门信息。

只需要修改一行代码

Institute institute = entityManager.getReference(Institute.class, instituteId);

修改成getReference()方法后,就不用去查询研究所的实体啦。

Hibernate: 
   insert 
   into
       departments
       (institute_id, name, id) 
   values
       (?, ?, ?)

 

查询多对一多方的一条记录,查询方法如下:

public void queryDepartment(Long departmentId) {
    EntityManager entityManager = null;
    try {
        entityManager = this.entityManagerFactory.createEntityManager();
        EntityTransaction tx = entityManager.getTransaction();
        tx.begin();
        Department department = entityManager.find(Department.class, departmentId);
        department.getInstitute().getName();

        tx.commit();
    } finally {
        entityManager.close();
    }
 
}

测试代码

@Test
public void testQueryDepartment() {
    Institute institute = new Institute("深圳研究所");
    institute.addDepartment(
            new Department("深圳研究所1部")
    );
    InstituteDAO dao = new InstituteDAO();
    dao.save(institute);
    Long instituteId = institute.getId();

    DepartmentDAO departmentDAO = new DepartmentDAO();
    Department department = departmentDAO.saveDepartment(instituteId);
    Long departmentId = department.getId();
    departmentDAO.queryDepartment(departmentId);
}

日志信息:

Hibernate: 
   select
       department0_.id as id1_0_0_,
       department0_.institute_id as institut3_0_0_,
       department0_.name as name2_0_0_ 
   from
       departments department0_ 
   where
       department0_.id=?
Hibernate: 
   select
       institute0_.id as id1_1_0_,
       institute0_.name as name2_1_0_ 
   from
       institutes institute0_ 
   where
       institute0_.id=?

可以看出,为了获取研究所的信息,同时也查询了研究所实体,在此情况下,可以使用另一种方法不用查询研究所实体。

public void queryDepartmentByJPQL(Long departmentId) {
        EntityManager entityManager = null;
        try {
            entityManager = this.entityManagerFactory.createEntityManager();
            EntityTransaction tx = entityManager.getTransaction();
            tx.begin();
            Department department = entityManager.createQuery("select dt from Department dt join fetch\n" +
                            "                     dt.institute where dt.id =:id", Department.class)
                    .setParameter("id",departmentId)
                    .getSingleResult();;
            department.getInstitute().getName();
            entityManager.persist(department);
            tx.commit();
        } finally {
            entityManager.close();
        }
    }

测试方法只需要修改一行

departmentDAO.queryDepartmentByJPQL(departmentId);

日志信息如下:

Hibernate: 
   select
       department0_.id as id1_0_0_,
       institute1_.id as id1_1_1_,
       department0_.institute_id as institut3_0_0_,
       department0_.name as name2_0_0_,
       institute1_.name as name2_1_1_ 
   from
       departments department0_ 
   inner join
       institutes institute1_ 
           on department0_.institute_id=institute1_.id 
   where
       department0_.id=?

可以看出,这次只发出了一条SQL,不同于上一种方式发出了两条SQL,优化了性能,同时也取得了相应的值。

深入理解关系数据表之间的关系

更新于 2023.05.19 1分钟阅读 0 评论 5 推荐

    作者:

首先我们需要理解的是,关系是怎么产生的?在关系表中,关系是属于不同表的相应的行来构建形成的。

子表定义了一个外键引用了父表主键的时候,表关系就形成。所以理解表关系,需要理解上述四个关键因素以及它们之间的关系。

表关系建立在外键的基础上,因此,每一类表关系有三类关系类型:

  1.  一对多是最普通的关系,它通过将父表的一行与子表的多行关联起来形成这种关系。
  2.  一对一关系需要子表的主键通过外键的形式与父表的主键引用而关联起来。(这里是所谓的共享主键)
  3.  多对多关系需要一个包含两个外键的中间表来引用两个父表的主键,从而关联起来形成这种关系。

这里分别用三张图来说明三种关系:

One-To-Many

 

One-To-One

 

Many-To-Many

 

以上图示简单明了,如果不能理解的话,那么就试着寻找关系的四个要素:父表,主键,子表,外键。理解了这四个要素,那么你就理解的关系数据库表之间的各种关系。

 

 

BTree的一种简单实现

更新于 2023.05.15 36分钟阅读 0 评论 15 推荐

    作者:

最近在学习和理解红黑树的过程中,发现想要深入理解红黑树的本质离不开一种重要的数据结构,这种数据结构就是平衡树(BTree),今天开始学习BTree的一些基本操作。

require("@fatso83/mini-mocha").install();
const { expect } = require('chai');
class BTreeNode {
  constructor(isLeaf){
    //是否属于叶子节点
    this.isLeaf = isLeaf;
    //节点用于存储具体数据的数组,且数据从前到后有顺序
    this.values = [];
    //存储节点对应的分支,或者子节点
    this.children = [];
    //存储节点所在的树节点
    this.tree = null;
    //节点对应的父节点
    this.parent = null;
  }

  /**
   * 获取节点存储数据的长度
   */
  get n() {
    return this.values.length;
  }

  /**
   * 在某个节点增加一个值
   */
  addValue(value) {
    if(!value) {
      return;
    }
    let pos = 0;
    while(pos < this.n && this.values[pos] < value) {
      pos++;
    }
    return this.values.splice(pos,0,value);
  }
  removeValue(pos) {
    if (pos >= this.n) {
      return null;
    }
    return this.values.splice(pos, 1)[0];
  }
  
  /**
   * 在指定位置添加一个子节点
   */
  addChild(node, pos) {
    this.children.splice(pos,0,node);
    node.parent = this;
  }

  /**
   * 删除指定位置的节点
   */
  removeChild(pos) {
    return this.children.splice(pos,1)[0];
  }
}

describe('BTreeNode test',()=>{
  it('BTreeNode init test',()=>{
    let bTreeNode = new BTreeNode(false);
    expect(bTreeNode.isLeaf).to.equal(false);
    expect(bTreeNode.n).to.equal(0);
    expect(bTreeNode.children.length).to.equal(0);
    expect(bTreeNode.tree).to.equal(null);
    expect(bTreeNode.parent).to.equal(null);
  });

  it('BTreeNode addValue test',()=>{
    let bTreeNode = new BTreeNode(false);
    bTreeNode.addValue(5);
    bTreeNode.addValue(8);
    bTreeNode.addValue(2);
    expect(bTreeNode.n).to.equal(3);
    const values = bTreeNode.values;
    expect(values.join('')).to.equal('258')
  });

  it('BTreeNode removeValue test',()=>{
    let bTreeNode = new BTreeNode(false);
    bTreeNode.addValue(3);
    bTreeNode.addValue(9);
    bTreeNode.addValue(7);
    bTreeNode.addValue(6);
    expect(bTreeNode.n).to.equal(4);
    const values = bTreeNode.values;
    expect(values.join('')).to.equal('3679');
    bTreeNode.removeValue(2);
    expect(bTreeNode.n).to.equal(3);
    const values1 = bTreeNode.values;
    expect(values1.join('')).to.equal('369');
  })
  
});

class BTree {
  constructor(order){
    this.order = order;
    this.root = null;
  }

  /**
   * Search a value in the Tree and return the node. O(log N)
   * @param {number} value 
   * @returns {BTreeNode}
   */
  searchValue(node,value) {
    if (node.values.includes(value)) {
      return node;
    }
    if (node.leaf) {
      return null;
    }
    let child = 0;
    while(child <= node.n && node.values[child]< parseInt(value,10)) {
      child++;
    }
    return this.searchValue(node.children[child],value);
  }

  /**
   * Insert a new value in the tree O(log N)
   * @param {number} value
   */
  insert(value) {
    const actual = this.root;
    if (actual.n === 2 * this.order - 1) {
      // Create a new node to become the root
      // Append the old root to the new one
      const temp = new BTreeNode(false);
      temp.tree = this;
      this.root = temp;
      temp.addChild(actual, 0);
      this.split(actual, temp, 1);
      this.insertNonFull(temp, parseInt(value));
    } else {
      this.insertNonFull(actual, parseInt(value));
    }
  }

  /**
   * Insert a value in a not-full node. O(1)
   * @param {BTreeNode} node 
   * @param {number} value
   */
  insertNonFull(node, value) {
    if (node.leaf) {
      node.addValue(value);
      return;
    }
    let temp = node.n;
    //找到需要插入的位置
    while(temp >1 && value < node.values[temp-1]) {
      temp = temp-1;
    }
    if(node.children[temp].n === (this.order * 2 -1) ) {
      this.split(node.children[temp], node, temp + 1);
      if (value  > node.values[temp]) {
        temp = temp + 1;
      }
    }
    this.insertNonFull(node.children[temp], value);
  }

  /**
   * Divide child node from parent into parent.values[pos-1] and parent.values[pos]
   * O(1)
   * @param {BTreeNode} child 
   * @param {BTreeNode} parent 
   * @param {number} pos 
   */
  split(child, parent, pos) {
    const newChild = new BTreeNode(child.leaf);
    newChild.tree = this.root.tree;
    // Create a new child
    // Pass values from the old child to the new
    for (let k = 1; k < this.order; k++) {
      newChild.addValue(child.removeValue(this.order));
    }
    // Trasspass child nodes from the old child to the new
    if (!child.leaf) {
      for (let k = 1; k <= this.order; k++) {
        newChild.addChild(child.deleteChild(this.order), k - 1);
      }
    }
    // Add new child to the parent
    parent.addChild(newChild, pos);
    // Pass value to parent
    parent.addValue(child.removeValue(this.order - 1));
    parent.leaf = false;
  }

  /**
   * Deletes the value from the Tree. O(log N)
   * @param {number} value 
   */
  delete(value) {
    if (this.root.n === 1 && !this.root.leaf &&
      this.root.children[0].n === this.order-1 && this.root.children[1].n === this.order -1) {
      // Check if the root can shrink the tree into its childs
      this.mergeNodes(this.root.children[1], this.root.children[0]);
      this.root = this.root.children[0];
    }
    // Start looking for the value to delete
    this.deleteFromNode(this.root, parseInt(value, 10));
  }

  /**
   * Delete a value from a node
   * @param {BTreeNode} node 
   * @param {number} value 
   */
  deleteFromNode(node, value) {
    const index = node.values.indexOf(value);
    if (index >=0) {
      // Value present in the node simple case1 叶子节点且数目也足够,直接删除
      if (node.leaf && node.n > this.order - 1) {
        // If the node is a leaf and has more than order-1 values, just delete it
        node.removeValue(node.values.indexOf(value));
        return true;
      }
      if(node.children[index].n > this.order-1 || 
        node.children[index+1].n > this.order-1) {
        // One of the immediate children has enough values to transfer
        if (node.children[index].n > this.order - 1) {
          // Replace the target value for the higher of left node.
          // Then delete that value from the child
          const predecessor = this.getMinMaxFromSubTree(node.children[index], 1);
          node.values[index] = predecessor;
          return this.deleteFromNode(node.children[index], predecessor);
        }
        const successor = this.getMinMaxFromSubTree(node.children[index+1], 0);
        node.values[index] = successor;
        return this.deleteFromNode(node.children[index+1], successor);
      }
      // Children has not enough values to transfer. Do a merge
      this.mergeNodes(node.children[index + 1], node.children[index]);
      return this.deleteFromNode(node.children[index], value);
    }
    // Value is not present in the node
    if (node.leaf) {
      // Value is not in the tree
      return false;
    }
    // Value is not present in the node, search in the children
    let nextNode = 0;
    while (nextNode < node.n && node.values[nextNode] < value) {
      nextNode++;
    }
    if (node.children[nextNode].n > this.order - 1) {
      // Child node has enough values to continue
      return this.deleteFromNode(node.children[nextNode], value);
    }
    // Child node has not enough values to continue
    // Before visiting next node transfer a value or merge it with a brother
    if ((nextNode > 0 && node.children[nextNode - 1].n > this.order - 1) ||
      (nextNode < node.n && node.children[nextNode + 1].n > this.order - 1)) {
      // One of the immediate children has enough values to transfer
      if (nextNode > 0 && node.children[nextNode - 1].n > this.order - 1) {
        this.transferValue(node.children[nextNode - 1], node.children[nextNode]);
      } else {
        this.transferValue(node.children[nextNode + 1], node.children[nextNode]);
      }
      return this.deleteFromNode(node.children[nextNode], value);
    }
    // No immediate brother with enough values.
    // Merge node with immediate one brother
    this.mergeNodes(
      nextNode > 0 ? node.children[nextNode - 1] : node.children[nextNode + 1],
      node.children[nextNode]);
    return this.deleteFromNode(node.children[nextNode], value);
  }

  /**
   * Get the lower or higher value in a sub-tree. O(log N)
   * @param {BTreeNode} node 
   * @param { 0 | 1 } max 1 for find max, 0 for min
   * @returns {number}
   */
  getMinMaxFromSubTree(node, max) {
    while (!node.leaf) {
      node = node.children[max ? node.n : 0];
    }
    return node.values[max ? node.n - 1 : 0];
  }

  /**
   * Transfer one value from the origin to the target. O(1)
   * @param {BTreeNode} origin 
   * @param {BTreeNode} target 
   */
  transferValue(origin, target) {
    const indexo = origin.parent.children.indexOf(origin);
    const indext = origin.parent.children.indexOf(target);
    if (indexo < indext) {
      target.addValue(target.parent.removeValue(indexo));
      origin.parent.addValue(origin.removeValue(origin.n-1));
      if (!origin.leaf) {
        target.addChild(origin.deleteChild(origin.children.length-1), 0);
      }
    } else {
      target.addValue(target.parent.removeValue(indext));
      origin.parent.addValue(origin.removeValue(0));
      if (!origin.leaf) {
        target.addChild(origin.deleteChild(0), target.children.length);
      }
    }
  }

  /**
   * Mix 2 nodes into one. O(1)
   * @param {BTreeNode} origin 
   * @param {BTreeNode} target 
   */
  mergeNodes(origin, target) {
    const indexo = origin.parent.children.indexOf(origin);
    const indext = target.parent.children.indexOf(target);
    target.addValue(target.parent.removeValue(Math.min(indexo, indext)));
    for (let i = origin.n - 1; i >= 0; i--) {
      target.addValue(origin.removeValue(i));
    }
    // Remove reference to origin node
    target.parent.deleteChild(indexo);
    // Transfer all the children from origin node to target
    if (!origin.leaf) {
      while (origin.children.length) {
        if (indexo > indext) {
          target.addChild(origin.deleteChild(0), target.children.length);
        } else {
          target.addChild(origin.deleteChild(origin.children.length - 1), 0);
        }
      }
    }
  }
}

Graphs 简单学习

更新于 2023.05.12 22分钟阅读 1 评论 15 推荐

    作者:

最近学习数据结构的时候,发现一个有意思的规律,那就是复杂的数据结构一定是对简单数据结构的封装和抽象。因此,今天学习一个稍微复杂的数据结构,那就是Graphs。

require("@fatso83/mini-mocha").install();
const { expect } = require('chai');
class Node {
  constructor(data) {
    this.data = data;
    this.nextElement = null;
  }
}

class LinkedList {
  constructor() {
    this.head = null;
  }

  //Insertion At Head  
  insertAtHead(newData) {
    let tempNode = new Node(newData);
    tempNode.nextElement = this.head;
    this.head = tempNode;
    return this; //returning the updated list
  }

  isEmpty() {
    return (this.head == null);
  }

  //function to print the linked list
  printList() {
    const arrStr = [];
    if (this.isEmpty()) {
      arrStr.push('Empty List');
      console.log(arrStr.join(''));
      return false;
    } else {
      let temp = this.head;
      while (temp != null) {
        arrStr.push(temp.data);
        arrStr.push('->');
        temp = temp.nextElement;
      }
      arrStr.push('null');
      console.log(arrStr.join(''));
      return true;
    }
  }

  getHead() {
    return this.head;
  }
  setHead(newHead) {
    this.head = newHead;
    return this;
  }
  getListStr() {
    if (this.isEmpty()) {
      console.log("Empty List");
      return "null";
    } else {
      let st = "";
      let temp = this.head
      while (temp != null) {
        st += (temp.data);
        st += " -> ";
        temp = temp.nextElement;
      }
      st += "null";
      return st;
    }
  }
  insertAtTail(newData) {
    //Creating a new Node with data as newData
    let node = new Node(newData);

    //check for case when list is empty
    if (this.isEmpty()) {
      //Needs to Insert the new node at Head
      this.head = node;
      return this;
    }

    //Start from head
    let currentNode = this.head;

    //Iterate to the last element
    while (currentNode.nextElement != null) {
      currentNode = currentNode.nextElement;
    }

    //Make new node the nextElement of last node of list
    currentNode.nextElement = node;
    return this;
  }
  search(value) {
    //Start from the first element
    let currentNode = this.head;

    //Traverse the list until you find the value or reach the end
    while (currentNode != null) {
      if (currentNode.data == value) {
        return true; //value found
      }
      currentNode = currentNode.nextElement
    }
    return false; //value not found
  }
  deleteAtHead() {
    //if list is empty, do nothing
    if (this.isEmpty()) {
      return this;
    }
    //Get the head and first element of the list
    let firstElement = this.head;

    //If list is not empty, link head to the nextElement of firstElement
    this.head = firstElement.nextElement;

    return this;
  }
  deleteVal(value) {
    let deleted = null; //True or False
    //Write code here

    //if list is empty return false
    if (this.isEmpty()) {
      return false;
    }

    //else get pointer to head
    let currentNode = this.head;
    // if first node's is the node to be deleted, delete it and return true
    if (currentNode.data == value) {
      this.head = currentNode.nextElement;
      return true;
    }

    // else traverse the list
    while (currentNode.nextElement != null) {
      // if a node whose next node has the value as data, is found, delete it from the list and return true
      if (currentNode.nextElement.data == value) {
        currentNode.nextElement = currentNode.nextElement.nextElement;
        return true;
      }
      currentNode = currentNode.nextElement;
    }
    //else node was not found, return false
    deleted = false;
    return deleted;
  }
  deleteAtTail() {
    // check for the case when linked list is empty
    if (this.isEmpty()) {
      return this;
    }
    //if linked list is not empty, get the pointer to first node
    let firstNode = this.head;
    //check for the corner case when linked list has only one element
    if (firstNode.nextElement == null) {
      this.deleteAtHead();
      return this;
    }
    //otherwise traverse to reach second last node
    while (firstNode.nextElement.nextElement != null) {
      firstNode = firstNode.nextElement;
    }
    //since you have reached second last node, just update its nextElement pointer to point at null, skipping the last node
    firstNode.nextElement = null;
    return this;
  }
}
//定义一个Graphs
class Graph {
  constructor(vertices){
    //存储顶点数目
    this.vertices = vertices;
    //Defining an array which can 
    //hold LinkedLists equal to the number of vertices in the graph
    //存储顶点对应的边的邻接表
    this.list = [];
    for (let i = 0 ; i < this.vertices ;i++) {
      const temp = new LinkedList();
      this.list.push(temp);
    }
  }

  printGraph() {
    console.log(">>Adjacency List of Directed Graph<<");
    var i;
    for (i = 0; i < this.list.length; i++) {
      console.log("|" + (i) + "| => ");
      let temp = this.list[i].getHead();
      while (temp != null) {
        console.log("[" + (temp.data) + "] -> ");
        temp = temp.nextElement;
      }
      console.log("end ");
    }
  }
  addEdge(source,destination) {
    if (source < this.vertices && destination < this.vertices)
      this.list[source].insertAtHead(destination);
  }
}

describe('LinkedList test',()=>{
  it('LinkedList insertAtHead test',()=>{
    const list = new LinkedList();
    expect(list.head).to.equal(null);
    list.insertAtHead(10);
    expect(list.head.data).to.equal(10);
    list.insertAtHead(20);
    expect(list.head.data).to.equal(20);
  });

  it('LinkedList isEmpty test',()=>{
    const list = new LinkedList();
    expect(list.isEmpty()).to.equal(true);
    list.insertAtHead(10);
    expect(list.isEmpty()).to.equal(false);
  });
  it('LinkedList printList test',()=>{
    const list = new LinkedList();
    expect(list.printList()).to.equal(false);
    list.insertAtHead(10).insertAtHead(20).insertAtHead(30);
    expect(list.printList()).to.equal(true);
  });
});

let g = new Graph(4);
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 3);
g.addEdge(2, 3);
g.printGraph();

从以上代码可以看出,我们在Graph中使用了LinkedList数据结构,因此,如果熟练掌握了基础的数据结构,使用起来还是比较容易理解的;

我们为这个Graph添加了两个重要方法,

addEdgeprintGraph一个用于构建整个Graph,另一个用于查看我们构建的情况是否正确。

我们以后再在这个类的基础上学习Graph的Breadth-First Search(BFS)Depth-First Search (DFS)。

本文水平有限,如果发现有啥问题,希望能提出来一起讨论。

Union Find 数据结构和算法理解

更新于 2023.05.11 2分钟阅读 2 评论 15 推荐

    作者:

最近开始学习一些不那么简单的算法,比如上一篇中的Graph就是一种不那么容易理解和学习的数据结构,今天来学习另一种数据结构和算法。

Union-Find这种算法的有哪些特点呢?

1、用最少的代码解决相对复杂的问题,而且时间和空间复杂度还相当优异。

2、这种算法能解决类似问题,比如:给定一组顶点和边,找出哪些组件属于同一个组,及存在多少个这样的组?

3、这种算法的另一个名称:"Disjoint Set"。

Union-Find算法的核心操作有哪些?

1、基本思想是使用数组(或类数组)数据结构模拟森林(一组树)。数组的索引代表节点。数组的值代表每个节点的父节点。

2、例如,如果 array[i] == i,则表示节点 i 是该组的父节点。如果 array[i] == j 且 i != j,则表示节点 i 指向节点 j,即节点 j 是节点 i 的父节点。

3、所以要找到节点 i 的父节点,我们可以从索引 i 向上遍历,直到遇到 array[j] == j,这意味着我们找到了 i 所在组的父节点。

4、总结下来,此算法有两个核心操作,那就是find以及union。

require("@fatso83/mini-mocha").install();
const { expect } = require('chai');
class UF {
  constructor(N){
    //存储父节点
    this.parent = Array.from({ length: N }, (_, i) => i);
    //代表所属的组有多少个元素
    this.count = new Array(N).fill(1);
  }

  /**
   * 寻找一个节点的最终父节点
   */
  find(x) {
    if (this.parent[x]!==x) {
      this.parent[x] = this.find(this.parent[x]);
    }
    return this.parent[x]
  }

  union(x, y){
    const xp = this.find(x), yp = this.find(y);
    if (xp == yp) return;
    if (this.count[xp] < this.count[yp]) {
      //说明yp节点是父节点,需要合并到yp节点所属的组中
      this.parent[xp] = yp;
      this.count[yp] += this.count[xp];
    } else {
      //合并到xp节点所属的组中
      this.parent[yp] = xp;
      this.count[xp] += this.count[yp];
    }
  }

  group() {
    return this.parent.filter((item,index)=>item==index).length;
  }
}

/**
 * 增加了一个单元测试
 */
describe('test UF', function(){
  
  it('test union of uf',()=>{
    const uf = new UF(10);
    uf.union(1,3);
    uf.union(2,3);
    uf.union(3,4);
    uf.union(7,8);
    expect(uf.group()).to.equal(6);
  });
  it('test find of uf',()=>{
    const uf = new UF(10);
    uf.union(1,3);
    uf.union(2,3);
    uf.union(3,4);
    uf.union(7,8);
    expect(uf.find(4)).to.equal(1);
    expect(uf.find(8)).to.equal(7);
  });
});

下面再来看看另一个例子:

const countComponents = function(n, edges) {
  const UF = Array.from({ length: n }, (_, i) => i);
  edges.forEach(([e1, e2]) => union(e1, e2));
  return UF.filter((c, i) => c == i).length;

  function union(c1, c2) {
    const p1 = find(c1), p2 = find(c2);
    if (p1 != p2) UF[p1] = p2;
  }

  function find(c) {
    if (c != UF[c]) UF[c] = find(UF[c]);
    return UF[c];
  };
};

这里例子的作用其实就是在一个无向图中,如果节点连接在一起算一个组件的话,那么这个方法就是计算出有多少个组件?大家认真想想,是不是和webpack中打包的时候,计算有多少个Chunck很像,也许webpack就是这样计算的。哈哈,猜的,如果有知道的,欢迎讨论。

这里再用另外一个例子来解释这个算法的作用:

require("@fatso83/mini-mocha").install();
const { expect } = require('chai');
class UF {
  constructor(N){
    //存储父节点
    this.parent = Array.from({ length: N }, (_, i) => i);
    //代表所属的组有多少个元素
    this.count = new Array(N).fill(1);
  }

  /**
   * 寻找一个节点的最终父节点
   */
  find(x) {
    if (this.parent[x]!==x) {
      this.parent[x] = this.find(this.parent[x]);
    }
    return this.parent[x]
  }

  union(x, y){
    const xp = this.find(x), yp = this.find(y);
    if (xp == yp) return;
    if (this.count[xp] < this.count[yp]) {
      //说明yp节点是父节点,需要合并到yp节点所属的组中
      this.parent[xp] = yp;
      this.count[yp] += this.count[xp];
    } else {
      //合并到xp节点所属的组中
      this.parent[yp] = xp;
      this.count[xp] += this.count[yp];
    }
  }

  group() {
    return this.parent.filter((item,index)=>item==index).length;
  }
}

const arrs = ["tars","rats","arts","star"];
//比较两个字符串是否相似
const isSimilar = (str1, str2)=>{
  const obj = {};
  let counter = 0;
  for (let i = 0 ; i < str1.length ; i++) {
    if (str1[i] !== str2[i]) {
      counter++;
      obj[str1[i]] = str2[i];
    }
  }
  return (counter === 2 && oppositeObj(obj))? true : false
}

const oppositeObj = (obj)=>{
  const keys = Object.keys(obj);
  const length = keys.length;
  if (length!=2){
    return false;
  }
  if (obj[keys[0]] !== keys[1]) {
    return false;
  }
  return true;
}

describe('pars-keys test',()=>{
  it('pars-keys obj={} test',()=>{
    const obj = {};
    const result = oppositeObj(obj);
    expect(result).to.equal(false);
  });
  it('pars-keys obj not two keys test',()=>{
    let obj = {
      'a': 'c'
    };
    const result = oppositeObj(obj);
    expect(result).to.equal(false);
    let obj1 = {
       'a': 'c',
       'b': 'b',
       'c':'a'
    };
    const result1 = oppositeObj(obj1);
    expect(result1).to.equal(false);
  });
  it('pars-keys obj has two key test',()=>{
    let obj = {
      'a':'c',
      'b':'c'
    }
    const result = oppositeObj(obj);
    expect(result).to.equal(false);
    let obj1 = {
      'a':'c',
      'c': 'a'
    }
    const result1 = oppositeObj(obj1);
    expect(result1).to.equal(true);
  });
});
describe('similar string group test',()=>{
    it('UF group test',()=>{
        const uf = new UF(4)
        for (let i = 0 ; i < arrs.length; i++) {
          for (j = i+1; j < arrs.length; j++) {
            if (isSimilar(arrs[i], arrs[j])) {
              uf.union(i,j);
            }
          }
        }
        expect(uf.group()).to.equal(2);  
    });
});
     

为什么React的render方法会执行两次?

更新于 2023.04.27 2分钟阅读 0 评论 15 推荐

    作者:

最近在写React组件的时候,发现了一个奇怪的问题,就是组件的render方法总是执行两次。

import Card from './src/Card';
import FriendProfile from './src/FriendProfile';

export default function App() {
  console.log('render App');
  return (
    ....
  );
}

这段代码在执行的时候,render App总是会执行两次。

经过网上搜索,发现了原因是:

在运行App的时候使用了严格模式(strict mode)。如果不使用这样的运行模式,实际上是不会执行两次的。

同时需要记住的一点就是,React在执行两次渲染的过程中,也会执行React组件的生命周期函数,比如会先卸载组件,然后再加载组件,因此React的两个生命周期函数componentDidMount():以及componentWillUnmount(): 都会执行一次。

我们可以利用执行两次的机会,检查我们组件的函数是否包含副作用,因此可以起到减少bug的作用。

至于为什么要使用严格模式,官方的说法是,这种模式一般在我们开发过程中使用,产品部署以后就会忽略,至于开发阶段使用的原因是:

检测渲染阶段那些存在的不期望的副作用的函数,方便开发者找出bug

好了,这里记录一下遇到的小问题和解决方法。

numpy.tile()扩展函数

发布于 2023.04.12 0分钟阅读 0 评论 5 推荐

    作者:

理解tile的意思,就容易理解tile的用法:

  • n.瓦片; (贴墙或铺地用的)瓷砖; 地砖; 小方地毯; 片状材料; (铺屋顶的)瓦; (棋盘游戏的)棋子;
  • vt.铺瓦; 铺地砖; 贴瓷砖; 平铺显示; 瓦片式显示

定义

numpy.tile(A, reps)

通过重复数组A来构造新的数组,从而达到扩展A的作用。重复次数有reps指定。tile原意是铺瓷砖,形象理解就把A作为瓷砖,平铺显示。

参数

  • A:array_like,输入数组。
  • reps:array_like,A沿各个轴的重复次数。
  • 返回值cndarray,扩展后的数组

假设reps长度为d,则新数组的扩展后的维度为max(d,A.ndim)。

示例

a = np.array([0, 1, 2])
>>> np.tile(a, 2)
array([0, 1, 2, 0, 1, 2])
>>> np.tile(a, (2, 2))
array([[0, 1, 2, 0, 1, 2],
       [0, 1, 2, 0, 1, 2]])
>>> np.tile(a, (2, 1, 2))
array([[[0, 1, 2, 0, 1, 2]],
       [[0, 1, 2, 0, 1, 2]]])
  • np.tile(a, 2)表示将数组a,在一维扩展2倍。
  • np.tile(a, (2,2))表示在将数组a,在一维数组扩展2倍,然后再二维数组扩展2倍。
  • np.tile(a, (2,1,2))表示在将数组a,在一维数组扩展2倍,在二维数组不扩展,最后在三维数组扩展2倍。
b = np.array([[1, 2], [3, 4]])
>>> np.tile(b, 2)
array([[1, 2, 1, 2],
       [3, 4, 3, 4]])
>>> np.tile(b, (2, 1))
array([[1, 2],
       [3, 4],
       [1, 2],
       [3, 4]])

 

 

 

Queue BFS 初步学习

发布于 2023.04.11 1分钟阅读 0 评论 5 推荐

    作者:

最近在学习数据结构的时候,学到了树的广度优先搜索可以使用队列来实现,今天尝试着写一下:

定义一棵树:

let tree = {
	"10": {
		value: "10",
		left: "4",
		right: "17",
	},
	"4": {
		value: "4",
		left: "1",
		right: "9",
	},
	"17": {
		value: "17",
		left: "12",
		right: "18",
	},
	"1": {
		value: "1",
		left: null,
		right: null,
	},
	"9": {
		value: "9",
		left: null,
		right: null,
	},
	"12": {
		value: "12",
		left: null,
		right: null,
	},
	"18": {
		value: "18",
		left: null,
		right: null,
	},
};
//定义搜索方法,关键有两点:
// 1、进入队列的时候一定是层序,
// 2、同时访问结束的节点,一定要出队列
const BreadthFirstSearch = (tree, rootNode, searchValue)=>{
  //定义一个队列
  let queue= [];
  queue.push(rootNode);
  while(queue.length>0) {
    //拿出队列的第一个元素赋值给currentNode
    let currentNode = queue[0];
    console.log('currentValue is:',currentNode.value);
    if (currentNode.value === searchValue ) {
      console.log('Found it');
      return;
    }
    //进入队列的顺序一定是层序
    if (currentNode.left !== null) {
      queue.push(tree[currentNode.left]);
    }
    if (currentNode.right !== null) {
      queue.push(tree[currentNode.right]);
    }
    //访问过的元素直接出队列
    queue.shift();
  }
  console.log('sorry not such node');
}
BreadthFirstSearch(tree, tree[10], "42");
BreadthFirstSearch(tree, tree[17], "18");

以前觉得数据结构很难,实际上学习起来的话,抓住关键的点,还是比较容易理解的额

RxJs基础

更新于 2023.04.10 17分钟阅读 0 评论 15 推荐

    作者:

Rxjs基础

我们知道在Rxjs中有几类基础组件,它们分别是:

1、Observables

2、Observers

3、Operators

4、Subjects

5、Schedulers

以上这些组件是Rxjs的核心构建块。我们需要理解它们如何协同工作以便提供强大的功能。一旦对它们有了更高层次的理解,使用起来将会得心应手。那么,让我们一一来看看它们。先来看一个例子:

//import { from } from "rxjs";
let from = require("rxjs").from;
let nums = [1, 2, 4, 34, 56, 789];
let numsObservable$ = from(nums); //Observable
let observer = {
  next: num => console.log(num),
  error: err => console.error(err),
  complete: () => console.log("Execution completed")
};
numsObservable$.subscribe(observer); //Observer subscribed

说明

1、从rxjs中导入必要的函数from

2、定义nums 是一个数字数组,然后将其转换为 Observable。

3、创建了一个名为numObservable$ 的 rxjs Observable,借助它可以将数组转换为Observable。注意在 Observables 末尾附加 $ 是一种命名约定。

4、当我们只创建一个 observable 时,什么都不会被执行或调用。要执行任何操作,您需要订阅 Observable。

5、numsObservable$ 订阅了一个名为observer的对象。注意,这个observer有 3 个属性,next、error 和 complete。一旦订阅,马上执行,因此订阅是执行的触发器

6、执行完成后,您可以在控制台中看到“执行完成”消息。

 

什么是Observable

Observable 是可以随时间到达的事件流或数据流。使用RxJS函数,您可以从几乎任何来源(事件、Web 服务、套接字数据等)创建 Observable。换句话说,Observable是未来数据的集合。

在上面的示例中,我使用 from 运算符从数组创建 Observable。同样,也有 fromEvent 操作符来创建 JavaScript 事件。

Observables 可以是Lazy或Cold。除非调用 subscribe 方法,否则它们不会激活任何producer。

下面的代码展示了如何使用 fromEvent 从点击事件创建一个 Observable。

import { fromEvent } from "rxjs";
let clicksObservable$ = fromEvent(document, "click");

 

什么是Observer

Observable 表示可被观察的数据源,任何对数据源感兴趣可以订阅 Observables。Observer是通过订阅,注册对数据流感兴趣的代码。(因此Observer应该是可执行的,是有行为的,比如成功会怎么样,失败会怎么样,什么时候算结束,结束后还需要执行啥?)

Observer提供了迭代Observable上的数据流(序列)。

import { Observer } from './types';
import { config } from './config';
import { hostReportError } from './util/hostReportError';
export const empty: Observer = {
  closed: true,
  next(value: any): void { /* noop */},
  error(err: any): void {
    if (config.useDeprecatedSynchronousErrorHandling) {
      throw err;
    } else {
      hostReportError(err);
    }
  },
  complete(): void { /*noop*/ }
};

以上是一个empty Observer,基本上,Observer有 3 种方法,next、error 和 complete。理解它们中的每一个很重要。

let observer = {
  next: num => console.log(num),
  error: err => console.error(err),
  complete: () => console.log("Execution completed")
};

next – 接受一个参数(值)。这是 Observable 正在推送/生成的数据。所以每次 observable 推送数据时 next(value) 都会收到该值。

error – 接受一个参数作为错误。如果Observer抛出错误,则执行此方法。

complete - 不接受任何参数。一旦 Observable 完成其操作/执行,此方法就会被执行。

 

什么是Operator

运算符是允许操纵 Observables 产生的值的函数。它们是每个人都喜欢使用 RxJS 的另一个原因。我将使用下面的代码示例向您解释运算符的概念。

import { from } from "rxjs";
import { map } from "rxjs/operators";
const nObservable$ = from([1, 2, 3, 5, 56, 567, 89]);
nObservable$.pipe(
  map(val => val * 2)
).subscribe(
  value => console.log(value)
);

上面的代码将存储在数组中的数字加倍。重要的是要注意 map 函数的使用,您可以将其与 Array.prototype.map() 相关联。正如您所注意到的,map 运算符放在管道方法中。

让我们看看另一个捕获鼠标点击并记录 x 坐标的示例。我将使用 pluck 运算符来执行此操作。

import { fromEvent } from "rxjs";
import { pluck } from "rxjs/operators";
const clicksObservable$ = fromEvent(document, "click");
clicksObservable$
  .pipe(pluck("clientX"))
  .subscribe({
    next: clientX => console.log(clientX),
    error: err => console.error(err),
    complete: () => console.log("Execution completed")
  });

rxjs/operators 中有很多 Operator。可以说,学习Rxjs的使用就是学习这些Operator,但是想要深入理解Rxjs还是需要深入学习基本的概念。

 

什么是Rxjs中的Subject

Subject 将数据推送到多个Observer,Observable一次将数据推送给单个Observer。 Subject 允许您发出新值,这与订阅 Observables 的情况下被动等待数据不同。

Subject用于多播,Observable用于单播。

如果您是 RxJS 的新手,请稍事休息,喝杯茶、咖啡或任何您喜欢的东西,然后查看下面的代码示例以从高层次上理解Subject。

如果您在 GitHub 上查看 Subject 的源代码,可以看到允许我们在subject调用.next,.error,.complete等。

让我们看看下面的例子,我只是向你展示了调用 next(),你也可以在需要时调用 error(err) complete()

import { Subject } from "rxjs";
const mySubject$ = new Subject();
mySubject$.subscribe({
  next : (num) => console.log("ObserverA: " + num)
});
mySubject$.subscribe({
  next : (num) => console.log("ObserverB: " + num)
});
mySubject$.next(5);
mySubject$.next(101);

输出如下:

ObserverA: 5
ObserverB: 5
ObserverA: 101
ObserverB: 101

如上所示,您可以使用 next(value) 向多个Observer发出下一个值。当值的发出完成时,您可以调用 complete() 如下。

mySubject$.complete();

 

什么是Rxjs中的Schedulers

Schedulers是 rxjs 构建块的另一个重要部分。Schedulers控制订阅何时开始以及何时传递通知。有几个开箱即用的Schedulers,如 queueScheduler、asyncScheduler、asapScheduler 等。

在一个简单的项目中,您甚至可能不需要触摸这部分,但是,当您需要对上下文进行精确控制时,它很有用。看下面的例子:

import { Observable, asyncScheduler } from 'rxjs';
import { observeOn } from 'rxjs/operators';
const observable = new Observable((subscriber) => {
  subscriber.next(1);
  subscriber.next(2);
  subscriber.next(3);
  subscriber.complete();
}).pipe(
  observeOn(asyncScheduler)
);
console.log('just before subscribe');
observable.subscribe({
  next(x) {
    console.log('got value ' + x)
  },
  error(err) {
    console.error('something wrong occurred: ' + err);
  },
  complete() {
     console.log('done');
  }
});
console.log('just after subscribe');

输出如下:

just before subscribe
just after subscribe
got value 1
got value 2
got value 3
done

总结:

以上就是对Rxjs中的基本构建块有了大致的理解和分析,记录一下方便以后查阅。

 

AVLTree简单实现

发布于 2023.04.06 17分钟阅读 0 评论 5 推荐

    作者:

今天突然感觉AVL树在Tree这种数据结构中有比较重要的作用,特别是理解如何将不平衡的树转化为平衡树的左旋和右旋操作是理解和学习AVL树的基础。

先上代码:

//定义树的基本节点
const Node = function(item){
	//基本元素
	this.item = item;
 	//节点的高度
 	this.height = 1;
	this.left = null;
	this.right = null;
}
const AVLTree = function(){
	//定义树的根节点
	let root = null;
	this.height = (Node)=>{
		if (Node == null){
			return 0;
 		}
		return Node.height;
	}
	this.rightRotate = (y)=>{
		
		let x = y.left;
		let T2 = x.right;
		x.right = y;
 		y.left = T2;
		y.height = Math.max(this.height(y.left), this.height(y.right))+1;
		x.height = Math.max(this.height(x.left), this.height(x.right))+1;
		return x;
	}
	this.leftRotate = (x)=> {
        let y = x.right;
        let T2 = y.left;
        y.left = x;
        x.right = T2;
        y.height = Math.max(this.height(y.left), this.height(y.right)) + 1;
        x.height = Math.max(this.height(x.left), this.height(x.right)) + 1;
        return y;
    }
 	this.getBalanceFactor = (Node)=>{
    	if (Node == null) {
            return 0;
        }
        return this.height(Node.left) - this.height(Node.right);
    }
	const insertNodeHelper = (node, item)=>{
    	//three case 
    	//根节点为空
    	if (node == null) {
      		return new Node(item);
    	}
    	//搜索需要插入的位置
    	if (item < node.item) {
      	//插入左子树
      		node.left = insertNodeHelper(node.left, item);
    	} else if (item > node.item) {
      		node.right = insertNodeHelper(node.right, item);
    	} else {
      	//这里返回是处理递归的结束
      		return node;
    	}
    	//处理插入的情况
    	node.height = 1 + Math.max(this.height(node.left), this.height(node.right));
    	let balanceFactor  = this.getBalanceFactor(node);
    	if (balanceFactor > 1) {
      		if (item < node.left.item) {//说明是插入左子树的左子树导致不平衡,因此需要右旋
        		return this.rightRotate(node);
      		} else if (item > node.left.item) {//说明是插入左子树的右子树导致不平衡,需要先左旋,再右旋
        		node.left = this.leftRotate(node.left);
        		return this.rightRotate(node);
      		}
    	} 
    	if (balanceFactor < -1) {
      		if (item > node.right.item) {
        		return this.leftRotate(node);
      		} else if (item < node.right.item) {
        		node.right = this.rightRotate(node.right);
       			return this.leftRotate(node);
     		}
    	}

    	return node;
  	}

  	this.insertNode = (item) => {
    	// console.log(root);
    	root = insertNodeHelper(root, item);
  	}

  	const deleteNodeHelper = (root, item)=>{
    	if (root == null){
      		return root;
    	}
    	if (item < root.item) {
      		root.left =  deleteNodeHelper(root.left, item);
    	} else if (item > root.item) {
      		root.right = deleteNodeHelper(root.right, item);
    	} else {
      		//只有一个节点
      		if(root.left == null || root.right == null) {
        		let temp = null;
        		if (temp == root.left) {
          			temp = root.right;
        		} else {
          			temp = root.left;
        		}
        		if (temp == null) {
          		//全部左右子节点为空
          			temp = root;
          			root = null;
          		//根节点为空
        		} else{
          		//根节点赋值为temp子节点
          			root = temp;
        		}
      		} else {
        		// 获取右子树最小的节点
        		let temp  = this.nodeWithMimumValue(root.right);
        		root.item = temp.item;
        		root.right = deleteNodeHelper(root.right, temp.item);
      		}
    	}
    	if (root == null) {
      		return root;
    	}

    	root.height = Math.max(this.height(root.left), this.height(root.right)) + 1;
    	let balanceFactor = this.getBalanceFactor(root);
    	if (balanceFactor > 1) {
     	 	if (this.getBalanceFactor(root.left) >=0) {
        		//右旋
        		return this.rightRotate(root);
      		} else {
        		root.left = this.leftRotate(root.left);
        		return this.rightRotate(root);
      		}
    	} 
    	if (balanceFactor < -1) {
      		if (this.getBalanceFactor(root.right) <= 0) {
        		//左旋
        		return this.leftRotate(root);
      		} else {
        		root.right = this.rightRotate(root.right);
        		return this.leftRotate(root);
      		}
    	}
    	return root;
  	}
 	this.deleteNode = (item) => {
    	// console.log(root);
    	root = deleteNodeHelper(root, item);
  	}

  	this.nodeWithMimumValue = (node) => {
    	let current = node;
    	while (current.left !== null){
      		current = current.left;
    	}
    	return current;
  	}

  	this.preOrder = () => {
    	preOrderHelper(root);
  	}
  
  	const preOrderHelper = (node) => {
    	if (node) {
      		console.log(node.item);
      		preOrderHelper(node.left);
      		preOrderHelper(node.right);
    	}
  	}
}

let tree = new AVLTree();
tree.insertNode(33);
tree.insertNode(13);
tree.insertNode(53);
tree.insertNode(9);
tree.insertNode(21);
tree.insertNode(61);
tree.insertNode(8);
tree.insertNode(11);
tree.preOrder();
tree.deleteNode(13);
console.log("After Deletion: ");
tree.preOrder();

平衡二叉树的代码主要包括插入和删除两个基本操作,而这两个基本操作最关键的是理解左旋和右旋。关于左旋和右旋的知识,我们以后再慢慢分享。

理解CKEditor5的schema

更新于 2023.03.13 3分钟阅读 0 评论 5 推荐

    作者:

我们知道,CKEditor5是一个用MVC架构设计的富文本编辑器。

如上图所示,三层分别是:Model,Controller, View

首先,第一个问题是schema属于那一层?

经过官方文档的初步学习,我们可以看到:

editor.model.schema;                // -> The model's schema.

因此,我们可以得出结论:schema属于模型层:

其次我们需要理解的是schema的作用是什么?

  • Where a node is allowed or disallowed (e.g. paragraph is allowed in $root, but not in heading1).
  • What attributes are allowed for a certain node (e.g. image can have the src and alt attributes).
  • Additional semantics of model nodes (e.g. image is of the “object” type and paragraph of the “block” type).

官网上是这样介绍的,我自己的理解就是:

  • 模型节点可放位置或不可放置位置。比如模型元素paragraph可放置$root下,却不可放置heading1下。
  • 指定模型节点允许属性和禁止属性。比如模型元素image允许属性src和alt,禁止其他属性。
  • 模型节点附加语意。比如模型节点image是一个对象类型(整体对待)而p是一个块类型(可包含元素,且元素可以分割)。

再次我们思考一下schema的信息有啥作用:

  • What happens with the pasted content and what is filtered out (note: in case of pasting the other important mechanism is the conversion. HTML elements and attributes which are not upcasted by any of the registered converters are filtered out before they even become model nodes, so the schema is not applied to them; the conversion will be covered later in this guide).
  • To which elements the heading feature can be applied (which blocks can be turned to headings and which elements are blocks in the first place).
  • Which elements can be wrapped with a block quote.
  • Whether the bold button is enabled when the selection is in a heading (and whether the text in this heading can be bolded).
  • Where the selection can be placed (which is — only in text nodes and on object elements).
  • etc.

最后,我们需要理解的是schema的定义包括哪些具体内容:

这里可以参考:SchemaItemDefinition,这里说说常用的属性

  1. allowIn : String | Array<String> 定义的模型节点可以作为哪些模型节点的子节点。白话来说就是可以放在哪些节点下面。
     
  2. allowWhere :  String | Array<String> 定义的模型节点从目标模型节点继承allowIn属性。通俗解释就是目标模型节点可以放置的位置,我们定义的模型节点就可以放置。
     
  3. allowAttributes : String | Array<String> 定义的模型节点允许包含哪些属性。通俗理解就是模型节点允许哪些属性。
     
  4. allowAttributesOf: String | Array<String> 定义的模型节点从目标模型节点继承allowAttributes属性。 通俗解释就是目标模型节点允许哪些属性,我们定义的模型节点就允许哪些属性。
     
  5. allowChildren: String | Array<String> 定义的模型节点可以包含哪些模型子节点。通俗理解就是模节点内部可以放置哪些模型子节点。
     
  6. allowContentOf : String | Array<String> 定义的模型节点从目标模型节点继承allowChildren属性。通俗理解就是目标模型节点可以包含哪些子模型节点,我们定义的模型节点就可以包含那些子模型节点。
     
  7. isLimit :  设置为 true 时,元素内的所有操作只会修改内容,不会影响元素本身。也就是说该元素无法使用回车键拆分,无法在元素内部使用删除键删除该元素(如果把整个 Molde 理解为一张网页,Limit Element 就相当于 iframe);
     
  8. isObject :  是否为一个完整对象(完整对象会被整体选中,无法使用回车拆分,无法直接编辑文本);
     
  9. isBlock : 是否为块元素,类似 HTML 中的块元素;
     
  10. isInline : 是否为行内元素。但对于 <a> <strong> 这些需要即时编辑的行内标签,在编辑器中以文本属性来区分,所以 isInline 只用于独立的元素,即 isObject 应设置为 true;
schema.register( 'newCodeBlock', {
         allowWhere: '$block',
         allowChildren: '$text',
         isBlock: true,
         allowAttributes: [ 'language' ]
 } );

用上面的代码作为例子,我们可以得出,schema:newCodeBlock可以放置在任何$block可以放置的地方,因为它继承了$block,newCodeBlock里面只能放置文本,而且是一个块元素,允许有属性language

 

总结

model的schema的作用就是:

定义允许的模型结构:——模型元素如何嵌套

定义允许的属性:——元素和文本节点可能允许和不允许的属性

其他特征:——内联还是块,对外部行为 的原子性反应等。

kubernetes服务的版本回退

更新于 2023.03.13 17分钟阅读 0 评论 4 推荐

    作者:

下面简单说一下kubernetes的版本回退,因为已经在线上使用挺久的时间了,是利用kubernetes deployment的rollout histrory回退到指定版本。
 

先给deployment打个样,webservice服务配置如下:

kind: Deployment
apiVersion: apps/v1beta2
metadata:
  name: pre-webservice
  labels:
    k8s-app: pre-webservice
  annotations:
    kubernetes.io/change-cause: pre-201806291944-216d6afd
spec:
  replicas: 1
  revisionHistoryLimit: 5
  minReadySeconds: 10
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
  selector:
    matchLabels:
      k8s-app: pre-webservice
  template:
    metadata:
      labels:
        k8s-app: pre-webservice
    spec:
      containers:
      - name: pre-webservice
        image: harbor.bbotte.com/service/webservice:pre-201806291944-216d6afd
        imagePullPolicy: IfNotPresent
        resources:
          limits:
            memory: 2048Mi
        ports:
        - containerPort: 9080
        livenessProbe:
          tcpSocket:
            port: 9080
          initialDelaySeconds: 15
          periodSeconds: 20
        volumeMounts:
          - name: pre-webservice-data-storage
            mountPath: /opt
      imagePullSecrets:
        - name: harbor-auth
      volumes:
      - name: pre-webservice-data-storage
        persistentVolumeClaim:
          claimName: gfs
 
---
kind: Service
apiVersion: v1
metadata:
  name: pre-webservice-com
  labels:
    k8s-app: pre-webservice
spec:
  selector:
    k8s-app: pre-webservice
  ports:
  - port: 9080
    targetPort: 9080

revisionHistoryLimit控制保留几个版本信息。版本回退即把服务回滚回去,镜像的版本号为: gitlab分支-时间戳-git_commit_short

kubernetes.io/change-cause 信息为docker的版本号,以免不知道kubernetes发布的版本和镜像之间的关系,即images 最后面一段,这个是有模板创建的 yaml 配置文件,

部署的命令,需要加 –record=false

kubectl apply -f webservice.yaml --record=false

查看已发布的版本信息:

kubectl rollout history deploy/pre-webservice
deployments "pre-webservice"
REVISION  CHANGE-CAUSE
55        pre-201806281524-57a710e1
56        pre-201806281555-403c38aa
57        pre-201806281601-166ffb31
58        pre-201806291944-216d6afd

前面是k8s保留的版本号,后面是docker-image的版本号

回滚到指定版本,因为版本号是以gitlab分支-时间戳-git_commit_short命名,所以可以依据gitlab提交的版本或者时间来回退

kubectl rollout undo daemonset/webservice --to-revision=56

这样就回退到56的版本

Go语言:标识符命名最佳实践

更新于 2023.02.28 1分钟阅读 0 评论 5 推荐

    作者:

在 Go 语言中,标识符的命名非常重要,它们使你的代码易于阅读、易于理解和易于维护。

编写Go代码时,遵循以下标识符命名的最佳实践:

1、使用有意义的名称

使用能够准确反映其作用的名称,让代码易于理解和维护。

func calculateArea(width float64, height float64) float64 {
  return width * height
}

2、避免使用缩写

除非使用缩写比使用全称更易于理解,否则应该避免使用缩写。

func convertMillisecondsToSeconds(milliseconds int64) float64 {
  return float64(milliseconds) / 1000
}

3、使用驼峰式命名

Go语言中建议使用驼峰式命名(CamelCase),即首字母小写,后续单词首字母大写。

var backgroundColor string
var userName string

4、对于公共变量或函数,使用首字母大写的驼峰式命名

这样可以使其在包外可见,符合Go语言的导出规则。

type Rectangle struct {
  Width  float64
  Height float64
}

func NewRectangle(width float64, height float64) *Rectangle {
  return &Rectangle{Width: width, Height: height}
}

5、对于私有变量或函数,使用首字母小写的驼峰式命名

这样可以使其在包外不可见,符合Go语言的封装规则。

type Rectangle struct {
  Width  float64
  Height float64
}

func NewRectangle(width float64, height float64) *Rectangle {
  return &Rectangle{Width: width, Height: height}
}

6、避免使用下划线

Go语言中通常不使用下划线来分隔单词,除非是用于声明未使用的变量。

var fullName string
var firstName string

7、避免使用关键字

Go语言中有一些关键字(如if、for、func等),不能用作标识符。

 

Go语言获取对象的类型

更新于 2023.02.28 1分钟阅读 0 评论 5 推荐

    作者:

Go reflect包含有检查变量类型的方法,示例代码如下:

package main

import (
    "fmt"
    "reflect"
)

func main() {

    v1:= "string"
    v2 := 100
    v3 := 11.1

    fmt.Println(reflect.TypeOf(v1))
    fmt.Println(reflect.TypeOf(v2 ))
    fmt.Println(reflect.TypeOf(v2 ))
}

依次输出如下:

string
int
float64

参考文档:http://golang.org/pkg/reflect/#Type

Gradle手动安装说明

更新于 2023.02.28 0分钟阅读 0 评论 5 推荐

    作者:

环境要求

Gradle依赖于JDK8或以上版本。在安装之前,先执行java -version检查JDK版本:

>java -version
java version "11.0.13" 2021-10-19 LTS
Java(TM) SE Runtime Environment 18.9 (build 11.0.13+10-LTS-370)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.13+10-LTS-370, mixed mode)

安装过程

手动安装Gradle,需要下载安装包。

一、下载最新安装包

Gradle下载地址:https://gradle.org/releases

Gradle提供了两种的安装下载:

  • 二进制版
  • 完整版,完整版包含了文档和源码。你可以根据自己的需要选择下载。

这里以gradle-7.3.3-bin.zip为例做说明。

二、解压压缩包

Windows

创建目录C:\Gradle,接着把gradle-7.3.3-bin.zip解压到C:\Gradle

Linux和MacOS

选在目标目录,解压缩发行版 zip 文件,这里选择/opt/gradle:

$ mkdir /opt/gradle
$ unzip -d /opt/gradle gradle-7.3.3-bin.zip
$ ls /opt/gradle/gradle-7.3.3
LICENSE  NOTICE  bin  getting-started.html  init.d  lib  media

三、配置环境变量

Windows 10

找到编辑环境变量的配置

  • 方法一:在左下角搜索栏输入“环境变量”,选择“编辑系统环境变量”。
  • 方法二:在文件资源管理器中右键单击此电脑(或计算机)图标,然后单击属性 -> 高级系统设置 -> 环境变量。

在系统变量下选择路径,然后单击编辑。为 C:\Gradle\gradle-7.3.3\bin 添加一个条目。点击确定保存:

Linux和MacOS

把Gradle解压后的bin目录配置到PATH 环境变量:

$ export PATH=$PATH:/opt/gradle/gradle-7.3.3/bin

四、验证安装

打开控制台(或 Windows 命令提示符)并运行 gradle -v :

>gradle -v

Welcome to Gradle 7.3.3!

Here are the highlights of this release:
 - Easily declare new test suites in Java projects
 - Support for Java 17
 - Support for Scala 3

For more details see https://docs.gradle.org/7.3.3/release-notes.html


------------------------------------------------------------
Gradle 7.3.3
------------------------------------------------------------

Build time:   2021-12-22 12:37:54 UTC
Revision:     6f556c80f945dc54b50e0be633da6c62dbe8dc71

Kotlin:       1.5.31
Groovy:       3.0.9
Ant:          Apache Ant(TM) version 1.10.11 compiled on July 10 2021
JVM:          11.0.13 (Oracle Corporation 11.0.13+10-LTS-370)
OS:           Windows 10 10.0 amd64

Git查看,修改用户名和邮箱(local和global)

发布于 2023.02.27 0分钟阅读 0 评论 5 推荐

    作者:

git config命令查看用户名和邮箱

#查看用户名
git config user.name
#查看邮箱
git config user.email

git config命令设置用户名和邮箱

1. --local对当前仓库设置(仅对当前仓库有效)

#设置用户名
git config --local user.name '用户名'
#设置邮箱
git config --local user.email '用户邮箱'

2. --global对全局仓库设置

#设置用户名
git config --global user.name '用户名'
git config --global user.email '用户邮箱'

 

 

 

JPA批量更新多个id的实体对象

发布于 2023.02.26 5分钟阅读 0 评论 5 推荐

    作者:

在 Spring Data JPA 中,可以使用 @Query 注解和 HQL 或者 SQL 语句来执行自定义的更新操作。如果要根据多个 ID 更新实体对象,可以使用 IN 关键字。

基于HQL的示例

以下是一个示例,假设我们有一个名为 User 的实体类,其中包含 id 和 name 属性。现在我们想要根据多个 ID 更新这些用户的姓名:


@Repository
public interface UserRepository extends JpaRepository<User, Long> {
   @Transactional
   @Modifying
   @Query("UPDATE User u SET u.name = :name WHERE u.id IN (:ids)")
   int updateUsersNameByIds(@Param("name") String name, @Param("ids") List<Long> ids);
}

示例中定义的更新方法 updateUsersNameByIds(),使用 @Query 注解设置更新操作的 HQL 语句,其中 :name:ids 是参数占位符。@Param 注解用于指定参数名称。

在 HQL 语句中,使用使用了 IN 关键字并传入了一个 List<Long> 类型的参数 ids,表示要更新的用户 ID 列表。

最后,使用 @Modifying@Transactional 注解分别标记该方法为更新操作和事务性操作。

使用该方法时,只需要传入要更新的用户姓名和 ID 列表即可:

基于Native方式SQL

将上面的示例改写为native的sql方式,可以在 @Query 注解中设置 nativeQuery=true 属性。
 

改后的示例:

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
   @Transactional
   @Modifying
   @Query(value = "UPDATE user SET name = :name WHERE id IN (:ids)", nativeQuery = true)
   int updateUsersNameByIds(@Param("name") String name, @Param("ids") List<Long> ids);
}

上面的代码中,我们使用了 value 属性指定了 Native SQL 语句,其中 :name :ids 是参数占位符,与 HQL 语句类似。同时设置了 nativeQuery = true 表示该语句是原生的 SQL 语句。

原生的SQL,UPDATE语句对应的是数据库表中的表名user,和字段名name,id;而在HQL中,对应的是实体名User和实体属性name,id。

 

 

Injector框架理解(二)

更新于 2023.01.11 7分钟阅读 0 评论 5 推荐

    作者:

今天我们继续学习didi这个依赖注入框架,上一节我们知道了怎么定义组件,怎么定义模块,怎么通过Injector这个类来载入模块和获取组件。

今天我们来学习如何重用模块:

我们首先定义好组件和模块:

const {Injector} = require('didi');
class FooType {
    constructor() {
      this.name = 'foo';
    }
}

function barFactory(foo) {
    return foo;
}

const module1 = ({
    foo: [ 'type', [ FooType ] ],
    bar: [ 'factory', [ 'foo', barFactory ] ],
});
const injector1 = new Injector([module1]);
console.log('injector1:',injector1);
console.log(injector1.get('foo')===injector1.get('bar'))

const injector2 = new Injector([module1]);
console.log('injector2:',injector2);
console.log(injector2.get('foo')===injector2.get('bar'))

console.log(injector1.get('foo')===injector2.get('foo'))

说明一下,这里的代码,首先我们定义了两个组件,一个是FooType,另一个是工厂方法barFactory返回的foo组件,其实看模块的定义实际上就是FooType的具体实例。然后我们将这两个组件定义为模块,这个模块的定义是可以重用的,用不同的依赖注入加载器加载后,它们的实例存在于不同的加载器中,虽然实例不同,但是它们的作用是相同的。

我们先看看运行的效果:

可以看出,这两个加载器中的实例是一模一样的。因此,定义好了模块以后是可以重用的。这里也存在一个问题,那就是组件的定义:

模块中组件的定义是这样的:

 const preModule =({
    foo: [
      'factory',
      function() {
        return 'foo-value';
      }
    ],
    bar: ['value', 'bar-value'],
    bub: ['type', BubType],
    buz: ['type', BuzType]
});
const curModule = ({
    foo: [ 'type', [ FooType ] ],
    bar: [ 'factory', [ 'foo', barFactory ] ],
});
//注意对于factory类型的组件,存在两种不同的定义方法,它们的第二个参数可以是一个函数
//也可以是一个数组,从后面可以看出,这是一个配置有依赖组件的数组
//同时,对于type类型的组件,第二个参数可以是一个类,也可以是一个数组,而数组中的值是一个类
//以上这样设计的原因,我们会在后文进行分析

依赖注入的方式,数组定义: 

const {Injector} = require('didi');
class FooType {
    constructor() {
      this.name = 'foo';
    }
}

function barFactory(foo) {
    return foo;
}

const module1 = ({
    foo: [ 'type', [ FooType ] ],
    bar: [ 'factory', [ 'foo', barFactory ] ],
});
const injector3 = new Injector([ module1 ]);
function fn(foo, bar) {
    console.log('foo0:',foo);
    console.log('foo1:',injector3.get('foo'));
    console.log('bar0:',bar);
    console.log('bar1:',injector3.get('bar'));
}
const annotatedFn = [ 'foo', 'bar', fn ];

injector3.invoke(annotatedFn);

我们看运行结果:

可以知道,依赖注入的配置可以是一个数组,而数组的最后一个就是组件函数,前面的所有参数就是这个函数组件依赖的组件,比如这里的fn组件依赖于foo和bar组件可以这样配置。

const annotatedFn = [ 'foo', 'bar', fn ];

好了,今天我们主要学习并理解了didi框架的模块重用以及数组形式的组件依赖注入配置

 

Injector框架理解(一)

更新于 2023.01.12 0分钟阅读 0 评论 5 推荐

    作者:

在可测试js的框架学习过程中,我们不断提到了依赖注入,依赖注入使用最多的就是在java中的spring,为了在js中使用依赖注入,我们今天学习另一个不那么常用的框架:didi

首先看看安装命令

npm install didi

这个时候,我看到项目下的package.json

{
  "name": "babelwebpack",
  "version": "1.0.0",
  "description": "babel webpack ingegraty",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "pack": "webpack",
    "publish": "webpack-dev-server --output-public-path=/dev/"
  },
  "keywords": [
    "babel",
    "webpack"
  ],
  "author": "hk",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.18.10",
    "@babel/core": "^7.18.13",
    "@babel/preset-env": "^7.18.10",
    "webpack": "^5.74.0",
    "webpack-cli": "^4.10.0",
    "webpack-dev-server": "^4.10.1"
  },
  "dependencies": {
    "babel-loader": "^8.2.5",
    "didi": "^9.0.0"
  }
}

我再贴出我的webpack.config.js

var path = require('path');

module.exports = {
   entry: {
      /** 源代码根路径 */
      app: './src/main.js'
   },
   output: {
      /** 这里配置的是webpack的打包输出路径 */
      path: path.resolve(__dirname, 'dev'),
      filename: 'main_bundle.js'
   },
   mode:'development',
   module: {
      rules: [
         {
            test: /\.js$/,
            include: path.resolve(__dirname, 'src'),
            loader: 'babel-loader',
            options: {
               presets: ['@babel/preset-env']
            }
         }
      ]
   },
   devServer: {
      static: {
         /**这里配置的开发服务器的根路径 */
         directory: path.join(__dirname, './public'),
      },
      hot: true,
      compress: true,
      port: 9999,
   },
};

好了,我要开始使用didi

首先我定义三个组件:

const {Injector} =require('didi');
/*
 * 定义一个Car组件,这个Car组件依赖于engine组件
 */
function Car(engine) {
    this.start = function() {
      engine.start();
    };
}
 
/**
 * 定义一个Engine组件,同时这个Engine组件依赖于power组件
 */ 
function createPetrolEngine(power) {
    return {
      start: function() {
        console.log('Starting engine with ' + power + 'hp');
      }
    };
}
  
// define a (didi) Car module,定义一个CarModule,这个模块由三个组件构建
// it declares available components by name and specifies how these are provided
const carModule = {
    //请求Car组件的时候,依赖注入器会调用Car的构造函数来创建组件
    // asked for 'car', the injector will call new Car(...) to produce it
    'car': ['type', Car],
	//请求Engine组件的时候,依赖注入器会调用工厂方法来创建组件
    // asked for 'engine', the injector will call createPetrolEngine(...) to produce it
    'engine': ['factory', createPetrolEngine],
	//请求power组件,依赖注入器会通过复制一个value值来创建。
    // asked for 'power', the injector will give it number 1184
    'power': ['value', 1184] // probably Bugatti Veyron
};
  
// instantiate an injector with a set of (didi) modules
const injector = new Injector([
    carModule
]);

//使用依赖注入器来检索组件并调用组件的API 
// use the injector API to retrieve components
injector.get('car').start();

//使用依赖注入器的invoke函数来注入相应的组件 
// ...or invoke a function, injecting the arguments
injector.invoke(function(car) {
    console.log('started', car);
    car.start();
});

大家注意以上代码,我们需要指出几个概念,首先理解什么是组件,什么是模块,什么是依赖注入加载器

组件可以是一个类,一个函数,甚至是一个值,在以上的代码中,有一个叫做Car的组件,一个叫做Engine的组件,以及一个叫做power的组件,而这三个组件又构成了一个模块,叫做carModule,这个模块通过依赖注入加载器加载以后,我就可以使用这些组件啦。

使用组件的方式有两种,第一种通过调用get()方法拿到组件以后,再使用。

另一种是通过调用invoke方法,通过一个函数来获取依赖注入参数,然后再使用。大家可以运行代码试试效果,不过呢,这里有个小问题就是,只有在编辑状态的时候才能运行代码,而且只支持noderequire引入,不知道这个部分以后是否可以优化。

 

我们再举一个例子

const {Injector} =require('didi');
class BubType{
    constructor() {
      this.name = 'bub-value';
    }
}

function BuzType() {
    this.name = 'buz-value';
}

/**
  * 在一个模块定义了四个组件
  */
const newInjector = new Injector([
    {
      foo: [
        'factory',
        function() {
          return 'foo-value';
        }
      ],
      bar: ['value', 'bar-value'],
      bub: ['type', BubType],
      buz: ['type', BuzType]
    }
]);

console.log('foo:',newInjector.get('foo'));
console.log('bar:',newInjector.get('bar'));
console.log('bub:',newInjector.get('bub'));
console.log('buz:',newInjector.get('buz'));

在这里,我们可以得出一个小小的结论就是:Injector的构造函数是一个数组对象,数组中的每个对象实际上是会生成一个模块,同时产生组件的方式有三类:分别对应三种不同的key值,它们是factory,type,value

这里我们大概理解了基本的用法了吧,其实这个简单的框架为我们组织前端的代码提供了很好的组织方式,能够让我们的代码更加模块化。欢迎大家一起讨论学习。

webpack初步理解

更新于 2023.01.06 73分钟阅读 0 评论 5 推荐

    作者:

Webpack 是一个非常强大且有趣的工具,它被视为当今许多 Web 开发人员用来构建其应用程序的基础组件。然而,许多人会认为使用它是一个相当大的挑战,主要是因为它的复杂性

 

webpack bundle过程图表

modules

模块是一个文件的升级版本。一个模块,一旦创建并且构建之后,除了包含原始的文件代码之外,还有一些其他有意义的信息:比如模块使用的加载器,模块的依赖项,模块的导出(如果存在的话)以及模块的hash值等等。

The  entry  object

Entry对象(也可以称为入口对象)记住一点:enrty对象中的每一项都是模块树中的根模块。模块树,因为根模块可能需要一些其他模块(又称为依赖项),这些模块(依赖项也是模块)可能也需要其他模块等等,因此您可以在更高的级别上理解如何构建这棵模块树。所有这些模块树都存储联结在一个 ModuleGraph(模块图) 中。

另外一点我们需要知道的是:webpack 是建立在许多插件之上的。尽管bundle过程已经构建起来,但是可以嵌入很多方法来添加自定义逻辑。webpack的扩展通过hooks来实现。比如,你可以在模块图已经构建之后,或者为chunk生成新的assets 时或者模块将要构建前(加载器运行时、解析源代码时),添加一些自定义逻辑等等。hooks非常有趣,可以为许多与 webpack 定制相关的问题提供解决方案。大多数时候,hooks是根据它们的功能分组的,每一个插件都有明确定义的功能。例如,有一个插件负责处理 import() 函数(负责解析注释和参数)它被称为 ImportParserPlugin,它所做的只是在 AST 解析期间遇到 import() 调用时添加一个hook。

同时不足为奇的是,有几个负责处理entry对象的插件。有一个 EntryOptionPlugin,它实际上接受entry对象为参数并为入口对象中的每个项目(entry item)创建一个 EntryPlugin。这部分很重要:入口对象的每一项都将产生一棵模块树(所有这些模块树都是彼此分离的)。基本上,EntryPlugin 开启每一棵模块树的创建过程,每个模块树都会将信息添加到同一个地方,即 ModuleGraph。因此,我们会说 EntryPlugin 开启了这个复杂的处理过程。

入口对象插件处理

结合初始图片来看,我们可以知道,EntryPlugin 也是创建 EntryDependency 的地方。基于上图,让我们通过自己实现EntryOptionsPlugin来进一步了解EntryOptionsPlugin的重要性:

class CustomEntryOptionPlugin {
  // This is the standard way of creating plugins.
  // It's either this, or a simple function, but we're using this approach
  // in order to be on par with how most of the plugins are created.
  apply(compiler) {
    // Recall that hooks offer us the possibility to intervene in the
    // bundling process.
    // With the help of the `entryOption` hook, we're adding the logic
    // that will basically mean the start of the bundling process. As in,
    // the `entryObject` argument will hold the `entry` object from the
    // configuration file and we'll be using it to set up the creation of
    // module trees.
    compiler.hooks.entryOption.tap('CustomEntryOptionPlugin', entryObject => {
      // The `EntryOption` class will handle the creation of a module tree.
      const EntryOption = class {
        constructor (options) {
          this.options = options;
        };

        // Since this is still a plugin, we're abiding by the standard.
        apply(compiler) {
          // The `start` hook marks the start of the bundling process.
          // It will be called **after** `hooks.entryOption` is called.
          compiler.hooks.start('EntryOption', ({ createModuleTree }) => {
            // Creating new tree of modules, based on the configuration of this plugin.
            // The `options` contain the name of the entry(which essentially is the name of the chunk)
            // and the file name.
            // The `EntryDependency` encapsulates these options and also provides way to
            // create modules(because it maps to a `NormalModuleFactory`, which produces `NormalModule`s).
            // After calling `createModuleTree`, the source code of the file will be found,
            // then a module instance will be created and then webpack will get its AST, which 
            // will be further used in the bundling process.
            createModuleTree(new EntryDependency(this.options));
          });
        };
      };

      // For each item in the `entryObject` we're preparing
      // the creation of a module tree. Remember that each
      // module tree is independent of others.
			// The `entryObject` could be something like this: `{ a: './a.js' }`
      for (const name in entryObject) {
        const fileName = entryObject[name];
        // We're fundamentally saying: `ok webpack, when the bundling process starts,
        // be ready to create a module tree for this entry`.
        new EntryOption({ name, fileName }).apply(compiler);
      };
    });
  }
};

在本节的最后一部分,我们将稍微扩展介绍什么是依赖(Dependency),因为我们将在本文中进一步使用它。您现在可能想知道 EntryDependency 是什么以及为什么需要它。从我的角度来看,当创建新模块时,它可以归结为一种智能抽象。简单地说,依赖就是实际模块实例的准备阶段(初级阶段)。例如,甚至entry对象的项目从webpack 的视角来看也是依赖项,它们指明要创建模块实例的最低限度要求:它的路径(例如 ./a.js、./b.js)。没有依赖项就无法创建模块,因为依赖项包含模块的请求以及其他重要信息,即可以找到模块的源文件路径(例如'./a.js')。依赖项还指示如何构造该模块,它怎样使用一个模块工厂(module factory)来完成模块构建。模块工厂知道如何从原始状态(字符串源代码)转化到一些可由webpack使用的具体实体。EntryDependency 实际上是 ModuleDependency 的一种类型,这意味着它肯定会保持模块的请求,并且它指向的模块工厂是 NormalModuleFactory。然后,NormalModuleFactory 确切地知道要做什么才能从一条路径创建对 webpack 有意义的东西。另一种思考方式是,一个模块起初只是一个简单的路径(在入口对象中或导入语句的一部分中),然后它成为一个依赖项,最后成为一个模块。 这是一种可视化的方法:

因此,在创建模块树的根模块时,最开始会使用EntryDependency。

对于其他的模块,它们是其他类型的依赖项。例如,如果您使用import语句,如 import defaultFn from './a.js' ,那么将有一个HarmonyImportSideEffectDependency保存模块的请求(在本例中为 './a.js')并映射到NormalModuleFactory。因此,文件“a.js”将会是一个新模块,希望现在您可以理解依赖项所起的重要作用。它们本质上是指导 webpack 如何创建模块。我们将在本文后面揭示有关依赖项的更多信息。

快速回顾一下我们在本节中学到的内容:

1、对于 entry 对象中的每一项,都会有一个 EntryPlugin 实例,在该实例中创建了一个 EntryDependency。这个 EntryDependency 保存模块的请求(即文件的路径),并且还提供了一种通过映射到模块工厂(即 NormalModuleFactory)来充分利用该请求。模块工厂知道如何仅从文件路径创建对 webpack 有用的实体。

2、再次,依赖关系对于创建模块至关重要,因为它包含重要信息,例如模块的请求以及如何处理该请求。有几种类型的依赖关系,并非所有类型的依赖都对创建新模块有用。从每个 EntryPlugin 实例出发并在新创建的 EntryDependency 的帮助下,将创建一个模块树。模块树建立在模块及其依赖关系之上,这些依赖项也是模块,也可以有依赖关系。

现在,让我们通过了解有关 ModuleGraph 的更多信息来继续我们的学习之旅。

 

理解 ModuleGraph

ModuleGraph 是一种跟踪已经构建好的模块的方法。它特别依靠依赖关系,因为它们提供了连接 2 个不同模块的方法。例如:

// a.js
import defaultBFn from './b.js';
// b.js
export default function () { console.log('Hello from B!'); }

这里我们有 2 个文件,所以有 2 个模块。文件 a 需要文件 b 中的某些内容,因此在 a 中存在由 import 语句建立的依赖关系。就 ModuleGraph 而言,依赖项定义了一种连接 2 个模块的方式。甚至上一节中的 EntryDependency 也连接了 2 个模块:图的根模块,我们将其称为空模块,以及与入口文件关联的模块。上面的代码片段可以可视化如下:

 

阐明简单模块(即 NormalModule 实例)和属于 ModuleGraph 的模块之间的区别很重要。ModuleGraph 的节点称为 ModuleGraphModule(这里是不是应该称呼:模块图模块),它只是一个修饰的 NormalModule 实例。ModuleGraph 借助具有以下签名的映射跟踪这些装饰模块:

 Map<Module, ModuleGraphModule>

这些方面是有必要提及的,因为如果只有 NormalModule 实例,那么您对它们无能为力,它们不知道如何相互通信。ModuleGraph 赋予这些裸模块意义,通过在上述映射的帮助下互连它们,该映射为每个 NormalModule 分配一个 ModuleGraphModule。这将在构建 ModuleGraph 部分很有意义,我们将使用 ModuleGraph 及其内部映射来遍历模块图。。我们将属于 ModuleGraph 的模块简称为模块,因为区别仅包含几个附加属性。

对于属于 ModuleGraph 的节点,是有少有的几个定义好的东西:传入连接和传出连接。Connection是 ModuleGraph 的另一个小实体,它包含有意义的信息,例如:源模块、目标模块和连接前面提到的 2 个模块的依赖项。具体来说,根据上图,新建了一个连接:

// This is based on the diagram and the snippet from above.
Connection: {
	originModule: A,
	destinationModule: B,
	dependency: ImportDependency
}

并且上面的连接将被添加到 A.outgoingConnections 集合和 B.incomingConnections 集合中。

这些是 ModuleGraph 的基本概念。正如上一节中已经提到的,从entry创建的所有模块树都会将有意义的信息输出到同一个地方,即 ModuleGraph。这是因为所有这些模块树最终都会与空模块(ModuleGraph 的根模块)相连。通过 EntryDependency 和从入口文件创建的模块建立与空模块的连接。这是我对 ModuleGraph 的看法:

如您所见,空模块与从entry对象中的项目生成的每个模块树的根模块都有一个连接。图中的每条边代表 2 个模块之间的连接,每个连接都包含有关源节点、目标节点和依赖项的信息(这非正式地回答了为什么这 2 个模块连接的问题?)。

现在我们对 ModuleGraph 有点熟悉了,让我们看看它是如何构建的。

构建 ModuleGraph

正如我们在上一节中看到的,ModuleGraph 以一个空模块开始,其直接后代是模块树的根模块,这些模块树是从entry对象项构建的。因此,为了了解 ModuleGraph 是如何构建的,我们将研究单个模块树的构建过程。

第一个模块的创建

我们将从一个非常简单的entry对象开始:

entry: {
	a: './a.js',
}

根据第一部分所说的,在某些时候,我们最终会得到一个请求为“./a.js”的EntryDependency。这个 EntryDependency 提供了一种从该请求创建有意义的东西的方法,因为它映射到一个模块工厂,即 NormalModuleFactory。这是我们在第一部分中没提到的地方。

该过程的下一步是 NormalModuleFactory 起作用的地方。NormalModuleFactory,如果它成功完成它的任务,将创建一个 NormalModule为了确保没有不确定性,NormalModule 只是文件源代码的反序列化版本,它只不过是一个原始字符串。原始字符串不会带来太多价值,因此 webpack 不能用它做太多事情。NormalModule 还将源代码存储为字符串,但同时,它还将包含其他有意义的信息和功能,例如:应用到它的加载器,构建模块的逻辑,生成运行时代码的逻辑,它的哈希值等等。换句话说,从 webpack 的角度来看,NormalModule 是一个简单原始文件的有用版本

为了让 NormalModuleFactory 输出一个 NormalModule,它必须经过一些步骤。创建模块后还有一些事情要做,例如构建模块并处理其依赖项(如果有的话)。

这又是我们一直在关注的图表,现在专注于构建 ModuleGraph 部分:

NormalModuleFactory 通过调用它的 create 方法开始它的魔力。然后,process过程开始。这里是请求(文件的路径)被解析的地方,以及该类型文件的加载器。请注意,在此步骤中,将仅确定加载程序的文件路径,尚未调用加载程序。

模块构建处理

解析完所有必要的文件路径后,NormalModule创建完成。但是,在这一点上,该模块不是很有价值。构建模块后会出现很多相关信息。 NormalModule 的构建过程包括以下几个步骤:

  • 首先,将在原始源代码上调用加载器;如果有多个加载器,那么一个加载器的输出可能是另一个加载器的输入(在配置文件中提供加载器的顺序很重要);
  • 其次,通过所有加载器运行后的结果字符串将用 acorn(一个 JavaScript 解析器)解析,从而产生给定文件的 AST;
  • 最后分析AST;分析是必要的,因为在这个阶段会确定当前模块的依赖关系(例如其他模块),webpack 可以检测到它的神奇功能(例如 require.context、module.hot)等;AST 分析发生在 JavascriptParser 中,如果您单击链接,您应该会看到那里处理了很多案例;这部分过程是最重要的部分,因为捆绑过程中接下来的很多事情都取决于这部分;

通过生成的 AST 发现依赖关系

一种思考处理过程的方法,无需过多详细介绍,如下所示:

其中 moduleInstance 是指从 index.js 文件创建的 NormalModule。红色的 dep 指的是从第一个 import 语句创建的依赖项,蓝色的 dep 指的是第二个 import 语句。这只是查看事物的一种简化方式。实际上,如前所述,依赖项是在获得 AST 之后添加的。

现在已经检查了 AST,是时候继续构建我们在本节开头谈到的模块树的过程了。下一步是处理在上一步中找到的依赖项。如果我们按照上图,index 模块有两个依赖,它们也是模块,即 math.js 和 utils.js。但在依赖项成为实际模块之前,我们只有index模块,其 module.dependencies 有 2 个值,其中包含模块请求(文件路径)、导入说明符(例如 sum、greet)等信息。为了将它们变成模块,我们需要使用这些依赖关系映射到的 ModuleFactory 并重复上述相同的步骤(重复在本节开头的图中用虚线箭头表示)。在处理完当前模块的依赖关系之后,这些依赖关系可能也有依赖关系,并且这个过程一直持续到没有更多的依赖关系为止。这就是模块树的构建方式,当然还要确保正确设置父模块和子模块之间的连接。

根据我们到目前为止所获得的知识,我们自己实际试验 ModuleGraph 将是一个很好的练习。为此,让我们看看一种实现自定义插件的方法,该插件将允许我们遍历 ModuleGraph。这是描述模块如何相互依赖的图表:

为了确保图中的所有内容都可以理解,a.js 文件导入 b.js 文件,该文件同时导入 b1.js 和 c.js,然后 c.js 导入 c1.j 和 d.js,最后,d .js 导入 d1.js。最后,ROOT 指的是空模块,它是 ModuleGraph 的根。入口选项仅包含一个值 a.js:

// webpack.config.js
const config = {
  entry: path.resolve(__dirname, './src/a.js'),
	/* ... */
};

现在让我们看看我们的自定义插件是什么样子的:

// The way we're adding logic to the existing webpack hooks
// is by using the `tap` method, which has this signature:
// `tap(string, callback)`
// where `string` is mainly for debugging purposes, indicating
// the source where the custom logic has been added from.
// The `callback`'s argument depend on the hook on which we're adding custom functionality.

class UnderstandingModuleGraphPlugin {
  apply(compiler) {
    const className = this.constructor.name;
    // Onto the `compilation` object: it is where most of the *state* of
    // the bundling process is kept. It contains information such as the module graph,
    // the chunk graph, the created chunks, the created modules, the generated assets
    // and much more.
    compiler.hooks.compilation.tap(className, (compilation) => {
      // The `finishModules` is called after *all* the modules(including
      // their dependencies and the dependencies' dependencies and so forth)
      // have been built.
      compilation.hooks.finishModules.tap(className, (modules) => {
        // `modules` is the set which contains all the built modules.
        // These are simple `NormalModule` instances. Once again, a `NormalModule`
        // is produced by the `NormalModuleFactory`.
        // console.log(modules);

        // Retrieving the **module map**(Map<Module, ModuleGraphModule>).
        // It contains all the information we need in order to traverse the graph.
        const {
          moduleGraph: { _moduleMap: moduleMap },
        } = compilation;

        // Let's traverse the module graph in a DFS fashion.
        const dfs = () => {
          // Recall that the root module of the `ModuleGraph` is the
          // *null module*.
          const root = null;

          const visited = new Map();

          const traverse = (crtNode) => {
            if (visited.get(crtNode)) {
              return;
            }
            visited.set(crtNode, true);

            console.log(
              crtNode?.resource ? path.basename(crtNode?.resource) : 'ROOT'
            );

            // Getting the associated `ModuleGraphModule`, which only has some extra
            // properties besides a `NormalModule` that we can use to traverse the graph further.
            const correspondingGraphModule = moduleMap.get(crtNode);

            // A `Connection`'s `originModule` is the where the arrow starts
            // and a `Connection`'s `module` is there the arrow ends.
            // So, the `module` of a `Connection` is a child node.
            // Here you can find more about the graph's connection: https://github.com/webpack/webpack/blob/main/lib/ModuleGraphConnection.js#L53.
            // `correspondingGraphModule.outgoingConnections` is either a Set or undefined(in case the node has no children).
            // We're using `new Set` because a module can be reference the same module through multiple connections.
            // For instance, an `import foo from 'file.js'` will result in 2 connections: one for a simple import
            // and one for the `foo` default specifier. This is an implementation detail which you shouldn't worry about.
            const children = new Set(
              Array.from(
                correspondingGraphModule.outgoingConnections || [],
                (c) => c.module
              )
            );
            for (const c of children) {
              traverse(c);
            }
          };

          // Starting the traversal.
          traverse(root);
        };

        dfs();
      });
    });
  }
}

根据模块层次结构,运行 build 命令后,我们应该得到以下输出:

a.js
b.js
b1.js
c.js
c1.js
d.js
d1.js

现在已经构建了 ModuleGraph,希望您已经掌握了它,是时候了解接下来会发生什么了。根据主图,下一步将是创建块,所以让我们开始吧。但在此之前,有必要澄清一些重要的概念,例如 Chunk、ChunkGroup 和 EntryPoint。

 

澄清 Chunk,ChunkGroup,EntryPoint

现在我们对什么是模块有了一些了解,我们将在此基础上解释本节标题中提到的概念。为了再次快速解释什么是模块,只要知道模块是文件的升级版本就足够了。一个模块,一旦创建和构建,除了原始源代码之外,还包含许多有意义的信息,例如:使用的加载器、它的依赖项、它的导出(如果有的话)、它的哈希等等。

一个chunk封装一个或多个模块。乍一看,可能会认为entry文件的数量(一个entry文件=入口对象的一项)与生成的块的数量成正比。这个陈述部分正确,因为entry对象可能只有一个项目但是chunks的数量可能大于一个。确实,对于每个entry item,在 dist 目录中都会有一个相应的块,但是可以隐式创建其他块,例如在使用 import() 函数时。但是不管是怎么创建的,每个chunk都会在dist目录下有一个对应的文件。我们将在构建 ChunkGraph 部分对此进行展开说明,我们将阐明哪些模块属于一个块,哪些不属于?

一个 ChunkGroup 包含一个或多个chunk。一个 ChunkGroup可以是另一个 ChunkGroup的父级或子级。例如,当使用动态导入时,对于每个使用的 import() 函数,都会创建一个 ChunkGroup,其父级将是一个现有的 ChunkGroup,它包含使用 import() 函数的文件(即模块)。在构建 ChunkGraph 部分可以看到这一事实的可视化。

EntryPoint 是一种 ChunkGroup,它是为entry对象中的每个项目创建的。chunk属于entrypoint这一事实对渲染过程有影响,因为我们将在以后的文章中更清楚地说明这一点。

鉴于我们对这些概念比较熟悉,让我们继续了解 ChunkGraph

构建ChunkGraph

回想一下,到目前为止,我们所拥有的只是一个 ModuleGraph,我们在上一节中讨论过。但是,ModuleGraph 只是bundling过程的必要部分。必须利用它才能使代码拆分等功能成为可能。

在bundling过程的这一点上,对于来自entry对象的每个项目,都会有一个entrypoint。由于它是 ChunkGroup 的一种,因此它至少会包含一个 chunk。所以,如果 entry 对象有 3 个 item,就会有 3 个 EntryPoint 实例,每个实例都有一个 chunk,也叫 entrypoint chunk,名字就是 entry item key 的值。与入口文件关联的模块称为入口模块,它们中的每一个都将属于它们的 entrypoint chunk。它们很重要,因为它们是 ChunkGraph 构建过程的起点。请注意,一个块可以有多个入口模块:

// webpack.config.js
entry: {
  foo: ['./a.js', './b.js'],
}

在上面的示例中,将有一个名为 foo 的块(项目的键)将有 2 个入口模块:一个与 a.js 文件关联,另一个与 b.js 文件关联。当然,该块将属于基于entry item创建的 EntryPoint 实例。

在详细介绍之前,让我们举一个例子,我们将在此基础上讨论构建过程:

entry: {
    foo: [path.join(__dirname, 'src', 'a.js'), path.join(__dirname, 'src', 'a1.js')],
    bar: path.join(__dirname, 'src', 'c.js'),
}

此示例将包含前面提到的内容:ChunkGroups(以及因此动态导入)、chunks和entrypoint的父子关系。

ChunkGraph 以递归方式构建。它首先将所有入口模块添加到队列中。然后,当一个入口模块被处理时,这意味着它的依赖项(也是模块)将被检查,并且每个依赖项也将被添加到队列中。这不断重复,直到队列变空。这部分过程是访问模块的地方。然而,这只是第一部分。回想一下,ChunkGroups 可以是其他 ChunkGroups 的父/子。这些连接在第二部分中得到解决。例如,如前所述,动态导入(即 import() 函数)将产生一个新的子 ChunkGroup。用 webpack 的说法,import() 表达式定义了一个异步的依赖块。从我的角度来看,它被称为块,因为首先想到的是包含其他对象的东西。在 import('./foo.js').then(module => ...) 的情况下,很明显我们的意图是异步加载一些东西,很明显,为了使用模块变量,在实际模块可用之前,必须解析 foo(包括 foo 本身)的所有依赖项(即模块)。我们将在以后的文章中彻底讨论 import() 函数的工作原理以及它的特殊性(例如魔术注释和其他选项)。

如果这激发了您的好奇心,那么这里就是在 AST 分析期间创建块的位置。

总结 ChunkGraph 构建过程的源代码可以在这里找到。

现在,让我们看一下根据我们上面的配置创建的 ChunkGraph 的图表:

该图说明了 ChunkGraph 的一个非常简化的版本,但它应该足以突出显示结果Chunk和 ChunkGroup 之间的关系。我们可以看到 4 个块,所以会有 4 个输出文件。 foo 块将有 4 个模块,其中 2 个是入口模块。bar chunk 将只有 1 个入口模块,而另一个可以被视为普通模块。我们还可以注意到,每个 import() 表达式都会产生一个新的 ChunkGroup(其父级是 bar EntryPoint),其中涉及一个新的 chunk。

 

生成文件的内容是根据 ChunkGraph 确定的,所以这就是为什么它对整个打包过程非常重要。我们将在下一节简要讨论块资产(即生成的文件)。

在探索我们将使用 ChunkGraph 的实际示例之前,重要的是要提及它的一些特殊性。与 ModuleGraph 类似,属于 ChunkGraph 的节点称为 ChunkGraphChunk(读作属于 ChunkGraph 的块),它只是一个装饰块,这意味着它作为一些额外的属性,例如作为块的一部分的模块,块的入口模块等。就像 ModuleGraph 一样,ChunkGraph 借助具有以下签名的映射使用附加属性跟踪这些块:WeakMap<Chunk, ChunkGraphChunk>。与 ModuleGraph 的 map 相比,由 ChunkGraph 维护的这个 map 不包含有关 chunk 之间连接的信息。相反,所有必要的信息(例如它所属的 ChunkGroups)都保存在块本身中。请记住,Chunk在 ChunkGroups 中组合在一起,并且这些ChunkGroup之间可以存在父子关系(正如我们在上图中看到的那样)。模块不是这样,因为模块可以相互依赖,但是没有严格的父模块概念。

现在让我们尝试在自定义插件中使用 ChunkGraph,以便更好地理解它。请注意,我们正在考虑的这个例子是上图描述的例子:

const path = require('path');

// We're printing this way in order to highlight the parent-child
// relationships between `ChunkGroup`s.
const printWithLeftPadding = (message, paddingLength) => console.log(message.padStart(message.length + paddingLength));

class UnderstandingChunkGraphPlugin {
  apply (compiler) {
    const className = this.constructor.name;
    compiler.hooks.compilation.tap(className, compilation => {
      // The `afterChunks` hook is called after the `ChunkGraph` has been built.
      compilation.hooks.afterChunks.tap(className, chunks => {
        // `chunks` is a set of all created chunks. The chunks are added into
        // this set based on the order in which they are created.
        // console.log(chunks);
        
        // As we've said earlier in the article, the `compilation` object
        // contains the state of the bundling process. Here we can also find
        // all the `ChunkGroup`s(including the `Entrypoint` instances) that have been created.
        // console.log(compilation.chunkGroups);
        
        // An `EntryPoint` is a type of `ChunkGroup` which is created for each
        // item in the `entry` object. In our current example, there are 2.
        // So, in order to traverse the `ChunkGraph`, we will have to start
        // from the `EntryPoints`, which are stored in the `compilation` object.
        // More about the `entrypoints` map(<string, Entrypoint>): https://github.com/webpack/webpack/blob/main/lib/Compilation.js#L956-L957
        const { entrypoints } = compilation;
        
        // More about the `chunkMap`(<Chunk, ChunkGraphChunk>): https://github.com/webpack/webpack/blob/main/lib/ChunkGraph.js#L226-L227
        const { chunkGraph: { _chunks: chunkMap } } = compilation;
        
        const printChunkGroupsInformation = (chunkGroup, paddingLength) => {
          printWithLeftPadding(`Current ChunkGroup's name: ${chunkGroup.name};`, paddingLength);
          printWithLeftPadding(`Is current ChunkGroup an EntryPoint? - ${chunkGroup.constructor.name === 'Entrypoint'}`, paddingLength);
          
          // `chunkGroup.chunks` - a `ChunkGroup` can contain one or mode chunks.
          const allModulesInChunkGroup = chunkGroup.chunks
            .flatMap(c => {
              // Using the information stored in the `ChunkGraph`
              // in order to get the modules contained by a single chunk.
              const associatedGraphChunk = chunkMap.get(c);
              
              // This includes the *entry modules* as well.
              // Using the spread operator because `.modules` is a Set in this case.
              return [...associatedGraphChunk.modules];
            })
            // The resource of a module is an absolute path and
            // we're only interested in the file name associated with
            // our module.
            .map(module => path.basename(module.resource));
          printWithLeftPadding(`The modules that belong to this chunk group: ${allModulesInChunkGroup.join(', ')}`, paddingLength);
          
          console.log('\n');
          
          // A `ChunkGroup` can have children `ChunkGroup`s.
          [...chunkGroup._children].forEach(childChunkGroup => printChunkGroupsInformation(childChunkGroup, paddingLength + 3));
        };
        
		// Traversing the `ChunkGraph` in a DFS manner.
        for (const [entryPointName, entryPoint] of entrypoints) {
          printChunkGroupsInformation(entryPoint, 0);
        }
      });
    });
  }
}; 

这是您应该看到的输出:

Current ChunkGroup's name: foo;
Is current ChunkGroup an EntryPoint? - true
The modules that belong to this chunk group: a.js, b.js, a1.js, b1.js

Current ChunkGroup's name: bar;
Is current ChunkGroup an EntryPoint? - true
The modules that belong to this chunk group: c.js, common.js


    Current ChunkGroup's name: c1;
    Is current ChunkGroup an EntryPoint? - false
    The modules that belong to this chunk group: c1.js

    Current ChunkGroup's name: c2;
    Is current ChunkGroup an EntryPoint? - false
    The modules that belong to this chunk group: c2.js

我们使用缩进来区分父子关系。我们还可以注意到输出与图表一致,因此我们可以确定遍历的正确性。

生成Chunk Assets

值得一提的是,生成的文件不仅仅是原始文件的复制粘贴版本,因为为了实现其功能,webpack 需要添加一些自定义代码,以使一切按预期工作。

这就引出了 webpack 如何知道要生成什么代码的问题。好吧,这一切都从最基本(也是最有用的)层开始:模块。一个模块可以导出成员,导入其他成员,使用动态导入,使用 webpack 特定的功能(例如 require.resolve)等。根据模块的源代码,webpack 可以确定生成哪些代码以实现所需的功能。这发现在 AST 分析期间开始,在此发现依赖项。尽管到目前为止我们一直在交替使用依赖项和模块,但事情在幕后有点复杂。

例如,一个简单的 import { aFunction } from './foo' 将产生 2 个依赖项(一个用于 import 语句本身,另一个用于说明符,即 aFunction),将从中创建单个模块。另一个例子是 import() 函数。正如前面部分中提到的,这将导致异步的依赖块,其中一个依赖项是 ImportDependency,它特定于动态导入。这些依赖关系是必不可少的,因为它们带有一些关于应该生成什么代码的提示。例如, ImportDependency 知道要告诉 webpack 什么才能异步获取导入的模块并使用其导出的成员。这些提示可以称为运行时要求。例如,如果模块导出了它的一些成员,就会有一些依赖(回想一下我们现在不是指模块),即 HarmonyExportSpecifierDependency,它将通知 webpack 它需要处理导出成员的逻辑。

总而言之,一个模块将附带其运行时要求,这取决于该模块在其源代码中使用的内容。块的运行时要求将是属于该块的所有模块的所有运行时要求的集合。现在 webpack 知道了一个块的所有需求,它将能够正确地生成运行时代码。这也称为渲染过程,我们将在专门的文章中详细讨论。现在,了解渲染过程严重依赖 ChunkGraph 就足够了,因为它包含ChunkGroup(即 ChunkGroup、EntryPoint),其中包含Chunk,其中包含Module,以细粒度的方式,包含有关将由 webpack 生成的运行时代码的信息和提示。

Hibernate映射计算的属性

发布于 2023.01.04 43分钟阅读 0 评论 5 推荐

    作者:

在一般的实体映射中,一般都是一个属性对应数据库的某一列。今天我们来看看如何映射通过其他属性计算出来的属性。好了先看看具体的两个领域类:

新建一个Account.java

@Entity(name = "Account")
@Table(name = "account")
public class Account {

    @Id
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private User owner;

    private String iban;

    private long cents;

    private double interestRate;

    private Timestamp createdOn;

    @Transient
    private double dollars;

    @Transient
    private long interestCents;

    @Transient
    private double interestDollars;

    public Account() {
    }

    public Account(
            Long id, User owner, String iban,
            long cents, double interestRate, Timestamp createdOn) {
        this.id = id;
        this.owner = owner;
        this.iban = iban;
        this.cents = cents;
        this.interestRate = interestRate;
        this.createdOn = createdOn;
    }

    @PostLoad
    private void postLoad() {
        this.dollars = cents / 100D;

        long months = createdOn.toLocalDateTime().until(
                LocalDateTime.now(),
                ChronoUnit.MONTHS)
                ;

        double interestUnrounded = (
                (interestRate / 100D) * cents * months
        ) / 12;

        this.interestCents = BigDecimal.valueOf(interestUnrounded)
                .setScale(0, BigDecimal.ROUND_HALF_EVEN)
                .longValue();

        this.interestDollars = interestCents / 100D;
    }

    public double getDollars() {
        return this.dollars;
//        return cents / 100D;
    }

    public long getInterestCents() {
        return this.interestCents;
//        long months = createdOn.toLocalDateTime().until(
//                LocalDateTime.now(),
//                ChronoUnit.MONTHS
//        );
//
//        double interestUnrounded = (
//                (interestRate / 100D) * cents * months
//        ) / 12;
//
//        return BigDecimal.valueOf(interestUnrounded)
//                .setScale(0, BigDecimal.ROUND_HALF_EVEN)
//                .longValue();
    }

    public double getInterestDollars() {
        return this.interestDollars;
//        return getInterestCents() / 100D;
    }
}

新建一个User.java

@Entity(name = "User")
@Table(name = "user")
public class User {
    @Id
    private Long id;

    private String firstName;

    private String lastName;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' +
                '}';
    }
}

注意,这里我们在Account类中新增了三个属性,分别是:dollars,interestCents,interestDollars,而这些属性通过@Preload这个监听器方法来初始化,并且通过相应的get方法将属性暴露出来。

下面看看测试方法:

public class FormulaPreloadTest extends AbstractTest {
    @Override
    protected Class[] entities() {
        return new Class[]{
                Account.class,
                User.class
        };
    }

    @Test
    public void insertAccount() {
        doInJPA(entityManager -> {
            User user = new User();
            user.setId(1L);
            user.setFirstName("John");
            user.setFirstName("Doe");

            entityManager.persist(user);
            Account account = new Account(
                    1L,
                    user,
                    "ABC123",
                    12345L,
                    6.7,
                    Timestamp.valueOf(
                            LocalDateTime.now().minusMonths(3)
                    )
            );
            entityManager.persist(account);
        });

        doInJPA(entityManager -> {
            Account account = entityManager.find(Account.class, 1L);

            assertEquals(123.45D, account.getDollars(), 0.001);
            assertEquals(207L, account.getInterestCents());
            assertEquals(2.07D, account.getInterestDollars(), 0.001);
        });
    }
}

运行之后可以看看,我们可以直接拿到这些属性的值,验证通过:

日志信息如下:

[INFO] 2023-01-04 13:34:16 method: net.ttddyy.dsproxy.support.CommonsLogUtils.writeLog(CommonsLogUtils.java:23)----Name:DATA_SOURCE_PROXY, Connection:2, Time:0, Success:True, Type:Statement, Batch:False, QuerySize:1, BatchSize:0, Query:["drop table if exists account CASCADE "], Params:[]
[INFO] 2023-01-04 13:34:16 method: net.ttddyy.dsproxy.support.CommonsLogUtils.writeLog(CommonsLogUtils.java:23)----Name:DATA_SOURCE_PROXY, Connection:2, Time:0, Success:True, Type:Statement, Batch:False, QuerySize:1, BatchSize:0, Query:["drop table if exists user CASCADE "], Params:[]
[INFO] 2023-01-04 13:34:16 method: net.ttddyy.dsproxy.support.CommonsLogUtils.writeLog(CommonsLogUtils.java:23)----Name:DATA_SOURCE_PROXY, Connection:3, Time:0, Success:True, Type:Statement, Batch:False, QuerySize:1, BatchSize:0, Query:["create table account (id bigint not null, cents bigint not null, createdOn timestamp, iban varchar(255), interestRate double not null, owner_id bigint, primary key (id))"], Params:[]
[INFO] 2023-01-04 13:34:16 method: net.ttddyy.dsproxy.support.CommonsLogUtils.writeLog(CommonsLogUtils.java:23)----Name:DATA_SOURCE_PROXY, Connection:3, Time:1, Success:True, Type:Statement, Batch:False, QuerySize:1, BatchSize:0, Query:["create table user (id bigint not null, firstName varchar(255), lastName varchar(255), primary key (id))"], Params:[]
[INFO] 2023-01-04 13:34:16 method: net.ttddyy.dsproxy.support.CommonsLogUtils.writeLog(CommonsLogUtils.java:23)----Name:DATA_SOURCE_PROXY, Connection:3, Time:1, Success:True, Type:Statement, Batch:False, QuerySize:1, BatchSize:0, Query:["alter table account add constraint FKlijilgu3y8bx1rb3oirmqlw5k foreign key (owner_id) references user"], Params:[]
[INFO] 2023-01-04 13:34:16 method: org.hibernate.engine.transaction.jta.platform.internal.JtaPlatformInitiator.initiateService(JtaPlatformInitiator.java:52)----HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
[INFO] 2023-01-04 13:34:16 method: net.ttddyy.dsproxy.support.CommonsLogUtils.writeLog(CommonsLogUtils.java:23)----Name:DATA_SOURCE_PROXY, Connection:4, Time:0, Success:True, Type:Prepared, Batch:False, QuerySize:1, BatchSize:0, Query:["insert into user (firstName, lastName, id) values (?, ?, ?)"], Params:[(Doe,NULL(VARCHAR),1)]
[INFO] 2023-01-04 13:34:16 method: net.ttddyy.dsproxy.support.CommonsLogUtils.writeLog(CommonsLogUtils.java:23)----Name:DATA_SOURCE_PROXY, Connection:4, Time:0, Success:True, Type:Prepared, Batch:False, QuerySize:1, BatchSize:0, Query:["insert into account (cents, createdOn, iban, interestRate, owner_id, id) values (?, ?, ?, ?, ?, ?)"], Params:[(12345,2022-10-04 13:34:16.4853734,ABC123,6.7,1,1)]
[INFO] 2023-01-04 13:34:16 method: net.ttddyy.dsproxy.support.CommonsLogUtils.writeLog(CommonsLogUtils.java:23)----Name:DATA_SOURCE_PROXY, Connection:5, Time:0, Success:True, Type:Prepared, Batch:False, QuerySize:1, BatchSize:0, Query:["select account0_.id as id1_0_0_, account0_.cents as cents2_0_0_, account0_.createdOn as createdo3_0_0_, account0_.iban as iban4_0_0_, account0_.interestRate as interest5_0_0_, account0_.owner_id as owner_id6_0_0_ from account account0_ where account0_.id=?"], Params:[(1)]
[INFO] 2023-01-04 13:34:16 method: org.hibernate.tool.schema.internal.SchemaDropperImpl$DelayedDropActionImpl.perform(SchemaDropperImpl.java:538)----HHH000477: Starting delayed evictData of schema as part of SessionFactory shut-down'
[INFO] 2023-01-04 13:34:16 method: net.ttddyy.dsproxy.support.CommonsLogUtils.writeLog(CommonsLogUtils.java:23)----Name:DATA_SOURCE_PROXY, Connection:6, Time:1, Success:True, Type:Statement, Batch:False, QuerySize:1, BatchSize:0, Query:["drop table if exists account CASCADE "], Params:[]
[INFO] 2023-01-04 13:34:16 method: net.ttddyy.dsproxy.support.CommonsLogUtils.writeLog(CommonsLogUtils.java:23)----Name:DATA_SOURCE_PROXY, Connection:6, Time:0, Success:True, Type:Statement, Batch:False, QuerySize:1, BatchSize:0, Query:["drop table if exists user CASCADE "], Params:[]

此外还有另一种方法,那就是hibernate提供的@Formula注解:

代码如下:

public class FormulaTest extends AbstractTest {
    @Override
    protected Class[] entities() {
        return new Class[]{
                Account.class,
                User.class
        };
    }

    @Test
    public void insertAccount() {
        doInJPA(entityManager -> {
            User user = new User();
            user.setId(1L);
            user.setFirstName("John");
            user.setFirstName("Doe");

            entityManager.persist(user);
            Account account = new Account(
                    1L,
                    user,
                    "ABC123",
                    12345L,
                    6.7,
                    Timestamp.valueOf(
                            LocalDateTime.now().minusMonths(3)
                    )
            );
            entityManager.persist(account);
        });

        doInJPA(entityManager -> {
            Account account = entityManager.find(Account.class, 1L);

            assertEquals(123.45D, account.getDollars(), 0.001);
            assertEquals(207L, account.getInterestCents());
            assertEquals(2.07D, account.getInterestDollars(), 0.001);
        });
    }

    @Entity(name = "Account")
    @Table(name = "account")
    public static class Account {
        @Id
        private Long id;

        @ManyToOne(fetch = FetchType.LAZY)
        private User owner;

        private String iban;

        private long cents;

        private double interestRate;

        private Timestamp createdOn;

        @Formula("cents::numeric / 100")
        private double dollars;

        @Formula("round((interestRate::numeric / 100) * cents * date_part('month', age(now(), createdOn))/ 12 ")
        private long interestCents;

        @Formula("round((interestRate::numeric / 100) * cents * date_part('month', age(now(), createdOn))/ 12)/ 100::numeric ")
        private double interestDollars;

        public Account() {
        }

        public Account(
                Long id, User owner, String iban,
                long cents, double interestRate, Timestamp createdOn) {
            this.id = id;
            this.owner = owner;
            this.iban = iban;
            this.cents = cents;
            this.interestRate = interestRate;
            this.createdOn = createdOn;
        }

        @Transient
        public double getDollars() {
            return this.dollars;
        }

        @Transient
        public long getInterestCents() {
            return this.interestCents;
        }
        @Transient
        public double getInterestDollars() {
            return this.interestDollars;
        }
    }

    @Entity(name = "User")
    @Table(name = "user")
    public static class User {
        @Id
        private Long id;

        private String firstName;

        private String lastName;

        public Long getId() {
            return id;
        }

        public void setId(Long id) {
            this.id = id;
        }

        public String getFirstName() {
            return firstName;
        }

        public void setFirstName(String firstName) {
            this.firstName = firstName;
        }

        public String getLastName() {
            return lastName;
        }

        public void setLastName(String lastName) {
            this.lastName = lastName;
        }

        @Override
        public String toString() {
            return "User{" +
                    "id=" + id +
                    ", firstName='" + firstName + '\'' +
                    ", lastName='" + lastName + '\'' +
                    '}';
        }
    }
}

注意,这里我们给属性添加的@Formula注解,并且将对应属性的get方法标注为@Transient,这里有一个小小的问题是@Formula注解的内容依赖于具体的数据库SQL的语法,因此兼容性不是很好;因此这个方法需要酌情考虑。

 

JPA——persistence.xml深入理解

更新于 2023.01.03 0分钟阅读 0 评论 5 推荐

    作者:

在本文中,我们会解释 JPA persistence.xml 配置文件的用途,以及如何使用可用的 XML 标记或属性设置 Java Persistence 应用程序。

虽然 Spring 应用程序可以在不需要 XML JPA 配置文件的情况下进行引导,但理解每个配置选项的含义仍然很重要,因为 Spring 在构建 Java Persistence LocalContainerEntityManagerFactoryBeanHibernate-specific LocalSessionFactoryBean 时还提供了另一种方法。

Persistence Unit

persistence.xml 配置文件用于配置给定的 JPA 持久化单元。持久化单元定义了引导 EntityManagerFactory 所需的所有元数据,例如实体映射、数据源和事务设置,以及 JPA proviser配置属性。

EntityManagerFactory 的目标是创建我们可以用于实体状态转换的 EntityManager 对象。

因此,persistence.xml 配置文件定义了引导 JPA EntityManagerFactory 所需的所有元数据。

JPA 持久化 XML 文件位置

传统上,persistence.xml 位于一个 META-INF 文件夹中,该文件夹需要驻留在 Java 类路径的根目录中。如果您使用的是 Maven,则可以将其存储在资源文件夹中,如下所示:

src/main/resources/META-INF/persistence.xml

JPA 持久性 XML 文件结构

persistence.xml 配置文件结构如下:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
     xmlns="http://xmlns.jcp.org/xml/ns/persistence"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
     http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
 
    <persistence-unit
        name="HypersistenceOptimizer"
        transaction-type="JTA">
 
        <description>
            Hypersistence Optimizer is a dynamic analyzing tool that can scan
            your JPA and Hibernate application and provide you tips about the
            changes you need to make to entity mappings, configurations, queries,
            and Persistence Context actions to speed up your data access layer.
        </description>
 
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
 
        <jta-data-source>java:global/jdbc/default</jta-data-source>
 
        <properties>          
            <property
                name="hibernate.transaction.jta.platform"
                value="SunOne"
            />
        </properties>
    </persistence-unit>
</persistence>

持久化标记<persistence>是根 XML 元素,它定义了 JPA 版本2.2和用于验证 persistence.xml 配置文件的 XML 模式。

持久化单元

<persistence-unit> 元素定义了关联的 JPA Persistence Unit 的名称,稍后您可以在使用 @PersistenceUnit JPA 注解注入关联的 EntityManagerFactory 实例时使用它来引用它:

@PersistenceUnit(name = "HypersistenceOptimizer")
private EntityManagerFactory entityManagerFactory;

transaction-type 属性定义了 JPA 事务策略,它可以取以下两个值之一:

  • JTA
  • RESOURCE_LOCAL

传统上,Java EE 应用程序默认使用 JTA,这需要有一个使用 2PC(两阶段提交)协议的 JTA 事务管理器以原子方式将更改应用于多个数据源(例如,数据库系统、JMS 队列、缓存)。

如果要将数据更改传播到单个数据源,则不需要 JTA,因此 RESOURCE_LOCAL 事务类型是一种更有效的替代方案。例如,默认情况下,Spring 应用程序使用 RESOURCE_LOCAL 事务,而要使用 JTA,您需要显式选择 JtaTransactionManager Spring bean。

简单总结一:如果涉及到多个数据源,则使用JTA,否则使用RESOURCE_LOCAL 。

描述

<description>元素允许您提供有关当前持久化单元目标的更多详细信息。

provider

<provider> XML 元素定义了实现 JPA PersistenceProvider 接口的完全限定类名。

如果您使用的是 Hibernate 4.3 或更新版本,那么您需要使用 org.hibernate.jpa.HibernatePersistenceProvider 类名。

如果您使用的是 Hibernate 4.2 或更早版本,则需要使用 org.hibernate.ejb.HibernatePersistence 类名。

jta 数据源和非 jta 数据源

JPA 规范定义两个不同的 XML 标记来提供 JNDI 数据源名称是非常不寻常的。应该有一个单独的数据源属性,因为事务类型已经指定是否使用 JTA。

不,如果您使用 JTA,您可以使用 jta-data-source 为关联的 JTA DataSource 指定 JNDI 名称,而对于 RESOURCE_LOCAL,您需要使用 non-jta-data-source。

如果您使用的是 Hibernate,还可以使用 hibernate.connection.datasource 配置属性来指定要使用的 JDBC 数据源。

properties

properties 元素允许您定义要配置的 JPA 或 JPA 提供程序特定的属性:

  • the Hibernate Dialect
  • the JTA transaction platform (e.g., GlassFish, JBoss, Bitronix, Atomikos)
  • 是否应该自动生成数据库模式
  • Hibernate 是否应该跳过对 RESOURCE_LOCAL 事务的自动提交检查
  • 激活慢SQL查询日志
  • 您可以在 org.hibernate.cfg.AvailableSettings 界面中找到更多属性。

举个例子:

<properties>
    <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
    <property name="hibernate.connection.driver_class" value="org.h2.Driver"/>
    <!-- H2 is running in pure in Memory db mode, data1.txt will be lost as soon as connection is closed -->
    <property name="hibernate.connection.url" value="jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1;MVCC=TRUE"/>
    <property name="hibernate.connection.username" value="sa"/>
    <property name="hibernate.connection.pool_size" value="5"/>
    <property name="hibernate.show_sql" value="true"/>
    <property name="hibernate.format_sql" value="true"/>
    <property name="hibernate.hbm2ddl.auto" value="update"/>
    <property name="org.hibernate.type.descriptor.sql.BasicBinder" value="true"/>
    <property name="org.hibernate.type.descriptor.sql.BasicExtractor" value="true"/>
</properties>

实体映射设置

默认情况下,Hibernate 能够根据 @Entity 注解的存在来查找 JPA 实体类,因此您无需声明实体类。

exclude-unlisted-classes

但是,如果要显式设置要使用的实体类,并排除在当前 Java 类路径中找到的任何其他实体类,则需要将 exclude-unlisted-classes 元素设置为 true 值:

<exclude-unlisted-classes>true</exclude-unlisted-classes>

class

在上面设置了 exclude-unlisted-classes XML 元素后,需要通过 class XML 元素指定当前 Persistence Unit 注册的实体类的列表:

<class>io.hypersistence.optimizer.forum.domain.Post</class>
<class>io.hypersistence.optimizer.forum.domain.PostComment</class>
<class>io.hypersistence.optimizer.forum.domain.PostDetails</class>
<class>io.hypersistence.optimizer.forum.domain.Tag</class>

绝大多数 JPA 和 Hibernate 应用程序使用注释来构建对象关系映射元数据。 但是,即使您正在使用注解,您仍然可以使用 XML 映射来覆盖静态注解元数据,并使用通过 orm.xml 配置文件提供的元数据。

mapping-file

默认情况下,orm.xml 配置文件位于 META-INF 文件夹中。如果要使用不同的文件位置,可以使用 persistence.xml 文件中的 mapping-file XML 元素,如下所示:

<mapping-file>file:///D:/Vlad/Work/Examples/mappings/orm.xml</mapping-file>

jar-file

但默认情况下,JPA 提供程序将扫描当前 Java 类路径以加载实体类或 XML 映射。如果要提供一个或多个要扫描的 JAR 文件,可以使用 jar-file 元素,如下所示:

<jar-file>lib/hypersistence-optimizer-glassfish-hibernate-example.jar</jar-file>

shared-cache-mode

shared-cache-mode 元素允许您定义将实体存储在二级缓存中的 SharedCacheMode 策略,它可以采用以下值之一:

  • ALL – 将所有实体存储在二级缓存中,
  • NONE – 实体不存储在二级缓存中,
  • ENABLE_SELECTIVE – 默认情况下不缓存任何实体,除了标记有 @Cacheable(true) 注释的实体,它们将被缓存
  • DISABLE_SELECTIVE - 默认情况下所有实体都被缓存,除了标有@Cacheable(false) 注释的实体
  • 未指定 - 使用 JPA 提供程序默认缓存策略。这也是未设置 shared-cache-mode 元素时使用的默认值。

您还可以使用 javax.persistence.cache.storeMode 属性以编程方式覆盖共享缓存模式策略,如下所示:

EntityManagerFactory entityManagerFactory = Persistence
.createEntityManagerFactory(
    "HypersistenceOptimizer",
    Collections.singletonMap(
        "javax.persistence.cache.storeMode",
        SharedCacheMode.ENABLE_SELECTIVE
    )
);

validation-mode

验证模式 XML 元素指定 ValidationMode 策略,它指示 JPA 提供者是否应该在运行时检查实体 Bean Validation。

验证模式元素可以采用以下值:

  • AUTO – 如果在当前 Java 类路径中找到 Bean Validation 提供程序,它将自动注册,并且所有实体都将被验证。 如果未找到 Bean Validation 提供程序,则不会验证实体。 这是默认值。
  • CALLBACK – 实体必须始终由 Bean 验证提供程序进行验证。如果 JPA 提供者没有在类路径上找到 Bean Validation 实现,则引导过程将失败。
  • NONE – 即使在类路径中找到 Bean Validation 提供程序,也不会验证实体。

您还可以使用 javax.persistence.validation.mode 属性以编程方式覆盖验证模式策略,如下所示:

EntityManagerFactory entityManagerFactory = Persistence
.createEntityManagerFactory(
    "HypersistenceOptimizer",
    Collections.singletonMap(
        "javax.persistence.validation.mode",
        ValidationMode.CALLBACK
    )
);

以上属性基本上总结了持久化单元的一些基本属性,如果还有其他的属性,欢迎大家一起讨论学习。

CSS——Flexbox布局另类理解

发布于 2022.12.25 45分钟阅读 0 评论 5 推荐

    作者:

Flexbox是一种非常强大的布局模式。当我们真正理解它们如何工作时,我们可以构建自动响应的动态布局,并且根据需要重新排列元素。

看下面一个例子:

新建一个dynamic-layout.html文件

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <link rel="stylesheet" href="dynamic-layout.css"/>
  <style>
	  form {
		display: flex;
		align-items: flex-end;
		flex-wrap: wrap;
		gap: 8px;
	  }
	  .name {
		flex-grow: 1;
		flex-basis: 120px;
	  }
	  .email {
		flex-grow: 3;
		flex-basis: 170px;
	  }
	  button {
		flex-grow: 1;
		flex-basis: 70px;
	  }
  </style>
  <title>dynamic-layout</title>
</head>
<body>
	<form>
	  <label class="name" for="name-field">
		Name:
		<input id="name-field" />
	  </label>
	  <label class="email" for="email-field">
		Email:
		<input id="email-field" type="email" />
	  </label>
	  <button>
		Submit
	  </button>
	</form>
</body>
</html>

新建一个dynamic-layout.css文件

*, *:before, *:after {
    box-sizing: border-box;
    line-height: 1.5;
    line-height: calc(1em + 0.5rem);
    -webkit-font-smoothing: antialiased;
    font-family: Wotfard;
}

/* Cosmetic styles */
form {
  padding: 8px;
  border: 1px solid hsl(0deg 0% 50%);
}

label {
  font-weight: 500;
}
input {
  display: block;
  width: 100%;
  height: 2.5rem;
  margin-top: 4px;
}
button {
  height: 2.5rem;
}

如果将页面的宽度由大到小不断调整,会依次出现以上四种不同的布局;也许大部分人说,这有啥难的,不就是媒体查询吗?可是你注意到没,在CSS代码中一点媒体查询的痕迹也没有,完全是这个demo的作者出于对Flexbox布局模式的深入理解,今天我们就来学习对Flexbox布局算法的深入理解,这样你也能学会不用媒体查询也能动态布局啦。

在这篇文章中,我想提高您对Flexbox布局的心智模型。通过了解这些属性中的每一个,我们将建立对 Flexbox 算法如何工作的感觉。无论您是 CSS 初学者,还是已经使用 Flexbox 多年,我敢打赌您会学到很多东西!

Flexbox简介

CSS由许多不同的布局算法组成,官方称为“布局模式”。每种布局模式在CSS 中有自己的子语言。默认布局模式是 Flow 布局,但我们可以通过更改父容器上的 display 属性来选择使用 Flexbox:

看下面的例子:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <link rel="stylesheet" href="default-flexbox.css"/>
  <style>
	.wrapper{
		
	}
	.wrapper-item {
		background:hsl(210deg,8%,50%);
		font-size: 1rem;
		margin: 2px;
		padding: 10px 16px;
		transform: none;
		transform-origin: 50% 50% 0px;
		color:#fff;
	}
  </style>
  <title>default-to-flexbox</title>
</head>
<body>
	<div class="wrapper">
		<div class="wrapper-item">hello</div>
		<div class="wrapper-item">to</div>
		<div class="wrapper-item">the</div>
		<div class="wrapper-item">world</div>
	</div>
</body>
</html>

当我们将display设置为 flex 时,我们创建了一个“flex 格式化上下文”。这意味着,默认情况下,所有子项都将根据 Flexbox 布局算法进行定位。

每个布局算法都是为解决特定问题而设计的。默认的“Flow”布局旨在创建数字文档;它本质上是 Microsoft Word 布局算法。标题和段落作为块垂直堆叠,而文本、链接和图像等内容则不显眼地位于这些块中。

那么,Flexbox解决了什么问题呢?Flexbox 就是将一组项目排列成一行或一列,并让我们对这些项目的分布对齐方式进行大量控制。顾名思义,Flexbox 就是关于灵活性的。我们可以控制项目是增长还是收缩额外空间的分配方式等等。

它过时了吗? 

你可能想知道:既然 CSS Grid 在现代浏览器中得到了很好的支持,那么 Flexbox 是不是已经过时了?

CSS Grid 是一种美妙的布局模式,但它解决的问题与 Flexbox 不同。我们应该学习这两种布局模式,并使用正确的工具来完成工作。

当涉及到在垂直或水平列表中排列项目的动态、流畅的 UI 时,Flexbox 仍然占据主导地位。

老实说,作为一个同时熟悉 CSS Grid 和 Flexbox 的人,我仍然发现自己经常接触 Flexbox!

Flex direction

如前所述,Flexbox 就是控制行或列中元素的分布。默认情况下,项目将并排堆叠成一行,但我们可以使用 flex-direction 属性翻转到一列:

使用 flex-direction: row,主轴从左到右水平运行。当我们翻转到 flex-direction: column 时,主轴从上到下垂直运行。

在 Flexbox 中,一切都基于主轴。该算法不关心垂直/水平,甚至行/列。所有的规则都是围绕这个主轴和垂直运行的交叉轴构建的。

这很酷。当我们学习了 Flexbox 的规则后,我们可以从水平布局无缝切换到垂直布局。所有的规则都是围绕这个主轴和垂直运行的交叉轴构建的。

默认情况下,子项目将根据以下 2 条规则定位:

  1. 主轴:子项目将在容器的开头聚集在一起。
  2. 交叉轴:子项目会伸展开来填满整个容器。

看看这个规则的可视化展示

在 Flexbox 中,我们决定主轴是水平运行还是垂直运行。这是所有 Flexbox布局计算都与之关联的基础。

Alignment

我们可以使用 justify-content 属性更改子项目沿主轴的分布方式:

说到主轴,我们一般不会从对齐单个子项目的角度来考虑。相反,这完全取决于群体(全体子项目)的分布。我们可以将所有子项目集中在一个特定的位置(使用 flex-start、center 和 flex-end),或者我们可以将它们分开(使用 space-between、space-around 和 space-evenly)。看下面的图片:

对于交叉轴,情况有些不同。我们使用 align-items 属性:

我们看看不同属性的情况:

很有趣……对于 align-items,我们有一些与 justify-content 相同的选项,但没有完美的重叠。

它们为什么不共享相同的选项?我们很快就会揭开这个谜团,但首先,我需要再分享一个对齐属性:align-self。

justify-content align-items 不同,align-self 应用于子元素,而不是容器。它允许我们更改特定子项沿交叉轴的对齐方式

大家可以去尝试一下,这个属性的值和align-items的值是一模一样,一个是对所有元素,一个是对特定子元素。

为什么单个子元素没有justify-self要理解为什么,我们需要更深入地研究 Flexbox 算法。

Content 与 items

根据我们目前所学的内容,Flexbox 似乎看起来非常随意。为什么是 justify-contentalign-items,而不是 justify-itemsalign-content

同时,为什么有一个align-self,而没有一个justify-self??

这些问题触及了关于 Flexbox 最重要却最容易被误解的事情之一。为了帮助解释,我想用一个比喻。

在 Flexbox 中,项目沿主轴分布。默认情况下,它们并排排列整齐。我可以画一条水平直线,把所有的子项目都串起来,就像烤肉串一样?:

但是,交叉轴不同。一条直线只会与其中一个子项目相交。

它不像烤肉串,更像是一群鸡尾酒香肠?

这里有一个显着的区别。使用鸡尾酒香肠,每个项目都可以沿着它的棒移动而不会干扰任何其他项目。

相比之下,我们的主轴串在每个兄弟项目上,单个项目不能沿着它的棒移动而不撞到它的兄弟项目!

这就是是主轴/交叉轴之间的根本区别。当我们谈论交叉轴对齐时,每个项目都可以做任何它想做的事情。但是,在主轴上,我们只能考虑如何分配组。

这就是为什么没有justify-self 的原因。假设中间项目设置 justify-self: flex-start 意味着什么?前面那里已经有另一块(另一个子项目)了!

考虑到所有这些情境,我们对我们一直在谈论的所有 4 个术语给出一个正确的定义:

  • justify — 沿主轴定位某物。
  • align — 沿交叉轴定位某物。
  • content——一组可以分布的“东西”。
  • items — 可以单独定位的单个项目。

因此:我们有 justify-content 来控制组沿主轴的分布,我们有 align-items 来沿交叉轴单独定位每个项目。这是我们使用 Flexbox 管理布局的两个主要属性。

没有 justify-items 的原因与没有 justify-self 的原因相同;当谈到主轴时,我们必须将项目视为一个组,作为可以分布的内容。

align-content呢?实际上,这确实存在于 Flexbox 中!我们稍后会在讨论 flex-wrap 属性时介绍它。

算法的输入

我们倾向于将 CSS 语言视为属性的集合,但我认为这是错误的思维模式。正如我们上一篇所见,width 属性的行为因所使用的布局模式而异!

相反,我喜欢将 CSS 视为布局模式的集合。每种布局模式都是一种算法,可以实现或重新定义每个 CSS 属性。我们为我们的 CSS 声明(键/值对)提供一个算法,算法决定如何使用它们。

换句话说,我们编写的 CSS 是这些算法的输入,就像传递给函数的参数一样。如果我们想真正对 CSS 感到舒服自在,仅仅学习属性是不够的;我们必须了解算法如何使用这些属性。

Growing and shrinking

我们已经看到 Flexbox 算法具有一些内置的灵活性,具有推荐的大小。但要真正了解 Flexbox 的特点,我们需要讨论 3 个属性:flex-grow、flex-shrink 和 flex-basis

flex-basis

我承认:很长一段时间,我都没有真正理解 flex-basis 到底是怎么回事。 🤗

简单来说:在 Flex 行中,flex-basis 与宽度起相同的作用。在 Flex 列中,flex-basis 与高度起相同的作用。

正如我们所了解的,Flexbox 中的所有内容都与主轴/交叉轴挂钩。例如,justify-content 会沿主轴分布子项,无论主轴水平还是垂直,它的工作方式都完全相同。

但是,宽度和高度不遵循此规则!宽度将始终影响水平尺寸。当我们将 flex-direction 从行翻转到列时,它不会突然变成高度。

因此,Flexbox 作者创建了一个通用的“大小”属性,称为 flex-basis。它就像宽度或高度,但与其他一切一样与主轴挂钩。它允许我们在主轴方向上设置元素的推荐大小,无论是水平还是垂直。

看下面的例子:

就像我们看到的宽度一样,flex-basis 更像是一个建议,而不是硬性约束(黄色区域在flex-basis为250时,实际宽度没有250,而是一个弹性的建议值)。在某个时刻,没有足够的空间让所有元素都按指定的大小放置,因此它们必须妥协以避免溢出。

不完全一样

通常,我们可以在 Flex 行中交替使用 width 和 flex-basis,但也有一些例外。例如,width 属性对图像等替换元素的影响不同于 flex-basis。此外,宽度可以将项目缩小到其最小尺寸以下,而 flex-basis 不能。

flex-grow

默认情况下,Flex 上下文中的元素将沿着主轴缩小到它们的最小舒适尺寸。这通常会产生额外的空间。我们可以指定如何使用 flex-grow 属性消耗该空间:看下图

flex-grow 的默认值为 0,这意味着增长是可选的。如果我们想让孩子吞噬容器中的任何额外空间,我们需要明确地告诉它。

如果多个子项目设置 flex-grow 怎么办?在这种情况下,额外的空间在子项目之间分配,根据他们的 flex-grow 值按比例分配。

flex-shrink

到目前为止,在我们看到的大多数示例中,我们都有额外的空间可以使用。但是,如果我们的子项目对于他们的容器来说太大了怎么办?

有趣,对吧?两个项目都会收缩,但它们会按比例收缩。第一个子项目的宽度始终是第二个子项目的 2 倍。

友情提示,flex-basis 与 width 的作用相同。我们将使用 flex-basis,因为它是常规的,但如果我们使用 width,我们会得到完全相同的结果!

flex-basis 和 width 设置元素的推荐大小。 Flexbox 算法可能会将元素缩小到低于此所需大小,但默认情况下,它们将始终一起缩放,并保持两个元素之间的比例。

现在,如果我们不希望我们的元素按比例缩小怎么办?这就是 flex-shrink 属性的用武之地。

好吧,所以:我们有两个子项目,每个子项目的推荐大小都是 250px。容器需要至少 500 像素宽才能容纳这些处于推荐大小的子项目。

假设我们将容器缩小到 400 像素。好吧,我们不能把 500 像素的内容塞进 400 像素的包里!我们有 100px 的赤字。我们的元素将需要放弃 100px 的总尺寸,以便它们适合。

flex-shrink 属性让我们决定如何支付余额。

就像 flex-grow 一样,它是一个比率。默认情况下,两个子项目都有 flex-shrink: 1,所以每个子项目支付 ½ 的余额。他们每个人放弃 50px,他们的实际大小从 250px 缩小到 200px。

现在,假设我们将第一个子节点设置为 flex-shrink: 3:

我们的总赤字为 100px。通常,每个子项目会支付 ½,但因为我们已经修改了 flex-shrink,所以第一个元素最终支付 ¾ (75px),第二个元素支付 ¼ (25px)。

请注意,绝对值并不重要,重要的是比率。如果两个子项目都有 flex-shrink: 1,每个子项目将支付总赤字的 ½。如果两个子项目都被调高到 flex-shrink: 1000,每个子项目将支付总赤字的 1000/2000。无论哪种方式,结果都是一样的。

收缩和比例

在我们一直在查看的示例中,两个 Flex 子项具有相同的推荐大小 (250px)。在弄清楚如何缩小它们时,我们可以专门使用 flex-shrink 来计算它。

不过,正如我们之前看到的,缩小算法也会尝试保持兄弟姐妹之间的比例。如果第一个孩子是第二个孩子的 2 倍,它会收缩得更厉害。

因此,完整的计算涉及查看每个子项的相对 flex-shrink 及其相对大小。

不久前,我对 flex-shrink 有了一个顿悟:我们可以将其视为 flex-grow 的“逆”。它们是同一枚硬币的两面:

  • flex-grow 控制当项目小于容器时如何分配额外空间。
  • flex-shrink 控制当项目大于容器时如何删除空间。

这意味着一次只能激活其中一个属性。如果有额外的空间,则 flex-shrink 无效,因为项目不需要收缩。如果子项目们对于他们的容器来说太大了, flex-grow 就没有效果,因为没有额外的空间可以分配。

我喜欢将其视为两个独立的领域。你要么在地球上,要么在颠倒的地方?每个世界都有自己的规则。

Preventing shrinking

有时,我们不希望我们的一些 Flex 子项收缩。

我一直在使用 SVG 图标和形状注意到这一点。让我们看一个简化的例子:

当容器变窄时,我们的两个圆圈会被压扁成椭圆形。如果我们希望它们保持循环怎么办?

我们可以通过设置 flex-shrink: 0 来做到这一点:

当我们将 flex-shrink 设置为 0 时,我们实际上完全“选择退出”收缩过程。 Flexbox 算法会将 flex-basis(或宽度)视为硬性最小限制。

看下面的例子:

新建一个shrink-prevent.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <link rel="stylesheet" href="shrink-prevent.css"/>
  <style>
	/* This is the key property: */
	  .item.ball {
		flex-shrink: 0;
	  }
  </style>
  <title>shrink-prevent-flexbox</title>
</head>
<body>
	<div class="wrapper">
	  <div class="item ball"></div>
	  <div class="item stretch"></div>
	  <div class="item ball"></div>
	</div>
</body>
</html>

新建一个shrink-prevent.css

body {
  background: hsl(210deg, 30%, 12%);
  color: hsl(0deg 0% 100%);
}
.wrapper {
  display: flex;
  gap: 8px;
}
.item {
  height: 32px;
  border: 2px solid hsl(210deg 8% 50%);
  background: hsl(210deg 15% 20%);
}
.item.stretch {
  /*
    Because this item is empty, it
    has a default hypothetical width
     of 0px. This isn't realistic,
     though; in a typical case,
     there would be content in here!
     And so we set a width of 300px
     to simulate it containing stuff.
  */
  width: 300px;
  flex-grow: 1;
  border-radius: 16px;
}
.item.ball {
  width: 32px;
  /*
    NOTE: We could prevent the circle
    from squishing into an oval by
    setting border-radius: 16px
    instead. I'm using percentages
    because it makes for a better
    demo. But even with a pixel-based
    radius, we still need
    flex-shrink: 0 to prevent the
    item from shrinking (give it a
    shot and see the difference!).
  */
  border-radius: 50%;
}

相信通过这个例子大家能理解怎么阻止收缩啦!

The minimum size gotcha

我们还需要在这里讨论一件事,这非常重要。这可能是整篇文章中最有用的东西!

假设我们正在为电子商务商店构建一个流畅的搜索表单:

当容器收缩到一定程度以下时,内容就会溢出!

但为什么?? flex-shrink 的默认值为 1,我们没有删除它,所以搜索输入应该可以根据需要缩小!为什么它拒绝收缩?

事情是这样的:除了推荐的大小,Flexbox 算法还关心另一个重要的大小:最小大小。

Flexbox 算法拒绝将子项目缩小到其最小尺寸以下。无论我们将 flex-shrink 调到多高,内容都会溢出而不是进一步缩小!

文本输入的默认最小尺寸为 170px-200px(因浏览器而异)。这就是我们在上面遇到的限制。

在其他情况下,限制因素可能是元素的内容。例如,尝试调整此容器的大小:

对于包含文本的元素,最小宽度是最长牢不可破的字符串的长度。

好消息是:我们可以使用 min-width 属性重新定义最小尺寸。

通过直接在 Flex 子元素上设置 min-width: 0px,我们告诉 Flexbox 算法覆盖“内置”最小宽度。因为我们将它设置为 0px,元素可以根据需要缩小。同样的技巧可以在具有 min-height 属性的 Flex 列中使用(尽管问题似乎并不经常出现)。

提醒一句:非必要不要修改这个属性的值,因为可能造成访问性问题。

Gaps

近年来最大的 Flexbox 中改进之一是 gap 属性:

gap 允许我们沿着主轴在每个 Flex 子元素之间创建空间。这对于导航标题之类的东西非常有用:

gap 是 Fl​​exbox 语言的一个相对较新的补充,但自 2021 年初以来,它已在所有现代浏览器中实现。

Auto margins

我想分享另一个与间距相关的技巧。它在 Flexbox 的早期就已经存在,但它相对晦涩难懂,当我第一次发现它时,它让我大吃一惊。

margin 属性用于在特定元素周围添加空间。在某些布局模式中,例如 Flow 和 Positioned,它甚至可以用于使元素居中,margin: auto。

Flexbox 中的Auto margin更有趣:

早些时候,我们看到了 flex-grow 属性如何吞噬任何额外的空间,并将其应用于子项目。自动边距会吞噬额外的空间,并将其应用于元素的边距。它使我们能够精确控制在何处分配额外空间。

常见的标题布局在一侧具有徽标,在另一侧具有一些导航链接。以下是我们如何使用自动边距构建此布局:

新建auto-margin.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <link rel="stylesheet" href="auto-margin.css"/>
  <style>
	ul {
		display: flex;
		gap: 12px;
	  }
	  li.logo {
		margin-right: auto;
	  }
  </style>
  <title>auto-margin-flexbox</title>
</head>
<body>
	<nav>
	  <ul>
		<li class="logo">
		  <a href="/">
			Corpatech
		  </a>
		</li>
		<li>
		  <a href="">
			Mission
		  </a>
		</li>
		<li>
		  <a href="">
			Contact
		  </a>
		</li>
	  </ul>
	</nav>
</body>
</html>

新建auto-margin.css

body {
  padding: 0;
}
nav {
  padding: 12px;
  border-bottom: 1px dotted
    hsl(0deg 0% 0% / 0.2);
}
ul {
  list-style-type: none;
  align-items: baseline;
  padding: 0px;
  margin: 0;
}
ul a {
  color: inherit;
  text-decoration: none;
  font-size: 0.875rem;
}
.logo a {
  font-size: 1.125rem;
  font-weight: 500;
}

Corpatech 徽标是列表中的第一个列表项。通过给它 margin-right: auto,我们收集了所有额外的空间,并将其强制放在第一项和第二项之间。

我们有很多其他方法可以解决这个问题:我们可以将导航链接分组到它们自己的 Flex 容器中,或者我们可以使用 flex-grow 增加第一个列表项。但就个人而言,我喜欢自动边距解决方案。我们将额外的空间视为一种资源,并决定它应该放在哪里。

Wrapping

到目前为止,我们已经介绍了很多东西。我还想分享一件重要的事情。

到目前为止,我们所有的项目都并排放置在一行/一列中。 flex-wrap 属性允许我们改变它。

大多数时候,当我们在二维空间工作时,我们会想要使用 CSS Grid,但是 Flexbox + flex-wrap 肯定有它的用处!这个特殊的例子展示了“解构的煎饼”布局,其中 3 个项目在中型屏幕上堆叠成一个倒金字塔。

当我们设置 flex-wrap: wrap 时,项目不会缩小到低于其假设大小。至少,当换行到下一行/列是一个选项时不会!

可是等等!我们的烤肉串/鸡尾酒香肠比喻怎么样?

使用 flex-wrap: wrap,我们不再有一条可以串起每个项目的主轴线。实际上,每一行都充当了自己的迷你弹性容器。每行都有自己的串,而不是 1 个大串:

到目前为止,我们学到的所有规则在这个缩小的范围内继续适用。例如,justify-content 会将两块分布在每根棍子上。

但是嗯...既然我们有多行,align-items是如何工作的?交叉轴现在可以与多个项目相交!

花点时间考虑一下。当我们改变这个属性时,你认为会发生什么?一旦你有了答案(或至少是一个想法),看看它是否正确:

每一行都是它自己的迷你 Flexbox 环境。 align-items 将在环绕每一行的不可见框中向上或向下移动每个项目。

但是如果我们想自己对齐行呢?我们可以使用 align-content 属性来做到这一点:

总结一下这里发生的事情:

  • flex-wrap: wrap 给了我们两行东西。
  • 在每一行中,align-items 允许我们将每个子项目向上或向下滑动
  • 但是,缩小后,我们在单个 Flex 上下文中有这两行!交叉轴现在将与两行相交,而不是一行。因此,我们不能单独移动行,我们需要将它们作为一个组进行分配。
  • 使用我们上面的定义,我们处理的是内容,而不是项目。但我们还在谈论交叉轴!所以我们想要的属性是 align-content。

CSS——理解布局算法

发布于 2023.01.30 9分钟阅读 0 评论 5 推荐

    作者:

大家在学习CSS的时候,是不是常常有一些灵感浮现的时刻,或者是看到别人只是用简单的CSS就能做出如此炫酷的效果。其实我也有类似的经历,我那时候总是关注折写出来的CSS的属性和值有哪些情况,它们分别的效果是什么?比如说z-index:10肯定在z-index:5之上; justify-content:center就是在flex布局的时候,让元素框居中;我琢磨着如果能够将CSS的属性和值学习得越多,那么就能够更加深刻的理解这一门语言。

随着时间的推移,我对CSS的关键认知有了变化:CSS不仅仅是一系列属性的集合,还是各种相互关联的布局算法的集合;每个算法都是一个复杂的系统,且有自身的规则和隐秘机制。

了解特定属性和值的作用是不够的;还应该理解布局算法是怎样工作的?这些算法怎样使用我们提供的属性来进行布局。

您也许也遇到过这样的情况,写了以前多次写过的CSS属性,但是却得到了不是预期输出的布局效果,这样的不安会让我们觉得特别沮丧。因此CSS会让我们心理上觉得不一致和不稳定。同时又陷入深深的疑问:为什么相同的输入会产生不同的输出?

发生这种情况是因为这些属性和值工作在一个复杂的系统中,一些微妙的上下文变化改变了属性的行为方式。我们对CSS的认知模型不完整,导致产生了各种各样的惊吓。

当我开始深入理解CSS的布局算法的时候,一切变得豁然开朗。困扰多年的迷惑也云开雾散。我开始认识到CSS是一门强大语言,也开始享受编写的乐趣。

本文将用一个全新的视角来帮助理解CSS背后发生的事情;同时用这个视角来解惑那些惊人的疑团。

布局算法

什么是布局算法呢?您可能已经非常熟悉它们了,布局算法包括:

  • Flexbox
  • Positioned
  • Grid
  • Table
  • Flow

技术上它们也被成为布局模式,而不叫布局算法;我这里称呼布局算法是希望能够显得高大上一点,欢迎拍砖。

浏览器在渲染HTML页面的时候,每个元素都会使用一个主要的布局算法来计算元素的页面布局。我们可以使用特定的CSS声明来选择不同的布局算法。举个例子:position:absolute会将元素切换为Positioned布局;默认是Flow布局。

我们先看一个例子。有下面的CSS代码

.box {
  z-index: 10;
}

我们的首要工作是确定将使用哪种布局算法来渲染.box元素。根据提供的CSS,我们可以确定的是使用Flow布局算法。

Flow是网络上最通用的布局算法。它诞生于网络被视为一系列超连接文档,就像是世界上最大的档案库一样。它和微软的Word文字处理软件中使用的布局算法类似。

Flow也是非表格HTML元素的默认布局算法,除非你明确指定一种布局算法,否则就是Flow布局。

z-index属性用于控制堆叠顺序,如果它们发生重叠,哪一个显示在“顶部”。但是这里有一个事实是:在Flow布局中根本没有实现这个属性。Flow就是创建文档样式的布局,相信您没有看到过允许元素重叠的文字处理软件。

网络上有一种这样的说法:

如果不将position设置为“relative”或“absolute”之类的值,则不能使用 z-index,因为 z-index 属性取决于position属性。

以上说法不完全错,但是却有一点微妙的误解。更准确的说法是z-index属性没有在Flow布局中实现,如果想让这个属性起作用,那么需要选择一个实现了此属性的不同布局算法。

您可能觉得在此我有点小题大做,但是这个小误解会产生大困惑。举例如下:

新建一个html文件

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <link rel="stylesheet" href="layout.css"/>
  <style>
	  .row {
		display: flex;
		gap: 16px;
	  }
	  .raised.item {
		z-index: 2;
		background: hotpink;
	  }
  </style>
  <title>layout-algorithm</title>
</head>
<body>
	<ul class="row">
	  <li class="item"></li>
	  <li class="raised item"></li>
	  <li class="item"></li>
	</ul>
</body>
</html>

新建一个layout.css文件

.row {
  list-style-type: none;
  padding: 16px;
}
.item {
  width: 50px;
  height: 50px;
  border: 2px solid;
  border-radius: 4px;
  background: white;
}
.raised.item {
  margin-top: 8px;
  margin-right: -32px;
}

效果如图所示:

在这个例子中,我们使用Flexbox布局算法安排了3 个兄弟元素。中间的元素设置的z-index属性,可以看到能正常工作,显示在最顶端。如果将这个属性删掉,可以看到效果如下:

可以看到这个时候元素不再出现在顶层。

为什么会这样?我们没有在任何地方设置position: relative。按照前面所说,这个属性能够正常工作是因为Flexbox布局算法实现了z-index这个属性。当语言作者设计Flexbox布局算法时,他们决定实现 z-index 属性来控制堆叠顺序,就像在Positioned布局算法中一样。

这是关键的心智模式(不会觉得不安了吧)转变。CSS 属性本身是没有意义的(一定要结合具体的布局算法才有意义)。由布局算法来定义它们的作用,以及它们在计算中的工作方式。

需要明确的是,有一些 CSS 属性在所有布局算法中都是一样的。 color: red 无论如何都会产生红色文本。但是每个布局算法都可以覆盖任何属性的默认行为。许多属性没有任何默认行为。

有一个让人吃惊的例子,您通过width属性的实现方式因为不同算法而有所区别吗?看下面的例子:

新建一个width.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <link rel="stylesheet" href="width.css"/>
  <style>
	  .flex-wrapper {
		display: flex;
	  }
	  .item {
		width: 2000px;
	  }
  </style>
  <title>width-different-with-algorithm</title>
</head>
<body>
	<div class="item"></div>

	<div class="flex-wrapper">
	  <div class="item"></div>
	</div>
</body>
</html>

再新建一个width.css

.row {
  list-style-type: none;
  padding: 16px;
}
.item {
  height: 50px;
  border: 2px solid;
  border-radius: 4px;
  background: hotpink;
  margin: 16px;
}

效果如下:

第一个.item元素有一个宽度为2000px的属性,这个元素使用的是Flow布局算法来渲染,因此它实际占据了2000px的空间宽度;在Flow布局算法中,宽度是硬性规则,没有含糊。

第二个.item元素在一个flex容器中渲染,因此采用的布局算法是Flexbox;而在Flexbox布局算法中,宽度这是一个建议值,没有强制硬性规定。

Flexbox规范称宽度为推荐大小。它是元素在容器中的实际大小,而没有任何约束或者强制一定是多少。在一个足够大的容器中,这个大小可能是2000px;但是在一个比较小的容器中,元素的大小会收缩,以便适应容器的大小。

在这里,视角(不同的布局算法)非常重要。当涉及到Flexbox布局时,宽度并没有什么特别的警告。这是 Flexbox布局算法实现 width属性不同于 Flow布局算法而已。 

您可以这里理解这种机制:

我们编写的CSS属性是输入,就像传递给函数的参数一样。选择如何处理这些输入取决于布局算法。如果我们想深入理解CSS,就需要了解布局算法的工作原理;仅了解属性是远远不够的。

识别布局算法

CSS没有一个布局算法的属性,比如layout-mode;有几个属性可以调整所使用的布局算法,这让布局算法会变得非常棘手!

在某些情况下,应用于元素的 CSS 属性将选择特定的布局模式。看下面的代码

.help-widget {
  /* Uses Positioned layout, because of this declaration: */
  position: fixed;
  right: 0;
  bottom: 0;
}
.floated {
  /* Uses Float layout, because of this declaration: */
  float: left;
  margin-right: 32px;
}

在其他情况下,我们需要查看父级元素使用的CSS属性。例如:

<style>
  .row {
    display: flex;
  }
</style>
<ul class="row">
  <li class="item"></li>
  <li class="item"></li>
  <li class="item"></li>
</ul>

当我们应用display: flex时,我们实际上并没有为.row元素使用Flexbox布局算法;相反,我们说它的子元素应该使用 Flexbox 布局来定位。

用技术话语来说,display: flex创建了一个 flex格式化上下文。所有直接子级都将应用此上下文,这意味着子元素将使用 Flexbox 布局而不是默认的 Flow 布局。

display: flex也会把一个内联元素,比如 <span>变成块级元素,所以它确实对父元素的布局有一些影响。但它不会改变使用的布局算法。

布局算法变体

一些布局算法可以分割成多个变体。

例如,当我们使用Positioned布局时,它指的是几种不同的“定位方案”:

  • Relative
  • Absolute
  • Fixed
  • Sticky

每个变体有点像它自己的迷你布局算法,尽管它们确实共享一些共同点(例如,它们都可以使用 z-index 属性)。

同样,在 Flow 布局中,元素可以是块状或内联的。稍后我们将详细讨论 Flow 布局。

算法冲突

当多个布局算法应用在一个元素上会发生什么呢?这是一个有意思的话题。看个例子

<style>
  .row {
    display: flex;
  }
  .primary.item {
    position: absolute;
  }
</style>
<ul class="row">
  <li class="item"></li>
  <li class="primary item"></li>
  <li class="item"></li>
</ul>

所有三个列表项都是Flex 容器中的子项,因此它们应该根据 Flexbox 进行定位。但是那个中间的子项目通过设置 position: absolute 选择了 Positioned 布局。

据我了解,元素将使用主布局模式呈现。这有点像特异性:某些布局模式比其他布局模式具有更高的优先级。

我不知道确切的层次结构,但Positioned布局往往胜过一切。因此,在这个例子中,中间的子项目将使用 Positioned 布局,而不是 Flexbox。

因此,Flexbox 的计算结果就好像只有两个子项目,而不是三个。就 Flexbox 算法而言,那个中间子项目不存在;它对算法完全没有影响。

一般来说,冲突通常是非常明显的/有意的。但是,如果您发现某个元素的行为方式与您期望的不同,则值得尝试确定它使用的是哪种布局算法。答案可能会让你大吃一惊!

相对定位

这里有一个难题,如果每一个元素都使用单一布局算法渲染,那么我们应该如何理解相对定义呢?

具有 position: relative 的元素使用 Positioned 布局清晰呈现。它可以使用独有的定位布局属性,如 top 或 left。然而,它也可以参与 Flexbox / Grid 布局!

这个问题比较复杂,超出了本文讨论的范畴,但是可以快速理解一下

每个元素都在特定的格式化上下文中渲染,由布局算法决定是否参与其中。通常,Positioned 布局算法会忽略此类上下文,但它会排除相对定位的例外情况。

当在 Flexbox 上下文中渲染相对定位的元素时,Positioned 布局算法将允许它参与。一旦使用该上下文确定了它的大小/位置,它就会应用 Positioned 布局内容(例如,使用 top 或 left 调整位置)。

你可以把它想成有点像组合。 Positioned 布局算法将为相对定位的元素组合 Flexbox 布局算法。

内联魔法空间

我们首先来看一个经典的令人困惑的CSS问题,看看布局算法是怎么样帮助我们理解并解决这个问题的。

新建一个inline-magic-space.html的文件

<!DOCTYPE html>
<html lang="en" >
<head>
  <meta charset="UTF-8">
  <title>inline magic space</title>
  <link rel="stylesheet" href="normalize.min.css">
  <link rel="stylesheet" href="inline-magic-space.css">
</head>
<body>
<div class="photo-wrapper">
  <img
    class="cat-photo"
    alt="A basketful of cats"
    src="cats.jpg"
  />
</div>
</body>
</html>

新建一个inline-magic-space.css的文件

.photo-wrapper {
  border: 1px solid;
}

.cat-photo {
  width: 250px;
  max-width: 100%;
}

效果如下所示:

为什么图像下面有一些额外的空白呢?

如果试着调试一下,你就会发现,图像的宽度和高度都是250px,但是外面的包装器元素.photo-wrapper的高度实际上会多几个像素,为什么会这样呢?

如果您熟悉盒模型,您就会知道可以使用填充、边框和边距来分隔元素。您可能认为图像上有一些边距,或者容器上有一些填充?

在这个例子下,这些属性均没有作用。这就是为什么多年来,我一直私下将其称为“内联魔法空间”。它不是由通常的罪魁祸首引起的。

要了解这里发生了什么,我们必须更深入地研究 Flow 布局。

Flow布局算法

如前文所述,Flow布局是为文档设计的,类似于文字处理软件。

文档具有以下特点:

  • 单个字符组合成单词和句子。当没有足够的水平空间时,这些元素内联、并排和换行。
  • 段落被视为块,如标题或图像。块将垂直堆叠,一个在另一个之上,从上到下。

Flow布局就是基于这种结构。单个元素可以排列为行内元素(并排,如段落中的单词),或排列为块元素(从上到下堆叠的大块砖):

大多数 HTML 元素都带有合理的默认值。 <p> 和 <h1> 被认为是块级元素,而 <span> 和 <strong> 被认为是内联元素。

行内元素用于段落中间,而不是布局的一部分。例如,也许我们想在句子中间添加一个小图标。

为了确保行内元素不会对周围文本的易读性产生负面影响,添加了一些额外的垂直空间。

那么,回到我们的谜团:为什么我们的图像有一些额外的空间像素?因为图片默认是行内元素!

Flow 布局算法将此图像视为段落中的字符,并在下方添加一点空间以确保它不会令人不舒服地靠近(理论上的)下一行文本中的字符。

默认情况下,内联元素是“基线”对齐的。这意味着图像的底部将与文本所在的不可见水平线对齐。这就是图像下方有一些空间的原因——该空间用于下行,例如字母 j 和 p。

所以它不是边距、填充或边框……它是 Flow 布局应用于内联元素的固有空间位。

解决方案

有多种方法可以解决此问题。也许最简单的方法是在 Flow 布局中将此图像视为一个块:

.cat-photo {
    display: block;
}

或者,因为这种行为是 Flow 布局所独有的,我们可以转向不同的布局算法:

/*
  We flip its *parent* to Flex, so
  that the child will use Flexbox
  instead of Flow:
*/
.photo-wrapper {
    display: flex;
}

最后,我们还可以通过使用line-height将额外空间缩小为 0 来解决这个问题:

.photo-wrapper {
  line-height: 0;
}

此解决方案通过将其设置为 0 来删除所有额外的行间距。这会使多行文本完全不可读,但由于此容器不包含文本,因此这不是问题。

我建议使用前面两种解决方案中的一种。提出这个纯粹是因为它很有趣(并且因为它证明了问题是由于行间距造成的!)。

行高和可访问性

当我们谈论行高时:您知道“无样式”的 HTML 实际上被认为是不可访问的,因为行靠得太近了吗?当行间距不够大时,患有阅读障碍的人很难解析文本。

大多数浏览器的默认行高为 1.1 到 1.2,但根据 WCAG 指南,我们应该将正文的行高至少设置为 1.5。

好啦。本文主要为了给大家建立起一种对CSS的稳定和一致感,每次遇到输出和预期不一致的时候,思考一下当前的元素采用的是什么布局算法,然后再结合具体的属性进行分析,写得越多,积累得越多,并且思考得越多,您就会对CSS更加自信。

 

CSS-Layout Ribbon

更新于 2022.12.16 14分钟阅读 0 评论 5 推荐

    作者:

最近在设计页面的时候,用到了css布局中的一个Ribbon布局,样子如下图所示:

刚开始打算用这个样式作为文章的标题,后来暂时没有使用,但是在学习这个布局的时候,积累了很多css的知识,今天在此记录一下,方便以后查找温习。

首先,我贴出这部分代码的dom结构:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8"/>
    <title>Ribbons设计 - 实例</title>
    <link rel="stylesheet" type="text/css" href="./ribbon.css" />
</head>

<body>
	<div class="ribbon">
		<!-- The left side -->
		<div class="ribbon__side ribbon__side--l"></div>

		<!-- The left triangle displayed below the content -->
		<div class="ribbon__triangle ribbon__triangle--l"></div>

		<!-- The right triangle displayed below the content -->
		<div class="ribbon__triangle ribbon__triangle--r"></div>

		<!-- The right side -->
		<div class="ribbon__side ribbon__side--r"></div>

		<!-- The content -->
		<div class="ribbon__content"></div>
	</div>
</body>
</html>

然后再贴出对应的ribbon.css的代码

.ribbon {
    /* Center the content */
    align-items: center;
    display: flex;
    justify-content: center;

    /* Size */
    height: 2rem;

    /* Use to position the corners */
    position: relative;
	
	width: 6rem;
	margin : 6rem auto;
}

.ribbon__content {
    /* Background color */
    background-color: #9ca3af;
    z-index: 1;

    height: 100%;
    width: 100%;
}
.ribbon__side {
    bottom: -0.5rem;
    position: absolute;

    /* Displayed under the ribbon */
    z-index: 1;

    /* Background */
    border: 1rem solid #d1d5db;
}

.ribbon__side--l {
    /* Position */
    left: -1.5rem;
    border-color: #d1d5db #d1d5db #d1d5db transparent;
}

.ribbon__side--r {
    /* Position */
    right: -1.5rem;
    border-color: #d1d5db transparent #d1d5db #d1d5db;
}

.ribbon__triangle {
    position: absolute;
    top: 100%;

    border: 0.5rem solid transparent;
    border-bottom-width: 0;
    border-top-color: #1c1919;
	z-index:2;
}

.ribbon__triangle--l {
    border-right-width: 0;
    left: 0;
}

.ribbon__triangle--r {
    border-left-width: 0;
    right: 0;
}

注释部分解释了各部分代码在实现这个样式的时候起到的作用。我们来具体分析和研究一下:

从这个ribbon的dom结构可以看出,它有最外部的容易.ribbony以及内部的五个部分组成,分别是左右的支出去的两部分结构,内容区下面的三角形以及具体的内容区组成。

首先看看这两个支出去的部分是怎么实现的呢?

它们的代码如下:

.ribbon__side {
    bottom: -0.5rem;
    position: absolute;

    /* Displayed under the ribbon */
    z-index: 1;

    /* Background */
    border: 1rem solid #d1d5db;
}

.ribbon__side--l {
    /* Position */
    left: -1.5rem;
    border-color: #d1d5db #d1d5db #d1d5db transparent;
}

.ribbon__side--r {
    /* Position */
    right: -1.5rem;
    border-color: #d1d5db transparent #d1d5db #d1d5db;
}

我们知道父元素是相对定位(position:relative),而这里的两个元素采用了绝对定位,而共同点是bottom:0.5rem;然后一个向左,一个向右,分别都是-1.5rem;最后都设置border为1rem;并且border有三个方向有颜色,而一个方向透明,显然,如果是左边支出的那部分,显然是左边的border-color为透明的颜色,按照上右下左的原理,border-color: #d1d5db #d1d5db #d1d5db transparent;的最左边的颜色为透明。同样的到来,右边支出的部分应该是右方的颜色为透明,同理按照上右下左的原理,border-color: #d1d5db transparent #d1d5db #d1d5db;最右边的颜色为透明。

至于这里为什么这样设置border就会出现这样的形状呢?可以参考这篇文章(css绘制三角形原理-border)

简单说明一下:

.t2 {
  width: 50px;
  height: 50px;
  border: 2px solid orange;
}

我们使用border的时候一般只是小小的一段,大家可能会觉得border是有四个小矩形拼接而成;然而实际上呢?请看下面的例子:

.t2 {
  width: 100px;
  height: 100px;
  border: 40px solid;
  border-color: orange blue red green;
}

我们可以看到明显的border的四条边不是长方形,而是梯形,很像相框的四条边

我们再思考一下极端的情况,我将这个dom的width和height都设置为0;

.t2 {
  width: 0;
  height: 0;
  border: 40px solid;
  border-color: orange blue red green;
}

大家看到了没,实际上是不是就是四个不同颜色的小三角形拼接而成的,如果你想实现其他的效果,是不是只需要让其中的某些三角形的颜色透明就OK啦。

好啦,我上面的例子实际上是不是就是让左边的三角形透明就达到效果啦。

这里有个问题,为什么在ribbon的css中没有设置宽度和高度;其实是这样的,没有设置宽度和高度的时候,默认是auto,并且这个dom中也没有文字其他的,它的实际宽度和高度其实就是0;

如果大家感兴趣的话,可以在<div class="ribbon__side ribbon__side--l"></div>这个dom中添加一些内容,此时这个dom结构会变大,而图形也将不再是原来的结构,如图所示:

这时,如果将此dom结构的宽度和高度设置为0;

.ribbon__side {
    bottom: -0.5rem;
    position: absolute;

    /* Displayed under the ribbon */
    z-index: 1;

    /* Background */
    border: 1rem solid #d1d5db;
	width: 0;
	height:0; 
}

此时图形恢复原样,但是内容溢出;因此如果你想在这个里面显示一些内容,最好将容器设置大一点,这样内容才不会溢出。

有了这个使用border来构建三角形的基础知识,那么在这个ribbon结构中最下面的两个黑色的三角形,不用我解释您也能轻松理解啦。

如果有不理解的地方,可以参考这篇文章,也可以留言讨论学习。

 

javascript——嵌套函数作用域

更新于 2022.12.16 4分钟阅读 0 评论 5 推荐

    作者:

javascript作用域

我们知道,js中有三个作用域,分别是block scope(块作用域),function scope(函数作用域),globle scope(全局作用域);

今天我们来看看什么是嵌套函数作用域

嵌套函数作用域

let a = 10;
function outer() {
	let b = 20;
	function inner() {
		let c = 30;
		console.log(a,b,c);
	}
	inner();
}
outer();
// 10 20 30

我们来分析一下,在outer()函数内部定义了一个 inner()函数。在执行outer()函数的时候,会执行inner()函数,这个时候在打印a变量的时候,js引擎会在当前的函数作用域(inner函数)中查找是否有a变量,如果没有,引擎会继续在上级函数(outer函数)中查找,此时任然没有,那么就在全局作用域查找,此时有值,因此就打印出a的值,如果此时还没有值,就打印出undefined。同样的道理对于b和c都是如此。

 

闭包(closure)

我们先看看闭包的定义:

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function's scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time.

这个定义记住关键一点,它是在运行时产生的,一定是在函数创建时刻才有闭包

function outer() {
	let counter = 0;
	function inner() {
		counter++;
		console.log(counter);
	}
	inner();
}
outer();
outer();
// 1 1 
function outer() {
	let counter = 0;
	function inner() {
		counter++;
		console.log(counter);
	}
	return inner;
}
let fn = outer();
fn();
fn();
// 1 2 

理解了上面两个例子的区别,您就理解了闭包,

1、闭包一定是在函数创建时候生成的。

2、闭包可以产生私有变量。

新建章节

发布于 2022.12.15 0分钟阅读 0 评论 10 推荐

    作者:

JPA实体状态学习-(瞬时态:Transient)

更新于 2022.12.14 14分钟阅读 0 评论 5 推荐

    作者:

为了学习实体的状态,我们还是贴出这张实体状态转换迁移图:

Transient(瞬时态)

按照上图的描述,java对象在内存中被赋值后,没有调用entityManager.persist()方法之前实体对象所处的状态就是瞬時態

举个例子:

Teacher teacher = new Teacher("email@dot.com");

此时,实例teacher就处于new/transient态(备注:这里的new和transient是同一个意思

 

Persistent(持久态)

掌握这个状态,需要记住一句关键的话:

An Object that is associated with persistence context (hibernate session) are in Persistent state. Any changes made to objects in this state are automatically propagated to databases without manually invoking persist/merge/remove

与持久上下文关联的对象处于持久态。任何对这个对象属性的更改都会自动传播到数据库而不需手动调用persist/merge/remove方法。(备注:这里的关联的意思就是说实体对象被持久上下文管理,也有的翻译将这个状态称作托管态)

代码举例:

这里我们还是用代码来说明:

**
 * 测试保存,验证持久态的数据更新
 */
public void saveTeacher() {
    Teacher teacher = new Teacher("new_email@gmail.com");
    logger.info("teacher: {} is transient state",teacher);
    EntityManager entityManager = entityManagerFactory.createEntityManager();
    EntityTransaction tx = entityManager.getTransaction();
    tx.begin();
    entityManager.persist(teacher);
    logger.info("teacher: {} is persistent state",teacher);
    teacher.setEmail("updated_email@gmail.com");
    Long persistedId = teacher.getId();
    logger.info("teacher: {} has changed email",teacher);
    tx.commit();
    entityManager.close();
    logger.info("teacher: {} is detached state",teacher);
    entityManager = entityManagerFactory.createEntityManager();
    tx = entityManager.getTransaction();
    tx.begin();
    teacher = entityManager.find(Teacher.class, persistedId);
    tx.commit();
    entityManager.close();
    logger.info("Persisted teacher: {}", teacher);
}

下面截取出相应的日志信息来分析问题:

teacher: Student{id=null, email='new_email@gmail.com'} is transient state
Hibernate: 
   call next value for hibernate_sequence
teacher: Student{id=1, email='new_email@gmail.com'} is persistent state
teacher: Student{id=1, email='updated_email@gmail.com'} has changed email
Hibernate: 
   insert 
   into
       teachers
       (email, id) 
   values
       (?, ?)
Hibernate: 
   update
       teachers 
   set
       email=? 
   where
       id=?
teacher: Student{id=1, email='updated_email@gmail.com'} is detached state
Hibernate: 
   select
       teacher0_.id as id1_0_0_,
       teacher0_.email as email2_0_0_ 
   from
       teachers teacher0_ 
   where
       teacher0_.id=?
Persisted teacher: Student{id=1, email='updated_email@gmail.com'}

日志分析:

以上的日志说明了一个teacher对象从transient----->persistent----->detached 过程中,SQL语句的执行情况:

执行persist()到持久态后执行一条insertSQL语句;

当改变持久态对象的属性时会执行一条update语句,这两条语句显然都是tx.commit()后才发出;

最后发出一个select语句来查询数据库中最终的teacher对象,用于验证之前的结论。

管理这一过程的就是持久化上下文。

我们测试在detached态时修改对象的状态有啥不一样:

/**
 * 测试保存,验证持久态的数据更新
 */
public void saveTeacher() {
    Teacher teacher = new Teacher("new_email@gmail.com");
    logger.info("teacher: {} is transient state",teacher);
    EntityManager entityManager = entityManagerFactory.createEntityManager();
    EntityTransaction tx = entityManager.getTransaction();
    tx.begin();
    entityManager.persist(teacher);
    logger.info("teacher: {} is persistent state",teacher);
    teacher.setEmail("updated_email@gmail.com");
    Long persistedId = teacher.getId();
    logger.info("teacher: {} has changed email",teacher);
    tx.commit();
    //增加了这一行代码后
    teacher.setEmail("updated_email1@gmail.com");
    entityManager.close();
    logger.info("teacher: {} is detached state",teacher);
    entityManager = entityManagerFactory.createEntityManager();
    tx = entityManager.getTransaction();
    tx.begin();
    teacher = entityManager.find(Teacher.class, persistedId);
    tx.commit();
    entityManager.close();
    logger.info("Persisted teacher: {}", teacher);
}

这里我只截取部分输出,能够看到结果表明原理就好

teacher: Student{id=1, email='updated_email1@gmail.com'} is detached state

Persisted teacher: Student{id=1, email='updated_email@gmail.com'}

可以知道,detached状态的对象属性email已经改变成updated_email1@gmail.com,但是从数据库查询数来的值没有改变,还是updated_email@gmail.com

结论说明 

  • Detached状态的实体属性改变不会同步更新数据库
  • Persistent状态的实体属性改变会同步更新数据库

下一篇,我会尝试用代码来说明其他实体状态的迁移,欢迎大家多多分享和交流。

 

JPA实体状态深入理解

更新于 2022.12.14 0分钟阅读 0 评论 5 推荐

    作者:

我们在学习JPA实体状态的时候,常常会问,JPA的实体有多少状态呢?相信这个问题不难回答:

  • 瞬时态(transient)
  • 托管态(persistent)
  • 游离态(detached)
  • 移除态(removed)

注意:这里最后一个移除态,有的时候也叫删除态(deleted),至于它和移除态有啥区别,暂时没有想到,如果您对此有更加深刻的理解,请留言回复。

为什么会有这四种状态呢?

啥,这个也有为啥,网上不是都这么说的,你怎么会提出这么个奇怪的问题?其实不然,我们对一个事物的理解,不仅要理解表象,更要深入理解本质:

这里我说说自己的一些浅见:

 

首先,一个实体对象从创建到持久化的数据库,必然会有自己的生命周期,而生命周期是由一些状态构成的,因此,我理解的一个结论是:实体状态是对实体对象生命周期的一个抽象。

其次,想到了状态的变化,您想到了啥?对了,就是状态机。想到深入理解实体状态的变化,其实就是掌握实体生命周期状态的迁移。

以上图片就是实体生命周期中状态的迁移变化图。

我这里有一个小小的心得就是,如果在使用JPA的时候遇到问题,我的第一反应就是,当前我操作的实体属于生命周期的哪个状态?然后再排查其他情况。

平时我们一般使用JPA比较多,而JPA的实现默认是hibernate实现的,因此为了理解实体的状态转化图,在下面贴出了另外两张图来提供给大家参考:

JPA状态转化图:

Hibernate状态转化图:'

最后,为啥一定需要四种状态来管理实体对象的生命周期呢?removed和detached有啥区别呢?我们留待下一篇通过代码来学习讨论。我们会使用Hibernate来作为例子进行讨论。

如何集成多个限界上下文?

发布于 2023.02.28 0分钟阅读 0 评论 5 推荐

    作者:

在进行领域驱动设计时,发现有两个情境很容易产生误解?他们是:多个限界上下文集成聚合设计。在本文中,主要介绍多个限界上下文集成。

通用语言

任何研究过 DDD 的人都已经了解到与其一起使用的许多概念:限界上下文、实体、聚合、值对象等。其中一些概念(实体,聚合,值对象)与代码直接关联,很容易理解和使用,而另一些(限界上下文)则由于其抽象性而难以吸收应用。通用语言就是这样一种情况,尽管它简单而强大,但很容易被忽视或遗忘。

通用语言的目标是建立一种明确的交流方式或语言,用于专家和开发人员之间的交流。这意味着无论是否以代码表示的所有概念,例如类和进程,都不应随意命名或从以开发人员为中心的角度来看,而是集中讨论您的软件解决方案如何尝试解决给定的问题。

也许一个典型的例子是我们倾向于将使用系统的个人称为用户,而领域专家将其称为客户。或者倾向于将我们操纵的概念称为 SomethingData 或 SomethingInformation。

这种看似无害的差异是可能导致混乱或成为“破电话游戏”的一个例子,在这种情况下,您的意图从业务预期到实际实施的时候,意图就会丢失。

达成正确的通用语言上是很强大的,因为它可以帮助您将隐含的定义明确化,这总是一件好事。想象一下,您发现它确实是一个目的地地址,而不是调用某些运输信息。

现在让我们谈谈作用域。在编程语言中,作用域很容易理解为某个项目(例如变量)的程序作用域。在该作用域之外,无法保证存在相同的概念,甚至无法保证具有相同的价值/意义。当我们将此讨论带到我们的非编程语言时,我们承认我们精心设计的语言存在局限性。这意味着可以甚至鼓励不追求在其所有应用程序中使用的概念的通用企业作用域定义。要定义此类限制并探索为什么需要这样做,有必要讨论限界上下文。

让我们设定一些边界

在讨论限界上下文 (BC) 的概念时,Eric Evans 表示我们应该“明确定义模型应用的上下文。 在团队组织、应用程序特定部分内的使用以及代码库和数据库模式等物理表现方面明确设置界限。 在这些范围内保持模型严格一致,但不要被外部问题分散注意力或混淆”。

该声明揭示了一个事实,即我们的模型以及最终定义它们的语言的有效性是有限的。它还强调,有些事情“超出”我们不应该“分心”的界限。虽然讨论如何定义这些边界超出了本文的范围,但我想重点关注限制的存在,如图 1 所示。

Figure 1. Different Bounded Contexts and their relationships.

在此图中,我们注意到以下特征:

1、限界上下文之间可能有也可能没有关系

2、从一个限界上下文到另一个模型可以具有相同的名称但不同的定义

3、模型在一个限界上下文和另一个限界上下文之间可以有不同的名称,但具有一些或所有共同特征

对于没有任何关系的限界上下文,除了重申一下,如果你在两者中找到相同的模型名称就可以了,其他没什么好说的。假设这是仔细决定/迭代的结果。

另一方面,有共享关系的限界上下文,让我们看一下上下文映射,它提供了关于它们的关系如何影响它们的模型和语言的额外信息。

Upstream/Downstream

Figure 2. An upstream/downstream relationship.

在这里,上游是决定关系的人,使下游采用该语言作为自己的一部分。在我们的示例中,不仅在代码中使用了 Customer Profile,而且还被识别为 Checkout 限界上下文的概念部分。

Anti-Corruption Layer

Figure 3. An upstream/downstream with an anti-corruption layer.

在此示例中,Warehouse 不需要从产品信息管理 (PIM) 导入产品定义,而是定义一个本地概念,该概念具有所需信息的子集。

让我们从实现的角度仔细看看这些例子。

Connecting the Dots

我们的第一个实现将说明如何表示上游/下游关系。代码和讨论将集中在集成方面,而不是其他与 DDD 相关的方面。

我们虚构的 Customer Profile 定义了一个名为 Ordering Preferences 的模型,供 Checkout 使用。想象一下,作为其运营的一部分,它需要了解客户的预定义决定,以加快交付所有购买的商品。

class PrepareCheckoutCommandHandler {
    // other dependencies would also be injected here
    constructor(private readonly orderingPreferenceService: OrderingPreferenceService) {
    }

    // omitting error handling aspects
    async handle(command: PrepareCheckoutCommand): Promise<CheckoutDTO> {
        // obtain the ordering preferences for a given customer
        const orderingPreferences = await this.orderingPreferenceService.getOrderingPreferences(command.customerId);
        // ... potentially call other dependencies
        const checkout = new Checkout(command.customerId, command.cart, orderingPreferences);
        return toDTO(checkout);
    }       
}

我们的应用程序服务——命令处理程序——需要为试图启动结账流程的客户获取订购偏好。有了这些信息,就可以创建一个 Checkout 来封装预期要遵循的业务规则。

所以 OrderingPreferences 是一个本地概念,从 Customer Profile 导入并通过 OrderingPreferencesService 获得。一种常见的方法是将其表示为一个接口。

interface OrderingPreferencesService {
    getOrderingPreference(customerId: string): OrderingPreference;
}

在我们的基础设施层中,我们有这样一个接口的具体实现,它实际上与客户资料交互并产生预期值对象。

class CustomerProfileOrderingPreferencesService implements OrderingPreferencesService {
    constructor(private readonly httpClient: HttpClient) {
    }

    async getOrderingPreferences(customerId: string): Promise<OrderingPreferences> {
        const response = await this.httpClient.get(`/customers/${customerId}/ordering-preferences`);
        return OrderingPreferences.create(response.data);
    }
}

在这里我们看到我正在使用某种 HTTP 客户端发出此请求以检索数据,此时它只是一个数据集合。然后我使用该数据创建 OrderingPreferences,这是公认的本地概念。

现在让我们看看我们的第二个实现,它旨在说明您何时不导入概念,而是必须根据外部定义构建一个本地概念。

我们有一个仓库操作,为了处理和运送客户订单,要求我们提供一些用于海关目的的监管信息。与下订单时收到的上下文信息相反,这些额外的信息位于产品信息管理 (PIM) 的另一个限界上下文中。在 PIM 中,此信息是产品模型的一部分。

Figure 4. Free Trade Agreement made up of portions of the Product definition.

经过讨论,决定在当地没有必要有相同的产品概念,所需的信息被称为自由贸易协定。

像以前一样,我们有一个应用程序服务,它注入了依赖项。

class CreateOutboundHandler {
    constructor(private readonly freeTradeAgreementService: FreeTradeAgreementService) {
    }

    async handle(command: CreateOutboundCommand): Promise<OutboundDTO> {
        const freeTradeAgreement = await this.freeTradeAgreementService.getFromProduct(command.productId);
        const outbound = new Outbound(command.productId, command.quantity, freeTradeAgreement);
        return toDTO(outbound);
    }
}

首先要提到的是,您没有将 PIM 的产品定义公开给您的应用程序。它期望一个自由贸易协定值对象作为输出。和以前一样,一种方法是定义一个具有具体基础设施实现的接口。

// In your application
interface FreeTradeAgreementService {
    getFromProduct(productId: string): FreeTradeAgreement;
}

// In your infrastructure
class TranslatingFreeTradeAgreementService implements FreeTradeAgreementService {
    constructor(private readonly adapter: FreeTradeAgreementAdapter) {
    }

    async getFromProduct(productId: string): Promise<FreeTradeAgreement> {
        // More complex situations could require multiple calls to separate services in order to
        // build the local concept
        return await this.adapter.getFreeTradeAgreement(productId);
    }
}

TranslatingFreeTradeAgreementService 的前缀旨在明确此服务的作用,即采用外部概念并生成遵循本地语言的内容。

至于信息的实际检索及其操作,一种推荐的方法是在适配器和翻译器之间拆分职责,前者提出实际请求,后者负责验证并将必要的信息传递给您 值对象。

class FreeTradeAgreementAdapter {
    constructor(private readonly httpClient: HttpClient, private readonly translator: FreeTradeAgreementTranslator) {
    }

    getFreeTradeAgreement(productId: string): FreeTradeAgreement {
        const response = this.httpClient.get(`/products/${productId}`);
        return this.translator.translate(response.data);
    }
}

class FreeTradeAgreementTranslator {
    
    async translate(product: any): FreeTradeAgreement {
        // do some validation, conversion from string to number, etc.
        return FreeTradeAgreement.create(product.limitAmount, product.percentage);
    }
}

很好,但我需要所有这些吗?

使用的结构肯定有很多部分:接口、具体的翻译实现、适配器和转化器。您的第一反应可能是认为它在现实生活中没有用处太多或太复杂。最后,决定将取决于您和您采用的开发生态系统、编程语言和工具。

这里简要总结了每个组件的用途和一些实际注意事项。

The Service Interface

如果您知道并相信 SOLID 原则可以促进良好实践,那么该界面可以帮助您专注于意图,而不是被迫预先对太多实施细节做出决定。

一些开发语言允许您动态替换现有的实现以方便测试,这可以说可以让您跳过接口定义。

The Translating Service

这里的目标是确保您的翻译服务知道它需要联系哪些服务才能返回您的应用程序期望的本地概念。

它依靠一个或多个适配器来执行其命令,更像是一个编排器。

The Adapter

它是实际使用基础设施、http、gRPC 等从外部限界上下文获取数据并将其传递给转化器的一种。

如果您只有一个 BC 要联系,一个潜在的简化方法是直接从翻译服务进行远程呼叫和转化。

The Translator

获取从外部限界上下文接收的数据,验证它是否包含我们需要的结构,并使用它来创建所需的值对象。

转化中不应该存在任何实际的业务规则。在这里,您要验证预期存在的数据是否确实存在,丢弃我们不需要的数据,必要时在数据中执行最小转换,并将其传递给值对象以进行实际的域验证。

由于您将不得不编写代码来执行此操作,如果您想避免创建单独的类,您可以将此功能分组为适配器的内部部分。

从视觉上看,这是所有部分之间的关​​系:

我的建议是遵循垂直开发方法并不断重构您的代码。随着您的进步和获得更多关于所需集成的知识,这种功能分解将变得更加容易。

总结

集成多个限界上下文更像是一种规范而不是例外,因此花时间了解可以维护这些关系的多种形式是必不可少的。正如我们所看到的,这不是“魔法”,但确实需要一些时间来了解您如何进行这种集成,而不仅仅是技术方面。

如果您将依赖同步或异步通信,REST 或 gRPC 是重要且必要的方面,通常应推迟到您实际需要做出决定时再考虑。而且,这是在了解关系类型和将采用的(领域)语言之后发生的。

JUnit——Ignore测试

发布于 2022.12.05 7分钟阅读 0 评论 5 推荐

    作者:

有时我们的代码在运行测试用例时没有完全准备好。结果,测试用例失败。 @Ignore 注释在这种情况下会有所帮助。

带有@Ignore 注解的测试方法将不会被执行。

如果一个测试类被@Ignore注解,那么它的任何测试方法都不会被执行。

创建测试用例类

创建一个 java 测试类,例如 TestJunit.java

将测试方法 testPrintMessage()testSalutationMessage() 添加到测试类。

在方法 testPrintMessage() 中添加一个注解 @Ignore

import org.junit.Test;
import org.junit.Ignore;
import static org.junit.Assert.assertEquals;

public class TestJunit {

   String message = "Robert";	
   MessageUtils messageUtil = new MessageUtils(message);
   
   @Ignore
   @Test
   public void testPrintMessage() {
      System.out.println("Inside testPrintMessage()");
      message = "Robert";
      assertEquals(message,messageUtil.printMessage());
   }

   @Test
   public void testSalutationMessage() {
      System.out.println("Inside testSalutationMessage()");
      message = "Hi!" + "Robert";
      assertEquals(message,messageUtil.salutationMessage());
   }
}

创建测试运行器类

import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;

public class TestRunner {
   public static void main(String[] args) {
      Result result = JUnitCore.runClasses(TestJunit.class);

      for (Failure failure : result.getFailures()) {
         System.out.println(failure.toString());
      }
		
      System.out.println(result.wasSuccessful());
   }
}  	

输出如下:

Inside testSalutationMessage()
Hi!Robert
true

可以看到在方法上加了@Ignore的测试用例没有执行

下面我们看看@Ignore注释用在一个类上的情况,更新测试用例类如下:

import org.junit.Test;
import org.junit.Ignore;
import static org.junit.Assert.assertEquals;

@Ignore
public class TestJunit {

   String message = "Robert";	
   MessageUtils messageUtil = new MessageUtils(message);
     
   @Test
   public void testPrintMessage() {
      System.out.println("Inside testPrintMessage()");
      message = "Robert";
      assertEquals(message,messageUtil.printMessage());
   }

   @Test
   public void testSalutationMessage() {
      System.out.println("Inside testSalutationMessage()");
      message = "Hi!" + "Robert";
      assertEquals(message,messageUtil.salutationMessage());
   }
	
}

再次运行一下,可以知道一个测试类也没有运行,只是输出了一个true

true

简单吧

 

SVM应用

发布于 2022.12.02 1分钟阅读 0 评论 10 推荐

    作者:

from sklearn.model_selection import train_test_split
from sklearn.model_selection import GridSearchCV

linear_svc = LinearSVC()
X_train, X_test, y_train, y_test = train_test_split(X,y)

#define a list of parameters
params_svc = {'C': [0.001, 0.01, 0.1, 1, 10, 100]}

grid_svc = GridSearchCV(linear_svc, params_svc, cv=5, n_jobs=2,scoring = 'roc_auc', return_train_score=True)
grid_svc.fit(X_train, y_train)

print('train score: ', grid_svc.score(X_train, y_train))
print('test score: ', grid_svc.score(X_train, y_train))
print(grid_svc.best_params_)

JUnit——Suite测试

发布于 2022.12.01 4分钟阅读 0 评论 5 推荐

    作者:

Test Suite用于捆绑一些单元测试用例并将它们一起运行。在 JUnit 中,@RunWith 和@Suite 注释都用于运行suite test。本文以具有两个测试类 TestJunit1 和 TestJunit2 的示例为例,它们使用测试套件一起运行。

创建一个要测试的 java 类,比如 MessageUtil.java

/*
 * This class prints the given message on console.
 */

public class MessageUtil {

   private String message;

   //Constructor
   //@param message to be printed
   public MessageUtil(String message){
      this.message = message; 
   }

   // prints the message
   public String printMessage(){
      System.out.println(message);
      return message;
   }   

   // add "Hi!" to the message
   public String salutationMessage(){
      message = "Hi!" + message;
      System.out.println(message);
      return message;
   }   
}  

创建一个名为 TestJunit1.java 的 java 类文件

import org.junit.Test;
import org.junit.Ignore;
import static org.junit.Assert.assertEquals;

public class TestJunit1 {

   String message = "Robert";	
   MessageUtil messageUtil = new MessageUtil(message);
   
   @Test
   public void testPrintMessage() {	
      System.out.println("Inside testPrintMessage()");    
      assertEquals(message, messageUtil.printMessage());     
   }
}

创建一个名为 TestJunit2.java 的 java 类文件

import org.junit.Test;
import org.junit.Ignore;
import static org.junit.Assert.assertEquals;

public class TestJunit2 {

   String message = "Robert";	
   MessageUtil messageUtil = new MessageUtil(message);
 
   @Test
   public void testSalutationMessage() {
      System.out.println("Inside testSalutationMessage()");
      message = "Hi!" + "Robert";
      assertEquals(message,messageUtil.salutationMessage());
   }
}

创建测试套件类

创建一个java类。 

在类上附加 @RunWith(Suite.class) 注解。 

使用 @Suite.SuiteClasses 注释添加对 JUnit 测试类的引用。

创建一个名为 TestSuite.java 的 java 类文件

import org.junit.runner.RunWith;
import org.junit.runners.Suite;

@RunWith(Suite.class)

@Suite.SuiteClasses({
   TestJunit1.class,
   TestJunit2.class
})

public class JunitTestSuite {   
}  	

创建测试运行器类

创建一个名为 TestRunner.java 的 java 类文件

import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;

public class TestRunner {
   public static void main(String[] args) {
      Result result = JUnitCore.runClasses(JunitTestSuite.class);

      for (Failure failure : result.getFailures()) {
         System.out.println(failure.toString());
      }
		
      System.out.println(result.wasSuccessful());
   }
}  

输出如下:

Inside testPrintMessage()
Robert
Inside testSalutationMessage()
Hi Robert
true