Skip to content

Commit

Permalink
[Microfrontends] Update the Multi-Zones example with latest practices (
Browse files Browse the repository at this point in the history
…#958)

### Description

This PR updates the `solutions/microfrontends` example with some more
recent practices:

- Converts both applications to use App Router
- Uses `assetPrefix` instead of `basePath`
- Updates to most recent versions of dependencies (Next, React, Turbo)
- Uses Speculation Rules for prefetching/prerendering

### Demo URL

<!--
Provide a URL to a live deployment where we can test your PR. If a demo
isn't possible feel free to omit this section.
-->

### Type of Change

- [ ] New Example
- [ ] Example updates (Bug fixes, new features, etc.)
- [ ] Other (changes to the codebase, but not to examples)

### New Example Checklist

- [ ] 🛫 `npm run new-example` was used to create the example
- [ ] 📚 The template wasn't used but I carefuly read the [Adding a new
example](https://github.com/vercel/examples#adding-a-new-example) steps
and implemented them in the example
- [ ] 📱 Is it responsive? Are mobile and tablets considered?
  • Loading branch information
mknichel authored Oct 1, 2024
1 parent 6afd45a commit d60f473
Show file tree
Hide file tree
Showing 41 changed files with 7,909 additions and 6,561 deletions.
36 changes: 14 additions & 22 deletions solutions/microfrontends/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ relatedTemplates:

# Microfrontends

Microfrontends allow teams to work independently of each other by splitting the application into smaller, shareable, and modular components. The primary goal for a microfrontend strategy is to improve collaboration across teams of developers.
Microfrontends allow teams to work independently of each other by splitting the application into smaller, shareable, and modular components. The primary goal for a microfrontend strategy is to reduce the size of a single application to improve developer velocity while still allowing teams to collaborate with each other.

We recommend reading the ["How it works"](#how-it-works) section to understand the reasoning behind our implementation and the ["What's included"](#whats-included) section to know more about the tools we used.

Expand Down Expand Up @@ -47,7 +47,7 @@ Next, run the included Next.js apps in development mode:
pnpm dev
```

Deploy on [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=edge-middleware-eap) ([Documentation](https://nextjs.org/docs/deployment)).
Deploy on [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=vercel-examples) ([Documentation](https://nextjs.org/docs/deployment)).

## What's Included?

Expand All @@ -65,9 +65,15 @@ The example is a monorepo built with [Turborepo](https://turborepo.org/) with th

There are many strategies for designing microfrontends and your approach will be dictated by how you want to structure your applications and teams. We'll share a few different approaches and how they work.

### Monorepo Support
### Multi-Zones

[Multi-Zones](https://nextjs.org/docs/app/building-your-application/deploying/multi-zones) is a way of having independent Next.js applications that all render on a common domain. This is a method for building separation of concerns in large teams. It works well if a single domain has separate groupings of pages where a user doesn't navigate between the groups very often.

One of the challenges of building microfrontends is dependency management and build systems. In the packages and apps in this monorepo, we'll be using [Turborepo](https://turborepo.org/) and Changesets to earn great developer experience for our teams with minimal configuration.
In this example, [./apps/main](./apps/main) is our main app, and [./apps/docs](./apps/docs) is a separate app that handles all routes for [`/docs/**`](./apps/main/next.config.js). In the demo, you'll notice that navigating to `/docs` keeps you in the same domain. We have multiple apps in the same domain that are built independent of each other.

You'll notice that transitions between `/docs/*` and `/` have to perform a full page refresh because the separate Next.js apps can't share their JS and don't have common chunks. Next.js prefetching is not possible here, and you have to rely on your own browser prefetching to streamline the transitions (see `packages/acme-components/src/prefetch-cross-zone-links.tsx` for an example). The slower transitions between apps may or may not be a problem depending on your specific use case. For that reason, we only recommend using Multi-Zones for cases where you have pages that are logically in separate applications but need to be served on the same domain.

For example, having a home app with your landing, marketing and legal pages and then having another app that handles all the pages related to documentation is a good separation of concerns, your users will only notice a slow transition once they move from your home app to view your documentation. Pro tip: Using `target="_blank"` in this situation is a nice improvement!

### Design System with Tailwind and CSS Modules

Expand All @@ -77,35 +83,21 @@ All the CSS used by the app and components is unified by Tailwind, so having com

HMR and React Fast Refresh work as expected even though the components live outside the app and have a different build process.

### Pages Living Outside the Next.js App

[./packages/acme-pages](./packages/acme-pages) contains all the pages that are used in the Next.js app. They are compiled with [SWC](https://swc.rs/) and work in the same way as [./packages/acme-design-system](./packages/acme-design-system).

With this approach, we will need to be mindful of dead code elimination when there is server-only code (e.g. `getStaticProps`, `getStaticPaths` or `getServerSideProps`) which can't be properly distinguished by the Next.js app. To avoid including server code in pages, it's recommended to have data fetching methods in a different file and import them from the page in the Next.js app.

### Multi Zones

[Multi Zones](https://nextjs.org/docs/advanced-features/multi-zones) are a way of having independent Next.js applications that merge on a common domain. This is a method for building separation of concerns in large teams.

In this example, [./apps/main](./apps/main) is our main app, and [./apps/docs](./apps/docs) is a separate app that handles all routes for [`/docs/**`](./apps/main/next.config.js). In the demo, you'll notice that navigating to `/docs` keeps you in the same domain. We have multiple apps in the same domain that are built independent to each other.

You'll notice that transitions between `/docs/*` and `/` are not as smooth as you're used to with typical Next.js applications. You will get a full page refresh because Next.js apps can't share their JS and don't have common chunks, prefetching is not possible because the build outputs are different.

Compared with the internal packaging approach from above, there's a UX impact when employing a Multi Zone strategy. The slower transitions between apps may or may not be a problem depending on your specific use case. For that reason, we only recommend using Multi Zones for cases where you need to merge applications that work on their own, but not as a way of arbitrarily moving pages out of an app.
### Monorepo Support

For example, having a home app with your landing, marketing and legal pages and then having another app that handles all the pages related to documentation is a good separation of concerns, your users will only notice a slow transition once they move from your home app to view your documentation. Pro tip: Using `target="_blank"` in this situation is a nice improvement!
This example uses a monorepo to make it easier to share code across separate microfrontends. When a change is made to a component used by multiple applications, the developer only has to change one repository and Vercel will automatically deploy all affected applications. This example uses [Turborepo](https://turborepo.org/) to improve the monorepo experience.

### Polyrepos

The tooling and approaches described above should also work with polyrepos. The most important difference is that, when packages are outside of your application's repository, you won't be able to have hot module reloading for your packages out-of-the-box. In this case, you will install the package in your applications and control updates with versioning. To earn HMR, you would need to [link node modules with a package manager](https://pnpm.io/cli/link).
While a monorepo is very useful, the tooling and approaches described above should also work with polyrepos. The most important difference is that, when packages are outside of your application's repository, you won't be able to have hot module reloading for your packages out-of-the-box. In this case, you will install the package in your applications and control updates with versioning. To earn HMR, you would need to [link node modules with a package manager](https://pnpm.io/cli/link). Any time the common code is changed, the package version would need to be bumped and consumers would have to update their dependency and release the application.

### Module Federation

Module federation is a strategy for building applications in a large organization with many teams that want to prioritize shipping velocity. We encourage you to research module federation as an option for helping teams build as a part of a large organization where teams may not have the opportunity to communicate and work together.

## Versioning & Publishing Packages

We enjoy using [Changesets](https://github.com/changesets/changesets) to manage versions, create changelogs, and publish to npm. It's preconfigured in this example so you can start publishing packages immediately.
If sharing code across repositories, [Changesets](https://github.com/changesets/changesets) is a great tool to manage versions, create changelogs, and publish to npm. It's preconfigured in this example so you can start publishing packages immediately.

> It's worth installing the [Changesets bot](https://github.com/apps/changeset-bot) on your repository to more easily manage contributions.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Layout, Page, Text, Code, Link } from '@vercel/examples-ui'
import Navbar from '@acme/pages/components/navbar'
import { Page, Text, Code, Link } from '@vercel/examples-ui'
import { Navbar } from '@acme/components/navbar'

export default function About() {
export default function AboutPage() {
return (
<Page>
<Navbar isDocsApp />
Expand All @@ -10,22 +10,20 @@ export default function About() {
</Text>
<Text className="mb-4">
This is the about page in the docs app (
<Code>apps/docs/pages/about.tsx</Code>).
<Code>apps/docs/app/docs/about/page.tsx</Code>).
</Text>
<Text>
Navigations between <Link href="/">Docs</Link> and{' '}
<Link href="/about">About Docs</Link> are client-side transitions
Navigations between <Link href="/docs">Docs</Link> and{' '}
<Link href="/docs/about">About Docs</Link> are client-side transitions
because they&apos;re part of the same Next.js app. Navigating to{' '}
<a
className="text-link hover:text-link-light transition-colors"
href="/"
>
Home (Multi Zones)
Home (Multi-Zones)
</a>{' '}
requires a page refresh because it lives in a different Next.js app.
</Text>
</Page>
)
}

About.Layout = Layout
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Layout, Page, Text, Code, Link } from '@vercel/examples-ui'
import Navbar from '@acme/pages/components/navbar'
import { Page, Text, Code, Link } from '@vercel/examples-ui'
import { Navbar } from '@acme/components/navbar'

export default function Index() {
export default function IndexPage() {
return (
<Page>
<Navbar isDocsApp />
Expand All @@ -10,22 +10,20 @@ export default function Index() {
</Text>
<Text className="mb-4">
This is the index page in the docs app (
<Code>apps/docs/pages/index.tsx</Code>).
<Code>apps/docs/app/docs/page.tsx</Code>).
</Text>
<Text>
Navigations between <Link href="/">Docs</Link> and{' '}
<Link href="/about">About Docs</Link> are client-side transitions
Navigations between <Link href="/docs">Docs</Link> and{' '}
<Link href="/docs/about">About Docs</Link> are client-side transitions
because they&apos;re part of the same Next.js app. Navigating to{' '}
<a
className="text-link hover:text-link-light transition-colors"
href="/"
>
Home (Multi Zones)
Home (Multi-Zones)
</a>{' '}
requires a page refresh because it lives in a different Next.js app.
</Text>
</Page>
)
}

Index.Layout = Layout
20 changes: 20 additions & 0 deletions solutions/microfrontends/apps/docs/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { PrefetchCrossZoneLinks } from '@acme/components/prefetch'
import { Layout } from '@vercel/examples-ui'
import '@vercel/examples-ui/globals.css'

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html>
<body>
<Layout title="Microfrontends" path="solutions/microfrontends">
{children}
</Layout>
<PrefetchCrossZoneLinks hrefs={['/', '/about']} />
</body>
</html>
)
}
2 changes: 1 addition & 1 deletion solutions/microfrontends/apps/docs/next-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
19 changes: 17 additions & 2 deletions solutions/microfrontends/apps/docs/next.config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
module.exports = {
basePath: '/docs',
/** @type {import('next').NextConfig} */
const nextConfig = {
assetPrefix: '/docs-static',
async rewrites() {
return {
beforeFiles: [
// This rewrite is necessary to support assetPrefix only in Next 14 and below.
// It is not necessary in Next 15.
{
source: '/docs-static/_next/:path*',
destination: '/_next/:path*',
},
],
}
},
}

module.exports = nextConfig
23 changes: 14 additions & 9 deletions solutions/microfrontends/apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,33 @@
"license": "MIT",
"private": true,
"scripts": {
"dev": "next dev -p 3001",
"build": "next build",
"start": "next start -p 3001",
"clean": "rm -rf .next && rm -rf .turbo",
"dev": "next dev -p 3001 --turbo",
"lint": "next lint",
"clean": "rm -rf .next && rm -rf .turbo"
"start": "next start -p 3001",
"type-check": "tsc --noEmit"
},
"dependencies": {
"@acme/components": "workspace:*",
"@acme/design-system": "workspace:*",
"@acme/pages": "workspace:*",
"@acme/utils": "workspace:*",
"@vercel/examples-ui": "^1.0.4",
"next": "latest",
"react": "latest",
"react-dom": "latest"
"next": "^14.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@types/react": "latest",
"@types/react": "^18.3.10",
"@types/react-dom": "^18.3.0",
"autoprefixer": "^10.4.12",
"eslint": "^8.11.0",
"eslint-config-acme": "workspace:*",
"postcss": "^8.4.18",
"tailwindcss": "^3.2.1",
"typescript": "5.1.3"
},
"engines": {
"node": "20.x"
}
}
}
21 changes: 0 additions & 21 deletions solutions/microfrontends/apps/docs/pages/_app.tsx

This file was deleted.

7 changes: 3 additions & 4 deletions solutions/microfrontends/apps/docs/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ module.exports = {
require('@acme/design-system/tailwind'),
],
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
'./app/**/*.{js,ts,jsx,tsx}',
// Add the external packages that are using Tailwind CSS
'../../packages/acme-components/src/**/*.{js,ts,jsx,tsx}',
'./node_modules/@acme/design-system/dist/**/*.js',
'./node_modules/@vercel/examples-ui/**/*.js',
'./node_modules/@acme/design-system/**/*.js',
'./node_modules/@acme/pages/**/*.js',
],
}
10 changes: 8 additions & 2 deletions solutions/microfrontends/apps/docs/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,14 @@
"@components": ["components/index"],
"@components/*": ["components/*"]
},
"incremental": true
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
10 changes: 8 additions & 2 deletions solutions/microfrontends/apps/docs/vercel.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
{
"ignoreCommand": "pnpm dlx turbo-ignore"
}
"buildCommand": "pnpm --workspace-root exec turbo --no-daemon --filter docs build --env-mode loose",
"installCommand": "node -v && pnpm --workspace-root --filter docs... install --config.dedupe-peer-dependents=false",
"build": {
"env": {
"ENABLE_EXPERIMENTAL_COREPACK": "1"
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Layout, Page, Text, Code, Link } from '@vercel/examples-ui'
import Navbar from '../components/navbar'
import { Page, Text, Code, Link } from '@vercel/examples-ui'
import { Navbar } from '@acme/components/navbar'

export default function About(): React.ReactNode {
export default function AboutPage(): React.ReactNode {
return (
<Page>
<Navbar />
Expand All @@ -10,8 +10,7 @@ export default function About(): React.ReactNode {
</Text>
<Text>
This is the about page, defined in{' '}
<Code>packages/acme-pages/src/about</Code> and imported by{' '}
<Code>apps/main/pages/about.tsx</Code>
<Code>apps/main/app/about/page.tsx</Code>
</Text>
<Text className="mt-4">
Navigations between <Link href="/">Home</Link> and{' '}
Expand All @@ -22,12 +21,10 @@ export default function About(): React.ReactNode {
className="text-link hover:text-link-light transition-colors"
href="/docs"
>
Docs (Multi Zones)
Docs (Multi-Zones)
</a>{' '}
requires a page refresh because it lives in a different Next.js app.
</Text>
</Page>
)
}

About.Layout = Layout
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use client'

import { useState, useEffect } from 'react'
import { Button, Quote } from '@acme/design-system'
import { matchingTextColor, randomColor } from '@acme/utils'

export function ColoredButton() {
const [bgColor, setBgColor] = useState('')
const [textColor, setTextColor] = useState('')
const changeColor = () => {
const bg = randomColor()
setBgColor(bg)
setTextColor(matchingTextColor(bg))
}

useEffect(changeColor, [])

return (
<>
{bgColor && textColor && (
<>
<Button
className="mb-4"
style={{
backgroundColor: bgColor,
color: textColor,
borderColor: textColor,
}}
onClick={changeColor}
>
Change Color
</Button>
</>
)}
</>
)
}
Loading

0 comments on commit d60f473

Please sign in to comment.