普通视图
白宫确认终止部分关税措施
马斯克:星舰每年将发射超1万颗卫星
国际金银大幅收涨 COMEX白银期货涨8.61%
美股三大指数集体收涨,谷歌涨超4%
特朗普宣布签署行政令 加征10%全球进口关税
《彻底解决CSS冲突!模块化CSS实战指南》
彻底解决CSS冲突!模块化CSS实战指南(Vue+React全覆盖)
作为前端开发者,你一定踩过「CSS冲突」的坑:多人协作时,自己写的样式被同事覆盖、组件复用后样式串扰、全局样式污染局部组件,排查起来费时费力,甚至越改越乱。
其实解决这个问题的核心,就是「CSS模块化」—— 让CSS样式和组件绑定,实现“样式私有化”,既不影响其他组件,也不被其他组件影响。
本文将拆解3种主流的模块化CSS实现方案(Vue scoped、React styled-components、React CSS Module),从原理、代码实战到适用场景,全程无废话,新手也能快速上手,彻底告别CSS冲突烦恼!
一、为什么需要模块化CSS?
在讲解具体方案前,我们先搞懂「为什么会出现CSS冲突」,以及「模块化CSS到底解决了什么问题」。
传统CSS是「全局作用域」,无论你把样式写在哪里,只要类名重复,就会出现样式覆盖——尤其是多人协作、组件复用的场景,比如:
- 你写了一个
.button样式,同事也写了一个.button,后加载的样式会覆盖先加载的; - 复用组件时,组件内部的样式不小心污染了父组件或其他兄弟组件;
- 项目后期维护时,不敢轻易修改CSS,生怕影响到其他未知的组件。
而模块化CSS的核心目标,就是「让样式只作用于当前组件」,实现:
- 样式私有化:组件内部样式不泄露、不污染全局;
- 避免冲突:不同组件可使用相同类名,互不影响;
- 便于维护:样式和组件绑定,修改组件时无需担心影响其他部分;
- 多人协作友好:各自开发组件,无需担心样式冲突。
下面我们结合具体实战代码,分别讲解Vue和React中最常用的3种模块化CSS方案,每一种都附完整代码解析,直接复制就能用。
二、Vue中模块化CSS:scoped样式(最简单直接)
如果你用Vue开发,最省心的模块化方案就是「scoped样式」—— 只需在style标签上添加 scoped 属性,Vue会自动为当前组件的样式添加唯一标识,实现样式私有化,无需额外配置,开箱即用。
1. 实战代码
<script setup>
// 引入子组件
import HelloWorld from './components/HelloWorld.vue'
</script>
<template>
<div>
<h1 class="txt">Hello world in App</h1>
<h2 class="txt2">一点点</h2>
<HelloWorld />
</div>
</template>
<style scoped>
// 加了scoped,这些样式只作用于当前App组件
.txt {
color: red;
}
.txt2 {
color: pink;
}
</style>
2. 核心原理(极简理解)
Vue会自动为加了 scoped 的样式做两件事:
- 给当前组件模板中的所有DOM元素,添加一个唯一的自定义属性(比如
data-v-xxxxxxx); - 给当前style中的所有样式选择器,自动添加这个自定义属性作为后缀(比如
.txt[data-v-xxxxxxx])。
这样一来,当前组件的样式就只会匹配带有该自定义属性的DOM,不会影响其他组件——哪怕子组件HelloWorld中也有 .txt 类名,也不会和App组件的 .txt 冲突。
3. 注意点(避坑重点)
- scoped样式只作用于当前组件的模板,不会影响子组件的模板(除非使用
::v-deep穿透); - 如果一个组件既有scoped样式,又有全局样式(不加scoped),全局样式会作用于整个项目;
- 适用场景:Vue项目通用,尤其是简单组件、中小型项目,无需额外配置,开箱即用。
三、React中模块化CSS:方案1 styled-components(CSS in JS)
React本身没有内置的模块化CSS方案,需要借助第三方库。其中「styled-components」是最流行的方案之一,核心思想是「CSS in JS」—— 用JS语法写CSS,将样式和组件完全绑定,实现模块化。
它的优势是:样式可以直接使用JS变量、props传参,实现动态样式,同时天然避免冲突,开发效率极高。
1. 实战代码
// 1. 安装依赖(先执行这一步)
// npm install styled-components
// 2. 引入并使用styled-components
import { useState } from 'react';
import styled from 'styled-components'; // 导入样式组件库
// 3. 定义样式组件:用styled.标签名`样式内容`的语法
const Button = styled.button`
background: ${props => props.primary ? 'blue' : 'white'}; // 接收props,动态切换背景色
color: ${props => props.primary ? 'white' : 'blue'}; // 动态切换文字色
border: 1px solid blue;
padding: 8px 16px;
border-radius: 4px;
`
console.log(Button); // 本质是一个React组件
function App() {
return (
<>
{/* 4. 使用样式组件,可传递props控制样式 */}
<Button>默认按钮</Button>
<Button primary>主要按钮</Button>
</>
)
}
export default App;
2. 核心原理
styled-components会将你写的CSS样式,动态生成一个唯一的类名(比如 sc-bdVaJa),并将这个类名绑定到对应的React组件上。
因为类名是自动生成的、全局唯一的,所以无论你在多少个组件中使用Button样式组件,都不会出现样式冲突。
同时,它支持通过props传递参数(比如上面的primary),实现动态样式——这是传统CSS很难做到的。
3. 优势与适用场景
优势:
- 样式与组件完全绑定,天然模块化,无冲突;
- 支持JS变量、props传参,轻松实现动态样式;
- 无需额外配置,写起来简洁高效。
适用场景:
React项目通用,尤其是需要大量动态样式、组件复用率高的场景(比如后台管理系统、UI组件库)。
四、React中模块化CSS:方案2 CSS Module(最贴近传统CSS)
如果你习惯写传统CSS,又想实现模块化,「CSS Module」会是最佳选择。它的核心思想是「将CSS文件编译成JS对象」,通过JS对象访问类名,实现样式私有化。
它的优势是:完全保留传统CSS写法,学习成本低,同时避免冲突,是React项目中最常用的模块化方案之一。
1. 实战代码
CSS Module的使用分为3步:创建CSS文件(后缀为.module.css)、导入CSS对象、使用对象中的类名,步骤清晰,上手简单。
第一步:创建Button.module.css(样式文件)
/* 注意:文件名必须是 组件名.module.css */
.button {
background-color: blue;
color: white;
padding: 10px 20px;
}
.txt {
color: red;
background-color: orange;
font-size: 30px;
}
第二步:创建Button组件(使用CSS Module)
// 1. 导入CSS Module文件,会被编译成JS对象(styles)
import styles from './Button.module.css'
console.log(styles); // 打印结果:{button: "Button_button__xxxx", txt: "Button_txt__xxxx"}
// 类名被编译成“文件名_类名__hash值”,全局唯一
export default function Button() {
return (<>
{/* 2. 通过styles对象访问类名,避免冲突 */}
<h1 className={styles.txt}>你好, 世界!!! </h1>
<button className={styles.button}>My Button</button>
</>)
}
第三步:多组件协作(验证无冲突)
再创建一个AnotherButton组件,使用相同的类名.button,验证模块化的冲突避免效果:
/* anotherButton.module.css */
.button {
background-color: red;
color: black;
padding: 10px 20px;
}
// AnotherButton.jsx
import styles from './anotherButton.module.css'
export default function AnotherButton() {
return <button className={styles.button}>My Another Button</button>
}
// App.jsx(引入两个组件)
import Button from './components/Button';
import AnotherButton from './components/AnotherButton';
export default function App() {
return (
<>
{/* 两个组件都有.button类名,但不会冲突 */}
<Button />
<AnotherButton />
</>
)
}
2. 核心原理
- React会将
.module.css后缀的文件,编译成一个JS对象(比如上面的styles); - CSS文件中的每个类名,都会被编译成「文件名_类名__hash值」的格式(比如
Button_button__xxxx),确保全局唯一; - 组件中通过
styles.类名的方式使用样式,本质是引用编译后的唯一类名,从而避免冲突。
3. 优势与适用场景
优势:
- 完全保留传统CSS写法,学习成本低,适合习惯写原生CSS的开发者;
- 类名自动哈希,彻底避免冲突,多人协作友好;
- 样式与组件分离,结构清晰,便于维护。
适用场景:
React项目通用,尤其是大型项目、多人协作项目,以及需要严格区分样式职责的场景。
五、3种模块化CSS方案对比(选型指南)
很多开发者会纠结“该选哪种方案”,这里整理了一张对比表,结合项目场景快速选型,避免踩坑:
| 方案 | 技术栈 | 核心特点 | 优势 | 适用场景 |
|---|---|---|---|---|
| Vue scoped | Vue | style标签加scoped,自动添加唯一标识 | 无需额外配置,开箱即用,简单高效 | Vue项目通用,中小型项目、简单组件 |
| styled-components | React | CSS in JS,样式与组件绑定,支持动态样式 | 动态样式方便,组件化程度高 | React项目,需要大量动态样式、UI组件库 |
| CSS Module | React | CSS文件编译成JS对象,类名哈希唯一 | 贴近传统CSS,学习成本低,多人协作友好 | React项目,大型项目、多人协作、样式与组件分离 |
六、常见问题与避坑指南
1. Vue scoped样式无法作用于子组件?
原因:scoped样式默认只作用于当前组件的模板,子组件的模板不会被添加自定义属性。
解决方案:使用::v-deep穿透scoped,比如:
<style scoped>
/* 穿透scoped,作用于子组件的.txt类名 */
::v-deep .txt {
color: green;
}
</style>
2. React CSS Module 类名不生效?
原因:文件名没有加.module.css后缀,或者导入方式错误。
解决方案:
- 确保文件名是「组件名.module.css」(比如Button.module.css);
- 导入时必须用
import styles from './xxx.module.css',不能省略module。
3. styled-components 样式不生效?
原因:没有安装依赖,或者语法错误(比如模板字符串写错)。
解决方案:
- 先执行
npm install styled-components安装依赖; - 确保样式定义用的是「模板字符串」(``),不是单引号或双引号。
七、总结
模块化CSS的核心,就是「解决样式冲突、实现样式私有化」,不同技术栈有不同的最优方案,但核心思路一致:
- Vue项目:优先用
scoped,简单高效,无需额外配置; - React项目:需要动态样式用
styled-components,习惯传统CSS用CSS Module。
无论选择哪种方案,都能彻底告别CSS冲突的烦恼,让组件开发更高效、维护更轻松。尤其是多人协作的项目,模块化CSS更是必备技能——学会它,能让你少踩80%的样式坑!
结合本文的代码示例,动手实操一遍,就能快速掌握模块化CSS的使用技巧。如果觉得本文对你有帮助,欢迎点赞、收藏、转发,也可以在评论区交流你的使用心得和踩坑经历~
深度解析vue的生命周期
概述
Vue 生命周期是 Vue 实例从创建到销毁的整个过程,包含多个关键阶段,每个阶段都有对应的生命周期钩子函数,允许我们在特定时机执行自定义逻辑。
每个 Vue 组件实例在创建时都需要经历一系列的初始化步骤,比如设置好数据侦听,编译模板,挂载实例到 DOM,以及在数据改变时更新 DOM。在此过程中,它也会运行被称为生命周期钩子的函数,让开发者有机会在特定阶段运行自己的代码。
生命周期图示
![]()
创建
- 初始化组件的选项(data、methods、computed 等)
- 建立响应式数据系统
beforeCreate
- 时机:实例初始化之后,数据观测和事件配置之前
DOM正在构建/已完成,CSSOM可能尚未完成 - 特点:无法访问到
data、computed、methods等
可以访问this但值为空对象 - 常用场景:
created
- 时机:模板编译/挂载之前,初始化选项API之后
- 特点:可以访问
data、computed、methods
模板还未编译,$el属性还不存在 - 常用场景:异步请求、数据初始化
created() {
console.log('created', this.message); // 'Hello Vue'
console.log('created', this.$el); // undefined
// 适合在这里调用API获取初始数据
this.fetchData();
}
挂载
Vue的挂载阶段是组件从创建到渲染到真实DOM的过程,主要包括两个关键钩子函数
beforeMount
-
时机:在挂载开始之前被调用,此时模板编译已完成,但尚未将真实DOM插入页面。
-
特点:
- 虚拟DOM已经生成
- 模板已编译成render函数
- 尚未替换el内部的HTML内容
- 无法直接操作DOM元素
- 常用场景:
beforeMount() {
// 1. 最后一次数据修改机会(不会触发重渲染)
this.someData = this.processData(this.someData);
// 2. 初始化一些不依赖DOM的配置
this.initConfig();
// 3. 服务端渲染(SSR)中唯一可用的挂载阶段钩子
}
mounted
-
时机:实例挂载完成后调用,此时真实DOM已经渲染完成。
-
特点:
- 真实DOM已生成并插入页面
- 可访问和操作DOM元素
- 可访问子组件
- 不保证所有子组件都已挂载(需使用$nextTick)
- 常用场景:
mounted() {
// 1. DOM操作
this.$refs.input.focus();
// 2. 第三方DOM库初始化
new Chart(this.$refs.canvas, this.chartData);
// 3. 发起数据请求
this.fetchData();
// 4. 添加事件监听
window.addEventListener('resize', this.handleResize);
// 5. 确保子组件已挂载
this.$nextTick(() => {
// 所有子组件都已挂载
});
}
总结对比
| 特性 | beforeMount | mounted |
|---|---|---|
| 访问el | ❌ undefined | ✅ 可访问 |
| 访问真实DOM | ❌ 不可 | ✅ 可 |
| 服务端渲染 | ✅ 可用 | ❌ 不可用 |
| 数据修改 | 不触发更新 | 触发更新 |
| 主要用途 | 最后的数据处理 | DOM操作、请求、插件初始化 |
更新
更新阶段是当响应式数据发生变化时,Vue重新渲染组件的过程,主要包括两个关键钩子函数
beforeUpdate
-
时机:数据变化后,DOM重新渲染之前调用。
-
特点:
- 可以访问更新前的DOM状态
- 数据已经更新,但视图尚未同步
- 适合在更新前访问现有DOM
- 避免在此阶段修改数据(可能导致无限循环)
-
常用场景:
beforeUpdate() {
// 1. 获取更新前的DOM状态(如滚动位置)
this.scrollPosition = this.$refs.container.scrollTop;
// 2. 手动移除动态添加的内容
this.cleanupDynamicContent();
// 3. 记录变化前的状态用于对比
this.beforeData = { ...this.formData };
// 4. 手动处理DOM操作前的准备工作
this.$refs.message.innerHTML = '数据更新中...';
// 5. 计算需要保持的状态(如滚动位置保持)
this.shouldRestoreScroll = true;
}
updated
-
时机:数据变化导致DOM重新渲染完成后调用。
-
特点:
- DOM已更新,可以获取最新DOM状态
- 可以执行依赖于DOM的操作
- 避免在此修改数据(可能导致无限循环)
- 不保证所有子组件都已更新(需用$nextTick)
-
常用场景:
updated() {
// 1. 获取更新后的DOM信息(如元素高度、宽度)
const newHeight = this.$refs.content.offsetHeight;
// 2. 更新完成后滚动到底部或指定位置
if (this.autoScroll) {
this.$refs.chatContainer.scrollTop =
this.$refs.chatContainer.scrollHeight;
}
// 3. 使用$nextTick确保所有子组件更新完成
this.$nextTick(() => {
this.updateComplete = true;
});
// 4. 第三方图表库重新渲染
if (this.chart) {
this.chart.resize();
}
// 5. 触发自定义事件通知外部状态变化
this.$emit('updated', this.getLatestData());
}
总结对比
| 特性 | beforeUpdate | updated |
|---|---|---|
| 执行时机 | DOM更新前 | DOM更新后 |
| 数据状态 | 已更新 | 已更新 |
| DOM状态 | 旧DOM | 新DOM |
| 修改数据 | 谨慎使用 | 极不推荐 |
| 主要用途 | 获取更新前状态、准备操作 | DOM相关操作、第三方库更新 |
| 执行频率 | 每次数据变化 | 每次数据变化 |
卸载
卸载阶段是组件从DOM中移除、清理资源的过程,主要包括两个关键钩子函数
beforeUnmount (Vue 3) / beforeDestroy (Vue 2)
-
时机:组件卸载前调用,实例仍然完全可用。
-
特点:
- 组件实例仍完全可用
- 可以访问data、methods等
- 适合清理资源
- 组件还未销毁
-
常用场景:
// Vue 3 Composition API
onBeforeUnmount(() => {
// 1. 清除定时器
clearInterval(this.timer);
clearTimeout(this.timeout);
// 2. 取消网络请求
if (this.pendingRequest) {
this.pendingRequest.cancel();
}
// 3. 移除全局事件监听
window.removeEventListener('resize', this.handleResize);
document.removeEventListener('click', this.handleClick);
// 4. 销毁第三方库实例
if (this.chart) {
this.chart.dispose();
}
// 5. 取消订阅
this.$bus.off('event', this.handleEvent);
})
// Vue 2 Options API
beforeDestroy() {
// 1. 清除定时器
clearInterval(this.timer);
// 2. 取消网络请求
if (this.pendingRequest) {
this.pendingRequest.cancel();
}
// 3. 移除全局事件监听
window.removeEventListener('resize', this.handleResize);
}
unmounted (Vue 3) / destroyed (Vue 2)
-
时机:组件卸载后调用,此时组件实例已被销毁。
-
特点:
- 组件实例已被销毁
- 所有指令解绑
- 所有事件监听已移除
- 无法访问组件数据和方法
-
常用场景:
// Vue 3 Composition API
onUnmounted(() => {
// 1. 最终的清理确认
console.log('组件已卸载');
// 2. 触发外部通知
this.$emit('destroyed');
// 3. 记录日志
console.log('组件销毁完成', this.$options.name);
})
// Vue 2 Options API
destroyed() {
// 1. 最终的清理确认
console.log('组件已销毁');
// 2. 触发外部通知
this.$emit('destroyed');
// 3. 清理DOM引用
this.$refs = {};
}
总结对比
| 特性 | beforeUnmount/beforeDestroy | unmounted/destroyed |
|---|---|---|
| 执行时机 | 卸载前 | 卸载后 |
| 实例状态 | 完全可用 | 已销毁 |
| 访问data | ✅ 可访问 | ❌ 不可访问 |
| 访问methods | ✅ 可调用 | ❌ 不可调用 |
| 主要用途 | 清理资源、取消订阅 | 最终确认、日志记录 |
| 事件监听 | 可移除 | 已自动移除 |
特殊钩子函数
activated
-
时机:被keep-alive缓存的组件激活时调用。
-
特点:
- 组件从缓存中重新激活
- 适用于频繁切换的组件
- 可替代mounted的部分功能
-
常用场景:
activated() {
// 1. 刷新数据
this.refreshData();
// 2. 恢复状态
this.restoreState();
// 3. 重新添加事件监听
window.addEventListener('scroll', this.handleScroll);
}
deactivated
-
时机:被keep-alive缓存的组件停用时调用。
-
特点:
- 组件被缓存而非销毁
- 组件实例仍保留
- 适合暂停操作而非清理
-
常用场景:
deactivated() {
// 1. 暂停视频播放
this.pauseVideo();
// 2. 保存当前状态
this.saveState();
// 3. 移除临时事件监听
window.removeEventListener('scroll', this.handleScroll);
}
errorCaptured
-
时机:捕获后代组件错误时调用。
-
特点:
- 可捕获子组件、孙组件的错误
- 返回false可阻止错误继续传播
- 可用于错误处理和上报
-
常用场景:
errorCaptured(err, vm, info) {
// 1. 错误日志上报
this.logErrorToServer(err, info);
// 2. 显示错误提示
this.errorMessage = '组件加载失败';
// 3. 阻止错误继续传播
return false;
}
完整生命周期对比表
| 阶段 | Vue 2 | Vue 3 (Options) | Vue 3 (Composition) | 主要用途 |
|---|---|---|---|---|
| 创建 | beforeCreate | beforeCreate | setup() | 初始化前 |
| 创建 | created | created | setup() | 初始化完成 |
| 挂载 | beforeMount | beforeMount | onBeforeMount | 挂载前准备 |
| 挂载 | mounted | mounted | onMounted | DOM操作、请求 |
| 更新 | beforeUpdate | beforeUpdate | onBeforeUpdate | 更新前状态获取 |
| 更新 | updated | updated | onUpdated | 更新后DOM操作 |
| 卸载 | beforeDestroy | beforeUnmount | onBeforeUnmount | 清理资源 |
| 卸载 | destroyed | unmounted | onUnmounted | 销毁确认 |
| 缓存 | activated | activated | onActivated | 缓存激活 |
| 缓存 | deactivated | deactivated | onDeactivated | 缓存停用 |
| 错误 | errorCaptured | errorCaptured | onErrorCaptured | 错误处理 |
available没你想象中的可靠
available没你想象中的可靠
随着iOS系统的更新,iOS官方提供了许多的高版本才支持的功能。但是往往我们需要支持多个版本的iOS系统,在支持新功能的同时也要照顾低版本用户。通常情况下,我们会使用@available(iOS 14.0, *)等方式来做版本限制,但是这可能会引入非预期的crash。
背景
![]()
示例代码如上图所示,UniformTypeIdentifiers.h是一个iOS14以后的系统库,typeWithIdentifier也是一个iOS14以后才支持的API,那么如果我们把用到API的地方放到@available(iOS 14.0, *)的判断条件内,是不是就可以了呢?
![]()
当你在iOS13上运行时会发现报错,内容如上图所示,在低版本中,dyld在启动我们的程序时找不到对应的动态库。
那么为什么我的代码会新增对系统库UniformTypeIdentifiers的依赖呢?对比了两次提交的构建产物发现,我的代码提交之前对UniformTypeIdentifiers的依赖是weak,提交之后就不是了。
![]()
图片 1提交之前
![]()
图片 2 提交之后
很显然,我的available包裹的代码也参与的编译,导致系统认为我依赖了UniformTypeIdentifiers库,然后给我设置了强行依赖。
分析
探索编译参数
既然是编译后出现问题,那么想当然是编译的参数设置有问题。查看了build setting,build phases发现都没有设置UniformTypeIdentifiers的依赖。打开编译log搜索UniformTypeIdentifiers关键字,发现使用的是-framework进行的链接。
很显然这里是使用了strong的方式去依赖,如果改成-weak_framework方式,他就是weak了,但是这里并没有改动,说明这不是根本原因。肯定是某个地方强依赖了符号导致的。
探索符号依赖
进入对应的编译产物目录,找到对应的.o文件,通过nm命令查看对应的符号,发现对于UTType的依赖是强依赖,并不是weak。
![]()
这说明即使我们的代码使用了Available包裹,能够让他在低版本运行时不会因为方法找不到,但是也可能会导致因为低版本的系统库没有对应的动态库从而导致启动的时候就会crash。
解决方案
知道了问题然后解决方案就很多了,首先我们现在build phases中设置对于UniformTypeIdentifiers是weak,也就是可选依赖。
![]()
防劣化
那么怎么防止后续再出现同样的问题呢?
我们可以通过脚本在每次构建之后分析当前的动态库依赖,如果发现新增动态库或者动态库的依赖方式发生变化。则告警。
告警格式如下:
![]()
番外篇
当我把同样的代码放到demo工程中的时候。表现却不一样。同样的代码,同样的最低系统支持,同样的编译参数。
![]()
图片 3编译参数
![]()
图片 4 产物依赖![]()
图片 5 符号依赖
可以看到,符号依赖也是weak的。这就很奇怪了。按照上面的结论这里应该是strong才对。那么到底是为什么同样的代码在不同的工程中产生了不一样的结论呢?
抛开无关的条件之后,最基本的代码如下所示。
#import <UIKit/UIKit.h>
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
void test(void) {
if (@available(iOS 14.0, *)) {
UTType *t = [UTType typeWithIdentifier:@"public.image"];
(void)t;
}
}
调用命令行编译
# 编译
xcrun clang -c test.m -o test.o \
-target arm64-apple-ios12.0 \
-isysroot $(xcrun --sdk iphoneos --show-sdk-path) \
-fmodules \
-fmodules-cache-path=/tmp/modcache
# 检查符号
nm -m test.o | grep "OBJC_CLASS.*UTType"
结果发现依然是weak
但是当我们改变一下import的顺序,发现结果不一样
代码如下:
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#import <UIKit/UIKit.h>
void test(void) {
if (@available(iOS 14.0, *)) {
UTType *t = [UTType typeWithIdentifier:@"public.image"];
(void)t;
}
}
如果我们先导入UTI,然后再导入UIKit。他就会变成strong。
所以是在编译的时候系统分析符号依赖时,如果先导入UTI,他会认为此时符号是strong的,然后我们编译完成真正的用到了UTI的符号,此时就会变成强依赖符号。最后生成的产物就会强依赖UniformTypeIdentifiers库。
// 源码来源: https://github.com/llvm/llvm-project/blob/main/clang/lib/AST/DeclBase.cpp
bool Decl::isWeakImported() const {
bool IsDefinition;
if (!canBeWeakImported(IsDefinition))
return false;
// ⚠️ 关键:只检查"最新声明"的属性
for (const auto *A : getMostRecentDecl()->attrs()) {
if (isa<WeakImportAttr>(A))
return true;
if (const auto *Availability = dyn_cast<AvailabilityAttr>(A)) {
if (CheckAvailability(getASTContext(), Availability, nullptr,
VersionTuple()) == AR_NotYetIntroduced)
return true;
}
}
return false; // 默认返回 false(Strong)
}
【从零开始学习Vue|第七篇】深入组件——Props
1. Props声明
- 一个组件需要显式声明它所接受的 props,这样 Vue 才能知道外部传入的哪些是 props,哪些是透传 attribute
在使用 <script setup> 的单文件组件中,props 可以使用 defineProps() 宏来声明:
<script setup>
const props = defineProps(['foo'])
console.log(props.foo)
</script>
除了使用字符串数组来声明 props 外,还可以使用对象的形式:
// 使用 <script setup>
defineProps({
title: String,
likes: Number
})
对于以对象形式声明的每个属性,key 是 prop 的名称,而值则是该 prop 预期类型的构造函数。比如,如果要求一个 prop 的值是 number 类型,则可使用 Number 构造函数作为其声明的值。
2. 响应式Props解构(vue3.5)
2.1. Vue3.5 之前
在 Vue 3.4 及之前版本,直接解构 defineProps() 会丢失响应性:
<script setup>
// ❌ 错误方式 - 会丢失响应性
const { title } = defineProps(['title'])
// 当父组件更新 title 时,这里的 title 不会更新
</script>
传统解决方案:需要使用 toRefs() 或 toRef() 来保持响应性:
<script setup>
import { toRefs, toRef } from 'vue'
const props = defineProps(['title', 'count'])
// 方案1:toRefs 保持响应式引用
const { title, count } = toRefs(props)
// 方案2:toRef 单个转换
const title = toRef(props, 'title')
</script>
2.2. Vue3.5 新特征
Vue 3.5 允许直接解构 defineProps(),同时自动保持响应性:
<script setup>
// ✅ Vue 3.5+ 可以直接解构,保持响应性
const { title, count } = defineProps(['title', 'count'])
// 这些变量是响应式的,会随父组件更新而更新
</script>
3. 传递prop的细节
3.1. Prop名字格式
如果一个 prop 的名字很长,应使用 camelCase 形式,因为它们是合法的 JavaScript 标识符,可以直接在模板的表达式中使用,也可以避免在作为属性 key 名时必须加上引号。
defineProps({
greetingMessage: String
})
<span>{{ greetingMessage }}</span>
3.2. props校验详解
3.2.1. 基础语法(对象语法)
<script setup>
const props = defineProps({
name: {
type: String,
required: true,
default: '匿名用户',
validator: (value) => {
return value.length > 0
}
}
})
</script>
也可以支持多类型,如下
<script setup>
const props = defineProps({
// 可以是字符串或数字
id: [String, Number],
// 可以是字符串或布尔值
flag: [String, Boolean],
// 多种类型
value: [String, Number, Boolean, Object]
})
</script>
告别后端转换:前端实现 Word & PDF 高性能预览实战
在企业级应用中,文档预览不仅仅是“能看”,更是关于隐私安全(不传三方服务器 )与 极致性能(大文件不卡顿)的博弈。
今天我们就从实战角度,手把手拆解如何利用 docx-preview 和 vue-pdf-embed 搭建一套纯前端、工业级的文档预览系统。
一、 通俗易懂:它们到底是怎么工作的?
我们可以把文档预览想象成“翻译”过程:
- docx-preview:它像是一个**“拆解大师”**。Word 文档(.docx)本质上是一个压缩包,里面装满了 XML 格式的文字和排版信息。这个库在浏览器里直接解压它,并把 XML 翻译成我们熟悉的 HTML 网页。
-
vue-pdf-embed:它像是一个**“高清投影仪”**。基于强大的
pdf.js,它将 PDF 的每一页绘制在 Canvas(画布)上,并额外覆盖一层透明的“文字层”,让你可以像在网页上一样选中和复制文字。
二、 Word 预览篇:docx-preview 极速落地
在金融场景下,Word 预览最怕样式乱掉。使用这个库时,必须注意样式隔离。
1. 实战代码:封装一个稳健的 Word 预览组件
代码段
<template>
<div class="word-preview-container">
<div v-if="loading" class="status-tip">文档解析中...</div>
<div ref="fileContainer" class="render-box"></div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue';
import { renderAsync } from 'docx-preview';
const props = defineProps({ url: { type: String, required: true } });
const fileContainer = ref(null);
const loading = ref(false);
const getFileAndRender = async () => {
loading.value = true;
try {
// 1. 获取二进制流
const response = await fetch(props.url);
const blob = await response.blob();
// 2. 渲染
await renderAsync(blob, fileContainer.value, null, {
className: "docx-inner", // 自定义类名
inWrapper: true, // 必须开启,确保样式被包裹在内部,不污染全局
ignoreWidth: false, // 尊重原文档宽度
});
} catch (e) {
console.error('Word 预览失败', e);
} finally {
loading.value = false;
}
};
onMounted(() => getFileAndRender());
</script>
<style scoped>
.render-box {
width: 100%;
height: 80vh;
overflow-y: auto;
/* 解决 8 年老兵最头疼的:居中显示与背景色 */
background-color: #f0f2f5;
padding: 20px;
}
</style>
三、 PDF 预览篇:vue-pdf-embed 的深度掌控
在处理金融合规文档或长篇研报时,单纯展示图片是不够的。你需要**文字层(Text Layer)**来搜索和复制。
1. 实战代码:带“文字层”的高保真预览
代码段
<template>
<div class="pdf-preview-box">
<VuePdfEmbed
:source="props.url"
text-layer
annotation-layer
class="pdf-canvas"
/>
</div>
</template>
<script setup>
import VuePdfEmbed from 'vue-pdf-embed'
// 必须引入样式,否则文字层会错位
import 'vue-pdf-embed/dist/styles/textLayer.css'
import 'vue-pdf-embed/dist/styles/annotationLayer.css'
const props = defineProps({ url: { type: String, required: true } })
</script>
<style scoped>
.pdf-preview-box {
width: 100%;
height: 80vh;
overflow-y: auto;
}
/* 优化 Canvas 渲染,防止高分屏模糊 */
.pdf-canvas {
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
margin-bottom: 20px;
}
</style>
四、“性能避坑”指南
-
内存回收:这两个库在渲染大文件时会占用极高内存。在 Vue 组件卸载(
onUnmounted)时,务必清空容器内容(fileContainer.value.innerHTML = ''),否则多看几个文档浏览器就 OOM 了。 -
异步切断:如果用户点击列表过快,前一个文档还没加载完就换下一个,记得使用
AbortController取消之前的fetch请求。 -
样式冲突:
docx-preview会插入大量 CSS。一定要开启inWrapper: true配置,否则你会发现你的导航栏背景色莫名其妙被 Word 的背景色覆盖了。
HTML 早已不是标签了,它现在是系统级接口:这 9 个 API 直接干翻常用 JS 库
HTML 早已不再是简单的“超文本标记”,它更像是一个连接底层硬件、浏览器内核与用户交互的系统级接口集合。
在现代 Web 架构中,很多原本依赖庞大 JS 库(如 jQuery, Axios, Socket.io)实现的功能,现在通过原生 HTML API 就能以更低的功耗和更高的性能完成。
一、 Popover API:零 JS 实现“浮层顶层化”
场景: 在监控仪表盘中,点击“详细指标”展示一个不被父容器 overflow: hidden 遮挡的浮窗。
-
HTML 实现:
HTML
<button popovertarget="metric-detail">查看详情</button> <div id="metric-detail" popover> <h4>实时指标详情</h4> <p>CPU 负载: 85%</p> </div> -
底层干货: 它会自动进入浏览器的 Top Layer(顶层渲染层),层级永远高于
z-index: 9999,且无需任何 JS 监听点击外部关闭的逻辑。
二、 Dialog API:受控的模态对话框
场景: 监控报警触发时,弹出一个强制用户交互的模态确认框。
-
HTML 与 JS 交互:
HTML
<dialog id="alarm-dialog"> <form method="dialog"> <p>确认关闭此报警?</p> <button value="cancel">取消</button> <button value="confirm">确认</button> </form> </dialog> <script> const dialog = document.getElementById('alarm-dialog'); // 1. 弹出模态框:自带背景遮罩 (::backdrop) dialog.showModal(); // 2. 获取结果:无需监听按钮点击,直接监听 close 事件 dialog.addEventListener('close', () => { console.log('用户选择了:', dialog.returnValue); // 'confirm' 或 'cancel' }); </script>
三、 Speculation Rules API:让页面跳转“瞬发”
场景: 监控首页有很多链接通往“分析页”,你预测用户 80% 的概率会点第一个链接。
-
具体配置:
HTML
<script type="speculationrules"> { "prerender": [{ "source": "list", "urls": ["/analysis/cpu-metrics"], "score": 0.8 }] } </script> -
工程意义: 这不是简单的预加载,而是预渲染。浏览器会在后台开启一个隐形标签页渲染目标页面。当用户点击时,页面切换时间趋于 0ms。
四、 View Transitions API:极致的 UI 平滑度
场景: 在监控系统中,从“列表视图”切换到“详情视图”,希望卡片能有一个平滑的缩放位移动画。
-
代码实现:
JavaScript
function switchView() { // 1. 检查浏览器支持 if (!document.startViewTransition) { updateDOM(); // 降级处理 return; } // 2. 开启视图转换 document.startViewTransition(() => { // 在回调函数中执行 DOM 变更 updateDOM(); }); } -
CSS 配合:
CSS
/* 给需要动画的元素定义一个唯一的转换名称 */ .metric-card { view-transition-name: active-card; } -
原理: 浏览器会截取“旧状态”和“新状态”的快照,并自动在两者之间创建位移、缩放和淡入淡出动画。
五、 WebAssembly (Wasm) 与 JS 的零拷贝交互
场景: 监控系统中,前端需要实时计算成千上万个点的趋势。
-
具体用法:
JavaScript
// 在 HTML 中直接通过 Module 引入 import init, { calculate_metrics } from './analytics_bg.wasm'; async function run() { await init(); const buffer = new SharedArrayBuffer(1024); // 使用共享内存 const view = new Float64Array(buffer); // 直接把内存地址传给 Wasm 处理,避免数据在大规模拷贝时的开销 const result = calculate_metrics(view); } -
工程价值: HTML 通过 Module 赋予了 Wasm 极高的集成度。对于计算密集型任务,这是 Node.js 或前端的终极提速手段。
六、 WebTransport API:HTTP/3 时代的实时通信
场景: 在你的监控系统中,如果有数万台设备在毫秒级上报数据,WebSocket 的 TCP 队头阻塞(Head-of-Line Blocking)会导致延迟堆积。
-
具体用法:
JavaScript
// 建立基于 HTTP/3 QUIC 的连接 const transport = new WebTransport("https://metrics.your-server.com:443"); await transport.ready; // 发送不可靠(双向)流:适合对实时性要求极高、丢失一两帧也没关系的监控指标 const writer = transport.datagrams.writable.getWriter(); const data = new TextEncoder().encode(JSON.stringify({ cpu: 85 })); await writer.write(data); -
工程价值: 它基于 UDP,不仅比 WebSocket 更快,还支持多路复用。即使网络波动,其中一个流卡住了,也不会影响其他流。
七、 Intersection Observer API (V2):精准感知“真实可见性”
场景: 监控 SDK 的广告反欺诈,或者极高性能的长列表渲染。
-
具体用法:
JavaScript
const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { // isVisible 会检测该元素是否被其他元素遮挡,或者是否有滤镜/透明度导致看不见 if (entry.isIntersecting && entry.isVisible) { sendMetric('element-real-view'); } }); }, { trackVisibility: true, // 开启真实可见性追踪 delay: 100 // 延迟检测以减轻 CPU 压力 }); observer.observe(targetNode); -
工程价值: 它是实现“无感监控”的利器。相比于 V1,它能告诉你用户是否真的看到了元素,而不仅仅是元素在视口内。
八、 Compression Streams API:浏览器原生无损压缩
场景: 监控 SDK 在上报巨大的 JSON 日志(如数 MB 的错误堆栈)前,先在前端进行压缩。
-
具体用法:
JavaScript
async function compressAndSend(data) { const stream = new Blob([JSON.stringify(data)]).stream(); const compressedStream = stream.pipeThrough(new CompressionStream('gzip')); // 这里的 response 就是 Gzip 压缩后的二进制流 const response = await new Response(compressedStream).blob(); navigator.sendBeacon('/log', response); } -
工程价值: 彻底抛弃
pako.js等三方库,减少了包体积,且利用浏览器原生能力,压缩效率更高。
九、 File System Access API:把 Web 应用变成本地工具
场景: 开发一个本地离线日志分析工具,直接读取并保存用户的 GB 级日志文件。
-
具体用法:
JavaScript
async function openLogFile() { // 1. 获取文件句柄 const [handle] = await window.showOpenFilePicker(); const file = await handle.getFile(); // 2. 像 Node.js 一样获取可写流 const writable = await handle.createWritable(); await writable.write("New Log Entry"); await writable.close(); } -
工程价值: 不再是
input type="file"那种简单的“上传”,而是真正实现了对文件的双向读写。
每日一题-二进制表示中质数个计算置位🟢
给你两个整数 left 和 right ,在闭区间 [left, right] 范围内,统计并返回 计算置位位数为质数 的整数个数。
计算置位位数 就是二进制表示中 1 的个数。
- 例如,
21的二进制表示10101有3个计算置位。
示例 1:
输入:left = 6, right = 10 输出:4 解释: 6 -> 110 (2 个计算置位,2 是质数) 7 -> 111 (3 个计算置位,3 是质数) 9 -> 1001 (2 个计算置位,2 是质数) 10-> 1010 (2 个计算置位,2 是质数) 共计 4 个计算置位为质数的数字。
示例 2:
输入:left = 10, right = 15 输出:5 解释: 10 -> 1010 (2 个计算置位, 2 是质数) 11 -> 1011 (3 个计算置位, 3 是质数) 12 -> 1100 (2 个计算置位, 2 是质数) 13 -> 1101 (3 个计算置位, 3 是质数) 14 -> 1110 (3 个计算置位, 3 是质数) 15 -> 1111 (4 个计算置位, 4 不是质数) 共计 5 个计算置位为质数的数字。
提示:
1 <= left <= right <= 1060 <= right - left <= 104
【宫水三叶】一题双解 :「lowbit」&「分治」
模拟 + lowbit
利用一个 int 的二进制表示不超过 $32$,我们可以先将 $32$ 以内的质数进行打表。
从前往后处理 $[left, right]$ 中的每个数 $x$,利用 lowbit 操作统计 $x$ 共有多少位 $1$,记为 $cnt$,若 $cnt$ 为质数,则对答案进行加一操作。
代码:
###Java
class Solution {
static boolean[] hash = new boolean[40];
static {
int[] nums = new int[]{2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31};
for (int x : nums) hash[x] = true;
}
public int countPrimeSetBits(int left, int right) {
int ans = 0;
for (int i = left; i <= right; i++) {
int x = i, cnt = 0;
while (x != 0 && ++cnt >= 0) x -= (x & -x);
if (hash[cnt]) ans++;
}
return ans;
}
}
- 时间复杂度:$O((right - left) * \log{right})$
- 空间复杂度:$O(C)$
模拟 + 分治
枚举 $[left, right]$ 范围内的数总是不可避免,上述解法的复杂度取决于复杂度为 $O(\log{x})$ 的 lowbit 操作。
而比 lowbit 更加优秀的统计「二进制 $1$ 的数量」的做法最早在 (题解) 191. 位1的个数 讲过,采用「分治」思路对二进制进行成组统计,复杂度为 $O(\log{\log{x}})$。
代码:
###Java
class Solution {
static boolean[] hash = new boolean[40];
static {
int[] nums = new int[]{2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31};
for (int x : nums) hash[x] = true;
}
public int countPrimeSetBits(int left, int right) {
int ans = 0;
for (int i = left; i <= right; i++) {
int x = i;
x = (x & 0x55555555) + ((x >>> 1) & 0x55555555);
x = (x & 0x33333333) + ((x >>> 2) & 0x33333333);
x = (x & 0x0f0f0f0f) + ((x >>> 4) & 0x0f0f0f0f);
x = (x & 0x00ff00ff) + ((x >>> 8) & 0x00ff00ff);
x = (x & 0x0000ffff) + ((x >>> 16) & 0x0000ffff);
if (hash[x]) ans++;
}
return ans;
}
}
- 时间复杂度:$O((right - left) * \log{\log{right}})$
- 空间复杂度:$O(C)$
其他「位运算」相关内容
考虑加练其他「位运算」相关内容 🍭🍭🍭
| 题目 | 题解 | 难度 | 推荐指数 |
|---|---|---|---|
| 137. 只出现一次的数字 II | LeetCode 题解链接 | 中等 | 🤩🤩🤩 |
| 190. 颠倒二进制位 | LeetCode 题解链接 | 简单 | 🤩🤩🤩 |
| 191. 位1的个数 | LeetCode 题解链接 | 简单 | 🤩🤩🤩 |
| 231. 2 的幂 | LeetCode 题解链接 | 简单 | 🤩🤩🤩 |
| 338. 比特位计数 | LeetCode 题解链接 | 简单 | 🤩🤩🤩 |
| 342. 4的幂 | LeetCode 题解链接 | 简单 | 🤩🤩🤩 |
| 461. 汉明距离 | LeetCode 题解链接 | 简单 | 🤩🤩🤩🤩 |
| 477. 汉明距离总和 | LeetCode 题解链接 | 简单 | 🤩🤩🤩🤩 |
| 1178. 猜字谜 | LeetCode 题解链接 | 困难 | 🤩🤩🤩🤩 |
| 剑指 Offer 15. 二进制中1的个数 | LeetCode 题解链接 | 简单 | 🤩🤩🤩 |
注:以上目录整理来自 wiki,任何形式的转载引用请保留出处。
最后
如果有帮助到你,请给题解点个赞和收藏,让更多的人看到 ~ ("▔□▔)/
也欢迎你 关注我 和 加入我们的「组队打卡」小群 ,提供写「证明」&「思路」的高质量题解。
所有题解已经加入 刷题指南,欢迎 star 哦 ~
二进制表示中质数个计算置位
方法一:数学 + 位运算
我们可以枚举 $[\textit{left},\textit{right}]$ 范围内的每个整数,挨个判断是否满足题目要求。
对于每个数 $x$,我们需要解决两个问题:
- 如何求出 $x$ 的二进制中的 $1$ 的个数,见「191. 位 1 的个数」,下面代码用库函数实现;
- 如何判断一个数是否为质数,见「204. 计数质数」的「官方解法」的方法一(注意 $0$ 和 $1$ 不是质数)。
###Python
class Solution:
def isPrime(self, x: int) -> bool:
if x < 2:
return False
i = 2
while i * i <= x:
if x % i == 0:
return False
i += 1
return True
def countPrimeSetBits(self, left: int, right: int) -> int:
return sum(self.isPrime(x.bit_count()) for x in range(left, right + 1))
###C++
class Solution {
bool isPrime(int x) {
if (x < 2) {
return false;
}
for (int i = 2; i * i <= x; ++i) {
if (x % i == 0) {
return false;
}
}
return true;
}
public:
int countPrimeSetBits(int left, int right) {
int ans = 0;
for (int x = left; x <= right; ++x) {
if (isPrime(__builtin_popcount(x))) {
++ans;
}
}
return ans;
}
};
###Java
class Solution {
public int countPrimeSetBits(int left, int right) {
int ans = 0;
for (int x = left; x <= right; ++x) {
if (isPrime(Integer.bitCount(x))) {
++ans;
}
}
return ans;
}
private boolean isPrime(int x) {
if (x < 2) {
return false;
}
for (int i = 2; i * i <= x; ++i) {
if (x % i == 0) {
return false;
}
}
return true;
}
}
###C#
public class Solution {
public int CountPrimeSetBits(int left, int right) {
int ans = 0;
for (int x = left; x <= right; ++x) {
if (IsPrime(BitCount(x))) {
++ans;
}
}
return ans;
}
private bool IsPrime(int x) {
if (x < 2) {
return false;
}
for (int i = 2; i * i <= x; ++i) {
if (x % i == 0) {
return false;
}
}
return true;
}
private static int BitCount(int i) {
i = i - ((i >> 1) & 0x55555555);
i = (i & 0x33333333) + ((i >> 2) & 0x33333333);
i = (i + (i >> 4)) & 0x0f0f0f0f;
i = i + (i >> 8);
i = i + (i >> 16);
return i & 0x3f;
}
}
###go
func isPrime(x int) bool {
if x < 2 {
return false
}
for i := 2; i*i <= x; i++ {
if x%i == 0 {
return false
}
}
return true
}
func countPrimeSetBits(left, right int) (ans int) {
for x := left; x <= right; x++ {
if isPrime(bits.OnesCount(uint(x))) {
ans++
}
}
return
}
###C
bool isPrime(int x) {
if (x < 2) {
return false;
}
for (int i = 2; i * i <= x; ++i) {
if (x % i == 0) {
return false;
}
}
return true;
}
int countPrimeSetBits(int left, int right){
int ans = 0;
for (int x = left; x <= right; ++x) {
if (isPrime(__builtin_popcount(x))) {
++ans;
}
}
return ans;
}
###JavaScript
var countPrimeSetBits = function(left, right) {
let ans = 0;
for (let x = left; x <= right; ++x) {
if (isPrime(bitCount(x))) {
++ans;
}
}
return ans;
};
const isPrime = (x) => {
if (x < 2) {
return false;
}
for (let i = 2; i * i <= x; ++i) {
if (x % i === 0) {
return false;
}
}
return true;
}
const bitCount = (x) => {
return x.toString(2).split('0').join('').length;
}
复杂度分析
-
时间复杂度:$O((\textit{right}-\textit{left})\sqrt{\log\textit{right}})$。二进制中 $1$ 的个数为 $O(\log\textit{right})$,判断值为 $x$ 的数是否为质数的时间为 $O(\sqrt{x})$。
-
空间复杂度:$O(1)$。我们只需要常数的空间保存若干变量。
方法二:判断质数优化
注意到 $\textit{right} \le 10^6 < 2^{20}$,因此二进制中 $1$ 的个数不会超过 $19$,而不超过 $19$ 的质数只有
$$
2, 3, 5, 7, 11, 13, 17, 19
$$
我们可以用一个二进制数 $\textit{mask}=665772=10100010100010101100_{2}$ 来存储这些质数,其中 $\textit{mask}$ 二进制的从低到高的第 $i$ 位为 $1$ 表示 $i$ 是质数,为 $0$ 表示 $i$ 不是质数。
设整数 $x$ 的二进制中 $1$ 的个数为 $c$,若 $\textit{mask}$ 按位与 $2^c$ 不为 $0$,则说明 $c$ 是一个质数。
###Python
class Solution:
def countPrimeSetBits(self, left: int, right: int) -> int:
return sum(((1 << x.bit_count()) & 665772) != 0 for x in range(left, right + 1))
###C++
class Solution {
public:
int countPrimeSetBits(int left, int right) {
int ans = 0;
for (int x = left; x <= right; ++x) {
if ((1 << __builtin_popcount(x)) & 665772) {
++ans;
}
}
return ans;
}
};
###Java
class Solution {
public int countPrimeSetBits(int left, int right) {
int ans = 0;
for (int x = left; x <= right; ++x) {
if (((1 << Integer.bitCount(x)) & 665772) != 0) {
++ans;
}
}
return ans;
}
}
###C#
public class Solution {
public int CountPrimeSetBits(int left, int right) {
int ans = 0;
for (int x = left; x <= right; ++x) {
if (((1 << BitCount(x)) & 665772) != 0) {
++ans;
}
}
return ans;
}
private static int BitCount(int i) {
i = i - ((i >> 1) & 0x55555555);
i = (i & 0x33333333) + ((i >> 2) & 0x33333333);
i = (i + (i >> 4)) & 0x0f0f0f0f;
i = i + (i >> 8);
i = i + (i >> 16);
return i & 0x3f;
}
}
###go
func countPrimeSetBits(left, right int) (ans int) {
for x := left; x <= right; x++ {
if 1<<bits.OnesCount(uint(x))&665772 != 0 {
ans++
}
}
return
}
###C
int countPrimeSetBits(int left, int right){
int ans = 0;
for (int x = left; x <= right; ++x) {
if ((1 << __builtin_popcount(x)) & 665772) {
++ans;
}
}
return ans;
}
###JavaScript
var countPrimeSetBits = function(left, right) {
let ans = 0;
for (let x = left; x <= right; ++x) {
if (((1 << bitCount(x)) & 665772) != 0) {
++ans;
}
}
return ans;
};
const bitCount = (x) => {
return x.toString(2).split('0').join('').length;
}
复杂度分析
-
时间复杂度:$O(\textit{right}-\textit{left})$。
-
空间复杂度:$O(1)$。我们只需要常数的空间保存若干变量。
二进制表示中质数个计算置位 - Java超越99%的简单写法
解题思路
-
L,R最大为 $10^6$,转换为二进制,有 20 位,故 计算置位 个数不会超过 20。即求出 20 以内的质数列表即可。 - 使用
Integer.bitCount(i)函数可快速求得i的二进制形式中 1 的个数。
代码:
class Solution {
public int countPrimeSetBits(int L, int R) {
//0-20的质数列表,prime[i]为1,则i为质数
int[] primes = {0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1};
int res = 0;
for (int i = L; i <= R; i++) {
int t = Integer.bitCount(i);
res += primes[t];
}
return res;
}
}
告别“幻影坦克”:手把手教你丝滑规避布局抖动,让页面渲染快如闪电!
🚀 告别“幻影坦克”:手把手教你丝滑规避布局抖动,让页面渲染快如闪电!
前端性能优化专栏 - 第十篇
在前几篇中,我们聊过了字体加载优化(拒绝 FOIT/FOUT)、SVG 雪碧图(终结 HTTP 请求地狱)以及图片加载策略。如果说那些是针对“外部资源”的闪电战,那么今天我们要聊的,则是针对“浏览器内部渲染”的持久战。
不知道你有没有遇到过这种诡异的情况:明明资源都加载完了,图片也秒开了,但页面滚动起来却像是在跳“霹雳舞”,卡顿得让人怀疑人生?或者 CPU 占用率莫名其妙飙升,风扇转得比你赶需求的心还快?
恭喜你,你可能遇到了前端性能优化中的“隐形杀手”——布局抖动(Layout Thrashing) 。今天,咱们就来扒一扒这个让浏览器引擎“抓狂”的罪魁祸首,看看如何用最优雅的姿势把它按在地上摩擦。
⚠️ 什么是布局抖动?(Layout Thrashing)
布局抖动,在学术界有个更响亮的名字叫强制同步布局(Forced Synchronous Layout) 。
📖 专业名词解释:强制同步布局 正常情况下,浏览器会把 DOM 变更“攒着”批量处理。但如果你在修改样式后立即读取几何属性,浏览器为了给你一个准确的数值,不得不打破节奏,立刻执行一次完整的样式计算和布局过程。这种“被迫营业”的行为就是强制同步布局。
典型特征
在极短的时间内,代码交替执行以下操作:
- 写:修改 DOM 样式(比如改个宽度、高度、位置)。
-
读:读取 DOM 的几何属性(比如
offsetWidth、clientHeight、offsetTop等)。
![]()
✨ 浏览器的一帧:理想与现实
在理想的世界里,浏览器是非常“聪明”且“懒惰”的。它会把所有的 DOM 变更(写操作)先攒着,等到这一帧快结束的时候,再统一进行渲染流水线。
理想的渲染流水线:
- Recalculate Style(计算样式)
- Layout / Reflow(计算布局)
- Paint(绘制)
- Composite(合成)
在 60 FPS 的要求下,一帧只有 16.6ms。浏览器希望一次性完成这些工作。但布局抖动会让浏览器在同一帧内多次重新布局和重绘,直接导致 CPU 飙升、帧率下降、交互延迟。
🔄 强制同步布局是如何被触发的?
当你先写入了 DOM(改了样式),紧接着又去读取依赖布局的属性时,浏览器心里苦啊: “你刚改了样式,我还没来得及算新的布局呢!为了给你一个准确的读数,我只能现在停下手头所有活儿,强行算一遍布局给你看。”
如果在循环中不断交替读写,就会产生灾难性的后果。
❌ 错误示例:布局抖动制造机
const paragraphs = document.querySelectorAll('p');
const box = document.querySelector('.box');
for (let i = 0; i < paragraphs.length; i++) {
// 每次循环:先写(改宽度),再读(读 box.offsetWidth)
// 浏览器:我太难了,每一轮都要重算一遍布局!
paragraphs[i].style.width = box.offsetWidth + 'px';
}
后果: 循环次数 = 潜在布局计算次数。列表越长,性能灾难越明显。
🔧 终极武器:读写分离
解决布局抖动的核心思想非常简单,就四个字:读写分离。
✅ 优化后的代码:丝滑顺畅
const paragraphs = document.querySelectorAll('p');
const box = document.querySelector('.box');
// 1. 先完成所有“读取操作”,并缓存结果
const width = box.offsetWidth;
// 2. 再进行所有“写入操作”
for (let i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = width + 'px';
}
💡 关键思想:
- 原则 1:先读后写,批量进行。先把所有需要的布局信息一次性读出来并缓存,再用这些缓存值进行批量 DOM 更新。
- 原则 2:避免读写交织。在一个宏任务或一帧内,保持“所有读在前,所有写在后”的严谨顺序。
![]()
🛠️ 更多实战技巧
除了读写分离,还有这些锦囊妙计:
-
批量 DOM 更新:使用
DocumentFragment或一次性字符串拼接再设置innerHTML,避免在循环中频繁增删节点。 -
利用样式类:给节点添加/移除 class,而不是多次逐个设置
style.xxx。 -
动画优化:动画过程尽量用
transform、opacity。几何测量放在动画开始前或节流后的回调中。 -
使用
requestAnimationFrame:在一帧开始时集中“读”,在回调中集中“写”。
⚠️ 在 React/Vue 框架中仍会踩坑吗?
会! 框架并不会自动帮你规避所有布局抖动。
典型场景:
- 在
useEffect中:先测量 DOM(读),再立即设置状态导致重新渲染,同时又在后续 effect 中继续读。 - 在
useLayoutEffect中:由于它在浏览器绘图前同步执行,读写交织更容易触发同步布局。
✅ 小结回顾
理解浏览器渲染机制 + 有意识地“分离读写”,是迈向高级前端开发者的必经之路。
- 什么是布局抖动:在短时间内交替读写 DOM 几何属性,迫使浏览器在一帧内多次同步布局计算。
- 为什么会发生:浏览器为了返回准确的几何信息,被迫打破原本“延迟、批量”的优化策略。
- 如何避免:分离读写操作,先读后写,成批进行。
掌握了这些,你就能让你的页面告别“打摆子”,重回丝滑巅峰!
下一篇预告:浏览器重排与重绘——那些年我们一起追过的渲染流水线。 敬请期待!
【节点】[MainLightShadow节点]原理解析与实际应用
摘要 MainLightShadow节点是Unity URP ShaderGraph中处理主光源阴影的关键工具,支持实时阴影与ShadowMask阴影的动态混合。该节点封装了阴影映射和光照贴图技术,通过LightmapUV和PositionWS输入端口实现高质量阴影效果,输出0-1范围的阴影强度值用于材质调制。文章详细解析了节点功能、端口配置、使用方法及常见问题,并通过基础漫反射、风格化阴影等示例展示其应用场景。节点仅兼容URP管线,需配合正确的场景阴影设置使用,平衡性能与视觉效果。
Main Light Shadow 节点是 Unity URP Shader Graph 中用于处理主光源阴影信息的重要工具。在实时渲染中,阴影是实现真实感光照效果的关键因素之一,它能够为场景中的物体提供深度感和空间关系。该节点专门设计用于获取和混合主光源的阴影数据,包括实时阴影和 ShadowMask 阴影,同时根据场景设置动态调整阴影的混合距离。这使得开发者能够创建更加复杂和逼真的阴影效果,而无需手动编写复杂的着色器代码。
在 Unity 的通用渲染管线中,阴影处理是一个多层次的过程,涉及实时阴影映射、ShadowMask 技术以及阴影混合。Main Light Shadow 节点将这些功能封装成一个易于使用的节点,简化了着色器开发流程。通过该节点,开发者可以轻松访问主光源的阴影信息,并将其应用于材质表面,实现从完全阴影到完全光照的平滑过渡。这在开放世界游戏或动态光照场景中尤为重要,因为阴影需要根据物体与光源的距离和场景设置进行动态调整。
该节点的设计考虑了性能和质量的平衡。它支持 URP 的混合阴影系统,允许在同一个场景中使用实时阴影和烘焙阴影,并根据距离进行无缝混合。这意味着在近处,物体可以使用高质量的实时阴影,而在远处,则可以切换到性能更优的烘焙阴影,从而在保持视觉质量的同时优化渲染性能。此外,节点还处理了 ShadowMask 阴影,这是一种基于光照贴图的阴影技术,适用于静态物体,能够提供高质量的阴影效果而不增加实时计算开销。
使用 Main Light Shadow 节点时,开发者需要理解其输入和输出端口的含义,以及如何将这些端口与其他节点连接以构建完整的阴影效果。例如,通过将节点的输出连接到材质的 Alpha 通道或颜色输入,可以控制阴影的强度和分布。同时,节点还支持自定义光照贴图 UV 和世界空间位置输入,这使得它能够适应复杂的材质需求,如基于物体位置动态调整阴影。
在本文中,我们将深入探讨 Main Light Shadow 节点的各个方面,包括其详细描述、端口功能、使用方法、示例应用以及常见问题解答。通过阅读本文,您将能够掌握如何高效地使用该节点来增强您的 URP 项目中的阴影效果。
描述
Main Light Shadow 节点是 URP Shader Graph 中的一个功能节点,主要用于获取主光源的阴影信息。阴影在实时渲染中扮演着关键角色,它不仅增强了场景的真实感,还帮助用户理解物体之间的空间关系。该节点通过结合实时阴影和 ShadowMask 阴影数据,提供了一个统一的接口来处理主光源的阴影计算。实时阴影是通过动态阴影映射技术生成的,适用于移动物体或动态光源,而 ShadowMask 阴影则是基于预计算的光照贴图,适用于静态物体,以优化性能。
该节点的一个核心特性是其能够根据场景设置动态调整阴影的混合距离。在 URP 中,阴影混合是一种技术,用于在实时阴影和烘焙阴影之间实现平滑过渡。例如,在近距离内,物体可能使用实时阴影以保持高精度和动态响应,而在远距离,则切换到 ShadowMask 阴影以减少计算开销。Main Light Shadow 节点自动处理这种混合过程,输出一个介于 0 到 1 之间的值,其中 0 表示完全阴影(无光照),1 表示完全光照(无阴影)。这使得开发者可以轻松地将阴影效果集成到材质中,无需关心底层的混合逻辑。
此外,Main Light Shadow 节点还支持光照贴图 UV 输入,这使得它能够正确处理基于光照贴图的阴影信息。光照贴图是一种预先计算的光照数据,存储在纹理中,用于静态物体的阴影和光照。通过提供正确的光照贴图 UV,节点可以采样 ShadowMask 数据,并将其与实时阴影混合。世界空间位置输入则用于计算实时阴影,因为它提供了物体在场景中的准确位置,以便与阴影映射进行比较。
该节点的输出是一个浮点值,表示混合后的阴影强度。这个值可以用于调制材质的颜色、透明度或其他属性,以实现阴影效果。例如,在简单的漫反射材质中,可以将阴影输出与基础颜色相乘,使得阴影区域变暗。在更复杂的材质中,阴影输出可能用于控制高光强度或反射率,以模拟更真实的光照行为。
需要注意的是,Main Light Shadow 节点仅适用于通用渲染管线。在高清渲染管线中,阴影处理方式不同,因此该节点不被支持。在 URP 中,节点的行为还受到项目设置中的阴影配置影响,例如阴影距离、ShadowMask 模式和混合参数。因此,在使用该节点时,开发者应确保场景和项目设置正确,以获得预期的阴影效果。
支持的渲染管线
- 通用渲染管线:Main Light Shadow 节点完全兼容 URP,并利用了 URP 的阴影管线和混合系统。在 URP 中,该节点可以访问实时阴影映射和 ShadowMask 数据,并根据场景设置进行混合。这使得它成为 URP 项目中处理主光源阴影的首选工具。
高清渲染管线不支持此节点:HDRP 使用不同的阴影和光照系统,包括更高级的阴影映射技术和光线追踪阴影。因此,Main Light Shadow 节点在 HDRP 中不可用。HDRP 用户应使用 HDRP 特定的阴影节点或着色器功能来实现类似效果。
端口
Main Light Shadow 节点包含多个输入和输出端口,每个端口都有特定的功能和绑定类型。理解这些端口的含义和用法是正确使用该节点的关键。以下将详细说明每个端口的作用,并提供使用示例。
| 名称 | 方向 | 类型 | 绑定 | 描述 |
|---|---|---|---|---|
| Lightmap UV | 输入 | Vector 2 | 无 | 输入光照贴图的 UV 坐标,用于采样 ShadowMask 阴影数据。如果未提供,节点可能使用默认的 UV 或无法正确混合 ShadowMask 阴影。 |
| Position WS | 输入 | Vector 3 | World Space | 输入世界空间的顶点位置信息,用于计算实时阴影。该位置应与渲染的物体表面点一致,以确保阴影映射正确采样。 |
| Out | 输出 | Float | 无 | 输出混合后的主光源阴影信息,范围从 0 到 1。0 表示完全阴影(无光照),1 表示完全光照(无阴影)。该输出可用于调制材质属性,如颜色或透明度。 |
Lightmap UV 输入端口
Lightmap UV 输入端口用于接收光照贴图的 UV 坐标,这些坐标用于采样 ShadowMask 阴影数据。光照贴图是预计算的光照和阴影信息,存储在纹理中,适用于静态物体。在 URP 中,ShadowMask 阴影是一种基于光照贴图的阴影技术,它允许静态物体使用高质量的烘焙阴影,而不需要实时计算。
- 功能说明:当提供 Lightmap UV 时,Main Light Shadow 节点会使用这些坐标来查找 ShadowMask 纹理中的阴影数据。这对于静态物体至关重要,因为它们依赖于光照贴图来表现阴影。如果未提供 Lightmap UV,节点可能无法正确混合 ShadowMask 阴影,导致阴影效果不完整或错误。
- 使用示例:在 Shader Graph 中,您可以通过 UV 节点或自定义计算来生成 Lightmap UV。通常,静态物体的光照贴图 UV 在导入模型时自动生成,并存储在模型的第二套 UV 通道中。您可以使用 Texture Coordinate 节点并选择 Lightmap 通道来获取这些 UV。
- 注意事项:如果您的场景中未使用 ShadowMask 阴影,或者物体是动态的,则 Lightmap UV 输入可能不是必需的。但在大多数情况下,提供正确的 Lightmap UV 可以确保阴影混合的正确性,尤其是在静态和动态物体共存的场景中。
Position WS 输入端口
Position WS 输入端口用于接收世界空间中的顶点位置信息。该位置用于实时阴影计算,因为实时阴影映射基于世界空间中的深度比较。节点使用这个位置来查询主光源的阴影映射纹理,确定该点是否处于阴影中。
- 功能说明:Position WS 应代表渲染物体表面的具体点位置。在顶点着色器阶段,这通常是顶点的世界位置;在片段着色器阶段,这可能是插值后的世界位置。使用片段级的世界位置可以提高阴影的精度,尤其是在曲面或细节丰富的物体上。
- 使用示例:在 Shader Graph 中,您可以使用 Position 节点并设置为 World Space 来获取 Position WS。然后,将其连接到 Main Light Shadow 节点的 Position WS 输入端口。对于高质量阴影,建议在片段着色器中使用世界位置,但这可能会增加计算开销。
- 注意事项:如果未提供 Position WS,节点可能无法计算实时阴影,导致阴影效果缺失。此外,位置信息应与阴影映射的坐标系一致,以避免阴影偏移或错误。在移动物体上,实时阴影会根据位置动态更新,因此确保位置输入准确至关重要。
Out 输出端口
Out 输出端口是节点的最终输出,提供一个浮点值,表示混合后的主光源阴影强度。这个值范围从 0 到 1,其中 0 表示该点完全处于阴影中(无主光源照射),1 表示该点完全被主光源照亮。
- 功能说明:输出值结合了实时阴影和 ShadowMask 阴影,并根据场景的阴影混合设置进行插值。例如,在阴影混合距离内,输出可能介于 0 和 1 之间,表示部分阴影。开发者可以使用这个值来调制材质的外观,如降低阴影区域的亮度或调整高光效果。
- 使用示例:将 Out 端口连接到材质的 Base Color 输入,并通过乘法节点将其与颜色值结合,可以实现基本的阴影变暗效果。例如,
Base Color * Shadow Output会使阴影区域变暗。您还可以使用该输出控制其他属性,如透明度或发射强度,以创建更复杂的效果。 - 注意事项:输出值是一个标量,因此它仅表示阴影的强度,而不包含颜色或方向信息。对于多光源阴影,Main Light Shadow 节点仅处理主光源(通常是场景中最亮的方向光)。如果需要其他光源的阴影,应使用额外的阴影节点或自定义计算。
端口绑定和类型
端口的绑定和类型决定了它们如何与其他节点交互。Main Light Shadow 节点的输入端口没有强制绑定,但建议根据功能需求提供正确的数据。输出端口是一个简单的浮点值,可以轻松连接到任何接受浮点输入的端口。
- Lightmap UV 端口:类型为 Vector 2,表示二维纹理坐标。它没有特定绑定,但应来自光照贴图 UV 源。
- Position WS 端口:类型为 Vector 3,绑定到世界空间。这意味着输入的位置数据应在世界坐标系中表示。
- Out 端口:类型为 Float,无绑定,可直接用于调制其他属性。
通过正确使用这些端口,开发者可以充分利用 Main Light Shadow 节点的功能,实现高质量的阴影效果。在下一部分中,我们将通过具体示例展示如何在实际项目中使用该节点。
使用方法
使用 Main Light Shadow 节点需要一定的设置步骤,包括配置输入数据、连接输出以及调整场景参数。以下将详细介绍如何在 URP Shader Graph 中正确使用该节点,并提供一个完整的示例。
基本设置
首先,在 Shader Graph 中创建一个新图形或打开现有图形。然后,从节点库中添加 Main Light Shadow 节点。通常,该节点位于 Light 类别下。添加后,您将看到其输入和输出端口。
- 步骤 1:提供 Position WS 输入。使用 Position 节点,将其空间设置为 World Space,然后连接到 Main Light Shadow 节点的 Position WS 端口。这确保了实时阴影的正确计算。
- 步骤 2:提供 Lightmap UV 输入(可选但推荐)。使用 Texture Coordinate 节点,将其通道设置为 Lightmap,然后连接到 Lightmap UV 端口。这对于静态物体的 ShadowMask 阴影至关重要。
- 步骤 3:使用 Out 输出。将 Out 端口连接到您的材质属性,例如 Base Color。您可能需要使用乘法或其他数学节点来调制阴影效果。
示例:创建基础阴影材质
以下是一个简单示例,演示如何使用 Main Light Shadow 节点创建一个具有阴影效果的漫反射材质。
- 创建新 Shader Graph:在 Unity 编辑器中,右键单击项目窗口,选择 Create > Shader Graph > URP > Lit Shader Graph。命名并打开该图形。
- 添加 Main Light Shadow 节点:在 Shader Graph 窗口中,右键单击空白区域,搜索 "Main Light Shadow" 并添加节点。
- 设置输入:添加 Position 节点(设置为 World Space)并连接到 Position WS 输入。添加 Texture Coordinate 节点(设置为 Lightmap)并连接到 Lightmap UV 输入。
- 连接输出:添加 Multiply 节点。将 Main Light Shadow 节点的 Out 输出连接到 Multiply 节点的 A 输入,将 Base Color 属性连接到 B 输入。然后将 Multiply 节点的输出连接到主节点的 Base Color 输入。
- 测试效果:在场景中创建一个材质,使用该 Shader Graph,并将其应用于一个物体。确保场景中有主光源(如方向光)并启用了阴影。调整光源位置和阴影设置以观察效果。
在这个示例中,阴影输出会调制基础颜色,使得阴影区域变暗。您可以通过调整光源或物体位置来验证实时阴影,并通过烘焙光照来测试 ShadowMask 阴影。
高级用法
对于更复杂的效果,Main Light Shadow 节点可以与其他节点结合使用。例如:
- 阴影颜色调整:使用 Color 节点和 Lerp 节点,根据阴影输出在阴影颜色和光照颜色之间插值。这可以实现彩色阴影或风格化效果。
- 阴影强度控制:添加一个浮点属性,用于缩放阴影输出。例如,
Shadow Output * Shadow Strength,其中 Shadow Strength 是一个可调参数,允许艺术家控制阴影的黑暗程度。 - 多通道阴影:将阴影输出用于其他光照计算,如高光或环境光遮蔽。例如,在高光计算中,减少阴影区域的高光强度以增强真实感。
场景配置
Main Light Shadow 节点的行为受项目设置中的阴影配置影响。在 Unity 编辑器中,转到 Edit > Project Settings > Graphics > URP Global Settings(或直接编辑 URP 资产),检查阴影相关设置:
- 阴影距离:控制实时阴影的渲染距离。超出此距离的物体不会投射实时阴影,可能依赖 ShadowMask。
- ShadowMask 模式:例如,ShadowMask 或 Distance Shadowmask。在 Distance Shadowmask 模式下,URP 会在一定距离内混合实时阴影和 ShadowMask 阴影。
- 混合参数:如阴影混合距离,控制实时阴影和烘焙阴影之间的过渡区域。
确保这些设置与您的项目需求匹配。例如,在开放世界游戏中,您可能设置较大的阴影距离和平滑的混合参数,以实现无缝的阴影过渡。
性能考虑
使用 Main Light Shadow 节点时,应注意性能影响:
- 实时阴影:依赖于阴影映射,可能增加 GPU 负载。尽量减少实时阴影的分辨率和距离,以优化性能。
- ShadowMask 阴影:基于光照贴图,对性能影响较小,但需要预计算和内存存储。确保光照贴图分辨率适中,避免过度占用内存。
- 混合计算:阴影混合在着色器中执行,增加片段着色器的计算量。在低端设备上,考虑简化混合逻辑或使用更高效的阴影技术。
通过遵循这些使用方法,您可以有效地集成 Main Light Shadow 节点到您的 URP 项目中,实现高质量且性能友好的阴影效果。
示例与效果展示
为了更好地理解 Main Light Shadow 节点的应用,本节将通过几个具体示例展示其在不同场景下的效果。每个示例将包括设置步骤、效果描述和可能的变体。
示例 1:基础漫反射阴影
这是最简单的应用场景,演示如何将主光源阴影应用于标准漫反射材质。
- 设置步骤:
- 在 Shader Graph 中,创建如上文所述的图形,其中 Main Light Shadow 输出与基础颜色相乘。
- 应用材质到一个立方体或球体,并放置在一个平面上。
- 添加一个方向光作为主光源,启用实时阴影。
- 效果描述:当物体移动时,实时阴影会动态更新。如果场景包含烘焙光照,ShadowMask 阴影将用于静态物体,并与实时阴影混合。例如,当物体靠近静态物体时,阴影会平滑过渡。
- 变体:尝试调整光源的阴影强度或颜色,观察阴影输出的变化。您还可以通过修改阴影混合距离来改变过渡效果。
示例 2:风格化阴影
在这个示例中,我们使用 Main Light Shadow 节点创建非真实感阴影,例如卡通风格或彩色阴影。
- 设置步骤:
- 在 Shader Graph 中,添加一个 Color 节点用于阴影颜色(例如,蓝色)。
- 使用 Lerp 节点,将基础颜色和阴影颜色作为输入,Main Light Shadow 输出作为插值因子。
- 连接 Lerp 输出到 Base Color。
- 效果描述:阴影区域显示为蓝色,而非简单的变暗。这可以用于艺术化渲染或特定游戏风格。
- 变体:尝试使用纹理采样或其他颜色逻辑来创建更复杂的效果,例如渐变阴影或图案阴影。
示例 3:动态阴影调制
这个示例展示如何根据阴影输出动态调整其他材质属性,如高光或透明度。
- 设置步骤:
- 在 Shader Graph 中,将 Main Light Shadow 输出连接到 Specular 输入。例如,使用乘法节点减少阴影区域的高光强度。
- Alternatively,将阴影输出用于 Alpha 控制,实现阴影区域的半透明效果。
- 效果描述:在阴影区域,物体表面变得不那么反光或部分透明,增强真实感或创建特殊效果。
- 变体:结合其他光照节点,如 Main Light 节点,来实现更复杂的光照模型。
示例 4:多光源阴影处理
虽然 Main Light Shadow 节点仅处理主光源阴影,但可以与其他技术结合来处理多光源阴影。
- 设置步骤:
- 使用 Additional Lights 节点获取其他光源信息,并手动计算阴影(例如,通过屏幕空间阴影或自定义阴影映射)。
- 将主光源阴影与其他阴影结合,例如取最小值或平均值,以模拟多光源阴影。
- 效果描述:物体在所有光源下都投射阴影,提供更真实的光照交互。
- 变体:在性能允许的情况下,使用 URP 的阴影堆栈或其他资产扩展多阴影支持。
通过这些示例,您可以看到 Main Light Shadow 节点的灵活性和强大功能。在实际项目中,根据需求调整设置和组合其他节点,可以实现各种阴影效果。
常见问题与解决方案
在使用 Main Light Shadow 节点时,可能会遇到一些问题。本节列出常见问题及其解决方案,帮助您快速排除故障。
问题 1:阴影不显示或显示不正确
- 可能原因:
- Position WS 输入不正确:如果位置数据不准确,实时阴影可能无法计算。
- Lightmap UV 缺失或错误:如果未提供 Lightmap UV,ShadowMask 阴影可能无法工作。
- 场景阴影设置错误:例如,阴影距离过小或 ShadowMask 未启用。
- 解决方案:
- 检查 Position WS 输入是否来自世界空间位置节点,并确保在片段着色器中使用以提高精度。
【Unity Shader Graph 使用与特效实现】专栏-直达 (欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,🙏)
前端构建产物里的 __esModule 是什么?一次讲清楚它的原理和作用
如果你经常翻构建后的代码,基本都会看到这样一行:
Object.defineProperty(exports, "__esModule", { value: true });
![]()
很多人第一次看到都会疑惑:
- 这是干嘛的?
- 能删吗?
- 不加会怎么样?
- 和 default 导出有什么关系?
这篇文章专门把这个现象讲清楚。
太长不看版
Object.defineProperty(exports, "__esModule", { value: true });
本质就是:
标记“这个 CommonJS 文件是从 ES Module 转译来的”,用于默认导出语义的互操作。
它不是功能代码,不是业务逻辑。
它只是模块系统演化过程中的一个兼容标志。
一、为什么会出现 __esModule?
根本原因只有一个:
ES Module 和 CommonJS 的语义不一样。
我们简单对比一下。
ES Module
export default function foo() {}
CommonJS
module.exports = function foo() {}
两者看起来都叫“默认导出”,但内部机制完全不同。
当构建工具(TypeScript / Babel / Webpack / Rspack 等)把 ESM 转成 CJS 时,语义必须“模拟”出来。
于是就变成:
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = foo;
关键问题来了:
如何区分“普通 CJS 模块”和“从 ESM 转过来的 CJS 模块”?
这就是 __esModule 存在的意义。
二、__esModule 到底做了什么?
它只是一个标记。
exports.__esModule = true
之所以用 Object.defineProperty,是为了:
- 不可枚举
- 更符合 Babel 的标准输出
- 避免污染遍历结果
本质就是:
告诉别人:这个模块原本是 ES Module。
仅此而已。
三、真正的核心:默认导出的互操作问题
来看一个经典场景。
1️⃣ 原始 ESM
export default function foo() {}
2️⃣ 被编译成 CJS
exports.default = foo;
3️⃣ 用 CommonJS 引入
const foo = require('./foo');
得到的其实是:
{
default: [Function: foo]
}
这就有问题了。
我们希望的是:
foo() // 直接调用
而不是:
foo.default()
于是构建工具会生成一个 helper:
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
逻辑是:
- 如果模块带有
__esModule标记 → 说明是 ESM 转的 → 直接用default - 如果没有 → 说明是普通 CJS → 包一层
{ default: obj }
这就是整个互操作的关键。
四、为什么不能只判断 default 属性?
因为普通 CJS 也可能写:
module.exports = {
default: something
}
这时你没法区分:
- 是 ESM 编译产物
- 还是普通对象刚好有个 default 字段
所以必须有一个“官方标记”。
__esModule 就成了事实标准。
五、什么时候会生成它?
只要发生:
ESM → CJS 转译
基本都会生成。
常见场景:
- TypeScript 编译为
module: commonjs - Babel preset-env 输出 CJS
- Webpack / Rspack 输出 target 为 node + CJS
- 老 Node 项目混用 import / require
如果你使用:
{
"type": "module"
}
并且输出原生 ESM
那就不会有 __esModule。
它只存在于“模块系统过渡时代”。
注意:它不是 JS 语言特性
非常重要的一点:
__esModule不是语言规范的一部分。
它是:
- Babel 约定
- 构建器约定
- 社区事实标准
是一种“工程层解决方案”。
换句话说:
它属于模块系统演化历史的一部分。
从更高层看:模块系统的过渡遗产
JavaScript 的模块系统经历了三代:
- 无模块(全局变量时代)
- CommonJS(Node 时代)
- ES Module(标准化)
但 Node 生态已经建立在 CJS 上。
所以必须有一个桥接层。
__esModule 就是这座桥的一块砖。
它存在的原因不是设计优雅,而是历史兼容。