前端开发笔记与实践

发布于:2025-05-17 ⋅ 阅读:(18) ⋅ 点赞:(0)

一、Vue 开发规范与响应式机制

1. 组件命名规范

  • 自定义组件使用大驼峰命名法(如 MyComponent),符合 Vue 官方推荐,便于与原生 HTML 元素区分。

2. Proxy vs defineProperty

特性 Proxy(Vue3) Object.defineProperty(Vue2)
拦截范围 支持对象增删改查等所有基本操作 只能监听已有属性
数组支持 自动拦截数组变异方法 需手动重写数组方法
性能 更高效 层级嵌套需递归处理

Vue3 使用 Proxy 实现更全面的响应式拦截,而 Vue2 依赖 defineProperty 实现有限拦截。

3. 响应式核心流程

  1. Observer:将数据转换为响应式对象(通过 definePropertyProxy
  2. Watcher:追踪依赖,记录哪些函数(如 render)用到了哪些数据
  3. Dep:每个属性维护一个依赖列表,当属性变化时通知 Watcher 更新
  4. 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 无感刷新方案

  1. 请求失败且为 401 错误
  2. 检查是否为刷新接口本身 → 是则跳过
  3. 若已有刷新 Promise 存在 → 复用
  4. 发起刷新请求 → 替换新 token 重新发起原始请求
  5. 失败 → 清除 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 实现方式之一,但二者没有必然联系

网站公告

今日签到

点亮在社区的每一天
去签到