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
Comments | NOTHING