Files
font2pic/miniprogram/utils/mp/font-loader.js
2026-02-10 14:10:20 +08:00

241 lines
7.0 KiB
JavaScript

const { request, downloadFile, readFile } = require('./wx-promisify')
const { ensureRouteReady, checkRouteOnFailure } = require('./route-manager')
const localFonts = require('../../assets/fonts')
const localDefaultConfig = require('../../assets/default')
const fontBufferCache = new Map()
const MAX_FONT_CACHE = 4
function normalizePath(path, baseUrl) {
if (!path) {
return ''
}
if (/^https?:\/\//i.test(path)) {
return path
}
if (path.startsWith('//')) {
return `https:${path}`
}
if (path.startsWith('/')) {
return `${baseUrl}${path}`
}
return `${baseUrl}/${path}`
}
function normalizeFontItem(item, baseUrl) {
const path = item.path || item.url || ''
const normalizedPath = normalizePath(path, baseUrl)
const filename = item.filename || normalizedPath.split('/').pop() || `${item.name || 'font'}.ttf`
return {
id: item.id || `${item.category || '默认'}/${item.name || filename}`,
name: item.name || filename.replace(/\.[^.]+$/, ''),
category: item.category || '默认',
filename,
path,
url: normalizedPath,
}
}
function normalizeManifest(fonts, baseUrl) {
if (!Array.isArray(fonts)) {
return []
}
return fonts
.map((item) => normalizeFontItem(item, baseUrl))
.filter((item) => item.url)
}
function normalizeDefaultConfig(config) {
const payload = config && typeof config === 'object' ? config : {}
const selectedRaw = Array.isArray(payload.selectedFontIds)
? payload.selectedFontIds
: (Array.isArray(payload.selectedFonts) ? payload.selectedFonts : [])
const favoriteRaw = Array.isArray(payload.favoriteFontIds)
? payload.favoriteFontIds
: (Array.isArray(payload.favoriteFonts) ? payload.favoriteFonts : [])
const selectedFontIds = Array.from(
new Set(
selectedRaw
.map((item) => String(item || '').trim())
.filter(Boolean),
),
)
const favoriteFontIds = Array.from(
new Set(
favoriteRaw
.map((item) => String(item || '').trim())
.filter(Boolean),
),
)
const result = {
selectedFontIds,
favoriteFontIds,
}
if (typeof payload.inputText === 'string') {
result.inputText = payload.inputText
}
if (typeof payload.textColor === 'string') {
result.textColor = payload.textColor
}
if (payload.fontSize !== undefined && payload.fontSize !== null && payload.fontSize !== '') {
const fontSize = Number(payload.fontSize)
if (Number.isFinite(fontSize)) {
result.fontSize = fontSize
}
}
if (payload.letterSpacing !== undefined && payload.letterSpacing !== null && payload.letterSpacing !== '') {
const letterSpacing = Number(payload.letterSpacing)
if (Number.isFinite(letterSpacing)) {
result.letterSpacing = letterSpacing
}
}
return result
}
function buildDefaultConfigUrl(manifestUrl, baseUrl) {
const manifest = String(manifestUrl || '').trim()
if (manifest) {
const withoutHash = manifest.split('#')[0]
const [pathPart] = withoutHash.split('?')
const slashIndex = pathPart.lastIndexOf('/')
if (slashIndex >= 0) {
return `${pathPart.slice(0, slashIndex + 1)}default.json`
}
}
return normalizePath('/miniprogram/assets/default.json', baseUrl)
}
async function loadFontsManifest(options = {}) {
const app = getApp()
await ensureRouteReady(app)
const manifestUrl = options.manifestUrl || app.globalData.fontsManifestUrl
const baseUrl = options.baseUrl || app.globalData.fontsBaseUrl
if (Array.isArray(app.globalData.fonts) && app.globalData.fonts.length > 0) {
return app.globalData.fonts
}
try {
const response = await request({
url: manifestUrl,
method: 'GET',
timeout: 10000,
})
if (response.statusCode < 200 || response.statusCode >= 300) {
throw new Error(`获取字体清单失败,状态码: ${response.statusCode}`)
}
const fonts = normalizeManifest(response.data, baseUrl)
if (!fonts.length) {
throw new Error('字体清单为空')
}
app.globalData.fonts = fonts
return fonts
} catch (error) {
console.warn('远程字体清单加载失败,回退到本地清单:', error)
await checkRouteOnFailure(app).catch((routeError) => {
console.warn('字体清单失败后的路由检查失败:', routeError)
})
const effectiveBaseUrl = app.globalData.fontsBaseUrl || baseUrl
const fallbackFonts = normalizeManifest(localFonts, effectiveBaseUrl)
app.globalData.fonts = fallbackFonts
return fallbackFonts
}
}
async function loadDefaultConfig(options = {}) {
const app = getApp()
await ensureRouteReady(app)
const manifestUrl = options.manifestUrl || app.globalData.fontsManifestUrl
const baseUrl = options.baseUrl || app.globalData.fontsBaseUrl
const defaultConfigUrl = options.defaultConfigUrl ||
app.globalData.defaultConfigUrl ||
buildDefaultConfigUrl(manifestUrl, baseUrl)
if (app.globalData.defaultConfig && typeof app.globalData.defaultConfig === 'object') {
return app.globalData.defaultConfig
}
try {
const response = await request({
url: defaultConfigUrl,
method: 'GET',
timeout: 10000,
})
if (response.statusCode < 200 || response.statusCode >= 300) {
throw new Error(`获取默认配置失败,状态码: ${response.statusCode}`)
}
const remoteConfig = normalizeDefaultConfig(response.data)
app.globalData.defaultConfig = remoteConfig
return remoteConfig
} catch (error) {
console.warn('远程 default.json 加载失败,回退到本地默认配置:', error)
await checkRouteOnFailure(app).catch((routeError) => {
console.warn('default 配置失败后的路由检查失败:', routeError)
})
const fallbackConfig = normalizeDefaultConfig(localDefaultConfig)
app.globalData.defaultConfig = fallbackConfig
return fallbackConfig
}
}
function setLruCache(key, value) {
if (fontBufferCache.has(key)) {
fontBufferCache.delete(key)
}
fontBufferCache.set(key, value)
while (fontBufferCache.size > MAX_FONT_CACHE) {
const firstKey = fontBufferCache.keys().next().value
fontBufferCache.delete(firstKey)
}
}
async function loadFontBuffer(fontItem) {
const cacheKey = fontItem.id
if (fontBufferCache.has(cacheKey)) {
const cached = fontBufferCache.get(cacheKey)
setLruCache(cacheKey, cached)
return cached
}
if (!fontItem.url) {
throw new Error('字体地址为空')
}
const downloadRes = await downloadFile({ url: fontItem.url })
if (downloadRes.statusCode < 200 || downloadRes.statusCode >= 300) {
throw new Error(`字体下载失败,状态码: ${downloadRes.statusCode}`)
}
const readRes = await readFile(downloadRes.tempFilePath)
const result = {
tempFilePath: downloadRes.tempFilePath,
buffer: readRes.data,
}
setLruCache(cacheKey, result)
return result
}
function listCategories(fonts) {
const set = new Set(fonts.map((font) => font.category || '默认'))
return ['全部', '收藏', ...Array.from(set)]
}
module.exports = {
loadFontsManifest,
loadDefaultConfig,
loadFontBuffer,
listCategories,
}