This commit is contained in:
Michael Zhang 2020-02-17 19:04:27 -06:00
parent 1d2df8e326
commit 45da288831
Signed by: michael
GPG key ID: BDA47A31A3C8EE6B
8 changed files with 121 additions and 11 deletions

View file

@ -4,14 +4,13 @@ date = 2020-02-11
template = "post.html" template = "post.html"
[taxonomies] [taxonomies]
tags = [] tags = ["enterprise", "web", "ui", "rust", "design"]
[extra]
toc = true
+++ +++
This past weekend, while on my trip to Minneapolis, I completed a very early prototype of "enterprise", a new UI framework I've been kind of envisioning over the past couple of weeks. While the UI framework is mainly targeted at web apps, the hope is that with a bit more effort, native UIs can be produced with almost no changes to existing applications. Before I begin to describe how it works, I'd like to acknowledge [Nathan Ringo][1] for his massively helpful feedback in both the brainstorming and the implementation process. This past weekend, while on my trip to Minneapolis, I completed a very early prototype of "enterprise", a new UI framework I've been kind of envisioning over the past couple of weeks. While the UI framework is mainly targeted at web apps, the hope is that with a bit more effort, native UIs can be produced with almost no changes to existing applications. Before I begin to describe how it works, I'd like to acknowledge [Nathan Ringo][1] for his massively helpful feedback in both the brainstorming and the implementation process.
<!-- more -->
## Goals of the project ## Goals of the project
This project was born out of many frustrations with existing web frameworks. This is kind of the combination of several projects I wanted to tackle; since it's such a long-term thing I'm going to document a bit of what I want to achieve so I can stay on track. The high-level goals of the project are: This project was born out of many frustrations with existing web frameworks. This is kind of the combination of several projects I wanted to tackle; since it's such a long-term thing I'm going to document a bit of what I want to achieve so I can stay on track. The high-level goals of the project are:

View file

@ -0,0 +1,96 @@
+++
title = "enterprise: syntax update"
date = 2020-02-17
template = "post.html"
[taxonomies]
tags = ["enterprise", "web", "ui", "syntax"]
+++
[Enterprise][1]'s frontend DSL just got a syntax! Although the major functionality hasn't really changed, I threw out the ugly verbose AST-construction syntax for a hand-rolled recursive-descent-ish parser. The rehashed "Hello, world" example looks a bit like this:
```
component HelloWorld {
model {
name: String = "hello",
}
view {
<input bind:value="name" />
"Hello, " {name} "!"
}
}
```
This compiles using `cargo-web` into a working version of the last post's prototype. You'll notice that quoted literals are used to represent text rather than just typing it out directly like in XML. This is because I'm actually borrowing Rust syntax and parsing it a bit differently. If I had bare text, then everything you put would have to follow Rust's lexical rules; additionally, data about spacing would be a lot more complicated (and unstable!) to retrieve.
I could possibly have thrown the whole thing into a parser-generator, using Rust's `proc-macro::TokenTree` as tokens, but TokenTree actually gives you blocks (eg. `()` `{}` `[]`) for free, so I can parse expressions like `{name}` incredibly easily.
Syntax isn't the only thing that's changed since the last update: I've also revamped how builds work.
New Build Method
----------------
I'm switching to a build method that's rather unconventional. The original approach looked something like this.
```dot
digraph "dependency graph" {
graph[bgcolor="transparent", class="default-coloring"];
rankdir="LR";
"Component DSL" -> "AST" [label = "Parsing"];
"AST" -> "Dependency Graph" [label = "Graph traversal"];
}
```
Problem here is, when we want code to be modular, the graph traversal approach is going to need information about _all_ modules that are imported in order to
be able to produce a flat set of instructions in the final result. If I make a library for a component (say, `enterprise-router`), what should its crate's contents be?
> **Tangent**: Here's where I'm going to distract myself a little and put this into a more big-picture perspective. Ultimately, the ideal manifestation of an architecture/business-logic separation would be a DSL that completely hides all implementation of its internals.
>
> That's a pretty far-out goal, so I'm building enterprise incrementally. Sadly, large parts of the language will still rely on the language in which this framework is implemented, Rust. This means that the underlying implementation of features such as modules and async will be relying on the Rust language having these features. However, note that in the long term, a separate DSL for business logic will be planned.
So what's the solution here? Instead of visiting your component node by node when your component is defined, all the framework is going to do is parse your definition and store the AST of your component as-is. I chose here to serialize ASTs as JSON data and dump it into a static string that will be bundled into your crate.
Then, in your `build.rs` file, you'll call something like `enterprise_compiler::build(App)`, where `App` is the name of the static string containing the JSON data of the description of your app. This will actually perform the analysis process, calculating the graph of update dependencies, as well as generating the code that will go into a Rust module that you can include into your code.
Your `build.rs` file might look something like this:
```rs
#[macro_use]
extern crate enterprise_macros;
component! {
component HelloWorld {
model {
name: String = "hello",
}
view {
<input bind:value="name" />
"Hello, " {name} "!"
}
}
}
fn main() {
enterprise_compiler::process("helloworld", HelloWorld);
}
```
This will create a string called `HelloWorld` for the HelloWorld component, and then analyze and generate the final application code for it into a file called `helloworld.rs` that you can `mod` into your application. The advantage to this approach is that external modules can just rely on Rust's crate system, since we're just fetching strings out of other crates.
Source code: [here][3].
Next Steps
----------
As mentioned in my previous post, I'm still working on implementing [TodoMVC][2], a simple Todo application that should flesh out some more of the reactive functionalities of the framework. This should solidify some more of the questions regarding interactions between data model and DOM.
I'll also try to abstract more of the system away so it's less dependent on stdweb's implementation. This means adding a notion of "backend", where different backends may have different implementations of a particular component.
[1]: @/enterprise/2020-02-11-prototype/index.md
[2]: http://todomvc.com/
[3]: https://git.iptq.io/michael/enterprise/src/commit/1453885ed2c3a5159431bb41398b9b8bea4d49f5

View file

@ -47,6 +47,12 @@ a {
} }
} }
blockquote {
color: $small-text-color;
border-left: 4px solid $small-text-color;
padding-left: 12px;
}
.postlisting-row td { .postlisting-row td {
padding-bottom: 12px; padding-bottom: 12px;
} }

View file

@ -1,4 +1,6 @@
.graphviz { .graphviz {
margin: 24px auto;
svg { svg {
max-width: 100%; max-width: 100%;
width: 100%; width: 100%;

View file

@ -8,7 +8,7 @@ $monofont: "Roboto Mono", "Roboto Mono for Powerline", "Inconsolata", "Consolas"
@media (prefers-color-scheme: light) { @media (prefers-color-scheme: light) {
$background-color: white; $background-color: white;
$text-color: #15202B; $text-color: #15202B;
$small-text-color: lighten($text-color, 10%); $small-text-color: lighten($text-color, 15%);
$link-color: royalblue; $link-color: royalblue;
@import "content"; @import "content";
@import "graph"; @import "graph";
@ -16,8 +16,8 @@ $monofont: "Roboto Mono", "Roboto Mono for Powerline", "Inconsolata", "Consolas"
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
$background-color: #15202B; $background-color: #15202B;
$text-color: #EADFD4; $text-color: #D4D4D4;
$small-text-color: darken($text-color, 10%); $small-text-color: darken($text-color, 15%);
$link-color: lightskyblue; $link-color: lightskyblue;
@import "content"; @import "content";
@import "graph"; @import "graph";

View file

@ -1,18 +1,25 @@
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"> <rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
<channel> <channel>
<title>{{ config.title }}</title> <title>{{ config.title }}</title>
<link>{{ config.base_url }}</link> <link>{{ config.base_url | safe }}</link>
<description>{{ config.description }}</description> <description>{{ config.description }}</description>
<generator>zola</generator> <generator>zola</generator>
<language>{{ config.default_language }}</language> <language>{{ config.default_language }}</language>
<atom:link href="{{ feed_url }}" rel="self" type="application/rss+xml"/> <atom:link href="{{ feed_url | safe }}" rel="self" type="application/rss+xml"/>
<lastBuildDate>{{ last_build_date | date(format="%a, %d %b %Y %H:%M:%S %z") }}</lastBuildDate> <lastBuildDate>{{ last_build_date | date(format="%a, %d %b %Y %H:%M:%S %z") }}</lastBuildDate>
{% for page in pages %} {% for page in pages %}
<item> <item>
<title>{{ page.title }}</title> <title>{{ page.title }}</title>
<pubDate>{{ page.date | date(format="%a, %d %b %Y %H:%M:%S %z") }}</pubDate> <pubDate>{{ page.date | date(format="%a, %d %b %Y %H:%M:%S %z") }}</pubDate>
<link>{{ page.permalink }}</link> <link>{{ page.permalink | safe }}</link>
<description>{% if page.summary %}{{ page.summary }}{% else %}{{ page.content }}{% endif %}</description> <description>
{% if page.summary %}
{{ page.summary | safe }}
<a href="{{ page.permalink | safe }}#continue-reading">Continue reading...</a>
{% else %}
Read <a href="{{ page.permalink | safe }}">{{ page.title }}</a>.
{% endif %}
</description>
</item> </item>
{% endfor %} {% endfor %}
</channel> </channel>