Astro 博客里 Expressive Code 的配置
- Date / 日期
- Updated / 更新
- 2026.10.12
- Reading / 阅读
- 9 Min
- Words / 字数
- 1742
- Tags / 标签
- #Astro#Expressive Code#代码高亮#前端开发
Astro 博客里 Expressive Code 的配置
用在哪、怎么挂上去
博客的代码高亮交给 Expressive Code。它在 Shiki(VS Code 同款高亮引擎)之上加了编辑器/终端窗口框架、代码块标题、行号、行标记这些东西,正好覆盖写技术笔记时的需求,所以没有自己拼高亮方案。
它在 astro.config.mjs 里作为一个 Astro 集成挂上去:
import astroExpressiveCode from 'astro-expressive-code'
export default defineConfig({ integrations: [ astroExpressiveCode({ // ... }), ],})Tailwind 走的是另一条路,它现在是 Vite 插件(@tailwindcss/vite),和 Expressive Code 的集成互不干扰:
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({ vite: { plugins: [tailwindcss()], },})样式内联,而不是单独发一个 CSS 文件
有一个看起来不重要的开关:
astroExpressiveCode({ emitExternalStylesheet: false,})默认情况下 Expressive Code 会把代码块样式生成成独立的 CSS 文件。关掉它会把样式内联进首个代码块,省一个请求。
但真正的原因不是性能,是页面过渡。站点用了 <ClientRouter /> 做站内切换,如果样式在外部文件里,切换瞬间会先渲染出一块没有样式的代码,再”闪”一下变成正常的。内联之后这个问题就没了。
四套主题,两两互斥
themes: [ 'one-light', 'one-dark-pro', 'github-light-high-contrast', 'github-dark-high-contrast',],普通和高对比各两套。高对比那两套是给视力不佳的读者准备的,它会把注释和标点的对比也一起拉满,所以配色风格和普通主题明显不同。这一点是刻意的,不是没调好。
站点用 <html data-theme="light|dark"> 手动切明暗,所以关掉媒体查询自动切换:
useDarkModeMediaQuery: false,四套主题需要互斥,否则会同时命中。用 themeCssSelector 给每套主题算出一个选择器:
themeCssSelector: (theme) => { const mode = theme.type === 'dark' ? 'dark' : 'light' const highContrast = String(theme.name ?? '').includes('high-contrast') return highContrast ? `[data-theme='${mode}'][data-contrast='high']` : `[data-theme='${mode}']:not([data-contrast='high'])`},普通主题排除掉 data-contrast='high' 的情况,高对比主题只在它存在时生效。站点的”高对比”开关就是往 <html> 上写这个属性,代码块跟着一起切。
几处样式覆盖
styleOverrides: { borderRadius: '2px', frames: { editorActiveTabForeground: 'var(--c-muted)', terminalTitlebarForeground: 'var(--c-muted)', }, codeFontFamily: "'JetBrainsMono Nerd Font', 'JetBrains Mono', 'Fira Code', ui-monospace, monospace", textMarkers: { markHue: '32', borderOpacity: '50%', },},圆角收到 2px:默认皮肤(莱茵生命)整体没有圆角,代码块留着默认的圆角会显得格格不入。
标题文字色指向 var(--c-muted):主题自带的标题色接近纯黑或纯白,和站点其他地方对不上。指向 CSS 变量的好处是高对比模式下自动跟着走,不用再写特例,那个变量在高对比下本身就是纯黑/纯白。
markHue 从 310 改成 32:行标记的底色原本是品红(310),跟站点的杏金底色打架。挪到强调色的色相上就顺了。borderOpacity 是给标记加一圈浅边,让它在深色背景下也看得清边界。
字体:JetBrainsMono Nerd Font 在前,是为了让代码块里的 Nerd Font 图标能正常显示;后面两个是给没装 Nerd Font 的机器兜底。
行号:默认开,shell 类关掉
defaultProps: { showLineNumbers: true, overridesByLang: { 'bash,sh,zsh,shell,console,terminal,fish': { showLineNumbers: false }, },},默认开行号是因为笔记里的代码基本都需要指着某一行说话。但终端命令不一样:那几行是”照着敲一遍”用的,行号既没用又占宽度,所以按语言关掉。
另外还有一个语言别名,用来兼容文章里写法不规范的围栏:
shiki: { langAlias: { Julia: 'julia' },},自己写的两个插件
装上 Expressive Code 之后还有两件事它默认不管,于是各写了一个插件。
没写标题的时候,显示语言名
代码块可以用 title="hello.js" 指定标题,但不写的时候头部是空的,非终端框架甚至连整条头部都不显示,一篇文章里有的块有头、有的没头,看起来很乱。
这个插件的作用是:没有标题的代码块,把语言名补到头部去(Julia 显示成 Julia,py 显示成 Python)。
一开始我想直接用 defaultProps 塞一个默认标题,但那样不行。Expressive Code 的 frames 插件本身有个能力:没写 title 时,会从代码前 4 行的注释里抽文件名当标题(比如代码开头写 // hello.js)。提前塞默认值会把这个功能顶掉。
所以插件挂在 postprocessRenderedBlock 钩子上,它在标题(包括自动抽出来的那个)已经定下来之后才跑。这时候还没有标题的,才是真正该显示语言名的那些。
代价是插件依赖了 Expressive Code 的几个类名(frame、has-title、header、title),这些属于它对外公开的样式配置项,相对稳定,将来大版本升级时留个心就行。
把「自动换行」做成运行时开关
Expressive Code 的换行(代码块加 wrap)本来是构建期决定的:加了就在输出里写死,没加就没有。而这个博客想要的是一个能随时切的开关。
麻烦在于悬挂缩进,换行之后的续行要对齐到原来那一行的缩进位置,靠的是 --ecIndent 这个内联变量。Expressive Code 里负责这件事的两条规则是常驻的:
.expressive-code .ec-line .code { padding-inline-start: calc(var(--ecIndent, 0ch) + var(--ec-codePadInl) - var(--ecGtrBrdWd)); text-indent: calc(var(--ecIndent, 0ch) * -1);}--ecIndent 缺省是 0ch,两条规则正好抵消,所以平时看不出区别。既然如此,只要提前把 --ecIndent 写好,打开换行时就只剩 white-space 和 overflow-wrap 要切,不需要运行时脚本,也不会有开关一按就重新布局的闪烁。
缩进长度没有自己去解析源码,而是读 Expressive Code 自己渲染出来的 <span class="indent">(行首空白原样放在里面)。这样跟它的渲染结果永远一致,不会因为它改了渲染方式而对不上。
写法速查
代码块的通栏语法如下,本站目前只用到 title,其余留着备用。
| 写法 | 效果 |
|---|---|
```js | 指定语言,同时决定头部显示的语言名 |
```js title="hello.js" | 自定义标题(会覆盖语言名) |
```js {2,4-6} | 高亮指定行 |
```js ins={2} del={4-5} | 插入/删除标记 |
```js "return" /data/ | 用字符串或正则标记行内文本 |
```js frame="terminal" | 换成终端窗口样式(code / none 同理) |
```js showLineNumbers=false | 单个块关掉行号 |
```js wrap | 自动换行(配合上面那个运行时开关) |
几个例子:
```js title="hello.js" {2} ins={3}function hello(name) { console.log(`Hello, ${name}!`)}```.astro 和 .mdx 里还可以用 <Code> 组件渲染字符串:
---import { Code } from 'astro-expressive-code'const codeString = `console.log('Hello!')`---
<Code code={codeString} lang="javascript" />一个顺带的坑
上面提到的”从首行注释抽标题”这个功能,和语言标识插件是相冲的。如果哪天发现某个代码块的头部长得不对,先看它的前几行有没有类似 // 文件名 的注释,那会被 frames 插件当成标题,语言标识就自动让位了。

Comment评论