はじめに

インフラは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!する

Screenshot 2023-01-27 at 17.09.47.png

フロントエンド側

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
Screenshot 2023-01-28 at 10.50.03.png

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にアクセスします。

Screenshot 2023-01-28 at 19.49.39.png

バックエンド側

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

結果

rust_movies.gif
广告
将在 10 秒后关闭
bannerAds