资讯动态

前端个人项目实战:大屏自适应、大文件上传、国际化与微前端落地

发布时间:2026/9/19 9:15:29 来源:尧图企业网站定制
前端个人项目开发记录这个系列写到第六篇内容开始从“实现功能”往“方案取舍”上偏了。这一篇我打算把最近一个中后台项目里比较有代表性的几个环节一次性整理出来大屏自适应、大文件上传、国际化、微前端。项目本身不算大但涉及到的都是日常前端开发里高频出现的场景正好把当时做的技术选型和踩坑过程一起记录下来。这篇内容适合两类人看。一类是正在做前端个人项目、想从“能跑”进阶到“像样”的开发者另一类是工作中经常要处理后台管理系统、数据可视化、多语言这类常见场景的前端工程师。整体不会讲太多框架源码层面的东西更多是落地层面的细节和取舍偏实操向。每个模块我都会给出具体实现思路和关键代码你可以直接拿去做参考。1. 整体定位与方案选型1.1 为什么还是要用 Vue3 Element Plus这个项目一开始我就定了调子模拟一个真实团队的开发模式而不是只做单个静态页面。所以技术栈上选择了 Vue3 Element Plus这是当下中后台项目里非常稳定的组合。有人可能会觉得这套东西太常见没什么亮点但我的经验是对于个人项目技术的“稳定”远比“新奇”重要。Element Plus 组件齐全、API 设计符合直觉遇到问题基本在社区都能找到答案个人开发时能省下大量排查时间。项目里我同时引入了一个基于 ECharts 的大屏看板模块一个文件管理模块一个多语言切换模块还预留了微前端接入的扩展位。这些模块单独看不复杂但组合到一起之后配置、路由、状态管理、构建产物治理都会变得复杂起来。个人项目到这一步才真正有一点“工程化”的感觉。1.2 四个核心模块怎么塞进同一个项目这四个模块不是随意堆叠而是模拟一个后台系统从 0 到 1 再到多团队协作的演进过程大屏模块解决“数据展示”问题属于前端可视化能力。文件上传解决“数据生产”问题属于前端工程中非常典型的交互场景。国际化解决“产品落地”问题属于业务全球化时必备的基础能力。微前端解决“组织协作”问题属于架构层面把单体应用拆分成可独立部署的子应用。把这四块内容放在同一个项目里一方面让个人作品更完整另一方面也方便观察它们之间的边界。比如微前端改造之后国际化配置要放到主应用还是子应用大屏文件上传的接口前缀要如何处理这些交叉问题比单一模块的难度高不少。2. 大屏自适应从 rem 到 scale 的取舍2.1 三种大屏适配思路对比大屏适配业内常见方案有三种。我先列个表做对比再讲一下我为什么最后选了第三种。方案核心思路优点缺点适用场景rem / vw 方案根据屏幕宽度动态计算基准字号页面元素随容器缩放布局自适应粒度细字体和容器容易产生精度误差图表尺寸不连续移动端优先的页面媒体查询 百分比通过断点切换布局样式可控性强需要写多套样式无法覆盖所有分辨率后台普通响应式页面transform scale 等比例缩放按照设计稿固定宽高按比例整体缩放开发体验一致不会出现布局错乱屏幕比例不一致时会有留白数据大屏、固定比例场景我这个项目的设计稿是 1920 x 1080大屏只在这个基准宽高下开发其他分辨率下利用 CSS 的 transform 属性进行整体缩放。这样做最大的好处是开发时你不用纠结“这个卡片在 1366 下怎么换行”只要保证设计稿 1920 下好看其他尺寸交给 JS 自动缩放。2.2 用指令封装缩放逻辑scale 方案的实现并不复杂关键是要封装好避免每个页面都重复去写监听代码。我实现了一个v-screen-scale自定义指令核心逻辑是// screen-scale.js import { onMounted, onBeforeUnmount } from vue const designWidth 1920 const designHeight 1080 function setScale(el, scale) { el.style.transform scale(${scale}) el.style.transformOrigin left top el.style.width ${designWidth}px el.style.height ${designHeight}px } export const screenScale { mounted(el, binding) { const { width designWidth, height designHeight } binding.value || {} function resize() { const scaleX window.innerWidth / width const scaleY window.innerHeight / height const scale Math.min(scaleX, scaleY) setScale(el, scale) } resize() el.__resizeHandler__ resize window.addEventListener(resize, resize) }, unmounted(el) { window.removeEventListener(resize, el.__resizeHandler__) } }在模板里使用的时候直接给大屏容器挂上指令即可template div v-screen-scale{ width: 1920, height: 1080 } classdashboard-page !-- 页面内容 -- /div /template这里transformOrigin: left top是关键不带这一句缩放原点默认是元素中心缩放后整体位置就偏了。2.3 缩放方案的两个隐藏坑第一个坑是事件坐标偏移。缩放之后鼠标点击的真实坐标和设计稿坐标是不同步的。如果大屏上有点击交互比如点击某个地图区域弹详情需要做一个坐标换算function getMousePositionOnDesign(e) { const rect document.querySelector(.dashboard-page).getBoundingClientRect() const scaleX e.clientX - rect.left const scaleY e.clientY - rect.top return { x: scaleX / (rect.width / 1920), y: scaleY / (rect.height / 1080) } }如果不做这个换算你会发现缩放比例越大点击偏移越明显。第二个坑是内置弹窗的定位问题。Element Plus 的弹窗默认渲染在 body 下挂载位置在缩放容器之外所以不会被 transform 作用到直接导致弹窗尺寸和定位与缩放后的页面不匹配。解决办法是指定teleported属性把弹窗渲染到缩放容器内部或者手动计算弹窗位置。3. 大文件上传Worker 计算哈希与并发控制3.1 为什么必须分片上传刚开始做文件上传的时候我直接用了 FormData 一次性提交几百 KB、几 MB 的小文件完全没问题。但当我尝试上传一个 500MB 的压缩包时问题就来了请求耗时太长中间只要网络闪断就得整个重传而且浏览器内存和服务器接收缓冲都有压力。分片上传的原理很简单把大文件切成若干个小块每块独立上传服务端收到全部块之后再合并。任何一块失败只需要重传那一块。在实际商用的上传组件里分片几乎是标配。3.2 在 Worker 里计算文件指纹分片之前需要给整个文件算一个指纹也就是 hash 值。这个指纹的作用有两个一是作为文件名的一部分避免重复上传二是用来实现秒传和断点续传后端记录下已经接收的分片编号下次从断点继续。但文件越大算 hash 越慢。不加处理直接在主线程算一个 500MB 文件可能让页面卡住好几秒用户这时候滚动页面是完全没有响应的体验非常差。这就是要用 Web Worker 的原因把计算任务丢到独立线程里主线程依然保持流畅。我的实现思路是// hashWorker.js self.importScripts(/spark-md5.min.js) self.onmessage function (e) { const file e.data.file const chunkSize 2 * 1024 * 1024 const chunks Math.ceil(file.size / chunkSize) const spark new self.SparkMD5.ArrayBuffer() let currentChunk 0 const fileReader new FileReader() fileReader.onload function (event) { spark.append(event.target.result) currentChunk self.postMessage({ type: progress, progress: Number((currentChunk / chunks).toFixed(2)) }) if (currentChunk chunks) { loadNext() } else { self.postMessage({ type: done, hash: spark.end() }) } } function loadNext() { const start currentChunk * chunkSize const end Math.min(start chunkSize, file.size) fileReader.readAsArrayBuffer(file.slice(start, end)) } loadNext() }由于spark-md5这个库依赖self上下文所以直接通过importScripts引入而不是用 ES Module 的方式导入这样在 Worker 环境里运行最稳。3.3 并发上传与失败重试机制拿到 hash 之后每个分片带着序号并发上传。并发数不是越大越好我实测下来前端并发控制在 3 到 6 个比较合适。并发太高浏览器会创建大量连接占满带宽不说还可能触发服务器连接数限制。下面是核心的上传队列逻辑async function uploadChunks(file, hash, chunkSize, concurrency 4) { const chunks Math.ceil(file.size / chunkSize) const tasks [] for (let i 0; i chunks; i) { const start i * chunkSize const end Math.min(start chunkSize, file.size) const blob file.slice(start, end) tasks.push({ index: i, blob }) } let current 0 const results [] async function worker() { while (current tasks.length) { const task tasks[current] current try { const res await uploadSingleChunk({ chunk: task.blob, hash, index: task.index }) results.push(res) updateProgress() } catch (err) { // 失败重试最多重试 3 次 const ok await retryUpload(task, hash) if (!ok) { // 通知用户该分片失败 } } } } const workers Array.from({ length: concurrency }, () worker()) await Promise.all(workers) return results }这里retryUpload内部用了指数退避策略比如第一次失败等 1 秒重试第二次等 2 秒第三次等 4 秒。直接连续重试容易撞上网络还在抖动的窗口期成功率反而不高。3.4 服务端校验与秒传逻辑跟后端约定的接口是POST /upload/check带上文件 hash 和文件名后端返回三种状态文件已存在直接秒传成功、部分分片存在返回已存在的分片列表前端跳过这些分片、完全不存在全量上传。这个接口设计非常实用不仅断点续传靠它秒传也靠它。前端收到已存在分片之后只需要上传缺失的分片最后再调POST /upload/merge让服务端合并文件。整体流程走下来用户感知到的就是拖进去上传进度条走完速度很快完全不用关心内部逻辑。4. 国际化改造从写死文案到语言包管理4.1 为什么项目要做国际化很多人觉得国际化就是“把中文换成英文”其实真正做起来要比这复杂得多。日期格式、货币符号、数字千分位、文案里带参数、用户输入内容与翻译文案的组合这些都是容易出问题的点。这个项目从一开始就内置了国际化需求所以没有走“先写死中文后面再改”的路子而是通过vue-i18n一步到位。4.2 locale 语言包的结构设计语言包如果随便写在几个大 JS 文件里项目变大之后一定会乱。我采用“按模块切分”的方式组织src locales index.ts zh-CN common.ts dashboard.ts upload.ts en-US common.ts dashboard.ts upload.tslocales/index.ts里做动态合并import { createI18n } from vue-i18n const modules import.meta.glob(./zh-CN/*.ts, { eager: true }) function loadMessages(modules: Recordstring, any) { const messages: Recordstring, any {} for (const path in modules) { const key path.replace(/\.\/\w\/(.*)\.ts$/, $1) messages[key] modules[path].default } return messages } export const i18n createI18n({ legacy: false, locale: localStorage.getItem(locale) || zh-CN, fallbackLocale: zh-CN, messages: { zh-CN: loadMessages(import.meta.glob(./zh-CN/*.ts, { eager: true })), en-US: loadMessages(import.meta.glob(./en-US/*.ts, { eager: true })) } })这种方式的优势是增加新的模块语言包时不需要手动去index.ts里注册文件丢进对应目录即可自动加载。对于个人项目来说少一步配置就少一个出错点。4.3 动态传参和复数处理文案经常需要拼动态数据比如“共 3 个文件上传失败”。直接在 JS 里拼字符串会导致英文环境下语序不通正确做法是使用 vue-i18n 的参数化翻译// zh-CN/upload.ts export default { upload.failedCount: 共 {count} 个文件上传失败 } // en-US/upload.ts export default { upload.failedCount: {count} files failed to upload }使用时传参数template p{{ $t(upload.failedCount, { count: failCount }) }}/p /template英文的复数变化比如 “1 file” 和 “2 files”vue-i18n提供了复数语法英文语言包里写{count} file | {count} files框架会根据数字自动选择对应形式。我之前就是忽略了这一点上线后英文环境下一度出现 “1 files” 这种低级错误。4.4 语言包的 key 命名规范和团队协作key 命名我试过两层结构比如upload.failedCount也试过纯扁平化的upload_failed_count。最终项目里保留了层级结构原因是前端组件的语义比较清晰层级化 key 在 IDE 里能更好地自动补全。不过层级多了也容易出现 key 名漏写、错写的问题。我现在的做法是抽了一个useLocale的复合函数里面封装了带默认值的t方法。开发时 key 缺失会立即在控制台打印警告再配合 lint 规则检查语言包 key 的一致性基本能做到“漏写早发现”。对于个人项目这可能有点过重但如果你想让代码具备真实团队协同的规范性这个习惯值得养成。5. qiankun 微前端落地实录5.1 个人项目要不要上微前端先泼一盆冷水如果你只是自己写一个管理后台完全没有必要上微前端。微前端的核心价值在组织维度多个团队、多套技术栈、独立部署这才是它的适用场景。我之所以在这个项目里引入 qiankun是为了模拟“主应用 子应用”的协作模式。主应用负责公共框架和登录态子应用独立负责一个业务域并且可以单独开发、单独部署。这样个人作品在面试和作品集里就能体现架构层面的思考而不只是“会写页面”。5.2 子应用改造三件套qiankun 要求子应用暴露生命周期钩子同时要处理好路由和公开路径。以下三个配置是必须的。第一publicPath需要动态设置否则子应用构建后的资源路径在主应用里全都 404。开发环境可以用相对路径生产环境建议根据window.__POWERED_BY_QIANKUN__判断if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ }第二子应用必须导出bootstrap、mount、unmount三个生命周期钩子。很多人只写了mount忘了unmount导致子应用切走之后定时器、事件监听还挂在内存里。我的做法是在unmount里统一清理实例和所有副作用。第三路由模式需要切换为createWebHistory(window.__POWERED_BY_QIANKUN__ ? /child-app/ : /)。这一步很容易被忽略不处理的话子应用里的路由跳转在主应用环境下会冲突。5.3 样式隔离的实际困难和妥协qiankun 自带样式隔离但strictStyleIsolation会带来一个副作用它给子应用容器加了shadow DOM而 Element Plus 的弹窗默认挂载到 body 下会直接跑到子应用容器外面去样式和事件都会出问题。我在项目里关闭了严格隔离改为主应用和子应用各自约定 CSS 前缀。Element Plus 提供 namespace 配置可以让所有组件类名从el-变成app-el-这样子应用之间样式冲突的概率大幅下降。虽然这样做的“隔离”不如 shadow DOM 彻底但胜在稳定对业务开发的干扰也最小。5.4 公共依赖怎么抽取主应用和子应用都依赖 Vue 和 Element Plus如果各打包一份首屏体积会明显膨胀。qiankun 官方推荐的是把公共依赖抽成 externals然后用全局变量方式注入。实际实施时我在主应用的index.html里通过 CDN 引入vue.global.prod.js和element-plus.full.min.js子应用 webpack 配置里做 externalsmodule.exports { externals: { vue: Vue, element-plus: ElementPlus } }这样的好处是子应用构建产物缩小非常多坏处是需要保证版本一致。我踩过的坑是主应用和子应用一个用 Vue 3.3.x一个用 Vue 3.2.x结果部分响应式 API 表现异常。个人项目偶尔一次无所谓但如果是真实多团队协作公共依赖版本必须统一管理最好建立类似共享依赖清单的机制。6. 常见问题与排查技巧实录6.1 高频问题对照表项目开发过程中我整理了一个问题表都是真实踩坑后总结出来的。先看整体再逐条详细说明问题现象根因解决方案Element Plus 表格从隐藏容器切换回来宽度错乱表格初始化时容器不可见宽度计算为 0使用doLayout或切换后重新渲染大屏缩放后点击地图区域偏移未进行坐标换算用getBoundingClientRect 缩放比计算Worker 里importScripts报错CDN 地址写错或跨域本地静态目录放置文件使用相对路径国际化切换后部分表格列头没变组件缓存导致不重新渲染使用Table组件的:key绑定 locale子应用切换后定时器不清理只写了 mount没写 unmount生命周期里统一清理副作用上传巨大文件时进程内存占用翻倍一次性读入 ArrayBuffer使用file.slice分片读取6.2 Element Plus 表格宽度计算错乱这个问题出现的频率非常高。场景是主界面有两个 tab一个 tab 里放普通列表另一个 tab 里放表格。页面加载后先切到表格所在的 tab你会发现表格的列宽有时候会挤在一起有时候会出现多余的空白。原因在于 Element Plus 表格在初始化时会根据容器宽度计算列宽如果初始化时父容器是display: none计算出来的宽度就是 0。切回显式状态后表格没有重新计算。解决办法有两个。一个是给表格显示的条件加v-if让表格在 tab 真正激活的时候才渲染缺点是不太符合“提前渲染好再切换”的性能预期。另一个是通过ref拿到表格实例在nextTick后调用doLayout方法强制重新布局。我项目里用的第二种实测有效而且改动量小。6.3 大屏缩放与弹窗的边界问题前面提到过 transform 缩放容器会让 body 下挂载的弹窗错位。更隐蔽的一个问题是缩放容器内的 fixed 定位元素在某些浏览器里会按照“包含块”而非视口来定位结果就是本来想做悬浮在窗口右下角的按钮被一起缩放和移动了。解决方案比较直接把 fixed 元素也放进缩放容器内并把整个页面结构设计成“大屏容器包含一切”。这样虽然不符合一般页面的层次逻辑但在大屏项目里反而是最稳的方式。如果你一定要让某个元素固定在浏览器窗口而不受缩放影响那就只能通过 JS 动态计算位置或者把该元素放到缩放容器之外代价是需要额外处理定位参数。6.4 Worker 哈希计算的性能瓶颈500MB 文件用FileReader分片读取再调用 SparkMD5 累加其实是一件比较耗时的事情。不同机器的耗时差异很大我实测在低性能设备上可能接近 10 秒。所以界面上的进度反馈一定要做好否则用户第一反应是“页面死了”。我在 Worker 里把处理进度通过postMessage传到主线程渲染成一个进度条。同时给主线程加了一个“可以取消本次计算”的按钮取消时调用worker.terminate()强制终止 Worker 线程。这样既避免浪费 CPU也让用户对过程有控制感。有人问过能不能用按内容采样算 hash 来提速确实能提速但那样做不到“内容变化后 hash 变化”的严格检测。上传场景里 hash 是用来做秒传判断的必须做到内容级别精确否则用户改了文件内容后服务端还可能误判为同一份文件。所以这个时间投入是值得的。6.5 微前端子应用加载静态资源 404qiankun 接入初期最容易看到的现象是子应用的 JS 能加载但字体文件、图片资源 404。这基本都是publicPath没配置好导致的。开发环境里子应用跑在自己的 dev server 上资源路径是相对路径没问题生产环境如果不做处理资源会按主应用的域名去拼自然 404。正确处理方式就是__webpack_public_path__ window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__这句配置要在所有模块代码执行之前运行。如果使用了 webpack 5也可以在output.publicPath里写上对应逻辑。还有一个容易被忽略的细节子应用的 history 路由默认 mode 下如果用户刷新主应用页面主应用路由拿去请求后端会得到一个 404 页面。主应用和子应用都要配置路由 fallback通常由生产环境的 nginx 配置try_files解决。这个配置不是前端代码层面的问题但排查起来会让前端很头疼提前记住它。最后再分享一个开发习惯写到这里我想提一个和本期技术内容无关但很关键的小习惯个人项目一定要有一份“实践笔记”或者“排错记录”。我这次整理的内容有相当一部分都是翻自己之前随手记的问题清单才找回来的。很多问题刚解决的时候你以为自己永远不会忘但实际上过两个月就会模糊。下次遇到类似问题时重新排查一遍的成本比当初踩坑要高得多。我的做法是每个模块开发完先写一页“遇到什么困难、试过什么方案、最后怎么解决”不要求文笔好看只要求关键操作步骤真实。等到系列文章写到第六篇这些笔记就变成了最好的一手素材也直接反映一个前端的成长路径。个人项目不是做给别人看的而是通过一次次复现和复盘让自己真正理解“为什么”要这么写代码。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价