Как сгенерировать меню на основе файлов в каталоге страниц в Next.js
Я пытаюсь создать компонент меню, который считывает содержимое папки страниц во время сборки. Однако успеха у меня не было. Вот что я пробовал:
import path from "path";
import * as ChangeCase from "change-case";
export default class Nav extends React.Component {
render() {
return (
<nav>
{this.props.pages.map((page) => (
<a href={page.link}>{page.name}</a>
))}
</nav>
);
}
async getStaticProps() {
let files = fs.readdirSync("../pages");
files = files.filter((file) => {
if (file == "_app.js") return false;
const stat = fs.lstatSync(file);
return stat.isFile();
});
const pages = files.map((file) => {
if (file == "index.js") {
const name = "home";
const link = "/";
} else {
const link = path.parse(file).name;
const name = ChangeCase.camelCase(link);
}
console.log(link, name);
return {
name: name,
link: link,
};
});
return {
props: {
pages: pages,
},
};
}
}
Это не работает, компонент не получает поддержку страниц. Я попытался переключиться на функциональный компонент, вернув обещание отgetStaticProps()
, переходя на getServerSideProps()
и включение кода чтения каталога в метод рендеринга.
Первые два не работают, потому что getStaticProps()
а также getServerSideProps()
никогда не вызывается, если компонент не является страницей, и включение кода в метод рендеринга завершается ошибкой, потому что fs не определен или не импортируется, поскольку код может выполняться во внешнем интерфейсе, у которого не было бы доступа к fs.
Я также пробовал добавить код в getStaticProps()
функция внутри _app.js
, в надежде отправить страницы компоненту через контекст, но кажется getStaticProps()
там тоже не звонят.
Я мог бы запустить код в функции getStaticProps страниц, содержащих меню, но мне пришлось бы повторять это для каждой страницы. Даже если я извлечу логику в модуль, который вызывается из getStaticProps, что-то вроде:
// ...
export async function getStaticProps() {
return {
props: {
pages: MenuMaker.getPages(),
// ...
}
}
}
а затем передать страницы в компонент навигации внутри страницы через компонент Layout:
export default function Page(props) {
return (
<Layout pages={props.pages}></Layout>
)
}
тогда это еще много шаблонов, которые нужно добавить на каждую страницу сайта.
Конечно, есть способ получше... Не может быть, чтобы нет возможности добавить статические данные в глобальное состояние во время сборки, не так ли? Как создать динамическое меню во время сборки?
3 ответа
Мне удалось заставить это работать, экспортировав функцию из next.config.js и установив переменную среды, которая содержит структуру меню. Я отвел код загрузки меню в его собственный файл. Увидев результат, я лучше понимаю, почему мне не удалось найти пример того, чтобы кто-то делал что-то подобное:
Меню составлено не так, как хотелось бы. Я мог бы отсортировать его по алфавиту или по дате изменения, но на практике его почти всегда нужно вручную отсортировать по тематике страниц. Я мог бы использовать целое число, прикрепленное к имени файла или где-нибудь в файле (возможно, в строке комментария). Но, оглядываясь назад, я думаю, что просто жесткое кодирование ссылок в компоненте, вероятно, является лучшим способом в конце концов, поскольку он предлагает гораздо большую гибкость и, вероятно, не будет намного труднее даже в очень долгосрочной перспективе.
При этом я делюсь своим решением, поскольку это способ инициализировать статическое состояние всего приложения. Это не идеально, вам придется перезапустить сервер разработки, если вы хотите пересчитать здесь переменные, поэтому меня все еще интересуют другие возможные решения, но это работает. Итак, вот оно:
next.config.js
const menu = require("./libraries/menu.js");
module.exports = (phase, { defaultConfig }) => {
return {
// ...
env: {
// ...
menu: menu.get('pages'),
// ...
},
// ...
};
};
библиотеки / menu.js
const fs = require("fs");
const path = require("path");
const ccase = require("change-case");
module.exports = {
get: (pagePath) => {
if (pagePath.slice(-1) != "/") pagePath += "/";
let files = fs.readdirSync(pagePath);
files = files.filter((file) => {
if (file == "_app.js") return false;
const stat = fs.lstatSync(pagePath + file);
return stat.isFile();
});
return files.map((file) => {
if (file == "index.js") {
return {
name: "Home";
link: "/";
};
} else {
link = path.parse(file).name;
return {
link: link;
name: ccase.capitalCase(link);
};
}
});
},
};
Затем фактическое меню создается из переменной среды в компоненте, который может быть включен в макет:
компоненты / nav.js
import Link from "next/link";
export default class Nav extends React.Component {
render() {
return (
<nav>
{process.env.menu.map((item) => (
<Link key={item.link} href={item.link}>
<a href={item.link}>
{item.name}
</a>
</Link>
))}
</nav>
);
}
}
И я получил вот что:-
- index.jsx
import fs from 'fs';
const index = () => {
return (
<div>
home
</div>
)
}
export const getStaticProps = async () => {
const dirs = fs.readdirSync('pages');
const menu = dirs.map(dir => dir.replace('.jsx', ''))
const pureMenu = menu.filter(m => m !== '_app' && m !== 'api');
return { props: { menu: pureMenu } };
}
export default index;
- _app.jsx
import Layout from '../components/Layout'
function MyApp({ Component, pageProps }) {
return <Layout>
<Component {...pageProps} />
</Layout>
}
export default MyApp
- Layout.jsx
import Navbar from './Navbar'
const Layout = ({ children }) => {
return (
<div>
<Navbar menu={children.props.menu} />
{children}
</div>
)
}
export default Layout
- Navbar.jsx
import Link from 'next/link'
const Navbar = ({ menu }) => {
return (
<div>
<ul>
{menu.map((m, i) => <li key={i}>
<Link href={m === 'index' ? `/` : `/${m}`}>
<a>
{m === 'index' ? 'home' : m}
</a>
</Link>
</li>)}
</ul>
</div>
)
}
export default Navbar
Вы можете попробовать это:
const fg = require('fast-glob');
const pages = await fg(['pages/**/*.js'], { dot: true });