3 Years of VueJS (2021)

Have you ever wondered what a complex vue js application really looks like?

Today I’m happy to take a deep dive into the architecture of Diffgram’s Open Annotation UI. This is intended for people who are curious about VueJS at scale.

Annotation Focused Modules

While there are hundreds of components in the system, one of the most interesting is the Annotation focused ones.

Example — Drawing a Polygon

Overview of the Instance List Component

Now I unpack what the instance list component is doing. Here the black are functions shared between all instance types. The purple is polygon specific.

Code
for (var i in this.instance_list) {   this.draw_single_instance(ctx, i)}

Conditioning on type

Code

else if ([“polygon”, “line”].includes(instance.type)) { ctx.beginPath() this.draw_polygon(instance, ctx, i)}
  1. draw_polygon_control_points()
  2. draw_many_polygon_circles()
  3. draw_circle_from_instance()
  4. draw_circle()
  5. is_mouse_in_path()

is_mouse_in_path?

One of the common questions other components ask is “Which instance is the user on?” Often, it’s easiest to determine this while drawing the path.

User control

How does the canvas know how large to make the control points? What if the user doesn’t want any control points? Zooming out back up to Annotation Core, we see there is a little<v-slider> component.

draw_circle: function (x, y, ctx) { ctx.arc(x, y, this.$props.vertex_size, 0, 2 * Math.PI); ctx.moveTo(x, y) // reset},

Why such deep control? Why not use a higher level drawing library?

Why:

  1. Our needs often don’t appear to line up with those of the library. For example when it comes to rendering video at the breadth and depth we want to, it’s simply not something these higher level libraries tend to support.
  2. Support for Vue was largely missing when we started. While support is getting better, generally it ends up being thing wrappers.
  3. We have also found at least so far that often performance issues are rarely to do with these types of drawing functions themselves.
  1. It allows us to maintain a consistent definition. If we call something `x_min` — we can use that up and to the point we have to interact with `canvas`. Given we are in a scientific realm, it feels a lot more consistent to be very precise about this.
  2. Easy to modify. For example, we were able to “drop in” the user modifiable vertex size (and similar features) relatively easily.
  1. Sometimes it’s a bit of a distraction — reasonable user expectations like moving an instance, resizing, etc. can sometimes become a surprising amount of work to get right.

Problems

Why is this so hard? What’s the big deal?

To try and give an idea of scope, if I imagine “flattening” all of the annotation imports (non library code), there are probably over 45,000 unique lines.

Annotation core got large

Annotation Core become a bottleneck. Despite importing 35 components, the file has grown to an insane 7569 lines of code. While that’s “only” 15% of the overall sub system, it’s still waaaaaay too large to reasonably reason about. There’s many reasons for this. I’ll unpack a few and speak to our new direction for it.

Code in JS — Presentation Layer in Vue

First — to be clear we are entirely “on team Vue”. We are fans. It’s easier to use then react and just as scalable.

Each Instance Type has its Own Class

As we add more instance types, and as these interactions become more complex, we have realized that it is needed to have dedicated classes for each type.

User Interaction Paradigm

As interactions become more complex, the built-in primitives of “mouse down” “mouse up” etc start to lose their meaning.

Drawable Canvas — Divide Canvas and rest of Annotation UI

Another divide and conquer opportunity — separating the concern of canvas drawing, from annotation UI aspects. We made great progress on this and have an example here (being used in new Templating setup).

Make Error Propagation Easy — Regular Error

Let’s say that you wanted to make a new endpoint. How do you propagate errors back to the user?

.catch(error => { this.error = this.$route_api_errors(error) })
<v_error_multiple :error=”error”> </v_error_multiple>

Wrap Library Components

A little while back, a UI component library made a big modification. It caused a bunch of breaking changes that were a huge pain to fix.

Button with Menu example

Code

Why regular components

Both the Regular Error and Button with Menu are examples of reusable, and fairly abstract, components. While abstract and reusable components is standard practice in many paradigms, in the context of 3rd party UI libraries it can be surprisingly hard to do. I think the effort is worth it because the pros outweigh the cons. Overall it allows us to move faster and ship better quality code.

  • Gives us protection that if the library changes again(or we wish to move to another library, implementation etc) we can do so in one place.
  • Consistency. Encourages new developers to put icons and a descriptive message.
  • Is simple to read and less error prone.
  • Allows us to modify the library, eg adding the <a> tag so that default actions like right click work normally.
  • Allows us to set defaults, enforce typescript validation if needed, and generally control what library features are “enabled”.
  • Gives us a clear expansion point in the future — eg for multiple language support.
  • It is less flexible. There are some cases that it simply doesn’t cover.
  • There are occasionally awkward syntax changes that if “forces”. For example `data-cy=”data_cy”` for testing (`-` is changed to `_`). Most of these likely have a more elegant solution but the trade off of time to determine it is present.
  • Certain things become more difficult to pass around, for example `style`.
  • In theory it introduces a performance overhead. In reality most of this is so many calls deep that it’s not really relevant.
  • It creates more “risk” in that edits to it potentially have more far reaching effects. However — this is somewhat offset in that it forces us into a more rigorous testing mindset and makes bugs more apparent. It’s almost like a micro library dependency in a sense.

Vuex — a comment

We do use Vuex for some things.

What I have found success using Vuex:

Things that change infrequently, but are needed everywhere.

WET & DRY

Zooming out for a moment. Two major paradigms in programming are WET (Write everything twice) and DRY (Don’t repeat yourself).

Closing thoughts

Thank you very much for reading!

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store