普通视图
方大集团与泸州老窖集团签署战略合作协议
申通快递与宁德时代签署战略合作协议
科达液压与科上、汇川签约 开启废钢加工设备电液伺服系统领域合作
华为诺亚方舟实验室主任王云鹤离职
讯飞医疗2025年营收同比增长25%
小米SU7 Ultra新增暮光玫瑰车漆
我是如何构建一个 AI 原生量化系统的
Meta内容政策主管比克特离职,将赴哈佛大学任教
31省份中央育儿补贴补助资金揭晓:广东河南四川居前三
vivo秦飞:手机厂商早就能做智能体 但安全比速度更重要
Tauri 2 Linux 上 asset://localhost 访问返回 403 避坑指南
很多人在 Tauri v2(尤其是 Linux 系统)中使用 convertFileSrc() 或 asset://localhost 协议加载本地图片、视频、音频等资源时,经常遇到 403 Forbidden 错误。Windows/macOS 可能正常,Linux 却直接翻车。
本文把整个坑的来龙去脉、根本原因、glob 匹配规则彻底讲清楚,并给出最稳的配置方案,帮助大家一次性避坑。
一、问题现象
- 使用
convertFileSrc(fullPath)生成的 URL 在<img>、<video>、<audio>等标签中加载失败 - 浏览器控制台报 403
- 终端(Rust 侧)日志提示类似:
asset protocol not configured to allow the path: /home/user/.local/share/xxx/xxx.png - 尤其容易出现在 隐藏目录(以
.开头的目录)下:.local/share、.cache、.config等
二、根本原因:Tauri 的 Glob Scope + Linux 隐藏目录规则
Tauri v2 的 assetProtocol.scope 使用的是 Rust globset 库实现的 glob 模式来做安全校验。只有路径匹配 scope 里的 glob,才允许浏览器通过 asset 协议访问。
最坑的一点在于,Linux(Unix-like 系统)下:
通配符
*、?、**等 默认不会匹配以.开头的路径(dotfiles / dotdirs),除非你在 glob 模式里字面写出.。
所以即使你写了最宽松的 "**/*",它也进不了 .local、.cache 等隐藏目录,导致 403。
这不是 bug,而是 Tauri 为了安全故意设计的(和 Linux shell 的 ls * 默认不显示隐藏文件一样)。
三、Glob 模式最容易搞混的两个写法:**/ vs **/*
| glob 写法 | 含义 | 能匹配什么 | 在 assetProtocol.scope 里的实际效果 | 推荐程度 |
|---|---|---|---|---|
**/* |
递归匹配所有文件 | 文件(如 a.png、sub/b.mp4) |
✅ 强烈推荐 | ★★★★★ |
**/ |
递归匹配所有目录 | 纯目录路径(如 images/、sub/) |
❌ 几乎没用(scope 要的是文件路径) | ★☆☆☆☆ |
一句话总结:
-
**/*= “递归所有文件”(你 99% 的情况都需要这个) -
**/= “递归所有目录”(基本不要单独写在 scope 里)
正确写法是 你的路径/**/* 或直接 **/*
四、正确配置(一步到位)
1. 主配置(推荐同时加 Linux 专属配置)
src-tauri/tauri.conf.json(全局):
{
"app": {
"security": {
"csp": "default-src 'self' ipc: http://ipc.localhost; img-src 'self' asset: http://asset.localhost; video-src 'self' asset: http://asset.localhost; audio-src 'self' asset: http://asset.localhost; style-src 'self' 'unsafe-inline';",
"assetProtocol": {
"enable": true,
"scope": [
"**/*",
"**/.local/share/**/*",
"**/.cache/**/*",
"$CACHE/**",
"$CONFIG/**",
"$HOME/**"
]
}
}
}
}
src-tauri/tauri.linux.conf.json(Linux 专属,强烈建议):
{
"app": {
"security": {
"assetProtocol": {
"enable": true,
"scope": [
"**/*",
"**/.local/share/**/*",
"**/.cache/**/*",
"$CACHE/**",
"$CONFIG/**"
]
}
}
}
}
这样 Windows/macOS 不会被多余的 scope 影响。
2. 代码侧使用(不变)
import { convertFileSrc } from '@tauri-apps/api/core';
const assetUrl = convertFileSrc(absoluteFilePath);
五、操作流程
- 按上面修改配置文件
- (推荐)
cargo clean -
pnpm tauri dev(或npm run tauri dev)测试 - 还是 403?看终端日志,把报错里提示的路径对应的 glob 补进去
六、额外避坑小贴士
- 用 Tauri 内置变量
$CACHE、$CONFIG最香,自动处理平台差异 - 如果是用户通过
dialog.open()选择的路径,Tauri 会自动扩展 scope,但持久化路径仍需写进配置 - 打包进 bundle 的资源不需要 assetProtocol,走
frontendDist即可 - Rust 版本建议 ≥ 1.77,Tauri CLI 保持最新
总结:
Tauri 2 的 asset 403 坑,99% 是因为 Linux 下 glob 默认不匹配 . 开头的隐藏目录。只要把 **/* + **/.local/share/**/* + $CACHE/** 写全,问题基本秒解。
把这篇配置直接复制到你的项目里,基本不会再踩这个坑了。
希望这篇文章能帮到更多 Tauri 开发者少走弯路!
如果你还有其他 Tauri v2 的奇葩问题,欢迎继续留言~
三大运营商算力收入提升 Token服务逐渐成为经营主线
React 性能优化(下):useCallback 与 useTransition 实战
引言
在 React 应用性能优化中,useCallback 和 useTransition 是两个强大但常被误解的 Hook。本文将深入探讨它们的正确使用场景、常见陷阱以及实际代码示例,帮助你写出更高效的 React 应用。
useCallback:避免不必要的函数重建
核心原理
useCallback 返回一个记忆化的回调函数,只有在依赖项变化时才会重新创建。这对于避免子组件不必要的重新渲染至关重要。
基础用法
import React, { useState, useCallback } from 'react';
function ParentComponent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// ❌ 错误:每次渲染都会创建新函数
const handleClick = () => {
console.log('Count:', count);
};
// ✅ 正确:使用 useCallback 记忆化
const handleMemoizedClick = useCallback(() => {
console.log('Count:', count);
}, [count]);
return (
<div>
<button onClick={handleMemoizedClick}>点击</button>
<input value={text} onChange={e => setText(e.target.value)} />
</div>
);
}
配合 React.memo 使用
import React, { useState, useCallback, memo } from 'react';
const ChildComponent = memo(({ onIncrement, value }) => {
console.log('Child rendered');
return (
<button onClick={onIncrement}>
Count: {value}
</button>
);
});
function ParentComponent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// ✅ 只有 count 变化时,onIncrement 才会变化
const handleIncrement = useCallback(() => {
setCount(prev => prev + 1);
}, []);
return (
<div>
<ChildComponent onIncrement={handleIncrement} value={count} />
<input value={text} onChange={e => setText(e.target.value)} />
</div>
);
}
常见陷阱
// ❌ 陷阱 1:依赖项过多导致失去优化效果
const handler = useCallback(() => {
doSomething(a, b, c, d, e);
}, [a, b, c, d, e]); // 几乎每次都会重新创建
// ✅ 解决:只依赖真正需要的变量
const handler = useCallback(() => {
doSomething(a, b);
}, [a, b]);
// ❌ 陷阱 2:在 useCallback 内部使用非稳定引用
const handler = useCallback(() => {
config.doSomething(); // config 每次都是新对象
}, [config]);
// ✅ 解决:依赖稳定的值
const handler = useCallback(() => {
configRef.current.doSomething();
}, []);
useTransition:优化耗时更新
核心概念
useTransition 允许你将某些状态更新标记为"过渡"更新,让 UI 保持响应式,避免阻塞用户交互。
基础用法
import React, { useState, useTransition } from 'react';
function SearchComponent() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleSearch = (value) => {
setQuery(value);
// ✅ 将耗时的过滤操作标记为过渡更新
startTransition(() => {
const filtered = heavyFilter(value);
setResults(filtered);
});
};
return (
<div>
<input
value={query}
onChange={e => handleSearch(e.target.value)}
placeholder="搜索..."
/>
{isPending && <span>加载中...</span>}
<ul>
{results.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
function heavyFilter(query) {
// 模拟耗时操作
const data = largeDataset.filter(item =>
item.name.includes(query)
);
return data;
}
实际场景:标签切换
import React, { useState, useTransition } from 'react';
function TabComponent() {
const [activeTab, setActiveTab] = useState('posts');
const [isPending, startTransition] = useTransition();
const tabs = {
posts: <PostsList />,
comments: <CommentsList />,
analytics: <AnalyticsPanel />
};
const handleTabChange = (tab) => {
startTransition(() => {
setActiveTab(tab);
});
};
return (
<div>
<nav>
{Object.keys(tabs).map(tab => (
<button
key={tab}
onClick={() => handleTabChange(tab)}
disabled={isPending}
>
{tab}
</button>
))}
</nav>
{isPending && <div className="spinner">切换中...</div>}
<main>
{tabs[activeTab]}
</main>
</div>
);
}
与 Suspense 配合
import React, { useState, useTransition, Suspense } from 'react';
function Dashboard() {
const [activeView, setActiveView] = useState('overview');
const [isPending, startTransition] = useTransition();
const handleViewChange = (view) => {
startTransition(() => {
setActiveView(view);
});
};
return (
<div>
<TabNav onChange={handleViewChange} active={activeView} />
<Suspense fallback={<LoadingSkeleton />}>
<ViewContent view={activeView} />
</Suspense>
</div>
);
}
性能对比实测
// 未优化的版本
function UnoptimizedList({ items }) {
const [filter, setFilter] = useState('');
// 每次输入都会重新渲染整个列表
const filtered = items.filter(item =>
item.name.includes(filter)
);
return (
<div>
<input onChange={e => setFilter(e.target.value)} />
<List data={filtered} />
</div>
);
}
// 优化后的版本
function OptimizedList({ items }) {
const [filter, setFilter] = useState('');
const [displayItems, setDisplayItems] = useState(items);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const value = e.target.value;
setFilter(value);
startTransition(() => {
const filtered = items.filter(item =>
item.name.includes(value)
);
setDisplayItems(filtered);
});
};
return (
<div>
<input value={filter} onChange={handleChange} />
{isPending && <LoadingIndicator />}
<List data={displayItems} />
</div>
);
}
最佳实践总结
useCallback 使用指南
-
优先优化子组件:只有当函数作为 prop 传递给
React.memo组件时才需要 -
避免过早优化:不是所有函数都需要
useCallback - 注意依赖项:确保依赖项稳定且必要
- 配合 useMemo:复杂计算场景结合使用
useTransition 使用指南
- 识别耗时更新:列表过滤、大数据渲染、复杂计算
-
提供加载反馈:使用
isPending显示加载状态 - 区分优先级:用户输入立即响应,数据更新可延迟
- 避免滥用:简单更新不需要过渡
总结
useCallback 和 useTransition 是 React 性能优化的利器,但需要正确使用:
-
useCallback解决的是函数引用稳定性问题 -
useTransition解决的是更新优先级问题
记住:性能优化应该基于实际测量,而非猜测。使用 React DevTools Profiler 找出真正的瓶颈,再针对性地应用这些 Hook。
这 5 个 Elements 小技巧,真的能提高调试效率
大家好,我是小贺。
如果你是前端,浏览器开发者工具大概已经是每天都要打交道的老朋友了。
但问题也恰恰在这里。我相信很多人会打开 F12,却只会最基础的查看元素、改两行样式、看看报错。真正能让调试变轻松的功能,反而常常被忽略。
这篇不讲太多原理,直接分享 5 个我觉得非常实用的 Elements 面板小技巧。它们不复杂,但一旦用顺手,确实能帮你少走不少弯路。
1. 选中即消失?“强制状态”拯救你
你一定遇到过这种情况:想调试一个鼠标悬浮(hover)后才出现的二级菜单,结果鼠标刚一移过去,菜单就没了,根本来不及查看。
这时候不用和手速较劲。
在 Elements 面板里,右键点击触发菜单的元素,选择 Force state -> :hover。
这样元素就会被强制锁定在悬浮状态。除了 :hover,常见的 :active、:focus 这些状态也都能手动模拟。
很多“选中就消失”的交互问题,本质上都可以靠这个功能解决。
![]()
2. 只有高手才知道的“H 键大法”
有时候你只是想临时看一眼页面效果,比如:
- 这个 Banner 去掉之后页面会不会更干净?
- 这个弹窗先隐藏,下面的布局会不会乱?
- 这个模块不显示时,留白是不是太大?
这时其实不用手动改 display: none。
直接在 Elements 面板里选中元素,按一下键盘上的 H。
元素会立刻隐藏,再按一次又能恢复。
如果你想进一步测试,可以直接按 Delete 把节点删掉;删错了也不用慌,Ctrl + Z 在开发者工具里同样可以撤回。
这个功能特别适合快速试布局,不用来回改代码、刷新页面。
![]()
3. $0:Elements 与控制台的“传送门”
假设你已经在 Elements 面板里选中了一个很深层的 div,这时候想在 Console 里看一下它的 offsetHeight,或者直接调用它的方法。
如果还要自己手写一遍 document.querySelector(...),就太费劲了。
按 Esc 呼出底部控制台,直接输入 $0。
$0 就是你当前在 Elements 面板里选中的那个元素。
比如你可以直接这样用:
$0.offsetHeight;
$0.classList;
$0.getBoundingClientRect();
更方便的是,浏览器还会记住你的选择历史:$1 是上一次选中的元素,$2 是上上次。
调试复杂页面时,这个功能真的非常顺手。
![]()
4. 截图不用描边,自带“像素级”捕获
有时候你想把某个按钮、卡片、弹窗单独截出来发给设计师,或者临时拿去做文档、做演示。
这时候最省事的方法,不是手动截图框选,而是直接让浏览器截这个节点。
操作步骤很简单:
- 在
Elements面板里选中目标元素 - 按
Ctrl + Shift + P打开命令面板 - 输入
screenshot(截图) - 选择
Capture node screenshot
这样导出的通常是一个干净的 PNG,元素边缘也会比手动框选更完整。对前端、设计协作、写文档的人来说,这个功能都很好用。
![]()
彩蛋:一键开启“整页可编辑”模式
有时候你只是想临时改改页面上的文字,看看排版效果,又不想在 Elements 里逐个节点去双击编辑。
在 Console 里输入:
document.designMode = 'on';
执行之后,整个页面会进入一种近似“可编辑文档”的状态。很多文字内容都可以直接点进去修改、删除、试排版。
它当然不能代替真正改代码,但在演示想法、快速验证文案展示效果时,非常方便。
![]()
5. 元素找不到了?试试“滚动到视图”
页面一长,DOM 结构一复杂,经常会出现一种情况:
你在 Elements 面板里明明看到了某个节点,但一时找不到它到底对应页面上的哪一块。
这时可以直接在对应的 HTML 标签上右键,选择 Scroll into view(滚动到视图)。
浏览器会自动把页面滚动到这个元素所在的位置。
这个小功能看起来不显眼,但在排查长页面、复杂列表、深层组件时,能省下很多来回找位置的时间。
![]()
写在最后
开发者工具里真正有用的,很多都不是“高级功能”,而是这些平时不太起眼、但一旦掌握就会频繁用到的小能力。
它们不会让你一夜之间变成高手,但会让你少做很多重复劳动。
如果你现在就在电脑前,不妨打开浏览器按一次 F12,先试试 $0 和 H 键,基本马上就能体会到差别。
这里是《你真的会使用浏览器么?》系列的第一篇。如果大家喜欢我们后续继续更新。
ps:文章演示网站为-(小贺的博客)
富士康旗下富储科能即将发布储能新品
国内多家品牌金饰克价逼近1400元
做了一个AI聊天应用后,我决定试试这个状态管理库
AI应用前端最大的坑,不是LLM调用,而是状态管理
背景
最近做了个AI聊天应用,类似ChatGPT的那种。
本来想用Redux,毕竟老牌方案,结果被毒打了一遍。
Redux的痛
痛点1:状态类型爆炸
// AI聊天应用需要管理的状态
interface ChatState {
// 对话
messages: Message[];
// 流式响应
streamingText: string;
isStreaming: boolean;
// 工具调用
toolCalls: ToolCall[];
currentToolCall: ToolCall | null;
toolResults: Record<string, any>;
// 上下文
contextWindow: Message[];
longTermMemory: MemoryItem[];
// 用户意图
currentIntent: Intent | null;
intentHistory: Intent[];
// 执行状态
currentStep: number;
stepResults: Record<number, any>;
// 错误处理
errors: Error[];
retryQueue: RetryItem[];
// ...
}
这还只是一个聊天模块的状态。
痛点2:action type能写死你
// 光是状态更新action就能写30个
const ADD_MESSAGE = "chat/ADD_MESSAGE";
const UPDATE_STREAMING = "chat/UPDATE_STREAMING";
const APPEND_STREAMING = "chat/APPEND_STREAMING";
const START_TOOL_CALL = "chat/START_TOOL_CALL";
const COMPLETE_TOOL_CALL = "chat/COMPLETE_TOOL_CALL";
const UPDATE_CONTEXT = "chat/UPDATE_CONTEXT";
const ADD_INTENT = "chat/ADD_INTENT";
// ... 还有几十个
痛点3:跨组件同步难
// Chat组件
const messages = useSelector((s) => s.chat.messages);
// Status组件
const toolCalls = useSelector((s) => s.chat.toolCalls);
// 怎么保证两个组件状态一致?靠redux-thunk?middleware?
然后我用了easy-model
// 一个类搞定AI聊天全状态
class AIChatModel {
// 对话
messages: Message[] = [];
// 流式响应
streamingText = "";
isStreaming = false;
// 工具调用
toolCalls: ToolCall[] = [];
currentToolCall: ToolCall | null = null;
toolResults: Map<string, any> = new Map();
// 上下文
contextWindow: Message[] = [];
longTermMemory: MemoryItem[] = [];
// 用户意图
currentIntent: Intent | null = null;
intentHistory: Intent[] = [];
// 执行
currentStep = 0;
stepResults: Map<number, any> = new Map();
// 错误
errors: Error[] = [];
// 方法
@loader.load()
async sendMessage(content: string) {
this.messages.push({ role: "user", content });
this.isStreaming = true;
const response = await llm.streamChat(this.messages);
for await (const chunk of response) {
this.streamingText += chunk;
}
this.messages.push({ role: "assistant", content: this.streamingText });
this.streamingText = "";
this.isStreaming = false;
}
async executeToolCall(tool: Tool, params: any) {
this.currentToolCall = { tool, params, status: "running" };
this.toolCalls.push(this.currentToolCall);
const result = await tool.execute(params);
this.currentToolCall.status = "completed";
this.currentToolCall.result = result;
this.toolResults.set(tool.name, result);
this.currentToolCall = null;
}
}
一个类,200行代码搞定Redux 500行都搞不定的事。
还能解决什么问题?
1. 撤销重做,调试AI回复
const chat = useModel(AIChatModel, []);
const history = useModelHistory(chat);
// 用户想撤回AI的上一次回复?
history.back();
// 想重做?
history.forward();
2. 跨组件状态共享
// 聊天区域
function ChatArea() {
const chat = useModel(AIChatModel, ["main"]);
return <MessageList messages={chat.messages} />;
}
// 状态显示
function StatusPanel() {
const chat = useModel(AIChatModel, ["main"]);
return <StatusBadge isStreaming={chat.isStreaming} />;
}
// 工具调用面板
function ToolPanel() {
const chat = useModel(AIChatModel, ["main"]);
return <ToolList calls={chat.toolCalls} />;
}
// 三个组件,状态自动同步
3. 深度监听
// 监听任意状态变化
watch(chat, (keys, prev, next) => {
console.log(`状态变化: ${keys.join(".")}`, prev, "→", next);
// "messages.5.content" - 第6条消息内容变了
// "toolCalls.0.status" - 第一个工具调用状态变了
// "streamingText" - 流式文本更新了
});
对比其他方案
| 特性 | Redux | Zustand | MobX | easy-model |
|---|---|---|---|---|
| 类模型 | ❌ | ❌ | ✅ | ✅ |
| 无装饰器 | ✅ | ✅ | ❌ | ✅ |
| 依赖注入 | ❌ | ❌ | ❌ | ✅ |
| 撤销重做 | ❌ | ❌ | ❌ | ✅ |
| 深度监听 | ❌ | ⚠️ | ✅ | ✅ |
| TypeScript友好 | ⚠️ | ✅ | ⚠️ | ✅ |
结论
AI应用前端,状态管理选easy-model就对了。
GitHub: github.com/ZYF93/easy-…
npm: pnpm add @e7w/easy-model
做AI应用前端,状态管理别再踩坑了,点个⭐️!
详解github workflows流
Workflows允许你在 GitHub(文章以此为例) 仓库中自动化构建、测试、部署等软件开发流程
1. 什么是Workflows
- GitHub Actions 是 GitHub 提供的 CI/CD(持续集成/持续部署)平台,可以自动化执行软件开发工作流。基本上代码管理都支持actions,百用不赖
-
Workflow 是 Actions 中的一个可配置的自动化过程,由一个或多个 job 组成,由特定事件触发。每个 workflow 是一个独立的 YAML 文件,存放在仓库的
.github/workflows目录下。
2. 基本概念
| 概念 | 说明 |
|---|---|
| Workflow | 一个自动化流程,由一个 YAML 文件定义。 |
| Event | 触发 workflow 运行的特定活动,如 push、pull_request、schedule 等。 |
| Job | workflow 中的一个任务,由多个 step 组成。一个 workflow 可以包含多个 job,它们可以并行或串行运行。 |
| Step | job 中的单个任务,可以是 shell 命令或一个 action。 |
| Action | 可复用的最小单元,可以封装常用操作(如检出代码、设置 Node.js 环境等)。 |
| Runner | 运行 workflow 的服务器。GitHub 提供托管运行器(如 ubuntu-latest、windows-latest),也可自托管。 |
3. 简单示例
在仓库根目录下创建 .github/workflows 目录,然后放入一个或多个 .yml 或 .yaml 文件
实例如下
name: CI # 名称
on: # 触发条件
push:
branches: [ main ]
jobs: # 定义作业
build:
runs-on: ubuntu-latest # 运行环境
steps:
- name: Checkout code
uses: actions/checkout@v4 # 使用官方 checkout action
- name: Run a one-line script
run: echo Hello, world!
4. 触发事件 (on)
on 字段定义 workflow 何时运行。可以是单个事件、事件列表,或带有条件的事件。
4.1 基本事件
-
push代码推送时触发 -
pull_requestPR 相关事件 -
workflow_dispatch手动触发(需要在 GitHub 界面点击按钮) -
schedule定时触发(cron) - 其他事件:如
release、issues、watch、fork见 👉🏻 详细文档地址
下面是详细的事件类型,贯穿仓库管理中的各个过程和节点。
4.2 多事件与条件过滤
可以组合多个事件,并利用 branches、paths、tags 等过滤器。
on:
push:
branches:
- main
pull_request:
branches:
- main
paths-ignore:
- 'docs/**'
5. Jobs 与 Steps
Jobs: 定义一个任务
Steps: 每个 step 可以是一个 run(执行 shell 命令)或 uses(引用一个 action)。
jobs:
job1:
runs-on: ubuntu-latest # 指定运行器镜像。常用值:`ubuntu-latest`, `windows-latest`, `macos-latest`,也可以指定版本如 `ubuntu-22.04`。
steps:
- name: Step 1 # 可选,显示在日志中,推荐使用。
if: github.ref == 'refs/heads/main' # 使用 `if` 条件,支持表达式。
run: echo "Step 1"
with:
node-version: '22'
job2:
runs-on: windows-latest
needs: job1 # job2 在 job1 完成后运行;(定义依赖关系,默认所有 job 并行运行,加 `needs` 后变为串行)
steps:
- name: Step 2
run: echo "Step 2"
6. Actions 的使用
Actions 是可复用的代码单元,可以从 GitHub Marketplace 获取或自定义。
6.1 使用官方或第三方 Action
格式:{owner}/{repo}@{ref} 或 {owner}/{repo}/{path}@{ref}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: my-artifact
path: dist/
6.2 自定义 Action
可以创建 Docker 容器 Action 或 JavaScript Action,放在仓库中本地引用:
- name: My custom action
uses: ./.github/actions/my-action
7. 环境变量与 Secrets
7.1 环境变量
可以在 workflow、job 或 step 级别设置环境变量。
env:
NODE_ENV: production
jobs:
build:
env:
API_URL: https://api.example.com
steps:
- name: Use env
run: echo ${{ env.API_URL }}
7.2 Secrets
敏感信息如密码、token 应存储在 GitHub 仓库的 Settings → Secrets and variables → Actions 中,然后在 workflow 中通过 ${{ secrets.SECRET_NAME }} 引用。
- name: Deploy
run: npm run deploy -- --token ${{ secrets.DEPLOY_TOKEN }}
注意:Secrets 不会出现在日志中,但需小心避免将 secret 作为命令行参数输出(如
echo ${{ secrets.TOKEN }}会暴露)。
8. 矩阵策略 (Matrix)
矩阵允许你使用变量组合并行运行多个 job,常用于测试多个操作系统、Node.js 版本等。
yaml
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [16, 18, 20]
fail-fast: false # 是否在某个组合失败时取消所有
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
- matrix:定义变量组合。
- include/exclude:可以添加或排除特定组合。
9. 缓存依赖
通过 actions/cache 可以缓存依赖,加速构建。
- name: Cache npm
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
更常见的是使用针对特定生态的缓存 action,如 actions/setup-node 已内置缓存:
- uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
10. 工件 (Artifacts) 与部署
10.1 上传/下载工件
可以在 job 间传递文件。
- name: Upload build
uses: actions/upload-artifact@v4
with:
name: build
path: dist/
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: dist/
10.2 部署到 GitHub Pages
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dist
GITHUB_TOKEN 是 GitHub 自动提供的临时 token,具有仓库写入权限。
11. 条件判断与上下文
11.1 条件执行
使用 if 条件,支持表达式。
- name: Only on main branch
if: github.ref == 'refs/heads/main'
run: echo "Deploying..."
11.2 上下文
GitHub Actions 提供丰富的上下文变量,如 github、env、secrets、matrix 等。
- name: Print event name
run: echo ${{ github.event_name }}
常用上下文:
-
github:包含仓库信息、触发者等。 -
runner:运行器信息。 -
env:环境变量。 -
secrets:仓库机密。 -
steps:上一步的输出。
12. 工作流嵌套与复用
12.1 调用其他 workflow
使用 workflow_call 事件,可以在一个 workflow 中调用另一个 workflow。
被调用的 workflow 需定义 on: workflow_call,并可定义输入和输出。
# .github/workflows/build.yml
name: Build
on:
workflow_call:
inputs:
node-version:
required: true
type: string
secrets:
npm_token:
required: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- run: npm ci
- run: npm run build
调用方:
name: Main Workflow
on: push
jobs:
call-build:
uses: ./.github/workflows/build.yml
with:
node-version: '18'
secrets:
npm_token: ${{ secrets.NPM_TOKEN }}
13. 运行器组与自托管运行器
如果不想使用 GitHub 托管运行器,可以添加自托管运行器(Self-hosted runner)。
jobs:
build:
runs-on: self-hosted
可以为自托管运行器指定标签,如 runs-on: [self-hosted, linux]。
14. 调试与日志
- 启用 step 调试:在 workflow 中设置
ACTIONS_STEP_DEBUGsecret 为true,会输出更详细的日志。 - 启用 runner 调试:设置
ACTIONS_RUNNER_DEBUGsecret 为true,会输出 runner 级别的调试信息。 - 手动重试:在 GitHub Actions 页面,可以重新运行失败的 job。
15. 安全最佳实践
-
最小权限原则:使用
permissions字段限制 workflow 的 token 权限。permissions: contents: read issues: write -
避免在日志中打印 secrets。
-
使用环境 (Environment) :对部署等敏感操作,可配置环境并设置保护规则。
environment: production -
审查第三方 Action:优先使用官方或经过验证的 Action,固定版本 tag 或 commit SHA。
参考资料: