普通视图

发现新文章,点击刷新页面。
今天 — 2026年2月1日技术

CSS3动画使用教程

2026年2月1日 12:37

你想要一份更详细、更系统的CSS3动画(animation)使用教程,我会从基础概念到实战案例,一步步拆解CSS3动画的核心用法,让你能轻松上手并灵活运用。

CSS3 动画(animation)全教程

CSS3动画是通过@keyframes定义关键帧,再通过animation属性将动画应用到元素上,能实现比transition更复杂、更灵活的动态效果,且无需JavaScript参与。

一、核心概念理解

在开始写代码前,先理清两个核心部分:

  1. @keyframes(关键帧) :定义动画的"关键状态",比如起始、中间、结束时元素的样式。
  2. animation 属性:将定义好的关键帧动画应用到元素上,并设置动画的时长、循环、速度等规则。

二、基础语法与步骤

步骤1:定义关键帧(@keyframes)
/* 格式:@keyframes 动画名称 { 关键帧规则 } */
@keyframes 动画名称 {
  /* 0% 表示动画开始(也可以用 from 替代) */
  0% {
    /* 起始样式 */
    transform: translateX(0);
    opacity: 0;
  }
  /* 50% 表示动画进行到一半 */
  50% {
    /* 中间样式 */
    transform: translateX(100px);
    opacity: 1;
  }
  /* 100% 表示动画结束(也可以用 to 替代) */
  100% {
    /* 结束样式 */
    transform: translateX(200px);
    opacity: 0;
  }
}
步骤2:应用动画(animation 属性)
/* 给元素添加动画 */
.animated-box {
  width: 100px;
  height: 100px;
  background: #007bff;
  
  /* 核心:animation 复合属性(推荐) */
  /* 格式:动画名称 时长 速度曲线 延迟 循环次数 方向 填充模式 播放状态 */
  animation: 动画名称 2s ease 0.5s infinite alternate forwards running;
  
  /* 也可以拆分为单个属性(便于理解和调试) */
  /* animation-name: 动画名称;          // 必选:指定关键帧名称 */
  /* animation-duration: 2s;            // 必选:动画时长(默认0,无效果) */
  /* animation-timing-function: ease;  // 可选:速度曲线(默认ease) */
  /* animation-delay: 0.5s;             // 可选:延迟播放(默认0) */
  /* animation-iteration-count: infinite; // 可选:循环次数(默认1,infinite无限) */
  /* animation-direction: alternate;    // 可选:播放方向(默认normal) */
  /* animation-fill-mode: forwards;     // 可选:动画结束后样式(默认none) */
  /* animation-play-state: running;     // 可选:播放状态(默认running,paused暂停) */
}

三、关键属性详解(必掌握)

1. 速度曲线(animation-timing-function)

控制动画的播放速度,常用值:

/* 常用值示例 */
.animated-box {
  /* linear:匀速(最常用) */
  animation-timing-function: linear;
  /* ease:慢→快→慢(默认) */
  /* ease-in:慢→快 */
  /* ease-out:快→慢 */
  /* ease-in-out:慢→快→慢(比ease更平缓) */
  /* 自定义贝塞尔曲线(精准控制) */
  /* animation-timing-function: cubic-bezier(0.1, 0.7, 1.0, 0.1); */
}
2. 播放方向(animation-direction)

控制动画是否反向播放:

.animated-box {
  /* normal:正常播放(默认),从0%→100% */
  /* alternate:交替播放,奇数次正向(0%→100%),偶数次反向(100%→0%) */
  /* reverse:反向播放(100%→0%) */
  /* alternate-reverse:反向交替播放 */
  animation-direction: alternate;
}
3. 填充模式(animation-fill-mode)

控制动画开始前/结束后的元素样式:

.animated-box {
  /* none:默认,动画结束后回到初始样式 */
  /* forwards:动画结束后,保持最后一帧样式 */
  /* backwards:动画延迟期间,保持第一帧样式 */
  /* both:同时应用forwards和backwards */
  animation-fill-mode: forwards;
}
4. 播放状态(animation-play-state)

常用于通过:hover、JS控制动画暂停/播放:

.animated-box {
  animation: move 2s infinite;
}
/* 鼠标悬停时暂停动画 */
.animated-box:hover {
  animation-play-state: paused;
}

四、实战案例(直接复用)

案例1:呼吸灯效果(透明度变化)
/* 定义关键帧 */
@keyframes breathe {
  0% {
    opacity: 1;
    transform: scale(1);
  }
  50% {
    opacity: 0.5;
    transform: scale(1.1);
  }
  100% {
    opacity: 1;
    transform: scale(1);
  }
}

/* 应用动画 */
.breathe-box {
  width: 80px;
  height: 80px;
  background: #ff6700;
  border-radius: 50%;
  /* 匀速、无限循环 */
  animation: breathe 2s linear infinite;
}
案例2:加载动画(旋转+多元素)
<!-- HTML结构 -->
<div class="loader">
  <div class="loader-item"></div>
  <div class="loader-item"></div>
  <div class="loader-item"></div>
</div>
/* 定义旋转关键帧 */
@keyframes load {
  0% {
    transform: translateY(0);
    opacity: 0.8;
  }
  50% {
    transform: translateY(-20px);
    opacity: 0.2;
  }
  100% {
    transform: translateY(0);
    opacity: 0.8;
  }
}

.loader {
  display: flex;
  gap: 8px;
  justify-content: center;
  align-items: center;
  height: 100px;
}

.loader-item {
  width: 12px;
  height: 30px;
  background: #007bff;
  border-radius: 6px;
  animation: load 1.2s ease infinite;
}
/* 给每个小球设置不同延迟,实现错落效果 */
.loader-item:nth-child(2) {
  animation-delay: 0.2s;
}
.loader-item:nth-child(3) {
  animation-delay: 0.4s;
}
案例3:文字渐入动画
@keyframes text-fade {
  0% {
    transform: translateY(20px);
    opacity: 0;
  }
  100% {
    transform: translateY(0);
    opacity: 1;
  }
}

.fade-text {
  font-size: 24px;
  animation: text-fade 0.8s ease forwards;
  /* 初始状态隐藏 */
  opacity: 0;
}

五、使用注意事项

  1. 性能优化:优先使用transformopacity属性做动画(浏览器硬件加速,无重绘),避免用widthheighttopleft(会触发频繁重排,卡顿)。
  2. 兼容性:现代浏览器(Chrome/Firefox/Safari/Edge)均支持CSS3动画,无需加前缀;如需兼容老旧浏览器(如IE10-),可加前缀-webkit-(如@-webkit-keyframes-webkit-animation)。
  3. 动画暂停:通过animation-play-state: paused暂停动画,比重新设置时长为0更优雅。

总结

  1. 核心结构:CSS3动画由@keyframes(定义关键帧)和animation(应用动画)两部分组成,animation-duration是必选属性(否则无动画效果)。
  2. 常用属性animation-iteration-count: infinite(无限循环)、animation-direction: alternate(交替播放)、animation-fill-mode: forwards(保持结束样式)是高频组合。
  3. 性能原则:动画优先操作transformopacity,避免触发页面重排,保证动画流畅。

你可以把这些案例代码复制到HTML文件中运行,修改关键帧的样式、动画时长、速度曲线等参数,直观感受不同设置的效果,很快就能熟练掌握。如果想实现某个特定的动画效果(比如弹跳、滑动、闪烁),可以告诉我,我会针对性给出代码。

你的 sideEffects 真的配对了吗?—— 深度拆解构建工具的 Tree-shaking 潜规则

作者 donecoding
2026年2月1日 12:13

🚀 省流助手(速通结论):

  1. sideEffects 是给宿主(用你包的项目)看的声明,不是给你自己构建减重用的。
  2. 只要包里包含 CSS/样式全局监听process.on)或修改全局变量绝不能简单设为 false
  3. /* @__PURE__ */ 的意思是  “这行没用到请删掉” ,而不是 “不能删”。
  4. Bundle 并不安全:即便你打包成了单文件,一旦声明了 false,宿主打包工具依然能从内部“抠掉”你的副作用代码。

一、 线上“失踪”案:谁偷走了我的初始化逻辑?

很多开发者都遇到过这种诡异场景:本地开发时一切正常的全局监听(如 process.on('exit'))或样式文件,发布成 npm 包被别人使用后,在生产环境竟然“失效”了。

检查代码,逻辑都在;检查产物,文件也引了。最后发现,根源竟然是你在 package.json 中随手写下的那行:

json

"sideEffects": false

请谨慎使用此类代码。

你以为是在帮宿主做性能优化,实际上你是在给自己的代码下“逐客令”。

二、 生效时刻:它是谁的“紧箍咒”?

误区:  认为在库里写了 sideEffects: false,自己执行 vite build 时包体积就会变小。

真相:它的真正战场是「宿主编译时刻」。

  1. 自身构建时:当你运行构建指令时,工具遵循作者意图。只要你在入口写了 import './effect.ts',这段代码就会物理存在于你的 dist 产物中。
  2. 宿主打包时:当其他项目安装了你的包,宿主工具(Vite/Webpack)会读取你的声明。如果你承诺了“无副作用”,一旦宿主没引用你该模块导出的变量,工具就会开启“外科手术”:即使你的单文件 Bundle 物理上包含了这段代码,工具也会在最终输出时将其精准剔除。

三、 穿透 Bundle 的“外科手术”

这是最隐蔽的陷阱。很多开发者认为:“我打包时已经把副作用合并进 index.js 了,宿主引用了 index.js 就安全了。”

错了。  现代打包工具具备 Module Concatenation(模块提升)  能力。它们能“看穿” Bundle 内部的结构。只要你声明了 false,它们有能力从一个大的文件块中只“抠”出用到的函数,而把剩下的(包括那段 import './effect.ts' 产生的内容)当作垃圾直接丢弃。

四、 微观博弈:/* @__PURE__ */ 到底在帮谁?

如果说 sideEffects 是文件级的“粗调”,那么 /* @__PURE__ */ 就是语句级的“微操”。

纠正一个常见误区:  它是标记“可以删”,而不是“不能删”。

假设你的工具库有一个文件导出了 100 个函数,宿主只用了其中 1 个。

  • 如果没有标记:剩下的 99 个导出中,如果包含 export const config = init() 这种函数执行,打包工具会因为不敢确定 init() 是否修改了全局变量而保守地保留这一行。
  • 如果加上标记:你是在给工具发“免责声明”。工具看到 /* @__PURE__ */,发现没人用 config,就会放心地把这一行代码从产物中抹除。

五、 避坑总结:白名单管理

为了不让代码被“误杀”,你不能在包含副作用的文件里写 false。最专业的做法是使用数组进行精准保护

哪些文件必须进 sideEffects 数组?

  1. 样式文件*.css*.scss
  2. 环境初始化:修改 global 或 window 的脚本。
  3. 进程监控:包含 process.on 或 interval 的逻辑。

推荐配置:

json

{
  "sideEffects": [
    "**/*.css",
    "./dist/_init/*.mjs"
  ]
}

请谨慎使用此类代码。

结语

Tree-shaking 是一场开发者与构建工具之间的博弈。工具的本质是“保守”的,而 sideEffects: false 是你交给工具的一把“激进”的剪刀。

在下一篇中,我们将深入探讨:如何通过工程架构设计,强制开发者在编写副作用代码时进行“决策”,从而构建一套永远不会被意外误删的“契约式”架构。

Vue<前端页面版本检测>

2026年2月1日 14:13

为什么需要版本检测

1. 解决浏览器缓存问题

  • 静态资源缓存:浏览器会缓存 JS、CSS 等静态资源,用户可能继续使用旧版本
  • 用户体验影响:用户无法及时获取新功能,导致功能缺失或操作异常

2. 保障功能一致性

  • 功能同步:确保所有用户都能使用最新的功能和修复
  • 数据一致性:避免因版本差异导致的数据不一致问题

3. 提升用户体验

  • 主动提醒:在新版本发布后主动通知用户更新
  • 无缝升级:减少用户手动刷新页面的需求

版本检测核心思路

version1.gif

整体架构

构建阶段 → 版本文件生成 → 运行时检测 → 版本对比 → 用户提醒

技术实现要点

1. 版本标识生成

  • 构建时生成:每次打包时生成唯一的版本标识
  • 时间戳方案:使用时间戳确保每次构建版本号唯一

2. 版本文件部署

  • JSON 格式:将版本信息保存为 version.json 文件
  • 静态访问:通过 HTTP 请求可直接访问版本文件

3. 客户端检测机制

  • 定时轮询:定期检查服务器版本文件
  • 版本对比:比较本地缓存版本与服务器版本
  • 智能提醒:仅在版本不一致时提醒用户

版本检测实现步骤

步骤一:构建版本文件生成脚本

创建 build-version.js 文件:

// build-version.js (自动生成版本文件脚本)
const fs = require('fs')
const path = require('path')

// 方案A:使用时间戳作为版本标识(最简单,确保每次打包唯一)
const version = new Date().getTime().toString()

// 版本文件内容
const versionJson = {
  version: version,
  updateTime: new Date().toLocaleString() // 可选:添加更新时间,便于排查
}

// 写入version.json文件(项目根目录)
const versionPath = path.resolve(__dirname, 'public', 'version.json')
fs.writeFileSync(versionPath, JSON.stringify(versionJson, null, 2), 'utf-8')

console.log(`✅ 自动生成版本文件成功,版本号:${version}`)

步骤二:修改构建命令

在 package.json 中修改构建命令:

{
  "scripts": {
    "build:prod": "node build-version.js && vue-cli-service build"
  }
}

步骤三:配置 Vue 构建过程

在 vue.config.js 中添加版本文件复制配置:

chainWebpack(config) {
  // ... 其他配置
  
  // 复制 version.json 到 dist 目录
  config.plugin('copy')
    .tap(args => {
      const hasVersionJson = args[0].some(item => item.from === 'version.json')
      if (!hasVersionJson) {
        args[0].push({
          from: path.resolve(__dirname, 'public/version.json'),
          to: path.resolve(__dirname, 'dist/version.json')
        })
      }
      return args
    })
}

步骤四:实现版本检测工具类

创建 src/utils/versionUpdate.js

// src/utils/versionUpdate.js
import { Notification } from 'element-ui'
/**
 * 版本更新检测工具类(仅生产环境启用轮询,内置环境判断)
 */
class VersionUpdate {
  constructor(options = {}) {
    this.config = {
      versionFileUrl: '/version.json', // 版本文件地址
      localVersionKey: 'cmpVersion', // 本地存储的版本号key
      disableFetchCache: true, // 禁用Fetch缓存
      pollInterval: 5 * 60 * 1000, // 5分钟轮询一次
      hasNotified: false // 是否已提醒过用户有新版本
    }
    Object.assign(this.config, options)
    // 定时轮询定时器
    this.pollTimer = null
    // 识别当前环境(Vue CLI 4 自动注入的环境变量)
    this.isProduction = process.env.NODE_ENV === 'production'
  }

  /**
   * 核心方法:执行版本检测
   */
  async checkVersion(isInit = false) {
    try {
      if (this.config.hasNotified) return false

      const localVersion = localStorage.getItem(this.config.localVersionKey) || ''
      const fetchOptions = {}
      if (this.config.disableFetchCache) {
        fetchOptions.cache = 'no-cache'
      }

      const response = await fetch(this.config.versionFileUrl, fetchOptions)
      if (!response.ok) {
        throw new Error(`版本文件请求失败,状态码:${response.status}`)
      }
      const latestVersionInfo = await response.json()
      const serverVersion = latestVersionInfo.version

      if (isInit) {
        this.cacheLatestVersion(serverVersion)
        return true
      }

      if (serverVersion && serverVersion !== localVersion) {
        this.config.hasNotified = true
        console.log('有新版本可用', latestVersionInfo)
        Notification({
          title: '🎉 有新版本可用',
          dangerouslyUseHTMLString: true,
          message: `<p style="font-size:12px;">建议点击刷新页面,以获取最新功能和修复</p> <p style="color:#cccccc;font-size:12px;">更新时间:${latestVersionInfo.updateTime}</p>`,
          duration: 0,
          customClass: 'check-version-notify',
          onClick: () => {
            this.forceRefreshPage()
          },
          onClose: () => {
            this.resetNotifyFlag()
          }
        })
        return true
      } else {
        // 版本一致时,重置提醒标记,便于后续轮询检测新版本
        this.config.hasNotified = false
        // console.log('当前已是最新版本,已缓存最新版本号')
        return false
      }
    } catch (error) {
      console.warn('版本检测异常,不影响应用运行:', error.message)
      return false
    }
  }
  /**
   * 启动定时轮询检测(内置环境判断:仅生产环境生效)
   */
  async startPolling() {
    // 核心:非生产环境,直接返回,不启动轮询
    if (!this.isProduction) {
      console.log('当前为非生产环境,不启动版本检测轮询')
      return
    }

    // 生产环境:正常启动轮询
    this.stopPolling() // 先停止已有轮询,避免重复启动
    this.checkVersion(true) // 立即执行一次检测

    this.pollTimer = setInterval(() => {
      this.checkVersion()
    }, this.config.pollInterval)

    console.log(`生产环境版本轮询检测已启动,每隔${this.config.pollInterval / 1000 / 60}分钟检测一次`)
  }

  /**
   * 停止定时轮询检测
   */
  stopPolling() {
    if (this.pollTimer) {
      clearInterval(this.pollTimer)
      this.pollTimer = null
      console.log('版本轮询检测已停止')
    }
  }

  /**
   * 重置提醒标记
   */
  resetNotifyFlag() {
    this.config.hasNotified = false
  }

  // 缓存最新版本号
  cacheLatestVersion(version) {
    localStorage.setItem(this.config.localVersionKey, version)
    this.resetNotifyFlag()
  }

  // 强制刷新页面
  forceRefreshPage() {
    window.location.reload(true)
  }
}

const versionUpdateInstance = new VersionUpdate()
export { VersionUpdate, versionUpdateInstance }
export default versionUpdateInstance

创建自定义.check-version-notify的版本检测全局样式:

image.png

// 版本检测通知样式
.check-version-notify{
  border: 3px solid transparent !important;
  cursor: pointer;
  background-color: rgba(255, 255, 255, 0.6) !important;
  backdrop-filter: blur(5px);
  &:hover{
    border: 3px solid $--color-primary !important;
  }
  .el-notification__icon{
    font-size: 18px;
    height: 18px;
  }
  .el-notification__title{
    font-size: 14px;
    line-height: 18px;
  }
  .el-notification__group{
    margin-left: 8px;
  }
}

步骤五:在应用入口启动版本检测

在 App.vue 或合适的入口文件中启动版本检测:

import versionUpdate from '@/utils/versionUpdate'
...
mounted() {
  versionUpdate.startPolling()
},
beforeDestroy() {
  versionUpdate.stopPolling()
}

将数组分成最小总代价的子数组 I

2026年1月29日 19:49

方法一:排序

思路与算法

根据题意可知一个数组的代价是它的第一个元素。需要将给定数组 $\textit{nums}$ 分成 $3$ 个连续且没有交集的子数组,题目要求返回这 $3$ 子数组的最小代价和。
根据题意可知,第一个子数组的代价已确定为 $\textit{nums}[0]$。如果确定了第二个子数组的第一个数的位置和第三个子数组的第一个数的位置,此时子数组的划分方案也就确定。我们可以任意选择两个索引 $(i,j)$ 作为第二个子数组的起始位置和第三个子数组的起始位置,且满足 $1 \le i < j \le n -1$,其中 $n$ 表示给定数组 $\textit{nums}$ 的长度。此时,第二个子数组的代价为 $\textit{nums}[i]$,第三个子数组的代价为 $\textit{nums}[j]$。为保证代价和最小,此时可以在 $[1,n−1]$ 中的选择值最小的两个下标即可,可将子数组 $\textit{nums}[1 \cdots n-1]$ 按照从小到大排序,取最小的两个元素即可。

代码

###C++

class Solution {
public:
    int minimumCost(vector<int>& nums) {
        sort(nums.begin() + 1, nums.end());
        return reduce(nums.begin(), nums.begin() + 3, 0);
    }
};

###Java

class Solution {
    public int minimumCost(int[] nums) {
        Arrays.sort(nums, 1, nums.length);
        return nums[0] + nums[1] + nums[2];
    }
}

###C#

public class Solution {
    public int MinimumCost(int[] nums) {
        Array.Sort(nums, 1, nums.Length - 1);
        return nums.Take(3).Sum();
    }
}

###Go

func minimumCost(nums []int) int {
    sort.Ints(nums[1:])
    return nums[0] + nums[1] + nums[2]
}

###Python

class Solution:
    def minimumCost(self, nums: List[int]) -> int:
        nums[1:] = sorted(nums[1:])
        return sum(nums[:3])

###C

int cmp(const void *a, const void *b) {
    return (*(int *)a) - (*(int *)b);
}

int minimumCost(int *nums, int numsSize) {
    qsort(nums + 1, numsSize - 1, sizeof(int), cmp);
    return nums[0] + nums[1] + nums[2];
}

###JavaScript

var minimumCost = function(nums) {
    nums = [nums[0], ...nums.slice(1).sort((a, b) => a - b)];
    return nums.slice(0, 3).reduce((sum, num) => sum + num, 0);
};

###TypeScript

function minimumCost(nums: number[]): number {
    nums = [nums[0], ...nums.slice(1).sort((a, b) => a - b)];
    return nums.slice(0, 3).reduce((sum, num) => sum + num, 0);
};

###Rust

impl Solution {
    pub fn minimum_cost(mut nums: Vec<i32>) -> i32 {
        if nums.len() > 1 {
            let (first, rest) = nums.split_at_mut(1);
            rest.sort();
        }
        nums.iter().take(3).sum()
    }
}

复杂度分析

  • 时间复杂度:$O(n \log n)$,其中 $n$ 表示给定数组 $\textit{nums}$ 的长度。排序需要 $O(n \log n)$ 的时间。

  • 空间复杂度:$O(\log n)$。排序需要 $O(\log n)$ 的栈空间。

方法二:维护最小值和次小值

思路与算法

根据方法一可知,我们需要找到下标在 $[1,n−1]$ 中的两个最小元素,此时可以在遍历数组的过程中维护最小值 $\textit{first}$ 和次小值 $\textit{second}$,最终答案即为 $\textit{nums}[0] + \textit{first} + \textit{second}$。

代码

###C++

class Solution {
public:
    int minimumCost(vector<int> &nums) {
        int first = INT_MAX, second = INT_MAX;
        for (int i = 1; i < nums.size(); i++) {
            int x = nums[i];
            if (x < first) {
                second = first;
                first = x;
            } else if (x < second) {
                second = x;
            }
        }
        return nums[0] + first + second;
    }
};

###Java

class Solution {
    public int minimumCost(int[] nums) {
        int first = Integer.MAX_VALUE;
        int second = Integer.MAX_VALUE;

        for (int i = 1; i < nums.length; i++) {
            int x = nums[i];
            if (x < first) {
                second = first;
                first = x;
            } else if (x < second) {
                second = x;
            }
        }
        return nums[0] + first + second;
    }
}

###C#

public class Solution {
    public int MinimumCost(int[] nums) {
        int first = int.MaxValue;
        int second = int.MaxValue;
        
        for (int i = 1; i < nums.Length; i++) {
            int x = nums[i];
            if (x < first) {
                second = first;
                first = x;
            } else if (x < second) {
                second = x;
            }
        }
        return nums[0] + first + second;
    }
}

###Go

func minimumCost(nums []int) int {
    first := int(^uint(0) >> 1)
    second := int(^uint(0) >> 1)
    
    for i := 1; i < len(nums); i++ {
        x := nums[i]
        if x < first {
            second = first
            first = x
        } else if x < second {
            second = x
        }
    }
    return nums[0] + first + second
}

###Python

class Solution:
    def minimumCost(self, nums: List[int]) -> int:
        return nums[0] + sum(nsmallest(2, nums[1:]))

###C

int minimumCost(int* nums, int numsSize) {
    int first = INT_MAX;
    int second = INT_MAX;
    
    for (int i = 1; i < numsSize; i++) {
        int x = nums[i];
        if (x < first) {
            second = first;
            first = x;
        } else if (x < second) {
            second = x;
        }
    }
    return nums[0] + first + second;
}

###JavaScript

var minimumCost = function(nums) {
    let first = Number.MAX_SAFE_INTEGER;
    let second = Number.MAX_SAFE_INTEGER;
    
    for (let i = 1; i < nums.length; i++) {
        const x = nums[i];
        if (x < first) {
            second = first;
            first = x;
        } else if (x < second) {
            second = x;
        }
    }
    return nums[0] + first + second;
};

###TypeScript

function minimumCost(nums: number[]): number {
    let first: number = Number.MAX_SAFE_INTEGER;
    let second: number = Number.MAX_SAFE_INTEGER;
    
    for (let i = 1; i < nums.length; i++) {
        const x: number = nums[i];
        if (x < first) {
            second = first;
            first = x;
        } else if (x < second) {
            second = x;
        }
    }
    return nums[0] + first + second;
};

###Rust

impl Solution {
    pub fn minimum_cost(nums: Vec<i32>) -> i32 {
        let mut first = i32::MAX;
        let mut second = i32::MAX;
        
        for i in 1..nums.len() {
            let x = nums[i];
            if x < first {
                second = first;
                first = x;
            } else if x < second {
                second = x;
            }
        }
        nums[0] + first + second
    }
}

复杂度分析

  • 时间复杂度:$O(n)$,其中 $n$ 是数组 $\textit{nums}$ 的长度。

  • 空间复杂度:$O(1)$。

【节点】[ViewDirection节点]原理解析与实际应用

作者 SmalBox
2026年2月1日 11:54

【Unity Shader Graph 使用与特效实现】专栏-直达

在Unity的Shader Graph中,View Direction节点是一个功能强大且常用的工具,它允许开发者访问网格顶点或片元的视图方向矢量。这个矢量表示从顶点或片元指向摄像机的方向,在光照计算、反射效果、边缘光等众多视觉效果中扮演着关键角色。

View Direction节点的基本概念

View Direction节点输出的矢量本质上是从当前处理的顶点或片元位置指向摄像机位置的矢量。这个矢量在不同的渲染计算中有着广泛的应用,特别是在需要基于观察角度变化效果的场景中。

视图方向在计算机图形学中是一个基础概念,它描述了表面点相对于观察者的方向关系。在Shader Graph中,View Direction节点封装了这一计算,让开发者能够轻松获取和使用这一重要数据。

从Unity 11.0版本开始,View Direction节点在URP和HDRP中的行为已经统一,都会对所有坐标空间下的视图方向进行标准化处理。这一变化简化了跨渲染管线的着色器开发,确保了行为的一致性。

节点参数详解

坐标空间选择

View Direction节点提供了一个重要的控件参数——Space下拉选单,允许开发者选择输出视图方向矢量的坐标空间。理解不同坐标空间的特性对于正确使用该节点至关重要。

  • Object空间:在此空间下,视图方向是相对于物体自身坐标系表达的。这意味着无论物体如何旋转、移动或缩放,视图方向都会相对于物体的本地坐标系进行计算。在需要基于物体自身方向的效果时特别有用,如某些类型的卡通渲染或物体特定的光照效果。
  • View空间:也称为摄像机空间,在此空间中,摄像机位于原点,视图方向是相对于摄像机坐标系的。这个空间下的计算通常更高效,因为许多与视图相关的变换已经完成。适用于屏幕空间效果、与摄像机直接相关的特效。
  • World空间:在此空间下,视图方向是基于世界坐标系表达的。这是最直观的空间之一,因为所有场景中的物体都共享同一世界坐标系。适用于需要与世界坐标交互的效果,如全局光照、环境遮挡等。
  • Tangent空间:也称为切线空间,这是一个相对于表面法线的局部坐标系。在此空间下,视图方向是相对于每个顶点或片元的法线方向表达的。特别适用于法线贴图、视差映射等需要基于表面方向的效果。

输出端口

View Direction节点只有一个输出端口,标记为"Out",输出类型为Vector 3。这个三维矢量包含了在当前选择的坐标空间下的视图方向。

输出的矢量始终是标准化的,即其长度为1。这一特性使得开发者可以直接使用该矢量进行点积计算等需要单位矢量的操作,而无需额外的标准化步骤。

在不同渲染管线中的行为差异

理解View Direction节点在不同渲染管线中的历史行为差异对于维护和迁移现有项目非常重要。

在Unity 11.0版本之前,View Direction节点在URP和HDRP中的工作方式存在显著差异:

  • 在URP中,该节点仅在Object空间下输出标准化的视图方向,在其他坐标空间下则保持原始长度
  • 在HDRP中,该节点在所有坐标空间下都会标准化视图方向

这种不一致性可能导致相同的着色器在不同渲染管线中产生不同的视觉效果。从11.0版本开始,Unity统一了这一行为,View Direction节点在所有渲染管线和所有坐标空间下都会输出标准化的视图方向。

对于需要在URP中使用旧行为(在Object空间外使用未标准化视图方向)的开发者,Unity提供了View Vector节点作为替代方案。这个节点保持了旧版本View Direction节点的行为,确保了向后兼容性。

实际应用场景

View Direction节点在着色器开发中有着广泛的应用,以下是一些常见的应用场景:

光照计算

在光照模型中,视图方向是计算高光反射的关键要素。结合表面法线和光照方向,视图方向用于确定观察者看到的高光强度。

  • 在Blinn-Phong光照模型中,使用法线、光照方向和视图方向的半角矢量来计算高光
  • 在基于物理的渲染中,视图方向是双向反射分布函数的重要输入

边缘光效果

视图方向可用于创建边缘光效果,当表面几乎与视图方向平行时增强其亮度。

  • 通过计算表面法线与视图方向的点积,可以确定表面的边缘程度
  • 结合菲涅耳效应,可以创建逼真的边缘发光效果

反射效果

视图方向在反射计算中至关重要,无论是平面反射、环境映射还是屏幕空间反射。

  • 在立方体环境映射中,使用视图方向计算反射矢量
  • 在屏幕空间反射中,视图方向用于确定反射射线的方向

视差映射

在视差映射技术中,视图方向用于模拟表面的深度和凹凸感。

  • 在切线空间中使用视图方向偏移纹理坐标
  • 创建更真实的表面凹凸效果,增强场景的立体感

使用示例与步骤

基础视图方向可视化

创建一个简单的着色器,直接显示视图方向:

  • 在Shader Graph中创建新图
  • 添加View Direction节点,选择World空间
  • 将View Direction节点连接到主节点的Base Color端口
  • 由于视图方向可能包含负值,需要将其映射到0-1范围
  • 可以使用Remap节点或简单的数学运算完成这一映射

这个简单的示例可以帮助开发者直观理解视图方向在不同表面区域的变化。

创建菲涅耳效果

菲涅耳效果模拟了物体表面在掠射角(表面几乎与视图平行)反射率增加的现象:

  • 添加View Direction节点和Normal节点,确保使用相同的坐标空间
  • 使用Dot Product节点计算法线和视图方向的点积
  • 使用One Minus节点反转结果,使掠射角的值接近1
  • 使用Power节点控制效果的衰减程度
  • 将结果与颜色或纹理相乘,连接到发射或基础颜色

实现简单的边缘光

创建一个基础的边缘光效果:

  • 按照菲涅耳效果的步骤计算边缘因子
  • 使用Smoothstep或Color节点控制边缘光的范围和颜色
  • 将结果添加到现有的光照计算中
  • 可以结合深度或屏幕空间信息增强效果的真实性

高级反射效果

创建一个基于视图方向的反射效果:

  • 使用View Direction节点和Normal节点计算反射方向
  • 将反射方向用于采样环境贴图或反射探针
  • 结合粗糙度贴图控制反射的模糊程度
  • 使用菲涅耳效应混合反射颜色和表面颜色

性能考虑与最佳实践

虽然View Direction节点本身计算开销不大,但在大规模使用时应考虑性能影响:

  • 在片元着色器中计算视图方向比在顶点着色器中计算更精确但更昂贵
  • 对于不需要高精度的效果,考虑在顶点着色器中计算视图方向并插值
  • 避免在着色器中重复计算视图方向,尽可能重用计算结果
  • 根据具体需求选择合适的坐标空间,减少不必要的空间转换

在移动平台或性能受限的环境中,应特别关注视图方向计算的开销:

  • 尽可能使用计算量较小的坐标空间
  • 考虑使用近似计算替代精确的视图方向
  • 对于远处或小物体,可以使用简化的视图方向计算

常见问题与解决方案

视图方向显示异常

当视图方向显示不正确时,通常是由于坐标空间不匹配造成的:

  • 确保View Direction节点和与之交互的其他节点使用相同的坐标空间
  • 检查物体的变换矩阵是否包含非常规的缩放或旋转
  • 验证摄像机的设置,特别是正交投影与透视投影的区别

性能问题

如果着色器因视图方向计算导致性能下降:

  • 分析是否真的需要在片元级别计算视图方向
  • 考虑使用更简化的计算模型
  • 检查是否有重复的视图方向计算可以合并

跨平台兼容性

确保视图方向相关效果在不同平台上表现一致:

  • 测试在不同图形API下的表现
  • 验证在移动设备上的精度和性能
  • 考虑为不同平台提供不同的精度或实现

进阶技巧与创意应用

结合时间变化的动态效果

通过将视图方向与时间参数结合,可以创建动态变化的视觉效果:

  • 使用视图方向驱动动画或纹理偏移
  • 创建随着观察角度变化而动态调整的效果
  • 实现类似全息图或科幻界面元素的视觉效果

非真实感渲染

在卡通渲染或其他非真实感渲染风格中,视图方向可以用于:

  • 控制轮廓线的粗细和强度
  • 实现基于角度的色彩简化
  • 创建手绘风格的笔触效果

特殊材质模拟

视图方向在模拟特殊材质时非常有用:

  • 模拟丝绸、缎子等具有角度相关反射的织物
  • 创建各向异性材料如拉丝金属的效果
  • 实现液晶显示屏的角度相关颜色变化

【Unity Shader Graph 使用与特效实现】专栏-直达 (欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,🙏)

Flutter 中的 FittedBox 详解:比如让金额显示自适应

作者 J船长
2026年2月1日 11:44

Flutter 中的 FittedBox 详解:如何让金额显示自适应

在开发 Flutter 应用时,我们常常遇到需要根据屏幕尺寸或父容器大小自动调整子组件大小的情况。特别是在处理金额显示时,金额可能从一个较小的数字(例如:100)逐渐增大(例如:100,000,000,00000)。这种情况下,如何保证金额无论大小都能自适应父容器而不被截断或者变形呢?这时,FittedBox 就能派上用场,它能够帮助我们根据父容器的大小自动缩放子组件,确保内容能够完美展示。

例子:金额的自适应显示

假设我们要在界面中显示一个金额。刚开始时,金额是 100,随着时间的推移,它逐渐变大,比如 100,000,000,00000。如果我们不对文本进行缩放,随着金额的增加,数字可能会超出屏幕,导致 UI 出现溢出。这里,FittedBox 就能解决这个问题。它会根据父容器的尺寸自动缩放文本,确保金额内容始终能适应屏幕空间。

示例代码:

Flexible(
  child: FittedBox(
    fit: BoxFit.scaleDown,
    alignment: AlignmentDirectional.centerStart,
    // 金额显示文本 - 线性渐变(垂直方向)
    child: ShaderMask(
      shaderCallback: (bounds) => const LinearGradient(
        begin: Alignment.topCenter,
        end: Alignment.bottomCenter,
        colors: [
          Color(0xFFFFC30B),
          Color(0xFFFFF4CD),
          Color(0xFFFFBB10),
        ],
      ).createShader(bounds),
      child: const Text(
        '100,000,000,00000',
        style: TextStyle(
          fontSize: 40,
          fontWeight: FontWeight.w900,
          color: Colors.white,
        ),
      ),
    ),
  ),
)

关键点:

  • FittedBox: 它是 Text 组件的父容器,负责调整文本的大小,使其适应可用的空间。随着金额变大,FittedBox 会根据父容器的变化自动缩放文本。
  • BoxFit.scaleDown: 这个 fit 属性确保文字不会超出容器的边界。如果金额变大,文字会被缩小以适应容器。
  • ShaderMask: 为金额文本添加了线性渐变的效果,使数字显示得更加醒目。

在这个例子中,Text 组件会根据金额的大小自动调整字体大小。假设金额从 100 增长到 100,000,000,00000FittedBox 会确保文本始终适应父容器的大小,并随着数字增大而逐渐缩小字体大小,从而避免文本溢出或失真。

FittedBox 的作用和原理

FittedBox 是一个用于缩放其子组件的布局小部件。它会根据父容器的尺寸来自动调整子组件的大小,确保子组件不会超出父容器的边界。使用 FittedBox,我们可以避免文字、图片等内容溢出或者失真,确保 UI 布局在各种屏幕尺寸上都能适应良好。

fit 属性的不同值

FittedBox 通过 fit 属性来决定如何缩放其子组件。fit 属性可以设置为不同的 BoxFit 枚举值,其中一些常见的值包括:

  • BoxFit.contain: 保持子组件的宽高比,同时让子组件完全显示在父组件的空间中。
  • BoxFit.cover: 让子组件覆盖整个父容器,可能会裁剪部分内容,但不会出现空白。
  • BoxFit.fill: 拉伸子组件以完全填充父组件,可能会导致内容变形。
  • BoxFit.scaleDown: 如果子组件的尺寸大于父组件,它会缩小子组件;如果子组件本身小于父组件,它将保持原尺寸。

在前面的金额示例中,我们使用了 BoxFit.scaleDown,确保金额文本不会被拉伸或者裁剪,而是适应父容器的大小。

alignment 属性

FittedBox 还允许通过 alignment 属性来控制子组件在容器中的对齐方式。alignment 属性接受一个 AlignmentGeometry 类型的值,可以让我们选择子组件在容器中的不同对齐方式。

例如:

  • Alignment.topLeft: 左上角对齐
  • Alignment.center: 居中对齐
  • Alignment.bottomRight: 右下角对齐

结合其他小部件使用

FittedBox 可以与其他布局小部件(如 FlexibleExpandedAlign 等)结合使用,以达到更加灵活的布局效果。例如,我们可以在 RowColumnFlex 中使用 Flexible 来让 FittedBox 更好地适应父容器的大小。

示例:配合 Flexible 使用
Row(
  children: [
    Flexible(
      child: FittedBox(
        fit: BoxFit.scaleDown,
        alignment: Alignment.center,
        child: Text(
          'Hello, Flutter!',
          style: TextStyle(fontSize: 30),
        ),
      ),
    ),
  ],
)

在这个例子中,Text 组件会根据 Flexible 给定的空间自动缩放,而不会超出父容器的边界。

FittedBox 的实际应用场景

1. 自动缩放文本

在显示动态变化的数字时,尤其是金额等大数字,FittedBox 可以自动缩放文本,确保它们适应容器的宽度和高度。例如,当显示一个不断增长的金额时,FittedBox 会根据金额的增加,自动调整字体大小,以适应屏幕空间。

2. 图片适配

在显示图片时,FittedBox 可以确保图片根据父容器的大小进行自动缩放或裁剪,避免图片超出边界或者变形。尤其在响应式设计中,FittedBox 提供了非常灵活的解决方案。

3. 保持比例缩放

FittedBox 也可以用于保持子组件的宽高比不变。比如,当我们有一个矩形图形或者图片时,我们可以使用 FittedBox 来确保图形始终保持比例缩放,而不会失真。

总结

FittedBox 是 Flutter 中一个非常强大的小部件,它使得我们能够轻松地让子组件自适应父容器的大小。通过合理使用 fitalignment 等属性,FittedBox 可以帮助我们处理复杂的布局需求,特别是在响应式设计中。无论是缩放文本、图片,还是保持宽高比,FittedBox 都能提供简单而灵活的解决方案。

在实际开发中,FittedBox 常常用于解决布局溢出、图片裁剪、文字缩放等问题,是 Flutter 中不可或缺的布局工具之一。通过理解和掌握 FittedBox 的用法,你可以更加高效地构建响应式和自适应的用户界面。

Node.js 进程退出时,为什么你的日志总会“断尾”?

作者 donecoding
2026年1月31日 23:14
一、 进程弥留之际,我们在清理什么? 很多开发者习惯在进程退出时忙着将对象置空、清空 Map。请停止这种无效劳动。 进程退出后,操作系统会强制回收所有物理内存。我们真正关心的是那些  “操作系统无法自

每日一题-将数组分成最小总代价的子数组 I🟢

2026年2月1日 00:00

给你一个长度为 n 的整数数组 nums 。

一个数组的 代价 是它的 第一个 元素。比方说,[1,2,3] 的代价是 1 ,[3,4,1] 的代价是 3 。

你需要将 nums 分成 3 个 连续且没有交集 的子数组。

请你返回这些子数组最小 代价 总和 。

 

示例 1:

输入:nums = [1,2,3,12]
输出:6
解释:最佳分割成 3 个子数组的方案是:[1] ,[2] 和 [3,12] ,总代价为 1 + 2 + 3 = 6 。
其他得到 3 个子数组的方案是:
- [1] ,[2,3] 和 [12] ,总代价是 1 + 2 + 12 = 15 。
- [1,2] ,[3] 和 [12] ,总代价是 1 + 3 + 12 = 16 。

示例 2:

输入:nums = [5,4,3]
输出:12
解释:最佳分割成 3 个子数组的方案是:[5] ,[4] 和 [3] ,总代价为 5 + 4 + 3 = 12 。
12 是所有分割方案里的最小总代价。

示例 3:

输入:nums = [10,3,1,1]
输出:12
解释:最佳分割成 3 个子数组的方案是:[10,3] ,[1] 和 [1] ,总代价为 10 + 1 + 1 = 12 。
12 是所有分割方案里的最小总代价。

 

提示:

  • 3 <= n <= 50
  • 1 <= nums[i] <= 50
❌
❌