3 Years of VueJS (2021)

The code for all this is available here.

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_many_polygon_circles()
  2. draw_circle_from_instance()
  3. draw_circle()
  4. 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. Support for Vue was largely missing when we started. While support is getting better, generally it ends up being thing wrappers.
  2. We have also found at least so far that often performance issues are rarely to do with these types of drawing functions themselves.
  1. Easy to modify. For example, we were able to “drop in” the user modifiable vertex size (and similar features) relatively easily.

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.

  • 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.
  • 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`.
  • 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!