普通视图
央行:10月债券市场共发行各类债券63574.6亿元
央行:10月银行间债券市场现券成交26.6万亿元
小米汽车:11月新增17家门店
安康记1.1.x版本发布
JavaScript新手必看系列之预编译
前言
预编译是JavaScript的核心概念,也是新手向中级进阶的必经之路。理解它,意味着你能:
- 彻底搞懂变量提升
- 理解函数声明的“特权”
- 避免常见的作用域陷阱
本文用最简单的语言和示例,带你快速掌握全局预编译与函数预编译的完整过程。
What's that?
简单说,JS在执行代码前会进行“准备工作” —— 预编译。在这个阶段JS的V8引擎会进行变量声明,函数声明的提升
预编译的两种场景
1. 全局作用域下的预编译
分三步:
1.创建 GO(Global Object)
-
在浏览器中就是 window 对象
-
在 Node.js 中就是 global 对象
2.找到变量声明,作为作为GO属性名,值为undefined
-
找到函数声明,作为GO属性名,值为函数体
举个简单的例子:
console.log(a) // undefined
var a = 1
console.log(a) // 1
发什么了什么?
- 创建 GO ={}
- 找到变量声明 var a ,GO = { a : undefined }
- 找函数声明,没有函数声明就不找
执行过程如下:
//预编译后就相当于:
var a = undefined
console.log(a) //输出 undefined
a = 1
console.log(a) //输出 1
2.函数作用域下的预编译
函数较为复杂,四步:
-
创建AO(Activation Object)
-
找形参和变量声明,作为AO属性名,值为 undefined
-
将实参赋给形参
-
找函数声明,作为AO 属性名,值为函数体
举例:
function fn(a) {
console.log(a);
var a = 123
console.log(a);
function a() {}
var b = function() {}
console.log(b);
function c() {}
var c = a
console.log(c);
}
fn(1)
干了什么?
- 创建AO = {
2.找形参和变量声明,作为AO属性名,值为 undefined
AO = { a : undefined
b : undefined
c : undefined }
3.将实参赋给形参
AO = { a : 1
b : undefined
c : undefined
}
4.找函数声明,作为AO 属性名,值为函数体
AO = { a : function a() {}
b : undefined
c : function c() {}
}
函数体内执行过程:
// 预编译后的状态:
var c
var b
var a
console.log(a); // function a() {}
a = 123
console.log(a); // 123
b = function() {}
console.log(b); //function b() {}
c = a
console.log(c); //123
掌握了预编译,你就真正理解了 JavaScript 的执行机制,这对后续学习闭包、作用域链等概念至关重要!
希望这篇文章能帮助你彻底理解 JavaScript 预编译,如果有任何疑问,欢迎在评论区讨论!
swift的inout的用法
江西铜业:拟收购境外上市公司SolGold Plc股份,目前仍处非正式要约阶段
前端跨页面通讯终极指南②:BroadcastChannel 用法全解析
前言
上一篇介绍了PostMessage跨页面通讯的方式。有没有一种更简洁的方式,兄弟页面也能像父子页面一样通讯。今天就介绍一个更高效、更简洁的方案——BroadcastChannel API,它能轻松搞定父子、子父、兄弟页面间的通讯。
1. BroadcastChannel是什么?
BroadcastChannel 接口表示给定源的任何浏览上下文都可以订阅的命名频道。它允许同源的不同浏览器窗口、标签页、frame 或者 iframe 下的不同文档之间相互通信。消息通过 message 事件进行广播,该事件在侦听该频道的所有 BroadcastChannel 对象上触发,发送消息的对象除外。
简单来说,它就像一个“无线电台”,多个页面只要订阅了同一个“频道”(指定相同的频道名称),就能接收该频道发送的所有消息,实现数据的双向流转。
需要注意,BroadcastChannel仅支持同源页面,兼容性略低,需要搭配其他方案作为降级处理。
2. 如何使用
BroadcastChannel的使用流程如下:
- 创建频道
- 订阅消息
- 发送消息
- 关闭订阅
2.1 创建频道(主题)
通过 new BroadcastChannel('channel-name')创建一个“频道”(相当于发布-订阅中的“主题”)。所有加入同一频道的上下文,共享同一个通信通道。
// 1. 创建/订阅指定名称的频道(关键:多页面频道名必须一致)
const channel = new BroadcastChannel('my-channel');
2.2 订阅消息(监听)
通过监听 message事件订阅该频道的消息(相当于订阅者注册回调):
channel.onmessage = (e) => {
console.log('收到消息:', e.data); // e.data 就是发送的消息内容
// 根据消息类型执行对应逻辑
if (e.data.type === 'refresh') {
// 执行刷新操作
}
};
2.3 发布消息(发送)
通过 postMessage()向频道发送消息(相当于发布者触发发布):
channel.postMessage({
type: 'refresh',
data: { id: 123 }
});
2.4 退订(关闭)
通过 close()方法离开频道(可选,浏览器通常会在上下文销毁时自动清理):
window.addEventListener('beforeunload', () => {
channel.close();
});
// 1. 创建/订阅指定名称的频道(关键:多页面频道名必须一致)
const channel = new BroadcastChannel('my-channel');
// 2. 接收消息
channel.onmessage = (e) => {
console.log('收到消息:', e.data); // e.data 就是发送的消息内容
// 根据消息类型执行对应逻辑
if (e.data.type === 'refresh') {
// 执行刷新操作
}
};
// 3. 发送消息(支持字符串、对象等多种数据类型)
channel.postMessage({
type: 'refresh',
data: { id: 123 }
});
// 4. 关闭频道(页面卸载时调用,避免内存泄漏)
window.addEventListener('beforeunload', () => {
channel.close();
});
3、实践场景
下面我们针对前端最常见的父子页面(父窗口打开子窗口)、子父页面(子窗口向父窗口反馈)、兄弟页面(同一父页面打开的多个子窗口)三种场景,分别给出具体的实现代码。
3.1 父子通讯
父页面点击“刷新子页面”按钮,子页面接收到指令后刷新数据。
父页面代码(打开子窗口并发送消息):
// 1. 创建频道
const parentChannel = new BroadcastChannel('parent-child-channel');
// 2. 点击按钮向子窗口发送消息
document.getElementById('refreshChildBtn').addEventListener('click', () => {
parentChannel.postMessage({
type: 'refresh-data',
message: '父页面指令:请刷新数据'
});
});
// 3. 页面卸载时关闭频道
window.addEventListener('beforeunload', () => {
parentChannel.close();
});
子页面代码(接收父页面消息并执行操作):
// 1. 订阅同一个频道(频道名必须和父页面一致)
const childChannel = new BroadcastChannel('parent-child-channel');
// 2. 接收父页面消息
childChannel.onmessage = (e) => {
const { type, message } = e.data;
if (type === 'refresh-data') {
// 显示接收的消息
document.getElementById('message').textContent = message;
// 执行刷新数据的逻辑
refreshData();
}
};
// 刷新数据的核心函数
function refreshData() {
// 模拟请求接口刷新数据
console.log('子页面正在刷新数据...');
// 这里写具体的刷新逻辑
}
// 3. 页面卸载时关闭频道
window.addEventListener('beforeunload', () => {
childChannel.close();
});
3.2 子父通讯(子窗口向父窗口反馈结果)
需求:子页面完成表单提交后,向父页面发送“提交成功”的消息,父页面接收到后关闭子窗口并刷新自身数据。
子页面代码(提交表单后发送消息):
// 1. 订阅频道(和父页面保持一致)
const childChannel = new BroadcastChannel('child-parent-channel');
// 2. 表单提交逻辑
document.getElementById('submitForm').addEventListener('submit', async (e) => {
e.preventDefault();
try {
// 模拟表单提交接口请求
await submitFormData();
// 提交成功后向父页面发送消息
childChannel.postMessage({
type: 'submit-success',
data: { formId: 456, status: 'success' }
});
// 延迟关闭子窗口,确保消息发送完成
setTimeout(() => {
window.close();
}, 300);
} catch (error) {
console.error('提交失败:', error);
}
});
// 3. 页面卸载时关闭频道
window.addEventListener('beforeunload', () => {
childChannel.close();
});
父页面代码(接收子页面消息并执行操作):
// 1. 创建频道
const parentChannel = new BroadcastChannel('child-parent-channel');
// 2. 接收子页面消息
parentChannel.onmessage = (e) => {
const { type, data } = e.data;
if (type === 'submit-success') {
console.log('子页面表单提交成功:', data);
// 执行父页面刷新逻辑
parentRefreshData();
// (可选)关闭子窗口(如果子窗口未自行关闭)
// childWindow.close();
}
};
// 父页面刷新数据函数
function parentRefreshData() {
console.log('父页面正在刷新数据...');
// 具体刷新逻辑
}
// 3. 页面卸载时关闭频道
window.addEventListener('beforeunload', () => {
parentChannel.close();
});
3.3 兄弟通讯(多个子窗口间同步状态)
需求:父页面打开两个子窗口A和B,当子窗口A修改数据后,子窗口B实时同步更新数据。
子窗口A代码(修改数据后发送消息):
// 1. 订阅兄弟通讯频道
const brotherChannel = new BroadcastChannel('brother-channel');
// 2. 模拟修改数据操作
document.getElementById('updateDataBtn').addEventListener('click', () => {
const newData = {
id: 789,
content: '子窗口A修改后的新内容',
updateTime: new Date().toLocaleString()
};
// 保存修改后的数据
saveData(newData);
// 向频道发送消息,通知其他兄弟窗口
brotherChannel.postMessage({
type: 'data-updated',
newData: newData
});
});
// 3. 页面卸载时关闭频道
window.addEventListener('beforeunload', () => {
brotherChannel.close();
});
子窗口B代码(接收消息并同步数据):
// 1. 订阅同一个兄弟通讯频道
const brotherChannel = new BroadcastChannel('brother-channel');
// 2. 接收子窗口A发送的消息
brotherChannel.onmessage = (e) => {
const { type, newData } = e.data;
if (type === 'data-updated') {
console.log('收到子窗口A的更新消息:', newData);
// 同步更新页面数据展示
document.getElementById('dataContent').textContent = newData.content;
document.getElementById('updateTime').textContent = newData.updateTime;
}
};
// 3. 页面卸载时关闭频道
window.addEventListener('beforeunload', () => {
brotherChannel.close();
});
接收输入如下:
![]()
4. 总结
最后总结一下:对比传统的postMessage跨页面通讯方案,BroadcastChannel兄弟页面无需转发,几行代码就能轻松实现通讯。
百利天恒:收到与百时美施贵宝就iza-bren项目里程碑付款2.5亿美元
el-button源码解读3——:class="buttonKls"与颜色系统的关系
说明 :class="buttonKls" 与颜色系统的关系:
1. buttonKls 的作用
buttonKls 是一个计算属性,生成按钮需要的所有 CSS 类名:
const buttonKls = computed(() => [
// el-button
ns.b(),
ns.m(_type.value),
ns.m(_size.value),
ns.is('disabled', _disabled.value),
ns.is('loading', props.loading),
ns.is('plain', _plain.value),
ns.is('round', _round.value),
ns.is('circle', props.circle),
ns.is('text', _text.value),
ns.is('link', props.link),
ns.is('has-bg', props.bg),
])
假设 type="primary",buttonKls 可能包含:
['el-button', 'el-button--primary', 'el-button--small', 'is-loading', ...]
2. 颜色系统的触发机制
颜色系统通过 CSS 类选择器触发。当 buttonKls 包含 'el-button--primary' 时,会匹配到对应的 CSS 规则。
3. 完整的关系链
用户使用: <el-button type="primary">
↓
props.type = 'primary'
↓
_type.value = 'primary'
↓
buttonKls = ['el-button', 'el-button--primary', ...]
↓
:class="buttonKls" → class="el-button el-button--primary"
↓
CSS 匹配: .el-button--primary { ... }
↓
button-variant('primary') 生成 CSS 变量
↓
应用颜色样式
4. 在 CSS 中的对应关系
看 button.scss 中的定义:
@each $type in (primary, success, warning, danger, info) {
@include m($type) {
@include button-variant($type);
}
}
当 $type = 'primary' 时:
-
@include m('primary')→ 生成.el-button--primary选择器 -
@include button-variant('primary')→ 生成颜色相关的 CSS 变量
5. 实际渲染过程
步骤 1:Vue 组件生成类名
<el-button type="primary">按钮</el-button>
// buttonKls 计算后
['el-button', 'el-button--primary']
<!-- 最终渲染 -->
<button class="el-button el-button--primary">
按钮
</button>
步骤 2:CSS 匹配类名
浏览器看到 class="el-button--primary",匹配到:
// button.scss 中生成的
.el-button--primary {
// button-variant('primary') 生成的 CSS 变量
--el-button-bg-color: var(--el-color-primary);
--el-button-border-color: var(--el-color-primary);
--el-button-text-color: var(--el-color-white);
--el-button-hover-bg-color: var(--el-color-primary-light-3);
--el-button-active-bg-color: var(--el-color-primary-dark-2);
}
步骤 3:应用颜色
基础样式使用这些 CSS 变量:
.el-button {
background-color: var(--el-button-bg-color);
border-color: var(--el-button-border-color);
color: var(--el-button-text-color);
}
因为 .el-button--primary 定义了这些变量,所以按钮会显示 primary 的颜色。
6. 关键理解
:class="buttonKls" 是连接 Vue 组件和 CSS 颜色系统的桥梁:
Vue 组件层面 CSS 样式层面
─────────────────────────────────
buttonKls .el-button--primary
↓ ↓
'el-button--primary' → 匹配 CSS 选择器
↓ ↓
应用类名 应用颜色样式
7. 不同类型的关系
| 用户传入 | buttonKls 包含 | CSS 匹配 | 颜色应用 |
|---|---|---|---|
type="primary" |
'el-button--primary' |
.el-button--primary |
蓝色 (#409eff) |
type="success" |
'el-button--success' |
.el-button--success |
绿色 (#67c23a) |
type="warning" |
'el-button--warning' |
.el-button--warning |
橙色 (#e6a23c) |
type="danger" |
'el-button--danger' |
.el-button--danger |
红色 (#f56c6c) |
8. 完整示例
<el-button type="primary" size="small" :loading="true">
提交
</el-button>
生成的类名:
buttonKls = [
'el-button', // 基础类
'el-button--primary', // 类型类(触发颜色系统)
'el-button--small', // 尺寸类
'is-loading' // 状态类
]
最终 HTML:
<button class="el-button el-button--primary el-button--small is-loading">
提交
</button>
CSS 匹配:
.el-button--primary { // ← 这个类触发了颜色系统
--el-button-bg-color: var(--el-color-primary);
// ...
}
9. 总结
-
:class="buttonKls"生成类名(如el-button--primary) - CSS 通过类选择器匹配这些类名
- 匹配到的规则通过
button-variant生成颜色变量 - 基础样式使用这些变量,最终显示对应颜色
核心关系:buttonKls 中的类型类名(如 el-button--primary)是触发颜色系统的开关,CSS 通过这个类名应用对应的颜色样式。
沪电股份:向香港联交所递交H股发行上市申请并刊发申请资料
搞懂作用域链与闭包:JS底层逻辑变简单
JS底层小揭秘:作用域链与闭包,代码+图解一看就懂
在 JavaScript 的学习过程中,理解其底层运行机制是进阶的关键,而作用域链和闭包更是其中的核心概念。,很多人只停留在“会用”,没搞懂底层逻辑。本文结合代码+调用栈图解,从V8引擎的运行机制出发,拆解这两个概念的本质,帮你从底层视角搞懂 JS 的执行规则。
一、先搭好JS底层的基础框架
JS代码能运行,依赖V8引擎的三个核心模块:
- 调用栈:分编译阶段(处理变量/函数提升)和执行阶段(创建执行上下文并压入栈,执行完弹出);
- 执行上下文:全局执行上下文(始终在栈底)+ 函数执行上下文(函数调用时创建);
-
作用域:定义变量的查找范围和生命周期,包含
let/const的块级作用域(依托栈结构的词法环境),以及var的变量提升特性。
二、作用域链:静态的变量查找路径
作用域链(词法作用域链)的核心是:它由函数声明的位置决定,编译阶段就固定了,和调用顺序无关。
案例1:为什么bar里的myName取全局值?
对应代码与图示:
function bar(){
console.log(myName); // 输出“极客时间”
}
function foo() {
var myName = '极客邦'
bar() // 在foo内部调用bar
}
var myName = '极客时间'
foo();
![]()
![]()
运行逻辑拆解:
-
编译阶段:
bar和foo被声明在全局作用域,因此它们的作用域链默认“自身→全局”; -
执行阶段:
-
foo调用时,创建foo执行上下文(变量环境包含myName="极客邦")并压入栈; -
foo内部调用bar,创建bar执行上下文并压入栈; -
bar中查找myName:自身变量环境无→通过outer指向的全局执行上下文查找→取全局的"极客时间"。
-
案例2:块级作用域下的变量查找
对应代码与图示:
function bar () {
var myName = '极客世界';
let test1 = 100;
if (1) {
let myName = "Chrome 浏览器";
console.log(test);
}
}
function foo() {
var myName = '极客邦';
let test = 2;
{
let test = 3;
bar();
}
}
var myName = '极客时间';
let myAge = 10;
let test = 1;
foo();
![]()
查找过程:
-
bar内部if块的console.log(test),先查自身块级词法环境(只有myName="Chrome 浏览器")→ 没找到; - 查
bar函数的词法环境(有test1=100)→ 没找到; - 查
bar的变量环境(有myName="极客世界")→ 没找到; - 查全局执行上下文的词法环境(有
test=1)→ 但由于bar的作用域链是“自身块级→bar函数→全局”,运行中会输出1。
三、闭包:函数“背着”变量的专属背包
闭包是词法作用域的延伸——外部函数执行后,其内部变量被嵌套函数引用,因此不会被垃圾回收,形成一个“变量背包”供嵌套函数使用。
案例:闭包的形成与运行
对应代码与图示:
function foo() {
var myName = '极客时间'
let test1 = 1
const test2 = 2
var innerBar = {
getName: function() {
console.log(test1) // 输出1
return myName
},
setName: function(newName) {
myName = newName // 修改foo内部的myName
}
}
return innerBar
}
var bar = foo() // foo执行上下文出栈
bar.setName("极客邦")
console.log(bar.getName()); // 输出1 + “极客邦”
![]()
![]()
闭包运行流程:
-
foo执行时:创建执行上下文,变量环境存储
myName="极客时间",词法环境存储test1=1、test2=2,并压入栈; -
foo返回innerBar后:foo执行上下文出栈,但
getName/setName引用了myName和test1,这两个变量被保留在内存中(形成闭包,即“专属背包”); -
调用bar.setName/getName时:
-
setName执行时,通过闭包找到myName并修改为"极客邦"; -
getName执行时,通过闭包找到test1并输出1,同时返回修改后的myName。
-
四、核心总结
- 作用域链是静态的:由函数声明位置决定,编译阶段固定,和调用顺序无关;
- 闭包是词法作用域的延伸:嵌套函数引用外部函数变量,导致外部函数变量不被回收,形成“变量背包”;
- 底层逻辑的关键:理解调用栈、执行上下文、作用域的关系,是搞懂JS变量查找和内存管理的基础。
JavaScript 的底层运行机制中,词法作用域是基础,它决定了作用域链的静态查找规则,而闭包则是词法作用域的延伸,通过保留自由变量实现了函数对外部作用域的持久访问。理解这些概念,不仅能帮助我们写出更符合 JS 运行逻辑的代码,还能解决实际开发中变量作用域、内存泄漏等常见问题。掌握作用域链与闭包,是深入理解 JavaScript 语言特性的重要一步。
element-plus源码解读3——【scss】颜色系统完整流程
一、基础颜色定义(源头)
位置:packages/theme-chalk/src/common/var.scss
scss知识点:$ 表示变量
- 定义所有颜色的基础值
// types
$types: primary, success, warning, danger, error, info;
// Color
$colors: () !default;
$colors: map.deep-merge(
(
'white': #ffffff,
'black': #000000,
'primary': (
'base': #409eff,
),
'success': (
'base': #67c23a,
),
'warning': (
'base': #e6a23c,
),
'danger': (
'base': #f56c6c,
),
'error': (
'base': #f56c6c,
),
'info': (
'base': #909399,
),
),
$colors
);
二、自动生成颜色变体(light/dark)
位置:packages/theme-chalk/src/common/var.scss
定义一个mixin: set-color-mix-level:
将基础颜色与白色或黑色混合
生成指定级别的浅色或深色变体
将生成的颜色添加到$colors map中
// mix colors with white/black to generate light/dark level
@mixin set-color-mix-level(
$type,
$number,
$mode: 'light',
$mix-color: $color-white
) {
$colors: map.deep-merge(
(
$type: (
/**
* roundColor:将颜色的 RGB 通道值四舍五入为整数,并返回 rgba() 格式的颜色。
* color.mix:混合颜色 第一个参数和第二个参数按照第三个参数的百分比混合颜色
* map.get($colors, $type, 'base'):获取颜色
* math.percentage(math.div($number, 10)):将数字转换为百分比
*/
'#{$mode}-#{$number}': roundColor(
color.mix(
$mix-color,
map.get($colors, $type, 'base'),
math.percentage(math.div($number, 10))
)
),
),
),
$colors
) !global;
}
使用
// $colors.primary.light-i
// --el-color-primary-light-i
// 10% 53a8ff
// 20% 66b1ff
// 30% 79bbff
// 40% 8cc5ff
// 50% a0cfff
// 60% b3d8ff
// 70% c6e2ff
// 80% d9ecff
// 90% ecf5ff
// 外层循环:遍历颜色类型
@each $type in $types {
// 内层循环:生成 1-9 的变体
@for $i from 1 through 9 {
// 调用 mixin 生成不同亮度的颜色
@include set-color-mix-level($type, $i, 'light', $color-white);
}
}
// 第 1 轮:$type = primary
@include set-color-mix-level(primary, 1, 'light', $color-white); // 生成 primary-light-1
@include set-color-mix-level(primary, 2, 'light', $color-white); // 生成 primary-light-2
@include set-color-mix-level(primary, 3, 'light', $color-white); // 生成 primary-light-3
// ... 继续到 9
// 第 2 轮:$type = success
@include set-color-mix-level(success, 1, 'light', $color-white); // 生成 success-light-1
@include set-color-mix-level(success, 2, 'light', $color-white); // 生成 success-light-2
// ... 继续到 9
// 依此类推,直到遍历完所有 6 种类型
三、生成 CSS 变量(全局)
位置:packages/theme-chalk/src/var.scss
// join var name
// joinVarName(('button', 'text-color')) => '--el-button-text-color'
// 将$list遍历中间用-拼接每一个$item
@function joinVarName($list) {
$name: '--' + config.$namespace;
@each $item in $list {
@if $item != '' {
$name: $name + '-' + $item;
}
}
@return $name;
}
===============================================================
// set css var value, because we need translate value to string
// for example:
// @include set-css-var-value(('color', 'primary'), red);
// --el-color-primary: red;
// 返回 变量名:颜色值 的形式
@mixin set-css-var-value($name, $value) {
#{joinVarName($name)}: #{$value};
}
================================================================
@mixin set-css-color-type($colors, $type) {
// 生成基础颜色变量
@include set-css-var-value(('color', $type), map.get($colors, $type, 'base'));
// 结果:--el-color-primary: #409eff;
// 生成浅色变量(3, 5, 7, 8, 9)
@each $i in (3, 5, 7, 8, 9) {
@include set-css-var-value(
('color', $type, 'light', $i),
map.get($colors, $type, 'light-#{$i}')
);
}
// 结果:
// --el-color-primary-light-3: #79bbff;
// --el-color-primary-light-5: #a0cfff;
// --el-color-primary-light-7: #c6e2ff;
// --el-color-primary-light-8: #d9ecff;
// --el-color-primary-light-9: #ecf5ff;
// 生成深色变量
@include set-css-var-value(
('color', $type, 'dark-2'),
map.get($colors, $type, 'dark-2')
);
// 结果:--el-color-primary-dark-2: ...;
}
=================================================================
:root {
color-scheme: light;
// --el-color-#{$type}
// --el-color-#{$type}-light-{$i}
@each $type in (primary, success, warning, danger, error, info) {
@include set-css-color-type($colors, $type);
}
生成的css变量,以primary为例子
:root {
--el-color-primary: #409eff;
--el-color-primary-light-3: #79bbff;
--el-color-primary-light-5: #a0cfff;
--el-color-primary-light-7: #c6e2ff;
--el-color-primary-light-8: #d9ecff;
--el-color-primary-light-9: #ecf5ff;
--el-color-primary-dark-2: #337ecc;
}
四、组件级使用——以el-button为例子
位置:packages/theme-chalk/src/button.scss
// generate css var from existing css var
// for example:
// @include css-var-from-global(('button', 'text-color'), ('color', $type))
// --el-button-text-color: var(--el-color-#{$type});
@mixin css-var-from-global($var, $gVar) {
$varName: joinVarName($var);
$gVarName: joinVarName($gVar);
#{$varName}: var(#{$gVarName});
}
==========================================================
$button-color-types 是一个 SCSS 变量(map),定义在 button-variant mixin 内部。它是一个嵌套的 map,用于定义按钮在不同状态下的颜色映射关系。
$button-color-types: (
'': (
'text-color': (
'color',
'white',
),
'bg-color': (
'color',
$type,
),
'border-color': (
'color',
$type,
),
'outline-color': (
'color',
$type,
'light-5',
),
'active-color': (
'color',
$type,
'dark-2',
),
),
'hover': (
'text-color': (
'color',
'white',
),
'link-text-color': (
'color',
$type,
'light-5',
),
'bg-color': (
'color',
$type,
'light-3',
),
'border-color': (
'color',
$type,
'light-3',
),
),
'active': (
'bg-color': (
'color',
$type,
'dark-2',
),
'border-color': (
'color',
$type,
'dark-2',
),
),
'disabled': (
'text-color': (
'color',
'white',
),
'bg-color': (
'color',
$type,
'light-5',
),
'border-color': (
'color',
$type,
'light-5',
),
),
);
// 结构层次
$button-color-types (第一层:状态)
├─ '' (默认状态)
│ ├─ 'text-color' → ('color', 'white')
│ ├─ 'bg-color' → ('color', $type)
│ ├─ 'border-color' → ('color', $type)
│ ├─ 'outline-color' → ('color', $type, 'light-5')
│ └─ 'active-color' → ('color', $type, 'dark-2')
│
├─ 'hover' (悬停状态)
│ ├─ 'text-color' → ('color', 'white')
│ ├─ 'link-text-color' → ('color', $type, 'light-5')
│ ├─ 'bg-color' → ('color', $type, 'light-3')
│ └─ 'border-color' → ('color', $type, 'light-3')
│
├─ 'active' (激活状态)
│ ├─ 'bg-color' → ('color', $type, 'dark-2')
│ └─ 'border-color' → ('color', $type, 'dark-2')
│
└─ 'disabled' (禁用状态)
├─ 'text-color' → ('color', 'white')
├─ 'bg-color' → ('color', $type, 'light-5')
└─ 'border-color' → ('color', $type, 'light-5')
================================================================
@each $type, $typeMap in $button-color-types {
// 内层循环,遍历第二层(属性):text-color, bg-color, border-color, outline-color, active-color
// $typeColor:属性名,例如:'text-color'
// $list:属性值,例如:('color', 'white')
@each $typeColor, $list in $typeMap {
// 调用 css-var-from-global 生成 CSS 变量
// 例如:@include css-var-from-global(('button', 'hover', 'text-color'), ('color', 'white'));
@include css-var-from-global(('button', $type, $typeColor), $list);
}
}
// 当 $type = 'primary' 时,会生成:
/* 默认状态 */
--el-button-text-color: var(--el-color-white);
--el-button-bg-color: var(--el-color-primary);
--el-button-border-color: var(--el-color-primary);
--el-button-outline-color: var(--el-color-primary-light-5);
--el-button-active-color: var(--el-color-primary-dark-2);
/* 悬停状态 */
--el-button-hover-text-color: var(--el-color-white);
--el-button-hover-link-text-color: var(--el-color-primary-light-5);
--el-button-hover-bg-color: var(--el-color-primary-light-3);
--el-button-hover-border-color: var(--el-color-primary-light-3);
/* 激活状态 */
--el-button-active-bg-color: var(--el-color-primary-dark-2);
--el-button-active-border-color: var(--el-color-primary-dark-2);
/* 禁用状态 */
--el-button-disabled-text-color: var(--el-color-white);
--el-button-disabled-bg-color: var(--el-color-primary-light-5);
--el-button-disabled-border-color: var(--el-color-primary-light-5);
=====================================================================
完整源码:
@mixin button-variant($type) {
$button-color-types: (
'': (
'text-color': (
'color',
'white',
),
'bg-color': (
'color',
$type,
),
'border-color': (
'color',
$type,
),
'outline-color': (
'color',
$type,
'light-5',
),
'active-color': (
'color',
$type,
'dark-2',
),
),
'hover': (
'text-color': (
'color',
'white',
),
'link-text-color': (
'color',
$type,
'light-5',
),
'bg-color': (
'color',
$type,
'light-3',
),
'border-color': (
'color',
$type,
'light-3',
),
),
'active': (
'bg-color': (
'color',
$type,
'dark-2',
),
'border-color': (
'color',
$type,
'dark-2',
),
),
'disabled': (
'text-color': (
'color',
'white',
),
'bg-color': (
'color',
$type,
'light-5',
),
'border-color': (
'color',
$type,
'light-5',
),
),
);
// 外层循环,遍历第一层(状态):'', 'hover', 'active', 'disabled'
// $type状态名例如'hover';
// $typeMap:状态对应的属性值,例如:('text-color': (
// 'color',
// 'white',
// ),)
@each $type, $typeMap in $button-color-types {
// 内层循环,遍历第二层(属性):text-color, bg-color, border-color, outline-color, active-color
// $typeColor:属性名,例如:'text-color'
// $list:属性值,例如:('color', 'white')
@each $typeColor, $list in $typeMap {
// 调用 css-var-from-global 生成 CSS 变量
// 例如:@include css-var-from-global(('button', 'hover', 'text-color'), ('color', 'white'));
@include css-var-from-global(('button', $type, $typeColor), $list);
}
}
&.is-plain,
&.is-text,
&.is-link {
@include button-plain($type);
}
}
五、应用样式(最终渲染)
位置:packages/theme-chalk/src/button.scss
@include b(button) {
display: inline-flex;
justify-content: center;
align-items: center;
line-height: 1;
// min-height will expand when in flex
height: map.get($input-height, 'default');
white-space: nowrap;
cursor: pointer;
color: getCssVar('button', 'text-color');
text-align: center;
box-sizing: border-box;
outline: none;
transition: 0.1s;
font-weight: getCssVar('button', 'font-weight');
user-select: none;
vertical-align: middle;
-webkit-appearance: none;
background-color: getCssVar('button', 'bg-color');
border: getCssVar('border');
border-color: getCssVar('button', 'border-color');
&:hover {
color: getCssVar('button', 'hover', 'text-color');
border-color: getCssVar('button', 'hover', 'border-color');
background-color: getCssVar('button', 'hover', 'bg-color');
outline: none;
}
// 这些生成el-button的基础变量
======================================================
@each $type in (primary, success, warning, danger, info) {
@include m($type) {
@include button-variant($type);
}
}
// $type = 'primary' 时,最终编译后的 CSS(.el-button--primary):
.el-button--primary {
/* 基础样式 */
display: inline-flex;
justify-content: center;
align-items: center;
/* 颜色(使用 CSS 变量) */
color: var(--el-button-text-color);
/* ↓ 展开为 */
color: var(--el-color-white); /* #ffffff */
background-color: var(--el-button-bg-color);
/* ↓ 展开为 */
background-color: var(--el-color-primary); /* #409eff */
border-color: var(--el-button-border-color);
/* ↓ 展开为 */
border-color: var(--el-color-primary); /* #409eff */
}
.el-button--primary:hover {
background-color: var(--el-button-hover-bg-color);
/* ↓ 展开为 */
background-color: var(--el-color-primary-light-3); /* #79bbff */
border-color: var(--el-button-hover-border-color);
/* ↓ 展开为 */
border-color: var(--el-color-primary-light-3); /* #79bbff */
}
.el-button--primary:active {
background-color: var(--el-button-active-bg-color);
/* ↓ 展开为 */
background-color: var(--el-color-primary-dark-2); /* #337ecc */
border-color: var(--el-button-active-border-color);
/* ↓ 展开为 */
border-color: var(--el-color-primary-dark-2); /* #337ecc */
}
六、完整流程图
┌─────────────────────────────────────────────────────────┐
│ 阶段 1: 基础颜色定义 │
│ var.scss: primary.base = #409eff │
└─────────────────┬───────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 阶段 2: 自动生成颜色变体 │
│ set-color-mix-level() │
│ - primary.light-3 = #79bbff (30% 白色混合) │
│ - primary.dark-2 = #337ecc (20% 黑色混合) │
└─────────────────┬───────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 阶段 3: 生成全局 CSS 变量 │
│ var.scss: :root { │
│ --el-color-primary: #409eff │
│ --el-color-primary-light-3: #79bbff │
│ --el-color-primary-dark-2: #337ecc │
│ } │
└─────────────────┬───────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 阶段 4: 按钮组件使用颜色 │
│ button-variant('primary') │
│ 生成组件级 CSS 变量: │
│ --el-button-bg-color: var(--el-color-primary) │
│ --el-button-hover-bg-color: var(--el-color-primary-light-3)│
│ --el-button-active-bg-color: var(--el-color-primary-dark-2)│
└─────────────────┬───────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 阶段 5: 最终渲染 │
│ .el-button--primary { │
│ background-color: var(--el-button-bg-color); │
│ /* 浏览器解析为 #409eff */ │
│ } │
└─────────────────────────────────────────────────────────┘
七、优势
- 统一管理:所有颜色在一个地方定义。
- 自动计算:light/dark 变体自动生成。
- 易于定制:修改基础色即可影响所有变体和组件。
- 性能:使用 CSS 变量,支持运行时动态切换主题。
赣锋锂业:赣锋国际拟向中非基金发行1亿美元可交换票据
CSS 视口单位进化论:从 100vh 的「骗局」到 dvh 的救赎
关键词:CSS / 视口单位 / 移动端适配 / 响应式设计 / dvh / svh / lvh / 100vh bug / 前端工程化 / 用户体验
引言:一个持续了十年的移动端「谎言」
如果你做过移动端 H5 开发,或者写过全屏落地的营销页,你一定遇到过那个著名的「100vh 骗局」。
设计师给了你一张完美的设计稿:首屏全屏,底部按钮固定,不允许有滚动条。
你自信满满地写下:
.hero-section {
height: 100vh;
}
然后你在 iPhone 的 Safari 里打开,灾难发生了:底部的按钮被浏览器底部的工具栏遮住了一半,或者页面莫名其妙出现了一截滚动条。
![]()
(图解:左侧是理想设计,按钮在最底部;右侧是 Safari 现状,浏览器工具栏无情地盖住了你的按钮)
为了解决这个问题,前端社区发明了无数「歪门邪道」:
- 用
window.innerHeight动态计算高度赋给 CSS 变量--vh。 - 监听
resize事件疯狂重绘。 - 使用
position: fixed; top: 0; bottom: 0;这种古老的 hack。
为什么一个简单的「全屏」需求,在移动端会如此艰难?
这背后其实是 Web 标准与移动端原生 UI(动态地址栏、底部工具栏)长达十年的博弈。
好消息是,CSS 终于迎来了真正解决问题的新一代视口单位:svh、lvh、dvh 以及它们在宽度和逻辑方向上的兄弟们。
这不是简单的语法升级,这是 Web 终于承认:移动端的屏幕,从来就不是一个静态的矩形。
一、旧时代的妥协:为什么 100vh 不是真的 100% 高度?
1.1 完美的初衷与残酷的现实
在 PC 时代,vh (Viewport Height) 的定义非常完美:视口高度的 1%。浏览器窗口多高,100vh 就多高。
但在移动端,情况变了。iOS Safari 和 Chrome Android 为了给用户更多阅读空间,设计了动态工具栏:
- 初始状态:地址栏、底部工具栏展开,可视区域变小。
- 上滑状态:地址栏缩小/隐藏,工具栏消失,可视区域变大。
这时候,100vh 该等于「展开时的高度」还是「隐藏时的高度」?
1.2 浏览器的「摆烂」选择
早期的 Safari 做了一个影响深远的决定:为了避免滚动时页面元素跳动(Reflow),100vh 永远等于「地址栏隐藏时的最大高度」。
![]()
(图解:无论你的工具栏是否展开,Safari 始终认为 100vh 是那个「最大值」。这就导致当工具栏展开(实际只有 80% 高度)时,你的 100vh 内容溢出了 20%。)
这就解释了为什么你的按钮被遮住了:
- 浏览器告诉你 100vh 是 800px(假设全屏高度)。
- 但实际上底部的工具栏占了 80px,可视区域只有 720px。
- 你的按钮画在了第 750px 的位置,正好被工具栏盖得严严实实。
这个「特性」被无数开发者吐槽为 Bug,但浏览器厂商坚持这是「Feature」。
「他们为了滚动的丝滑,牺牲了布局的精确。
在很长一段时间里,我们只能用 JS 来修补 CSS 的谎言。」
二、新秩序的建立:svh、lvh、dvh 的三态哲学
W3C 终于意识到,移动端的视口不是一个固定的值,而是一个在「大」与「小」之间薛定谔的波动状态。
于是,CSS Viewport Units Level 3 引入了一套全新的单位体系,把视口高度拆分为三种状态。
![]()
2.1 svh (Small Viewport Height):永远不再被遮挡
定义:视口高度的最小值(即地址栏、工具栏全部展开时的可视高度)。
- 应用场景:首屏全屏页、底部固定按钮、Modal 弹窗。
-
价值:如果你写
height: 100svh,你的页面底部绝对不会被工具栏遮挡。它是最安全的「一屏」。
/* 以前的 JS Hack */
.modal {
height: calc(var(--vh, 1vh) * 100);
}
/* 现在的原生写法 */
.modal {
height: 100svh;
}
2.2 lvh (Large Viewport Height):沉浸式的极致
定义:视口高度的最大值(即地址栏、工具栏全部收起时的可视高度)。
- 应用场景:背景图、视差滚动容器。
-
价值:当你希望背景铺满整个「潜在」屏幕,不在乎一部分被遮挡时,用
lvh。它等同于旧时代的100vh。
2.3 dvh (Dynamic Viewport Height):动态的完美与代价
定义:动态视口高度。它会随着地址栏的伸缩,实时在 svh 和 lvh 之间变化。
- 应用场景:追求极致体验的流式布局。
-
代价:当用户滚动页面时,
100dvh的值会不断变化。这意味着浏览器可能需要不断重绘(Repaint)甚至重排(Reflow)。 -
现状:现代浏览器对
dvh做了很多优化,性能损耗在大多数场景下已可忽略不计。
三态哲学总结:
- svh 是保底的安全(Safe)。
- lvh 是理想的宏大(Large)。
- dvh 是真实的动态(Dynamic)。
三、维度的扩张:宽度、逻辑方向与极值
这套逻辑不仅修复了高度问题,还顺带重构了整个视口单位体系。
3.1 宽度的进化:svw / lvw / dvw
虽然移动端宽度的变化(主要是滚动条出现/消失)不如高度那么剧烈,但逻辑是一致的。
- vw:依然是默认视口宽度(通常包含滚动条)。
- svw / lvw / dvw:处理滚动条存在与否时的细微差异(在桌面端更明显)。
3.2 逻辑方向:vi / vb —— 国际化的必修课
随着 CSS 逻辑属性(Logical Properties)的普及,我们不再总是谈论 width 和 height,而是谈论 inline(文本流方向)和 block(块堆叠方向)。
-
vi (Viewport Inline):视口在行内方向的大小。
- 横屏/竖屏书写模式下:等于
vw。 - 竖排书写模式(如古诗词页面):等于
vh。 - svi / lvi / dvi:对应的三态变体。
- 横屏/竖屏书写模式下:等于
-
vb (Viewport Block):视口在块级方向的大小。
- 横屏/竖屏书写模式下:等于
vh。 - svb / lvb / dvb:对应的三态变体。
- 横屏/竖屏书写模式下:等于
实战案例:一个适配横竖屏书写的古诗卡片
.poem-card {
/* 无论横排还是竖排,永远占满视口在「块」方向上的 80% */
block-size: 80vb;
/* 无论横排还是竖排,永远占满视口在「行」方向上的 90% */
inline-size: 90vi;
/* 自动适配书写模式 */
writing-mode: vertical-rl; /* 切换这个属性,布局依然完美 */
}
3.3 极值单位:vmin / vmax 的三态
老朋友 vmin(vw/vh 中较小者)和 vmax(较大者)也全员升级:
- svmin / svmax
- lvmin / lvmax
- dvmin / dvmax
场景:做横竖屏适配的 H5 游戏或画报时,svmin 是保证内容在任何旋转状态下都完整可见的神器。
四、工程实战:如何在 2025 年写好一个全屏页面?
4.1 放弃 100vh,拥抱 dvh(但要做降级)
在 2025 年,如果你的目标浏览器支持率允许(iOS 15.4+, Chrome 108+),dvh 是全屏容器的最佳选择。
.full-screen-hero {
/* 降级方案:给旧浏览器一个固定的值 */
height: 100vh;
/* 现代方案:使用动态高度,完美贴合 */
height: 100dvh;
}
4.2 关键交互区域用 svh
对于底部的操作栏(Action Bar),不要用 dvh,因为你不想让按钮在用户手指滑动时「跳来跳去」。用 svh 锁定位置。
.bottom-bar {
position: fixed;
bottom: 0;
width: 100%;
/* 确保它永远在可视区底部,哪怕地址栏展开 */
bottom: calc(100vh - 100svh); /* 高级技巧:计算工具栏高度偏移 */
}
/* 或者更简单的布局思维: */
.app-container {
min-height: 100svh;
display: grid;
grid-template-rows: 1fr auto;
}
4.3 慎用 lvh,除非你在做特效
lvh 在实际 UI 布局中用得很少。它更多用于视觉背景,或者那种「滑一下就全屏」的沉浸式阅读体验。如果你用 lvh 做布局容器,用户大概率会因为点不到底部的按钮而骂娘。
「成熟的工程师懂得:
UI 的稳定性(svh)优先于 UI 的充满感(lvh),
而 dvh 是两者之间的优雅平衡。」
五、总结:从「对抗」到「接纳」
回顾 CSS 视口单位的进化史,其实是一部 Web 与移动端原生特性的磨合史。
- vh 时代:Web 试图假装自己还在 PC 上,无视了移动端复杂的 UI 变化,结果撞得头破血流(100vh bug)。
- JS Hack 时代:开发者用脚本强行修正高度,对抗浏览器的默认行为,性能差且代码丑陋。
- svh/dvh 时代:标准终于接纳了移动端的复杂性。我们不再强求一个「唯一的 100%」,而是承认「屏幕是会变的」。
这给了我们两个重要的启示:
- 不存在完美的静态适配。移动端设备极其碎片化,折叠屏、刘海屏、动态栏……拥抱动态(Dynamic)才是终极解法。
-
工具的粒度决定了体验的细腻度。从粗糙的
vh到精细的svh/lvh/dvh,前端工程化的本质,就是不断提升对像素级体验的掌控力。
下一次,当设计师问你「能不能把这个页面铺满全屏,不要滚动条,也不要被遮挡」时,
你可以自信地合上电脑(或者打开 VS Code),告诉他:
「当然可以,因为现在的 CSS,终于懂手机了。」
三百多万人围观的 AI 油画视频,是技术的神作,还是没有灵魂的电子垃圾
「比蒙娜丽莎更美的,就是正在燃烧的蒙娜丽莎」,这是多年前一档辩论节目里,大家对于艺术价值的不同理解方式,那时听到可能觉得挺激进,笑一笑便过了。
最近一段把几幅经典油画「复活」的 AI 视频,在 X 上引起了巨大的争议,视频刷到 300 多万播放,被不少人称之为栩栩如生的艺术;比经典油画作品更好看的,是会流动的油画?AI 做的东西是不是没有任何艺术价值?
网友分享的油画视频里,经典的油画元素不再静止,颜料开始流动,天空的云朵、火山的喷发、还有海浪的汹涌都变得生动自然,仿佛那些存在几百年前的画布,突然拥有了生命一样。
乍看之下,这就是一场视觉盛宴的享受;如果不是其中几个视频,忘了去掉右下角 Google Veo 视频生成的水印,甚至会觉得完全是用 CG 特效制作实现,毕竟对油画风格来说,没有很明显的「AI 味」。
但是点开评论区之后,发现网友们撕成了两派,有人说,这就是新时代的艺术,是全新的审美体验;有人就不买单,用 AI 时代最刻薄的词汇——Slop(垃圾/泔水)一言蔽之,说等到 AI 有意识了,再来谈配不配成为艺术。
同样的一条 AI 视频,让人看到了艺术、技术、恐惧、愤怒、敬畏、厌烦,还有时代变化。
如果不说这是 AI,你的第一反应是
X 网友发布的这则视频,其实并不是他本人的原创,在评论区有人指出来,说他没有标注视频来源,也没说明使用了 AI,只是单纯地为了赚取流量。现在这波流量,也确实是被他赚到了。
![]()
视频最早是出现 YouTube 上,一位有着 2000 多粉丝的博主@bandyquantguy,他是宾夕法尼亚州立大学艺术与建筑学院的一名助理教学教授。频道内发布的内容,基本上都是不同油画的动态视频作品,长度在一分半到 3 分钟不等。
而那条被转发到 X 上的视频,并收获了三百多万的观看,是他将近一个月之前的作品。
当我看着满屏的动态油画时,说实话,根本没想到这是否通过 AI 生成。一方面是对油画艺术的不了解,是否有相关的技术,或者爱好者在专门做类似的工作。另一方面,大多数时候,我们所说的 AI 味,主要是在制作一些写实的画面,像现实世界、真人版等。而这种风格化本身就足够突出的内容,AI 的处理反而不会太突兀。
有网友评价,这是他见过最原汁原味的 AI 艺术作品之一,因为视频没有生硬地模仿现实,而是创造了一种介于梦境,与现实之间的流体美感。
第一眼都是觉得「震撼」,而这种迎面而来的视觉冲击,在知道它是 AI 生成的之后,也变成了争议的起点。大多数的人会觉得,这样的作品很棒;但对剩下一部分人来说,如果光靠 AI 就能得到原本属于「艺术」的内容,那该有多可怕。
Slop,AI 是原罪
所以,打压和看衰,成了评论区的另一种态度,Slop 就是代表性的关键词。
在 AI 语境下,Slop 指的是通过 AI 大量生成的、看似有内容实则空洞的劣质品。像是之前奥特曼推出 Sora,就有人犀利的丢下断言,Sora 生成的视频,全部都是 AI Slop。
这次,也有网友说,这样的油画视频,不应该放在社交媒体上,Sora 才是他最好的归宿,那里都是同样的 AI 垃圾。为什么画面如此精美的视频,会被称为 Slop?反而一些 AI 味明显的视频,激不起大家这么热烈的反馈。
因为它是机器盲目的困境。
反对者认为,AI 生成的内容,是缺乏意义,就像是一台机器盲目的梦境,它甚至不知道自己正在做梦。
他们的观点是,艺术不仅仅是停留在好看,那只是媚俗。艺术需要人类的意图、深度和复杂性。在这个视频里,原本油画的笔触,变成了毫无逻辑的像素流动,就像是单纯地为了展示「我能动」而动,没有任何节奏、理由或概念支撑这个视频的内容。
对他们来说,AI 最大的问题从来不是不够好看,而是「不够人」。
AI 的每一次选择,只不过是概率。而人类创作一个作品,画一幅油画,背后包含的是对人生、对世界的思考和回应;有情感、时间、技巧、犹豫和失败等经历。
支持者觉得,现在的 AI,就像相机刚被发明时一样,不是在替代传统,而是在扩张想象力。甚至有网友说,「我想艺术家们在作画前,脑海中可能就有这样的画面,现在我们也能走进他们的灵感了。」
是结果,还是过程重要
如果这个视频是一个人类艺术家,花费 1000 个小时,一帧一帧手绘出来的,评论区又会发生什么。除了震撼,大概还是一样,会有人说,这视频顶多用来作为我的手机屏保,除了好看也就仅此而已了。
艺术是主观觉得还是客观认定呢,其实都没有明确的界定。网友的期待,大概是希望,艺术应该是需要「努力」才能抵达的地方,而 AI 正在稀释「努力」在艺术中的价值。
前段时间,一幅名为《太空歌剧院》的画作,拿到了艺术比赛的头奖,还有 AI 画作甚至在拍卖市场,以十万、百万的价格被拍走。
我们在一个输入提示词就能生成图像的世界里,任何一个人不需要复杂的技巧,也不需要付出多少汗水,都有机会创作自己的作品,作品的意义也不再靠时间来定义。
![]()
▲提示词:将油画纹理动画化为粘稠流体模拟,厚重的颜料笔触融化并流动。旋转的天空、粘稠的黄色光芒、翻腾的蓝色云朵。
当 AI 把时间成本压缩到几秒钟,这种神圣感瞬间崩塌,剩下的就只有廉价。「这不难做吧」、「我用 Veo 3 也能生成」,这样的东西,自然就成不了艺术了。
更有趣的是,有网友提到,这是一种很明显的社会心理学现象,巴浦洛夫的狗。现在我们只要看到 AI 的标签,就有了条件反射,瞬间进入狂怒模式,无论作品本身好坏,一律打成 Slop。
![]()
具体来说,就是眼睛告诉我,这个视频还不错,但大脑告诉我,这是 AI,AI 做的都是不好的,为了调和这种矛盾,我就必须强行说服自己,它看起来很糟糕。
心理效应是存在,更多的我想其实还是,AI 内容的泛滥,正在把我们的审美阈值无限拔高。
没有 AI 的时候,让一幅油画像这样动起来,可以说是「魔法」一样的存在。现在如果这些作品没有极强的故事性,或情感内核,仅仅是视觉特效,已经很难打动被各种 AI 效果喂饱的我们。
▲ YouTube 上该博主的其他油画视频
无论评论区吵得多么不可开交,一个事实是无法改变:AI 不会消失,艺术也不会因为 AI 的出现而消失。
就像一些网友说的,「电力曾让蜡烛工厂破产,但人类具有适应性」。现在的混乱,也许只是新旧审美体系,交替时发生的阵痛。
如果在 100 年前我们按一下播放键,就能听到录好的歌,大概也会有人觉得,只有黑胶唱片出来的声音才是真音乐;现在我们只是习惯了,现场、黑胶、手机、音响都有好音乐。
艺术,从来看的是最终的愿景,是我想让你看到什么,而不是用了什么工具,我花了多久才做出来。一个活过来的 AI 艺术,就算是简单的几行提示词,一样倾注了真正属于创作者的叙事、情感与意图。
#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。
看完《疯狂动物城2》这样发合照火爆朋友圈,Nano Banana Pro 新玩法来了 | 附教程
![]()
疯狂动物城2 前几天刚正式上映,超过哪吒,成为中国影史动画电影预售票房冠军,双休日买好票准备去看了吗。
社交媒体上,早在预售的时候,就已经都是警官兔朱迪与狐尼克,出现在不同网友的电影院合影里。
![]()
照片里的网友,左拥右抱着兔朱迪和狐尼克,作出自拍的姿势,而背后的影院屏幕上还显示着疯狂动物城2 的海报。同时人脸的一致性保持,让人觉得这就是电影放映后的粉丝福利,动画片主角跑出来免费合影。
评论区都在求攻略,有网友贴心地给出了制作指南。输入下面的提示词,然后图 1 上传自己的照片,图 2 和 图 3 分别上传朱迪和尼克的照片,图 4 是一张电影海报,通过豆包或者剪映 App 的生图功能,就能得到一张同款合照。
不要换脸,人物脸部不能有任何改变!将图1和图2 和图 3 的三人融合成一张三人俯拍自拍照,画面构图紧凑,三位主体靠得很近,图 1 人物居中,头部略微上仰,眼神直视镜头,营造出强烈的视觉冲击力。左侧和右侧人物站得略靠后,整体都需要保持人脸相似度,略微内扣身体,拍摄角度为高角度俯拍,使头部比例被夸张放大,典型的日韩视觉自拍风格。简洁干净,进一步凸显人物主体。整体画面清晰度高,用iphone前置自拍,最终呈现出精致、时尚、略带的合影效果。要求人物实现无缝融进画面,视觉过渡自然,整体画面光线明亮且均匀,背景为超大电影院坐满了观众,这三人背对银幕,银幕显示参考图 4《疯狂动物城2》电影精美海报填满屏幕。
![]()
▲图片和提示词来源:小红书@424180556
我们也使用 Nano Banana Pro 生成了同样的合照,不过得益于它的强大,我们完全不需要输入太多的提示词,只是上传四张图片,然后简单的告诉它要做什么就可以。
![]()
▲提示词:把这个男生和另外两个疯狂动物城角色放到一张合照里,他们就像是拿着手机在自拍,背景是电影院,电影院的屏幕上显示着我发给你疯狂动物城海报;16:9 的大小,4K 画质。
甚至还可以脑洞大开,直接丢给它这四张图片,然后输入提示词「合照」,完全不用担心人脸会改变,我们得到了这些 Nano Banana Pro 发挥自己的脑洞,生成的图片;横屏和竖屏的电影海报都有了。
![]()
![]()
▲提示词:合照
我们还可以试试最近玩 Nano Banana Pro 特别热门的提示词玩法,输入「xx,但不是xx」,上面在海报前拍照的那张图片,我们输入的提示词就是「合照,但不是合照」。
除了合照的玩法,配合新升级的 Nano Banana Pro,我们还有很多疯狂动物城2 联动玩法。
玩法一:全世界都是疯狂动物城
还记得 GPT-4o 刚出来那会儿,铺天盖地都是吉卜力风,现在疯狂动物城风也来了,而且还带来了更丰富的控制。
就像这张现实世界的街景图,只是把路上的行人,换成了可爱的动物角色,而马路和建筑还保留着原来的风格。
![]()
提示词:疯狂动物城风格的真实场景融合。迪士尼级别角色设计,富有表现力的形状,柔软毛发质感,流畅环境光照。保持原始照片结构,保留现实世界的光线、透视和材质。将拟人化元素自然融入环境中。高分辨率,电影级色彩调节。
拿着这套提示词,几乎可以把所有的照片,都转成这种现实+疯狂动物城的风格。还记得我们之前 AI DONE 活动预告里面,4 位 AI 大佬走在斑马线上吗,现在你还能不能,分辨出从左到右都是谁。
![]()
还有这张在网上疯狂传播的 AI 合照,把硅谷最有权有势的 CEO 都集结在一起。
![]()
▲ 由于提示词里面提到了「迪士尼」,所以这个狮市长看起来更像其他迪士尼系列电影的角色
除了保留一定的现实世界风格,把整个图片都换成疯狂动物城风,也是一样的好玩,Nano Banana Pro 表示,这都是小 case。
把开头那张只是转换了路上行人的照片,全部换成疯狂动物城的元素,色彩更加鲜艳,元素也更协调。
![]()
提示词:将这个现实世界的照片转换为《疯狂动物城》风格的动物城市,保留原始图片原始的全部元素和布局结构,但转换为风格化的拟人化基础设施,4K 画质。
同样的提示词,我们还把爱范儿在大兴安岭拍摄的样片,也进行一次风格转换。Nano Banana Pro 把大兴安岭的冷,描绘地更加极致,雪花飘在了衣服上。
![]()
还有随手拍摄的合照,也能一键换装,全员变身可爱的小动物。
![]()
▲ 原图来源:Unsplash@绵绵
如果希望留住能识别的面部特征,而不是直接把整个头部都换成动物头,实现起来也很简单,只用加一句提示词。
完全保留人物的人类面部特征和身份。
玩法二:合照,三个人可不够
开头只有朱迪和尼克的合影,只是疯狂动物城里面的两个主要角色,在电影里还有狸宝、宝伯特、马飞扬,闪电、夏奇羊、牛局长等数十个动物角色。
![]()
在 Nano Banana Pro 里,这些角色可以全部一次性发给它处理,所有角色都在舞台上。
![]()
提示词:图一是我,图2到图8,每张图片里面有4个疯狂动物城里面的角色,现在你需要把我和他们放在一张合照里面,就像是在线下的电影院,这些电影角色主创都在台上,我和他们一起合照,我在中间,拿着手机自拍,然后他们都在我旁边或者身后。
有了这么多的角色图片,我们还可以做一张疯狂动物城警局的照片墙,Judy、Nick、Bogo、Clawhauser 都在那块墙上,然后我们上传的照片也被贴到同一块墙上。
![]()
提示词:
创建一个16:9、4K分辨率的最终场景,设置在《疯狂动物城》警察局(Zootopia Police Department ,ZPD)内部。
一个真实感的警察局办公室走廊,主体背后是一个大型“ZPD员工照片墙”。场景前方:
用户(来自图像1)站在中心位置,举着手机自拍。
保留用户真实的人类面孔和身份信息。
不要将脸转换为动物,仅应用非常轻微的《疯狂动物城》风格灯光和色彩调节。背景:
用一整面墙填满用户身后的区域,并排列成网格状的拍立得风格员工ID照片。
使用图像2-8中的所有角色(每张包含四个角色),并将它们安排为单独的拍立得ID肖像展示在墙上。每个角色肖像应满足以下要求:
– 保持上传图片中原始角色身份不变。
– 遵循一致的《疯狂动物城》宣传艺术外观:柔和迪士尼灯光、干净背景、清晰人物轮廓线条。
– 匹配官方ZPD ID照片框架比例(头像或胸部以上)。同时,将用户自己的“ZPD风格ID照片”添加到墙上,放置于网格中央附近的位置,用与其他相同样式的拍立得边框呈现。
视觉规则:
– 保持真实办公环境中的墙壁材质、灯光效果的一致性。
– 所有ID照片必须对齐边框,统一色调,相同尺寸。
– 不要扭曲用户自拍姿势。
– 角色保持卡通化,但融入现实环境中。
– 可适度添加ZPD标志、警徽图标以及蓝色点缀灯光以增强真实性。最终目标:
结果应展现出仿佛该用户已加入了ZPD,在官方员工照片墙前,与28位《疯狂动物城》的角色及其个人ID肖像一起合影留念之效果;画面需高清晰度,4K质量,有电影感但具备现实主义氛围感。
![]()
玩法三:这是我的疯狂动物城 ID
和风格转换有点类似,但又不完全相同。我们可以让 AI 来猜测,上传的人像图片,会是疯狂动物城里面的谁。
![]()
提示词:根据你对这张人像图片的分析,将这幅肖像转化为迪士尼《疯狂动物城》风格,最适合这张肖像图片的拟人化动物角色。保留原始面部结构和性格特征。柔软的毛发质感、富有表现力的眼睛、流畅的迪士尼光影效果、友好的配色方案以及电影海报级别的质量。
虽然 Nano Banana 大多数时候都只记得朱迪,但还是会根据我们上传的图片,调节图片的色彩、眼神的信息,同时保持我们的服装和姿势不变。
不喜欢这种身份证,还可以生成疯狂动物城的同款角色海报。
![]()
把图一的角色替换到图二的角色海报里,成为新的官宣角色。
由于 Nano Banana Pro 现在的指令理解和遵循能力,都得到了提升,所以我们的提示词甚至不需要太复杂的描述,一般口语化的表达,它也能听懂。
然后直接让他把狐尼克和兔朱迪换成马斯克和奥特曼。
![]()
▲提示词:把这个海报里面的两个角色替换成 Elon Musk 和 Sam Altman,然后名字也改成对应的马斯克和奥特曼
如果觉得这种 ID 照片还不够正式的话,直接让 Nano Banana Pro 生成一张带着边框,风格明显的证件照。
![]()
提示词:
将这张肖像转化为《疯狂动物城》风格的官方身份证照片,同时完全保留人物的人类面部特征和身份。
保持人物原本的眼睛、鼻子、嘴巴以及整体脸型,不进行动物替换。
仅应用迪士尼《疯狂动物城》的视觉风格:柔和边缘光照效果、平滑阴影处理、温和色彩搭配、灯光边缘处微妙的毛发质感、人类眼睛稍微放大但仍然保留人类特点,以及电影级卡通写实效果。
使用干净的工作室风格ZPD身份证背景(蓝色渐变或中性背景)。
保持人物可识别性及自然肤色,仅添加与《疯狂动物城》宣传画一致的轻度艺术化处理。
不改变面部结构;增强表情清晰度,并在眼睛上增加些许迪士尼式光泽效果。
高质量,符合官方文件要求的框架设计。
还可以在提示词里面指定好名字、身份编号、工作部门等信息;Nano Banana Pro 也完全能准确渲染所有的文字。
尽管 Nano Banana Pro 比起之前的版本更能理解我们的意图,但有时候多尝试几次,还是会给我们一些额外的惊喜。
最近社交媒体上很多人在尝试,用尽可能少的提示词,看 Nano Banana Pro 会生成什么。还有一种更高级的玩法是,Gemini 3.0 Pro 多模态推理和理解能力的提升,用来做逆向工程表现也很不错。
例如,我们拿疯狂动物城这张海报来测试,在 Gemini 3.0 Pro 模型里面,输入提示词「提取这张图片的提示词,越详细越好。」然后再把提示词给 Nano Banana Pro,得到的 AI 图片,还原度其实算非常高。
![]()
▲ 提取到的提示词:
A high-quality 3D animated movie poster for Disney’s “Zootopia 2,” characterized by incredible detail, highly textured fur, vibrant saturated colors, and bright daylight.
Composition & Angle: A dynamic, chaotic street scene captured with a low-angle shot looking upwards through a wide-angle fisheye lens, creating strong perspective distortion and a sense of overwhelming crowds.
Central Characters: In the center, Nick Wilde (fox) wearing a pink floral Hawaiian shirt, grey pants, and a tie, is running forward with a panicked, wide-mouthed expression. Next to him, Judy Hopps (rabbit) in her blue police uniform is running with a shocked but determined look. Behind them, a massive blue python with yellow underbelly scales (new character Gary) is hilariously coiled through the crowd with a surprised expression and tongue sticking out.
Environment & Key Props: A large grey metal street light pole cuts diagonally from the top left to bottom right. Mounted on it is a green street sign that clearly reads “Disney” logo above “疯狂动物城” with a large background number “2”. Below the sign, a yellow traffic light housing displays a glowing cyan LED number “2” instead of standard lights.
Background & Crowd: To the left, a pink double-decker bus filled with slow-moving sloths hanging out of windows (including Flash). A dense, diverse crowd fills the street, including a giraffe stretching its neck, an elephant, a lynx holding up a smartphone taking a photo, a beaver, and a rhino in casual clothes. Overhead, a horse is swinging on a rope across the street. In the foreground, a green lizard is tripped up, and a seal wears headphones.
Setting: In the far distance under a bright blue sky with clouds, the towering, futuristic skyscrapers of the Zootopia skyline are visible. The overall atmosphere is energetic, humorous, and crowded.
Text Elements (Visual Placement): Floating 3D text in the middle left reads “老搭档 新案件” and middle right reads “嗨翻全城”. At the very bottom center, text reads “11月26日 全国影院献映”.
Technical Specs: 8k resolution, cinematic lighting, highly detailed render. –ar 2:3
在我们的测试中发现 AI Studio 生成的图片,有时候并不是按照指令的 4K 要求给出原图,大小一般在 1MB 以下,而在 Gemini App 或者网页里,生成的图片则是完整的 4K 分辨率,图片大小一般在 7MB 左右。
Gemini 使用 Nano Banana Pro 的次数是有限制,在部分时候会像 ChatGPT 一样,弹出使用旧模型的提示。
但大多数时候,每天的使用次数是足够普通用户的使用。
![]()
我们喜欢疯狂动物城,大概是因为它打造的乌托邦,是一个勇敢善良的美丽新世界。现在 AI 迷人的地方,正是让我们也有了进入这个新世界的能力,同样地体验和传播,这份纯真的美好。
原文链接:https://mp.weixin.qq.com/s/8EJNx4Qjww09htrEfenJDA
#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。