Monorepo là gì? Tạo Monorepo với Nx, Next.js và TypeScript

Monorepo là gì

Trong bài viết này, chúng ta sẽ tìm hiểu monorepo là gì và monorepo giúp phát triển ứng dụng nhanh hơn với trải nghiệm lập trình được cải thiện như thế nào. Chúng ta sẽ thảo luận về những lợi thế của việc sử dụng các công cụ phát triển Nx để quản lý monorepo và tìm hiểu cách sử dụng các công cụ đó để xây dựng ứng dụng Next.js .

Monorepo là gì và tại sao chúng ta nên cân nhắc sử dụng Monorepo

Monorepo là một kho lưu trữ duy nhất chứa các ứng dụng, công cụ và cấu hình của nhiều dự án hoặc thành phần dự án. Đó là một giải pháp thay thế cho việc tạo các kho lưu trữ riêng biệt cho từng dự án hoặc một phần của dự án.

Hãy xem xét một tình huống trong đó chúng ta đang phát triển một ứng dụng trang tổng quan bằng cách sử dụng một số thư viện hoặc framework front-end. Phần code cho ứng dụng front-end này có thể được lưu trữ trong kho lưu trữ dashboard. Các thành phần giao diện người dùng mà kho lưu trữ này sử dụng có thể được lưu trữ trong một kho lưu trữ khác có tên là components. Giờ đây, mỗi khi chúng ta cập nhật kho lưu trữ components, chúng ta phải truy cập vào kho lưu trữ dashboard và cập nhật phần phụ thuộc (dependency) của components.

Two repositories - dashboard and components

Để phần nào giải quyết nan đề này, chúng ta có thể hợp nhất repo components với repo dashboard.

Components repo merged into dashboard repo

Tuy nhiên, có thể có một ứng dụng front-end khác cho trang web tiếp thị được lưu trữ trong kho lưu trữ marketing và ứng dụng này phụ thuộc vào kho lưu trữ components. Vì vậy, chúng ta sẽ phải sao chép components và hợp nhất nó với marketing. Tuy nhiên, từ đó, bất kỳ thay đổi nào liên quan đến components sẽ phải được thực hiện ở hai nơi, vô cùng bất tiện.

Dashboard and marketing repos, each with a copy of components

Vấn đề trên có thể được giải quyết bằng cách sử dụng monorepo, trong đó các thành phần dashboardcomponents và marketing nằm trong một kho lưu trữ duy nhất.

Monorepo containing dashboard, marketing and components

Có nhiều lợi ích khác nhau khi sử dụng monorepo:

  • Việc cập nhật các gói dễ dàng hơn nhiều, vì tất cả các ứng dụng và thư viện đều nằm trong một kho lưu trữ duy nhất. Vì tất cả các ứng dụng và gói đều nằm trong cùng một kho, nên việc thêm mã mới hoặc sửa đổi mã hiện có có thể được kiểm tra và vận chuyển rất dễ dàng.
  • Việc khôi phục lại mã dễ dàng hơn nhiều, vì chúng ta sẽ thực hiện điều đó chỉ ở một nơi duy nhất thay vì sao chép cùng một nội dung trên nhiều kho lưu trữ.
  • Một monorepo cho phép cấu hình nhất quán cho các CI/CD pipeline, có thể được sử dụng lại bởi tất cả các ứng dụng và thư viện hiện có trong cùng một kho lưu trữ.
  • Việc xuất bản các gói cũng trở nên dễ dàng hơn nhiều do các công cụ như Nx.

Nx CLI giúp chúng ta tạo các ứng dụng Next.js mới và thư viện thành phần React. Nó cũng sẽ giúp chúng ta chạy một development server web với hot module reload (HMR). Nó cũng có thể thực hiện một loạt những tác vụ quan trọng khác như lint, format và generate code. Lợi thế của việc sử dụng CLI theo cách này là nó sẽ cung cấp cảm giác chuẩn hóa trong cơ sở mã của chúng ta. Khi cơ sở mã của chúng ta phát triển, rất khó để quản lý và hiểu được những phần mã phức tạp đang "ẩn mình". Nx CLI loại bỏ hầu hết những phức tạp đó bằng cách cung cấp các công cụ để tự động hóa việc tạo mã.

Phần mềm cần thiết

Chúng ta sẽ cần cài đặt những thứ sau cho mục đích chạy ứng dụng của mình:

Bạn có thể click để đọc thêm chi tiết về npm và Yarn nhé.

Các công nghệ sau sẽ được sử dụng trong ứng dụng:

Ta cũng sẽ cần một tài khoản Product Hunt.

Cài đặt và khởi động Nx Workspace

Chúng ta có thể cài đặt Nx CLI với dòng lệnh sau:

npm install nx -g

Lệnh trên sẽ cài đặt Nx CLI trên cấp global. Điều này rất hữu ích vì bây giờ chúng ta có thể tạo một ứng dụng Next.js mới với CLI này từ bất kỳ thư mục nào.

Tiếp theo, chúng ta cần chạy lệnh sau bên trong thư mục mà ta muốn tạo monorepo:

npx create-nx-workspace@latest nx-nextjs-monorepo

Lệnh trên sẽ tạo một không gian làm việc Nx. Tất cả các ứng dụng Nx có thể nằm bên trong một không gian làm việc Nx.

Bạn có thể cần thay thế nx-nextjs-monorepo bằng tên của workspace. Bạn có thể đặt tên là bất cứ thứ gì bạn thích. Tên của workspace thường là tên của một tổ chức, công ty, v.v.

Khi chạy lệnh trên, chúng ta sẽ được cung cấp một loạt các bước giúp tạo loại ứng dụng mình muốn tạo với Nx.

  • Bước 1: Đầu tiên nó sẽ hỏi loại ứng dụng chúng ta muốn tạo. Chúng ta sẽ chọn Next.js từ danh sách các tùy chọn.

    Create a workspace

  • Bước 2: Nhập tên của ứng dụng cần tạo. Chúng ta có thể nhập tùy ý. Trong trường hợp này, chúng ta sẽ đặt tên là "product hunt".

    Enter the application name

  • Bước 3: Tại đây chúng ta sẽ quyết định loại stylesheet mình muốn sử dụng. Ta sẽ chọn Styled Components.

    Enter the default stylesheet format

  • Bước 4: Tại đây ta sẽ quyết định có sử dụng Nx Cloud hay không. Đây là một nền tảng giúp tăng tốc quá trình phát triển các ứng dụng Nx. Trong trường hợp này, chúng ta sẽ chọn No.

    Use Nx Cloud?

Nx sau đó sẽ dàn dựng tất cả các tệp và thư mục và tạo cấu trúc sau cho chúng ta.

Directory structure

Đường dẫn apps chứa tất cả các ứng dụng. Trong trường hợp của chúng ta, thư mục này sẽ chứa ứng dụng Next.js mà ta đang phát triển (tên là product-hunt). Thư mục này cũng chứa các ứng dụng thử nghiệm end-to-end (có tên product-hunt-e2e) được dựng bằng Cypress.

Thư mục libs chứa tất cả các thư viện như các component, utility function, v.v. Các thư viện này có thể được sử dụng bởi bất kỳ ứng dụng nào có trong thư mục apps.

Thư mục tools chứa tất cả các tập lệnh tùy chỉnh, codemod, v.v., được sử dụng để thực hiện các sửa đổi nhất định lên cơ sở mã của chúng ta.

Lưu ý: bạn có thể dọc thêm thông tin về cấu trúc thư mục ở đây.

Xây dựng Trang chính của Product Hunt bằng Next.js

Trong bước này, chúng tôi sẽ xây dựng front-page của Producthunt. Ta sẽ tìm nạp dữ liệu từ  Product Hunt API chính thức. API Product Hunt cung cấp giao diện GraphQL có tại https://api.producthunt.com/v2/api/graphql. Nó có thể được truy cập thông qua access_token, có thể được tạo từ Bảng điều khiển API của Product Hunt.

Để tạo một ứng dụng mới, chúng ta cần nhấp vào nút ADD AN APPLICATION.

Add an application

Tiếp theo, chúng ta có thể thêm Tên cho ứng dụng của mình, thêm https://localhost:4200/ làm Redirect URI cho ứng dụng mới và nhấp vào nút Create Application.

Create an application

Bây giờ chúng ta sẽ có thể xem thông tin đăng nhập của ứng dụng mới.

New application credentials

Tiếp đến, chúng ta cần tạo Developer Token bằng cách nhấp vào nút CREATE TOKEN ngay trong trang này.

Create developer token

Thao tác này sẽ tạo token mới và hiển thị token lên trang.

Display developer token

Tiếp theo, chúng ta cần lưu trữ các thông tin đăng nhập này bên trong ứng dụng của mình. Chúng ta có thể tạo một tệp .env.local mới bên trong thư mục apps/product-hunt với nội dung sau:

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

NEXT_PUBLIC_PH_API_ENDPOINT=https://api.producthunt.com/v2/api/graphql
NEXT_PUBLIC_PH_TOKEN=<your-developer-token>

Vì Product Hunt API nằm trong GraphQL, chúng ta sẽ phải cài đặt một vài gói để ứng dụng hoạt động với GraphQL. Từ thư mục gốc, ta cần chạy lệnh sau để cài đặt các gói cần thiết:

yarn add graphql-hooks graphql-hooks-memcache

graphql-hooks là một client GraphQL hooks-first tối giản giúp chúng ta yêu cầu dữ liệu từ máy chủ GraphQL.

graphql-hooks-memcache là công cụ thực hiện caching in-memory cho graphql-hooks.

Tiếp theo, chúng ta cần khởi tạo client GraphQL từ gói graphql-hooks. Chúng ta có thể làm điều đó bằng cách tạo tệp graphql-client.ts mới bên trong thư mục apps/product-hunt/lib với nội dung sau:

// 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;
};

Đoạn mã trên tương tự như ví dụ chính thức về GraphQL của Next.js ví dụ chính thức về GraphQL của Next.js. Ý tưởng chính của tệp trên là để tạo một client GraphQL giúp chúng ta yêu cầu dữ liệu từ máy chủ GraphQL.

Hàm createClient chịu trách nhiệm tạo client GraphQL bằng gói graphql-hooks.

Hàm initializeGraphQL chịu trách nhiệm khởi tạo client GraphQL của chúng ta bằng cách sử dụng createClient cũng như hydrate ứng dụng GraphQL ở phía client. Điều này là cần thiết vì chúng ta đang sử dụng Next.js, cho phép tìm nạp dữ liệu ở cả phía client và server. Vì vậy, nếu dữ liệu được tìm nạp ở phía máy chủ, thì phía client cũng cần được hydrate với cùng dữ liệu mà không thực hiện bất kỳ yêu cầu bổ sung nào đối với GraphQL server.

useGraphQLClient là một hook có thể được sử dụng để tạo client GraphQL.

 Tiếp đến, chúng ta cũng sẽ cần tạo thêm một tệp, graphql-request.ts, bên trong đường dẫn  apps/product-hunt/lib với nội dung sau:

// 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;

Hàm graphQLRequest chịu trách nhiệm trả về kết quả của truy vấn GraphQL, cũng như thêm kết quả vào cache của GraphQL client.

Đoạn mã trên tương tự như ví dụ chính thức về GraphQL của Next.js.

Tiếp theo, chúng ta cần cập nhật tệp apps/product-hunt/pages/_app.tsx với nội dung sau:

// 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;

Đoạn mã trên sẽ đảm bảo rằng toàn bộ ứng dụng của chúng ta có quyền truy cập vào GraphQL context provider bằng cách gói ứng dụng với ClientContext.Provider.

Tiếp theo, chúng ta cần tạo thêm một tệp, all-posts.ts, bên trong đường dẫn apps/product-hunt/queries với nội dung sau:

// 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;

Truy vấn GraphQL ở trên sẽ cho phép chúng ta tìm nạp tất cả các post từ endpoint ProductHunt GraphQL API.

Chúng ta cũng hãy tạo một tệp product.ts mới bên trong đường dẫn apps/product-hunt/types với nội dung sau để xác định Loại 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;
  };
}

Đoạn mã trên thêm các TypeScript type cho Product. Một sản phẩm có thể có ID, tên, tagline, slug, thumbnail và người dùng. Đây là cách Product Hunt GraphQL trả về dữ liệu.

Tiếp theo, chúng ta cần cập nhật tệp apps/product-hunt/pages/index.tsx với nội dung sau:

// 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;

Trong đoạn mã trên, chúng ta đang thực hiện hai điều:

  1. Ta đang tìm nạp dữ liệu qua truy vấn GraphQL ALL_POSTS_QUERY và sau đó, chúng ta sẽ map các kết quả mảng data trả về bởi ProductHunt API.

  2. Chúng ta đang tìm nạp dữ liệu trong thời gian phát triển (during build time) thông qua getStaticProps, đây là một hàm Next.js. Tuy nhiên, nếu ta tìm nạp dữ liệu trong thời gian phát triển, dữ liệu có thể bị cũ đi. Vì vậy, chúng ta sẽ sử dụng tùy chọn revalidate. Xác thực lại một số (giây) tùy chọn mà sau đó, việc tạo lại trang có thể xảy ra. Thao tác này còn được gọi là Tái tạo tĩnh tăng dần (Incremental Static Regeneration).

Hãy cũng thêm các style bằng cách thêm nội dung sau vào tệp 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;
`;

Bây giờ, nếu chạy lệnh yarn start bên trong cửa sổ terminal mới, chúng ta sẽ thấy màn hình sau trên http://localhost:4200/.

Server error

Để khắc phục sự cố trên, chúng ta cần cập nhật tệp apps/product-hunt/next.config.js với nội dung sau:

// 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"],
  },
});

Chúng ta đã thêm các tên miền mà từ đó Product Hunt API tìm nạp hình ảnh. Thao tác này là cần thiết vì chúng ta đang sử dụng Image component của Next.

Bây giờ, nếu chúng ta khởi động lại máy chủ của mình, ta sẽ có thể thấy màn hình sau tại http://localhost:4200/.

Product Hunt demo page

Tạo Reusable Component Library

Chúng ta đã xây dựng thành công front page của Product Hunt. Tuy nhiên, ta có thể thấy rằng tất cả các style của chúng ta đều nằm trong một ứng dụng duy nhất. Vì vậy, nếu muốn sử dụng lại các style tương tự để phát triển một ứng dụng khác, ta sẽ phải sao chép các style này vào ứng dụng mới.

Một cách để giải quyết vấn đề này là tạo một thư viện thành phần riêng biệt và lưu trữ các style này trong đó. Thư viện thành phần đó có thể được sử dụng lại bởi nhiều ứng dụng.

Để tạo một thư viện React mới trong Nx, chúng ta có thể chạy command sau từ thư viện gốc của dự án:

nx generate @nrwl/react:library components

Lệnh trên sẽ hỏi chúng ta một số thông tin.

Select stylesheet format

Vì chúng ta đang sử dụng Styled Components nên ta sẽ chọn tùy chọn đó trong các tùy chọn ở trên. Sau đó, chúng ta sẽ xem những thay đổi sau trên terminal.

List of generated files

Tiếp theo, chúng ta sẽ sao chép tất cả các style từ apps/product-hunt/public/styles.ts vào tệp libs/components/src/lib/components.tsx.

Chúng ta cũng cần import tất cả các style từ thư viện này. Để làm điều đó, hãy sửa đổi tệp 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";

Nếu nhìn vào tệp tsconfig.base.json, chúng ta sẽ thấy dòng sau:

// tsconfig.base.json

"paths": {
  "@nx-nextjs-monorepo/components": ["libs/components/src/index.ts"]
}

@nx-nextjs-monorepo/components là tên của thư viện thành phần của chúng ta. Do đó, chúng ta đã import tất cả các style từ thư viện đó lên tệp apps/product-hunt/pages/index.tsx.

Bạn đã có thể xóa tệp apps/product-hunt/public/styles.ts vì mình không cần tệp này nữa.

Bây giờ, nếu chúng ta khởi động lại máy chủ Nx của mình, ta sẽ thấy màn hình sau trên http://localhost:4200/.

The Produzct Hunt demo is still running

Lời kết

Trong bài viết này, chúng ta đã tìm hiểu cách tận dụng Nx để xây dựng một monorepo với Next.js và Styled Components. Chúng ta cũng đã tìm hiểu cách monorepos giúp cải thiện trải nghiệm lập trình và đẩy nhanh tốc độ phát triển ứng dụng. Chúng ta đã xây dựng ứng dụng Next.js và thư viện Styled Components, nhưng với Nx, ta hoàn toàn có thể tạo các ứng dụng AngularCypressNestGatsbyExpress và Storybook bằng cách sử dụng generators của chúng.

Và đừng quên: phần code trong bài viết này có sẵn trên GitHub, và bạn có thể tìm thấy bản demo của ứng dụng tại đây.

Theo Sitepoint

Nhận xét

Bài đăng phổ biến