Merge branch 'refine/detail-optimization'
This commit is contained in:
+34
-22
@@ -28,6 +28,9 @@ export function isDemoMode() {
|
||||
let rawCache = null
|
||||
let rawCacheAt = null
|
||||
|
||||
// 包列表构建缓存(rawCache/ignored/threshold 不变时复用,避免每次请求全量 map 7000+ 条)
|
||||
let packagesCache = null
|
||||
|
||||
// —— 异步扫描状态(后台重扫时供前端轮询进度) ——
|
||||
export const scanState = {
|
||||
scanning: false, done: false, packages: 0, bytes: 0, tgzBytes: 0, diskBytes: null,
|
||||
@@ -227,6 +230,7 @@ export function hasScanCache() {
|
||||
export function invalidateScan() {
|
||||
rawCache = null
|
||||
rawCacheAt = null
|
||||
packagesCache = null // 清空包列表构建缓存
|
||||
clearPkgCache() // storage 目录切换 / 清理动作后废弃增量缓存,避免旧目录条目残留
|
||||
}
|
||||
|
||||
@@ -235,6 +239,7 @@ export function getScannedAt() {
|
||||
}
|
||||
|
||||
// 构建包列表(统一状态计算 + 排序 + 忽略标记 + 来源筛选)
|
||||
// 缓存全量列表:rawCache/ignored/threshold 不变时复用,避免每次请求全量 map 7000+ 条
|
||||
export function buildPackages(keyword = '', origin = 'all') {
|
||||
const threshold = Number(getSetting('version_threshold', DEFAULT_VERSION_THRESHOLD))
|
||||
const ignoredRows = getIgnoredRows()
|
||||
@@ -242,27 +247,34 @@ export function buildPackages(keyword = '', origin = 'all') {
|
||||
const ignoredAtMap = Object.fromEntries(ignoredRows.map(r => [r.pkg_name, (r.ignored_at || '').slice(0, 10)]))
|
||||
const raw = getRawPackages() || []
|
||||
|
||||
let list = raw.map(p => {
|
||||
let status
|
||||
if (p.status === 'broken') status = 'broken'
|
||||
else if (ignoredSet.has(p.name)) status = 'ignored'
|
||||
else status = p.versions > threshold ? 'warn' : 'ok'
|
||||
return {
|
||||
name: p.name,
|
||||
versions: p.versions,
|
||||
size: p.size,
|
||||
lastUpdated: p.lastUpdated,
|
||||
visits: p.visits || 0,
|
||||
distTags: p.distTags || {},
|
||||
status,
|
||||
ignoredAt: ignoredAtMap[p.name] || null,
|
||||
description: p.description || null,
|
||||
homepage: p.homepage || null,
|
||||
// 原版界面字段:来源(private/proxy)与最新版本;列表不携带 readme/versionsDetail(响应体小),详情接口按需返回
|
||||
origin: p.origin || null,
|
||||
latest: p.latest || null,
|
||||
}
|
||||
})
|
||||
// 缓存 key:raw 长度 + ignored 内容 hash + threshold(任一变化即失效)
|
||||
const ignoredHash = ignoredRows.map(r => `${r.pkg_name}:${r.ignored_at || ''}`).join('|')
|
||||
const cacheKey = `${raw.length}_${ignoredHash}_${threshold}`
|
||||
if (!packagesCache || packagesCache.key !== cacheKey) {
|
||||
const list = raw.map(p => {
|
||||
let status
|
||||
if (p.status === 'broken') status = 'broken'
|
||||
else if (ignoredSet.has(p.name)) status = 'ignored'
|
||||
else status = p.versions > threshold ? 'warn' : 'ok'
|
||||
return {
|
||||
name: p.name,
|
||||
versions: p.versions,
|
||||
size: p.size,
|
||||
lastUpdated: p.lastUpdated,
|
||||
visits: p.visits || 0,
|
||||
distTags: p.distTags || {},
|
||||
status,
|
||||
ignoredAt: ignoredAtMap[p.name] || null,
|
||||
description: p.description || null,
|
||||
homepage: p.homepage || null,
|
||||
// 原版界面字段:来源(private/proxy)与最新版本;列表不携带 readme/versionsDetail(响应体小),详情接口按需返回
|
||||
origin: p.origin || null,
|
||||
latest: p.latest || null,
|
||||
}
|
||||
})
|
||||
packagesCache = { key: cacheKey, list }
|
||||
}
|
||||
let list = packagesCache.list
|
||||
|
||||
// 检索:不区分大小写子串,匹配包名(版本号匹配由前端本地完成,避免列表携带明细)
|
||||
const kw = keyword.trim().toLowerCase()
|
||||
@@ -276,7 +288,7 @@ export function buildPackages(keyword = '', origin = 'all') {
|
||||
|
||||
// 排序:预警置顶 → 正常 → 忽略 → 异常;同状态按版本数降序
|
||||
const rank = { warn: 0, ok: 1, ignored: 2, broken: 3 }
|
||||
list.sort((a, b) => {
|
||||
list = [...list].sort((a, b) => {
|
||||
if (rank[a.status] !== rank[b.status]) return rank[a.status] - rank[b.status]
|
||||
return b.versions - a.versions
|
||||
})
|
||||
|
||||
@@ -20,6 +20,7 @@ process.env.UV_THREADPOOL_SIZE = process.env.UV_THREADPOOL_SIZE || '8'
|
||||
import express from 'express'
|
||||
import path from 'node:path'
|
||||
import fs from 'node:fs'
|
||||
import zlib from 'node:zlib'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { resolveStorageDir } from './scanner.js'
|
||||
import { isDemoMode, scanState, runScanAsync } from './scan-manager.js'
|
||||
@@ -40,6 +41,26 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const app = express()
|
||||
app.use(express.json({ limit: '1mb' }))
|
||||
|
||||
// gzip 压缩:大响应(如 /api/packages 7000+ 条)减少传输体积 60-80%,Node 内置 zlib 无需新依赖
|
||||
app.use((req, res, next) => {
|
||||
const accept = req.headers['accept-encoding'] || ''
|
||||
if (!accept.includes('gzip')) return next()
|
||||
const originalJson = res.json.bind(res)
|
||||
res.json = (data) => {
|
||||
try {
|
||||
const json = JSON.stringify(data)
|
||||
if (json.length < 1024) return originalJson(data) // 小响应不压缩,避免开销
|
||||
const compressed = zlib.gzipSync(json)
|
||||
res.setHeader('Content-Encoding', 'gzip')
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.send(compressed)
|
||||
} catch {
|
||||
originalJson(data)
|
||||
}
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
const PORT = Number(process.env.PORT || 3000)
|
||||
|
||||
// —— 路由装配:登录/登出/主题公开,其余 /api/* 需鉴权 ——
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
# storage 需读写挂载:清理版本要删除 .tgz 并回写 package.json 元数据(只读会报 EROFS);
|
||||
# 容器以 root 运行以写入 verdaccio 卷(其属主为 verdaccio 用户 uid 10001,非本容器 uid 10000)
|
||||
services:
|
||||
verdaccio-manager:
|
||||
verdaccio-mate:
|
||||
image: verdaccio-mate:latest # 已导入的 amd64 镜像(docker load 后直接可用)
|
||||
restart: always
|
||||
user: "0:0" # root:清理需写 verdaccio 卷;vcm-data 命名卷 root 亦可写
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user