介绍
我们将使用node.js,react和Atlas构建一个全堆栈应用程序。
Node.js 是一个开源的,跨平台的,后端的JavaScript运行时环境,可在V8引擎上运行并在Web浏览器外执行JavaScript代码。它旨在构建可扩展网络应用程序,并且通常用于构建服务器端应用程序。 Node.js提供了一个事件驱动的,非阻滞I/O模型,它使其轻巧有效,非常适合跨分布式设备运行的数据密集型实时应用程序。
React 是用于构建用户界面的JavaScript库。它允许开发人员创建可重复使用的UI组件并管理这些组件的状态。
Atlas 是一种开源数据库架构管理工具。此工具使您可以检查和修改数据库,更改模式并迁移数据。借助Atlas,为您的数据库设计和创建新的模式很简单,而没有SQL语法的复杂性。
在文章结尾处,您将拥有一个可以轻松扩展的全堆基础。此外,您将被介绍给 Atlas ,它允许进行检查,修改,改变架构的数据库。
。先决条件
进一步进行之前,您需要以下内容:
- Atlas
- docker
- mySQL数据库
- Node
- npm develices>
- javascript
- vs-code(您可以使用任何IDE或Editor)
您也有望对这些技术具有基本知识。
入门
项目结构:
project/
api/
config/
dbConfig.js
controllers/
TodoController.js
Models/
index.js
todoModel.js
Routes/
todoRoutes.js
schema/
schema.hcl (encoding must be UTF-8)
.env
index.js
package.json
front/
public/
index.html
src/
App.js
index.css
NewTodo.js
Todo.js
TodoList.js
package.json
postcss.config.js
tailwind.config.js
我们的第一步是创建项目文件夹:
mkdir api
mkdir front
使用Atlas检查,设计和迁移数据库:
我们将设计我们的数据库架构,并使用atlas迁移到我们的数据库中。
MacOS + Linux:
curl -sSf https://atlasgo.sh | sh
Windows:
Download atlas from the latest release, rename it to atlas.exe, and move it to
"C:\Windows\System32" and access "atlas" from the PowerShell anywhere.
运行mysql:
docker run --rm -d --name atlas-mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=pass -e MYSQL_DATABASE=todo_atlas mysql
此命令创建一个带有名称âtlas-Mysqlâ的新容器,并将root用户的密码设置为“通过”。它还创建了一个名为todo_atlas的新数据库。
连接到Docker终端:
docker exec -it atlas-mysq bash
输入此命令以docker终端访问mysql:-u标志意味着用户,即root是用户,-p flag表示密码,即。通过是密码。
mysql -uroot -ppass
创建一个新用户并添加特权:
CREATE USER 'todo_atlas_user'@'%' IDENTIFIED BY 'todo_atlas_password';
GRANT ALL ON todo_atlas.* TO 'todo_atlas_user'@'%';
FLUSH PRIVILEGES;
这将使用用户名来创建一个新用户。
现在在/api
中创建一个模式文件夹:
cd api
mkdir schema
cd schema
键入以下内容以通过ATLAS命令检查数据库:
atlas schema inspect -u "mysql://todo_atlas_user:todo_atlas_password@localhost:3306/todo_atlas" > schema.hcl
如果您与编辑打开/api/schema/schema.hcl
,则会找到架构:
schema "todo_atlas" {
charset = "utf8mb4"
collate = "utf8mb4_0900_ai_ci"
}
现在让我们的应用程序设计数据库(确保Schema.HCl具有UTF-8编码):
我们将创建一个表名称âtodos,它将有四列:“ id”,“ title”,“ description”和“ alterted”。
要定义表,我们将使用“表”关键字。
要定义列,我们将使用“列”关键字。
要定义主索引,我们将使用“ prientar_key”关键字。
要定义索引,我们将使用“索引”关键字。
我们将使用DDL(数据定义语言)定义我们的架构。您可以在Atlas-DDL上了解有关DDL的更多信息。
schema "todo_atlas" {
charset = "utf8mb4"
collate = "utf8mb4_0900_ai_ci"
}
table "todos" {
schema = schema.todo_atlas
column "id" {
null = false
type = bigint
unsigned = true
auto_increment = true
}
column "title" {
null = false
type = varchar(41)
}
column "description" {
null = true
type = text
}
column "completed" {
null = false
type = bool
default = 0
}
primary_key {
columns = [column.id]
}
index "todos_UN" {
unique = true
columns = [column.title]
}
}
是时候使用声明性架构迁移将我们的模式迁移到数据库中,以遵循此简单命令:
atlas schema apply -u "mysql://todo_atlas_user:todo_atlas_password@localhost:3306/todo_atlas" --to file://schema.hcl
我们的架构已成功迁移,我们已经准备好创建我们的待办事项。
还有另一种类型的迁移,该迁移版本为模式迁移。要了解更多信息,请访问versioned workflow。
API设置
现在已经设置了数据库并且已经安装了所有依赖关系,现在该创建我们的应用程序后端了。
通过键入:
从/api/schema
返回我们的/api
文件夹
cd ..
让我们在/api
文件夹中启动一个新项目:
npm init -y
现在我们需要安装这些依赖项:
npm install express cors dotenv nodemon
创建数据库连接器
我们首先需要与数据库的连接,以便我们可以在应用程序中使用。
首先,让我们安装以下依赖项:
npm install sequelize
让我们创建一个.env
,在/api
文件夹中具有所需变量:
#database
HOST ='localhost'
USER ='todo_atlas_user'
PASSWORD ='todo_atlas_password'
DATABASE ='todo_atlas'
#application backend
PORT = 5000
在/api/configs/
中创建dbConfig.js
:
const dotenv = require('dotenv');
let result = dotenv.config();
module.exports = {
HOST: process.env.HOST,
USER: process.env.USER,
PASSWORD: process.env.PASSWORD,
DB: process.env.DATABASE,
dialect: 'mysql',
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
}
}
创建模型
我们将创建与数据库交互的模型。我们将在todoModel.js
中创建模型:
module.exports = (sequelize, DataTypes) => {
const todo = sequelize.define("todos", {
title: {
type: DataTypes.STRING
},
description: {
type: DataTypes.TEXT
},
completed: {
type: DataTypes.BOOLEAN
}
}, {
timestamps: false // disable timestamps
})
return todo
}
我们将从index.js
访问我们的模型,在/api/models/
中创建index.js
const dbConfig = require('../configs/dbConfig.js');
const {Sequelize, DataTypes} = require('sequelize');
const sequelize = new Sequelize(
dbConfig.DB,
dbConfig.USER,
dbConfig.PASSWORD, {
host: dbConfig.HOST,
dialect: dbConfig.dialect,
operatorsAliases: false,
pool: {
max: dbConfig.pool.max,
min: dbConfig.pool.min,
acquire: dbConfig.pool.acquire,
idle: dbConfig.pool.idle
}
}
)
sequelize.authenticate()
.then(() => {
console.log('connected...')
})
.catch(err => {
console.log('Error :'+ err)
})
const db = {}
db.Sequelize = Sequelize
db.sequelize = sequelize
db.todo = require('./todoModel.js')(sequelize, DataTypes)
db.sequelize.sync({ force: false })
.then(() => {
console.log('yes re-sync done!')
}).catch(err => {
console.log('Error :'+ err)
})
module.exports = db
创建控制器
在/api/controllers
中创建todoController.js
,我们将为每个路线重新设置一个控制器函数:
首先,我们将导入并初始化我们的模型:
const db = require('../models')
const todos = db.todo
我们将创建一个控制器函数createTodo
,用于创建新的todo:
// 1. create todo
const createTodo = async (req, res) => {
let info = {
title: req.body.title,
description: req.body.description ? req.body.description : "No description yet" ,
published: 0
}
try {
// Check if the title already exists in the database
let todo = await todos.findOne({ where :{title : info.title }});
if (todo!=null) {
// Title already exists, return a 409 (Conflict) error
res.status(409).json({ message: 'Title already exists' });
return;
}
// Title does not exist, insert the new todo into the database
if(req.body.title.length<41){
let todo = await todos.create(info);
res.status(201).json(todo);
}
else{
res.status(409).json({ message: 'Title is too long' });
}
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Internal Server Error'+err });
}
}
我们将创建另一个函数getAllTodos
,以获取我们的所有待办事项列表:
// 2. get all todos
const getAllTodos =async (req, res) => {
try {
let todo = await todos.findAll()
console.log(todo)
res.status(200).json(todo)
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Internal Server Error' })
}
}
我们将创建updateTodo
,以更新需要更改时的待办事项状态:
// 3. update todo by id
const updateTodo = async (req, res) => {
try{
let id = req.params.id
let todo = await todos.update(req.body, { where: { id: id }})
res.status(200).send(todo)
}catch(err){
console.error(err);
res.status(500).json({ message: 'Internal Server Error' })
}
}
我们将创建另一个函数deleteTodo
,用于通过其ID删除todo:
// 4. delete todo by id
const deleteTodo = async (req, res) => {
try{
let id = req.params.id
await todos.destroy({ where: { id: id }} )
res.status(200).send('Todo is deleted !')
}catch(err){
console.error(err);
res.status(500).json({ message: 'Internal Server Error'+err })
}
}
我们将创建最后一个控制器函数deleteAll
,用于删除列表中的所有todo:
// 5. delete all todos
const deleteAll = async (req, res) => {
try{
await todos.destroy({truncate : true} )
res.status(200).send('All todos are deleted !')
}catch(err){
console.error(err);
res.status(500).json({ message: 'Internal Server Error'+err })
}
}
最后,我们将导出控制器功能,以便可以从路由器访问:
module.exports = {
createTodo,
getAllTodos,
updateTodo,
deleteTodo,
deleteAll
}
创建路由器
我们将定义有关API的所有路线。
路由器定义了一组路由,每个路由都与特定的HTTP方法相关联(例如,获取,发布,put,删除)。当向路由提出请求时,路由器将将URL与适当的路由匹配,并在控制器中执行关联的代码。
在/api/routes
中创建todoRoutes.js
,我们将为每个路线创建一个路由器:
// import controllers
const todoController = require('../controllers/todoController')
// router instance
const router = require('express').Router()
// defining routes
router.get('/todos', todoController.getAllTodos)
router.post('/todos',todoController.createTodo)
router.put('/todos/:id',todoController.updateTodo)
router.delete('/todos/:id',todoController.deleteTodo)
router.delete('/delete-all',todoController.deleteAll)
module.exports = router
创建API
这是我们的API。此应用程序通过控制器和中间件管理所有请求和过程。
在/api
文件夹中创建server.js
:
const express = require('express')
const cors = require('cors')
// expess initializes
const app = express()
// middleware
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
// router
const router = require('./routes/todoRoutes.js')
app.use('/api/v1', router)
//port
const PORT = process.env.PORT || 5000
//server
app.listen(PORT, () => {
console.log(`server is running on port ${PORT}`)
})
现在配置package.json
以运行我们的API:
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "nodemon server",
"start": "node server"
},
运行API
npm start
or
npm run dev
两个之间的区别是,npm start
不支持热重新加载,但npm run dev
会。
我们的API应该在端口5000上运行。
如果您对依赖关系感到困惑,请访问GitHub目录。
前端设置
初始化反应
goto /front
并通过以下命令创建React应用:
npx create-react-app ./
在/front/package.json
中复制以下内容:
{
"name": "Todo-Frontend",
"version": "0.1.0",
"private": true,
"proxy": "http://localhost:5000",
"dependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"autoprefixer": "^10.4.14",
"axios": "^1.3.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"tailwindcss": "^3.2.7"
}
}
现在我们需要安装所有依赖项:
npm install
创建tailwind.config.js
:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{js,jsx,ts,tsx}",],
theme: {
extend: {},
},
plugins: [],
}
创建postcss.config.js
:
module.exports = {
plugins: [
require('tailwindcss'),
require('autoprefixer'),
]
}
删除/front/src
中的所有内容,然后在/front/src/
中创建index.js
:
在/front/src
中创建index.css
:
@tailwind base;
@tailwind components;
@tailwind utilities;
在/front/src
中创建index.css
:
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
创建组件
我们安装了所有依赖项,以进行react和配置。是时候制作应用程序前端的组件了。
在/front/src
中创建NewTodo.js
:
import React, { useState } from 'react';
const NewTodo = ({ addTodo }) => {
const [title, setTitle] = useState('');
const handleSubmit = e => {
e.preventDefault();
addTodo({
title: title,
completed: false
});
setTitle('');
};
return (
<form onSubmit={handleSubmit}>
<input type="text" placeholder="Add a new Todo" value={title} onChange={e => setTitle(e.target.value)} />
<button type="submit">Add</button>
</form>
);
};
export default NewTodo;
在/front/src
中创建Todo.js
:
import React from 'react';
const Todo = ({ todo, deleteTodo, toggleCompleted }) => {
return (
<div className="todo">
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleCompleted(todo.id)}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>{todo.title}</span>
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</div>
);
};
export default Todo;
在/front/src
中创建TodoList.js
:
import React from 'react';
import Todo from './Todo';
const TodoList = ({ todos, deleteTodo, toggleCompleted }) => {
return (
<div className="todo-list">
{todos.map(todo => (
<Todo key={todo.id} todo={todo} deleteTodo={deleteTodo} toggleCompleted={toggleCompleted} />
))}
</div>
);
};
export default TodoList;
创建前端
我们已经制造了组件,现在我们将创建todo应用程序的Frotend。
在/front/src
中创建App.js
:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const base = "api/v1";
function App() {
const [todos, setTodos] = useState([]);
const [newTodo, setNewTodo] = useState('');
const [error, setError] = useState('');
useEffect(() => {
axios.get(base+'/todos')
.then(response => {
setTodos(response.data);
})
.catch(error => {
console.log(error);
});
}, []);
const handleInputChange = (event) => {
setNewTodo(event.target.value);
};
const handleAddTodo = () => {
if (newTodo.trim() === '') {
return;
}
axios.post(base+'/todos', { title: newTodo })
.then(response => {
setTodos([...todos, response.data]);
setNewTodo('');
setError('');
})
.catch(error => {
setError(error);
console.log(error);
});
};
const handleDeleteTodo = (id) => {
axios.delete(base+`/todos/${id}`)
.then(response => {
setTodos(todos.filter(todo => todo.id !== id));
})
.catch(error => {
console.log(error);
});
};
const handleToggleTodo = (id) => {
const updatedTodos = todos.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed;
}
return todo;
});
axios.put(base+`/todos/${id}`, { completed: updatedTodos.find(todo => todo.id === id).completed })
.then(response => {
setTodos(updatedTodos);
})
.catch(error => {
console.log(error);
});
};
return (
<div className='container mx-auto bg-mnblue'>
<div className="flex flex-col items-center h-screen bg-grey-300">
<h1 className=' py-2 font-bold text-white'>Todo App</h1>
<div className='flex-col py-2 mb-2' >
<input aria-label="Todo input" className="mr-2 shadow appearance-none border rounded w-80 py-2 px-3 text-black leading-tight focus:outline-none focus:shadow-outline "type="text" value={newTodo} onChange={handleInputChange} placeholder="Add task." />
<button className="shadow bg-mint px-3 hover:bg-mint-light focus:shadow-outline focus:outline-none text- font-bold py-1 px-1 rounded" onClick={handleAddTodo} >Add Todo</button>
{error? (<div className='mt-2 p-1 text-center bg-gray-300' style={{ color: 'red' }}>{error.response.data.message}</div>) : (<div></div>)}
</div>
<div style={{borderTop:"solid 2px black"}}></div>
<ul className="flex flex-col w-full " style={{ listStyle: 'none' , maxWidth: '500px'}} >
{todos.map(todo => (
<li className='bg-saffron flex p-1 m-1 rounded' key={todo.id} >
<input aria-label="Todo status toggle" className="px-2 " type="checkbox" checked={todo.completed} onChange={() => handleToggleTodo(todo.id)} />
<span className="mx-2 text-center flex-1 " style={{ textDecoration: todo.completed ? 'line-through' : 'none', color: todo.completed ? '#FB4D3D' : 'black' }}>{todo.title}</span>
<button className="float-end bg-tomato hover:bg-white hover:text-tomato focus:shadow-outline focus:outline-none text-white font-bold mx-auto mr-1 px-1 rounded" onClick={() => handleDeleteTodo(todo.id)} >Delete</button>
</li>
))}
</ul>
</div>
</div>
);
}
export default App;
运行前端
npm start
结论
在本教程中,我们学会了如何使用node.js,react和Atlas创建全堆栈应用程序。感谢您到目前为止的阅读,希望您喜欢它!