实际项目开发
umi相关⭐
umi中的基本配置项
1. 基本配置
publicPath
:指定打包后的资源路径。这对于部署在子目录的应用非常重要。publicPath: '/my-app/',
outputPath
:指定输出目录,默认为dist
。outputPath: 'build',
base
:路由前缀,默认为/
要在非根目录下部署 umi 项目时,你可以使用 base 配置。
base 配置允许你为应用程序设置路由前缀。比如有路由
/
和/users
,设置 base 为/foo/
后就可通过/foo/
和/foo/users
访问到之前的路由。
2. 代码分割
chainWebpack
:允许使用 Webpack Chain API 来进一步配置 Webpack。chainWebpack(memo) { memo.module .rule('images') .use('url-loader') .loader('url-loader') .options({ limit: 10240, // 10kb name: 'static/[name].[hash:8].[ext]', }); },
dynamicImport
:控制动态导入的行为,可以开启或关闭代码分割。dynamicImport: { loading: '@/components/PageLoading/index', webpackChunkName: true, },
3. 压缩
optimizeCss
:启用 CSS 压缩。optimizeCss: { enabled: true, },
compress
:启用 JavaScript 压缩。compress: true,
4. 资源加载
proxy
:配置代理服务器,这对于开发阶段的 API 调用很有用。proxy: { '/api': { target: 'http://localhost:3000', }, },
extraBabelPlugins
:添加额外的 Babel 插件。extraBabelPlugins: [ [ 'import', { libraryName: 'antd', style: 'css' }, ], ],
5. 分析工具
analysis
:开启 Webpack Bundle Analyzer 来分析打包后的文件大小。analysis: true,
disableCSSnano
:禁用 CSS 压缩工具 CSSnano。disableCSSnano: true,
hash
:为输出文件添加哈希值。hash: true,
define
:定义全局变量。define: { 'process.env': { NODE_ENV: '"production"', }, },
umi项目性能优化
去除console
具体操作:https://juejin.cn/post/7103831294013865992
1.安装babel-plugin-transform-remove-console
插件
2.在config.ts或.umirc.ts中进行配置
//在生产环境中取消console
extraBabelPlugins: [isProd ? 'transform-remove-console' : '']
//或者
//在所有环境中取消console
extraBabelPlugins: [transform-remove-console]
开启gzip压缩
具体操作:https://juejin.cn/post/7103831294013865992
gzip压缩是前端性能优化的一种手段,减少build之后包的体积,加快首屏渲染速度
1.安装 compression-webpack-plugin
插件
2.在config.ts或.umirc.ts中进行配置
chainWebpack: function (config, { webpack }) {
config.merge({
optimization: {
splitChunks: {
chunks: 'all',
minSize: 1000,
minChunks: 2,
automaticNameDelimiter: '.',
cacheGroups: {
vendor: {
name: 'vendors',
test({ resource }) {
return /[\\/]node_modules[\\/]/.test(resource)
},
priority: 10,
},
},
},
},
})
//在生产环境开启gzip压缩
if (isProd) {
// Gzip压缩
config.plugin('compression-webpack-plugin').use(CompressionPlugin, [
{
test: /\.(js|css|html)$/i, // 匹配
threshold: 10240, // 超过10k的文件压缩
deleteOriginalAssets: false, // 不删除源文件
},
])
}
}
3.还需要后端在nginx中配置 gzip_static on
//nginx 配置
gzip_static on //检测是否存在gzip文件,有,则返回给客户端
umi中useRequest的底层是什么
Umi.js 中的 useRequest
是一个基于 React Hooks 的功能,用于简化发起网络请求的操作,并且自动处理请求的状态(如 loading、error)。useRequest
的底层实现主要依赖于几个关键部分:
React Hooks:
useEffect
:用于监听请求的触发条件,并在条件变化时执行请求。useState
:用于管理请求的状态(如 loading 状态、错误信息等)。
请求处理:
- Umi 的
request
方法:如前所述,这是基于fetch
API 的封装,负责实际的网络请求。
- Umi 的
状态管理:
- 通过
useState
创建的状态来跟踪请求的进度(如 loading、error、data)。
- 通过
具体来说,useRequest
的工作流程大致如下:
- 初始化状态:使用
useState
初始化请求的状态,如loading
、error
和data
。 - 注册请求函数:通过
useCallback
创建一个请求函数,这个函数会在每次调用时返回一个新的函数实例,保证每次请求都是独立的。 - 监听依赖项变化:使用
useEffect
监听依赖项的变化,当依赖项改变时触发请求。 - 处理请求结果:
- 请求成功时更新数据状态。
- 请求失败时处理错误,并更新错误状态。
- 清理副作用:在组件卸载或不再需要请求时,取消请求,防止内存泄漏。
示例代码
假设有一个简单的 useRequest
实现(简化版):
import { useState, useEffect, useCallback } from 'react';
import { request } from '@umijs/max';
function useRequest(url, options = {}) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [data, setData] = useState(null);
const fetchData = useCallback(async () => {
try {
setLoading(true);
const response = await request(url, options);
setData(response);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
}, [url, options]);
useEffect(() => {
fetchData();
}, [fetchData]); // 依赖项中包含 fetchData,确保每次依赖项改变时重新请求
return { loading, error, data, refetch: fetchData };
}
MFSU是什么
MFSU 是一种基于 webpack5 新特性 Module Federation(模块联邦) 的打包提速方案。
Module Federation 是 Webpack 5 引入的一项新特性,它允许你将应用拆分成多个可以互相共享模块的“远程”应用。这些应用可以独立部署,并且在运行时按需加载彼此的模块。
其核心的思路是通过分而治之,将应用源代码的编译和应用依赖的编译分离,将变动较小的应用依赖构建为一个 Module Federation 的 remote 应用,以免去应用热更新时对依赖的编译。
开启mfsu可以大幅减少启动项目和热更新所需的时间。在生产模式,也可以通过提前编译依赖,大幅提升部署效率。
MFSU 的底层实现
1. Webpack Module Federation
Module Federation 提供了以下关键特性:
- Remote Entry Files:每个应用会生成一个或多个远程入口文件(remote entry file),其中包含模块的元数据和加载地址。
- Shared Modules:定义哪些模块可以被共享,并且可以被其他应用使用。
- Exposure and Consumption:定义模块的暴露和消费方式。
2. @umijs/plugin-mfsu
Umi.js 提供了一个名为 @umijs/plugin-mfsu
的插件,它负责配置和管理 Module Federation 的相关设置。这个插件为 Umi.js 添加了对 Module Federation 的支持,并提供了许多便捷的功能,如自动代码分割、动态加载子应用等。
总结
MFSU 的底层实现主要依赖于 Webpack Module Federation 和 Umi.js 的 @umijs/plugin-mfsu
插件。通过这些技术,Umi.js 能够支持微前端架构,使得应用可以被拆分成多个独立部署的子应用,并且在运行时动态加载和组合这些子应用。这种架构模式不仅提高了应用的可维护性和灵活性,还可以显著改善应用的性能和加载速度。
为什么比vite还快
主要是因为它在微前端场景下进行了专门的优化,尤其是在动态加载和代码分割方面。
- 模块联邦:MFSU 利用了 Module Federation,这使得微前端架构下的子应用可以动态加载,减少了初始加载时间。
- 代码分割:MFSU 提供了更细粒度的代码分割,使得每个子应用都可以作为一个独立的模块加载,而不是一次性加载整个应用。
- 全局状态管理:MFSU 支持全局状态管理,这在微前端场景下非常有用,可以减少状态同步的复杂性。
在某些特定场景下,如:
- 大规模应用:对于非常大的应用,MFSU 的模块联邦和代码分割策略可以显著提高应用的加载速度。
- 高并发请求:在需要频繁加载不同子应用的情况下,MFSU 的按需加载策略可以减少不必要的资源加载。
相比之下,Vite 更适合快速开发和迭代,特别是在较小规模的应用中。
如果你的应用是一个大型微前端项目,MFSU 可能更适合你;如果你的应用规模较小,或者更关注开发体验,那么 Vite 可能是一个更好的选择。
umi和next的区别
- 定位与背景
- Next.js 由 Vercel 维护,专注于 React 生态的服务端渲染(SSR)和静态站点生成(SSG),适合构建高性能、SEO 友好的 Web 应用(如博客、电商、企业官网等)。
- Umi 由蚂蚁金服(Ant Group)开发,定位于企业级中后台前端解决方案,提供开箱即用的约定式路由、插件体系、数据流集成等,适合快速开发复杂的中后台系统。
- 核心功能对比
功能 | Next.js | Umi |
---|---|---|
路由 | 文件系统路由(基于 pages 目录) | 约定式路由(基于文件结构)或配置式路由 |
数据获取 | getServerSideProps /getStaticProps | 客户端获取(或结合插件扩展服务端能力) |
SSR/SSG 支持 | 原生支持 | 需通过插件(如 @umijs/plugin-ssr ) |
插件系统 | 有限(依赖社区插件) | 核心特性,高度可扩展 |
状态管理 | 需自行集成(如 Redux、Zustand) | 内置集成 Dva、Umi Hooks 等 |
构建工具 | Webpack(默认)或 Turbopack | Webpack(基于 Umi 封装) |
国际化 | 需自行配置 | 内置插件 @umijs/plugin-locale |
- 适用场景
- Next.js
- 需要 SEO 优化的内容型网站(如博客、官网)。
- 混合渲染应用(SSR + SSG + CSR)。
- 面向全球用户的国际化项目(社区生态更广泛)。
- Umi
- 企业级中后台系统(如管理后台、数据面板)。
- 需要快速搭建且维护成本低的内部项目。
- 依赖 Ant Design Pro 生态的项目。
- 生态与社区
- Next.js
- 国际化社区活跃,插件和教程丰富。
- 深度集成 Vercel 部署平台。
- Umi
- 国内生态为主,与 Ant Design Pro 深度绑定。
- 适合需要对接阿里系技术栈(如微前端 qiankun)的项目。
- 开发体验
- Next.js
- 灵活但配置成本较高(如自定义 Webpack)。
- 适合对 React 生态有经验的开发者。
- Umi
- 开箱即用,通过配置或插件简化开发流程。
- 适合追求快速上手的团队(尤其是 Ant Design 用户)。
总结选择建议
- 选 Next.js 如果:需要 SEO/SSR、面向全球用户、或构建内容密集型应用。
- 选 Umi 如果:开发中后台系统、追求开箱即用、或依赖 Ant Design 生态。
实际项目相关
权限设计方案⭐
三种前后端权限管理方案:
1、通过用户角色来过滤菜单(前端方式控制),路由在前端配置,通过API返回角色过滤
2、通过后台来动态生成路由表(后台方式控制)
3、通过后台返回所有权限集合(包括菜单和按钮),前端固定路由,进行过滤
多页签中的右键选中,是如何做到把浏览器原生右键打开的弹窗覆盖掉的?
自定义右键菜单具体实现
1、禁用浏览器弹出默认菜单的行为,通过阻止contextMenu事件的默认行为,并同时触发自定义菜单的显示
2、通过Dropdown实现菜单样式,触发器trigger
配置contextMenu
,从而实现鼠标右键触发下拉菜单
const setTab = useMemoizedFn((tab, key, index) => (
<span onContextMenu={(event) => event.preventDefault()}>
<Dropdown overlay={setMenu(key, index)} trigger={['contextMenu']}>
<span className={styles.tabTitle}>{tab}</span>
</Dropdown>
</span>
));
竞态问题(TODO)
AbortController
https://juejin.cn/post/6970710521104302110
竞态问题(Race Condition)是指在并发编程中,当多个操作顺序不确定时,可能导致意外的结果。在前端开发中,竞态问题通常出现在异步操作中,比如 AJAX 请求、事件监听器的回调、定时器等。下面详细介绍如何解决前端开发中的竞态问题。
常见的竞态问题场景
- 多次请求同一资源:例如,当用户快速点击按钮时,可能会触发多次 AJAX 请求。
- 状态更新冲突:多个异步操作可能同时更新同一个状态,导致数据不一致。
- 异步回调顺序:多个异步操作的回调函数执行顺序不确定,可能导致逻辑错误。
解决竞态问题的方法
1. 使用 Promises 和 async/await
Promises 和 async/await 可以帮助你更好地控制异步操作的执行顺序,避免竞态问题。
示例代码:
async function fetchData(url) {
try {
const response = await fetch(url);
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Error fetching data:', error);
}
}
// 使用 async/await 控制异步操作顺序
(async function() {
await fetchData('https://api.example.com/data1');
await fetchData('https://api.example.com/data2');
})();
2. 使用防抖(Debounce)和节流(Throttle)
防抖(Debounce)和节流(Throttle)技术可以减少不必要的重复操作,防止在短时间内触发多次异步请求或事件处理。
防抖(Debounce):
function debounce(func, wait) {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), wait);
};
}
const buttonClickHandler = debounce(function() {
console.log('Button clicked');
}, 300);
节流(Throttle):
function throttle(func, limit) {
let inThrottle;
return function() {
const args = arguments;
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
const resizeHandler = throttle(function() {
console.log('Window resized');
}, 200);
3. 使用锁机制(Lock)
在某些情况下,可以使用锁机制来确保同一时间内只有一个操作可以执行。这种方法适用于状态更新等场景。
示例代码:
let lock = false;
function updateState(newState) {
if (lock) return;
lock = true;
// 更新状态
console.log('State updated to:', newState);
// 模拟异步操作
setTimeout(() => {
lock = false;
}, 1000);
}
document.getElementById('button').addEventListener('click', () => {
updateState('new state');
});
4. 使用原子操作
在更新状态时,尽量使用不可分割的操作,确保状态的一致性。
示例代码:
let counter = 0;
function incrementCounter() {
// 原子操作
counter++;
console.log('Counter incremented to:', counter);
}
incrementCounter();
incrementCounter();
5. 使用状态管理库
状态管理库如 Redux、MobX 等可以帮助你更好地管理应用的状态,避免竞态问题。
Redux 示例:
import { createStore } from 'redux';
import { Provider } from 'react-redux';
const initialState = { count: 0 };
function reducer(state = initialState, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
default:
return state;
}
}
const store = createStore(reducer);
function App() {
return (
<Provider store={store}>
<button onClick={() => store.dispatch({ type: 'INCREMENT' })}>
Increment
</button>
<p>{store.getState().count}</p>
</Provider>
);
}
6. 使用中间件
在使用 Redux 等状态管理库时,可以使用中间件如 redux-thunk
或 redux-saga
来处理异步操作,并确保操作的顺序。
Redux Thunk 示例:
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
const initialState = { count: 0 };
function reducer(state = initialState, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
default:
return state;
}
}
const store = createStore(reducer, applyMiddleware(thunk));
function incrementAsync() {
return (dispatch) => {
setTimeout(() => {
dispatch({ type: 'INCREMENT' });
}, 1000);
};
}
function App() {
return (
<Provider store={store}>
<button onClick={incrementAsync}>
Increment Async
</button>
<p>{store.getState().count}</p>
</Provider>
);
}
如何防止接口重复请求
1. 按钮禁用(最简单直接)
在请求发出后禁用操作按钮,请求完成后恢复:
// Vue3 示例(其他框架同理)
const loading = ref(false);
const fetchData = async () => {
if (loading.value) return; // 防止重复点击
loading.value = true;
try {
await axios.get('/api/data');
} finally {
loading.value = false; // 无论成功失败都恢复按钮
}
};
适用场景:表单提交、支付按钮等关键操作。
2. 请求锁(标志位)
通过标志变量控制请求状态:
let isFetching = false; // 全局请求锁
const fetchData = async () => {
if (isFetching) return;
isFetching = true;
try {
const res = await fetch('/api/data');
// 处理数据...
} finally {
isFetching = false; // 解锁
}
};
3. 请求取消(AbortController)
取消上一次未完成的相同请求(适用于搜索联想等场景):
let activeRequestController = null; // 存储当前活动的控制器
async function fetchData(url, delay = 0) {
// 取消上一个未完成的请求
if (activeRequestController) {
activeRequestController.abort('取消重复请求');
}
// 创建新的AbortController
const controller = new AbortController();
activeRequestController = controller;
try {
// 发送请求(添加延迟参数模拟慢请求)
const response = await axios.get(url, {
signal: controller.signal, // 关键:传递取消信号
params: { delay } // 模拟慢请求
});
// 请求完成后清除控制器
activeRequestController = null;
return response.data;
} catch (error) {
// 如果是主动取消的请求,不视为错误
if (axios.isCancel(error)) {
throw '请求被取消';
}
throw error;
}
}
4. 防抖(Debounce)
合并短时间内多次触发(如搜索框输入):
const debounce = (fn, delay) => {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
};
// 使用:300ms内连续输入只发1次请求
const search = debounce((query) => {
fetch(`/api/search?q=${query}`);
}, 300);
5. 节流(Throttle)
限制请求频率(如滚动加载):
const throttle = (fn, interval) => {
let lastTime = 0;
return (...args) => {
const now = Date.now();
if (now - lastTime >= interval) {
fn(...args);
lastTime = now;
}
};
};
// 使用:每500ms最多触发1次
const loadMore = throttle(() => {
fetch('/api/more-data');
}, 500);
6. 请求缓存(避免重复数据)
缓存已请求的数据(如切换Tab时):
const cache = new Map(); // 简单内存缓存
const fetchData = async (id) => {
if (cache.has(id)) {
return cache.get(id); // 返回缓存数据
}
const res = await axios.get(`/api/data/${id}`);
cache.set(id, res.data); // 设置缓存
return res.data;
};
7. 请求映射表(高级场景)
跟踪每个请求的状态(适用于复杂并发控制):
const pendingRequests = new Map();
const fetchData = async (key) => {
if (pendingRequests.has(key)) {
return pendingRequests.get(key); // 返回已存在的Promise
}
const promise = axios.get(`/api/data?key=${key}`);
pendingRequests.set(key, promise);
try {
const res = await promise;
return res.data;
} finally {
pendingRequests.delete(key); // 清理
}
};
后端返回 10w 条数据,前端该如何处理⭐
处理后端返回的 10 万条数据时,前端需要避免直接渲染所有数据(会导致页面崩溃),而是采用性能优化策略。
1、服务器端分页和过滤
- 服务器端分页: 最理想的方法是让后端只返回当前页面所需的数据,通过接口传递分页参数(例如 page 和 limit)。这样前端只处理少量数据,减轻渲染压力。
- 服务器端过滤和排序: 在后端做数据筛选,只返回满足条件的数据,进一步减少前端接收的数据量。
2、前端虚拟化技术
虚拟滚动(Virtual Scrolling): 如果一定要在前端加载大数据集,采用虚拟列表技术仅渲染当前可见区域的数据项。例如使用 React 的 react-window 或 react-virtualized;对于 Vue 也有类似的虚拟列表组件。
// 使用现有库(推荐): // - React: react-window / react-virtualized // - Vue: vue-virtual-scroller // - 原生:使用 IntersectionObserver 或计算滚动位置 // 示例(react-window): import { FixedSizeList as List } from 'react-window'; const items = Array.from({ length: 100000 }, (_, index) => `Item ${index}`); const Row = ({ index, style }) => ( <div style={style}>{items[index]}</div> ); const VirtualList = () => ( <List height={600} itemCount={items.length} itemSize={35} width={300}> {Row} </List> );
懒加载(Lazy Loading): 仅在用户滚动或交互时动态加载数据,避免一次性加载所有数据。
let currentPage = 1; const container = document.getElementById('list'); container.addEventListener('scroll', () => { if (container.scrollTop + container.clientHeight >= container.scrollHeight) { loadPage(++currentPage); } });
3、异步数据处理
Web Worker: 若前端需要对数据进行复杂计算,可以使用 Web Worker 将计算放到后台线程,避免阻塞主线程。
// 主线程 const worker = new Worker('data-worker.js'); worker.postMessage({ action: 'sort', data: bigData }); worker.onmessage = (e) => updateUI(e.data); // data-worker.js self.onmessage = (e) => { if (e.data.action === 'sort') { const sorted = e.data.data.sort(/* ... */); self.postMessage(sorted); } };
分批渲染: 将数据分为小批次,逐步渲染到页面上,从而分摊渲染开销,保持 UI 流畅。
function renderChunk(data, chunkSize = 100) { let index = 0; function process() { const chunk = data.slice(index, index + chunkSize); chunk.forEach(item => renderItem(item)); index += chunkSize; if (index < data.length) requestAnimationFrame(process); } process(); }
4、数据缓存与状态管理
- 数据缓存: 对于已加载的数据可以使用内存缓存或者 IndexedDB 缓存,避免重复请求,提升用户体验。
- 状态管理工具: 使用 Redux、Vuex 等状态管理工具,结合分页、懒加载策略管理大数据集的状态,避免全量数据占用内存。
一个网页,一开始很流畅,越用越卡顿,要怎么解决?
1.内存泄漏检查
- 表现: 内存占用持续上升,不释放。
- 解决方案:
- 使用 Chrome DevTools 的 Memory 面板 ,录制快照 (Heap Snapshot),查看 DOM 节点、事件监听器是否未被正确清理。
- 确保组件卸载时清除定时器、事件监听 (
removeEventListener
) 和订阅。 - 避免闭包导致无法释放变量。
2. 不必要的状态和数据堆积
- 表现: 前端状态或数据管理混乱,状态持续增长。
- 解决方案:
- 检查状态管理工具(Redux、Vuex)中的数据,避免存储大规模冗余数据。
- 使用数据分页、懒加载策略,减少前端数据体积。
- 清理过期缓存数据,避免 IndexedDB 或 LocalStorage 无限堆积。
3. 节流和防抖优化
- 表现: 频繁用户交互导致过多重渲染或计算。
- 解决方案:
- 使用
throttle
和debounce
控制滚动、输入等高频事件。 - 框架层面可使用 React.memo、Vue 的 computed 属性等避免不必要渲染。
- 使用
4. DOM 操作与渲染性能优化
- 表现: 频繁重排重绘,页面渲染卡顿。
- 解决方案:
- 检查是否有频繁的 DOM 操作,优化为批量更新。
- 使用虚拟 DOM 或虚拟滚动(virtual scrolling)技术。
- 尽量避免触发 Layout Throttle 属性(如
offsetWidth
、getBoundingClientRect()
)。
5. 垃圾回收 (GC) 问题
- 表现: 短时间内频繁的卡顿现象。
- 解决方案:
- 检查大对象频繁创建导致的 GC 开销。
- 优化对象复用策略,减少不必要的内存分配。
6. 资源管理优化
- 表现: 资源加载越来越慢。
- 解决方案:
- 使用
IntersectionObserver
实现懒加载,避免图片和第三方资源过早加载。 - 确保 WebSocket 连接、第三方 SDK 及时关闭。
- 使用
7. 工具与监控
- 工具:
- Chrome DevTools: 内存分析(Heap)、性能分析(Performance)。
- Lighthouse: 检查性能瓶颈。
- 前端监控平台(如 Sentry): 收集卡顿和性能数据。
一个 web 系统,加载很慢,交给你来优化,你会怎么办?
1.性能分析与瓶颈定位
- 使用 Chrome DevTools 的 Performance 和 Network 面板,分析页面加载的时间消耗,找出以下关键瓶颈:
- 白屏时间 (First Paint)过长
- 首屏渲染时间 (Largest Contentful Paint, LCP)过慢
- 阻塞资源 导致延迟加载
- API 请求过多或响应时间长
2. 资源加载优化
- 减少 HTTP 请求数量:
- 合并 CSS、JS 文件,或者采用 Tree Shaking 去掉无用代码。
- 使用雪碧图(Sprites)处理小图片,或者直接改用 SVG。
- 压缩与优化资源:
- 压缩图片(使用 WebP),优化视频大小。
- 压缩 JS、CSS、HTML 文件,开启 Gzip 或 Brotli 压缩。
- Lazy Loading:
- 延迟加载图片和视频,使用
loading="lazy"
属性。 - 采用懒加载策略来加载非首屏模块。
- 延迟加载图片和视频,使用
3. 静态资源缓存
- 启用浏览器缓存: 设置
Cache-Control
、ETag
等响应头,缓存静态资源。 - 使用 CDN: 静态资源分发到 CDN 节点,减少服务器负载。
4. 网络传输优化
- 启用 HTTP/2 或 HTTP/3: 并行加载资源,降低传输延迟。
- 减少跨域请求: 优化 API 接口分布,避免预检请求(OPTIONS)。
- 开启 DNS 预解析: 提前解析第三方域名。
5. 渲染与框架性能优化
- 服务端渲染(SSR)/静态生成(SSG): 减少客户端渲染时间,提升首屏性能。
- 组件懒加载: 分离路由和组件,按需加载代码。
- 虚拟化列表: 渲染大量数据时使用
react-window
或类似方案。
6. 后端与 API 优化
- 数据库优化:
- 建立索引,优化查询。
- 数据库结果分页返回。
- 接口合并与优化: 减少多次 API 调用,使用 GraphQL 或批量 API。
- 缓存策略: 使用 Redis 等缓存热点数据,减轻数据库查询压力。
7. 用户体验提升
- 骨架屏: 在加载内容前显示占位图,减少白屏时间。
- Loading 动画: 提升用户感知体验。
8. 监控与持续优化
- 引入性能监控工具:
- Lighthouse 进行性能分析。
- Sentry 捕获性能问题。
- Web Vitals(FCP、LCP、CLS)实时监控。
如果让你从零搭建一个后台管理系统,你会怎么做?
1⃣ 技术选型: Vite + Vue3 + Pinia + Element Plus/Ant Design Vue(主流方案); 2⃣ 工程化: 配好ESLint/Prettier/Commit规范,拆分环境变量; 3⃣ 核心模块: 权限路由(动态路由表+按钮级权限)、请求封装(拦截器+错误统一处理)、状态管理(按模块拆分Store); 4⃣ 性能与安全: 路由懒加载、接口防抖、XSS过滤(展现全局观); 5⃣ 协作: 用GitFlow管理分支,写清晰README和CHANGELOG(工程意识)
遇到复杂Bug(如页面内存泄漏)怎么排查?
1⃣ 复现 & 定位: 用Chrome DevTools的Memory面板抓快照,对比增量; 2⃣ 常见嫌疑点: 检查全局事件监听、定时器、闭包引用、第三方库订阅; 3⃣ 工具验证: 用 performance.memory 或 window.performance 监控; 4⃣ 解决: 移除无用引用、解绑事件、优化组件销毁逻辑。
其他
什么是服务端渲染?(SSR)⭐
服务端渲染(Server-Side Rendering,简称 SSR)是一种将网页内容在服务器端动态生成并发送给客户端的技术。传统的客户端渲染(Client-Side Rendering,简称 CSR)是在客户端浏览器中使用 JavaScript 动态生成页面内容。
在传统的客户端渲染中,浏览器首先下载一个空的 HTML 页面,然后通过 JavaScript 请求数据并生成页面内容。这种方式的优点是可以提供更丰富的交互和动态效果,但也存在一些缺点。例如,搜索引擎爬虫可能无法正确解析和索引页面内容,导致 SEO(搜索引擎优化)问题。同时,初始加载时用户可能会看到空白的页面或者出现闪烁的内容。
相比之下,服务端渲染通过在服务器上预先生成完整的 HTML 页面,将其发送给客户端浏览器。这样,浏览器在接收到页面时就能够立即显示完整的内容,而不需要等待 JavaScript 的下载和执行。这样可以提高页面的加载速度和首次渲染速度,并且对于搜索引擎爬虫来说更容易解析和索引页面内容,有利于 SEO。
客户端渲染:获取 HTML 文件,根据需要下载 JavaScript 文件,运行文件,生成 DOM,再渲染。
服务端渲染:服务端返回 HTML 文件,客户端只需解析 HTML。
SSR 的核心优势
更快的首屏加载
SSR 在服务器端生成 HTML,用户无需等待 JavaScript 加载完成即可看到页面内容。
更好的 SEO
搜索引擎可以抓取服务器渲染的完整 HTML 内容,而不是空的
<div id="root"></div>
。更好的用户体验
对于低性能设备或网络较差的用户,SSR 可以提供更快的初始渲染。
缺点:配置麻烦,增加了服务器的计算压力。
SSR 的基本原理
- 服务器端
- 使用
ReactDOMServer
将 React 组件渲染为 HTML 字符串。 - 将生成的 HTML 字符串嵌入到 HTML 模板中,并发送给客户端。
- 使用
- 客户端
- 客户端接收到 HTML 后,React 会“接管”页面(hydration),使其成为可交互的 SPA(单页应用)。
客户端渲染过程
- 访问客户端渲染的网站。
- 服务器返回一个包含了引入资源语句和
<div id="app"></div>
的 HTML 文件。 - 客户端通过 HTTP 向服务器请求资源,当必要的资源都加载完毕后,执行
new Vue()
开始实例化并渲染页面。
服务端渲染过程
- 访问服务端渲染的网站。
- 服务器会查看当前路由组件需要哪些资源文件,然后将这些文件的内容填充到 HTML 文件。如果有 ajax 请求,就会执行它进行数据预取并填充到 HTML 文件里,最后返回这个 HTML 页面。
- 当客户端接收到这个 HTML 页面时,可以马上就开始渲染页面。与此同时,页面也会加载资源,当必要的资源都加载完毕后,开始执行
new Vue()
开始实例化并接管页面。
从上述两个过程中可以看出,区别就在于第二步。客户端渲染的网站会直接返回 HTML 文件,而服务端渲染的网站则会渲染完页面再返回这个 HTML 文件。
Nuxt.js、Next.js、Nest.js的区别
- Nuxt.js 和 Next.js 都是用于构建服务器渲染应用的框架(SSR),分别基于 Vue.js 和 React。
- Nuxt.js 适用于构建 Vue.js 应用程序,提供了默认的配置和约定,使得开发 SSR 应用更加简单。
- Next.js 适用于构建 React 应用程序,具有出色的性能和开发体验,并支持静态生成和服务器端渲染。
- Nest.js 是一个用于构建 Node.js 服务器端应用的框架,结合了 TypeScript 和面向对象编程的概念,提供了模块化的架构设计和丰富的功能。
什么是SEO,怎么解决SEO不友好
seo是搜索引擎优化。在搜索引擎自然排名机制的基础上,对网站进行内部及外部的调整优化,改进网站在搜索引擎中的关键词自然排名,获得更多的流量
单页面的内容是根据路由变化动态生成并展示出来的,很多页面的内容是通过ajax异步获取的,网络抓取工具并不会等待异步请求完成后再行抓取页面内容
搜索引擎爬虫是不会等待异步请求数据结束后再抓取信息的
解决方式可以使用SSR(服务端渲染)
或者是预渲染
如何上传大文件?⭐
大文件上传主要有以下几种方案:
切片上传
- 将大文件分割成小块
- 并发上传多个切片
- 服务端合并所有切片
- 支持断点续传和进度显示
实现步骤:
1.前端切片
function createFileChunk(file, size = 1 * 1024 * 1024) {
const chunks = []
let cur = 0
while (cur < file.size) {
chunks.push(file.slice(cur, cur + size))
cur += size
}
return chunks
}
2.上传切片
async function uploadChunks(chunks) {
const requests = chunks.map((chunk, index) => {
const formData = new FormData()
formData.append('chunk', chunk)
formData.append('index', index)
return axios.post('/upload', formData)
})
await Promise.all(requests)
}
3.发送合并请求
await axios.post('/merge', {
filename: file.name,
size: chunks.length,
})
断点续传
- 记录已上传的切片
- 重新上传时跳过已上传的部分
- 可以通过 localStorage 存储进度
- 使用 hash 标识文件和切片
秒传
- 上传前先发送文件 hash
- 服务端存在相同文件则直接返回
- 可以使用 spark-md5 计算文件 hash
性能优化
- 并发控制:限制同时上传的切片数
- 切片大小:根据网络状况动态调整
- 进度显示:计算整体上传进度
- 错误重试:单个切片上传失败后重试
网页多标签页之间如何通讯?和 iframe 如何通讯?
网页多标签页和 iframe 通讯的关键考点是跨窗口和跨域通信模型的选择,以及不同场景下的适用方法。可以从以下几个方面分析:
1. 多标签页之间的通讯方法
BroadcastChannel API 同源的多个标签页可以使用
BroadcastChannel
进行消息广播,简单方便。 示例 :const channel = new BroadcastChannel('my_channel') channel.postMessage('Hello from another tab!') channel.onmessage = (event) => { console.log('Received message:', event.data) }
LocalStorage + Storage 事件监听 不同标签页可以共享
localStorage
,通过监听storage
事件实现通讯。 示例 :window.addEventListener('storage', (event) => { if (event.key === 'my_key') { console.log('Received message:', event.newValue) } }) localStorage.setItem('my_key', 'Hello from another tab!')
Service Worker 通过
Service Worker
作为中介,实现跨标签页通讯。适合 PWA 场景。WebSocket 通过服务器中转实现实时通讯,适合跨域或需要长连接的场景。
2. iframe 通讯方法
postMessage API 最常用的方式,可以跨域发送消息。父页面和 iframe 双向通信都支持。 示例(父页面向 iframe 发送消息) :
const iframe = document.querySelector('iframe') iframe.contentWindow.postMessage('Hello iframe!', '*') window.addEventListener('message', (event) => { console.log('Received from iframe:', event.data) })
URL Hash 传参 通过修改 iframe 的 URL 哈希来传递参数。适用于简单场景。 示例 :
iframe.src = 'https://example.com#message=Hello'
共享 Cookie 或 LocalStorage 在同源环境下可以通过共享存储机制间接通讯。
从 0 搭建一个前端项目,需要考虑哪些方面
现在创建一个 Vue React 一般都是拿脚手架 cli 一键生成项目,这是最基础的。
在实际工作中还需要考虑更多的内容。
- 代码仓库,发布到哪个 npm 仓库(如有需要)
- 技术选型 Vue React 等
- 代码目录规范
- 打包构建 webpack 等,做打包优化
- eslint prettier commit-lint
- husky pre-commit
- 单元测试 + 集成测试
- CI/CD 流程,自动测试,自动发布测试环境,自动部署
- 开发环境,预发布环境
- 开发文档,研发规范
如何做好技术选型
当在工作中选择一个 语言/框架/工具 时,需要考虑什么?
- 社区热门程度,搜索引擎和 StackOverflow 上能搜出多少相关资料?
- 创办时间和发展时间,不要用太新的技术,首先要求稳
- 看使用人数,参考 GitHub star 数量和 npm 下载量
- 看社区生态的完善程度,第三方的 UI 、组件、插件等,都是否完善
- 团队成员的学习成本 —— 这一点很重要,很多人会忽略
如何理解技术方案设计?是否做过技术方案设计?
- state 数据结构,如会存储在 vuex 或 redux 中
- 组件 UI 结构,嵌套关系,属性如何传递
- 会用到哪些服务端的 API ,哪些是现有的,哪些需要新开发
- 如有复杂逻辑,说明计算过程,时间复杂度
- 是否有性能隐患?
- 是否有安全隐患?
线上出了严重 bug 你该如何解决?
- 回滚,及时止损 —— 这一步最重要,很多人不知道这一步!!!
- 通知项目组成员,看谁最近有过上线?—— 线上 bug 一般是最近一次上线导致的
- 在本地或测试环境浮现 bug,查找原因
- 修复,测试,重新上线
- 开复盘会议,以后如何规避此类问题 —— 复盘会议,也是很多人不知道的
另,如果你项目没有监控报警的话,最好加一个,这样线上有 bug 会及时报警。
如何保障代码质量
- 配置统一的 eslint 和 prettier 规则,规范代码格式
- 每次代码合并,都进行 code review ,外加每周一次团队 code review
- 编写单元测试,提交 commit 时自动触发单元测试
- 使用 Sentry 等平台进行线上错误报警,并及时修复问题
ui组件库选型时,会考虑哪些因素
1. 技术栈匹配
- 框架兼容性:是否支持项目所用前端框架(React、Vue、Angular等)。 示例:Ant Design(React)、Element UI(Vue)、NG-ZORRO(Angular)。
- 版本适配性:是否兼容当前技术栈版本(如React 18+、Vue 3)。
2. 功能覆盖与组件丰富度
- 基础组件:按钮、表单、表格、弹窗等是否齐全。
- 高级组件:是否包含复杂需求组件(如可编辑表格、拖拽排序、树形控件)。
- 扩展性:是否支持自定义组件或覆盖默认行为。
3. 定制化能力
- 主题系统:是否支持通过CSS变量、主题配置文件或设计工具(如Figma插件)调整样式。
- 样式覆盖:能否灵活修改组件样式(如通过CSS-in-JS、Sass/Less变量)。 示例:Material-UI的ThemeProvider、Ant Design的定制主题工具。
4. 文档与开发者体验
- 文档质量:是否提供清晰的API文档、示例代码和最佳实践指南。
- 开发工具:是否有配套的CLI工具、TypeScript支持、IDE插件等。
- 学习曲线:组件API设计是否直观,上手难度如何。
5. 性能与体积
- 包大小:是否支持按需加载(Tree-shaking)以减少最终构建体积。
- 渲染性能:大数据量场景下的表现(如虚拟滚动、懒加载)。 示例:AG Grid针对大数据表格优化,TanStack Table轻量灵活。
6. 可访问性(A11y)
- WCAG合规性:是否遵循无障碍标准(如ARIA标签、键盘导航)。
- 屏幕阅读器支持:组件是否通过无障碍测试工具(如axe、Lighthouse)。
7. 社区与维护
- 活跃度:GitHub Stars、Issue响应速度、版本更新频率。
- 长期维护:是否有稳定团队或公司支持(如Ant Design由蚂蚁集团维护)。
- 生态插件:是否有第三方插件或工具链支持(如ProComponents、Storybook集成)。
8. 企业级需求
- 国际化(i18n):是否内置多语言支持。
- 权限控制:组件是否支持动态渲染(如根据权限隐藏按钮)。
- 安全合规:是否符合企业安全要求(如XSS防护)。
9. 设计与一致性
- 设计语言:是否符合产品品牌风格(如Material Design、Ant Design风格)。
- 设计资源:是否提供Figma/Sketch设计文件(如IBM Carbon、Semi Design)。
10. 商业与法律因素
- 开源协议:MIT、Apache 2.0等是否允许商用(避免GPL等限制性协议)。
- 商业支持:是否需要付费版(如DevExtreme、Kendo UI)或专业服务。
11. 团队适配性
- 熟悉度:团队是否已有相关技术经验,降低学习成本。
- 内部规范:是否与现有代码规范、工程化流程兼容(如SSR、微前端支持)。
12. 跨端与响应式
- 移动端适配:是否提供移动端组件(如Vant、MUI Mobile)。
- 响应式布局:是否支持不同屏幕尺寸(如Grid布局、断点系统)。
最终决策:优先选择技术栈匹配度高、社区活跃、文档完善的库,同时结合团队习惯和长期维护成本。
例如:
- 中后台系统:Ant Design(React)、Element Plus(Vue 3)。
- 极致性能:Headless UI(无样式)+ Tailwind CSS。
- 跨端应用:MUI(Web)、React Native Paper(移动端)。