はじめに
インフラはDockerとDocker Composeです。
バックエンドはactix-web(Rustのフレームワーク)
フロントエンドはReact(TypeScriptのフレームワーク)
データベースはMySqlです。
リマインダー(todoリスト)を目標に開発を行います。
レポジトリーはこちらです。
RustはCargo.tomlに記述してパッケージをダウンロードする方式です。
パッケージの最新バージョンは以下で確認すると良いです。
また、Rustはtargetレポジトリーが非常に重いので、gitignoreに入れておくといいです。
バックエンド側
バックエンド側を実装します。
環境構築
FROM rust:1-slim-buster
WORKDIR /usr/src/myapp
version: "3"
services:
web:
build: ./opt
container_name: "practice-rust"
tty: true
volumes:
- ./opt:/usr/src/myapp
docker compose exec web bash
cargo new practice-rust
以下のようなファイル構成になっていると思います。
.gitフォルダーが自動で作られます。
├── docker-compose.yml
└── opt
├── Dockerfile
└── practice-rust
├── Cargo.toml
└── src
└── main.rs
async fn main() {
println!("Hello, world!");
}
Hello, world!
root@500969f0546b:/usr/src/myapp/practice-rust# cargo run
Compiling practice-rust v0.1.0 (/usr/src/myapp/practice-rust)
Finished dev [unoptimized + debuginfo] target(s) in 8.37s
Running `target/debug/practice-rust`
Hello, world!
コンパイルに時間がかかります。
actix-webを使う
actix-webはRustのwebのフレームワークです。
Cargo.tomlにactix-web="4"を追加します。
[package]
name = "practice-rust"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web="4"
main.rsを以下のように書き換えます。
use actix_web::{get, App, HttpServer, Responder};
#[get("/")]
async fn index() -> impl Responder {
"Hello world!"
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new().service(index)
})
.bind(("web", 8080))?
.run()
.await
}
service(index)を追加することで、ルーティングしたい関数を追加します。
bind(("web", 8080))のwebはdocker-compseに書いたサービス名です。
Hello world!する
フロントエンド側
React(typescript)を実装
FROM rust:1-slim-buster
WORKDIR /usr/src/myapp
version: "3"
services:
web:
build: ./opt
container_name: "practice-rust"
working_dir: "/usr/src/myapp/practice-rust"
tty: true
volumes:
- ./opt:/usr/src/myapp
ports:
- "8080:8080"
frontend:
build: ./frontend
container_name: "frontend-rust"
volumes:
- ./frontend:/usr/src/app
ports:
- "3000:3000"
tty: true
viteでインストール
以下の順番でインストールを実行します。
docker compose exec web bash
npm create vite@latest
cd rust-front
npm install
wellcome画面
docker上では、そのまま開発できないので、設定ファイルをいじります。
"scripts": {
"dev": "vite --host",
"build": "tsc && vite build",
"preview": "vite preview"
},
export default defineConfig({
server: {
host: '0.0.0.0',
port: 3000,
},
plugins: [react()],
})
準備が完了したので、
npm run dev
api通信
use actix_web::{get, App, HttpResponse, HttpServer, Responder};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct Todo {
id: i32,
content: String,
checked: bool,
}
#[get("/api/todo")]
async fn todo_index() -> impl Responder {
HttpResponse::Ok().json(Todo {
id: 1,
content: "やることはapi".to_string(),
checked: false,
})
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new().service(todo_index)
})
.bind(("web", 8080))?
.run()
.await
}
npm install axios
import { useEffect, useState } from 'react'
import './App.css'
import axios from 'axios';
function App() {
const url = "http://localhost/api/todo";
const [count, setCount] = useState(0)
useEffect(() => {
axios.get(url).then((res) => {
console.log(res.data);
}).catch(error => {
console.log(error);
});
}, [])
return (
<div className="App">
helloworld
</div>
)
}
export default App
nginxの導入
cors対策の為、nginxを導入しリバースプロキシを行います。
docker-compose.ymlに以下の行を追加します。
webserver:
build: ./webserver
ports:
- "80:80"
volumes:
- ./webserver:/etc/nginx/conf.d
rust.confを/webserverにいれ、リバースプロキシをします。
server {
listen 80;
listen [::]:80;
server_name localhost;
location / {
proxy_pass http://frontend:3000;
}
location /api/ {
proxy_pass http://web:8080;
}
}
loccalhostにアクセスします。
バックエンド側
mysqlに接続
dockerのmysql環境構築は割愛します。
以下の記事は、下記を参考にしています。
Cargo.tomlに以下を追加します。
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
diesel = { version = "2.0", features = ["mysql","r2d2"] }
dotenv = "0.15"
r2d2 = "0.8"
cargo install diesel_cli --no-default-features --features mysql --force
migrationする
diesel setup
diesel migration generate create_todos
migrationsディレクトリー配下にup.sqlとdown.sqlができていると思います。
CREATE TABLE todos
(
id INT PRIMARY KEY AUTO_INCREMENT,
content TEXT DEFAULT NULL
);
DROP TABLE todos;
mysqlのURLをenvファイルに書き込みます。
DATABASE_URL=mysql://rust:rust@db:3306/todoproject
tableを生成するには、以下のコマンドを実行します。
diesel migration run
use diesel::mysql::MysqlConnection;
use diesel::r2d2::ConnectionManager;
use dotenv::dotenv;
pub type Pool = r2d2::Pool<ConnectionManager<MysqlConnection>>;
pub fn establish_connection() -> Pool {
dotenv().ok();
std::env::set_var("RUST_LOG", "actix_web=debug");
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
// create db connection pool
let manager = ConnectionManager::<MysqlConnection>::new(database_url);
let pool: Pool = r2d2::Pool::builder()
.build(manager)
.expect("Failed to create pool.");
pool
}
use crate::schema::todos;
use serde::{Deserialize, Serialize};
#[derive(Queryable, Insertable, Deserialize, Serialize)]
#[diesel(table_name = todos)]
pub struct Todo {
pub id: i32,
pub content: String,
}
#[derive(Queryable, Insertable, Deserialize, Serialize)]
#[diesel(table_name = todos)]
pub struct Updatetodo {
pub content: String,
}
// @generated automatically by Diesel CLI.
table! {
todos (id) {
id -> Integer,
content -> Text,
}
}
以下を実行すると、get,post,deleteができます。
#[macro_use]
extern crate diesel;
use diesel::RunQueryDsl;
use actix_web::web::Data;
use actix_web::{get, post, delete, web, App, HttpResponse, HttpServer, Responder};
mod db;
mod model;
mod schema;
#[get("/api/todo")]
async fn todo_index(db: web::Data<db::Pool>) -> impl Responder {
let mut conn = db.get().unwrap();
let todo = schema::todos::table
.load::<model::Todo>(&mut conn)
.expect("Error not showing todo list");
HttpResponse::Ok().json(todo)
}
#[post("/api/todo")]
async fn new_todo(db: web::Data<db::Pool>, c: web::Json<model::Updatetodo>) -> impl Responder {
let mut conn = db.get().unwrap();
let new_todo = model::Updatetodo {
content: c.content.to_string(),
};
diesel::insert_into(schema::todos::dsl::todos)
.values(&new_todo)
.execute(&mut conn)
.expect("Error not saving new todo");
HttpResponse::Created().body("get ok")
}
#[delete("/api/todo/{id}")]
async fn delete_todo(db: web::Data<db::Pool>, path: web::Path<i32>) -> impl Responder {
let id = path.into_inner();
let mut conn = db.get().unwrap();
let target = schema::todos::dsl::todos
.filter(schema::todos::dsl::id.eq(id));
diesel::delete(target)
.execute(&mut conn)
.expect("Error deleting new post");
HttpResponse::Created().body("Delete complete")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let pool = db::establish_connection();
HttpServer::new(move|| {
App::new()
.app_data(Data::new(pool.clone())
.service(todo_index)
.service(new_todo)
.service(delete_todo)
})
.bind(("web", 8080))?
.run()
.await
}
フロントエンド側
reactにリマインダーを実装
typesriptは型宣言が必要です。
でも、Rustより遥かに簡単。
type Todo = {
id: number,
content: string,
}
useState<型>(初期値)です。
setTodosはセッターです。
const [todos, setTodos] = useState<Todo[]>([]);
stringを型にしたパターンです。
useState<string>("");
import { useEffect, useState } from 'react'
import './App.css'
import axios from 'axios';
function App() {
const url = "http://localhost/api/todo";
const [inputValue, setInputValue] = useState("");
const [todos, setTodos] = useState<Todo[]>([]);
type Todo = {
id: number,
content: string,
}
const onWriting = (e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
setInputValue(e.target.value);
};
const onSave = (e: { preventDefault: () => void }) => {
e.preventDefault();
if (!inputValue) { return; }
axios.post('/api/todo', { content: inputValue })
.then(function (response) {
console.log(response.data);
axios.get(url).then((res) => {
setTodos([...res.data]);
}).catch(error => {
console.log(error);
});
})
setInputValue("");
};
const onDelete = (uid: number) => {
axios.delete('/api/todo/' + String(uid))
.then(function (response) {
console.log(response.data);
axios.get(url).then((res) => {
setTodos([...res.data]);
}).catch(error => {
console.log(error);
});
})
};
useEffect(() => {
axios.get(url).then((res) => {
setTodos([...res.data]);
}).catch(error => {
console.log(error);
});
}, [])
return (
<div className="App">
<h1>リマインダー</h1>
<form onSubmit={(e) => onSave(e)}>
<input type="text" style={{ width: "400px", height: "30px" }} value={inputValue} onChange={(e) => onWriting(e)} />
<input type="submit" style={{ width: "100px", height: "35px" }} value="送信" />
</form>
<br></br>
<table border={1}>
{todos.map((arr: Todo) => (
<tr>
<td style={{ textAlign: 'left' }} width={500}>{arr.content}</td>
<td>
<button onClick={() => onDelete(arr.id)}>削除</button>
</td>
</tr>
))}
</table>
</div >
)
}
export default App