注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

URL地址末尾加不加”/“有什么区别

web
URL 结尾是否带 / 主要影响的是 服务器如何解析请求 以及 相对路径的解析方式,具体区别如下: 1. 基础概念 URL(统一资源定位符) :用于唯一标识互联网资源,如网页、图片、API等。 目录 vs. 资源: 以 / 结尾的 URL 通常表示目录,...
继续阅读 »

URL 结尾是否带 / 主要影响的是 服务器如何解析请求 以及 相对路径的解析方式,具体区别如下:




1. 基础概念



  • URL(统一资源定位符) :用于唯一标识互联网资源,如网页、图片、API等。

  • 目录 vs. 资源



    • / 结尾的 URL 通常表示目录,例如:


      https://example.com/folder/


    • 不以 / 结尾的 URL 通常指向具体的资源(如文件),例如:


      https://example.com/file







2. / 和不带 / 的具体区别


(1)目录 vs. 资源



  • https://example.com/folder/



    • 服务器通常会将其解析为 目录,并尝试返回该目录下的默认文件(如 index.html)。



  • https://example.com/folder



    • 服务器可能会将其视为 文件,如果 folder 不是文件,而是目录,服务器可能会返回 301 重定向到 folder/




📌 示例





(2)相对路径解析


URL 末尾是否有 / 会影响相对路径的解析


假设 HTML 页面包含以下 <img> 标签:


<img src="image.png">

📌 示例:



原因:



  • / 结尾的 URL,浏览器会认为它是一个目录,相对路径会基于 folder/ 解析。

  • 不带 /,浏览器可能认为 folder文件,相对路径解析可能会出现错误。




(3)SEO 影响


搜索引擎对 https://example.com/folder/https://example.com/folder 可能会视为两个不同的页面,导致 重复内容问题,影响 SEO 排名。因此:





(4)API 请求


对于 RESTful API,带 / 和不带 / 可能导致不同的行为:



一些 API 服务器对 / 非常敏感,因此最好遵循 API 文档的规范。




3. 总结


URL 形式作用影响
https://example.com/folder/目录通常返回 folder/ 下的默认文件,如 index.html,相对路径解析基于 folder/
https://example.com/folder资源(或重定向)可能被解析为文件,或者服务器重定向到 folder/,相对路径解析可能错误
https://api.example.com/data/API 路径可能与 https://api.example.com/data 表现不同,具体由 API 设计决定

如果你在开发网站,建议:



  1. 统一 URL 规则,例如所有目录都加 / 或者所有请求都不加 /,然后用 301 重定向 确保一致性。

  2. 测试 API 的行为,确认带 / 和不带 / 是否影响请求结果。


作者:Chiyamin
来源:juejin.cn/post/7468112128928350242
收起阅读 »

用node帮老婆做excel工资表

web
我是天元,立志做1000个有趣的项目的前端。如果你喜欢的话,请点赞,收藏,转发。评论领取零花钱+100勋章 背景 我老婆从事HR的工作,公司有很多连锁店,她需要将所有的门店的工资汇总计算,然后再拆分给各门店请确认,最后再提供给财务发工资。 随着门店数量渐渐增...
继续阅读 »

我是天元,立志做1000个有趣的项目的前端。如果你喜欢的话,请点赞,收藏,转发。评论领取零花钱+100勋章



背景


我老婆从事HR的工作,公司有很多连锁店,她需要将所有的门店的工资汇总计算,然后再拆分给各门店请确认,最后再提供给财务发工资。


随着门店数量渐渐增多,渐渐的我老婆已经不堪重负,每天加班都做不完,严重影响夫妻感情生活。


最终花费了2天的时间,完成了整个node程序,她只需要传入工资表,相应的各种表格在10s内自动输出。目前已正式交付,得到了每月零花钱提高100元的重大成果


整体需求



  • 表格的导入和识别

  • 表格的计算(计算公式要代入),表格样式正确

  • 最终结果按照门店拆分为工资表


需求示例(删减版)


image.png

需求为,根据传入的基本工资及补发补扣,生成总工资表,门店工资表,财务工资表发放表。


工资表中字段为门店,姓名,基本工资,补发补扣,最终工资(基本工资+补发补扣)。最后一行为总计

门店工资表按照每个门店,单独一个表格,字段同工资表。最后一行为总计


工资表


image.png


基础工资


image.png


补发补扣


image.png


技术选型


这次的主力库为exceljs,官方文档介绍如下



读取,操作并写入电子表格数据和样式到 XLSX 和 JSON 文件。


一个 Excel 电子表格文件逆向工程项目



选择exceljs是因为它支持完整的excel的样式及公式。


安装及目录结构


优先安装exceljs


npm init
yarn add exceljs

创建input,out,src三个文件夹,src放入index.js

image.png


package.json增加start脚本


"scripts": {
"start": "node src/index.js"
},

代码相关


导入


通过new Excel.Workbook();创建一个工作簿,通过workbook.xlsx.readFile来导入文件, 注意这是个promise


const ExcelJS = require("exceljs");
const path = require("path");
const inputPath = path.resolve(__dirname, "../input");
const outputPath = path.resolve(__dirname, "../out");

const loadInput =async () => {
const workbook = new ExcelJS.Workbook();
const inputFile = await workbook.xlsx.readFile(inputPath + "/工资表.xlsx")
};

loadInput()

数据拆分


通过getWorksheetApi,我们可以获取到对应的工作表的内容


  const loadInput =async () => {
...
// 基本工资
const baseSalarySheet = inputFile.getWorksheet("基本工资");
// 补发补扣
const supplementSheet = inputFile.getWorksheet("补发补扣");

}

然后我们需要进一步的来进行拆分,因为第一行为每个工作表的头,这部分在我们实际数据处理中不会使用,所以通过getRows来获取实际的内容。


  const baseSalaryContent = baseSalarySheet.getRows(
2,
baseSalarySheet.rowCount
);
baseSalaryContent.map((row) => {
console.log(row.values);
});

/**
[ <1 empty item>, '2024-02', '海贼王', '路飞', 12000 ]
[ <1 empty item>, '2024-02', '海贼王', '山治', 8000 ]
[ <1 empty item>, '2024-02', '火影忍者', '鸣人', '6000' ]
[ <1 empty item>, '2024-02', '火影忍者', '佐助', 7000 ]
[ <1 empty item>, '2024-02', '火影忍者', '雏田', 5000 ]
[ <1 empty item>, '2024-02', '一拳超人', '琦玉', 4000 ]
[]
[]
**/

可以看到实际的内容已经拿到了,我们要根据这些内容拼装一下最终便于后续的调用。

我们可以通过 row.getCellApi获取到对应某一列的内容,例如门店是在B列,那么我们就可以使用row.getCell('B')来获取。

因为我们需要拆分门店,所以这里的基本工资,我们以门店为单位,把数据进行拆分



const baseSalary = {};

baseSalaryContent.forEach((row) => {
const shopName = row.getCell("B").value;
if (!shopName) return; // 过滤空行

const name = row.getCell("C").value;
const salary = row.getCell("D").value;

if (!baseSalary[shopName]) {
baseSalary[shopName] = [];
}
baseSalary[shopName].push({
name,
salary,
});
});

这样我们得到了一个以门店名称为key的对象,value为该门店的员工信息数组。利用相同方法,获取补发补扣。因为每个人已经确定了门店,所以后续只需要根据姓名来做key,拆分成一个object即可


  // 补发补扣
const supplement = {};
supplementSheet.getRows(2, supplementSheet.rowCount).forEach((row) => {
const name = row.getCell("C").value;
const type = row.getCell("H").value;

let count = row.getCell("D").value;
// 如果为补扣,则金额为负数
if (type === "补扣") {
count = -count;
}
if (!supplement[name]) {
supplement[name] = 0;
}
supplement[name] += count;
});



数据组合


门店工资表


因为每个门店需要独立一张表,所以需要遍历baseSalary




Object.keys(baseSalary).forEach((shopName) => {
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet("工资表");

// 添加表头
worksheet.addRow([
"序号",
"门店",
"姓名",
"基本工资",
"补发补扣",
"最终工资",
]);
baseSalary[shopName].forEach((employee, index) => {
worksheet.addRow([
index + 1,
shopName,
employee.name,
+employee.salary,
supplement[employee.name] || 0,
+employee.salary + (supplement[employee.name] || 0),
]);
});
});

此时你也可以快进到表格输出来查看输出的结果,以便随时调整


这样我们就把基本工资已经写入工作表了,但是这里存在问题,最终工资使用的是一个数值,而没有公式。所以我们需要改动下


 worksheet.addRow([        index + 1,        shopName,        employee.name,        employee.salary,        supplement[employee.name] || 0,
{
formula: `D${index + 2}+E${index + 2}`,
result: employee.salary + (supplement[employee.name] || 0),
},
]);

这里的formula将对应到公式,而result是显示的值,这个值是必须写入的,如果你写入了错误的值,会在表格中显示该值,但是双击后,公式重新计算,会替换为新的值。所以这里必须计算正确


合计


依照上方的逻辑,继续添加一行作为合计,但是之前计算的时候,需要添加一个临时变量,记录下合计的相关内容。


     const count = [0, 0, 0];
baseSalary[shopName].forEach((employee, index) => {
count[0] += +employee.salary;
count[1] += supplement[employee.name] || 0;
count[2] += +employee.salary + (supplement[employee.name] || 0);
worksheet.addRow([
index + 1,
shopName,
employee.name,
+employee.salary,
supplement[employee.name]
|| 0,
{
formula: `D${index + 2}+E${index + 2}`,
result: +employee.salary + (supplement[employee.name] || 0),
},
]);
});

然后在尾部添加一行


worksheet.addRow([      "合计",      "",      "",      {        formula: `SUM(D2:D${baseSalary[shopName].length + 1})`,
result: count[0],
},
{
formula: `SUM(E2:E${baseSalary[shopName].length + 1})`,
result: count[1],
},
{
formula: `SUM(F2:F${baseSalary[shopName].length + 1})`,
result: count[2],
},
]);

美化


表格的合并,可以使用mergeCells


   worksheet.mergeCells(
`A${baseSalary[shopName].length + 2}:C${baseSalary[shopName].length + 2}`
);

这样就合并了我们的最后一行的前三列,接下来我们要给表格添加线条。

对于批量的添加,可以直接使用addConditionalFormatting,它将在一个符合条件的单元格范围内添加规则



worksheet.addConditionalFormatting({
ref: `A1:F${baseSalary[shopName].length + 2}`,
rules: [
{
type: "expression",
formulae: ["true"],
style: {
border: {
top: { style: "thin" },
left: { style: "thin" },
bottom: { style: "thin" },
right: { style: "thin" },
},
alignment: { vertical: "top", horizontal: "left", wrapText: true },
},
},
],
});

表格输出


现在门店工资表已经拆分完成,我们可以直接保存了,使用xlsx.writeFileApi来保存文件


 Object.keys(baseSalary).forEach((shopName) => {

...

workbook.xlsx.writeFile(outputPath + `/${shopName}工资表.xlsx`);
})

最终效果


image.png


image.png


相关代码地址


github.com/tinlee/1000…


作者:天元reborn
来源:juejin.cn/post/7346421986607087635
收起阅读 »

蓝牙耳机丢了,我花几分钟写了一个小程序,找到了!​

web
你是否曾经经历过蓝牙耳机不知道丢到哪里去的困扰?特别是忙碌的早晨,准备出门时才发现耳机不见了,整个心情都被影响。幸运的是,随着技术的进步,我们可以利用一些简单的小程序和蓝牙技术轻松找到丢失的耳机。今天,我要分享的是我如何通过一个自制的小程序,利用蓝牙发现功能,...
继续阅读 »

你是否曾经经历过蓝牙耳机不知道丢到哪里去的困扰?特别是忙碌的早晨,准备出门时才发现耳机不见了,整个心情都被影响。幸运的是,随着技术的进步,我们可以利用一些简单的小程序和蓝牙技术轻松找到丢失的耳机。今天,我要分享的是我如何通过一个自制的小程序,利用蓝牙发现功能,成功定位自己的耳机。这不仅是一次有趣的技术尝试,更是对日常生活中类似问题的一个智能化解决方案。

1. 蓝牙耳机丢失的困扰

现代生活中,蓝牙耳机几乎是每个人的必备品。然而,耳机的体积小、颜色常常与周围环境融为一体,导致丢失的情况时有发生。传统的寻找方式依赖于我们对耳机放置地点的记忆,但往往不尽人意。这时候,如果耳机还保持在开机状态,我们就可以借助蓝牙技术进行定位。然而,市场上大部分设备并没有自带这类功能,而我们完全可以通过编写小程序实现。

2. 蓝牙发现功能的原理

蓝牙发现功能是通过设备之间的信号传输进行连接和识别的。当一个蓝牙设备处于开机状态时,它会周期性地广播自己的信号,周围的蓝牙设备可以接收到这些信号并进行配对。这个过程的背后其实是信号的强度和距离的关系。当我们在手机或其他设备上扫描时,能够检测到耳机的存在,但并不能直接告诉我们耳机的具体位置。此时,我们可以通过信号强弱来推测耳机的大概位置。

3. 实现步骤:从构想到实践

有了这个想法后,我决定动手实践。首先,我使用微信小程序作为开发平台,利用其内置的蓝牙接口实现设备扫描功能。具体步骤如下:

  • • 环境搭建:选择微信小程序作为平台主要因为其开发简便且自带蓝牙接口支持。
  • • 蓝牙接口调用:调用wx.openBluetoothAdapter初始化蓝牙模块,确保设备的蓝牙功能开启。
  • • 设备扫描:通过wx.startBluetoothDevicesDiscovery函数启动设备扫描,并使用wx.onBluetoothDeviceFound监听扫描结果。
  • • 信号强度分析:通过读取蓝牙信号强度(RSSI),结合多次扫描的数据变化,推测设备的距离,最终帮助定位耳机。

在代码的实现过程中,信号强度的变化尤为重要。根据RSSI值的波动,我们可以判断耳机是在靠近还是远离,并通过走动测试信号的变化,逐渐缩小搜索范围。

下面是我使用 Taro 实现的全部代码:

import React, { useState, useEffect } from "react";
import Taro, { useReady } from "@tarojs/taro";
import { ViewText } from "@tarojs/components";
import { AtButtonAtIconAtProgressAtListAtListItem } from "taro-ui";
import "./index.scss";

const BluetoothEarphoneFinder = () => {
  const [isSearching, setIsSearching] = useState(false);
  const [devices, setDevices] = useState([]);
  const [nearestDevice, setNearestDevice] = useState(null);
  const [isBluetoothAvailable, setIsBluetoothAvailable] = useState(false);
  const [trackedDevice, setTrackedDevice] = useState(null);

  useEffect(() => {
    if (isSearching) {
      startSearch();
    } else {
      stopSearch();
    }
  }, [isSearching]);

  useEffect(() => {
    if (devices.length > 0) {
      const nearest = trackedDevice
        ? devices.find((d) => d.deviceId === trackedDevice.deviceId)
        : devices[0];
      setNearestDevice(nearest || null);
    } else {
      setNearestDevice(null);
    }
  }, [devices, trackedDevice]);

  const startSearch = () => {
    const startDiscovery = () => {
      setIsBluetoothAvailable(true);
      Taro.startBluetoothDevicesDiscovery({
        success: () => {
          Taro.onBluetoothDeviceFound((res) => {
            const newDevices = res.devices.map((device) => ({
              name: device.name || "未知设备",
              deviceId: device.deviceId,
              rssi: device.RSSI,
            }));
            setDevices((prevDevices) => {
              const updatedDevices = [...prevDevices];
              newDevices.forEach((newDevice) => {
                const index = updatedDevices.findIndex(
                  (d) => d.deviceId === newDevice.deviceId
                );
                if (index !== -1) {
                  updatedDevices[index] = newDevice;
                } else {
                  updatedDevices.push(newDevice);
                }
              });
              return updatedDevices.sort((a, b) => b.rssi - a.rssi);
            });
          });
        },
        fail: (error) => {
          console.error("启动蓝牙设备搜索失败:", error);
          Taro.showToast({
            title: "搜索失败,请重试",
            icon: "none",
          });
          setIsSearching(false);
        },
      });
    };

    Taro.openBluetoothAdapter({
      success: startDiscovery,
      fail: (error) => {
        if (error.errMsg.includes("already opened")) {
          startDiscovery();
        } else {
          console.error("初始化蓝牙适配器失败:", error);
          Taro.showToast({
            title: "蓝牙初始化失败,请检查蓝牙是否开启",
            icon: "none",
          });
          setIsSearching(false);
          setIsBluetoothAvailable(false);
        }
      },
    });
  };

  const stopSearch = () => {
    if (isBluetoothAvailable) {
      Taro.stopBluetoothDevicesDiscovery({
        complete: () => {
          Taro.closeBluetoothAdapter({
            complete: () => {
              setIsBluetoothAvailable(false);
            },
          });
        },
      });
    }
  };

  const getSignalStrength = (rssi) => {
    if (rssi >= -50return 100;
    if (rssi <= -100return 0;
    return Math.round(((rssi + 100) / 50) * 100);
  };

  const getDirectionGuide = (rssi) => {
    if (rssi >= -50return "非常接近!你已经找到了!";
    if (rssi >= -70return "很近了,继续朝这个方向移动!";
    if (rssi >= -90return "正确方向,但还需要继续寻找。";
    return "信号较弱,尝试改变方向。";
  };

  const handleDeviceSelect = (device) => {
    setTrackedDevice(device);
    Taro.showToast({
      title: `正在跟踪: ${device.name}`,
      icon: "success",
      duration: 2000,
    });
  };

  return (
    <View className="bluetooth-finder">
      {isSearching && (
        <View className="loading-indicator">
          <AtIcon value="loading-3" size="30" color="#6190E8" />
          <Text className="loading-text">搜索中...Text>
        View>
      )}
      {nearestDevice && (
        <View className="nearest-device">
          <Text className="device-name">{nearestDevice.name}Text>
          <AtProgress
            percent={getSignalStrength(nearestDevice.rssi)}
            status="progress"
            isHidePercent
          />

          <Text className="direction-guide">
            {getDirectionGuide(nearestDevice.rssi)}
          Text>
        View>
      )}
      <View className="device-list">
        <AtList>
          {devices.map((device) => (
            <AtListItem
              key={device.deviceId}
              title={device.name}
              note={`${device.rssidBm`}
              extraText={
                trackedDevice && trackedDevice.deviceId === device.deviceId
                  ? "跟踪中"
                  : ""
              }
              arrow="right"
              onClick={() =>
 handleDeviceSelect(device)}
            />
          ))}
        AtList>
      View>
      <View className="action-button">
        <AtButton
          type="primary"
          circle
          onClick={() =>
 setIsSearching(!isSearching)}
        >
          {isSearching ? "停止搜索" : "开始搜索"}
        AtButton>
      View>
    View>
  );
};

export default BluetoothEarphoneFinder;

嘿嘿,功夫不负苦心人,我最终通过自己的小程序找到了我的蓝牙耳机。

我将我的小程序发布到了微信小程序上,目前已经通过审核,可以直接使用了。搜索老码宝箱 即可体验。 

顺带还加了非常多的小工具,而且里面还有非常多日常可能会用到的工具,有些还非常有意思。

比如

绘制函数图

每日一言

汇率转换(实时)

BMI 计算

简易钢琴

算一卦

这还不是最重要的

最重要的是,这里的工具是会不断增加的,而且,更牛皮的是,你还可以给作者提需求,增加你想要的小工具,作者是非常欢迎一起讨论的。有朝一日,你也希望你的工具也出现在这个小程序上,被千万人使用吧。

4. 实际应用与优化空间

这个小程序的实际效果超出了我的预期。我能够通过它快速找到丢失的耳机,整个过程不到几分钟时间。然而,值得注意的是,由于蓝牙信号会受到环境干扰,例如墙体、金属物等,导致信号强度并不总是精确。在后续的优化中,我计划加入更多的信号处理算法,例如利用三角定位技术,结合多个信号源来提高定位精度。此外,还可以考虑在小程序中加入可视化的信号强度图,帮助用户更直观地了解耳机的大致方位。

一些思考:

蓝牙耳机定位这个小程序的开发,展示了技术在日常生活中的强大应用潜力。虽然这个项目看似简单,但背后的原理和实现过程非常具有教育意义。通过这次尝试,我们可以看到,借助开源技术和简单的编程能力,我们能够解决许多日常生活中的实际问题。

参考资料:

    1. 微信小程序官方文档:developers.weixin.qq.com
    1. 蓝牙信号强度(RSSI)与距离关系的研究:http://www.bluetooth.com
    1. 个人开发者经验分享: 利用蓝牙发现功能定位设备

  • 作者:老码小张
    来源:juejin.cn/post/7423610485180727332
    收起阅读 »

    前端可玩性UP项目:大屏布局和封装

    web
    前言 autofit.js 发布马上要一年了,也收获了一批力挺用户,截至目前它在github上有1k 的 star,npm 上有超过 13k 的下载量。 这篇文章主要讲从设计稿到落地开发大屏应用,大道至简,这篇文章能帮助各位潇洒自如的开发大屏。 分析设计稿 分...
    继续阅读 »

    前言


    autofit.js 发布马上要一年了,也收获了一批力挺用户,截至目前它在github上有1k 的 star,npm 上有超过 13k 的下载量。


    这篇文章主要讲从设计稿到落地开发大屏应用,大道至简,这篇文章能帮助各位潇洒自如的开发大屏。


    分析设计稿


    分析设计稿之前先吐槽一下大屏这种展现形式,这简直就是自欺欺人、面子工程的最直接的诠释,是吊用没有,只为了好看,如果设计的再不好看啊,这就纯纯是屎。在我的理解中,这就像把PPT放到了web端,仅此而已。



    但是王哥告诉我:"你看似没有用的东西,其实都有用,很多想真正做出有用的产品的企业,没钱,就要先把面子工程做好,告诉别人他们要做一件什么事,这样投资人才会看到,后面才有机会发展。"



    布局方案


    image.png
    上图展示了一个传统意义上且比较普遍的大屏形态,分为四个部分,分别是


    头部


    头部经常放标题、功能菜单、时间、天气


    左右面板


    左右面板承载了各种数字和报表,还有视频、轮播图等等


    中间


    中间部分一般放地图,这其中又分假地图(一张图片)、图表地图(如echarts)、地图引擎(如:leaflet、mapbox、高德、百度)。或者有的还会放3D场景,一般有专门的同事去做3D场景,然后导入到web端。


    大屏的设计通常的分辨率是 1920*1080 的,这也是迄今为止应用最广泛的显示器配置,当然也有基于客户屏幕做的异形分辨率,这就五花八门了。


    但是万变不离其宗,分辨率的变化不会影响它的基本结构,根据上面的图,我们可以快速构建结构代码


      <div class='Box'>
       <div class="header"></div>
       <div class="body">
         <div class="leftPanel"></div>
         <div class="mainMap"></div>
         <div class="rightPanel"></div>
       </div>
     </div>

    上面的代码实现了最简单的上下(Box)+左右(body)的布局结构,完全不需要任何定位策略。


    要实现上图的效果,只需最简单的CSS即可完成布局。


    组件方案


    大屏虽然是屎,但是是一种可玩性很强的项目,想的越复杂,做起来就越复杂,想的越简单,做起来就越简单。


    可以疯狂封装炫技,因为大屏里面的可玩组件简直太多了,且涵盖的太全了,想怎么玩都可以,包括但不限于 各类图表库的封装(echarts、highCharts、vChart)、轮播图(swiper)、地图引擎、视频库(包括直播流)等等。


    如果想简单,甚至可以不用封装,可以看到结构甚至简单到不用CSS几行就可以搭建出基本框架,只把header、leaftPanel、rightPanel、map封装一下就可以了。


    这里还有一个误区,就是大家都喜欢把 大型的组件库 拉到大屏里来用,结果做完了发现好像只用了一个 toast 和一个下拉组件,项目打包后却增大了几十倍的体积,其实像这种简单的组件,完全可以手写,或者找小的独立包来用,一方面会减小体积,不至于让项目臃肿,另一方面可以锻炼自己的手写能力,这才是有必要的封装。


    适配


    目前主流的适配方案,依然是 rem 方案,其原理就是根据根元素的 font-size 自动计算大小,但是此方法需要手动计算 rem 值,或者使用第三方插件如postcss等,但是此方案还有一个弊端,就是无法向下兼容,因为浏览器中最小的文字大小是12px。


    vh/vw方案就不再赘述了,原理基本和 rem/em 相似,都涉及到单位的转换。


    autofit.js


    主要讲一下使用 autofit.js 如何快速实现适配。


    不支持的场景


    首先 autofit.js 不支持 elementUI(plus)、ant-design等组件库,具体是不支持包含popper.js的组件,popper.js 在计算弹出层位置时,不会考虑 scale 后的元素的视觉大小,所以会造成弹出元素的位置偏移。


    其次,不支持 百度地图,百度地图对窗口缩放事件没有任何处理,有同学反馈说,即使使用了resize属性,百度地图在和autofit.js共同使用时,也会有事件热区偏移的问题。而且百度地图使用 bd-09 坐标系,和其他图商不通用,引擎的性能方面也差点意思,个人不推荐在开发中使用百度地图。


    然后一些拖拽库,如甘特图插件,可能也不支持,他们在计算鼠标位置时同样没有考虑 scale 后的元素的视觉大小。


    用什么单位


    不支持的单位:vh、vw、rem、em


    让我诧异的是,老有人问我该用什么单位,主要徘徊在 px 和 % 之间,加群的同学多数是因为用了相对单位,导致留白了。不过人各有所长,跟着各位大佬我也学到了很多。


    看下图


    image.png
    假如有两个宽度为1000的元素,他们内部都有一个子元素,第一个用百分比设置为 width:50%;left:1% , 第二个设置为 wdith:500px;left:10px 。此时,只要外部的1000px的容器宽度不变,这两个内部元素在视觉上是一模一样的,且在实际数值上也是一模一样的,他们宽度都为500px,距离左侧10px。


    但是如果外部容器变大了,来看一下效果:


    image.png
    在样式不变的情况下,仅改变外部容器大小,差异就出来了,由上图可知,50%的元素依然占父元素的一半,实际宽度变成了 1000px,距离左侧的实际距离变成了 20px。


    这当然不难理解,百分比单位是根据 最近的、有确定大小的父级元素计算的。


    所以,应该用什么单位其实取决于想做什么,举个例子:在1920*1080基础上开发,中间的地图写成了宽度为 500px ,这在正常情况下,看起来没有任何问题,它大概占屏幕的 26%,当屏幕分辨率达到4096*2160时,它在屏幕上只占 12%,看起来就是缩在一角。而当你设置宽度为26%时,无论显示器如何变化,它始终占屏幕26%。


    autofit.js 所干的事,就是把1000px 变成了 2000px或者把2000px变成了1000px,并给它设置了一个合适的缩放大小。


    图表、图片拉伸


    背景或各种图片按需设置 object-fit: cover;即可


    图表如echarts一般推荐使用百分比,且监听窗口变化事件做resize()


    结语


    再次感慨,大道至简,事情往往没有那么复杂,祝各位前程似锦。


    作者:德莱厄斯
    来源:juejin.cn/post/7344625554530779176
    收起阅读 »

    我终于从不想上班又不能裸辞的矛盾中挣扎出来了

    最近的状态有一种好像一个泄了气的皮球的感觉一样,就是对生活中很多事情都提不起来兴趣。 我希望自己可以多看一点书,但是我不想动;我希望自己可以练习书法,但是我不想动;我希望自己可以学会一门乐器,但是我不想动。 相比上面三点,我更希望的是我可以早上起来不用上班,但...
    继续阅读 »

    最近的状态有一种好像一个泄了气的皮球的感觉一样,就是对生活中很多事情都提不起来兴趣。


    我希望自己可以多看一点书,但是我不想动;我希望自己可以练习书法,但是我不想动;我希望自己可以学会一门乐器,但是我不想动。


    相比上面三点,我更希望的是我可以早上起来不用上班,但是这只是我的希望而已。


    这就是我最近的生活状态。


    我有一种我的生活仿佛失去了控制的感觉,每一天我的内心好像都有一个小人在不断呐喊,说我不想上班。因为这个声音,我一度非常非常想要裸辞,但是我为什么没有裸辞呢?


    还不是因为我买房买车欠了十几万,我到现在才还了两万而已,再加上我每个月还有房贷要还。


    然而,当我经常不情愿地做着跟我心里想法相悖的行为的时候,我发现自己常常会做一些小动作来向自己表达抗议和不满。


    比如说,我的工作会变得越来越低效,上班的时候会偷偷地摸鱼,还有就是变得越来越容易拖延。


    就好像这样的我,可以让那个不想上班的我,取得了一丢丢的小胜利一样。


    一旦开始接受自己没有办法辞职,并且还要上个几十年班这样的结果时,就会让人有一种破罐子破摔的想法。


    而且随之而来的是一种对未来,对生活的无力感。


    这种无力感渐渐地渗透在我生活的方方面面,以至于让我慢慢地对很多东西都提不起兴趣,我生活中的常态就变成了不想动。


    但是有趣的事情发生了,有一天我在和我朋友聊天的时候,我的脑子里面突然出现了一个想法,就是我决定两年之后我要实现我不上班的这个目标。


    当有了这个想法之后,我就开始认真思考这件事情的可行度。


    通过分析我现在收支情况,我把两年之内改成了2026年之前,因为我觉得这样会让我更加信服这个目标的可行性。


    同时我把这个想法也拆分成了两个更为具体的目标,其中一个就是我要在2026年之前还完欠的所有钱。


    第二个就是我需要给自己存够20万,这20万是不包括投资理财或者基金股票里面的钱,而是我完全可以自由支配的。


    毕竟没有人可以在没有工作的情况下,没有收入的情况下。没有存款的情况下,还能保持一个不焦虑的状态。


    当我得出了这两个具体的目标之后,我整个人瞬间被一种兴奋的状态填满,我瞬间找到了工作的意义和动力。


    也许你会说,我的这个想法对我现在生活根本起不到任何的改变作用。


    我依旧还需要每天七点起床,还是要每天重复地去过我两点一线的生活。


    但是于我自己而言,当我给我上班的这件事情加了一个两年的期限之后,我突然觉得我的未来,我的生活都充满了希望。


    我整个人从不想动的状态,变成了一种被兴奋的状态填满的感觉。


    所以,如果你和我一样有一些类似的困扰,有一些你不想做而又不得不做的事情,让你有一种深陷泥潭,无法前进的感觉,那你不妨试一下这个方法。


    结合你自己的实际情况,为你不想做这件事情,设计一个期限,这个期限必须要是你认可,你接受,并且你认为你可以在这个截止时间之前完成的。


    我想这个决定应该会对你的生活带来一些改变。


    作者:程序员Winn
    来源:juejin.cn/post/7428154034480906278
    收起阅读 »

    2025年了,令人唏嘘的Angular,现在怎么样了🚀🚀🚀

    web
    迅速崛起和快速退出 时间回到2014年,此时的 Angular 1.x 习得了多种武林秘籍,左手降龙十八掌、右手六脉神剑,哦不,左手双向数据绑定、右手依赖注入、上能模块化开发、下有模板引擎 和 前端路由, 背后还有Google这个风头无两的带头大哥做技术背书,...
    继续阅读 »

    迅速崛起和快速退出


    时间回到2014年,此时的 Angular 1.x 习得了多种武林秘籍,左手降龙十八掌、右手六脉神剑,哦不,左手双向数据绑定、右手依赖注入、上能模块化开发、下有模板引擎前端路由, 背后还有Google这个风头无两的带头大哥做技术背书,可以说集万千功能和宠爱于一身,妥妥的主角光环。


    而此时的江湖,B端开发正尝到了 SPA 的甜头,积极的从传统的 MVC 开发模式转变为更为方便快捷的单页面应用开发模式,



    文章同步在公众号:萌萌哒草头将军,欢迎关注!



    一拍即合,强大的一站式单页面开发框架Angular自然而然,就成了公认的武林盟主,江湖一哥。


    angular下载量


    但是好景不长,2016年9月14日 Angular 2.x 的发布,彻底断送了武林盟主的宝座,



    Vue:大哥,你可是真给机会呀!



    ts下载量


    2.0 使用ts彻底重写(最早支持ts的框架)、放弃了脏检查更新机制,引入了响应式系统、使用现代浏览器标准、加入装饰器语法,和 1.0 完全不兼容。可以从上图看到,此时大家基本上还不太接受ts!


    新手面对陡然升高的学习曲线叫苦连连,已经入坑的开发者因为巨大的迁移工作而怨声载道。


    此时,默默耕耘了两年的小弟,Vue已经拥有完备的本地化文档和丰富的可选生态,而且作为新手你只要会使用htmlcssjavascript,就可以上手写项目了。


    所以,此时的 Vue 振臂一呼:“王侯将相宁有种乎!”,立马新皇加冕!


    积极改变,三拜义父的数据驱动



    忆往昔峥嵘岁月稠,恰同学少年,风华正茂;书生意气,挥斥方遒。



    一转眼,angular 已经发布第19个大版本了(平均一年两个版本)。


    失去武林盟主的Angular,飘零半生,未逢明主,公若不弃,Angular愿拜为义父,


    脏检查机制响应式系统,再到Signals系统Angular 历经沧桑的数据驱动方式可以说是前端发展的缩影。


    脏检查机制


    脏检查机制 是通过拦截异步操作,http setTimeout 用户交互事件等,触发变更检测系统,从根组件开始检查组件中数据是否有更新,有更新时,对应的 $scope 变量会被标记为 ,然后同步的更新dom的内容,重新开始变更检查,直到稳定后标记为干净,即通过稳定性检查!


    <!DOCTYPE html>
    <html lang="en" ng-app="myApp">
    <head>
    <meta charset="UTF-8">
    <title>AngularJS Counter</title>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
    </head>
    <body ng-controller="CounterController as ctrl">

    <h1>Count: {{ ctrl.count }}</h1>
    <h2>Double Count: {{ ctrl.doubleCount() }}</h2>
    <button ng-click="ctrl.increment()">+1</button>

    <script>
    angular.module('myApp', [])
    .controller('CounterController', function() {
    var vm = this;
    vm.count = 0;

    vm.increment = function() {
    vm.count++;
    console.log('Latest count:', vm.count);
    };

    vm.doubleCount = function() {
    return vm.count * 2;
    };
    });
    </script>
    </body>
    </html>

    但是这种检查机制存在缺陷,例如,当数据量十分庞大时,就会触发非常多次的脏检查机制


    响应式系统


    响应式系统 没有出现之前,脏检查机制 是唯一的选择,但是响应式系统凭借快速轻便的特点,立马在江湖上引起了不小的轰动,Angular也放弃了笨重的脏检查机制采用了响应式系统!


    // app.component.ts
    import { Component } from '@angular/core';

    @Component({
    selector: 'app-root',
    template: `
    <h1>Count: {{ count }}</h1>
    <h2>Double Count: {{ doubleCount() }}</h2>
    <button (click)="increment()">+1</button>
    `
    ,
    })
    export class AppComponent {
    count: number = 0;

    increment() {
    this.count++;
    console.log('Latest count:', this.count);
    }

    doubleCount() {
    return this.count * 2;
    }
    }

    和我们熟知的Vue的响应式不同,Angular的响应式采用双向数据流的设计,这也使得它在面对复杂项目时,性能和维护上不如Vue快捷方便。


    所以,为了更好的驾驭双向数据流的响应式系统,Angular也是自创了很多绝学,例如:局部变更检测。


    该绝学主要招式:组件级变更检测策略、引入zonejsOnPush 策略等。


    1. 组件级变更检测策略

    每个组件都有自己的更新策略,只有组件的属性和文本发生变化时,才会触发变更检测!


    2. 引入zonejs

    引入zonejs拦截http setTimeout 用户交互事件等异步操作


    3. OnPush 策略

    默认情况下,整个组件树在变更时更新。


    默认策略


    但是开发者可以选择 OnPush 策略,使得组件仅在输入属性发生变化、事件触发或手动调用时才进行变更检测。这进一步大大减少了变更检测的频率,适用于数据变化不频繁的场景。


    OnPush策略


    Signals系统


    很快啊,当SolidJS凭借优异的信号系统在江湖上闯出了响亮的名声,这时,大家才意识到,原来还有更优秀的开发方式!signal系统的开发方式,也被公认为新一代的武林绝技!


    于是,Angular 16它来了,它带着signalmemoeffect三件套走来了!


    当使用signal时,更新仅仅发生在当前组件。


    signal系统


    // app.component.ts
    import { Component, signal, effect, memo } from '@angular/core';

    @Component({
    selector: 'app-root',
    template: `
    <h1>Count: {{ count() }}</h1>
    <h2>Double Count: {{ doubleCount() }}</h2>
    <button (click)="increment()">+1</button>
    `
    ,
    styles: []
    })
    export class AppComponent {
    // 使用 signal 来管理状态
    count = signal(0);

    // 使用 memo 来计算 doubleCount
    doubleCount = memo(() => this.count() * 2);

    constructor() {
    // 使用 effect 来监听 count 的变化
    effect(() => {
    console.log('Latest count:', this.count());
    });
    }

    increment() {
    // 更新 signal 的值
    this.count.set(this.count() + 1);
    }
    }

    总结


    Angular 虽然在国内市场一蹶不振,但是在国际市场一直默默耕耘 10 年。它作为一站式解决方案的框架,虽然牺牲了灵活性,但是也为开发者提供了沉浸式开发的选择!


    且它不断创新、积极拥抱新技术的精神令人十分钦佩!


    今天的内容就这些了,如果你觉得还不错,可以关注我。


    如果文章中存在问题,欢迎指正!


    作者:萌萌哒草头将军
    来源:juejin.cn/post/7468526097011097654
    收起阅读 »

    这个排队系统设计碉堡了

    先赞后看,Java进阶一大半 各位好,我是南哥。 我在网上看到某厂最后一道面试题:如何设计一个排队系统? 关于系统设计的问题,大家还是要多多思考,可能这道题考的不是针对架构师的职位,而是关于你的业务设计能力。如果单单只会用开源软件的API,那似乎我们的竞争力...
    继续阅读 »

    先赞后看,Java进阶一大半



    各位好,我是南哥。


    我在网上看到某厂最后一道面试题:如何设计一个排队系统?


    关于系统设计的问题,大家还是要多多思考,可能这道题考的不是针对架构师的职位,而是关于你的业务设计能力。如果单单只会用开源软件的API,那似乎我们的竞争力还可以再强些。学习设计东西、创作东西,把我们设计的产品给别人用,那竞争力一下子提了上来。


    15岁的初中生开源了 AI 一站式 B/C 端解决方案chatnio,该产品在上个月被以几百万的价格收购了。这值得我们思考,程序创造力、设计能力在未来会变得越来越重要。


    在这里插入图片描述



    ⭐⭐⭐收录在《Java学习/进阶/面试指南》:https://github/JavaSouth



    精彩文章推荐



    1.1 数据结构


    排队的一个特点是一个元素排在另一个元素的后面,形成条状的队列。List结构、LinkedList链表结构都可以满足排队的业务需求,但如果这是一道算法题,我们要考虑的是性能因素。


    排队并不是每个人都老老实实排队,现实会有多种情况发生,例如有人退号,那属于这个人的元素要从队列中删除;特殊情况安排有人插队,那插入位置的后面那批元素都要往后挪一挪。结合这个情况用LinkedList链表结构会更加合适,相比于List,LinkedList的性能优势就是增、删的效率更优。


    但我们这里做的是一个业务系统,采用LinkedList这个结构也可以,不过要接受修改、维护起来困难,后面接手程序的人难以理解。大家都知道,在实际开发我们更常用List,而不是LinkedList。


    List数据结构我更倾向于把它放在Redis里,有以下好处。


    (1)数据存储与应用程序拆分。放在应用程序内存里,如果程序崩溃,那整条队列数据都会丢失。


    (2)性能更优。相比于数据库存储,Redis处理数据的性能更加优秀,结合排队队列排完则销毁的特点,甚至可以不存储到数据库。可以补充排队记录到数据库里。


    简单用Redis命令模拟下List结构排队的处理。


    # 入队列(将用户 ID 添加到队列末尾)
    127.0.0.1:6379> RPUSH queue:large user1
    127.0.0.1:6379> RPUSH queue:large user2

    #
    出队列(将队列的第一个元素出队)
    127.0.0.1:6379> LPOP queue:large

    #
    退号(从队列中删除指定用户 ID)
    127.0.0.1:6379> LREM queue:large 1 user2

    #
    插队(将用户 ID 插入到指定位置,假设在 user1 之前插入 user3)
    127.0.0.1:6379> LINSERT queue:large BEFORE user1 user3

    1.2 业务功能


    先给大家看看,南哥用过的费大厨的排队系统,它是在公众号里进行排队。


    我们可以看到自己现在的排队进度。


    在这里插入图片描述


    同时每过 10 号,公众号会进行推送通知;如果 10 号以内,每过 1 号会微信公众号通知用户实时排队进度。最后每过 1 号就通知挺人性化,安抚用户排队的焦急情绪。


    在这里插入图片描述


    总结下来,我们梳理下功能点。虽然上面看起来是简简单单的查看、通知,背后可能隐藏许多要实现的功能。


    在这里插入图片描述


    1.3 后台端


    (1)排队开始


    后台管理员创建排队活动,后端在Redis创建List类型的数据结构,分别创建大桌、中桌、小桌三条队列,同时设置没有过期时间。


    // 创建排队接口
    @Service
    public class QueueManagementServiceImpl {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    // queueType为桌型
    public void createQueue(String queueType) {
    String queueKey = "queue:" + queueType;
    redisTemplate.delete(queueKey); // 删除队列,保证队列重新初始化
    }
    }


    (2)排队操作


    前面顾客用餐完成后,后台管理员点击下一号,在Redis的表现为把第一个元素从List中踢出,次数排队的总人数也减 1。


    // 排队操作
    @Service
    public class QueueManagementServiceImpl {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
    * 将队列中的第一个用户出队
    */

    public void dequeueNextUser(String queueType) {
    String queueKey = "queue:" + queueType;
    String userId = redisTemplate.opsForList().leftPop(queueKey);
    }
    }

    1.4 用户端


    (1)点击排队


    用户点击排队,把用户标识添加到Redis队列中。


    // 用户排队
    @Service
    public class QueueServiceImpl {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    public void enterQueue(String queueType, String userId) {
    String queueKey = "queue:" + queueType;
    redisTemplate.opsForList().rightPush(queueKey, userId);
    log.info("用户 " + userId + " 已加入 " + queueType + " 队列");
    }
    }


    (2)排队进度


    用户可以查看三条队列的总人数情况,直接从Redis三条队列中查询队列个数。此页面不需要实时刷新,当然可以用WebSocket实时刷新或者长轮询,但具备了后面的用户通知功能,这个不实现也不影响用户体验。


    而用户的个人排队进度,则计算用户所在队列前面的元素个数。


    // 查询排队进度
    @Service
    public class QueueServiceImpl {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    public long getUserPositionInQueue(String queueType, String userId) {
    String queueKey = "queue:" + queueType;
    List<String> queue = redisTemplate.opsForList().range(queueKey, 0, -1);
    if (queue != null) {
    return queue.indexOf(userId);
    }
    return -1;
    }
    }


    (3)用户通知


    当某一个顾客用餐完成后,后台管理员点击下一号。此时后续的后端逻辑应该包括用户通知。


    从三个队列里取出当前用户进度是 10 的倍数的元素,微信公众号通知该用户现在是排到第几桌了。


    从三个队列里取出排名前 10 的元素,微信公众号通知该用户现在的进度。


    // 用户通知
    @Service
    public class NotificationServiceImpl {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    private void notifyUsers(String queueType) {
    String queueKey = "queue:" + queueType;
    // 获取当前队列中的所有用户
    List<String> queueList = jedis.lrange(queueKey, 0, -1);

    // 通知排在10的倍数的用户
    for (int i = 0; i < queueList.size(); i++) {
    if ((i + 1) % 10 == 0) {
    String userId = queueList.get(i);
    sendNotification(userId, "您的排队进度是第 " + (i + 1) + " 位,请稍作准备!");
    }
    }

    // 通知前10位用户
    int notifyLimit = Math.min(10, queueList.size()); // 避免队列小于10时出错
    for (int i = 0; i < notifyLimit; i++) {
    String userId = queueList.get(i);
    sendNotification(userId, "您已经在前 10 位,准备好就餐!");
    }
    }
    }

    这段逻辑应该移动到前面后台端的排队操作。


    1.5 存在问题


    上面的业务情况,实际上排队人员不会太多,一般会比较稳定。但如果每一条队列人数激增的情况下,可以预见到会有问题了。


    对于Redis的List结构,我们需要查询某一个元素的排名情况,最坏情况下需要遍历整条队列,时间复杂度是O(n),而查询用户排名进度这个功能又是经常使用到。


    对于上面情况,我们可以选择Redis另一种数据结构:Zset。有序集合类型Zset可以在O(lgn)的时间复杂度判断某元素的排名情况,使用ZRANK命令即可。


    # zadd命令添加元素
    127.0.0.1:6379> zadd 100run:ranking 13 mike
    (integer) 1
    127.0.0.1:6379> zadd 100run:ranking 12 jake
    (integer) 1
    127.0.0.1:6379> zadd 100run:ranking 16 tom
    (integer) 1

    # zrank命令查看排名
    127.0.0.1:6379> zrank 100run:ranking jake
    (integer) 0
    127.0.0.1:6379> zrank 100run:ranking tom
    (integer) 2

    # zscore判断元素是否存在
    127.0.0.1:6379> zscore 100run:ranking jake
    "12"

    我是南哥,南就南在Get到你的点赞点赞点赞。


    在这里插入图片描述



    创作不易,不妨点赞、收藏、关注支持一下,各位的支持就是我创作的最大动力❤️



    作者:JavaSouth南哥
    来源:juejin.cn/post/7436658089703145524
    收起阅读 »

    ⚔️不让我在控制台上调试,哼,休想🛠️

    web
    在 JavaScript 中,使用 debugger 关键字可以在代码执行到该位置时触发断点调试。这可以帮助开发人员进行代码调试和排错。然而,有些网站开发者可能会故意使用 debugger 关键字来阻止调试,从而增加代码的安全性。但仍然有一些方法可以绕过这种防...
    继续阅读 »

    在 JavaScript 中,使用 debugger 关键字可以在代码执行到该位置时触发断点调试。这可以帮助开发人员进行代码调试和排错。然而,有些网站开发者可能会故意使用 debugger 关键字来阻止调试,从而增加代码的安全性。但仍然有一些方法可以绕过这种防护措施,进行调试和排错。


    禁用浏览器debugger


    因为 debugger 其实就是对应的一个断点,它相当于用代码显示地声明了一个断点,要解除它,我们只需要禁用这个断点就好了。


    禁用全局断点


    全局禁用开关位于 Sources 面板的右上角,如下图所示:


    image-20240516204937081.png


    点击它,该按钮会被激活,变成蓝色。


    这个时候我们再重新点击一下 Resume script execution(恢复脚本执行)按钮,跳过当前断点,页面就不会再进入到无限 debugger 的状态了。


    image-20240516205310992.png


    注意,禁用所有断点可能会导致你错过一些潜在的问题或错误,因为代码将会连续执行而不会在可能的问题点停止。因此,在禁用所有断点之前,请确保你已经理解了代码的行为,并且明白在出现问题时该如何调试。


    禁用局部断点


    尝试使用另一种方法来跳过这个无限 debugger。在 debugger 语句所在的行的行号上单击鼠标右键,此时会出现一个快捷菜单,操作下图所示:


    动画.gif


    添加条件断点


    在JS代码 debugger 行数位置的最左侧点击右键,添加条件断点(满足条件才会进入断点),将条件设置为false,就是条件永远不成立,永远不会断下来。


    动画.gif


    添加条件断点还可以监视获取一些变量信息,还是挺好用的。


    如果是简单的debugger断点,直接用上边的方式就可以,如果是通过定时器触发的debugger断点,就需要进行Hook处理了。


    以上的方案执行完毕之后有时候会跳转空页面,这时候只需要在空页面上打开原先地址即可。


    先打开控制台


    有时候我们一打开网页,就直接进入空页面,控制台上的js和html文件也随之为空,这时候需要在空白页面,或者F12等键无法打开控制台等,这种可以先打开控制台,然后再在空白页面上打开网站即可。


    可以在这个网站上试一下。


    替换文件


    直接使用浏览器开发者工具替换修改js(Sources面板 --> Overrides),或者通过FD工具替换。


    这种方式的核心思路,是替换 JS 文件中的 debugger 关键字,并保存为本地文件,在请求返回的时候、通过正则匹配等方式、拦截并替换返回的 JS 代码,以达到绕过 debugger 的目的。也可以直接删掉相关的debugger代码。


    具体实现可参考:2024最新版JavaScript逆向爬虫教程-------基础篇之无限debugger的原理与绕过


    快捷方案-使用油猴等插件


    使用这种方法,就不需要再打 script 断点。直接安装插件即可。


    image-20240516203434774.png


    参考文献


    2024最新版JavaScript逆向爬虫教程-------基础篇之无限debugger的原理与绕过


    解决浏览器调试无限debugger


    作者:Aplee
    来源:juejin.cn/post/7369505226921738278
    收起阅读 »

    🔏别想调试我的前端页面代码🔒

    web
    这里我们不介绍禁止右键菜单, 禁止F12快捷键和代码混淆方案。 无限debugger 前端页面防止调试的方法主要是通过不断 debugger 来疯狂输出断点,因为 debugger 在控制台被打开的时候就会执行 由于程序被 debugger 阻止,所以无法进...
    继续阅读 »

    71e52c67f5094e44b92ccaed93db15c5.jpg


    这里我们不介绍禁止右键菜单, 禁止F12快捷键代码混淆方案。


    无限debugger



    • 前端页面防止调试的方法主要是通过不断 debugger 来疯狂输出断点,因为 debugger 在控制台被打开的时候就会执行

    • 由于程序被 debugger 阻止,所以无法进行断点调试,所以网页的请求也是看不到的.


    基础方案


    (() => {
     function ban() {
       setInterval(() => { debugger; }, 50);
    }
     try {
       ban();
    } catch (err) { }
    })();


    • setInterval 中的代码写在一行,可以禁止用户断点,即使添加 logpointfalse 也无用

    • 当然即使有些人想到用左下角的格式化代码,将其变成多行也是没用的


    浏览器宽高


    根据浏览器宽高、与打开F12后的宽高进行比对,有差值,说明打开了调试,则替换html内容;



    • 通过检测窗口的外部高度和宽度与内部高度和宽度的差值,如果差值大于 200,就将页面内容设置为 "检测到非法调试"。

    • 通过使用间隔为 50 毫秒的定时器,在每次间隔内执行一个函数,该函数通过创建一个包含 debugger 语句的函数,并立即调用该函数的方式来试图阻止调试器的正常使用。


    (() => {
     function block() {
       if (window.outerHeight - window.innerHeight > 200 || window.outerWidth - window.innerWidth > 200) {
         document.body.innerHTML = "检测到非法调试";
      }
       setInterval(() => {
        (function () {
           return false;
        }
        ['constructor']('debugger')
        ['call']());
      }, 50);
    }
     try {
       block();
    } catch (err) { }
    })();

    关闭断点,调整空页面


    在不打开发者工具的情况下,debugger是不会执行将页面卡住,而恰恰是利用debugger的这一点,如果你打开开发者工具一定会被debugger卡住,那么上下文时间间隔就会增加,在对时间间隔进行判断,就能巧妙的知道绝对开了开发者工具,随后直接跳转到空白页,一气呵成。(文心一言采用方案)


    setInterval(function () {
     var startTime = performance.now();
     // 设置断点
     debugger;
     var endTime = performance.now();
     // 设置一个阈值,例如100毫秒
     if (endTime - startTime > 100) {
       window.location.href = 'about:blank';
    }
    }, 100);

    第三方插件


    disable-devtool



    disable-devtool可以禁用所有一切可以进入开发者工具的方法,防止通过开发者工具进行的代码搬运。



    该库有以下特性:



    1. 支持可配置是否禁用右键菜单

    2. 禁用 f12 和 ctrl+shift+i 等快捷键

    3. 支持识别从浏览器菜单栏打开开发者工具并关闭当前页面

    4. 开发者可以绕过禁用 (url参数使用tk配合md5加密)

    5. 多种监测模式,支持几乎所有浏览器(IE,360,qq浏览器,FireFox,Chrome,Edge...)

    6. 高度可配置、使用极简、体积小巧

    7. 支持npm引用和script标签引用(属性配置)

    8. 识别真移动端与浏览器开发者工具设置插件伪造的移动端,为移动端节省性能

    9. 支持识别开发者工具关闭事件

    10. 支持可配置是否禁用选择、复制、剪切、粘贴功能

    11. 支持识别 eruda 和 vconsole 调试工具

    12. 支持挂起和恢复探测器工作

    13. 支持配置ignore属性,用以自定义控制是否启用探测器

    14. 支持配置iframe中所有父页面的开发者工具禁用


    🦂使用🦂


    <script disable-devtool-auto src='https://cdn.jsdelivr.net/npm/disable-devtool'>script>

    更多使用方法参见官网:disable-devtool


    disable-devtool



    console-ban禁止 F12 / 审查开启控制台,保护站点资源、减少爬虫和攻击的轻量方案,支持重定向、重写、自定义多种策略。



    使用


    <head>

    <script src="https://cdn.jsdelivr.net/npm/console-ban@5.0.0/dist/console-ban.min.js">script>
    <script>
     
    // default options
     
    ConsoleBan.init()
     
    // custom options
     
    ConsoleBan.init({
       
    redirect: '/404'
      })
    script>
    head>

    在项目中使用:


      yarn add console-ban

    import { init } from 'console-ban'

    init(options)

    重定向


    ConsoleBan.init({
    // 重定向至 /404 相对地址
    redirect: '/404',
    // 重定向至绝对地址
    redirect: 'http://domain.com/path'
    })

    使用重定向策略可以将用户指引到友好的相关信息地址(如网站介绍),亦或是纯静态 404 页面,高防的边缘计算或验证码等页面。


    注:若重定向后的地址可以通过 SPA 路由切换或 pjax 局部加载技术等进行非真正意义上的页面切换,则切换后的控制台监测将不会再次生效,对于 SPA 你可以在路由卫士处重新注册本实例,其他情况请引导至真正的其他页面。


    重写


    var div = document.createElement('div')
    div.innerHTML = '不要偷看啦~'

    ConsoleBan.init({
    // 重写 body 为字符串
    write: '

    不要偷看啦~

    '
    ,
    // 可传入节点对象
    write: div
    })

    重写策略可以完全阻断对网站内容的审查,但较不友好,不推荐使用。


    回调函数


    ConsoleBan.init({
    callback: () => {
      // ...
    }
    })

    回调函数支持自定义打开控制台后的策略。


    参数


    namerequiredtypedefaultdescription
    clearnobooleantrue禁用 console.clear 函数
    debugnobooleantrue是否开启定时 debugger 反爬虫审查
    debugTimenonumber3000定时 debugger 时间间隔(毫秒)
    redirectnostring-开启控制台后重定向地址
    writenostring 或Element-开启控制台后重写 document.body 内容,支持传入节点或字符串
    callbacknoFunction-开启控制台后的回调函数
    bfcachenobooleantrue禁用 bfcache 功能

    注:redirectwritecallback 三种策略只能取其一,优先使用回调函数。


    参考文章


    禁止别人调试自己的前端页面代码


    前端防止恶意调试


    禁止调试,阻止浏览器F12开发者工具


    前端防止调试技术


    结语


    需要注意的是,这些技术可以增加攻击者分析和调试代码的难度,但无法完全阻止恶意调试。因此,对于一些敏感信息或关键逻辑,最好的方式是在后端进行处理,而不是完全依赖前端来保护。


    下篇文章主要介绍如何破解这些禁止调试的方法。


    矛与盾:⚔️不让我在控制台上调试,哼,休想🛠️


    作者:Aplee
    来源:juejin.cn/post/7368313344712179739
    收起阅读 »

    DeepSeek引发行业变局,2025 IT人该如何破局抓住机遇

    一. 🎯 变局中抓住核心 这个春节被DeepSeek消息狂轰滥炸,很多做IT朋友已经敏锐的意识到 一场变局已经酝酿,整个IT行业都将迎来洗牌重塑。 中小IT企业、个人创业者、普通人该如何面对这场变局,如何不被市场淘汰,如何抓住机遇? 先说结论 2025年,谁能...
    继续阅读 »

    image.png


    一. 🎯 变局中抓住核心


    这个春节被DeepSeek消息狂轰滥炸,很多做IT朋友已经敏锐的意识到 一场变局已经酝酿,整个IT行业都将迎来洗牌重塑。 中小IT企业、个人创业者、普通人该如何面对这场变局,如何不被市场淘汰,如何抓住机遇?


    先说结论


    2025年,谁能将


    🔥技术热点 转换成 🚀业务引擎


    谁就能在这场变局中抢得先机


    2025年,选择躺平视而不见,以后的路将越来越窄


    二. 🧐 AI巨头垄断,小公司别硬刚


    头部AI/大模型厂商 (OpenAI、DeepSeek、字节、阿里、百度…)


    通过大模型底座控制生态入口


    中小IT公司沦为“AI插件开发者”


    ⬇️


    说直白点就是别学大厂烧钱训练大模型


    “不要用你搬砖攒下的血汗钱挑战巨头们躺赚的钱袋子”


    合理的生存之计是:



    • 直接调用低成本接入大厂的大模型能力

    • 通过云服务+开源模型聚焦1-2个细分垂直赛道开发领域专属大模型应用



    当然你也可以不信邪

    学习DeepSeek不走寻常路

    十年量化无人问,一朝DS天下知

    闷声鼓捣一个大的



    三. 🖊️ 产品思维要转变


    对于产品现在客户要的不是功能,是智商


    产品的设计思路一定是


    从功能导向 ➡️ 智能导向


    堆功能堆指标是底限,堆智能才是上限


    无论是硬件还是软件公司,殊途同归


    卖硬件 ➡️ 卖智能,卖软件 ➡️ 卖智能


    四. 🔧 定制化服务市场潜力大


    虽然AI巨头都推出了N个


    行业标准化AI解决方案


    以近乎成本价抢占市场


    但是,中国客户还是喜欢”定制化“


    有数据统计,60%以上的行业需求无法被标准化方案满足



    • 中小IT公司:



      • 大厂不愿做,我做 📣

      • 大厂不屑做,我做 📣

      • 大厂不会做,我做 📣




    比如,


    现在做企业AI应用开发


    需要触碰企业长年积累的数据


    客户有很强意识👉🏻这是核心资产


    所以开发时,就要求定制化+本地化



    • 只有定制化,才能构建数据护城河

    • 只有定制化,客户对数据隐私才放心
      ...


    也许这不是真理,但却是刚需


    总之,客户定制化理由千千万万


    这就是IT人的机会


    五. 💰 在你懂而别人不懂的领域赚钱


    小公司



    • 聚焦“AI+垂直场景”做深行业Know-How

    • 避免与通用大模型正面竞争


    中等公司



    • 构建“私有化模型+数据闭环”

    • 在特定领域建立技术壁垒


    六. 💯 存量市场以稳为主,增量市场探索可能


    存量业务



    • 用AI改造现有产品和客户场景

    • 对于已经稳定的客户和产品应当积极引入 AI 技术进行升级改造


    增量市场



    • 探索AI原生需求

    • 要善于挖掘客户对AI的新需求并及时满足,抢占市场先机


    此过程中,有两点需要注意



    • 敏捷性 > 规模



      • 快速试错、小步快跑的模式比巨额投入更重要



    • 场景落地 > 技术炫技



      • 能解决具体业务痛点的“60分AI方案”比追求“99分技术指标”更易存活




    七. 💥 纯技术团队将面临淘汰


    开发团队



    • 必须重构开发流程

    • 建立“AI+人工”混合开发模式

    • 开发流程需和AI工具链深度集成

    • 开发不要过重,采用轻量化技术路线


    部署和运维团队



    • 同样建立“AI+人工”混合运维模式

    • 智能运维手段(故障预测、根因分析)将成标配

    • 内部要刻意培养AI-Aware工程师



    未来技术人员的筛选条件可能不再是年龄、学历、工作经验而是你有没有 AI Awareness



    八. 📝 总结


    在这场变局中能活好的普通IT公司,AI创业者


    不一定是技术最强的


    而是最会借力AI


    用行业经验+客户积累+AI工具


    做巨头看不上的 “小而美”生意 🤩


    作者:赛博东哥CyberFD
    来源:juejin.cn/post/7468203211725783094
    收起阅读 »

    表妹问:前端好玩吗?我说好玩,但表妹接下来的回复看哭了我

    web
    表妹问:前端好玩吗?我说好玩,但表妹接下来的回复看哭了我。 是的,回复如下: 这红海血途上,新兵举着 "大前端" 旌旗冲锋,老兵拖着node_modules残躯撤退。资本织机永不停歇,框架版本更迭如暴君换季,留下满地deprecated警告如秋后落叶。 其一...
    继续阅读 »

    表妹问:前端好玩吗?我说好玩,但表妹接下来的回复看哭了我。


    微信聊天图片 - 20250226100527.png


    是的,回复如下:


    这红海血途上,新兵举着 "大前端" 旌旗冲锋,老兵拖着node_modules残躯撤退。资本织机永不停歇,框架版本更迭如暴君换季,留下满地deprecated警告如秋后落叶。


    其一、夹缝中的苦力


    世人都道前端易,不过调接口、改颜色,仿佛稚童搭积木。却不知那屏幕上寸寸像素之间,皆是血泪。产品拍案,需求朝夕三变,昨日之红蓝按钮,今晨便成黑白圆角。UI稿纸翻飞如雪,设计师手持“用户体验”四字大旗,将五更赶工的代码尽数碾碎。后端端坐高台,接口文档空悬如镜花水月,待到交付时辰,方抛来残缺数据。此时节,前端便成了那补天的女娲,于混沌中捏造虚拟对象,用JSON.parse('{"data": undefined}')这等荒诞戏法,将虚无粉饰成真实。


    看这段代码何等悲凉:


    // 后端曰:此接口返data字段,必不为空
    fetch('api/data').then(res => {
    const { data } = res;
    render(data[0].children[3].value || '默认值'); // 层层掘墓,方见白骨
    });

    此乃前端日常——在数据废墟里刨食,用||?.铸成铁锹,掘出三分体面。


    其二、技术的枷锁


    JavaScript本是脚本小儿,如今却要扛鼎江山。君不见React、Vue、Angular三座大山压顶,每年必有新神像立起。昨日方学得Redux真经,今朝GraphQL又成显学。更有Electron、ReactNative、Flutter诸般法器,教人左手写桌面应用,右手调移动端手势。所谓“大前端”,实乃资本画饼之术,人前跨端写,人后页面仔——既要马儿跑,又言食草易。


    且看这跨平台代码何等荒诞:


    // 一套代码统治三界(iOS/Android/Web)
    <View>
    {Platform.OS === 'web' ?
    <div onClick={handleWebClick} /> :
    <TouchableOpacity onPress={handleNativePress} />
    }
    </View>

    此类缝合怪代码,恰似给长衫打补丁,既失体统,又损性能。待到内存泄漏、渲染卡顿时,众人皆指前端曰:"此子学艺不精!"


    何人怜悯前端 node18 react19 逐人老,后端写着 java8 看着 java22 笑。


    其三、尊严的消亡


    领导提拔,必先问尔可懂SpringBoot、MySQL分库分表?纵使前端用WebGL绘出三维宇宙,用WebAssembly重写操作系统,在会议室里仍是“做界面的”。工资单上数字最是直白——同司后端新人起薪一万五,前端老将苦熬三年方摸得此数。更可笑者,产品经理醉酒时吐真言:"你们不就是改改CSS么?"


    再看这可视化代码何等心酸:


    // 用Canvas画十万级数据点
    ctx.beginPath();
    dataPoints.forEach((point, i) => {
    if (i % 100 === 0) ctx.stroke(); // 分段渲染防卡死
    ctx.lineTo(point.x, point.y);
    });

    此等精密计算,在他人眼中不过"动画效果",与美工修图无异。待浏览器崩溃,众人皆曰:"定是前端代码劣质!"


    技术大会,后端高谈微服务、分布式,高并发,满座掌声如雷,实则系统使用量百十来人也是远矣;前端言及 CSS 栅格、浏览器渲染,众人瞌睡连天。领导抚掌笑曰:“后端者,国之重器;前端者,雕虫小技。” 晋升名单,后端之名列如长蛇,前端者埋没于墙角尘埃。纵使将那界面写出花来,终是 “美工” 二字定终身。


    其四、维护者的悲歌


    JavaScript本无类型,如野马脱缰。若非经验老道之一,常写出这等代码:


    function handleData(data) {
    if (data && typeof data === 'object') { // 万能判断
    return data.map(item => ({
    ...item,
    newProp: item.id * Math.random() // 魔改数据
    }));
    }
    return []; // 默认返回空阵,埋下百处报错
    }

    此类代码如瘟疫蔓延,领导却言“这些功能实习生也能写!”,却不顾三月后连作者亦不敢相认,只得下任前端难上加难。


    而后端有Type大法,编译检查护体,有Swagger契约,有Docker容器,纵使代码如乱麻,只需扩内存、增实例,便可遮掩性能疮疤。


    其五、末路者的自白


    诸君且看这招聘启事:"需精通Vue3+TS+Webpack,熟悉React/Node.js,有Electron/小程序经验,掌握Three.js/WebGL者重点考虑。" 薪资却标着"6-8K"。更有机智者发明"全栈"之名,实欲以一人之躯,承三头六臂之劳。


    再看这面试题何等荒谬:


    // 手写Promise实现A+规范
    class MyPromise {
    // 三千行后,方知自己仍是蝼蚁
    }

    此等屠龙之术,入职后唯调API用。恰似逼庖丁解牛,却令其日日杀鸡。


    或以使用组件库之经验薪资招之,又以写不好组件库之责裁出。


    尾声:铁屋中的叩问


    前端者,数字化时代的纺织工也。资本织机日夜轰鸣,框架如梭穿行不息。程序员眼底血丝如网。所谓"全栈工程师",实为包身工雅称;所谓"技术革新",不过剥削新法。


    若仍有少年热血未冷,欲投身此业,且听我一言:君有凌云志,何不学Rust/C++,做那操作系统、数据库等真·屠龙技?莫要困在这CSS牢笼中,为圆角像素折腰,为虚无需求焚膏。前端之路,已是红海血途,望后来者三思,三思!


    作者:四叶草会开花
    来源:juejin.cn/post/7475351155297402891
    收起阅读 »

    Spring 6.0 + Boot 3.0:秒级启动、万级并发的开发新姿势

    Spring生态重大升级全景图 一、Spring 6.0核心特性详解 1. Java版本基线升级 最低JDK 17:全面拥抱Java模块化特性,优化现代JVM性能 虚拟线程(Loom项目):轻量级线程支持高并发场景(需JDK 19+) // 示例:虚拟...
    继续阅读 »

    Spring生态重大升级全景图


    Spring 6.0 + Boot 3.0 技术体系.png




    一、Spring 6.0核心特性详解


    1. Java版本基线升级



    • 最低JDK 17:全面拥抱Java模块化特性,优化现代JVM性能

    • 虚拟线程(Loom项目):轻量级线程支持高并发场景(需JDK 19+)


    // 示例:虚拟线程使用
    Thread.ofVirtual().name("my-virtual-thread").start(() -> {
    // 业务逻辑
    });




      1. 虚拟线程(Project Loom)



    • 应用场景:电商秒杀系统、实时聊天服务等高并发场景


    // 传统线程池 vs 虚拟线程
    // 旧方案(平台线程)
    ExecutorService executor = Executors.newFixedThreadPool(200);
    // 新方案(虚拟线程)
    ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor();
    // 处理10000个并发请求
    IntStream.range(0, 10000).forEach(i ->
    virtualExecutor.submit(() -> {
    // 处理订单逻辑
    processOrder(i);
    })
    );

    2. HTTP接口声明式客户端



    • @HttpExchange注解:类似Feign的声明式REST调用


    @HttpExchange(url = "/api/users")
    public interface UserClient {
    @GetExchange
    List<User> listUsers();
    }

    应用场景:微服务间API调用


    @HttpExchange(url = "/products", accept = "application/json")
    public interface ProductServiceClient {
    @GetExchange("/{id}")
    Product getProduct(@PathVariable String id);
    @PostExchange
    Product createProduct(@RequestBody Product product);
    }
    // 自动注入使用
    @Service
    public class OrderService {
    @Autowired
    private ProductServiceClient productClient;

    public void validateProduct(String productId) {
    Product product = productClient.getProduct(productId);
    // 校验逻辑...
    }
    }

    3. ProblemDetail异常处理



    • RFC 7807标准:标准化错误响应格式


    {
    "type": "https://example.com/errors/insufficient-funds",
    "title": "余额不足",
    "status": 400,
    "detail": "当前账户余额为50元,需支付100元"
    }


    • 应用场景:统一API错误响应格式


    @RestControllerAdvice
    public class GlobalExceptionHandler {
    @ExceptionHandler(ProductNotFoundException.class)
    public ProblemDetail handleProductNotFound(ProductNotFoundException ex) {
    ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.NOT_FOUND);
    problem.setType(URI.create("/errors/product-not-found"));
    problem.setTitle("商品不存在");
    problem.setDetail("商品ID: " + ex.getProductId());
    return problem;
    }
    }
    // 触发异常示例
    @GetMapping("/products/{id}")
    public Product getProduct(@PathVariable String id) {
    return productRepo.findById(id)
    .orElseThrow(() -> new ProductNotFoundException(id));
    }

    4. GraalVM原生镜像支持



    • AOT编译优化:启动时间缩短至毫秒级,内存占用降低50%+

    • 编译命令示例:


    native-image -jar myapp.jar



    二、Spring Boot 3.0突破性改进


    1. 基础架构升级



    • Jakarta EE 9+:包名javax→jakarta全量替换

    • 自动配置优化:更智能的条件装配策略



      1. OAuth2授权服务器
        应用场景:构建企业级认证中心




    # application.yml配置
    spring:
    security:
    oauth2:
    authorization-server:
    issuer-url: https://auth.yourcompany.com
    token:
    access-token-time-to-live: 1h

    定义权限端点


    @Configuration
    @EnableWebSecurity
    public class AuthServerConfig {
    @Bean
    public SecurityFilterChain authServerFilterChain(HttpSecurity http) throws Exception {
    http
    .authorizeRequests(authorize -> authorize
    .anyRequest().authenticated()
    )
    .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
    return http.build();
    }
    }

    2. GraalVM原生镜像支持


    应用场景:云原生Serverless函数


    # 打包命令(需安装GraalVM)
    mvn clean package -Pnative
    # 运行效果对比
    传统JAR启动:启动时间2.3s | 内存占用480MB
    原生镜像启动:启动时间0.05s | 内存占用85MB

    3. 增强监控(Prometheus集成)



    • Micrometer 1.10+:支持OpenTelemetry标准

    • 全新/actuator/prometheus端点:原生Prometheus格式指标

    • 应用场景:微服务健康监测


    // 自定义业务指标
    @RestController
    public class OrderController {
    private final Counter orderCounter = Metrics.counter("orders.total");
    @PostMapping("/orders")
    public Order createOrder() {
    orderCounter.increment();
    // 创建订单逻辑...
    }
    }
    # Prometheus监控指标示例
    orders_total{application="order-service"} 42
    http_server_requests_seconds_count{uri="/orders"} 15



    三、升级实施路线图


    升级准备阶段.png


    四、新特性组合实战案例


    场景:电商平台升级


    // 商品查询服务(组合使用新特性)
    @RestController
    public class ProductController {
    // 声明式调用库存服务
    @Autowired
    private StockServiceClient stockClient;
    // 虚拟线程处理高并发查询
    @GetMapping("/products/{id}")
    public ProductDetail getProduct(@PathVariable String id) {
    return CompletableFuture.supplyAsync(() -> {
    Product product = productRepository.findById(id)
    .orElseThrow(() -> new ProductNotFoundException(id));

    // 并行查询库存
    Integer stock = stockClient.getStock(id);
    return new ProductDetail(product, stock);
    }, Executors.newVirtualThreadPerTaskExecutor()).join();
    }
    }



    四、升级实践建议



    1. 环境检查:确认JDK版本≥17,IDE支持Jakarta包名

    2. 渐进式迁移

      • 先升级Spring Boot 3.x → 再启用Spring 6特性

      • 使用spring-boot-properties-migrator检测配置变更



    3. 性能测试:对比GraalVM原生镜像与传统JAR包运行指标


    通过以上升级方案:



    1. 使用虚拟线程支撑万级并发查询

    2. 声明式客户端简化服务间调用

    3. ProblemDetail统一异常格式

    4. Prometheus监控接口性能




    本次升级标志着Spring生态正式进入云原生时代。重点关注:虚拟线程的资源管理策略、GraalVM的反射配置优化、OAuth2授权服务器的定制扩展等深度实践方向。


    作者:后端出路在何方
    来源:juejin.cn/post/7476389305881296934
    收起阅读 »

    如何优雅的回复面试官问:“你能接受加班吗?”

    面试官问:“你能接受加班吗?”我脑袋嗡的一声,余音绕梁三日不绝于耳。 那一刻,我简直觉得自己像被突然砸中脑袋,脑袋里嗡的一声,余音绕梁三日。作为一个职场小白,这种问题简直颠覆了我对面试的认知。于是,我一时心血来潮,脱口而出一句:“领导抗揍吗?” 结果,大家猜到...
    继续阅读 »

    面试官问:“你能接受加班吗?”我脑袋嗡的一声,余音绕梁三日不绝于耳。


    那一刻,我简直觉得自己像被突然砸中脑袋,脑袋里嗡的一声,余音绕梁三日。作为一个职场小白,这种问题简直颠覆了我对面试的认知。于是,我一时心血来潮,脱口而出一句:“领导抗揍吗?” 结果,大家猜到了,面试是上午结束的,Offer是当天中午凉的。


    是牛是马


    如何巧妙回答


    “我认为加班是工作中不可避免的一部分,尤其是在一些特殊项目或紧急情况下。我非常热爱我的工作,并且对公司的发展充满信心,因此我愿意为了团队的成功付出额外的努力。当然,我也注重工作效率和时间管理,尽量在正常工作时间内完成任务。如果确实需要加班,我也会根据公司合理的安排,积极的响应。”


    作为一名资深的面试官,今天面对这个问题,坐下来和大家聊聊应该怎么回答呢?面试官究竟喜欢怎样的回答?让我们深入分析一下。


    面试官的心理


    在职场中,想要出色地应对面试,需要具备敏锐的观察力和理解力。学会细致入微地观察,善于捕捉每一个细微的线索,这样才能在面试中游刃有余。懂的察言观色,方能尽显英雄本色。


    请在此添加图片描述


    面试官的考量点



    • 评估工作稳定性


    面试官提出“能否接受加班”的问题,旨在深入了解求职者的职业稳定性和对加班安排的适应性。这一评估有助于预测求职者入职后的表现和长期留任的可能性。工作稳定性是企业考量员工的关键指标之一,通过这一问题,面试官能够洞察求职者的职业发展规划及其对未来工作的期望。



    • 筛选合适的候选人


    通过询问加班的接受度,面试官筛选出那些愿意为达成工作目标而投入额外时间和精力的候选人。这种筛选方式有助于确保团队的整体运作效率和协作精神。合适的候选人不仅能快速融入团队,还能显著提升工作效率。因此,面试官借此问题寻找最匹配岗位需求的员工。



    • 了解求职者的价值观


    面试官还利用这个问题来探查求职者的价值观和工作态度,以此判断他们是否与公司的文化和核心价值观相契合。员工的价值观和态度对公司的长远发展起着至关重要的作用。通过这一询问,面试官能够确保求职者的个人目标与公司的发展方向保持一致,从而促进整体的和谐与进步。


    考察的问题的意义


    要理解问题的本质……为什么面试官会提出这样的问题?难道是因为你的颜值过高,引发了他的嫉妒?


    请在此添加图片描述



    • 工作态度


    面试官通过询问加班的接受度,旨在评估求职者是否展现出积极的工作态度和强烈的责任心。在许多行业中,加班已成为常态,面试官借此问题了解求职者是否愿意在工作上投入额外的时间和精力。积极的工作态度和责任心是职场成功的关键因素,通过这一问题,面试官能够初步判断求职者是否适应高强度的工作环境。



    • 岗位匹配度


    特定岗位因其工作性质可能需要频繁加班。面试官通过提出加班相关的问题,旨在了解求职者是否能适应这类岗位的工作强度。由于不同岗位对工作强度的要求各异,面试官希望通过这一问题确保求职者对即将承担的角色有明确的认识,从而防止入职后出现期望不一致的情况。



    • 抗压能力


    加班往往伴随压力,面试官通过这一问题考察求职者的抗压能力和情绪管理技巧。抗压能力对于职场成功至关重要,面试官借此了解求职者在高压环境下的表现,以判断其是否符合公司的需求。



    • 公司文化


    面试官还利用这个问题来评估求职者对公司加班文化的接受程度,以此判断其价值观是否与公司相符。公司文化对员工的工作体验和满意度有着深远的影响,面试官希望通过这一问题确保求职者能够认同并融入公司文化。


    回答的艺术


    “知己知彼,百战不殆。”在面试中,回答问题的关键在于展现出积极和正向的态度。


    请在此添加图片描述



    • 积极态度


    在回答有关加班的问题时,表达你对工作的热爱和对公司的忠诚,强调你愿意为了团队的成功而付出额外的努力。这种积极的态度不仅展示了你的职业素养和对工作的热情,还能显著提升面试官对你的好感。


    例如:“我非常热爱我的工作,并且对公司的发展充满信心。我相信为了实现公司的目标和团队的成功,适当的加班是可以接受的。”



    • 灵活性和效率


    强调你在时间管理和工作效率上的能力,表明你在确保工作质量的同时,会尽可能减少加班的需求。灵活性和效率是职场中极为重要的技能,面试官可以通过这个问题了解你的实际工作表现。


    例如:“我在工作中注重效率和时间管理,通常能够在规定的工作时间内完成任务。当然,如果有特殊情况需要加班,我也会全力以赴。”



    • 平衡工作与生活


    适当地提到你对工作与生活平衡的重视,并希望公司在安排加班时能够充分考虑到员工的个人需求。平衡工作与生活是职场人士普遍关注的问题,面试官通过这个问题可以了解你的个人需求和期望。


    例如:“我非常重视工作与生活的平衡,希望在保证工作效率的同时,也能有足够的时间陪伴家人和进行个人活动。如果公司能够合理安排加班时间,我会非常乐意配合。”



    • 适度反问


    在回答时,可以适当地向面试官询问关于公司加班的具体情况,以便更全面地了解公司的加班文化和预期。这样的反问可以展现你的主动性和对公司的兴趣,有助于获取更多信息,做出更加明智的回答。


    例如:“请问公司通常的加班情况是怎样的?是否有相关的加班补偿或调休安排?”


    最后


    所谓士为知己者死,遇良将则冲锋陷阵,择良人则共谋天下。在职场这场没有硝烟的战争中,我们每个人都是一名战士,寻找着属于自己的知己和良将。当面试官提出挑战性问题时,我们不仅要展示自己的能力和才华,更要表现出对工作的热爱和对公司的忠诚。


    面对“你能接受加班吗?”这样的问题,我们应以积极的态度、灵活的思维和对工作与生活平衡的重视来回应。这样的回答不仅能展示我们的职业素养,还能让我们在众多求职者中脱颖而出,赢得面试官的青睐。


    正如士为知己者死,我们在职场中也要找到那个能理解我们、支持我们的知己;遇良将则冲锋陷阵,我们要在优秀的领导下发挥自己的潜能,为公司的发展贡献力量;择良人则共谋天下,我们要与志同道合的同事共同努力,实现职业生涯的辉煌。


    总之一句话,在面试中展现出积极向上的形象,不仅能为我们的职业生涯加分,更能让我们在职场上找到属于自己的价值和归属感。让我们以这句话为指引,勇敢地迎接职场的挑战,书写属于自己的辉煌篇章。


    作者:不惑_
    来源:juejin.cn/post/7457211584709066792
    收起阅读 »

    DeepSeek R1 对编程语言未来的预测

    欢迎关注我的训练营:云原生AI实战营,一个助力 Go 开发者在 AI 时代建立技术竞争力的实战营。 今天分享下 DeepSeek 对于编程语言未来的预测,这个应该也是很多开发者关注的,因为它可能会影响未来的职业选择。 因我自己最近也在关注这方面,所以今天就问...
    继续阅读 »

    欢迎关注我的训练营:云原生AI实战营,一个助力 Go 开发者在 AI 时代建立技术竞争力的实战营。



    今天分享下 DeepSeek 对于编程语言未来的预测,这个应该也是很多开发者关注的,因为它可能会影响未来的职业选择。


    因我自己最近也在关注这方面,所以今天就问了 DeepSeek。DeepSeek 给出的回答跟我的想法很一致,所以今天把 DeepSeek 的回答分享出来。


    问题:Go、Java、Rust、Python 语言未来前景如何


    我的问题是Go、Java、Rust、Python 语言未来前景如何。之所以问这 4 门语言,是因为这四门语言在后端开发中最受欢迎,各自在其擅长的领域中发光发热。


    目前,我觉得 Go 和 Java 在领域上有一定程度或者很大程度的重合。Rust、Python、(Go、Java)有其擅长的领域,交叉点不多。


    我想弄明白的是,每门语言是否会在其领域继续发展状态,或者会有蚕食其他领域的现象。通过这些,也想知道未来编程语言的选型为何。


    DeepSeek 思考过程



    提示:以下是 DeepSeek 的思考过程



    好的,用户问的是Go、Java、Rust、Python这四种语言的未来前景如何。我需要逐一分析每种语言的特点、现状以及发展趋势,同时比较它们的优缺点和应用场景。首先,我应该回忆每种语言的主要用途和社区活跃度,然后看看市场需求和技术趋势。


    对于 Go 语言,它由Google开发,以高并发和高效编译著称,适合云原生和微服务。近年来Docker、Kubernetes的流行推动了Go的应用,尤其是在后端和DevOps领域。但Go在泛型和生态系统方面可能不如其他语言,所以需要评估这些因素是否会影响其未来。


    Java作为老牌企业级语言,稳定性强,广泛应用于大型系统、Android开发和大数据。虽然有人认为Java在下降,但它的庞大现有代码库和企业依赖意味着它仍会长期存在。不过,Java需要面对Kotlin等语言的竞争,尤其是在Android开发方面,还有云原生趋势下轻量级语言的挑战。


    Rust以内存安全和性能为卖点,逐渐在系统编程、嵌入式和高性能计算领域崭露头角。被Linux内核和Windows驱动采用,说明其潜力。但Rust的学习曲线陡峭,可能在普及上需要时间,但在对安全性要求高的领域会持续增长。


    Python作为脚本语言之王,在数据科学、AI、Web开发等领域占据主导。随着AI的爆发,Python的需求持续增长,但性能问题可能在需要高性能的场景中成为瓶颈。不过,Python的易用性和丰富的库支持使其在教育和小型项目中保持优势。


    接下来要考虑的是行业趋势,比如云计算的持续增长对Go有利,AI发展促进Python,系统级需求推动Rust,而企业维护则维持Java。同时,新语言的出现是否会影响这些老牌语言,例如Rust可能替代部分C/C++的领域,而Go可能继续在云原生领域扩展。


    用户可能的深层需求是选择学习方向或技术选型,需要明确各语言的优劣势和适用场景,帮助他们做出决策。因此,回答时要突出各语言的优势领域和未来可能的发展方向,以及潜在的风险点,比如Java可能面临市场份额被蚕食,但不会很快消失。


    总结部分需要对比各语言,指出它们在不同领域的地位,并给出建议,比如根据项目需求选择合适语言,而不是盲目跟风。同时,强调多语言掌握的重要性,因为现代开发往往需要多种技术结合。


    DeepSeek R1 四种语言前景分析


    以下是 DeepSeek R1 关于 Go、Java、Rust、Python 四种语言的前景分析,结合技术趋势、市场需求和生态系统发展分析如下:


    1. Go (Golang)


    优势领域



    • 云原生与微服务:Go 的并发模型(goroutine)和高效编译特性使其成为 Kubernetes、Docker、Istio 等云原生工具的首选语言。

    • 高性能后端:适合 API 网关、分布式系统、实时通信等高并发场景(如 Twitch、Uber 的后端)。

    • 开发效率:语法简洁,编译速度快,适合快速迭代的工程团队。


    未来前景



    • 持续增长:随着云计算的普及,Go 在云原生领域的地位将进一步巩固。

    • 企业采用:越来越多公司(如字节跳动、腾讯)将 Go 作为核心后端语言。

    • 挑战:泛型支持的完善程度、生态多样性仍需提升。


    推荐方向:云原生开发、基础设施工具链、高性能服务端。


    2. Java


    优势领域



    • 企业级应用:银行、电信、政府等传统行业的核心系统(如 IBM、Oracle 的解决方案)。

    • Android 开发:仍占据主导地位(尽管 Kotlin 在 Android 中逐渐取代)。

    • 大数据生态:Hadoop、Spark、Flink 等框架依赖 Java 生态。


    未来前景



    • 稳定但缓慢衰退:Java 的存量市场庞大,但新项目可能更倾向于现代语言(如 Kotlin、Go)。

    • 云原生转型:Quarkus、Spring Native 等框架帮助 Java 适应云原生环境。

    • 挑战:冗长的语法、内存消耗较高,对轻量级场景竞争力不足。


    推荐方向:传统企业系统维护、Android 底层开发、大数据工程。


    3. Rust


    优势领域



    • 系统编程:取代 C/C++ 的潜力(如操作系统、嵌入式、浏览器引擎)。

    • 内存安全:所有权模型彻底解决内存泄漏和数据竞争问题,适合对安全性要求极高的场景(如区块链、金融系统)。

    • 高性能计算:在游戏引擎、实时渲染等领域逐渐应用(如 Discord 的音频服务)。


    未来前景



    • 高速增长:被 Linux 内核、Windows 驱动开发等官方支持,逐步进入主流。

    • 生态扩展:WebAssembly、区块链(如 Solana)等领域对 Rust 需求激增。

    • 挑战:学习曲线陡峭,中小型项目采用成本较高。


    推荐方向:系统级开发、安全敏感型应用、区块链底层。


    4. Python


    优势领域



    • 数据科学与 AI:TensorFlow、PyTorch、NumPy 等库的绝对统治地位。

    • 脚本与自动化:运维、测试、爬虫等场景的首选语言。

    • 教育与科研:语法简单,适合快速验证算法和原型开发。


    未来前景



    • AI 驱动的霸主:随着生成式 AI(如 ChatGPT)的爆发,Python 需求将持续增长。

    • 性能瓶颈:在需要高性能的场景(如高频交易)可能被 Rust/Go 取代。

    • 生态护城河:庞大的库和社区资源确保其长期不可替代性。


    推荐方向:机器学习工程、数据分析、快速原型开发。


    综合对比与趋势总结


    维度GoJavaRustPython
    增长潜力高(云原生)低(存量市场)极高(系统替代)高(AI 驱动)
    学习曲线中等中等偏高
    性能中等极高
    适用领域后端/基础设施企业系统/Android系统/安全/区块链AI/脚本/科研


    未来 5 年预测



    1. GoRust 将成为增长最快的语言,分别主导云原生和系统级开发。

    2. Python 因 AI 热潮保持霸主地位,但需警惕性能相关的替代方案。

    3. Java 缓慢衰退,但仍长期占据企业市场。


    建议



    • 根据职业目标选择:

      • 追求高薪和前沿技术:优先 Rust 或 Go。

      • 稳重型职业发展:Java 或 Python。



    • 多语言组合:例如 Python(AI) + Go(后端)Rust(底层) + Python(脚本)


    个人观点



    提示:以下是我个人总结,非 DeepSeek 的输出。



    DeepSeek R1 对于四种语言的前景分析、未来 5 年预测及给出的建议,其实跟我的想法是完全一致的。


    根据 JetBrains2024 的市场调研,Go 在四种语言中的工资是最高的,背后的原因可能是因为云原生的技术门槛。Go 在云原生领域占据霸主地位,而云原生本身技术门槛要比业务 CURD 高很多,所以薪资自然要比其他语言高。其实,这也说明了,越是门槛高、技术含量高的岗位薪资越有竞争力(这是一句废话)。


    Python 依托于其强大的生态及数据处理能力,在 AI 领域成为霸主语言。在 AI 时代,程序员不可避免的要去学习、掌握 Python。


    Rust 优点是高性能、高安全,但缺点是学习门槛高、开发效率相较于 Go 低。所以 Rust 很适合系统层软件的开发,反倒不适合应用层软件的开发。在一个业务向的企业中,一般系统层会使用云基础设施,非核心的系统层,也可以用 Go 语言开发。当前很多优秀的系统层软件都是基于 Go 语言开发的。


    所以,在一个企业中,系统层软件和应用层软件,往往是二选一的关系,也意味着,在编程语言上,也有了明确的选择:应用型企业选择 Go。如果企业核心产品是系统层软件,那么可以选择 Rust。


    所以最终的编程语言选择一般是:Python(AI) + Go(后端) 或 Rust(底层) + Python(AI)。


    当然,企业完全可以根据需要选择更多的编程技术栈组合。上述只是介绍一种通用情况下的选择建议。


    另外,在编程语言选择时,建议主攻一门核心语言,同时根据职业目标补充其他相关语言,或者在不同阶段调整策略。这样既避免单一风险,又保持专业性。


    作者:孔令飞
    来源:juejin.cn/post/7475609849939410983
    收起阅读 »

    央国企求职“性价比分析”:为什么这几年央国企火了?

    浣熊say官方网站:hxsay.com/ 浣熊say官方星球:​hxsay.com/about/我的星球/… 正文 不知道最近大家有没有发现,越来越多的人在职业选择上都偏向与央国企、体制内等稳定性较高的岗位,而放弃了去私企、互联网等工资高但是强度大的工作。 从...
    继续阅读 »


    浣熊say官方网站:hxsay.com/


    浣熊say官方星球:​hxsay.com/about/我的星球/…


    正文


    不知道最近大家有没有发现,越来越多的人在职业选择上都偏向与央国企、体制内等稳定性较高的岗位,而放弃了去私企、互联网等工资高但是强度大的工作。


    从我身边的人了解到这一趋势不仅仅存在于工作了多年的职场老油条,希望找个地方躺平,在应届毕业生的群体里面也越来越明显。然而放在10年前,也就是2014年的时候谁毕业要是去国企、体制内可能会被笑话没有理想、躺平。


    但是这两年风向仿佛突然变化了,公务员、央国企突然之间变得香了起来,似乎打工人也随着年龄的增长突然明白了一个道理,比起靠着燃烧生命加班挣来的卖命钱以及生活在不知道什么时候就会被干掉的压力之下,不如稳定的央国企来得实在。


    35岁被毕业和干到退休的收入差距有多大?


    首先叠甲,我这里说的国企是垄断央企的二级以上的公司或者省属国企总部,这些国企一般掌握着国家、省级的核心资源,不违法犯罪的情况下大概率是能干到退休的。当然,如果有人跟我杠说什么某某银行科技子公司,某某央企的孙子公司一样末尾淘汰,一样裁员不稳定,那么我只能说你说得都对!


    假设我硕士毕业就去国企,然后月薪8k,2个月年终(央企二级公司,省属国企很容易达到),那么一年的收入是14*0.8 = 11.2w,然后男性目前的法定退休年龄是65岁,从25岁~65岁工作40年,总收入为 448w。


    假设你硕士毕业就去互联网大厂,然后月薪3w,4个月年终(这里也是取得平均值来估计的),那么一年的收入为48w,然后35岁一般确实是互联网的大限,25~35岁工作10年,总收入为:480w。


    其实,大多数情况下互联网大厂拿到3w的也是凤毛麟角,国企8k一个月的其实还是遍地都是,甚至一些省会的公务员都能达到8k/月甚至更多,两者职业生涯的总收入其实是差不多的。而且这里为了公平都没有考虑随着工龄增长工资的增长情况,其实在互联网大厂拿到100w年薪的难度远远大于你在国企熬年限,涨到1.5w。


    所以,其实无论是选择私企打工挣钱,还是垄断国企躺平,你整个职业生涯获得的工资性收入都是差不多的,以2024年的世界观来看,很多私企甚至很难稳定拿到3w/月的工资到35岁。


    有时候一个裁员毕业潮下来,你就不得不面临重新找工作的窘境,以前经济好的时候且没有AI时候,从事技术研发的人还可以自信的说我有技术走到哪里都不怕 。 如今,AI取代大多数工作岗位已经是明牌的事情了,那些掌握技术的人可能也不得不担忧自己能否快速找到合适自己的工作。


    虽然,最近两会有委员提出取消35岁的年龄限制,我其实个人并不看好这个提案,因为本质说社会上的私企卡35岁主要是因为廉价、能加班的年轻人太多了,企业处于成本考虑肯定愿意招聘这些年轻人,那么上了年龄的中年人不能加班就可以滚蛋了。 这个事情不是一个提案就能解决的,除非整个职场氛围得到了改变,所有公司都将老员工视作一种公司财富而不是消耗品的时候,才是35岁年龄其实真的消失的时候。


    普通打工人还真的需要考虑当你年龄上来之后,失去手头这份工作之后怎么办,你辛辛苦苦寒窗苦读这么多年,出入的高级写字楼,做的都是产值上千万的项目。突然让你失业在家,跑滴滴,送外卖这个心里落差你能接受吗?


    当35岁你在街头送着外卖,跑着滴滴,你在央国企的同学或许已经是一个小领导,你去当公务员的同学现在是一个科长,他们再不济也是个小职工有着稳定的收入,不太为生计发愁,不知道那个时候的同学聚会你还有心情去参加不?


    对于打工人来说稳定到底意味着什么?


    20多岁的年轻人总觉得世界是自己的,脑子里面全部是幻想,总觉得爽文小说当中的主角是自己,不说大富大贵至少能够在企业混的风生水起,升职加薪,当上领导。


    这些愣头青的想法我也有过,但是对大多数没有抱上大腿的人来说,工作2~3年之后就会让你明白这个世界的真实运转规则,很多事情不是下位者能够决定的,无论是在国企还是私企,本质上事情还是上位者说了算。


    简单来说就是,领导说你行你就是行,领导说你不行那么就赶紧想办法跑路吧。


    这种情况在私企、国企其实都会遇到,大家刻板印象中老是说国企的官僚主义严重,但是其实私企才是官僚主义更加严重的地方,而且比起来国企就是小打小闹。


    本质上来说在真的央国企你的领导实际上是没有人事权的,他就算再讨厌你也只能通过调岗、扣绩效等方式来恶心你,但是真的没办法裁掉你。


    但是在私企领导其实就是你们这个地方的土皇帝,你让领导不开心或者领导不喜欢你,那么是真的可以裁掉你,可能就是一句话的事你下午就不用来上班了都是有可能的事情。在这种地方,你除了舔领导,拼命加班,拼命卷之外没有任何办法,因为上位者掌握着你的生死存亡。


    在这种极度竞争和内卷的环境下,你的全部精力都会投入到工作当中,但是其实你并不参与蛋糕的分配,也就是你卷出来的成果、剩余价值大部份被老板拿走了。同时,高强度的工作还会剥夺你其它的可能,让你没时间陪家人,没时间发展自己的事业,当你不得不开始发展自己的事业的时候,就是你已经失业的时候。


    而在央国企的工作情况就会好很多,首先大多数岗位是比较稳定的,你不用过于担心失业裁员的情况发生。其次,至少在项目不忙的时候,你的休息时间是可以保障的,利用这些时间你其实可以选择发展自己的事业,就像刘慈心一样写科幻小说未来说不定能从副业上面赚到你一辈子赚不到的钱。


    所以,比起那些完全占用你时间和心智的工作,我其实觉得轻松但是钱不那么多的工作更加适合一个人的长期发展,从一生的尺度上看财富积累未必会比短短的靠25~35这10年间挣到的钱少。


    为什么这几年央国企火了?


    其实很多在校的学弟、学妹们沟通,我发现现在的孩子比我们当年看得明白很多,也可能是不同的时代背景造就了不同的人的观点。


    我们还是学生的时候听到的故事还都是什么王者荣耀100个月年终,互联网财富自由之类的神话,但是疫情的3年互联网和诸多的财富神话跌落神坛,大多数普通人能够保住手头的这份工作就是件值得庆幸的事情了。 即使是去华为、阿里、腾讯这样的大厂也很难有机会再实现当年的财富神话,技术改变世界的思潮也正在慢慢退潮,现在这些大厂也很难让你挣到财富自由的钱,逐渐变成了一份普通工作而已。


    当你在校园中搏杀了20几年好不容易拿到了学士、硕士、博士文凭,这些私企会告诉你现实的残酷,你手中的文凭只能帮你到入职的3个月,之后就各凭本事了。 资本是逐利的,中国的企业更加喜欢揠苗助长,没有任何培养一个员工的文化在里面。所谓的培养更多的是PUA,告诉你这儿也不行,哪儿也不行,然后在绩效考核的时候顺利成章的把锅甩给你,来满足组长必须找一个倒霉蛋背绩效的制度要求。 我不否认能力极强者、能卷的人在这种环境中能够获得快速的升职加薪和财富,但并不是每个人都是大神,也不是每个人在做好本职工作之外还有心情去舔领导。


    入职央国企能够在很大成都上帮你避免上述的问题,大型的央国企平台很大有足够的时间和资源来让员工成长,对于刚入职的新员工可能前面半年都不会给你安排真正意义上的工作,多数是各种培训,各种学习。 我以前经常听到在国企的同学抱怨又是什么培训、什么学习、什么党会,让人觉得很烦没有意义,但是在我看来每个人都应该感恩这些公司花着钱让你不干活儿的活动,真的不是所有的公司都这么有耐心。 除此之外,很少有央国企会期待从你身上压榨什么,因为大多数央国企从事的都是垄断行业,拥有足够的利润,并且这些利润也并非属于领导或者某个人的,而是属于整个集团,国家。你和领导本质上都一样,只是这个国企的打工人,没必要压榨你太过分,毕竟赚的钱也一分不会到领导自己包里,对你个人的包容性也会更强一些。


    所以当经济增长变缓,私企难以让人挣到足以财富自由的钱,大家就会发现其实没有必要去承担那么大的压力只是比稳定的工作多一点儿钱。这个时候一份稳定、有自己业余时间的央国企工作性价比就变得更高了起来。一边可以用工资保障自己的生活,一边开拓自己的事业在副业这个第二战场挣到更多的钱,确实会比在私企打工35被裁要体面得多。


    The End


    其实对于职业的选择,有一个核心逻辑就是去门槛更高的地方。


    有的人说,大厂门槛很高啊,问那么多技术,刷那么多题,也是万里挑一才能进去一个人。


    但是,实际上这些东西不算门槛,真正的门槛是把人堵在外面的不可逾越的鸿沟,比如说:如果你本科不是学的临床专业,那么你一辈子都没办法当上医生,除非重新高考!这才是真正意义上的门槛,而无论是多么高深的技术,只要肯学都能够学会的。


    所以,大型垄断央国企其实是个门槛更高的地方,好的岗位除了应届生就没有就会进去,同时一旦进去占住坑了也很难被裁掉,除非你自己离职。大家可能经常会听说哪个国企的用自己的业余时间努力学习然后去了互联网大厂的。但是你可能完全没有听过那个私企的毕业没去 "中国烟草" 靠着自己的不懈努力,社招进入了中国烟草。


    如果是应届生,尽量去门槛高、稳定的地方,考虑长期发展而不是贪图短时间的利益,这样一来即使你的能力没有那么强,也可以用马拉松的方式跑到最后。


    人生是一段长跑,不到最后一刻不知道谁输谁赢,就算是活得比别人长,那么其实你最后也胜利了。


    作者:浣熊say
    来源:juejin.cn/post/7343161077061992458
    收起阅读 »

    小红书创始人瞿芳,武汉人,北京外国语大学毕业,2013 年从外企离职,目前身价 120 亿

    大家好,我是二哥呀。 今天我们来聊聊小红书的创始人瞿芳,1985 年出生于湖北武汉,毕业于武汉外国语学校,硕士就读于北京外国语学校。 毕业后进入贝塔斯曼工作,这是一家老牌外企,1835 年就有了,真的非常老,瞿芳在这里工作了 5 年。 瞿芳的执行力也是拉满,2...
    继续阅读 »

    大家好,我是二哥呀。


    今天我们来聊聊小红书的创始人瞿芳,1985 年出生于湖北武汉,毕业于武汉外国语学校,硕士就读于北京外国语学校。


    毕业后进入贝塔斯曼工作,这是一家老牌外企,1835 年就有了,真的非常老,瞿芳在这里工作了 5 年。


    瞿芳的执行力也是拉满,2013 年 5 月底离职,6 月赴美寻找风投,7 月初就和老乡毛文超在上海创立了小红书的母公司行吟信息科技有限公司。


    长相上我觉得有一点邓丽君的感觉,大家觉得呢?



    • 2015-2016 年,瞿芳连续两年被《创业邦》评为“值得关注的女性创业者”;这些年小红书的成长,瞿芳确实功不可没,值得关注。

    • 2017 年,瞿芳荣登腾讯“我是创始人”荣耀榜单;小红书背后有阿里和腾讯两家大佬的投资,原来两家是从来不投一家公司的,瞿芳背后的斡旋算是让两家暂时握了手。

    • 2020 年,瞿芳入选“中国最具影响力的 30 位商界女性”榜单;目前来看,小红书还处在上升势头,并且流量拉满,瞿芳的身价肯定也会水涨船高。

    • 2024 年,瞿芳以 120 亿元的人民币财富位列《2024-胡润榜》的第 433 位;这还是在小红书没有上市的情况下。


    瞿芳曾在采访中强调,用户体验和社区氛围是小红书最看重的


    这也是小红书这个平台和微博、抖音最大的区别,你可能在小红书上百粉不到,但发布的内容却会被推荐到平台首页,成为爆款。


    微的推荐机制现在也有这种趋势,就是粉丝数越少,反而被推荐的机会越多。


    沉默王二 2024 年就有 3000 万次阅读


    瞿芳认为,品牌与用户的沟通应该从“教学模式”转向“恋爱模式”。


    也就是说,我们创作者不能再以老师的角度切入,把读者作为学生来传达信息,而是奔着双方恋爱的方式切入。


    更加的纯粹,双方的地位更加的对等。


    宝子们,都看到了吧,我爱你们,😄


    2013 年的时候,跨境旅游开始兴起,于是,瞿芳就和毛文超一起去找当地的购物达人,把他们的经验编成了一本厚厚的 PDF,书名就叫“小红书”。


    这本 PDF 放到网上以后,引起了巨大的反响,一个月的下载量就突破了 50 万次。


    尝到了甜头后,瞿芳和毛文超再接再厉,于 2013 年 12 月上线了小红书 App,相当于提供了一个购物的分享平台,注意不是电商平台,而是社区分享平台,让用户来分享自己的购物心得。


    这个定位就非常的巧妙。


    如果单纯地做电商平台,那么竞争对手多了去,比如说淘宝、天猫、京东,以及拼多多。


    但做社区平台的话,当时还没有什么竞争对手,虽然点评和美图秀秀都曾在自己的业务中加入大量的社区内容,并放出豪言,但最终都没有竞争过小红书。


    2014 年,小红书就聚集了几百万用户了,于是瞿芳就上线了一款希腊产的清洗液,结果直接被秒光了。


    到 2017 年,小红书的营收就突破了 100 亿。


    截止到目前,小红书已经发展成为了一个生活社区,基本上你想要的东西,你想找的地方,你想看的美女,小红书上都有。据说,月活用户已经达到了 3 亿。


    其中女性用户占比 70%,日均用户搜索渗透率达到 60%,用户生成内容(UGC)占比高达 90%。


    根本不需要 KOL。


    2025 年 1 月,由于 TikTok 可能会被美国封禁,所以大量的海外用户开始涌入小红书。


    中西文化的融合,在此刻显然格外的自然和松弛。


    我现在打开小红书,已经很少看到原住民发的东西了,这波算法也被太平洋彼岸的热情感染了。



    瞿芳在一次采访中的一段话我觉得很值得分享给大家,我套用一下:



    “就像今天手机屏幕前的你们,可能大学生可能是工作党,但不管大家是怎样的身份,回到家里,可能还是会跟家人吃一顿最简单的饭,跟最爱的人一起去做一些有创造性的事情。”



    我们要回到生活中去,而不只是活在虚拟世界里。


    三分恶面渣逆袭


    我这人一向说到做到,每天给大家汇报一下面渣逆袭的进度,这就来。今天修改到第 36 题。


    35.你们线上用的什么垃圾收集器?


    我们生产环境中采用了设计比较优秀的 G1 垃圾收集器,因为它不仅能满足低停顿的要求,而且解决了 CMS 的浮动垃圾问题、内存碎片问题。


    G1 非常适合大内存、多核处理器的环境。



    以上是比较符合面试官预期的回答,但实际上,大多数情况下我们可能还是使用的 JDK 8 默认垃圾收集器。



    可以通过以下命令查看当前 JVM 的垃圾收集器:


    java -XX:+PrintCommandLineFlags -version

    二哥的 Java 进阶之路:JDK 默认垃圾收集器


    UseParallelGC = Parallel Scavenge + Parallel Old,表示新生代用Parallel Scavenge收集器,老年代使用Parallel Old 收集器。


    因此你也可以这样回答:


    我们系统的业务相对复杂,但并发量并不是特别高,所以我们选择了适用于多核处理器、能够并行处理垃圾回收任务,且能提供高吞吐量的Parallel GC


    但这个说法不讨喜,你也可以回答:


    我们系统采用的是 CMS 收集器,能够最大限度减少应用暂停时间。


    内容来源


    三分恶的面渣逆袭:javabetter.cn/sidebar/san…
    二哥的 Java 进阶之路(GitHub 已有 13000+star):github.com/itwanger/to…


    最后,把二哥的座右铭送给大家:没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。共勉 💪。


    作者:沉默王二
    来源:juejin.cn/post/7461772464738402342
    收起阅读 »

    谈谈在大环境低迷下,找工作和入职三个月后的感受

    前言 今天是新公司入职后的三个多月了个人也是如愿的转正了,对于我个人而言此时的心情好像没有三个月前刚拿到offer那样的喜悦和兴奋了,更像一件很普通的事情在你身边发生了吧。从2023年底离职在到2024年初开始找工作中间休息了三个月,找工作到入职花了一个多月,...
    继续阅读 »

    前言


    今天是新公司入职后的三个多月了个人也是如愿的转正了,对于我个人而言此时的心情好像没有三个月前刚拿到offer那样的喜悦和兴奋了,更像一件很普通的事情在你身边发生了吧。从2023年底离职在到2024年初开始找工作中间休息了三个月,找工作到入职花了一个多月,在这个过程中也是第一次真真切切感受到了所谓大环境低迷下的“前端已死的言论”,也给大家分享一下自己入职三个月的个人感受吧。


    从上一家公司离职时的个人感受


    因为上一家公司的工作性质是人力外包驻场开发,年底客户公司(中国移动成都产业研究院)我所在的项目组不需要外包人员了,个人也是被迫拿了赔偿灰溜溜的走人了。


    工作感受:对于这段工作经历我个人还是比较认可的,毕竟这里没有任何工作压力,也不加班,工作量少,有很多时间去学习新东西,做做自己的开源,认识了新的朋友等等。


    学历的重要性:在这里面随便拎一个人出来可能就是研究生学历的国企单位,自己真实的意识到了学历的重要性(第一学历小专科的我瑟瑟发抖)。


    和优秀的人共事:如果在一个长期压抑低沉消极的环境下工作无论你的性格在怎么积极乐观开朗,可能也很容易被影响到。相反如果是和在一群积极乐观开朗充满自信的环境和人一起工作,相信你也会变得积极,乐观,自信这或许也是我这一段工作经历最大的收获吧。


    2023年底找工作的市场就业环境


    抱着试一试的心态在boss上更新了自己的简历状态,不出所料软件上面安静的奇怪ps:49入国军的感觉已读未回可能是很失望的感觉吧,但年底找工作令人绝望的是大多数公司都是未读未回,这也就意味着年底基本上是没有正常公司招聘的了。


    大概投了两周简历后终于在智联招聘上约到了一个短期三个月岗位的面试,现场两轮面试通过了,不过最终还是没有选择去。


    原因有很多:



    1. 现场的工作环境个人感觉很压抑,从接待我前台和面试官都能感觉满脸写着疲惫

    2. 说公司最近在996,你也需要和我们一起

    3. 招聘岗位和工作内容是threejs开发,薪资却说只能给到普通前端开发的水平

    4. 人力外包公司hr的反复无常令我恶心,二面通过后hr给我打电话最主要就是聊薪资吧,电话内容也很简单hr:成都大部分前端的薪资是在XX-XX,可能给不到你想要的薪资,可能要往下压个1-2K。:我提的薪资完全是在你们发布招聘岗位薪资的区间,既然你们给不到为什么要这样写了(有感到被侮辱了)。过了几天之后人力外包的hr又给我电话,说可以在原来提的薪资基础上加1.4K,希望能早点去客户公司入职。


    总结:年底招聘的公司基本上没啥好鸟,如果你的经济能力还行的话让自己放松休息一段时间也是不错的选择


    2024年初找工作:真实的感受到了大环境的低迷下的市场行情


    印象最深刻的是在疫情时期的2021年,那会儿出来找工作boos上会有很多HR主动给你打招呼,一周大概能五六个面试,大专学历也有机会去自研公司


    解封之后本以为市场行情会变得回缓,结果大概就是今年可是未来十年行情最好的一年


    简单总结一下2024年的成都就业环境大概这样的:



    1. 只有外包公司会招专科学历

    2. boss上只给hr发一句打招呼的快捷语,99% 都是已读不回

    3. 大多数要完简历之后就没有后续了

    4. 待遇好的公司对于学历的要求更严格了(211,985)

    5. 给你主动打招呼的基本上都是人力外包公司


    截至入职新公司前boss上面的投递状况:沟通了1384个岗位,投递了99份简历,一共约到了 8 家公司的面试


    image.png


    今年找工作的个人感受:不怕面试,就怕没有面试机会


    首先说一下个人的一些情况吧,因为在创业小公司待过在技术栈方面个人认为算是比较全面的了


    项目经验:做过管理系统(CRM,B2C,ERP,saas等管理系统)、商城和门户网站(响应式,自适应)、移动端(H5,小程序,app)、低代码和可视化(工作流,可视化大屏,3d编辑器)、第三方开发(腾讯IM,企业微信侧边栏)、微前端


    项目经历:从0-1搭建过整个大项目的基建工作,封装过项目中很多功能性组件和UI组件二次封装(提高开发效率),接手过屎山代码并重构优化,约定项目的开发规范,处理很多比较棘手的疑难bug和提供相关的技术方案,没有需求概念下的敏捷开发,从0-1的技术调研等


    代码方面:写过几个开源项目虽然star数量不多(目前最多一个项目是600+),但在代码规范和可读性方面个人认为还是比较OK的(至少不会写出屎山代码吧)


    工作经验(4年):2020毕业至今一直从事前端开发工作


    学历:自考本科学历(貌似没啥卵用)


    学历确实是我很硬伤的一个点但是没办法,人嘛总归要为年轻时的无知买单吧


    在这样的背景下开启了24年的找工作,从2月26号开始投递简历到4月1号拿到offer差不多一个多月左右时间,一共约到了8加公司的面试,平均一周两家公司


    大概统计了一下这些公司的面试情况:


    公司A:



    1. 数组哪些方法会触发Vue监听,哪些不会触发监听

    2. position 有哪些属性

    3. vue watch和computed的区别,computed和method的区别

    4. vue的watch是否可以取消? 怎么取消?

    5. position:absolute, position:fixed那些会脱离文档流

    6. 如何获取到 pomise 多个then 之后的值

    7. 常见的http状态码

    8. 谈谈你对display:flex 弹性盒子属性的了解

    9. 如何判断一个值是否是数组

    10. typeof 和instanceof的区别

    11. es6-es10新增了那些东西

    12. 离职原因,期望薪资,职业规划


    公司B


    到现场写了一套笔试题,内容记不清楚了


    公司C



    1. vue router 和route 区别

    2. 说说重绘和重排

    3. css 权重

    4. 项目第一次加载太慢优化

    5. 谈谈你对vue这种框架理解

    6. sessionstorage cookie localstorage 区别

    7. 了解过.css 的优化吗?

    8. 闭包

    9. 内存泄漏的产生

    10. 做一个防重复点击你有哪些方案

    11. 解释一些防抖和节流以及如何实现

    12. 说一下你对 webScoket的了解,以及有哪些API

    13. 说一下你对pomise的理解

    14. vue2,vue3 中 v-for 和v-if的优先级

    15. 说说你对canvas的理解


    公司D


    笔试+面试



    1. vue 首屏加载过慢如何优化

    2. 说说你在项目中封装的组件,以及如何封装的

    3. 后台管理系统权限功能菜单和按钮权限如何实现的

    4. vue 中的一些项目优化

    5. 期望薪资,离职原因,

    6. 其他的记不清楚了


    公司E


    笔试+面试+和老板谈薪资


    1.笔试:八股文


    2.面试:主要聊的是项目内容比如项目的一些功能点的实现,和项目的技术点


    3.老板谈薪资:首先就是非技术面的常规三件套(离职原因,期望薪资,职业规划),然后就是谈薪资(最终因为薪资给的太低了没有选择考虑这家)


    公司F


    也是最想去的一家公司,一个偏管理的前端岗位(和面试官聊的非常投缘,而且整个一面过程也非常愉快感受到了十分被尊重)


    可惜的是复试的时候因为学历原因,以及一些职业规划和加班出差等方面上没有达到公司的预期也是很遗憾的错过了


    一面:



    1. vue 响应式数据原理

    2. 说说es6 promise async await 以及 promise A+规范的了解

    3. 谈谈es6 Map 函数

    4. 如何实现 list 数据结构转 tree结构

    5. webScoke api 介绍

    6. webScoke 在vue项目中如何全局挂载

    7. vuex 和 pinia 区别

    8. 谈谈你对微任务和宏任务的了解

    9. call apply bind 区别

    10. 前端本地数据存储方式有哪些

    11. 数组方法 reduce 的使用场景

    12. 说说你对 css3 display:flex 弹性盒模型 的理解

    13. vue template 中 {{}} 为什么能够被执行

    14. threejs 加载大模型有没有什么优化方案

    15. 离职原因,住的地方离公司有多远,期望薪资

    16. 你有什么想需要了解的,这个岗位平时的工作内容


    二面:


    1.我看写过一个Express+Mongoose服务端接口的开源项目,说说你在写后端项目时遇到过的难点


    2.介绍一下你写的threejs 3d模型可视化编辑器 这个项目


    3.以你的观点说一下你对three.js的了解,以及three.js在前端开发中发挥的作用


    4.现在的AI工具都很流行,你有没有使用过AI工具来提高你对开发效率


    5.说说你认为AI工具对你工作最有帮助的地方是哪些


    6.以你的观点谈谈你对AI的看法,以及AI未来发展的趋势


    7.你能接受出差时间是多久


    8.你是从去年离职的到今天这这几个月时间,你是去了其他公司只是没有写在简历上吗?


    9.说说你的职业规划,离职原因,你的优点和缺点,平时的学习方式


    公司G


    一共两轮面试,也是最终拿到正式offer入职的公司


    一面:



    1. 主要就是聊了一下简历上写的项目

    2. 项目的技术难点

    3. 项目从0-1搭建的过程

    4. 项目组件封装的过程

    5. vue2 和 vue3 区别

    6. vue响应式数据原理

    7. 对于typescript的熟练程度

    8. 会react吗? 有考虑学习react吗?

    9. 说一下你这个three.js3d模型可视化编辑器项目的一个实现思路,为什么会想写这样一个项目


    二面:



    1. 说说了解的es6-es10的东西有哪些

    2. 说说你对微任务和宏任务的了解

    3. 什么是原型链

    4. 什么是闭包,闭包产生的方式有哪些

    5. vue3 生命周期变化

    6. vue3 响应式数据原理

    7. ref 和 reactive 你觉得在项目中使用那个更合适

    8. 前端跨越方式有哪些

    9. 经常用的搜索工具有哪些?

    10. 谷歌搜索在国内能使用吗?你一般用的翻墙工具是哪种?

    11. 用过ChatGPT工具吗? 有付费使用过吗?

    12. 你是如何看待面试造航母工作拧螺丝螺丝的?

    13. 谈谈你对加班的看法?

    14. 你不能接受的加班方式是什么?

    15. 为什么会选择自考本科?

    16. 你平时的学习方式是什么?

    17. 一般翻墙去外网都会干什么?,外网学习对你的帮助大吗?

    18. 上一家公司的离职原因是什么,期望薪资是多少, 说说你的职业规划

    19. 手里有几个offer?


    hr电话:



    1. 大概说了一下面试结果通过了

    2. 然后就是介绍了一下公司的待遇和薪资情况?

    3. 问了一下上一家公司的离职原因以及上一家公司的规模情况?

    4. 手里有几个offer?

    5. 多久能入职?


    因为后面没有别的面试了,再加上离职到在到找工作拿到offer已经有四个月时间没有上班了,最终选择了入职这家公司


    image.png


    入职第三天:我想跑路了!


    入职后的第一天,先是装了一下本地电脑环境然后拉了一下项目代码熟悉一下,vue3,react,uniapp 项目都有


    崩溃的开始:PC端是一个saas 系统由五个前端项目构成,用的是react umi 的微前端项目来搭建的,也是第一次去接触微前端这种技术栈,要命的是这些项目没有一个是写了readme文档的,项目如何启动以及node.js版本这些只能自己去package.json 文件去查看,在经过一番折腾后终于是把这几个项目给成功跑起来了,当天晚上回家也是专门了解了一下微前端


    开始上强度: 入职的第二天被安排做了一个小需求,功能很简单就是改个小功能加一下字段,但是涉及的项目很多,pc端两个项目,小程序两个项目。在改完PC端之后,开始启动小程序项目不出所料又是一堆报错,最终在别的前端同事帮助下终于把小程序项目给启动成功了。


    人和代码有一个能跑就行:入职的第三天也从别的同事那里了解到了,之前sass项目组被前端大规模裁员过,原因嘛懂得都懂? 能写出这样一堆屎山代码的人,能不被裁吗?


    第一次知道 vue 还可以这样写


    image.png


    对于一个有代码强迫症的人来说,在以后的很长一段时间里要求优化和接触完全是一堆屎山一样代码,真的是很难接受的


    入职一个月:赚钱嘛不寒掺


    在有了想跑路的想法过后,也开始利用上班的空余时间又去投递简历了,不过现实就是在金三银四的招聘季,boss上面依旧是安静的可怕,在退一步想可能其他公司的情况也和这边差不多,于是最终还是选择接受了现实,毕竟赚钱嘛不寒掺


    入职两个月:做完一个项目迭代过后,感觉好多了


    在入职的前一个月里,基本上每天都要加班,原因也很简单:


    1.全是屎山的项目想要做扩展新功能是非常困难的


    2.整个项目的逻辑还是很多很复杂的只能边写项目边熟悉


    3.因为裁了很多前端,新人还没招到,但是业务量没有减少只能加班消化


    功能上线的晚上,加班到凌晨3点


    image.png


    在开发完一个项目迭代过后也对项目有了一些大概的了解,之后的一些开发工作也变得简单了许多


    入职三个月:工作氛围还是很重要滴


    在入职三个月后,前端组团队的成员也基本上是组建完成了,一共14人,saas项目组有四个前端,虽然业务量依然很多但是好在有更多的人一起分担了,每周的加班时间也渐渐变少了


    在一次偶然间了解到我平时也喜欢打篮球后,我和公司后端组,产品组的同事之间也开始变得有话题了,因为大家也喜欢打球,后来还拉了一个篮球群周末有时间大家也会约出来一起打打球


    image.png


    image.png


    当你有存在价值后一切的人情世故和人际关系都会变得简单起来


    在这个世界上大多数时候除了你的父母等直系亲属和另一半,可能会对你无条件的付出


    其余任何人对你尊重和示好,可能都会存在等价的利益交换吧


    尤其是在技术研发的岗位,只有当你能够完全胜任这份工作时,并且能够体现出足够的价值时才能够有足够的话语权


    入职三个月后的感受



    1. 公司待遇:虽然是一个集团下面的子公司 (200+人)但待遇只能说一般吧,除了工资是我期望的薪资范围,其他的福利待遇都只能是很一般(私企嘛,懂得都懂)

    2. 工作强度: 听到过很多从大厂来的新同事抱怨说这边的工作量会很大,对我来说其实都还ok,毕竟之前在极端的高压环境下工作过

    3. 工作氛围:从我的角度来讲的话,还是很不错的,相处起来也很轻松简单,大家也有很多共同话题,没有之前在小公司上班那么累


    大环境低迷下,随时做好被裁掉的准备


    从2020年毕业工作以来,最长的一段工作经历是1年4个月,有过三家公司的经历


    裁员原因也很简单:创业小公司和人力外包,要么就是小公司经营问题公司直接垮掉,或者就是人力外包公司卸磨杀驴


    除非你是在国企单位上班,否则需要随时做好被裁掉的准备


    什么都不怕,就怕太安逸了


    这句话出自《我的团长我的团》电视剧里面龙文章故意对几十个过江的日本人围而不歼时和虞啸卿的对话,龙文章想通过这几十个日本人将禅达搅得鸡犬不宁,来唤醒还在沉睡在自己温柔乡的我们,因为就在我们放松警惕时日本人就已经将枪口和大炮对准了我们。


    或许大家会很不认同这句话吧,如果你的父母给你攒下了足够的资本完全可以把我刚才的话当做放屁,毕竟没有哪一个男生毕业之前的梦想是车子和房子,从事自己喜欢的工作不好吗? 但现实却是你喜欢工作的收入很难让你在这座城市里体面的生活


    于我而言前端行业的热爱更多是因为能够给我带来不错的收入所以我选择了热爱它吧,所以保持终身学习的状态也是我需要去做的吧


    前端已死?


    前端彻底死掉肯定是不会的,在前后端分离模式下的软件开发前端肯定是必不可少的一个岗位,只不过就业环境恶劣下的情况里肯定会淘汰掉很多人,不过35岁之后我还是否能够从事前端行业真的是一个未知数


    结语


    选择或者躺平,只是两种不同的生活态度没有对与错,偶尔躺累了起来卷一下也是可以的,偶尔卷累了躺起了休息一下也是不错的。


    在这个网络上到处是人均年收入百万以及各种高质量生活的时代,保持独立思考,如何让自己不被负面情绪所影响才是最重要的吧


    作者:答案answer
    来源:juejin.cn/post/7391065678546157577
    收起阅读 »

    裸辞后,我活得像个废物,但我终于开始活自己

    哈喽,大家好!我是Web大鹅只会叫!下去沉淀了那么久,终于有时间回来给大家写点什么了! 你问我人为什么活着?我哪知道啊?我又不是上帝!释迦牟尼为了解这个问题还跑去出家,结果发现人活着就是为了涅槃——也就是死。所以别问我人生的意义,我也没搞明白!不过我能告诉你...
    继续阅读 »

    哈喽,大家好!我是Web大鹅只会叫!下去沉淀了那么久,终于有时间回来给大家写点什么了!


    image.png


    你问我人为什么活着?我哪知道啊?我又不是上帝!释迦牟尼为了解这个问题还跑去出家,结果发现人活着就是为了涅槃——也就是死。所以别问我人生的意义,我也没搞明白!不过我能告诉你一个答案,那就是裸辞后,我终于知道了为什么要活着——那就是为了“活得自由”!


    裸辞后,那些走过的路,和你说的“脏话”


    2024年8月,我做了一个震惊所有人的决定——裸辞!是的,没错,我就是那种毫不犹豫地辞了职、丢下稳定收入和安稳生活,拿着背包去走四方的“疯子”。放下了每天早起20公里开车上班的压力,放下了无聊的加班、枯燥的开会,放下了所谓的“你要努力争取美好生活”的叮嘱。一切都在“离开”这一刻轻轻拂去,带着一种挥之不去的自由感。


    带着亲人的责怪、朋友的疑问、同事的眼神、以及自己满满的疑惑,我开始了这段没有目的的旅行。我不知道我想找什么,但我知道,我不想再活得像以前那样。


    我走过了无数地方,南京、湖州、宁波、杭州、义乌、金华、嘉兴、镇江、扬州、苏州、无锡、上海……一路走来,路过了每个风景,每个城市,每个人生。我甚至去了中国最北的漠河,站在寒风凛冽的雪地里,终于明白了一个道理:“你活着,才是最值得庆祝的事。


    你知道吗,最让人清醒的,不是远方的景色,而是走出去之后,终于能脱离了那一套“你该做什么”的公式。每天不用设闹钟,不用准时吃饭,不用打卡上班,不用开会骂娘,再也不被地铁里的拥挤挤得喘不过气。生活突然变得宽松,我竟然开始意识到:我一直追求的美好生活,原来只是在为别人拼命。


    走出内卷圈子的那一刻,我认为我是世界上最快乐的小孩了,我们渴望着幸福却糟践着现在,你所期望的美好未来却始终都是下一个阶段!你认为你是一个努力拼搏美好未来的人。可是现实比理想残酷的多!你没日没夜的拼搏,却让别人没日没夜的享受!你用尽自己半条命,换来的是下半辈子的病!我在裸辞后就告诉我自己:从今以后你想干什么、就干什么!你就是世界的主人! 嗯~ 爽多了!


    走过的路,都在暗示我


    我在大兴安岭漠河市的市里住了5天,住在一个一天40元的宾馆、干净、暖和!老板是一个退休的铁道工人。脸和双手都布满了冻伤,他的妻子(大姨)很面善。每天都会在我回来的时候和我聊上几句从前,安排一些极寒天气的注意事项。


    有一天我去北极村回来,大姨和我聊了一会。大姨对我讲:“趁年轻、别把自己困起来,出去走走。不像我们,60年没出过这片土地,到头还要葬在这片土地上!”。她说这句话的时候没有忧虑、没有悲伤,却是一种满足感。是啊!60多了,还能追求什么?忙了大半辈子,把孩子都送出了这片土地,自己也没有激情出去走走了,很害怕自己的以后也是这样。


    我20多岁的年纪,想的不是努力拼搏挣钱、不是搞事业。却总想着无所事事。我觉得自己像一个没有完全“被时间遗弃”的人,我甚至觉得自己不属于这个时代,这个不知道为了什么而拼命的时代。每走一步都好像在掏空自己积压已久的情绪:压力、焦虑、焦灼,让我很享受这种感觉。然后我想起来一本书里的话:“你活着,不是为了活给别人看。“ 是啊,我们都明白这个大道理,可自己从来没打算让自己脱离这个主线。我开始明白,我这次的旅行不是去寻找什么,而是放下什么!


    从别人嘴里听到的“脏话”,其实是自己内心的尖刺


    这段时间里,我经常回想起来那些让我神经紧绷的日子。尤其是我对“人” 这个物种越来越敏感的那个时期————‘恶毒、自私、无理、肮脏’。朋友的欺骗、同事的推锅、亲人的折磨都是罪恶的!可是到头来,事情还是发生了。地球还是在自转,太阳一样正常升起落下。这些都没有在你认为的这些琐事中消失。我不明白我还在纠结什么?


    事实上,这些乱七八糟的事情并不是指向我个人的,它只是我内心脆弱的反射。是的,我一直在内耗自己罢了,把自己放在了一个焦虑的漩涡里。假装没事、假装坚强,结果别人一句话就能作为击垮我的最后一击。直到有一天,我发现我讨厌的只是我自己。所以我决定我不要去在意别人说什么、做什么,我不要逃避问题,我想听听我内心的想法,我不想让自己认为别人在定义我。


    过程的意义:也许就是为了“停一停”


    好了,我知道我的文采不好,但是也应该有个结尾。


    在这一路上,我认识了很多有趣的人,他们不同风格的服装、不同口音,各式各样的生活方式。也有着各式各样的理想和困惑。有的喜欢在山顶等着日出的奇迹,有的则是想在湖边静静地坐着。而我,就是个迷途的羔羊,没有群体头羊的带领,我穿行在这些不同的路途中,慢慢摸索着向所有方向前进着。


    偶尔我也会停下来,坐在湖边吹着风、闭上眼睛,听风,感受这一刻的宁静。然后我会微笑,我认为这个时候的我有了轻松的感觉。生活的答案我在这个时候找到了。


    我意识到,未来不是重要的,现在才是应该享受的。我不知道我下一步要去哪里,但是我想先停下来看一看,呼吸一下。停下来不是因为我没有了目的,而是我知道,目的地并不重要,重要的是,我和自己在一起,心里不再有那么多焦虑,不再被过去的焦虑所束缚。


    所以,我选择了离开,离开这一切,放下所有的焦虑和期待,享受我自己想要的生活。也许,活着的意义不在于追寻一个遥远的目标,而是过好每一个‘现在’。


    作者:Web大鹅只会叫
    来源:juejin.cn/post/7454064311079813132
    收起阅读 »

    好人难当,坏人不做

    好人难当,以后要多注意了,涨点记性。记录三件事情证明下: 1. 免费劳动 之前和一个同学一起做一个项目,说是创业,不过项目做好了,倒是他家店铺自己用起来了,后来一直让我根据他家的需求进行修改,我也一一的改了,他倒是挺感谢我的,说是请吃饭。不过也一直没请,后面都...
    继续阅读 »

    好人难当,以后要多注意了,涨点记性。记录三件事情证明下:


    1. 免费劳动


    之前和一个同学一起做一个项目,说是创业,不过项目做好了,倒是他家店铺自己用起来了,后来一直让我根据他家的需求进行修改,我也一一的改了,他倒是挺感谢我的,说是请吃饭。不过也一直没请,后面都一年多过去了,还让我免费帮他改需求,我就说没时间,他说没时间的话可以把源码给他,他自己学着修改,我就直接把源码给他了,这个项目辛苦了一个多月,钱一毛也没赚到,我倒是搭进去一台服务器,一年花了三百多吧。现在源码给他就给他了吧,毕竟同学一场。没想到又过了半年,前段时间又找我来改需求了。这个项目他们家自己拿着赚钱,又不给我一毛钱,我相当于免费给他家做了个软件,还要出服务器钱,还要免费进行维护。我的时间是真不值钱啊,真成义务劳动了。我拒绝了,理由是忙,没时间。


    总结一下,这些人总会觉得别人帮自己是理所当然的,各种得寸进尺。


    2. 帮到底吧


    因为我进行了仲裁,有了经验,然后被一个人加了好友,是一个前同事(就是我仲裁的那家公司),然后这哥们各种问题我都尽心回答,本着能帮别人一点就帮别人一点的想法,但是我免费帮他,他仲裁到手多少钱,又不会给我一毛钱。这哥们一个问题接一个,我都做了回答,后来直接要求用我当做和公司谈判的筹码,我严词拒绝了,真不知道这人咋想的,我帮你并没有获得任何好处,你这个要求有点过分了,很简单,他直接把我搬出来和公司谈判,公司肯定会找我,会给我带来麻烦,这人一点也没想这些事。所以之后他再询问有关任何我的经验,我已经不愿意帮他了。


    总结一下,这些人更进一步,甚至想利用下帮自己的人,不考虑会给别人带来哪些困扰。


    3. 拿你顶缸


    最近做了通过一个亲戚接了一个项目,而这个亲戚的表姐是该项目公司的领导,本来觉得都是有亲戚关系的,项目价格之类开始问了,他们没说,只是说根据每个人的工时进行估价,后面我们每个人提交了个人报价,然后还是一直没给明确答复,本着是亲戚的关系,觉得肯定不会坑我。就一直做下去了,直到快做完了,价格还是没有出来,我就直接问了这个价格的事情,第二天,价格出来了,在我报价基础上直接砍半。我当然不愿意了,后来经过各种谈判,我终于要到了一个勉强可以的价格,期间群里谈判也是我一个人在说话,团队的其他人都不说话。后来前端的那人问我价格,我也把过程都实话说了,这哥们也要加价,然后就各种问我,我也啥都告他了。后来这个前端在那个公司领导(亲戚表姐)主动亮明身份,她知道这个前端和那个亲戚关系好,然后这个前端立马不好意思加价了,并且还把锅甩我头上,说是我没有告诉他她是他姐。还说我不地道,我靠,你自己要加价,关我啥事,你加钱也没说要分我啊,另外我给自己加价的时候你也没帮忙说话啊,我告诉你我加价成功了是我好心,也是想着你能加点就加点吧,这时候你为了面子不加了,然后说成要加价的理由是因为我,真是没良心啊。后面还问我关于合同的事情,我已经不愿意回答他了,让他自己找对面公司问去。


    总结一下,这些人你帮他了他当时倒是很感谢你,但是一旦结果有变,会直接怪罪到你头上。


    4. 附录文章


    这个文章说得挺好的《你的善良,要有锋芒》


    你有没有发现,善良的人,心都很软,他们不好意思拒绝别人,哪怕为难了自己,也要想办法帮助身边的人。善良的人,心都很细,他们总是照顾着别人的情绪,明明受委屈的是自己,却第一时间想着别人会不会难过。


    也许是习惯了对别人好,你常常会忽略自己的感受。有时候你知道别人是想占你便宜,你也知道别人不是真心把你当朋友,他们只是觉得你好说话,只是看中了你的善良,但是你没有戳穿,你还是能帮就帮,没有太多怨言。


    你说你不想得罪人,你说你害怕被孤立,可是有人在乎过你吗?


    这个世界上形形色色的人很多,有人喜欢你,有人讨厌你,你没有办法做到对每一个人好,也没办法要求每一个人都是真心爱你。所以你要有自己的选择,与舒服的人相处,对讨厌的人远离,过你自己觉得开心自在的生活就好,没必要为了便利别人,让自己受尽委屈。


    看过一段话:善良是很珍贵的,但善良要是没有长出牙齿来,那就是软弱。


    你的善良,要有锋芒,不要把时间浪费在不值得的人身上。对爱你的人,倾心相助,对利用你的人,勇敢说不。


    愿你的善良,能被真心的人温柔以待。


    作者:一线大码
    来源:juejin.cn/post/7455667125798780980
    收起阅读 »

    让闲置 Ubuntu 服务器华丽转身为家庭影院

    让闲置 Ubuntu 服务器华丽转身为家庭影院在数字化的时代,家里的设备更新换代频繁,很容易就会有闲置的服务器吃灰。我家里就有一台闲置的 Ubuntu 24.04 服务器,一直放在角落,总觉得有些浪费。于是,我决定让它重新发挥作用,打造一个属于自己的家庭影院。...
    继续阅读 »

    让闲置 Ubuntu 服务器华丽转身为家庭影院

    在数字化的时代,家里的设备更新换代频繁,很容易就会有闲置的服务器吃灰。我家里就有一台闲置的 Ubuntu 24.04 服务器,一直放在角落,总觉得有些浪费。于是,我决定让它重新发挥作用,打造一个属于自己的家庭影院。

    一、实现 Windows 与 Ubuntu 服务器文件互通

    要打造家庭影院,首先得让本地 Windows 电脑和 Ubuntu 服务器之间能够方便地传输电影文件。我选择安装 Samba 来实现这一目的。

    1. 安装 Samba:在 Ubuntu 服务器的终端中输入命令

      sudo apt-get install samba samba-common

      系统会自动下载并安装 Samba 相关的软件包。

    2. 备份配置文件:为了以防万一,我先将原来的 Samba 配置文件进行备份,执行命令

      mv /etc/samba/smb.conf /etc/samba/smb.conf.bak
    3. 新建配置文件:使用 vim /etc/samba/smb.conf 命令打开编辑器,写入以下配置内容:
    [global]
    server min protocol = CORE
    workgroup = WORKGR0UP
    netbios name = Nas
    security = user
    map to guest = bad user
    guest account = nobody
    client min protocol = SMB2
    server min protocol = SMB2
    server smb encrypt = off
    [NAS]
    comment = NASserver
    path = /home/bddxg/nas
    public = Yes
    browseable = Yes
    writable = Yes
    guest ok = Yes
    passdb backend = tdbsam
    create mask = 0775
    directory mask = 0775

    这里需要注意的是,我计划的媒体库目录是个人目录下的 nas/,所以 path 是 /home/bddxg/nas ,如果大家要部署的话记得根据自己的实际情况修改为对应的位置。 

    1. 连接 Windows 电脑:在 Windows 电脑这边基本不需要什么复杂配置,因为在网络里无法直接看到 Ubuntu,我直接在电脑上添加了网络位置。假设服务器地址是 192.168.10.100,那么添加网络位置就是 \\192.168.10.100\nas,这样就可以在 Windows 电脑和 Ubuntu 服务器之间传输文件了。

    二、安装 Jellyfin 搭建家庭影院

    文件传输的问题解决后,接下来就是安装 Jellyfin 来实现家庭影院的功能了。

    1. 尝试 Docker 安装失败:一开始我选择使用 Docker 安装,毕竟 Docker 有很多优点,使用起来也比较方便。按照官网指南进行操作,在第三步启动 Docker 并挂载本地目录的时候却一直失败。报错信息为:

      docker: Error response from daemon: error while creating mount source path '/srv/jellyfin/cache': mkdir /srv/jellyfin: read-only file system.

      即使我给 /srv/jellyfin 赋予了 777 权限也没有效果。无奈之下,我决定放弃 Docker 安装方式,直接安装 server 版本的 Jellyfin。

    1. 安装 server 版本的 Jellyfin:在终端中输入命令 curl https://repo.jellyfin.org/install-debuntu.sh | sudo bash,安装过程非常顺利。

    1. 配置 Jellyfin:安装完成后,通过浏览器访问 http://192.168.10.100:8096 进入配置页面。在添加媒体库这里,我遇到了一个麻烦,网页只能选择到 /home/bddxg 目录,无法继续往下选择到我的媒体库位置 /home/bddxg/nas。于是我向 deepseek 求助,它告诉我需要执行命令:

      sudo usermod -aG bddxg jellyfin
      # 并且重启 Jellyfin 服务
      sudo systemctl restart jellyfin

      按照它的建议操作后,我刷新了网页,重新配置了 Jellyfin,终于可以正常添加媒体库了。

    2. 电视端播放:在电视上安装好 Jellyfin apk 客户端后,现在终于可以正常读取 Ubuntu 服务器上的影视资源了,坐在沙发上,享受着大屏观影的乐趣,这种感觉真的太棒了!

     通过这次折腾,我成功地让闲置的 Ubuntu 服务器重新焕发生机,变成了一个功能强大的家庭影院。希望我的经验能够对大家有所帮助,也欢迎大家一起交流更多关于服务器利用和家庭影院搭建的经验。

    [!WARNING] 令人遗憾的是,目前 jellyfin 似乎不支持rmvb 格式的影片, 下载资源的时候注意影片格式,推荐直接下载 mp4 格式的资源


    本次使用到的软件名称和版本如下:

    软件名版本号安装命令
    sambaVersion 4.19.5-Ubuntusudo apt-get install samba samba-common
    jellyfinJellyfin.Server 10.10.6.0curl https://repo.jellyfin.org/install-debuntu.sh | sudo bash
    ffmpeg(jellyfin 内自带)ffmpeg version 7.0.2-Jellyfinnull

    作者:冰冻大西瓜
    来源:juejin.cn/post/7476614823883833382

    收起阅读 »

    Mybatis接口方法参数不加@Param,照样流畅取值

    在 MyBatis 中,如果 Mapper 接口的方法有多个参数,但没有使用 @Param 注解,默认情况下,MyBatis 会将这些参数放入一个 Map 中,键名为 param1、param2 等,或者使用索引 0、1 等来访问。以下是具体的使用方法和注意事...
    继续阅读 »

    在 MyBatis 中,如果 Mapper 接口的方法有多个参数,但没有使用 @Param 注解,默认情况下,MyBatis 会将这些参数放入一个 Map 中,键名为 param1param2 等,或者使用索引 01 等来访问。以下是具体的使用方法和注意事项。




    一、Mapper 接口方法


    假设有一个 Mapper 接口方法,包含多个参数但没有使用 @Param 注解:


    public interface UserMapper {
    User selectUserByNameAndAge(String name, int age);
    }



    二、XML 文件中的参数引用


    在 XML 文件中,可以通过以下方式引用参数:


    1. 使用 param1param2 等


    MyBatis 会自动为参数生成键名 param1param2 等:


    <select id="selectUserByNameAndAge" resultType="User">
    SELECT * FROM user WHERE name = #{param1} AND age = #{param2}
    </select>

    2. 使用索引 01 等


    也可以通过索引 01 等来引用参数:


    <select id="selectUserByNameAndAge" resultType="User">
    SELECT * FROM user WHERE name = #{0} AND age = #{1}
    </select>



    三、注意事项



    1. 可读性问题



      • 使用 param1param2 或索引 01 的方式可读性较差,容易混淆。

      • 建议使用 @Param 注解明确参数名称。



    2. 参数顺序问题



      • 如果参数顺序发生变化,XML 文件中的引用也需要同步修改,容易出错。



    3. 推荐使用 @Param 注解



      • 使用 @Param 注解可以为参数指定名称,提高代码可读性和可维护性。


        public interface UserMapper {
        User selectUserByNameAndAge(@Param("name") String name, @Param("age") int age);
        }

        XML 文件:


        <select id="selectUserByNameAndAge" resultType="User">
        SELECT * FROM user WHERE name = #{name} AND age = #{age}
        </select>







    四、示例代码


    1. Mapper 接口


    public interface UserMapper {
    User selectUserByNameAndAge(String name, int age);
    }

    2. XML 文件


    <select id="selectUserByNameAndAge" resultType="User">
    SELECT * FROM user WHERE name = #{param1} AND age = #{param2}
    </select>

    或者:


    <select id="selectUserByNameAndAge" resultType="User">
    SELECT * FROM user WHERE name = #{0} AND age = #{1}
    </select>

    3. 测试代码


    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    User user = mapper.selectUserByNameAndAge("John", 25);
    System.out.println(user);
    sqlSession.close();




    • 如果 Mapper 接口方法有多个参数且没有使用 @Param 注解,可以通过 param1param2 或索引 01 等方式引用参数。

    • 这种方式可读性较差,容易出错,推荐使用 @Param 注解明确参数名称。

    • 使用 @Param 注解后,XML 文件中的参数引用会更加清晰和易于维护。


    作者:码农liuxin
    来源:juejin.cn/post/7475643579781333029
    收起阅读 »

    Java web后端转Java游戏后端

    作为Java后端开发者转向游戏后端开发,虽然核心编程能力相通,但游戏开发在架构设计、协议选择、实时性处理等方面有显著差异。以下从实际工作流程角度详细说明游戏后端开发的核心要点及前后端协作流程: 一、游戏后端核心职责 实时通信管理 采用WebSocket/...
    继续阅读 »

    作为Java后端开发者转向游戏后端开发,虽然核心编程能力相通,但游戏开发在架构设计、协议选择、实时性处理等方面有显著差异。以下从实际工作流程角度详细说明游戏后端开发的核心要点及前后端协作流程:




    一、游戏后端核心职责



    1. 实时通信管理



      • 采用WebSocket/TCP长连接(90%以上MMO游戏选择)

      • 使用Netty/Mina框架处理高并发连接(单机支撑5W+连接是基本要求)

      • 心跳机制设计(15-30秒间隔,检测断线)



    2. 游戏逻辑处理



      • 战斗计算(需在50ms内完成复杂技能伤害计算)

      • 状态同步(通过Delta同步优化带宽,减少60%数据传输量)

      • 定时器管理(Quartz/时间轮算法处理活动开启等)



    3. 数据持久化



      • Redis集群缓存热点数据(玩家属性缓存命中率需>95%)

      • 分库分表设计(例如按玩家ID取模分128个库)

      • 异步落库机制(使用Disruptor队列实现每秒10W+写入)






    二、开发全流程实战(以MMORPG为例)


    阶段1:预研设计(2-4周)



    • 协议设计
      // 使用Protobuf定义移动协议
      message PlayerMove {
      int32 player_id = 1;
      Vector3 position = 2; // 三维坐标
      float rotation = 3; // 朝向
      int64 timestamp = 4; // 客户端时间戳
      }

      message BattleSkill {
      int32 skill_id = 1;
      repeated int32 target_ids = 2; // 多目标锁定
      Coordinate cast_position = 3; // 技能释放位置
      }


    • 架构设计
      graph TD
      A[Gateway] --> B[BattleServer]
      A --> C[SocialServer]
      B --> D[RedisCluster]
      C --> E[MySQLCluster]
      F[MatchService] --> B



    阶段2:核心系统开发(6-8周)



    1. 网络层实现


      // Netty WebSocket处理器示例
      @ChannelHandler.Sharable
      public class GameServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
      @Override
      protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame frame) {
      ProtocolMsg msg = ProtocolParser.parse(frame.text());
      switch (msg.getType()) {
      case MOVE:
      handleMovement(ctx, (MoveMsg)msg);
      break;
      case SKILL_CAST:
      validateSkillCooldown((SkillMsg)msg);
      broadcastToAOI(ctx.channel(), msg);
      break;
      }
      }
      }


    2. AOI(Area of Interest)管理



      • 九宫格算法实现视野同步

      • 动态调整同步频率(近距离玩家100ms/次,远距离500ms/次)



    3. 战斗系统



      • 采用确定性帧同步(Lockstep)

      • 使用FixedPoint替代浮点数运算保证一致性






    三、前后端协作关键点



    1. 协议版本控制



      • 强制版本校验:每个消息头包含协议版本号


      {
      "ver": "1.2.3",
      "cmd": 1001,
      "data": {...}
      }


    2. 调试工具链建设



      • 开发GM指令系统:


      /debug latency 200  // 模拟200ms延迟
      /simulate 5000 // 生成5000个机器人


    3. 联调流程



      • 使用Wireshark抓包分析时序问题

      • Unity引擎侧实现协议回放功能

      • 自动化测试覆盖率要求:

        • 基础协议:100%

        • 战斗用例:>85%








    四、性能优化实践



    1. JVM层面



      • G1GC参数优化:


      -XX:+UseG1GC -XX:MaxGCPauseMillis=50 
      -XX:InitiatingHeapOccupancyPercent=35


    2. 网络优化



      • 启用Snappy压缩协议(降低30%流量)

      • 合并小包(Nagle算法+50ms合并窗口)



    3. 数据库优化



      • 玩家数据冷热分离:

        • 热数据:位置、状态(Redis)

        • 冷数据:成就、日志(MySQL)








    五、上线后运维



    1. 监控体系



      • 关键指标报警阈值设置:

        • 单服延迟:>200ms

        • 消息队列积压:>1000

        • CPU使用率:>70%持续5分钟





    2. 紧急处理预案



      • 自动扩容规则:
        if conn_count > 40000:
        spin_up_new_instance()
        if qps > 5000:
        enable_rate_limiter()







    六、常见问题解决方案


    问题场景:战斗不同步

    排查步骤



    1. 对比客户端帧日志与服务端校验日志

    2. 检查确定性随机数种子一致性

    3. 验证物理引擎的FixedUpdate时序


    问题场景:登录排队

    优化方案



    1. 令牌桶限流算法控制进入速度

    2. 预计等待时间动态计算:
      wait_time = current_queue_size * avg_process_time / available_instances



    通过以上流程,Java后端开发者可逐步掌握游戏开发特性,重点需要转变的思维模式包括:从请求响应模式到实时状态同步、从CRUD主导到复杂逻辑计算、从分钟级延迟到毫秒级响应的要求。建议从简单的棋牌类游戏入手,逐步过渡到大型实时游戏开发。


    作者:加瓦点灯
    来源:juejin.cn/post/7475292103146684479
    收起阅读 »

    这个中国亲戚关系计算器让你告别“社死”

    web
    大家好,我是 Java陈序员。 由于为了生活奔波,常年在外,导致很多关系稍疏远的亲戚之间来往并不多。 因此节假日回家时,往往会搞不清楚哪位亲戚应该喊什么称呼,很容易“社死”。 今天给大家介绍一个亲戚关系计算器,让你快速的计算出正确的亲戚称谓! 关注微信公众号...
    继续阅读 »

    大家好,我是 Java陈序员


    由于为了生活奔波,常年在外,导致很多关系稍疏远的亲戚之间来往并不多。


    因此节假日回家时,往往会搞不清楚哪位亲戚应该喊什么称呼,很容易“社死”。


    今天给大家介绍一个亲戚关系计算器,让你快速的计算出正确的亲戚称谓!



    关注微信公众号:【Java陈序员】,获取开源项目分享、AI副业分享、超200本经典计算机电子书籍等。



    项目介绍


    relationship —— 中国亲戚关系计算器,只需简单的输入即可算出称谓。



    输入框兼容了不同的叫法,你可以称呼父亲为:“老爸”、“爹地”、“老爷子”等等,方便不同地域的习惯叫法。


    快捷输入按键,只需简单的点击即可完成关系输入,算法还支持逆向查找称呼哦~


    功能特色:



    • 使用别称查询:姥姥的爸爸的老窦 = 外曾外曾祖父

    • 使用合称查询:姐夫的双亲 = 姊妹姻父 / 姊妹姻母

    • 大小数字混合查询:大哥的二姑妈的七舅姥爷 = 舅曾外祖父

    • 不限制祖辈孙辈跨度查询:舅妈的婆婆的外甥的姨妈的侄子 = 舅表舅父

    • 根据年龄推导可能性:哥哥的表姐 = 姑表姐 / 舅表姐

    • 根据语境确认性别:老婆的女儿的外婆 = 岳母

    • 支持古文式表达:吾父之舅父 = 舅爷爷

    • 解析某称谓关系链:七舅姥爷 = 妈妈的妈妈的兄弟

    • 算两个亲戚间的合称关系:奶奶 + 外婆 = 儿女亲家


    项目地址:


    https://github.com/mumuy/relationship

    在线体验:


    https://passer-by.com/relationship/

    移动端体验地址:


    https://passer-by.com/relationship/vue/

    功能体验


    1、关系找称呼



    2、称呼找关系



    3、两者间关系



    4、两者的合称



    安装使用


    1、直接引入安装


    <script src="https://passer-by.com/relationship/dist/relationship.min.js">

    获取全局方法 relationship.


    2、使用 npm 包管理安装


    安装依赖:


    npm install relationship.js

    包引入:


    // CommonJS 引入
    const relationship = require("relationship.js");

    // ES Module 引入
    import relationship from 'relationship.js';

    3、使用方法:唯一的计算方法 relationship.



    • 选项模式 relationship(options)


      构造函数:


      var options = {
      text:'', // 目标对象:目标对象的称谓汉字表达,称谓间用‘的’字分隔
      target:'', // 相对对象:相对对象的称谓汉字表达,称谓间用‘的’字分隔,空表示自己
      sex:-1, // 本人性别:0表示女性,1表示男性
      type:'default', // 转换类型:'default'计算称谓,'chain'计算关系链,'pair'计算关系合称
      reverse:false, // 称呼方式:true对方称呼我,false我称呼对方
      mode:'default', // 模式选择:使用setMode方法定制不同地区模式,在此选择自定义模式
      optimal:false, // 最短关系:计算两者之间的最短关系
      };

      代码示例:


      // 如:我应该叫外婆的哥哥什么?
      relationship({text:'妈妈的妈妈的哥哥'});
      // => ['舅外公']

      // 如:七舅姥爷应该叫我什么?
      relationship({text:'七舅姥爷',reverse:true,sex:1});
      // => ['甥外孙']

      // 如:舅公是什么亲戚
      relationship({text:'舅公',type:'chain'});
      // => ['爸爸的妈妈的兄弟', '妈妈的妈妈的兄弟', '老公的妈妈的兄弟']

      // 如:舅妈如何称呼外婆?
      relationship({text:'外婆',target:'舅妈',sex:1});
      // => ['婆婆']

      // 如:外婆和奶奶之间是什么关系?
      relationship({text:'外婆',target:'奶奶',type:'pair'});
      // => ['儿女亲家']


    • 语句模式 relationship(exptession)



      参数 exptession 句式可以为:xxx是xxx的什么人、xxx叫xxx什么、xxx如何称呼xxx等。



      代码示例:


      // 如:舅妈如何称呼外婆?
      relationship('舅妈如何称呼外婆?');
      // => ['婆婆']

      // 如:外婆和奶奶之间是什么关系?
      relationship('外婆和奶奶之间是什么关系?');
      // => ['儿女亲家']



    4、其他 API


    // 获取当前数据表 
    relationship.data

    // 获取当前数据量
    relationship.dataCount

    // 用户自定义模式
    relationship.setMode(mode_name,mode_data)

    最后


    推荐的开源项目已经收录到 GitHub 项目,欢迎 Star


    https://github.com/chenyl8848/great-open-source-project

    或者访问网站,进行在线浏览:


    https://chencoding.top:8090/#/


    大家的点赞、收藏和评论都是对作者的支持,如文章对你有帮助还请点赞转发支持下,谢谢!



    作者:Java陈序员
    来源:juejin.cn/post/7344573753538330678
    收起阅读 »

    实现抖音 “视频无限滑动“效果

    web
    前言 在家没事的时候刷抖音玩,抖音首页的视频怎么刷也刷不完,经常不知不觉的一刷就到半夜了😅 不禁感叹道 "垃圾抖音,费我时间,毁我青春😅" 这是我的 模仿抖音 系列文章的第二篇,本文将一步步实现抖音首页 视频无限滑动 的效果,干货满满 第一篇:200行代码...
    继续阅读 »

    前言


    在家没事的时候刷抖音玩,抖音首页的视频怎么刷也刷不完,经常不知不觉的一刷就到半夜了😅

    不禁感叹道 "垃圾抖音,费我时间,毁我青春😅"




    这是我的 模仿抖音 系列文章的第二篇,本文将一步步实现抖音首页 视频无限滑动 的效果,干货满满



    第一篇:200行代码实现类似Swiper.js的轮播组件

    第三篇:Vue 路由使用介绍以及添加转场动画

    第四篇:Vue 有条件路由缓存,就像传统新闻网站一样

    第五篇:Github Actions 部署 Pages、同步到 Gitee、翻译 README 、 打包 docker 镜像



    如果您对滑动原理不太熟悉,推荐先看我的这篇文章:200行代码实现类似Swiper.js的轮播组件


    最终效果


    在线预览:dy.ttentau.top/


    Github地址:github.com/zyronon/dou…


    源码:SlideVerticalInfinite.vue


    实现原理


    无限滑动的原理和虚拟滚动的原理差不多,要保持 SlideList 里面永远只有 NSlideItem,就要在滑动时不断的删除和增加 SlideItem

    滑动时调整 SlideList 的偏移量 translateY 的值,以及列表里那几个 SlideItemtop 值,就可以了


    为什么要调整 SlideList 的偏移量 translateY 的值同时还要调整 SlideItemtop 值呢?

    因为 translateY 只是将整个列表移动,如果我们列表里面的元素是固定的,不会变多和减少,那么没关系,只调整 translateY 值就可以了,上滑了几页就减几页的高度,下滑同理


    但是如果整个列表向前移动了一页,同时前面的 SlideItem 也少了一个,,那么最终效果就是移动了两页...因为 塌陷 了一页

    这显然不是我们想要的,所以我们还需要同时调整 SlideItemtop 值,加上前面少的 SlideItem 的高度,这样才能显示出正常的内容


    步骤


    定义




    virtualTotal:页面中同时存在多少个 SlideItem,默认为 5


    //页面中同时存在多少个SlideItem
    virtualTotal: {
    type: Number,
    default: () => 5
    },

    设置这个值可以让外部组件使用时传入,毕竟每个人的需求不同,有的要求同时存在 10 条,有的要求同时存在 5 条即可。

    不过同时存在的数量越大,使用体验就越好,即使用户快速滑动,我们依然有时间处理。

    如果只同时存在 5 条,用户只需要快速滑动两次就到底了(因为屏幕中显示第 3 条,刚开始除外),我们可能来不及添加新的视频到最后




    render:渲染函数,SlideItem内显示什么由render返回值决定


    render: {
    type: Function,
    default: () => {
    return null
    }
    },

    之所以要设定这个值,是因为抖音首页可不只有视频,还有图集、推荐用户、广告等内容,所以我们不能写死显示视频。

    最好是定义一个方法,外部去实现,我们内部去调用,拿到返回值,添加到 SlideList




    list:数据列表,外部传入


    list: {
    type: Array,
    default: () => {
    return []
    }
    },

    我们从 list 中取出数据,然后调用并传给 render 函数,将其返回值插入到 SlideList中


    初始化



    watch(
    () => props.list,
    (newVal, oldVal) => {
    //新数据长度比老数据长度小,说明是刷新
    if (newVal.length < oldVal.length) {
    //从list中取出数据,然后调用并传给render函数,将其返回值插入到SlideList中
    insertContent()
    } else {
    //没数据就直接插入
    if (oldVal.length === 0) {
    insertContent()
    } else {
    // 走到这里,说明是通过接口加载了下一页的数据,
    // 为了在用户快速滑动时,无需频繁等待请求接口加载数据,给用户更好的使用体验
    // 这里额外加载3条数据。所以此刻,html里面有原本的5个加新增的3个,一共8个dom
    // 用户往下滑动时只删除前面多余的dom,等滑动到临界值(virtualTotal/2+1)时,再去执行新增逻辑
    }
    }
    }
    )

    watch 监听 list 是因为它一开始不一定有值,通过接口请求之后才有值

    同时当我们下滑 加载更多 时,也会触发接口请求新的数据,用 watch 可以在有新数据时,多添加几条到 SlideList 的最后面,这样用户快速滑动也不怕了


    如何滑动


    这里就不再赘述,参考我的这篇文章:200行代码实现类似Swiper.js的轮播组件


    滑动结束


    判断滑动的方向


    当我们向上滑动时,需要删除最前面的 dom ,然后在最后面添加一个 dom

    下滑时反之


    slideTouchEnd(e, state, canNext, (isNext) => {
    if (props.list.length > props.virtualTotal) {
    //手指往上滑(即列表展示下一条视频)
    if (isNext) {
    //删除最前面的 `dom` ,然后在最后面添加一个 `dom`
    } else {
    //删除最后面的 `dom` ,然后在最前面添加一个 `dom`
    }
    }
    })

    手指往上滑(即列表展示下一条视频)



    • 首先判断是否要加载更多,快到列表末尾时就要加载更多数据了

    • 再判断是否符合 腾挪 的条件,即当前位置要大于 half,且小于列表长度减 half

    • 在最后面添加一个 dom

    • 删除最前面的 dom

    • 将所有 dom 设置为最新的 top 值(原因前面有讲,因为删除了最前面的 dom,导致塌陷一页,所以要加上删除 dom 的高度)


    let half = (props.virtualTotal - 1) / 2

    //删除最前面的 `dom` ,然后在最后面添加一个 `dom`
    if (state.localIndex > props.list.length - props.virtualTotal && state.localIndex > half) {
    emit('loadMore')
    }

    //是否符合 `腾挪` 的条件
    if (state.localIndex > half && state.localIndex < props.list.length - half) {
    //在最后面添加一个 `dom`
    let addItemIndex = state.localIndex + half
    let res = slideListEl.value.querySelector(`.${itemClassName}[data-index='${addItemIndex}']`)
    if (!res) {
    slideListEl.value.appendChild(getInsEl(props.list[addItemIndex], addItemIndex))
    }

    //删除最前面的 `dom`
    let index = slideListEl.value
    .querySelector(`.${itemClassName}:first-child`)
    .getAttribute('data-index')
    appInsMap.get(Number(index)).unmount()

    slideListEl.value.querySelectorAll(`.${itemClassName}`).forEach((item) => {
    _css(item, 'top', (state.localIndex - half) * state.wrapper.height)
    })
    }

    手指往下滑(即列表展示上一条视频)


    逻辑和上滑都差不多,不过是反着来而已



    • 再判断是否符合 腾挪 的条件,和上面反着

    • 在最前面添加一个 dom

    • 删除最后面的 dom

    • 将所有 dom 设置为最新的 top


    //删除最后面的 `dom` ,然后在最前面添加一个 `dom`
    if (state.localIndex >= half && state.localIndex < props.list.length - (half + 1)) {
    let addIndex = state.localIndex - half
    if (addIndex >= 0) {
    let res = slideListEl.value.querySelector(`.${itemClassName}[data-index='${addIndex}']`)
    if (!res) {
    slideListEl.value.prepend(getInsEl(props.list[addIndex], addIndex))
    }
    }
    let index = slideListEl.value
    .querySelector(`.${itemClassName}:last-child`)
    .getAttribute('data-index')
    appInsMap.get(Number(index)).unmount()

    slideListEl.value.querySelectorAll(`.${itemClassName}`).forEach((item) => {
    _css(item, 'top', (state.localIndex - half) * state.wrapper.height)
    })
    }

    其他问题


    为什么不直接用 v-for直接生成 SlideItem 呢?


    如果内容不是视频就可以。要删除或者新增时,直接操作 list 数据源,这样省事多了


    如果内容是视频,修改 list 时,Vue 会快速的替换 dom,正在播放的视频,突然一下从头开始播放了😅😅😅


    如何获取 Vue 组件的最终 dom


    有两种方式,各有利弊



    • Vuerender 方法

      • 优点:只是渲染一个 VNode 而已,理论上讲内存消耗更少。

      • 缺点:但我在开发中,用了这个方法,任何修改都会刷新页面,有点难蚌😅



    • VuecreateApp 方法再创建一个 Vue 的实例

      • 和上面相反😅




    import { createApp, onMounted, reactive, ref, render as vueRender, watch } from 'vue'

    /**
    * 获取Vue组件渲染之后的dom元素
    * @param item
    * @param index
    * @param play
    */

    function getInsEl(item, index, play = false) {
    // console.log('index', cloneDeep(item), index, play)
    let slideVNode = props.render(item, index, play, props.uniqueId)
    const parent = document.createElement('div')
    //TODO 打包到线上时用这个,这个在开发时任何修改都会刷新页面
    if (import.meta.env.PROD) {
    parent.classList.add('slide-item')
    parent.setAttribute('data-index', index)
    //将Vue组件渲染到一个div上
    vueRender(slideVNode, parent)
    appInsMap.set(index, {
    unmount: () => {
    vueRender(null, parent)
    parent.remove()
    }
    })
    return parent
    } else {
    //创建一个新的Vue实例,并挂载到一个div上
    const app = createApp({
    render() {
    return <SlideItem data-index={index}>{slideVNode}</SlideItem>
    }
    })
    const ins = app.mount(parent)
    appInsMap.set(index, app)
    return ins.$el
    }
    }

    总结


    原理其实并不难。主要是一开始可能会用 v-for 去弄,折腾半天发现不行。v-for 不行,就只能想想怎么把 Vue 组件搞到 html 里面去,又去研究如何获取 Vue 组件的最终 dom,又查了半天资料,Vue 官方文档也不写,还得去翻 api ,麻了


    结束



    以上就是文章的全部内容,感谢看到这里,希望对你有所帮助或启发!创作不易,如果觉得文章写得不错,可以点赞收藏支持一下,也欢迎关注我的公众号 前端张余让,我会更新更多实用的前端知识与技巧,期待与你共同成长~



    作者:前端张余让
    来源:juejin.cn/post/7361614921519054883
    收起阅读 »

    autohue.js:让你的图片和背景融为一体,绝了!

    web
    需求 先来看这样一个场景,拿一个网站举例 这里有一个常见的网站 banner 图容器,大小为为1910*560,看起来背景图完美的充满了宽度,但是图片原始大小时,却是: 它的宽度只有 1440,且 background-size 设置的是 contain ...
    继续阅读 »

    需求


    先来看这样一个场景,拿一个网站举例


    image.png


    这里有一个常见的网站 banner 图容器,大小为为1910*560,看起来背景图完美的充满了宽度,但是图片原始大小时,却是:


    image.png


    它的宽度只有 1440,且 background-size 设置的是 contain ,即等比例缩放,那么可以断定它两边的蓝色是依靠背景色填充的。


    那么问题来了,这是一个 轮播banner,如果希望添加一张不是蓝色的图片呢?难道要给每张图片提前标注好背景颜色吗?这显然是非常死板的做法。


    所以需要从图片中提取到图片的主题色,当然这对于 js 来说,也不是什么难事,市面上已经有众多的开源库供我们使用。


    探索


    首先在网络上找到了以下几个库:



    • color-thief 这是一款基于 JavaScript 和 Canvas 的工具,能够从图像中提取主要颜色或代表性的调色板

    • vibrant.js 该插件是 Android 支持库中 Palette 类的 JavaScript 版本,可以从图像中提取突出的颜色

    • rgbaster.js 这是一段小型脚本,可以获取图片的主色、次色等信息,方便实现一些精彩的 Web 交互效果


    我取最轻量化的 rgbaster.js(此库非常搞笑,用TS编写,npm 包却没有指定 types) 来测试后发现,它给我在一个渐变色图片中,返回了七万多个色值,当然,它准确的提取出了面积最大的色值,但是这个色值不是图片边缘的颜色,导致设置为背景色后,并不能完美的融合。


    另外的插件各位可以参考这几篇文章:



    可以发现,这些插件主要功能就是取色,并没有考虑实际的应用场景,对于一个图片颜色分析工具来说,他们做的很到位,但是在大多数场景中,他们往往是不适用的。


    在文章 2 中,作者对比了三款插件对于图片容器背景色的应用,看起来还是 rgbaster 效果好一点,但是我们刚刚也拿他试了,它并不能适用于颜色复杂度高的、渐变色的图片。


    思考


    既然又又又没有人做这件事,正所谓我不入地狱谁入地狱,我手写一个


    整理一下需求,我发现我希望得到的是:



    1. 图片的主题色(面积占比最大)

    2. 次主题色(面积占比第二大)

    3. 合适的背景色(即图片边缘颜色,渐变时,需要边缘颜色来设置背景色)


    这样一来,就已经可以覆盖大部分需求了,1+2 可以生成相关的 主题 TAG、主题背景,3 可以使留白的图片容器完美融合。


    开搞


    ⚠⚠ 本小节内容非常硬核,如果不想深究原理可以直接跳过,文章末尾有用法和效果图 ⚠⚠


    思路


    首先需要避免上面提到的插件的缺点,即对渐变图片要做好处理,不能取出成千上万的颜色,体验太差且实用性不强,对于渐变色还有一点,即在渐变路径上,每一点的颜色都是不一样的,所以需要将他们以一个阈值分类,挑选出一众相近色,并计算出一个平均色,这样就不会导致主题色太精准进而没有代表性。


    对于背景色,需要按情况分析,如果只是希望做一个协调的页面,那么大可以直接使用主题色做渐变过渡或蒙层,也就是类似于这种效果


    image.png


    但是如果希望背景与图片完美衔接,让人看不出图片边界的感觉,就需要单独对边缘颜色取色了。


    最后一个问题,如果图片分辨率过大,在遍历像素点时会非常消耗性能,所以需要降低采样率,虽然会导致一些精度上的丢失,但是调整为一个合适的值后应该基本可用。


    剩余的细节问题,我会在下面的代码中解释


    使用 JaveScript 编码


    接下来我将详细描述 autohue.js 的实现过程,由于本人对色彩科学不甚了解,如有解释不到位或错误,还请指出。


    首先编写一个入口主函数,我目前考虑到的参数应该有:


    export default async function colorPicker(imageSource: HTMLImageElement | string, options?: autoColorPickerOptions)
    type thresholdObj = { primary?: number; left?: number; right?: number; top?: number; bottom?: number }
    interface autoColorPickerOptions {
    /**
    * - 降采样后的最大尺寸(默认 100px)
    * - 降采样后的图片尺寸不会超过该值,可根据需求调整
    * - 降采样后的图片尺寸越小,处理速度越快,但可能会影响颜色提取的准确性
    **/

    maxSize?: number
    /**
    * - Lab 距离阈值(默认 10)
    * - 低于此值的颜色归为同一簇,建议 8~12
    * - 值越大,颜色越容易被合并,提取的颜色越少
    * - 值越小,颜色越容易被区分,提取的颜色越多
    **/

    threshold?: number | thresholdObj
    }


    概念解释 Lab ,全称:CIE L*a*bCIE L*a*b*CIE XYZ色彩模式的改进型。它的“L”(明亮度),“a”(绿色到红色)和“b”(蓝色到黄色)代表许多的值。与XYZ比较,CIE L*a*b*的色彩更适合于人眼感觉的色彩,正所谓感知均匀



    然后需要实现一个正常的 loadImg 方法,使用 canvas 异步加载图片


    function loadImage(imageSource: HTMLImageElement | string): Promise<HTMLImageElement> {
    return new Promise((resolve, reject) => {
    let img: HTMLImageElement
    if (typeof imageSource === 'string') {
    img = new Image()
    img.crossOrigin = 'Anonymous'
    img.src = imageSource
    } else {
    img = imageSource
    }
    if (img.complete) {
    resolve(img)
    } else {
    img.onload = () => resolve(img)
    img.onerror = (err) => reject(err)
    }
    })
    }

    这样我们就获取到了图片对象。


    然后为了图片过大,我们需要进行降采样处理


    // 利用 Canvas 对图片进行降采样,返回 ImageData 对象
    function getImageDataFromImage(img: HTMLImageElement, maxSize: number = 100): ImageData {
    const canvas = document.createElement('canvas')
    let width = img.naturalWidth
    let height = img.naturalHeight
    if (width > maxSize || height > maxSize) {
    const scale = Math.min(maxSize / width, maxSize / height)
    width = Math.floor(width * scale)
    height = Math.floor(height * scale)
    }
    canvas.width = width
    canvas.height = height
    const ctx = canvas.getContext('2d')
    if (!ctx) {
    throw new Error('无法获取 Canvas 上下文')
    }
    ctx.drawImage(img, 0, 0, width, height)
    return ctx.getImageData(0, 0, width, height)
    }



    概念解释,降采样:降采样(Downsampling)是指在图像处理中,通过减少数据的采样率或分辨率来降低数据量的过程。具体来说,就是在保持原始信息大致特征的情况下,减少数据的复杂度和存储需求。这里简单理解为将图片强制压缩为 100*100 以内,也是 canvas 压缩图片的常见做法。



    得到图像信息后,就可以对图片进行像素遍历处理了,正如思考中提到的,我们需要对相近色提取并取平均色,并最终获取到主题色、次主题色。


    那么问题来了,什么才算相近色,对于这个问题,在 常规的 rgb 中直接计算是不行的,因为它涉及到一个感知均匀的问题



    概念解释,感知均匀:XYZ系统和在它的色度图上表示的两种颜色之间的距离与颜色观察者感知的变化不一致,这个问题叫做感知均匀性(perceptual uniformity)问题,也就是颜色之间数字上的差别与视觉感知不一致。由于我们需要在颜色簇中计算出平均色,那么对于人眼来说哪些颜色是相近的?此时,我们需要把 sRGB 转化为 Lab 色彩空间(感知均匀的),再计算其欧氏距离,在某一阈值内的颜色,即可认为是相近色。



    所以我们首先需要将 rgb 转化为 Lab 色彩空间


    // 将 sRGB 转换为 Lab 色彩空间
    function rgbToLab(r: number, g: number, b: number): [number, number, number] {
    let R = r / 255,
    G = g / 255,
    B = b / 255
    R = R > 0.04045 ? Math.pow((R + 0.055) / 1.055, 2.4) : R / 12.92
    G = G > 0.04045 ? Math.pow((G + 0.055) / 1.055, 2.4) : G / 12.92
    B = B > 0.04045 ? Math.pow((B + 0.055) / 1.055, 2.4) : B / 12.92

    let X = R * 0.4124 + G * 0.3576 + B * 0.1805
    let Y = R * 0.2126 + G * 0.7152 + B * 0.0722
    let Z = R * 0.0193 + G * 0.1192 + B * 0.9505

    X = X / 0.95047
    Y = Y / 1.0
    Z = Z / 1.08883

    const f = (t: number) => (t > 0.008856 ? Math.pow(t, 1 / 3) : 7.787 * t + 16 / 116)
    const fx = f(X)
    const fy = f(Y)
    const fz = f(Z)
    const L = 116 * fy - 16
    const a = 500 * (fx - fy)
    const bVal = 200 * (fy - fz)
    return [L, a, bVal]
    }

    这个函数使用了看起来很复杂的算法,不必深究,这是它的大概解释:



    1. 获取到 rgb 参数

    2. 转化为线性 rgb(移除 gamma矫正),常量 0.04045 是sRGB(标准TGB)颜色空间中的一个阈值,用于区分非线性和线性的sRGB值,具体来说,当sRGB颜色分量大于0.04045时,需要通过 gamma 校正(即采用 ((R + 0.055) / 1.055) ^ 2.4)来得到线性RGB;如果小于等于0.04045,则直接进行线性转换(即 R / 12.92

    3. 线性RGB到XYZ空间的转换,转换公式如下:



      • X = R * 0.4124 + G * 0.3576 + B * 0.1805

      • Y = R * 0.2126 + G * 0.7152 + B * 0.0722

      • Z = R * 0.0193 + G * 0.1192 + B * 0.9505



    4. 归一化XYZ值,为了参考白点(D65),标准白点的XYZ值是 (0.95047, 1.0, 1.08883)。所以需要通过除以这些常数来进行归一化

    5. XYZ到Lab的转换,公式函数:const f = (t: number) => (t > 0.008856 ? Math.pow(t, 1 / 3) : 7.787 * t + 16 / 116)

    6. 计算L, a, b 分量


      L:亮度分量(表示颜色的明暗程度)



      • L = 116 * fy - 16


      a:绿色到红色的色差分量



      • a = 500 * (fx - fy)


      b:蓝色到黄色的色差分量



      • b = 200 * (fy - fz)




    接下来实现聚类算法


    /**
    * 对满足条件的像素进行聚类
    * @param imageData 图片像素数据
    * @param condition 判断像素是否属于指定区域的条件函数(参数 x, y)
    * @param threshold Lab 距离阈值,低于此值的颜色归为同一簇,建议 8~12
    */

    function clusterPixelsByCondition(imageData: ImageData, condition: (x: number, y: number) => boolean, threshold: number = 10): Cluster[] {
    const clusters: Cluster[] = []
    const data = imageData.data
    const width = imageData.width
    const height = imageData.height
    for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
    if (!condition(x, y)) continue
    const index = (y * width + x) * 4
    if (data[index + 3] === 0) continue // 忽略透明像素
    const r = data[index]
    const g = data[index + 1]
    const b = data[index + 2]
    const lab = rgbToLab(r, g, b)
    let added = false
    for (const cluster of clusters) {
    const d = labDistance(lab, cluster.averageLab)
    if (d < threshold) {
    cluster.count++
    cluster.sumRgb[0] += r
    cluster.sumRgb[1] += g
    cluster.sumRgb[2] += b
    cluster.sumLab[0] += lab[0]
    cluster.sumLab[1] += lab[1]
    cluster.sumLab[2] += lab[2]
    cluster.averageRgb = [cluster.sumRgb[0] / cluster.count, cluster.sumRgb[1] / cluster.count, cluster.sumRgb[2] / cluster.count]
    cluster.averageLab = [cluster.sumLab[0] / cluster.count, cluster.sumLab[1] / cluster.count, cluster.sumLab[2] / cluster.count]
    added = true
    break
    }
    }
    if (!added) {
    clusters.push({
    count: 1,
    sumRgb: [r, g, b],
    sumLab: [lab[0], lab[1], lab[2]],
    averageRgb: [r, g, b],
    averageLab: [lab[0], lab[1], lab[2]]
    })
    }
    }
    }
    return clusters
    }

    函数内部有一个 labDistance 的调用,labDistance 是计算 Lab 颜色空间中的欧氏距离的


    // 计算 Lab 空间的欧氏距离
    function labDistance(lab1: [number, number, number], lab2: [number, number, number]): number {
    const dL = lab1[0] - lab2[0]
    const da = lab1[1] - lab2[1]
    const db = lab1[2] - lab2[2]
    return Math.sqrt(dL * dL + da * da + db * db)
    }


    概念解释,欧氏距离:Euclidean Distance,是一种在多维空间中测量两个点之间“直线”距离的方法。这种距离的计算基于欧几里得几何中两点之间的距离公式,通过计算两点在各个维度上的差的平方和,然后取平方根得到。欧氏距离是指n维空间中两个点之间的真实距离,或者向量的自然长度(即该点到原点的距离)。



    总的来说,这个函数采用了类似 K-means 的聚类方式,将小于用户传入阈值的颜色归为一簇,并取平均色(使用 Lab 值)。



    概念解释,聚类算法:Clustering Algorithm 是一种无监督学习方法,其目的是将数据集中的元素分成不同的组(簇),使得同一组内的元素相似度较高,而不同组之间的元素相似度较低。这里是将相近色归为一簇。




    概念解释,颜色簇:簇是聚类算法中一个常见的概念,可以大致理解为 "一类"



    得到了颜色簇集合后,就可以按照count大小来判断哪个是主题色了


      // 对全图所有像素进行聚类
    let clusters = clusterPixelsByCondition(imageData, () => true, threshold.primary)
    clusters.sort((a, b) => b.count - a.count)
    const primaryCluster = clusters[0]
    const secondaryCluster = clusters.length > 1 ? clusters[1] : clusters[0]
    const primaryColor = rgbToHex(primaryCluster.averageRgb)
    const secondaryColor = rgbToHex(secondaryCluster.averageRgb)

    现在我们已经获取到了主题色、次主题色 🎉🎉🎉


    接下来,我们继续计算边缘颜色


    按照同样的方法,只是把阈值设小一点,我这里直接设置为 1 (threshold.top 等都是1)


      // 分别对上、右、下、左边缘进行聚类
    const topClusters = clusterPixelsByCondition(imageData, (_x, y) => y < margin, threshold.top)
    topClusters.sort((a, b) => b.count - a.count)
    const topColor = topClusters.length > 0 ? rgbToHex(topClusters[0].averageRgb) : primaryColor

    const bottomClusters = clusterPixelsByCondition(imageData, (_x, y) => y >= height - margin, threshold.bottom)
    bottomClusters.sort((a, b) => b.count - a.count)
    const bottomColor = bottomClusters.length > 0 ? rgbToHex(bottomClusters[0].averageRgb) : primaryColor

    const leftClusters = clusterPixelsByCondition(imageData, (x, _y) => x < margin, threshold.left)
    leftClusters.sort((a, b) => b.count - a.count)
    const leftColor = leftClusters.length > 0 ? rgbToHex(leftClusters[0].averageRgb) : primaryColor

    const rightClusters = clusterPixelsByCondition(imageData, (x, _y) => x >= width - margin, threshold.right)
    rightClusters.sort((a, b) => b.count - a.count)
    const rightColor = rightClusters.length > 0 ? rgbToHex(rightClusters[0].averageRgb) : primaryColor

    这样我们就获取到了上下左右四条边的颜色 🎉🎉🎉


    这样大致的工作就完成了,最后我们将需要的属性导出给用户,我们的主函数最终长这样:


    /**
    * 主函数:根据图片自动提取颜色
    * @param imageSource 图片 URL 或 HTMLImageElement
    * @returns 返回包含主要颜色、次要颜色和背景色对象(上、右、下、左)的结果
    */

    export default async function colorPicker(imageSource: HTMLImageElement | string, options?: autoColorPickerOptions): Promise<AutoHueResult> {
    const { maxSize, threshold } = __handleAutoHueOptions(options)
    const img = await loadImage(imageSource)
    // 降采样(最大尺寸 100px,可根据需求调整)
    const imageData = getImageDataFromImage(img, maxSize)

    // 对全图所有像素进行聚类
    let clusters = clusterPixelsByCondition(imageData, () => true, threshold.primary)
    clusters.sort((a, b) => b.count - a.count)
    const primaryCluster = clusters[0]
    const secondaryCluster = clusters.length > 1 ? clusters[1] : clusters[0]
    const primaryColor = rgbToHex(primaryCluster.averageRgb)
    const secondaryColor = rgbToHex(secondaryCluster.averageRgb)

    // 定义边缘宽度(单位像素)
    const margin = 10
    const width = imageData.width
    const height = imageData.height

    // 分别对上、右、下、左边缘进行聚类
    const topClusters = clusterPixelsByCondition(imageData, (_x, y) => y < margin, threshold.top)
    topClusters.sort((a, b) => b.count - a.count)
    const topColor = topClusters.length > 0 ? rgbToHex(topClusters[0].averageRgb) : primaryColor

    const bottomClusters = clusterPixelsByCondition(imageData, (_x, y) => y >= height - margin, threshold.bottom)
    bottomClusters.sort((a, b) => b.count - a.count)
    const bottomColor = bottomClusters.length > 0 ? rgbToHex(bottomClusters[0].averageRgb) : primaryColor

    const leftClusters = clusterPixelsByCondition(imageData, (x, _y) => x < margin, threshold.left)
    leftClusters.sort((a, b) => b.count - a.count)
    const leftColor = leftClusters.length > 0 ? rgbToHex(leftClusters[0].averageRgb) : primaryColor

    const rightClusters = clusterPixelsByCondition(imageData, (x, _y) => x >= width - margin, threshold.right)
    rightClusters.sort((a, b) => b.count - a.count)
    const rightColor = rightClusters.length > 0 ? rgbToHex(rightClusters[0].averageRgb) : primaryColor

    return {
    primaryColor,
    secondaryColor,
    backgroundColor: {
    top: topColor,
    right: rightColor,
    bottom: bottomColor,
    left: leftColor
    }
    }
    }


    还记得本小节一开始提到的参数吗,你可以自定义 maxSize(压缩大小,用于降采样)、threshold(阈值,用于设置簇大小)


    为了用户友好,我还编写了 threshold 参数的可选类型:number | thresholdObj


    type thresholdObj = { primary?: number; left?: number; right?: number; top?: number; bottom?: number }

    可以单独设置主阈值、上下左右四边阈值,以适应更个性化的情况。


    autohue.js 诞生了


    名字的由来:秉承一贯命名习惯,auto 家族成员又多一个,与颜色有关的单词有好多个,我取了最短最好记的一个 hue(色相),也比较契合插件用途。


    此插件已在 github 开源:GitHub autohue.js


    npm 主页:NPM autohue.js


    在线体验:autohue.js 官方首页


    安装与使用


    pnpm i autohue.js

    import autohue from 'autohue.js'

    autohue(url, {
    threshold: {
    primary: 10,
    left: 1,
    bottom: 12
    },
    maxSize: 50
    })
    .then((result) => {
    // 使用 console.log 打印出色块元素s
    console.log(`%c${result.primaryColor}`, 'color: #fff; background: ' + result.primaryColor, 'main')
    console.log(`%c${result.secondaryColor}`, 'color: #fff; background: ' + result.secondaryColor, 'sub')
    console.log(`%c${result.backgroundColor.left}`, 'color: #fff; background: ' + result.backgroundColor.left, 'bg-left')
    console.log(`%c${result.backgroundColor.right}`, 'color: #fff; background: ' + result.backgroundColor.right, 'bg-right')
    console.log(`%clinear-gradient to right`, 'color: #fff; background: linear-gradient(to right, ' + result.backgroundColor.left + ', ' + result.backgroundColor.right + ')', 'bg')
    bg.value = `linear-gradient(to right, ${result.backgroundColor.left}, ${result.backgroundColor.right})`
    })
    .catch((err) => console.error(err))


    最终效果


    image.png


    复杂边缘效果


    image.png


    纵向渐变效果(这里使用的是 left 和 right 边的值,可能使用 top 和 bottom 效果更佳)


    image.png


    纯色效果(因为单独对边缘采样,所以无论图片内容多复杂,纯色基本看不出边界)


    image.png


    突变边缘效果(此时用css做渐变蒙层应该效果会更好)


    image.png


    横向渐变效果(使用的是 left 和 right 的色值),基本看不出边界


    参考资料



    番外


    Auto 家族的其他成员



    作者:德莱厄斯
    来源:juejin.cn/post/7471919714292105270
    收起阅读 »

    记一次 CDN 流量被盗刷经历

    先说损失,被刷了 70 多RMB,还好止损相对即时了,亏得不算多,PCDN 真可恶啊。 600多G流量,100多万次请求。 怎么发现的 先是看到鱼皮大佬发了一篇推文突发,众多网站流量被盗刷!我特么也中招了。 抱着看热闹的心情点开阅读了。。。心想,看看自己的中...
    继续阅读 »

    先说损失,被刷了 70 多RMB,还好止损相对即时了,亏得不算多,PCDN 真可恶啊。



    600多G流量,100多万次请求。


    怎么发现的


    先是看到鱼皮大佬发了一篇推文突发,众多网站流量被盗刷!我特么也中招了


    抱着看热闹的心情点开阅读了。。。心想,看看自己的中招没,结果就真中招了 🍉。


    被盗刷资源分析


    笔者在 缤纷云七牛云又拍云 都有存放一些图片资源。本次中招的是 缤纷云,下面是被刷的资源。



    IP来源


    查了几个 IP 和文章里描述的大差不差,都是来自山西联通的请求。



    大小流量计算


    按日志时间算的话,QPS 大概在 20 左右,单文件 632 K,1分钟大概就760MB ,1小时约 45G 左右。


    看了几天前的日志,都是 1 小时刷 40G 就停下,从 9 点左右开始,刷到 12 点。


    07-0907-08

    但是 10 号的就变多了,60-70 GB 1次了。也是这天晚上才开始做的反制,不知道是不是加策略的时候影响到它计算流量大小了 😝。



    反制手段


    Referer 限制


    通过观察这些资源的请求头,发现 Referer 和请求资源一致,通常情况下,不应该这样,应该是笔者的博客地址https://sugarat.top



    于是第一次就限制了 Referer 头不能为空,同时将 cdn.bitiful.sugarat.top 的来源都拉黑。


    这个办法还比较好使,后面的请求都给 403 了。



    但这个还是临时解决方案,在 V 站上看到讨论,说资源是人为筛选的,意味着 Referer 换个资源还是会发生变化。


    IP 限制


    有 GitHub 仓库 unclemcz/ban-pcdn-ip 收集了此次恶意刷流量的 IP。


    CDN 平台一般支持按 IP 或 IP 段屏蔽请求(虽然后者可能会屏蔽一些正常请求),可以将 IP 段配置到平台上,这样就能限制掉这些 IP 的请求。


    缤纷云上这块限制还比较弱,我就直接把缤纷云的 CDN 直接关了,七牛云和又拍云上都加上了 IP 和 地域运营商的限制,等这阵风头过去再恢复。


    七牛云又拍云

    限速


    限制单 IP 的QPS和峰值流量。



    但是这个只能避免说让它刷得慢一点,还是不治本。



    最后


    用了CDN的话,日常还是多看看,能加阈值控制的平台优先加上,常规的访问控制防盗链的啥的安排上。



    作者:粥里有勺糖
    来源:juejin.cn/post/7390678994998526003
    收起阅读 »

    新来的总监,把闭包讲得那叫一个透彻

    😃文章首发于公众号[精益码农]。 闭包作为前端面试的必考题目,常让1-3年工作经验的Javascripter感到困惑,我的主力语言C#/GO均有闭包。 1. 闭包:关键点在于函数是否捕获了其外部作用域的变量 闭包的形成: 定义函数时, 函数引用了其外部作用域的...
    继续阅读 »

    😃文章首发于公众号[精益码农]。


    闭包作为前端面试的必考题目,常让1-3年工作经验的Javascripter感到困惑,我的主力语言C#/GO均有闭包。


    1. 闭包:关键点在于函数是否捕获了其外部作用域的变量


    闭包的形成: 定义函数时, 函数引用了其外部作用域的变量, 之后就形成了闭包。


    闭包的结果: 引用的变量和定义的函数都会一同存在(即使已经脱离了函数定义/引用的变量的作用域),一直到闭包被消灭。


        public  static Action Closure()
    {
    var x = 1;
    Action action= () =>
    {
    var y = 1;
    var result = x + y;
    Console.WriteLine(result);
    x++;
    };
    return action;
    }

    public static void Main() {
    var a=Closure();
    a();
    a();
    }
    // 调用函数输出
    2
    3

    委托action是一个函数,它使用了“x”这个外部作用域的变量(x变量不是函数内局部变量),变量引用将被捕获形成闭包。


    即使action被返回了(即使“x”已经脱离了它被引用时的作用域环境(Closure)),但是两次执行能输出2,3 说明它脱离原引用环境仍然能用。




    当你在代码调试器(debugger)里观察“action”时,可以看到一个Target属性,里面封装了捕获的x变量:




    实际上,委托,匿名函数和lambda都是继承自Delegate类
    Delegate不允许开发者直接使用,只有编译器才能使用, 也就是说delegate Action都是语法糖。



    • Method:MethodInfo反射类型- 方法执行体

    • Target:当前委托执行的对象,这些语法糖由编译器生成了继承自Delegate类型的对象,包含了捕获的自由变量。



    再给一个反例:


    public class Program
    {
    private static int x = 1; // 静态字段
    public static void Main()
    {
    var action = NoClosure();
    action();
    action();
    }

    public static Action NoClosure(){
    Action action=()=>{
    var y =1;
    var sum = x+y;
    Console.WriteLine($"sum = { sum }");
    x++;
    };
    return action;
    }
    }

    x 是静态字段,在程序中有独立的存储区域, 不在线程的函数堆栈区,不属于某个特定的作用域。


    匿名函数使用了 x,但没有捕获外部作用域的变量,因此不构成闭包, Target属性对象无捕获的字段。


    从编程设计的角度:闭包开创了除全局变量传值, 函数参数传值之外的第三种变量使用方式。


    2. 闭包的形成时机和效果


    闭包是词法闭包的简称,维基百科上是这样定义的:

    在计算机科学中,闭包是在词法环境中绑定自由变量的一等函数”。


    闭包的形成时机:



    • 一等函数

    • 外部作用域变量


    闭包的形态:

    会捕获闭包函数内引用的外部作用域变量, 一直持有,直到闭包函数不再使用被销毁。



    内部实现是形成了一个对象(包含执行函数和捕获的变量,参考Target对象), 只有形成堆内存,才有后续闭包销毁的行为,当闭包这个对象不再被引用时,闭包被GC清理。



    闭包的作用周期:


    离不开作用域这个概念,函数理所当然管控了函数内的局部变量作用域,但当它引用了外部有作用域的变量时, 就形成了闭包函数。
    当闭包(例如一个委托或 lambda 表达式)不再被任何变量、对象或事件持有引用时,它就变成了“不可达”对象, 闭包被gc清理,其实就是堆内存被清理。


    2.1 一等函数


    一等函数很容易理解,就是在各语言, 函数被认为是某类数据类型, 定义函数就成了定义变量, 函数也可以像变量一样被传递。


    很明显,在C#中我们常使用的匿名函数、lambda表达式都是一等函数。


    Func<string,string> myFunc = delegate(string var1)
    {
    return "some value";
    };
    Func<string,string> myFunc = var1 => "some value";

    string myVar = myFunc("something");

    2.2 自由变量


    在函数中被引用的外部作用域变量, 注意, 这个变量是外部有作用域的变量,也就说排除全局变量(这些变量在程序的独立区域, 不属于任何作用域)。


    public void Test() 
    {
    var myVar = "this is good";
    Func<string,string> myFunc = delegate(string var1)
    {
    return var1 + myVar;
    };
    }

    上面这个示例,myFunc形成了闭包,捕获了myVar这个外部作用域的变量;
    即使Test函数返回了委托myFunc(脱离了定义myVar变量的作用域),闭包依然持有myVar的变量引用,
    注意,引用变量,并不是使用当时变量的副本值


    我们再回过头来看结合了线程调度的闭包面试题。


    3. 闭包函数关联线程调度: 依次打印连续的数字


     static void Closure1()
    {
    for (int i = 0; i < 10; i++)
    {
    Task.Run(()=> Console.WriteLine(i));
    }
    }

    每次输出数字不固定


    并不是预期的 0.1.2.3.4.5.6.7.8.9


    首先形成了闭包函数()=> Console.WriteLine(i), 捕获了外部有作用域变量i的引用, 此处捕获的变量i相对于函数是全局变量。
    但是Task调度闭包函数的时机不确定, 所以打印的是被调度时引用的变量i值。


    数字符合但乱序:为每个闭包函数绑定独立变量


    循环内增加局部变量, 解绑全局变量 (或者可以换成foreach,foreach相当于内部给你整了一个局部变量)。


    能输出乱序的0,1,2,3,4,5,6,7,8,9


    因为每次循环内产生的闭包函数捕获了对应的局部变量j,这样每个任务执行环境均独立维护了一个变量j, 这个j不是全局变量, 但是由于Task启动时机依然不确定,故是乱序。



    数字符合且有序


    核心是解决 Task调度问题。


    思路是:一个共享变量,每个任务打印该变量自增的一个阶段,但是该自增不允许被打断。


     public static void Main(string[] args)
    {
    var s =0;
    var lo = new Program();
    for (int i = 0; i < 10; i++)
    {
    Task.Run(()=>
    {
    lock(lo)
    {
    Console.WriteLine(s); // 依然形成了闭包函数, 之后闭包函数被线程调度
    s++;
    }
    });
    }
    Thread.Sleep(2000);
    } // 上面是一个明显的锁争用

    3.Golang闭包的应用


    gin 框架中中间件的默认形态是:


    package middleware
    func AuthenticationMiddleware(c *gin.Context) {
    ......
    }

    // Use方法的参数签名是这样: type HandlerFunc func(*Context), 不支持入参
    router.Use(middleware.AuthenticationMiddleware)

    实际实践上我们又需要给中间件传参, 闭包提供了这一能力。


    func Authentication2Middleware(log *zap.Logger) gin.HandlerFunc  {
    return func(c *gin.Context) {
    ... 这里面可以利用log 参数。
    }
    }

    var logger *zap.Logger
    api.Use(middleware.Authentication2Middleware(logger))

    总结


    本文屏蔽语言差异,理清了[闭包]的概念核心: 函数引用了其外部作用域的变量,


    核心特征:一等函数、自由变量,核心结果: 即使脱离了原捕获变量的原作用域,闭包函数依然持有该变量引用。


    不仅能帮助我们应对多语种有关闭包的面试题, 也帮助我们了解[闭包]在通用语言中的设计初衷。


    另外我们通过C# 调试器巩固了Delegate 抽象类,这是lambda表达式,委托,匿名函数的底层抽象数据结构类,包含两个重要属性 Method Target,分别表征了方法执行体、当前委托作用的对象,


    可想而知,其他语言也是通过这个机制捕获闭包当中的自由变量。


    作者:不卷牛马
    来源:juejin.cn/post/7474982751365038106
    收起阅读 »

    Java利用Deepseek进行项目代码审查

    一、为什么需要AI代码审查?写代码就像做饭,即使是最有经验的厨师(程序员),也难免会忘记关火(资源未释放)、放错调料(逻辑错误)或者切到手(空指针异常)。Deepseek就像一位24小时待命的厨房监理,能帮我们实时发现这些"安全隐患"。二、环境准备(5分钟搞定...
    继续阅读 »

    一、为什么需要AI代码审查?

    写代码就像做饭,即使是最有经验的厨师(程序员),也难免会忘记关火(资源未释放)、放错调料(逻辑错误)或者切到手(空指针异常)。Deepseek就像一位24小时待命的厨房监理,能帮我们实时发现这些"安全隐患"。

    二、环境准备(5分钟搞定)

    1. 安装Deepseek插件(以VSCode为例):
      • 插件市场搜索"Deepseek Code Review"
      • 点击安装(就像安装手机APP一样简单)

    1. Java项目配置:

    <dependency>
    <groupId>com.deepseekgroupId>
    <artifactId>code-analyzerartifactId>
    <version>1.3.0version>
    dependency>

    三、真实案例:用户管理系统漏洞检测

    原始问题代码:

    public class UserService {
    // 漏洞1:未处理空指针
    public String getUserRole(String userId) {
    return UserDB.query(userId).getRole();
    }

    // 漏洞2:资源未关闭
    public void exportUsers() {
    FileOutputStream fos = new FileOutputStream("users.csv");
    fos.write(getAllUsers().getBytes());
    }

    // 漏洞3:SQL注入风险
    public void deleteUser(String input) {
    Statement stmt = conn.createStatement();
    stmt.execute("DELETE FROM users WHERE id = " + input);
    }
    }

    使用Deepseek审查后:

    智能修复建议:

    1. 空指针防护 → 建议添加Optional处理
    2. 流资源 → 推荐try-with-resources语法
    3. SQL注入 → 提示改用PreparedStatement

    修正后的代码:

    public class UserService {
    // 修复1:Optional处理空指针
    public String getUserRole(String userId) {
    return Optional.ofNullable(UserDB.query(userId))
    .map(User::getRole)
    .orElse("guest");
    }

    // 修复2:自动资源管理
    public void exportUsers() {
    try (FileOutputStream fos = new FileOutputStream("users.csv")) {
    fos.write(getAllUsers().getBytes());
    }
    }

    // 修复3:预编译防注入
    public void deleteUser(String input) {
    PreparedStatement pstmt = conn.prepareStatement(
    "DELETE FROM users WHERE id = ?");
    pstmt.setString(1, input);
    pstmt.executeUpdate();
    }
    }

    四、实现原理揭秘

    Deepseek的代码审查就像"X光扫描仪",通过以下三步工作:

    1. 模式识别:比对数千万个代码样本
      • 就像老师批改作业时发现常见错误
    1. 上下文理解:分析代码的"人际关系"
      • 数据库连接有没有"成对出现"(打开/关闭)
      • 敏感操作有没有"保镖"(权限校验)
    1. 智能推理:预测代码的"未来"
      • 这个变量走到这里会不会变成null?
      • 这个循环会不会变成"无限列车"?

    五、进阶使用技巧

    1. 自定义审查规则(配置文件示例):
    rules:
    security:
    sql_injection: error
    performance:
    loop_complexity: warning
    style:
    var_naming: info

    2. 与CI/CD集成(GitHub Action示例):

    - name: Deepseek Code Review
    uses: deepseek-ai/code-review-action@v2
    with:
    severity_level: warning
    fail_on: error

    六、开发者常见疑问

    Q:AI会不会误判我的代码?
    A:就像导航偶尔会绕路,Deepseek给出的是"建议"而非"判决",最终决策权在你手中

    Q:处理历史遗留项目要多久?
    A:10万行代码项目约需3-5分钟,支持增量扫描

    七、效果对比数据

    指标人工审查Deepseek+人工
    平均耗时4小时30分钟
    漏洞发现率78%95%
    误报率5%12%
    知识库更新速度季度实时

    作者:Java技术小馆
    来源:juejin.cn/post/7473799336675639308

    收起阅读 »

    停止在TS中使用.d.ts文件

    web
    看到Matt Pocock 在 X 上的一个帖子提到不使用 .d.ts 文件的说法。 你赞同么?是否也应该把 .d.ts 文件都替换为 .ts 文件呢? 我们一起来看看~ .d.ts 文件的用途 首先,我们要澄清的是,.d.ts 文件并不是毫无用处的。 ...
    继续阅读 »

    看到Matt Pocock 在 X 上的一个帖子提到不使用 .d.ts 文件的说法。



    你赞同么?是否也应该把 .d.ts 文件都替换为 .ts 文件呢?


    我们一起来看看~




    .d.ts 文件的用途


    首先,我们要澄清的是,.d.ts 文件并不是毫无用处的。



    .d.ts 文件的用途主要用于为 JavaScript 代码提供类型描述。



    .d.ts 文件是严格的蓝图,用于表示你的源代码可以使用的类型。最早可以追溯到2012年。其设计受到头文件、接口描述语言(IDL)、JSDoc 等实践的启发,可以被视为 TypeScript 版本的头文件。


    .d.ts 文件只能包含声明,所以让我们通过一个代码示例来看看声明和实现之间的区别。假设我们有一个函数,它将两个数字相加:


    // 声明 (.d.ts)
    export function add(num1: number, num2: number): number;

    // 实现 (.ts)
    export function add(num1: number, num2: number): number {
    return num1 + num2;
    }

    正如你所见,add 函数的实现实际上展示了加法的执行过程,并返回结果,而声明则没有。




    那么 .d.ts 文件在实践中是如何使用的呢?


    假设我们有一个 add 函数,分别在两个文件中存储声明和实现:add.d.tsadd.js


    现在我们创建一个新文件 index.js,它将实际使用 add 函数:


    import { add } from "./x";

    const result = add(1, 4);
    console.log(result); // 输出:5

    请注意,在这个 JS 文件中,add 函数具有类型安全性,因为函数在 add.d.ts 中被标注了类型声明。




    替换方案 .ts 文件


    我们已经了解了 .d.ts 文件的工作原理以及它们的用途。Matt 之所以认为不需要.d.ts 文件,是因为它也可以放在一个 .ts 文件中直接创建带有类型标注的实现。也就是说,拥有一个包含声明和实现的单个 add.ts 文件,等同于分别定义了 add.d.tsadd.js 文件。


    这意味着你无需担心将声明文件与其对应的实现文件分开组织。




    不过,针对类库,将 .d.ts 文件与编译后的 JavaScript 源代码一起使用,比存储 .ts 文件更高效,因为你真正需要的只是类型声明,以便用户在使用你的库时能够获得类型安全性。


    这确实没错,需要强调的是,更推荐自动生成。通过更改 package.jsontsconfig.json 文件中的几个设置,从 .ts 文件自动生成 .d.ts 文件:



    • tsconfig.json:确保添加 declaration: true,以支持 .d.ts 文件的生成。


    {
    "compilerOptions": {
    "declaration": true,
    "target": "ES6",
    "module": "commonjs",
    "outDir": "./dist",
    "strict": true
    },
    "include": ["src/**/*"]
    }


    • package.json:确保将 types 属性设置为生成的 .d.ts 文件,该文件位于编译后的源代码旁边。


    {
    "name": "stop using d.ts",
    "version": "1.0.0",
    "main": "dist/index.js",
    "types": "dist/index.d.ts",
    "scripts": {
    "build": "tsc"
    }
    }



    结论


    .d.ts 文件中可以做到的一切,都可以在 .ts 文件中完成。


    .ts 文件中使用 declare global {} 语法时,花括号内的内容将被视为全局环境声明,这本质上就是 .d.ts 文件的工作方式。


    所以即使不使用.d.ts文件,也可以拥有全局可访问的类型。.ts文件在功能上既可以包含代码实现,又可以包含类型声明,而且还能实现全局的类型声明,从而在开发过程中可以更加方便地管理和使用类型,避免了在.d.ts文件和.ts文件之间进行复杂的协调和组织,提高了开发效率和开发体验。




    另外需要注意的是,在大多数项目中,开发者会在 TypeScript 的配置文件(tsconfig.json)中将 skipLibCheck 选项设置为 true。skipLibCheck 的作用是跳过对库文件(包括 .d.ts 文件)的类型检查。当设置为 true 时,TypeScript 编译器不会对这些库文件进行严格的类型检查,从而加快编译速度。但这也会影响项目中自己编写的 .d.ts 文件。这意味着,即使 .d.ts 文件中定义的类型存在错误,TypeScript 编译器也不会报错,从而失去了类型安全性的保障。


    而我们直接使用 .ts 文件,就不会有这个问题了,同事手动编写 .d.ts 文件,也会更加安全和高效。




    因此,.d.ts 文件确实没有必要编写。在 99% 的情况下,.ts 文件更适合使用,可以改善开发体验,并降低库代码中出现类型错误的可能性。


    怎么样??你同意他的看法么?


    作者:叶知秋水
    来源:juejin.cn/post/7463817822474682418
    收起阅读 »

    我们都被困在系统里

    前言 Hi 你好,我是东东拿铁,一个正在探索个人IP的后端程序员。 2020年外卖最火热的时候,有一篇文章《外卖骑手,困在系统里》。 作为一个互联网从业人员,我之前从未有机会体会到,当每一个工作都要被时间和算法压榨时,我会是一种怎样的感受。 而最近的一段经历...
    继续阅读 »

    前言


    Hi 你好,我是东东拿铁,一个正在探索个人IP的后端程序员。


    2020年外卖最火热的时候,有一篇文章《外卖骑手,困在系统里》。



    作为一个互联网从业人员,我之前从未有机会体会到,当每一个工作都要被时间和算法压榨时,我会是一种怎样的感受。


    而最近的一段经历,我感觉也被困在系统里了。


    起因


    如果你是一个研发人员,免不了要值班、处理线上问题。当然这都很正常,每个系统都有bug或者咨询类的问题。


    由于我们面临的客户比较多,加上系统有一些易用性的问题或bug,提出来的问题不少。


    公司有一项政策,当客服人员提交工单之后,系统对每一个单子有超时时间,如果超出一定时间你还未提交,罚款50元。


    挺奇葩的,谁能保证1个小时就一定能排查出问题呢?


    于是就会有一个场景,如果赶上问题多,一下子来5、6个工单,恰巧遇到不容易排查的耽误时间的话,处理后面的工单,都面临着超时的压力。



    之前同事们对值班这件事,充满了怨言,大多都会吐槽几个点



    1. 系统bug太多了,又是刚刚某某需求改出来的问题

    2. 需求设计不合理,很多奇怪的操作导致了系统问题

    3. 客服太懒了,明明可以自己搜,非得提个工单问

    4. 基础设施差,平台不好用


    我不太爱吐槽,但当工单一下子来的太多的时候,我不由自主的陷入机械的处理问题中,压缩思考的时间,只求不要超时就好。


    明明系统有很多问题需要解决、流程也有很多可以优化,可当系统给到我们的压力越来越多时,我们便不去思考,陷入只有吐槽、怨言和避免罚款的状态。


    当陷入了系统的支配,只能被动接受,甚至有了一些怨言的时候,我意识到,这样的状态,是有问题的。


    被困住的打工人


    外卖员为什么不遵守交通规则呢?


    外卖小哥为了多赚钱、避免处罚,我之前也很不理解,为什么为了避免处罚,连自己的生命安全都可以置之不顾。



    但转念一想,我们虽然不用在马路上奔波,可受到“系统”的压力,可是一点也不比外卖员少。


    大家一定有过类似的经历:你骑车或者开车去上班,距离打卡时间所剩无几,你在迟到的边缘疯狂试探,可能多一个红绿灯,你就赶不上了,这时候你会不会狠踩几脚油门、闯一个黄灯,想要更快一点呢?


    但随着裁员、降本增效、各类指标的压力越来越大,我们被迫不停的内卷,不断压榨自己,才能满足职场要求越来越严格的“算法”,比如,每半年一次的绩效考核,月度或者季度的OKR、KPI,还有处理不完的线上问题、事故,充斥在我们的脑海里面。


    其实我们何尝不是“外卖员”呢?外卖员是为了不被扣钱,我们是为了年终奖、晋升罢了。


    所以回过头来看,其实我们早早的就被困在“系统”中了,为了满足系统的要求,我们不得不埋头苦干,甚至加班透支身体,作出很多非常短线思维的事情。


    但为什么,我之前从来没有过被困住的感觉,为什么我现在才回过神来,意识到这个问题呢?


    我想,大概是越简单的事情,你作出的反应就越快、越激烈。而越复杂、时间越长的事情,你作出的反应就越缓慢,甚至忽略掉。


    比如上班即将迟到的你,你会立刻意识到,迟到可能会受到处罚。但是年终评估你的绩效目标时,你或许只有在最后的几个月才会意识到,某某事情没完成,年终奖或许要少几个月而感到着急。


    积极主动


    最近正好在读《高效能人士的七个习惯》,其中第一个习惯就是积极主动


    书中说到:人性的本质是主动而非被动的,人类不仅能针对特定环境选择回应方式,更能主动创造有利的环境。


    我们面对的问题可以分为三类:



    • 可直接控制的(问题与自身的行为有关)

    • 可间接控制的(问题与他人的行为有关)

    • 无法控制的(我们无能为力的问题,例如我们的过去或现实的环境)


    对于这三类问题,积极主动的话,应该如何加以解决呢。


    可直接控制的问题


    针对可直接控制的问题,可以通过培养正确习惯来解决。


    从程序员角度来看,线上bug多,可以在开发前进行技术设计,上线前进行代码CR,自动化测试,帮助自己避免低级的问题。


    面对处理工单时咨询量特别多的问题,随手整理个文档出来,放到大家都可以看到的地方。


    可间接控制的


    对于可间接控制的,我们可以通过改进施加影响的方法来解决。


    比如流程机制的不合理,你可以通过向上反馈的方式施加影响,提出自己的建议而不是吐槽。


    无法控制的


    对于无法控制的,我们要做的就是改变面部曲线,以微笑、真诚与平和来接受现实。


    虽然反馈问题的人或许能力参差不齐,导致工单量很多,但我们意识到这一点是无法避免的,不如一笑而过,这样才不至于被问题左右。


    说在最后


    好了,文章到这里就要结束了。


    最近由于值班的原因,陷入了一段时间的无效忙碌中,每一天都很累,几乎抽不出时间来思考,所以更新的频率也降下来了。


    但还好,及时的意识到问题,把最近的一点思考分享出来,希望我们每个人都不会被“系统”困住。


    欢迎你在评论区和我分享,也希望你点赞、评论、收藏,让我知道对你有所收获,这对我来说很重要。也欢迎你加我的wx:Ldhrlhy10,一起交流~


    本篇文章是第41篇原创文章,2024目标进度41/100,欢迎有趣的你,关注我。


    作者:东东拿铁
    来源:juejin.cn/post/7385098943942656054
    收起阅读 »

    再见Typora,这款大小不到3M的Markdown编辑器,满足你的所有幻想!

    Typora 是一款广受欢迎的 Markdown 编辑器,以其所见即所得的编辑模式和优雅的界面而闻名,长期以来是许多 Markdown 用户的首选。然而,从 2021 年起,Typora 不再免费,采用一次性付费授权模式。虽然费用不高,但对于轻量使用者或预算有...
    继续阅读 »

    Typora 是一款广受欢迎的 Markdown 编辑器,以其所见即所得的编辑模式和优雅的界面而闻名,长期以来是许多 Markdown 用户的首选。然而,从 2021 年起,Typora 不再免费,采用一次性付费授权模式。虽然费用不高,但对于轻量使用者或预算有限的用户可能并不友好。



    今天来推荐一款开源替代品,一款更加轻量化、注重隐私且完全免费的 Markdown 编辑器,专为 macOS 用户开发。


    项目介绍


    MarkEdit 是一款轻量级且高效的 Markdown 编辑器,专为 macOS 用户设计,安装包大小不到 3 MB。它以简洁的设计和流畅的性能,成为技术写作、笔记记录、博客创作以及项目文档编辑的理想工具。无论是编写技术文档、撰写博客文章,还是编辑 README 文件,MarkEdit 都能以快速响应和便捷操作帮助用户专注于内容创作。


    图片


    根据官方介绍,MarkEdit 免费的原因如下:



    MarkEdit 是完全免费和开源的,没有任何广告或其他服务。我们之所以发布它,是因为我们喜欢它,我们不期望从中获得任何收入。



    功能特性


    MarkEdit 的核心功能围绕 Markdown 写作展开,注重实用与高效,以下是其主要特性:



    • 实时语法高亮:清晰呈现 Markdown 的结构,让文档层次分明。

    • 多种主题:提供不同的配色方案,总有一种适合你。

    • 分屏实时预览:支持所见即所得的写作体验,左侧编辑,右侧实时渲染。

    • 文件树视图:适合多文件项目管理,方便在项目间快速切换。

    • 文档导出:支持将 Markdown 文件导出为 PDF 或 HTML 格式,方便分享和发布。

    • CodeMirror 插件支持:通过插件扩展功能,满足更多 Markdown 使用需求。

    • ......


    MarkEdit 的特点让它能胜任多种写作场合:



    • 技术文档:帮助开发者快速记录项目相关文档。

    • 博客创作:支持实时预览,让博客排版更直观。

    • 个人笔记:轻量且启动迅速,适合日常记录。

    • 项目文档:文件管理功能让多文件项目的编辑更加高效。


    效果展示


    多种主题风格,总有一种适合你:




    实时预览,让博客排版更直观:



    设置界面,清晰直观:



    安装方法


    方法 1:安装包下载


    找到 MarkEdit 的最新版本安装包下载使用即可,地址:github.com/MarkEdit-ap…


    方法 2:通过 Homebrew


    在终端中运行相关命令即可完成安装。


    brew install markedit

    注意:MarkEdit 支持 macOS Sonoma 和 macOS Sequoia, 历史兼容版本包括 macOS 12 和 macOS 13。


    总结


    MarkEdit 是一款专注于 Markdown 写作的 macOS 原生编辑器,以简洁、高效、隐私友好为核心设计理念。无论是日常写作还是处理复杂文档,它都能提供流畅的体验和强大的功能。对于追求高效写作的 macOS 用户来说,MarkEdit 是一个不可多得的优秀工具。


    项目地址:github.com/MarkEdit-ap…


    作者:Github掘金计划
    来源:juejin.cn/post/7456685819047919651
    收起阅读 »

    前端适配:你一般用哪种方案?

    web
    前言 最近在公司改bug,突然发现上一个前端留下的毛病不少,页面存在各种适配问题,为此甲方爸爸时常提出宝贵意见! 你的页面是不是时常是这样: 侧边栏未收缩时: 收缩后: 这样(缩小挤成一坨): 又或是这样: 那么废话不多说,今天由我不是程序猿kk为大家...
    继续阅读 »

    前言


    最近在公司改bug,突然发现上一个前端留下的毛病不少,页面存在各种适配问题,为此甲方爸爸时常提出宝贵意见!


    你的页面是不是时常是这样:


    侧边栏未收缩时:
    image.png


    收缩后:


    image.png


    这样(缩小挤成一坨):


    image.png


    又或是这样:


    image.png


    那么废话不多说,今天由我不是程序猿kk为大家讲解一些前端必备知识:适配工作。


    流式布局


    学会利用相对单位(例如百分比,vh或是vw),而不是只会用px一类固定单位设计布局,前言中提到的收缩后多出一大块空白,就是由于写死了宽度,例如1000px或是89vw,那么当侧边栏进行收缩,右边内容宽度还是只有89个vw,因此我们可以将其更改为100%,这样不论侧边栏是否收缩,内容都会占满屏幕的全部。


    .map {
    width: 100%;
    height: 90vh;
    position: relative;
    }

    image.png


    image.png


    rem和第三方插件


    什么是rem


    rem与em不同,rem会根据html的根节点字体大小进行变换,例如1rem就是一个字体大小那么大,比如根大小font size为12px,那么1rem即12px,大家可以在网上寻找单位换算工具进行换算(从设计稿的px到rem)或是下载相关插件例如gulp-px3rem,这样在不同分辨率,不同缩放比的电脑下都能够轻松应对了。


    使用


    第三方插件,例如做移动端适配的flexible.js,lib-flexible库,其核心原理就是rem,我们需要做的就是根据不同屏幕计算出不同的fontsize,而页面中元素都是用rem做单位,据此实现了自适应


    源码:


    ;(function(win, lib) {
    var doc = win.document;
    var docEl = doc.documentElement;
    var metaEl = doc.querySelector('meta[name="viewport"]');
    var flexibleEl = doc.querySelector('meta[name="flexible"]');
    var dpr = 0;
    var scale = 0;
    var tid;
    var flexible = lib.flexible || (lib.flexible = {});

    if (metaEl) {
    console.warn('将根据已有的meta标签来设置缩放比例');
    var match = metaEl.getAttribute('content').match(/initial-scale=([d.]+)/);
    if (match) {
    scale = parseFloat(match[1]);
    dpr = parseInt(1 / scale);
    }
    } else if (flexibleEl) {
    var content = flexibleEl.getAttribute('content');
    if (content) {
    var initialDpr = content.match(/initial-dpr=([d.]+)/);
    var maximumDpr = content.match(/maximum-dpr=([d.]+)/);
    if (initialDpr) {
    dpr = parseFloat(initialDpr[1]);
    scale = parseFloat((1 / dpr).toFixed(2));
    }
    if (maximumDpr) {
    dpr = parseFloat(maximumDpr[1]);
    scale = parseFloat((1 / dpr).toFixed(2));
    }
    }
    }

    if (!dpr && !scale) {
    var isAndroid = win.navigator.appVersion.match(/android/gi);
    var isIPhone = win.navigator.appVersion.match(/iphone/gi);
    var devicePixelRatio = win.devicePixelRatio;
    if (isIPhone) {
    // iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案
    if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
    dpr = 3;
    } else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
    dpr = 2;
    } else {
    dpr = 1;
    }
    } else {
    // 其他设备下,仍旧使用1倍的方案
    dpr = 1;
    }
    scale = 1 / dpr;
    }

    docEl.setAttribute('data-dpr', dpr);
    if (!metaEl) {
    metaEl = doc.createElement('meta');
    metaEl.setAttribute('name', 'viewport');
    metaEl.setAttribute('content', 'initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
    if (docEl.firstElementChild) {
    docEl.firstElementChild.appendChild(metaEl);
    } else {
    var wrap = doc.createElement('div');
    wrap.appendChild(metaEl);
    doc.write(wrap.innerHTML);
    }
    }

    function refreshRem(){
    var width = docEl.getBoundingClientRect().width;
    if (width / dpr > 540) {
    width = 540 * dpr;
    }
    var rem = width / 10;
    docEl.style.fontSize = rem + 'px';
    flexible.rem = win.rem = rem;
    }

    win.addEventListener('resize', function() {
    clearTimeout(tid);
    tid = setTimeout(refreshRem, 300);
    }, false);
    win.addEventListener('pageshow', function(e) {
    if (e.persisted) {
    clearTimeout(tid);
    tid = setTimeout(refreshRem, 300);
    }
    }, false);

    if (doc.readyState === 'complete') {
    doc.body.style.fontSize = 12 * dpr + 'px';
    } else {
    doc.addEventListener('DOMContentLoaded', function(e) {
    doc.body.style.fontSize = 12 * dpr + 'px';
    }, false);
    }


    refreshRem();

    flexible.dpr = win.dpr = dpr;
    flexible.refreshRem = refreshRem;
    flexible.rem2px = function(d) {
    var val = parseFloat(d) * this.rem;
    if (typeof d === 'string' && d.match(/rem$/)) {
    val += 'px';
    }
    return val;
    }
    flexible.px2rem = function(d) {
    var val = parseFloat(d) / this.rem;
    if (typeof d === 'string' && d.match(/px$/)) {
    val += 'rem';
    }
    return val;
    }

    })(window, window['lib'] || (window['lib'] = {}));

    大家如果对相关原理感兴趣,可以阅读:flexible.js如何实现rem自适应-前端开发博客


    在实际开发中应用场景不同效果不同,因此不能写死px。


    在PC端适配我们可以自动转换rem适配方案(postcss-pxtorem、amfe-flexible),这里以vue3+vite为例子。事实上amfe-flexible是lib-flexible的升级版。


    注意: 行内样式px不会转化为rem


    npm install postcss postcss-pxtorem --save-dev  // 我试过了反正我报错了,版本太高 大家可以指定5.1.1
    npm install postcss-pxtorem@^5.1.1
    npm i amfe-flexible --save

    记得在main.js中引入amfe-flexible


    import "amfe-flexible"

    相关配置


    image.png


    媒体查询


    通过查询不同的宽度来执行不同的css代码,最终以达到界面的配置。


    在 CSS 中使用 @media 查询来检测屏幕宽度。当屏幕宽度小于 1024px 时,增加 margin-top 以向下移动表格。



    .responsive-table {
    transition: margin-top 0.3s; /* 添加过渡效果 */
    }

    @media (max-width: 1024px) {
    .responsive-table {
    margin-top: 200px; /* 向下移动的距离 */
    }
    }

    弹性布局


    创建一个响应式的卡片布局,当屏幕宽度减小时,卡片会自动换行。


    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Flexbox Example</title>
    <style>
    body {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-wrap: wrap;
    margin: 0;
    height: 100vh;
    background-color: #f0f0f0;
    }

    .card-container {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    width: 90%;
    }

    .card {
    background-color: white;
    border: 1px solid #ccc;
    border-radius: 5px;
    padding: 20px;
    margin: 10px;
    flex: 1 1 300px; /* 基于300px,允许增长和收缩 */
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
    transition: transform 0.3s;
    }

    .card:hover {
    transform: translateY(-5px);
    }
    </style>
    </head>
    <body>
    <div class="card-container">
    <div class="card">Card 1</div>
    <div class="card">Card 2</div>
    <div class="card">Card 3</div>
    <div class="card">Card 4</div>
    <div class="card">Card 5</div>
    </div>
    </body>
    </html>

    小结


    还是多提一嘴,应该不会有小伙伴把字体大小的单位也用rem吧?


    作者:zykk
    来源:juejin.cn/post/7431999862919446539
    收起阅读 »

    独立开发:家人不支持怎么办?

    大家好,我是农村程序员,独立开发者,前端之虎陈随易。 这是我的个人网站:chensuiyi.me,欢迎一起交朋友~ 有很多人跟我聊到过这个问题:做独立开发,家人不支持怎么办?。 在我交流沟通下,最终发现,这些人都走进了一个误区:独立开发者 等于 我要辞职全...
    继续阅读 »

    大家好,我是农村程序员,独立开发者,前端之虎陈随易。


    个人网站


    这是我的个人网站:chensuiyi.me,欢迎一起交朋友~




    有很多人跟我聊到过这个问题:做独立开发,家人不支持怎么办?


    在我交流沟通下,最终发现,这些人都走进了一个误区:独立开发者 等于 我要辞职全职做独立开发


    请看我对独立开发者的分类:



    1. 业余独立开发。特点:上班 + 下班的业余时间独立开发

    2. 兼职独立开发。特点:不上班 + 没有充足的时间做独立开发

    3. 全职独立开发。特点:不上班 + 有充足的时间做独立开发

    4. 混合独立开发。特点:上班+兼职+没有充足的时间做独立开发


    现在是不是一目了然了。


    你可以根据自己当下的情况,特点,去选择做哪一种 独立开发


    我们目前所看到的 全职独立开发,只有极少数人可以做到。


    这对于个人的内在要求,包括自律,坚持,执行力,产品力,都有着较高的要求。


    同时呢,来自家人的态度和压力,也是 全职独立开发 的重要条件。


    不要一开始,啥独立开发的经验都没有,就想做 全职独立开发


    那么当你可以 理性地选择 适合自己当下情况的的独立开发方式后,你会发现,家人还会不支持吗?至少不会那么反对了。


    所以这个问题的答案就是这么简单,只要看了我对独立开发的分类,你就明白了。


    独立开发,本就是一个人的战斗,不要妄想这家人会支持你,他们最大的支持就是不反对。


    我们遇到这样的问题时,不要觉得家人怎么怎么样,自己受到了多大的委屈和不理解一样。


    他们的想法,是完全没有问题的。


    人是社会动物,必然要考虑当下的生存问题,这是十分合理且正常的。


    那么,如果上面的问题解决后,家人还是不支持,怎么办呢?


    也很简单啊,自己偷摸摸继续折腾呗,难道一定要得到家人的支持,才能做独立开发吗?


    《明朝那些事》 的作者,当年明月,赚了几千万家人才知道呢。


    当然,我不是说你,也不是说我自己,可以赚几千万,我们可以定目标,但不能做梦。


    总而言之就是说,做独立开发,要做好一个人长期战斗的准备。


    因为你很有可能,很多年都无法比较稳定地每个月赚 5000 块钱,独立开发远没有我们想象的那么轻松。


    如果你实在没有时间,没有干劲,没有激情做独立开发,那么不如其他方向,说不定能获得更好的回报。


    独立开发是一个美好的梦,不稳定,也容易破碎。


    那么我为什么一直在坚持做独立开发呢?因为我想让美梦成真。


    作者:农村程序员陈随易
    来源:juejin.cn/post/7434366864866099234
    收起阅读 »

    制作一个页面时,需要兼容PC端和手机端,你是要分别做两个页面还是只做一个页面自适应?为什么?说说你的理由

    web
    在制作一个页面时,如何兼容PC端和手机端是一个重要的设计决策。这个决策通常有两个选择:分别制作两个页面,或者只制作一个自适应页面。以下是我选择后者的理由,以及如何实现自适应设计的相关内容。 选择自适应设计的理由 提高开发效率 制作一个自适应页面可以显著提高开...
    继续阅读 »

    在制作一个页面时,如何兼容PC端和手机端是一个重要的设计决策。这个决策通常有两个选择:分别制作两个页面,或者只制作一个自适应页面。以下是我选择后者的理由,以及如何实现自适应设计的相关内容。


    选择自适应设计的理由



    1. 提高开发效率

      制作一个自适应页面可以显著提高开发效率。开发者只需编写一次代码,就可以在不同设备上展示同样的内容,而不必为每个设备维护多个代码库。这意味着在后续的更新和维护中,相比于维护两个独立的页面,自适应页面的工作量会大幅减少。

    2. 一致的用户体验

      用户在不同设备上访问同一网站时,能够获得一致的用户体验是极其重要的。自适应设计可以确保在不同屏幕尺寸和分辨率下,用户看到的内容和布局尽可能一致,避免用户在不同设备上获取不同信息的困惑。

    3. SEO优化

      使用单一的自适应页面有助于SEO优化。搜索引擎更倾向于索引和排名内容一致的网站,而不是将相同内容分散在多个页面上。使用自适应设计可以集中流量,提高网站在搜索引擎中的权重。

    4. 成本效益

      维护两个独立的页面不仅需要更多的时间和人力成本,还可能导致代码重复,增加出错的可能性。自适应设计能够通过一个代码库降低开发和维护成本。

    5. 响应式设计的灵活性

      现代的 CSS 和 JavaScript 技术(例如 Flexbox 和 CSS Grid)使得创建自适应布局变得更加简单和灵活。媒体查询(Media Queries)可以帮助开发者根据设备的特性(如宽度、高度、分辨率)调整布局和样式,从而提供最佳的用户体验。


    如何实现自适应设计



    1. 使用媒体查询

      媒体查询是实现自适应设计的核心。它允许你根据设备的屏幕尺寸和特性应用不同的样式。例如:


      /* 默认样式 */
      .container {
      width: 100%;
      padding: 20px;
      }

      /* 针对手机的样式 */
      @media (max-width: 600px) {
      .container {
      padding: 10px;
      }
      }

      /* 针对平板的样式 */
      @media (min-width: 601px) and (max-width: 900px) {
      .container {
      padding: 15px;
      }
      }


    2. 使用流式布局

      使用流式布局可以使元素在不同的屏幕上根据可用空间自动调整大小。例如,使用百分比而不是固定像素值来设置宽度:


      .box {
      width: 50%; /* 宽度为父容器的一半 */
      height: auto; /* 高度自动适应内容 */
      }


    3. 灵活的图片和媒体

      为了确保图片和视频在不同设备上显示良好,使用 max-width: 100% 来确保媒体不会超出其容器的宽度:


      img {
      max-width: 100%;
      height: auto; /* 保持图片的纵横比 */
      }


    4. 测试和优化

      在开发完成后,确保在不同设备和浏览器上进行测试。可以使用 Chrome 的开发者工具模拟不同的设备,以检查自适应设计的效果。同时,收集用户反馈并进行必要的优化。


    总结


    在制作兼容PC端和手机端的页面时,选择制作一个自适应页面是更为高效和经济的方案。它不仅能提高开发效率和维护成本,还能提供一致的用户体验,增强SEO效果。此外,现代的CSS技术使得实现自适应设计变得更加简单。因此,采用自适应设计是现代Web开发的最佳实践之一。


    作者:Riesenzahn
    来源:juejin.cn/post/7476010111887949861
    收起阅读 »

    别让这6个UI设计雷区毁了你的APP!

    web
    一款成功的APP不仅仅取决于其功能性,更取决于用户体验,这其中,UI设计又至关重要。优秀的UI设计能够为用户带来直观、愉悦的交互体验,甚至让用户“一见钟情”,从而大大提高产品吸引力。 然而,有很多设计师在追求创新与美观的同时,往往会不经意间“踩雷”,忽略了一些...
    继续阅读 »

    一款成功的APP不仅仅取决于其功能性,更取决于用户体验,这其中,UI设计又至关重要。优秀的UI设计能够为用户带来直观、愉悦的交互体验,甚至让用户“一见钟情”,从而大大提高产品吸引力。


    然而,有很多设计师在追求创新与美观的同时,往往会不经意间“踩雷”,忽略了一些普通用户更在意的问题和痛点。本文,笔者将谈谈UI设计中的常见误区,同时展示了优秀的UI设计例子,还分享一些行业内推荐的设计工具,希望能助你在设计的道路上更加得心应手~


    UI设计常见误区


    1、过度设计


    设计大师 Dieter Rams 说过一句话:“好的设计是尽可能简单的设计。”


    不过,说起来容易做起来难。产品经理和设计师总是不自觉地让产品过于复杂,今天想在产品中再添加一个功能,明天想在页面上再增加一个元素。


    尤其是新手设计师,总希望通过增加视觉元素、动画效果和复杂的交互来提升用户体验。然而,这种做法往往适得其反,一个简洁、清晰且直观的界面一定比一个装饰过多、功能复杂的界面更能吸引用户。设计师应该专注于APP的核心功能,避免不必要的元素,确保设计美观又实用。



    简约风接单APP界面


    http://www.mockplus.cn/example/rp/…


    2、忽视用户反馈


    有时候,设计师可能过于相信自己的设计,忽略了用户的意见和反馈。这种情况下,即使设计再怎么创新或美观,如果不符合用户的实际需求和使用习惯,也难以获得用户的认可。


    毕竟,UI设计不是艺术创作,用户反馈是设计迭代过程中的宝贵资源,它可以帮助设计师了解用户的真实体验,了解设计的不足,从而进行针对性的优化改进。



    FARFETCH APP界面


    http://www.mockplus.cn/example/rp/…


    3、色彩搭配不合适


    色彩搭配不合适是UI设计中一个常见的误区。有的设计师会自动倾向于选择那些自己喜欢的颜色,而不是基于用户的偏好和产品特征、背景等来选择颜色。有时,颜色的过度使用或不恰当的搭配会分散用户的注意力,甚至造成视觉疲劳。


    另外,为了让用户能看清UI设计的各个方面,需要有足够的对比度。这就是为什么黑色文字配上白色背景效果那么好 —— 文字非常清晰,易于理解。



    插画风APP界面


    http://www.mockplus.cn/example/rp/…


    4、忽略可访问性


    对一个APP来说,所有用户,无论是有视觉障碍的人还是老年用户,都应该能够轻松地使用APP。设计如果不考虑这些用户群体的需要,会无意中排除一大部分潜在用户。


    为了提高APP的可访问性,设计师应该考虑到字体大小、颜色对比度、语音读出功能等,确保所有用户都能舒适地使用。



    社交类APP界面


    http://www.mockplus.cn/example/rp/…


    5、布局空滤不全面


    有的产品界面过多,布局考虑不够全面几乎是常常发生的事。布局不合适有很多种情况,比如元素过于密集、排版杂乱、组件没有对齐、文本大小间距不合适等等。这种情况会导致信息展示不清晰或用户操作不便,非常影响用户的使用体验和效率。


    一个美观舒适的布局应该合理利用空间,确保信息的层次清晰,充分考虑元素的大小和间距,让界面既清晰又易于操作,用户可以轻松地找到他们关心的内容。


    想要避免这一误区的,在设计初期就需要考虑用户的需求和行为模式,不断迭代优化布局设计,找到最佳的解决方案。



    加密货币钱包APP界面


    http://www.mockplus.cn/example/rp/…


    了解了上述UI设计的常见误区后,接下来是选择合适的设计工具来提升设计效率和质量。目前,市面上有很多优秀的UI设计工具,但以下几款因其强大的功能和良好的用户体验受到设计师的广泛推荐,一起来看看!


    UI工具推荐


    1、摹客 DT


    摹客DT(http://www.mockplus.cn/dt)是一款国内很多UI设计师都爱用的工具,它提供了一整套完整的专业矢量编辑功能,丰富的图层样式选项,可以轻松创建高质量的App设计,并且还能在线实时协同,搞定团队资产管理难题不是问题。



    主要功能点和亮点:


    1)所有功能完全免费(参与它们的小活动还能永久免费使用),包含所有高级功能、导出能力等;


    2)颜色、文本样式、图层样式都可以保存为资源,复用到其他设计元素;


    3)支持导出SVG、JPG、PNG、webP、PDF格式文件,适配不同使用场景;


    4)具有完善的团队协作和项目管理能力,改变传统设计流程,降低成本,提升效率。


    **价格:**完全免费


    **学习难度:**简单,新手上手无难度


    **使用环境:**Web/客户端/Android/iOS


    **推荐理由:**摹客DT,是一款更适合中国设计师的UI/UX设计工具。与一些老牌设计工具(Photoshop、XD等),它的学习门槛更低,上手更简单,社区和实时客户支持更迅速友好;作为Web端工具,摹客DT支持多人实时编辑,大大提高了团队设计效率。


    推荐评级:⭐⭐⭐⭐⭐


    2、Figma


    Figma(http://www.figma.com/)是现在最流行的UI设…



    主要功能点及亮点:


    1)丰富的功能和工具:提供了全面的工具和功能来设计界面,包括矢量图形工具、网格和布局指导等。


    2)设计组件和样式库:支持创建可复用的设计组件和样式库,节省时间和工作量。


    3)插件生态系统:有丰富的插件生态系统,可以通过插件扩展功能,增加额外的设计工具和集成,以满足特定需求。


    **价格:**提供免费版和付费版(12美元/月起)


    **学习难度:**对新手相对友好,操作简单。


    **使用环境:**Figma是基于Web的平台,通过浏览器即可使用。


    推荐理由:


    Figma设计功能强大,提供了丰富的插件和集成,还有有一个庞大的社区交流学习,是一款非常适合团队协作的工具。遗憾的是,Figma作为一款国外设计工具,在国内使用还是存在一定的劣势,比如网络访问限制、数据隐私和安全、本地化支持等,所以只能给到四星。


    推荐评级:⭐⭐⭐


    3、Sketch


    Sketch(http://www.sketch.com/)是一款专业的UI/U…



    主要功能及亮点:



    1. 1)矢量编辑:直观的矢量编辑工具和可编辑的布尔运算,支持形状绘制、路径编辑、填充和描边等常见的矢量操作,使用户能够轻松创建和编辑各种设计元素。

    2. 2)设计规范库:设计师可以在sketch中把常用的颜色、图层样式存为规范,也可以将已经设计好的矢量图标存到规范库,在整个设计中复用它们,提高工作效率。


    3)Sketch支持众多插件和第三方资源,使得其生态系统非常丰富,能够更好地满足设计师的需求。


    **价格:**标准订阅 10//人(按年付费),10/月/人(按年付费),12/月/人(按月付费)


    **使用环境:**macOS操作系统


    推荐理由:


    Sketch被广泛应用于移动应用、网页设计和用户界面设计等领域。它友好的界面、丰富的设计资源使设计师们能够快速创建出符合良好用户体验和视觉效果要求的设计。可惜的是,它只支持macOS系统,限制了部分用户使用。


    **推荐评级:**⭐⭐⭐⭐


    4、Adobe XD


    Adobe XD(helpx.adobe.com/support/xd.…



    主要功能及亮点:


    1)丰富的设计和布局功能:设计师可以借助这些功能,轻松创建响应式设计,预览在不同设备上的效果,确保设计的一致性和灵活性。


    2)丰富的交互动画和过渡效果:可以创建十分流畅的交互体验,用来更好地展示APP或网站的交互逻辑。


    3)共享和协作:支持团队协作,用户可以轻松地共享设计文件,协同编辑,以及通过实时协作功能进行设计评审。


    **价格:**提供免费试用,提供付费订阅 $9.99/月


    **学习难度:**中


    **使用环境:**Windows、macOS


    **推荐理由:**Adobe XD与Adobe Creative Cloud紧密集成,十分方便同时使用Adobe其他软件的用户。不过有个问题是,Adobe xd已经官宣停止更新,只向老用户提供服务了,如果是之前没有使用Adobe XD的用户,最好选择其他产品。


    **推荐评级:**⭐️⭐️⭐️


    五、Principle


    Principle是一款专门用于UI/UX设计的软件,它的设计理念是想要让设计师能够轻松创建出高质量、富有吸引力的产品界面效果。



    主要功能及亮点:


    1)高级动画控制:软件支持创建复杂的动画效果,包括过渡、转换和多状态动画。这对于展示复杂的交互过程和动态效果非常有用。


    2)实时预览:Principle允许设计师实时预览他们的工作成果,这意味着可以即时查看和测试动画效果,确保设计的交互体验符合预期。


    3)组件和重复使用:设计师可以创建可重复使用的组件,这大大提高了工作效率,特别是在处理具有相似元素或布局的多个界面时。


    价格:$129


    **学习难度:**中


    **使用环境:**MacOS


    推荐理由:


    设计师通过Principle可以快速地制作出具有丰富交互的产品界面设计,并且它能和Sketch进行无缝链接,可以快速导入你在Sketch里已经做好的图,


    推荐评级:⭐️⭐️⭐️⭐️


    好的UI设计不仅仅是视觉上的享受,更是能让用户能够轻松、愉快地使用APP。避免上述UI设计误区,选择合适的设计工具,可以帮助我们设计出既美观又实用的产品。


    希望本文章能为UI设计师/准UI设计师们有所帮助,创作出更多优秀的设计作品~


    看到这里的读者朋友有福啦!本人吐血搜集整理,全网最全产品设计学习资料!


    只要花1分钟填写**问卷**就能免费领取以下超值礼包:


    1、产品经理必读的100本书 包含:产品思维、大厂案例、技能提升、数据分析、项目管理等各类经典书籍,从产品入门到精通一网打尽! 2、UI/UX设计师必读的115本书 包含:UI/UX设计、平面设计、版式设计、设计心理学、设计思维等各类经典书籍,看完你就是设计大神! 3、30G互联网人知识礼包 包含:



    • 10GPM礼包,产品案例、大咖分享、行业报告等应有尽有

    • 10GUI/UE资源,优秀设计案例、资料包、源文件免费领

    • 5G运营资料包,超全产品、电商、新媒体、活动等运营技能

    • 5G职场/营销资料包,包含产品设计求职面试、营销增长等


    4、50G热门流行的AI学习大礼包


    包含:AI绘画、AIGC精选课程、AI职场实用教程等


    5、30G职场必备技能包


    包含:精选PPT模板、免费可商用字体包、各岗位能力模型、Excel学习资料、求职面试、升职加薪、职场写作和沟通等实用资源。


    礼包资源持续更新,互联网行业知识一网打尽!礼包领取地址:


    docs.qq.com/form/page/D…



    作者:摹客
    来源:juejin.cn/post/7356535808931627046
    收起阅读 »

    后端:没空,先自己 mock 去

    web
    前言后端开发忙,不给你接口?后端抱怨你在测试过程中,频繁的给脏数据?后端修个接口很慢没法测试?有了 mockjs ,这些问题将迎刃而解。不要 998,pnpm i 带回家!真这么丝滑?请看我的使用方式:当后端接口无法满足要求,且不能及时更改时。例如后端返回{ ...
    继续阅读 »

    前言

    后端开发忙,不给你接口?

    后端抱怨你在测试过程中,频繁的给脏数据?

    后端修个接口很慢没法测试?

    image.png

    有了 mockjs ,这些问题将迎刃而解。不要 998,pnpm i 带回家!

    真这么丝滑?

    请看我的使用方式:

    当后端接口无法满足要求,且不能及时更改时。例如后端返回

    {
    "err_no": 0,
    "err_msg": "success",
    "data": [
    {
    "comment_id": "7337487924836287242",
    "user_info": {
    "user_name": "陈陈陈_",
    }
    }
    ],
    }

    但我此时希望增加一个 user_type 来确定页面的展示。

    那我就直接起一个文件:user.js,把刚才的响应 copy 过来,并追加改动

    myMock('/api/v1/user', 'post', () => {
    return {
    "err_no": 0,
    "err_msg": "success",
    "data": [
    {
    "comment_id": "7337487924836287242",
    "user_info": {
    "user_name": "陈陈陈_",
    "user_type": "admin",
    }
    }
    ],
    }
    });

    如此一来,这个请求就被无缝替换为了我们的 mock,可以随便测试了。

    如何接入 mockjs

    有的同学就要问了,主播主播,你的 mockjs 确实很方便,怎么接入比较好呀。别急,我们一步一步来

    1. 安装 mockjs
    pnpm i mockjs

    如果是使用 ts 的同学,可能需要额外安装 type 类型包:@types/mockjs

    1. 新建一个 mock 文件夹,在 mock/index.ts 放入基本路径
    // 各种 mock 的文件,视条件而定,我这里有俩文件就引入了俩
    import './login/user.js';
    import './model/model.js';

    并且在你的项目入口 ts 中引入 mock/index.ts

    import './mock/index'; // 引入 mock 配置
    1. 导出一个 myMock 方法,并追加一个 baseUrl 方便直接联动你的 axios
    import { ENV_TEST } from '@/api/config/interceptor';
    import Mock from 'mockjs';

    export const myMock = (
    path: string,
    method: 'get' | 'post',
    callback: (options: any) => any
    ) => {
    Mock.mock(`${ENV_TEST}${path}`, method, callback);
    };

    如此一来,你就可以在 mock 文件夹下去搞了,比如:

    我想新增一个服务模块的各类接口的 mock,那么我就新增一个 service 文件夹,在其下增加一个 index.ts,并对对应路径进行 mock

    myMock('/api/v1/service', 'get', () => {
    return {
    code: 0,
    msg: 'hello service',
    data: null,
    };
    });

    另外,别忘了在 mock/index.ts 引入文件

    不显示在 network 中?

    需要说明的是,这样走 mock 是不会触发真正的请求的,相当于 xhr 直接被 mock 拦截了下来并给了你返回值。所以你无法在 network 中看到你的请求。

    这是个痛点,目前比较好的解决方案还是起一个单独的服务来 mock。但这样也就意味着,需要另起一个项目来单独做 mock,太不优雅了。

    有没有什么办法,既可以走上述简单的mock,又可以在需要的时候起一个服务来查看 network,并且不需要额外维护两套配置呢?

    有的兄弟,有的。

    import express from 'express';
    import bodyParser from 'body-parser';
    import Mock from 'mockjs';
    import './login/user.js';
    import './model/model.js';
    import { ENV_TEST } from './utils/index.js';

    const app = express();
    const port = 3010;

    // 使用中间件处理请求体和CORS
    app.use(bodyParser.json());

    // 设置CORS头部
    app.use(( _ , res, next) => {
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
    res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    next();
    });

    // 设置Mock路由的函数
    const setupMockRoutes = () => {
    const mockApis = Mock._mocked || {};

    // 遍历每个Mock API,并生成对应的路由
    Object.keys(mockApis).forEach((key) => {
    const { rurl, rtype, template } = mockApis[key];
    const route = rurl.replace(ENV_TEST, ''); // 去掉环境前缀

    // 根据请求类型(GET, POST, 等)设置路由
    app[rtype.toLowerCase()](route, (req, res) => {
    const data =
    typeof template === 'function' ? template(req.body || {}) : template;
    res.json(Mock.mock(data)); // 返回模拟数据
    });
    });
    };

    // 设置Mock API路由
    setupMockRoutes();

    // 启动服务器
    app.listen(port, () => {
    process.env.NODE_ENV = 'mock'; // 设置环境变量
    console.log(`Mock 服务已启动,访问地址: http://localhost:${port}`);
    });

    直接在 mock 文件夹下追加这个启动文件,当你需要看 network 的时候,将环境切换为 mock 环境即可。本质是利用了 Mock._mocked 可以拿到所有注册项,并用 express 起了一个后端服务响应这些注册项来实现的。

    在拥有了这个能力的基础上,我们就可以调整我们的命令

      "scripts": {
    "dev": "cross-env NODE_ENV=test vite",
    "mock": "cross-env NODE_ENV=mock vite & node ./src/mock/app.js"
    },

    顺便贴一下我的 env 配置:

    export const ENV_TEST = 'https://api-ai.com/fuxi';
    export const ENV_MOCK = 'http://localhost:3010/';

    let baseURL: string = ENV_TEST;

    console.log('目前环境为:' + process.env.NODE_ENV);
    switch (process.env.NODE_ENV) {
    case 'mock':
    baseURL = ENV_MOCK;
    break;
    case 'test':
    baseURL = ENV_TEST;
    break;
    case 'production':
    break;
    default:
    baseURL = ENV_TEST;
    break;
    }

    export { baseURL };

    这样一来,如果你需要看 network ,就 pnpm mock,如果不需要,就直接 pnpm dev,完全不需要其他心智负担。

    三个字:

    image.png

    参数相关

    具体的 api 可查阅:github.com/nuysoft/Moc… 相关的文章也非常多,就不展开说明了。

    如果这篇文章对你有帮助,不妨点个赞吧~


    作者:imoo
    来源:juejin.cn/post/7460091261762125865

    收起阅读 »

    前端哪有什么设计模式

    前言 常网IT源码上线啦! 本篇录入吊打面试官专栏,希望能祝君拿下Offer一臂之力,各位看官感兴趣可移步🚶。 有人说面试造火箭,进去拧螺丝;其实个人觉得问的问题是项目中涉及的点 || 热门的技术栈都是很好的面试体验,不要是旁门左道冷门的知识,实际上并不会用...
    继续阅读 »

    前言



    • 常网IT源码上线啦!

    • 本篇录入吊打面试官专栏,希望能祝君拿下Offer一臂之力,各位看官感兴趣可移步🚶。

    • 有人说面试造火箭,进去拧螺丝;其实个人觉得问的问题是项目中涉及的点 || 热门的技术栈都是很好的面试体验,不要是旁门左道冷门的知识,实际上并不会用到的。

    • 接下来想分享一些自己在项目中遇到的技术选型以及问题场景。





    你生命的前半辈子或许属于别人,活在别人的认为里。那把后半辈子还给你自己,去追随你内在的声音。



    1.jpg


    一、前言


    之前在讨论设计模式、算法的时候,一个后端组长冷嘲热讽的说:前端哪有什么设计模式、算法,就好像只有后端语言有一样,至今还记得那不屑的眼神。


    今天想起来,就随便列几个,给这位眼里前端无设计模式的人,睁眼看世界。


    二、观察者模式 (Observer Pattern)


    观察者模式的核心是当数据发生变化时,自动通知并更新相关的视图。在 Vue 中,这通过其响应式系统实现。


    Vue 2.x:Object.defineProperty


    在 Vue 2.x 中,响应式系统是通过 Object.defineProperty 实现的。每当访问某个对象的属性时,getter 会被触发;当设置属性时,setter 会触发,从而实现数据更新时视图的重新渲染。


    源码(简化版):


    function defineReactive(obj, key, val) {
    // 创建一个 dep 实例,用于收集依赖
    const dep = new Dep();

    Object.defineProperty(obj, key, {
    get() {
    // 当访问属性时,触发 getter,并把当前 watcher 依赖收集到 dep 中
    if (Dep.target) {
    dep.addDep(Dep.target);
    }
    return val;
    },
    set(newVal) {
    if (newVal !== val) {
    val = newVal;
    dep.notify(); // 数据更新时,通知所有依赖重新渲染
    }
    }
    });
    }


    • Dep :它管理依赖,addDep 用于添加依赖,notify 用于通知所有依赖更新。


    class Dep {
    constructor() {
    this.deps = [];
    }

    addDep(dep) {
    this.deps.push(dep);
    }

    notify() {
    this.deps.forEach(dep => dep.update());
    }
    }


    • 依赖收集:当 Vue 组件渲染时,会创建一个 watcher 对象,表示一个视图的更新需求。当视图渲染过程中访问数据时,getter 会触发,并将 watcher 添加到 dep 的依赖列表中。


    Vue 3.x:Proxy


    Vue 3.x 使用了 Proxy 来替代 Object.defineProperty,从而实现了更高效的响应式机制,支持深度代理。


    源码(简化版):


    function reactive(target) {
    const handler = {
    get(target, key) {
    // 依赖收集:当访问某个属性时,触发 getter,收集依赖
    track(target, key);
    return target[key];
    },
    set(target, key, value) {
    // 数据更新时,通知相关的视图更新
    target[key] = value;
    trigger(target, key);
    return true;
    }
    };

    return new Proxy(target, handler);
    }



    • track:收集依赖,确保只有相关组件更新。

    • trigger:当数据发生变化时,通知所有依赖重新渲染。


    三、发布/订阅模式 (Publish/Subscribe Pattern)


    发布/订阅模式通过中央事件总线(Event Bus)实现不同组件间的解耦,Vue 2.x 中,组件间的通信就是基于这种模式实现的。


    Vue 2.x:事件总线(Event Bus)


    事件总线就是一个中央的事件处理器,Vue 实例可以充当事件总线,用来处理不同组件之间的消息传递。


    // 创建一个 Vue 实例作为事件总线
    const EventBus = new Vue();

    // 组件 A 发布事件
    EventBus.$emit('message', 'Hello from A');

    // 组件 B 订阅事件
    EventBus.$on('message', (msg) => {
    console.log(msg); // 输出 'Hello from A'
    });


    • $emit:用于发布事件。

    • $on:用于订阅事件。

    • $off:用于取消订阅事件。


    四、工厂模式 (Factory Pattern)


    工厂模式通过一个函数生成对象或实例,Vue 的组件化机制和动态组件加载就是通过工厂模式来实现的。


    Vue 的 render 函数和 functional 组件支持动态生成组件实例。例如,functional 组件本质上是一个工厂函数,通过给定的 props 返回一个 VNode。


    Vue.component('dynamic-component', {
    functional: true,
    render(h, context) {
    // 工厂模式:根据传入的 props 创建不同的 VNode
    return h(context.props.type);
    }
    });


    • functional 组件:它没有实例,所有的逻辑都是在 render 函数中处理,返回的 VNode 就是组件的“产物”。


    五、单例模式 (Singleton Pattern)


    单例模式确保某个类只有一个实例,Vue 实例就是全局唯一的。


    在 Vue 中,全局的 Vue 构造函数本身就是一个单例对象,通常只会创建一个 Vue 实例,用于管理应用的生命周期和全局配置。


    const app = new Vue({
    data: {
    message: 'Hello, Vue!'
    }
    });


    • 单例保证:整个应用只有一个 Vue 实例,所有全局的配置(如 Vue.config)都是共享的。


    六、模板方法模式 (Template Method Pattern)


    模板方法模式定义了一个操作中的算法框架,而将一些步骤延迟到子类中。Vue 的生命周期钩子就是一个模板方法模式的实现。


    Vue 定义了一系列生命周期钩子(如 createdmountedupdated 等),它们实现了组件从创建到销毁的完整过程。开发者可以在这些钩子中插入自定义逻辑。


    Vue.component('my-component', {
    data() {
    return {
    message: 'Hello, 泽!'
    };
    },
    created() {
    console.log('Component created');
    },
    mounted() {
    console.log('Component mounted');
    },
    template: '<div>{{ message }}</div>'
    });

    Vue 组件的生命周期钩子实现了模板方法模式的核心思想,开发者可以根据需要重写生命周期钩子,而 Vue 保证生命周期的流程和框架。


    七、策略模式 (Strategy Pattern)


    策略模式通过定义一系列算法,将它们封装起来,使它们可以相互替换。Vue 的 计算属性(computed)方法(methods) 可以看作是策略模式的应用。


    计算属性允许我们定义动态的属性,其值是基于其他属性的计算结果。Vue 会根据依赖关系缓存计算结果,只有在依赖的属性发生变化时,计算属性才会重新计算。


    new Vue({
    data() {
    return {
    num1: 10,
    num2: 20
    };
    },
    computed: {
    sum() {
    return this.num1 + this.num2;
    }
    }
    });

    八、装饰器模式 (Decorator Pattern)


    装饰器模式允许动态地给对象添加功能,而无需改变其结构。在 Vue 中,指令就是一种装饰器模式的应用,它通过指令来动态地改变元素的行为。


    <div v-bind:class="className"></div>
    <div v-if="isVisible">谁的疯太谍</div>

    这些指令动态地修改 DOM 元素的行为,类似于装饰器在不修改对象结构的情况下,动态地增强其功能。


    九、代理模式 (Proxy Pattern)


    代理模式通过创建一个代理对象来控制对目标对象的访问。在 Vue 3.x 中,响应式系统就是通过 Proxy 来代理对象的访问。


    vue3


    const state = reactive({
    count: 0
    });

    state.count++; // 会触发依赖更新

    reactive:使用 Proxy 对对象进行代理,当对象的属性被访问或修改时,都会触发代理器的 get 和 set 操作。


    function reactive(target) {
    const handler = {
    get(target, key) {
    // 依赖收集:当访问某个属性时,触发 getter,收集依赖
    track(target, key);
    return target[key];
    },
    set(target, key, value) {
    // 数据更新时,触发依赖更新
    target[key] = value;
    trigger(target, key);
    return true;
    }
    };

    return new Proxy(target, handler);
    }


    • track:当读取目标对象的属性时,收集依赖,这通常涉及到将当前的 watcher 加入到依赖列表中。

    • trigger:当对象的属性发生改变时,通知所有相关的依赖(如组件)更新。


    这个 Proxy 机制使得 Vue 可以动态地观察和更新对象的变化,比 Object.defineProperty 更具灵活性。


    十、适配器模式 (Adapter Pattern)


    适配器模式用于将一个类的接口转换成客户端期望的另一个接口,使得原本不兼容的接口可以一起工作。Vue 的插槽(Slots)和组件的跨平台支持某种程度上借用了适配器模式的思想。


    Vue 插槽机制


    Vue 的插槽机制是通过提供一个适配层,将父组件传入的内容插入到子组件的指定位置。开发者可以使用具名插槽、作用域插槽等方式,实现灵活的插槽传递。


    <template>
    <child-component>
    <template #header>
    <h1>This is the header</h1>
    </template>
    <p>This is the default content</p>
    </child-component>

    </template>

    父组件通过 #header 插槽插入了一个标题内容,而 child-component 会将其插入到适当的位置。这里,插槽充当了一个适配器,允许父组件插入的内容与子组件的内容结构灵活匹配。


    十全十美


    至此撒花~


    后记


    我相信技术不分界,不深入了解,就不要轻易断言。


    一个圆,有了一个缺口,不知道的东西就更多了。


    但是没有缺口,不知道的东西就少了。


    这也就是为什么,知道得越多,不知道的就越多。


    谢谢!


    最后,祝君能拿下满意的offer。


    我是Dignity_呱,来交个朋友呀,有朋自远方来,不亦乐乎呀!深夜末班车



    👍 如果对您有帮助,您的点赞是我前进的润滑剂。



    以往推荐


    小小导出,我大前端足矣!


    靓仔,说一下keep-alive缓存组件后怎么更新及原理?


    面试官问我watch和computed的区别以及选择?


    面试官问我new Vue阶段做了什么?


    前端仔,快把dist部署到Nginx上


    多图详解,一次性啃懂原型链(上万字)


    Vue-Cli3搭建组件库


    Vue实现动态路由(和面试官吹项目亮点)


    项目中你不知道的Axios骚操作(手写核心原理、兼容性)


    VuePress搭建项目组件文档


    原文链接


    juejin.cn/post/744421…


    作者:Dignity_呱
    来源:juejin.cn/post/7444215159289102347
    收起阅读 »

    再见 XShell!一款万能通用的终端工具,用完爱不释手!

    作为一名后端开发,我们经常需要使用终端工具来管理Linux服务器。最近发现一款比Xshell更好用终端工具XPipe,能支持SSH、Docker、K8S等多种环境,还具有强大的文件管理工具,分享给大家! XPipe简介 XPipe是一款全新的终端管理工具,具...
    继续阅读 »

    作为一名后端开发,我们经常需要使用终端工具来管理Linux服务器。最近发现一款比Xshell更好用终端工具XPipe,能支持SSH、Docker、K8S等多种环境,还具有强大的文件管理工具,分享给大家!



    XPipe简介


    XPipe是一款全新的终端管理工具,具有强大的文件管理功能,目前在Github上已有4.8k+Star。它可以基于你本地安装的命令行工具(例如PowerShell)来执行远程命令,反应速度非常快。如果你有使用 ssh、docker、kubectl 等命令行工具来管理服务器的需求,使用它就可以了。


    XPipe具有如下特性:



    • 连接中心:能轻松实现所有类型的远程连接,支持SSH、Docker、Podman、Kubernetes、Powershell等环境。

    • 强大的文件管理功能:具有对远程系统专门优化的文件管理功能。

    • 多种命令行环境支持:包括bash、zsh、cmd、PowerShell等。

    • 多功能脚本系统:可以方便地管理可重用脚本。

    • 密码保险箱:所有远程连接账户均完全存储于您本地系统中的一个加密安全的存储库中。


    下面是XPipe使用过程中的截图,界面还是挺炫酷的!




    这或许是一个对你有用的开源项目,mall项目是一套基于 SpringBoot3 + Vue 的电商系统(Github标星60K),后端支持多模块和2024最新微服务架构 ,采用Docker和K8S部署。包括前台商城项目和后台管理系统,能支持完整的订单流程!涵盖商品、订单、购物车、权限、优惠券、会员、支付等功能!



    项目演示:



    使用



    • 首先去XPipe的Release页面下载它的安装包,我这里下载的是Portable版本,解压即可使用,地址:github.com/xpipe-io/xp…




    • 下载完成后进行解压,解压后双击xpiped.exe即可使用;




    • 这里我们先进行一些设置,将语言设置成中文,然后设置下主题,个人比较喜欢黑色主题;




    • 接下来新建一个SSH连接,输入服务器地址后,选择添加预定义身份




    • 这个预定义身份相当于一个可重用的Linux访问账户;




    • 然后输入连接名称,点击完成即可创建连接;




    • 我们可以发现XPipe能自动发现服务器器上的Docker环境并创建连接选项,如果你安装了K8S环境的话,也是可以发现到的;




    • 然后我们单击下Linux-local这个连接,就可以通过本地命令行工具来管理Linux服务器了;




    • 如果你想连接到某个Docker容器的话,直接点击对应容器即可连接,这里以mysql为例;




    • 选中左侧远程服务器,点击右侧的文件浏览器按钮可以直接管理远程服务器上的文件,非常方便;




    • 所有脚本功能中,可以存储我们的可重用脚本;




    • 所有身份中存储着我们的账号密码,之前创建的Linux root账户在这里可以进行修改。



    总结


    今天给大家分享了一款好用的终端工具XPipe,界面炫酷功能强大,它的文件管理功能确实惊艳到我了。而且它可以用本地命令行工具来执行SSH命令,对比一些套壳的跨平台终端工具,反应速度还是非常快的!


    项目地址


    github.com/xpipe-io/xp…


    作者:MacroZheng
    来源:juejin.cn/post/7475662844789637160
    收起阅读 »

    关于生娃的思考

    生娃是人生很重要的事,值得花时间思考。我断断续续想了几年,没想明白,最近看 B 站半佛老师对哪吒 2 的影评,理解了什么情况下可以生娃,把感受分享给大家。 什么时候可以给孩子无条件的、不抱任何期待的爱,什么时候就可以生娃了。 半佛老师在影评中表示非常羡慕哪吒有...
    继续阅读 »

    生娃是人生很重要的事,值得花时间思考。我断断续续想了几年,没想明白,最近看 B 站半佛老师对哪吒 2 的影评,理解了什么情况下可以生娃,把感受分享给大家。


    什么时候可以给孩子无条件的、不抱任何期待的爱,什么时候就可以生娃了。


    半佛老师在影评中表示非常羡慕哪吒有那样的父母,因为哪吒的父母给哪吒无条件的、不抱任何期待的爱,而半佛老师的童年比较悲惨,让他有点羡慕嫉妒恨。为此半佛特意查了导演饺子的家庭,希望饺子是因为缺爱才把哪吒父母写的那么好,结果发现饺子就是哪吒本吒,他彻底破防了。


    过年回家免不了和父母共处几天,我也几度破防,好几次想直接回出租屋。也被催生,丈母娘也催。


    之前对生娃的看法,我觉得生娃太费钱了,而我没钱,就算我有钱,我也不想把钱花在娃上面,我想花在自己身上,多体验人生。


    其次我自己这辈子都没活明白,也挺痛苦,何必生个娃来这悲惨世间走一遭?所以我不生娃。


    但这次我有新的想法,是否生娃,应取决于我是否做好了为人父母的准备,即是否可以给孩子无条件的、不抱任何期待的爱。这决定孩子一辈子是否幸福。


    两个关键词,无条件、不抱期待。


    无条件


    考试考得好,父母就爱,考得不好就不爱;听话就爱,不听话就不爱;不知道你们怎样,我小时候是这样的。


    这让我没有安全感,下意识会做些讨好父母的行为,来获得父母的 “ 爱 ”。


    当我成年的时候,我妈发现这招不管用了,我不需要他们的爱,我需要钱,因为钱能给我安全感。


    所以我大学去食堂打工,每天中午和晚上早点下课过去,结果一个月才 300 块钱包两顿饭。


    不抱期待


    期待这个东西,在教育中尤为突出,否则不会也有那么多鸡娃的父母。


    父母对孩子的期待,让孩子有非常强烈的愧疚。我辛辛苦苦把你从农村带到城市,你就考这么几分?我辛辛苦苦把你拉扯大,你就这么对我?


    在这种环境下,我成为了一个逆子,六亲不认,自动屏蔽亲情。我不接受他们的爱,我也不给他们爱。


    大学打工的 300 块不能养活我,每年还要交学费,我一想到学费是父母出的,他们又会以此要挟,我辛辛苦苦赚钱供你上大学,结果你就这样?


    所以只读了半年大学,我退学了,自己出去找工作,我必须在经济上独立,必须逃离这个家。


    别人说家是港湾,外面受伤了家是永远的依靠,对我来说家就是伤害。


    其实我这样还算好的,至少活下来了,还有更多孩子,承受不了这种愧疚,选择了自杀,他们要把愧疚,加倍偿还给父母。


    我身边就有这样的案例,跳楼了,他父母不知道为什么说了两句,孩子就直接从阳台上跳下去了。


    我可太懂了,我也想过自杀,一来是当时家里装了防盗窗,我有点胖钻不过去;然后换成了撞墙,头上撞了几个包有点疼,基因让我停下了;我还尝试过离家出走,也没走成。


    但我也在脑海中无数次幻想,要是我死了,我爸妈有多愧疚,这就是我自杀的目的。


    我写的和想的有点黑暗,有人说什么爸妈把你辛辛苦苦拉扯大,不容易什么的。


    但是我不需要,我没求着来这个世上。


    所以我对自己生娃,非常谨慎,我不希望他的童年会想自杀,我不希望他成年后和父母几乎断绝关系,我希望他要来就过的幸福点,这取决我能否会给他无条件的、没有期待的爱。


    愿各位都是好父母,愿世上孩子都幸福,以上是我的思考,共勉。


    作者:jianzhangg
    来源:juejin.cn/post/7467353503088246784
    收起阅读 »

    官方回应无虚拟DOM版Vue为什么叫Vapor

    web
    相信很多人和我一样,好奇无虚拟DOM版的Vue为什么叫Vue Vapor。之前看过一个很新颖的观点:Vue1时代就没有虚拟DOM,而那个时代恰好处于前端界的第一次工业革命,也就是以声明式代替命令式的写法。所以无虚拟DOM版Vue让人感觉梦回Vue1,于是就采取...
    继续阅读 »

    相信很多人和我一样,好奇无虚拟DOM版的Vue为什么叫Vue Vapor。之前看过一个很新颖的观点:Vue1时代就没有虚拟DOM,而那个时代恰好处于前端界的第一次工业革命,也就是以声明式代替命令式的写法。所以无虚拟DOM版Vue让人感觉梦回Vue1,于是就采取了Vapor这个名字。



    Vapor是蒸汽的意思,第一次工业革命开创了以机器代替手工劳动的时代,该革命以蒸汽机作为动力机被广泛使用为标志。



    不过这个说法并非来自官方,虽然乍一听还挺有道理的,但总感觉官方并不一定是这么想的。事实也的确如此,在官方的Vue Conf中,Vue Vapor的作者出面说明了Vapor这个名字的含义:


    SCR-20250301-sbrn.png


    由于无虚拟DOM的特性,纯Vapor模式下可以去掉很多代码,比如VDom Diff。所以Vue Vapor的包体积可以做的更加的轻量化,像水蒸气一样轻。



    (前面那段话是官方说的,这段话是我说的)当然不是说Vapor模式就不需要diff算法了,我看过同为无虚拟DOM框架的SvelteSolid源码,无虚拟DOM只是不需要vDom间的Diff算法了,列表之间还是需要diff的。毕竟再怎么编译,你从后端获取到的数组,编译器也不可能预测得到。



    那具体能轻量多少呢?官方给出的数据是比虚拟DOMVue33.6%


    SCR-20250301-scmd.png


    Vapor的名字除了想表示轻量化之外还有一个另外一个原因,那就是Solid。可能有人会说这关Solid什么事啊?实际上Vapor的灵感正是来自于Solid(尤雨溪亲口承认)而Solid代表固体:


    SCR-20250301-sirl.png


    为了跟Solid有个趣味联动,那无虚拟DOM就是气体好了:


    SCR-20250301-sjel.png


    以上就是Vue Vapor作者告诉大家为什么叫Vapor的两大原因。


    性能


    之前都说虚拟DOM是为了性能,怎么现在又反向宣传了?无虚拟DOM怎么又逆流而上成为了性能的标杆了呢?这个话题我们留到下一篇文章去讲,本篇文章我们只看数据:


    SCR-20250301-scoj.png


    SCR-20250301-scsm.png


    从左到右依次为:



    • 原生JS:1.01

    • Solid:1.09

    • Svelte:1.11

    • 无虚拟DOMVue:1.24

    • 虚拟DOMVue:1.32

    • React:1.55


    数字越小代表性能越高,但无论再怎么高都不可能高的过手动优化过的原生JS,毕竟无论什么框架最终打包编译出来的还是JS。不过框架的性能其实已经挺接近原生的了,尤其是以无虚拟DOM著称的SolidSvelte。但无虚拟DOMVue和虚拟DOMVue之间并没有拉开什么很大的差距,1.241.32这两个数字证明了其实二者的性能差距并不明显,这又是怎么一回事呢?


    一个原因是Vue3本来就做了许多编译优化,包括但不限于静态提升、大段静态片段将采用innerHTML渲染、编译时打标记以帮助虚拟DOM走捷径等… 由于本来的性能就已经不错了,所以可提升的空间自然也没多少。


    看完上述原因肯定有人会说:可提升的空间没多少可以理解,那为什么还比同为无虚拟DOMSolidSvelte差那么多?如果VaporSolid性能差不多的话,那可提升空间小倒是说得过去。但要是像现在这样差的还挺多的话,那这个理由是不是就有点站不住脚了?之所以会出现这样的情况是因为Vue Vapor的作者在性能优化和快速实现之间选择了后者,毕竟尤雨溪第一次公布要开始做无虚拟DOMVue的时间是在2020年:


    6ccefc4a3c7e8656b9d0537927bd42b5.jpeg


    而如今已经是2025年了。也就是说如果一个大一新生第一次满心欢喜的听到无虚拟DOMVue的消息后,那么他现在大概率已经开始毕业工作了都没能等到无虚拟DOMVue的发布。这个时间拖的有点太长了,甚至从Vue2Vue3都没用这么久。


    所以现在的首要目标是先实现出来一版能用的,后面再慢慢进行优化。我们工作不也是这样么?先优先把功能做出来,优化的事以后再说。那么具体该怎么优化呢:


    SCR-20250301-scwz.png


    现在的很多渲染逻辑继承自原版Vue3,但无虚拟DOMVue可以采用更优的渲染逻辑。而且现在的Vapor是跟着原版Vue的测试集来做的,这也是为了实现和原版Vue一样的行为。但这可能并不是最优解,之后会慢慢去掉一些不必要的功能,尤其是对性能产生较大影响的功能。


    往期精彩文章:


    《尤雨溪:从Vue1到Vue3.6》


    作者:页面魔术
    来源:juejin.cn/post/7477104460452872202
    收起阅读 »

    Netflix 删除了 React ?

    web
    来源 Netflix Removed React? Netflix 删除了 React "Netflix 删除了 React,网站加载时间减少了 50%!" 这条爆炸性新闻在推特炸开了锅。这不禁让人想起半年前"微软应该禁止所有新项目使用 React" 的新闻 ...
    继续阅读 »

    来源 Netflix Removed React?


    Netflix 删除了 React


    "Netflix 删除了 React,网站加载时间减少了 50%!"


    这条爆炸性新闻在推特炸开了锅。这不禁让人想起半年前"微软应该禁止所有新项目使用 React" 的新闻 - 两大科技巨头相继 "抛弃" React,难道这预示着什么?


    "React 心智负担重、性能差、bundle 体积大...". 一时间,各种唱衰 React 的声音此起彼伏。有人甚至信誓旦旦地表示:"只要像 Netflix 一样干掉 React,你的网站性能立马提升 50%!"


    fake-twitter.png


    这条发布于 2024 年 10 月 27 日的推文有着惊人的 150 万浏览量。但是,这大概率是 AI 生成的假新闻。
    事实上,我们去 Netflix 的官网打开 react-devtools,发现他们依然在使用 React 构建他们的网站。


    netflix-react-devtools.png


    Netflix 的真实案例


    这篇 AI 生成的假新闻灵感来自 2017 年 Netflix 工程师在 hack news 上发布的一篇文章 - Netflix: Removing client-side React.js improved performance by 50%


    他直接移除了这篇文章最重要的部分 - client-side React.js, 也就是客户端的 React.js 代码。


    实际的情况是,Netflix 团队在 2017 年的时候在使用 React 构建他们的 landing page


    为什么在一个简单的 landing page 上要使用 React 呢?因为在 landing page



    • Netflix 需要处理大量的 AB 测试

    • 支持近 200 个国家的本地化

    • 根据用户设备、地理位置等因素动态调整内容

    • 需要服用现有的 React 组件


    基于上述需求的考虑,Netflix 团队选择了使用 React 来构建他们的 landing page


    为了优化页面加载性能,他们采用了服务端渲染的方案。同时,为了保证后续交互的流畅性,系统会预先加载(pre-fetch)后续流程所需的 React/Redux 相关代码。


    从架构上看,这个 landing page 本质上仍然是一个单页面应用(SPA),保持了 SPA 快速响应的优势。不同之处在于首次访问时,页面内容是由服务端直接生成的,这样可以显著提升首屏加载速度。


    这样做的缺点


    显然,Netflix 在 2017 年这么做是有原因的,他们当时的确也没有更好的方案。在 2025 年的今天,
    再来回顾这个方案,显然有以下缺点:


    数据重复获取


    在首屏渲染时,服务端需要获取数据来生成 HTML,而在客户端激活(hydration)后,为了保持交互性,往往又需要重新获取一遍相同的数据。
    这种重复的数据获取不仅浪费资源,还可能带来不必要的性能开销。


    客户端代码体积膨胀


    因为本质上,Netflixlanding page 是一个还是一个 SPA,那么不可避免的,所有可能的 UI 状态都需要打包,
    即使用户只需要其中的一部分代码。例如,在一个很常见的 tabs 页面


      <Tabs
    defaultActiveKey="1"
    items={[
    {
    label: 'Tab 1',
    key: '1',
    children: 'Tab 1',
    },
    {
    label: 'Tab 2',
    key: '2',
    children: 'Tab 2',
    disabled: true,
    },
    ]}
    />

    即使用户只点击了 Tab 1, 即使 Tab 2 没有被渲染,但是 Tab 2 的代码也会被打包。


    如何解决这些问题


    React Server Components (RSC) 为上述问题提供了优雅的解决方案:


    避免数据重复获取


    使用 RSC,组件可以直接在服务器端获取数据并渲染,客户端只接收最终的 HTML 结果。不再需要在客户端重新获取数据。


    智能代码分割


    RSC 允许我们选择性地决定哪些组件在服务器端运行,哪些在客户端运行。例如:


    function TabContent({ tab }: { tab: string }) {
    // 这部分代码只在服务器端运行,不会打包到客户端
    return <div>{tab} 内容</div>
    }

    // 客户端组件
    'use client'
    function TabWrapper({ children }) {
    const [activeTab, setActiveTab] = useState('1')
    return (
    <div>
    {/* Tab 切换逻辑 */}
    {children}
    </div>

    )
    }

    在这个例子中:



    • TabContent 的所有可能状态都在服务器端预渲染

    • 只有实际需要交互的 TabWrapper 会发送到客户端

    • 用户获得了更小的 bundle 体积和更快的加载速度


    这不就是 PHP?


    经常会看到有人说:"Server Components 不就是重新发明了 PHP 吗?都是在服务端生成 HTML。"


    显然,PHP 与现在的 Server Components 在开发体验上有本质的区别。


    1. 细粒度的服务端-客户端混合


    与 PHP 不同,RSC 允许我们在组件级别决定渲染位置,用一个购物车的例子来说明:


    // 服务端组件
    function ProductDetails({ id }: { id: string }) {
    // 在服务器端获取数据和渲染
    const product = await db.products.get(id);
    return <div>{product.name}</div>;
    }

    // 客户端组件
    'use client'
    function AddToCart({ productId }: { productId: string }) {
    // 在客户端处理交互
    return <button onClick={() => addToCart(productId)}>加入购物车</button>;
    }

    // 混合使用
    function ProductCard({ id }: { id: string }) {
    return (
    <div>
    <ProductDetails id={id} />
    <AddToCart productId={id} />
    </div>

    );
    }

    这种设计充分利用了服务端和客户端各自的优势 - 在服务端可以直接访问数据库获取 ProductDetails 所需的数据,而在客户端则能更好地处理 AddToCart 这样的用户交互。这不仅提升了性能,也让代码结构更加清晰合理。


    2. 保持组件的可复用性


    RSC 最强大的特性之一是组件的可复用性不受渲染位置的影响:


    // 这个组件可以在服务端渲染
    function UserProfile({ id }: { id: string }) {
    return <ProfileCard id={id} />;
    }

    // 同样的组件也可以在客户端动态加载
    'use client'
    function UserList() {
    const [selectedId, setSelectedId] = useState(null);
    return selectedId ? <ProfileCard id={selectedId} /> : null;
    }

    因为都是 React 组件,区别仅仅是渲染位置的不同,同一个组件可以:



    • 在服务端预渲染时使用

    • 在客户端动态加载时使用

    • 在流式渲染中使用


    这种统一的组件模型是 PHP 等传统服务端渲染所不具备的。


    3. 智能的序列化


    RSC 还提供了智能的序列化机制,可以自动将组件的 propsstate 序列化,从而在服务端和客户端之间传递。
    避免了重复获取数据的问题。


    // 服务端组件
    async function Comments({ postId }: { postId: string }) {
    // 1. 获取评论数据
    const comments = await db.comments.list(postId);

    // 2. 传递给客户端组件
    return <CommentList initialComments={comments} />;
    }

    // 客户端组件
    'use client'
    function CommentList({ initialComments }) {
    // 3. 直接使用服务端数据,无需重新请求
    const [comments, setComments] = useState(initialComments);

    return (
    // 渲染评论列表
    );
    }

    4. 渐进式增强


    RSC 还提供了渐进式增强的能力,可以在服务端和客户端之间无缝过渡。



    • 首次访问时返回完整的 HTML

    • 按需加载客户端交互代码

    • 保持应用的可访问性


    这让我们能够构建既有良好首屏体验,又能提供丰富交互的现代应用,完美解决了在 2017 年 Netflix 所提出的问题。


    总结


    通过对上面这些案例的分析,我们可以看出


    1. 不要轻信网络传言


    网络上充斥着各种技术传言。虽然像上面这种完全虚构的假新闻容易识破,但有些传言会巧妙地利用真实数据和案例,通过夸张的描述来误导技术选型和决策,这类信息需要我们格外谨慎分辨。
    例如:



    svelte 放弃 TypeScript 改用 JSdoc 进行类型检查



    这个确实是一个真的新闻,但是并不代表着 Typescript 的没落,实际上



    • Svelte 团队选择 JSDoc 是为了减少编译时间

    • 这是针对框架源码的优化,而不是面向使用者的建议

    • Svelte 依然完整支持 TypeScript,用户代码可以继续使用 .ts/.ts



    tauri 打包后的体积比 electron 小多了,我们应该放弃 electron 使用 tauri



    技术选型不能仅仅看单一指标。虽然 tauri 的打包体积确实小于 electron,但在开发体验、性能、稳定性、生态和社区支持等关键维度上都存在明显短板。


    如果你尝试用 tauri 开发复杂应用,很可能会因为生态不完善、社区支持不足而陷入困境。当你遇到问题去 GitHub 寻找解决方案时,看到许多库已经一年未更新,就会明白为什么大多数团队仍在选择 electron


    2. 历史的选择


    2017 年的 Netflix 面临着复杂的业务需求,他们选择了当时最佳的解决方案 - 服务端渲染 + 客户端激活。这个方案虽然解决了问题,但也带来了一些困扰:



    • 数据需要在服务端和客户端重复获取

    • JavaScript bundle 体积过大


    3. RSC 带来的改变


    React Server Components 为这些历史遗留问题带来了全新的解决思路:



    • 服务端渲染与客户端渲染完美融合

    • 智能的代码分割,最小化客户端 bundle 体积

    • 数据获取更高效,避免重复请求

    • 渐进式增强,提供流畅的用户体验


    4. 技术演进的启示


    Netflix 2017 年的实践到今天的 RSC,我们可以看到:



    • 技术方案在不断进化,过去的最佳实践可能已不再适用

    • RSC 不是简单的"回归服务端",而是开创了全新的开发模式

    • 性能与开发体验不再是非此即彼的选择


    RSC 代表了现代前端开发的新趋势 - 既保持了 React 强大的组件化能力,又通过创新的架构设计解决了历史难题。这让我们终于可以在性能和开发体验之间找到完美平衡。


    作者:snow分享
    来源:juejin.cn/post/7459029441039794211
    收起阅读 »

    一次失败的UI规范制定

    web
    前言 在公司中,前端使用了统一的组件element-ui,但是有一些页面大家并没有达成共识,造成了不同的团队开发出来的页面不同,公司决定在24年8月的迭代进行统一调整。在这个中间,我遇到了很多意料之外的问题,希望未来的你遇到了类似的问题,可以尽量避免 为什么会...
    继续阅读 »

    前言


    在公司中,前端使用了统一的组件element-ui,但是有一些页面大家并没有达成共识,造成了不同的团队开发出来的页面不同,公司决定在24年8月的迭代进行统一调整。在这个中间,我遇到了很多意料之外的问题,希望未来的你遇到了类似的问题,可以尽量避免


    为什么会产生这个问题


    image.png
    这个问题我也思考过,大概有以下原因



    1. 我们有4个产品经理,每个人负责不同的模块,都有各自的风格,造成页面不统一

    2. 没有一个严格的UI规范,前端开发和测试并没有一个可以参考标准页面

    3. 22年至今,项目都在疯狂的迭代功能,并没有人停下来看看之前做的功能有哪些问题,我们该怎么去优化


    项目背景


    参与人:UI设计师、前端开发、产品(主要负责审核,并没参与讨论)、测试


    牵头人:UI设计师
    职责:找出问题点,整理为在线文档


    解决者:前端
    职责:整理问题点、改公告组件、输出文档


    主要问题如下



    1. 弹窗(Dialog)组件大小不一。宽度有400px、700px、800px、940px、1250px、50%、60%等,用户在操作的时候,会感觉到页面时大时小,不统一

    2. 表格样式很多(Table)。搜索的列未对齐、分页码数未统一、表格后的操作栏千奇百怪、表头的标题有的进行了换行等

    3. 颜色的乱用。颜色有很多,有各种颜色的红色

    4. 弹窗表单的按钮位置不同。有的取消在左,有的取消在右面。

    5. 等等一些小问题就不一一列举了


    弹窗组件大小不一


    弹窗大小不统一部分截图


    800px
    image.png


    600px


    image.png


    1180px


    image.png


    解决方案


    我们在私服中clone了一份element-ui,直接修改了源码


    默认制定了三个尺寸的size,small(600px), medium(900px),large(1200px),满足基本的需求。当需要支持特殊组件的时候,也可以直接设置Width满足需求


    image.png


    表格不统一


    部分截图


    image.png


    上方的截图有几个问题



    1. 搜索条件(查找人员)没有和新增按钮对齐

    2. 离职和删除的按钮是比较敏感的操作,但是夹在了一堆按钮中间

    3. 操作按钮有的有icon,有的没icon,看着些许的混乱


    进行修改后效果如下,页面看着更加的工整
    image.png


    解决方案如下



    1. 搜索条件的第一个在组件内直接进行计算宽度,防止业务端进行二次修改,如果是自定义的搜索条件,并且设置了宽度,需要手动进行修改

    2. 一些危险的操作:比如离职、删除统一放在最后,并且每个操作按钮都要有icon


    表格按钮的调整


    调整前

    image.png


    调整后

    image.png


    解决方案如下

    表格后的按钮去除所有的icon,直接在组件内进行拦截,默认展示三个按钮,多余的放在dropDown内,当然也支持业务端设置显示几个按钮


    核心部分代码如下

    image.png


    分页数据不统一


    调整前

    image.png


    调整后

    image.png


    解决方案

    分页条数统一改为(20,50,100)


    考虑的因素为:之前的页面有15一页的。当页面过高的时候,会撑不满一个页面,造成table的页面空白,不太美观


    弹窗中,下方的操作栏的按钮位置不统一


    调整前


    image.png


    调整后


    image.png


    解决方案


    所有的按钮都是取消在左,保存在右面,同时,confirm组件,在element-ui的代码中直接修改调整位置,减少业务端工作量


    image.png


    颜色的乱用


    部分截图


    image.png


    image.png


    image.png


    image.png


    解决方案


    在主应用的根节点 使用css变量,在每个子应用的页面直接使用根节点的样式,避免子应用的颜色出现各种奇奇怪怪的颜色。


    image.png


    使用的地方


    image.png


    等等
    当然还有一些基础颜色的修改、按钮最小宽度的修改、一些表格间距的调整等。这些和设计师、产品达成共识后,进行修改就行了,也就不一一列举了


    交付给测试



    1. 我这边开发的差不多了,输出了一个文档,别的前端开发按照着文档进行开发。

    2. 测试按照文档进行编写测试用例


    不好搞了


    image.png


    测试这边疯狂提bug。


    还有一个小小的背景


    测试这边其实是有一个绩效考核:bug提的越多,绩效越高


    但是,我们这边项目上线的时候,其实还有一个要求,bug不能超过9个


    这个UI规范制定,到这个功能的提测,只有10天就项目上线了。


    有的功能并没有做,但是测试觉得不合理,就提了这个bug(比如表单的label也没有对齐,但是这个迭代并没有去做修复),造成我们前端开发这边bug超级多


    同时,开发这边也会有绩效考核,bug越多绩效就会越低,会扣除工资,大家心态崩溃了,改的速度根本赶不上提的速度。


    当然,也有部分功能是我这边测试不充分,造成业务端不好去实现


    找领导协助


    image.png


    这个过程是个很折磨人的过程, 开发同事在抱怨测试乱提、测试也在提本次优化范围外的bug,内心也很着急



    1. 重新定义了测试的范围,超过范围的问题可以先记着,不再提bug,方便后续进行修改

    2. 前端开发的bug都先指派给我,我看看能否从底层解决,我解决后再转给研发经理(研发经理没有此项的考核),当底层解决不了,业务端解决后,也指派给我,这样前端的开发就没有这么暴躁了


    如果再来一次UI规范的升级我会怎么做



    1. 先在每个团队中找一个人配合我,先升级页面后,进行灰度测试

    2. 在决定我们这个页面要成什么样子的时候,一定要拉上产品。设计师在做设计稿的时候,虽然每次都说了找产品的领导确认过了,但是领导还是比较忙, 并没有很多精力来做这件事情,可以找他们组内的一个资历比较深的产品一起讨论下。开发、UI设计师站的角度不同,一定要需要产品来参与讨论,做的页面才会更加的易用

    3. 限制测试的范围。 测试测着测试着会发散思维,导致改不完的bug,最终会导致开发没有了心态

    4. UI标准的功能,越早出来越好,越大后期需要投入的人力越多


    作者:pauldu
    来源:juejin.cn/post/7456685819047608355
    收起阅读 »

    uni-app 实现好看易用的抽屉效果

    web
    往期文章推荐: 软考小工具重磅更新啦!最好用的软考刷题工具 uni-app 高效开发小程序技巧:自动化切换环境变量 JavaScript 访问者模式:打造高扩展性的对象结构 一. 前言 我之前使用 uni-app 和 uniCloud 开发了一款软考刷题应用,...
    继续阅读 »

    往期文章推荐:


    软考小工具重磅更新啦!最好用的软考刷题工具


    uni-app 高效开发小程序技巧:自动化切换环境变量


    JavaScript 访问者模式:打造高扩展性的对象结构


    一. 前言


    我之前使用 uni-appuniCloud 开发了一款软考刷题应用,在这个应用中,我使用了抽屉组件来实现一些功能,切换题库,如下图所示:


    youti1.gif


    在移动应用开发中,抽屉(Drawer)是一种常见的界面设计模式,这个组件可以在需要侧边导航或者额外信息展示的地方使用。它允许用户通过侧滑的效果打开一个菜单或额外的内容区域。


    这种设计不仅能够节省屏幕空间,还能提供一种直观的交互方式。


    例如,在电商应用中,可以将购物车或分类列表放在抽屉里;在新闻阅读器中,可以放置频道选择等;而在有题记刷题软件中,我主要用于题库的选择功能。


    本文将介绍如何使用 uni-app 框架来实现一个简单的抽屉组件:DrawerWindow。文末提供完整的代码示例,让你能够轻松地在 uni-app 中实现抽屉效果。


    二. 实现分析


    Vue 组件的结构通常由三个主要部分组成:模板(<template>)、脚本(<script>)和样式(<style>),标准的的单文件组件(SFC)结构。


    uni-app 也是如此,在这个组件中,我们也将使用 Vue 的单文件组件(SFC)结构,这意味着我们将在一个 .vue 文件中同时包含模板、脚本和样式。


    接下来我们按照这个格式来简单实现一下。


    1. 模板页面 (<template>)


    首先,模版页面是很简单的部分,我们需要创建一个基础的 Vue 组件,该组件包含了主页面、抽屉内容和关闭按钮三个部分。以下是组件的模板代码:


    <template>
    <view class="drawer-window-wrap">
    <scroll-view scroll-y class="DrawerPage" :class="{ show: modalName === 'viewModal' }">
    <!-- 主页面 -->
    <slot></slot>
    </scroll-view>
    <!-- 关闭抽屉 -->
    <view class="DrawerClose" :class="{ show: modalName === 'viewModal' }" @tap="hide">
    <u-icon name="backspace"></u-icon>
    </view>
    <!-- 抽屉页面 -->
    <scroll-view scroll-y class="DrawerWindow" :class="{ show: modalName === 'viewModal' }">
    <slot name="drawer"></slot>
    </scroll-view>
    </view>
    </template>

    在模板部分,我们主要定义了三个主要元素:主页面、关闭按钮和抽屉页面。每个元素都有一个class绑定,这个绑定会根据 modalName 的状态来决定是否添加 .show 类。



    • 主页面 (<scroll-view class="DrawerPage">):



      • 这个滚动视图代表应用的主要内容区域。

      • 当抽屉打开时,它会被缩小并移向屏幕右侧。

      • 提供默认插槽 <slot></slot>,允许父组件传递自定义内容到这个位置。



    • 关闭按钮 (<view class="DrawerClose">):



      • 位于屏幕右侧的一个透明背景层,当点击时触发 hide() 方法来关闭抽屉。

      • 包含了一个图标 <u-icon name="backspace"></u-icon>,这里使用的是 uView UI 库中的图标组件。你可以选用其他组件库里的图标或者图片。



    • 抽屉页面 (<scroll-view class="DrawerWindow">):



      • 这是抽屉本身的内容区域,通常包含菜单或其他附加信息。

      • 同样地,定义特有的插槽名称,<slot name="drawer"></slot> 允许从外部插入特定的内容。

      • 抽屉默认是隐藏的,并且当显示时会有动画效果。




    在这里,我们主要使用了 <slot> 元素来定义可以插入自定义内容的位置。modalName 属性用来控制抽屉的状态。


    2. 逻辑处理 (<script>)


    接下来,逻辑处理其实也很简单,主要会定义打开和关闭抽屉的方法:


    <script>
    export default {
    data() {
    return {
    modalName: null
    }
    },
    methods: {
    // 打开抽屉
    show() {
    this.modalName = 'viewModal';
    },
    // 关闭抽屉
    hide() {
    this.modalName = null;
    }
    }
    }
    </script>


    • 数据 (data):



      • modalName: 用于控制抽屉状态的数据属性。当它的值为 'viewModal' 时,表示抽屉处于打开状态;否则,抽屉是关闭的。



    • 方法 (methods):



      • show(): 将 modalName 设置为'viewModal',从而通过 CSS 样式控制抽屉显示。

      • hide(): 将 modalName 重置为 null,控制抽屉隐藏。




    当调用 show() 方法时,modalName 被设置为 'viewModal',这会触发 CSS 中的 .show 类,从而显示抽屉;反之,调用 hide() 方法则会隐藏抽屉。


    3. 样式设计 (<style>)


    在这个组件中,其实要做好的在样式部分,主要是显示抽屉的动画部分。在主页面,我们主要定义了三个主要的样式类:主页面、关闭按钮和抽屉页面。



    • 主页面样式 (DrawerPage):



      • 初始状态下占据整个屏幕宽度和高度。

      • 当抽屉打开时(即有.show类),页面会缩小并移动到屏幕右侧 85%的位置,同时增加阴影效果以模拟深度。



    • 关闭按钮样式 (DrawerClose):



      • 默认情况下是不可见且不响应用户交互的。

      • 当抽屉打开时,按钮变为可见并可点击,提供了一种关闭抽屉的方式。



    • 抽屉页面样式 (DrawerWindow):



      • 初始状态下位于屏幕左侧外侧,不显示也不响应交互。

      • 当抽屉打开时,抽屉平滑滑入屏幕内,变得完全可见且可以与用户互动。



    • 动画与过渡



      • 所有的 .show 类都带有 transition: all 0.4s;,这使得任何属性的变化都会有一个 0.4 秒的平滑过渡效果。

      • 抽屉和主页面的 transform 属性被用来控制它们的位置和大小变化。

      • opacitypointer-events 属性确保在不需要时抽屉不会影响用户的操作。




    如下代码所示,我们主要添加一些 CSS 样式来实现平滑的过渡效果以及视觉上的美观:


    <style lang="scss">
    // 省略其他样式...
    .DrawerPage.show,
    .DrawerWindow.show,
    .DrawerClose.show {
    transition: all 0.4s;
    }

    .DrawerPage.show {
    transform: scale(0.9, 0.9) translateX(85vw);
    box-shadow: 0 0 60rpx rgba(0, 0, 0, 0.2);
    }

    .DrawerWindow.show {
    transform: scale(1, 1) translateX(0%);
    opacity: 1;
    pointer-events: all;
    }

    .DrawerClose.show {
    width: 15vw;
    color: #fff;
    opacity: 1;
    pointer-events: all;
    }
    </style>

    以上的这些样式确保了当抽屉显示或隐藏时有流畅的动画效果,并且在不需要的时候不会影响用户的交互。


    三. 完整代码


    1. 完整抽屉组件,复制可使用


    <template>
    <view class="drawer-window-wrap">
    <scroll-view scroll-y class="DrawerPage" :class="modalName == 'viewModal' ? 'show' : ''">
    <!-- 主页面 -->
    <slot></slot>
    </scroll-view>
    <!-- 关闭抽屉 -->
    <view class="DrawerClose" :class="modalName == 'viewModal' ? 'show' : ''" @tap="hide()">
    <u-icon name="backspace"></u-icon>
    </view>
    <!-- 抽屉页面 -->
    <scroll-view scroll-y class="DrawerWindow" :class="modalName == 'viewModal' ? 'show' : ''">
    <slot name="drawer"></slot>
    </scroll-view>
    </view>
    </template>

    <script>
    export default {
    data() {
    return {
    modalName: null
    }
    },
    methods: {
    // 打开抽屉
    show() {
    this.modalName = 'viewModal'
    },
    // 关闭抽屉
    hide() {
    this.modalName = null
    }
    }
    }
    </script>

    <style lang="scss">
    page {
    width: 100vw;
    overflow: hidden !important;
    }

    .DrawerPage {
    position: fixed;
    width: 100vw;
    height: 100vh;
    left: 0vw;
    background-color: #f1f1f1;
    transition: all 0.4s;
    }

    .DrawerPage.show {
    transform: scale(0.9, 0.9);
    left: 85vw;
    box-shadow: 0 0 60rpx rgba(0, 0, 0, 0.2);
    transform-origin: 0;
    }

    .DrawerWindow {
    position: absolute;
    width: 85vw;
    height: 100vh;
    left: 0;
    top: 0;
    transform: scale(0.9, 0.9) translateX(-100%);
    opacity: 0;
    pointer-events: none;
    transition: all 0.4s;
    background-image: linear-gradient(45deg, #1cbbb4, #2979ff) !important;
    }

    .DrawerWindow.show {
    transform: scale(1, 1) translateX(0%);
    opacity: 1;
    pointer-events: all;
    }

    .DrawerClose {
    position: absolute;
    width: 40vw;
    height: 100vh;
    right: 0;
    top: 0;
    color: transparent;
    padding-bottom: 50rpx;
    display: flex;
    align-items: flex-end;
    justify-content: center;
    background-image: linear-gradient(90deg, rgba(0, 0, 0, 0.01), rgba(0, 0, 0, 0.6));
    letter-spacing: 5px;
    font-size: 50rpx;
    opacity: 0;
    pointer-events: none;
    transition: all 0.4s;
    }

    .DrawerClose.show {
    opacity: 1;
    pointer-events: all;
    width: 15vw;
    color: #fff;
    }
    </style>

    2. 在父组件中使用抽屉组件


    在父组件中,可以通过以下简单的代码使用它,你可以继续进行丰富:


    <template>
    <drawer-window ref="drawerWindow">
    <view class="main-container" @click="$refs.drawerWindow.show()">
    主页面,点击打开抽屉
    </view>
    <view slot="drawer" class="drawer-container"> 抽屉页面 </view>
    </drawer-window>
    </template>

    <script>
    export default {}
    </script>

    <style lang="scss" scoped>
    .main-container,
    .drawer-container {
    font-weight: 700;
    font-size: 20px;
    text-align: center;
    color: #333;
    padding-top: 100px;
    }
    </style>

    以上代码的实现效果如下图所示:


    youti2.gif


    四. 小程序体验


    以上的组件,来源于我独立开发的软考刷题小程序中的效果,想要体验或软考刷题的掘友可以参考以下文章,文末获取:


    软考小工具重磅更新啦!最好用的软考刷题工具


    五. 结语


    通过以上步骤,我们已经构建了一个基本的抽屉组件。当然,你也可以根据具体的应用场景对这个组件进行进一步的定制和优化。


    作者:前端梦工厂
    来源:juejin.cn/post/7417374536670707727
    收起阅读 »

    个人或个体户,如何免费使用微信小程序授权登录

    web
    需求 个人或个体户,如何免费使用微信小程序授权,快速登录进系统内部? 微信授权登录好处: 不用自己开发一个登录模块,节省开发和维护成本 安全性得到了保障,安全验证完全交由腾讯验证,超级可靠哇 可能有的人会问,为何不用微信公众号授权登录?原因很简单,因为一年...
    继续阅读 »

    需求


    个人或个体户,如何免费使用微信小程序授权,快速登录进系统内部?


    微信授权登录好处:



    1. 不用自己开发一个登录模块,节省开发和维护成本

    2. 安全性得到了保障,安全验证完全交由腾讯验证,超级可靠哇


    可能有的人会问,为何不用微信公众号授权登录?原因很简单,因为一年要300元,小公司得省钱啊!


    实现步骤说明


    所有的步骤里包含四个对象,分别是本地后台本地微信小程序本地网页、以及第三方微信后台



    1. 本地后台调用微信后台https://api.weixin.qq.com/cgi-bin/token接口,get请求,拿到返回的access_token

    2. 本地后台根据拿到的access_token,调用微信后台https://api.weixin.qq.com/wxa/getwxacodeunlimit接口,得到二维码图片文件,将其输出传递给本地网页显示

    3. 本地微信小程序本地网页的二维码图片,跳转至小程序登录页面,通过wx.login方法,在success回调函数内得到code值,并将该值传递给本地后台

    4. 本地后台拿到code值后,调用微信后台https://api.weixin.qq.com/sns/jscode2session接口,get请求,得到用户登录的openid即可。



    注意点:



    1. 上面三个微信接口/cgi-bin/token/getwxacodeunlimit/jscode2session必须由本地后台调用,微信小程序那边做了前端限制;

    2. 本地网页如何得知本地微信小程序已扫码呢?


    本地微信小程序code,通过A接口,将值传给后台,后台拿到openid后,再将成功结果返回给本地微信小程序;同时,本地网页不断地轮询A接口,等待后台拿到openid后,便显示登录成功页面。



    微信小程序核心代码


    Page({
    data: {
    theme: wx.getSystemInfoSync().theme,
    scene: "",
    jsCode: "",
    isLogin: false,
    loginSuccess: false,
    isChecked: false,
    },
    onLoad(options) {
    const that = this;
    wx.onThemeChange((result) => {
    that.setData({
    theme: result.theme,
    });
    });
    if (options !== undefined) {
    if (options.scene) {
    wx.login({
    success(res) {
    if (res.code) {
    that.setData({
    scene: decodeURIComponent(options.scene),
    jsCode: res.code,
    });
    }
    },
    });
    }
    }

    },
    handleChange(e) {
    this.setData({
    isChecked: Boolean(e.detail.value[0]),
    });
    },
    formitForm() {
    const that = this;
    if (!this.data.jsCode) {
    wx.showToast({
    icon: "none",
    title: "尚未微信登录",
    });
    return;
    }
    if (!this.data.isChecked) {
    wx.showToast({
    icon: "none",
    title: "请先勾选同意用户协议",
    });
    return;
    }
    wx.showLoading({
    title: "正在加载",
    });
    let currentTimestamp = Date.now();
    let nonce = randomString();
    wx.request({
    url: `A接口?scene=${that.data.scene}&js_code=${that.data.jsCode}`,
    header: {},
    method: "POST",
    success(res) {
    wx.hideLoading();
    that.setData({
    isLogin: true,
    });
    if (res.statusCode == 200) {
    that.setData({
    loginSuccess: true,
    });
    } else {
    if (res.statusCode == 400) {
    wx.showToast({
    icon: "none",
    title: "无效请求",
    });
    } else if (res.statusCode == 500) {
    wx.showToast({
    icon: "none",
    title: "服务内部错误",
    });
    }
    that.setData({
    loginSuccess: false,
    });
    }
    },
    fail: function (e) {
    wx.hideLoading();
    wx.showToast({
    icon: "none",
    title: e,
    });
    },
    });
    },
    });


    scene为随机生成的8位数字


    本地网页核心代码


        let isInit = true
    function loginWx() {
    isInit = false
    refreshQrcode()
    }
    function refreshQrcode() {
    showQrLoading = true
    showInfo = false
    api.get('/qrcode').then(qRes => {
    if (qRes.status == 200) {
    imgSrc = `${BASE_URL}${qRes.data}`
    pollingCount = 0
    startPolling()
    } else {
    showToast = true
    toastMsg = '二维码获取失败,请点击刷新重试'
    showInfo = true
    }
    }).finally(() => {
    showQrLoading = false
    })
    }

    // 开始轮询
    // 1000毫秒轮询一次
    function startPolling() {
    pollingInterval = setInterval(function () {
    pollDatabase()
    }, 1000)
    }
    function pollDatabase() {
    if (pollingCount >= maxPollingCount) {
    clearInterval(pollingInterval)
    showToast = true
    toastMsg = '二维码已失效,请刷新'
    showInfo = true
    return
    }
    pollingCount++
    api.get('/result').then(res => {
    if (res.status == 200) {
    clearInterval(pollingInterval)
    navigate('/os', { replace: true })
    } else if (res.status == 408) {
    clearInterval(pollingInterval)
    showToast = true
    toastMsg = '二维码已失效,请刷新'
    showInfo = true
    }
    })
    }



    html的部分代码如下所示


         <button class="btn" on:click={loginWx}>微信登录</button>
    <div id="qrcode" class="relative mt-10">
    {#if imgSrc}
    <img src={imgSrc} alt="二维码图片"/>
    {/if}
    {#if showQrLoading}
    <div class="mask absolute top-0 left-0 w-full h-full z-10">
    <Loading height="12" width="12"/>
    </div>
    {/if}
    </div>

    尾声


    若需要完整代码,或想知道如何申请微信小程序,欢迎大家关注或私信我哦~~


    附上网页微信授权登录动画、以及小程序登录成功后的截图


    动画.gif


    微信图片_20240401151120.png


    作者:zwf193071
    来源:juejin.cn/post/7351649413401493556
    收起阅读 »

    基于uniapp带你实现了一个好看的轮播图组件

    web
    背景 最近,朋友说在做uniapp微信小程序项目时接到一个需求,需要实现一个类似上图的轮播图效果,问我有没有什么好的实现方案,他也和我说了他的方案,比如让产品直接把图片切成两部分再分别进行上传,但是我觉得这个方案不够灵活,当每次修改banner图片时,都得让产...
    继续阅读 »

    vi-swiper-66.gif


    背景


    最近,朋友说在做uniapp微信小程序项目时接到一个需求,需要实现一个类似上图的轮播图效果,问我有没有什么好的实现方案,他也和我说了他的方案,比如让产品直接把图片切成两部分再分别进行上传,但是我觉得这个方案不够灵活,当每次修改banner图片时,都得让产品把图切好再分别上传。下文将探究一个更好的实现思路。


    微信图片_20240607010418.jpg


    需求分析


    由文章顶部的gif动图,我们可以看出每次执行轮播动画时,只会裁剪图片的中间部分进行滚动,图片的其余部分保持不变,等待轮播动画执行完成后,再淡化背景图片切换到下一张轮播图。


    从中可得出两点关键信息



    1.两种相同图片堆叠在一起,一张背景图(大图),一张轮播图(小图);


    2.需要对图片中间部分进行裁剪,并且定位到刚好能够和背景图重合得区域;



    根据以上得出的信息,我们还需解决两个疑问:



    1.如何对图片进行裁剪?


    2.图片裁剪后如何定位和背景图重合的区域?



    前端裁剪图片可以使用canvans,但是兼容性不好,太麻烦!还有没有好一点的方法呢?当然有,参考css中的雪碧图进行图片裁剪显示!!但还是有些麻烦,还有没有简单的方式呢?有,那就是使用css属性overflow: hidden;进行图片裁剪,下文也主要是讲这个方案。


    开始实现


    vi-swiper.vue


    <template>
    <view class="v-banner" :style="[boxStyle]">
    <swiper class="v-swiper" autoplay indicator-dots circular
    @animationfinish="onAnimationfinish"
    >

    <swiper-item class="v-swiperi-tem" v-for="(url,index) in list" :key="index">
    <image class="v-img" :src="url" mode="scaleToFill"></image>
    </swiper-item>
    </swiper>
    </view>

    </template>

    <script>
    export default {
    props: {
    // 当前索引
    value: {
    type: Number,
    default: 0
    },
    // 轮播图列表
    list: {
    type: Array,
    default: () => []
    }
    },
    computed: {
    boxStyle() {
    return {
    backgroundImage: `url(${this.list[this.value]})`,
    // 开启background-image转场动画
    transition: '1s background-image'
    }
    }
    },
    methods: {
    // 轮播图动画结束后更新底部更新图索引
    onAnimationfinish(e) {
    this.$emit('input', e.detail.current)
    }
    }
    }
    </script>


    <style lang="scss">
    /*sass变量,用于动态计算*/
    $swiperWidth: 650rpx;
    $swiperHeight: 350rpx;
    $verticalPadding: 60rpx;
    $horizontalPadding: 50rpx;
    $imgWidth: $swiperWidth + $horizontalPadding * 2;
    $imgHeight: $swiperHeight + $horizontalPadding * 2;

    .v-banner {
    /* 因为需要根据内边距动态调节背景图宽高,所以设为行内块 */
    display: inline-block;
    // 背景图铺满容器
    background-size: 100% 100%;
    padding: $verticalPadding $horizontalPadding;
    .v-swiper {
    height: $swiperHeight;
    width: $swiperWidth;
    // 裁剪图片
    overflow: hidden;
    .v-swiperi-tem {
    .v-img {
    width: $imgWidth;
    height: $imgHeight;
    }
    }
    }
    }
    </style>


    以上代码主要实现思路是让底部背景图大小和轮播图大小相同使两种重合,尺寸相等才能重合swiper轮播图容器组件固定宽高,使用overflow: hidden;来裁剪内部图片, 然后给底部背景图容器使用padding内边距来撑开容器,达到两种图片堆叠的效果;图片转场通过transition设置动画。


    以上组件页面显示效果如下:


    image.png


    发现两张图片还没有重合在一起,原因是两张图片虽然大小一致了,但是位置不对,如下图所示:


    image.png


    那么由上图描绘的信息可知,想要两张图重合,那么需要把轮播图分别向上和向左移动对应的内边距距离即可,我们可以通过给轮播图设置负的外边距实现,样式如下:


    .v-img {
    ...
    // 使两张图片重合
    margin-top: -$verticalPadding;
    margin-left: -$horizontalPadding;
    }

    效果如下图所示:


    image.png


    到这我们实现了图片的裁剪和重合,已经实现了最终效果。完整的代码会在文章结尾附上。


    另外,我已经把这个组件发布到了uniapp插件市场,并且做了相应封装,可灵活定制轮播图大小及相关样式,感兴趣的可以点击这里:


    vi-swiper轮播图,跳转到文档查阅源码或使用。


    总结


    这个堆叠轮播图效果实现起来不难,主要是要找对思路,一句话概括就是两张大小相等的图片进行重合,轮播图容器对图片进行裁剪


    完整代码


    vi-swiper.vue


    <template>
    <view class="v-banner" :style="[boxStyle]">
    <swiper class="v-swiper" autoplay indicator-dots circular
    @animationfinish="onAnimationfinish"
    >

    <swiper-item class="v-swiperi-tem" v-for="(url,index) in list" :key="index">
    <image class="v-img" :src="url" mode="scaleToFill"></image>
    </swiper-item>
    </swiper>
    </view>

    </template>

    <script>
    export default {
    props: {
    // 当前索引
    value: {
    type: Number,
    default: 0
    },
    // 轮播图列表
    list: {
    type: Array,
    default: () => []
    }
    },
    computed: {
    boxStyle() {
    return {
    backgroundImage: `url(${this.list[this.value]})`,
    // 开启background-image转场动画
    transition: '1s background-image'
    }
    }
    },
    methods: {
    // 轮播图动画结束后更新底部更新图索引
    onAnimationfinish(e) {
    this.$emit('input', e.detail.current)
    }
    }
    }
    </script>


    <style lang="scss">
    /*sass变量,用于动态计算*/
    $swiperWidth: 650rpx;
    $swiperHeight: 350rpx;
    $verticalPadding: 60rpx;
    $horizontalPadding: 50rpx;
    $imgWidth: $swiperWidth + $horizontalPadding * 2;
    $imgHeight: $swiperHeight + $horizontalPadding * 2;

    .v-banner {
    /* 因为需要根据内边距动态调节背景图宽高,所以设为行内块 */
    display: inline-block;
    // 背景图铺满容器
    background-size: 100% 100%;
    padding: $verticalPadding $horizontalPadding;
    .v-swiper {
    height: $swiperHeight;
    width: $swiperWidth;
    // 裁剪图片
    overflow: hidden;
    .v-swiperi-tem {
    .v-img {
    width: $imgWidth;
    height: $imgHeight;
    margin-top: -$verticalPadding;
    margin-left: -$horizontalPadding;
    }
    }
    }
    }
    </style>


    作者:vilan_微澜
    来源:juejin.cn/post/7377245069474021412
    收起阅读 »

    Java 泛型中的通配符 T,E,K,V,?有去搞清楚吗?

    前言不久前,被人问到Java 泛型中的通配符 T,E,K,V,? 是什么?有什么用?这不经让我有些回忆起该开始学习Java那段日子,那是对泛型什么的其实有些迷迷糊糊的,学的不这么样,是在做项目的过程中,渐渐有又看到别人的代码、在看源码的时候老是遇见,之后就专门...
    继续阅读 »

    前言

    不久前,被人问到Java 泛型中的通配符 T,E,K,V,? 是什么?有什么用?这不经让我有些回忆起该开始学习Java那段日子,那是对泛型什么的其实有些迷迷糊糊的,学的不这么样,是在做项目的过程中,渐渐有又看到别人的代码、在看源码的时候老是遇见,之后就专门去了解学习,才对这几个通配符 T,E,K,V,?有所了解。

    泛型有什么用?

    在介绍这几个通配符之前,我们先介绍介绍泛型,看看泛型带给我们的好处。
    Java泛型是JDK5中引入的一个新特性,泛型提供了编译是类型安全检测机制,这个机制允许开发者在编译是检测非法类型。泛型的本质就是参数化类型,就是在编译时对输入的参数指定一个数据类型。

    1. 类型安全:编译是检查类型是否匹配,避免了ClassCastexception的发生。
    // 非泛型写法(存在类型转换风险)
    List list1 = new ArrayList();
    list1.add("a");
    Integer num = (Long) list1.get(0); // 运行时抛出 ClassCastException

    // 泛型写法(编译时检查类型)
    List list2 = new ArrayList<>();
    // list.add(1); // 编译报错
    list2.add("a");
    String str = list2.get(0); // 无需强制转换
    1. 消除代码强制类型转换:减少了一些类型转换操作。
    // 非泛型写法
    Map map1 = new HashMap();
    map1.put("user", new User());
    User user1 = (User) map1.get("user");

    // 泛型写法
    Map map2 = new HashMap<>();
    map2.put("user", new User());
    // 自动转换
    User user2 = map2.get("user");

    3.代码复用:可以支持多种数据类型,不要重复编写代码,例如:我们常用的统一响应结果类。

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class Result {
    /**
    * 响应状态码
    */

    private int code;

    /**
    * 响应信息
    */

    private String message;

    /**
    * 响应数据
    */

    private T data;

    /**
    * 时间戳
    */

    private long timestamp;
    其他代码省略...
    1. 增强可读性:通过类型参数就直接能看出要填入什么类型。
    List list = new ArrayList<>();

    泛型里的通配符

    我们在使用泛型的时候,经常会使用或者看见多种不同的通配符,常见的 T,E,K,V,?这几种,相信大家一定不陌生,但是真的问你他们有什么作用?有什么区别时,很多人应该是不能很好的介绍它们的,接下来我就来给大家介绍介绍。

    T,E,K,V

    1. T(Type) T表示任意类型参数,我们举个例子
    pubile class A{
    prvate T t;
    //其他省略...
    }

    //创建一个不带泛型参数的A
    A a = new A();
    a.set(new B());
    B b = (B) a.get();//需要进行强制类型转换

    //创建一个带泛型参数的A
    A a = new A();
    a.set(new B());
    B b = a.get();
    1. E(Element) E表示集合中的元素类型
    List list = new ArrayList<>();
    1. K(Key) K表示映射的键的数据类型
    Map map = new HashMap<>();
    1. V(Value) V表示映射的值的数据类型
    Map map = new HashMap<>();

    通配符 ?

    1. 无界通配符 表示未知类型,接收任意类型
       // 使用无界通配符处理任意类型的查询结果
    public void logQueryResult(List resultList) {
    resultList.forEach(obj -> log.info("Result: {}", obj));
    }
    1. 上界通配符 表示类型是T或者是子类
     // 使用上界通配符读取缓存
    public extends Serializable> T getCache(String key, Class clazz) {
    Object value = redisTemplate.opsForValue().get(key);
    return clazz.cast(value);
    }
    1. 下界通配符 表示类型是T或者是父类
      // 使用下界通配符写入缓存
    public void setCache(String key, super Serializable> value) {
    redisTemplate.opsForValue().set(key, value);
    }


    综合示例:

    import java.util.ArrayList;
    import java.util.List;

    public class Demo {
    //实体类
    class Animal {
    void eat() {
    System.out.println("Animal is eating");
    }
    }

    class Dog extends Animal {
    @Override
    void eat() {
    System.out.println("Dog is eating");
    }
    }

    class Husky extends Dog {
    @Override
    void eat() {
    System.out.println("Husky is eating");
    }
    }

    /**
    * 无界通配符
    */

    // 只能读取元素,不能写入(除null外)
    public static void printAllElements(List list) {
    for (Object obj : list) {
    System.out.println(obj);
    }
    // list.add("test"); // 编译错误!无法写入具体类型
    list.add(null); // 唯一允许的写入操作
    }

    /**
    * 上界通配符
    */

    // 安全读取为Animal,但不能写入(生产者场景)
    public static void processAnimals(List animals) {
    for (Animal animal : animals) {
    animal.eat();
    }
    // animals.add(new Dog()); // 编译错误!无法确定具体子类型
    }

    /**
    * 下界通配符
    */

    // 安全写入Dog,读取需要强制转换(消费者场景)
    public static void addDogs(Listsuper Dog> dogList) {
    dogList.add(new Dog());
    dogList.add(new Husky()); // Husky是Dog子类
    // dogList.add(new Animal()); // 编译错误!Animal不是Dog的超类

    Object obj = dogList.get(0); // 读取只能为Object
    if (obj instanceof Dog) {
    Dog dog = (Dog) obj; // 需要显式类型转换
    }
    }

    public static void main(String[] args) {
    // 测试无界通配符
    List strings = List.of("A", "B", "C");
    printAllElements(strings);

    List integers = List.of(1, 2, 3);
    printAllElements(integers);

    // 测试上界通配符
    List dogs = new ArrayList<>();
    dogs.add(new Dog());
    processAnimals(dogs);

    List huskies = new ArrayList<>();
    huskies.add(new Husky());
    processAnimals(huskies);

    // 测试下界通配符
    List animals = new ArrayList<>();
    addDogs(animals);
    System.out.println(animals);

    List objects = new ArrayList<>();
    addDogs(objects);
    }
    }

    我们需要清楚,这些只是我们开发过程中约定,不是强制规定,但遵循它们可以提高代码的可读性。



    我们在很多时候只是单纯的会使用某些技术,但是对它们里面许许多多常见的都是一知半解的,只是会使用确实很重要,但是如果有时间,我们不妨好好的在对这些技术进行深入学习,不仅知其然,而且知其所以然,这样我们的技术才会不断提升进步。


    作者:镜花水月linyi
    来源:juejin.cn/post/7475629913329008649

    总结

    收起阅读 »

    在村里上班的程序员

    今天是在山上办公的第二天,现在时间是晚上的10点36分,刚刚写完今日份工作日报又刷10分钟手机的我决定写上一篇日记简单记下最近两天我的日常。 我的上班,是跟着国家法定节假日安排走的,前天初八,是我25年年后上班的第一天。 阿妮的假期还未结束,她决定回一趟娘家,...
    继续阅读 »

    今天是在山上办公的第二天,现在时间是晚上的10点36分,刚刚写完今日份工作日报又刷10分钟手机的我决定写上一篇日记简单记下最近两天我的日常。


    我的上班,是跟着国家法定节假日安排走的,前天初八,是我25年年后上班的第一天。


    阿妮的假期还未结束,她决定回一趟娘家,由此,我在初八一大早——7点半到8点50分——先送阿妮进城,送到她的外公外婆家。接着,我在外公外婆家办公一天。


    图片


    初八那天的午饭


    大嘎嘎(外公)小嘎嘎(外婆)总是很欢迎我和阿妮的到来,午饭晚饭,我连连摇头不停摆手依然吃得很撑。


    我干活的节奏,和年前是一样的:上午9点到12点多些,下午2点到6点7点,晚上8点9点到10点更多些,都是我的干活——单纯指做工作上的事情——时间。


    阿妮晚上约了她的初中同学一起聚餐,我带上电脑跟着大家一起。烧烤桌上,对着电脑试用VS Code新插件Cline的我,是没有感受到心理压力的;烧烤桌上的干活时间,我是专注的。


    --


    初九一大早,我从县城回家回到山上,带着两个帽子和几个包子。帽子是初八晚上一起逛楼下街道时阿妮买给我父亲母亲的,包子,是我自己想吃再给父亲母亲带着的。


    初九是个很好日子,返程路上,我碰见的接亲队伍有4个。


    初九的天气,很有些特色,上午飘雪,下午大太阳,晚上很有些冷。我在山上的上班,上午烤电火,下午太阳下干活,晚上的干活,有一部分是洗漱完毕躺床上完成的。


    图片


    我的办公桌椅


    昨天的在家办公体验不错,但我需要对自己诚实些,当时我的内心,有两个小小担忧存在。


    第一个关于当前的环境,下午父母外出吃酒只留我一个人在家,我家近处只有邻居一户。到了夜晚,整个世界很安静,安静到我能听见自己的呼吸,我的一个小小动作都能发出很大声响,我承认自己理智相信科学,但同时我也看过许多志怪故事。对的,晚上一个人在家,我很有些害怕。


    第二个是工作内容,我总觉得自己近来的效率并没有很高。其实不太对,其实效率应该是ok的,不ok的是我关于AI技术的知识储备太少,而我并没有给自己一个好的计划去每天进步一点点。emmm~其实还是有些不太对,这个概念有些大,更准确的是,我感觉自己在有些工作内容上,还可以做的好一些。


    晚上11点又过10分钟,背完单词的我睡下,脑子中那轻微恐惧并不散去,直到深睡。


    很神奇的是,今天早上6点多醒来,那对于安静的害怕,一点都不剩下。


    --


    整个过年前后,我都感觉自己很忙,忙到已经坚持周更四年多的公众号都不能在周日及时更新。今天即便晚睡也想要写上一篇日记的情绪,来源于下午6点发生的一件,一件让我感觉有些窘迫的事情。


    伯娘进门问我:”你们作料放啊哪儿的呢?“


    ”你老汉喊我来给你煮一碗面条。我给他说,叫你到我们那里去吃,你老汉儿说你走不开哈,那我就来给你煮面条了啊。“


    当时我真的很窘迫,感觉很有些尴尬,而同时内心,又会有些开心情绪蔓延。


    我是很想给自己找一个借口的:我以为父母吃酒,会在下午三点左右回家。我的早餐,是昨天剩下的两个肉包;我的午餐,是一碗加了母亲自制麻糖的泡阴米子,再加一个橙子与一个苹果。我认为肉包、橙子与苹果,是可以撑到母亲回家的。


    工作日,我真的不想做饭。


    即便并不很饿,但我确实有让阿雪对母亲说:“你再不回家,你的大娃娃和你的喂的猪,都要饿趴了哦。”


    图片


    很是粘人的狗子


    对的,今天干活间隙,我有喂猪——猪食当然是母亲提前煮好的,遛狗——我只需要前面走着,狗自然在后面跟着。


    伯娘烧火又煮面,花半小时。


    我为面条,拍了一张好看照片。


    图片


    好吃的清汤面与好看的照片


    时间已经是晚上的11点20分,且快快睡觉。


    今晚,我一点都不害怕。


    作者:我要改名叫嘟嘟
    来源:juejin.cn/post/7468970428918906906
    收起阅读 »

    入职第一天,看了公司代码,牛马沉默了

    入职第一天就干活的,就问还有谁,搬来一台N手电脑,第一分钟开机,第二分钟派活,第三分钟干活,巴适。。。。。。打开代码发现问题不断读取配置文件居然读取两个配置文件,一个读一点,不清楚为什么不能一个配置文件进行配置 一边获取WEB-INF下的配置文件,一...
    继续阅读 »

    入职第一天就干活的,就问还有谁,搬来一台N手电脑,第一分钟开机,第二分钟派活,第三分钟干活,巴适。。。。。。

    4f7ca8c685324356868f65dd8862f101~tplv-k3u1fbpfcp-jj-mark_3024_0_0_0_q75.jpg

    打开代码发现问题不断

    1. 读取配置文件居然读取两个配置文件,一个读一点,不清楚为什么不能一个配置文件进行配置

    image.png

    image.png

    image.png 一边获取WEB-INF下的配置文件,一边用外部配置文件进行覆盖,有人可能会问既然覆盖,那可以全在外部配置啊,问的好,如果全用外部配置,咱们代码获取属性有的加上了项目前缀(上面的两个put),有的没加,这样配置文件就显得很乱不可取,所以形成了分开配置的局面,如果接受混乱,就写在外部配置;不能全写在内部配置,因为

    prop_c.setProperty(key, value);

    value获取外部配置为空的时候会抛出异常;properties底层集合用的是hashTable

    public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
    throw new NullPointerException();
    }
    }
    1. 很多参数写死在代码里,如果有改动,工作量会变得异常庞大,举例权限方面伪代码
    role.haveRole("ADMIN_USE")
    1. 日志打印居然sout和log混合双打

    image.png

    image.png

    先不说双打的事,对于上图这个,应该输出包括堆栈信息,不然定位问题很麻烦,有人可能会说e.getMessage()最好,可是生产问题看多了发现还是打堆栈好;还有如果不是定向返回信息,仅仅是记录日志,完全没必要catch多个异常,一个Exception足够了,不知道原作者这么写的意思是啥;还是就是打印日志要用logger,用sout打印在控制台,那我日志文件干啥;

    4.提交的代码没有技术经理把关,下发生产包是个人就可以发导致生产环境代码和本地代码或者数据库数据出现不一致的现象,数据库数据的同步是生产最容易忘记执行的一个事情;比如我的这家公司上传文件模板变化了,但是没同步,导致出问题时开发环境复现问题真是麻烦;

    5.随意更改生产数据库,出不出问题全靠开发的职业素养;

    6.Maven依赖的问题,Maven引pom,而pom里面却是另一个pom文件,没有生成的jar供引入,是的,我们可以在dependency里加上

    <type>pom

    来解决这个问题,但是公司内的,而且实际也是引入这个pom里面的jar的,我实在不知道这么做的用意是什么,有谁知道;求教 a972880380654b389246a3179add2cca~tplv-k3u1fbpfcp-jj-mark_3024_0_0_0_q75.jpg

    以上这些都是我最近一家公司出现的问题,除了默默接受还能怎么办;

    那有什么优点呢:

    1. 不用太怎么写文档
    2. 束缚很小
    3. 学到了js的全局调用怎么写的(下一篇我来写,顺便巩固一下)

    解决之道

    怎么解决这些问题呢,首先对于现有的新项目或升级的项目来说,spring的application.xml/yml 完全可以写我们的配置,开发环境没必要整外部文件,如果是生产环境我们可以在脚本或启动命令添加 nohup java -Dfile.encoding=UTF-8 -Dspring.config.location=server/src/main/config/application.properties -jar xxx.jar & 来告诉jar包引哪里的配置文件;也可以加上动态配置,都很棒的,

    其次就是规范代码,养成良好的规范,跟着节奏,不要另辟蹊径;老老实实的,如果原项目上迭代,不要动源代码,追加即可,没有时间去重构的;

    我也曾是个快乐的童鞋,也有过崇高的理想,直到我面前堆了一座座山,脚下多了一道道坑,我。。。。。。!


    作者:小红帽的大灰狼
    来源:juejin.cn/post/7371986999164928010
    收起阅读 »

    React:我做出了一个违背祖训的决定!

    web
    React 的 useEffect,大家都熟吧?这玩意儿就像个万金油,啥副作用都能往里塞:取数据、搞订阅、手动操作 DOM……反正渲染完了,它帮你擦屁股。它帮你擦屁股。 但是!React 团队最近搞了个大新闻,他们居然要对 useEffect 动刀子了!而且,...
    继续阅读 »

    image.png


    React 的 useEffect,大家都熟吧?这玩意儿就像个万金油,啥副作用都能往里塞:取数据、搞订阅、手动操作 DOM……反正渲染完了,它帮你擦屁股。它帮你擦屁股。


    但是!React 团队最近搞了个大新闻,他们居然要对 useEffect 动刀子了!而且,这次的改动,用他们的话说,简直是——“违背祖训”!“违背祖训”!


    useEffect 要变身?实验性 CRUD 支持来了!


    新的 useEffect 签名,整合了以前一个实验性的 Hook useResourceEffect 的功能,现在长这样:


    function useEffect(
    create: (() => (() => void) | void) | (() => {...} | void | null),
    createDeps: Array<mixed> | void | null,
    update?: ((resource: {...} | void | null) => void) | void,
    updateDeps?: Array<mixed> | void | null,
    destroy?: ((resource: {...} | void | null) => void) | void,
    ): void

    是不是看得一脸懵逼?别慌,我来给你翻译翻译。


    以前的 useEffect,创建和清理都挤在一个函数里,跟两口子似的,难舍难分。举个栗子:


    useEffect(() => {
    // 创建阶段:发起请求
    const controller = new AbortController();
    fetch('/api/data', { signal: controller.signal })
    .then(response => response.json())
    .then(data => setData(data));

    // 清理阶段:取消请求
    return () => {
    controller.abort();
    };
    }, [someDependency]);

    看到了吧?创建(发起请求)和清理(取消请求)都得写在一个函数里。


    现在好了,React 团队直接把它们拆散了!新签名里,创建、更新、销毁,各司其职,清清楚楚:



    • create: 专门用来造东西(比如,发个请求,整个订阅)。

    • createDeps: create 的跟屁虫,它们一变,create 就得重新执行。

    • update (可选): 想更新?找它!它会拿着 create 造出来的东西,给你更新。

    • updateDeps (可选): update 的小弟,它们一变,update 就得带着老东西,重新来过。

    • destroy: 可选的销毁时候的回调。


    “祖宗之法不可变”?React:我就变!


    自从 Hook 在 2016 年推出,到现在已经九年了!九年啊!“组合优于继承”、“函数式编程”,这些 React 的“祖训”,各路大神、大V,哪个没给你讲过几百遍?哪个没给你讲过几百遍?


    useEffect 把创建和清理揉在一起,也算是“组合”的一种体现,深入人心。可现在呢?React 团队居然亲手把它拆了!这……这简直是自己打自己的脸啊!


    不过,话说回来,这种拆分,对于那些复杂的副作用,确实更清晰、更好管理。以前,你可能得在一个 useEffect 里写一堆 if...else,现在,你可以把它们放到不同的阶段,代码更清爽,逻辑更分明。


    注意!前方高能预警!


    这个 CRUD 功能,现在还是个“试验品”,React 团队还没打算把它放出来。你要是头铁,非要试试,记得先去把 feature flag 打开。不然,你会看到这个:


    useEffect CRUD overload is not enabled in this build of React.

    重要的事情说三遍:这都是猜的!猜的!猜的!猜的!猜的!猜的!


    现在,关于这个新特性,React 团队还没放出任何官方文档或者 RFC。所以,这篇文章,你看看就好,别太当真。它就是基于代码瞎猜的。等官方消息出来了,咱们再好好研究!


    作者:锈儿海老师
    来源:juejin.cn/post/7470819965014474771
    收起阅读 »

    为什么程序员痴迷于错误信息上报?

    前言上一篇已经聊过日志上报的调度原理,讲述如何处理日志上报堆积、上报失败以及上报优化等方案。从上家公司开始,监控就由我们组身强体壮的同事来负责,而我只能开发Admin和H5;经过一系列焦虑的面试后,咸鱼翻身,这辈子我也做上监控了。千万不要以为我是因为监控的重要...
    继续阅读 »

    前言

    上一篇已经聊过日志上报的调度原理,讲述如何处理日志上报堆积、上报失败以及上报优化等方案。

    从上家公司开始,监控就由我们组身强体壮的同事来负责,而我只能开发AdminH5;经过一系列焦虑的面试后,咸鱼翻身,这辈子我也做上监控了。千万不要以为我是因为监控的重要性才这么执着,人往往得不到的东西才是最有吸引力的。

    在写这篇文章时,我也在思考,为什么走到哪里都会有一群程序员喜欢封装监控呢?即使换个公司、换个组,依然可能需要有人来迭代监控。嗯,话不多说,先点关注,正文开始

    错误监控的核心价值

    如果让你封装一个前端监控,你会怎么设计监控的上报优先级?

    对于一个网页来说,能否带给用户好的体验,核心指标就是 白屏时长 和 FMP时长,这两项指标直接影响用户的 留存率 和 体验

    下面通过数据加强理解:

    • 白屏时间 > 3秒 导致用户流失率上升47%
    • 接口错误率 > 0.5%  造成订单转化率下降23%
    • JS错误数 > 1/千次访问 预示着系统稳定性风险

    设想一下,当你访问页面时白屏等待了3秒,并且页面没有骨架屏或者Loading态时,你会不会觉得这个页面挂了?这时候如果我们的监控优先关注的是性能,可能用户已经退出了,我们的上报还没调用到。

    在这个白屏等待的过程中,JS Error可能已经打印在控制台了,接口可能已经返回了错误信息,但是程序员却毫无感知。

    优先上报错误信息,本质是为了提升生产环境的错误响应速度、减少生产环境的损失、提高上线流程的规范。以下是错误响应的黄金时间轴:

    时间窗口响应动作业务影响
    < 1分钟自动熔断异常接口避免错误扩散
    1-5分钟触发告警通知值班人员降低MTTR(平均修复时间)
    >5分钟生成故障诊断报告优化事后复盘流程

    重要章节

    一:错误类型,你需要关注的五大场景

    技术本质:任何错误收集系统都需要先明确错误边界。前端错误主要分为两类: 显性错误(直接阻断执行)和 隐性错误(资源加载、异步异常等)。

    // 显性错误(同步执行阶段)
    function criticalFunction() {
    undefinedVariable.access(); // ReferenceError
    }

    // 隐性错误(异步场景)
    fetchData().then(() => {
    invalidJSON.parse(); // 异步代码中的错误
    });

    关键分类: 通过错误本质将前端常见错误分为5种类型,图示如下。 image.png

    1. 语法层错误(SyntaxError)
      ESLint 可拦截,但运行时需注意动态语法(如 eval,这个用法不推荐)。
    2. 运行时异常
      错误的时机场景大部分是在页面渲染完成后,用户对页面发生交互行为,触发JS执行异常。以下是模拟报错的一个例子,用于学习。 // 典型场景 element.addEventListener('click', () => { throw new Error('Event handler crash'); });
    3. 资源加载失败
      常见的资源比如图片、JS脚本、字体文件、外链引入的三方依赖等。我们可以通过全局监听处理,比如使用document.addEventListener('error', handler, true)来捕获资源加载失败的情况。但需要注意以下几点:

    收起阅读 »