前端开发笔记与实践
一、Vue 开发规范与响应式机制
1. 组件命名规范
- 自定义组件使用大驼峰命名法(如
MyComponent
),符合 Vue 官方推荐,便于与原生 HTML 元素区分。
2. Proxy vs defineProperty
特性 | Proxy (Vue3) | Object.defineProperty (Vue2) |
---|---|---|
拦截范围 | 支持对象增删改查等所有基本操作 | 只能监听已有属性 |
数组支持 | 自动拦截数组变异方法 | 需手动重写数组方法 |
性能 | 更高效 | 层级嵌套需递归处理 |
Vue3 使用
Proxy
实现更全面的响应式拦截,而 Vue2 依赖defineProperty
实现有限拦截。
3. 响应式核心流程
- Observer:将数据转换为响应式对象(通过
defineProperty
或Proxy
) - Watcher:追踪依赖,记录哪些函数(如
render
)用到了哪些数据 - Dep:每个属性维护一个依赖列表,当属性变化时通知 Watcher 更新
- Scheduler:异步调度更新,避免重复渲染,提高性能
4. 调度器作用
- 合并多次变更,减少 render 触发次数
- 异步执行更新(基于
nextTick
和微任务队列) - 确保每个 Watcher 只执行一次更新
5. Vue3响应式性能对照
操作 | Vue2 (defineProperty) | Vue3 (Proxy) |
---|---|---|
初始化1000属性 | 15ms | 3ms |
新增100属性 | 需要Vue.set | 直接赋值 |
数组操作 | 特殊处理 | 原生支持 |
内存占用 | 每个属性1KB | 整个对象3KB |
二、CSS 与 SCSS 进阶
1. CSS 新单位
单位 | 计算依据 | 典型应用场景 |
---|---|---|
vmin | 视口宽高中较小值 | 移动端全面屏适配(适配小屏幕) |
vmax | 视口宽高中较大值 | 大屏展示元素尺寸控制(适配宽屏) |
dvh | 动态视口高度 | 解决移动浏览器工具栏遮挡问题 |
svh/lvh | 小/大视口高度 | 特定视口比例布局 |
/* 确保元素在任何设备上都可见 */
.modal {width: min(90vw, 800px);height: max(60vh, 400px);/* 移动端避开地址栏 */height: calc(100dvh - 60px);
}
2. SCSS 循环与变量
$num: 20;@for $i from 1 through $num {.btn:nth-child(#{$i}) {transform: scale(0.1 * $i);}
}
三、JavaScript 进阶技巧
1. 判断是否是数组
方法 | 是否可靠 | 说明 |
---|---|---|
Array.isArray() | ✅ 推荐 | ES6 标准方法 |
obj instanceof Array | ❌ | 受原型链影响 |
Object.prototype.toString.call(obj) | ❌ | 可被 Symbol.toStringTag 修改 |
2. 稀疏数组判断
function isSparseArray(arr) {if (!Array.isArray(arr)) return false;for (let i = 0; i < arr.length; i++) {if (!(i in arr)) return true;}return false;
}
3. 数组去空字符串
const arr = ['', '1'];
const filtered = arr.filter(Boolean); // ['1']
4. 字符串路径转类名
const path = 'view/home/index';
const className = path.split('/').join('-'); // view-home-index
5. 垃圾回收机制
- 垃圾判定:不可达内存
- 回收策略:
- 引用计数(易产生循环引用泄漏)
- 标记清除(主流浏览器采用)
- 闭包问题:词法环境未释放导致内存膨胀
- 手动释放:将引用设为
null
四、TypeScript 技巧
1. 函数参数类型约束
const obj = {age: 18,name: 'zhangsan'
};function fn(key: keyof typeof obj) {const v = obj[key];
}
2. 获取三方库函数参数/返回值类型
import { fn } from "some-lib";type FnParams = Parameters<typeof fn>[0]; // 获取第一个参数类型
type FnReturn = ReturnType<typeof fn>; // 获取返回值类型
3. 元组生成联合类型
const obj = {a: 1,b: 2,z: 36
};
type KeysType = keyof typeof obj; // 'a' | 'b' | ... | 'z'function getValue(key: KeysType) {console.log(obj[key]);
}
五、文件上传与下载
1. 文件上传交互方式
-
多选:
<input type="file" multiple>
-
文件夹选择:
<input type="file" webkitdirectory mozdirectory odirectory>
-
拖拽上传:
div.ondragover = e => e.preventDefault(); div.ondrop = e => {e.preventDefault();for (const item of e.dataTransfer.items) {const entry = item.webkitGetAsEntry();if (entry.isFile) {entry.file(file => console.log(file));} else {const reader = entry.createReader();reader.readEntries(entries => console.log(entries));}} };
-
拖拽上传增强版
class AdvancedDropzone {constructor(selector) {this.el = document.querySelector(selector);this.setupEvents();this.preview = this.createPreview();}setupEvents() {this.el.addEventListener('dragover', this.highlight.bind(this));this.el.addEventListener('dragleave', this.unhighlight.bind(this));this.el.addEventListener('drop', this.handleDrop.bind(this));}async handleDrop(e) {e.preventDefault();this.unhighlight();const entries = Array.from(e.dataTransfer.items).map(item => item.webkitGetAsEntry());const files = [];for (const entry of entries) {if (entry.isFile) {files.push(this.getFile(entry));} else {files.push(...await this.traverseDirectory(entry));}}this.previewFiles(files);}async traverseDirectory(dir) {const reader = dir.createReader();const entries = await new Promise(resolve => {reader.readEntries(resolve);});const files = [];for (const entry of entries) {if (entry.isFile) {files.push(await this.getFile(entry));} else {files.push(...await this.traverseDirectory(entry));}}return files;} }
-
大文件分片上传
class ChunkedUploader {constructor(file, options = {}) {this.file = file;this.chunkSize = options.chunkSize || 5 * 1024 * 1024;this.concurrent = options.concurrent || 3;this.chunks = Math.ceil(file.size / this.chunkSize);this.queue = [];}async upload() {const chunks = Array.from({ length: this.chunks }, (_, i) => i);const results = await pMap(chunks, this.uploadChunk.bind(this), {concurrency: this.concurrent});return this.finalize();}async uploadChunk(index) {const start = index * this.chunkSize;const end = Math.min(start + this.chunkSize, this.file.size);const chunk = this.file.slice(start, end);const form = new FormData();form.append('chunk', chunk);form.append('index', index);form.append('total', this.chunks);await axios.post('/upload', form, {onUploadProgress: this.createProgressHandler(index),__chunkIndex: index});} }
2. 文件上传网络请求
方式 | 支持进度 | 支持取消 |
---|---|---|
XHR / Axios | ✅ 上传/下载 | ✅ |
Fetch | ✅ 下载 | ✅(AbortController) |
3. 文件下载方式
-
<a>
标签下载(同源限制):<a href="http://xxx.pdf" download>Download</a>
-
Blob + URL.createObjectURL(跨域无 token 限制):
fetch(url).then(res => res.blob()).then(blob => {const url = URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = 'file.pdf';a.click();URL.revokeObjectURL(url); });
六、性能优化技巧
1. 环境兼容性封装(一次性判断)
const addEvent = (() => {if (ele.addEventListener) {return (ele, eventName, handler) => ele.addEventListener(eventName, handler);} else if (ele.attachEvent) {return (ele, eventName, handler) => ele.attachEvent('on' + eventName, handler);} else {return (ele, eventName, handler) => ele['on' + eventName] = handler;}
})();
2. Token 无感刷新方案
- 请求失败且为 401 错误
- 检查是否为刷新接口本身 → 是则跳过
- 若已有刷新 Promise 存在 → 复用
- 发起刷新请求 → 替换新 token 重新发起原始请求
- 失败 → 清除 token 跳转登录页
let refreshPromise: Promise<any> | null = null;export async function refreshToken() {if (refreshPromise) return refreshPromise;refreshPromise = new Promise(async resolve => {try {const res = await axios.get('/refresh_token', {headers: { Authorization: `Bearer ${getRefreshToken()}` },__isRefreshToken: true});if (res.code === 0) resolve(true);else resolve(false);} catch (e) {resolve(false);} finally {refreshPromise = null;}});return refreshPromise;
}
七、扩展知识
1. 单点登录(SSO)与 JWT 关系
- 单点登录:用户只需登录一次即可访问多个系统
- JWT:是一种 Token 生成与验证机制,常用于身份认证
- 关系:JWT 可作为 SSO 的 Token 实现方式之一,但二者没有必然联系