veb: deprecate vweb (part 1); update the blog tutorial

This commit is contained in:
Alexander Medvednikov 2025-03-01 03:34:16 +03:00
parent 8dbde185ef
commit f83af8a1a2
4 changed files with 134 additions and 114 deletions

View File

@ -4,7 +4,7 @@ import os
import log import log
import flag import flag
import time import time
import vweb import veb
import net.urllib import net.urllib
// This tool regenerates V's bootstrap .c files // This tool regenerates V's bootstrap .c files
@ -79,11 +79,14 @@ mut:
// webhook server // webhook server
struct WebhookServer { struct WebhookServer {
vweb.Context
mut: mut:
gen_vc &GenVC = unsafe { nil } // initialized in init_server gen_vc &GenVC = unsafe { nil } // initialized in init_server
} }
struct Context {
veb.Context
}
// storage for flag options // storage for flag options
struct FlagOptions { struct FlagOptions {
work_dir string work_dir string
@ -116,7 +119,8 @@ fn main() {
} }
// webhook server mode // webhook server mode
if flag_options.serve { if flag_options.serve {
vweb.run[WebhookServer](&WebhookServer{}, flag_options.port) mut server := &WebhookServer{}
veb.run_at[WebhookServer, Context](mut server, port: flag_options.port)!
} else { } else {
// cmd mode // cmd mode
mut gen_vc := new_gen_vc(flag_options) mut gen_vc := new_gen_vc(flag_options)
@ -152,7 +156,7 @@ pub fn (mut ws WebhookServer) index() {
} }
// gen webhook // gen webhook
pub fn (mut ws WebhookServer) genhook() { pub fn (mut ws WebhookServer) genhook() veb.Result {
// request data // request data
// println(ws.vweb.req.data) // println(ws.vweb.req.data)
// TODO: parse request. json or urlencoded // TODO: parse request. json or urlencoded
@ -160,10 +164,9 @@ pub fn (mut ws WebhookServer) genhook() {
ws.gen_vc.generate() ws.gen_vc.generate()
// error in generate // error in generate
if ws.gen_vc.gen_error { if ws.gen_vc.gen_error {
ws.json('{status: "failed"}') return ctx.json('{status: "failed"}')
return
} }
ws.json('{status: "ok"}') return ctx.json('{status: "ok"}')
} }
pub fn (ws &WebhookServer) reset() { pub fn (ws &WebhookServer) reset() {

View File

@ -1,28 +1,24 @@
## Building a 400 KB web blog in V & SQLite # Building a 400 KB Web Blog in V & SQLite with Veb
Hello, Hello,
In this guide, we'll build a simple web blog in V. In this guide, we'll build a simple web blog using V and the Veb framework.
The benefits of using V for web: The benefits of using V and Veb for web development:
- A safe, fast, language with the development agility of Python or Ruby and - A safe, fast language with the development agility of Python or Ruby and the performance of C
the performance of C. - Zero dependencies: everything needed comes in a 1 MB package
- Zero dependencies: everything you need for web development comes with the language - Very small binaries: this blog will be about 150 KB
in a 1 MB package. - Easy deployments: single binary including precompiled templates
- Very small resulting binaries: the blog we'll create in this tutorial is about 150 KB. - Runs on minimal hardware: $3 instance sufficient for most apps
- Easy deployments: a single binary file that even includes the precompiled templates. - Fast development with minimal boilerplate
- Runs on the cheapest hardware with minimum footprint: for most apps a $3 instance
is enough.
- Fast development without any boilerplate.
> [!NOTE] > [!NOTE]
> V and Vweb are at a very early stage and are changing rapidly. > V and Veb are at an early stage and evolving rapidly.
The code is available [here](./code/blog). The code is available [here](./code/blog).
### Installing V ## Installing V
``` ```
wget --quiet https://github.com/vlang/v/releases/latest/download/v_linux.zip wget --quiet https://github.com/vlang/v/releases/latest/download/v_linux.zip
unzip v_linux.zip unzip v_linux.zip
@ -30,21 +26,20 @@ cd v
sudo ./v symlink sudo ./v symlink
``` ```
Now V should be globally available on your system.
V should now be globally available.
> On macOS use `v_macos.zip`, on Windows - `v_windows.zip`. > On macOS use `v_macos.zip`, on Windows - `v_windows.zip`.
> If you use a BSD system, Solaris, Android, or simply want to install V > For BSD, Solaris, Android, or source installation, see:
> from source, follow the simple instructions here:
> https://github.com/vlang/v#installing-v-from-source > https://github.com/vlang/v#installing-v-from-source
### Install SQLite development dependency ## Install SQLite Development Dependency
If you don't have it already installed, look at the See [`sqlite` README](../../vlib/db/sqlite/README.md) for instructions if not already installed.
[`sqlite` README](../../vlib/db/sqlite/README.md) for instructions.
### Creating a new Vweb project ## Creating a New Veb Project
V projects can be created anywhere and don't need to have a certain structure: V projects can be created anywhere:
```bash ```bash
mkdir blog mkdir blog
@ -58,20 +53,21 @@ First, let's create a simple hello world website:
// blog.v // blog.v
module main module main
import vweb import veb
struct App { pub struct Context {
vweb.Context veb.Context
} }
pub struct App {}
fn main() { fn main() {
app := App{} mut app := &App{}
vweb.run(app, 8081) veb.run[App, Context](mut app, 8081)
} }
@['/index'] pub fn (app &App) index(mut ctx Context) veb.Result {
pub fn (mut app App) index() vweb.Result { return ctx.text('Hello world from Veb!')
return app.text('Hello world from vweb!')
} }
``` ```
@ -82,17 +78,20 @@ v run blog.v
``` ```
``` ```
Running a Vweb app on http://localhost:8081 ... Running a Veb app on http://localhost:8081 ...
``` ```
Vweb helpfully provided a link, open http://localhost:8081/ in your browser: Veb helpfully provided a link, open http://localhost:8081/ in your browser:
<img width=662 src="https://github.com/vlang/v/blob/master/tutorials/building_a_simple_web_blog_with_vweb/img/hello.png?raw=true"> <img width=662 src="https://github.com/vlang/v/blob/master/tutorials/building_a_simple_web_blog_with_vweb/img/hello.png?raw=true">
The `App` struct is an entry point of our web application. If you have experience The App struct holds shared application data, while Context handles per-request data and embeds
with an MVC web framework, you can think of it as a controller. (Vweb is `veb.Context` for response methods like `.text()`.
not an MVC framework however.) It embeds the vweb Context object, that's why we get access
to methods like `.text()`. If you have experiencewith an MVC web framework, you can think of it as a controller. (Veb is
not an MVC framework however.)
As you can see, there are no routing rules. The `index()` action handles the `/` request by default. As you can see, there are no routing rules. The `index()` action handles the `/` request by default.
Vweb often uses convention over configuration and adding a new action requires Vweb often uses convention over configuration and adding a new action requires
@ -100,14 +99,16 @@ no routing rules either:
```v oksyntax ```v oksyntax
// blog.v // blog.v
import vweb import veb
import time import time
fn (mut app App) time() vweb.Result { fn (mut app App) time() veb.Result {
return app.text(time.now().format()) return app.text(time.now().format())
} }
``` ```
Custom routes can be defined using attributes like @['/index'].
<img width=662 src="https://github.com/vlang/v/blob/master/tutorials/building_a_simple_web_blog_with_vweb/img/time.png?raw=true"> <img width=662 src="https://github.com/vlang/v/blob/master/tutorials/building_a_simple_web_blog_with_vweb/img/time.png?raw=true">
> TIP: run the following command to live-reload the server: `v watch run blog.v` > TIP: run the following command to live-reload the server: `v watch run blog.v`
@ -122,12 +123,12 @@ Let's return an HTML view instead. Create `index.html` in the same directory:
```html ```html
<html> <html>
<head> <head>
<title>V Blog</title> <title>V Blog</title>
</head> </head>
<body> <body>
<b>@message</b> <b>@message</b>
<br /> <br />
<img src="https://vlang.io/img/v-logo.png" width="100" /> <img src="https://vlang.io/img/v-logo.png" width="100" />
</body> </body>
</html> </html>
``` ```
@ -136,9 +137,9 @@ and update our `index()` action so that it returns the HTML view we just created
```v ignore ```v ignore
// blog.v // blog.v
pub fn (mut app App) index() vweb.Result { pub fn (mut app App) index() veb.Result {
message := 'Hello, world from Vweb!' message := 'Hello, world from Vweb!'
return $vweb.html() return $veb.html()
} }
``` ```
@ -162,7 +163,7 @@ to modify any data from a view. `<b>@foo.bar()</b>` will only work if the `bar()
doesn't modify `foo`. doesn't modify `foo`.
The HTML template is compiled to V during the compilation of the website, The HTML template is compiled to V during the compilation of the website,
that's done by the `$vweb.html()` line. that's done by the `$veb.html()` line.
(`$` always means compile time actions in V.) offering the following benefits: (`$` always means compile time actions in V.) offering the following benefits:
- Great performance, since the templates don't need to be compiled - Great performance, since the templates don't need to be compiled
@ -185,12 +186,12 @@ Add a SQLite handle to `App`:
```v oksyntax ```v oksyntax
// blog.v // blog.v
import db.sqlite import db.sqlite
import vweb import veb
struct App { struct App {
vweb.Context // ...
pub mut:
db sqlite.DB db sqlite.DB
// ...
} }
``` ```
@ -202,7 +203,7 @@ since a DB connection doesn't have to be set up for each request.
```v oksyntax ```v oksyntax
// blog.v // blog.v
fn main() { fn main() {
mut app := App{ mut app := &App{
db: sqlite.connect(':memory:')! db: sqlite.connect(':memory:')!
} }
sql app.db { sql app.db {
@ -223,7 +224,7 @@ fn main() {
insert first_article into Article insert first_article into Article
insert second_article into Article insert second_article into Article
}! }!
vweb.run(app, 8080) veb.run[App, Context](mut app, 8081)
} }
``` ```
@ -254,9 +255,9 @@ Let's fetch the articles in the `index()` action:
```v ignore ```v ignore
// blog.v // blog.v
pub fn (app &App) index() vweb.Result { pub fn (app &App) index() veb.Result {
articles := app.find_all_articles() articles := app.find_all_articles()
return $vweb.html() return $veb.html()
} }
``` ```
@ -266,8 +267,8 @@ Finally, let's update our view:
<body> <body>
@for article in articles @for article in articles
<div> <div>
<b>@article.title</b> <br /> <b>@article.title</b> <br />
@article.text @article.text
</div> </div>
@end @end
</body> </body>
@ -309,7 +310,7 @@ bad queries will always be handled by the developer:
```v ignore ```v ignore
// article.v // article.v
article := app.retrieve_article() or { article := app.retrieve_article() or {
return app.text('Article not found') return ctx.text('Article not found')
} }
``` ```
@ -320,40 +321,45 @@ Create `new.html`:
```html ```html
<html> <html>
<head> <head>
<title>V Blog</title> <title>V Blog</title>
</head> </head>
<body> <body>
<form action="/new_article" method="post"> <form action="/new_article" method="post">
<input type="text" placeholder="Title" name="title" /> <br /> <input type="text" placeholder="Title" name="title" /> <br />
<textarea placeholder="Text" name="text"></textarea> <textarea placeholder="Text" name="text"></textarea>
<input type="submit" /> <input type="submit" />
</form> </form>
</body> </body>
</html> </html>
``` ```
```v ignore ```v ignore
// article.v @['/new']
import vweb pub fn (app &App) new(mut ctx Context) veb.Result {
return $veb.html()
}
@['/new_article'; post]
pub fn (app &App) new_article(mut ctx Context) veb.Result {
title := ctx.form['title'] or { '' }
text := ctx.form['text'] or { '' }
@[post]
pub fn (mut app App) new_article(title string, text string) vweb.Result {
if title == '' || text == '' { if title == '' || text == '' {
return app.text('Empty text/title') return ctx.text('Empty text/title')
} }
article := Article{ article := Article{
title: title title: title
text: text text: text
} }
println(article)
sql app.db { sql app.db {
insert article into Article insert article into Article
} or { panic(err) } } or { panic(err) }
return app.redirect('/') return ctx.redirect('/')
} }
``` ```
The decorator on our function tells vweb that it is an HTTP POST type operation. The decorator on our function tells Veb that it is an HTTP POST type operation.
This time Vweb parses the HTTP form and assigns correct values with correct types to This time Vweb parses the HTTP form and assigns correct values with correct types to
function arguments, which saves a lot of typing (e.g. `title := app.form['title']` is function arguments, which saves a lot of typing (e.g. `title := app.form['title']` is
@ -369,8 +375,8 @@ Next we need to add the HTML endpoint to our code like we did with `index.html`:
```v ignore ```v ignore
@['/new'] @['/new']
pub fn (mut app App) new() vweb.Result { pub fn (mut app App) new() veb.Result {
return $vweb.html() return $veb.html()
} }
``` ```
@ -383,11 +389,10 @@ to render everything on the client or need an API, creating JSON endpoints
in V is very simple: in V is very simple:
```v oksyntax ```v oksyntax
// article.v import veb
import vweb
@['/articles'; get] @['/articles'; get]
pub fn (mut app App) articles() vweb.Result { pub fn (mut app App) articles() veb.Result {
articles := app.find_all_articles() articles := app.find_all_articles()
return app.json(articles) return app.json(articles)
} }
@ -417,10 +422,10 @@ Run
v -d use_openssl -o blog -prod . && strip ./blog v -d use_openssl -o blog -prod . && strip ./blog
``` ```
This will result in a ~400KB binary. `-d use_openssl` tells vweb to link to OpenSSL. This will result in a ~400KB binary. `-d use_openssl` tells Veb to link to OpenSSL.
Without this flag mbedtls will be embedded, and the binary size will increase to ~700KB. Without this flag mbedtls will be embedded, and the binary size will increase to ~700KB.
### To be continued... ### To be continued...
For an example of a more sophisticated web app written in V, check out Vorum: https://github.com/vlang/vorum For an example of a more sophisticated web app written in V, check out Gitly: https://github.com/vlang/gitly

View File

@ -1,78 +1,90 @@
module main module main
import vweb import veb
import time import time
import db.sqlite import db.sqlite
import json import json
struct App { // Context struct must embed veb.Context
vweb.Context pub struct Context {
veb.Context
pub mut: pub mut:
db sqlite.DB
user_id string user_id string
} }
// App struct for shared data
pub struct App {
pub mut:
db sqlite.DB
}
// Main function
fn main() { fn main() {
mut app := App{ mut app := &App{
db: sqlite.connect('blog.db') or { panic(err) } db: sqlite.connect('blog.db') or { panic(err) }
} }
sql app.db { sql app.db {
create table Article create table Article
}! }!
vweb.run(app, 8081) // Use veb.run with App and Context types
veb.run[App, Context](mut app, 8081)
} }
/* /*
pub fn (mut app App) index_text() vweb.Result { // Middleware to run before each request
app.vweb.text('Hello, world from vweb!') pub fn (mut ctx Context) before_request() bool {
return vweb.Result{} ctx.user_id = ctx.get_cookie('id') or { '0' }
} return true
pub fn (app &App) index_html() vweb.Result {
message := 'Hello, world from Vweb!'
return $vweb.html()
} }
*/ */
// Index endpoint
@['/index'] @['/index']
pub fn (app &App) index() vweb.Result { pub fn (app &App) index(mut ctx Context) veb.Result {
articles := app.find_all_articles() articles := app.find_all_articles()
return $vweb.html() return $veb.html()
}
pub fn (mut app App) before_request() {
app.user_id = app.get_cookie('id') or { '0' }
} }
// New article form endpoint
@['/new'] @['/new']
pub fn (mut app App) new() vweb.Result { pub fn (app &App) new(mut ctx Context) veb.Result {
return $vweb.html() return $veb.html()
} }
// Create new article endpoint
@['/new_article'; post] @['/new_article'; post]
pub fn (mut app App) new_article(title string, text string) vweb.Result { pub fn (app &App) new_article(mut ctx Context) veb.Result {
title := ctx.form['title'] or { '' }
text := ctx.form['text'] or { '' }
if title == '' || text == '' { if title == '' || text == '' {
return app.text('Empty text/title') return ctx.text('Empty text/title')
} }
article := Article{ article := Article{
title: title title: title
text: text text: text
} }
println('posting article') println('posting article')
println(article) println(article)
sql app.db { sql app.db {
insert article into Article insert article into Article
} or {} } or {}
return app.redirect('/') return ctx.redirect('/')
} }
// Get all articles endpoint
@['/articles'; get] @['/articles'; get]
pub fn (mut app App) articles() vweb.Result { pub fn (app &App) articles(mut ctx Context) veb.Result {
articles := app.find_all_articles() articles := app.find_all_articles()
json_result := json.encode(articles) json_result := json.encode(articles)
return app.json(json_result) return ctx.json(json_result)
} }
fn (mut app App) time() { // Time endpoint
app.text(time.now().format()) @['/time']
pub fn (app &App) time(mut ctx Context) veb.Result {
return ctx.text(time.now().format())
} }

View File

@ -3,7 +3,7 @@
<title>V Blog</title> <title>V Blog</title>
</head> </head>
<body> <body>
user id = @app.user_id <br> user id = @ctx.user_id <br>
@for article in articles @for article in articles
<div> <div>
<b>@article.title</b> <br> <b>@article.title</b> <br>