Danil Somsikov | d884ae7 | 2025-02-12 07:56:13 | [diff] [blame^] | 1 | # Chromium DevTools UI Engineering |
| 2 | |
| 3 | ## Objective and scope |
| 4 | |
| 5 | This document defines how to build Chromium DevTools UI. It aims at improving consistency, maintainability and extensibility of the current code |
| 6 | |
| 7 | **Consistency** here means to have one way to do one thing, in the context of this doc to have a single reusable component per repeated task. |
| 8 | |
| 9 | **Maintainability** is addressed here through the [separation of concerns](https://en.wikipedia.org/wiki/Separation_of_concerns) while avoiding unnecessary indirection. |
| 10 | |
| 11 | **Extensibility** requires the ease of understanding and imitation, i.e. being able to take an existing code, understand and use it as an example to solve another problem. |
| 12 | |
| 13 | Additionally, all the changes need to be applicable to the existing code **point-wise** instead of requiring extensive rewriting. |
| 14 | |
| 15 | ## Reusable web components |
| 16 | |
| 17 | Common UI primitives need to be implemented as web components. Examples include buttons, checkboxs, or data grids. Web components should not be used solely for encapsulation, we should not have single-use web components. |
| 18 | |
| 19 | Not all reusable code should be a web component: in the Application panel we have several similar views showing key-value pairs in a datagrid with a preview sidebar. Making this a truly reusable component will lead to an unjustifiable complexity. Instead we should have extracted a base class or helpers implementing the common functionality. |
| 20 | |
| 21 | In implementation we should prefer wrapping existing code under ui/legacy over new implementation. The former is the most feature-rich (including less obvious aspects like a11y) and has stood the test of time. |
| 22 | |
| 23 | We should however strive to expose a “HTML-native” API: e.g. toolbar doesn’t need an `items` setter if its child HTML elements could define its content. Even the data grid doesn’t need to expose data grid nodes, when `<tr>` and `<td>`’s are enough. |
| 24 | |
| 25 | ## Model-View-Presenter architecture |
| 26 | |
| 27 | We should strictly separate business logic, from UI logic and presentation. This means that most of the UI code should be centered around a presenter (subclass of a `UI.Widget`) that gets a view function injected. All the logic that is not related to the DevTools UI (i.e. that would stay the same if we rewrite DevTools as a command-line tool), should belong to the model layer. |
| 28 | |
| 29 | The presenter code should make no assumptions about the details of model or view code. It should not care what race conditions CDP exposes or how many layers of `<div class=”wrapper”>` does the markup have. However, for simplicity, the injected view function should have a default implementation inlined with a presenter (see an example below). |
| 30 | |
| 31 | In tests, we use a simple stub as a view function, which allows us to test the presenter logic without any DOM manipulation. To test the view function itself we should use screenshot and e2e tests. |
| 32 | |
| 33 | ## Declarative and orchestrated DOM updates |
| 34 | |
| 35 | We should no longer use imperative API to update DOM. Instead we rely on orchestrated rendering of lit-html templates. The view function described above should be a call to lit-html `render`. The view function should be called from `UI.Widget`’s `performUpdate` method, which by default is scheduled using `requestAnimationFrame`. |
| 36 | |
| 37 | To embed another presenter (`UI.Widget`) in the lit-html template, use `<devtools-widget .widgetConfig=${widgetConfig(<class>, {foo: 1, bar: 2})}` |
| 38 | |
| 39 | This will instantiate a `Widget` class with the web component as its `element` and, optionally, will set the properties provided in the second parameter. The widget won’t be re-instantiated on the subsequent template renders, but the properties would be updated. For this to work, the widget needs to accept `HTMLElement` as a sole constructor parameter and properties need to be public members or setters. |
| 40 | |
| 41 | ## Examples |
| 42 | |
| 43 | ```html |
| 44 | <devtools-widget .config=${widgetConfig(ElementsPanel)}> |
| 45 | <devtools-split-widget> |
| 46 | <devtools-widget slot="main".config=${widgetConfig(ElementsTree)} /> |
| 47 | <devtools-tab-pane slot="sidebar" /> |
| 48 | <devtools-widget .config=${widgetConfig(StylesPane, {element: input.element})} /> |
| 49 | <devtools-widget .config=${widgetConfig(ComputedPane, {element: input.element})} /> |
| 50 | ... |
| 51 | </devtools-tab-pane> |
| 52 | </devtools-split-widget> |
| 53 | </devtools-widget> |
| 54 | ``` |
| 55 | |
| 56 | ```ts |
| 57 | class StylesPane extends UI.Widget { |
| 58 | constructor(element, view = (input, output, target) => { |
| 59 | render(html` |
| 60 | <devtools-widget .config=${widgetConfig(MetricsPane, {element: input.element})} /> |
| 61 | <devtools-toolbar> |
| 62 | <devtools-filter-input @change=${input.onFilter}/> |
| 63 | <devtools-checkbox @change=${input.onShowAll}>Show All<devtools-checkbox> |
| 64 | <devtools-checkbox @change=${input.onGroup}>Group<devtools-checkbox> |
| 65 | </devtools-toolbar> |
| 66 | <devtools-tree-outline> |
| 67 | ${input.properties.map(p => html`<li> |
| 68 | <dt>${p.key}</dt><dd>${renderValue(p.value)}</dd> |
| 69 | <ol>${p.subproperties.map(...)} |
| 70 | </li>`)} |
| 71 | </devtools-tree-outline>` |
| 72 | } |
| 73 | } |
| 74 | ``` |
| 75 | |
| 76 | [https://source.chromium.org/chromium/chromium/src/+/main:third\_party/devtools-frontend/src/front\_end/panels/protocol\_monitor/ProtocolMonitor.ts;l=197](https://source.chromium.org/chromium/chromium/src/+/main:third_party/devtools-frontend/src/front_end/panels/protocol_monitor/ProtocolMonitor.ts;l=197) |
| 77 | |
| 78 | [https://source.chromium.org/chromium/chromium/src/+/main:third\_party/devtools-frontend/src/front\_end/panels/developer\_resources/DeveloperResourcesListView.ts;l=86](https://source.chromium.org/chromium/chromium/src/+/main:third_party/devtools-frontend/src/front_end/panels/developer_resources/DeveloperResourcesListView.ts;l=86) |
| 79 | |
| 80 | [https://source.chromium.org/chromium/chromium/src/+/main:third\_party/devtools-frontend/src/front\_end/panels/timeline/TimelineSelectorStatsView.ts;l=113](https://source.chromium.org/chromium/chromium/src/+/main:third_party/devtools-frontend/src/front_end/panels/timeline/TimelineSelectorStatsView.ts;l=113) |