基于浏览器渲染的组件测试

news2024/12/20 13:12:22

目录

为什么需要自动化测试

测试的类型

组件测试的方式

白盒测试

黑盒测试

灰盒测试

推荐的方案

Playwright 组件测试案例

Playwright 简介

playwright 架构图

BrowserContext

组件测试原理

组件引入

模型封装

组件渲染测试

组件 Props 测试

组件 Events 测试

组件 Slots 测试

快捷键测试

Cypress 组件测试案例

Cypress 简介

开始

组件引入

模型封装

组件渲染测试

组件 Props 测试

组件 Events 测试

组件 Slots 测试

快捷键测试

总结


为什么需要自动化测试

自动化测试是利用计算机去检查软件是否正常运行的方法,自动化测试一旦被创建,他可以不会吹灰之力的进行无数次重复测试。自动化测试能够预防无意引入的 bug,并鼓励开发者将应用分解为可测试、可维护的函数、模块、类和组件。

在手工测试过程中,我们应该都经历过或者看到过类似的问题:

  • 版本发布时需要花好几个小时甚至几天来对我们的应用进行测试,其中老功能的测试占了不少比例
  • 应用功能越来越庞大参与人越来越多后,实现一个小的 feature 或者是改一个 BUG,你会变的越来越小心翼翼,总会担心这会不会影响到其他功能
  • 代码重构总是伴随着大量的回归测试

通过自动化测试,可以有效帮助团队改善这些问题,让你的团队更快速、自信地构建复杂的应用。

注意,并不是所有应用都有这样的特点与对应的问题存在,比如 C 端的各种活动页面,应用的生命周期短且开发周期也非常短,做自动化就是完全没必要的。

测试的类型

以下更多的是从前端的视角来对我们可能进行的自动化测试进行分类

  • 单元测试:检查给定函数、类或组合式函数的输入是否产生预期的输出或副作用。
  • 组件测试:检查你的组件是否正常挂载和渲染、是否可以与之互动,以及表现是否符合预期。这些测试比单元测试导入了更多的代码,更复杂,需要更多时间来执行。
  • UI 测试:检查 UI 界面是否符合预期,往往会通过 Mock 解决对于后端的依赖。
  • 端到端测试:检查跨越多个页面的功能,并对生产构建的应用进行实际的网络请求。这些测试通常涉及到建立一个数据库或其他后端。(广义上的 UI 测试,也可以认为是端到端测试)

组件测试的方式

白盒测试

白盒测试知晓一个组件的实现细节和依赖关系。它们更专注于将组件进行更独立的测试。这些测试通常会涉及到模拟一些组件的部分子组件,以及设置插件的状态和依赖性。

通常各种开源 UI 组件库的测试实现(如ant-design),就更接近于这里表达的白盒测试类型,他们一般会使用 Jest、Vitest 这样的测试框架。

黑盒测试

黑盒测试不知晓一个组件的实现细节。这些测试尽可能少地模拟,以测试组件在页面中工作的真实情况。如果你的组件包含了多个子组件,比如你的业务里面自己封装了一个树状穿梭框 TreeTransfer 组件,它包含了一个 Tree 组件与一个 List 组件,那我们只会选择对把他们集成起来的 TreeTransfer 组件进行测试,而不是单独测试 Tree 与 List 组件。

灰盒测试

介于白盒与黑盒之间,不仅关注组件表面的输入输出正确性,同时也关注组件内部的情况。不像白盒测试那样详细完整,但又比黑盒测试更关注内部逻辑,常常通过一些表征性的现象、事件、标志去判断内部状态。

Playwright与Cypress推出的 Components Testing 功能,在真实的浏览器中去渲染组件然后对它进行自动化测试,就是一种更接近于组件黑盒测试或者灰盒测试的体现。

推荐的方案

我们更加推荐使用真实的浏览器去渲染我们的组件进行黑盒或灰盒测试,越接近用户使用方式的测试时是越可信的测试。同时 Jest 等测试框架通过 jsdom 去模拟生成 Dom 的方案也有着很多局限性,比如一些与宽高计算相关的功能测试它们就无法实现。

我们选择当前最热门的两个端到端测试框架Playwright(41.8k Star)Cypress(40.4K)针对于 Components Testing 功能去进行对比,他们都能够在真实地浏览器中渲染组件进行组件测试。

Playwright 组件测试案例

Playwright 简介

Playwright 是 2020 年微软推出的一个专门用来做 Web 应用的测试与自动化的框架,他们够通过同一套 API 去测试你的 Web 应用运行在 Chromium, Firefox and WebKit 浏览器的情况。支持使用 Jascript/TypeScript/Java/Python/.NET 语言。

更多的特点以及介绍可以查看官方文档首页。

与 Puppeteer(一个使用 NodeJS 操作浏览器的库)的关系与差异:

它的开发团队来自于 Puppeteer,Puppeteer 受到了广泛的欢迎,他们决定把他发扬广大,适用在更多的浏览器上,并且充分借鉴它的优点与踩过的坑,做出大量新的设计,也产成了很多破坏性的变更,于是有了 Playwright。

playwright 架构图

 @playwright/test 里面实现了测试用例的运行、断言、测试报告生成等功能

@playwright/test 中的自动化测试代码会调用到 Playwright 中各种操作浏览器的 API

而 playwright 通过Chrome DevTools Protocol去控制浏览器,使用 Websocket 进行通信,整个过程中不会出现频繁启动浏览器与建立通信的情况

BrowserContext

存在于 browser 与 page 之间,每个 browser 可以创建出多个完全独立的 context,context 的创建速度快且资源消耗少,每个 context 可以创建多个 page。

通过这个设计,我们可以去操作多个 session 独立的浏览器上下文,同时在每次运行测试时也可以做到只启动一次浏览器,每一个 test case 都使用一个独立 context 去进行测试。

组件测试原理

 开始

参考官方文档中的How to get started

在下面两个仓库也提供了通过 Playwright 搭建的组件测试框架,下面所有演示的完整代码都可以在里面找到

  • Vue2 Playwright Components Testing
  • Vue3 Playwright Components Testing

组件引入

// playwright/index.ts 在Vue2中引入组件
// Import styles, initialize component theme here.
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
Vue.use(ElementUI);

// playwright/index.ts 在Vue3中引入组件
// Import styles, initialize component theme here.
import { beforeMount } from '@playwright/experimental-ct-vue/hooks';
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';

beforeMount(async ({ app }) => {
  app.use(ElementPlus);
});

除了全局引入,也可以选择在测试文件中按需引入对应的被测试组件。

模型封装

类似于端到端测试中的页面对象模型,我们对组件测试过程中的通用行为、逻辑进行封装,来达到简化逻辑与代码复用的目的。

const useSelect = (ct: Locator, page: Page) => {
  const pickSelectOption = async ({ text, nth }: { text?: string; nth?: number }) => {
    if (text) {
      await page.locator(`.el-select-dropdown:visible .el-select-dropdown__item :text-is("${text}")`).click();
    } else {
      await page.locator(`.el-select-dropdown:visible .el-select-dropdown__item >> nth=${nth}`).click();
    }
  };

  const openPopover = async () => {
    await ct.locator('.el-input').click();
    // 等待popover动画执行完毕
    // eslint-disable-next-line playwright/no-wait-for-timeout
    await page.waitForTimeout(400);
  };

  return {
    pickSelectOption,
    openPopover,
  };
};

组件渲染测试

我们推荐使用视觉对比去测试组件渲染是否符合预期。

 
<!-- Select.vue -->
<template>
  <el-select v-bind="propsParams" v-model="value" placeholder="请选择" v-on="eventsParams">
    <el-option
      v-for="item in options"
      :key="item.value"
      :label="item.label"
      :value="item.value"
      :disabled="item.disabled"
    >
    </el-option>
  </el-select>
</template>

<script>
export default {
  props: {
    // component props
    propsParams: {
      type: Object,
      default: () => ({}),
    },
    // component events
    eventsParams: {
      type: Object,
      default: () => ({}),
    },
    // custom props
    defaultValue: {
      type: String,
      default: '',
    },
    options: {
      type: Array,
      default: () => [],
    },
  },
  data() {
    return {
      value: this.defaultValue,
    };
  },
};
</script>

test('mount work', async ({ page, mount }) => {
  const ct = await mount(SelectBase, {
    props: {
      // custom props
      options: baseOptions,
    },
  });
  const { openPopover } = useSelect(ct, page);

  await openPopover();

  // Visual comparisons
  // allow 5% pixe ratio diff
  await expect(page).toHaveScreenshot({ maxDiffPixelRatio: 0.5 });
});

组件 Props 测试

test('single select work', async ({ page, mount }) => {
  const ct = await mount(SelectBase, {
    props: {
      options: baseOptions,
      defaultValue: baseOptions[0].value,
    },
  });
  const { pickSelectOption, openPopover } = useSelect(ct, page);

  await openPopover();
  await pickSelectOption({ text: baseOptions[1].label });

  await expect(ct.locator('.el-input input')).toHaveValue(baseOptions[1].label);
});

组件 Events 测试

test('event work', async ({ page, mount }) => {
  const messages: string[] = [];

  const ct = await mount(SelectBase, {
    props: {
      propsParams: {
        clearable: true,
      },
      eventsParams: {
        change: () => messages.push('change-trigger'),
        clear: () => messages.push('clear-trigger'),
        'visible-change': () => messages.push('visible-change-trigger'),
      },
      options: baseOptions,
    },
  });
  const { pickSelectOption, openPopover } = useSelect(ct, page);

  await openPopover();
  await pickSelectOption({ text: baseOptions[0].label });

  await ct.locator('.el-input').hover();
  await ct.locator('.el-icon-circle-close').click();

  expect(messages).toContain('change-trigger');
  expect(messages).toContain('clear-trigger');
  expect(messages).toContain('visible-change-trigger');
});

组件 Slots 测试

test('slots work', async ({ mount }) => {
  const ct = await mount(Button, {
    slots: {
      default: 'click me',
    },
  });

  await expect(ct).toContainText('click me');
});

test('jsx slots work', async ({ mount }) => {
  const ct = await mount(<el-button>click me</el-button>);

  await expect(ct).toContainText('click me');
});

快捷键测试

test('keyboard operations', async ({ page, mount }) => {
  const ct = await mount(SelectBase, {
    props: {
      options: baseOptions,
      defaultValue: baseOptions[0].value,
    },
  });
  const { openPopover } = useSelect(ct, page);

  await openPopover();

  await ct.locator('.el-input').press('ArrowDown');
  await ct.locator('.el-input').press('ArrowDown');
  await ct.locator('.el-input').press('Enter');

  await expect(ct.locator('.el-input input')).toHaveValue(baseOptions[1].label);
});

Cypress 组件测试案例

Cypress 简介

Cypress 是基于 JavsScript 的前端测试工具,可以对浏览器中运行的任何内容进行更快速简单可靠的测试,它可以用来编写所有类型的测试(端到端测试、接口测试、单元测试)。

更多的特点以及介绍可以查看Why Cyrpess的 Features 章节。

Cypress 架构图

 Cypress 的测试代码与被测试的 web 应用会直接在同一个浏览器中运行,不需要额外的驱动程序(如 WebDriwer),Cypress 对于被测试的 web 应用很强的控制能力。

在浏览器这个级别,Cypress 能够直接操作来自 web 应用的 Dom、Window、Local Storage、network 等内容。

浏览器之后是一个 Nodejs 进程,通过它启动浏览器后,与浏览器之间会使用 WebSocket 链接进行通信。同时在这里对于网络请求会进行代理控制,可以做到读取和更改网络请求等操作。

在操作系统级别,Cypress 通过 NodeJs 进程可以做到截图、录制视频、文件读写等操作。

开始

参考官方文档中的Quick Start vue

在下面的仓库也提供了通过 Cypress 搭建的组件测试框架,下面所有演示的完整代码都可以在里面找到

  • Vue3 Cypress Components Testing

组件引入

// cypress/support/component.ts 在Vue3引入全局组件,引入方式不友好,Vue3里面更推荐按需引入
import { mount } from 'cypress/vue'
import Button from '../../src/components/Button.vue'

Cypress.Commands.add('mount', (component, options = {}) => {
  // Setup options object
  options.global = options.global || {}
  options.global.components = options.global.components || {}

  // Register global components
  options.global.components['Button'] = Button

  return mount(component, options)
})

// cypress/support/component.ts 在Vue2引入全局组件
import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import { mount } from 'cypress/vue';

Vue.use(ElementUI);

Cypress.Commands.add('mount', mount);

模型封装

const selectModal = {
  pickSelectOption: ({ text, nth }: { text?: string; nth?: number }) => {
    if (text) {
      cy.get('.el-select-dropdown:visible .el-select-dropdown__item').contains(text).click();
    } else {
      cy.get(`.el-select-dropdown:visible .el-select-dropdown__item::nth-child(${nth})`).click();
    }
  },
  openPopover: () => {
    // 等待popover动画执行完毕
    cy.get('.el-input').click().wait(400);
  },
};

组件渲染测试

<template>
  <el-select v-bind="propsParams" v-model="value" placeholder="请选择" v-on="eventsParams">
    <el-option
      v-for="item in options"
      :key="item.value"
      :label="item.label"
      :value="item.value"
      :disabled="item.disabled"
    >
    </el-option>
  </el-select>
</template>

<script lang="ts" setup>
import { ref } from 'vue';
import { ElSelect, ElOption } from 'element-plus';

interface OptionItem {
  value: string;
  label: string;
  disabled?: boolean;
}

const props = withDefaults(
  defineProps<{
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    propsParams?: Record<string, any>;
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    eventsParams?: Record<string, any>;
    defaultValue?: string;
    options: OptionItem[];
  }>(),
  {
    propsParams: () => ({}),
    eventsParams: () => ({}),
    defaultValue: '',
    options: () => [],
  },
);

const value = ref(props.defaultValue);
</script>

it('mount work', () => {
  cy.mount(SelectBase, {
    props: {
      options: baseOptions,
    },
  });

  selectModal.openPopover();

  /**
   * 官方文档推荐的cypress-plugin-snapshots插件在cypress10.6.0使用时报错
   * 相关issue见:https://github.com/meinaart/cypress-plugin-snapshots/issues/215
   * 这里使用https://github.com/FRSOURCE/cypress-plugin-visual-regression-diff 来实现视觉对比
   */
  cy.matchImage();
});

组件 Props 测试

it('single select work', () => {
  cy.mount(SelectBase, {
    props: {
      options: baseOptions,
      defaultValue: baseOptions[0].value,
    },
  });

  selectModal.openPopover();
  selectModal.pickSelectOption({ text: baseOptions[1].label });
  cy.get('.el-input input').should('have.value', baseOptions[1].label);

组件 Events 测试

it('event work', () => {
  const messages: string[] = [];

  cy.mount(SelectBase, {
    props: {
      propsParams: {
        clearable: true,
      },
      eventsParams: {
        change: () => messages.push('change-trigger'),
        clear: () => messages.push('clear-trigger'),
        'visible-change': () => messages.push('visible-change-trigger'),
      },
      options: baseOptions,
    },
  });

  selectModal.openPopover();
  selectModal.pickSelectOption({ text: baseOptions[0].label });

  cy.get('.el-input').click();
  cy.get('.el-select__icon:visible').click();

  cy.wrap(messages)
    .should('include', 'clear-trigger')
    .should('include', 'change-trigger')
    .should('include', 'visible-change-trigger');
});

组件 Slots 测试

it('slot work', () => {
  cy.mount(ElButton, {
    slots: {
      default: () => <span>click me</span>,
    },
  });

  cy.get('button').should('have.text', 'click me');
});

it('jsx slot work', () => {
  cy.mount(() => <ElButton>click me</ElButton>);

  cy.get('button').should('have.text', 'click me');
});

快捷键测试

it('keyboard operations work', () => {
  cy.mount(SelectBase, {
    props: {
      options: baseOptions,
      defaultValue: baseOptions[0].value,
    },
  });

  selectModal.openPopover();

  cy.get('.el-input').type('{downArrow}');
  cy.get('.el-input').type('{downArrow}');
  cy.get('.el-input').type('{enter}');

  cy.get('.el-input input').should('have.value', baseOptions[1].label);
});

总结

在前端自动化测试中,组件测试是我们认为最具有性价比的测试类型,在对组件进行测试时对于其他服务基本没有依赖,可以独立进行测试,同时各种基础组件、业务组件也广泛的使用在我们的系统中的各个地方,通过自动化测试保障好它们的质量对于系统整体的质量提升有这明显的帮助。

组件测试方面两个测试框架功能层面的差距基本没有,如果你仅仅只考虑组件测试,那么这两个框架都是非常推荐的。



这篇贴子到这里就结束了,最后,希望看这篇帖子的朋友能够有所收获。

 获取方式:留言【777】即可免费获取

如果你觉得文章还不错,请大家 点赞、分享、留言 下,因为这将是我持续输出更多优质文章的最强动力!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/653606.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

运维数字化转型:用数字化思维重塑运维体系(文末送书五本)

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…

基于Java学生请假系统设计实现(源码+lw+部署文档+讲解等)

博主介绍&#xff1a; ✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战 ✌ &#x1f345; 文末获取源码联系 &#x1f345; &#x1f447;&#x1f3fb; 精…

FDTD 时域有限差分数值模拟方法与应用,COMSOL 多场耦合仿真技术与应用

专题一&#xff1a;COMSOL多物理场耦合 &#xff08;一&#xff09;案列应用实操教学&#xff1a; 案例一 光子晶体能带分析、能谱计算、光纤模态计算、微腔腔膜求解 案例二 类比凝聚态领域魔角石墨烯的moir 光子晶体建模以及物理分析 案例三 传播表面等离激元和表面等离…

Cat.4网络DTU,稳定快速的数据传输神器

好兄弟们&#xff01;你们有没有遇到过&#xff0c;半夜在家睡得正香&#xff0c;突然领导一个电话干过来告诉你设备数据传输中断了&#xff0c;让你赶紧看看怎么回事的情况。简直让人崩溃&#xff01; 在现代工业和物联网应用中&#xff0c;数据传输的稳定性和速度对于设备的运…

Python-Inspect.exe-uiautomation-基本操作-获取微信群成员信息

文章目录 1.Inspect.exe2.uiautomation使用2.1.简介和安装2.2.获取微信群成员昵称2.3.常用控件类型2.4.比较通用的属性2.4.窗口常见操作2.5.常见鼠标和键盘操作3.总结1.Inspect.exe 检查 (Inspect.exe) 是一种基于 Windows 的工具,可以选择任何 UI 元素并查看其辅助功能数据。…

ASEMI代理光宝光耦LTV-5314资料,LTV-5314规格书

编辑-Z 在电子设备的设计和制造过程中&#xff0c;光耦合器是一种至关重要的组件。它们在电路中起到隔离作用&#xff0c;防止电流反向流动&#xff0c;从而保护设备免受损坏。其中&#xff0c;光耦LTV-5314是一种广受欢迎的光耦合器&#xff0c;以其卓越的性能和可靠的稳定性…

MaxCompute-批量导出项目空间的建表语句(DDL)

MaxCompute-批量导出项目空间的建表语句&#xff08;DDL&#xff09; 项目背景 最近需要做项目空间的数据备份&#xff0c;包括表结构&#xff08;建表语句&#xff09;&#xff0c;以便在系统出现问题时&#xff0c;或者数据丢失时进行恢复。 所遇问题 前面我介绍过MaxCom…

【算法】原地哈希与快速幂

文章目录 一、原地哈希二、快速幂2.1 指数无负数2.2 指数有负数 一、原地哈希 直接看例题&#xff1a;题目链接 题目描述&#xff1a; 给你一个未排序的整数数组 nums &#xff0c;请你找出其中没有出现的最小的正整数。 请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间…

【机器学习】十大算法之一 “SVM”

作者主页&#xff1a;爱笑的男孩。的博客_CSDN博客-深度学习,活动,python领域博主爱笑的男孩。擅长深度学习,活动,python,等方面的知识,爱笑的男孩。关注算法,python,计算机视觉,图像处理,深度学习,pytorch,神经网络,opencv领域.https://blog.csdn.net/Code_and516?typeblog个…

python获取某乎热搜数据并保存成Excel

python获取知乎热搜数据 一、获取目标、准备工作二、开始编码三、总结 一、获取目标、准备工作 1、获取目标&#xff1a; 本次获取教程目标&#xff1a;某乎热搜 2、准备工作 环境python3.xrequestspandas requests跟pandas为本次教程所需的库&#xff0c;requests用于模拟h…

迟来的函数传参补充——传引用【引用调用】【c++】

文章目录 1、传引用1.1、特点1.2、使用1.2.1、一般引用1.2.2、常量引用 1.3、案例1.3.1、常见变量引用做函数参数1.3.2、结构体引用做函数参数 1、传引用 函数传参&#xff0c;几乎一直在用简单的值传递&#xff0c;或者传指针&#xff0c;前者生成一个源结构的副本&#xff0…

知识点滴 - 食物的寒热之分

昨晚多吃了写菠萝蜜&#xff0c;结果第二天就流鼻血了。以前吃晒干的龙眼&#xff0c;也流过鼻血。看来某些水果一次性吃太多&#xff0c;会有问题。 那就来研究研究水果的属性&#xff0c;识别哪些水果吃多了上火&#xff0c;哪些说过吃多了受寒。 中医认为&#xff0c;所有的…

sharding5.0.0分表分库

sharding官网参考 https://shardingsphere.apache.org/document/current/cn/overview/ https://shardingsphere.apache.org/document/legacy/4.x/document/cn/features/sharding/use-norms/pagination/ https://shardingsphere.apache.org/document/legacy/4.x/document/cn/d…

Hector SLAM Scan Matching 理解

Hector SLAM 参考https://www.cnblogs.com/cyberniklee/p/8484104.html 搞清楚几个点有助于对scan matching的理解 占用栅格地图中每个地图点包括点坐标、占用值最大为1&#xff0c;表示该栅格被占用的概率、以及占用值对坐标的梯度&#xff0c;由于地图点是离散的&#xff…

HashMap底层实现

首先来看一下put方法的源码&#xff0c;在HashMap中最重要的就是put方法的执行逻辑以及一些控制参数的意义比较重要。 【put方法】 问题1&#xff1a;如何计算数组位置&#xff1f; 答案&#xff1a; 1、首先在插入<K &#xff0c;V> 时&#xff0c;会先将其包装成一…

商城系统是如何分账的?-加速度shopfa

微信小程序凭借着其方便快捷&#xff0c;无需下载且依附于微信这个庞大的社交平台等特点&#xff0c;让无数的企业商家都为之痴迷&#xff0c;并都纷纷投入到了小程序的搭建工作&#xff0c;但你知道吗&#xff0c;通过小程序进行交易&#xff0c;资金的结算是十分复杂的&#…

使用单元测试框架unittest进行有效测试

一、介绍 在软件开发中&#xff0c;单元测试是一种测试方法&#xff0c;它用于检查单个软件组件&#xff08;例如函数或方法&#xff09;的正确性。Python 提供了一个内置的单元测试库&#xff0c;名为 unittest&#xff0c;可以用来编写测试代码&#xff0c;然后运行测试&…

基于Java图书管理系统设计实现(源码+lw+部署文档+讲解等)

博主介绍&#xff1a; ✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战 ✌ &#x1f345; 文末获取源码联系 &#x1f345; &#x1f447;&#x1f3fb; 精…

springboot+vue漫画网站(java项目源码+文档)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的漫画网站。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 &#x1f495;&#x1f495;作者&#xff1a;风歌&#…

Elasticsearch:实用 BM25 - 第 2 部分:BM25 算法及其变量

BM25算法 我将尽可能深入这里的数学以解释正在发生的事情&#xff0c;但这是我们查看 BM25 公式的结构以深入了解正在发生的事情的部分。 首先我们来看看公式&#xff0c;然后我将把每个组件分解成可以理解的部分&#xff1a; 我们可以看到一些常见的组件&#xff0c;如 qi、I…