使用 grunt-markdown 生成 HTML 并注入 Prism.js
解读
在国内前端工程化面试中,**“用 Grunt 把 Markdown 转成 HTML 并自动注入 Prism.js 代码高亮”常被用来考察候选人对“老项目维护 + 构建链整合”**的能力。
面试官真正想听的不是“能跑起来”,而是:
- 你能否在不破坏原有 Gruntfile 结构的前提下,最小侵入地插入一个新任务;
- 你能否精准控制生成 HTML 的 head 与 body 片段,让 Prism 的 CSS/JS 只在含有
<pre><code>的页面出现; - 你能否解决国内网络问题(Prism CDN 不稳定)并把依赖锁版本、本地化;
- 你能否保证开发体验,即文件监听 + Livereload 仍能实时预览高亮效果;
- 你能否输出可交付物,让测试、运维同学一键
grunt dist拿到带高亮的静态文档。
知识点
- grunt-markdown 的
template/templateContext选项,支持用 Lodash 模板复写整页骨架 - grunt-contrib-copy + grunt-contrib-concat,把 Prism 的 CSS/JS 从 node_modules 拷到 dist 并合并版本号,避免国内 CDN 抖动
- grunt-contrib-uglify 与 cssmin,对 Prism 进行二次压缩,减少 15% 体积
- grunt-contrib-watch 的
spawn: false模式,复用进程,让 Markdown 修改后 200 ms 内刷新浏览器 - grunt-banner 在生成的 HTML 里追加公司版权注释,满足国内甲方合规审计
- Gruntfile.js 的 task 依赖图,必须让
markdown:docs→inject:prism→usemin→htmlmin串行,防止 MD5 文件名错乱 - Prism 插件体系:
line-numbers、copy-to-clipboard、toolbar,面试时要能说出如何只注册需要的语言包,避免把 2 MB 全量打包进去 - Node 版本锁:
.nvmrc写死 14.21.3,对齐淘宝镜像源,否则node-sass与prismjs在 M1 机器上会编译失败
答案
- 安装依赖(淘宝源加速)
npm i -D grunt-markdown grunt-contrib-copy grunt-contrib-concat grunt-contrib-cssmin grunt-contrib-uglify grunt-contrib-watch grunt-banner
npm i -D prismjs@1.29.0 # 锁版本
- 目录约定(国内团队惯用)
docs-src/*.md
docs-src/templates/page.html // Lodash 模板
static/prism/ // 构建后存放本地化 Prism 资源
dist/docs/ // 最终交付物
- Gruntfile 核心片段(可直接粘贴进老项目)
module.exports = function(grunt) {
grunt.initConfig({
// 1. 把 Markdown 转成 HTML
markdown: {
options: {
template: 'docs-src/templates/page.html',
templateContext: {
prismCss: '<link rel="stylesheet" href="../static/prism/prism.min.css">',
prismJs: '<script src="../static/prism/prism.min.js"></script>'
},
markdownOptions: {
gfm: true,
highlight: function(code, lang) {
// 关键:让 Prism 在 Node 端就高亮,减少浏览器端闪烁
const prism = require('prismjs');
const loadLanguages = require('prismjs/components/');
loadLanguages([lang || 'markup']);
return prism.highlight(code, prism.languages[lang], lang);
}
}
},
docs: {
files: [{
expand: true,
cwd: 'docs-src',
src: '*.md',
dest: 'dist/docs',
ext: '.html'
}]
}
},
// 2. 本地化 Prism 资源
copy: {
prism: {
files: [
{src: 'node_modules/prismjs/themes/prism-tomorrow.css',
dest: 'static/prism/prism.css'},
{src: 'node_modules/prismjs/prism.js',
dest: 'static/prism/prism.js'},
{expand: true, cwd: 'node_modules/prismjs/components',
src: ['prism-javascript.min.js', 'prism-css.min.js', 'prism-markup.min.js'],
dest: 'static/prism/components'}
]
}
},
concat: {
prismCss: {
src: ['static/prism/prism.css'],
dest: 'static/prism/prism.min.css'
},
prismJs: {
src: ['static/prism/prism.js', 'static/prism/components/*.js'],
dest: 'static/prism/prism.min.js'
}
},
cssmin: {
prism: { src: 'static/prism/prism.min.css', dest: 'static/prism/prism.min.css' }
},
uglify: {
prism: { src: 'static/prism/prism.min.js', dest: 'static/prism/prism.min.js' }
},
// 3. 注入公司版权
usebanner: {
dist: {
options: { banner: '<!-- Copyright © <%= grunt.template.today("yyyy") %> 公司名,All Rights Reserved. -->' },
files: { src: 'dist/docs/*.html' }
}
},
// 4. 监听
watch: {
md: {
files: ['docs-src/**/*.md', 'docs-src/templates/*.html'],
tasks: ['markdown:docs', 'usebanner'],
options: { livereload: 1337, spawn: false }
}
}
});
grunt.loadNpmTasks('grunt-markdown');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-cssmin');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-banner');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.registerTask('prism', ['copy:prism', 'concat', 'cssmin', 'uglify']);
grunt.registerTask('docs', ['prism', 'markdown:docs', 'usebanner']);
grunt.registerTask('default', ['docs', 'watch']);
};
- 模板文件
docs-src/templates/page.html(只展示关键片段)
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title><%= title %></title>
<%= prismCss %>
</head>
<body>
<div class="markdown-body"><%= content %></div>
<%= prismJs %>
</body>
</html>
- 一键交付
grunt docs // 生成带高亮的静态文档
grunt default // 开发模式,支持 Livereload
拓展思考
-
如果文档站点后续要迁移到 Vite 或 Webpack 5,如何复用已有的 Prism 语言包列表并避免双份构建?
思路:把static/prism/components/*.js改成 ES Module,用import.meta.glob动态加载,Grunt 与 Vite 共用同一份语言包 JSON 索引,实现无痛迁移。 -
当 Markdown 里出现自定义容器(::: warning)时,Grunt 生态没有现成插件,可手写一个 30 行的 Grunt 插件:
- 用
markdown-it-container在 Node 侧解析; - 把解析结果缓存到
grunt.config('__mdTokens'); - 在
templateContext里作为变量注入,实现警告框高亮。
- 用
-
对于国内移动端场景,Prism 的
line-numbers插件在 UC 浏览器下错位,可关闭该插件并改用 postcss-write-svg 生成1 px 竖线背景图,兼容老旧内核。