使用 Nx 快速建構包含 React, Vue 和 Svelte 的 Monorepo

Eason Lin
15 min readAug 25, 2024

--

Photo by Chang Duong on Unsplash

最近在整理我用來做 Frontend Mentor 的作品時,Review 了一下我的整個專案,專案架構大概是這樣:

/
my-apps
├── app1
│ ├── node_modules
│ ├── dist
│ ├── src
│ ├── index.html
│ ├── package.json
│ ├── vite.config.ts
│ └── ...
├── app2
│ ├── node_modules
│ ├── dist
│ ├── src
│ ├── index.html
│ ├── package.json
│ ├── vite.config.ts
│ └── ...
└── app3
├── node_modules
├── dist
├── src
├── index.html
├── package.json
├── vite.config.ts
└── ...

其中有四個專案使用了 Vue;兩個使用了 React;一個使用了 Svelte,部分使用了 Axios,部分使用了 TypeScript,所有的專案都使用了 Vite。這樣的專案架構想當然每一個專案都可以正常的再開發或生產環境下工作。只是我就在想:既然使用到的庫、框架、打包工具有這麼高的重複率,有沒有可能讓它們在套件庫這部分可以共用呢?於是我就去研究了 Monorepo。

什麼是 Monorepo?

如果要理解什麼是 Monorepo,我覺得最快的辦法就是跟 Multi-repo 比。我們可以先將 “repo” 這個詞拆解出來。

repo 其實就是 Repository 的簡稱。如果有用過 Github,應該會知道當我們將某個專案推上 Github 後,要到 Github 上確認時,就會在 Repository 中尋找它。

點擊 Your repositiories 就可以查到自己在 Github 上的 repo

我們在工作之餘若要累積作品或貢獻原始碼,通常會叫做「做 side project」,Repository 跟我們常講的 project 不同的是,它們還包含變更歷史記錄,讓其能為團隊提供透明度並控制不同的版本。目前大多團隊會使用 Git 作為版本控制的工具,並搭配 Github, Gitlab 或是 Bitbucket 作為管理平台。

Multi-repo 的 “Multi” 代表了「多個」,也就是說,Multi-repo 中的每一個 專案都會作為完全獨立的 Repository,會有自己的版控、依賴和環境設定等。在開發上,把這個 Repository 完全獨立出來安裝是不會有問題的,因為它與其他 Repository 無關。

Multi-repo

Monorepo 的 Mono 為 “monolithic” 的縮寫,代表「整體式的」。Monorepo 中的所有專案會作為一個統一的 Repository,彼此共享同一個版控、依賴和環境設定等。在開發上,只要安裝好了專案,裡面所有的專案都會可以在開發環境中進行開發。

Monorepo

Nx 就能夠快速為我們建置 Monorepo 的環境。

什麼是 Nx?

Nx 是一個功能強大的開源建置系統,提供用於提高開發人員生產力、優化 CI 效能和維護程式碼品質的工具和技術。我們可以使用 Nx 中的 Integrated monorepo 來為我們建立 Monorepo。

Intergrated monorepo 是一個配置了一組功能的儲存庫,這些功能可以交互運作,以實現允許開發人員專注於建置功能而不是儲存庫中工具的配置、協調和維護的目標。

例如專案中通常會有 TypeScript, Prettier, ESLint 等工具的設定,透過 Intergrated Monorepo 就可以預先設定好,讓構建新的專案時不必再額外設定一次。此外,我們也可以透過 Intergrated Monorepo 先把單元測試、E2E 測試等環境都先建立起來。

以 React 為例

要建立一個包含 React 的 Monorepo,我們可以在根目錄輸入:

npx create-nx-workspace@latest my-nx-project --preset=react-monorepo

它在建立時會像 Vue-cli 一樣很貼心地先問一些問題,根據我們的所需進行客製化,像我選的 bundler 就是 vite, E2E 選擇了 Cypress, 樣式選擇了 styled-components, CI provider 則是選擇了 Gitlab。

它建立出來的專案會像是這樣:

可以注意到它使用了 apps 資料夾來存放 React 專案和 E2E 測試;如果展開 src 資料夾:

會看到 Nx 幫我們寫好了 app.tsx 的測試。此外,我們也能看到 ESLint, Prittier, Jest 等常用設定都被寫在根目錄,表示它們可以被 apps 中各個不同的專案共用,也能看到 CI 的部分已經先依照我們的選擇建立了 .gitlab-ci.yml

要啟動 my-react-app 的本地環境,可以輸入:

npx serve my-react-app

成功的話應該可以在 localhost:4200 看到建立好的畫面。

在 Repo 中新增 Vue 專案

Nx 提供了許多插件,讓我們能用簡單的方式去處理相對繁瑣的問題。假如團隊想要新增一個 Vue 專案在 Repo 中,我們就可以使用 Vue 的插件去快速建立 Vue 的專案。首先我們先安裝 @nx/vue。在專案根目錄中輸入:

npx nx add @nx/vue

接著輸入:

npx nx g @nx/vue:app my-vue-app

到這個步驟時,Nx 會確認我們希望它的名稱和擺放位置,我會選下面那個。完成後在終端機輸入:

npx nx serve my-vue-app

就能在對應埠號看到畫面:

在 Repo 中新增 Svelte 專案

由於 Nx 並沒有內建對 Svelte 的插件,官方文件的敘述看起來也是以「先建立 Repo、後安裝 Svelte」並沒有像 React 或 Vue 這麼完整,也沒有覆蓋到在已有 Repo 的情況下如何新增 Svelte 專案,因此我自己摸索出了一套方法,這方法可能不是最好的,如果你有更好的方法我也十分樂見我的方法被推翻。基本上思路就是建立一個有 TypeScript 的 Vite 專案>手動新增 Svelte 的依賴套件>調整相關檔案:

# 新增 @nx/web 插件,這個插件可以用來轉換一個沒有框架(這裡我就先把React歸類在框架了)的 TS 專案
npx nx add @nx/web

# 建立專案
npx nx g @nx/web:app my-svelte-app

接著我們新增 Svelte 的依賴套件和 @nx/js

# 新增 Svelte 的依賴套件
npm add -D vitest vite svelte svelte-check @sveltejs/vite-plugin-svelte

# 新增 @nx/js
nx add @nx/js

接著我們就可以建立第一個 Svelte 組件。首先刪除 src/app 中的預設檔案,接著建立 src/App.svelte

<script lang="ts">
let count: number = 0;
const increment = () => {
count += 1;
};
</script>

<button on:click={increment}>
count is {count}
</button>

調整 src/main.ts,改成引入 App.svelte

import App from './App.svelte';

const app = new App({
target: document.getElementById('app') as HTMLElement,
});

export default app;

接著在 src/main.ts 應該會出現紅字,因為 TypeScript 看不懂 .svelte 。我們建立 src/svelte-shims.d.ts

// src/svelte-shims.d.ts
declare module '*.svelte' {
import type { ComponentType } from 'svelte';
const component: ComponentType;
export default component;
}

將 Svelte 專案的 tsconfig.json 改為以下:

{
"compilerOptions": {
"strict": true,
"types": ["vite/client"]
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../tsconfig.base.json"
}

一樣在 Svelte 專案根目錄的 vite.config.ts 加入 Svelte 的設定:

/// <reference types='vitest' />
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte'; // 加上這行

import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';

export default defineConfig({
root: __dirname,
cacheDir: '../../node_modules/.vite/apps/my-svelte-app',

server: {
port: 4200,
host: 'localhost',
},

preview: {
port: 4300,
host: 'localhost',
},

plugins: [nxViteTsPaths(), svelte()], // 加上這行
// ...

在 Svelte 專案根目錄新增 svelte.config.js ,加入以下設定:

import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

export default {
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
// for more information about preprocessors
preprocess: vitePreprocess(),
};

新增 package.json ,加入以下設定:

{
"type": "module"
}

最後,調整 index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>MySvelteApp</title>
<base href="/" />

<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

在 Repository 根目錄輸入:

npx nx serve my-svelte-app

應該就能在對應的 Port 看到:

建立共用 lib

我們可能會想要有共用工具或是共同的 CodeBase 讓專案在建立階段就擁有一定成熟度。要建立共用的 lib,一樣在 Repository 根目錄終端機輸入:

npx nx g @nx/js:lib my-lib

這時候如果到根目錄的 tsconfig.base.json,會發現 Nx 為我們建立好了 lib 的別名:

{
"compileOnSave": false,
"compilerOptions": {
"rootDir": ".",
"sourceMap": true,
"declaration": false,
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"importHelpers": true,
"target": "es2015",
"module": "esnext",
"lib": ["es2020", "dom"],
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"baseUrl": ".",
"paths": {
"@my-nx-project/my-lib": ["my-lib/src/index.ts"]
}
},
"exclude": ["node_modules", "tmp"]
}

如果我們到 my-lib/src/index.ts,會看到

export * from './lib/my-lib';

表示我們可以在任何專案中引入它。我們就可以在專案中引入它:

// Svelte
// App.svelte
<script lang="ts">
import { myLib } from '@my-nx-project/my-lib';
let count: number = 0;
const increment = () => {
count += 1;
};
</script>

<button on:click={increment}>
count is {count}
</button>
<span>myLib says {myLib()}</span>
// React
// app.tsx
import styled from 'styled-components';

import NxWelcome from './nx-welcome';
import { myLib } from '@my-nx-project/my-lib';

const StyledApp = styled.div`
// Your style here
`;

const StyledH1 = styled.h1`
text-align: center;
font-size: 24px;
`

const myLibResult = myLib();

export function App() {
return (
<StyledApp>
<StyledH1>{myLibResult}</StyledH1>
<NxWelcome title="my-react-app" />
</StyledApp>
);
}

export default App;
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router';
import { myLib } from '@my-nx-project/my-lib';
</script>

<template>
<h1 :style="{
fontSize: '24px',
textAlign: 'center',
}">{{ myLib() }}</h1>
<header>
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
</nav>
</header>
<RouterView />
</template>
Svelte
React
Vue

如果有人需要原始碼,可參照我的 Repo:https://github.com/EasonLin0716/Nx-React-Vue-Svelte

以上就是關於如何在 Nx 快速建構 React, Vue, Svelte 和共用庫的手把手教學,其實今天講的都只是 Nx 的冰山一角,Nx 本身還包含非常多的功能,例如可以清楚呈現依賴關係的 graph 功能、自動化測試、CI 等,有興趣都可以到官方文件上進行查閱。如果內文有任何錯誤也歡迎指出,感謝你的閱讀。

References:

https://nx.dev/getting-started/intro

https://www.thoughtworks.com/insights/blog/agile-engineering-practices/monorepo-vs-multirepo

--

--

Eason Lin
Eason Lin

Written by Eason Lin

Frontend Web Developer | Books

No responses yet