新闻详情

新闻详情

首页 / 资讯中心 / 详情

前端Ajax封装实战:从XMLHttpRequest底层原理到完整实现

发布时间:2026/9/30 10:33:29来源:尧图网络
前端Ajax封装实战:从XMLHttpRequest底层原理到完整实现
先交代一个背景前端项目里用得最多的网络请求工具要么是 axios要么是 jQuery 的$.ajax。但如果你做过纯原生开发、写过小程序插件、或者被公司安全策略限制不能随意引第三方库你就会发现手头那套 XMLHttpRequest 代码总在重复劳动——每个页面都要重新处理 URL 参数拼接、超时、错误码、loading 状态。这篇就把我自己的 Ajax 封装思路完整拆出来从底层原理到可落地的完整代码再到实践中踩过的编码、缓存、取消请求这些坑一次讲透。1. 为什么还要自己封装一套 Ajax而不是直接引 axios1.1 第三方库解决不了的那部分场景很多人一听到封装 Ajax第一反应是axios 不是已经很好用了吗确实axios 在浏览器和 Node 端都表现稳定拦截器、取消请求、转换数据这些功能开箱即用。但实际开发里我遇到过几种必须自己动手的情况项目对依赖体积有严格要求一个工具库也不让引要求纯原生 JS 实现。公司内网环境没法访问 CDNnpm 私服上又没有维护 axios。需要深度定制请求行为比如统一加密、统一签名、统一埋点axios 的拦截器虽然能做但包一层自己的封装反而更顺手。旧项目维护里面全是原生 XHR 代码想逐步替换成统一封装的请求层直接换 axios 成本太高。更重要的一个理由是XMLHttpRequest 本身并不复杂但它的细节很碎。状态码判断、请求头设置、参数序列化、超时处理、错误分类这些逻辑如果不做收敛就会散落在各个业务代码里。时间一长有的人用onreadystatechange判断状态有的人用onload有的人把错误信息直接弹给用户混乱程度可想而知。1.2 封装的核心价值在于统一约定我理解的封装不是简单包一层函数就算完事而是把整个请求生命周期里的不确定性收敛到一处。具体来说一套合格的前端请求封装要解决这四类问题统一的调用方式不管 GET、POST还是带文件上传调用方只需要关注url、data、config。统一的错误处理网络错误、HTTP 错误码、业务错误码、超时、取消这些都要能区分开不能混在一起。统一的预处理链路请求发出去之前要加 token、加时间戳签名响应回来之后要解包、要判断业务码这些逻辑不能散落在业务组件里。统一的取消与超时管理页面切换、组件卸载时该中断的请求要能中断避免回调里操作已销毁的 DOM 导致报错。明白了这四件事封装方案的设计就有方向了。下面先从原生 XHR 的底层细节说起这些是封装的基石。2. 动手前必须吃透的 XMLHttpRequest 底层行为2.1 请求状态与 readyState 的完整流转XMLHttpRequest 对象有一个readyState属性从 0 到 4一共五个状态readyState含义触发时机0UNSENTXHR 实例已创建open()尚未调用1OPENEDopen()已调用send()尚未调用2HEADERS_RECEIVEDsend()已调用响应头和响应状态已可获取3LOADING响应体下载中responseText已包含部分数据4DONE整个请求流程完成无论成功还是失败这里有一个新手很容易搞混的点readyState 4只代表请求结束了不代表请求成功了。HTTP 404、500 这些错误状态同样会走到 4。真正的成功判断要看两件事HTTP 状态码在 2xx 范围以及业务层面的响应内容符合预期。所以封装的时候我通常这样区分事件const xhr new XMLHttpRequest(); xhr.open(GET, /api/user, true); xhr.onload function () { if (xhr.status 200 xhr.status 300) { // 走到这里说明 HTTP 层面成功了 } else { // HTTP 错误比如 404、500 } }; xhr.onerror function () { // 网络异常比如断网、跨域被拦截 }; xhr.ontimeout function () { // 超时 }; xhr.onabort function () { // 请求被取消 };onload和onreadystatechange的区别在于onreadystatechange每次状态变化都会触发而onload只在readyState 4时触发一次。封装内部我倾向于用onload配合status判断代码更直观。2.2 responseType 与 responseText 的关系XHR 的responseType属性决定了返回数据的解析方式常见的取值有默认字符串、json、text、blob、arraybuffer、document。这里有个很重要但容易忽略的细节设置responseType的时机必须在open()之后、send()之前。如果放在别的位置某些浏览器会直接报错或者默默忽略。另外一个细节是当responseType设置为json时如果服务端返回的不是合法 JSONresponse会是null并且控制台可能报解析错误。为了更可控我习惯把responseType和手动JSON.parse结合使用——设置responseType为text然后自己在封装层做 JSON 解析出错时抛自定义错误。这样报错信息更友好也方便统一处理返回内容不是 JSON的情况。2.3 请求头设置的两个天然限制通过setRequestHeader可以自定义请求头但不是所有头都能随便设置。浏览器基于安全考虑对以下两类做了限制受保护的首部字段比如Cookie、Host、Content-Length等由浏览器接管脚本无法修改。跨域场景的自定义头限制跨域请求时如果设置了非简单请求头比如自定义的X-Token必须先发一个OPTIONS预检请求服务端返回对应的Access-Control-Allow-Headers才能放行。这个限制直接影响参数加密场景。之前有个项目里直接在拦截器里加了X-Signature自定义头本地联调没问题一上到测试环境就发现所有请求都变成了两次——多出来一次OPTIONS预检而且预检失败导致真实请求根本没发出去。所以封装内部要支持配置项来关闭某些请求头或者至少让调用方知道跨域场景下加自定义头必须让后端在网关层放行预检。3. 封装方案的整体设计接口约定与模块边界3.1 我先定义了这五个配置项封装之前先明确调用方需要暴露给业务侧的能力。我设计的核心 API 长这样// 基础 GET 请求 ajax.get(/api/user, { params: { id: 123 } }) .then(res console.log(res)) .catch(err console.error(err)); // 基础 POST 请求 ajax.post(/api/user/save, { name: 张三, age: 18 }); // 带完整配置的请求 ajax.request({ url: /api/upload, method: POST, data: formData, timeout: 10000, headers: { Content-Type: multipart/form-data }, withCredentials: true, responseType: json, onUploadProgress: e console.log(e.loaded / e.total) });对应的配置项我收敛成一份默认配置const DEFAULTS { method: GET, baseURL: , url: , params: null, // URL 查询参数对象形式 data: null, // 请求体 headers: {}, // 请求头 timeout: 10000, // 超时时间默认 10 秒 withCredentials: false, responseType: json, // 内部使用的钩子 onUploadProgress: null, signal: null };3.2 以实例为单元的请求器而不是纯函数封装形态上我选择用class实现而不是简单的函数。原因有两个可以维护实例级别的拦截器数组比如interceptors.request和interceptors.response。后续扩展时可以基于同一个类派生多个不同配置的实例比如一个带 token 的实例、一个不带 token 的实例。核心骨架如下class Ax { constructor(config {}) { this.defaults Object.assign({}, DEFAULTS, config); this.interceptors { request: [], response: [] }; } // 注册请求拦截器 useRequestInterceptor(fulfilled, rejected) { this.interceptors.request.push({ fulfilled, rejected }); } // 注册响应拦截器 useResponseInterceptor(fulfilled, rejected) { this.interceptors.response.push({ fulfilled, rejected }); } // 核心请求方法 request(config {}) {} get(url, config {}) { return this.request(Object.assign({}, config, { url, method: GET })); } post(url, data {}, config {}) { return this.request(Object.assign({}, config, { url, method: POST, data })); } }这样设计的好处是调用方可以const http new Ax({ baseURL: /api })创建一个全局实例也可以为不同模块创建不同实例互不干扰。4. 核心实现从参数序列化到拦截器链路的完整代码4.1 先把 URL 参数序列化写好GET 请求的参数要拼在 URL 查询字符串上POST 的 JSON 数据要序列化成字符串。这里最容易出问题的是嵌套对象和数组的处理。function isObject(value) { return value ! null typeof value object; } function isPlainObject(value) { if (!isObject(value)) return false; const proto Object.getPrototypeOf(value); return proto Object.prototype || proto null; } function serializeParams(params) { if (!params) return ; const parts []; Object.keys(params).forEach(key { const value params[key]; if (value null || value undefined) { parts.push(${encodeURIComponent(key)}); } else if (Array.isArray(value)) { value.forEach(item { parts.push(${encodeURIComponent(key)}[]${encodeURIComponent(item)}); }); } else if (isPlainObject(value)) { parts.push(${encodeURIComponent(key)}${encodeURIComponent(JSON.stringify(value))}); } else { parts.push(${encodeURIComponent(key)}${encodeURIComponent(value)}); } }); return parts.join(); }序列化时用encodeURIComponent对 key 和 value 编码能避免中文乱码和特殊字符冲突。对于数组我采用key[]value1key[]value2这种形式后端Spring MVC、Express都支持这种格式。4.2 请求头里的 Content-Type 与编码格式热搜词里有人专门搜ajax请求设置编码格式这确实是个高频坑。原生 XHR 发送 POST 请求时如果data是普通对象需要手动序列化而服务端最终能不能正确解析取决于你设置的Content-Typeapplication/x-www-form-urlencoded表单格式需要把对象转成keyvaluekey2value2。application/jsonJSON 格式需要JSON.stringify。multipart/form-data文件上传格式需要传FormData对象而且不能手动设置 Content-Type要让浏览器自动带上boundary分隔符。我在封装里这样处理function buildBody(data, headers) { if (!data) return null; let contentType headers[Content-Type] || headers[content-type] || ; if (data instanceof FormData) { // FormData 不手动设 Content-Type浏览器自己带 boundary delete headers[Content-Type]; return data; } if (typeof data string) { return data; } if (typeof data object) { // 如果调用方没有显式指定 Content-Type默认当 JSON 处理 if (!contentType) { headers[Content-Type] application/json;charsetUTF-8; return JSON.stringify(data); } if (contentType.includes(application/x-www-form-urlencoded)) { return serializeParams(data); } if (contentType.includes(application/json)) { return JSON.stringify(data); } } return data; }乱码问题的根源在于 JS 字符串的 UTF-8 编码和服务端解码方式不一致。在Content-Type里显式声明charsetUTF-8能规避绝大多数中文乱码问题。还有一点容易被忽略encodeURIComponent默认按 UTF-8 编码这也是为什么 GET 请求中文参数在绝大多数后端框架里不用额外处理的原因。4.3 核心请求流程拦截器 Promise拦截器机制说穿了就是在发出请求前执行一组函数在拿到响应后执行另一组函数。但要注意如果直接在request()里同步调用拦截器那么业务侧注册的拦截器可能在请求发出之后才生效。所以要先把拦截器链路从 Promise 链里走一遍再做真正的 XHR 请求。request(config {}) { const merged Object.assign({}, this.defaults, config); const xhr new XMLHttpRequest(); const { method, url, params, data, headers, timeout, withCredentials, responseType } merged; // 请求拦截器链 let promise Promise.resolve(merged); this.interceptors.request.forEach(interceptor { promise promise.then(interceptor.fulfilled, interceptor.rejected); }); return promise.then(finalConfig { const queryString serializeParams(finalConfig.params); const fullUrl finalConfig.baseURL finalConfig.url (queryString ? ?${queryString} : ); const requestBody buildBody(finalConfig.data, finalConfig.headers); xhr.open(finalConfig.method.toUpperCase(), fullUrl, true); xhr.timeout finalConfig.timeout; xhr.withCredentials finalConfig.withCredentials; xhr.responseType finalConfig.responseType json ? text : finalConfig.responseType; Object.keys(finalConfig.headers).forEach(key { xhr.setRequestHeader(key, finalConfig.headers[key]); }); if (finalConfig.signal) { finalConfig.signal.addEventListener(abort, () xhr.abort()); } return new Promise((resolve, reject) { xhr.onload function() { if (xhr.status 200 xhr.status 300) { let response xhr.responseText; if (finalConfig.responseType json) { try { response JSON.parse(response); } catch (e) { reject(new Error(响应数据不是合法 JSON: ${e.message})); return; } } resolve(response); } else { const error new Error(HTTP 错误状态码 ${xhr.status} ${xhr.statusText}); error.code HTTP_${xhr.status}; error.status xhr.status; reject(error); } }; xhr.onerror function() { const error new Error(网络异常请检查网络连接); error.code NETWORK_ERROR; reject(error); }; xhr.ontimeout function() { const error new Error(请求超时${xhr.timeout}ms); error.code TIMEOUT; reject(error); }; xhr.onabort function() { const error new Error(请求已取消); error.code ABORT_ERROR; reject(error); }; xhr.send(requestBody); }).then( // 响应拦截器链 response { let result Promise.resolve(response); this.interceptors.response.forEach(interceptor { result result.then(interceptor.fulfilled, interceptor.rejected); }); return result; } ); }); }这里我做了两处有意的设计responseType为json时底层用text然后手动JSON.parse。这样 JSON 解析失败时我能捕获并抛出带明确信息的错误而不是让控制台报一个难以定位的 SyntaxError。响应拦截器放在onload之后的.then()链里保证 HTTP 层错误不进入响应拦截器的fulfilled。如果业务想在拦截器里统一处理 HTTP 错误码可以注册rejected函数。4.4 业务错误码的封装约定市面上后端接口有一种常见结构HTTP 200但 body 里带业务错误码比如{ code: 40101, message: 未登录 }。这种包装我习惯放在响应拦截器里做判断而不是在每个业务页面里重复处理。http.useResponseInterceptor(response { if (response response.code ! undefined) { if (response.code 0 || response.code 200) { return response.data; // 直接返回业务数据 } const error new Error(response.message || 业务处理失败); error.code response.code; error.bizCode response.code; return Promise.reject(error); } return response; });这样做的好处是业务侧调用ajax.post(/api/login, data)时.then()里拿到的直接就是data字段不需要每处都写res.data.data.name少了很多模板代码。5. 超时、取消与重试容易被业务侧忽略的高级能力5.1 超时到底由什么决定XHR 的timeout属性是浏览器层面的超时时间单位毫秒。注意它的计时起点是send()调用之后不是open()之后。而且超时只代表请求过程超过了时限不代表服务端没处理完。某些场景下服务端还在跑但前端已经报超时了。这个特性在封装里要书面写清楚避免业务侧把前端报超时等同于后端没处理。超时时间的设置我一般遵循一个经验值请求类型建议超时时间原因普通查询10s大多数接口 1s 内返回文件上传120s大文件耗时不确定批量导出180s后端生成文件耗时较长封装里可以提供timeout配置让不同请求覆盖默认值。5.2 用 AbortController 统一取消请求组件卸载、用户取消筛选、重复点击提交按钮等等都涉及取消请求。原生 XHR 有xhr.abort()方法但直接暴露给业务侧不好看。更优雅的方式是用AbortController这是浏览器原生支持的取消信号机制。// 业务侧用法 const controller new AbortController(); const promise ajax.get(/api/list, { signal: controller.signal, params: { page: 1 } }); // 用户点了取消或者组件卸载时 controller.abort();封装内部已经通过signal.addEventListener(abort, () xhr.abort())把AbortController的信号和xhr.abort()关联起来了所以在业务侧不需要直接碰 XHR 对象。一个重要提醒如果组件已经卸载但请求还在进行回调里的setState会触发一个在卸载组件上更新状态的警告甚至导致内存泄漏。推荐的做法是在组件的卸载生命周期里把控制器保存下来并主动abort()。有了统一的signal支持这套逻辑可以被封装到业务框架的自定义 Hook 里非常顺手。5.3 每次失败的请求都值得自动重试吗自动重试不是银弹我用下来比较实用的判据是网络错误NETWORK_ERROR可以重试网络抖动很常见。超时TIMEOUT视接口而定查询类接口可以重试写操作不要盲目重试。HTTP 4xx 不重试这是参数或权限问题。HTTP 5xx 可以重试但控制次数。重试一定要有限制并且要加退避延迟否则接口出问题时浏览器会瞬间发一堆请求把服务端打挂。我一般用Promise加定时器实现简单的指数退避function retryRequest(fn, times 2, delay 300) { return fn().catch(err { if (times 0) return Promise.reject(err); if (!isRetryable(err)) return Promise.reject(err); return new Promise(resolve setTimeout(resolve, delay)) .then(() retryRequest(fn, times - 1, delay * 2)); }); } function isRetryable(err) { return err.code NETWORK_ERROR || (err.status err.status 500) || err.code TIMEOUT; }把retry配置加进request()里调用方可以直接ajax.get(url, { retry: 2 })实现很简洁。6. 踩过的坑缓存、乱码与跨域预检的真实排查过程6.1 GET 请求的浏览器缓存问题排查过一个问题后台改了配置前端刷新页面但数据一直不变。打开 DevTools Network 面板才发现请求根本没到服务端走的是浏览器磁盘缓存。原因很简单GET 请求的 URL 没变化浏览器按缓存策略直接返回了旧内容。解决方法有两个层面。服务端层面可以设置Cache-Control: no-cache。前端层面的通用做法是给 GET 请求追加一个不重复的查询参数比如_tDate.now()。但要小心如果项目里做了接口签名拼接随机参数会导致签名失效。所以我封装的方案是提供一个noCache配置默认关只有调用方明确需要时才会追加时间戳参数。if (finalConfig.noCache method.toUpperCase() GET) { finalConfig.params Object.assign({}, finalConfig.params, { _t: Date.now() }); }6.2 中文参数编码与后端解码不一致有一次联调前端传的中文关键词后端一直收不到正确值。排查过程是这样的先看 Network 里请求 URL中文参数已经被浏览器编码成%E4%B8%AD%E6%96%87这种形式说明前端编码没错。然后看后端日志发现 Tomcat 默认用 ISO-8859-1 解码 URL导致中文全部变成乱码。这个问题的根节点在后端配置不在前端。但前端可以做的努力是使用encodeURIComponent进行编码。因为浏览器地址栏里如果直接放中文字符理论上也能发出去但因为历史上不同浏览器和服务器的解码习惯不一致会产生各种诡异问题。统一编码之后后端只要按 UTF-8 解码基本都能对上。6.3 跨域请求带上自定义头之后多出来的 OPTIONS要给加签接口传自定义头X-Signature结果请求变成两次第二次OPTIONS预检没通过真实请求根本没发出去。排查链路如下第一次接触时觉得奇怪为什么我明明只发了一个 POST浏览器发了两个查 Network 发现第一个是OPTIONSResponse Headers 里没有Access-Control-Allow-Headers字段。确认是后端网关没配置放行X-Signature。后端在跨域配置里加上了Access-Control-Allow-Headers: X-Signature, Content-Type问题解决。这个坑在本地联调时不容易暴露因为本地开发常用 Vite/Webpack 代理转发属于同源请求没有预检。一上到跨域环境就立刻现出原形。所以在封装文档里我特意标明跨域环境下使用自定义请求头务必先确认后端放行了对应 Header。7. 进阶对比自研封装与 fetch、axios 的取舍7.1 fetch 看起来很美但用起来有几个痛点fetch是基于 Promise 的现代替代方案但它并不完美默认不带 Cookie需要手动设置credentials: include否则登录态丢失。接收 HTTP 4xx、5xx 响应时fetch不会 rejectresponse.ok为 false 依然进入 resolve。很多人第一次用 fetch 都栽在这个坑上。取消请求需要配合AbortController实现起来比 XHR 复杂。没有原生的上传进度事件只能自己用 ReadableStream 处理。自研封装如果基于 fetch 做需要额外做很多补丁工作不如 XHR 成熟。所以我的封装选择底层用 XHR这是权衡后的决定。7.2 选型对照表能力自研 XHR 封装axiosfetchPromise 支持实现简单原生支持原生支持取消请求结合 AbortController 实现原生支持需手写 AbortController上传进度xhr.upload.onprogress原生支持不支持需自己封装流拦截器自己实现内置无HTTP 错误处理封装层统一自动 reject不会 reject需手动依赖体积几乎为零约 30KB浏览器内置学习成本低低低自研封装最明显的短板是生态兼容性axios 有大量官方适配器、社区插件和类型定义小型项目直接引 axios 是更经济的选择。但如果公司政策限制、项目体积敏感、或者你有能力维护XHR 封装并不输给 axios。7.3 什么时候我推荐直接上 axios如果你判断自己团队没有意愿也没有精力长期维护一套请求库那我建议直接用 axios把精力花在业务上。封装的价值在于深度的定制能力如果一个项目只是简单 GET/POST任何方案都可以选最省事的那个。反过来如果有这些需求自研封装更合适加密和签名策略经常变每个版本都要发布到多个业务线。需要兼容古老浏览器环境比如 IE11 以下的 XHR 限流策略。需要跟后端网关深度配合比如定制错误码映射、灰度上报逻辑。8. 我这里实际用了大半年之后的几点体会封装这套代码前后迭代了三个版本第一个版本只有简单的 GET/POST第二个版本加了拦截器和错误分类第三个版本补齐了超时、取消、重试和编码细节。如果让我重新来一次我会在一开始就考虑这些事设计公共配置时不要贪多先把高频场景覆盖住再慢慢加。一开始就想着把上传、下载、断点续传全做进去只会让自己陷入细节的泥潭。错误信息一定要带code字段。业务侧要根据错误码做不同提示没有统一错误码的设计后面排查问题会很痛苦。每个新功能都要写最小示例。封装这类基础层代码最怕实现方式只可意会不可言传。把最小可运行的示例放在 README 里团队其他人接手成本会低很多。最后分享一个写基础组件时很有用的习惯封装对外 API 时尽量让默认行为安全。比如默认超时、默认不携带 Cookie、默认 GET 请求不缓存——这些默认值都是在出现几次线上问题之后才调整成现在的样子。如果你也准备自己写一套务必要有人踩过这些坑否则很多隐患都埋在里面。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

deep-learning-for-image-processing 文献导航:图像分类、目标检测与分割经典论文系统研读指南 2026/9/30 11:00:14

deep-learning-for-image-processing 文献导航:图像分类、目标检测与分割经典论文系统研读指南

示例工程 【免费下载链接】deep-learning-for-image-processing deep learning for image processing including classification and object-detection etc. 项目地址: https://gitcode.com/gh_mirrors/de/deep-learning-for-image-processing 点击查看 免费下载 导…

阅读更多 →
智诺方AI|论文讨论章节怎么优化?兼顾原创表达与低重复率 2026/9/30 11:00:04

智诺方AI|论文讨论章节怎么优化?兼顾原创表达与低重复率

智诺方AI|论文讨论章节怎么优化?兼顾原创表达与低重复率,智诺方ai官网www.znfai.cn 微信公众号搜一搜 智诺方ai 论文的讨论章节,是整篇论文的精华所在。区别于结果部分只陈列数据,讨论章节要求你解读结果、对比前人研究…

阅读更多 →
MiniPCIe 无线模块选型实践:射频指标、驱动适配与载板兼容 2026/9/30 11:00:04

MiniPCIe 无线模块选型实践:射频指标、驱动适配与载板兼容

给嵌入式整机配无线模块,参数表只能回答一半问题,另一半在驱动、载板和校准口径里。本文以一块双频 22 802.11ac MiniPCIe 模块(型号 WLE600VX,高通 QCA9882 平台)为参考,整理 MiniPCIe 无线模块选型与集成…

阅读更多 →
N_m3u8DL-RE 快速上手指南:8 条命令从装好到出片,覆盖 m3u8/DASH 下载、选轨、解密、直播录制 2026/9/30 11:00:04

N_m3u8DL-RE 快速上手指南:8 条命令从装好到出片,覆盖 m3u8/DASH 下载、选轨、解密、直播录制

N_m3u8DL-RE 快速上手指南:8 条命令从装好到出片,覆盖 m3u8/DASH 下载、选轨、解密、直播录制 【免费下载链接】N_m3u8DL-RE Cross-Platform, modern and powerful stream downloader for MPD/M3U8/ISM. English/简体中文/繁體中文. 项目地址: https:…

阅读更多 →
如何用 Whisper Diarization 三步从一段录音中拿到带说话人标签的转录稿 2026/9/30 11:00:04

如何用 Whisper Diarization 三步从一段录音中拿到带说话人标签的转录稿

如何用 Whisper Diarization 三步从一段录音中拿到带说话人标签的转录稿 【免费下载链接】whisper-diarization Automatic Speech Recognition with Speaker Diarization based on OpenAI Whisper 项目地址: https://gitcode.com/GitHub_Trending/wh/whisper-diarization …

阅读更多 →
MySQL 5.7主从同步功能 2026/9/30 10:59:57

MySQL 5.7主从同步功能

目录 一、环境准备 二、主库配置 1. 修改配置文件 2. 创建复制用户 3. 查看主库状态 三、从库配置 1. 修改配置文件 2. 配置主从同步 四、验证同步状态 1. 查看从库状态 2. 测试数据同步 五、MySQL主从同步与Canal、Otter在功能上的区别 六、创建mysql用户并赋权&…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉