mirror of
https://github.com/vlang/v.git
synced 2023-08-10 21:13:21 +03:00
vweb: implement live page reload in development, based on polling (useful with watch) (#17683)
This commit is contained in:
parent
658b116d07
commit
6e1e406288
@ -309,11 +309,18 @@ fn main() {
|
|||||||
context.pid = os.getpid()
|
context.pid = os.getpid()
|
||||||
context.vexe = os.getenv('VEXE')
|
context.vexe = os.getenv('VEXE')
|
||||||
|
|
||||||
mut fp := flag.new_flag_parser(os.args[1..])
|
watch_pos := os.args.index('watch')
|
||||||
|
all_args_before_watch_cmd := os.args#[1..watch_pos]
|
||||||
|
all_args_after_watch_cmd := os.args#[watch_pos + 1..]
|
||||||
|
// dump(os.getpid())
|
||||||
|
// dump(all_args_before_watch_cmd)
|
||||||
|
// dump(all_args_after_watch_cmd)
|
||||||
|
|
||||||
|
// Options after `run` should be ignored, since they are intended for the user program, not for the watcher.
|
||||||
|
// For example, `v watch run x.v -a -b -k', should pass all of -a -b -k to the compiled and run program.
|
||||||
|
only_watch_options, has_run := all_before('run', all_args_after_watch_cmd)
|
||||||
|
mut fp := flag.new_flag_parser(only_watch_options)
|
||||||
fp.application('v watch')
|
fp.application('v watch')
|
||||||
if os.args[1] == 'watch' {
|
|
||||||
fp.skip_executable()
|
|
||||||
}
|
|
||||||
fp.version('0.0.2')
|
fp.version('0.0.2')
|
||||||
fp.description('Collect all .v files needed for a compilation, then re-run the compilation when any of the source changes.')
|
fp.description('Collect all .v files needed for a compilation, then re-run the compilation when any of the source changes.')
|
||||||
fp.arguments_description('[--silent] [--clear] [--ignore .db] [--add /path/to/a/file.v] [run] program.v')
|
fp.arguments_description('[--silent] [--clear] [--ignore .db] [--add /path/to/a/file.v] [run] program.v')
|
||||||
@ -336,7 +343,12 @@ fn main() {
|
|||||||
eprintln('Error: ${err}')
|
eprintln('Error: ${err}')
|
||||||
exit(1)
|
exit(1)
|
||||||
}
|
}
|
||||||
context.opts = remaining_options
|
context.opts = []
|
||||||
|
context.opts << all_args_before_watch_cmd
|
||||||
|
context.opts << remaining_options
|
||||||
|
if has_run {
|
||||||
|
context.opts << all_after('run', all_args_after_watch_cmd)
|
||||||
|
}
|
||||||
context.elog('>>> context.pid: ${context.pid}')
|
context.elog('>>> context.pid: ${context.pid}')
|
||||||
context.elog('>>> context.vexe: ${context.vexe}')
|
context.elog('>>> context.vexe: ${context.vexe}')
|
||||||
context.elog('>>> context.opts: ${context.opts}')
|
context.elog('>>> context.opts: ${context.opts}')
|
||||||
@ -347,14 +359,15 @@ fn main() {
|
|||||||
if context.is_worker {
|
if context.is_worker {
|
||||||
context.worker_main()
|
context.worker_main()
|
||||||
} else {
|
} else {
|
||||||
context.manager_main()
|
context.manager_main(all_args_before_watch_cmd, all_args_after_watch_cmd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn (mut context Context) manager_main() {
|
fn (mut context Context) manager_main(all_args_before_watch_cmd []string, all_args_after_watch_cmd []string) {
|
||||||
myexecutable := os.executable()
|
myexecutable := os.executable()
|
||||||
mut worker_opts := ['--vwatchworker']
|
mut worker_opts := all_args_before_watch_cmd.clone()
|
||||||
worker_opts << os.args[2..]
|
worker_opts << ['watch', '--vwatchworker']
|
||||||
|
worker_opts << all_args_after_watch_cmd
|
||||||
for {
|
for {
|
||||||
mut worker_process := os.new_process(myexecutable)
|
mut worker_process := os.new_process(myexecutable)
|
||||||
worker_process.set_args(worker_opts)
|
worker_process.set_args(worker_opts)
|
||||||
@ -384,3 +397,19 @@ fn (mut context Context) worker_main() {
|
|||||||
spawn context.compilation_runner_loop()
|
spawn context.compilation_runner_loop()
|
||||||
change_detection_loop(context)
|
change_detection_loop(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn all_before(needle string, all []string) ([]string, bool) {
|
||||||
|
needle_pos := all.index(needle)
|
||||||
|
if needle_pos == -1 {
|
||||||
|
return all, false
|
||||||
|
}
|
||||||
|
return all#[..needle_pos + 1], true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn all_after(needle string, all []string) []string {
|
||||||
|
needle_pos := all.index(needle)
|
||||||
|
if needle_pos == -1 {
|
||||||
|
return all
|
||||||
|
}
|
||||||
|
return all#[needle_pos + 1..]
|
||||||
|
}
|
||||||
|
4
examples/vweb/footer.html
Normal file
4
examples/vweb/footer.html
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<br/><br/>
|
||||||
|
footer
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -1 +1,8 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang=en>
|
||||||
|
<head>
|
||||||
|
<meta charset=utf-8>
|
||||||
|
<title>vweb example page</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
header <br><br>
|
header <br><br>
|
||||||
|
@ -21,3 +21,4 @@ For loop demo: <br>
|
|||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
End.
|
End.
|
||||||
|
@include 'footer.html'
|
||||||
|
@ -195,6 +195,11 @@ pub fn mark_used(mut table ast.Table, pref_ &pref.Preferences, ast_files []&ast.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if k.ends_with('before_request') {
|
||||||
|
// TODO: add a more specific check for the .before_request() method in vweb apps
|
||||||
|
all_fn_root_names << k
|
||||||
|
continue
|
||||||
|
}
|
||||||
if method_receiver_typename == '&sync.Channel' {
|
if method_receiver_typename == '&sync.Channel' {
|
||||||
all_fn_root_names << k
|
all_fn_root_names << k
|
||||||
continue
|
continue
|
||||||
|
@ -807,7 +807,7 @@ pub fn parse_args_and_show_errors(known_external_commands []string, args []strin
|
|||||||
if command == '' {
|
if command == '' {
|
||||||
command = arg
|
command = arg
|
||||||
command_pos = i
|
command_pos = i
|
||||||
if res.is_eval_argument || command in ['run', 'crun'] {
|
if res.is_eval_argument || command in ['run', 'crun', 'watch'] {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
} else if is_source_file(command) && is_source_file(arg)
|
} else if is_source_file(command) && is_source_file(arg)
|
||||||
|
@ -168,6 +168,7 @@ pub mut:
|
|||||||
header http.Header // response headers
|
header http.Header // response headers
|
||||||
// ? It doesn't seem to be used anywhere
|
// ? It doesn't seem to be used anywhere
|
||||||
form_error string
|
form_error string
|
||||||
|
livereload_poll_interval_ms int = 250
|
||||||
}
|
}
|
||||||
|
|
||||||
struct FileData {
|
struct FileData {
|
||||||
@ -190,8 +191,8 @@ pub fn (ctx Context) init_server() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Defining this method is optional.
|
// Defining this method is optional.
|
||||||
// This method called before every request (aka middleware).
|
// This method is called before every request (aka middleware).
|
||||||
// Probably you can use it for check user session cookie or add header.
|
// You can use it for checking user session cookies or to add headers.
|
||||||
pub fn (ctx Context) before_request() {}
|
pub fn (ctx Context) before_request() {}
|
||||||
|
|
||||||
// TODO - test
|
// TODO - test
|
||||||
@ -202,17 +203,22 @@ pub fn (mut ctx Context) send_response_to_client(mimetype string, res string) bo
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
ctx.done = true
|
ctx.done = true
|
||||||
|
//
|
||||||
// build header
|
|
||||||
header := http.new_header_from_map({
|
|
||||||
http.CommonHeader.content_type: mimetype
|
|
||||||
http.CommonHeader.content_length: res.len.str()
|
|
||||||
}).join(ctx.header)
|
|
||||||
|
|
||||||
mut resp := http.Response{
|
mut resp := http.Response{
|
||||||
header: header.join(vweb.headers_close)
|
|
||||||
body: res
|
body: res
|
||||||
}
|
}
|
||||||
|
$if vweb_livereload ? {
|
||||||
|
if mimetype == 'text/html' {
|
||||||
|
resp.body = res.replace('</html>', '<script src="/vweb_livereload/${vweb_livereload_server_start}/script.js"></script>\n</html>')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// build the header after the potential modification of resp.body from above
|
||||||
|
header := http.new_header_from_map({
|
||||||
|
http.CommonHeader.content_type: mimetype
|
||||||
|
http.CommonHeader.content_length: resp.body.len.str()
|
||||||
|
}).join(ctx.header)
|
||||||
|
resp.header = header.join(vweb.headers_close)
|
||||||
|
//
|
||||||
resp.set_version(.v1_1)
|
resp.set_version(.v1_1)
|
||||||
resp.set_status(http.status_from_int(ctx.status.int()))
|
resp.set_status(http.status_from_int(ctx.status.int()))
|
||||||
send_string(mut ctx.conn, resp.bytestr()) or { return false }
|
send_string(mut ctx.conn, resp.bytestr()) or { return false }
|
||||||
@ -504,6 +510,19 @@ fn handle_conn[T](mut conn net.TcpConn, mut app T, routes map[string]Route) {
|
|||||||
// Calling middleware...
|
// Calling middleware...
|
||||||
app.before_request()
|
app.before_request()
|
||||||
|
|
||||||
|
$if vweb_livereload ? {
|
||||||
|
if url.path.starts_with('/vweb_livereload/') {
|
||||||
|
if url.path.ends_with('current') {
|
||||||
|
app.handle_vweb_livereload_current()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if url.path.ends_with('script.js') {
|
||||||
|
app.handle_vweb_livereload_script()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Static handling
|
// Static handling
|
||||||
if serve_if_static[T](mut app, url) {
|
if serve_if_static[T](mut app, url) {
|
||||||
// successfully served a static file
|
// successfully served a static file
|
||||||
|
48
vlib/vweb/vweb_livereload.v
Normal file
48
vlib/vweb/vweb_livereload.v
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
module vweb
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
// Note: to use live reloading while developing, the suggested workflow is doing:
|
||||||
|
// `v -d vweb_livereload watch --keep run your_vweb_server_project.v`
|
||||||
|
// in one shell, then open the start page of your vweb app in a browser.
|
||||||
|
//
|
||||||
|
// While developing, just open your files and edit them, then just save your
|
||||||
|
// changes. Once you save, the watch command from above, will restart your server,
|
||||||
|
// and your HTML pages will detect that shortly, then they will refresh themselves
|
||||||
|
// automatically.
|
||||||
|
|
||||||
|
// vweb_livereload_server_start records, when the vweb server process started.
|
||||||
|
// That is later used by the /script.js and /current endpoints, which are active,
|
||||||
|
// if you have compiled your vweb project with `-d vweb_livereload`, to detect
|
||||||
|
// whether the web server has been restarted.
|
||||||
|
const vweb_livereload_server_start = time.ticks().str()
|
||||||
|
|
||||||
|
// handle_vweb_livereload_current serves a small text file, containing the
|
||||||
|
// timestamp/ticks corresponding to when the vweb server process was started
|
||||||
|
[if vweb_livereload ?]
|
||||||
|
fn (mut ctx Context) handle_vweb_livereload_current() {
|
||||||
|
ctx.send_response_to_client('text/plain', vweb.vweb_livereload_server_start)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle_vweb_livereload_script serves a small dynamically generated .js file,
|
||||||
|
// that contains code for polling the vweb server, and reloading the page, if it
|
||||||
|
// detects that the vweb server is newer than the vweb server, that served the
|
||||||
|
// .js file originally.
|
||||||
|
[if vweb_livereload ?]
|
||||||
|
fn (mut ctx Context) handle_vweb_livereload_script() {
|
||||||
|
res := '"use strict";
|
||||||
|
function vweb_livereload_checker_fn(started_at) {
|
||||||
|
fetch("/vweb_livereload/" + started_at + "/current", { cache: "no-cache" })
|
||||||
|
.then(response=>response.text())
|
||||||
|
.then(function(current_at) {
|
||||||
|
// console.log(started_at); console.log(current_at);
|
||||||
|
if(started_at !== current_at){
|
||||||
|
// the app was restarted on the server:
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const vweb_livereload_checker = setInterval(vweb_livereload_checker_fn, ${ctx.livereload_poll_interval_ms}, "${vweb.vweb_livereload_server_start}");
|
||||||
|
'
|
||||||
|
ctx.send_response_to_client('text/javascript', res)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user