本课是前端从入门到高级开发的核心进阶课,聚焦性能优化与高级工程化两大核心能力。性能优化以用户体验为核心,覆盖渲染、构建、网络全链路,从指标检测到落地优化,形成完整的优化方法论;高级工程化则是企业级项目开发的必备能力,通过Monorepo、CI/CD、质量门禁、监控体系,实现项目全生命周期的标准化、自动化管理。课程通过可落地的代码示例,贴合之前所学的Vue、React、跨端、桌面端技术栈,打通从开发到上线的全流程。掌握本课内容,你将具备高级前端开发的核心能力,能够独立负责企业级项目的性能优化与工程化体系搭建,完成从中级到高级前端开发的跨越。
一、课程学习目的
-
掌握前端性能核心衡量标准,理解 Web Vitals 核心指标,建立"先检测、后优化"的科学优化思维。
-
精通前端渲染、构建打包、网络请求、资源加载全链路性能优化方案,掌握可落地的优化技巧。
-
掌握 Vue/React 框架级渲染优化、跨端应用(uni-app/RN)、Electron 桌面端专项优化方法。
-
理解前端高级工程化体系,掌握 Monorepo 项目管理、CI/CD 自动化流程、代码质量门禁、灰度发布等企业级方案。
-
学会使用性能检测工具,精准定位性能瓶颈,避免无效优化与过度优化。
-
建立企业级前端项目的全生命周期管理思维,从开发、构建、部署、监控全流程保障项目质量与性能。
二、核心知识点讲解
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()
四、掌握技巧与方法
-
先检测,后优化:使用Chrome DevTools、Lighthouse、WebPageTest等工具定位性能瓶颈,不要盲目优化。
-
抓核心矛盾:优先优化影响用户体验的核心指标(LCP、FID、CLS),优先解决首屏加载、交互卡顿等用户可感知的问题。
-
渐进式优化:从低成本、高收益的优化项入手,比如图片压缩、代码分割、缓存配置,再做深度的渲染优化。
-
避免过度优化:优化需要付出维护成本,不要为了微小的性能提升过度复杂化代码,以业务需求为核心。
-
工程化前置:把规范、检查、测试、构建优化嵌入到开发流程中,从源头保障项目质量,而不是上线后再补优化。
-
线上持续监控:性能优化不是一次性工作,建立线上监控体系,持续跟踪指标变化,及时发现线上性能问题。
-
跨端专项优化:不同端的性能瓶颈不同,Web端重点优化加载与渲染,App/桌面端重点优化内存占用与启动速度。
五、课后作业
基础作业
-
使用Lighthouse对自己开发的项目进行性能检测,生成检测报告,定位3个核心性能问题。
-
实现图片懒加载功能,优化页面图片加载性能。
-
配置Vite/Webpack构建压缩,剔除生产环境console,生成包体积分析报告。
进阶作业
-
对Vue/React项目进行渲染优化,使用缓存API减少不必要的组件重渲染。
-
实现虚拟列表,优化1000条以上数据的长列表滚动性能。
-
配置ESLint+Prettier+TypeScript,搭建代码规范检查体系,实现提交代码自动校验。
实战作业
- 对之前开发的全栈项目进行全链路性能优化,包含首屏加载、渲染性能、构建打包、网络请求优化,优化后Lighthouse评分达到90分以上;同时搭建基础工程化体系,实现代码规范校验、自动化构建、性能指标上报功能。
上一课:Electron 桌面应用开发 实战作业代码
代码功能说明
本实战作业基于Electron Forge开发完整的电子单词本桌面应用,完全贴合课程核心知识点,符合Electron安全开发规范。项目采用主进程+渲染进程+预加载脚本的标准架构,主进程负责窗口管理、系统菜单、文件操作、IPC通信与本地数据持久化;渲染进程负责页面交互、单词展示、搜索过滤;预加载脚本作为通信桥梁,暴露安全API。实现单词增删改查、分类管理、批量导入导出、记忆标记、搜索过滤全功能,适配Windows与macOS双端,支持打包生成可执行安装包,完整覆盖Electron桌面应用开发全流程。
注意事项
-
必须安装Node.js 18.0及以上版本,否则无法正常运行Electron Forge项目。
-
严格遵循Electron安全规范,始终启用
contextIsolation上下文隔离,禁用nodeIntegration,禁止在渲染进程直接使用Node.js API。 -
主进程负责系统级操作,渲染进程仅负责页面渲染,职责分离,不要在主进程编写页面逻辑。
-
应用数据存储使用
app.getPath('userData')路径,禁止使用相对路径,保证跨平台兼容性。 -
打包前需配置对应平台的应用图标,Windows使用.ico格式,macOS使用.icns格式,避免打包失败。
-
macOS平台打包需安装Xcode命令行工具,Windows平台需安装Visual Studio生成工具,否则无法完成原生构建。
-
调试主进程使用VS Code断点调试,渲染进程使用Chromium开发者工具,和前端调试逻辑一致。
-
打包后的安装包仅能在对应平台运行,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
作业验收标准
-
项目可正常启动,无报错,应用窗口正常显示,功能完整可用。
-
单词增删改查、分类筛选、搜索过滤功能正常,数据可持久化存储。
-
单词导入导出功能正常,系统菜单可正常使用。
-
代码符合Electron安全规范,主进程与渲染进程职责分离,IPC通信逻辑正确。
-
可正常打包生成对应平台的安装包,安装后可正常运行。
-
代码规范,注释清晰,结构合理,完整覆盖课程核心知识点。