使用Node.js,React和Atlas构建全堆栈应用程序
#react #开源 #node #atlas

介绍

我们将使用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
  • 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创建全堆栈应用程序。感谢您到目前为止的阅读,希望您喜欢它!