20
21

More than 1 year has passed since last update.

Dockerではじめるactix-web(Rust)とReact(TypeScript)

Last updated at Posted at 2023-01-31

はじめに

インフラは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
docker-compose.yml
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

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"を追加します。

Cargo.toml
[package]
name = "practice-rust"
version = "0.1.0"
edition = "2021"

[dependencies]
actix-web="4"

main.rsを以下のように書き換えます。

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!する

Screenshot 2023-01-27 at 17.09.47.png

フロントエンド側

React(typescript)を実装

FROM rust:1-slim-buster

WORKDIR /usr/src/myapp
docker-compose.yml
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上では、そのまま開発できないので、設定ファイルをいじります。

package.json
  "scripts": {
    "dev": "vite --host",
    "build": "tsc && vite build",
    "preview": "vite preview"
  },
vite.config.ts
export default defineConfig({
  server: {
    host: '0.0.0.0',
    port: 3000,
  },
  plugins: [react()],
})

準備が完了したので、

npm run dev

Screenshot 2023-01-28 at 10.50.03.png

api通信

main.rs
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
app.tsx
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に以下の行を追加します。

docker-compose.yml
  webserver:
    build: ./webserver
    ports:
      - "80:80"
    volumes:
      - ./webserver:/etc/nginx/conf.d

rust.confを/webserverにいれ、リバースプロキシをします。

rust.conf
server {
    listen 80;
    listen [::]:80;
    server_name localhost;

    location / {
        proxy_pass http://frontend:3000;
    }

    location /api/ {
        proxy_pass http://web:8080;
    }

}

loccalhostにアクセスします。

Screenshot 2023-01-28 at 19.49.39.png

バックエンド側

mysqlに接続

dockerのmysql環境構築は割愛します。

以下の記事は、下記を参考にしています。

Cargo.tomlに以下を追加します。

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ができていると思います。

up.sql
CREATE TABLE todos
(
    id INT PRIMARY KEY AUTO_INCREMENT,
    content TEXT DEFAULT NULL
);
down.sql
DROP TABLE todos;

mysqlのURLをenvファイルに書き込みます。

.env
DATABASE_URL=mysql://rust:rust@db:3306/todoproject

tableを生成するには、以下のコマンドを実行します。

diesel migration run
db.rs
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
}
model.rs
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,
}
schema.rs
// @generated automatically by Diesel CLI.

table! {
    todos (id) {
        id -> Integer,
        content -> Text,
    }
}

以下を実行すると、get,post,deleteができます。

main.rs
#[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より遥かに簡単。

app.tsx
  type Todo = {
    id: number,
    content: string,
  }

useState<型>(初期値)です。
setTodosはセッターです。

app.tsx
const [todos, setTodos] = useState<Todo[]>([]);

stringを型にしたパターンです。

app.tsx
useState<string>("");
app.tsx
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

結果

rust_movies.gif

20
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
21