├── .dockerignore ├── public ├── logo.png ├── favicon.ico └── vercel.svg ├── assets └── images │ └── 2.jpeg ├── markdown ├── 第二篇文章.md └── 第一篇博客.md ├── next.config.js ├── custom.d.ts ├── .babelrc ├── pages ├── api │ ├── hello.js │ └── v1 │ │ ├── posts.tsx │ │ ├── sessions.tsx │ │ ├── users.tsx │ │ └── posts │ │ └── [id].tsx ├── _app.js ├── posts │ ├── first-post.js │ ├── new.tsx │ ├── [id] │ │ └── edit.tsx │ ├── [id].tsx │ └── index.tsx ├── index.tsx ├── sign_up.tsx └── sign_in.tsx ├── styles └── global.css ├── Dockerfile ├── lib ├── withSession.tsx ├── getDatabaseConnection.tsx └── post.tsx ├── next-env.d.ts ├── bin └── deploy.sh ├── .gitignore ├── ormconfig.simple.json ├── src ├── entity │ ├── Comment.ts │ ├── Post.ts │ └── User.ts ├── migration │ ├── 1592561070934-AddUniqueUsernameToUsers.ts │ ├── 1592201997493-CreateUsers.ts │ ├── 1592205680555-CreatePosts.ts │ ├── 1592206097026-CreateComments.ts │ ├── 1592208800876-RenameColumns.ts │ └── 1592206406201-AddCreatedAtAndUpdateAt.ts ├── seed.ts └── model │ └── SignIn.ts ├── hooks ├── usePosts.tsx ├── usePager.tsx └── useForm.tsx ├── tsconfig.json ├── nginx.conf ├── migrate.patch ├── package.json ├── README.md ├── dist ├── seed.js ├── migration │ ├── 1592561070934-AddUniqueUsernameToUsers.js │ ├── 1592208800876-RenameColumns.js │ ├── 1592201997493-CreateUsers.js │ ├── 1592205680555-CreatePosts.js │ ├── 1592206097026-CreateComments.js │ └── 1592206406201-AddCreatedAtAndUpdateAt.js ├── entity │ ├── Comment.js │ ├── Post.js │ └── User.js └── model │ └── SignIn.js └── study.md /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maricaya/nextjs-blog/HEAD/public/logo.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maricaya/nextjs-blog/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /assets/images/2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maricaya/nextjs-blog/HEAD/assets/images/2.jpeg -------------------------------------------------------------------------------- /markdown/第二篇文章.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 第二篇博客 3 | date: 2020-5-30 4 | --- 5 | 6 | 123 7 | 111111 8 | 1234566 9 | -------------------------------------------------------------------------------- /markdown/第一篇博客.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 第一篇博客 3 | date: 2020-5-30 4 | --- 5 | 6 | 123 7 | 1111111 8 | 1234566 9 | 文字文字 10 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const withImages = require('next-images') 2 | 3 | module.exports = withImages({ 4 | webpack(config, options) { 5 | return config 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /custom.d.ts: -------------------------------------------------------------------------------- 1 | type Post = { 2 | id: string; 3 | date: string; 4 | title: string; 5 | content: string; 6 | htmlContent: string; 7 | } 8 | 9 | type User = { 10 | id: string; 11 | } 12 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": [ 4 | [ 5 | "@babel/plugin-proposal-decorators", 6 | { 7 | "legacy": true 8 | } 9 | ] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /pages/api/hello.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default (req, res) => { 4 | res.statusCode = 200 5 | res.json({ name: 'John Doe' }) 6 | } 7 | -------------------------------------------------------------------------------- /styles/global.css: -------------------------------------------------------------------------------- 1 | * {margin: 0;padding: 0;box-sizing: border-box;} 2 | *::after, *::before {box-sizing: border-box;} 3 | a {text-decoration: none; color: #00adb5; border-bottom: 1px solid;} 4 | a, input, button {font: inherit;} 5 | h1 {margin: 8px 0;} 6 | p {margin: 4px 0;} 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12 2 | WORKDIR /usr/src/app 3 | COPY package.json ./ 4 | COPY yarn.lock ./ 5 | 6 | RUN yarn install 7 | COPY . . 8 | EXPOSE 3000 9 | CMD ["yarn", "start"] 10 | 11 | # docker build -t sunnyla/node-web-app . 12 | # docker run -p 3000:3000 -d sunnyla/node-web-app 13 | -------------------------------------------------------------------------------- /lib/withSession.tsx: -------------------------------------------------------------------------------- 1 | import {withIronSession} from 'next-iron-session'; 2 | import {GetServerSideProps, NextApiHandler} from 'next'; 3 | 4 | export function withSession(handler: NextApiHandler | GetServerSideProps) { 5 | return withIronSession(handler, { 6 | password: process.env.SECRET, 7 | cookieName: 'blog', 8 | cookieOptions: {secure: false} 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | import * as next from 'next' 4 | 5 | declare module "*.png" { 6 | const value: string; 7 | export default value; 8 | } 9 | 10 | declare module 'next' { 11 | import {Session} from 'next-iron-session'; 12 | 13 | interface NextApiRequest { 14 | session: Session 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import 'styles/global.css' 3 | import 'github-markdown-css' 4 | 5 | export default function App({ Component, pageProps }) { 6 | return
7 | 8 | 第一篇文章 9 | 11 | 12 | 13 |
14 | } 15 | -------------------------------------------------------------------------------- /bin/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | docker start 54f && 3 | cd /home/blog/app/nextjs-blog/ && 4 | git pull && 5 | yarn install --production=false && 6 | yarn build && 7 | git apply migrate.patch; 8 | yarn compile && 9 | yarn m:run && 10 | git reset --hard HEAD && 11 | docker build -t sunnyla/node-web-app . && 12 | docker kill app && 13 | docker rm app && 14 | #:--network=host 会导致端口映射失效,端口直接就是阿里云机器的端口,但这种模式比较容易理解 15 | docker run --name app --network=host -p 3000:3000 -d sunnyla/node-web-app && 16 | echo "ok!" 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | /blog-data/ 3 | # dependencies 4 | /node_modules 5 | /.idea/ 6 | /.vscode/ 7 | /.pnp 8 | .pnp.js 9 | */.env.local 10 | ormconfig.json 11 | em.sh 12 | # testing 13 | /coverage 14 | 15 | # next.js 16 | /.next/ 17 | /out/ 18 | 19 | # production 20 | /build 21 | 22 | # misc 23 | .DS_Store 24 | 25 | # debug 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | # local env files 31 | .env.local 32 | .env.development.local 33 | .env.test.local 34 | .env.production.local 35 | -------------------------------------------------------------------------------- /ormconfig.simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "postgres", 3 | "host": "localhost", 4 | "port": 5432, 5 | "username": "blog", 6 | "password": "", 7 | "database": "blog_development", 8 | "synchronize": false, 9 | "logging": false, 10 | "entities": [ 11 | "dist/entity/**/*.js" 12 | ], 13 | "migrations": [ 14 | "dist/migration/**/*.js" 15 | ], 16 | "subscribers": [ 17 | "dist/subscriber/**/*.js" 18 | ], 19 | "cli": { 20 | "entitiesDir": "src/entity", 21 | "migrationsDir": "src/migration", 22 | "subscribersDir": "src/subscriber" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/entity/Comment.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | CreateDateColumn, 4 | Entity, 5 | ManyToOne, 6 | PrimaryGeneratedColumn, 7 | UpdateDateColumn 8 | } from "typeorm"; 9 | import {User} from "./User"; 10 | import {Post} from "./Post"; 11 | 12 | @Entity('comments') 13 | export class Comment { 14 | @PrimaryGeneratedColumn('increment') 15 | id: number; 16 | @Column('text') 17 | content: string; 18 | @CreateDateColumn() 19 | createdAt: Date; 20 | @UpdateDateColumn() 21 | updateAt: Date; 22 | @ManyToOne('User', 'comments') 23 | user: User; 24 | @ManyToOne('Post', 'comments') 25 | post: Post; 26 | } 27 | -------------------------------------------------------------------------------- /src/migration/1592561070934-AddUniqueUsernameToUsers.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner, TableIndex} from "typeorm"; 2 | 3 | export class AddUniqueUsernameToUsers1592561070934 implements MigrationInterface { 4 | 5 | public async up(queryRunner: QueryRunner): Promise { 6 | await queryRunner.createIndex('users', 7 | new TableIndex({ 8 | name: 'users_username', columnNames: ['username'], 9 | isUnique: true 10 | })) 11 | } 12 | 13 | public async down(queryRunner: QueryRunner): Promise { 14 | await queryRunner.dropIndex('users', 'users_username'); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /hooks/usePosts.tsx: -------------------------------------------------------------------------------- 1 | import {useEffect, useState} from 'react'; 2 | import axios from 'axios'; 3 | 4 | export const usePosts = () => { 5 | const [posts, setPosts] = useState([]); 6 | const [isLoading, setIsLoading] = useState(false); 7 | const [isEmpty, setIsEmpty] = useState(false); 8 | useEffect(() => { 9 | setIsLoading(true); 10 | axios.get('/api/v1/posts').then(response => { 11 | setPosts(response.data); 12 | setIsLoading(false); 13 | if (response.data.length === 0) { 14 | setIsEmpty(true); 15 | } 16 | }, () => { 17 | setIsLoading(false); 18 | }); 19 | }, []); 20 | return {posts, setPosts, isLoading, setIsLoading, isEmpty, setIsEmpty}; 21 | }; 22 | -------------------------------------------------------------------------------- /pages/posts/first-post.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | import Head from 'next/head' 4 | 5 | export default function FirstPost() { 6 | return ( 7 | 8 | 9 | 第一篇文章 10 | 12 | 13 |
first post 14 |
15 | 回到首页 16 | 17 | 点击这里 18 | 19 |
20 |
21 | ) 22 | } -------------------------------------------------------------------------------- /src/entity/Post.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | CreateDateColumn, 4 | Entity, 5 | ManyToOne, 6 | OneToMany, 7 | PrimaryGeneratedColumn, 8 | UpdateDateColumn 9 | } from "typeorm"; 10 | import {User} from "./User"; 11 | import {Comment} from "./Comment"; 12 | 13 | @Entity('posts') 14 | export class Post { 15 | @PrimaryGeneratedColumn('increment') 16 | id: number; 17 | @Column('varchar') 18 | title: string; 19 | @Column('text') 20 | content: string; 21 | authorId: number; 22 | @CreateDateColumn() 23 | createdAt: Date; 24 | @UpdateDateColumn() 25 | updateAt: Date; 26 | @ManyToOne('User', 'posts') 27 | author: User; 28 | @OneToMany('Comment', 'post') 29 | comments: Comment[]; 30 | } 31 | -------------------------------------------------------------------------------- /src/migration/1592201997493-CreateUsers.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner, Table} from "typeorm"; 2 | 3 | export class CreateUsers1592201997493 implements MigrationInterface { 4 | 5 | public async up(queryRunner: QueryRunner): Promise { 6 | return await queryRunner.createTable(new Table({ 7 | name: 'users', 8 | columns: [ 9 | {name: 'id', isGenerated: true, type: 'int', generationStrategy: 'increment', isPrimary: true}, 10 | {name: 'username', type: 'varchar'}, 11 | {name: 'passwordDigest', type: 'varchar'} 12 | ] 13 | })) 14 | } 15 | 16 | public async down(queryRunner: QueryRunner): Promise { 17 | return await queryRunner.dropTable('users'); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "emitDecoratorMetadata": true, 4 | "experimentalDecorators": true, 5 | "noImplicitAny": true, 6 | "baseUrl": ".", 7 | "target": "es5", 8 | "module": "esnext", 9 | "strict": false, 10 | "esModuleInterop": true, 11 | "lib": [ 12 | "dom", 13 | "dom.iterable", 14 | "esnext" 15 | ], 16 | "allowJs": true, 17 | "skipLibCheck": true, 18 | "forceConsistentCasingInFileNames": true, 19 | "noEmit": true, 20 | "moduleResolution": "node", 21 | "resolveJsonModule": true, 22 | "isolatedModules": true, 23 | "jsx": "preserve" 24 | }, 25 | "exclude": [ 26 | "node_modules" 27 | ], 28 | "include": [ 29 | "next-env.d.ts", 30 | "**/*.ts", 31 | "**/*.tsx" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /pages/api/v1/posts.tsx: -------------------------------------------------------------------------------- 1 | import {NextApiHandler} from 'next'; 2 | import {getDatabaseConnection} from '../../../lib/getDatabaseConnection'; 3 | import {Post} from 'src/entity/Post'; 4 | import {withSession} from '../../../lib/withSession'; 5 | 6 | const Posts: NextApiHandler = withSession(async (req, res) => { 7 | if (req.method === 'POST') { 8 | const {title, content} = req.body; 9 | const post = new Post(); 10 | post.title = title; 11 | post.content = content; 12 | const user = req.session.get('currentUser'); 13 | if (!user) { 14 | res.statusCode = 401; 15 | res.end(); 16 | return; 17 | } 18 | post.author = user; 19 | const connection = await getDatabaseConnection(); 20 | await connection.manager.save(post); 21 | res.json(post); 22 | } 23 | }); 24 | 25 | export default Posts; 26 | -------------------------------------------------------------------------------- /pages/api/v1/sessions.tsx: -------------------------------------------------------------------------------- 1 | import {NextApiHandler} from 'next'; 2 | import {SignIn} from '../../../src/model/SignIn'; 3 | import {withSession} from '../../../lib/withSession'; 4 | 5 | const Sessions: NextApiHandler = async (req, res) => { 6 | const {username, password} = req.body; 7 | res.setHeader('Content-Type', 'application/json; charset=utf-8'); 8 | const signIn = new SignIn(); 9 | signIn.username = username; 10 | signIn.password = password; 11 | await signIn.validate(); 12 | if (signIn.hasErrors()) { 13 | res.statusCode = 422; 14 | res.end(JSON.stringify(signIn.errors)); 15 | } else { 16 | req.session.set('currentUser', signIn.user); 17 | await req.session.save(); 18 | res.statusCode = 200; 19 | res.end(JSON.stringify(signIn.user)); 20 | } 21 | }; 22 | 23 | export default withSession(Sessions); 24 | -------------------------------------------------------------------------------- /src/migration/1592205680555-CreatePosts.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner, Table} from "typeorm"; 2 | 3 | export class CreatePosts1592205680555 implements MigrationInterface { 4 | 5 | public async up(queryRunner: QueryRunner): Promise { 6 | return await queryRunner.createTable(new Table({ 7 | name: 'posts', 8 | columns: [ 9 | {name: 'id', isGenerated: true, type: 'int', generationStrategy: 'increment', isPrimary: true}, 10 | {name: 'title', type: 'varchar'}, 11 | {name: 'content', type: 'text'}, 12 | {name: 'author_id', type: 'int'} 13 | ] 14 | })) 15 | } 16 | 17 | public async down(queryRunner: QueryRunner): Promise { 18 | return await queryRunner.dropTable('posts'); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/migration/1592206097026-CreateComments.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner, Table} from "typeorm"; 2 | 3 | export class CreateComments1592206097026 implements MigrationInterface { 4 | 5 | public async up(queryRunner: QueryRunner): Promise { 6 | return await queryRunner.createTable(new Table({ 7 | name: 'comments', 8 | columns: [ 9 | {name: 'id', isGenerated: true, type: 'int', generationStrategy: 'increment', isPrimary: true}, 10 | {name: 'user_id', type: 'int'}, 11 | {name: 'post_id', type: 'int'}, 12 | {name: 'content', type: 'text'} 13 | ] 14 | })) 15 | } 16 | 17 | public async down(queryRunner: QueryRunner): Promise { 18 | return await queryRunner.dropTable('comments'); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen [::]:80; 4 | server_name localhost; 5 | gzip on; 6 | gzip_disable "msie6"; 7 | 8 | gzip_comp_level 6; 9 | gzip_min_length 1100; 10 | gzip_buffers 16 8k; 11 | gzip_proxied any; 12 | gzip_types 13 | text/plain 14 | text/css 15 | text/js 16 | text/xml 17 | text/javascript 18 | application/javascript 19 | application/x-javascript 20 | application/json 21 | application/xml 22 | application/rss+xml 23 | image/svg+xml/javascript; 24 | location ~ ^/nextjs-blog/_next/static/ { 25 | root /usr/share/nginx/html/; 26 | expires 30d; 27 | } 28 | 29 | location / { 30 | proxy_pass http://0.0.0.0:3000; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/seed.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import {createConnection} from "typeorm"; 3 | import {User} from "./entity/User"; 4 | import {Post} from "./entity/Post"; 5 | import {Comment} from "./entity/Comment"; 6 | 7 | createConnection().then(async connection => { 8 | const {manager} = connection; 9 | // 创建 user1 10 | const u1 = new User(); 11 | u1.username = 'frank'; 12 | u1.passwordDigest = 'xxx'; 13 | await manager.save(u1); 14 | // // 创建 post 1 15 | const p1 = new Post(); 16 | p1.title = 'Post 1'; 17 | p1.content = 'My First Post'; 18 | p1.author = u1; 19 | await manager.save(p1); 20 | const c1 = new Comment(); 21 | c1.user = u1; 22 | c1.post = p1; 23 | c1.content = 'Awesome!'; 24 | await manager.save(c1); 25 | await connection.close(); 26 | console.log('OK!'); 27 | }) 28 | .catch(error => console.log(error)); 29 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import {NextPage} from 'next'; 2 | import * as React from 'react'; 3 | import Link from 'next/link'; 4 | 5 | const Home: NextPage = () => { 6 | return ( 7 | <> 8 |
9 | 10 |

Marica 的个人博客

11 |

我是一个热爱编程的人

12 |

13 | 14 | 文章列表 15 | 16 |

17 |
18 | 33 | 34 | ); 35 | }; 36 | 37 | export default Home; 38 | -------------------------------------------------------------------------------- /pages/api/v1/users.tsx: -------------------------------------------------------------------------------- 1 | import {NextApiHandler} from 'next'; 2 | import {getDatabaseConnection} from '../../../lib/getDatabaseConnection'; 3 | import {User} from 'src/entity/User'; 4 | 5 | const Users: NextApiHandler = async (req, res) => { 6 | const {username, password, passwordConfirmation} = req.body; 7 | const connection = await getDatabaseConnection(); // 第一次链接能不能用 get 8 | 9 | const user = new User(); 10 | user.username = username.trim(); 11 | user.username = username; 12 | user.password = password; 13 | user.passwordConfirmation = passwordConfirmation; 14 | 15 | await user.validate(); 16 | 17 | if (user.hasErrors()) { 18 | res.statusCode = 422; 19 | res.write(JSON.stringify(user.errors)); 20 | } else { 21 | await connection.manager.save(user); 22 | res.statusCode = 200; 23 | res.write(JSON.stringify(user)); 24 | } 25 | res.end(); 26 | }; 27 | 28 | export default Users; 29 | -------------------------------------------------------------------------------- /src/model/SignIn.ts: -------------------------------------------------------------------------------- 1 | import {getDatabaseConnection} from '../../lib/getDatabaseConnection'; 2 | import {User} from '../entity/User'; 3 | import md5 from 'md5'; 4 | 5 | export class SignIn { 6 | username: string; 7 | password: string; 8 | user: User; 9 | 10 | errors = { 11 | username: [] as string[], password: [] as string[] 12 | } 13 | async validate() { 14 | if (this.username.trim() === '') { 15 | this.errors.username.push('请填写用户名'); 16 | } 17 | const connection = await getDatabaseConnection(); 18 | const user = await connection.manager.findOne(User, {where: {username: this.username}}) 19 | this.user = user; 20 | if (user) { 21 | if (user.passwordDigest === md5(this.password)) { 22 | this.errors.password.push('密码与用户名不匹配'); 23 | } 24 | } else { 25 | this.errors.username.push('用户名不存在'); 26 | } 27 | } 28 | hasErrors() { 29 | return !!Object.values(this.errors).find(v => v.length > 0); 30 | } 31 | } -------------------------------------------------------------------------------- /pages/sign_up.tsx: -------------------------------------------------------------------------------- 1 | import {NextPage} from "next"; 2 | import * as React from "react"; 3 | import axios from 'axios'; 4 | import {useForm} from '../hooks/useForm'; 5 | 6 | const signUp: NextPage = () => { 7 | const {form} = useForm({ 8 | initFormData: { username: '', 9 | password: '', 10 | passwordConfirmation: ''}, 11 | fields: [ 12 | {label: '用户名', type: 'text', key: 'username'}, 13 | {label: '密码', type: 'password', key: 'password'}, 14 | {label: '确认密码', type: 'password', key: 'passwordConfirmation'} 15 | ], 16 | buttons: , 17 | submit: { 18 | request: formData => axios.post(`/api/v1/users`, formData), 19 | success: () => window.alert('注册成功') 20 | } 21 | }); 22 | return ( 23 | <> 24 |

注册

25 | {form} 26 | 27 | ) 28 | }; 29 | 30 | export default signUp; 31 | -------------------------------------------------------------------------------- /src/migration/1592208800876-RenameColumns.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner} from "typeorm"; 2 | 3 | export class RenameColumns1592208800876 implements MigrationInterface { 4 | public async up(queryRunner: QueryRunner): Promise { 5 | // await queryRunner.renameColumn('users', 'password_digest', 'passwordDigest'); 6 | await queryRunner.renameColumn('posts', 'author_id', 'authorId'); 7 | await queryRunner.renameColumn('comments', 'user_id', 'userId'); 8 | await queryRunner.renameColumn('comments', 'post_id', 'postId'); 9 | } 10 | 11 | public async down(queryRunner: QueryRunner): Promise { 12 | // await queryRunner.renameColumn('users', 'passwordDigest', 'password_digest'); 13 | await queryRunner.renameColumn('posts', 'authorId', 'author_id'); 14 | await queryRunner.renameColumn('comments', 'userId', 'user_id'); 15 | await queryRunner.renameColumn('comments', 'postId', 'post_id'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/getDatabaseConnection.tsx: -------------------------------------------------------------------------------- 1 | import {createConnection, getConnectionManager} from 'typeorm'; 2 | import 'reflect-metadata'; 3 | import {Post} from 'src/entity/Post'; 4 | import {User} from 'src/entity/User'; 5 | import {Comment} from 'src/entity/Comment'; 6 | import config from '../ormconfig.json'; 7 | 8 | const create = async () => { 9 | // @ts-ignore 10 | return createConnection({ 11 | ...config, 12 | host: process.env.NODE_ENV === 'production' ? 'localhost' : config.host, 13 | database: process.env.NODE_ENV === 'production' ? 'blog_production' : 'blog_development', 14 | entities: [Post, User, Comment] 15 | }); 16 | }; 17 | 18 | const promise = (async function () { 19 | const manager = getConnectionManager(); 20 | const current = manager.has('default') && manager.get('default'); 21 | if (current) {await current.close();} 22 | return create(); 23 | })(); 24 | 25 | export const getDatabaseConnection = async () => { 26 | return promise; 27 | }; 28 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /pages/api/v1/posts/[id].tsx: -------------------------------------------------------------------------------- 1 | import {NextApiHandler} from 'next'; 2 | import {Post} from 'src/entity/Post'; 3 | import {withSession} from '../../../../lib/withSession'; 4 | import {getDatabaseConnection} from 'lib/getDatabaseConnection'; 5 | 6 | const Posts: NextApiHandler = withSession(async (req, res) => { 7 | if (req.method === 'PATCH') { 8 | const {title, content, id} = req.body; 9 | const connection = await getDatabaseConnection(); 10 | const post = await connection.manager.findOne('Post', id); 11 | post.title = title; 12 | post.content = content; 13 | const user = req.session.get('currentUser'); 14 | if (!user) { 15 | res.statusCode = 401; 16 | res.end(); 17 | return; 18 | } 19 | post.author = user; 20 | await connection.manager.save(post); 21 | res.json(post); 22 | } else if (req.method === 'DELETE') { 23 | const id = req.query.id.toString(); 24 | const connection = await getDatabaseConnection(); 25 | const result = await connection.manager.delete('Post', id); 26 | res.statusCode = result.affected >= 0 ? 200 : 400; 27 | res.end(); 28 | } 29 | }); 30 | 31 | export default Posts; 32 | -------------------------------------------------------------------------------- /lib/post.tsx: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs, {promises as fsPromise} from 'fs'; 3 | import matter from 'gray-matter'; 4 | import marked from 'marked'; 5 | 6 | 7 | const markdownDir = path.join(process.cwd(), 'markdown'); 8 | export const getPosts = async () => { 9 | const fileNames = await fsPromise.readdir(markdownDir); 10 | const posts = fileNames.map(fileName => { 11 | const fullPath = path.join(markdownDir, fileName); 12 | const id = fileName.replace(/\.md$/g, ''); 13 | const text = fs.readFileSync(fullPath, 'utf8'); 14 | const {data: {title, date}} = matter(text); 15 | return { 16 | id, title, date 17 | }; 18 | }); 19 | return posts; 20 | }; 21 | 22 | export const getPost = async (id: string) => { 23 | const fullPath = path.join(markdownDir, `${id}.md`); 24 | const text = fs.readFileSync(fullPath, 'utf8'); 25 | const {data: {title, date}, content} = matter(text); 26 | const htmlContent = marked(content); 27 | 28 | return { 29 | id, title, date, content, htmlContent 30 | }; 31 | }; 32 | 33 | export const getPostIds = async () => { 34 | const fileNames = await fsPromise.readdir(markdownDir); 35 | return fileNames.map(item => item.replace(/\.md$/g, '')); 36 | }; 37 | -------------------------------------------------------------------------------- /migrate.patch: -------------------------------------------------------------------------------- 1 | diff --git a/src/entity/User.ts b/src/entity/User.ts 2 | index 59ccec2..116ce7a 100644 3 | --- a/src/entity/User.ts 4 | +++ b/src/entity/User.ts 5 | @@ -8,7 +8,7 @@ import { 6 | } from 'typeorm'; 7 | import {Post} from './Post'; 8 | import {Comment} from './Comment'; 9 | -import {getDatabaseConnection} from '../../lib/getDatabaseConnection'; 10 | +// import {getDatabaseConnection} from '../../lib/getDatabaseConnection'; 11 | import md5 from 'md5'; 12 | import _ from 'lodash'; 13 | 14 | @@ -49,11 +49,11 @@ export class User { 15 | if (this.username.trim().length <= 3) { 16 | this.errors.username.push('太短'); 17 | } 18 | - const found = await (await getDatabaseConnection()).manager.find( 19 | - User, {username: this.username}); 20 | - if (found.length > 0) { 21 | - this.errors.username.push('已存在,不能重复注册'); 22 | - } 23 | + // const found = await (await getDatabaseConnection()).manager.find( 24 | + // User, {username: this.username}); 25 | + // if (found.length > 0) { 26 | + // this.errors.username.push('已存在,不能重复注册'); 27 | + // } 28 | if (this.password === '') { 29 | this.errors.password.push('不能为空'); 30 | } 31 | -------------------------------------------------------------------------------- /pages/posts/new.tsx: -------------------------------------------------------------------------------- 1 | import {NextPage} from 'next'; 2 | import * as React from 'react'; 3 | import axios from 'axios'; 4 | import {useForm} from '../../hooks/useForm'; 5 | 6 | const PostsNew: NextPage = () => { 7 | // 类型是静态分析,不受代码顺序影响 8 | const {form} = useForm({ 9 | initFormData: {title: '', content: ''}, 10 | fields: [ 11 | {label: '标题', type: 'text', key: 'title'}, 12 | {label: '内容', type: 'textarea', key: 'content'} 13 | ], 14 | buttons:
15 | 16 |
, 17 | submit: { 18 | request: formData => axios.post(`/api/v1/posts`, formData), 19 | success: () => { 20 | window.alert('提交成功'); 21 | window.location.href = '/posts'; 22 | } 23 | } 24 | }); 25 | return ( 26 |
27 |
28 | {form} 29 |
30 | 48 |
49 | ); 50 | }; 51 | export default PostsNew; 52 | -------------------------------------------------------------------------------- /src/migration/1592206406201-AddCreatedAtAndUpdateAt.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner, TableColumn} from "typeorm"; 2 | 3 | export class AddCreatedAtAndUpdateAt1592206406201 implements MigrationInterface { 4 | 5 | public async up(queryRunner: QueryRunner): Promise { 6 | await queryRunner.addColumns('users', [ 7 | new TableColumn({name: 'createdAt', type: 'time', isNullable: false, default: 'now()'}), 8 | new TableColumn({name: 'updateAt', type: 'time', isNullable: false, default: 'now()'}) 9 | ]); 10 | await queryRunner.addColumns('posts', [ 11 | new TableColumn({name: 'createdAt', type: 'time', isNullable: false, default: 'now()'}), 12 | new TableColumn({name: 'updateAt', type: 'time', isNullable: false, default: 'now()'}) 13 | ]); 14 | await queryRunner.addColumns('comments', [ 15 | new TableColumn({name: 'createdAt', type: 'time', isNullable: false, default: 'now()'}), 16 | new TableColumn({name: 'updateAt', type: 'time', isNullable: false, default: 'now()'}) 17 | ]) 18 | } 19 | 20 | public async down(queryRunner: QueryRunner): Promise { 21 | await queryRunner.dropColumn('users', 'createdAt'); 22 | await queryRunner.dropColumn('users', 'updateAt'); 23 | await queryRunner.dropColumn('posts', 'createdAt'); 24 | await queryRunner.dropColumn('posts', 'updateAt'); 25 | await queryRunner.dropColumn('comments', 'createdAt'); 26 | await queryRunner.dropColumn('comments', 'updateAt'); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /pages/sign_in.tsx: -------------------------------------------------------------------------------- 1 | import {GetServerSideProps, GetServerSidePropsContext, NextPage} from 'next'; 2 | import * as React from "react"; 3 | import axios from 'axios' 4 | import {withSession} from '../lib/withSession'; 5 | import {User} from '../src/entity/User'; 6 | import {useForm} from '../hooks/useForm'; 7 | import qs from 'querystring'; 8 | 9 | const SignIn: NextPage<{ user: User }> = (props) => { 10 | const {form} = useForm({ 11 | initFormData: { username: '', password: ''}, 12 | fields: [ 13 | {label: '用户名', type: 'text', key: 'username'}, 14 | {label: '密码', type: 'password', key: 'password'} 15 | ], 16 | buttons: , 17 | submit: { 18 | request: formData => 19 | axios.post(`/api/v1/sessions`, formData), 20 | success: () => { 21 | window.alert('登录成功'); 22 | const query = qs.parse(window.location.search.substr(1)); 23 | window.location.href = query.returnTo?.toString() || '/'; 24 | } 25 | } 26 | }); 27 | return ( 28 | <> 29 | {props.user &&
当前登录用户为 {props.user.username}
} 30 |

登录

31 | {form} 32 | 33 | ); 34 | }; 35 | 36 | export default SignIn; 37 | 38 | export const getServerSideProps: GetServerSideProps = withSession(async (context: GetServerSidePropsContext) => { 39 | // @ts-ignore 40 | const user = context.req.session.get('currentUser') || ''; 41 | return { 42 | props: { 43 | user: JSON.parse(JSON.stringify(user)) 44 | } 45 | }; 46 | }); 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-blog", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "concurrently \"next dev\" \"yarn compile:watch\"", 7 | "compile:watch": "babel -w ./src --out-dir dist --extensions .ts,.tsx", 8 | "compile": "babel ./src --out-dir dist --extensions .ts,.tsx", 9 | "build": "next build", 10 | "start": "next start", 11 | "m:create": "typeorm migration:create", 12 | "m:run": "typeorm migration:run", 13 | "m:revert": "typeorm migration:revert", 14 | "e:create": "typeorm entity:create" 15 | }, 16 | "dependencies": { 17 | "@babel/cli": "^7.10.1", 18 | "@types/axios": "^0.14.0", 19 | "axios": "^0.19.2", 20 | "classnames": "^2.2.6", 21 | "github-markdown-css": "^4.0.0", 22 | "gray-matter": "^4.0.2", 23 | "lodash": "^4.17.15", 24 | "marked": "^1.1.0", 25 | "md5": "^2.2.1", 26 | "next": "9.4.1", 27 | "next-images": "^1.4.0", 28 | "next-iron-session": "^4.1.7", 29 | "pg": "^8.2.1", 30 | "react": "16.13.1", 31 | "react-dom": "16.13.1", 32 | "reflect-metadata": "^0.1.13", 33 | "typeorm": "^0.2.25", 34 | "ua-parser-js": "^0.7.21" 35 | }, 36 | "devDependencies": { 37 | "@babel/plugin-proposal-decorators": "^7.10.1", 38 | "@types/classnames": "^2.2.10", 39 | "@types/lodash": "^4.14.155", 40 | "@types/marked": "^0.7.4", 41 | "@types/md5": "^2.2.0", 42 | "@types/node": "^14.0.11", 43 | "@types/react": "^16.9.35", 44 | "@types/react-dom": "^16.9.8", 45 | "@types/ua-parser-js": "^0.7.33", 46 | "concurrently": "^5.2.0", 47 | "file-loader": "^6.0.0", 48 | "typescript": "^3.9.3" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 项目创建过程 2 | - [Next.js + TypeScript 入门之项目搭建、三种渲染方式(BSR、SSG、SSR)](https://juejin.im/post/6855917901090652174) 3 | - [TS + TypeORM 踩坑实践 (一) hello ORM](https://juejin.im/post/6857391336929263624) 4 | - [TS + TypeORM 踩坑实践 (二) 操作数据表](https://juejin.im/post/6858509402798817294) 5 | - [从 0 开始部署你的 Node 应用(Ubantu、docker、nginx)](https://juejin.im/post/6864785804066029575) 6 | 7 | # 代码使用 8 | 9 | 请下载本代码,然后用 WebStorm 或者 VSCode 打开。 10 | 11 | ## 启动数据库 12 | 13 | 如果你没有创建过数据库,请运行 14 | ```bash 15 | mkdir blog-data 16 | docker run -v "$PWD/blog-data":/var/lib/postgresql/data -p 5432:5432 -e POSTGRES_USER=blog -e POSTGRES_HOST_AUTH_METHOD=trust -d postgres:12.2 17 | 18 | 或者旧版 Windows Docker 客户端运行下面的代码 19 | 20 | docker run -v "blog-data":/var/lib/postgresql/data -p 5432:5432 -e POSTGRES_USER=blog -e POSTGRES_HOST_AUTH_METHOD=trust -d postgres:12.2 21 | ``` 22 | 23 | 如果你创建过数据库,请运行 24 | 25 | ```bash 26 | docker ps -a 27 | docker restart 容器id 28 | ``` 29 | 30 | ## 创建数据库 31 | 32 | ``` 33 | docker exec -it bash 34 | psql -U blog 35 | CREATE DATABASE blog_development ENCODING 'UTF8' LC_COLLATE 'en_US.utf8' LC_CTYPE 'en_US.utf8'; 36 | ``` 37 | 38 | ## 数据表 39 | 40 | 首先修改 ormconfig.json 中的 host,然后运行 41 | 42 | ``` 43 | yarn m:run 44 | ``` 45 | 46 | ## 开发 47 | 48 | ```bash 49 | yarn dev 50 | # or 51 | npm run dev 52 | ``` 53 | 54 | ## 部署 55 | 56 | ```bash 57 | # 执行本地脚本 58 | git push 59 | ssh blog@dev1 'bash -s' < bin/deploy.sh 60 | # 执行服务器上脚本 61 | ssh blog@dev1 'sh /home/blog/app/nextjs-blog/bin/deploy.sh' 62 | ``` 63 | 64 | ## NGINX 配置 65 | ```bash 66 | docker run --name nginx1 --network=host -v /home/blog/nginx.conf:/etc/nginx/conf.d/default.conf -v /home/blog/app/nextjs-blog/.next/static/:/usr/share/nginx/html/_next/static/ -d nginx:1.19.1 67 | ``` 68 | -------------------------------------------------------------------------------- /hooks/usePager.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import * as React from 'react'; 3 | import _ from 'lodash'; 4 | 5 | type usePagerOptions = { 6 | page: number, 7 | totalPage: number, 8 | urlMaker?: (n: number) => string 9 | } 10 | const defaultUrlMaker = (n:number) => `?page=${n}`; 11 | 12 | export const usePager = (options: usePagerOptions) => { 13 | const {page, totalPage, urlMaker: _urlMaker} = options; 14 | const urlMaker = _urlMaker || defaultUrlMaker; 15 | const numbers = []; 16 | numbers.push(1); 17 | for (let i = page - 3; i <= page + 3; i++) { 18 | numbers.push(i); 19 | } 20 | numbers.push(totalPage); 21 | const pageNumbers = _.uniq(numbers).sort().filter(n => n >= 1 && n <= totalPage).reduce((result, n) => n - (result[result.length - 1] || 0) === 1 ? 22 | result.concat(n) : result.concat(-1, n), []); 23 | 24 | const pager = totalPage > 1 ? ( 25 |
26 | {page !== 1 && 27 | 上一页 28 | } 29 | {pageNumbers.map(n => n === -1 ? 30 | ... : 31 | {n} 32 | )} 33 | 34 | {page < totalPage && 35 | 下一页 36 | } 37 | 第 {page}/{totalPage} 页 38 | 39 | 48 |
49 | ) : null; 50 | return {pager}; 51 | }; 52 | -------------------------------------------------------------------------------- /pages/posts/[id]/edit.tsx: -------------------------------------------------------------------------------- 1 | import {GetServerSideProps, NextPage} from 'next'; 2 | import * as React from 'react'; 3 | import {getDatabaseConnection} from '../../../lib/getDatabaseConnection'; 4 | import {useForm} from 'hooks/useForm'; 5 | import axios from 'axios'; 6 | 7 | type Props = { 8 | id: number; 9 | post: Post; 10 | } 11 | 12 | const PostsEdit: NextPage = (props) => { 13 | const {post, id} = props; 14 | console.log('post'); 15 | console.log(post); 16 | const {form} = useForm({ 17 | initFormData: {title: post.title, content: post.content}, 18 | fields: [ 19 | {label: '大标题', type: 'text', key: 'title'}, 20 | {label: '内容', type: 'textarea', key: 'content'}, 21 | ], 22 | buttons:
23 | 24 |
, 25 | submit: { 26 | request: formData => axios.patch(`/api/v1/posts/${id}`, {...formData, id}), 27 | success: () => { 28 | window.alert('提交成功'); 29 | } 30 | } 31 | }); 32 | return ( 33 |
34 |
35 | {form} 36 |
37 | 55 |
56 | ); 57 | }; 58 | 59 | export default PostsEdit; 60 | 61 | export const getServerSideProps: GetServerSideProps = async (context) => { 62 | const {id} = context.params; 63 | 64 | const connection = await getDatabaseConnection(); 65 | const post = await connection.manager.findOne('Post', context.params.id); 66 | 67 | return { 68 | props: { 69 | id: parseInt(id.toString()), 70 | post: JSON.parse(JSON.stringify(post)) 71 | // currentUser 72 | } 73 | }; 74 | }; 75 | -------------------------------------------------------------------------------- /dist/seed.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); 4 | 5 | var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); 6 | 7 | var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator")); 8 | 9 | require("reflect-metadata"); 10 | 11 | var _typeorm = require("typeorm"); 12 | 13 | var _User = require("./entity/User"); 14 | 15 | var _Post = require("./entity/Post"); 16 | 17 | var _Comment = require("./entity/Comment"); 18 | 19 | (0, _typeorm.createConnection)().then( /*#__PURE__*/function () { 20 | var _ref = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee(connection) { 21 | var manager, u1, p1, c1; 22 | return _regenerator["default"].wrap(function _callee$(_context) { 23 | while (1) { 24 | switch (_context.prev = _context.next) { 25 | case 0: 26 | manager = connection.manager; // 创建 user1 27 | 28 | u1 = new _User.User(); 29 | u1.username = 'frank'; 30 | u1.passwordDigest = 'xxx'; 31 | _context.next = 6; 32 | return manager.save(u1); 33 | 34 | case 6: 35 | // // 创建 post 1 36 | p1 = new _Post.Post(); 37 | p1.title = 'Post 1'; 38 | p1.content = 'My First Post'; 39 | p1.author = u1; 40 | _context.next = 12; 41 | return manager.save(p1); 42 | 43 | case 12: 44 | c1 = new _Comment.Comment(); 45 | c1.user = u1; 46 | c1.post = p1; 47 | c1.content = 'Awesome!'; 48 | _context.next = 18; 49 | return manager.save(c1); 50 | 51 | case 18: 52 | _context.next = 20; 53 | return connection.close(); 54 | 55 | case 20: 56 | console.log('OK!'); 57 | 58 | case 21: 59 | case "end": 60 | return _context.stop(); 61 | } 62 | } 63 | }, _callee); 64 | })); 65 | 66 | return function (_x) { 67 | return _ref.apply(this, arguments); 68 | }; 69 | }())["catch"](function (error) { 70 | return console.log(error); 71 | }); -------------------------------------------------------------------------------- /src/entity/User.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | CreateDateColumn, 4 | UpdateDateColumn, 5 | Entity, 6 | PrimaryGeneratedColumn, 7 | OneToMany, BeforeInsert 8 | } from 'typeorm'; 9 | import {Post} from './Post'; 10 | import {Comment} from './Comment'; 11 | import {getDatabaseConnection} from '../../lib/getDatabaseConnection'; 12 | import md5 from 'md5'; 13 | import _ from 'lodash'; 14 | 15 | @Entity('users') 16 | export class User { 17 | @PrimaryGeneratedColumn('increment') 18 | id: number; 19 | @Column('varchar') 20 | username: string; 21 | @Column('varchar') 22 | passwordDigest: string; 23 | @CreateDateColumn() 24 | createdAt: Date; 25 | @UpdateDateColumn() 26 | updateAt: Date; 27 | @OneToMany('Post', 'author') 28 | posts: Post[]; 29 | @OneToMany('Comment', 'user') 30 | comments: Comment[]; 31 | errors = { 32 | username: [] as string[], 33 | password: [] as string[], 34 | passwordConfirmation: [] as string[] 35 | }; 36 | password: string; 37 | passwordConfirmation: string; 38 | 39 | async validate() { 40 | if (this.username.trim() === '') { 41 | this.errors.username.push('不能为空'); 42 | } 43 | if (!/[a-zA-Z0-9]/.test(this.username.trim())) { 44 | this.errors.username.push('格式不合法'); 45 | } 46 | if (this.username.trim().length > 42) { 47 | this.errors.username.push('太长'); 48 | } 49 | if (this.username.trim().length <= 3) { 50 | this.errors.username.push('太短'); 51 | } 52 | const found = await (await getDatabaseConnection()).manager.find( 53 | User, {username: this.username}); 54 | if (found.length > 0) { 55 | this.errors.username.push('已存在,不能重复注册'); 56 | } 57 | if (this.password === '') { 58 | this.errors.password.push('不能为空'); 59 | } 60 | if (this.password !== this.passwordConfirmation) { 61 | this.errors.passwordConfirmation.push('密码不匹配'); 62 | } 63 | } 64 | 65 | hasErrors() { 66 | return !!Object.values(this.errors).find(v => v.length > 0); 67 | } 68 | 69 | @BeforeInsert() 70 | generatePasswordDigest() { 71 | this.passwordDigest = md5(this.password); 72 | } 73 | 74 | toJSON() { 75 | return _.omit(this, ['password', 'passwordConfirmation', 'passwordDigest', 'errors']); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /pages/posts/[id].tsx: -------------------------------------------------------------------------------- 1 | import React, {useCallback} from 'react'; 2 | import {GetServerSideProps, GetServerSidePropsContext, NextPage} from 'next'; 3 | import {getDatabaseConnection} from '../../lib/getDatabaseConnection'; 4 | import {Post} from '../../src/entity/Post'; 5 | import marked from 'marked'; 6 | import Link from 'next/link'; 7 | import {withSession} from '../../lib/withSession'; 8 | import axios from 'axios'; 9 | import {useRouter} from 'next/router'; 10 | 11 | type Props = { 12 | post: Post, 13 | currentUser: User | null, 14 | id: number 15 | } 16 | 17 | const postsShow: NextPage = (props) => { 18 | const {post, currentUser, id} = props; 19 | const router = useRouter(); 20 | 21 | const onRemove = useCallback(() => { 22 | axios.delete(`/api/v1/posts/${id}`).then(() => { 23 | window.alert('删除成功'); 24 | router.push('/posts') 25 | }, () => { 26 | window.alert('删除失败') 27 | }) 28 | }, [id]); 29 | 30 | return ( 31 | <> 32 |
33 |
34 |

{post.title}

35 | {currentUser && 36 |

37 | 编辑 38 | 删除 39 |

40 | } 41 |
42 |
43 |
44 |
45 | 59 | 60 | ); 61 | }; 62 | export default postsShow; 63 | 64 | export const getServerSideProps: GetServerSideProps = withSession((async (context: GetServerSidePropsContext) => { 65 | const connection = await getDatabaseConnection(); 66 | const id = context.params.id; 67 | const post = await connection.manager.findOne('Post', context.params.id); 68 | 69 | const currentUser = (context.req as any).session.get('currentUser') || null; 70 | 71 | return { 72 | props: { 73 | id: parseInt(id.toString()), 74 | post: JSON.parse(JSON.stringify(post)), 75 | currentUser 76 | } 77 | }; 78 | })); 79 | -------------------------------------------------------------------------------- /dist/migration/1592561070934-AddUniqueUsernameToUsers.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports.AddUniqueUsernameToUsers1592561070934 = void 0; 9 | 10 | var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); 11 | 12 | var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator")); 13 | 14 | var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); 15 | 16 | var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass")); 17 | 18 | var _typeorm = require("typeorm"); 19 | 20 | var AddUniqueUsernameToUsers1592561070934 = /*#__PURE__*/function () { 21 | function AddUniqueUsernameToUsers1592561070934() { 22 | (0, _classCallCheck2["default"])(this, AddUniqueUsernameToUsers1592561070934); 23 | } 24 | 25 | (0, _createClass2["default"])(AddUniqueUsernameToUsers1592561070934, [{ 26 | key: "up", 27 | value: function () { 28 | var _up = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee(queryRunner) { 29 | return _regenerator["default"].wrap(function _callee$(_context) { 30 | while (1) { 31 | switch (_context.prev = _context.next) { 32 | case 0: 33 | _context.next = 2; 34 | return queryRunner.createIndex('users', new _typeorm.TableIndex({ 35 | name: 'users_username', 36 | columnNames: ['username'], 37 | isUnique: true 38 | })); 39 | 40 | case 2: 41 | case "end": 42 | return _context.stop(); 43 | } 44 | } 45 | }, _callee); 46 | })); 47 | 48 | function up(_x) { 49 | return _up.apply(this, arguments); 50 | } 51 | 52 | return up; 53 | }() 54 | }, { 55 | key: "down", 56 | value: function () { 57 | var _down = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee2(queryRunner) { 58 | return _regenerator["default"].wrap(function _callee2$(_context2) { 59 | while (1) { 60 | switch (_context2.prev = _context2.next) { 61 | case 0: 62 | _context2.next = 2; 63 | return queryRunner.dropIndex('users', 'users_username'); 64 | 65 | case 2: 66 | case "end": 67 | return _context2.stop(); 68 | } 69 | } 70 | }, _callee2); 71 | })); 72 | 73 | function down(_x2) { 74 | return _down.apply(this, arguments); 75 | } 76 | 77 | return down; 78 | }() 79 | }]); 80 | return AddUniqueUsernameToUsers1592561070934; 81 | }(); 82 | 83 | exports.AddUniqueUsernameToUsers1592561070934 = AddUniqueUsernameToUsers1592561070934; -------------------------------------------------------------------------------- /dist/entity/Comment.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports.Comment = void 0; 9 | 10 | var _initializerDefineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/initializerDefineProperty")); 11 | 12 | var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); 13 | 14 | var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); 15 | 16 | var _applyDecoratedDescriptor2 = _interopRequireDefault(require("@babel/runtime/helpers/applyDecoratedDescriptor")); 17 | 18 | var _initializerWarningHelper2 = _interopRequireDefault(require("@babel/runtime/helpers/initializerWarningHelper")); 19 | 20 | var _typeorm = require("typeorm"); 21 | 22 | var _dec, _dec2, _dec3, _dec4, _dec5, _dec6, _dec7, _class, _class2, _descriptor, _descriptor2, _descriptor3, _descriptor4, _descriptor5, _descriptor6, _temp; 23 | 24 | var Comment = (_dec = (0, _typeorm.Entity)('comments'), _dec2 = (0, _typeorm.PrimaryGeneratedColumn)('increment'), _dec3 = (0, _typeorm.Column)('text'), _dec4 = (0, _typeorm.CreateDateColumn)(), _dec5 = (0, _typeorm.UpdateDateColumn)(), _dec6 = (0, _typeorm.ManyToOne)('User', 'comments'), _dec7 = (0, _typeorm.ManyToOne)('Post', 'comments'), _dec(_class = (_class2 = (_temp = function Comment() { 25 | (0, _classCallCheck2["default"])(this, Comment); 26 | (0, _initializerDefineProperty2["default"])(this, "id", _descriptor, this); 27 | (0, _initializerDefineProperty2["default"])(this, "content", _descriptor2, this); 28 | (0, _initializerDefineProperty2["default"])(this, "createdAt", _descriptor3, this); 29 | (0, _initializerDefineProperty2["default"])(this, "updateAt", _descriptor4, this); 30 | (0, _initializerDefineProperty2["default"])(this, "user", _descriptor5, this); 31 | (0, _initializerDefineProperty2["default"])(this, "post", _descriptor6, this); 32 | }, _temp), (_descriptor = (0, _applyDecoratedDescriptor2["default"])(_class2.prototype, "id", [_dec2], { 33 | configurable: true, 34 | enumerable: true, 35 | writable: true, 36 | initializer: null 37 | }), _descriptor2 = (0, _applyDecoratedDescriptor2["default"])(_class2.prototype, "content", [_dec3], { 38 | configurable: true, 39 | enumerable: true, 40 | writable: true, 41 | initializer: null 42 | }), _descriptor3 = (0, _applyDecoratedDescriptor2["default"])(_class2.prototype, "createdAt", [_dec4], { 43 | configurable: true, 44 | enumerable: true, 45 | writable: true, 46 | initializer: null 47 | }), _descriptor4 = (0, _applyDecoratedDescriptor2["default"])(_class2.prototype, "updateAt", [_dec5], { 48 | configurable: true, 49 | enumerable: true, 50 | writable: true, 51 | initializer: null 52 | }), _descriptor5 = (0, _applyDecoratedDescriptor2["default"])(_class2.prototype, "user", [_dec6], { 53 | configurable: true, 54 | enumerable: true, 55 | writable: true, 56 | initializer: null 57 | }), _descriptor6 = (0, _applyDecoratedDescriptor2["default"])(_class2.prototype, "post", [_dec7], { 58 | configurable: true, 59 | enumerable: true, 60 | writable: true, 61 | initializer: null 62 | })), _class2)) || _class); 63 | exports.Comment = Comment; -------------------------------------------------------------------------------- /dist/migration/1592208800876-RenameColumns.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports.RenameColumns1592208800876 = void 0; 9 | 10 | var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); 11 | 12 | var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator")); 13 | 14 | var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); 15 | 16 | var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass")); 17 | 18 | var RenameColumns1592208800876 = /*#__PURE__*/function () { 19 | function RenameColumns1592208800876() { 20 | (0, _classCallCheck2["default"])(this, RenameColumns1592208800876); 21 | } 22 | 23 | (0, _createClass2["default"])(RenameColumns1592208800876, [{ 24 | key: "up", 25 | value: function () { 26 | var _up = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee(queryRunner) { 27 | return _regenerator["default"].wrap(function _callee$(_context) { 28 | while (1) { 29 | switch (_context.prev = _context.next) { 30 | case 0: 31 | _context.next = 2; 32 | return queryRunner.renameColumn('posts', 'author_id', 'authorId'); 33 | 34 | case 2: 35 | _context.next = 4; 36 | return queryRunner.renameColumn('comments', 'user_id', 'userId'); 37 | 38 | case 4: 39 | _context.next = 6; 40 | return queryRunner.renameColumn('comments', 'post_id', 'postId'); 41 | 42 | case 6: 43 | case "end": 44 | return _context.stop(); 45 | } 46 | } 47 | }, _callee); 48 | })); 49 | 50 | function up(_x) { 51 | return _up.apply(this, arguments); 52 | } 53 | 54 | return up; 55 | }() 56 | }, { 57 | key: "down", 58 | value: function () { 59 | var _down = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee2(queryRunner) { 60 | return _regenerator["default"].wrap(function _callee2$(_context2) { 61 | while (1) { 62 | switch (_context2.prev = _context2.next) { 63 | case 0: 64 | _context2.next = 2; 65 | return queryRunner.renameColumn('posts', 'authorId', 'author_id'); 66 | 67 | case 2: 68 | _context2.next = 4; 69 | return queryRunner.renameColumn('comments', 'userId', 'user_id'); 70 | 71 | case 4: 72 | _context2.next = 6; 73 | return queryRunner.renameColumn('comments', 'postId', 'post_id'); 74 | 75 | case 6: 76 | case "end": 77 | return _context2.stop(); 78 | } 79 | } 80 | }, _callee2); 81 | })); 82 | 83 | function down(_x2) { 84 | return _down.apply(this, arguments); 85 | } 86 | 87 | return down; 88 | }() 89 | }]); 90 | return RenameColumns1592208800876; 91 | }(); 92 | 93 | exports.RenameColumns1592208800876 = RenameColumns1592208800876; -------------------------------------------------------------------------------- /pages/posts/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Link from 'next/link'; 3 | import {GetServerSideProps, GetServerSidePropsContext, NextPage} from 'next'; 4 | import {Post} from 'src/entity/Post'; 5 | import {getDatabaseConnection} from 'lib/getDatabaseConnection'; 6 | import {UAParser} from 'ua-parser-js'; 7 | import qs from 'querystring'; 8 | import {usePager} from '../../hooks/usePager'; 9 | import {withSession} from '../../lib/withSession'; 10 | 11 | type Props = { 12 | posts: Post[], 13 | count: number, 14 | perPage: number, 15 | page: number, 16 | totalPage: number, 17 | currentUser: User | null 18 | } 19 | 20 | const PostsIndex: NextPage = (props) => { 21 | const {posts, page, totalPage, currentUser} = props; 22 | const {pager} = usePager({ 23 | page, totalPage 24 | }); 25 | return ( 26 | <> 27 |
28 |
29 |

文章列表

30 | { 31 | currentUser && 32 | 新增文章 33 | } 34 |
35 | {posts.map(post => 36 |
37 | 38 | {post.title} 39 | 40 |
41 | )} 42 |
43 | {pager} 44 |
45 |
46 | 72 | 73 | ); 74 | }; 75 | 76 | export default PostsIndex; 77 | 78 | export const getServerSideProps: GetServerSideProps = withSession( 79 | async (context: GetServerSidePropsContext) => { 80 | const index = context.req.url.indexOf('?'); 81 | const search = context.req.url.substr(index + 1); 82 | const query = qs.parse(search); 83 | 84 | const page = parseInt(query.page && query.page.toString()) || 1; 85 | 86 | const currentUser = (context.req as any).session.get('currentUser') || null; 87 | 88 | console.log('环境变量', process.env.SECRET); 89 | 90 | const connection = await getDatabaseConnection(); 91 | const perPage = 10; 92 | const [posts, count] = await connection.manager.findAndCount(Post, { 93 | skip: (page - 1) * perPage, take: perPage 94 | }); 95 | const ua = context.req.headers['user-agent']; 96 | 97 | const result = new UAParser(ua).getResult(); 98 | return { 99 | props: { 100 | browser: result.browser, 101 | posts: JSON.parse(JSON.stringify(posts)), 102 | count, 103 | perPage, page, 104 | currentUser, 105 | totalPage: Math.ceil(count / perPage) 106 | } 107 | }; 108 | }); 109 | -------------------------------------------------------------------------------- /hooks/useForm.tsx: -------------------------------------------------------------------------------- 1 | // 表示 useForm 里有一个类型,这个类型也是 initFormData 的类型 2 | // 因为不知道 T 是什么, 3 | import {ReactChild, useCallback, useState} from 'react'; 4 | import * as React from 'react'; 5 | import {AxiosResponse} from 'axios'; 6 | import cs from 'classnames'; 7 | 8 | type Field = { 9 | label: string, 10 | type: 'text' | 'password' | 'textarea', 11 | key: keyof T, 12 | className?: string 13 | }; 14 | 15 | type useFormOptions = { 16 | initFormData: T; 17 | fields: Field[]; 18 | buttons: ReactChild; 19 | submit: { 20 | request: (formData: T) => Promise>; 21 | success: () => void; 22 | } 23 | } 24 | 25 | export function useForm(options: useFormOptions) { 26 | // 非受控 27 | const {initFormData, fields, buttons, submit} = options; 28 | const [formData, setFormData] = useState(initFormData); 29 | const [errors, setErrors] = useState(() => { 30 | const e: { [k in keyof T]?: string[] } = {}; 31 | (Object.keys(initFormData) as Array) 32 | .map((key) => {e[key] = [];}); 33 | return e; 34 | }); 35 | 36 | const onChange = useCallback((key: keyof T, value: any) => { 37 | setFormData({...formData, [key]: value}); 38 | }, [formData]); 39 | 40 | const _onSubmit = useCallback((e) => { 41 | e.preventDefault(); 42 | submit.request(formData).then(() => { 43 | submit.success(); 44 | }, (error) => { 45 | if (error.response) { 46 | const response: AxiosResponse = error.response; 47 | if (response.status === 422) { 48 | setErrors(response.data); 49 | } else if (response.status === 401) { 50 | window.alert('请先登录'); 51 | window.location.href = `/sign_in?returnTo=${encodeURIComponent(window.location.pathname)}`; 52 | } 53 | } 54 | }); 55 | }, [submit, formData]); 56 | 57 | const form = ( 58 |
59 | {fields.map(field => 60 |
61 |