2019-12-14 05:39:51 +01:00
|
|
|
## Building a 150 KB web blog in V with 0 dependencies
|
|
|
|
|
2019-12-14 03:31:09 +01:00
|
|
|
Hello,
|
|
|
|
|
2020-01-18 17:03:47 +01:00
|
|
|
In this guide, we'll build a simple web blog in V.
|
2019-12-14 03:31:09 +01:00
|
|
|
|
|
|
|
The benefits of using V for web:
|
2020-07-01 10:30:22 +02:00
|
|
|
- A safe, fast, language with the development agility of Python or Ruby and
|
2019-12-14 03:31:09 +01:00
|
|
|
the performance of C.
|
|
|
|
- Zero dependencies: everything you need for web development comes with the language
|
|
|
|
in a 1 MB package.
|
|
|
|
- Very small resulting binaries: the blog we'll create in this tutorial is about 150 KB.
|
|
|
|
- Easy deployments: a single binary file that even includes the precompiled templates.
|
2019-12-14 05:36:18 +01:00
|
|
|
- Runs on the cheapest hardware with minimum footprint: for most apps a $3 instance
|
2019-12-14 03:31:09 +01:00
|
|
|
is enough.
|
2019-12-14 06:24:14 +01:00
|
|
|
- Fast development without any boilerplate.
|
2019-12-14 03:31:09 +01:00
|
|
|
|
|
|
|
*Please note that V and Vweb are at a very early stage and are changing rapidly.*
|
|
|
|
|
2019-12-14 16:50:25 +01:00
|
|
|
The code is available <a href='https://github.com/vlang/v/tree/master/tutorials/code/blog'>here</a>.
|
2019-12-14 05:36:18 +01:00
|
|
|
|
2019-12-14 03:31:09 +01:00
|
|
|
|
|
|
|
### Installing V
|
|
|
|
|
|
|
|
```
|
|
|
|
wget https://github.com/vlang/v/releases/latest/download/v_linux.zip
|
|
|
|
unzip v_linux.zip
|
|
|
|
cd v
|
|
|
|
sudo ./v symlink
|
|
|
|
```
|
|
|
|
|
|
|
|
Now V should be globally available on your system.
|
|
|
|
|
|
|
|
> 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
|
|
|
|
from source, follow the simple instructions here:
|
|
|
|
https://github.com/vlang/v#installing-v-from-source
|
|
|
|
|
|
|
|
|
|
|
|
### Creating a new Vweb project
|
|
|
|
|
2019-12-14 05:36:18 +01:00
|
|
|
V projects can be created anywhere and don't need to have a certain structure:
|
2019-12-14 03:31:09 +01:00
|
|
|
|
|
|
|
```bash
|
|
|
|
mkdir blog
|
|
|
|
cd blog
|
|
|
|
touch blog.v
|
|
|
|
```
|
|
|
|
|
2020-01-18 17:03:47 +01:00
|
|
|
First, let's create a simple hello world website:
|
2019-12-14 03:31:09 +01:00
|
|
|
|
|
|
|
```v
|
|
|
|
// blog.v
|
|
|
|
module main
|
|
|
|
|
2020-05-20 14:21:30 +02:00
|
|
|
import vweb
|
2019-12-14 03:31:09 +01:00
|
|
|
|
2020-07-01 00:45:19 +02:00
|
|
|
struct App {
|
|
|
|
pub mut:
|
2019-12-14 03:31:09 +01:00
|
|
|
vweb vweb.Context
|
|
|
|
}
|
|
|
|
|
|
|
|
fn main() {
|
2020-07-01 00:45:19 +02:00
|
|
|
vweb.run<App>(8081)
|
2019-12-14 03:31:09 +01:00
|
|
|
}
|
|
|
|
|
2020-07-07 22:01:18 +02:00
|
|
|
pub fn (mut app App) index() vweb.Result {
|
2019-12-14 03:31:09 +01:00
|
|
|
app.vweb.text('Hello, world from vweb!')
|
2020-07-04 18:56:18 +02:00
|
|
|
return vweb.Result{}
|
2019-12-14 03:31:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn (app &App) init() {}
|
2020-07-01 00:45:19 +02:00
|
|
|
pub fn (app &App) init_once() {}
|
2019-12-14 03:31:09 +01:00
|
|
|
```
|
|
|
|
|
|
|
|
Run it with
|
|
|
|
|
|
|
|
```bash
|
|
|
|
v run blog.v
|
|
|
|
```
|
|
|
|
|
2019-12-14 16:59:02 +01:00
|
|
|
```
|
2020-07-01 00:45:19 +02:00
|
|
|
Running a Vweb app on http://localhost:8081 ...
|
2019-12-14 16:59:02 +01:00
|
|
|
```
|
|
|
|
|
2020-07-01 00:45:19 +02:00
|
|
|
Vweb helpfully provided a link, open http://localhost:8081/ in your browser:
|
2019-12-14 03:31:09 +01:00
|
|
|
|
2019-12-14 17:26:22 +01:00
|
|
|
<img width=662 src="https://github.com/vlang/v/blob/master/tutorials/img/hello.png?raw=true">
|
2019-12-14 03:31:09 +01:00
|
|
|
|
|
|
|
The `App` struct is an entry point of our web application. If you have experience
|
|
|
|
with an MVC web framework, you can think of it as a controller. (Vweb is
|
|
|
|
not an MVC framework however.)
|
|
|
|
|
|
|
|
As you can see, there are no routing rules. The `index()` action handles the `/` request by default.
|
2020-01-18 17:03:47 +01:00
|
|
|
Vweb often uses convention over configuration and adding a new action requires
|
2019-12-14 03:31:09 +01:00
|
|
|
no routing rules either:
|
|
|
|
|
|
|
|
```v
|
2020-07-28 10:29:51 +02:00
|
|
|
fn (mut app App) time() vweb.Result {
|
2019-12-14 03:31:09 +01:00
|
|
|
app.vweb.text(time.now().format())
|
2020-07-28 10:29:51 +02:00
|
|
|
return vweb.Result{}
|
2019-12-14 03:31:09 +01:00
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
|
2019-12-14 17:26:22 +01:00
|
|
|
<img width=662 src="https://github.com/vlang/v/blob/master/tutorials/img/time.png?raw=true">
|
2019-12-14 03:31:09 +01:00
|
|
|
|
|
|
|
>You have to rebuild and restart the website every time you change the code.
|
2020-01-18 17:03:47 +01:00
|
|
|
In the future, Vweb will detect changes and recompile the website in the background
|
2019-12-14 03:31:09 +01:00
|
|
|
while it's running.
|
|
|
|
|
2020-01-18 17:03:47 +01:00
|
|
|
The `.text(string)` method returns a plain text document with the provided
|
2019-12-14 03:31:09 +01:00
|
|
|
text, which isn't frequently used in websites.
|
|
|
|
|
|
|
|
|
|
|
|
### HTML View
|
|
|
|
|
|
|
|
Let's return an HTML view instead. Create `index.html` in the same directory:
|
|
|
|
|
|
|
|
```html
|
|
|
|
<html>
|
2020-07-28 10:29:51 +02:00
|
|
|
<head>
|
2019-12-14 03:31:09 +01:00
|
|
|
<title>V Blog</title>
|
2020-07-28 10:29:51 +02:00
|
|
|
</head>
|
2019-12-14 03:31:09 +01:00
|
|
|
<body>
|
|
|
|
<b>@message</b>
|
|
|
|
<br>
|
|
|
|
<img src='https://vlang.io/img/v-logo.png' width=100>
|
|
|
|
</body>
|
|
|
|
</html>
|
|
|
|
```
|
|
|
|
|
|
|
|
and update our `index()` action so that it returns the HTML view we just created:
|
|
|
|
|
|
|
|
```v
|
2020-07-07 22:01:18 +02:00
|
|
|
pub fn (mut app App) index() vweb.Result {
|
2019-12-14 03:31:09 +01:00
|
|
|
message := 'Hello, world from Vweb!'
|
2020-07-04 18:56:18 +02:00
|
|
|
return $vweb.html()
|
2019-12-14 03:31:09 +01:00
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2019-12-14 17:26:22 +01:00
|
|
|
<img width=662 src="https://github.com/vlang/v/blob/master/tutorials/img/hello_html.png?raw=true">
|
2019-12-14 03:31:09 +01:00
|
|
|
|
|
|
|
Good, now we have an actual HTML page.
|
|
|
|
|
|
|
|
The V template language is similar to C#'s Razor: `@message` prints the value
|
|
|
|
of `message`.
|
|
|
|
|
|
|
|
You may notice something unusual: the `message` variable created in the `index()`
|
|
|
|
action is automatically available in the view.
|
|
|
|
|
|
|
|
It's another feature of Vweb to reduce the boilerplate in your web apps.
|
|
|
|
No need to create view models just to pass data, or use an unsafe and untyped
|
|
|
|
alternative, like C#'s `ViewBag["message"]`.
|
|
|
|
|
2020-01-18 17:03:47 +01:00
|
|
|
Making all action variables available in the view may seem crazy,
|
2019-12-14 03:31:09 +01:00
|
|
|
but V is a language with pure functions by default, and you won't be able
|
|
|
|
to modify any data from a view. `<b>@foo.bar()</b>` will only work if the `bar()` method
|
|
|
|
doesn't modify `foo`.
|
|
|
|
|
|
|
|
The HTML template is compiled to V during the compilation of the website, that's done by the `$vweb.html()` line.
|
|
|
|
(`$` always means compile time actions in V.) offering the following benefits:
|
|
|
|
|
|
|
|
- Great performance, since the templates don't need to be compiled
|
|
|
|
on every request, like in almost every major web framework.
|
|
|
|
|
|
|
|
- Easier deployment, since all your HTML templates are compiled
|
|
|
|
into a single binary file together with the web application itself.
|
|
|
|
|
|
|
|
- All errors in the templates are guaranteed to be caught during compilation.
|
|
|
|
|
2020-07-04 18:56:18 +02:00
|
|
|
|
2019-12-14 03:31:09 +01:00
|
|
|
### Fetching data with V ORM
|
|
|
|
|
|
|
|
Now let's display some articles!
|
|
|
|
|
2020-07-04 18:56:18 +02:00
|
|
|
We'll be using V's builtin ORM and a SQLite database.
|
|
|
|
(V ORM will also support MySQL, Postgre, and SQL Server soon.)
|
2019-12-14 03:31:09 +01:00
|
|
|
|
2020-07-04 18:56:18 +02:00
|
|
|
Create a SQLite file with the schema:
|
2019-12-14 03:31:09 +01:00
|
|
|
```sql
|
2020-07-07 22:01:18 +02:00
|
|
|
drop table if exists Article;
|
2019-12-14 03:31:09 +01:00
|
|
|
|
2020-07-04 18:56:18 +02:00
|
|
|
create table Article (
|
|
|
|
id integer primary key,
|
|
|
|
title text default "",
|
|
|
|
text text default ""
|
2019-12-14 03:31:09 +01:00
|
|
|
);
|
|
|
|
|
2020-07-04 18:56:18 +02:00
|
|
|
insert into Article (title, text) values (
|
|
|
|
"Hello, world!",
|
|
|
|
"V is great."
|
2019-12-14 03:31:09 +01:00
|
|
|
);
|
|
|
|
|
2020-07-04 18:56:18 +02:00
|
|
|
insert into Article (title, text) values (
|
|
|
|
"Second post.",
|
|
|
|
"Hm... what should I write about?"
|
2019-12-14 03:31:09 +01:00
|
|
|
);
|
|
|
|
```
|
|
|
|
|
2020-07-04 18:56:18 +02:00
|
|
|
Run the file with `sqlite3 blog.db < blog.sqlite`.
|
2019-12-14 03:31:09 +01:00
|
|
|
|
|
|
|
|
2020-07-04 18:56:18 +02:00
|
|
|
Add a SQLite handle to `App`:
|
2019-12-14 03:31:09 +01:00
|
|
|
|
|
|
|
```v
|
2020-07-04 18:56:18 +02:00
|
|
|
import sqlite
|
|
|
|
|
2019-12-14 03:31:09 +01:00
|
|
|
struct App {
|
2020-07-01 00:45:19 +02:00
|
|
|
pub mut:
|
2019-12-14 03:31:09 +01:00
|
|
|
vweb vweb.Context
|
2020-07-04 18:56:18 +02:00
|
|
|
db sqlite.DB
|
2019-12-14 03:31:09 +01:00
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
2020-07-01 00:45:19 +02:00
|
|
|
Modify the `init_once()` method we created earlier to connect to a database:
|
2019-12-14 03:31:09 +01:00
|
|
|
|
|
|
|
```v
|
2020-07-01 00:45:19 +02:00
|
|
|
pub fn (mut app App) init_once() {
|
2020-07-04 18:56:18 +02:00
|
|
|
db := sqlite.connect(':memory:') or { panic(err) }
|
|
|
|
db.exec('create table `Article` (id integer primary key, title text default "", text text default "")')
|
|
|
|
db.exec('insert into Article (title, text) values ("Hello, world!", "V is great.")')
|
|
|
|
db.exec('insert into Article (title, text) values ("Second post.", "Hm... what should I write about?")')
|
2019-12-14 03:31:09 +01:00
|
|
|
app.db = db
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2020-07-01 00:45:19 +02:00
|
|
|
Code in the `init_once()` function is run only once during app's startup, so we are going
|
2019-12-14 03:31:09 +01:00
|
|
|
to have one DB connection for all requests.
|
|
|
|
|
|
|
|
Create a new file `article.v`:
|
|
|
|
|
|
|
|
|
|
|
|
```v
|
2020-07-04 18:56:18 +02:00
|
|
|
// article.v
|
2019-12-14 03:31:09 +01:00
|
|
|
module main
|
|
|
|
|
|
|
|
struct Article {
|
|
|
|
id int
|
|
|
|
title string
|
|
|
|
text string
|
|
|
|
}
|
|
|
|
|
2019-12-14 17:58:55 +01:00
|
|
|
pub fn (app &App) find_all_articles() []Article {
|
2020-07-01 00:45:19 +02:00
|
|
|
return sql app.db {
|
|
|
|
select from Article
|
|
|
|
}
|
2019-12-14 03:31:09 +01:00
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
Let's fetch the articles in the `index()` action:
|
|
|
|
|
|
|
|
```v
|
2020-07-07 22:01:18 +02:00
|
|
|
pub fn (app &App) index() vweb.Result {
|
2019-12-14 03:31:09 +01:00
|
|
|
articles := app.find_all_articles()
|
2020-07-01 00:45:19 +02:00
|
|
|
return $vweb.html()
|
2019-12-14 03:31:09 +01:00
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Finally, let's update our view:
|
|
|
|
|
|
|
|
```html
|
|
|
|
<body>
|
|
|
|
@for article in articles
|
|
|
|
<div>
|
|
|
|
<b>@article.title</b> <br>
|
|
|
|
@article.text
|
|
|
|
</div>
|
|
|
|
@end
|
|
|
|
</body>
|
|
|
|
```
|
|
|
|
|
|
|
|
```bash
|
|
|
|
v run .
|
|
|
|
```
|
|
|
|
|
2019-12-14 17:26:22 +01:00
|
|
|
<img width=662 src="https://github.com/vlang/v/blob/master/tutorials/img/articles1.png?raw=true">
|
2019-12-14 03:31:09 +01:00
|
|
|
|
|
|
|
That was very simple, wasn't it?
|
|
|
|
|
|
|
|
The built-in V ORM uses a syntax very similar to SQL. The queries are built with V.
|
|
|
|
For example, if we only wanted to find articles with ids between 100 and 200, we'd do:
|
|
|
|
|
|
|
|
```
|
2020-07-01 00:45:19 +02:00
|
|
|
return sql app.db {
|
|
|
|
select from Article where id >= 100 && id <= 200
|
|
|
|
}
|
2019-12-14 03:31:09 +01:00
|
|
|
```
|
|
|
|
|
|
|
|
Retrieving a single article is very simple:
|
|
|
|
|
|
|
|
```v
|
|
|
|
|
|
|
|
pub fn (app &App) retrieve_article() ?Article {
|
2020-07-01 00:45:19 +02:00
|
|
|
return sql app.db {
|
|
|
|
select from Article limit 1
|
|
|
|
}
|
2019-12-14 03:31:09 +01:00
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
V ORM uses V's optionals for single values, which is very useful, since
|
|
|
|
bad queries will always be handled by the developer:
|
|
|
|
|
|
|
|
```v
|
|
|
|
article := app.retrieve_article(10) or {
|
|
|
|
app.vweb.text('Article not found')
|
|
|
|
return
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
|
2019-12-14 22:55:05 +01:00
|
|
|
### Adding new articles
|
|
|
|
|
|
|
|
Create `new.html`:
|
|
|
|
|
|
|
|
```html
|
|
|
|
<html>
|
2020-07-28 10:29:51 +02:00
|
|
|
<head>
|
2019-12-14 22:55:05 +01:00
|
|
|
<title>V Blog</title>
|
2020-07-28 10:29:51 +02:00
|
|
|
</head>
|
2019-12-14 22:55:05 +01:00
|
|
|
<body>
|
|
|
|
<form action='/new_article' method='post'>
|
|
|
|
<input type='text' placeholder='Title' name='title'> <br>
|
|
|
|
<textarea placeholder='Text' name='text'></textarea>
|
|
|
|
<input type='submit'>
|
|
|
|
</form>
|
|
|
|
</body>
|
|
|
|
</html>
|
|
|
|
```
|
|
|
|
|
|
|
|
```v
|
2020-07-01 00:45:19 +02:00
|
|
|
pub fn (mut app App) new_article() vweb.Result {
|
2019-12-14 22:55:05 +01:00
|
|
|
title := app.vweb.form['title']
|
|
|
|
text := app.vweb.form['text']
|
|
|
|
if title == '' || text == '' {
|
2019-12-14 23:00:04 +01:00
|
|
|
app.vweb.text('Empty text/title')
|
2020-07-28 10:29:51 +02:00
|
|
|
return vweb.Result{}
|
2019-12-14 22:55:05 +01:00
|
|
|
}
|
|
|
|
article := Article{
|
|
|
|
title: title
|
|
|
|
text: text
|
|
|
|
}
|
2020-07-04 18:56:18 +02:00
|
|
|
println(article)
|
2020-07-01 00:45:19 +02:00
|
|
|
sql app.db {
|
|
|
|
insert article into Article
|
|
|
|
}
|
2020-07-28 10:29:51 +02:00
|
|
|
app.vweb.redirect('/')
|
|
|
|
return vweb.Result{}
|
2019-12-14 22:55:05 +01:00
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
> Untyped `form['key']` is temporary. Very soon Vweb will accept query and form
|
|
|
|
parameters via function arguments: `new_article(title, text string) {`.
|
|
|
|
|
|
|
|
We need to update `index.html` to add a link to the "new article" page:
|
2019-12-14 23:00:04 +01:00
|
|
|
|
|
|
|
```html
|
2019-12-14 22:55:05 +01:00
|
|
|
<a href='/new'>New article</a>
|
2019-12-14 23:00:04 +01:00
|
|
|
```
|
2019-12-14 22:55:05 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### JSON endpoints
|
|
|
|
|
2020-01-18 17:03:47 +01:00
|
|
|
This tutorial used the traditional server-side rendering. If you prefer
|
2019-12-14 22:55:05 +01:00
|
|
|
to render everything on the client or need an API, creating JSON endpoints
|
|
|
|
in V is very simple:
|
|
|
|
|
|
|
|
```v
|
2020-07-28 10:29:51 +02:00
|
|
|
pub fn (mut app App) articles() vweb.Result {
|
2019-12-14 22:55:05 +01:00
|
|
|
articles := app.find_all_articles()
|
|
|
|
app.vweb.json(json.encode(articles))
|
2020-07-28 10:29:51 +02:00
|
|
|
return vweb.Result{}
|
2019-12-14 22:55:05 +01:00
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
<img width=662 src="https://github.com/vlang/v/blob/master/tutorials/img/articles_json.png?raw=true">
|
|
|
|
|
|
|
|
|
|
|
|
|
2019-12-16 23:21:10 +01:00
|
|
|
To be continued...
|
2019-12-14 03:31:09 +01:00
|
|
|
|
2019-12-14 06:24:14 +01:00
|
|
|
For an example of a more sophisticated web app written in V, check out Vorum: https://github.com/vlang/vorum
|