阅读视图

发现新文章,点击刷新页面。

JavaScript闭包实战:从类封装到防抖函数的深度解析

前言

闭包一直是JavaScript中最具魅力也最容易让人困惑的特性之一。很多开发者知道闭包的基本概念,但在实际应用中往往不知道如何巧妙运用。今天我们就来深入探讨闭包的核心应用场景,通过类封装和防抖函数的实战案例,看看闭包如何在实际开发中发挥作用。

闭包的核心应用场景

在深入代码之前,让我们先梳理一下闭包的主要应用场景:

  • 记忆函数 - 缓存计算结果
  • 柯里化 - 函数参数复用
  • 私有变量 - 数据封装
  • 函数防抖 - 控制执行频率
  • 偏函数 - 预设参数
  • 事件监听器 - 保持上下文
  • 立即执行函数 - 创建独立作用域

今天我们重点关注私有变量函数防抖两个场景,它们在实际开发中使用频率很高。

用闭包实现类的封装

传统面向对象编程的痛点

在JavaScript中,传统的对象创建方式往往无法很好地实现私有变量。所有通过this添加的属性都是公开的,任何人都可以直接访问和修改。

闭包解决方案:创建真正的私有变量

让我们看看如何用闭包来实现真正的私有变量:

function CreateCounter(num) {
    // 公共属性 - 外部可直接访问
    this.num = num;
    
    // 私有变量 - 外部无法直接访问
    let count = 0;
    
    // 返回对象作为对外接口
    return {
        num: num,
        increment: () => {
            count++;
        },
        decrement: () => {
            count--;
        },
        getCount: () => {
            console.log('count 被访问了');
            return count;
        }
    }
}

// 使用示例
const counter = CreateCounter(10);
console.log(counter.num); // 10 - 可以直接访问
// console.log(counter.count); // undefined - 无法直接访问私有变量

// 闭包延长了变量的生命周期,不能直接操作它
counter.increment();
console.log(counter.getCount()); // 1

完整的类封装实战案例

让我们看一个更完整的例子,展示如何用闭包实现一个Book类:

function Book(title, author, year) {
    // 私有变量 - 以_开头的变量表示私有(编程风格约定)
    let _title = title;
    let _author = author;
    let _year = year;
    
    // 私有方法 - 外部无法直接访问
    function getFullTitle() {
        return `${_title} by ${_author}`;
    }
    
    // 公共方法 - 外部可以访问
    this.getTitle = function() {
        return _title;
    }
    
    this.getAuthor = function() {
        return _author;
    }
    
    this.getYear = function() {
        return _year;
    }
    
    this.getFullInfo = function() {
        return `${getFullTitle()}, published in ${_year}`;
    }
    
    // 提供受控的修改接口
    this.updateYear = function(newYear) {
        if (typeof newYear === 'number' && newYear > 0) {
            _year = newYear;
        } else {
            console.error('Invalid year');
        }
    }
}

// 使用示例
let book = new Book("JavaScript高级程序设计", "Nicholas C. Zakas", 2010);
console.log(book.getTitle()); // "JavaScript高级程序设计"
console.log(book.getFullInfo()); // "JavaScript高级程序设计 by Nicholas C. Zakas, published in 2010"

book.updateYear(2015);
console.log(book.getYear()); // 2015

// 尝试直接访问私有变量会失败
console.log(book._title); // undefined

封装的核心思想

这种封装方式的精髓在于:

  1. 函数内部的变量成为私有 - 在函数作用域内,但外部无法直接访问
  2. 提供公共方法作为接口 - 通过this添加的方法是公开的
  3. 私有方法增强内部逻辑 - 如getFullTitle(),只供内部使用
  4. 受控的数据修改 - 如updateYear(),包含验证逻辑

通过这种方式,我们实现了真正的数据封装,既保护了内部状态,又提供了可控的访问接口。

防抖(Debounce)深度解析

什么是防抖

防抖的核心思想是:在某段时间内只执行最后一次触发,其他的都会被取消

这在处理高频事件时特别有用,比如:

  • Google搜索建议的Ajax请求
  • 图片懒加载中的scroll事件
  • 输入框的实时搜索

基础防抖实现

function debounce(fn, delay) {
    // 返回一个新函数来控制原函数的执行频率
    return function(args) {
        // 如果已经有定时器在等待,就清除它
        if (fn.id) {
            clearTimeout(fn.id);
        }
        // 设置新的定时器
        fn.id = setTimeout(function() {
            fn(args);
        }, delay);
    }
}

实战应用:搜索建议优化

// 获取输入框元素
let inputA = document.getElementById('inputA');
let inputB = document.getElementById('inputB');

// 模拟Google搜索建议的Ajax请求
function ajax(content) {
    console.log('发送请求:', content);
    // 这里可能是耗时的网络请求
    // 如果频繁执行,服务器会直接宕机
}

// 普通方式 - 每次输入都会触发请求
inputA.addEventListener('keyup', (e) => {
    ajax(e.target.value); // 频繁执行,服务器压力大
});

// 防抖优化 - 用户停止输入250ms后才发送请求
let debounceAjax = debounce(ajax, 250);
inputB.addEventListener('keyup', function(event) {
    debounceAjax(event.target.value);
});

防抖的价值

防抖的核心价值在于理解用户意图。用户在输入时,我们不需要对每个字符都做出响应,而是等待用户完成一个完整的输入动作。这样既减少了服务器压力,也提升了用户体验。

this丢失问题及解决方案

JavaScript中this的动态绑定机制

在深入this丢失问题之前,我们需要理解JavaScript中this的核心特性:this不是在函数定义时确定的,而是在函数调用时动态绑定的

// 不同的调用方式,this指向完全不同
var obj = {
    name: 'test',
    fn: function() {
        console.log(this.name);
    }
};

obj.fn();        // 'test' - 作为对象方法调用,this指向obj
var fn = obj.fn;
fn();            // undefined - 作为普通函数调用,this指向全局对象

防抖中this丢失的完整分析

让我们通过一个完整的例子来分析this是如何丢失的:

// 问题版本的防抖函数
function debounce(fn, delay) {
    return function(args) {
        if (fn.id) {
            clearTimeout(fn.id);
        }
        fn.id = setTimeout(function() {
            fn(args); // 关键问题点:this丢失
        }, delay);
    }
}

let obj = {
    count: 0,
    inc: debounce(function(val) {
        console.log(this); // 打印结果:undefined 或 window
        this.count += val; // 报错或操作错误对象
    }, 500)
};
obj.inc(2);

this丢失的根本原因

1. 调用链条的分析

  • 第一步:obj.inc(2) - 此时this正确指向obj
  • 第二步:进入防抖函数返回的匿名函数
  • 第三步:setTimeout的回调函数被JavaScript引擎调用

2. setTimeout的执行机制 当我们写下这样的代码时:

setTimeout(function() {
    fn(args);
}, delay);

JavaScript引擎实际上是在全局作用域中执行这个回调函数,相当于:

// 在全局作用域中调用
function globalCallback() {
    fn(args); // 这里的fn()调用没有明确的调用者
}

3. 函数调用时this的确定规则

  • 当函数作为对象方法调用时:obj.method() - this指向obj
  • 当函数作为普通函数调用时:method() - this指向全局对象(严格模式下为undefined)

在setTimeout回调中,fn(args)就属于第二种情况,因此this丢失。

解决方案:闭包保存this引用

function debounce(fn, delay) {
    return function(args) {
        // 关键:保存当前的this引用
        var that = this;
        console.log(that, '当前this指向正确的对象');
        
        if (fn.id) {
            clearTimeout(fn.id);
        }
        
        fn.id = setTimeout(function() {
            // 使用call方法显式指定this
            fn.call(that, args);
        }, delay);
    }
}

let obj = {
    count: 0,
    inc: debounce(function(val) {
        console.log(this.count += val); // 正确指向obj
        console.log(this.count, '计数正确更新');
    }, 500)
};
obj.inc(2);

解决方案的原理剖析

1. 闭包的作用

  • 外层函数执行时,that变量保存了正确的this值
  • 内层的setTimeout回调函数通过闭包机制可以访问到that
  • 这样就形成了一个"this值的桥梁"

2. call方法的显式绑定 fn.call(that, args)的作用是:

  • 显式指定fn函数执行时的this值
  • 绕过JavaScript的默认this绑定机制
  • 确保函数在正确的上下文中执行

3. 执行流程分析

obj.inc(2)
↓
// 进入防抖函数返回的函数,this = obj
var that = this; // that = obj// setTimeout回调执行,虽然在全局作用域
// 但通过fn.call(that, args)显式指定了this
fn.call(that, args) // 相当于 fn.call(obj, args)

其他解决方案

除了保存this引用的方法,还有其他几种解决方案:

1. 使用箭头函数

function debounce(fn, delay) {
    return function(args) {
        if (fn.id) {
            clearTimeout(fn.id);
        }
        fn.id = setTimeout(() => {
            fn.call(this, args); // 箭头函数继承外层this
        }, delay);
    }
}

2. 使用bind方法

function debounce(fn, delay) {
    return function(args) {
        if (fn.id) {
            clearTimeout(fn.id);
        }
        fn.id = setTimeout(fn.bind(this, args), delay);
    }
}

通过这样的深入分析,我们不仅解决了this丢失的问题,更重要的是理解了JavaScript中this绑定的核心机制。

高阶函数的设计思想

什么是高阶函数

从我们的防抖实现中可以看出,函数的参数也是函数,这就是高阶函数的特征。高阶函数是函数式编程的核心概念,它允许我们:

  1. 抽象通用逻辑 - 将fn作为参数,让防抖函数可以适用于任何函数
  2. 延迟执行 - 通过闭包保存状态,控制函数的执行时机
  3. 状态管理 - 利用闭包的特性,为每个防抖函数实例维护独立的状态

设计模式的体现

我们的防抖函数体现了几个重要的设计模式:

  • 装饰器模式 - 在不修改原函数的情况下,为其添加防抖功能
  • 代理模式 - 返回的函数作为原函数的代理,控制其访问
  • 策略模式 - 不同的delay参数代表不同的防抖策略

实战建议

1. 选择合适的延迟时间

  • 搜索建议:200-300ms,平衡响应速度和请求频率
  • 按钮点击:500-1000ms,防止重复提交
  • 窗口大小调整:100-200ms,保证响应性

2. 注意内存泄漏

在单页应用中,记得在组件卸载时清理定时器:

// 清理函数
function clearDebounce(debouncedFn) {
    if (debouncedFn.id) {
        clearTimeout(debouncedFn.id);
    }
}

3. 考虑立即执行版本

有时我们需要立即执行一次,然后再进行防抖:

function debounce(fn, delay, immediate = false) {
    return function(args) {
        var that = this;
        var callNow = immediate && !fn.id;
        
        if (fn.id) {
            clearTimeout(fn.id);
        }
        
        fn.id = setTimeout(function() {
            fn.id = null;
            if (!immediate) fn.call(that, args);
        }, delay);
        
        if (callNow) fn.call(that, args);
    }
}

总结

闭包在JavaScript开发中的应用远比我们想象的更加广泛和实用。通过本文的探讨,我们了解了:

  1. 类封装:如何用闭包实现真正的私有变量和方法
  2. 防抖机制:如何控制函数执行频率,优化性能和用户体验
  3. this问题:如何在异步调用中保持正确的上下文
  4. 高阶函数:如何设计通用的、可复用的工具函数

这些技术不仅仅是理论知识,更是我们日常开发中解决实际问题的利器。掌握了这些技巧,你就能写出更加优雅、高效的JavaScript代码。

记住,闭包的核心价值在于状态管理作用域控制。无论是数据封装还是执行控制,都离不开对这两个核心概念的深入理解。闭包让我们能够创建既安全又灵活的代码结构,这正是现代JavaScript开发的精髓所在。

告别刷新就丢数据!localStorage 全面指南

localStorage:网页本地存储

在现代 Web 开发中,为了提升用户体验和减少服务器请求,前端常常需要将一些数据缓存在用户的浏览器中。HTML5 提供了多种客户端存储方案,其中 localStorage 是最常用的一种持久化存储机制。

一、什么是 localStorage

localStorage 是一种由浏览器提供的 Web Storage API,它允许网页在用户的浏览器中长期保存键值对数据。与 sessionStorage 不同的是,localStorage 的数据不会因为浏览器关闭或页面刷新而丢失,只有当用户主动清除浏览器缓存或者通过代码显式删除时,这些数据才会被清除。

二、基本特性

  • 持久性:数据可以在浏览器中长期保存

  • 作用域:同源策略限制(相同协议 + 域名 + 端口)

  • 容量限制:通常最大为 5MB(视浏览器而定),若数据比5MB还大可以使用indexDB

  • 数据类型:只能存储字符串,复杂类型需序列化

        const person = {
            name: "张三",
            age: 18,
        }
        localStorage.setItem('personName', person); // 没有序列化
        localStorage.setItem('person', JSON.stringify(person)); // 序列化
    
    let persons = JSON.parse(localStorage.getItem('persons')); // 将json字符串zh
    

    image-20250710113917773.png

  • 同步操作:所有操作都是同步的,可能影响性能

三、常用方法

localStorage 提供了一组简单易用的方法来进行数据操作:


localStorage.setItem('key', 'value');// 存储数据

const value = localStorage.getItem('key');// 获取数据

localStorage.removeItem('key');// 删除数据

localStorage.clear();// 清空所有数据

const keyName = localStorage.key(n);// 获取第 n 个 key 的名称

const length = localStorage.length;// 获取当前存储项的数量

以一个增加列表项为例

输入姓名年龄后存储到localStorage,再次刷新页面数据依然存在。

html代码

    <form action="">
      姓名<input
        type="text"
        name="username"
        id="username"
        placeholder="请输入姓名"
        required
      /><br />
      年龄<input
        type="text"
        name="userage"
        id="userage"
        placeholder="请输入年龄"
        required
      />
      <input type="submit" value="提交" />
    </form>
    <ul id="person-list"></ul>

js代码

        
      const key = "persons";
// 添加数据
      document.querySelector("form").addEventListener("submit", (e) => {
        e.preventDefault();
        const username = e.target.username.value;
        const userage = e.target.userage.value;
        if (!username || !userage) return;
        const person = {
          username: username,
          userage: userage,
        };
        let persons = JSON.parse(localStorage.getItem(key)) || [];
        persons.push(person);
        localStorage.setItem(key, JSON.stringify(persons));
        refush();
      });

      document.addEventListener("DOMContentLoaded", () => {
        refush();
      });

 // 读取localStorage中的数据并展示
      function refush() {
        const personUl = document.getElementById("person-list");
        const persons = JSON.parse(localStorage.getItem(key)) || [];
        if(persons.length === 0){
          personUl.innerHTML = "暂无数据";
          return;
        }
        personUl.innerHTML = "";
        persons.forEach((person) => {
          const li = document.createElement("li");
          li.innerHTML = `${person.username} ${person.userage}`;
          personUl.appendChild(li);
        });
      }
    

初始时没有任何数据

image-20250710114741636.png localStorage存储实现,刷新后数据不丢失

ovwhe-dqle5.gif

四、用途与注意事项

localStorage适合存储用户的非敏感偏好设置(如主题、语言),缓存静态数据(如菜单结构、地区列表),实现轻量级的状态管理(如登录态标记)

不适合存储敏感信息(如密码、token),容易受到 XSS 攻击。存储大量结构化数据时,应考虑使用 IndexedDB,也不是和存储频繁更新的数据,避免阻塞主线程

六、与其他存储方式对比

存储方式 生命周期 容量 类型 安全性 适用场景
localStorage 永久 5MB 左右 字符串 低(易受 XSS) 轻量缓存、偏好设置
sessionStorage 浏览器标签关闭即失效 5MB 左右 字符串 单次会话数据
Cookie 可设置过期时间 4KB 左右 字符串 中(可加密传输) 登录态、跟踪用户
IndexedDB 永久 几百 MB 到 GB 级 结构化数据 大数据、离线应用
Web Worker Cache 自定义 无明确限制 可缓存资源 PWA、Service Worker 缓存

JavaScript闭包实战:解析节流函数的精妙实现 🚀

👋 大家好,今天我们将深入解析一个实际项目中的节流函数实现,探讨闭包与高阶函数如何巧妙结合来提升前端性能。

🔍 为什么需要函数节流?

在前端开发中,我们经常遇到高频触发的事件,如滚动、输入、鼠标移动等。如果每次事件触发都执行复杂操作,会导致性能问题:

// 未优化的输入事件
inputElement.addEventListener('keyup', function(e) {
    // 每次按键都会触发请求,可能导致:
    // 1. 服务器压力过大
    // 2. 页面卡顿
    // 3. 不必要的计算资源浪费
    ajax(e.target.value);
});

💡 核心概念:节流(throttle)是指在一定时间内,无论事件触发多少次,函数只会执行一次。

🌟 高阶函数:函数式编程的精髓

在深入节流函数前,我们需要理解高阶函数的概念:

高阶函数是指接收函数作为参数和/或返回函数作为结果的函数。

节流函数是高阶函数的典型应用:它接收一个函数作为参数,并返回一个增强版的新函数。这种模式使我们能够在不修改原函数的情况下,为其添加节流功能。

🧩 深入解析:节流函数的实现

让我们逐行分析这个精巧的节流函数实现:

function throttle(fn, delay) {
    let last,       // 上次执行的时间
        deferTimer; // 延迟定时器ID
    
    return function(...args) {
        let that = this;  // 保存上下文
        let now = +new Date();  // 当前时间戳
        
        if (last && now < last + delay) {
            // 如果距离上次执行不足delay时间
            clearTimeout(deferTimer);
            deferTimer = setTimeout(function() {
                last = now;
                fn.apply(that, args);
            }, delay);
        } else {
            // 首次执行或者距离上次执行已超过delay
            last = now;
            fn.apply(that, args);
        }
    };
}

代码解析:闭包与高阶函数的完美结合

  1. 高阶函数特性

    • 接收函数参数:throttle接收原始函数fn作为参数
    • 返回函数:返回一个包装后的新函数
    • 函数增强:为原函数添加了节流功能,而不改变其行为
  2. 闭包的关键作用

    • 状态保持:lastdeferTimer变量在返回的函数中持续存在
    • 私有变量:这些变量对外部不可见,避免了全局污染
    • 上下文保存:通过that = this捕获调用上下文
  3. 巧妙的执行逻辑

    • 条件判断:if(last && now < last + delay)检查是否在冷却期内
    • 定时器复用:每次触发都会清除之前的定时器,确保只有最后一次调用生效
    • 首次立即执行:首次调用或冷却期过后立即执行函数

与简单节流实现的对比

这个实现比简单的节流函数更加高级,它结合了"立即执行"和"延迟执行"两种模式:

  • 首次触发立即执行
  • 冷却期内的触发会被延迟到冷却期结束
  • 最后一次触发一定会执行,不会丢失

这种实现确保了良好的用户体验 - 既有即时响应,又不会过度执行。

🔧 this指向问题:闭包中的上下文处理

注意代码中的这一行:let that = this;

这行代码解决了JavaScript中常见的this指向问题。在setTimeout回调中,this默认指向全局对象(window),而不是触发事件的元素。通过闭包捕获当前上下文,我们确保了函数在正确的上下文中执行。

// 错误示例:没有保存上下文
function badThrottle(fn, delay) {
    let timer;
    return function(...args) {
        if (!timer) {
            timer = setTimeout(function() {
                // 这里的this指向window,而非调用对象
                fn.apply(this, args); // 错误的上下文
                timer = null;
            }, delay);
        }
    };
}

// 正确示例:4.html中的实现
deferTimer = setTimeout(function() {
    last = now;
    fn.apply(that, args); // 正确的上下文
}, delay);

📊 实际应用:输入事件优化

看看代码中如何应用节流函数优化输入事件:

// 原始函数
const ajax = function(content) {
    // 这里可能是复杂的AJAX请求
    console.log('ajax request', + content);
}

// 创建节流版本
let throttleAjax = throttle(ajax, 200);

// 应用到输入事件
inputC.addEventListener('keyup', function(e) {
    throttleAjax(e.target.value);
});

这个实现优雅地解决了以下问题:

  1. 减少请求频率:无论用户输入多快,最多每200ms发送一次请求
  2. 保留即时反馈:首次输入立即响应,提供良好用户体验
  3. 确保最终结果:用户停止输入后的最后一次输入会被处理

🧪 深入思考:节流函数的工作原理

让我们通过一个时间轴来理解这个节流函数的工作原理:

时间轴: 0ms --- 100ms --- 200ms --- 300ms --- 400ms
事件触发:  ↑       ↑        ↑                 ↑
实际执行:  ↑                 ↑                 ↑
  1. 0ms:首次触发,立即执行(last为null,走else分支)
  2. 100ms:触发事件,但距离上次执行不足200ms,设置定时器
  3. 200ms:又触发事件,清除之前的定时器,重新设置定时器
  4. 300ms:没有事件触发,但之前设置的定时器执行
  5. 400ms:触发事件,已经过了200ms,立即执行

这种机制确保了:

  • 函数不会过于频繁执行(最少间隔200ms)
  • 不会丢失用户的最后一次操作
  • 首次操作能立即得到响应

🔍 闭包在节流函数中的核心价值

节流函数是闭包应用的经典案例,闭包在其中提供了三个关键能力:

  1. 状态记忆:记住上次执行时间和定时器ID
  2. 函数增强:在不修改原函数的情况下添加新功能
  3. 上下文保存:确保函数在正确的上下文中执行

如果没有闭包,我们将无法实现这种优雅的节流功能,可能需要使用全局变量或复杂的类设计。

💡 高阶函数的威力

高阶函数是函数式编程的核心概念,它使我们能够:

  1. 抽象行为模式:将"节流"这种行为模式抽象为可重用函数
  2. 分离关注点:业务逻辑(ajax请求)与控制逻辑(节流)分离
  3. 代码复用:一次编写节流函数,到处应用

在我们的例子中,throttle函数可以应用于任何需要节流的函数,而不仅仅是ajax请求。

📝 总结:闭包与高阶函数的完美结合

通过对4.html中节流函数的深入分析,我们看到了闭包和高阶函数如何协同工作,创造出优雅而强大的解决方案。这种实现:

  • 利用高阶函数实现了行为增强和关注点分离
  • 通过闭包保持状态和上下文
  • 巧妙平衡了性能用户体验

掌握这种模式,将帮助你写出更加优雅、高效的JavaScript代码,应对各种前端性能挑战。

🌈 实践建议:在处理高频事件时,优先考虑使用节流函数;理解闭包和高阶函数的结合,能够帮助你更好地实现各种函数增强模式。

8个你必须掌握的「Vue」实用技巧

1. 利用 v-bind="attrs"和 v−on="listeners" 简化组件通信

在开发复杂组件时,我们经常需要传递大量属性和事件。使用 v-bind="attrs"von="attrs" 和 v-on="listeners" 可以避免繁琐的逐个声明:

<!-- 父组件 -->
<template>
  <child-component :name="name" :age="age" @update="handleUpdate" @delete="handleDelete" />
</template>

<!-- 子组件 -->
<template>
  <grandchild-component v-bind="$attrs" v-on="$listeners" />
</template>

这样,所有属性和事件都会自动传递给孙组件,无需在中间组件中重复声明。

2. 使用 computed 的 setter 实现双向绑定

computed 不仅可以用于计算属性,还可以通过 setter 实现双向绑定:

export default {
  data() {
    return {
      firstName: '张',
      lastName: '三'
    }
  },
  computed: {
    fullName: {
      get() {
        return this.firstName + ' ' + this.lastName
      },
      set(value) {
        const [first, last] = value.split(' ')
        this.firstName = first
        this.lastName = last
      }
    }
  }
}

3. 使用 provide/inject 实现跨级组件通信

对于深层嵌套的组件通信,provide/inject 比 props 和事件更优雅:

// 祖先组件
export default {
  provide() {
    return {
      theme: this.theme,
      user: this.user
    }
  }
}

// 深层子组件
export default {
  inject: ['theme', 'user'],
  mounted() {
    console.log(this.theme, this.user)
  }
}

4. 使用 v-once 和 v-memo 优化性能

对于静态内容或不需要频繁更新的内容,使用 v-once 可以避免不必要的重新渲染:

<!-- 只渲染一次 -->
<div v-once>{{ staticContent }}</div>

<!-- Vue 3 中的 v-memo -->
<div v-memo="[value]">{{ expensiveOperation(value) }}</div>

5. 使用自定义指令封装 DOM 操作

将常用的 DOM 操作封装成自定义指令,提高代码复用性:

// 注册全局指令
Vue.directive('focus', {
  inserted(el) {
    el.focus()
  }
})

// 注册局部指令
export default {
  directives: {
    color: {
      bind(el, binding) {
        el.style.color = binding.value
      },
      update(el, binding) {
        el.style.color = binding.value
      }
    }
  }
}

6. 使用函数式组件优化无状态组件

对于没有状态、没有实例的组件,使用函数式组件可以减少性能开销:

<template functional>
  <div class="list-item">
    <span>{{ props.item.name }}</span>
    <span>{{ props.item.value }}</span>
  </div>
</template>

7. 使用 watch 的 deep 和 immediate 选项

watch 不仅可以监听数据变化,还可以通过选项实现更复杂的监听:

export default {
  watch: {
    // 深度监听对象变化
    user: {
      handler(newVal, oldVal) {
        console.log('用户数据发生变化')
      },
      deep: true,
      immediate: true // 立即执行一次
    }
  }
}

8. 使用 mixin 提取公共逻辑

将多个组件共用的逻辑提取到 mixin 中,避免代码重复:

// commonMixin.js
export default {
  data() {
    return {
      loading: false
    }
  },
  methods: {
    async fetchData(api) {
      this.loading = true
      try {
        const res = await this.$http.get(api)
        return res.data
      } finally {
        this.loading = false
      }
    }
  }
}

// 组件中使用
export default {
  mixins: [commonMixin],
  mounted() {
    this.fetchData('/api/users')
  }
}

react合成事件

react源码要封装合成事件优势

  • 通过事件委托机制,将所有事件处理程序附加到根元素上,而不是每个子元素;提高了性能和内存使用效率,特别是在有大量元素的情况下
  • 重新封装的合成事件消除了浏览器的差异,兼容性好

如何实现的

  • 事件注册: 在createRoot之前就会调用
// 当前React18提供了以下5个合成事件
Plugin SimpleEventPlugin.registerEvents(); // SimpleEventPlugin 处理一些常用的基础事件,详细请参见DOMEventProperties.js 中的registerSimpleEvents注册函数 
EnterLeaveEventPlugin.registerEvents(); // 鼠标移动事件 
ChangeEventPlugin.registerEvents(); // 修改事件 
SelectEventPlugin.registerEvents(); // 处理选择事件 
BeforeInputEventPlugin.registerEvents(); // 处理控件输入前事件
  • 事件绑定
const createRoot(rootContainerElement){
   .....
   // 在根元素上进行事件绑定
   listenToAllSupportedEvents(rootContainerElement);
}
  • 事件触发
 export function dispatchEvent(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
  nativeEvent: AnyNativeEvent,
): void {
  ......
  // 收集沿途事件、构建合成事件、遍历捕获事件、遍历冒泡事件
  dispatchEventForPluginEventSystem(
    domEventName,
    eventSystemFlags,
    nativeEvent,
    null,
    targetContainer,
  );
}

function dispatchEventsForPlugins(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  nativeEvent: AnyNativeEvent,
  targetInst: null | Fiber,
  targetContainer: EventTarget,
): void {
  const nativeEventTarget = getEventTarget(nativeEvent);
  // 存放事件的队列
  const dispatchQueue: DispatchQueue = [];
  // 收集沿途事件、构建合成事件
  extractEvents(
    dispatchQueue,
    domEventName,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags,
    targetContainer,
  );
  // 遍历捕获事件、遍历冒泡事件
  processDispatchQueue(dispatchQueue, eventSystemFlags);
}

大致流程图如下:

image.png

文章借鉴 juejin.cn/post/739947…

深拷贝还在用lodash吗?来试试原装的structuredClone()吧!

一直以来深拷贝是前端的一个必考科目,面试来个手写deepClone函数你麻不麻?现在2025年了,得站起来了,来看看原装的structuredClone()函数吧。

以前的深拷贝

基本就两个实现方式

  • JSON.parse(JSON.stringify(obj))
  • lodash 的 cloneDeep

JSON.parse(JSON.stringify(obj))的优点是简单清晰,原生方法,缺点对象存在循环引用会报错:

const obj = { a: 1 }
obj.b = obj

const obj1 = JSON.parse(JSON.stringify(obj))

image.png

还有Date对象会转化成字符串,Set、Map、正则、Error 对象,该方法会将其转成空对象字面量 { } ,如果存在 undefined,该方法会直接忽略

const obj = { date: new Date('2025-07-10') }
const obj1 = JSON.parse(JSON.stringify(obj))
obj1.date // '2025-07-10T00:00:00.000Z'

const obj = {
    set: new Set([1, 2, 3]),
    map: new Map([['key1', 'value1'],['key2', 'value2']]),
    regex: /abc/gi,
    error: new Error('error!'),
    undefined: undefined
}
const obj1 = JSON.parse(JSON.stringify(obj))
console.log(obj1)

wechat_2025-07-10_170421_556.png

至于lodash老问题了

a246b8b8-7fe9-4081-a05d-d761ec214340.png 就算按需引入体积也不小

5eca1bb9-823e-4ed2-97f0-d08663888d51.png

对有的体积有严格要求的项目直接就PASS了。

现在的深拷贝

废话不多说直接看代码

const obj = { name: 'John' };
const clonedObj = structuredClone(obj);
console.log(obj === clonedObj); // false
obj.itself = obj
const clonedObj2 = structuredClone(obj);
console.log(clonedObj2.itself === obj); // false
console.log(clonedObj2.itself === clonedObj2); // true

const complex = {
    map: new Map([["key", "value"]]),
    set: new Set([1, 2, 3]),
    date: new Date(),
    regex: /abc/gi
};
const clone = structuredClone(complex);
console.log(clone); 
console.log(clone === complex); // false

559ba72b-95ee-4222-972a-e9d9f33ae953.png

优雅,简洁,舒服,还得是原装的。

StructuredClone()方法使用结构化克隆算法将给定的值进行深拷贝,该方法还支持把原值中的可转移对象转移(而不是拷贝)到新对象上。可转移对象与原始对象分离并附加到新对象,它们将无法在原始对象中被访问。这是MDN上的定义,转移这个定义有点理解不了,这是官方示例

// 创建一个给定字节大小的 ArrayBuffer
const buffer1 = new ArrayBuffer(16);

const object1 = {
  buffer: buffer1,
};

// 克隆包含 buffer 的对象,并将其转移
const object2 = structuredClone(object1, { transfer: [buffer1] });

// 从克隆后的 buffer 创建数组
const int32View2 = new Int32Array(object2.buffer);
int32View2[0] = 42;
console.log(int32View2[0]);

// 从原 buffer 创建数组将抛出 TypeError
const int32View1 = new Int32Array(object1.buffer);

9f38078f-c1a4-486a-8c27-503d7ff0bcb7.png 我粗浅的理解是键值之间的连接打断了,把缓存区值的指向指向了新的克隆对象,大概这么个意思,评论如果有大神知道,欢迎交流讨论!

最后看一下兼容性:

7d54ea79-c23a-4f7c-b100-0e169a248fbb.png 可以看到基本主流浏览器(除了IE)都支持了这个方法,Node17版本也支持了,可以放心大胆的用起来了。

🚀Promise 从入门到手写:核心方法实现全指南

Promise的介绍、原理和手写

一、Promise 的诞生:解决回调地狱的终极方案

在 JavaScript 的异步编程发展史上,回调函数曾是处理异步操作的主要手段。但随着业务逻辑的复杂化,回调嵌套的层数越来越深,形成了令人头疼的 "回调地狱"。例如:

fs.readFile('a.txt', 'utf8', (err, data1) => {

    fs.readFile(`./${data1}.txt', 'utf8', (err, data2) => {

        fs.readFile(`./${data2}.txt', 'utf8', (err, data3) => {

            // 深层嵌套的回调逻辑, 从这开始如果想处理异常就已经有点麻烦了

        });

    });

});

这种代码结构不仅可读性差,维护成本高,而且异常处理困难。Promise 的出现正是为了规范异步编程,通过链式调用让异步逻辑更清晰,状态管理更可控。ECMAScript 6 正式将 Promise 纳入标准,使其成为现代 JavaScript 异步编程的基石。

二、Promise 核心概念解析

(一)Promise 的三种状态

  1. pending(进行中):初始状态,异步操作未完成或未拒绝

  2. fulfilled(已完成):异步操作成功完成

  3. rejected(已拒绝):异步操作失败

状态转换具有单向性:pending可以转换为fulfilled或rejected,但一旦确定状态就无法再改变。

(二)Promise 构造函数

const promise = new Promise((resolve, reject) => {

    // 执行异步操作

    setTimeout(() => {

        const success = true;

        if (success) {

            resolve('操作成功'); // 状态转为fulfilled

        } else {

            reject('操作失败'); // 状态转为rejected

        }

    }, 1000);

});
  • resolve:成功时调用的回调函数,参数为成功状态的返回值

  • reject:失败时调用的回调函数,参数为失败状态的原因

构造函数的手写
class MyPromise {

    constructor(executor) {

        this.state = 'pending';

        this.value = undefined;

        this.fulfilledCallbacks = [];

        this.rejectedCallbacks = [];



        const resolve = (value) => {

            if (this.state !== 'pending') return;

            this.state = 'fulfilled';

            this.value = value;

            // 异步执行回调(模拟微任务)

            setTimeout(() => {

                this.fulfilledCallbacks.forEach(fn => fn(value));

            }, 0);

        };



        const reject = (reason) => {

            if (this.state !== 'pending') return;

            this.state = 'rejected';

            this.value = reason;

            setTimeout(() => {

                this.rejectedCallbacks.forEach(fn => fn(reason));

            }, 0);

        };



        try {

            executor(resolve, reject);

        } catch (error) {

        }

    }

(三)关键方法详解

then () - 异步结果处理
promise.then(

    (result) => { // fulfilled状态处理函数

        console.log(result); // 输出"操作成功"

    }, 

    (error) => { // rejected状态处理函数

        console.error(error);

    }

);
then方法的手写
MyPromise.prototype.then = function (onFulfilled, onRejected) {
    // 参数默认值处理
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val;
    onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err; };

    const newPromise = new MyPromise((resolve, reject) => {
        if (this.state === 'fulfilled') {
            // 立即执行,返回值决定新Promise状态
            setTimeout(() => {
                try {
                    const x = onFulfilled(this.value);
                    resolvePromise(newPromise, x, resolve, reject); // 核心解析函数
                } catch (error) {
                    reject(error);
                }
            }, 0);
        }

        if (this.state === 'rejected') {
            setTimeout(() => {
                try {
                    const x = onRejected(this.value);
                    resolvePromise(newPromise, x, resolve, reject);
                } catch (error) {
                    reject(error);
                }
            }, 0);
        }

    }
  }
catch () - 统一错误处理
promise.then(result => {

// 成功处理逻辑

}).catch(error => {

// 统一处理所有拒绝状态

console.error('发生错误:', error);

});
finally () - 最终执行逻辑
promise

.then(result => process(result))

.catch(error => handleError(error))

.finally(() => {

// 无论成功或失败都会执行

console.log('异步操作结束');

});

三、Promise 链式调用的最佳实践

(一)链式调用的实现原理

每次调用then()或catch()都会返回一个新的 Promise 对象,从而实现链式调用。新 Promise 的状态由以下规则决定:

  • 若回调函数返回有效值,新 Promise 状态为fulfilled,值为返回值

  • 若回调函数抛出错误,新 Promise 状态为rejected,值为错误对象

  • 若回调函数返回另一个 Promise,新 Promise 状态由该 Promise 决定

(二)典型应用场景

1. 顺序执行异步操作
fetchUser()

    .then(user => fetchOrder(user.id))

    .then(order => processOrder(order))

    .catch(error => logError(error));
2. 错误处理策略
asyncFunction()

    .then(result => {

        // 可能抛出错误的操作

        if (result.invalid) {

            throw new Error('无效结果');

        }

        return process(result);

    })

    .catch(error => {

        // 可以针对不同错误类型处理

        if (error instanceof NetworkError) {

            retryRequest();

        } else if (error.message === '无效结果') {

            showValidationError();

        }

    });

四、高级特性与静态方法

Promise.all () - 并行执行多个异步操作

const promise1 = fetch('https://api/data1');

const promise2 = fetch('https://api/data2');



Promise.all([promise1, promise2])

    .then(responses => responses.map(res => res.json()))

    .then(data => console.log('全部数据:', data))

    .catch(error => console.error('任一请求失败:', error));

  • 输入:Promise 数组

  • 输出:包含所有 Promise 结果的数组

  • 特点:只要有一个 Promise 拒绝,整体就拒绝

Promise.all()的手写
MyPromise.all = function (promises) {
    return new MyPromise((resolve, reject) => {
        const results = [];
        let count = 0;
        promises.forEach((p, index) => {
            MyPromise.resolve(p).then((value) => {
                results[index] = value;
                if (++count === promises.length) {
                    resolve(results);
                }
            }, reject); // 第一个失败立即触发reject
        });
    });
};

Promise.race () - 竞争执行

const timeout = new Promise((_, reject) => {

    setTimeout(() => reject(new Error('请求超时')), 5000);

});



const fetchData = fetch('https://api/data');

Promise.race([fetchData, timeout])

    .then(data => process(data))

    .catch(error => handleError(error));
  • 输入:Promise 数组

  • 输出:第一个解决(fulfilled/rejected)的 Promise 结果

Promise.race () 的手写
MyPromise.race = function (promises) {
    return new MyPromise((resolve, reject) => {
        promises.forEach(p => {
            MyPromise.resolve(p).then(resolve, reject);
        });
    });
};

Promise.allSettled () - 获取所有结果(包括拒绝)

const promises = [

    Promise.resolve(1),

    Promise.reject('失败'),

    Promise.resolve(3)

];



Promise.allSettled(promises)

    .then(results => {

        results.forEach(result => {

            if (result.status === 'fulfilled') {

                console.log('成功值:', result.value);

            } else {

                console.error('失败原因:', result.reason);

            }

        });

    });
Promise.allSettled ()的手写
MyPromise.allSettled = function(promises) {
    return new MyPromise((resolve) => {
      const results = [];
      let count = 0;
      promises.forEach((p, index) => {
        MyPromise.resolve(p).then(
          (value) => {
            results[index] = { status: 'fulfilled', value };
            if (++count === promises.length) resolve(results);
          },
          (reason) => {
            results[index] = { status: 'rejected', reason };
            if (++count === promises.length) resolve(results);
          }
        );
      });
    });
  };

(四)Promise.any () - 获取第一个成功结果

const promises = [

    Promise.reject('失败1'),

    Promise.resolve('成功'),

    Promise.reject('失败2')

];



Promise.any(promises)

    .then(value => console.log('第一个成功值:', value))

    .catch(error => console.error('所有都失败:', error));

五、Promise 与 async/await 的完美结合

(一)语法糖带来的代码简化

传统 Promise 写法:


fetchUser()

    .then(user => fetchProfile(user.id))

    .then(profile => saveProfile(profile))

    .catch(error => handleError(error));

async/await 写法:


async function saveUserProfile() {

    try {

        const user = await fetchUser();

        const profile = await fetchProfile(user.id);

        await saveProfile(profile);

    } catch (error) {

        handleError(error);

    }

}

(二)错误处理机制

  • try/catch捕获整个异步块的错误

  • 单个await表达式可以用try/catch单独处理

  • 保持与 Promise 相同的错误传递机制

六、最佳实践与注意事项

(一)错误处理原则

  1. 始终添加catch处理程序或使用try/catch

  2. 避免未处理的 Promise 拒绝(可通过全局监听unhandledrejection事件)

  3. 在链式调用中保持错误处理的连续性

(二)性能优化

  1. 避免创建不必要的 Promise 对象

  2. 合理使用并行执行(Promise.all)替代顺序执行

  3. 注意内存管理,及时释放不再需要的 Promise 引用

(三)常见反模式


// 反模式:在then中返回非Promise值时未正确处理

somePromise.then(() => {

    return makeRequest(); // 假设makeRequest不是Promise

}).then(result => { /* 这里result会是undefined */ });



// 正确做法:确保返回Promise或有效值

somePromise.then(() => {

    return Promise.resolve(makeRequest());

}).then(result => { /* 正确获取结果 */ });

七、Promise 的浏览器兼容性

兼容性现状

  • 现代浏览器(Chrome, Firefox, Edge, Safari 10+)完全支持 Promise

  • IE 浏览器需要 polyfill(如 es6-promise 库)

八、总结

当然,现在前端卷成这样的环境下,掌握 Promise 早已不是前端开发者的加分项,很多时候它只是面试八股里的一道例题,但这不代表它不重要。

它不仅是一种技术实现,更代表着一套完整的异步思维模式 —— 当你能熟练运用状态管理、结果传递和流程控制这三大核心武器时,再复杂的异步难题也会迎刃而解。

Promise 的出现标志着 JavaScript 异步编程的成熟,它通过标准化的状态管理和链式调用机制,解决了回调地狱的难题。结合 async/await 语法糖,使得异步代码可以写成同步风格,极大提升了代码的可读性和可维护性。

建议大伙在实际项目中多使用 Promise 进行异步操作封装,遵循最佳实践,也愿诸位写出更优雅、更健壮的代码。

Flutter抓包

我们在做应用开发的时候,经常需要利用一些工具来抓取网络请求接口,这样可以极大的方便接口联调。在使用 Flutter 做应用开发时就需要用到 Charles 这个抓包工具。

1. 抓包工具 Charles

我们到Charles官网下载:

image.png

1.1 Charles 破解

Charles 是收费软件,首次打开会提示你,可以免费试用30天,但是每次使用时间不能超过30分钟,并且启动时将会有10秒的延时。

相关破解这里不好详述,读者可自己查询。

1.2. Charles 抓包设置

1.2.1 配置 Charles 根证书

首先打开 Charles,help -> SSL Proxying -> install charles root certificate

image.png

之后会弹出钥匙串,如果没有弹出,请自行打开,如下图:

image.png

系统默认是不信任 Charles 的证书的,此时对不信任的证书右键,在弹出的下拉菜单中选择『显示简介』,点击使用此证书时,把使用系统默认改为始终信任,如下图:

image.png

然后关掉当前弹框系统会提示了输入密码,输入或者指纹验证后,出现下图所示即说明 charles 的证书已经被信任了:

image.png

还有钥匙串的系统的证书也需要同上述一样的操作选择信任:

image.png

image.png

1.2.2 Proxy Settings 代理设置

设置代理暴露端口:

image.png

image.png

1.2.3 SSL Proxying Settings(https url 抓包设置)

image.png

image.png

一般来说 配置 *.80和 *.443就可以了,但是有的可能不是443 直接配置 *

image.png

配置*号,即可以抓取ssl/https所有端口信息,然后已经可以抓取mac端的包了。

1.2.4 其他配置

  1. 如果你想过滤掉无关自身需求的站点,只想看单独某个站点,最简便的做法就是直接在 Charles 左下角过滤框中输入单独想看的站点请求:

image.png

如果你想看某几个站点的,按照下面操作:

image.png

image.png

然后,你就可以在列表中查看到添加过的网址,想要继续添加别的站点继续点击 Add 即可。

image.png

  1. 修改服务器返回内容,mock 模拟假数据,勾选Enable Map Local ,这样和后端调试的时候就不用等后端接口都完成才能调试了,只要有数据格式即可,调试的时候大有用处。对于一些平常测试不到的情况,异常情况等都可以随意模拟修改,这个功能很实用。

image.png

image.png

只需要本地创建一个.json文件,写 json 数据和接口关联起来即可。

1.3 移动端配置

我们查看Charles的 help,按照下图所示来操作:

image.png

这里,我们可以看到一个弹框,告诉了你怎么给手机设置代理:

image.png

ps: 一定要让电脑和手机都在同一个局域网中(或是同一个WI-FI当中)

我这里以 iphone 为例,打开手机的设置,按照如下操作进行即可:

image.png

image.png

image.png

将上述 Charies 弹框中所列出的你的电脑的 ip 地址和端口号填写到下图:

image.png

配置完成后,Charles 会出现如下图所示弹窗,点击 Allow 允许即可:

image.png

然后打开手机上的safari浏览器输入 chls.pro/ssl 网址,会下载一个 pem 格式的文件,要将其后缀 pem 改为 crt。如果你的手机浏览器无法安装,也可以在电脑上进入该网页下载安装,然后通过隔空投送到iphone上。

image.png

image.png

点击安装,开始认证:

image.png

image.png

然后回到手机设置页面,点击通用 ——> 关于本机 ——> 证书信任设置:

image.png

至此,Charles 就可以抓包移动端接口数据了。

2. 配置 Flutter 代理

完成工具准备后,由于 Flutter 默认不走系统代理(Flutter 采用的是自己独立的网络栈,而非系统原生的网络 API,所以它不会自动使用系统代理设置),所以我们还需要手动在 Flutter 项目中配置代理,这样 Charles 才能正确抓到包。

这里提供两种方案,一种是在请求库的配置里设置代理,另一种是利用 Flutter 原生的类来完成。

2.1 http 请求库配置代理

2.1.1 dio 配置代理

我们希望有一个设置代理的页面,让开发者在页面上输入代理。这里我构建了一个简单点的设置代理的UI页面:

class ProxySettingPage extends StatefulWidget {
  const ProxySettingPage({Key? key}) : super(key: key);

  @override
  State<ProxySettingPage> createState() => _ProxySettingPageState();
}

class _ProxySettingPageState extends State<ProxySettingPage> {
  late bool _isOn; // 是否开启代理
  final TextEditingController _controller = TextEditingController(); // TextEditingController 实例,用于控制和获取 TextField 的内容

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: CustomNavigatorBar(title: 'PROXY'),
      body: Column(
        children: [
          TextField(
            controller: _controller,
            decoration: const InputDecoration(
              hintText: '172.168.0.1',
            ),
          ),
          CupertinoSwitch( // switch开关组件,用户可以切换代理的开关状态。当状态改变时,调用 _action 方法更新设置。
            value: _isOn,
            onChanged: _action
          )
        ],
      ),
    );
  }
}

初始化时,我们需要从 Cookies 中获取代理设置的状态和 IP 地址,并填入文本框:

@override
void initState() {
  super.initState();
  _isOn = Cookies.getBool('xx_proxy_on') ?? false;
  _controller.text = Cookies.getString('xx_proxy_ip') ?? '';
}

接下来开始写 _action 方法,处理代理开关状态的变化:

  1. 根据 isOn 的值,更新 Cookies 中的代理状态和 IP 地址。
  2. 在 Flutter 项目中通常用 dio 库做 http 请求,我们可以通过 dio 的httpClientAdapter属性配置我们的本地代理(DefaultHttpClientAdapter 提供了一个onHttpClientCreate 回调来设置底层 HttpClient的代理)。
  3. 更新 Http().dio 为新的 Dio 实例,并设置自定义代理配置。
_action(bool isOn) {
  _isOn = isOn;
  Cookies.putBool('xx_proxy_on', isOn);
  Cookies.putString('xx_proxy_ip', _controller.text);
  FocusManager.instance.primaryFocus?.unfocus();
  
  Dio oldDio = Http().dio; // 从 Http 服务中获取当前的 Dio 实例, 并将其保存到 oldDio 变量中
  Dio dio = Dio(oldDio.options); // 通过 oldDio.options 创建一个新的 Dio 实例, 它继承了之前的 Dio 配置,如超时时间、请求头等
  dio.interceptors.addAll(oldDio.interceptors); // 将旧 Dio 实例的所有拦截器添加到新的 Dio 实例中。这些拦截器可以用于请求和响应的拦截与处理,比如日志记录、错误处理
  (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (HttpClient client) { // 将新的 dio 实例的 httpClientAdapter 强制转换为 DefaultHttpClientAdapter,并设置 onHttpClientCreate 回调。这允许我们对创建的 HttpClient 进行自定义配置
    client.findProxy = (url) { // 在 findProxy 回调中,根据 isOn 的值决定是否启用代理
      if (!isOn) { // 如果 isOn 为 false,返回 'DIRECT',表示直接连接,不使用代理
        return 'DIRECT';
      }
      String ip = Cookies.getString('xx_proxy_ip') ?? '';
      return 'PROXY $ip:8888'; // 如果 isOn 为 true,从 Cookies 中获取代理 IP 地址,并返回 `PROXY $ip:8888`,表示使用指定的代理服务器和端口
    };
    client.userAgent = null; // 清除 userAgent 设置。如果之前有自定义的用户代理字符串,这里将其设置为 null
    client.badCertificateCallback = (X509Certificate cert, String host, int port) => true; // 设置一个回调函数来处理无效证书。在这里,所有的证书都被接受,无论其是否有效,这通常用于开发环境中测试,但不推荐在生产环境中使用
    return client;
  };
  Http().dio = dio; // 更新全局 Dio 实例。以后所有通过 Http().dio 进行的请求都会使用新的代理设置
  setState(() {});
}

其中client.findProxy函数用来返回我们的代理接口,Charles 的默认的系统代理端口是8888,所以这里配置成 PROXY $ip:8888 就可以了。

Flutter 请求 https 时用的是自己的CA认证证书,所以 Charles 在认证证书时会通不过证书校验,从而导致抓包错误。故而我们直接通过 client.badCertificateCallback 函数返回 true 来忽略证书就好了。

当然,在有些实际项目中,不会单独写个页面让开发者手填代理,而是将代理抽取到配置文件中,只要你在代码中手动添加/修改你的代理即可,如下图所示:

image.png

ps:一般情况下,开发者需要填写的代理都是个人电脑 ip 地址,我们除了可以通过在终端输入命令ipconfig来查看 ip 外,还可以通过 Charles 来查看,如图所示:

image.png

2.1.2 web_socket_channel 配置代理

如果你的项目中需要抓取 websocket, 那么在有了上述的 dio 配置后还不够,这时候我们的 websocketb 请求还是无法抓包,我们需要借助 web_socket_channel 这个库。

因为官方的 web_socket_channel 还不支持代理,所以我们不能直接从官网拉取这个库。我们可以改成以下方式进行安装:

dependencies:
  web_socket_channel:
      git:
        url: https://github.com/IFreeOvO/web_socket_channel.git
        ref: master

基于上述 dio 配置的思路,我们来开始配置 web_socket_channel

import 'package:web_socket_channel/io.dart';

// 创建一个自己的 HttpClient 对象
SecurityContext ctx = SecurityContext.defaultContext; // SecurityContext.defaultContext 返回默认的安全上下文 (SecurityContext) 实例。这个上下文用于配置与 SSL/TLS 相关的安全设置,比如证书验证
HttpClient client = HttpClient(context: ctx) // 创建一个 HttpClient 实例,并将之前获取的 SecurityContext (ctx) 传递给它。这使得 HttpClient 使用该上下文进行安全通信
  ..findProxy = ((url) { // 使用级联操作符 `..` 来设置 HttpClient 的 findProxy 属性
    String ip = Cookies.getString('xx_proxy_ip') ?? '';
    return 'PROXY $ip:8888';
  })
  ..badCertificateCallback = (cert, host, port) { // 使用级联操作符 `..` 设置 HttpClient 的 badCertificateCallback
  return true;
};

_channel = IOWebSocketChannel.connect( // 使用 IOWebSocketChannel 的 connect 方法创建一个 WebSocket 连接
  'wss://xxx.com',
  customClient: client, // 指定使用前面创建的自定义 HttpClient
);

2.2 重写原生方法

在入口文件main.dart中,我们需要定义一个HttpOverrides的子类,重写它的createHttpClient方法。原理也是一样的,把findProxybadCertificateCallback方法进行替换,然后挂载到全局。

// 重写 HttpOverrides
class MyHttpOverrides extends HttpOverrides {
  @override
  HttpClient createHttpClient(SecurityContext? context) {
    var http = super.createHttpClient(context);
    http.findProxy = (url) {
      return 'PROXY localhost:8888'; // 将 localhost 改成具体的 ip 地址
    };
    http.badCertificateCallback =
        (X509Certificate cert, String host, int port) => true;
    return http;
  }
}

void main() {
  HttpOverrides.global = MyHttpOverrides(); // 应用中的所有 HTTP 请求都使用自定义的 HttpClient 设置
  runApp(MyApp());
}

这种方案的好处是不受第三方请求库限制,配置完后httpswebsocket都能正确抓包。

一个树状结构的参数需求

基于react antd设计的树状参数需求,之前遇到过这个层级比较深的参数需求,类型与请求后端设计参数,类型可以选择数组对象数字字符串等

image.png

import React, { useEffect, useState, forwardRef, useImperativeHandle } from 'react';
import { Form, Select, Input, Row, Col, Button, Tree, Space } from 'antd';
import { PlusCircleOutlined, MinusCircleOutlined, PlusOutlined, DeleteOutlined } from '@ant-design/icons';
import styles from '../index.module.less';
import { cloneDeep } from 'lodash';

const ParamSet = forwardRef((props, ref) => {
  const { onChange, value } = props;
  const [treeData, setTreeData] = useState([
    {
      title: (key) => renderTreeNodesTitle(key),
      key: '0-0',
      isLeaf: false,
      name: 'root',
      type: 'object',
      desc: 'root',
      children: [],
    },
  ]);
  const addNode = (currNode) => {
    if (currNode.isLeaf) {
      currNode.isLeaf = false;
    }
    console.log('currNode', currNode);
    if (currNode.children && currNode.children.length > 0) {
      if (currNode.type === 'array') {
        const firstChild = currNode.children[0];
        currNode.children.push({
          title: (key) => renderTreeNodesTitle(key),
          key: `${currNode.key}-${Date.now().toString().slice(-5)}`,
          isLeaf: true,
          parentType: currNode.type,
          parentNode: currNode,
          name: '',
          type: firstChild.type,
          value: '',
          desc: '',
        });
      } else if (currNode.type === 'object') {
        currNode.children.push({
          title: (key) => renderTreeNodesTitle(key),
          key: `${currNode.key}-${Date.now().toString().slice(-5)}`,
          isLeaf: true,
          parentType: currNode.type,
          name: '',
          type: 'string',
          value: '',
          desc: '',
        });
      }
      console.log('treeData', treeData);
      setTreeData([...treeData]);
      return;
    }
    console.log('zhepaobulai');
    currNode.children = [
      {
        title: (key) => renderTreeNodesTitle(key),
        key: `${currNode.key}-${Date.now().toString().slice(-5)}`,
        isLeaf: true,
        parentType: currNode.type,
        parentNode: currNode,
        name: '',
        type: 'string',
        value: '',
        desc: '',
      },
    ];
    setTreeData([...treeData]);
  };
  const findNodes = (treeData, currNode) => {
    const res = treeData.filter((item) => {
      if (item.key !== currNode.key) {
        if (item.children?.length) {
          item.children = findNodes(item.children, currNode);
        }
        return true;
      } else {
        return false;
      }
    });
    return res;
  };
  const delNode = (currNode) => {
    const res = findNodes(treeData, currNode);
    setTreeData(res);
  };
  const changeParamName = (e, currNode) => {
    console.log(e.target.value);
    currNode.name = e.target.value;
    setTreeData([...treeData]);
  };
  const changeParamType = (e, currNode) => {
    currNode.type = e;
    if (e === 'object' || e === 'array') {
      currNode.children = null;
      currNode.isLeaf = false;
      currNode.value = '';
    } else {
      currNode.children = null;
      currNode.isLeaf = true;
    }
    if (currNode.parentType === 'array') {
      currNode.parentNode.children.forEach((item) => {
        item.type = currNode.type;
      });
    }
    setTreeData([...treeData]);
  };
  const changeParamValue = (e, currNode) => {
    currNode.value = e.target.value;
    setTreeData([...treeData]);
  };
  const changeParamDesc = (e, currNode) => {
    currNode.desc = e.target.value;
    setTreeData([...treeData]);
  };
  const renderTreeNodesTitle = (currNode) => {
    console.log('整棵树', treeData);
    return (
      <div key={currNode.key}>
        <Space size={3}>
          <Input placeholder="参数名" value={currNode.name} disabled={currNode.parentType === 'array'} onChange={(e) => changeParamName(e, currNode)} />
          <Select
            placeholder="参数类型"
            style={{ width: 120 }}
            value={currNode.type}
            onChange={(e) => changeParamType(e, currNode)}
            options={[
              { value: 'string', label: 'string' },
              { value: 'integer', label: 'integer' },
              { value: 'number', label: 'number' },
              { value: 'boolean', label: 'boolean' },
              { value: 'array', label: 'array' },
              { value: 'object', label: 'object' },
            ]}
          />
          <div style={{ width: '120px' }}>
            <Input
              placeholder="参数值"
              disabled={['object', 'array'].includes(currNode.type)}
              value={currNode.value}
              onChange={(e) => changeParamValue(e, currNode)}
            />
          </div>
          <div style={{ width: '120px' }}>
            <Input placeholder="备注" value={currNode.desc} onChange={(e) => changeParamDesc(e, currNode)} />
          </div>
          <div style={{ width: '10px' }}>
            {['object', 'array'].includes(currNode.type) && (
              <PlusCircleOutlined className={styles.headerIcon} onClick={() => addNode(currNode)} />
            )}
          </div>
          <div style={{ width: '10px' }}>
            {!currNode.children?.length && (
              <MinusCircleOutlined className={styles.headerIcon} onClick={() => delNode(currNode)} />
            )}
          </div>
        </Space>
      </div>
    );
  };

  const add = () => {};
  const formatTreeData = (treeData = []) => {
    const res = treeData.map((item) => {
      if (item.type === 'object' || item.type === 'array') {
        if (item.children?.length) {
          item.children = formatTreeData(item.children);
        }
        return {
          name: item.name,
          type: item.type,
          children: item.children,
          desc: item.desc,
        };
      }
      return {
        name: item.name,
        type: item.type,
        value: item.value,
        desc: item.desc,
      };
    });
    return res;
  };
  useEffect(() => {
    const copyData = cloneDeep(treeData);
    const resTreeData = formatTreeData(copyData);
    onChange(resTreeData);
  }, [treeData]);
  useImperativeHandle(ref, () => ({
    getParams: (newParams) => {
      return treeData;
    },
  }));
  return (
    <div>
      <Tree autoExpandParent={true} defaultExpandAll={true} treeData={treeData} />
      {/* <Row>
        <Col span={14}>
          <Button type="dashed" onClick={() => add()} icon={<PlusOutlined />}>
            添加参数信息
          </Button>
        </Col>
      </Row> */}
    </div>
  );
});
export default ParamSet;



前端部署-docker

linux 基本命令

在 Linux 系统中,掌握基本命令是高效操作和管理的关键。以下是常用的 Linux 基本命令分类及说明,涵盖文件管理、系统信息、权限控制、网络操作等核心场景:


一、文件与目录管理

命令 示例 说明
ls ls -l /home 列出目录内容,-l 显示详细信息(权限、所有者等)
cd cd /var/log 切换目录,cd ~ 返回用户主目录
pwd pwd 显示当前工作目录的绝对路径
mkdir mkdir new_folder 创建新目录
rm rm file.txt 删除文件,-r 递归删除目录(如 rm -r dir/
cp cp file.txt /backup/ 复制文件或目录(加 -r 复制目录)
mv mv old.txt new.txt 重命名或移动文件/目录
touch touch newfile.txt 创建空文件或更新文件时间戳
find find /home -name "*.log" 按名称搜索文件
grep grep "error" log.txt 在文件中搜索文本内容

二、文件内容查看与编辑

命令 示例 说明
cat cat file.txt 查看整个文件内容
less/more less largefile.log 分页查看文件(支持上下滚动)
head/tail tail -f /var/log/syslog 查看文件开头/末尾内容(-f 实时跟踪日志)
vim/nano vim config.conf 文本编辑器(nano 更简单,适合新手)

三、系统信息与进程管理

命令 示例 说明
df df -h 查看磁盘空间使用情况(-h 人性化显示)
du du -sh /home 查看目录占用空间(-s 汇总,-h 人性化)
top/htop htop 动态查看进程和资源占用(htop 更直观)
ps ps aux 显示当前进程状态
kill kill -9 1234 终止进程(-9 强制终止)
free free -m 查看内存使用情况(-m 以 MB 为单位)
uname uname -a 显示系统信息(内核版本、主机名等)

四、用户与权限管理

命令 示例 说明
sudo sudo apt update 以超级用户权限执行命令
su su - username 切换用户(- 加载目标用户环境)
chmod chmod 755 script.sh 修改文件权限(755 表示所有者可读写执行,其他用户可读执行)
chown chown user:group file.txt 修改文件所有者或组
passwd passwd 修改当前用户密码

五、网络管理

命令 示例 说明
ifconfig/ip ip a 查看网络接口信息(ifconfig 已逐渐被 ip 替代)
ping ping google.com 测试网络连通性
netstat/ss ss -tulnp 查看端口监听状态(ss 更高效)
curl/wget curl -I https://example.com 下载文件或测试 HTTP 请求
ssh ssh user@192.168.1.100 远程登录服务器

六、压缩与归档

命令 示例 说明
tar tar -czvf archive.tar.gz dir/ 压缩目录为 .tar.gz 文件(-c 创建,-z gzip 压缩,-v 显示过程,-f 指定文件名)
unzip unzip file.zip 解压 ZIP 文件

七、系统服务管理

命令 示例 说明
systemctl systemctl start nginx 启动服务(适用于 Systemd 系统,如 Ubuntu 16.04+/CentOS 7+)
service service nginx restart 旧版系统(如 Ubuntu 14.04)的服务管理命令
journalctl journalctl -u nginx 查看服务日志(Systemd 系统)

八、其他实用命令

命令 示例 说明
crontab crontab -e 编辑定时任务(如每天备份数据库)
alias alias ll='ls -l' 创建命令别名(临时生效,写入 ~/.bashrc 可永久生效)
history history 查看命令历史记录
export export PATH=$PATH:/new/path 设置环境变量

Mac 连接远程服务器

juejin.cn/post/719964…

www.terminal.icu/

安装 docker

juejin.cn/post/713167…

安装Docker Compose

1、下载Docker Compose二进制文件

DOCKER_COMPOSE_VERSION=v2.27.1
sudo curl -L "https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

2、赋予可执行权限

  • 下载完成后,为Docker Compose文件赋予可执行权限:
sudo chmod +x /usr/local/bin/docker-compose

3、创建符号链接

  • docker-compose命令链接到/usr/bin目录,以便全局访问:
sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose

4、验证安装

  • 运行以下命令查看Docker Compose版本信息:
docker-compose --version
  • 如果安装成功,将显示Docker Compose的版本号。

docker设置阿里云镜像

image.png

image.png

运行命令:mkdir -p /etc/docker

重启服务器: systemctl restart docker

docker 基础

一、基本概念和组成

将代码环境统一绑定,实现了跨平台、跨服务器。只需要一次配置好环境,换到别的机子上就可以一键部署好,大大简化了操作。

1、镜像(image)

Docker 镜像(Image)就是一个只读的模板。镜像可以用来创建 Docker 容器,一个镜像可以创建很多容器。相当于js中的一个基础的工具类,可以创建出多个容器提供使用。

镜像信息:

image.png

列名 含义 示例值
REPOSITORY 镜像所属的仓库名称(标识镜像来源)。 ubuntunginxmy-app
TAG 镜像的标签(版本号),默认为 latest 20.04latestv1.0
IMAGE ID 镜像的唯一标识符(前12位哈希值,完整 ID 可通过 docker inspect 查看)。 a1b2c3d4e5f6
CREATED 镜像的创建时间(相对于当前时间的时长或具体日期)。 2 weeks ago2024-01-01
SIZE 镜像的大小(压缩后的存储占用空间)。 72.9MB142MB

2、容器(container)

Docker 利用容器(Container)独立运行的一个或一组应用,应用程序或服务运行在容器里面,容器就类似于一个虚拟化的运行环境,容器是用镜像创建的运行实例。容器为镜像提供了一个标准的和隔离的运行环境,它可以被启动、开始、停止、删除。每个容器都是相互隔离的、保证安全的平台。

容器信息:

image.png

列名 含义 示例值
CONTAINER ID 容器的唯一标识符(缩写,完整 ID 可通过 docker ps --no-trunc 查看)。 a1b2c3d4e5f6
IMAGE 容器基于的镜像名称(与 docker images 中的 REPOSITORY 列一致)。 nginx:latest
COMMAND 容器启动时执行的命令(默认是镜像的 ENTRYPOINT 或 CMD)。 "/docker-entrypoint.…"
CREATED 容器创建后的运行时间(格式为 时长 或 YYYY-MM-DD HH:MM:SS)。 2 minutes ago
STATUS 容器的当前状态(如运行中、暂停、退出等)。 Up 2 minutes
PORTS 容器暴露的端口映射(格式为 主机端口:容器端口 或 容器端口)。 0.0.0.0:8080->80/tcp
NAMES 容器的随机名称(可通过 --name 参数自定义)。 hopeful_swanson

3、仓库(repository)

仓库(Repository)是集中存放镜像文件的场所。类似于GitHub的一个公有或私有的仓库。用户可根据需求获取不同的镜像使用。

二、docker 命令

1、docker 运行

功能 命令
启动docker systemctl start docker
停止docker systemctl stop docker
重启docker systemctl restart docker
查看docker状态 systemctl status docker
设置docker开机启动 systemctl enable docker
查看docker概要信息 docker info
查看docker总体帮助文档 docker --help

2、docker 镜像

功能 命令
查看本地主机上的镜像 docker images
搜索远端镜像仓库镜像 docker search 镜像名字
获取镜像到本地 docker pull 镜像名字[:TAG]
获取最新镜像到本地 docker pull 镜像名字:latest
删除单个镜像 docker rmi -f 镜像ID
删除多个镜像 docker rmi -f 镜像名1:TAG 镜像名2:TAG
删除全部镜像 docker rmi -f $(docker images -qa)
删除所有的虚悬镜像 docker image prune

3、docker 容器

功能 命令 举例
启动容器 docker run 镜像名[:TAG] docker run nginx:latest
启动并设置镜像名称 docker run --name 自定义名称 镜像名[:TAG] docker run --name test_niginx nginx:latest
启动并后台运行 docker run -d 镜像名[:TAG] docker run -d nginx
启动并设置映射端口 docker run -p 对外端口:镜像暴露端口 镜像名[:TAG] docker run -p 8888:80 nginx
复合命令 docker run --name test_nginx -d -p 8888:80 nginx
查看全部的容器 dock ps
停止容器 docker stop [CONTAINER ID/IMAGE/NAMES]

4、docker 交互式容器【可编辑文件】

例如:docker run -it centos /bin/bash

参数说明:

-i: 交互式操作。

-t: 终端。

centos : centos 镜像。

/bin/bash:放在镜像名后的是命令,这里我们希望有个交互式 Shell,因此用的是 /bin/bash。

要退出终端,直接输入 exit:

退出容器操作:

两种退出方式

exit

run进去容器,exit退出,容器停止

ctrl+p+q

run进去容器,ctrl+p+q退出,容器不停止

三、dockerFile

是什么?

Dockerfile是用来构建Docker镜像的文本文件,是由一条条构建镜像所需的指令和参数构成的脚本。

为什么?

由于dockerHub中的镜像为通用的镜像,大多数都不满足我们项目的docker镜像的要求。那么就需要自定义项目docker镜像,从而实现项目不同系统环境的部署运行。

怎么做?

    1. 每条保留字指令都必须为大写字母且后面要跟随至少一个参数
    1. 指令按照从上到下,顺序执行
    1. #表示注释
    1. 每条指令都会创建一个新的镜像层并对镜像进行提交

DockerFile常用保留字指令

参考tomcat8的dockerfile入门

github.com/docker-libr…

指令 用途 关键注意事项
FROM 指定基础镜像 必须为第一条指令
LABEL 添加元数据 替代废弃的 MAINTAINER
ENV/ARG 设置环境变量/构建参数 ARG 仅构建阶段有效
WORKDIR 设置工作目录 推荐绝对路径
COPY/ADD 复制文件到镜像 优先用 COPY(除非需解压或下载)
RUN 执行构建命令 合并命令减少层数
EXPOSE 声明容器端口 需配合 docker run -p 使用
CMD 默认启动命令 可被 docker run 覆盖
ENTRYPOINT 固定启动命令 与 CMD 配合传递参数
HEALTHCHECK 容器健康检查 定义服务可用性检查
多阶段 FROM 优化镜像体积 分离构建和运行环境

镜像制作

docker build [选项] -t <镜像名称>:<标签> <构建上下文路径>

docker build -t my-nginx:latest .

简单示例

文件目录

image.png

nginx.conf

server {
    listen 80;
    server_name localhost;
    location / {
        root /usr/share/nginx/html;
        index index.html index.htm;
        try_files $uri $uri/ /index.html;
    }
}

dockerFile 配置

# 使用官方的nginx镜像作为父镜像
FROM nginx:alpine
 
# 删除默认的nginx网站配置文件
RUN rm /etc/nginx/conf.d/default.conf
 
# 复制构建后的前端文件到nginx的html目录
COPY ./dist /usr/share/nginx/html
 
# 复制nginx配置文件到容器中
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
 
# 暴露80端口
EXPOSE 80
 
# 启动nginx服务
CMD ["nginx", "-g", "daemon off;"]

制作镜像

docker build -t fe-dev:latest .

image.png

image.png

ERROR: Permission to Splode/pomotroid.git deni

在向分支 feat/about-page上传代码时

git push -u origin feat/about-page

出现ERROR: Permission to Splode/pomotroid.git deni错误

原因:- 你用的账号是 eleven-h,但这个仓库属于用户 Splode- 你没有权限向这个仓库推送代码。

解决:切换远程仓库为自己的 Fork

我意识到问题后,删除了原来的远程仓库配置,并添加了自己的 Fork:

git remote remove origin
git remote add origin git@github.com:Liar0320/pomotroid.git
  • 现在远程仓库变成了我自己的 Fork:Liar0320/pomotroid.git

接着我拉取了远程信息:

git fetch
From github.com:Liar0320/pomotroid
* [new branch] feat/about-page -> origin/feat/about-page
* [new branch] feat/exercise-reminder ...
  • 成功获取了远程仓库的所有分支信息。

📤 推送本地分支到自己 Fork 的远程仓库

此时我再次查看本地分支:

git branch
* feat/about-page
feat/main-view-redesign
feat/settings-interaction
feat/timer-settings-layout
master

提示当前分支没有设置上游分支:

git push
fatal: The current branch feat/about-page has no upstream branch.

于是我设置了上游分支并推送成功:

git push --set-upstream origin feat/about-page
Enumerating objects: 96, done.

...

Branch 'feat/about-page' set up to track remote branch 'feat/about-page' from 'origin'.
  • 这表示我已经成功将本地的 feat/about-page 分支推送到远程,并建立了跟踪关系。
  • 以后可以直接使用 git pushgit pull 操作该分支。

🧾 最后查看提交历史

git log

开箱即用的 Vue3 无限平滑滚动组件

Vue3 无限平滑滚动组件

这是一款开箱即用的 Vue3 无限平滑滚动组件,支持四个方向(左、右、上、下)自由切换,具有良好的可配置性。组件默认开启鼠标悬停暂停和滚轮滚动交互,滚动过程自然平滑,可用于新闻轮播、公告栏、商品展示等场景。通过 slidesPerView 控制一次显示的条目数,itemGap 控制每项之间的间距,支持自动复制内容避免滚动断层。使用简单,插槽式内容传入,代码结构清晰,适合二次封装或项目直接使用。本文贴出完整实现代码,拷贝即可使用。

先看效果

(gif不清晰,大概看看是这么个意思)

7月10日.gif

话不多说,直接贴代码

<template>
  <div class="scroll-wrapper" ref="wrapper">
    <div class="scroll-content" ref="scrollWrapper">
      <slot />
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'

const props = defineProps({
  direction: {
    type: String,
    default: 'left',
    validator: (val) => ['left', 'right', 'up', 'down'].includes(val),
  },
  step: {
    type: Number,
    default: 1, // 每帧滚动多少像素
  },
  pauseOnHover: {
    type: Boolean,
    default: true,
  },
  scrollOnWheel: {
    type: Boolean,
    default: true, //监听鼠标滚动的事件
  },
  slidesPerView: {
    type: Number,
    default: 1, // 每次显示几条
  },
  itemGap: {
    type: Number,
    default: 2, // 每项之间的间距(单位 px)
  },
})

const wrapper = ref(null)
const scrollWrapper = ref(null)

let isHovering = false
let scrollAmount = 0
let animationFrameId = 0
const COPYNUM = 2

// copy一份内容,防止循环一半切换时视觉空白
const duplicateContent = () => {
  const originalChildren = Array.from(scrollWrapper.value.children)
  // 先清空可能存在的克隆元素
  const clones = scrollWrapper.value.querySelectorAll('.cloned')
  clones.forEach((clone) => clone.remove())

  // 复制内容并插入到前面
  originalChildren.forEach((child) => {
    const clone = child.cloneNode(true)
    clone.classList.add('cloned')
    scrollWrapper.value.appendChild(clone)
  })
}

// 边界情况处理,尤其在鼠标滚动的时候
const resetIfOutOfBounds = () => {
  const sw = scrollWrapper.value
  const horizontal = ['left', 'right'].includes(props.direction)
  const scrollSize = horizontal ? sw.scrollWidth : sw.scrollHeight
  const clientSize = horizontal ? sw.clientWidth : sw.clientHeight
  const halfLimit = scrollSize / COPYNUM
  const maxScroll = scrollSize - clientSize
  if (['left', 'up'].includes(props.direction)) {
    if (scrollAmount >= halfLimit || scrollAmount <= 0) {
      scrollAmount = 0
    }
  } else {
    const minScroll = maxScroll - halfLimit
    if (scrollAmount >= maxScroll || scrollAmount <= minScroll) {
      scrollAmount = maxScroll
    }
  }
}
const scroll = () => {
  if (!(props.pauseOnHover && isHovering)) {
    if (['left', 'up'].includes(props.direction)) {
      scrollAmount += props.step
    } else {
      scrollAmount -= props.step
    }
  }
  if (props.direction === 'left' || props.direction === 'right') {
    scrollWrapper.value.style.transform = `translateX(${-scrollAmount}px)`
  } else {
    scrollWrapper.value.style.transform = `translateY(${-scrollAmount}px)`
  }
  resetIfOutOfBounds()
  animationFrameId = requestAnimationFrame(scroll)
}

const onMouseEnter = () => (isHovering = true)
const onMouseLeave = () => (isHovering = false)

const onWheel = (e) => {
  if (!props.scrollOnWheel) return
  e.preventDefault()
  const delta = e.deltaY || e.deltaX
  scrollAmount += delta
}

const setItemSize = () => {
  const wrapperSize =
    props.direction === 'left' || props.direction === 'right'
      ? wrapper.value.clientWidth
      : wrapper.value.clientHeight
  const itemSize = (wrapperSize - props.itemGap * (props.slidesPerView - 1)) / props.slidesPerView

  const isVertical = ['up', 'down'].includes(props.direction)
  scrollWrapper.value.style.flexDirection = isVertical ? 'column' : 'row'
  scrollWrapper.value.style.gap = props.itemGap + 'px'

  const items = Array.from(scrollWrapper.value.children).filter(
    (el) => !el.classList.contains('cloned'),
  )
  Array.from(items).forEach((el) => {
    if (isVertical) {
      el.style.height = itemSize + 'px'
    } else {
      el.style.width = itemSize + 'px'
    }
  })
}
const setInitialScrollAmount = () => {
  if (props.direction === 'left') {
    scrollAmount = 0
  } else if (props.direction === 'up') {
    scrollAmount = 0
  } else if (props.direction === 'down') {
    scrollAmount = scrollWrapper.value.scrollHeight - scrollWrapper.value.clientHeight
  } else if (props.direction === 'right') {
    scrollAmount = scrollWrapper.value.scrollWidth - scrollWrapper.value.clientWidth
  }
}

onMounted(() => {
  nextTick(() => {
    setItemSize()
    duplicateContent()
    setInitialScrollAmount()
    wrapper.value.addEventListener('mouseenter', onMouseEnter)
    wrapper.value.addEventListener('mouseleave', onMouseLeave)
    if (props.scrollOnWheel) {
      wrapper.value.addEventListener('wheel', onWheel, { passive: false })
    }
    animationFrameId = requestAnimationFrame(scroll)
  })
})

onBeforeUnmount(() => {
  cancelAnimationFrame(animationFrameId)
  wrapper.value.removeEventListener('mouseenter', onMouseEnter)
  wrapper.value.removeEventListener('mouseleave', onMouseLeave)
  if (props.scrollOnWheel) {
    wrapper.value.removeEventListener('wheel', onWheel)
  }
})
</script>

<style scoped>
.scroll-wrapper {
  overflow: hidden;
  width: 100%;
  height: 100%;
}

.scroll-content {
  display: flex;
  width: 100%;
  height: 100%;
}
.scroll-content > * {
  flex-shrink: 0;
}
</style>

使用

<SeamlessScroll direction="up" :slidesPerView="4">
    <div class="upload-item" v-for="(item, index) in uploadList" :key="index">
      <div class="left-wrap">
        <SvgIcon
          name="pdf"
          :color="statusColorMap[item.status]?.color || '#157EFB'"
          size="23px"
          class="pdf-icon"
        />
        <p class="title">{{ item.title }}</p>
      </div>
      <span>{{ item.size }}</span>
      <span>{{ item.time }}</span>
      <p class="status" :style="{ color: statusColorMap[item.status]?.color }">
        {{ item.statusText }}
      </p>
    </div>
 </SeamlessScroll>

从区块链基础到DApp开发

一、Web技术演进

代际 特点 后端技术 用户权限
Web1.0 静态网页,信息只读 传统数据库 被动接收
Web2.0 用户生成内容,社交平台 对象存储/云服务 数据被平台控制
Web3.0 去中心化,用户拥有数据 区块链+智能合约 完全自主控制

二、区块链基础架构

1. 去中心化账本机制

区块链本质上是分布式账本技术,采用全网节点共同维护的数据库模型。交易数据被打包成"区块",按时间顺序连接成"链",实现不可篡改性:任何数据修改都会导致后续区块哈希值变化,立即触发系统警报。

2. 密码学账号体系

组件 功能说明 安全要点
地址(公钥) 公开收款标识(如1A1zP1...) 类似银行账号,可公开分享
私钥 64位16进制字符串 相当于银行卡密码,丢失即永久丧失资产控制权

关键口诀谁掌握私钥,谁控制资产! 私钥泄露会导致资产被盗(如邮箱密码被盗)。

三、主流区块链平台对比

比特币(Bitcoin)

  • 定位:全球第一种加密货币,数字黄金,总量2100万枚固定供应
  • 核心功能:点对点支付,无需银行中转(A→B直接转账)
  • 特点
    • 交易匿名性
    • 跨国转账快速
    • 手续费低(但价格波动大)

以太坊(Ethereum)

  • 定位可编程的世界计算机,支持智能合约开发
  • 燃料机制:所有操作消耗ETH作为Gas费
  • 生态应用:NFT、DeFi等创新场景
graph LR
  A[比特币] -->|价值存储| B[数字黄金]
  C[以太坊] -->|智能合约| D[DeFi/NFT/DAO]

四、核心组件解析

1. 加密钱包

  • 本质:私钥管理工具(不存储实际资产
  • 类型对比
    类型 安全性 便捷性 代表产品
    硬件钱包 ★★★★★ ★★☆☆☆ Ledger, Trezor
    软件钱包 ★★★☆☆ ★★★★☆ MetaMask
    交易所托管 ★★☆☆☆ ★★★★★ 币安, 欧易

2. 智能合约

  • 定义:自动执行的数字协议(如自动售货机)
  • 核心价值
    • 消除中间商(律师/公证)
    • 规则透明不可篡改
  • 风险:代码漏洞可能导致重大损失(如2016年DAO事件)
// 简单支付合约示例
pragma solidity ^0.8.0;
contract Payment {
    function payRent(address landlord) public payable {
        payable(landlord).transfer(msg.value); // 每月自动转账
    }
}

五、DApp开发体系

1. 技术架构

  • 前端:React/Vue + HTML/CSS/JavaScript
  • 通信层:Web3.js/Ethers.js连接合约
  • 后端:Solidity智能合约(运行于EVM)
  • 存储:IPFS等去中心化存储

2. 开发工具链

类别 工具 功能说明
开发环境 Remix/Hardhat Solidity编写与测试
节点服务 Alchemy Web3版AWS,提供API服务
监控工具 Moralis 链上警报与数据同步

3. DApp开发流程

  1. 需求分析 → 2. 智能合约开发 → 3. 前端集成 → 4. 测试网验证 → 5. 主网部署

六、应用场景与安全实践

创新应用场景

  • DeFi:去中心化借贷与交易
  • NFT市场:数字资产确权与交易
  • GameFi:区块链游戏经济系统
  • DAO:去中心化自治组织

安全最佳实践

  1. 私钥管理
    • 不截图/不云存储/不明文传输
    • 采用硬件钱包存储大额资产
  2. 合约审计
    • 形式化验证工具使用
    • 多重签名机制实施
  3. 资产策略
    • 冷热分离(冷钱包>95%资产)
    • 定期更换活跃地址

深入理解 React 中 useEffect 的 cleanUp 机制

深入理解 React 中 useEffect 的 cleanUp 机制

在 React 的函数组件中,useEffect 是一个非常强大的 Hook,用于处理副作用(如数据获取、订阅或手动更改 DOM)。然而,很多人可能并不清楚 useEffect 内部是如何处理副作用的清理(cleanUp)逻辑的。本文将深入探讨 useEffect 的 cleanUp 机制,帮助你更好地理解 React 的内部实现。

一、useEffect 的基本用法

在 React 中,useEffect 是一个用于处理副作用的 Hook。它可以接受一个回调函数和一个依赖数组(deps),当依赖数组中的值发生变化时,回调函数会被重新执行。同时,useEffect 还支持返回一个清理函数(cleanUp),用于在组件卸载或副作用更新前清理之前的副作用。

例如,以下是一个简单的 useEffect 使用示例:

import React, { useState, useEffect } from 'react';

function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('init');
    return () => {
      console.log('cleanup 0');
    };
  }, []);

  useEffect(() => {
    console.log('update1', count);
    return () => {
      console.log('cleanup 1');
    };
  }, [count]);

  useEffect(() => {
    console.log('update2', count);
    return () => {
      console.log('cleanup 2');
    };
  }, [count]);

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

export default App;

在这个例子中,我们定义了三个 useEffect

  1. 第一个 useEffect 的依赖数组为空([]),因此它只会在组件挂载时执行一次。
  2. 第二个和第三个 useEffect 的依赖数组包含 count,因此每当 count 的值发生变化时,它们都会重新执行。

每个 useEffect 都返回了一个清理函数(cleanUp),用于在副作用更新或组件卸载时执行清理操作。

二、cleanUp 的存放与调用

React 内部通过一个复杂的机制来管理 useEffect 的副作用和清理函数。以下是一个简化版的代码示例,展示了如何在 React 的内部实现中处理 useEffect 的 cleanUp 逻辑:

function commitEffectHooks() {
  function run(fiber) {
    if (!fiber) {
      return;
    }

    if (!fiber.alternate) {
      // 挂载阶段
      fiber?.effectHooks?.forEach((hook) => {
        hook.cleanup = hook.callback();
      });
    } else {
      // 更新阶段
      fiber.effectHooks?.forEach((newHook, index) => {
        if (newHook.deps.length > 0) {
          const oldEffectHook = fiber.alternate?.effectHooks[index];

          const needUpdate = oldEffectHook?.deps.some((oldDep, i) => {
            return oldDep !== newHook.deps[i];
          });

          if (needUpdate) {
            newHook.cleanup = newHook.callback();
          }
        }
      });
    }

    run(fiber.child);
    run(fiber.sibling);
  }

  function runCleanup(fiber) {
    if (!fiber) return;
    fiber.alternate?.effectHooks?.forEach((hook) => {
      if (hook.deps.length > 0) {
        hook.cleanup && hook.cleanup();
      }
    });
    runCleanup(fiber.child);
    runCleanup(fiber.sibling);
  }

  runCleanup(wipRoot);
  run(wipFiber);
}

三、cleanUp 的调用时机

从上述代码中可以看出,cleanUp 的调用时机主要有以下几种情况:

  1. 组件卸载时:在组件卸载时,React 会调用 runCleanup 函数,清理所有副作用。此时,无论依赖数组是否为空,cleanUp 都会被调用。
  2. 依赖数组变化时:在更新阶段,如果依赖数组中的值发生变化,React 会调用当前副作用的回调函数,并在调用之前执行上一次副作用的 cleanUp 函数。
  3. 依赖数组为空时:如果依赖数组为空([]),副作用只会执行一次,不会触发 cleanUp。这是因为依赖数组为空的副作用被视为“只在挂载时执行”的副作用。

四、处理依赖为空的情况

在某些情况下,我们可能需要显式地处理依赖数组为空的情况。例如,我们可以在 runCleanup 函数中添加一个额外的条件,确保即使依赖数组为空,cleanUp 也不会被调用:

function runCleanup(fiber) {
  if (!fiber) return;
  fiber.alternate?.effectHooks?.forEach((hook) => {
    if (hook.deps.length > 0) {
      hook.cleanup && hook.cleanup();
    }
  });
  runCleanup(fiber.child);
  runCleanup(fiber.sibling);
}

通过这种方式,我们可以确保只有依赖数组不为空时,cleanUp 才会被调用。

五、总结

通过本文的介绍,我们深入探讨了 React 中 useEffect 的 cleanUp 机制。useEffect 的 cleanUp 逻辑是 React 内部实现的关键部分,它确保了副作用的正确清理和更新。理解这些机制可以帮助我们更好地使用 useEffect,避免常见的副作用问题。

pnpm之monorepo项目, vite版本冲突, 导致vite.config.ts ts警告处理

在 monorepo 项目架构下,不同的仓库可能安装的 vite 版本是不同的, 有一些历史的老项目, 随着业务的变化,也有新的项目使用 最新的 vite@7.x 等初始化项目

比如, 我们可能在下面的代码片段中会出现 如下的 ts警告


const loadEnv = (mode: string) => {
    const envFiles = ["./env/.env", `./env/.env.${mode}`];

    const env: any = {};
    envFiles.forEach(envFile => {
        const loadedEnv = dotenv.config({ path: envFile, override: true });
        if (loadedEnv.error) {
            console.error(`环境变量文件加载失败 ${envFile}`);
        } else {
            Object.assign(env, loadedEnv.parsed || {});
        }
    });

    return env;
};

export default defineConfig(({ mode }: { mode: string }) => {
    console.log('当前环境:', mode);

    const globalEnv = loadEnv(mode)
    
     return {
      ...
     }
     
});

mode 的类型就会报错。

image.png

没有与此调用匹配的重载。  
最后一个重载给出了以下错误。  
类型“({ mode }: ConfigEnv) => { define: { "process.env": Record<string, string>; }; plugins: (PluginOption | Plugin<any> | Plugin<any>[])[]; base: string; ... 4 more ...; css: { ...; }; }”的参数不能赋给类型“UserConfigExport”的参数。  
不能将类型“({ mode }: ConfigEnv) => { define: { "process.env": Record<string, string>; }; plugins: (PluginOption | Plugin<any> | Plugin<any>[])[]; base: string; ... 4 more ...; css: { ...; }; }”分配给类型“UserConfigFnObject”。  
调用签名返回类型 "{ define: { "process.env": Record<string, string>; }; plugins: (PluginOption | Plugin<any> | Plugin<any>[])[]; base: string; resolve: { alias: { "@": string; }; }; build: { ...; }; esbuild: { ...; }; server: { ...; }; css: { ...; }; }""UserConfig" 不兼容。  
在这些类型中,"plugins" 的类型不兼容。  
不能将类型“(PluginOption | Plugin<any> | Plugin<any>[])[]”分配给类型“PluginOption[]”。  
不能将类型“PluginOption | Plugin<any> | Plugin<any>[]”分配给类型“PluginOption”。  
不能将类型“Plugin<any>”分配给类型“PluginOption”。  
不能将类型“import("D:/work/test-web/node_modules/.pnpm/[vite@5.4.1]()9_@[types+node@24._c928461cc00da890efe4f1ccdfab0111]()/node_modules/vite/dist/node/index").Plugin<any>”分配给类型“import("D:/work/test-web/node_modules/.pnpm/[vite@6.3]().5_@[types+node@24.0_3221e9d8a7877bfd9756da51f493fce4]()/node_modules/vite/dist/node/index").Plugin<any>”。  
属性“apply”的类型不兼容。  
不能将类型“"build" | "serve" | ((this: void, config: import("D:/work/test-web/node_modules/.pnpm/[vite@5.4.1]()9_@[types+node@24._c928461cc00da890efe4f1ccdfab0111]()/node_modules/vite/dist/node/index").UserConfig, env: import("D:/work/test-web/node_modules/.pnpm/[vite@5.4.1]()9_@[types+node@24._c928461cc00da89]()...”分配给类型“"build" | "serve" | ((this: void, config: import("D:/work/test-web/node_modules/.pnpm/[vite@6.3]().5_@[types+node@24.0_3221e9d8a7877bfd9756da51f493fce4]()/node_modules/vite/dist/node/index").UserConfig, env: import("D:/work/test-web/node_modules/.pnpm/[vite@6.3]().5_@[types+node@24.0_3221e9d8a7877bf]()...”。  
不能将类型“(this: void, config: UserConfig, env: ConfigEnv) => boolean”分配给类型“"build" | "serve" | ((this: void, config: UserConfig, env: ConfigEnv) => boolean)”。  
不能将类型“(this: void, config: import("D:/work/test-web/node_modules/.pnpm/[vite@5.4.1]()9_@[types+node@24._c928461cc00da890efe4f1ccdfab0111]()/node_modules/vite/dist/node/index").UserConfig, env: import("D:/work/test-web/node_modules/.pnpm/[vite@5.4.1]()9_@[types+node@24._c928461cc00da890efe4f1ccdfab0111]()/nod...”分配给类型“(this: void, config: import("D:/work/test-web/node_modules/.pnpm/[vite@6.3]().5_@[types+node@24.0_3221e9d8a7877bfd9756da51f493fce4]()/node_modules/vite/dist/node/index").UserConfig, env: import("D:/work/test-web/node_modules/.pnpm/[vite@6.3]().5_@[types+node@24.0_3221e9d8a7877bfd9756da51f493fce4]()/nod...”。  
参数“config”和“config” 的类型不兼容。  
不能将类型“import("D:/work/test-web/node_modules/.pnpm/[vite@6.3]().5_@[types+node@24.0_3221e9d8a7877bfd9756da51f493fce4]()/node_modules/vite/dist/node/index").UserConfig”分配给类型“import("D:/work/test-web/node_modules/.pnpm/[vite@5.4.1]()9_@[types+node@24._c928461cc00da890efe4f1ccdfab0111]()/node_modules/vite/dist/node/index").UserConfig”。  
属性“plugins”的类型不兼容。  
不能将类型“import("D:/work/test-web/node_modules/.pnpm/[vite@6.3]().5_@[types+node@24.0_3221e9d8a7877bfd9756da51f493fce4]()/node_modules/vite/dist/node/index").PluginOption[]”分配给类型“import("D:/work/test-web/node_modules/.pnpm/[vite@5.4.1]()9_@[types+node@24._c928461cc00da890efe4f1ccdfab0111]()/node_modules/vite/dist/node/index").PluginOption[]”。  
不能将类型“import("D:/work/test-web/node_modules/.pnpm/[vite@6.3]().5_@[types+node@24.0_3221e9d8a7877bfd9756da51f493fce4]()/node_modules/vite/dist/node/index").PluginOption”分配给类型“import("D:/work/test-web/node_modules/.pnpm/[vite@5.4.1]()9_@[types+node@24._c928461cc00da890efe4f1ccdfab0111]()/node_modules/vite/dist/node/index").PluginOption”。  
不能将类型“Plugin<any>”分配给类型“PluginOption”。  
不能将类型“import("D:/work/test-web/node_modules/.pnpm/[vite@6.3]().5_@[types+node@24.0_3221e9d8a7877bfd9756da51f493fce4]()/node_modules/vite/dist/node/index").Plugin<any>”分配给类型“import("D:/work/test-web/node_modules/.pnpm/[vite@5.4.1]()9_@[types+node@24._c928461cc00da890efe4f1ccdfab0111]()/node_modules/vite/dist/node/index").Plugin<any>”。  
属性“apply”的类型不兼容。  
不能将类型“"build" | "serve" | ((this: void, config: import("D:/work/test-web/node_modules/.pnpm/[vite@6.3]().5_@[types+node@24.0_3221e9d8a7877bfd9756da51f493fce4]()/node_modules/vite/dist/node/index").UserConfig, env: import("D:/work/test-web/node_modules/.pnpm/[vite@6.3]().5_@[types+node@24.0_3221e9d8a7877bf]()...”分配给类型“"build" | "serve" | ((this: void, config: import("D:/work/test-web/node_modules/.pnpm/[vite@5.4.1]()9_@[types+node@24._c928461cc00da890efe4f1ccdfab0111]()/node_modules/vite/dist/node/index").UserConfig, env: import("D:/work/test-web/node_modules/.pnpm/[vite@5.4.1]()9_@[types+node@24._c928461cc00da89]()...”。  
不能将类型“(this: void, config: UserConfig, env: ConfigEnv) => boolean”分配给类型“"build" | "serve" | ((this: void, config: UserConfig, env: ConfigEnv) => boolean)”。  
不能将类型“(this: void, config: import("D:/work/test-web/node_modules/.pnpm/[vite@6.3]().5_@[types+node@24.0_3221e9d8a7877bfd9756da51f493fce4]()/node_modules/vite/dist/node/index").UserConfig, env: import("D:/work/test-web/node_modules/.pnpm/[vite@6.3]().5_@[types+node@24.0_3221e9d8a7877bfd9756da51f493fce4]()/nod...”分配给类型“(this: void, config: import("D:/work/test-web/node_modules/.pnpm/[vite@5.4.1]()9_@[types+node@24._c928461cc00da890efe4f1ccdfab0111]()/node_modules/vite/dist/node/index").UserConfig, env: import("D:/work/test-web/node_modules/.pnpm/[vite@5.4.1]()9_@[types+node@24._c928461cc00da890efe4f1ccdfab0111]()/nod...”。  
参数“config”和“config” 的类型不兼容。  
不能将类型“import("D:/work/test-web/node_modules/.pnpm/[vite@5.4.1]()9_@[types+node@24._c928461cc00da890efe4f1ccdfab0111]()/node_modules/vite/dist/node/index").UserConfig”分配给类型“import("D:/work/test-web/node_modules/.pnpm/[vite@6.3]().5_@[types+node@24.0_3221e9d8a7877bfd9756da51f493fce4]()/node_modules/vite/dist/node/index").UserConfig”。  
属性“plugins”的类型不兼容。  
不能将类型“import("D:/work/test-web/node_modules/.pnpm/[vite@5.4.1]()9_@[types+node@24._c928461cc00da890efe4f1ccdfab0111]()/node_modules/vite/dist/node/index").PluginOption[]”分配给类型“import("D:/work/test-web/node_modules/.pnpm/[vite@6.3]().5_@[types+node@24.0_3221e9d8a7877bfd9756da51f493fce4]()/node_modules/vite/dist/node/index").PluginOption[]”。  
不能将类型“import("D:/work/test-web/node_modules/.pnpm/[vite@5.4.1]()9_@[types+node@24._c928461cc00da890efe4f1ccdfab0111]()/node_modules/vite/dist/node/index").PluginOption”分配给类型“import("D:/work/test-web/node_modules/.pnpm/[vite@6.3]().5_@[types+node@24.0_3221e9d8a7877bfd9756da51f493fce4]()/node_modules/vite/dist/node/index").PluginOption”。  
不能将类型“Promise<false | Plugin<any> | PluginOption[]>”分配给类型“PluginOption”。  
不能将类型“Promise<false | import("D:/work/test-web/node_modules/.pnpm/[vite@5.4.1]()9_@[types+node@24._c928461cc00da890efe4f1ccdfab0111]()/node_modules/vite/dist/node/index").Plugin<any> | import("D:/work/test-web/node_modules/.pnpm/[vite@5.4.1]()9_@[types+node@24._c928461cc00da890efe4f1ccdfab0111]()/node_module...”分配给类型“Promise<false | import("D:/work/test-web/node_modules/.pnpm/[vite@6.3]().5_@[types+node@24.0_3221e9d8a7877bfd9756da51f493fce4]()/node_modules/vite/dist/node/index").Plugin<any> | import("D:/work/test-web/node_modules/.pnpm/[vite@6.3]().5_@[types+node@24.0_3221e9d8a7877bfd9756da51f493fce4]()/node_module...”。  
不能将类型“false | import("D:/work/test-web/node_modules/.pnpm/[vite@5.4.1]()9_@[types+node@24._c928461cc00da890efe4f1ccdfab0111]()/node_modules/vite/dist/node/index").Plugin<any> | import("D:/work/test-web/node_modules/.pnpm/[vite@5.4.1]()9_@[types+node@24._c928461cc00da890efe4f1ccdfab0111]()/node_modules/vite/d...”分配给类型“false | import("D:/work/test-web/node_modules/.pnpm/[vite@6.3]().5_@[types+node@24.0_3221e9d8a7877bfd9756da51f493fce4]()/node_modules/vite/dist/node/index").Plugin<any> | import("D:/work/test-web/node_modules/.pnpm/[vite@6.3]().5_@[types+node@24.0_3221e9d8a7877bfd9756da51f493fce4]()/node_modules/vite/d...”。  
不能将类型“Plugin<any>”分配给类型“false | Plugin<any> | PluginOption[]”。

原因

项目中出现了 vite 5.x 和 vite 6.x 不同的版本。

简单的查看一下,看是否可以做适当的升级,或者将 vite 统一提升到 root 层。 子包中 不使用 vite 依赖。

然后重启 vscode 编辑器

在自己的云服务器上部署Shopify App

前面写过两篇部署Shopify App相关的文章,分别是部署在fly.ioRender上面,这两个都是Shopify官方推荐的第三方托管平台,虽然好用,但是终归是要花钱的。上周我把我们的Shopify App都从Render迁移到自己的云服务器上了,以下是部署流程:

1. 准备项目文件

首先,将项目代码上传到Ubuntu服务器上,可以使用git clone或者scp等方式,这里我使用git方式,先打开服务器,然后git clone项目代码

# 先cd到想要放置项目代码的目录,然后使用git clone代码
git clone <your-git-repository-url>
cd your_app_dir

2. 创建环境变量

项目需要一个.env文件来存储配置。基于Shopify应用需求和Prisma数据库配置,创建.env文件:

touch .env

编辑.env文件,添加必要的环境变量:

SHOPIFY_API_KEY=your_app_key
SHOPIFY_API_SECRET=your_app_secret
SHOPIFY_APP_URL=https://your_app_domain
SCOPES=read_customers,read_themes,write_products
NODE_ENV=production

3. 构建Docker镜像

使用项目中的Dockerfile构建Docker镜像:

docker build -t you_app_name .

4. 运行Docker容器

docker run -d \
  --name you_app_name \
  -p 3000:3000 \
  --env-file .env \
  -v $(pwd)/prisma:/app/prisma \
  you_app_name

这个命令会:

  • 以后台模式运行容器

  • 将容器的3000端口映射到主机的3000端口

  • 从.env文件加载环境变量

  • 将本地prisma目录挂载到容器内的/app/prisma,确保数据库文件持久化

5. 设置反向代理

设置Nginx反向代理,在Nginx配置文件中添加以下配置,我的Nginx配置文件的位置是:/etc/nginx/sites-available/default

server {
    listen 80;
    server_name your-domain.com; // 注意这里的domain必须与上面.env文件中填写的SHOPIFY_APP_URL一致
    
    location / {
        proxy_pass         http://localhost:3000;
        proxy_set_header   Host $host;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
    }
}

启用配置并重启Nginx:

sudo ln -s /etc/nginx/sites-available/default /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx

6. Docker容器管理命令

# 查看正在运行的容器
docker ps

# 查看容器日志
docker logs -f you_app_name

# 停止容器
docker stop you_app_name

# 启动容器
docker start you_app_name

# 重启容器
docker restart you_app_name

# 删除容器(如需重建)
docker rm you_app_name

7. 设置自动启动

确保Docker服务配置为开机自启动,并且容器设置为自动重启:

# 确保Docker服务开机自启
sudo systemctl enable docker

# 使用--restart=always参数重新运行容器
docker run -d \
  --name you_app_name \
  -p 3000:3000 \
  --env-file .env \
  -v $(pwd)/prisma:/app/prisma \
  --restart=always \
  you_app_name

8. 配置Partner后台App url

这个步骤和前面的fly.io和Render部署类似,进入Shopify Partner后台,然后进入App的Configuration页面,将前面步骤中的SHOPIFY_APP_URL的值(https://your\_app\_domain)填入到以下地方

然后进入App Overview页面,点击Select store,选择一个商店安装App来预览我们部署的App

接下来就可以在商店顺利打开我们部署的App了

9. 编写Docker脚本

从上面的docker启动命令可以看出这个命令还是比较长的,而且每次更新代码后重新部署的时候要先重新编译,然后暂停并移除之前的docker容器,再重新启动新的docker容器,步骤比较繁琐,所以我这里写了一个脚本,每次更新代码之后直接运行脚本就可以直接完成所有的流程,非常方便

./docker-build.sh

关于Shopify App开发可以参考我的专栏,会不断更新:Shopify App开发专栏

关于我:

曾在字节跳动等大厂工作超过8年,有资深的移动端、前端开发经验,目前在做Shopify相关业务的创业,关注我,我们一起探索Shopify的精彩世界。

卫星公众/小🍠:浅墨 momo

😁深入JS(二): 一文让你完全了解This作用与指向

一、 This作用

This关键字给予了访问当前上下文的能力

  • 通过this关键字,函数可以访问当前上下文对象的属性和方法

函数中的This

  • 函数为一个特殊的对象,每次调用将函数内容展开至当前上下文
  var name = "window";
      var person1 = {
        name: "person1",
        foo1: function () {
          console.log(this.name);
        },
      };
      person1.foo1();//person1

二、 this的指向

默认绑定

  • 默认绑定:当一个函数独立调用,不带任何修饰符的时候

函数为一个特殊的对象,每次调用将函数内容展开至当前上下文,因此可类比为window.foo()

function foo(){//foo独立调用
  console.log(this);//this指代window(全局)
}
foo();

隐式绑定

  • 隐式绑定:当函数的引用有上下文对象时(当函数被某个对象所拥有时)

函数为一个特殊的对象,每次调用将函数内容展开至当前上下文,因此可类比为obj.foo(),指向当前上下文

var obj = {
  a:1,
  foo:foo//引用foo
}
function foo(){
  console.log(this.a)
}
obj.foo()//1

显式绑定

  • call() apply() bind() 显示的将函数的this绑定到一个对象上
var obj = {
  a:1
}
function foo(){
  console.log(this.a)
}
foo.call(obj)//1

new绑定

  • new绑定:this指向创建出来的实例对象
function Person(){
  this.name = '小李'
}
let obj = new Person()//{name:小李}

至于为什么可以去看new函数的实现

其他This绑定

  1. 事件处理程序(addEventListener)中的 this
  • 当一个函数作为事件处理程序被调用时,this 通常指向触发事件的元素。
  1. 箭头函数中的 this
  • 箭头函数不会绑定自己的 this,而是继承自外围函数的作用域中的 this
  1. 定时器(setTimeout,setinterval)的 this
  • this 通常指向window

三、箭头函数的This

箭头函数没有this

为了消除函数的二义性(构造函数直接调用)

所以箭头函数this指向外层上下文,不能创建属性

箭头函数没有 this。如果访问 this,则会从外部获取。

 let name = "window";
      const obj = {
        name: "obj",
        sayHello: () => {
          console.log(this.name);//window
        },
      };
      obj.sayHello();

四、new 操作符的底层原理

new 操作符的实际行为可通过以下步骤模拟:

  1. 创建空对象 obj
  2. 将构造函数 this 绑定到 obj
  3. 执行构造函数代码
  4. 设置 obj.__proto__ 指向构造函数原型
  5. 返回 obj(除非构造函数返回引用类型)
function _new(fn, ...args) {
  let obj = {};
  obj.__proto__ = fn.prototype;
  fn.apply(obj, args);
  return obj;
}

五、手写 call 方法实现显式绑定

利用隐式绑定实现

 Function.prototype._call = function(obj, ...arr) {
   obj.fn = this
   obj.fn(...arr)   //隐式绑定
 }
Function.prototype._call=function(newThis,...arr){
    newThis=newThis||window
    let fn=Symbol()
    newThis[fn]=this
    Object.defineProperty(newThis, fn, { enumerable: false, });
    let ans=newThis[fn](...arr)
    delete newThis[fn]
    return ans
}

let arr = [1];
function fn() {
  console.log(this == arr);
}
fn._call(arr);

我为什么放弃了“大厂梦”,去了一家“小公司”?

我,前端八年。我的履历上,没有那些能让HR眼前一亮的名字,比如字节、阿里,国内那些头部的互联网公司。

“每个程序员都有一个大厂梦”,这句话我听了八年。说实话,我也有过,而且非常强烈。

刚毕业那几年,我把进大厂当作唯一的目标。我刷过算法题,背过“八股文”,也曾一次次地在面试中被刷下来。那种“求之不得”的滋味,相信很多人都体会过。

但今天,我想聊的是,我是如何从一开始的“执念”,到后来的“审视”,再到现在的“坦然”,并最终心甘情愿地在一家小公司里,找到了属于我自己的价值。

这是一个普通的、三十多岁的工程师,与自己和解的经历。


那段“求之不得”的日子

我还记得大概四五年前,是我冲击大厂最疯狂的时候。

市面上所有关于React底层原理、V8引擎、事件循环的面经,我都能倒背如流。我把LeetCode热题前100道刷了两遍,看到“数组”、“链表”这些词,脑子里就能自动冒出“双指针”、“哈希表”这些解法。

我信心满满地投简历,然后参加了一轮又一轮的面试。

结果呢?大部分都是在三轮、四轮之后,收到一句“感谢您的参与,我们后续会保持联系”。我一次次地复盘,是我哪里没答好?是项目经验不够亮眼?还是算法题的最优解没写出来?

那种感觉很糟糕。你会陷入一种深深的自我怀疑,觉得自己的能力是不是有问题,是不是自己“不配”进入那个“高手如云”的世界。


开始问自己:“大厂”真的是唯一的出路吗?

在经历了一段密集而失败的面试后,我累了,也开始冷静下来思考。

我观察身边那些成功进入大厂的朋友。他们确实有很高的薪水和很好的福利,但他们也常常在半夜的朋友圈里,吐槽着无休止的会议、复杂的流程、以及自己只是庞大系统里一颗“螺丝钉”的无力感。

我看到他们为了一个需求,要跟七八个不同部门的人“对齐”;看到他们写的代码,90%都是在维护内部庞大而陈旧的系统;看到他们即使想做一个小小的技术改进,也要经过层层审批。

我突然问自己:这真的是我想要的生活吗?我想要的是什么?

当我把这些想清楚之后,我发现,大厂的光环,对我来说,好像没那么耀眼了。


在“小公司”,找到了意想不到的“宝藏”

后来,我加入了一家规模不大的科技公司。在这里,我确实找到了我想要的东西。

成了一个“产品工程师”,而不仅仅是“前端工程师”

在小公司,边界是模糊的。

我不仅要写前端代码,有时候也得用Node.js写一点中间层。我需要自己去研究CI/CD,把自动化部署的流程跑起来。我甚至需要直接跟客户沟通,去理解他们最原始的需求。

这个过程很“野”,也很累,但我的成长是全方位的。我不再只关心页面好不好看,我开始关心整个产品的逻辑、服务器的成本、用户的留存。我的视野被强制性地拉高了。

“影响力”被无限放大

在这里,我就是前端的负责人。

用Vue还是React?用Tailwind CSS还是CSS Modules?这些技术决策,我能够和老板、和团队一起讨论,并最终拍板。我们建立的每一个前端规范,写的每一个公共组件,都会立刻成为整个团队的标准。

这种“规则制定者”的身份,和在大厂当一个“规则遵守者”,是完全不同的体验。你能清晰地看到自己的每一个决定,都对产品和团队产生了直接而深远的影响。

离“价值”更近了

最重要的一点是,我能非常直接地感受到自己工作的价值。

我花一周时间开发的新功能上线后,第二天就能从运营同事那里拿到用户的反馈数据。我知道用户喜不喜欢它,它有没有帮助公司赚到钱。这种即时的、正向的反馈,比任何KPI或者年终奖金,更能给我带来成就感。


还会羡慕那些在大厂的朋友吗?

当然会。我羡慕他们优厚的薪酬福利,羡慕他们能参与到改变数亿人的项目中去。

但我不再因此而焦虑,也不再因此而自我否定。

你可以多想一想你真正想要的是什么? 一个公司的名字,并不能定义你作为一名工程师的价值。你的价值,体现在你写的代码里,体现在你解决的问题里,也有可能体现在你创造的产品里。

找到一个能让你发光发热的地方,比挤进一个让你黯淡无光的地方,重要得多。

分享完毕。谢谢大家🙂

将你的Shopify App部署到fly.io上

当你的 Shopify App 在本地开发完成后,下一步就是部署上线,供 Shopify 商家使用。Shopify官方推荐将App部署在fly.io或者Render平台,本文先介绍如何将一个 Remix 构建的 Shopify App,部署到fly.io

一、fly.io介绍

Fly.io 是一个面向开发者的现代应用部署平台,允许你将 容器化的应用(如 Node.js、Go、Python、Rails 等)部署到全球多个边缘节点,以获得更快的响应速度和更好的可用性。

它的目标是:

“让开发者可以像部署本地应用一样,轻松把服务部署到全球。”

fly.io的主要特点:

  • 全球边缘部署:你可以将应用部署在靠近用户的节点(如东京、新加坡、法兰克福)

  • Docker 支持:Fly.io 支持基于 Docker 的自定义镜像部署

  • flyctl 工具链:通过命令行工具 flyctl 管理 app、部署、扩展实例等

  • 内建 TLS & Proxy:免费 HTTPS,自动路由请求到最近的节点

二、前置条件

在开始之前,请确保你具备以下准备:

  • 一个已注册并登录的 fly.io 账号

  • 安装了 Fly CLI 工具:

    brew方式安装

    brew install flyctl

    curl方式安装

    curl -L fly.io/install.sh | sh

三、部署到 Fly.io 的步骤

1. 登录fly.io

fly auth login

以上命令输入后会自动跳转到浏览器进行账号登录,我这里是用的gmail邮箱登录

浏览器登录成功后,命令行中会显示登录成功的提示

2. 运行 fly.io

注意如果第一次运行fly launch,请参考我下面的命令

fly launch --no-deploy --name adaptive-portfolio-app --internal-port 3000

这会在fly平台的Dashboard中创建一个名为adaptive-portfolio-app的项目,然后跳转至浏览器显示如下页面,你只需要确认设置就行

待命令行完成并输出如下信息

进入fly.io平台的Dashboard的页面,可以发现我们已经成功创建了一个应用

这时会在我们项目的根目录下生成一个fly.toml的配置文件,里面配置了我们部署到fly.io的配置信息,包括:

  • PORT:端口号

  • SCOPES:授权权限

  • SHOPIFY_API_KEY:这是我们App的API Key,可以在Shopify Partner后台查询

  • SHOPIFY_APP_URL:这是fly.io为我们部署的App创建的一个url,这个我们待会需要填写到Shopify Partner后台

还有一些fly.io部署机器的一些配置信息。

3. 发布 App

接下来我们需要先把我们的App发布一个版本

shopify app deploy

4. 部署到 fly.io

接下来可以发布到fly.io了,请注意这里一定要加上--depot=false,否则大概率会运行超时

fly deploy --depot=false

命令运行完成后会输出如下信息

我们直接打开这里的地址: adaptive-portfolio-app.fly.dev/

如果你能打开上面的页面,恭喜你,已经成功部署到fly.io了,接下来怎么在我们的商店里面能看到我部署的App呢?

5. 在测试商店预览

进入Shopify Partner后台,然后进入App的Configuration页面,将前面步骤中的部署到fly.io的url填入到以下地方

然后进入App Overview页面,点击Select store,选择一个商店安装App来预览我们部署的App

接下来就可以在商店顺利打开我们部署的App了

关于Shopify App开发可以参考我的专栏,会不断更新:Shopify App开发专栏

关于我:

曾在字节跳动等大厂工作超过8年,有资深的移动端、前端开发经验,目前在做Shopify相关业务的创业,关注我,我们一起探索Shopify的精彩世界

卫星公众/小🍠:浅墨 momo

❌