New Diffs: 3 layer frontend architecture proposal
In order to support the proposed shift to server side rendering of the diffs we need to reconsider our current architecture so that it works best with the diffs rendered on the server. I propose the new 3 layer architecture for diffs frontend which will consist of 3 layers:
- Diff wrapper (Web Component)
- Plugins
- Components
Layers can only communicate with the sibling layer. For example a component can communicate with a plugin but can not with a diff wrapper.
flowchart TD
A[Diff wrapper] -->|Context| B(Plugins)
B -->|Method calls| A
B -->|Props| C(Components)
C -->|Events| B
Diff wrapper
The purpose of the Diff wrapper is to provide:
- Data
- Low-level DOM manipulation abstractions
- Notify on diff element events (mounted, visible, etc.)
- Host plugins
Plugins
Plugins is an adapter-based layer which is necessary for connecting Diff wrapper with Components. Having a plugin layer provides us with better decoupling from the actual interface of the diff wrapper. It also hides the complexity of the integration between components and diffs.
Components
Components are modules that solve user tasks: handle discussions, show code quality findings, expand lines, etc. They can be anything: Vue components, pure JS functions, 3rd party modules. They communicate with diffs through Plugins system using primarily events and props.
Example code
The following code is just for general guidance, the final implementation might look very different to that.
Diff wrapper
import { DiffDiscussionsPlugin } from '../dicsussions/plugin';
const DIFF_PLUGINS = [DiffDiscussionsPlugin];
class DiffWrapper extends HTMLElement {
constructor() {
DIFF_PLUGINS.forEach(plugin => plugin.instantiate(this));
}
connectedCallback() {
this.dispatchEvent(new CustomEvent('mounted'));
}
}
Plugin
import Vue from 'vue';
import { discussionsStore } from './store';
import { DiffDiscussions } from './component';
export class DiffDiscussionPlugin {
instantiate(context) {
context.addEventListener('mounted', () => {
const diffHasDiscussions = state => state.discussions.filter(discussion => discussion.file_hash === context.file_hash);
discussionsStore.watch(diffHasDiscussions, (diffDiscussions) => {
diffDiscussions.forEach(diffDiscussion => {
const el = context.extendRow(diffDiscussion.row);
const remove = () => context.reduceRow(diffDiscussion.row);
new Vue({
el,
render(h) {
return h(DiffDiscussions, { props: { diffDiscussion }, on: { remove } });
}
});
});
}, { immediate: true });
});
}
}