perf: skulpt 改为按需加载,client-zip 换成 fflate
skulpt 此前被 SubmitCode.vue 静态引用,945K 的 chunk 落在做题页首屏, 学生打开任何一道题都要下 233KB gzip,只为提交时拿一个语法错误行号。 改成 import() 懒加载并在语言切到 Python3 时预取,加载失败放行提交、 交给后端判题兜底。构建产物确认该 chunk 只剩一处 import() 引用, index.html 不再引用。 client-zip 的能力 fflate 已经覆盖(utils/functions.ts 本就在用 zlibSync), 新增 createZipBlob 统一两处测试用例打包,删掉该依赖。压缩方式由 store 变为 deflate,Python zipfile 透明解压,后端不受影响。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { downloadZip } from "client-zip"
|
||||
import type { LANGUAGE, SQLDisplay, Testcase } from "utils/types"
|
||||
import { createZipBlob } from "utils/functions"
|
||||
import SQLDataTable from "oj/problem/components/SQLDataTable.vue"
|
||||
import {
|
||||
generateSQLTestcase,
|
||||
@@ -156,15 +156,13 @@ async function preview() {
|
||||
async function upload() {
|
||||
isUploading.value = true
|
||||
try {
|
||||
const now = new Date()
|
||||
const data = scripts.value
|
||||
.filter((s) => s.sql.trim())
|
||||
.map((s, i) => ({
|
||||
name: `${i + 1}.sql`,
|
||||
input: s.sql,
|
||||
lastModified: now,
|
||||
content: s.sql,
|
||||
}))
|
||||
const blob = await downloadZip(data).blob()
|
||||
const blob = createZipBlob(data)
|
||||
const file = new File([blob], "testcase.zip", { type: "application/zip" })
|
||||
|
||||
const res = await uploadTestcases(file, { sql: true })
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { downloadZip } from "client-zip"
|
||||
import type { LANGUAGE, Testcase } from "utils/types"
|
||||
import { createZipBlob } from "utils/functions"
|
||||
import { createTestSubmission } from "utils/judge"
|
||||
import { uploadTestcases } from "../../api"
|
||||
|
||||
@@ -158,15 +158,14 @@ async function run() {
|
||||
async function upload() {
|
||||
isUploading.value = true
|
||||
try {
|
||||
const now = new Date()
|
||||
const data = files.value
|
||||
.filter((f) => f.in.trim() && f.out && !f.error)
|
||||
.flatMap((f, i) => [
|
||||
{ name: `${i + 1}.in`, input: f.in, lastModified: now },
|
||||
{ name: `${i + 1}.out`, input: f.out, lastModified: now },
|
||||
{ name: `${i + 1}.in`, content: f.in },
|
||||
{ name: `${i + 1}.out`, content: f.out },
|
||||
])
|
||||
|
||||
const blob = await downloadZip(data).blob()
|
||||
const blob = createZipBlob(data)
|
||||
const file = new File([blob], "testcase.zip", { type: "application/zip" })
|
||||
|
||||
const res = await uploadTestcases(file)
|
||||
|
||||
@@ -17,7 +17,10 @@ import SubmissionResult from "./SubmissionResult.vue"
|
||||
import { getSubmitButtonState } from "./submitButtonState"
|
||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||
import { useUserStore } from "shared/store/user"
|
||||
import { checkPythonSyntax } from "oj/problem/utils/pythonSyntaxCheck"
|
||||
import {
|
||||
checkPythonSyntax,
|
||||
prefetchPythonSyntaxChecker,
|
||||
} from "oj/problem/utils/pythonSyntaxCheck"
|
||||
|
||||
// ==================== 异步组件 ====================
|
||||
const ProblemComment = defineAsyncComponent(
|
||||
@@ -50,6 +53,16 @@ const showResult = ref(false)
|
||||
const isFormatting = ref(false)
|
||||
const isSubmittingRequest = ref(false)
|
||||
|
||||
// ==================== Python 语法检测器预取 ====================
|
||||
// 选中 Python3 时就把 Skulpt 拉下来,避免点提交时才开始下载
|
||||
watch(
|
||||
() => codeStore.code.language,
|
||||
(language) => {
|
||||
if (language === "Python3") prefetchPythonSyntaxChecker()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// ==================== 提交冷却 ====================
|
||||
const { start: startCooldown, isPending: isCooldown } = useTimeout(5000, {
|
||||
controls: true,
|
||||
@@ -99,7 +112,7 @@ async function submit() {
|
||||
|
||||
// 0. Python3 语法检测
|
||||
if (codeStore.code.language === "Python3") {
|
||||
const syntaxError = checkPythonSyntax(codeStore.code.value)
|
||||
const syntaxError = await checkPythonSyntax(codeStore.code.value)
|
||||
if (syntaxError) {
|
||||
message.warning(`第 ${syntaxError.line} 行存在语法错误,请修正后再提交`)
|
||||
return
|
||||
|
||||
@@ -1,15 +1,47 @@
|
||||
// @ts-ignore - skulpt has no type definitions
|
||||
import Sk from "skulpt"
|
||||
|
||||
export interface PythonSyntaxError {
|
||||
line: number
|
||||
}
|
||||
|
||||
let skulptPromise: Promise<any> | null = null
|
||||
|
||||
/**
|
||||
* 按需加载 Skulpt(约 233KB gzip),只在提交 Python3 代码时才下载。
|
||||
* 结果缓存,同一页面只加载一次。
|
||||
*/
|
||||
function loadSkulpt(): Promise<any> {
|
||||
if (!skulptPromise) {
|
||||
// @ts-ignore - skulpt has no type definitions
|
||||
skulptPromise = import("skulpt").then((m) => m.default ?? m)
|
||||
}
|
||||
return skulptPromise
|
||||
}
|
||||
|
||||
/**
|
||||
* 提前把 Skulpt 拉下来,避免点提交时才开始下载。
|
||||
* 失败不影响功能,提交时会再试一次。
|
||||
*/
|
||||
export function prefetchPythonSyntaxChecker() {
|
||||
loadSkulpt().catch(() => {
|
||||
skulptPromise = null
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 用 Skulpt 检测 Python 代码中的语法错误。
|
||||
* 只编译不执行,不受 input() 等 IO 调用影响。
|
||||
* 加载失败时返回 null(放行提交),交给后端判题兜底。
|
||||
*/
|
||||
export function checkPythonSyntax(code: string): PythonSyntaxError | null {
|
||||
export async function checkPythonSyntax(
|
||||
code: string,
|
||||
): Promise<PythonSyntaxError | null> {
|
||||
let Sk: any
|
||||
try {
|
||||
Sk = await loadSkulpt()
|
||||
} catch {
|
||||
skulptPromise = null
|
||||
return null
|
||||
}
|
||||
|
||||
Sk.configure({ output: () => {} })
|
||||
try {
|
||||
Sk.compile(code, "prog.py", "exec")
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { getTime, intervalToDuration, parseISO, type Duration } from "date-fns"
|
||||
import { User } from "./types"
|
||||
import { USER_TYPE } from "./constants"
|
||||
import { strFromU8, strToU8, unzlibSync, zlibSync } from "fflate"
|
||||
import {
|
||||
strFromU8,
|
||||
strToU8,
|
||||
unzlibSync,
|
||||
zipSync,
|
||||
zlibSync,
|
||||
type Zippable,
|
||||
} from "fflate"
|
||||
import copyTextFallback from "copy-text-to-clipboard"
|
||||
import { customAlphabet } from "nanoid"
|
||||
|
||||
@@ -208,6 +215,22 @@ export function atou(base64: string): string {
|
||||
return strFromU8(unzipped)
|
||||
}
|
||||
|
||||
/**
|
||||
* 把若干文本文件打包成 zip Blob
|
||||
* @param files 文件名和文本内容
|
||||
* @param mtime 归档内的修改时间,默认当前时间
|
||||
*/
|
||||
export function createZipBlob(
|
||||
files: { name: string; content: string }[],
|
||||
mtime: Date = new Date(),
|
||||
): Blob {
|
||||
const entries: Zippable = {}
|
||||
for (const f of files) {
|
||||
entries[f.name] = strToU8(f.content)
|
||||
}
|
||||
return new Blob([zipSync(entries, { mtime })], { type: "application/zip" })
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制文本到剪贴板
|
||||
* 优先使用 Clipboard API(支持在 modal 中使用),失败时回退到 copy-text-to-clipboard
|
||||
|
||||
Reference in New Issue
Block a user