使用Next.js、TypeORM和Shadcn-UI从零搭建现代化SaaS系统(第一部分)

583
2024-06-21 14:50
5 个月前

项目初始化

我们这里使用pnpm-worksapce的形式去做代码演示

cd /Users/guojian/Desktop/project/cms/test/sass-startup
pnpm init

创建工作目录apps和 pnpm-workspace.yaml配置文件

packages:
  - "apps/*"

构建前端服务

cd apps
pnpx create-next-app@latest
# 全部使用默认值即可

初始化

添加shadcn/ui框架

# 初始化shadcn/ui
 pnpx shadcn-ui@latest init
 # 根据提示直接选择默认选项就好了

初始化

修改src/app/page.tsx

import Image from "next/image";
import {Button} from "@/components/ui/button";

export default function Home() {
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <Button>startUp</Button>
    </main>
  );
}

启动项目访问127.0.0.0:3000可以看到这个页面,则以为着shadcn/ui引入成功

首页
首页

下面我们继续修改,我们使用tailwindcss去随便写一下landingPage页面,以下页面和业务无关

landing page

我们的首页是首先默认是未登录的情况,利于seo,然后进入页面后,在客户端去请求,如果用户登陆后,我们则需要把去登录的页面改为跳转到dashboard页面。所以,我们需要封装一个checkLogin的hook,这个hook是在客户端mounted之后执行的。这里我们现暂停一下,因为检测是否登陆需要接口,我们需要用到数据库去判断用户是否登陆了。

添加typeorm数据库

我们这个项目呢因为前端驱动,而且是mvp的项目,所以我先不在apps那里添加server项目,去做接口,这个项目我们直接在nextjs里面完成,包括连接数据库,写接口等。

下面我们去初始化一个npm包,用他来统一管理orm,因为我们以后可能用nestjs,express,koa,等做我们的后端api接口,所以我们现预留这样的坑位出来。

我们在apps的同级目录下新建个文件夹 packages,然后修改pnpm-workspace.yaml

packages:
  - "apps/*"
  - "packages/*"

然后我们新建一个db的包,然后进入初始化package.json,步骤如下所示

cd packages
mkdir packages/db
cd db && pnpm init

我们的package.json如下所示

{
  "name": "@sass-startup/db",
  "version": "1.0.0",
  "description": "",
  "main": "dist/index.js",
  "scripts": {
    "watch": "tsc --w",
    "build": "tsc"
  },
  "dependencies": {
    "typeorm": "^0.3.20"
  },
  "peerDependencies": {
    "typeorm": "*"
  },
  "devDependencies": {
    "@types/node": "^20.14.2",
    "typescript": "^5.1.3"
  },
  "private": true
}

设置一下tsconfig.json,运行build,会打包出一个dist目录,我们的tsconfig.json如下所示

{
  "compilerOptions": {
    "module": "commonjs",
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "target": "es2017",
    "sourceMap": true,
    "outDir": "./dist",
    "baseUrl": "./src",
    "incremental": true,
    "strict": true,
    "lib": ["esnext"],
    "skipLibCheck": true,
    "strictNullChecks": false,
    "noImplicitAny": false,
    "strictBindCallApply": false,
    "forceConsistentCasingInFileNames": false,
    "noFallthroughCasesInSwitch": false,
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "disableSizeLimit": false,
  },
  "include": ["src/**/*.ts","types"],
  "exclude": ["node_modules","output","dist"]
}

现在我们创建一个用户实体和基础实体,来测试一下orm是否可行

在packages/db/src创建entities目录,里面创建一下文件

// packages/db/src/entities/base.entity.ts
import {Column, DeleteDateColumn, PrimaryGeneratedColumn, UpdateDateColumn} from 'typeorm';


export class BaseEntity {
  @PrimaryGeneratedColumn({ type: 'int', name: 'id', unsigned: true })
  id: number;
  @Column('datetime', {
    name: 'create_at',
    comment: '创建时间',
    default: () => 'CURRENT_TIMESTAMP',
  })
  createAt: Date;
  @UpdateDateColumn( {
    name: 'update_at',
    comment: '修改时间',

  })
  updateAt: Date;
  @DeleteDateColumn( {
    name: 'delete_at',
    comment: '修改时间',
  })
  deleteAt: Date;
}

export class BaseWithFullEntity extends BaseEntity {
  @Column('tinyint', {
    comment: '1(正常)0(禁用)',
    width: 1,
    default: 1
  })
  status:  number;
}

export class BaseWithIdEntity {
  @PrimaryGeneratedColumn({ type: 'int', name: 'id', unsigned: true })
  id: number;
}
// packages/db/src/entities/user.entity.ts
import { Column, Entity} from 'typeorm'
import {BaseWithFullEntity} from "./base.entity";

@Entity('user',{
    database:'sass'
})
export class SassStartUserEntity extends BaseWithFullEntity {
    @Column('varchar', { name: 'username', comment: '名称', length: 255,nullable:false })
    username: string;

    @Column('varchar', { name: 'password', comment: '名称', length: 255})
    password: string;
}
// packages/db/src/entities/index.ts
export {SassStartUserEntity} from './user.entity'
// packages/db/src/index.ts
// 我们使用类来导出这个typeorm的操作。后面我们会一步一步实现,现在只需要做链接就可以
import {entities } from './entities'
import {DataSource, DataSourceOptions, DefaultNamingStrategy} from "typeorm";
import {snakeCase} from "typeorm/util/StringUtils";

export const entityMap = entities

class CustomNamingStrategy extends DefaultNamingStrategy {
    tableName(targetName: string, userSpecifiedName: string): string {
        return userSpecifiedName ? userSpecifiedName : snakeCase(targetName);
    }
    columnName(propertyName: string, customName: string, embeddedPrefixes: string[]): string {
        if (customName) return customName;
        return snakeCase(embeddedPrefixes.concat(customName ? customName : propertyName).join('_'));
    }
}

export class DbDriver {

    public static config:DataSourceOptions
    public static dataSource:DataSource
    // 获取所有的实体,typeorm配置的时候用到
    static getEntities () {
        return Object.values(entityMap).map(entity=>{
            return entity
        })
    }
    // 设置typeorm 的配置文件
    static setConfig(config:typeof DbDriver['config']){
        DbDriver.config = config
    }

    // 链接typeorm
    static  connect (){
        return new Promise(async(resolve, reject)=>{
            if(!DbDriver.dataSource){
                const dataSource =new DataSource({
                    // 默认启用下横线模式
                    namingStrategy: new CustomNamingStrategy(),
                    ...DbDriver.config
                });
                await dataSource.initialize()
                DbDriver.dataSource =dataSource
                resolve(DbDriver.dataSource)
                console.log('database connect successfully')
            }else{
                resolve(DbDriver.dataSource)
            }
        })
    }
    //先写死实体
    static find(){
        return DbDriver.dataSource.getRepository(entityMap.SassStartUserEntity).find();
    }
}

nextjs引入typeorm

首先我们需要在client的package.json添加一下依赖

// apps/client/db
import {DbDriver} from "@sass-startup/db";
export class Db {
    static db:typeof DbDriver
    static get install(){
        return new Promise<typeof DbDriver>(async(resolve, reject)=>{
            if(Db.db){
                resolve(DbDriver)
            }
            await Db.getInstall();
            resolve(DbDriver)
        })
    }
    static async getInstall(){
        if(Db.db === undefined){
            // 配置
            DbDriver.setConfig({
                type: 'mysql',
                synchronize: false,
                logging: true,
                entities: [...DbDriver.getEntities()],
                logger: 'advanced-console',
                poolSize: 20,
                connectorPackage: 'mysql2',
                relationLoadStrategy:"query",
                username: 'root',
                host: '127.0.0.1',
                password:'password',
                port: 3306,
                timezone: '+08:00',
            })
            await DbDriver.connect().catch(e=>{
                console.log(e)
            })
            return DbDriver
        }
        return DbDriver
    }
}

修改一个我们的首页,查看一下是否成功。

// apps/client/src/page.tsx
import Example from "@/app/_component/homeView";
import {Db} from "../../db";


// 测试获取
const getData = async ()=>{
    const db = await Db.install
    const list = await db.find();
    return list
}
export default async function Home() {
    const data = await getData();

  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
        {JSON.stringify(data)}
     <Example></Example>
    </main>
  );
}

然后我们查看一下我们的网页,你这个时候会看到数据库的信息了

user list

这个时候查看一下控制台,会看到一堆的waring,我们来修复一下吧。错误如下所示

错误提示

这个时候我们只需要修改next.config.mjs里面增加externalPackages就可以了

添加如下代码

// apps/client/next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
    experimental:{
        serverComponentsExternalPackages: ["typeorm"],
    },
};

export default nextConfig;

现在我们访问一下首页,看一下控制台,世界瞬间清净了。

fix

在开发阶段我们把typeorm的配置,synchronize:true打开,然后创建一个数据库。这个时候会看到自动修改数据库表结构了,后面我们可以愉快的写代码了,现在这个版本还有很多问题。后面一个章节我们会继续完善DB的包,以及nextjs的整体框架搭建。现在就先到这里吧,遇到有问题可以给我留言哈。

项目依赖

{
  "name": "client",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@radix-ui/react-slot": "^1.1.0",
    "class-variance-authority": "^0.7.0",
    "clsx": "^2.1.1",
    "lucide-react": "^0.395.0",
    "next": "14.2.4",
    "react": "^18",
    "react-dom": "^18",
    "tailwind-merge": "^2.3.0",
    "tailwindcss-animate": "^1.0.7",
    "typeorm": "^0.3.20",
    "mysql2": "^3.10.1",
    "@sass-startup/db": "workspace:*"
  },
  "devDependencies": {
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "eslint": "^8",
    "eslint-config-next": "14.2.4",
    "postcss": "^8",
    "tailwindcss": "^3.4.1",
    "typescript": "^5"
  }
}
{
  "name": "@sass-startup/db",
  "version": "1.0.0",
  "description": "",
  "main": "dist/index.js",
  "scripts": {
    "watch": "tsc --w",
    "build": "tsc"
  },
  "dependencies": {
    "typeorm": "^0.3.20"
  },
  "peerDependencies": {
    "typeorm": "*"
  },
  "devDependencies": {
    "@types/node": "^20.14.2",
    "typescript": "^5.1.3"
  },
  "private": true
}

结束

这个文章搭建了一个nextjs与typeorm结合的前端开发框架,现在这个阶段我们只是调通了引用和封装db的操作。后面会写一下登陆和完善一下用户相关的操作。