ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Gatsby 中用 gatsby-transformer-javascript-static-exports 静态解析 JavaScript 文件的数据导出

Gatsby 中用 gatsby-transformer-javascript-static-exports 静态解析 JavaScript 文件的数据导出 Gatsby 中用 gatsby-transformer-javascript-static-exports 静态解析 JavaScript 文件的数据导出【免费下载链接】gatsbyReact-based framework with performance, scalability, and security built in.项目地址: https://gitcode.com/gh_mirrors/ga/gatsbyGatsby 除了 Markdown/MDX 的 frontmatter 之外还可以直接以 JavaScript 文件作为内容源。gatsby-transformer-javascript-static-exports是一个已废弃deprecated但原理典型的内容转换器插件它基于 Babelbabel/parserbabel/traverse在构建期对.js文件做静态语法分析把exports.data {...}或export const data {...}中的字面量数据抽取出来生成可在 GraphQL 中查询的JsFrontmatter节点。读完本文你将掌握该插件的安装配置、两种数据导出写法的可查询性、GraphQL 查询结构以及从源码层面理解它如何做 AST 解析、如何容错、如何创建 Gatsby 节点并了解它被gatsby-transformer-javascript-frontmatter替代后的迁移路径。背景JS 文件作为 Gatsby 内容源在 Gatsby 的数据层Data Layer中gatsby-source-*插件负责把外部内容变成File节点gatsby-transformer-*插件则挂在onCreateNode生命周期上把File节点的文本内容解析成带结构化的子节点最终通过 GraphQL 暴露给页面查询。gatsby-transformer-javascript-static-exports的定位是解析 JavaScript 文件从其中静态提取导出的数据对象。典型用法是把 React 组件文件同时当作内容文件——组件文件里除了默认导出的组件外还附带一个data对象存放元数据标题、日期、分类等由该插件抽取后供列表页、导航等 GraphQL 查询使用。需要注意该包目前已位于仓库的 deprecated-packages 目录README 开头明确声明THIS PACKAGE HAS BEEN DEPRECATED IN FAVOR OFGATSBY-TRANSFORMER-JAVASCRIPT-FRONTMATTER其替代品 gatsby-transformer-javascript-frontmatter 沿用同一套解析框架但把目标变量从data改为frontmatter并扩展支持js/jsx/ts/tsx四类扩展名见 src/gatsby-node.js 中fileExtsToProcess [js, jsx, ts, tsx]。如果你正在新建项目建议直接使用替代品本文仍以gatsby-transformer-javascript-static-exports为主体讲解其完整用法与实现。安装与启用插件安装命令来自 READMEnpm install gatsby-transformer-javascript-static-exports在项目的gatsby-config.js中注册// In your gatsby-config.js plugins: [gatsby-transformer-javascript-static-exports]该插件没有额外配置项是一个零选项插件。它的生效依赖前提是由gatsby-source-filesystem或其他产生 JavaScript 文件的 source 插件先创建出 JS 类型的File节点——从源码看插件通过shouldOnCreateNode做过滤// deprecated-packages/gatsby-transformer-javascript-static-exports/src/gatsby-node.js#L5-L8 function shouldOnCreateNode({ node }) { // This only processes JavaScript files. return node.internal.mediaType application/javascript }即只有mediaType为application/javascript的节点才会进入解析流程。注意替代品改用了node.extension白名单判断两者过滤方式不同废弃版依赖媒体类型替代品依赖扩展名这在处理.jsx/.ts文件时有实际差异废弃版只认application/javascript媒体类型。从 package.json 可以看到其依赖与运行环境约束运行时依赖babel/parser、babel/traverse、babel/runtime、bluebird版本要求^7.20.xpeerDependencies要求gatsby: ^5.0.0-next即面向 Gatsby 5 系版本engines要求node 18.0.0。构建方面该包的build脚本是babel src --out-dir .即src/gatsby-node.js会被 Babel 编译到包根目录后随包发布index.js则只是一个// noop占位文件插件的全部逻辑都挂在gatsby-node.js导出的onCreateNode/shouldOnCreateNode上。在 JS 文件中声明 data 对象两种写法插件只识别两种静态可见的导出方式两者都要求值是字面量对象写法一exports.data赋值import * as React from react exports.data { title: Choropleth on d3v4, written: 2017-05-04, layoutType: post, path: choropleth-on-d3v4, category: data science, description: Things about the choropleth. } export default MyComponent ...写法二命名导出export const dataexport const data { title: Choropleth on d3v4, written: 2017-05-04, layoutType: post, path: choropleth-on-d3v4, category: data science, description: Things about the choropleth., }对照 src/gatsby-node.js 中traverse(ast, {...})的两个访问器可以确认这一识别机制AssignmentExpression访问器匹配exports.data {...}这类赋值条件是左侧为MemberExpression且属性名为data然后逐个取右侧对象properties做静态求值ExportNamedDeclaration访问器匹配export const data {...}在VariableDeclaration中找到id.name data的声明对其init初始化对象做静态求值。这解释了为什么数据对象必须写成字面量访问器只读取node.key.name和elem.value这类 AST 结构任何运行时才拼出来的值变量引用、函数调用、拼接都不在静态求值能力范围内。parseData函数src/gatsby-node.js#L59-L78定义了能被静态求值的节点类型也是该插件能力边界的关键const parseData function parseData(node) { let value if (node.type TemplateLiteral) { // Experimental basic support for template literals: // Extract and join any text content; ignore interpolations value node.quasis.map(quasi quasi.value.cooked).join() } else if (node.type ObjectExpression) { value {} node.properties.forEach(elem { value[elem.key.name] parseData(elem.value) }) } else if (node.type ArrayExpression) { value node.elements.map(elem parseData(elem)) } else { value node.value } return value }也就是说data对象内支持字符串/数字等基元Literal直接取node.value嵌套对象ObjectExpression递归求值因此data.tags { a: { b: 1 } }这类嵌套结构可以抽取数组ArrayExpression元素逐一递归求值模板字符串TemplateLiteral源码注释明确标注为Experimental basic support只拼接quasis中的静态文本插值${...}部分会被忽略。底层解析配置Babel parser 的语法插件列表解析入口是babylon.parse(code, options)这里babylon实际require的是babel/parser见 src/gatsby-node.js#L1-L3。解析选项对插件能否吃下现代 JS 语法至关重要const options { sourceType: unambigious, allowImportExportEverywhere: true, plugins: [ jsx, flow, doExpressions, objectRestSpread, [ decorators, { decoratorsBeforeExport: true, }, ], classProperties, classPrivateProperties, classPrivateMethods, exportDefaultFrom, exportNamespaceFrom, asyncGenerators, functionBind, functionSent, dynamicImport, numericSeparator, optionalChaining, importMeta, bigInt, optionalCatchBinding, throwExpressions, pipelineOperator, nullishCoalescingOperator, ], }几个值得注意的点sourceType: unambigious让 parser 自动判断是 script 还是 module配合allowImportExportEverywhere: true即使 export 语句出现在文件任意位置也能解析语法插件覆盖了 JSX、Flow 类型注解、装饰器、类属性/私有成员、可选链、空值合并、管道操作符等因此带 Flow 类型标注或 class fields 的组件文件通常也能被解析对比替代品 gatsby-transformer-javascript-frontmatter/src/gatsby-node.js后者把sourceType改为module并按扩展名切换typescript/flow插件——这正对应了static-exports 只处理 js、frontmatter 版扩展到 ts/tsx的演进。节点创建与 GraphQL 查询结构解析完成后插件在finally分支中无条件创建节点src/gatsby-node.js#L128-L150const contentDigest createContentDigest(node) const nodeData { id: createNodeId(${node.id} JSFrontmatter), children: [], parent: node.id, node: { ...node }, internal: { contentDigest, type: JSFrontmatter, }, } nodeData.data { ...exportsData } if (node.internal.type File) { nodeData.fileAbsolutePath node.absolutePath } createNode(nodeData) createParentChildLink({ parent: node, child: nodeData })要点节点类型为JSFrontmatter节点 ID 采用 Gatsby 约定格式父节点ID JSFrontmatter抽取出的数据挂载在节点的data字段上这就是 GraphQL 中字段名为data的原因父节点若是File会附带fileAbsolutePath字段方便查询文件绝对路径通过createParentChildLink建立父子关系因此也可以从父File节点反向查询JsFrontmatter子节点。注意与替代品的一个行为差异废弃版无论是否解析到数据都会创建节点finally中无条件createNode而 gatsby-transformer-javascript-frontmatter 有if (!_.isEmpty(frontmatter))判断空 frontmatter 的文件不创建节点。GraphQL 查询示例数据就绪后可以像 README 中那样查询allJsFrontmatter{ allJsFrontmatter { edges { node { data { error path title written category description updated } } } } }返回示例{ data: { allJsFrontmatter: { edges: [ { node: { data: { error: false, path: choropleth-on-d3v4, title: Choropleth on d3v4, written: 2017-05-04, category: data science, description: Things about the choropleth., updated: null } } } ] } } }关于字段的几个行为细节README 与源码共同确认属性动态推断所有 JS 文件中data对象出现过的属性都会被导出为 schema 字段某个文件缺少的属性其值为null如上例中的updated。这得益于 Gatsby 的 schema inference 机制——每个JSFrontmatter节点的data字段形状各不相同schema 会取并集error字段正常时是false解析/遍历抛出异常时源码catch分支会将其替换为错误对象src/gatsby-node.js#L116-L127} catch (e) { exportsData { ...data, error: { err: true, code: e.code, message: e.message, stack: e.stack, }, } }README 中给出的错误字段示例error: { err: true, message: we threw an error, stack: This is a stringified stack trace },这种错误挂到查询数据上而不是让构建失败的设计让插件在部分文件存在语法问题比如用了当前 Babel 插件列表之外的新语法时仍能继续工作用户可以在查询层面检查error字段来做兜底处理。从 static-exports 迁移到 javascript-frontmatter由于本包已废弃存量站点迁移到 gatsby-transformer-javascript-frontmatter 时主要涉及三处变化对照两份 README 与源码导出变量改名exports.data/export const data全部改为exports.frontmatter/export const frontmatter替代品只匹配frontmatter属性名见 packages/gatsby-transformer-javascript-frontmatter/src/gatsby-node.js#L74-L98GraphQL 类型与字段改名查询根字段从allJsFrontmatter变为allJavascriptFrontmatter数据字段从node.data变为node.frontmatter依赖与过滤逻辑变化替代品 README 强调需同时安装并配置gatsby-source-filesystem过滤条件从媒体类型判断改为js/jsx/ts/tsx扩展名白名单且空 frontmatter 不再产生节点。小结gatsby-transformer-javascript-static-exports展示了 Gatsby 内容转换插件的一个经典范式shouldOnCreateNode过滤 →loadNodeContent读取源码 →babel/parser按宽松语法配置解析 AST →babel/traverse定向访问AssignmentExpression/ExportNamedDeclaration提取字面量 →createNodecreateParentChildLink挂到数据层 → 通过 schema inference 暴露为 GraphQL 可查字段并用error字段实现软失败。它的静态求值能力嵌套对象、数组、无插值的模板字符串与局限不执行代码、不支持变量引用都由parseData函数清晰划定。该包现已被gatsby-transformer-javascript-frontmatter取代后者在相同骨架上支持 TypeScript 扩展名并引入空 frontmatter 跳过逻辑理解本文拆解的实现细节对阅读 Gatsby 任意 transformer 插件的源码都是通用方法论。【免费下载链接】gatsbyReact-based framework with performance, scalability, and security built in.项目地址: https://gitcode.com/gh_mirrors/ga/gatsby创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进