使用Next.js、TypeORM和Shadcn-UI从零搭建现代化SaaS系统(第一部分)
项目初始化
我们这里使用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页面,以下页面和业务无关
我们的首页是首先默认是未登录的情况,利于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>
);
}
然后我们查看一下我们的网页,你这个时候会看到数据库的信息了
这个时候查看一下控制台,会看到一堆的waring,我们来修复一下吧。错误如下所示
这个时候我们只需要修改next.config.mjs里面增加externalPackages就可以了
添加如下代码
// apps/client/next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental:{
serverComponentsExternalPackages: ["typeorm"],
},
};
export default nextConfig;
现在我们访问一下首页,看一下控制台,世界瞬间清净了。
在开发阶段我们把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的操作。后面会写一下登陆和完善一下用户相关的操作。