🔥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 🌟

相关推荐
EnCi Zheng22 分钟前
M5-markconv自定义CSS样式指南 [特殊字符]
前端·css·python
kyriewen26 分钟前
你的网页慢,用户不说直接走——前端性能监控教你“读心术”
前端·性能优化·监控
广州华水科技26 分钟前
北斗GNSS变形监测在大坝安全监测中的应用与优势分析
前端
前端老石人37 分钟前
前端开发中的 URL 完全指南
开发语言·前端·javascript·css·html
CAE虚拟与现实38 分钟前
五一假期闲来无事,来个前段、后端的说明吧
前端·后端·vtk·three.js·前后端
Sarvartha1 小时前
三目运算符
linux·服务器·前端
晓晨的博客1 小时前
ROS1录制的bag包转换为ROS2格式
前端·chrome
Wect1 小时前
LeetCode 72. 编辑距离:动态规划经典题解
前端·算法·typescript
donecoding1 小时前
别再让 pnpm 跟着 nvm 跑了!独立安装终极指南
前端·node.js·前端工程化
GISer_Jing1 小时前
AI全栈转型_TS后端学习路线
前端·人工智能·后端·学习