🔥FormData+Ajax组合拳,居然现在还用这种原始方式?💥

1. FormData 和原生 AJAX 有什么区别?

graph TD A[HTML Form] -->|submit / serialize| B[FormData] B -->|append| C[Binary/Text/File] B -.->|send| D[原生AJAX] D -->|XMLHttpRequest| B style A fill:#ffe6cc,stroke:#ff9933,stroke-width:2px style B fill:#e6f7ff,stroke:#1890ff,stroke-width:2px style C fill:#f9f0ff,stroke:#722ed1,stroke-width:1px style D fill:#d9f7be,stroke:#52c41a,stroke-width:2px

FormData 是数据容器,AJAX 是传输通道,它们是"子弹"和"枪"的关系。

✅ FormData 职责:

  • 构建 multipart/form-data 格式数据
  • 自动处理文件上传、二进制流
  • 支持 append(key, value, filename) 添加字段
js 复制代码
const fd = new FormData();
fd.append('name', '张三');
fd.append('avatar', fileInput.files[0]); // 文件自动处理

✅ 原生 AJAX(XHR)职责:

  • 创建请求:new XMLHttpRequest()
  • 发送数据:xhr.send(fd)
  • 监听状态:xhr.onreadystatechange
js 复制代码
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/submit');
xhr.onload = () => console.log(xhr.response);
xhr.send(fd); // 发送 FormData

Q1: 能直接 send({name: '张三'}) 吗?

A1: 不能!必须 JSON.stringify + 设置 Content-Type: application/json,否则后端收不到 ❌

Q2: 为什么上传文件必须用 FormData?

A2: 普通 key=value 编码无法处理二进制流,multipart/form-data 才能分段传输文件 💡

👉继续看?Q3: jQuery 的 serialize() 和 FormData 冲突吗?

A3: 冲突!serialize() 输出 a=1&b=2,而 FormData 是二进制格式,混用会导致后端解析失败 🤯


2. 表单提交方式 & FormData 的角色

✅ 传统同步提交(刷新页面)

html 复制代码
<form action="/submit" method="post">
  <input name="name" />
  <button type="submit">提交</button>
</form>

浏览器自动构建请求,但整页刷新,用户体验差。

✅ 异步提交(无刷新)→ FormData 登场

js 复制代码
// 阻止默认提交
form.addEventListener('submit', e => {
  e.preventDefault();
  const fd = new FormData(form); // 直接从 form 构建!
  ajaxSubmit(fd);
});

new FormData(form) 可自动提取所有带 name 属性的表单控件

Q4: 如何获取 checkbox 多选值?

A4: 多个同名 name="hobby" 的 checkbox,FormData 会自动 append 多次,后端用数组接收 ✅


8. 异步方案演进史(回调 → Promise → async/await)

graph LR Callback[回调函数] --> Promise[Promise] Promise --> Async[async/await]

阶段一:回调地狱

js 复制代码
ajax('/step1', () => {
  ajax('/step2', () => {
    ajax('/step3', () => {
      console.log('完成');
    });
  });
});

阶段二:Promise 扁平化

js 复制代码
ajax('/step1')
  .then(() => ajax('/step2'))
  .then(() => ajax('/step3'))
  .then(() => console.log('完成'));

阶段三:async/await 伪同步

js 复制代码
async function run() {
  await ajax('/step1');
  await ajax('/step2');
  await ajax('/step3');
  console.log('完成');
}

Q5: Promise 如何解决回调地狱?

A5: 通过 .then 链式调用,将嵌套转为线性结构,错误统一捕获 💥


9. Promise 如何实现 .then 处理?

手写简化版 Promise:

js 复制代码
class MyPromise {
  constructor(executor) {
    this.state = 'pending';
    this.value = null;
    this.callbacks = []; // 存储 then 回调

    const resolve = (value) => {
      if (this.state !== 'pending') return;
      this.state = 'fulfilled';
      this.value = value;
      this.callbacks.forEach(cb => cb.onFulfilled(value));
    };

    executor(resolve, () => {});
  }

  then(onFulfilled) {
    return new MyPromise((resolve) => {
      if (this.state === 'pending') {
        this.callbacks.push({ onFulfilled: () => {
          const result = onFulfilled(this.value);
          resolve(result); // 支持链式调用
        }});
      }
      if (this.state === 'fulfilled') {
        const result = onFulfilled(this.value);
        resolve(result);
      }
    });
  }
}

⚠️这代码能处理异步 resolve 吗?

Q6: 为什么 then 要返回新 Promise?

A6: 实现链式调用,且支持 return 值传递给下一个 then 💡

Q7: 如何实现 catch?

A7: catch(onRejected) 等价于 then(null, onRejected)


13. 如何优化相对路径引用?

问题场景:

js 复制代码
// a/b/c.js 中引用 util
import utils from '../../utils/index.js'; // 容易出错

✅ 解决方案:

  1. Webpack/Vite 配置 alias
js 复制代码
// vite.config.js
export default {
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
      '@utils': path.resolve(__dirname, 'src/utils')
    }
  }
}
js 复制代码
import utils from '@/utils'; // 清晰稳定
  1. 使用绝对路径(Node.js)
js 复制代码
import path from 'path';
import utils from path.resolve(__dirname, '../utils');
  1. ESLint + Import 插件校验路径
json 复制代码
"rules": {
  "import/no-unresolved": "error"
}

Q8: alias 在生产环境生效吗?

A8: 生效!构建工具会在打包时替换为真实路径,不影响运行时 ✅


14. Node.js 文件查找优先级

require('module') 时,Node 按顺序查找:

graph TD A[模块请求] --> B{核心模块?} B -->|是| C[直接返回
fs/path等] B -->|否| D{node_modules/.bin?} D -->|是| E[返回命令行工具] D -->|否| F[当前目录node_modules] F --> G[向上递归查找] G --> H{找到模块文件?} H -->|是| I[按扩展名尝试] H -->|否| K[抛出MODULE_NOT_FOUND] I --> J1[.js] I --> J2[.json] I --> J3[.node] I --> J4[.mjs] J2 --> L[自动parse JSON] J3 --> M[返回C++扩展] J1 --> N[执行JS代码] J4 --> O[ES Module处理] style A fill:#f9f,stroke:#333,stroke-width:2px style C fill:#b9f,stroke:#333,stroke-width:1px style E fill:#ff9,stroke:#333,stroke-width:1px style J2 fill:#9f9,stroke:#333,stroke-width:1px style J3 fill:#f99,stroke:#333,stroke-width:1px style K fill:#f99,stroke:#333,stroke-width:1px

特殊规则:

  • require('./utils'):先找 utils.js,再找 utils.json,最后找 utils/index.js
  • package.json 中的 main 字段指定入口

Q9: 如何强制加载 .mjs?

A9: 文件名用 .mjs 后缀,或在 package.json 中设置 "type": "module" 💥


15. npm2 与 npm3+ 的区别(依赖扁平化革命)

npm2:嵌套依赖(树状结构)

kotlin 复制代码
node_modules/
├── lodash@1.0.0
└── request@2.0.0
    └── lodash@0.9.0  // 重复安装!

→ 依赖深度大,磁盘占用高,Maximum call stack size exceeded 风险

npm3+:扁平化依赖(革命性优化)

kotlin 复制代码
node_modules/
├── lodash@1.0.0      // 共享
├── request@2.0.0
└── underscore@1.8.0

npm 会尝试将所有依赖提升到根目录,只要版本兼容

npm5+ 增强:

  • 引入 package-lock.json → 锁定依赖树,保证一致性
  • 默认使用扁平化 + 符号链接(symlink)处理冲突

Q10: 为什么还需要 yarn/pnpm?

A10: pnpm 使用硬链接 + 内容寻址存储,进一步节省磁盘空间,适合大型 mono-repo 🌟

相关推荐
CF14年老兵15 分钟前
2025年我最爱的免费编程学习资源宝库 💻
前端·后端·trae
码出极致20 分钟前
ZGC 执行日志:解锁低延迟运行的核心密码
面试
北京_宏哥23 分钟前
🔥《刚刚问世》系列初窥篇-Java+Playwright自动化测试-32- 操作日历时间控件-下篇(详细教程)
java·前端·面试
王维志28 分钟前
⏱ TimeSpan:C#时间间隔结构
前端·后端·c#·.net
阿幸软件杂货间36 分钟前
【最新版】Edge浏览器(官方版)安装包_Edge浏览器(官方版)安装教程
前端·edge
RaidenLiu1 小时前
Flutter 状态管理:Provider 入门与实战
前端·flutter
隔壁老王z1 小时前
设计实现一个Web 终端:基于 Vue 3 和 Xterm.js 的实践
前端·iterm
中微子1 小时前
简单介绍跨域资源共享(CORS)
前端
極光未晚1 小时前
Vue 项目 webpack 打包体积分析:从 “盲猜优化” 到 “精准瘦身”
前端·vue.js·性能优化
刘小筛1 小时前
Ant Design Vue (2x) 按钮(button)单击后离开,按钮状态无变化
前端