RCNote:我有一个小想法

发布于 9 天前  629 次阅读


0. 写在开头

前段时间摸鱼的时候看到了一个比较有意思的记笔记的开源项目,叫做blossoms,但是摸鱼的时间零零散散的,下班之后也不想动脑子了,就一直没有部署成功(我的问题)。后来有专门在中午吃饭时间部署,结果发现我手上的两台设备(公司主机和笔记本)都无法成功安装docker,暂时放弃了。

但是机缘巧合之下,部门最近准备搭建一个知识库,我就可以名正言顺研究这个了。但是blossoms的许多技术我感觉我暂时没精力去学习,就找了ChatGPT“商量”开发一个简单的记笔记软件,并且只基于我目前已有的技术。

于是,就有了现在这篇文章,话不多说,直接扔文档。等我试试水,如果可以会将项目公布到Github上。

1. 项目结构

首先,设计项目的目录结构。以下是一个基本的项目结构:

markdown-notes/
├── backend/
│   ├── models/
│   ├── routes/
│   ├── services/
│   ├── utils/
│   ├── app.js
│   └── server.js
├── frontend/
│   ├── public/
│   ├── src/
│   │   ├── components/
│   │   ├── pages/
│   │   ├── styles/
│   │   ├── App.js
│   │   └── index.js
│   ├── package.json
│   └── webpack.config.js
├── database/
│   └── notes.db
├── package.json
└── README.md

2. 后端开发

2.1 初始化Node.js项目

backend目录下初始化Node.js项目:

cd backend
npm init -y
npm install express sqlite3 jsonwebtoken bcryptjs

2.2 创建Express应用

backend/app.js中创建一个基本的Express应用:

const express = require('express');
const bodyParser = require('body-parser');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const sqlite3 = require('sqlite3').verbose();

const app = express();
app.use(bodyParser.json());

// 数据库连接
const db = new sqlite3.Database('./database/notes.db');

// 路由
const authRoutes = require('./routes/auth');
const notesRoutes = require('./routes/notes');

app.use('/auth', authRoutes);
app.use('/notes', notesRoutes);

module.exports = app;

2.3 用户认证

backend/routes/auth.js中实现用户注册和登录功能:

const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const db = require('../models/db');

const router = express.Router();

router.post('/register', async (req, res) => {
    const { username, password } = req.body;
    const hashedPassword = await bcrypt.hash(password, 10);

    db.run('INSERT INTO users (username, password) VALUES (?, ?)', [username, hashedPassword], (err) => {
        if (err) return res.status(500).send(err.message);
        res.status(201).send('User registered');
    });
});

router.post('/login', async (req, res) => {
    const { username, password } = req.body;

    db.get('SELECT * FROM users WHERE username = ?', [username], async (err, user) => {
        if (err) return res.status(500).send(err.message);
        if (!user) return res.status(404).send('User not found');

        const isMatch = await bcrypt.compare(password, user.password);
        if (!isMatch) return res.status(401).send('Invalid password');

        const token = jwt.sign({ id: user.id }, 'secret', { expiresIn: '1h' });
        res.json({ token });
    });
});

module.exports = router;

2.4 笔记管理

backend/routes/notes.js中实现笔记的增删改查功能:

const express = require('express');
const jwt = require('jsonwebtoken');
const db = require('../models/db');

const router = express.Router();

const authenticate = (req, res, next) => {
    const token = req.headers['authorization'];
    if (!token) return res.status(401).send('No token provided');

    jwt.verify(token, 'secret', (err, decoded) => {
        if (err) return res.status(401).send('Invalid token');
        req.userId = decoded.id;
        next();
    });
};

router.get('/', authenticate, (req, res) => {
    db.all('SELECT * FROM notes WHERE user_id = ?', [req.userId], (err, rows) => {
        if (err) return res.status(500).send(err.message);
        res.json(rows);
    });
});

router.post('/', authenticate, (req, res) => {
    const { title, content } = req.body;
    db.run('INSERT INTO notes (user_id, title, content) VALUES (?, ?, ?)', [req.userId, title, content], (err) => {
        if (err) return res.status(500).send(err.message);
        res.status(201).send('Note created');
    });
});

router.put('/:id', authenticate, (req, res) => {
    const { title, content } = req.body;
    db.run('UPDATE notes SET title = ?, content = ? WHERE id = ? AND user_id = ?', [title, content, req.params.id, req.userId], (err) => {
        if (err) return res.status(500).send(err.message);
        res.status(200).send('Note updated');
    });
});

router.delete('/:id', authenticate, (req, res) => {
    db.run('DELETE FROM notes WHERE id = ? AND user_id = ?', [req.params.id, req.userId], (err) => {
        if (err) return res.status(500).send(err.message);
        res.status(200).send('Note deleted');
    });
});

module.exports = router;

2.5 图片上传

backend/routes/images.js中实现图片上传功能:

const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const jwt = require('jsonwebtoken');

const router = express.Router();

const storage = multer.diskStorage({
    destination: (req, file, cb) => {
        const userId = req.userId;
        const dir = `uploads/${userId}`;
        if (!fs.existsSync(dir)) {
            fs.mkdirSync(dir, { recursive: true });
        }
        cb(null, dir);
    },
    filename: (req, file, cb) => {
        cb(null, Date.now() + path.extname(file.originalname));
    }
});

const upload = multer({ storage });

router.post('/upload', authenticate, upload.single('image'), (req, res) => {
    res.json({ url: `/uploads/${req.userId}/${req.file.filename}` });
});

module.exports = router;

3. 前端开发

3.1 初始化React项目

frontend目录下初始化React项目:

cd frontend
npx create-react-app .
npm install axios marked react-markdown

3.2 登录页面

frontend/src/pages/Login.js中创建登录页面:

import React, { useState } from 'react';
import axios from 'axios';

const Login = ({ setToken }) => {
    const [username, setUsername] = useState('');
    const [password, setPassword] = useState('');

    const handleSubmit = async (e) => {
        e.preventDefault();
        const response = await axios.post('/auth/login', { username, password });
        setToken(response.data.token);
    };

    return (
        <form onSubmit={handleSubmit}>
            <input type="text" value={username} onChange={(e) => setUsername(e.target.value)} placeholder="Username" />
            <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" />
            <button type="submit">Login</button>
        </form>
    );
};

export default Login;

3.3 笔记页面

frontend/src/pages/Notes.js中创建笔记页面:

import React, { useState, useEffect } from 'react';
import axios from 'axios';
import ReactMarkdown from 'react-markdown';

const Notes = ({ token }) => {
    const [notes, setNotes] = useState([]);
    const [currentNote, setCurrentNote] = useState(null);
    const [content, setContent] = useState('');

    useEffect(() => {
        const fetchNotes = async () => {
            const response = await axios.get('/notes', { headers: { Authorization: token } });
            setNotes(response.data);
        };
        fetchNotes();
    }, [token]);

    const handleNoteClick = (note) => {
        setCurrentNote(note);
        setContent(note.content);
    };

    const handleSave = async () => {
        await axios.put(`/notes/${currentNote.id}`, { content }, { headers: { Authorization: token } });
    };

    return (
        <div>
            <div>
                {notes.map(note => (
                    <div key={note.id} onClick={() => handleNoteClick(note)}>
                        {note.title}
                    </div>
                ))}
            </div>
            <div>
                <textarea value={content} onChange={(e) => setContent(e.target.value)} />
                <button onClick={handleSave}>Save</button>
            </div>
            <div>
                <ReactMarkdown>{content}</ReactMarkdown>
            </div>
        </div>
    );
};

export default Notes;

3.4 图片上传

frontend/src/components/ImageUpload.js中创建图片上传组件:

import React, { useState } from 'react';
import axios from 'axios';

const ImageUpload = ({ token }) => {
    const [file, setFile] = useState(null);

    const handleFileChange = (e) => {
        setFile(e.target.files[0]);
    };

    const handleUpload = async () => {
        const formData = new FormData();
        formData.append('image', file);
        await axios.post('/images/upload', formData, { headers: { Authorization: token } });
    };

    return (
        <div>
            <input type="file" onChange={handleFileChange} />
            <button onClick={handleUpload}>Upload</button>
        </div>
    );
};

export default ImageUpload;

4. 数据库初始化

database目录下创建SQLite数据库,并初始化用户和笔记表:

CREATE TABLE users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    username TEXT UNIQUE NOT NULL,
    password TEXT NOT NULL
);

CREATE TABLE notes (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL,
    title TEXT NOT NULL,
    content TEXT NOT NULL,
    FOREIGN KEY (user_id) REFERENCES users (id)
);

5. 运行项目

backend目录下启动后端服务:

node server.js

frontend目录下启动前端服务:

npm start