乐闻世界logo
搜索文章和话题

服务端面试题手册

Android中View的绘制流程是怎样的?

View的绘制流程是Android UI系统的核心,理解绘制机制对于自定义View和性能优化至关重要。View绘制的三个阶段View的绘制流程遵循Measure → Layout → Draw三个阶段:View绘制流程:measure() → onMeasure() → 测量View宽高 ↓layout() → onLayout() → 确定View位置 ↓draw() → onDraw() → 绘制View内容第一阶段:Measure(测量)测量目的确定View的宽度和高度(measuredWidth/measuredHeight)。MeasureSpecMeasureSpec是父View对子View的尺寸要求,由模式和尺寸组成:| 模式 | 值 | 含义 || ----------- | ---------- | ----------------------- || EXACTLY | 0x40000000 | 精确值,如match_parent或具体数值 || AT_MOST | 0x80000000 | 最大值,如wrap_content || UNSPECIFIED | 0x00000000 | 无限制,如ScrollView中的子View |测量流程@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); // 根据模式计算实际尺寸 int width = calculateWidth(widthMode, widthSize); int height = calculateHeight(heightMeasureSpec); setMeasuredDimension(width, height);}第二阶段:Layout(布局)布局目的确定View在父容器中的位置(left, top, right, bottom)。布局流程@Overrideprotected void onLayout(boolean changed, int left, int top, int right, int bottom) { // 遍历子View,确定每个子View的位置 for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); child.layout(childLeft, childTop, childRight, childBottom); }}第三阶段:Draw(绘制)绘制步骤draw()方法包含6个步骤:public void draw(Canvas canvas) { // 1. 绘制背景 drawBackground(canvas); // 2. 保存画布状态 saveCount = canvas.getSaveCount(); // 3. 绘制内容(子类实现) onDraw(canvas); // 4. 绘制子View dispatchDraw(canvas); // 5. 绘制装饰(如滚动条) onDrawForeground(canvas); // 6. 恢复画布状态 canvas.restoreToCount(saveCount);}ViewGroup的绘制特点ViewGroup继承自View,但增加了子View管理:测量阶段:遍历测量所有子View布局阶段:确定子View的位置绘制阶段:dispatchDraw()绘制所有子View绘制优化技巧1. 减少重绘// 使用invalidate(Rect)局部重绘invalidate(0, 0, 100, 100);// 使用ViewStub延迟加载<ViewStub android:id="@+id/stub" android:layout="@layout/view" />2. 避免过度绘制移除不必要的背景使用clipRect减少绘制区域使用GPU Overdraw调试工具检测3. 使用硬件加速<application android:hardwareAccelerated="true">自定义View要点public class CustomView extends View { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 处理wrap_content if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST) { setMeasuredDimension(defaultWidth, defaultHeight); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 绘制自定义内容 canvas.drawCircle(cx, cy, radius, paint); }}面试要点理解MeasureSpec的三种模式掌握requestLayout()和invalidate()的区别了解绘制流程的触发时机掌握自定义View的基本步骤理解硬件加速和软件绘制的区别
阅读 0·3月7日 12:12

Android中内存泄漏的常见场景有哪些,如何检测和避免?

内存泄漏是指程序中已分配的内存由于某种原因未释放或无法释放,导致可用内存逐渐减少,最终可能引发OOM(Out Of Memory)崩溃。常见内存泄漏场景1. 静态变量持有Activity/Context引用public class Singleton { private static Singleton instance; private Context context; private Singleton(Context context) { this.context = context; // 泄漏!持有Activity引用 } // 解决方案:使用ApplicationContext private Singleton(Context context) { this.context = context.getApplicationContext(); }}2. 非静态内部类/匿名内部类public class MainActivity extends Activity { private Handler handler = new Handler() { // 非静态内部类 @Override public void handleMessage(Message msg) { // 持有外部Activity引用 } };}// 解决方案:使用静态内部类 + WeakReferenceprivate static class MyHandler extends Handler { private WeakReference<Activity> weakRef; MyHandler(Activity activity) { weakRef = new WeakReference<>(activity); }}3. 未取消注册的监听器和广播// 内存泄漏示例registerReceiver(receiver, filter);// 忘记在onDestroy中unregisterReceiver// 解决方案@Overrideprotected void onDestroy() { super.onDestroy(); unregisterReceiver(receiver);}4. 资源未关闭数据库Cursor未关闭IO流未关闭Bitmap未回收5. 集合中的对象引用static List<Activity> activityList = new ArrayList<>();// 添加Activity但不移除activityList.add(activity);内存泄漏检测工具1. Android Studio Memory Profiler实时监控内存分配堆转储(Heap Dump)分析查看对象引用链2. LeakCanarydependencies { debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.x'}自动检测内存泄漏生成泄漏引用链通知提示泄漏信息3. MAT(Memory Analyzer Tool)分析hprof文件查找支配树(Dominator Tree)识别泄漏嫌疑对象内存泄漏避免策略1. 使用ApplicationContext// 当不需要Activity特性时Intent intent = new Intent(getApplicationContext(), Target.class);2. 及时释放资源@Overrideprotected void onDestroy() { super.onDestroy(); // 移除Handler消息和回调 handler.removeCallbacksAndMessages(null); // 取消网络请求 call.cancel(); // 注销监听器 sensorManager.unregisterListener(listener);}3. 使用WeakReferenceprivate static class MyAsyncTask extends AsyncTask<Void, Void, Void> { private WeakReference<Activity> weakActivity; MyAsyncTask(Activity activity) { weakActivity = new WeakReference<>(activity); } @Override protected Void doInBackground(Void... voids) { // 后台任务 return null; } @Override protected void onPostExecute(Void result) { Activity activity = weakActivity.get(); if (activity != null && !activity.isFinishing()) { // 更新UI } }}4. 使用Lifecycle组件class MyObserver implements DefaultLifecycleObserver { @Override public void onDestroy(LifecycleOwner owner) { // 自动清理 }}面试要点理解Java内存模型和GC机制掌握引用类型:强引用、软引用、弱引用、虚引用熟悉常见泄漏场景和解决方案能够使用工具分析和定位泄漏了解Android内存优化最佳实践
阅读 0·3月7日 12:12

Android中如何进行性能优化,有哪些常用工具?

性能优化是Android开发的核心技能,涉及内存、UI、网络、电量等多个维度。1. 内存优化内存泄漏检测LeakCanary:自动检测内存泄漏Android Profiler:实时监控内存分配MAT:分析hprof文件,查找支配树内存优化策略// 1. 使用SparseArray替代HashMap<Integer, Object>val sparseArray = SparseArray<String>()// 2. 图片内存优化val options = BitmapFactory.Options().apply { inSampleSize = 2 // 缩放采样 inBitmap = reusableBitmap // Bitmap复用}// 3. 使用LRU缓存val cache = LruCache<String, Bitmap>(maxMemory / 8)大图优化使用inSampleSize压缩使用WebP格式使用图片加载库(Glide、Picasso、Coil)2. UI渲染优化布局优化<!-- 1. 减少布局层级 --><merge> <!-- 减少一层ViewGroup --><include> <!-- 复用布局 --><ViewStub> <!-- 延迟加载 -->避免过度绘制// 1. 移除不必要的背景window.setBackgroundDrawable(null)// 2. 使用clipRect减少绘制区域canvas.clipRect(left, top, right, bottom)列表优化// RecyclerView优化recyclerView.setHasFixedSize(true)recyclerView.setItemViewCacheSize(20)recyclerView.setRecycledViewPool(pool)3. 网络优化请求优化// 1. 使用连接池okHttpClient.connectionPool(ConnectionPool(5, 5, TimeUnit.MINUTES))// 2. 启用Gzip压缩okHttpClient.addInterceptor(GzipInterceptor())// 3. 合理设置超时okHttpClient.connectTimeout(10, TimeUnit.SECONDS)数据缓存// 1. 使用Cache-Control@Headers("Cache-Control: max-age=3600")// 2. 本地缓存策略val cache = Cache(cacheDir, 10 * 1024 * 1024) // 10MB4. 电量优化Doze模式和App Standby理解系统省电机制使用高优先级FCM消息批量处理后台任务后台任务优化// 使用WorkManager替代后台Serviceval constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresBatteryNotLow(true) .build()val workRequest = PeriodicWorkRequestBuilder<SyncWorker>(1, TimeUnit.HOURS) .setConstraints(constraints) .build()定位优化// 使用平衡精度模式locationRequest.priority = LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY// 及时移除定位更新fusedLocationClient.removeLocationUpdates(callback)5. 包体积优化资源优化android { // 1. 移除无用资源 lintOptions { checkReleaseBuilds false } // 2. 资源压缩 shrinkResources true minifyEnabled true // 3. 只保留特定语言 resConfigs "zh", "en"}代码优化// 1. 使用ProGuard/R8混淆proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')// 2. 动态功能模块android { dynamicFeatures = [':feature1', ':feature2']}6. 启动优化异步初始化延迟加载非必要组件使用SplashScreen API7. 常用性能分析工具| 工具 | 用途 | 使用场景 || --------------------- | ----------- | --------- || Android Profiler | CPU/内存/网络监控 | 实时监控应用性能 || Systrace | 系统级性能分析 | 分析帧率、启动时间 || Layout Inspector | 布局层级分析 | 优化布局嵌套 || GPU Overdraw | 过度绘制检测 | 优化绘制性能 || LeakCanary | 内存泄漏检测 | 开发阶段检测泄漏 || StrictMode | 违规检测 | 检测主线程IO等 || Battery Historian | 电量分析 | 分析电量消耗 |8. Systrace使用示例# 抓取tracepython systrace.py -a com.example -o trace.html sched gfx view# 分析重点# 1. 查看帧率(Frame)是否掉帧# 2. 查看UI线程是否阻塞# 3. 查看GC频率9. 性能优化检查清单[ ] 使用Release模式测试性能[ ] 在低端设备上验证[ ] 监控线上性能数据[ ] 定期进行性能回归测试[ ] 建立性能基准指标面试要点掌握内存优化的常见方法理解UI渲染原理和优化手段熟悉各种性能分析工具的使用了解电量优化的最佳实践掌握APK瘦身的技术方案
阅读 0·3月7日 12:11

Android中热修复技术的原理是什么,有哪些主流方案?

热修复(HotFix)是一种在不重新发布应用的情况下,动态修复线上Bug的技术方案。热修复的核心原理1. 类加载机制Android使用PathClassLoader和DexClassLoader加载类:PathClassLoader:加载已安装APK的dex文件DexClassLoader:加载任意路径的dex文件2. 热修复的基本思路原理:让类加载器优先加载修复后的类,覆盖有问题的类实现方式:1. 将修复代码打包成dex文件2. 通过反射插入到dexElements数组前面3. 类加载时优先找到修复类主流热修复方案对比| 方案 | 原理 | 优点 | 缺点 | 代表 || --------------- | --------------- | --------- | --------- | ------------- || 底层替换 | 替换ArtMethod结构体 | 即时生效,无需重启 | 兼容性差,稳定性低 | AndFix、Sophix || 类加载 | 修改dexElements数组 | 稳定性高,兼容性好 | 需要重启生效 | Tinker、QZone || Instant Run | 自定义ClassLoader | 开发调试方便 | 仅适用于开发 | Google官方 |详细方案分析1. Tinker(微信)原理:1. 生成新旧APK的差分包(patch.dex)2. 下载patch.dex到本地3. 合并patch.dex和原APK的dex4. 重启后通过修改dexElements加载新dex特点:- 支持类、资源、so库替换- 需要重启应用生效- 差分包小,下载快2. Sophix(阿里云)原理:1. 底层替换方案:替换ArtMethod的入口2. 类加载方案:作为兜底方案特点:- 即时生效,无需重启- 支持方法级修复- 收费方案,稳定性好3. Robust(美团)原理:1. 编译期在每个方法插入逻辑2. 运行时通过路由跳转到修复类特点:- 即时生效- 包体积增加少- 需要提前插入代码类加载方案实现细节Dex插桩核心代码public class HotFix { public static void patch(Context context, File patchDexFile) { try { // 获取PathClassLoader ClassLoader classLoader = context.getClassLoader(); // 获取pathList字段 Object pathList = getField(classLoader, "pathList"); // 获取dexElements字段 Object[] dexElements = (Object[]) getField(pathList, "dexElements"); // 创建新的dexElements(包含patch.dex) Object[] newElements = makeDexElements(patchDexFile); // 合并数组:patch.dex在前 Object[] combined = (Object[]) Array.newInstance( dexElements.getClass().getComponentType(), newElements.length + dexElements.length ); System.arraycopy(newElements, 0, combined, 0, newElements.length); System.arraycopy(dexElements, 0, combined, newElements.length, dexElements.length); // 替换dexElements setField(pathList, "dexElements", combined); } catch (Exception e) { e.printStackTrace(); } }}热修复的限制1. 无法修复的情况AndroidManifest.xml的修改新增四大组件资源ID变化导致的资源引用错误部分ROM的兼容性限制2. 安全风险代码注入风险需要校验patch签名传输过程需要加密面试要点理解类加载机制和双亲委托模型掌握dexElements插桩原理了解各方案的优缺点和适用场景理解热修复的局限性和安全风险熟悉Tinker、Sophix等主流方案
阅读 0·3月7日 12:11

axios 中如何实现并发请求和取消请求?请提供代码示例

Axios 并发请求Axios 提供了 axios.all() 和 axios.spread() 方法来处理并发请求,同时也支持使用原生的 Promise.all()。1. 使用 Promise.all()(推荐)// 同时发送多个请求async function fetchMultipleData() { try { const [users, posts, comments] = await Promise.all([ axios.get('/api/users'), axios.get('/api/posts'), axios.get('/api/comments') ]); console.log('Users:', users.data); console.log('Posts:', posts.data); console.log('Comments:', comments.data); return { users: users.data, posts: posts.data, comments: comments.data }; } catch (error) { console.error('至少一个请求失败:', error); throw error; }}2. 使用 axios.all()(传统方式)axios.all([ axios.get('/api/users'), axios.get('/api/posts'), axios.get('/api/comments')]).then(axios.spread((users, posts, comments) => { // 所有请求都成功时执行 console.log('Users:', users.data); console.log('Posts:', posts.data); console.log('Comments:', comments.data);})).catch(error => { // 任一请求失败时执行 console.error('请求失败:', error);});3. 并发请求的错误处理async function fetchWithErrorHandling() { const requests = [ axios.get('/api/users'), axios.get('/api/posts'), axios.get('/api/comments') // 可能失败的请求 ]; // 使用 Promise.allSettled 等待所有请求完成 const results = await Promise.allSettled(requests); results.forEach((result, index) => { if (result.status === 'fulfilled') { console.log(`请求 ${index} 成功:`, result.value.data); } else { console.error(`请求 ${index} 失败:`, result.reason.message); } }); // 过滤出成功的结果 const successfulResults = results .filter(result => result.status === 'fulfilled') .map(result => result.value.data); return successfulResults;}4. 限制并发数量// 使用 p-limit 或自定义实现限制并发async function fetchWithConcurrencyLimit(urls, limit = 3) { const results = []; const executing = []; for (const [index, url] of urls.entries()) { const promise = axios.get(url).then(res => ({ index, data: res.data })); results.push(promise); if (urls.length >= limit) { executing.push(promise); if (executing.length >= limit) { await Promise.race(executing); executing.splice( executing.findIndex(p => p === promise), 1 ); } } } return Promise.all(results);}// 使用const urls = ['/api/data/1', '/api/data/2', '/api/data/3', '/api/data/4'];fetchWithConcurrencyLimit(urls, 2);Axios 取消请求1. 使用 AbortController(推荐,v0.22.0+)// 创建 AbortControllerconst controller = new AbortController();// 发送请求时传入 signalaxios.get('/api/data', { signal: controller.signal}).then(response => { console.log(response.data);}).catch(error => { if (axios.isCancel(error)) { console.log('请求已取消:', error.message); } else { console.error('请求失败:', error); }});// 取消请求controller.abort('用户取消操作');// 5秒后自动取消setTimeout(() => { controller.abort('请求超时');}, 5000);2. 在 React 组件中使用import { useEffect } from 'react';function UserList() { useEffect(() => { const controller = new AbortController(); const fetchUsers = async () => { try { const response = await axios.get('/api/users', { signal: controller.signal }); // 处理数据 } catch (error) { if (axios.isCancel(error)) { console.log('组件卸载,请求已取消'); } else { console.error('获取用户失败:', error); } } }; fetchUsers(); // 组件卸载时取消请求 return () => { controller.abort('组件卸载'); }; }, []); return <div>User List</div>;}3. 在 Vue 组件中使用<script setup>import { onMounted, onUnmounted } from 'vue';let controller;onMounted(() => { controller = new AbortController(); axios.get('/api/data', { signal: controller.signal }) .then(response => { console.log(response.data); }) .catch(error => { if (axios.isCancel(error)) { console.log('请求已取消'); } });});onUnmounted(() => { controller?.abort('组件卸载');});</script>4. 取消多个请求const controllers = new Map();// 发送请求时保存 controllerfunction fetchWithCancel(key, url) { // 取消之前的同名请求 if (controllers.has(key)) { controllers.get(key).abort('重复请求,取消前一个'); } const controller = new AbortController(); controllers.set(key, controller); return axios.get(url, { signal: controller.signal }) .finally(() => { controllers.delete(key); });}// 使用:搜索框防抖场景fetchWithCancel('search', '/api/search?q=keyword');5. 请求超时自动取消async function fetchWithTimeout(url, timeout = 5000) { const controller = new AbortController(); const timeoutId = setTimeout(() => { controller.abort(`请求超时 (${timeout}ms)`); }, timeout); try { const response = await axios.get(url, { signal: controller.signal }); clearTimeout(timeoutId); return response.data; } catch (error) { clearTimeout(timeoutId); throw error; }}6. 取消请求的工具函数封装class RequestManager { constructor() { this.controllers = new Map(); } // 发送请求 async request(key, config) { // 取消之前的同名请求 this.cancel(key); const controller = new AbortController(); this.controllers.set(key, controller); try { const response = await axios({ ...config, signal: controller.signal }); return response; } finally { this.controllers.delete(key); } } // 取消指定请求 cancel(key, message = '请求被取消') { if (this.controllers.has(key)) { this.controllers.get(key).abort(message); this.controllers.delete(key); } } // 取消所有请求 cancelAll(message = '所有请求被取消') { this.controllers.forEach(controller => { controller.abort(message); }); this.controllers.clear(); }}// 使用const requestManager = new RequestManager();// 发送请求requestManager.request('userList', { method: 'GET', url: '/api/users'});// 取消指定请求requestManager.cancel('userList');// 取消所有请求(如页面切换时)requestManager.cancelAll();最佳实践组件卸载时取消请求:避免内存泄漏和状态更新错误重复请求时取消前一个:搜索框、表单提交等场景设置合理的超时时间:防止请求挂起正确处理取消错误:区分取消错误和业务错误使用 AbortController:现代浏览器标准 API,兼容性好
阅读 0·3月7日 12:10

如何在 axios 中实现请求和响应拦截器?请举例说明实际应用场景

Axios 拦截器概述Axios 拦截器允许你在请求发送前或响应接收后统一处理数据,是实现全局配置、错误处理、权限验证等功能的重要机制。请求拦截器(Request Interceptors)基本用法// 添加请求拦截器axios.interceptors.request.use( function (config) { // 在发送请求之前做些什么 return config; }, function (error) { // 对请求错误做些什么 return Promise.reject(error); });实际应用场景1. 统一添加认证 Tokenaxios.interceptors.request.use( config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, error => { return Promise.reject(error); });2. 添加时间戳防止缓存axios.interceptors.request.use( config => { if (config.method === 'get') { config.params = { ...config.params, _t: Date.now() }; } return config; });3. 显示加载状态let requestCount = 0;axios.interceptors.request.use( config => { requestCount++; if (requestCount === 1) { // 显示全局 loading showLoading(); } return config; });响应拦截器(Response Interceptors)基本用法// 添加响应拦截器axios.interceptors.response.use( function (response) { // 对响应数据做点什么 return response; }, function (error) { // 对响应错误做点什么 return Promise.reject(error); });实际应用场景1. 统一错误处理axios.interceptors.response.use( response => response, error => { const { response } = error; if (response) { switch (response.status) { case 401: // 未授权,清除 token 并跳转到登录页 localStorage.removeItem('token'); window.location.href = '/login'; break; case 403: message.error('没有权限访问该资源'); break; case 404: message.error('请求的资源不存在'); break; case 500: message.error('服务器内部错误'); break; default: message.error(response.data.message || '请求失败'); } } else { message.error('网络错误,请检查网络连接'); } return Promise.reject(error); });2. 响应数据格式化axios.interceptors.response.use( response => { // 假设后端统一返回格式:{ code: 0, data: {}, message: '' } const res = response.data; if (res.code !== 0) { message.error(res.message); return Promise.reject(new Error(res.message)); } return res.data; // 直接返回 data,简化组件中的调用 });3. 隐藏加载状态axios.interceptors.response.use( response => { requestCount--; if (requestCount === 0) { hideLoading(); } return response; }, error => { requestCount--; if (requestCount === 0) { hideLoading(); } return Promise.reject(error); });移除拦截器const myInterceptor = axios.interceptors.request.use(() => {});axios.interceptors.request.eject(myInterceptor);为实例添加拦截器const instance = axios.create({ baseURL: 'https://api.example.com'});instance.interceptors.request.use(config => { // 只对当前实例生效 return config;});多个拦截器的执行顺序axios.interceptors.request.use(config => { console.log('请求拦截器 1'); return config;});axios.interceptors.request.use(config => { console.log('请求拦截器 2'); return config;});// 执行顺序:请求拦截器 2 → 请求拦截器 1 → 发送请求// 响应拦截器执行顺序与添加顺序相反最佳实践错误处理要完整:请求和响应拦截器都要处理错误情况记得返回 config/response:否则请求不会继续使用实例隔离:不同服务使用不同实例,避免相互影响避免副作用:拦截器中不要修改原始参数对象添加请求标识:方便调试和追踪
阅读 0·3月7日 12:10

axios 中如何进行错误处理?请详细说明错误类型和处理策略

Axios 错误类型Axios 请求可能产生的错误主要分为以下几类:1. 请求配置错误URL 格式错误请求方法错误配置参数错误2. 网络错误无网络连接请求超时DNS 解析失败CORS 跨域错误3. HTTP 错误状态码4xx 客户端错误(400, 401, 403, 404 等)5xx 服务器错误(500, 502, 503 等)4. 请求取消错误主动取消请求组件卸载时取消错误对象结构当请求失败时,Axios 会返回一个包含以下属性的错误对象:try { await axios.get('/api/data');} catch (error) { console.log(error.message); // 错误信息 console.log(error.response); // 服务器响应(如果有) console.log(error.request); // 请求对象 console.log(error.config); // 请求配置 console.log(error.code); // 错误代码}错误处理策略1. 基础错误处理axios.get('/api/data') .then(response => { console.log(response.data); }) .catch(error => { if (error.response) { // 服务器返回了错误状态码 console.log('Status:', error.response.status); console.log('Data:', error.response.data); } else if (error.request) { // 请求已发送但没有收到响应 console.log('No response received'); } else { // 请求配置出错 console.log('Error:', error.message); } });2. 使用 async/await 处理错误async function fetchData() { try { const response = await axios.get('/api/data'); return response.data; } catch (error) { handleAxiosError(error); }}function handleAxiosError(error) { if (error.response) { // 服务器响应错误 const { status, data } = error.response; switch (status) { case 400: throw new Error(`请求参数错误: ${data.message}`); case 401: // 清除登录状态并跳转 logout(); throw new Error('登录已过期,请重新登录'); case 403: throw new Error('没有权限执行此操作'); case 404: throw new Error('请求的资源不存在'); case 422: throw new Error(`数据验证失败: ${data.message}`); case 500: throw new Error('服务器内部错误,请稍后重试'); case 502: throw new Error('网关错误'); case 503: throw new Error('服务暂时不可用'); default: throw new Error(data.message || '请求失败'); } } else if (error.request) { // 网络错误 if (error.code === 'ECONNABORTED') { throw new Error('请求超时,请检查网络连接'); } if (error.code === 'ERR_NETWORK') { throw new Error('网络错误,请检查网络连接'); } throw new Error('无法连接到服务器'); } else { // 其他错误 throw new Error(error.message); }}3. 全局错误处理(通过拦截器)// 创建 axios 实例const instance = axios.create({ timeout: 10000});// 响应拦截器统一处理错误instance.interceptors.response.use( response => response, error => { // 统一错误处理 const errorMessage = getErrorMessage(error); // 显示错误提示 message.error(errorMessage); // 记录错误日志 logError(error); return Promise.reject(error); });function getErrorMessage(error) { if (error.response) { const { status, data } = error.response; const messageMap = { 400: '请求参数错误', 401: '登录已过期', 403: '没有权限', 404: '资源不存在', 500: '服务器错误', 502: '网关错误', 503: '服务不可用' }; return data.message || messageMap[status] || '请求失败'; } if (error.request) { return '网络连接失败,请检查网络'; } return error.message || '未知错误';}4. 超时错误处理const instance = axios.create({ timeout: 5000, // 5秒超时 timeoutErrorMessage: '请求超时,请稍后重试'});// 或者自定义超时处理instance.interceptors.response.use( response => response, error => { if (error.code === 'ECONNABORTED' && error.message.includes('timeout')) { // 超时错误特殊处理 return Promise.reject(new Error('请求响应时间过长,请稍后重试')); } return Promise.reject(error); });5. 重试机制axios.interceptors.response.use(null, async (error) => { const { config } = error; // 设置重试次数 if (!config || !config.retry) { return Promise.reject(error); } config.retryCount = config.retryCount || 0; if (config.retryCount >= config.retry) { return Promise.reject(error); } config.retryCount += 1; // 延迟重试 const delayRetry = new Promise(resolve => { setTimeout(resolve, config.retryDelay || 1000); }); await delayRetry; return axios(config);});// 使用axios.get('/api/data', { retry: 3, retryDelay: 2000});6. 请求取消错误处理const controller = new AbortController();try { const response = await axios.get('/api/data', { signal: controller.signal });} catch (error) { if (axios.isCancel(error)) { console.log('请求被取消:', error.message); } else { // 处理其他错误 console.error('请求失败:', error); }}// 取消请求controller.abort('用户取消操作');最佳实践分层处理:全局拦截器 + 业务层处理用户友好:错误信息要清晰易懂错误分类:区分可恢复和不可恢复错误日志记录:记录错误便于排查问题降级策略:网络错误时提供缓存数据或默认数据错误处理流程图请求失败 ↓检查 error.response ↓存在 → HTTP 错误 → 根据状态码处理 ↓不存在 → 检查 error.request ↓存在 → 网络错误 → 提示用户检查网络 ↓不存在 → 配置错误 → 检查代码
阅读 0·3月7日 12:10

axios 有哪些高级特性?如文件上传下载、进度监控、CSRF 防护等

Axios 高级特性概览Axios 不仅支持基本的 HTTP 请求,还提供了许多高级特性,包括文件上传下载、进度监控、CSRF 防护、请求转换等。1. 文件上传基础文件上传// HTML// <input type="file" id="fileInput" />const uploadFile = async (file) => { const formData = new FormData(); formData.append('file', file); try { const response = await axios.post('/api/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); return response.data; } catch (error) { console.error('上传失败:', error); throw error; }};// 使用const fileInput = document.getElementById('fileInput');fileInput.addEventListener('change', (e) => { const file = e.target.files[0]; uploadFile(file);});多文件上传const uploadMultipleFiles = async (files) => { const formData = new FormData(); files.forEach((file, index) => { formData.append(`file${index}`, file); }); // 或者使用相同字段名 // files.forEach(file => { // formData.append('files', file); // }); const response = await axios.post('/api/upload-multiple', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); return response.data;};带进度条的文件上传const uploadWithProgress = (file, onProgress) => { const formData = new FormData(); formData.append('file', file); return axios.post('/api/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (progressEvent) => { if (progressEvent.lengthComputable) { const percentCompleted = Math.round( (progressEvent.loaded * 100) / progressEvent.total ); onProgress(percentCompleted); } } });};// React 组件中使用function FileUpload() { const [progress, setProgress] = useState(0); const handleUpload = async (file) => { try { const result = await uploadWithProgress(file, setProgress); console.log('上传成功:', result); } catch (error) { console.error('上传失败:', error); } }; return ( <div> <input type="file" onChange={(e) => handleUpload(e.target.files[0])} /> <progress value={progress} max="100" /> <span>{progress}%</span> </div> );}2. 文件下载基础文件下载const downloadFile = async (url, filename) => { try { const response = await axios.get(url, { responseType: 'blob' // 重要:设置响应类型为 blob }); // 创建下载链接 const blob = new Blob([response.data]); const downloadUrl = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = downloadUrl; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(downloadUrl); } catch (error) { console.error('下载失败:', error); }};// 使用downloadFile('/api/download/report.pdf', 'report.pdf');带进度条的文件下载const downloadWithProgress = async (url, filename, onProgress) => { const response = await axios.get(url, { responseType: 'blob', onDownloadProgress: (progressEvent) => { if (progressEvent.lengthComputable) { const percentCompleted = Math.round( (progressEvent.loaded * 100) / progressEvent.total ); onProgress(percentCompleted); } } }); const blob = new Blob([response.data]); const downloadUrl = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = downloadUrl; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(downloadUrl);};3. CSRF 防护自动 CSRF Token 处理const instance = axios.create({ // 从 cookie 中读取 CSRF token 的字段名 xsrfCookieName: 'XSRF-TOKEN', // 请求头中发送 CSRF token 的字段名 xsrfHeaderName: 'X-XSRF-TOKEN', // 允许携带 cookie withCredentials: true});手动设置 CSRF Token// 从 meta 标签获取 CSRF tokenconst csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');const instance = axios.create({ headers: { 'X-CSRF-TOKEN': csrfToken }});// 或者通过拦截器动态设置instance.interceptors.request.use(config => { const token = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'); if (token) { config.headers['X-CSRF-TOKEN'] = token; } return config;});4. 请求和响应转换请求数据转换const instance = axios.create({ // 在请求发送到服务器之前修改请求数据 transformRequest: [ function (data, headers) { // 对 data 进行转换 if (data instanceof FormData) { return data; } // 添加时间戳防止缓存 if (data && typeof data === 'object') { data._timestamp = Date.now(); } return JSON.stringify(data); } ]});响应数据转换const instance = axios.create({ // 在传递给 then/catch 之前修改响应数据 transformResponse: [ function (data) { // 解析 JSON const parsed = JSON.parse(data); // 统一处理响应格式 if (parsed.code !== 0) { throw new Error(parsed.message); } return parsed.data; } ]});5. 参数序列化自定义参数序列化import qs from 'qs';const instance = axios.create({ // 自定义 params 序列化 paramsSerializer: { encode?: (param: string): string => encodeURIComponent(param), // 自定义编码函数 serialize?: (params: Record<string, any>, options?: ParamsSerializerOptions): string => { // 使用 qs 库进行序列化 return qs.stringify(params, { arrayFormat: 'brackets' }); }, indexes: false // 数组参数不使用索引 }});// 使用instance.get('/api/search', { params: { q: 'keyword', tags: ['javascript', 'axios'] }});// 结果: /api/search?q=keyword&tags[]=javascript&tags[]=axios6. 代理配置// Node.js 环境const instance = axios.create({ proxy: { protocol: 'https', host: '127.0.0.1', port: 9000, auth: { username: 'mikeymike', password: 'rapunz3l' } }});// 或者使用环境变量const instance = axios.create({ proxy: false // 禁用代理});7. 适配器自定义适配器const instance = axios.create({ adapter: (config) => { return new Promise((resolve, reject) => { // 自定义请求实现 const xhr = new XMLHttpRequest(); xhr.open(config.method.toUpperCase(), config.url); xhr.onload = () => { resolve({ data: xhr.response, status: xhr.status, statusText: xhr.statusText, headers: {}, config, request: xhr }); }; xhr.onerror = () => reject(new Error('Request failed')); xhr.send(config.data); }); }});8. 验证状态码const instance = axios.create({ // 自定义合法状态码 validateStatus: (status) => { return status >= 200 && status < 300; // 默认值 // 或者接受所有状态码 // return true; // 或者只接受特定状态码 // return [200, 201, 204].includes(status); }});9. 最大内容长度和重定向const instance = axios.create({ // 最大响应内容长度(字节) maxContentLength: 2000, // 最大请求内容长度(字节) maxBodyLength: 2000, // 最大重定向次数 maxRedirects: 5, // 在 Node.js 中遵循重定向 // 在浏览器中此配置无效(浏览器自动处理重定向)});10. 完整的高级配置示例import axios from 'axios';import qs from 'qs';const advancedApi = axios.create({ baseURL: 'https://api.example.com', timeout: 30000, // 请求头 headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, // 携带 cookie withCredentials: true, // CSRF 防护 xsrfCookieName: 'XSRF-TOKEN', xsrfHeaderName: 'X-XSRF-TOKEN', // 响应类型 responseType: 'json', // 参数序列化 paramsSerializer: (params) => { return qs.stringify(params, { arrayFormat: 'repeat' }); }, // 请求转换 transformRequest: [ (data, headers) => { // 添加认证信息 const token = localStorage.getItem('token'); if (token) { headers.Authorization = `Bearer ${token}`; } // 如果不是 FormData,转换为 JSON if (data && !(data instanceof FormData)) { return JSON.stringify(data); } return data; } ], // 响应转换 transformResponse: [ (data) => { // 统一错误处理 if (data && data.code !== 0) { throw new Error(data.message); } return data?.data || data; } ], // 状态码验证 validateStatus: (status) => { return status >= 200 && status < 500; }, // 最大内容长度 maxContentLength: 50 * 1024 * 1024, // 50MB // 最大重定向次数 maxRedirects: 5});// 添加进度监控拦截器advancedApi.interceptors.request.use(config => { if (config.onUploadProgress || config.onDownloadProgress) { console.log('请求包含进度监控'); } return config;});export default advancedApi;最佳实践文件上传:始终使用 FormData,设置正确的 Content-Type文件下载:设置 responseType 为 'blob' 或 'arraybuffer'CSRF 防护:正确配置 xsrfCookieName 和 xsrfHeaderName进度监控:在需要用户体验的场景中使用数据转换:统一处理请求和响应数据格式错误处理:在 transformResponse 中统一处理业务错误
阅读 0·3月7日 12:10

什么是 Babel AST?如何编写一个自定义的 Babel 插件来操作 AST?

什么是 AST?AST(Abstract Syntax Tree,抽象语法树)是源代码的树状表示形式,它将代码结构化为节点层次结构,每个节点代表代码中的一个构造(如变量声明、函数调用等)。Babel AST 规范Babel 使用基于 ESTree 规范的 AST,并扩展了 JSX、TypeScript 等语法支持。AST 节点类型常见节点类型| 节点类型 | 说明 | 示例 || --------------------- | ----- | ---------------------- || Program | 程序根节点 | 整个文件 || Identifier | 标识符 | 变量名、函数名 || Literal | 字面量 | 1, "hello", true || VariableDeclaration | 变量声明 | const, let, var || FunctionDeclaration | 函数声明 | function foo() {} || CallExpression | 函数调用 | foo() || BinaryExpression | 二元表达式 | a + b || MemberExpression | 成员表达式 | obj.prop |AST 示例// 源代码const sum = (a, b) => a + b;// AST(简化版){ "type": "VariableDeclaration", "kind": "const", "declarations": [{ "type": "VariableDeclarator", "id": { "type": "Identifier", "name": "sum" }, "init": { "type": "ArrowFunctionExpression", "params": [ { "type": "Identifier", "name": "a" }, { "type": "Identifier", "name": "b" } ], "body": { "type": "BinaryExpression", "operator": "+", "left": { "type": "Identifier", "name": "a" }, "right": { "type": "Identifier", "name": "b" } } } }]}编写自定义 Babel 插件1. 基础插件结构// my-plugin.jsmodule.exports = function(babel) { const { types: t } = babel; return { name: 'my-custom-plugin', visitor: { // 访问者方法 Identifier(path) { console.log('Found identifier:', path.node.name); } } };};2. 实用插件示例示例 1:替换 console.log// remove-console-plugin.jsmodule.exports = function(babel) { const { types: t } = babel; return { name: 'remove-console', visitor: { CallExpression(path) { const { callee } = path.node; // 检查是否是 console.log 调用 if ( t.isMemberExpression(callee) && t.isIdentifier(callee.object, { name: 'console' }) && t.isIdentifier(callee.property, { name: 'log' }) ) { // 移除该节点 path.remove(); } } } };};示例 2:自动添加函数名// add-function-name-plugin.jsmodule.exports = function(babel) { const { types: t } = babel; return { name: 'add-function-name', visitor: { FunctionDeclaration(path) { const { node } = path; // 如果函数没有名称,添加一个默认名称 if (!node.id) { node.id = t.identifier('anonymous'); } } } };};示例 3:国际化字符串提取// i18n-plugin.jsmodule.exports = function(babel) { const { types: t } = babel; const strings = []; return { name: 'i18n-extractor', visitor: { StringLiteral(path) { const { node } = path; // 收集所有字符串 strings.push(node.value); // 替换为国际化函数调用 path.replaceWith( t.callExpression( t.identifier('t'), [t.stringLiteral(node.value)] ) ); } }, post(state) { // 输出收集到的字符串 console.log('Extracted strings:', strings); } };};3. 使用插件// babel.config.jsmodule.exports = { plugins: [ './remove-console-plugin.js', ['./i18n-plugin.js', { /* 插件选项 */ }] ]};Path 对象详解Path 的核心方法// 访问者中的 path 对象visitor: { Identifier(path) { // 节点信息 console.log(path.node); // AST 节点 console.log(path.parent); // 父节点 console.log(path.parentPath); // 父路径 // 节点操作 path.remove(); // 删除节点 path.replaceWith(newNode); // 替换节点 path.insertBefore(newNode); // 在前面插入 path.insertAfter(newNode); // 在后面插入 // 遍历 path.traverse({ ... }); // 子树遍历 path.skip(); // 跳过子树 path.stop(); // 停止遍历 // 检查 path.isIdentifier(); // 检查节点类型 path.findParent((p) => ...); // 查找父节点 path.getFunctionParent(); // 获取函数父节点 path.getStatementParent(); // 获取语句父节点 // 作用域 path.scope.hasBinding('name'); // 检查绑定 path.scope.rename('old', 'new'); // 重命名 path.scope.generateUid('name'); // 生成唯一标识符 }}高级插件技巧1. 状态管理module.exports = function(babel) { return { name: 'stateful-plugin', pre(state) { // 遍历前初始化状态 this.counter = 0; }, visitor: { Identifier(path) { this.counter++; } }, post(state) { // 遍历后输出结果 console.log(`Found ${this.counter} identifiers`); } };};2. 处理 JSX// 将 <div>Hello</div> 转换为 h('div', null, 'Hello')module.exports = function(babel) { const { types: t } = babel; return { name: 'jsx-transform', visitor: { JSXElement(path) { const { openingElement, children } = path.node; const tagName = openingElement.name.name; // 创建 h() 调用 const callExpr = t.callExpression( t.identifier('h'), [ t.stringLiteral(tagName), t.nullLiteral(), ...children.map(child => { if (t.isJSXText(child)) { return t.stringLiteral(child.value.trim()); } return child; }) ] ); path.replaceWith(callExpr); } } };};3. 源码映射支持module.exports = function(babel) { return { name: 'sourcemap-plugin', visitor: { Identifier(path) { // 保留原始位置信息 path.addComment('leading', ` Original: ${path.node.name} `); } } };};调试技巧// 查看 ASTconst parser = require('@babel/parser');const code = 'const a = 1';const ast = parser.parse(code);console.log(JSON.stringify(ast, null, 2));// 使用 @babel/template 简化节点创建const template = require('@babel/template').default;const buildRequire = template(` var IMPORT_NAME = require(SOURCE);`);const ast2 = buildRequire({ IMPORT_NAME: t.identifier('myModule'), SOURCE: t.stringLiteral('my-module')});最佳实践使用 path 而非直接操作 node - Path 提供更多上下文信息善用 path.scope - 正确处理变量作用域使用 @babel/template - 简化复杂 AST 节点的创建测试插件 - 使用 @babel/core 的 transformSync 进行单元测试参考官方插件 - 学习 Babel 官方插件的实现方式
阅读 0·3月7日 12:10

Solidity 中的 view、pure 和 payable 函数修饰符有什么区别?

在 Solidity 中,view、pure 和 payable 是三种重要的函数修饰符,它们定义了函数的行为特性和限制条件。1. View 修饰符定义:声明函数不会修改合约的状态变量,但可以读取状态。特点:可以读取状态变量(storage)不能修改状态变量不能发送 ETH不消耗 Gas(当被外部调用时)contract ViewExample { uint256 public storedData = 100; // view 函数可以读取状态 function getData() public view returns (uint256) { return storedData; // 读取状态变量 } // 错误:view 函数不能修改状态 function setData(uint256 _data) public view { // storedData = _data; // 编译错误! }}2. Pure 修饰符定义:声明函数既不读取也不修改合约状态,仅依赖于输入参数。特点:不能读取状态变量不能修改状态变量不能访问 msg.sender、msg.value 等全局变量不消耗 Gas(当被外部调用时)contract PureExample { uint256 public constant VALUE = 100; // pure 函数只依赖输入参数 function add(uint256 a, uint256 b) public pure returns (uint256) { return a + b; } // pure 函数可以读取常量 function getConstant() public pure returns (uint256) { return VALUE; // 常量不算状态读取 } // 错误:pure 函数不能读取状态变量 function getData() public pure returns (uint256) { // return storedData; // 编译错误! }}3. Payable 修饰符定义:允许函数接收 ETH(以太币)。特点:可以接收 ETH 转账可以读取和修改状态(默认行为)可以访问 msg.value 获取转账金额消耗 Gascontract PayableExample { mapping(address => uint256) public balances; // payable 函数可以接收 ETH function deposit() public payable { require(msg.value > 0, "Must send ETH"); balances[msg.sender] += msg.value; } // 查询合约余额 function getBalance() public view returns (uint256) { return address(this).balance; } // 提取 ETH function withdraw(uint256 amount) public { require(balances[msg.sender] >= amount, "Insufficient balance"); balances[msg.sender] -= amount; payable(msg.sender).transfer(amount); }}修饰符对比表| 特性 | view | pure | payable || ------------ | ---- | ---- | ------- || 读取状态 | ✅ | ❌ | ✅ || 修改状态 | ❌ | ❌ | ✅ || 接收 ETH | ❌ | ❌ | ✅ || 访问 msg.value | ❌ | ❌ | ✅ || Gas 消耗(外部调用) | 无 | 无 | 有 || 适用场景 | 查询数据 | 纯计算 | 资金操作 |组合使用某些修饰符可以组合使用:contract CombinedExample { // payable + view 不能组合,因为 view 不消耗 Gas,而 payable 需要处理转账 // 可以定义接收 ETH 的函数 receive() external payable {} fallback() external payable {} // 计算函数使用 pure function calculateFee(uint256 amount) public pure returns (uint256) { return amount * 5 / 100; // 5% 手续费 } // 查询函数使用 view function getContractBalance() public view returns (uint256) { return address(this).balance; }}实际应用示例contract Bank { mapping(address => uint256) private balances; uint256 public totalDeposits; // payable:接收存款 function deposit() public payable { balances[msg.sender] += msg.value; totalDeposits += msg.value; } // view:查询余额 function getBalance(address user) public view returns (uint256) { return balances[user]; } // pure:计算利息 function calculateInterest(uint256 principal, uint256 rate) public pure returns (uint256) { return principal * rate / 100; } // payable + 其他操作 function withdraw() public payable { uint256 amount = balances[msg.sender]; require(amount > 0, "No balance"); balances[msg.sender] = 0; totalDeposits -= amount; payable(msg.sender).transfer(amount); }}最佳实践优先使用 pure:如果函数不需要读取状态,使用 pure 可以节省 Gas明确使用 view:对于只读操作,明确标记 view 提高代码可读性谨慎使用 payable:确保 payable 函数有适当的访问控制和金额验证避免滥用:不要为了 Gas 优化而错误地使用这些修饰符,可能导致安全问题
阅读 0·3月7日 12:09