使用 Astro,我有一个布局文件:/src/layouts/MdxLayout.astro,我尝试设置允许的组件按钮和链接,如果从页面文件传入,则要渲染。
---
import BaseLayout from '@/layouts/Base/BaseLayout.astro';
import Button from '@/components/Button/Button';
import Link from "@/components/Link/Link";
---
<BaseLayout components={{ Button: Button, Link: Link }}>
<slot />
</BaseLayout>
希望这能在 /src/pages/mdx.mdx:
中起作用---
layout: '@/layouts/MdxLayout.astro'
title: "Hello, World!"
---
# Lorem ipsum
<Button>Click me</Button>
因此,我不必向我希望使用这些组件的所有 mdx 文件添加导入(即:
import Button from '@/components/Button/Button'
)。如果没有这个导入,我将面临错误:
预期要定义的组件按钮:您可能忘记导入、传递或提供它。
我的问题:有什么方法可以在布局文件中预定义组件,然后可以在使用此布局的 mdx 文件中使用这些组件,而无需将它们导入到 mdx 文件中?
这是可能的,但不是天生的。您有三个选择:
使用评论或重新炒作
您可以创建自定义 Remark/Rehype 插件,以使用组件的导入语句更新树。比如:
import type { Root } from 'hast';
import type { Plugin as UnifiedPlugin } from 'unified';
import { visit } from 'unist-util-visit';
export const rehypeAutoImportComponents: UnifiedPlugin<[], Root> =
() => (tree, file) => {
const importsStatements = [];
visit(tree, 'mdxJsxFlowElement', (node, index, nodeParent) => {
if (node.name === 'Button') importsStatements.push('import Button from "@/components/Button/Button"');
});
tree.children.unshift(...importsStatements);
};
您需要弄清楚如何解析路径别名以及如何避免重复的导入语句。
然后更新你的 Astro 配置文件:
import { rehypeAutoImportComponents } from './src/utils/rehype-auto-import-components';
export default defineConfig({
markdown: {
rehypePlugins: [
rehypeAutoImportComponents
],
},
});
您还可以向插件添加一个选项,以直接在 Astro 配置文件中传递组件,而不是在插件中对它们进行硬编码。
注意:使用此解决方案,您可以删除
components
中的 <BaseLayout ... />
属性。
现有库
astro-auto-import
或 Astro-M²DX
例如。
将 HTML 标签映射到自定义组件
另一种选择是将 HTML 标签映射到自定义组件:
---
import BaseLayout from '@/layouts/Base/BaseLayout.astro';
import Button from '@/components/Button/Button';
import Link from "@/components/Link/Link";
---
<BaseLayout components={{ a: Link, button: Button }}>
<slot />
</BaseLayout>
但是,为了能够映射 HTML 标签(即
<button />
),您需要一个插件来禁用 MDXJS 的 默认行为(忽略 HTML 标签)。比如:
import type { Root } from 'hast';
import type { Plugin as UnifiedPlugin } from 'unified';
import { visit } from 'unist-util-visit';
export const rehypeDisableExplicitJsx: UnifiedPlugin<[], Root> =
() => (tree) => {
visit(tree, ['mdxJsxFlowElement', 'mdxJsxTextElement'], (node) => {
if (node.data && '_mdxExplicitJsx' in node.data) {
delete node.data._mdxExplicitJsx;
}
});
};
然后你可以将此插件添加到你的 Astro 配置中:
import { rehypeDisableExplicitJsx } from './src/utils/rehype-disable-explicit-jsx';
export default defineConfig({
markdown: {
rehypePlugins: [
rehypeDisableExplicitJsx
],
},
});
这样,您的 MDX 文件就可以如下所示:
---
layout: '@/layouts/MdxLayout.astro'
title: "Hello, World!"
---
# Lorem ipsum
<button>Click me</button>
需要注意的是,对于更复杂的组件(当不存在 HTML 标签时),您将需要额外的逻辑。您可以将 div 映射到自定义组件,在其中检查特定类/属性以返回正确的组件。
谢谢阿曼德的回答,我让它与这个 rehype 插件一起工作:
import { parse, resolve } from 'node:path';
import { parse as parseJs } from 'acorn';
import type { VFile } from 'vfile';
const resolveModulePath = (path: string) => {
// Resolve relative paths
if (path.startsWith('.')) return resolve(path);
// Don’t resolve other paths (e.g. npm modules)
return path;
};
type NamedImportConfig = string | [from: string, as: string];
type ImportsConfig = (string | Record<string, string | NamedImportConfig[]>)[];
/**
* Use a filename to generate a default import name.
* @example
* getDefaultImportName('/path/to/cool-component.astro');
* // => coolcomponent
*/
function getDefaultImportName(path: string): string {
return parse(path).name.replaceAll(/[^\w\d]/g, '');
}
/**
* Create an import statement.
* @param imported Stuff to import (e.g. `Thing` or `{ Named }`)
* @param module Module to import from (e.g. `module-thing`)
*/
function formatImport(imported: string, module: string): string {
return `import ${imported} from ${JSON.stringify(module)};`;
}
/** Get the parts for a named import statement from config. */
function formatNamedImports(namedImport: NamedImportConfig[]): string {
const imports: string[] = [];
for (const imp of namedImport) {
if (typeof imp === 'string') {
imports.push(imp);
} else {
const [from, as] = imp;
imports.push(`${from} as ${as}`);
}
}
return `{ ${imports.join(', ')} }`;
}
/** Generate imports from a full imports config array. */
function processImportsConfig(config: ImportsConfig) {
const imports = [];
for (const option of config) {
if (typeof option === 'string') {
imports.push(formatImport(getDefaultImportName(option), resolveModulePath(option)));
} else {
for (const path in option) {
const namedImportsOrNamespace = option[path];
if (typeof namedImportsOrNamespace === 'string') {
imports.push(formatImport(`* as ${namedImportsOrNamespace}`, resolveModulePath(path)));
} else {
const importString = formatNamedImports(namedImportsOrNamespace);
imports.push(formatImport(importString, resolveModulePath(path)));
}
}
}
}
return imports;
}
/** Get an MDX node representing a block of imports based on user config. */
function generateImportsNode(config: ImportsConfig) {
const imports = processImportsConfig(config);
const js = imports.join('\n');
return {
type: 'mdxjsEsm',
value: '',
data: {
estree: {
...parseJs(js, { ecmaVersion: 'latest', sourceType: 'module' }),
type: 'Program',
sourceType: 'module',
},
},
};
}
type MfxFile = VFile & { data?: { astro?: { frontmatter?: { layout?: string } } } };
type PluginConfig = {
[layoutName: string]: string[];
};
export function AutoImportComponentsPerLayout(pluginConfig: PluginConfig) {
return function (tree: { children: unknown[] }, vfile: MfxFile) {
const fileLayout: string =
vfile?.data?.astro?.frontmatter?.layout?.split('/').pop()?.split('.')[0] || '';
if (!fileLayout) {
return;
}
// for each key in PluginConfig loop and add the array of imports to the imports array
for (const key in pluginConfig) {
if (Object.prototype.hasOwnProperty.call(pluginConfig, key)) {
if (key === fileLayout) {
const imports: string[] = [];
const components: string[] = pluginConfig[key];
components.forEach((component: string) => {
imports.push(`@/components/${component}/${component}.tsx`);
});
const importsNode = generateImportsNode(imports);
tree?.children.unshift(importsNode);
}
}
}
};
}
可以像这样使用:
integrations: [
mdx({
rehypePlugins: [[AutoImportComponentsPerLayout, { BaseLayout: ["Button", "Card"], AnotherLayout: ["Button"] } ]]
})
],