AT 的人生未必比 MT 更好 - 肘子的 Swift 周报 #118
学车时我开的是手动挡,起初因为技术生疏,常搞得手忙脚乱,所以第一台车就直接选了自动挡。但开了几年,我开始追求那种完全掌控的驾驶感,于是又增购了一台手动挡。遗憾的是,随着交通日益拥堵,换挡的乐趣逐渐被疲惫抵消,最终这台车也被冷落。算起来,我已经快二十年没认真开过手动挡了,但内心深处,我仍会时不时地怀念那段“人车合一”的时光。
学车时我开的是手动挡,起初因为技术生疏,常搞得手忙脚乱,所以第一台车就直接选了自动挡。但开了几年,我开始追求那种完全掌控的驾驶感,于是又增购了一台手动挡。遗憾的是,随着交通日益拥堵,换挡的乐趣逐渐被疲惫抵消,最终这台车也被冷落。算起来,我已经快二十年没认真开过手动挡了,但内心深处,我仍会时不时地怀念那段“人车合一”的时光。
这是一个用于自动提取GitLab提交记录的Node.js脚本,专为年终总结设计。它可以:
javascript
function readEnvFile(envPath) {
const content = fs.readFileSync(envPath, 'utf8');
const lines = content.split(/\r?\n/).filter(Boolean);
const env = {};
for (const line of lines) {
if (line.trim().startsWith('#')) continue;
const idx = line.indexOf('=');
if (idx === -1) continue;
const key = line.slice(0, idx).trim();
const value = line.slice(idx + 1).trim();
env[key] = value;
}
return env;
}
功能说明:读取.env配置文件,解析为键值对。
配置说明:
env
# GitLab服务器地址
GITLAB_URL=https://your.gitlab.server.com
# GitLab访问令牌(从GitLab个人设置中获取)
GITLAB_TOKEN=your_gitlab_access_token
# 可选:作者用户名(用于过滤提交)
GITLAB_AUTHOR_USERNAME=your_username
# 可选:指定项目ID(多个用逗号分隔)
GITLAB_PROJECT_IDS=123,456,789
javascript
function parseArgs(argv) {
const args = {};
for (let i = 2; i < argv.length; i++) {
const arg = argv[i];
if (arg.startsWith('--')) {
const [k, v] = arg.split('=');
args[k.slice(2)] = v;
}
}
return args;
}
功能说明:解析命令行参数,支持--since和--until参数。
javascript
function ensureIsoRange(sinceInput, untilInput) {
const sinceIsDateOnly = /^\d{4}-\d{2}-\d{2}$/.test(sinceInput);
const untilIsDateOnly = /^\d{4}-\d{2}-\d{2}$/.test(untilInput);
if (sinceIsDateOnly && untilIsDateOnly) {
const { since } = toIsoRangeDayStartEnd(sinceInput);
const { until } = toIsoRangeDayStartEnd(untilInput);
return { since, until };
}
const since = new Date(sinceInput).toISOString();
const until = new Date(untilInput).toISOString();
return { since, until };
}
功能说明:将用户输入的时间范围转换为ISO标准格式,支持日期格式和完整时间格式。
javascript
function requestJson(urlStr, headers = {}) {
return new Promise((resolve, reject) => {
const u = new URL(urlStr);
const { protocol, hostname, port, pathname, search } = u;
const lib = protocol === 'https:' ? https : http;
const options = {
hostname,
port: port || (protocol === 'https:' ? 443 : 80),
path: `${pathname}${search}`,
method: 'GET',
headers,
};
const req = lib.request(options, (res) => {
const { statusCode, headers: resHeaders } = res;
const chunks = [];
res.on('data', (c) => chunks.push(c));
res.on('end', () => {
const body = Buffer.concat(chunks).toString('utf8');
if (statusCode >= 200 && statusCode < 300) {
try {
const json = JSON.parse(body);
resolve({ json, headers: resHeaders, statusCode });
} catch (e) {
reject(new Error(`Invalid JSON ${statusCode}: ${body.slice(0, 200)}`));
}
} else {
reject(new Error(`HTTP ${statusCode}: ${body.slice(0, 200)}`));
}
});
});
req.on('error', reject);
req.end();
});
}
功能说明:发送HTTP/HTTPS请求,返回JSON格式的响应。
javascript
async function fetchAllCommits(baseUrl, token, id, since, until, author) {
const collected = [];
let page = 1;
for (;;) {
const params = { since, until, per_page: 100, page, with_stats: false, author };
const { commits, nextPage } = await fetchCommitsPage(baseUrl, token, id, params);
collected.push(...commits);
if (!nextPage) break;
page = parseInt(nextPage, 10);
if (!Number.isFinite(page) || page <= 0) break;
}
return collected;
}
功能说明:分页获取GitLab提交记录,支持作者过滤。
javascript
function filterNonMerge(commits) {
const filtered = [];
for (const commit of commits) {
const { parent_ids } = commit;
const nonMerge = Array.isArray(parent_ids) ? parent_ids.length <= 1 : true;
if (nonMerge) filtered.push(commit);
}
return filtered;
}
功能说明:过滤掉合并提交,只保留实际代码变更的提交。
javascript
function buildMarkdown(range, author, grouped) {
const { since, until } = range;
const { username, name } = author;
const lines = [];
lines.push(`# 提交汇总`);
lines.push(`- 作者: ${name || username || ''}`);
lines.push(`- 时间范围: ${since} 至 ${until}`);
for (const project of grouped.projects) {
const { name: projName } = project.meta;
lines.push(`\n项目: ${projName}`);
const commits = project.commits;
for (const commit of commits) {
lines.push(formatCommitLine(project.meta, commit));
}
}
return `${lines.join('\n')}\n`;
}
功能说明:生成Markdown格式的提交汇总报告。
安装依赖:无需额外依赖,使用Node.js内置模块。
配置.env文件:根据实际情况修改.env文件中的配置。
运行脚本:
bash
node fetch_commits.js --since=2025-01-01 --until=2025-12-31
node fetch_commits.js --since=2025-06-01 --until=2026-01-11 --author=你的提交用户名
查看报告:脚本会生成commits.md文件,包含指定时间范围内的提交记录。
javascript
const fs = require('fs');
const path = require('path');
const http = require('http');
const https = require('https');
function readEnvFile(envPath) {
const content = fs.readFileSync(envPath, 'utf8');
const lines = content.split(/\r?\n/).filter(Boolean);
const env = {};
for (const line of lines) {
if (line.trim().startsWith('#')) continue;
const idx = line.indexOf('=');
if (idx === -1) continue;
const key = line.slice(0, idx).trim();
const value = line.slice(idx + 1).trim();
env[key] = value;
}
return env;
}
function parseArgs(argv) {
const args = {};
for (let i = 2; i < argv.length; i++) {
const arg = argv[i];
if (arg.startsWith('--')) {
const [k, v] = arg.split('=');
args[k.slice(2)] = v;
}
}
return args;
}
function toIsoRangeDayStartEnd(dateStr) {
const start = new Date(`${dateStr}T00:00:00.000Z`);
const end = new Date(`${dateStr}T23:59:59.999Z`);
return { since: start.toISOString(), until: end.toISOString() };
}
function ensureIsoRange(sinceInput, untilInput) {
const sinceIsDateOnly = /^\d{4}-\d{2}-\d{2}$/.test(sinceInput);
const untilIsDateOnly = /^\d{4}-\d{2}-\d{2}$/.test(untilInput);
if (sinceIsDateOnly && untilIsDateOnly) {
const { since } = toIsoRangeDayStartEnd(sinceInput);
const { until } = toIsoRangeDayStartEnd(untilInput);
return { since, until };
}
const since = new Date(sinceInput).toISOString();
const until = new Date(untilInput).toISOString();
return { since, until };
}
function requestJson(urlStr, headers = {}) {
return new Promise((resolve, reject) => {
const u = new URL(urlStr);
const { protocol, hostname, port, pathname, search } = u;
const lib = protocol === 'https:' ? https : http;
const options = {
hostname,
port: port || (protocol === 'https:' ? 443 : 80),
path: `${pathname}${search}`,
method: 'GET',
headers,
};
const req = lib.request(options, (res) => {
const { statusCode, headers: resHeaders } = res;
const chunks = [];
res.on('data', (c) => chunks.push(c));
res.on('end', () => {
const body = Buffer.concat(chunks).toString('utf8');
if (statusCode >= 200 && statusCode < 300) {
try {
const json = JSON.parse(body);
resolve({ json, headers: resHeaders, statusCode });
} catch (e) {
reject(new Error(`Invalid JSON ${statusCode}: ${body.slice(0, 200)}`));
}
} else {
reject(new Error(`HTTP ${statusCode}: ${body.slice(0, 200)}`));
}
});
});
req.on('error', reject);
req.end();
});
}
function buildApiUrl(base, pathStr, query = {}) {
const u = new URL(pathStr, base);
const entries = Object.entries(query).filter(([, v]) => v !== undefined && v !== null);
for (const [k, v] of entries) {
u.searchParams.set(k, String(v));
}
return u.toString();
}
async function fetchProjectMeta(baseUrl, token, id) {
const url = buildApiUrl(baseUrl, `/api/v4/projects/${encodeURIComponent(id)}`);
const headers = { 'PRIVATE-TOKEN': token };
const { json } = await requestJson(url, headers);
const { name, path_with_namespace, web_url } = json;
return { id, name, path_with_namespace, web_url };
}
async function fetchCommitsPage(baseUrl, token, id, params) {
const url = buildApiUrl(
baseUrl,
`/api/v4/projects/${encodeURIComponent(id)}/repository/commits`,
params
);
const headers = { 'PRIVATE-TOKEN': token };
const { json, headers: resHeaders } = await requestJson(url, headers);
const { ['x-next-page']: nextPage, ['x-page']: page, ['x-total-pages']: totalPages } = resHeaders;
return { commits: json, nextPage, page, totalPages };
}
async function fetchAllCommits(baseUrl, token, id, since, until, author) {
const collected = [];
let page = 1;
for (;;) {
const params = { since, until, per_page: 100, page, with_stats: false, author };
const { commits, nextPage } = await fetchCommitsPage(baseUrl, token, id, params);
collected.push(...commits);
if (!nextPage) break;
page = parseInt(nextPage, 10);
if (!Number.isFinite(page) || page <= 0) break;
}
return collected;
}
function filterNonMerge(commits) {
const filtered = [];
for (const commit of commits) {
const { parent_ids } = commit;
const nonMerge = Array.isArray(parent_ids) ? parent_ids.length <= 1 : true;
if (nonMerge) filtered.push(commit);
}
return filtered;
}
function formatCommitLine(project, commit) {
const { short_id, title, message, committed_date, author_name, author_email } = commit;
const main = (title || message || '').replace(/\r?\n/g, ' ');
const ts = formatDateLocal(committed_date);
return `- ${ts} | ${short_id} | ${main} | ${author_name} <${author_email}>`;
}
function pad2(n) {
return String(n).padStart(2, '0');
}
function formatDateLocal(iso) {
const d = new Date(iso);
const y = d.getFullYear();
const m = pad2(d.getMonth() + 1);
const day = pad2(d.getDate());
const hh = pad2(d.getHours());
const mm = pad2(d.getMinutes());
const ss = pad2(d.getSeconds());
return `${y}-${m}-${day} ${hh}:${mm}:${ss}`;
}
function buildMarkdown(range, author, grouped) {
const { since, until } = range;
const { username, name } = author;
const lines = [];
lines.push(`# 提交汇总`);
lines.push(`- 作者: ${name || username || ''}`);
lines.push(`- 时间范围: ${since} 至 ${until}`);
for (const project of grouped.projects) {
const { name: projName } = project.meta;
lines.push(`\n项目: ${projName}`);
const commits = project.commits;
for (const commit of commits) {
lines.push(formatCommitLine(project.meta, commit));
}
}
return `${lines.join('\n')}\n`;
}
async function fetchMembershipProjects(baseUrl, token) {
const headers = { 'PRIVATE-TOKEN': token };
const projects = [];
let page = 1;
for (;;) {
const url = buildApiUrl(baseUrl, '/api/v4/projects', {
membership: true,
simple: true,
per_page: 100,
page,
order_by: 'last_activity_at',
});
const { json, headers: resHeaders } = await requestJson(url, headers);
for (const item of json) {
const { id, name, path_with_namespace, web_url } = item;
projects.push({ id, name, path_with_namespace, web_url });
}
const nextPage = resHeaders['x-next-page'];
if (!nextPage) break;
page = parseInt(nextPage, 10);
if (!Number.isFinite(page) || page <= 0) break;
}
return projects;
}
async function resolveAuthorQuery(baseUrl, token, username, override) {
if (override) return override;
if (!username) return null;
const url = buildApiUrl(baseUrl, '/api/v4/users', { username });
const headers = { 'PRIVATE-TOKEN': token };
const { json } = await requestJson(url, headers);
if (Array.isArray(json) && json.length > 0) {
const { name } = json[0];
return name || username;
}
return username;
}
function filterByAuthorName(commits, authorName) {
if (!authorName) return commits;
const out = [];
for (const commit of commits) {
const { author_name } = commit;
if (author_name === authorName) out.push(commit);
}
return out;
}
async function main() {
const cwd = process.cwd();
const envPath = path.join(cwd, '.env');
const env = readEnvFile(envPath);
const {
GITLAB_URL,
GITLAB_TOKEN,
GITLAB_AUTHOR_USERNAME,
} = env;
const args = parseArgs(process.argv);
const { since: sinceRaw, until: untilRaw, author: authorArg } = args;
if (!GITLAB_URL || !GITLAB_TOKEN || !sinceRaw || !untilRaw) {
process.stderr.write(
'缺少必要配置或参数。需要 GITLAB_URL, GITLAB_TOKEN, --since=YYYY-MM-DD, --until=YYYY-MM-DD\n'
);
process.exit(1);
}
const { since, until } = ensureIsoRange(sinceRaw, untilRaw);
const desiredAuthor = authorArg || 'zhouzb';
const authorQuery = await resolveAuthorQuery(GITLAB_URL, GITLAB_TOKEN, GITLAB_AUTHOR_USERNAME, desiredAuthor);
const authorInfo = { username: GITLAB_AUTHOR_USERNAME, name: desiredAuthor };
let metas = [];
if (env.GITLAB_PROJECT_IDS) {
const ids = env.GITLAB_PROJECT_IDS.split(',').map((s) => s.trim()).filter(Boolean);
for (const id of ids) {
const meta = await fetchProjectMeta(GITLAB_URL, GITLAB_TOKEN, id);
metas.push(meta);
}
} else {
metas = await fetchMembershipProjects(GITLAB_URL, GITLAB_TOKEN);
}
const grouped = { projects: [] };
for (const meta of metas) {
const { id } = meta;
const all = await fetchAllCommits(GITLAB_URL, GITLAB_TOKEN, id, since, until, authorQuery || undefined);
const filtered = filterByAuthorName(filterNonMerge(all), desiredAuthor);
if (filtered.length > 0) grouped.projects.push({ meta, commits: filtered });
}
const md = buildMarkdown({ since, until }, authorInfo, grouped);
fs.writeFileSync(path.join(cwd, 'commits.md'), md, 'utf8');
}
main().catch((e) => {
const { message } = e;
process.stderr.write(`${message}\n`);
process.exit(1);
});
在 Vue3 中更新 LogicFlow 节点名称有多种方式,下面我为你详细介绍几种常用方法。
updateText方法(推荐)这是最直接的方式,通过节点 ID 更新文本内容:
<template>
<div>
<div ref="container" style="width: 100%; height: 500px;"></div>
<button @click="updateNodeName">更新节点名称</button>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import LogicFlow from '@logicflow/core';
import '@logicflow/core/dist/style/index.css';
const container = ref(null);
const lf = ref(null);
const selectedNodeId = ref('');
onMounted(() => {
lf.value = new LogicFlow({
container: container.value,
grid: true,
});
// 示例数据
lf.value.render({
nodes: [
{
id: 'node_1',
type: 'rect',
x: 100,
y: 100,
text: '原始名称'
}
]
});
// 监听节点点击,获取选中节点ID
lf.value.on('node:click', ({ data }) => {
selectedNodeId.value = data.id;
});
});
// 更新节点名称
const updateNodeName = () => {
if (!selectedNodeId.value) {
alert('请先点击选择一个节点');
return;
}
const newName = prompt('请输入新的节点名称', '新名称');
if (newName) {
// 使用 updateText 方法更新节点文本
lf.value.updateText(selectedNodeId.value, newName);
}
};
</script>
setProperties方法更新这种方法可以同时更新文本和其他属性:
// 更新节点属性,包括名称
const updateNodeWithProperties = () => {
if (!selectedNodeId.value) return;
const newNodeName = '更新后的节点名称';
// 获取节点当前属性
const nodeModel = lf.value.getNodeModelById(selectedNodeId.value);
const currentProperties = nodeModel.properties || {};
// 更新属性
lf.value.setProperties(selectedNodeId.value, {
...currentProperties,
nodeName: newNodeName,
updatedAt: new Date().toISOString()
});
// 同时更新显示文本
lf.value.updateText(selectedNodeId.value, newNodeName);
};
实现双击节点直接进入编辑模式:
// 监听双击事件
lf.value.on('node:dblclick', ({ data }) => {
const currentNode = lf.value.getNodeModelById(data.id);
const currentText = currentNode.text?.value || '';
const newText = prompt('编辑节点名称:', currentText);
if (newText !== null) {
lf.value.updateText(data.id, newText);
}
});
结合 Menu 插件实现右键菜单编辑:
import { Menu } from '@logicflow/extension';
import '@logicflow/extension/lib/style/index.css';
// 初始化时注册菜单插件
lf.value = new LogicFlow({
container: container.value,
plugins: [Menu],
});
// 配置右键菜单
lf.value.extension.menu.setMenuConfig({
nodeMenu: [
{
text: '编辑名称',
callback: (node) => {
const currentText = node.text || '';
const newText = prompt('编辑节点名称:', currentText);
if (newText) {
lf.value.updateText(node.id, newText);
}
}
},
{
text: '删除',
callback: (node) => {
lf.value.deleteNode(node.id);
}
}
]
});
对于自定义节点,可以重写文本相关方法:
import { RectNode, RectNodeModel } from '@logicflow/core';
class CustomNodeModel extends RectNodeModel {
// 自定义文本样式
getTextStyle() {
const style = super.getTextStyle();
return {
...style,
fontSize: 14,
fontWeight: 'bold',
fill: '#1e40af',
};
}
// 初始化节点数据
initNodeData(data) {
super.initNodeData(data);
// 确保文本格式正确
this.text = {
x: data.x,
y: data.y + this.height / 2 + 10,
value: data.text || '默认节点'
};
}
}
// 注册自定义节点
lf.value.register({
type: 'custom-node',
view: RectNode,
model: CustomNodeModel
});
// 批量更新所有节点名称
const batchUpdateNodeNames = () => {
const graphData = lf.value.getGraphData();
const updatedNodes = graphData.nodes.map(node => ({
...node,
text: `${node.text}(已更新)`
}));
// 重新渲染
lf.value.render({
nodes: updatedNodes,
edges: graphData.edges
});
};
// 按条件更新节点
const updateNodesByCondition = () => {
const graphData = lf.value.getGraphData();
const updatedNodes = graphData.nodes.map(node => {
if (node.type === 'rect') {
return {
...node,
text: `矩形节点-${node.id}`
};
}
return node;
});
lf.value.render({
nodes: updatedNodes,
edges: graphData.edges
});
};
// 监听文本变化并自动保存
lf.value.on('node:text-update', ({ data }) => {
console.log('节点文本已更新:', data);
saveToBackend(lf.value.getGraphData());
});
// 实现撤销重做功能
const undo = () => {
lf.value.undo();
};
const redo = () => {
lf.value.redo();
};
// 启用历史记录
lf.value = new LogicFlow({
container: container.value,
grid: true,
history: true, // 启用历史记录
historySize: 100 // 设置历史记录大小
});
{value: '文本', x: 100, y: 100}
lf.render()之后再进行更新操作// 安全的更新函数
const safeUpdateNodeName = (nodeId, newName) => {
if (!lf.value) {
console.error('LogicFlow 实例未初始化');
return false;
}
const nodeModel = lf.value.getNodeModelById(nodeId);
if (!nodeModel) {
console.error(`节点 ${nodeId} 不存在`);
return false;
}
try {
lf.value.updateText(nodeId, newName);
return true;
} catch (error) {
console.error('更新节点名称失败:', error);
return false;
}
};
这些方法涵盖了 Vue3 中 LogicFlow 节点名称更新的主要场景,你可以根据具体需求选择合适的方式。
大家好,我是 Java陈序员。
无论是儿女结婚的喜宴,还是亲友离世的白事,礼金记账都是绕不开的环节。
传统手写礼簿,不仅考验书写速度和细心程度,还面临着“记重了、算错了、丢了账本”的风险,既费人力又不省心。
而市面上的电子记账工具,要么依赖网络,要么数据存在云端,总担心隐私泄露。
今天,给大家推荐一款纯本地运行的电子礼簿系统,不用连网、不用注册、数据加密存储、安全又好用,红白喜事都适配!
gift-book —— 一款纯本地、零后端、完全本地运行的单页 Web 应用,旨在为各类红白喜事提供一个现代化、安全、高效的礼金(份子钱)管理解决方案。
功能特色:
gift-book由纯静态文件组成,无需安装任何环境。
1、打开下载地址,下载 Windows 预编译应用(gift-book.exe)
https://github.com/jingguanzhang/gift-book/releases
2、双击运行 gift-book.exe
3、初始化:创建新事项
![]()
设置事项名称及管理密码(请务必牢记,丢失无法找回)。
4、记账:录入数据
![]()
5、归档:活动结束后,务必导出 Excel 或 PDF 文件到电脑,微信收藏或云盘永久保存
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
需要依赖代码编辑器(推荐 VS Code)和浏览器(Chrome/Edge)。
1、克隆或下载项目源码
git clone https://github.com/jingguanzhang/gift-book.git
2、在 VS Code 中打开项目代码
3、代码目录结构
gift-book
├── index1.html # v1.1 专业版主入口(核心代码均内嵌于此,方便单文件分发)
├── index.html # v1.0 基础版主入口
├── static/ # 静态资源目录
├── tailwindcss.js # 样式引擎
├── xlsx.full.min.js # Excel 导出库
├── pdf-lib.min.js # PDF 生成引擎
├── crypto-js.min.js # 加密库
└── fontkit & .ttf # 字体文件(用于 PDF 生成)
└── guest-screen.html # 副屏显示页面
4、右键 index.html 并选择 "Open with Live Server" 运行程序
需要在 VS Code 中提前安装插件
Live Server.
5、部署上线:无需编译,直接将所有文件上传至 GitHub Pages、Vercel、Nginx 或任何静态文件服务器即可
可以说,gift-book 这款纯本地电子礼簿,没有复杂的操作门槛,没有数据泄露的顾虑,只用简单的方式把账记准、记清、存好。快去试试吧~
项目地址:https://github.com/jingguanzhang/gift-book
推荐的开源项目已经收录到 GitHub 项目,欢迎 Star:
https://github.com/chenyl8848/great-open-source-project
或者访问网站,进行在线浏览:
https://chencoding.top:8090/#/
大家的点赞、收藏和评论都是对作者的支持,如文章对你有帮助还请点赞转发支持下,谢谢!
在科研成果评价领域,H 指数是一个非常经典的指标,而 LeetCode 274 题正是围绕 H 指数的计算展开。这道题看似简单,但背后藏着两种思路迥异的高效解法。今天我们就来深入剖析这道题,把两种解法的逻辑、实现和优劣讲透。
首先明确题目要求:给定一个整数数组 citations,其中 citations[i] 表示研究者的第 i 篇论文被引用的次数,计算并返回该研究者的 H 指数。
核心是理解 H 指数的定义(划重点):一名科研人员的 H 指数是指他至少发表了 h 篇论文,并且这 h 篇论文每篇的被引用次数都大于等于 h。如果存在多个可能的 h 值,取最大的那个。
举个例子帮助理解:若 citations = [1,3,1],H 指数是 1。因为研究者有 3 篇论文,其中至少 1 篇被引用 ≥1 次,而要达到 h=2 则需要至少 2 篇论文被引用 ≥2 次(实际只有 1 篇3次,不满足),所以最大的 h 是 1。
先看第一种解法的代码,这是一种基于计数排序的优化方案,适合对时间效率要求较高的场景。
function hIndex_1(citations: number[]): number {
const ciLen = citations.length;
const count = new Array(ciLen + 1).fill(0);
for (let i = 0; i < ciLen; i++) {
if (citations[i] > ciLen) {
count[ciLen]++;
} else {
count[citations[i]]++;
}
}
let total = 0;
for (let i = ciLen; i >= 0; i--) {
total += count[i];
if (total >= i) {
return i;
}
}
return 0;
};
H 指数的最大值不可能超过论文总数 n(因为要至少 h 篇论文,h 最多等于论文数)。所以对于引用次数超过 n 的论文,我们可以统一视为引用次数为 n(不影响 H 指数的计算)。
基于这个特点,我们可以用一个计数数组 count 统计每个引用次数(0 到 n)对应的论文数量,然后从后往前累加计数,找到第一个满足「累加总数 ≥ 当前引用次数」的数值,这个数值就是最大的 H 指数。
初始化变量:论文总数 ciLen = 5,计数数组 count 长度为 ciLen + 1 = 6,初始值全为 0(count = [0,0,0,0,0,0])。
统计引用次数分布:遍历 citations 数组,将每篇论文的引用次数映射到 count 中:
最终`count` 含义:引用 0 次的 1 篇、1 次的 1 篇、3 次的 1 篇、5 次及以上的 2 篇。
3 ≤ 5 → count[3]++ → count = [0,0,0,1,0,0]
0 ≤ 5 → count[0]++ → count = [1,0,0,1,0,0]
6 > 5 → count[5]++ → count = [1,0,0,1,0,1]
1 ≤ 5 → count[1]++ → count = [1,1,0,1,0,1]
5 ≤ 5 → count[5]++ → count = [1,1,0,1,0,2]
倒序累加找 H 指数:从最大可能的 h(即 ciLen=5)开始,累加 count[i](表示引用次数 ≥i 的论文总数),直到累加和 ≥i:
i=5:total = 0 + 2 = 2 → 2 < 5 → 继续
i=4:total = 2 + 0 = 2 → 2 < 4 → 继续
i=3:total = 2 + 1 = 3 → 3 ≥ 3 → 满足条件,返回 3
最终结果为 3,符合预期(3 篇论文被引用 ≥3 次:3、6、5)。
优点:时间复杂度 O(n),只需要两次遍历数组,效率极高;空间复杂度 O(n),仅需一个固定长度的计数数组。
缺点:需要额外的空间存储计数数组,对于论文数量极少的场景,空间开销不明显,但思路相对排序法更难理解。
第二种解法是基于排序的思路,逻辑更直观,容易理解,也是很多人首先会想到的方案。
function hIndex(citations: number[]): number {
// 思路:逆序排序
citations.sort((a, b) => b - a);
let res = 0;
for (let i = 0; i < citations.length; i++) {
if (citations[i] >= i + 1) {
res = i + 1;
}
}
return res;
};
将引用次数数组逆序排序(从大到小),此时排序后的数组第 i 个元素(索引从 0 开始)表示第 i+1 篇论文的引用次数。如果该元素 ≥ i+1,说明前 i+1 篇论文的引用次数都 ≥ i+1,此时 H 指数至少为 i+1。遍历完数组后,最大的这个 i+1 就是最终的 H 指数。
逆序排序数组:排序后 citations = [6,5,3,1,0]。
遍历数组找最大 h:初始化 res = 0,依次判断每个元素:
i=0:citations[0] = 6 ≥ 0+1=1 → res = 1
i=1:citations[1] = 5 ≥ 1+1=2 → res = 2
i=2:citations[2] = 3 ≥ 2+1=3 → res = 3
i=3:citations[3] = 1 ≥ 3+1=4 → 不满足,res 不变
i=4:citations[4] = 0 ≥ 4+1=5 → 不满足,res 不变
返回结果:最终 res = 3,与解法一结果一致。
优点:逻辑直观,容易理解和实现;空间复杂度低,若允许原地排序(如 JavaScript 的 sort 方法),空间复杂度为 O(log n)(排序的递归栈空间),否则为 O(1)。
缺点:时间复杂度由排序决定,为 O(n log n),对于大规模数据(如论文数量极多),效率不如解法一。
| 解法 | 时间复杂度 | 空间复杂度 | 核心优势 | 适用场景 |
|---|---|---|---|---|
| 计数排序法 | O(n) | O(n) | 时间效率极高,两次线性遍历 | 大规模数据,对时间要求高 |
| 逆序排序法 | O(n log n) | O(1) | 逻辑直观,空间开销小 | 小规模数据,追求代码简洁易读 |
混淆 H 指数的定义:容易把「至少 h 篇论文 ≥h 次」写成「h 篇论文 exactly h 次」,导致判断条件错误(如之前有同学把解法一的 total ≥ i 写成 total === i)。
排序方向错误:解法二必须逆序排序(从大到小),若正序排序会导致逻辑混乱,无法正确统计。
忽略边界情况:如 citations = [0](H 指数 0)、citations = [100](H 指数 1),需确保两种解法都能覆盖这些场景。
LeetCode 274 题的两种解法各有优劣:计数排序法以空间换时间,适合大规模数据;逆序排序法逻辑简洁,适合小规模数据。理解这两种解法的核心在于吃透 H 指数的定义——「至少 h 篇论文 ≥h 次引用」,所有的逻辑都是围绕这个定义展开的。
建议大家在练习时,先尝试自己实现逆序排序法(容易上手),再深入理解计数排序法的优化思路,通过对比两种解法的差异,加深对「时间复杂度」和「空间复杂度」权衡的理解。
Zustand 是一个轻量级、基于 hooks 的状态管理库,核心特点是:
非常适合以下 RN 场景:
yarn add zustand
# 或
npm install zustand
React Native 无需额外配置。
// store/useCounterStore.ts
import { create } from 'zustand';
type CounterState = {
count: number;
inc: () => void;
dec: () => void;
};
export const useCounterStore = create<CounterState>((set) => ({
count: 0,
inc: () => set((state) => ({ count: state.count + 1 })),
dec: () => set((state) => ({ count: state.count - 1 })),
}));
import React from 'react';
import { View, Text, Button } from 'react-native';
import { useCounterStore } from './store/useCounterStore';
export default function Counter() {
const count = useCounterStore((state) => state.count);
const inc = useCounterStore((state) => state.inc);
return (
<View>
<Text>Count: {count}</Text>
<Button title="+" onPress={inc} />
</View>
);
}
useStore(state => state.xxx)
const store = useStore();
这样会导致任意状态变更都触发重渲染
const count = useCounterStore((s) => s.count);
const inc = useCounterStore((s) => s.inc);
或:
const { count, inc } = useCounterStore(
(s) => ({ count: s.count, inc: s.inc })
);
type UIState = {
loading: boolean;
showLoading: () => void;
hideLoading: () => void;
};
export const useUIStore = create<UIState>((set) => ({
loading: false,
showLoading: () => set({ loading: true }),
hideLoading: () => set({ loading: false }),
}));
const loading = useUIStore((s) => s.loading);
type User = {
id: string;
name: string;
};
type AuthState = {
user?: User;
login: (u: User) => void;
logout: () => void;
};
export const useAuthStore = create<AuthState>((set) => ({
user: undefined,
login: (user) => set({ user }),
logout: () => set({ user: undefined }),
}));
type ListState = {
list: string[];
loading: boolean;
fetchList: () => Promise<void>;
};
export const useListStore = create<ListState>((set) => ({
list: [],
loading: false,
fetchList: async () => {
set({ loading: true });
const res = await fetch('https://example.com/list');
const data = await res.json();
set({ list: data, loading: false });
},
}));
RN 中无需 thunk / saga。
import { shallow } from 'zustand/shallow';
const { count, inc } = useCounterStore(
(s) => ({ count: s.count, inc: s.inc }),
shallow
);
store/
├── useAuthStore.ts
├── useUIStore.ts
├── useListStore.ts
避免一个大 Store。
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
export const useAuthStore = create(
persist(
(set) => ({
token: '',
setToken: (token: string) => set({ token }),
clearToken: () => set({ token: '' }),
}),
{
name: 'auth-storage',
storage: {
getItem: AsyncStorage.getItem,
setItem: AsyncStorage.setItem,
removeItem: AsyncStorage.removeItem,
},
}
)
);
| 维度 | Zustand | Redux Toolkit |
|---|---|---|
| 学习成本 | 极低 | 中 |
| 样板代码 | 极少 | 多 |
| Provider | 不需要 | 必须 |
| 异步 | 原生支持 | thunk / saga |
| DevTools | 有 | 强 |
| 大型团队 | 一般 | 更适合 |
个人建议:
![]()
源码分析: bgithub.xyz/lessfish/un…
官网中所带注释的源码:
Underscore.js 采用了 立即执行函数表达式 (IIFE) 作为核心模块结构,创建了一个封闭的作用域,避免了全局变量污染:
这种设计方式能够让 Underscore.js :
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ?
// 场景1:CommonJS 环境(Node.js)
module.exports = factory() :
typeof define === 'function' && define.amd ?
// 场景2:AMD 环境(如 RequireJS)
define('underscore', factory) :
// 场景3:无模块化的浏览器全局环境(兜底)
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, (function () {
var current = global._; // 保存当前全局的 _ 变量
var exports = global._ = factory(); // 把 underscore 挂载到全局 _
// 解决命名冲突的 noConflict 方法
exports.noConflict = function () { global._ = current; return exports; };
}()));
}(this, (function () {
// 核心实现...
})));
Underscore.js 同时支持两种调用方式:
函数式调用
_.map([1, 2, 3], function(num) { return
num * 2; });
面向对象调用(链式)
_([1, 2, 3]).map(function(num) { return
num * 2; }).value();
这种设计通过以下核心构造函数实现:
function _$1(obj) {
if (obj instanceof _$1) return obj;
if (!(this instanceof _$1)) return new
_$1(obj);
this._wrapped = obj;
}
Underscore.js 首先将所有功能实现为独立函数,然后通过 allExports 对象统一收集:
var allExports = {
__proto__: null,
VERSION: VERSION,
restArguments: restArguments,
isObject: isObject,
// ... 其他函数
};
通过 mixin 方法,将所有函数同时挂载到构造函数和原型链上:
function mixin(obj) {
each(functions(obj), function(name) {
var func = _$1[name] = obj[name];
_$1.prototype[name] = function() {
var args = [this._wrapped];
push.apply(args, arguments);
return chainResult(this, func.apply
(_$1, args));
};
});
return _$1;
}
// 执行挂载
var _ = mixin(allExports);
这种设计使得:
_.func() 方式调用_().func() 链式调用Underscore.js 还集成了原生数组的方法,分为两类:
变更方法(Mutator)
pop/push/reverse/shift/sort/splice/unshift 这些方法的核心是修改原数组(比如 push 往原数组加元素,shift 从原数组删第一个元素),执行后原数组本身变了,方法返回值只是 “操作结果”(比如 pop 返回删除的元素),而非新数组。
// 假设包装类实例:_([1,2,3])
const arrWrapper = _([1,2,3]);
// 调用mutator方法push
arrWrapper.push(4);
console.log(arrWrapper._wrapped); // [1,2,3,4](原数组被修改)
each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
// 从数组原型(ArrayProto是Array.prototype的简写)获取对应的原生方法
var method = ArrayProto[name];
// 为包装类原型添加当前遍历的方法(如pop/push等)
_$1.prototype[name] = function() {
// 获取包装类实例中包裹的原始数组(_wrapped是包装类存储原始数据的核心属性)
var obj = this._wrapped;
// 仅当原始数组不为null/undefined时执行(避免空指针错误)
if (obj != null) {
// 调用原生数组方法,将当前方法的参数透传给原生方法,直接修改原数组
// apply作用:绑定方法执行上下文为原始数组obj,参数以数组形式传递
method.apply(obj, arguments);
// 特殊边界处理:修复shift/splice清空数组后可能残留的索引0问题
// 原因:部分场景下shift/splice把数组清空(length=0)后,obj[0]仍可能残留undefined
// 删除索引0可保证空数组的结构完全干净,符合原生数组的预期行为
if ((name === 'shift' || name === 'splice') && obj.length === 0) {
delete obj[0];
}
}
// 返回链式调用结果:保证调用该方法后仍能继续调用包装类的其他方法
// chainResult会根据是否开启链式调用,返回包装类实例(this)或修改后的原数组(obj)
return chainResult(this, obj);
};
});
function chainResult(instance, obj) {
return instance._chain ? _$1(obj).chain() : obj;
}
function chain(obj) {
var instance = _$1(obj);
instance._chain = true;
return instance;
}
访问方法(Accessor)
concat/join/slice 这些方法的核心是返回新结果,原数组完全不变(比如 concat 拼接后返回新数组,原数组还是原样;slice 截取后返回新子数组)。
// 假设包装类实例:_([1,2,3])
const arrWrapper = _([1,2,3]);
// 2. 调用accessor方法concat
const newWrapper = arrWrapper.concat([5,6]);
console.log(arrWrapper._wrapped); // [1,2,3,4](原数组仍不变)
console.log(newWrapper._wrapped); // [1,2,3,4,5,6](新结果)
// 批量为自定义数组包装类的原型挂载数组非可变方法(不会修改原数组,返回新值)
// 目标:让自定义包装类实例调用这些方法时,获取原生方法的返回结果,并保持链式调用特性
each(['concat', 'join', 'slice'], function(name) {
// 从数组原型(ArrayProto)中获取对应的原生方法(如Array.prototype.concat)
// ArrayProto是Array.prototype的简写,常见于Underscore/Lodash等工具库
var method = ArrayProto[name];
// 为自定义数组包装类(_$1)的原型挂载当前遍历的方法
_$1.prototype[name] = function() {
// 获取包装类实例中包裹的原始数组/值(_wrapped是包装类存储原始数据的核心属性)
var obj = this._wrapped;
// 仅当原始数据不为null/undefined时执行原生方法(避免空指针错误)
if (obj != null) {
// 调用原生方法,透传参数并接收返回值
// 核心差异:这类方法不修改原数组,而是返回新值,因此需要用新值覆盖obj
obj = method.apply(obj, arguments);
}
// 返回链式调用结果:将新的返回值(obj)传入chainResult,保证链式调用的正确性
// 若开启链式则返回包装类实例,未开启则返回新的数组/值
return chainResult(this, obj);
};
});
Underscore.js 的链式调用是其一大特色,通过以下机制实现:
调用 _.chain() 后,所有方法执行完都会通过 chainResult 返回「新的包装对象」(而非原始数据),因此可以继续调用原型上的方法;直到调用 value() 方法(需补充实现),取出 _wrapped 里的原始数据,结束链式。
看下链式调用如何工作:
// 链式调用示例:过滤出大于2的数,再乘以2,最后获取结果
var finalResult = _.chain([1, 2, 3, 4])
.filter(function(x) { return x > 2; })
.map(function(x) { return x * 2; })
.value();
console.log(finalResult); // 输出:[6, 8]
下面看下核心代码是怎么实现的吧 ~
// 核心包装类 _$1(对应 Underscore 的 _ 函数)
function _$1(obj) {
// 如果是 _$1 实例,直接返回
if (obj instanceof _$1) return obj;
// 如果不是实例,创建实例并存储原始数据
if (!(this instanceof _$1)) return new _$1(obj);
this._wrapped = obj; // 存储被包装的原始数据(数组/对象)
this._chain = false; // 链式标记,默认关闭
}
// 结束链式:获取最终结果
_$1.prototype.value = function() {
return this._wrapped; // 取出包装对象里的原始数据
};
function chain(obj) {
var instance = _$1(obj);
instance._chain = true; // 开启链式标记
return instance;
}
function chainResult(instance, obj) {
// 关键判断:如果开启链式,返回新的包装对象(继续链式);否则返回原始数据
return instance._chain ? _$1(obj).chain() : obj;
}
// 模拟 Underscore 的 each/functions 工具函数(简化版)
function each(arr, callback) {
for (var i = 0; i < arr.length; i++) callback(arr[i], i);
}
function functions(obj) {
return Object.keys(obj).filter(key => typeof obj[key] === 'function');
}
function mixin(obj) {
each(functions(obj), function(name) {
var func = _$1[name] = obj[name]; // 挂载到 _$1 静态方法
_$1.prototype[name] = function() {
// 1. 构造参数:第一个参数是包装的原始数据 this._wrapped,后续是方法入参
var args = [this._wrapped];
push.apply(args, arguments);
// 2. 执行工具函数,得到结果
var result = func.apply(_$1, args);
// 3. 调用 chainResult,决定返回包装对象(链式)还是原始数据
return chainResult(this, result);
};
});
return _$1;
}
// 挂载常用工具方法(模拟 Underscore 的 filter/map)
mixin({
filter: function(arr, fn) {
return arr.filter(fn);
},
map: function(arr, fn) {
return arr.map(fn);
}
});
// 给 _$1 原型挂载 chain 方法(对应用户代码里的 instance.chain())
_$1.prototype.chain = function() {
return chain(this._wrapped);
};
Underscore.js 采用函数式编程范式,提供了大量高阶函数:
纯函数 :如 map 、 filter 等,不修改原数据,避免污染原数据
函数工厂 :如 tagTester 、 createPredicateIndexFinder 等。会返回一个新函数的函数,用于复用函数逻辑,减少重复代码
// 函数工厂:生成检测特定类型的函数
const tagTester = function(tag) {
// 返回新函数(检测类型)
return function(obj) {
return Object.prototype.toString.call(obj) === `[object ${tag}]`;
};
};
// 生产具体的检测函数
_.isArray = tagTester('Array');
_.isObject = tagTester('Object');
_.isFunction = tagTester('Function');
// 使用
console.log(_.isArray([1,2])); // true
console.log(_.isObject({a:1})); // true
函数组合 :如 compose 函数,将多个函数组合成一个新函数,执行顺序为 “从右到左”,前一个函数的输出作为后一个函数的输入
// 函数组合核心实现
_.compose = function(...funcs) {
return function(...args) {
// 从右到左执行函数
return funcs.reduceRight((result, func) => [func.apply(this, result)], args)[0];
};
};
// 示例:先过滤大于2的数,再乘以2,最后求和
const filterBig = arr => _.filter(arr, x => x > 2);
const double = arr => _.map(arr, x => x * 2);
const sum = arr => _.reduce(arr, (a, b) => a + b, 0);
// 组合函数:sum(double(filterBig(arr)))
const process = _.compose(sum, double, filterBig);
console.log(process([1,2,3,4])); // (3,4)→[6,8]→14
函数柯里化 :如 partial 函数,将多参数函数拆解为单参数函数链,可分步传参,延迟执行。
// 柯里化核心实现(简化版)
_.partial = function(func, ...fixedArgs) {
return function(...remainingArgs) {
// 合并固定参数和剩余参数,执行原函数
return func.apply(this, fixedArgs.concat(remainingArgs));
};
};
// 示例:固定乘法的第一个参数为2(创建“乘以2”的函数)
const multiply = (a, b) => a * b;
const double = _.partial(multiply, 2);
// 分步传参:先传2,后传3/4
console.log(double(3)); // 6
console.log(double(4)); // 8
Underscore.js 设计了完善的跨环境兼容机制,核心是 先检测、后适配、再降级 的策略:
Underscore.js 在设计中融入了多种性能优化策略:
缓存 :如 memoize 函数,缓存计算结果
// memoize 核心实现(简化版)
_.memoize = function(func, hashFunction) {
const cache = {}; // 缓存容器
hashFunction = hashFunction || function(args) {
return args[0]; // 默认用第一个参数作为缓存key
};
return function(...args) {
const key = hashFunction.apply(this, args);
// 缓存存在则直接返回,否则执行函数并缓存
if (!cache.hasOwnProperty(key)) {
cache[key] = func.apply(this, args);
}
return cache[key];
};
};
// 示例:缓存斐波那契计算结果(避免重复递归)
const fib = _.memoize(function(n) {
return n < 2 ? n : fib(n - 1) + fib(n - 2);
});
console.log(fib(10)); // 55(首次计算,缓存结果)
console.log(fib(10)); // 55(直接取缓存,无需计算)
延迟执行 :如 debounce 、 throttle 函数
// 防抖核心实现(简化版)
_.debounce = function(func, wait) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId); // 重置延迟
timeoutId = setTimeout(() => {
func.apply(this, args);
}, wait);
};
};
// 示例:搜索框输入后500ms执行搜索
const search = _.debounce(function(keyword) {
console.log('搜索:', keyword);
}, 500);
// 快速输入时,仅最后一次输入后500ms执行
search('a');
search('ab');
search('abc'); // 仅执行这一次
惰性求值 :通过链式调用实现
链式调用时,并非每一步都立即计算,而是延迟到最后一步 value () 才执行最终计算,减少中间临时数据的生成:
// 惰性求值示例:链式调用仅在value()时执行最终逻辑
const result = _.chain([1,2,3,4])
.filter(x => x > 2) // 暂存逻辑,不立即执行
.map(x => x * 2) // 暂存逻辑,不立即执行
.value(); // 执行所有逻辑,返回结果 [6,8]
原生方法优先 :当原生方法可用时,优先使用原生方法,JavaScript 原生方法(如 Array.prototype.map、Object.keys)由引擎底层实现(C++),比纯 JS 实现快得多。
Underscore.js 设计了良好的扩展机制:
mixin 方法 :允许用户添加自定义函数,可将自定义函数挂载到 Underscore 原型上,支持链式调用:
// 示例:自定义一个“求平方和”的方法,通过mixin挂载
_.mixin({
sumOfSquares: function(arr) {
return _.reduce(arr, (sum, x) => sum + x * x, 0);
}
});
// 直接调用 + 链式调用都支持
console.log(_.sumOfSquares([1,2,3])); // 1+4+9=14
const result = _.chain([1,2,3])
.filter(x => x > 1) // [2,3]
.sumOfSquares() // 4+9=13
.value();
console.log(result); // 13
自定义 iteratee :允许用户自定义迭代器行为
// 示例:自定义迭代器,处理对象数组的特定属性
const users = [
{ name: '张三', age: 20 },
{ name: '李四', age: 30 }
];
// 自定义迭代器:提取age属性并判断是否大于25
const ageIterator = user => user.age > 25;
const result = _.filter(users, ageIterator);
console.log(result); // [{ name: '李四', age: 30 }]
模板系统 :支持自定义分隔符、变量插值规则,适配不同场景
// 示例:自定义模板分隔符(默认是<% %>,改为{{ }})
_.templateSettings = {
evaluate: /{{(.+?)}}/g, // 执行代码:{{ code }}
interpolate: /{{=(.+?)}}/g // 插值:{{= value }}
};
// 使用自定义模板
const template = _.template('Hello {{= name }}! {{ if (age > 18) { }}成年{{ } else { }}未成年{{ } }}');
const html = template({ name: '张三', age: 20 });
console.log(html); // Hello 张三! 成年
Underscore 链式调用依赖每次方法调用创建新的 _$1 包装对象,且需通过 value() 触发最终计算:
map()/filter())都会实例化新的包装对象,频繁操作大型数据集时,内存分配和垃圾回收开销显著;_$1.prototype 上,每次调用需遍历原型链,效率低于原生方法的直接调用;_.chain([1,2,3]).map(x=>x*2).value() 比原生 [1,2,3].map(x=>x*2) 多了「包装对象创建→原型链查找→结果重新包装」三层开销。Array.isArray(),而是通过 Object.prototype.toString.call() 做类型判断,效率比原生 API 低 30% 以上;for 循环(而非 for...in),对象遍历未优先用 Object.keys() 过滤原型属性;map/filter 等方法的迭代器需通过 optimizeCb 封装闭包,每次迭代都会产生函数调用栈损耗,而原生方法由引擎内联优化,无此开销。Underscore 基于 IIFE 封装核心逻辑,闭包会长期持有内部变量(如 _ 构造函数、mixin 缓存、工具函数):
_.isArray() 一个方法,整个闭包内的所有变量也无法被垃圾回收,造成内存冗余;_ 变量常驻内存,进一步增加无意义的内存占用。同时支持「函数式调用(_.map())」和「对象链式调用(_().map())」,带来双重问题:
_wrapped 包装数据,函数式模式直接传参);_.reduce() 链式调用时 this 指向包装对象,函数式调用时需手动传 context)。_.reduce(collection, iteratee, [accumulator]) 与原生 Array.prototype.reduce(callback, [initialValue]) 参数顺序相反,用户切换使用时易出错;null/undefined/ 空对象的处理逻辑混乱(如 _.map(null) 返回 [],_.keys(null) 抛出错误);_.defaults()/_.extend() 对默认值、浅拷贝的规则未明确标注,导致相同输入可能产生不同预期结果。_.each())仅对原生方法做简单封装,无额外价值却增加调用层级;_.cloneDeep() 为后期补充),需手动嵌套 _.extend() 实现,易用性差。import { map } from 'underscore' 按需导入;_.isArray(),也会打包整个库(约 5KB),而原生 Array.isArray() 无体积成本;lodash-es/map 可按需导入,体积仅几百字节,Underscore 无此能力。_$1.prototype[name] = ...),与 ES6 class 语法脱节,现代开发者可读性差;this 指向包装对象,而箭头函数的词法 this 会导致 this._wrapped 报错,增加使用复杂度;ref/reactive)适配性差,不如 Lodash/Ramda 灵活。@types/underscore,存在类型覆盖不全(如链式调用返回类型推断错误)、版本不匹配(库更新后类型定义滞后)等问题;mixin 方法直接挂载函数到 _$1.prototype,若自定义方法名与内置方法冲突(如自定义 map),会覆盖原生逻辑,导致意外行为;mixin,无法像 Vue/React 那样通过插件注册、生命周期管理复杂扩展,生态扩展性差。optimizeCb 优化迭代器),代码可读性极低;Promise/async/await,处理异步数据流(如接口请求→数据处理)时,需手动封装 _.map + Promise.all,代码冗余;debounce/throttle)仅支持同步逻辑,无法处理异步回调的时序问题。_.extend()/_.defaults() 仅支持浅拷贝,深度拷贝需手动实现或依赖第三方扩展,而原生 structuredClone() 或 Lodash _.cloneDeep() 已原生支持;_.deepKeys),处理复杂对象需多层嵌套调用。_.partial() 模拟柯里化(无法自动柯里化多参数函数),_.compose() 仅支持同步函数组合,无异步组合能力;value() 时执行,无法像 Ramda 那样实现 “按需计算”,处理超大数据集时效率低。早期版本的 _.extend()/_.defaults() 未过滤 __proto__ 属性,若传入包含 __proto__: { evil: true } 的用户输入,会修改 Object.prototype,导致全局原型污染:
// 原型污染示例(旧版本 Underscore)
const obj = {};
_.extend(obj, { __proto__: { test: 123 } });
console.log({}.test); // 123(全局原型被污染)
_.template() 方法默认使用 eval 执行模板中的代码,若未过滤用户输入的模板字符串,易引发代码注入攻击:
// 模板注入风险
const userInput = "{{= alert('XSS') }}";
const template = _.template(userInput);
template(); // 执行恶意代码
ES6+ 引入的原生 API 完全覆盖 Underscore 核心能力,且性能更优(引擎级优化):
| Underscore 方法 | 原生替代方案 | 优势 |
|---|---|---|
_.map() |
Array.prototype.map() |
无包装对象开销,引擎内联优化 |
_.keys() |
Object.keys() |
原生实现,效率更高 |
_.extend() |
Object.assign() |
原生支持,无需额外依赖 |
_.debounce() |
浏览器原生 requestIdleCallback(或框架内置) |
更贴合现代浏览器调度机制 |
arguments 对象处理参数(如 _.partial()),而现代 JS 已支持剩余参数(...args),代码更简洁;class 语法相悖,学习和维护成本高。Underscore.js 的所有缺点本质是 “早期设计无法适配现代 JavaScript 生态”:
在物联网(IoT)飞速发展的今天,低功耗、远距离、广连接的通信技术成为实现大规模设备联网的关键。其中,LoRaWAN(Long Range Wide Area Network)凭借其长距离、低功耗、大容量和高安全性等优势,已成为全球主流的低功耗广域网(LPWAN)标准之一,广泛应用于智慧城市、工业物联网、农业监测、环境监控等多个领域。
本文将带你全面了解 LoRaWAN 的协议分层、网络架构、数据传输流程以及其安全机制,并重点解析一个常被忽视却至关重要的核心组件——网络服务器(NS),为何它是整个系统不可或缺的“大脑”。
同时,我们也将介绍如何借助成熟的 LoRaWAN 平台快速搭建自己的物联网系统,助力开发者和企业高效落地项目。
LoRaWAN 是基于 LoRa 调制技术 构建的上层通信协议,由 LoRa 联盟制定并维护,专为物联网场景优化设计。其协议栈分为两层:
LoRaWAN 采用典型的星型网络结构,主要包括以下四个部分:
如温湿度传感器、智能电表、门磁、水浸报警器等,通常由电池供电,具有超低功耗特性。它们通过 LoRa 无线方式将采集的数据发送给网关。
示例:农田中的土壤湿度传感器定时上报数据,用于自动灌溉控制。
又称集中器,是连接终端与后台系统的桥梁。它能同时接收多个终端的数据,并通过以太网或4G/5G回传至网络服务器。
这是整个 LoRaWAN 系统的“中枢神经”,负责处理所有来自网关的数据,进行去重、解码、路由选择、频率管理、速率调控等关键操作。
执行具体业务逻辑,比如数据分析、可视化展示、告警推送、远程控制指令下发等。例如,在智慧园区中,应用服务器可根据温湿度数据自动调节空调系统。
正是因为这种“多对一”的汇聚结构,NS 成为了保障数据完整性、网络效率和安全性的核心节点。
安全性是物联网不可忽视的一环。LoRaWAN 提供双重加密保护:
密钥在设备入网时通过 OTAA(Over-The-Air Activation)或 ABP(Activation By Personalization)方式生成,确保每次通信的安全性。
很多人误以为只要有了终端和网关就能完成通信,但实际上,没有 NS,LoRaWAN 网络根本无法正常运行。以下是 NS 的三大核心作用:
当一个终端发出的数据被多个网关接收到时,NS 必须从中选出最佳数据包(基于信号质量),剔除冗余信息,避免应用服务器重复处理。
可以说,NS 是 LoRaWAN 网络的大脑与心脏,决定了整个系统的稳定性、扩展性和安全性。
| 优势 | 说明 |
|---|---|
| ✅ 长距离通信 | 城市数公里,郊区超10公里 |
| ✅ 超低功耗 | 电池寿命可达5~10年 |
| ✅ 大规模连接 | 单网关支持数千终端 |
| ✅ 高安全性 | AES-128 加密 + 双重密钥机制 |
| ✅ 灵活部署 | 星型结构易扩展,支持私有部署 |
| ✅ 成本低廉 | 设备与基础设施成本低 |
对于中小企业或开发者而言,自研 NS 不仅成本高、周期长,还需应对复杂的协议兼容问题。更高效的方案是使用成熟的 LoRaWAN 平台。
门思科技(Manthink) 提供了一站式 LoRaWAN 解决方案:
LoRaWAN 技术正在重塑万物互联的方式。而作为网络核心的 NS 服务器,不仅是数据流转的枢纽,更是保障系统稳定与安全的基石。
如果你正计划开展一个小规模物联网项目,不妨试试 门思科技的 ThinkLink 平台 —— 它不仅免费支持 1000个设备接入,还兼容任何品牌的 LoRaWAN 网关与终端,真正实现“开箱即用”。
无需自建服务器,无需复杂配置,注册即用,极大降低初期投入和技术门槛。
🔗 推荐阅读与资源链接:
📌 关注我们,获取更多 LoRa、LoRaWAN、网关、NS 相关的技术干货与行业案例!
#LoRa #LoRaWAN #物联网 #网关 #NS #NetworkServer #智慧城市 #低功耗广域网 #门思科技 #Manthink #ThinkLink #工业物联网 #无线通信 #CSDN #微信公众号 #技术科普
该文章围绕浏览器存储及相关技术展开,核心涵盖Cookie、LocalStorage、SessionStorage、IndexedDB 四种浏览器存储方式(各有存储大小、使用场景等差异),同时介绍了 PWA(渐进式 Web 应用) 的特性与相关工具,以及 Service Worker 的作用、运行机制和调试方式,最终通过案例分析与实战帮助学习者掌握各类技术的概念、使用及选择逻辑。
![]()
| 存储方式 | 核心定位 | 存储大小 | 关键特性 | 典型用途 |
|---|---|---|---|---|
| Cookie | 维持 HTTP 无状态的客户端状态存储 | 约 4KB | 1. 生成方式:HTTP 响应头 set-cookie、JS 的 document.cookie;2. 关联对应域名(存在 CDN 流量损耗);3. 支持 httponly 属性;4. 可设置 expire 过期时间 | 辨别用户、记录客户基础信息 |
| LocalStorage | HTML5 专用浏览器本地存储 | 约 5M | 1. 仅客户端使用,不与服务端通信;2. 接口封装更友好;3. 持久化存储(除非主动清除) | 浏览器本地缓存方案 |
| SessionStorage | 会话级浏览器存储 | 约 5M | 1. 仅客户端使用,不与服务端通信;2. 接口封装更友好;3. 会话结束后数据清除 | 临时维护表单信息 |
| IndexedDB | 客户端大容量结构化数据存储 | -(无明确限制,支持大量数据) | 1. 低级 API,支持索引;2. 高性能数据搜索;3. 弥补 Web Storage 大容量存储短板 | 为应用创建离线版本 |
定义:并非单一技术,而是通过一系列 Web 新特性 + 优秀 UI 交互设计,渐进式增强 Web App 用户体验的新模型
核心特性:
可靠:无网络环境下可提供基本页面访问,避免 “未连接到互联网” 提示
快速:针对网页渲染和网络数据访问做了专项优化
融入:可添加到手机桌面,支持全屏显示、推送等原生应用类似特性
相关工具:lighthouse(下载地址:lavas.baidu.com/doc-assets/…
答案:两者核心差异集中在 4 点:1. 存储大小:Cookie 约 4KB,LocalStorage 约 5M;2. 通信特性:Cookie 会随 HTTP 请求发送至服务端(关联域名导致 CDN 流量损耗),LocalStorage 仅在客户端使用,不与服务端通信;3. 核心定位:Cookie 侧重维持 HTTP 无状态的客户端状态,LocalStorage 是 HTML5 设计的专用本地缓存方案;4. 附加特性:Cookie 支持 expire 过期时间和 httponly 属性,LocalStorage 无过期时间(需主动清除)且无 httponly 相关设置。
答案:PWA 的核心体验依赖两大关键技术:1. Service Worker:通过后台运行的脚本拦截网络请求、管理缓存响应,实现无网络环境下的基本页面访问(支撑 “可靠” 特性),同时优化网络数据访问效率(辅助 “快速” 特性);2. IndexedDB:提供客户端大容量结构化数据存储能力,为 PWA 离线版本提供数据支撑(强化 “可靠” 特性);此外,Web 新特性与优化的 UI 交互设计共同保障了 “快速” 和 “融入”(如桌面添加、全屏显示)特性的实现。
答案:需结合存储数据量、使用场景、是否与服务端交互等需求判断:1. 若需存储少量用户标识、会话状态(需随请求发送至服务端),选择 Cookie(约 4KB,支持过期时间);2. 若需在客户端持久化存储中等容量数据(不与服务端交互),如本地缓存配置、用户偏好,选择 LocalStorage(约 5M);3. 若需临时存储会话期间的表单数据、页面临时状态(会话结束后无需保留),选择 SessionStorage(约 5M);4. 若需存储大量结构化数据(如离线应用的本地数据库),支撑应用离线使用,选择 IndexedDB(无明确容量限制,支持索引和高性能搜索)。
作者:陈盛靖
业务群里面经常反馈,视频播放卡顿,视频播放总是停留在某一时刻就播放不了了。后面经过排查,发现这是因为弱网导致的。然而,用户数量众多,隔三差五总有人在群里反馈,有时问题一时半会好不了,用户就会怀疑不是网络,而是我们的系统问题。因此,我们希望能在弱网的时候展示提示,这样用户体验会更友好,同时也能减少一定的客诉。
我们使用的播放器是chimee(www.chimee.org/index.html)。遗憾的是,chimee并没有视频播放卡顿自动展示loading的功能,不过我们可以通过其插件能力,来编写一个自定义video-loading的插件。
常见的方法就是我们通过设定一个标准,然后检测用户设备的网络速度,在到达一定阈值时展示弱网提示。这里需要确定一个重要的点:什么情况下才算弱网?
我们的应用是h5,这里我们可以使用window对象中的NetworkInformation(developer.mozilla.org/zh-CN/docs/…),我们可以通过浏览器的debug工具,打印window.naviagtor.connection,这个对象内部就存储着网络信息:
![]()
其中各个属性含义如下表所示:
| 属性 | 含义 |
|---|---|
| downlink | 返回以兆比特每秒为单位的有效带宽估计,四舍五入到最接近的 25 千比特每秒的倍数。 |
| downlinkMax | 返回底层连接技术的最大下行速度,以兆比特每秒(Mbps)为单位。 |
| effectiveType | 返回连接的有效类型(意思是“slow-2g”、“2g”、“3g”或“4g”中的一个)。此值是使用最近观察到的往返时间和下行链路值的组合来确定的。 |
| rtt | 返回当前连接的有效往返时间估计,四舍五入到最接近的 25 毫秒的倍数。 |
| saveData | 如果用户在用户代理上设置了减少数据使用的选项,则返回 true。 |
| type | 返回设备用于网络通信的连接类型。它会是以下值之一: bluetooth cellular ethernet none wifi wimax other unknown |
| onchange | 接口的 change 事件在网络连接信息发生变化时被触发,并且该事件由 NetworkInformation(developer.mozilla.org/zh-CN/docs/…) 对象接收。 |
其中,我们可以通过effectiveType判断当前网络的大体情况,并且可以拿到一个预估的网络带宽(downlink)。我们可以通过监听onchange事件,在网络变差的时候,展示对应的弱网提示。
这个方案的优点是:
但缺点却十分明显:
effectiveType的变化可能是分钟级别的,对于短暂的网络波动,状态没办法做更精细的把控
对于不同一些主流浏览器不支持,例如Firefox、Safari等
![]()
不同的设备和浏览器,由于其差异,在不同的网络情况下,视频的播放情况是不一样的,如果我们固定一个标准,可能会导致在不同设备下,同一个网络速度,有人明明正常播放视频,但是却提示网络异常,这样用户会感到疑惑。
那有没有更好的方法呢?
chimee底层也是在html video上进行的二次封装,我们可以在插件的生命周期中,拿到对应的video元素节点。而在video标签中,存在这样两个事件:waiting和canplay。
其事件描述如下图所示:
![]()
当视频播放卡顿时,会触发waiting事件;而当视频播放恢复正常时,会触发canplay事件。只要监听这两个事件,我们就可以实现对应的功能了。
我们知道,现在大多数网站的视频在提示弱网的时候,都会展示当前设备的网络速度是多少。因此我们也希望在展示对应的信息。那么怎么实现网络速度的检测呢?
一个简单的方法是,我们可以通过获取一张固定大小的图片资源(不一定是图片,也可以是别的类型的资源),并统计请求该资源的请求速度,从而计算当前网络的带宽是多少。当然,图片大小要尽可能小一点,一是为了节省用户流量,二是为了避免在网络不好的情况下,图片请求太慢导致一直计算不出来。
具体代码如下:
funtion calculateSpeed() {
// 图片大小772Byte
const fileSize = 772;
// 拼接时间戳,避免缓存
const imgUrl = `https://xxx.png?timestamp=${new Date().getTime()}`;
return new Promise((resolve, reject) => {
let start = 0;
let end = 1000;
let img = document.createElement('img');
start = new Date().getTime();
img.onload = function (e) {
end = new Date().getTime();
// 计算出来的单位为 B/s
const speed = fileSize / (end > start ? end - start : 1000) * 1000;
resolve(speed);
}
img.src = imgUrl;
}).catch(err => { throw err });
}
function translateUnit(speed) {
if(speed === 0) return '0.00 B/s';
if(speed > 1024 * 1024) return `${(speed / 1024 / 1024).toFixed(2)} MB/s`;
if(speed > 1024) return `${(speed / 1024).toFixed(2)} KB/s`;
else return `${speed.toFixed(2)} B/s`;
}
我们可以通过setInterval来轮询调用该函数,从而实时展示当前网络情况。系统流程图如下:
![]()
我们可以通过Chrome浏览器开发者工具中的Network中的网络配置来模拟弱网情况
具体效果如下:
![]()
成功实现视频弱网提示,完结撒花🎉🎉🎉🎉🎉🎉。
上篇Vben Admin管理系统集成qiankun微服务(一)遗留的三个问题:
下面分步完成以上相关内容
主应用和子应用的数据传递主要使用props实现,上篇文章已经实现了部分没有详细解释,本篇补充以上内容。 通过props.userInfo和props.token 传递登录信息和授权信息,
vue-vben-admin/apps/web-antd/src/qiankun/config.ts
/** 本地应用测试微服务架构 */
export default {
subApps: [
{
name: 'basic', // 子应用名称,跟package.json一致
// entry: import.meta.env.VITE_API_BASE_URL, // 子应用入口,本地环境下指定端口
entry: 'http://localhost:5667', // 子应用入口,本地前端环境下指定端口'http://localhost:5174',发布可以调整为主系统:/app/workflow-app/= /app/插件名称/
container: '#sub-container', // 挂载子应用的dom
activeRule: '/app/basic', // 路由匹配规则
props: {
userInfo: [],
token: '',
}, // 主应用与子应用通信传值
sandbox: {
strictStyleIsolation: true, // 启用严格样式隔离
},
},
],
};
vue-vben-admin/apps/web-antd/src/qiankun/index.ts文件,实现代码主要是在beforeLoad函数
// 参考项目:https://github.com/wstee/qiankun-web
import { useAccessStore, useUserStore } from '@vben/stores';
import { registerMicroApps } from 'qiankun';
import config from './config';
const { subApps } = config;
export async function registerApps() {
try {
// 如果子应用是不定的,可以这里定义接口从后台获取赋值给subApps,动态添加
registerMicroApps(subApps, {
beforeLoad: [
(app: any) => {
// eslint-disable-next-line no-console
console.log('[主应用] beforeLoad', app.name);
const useStore = useUserStore();
const accessStore = useAccessStore();
app.props.token = accessStore.accessToken;
app.props.userInfo = useStore.userInfo;
},
],
// 生命周期钩子
loader: (loading: any) => {
// 可以在这里处理加载状态
// eslint-disable-next-line no-console
console.log('子应用加载状态:', loading);
},
beforeMount: [
(app) => {
// eslint-disable-next-line no-console
console.log('[主应用] beforeMount', app.name);
const container = document.querySelector(app.container);
if (container) container.innerHTML = '';
},
],
afterUnmount: [
(app) => {
// eslint-disable-next-line no-console
console.log('count: %s', app);
},
],
});
} catch (error) {
// eslint-disable-next-line no-console
console.log('count: %s', error);
}
}
修改代码读取主应用传递的参数,调整mount函数 caipu-vben-admin/apps/app-antd-child/web-demo/src/main.ts
async mount(props: any) {
const { container, token, userInfo } = props;
await initApplication(container);
const useStore = useUserStore();
const accessStore = useAccessStore();
console.log('[子应用] mounting', props);
console.log('[子应用] token:', token);
console.log('[子应用] userInfo:', userInfo);
useStore.setUserInfo(userInfo);
accessStore.setAccessToken(token);
// 监听主应用的主题事件
window.addEventListener('qiankun-theme-update', handleThemeUpdate);
// 移除并销毁loading
unmountGlobalLoading();
}
如果操作子应用时登录信息失效了呢,要让应用跳转到登录,可以修改setupAccessGuard函数,按照如下修改直接跳转到系统登录页。
caipu-vben-admin/apps/app-antd-child/src/router/guard.ts
// 没有访问权限,跳转登录页面
if (to.fullPath !== LOGIN_PATH) {
// return {
// path: LOGIN_PATH,
// // 如不需要,直接删除 query
// query:
// to.fullPath === preferences.app.defaultHomePath
// ? {}
// : { redirect: encodeURIComponent(to.fullPath) },
// // 携带当前跳转的页面,登录后重新跳转该页面
// replace: true,
// };
window.location = 'http://localhost:5666/#/login';
}
这样就实现主应用和子应用的信息同步了。
vben主题相关配置是在'@vben/preferences'包中,要调整的动态配置主要是在preferences.theme当中,所以实现主题同步只要把配置信息同步到子应用即可。
未通过props传递原因是加载子应用之后再调整偏好设置和主题 子应用不生效,所以考虑只能通另外一种方式实现,最终选择 window.dispatchEvent事件监听的方式实现。
![]()
调整 vue-vben-admin/apps/web-antd/src/layouts/basic.vue
# 引用包
import { preferences } from '@vben/preferences';
# 合适位置增加主题监听
watch(
() => ({
theme: preferences.theme,
}),
async ({ theme }) => {
alert('handler qiankun-theme start', theme);
// 子应用会监听这个事件并更新响应式对象
window.dispatchEvent(
new CustomEvent('qiankun-theme-update', {
detail: preferences,
}),
);
},
{
immediate: true,
},
);
如果细心的话,在上述子应用调整的main.ts,mount函数要已有说明,主要是增加事件监听qiankun-theme-update 和监听处理事件handleThemeUpdate,完整代码如下 caipu-vben-admin/apps/app-antd-child/web-demo/src/main.ts
import { initPreferences, updatePreferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores';
import '@vben/styles';
import '@vben/styles/antd';
import { unmountGlobalLoading } from '@vben/utils';
import {
qiankunWindow,
renderWithQiankun,
} from 'vite-plugin-qiankun/dist/helper';
import { bootstrap } from './bootstrap';
import { overridesPreferences } from './preferences';
let app: any = null;
/**
* 应用初始化完成之后再进行页面加载渲染
*/
async function initApplication(container: any = null) {
// name用于指定项目唯一标识
// 用于区分不同项目的偏好设置以及存储数据的key前缀以及其他一些需要隔离的数据
const env = import.meta.env.PROD ? 'prod' : 'dev';
const appVersion = import.meta.env.VITE_APP_VERSION;
const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${appVersion}-${env}`;
// app偏好设置初始化
await initPreferences({
namespace,
overrides: overridesPreferences,
});
// 启动应用并挂载
// vue应用主要逻辑及视图
app = await bootstrap(namespace, container);
// 移除并销毁loading
unmountGlobalLoading();
}
const initQianKun = async () => {
renderWithQiankun({
async mount(props: any) {
const { container, token, userInfo } = props;
await initApplication(container);
const useStore = useUserStore();
const accessStore = useAccessStore();
console.log('[子应用] mounting', props);
console.log('[子应用] token:', token);
console.log('[子应用] userInfo:', userInfo);
useStore.setUserInfo(userInfo);
accessStore.setAccessToken(token);
window.addEventListener('qiankun-theme-update', handleThemeUpdate);
// 移除并销毁loading
unmountGlobalLoading();
},
bootstrap() {
return new Promise((resolve, reject) => {
// eslint-disable-next-line no-console
console.log('[qiankun] app bootstrap');
resolve();
});
},
update(props: any) {
// eslint-disable-next-line no-console
console.log('[子应用] update');
const { container } = props;
initApplication(container);
},
unmount(props) {
// 移除事件监听
if (handleThemeUpdate) {
// eslint-disable-next-line no-console
console.log('remove sub apps theme handle:', app.name);
window.removeEventListener('qiankun-theme-update', handleThemeUpdate);
}
// eslint-disable-next-line no-console
console.log('[子应用] unmount', props);
app?.unmount();
app = null;
},
});
};
// 判断是否为乾坤环境,否则会报错iqiankun]: Target container with #subAppContainerVue3 not existed while subAppVue3 mounting!
qiankunWindow.__POWERED_BY_QIANKUN__
? await initQianKun()
: await initApplication();
const handleThemeUpdate = (event: any) => {
const newTheme = event.detail;
if (newTheme) {
// 更新响应式对象,由于是响应式的,Vue 会自动更新视图
console.log('子应用主题已更新(通过 props + 事件):', newTheme);
updatePreferences(newTheme);
}
};
子应用如果不是固定subApps,要从后台加载那如何实现呢,比如我的程序实现子应用动态插拔,后台安装子应用之后前台就要支持展示。 代码逻辑是:本地调试从config.ts获取固定配置,发布环境读取后台配置。主要看registerApps()。 核心代码是下面这段:
if (import.meta.env.PROD) {
const data = await GetMicroApp();
// 将获取的子应用数据转换为qiankun需要的格式
subApps = data.map((app: MicroApp) => ({
name: app.name, // 子应用名称
entry: app.entry, // 子应用入口地址
container: '#sub-container', // 子应用挂载节点
activeRule: app.activeRule, // 子应用激活规则
props: {
userInfo: [],
token: '',
}, // 主应用与子应用通信传值
sandbox: {
strictStyleIsolation: true, // 启用严格样式隔离
},
}));
}
完整文件代码是:
import type { MicroApp } from '#/api/apps/model';
import { useAccessStore, useUserStore } from '@vben/stores';
// 参考项目:https://github.com/wstee/qiankun-web
import { registerMicroApps } from 'qiankun';
import { GetMicroApp } from '#/api/apps';
import config from './config';
let { subApps } = config;
export async function registerApps() {
try {
// 判断是否是发布环境,发布环境从后台获p取subApps
if (import.meta.env.PROD) {
const data = await GetMicroApp();
// 将获取的子应用数据转换为qiankun需要的格式
subApps = data.map((app: MicroApp) => ({
name: app.name, // 子应用名称
entry: app.entry, // 子应用入口地址
container: '#sub-container', // 子应用挂载节点
activeRule: app.activeRule, // 子应用激活规则
props: {
userInfo: [],
token: '',
}, // 主应用与子应用通信传值
sandbox: {
strictStyleIsolation: true, // 启用严格样式隔离
},
}));
}
registerMicroApps(subApps, {
beforeLoad: [
(app: any) => {
// eslint-disable-next-line no-console
console.log('[主应用] beforeLoad', app.name);
const useStore = useUserStore();
const accessStore = useAccessStore();
app.props.token = accessStore.accessToken;
app.props.userInfo = useStore.userInfo;
// app.props.publicKey = import.meta.env.VITE_PUBLIC_KEY;
},
],
// 生命周期钩子
loader: (loading: any) => {
// 可以在这里处理加载状态
// eslint-disable-next-line no-console
console.log('子应用加载状态:', loading);
},
beforeMount: [
(app) => {
// eslint-disable-next-line no-console
console.log('[主应用] beforeMount', app.name);
// const container = document.querySelector(app.container);
// if (container) container.innerHTML = '';
// 仅隐藏容器,不删除 DOM
if (app.container.style) {
app.container.style.display = 'none';
}
},
],
beforeUnmount: (app) => {
// 重新显示容器
if (app.container.style) {
app.container.style.display = 'none';
}
},
afterUnmount: [
(app) => {
// eslint-disable-next-line no-console
console.log('count: %s', app);
},
],
});
} catch (error) {
// eslint-disable-next-line no-console
console.log('count: %s', error);
}
}
GetMicroApp()返回数据结构json结果如下,主要是data的内容:
{
"code": 200,
"data": [
{
"name": "caipu-site",
"entry": "/app/caipu-site/",
"activeRule": "/app/caipu-site"
},
{
"name": "email",
"entry": "/app/email/",
"activeRule": "/app/email"
},
{
"name": "ip2region",
"entry": "/app/ip2region/",
"activeRule": "/app/ip2region"
},
{
"name": "testdata",
"entry": "/app/testdata/",
"activeRule": "/app/testdata"
}
],
"msg": "",
"success": true,
"timestamp": 1768140865000
}
上文有小伙伴回复是否可以支持主应用多页签切换不同子应用的页面状态保持,抱歉多次尝试未在vben实现此功能,作为一名后端人员技术有限如您有实现方案,请不吝指教。
抽时间也会尝试下wujie微前端方案完善相关功能,基于以上浅显内容,欢迎大积极尝试和分享。 如你有更好的建议内容分享请给评论。
如有幸被转载请注明出处: go-caipu