如何构建Monorepo?在本文中,我们将了解 monorepo 是什么,以及 monorepos 如何帮助以更好的开发体验更快地开发应用程序。我们将讨论使用Nx开发工具管理 monorepo 的优势,并学习如何使用这些工具构建Next.js应用程序,包括详细的构建Monorepo实例。
Monorepo构建教程:Monorepo 是什么,为什么应该使用它一个monorepo是包含应用程序,工具和多个项目或项目部分的结构的单一存储库。它是为每个项目或项目的一部分创建单独存储库的替代方法。
考虑一个场景,我们使用一些前端库或框架构建仪表板应用程序。此前端应用程序的代码可能存储在dashboard
存储库中。此存储库使用的 UI 组件可能存储在另一个名为 的存储库中components
。现在,每次更新components
存储库时,我们都必须进入dashboard
存储库并更新components
依赖项。

文章图片
为了缓解这个问题,我们可以将
components
repo 与dashboard
repo合并。
文章图片
marketing
存储库中,并且依赖于components
存储库。所以,我们也必须复制components
它并与之合并marketing
。但是,正因为如此,任何相关的更改components
都必须在两个地方进行,这并不理想。
文章图片
上述问题可以通过使用 monorepo 来解决,其中
dashboard
,components
和marketing
组件驻留在一个单一的存储库中。
文章图片
- 包的更新要容易得多,因为所有应用程序和库都在一个存储库中。由于所有应用程序和包都在同一个存储库下,因此可以轻松测试和交付添加新代码或修改现有代码。
- 代码的重构要容易得多,因为我们只需要在一个地方进行,而不是跨多个存储库复制相同的内容。
- monorepo 允许持续配置 CI/CD 管道,可以被同一存储库中的所有应用程序和库重用。
- 由于像 Nx 这样的工具,包的发布也变得更加容易。
所需软件为了运行我们的应用程序,我们需要安装以下内容:
- npm
- Yarn(可选)
- Next.js用于构建我们的应用程序
- 用于向我们的应用程序添加样式的样式化组件
我们还需要一个Product Hunt帐户。
Monorepo构建教程:安装和引导 Nx 工作区我们可以使用以下命令安装Nx CLI:
npm install nx -g
上述命令将全局安装 Nx CLI。这很有用,因为现在我们可以使用这个 CLI 从任何目录创建一个新的 Next.js 应用程序。
接下来,我们需要在要创建 monorepo 的目录中运行以下命令:
npx create-nx-workspace@latest nx-nextjs-monorepo
上面的命令将创建一个 Nx 工作区。所有 Nx 应用程序都可以驻留在 Nx 工作区中。
你可能需要替换
nx-nextjs-monorepo
为你工作区的名称。它可以命名为任何你喜欢的名字。工作空间的名称一般是组织、公司等的名称。当我们运行上面的命令时,我们将获得一组步骤,这些步骤将创建我们想要使用 Nx 创建的应用程序类型。
- 第 1 步:它首先会询问我们要创建什么类型的应用程序。我们将从选项列表中选择 Next.js。
文章图片
- 第 2 步:它会询问我们要创建的应用程序的名称。我们可以称之为任何东西。在这种情况下,我们将其命名为“product-hunt”。
文章图片
- 第 3 步:它会询问我们想要使用什么类型的样式表。我们将选择样式化组件。
文章图片
- 第 4 步:它会询问我们是否要使用Nx Cloud,这是一个加速 Nx 应用程序构建的平台。在这种情况下,我们将选择否,但请检查一下。
文章图片

文章图片
该
apps
目录包含我们所有的应用程序。在我们的例子中,这个目录将包含我们正在构建的 Next.js 应用程序(名为product-hunt
)。此目录还包含product-hunt-e2e
使用Cypress 搭建的端到端测试应用程序(名为)。该
libs
目录包含所有库,如组件、实用功能等。这些库可供apps
目录中的任何应用程序使用。该
tools
目录包含所有自定义脚本、代码模块等,用于对我们的代码库进行某些修改。注意:有关目录结构的更多信息可在此处获得。
使用 Next.js 构建 Product Hunt 的首页如何构建Monorepo?在这一步中,我们将构建Producthunt 的首页。我们将从官方 Product Hunt API获取数据。Product Hunt API 提供了一个 GraphQL 接口,该接口位于https://api.producthunt.com/v2/api/graphql。它可以通过access_token访问,它可以从Product Hunt API Dashboard生成。
要创建新应用程序,我们需要单击“添加应用程序”按钮。

文章图片
Monorepo构建教程:接下来,我们可以为我们的应用程序添加一个名称和https://localhost:4200/作为我们新应用程序的重定向 URI,然后单击“创建应用程序”按钮。

文章图片
现在,我们将能够查看新应用程序的凭据。

文章图片
接下来,我们需要通过单击同一页面中的CREATE TOKEN按钮来生成Developer Token。

文章图片
这将生成一个新令牌并将其显示在页面上。

文章图片
接下来,我们需要将这些凭据存储在我们的应用程序中。我们可以
.env.local
在apps/product-hunt
目录中创建一个包含以下内容的新文件:// apps/product-hunt/.env.localNEXT_PUBLIC_PH_API_ENDPOINT=https://api.producthunt.com/v2/api/graphql
NEXT_PUBLIC_PH_TOKEN=<
your-developer-token>
由于 Product Hunt API 在 GraphQL 中,我们必须安装一些包才能使我们的应用程序与 GraphQL 一起工作。从根目录,我们需要运行以下命令来安装必要的包:
yarn add graphql-hooks graphql-hooks-memcache
graphql-hooks是一个最小的 hooks-first GraphQL 客户端。它帮助我们从 GraphQL 服务器请求数据。
graphql-hooks-memcache是
graphql-hooks
.接下来,我们需要从
graphql-hooks
包中初始化 GraphQL 客户端。我们可以通过graphql-client.ts
在apps/product-hunt/lib
目录中创建一个包含以下内容的新文件来实现:// apps/product-hunt/lib/graphql-client.tsimport { GraphQLClient } from "graphql-hooks";
import memCache from "graphql-hooks-memcache";
import { useMemo } from "react";
let graphQLClient;
const createClient = (initialState) => {
return new GraphQLClient({
ssrMode: typeof window === "undefined",
url: process.env.NEXT_PUBLIC_PH_API_ENDPOINT, // Server URL (must be absolute)
cache: memCache({ initialState }),
headers: {
Authorization: `Bearer ${process.env.NEXT_PUBLIC_PH_TOKEN}`,
},
});
};
export const initializeGraphQL = (initialState = null) => {
const _graphQLClient = graphQLClient ?? createClient(initialState);
// After navigating to a page with an initial GraphQL state, create a new
// cache with the current state merged with the incoming state and set it to
// the GraphQL client. This is necessary because the initial state of
// `memCache` can only be set once
if (initialState &
&
graphQLClient) {
graphQLClient.cache = memCache({
initialState: Object.assign(
graphQLClient.cache.getInitialState(),
initialState
),
});
}// For SSG and SSR always create a new GraphQL Client
if (typeof window === "undefined") {
return _graphQLClient;
}// Create the GraphQL Client once in the client
if (!graphQLClient) {
graphQLClient = _graphQLClient;
}return _graphQLClient;
};
export const useGraphQLClient = (initialState) => {
const store = useMemo(() => initializeGraphQL(initialState), [
initialState]);
return store;
};
上面的代码类似于官方的 Next.js GraphQL 示例。上述文件的主要思想是创建一个 GraphQL 客户端,它将帮助我们从 GraphQL 服务器请求数据。
该
createClient
函数负责使用graphql-hooks
包创建 GraphQL 客户端。该
initializeGraphQL
函数负责使用我们的 GraphQL 客户端初始化我们的 GraphQL 客户端createClient
,并在客户端对我们的 GraphQL 客户端进行补水。这是必要的,因为我们使用 Next.js,它允许我们在客户端和服务器端获取数据。因此,如果数据是在服务器端获取的,客户端也需要用相同的数据进行水化,而不需要向 GraphQL 服务器做任何额外的请求。这
useGraphQLClient
是一个可用于生成 GraphQL 客户端的钩子。接下来,我们还需要
graphql-request.ts
在apps/product-hunt/lib
目录中再创建一个文件,内容如下:// apps/product-hunt/lib/graphql-request.tsconst defaultOpts = {
useCache: true,
};
// Returns the result of a GraphQL query. It also adds the result to the
// cache of the GraphQL client for better initial data population in pages.// Note: This helper tries to imitate what the query hooks of `graphql-hooks`
// do internally to make sure we generate the same cache key
const graphQLRequest = async (client, query, options = defaultOpts) => {
const operation = {
query,
};
const cacheKey = client.getCacheKey(operation, options);
const cacheValue = https://www.lsbin.com/await client.request(operation, options);
client.saveCache(cacheKey, cacheValue);
return cacheValue;
};
export default graphQLRequest;
该
graphQLRequest
函数负责返回 GraphQL 查询的结果并将结果添加到 GraphQL 客户端的缓存中。上面的代码类似于官方的 Next.js GraphQL 示例。
接下来,我们需要
apps/product-hunt/pages/_app.tsx
使用以下内容更新文件:// apps/product-hunt/pages/_app.tsximport { ClientContext } from "graphql-hooks";
import { AppProps } from "next/app";
import Head from "next/head";
import React from "react";
import { useGraphQLClient } from "../lib/graphql-client";
const NextApp = ({ Component, pageProps }: AppProps) => {
const graphQLClient = useGraphQLClient(pageProps.initialGraphQLState);
return (
<
ClientContext.Provider value=https://www.lsbin.com/{graphQLClient}>
<
Head>
<
title>Welcome to product-hunt!<
/title>
<
/Head>
<
Component {...pageProps} />
<
/ClientContext.Provider>
);
};
export default NextApp;
上面的代码将确保我们整个应用程序可以访问的GraphQL上下文提供通过包装我们的应用程序与
ClientContext.Provider
。接下来,我们需要
all-posts.ts
在apps/product-hunt/queries
目录中再创建一个文件,内容如下:// apps/product-hunt/queries/all-posts.tsconst ALL_POSTS_QUERY = `
query allPosts {
posts {
edges {
node {
id
name
description
votesCount
website
thumbnail {
url
}
}
}
}
}
`;
export default ALL_POSTS_QUERY;
上面的 GraphQL 查询将允许我们从 ProductHunt GraphQL API 端点获取所有帖子。
我们还在目录中创建一个新
product.ts
文件apps/product-hunt/types
,内容如下定义Product
类型:// apps/product-hunt/types/product.tsexport default interface Product {
id: number;
name: string;
tagline: string;
slug: string;
thumbnail: {
image_url: string;
};
user: {
avatar_url: string;
name: string;
};
}
上面的代码添加TypeScript类型为
Product
。产品可以有 ID、名称、标语、slug、缩略图和用户。这就是 Product Hunt GraphQL 返回数据的方式。构建Monorepo实例:接下来,我们需要
apps/product-hunt/pages/index.tsx
使用以下内容更新文件:// apps/product-hunt/pages/index.tsximport { useQuery } from "graphql-hooks";
import { GetStaticProps, NextPage } from "next";
import Image from "next/image";
import React from "react";
import { initializeGraphQL } from "../lib/graphql-client";
import graphQLRequest from "../lib/graphql-request";
import {
StyledCard,
StyledCardColumn,
StyledCardLink,
StyledCardRow,
StyledCardTagline,
StyledCardThumbnailContainer,
StyledCardTitle,
StyledContainer,
StyledGrid,
} from "../public/styles";
import ALL_POSTS_QUERY from "../queries/all-posts";
import Product from "../types/product";
interface IProps {
hits: Product[
];
}const ProductsIndexPage: NextPage<
IProps> = () => {
const { data } = useQuery(ALL_POSTS_QUERY);
return (
<
StyledContainer>
<
StyledGrid>
{data.posts.edges.map(({ node }) => {
return (
<
StyledCardLink key={node.id} href=https://www.lsbin.com/{node.website} target="_blank">
<
StyledCard>
<
StyledCardColumn>
<
StyledCardThumbnailContainer>
<
Image src=https://www.lsbin.com/{node.thumbnail.url} layout="fill" />
<
/StyledCardThumbnailContainer>
<
/StyledCardColumn>
<
StyledCardColumn>
<
StyledCardRow>
<
StyledCardTitle>{node.name}<
/StyledCardTitle>
<
StyledCardTagline>{node.description}<
/StyledCardTagline>
<
/StyledCardRow>
<
/StyledCardColumn>
<
/StyledCard>
<
/StyledCardLink>
);
})}
<
/StyledGrid>
<
/StyledContainer>
);
};
export const getStaticProps: GetStaticProps = async () => {
const client = initializeGraphQL();
await graphQLRequest(client, ALL_POSTS_QUERY);
return {
props: {
initialGraphQLState: client.cache.getInitialState(),
},
revalidate: 60,
};
};
export default ProductsIndexPage;
在上面的代码片段中,我们做了两件事:
- 我们通过
ALL_POSTS_QUERY
GraphQL 查询获取数据,然后通过ProductHuntdata
API映射数组返回。 - 我们在构建期间通过getStaticProps获取数据,这是一个 Next.js 函数。但是,如果我们在构建期间获取数据,则数据可能会过时。所以,我们使用
revalidate
选项。重新验证一个可选数量(以秒为单位),之后可以发生页面重新生成。这也称为增量静态再生。
apps/product-hunt/public/styles.ts
文件中添加以下内容来添加样式:// apps/product-hunt/public/styles.tsimport styled from "styled-components";
export const StyledContainer = styled.div`
padding: 24px;
max-width: 600px;
margin: 0 auto;
font-family: sans-serif;
`;
export const StyledGrid = styled.div`
display: grid;
grid-template-columns: repeat(1, minmax(0, 1fr));
grid-gap: 24px;
`;
export const StyledCardLink = styled.a`
text-decoration: none;
color: #000;
`;
export const StyledCard = styled.div`
display: flex;
gap: 12px;
padding: 12px;
background-color: #f7f7f7;
`;
export const StyledCardColumn = styled.div`
display: flex;
flex-direction: column;
gap: 4px;
justify-content: space-between;
`;
export const StyledCardRow = styled.div`
display: flex;
flex-direction: column;
gap: 4px;
`;
export const StyledCardThumbnailContainer = styled.div`
object-fit: cover;
width: 150px;
height: 150px;
position: relative;
`;
export const StyledCardTitle = styled.div`
font-size: 18px;
font-weight: bold;
`;
export const StyledCardTagline = styled.div`
font-size: 14px;
line-height: 1.5;
`;
现在,如果我们
yarn start
在新的终端窗口中运行命令,我们将在http://localhost:4200/上看到以下屏幕。
文章图片
要解决上述问题,我们需要
apps/product-hunt/next.config.js
使用以下内容更新我们的文件:// apps/product-hunt/next.config.js// eslint-disable-next-line @typescript-eslint/no-var-requires
const withNx = require("@nrwl/next/plugins/with-nx");
module.exports = withNx({
nx: {
// Set this to false if you do not want to use SVGR
// See: https://github.com/gregberge/svgr
svgr: true,
},
images: {
domains: [
"ph-files.imgix.net", "ph-avatars.imgix.net"],
},
});
我们添加了Product Hunt API 从中获取图像的域。这是必要的,因为我们正在使用Next 的 Image 组件。
如何构建Monorepo?现在,如果我们重新启动服务器,我们应该能够在http://localhost:4200/上查看以下屏幕。

文章图片
创建可重用的组件库构建Monorepo实例:我们已经成功构建了 Product Hunt 的首页。但是,我们可以看到我们所有的样式都在一个应用程序下。因此,如果我们想在构建另一个应用程序时重用相同的样式,我们必须将这些样式复制到新应用程序中。
解决此问题的一种方法是创建一个单独的组件库并将这些样式存储在那里。该组件库可以被多个应用程序重用。
要在 Nx 中创建新的 React 库,我们可以从项目的根目录运行以下命令:
nx generate @nrwl/react:library components
上面的命令会给我们如下图所示的提示。

文章图片
由于我们使用的是样式化组件,因此我们将在上述提示中选择该选项。选择该选项后,我们将在终端上查看以下更改。

文章图片
接下来,我们将所有样式复制
apps/product-hunt/public/styles.ts
到libs/components/src/lib/components.tsx
文件中。我们还需要从这个库中导入所有的样式。为此,我们需要修改我们的
apps/product-hunt/pages/index.tsx
文件:// apps/product-hunt/pages/index.tsximport {
StyledCard,
StyledCardColumn,
StyledCardLink,
StyledCardRow,
StyledCardTagline,
StyledCardThumbnailContainer,
StyledCardTitle,
StyledContainer,
StyledGrid,
} from "@nx-nextjs-monorepo/components";
如果我们查看我们的
tsconfig.base.json
文件,我们将看到以下行:// tsconfig.base.json"paths": {
"@nx-nextjs-monorepo/components": [
"libs/components/src/index.ts"]
}
@nx-nextjs-monorepo/components
是我们组件库的名称。因此,我们已经从该apps/product-hunt/pages/index.tsx
文件库中导入了所有样式。我们可以删除该
apps/product-hunt/public/styles.ts
文件,因为我们不再需要它了。现在,如果我们重新启动 Nx 服务器,我们将在http://localhost:4200/上查看以下屏幕。

文章图片
【如何使用Nx、Next.js和TypeScript构建Monorepo()】并且不要忘记:本文的代码可在GitHub上找到,你可以在此处找到该应用程序的工作演示。
推荐阅读
- 如何使用React和Tailwind CSS构建网站()
- 如何使用MediaStream API录制音频(分步指南)
- Yarn和npm有什么区别(哪一个更好一点?)
- Airtable、Gatsby和React如何构建交互式甘特图()
- 如何使用Vue 3 Composition API创建可重用组件()
- 5种延迟加载图像以提高网站性能的方法合集
- CSS如何创建赛博朋克 2077按钮故障效果()
- 如何为你的网站创建CSS打字机效果(分步指南)
- 9个免费常用的最佳线框图工具合集