每日一题-在区间范围内统计奇数数目🟢
给你两个非负整数 low 和 high 。请你返回 low 和 high 之间(包括二者)奇数的数目。
示例 1:
输入:low = 3, high = 7 输出:3 解释:3 到 7 之间奇数数字为 [3,5,7] 。
示例 2:
输入:low = 8, high = 10 输出:1 解释:8 到 10 之间奇数数字为 [9] 。
提示:
0 <= low <= high <= 10^9
给你两个非负整数 low 和 high 。请你返回 low 和 high 之间(包括二者)奇数的数目。
示例 1:
输入:low = 3, high = 7 输出:3 解释:3 到 7 之间奇数数字为 [3,5,7] 。
示例 2:
输入:low = 8, high = 10 输出:1 解释:8 到 10 之间奇数数字为 [9] 。
提示:
0 <= low <= high <= 10^9$[\textit{low},\textit{high}]$ 中的正奇数个数,等于 $[1,\textit{high}]$ 中的正奇数个数,减去 $[1,\textit{low}-1]$ 中的正奇数个数。(这个想法类似 前缀和)
正奇数可以表示为 $2k-1$,其中 $k$ 是正整数。
$[1,n]$ 中的正奇数满足 $1\le 2k-1\le n$,解得
$$
1\le k \le \left\lfloor\dfrac{n+1}{2}\right\rfloor
$$
这有 $\left\lfloor\dfrac{n+1}{2}\right\rfloor$ 个整数 $k$。
所以答案为
$$
\left\lfloor\dfrac{\textit{high}+1}{2}\right\rfloor - \left\lfloor\dfrac{\textit{low}}{2}\right\rfloor
$$
###py
class Solution:
def countOdds(self, low: int, high: int) -> int:
return (high + 1) // 2 - low // 2
###java
class Solution {
public int countOdds(int low, int high) {
return (high + 1) / 2 - low / 2;
}
}
###cpp
class Solution {
public:
int countOdds(int low, int high) {
return (high + 1) / 2 - low / 2;
}
};
###c
int countOdds(int low, int high) {
return (high + 1) / 2 - low / 2;
}
###go
func countOdds(low, high int) int {
return (high+1)/2 - low/2
}
###js
var countOdds = function(low, high) {
return Math.floor((high + 1) / 2) - Math.floor(low / 2);
};
###rust
impl Solution {
pub fn count_odds(low: i32, high: i32) -> i32 {
(high + 1) / 2 - low / 2
}
}
欢迎在评论区分享你的思路/代码。
欢迎关注 B站@灵茶山艾府
假设一个区间【0,3】,序列是0,1,2,3 奇数个数是3+1/2=2,区间【0,4】,序列是0,1,2,3,4 奇数个数4+1/2=2。
所以,所以,所以,high为3或者4,加个1,然后除以2,奇数个数都是2,然后,请自己推【0,5】和【0,6】,奇数个数都是3。
得出公式 high+1/2是区间【0,high】的奇数个数
因为low,左边界是可以改变的,所以先求【0,high】的奇数个数,然后在求【0,low】的奇数个数,然后做差得到总奇数个数。
注意,注意,注意,把+1看成右边区间增大,这里low是相当于在【0,high】里面的,你别加1,右边界high增大就行了。
**举个例子:**区间【3,7】,high的奇数个数 7+1/2=4,,如果此时3+1/2=2,4-2=2,答案就错了,要3/2=1,最后答案才等于3。
high+1奇数个数 - low奇数个数 **=**总奇数个数。
用公式表示 (high+1)/2 - low/2
int countOdds(int low, int high){
return ((high+1)/2)-((low)/2); //严谨
}
![]()
思路与算法
如果我们暴力枚举 ${\rm [low, high]}$ 中的所有元素会超出时间限制。
我们可以使用前缀和思想来解决这个问题,定义 ${\rm pre}(x)$ 为区间 $[0, x]$ 中奇数的个数,很显然:
$${\rm pre}(x) = \lfloor \frac{x + 1}{2} \rfloor$$
故答案为 $\rm pre(high) - pre(low - 1)$。
代码
###C++
class Solution {
public:
int pre(int x) {
return (x + 1) >> 1;
}
int countOdds(int low, int high) {
return pre(high) - pre(low - 1);
}
};
###Java
class Solution {
public int countOdds(int low, int high) {
return pre(high) - pre(low - 1);
}
public int pre(int x) {
return (x + 1) >> 1;
}
}
###Python
class Solution:
def countOdds(self, low: int, high: int) -> int:
pre = lambda x: (x + 1) >> 1
return pre(high) - pre(low - 1)
###C#
public class Solution {
public int Pre(int x) {
return (x + 1) >> 1;
}
public int CountOdds(int low, int high) {
return Pre(high) - Pre(low - 1);
}
}
###Go
func pre(x int) int {
return (x + 1) >> 1
}
func countOdds(low int, high int) int {
return pre(high) - pre(low - 1)
}
###C
int pre(int x) {
return (x + 1) >> 1;
}
int countOdds(int low, int high) {
return pre(high) - pre(low - 1);
}
###JavaScript
var countOdds = function(low, high) {
return pre(high) - pre(low - 1);
};
function pre(x) {
return (x + 1) >> 1;
}
###TypeScript
function pre(x: number): number {
return (x + 1) >> 1;
}
function countOdds(low: number, high: number): number {
return pre(high) - pre(low - 1);
}
###Rust
impl Solution {
fn pre(x: i32) -> i32 {
(x + 1) >> 1
}
pub fn count_odds(low: i32, high: i32) -> i32 {
Self::pre(high) - Self::pre(low - 1)
}
}
复杂度分析
时间复杂度:$O(1)$。
空间复杂度:$O(1)$。
各位前端打工人,有没有过这种经历:明明写了 setTimeout(() => console.log('摸鱼')),结果同步代码还没跑完,摸鱼计划就被打断?其实 JS 单线程就像一个只能专注干一件事的打工人,而 Event Loop 就是它的 “高效摸鱼手册”—— 既能按时完成核心工作,又能把耗时任务 “挂起摸鱼”,今天咱们就一起好好聊聊这份手册!
要想摸鱼,得先知道 “工作台” 的规矩:
进程:好比公司的独立部门 —— 比如浏览器开个新标签页,就是开了个新部门,每个部门都有自己的办公资源(电脑、文件)。
线程:部门里真正干活的打工人 —— 浏览器部门里就有三个核心员工:
但这里有个 “办公室规定”:JS 引擎线程和渲染线程是 “互斥同事” ——JS 能修改 DOM(比如把按钮改成红色),要是它俩同时干活,页面就会出现 “排版错乱”(比如按钮画到一半被改成红色),所以必须 “你歇我干”。
更关键的是:JS 引擎线程是个 “独生子” (V8 引擎默认只开一个线程)。这就意味着:如果 JS 遇到一个耗时 10 秒的计算任务(比如统计 100 万条数据),它就会一直死磕这个任务,导致渲染线程没法干活,页面直接卡成 “PPT”—— 这就是 “硬卷” 的下场!
所以 JS 打工人的生存法则是:能摸鱼就不硬卷,耗时任务先 “挂起”,等核心工作做完再处理—— 这就是 “异步摸鱼” 的核心逻辑。
JS 里的 “摸鱼任务”(异步任务) 分两类,就像公司里的 “紧急任务” 和 “常规任务”,得按顺序处理,不能乱摸鱼:
Promise.then()、async/await 后续代码、process.nextTick()(Node 环境),相当于 “老板临时交代的小任务,必须在下班前做完”;setTimeout、setInterval、ajax 请求、I/O 操作、UI 渲染,相当于 “下周要交的报告,先放一放”;Event Loop 就是这套摸鱼规则的 “监督者”,它的工作流程就像打工人的一天,记好这 4 步,摸鱼不翻车:
光说不练假把式,咱们用真实代码模拟 JS 打工人的 “摸鱼一天”,看看 Event Loop 是怎么安排任务的!
setTimeout为啥 “跑不赢” 同步代码?先看这串经典代码:
let a = 1;
setTimeout(() => {
a = 2
}, 1000)
console.log(a);
分析摸鱼过程:
let a=1 → 执行console.log(a),此时a还是 1;setTimeout是宏任务,被扔进 “宏任务队列” 排队;a=2)。所以结果是:先输出 1,1 秒后a才变成 2。
![]()
Promise.then的 “VIP 特权”我们看一道经典面试题:
console.log(1);
new Promise((resolve) => {
console.log(2);
resolve();
})
.then(() => {
console.log(3);
setTimeout(() => {
console.log(4);
}, 0)
})
setTimeout(() => {
console.log(5);
setTimeout(() => {
console.log(6);
}, 0)
}, 0)
console.log(7);
是不是已经头皮发麻了?根本不清楚打印顺序是啥,但是这道面试题我们必须拿下!
摸鱼步骤拆解:
常规摸鱼(宏任务)开跑:
console.log(1) → 输出1;new Promise:Promise 构造函数里的代码是同步的,执行console.log(2) → 输出2,然后resolve();then是微任务,扔进 “微任务队列”;setTimeout:宏任务,扔进 “宏任务队列”;console.log(7) → 输出7。紧急摸鱼(微任务)接棒:
then的回调,执行它:console.log(3) → 输出3;setTimeout(4)是宏任务,扔进 “宏任务队列”。宏任务队列开跑(下一轮摸鱼) :
setTimeout):执行console.log(5) → 输出5;setTimeout(6)扔进宏任务队列;then里的setTimeout(4)):执行console.log(4) → 输出4;setTimeout(6):执行console.log(6) → 输出6。最终输出顺序:1 → 2 → 7 → 3 → 5 → 4 → 6
![]()
上图更清晰:
![]()
async/await 本质是 Promise 的语法糖,相当于给摸鱼任务加了 “自动排队” 功能,先搞懂它的用法:
console.log('script start');
async function async1() {
await async2()
console.log('async1 end');
}
async function async2() {
console.log('async2 end');
}
async1();
关键规则:
async函数本身相当于 “返回 Promise 的函数”;await fn()的本质是:把await后面的代码,塞进了fn()返回的 Promise 的then里(也就是微任务队列) 。拿这段代码分析:
同步执行console.log('script start') → 输出;
执行async1():
async1,遇到await async2() → 先执行async2()(同步),输出async2 end;await把后续的console.log('async1 end')扔进微任务队列;继续执行同步代码
![]()
OK既然知道了原理我们就实战摸鱼:
// 模拟耗时任务:向服务器要数据(宏任务)
function fetchData() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('常规摸鱼:发接口请求(耗时 1 秒)');
resolve('接口返回数据:用户列表');
}, 1000);
});
}
// 核心工作函数(async 标记为异步函数)
async function work() {
console.log('核心工作:开始处理用户数据');
// await 相当于“等待摸鱼任务完成,再继续核心工作”
const data = await fetchData();
// 这行代码会被扔进微任务队列,相当于“紧急摸鱼后的收尾工作”
console.log(`核心工作:使用${data}完成报表`);
}
// 执行核心工作
work();
// 其他同步任务
console.log('核心工作:处理其他紧急事务');
摸鱼流程拆解:
执行同步任务:
work() 函数,打印 核心工作:开始处理用户数据;await fetchData(),先执行 fetchData(),里面的 setTimeout 被扔进 “宏任务队列”(常规摸鱼);await 会暂停 work 函数,跳出去执行其他同步任务,打印 核心工作:处理其他紧急事务 → 同步任务完成。微任务队列为空,直接进入中场休息。
处理宏任务队列(常规摸鱼):
setTimeout 回调,打印 常规摸鱼:发接口请求(耗时 1 秒),Promise resolve 后,await 后面的代码被扔进 “微任务队列”。再次处理微任务队列:
console.log(核心工作:使用 ${data} 完成报表) → 核心工作收尾。![]()
这里的关键是:await 后面的代码会被自动塞进微任务队列,相当于 “摸鱼结束后,优先处理收尾工作”,不用手动写 then 回调,摸鱼更优雅!
大家可以复制代码去运行一下,时间延迟照片体现不出来~~
错! setTimeout(() => {}, 1000) 不是 “1 秒后立即执行”,而是 “1 秒后把任务扔进宏任务队列”,得等同步任务和微任务全部完成后才会执行。如果前面的任务耗时 2 秒,那摸鱼就得等 2 秒后才开始。
错! new Promise((resolve) => { 同步代码 }) 里的代码是同步执行的,只有 then、catch 回调才是微任务(异步)。比如下面的代码,会先打印 同步代码,再打印 微任务:
new Promise((resolve) => {
console.log('同步代码');
resolve();
})
.then(() => {
console.log('微任务')
});
3. 误区 3:async 函数返回值是 “原始数据”
错! async 函数默认返回一个 Promise 对象,哪怕你写 async function fn() { return 1; },调用 fn() 得到的也是 Promise { 1 },需要用 await 或 then 才能拿到值。
同步任务先干完,微任务队列清干净;
渲染页面歇一歇,宏任务来轮着干;
await 后藏微任务,Promise 构造是同步;
Event Loop 掌节奏,摸鱼工作两不误!
其实 JS 单线程的 “摸鱼哲学”,本质是 “优先级管理”—— 核心工作优先做,耗时任务排队做,既不耽误事,又不浪费时间。掌握了 Event Loop,你不仅能看懂 JS 异步代码的执行顺序,还能写出更高效的代码,就像打工人掌握了摸鱼技巧,工作效率翻倍,摸鱼也不心慌!
初入职场,我被安排用 Vue3 制作公司官网,有 5-6 个静态页面。开发完成后,领导在测试时提出一个问题:“为什么页面滑动后再切换到其它页面,返回时没有回到顶部?”调试后发现,是因为使用了 <keep-alive> 组件缓存页面导致的。这引发了我对 Vue 3 Keep-Alive 的浓厚兴趣。Keep-Alive 能帮助我们在页面间切换时保留组件的状态,使用户体验更加流畅。特别是在带有筛选和滚动列表的页面中,使用 Keep-Alive 可以在返回时保留用户之前的筛选条件和滚动位置,无需重新加载或初始化。
在本文中,我将结合实例,从基础到深入地解析 Vue 3 中的 Keep-Alive 组件原理、常见问题及最佳实践,帮助大家全面掌握这一功能。
<keep-alive> 是 Vue 的内置组件,用于缓存组件实例,避免在切换时重复创建和销毁组件实例。换言之,当组件被包裹在 <keep-alive> 中离开视图时,它不会被销毁,而是进入缓存;再次访问时,该组件实例会被重新激活,状态依然保留。
示例场景:用户从列表页进入详情页后再返回列表页。
没有 Keep-Alive 的情况:
用户操作:首页 → 探索页 → 文章详情 → 探索页
组件生命周期:
有 Keep-Alive 的情况:
用户操作:首页 → 探索页 → 文章详情 → 探索页
组件生命周期:
使用 <keep-alive> 包裹的组件,在离开时不会销毁,而是进入「停用(deactivated)」状态;再次访问时触发「激活(activated)」状态,原先所有的响应式数据都仍然保留。这意味着,探索页中的筛选条件和滚动位置都还能保留在页面返回时显示,提高了用户体验。
Keep-Alive 通过以下机制来实现组件缓存:
<keep-alive> 中,组件实例不会被销毁,而是存放在内存中。下次访问该组件时,直接复用之前缓存的实例。onActivated / onDeactivated 或 activated / deactivated。可以在这些钩子中执行恢复或清理操作,例如刷新数据或保存状态。<keep-alive> 默认会缓存所有包裹其中的组件实例。但如果需要精确控制,就会用到 include、exclude 属性,匹配组件的 name 选项来决定是否缓存。注意,这里的匹配依赖于组件的 name 属性,与路由配置无关。include:字符串、正则或数组,只有 name 匹配的组件才会被缓存。exclude:字符串、正则或数组,name 匹配的组件将不会被缓存。max:数字,指定最多缓存多少个组件实例,超过限制时会删除最近最少使用的实例。注意:include/exclude 匹配的是组件的 name 选项。在 Vue 3.2.34 及以后,如果使用了 <script setup>,组件会自动根据文件名推断出 name,无需手动声明。
最简单的使用方式是将动态组件放在 <keep-alive> 里面:
<template>
<keep-alive>
<component :is="currentComponent" />
</keep-alive>
</template>
这样每次切换 currentComponent 时,之前的组件实例会被缓存,状态不会丢失。
在 Vue Router 配置中,为了让路由页面支持缓存,需要将 <keep-alive> 放在 <router-view> 的插槽中:
<template>
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" />
</keep-alive>
</router-view>
</template>
这样 <keep-alive> 缓存的是路由对应的组件,而非 <router-view> 自身。不要包裹整个 <router-view>,而是通过插槽嵌套其渲染的组件。
如果只想缓存特定组件,可利用 include 属性:
<template>
<router-view v-slot="{ Component }">
<keep-alive include="Home,Explore">
<component :is="Component" />
</keep-alive>
</router-view>
</template>
include 中的名称必须与组件的 name 完全一致,否则不起作用。
以“探索”列表页为例:用户在该页设置筛选条件并滚动列表后,跳转到文章详情页,再返回“探索”页。如果没有使用 Keep-Alive,列表页组件会被重新创建,筛选条件和滚动位置会重置。
使用 <keep-alive> 缓存“探索”页后,返回时组件从缓存中激活,之前的 ref 值和 DOM 滚动位置依然保留。这保证了用户回到列表页时,能够看到原先浏览到的内容和筛选状态。
可以在组件中配合路由导航守卫保存和恢复滚动条位置:
onBeforeRouteLeave 钩子中记录 scrollTop。onActivated 钩子中恢复滚动条位置。我们经常希望缓存某些页面状态,同时让某些页面不被缓存,例如:
<script setup>
defineOptions({ name: 'Explore' })
</script>
然后在主组件中使用 include 指定名称:
<router-view v-slot="{ Component }">
<keep-alive include="Home,Explore,UserCenter">
<component :is="Component" />
</keep-alive>
</router-view>
理论上只缓存 Home、Explore、UserCenter。
name 属性,而非路由配置中的 name。<script setup> 会自动根据文件路径生成组件名,手动写的 name 可能与自动生成冲突。依赖组件名匹配容易出错,需要更灵活的方法。
Vue 3.2.34+ 使用 <script setup> 的单文件组件会自动根据文件名推断组件的 name。
src/pages/Explore/index.vue → 组件名 Explore
src/pages/User/Profile.vue → 组件名 Profile
无需手动定义 name,避免与自动推断冲突。
<keep-alive> 无法捕获组件原始 name。<script setup lang="js">
// Vue 会自动根据路径生成 name
</script>
const routes = [
{
path: '/explore',
name: 'Explore',
component: () => import('@/pages/Explore/index.vue'),
meta: { title: '探索', keepAlive: true }
},
{
path: '/login',
name: 'Login',
component: () => import('@/pages/Auth/index.vue'),
meta: { title: '登录', keepAlive: false }
},
{
path: '/article/:id',
name: 'ArticleDetail',
component: () => import('@/pages/ArticleDetail/index.vue'),
meta: { title: '文章详情', keepAlive: false }
}
]
<script setup lang="js">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const shouldCache = computed(() => route.meta?.keepAlive !== false)
</script>
<template>
<router-view v-slot="{ Component }">
<keep-alive v-if="shouldCache">
<component :is="Component" />
</keep-alive>
<component v-else :is="Component" />
</router-view>
</template>
默认缓存所有页面,只有 meta.keepAlive 明确为 false 时才不缓存。
| 页面类型 | 是否缓存 | 缓存原因 |
|---|---|---|
| 首页(静态) | ❌ 不缓存 | 内容简单,一般无需缓存 |
| 列表/浏览页 | ✅ 缓存 | 保持筛选条件、分页状态、滚动位置等 |
| 详情页 | ❌ 不缓存 | 每次展示不同内容,应重新加载 |
| 表单页 | ❌ 不缓存 | 避免表单数据残留 |
| 登录/注册页 | ❌ 不缓存 | 用户身份相关,每次重新初始化 |
| 个人中心/控制台 | ✅ 缓存 | 保留子页面状态,提升体验 |
<script setup>
// Vue 会自动推断 name
</script>
<script setup>
import { onActivated, onDeactivated } from 'vue'
onActivated(() => {
console.log('组件被激活(从缓存恢复)')
})
onDeactivated(() => {
console.log('组件被停用(进入缓存)')
})
</script>
max 限制。onActivated 中进行必要更新。<keep-alive> 和 <transition> 嵌套顺序正确。<keep-alive> 缓存组件实例,通过停用保留状态。name。meta.keepAlive 控制缓存。onActivated / onDeactivated 钩子。手动定义 Name → 自动 Name → Meta 控制
meta.keepAlive 控制缓存。这样既保持了代码简洁,又实现了灵活可控的缓存策略,确保用户在页面切换时能获得更好的体验。
顶点是构成3D模型的基本几何单元,每个顶点在三维空间中具有明确的坐标位置(x,y,z)。在Unity中,顶点不仅包含位置信息,还承载着模型渲染所需的多维数据:
通过Shader Graph的Position节点和数学运算,可动态生成顶点位置。例如,创建波浪效果:
// 伪代码示例:顶点位置偏移
float4 position = TransformPosition(float4(input.position.x, sin(input.position.x * 10) * 0.1, input.position.z, 1));
此代码通过正弦函数沿X轴生成周期性波动,实现水面扭曲效果。
三角形作为3D建模的最小单位,具有以下核心特性:
虽然多边形面片(如四边形)在建模中更直观,但渲染时会被分解为三角形。例如,Unity的网格渲染器会自动将四边形拆分为两个三角形,确保硬件兼容性。
在Shader Graph中,通过以下节点访问顶点数据:
创建凹凸效果时,可通过修改法线改变光照表现:
// 伪代码示例:法线扰动
float3 normal = normalize(input.normal + float3(0, sin(input.position.x * 10) * 0.1, 0));
此代码沿Y轴添加正弦波动,模拟表面起伏。
UV坐标通过将3D表面展开为2D平面实现纹理映射。例如,立方体需6组UV坐标,而球体通常使用球形投影或立方体映射。
复杂模型可能使用多组UV坐标:
在Shader Graph中,通过UV节点选择通道,结合Sample Texture 2D实现多纹理混合。
创建渐隐效果时,可通过顶点颜色控制透明度:
// 伪代码示例:颜色驱动透明度
float4 color = input.color * float4(1, 1, 1, smoothstep(0.5, 0.8, input.color.a));
此代码根据顶点Alpha值平滑调整透明度,实现边缘渐隐。
Attributes节点打包数据,减少采样次数。通过Create Mesh节点和Set Mesh节点,可在运行时生成网格:
// 伪代码示例:生成平面网格
Mesh mesh = new Mesh();
mesh.vertices = new Vector3[] {
Vector3.zero,
Vector3.right,
Vector3.up,
Vector3.right + Vector3.up
};
mesh.triangles = new int[] { 0, 1, 2, 0, 2, 3 };
此代码创建了一个包含两个三角形的平面。
使用Instancing节点和Set Mesh节点,可高效渲染大量相同网格:
// 伪代码示例:实例化渲染`
MaterialPropertyBlock props = new MaterialPropertyBlock();
props.SetVector("_Color", Color.red);
Renderer renderer = GetComponent<Renderer>();
renderer.SetPropertyBlock(props);
renderer.SetMaterial(material, 0);
此代码为所有实例设置统一颜色,减少Draw Calls。
Normalize节点修正。Tiling And Offset节点调整。URP Shader Graph通过可视化节点系统,大幅降低了着色器开发门槛。掌握网格数据处理的核心要点:
结合URP的渲染管线特性和Shader Graph的节点化设计,开发者可快速实现从简单材质到复杂视觉效果的全方位创作。
【Unity Shader Graph 使用与特效实现】专栏-直达 (欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,🙏)
在实习期间,我参与了公司项目的前端开发,页面主要包括首页(Home)和探索页(Explore)。在项目中,这两个页面都使用 window 作为滚动容器。测试时发现一个问题:
首页和探索页都使用 window 作为滚动容器
↓
它们共享同一个 window.scrollY(全局变量)
↓
用户在探索页滚动到 500px
↓
window.scrollY = 500(全局状态)
↓
切换到首页(首页组件被缓存,状态保留)
↓
但 window.scrollY 仍然是 500(全局共享)
↓
首页显示时,看起来也在 500px 的位置 ❌
这个问题的原因在于:
<keep-alive> 只缓存组件实例和 DOM,不管理滚动状态。window.scrollY 是全局浏览器状态,不会随组件缓存自动恢复。为了在自己的项目中解决类似问题,我考虑了手动管理滚动位置的方案:
/**
* 滚动位置管理工具
* 用于在 keep-alive 缓存页面时,为每个路由独立保存和恢复滚动位置
*/
const scrollPositions = new Map()
export function saveScrollPosition(routePath) {
const y = window.scrollY || document.documentElement.scrollTop || document.body.scrollTop
scrollPositions.set(routePath, y)
}
export function restoreScrollPosition(routePath, defaultY = 0) {
const saved = scrollPositions.get(routePath) ?? defaultY
requestAnimationFrame(() => {
window.scrollTo(0, saved)
document.documentElement.scrollTop = saved
document.body.scrollTop = saved
})
}
在组件中配合 Vue 生命周期钩子使用:
import { onActivated, onBeforeUnmount } from 'vue'
import { useRoute } from 'vue-router'
import { saveScrollPosition, restoreScrollPosition } from './scrollManager'
export default {
setup() {
const route = useRoute()
// 组件激活时恢复滚动
onActivated(() => {
restoreScrollPosition(route.path, 0)
})
// 组件离开前保存滚动
onBeforeUnmount(() => {
saveScrollPosition(route.path)
})
}
}
在公司项目中,由于页面结构简单,不需要为每个路由保存独立滚动位置,因此我采用了统一重置滚动到顶部的方式:
// 路由切换后重置滚动位置
router.afterEach((to, from) => {
if (to.path !== from.path) {
setTimeout(() => {
window.scrollTo(0, 0)
document.documentElement.scrollTop = 0
document.body.scrollTop = 0
}, 0)
}
})
这样可以保证:
<keep-alive> 缓存组件实例,但不管理 window 滚动状态,导致全局滚动共享问题。![]()
点进去,是一个style标签,里面有六万多行样式 进去使用正则查找,发现有11处一模一样的样式
^.el-textarea__inner \{
![]()
经过简单排查,发现问题在于element-variables.scss这个文件中,我框选的这一条代码。![]()
但是把它注释掉,样式就没了,因为项目引入样式的方式是scss。
于是乎去查看官方文档,确实没啥问题。
![]()
于是我起了一个新的vue2+element-ui+scss项目,用同样的方式引入。
结果发现,是一样的,也有重复的样式说明这是Element的问题。
![]()
element官方的scss文件中重复定义了样式
比如我引入以下样式
可以发现有两个重复样式
![]()
Element早已停更,假如你不是迫不得已,应该停止使用这个UI库。
以下的所有方法都并不是一种优雅的解决方式,但是他们可以解决当前的问题。
解决方法来自github,但是位于以下文章的引用让我发现这个问题。
[vue.js - ElementUI重复引入样式问题 - 学习前端历程 - SegmentFault 思否] (segmentfault.com/a/119000002…)
令人遗憾的是,这篇文章里的方法根本不起作用。
github.com/ElemeFE/ele…
你只需要创建postcss.config.js文件,添加cssnano: {}即可去掉重复的样式。
// postcss.config.js
module.exports = {
plugins: {
autoprefixer: {},
cssnano: {}
},
};
更换依赖为项目引入了额外的复杂性,所以这并不是推荐的方法
核心在于chainWebpack的配置,代码来自如下链接。
github.com/yibn2008/fa…
忽略下面的注释,这是我之前做的尝试。
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
chainWebpack: (config) => {
config.module.rules.delete('scss')
let scssRule = config.module.rule('scss')
.test(/\.scss$/);
[
{ name: 'vue-style-loader' },
{ name: 'css-loader' },
{ name: 'postcss-loader' },
{ name: 'fast-sass-loader' }
].forEach((load) => {
scssRule
.use(load.name)
.loader(load.loader || load.name)
.options(load.options || {})
})
},
// configureWebpack: {
// module: {
// rules: [
// {
// test: /\.(scss|sass)$/,
// use: [
// 'css-loader',
// {
// loader: 'fast-sass-loader',
// options: {
// // includePaths: [... ]
// }
// }
// ]
// },
// // other loaders ...
// ]
// }
// }
})
fast-sass-loader解决了这个问题,但是官方并没有给出vue-cli中的合理使用方式。
我找了很久如何在vue中使用这个东西。
当我直接修改vue中的webpack配置,卸载了sass-loader,完全没有作用。
包括github issue中有部分人也尝试使用这个工具,他们的配置也失败了,说明这不是个例。![]()
假如我要新加一个scss变量呢?
不推荐这种削足适履的方式
我没有尝试这种方式,但这种方式在原理上是可行的,因为他完全避开了问题,当使用css文件时,就不会编译,自然也就不会引发重复样式的问题。
github.com/ElemeFE/ele…
github.com/ElemeFE/ele…
如果可以,我真不想用vue2和element。
随着互联网技术的快速发展和宠物经济的持续升温,宠物用品电商平台已成为宠物主人购买宠物用品的主要渠道。设计并实现了一个基于Vue3和Node.js的全栈宠物用品购物系统,该系统采用前后端分离架构,包含用户购物系统和后台管理系统两个子系统。
系统前端采用Vue 3框架,结合TypeScript、Pinia状态管理、Vue Router路由管理和Element UI Plus组件库,实现了响应式的用户界面和流畅的交互体验。后端采用Node.js和Express框架,使用MongoDB作为数据库,通过JWT实现用户身份认证,构建了RESTful风格的API接口。系统实现了用户注册登录、商品浏览搜索、购物车管理、订单处理、社交互动、后台管理等核心功能。
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
前端
后端
# 安装根目录依赖
pnpm install
确保 MongoDB 服务已启动并运行在 localhost:27017
pnpm run import
这将自动导入:
pnpm run dev
启动后访问:
本系统采用前后端分离的架构模式,具有以下优势:
1. 职责分离
2. 技术独立
3. 团队协作
4. 可扩展性
┌─────────────────────────────────────────────────────────┐
│ 客户端层 │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ 用户系统 │ │ 管理系统 │ │
│ │ (Vue 3) │ │ (Vue 3) │ │
│ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────┘
│
│ HTTP/HTTPS
│ RESTful API
▼
┌─────────────────────────────────────────────────────────┐
│ 服务端层 │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Express 应用服务器 │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ 路由层 │ │ 中间件层 │ │ 控制器层 │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│
│ Mongoose ODM
▼
┌─────────────────────────────────────────────────────────┐
│ 数据层 │
│ ┌──────────────────────────────────────────────────┐ │
│ │ MongoDB 数据库 │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ 用户集合 │ │ 商品集合 │ │ 订单集合 │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
前端架构
用户系统 / 管理系统
├── Vue 3 (核心框架)
├── TypeScript (类型系统)
├── Pinia (状态管理)
├── Vue Router (路由管理)
├── Element UI Plus (UI组件库)
├── Axios (HTTP客户端)
└── Vite (构建工具)
后端架构
API服务器
├── Node.js (运行环境)
├── Express (Web框架)
├── MongoDB (数据库)
├── Mongoose (ODM)
├── JWT (身份认证)
├── Bcrypt (密码加密)
└── Multer (文件上传)
用户购物系统功能模块
用户购物系统
├── 用户管理模块
│ ├── 用户注册
│ ├── 用户登录
│ ├── 个人信息管理
│ └── 收货地址管理
├── 商品展示模块
│ ├── 首页展示
│ ├── 商品列表
│ ├── 商品详情
│ └── 商品搜索
├── 购物功能模块
│ ├── 购物车管理
│ ├── 订单创建
│ ├── 订单查询
│ └── 订单评价
└── 社交功能模块
├── 动态发布
├── 动态浏览
├── 点赞评论
└── 用户关注
后台管理系统功能模块
后台管理系统
├── 系统概览模块
│ ├── 数据统计
│ ├── 销售图表
│ └── 订单统计
├── 商品管理模块
│ ├── 商品列表
│ ├── 商品编辑
│ ├── 分类管理
│ └── 库存管理
├── 订单管理模块
│ ├── 订单列表
│ ├── 订单详情
│ ├── 发货处理
│ └── 退款处理
├── 用户管理模块
│ ├── 用户列表
│ ├── 用户详情
│ └── 用户状态管理
└── 数据统计模块
├── 销售统计
├── 商品排行
└── 用户分析
系统主要包含以下实体:
实体关系:
用户注册流程:
用户填写信息 → 前端验证 → 发送注册请求 → 后端验证 → 密码加密 →
存入数据库 → 生成 Token → 返回用户信息和 Token → 前端存储 Token →
自动登录 → 跳转首页
用户登录流程:
用户输入账号密码 → 前端验证 → 发送登录请求 → 后端查询用户 →
验证密码 → 生成 Token → 返回用户信息和 Token → 前端存储 Token →
跳转首页
核心技术点:
bcrypt 是一种专门为密码存储设计的哈希算法,具有以下特点:
// 密码加密实现
import bcrypt from 'bcryptjs';
// 注册时加密密码
const hashPassword = async (password) => {
// 生成盐值,10 是成本因子(cost factor)
// 成本因子越高,计算越慢,安全性越高
const salt = await bcrypt.genSalt(10);
// 使用盐值加密密码
const hashedPassword = await bcrypt.hash(password, salt);
return hashedPassword;
};
// 登录时验证密码
const verifyPassword = async (inputPassword, storedPassword) => {
// bcrypt.compare 会自动提取盐值进行比较
const isMatch = await bcrypt.compare(inputPassword, storedPassword);
return isMatch;
};
为什么不使用 MD5 或 SHA?
JWT(JSON Web Token)是一种无状态的身份认证方案,特别适合前后端分离架构。
JWT 结构:
JWT = Header.Payload.Signature
Header(头部):
{
"alg": "HS256", // 签名算法
"typ": "JWT" // Token 类型
}
Payload(载荷):
{
"userId": "64f8a1b2c3d4e5f6a7b8c9d0",
"username": "testuser",
"role": "user",
"iat": 1704067200, // 签发时间
"exp": 1704672000 // 过期时间
}
Signature(签名):
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
JWT 工作流程:
1. 用户登录成功
↓
2. 服务器生成 JWT Token
- 将用户信息编码到 Payload
- 使用密钥签名,防止篡改
↓
3. 返回 Token 给客户端
↓
4. 客户端存储 Token(localStorage 或 sessionStorage)
↓
5. 后续请求携带 Token
- 在 HTTP Header 中添加:Authorization: Bearer <token>
↓
6. 服务器验证 Token
- 验证签名是否有效
- 检查是否过期
- 提取用户信息
↓
7. 处理业务逻辑
JWT 实现代码:
import jwt from 'jsonwebtoken';
// 密钥(生产环境应使用环境变量)
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
// 生成 Token
const generateToken = (user) => {
const payload = {
userId: user._id,
username: user.username,
role: user.role
};
// 签发 Token,设置 7 天过期
const token = jwt.sign(payload, JWT_SECRET, {
expiresIn: '7d'
});
return token;
};
// 验证 Token 中间件
const authenticateToken = async (req, res, next) => {
try {
// 从请求头获取 Token
const authHeader = req.headers.authorization;
const token = authHeader && authHeader.split(' ')[1]; // Bearer <token>
if (!token) {
return res.status(401).json({
success: false,
message: '访问令牌缺失'
});
}
// 验证 Token
const decoded = jwt.verify(token, JWT_SECRET);
// 查询用户是否存在且状态正常
const user = await User.findById(decoded.userId);
if (!user || user.status !== 'active') {
return res.status(401).json({
success: false,
message: '用户不存在或已被禁用'
});
}
// 将用户信息附加到请求对象
req.user = {
userId: decoded.userId,
username: decoded.username,
role: decoded.role
};
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
success: false,
message: '令牌已过期,请重新登录'
});
}
return res.status(401).json({
success: false,
message: '无效的访问令牌'
});
}
};
JWT vs Session 对比:
| 特性 | JWT | Session |
|---|---|---|
| 存储位置 | 客户端 | 服务器 |
| 扩展性 | 好(无状态) | 差(需要共享 Session) |
| 性能 | 好(无需查询) | 一般(需要查询 Session) |
| 安全性 | 一般(Token 泄露风险) | 好(服务器控制) |
| 适用场景 | 前后端分离、微服务 | 传统 Web 应用 |
JWT 安全注意事项:
最近在学习大模型的实现原理,为了更好地理解整个流程,我把学习中的关键概念和实现细节整理成了笔记。一方面帮助自己梳理思路、加深印象,另一方面也作为日后复习和查阅的基础。内容会按照模型的实际处理流程展开,希望能以清晰易懂的方式记录大模型的核心机制。
大模型最经典的架构图来自《Attention Is All You Need》,从这张图可以看到 Transformer 的基础结构是由“编码器”和“解码器”两部分组成的。虽然现在的大模型(像 GPT、LLaMA 这种)大多只保留了右侧的解码器结构,但它们整体的工作流程仍然遵循 Transformer 的思路。
整体原理可以简单理解成:
整体来说,大模型的生成过程并不是一次性输出整段文本,而是每次只预测一个 token,然后把它接回去继续算,直到生成结束。
![]()
输入预处理虽然在 Transformer 的架构图中只占了一小块,但如果把整个生成流程拆开来看,它其实是整个大模型的第一步,也是后面所有计算的基础。输入处理得好不好,直接影响到模型能不能正确理解你的话。
在模型训练之前,会先收集海量的文本数据,然后训练一个 tokenizer。它的作用就是:
token 不一定是字,也不一定是词,更不是固定长度。现代 tokenizer 通常是“子词模式”,比如:
我 | 今天 | 吃 | 了 | 橙 | 子
happy → hap | py
unbelievable → un | believe | able
也就是说,词表中既可能有完整的词,也可能是词的一部分,这样可以极大减少词表大小,让模型处理能力更灵活。
当用户输入一句话,比如:
我今天想吃火锅
模型不会直接拿这个句子处理,而是:
得到的结果类似这样的一个数组:
[123, 520, 11, 98, 8801]
也就是数字形式的 token 序列。
模型不能直接理解 token id,因为 token id 只是一个“编号”,不包含任何语义。所以下一步是通过 embedding table,把 token id 转换成对应的向量:
向量的意义是:
让模型通过数字之间的关系来“理解”语言,比如相似的词向量更接近。
Transformer 最大的特点是:
它的注意力机制没有顺序意识。
换句话说,如果没有额外的位置信息,它只知道有哪些 token,不知道谁在前、谁在后。
这会导致严重的问题,比如:
对模型来说,单看 token 本身完全一样,只是顺序不同,所以必须把位置告诉模型。
因此,模型会为每个 token 生成一个位置 embedding。
早期 Transformer 位置 embedding是正弦余弦序列,现代模型常用更先进的 RoPE(旋转位置编码)。但无论哪种方法,目的都是:
告诉模型“你现在看到的是第 1 个、第 2 个、第 3 个 token…”
模型最终接收的是:
token 本身表达的含义(token embedding)
+
它在句子中的顺序(position embedding)
早期 Transformer 是直接做向量加法:
final_embedding = token_embedding + position_embedding
现代模型虽然底层机制更复杂(比如 RoPE 会作用到注意力的 Q、K 上),但整体来说:它们都是在让模型同时知道“词的语义”和“词的位置”。
这两个 embedding 合并之后,就是最终送入 Transformer Block 的输入。
假设一句话拆成 10 个 token,每个 embedding 是 4096 维,那么模型的实际输入会是一个:
10 × 4096 的矩阵
这就是 Transformer 后面所有自注意力、多头机制和深层计算的起点。
输入预处理的整个流程可以总结为:
把文本 → token → token id → token embedding → 加上位置 embedding → 得到最终的输入向量矩阵,送进 Transformer。
它解决了三件关键问题:
当这三步都准备好了,Transformer 才真正进入“理解和生成”的阶段。
兄弟们,咱们先看一眼你们项目的 build 产物。
是不是有个 index.js 或者 main.js,体积高达 2MB、3MB 甚至更大?
这就好比你去餐厅吃饭,你只是想点一盘花生米(首屏登录页),结果服务员把后厨里所有的鱼翅燕窝鲍鱼(后台管理系统、富文本编辑器、Echarts 图表库)全部端上了桌,还把门堵住说:“不吃完不许走!”。
用户的 4G 信号在哭泣,手机 CPU 在发烫。 首屏加载时间(FCP)长达 5 秒,用户早就关掉页面去看抖音小姐姐了。
今天,我们要给你的 React 项目做个抽脂手术。我们要用到 Code Splitting(代码分割) 和 Lazy Loading(懒加载),把那个巨大的 JS 文件切成无数个小块,只让用户加载他当前需要的东西。
绝大多数的 React 项目都是 SPA(单页应用)。 默认情况下,打包工具(Webpack/Vite)会把所有页面的代码打包进一个文件。 哪怕用户只访问首页,他也得下载“个人中心”、“设置”、“关于我们”的代码。
这是最大的浪费。
import React from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// 💀 致命伤:静态引入。
// 只要 App.js 被加载,Dashboard 和 Settings 的代码也就跟着被下载了
import Home from './pages/Home';
import Dashboard from './pages/Dashboard';
import Settings from './pages/Settings';
const App = () => (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</BrowserRouter>
);
我们要用 React.lazy 配合 import() 动态引入,再加个 Suspense 来处理加载过程中的空窗期。
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import LoadingSpinner from './components/LoadingSpinner';
// ✨ 魔法在这里:动态引入
// 只有当路由匹配到 /dashboard 时,浏览器才会去下载 Dashboard.js
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const App = () => (
<BrowserRouter>
{/* ⏳ Suspense 是必须的:在组件下载下来之前,先给用户看个转圈圈 */}
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</BrowserRouter>
);
就改了这么几行代码,你的 main.js 体积可能瞬间减少 50% 以上。首屏速度直接起飞。
切完路由就完事了吗? No No No。有些时候,同一个页面里也有巨大的胖子。
场景:你有一个“数据分析”页面,平时只展示列表。只有当用户点击“查看图表”按钮弹出一个 Modal 时,才需要渲染一个巨大的 ECharts 或者 Recharts 图表。 这玩意儿动不动就几百 KB。
如果用户根本不点那个按钮,这几百 KB 不就白下载了?
// 💀 哪怕不渲染,import 进来了就会被打包
import HeavyChart from './components/HeavyChart';
import HeavyEditor from './components/HeavyEditor';
const AnalysisPage = () => {
const [showChart, setShowChart] = useState(false);
return (
<div>
<button onClick={() => setShowChart(true)}>看图</button>
{/* 虽然条件渲染了,但代码早就下载好了 */}
{showChart && <HeavyChart />}
</div>
);
};
✅ 极致懒人写法:
// ✨ 只有用到我的时候,才来喊我
const HeavyChart = lazy(() => import('./components/HeavyChart'));
const AnalysisPage = () => {
const [showChart, setShowChart] = useState(false);
return (
<div>
<button onClick={() => setShowChart(true)}>看图</button>
{showChart && (
<Suspense fallback={<div>图表加载中...</div>}>
<HeavyChart />
</Suspense>
)}
</div>
);
};
听到切代码能优化性能,有些兄弟兴奋了,拿起刀就是一顿乱切。 把 Button、Icon、Text 全部懒加载。
千万别!
Suspense,用户一进来看到满屏的 Loading 转圈,体验比白屏还差。切割原则:
懒加载有一个小缺点:用户点击的时候才开始下载,会有几百毫秒的延迟。 如果要在性能和体验之间求极致,我们可以玩预加载。
比如:用户鼠标悬停在“查看图表”按钮上时,我们猜他大概率要点击了,这时候偷偷开始下载。
// 或者写个简单的函数
const prefetchChart = () => {
const component = import('./components/HeavyChart');
};
<button
onMouseEnter={prefetchChart} // 鼠标放上去就开始下
onClick={() => setShowChart(true)}
>
看图
</button>
现在的打包工具(Vite/Webpack)已经非常智能了,但它们不懂你的业务。它们不知道哪个页面是核心,哪个组件是冷门。
Code Splitting 就是把你对业务的理解告诉工具: “这个首页要最快速度出来!” “那个富文本编辑器,等用户真要写文章了再去加载!”
把你的应用从“一块大石头”变成“一堆小积木”,按需拿取。这才是现代前端工程化的精髓。
好了,我要去把那个引入了整个 lodash 却只用了一个 debounce 函数的屎山代码给优化了。
下期预告:你还在用
console.log调试代码吗?你还在面对满屏的红字不知所措吗? 下一篇,我们要聊聊 “React 调试神技” 。带你深入 React DevTools,看看那些你从未点过的按钮,是如何让你像 X 光一样看穿组件的。
前情回顾:在前两篇文章中,我们学习了Rust的所有权与借用机制,以及Copy、Clone、Send、Sync等trait。今天,我们要探索Rust中最让初学者"闻风丧胆"的概念——生命周期(Lifetime)。
想象你在一家图书馆借书。你拿到一张借书卡(引用),兴高采烈地准备去书架找书。结果走到半路,图书管理员突然把那本书给烧了(数据被释放)。你拿着借书卡傻眼了——这不是空指针吗?
fn main() {
let r;
{
let x = 5;
r = &x; // 编译器:停!x马上要死了,你不能引用它!
} // x的生命在此结束
println!("r: {}", r); // 💥 悬垂引用!
}
在C/C++中,这段代码会编译通过,然后在运行时给你一个"惊喜"。但Rust编译器会直接拒绝编译:
error[E0597]: `x` does not live long enough
这就是生命周期存在的意义:在编译期就确保所有引用都是有效的,彻底消除悬垂引用、野指针等内存安全问题。
考虑这个看似简单的函数:
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
编译器看到这段代码会陷入沉思:
编译器:我太难了😭
这时就需要我们显式地告诉编译器生命周期关系。
生命周期不是什么玄学,它就是引用保持有效的作用域范围。可以把它想象成:
'a、'b等):像给引用贴上有效期标签fn main() {
let string1 = String::from("long string");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
// result的生命周期不能超过string2
} // string2在这里结束
// println!("{}", result); // 💥 string2已经不在了!
}
&i32 // 普通引用
&'a i32 // 带生命周期标注的引用
&'a mut i32 // 带生命周期标注的可变引用
'a读作"tick a",就像给引用贴了个标签:"嘿,我的有效期是'a"。
回到之前的longest函数:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
这个标注告诉编译器:
x和y都在生命周期'a内有效'a内有效'a是x和y生命周期的交集(较短的那个)用大白话说就是:"返回值的有效期不会超过两个参数中最短的那个"。
fn main() {
let string1 = String::from("long string is long");
{
let string2 = String::from("xyz");
let result = longest(string1.as_str(), string2.as_str());
println!("The longest string is {}", result);
// ✅ 在string2的作用域内使用result,完全OK
}
}
好消息!大多数情况下不需要手动标注生命周期,编译器会自动推导。这得益于三条生命周期省略规则:
规则1: 每个引用参数都有自己的生命周期
// 你写的
fn first_word(s: &str) -> &str {
// 编译器理解的
fn first_word<'a>(s: &'a str) -> &str {
规则2: 如果只有一个输入生命周期参数,它被赋予所有输出生命周期
// 你写的
fn first_word(s: &str) -> &str {
// 编译器理解的
fn first_word<'a>(s: &'a str) -> &'a str {
规则3: 如果有多个输入生命周期参数,但其中一个是&self或&mut self,那么self的生命周期被赋予所有输出生命周期
impl<'a> ImportantExcerpt<'a> {
// 你写的
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention: {}", announcement);
self.part
}
// 编译器理解的(self的生命周期赋给返回值)
fn announce_and_return_part<'b>(&'a self, announcement: &'b str) -> &'a str {
println!("Attention: {}", announcement);
self.part
}
}
结构体中包含引用时,必须标注生命周期:
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let excerpt = ImportantExcerpt {
part: first_sentence,
};
// excerpt的生命周期不能超过novel
}
这个标注意味着:ImportantExcerpt的实例不能比它引用的part活得更久。
有时需要不同的生命周期参数:
fn announce<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
println!("Announcement: {}", y);
x // 只返回x,所以返回值生命周期只跟'a关联
}
'static
'static是一个特殊的生命周期,表示"活到程序结束":
let s: &'static str = "I have a static lifetime.";
// 字符串字面量存储在程序的二进制文件中,永远有效
注意:不要滥用'static!看到生命周期错误就加'static是新手常犯的错误。
// ❌ 错误:返回局部变量的引用
fn dangle() -> &str {
let s = String::from("hello");
&s
} // s在这里被释放,返回悬垂引用
// ✅ 正确:返回所有权
fn no_dangle() -> String {
let s = String::from("hello");
s
}
struct Book<'a> {
title: &'a str,
author: &'a str,
}
impl<'a> Book<'a> {
fn new(title: &'a str, author: &'a str) -> Self {
Book { title, author }
}
fn get_title(&self) -> &str {
// 省略了生命周期,编译器自动推导为&'a str
self.title
}
}
结合泛型使用:
use std::fmt::Display;
fn longest_with_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: Display,
{
println!("Announcement: {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}
'static
// ❌ 错误思路
fn bad_fix<'a>(x: &'a str) -> &'static str {
x // 💥 生命周期不匹配!
}
// ✅ 正确思路
fn good_fix<'a>(x: &'a str) -> &'a str {
x
}
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+
'b比'a短,所以r不能引用x。
// ❌ 这个不能直接编译
struct SelfRef<'a> {
value: String,
pointer: &'a String, // 想引用自己的value
}
自引用需要使用Pin等高级技巧,初学者建议避免。
生命周期本质上是所有权系统的延伸:
它们共同构成了Rust内存安全的铁三角。
生命周期是Rust的"杀手锏",也是初学者的"拦路虎"。但记住:
当你习惯了生命周期,你会发现它就像一位严格但负责的老师——虽然严厉,但确实让你写出更安全的代码。
下一篇我们将探索Rust的错误处理机制——如何优雅地处理Result和Option。Stay tuned!
练习题:试着理解并修复以下代码的生命周期问题
fn main() {
let string1 = String::from("abcd");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {}", result);
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
提示:问题出在哪里?如何调整代码结构来修复?
在技术演进的浪潮中,我们正站在一个关键拐点上:AI 不再只是“辅助工具”,而是成为应用的核心驱动力。与此同时,移动设备早已超越 PC,成为用户与数字世界交互的第一入口。如何将 AI First 与 Mobile First 的理念深度融合,打造真正智能、高效、普惠的新一代应用?本文将从实践出发,结合真实代码案例,探讨一条可落地的技术路径。
“AI First” 并非口号,而是一种产品设计哲学:以大语言模型(LLM)为核心引擎,重构用户交互逻辑和系统架构。
想象这样一个场景:
“豆包,帮我点杯少糖热奶茶,在美团、抖音、淘宝上比价,用上优惠券,选最便宜的那家下单。”
这背后涉及:
传统方式需要分别调用各平台 API、维护复杂的业务规则。而在 AI Agent 架构下,LLM 作为“调度中枢”,通过自然语言理解用户意图,动态调用工具链(Tool Calling),实现端到端自动化。
这就是 AI Native 应用的雏形——用户只需表达“做什么”,系统自动完成“怎么做”。
要让 AI 操作业务数据,关键一步是 打通自然语言与结构化数据的鸿沟。Text-to-SQL 正是这一桥梁。
我们以一个员工管理系统为例:
# 表结构
CREATE TABLE EMPLOYEES (
id INTEGER
name TEXT
department TEXT
salary INTEGER
)
当用户问:“工程部门员工的姓名和工资是多少? ”
我们将表结构(Schema)作为上下文注入 Prompt:
这是一个数据库的Schema:
CREATE TABLE EMPLOYEES (
id INTEGER
name TEXT
department TEXT
salary INTEGER
)
根据这个Schema,请输出一个SQL查询来回答以下问题。
只输出SQL查询语句本身……
问题:工程部门员工的姓名和工资是多少
LLM 返回:
SELECT name, salary FROM employees WHERE department = '工程';
执行后得到结果:
[('宁宁', 75000), ('悦悦', 80000), ('呆鱼', 80000)]
更惊人的是,它还能处理 增删改 操作:
INSERT INTO employees (name, department, salary) VALUES ('张三', '销售', 45000);
DELETE FROM employees WHERE name = '黄仁勋' AND department = '市场';
这意味着:非技术人员也能安全地操作数据库。后台管理不再局限于程序员,运营、产品、小编均可参与——这就是“数据库平权”。
“Mobile First” 常被误解为“先做移动端,再适配 PC”。但真正的 Mobile First 是:
@media 实现响应式布局,但默认样式按手机设计
在 AI 赋能下,移动端还可集成语音输入、图像识别(如拍菜单点单),进一步降低交互成本。
阿里云的 ModelScope(魔搭) 为开发者提供了强大基础设施:
例如,通过 ModelScope 微调一个“奶茶点单专用模型”,可显著提升对“少糖去冰加布丁”等口语化指令的理解准确率。
当 LLM 能理解用户意图、操作应用、调用服务、修改数据,传统的 App 界面可能不再是必需品。
未来的交互可能是:
而移动端,因其随身性、传感器丰富性、推送能力,将成为 AI Agent 的最佳载体。
我们正在从“人适应软件”走向“软件适应人”。
在 AI First 时代,开发者不再是“功能实现者”,而是:
拥抱变化,从今天开始:
让你的下一个项目,先问一句——“AI 能怎么帮用户做得更好?”
![]()
![]()
前端er Go-Frame 的学习笔记:实现 to-do 功能(一)
上一章已经把后端实现了大概的功能,目前写一下前端,在构建的过程中可能要改改后端不合理的地方,比如:
todo => todos
alova 的使用要学习一下前端的话,我想看一下 Alova 的使用方法,然后想一下如何简化前端,只用写资源名字,即可做到增删改查,
技术栈的话,选择 React 19 + Antd 6 + Alova
在项目根目录下,用 vite 的脚手架来搭建项目
pnpm create vite
然后输入项目名,选择框架等等
搭建好基础之后,把 antd 和 alova 安装一下
再安装一些 eslint, antfu-eslint,具体的使用配置可以看这个:github.com/antfu/eslin…
pnpm i -D eslint @antfu/eslint-config
然后写一下 eslint-config.js
import { antfu } from '@antfu/eslint-config'
// 第一个对象是基础配置(你没写东西)
// 第二个对象是覆盖 antfu 内置规则
export default antfu({
}, {
rules: {
'no-console': 'off', // 允许使用 console.log,不再警告
'prefer-const': 'off', // 允许使用 let,不强制要求用 const
'ts/ban-ts-comment': 'off', // 允许使用 @ts-ignore / @ts-nocheck 等注释
'no-case-declarations': 'off', // 允许在 switch/case 里直接写 const/let
'ts/no-use-before-define': 'off', // 允许变量或函数在定义前被使用
'ts/no-unused-expressions': 'off', // 允许类似条件 && 表达式的写法
'ts/no-empty-object-type': 'off', // 允许定义空对象类型 type A = {}
'ts/no-unsafe-function-type': 'off', // 允许使用 any 函数签名 (...args: any[]) => any
'ts/consistent-type-definitions': 'off', // 不强制只能用 type 或 interface,随便写
'style/indent': ['error', 4], // 强制使用 4 空格缩进
'style/jsx-indent-props': ['error', 4], // JSX 属性缩进也是 4 空格
'prefer-promise-reject-errors': 'off', // 允许 reject('xxx'),不强制必须 new Error()
'eslint-comments/no-unlimited-disable': 'off', // 允许写 /* eslint-disable */ 禁用所有规则
},
})
ok,依赖安装完之后,来看看这个空的项目,配置一下环境,以及 vite 的代理 proxy,和 tsconfig.json
ide 代码编辑器的配置,比方说 @ 这种 alias
vite.config.json 中的 @ 用于打包时候的描述,以及 proxy 代理请求后端接口![]()
![]()
![]()
还记的写后端时遇到的疑问吗 goframe.org/quick/scaff…,为什么用 /api/v1 当借口的前缀
目前后端是没有接口路由分组的,所以来改一下后端,让前端以后通过 /api/v1/todo 来访问
![]()
前端的话,要改这几个文件
Alova 的实例的 baseUrl 改一下.env 文件也改改这些就算是前端的配置了
![]()
// api/alova.ts
import { createAlova } from 'alova'
import adapterFetch from 'alova/fetch'
import ReactHook from 'alova/react'
export const alovaInstance = createAlova({
baseURL: '/api/v1', // 这里会自动拼接到每个接口的前面的
requestAdapter: adapterFetch(),
responded: response => response.json(),
statesHook: ReactHook, // 如果写 react 的话,要引入这个,不然白屏
})
![]()
然后写一下页面
// 页面 todo.tsx
import { useRequest } from 'alova/client'
import { Button } from 'antd'
import { alovaInstance } from '@/api/alova'
export function PageTodo() {
const { data } = useRequest(
alovaInstance.Get('/todo'),
)
console.log('data:>>', data)
return (
<>
<div>
这是 todo 的页面
</div>
<Button>点我123312</Button>
</>
)
}
然后再浏览器中试一下,可以看到前端能请求到接口了!
这里其实并没有那么顺利 当我没有改后端的
/api/v1分组之前,前端是请求不到后端的,我以为是后端没有配置跨域,(后来我也没去配置go-frame的跨域,因为不是这个问题) 是因为什么呢,就是vite的代理,我以为前端请求的/api/v1/todo,就会走代理,之后代理会把api/v1给去掉,但是这个想法是错误的
![]()
我准备把 tailwindcss 也安装一下,具体怎么安装可以看一下这个 https://tailwindcss.com/docs/installation/using-vite
然后让 AI 先写一个简单的页面
![]()
之后把其他功能实现一下
![]()
不过我看每次更新之后命中了 alova 的缓存了,导致刷新之后页面的数据没有更新(但是数据库已经更改了)
![]()
查了一下官方文档, 这里有个强制请求,我可以在 Alova 中配置一下这个接口
![]()
![]()
接下来准备把这两个项目放到 Docker 中
<component
:is="tag"
ref="_ref"
v-bind="_props"
:class="buttonKls"
:style="buttonStyle"
@click="handleClick"
>
:style="buttonStyle":用于在设置了 color 时,自动计算并应用按钮各状态(默认、悬停、激活、禁用)的颜色样式,无需手动设置每个状态的颜色。
const buttonStyle = useButtonCustomStyle(props)
/**
* 获取实例中props为name的值
*/
export const useProp = <T>(name: string): ComputedRef<T | undefined> => {
const vm = getCurrentInstance()
return computed(() => (vm?.proxy?.$props as any)?.[name])
}
/**
* 获取表单的disabled状态
* @param fallback 默认值
* @returns 表单的disabled状态
*/
export const useFormDisabled = (fallback?: MaybeRef<boolean | undefined>) => {
const disabled = useProp<boolean>('disabled')
const form = inject(formContextKey, undefined)
// 如果是表单内部的button那么是有值的,如果是外部的button那么是undefined
console.log('form', form)
/**
* 组件自身的 disabled prop
↓ (如果没有)
传入的 fallback 参数
↓ (如果没有)
表单的 disabled 状态
↓ (如果没有)
默认值 false
*/
return computed(
() => disabled.value || unref(fallback) || form?.disabled || false
)
}
/**
* 获取按钮自定义样式
* @param props
* @returns
*/
export function useButtonCustomStyle(props: ButtonProps) {
// 获取按钮的disabled状态
const _disabled = useFormDisabled()
// 获取按钮的命名空间
const ns = useNamespace('button')
// calculate hover & active color by custom color
// only work when custom color
return computed(() => {
let styles: Record<string, string> = {}
let buttonColor = props.color
if (buttonColor) {
// 检测buttonColor是否为CSS变量格式 ,并提取变量名 如 var(--el-color-primary)
const match = (buttonColor as string).match(/var\((.*?)\)/)
if (match) {
buttonColor = window
.getComputedStyle(window.document.documentElement)
.getPropertyValue(match[1])
}
// TinyColor: Fast, small color manipulation and conversion for JavaScript
const color = new TinyColor(buttonColor)
console.log('color', color)
// tint - 变亮(添加白色)变亮20%
// darken - 变暗(添加黑色)变暗20%
const activeBgColor = props.dark
? color.tint(20).toString()
: darken(color, 20)
if (props.plain) {
styles = ns.cssVarBlock({
'bg-color': props.dark
? darken(color, 90)
: color.tint(90).toString(),
'text-color': buttonColor,
'border-color': props.dark
? darken(color, 50)
: color.tint(50).toString(),
'hover-text-color': `var(${ns.cssVarName('color-white')})`,
'hover-bg-color': buttonColor,
'hover-border-color': buttonColor,
'active-bg-color': activeBgColor,
'active-text-color': `var(${ns.cssVarName('color-white')})`,
'active-border-color': activeBgColor,
})
if (_disabled.value) {
styles[ns.cssVarBlockName('disabled-bg-color')] = props.dark
? darken(color, 90)
: color.tint(90).toString()
styles[ns.cssVarBlockName('disabled-text-color')] = props.dark
? darken(color, 50)
: color.tint(50).toString()
styles[ns.cssVarBlockName('disabled-border-color')] = props.dark
? darken(color, 80)
: color.tint(80).toString()
}
} else {
const hoverBgColor = props.dark
? darken(color, 30)
: color.tint(30).toString()
const textColor = color.isDark()
? `var(${ns.cssVarName('color-white')})`
: `var(${ns.cssVarName('color-black')})`
styles = ns.cssVarBlock({
'bg-color': buttonColor,
'text-color': textColor,
'border-color': buttonColor,
'hover-bg-color': hoverBgColor,
'hover-text-color': textColor,
'hover-border-color': hoverBgColor,
'active-bg-color': activeBgColor,
'active-border-color': activeBgColor,
})
if (_disabled.value) {
const disabledButtonColor = props.dark
? darken(color, 50)
: color.tint(50).toString()
styles[ns.cssVarBlockName('disabled-bg-color')] = disabledButtonColor
styles[ns.cssVarBlockName('disabled-text-color')] = props.dark
? 'rgba(255, 255, 255, 0.5)'
: `var(${ns.cssVarName('color-white')})`
styles[ns.cssVarBlockName('disabled-border-color')] =
disabledButtonColor
}
}
}
return styles
})
}
==========================================
props:native-type
export const buttonNativeTypes = ['button', 'submit', 'reset'] as const
props:
/**
* @description native button type
*/
nativeType: {
type: String,
values: buttonNativeTypes,
default: 'button',
},