NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

Next.js + Rust 革新全栈开发,Rust 没那么难

  • 2023-08-27
    北京
  • 本文字数:11614 字

    阅读完需:约 38 分钟

Next.js + Rust 革新全栈开发,Rust没那么难

最近,shuttle 发布了新的 Node.js CLI 包,允许用户快速引导由 Next.js 前端加 Axum 后端(一种流行的 Rust Web 框架,以易于上手、语法简单著称)开发的应用程序。


本文打算构建的示例,是一个带有登录门户的记事本应用程序,提供用户注册、用户登录、密码重置等功能。用户在登录之后可以查看、创建、更新和删除笔记内容。本文将主要关注 Rust 后端方面,对于 React.js/Next.js 前端不会过多着墨。



完整代码仓库请参阅此处(https://github.com/joshua-mo-143/nodeshuttle-example)。


马上开始


运行以下命令,即可快速开始本次示例:


npx create-shuttle-app --ts 
复制代码


在按下回车键后,系统会提示我们输入名称——您可以随意起名,之后系统会自动安装 Rust 并引导一个使用 Next.js 的应用程序(由于这里我们添加了 ts 标志,所以使用的是 TypeScript);后端部分使用 Rust,再加上相应的 npm 命令,我们可以快速着手后端和前端的开发工作。这里我们使用的后端框架为 Axum,这是一套灵活的高性能框架,语法简单而且与 tower_http(用于创建中间件的强大库)高度兼容。


shuttle 是一个云开发平台,能够简化应用程序的部署流程。它最突出的优点就是“基础设施即代码”,允许大家直接通过代码定义基础设施,无需借助复杂的控制台或外部 yaml.config 文件。这种方式不仅提高了代码的清晰度,同时也能更好地保证编译时的输出质量。需要 Postgres 实例?只需添加相应注释即可。shuttle 还支持 secrets(作为环境变量)、静态文件夹和状态持久性。


接下来,我们需要安装 sqlx-cli,这款命令行工具能帮助我们管理数据库迁移。只须运行以下简单命令,即可完成安装:


cargo install sqlx-cli
复制代码


这样,只要前往项目文件夹内的后端目录,我们就能使用 sqlx migrate add schema 创建数据库迁移。此命令会添加一个迁移文件夹(如果之前不存在)和一个以_schema.sql 形式命名的新 SQL 文件,其中的“schema”部分代表我们的迁移名称。


这个 SQL 文件包含以下内容:


-- backend/migrations/<timestamp>_schema.sqlDROP TABLE IF EXISTS sessions;
CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, username VARCHAR UNIQUE NOT NULL, email VARCHAR UNIQUE NOT NULL, password VARCHAR NOT NULL, createdAt TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP );
CREATE TABLE IF NOT EXISTS notes ( id SERIAL PRIMARY KEY, message VARCHAR NOT NULL, owner VARCHAR NOT NULL, createdAt TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP );
INSERT INTO notes (message, owner) VALUES ('Hello world!', 'user');
CREATE TABLE IF NOT EXISTS sessions ( id SERIAL PRIMARY KEY, session_id VARCHAR NOT NULL UNIQUE, user_id INT NOT NULL UNIQUE);
复制代码


迁移会自动运行。但如果大家想要手动操作,也可以使用 sqlx migrate run --database-url。这种操作之所以可行,是因为我们已经将 SQL 文件设置为幂等,就是说只要已经存在该表、则不再重复创建。这里我们删除会话表,这样当应用程序重新上传之后,由于原先的 cookie 已经失效,用户就必须重新登录。


现在设置已经完成,马上进入正式开发!


前端


在这款应用中,我们需要以下几个页面:


  • 登录和注册页面;

  • 忘记密码时供用户重置密码的页面;

  • 显示记录的仪表板页面;

  • 用于编辑和创建新记录的页面。


大家可以通过以下方式克隆本文中的前端示例:


git clone https://github.com/joshua-mo-143/nodeshuttle-example-frontend
复制代码


克隆的代码仓库包含一个预先设置好的 src 目录,如下图所示:



其中 components 文件夹中包含两个布局组件,我们需要将页面组件嵌套在其中;另外还有一个用于在仪表板索引页面中编辑记录的 modal。Pages 文件夹则包含我们将在应用中使用的各相关页面组件(文件名代表相应路径)。


这里的 CSS 使用 TailwindCSS,并选择 Zustand 保证在不涉及太多模板的情况下实现简单的基本状态管理。


当用户登录之后,已有消息将显示为以下形式:



在后端构建完成之后,用户就能通过前端注册和登录(使用基于 cookie 会话的身份验证机制),并查看、创建、编辑和删除自己的消息。如果用户忘记了密码,还可以通过输入电子邮件来重置密码内容。


如果大家对示例中的前端不满意,也可以参考 GitHub 代码仓库(https://github.com/joshua-mo-143/nodeshuttle-example)来了解 API 调用和状态管理的设置方式。


现在前端部分已经完成,接下来就是后端环节了!


后端


前往 backend 文件夹,我们会看到一个名为 main.rs 的文件。其中包含一个函数,此函数会创建一个基础路由程序并返回“Hello,world!”我们将使用此文件作为应用程序的入口点,然后创建我们在 main 函数中调用的其他文件。


请确保您的 Cargo.toml 文件中包含以下内容:


# Cargo.toml[package]name = "static-next-server"version = "0.1.0"edition = "2021"publish = false
[dependencies]# the rust framework we will be using - https://github.com/tokio-rs/axum/axum = "0.6.1"# extra functionality for Axum https://github.com/tokio-rs/axum/axum-extra = { version = "0.4.2", features = ["spa", "cookie-private"] }# encryption hashing for passwords - https://github.com/Keats/rust-bcryptbcrypt = "0.13.0"# used for writing the CORS layer - https://github.com/hyperium/httphttp = "0.2.9"# send emails over SMTP - https://github.com/lettre/lettrelettre = "0.10.3"# random number generator (for creating a session id) - https://github.com/rust-random/randrand = "0.8.5"# used to be able to deserialize structs from JSON - https://github.com/serde-rs/serdeserde = { version = "1.0.152", features = ["derive"] }# environment variables on shuttleshuttle-secrets = "0.12.0"# the service wrapper for shuttleshuttle-runtime = "0.12.0"# allow us to use axum with shuttleshuttle-axum = "0.12.0"# this is what we use to get a shuttle-provisioned databaseshuttle-shared-db = { version = "0.12.0", features = ["postgres"] }# shuttle static folder supportshuttle-static-folder = "0.12.0"# we use this to query and connect to a database - https://github.com/launchbadge/sqlx/sqlx = { version = "0.6.2", features = ["runtime-tokio-native-tls", "postgres"] }# middleware for axum router - https://github.com/tower-rs/tower-httptower-http = { version = "0.4.0", features = ["cors"] }# pre-req for using shuttle runtime tokio = "1.26.0"# get a time variable for setting cookie max agetime = "0.3.20"
复制代码


完成之后,接下来就是设置主函数,这样就能使用 shuttle_shared_db 和 shuttle_secrets 来获取 shuttle 免费配置的数据库并使用 secrets,具体方式如下(包括基于 cookie 的会话存储功能,为简洁起见较为粗糙):


// main.rs #[derive(Clone)]pub struct AppState {    postgres: PgPool,    key: Key}
impl FromRef<AppState> for Key { fn from_ref(state: &AppState) -> Self { state.key.clone() }}
#[shuttle_runtime::main]async fn axum( #[shuttle_static_folder::StaticFolder] static_folder: PathBuf, #[shuttle_shared_db::Postgres] postgres: PgPool, #[shuttle_secrets::Secrets] secrets: SecretStore,) -> shuttle_axum::ShuttleAxum { sqlx::migrate!().run(&postgres).await;
let state = AppState { postgres, key: Key::generate() };
let router = create_router(static_folder, state);
Ok(router.into())}
复制代码


现在就可以创建路由程序了!我们首先要在 backend 目录的 src 文件夹中创建一个 router.rs 文件。我们的大部分路由程序代码都将存放在这里,并在准备好之后将最终版路由程序的函数导入到主文件当中。


现在打开 router.rs 文件并创建一个函数,该函数将返回一个能够路由至注册和登录的路由程序:


// router.rs
// typed request body for logging in - Deserialize is enabled via serde so it can be extracted from JSON responses in axum#[derive(Deserialize)]pub struct LoginDetails { username: String, password: String,}
pub fn create_router(state: AppState, folder: PathBuf) -> Router {// create a router that will host both of our new routes once we create them let api_router = Router::new() .route("/register", post(register)) .route("/login, post(login)) .with_state(state);
// return a router that nests our API router in an "/api" route and merges it with our static files Router::new() .nest("/api", api_router) .merge(SpaRouter::new("/", static_folder).index_file("index.html"))
}
复制代码


可以看到,接下来要做的就是编写路由程序中使用的函数。另外,我们也可以简单将多个方法串连起来,借此在同一路由内使用多个请求方法(后文将具体介绍)。


// backend/src/router.rspub async fn register(// this is the struct we implement and use in our router - we will need to import this from our main file by adding "use crate::AppState;" at the top of our app    State(state): State<AppState>,// this is the typed request body that we receive from a request - this comes from the axum::Json type    Json(newuser): Json<LoginDetails>,) -> impl IntoResponse { 
// avoid storing plaintext passwords - when a user logs in, we will simply verify the hashed password against the request. This is safe to unwrap as this will basically never fail let hashed_password = bcrypt::hash(newuser.password, 10).unwrap();
let query = sqlx::query("INSERT INTO users (username, , email, password) values ($1, $2, $3)")// the $1/$2 denotes dynamic variables in a query which will be compiled at runtime - we can bind our own variables to them like so: .bind(newuser.username) .bind(newuser.email) .bind(hashed_password) .execute(&state.postgres);
// if the request completes successfully, return CREATED status code - if not, return BAD_REQUEST match query.await { Ok(_) => (StatusCode::CREATED, "Account created!".to_string()).into_response(), Err(e) => ( StatusCode::BAD_REQUEST, format!("Something went wrong: {e}"), ) .into_response(), }}
复制代码


我们在这里对密码做散列处理,通过 SQLx 设置查询以创建新用户。如果成功,则返回 402 Created 状态码;如果不成功,则返回 400 Bad Request 状态码以指示错误。


模式匹配是 Rust 中一种非常强大的错误处理机制,而且提供多种使用方式:我们可以使用 if let else 和 let else,二者都涉及模式匹配,后文将具体介绍。


// backend/src/router.rspub async fn login(    State(mut state): State<AppState>,    jar: PrivateCookieJar,    Json(login): Json<LoginDetails>,) -> Result<(PrivateCookieJar, StatusCode), StatusCode> {    let query = sqlx::query("SELECT * FROM users WHERE username = $1")        .bind(&login.username)        .fetch_optional(&state.postgres);
match query.await { Ok(res) => {// if bcrypt cannot verify the hash, return early with a BAD_REQUEST error if bcrypt::verify(login.password, res.unwrap().get("password")).is_err() { return Err(StatusCode::BAD_REQUEST); }// generate a random session ID and add the entry to the hashmap let session_id = rand::random::<u64>().to_string();
sqlx::query("INSERT INTO sessions (session_id, user_id) VALUES ($1, $2) ON CONFLICT (user_id) DO UPDATE SET session_id = EXCLUDED.session_id") .bind(&session_id) .bind(res.get::<i32, _>("id")) .execute(&state.postgres) .await .expect("Couldn't insert session :(");

let cookie = Cookie::build("foo", session_id) .secure(true) .same_site(SameSite::Strict) .http_only(true) .path("/") .finish();
// propogate cookies by sending the cookie as a return type along with a status code 200 Ok((jar.add(cookie), StatusCode::OK))
}// if the query fails, return status code 400 Err(_) => Err(StatusCode::BAD_REQUEST), }}
复制代码


可以看到,请求仅采用各类 JSON 请求主体(因为我们将请求主体设定为 axum::Json 类型,所以它只会接受带有「username」和「password」JSON 请求主体的请求)。这样的 struct 必须实现 serde::Deserialize ,因为我们需要从 JSON 中提取数据,而且 JSON 请求参数本身将作为我们传递给路由函数的最后一个参数。


我们在登录请求中使用了名为 PrivateCookieJar 的 struct。通过这种方式,我们既可以自动处理 HTTP cookie,又不需要为其显式设置标题头(为了传播其中的变更,我们需要将其设置为返回类型并返回变更)。当用户想要访问受保护的路由时,需要从 cookie jar 当中获取值,再根据保存在数据库内的会话 ID 对其进行验证。因为使用的是私有 cookie jar,所以保存在客户端的任何 cookie 都将使用我们在初始 struct 内创建的密钥进行加密,且每次应用启动时都会生成一个新密钥。


现在我们已经添加了用于登录的路由,接下来看看如何添加用于注销的路由和用于验证会话的中间件:


// backend/src/router.rspub async fn logout(State(state): State<AppState>, jar: PrivateCookieJar) -> Result<PrivateCookieJar, StatusCode> {    let Some(cookie) = jar.get("foo").map(|cookie| cookie.value().to_owned()) else {        return Ok(jar)    };
let query = sqlx::query("DELETE FROM sessions WHERE session_id = $1") .bind(cookie) .execute(&state.postgres);

match query.await { Ok(_) => Ok(jar.remove(Cookie::named("foo"))), Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR) }}


pub async fn validate_session<B>( jar: PrivateCookieJar, State(state): State<AppState>,// Request<B> and Next<B> are required types for middleware from a function in axum request: Request<B>, next: Next<B>,) -> (PrivateCookieJar, Response) {// attempt to get the cookie - if it can't find a cookie, return 403 let Some(cookie) = jar.get("foo").map(|cookie| cookie.value().to_owned()) else {
println!("Couldn't find a cookie in the jar"); return (jar,(StatusCode::FORBIDDEN, "Forbidden!".to_string()).into_response()) };
// attempt to find the created session let find_session = sqlx::query("SELECT * FROM sessions WHERE session_id = $1") .bind(cookie) .execute(&state.postgres) .await;
// if the created session is OK, carry on as normal and run the route - else, return 403 match find_session { Ok(res) => (jar, next.run(request).await), Err(_) => (jar, (StatusCode::FORBIDDEN, "Forbidden!".to_string()).into_response()) }}
复制代码


可以看到,在注销路由这部分,我们会尝试销毁会话、返回 cookie 删除;至于验证路由,我们尝试获取会话 cookie,并保证 cookie 会话在数据库内有效。


下面来看如何为数据库内的各项记录创建最基本的 CRUD 功能。这里我们创建一个使用 sqlx::FromRow 的 struct,这样就能轻松从数据库中提取记录,具体代码如下所示:


// src/backend/router.rs#[derive(sqlx::FromRow, Deserialize, Serialize)]pub struct Note {    id: i32,    message: String,    owner: String,}
复制代码


之后,我们就可以直接使用 sqlx::query_as 并将该变量分类为 struct 的向量,借此实现预期功能,如下所示:


// src/backend/router.rspub async fn view_records(State(state): State<AppState>) -> Json<Vec<Note>> {    let notes: Vec<Note> = sqlx::query_as("SELECT * FROM notes ")        .fetch_all(&state.postgres)        .await.unwrap();
Json(notes)}
复制代码


很明显,我们要做的就是通过连接查询数据库,并确保我们分类后的返回 struct 上有 sqlx::FromRow 派生宏。通过同样的方式,我们也可以轻松编写出其他路由:


// backend/src/router.rs#[derive(Deserialize)]pub struct RecordRequest {    message: String,    owner: String}
pub async fn create_record( State(state): State<AppState>, Json(request): Json<RecordRequest>,) -> Response { let query = sqlx::query("INSERT INTO notes (message, owner) VALUES ($1, $2)") .bind(request.message) .bind(request.owner) .execute(&state.postgres);
match query.await { Ok(_) => (StatusCode::CREATED, "Record created!".to_string()).into_response(), Err(err) => ( StatusCode::BAD_REQUEST, format!("Unable to create record: {err}"), ) .into_response(), }}
// note here: the "path" is simply the id URL slug, which we will define laterpub async fn edit_record( State(state): State<AppState>, Path(id): Path<i32>, Json(request): Json<RecordRequest>,) -> Response {
let query = sqlx::query("UPDATE notes SET message = $1 WHERE id = $2 AND owner = $3") .bind(request.message) .bind(id) .bind(request.owner) .execute(&state.postgres);
match query.await { Ok(_) => (StatusCode::OK, format!("Record {id} edited ")).into_response(), Err(err) => ( StatusCode::BAD_REQUEST, format!("Unable to edit message: {err}"), ) .into_response(), }}
pub async fn destroy_record(State(state): State<AppState>, Path(id): Path<i32>) -> Response { let query = sqlx::query("DELETE FROM notes WHERE id = $1") .bind(id) .execute(&state.postgres);
match query.await { Ok(_) => (StatusCode::OK, "Record deleted".to_string()).into_response(), Err(err) => ( StatusCode::BAD_REQUEST, format!("Unable to edit message: {err}"), ) .into_response(), }}
复制代码


现在,我们已经为这款 Web 应用创建了所有基本功能!但在合并全部路由之前,我们还有最后一项工作。如果用户想要重置密码,应当如何操作?我们当然应该再提供一条自助式的密码重置路由,下面马上开始。


// backend/src/router.rs
pub async fn forgot_password( State(state): State<AppState>, Json(email_recipient): Json<String>,) -> Response { let new_password = Alphanumeric.sample_string(&mut rand::thread_rng(), 16);
let hashed_password = bcrypt::hash(&new_password, 10).unwrap();
sqlx::query("UPDATE users SET password = $1 WHERE email = $2") .bind(hashed_password) .bind(email_recipient) .execute(&state.postgres) .await;
let credentials = Credentials::new(state.smtp_email, state.smtp_password);
let message = format!("Hello!\n\n Your new password is: {new_password} \n\n Don't share this with anyone else. \n\n Kind regards, \nZest");
let email = Message::builder() .from("noreply <your-gmail-address-here>".parse().unwrap()) .to(format!("<{email_recipient}>").parse().unwrap()) .subject("Forgot Password") .header(ContentType::TEXT_PLAIN) .body(message) .unwrap();
// build the SMTP relay with our credentials - in this case we'll be using gmail's SMTP because it's free let mailer = SmtpTransport::relay("smtp.gmail.com") .unwrap() .credentials(credentials) .build();
// this part x`doesn't really matter since we don't want the user to explicitly know if they've actually received an email or not for security purposes, but if we do then we can create an output based on what we return to the client match mailer.send(&email) { Ok(_) => (StatusCode::OK, "Sent".to_string()).into_response(), Err(e) => (StatusCode::BAD_REQUEST, format!("Error: {e}")).into_response(), }}
复制代码


我们还需要在 Cargo.toml 层级上使用 Secrets.toml 和 Secrets.dev.toml 文件来添加必要的 secrets。为此,我们需要使用以下格式:


# Secrets.tomlSMTP_EMAIL="your-email-goes-here"SMTP_PASSWORD="your-email-password-goes-here"DOMAIN="<your-project-name-from-shuttle-toml>.shuttleapp.rs"
# You can create a Secrets.dev.toml to use secrets in a development environment - in this case, you can set domain to "127.0.0.1" and copy the other two variables as required.
复制代码


现在应用已经开发完成,接下来就是要为应用整体建立出口路由程序了。我们可以简单进行路由嵌套,并把中间件附加到受保护的路由上,如下所示:


// backend/src/router.rspub fn api_router(state: AppState) -> Router {// CORS is required for our app to work    let cors = CorsLayer::new()        .allow_credentials(true)        .allow_methods(vec![Method::GET, Method::POST, Method::PUT, Method::DELETE])        .allow_headers(vec![ORIGIN, AUTHORIZATION, ACCEPT])        .allow_origin(state.domain.parse::<HeaderValue>().unwrap());
// declare the records router let notes_router = Router::new() .route("/", get(view_records)) .route("/create", post(create_record)) .route(// you can add multiple request methods to a route like this "/:id", get(view_one_record).put(edit_record).delete(destroy_record), ) .route_layer(middleware::from_fn_with_state( state.clone(), validate_session, ));
// the routes in this router should be public, so no middleware is required let auth_router = Router::new() .route("/register", post(register)) .route("/login", post(login)) .route("/forgot", post(forgot_password)) .route("/logout", get(logout));
// return router that uses all routes from both individual routers, but add the CORS layer as well as AppState which is defined in our entrypoint function Router::new() .route("/health", get(health_check)) .nest("/notes", notes_router) .nest("/auth", auth_router) .with_state(state) .layer(cors)}
复制代码


我们可以简单定义两个路由程序来创建一个 API 路由程序,每个路由程序对应自己的路由路径(路由程序受到保护,只有会话通过验证时才会运行相应路由),之后直接返回一个带有健康检查的路由,嵌套我们之前的两个路由,最后为路由程序添加 CORS 和应用状态。


我们的最终路由函数大致如下:


// backend/src/router.rspub fn create_router(static_folder: PathBuf, state: AppState) -> Router {    let api_router = api_router(state);
// merge our static file assets Router::new() .nest("/api", api_router) .merge(SpaRouter::new("/", static_folder).index_file("index.html"))}
复制代码


我们接下来要在主函数(main.rs 当中)的初始入口点函数中使用此函数来生成路由程序,如下所示:


#[derive(Clone)]pub struct AppState {    postgres: PgPool,    key: Key,    smtp_email: String,    smtp_password: String,    domain: String,}
impl FromRef<AppState> for Key { fn from_ref(state: &AppState) -> Self { state.key.clone() }}
#[shuttle_runtime::main]async fn axum( #[shuttle_static_folder::StaticFolder] static_folder: PathBuf, #[shuttle_shared_db::Postgres] postgres: PgPool, #[shuttle_secrets::Secrets] secrets: SecretStore,) -> shuttle_axum::ShuttleAxum { sqlx::migrate!() .run(&postgres) .await .expect("Something went wrong with migrating :(");
let smtp_email = secrets .get("SMTP_EMAIL") .expect("You need to set your SMTP_EMAIL secret!");
let smtp_password = secrets .get("SMTP_PASSWORD") .expect("You need to set your SMTP_PASSWORD secret!");
// we need to set this so we can put it in our CorsLayer let domain = secrets .get("DOMAIN") .expect("You need to set your DOMAIN secret!");
let state = AppState { postgres, key: Key::generate(), smtp_email, smtp_password, domain, };
let router = create_router(static_folder, state);
Ok(router.into()) }
复制代码


请注意,对于从文件导入的函数,如果其位于前面提到的同一文件目录当中(use router),则需要在 lib.rs 文件中对其做定义;如果大家需要将函数从一个文件导入至另一个非主入口点文件中,也得进行同样的操作。


现在编程部分全部结束,大家可以试试实际部署效果了!


部署


感谢 shuttle,整个部署流程非常简单,只需在项目的根目录中运行 npm run deploy 即可。如果没有错误,shuttle 会启动我们的应用并返回部署信息列表和由 shuttle 配置的数据库连接字符串。如果需要再次查找此数据库字符串,可以在项目的 backend 目录下运行 cargo shuttle status 命令。


在实际部署之前,大家可能还需要提前运行 cargo fmt 和 cargo clippy,因为 Web 服务的构建过程中可能出现警告或错误。如果没有这些组件,大家也可以分别用 rustup component add rustfmt 和 rustup component add clippy 来替代——这里向各位 Rust 开发者强烈推荐这两款工具,绝对是您工具箱中的必备选项。


总结


感谢大家阅读本文!希望这篇文章能带您深入了解如何轻松构建 Rust Web 服务。过去几年间,Rust 实现了显著发展,也降低了新手学习的准入门槛。如果大家还停留在 Rust“生人勿近”的旧观念中,那实在是大可不必,现在正是上手体验的好时机。相信 Rust 强大的功能和愈发完善的用户友好度会给您留下深刻印象。


原文链接:


https://joshmo.hashnode.dev/nextjs-and-rust-an-innovative-approach-to-full-stack-development


相关阅读:


为什么说 Next.js 13 是一个颠覆性版本

Next.js 实践:从 SSR 到 CSR 的优雅降级

我们如何使用 Next.js 将 React 加载时间缩短 70%

先别急着“用 Rust 重写”,可能没有说的那么安全

2023-08-27 08:006226

评论

发布
暂无评论
发现更多内容

Linux下安装Python3.6.8(超级详细)

A-刘晨阳

Linux 运维 Python3 11月月更 pip3

Linux—watch命令详解– 周期性执行命令

A-刘晨阳

Linux 运维 11月月更 周期性监控

前端leetcde算法面试套路之树

js2030code

JavaScript LeetCode

别再自己瞎写工具类了,Spring Boot 内置工具类应有尽有, 建议收藏!!

程序知音

Java spring springboot java面试 后端技术

深入理解Metrics(三):Histograms

冰心的小屋

Java metrics Histograms

前端leetcde算法面试套路之堆

js2030code

JavaScript LeetCode

深入理解Metrics(四):Meters

冰心的小屋

Java metrics meters

slurm集群调度策略详解(1)-主调度

慕冰

Slurm 调度系统 主调度

用户+案例+认证+框架一键搞定,不愧是阿里最新Spring Security全栈笔记!

Java永远的神

Java 程序员 springsecurity java面试 安全框架

生于云,长于云,华为云推动开发者数字化创新

科技热闻

Nacos 多语言体系再添新员:首个 Rust 语言版本正式发布!

阿里巴巴云原生

阿里云 云原生 nacos

赛况激烈!2022 OceanBase数据库大赛50强诞生

OceanBase 数据库

中软国际发力智联网自主创新 数智赋能千行百业

中软国际AIoT开发者社区

华为开发者大会2022 智联网

网站收录量与索引量有什么区别?

源字节1号

软件开发 小程序开发

华为首发微存储新品,破解互联网海量数据高效存储难题

科技热闻

转行数据分析?你可能需要这块敲门砖!

博文视点Broadview

【收藏】锂电材料工厂设备日常保养评分标准

PreMaint

设备管理 设备日常保养

【解决方案】国产逆袭?基于国产 MCU 的网红筋膜枪方案分析

元器件秋姐

国产芯片 BLDC方案 BLDC应用 电机控制 元器件电商

一站式工业边缘数据采集处理与设备反控实践

EMQ映云科技

物联网 IoT neuron 11月月更 eKuiper

英特尔CEO出席2022世界互联网大会:五大超级技术力量释放数字时代全新可能

科技之家

什么是双机热备?实现方式有哪些?

行云管家

高可用 ha 热备 双机热备

全网首次公开!设计模式+代码+JVM调优,不愧是阿里Java性能优化核心原理全解手册

Java全栈架构师

程序员 程序人生 性能优化 后端 java面试

【LeetCode】最长同值路径Java题解

Albert

算法 LeetCode 11月月更

Wallys//Qualcomm ipq4018/IPQ4028 Access Point /high power/Wireless Dual band 11AC Wave2 /Indoor industrial wifi5 router

wallysmeng

IPQ4018 IPQ4028

相约2023,高通公司宣布参加第六届进博会

科技热闻

直播预告|AutoML Meetup V1 第四范式 & 百度 & AWS ,共探自动机器学习最佳实践

星策开源社区

人工智能 机器学习 百度 AWS 自动机器学习

vivo鲁京辉:从体验到生态,守正创新,vivo隐私安全再升级

Geek_2d6073

推荐一份关于JDK实现&源码解读的经典著作,强烈建议Java程序员都看看

程序员小毕

程序员 程序人生 Java并发 后端 jdk源码

Linux—nmap、nc命令 –网络探测工具和安全和端口扫描器

A-刘晨阳

Linux 运维 网络 11月月更

【C语言】double 关键字

謓泽

11月月更

前端leetcde算法面试套路之二叉树

js2030code

JavaScript LeetCode

Next.js + Rust 革新全栈开发,Rust没那么难_架构/框架_Josh Mo_InfoQ精选文章