商家营销工具架构升级总结
今年以来,商家营销工具业务需求井喷,需求数量多且耗时都比较长,技术侧面临很大的压力。因此这篇文章主要讨论营销工具前端要如何应对这样大规模的业务需求。
问题拆解
我们核心面对的问题主要如下:
1. 人力有限
我们除了要支撑存量页面的日常迭代,还需要完成大量页面的新增,虽然有短期的人力支援,但总体还是捉襟见肘。
2. 如何保障交付质量和体验?
商家营销工具核心的业务目标之一是体验优化,因此对前端交付页面的质量和体验,都有一定要求。而我们有大部分人力是新人,如何保证交付质量和体验?
3. 支援撤出后,长尾需求如何应对?
新增页面的持续优化迭代会带来大量长尾需求。支援撤出后如何应对?
问题总结一句话其实就是:如何高效高质量地支撑相比以往更多的需求。
最初的思路其实很简单:提高代码复用率。
这样:
1. 一方面提高了研发效率,以应对人力瓶颈。
2. 另一方面能通过沉淀下来的标准代码,拔高整体交付产物的质量和体验下限。
统一产品形态与设计规范
前端提高代码复用率的方式,最常用的就是组件沉淀(基础组件基本配套齐全,这里主要指业务组件)。但组件沉淀的实际效果,会受很多因素的影响:
1. 业务形态是否稳定?
针对一个业务场景沉淀的组件,会因该场景的业务调整而面临改造,不稳定的业务形态,会降低组件沉淀的收益。“三天两变”的业务形态下沉淀组件,可能会带来负收益。
2. UI是否稳定?
道理和业务形态相同。
我们当然有一些方法能够尽可能降低这些因素带来的影响。比如可以将 业务逻辑 与 UI 分离,让他们可以独立变更,互不影响;也可以对组件做更多层次更抽象的封装,来让组件能“灵活”地适应不同场景。
但在螺丝壳里做道场,不如考虑从源头解决问题。下边是几个存量工具的PC端页面:
营销工具的产品形态特点鲜明:
1. 大部分工具都有 管理、创建、编辑、查看 四种能力,主要通过 列表页 和 表单页 两个页面来承载。
2. 列表页、表单页各模块的样式和交互模式非常相近。比如列表页顶部的Banner区,创建页的图片预览区。
如果能统一部分模块甚至页面框架的交互模式和样式,那么根据这些场景沉淀的组件,可复用性是很高的。
因此我们对营销工具域内,可规范化的场景做了枚举。去推动产品和设计统一产品形态和设计规范。出乎意料的顺利,产品、设计、前端 三方很快达成了一致,最终我们确定了营销工具域产品&设计规范。
规范的种子其实早已种下,只是缺了一个浇水的人。
▐ 思考
当统一产品形态与设计规范这件事确定以后,技术上我们有了一些新的思路和要面对的问题:
接口的数据模型是否也能形成规范,我们可以将接口调用与数据处理也内置在组件内,做到更极致的提效。
不止于组件,我们是否可以将大的业务区块或者整个页面模版都沉淀下来?
一些数据源会频繁变更,业务逻辑又非常简单的模块,是否可以完全交由PD自行配置,免去迭代的开发工作量?
过去其实也有约定过一些产品&设计的规范,但都由于 规范粒度不够细、实践过程中产品&设计没有严格遵守规范、一遇到特殊定制逻辑就选择跳出规范等问题,导致最终各工具之间又逐渐趋于差异化,如何避免这种问题?
架构设计
基于上述的思考,我们针对商家营销工具场景,设计了一套技术架构,划分为以下几层:
页面容器层:这一层,我们用来收敛不同工具之间的共性部分。
-
如:页面UI框架、可配置化的模块(如列表页顶部Banner区)、请求方法、工具方法、公共依赖等。
场景模版层:这一层,我们用来收敛一些 共性 > 差异 的业务模块中的共性部分。它是可选的,当页面中没有 共性 > 差异 的模块时,那也就无需拆分出这一层。
-
如:首页的列表区块(包含:Tab区、筛选表单、列表、分页器),其中Tab切换、分页、筛选、样式布局等在不同工具下都是一致的,将这些逻辑固定下来,同时在模块中预留一些拓展点,支持不同工具定制,它就成为了列表场景的模版。
业务定制层:这一层,用来实现不同工具之间的差异部分。
-
如:在列表页,基于列表模版预留的拓展点进行定制;在表单页,实现单工具创建、编辑、查看态表单。
这样的架构设计:
1. 实现了我们最初的提高代码复用率的目标,提升开发效率,保障质量和体验下限。
2. 实现了一些共性需求或规范的更新,一次开发,整个业务域生效。
3. 会导致代码变更的影响范围被放大,页面容器层、场景模版层的变更,会一次影响到多个工具。但这是一把双刃剑:
对开发在变更代码时提出了更高的要求。
也让产品设计变得更加谨慎,因为越规范的部分越底层,越底层的部分影响范围越大,增强了规范的约束力。
架构串联
我们首先要面对的问题,是如何串联实现上述的三层架构。
参考微前端架构,我们可以先简单地将工具的共性部分放到一起作为主应用;而每个工具的定制部分作为微模块。
在主应用中,我们通过路由区分不同工具,以加载不同工具的微模块。同时我们会维护一个页面上下文,在微模块加载后注入进去,用来实现模块与主应用之间的数据通信。
我们可以快速的搭建出一个示例:
主应用:
import { MicroModule } from '@ice/stark-module';// 路由与为模块的映射
const MPathToModuleBaseInfo = {'/工具A/home': { name: '工具A-home-module', url: 'https://xxx.xxx.xxx' },'/工具A/create': { name: '工具A-create-module', url: 'https://xxx.xxx.xxx' },...
}// 页面主应用页面
export default function App() {const [state, setState] = useState(1);// 页面上下文const pageContext = useMemo(() => ({ setState }), []);return (...// 渲染微模块<MicroModulemoduleInfo={MPathToModuleBaseInfo[location.pathname]}// 将上下文作为 prop 注入进微模块pageContext={pageContext}/>;... )
}
微模块:
export default function Module(props) {// 获取上下文const { pageContext } = props || {};const handleClick = () => {// 与页面容器做数据通信pageContext.setState((preState: number) => preState + 1);}// 渲染业务定制模块return (<button onClick={handleClick}>state + 1</button>);
}
到这里,我们实现了页面容器层和业务定制层的串联。但场景模版层,有些无处安放。
将它放进主应用中?不太合适,因为场景模版的迭代频率是高于页面容器的,将这两层耦合在一起,会扩大场景模版的影响范围,增大维护压力。
将它放进微模块中?也不合适,这样每个工具都会单独维护一份场景模版,失去了这一层抽象的意义。
有个简单的方式能解决上述问题,将场景模版封装成一个 npm 包,在每个微模块中引入。但这个方案的缺陷在于,每次场景模版迭代,需要到各个工具中去升级npm版本。发布工作量让人头疼;同时还很难管控版本统一,很容易出现多个版本同时在线上运行的情况。
我们在微内核架构中,找到了灵感。这个名字可能会有点陌生,但如果叫它插件系统,大家应该就很熟悉了。像我们工作中经常接触的 Chrome、VS Code,就拥有强大的插件系统。
在微内核架构下,应用被分割为独立的插件模块和核心系统。在我们的设计中,场景模版就可以被看作是一个业务场景下的核心系统;而每个工具对场景模版的定制拓展,就是一个个插件。下边是一段示例:
插件基座:
// 路由与为插件资源的映射
const MPathToPluginInfo = {'/工具A/home': { url: 'https://xxx.xxx.xxx' },...
}
// 核心系统
export default function CoreApp() {const [tabList, setTabList] = useState([]);// 插件API生成const pluginApi = useMemo(() => {return {tabs: {add: setTabList;},}}, []);// 加载并运行插件const pluginResult = useMemo(() => {const plugin = registerPlugin(MPathToPluginInfo[location.pathname]);plugin(pluginApi);}, []);// 核心系统内部渲染 Tab List 的逻辑return (<Tab>{tabList.map((tab) => (<Tab.Item key={tab.key} title={tab.title}>{tab.content}</Tab.Item>))}</Tab>)
}
插件模块:
function Content() {return <div>content</div>
}export default function plugin(api) {// 使用 api 对核心系统进行定制api.tabs.add((preState) => ([...preState, { key: 'demo', title: '标题', content: <Content /> }]))
}
为了和上述微前端的实现结合,我们可以将核心系统作为一个常驻微模块由主应用加载。而加载插件的逻辑可以放在核心系统内部。
▐ 总结
到这里,架构的实现思路基本清晰了。
在表单页,我们可以直接通过 主应用 加载 微模块 的方式就能实现。
在列表页,我们针对列表场景,多做了一层抽象,将列表场景的共性逻辑封装成一个核心系统,各工具的差异逻辑则作为插件,对核心系统进行定制。通过 主应用 加载 核心系统,核心系统 加载 插件 的方式实现。
思路到最终实现,还有很多很多问题要解决。工程链路,渲染链路,发布链路,性能优化 等等。这也非一人之功,其中饱含了团队成员们的巧思,内容足以单开数篇文章,在本篇中就不做展开了。
分层实现
在上述架构下,开发业务定制层跟我们平常开发业务代码没什么区别,因此下边主要介绍 页面容器 和 场景模版 两层的一些实现思路。
▐ 页面容器层
UI框架
通常最底层的容器,是不耦合UI的。相比于页面逻辑,UI 的变化频率要高得多。耦合UI后,会导致容器要频繁迭代,影响健壮性。
但在统一了产品形态和设计规范以后,有很多页面框架层面的 UI 被固化下来,因此我们决定把整个页面框架的逻辑和UI都放进容器里。里边包含了:页面布局、骨架屏、Error兜底、权限校验 等等。
配置化
页面中同样存在一些UI一致,但是内容会频繁变化的模块,比如列表页顶部的Banner、公告、工具介绍、新人引导弹窗 等等。这些模块的变更,一般都是数据源的更新,因此我们决定把这些模块配置化,支持数据源远程下发;同时将配置产品化,将数据模型以表单形式表达,让PD能够自行修改,独立发布。
因已有配置产品化平台,接下来的工作就是对数据模型进行设计,并在项目中完成接入。
模块加载
在产品&设计规范统一后,我们可以,也应该把页面的UI框架固定下来。但出于对未来不确定性的担忧,我们在容器的实现上,给自己留了一些后路。
参考了一些C端搭建场景的容器设计,我们将页面按照垂直方向划分为一个个区块,通过一份配置进行渲染。区块与区块通常彼此独立,但也可以通过页面上下文或自建事件通信来实现交互。
import Entry from './components/Entry';
import Header from './components/Header';// 预设的本地区块
const MPresetNameToComponent = {'home-entry': Entry,'home-header': Header,
}const pageConfigs = [{ name: 'home-header', type: 'preSetComponent' },{ name: 'home-entry', type: 'preSetComponent' },{name: 'home-table-layout',type: 'microModule',moduleInfo: {...},},
]export default function App() {const pageContext = useMemo(() => {}, []);// 解析协议,渲染模块const renderModule = (config) => {const { name, type, moduleInfo } = config || {};if (type === 'preSetComponent') {const Component = MPresetNameToComponent[name];if (Component) {return <Component pageContext={pageContext} />}}if (type === 'microModule') {return (<MicroModule {...moduleInfo} pageContext={pageContext} />)}return null;}return pageConfigs.map((config) => renderModule(config));
}
这份配置目前直接写死在容器中,但在需要时,我们可以将其改造为远程下发。
这样的渲染模式给页面容器带来了一定的灵活性,它支持了一个工具对页面框架层面的定制诉求。比如工具A想在页面Banner区上方,添加策略推荐模块。按照固定框架的方式实现容器,我们想要支持这样的诉求则需要迭代容器,同时对工具A做特判,来渲染策略推荐模块,这会使得容器越来越臃肿。但通过协议渲染,则不会有这样的问题。
▐ 场景模版层
目前,我们仅在列表页做了这一层抽象。在规范中,列表区被固定为四个部分:Tab 区、筛选表单、列表、分页器。
基础逻辑
Tab区包含的逻辑很少,我们在核心系统中定义好Tab区的数据源,并在插件中通过API对数据源进行定制即可。
最终方案中,在插件里定制数据源:
// 添加Tab1
api.appendItem({ extensionId: EExtensionId.xx, title: 'xxx', type: 'table' });
// 添加Tab2
api.appendItem({ extensionId: EExtensionId.xx, title: 'xxx', type: 'table' });
筛选表单本质是列表接口入参的表达,因此逻辑上相对简单,很少有联动,且表单项的类型都很固定,最常用的就是 Input、Select、DatePicker、Radio 这几种,很适合通过 JSON Schema 的方式渲染,而这种渲染方式,已经有很成熟的解决方案了,比如 formily。我们也是基于它实现的。在核心系统中,我们完成表单实例的创建和提交的监听,插件只需要专注于声明表单项。
最终方案中,在插件里声明筛选表单项:
ExtensibleTable.modifyFilter(() => {return (form) => {// form 为核心系统中注入的表单实例return {// 表单项1A: {type: 'string','x-decorator': 'FormItem','x-component': 'Input','x-component-props': {placeholder: 'A',}},// 表单项2B: {type: 'number','x-decorator': 'FormItem','x-component': 'Select',enum: [{ label: 'xxxx', value: 0 },],'x-component-props': {placeholder: 'B',hasClear: true,}},}}
})
列表最核心的逻辑其实就是分页、筛选。我们已经完成了筛选表单的定制并能获取到表单值,剩余只需要串联搜索、分页的交互,管理列表、分页器的状态即可。ahooks 的 useFusionTable 已经为我们封装的很完整了,我们在核心系统中将他集成了进来,而在开发插件时,我们只用声明列表接口。
最终方案中,在插件里声明请求方法:
const listXXX = () => {return fetch('https://xxx.xxx.xxx')
}ExtensibleTable.modifyActions((columns, ctx) => {return {search: {// 声明列表接口request: ({ current, pageSize, filterValue }) => {// filterValue 中每个 key 和声明的表单项一一对应return listXXX({ current, pageSize, filterValue });}}
});
数据模型
上述的基础逻辑内置在核心系统中,已经能相当程度上减少列表区的开发工作量,但我们仍想更进一步。
列表核心是对服务端数据模型的表达,而数据模型又是对业务模型的抽象。在商家营销工具域内,不同工具之间有很多相似的业务模型,比如活动模型,它包含:活动名称、ID、时间、状态、绑定的商品等等字段信息。
在统一了产品形态和设计规范以后,我们顺其自然的想要推动服务端数据字段的统一。因此我们同样对可规范数据字段的场景进行了枚举,并以此去推动服务端统一数据字段与一些常用的功能型接口。最终我们达成了一致。
基于统一的数据,我们将部分列表单元格的渲染也内置到了核心系统中,根据插件中声明的单元格类型,核心系统自动去接口返回中找对应字段,并使用对应组件进行渲染。
最终方案中,在插件里声明列表列:
ExtensibleTable.modifyColumns((columns, ctx) => {return [// 活动信息模型 AA,对应字段 A、B、C{ title: '活动', 'x-component': 'AA', width: 180 },// 活动状态模型 BB,对应字段 D{ title: '活动状态', 'x-component': 'BB', width: 90 },// 活动时间模型 CC,对应字段 E、F、G{ title: '活动时间', 'x-component': 'CC', width: 180 },{title: 'xx',dataIndex: 'xx',cell: (value, index, record) => {// 该工具独有的单元格,自定义渲染},width: 240,},// 操作列数据模型 TT,对应字段 H{title: '操作',width: 150,'x-component': 'TT','x-component-props': { maxCol: 3, maxRow: 5 },},];});
交互模型
到这里,列表的主要逻辑中还剩余最后一块拼图,操作列交互。在中后台系统中,列表的操作是逻辑繁重的部分。我们在产品&设计规范中,对操作列的交互类型也做了统一。常见的交互类型有:二次确认、复制链接、导出文件、跳转 等。
我们将一个交互恒定的部分在核心系统中实现,在需要变化的部分预留好拓展点,由插件进行定制。
同时一些功能型接口,比如文件导出等,我们也推动了服务端统一,这样在插件中我们只需声明业务参数,甚至无需封装接口调用方法。
最终方案中,在插件里定义操作交互:
ExtensibleTable.modifyRowActions((columns, ctx) => {return {// 自定义交互custom: (option, rowData, tableContext) => {// 操作的业务类型为 A 时,对应的交互if (option.type === 'A') {// do things}// 操作类型为 B 时,对应的交互if (option.type === 'B') {// do things}},// 二次确认交互doubleCheck: (option, rowData) => {let message = '';// 不同业务类型对应不同的提示文案if (option?.type === 'C') { message = 'CCC'; };if (option?.type === 'D') { message = 'DDD'; };return {message,onConfirm: () => {// 操作类型为 E,点击确认时要调用的接口if (option.type === 'E') {return fetch(params).then(() => {Message.success('成功');});}if (option.type === 'F') {return fetch(params).then(() => {Message.success('成功');})}},}},// 导出文件交互export: (option, rowData) => {let type = '';if (option?.type === 'G') type = 'GGG';if (option?.type === 'H') type = 'HHH';// 声明导出文件的业务参数,无需封装接口调用方法return {params: {type,}}},}
});
结语
基于上述这套方案开发的增量页面进行了研发效能核算。以使用常规方案开发的排期耗时为基准,最终的提效收益是很明显的,都在 50% 以上。
除了研发提效外,这套方案还带来了一些额外的收益:
1. 一些在工具之间有共性的需求,比如资损校验等,在统一了设计规范之后,将其集成进 页面容器层 或 场景模版层 能做到一次开发,所有工具生效;
2. 部分模块可完全交给产品配置,变更无需排期;
3. 拔高了交付页面的质量和体验下限,视觉一致性也得到了保障。
同时也引入了一些问题:
1. 架构复杂度提升很多,对稳定性和页面性能是一个考验;
2. 同时也对线上问题的排查、变更影响范围的评估带来了一定影响。
因此,未来这套方案目前能想到的迭代方向有几个:
1. 降低由架构引入的问题带来的影响;
2. 探索表单场景是否也能做场景模版层的抽象;
3. 提升存量页面需求开发的研发效率。
团队介绍
我们是淘天集团-营销中后台前端团队,负责核心的淘宝&天猫营销业务,搭建千级运营小二、百万级商家和亿级消费者三方之间的连接通道,在这里将有机会参与到618、双十一等大型营销活动,与上下游伙伴协同作战,参与百万级流量后台场景的前端基础能力建设,通过标准化、归一化、模块化、低代码化的架构设计,保障商家与运营的经营体验和效率;参与面向亿级消费者的万级活动页面快速生产背后的架构设计、交付手段和协作方式。目前团队25届秋招进行中,对我们团队感兴趣的同学可以将简历发送至邮箱:wuzhiwei.wzw@taobao.com,欢迎加入!
¤ 拓展阅读 ¤
3DXR技术 | 终端技术 | 音视频技术
服务端技术 | 技术质量 | 数据算法
相关文章:

商家营销工具架构升级总结
今年以来,商家营销工具业务需求井喷,需求数量多且耗时都比较长,技术侧面临很大的压力。因此这篇文章主要讨论营销工具前端要如何应对这样大规模的业务需求。 问题拆解 我们核心面对的问题主要如下: 1. 人力有限 我们除了要支撑存量…...

移动硬盘无法读取:问题解析与高效数据恢复实战
一、移动硬盘无法读取的困扰 在数字化时代,移动硬盘作为数据存储和传输的重要媒介,承载着大量珍贵的数据资源。然而,当移动硬盘突然无法读取时,我们往往会陷入深深的困扰之中。这种无法读取的现象可能表现为插入电脑后毫无反应、…...

20241005给荣品RD-RK3588-AHD开发板刷Rockchip原厂的Android12时使用iperf3测网速
20241005给荣品RD-RK3588-AHD开发板刷Rockchip原厂的Android12时使用iperf3测网速 2024/10/5 14:06 对于荣品RD-RK3588-AHD开发板,eth1位置上的PCIE转RJ458的以太网卡是默认好用的! PCIE TO RJ45:RTL8111HS 被识别成为eth0了。inet addr:192.…...

node配置swagger
安装swagger npm install swagger-jsdoc swagger-ui-express 创建 swagger.js 配置文件 const path require(path); const express require(express); const swaggerUI require(swagger-ui-express); const swaggerJsDoc require(swagger-jsdoc); // 修改 swaggerDoc…...

MATLAB plot画线的颜色 形状
文章目录 前言一、MATLAB plot画线的颜色 形状?颜色选项标记选项示例代码详细说明 总结 前言 提示:这里可以添加本文要记录的大概内容: 项目需要: 提示:以下是本篇文章正文内容,下面案例可供参考 一、MA…...

Goland使用SSH远程Linux进行断点调试 (兼容私有库)
① 前置需求 ssh远程的 Linux 服务器必须安装 高于本地的 Go推荐golang 安装方式使用 apt yum snap 等系统自管理方式,(要安装最新版本的可以找找第三方源),如无特殊需求不要自行编译安装golang ② Goland设置 2.1、设置项处理…...

LLM | Ollama WebUI 安装使用(pip 版)
Open WebUI (Formerly Ollama WebUI) 也可以通过 docker 来安装使用 1. 详细步骤 1.1 安装 Open WebUI # 官方建议使用 python3.11(2024.09.27),conda 的使用参考其他文章 conda create -n open-webui python3.11 conda activate open-web…...

Three.js基础内容(一)
目录 一、几何体顶点和模型 1.1、点模型对象(Points)渲染顶点数据 1.2、线模型(Line)渲染顶点数据(画个心) 1.3、网格模型(Mesh)渲染顶点数据(三角形概念) 1.4、构建一个矩形平面几何体 1.5、几何顶点索引数据 1.6、顶点法线数据 1.7、查看three…...

网站建设制作需要注意
网站建设制作不仅仅是简单的技术活,更是一个企业或个人在互联网上展示自己形象和实力的重要手段。本文将探讨网站建设制作的重要性、步骤和关键要素。 1. 网站建设的重要性 1.1 品牌形象与宣传 一个精心设计的网站能够突显企业或个人的品牌形象,传递清晰…...

【Python】Uvicorn:Python 异步 ASGI 服务器详解
Uvicorn 是一个为 Python 设计的 ASGI(异步服务器网关接口)Web 服务器。它填补了 Python 在异步框架中缺乏一个最小化低层次服务器/应用接口的空白。Uvicorn 支持 HTTP/1.1 和 WebSockets,是构建现代异步Web应用的强大工具。 ⭕️宇宙起点 &a…...

类型转换【C++提升】(隐式转换、显式转换、自定义转换、转换构造函数、转换运算符重载......你想知道的全都有)
更多精彩内容..... 🎉❤️播主の主页✨😘 Stark、-CSDN博客 本文所在专栏: C系列语法知识_Stark、的博客-CSDN博客 座右铭:梦想是一盏明灯,照亮我们前行的路,无论风雨多大,我们都要坚持不懈。 一…...

微信小程序hbuilderx+uniapp+Android 新农村综合风貌旅游展示平台
目录 项目介绍支持以下技术栈:具体实现截图HBuilderXuniappmysql数据库与主流编程语言java类核心代码部分展示登录的业务流程的顺序是:数据库设计性能分析操作可行性技术可行性系统安全性数据完整性软件测试详细视频演示源码获取方式 项目介绍 小程序端…...

【AI大模型】使用Embedding API
一、使用OpenAI API 目前GPT embedding mode有三种,性能如下所示: 模型每美元页数MTEB得分MIRACL得分text-embedding-3-large9,61554.964.6text-embedding-3-small62,50062.344.0text-embedding-ada-00212,50061.031.4 MTEB得分为embedding model分类…...

面试速通宝典——11
188. 总结static的应用和作用 函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值。在模块内的static全局变量可以被模块内所用函数访问,但不能被模块外其他函…...

python:reportlab 将多个图片合并成一个PDF文件
承上一篇:java:pdfbox 3.0 去除扫描版PDF中文本水印 # 导出扫描版PDF文件中每页的图片文件 java -jar pdfbox-app-3.0.3.jar export:images -prefixtest -i your_book.pdf 导出 Writing image: test-1.jpg Writing image: test-2.jpg Writing image: t…...

决策树:机器学习中的强大工具
什么是决策树? 决策树是一种通过树状结构进行决策的模型。它的每个节点代表一个特征(或属性),每个分支代表特征的可能值,而每个叶子节点则代表最终的决策结果或分类。想象一下,在选择晚餐时,你…...

平面电磁波(解麦克斯韦方程)电场相位是复数的积分常数,电场矢量每个分量都有一个相位。磁场相位和电场一样,这是因为无损介质中实数的波阻抗
注意无源代表你立方程那个点xyzt处没有源,电场磁场也是这个点的。 j电流面密度,电流除以单位面积,ρ电荷体密度,电荷除以单位体积。 j方程组有16个未知数,每个矢量有三个xyz分量,即三个未知数,…...

复习HTML(进阶)
前言 上一篇的最后我介绍了在表单中,上传文件需要使用到 method属性 和enctype属性。本篇博客主要是详细的介绍这些知识 <form action"http://localhost:8080/test" method"post" enctype"multipart/form-data"> method属性…...

Qt 每日面试题 -7
61、如何安全的在另外一个线程中调用QObject对象的接口 QObject被设计成在一个单线程中创建与使用,因此,在一个线程中创建一个对象,而在另外的线程中调用它的函数,这样的行为不能保证工作良好。使用信号槽的队列连接或者QT的反射…...

《计算机原理与系统结构》学习系列——计算机的算数运算(下)
系列文章目录 目录 浮点数的表示和运算浮点数的表示浮点数的规格化浮点数标准IEEE754浮点数表示范围浮点数的转换浮点数的运算浮点数加法浮点数加法的硬件实现 精度浮点乘法浮点运算硬件 MIPS中的浮点指令 浮点数的表示和运算 浮点数的表示 表达非整型的数 可以表达很小和很大…...

二叉树进阶学习——从前序和中序遍历序列构造二叉树
1.题目解析 题目来源:105.从前序与中序遍历序列构造二叉树——力扣 测试用例 2.算法原理 首先要了解一个概念 前序遍历:按照 根节点->左子树->右子树的顺序遍历二叉树 中序遍历:按照 左子树->根节点->右子树的顺序遍历二叉树 题目…...

【数据分享】2000—2023年我国省市县三级逐年植被覆盖度(FVC)数据(Shp/Excel格式)
之前我们分享过2000—2023年逐月植被覆盖度(FVC)栅格数据(可查看之前的文章获悉详情)和Excel和Shp格式的省市县三级逐月FVC数据(可查看之前的文章获悉详情),原始的逐月栅格数据来源于高吉喜学者…...

【Python】Streamlit:为数据科学与机器学习打造的简易应用框架
Streamlit 是一个开源的 Python 库,专为数据科学家和机器学习开发者设计,旨在快速构建数据应用。通过简单的 Python 脚本,开发者无需掌握前端技术,即可将数据分析和模型结果转化为直观、交互式的 Web 应用。其简洁的 API 设计使得…...

OpenJudge | 置换选择排序
总时间限制: 1000ms 内存限制: 65536kB 描述 给定初始整数顺串,以及大小固定并且初始元素已知的二叉最小堆(为完全二叉树或类似完全二叉树,且父元素键值总小于等于任何一个子结点的键值),要求利用堆实现置换选择排序&a…...

如何提取b站的视频字幕,下载视频
打开视频地址 按F12打开—开发者工具 在开发者工具打开Network 过滤器关键字: 自动生成字幕:ai_subtitle 自制:json 打开/关闭字幕 刷新页面 找到字幕 点选字幕的respond 将方框中的内容复制; 复制到:https://www.drea…...

Vue中使用ECharts实现热力图的详细教程
在数据可视化领域,热力图是一种非常直观的表现形式,它通过颜色深浅来展示数据分布情况。在Vue项目中,我们可以使用ECharts这一强大的图表库来实现热力图。下面我将详细介绍如何在Vue中使用ECharts实现热力图。效果如下图: 一、准备…...

Arduino UNO R3自学笔记13 之 Arduino使用LM35如何测量温度?
注意:学习和写作过程中,部分资料搜集于互联网,如有侵权请联系删除。 前言:学习使用传感器测温。 1.LM35介绍 一般来讲当知道需求,就可以 通过既定要求的条件来筛选需要的器件,多方面的因素最终选定了器件…...

蓝桥杯【物联网】零基础到国奖之路:十六. 扩展模块之矩阵按键
蓝桥杯【物联网】零基础到国奖之路:十六. 扩展模块之矩阵按键 第一节 硬件解读第二节 CubeMX配置第三节 MDK代码 第一节 硬件解读 扩展模块和ADC模块是一摸一样的,插在主板上。 引脚对应关系: PB6-ROW1 PB7-ROW2 PB1-COLUMN1 PB0-COLUMN2 PA8-COLUMN3 …...

Apollo9.0 Planning2.0决策规划算法代码详细解析 (4): PlanningComponent::Proc()
🌟 面向自动驾驶规划算法工程师的专属指南 🌟 欢迎来到《Apollo9.0 Planning2.0决策规划算法代码详细解析》专栏!本专栏专为自动驾驶规划算法工程师量身打造,旨在通过深入剖析Apollo9.0开源自动驾驶软件栈中的Planning2.0模块&am…...

AAA Redis的过期删除策略+缓存雪崩+缓存一致性问题
目录 一、三种删除策略比较 二、缓存雪崩缓存击穿缓存穿透 三、缓存一致性 Redis学习笔记 一、三种删除策略比较 内存占用CPU占用特征定时删除节约内存,无占用不分时段占用CPU资源,频度高时间换空间惰性删除内存占用严重延时执行,CPU利用…...