
用 Gatsby 的 JavaScript 转换机制解析 Markdown 文章以 using-javascript-transforms 示例中的 First Post 为例【免费下载链接】gatsbyReact-based framework with performance, scalability, and security built in.项目地址: https://gitcode.com/gh_mirrors/ga/gatsby导读在 Gatsby 项目中Markdown 文件不只是正文 Frontmatter的静态文本而是可以被完整接入数据层GraphQL、构建流水线与模板渲染的数据源。本文以仓库examples/using-javascript-transforms示例中的第一篇文章 index.md 为贯穿全文的样本逐字段拆解其 Frontmatter 的语义并沿着gatsby-source-filesystem→gatsby-transformer-remark→gatsby-node.js→ GraphQL 模板查询的完整链路讲解 Gatsby 如何把一篇 Markdown 文章变成可访问的/a-first-post/路由。读完本文你将掌握在 Gatsby 中通过 JavaScript 代码控制 Markdown 文章路由、布局与元数据输出的完整实战方案。一、示例背景两种根数据类型using-javascript-transforms是一个刻意展示用 JavaScript 控制数据转换的 Gatsby 示例站点其 README.md 明确指出该示例使用了两种根数据类型root data types基于 Markdown 的路由例如/a-first-post/数据文件就是src/articles/2017-01-22-a-first-post/index.md即本文的关联文档基于 JavaScript 的路由例如src/articles/2017-03-09-choropleth-on-d3v4/index.js它用export const frontmatter {...}的方式导出与 Markdown Frontmatter 完全同构的元数据。这两种数据源最终汇入同一条处理流水线。README 特别提醒这里的JavaScript 路由不是指src/templates/*或src/components/*中的 React 组件而是指以 JavaScript 文件本身作为数据载体配合gatsby-transformer-javascript-frontmatter插件把frontmatter导出解析成 GraphQL 节点。此外该示例刻意不使用src/pages目录。README 对此的解释是Gatsby 默认会对src/pages下的任何 JavaScript 文件自动调用createPage而本示例放弃该默认行为改为在gatsby-node.js中全手动创建页面以获得对页面创建过程的完全控制——这正是理解后面createPages代码的前提。二、逐字段拆解First Post 的 Frontmatter 数据模型关联文档 2017-01-22-a-first-post/index.md 的 Frontmatter 是整条数据链路的契约全文如下--- title: First Post About First Post written: 2017-01-22 updated: 2017-03-04 layoutType: post path: /a-first-post/ category: Beginnings description: From humble beginnings to... space? ---每个字段都在下游被真实消费可以对照源码逐一验证Frontmatter 字段示例值消费位置源码证据作用titleFirst Post About First PostBlogPostChrome/index.js 的 GraphQL fragment文章标题最终写入title标签written2017-01-22PostPublished/index.js、HelmetBlock/index.js原始发布日期经moment格式化为D MMM YYYY展示并写入og:article:published_timeupdated2017-03-04同上更新时间为null时只显示 published否则显示 originally published … and updated …layoutTypepostgatsby-node.js 的createPages路由分派开关post用博客模板page用内页模板path/a-first-post/同上页面最终 URL直接作为createPage的pathcategoryBeginningsHelmetBlock/index.js文章分类写入og:article:tag与 Twitter 卡片标签descriptionFrom humble beginnings to... space?同上页面描述写入meta namedescription与og:description值得注意的设计是字段名刻意与 JavaScript 路由保持一致。对比src/articles/2017-03-09-choropleth-on-d3v4/index.js中导出的 frontmatter 对象export const frontmatter { title: Choropleth on d3v4, written: 2017-03-09, updated: 2017-04-28, layoutType: post, path: /choropleth-on-d3v4/, category: data science, description: Things about the choropleth., }字段结构完全同构因此下面的 GraphQL fragment 可以用一份定义同时覆盖两种节点类型见 BlogPostChrome/index.jsfragment MarkdownBlogPost_frontmatter on MarkdownRemark { frontmatter { title path layoutType written updated category description } } fragment JSBlogPost_frontmatter on JavascriptFrontmatter { frontmatter { title path layoutType written updated category description } }这种一种数据模型、两种载体.md 与 .js的设计正是本示例名为 using-javascript-transforms 的精髓只要数据形状一致Markdown 和 JavaScript 页面就能共享同一套渲染组件与样式。三、数据链路第一步文件如何变成 GraphQL 节点Gatsby 的数据流水线从 gatsby-config.js 开始plugins: [ { resolve: gatsby-source-filesystem, options: { name: pages, path: ${__dirname}/src/mainPages/ }, }, { resolve: gatsby-source-filesystem, options: { name: articles, path: ${__dirname}/src/articles/ }, }, gatsby-transformer-javascript-frontmatter, { resolve: gatsby-transformer-remark, options: { plugins: [gatsby-remark-prismjs] }, }, gatsby-plugin-sass, ],对本文的 Markdown 数据源来说关键角色有三个gatsby-source-filesystem把src/articles/以及src/mainPages/下的文件注册为File节点gatsby-transformer-remark把File节点转换为MarkdownRemark节点解析正文含 Frontmatter并挂载gatsby-remark-prismjs实现代码高亮package.json中声明了prismjs依赖gatsby-transformer-javascript-frontmatter对应地把index.js等 JavaScript 文件的export const frontmatter解析为JavascriptFrontmatter节点——README 中称其为jsFrontmattertransformer。转换完成后index.md的标题、日期、分类等 Frontmatter 字段就以markdownRemark.frontmatter的形式暴露给 GraphQL 查询。四、构建期逻辑slug 生成与页面创建4.1onCreateNode自动推导 slug在 gatsby-node.js 中示例先用onCreateNode为两类节点统一生成 slugexports.onCreateNode ({ node, actions, getNode }) { const { createNodeField } actions let slug if ( node.internal.type MarkdownRemark || node.internal.type JavascriptFrontmatter ) { const fileNode getNode(node.parent) const parsedFilePath path.parse(fileNode.relativePath) if (parsedFilePath.name ! index parsedFilePath.dir ! ) { slug /${parsedFilePath.dir}/${parsedFilePath.name}/ } else if (parsedFilePath.dir ) { slug /${parsedFilePath.name}/ } else { slug /${parsedFilePath.dir}/ } createNodeField({ node, name: slug, value: slug }) } }对本文的index.md位于目录2017-01-22-a-first-post下由于文件名恰好是index走的是第三个分支slug 推导为/2017-01-22-a-first-post/。这个由文件路径推导出的 slug 与 Frontmatter 中的path并存slug 用于 GraphQL 查询定位节点path用于决定页面最终 URL见下文模板查询中的query($slug: String!)。4.2createPages按layoutType分派模板createPages通过一次 GraphQL 查询同时拉取两类节点allMarkdownRemark { edges { node { frontmatter { layoutType path } fields { slug } } } } allJavascriptFrontmatter { edges { node { fileAbsolutePath frontmatter { layoutType path } fields { slug } } } }随后对每个 Markdown 节点按layoutType分支创建页面result.data.allMarkdownRemark.edges.forEach(edge { let { frontmatter } edge.node if (frontmatter.layoutType post) { createPage({ path: frontmatter.path, // required component: mdBlogPost, context: { slug: edge.node.fields.slug }, }) } else if (frontmatter.layoutType page) { createPage({ path: frontmatter.path, // required component: mdInsetPage, context: { slug: edge.node.fields.slug }, }) } })对于layoutType: post的 First Post实际效果就是以frontmatter.path/a-first-post/为 URL以src/templates/mdBlogPost.js为页面组件并把推导出的 slug 通过context传入模板查询。JavaScript 路由分支则完全不同——它没有模板而是直接把源文件本身当作组件component: path.resolve(edge.node.fileAbsolutePath),gatsby-node.js 中的注释解释得很清楚模板存在的意义是把非 React 数据转换为 React而jsFrontmatter捕获的 JavaScript 文件本身已是 React 组件因此直接 require 即可代价是每个 JavaScript 页面需要自己携带路由结构示例通过高阶组件src/components/BlogPostChrome与src/components/Layouts/*和 GraphQL fragment 来降低这种重复。五、模板查询query($slug: String!)与数据组装页面创建时写入context.slug会作为模板 GraphQL 查询的变量传入。mdBlogPost.js 的查询如下query($slug: String!) { markdownRemark(fields: { slug: { eq: $slug } }) { html ...MarkdownBlogPost_frontmatter } site { ...site_sitemetadata } }组件体渲染逻辑很简洁const { html } this.props.data.markdownRemark return ( BlogPostChrome {...{ frontmatter: this.props.data.markdownRemark.frontmatter, site: this.props.data.site, }} div classNamecontainer content div dangerouslySetInnerHTML{{ __html: html }} / /div /BlogPostChrome )gatsby-transformer-remark已经把 Markdown 正文编译为 HTML因此模板只需dangerouslySetInnerHTML注入并把frontmatter整体下发给BlogPostChrome。另一模板 mdInsetPage.js 用于layoutType: page的内页如src/mainPages/about.md结构与博客模板对称只是换用InsetPageLayout。六、Frontmatter 的最终去向页面展示与 SEO 元数据BlogPostChrome把 frontmatter 分发给两个组件见 BlogPostChrome/index.jsHelmetBlock {...this.props.frontmatter} / div classNamesection div classNamecontainer content{this.props.children}/div /div PostPublished {...this.props.frontmatter} /6.1 PostPublished发布时间的人性化展示PostPublished/index.js 对written/updated的消费逻辑是对本文数据模型最直接的运行时验证if (frontmatter.updated null) { published empublished {moment(frontmatter.written).format(D MMM YYYY)}/em } else { published ( em {originally published } {moment(frontmatter.written).format(D MMM YYYY)} { and updated } {moment(frontmatter.updated).format(D MMM YYYY)} /em ) }由于 First Post 的updated是2017-03-04非 null页面会渲染为 originally published 22 Jan 2017 and updated 4 Mar 2017。注意updated为null是只显示首次发布时间的唯一条件这是代码中显式的分支判断可作为字段取值约束的依据。6.2 HelmetBlockSEO 元数据的组装HelmetBlock/index.js 基于react-helmet把 Frontmatter 映射为一整套元标签Helmet title{frontmatter.title}/title meta namedescription content{frontmatter.description} / meta propertyog:url content{https://www.jacobbolda.com/${frontmatter.path}} / meta propertyog:description content{frontmatter.description} / meta propertyog:type contentarticle / meta propertyog:article:published_time content{moment(frontmatter.written, YYYY-MM-DD)} / meta propertyog:article:modified_time content{moment(frontmatter.updated, YYYY-MM-DD)} / meta propertyog:article:tag content{frontmatter.category} / meta nametwitter:label1 contentCategory / meta nametwitter:data1 content{frontmatter.category} / meta nametwitter:label2 contentWritten / meta nametwitter:data2 content{frontmatter.written} / /Helmet可以看出Frontmatter 中几乎每一个字段在这里都找到了最终出口title→titledescription→ 描述类 metawritten/updated→ Open Graph 时间标签category→ 文章标签与 Twitter 卡片数据。也就是说一篇 Markdown 文章的 SEO 表现完全由它的 Frontmatter 决定正文只负责内容呈现。七、端到端回顾从 index.md 到 /a-first-post/把整条链路串起来index.md的完整旅程如下gatsby-source-filesystem将src/articles/2017-01-22-a-first-post/index.md注册为File节点配置见 gatsby-config.jsgatsby-transformer-remark将其转换为MarkdownRemark节点正文渲染为htmlFrontmatter 解析为frontmatter对象onCreateNode依据相对路径2017-01-22-a-first-post/index.md推导 slug 为/2017-01-22-a-first-post/并写入fields.slugcreatePages查询到该节点读到layoutType: post与path: /a-first-post/调用createPage({ path: /a-first-post/, component: mdBlogPost, context: { slug } })构建期执行 mdBlogPost.js 的query($slug: String!)命中fields.slug对应的节点把html与 frontmatter 注入BlogPostChromePostPublished展示written/updated日期HelmetBlock输出完整 SEO meta最终在/a-first-post/渲染出文章页面。八、实战要点小结Frontmatter 是数据契约title、written、updated、layoutType、path、category、description七个字段在本示例中全部有实际消费代码新增文章时保持这套字段即可被模板无缝渲染layoutType是路由分派开关post走mdBlogPost模板page走mdInsetPage模板判断逻辑位于 gatsby-node.js 的createPagesslug 与 path 分工明确slug 由文件路径在onCreateNode中自动推导、用于 GraphQL 查询定位path由作者在 Frontmatter 中显式指定、决定页面 URL弃用src/pages以获得完全控制该示例不依赖 Gatsby 的默认页面创建而是用createPages全手动编排适合需要统一管理路由、模板与上下文的场景Markdown 与 JavaScript 数据源同构通过gatsby-transformer-javascript-frontmatterJavaScript 文件导出的frontmatter与 Markdown Frontmatter 形状一致二者可共享同一套 GraphQL fragment 与渲染组件运行方式在 examples/using-javascript-transforms 目录下按package.json脚本执行npm run develop即gatsby develop可本地开发npm run buildnpm run serve可构建并预览生产版本。如果你希望深入验证JavaScript 作为数据源的完整形态可以继续阅读仓库中的 index.jsChoropleth 文章、布局组件 Layouts/blogPost.js以及源插件gatsby-source-filesystem、转换插件gatsby-transformer-remark的实现它们共同构成了这套用 JavaScript 掌控 Markdown 数据的完整示例。【免费下载链接】gatsbyReact-based framework with performance, scalability, and security built in.项目地址: https://gitcode.com/gh_mirrors/ga/gatsby创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考