如何使用Nx、Next.js和TypeScript构建Monorepo?

2021年11月30日01:46:12 发表评论 1,117 次浏览

如何构建Monorepo?在本文中,我们将了解 monorepo 是什么,以及 monorepos 如何帮助以更好的开发体验更快地开发应用程序。我们将讨论使用Nx开发工具管理 monorepo 的优势,并学习如何使用这些工具构建Next.js应用程序,包括详细的构建Monorepo实例

Monorepo构建教程:Monorepo 是什么,为什么应该使用它

一个monorepo是包含应用程序,工具和多个项目或项目部分的结构的单一存储库。它是为每个项目或项目的一部分创建单独存储库的替代方法。

考虑一个场景,我们使用一些前端库或框架构建仪表板应用程序。此前端应用程序的代码可能存储在dashboard存储库中。此存储库使用的 UI 组件可能存储在另一个名为 的存储库中components。现在,每次更新components存储库时,我们都必须进入dashboard存储库并更新components依赖项。

如何使用Nx、Next.js和TypeScript构建Monorepo?

为了缓解这个问题,我们可以将componentsrepo 与dashboardrepo合并。

如何使用Nx、Next.js和TypeScript构建Monorepo?
构建Monorepo实例

但是,可能有另一个用于营销站点的前端应用程序存储在marketing存储库中,并且依赖于components存储库。所以,我们也必须复制components它并与之合并marketing。但是,正因为如此,任何相关的更改components都必须在两个地方进行,这并不理想。

如何使用Nx、Next.js和TypeScript构建Monorepo?

上述问题可以通过使用 monorepo 来解决,其中dashboard,componentsmarketing组件驻留在一个单一的存储库中。

如何使用Nx、Next.js和TypeScript构建Monorepo?
构建Monorepo实例

使用 monorepo 有多种优点:

  • 包的更新要容易得多,因为所有应用程序和库都在一个存储库中。由于所有应用程序和包都在同一个存储库下,因此可以轻松测试和交付添加新代码或修改现有代码。
  • 代码的重构要容易得多,因为我们只需要在一个地方进行,而不是跨多个存储库复制相同的内容。
  • monorepo 允许持续配置 CI/CD 管道,可以被同一存储库中的所有应用程序和库重用。
  • 由于像 Nx 这样的工具,包的发布也变得更加容易。

NX CLI将帮助我们创造新的Next.js申请并作出反应的组件库。它还将帮助我们运行带有热模块重新加载的开发 Web 服务器。它还可以做很多其他重要的事情,比如linting、格式化生成代码。使用像这样的 CLI 的好处是它将在我们的代码库中提供一种标准化的感觉。随着我们代码库的增长,管理和理解底层的复杂性变得非常困难。Nx CLI 通过提供工具来自动生成代码,从而消除了大部分复杂性。

所需软件

为了运行我们的应用程序,我们需要安装以下内容:

这些技术将在应用程序中使用:

注意:如果你想跟上进度,可以阅读有关如何使用 nvm 安装多个版本的 Node.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。如何使用Nx、Next.js和TypeScript构建Monorepo?
  • 第 2 步:它会询问我们要创建的应用程序的名称。我们可以称之为任何东西。在这种情况下,我们将其命名为“product-hunt”。如何使用Nx、Next.js和TypeScript构建Monorepo?
  • 第 3 步:它会询问我们想要使用什么类型的样式表。我们将选择样式化组件。如何使用Nx、Next.js和TypeScript构建Monorepo?
  • 第 4 步:它会询问我们是否要使用Nx Cloud,这是一个加速 Nx 应用程序构建的平台。在这种情况下,我们将选择否,但请检查一下。如何使用Nx、Next.js和TypeScript构建Monorepo?

Nx 现在将为所有文件和目录搭建脚手架,并为我们生成以下结构。

如何使用Nx、Next.js和TypeScript构建Monorepo?

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生成。

要创建新应用程序,我们需要单击“添加应用程序”按钮。

如何使用Nx、Next.js和TypeScript构建Monorepo?

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

如何使用Nx、Next.js和TypeScript构建Monorepo?

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

如何使用Nx、Next.js和TypeScript构建Monorepo?

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

如何使用Nx、Next.js和TypeScript构建Monorepo?

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

如何使用Nx、Next.js和TypeScript构建Monorepo?

接下来,我们需要将这些凭据存储在我们的应用程序中。我们可以.env.localapps/product-hunt目录中创建一个包含以下内容的新文件:

// apps/product-hunt/.env.local

NEXT_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-memcachegraphql-hooks.

接下来,我们需要从graphql-hooks包中初始化 GraphQL 客户端。我们可以通过graphql-client.tsapps/product-hunt/lib目录中创建一个包含以下内容的新文件来实现:

// apps/product-hunt/lib/graphql-client.ts

import { 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.tsapps/product-hunt/lib目录中再创建一个文件,内容如下:

// apps/product-hunt/lib/graphql-request.ts

const 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 = 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.tsx

import { 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={graphQLClient}>
      <Head>
        <title>Welcome to product-hunt!</title>
      </Head>
      <Component {...pageProps} />
    </ClientContext.Provider>
  );
};

export default NextApp;

上面的代码将确保我们整个应用程序可以访问的GraphQL上下文提供通过包装我们的应用程序与ClientContext.Provider

接下来,我们需要all-posts.tsapps/product-hunt/queries目录中再创建一个文件,内容如下:

// apps/product-hunt/queries/all-posts.ts

const 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.ts

export 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.tsx

import { 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={node.website} target="_blank">
              <StyledCard>
                <StyledCardColumn>
                  <StyledCardThumbnailContainer>
                    <Image src={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;

在上面的代码片段中,我们做了两件事:

  1. 我们通过ALL_POSTS_QUERYGraphQL 查询获取数据,然后通过ProductHunt dataAPI映射数组返回。
  2. 我们在构建期间通过getStaticProps获取数据,这是一个 Next.js 函数。但是,如果我们在构建期间获取数据,则数据可能会过时。所以,我们使用revalidate选项。重新验证一个可选数量(以秒为单位),之后可以发生页面重新生成。这也称为增量静态再生

我们还通过在apps/product-hunt/public/styles.ts文件中添加以下内容来添加样式:

// apps/product-hunt/public/styles.ts

import 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/上看到以下屏幕。

如何使用Nx、Next.js和TypeScript构建Monorepo?

要解决上述问题,我们需要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/上查看以下屏幕。

如何使用Nx、Next.js和TypeScript构建Monorepo?

创建可重用的组件库

构建Monorepo实例:我们已经成功构建了 Product Hunt 的首页。但是,我们可以看到我们所有的样式都在一个应用程序下。因此,如果我们想在构建另一个应用程序时重用相同的样式,我们必须将这些样式复制到新应用程序中。

解决此问题的一种方法是创建一个单独的组件库并将这些样式存储在那里。该组件库可以被多个应用程序重用。

要在 Nx 中创建新的 React 库,我们可以从项目的根目录运行以下命令

nx generate @nrwl/react:library components

上面的命令会给我们如下图所示的提示。

如何使用Nx、Next.js和TypeScript构建Monorepo?

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

如何使用Nx、Next.js和TypeScript构建Monorepo?

接下来,我们将所有样式复制apps/product-hunt/public/styles.tslibs/components/src/lib/components.tsx文件中。

我们还需要从这个库中导入所有的样式。为此,我们需要修改我们的apps/product-hunt/pages/index.tsx文件:

// apps/product-hunt/pages/index.tsx

import {
  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?
构建Monorepo实例

Monorepo构建教程结论

如何构建Monorepo?在本文中,我们学习了如何利用 Nx 构建带有 Next.js 和样式化组件的 monorepo。我们还学习了使用 monorepos 如何提高开发体验和构建应用程序的速度。我们已经构建了一个 Next.js 应用程序和一个样式化组件库,但是使用 Nx,可以使用它们的生成器生成AngularCypressNestGatsbyExpressStorybook应用程序。

并且不要忘记:本文的代码可在GitHub上找到,你可以在此处找到该应用程序的工作演示。

木子山

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: