前端项目优化(有了我,会发生...)

目录

一、图片处理

1.1、列表页用缩略图

1.2、批量校验尺寸

二、解决BUG

2.1、弹窗和组件层级错乱(缓存问题)

三、性能优化

[3.1、自动移除代码中所有的 console.* 调用](#3.1、自动移除代码中所有的 console.* 调用)

3.2、Vue项目Gzip+Nginx打包性能优化

3.3、防御性代码---属性安全访问

一、图片处理

1.1、列表页用缩略图

注意:为什么要用「后端缩略图」?

(1)、前端直接压缩会卡顿、耗性能

(2)、后端生成缩略图体积小 5~10 倍

(3)、列表加载速度提升非常明显

(4)、节省用户流量

**实际应用:**后端返回的列表数组中图片原始大小( 见图2的Intrinsic size)可能很大,但是前端展示时( 见图2的Rendered size)只要很小,这样页面在渲染时不至于在上百条数据展示时页面卡死。

页面渲染:

html 复制代码
<img :src="setimgUrl(item.img, { w: 300, hasWidth: true })" />

原图:https://test.com/a.jpg ===> 处理后:https://test.com/a-300.jpg

javascript 复制代码
// 在main.js封装的全局方法:用于处理后端缩略图、拼接域名 
 Vue.prototype.setimgUrl = (url, options = {}) => {
    const { w, h, hasWidth, ...others } = options
    let query = ''
    if (w) {
      query += query ? '&w=' : 'w=' + _.floor(_.multiply(w))
    }
    if (h) {
      query += query ? '&h=' : 'h=' + _.floor(_.multiply(h))
    }
    for (const key in others) {
      query += `${query ? '&' : ''}${key}=${options[key]}`
    }
    if (!url) return ''
    if (/((http|https):)?\/\/([^/]+)/i.test(url)) {
      if (
        (url.startsWith('https://test') || url.includes('custom.test.com')) &&
        !url.startsWith('https://test-psd') && hasWidth ) {
        let resultUrl = url.replace(/\.(png|jpg|jpeg)$/i, `-${w}.$1`)
        return resultUrl
      } else if (url.startsWith('https://test-psd') && hasWidth) {
//  如果是 psd 测试图片(阿里云 OSS)图片处理规则,直接拼接:?x-oss-process=image/resize,m_fixed,w_100
        return url + `?x-oss-process=image/resize,m_fixed,w_${w}`
      } else {
        return url
      }
      // 图片是MD5的形式
    } else if (url.indexOf('/') === -1) {
      return imagePath + url + (query ? '?' : '') + query
    } else {
      return filePath + url
    }
  }

这是一个Vue 全局图片处理函数:(本质)项目里所有图片 URL 的中央处理器

(1)、自动生成缩略图 URL(按后端规则拼接宽度)

(2)、自动补全图片域名 / 路径

(3)、兼容多种图片来源,统一格式输出

1.2、批量校验尺寸

扩展:****naturalWidth是HTML<img> 元素的一个只读属性,表示图像的原始固有宽度****(单位是 CSS 像素),这个宽度不受 CSS 样式(如 width、max-width 或 CSS 缩放)的影响。见图2

html 复制代码
<img id="myImg" src="example.jpg" style="width: 200px;">
<script>
  const img = document.getElementById('myImg');
  img.onload = () => {
    console.log(img.width);          // 200 (CSS 宽度)
    console.log(img.naturalWidth);   // 1000 (原始文件宽度)
  };
// 如果图片未加载完成 / 对于SVG或没有固有尺寸的图片,naturalWidth也可能会是0。
</script>

**实际应用:**前端提交图片数据给后端前,要进行批量图片尺寸校验,提前找出不符合尺寸的图片

改良前(使用 Image 对象 + onload事件):

优点 :跨域资源支持(但需 CORS),兼容性好。

缺点:异步,必须等待加载完成。

改良后(直接读取已存在的 <img> 元素):

javascript 复制代码
         if (params.imageList && params.imageList.length > 0) {
            const results = await this.mapLimit(params.imageList, 3, (item) =>
              this.checkImageSize(item.imagePath)
            )
            const sizeFail = []
            const loadFail = []
            results.forEach((r, idx) => {
              if (r.ok) return
              if (r.reason === 'load') loadFail.push(idx + 1)
              else sizeFail.push(idx + 1)
            })

            if (loadFail.length) {
              this.loading = false
              return this.$alert(
                this.$t('第') +
                  loadFail.join('、') +
                  this.$t('张图片加载失败,请稍后重试保存'),
                this.$t('错误提示')
              )
            }
            if (sizeFail.length) {
              this.loading = false
              return this.$alert(
                this.$t('第') +
                  sizeFail.join('、') +
                  this.$t(
                    '张图片不符合TIKTOK平台要求的尺寸:300 x 300 到 4000 x 4000像素'
                  ),
                this.$t('错误提示')
              )
            }
          },
    async mapLimit(list, limit, fn) {
      const ret = []
      let i = 0
      async function worker() {
        while (i < list.length) {
          const idx = i++
          ret[idx] = await fn(list[idx], idx)
        }
      }
      await Promise.all(
        Array.from({ length: Math.min(limit, list.length) }, worker)
      )
      return ret
    },
    checkImageSize(imagePath, retry = 2) {
      return new Promise((resolve) => {
        const img = new Image()
        let settled = false
        const timer = setTimeout(
          () => finish({ ok: false, reason: 'load' }),
          15000
        )

        const finish = (result) => {
          if (settled) return
          settled = true
          clearTimeout(timer)
          resolve(result)
        }

        img.onload = () => {
          const width = img.naturalWidth
          const height = img.naturalHeight
          const diff = Math.abs(width - height)
          const valid =
            width >= 300 &&
            width <= 4000 &&
            height >= 300 &&
            height <= 4000 &&
            diff <= 1
          finish({ ok: valid, reason: valid ? 'ok' : 'size', width, height })
        }

        img.onerror = () => {
          if (retry > 0) {
            clearTimeout(timer)
            // 避开可能被截断的缓存,隔一会再拉
            const sep = imagePath.includes('?') ? '&' : '?'
            const next = `${imagePath.split('?')[0]}${sep}_r=${Date.now()}`
            setTimeout(() => {
              this.checkImageSize(next, retry - 1).then(finish)
            }, 400)
            return
          }
          finish({ ok: false, reason: 'load' })
        }

        img.src = imagePath
      })
    },

优点 :不需要额外请求,适用于页面上已有的图片。

缺点:需要处理未加载完成的情况。

二、解决BUG

2.1、弹窗和组件层级错乱(缓存问题)

当前端弹窗上的某个组件样式层级错乱/被遮挡/列表看不到,直接ctrl+f5就解决了

1. 正常刷新(F5)为什么不行?

浏览器为了快,会缓存CSS、JS、图片。虽然有时候部署了新版本,但浏览器本地还是旧的CSS文件,所以页面展示错乱/被遮挡。

F5 刷新时,浏览器优先用本地缓存,不会去服务器拿新样式。

2. Ctrl+F5 为什么立刻就好?

Ctrl+F5 = 强制不使用缓存 + 重新从服务器下载所有文件

扔掉旧 CSS、拉取最新的 CSS、最新 CSS 里:z-index 正确、层级正确、不遮挡了
总结:

为什么偏偏是「弹窗 / 浮层 / 模态框」最容易出这个问题?

因为弹窗、抽屉、下拉框、提示框这些组件,极度依赖 z-index(层级)

永绝后患:

(1)、CSS 打包时加 hash

app.abc123.css → 改了代码自动变 app.def456.css

浏览器一看文件名变了,就不会用缓存。

(2)、HTML 加上禁用缓存头

不让浏览器缓存 index.html

(3)、强制清缓存部署

发布时刷新 CDN、清服务器缓存

|-------------------------------|----------------|-----------------|
| 问题 | 原因 | 解决方法 |
| 页面样式乱、布局错、弹窗被挡(样式类) | 浏览器缓存了旧CSS | 直接 Ctrl+F5 |
| 点击没反应、功能失效(JS功能类) | 缓存了旧JS文件 | Console(控制台) |
| 图片不更新/图标显示错误(静态资源类) | 图片 / 字体文件被缓存 | 直接 Ctrl+F5/后端 |
| 新页面入口不出现/导航菜单还是旧的(页面结构类) | index.html 被缓存 | 直接 Ctrl+F5/后端 |
| 接口请求还是旧地址/拿不到新字段 (接口/数据类) | 旧的 API 配置文件缓存 | Network(网络) /部署 |

三、性能优化

3.1、自动移除代码中所有的 console.* 调用

在生产环境中保留 console 语句主要会带来两个问题:

(1)、安全隐患:console.log 中打印的数据可能包含敏感的用户信息或业务数据

(2)、性能负担:数量庞大的 console 语句不仅会增加代码包的体积,运行时的打印操作本身也会消耗计算资源

以Vue2项目为例:此处使用babel-plugin-transform-remove-console插件

安装:npm install babel-plugin-transform-remove-console --save-dev

配置:在Babel配置文件(如 babel.config.js 或 .babelrc)中引入该插件:

javascript 复制代码
module.exports = {
  presets: ["@vue/cli-plugin-babel/preset"],
  env: {
    production: {
      plugins: [
        'transform-remove-console' // 配置
      ]
    }
  },
  plugins: [
    [
      "component",
      {
        libraryName: "element-ui",
        styleLibraryName: "~src/theme",
      },
    ],
    [
      "import",
      {
        libraryName: "vxe-table",
        style: true, // 样式是否也按需加载
      },
    ],
  ],
};

3.2、Vue项目Gzip+Nginx打包性能优化

(1)、减少文件体积:压缩后的文件体积通常能减小 60%-70%,提升首屏加载速度。

(2)、节约带宽成本:更小的文件体积也意味着更少的流量消耗。

(3)、使用方法:这个插件通常在构建生产环境代码时使用,配合 Nginx 等服务器开启 gzip_static on (写在nginx.conf文件里)后,服务器就能优先发送压缩好的 .gz 文件,浏览器会自动解压。注意gzip_static 指令需要放在 location 块内,不要写在全局 httpserver 块中。配置好后,执行 nginx -s reload 重启 Nginx。

以Vue2--Webpack项目为例:此处使用compression-webpack-plugin插件

安装:npm install compression-webpack-plugin --save-dev

配置:在 vue.config.js 中引入该插件:

javascript 复制代码
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  chainWebpack: config => {
    config
      .plugin('gzip-plugin')
      .use('compression-webpack-plugin', [
        {
          filename: '[path][base].gz',
          algorithm: 'gzip',
          test: /\.js$|\.html$|\.json$|\.css$|\.ttf$/,
          threshold: 102400, // 只有大小大于该值的资源会被处理100KB =100 * 1024
          deleteOriginalAssets: false // 删除原文件
        }
      ])
      .end()
  },
})

注意:配置完成后,执行打包命令(如 npm run build),你就会在输出目录(如 dist 文件夹)中,看到 .js 和 .css 文件旁边多出了同名但后缀加了 .gz 的压缩文件

以Vue3-- Vite项目为例:此处使用vite-plugin-compression插件

安装:npm install vite-plugin-compression -D

配置:在 vue.config.js 中引入该插件:

TypeScript 复制代码
import compression from 'vite-plugin-compression'
export default defineConfig({
  plugins: [
    vue(),
    compression({
      ext: '.gz', // 生成的文件扩展名
      compressionOptions: { level: 9 }, // 压缩级别,1-9,越高压缩率越大
      algorithm: 'gzip', // 压缩算法,可选 'gzip' | 'brotli'
      threshold: 51200, // 阈值:小于100kb的文件不压缩(压缩后反而可能变大)
      filter:/\.(js|css|html|svg|json)$/i , // 排除已经压缩过的文件(图片、视频、压缩包本身)
      deleteOriginFile: false, // 不要删除原始源文件,默认false,避免回退问题
    }),
  ],
  resolve: {
    alias: { '@': '/src' },
  },
})

3.3、防御性代码---属性安全访问

问题:

后端返回的 deleteRes.data 有时可能为空数组(例如没有数据、删除操作后、或者异常情况),前端未预判这种边界条件(类似情况在5年+前端也会出现)。对异步接口返回值的健壮性处理不足。

解决:

**?.**确保如果 deleteRes.data 不存在或不是数组,或者 0 为 undefined,表达式不会报错,而是返回 undefined。

**?? \[\]**在最终结果为 undefined 或 null 时,提供一个空数组作为备用值,保证 children 始终是一个可迭代的数组。

javascript 复制代码
    async loadFoldersData() {
      try {
        const [fileRes, deleteRes] = await Promise.all([
          get('/getCenterFolder'),
          get('/getFolderDelete')
        ])
        if (fileRes.code !== 200) return
        if (fileRes.data.length) {
          this.deleteFolders = [
            {
              folderName: '已删除素材',
              // children: deleteRes.data[0].children, 
// 报错原因:deleteRes.data可能是空数组,访问[0]会返回undefined,导致无法访问children属性
              children: deleteRes.data?.[0]?.children ?? [],
              id: this.deleteId
            }
          ]
        }
        this.folders = fileRes.data
        this.$nextTick(() => {
          this.$refs.tree.setCurrentKey(this.folderId || fileRes.data[0].id)
        })
      } catch (e) {
        console.error(e)
      }
    },

总结:

这属于前端开发中非常典型的"接口数据容错与属性安全访问 "问题。其核心是在异步获取的数据结构不确定时,通过**防御性编码(可选链、空值合并、默认值)**来避免运行时错误,提升应用的健壮性。

相关推荐
aa小小2 小时前
【无标题】
前端
计算机魔术师3 小时前
还在单线程跟 AI 聊天?Claude Code 并行多会话让你一个人干三个人的活
前端
志尊宝3 小时前
Vue3 零基础每日笔记(038):亲手封装 useDebounceFn 与 useThrottleFn——高频事件的流量阀
前端·javascript·vue·html·vue3
JavaGuide3 小时前
对标 MinIO!全新一代分布式文件系统正式发布
前端·后端
风骏时光牛马3 小时前
AI驱动自动化业务工作流搭建实践
前端
码云之上3 小时前
Skill 里的脚本终于能跑了,星悟接 CubeSandbox 的纪实
前端·人工智能·前端框架
IT_陈寒3 小时前
Vue computed属性这个坑,我居然踩了三次才爬出来
前端·人工智能·后端
烈风逍遥4 小时前
第三篇:组件化实践,SpTable 通用表格组件设计
前端·架构
计算机魔术师4 小时前
三周时间,128个PR,83万行代码——GitHub 是怎么让 AI 重写自己的
前端