JavaScript基础课程三十三、性能优化与工程化高级

本课是前端从入门到高级开发的核心进阶课,聚焦性能优化与高级工程化两大核心能力。性能优化以用户体验为核心,覆盖渲染、构建、网络全链路,从指标检测到落地优化,形成完整的优化方法论;高级工程化则是企业级项目开发的必备能力,通过Monorepo、CI/CD、质量门禁、监控体系,实现项目全生命周期的标准化、自动化管理。课程通过可落地的代码示例,贴合之前所学的Vue、React、跨端、桌面端技术栈,打通从开发到上线的全流程。掌握本课内容,你将具备高级前端开发的核心能力,能够独立负责企业级项目的性能优化与工程化体系搭建,完成从中级到高级前端开发的跨越。

一、课程学习目的

  1. 掌握前端性能核心衡量标准,理解 Web Vitals 核心指标,建立"先检测、后优化"的科学优化思维。

  2. 精通前端渲染、构建打包、网络请求、资源加载全链路性能优化方案,掌握可落地的优化技巧。

  3. 掌握 Vue/React 框架级渲染优化、跨端应用(uni-app/RN)、Electron 桌面端专项优化方法。

  4. 理解前端高级工程化体系,掌握 Monorepo 项目管理、CI/CD 自动化流程、代码质量门禁、灰度发布等企业级方案。

  5. 学会使用性能检测工具,精准定位性能瓶颈,避免无效优化与过度优化。

  6. 建立企业级前端项目的全生命周期管理思维,从开发、构建、部署、监控全流程保障项目质量与性能。

二、核心知识点讲解

1. 性能优化核心衡量指标

性能优化的核心是以用户体验为核心 ,而非单纯的技术参数优化,行业通用核心指标为 Google 推出的 Web Vitals

核心指标:

  • LCP(最大内容绘制):页面最大内容元素加载完成的时间,优秀标准≤2.5s,衡量页面加载性能。

  • FID(首次输入延迟):用户首次与页面交互到浏览器响应的时间,优秀标准≤100ms,衡量页面交互性能。

  • CLS(累积布局偏移):页面生命周期内非预期的布局偏移量,优秀标准≤0.1,衡量视觉稳定性。

  • FCP(首次内容绘制):页面首次绘制文本/图片的时间,衡量白屏时长。

  • TTI(可交互时间):页面完全可交互的时间,衡量页面可用性。

2. 前端渲染优化

渲染优化的核心是减少不必要的渲染、降低重排重绘、提升交互响应速度

核心优化方案:

  • 减少重排重绘:避免频繁修改DOM样式,使用class批量修改样式;开启GPU加速(transform、opacity);脱离文档流减少重排范围。

  • 懒加载:图片、视频、组件懒加载,仅加载可视区域内容,减少首屏资源加载。

  • 虚拟列表:长列表仅渲染可视区域DOM,滚动时动态替换内容,解决万级数据列表卡顿问题。

  • 框架级优化

    • Vue:使用v-show替代频繁切换的v-if;合理使用keep-alive缓存组件;避免watch过度依赖;使用pinia替代vuex减少渲染开销。

    • React:使用useMemo缓存计算结果、useCallback缓存函数、React.memo缓存组件;合理拆分状态,避免全组件重渲染。

3. 构建打包优化

构建优化的核心是缩小包体积、提升构建速度、优化加载策略,基于Vite/Webpack实现。

核心优化方案:

  • 代码分割(拆包):按路由、第三方依赖拆分代码,实现按需加载,避免首屏加载全量包。

  • Tree-Shaking:剔除项目中未使用的死代码,减少包体积,依赖ES6模块化语法。

  • 资源压缩:JS/CSS/HTML压缩,图片压缩(webp/avif格式),字体文件精简。

  • 预构建与缓存:Vite预构建第三方依赖,开启构建缓存,大幅提升二次构建速度。

  • 第三方依赖优化:按需引入组件库;替换大体积依赖;使用CDN引入大型依赖,不打入主包。

4. 网络请求优化

网络优化的核心是减少请求数量、缩小请求体积、降低请求耗时

核心优化方案:

  • HTTP缓存:合理设置强缓存(Cache-Control)与协商缓存(ETag/Last-Modified),避免重复请求不变资源。

  • CDN加速:静态资源部署到CDN,就近访问,降低延迟。

  • 请求优化:合并重复请求;接口防抖节流;数据预加载;使用HTTP2/HTTP3,开启多路复用。

  • 接口优化:接口按需返回字段;分页加载大数据;开启Gzip/Brotli压缩,减少传输体积。

5. 专项优化方案

  • 跨端应用优化:uni-app/RN使用原生渲染组件替代webview;长列表使用原生列表组件;减少JS桥接通信频次;图片懒加载与内存优化。

  • Electron桌面端优化:渲染进程与主进程职责分离,减少主进程阻塞;预加载脚本精简逻辑;关闭无用的Chromium插件;打包时剔除无用依赖,缩小安装包体积。

  • 用户体验优化:骨架屏、加载动画、降级处理,避免用户等待时的空白与卡顿感。

6. 前端高级工程化体系

前端工程化是对项目全生命周期的标准化、自动化、体系化管理,是企业级项目开发的核心能力。

核心模块:

  • Monorepo 项目管理:在一个仓库中管理多个子项目,共享依赖、统一规范、提升协作效率,主流工具:pnpm + Turborepo。

  • CI/CD 自动化流程:代码提交后自动执行代码检查、测试、构建、部署,实现持续集成与持续交付,主流工具:GitHub Actions、GitLab CI、Jenkins。

  • 代码质量闭环:ESLint+Prettier代码规范、TypeScript类型校验、单元测试/E2E测试、代码评审门禁,禁止不规范代码合并。

  • 前端监控体系:性能监控、错误监控、用户行为监控,线上问题及时报警与定位,主流方案:Sentry、自建监控平台。

  • 灰度发布与回滚:按比例放量发布新版本,出现问题快速回滚,降低线上故障影响范围。

三、示例程序

示例1:图片懒加载实现

JavaScript 复制代码
// 图片懒加载通用函数
function lazyLoadImage() {
  // 获取所有带lazy类的图片
  const imgList = document.querySelectorAll('img.lazy');
  // 浏览器原生IntersectionObserver监听元素是否进入可视区
  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      // 进入可视区
      if (entry.isIntersecting) {
        const img = entry.target;
        // 替换真实图片地址
        img.src = img.dataset.src;
        // 加载完成后移除监听
        img.onload = () => observer.unobserve(img);
      }
    });
  }, {
    rootMargin: '100px 0px', // 提前100px开始加载
    threshold: 0.1
  });
  // 监听所有图片
  imgList.forEach(img => observer.observe(img));
}

// 页面加载后执行
window.addEventListener('DOMContentLoaded', lazyLoadImage);

示例2:Vite 构建优化配置

JavaScript 复制代码
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { visualizer } from 'rollup-plugin-visualizer' // 包体积分析工具

export default defineConfig({
  plugins: [
    vue(),
    // 打包后生成包体积分析报告
    visualizer({ open: true, gzipSize: true, brotliSize: true })
  ],
  // 构建配置
  build: {
    // 目标浏览器版本
    target: 'es2015',
    // 拆包配置
    rollupOptions: {
      output: {
        // 手动拆分第三方依赖包
        manualChunks: {
          vue: ['vue', 'vue-router', 'pinia'],
          utils: ['lodash', 'axios']
        },
        // 代码分割文件名
        chunkFileNames: 'js/[name]-[hash].js',
        entryFileNames: 'js/[name]-[hash].js',
        assetFileNames: '[ext]/[name]-[hash].[ext]'
      }
    },
    // 开启gzip压缩
    minify: 'terser',
    terserOptions: {
      // 生产环境剔除console
      compress: { drop_console: true, drop_debugger: true }
    },
    // 触发警告的chunk大小
    chunkSizeWarningLimit: 500
  },
  // 预构建依赖
  optimizeDeps: {
    include: ['vue', 'vue-router', 'pinia', 'axios']
  }
})

示例3:React 渲染优化

JavaScript 复制代码
import { useState, useMemo, useCallback, memo } from 'react'

// 子组件用memo缓存,仅props变化时才重渲染
const ChildComponent = memo(({ onClick, list }) => {
  console.log('子组件渲染')
  return (
    <div>
      {list.map(item => <div key={item.id}>{item.name}</div>)}
      <button onClick={onClick}>点击</button>
    </div>
  )
})

function App() {
  const [count, setCount] = useState(0)
  const [list] = useState([{ id: 1, name: '测试' }])

  // useCallback缓存函数,避免每次渲染生成新函数导致子组件重渲染
  const handleClick = useCallback(() => {
    console.log('按钮点击')
  }, [])

  // useMemo缓存计算结果,仅依赖变化时才重新计算
  const totalCount = useMemo(() => {
    return list.reduce((total, item) => total + item.id, 0)
  }, [list])

  return (
    <div>
      <p>计数:{count}</p>
      <p>总数:{totalCount}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
      {/* 缓存后的子组件,不会因为父组件count变化而重渲染 */}
      <ChildComponent onClick={handleClick} list={list} />
    </div>
  )
}

export default App

示例4:前端性能埋点上报

JavaScript 复制代码
// 性能指标上报工具
class PerformanceMonitor {
  constructor() {
    this.init()
  }

  init() {
    // 页面加载完成后采集指标
    window.addEventListener('load', () => {
      this.getCoreVitals()
      this.getNavigationTiming()
    })
    // 监听页面隐藏时上报数据
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'hidden') {
        this.reportData()
      }
    })
  }

  // 获取Web Vitals核心指标
  getCoreVitals() {
    // LCP 最大内容绘制
    new PerformanceObserver((entryList) => {
      const entries = entryList.getEntries()
      const lastEntry = entries[entries.length - 1]
      this.data.LCP = lastEntry.startTime
    }).observe({ entryTypes: ['largest-contentful-paint'] })

    // CLS 累积布局偏移
    new PerformanceObserver((entryList) => {
      let cls = 0
      entryList.getEntries().forEach(entry => {
        if (!entry.hadRecentInput) {
          cls += entry.value
        }
      })
      this.data.CLS = cls
    }).observe({ entryTypes: ['layout-shift'] })
  }

  // 上报数据
  reportData() {
    navigator.sendBeacon('/api/performance/report', JSON.stringify(this.data))
  }
}

// 初始化监控
new PerformanceMonitor()

四、掌握技巧与方法

  1. 先检测,后优化:使用Chrome DevTools、Lighthouse、WebPageTest等工具定位性能瓶颈,不要盲目优化。

  2. 抓核心矛盾:优先优化影响用户体验的核心指标(LCP、FID、CLS),优先解决首屏加载、交互卡顿等用户可感知的问题。

  3. 渐进式优化:从低成本、高收益的优化项入手,比如图片压缩、代码分割、缓存配置,再做深度的渲染优化。

  4. 避免过度优化:优化需要付出维护成本,不要为了微小的性能提升过度复杂化代码,以业务需求为核心。

  5. 工程化前置:把规范、检查、测试、构建优化嵌入到开发流程中,从源头保障项目质量,而不是上线后再补优化。

  6. 线上持续监控:性能优化不是一次性工作,建立线上监控体系,持续跟踪指标变化,及时发现线上性能问题。

  7. 跨端专项优化:不同端的性能瓶颈不同,Web端重点优化加载与渲染,App/桌面端重点优化内存占用与启动速度。

五、课后作业

基础作业

  1. 使用Lighthouse对自己开发的项目进行性能检测,生成检测报告,定位3个核心性能问题。

  2. 实现图片懒加载功能,优化页面图片加载性能。

  3. 配置Vite/Webpack构建压缩,剔除生产环境console,生成包体积分析报告。

进阶作业

  1. 对Vue/React项目进行渲染优化,使用缓存API减少不必要的组件重渲染。

  2. 实现虚拟列表,优化1000条以上数据的长列表滚动性能。

  3. 配置ESLint+Prettier+TypeScript,搭建代码规范检查体系,实现提交代码自动校验。

实战作业

  1. 对之前开发的全栈项目进行全链路性能优化,包含首屏加载、渲染性能、构建打包、网络请求优化,优化后Lighthouse评分达到90分以上;同时搭建基础工程化体系,实现代码规范校验、自动化构建、性能指标上报功能。

上一课:Electron 桌面应用开发 实战作业代码

代码功能说明

本实战作业基于Electron Forge开发完整的电子单词本桌面应用,完全贴合课程核心知识点,符合Electron安全开发规范。项目采用主进程+渲染进程+预加载脚本的标准架构,主进程负责窗口管理、系统菜单、文件操作、IPC通信与本地数据持久化;渲染进程负责页面交互、单词展示、搜索过滤;预加载脚本作为通信桥梁,暴露安全API。实现单词增删改查、分类管理、批量导入导出、记忆标记、搜索过滤全功能,适配Windows与macOS双端,支持打包生成可执行安装包,完整覆盖Electron桌面应用开发全流程。

注意事项

  1. 必须安装Node.js 18.0及以上版本,否则无法正常运行Electron Forge项目。

  2. 严格遵循Electron安全规范,始终启用contextIsolation上下文隔离,禁用nodeIntegration,禁止在渲染进程直接使用Node.js API。

  3. 主进程负责系统级操作,渲染进程仅负责页面渲染,职责分离,不要在主进程编写页面逻辑。

  4. 应用数据存储使用app.getPath('userData')路径,禁止使用相对路径,保证跨平台兼容性。

  5. 打包前需配置对应平台的应用图标,Windows使用.ico格式,macOS使用.icns格式,避免打包失败。

  6. macOS平台打包需安装Xcode命令行工具,Windows平台需安装Visual Studio生成工具,否则无法完成原生构建。

  7. 调试主进程使用VS Code断点调试,渲染进程使用Chromium开发者工具,和前端调试逻辑一致。

  8. 打包后的安装包仅能在对应平台运行,Windows打包生成.exe,macOS打包生成.dmg,不可跨平台运行。

完整实战代码

项目结构

Plain 复制代码
electron-word-book/
├── src/
│   ├── main.js          # 主进程入口
│   ├── preload.js       # 预加载脚本
│   ├── index.html       # 渲染进程页面
│   ├── renderer.js      # 渲染进程逻辑
│   ├── index.css        # 页面样式
│   └── assets/          # 静态资源(图标等)
├── package.json         # 项目依赖与配置
├── forge.config.js      # Electron Forge打包配置
└── .gitignore           # Git忽略文件

package.json 依赖与配置

JSON 复制代码
{
  "name": "electron-word-book",
  "productName": "电子单词本",
  "version": "1.0.0",
  "description": "基于Electron开发的桌面单词本应用",
  "main": "src/main.js",
  "scripts": {
    "start": "electron-forge start",
    "package": "electron-forge package",
    "make": "electron-forge make",
    "lint": "eslint src"
  },
  "devDependencies": {
    "@electron-forge/cli": "^7.4.0",
    "@electron-forge/maker-deb": "^7.4.0",
    "@electron-forge/maker-rpm": "^7.4.0",
    "@electron-forge/maker-squirrel": "^7.4.0",
    "@electron-forge/maker-zip": "^7.4.0",
    "electron": "^29.0.0",
    "eslint": "^8.57.0"
  },
  "dependencies": {
    "electron-squirrel-startup": "^1.0.0"
  }
}

forge.config.js 打包配置

JavaScript 复制代码
const { FusesPlugin } = require('@electron-forge/plugin-fuses');
const { FuseV1Options, FuseVersion } = require('@electron/fuses');

module.exports = {
  packagerConfig: {
    asar: true,
    name: '电子单词本',
    icon: 'src/assets/icon',
    appCopyright: 'Copyright © 2024 电子单词本'
  },
  rebuildConfig: {},
  makers: [
    // Windows安装包
    {
      name: '@electron-forge/maker-squirrel',
      config: {
        name: 'electron_word_book',
        authors: '开发者',
        description: '电子单词本桌面应用'
      },
    },
    // macOS安装包
    {
      name: '@electron-forge/maker-zip',
      platforms: ['darwin'],
    },
    // Linux安装包
    {
      name: '@electron-forge/maker-deb',
      config: {},
    },
    {
      name: '@electron-forge/maker-rpm',
      config: {},
    },
  ],
  plugins: [
    {
      name: '@electron-forge/plugin-auto-unpack-natives',
      config: {},
    },
    new FusesPlugin({
      version: FuseVersion.V1,
      [FuseV1Options.RunAsNode]: false,
      [FuseV1Options.EnableCookieEncryption]: true,
      [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
      [FuseV1Options.EnableNodeCliInspectArguments]: false,
      [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
      [FuseV1Options.OnlyLoadAppFromAsar]: true,
    }),
  ],
};

src/main.js 主进程代码

JavaScript 复制代码
const { app, BrowserWindow, ipcMain, Menu, dialog } = require('electron');
const path = require('path');
const fs = require('fs');

// 处理Windows安装启动事件
if (require('electron-squirrel-startup')) app.quit();

// 全局窗口实例
let mainWindow;
// 数据存储路径
let dataPath;

// 创建主窗口
function createWindow() {
  // 初始化数据存储路径
  dataPath = path.join(app.getPath('userData'), 'word-data.json');

  mainWindow = new BrowserWindow({
    width: 1000,
    height: 700,
    minWidth: 800,
    minHeight: 500,
    title: '电子单词本',
    icon: path.join(__dirname, 'assets/icon.png'),
    webPreferences: {
      // 安全配置(必须开启)
      contextIsolation: true,
      nodeIntegration: false,
      // 预加载脚本路径
      preload: path.join(__dirname, 'preload.js')
    }
  });

  // 加载渲染进程页面
  mainWindow.loadFile(path.join(__dirname, 'index.html'));

  // 开发环境打开开发者工具
  if (!app.isPackaged) {
    mainWindow.webContents.openDevTools();
  }

  // 创建应用菜单
  createAppMenu();
}

// 创建应用系统菜单
function createAppMenu() {
  const template = [
    {
      label: '文件',
      submenu: [
        {
          label: '导入单词',
          accelerator: 'CmdOrCtrl+I',
          click: importWordFile
        },
        {
          label: '导出单词',
          accelerator: 'CmdOrCtrl+E',
          click: exportWordFile
        },
        { type: 'separator' },
        {
          label: '退出',
          role: 'quit',
          accelerator: 'CmdOrCtrl+Q'
        }
      ]
    },
    {
      label: '编辑',
      submenu: [
        { role: 'undo', label: '撤销', accelerator: 'CmdOrCtrl+Z' },
        { role: 'redo', label: '重做', accelerator: 'CmdOrCtrl+Shift+Z' },
        { type: 'separator' },
        { role: 'cut', label: '剪切', accelerator: 'CmdOrCtrl+X' },
        { role: 'copy', label: '复制', accelerator: 'CmdOrCtrl+C' },
        { role: 'paste', label: '粘贴', accelerator: 'CmdOrCtrl+V' },
        { role: 'selectAll', label: '全选', accelerator: 'CmdOrCtrl+A' }
      ]
    },
    {
      label: '视图',
      submenu: [
        { role: 'reload', label: '重新加载' },
        { role: 'forceReload', label: '强制重新加载' },
        { role: 'toggleDevTools', label: '切换开发者工具' },
        { type: 'separator' },
        { role: 'resetZoom', label: '重置缩放' },
        { role: 'zoomIn', label: '放大' },
        { role: 'zoomOut', label: '缩小' },
        { type: 'separator' },
        { role: 'togglefullscreen', label: '切换全屏' }
      ]
    }
  ];

  // macOS适配
  if (process.platform === 'darwin') {
    template.unshift({
      label: app.name,
      submenu: [
        { role: 'about', label: '关于电子单词本' },
        { type: 'separator' },
        { role: 'services', label: '服务' },
        { type: 'separator' },
        { role: 'hide', label: '隐藏' },
        { role: 'hideOthers', label: '隐藏其他' },
        { role: 'unhide', label: '显示全部' },
        { type: 'separator' },
        { role: 'quit', label: '退出' }
      ]
    });
  }

  const menu = Menu.buildFromTemplate(template);
  Menu.setApplicationMenu(menu);
}

// 导入单词文件
async function importWordFile() {
  const { filePaths } = await dialog.showOpenDialog(mainWindow, {
    title: '导入单词文件',
    filters: [{ name: 'JSON文件', extensions: ['json'] }],
    properties: ['openFile']
  });

  if (filePaths.length > 0) {
    try {
      const content = fs.readFileSync(filePaths[0], 'utf8');
      const wordData = JSON.parse(content);
      // 发送到渲染进程
      mainWindow.webContents.send('word:import', wordData);
      dialog.showMessageBox(mainWindow, {
        type: 'info',
        title: '导入成功',
        message: `成功导入${wordData.length}个单词`
      });
    } catch (err) {
      dialog.showErrorBox('导入失败', '文件格式错误,请检查JSON文件');
    }
  }
}

// 导出单词文件
async function exportWordFile() {
  const { filePath } = await dialog.showSaveDialog(mainWindow, {
    title: '导出单词文件',
    defaultPath: '电子单词本.json',
    filters: [{ name: 'JSON文件', extensions: ['json'] }]
  });

  if (filePath) {
    // 监听渲染进程返回的单词数据
    ipcMain.handleOnce('word:export-data', async () => {
      return filePath;
    });
  }
}

// 应用就绪创建窗口
app.whenReady().then(() => {
  createWindow();

  // macOS激活应用时重建窗口
  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
});

// 所有窗口关闭时退出应用(macOS除外)
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit();
});

// ------------------------------
// IPC通信处理(渲染进程 ↔ 主进程)
// ------------------------------

// 保存单词数据
ipcMain.handle('word:save', async (event, wordList) => {
  try {
    fs.writeFileSync(dataPath, JSON.stringify(wordList, null, 2));
    return { success: true };
  } catch (err) {
    return { success: false, error: err.message };
  }
});

// 加载单词数据
ipcMain.handle('word:load', async () => {
  try {
    if (fs.existsSync(dataPath)) {
      const content = fs.readFileSync(dataPath, 'utf8');
      return JSON.parse(content);
    }
    return [];
  } catch (err) {
    return [];
  }
});

// 写入导出文件
ipcMain.handle('word:write-export', async (event, filePath, wordList) => {
  try {
    fs.writeFileSync(filePath, JSON.stringify(wordList, null, 2));
    return { success: true };
  } catch (err) {
    return { success: false, error: err.message };
  }
});

src/preload.js 预加载脚本

JavaScript 复制代码
const { contextBridge, ipcRenderer } = require('electron');

// 向渲染进程暴露安全API,禁止直接暴露ipcRenderer
contextBridge.exposeInMainWorld('wordBookAPI', {
  // 保存单词数据
  saveWord: (wordList) => ipcRenderer.invoke('word:save', wordList),
  // 加载单词数据
  loadWord: () => ipcRenderer.invoke('word:load'),
  // 监听导入单词事件
  onWordImport: (callback) => ipcRenderer.on('word:import', (event, data) => callback(data)),
  // 获取导出路径
  onWordExport: (callback) => ipcRenderer.handle('word:export-data', callback),
  // 写入导出文件
  writeExportFile: (filePath, wordList) => ipcRenderer.invoke('word:write-export', filePath, wordList)
});

src/index.html 渲染进程页面

HTML 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>电子单词本</title>
  <link rel="stylesheet" href="index.css">
</head>
<body>
  <div class="app">
    <!-- 侧边栏分类 -->
    <aside class="sidebar">
      <h2>电子单词本</h2>
      <div class="category-list">
        <div class="category-item active" data-category="all">全部单词</div>
        <div class="category-item" data-category="known">已掌握</div>
        <div class="category-item" data-category="unknown">未掌握</div>
      </div>
    </aside>

    <!-- 主内容区 -->
    <main class="main-content">
      <!-- 搜索与添加区 -->
      <div class="header-bar">
        <input type="text" id="searchInput" placeholder="搜索单词..." class="search-input">
        <button id="addBtn" class="add-btn">添加单词</button>
      </div>

      <!-- 单词列表 -->
      <div class="word-list" id="wordList"></div>

      <!-- 空状态 -->
      <div class="empty-state" id="emptyState">
        <h3>暂无单词</h3>
        <p>点击添加按钮,开始你的单词学习之旅</p>
      </div>
    </main>
  </div>

  <!-- 添加单词弹窗 -->
  <div class="modal" id="addModal">
    <div class="modal-content">
      <h3>添加单词</h3>
      <div class="form-group">
        <label>英文单词</label>
        <input type="text" id="modalEnInput" placeholder="请输入英文单词">
      </div>
      <div class="form-group">
        <label>中文释义</label>
        <input type="text" id="modalCnInput" placeholder="请输入中文释义">
      </div>
      <div class="modal-footer">
        <button class="cancel-btn" id="cancelBtn">取消</button>
        <button class="confirm-btn" id="confirmAddBtn">确认添加</button>
      </div>
    </div>
  </div>

  <script src="renderer.js"></script>
</body>
</html>

src/renderer.js 渲染进程逻辑

JavaScript 复制代码
// 从预加载脚本获取API
const { wordBookAPI } = window;

// DOM元素
const searchInput = document.getElementById('searchInput');
const addBtn = document.getElementById('addBtn');
const wordList = document.getElementById('wordList');
const emptyState = document.getElementById('emptyState');
const addModal = document.getElementById('addModal');
const cancelBtn = document.getElementById('cancelBtn');
const confirmAddBtn = document.getElementById('confirmAddBtn');
const modalEnInput = document.getElementById('modalEnInput');
const modalCnInput = document.getElementById('modalCnInput');
const categoryItems = document.querySelectorAll('.category-item');

// 全局数据
let wordData = [];
let currentCategory = 'all';
let searchKey = '';

// 页面加载初始化
window.addEventListener('DOMContentLoaded', async () => {
  // 加载本地单词数据
  wordData = await wordBookAPI.loadWord();
  renderWordList();
  // 监听菜单导入事件
  wordBookAPI.onWordImport((data) => {
    wordData = data;
    renderWordList();
    wordBookAPI.saveWord(wordData);
  });
  // 监听菜单导出事件
  wordBookAPI.onWordExport(async () => {
    const filePath = await wordBookAPI.writeExportFile(filePath, wordData);
    if (filePath.success) {
      alert('导出成功');
    }
  });
});

// 渲染单词列表
function renderWordList() {
  // 过滤数据
  let filterData = wordData;
  // 分类过滤
  if (currentCategory === 'known') {
    filterData = filterData.filter(item => item.isKnown);
  } else if (currentCategory === 'unknown') {
    filterData = filterData.filter(item => !item.isKnown);
  }
  // 搜索过滤
  if (searchKey) {
    const key = searchKey.toLowerCase();
    filterData = filterData.filter(item => 
      item.en.toLowerCase().includes(key) || item.cn.includes(key)
    );
  }

  // 渲染
  wordList.innerHTML = '';
  if (filterData.length === 0) {
    emptyState.style.display = 'block';
    wordList.style.display = 'none';
    return;
  }

  emptyState.style.display = 'none';
  wordList.style.display = 'block';

  filterData.forEach((item, index) => {
    const div = document.createElement('div');
    div.className = `word-item ${item.isKnown ? 'known' : ''}`;
    div.innerHTML = `
      <div class="word-info">
        <div class="word-en">${item.en}</div>
        <div class="word-cn">${item.cn}</div>
      </div>
      <div class="word-actions">
        <button class="toggle-btn" data-index="${index}">${item.isKnown ? '取消掌握' : '已掌握'}</button>
        <button class="del-btn" data-index="${index}">删除</button>
      </div>
    `;
    wordList.appendChild(div);
  });

  // 绑定事件
  bindItemEvents();
}

// 绑定列表项事件
function bindItemEvents() {
  // 掌握状态切换
  document.querySelectorAll('.toggle-btn').forEach(btn => {
    btn.addEventListener('click', (e) => {
      const index = e.target.dataset.index;
      wordData[index].isKnown = !wordData[index].isKnown;
      renderWordList();
      wordBookAPI.saveWord(wordData);
    });
  });
  // 删除单词
  document.querySelectorAll('.del-btn').forEach(btn => {
    btn.addEventListener('click', (e) => {
      const index = e.target.dataset.index;
      wordData.splice(index, 1);
      renderWordList();
      wordBookAPI.saveWord(wordData);
    });
  });
}

// 搜索事件
searchInput.addEventListener('input', (e) => {
  searchKey = e.target.value;
  renderWordList();
});

// 分类切换
categoryItems.forEach(item => {
  item.addEventListener('click', () => {
    categoryItems.forEach(i => i.classList.remove('active'));
    item.classList.add('active');
    currentCategory = item.dataset.category;
    renderWordList();
  });
});

// 弹窗控制
addBtn.addEventListener('click', () => {
  addModal.style.display = 'flex';
  modalEnInput.focus();
});

cancelBtn.addEventListener('click', () => {
  addModal.style.display = 'none';
  modalEnInput.value = '';
  modalCnInput.value = '';
});

// 添加单词
confirmAddBtn.addEventListener('click', () => {
  const en = modalEnInput.value.trim();
  const cn = modalCnInput.value.trim();
  if (!en || !cn) return;

  wordData.unshift({
    en,
    cn,
    isKnown: false,
    createTime: new Date().toISOString()
  });

  renderWordList();
  wordBookAPI.saveWord(wordData);

  // 关闭弹窗
  addModal.style.display = 'none';
  modalEnInput.value = '';
  modalCnInput.value = '';
});

// 点击弹窗外部关闭
window.addEventListener('click', (e) => {
  if (e.target === addModal) {
    addModal.style.display = 'none';
    modalEnInput.value = '';
    modalCnInput.value = '';
  }
});

src/index.css 样式文件

CSS 复制代码
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background-color: #f5f7fa;
  color: #333;
  height: 100vh;
  overflow: hidden;
}

.app {
  display: flex;
  height: 100vh;
}

/* 侧边栏 */
.sidebar {
  width: 240px;
  background-color: #2c3e50;
  color: #fff;
  padding: 20px 0;
  display: flex;
  flex-direction: column;
}

.sidebar h2 {
  font-size: 20px;
  text-align: center;
  margin-bottom: 30px;
  padding: 0 20px;
}

.category-item {
  padding: 12px 20px;
  cursor: pointer;
  transition: background-color 0.2s;
}

.category-item:hover {
  background-color: #34495e;
}

.category-item.active {
  background-color: #3498db;
}

/* 主内容区 */
.main-content {
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.header-bar {
  padding: 20px;
  background-color: #fff;
  border-bottom: 1px solid #eee;
  display: flex;
  gap: 15px;
  align-items: center;
}

.search-input {
  flex: 1;
  padding: 10px 15px;
  border: 1px solid #ddd;
  border-radius: 6px;
  font-size: 14px;
  outline: none;
}

.search-input:focus {
  border-color: #3498db;
}

.add-btn {
  padding: 10px 20px;
  background-color: #27ae60;
  color: #fff;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-size: 14px;
}

.add-btn:hover {
  background-color: #2ecc71;
}

/* 单词列表 */
.word-list {
  flex: 1;
  padding: 20px;
  overflow-y: auto;
}

.word-item {
  background-color: #fff;
  padding: 20px;
  border-radius: 8px;
  margin-bottom: 12px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}

.word-item.known {
  opacity: 0.7;
}

.word-en {
  font-size: 18px;
  font-weight: 500;
  margin-bottom: 4px;
}

.word-cn {
  font-size: 14px;
  color: #666;
}

.word-actions {
  display: flex;
  gap: 10px;
}

.toggle-btn {
  padding: 6px 12px;
  background-color: #3498db;
  color: #fff;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
}

.del-btn {
  padding: 6px 12px;
  background-color: #e74c3c;
  color: #fff;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
}

/* 空状态 */
.empty-state {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  color: #999;
}

.empty-state h3 {
  margin-bottom: 10px;
  font-size: 18px;
}

/* 弹窗 */
.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0,0,0,0.5);
  display: none;
  justify-content: center;
  align-items: center;
  z-index: 1000;
}

.modal-content {
  background-color: #fff;
  padding: 30px;
  border-radius: 8px;
  width: 400px;
  max-width: 90%;
}

.modal-content h3 {
  margin-bottom: 20px;
  text-align: center;
}

.form-group {
  margin-bottom: 20px;
}

.form-group label {
  display: block;
  margin-bottom: 8px;
  font-size: 14px;
}

.form-group input {
  width: 100%;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 6px;
  font-size: 14px;
  outline: none;
}

.modal-footer {
  display: flex;
  gap: 15px;
  justify-content: flex-end;
  margin-top: 30px;
}

.cancel-btn {
  padding: 8px 20px;
  border: 1px solid #ddd;
  background-color: #fff;
  border-radius: 6px;
  cursor: pointer;
}

.confirm-btn {
  padding: 8px 20px;
  background-color: #27ae60;
  color: #fff;
  border: none;
  border-radius: 6px;
  cursor: pointer;
}

运行与打包命令

Bash 复制代码
# 1. 安装依赖
npm install

# 2. 启动开发环境(热更新)
npm run start

# 3. 打包应用(生成可执行文件)
npm run package

# 4. 生成安装包
npm run make

作业验收标准

  1. 项目可正常启动,无报错,应用窗口正常显示,功能完整可用。

  2. 单词增删改查、分类筛选、搜索过滤功能正常,数据可持久化存储。

  3. 单词导入导出功能正常,系统菜单可正常使用。

  4. 代码符合Electron安全规范,主进程与渲染进程职责分离,IPC通信逻辑正确。

  5. 可正常打包生成对应平台的安装包,安装后可正常运行。

  6. 代码规范,注释清晰,结构合理,完整覆盖课程核心知识点。

相关推荐
小笔学长3 个月前
缓存机制:减少重复计算
前端性能优化·javascript缓存机制·缓存应用场景·缓存管理策略
小笔学长3 个月前
Web Worker:利用多线程提升性能 ### 调试与错误处理
web worker·前端性能优化·web开发实战·浏览器多线程
RedEric7 个月前
Vue加载速度优化,verder.js和element.js加载速度慢解决方法
前端·javascript·vue.js·前端性能优化
周盛欢2 年前
微信小程序-分包
微信小程序·小程序·代码优化·性能提升·前端性能优化·构建打包·分包加载
程序员小白条2 年前
面试浏览器框架八股文十问十答第四期
前端·面试·职场和发展·性能优化·项目实战·八股文·前端性能优化
程序员小白条2 年前
面试计算机网络框架八股文十问十答第七期
计算机网络·面试·职场和发展·性能优化·八股文·前端性能优化·计网
程序员小白条2 年前
面试计算机网络框架八股文十问十答第六期
计算机网络·面试·职场和发展·性能优化·八股文·前端性能优化·计网
程序员小白条2 年前
面试 Vue 框架八股文十问十答第四期
前端·javascript·vue.js·面试·职场和发展·八股文·前端性能优化