Cómo Chrome DevTools pasó de la bicicleta a la versión estándar





Una nota rápida sobre cómo las DevTools de Chrome migraron del cargador de módulos interno a los módulos estándar de JavaScript. Le diremos cómo y por qué se retrasó la migración, sobre los costos ocultos de la migración y sobre las conclusiones del equipo de DevTools después de que se completó la migración. Pero comencemos con la historia de las herramientas de desarrollo web.



Introducción



Como probablemente sepa, Chrome DevTools es una aplicación web HTML, CSS y JavaScript. A lo largo de los años, DevTools se ha vuelto rico en funciones, inteligente y conocedor de la plataforma web moderna. Aunque DevTools se ha expandido, su arquitectura recuerda en gran medida a la original cuando la herramienta formaba parte de WebKit .



Contaremos la historia de DevTools, describiremos los beneficios y las limitaciones de las soluciones y lo que hemos hecho para mitigar esas limitaciones. Así que profundicemos en los sistemas modulares, cómo cargar código y cómo finalmente usamos los módulos de JavaScript.



En el principio no había nada



La interfaz ahora tiene muchos sistemas modulares y sus herramientas, así como un formato de módulo JavaScript estandarizado . Nada de esto fue cuando comenzó DevTools. La herramienta está construida sobre el código WebKit escrito hace más de 12 años.



La primera mención del sistema modular en DevTools se remonta a 2012: fue la introducción de una lista de módulos con la correspondiente lista de fuentes . Parte de la infraestructura de Python utilizada en ese momento para compilar y construir DevTools. En 2013, frontend_modules.json esta confirmación extrajo los módulos en un archivo y luego, en 2014, en módulos separados module.json( aquí ). Ejemplo module.json:



{
  "dependencies": [
    "common"
  ],
  "scripts": [
    "StylePane.js",
    "ElementsPanel.js"
  ]
}


Desde 2014, se ha module.jsonutilizado en herramientas de desarrollo para señalar módulos y archivos fuente. Mientras tanto, el ecosistema web creció rápidamente y se crearon muchos formatos de módulos: UMD, CommonJS y, finalmente, módulos JavaScript estandarizados. Sin embargo, DevTools está bloqueado module.json. El sistema modular único y no estandarizado tenía varias desventajas:



  1. module.json requería sus propias herramientas de construcción.
  2. No hubo integración IDE. Por supuesto, necesitaba herramientas especiales para crear archivos que entendía: ( jsconfig.jsonpara VS Code ).
  3. Las funciones, clases y objetos se han colocado en el ámbito global para permitir el intercambio entre módulos.
  4. El orden en el que se enumeran los archivos es importante. No había ninguna garantía de que el código en el que confía se haya cargado más que la verificación humana.


En general, al evaluar el estado actual del sistema modular DevTools y otros formatos de módulo más utilizados, llegamos a la conclusión de que module.jsoncreaba más problemas de los que resolvía.



Ventajas del estándar



Hemos elegido módulos de JavaScript. Cuando se tomó esta decisión, los módulos en el idioma todavía estaban marcados en Node.js y una gran cantidad de paquetes NPM no los admitían. Independientemente, llegamos a la conclusión de que los módulos de JavaScript eran la mejor opción.



La principal ventaja de los módulos es que son un formato estandarizado en el idioma . Cuando enumeramos los contrasmodule.json, nos dimos cuenta de que casi todos estaban relacionados con el uso de un formato de módulo único y no estandarizado. Elegir un formato de módulo no estandarizado significa que tenemos que invertir tiempo en la construcción de integraciones utilizando herramientas de construcción y las herramientas de nuestros colegas. Estas integraciones a menudo eran frágiles y carecían de soporte para las funciones, lo que requería tiempo de mantenimiento adicional y, a veces, errores complicados. Los errores terminaron afectando a los usuarios.



Dado que los módulos de JavaScript eran estándar, esto significaba que los IDE como VS Code, las herramientas de verificación de tipos como el compilador Closure / TypeScript y las herramientas de compilación como Rollup y minifiers podrían comprender el código fuente escrito. Además, cuando una nueva persona se une al equipo de DevTools, no tienen que perder el tiempo aprendiendo el propietario module.json.



Por supuesto, cuando DevTools comenzó, no existía ninguno de los beneficios anteriores. Se necesitaron años de trabajo en grupos de estándares para implementar el tiempo de ejecución. Tomó tiempo recibir comentarios de los desarrolladores, los usuarios de los módulos. Pero cuando aparecieron los módulos en el idioma, tuvimos una opción: seguir apoyando nuestro propio formato o invertir en la transición al nuevo formato.



¿Cuánto cuesta el brillo de la novedad?



Aunque los módulos JavaScript tenían muchos beneficios que queríamos utilizar, nos quedamos en el mundo module.json. Aprovechar los módulos del lenguaje significó que tuvimos que invertir un esfuerzo considerable en deuda técnica. Mientras tanto, la migración podría interrumpir funciones e introducir errores de regresión.



No se trataba de si deberíamos usar módulos JavaScript. La pregunta era qué tan cara es la capacidad de usar módulos JavaScript . Tuvimos que equilibrar el riesgo de molestar a los usuarios con las regresiones, el tiempo que tardaron los ingenieros en migrar y un período de deterioro en el estado del sistema en el que estaríamos operando.



El último punto resultó ser muy importante. Aunque teóricamente podríamos llegar a los módulos de JavaScript, durante la migración terminaríamos con un código que tomaría en cuenta ambos tipos de módulos . Esto no solo es un desafío técnico, sino que también significa que todos los ingenieros que trabajan en DevTools deben saber cómo trabajar en un entorno de este tipo. Tendrían que preguntarse constantemente: "¿Qué está pasando en este código? ¿Es module.jsonJS y cómo puedo hacer el cambio?"

El costo latente de la migración en términos de capacitación de colegas fue mayor de lo que esperábamos.
Después de analizar los costos, llegamos a la conclusión de que aún vale la pena cambiar a módulos en el idioma. Por tanto, nuestros principales objetivos fueron:



  1. Asegúrese de que los módulos estándar sean lo más útiles posible.
  2. Asegúrese de que la integración con los módulos existentes en la base sea module.jsonsegura y no genere un impacto negativo en el usuario (errores de regresión, frustración del usuario).
  3. Proporcione guías de migración de DevTools. Principalmente a través de controles y equilibrios integrados en el proceso para evitar errores accidentales.


Hoja de cálculo, conversiones y deuda técnica



El objetivo estaba claro. Pero las limitaciones eran module.jsondifíciles de sortear. Fueron necesarias varias iteraciones, prototipos y cambios arquitectónicos antes de que se nos ocurriera una solución utilizable. Terminamos escribiendo un documento de proyecto con una estrategia de migración. Este documento dio una estimación inicial del tiempo: 2-4 semanas.

La parte más intensa de la migración tomó 4 meses, ¡y pasaron 7 meses de principio a fin!
Sin embargo, el plan original ha resistido la prueba del tiempo: queríamos enseñarle al tiempo de ejecución de DevTools a cargar todos los archivos de la forma anterior para usar los que se enumeran en la matriz scripts module.json, mientras que todos los archivos enumerados en la matriz modulestenían que cargarse mediante importación de lenguaje dinámico . Cualquier archivo que esté en la matriz modulespuede funcionar con importy exportdesde ES6.



Además, queríamos migrar en 2 fases. Finalmente, dividimos la última fase en 2 subfases: exportación e importación. Los módulos y las fases se rastrearon en una hoja de cálculo grande:





Un fragmento de la tabla de migración aquí.



Fase de exportación



El primer paso fue agregar declaraciones de exportación para todas las entidades que deben compartirse entre módulos / archivos. La transformación se automatizó ejecutando un script para cada carpeta . Digamos que module.jsonexiste tal entidad:



Module.File1.exported = function() {
  console.log('exported');
  Module.File1.localFunctionInFile();
};
Module.File1.localFunctionInFile = function() {
  console.log('Local');
};


Aquí Moduleestá el nombre del módulo. File1- nombre del archivo. En el árbol de código, que tiene este aspecto: front_end/module/file1.JS.



El código anterior se traduce en esto:



export function exported() {
  console.log('exported');
  Module.File1.localFunctionInFile();
}
export function localFunctionInFile() {
  console.log('Local');
}

/** Legacy export object */
Module.File1 = {
  exported,
  localFunctionInFile,
};


Originalmente planeamos reescribir la importación en un archivo en esta etapa. Por ejemplo, en el ejemplo anterior, volveríamos a escribir Module.File1.localFunctionInFileen localFunctionInFile. Sin embargo, nos dimos cuenta de que sería más fácil automatizar y más seguro separar las dos transformaciones. Por lo tanto, "transferir todas las entidades a un archivo" se convertirá en la segunda subfase de importación.



Dado que agregar una palabra clave exportconvierte el archivo de un "script" en un "módulo", gran parte de la infraestructura de DevTools tuvo que actualizarse en consecuencia. El marco incluía un tiempo de ejecución de importación dinámico, así como herramientas como ESLint para ejecutarse en modo módulo.



Una molestia fue que nuestras pruebas se ejecutaron en un modo "no estricto". Los módulos de JavaScript implican que los archivos se ejecutan en modo estricto. Esto afectó las pruebas. Al final resultó que, un número no trivial de pruebas se basó en un modo no estricto, incluida una prueba en la que el operador estaba presente with.



Al final, actualizar la primera carpeta (agregar exportación) tomó aproximadamente una semana y varios intentos de recarga .



Fase de importación



Después de que todas las entidades se exportaron usando las declaraciones de exportación, mientras permanecían en el alcance global debido al legado, tuvimos que actualizar todas las referencias de entidades, si están en varios archivos, para usar importaciones de ES. El objetivo final es eliminar todas las exportaciones caducadas eliminando el alcance global. La transformación se automatizó ejecutando un script para cada carpeta .



Por ejemplo, las siguientes entidades module.json:



Module.File1.exported();
AnotherModule.AnotherFile.alsoExported();
SameModule.AnotherFile.moduleScoped();


Convertido a:



import * as Module from '../module/Module.js';
import * as AnotherModule from '../another_module/AnotherModule.js';

import {moduleScoped} from './AnotherFile.js';

Module.File1.exported();
AnotherModule.AnotherFile.alsoExported();
moduleScoped();


Sin embargo, este enfoque tiene salvedades:



  1. No todas las entidades fueron nombradas por principio Module.File.symbolName. Algunas entidades han sido nombradas Modele.Fileo incluso Module.CompletelyDifferentName. La discrepancia significó que tuvimos que crear un mapeo interno del antiguo objeto global al nuevo objeto importado.
  2. moduleScoped. , Events, , Events. , , , import Events .
  3. , . , . , , . , , ( DevTools). , , .


JavaScript



En febrero de 2020, 6 meses después del inicio en septiembre de 2019, se realizaron las últimas limpiezas en la carpeta ui /. Entonces la migración terminó de manera extraoficial. Cuando se asentó el polvo, marcamos oficialmente la migración completa el 5 de marzo de 2020 .



Ahora DevTools solo funciona con módulos JavaScript. Todavía estamos poniendo algunas entidades en el alcance global (en archivos heredados module.js) para pruebas heredadas o integraciones con otras partes de las herramientas del arquitecto. Se eliminarán con el tiempo, pero no consideramos que bloqueen el desarrollo. También tenemos una guía de estilo para módulos JavaScript .



Estadísticas



Las estimaciones conservadoras del número de CL (lista de cambios, un término utilizado en Gerrit, similar a la solicitud de extracción de GitHub) involucrados en esta migración son alrededor de 250 CL, en su mayoría realizados por 2 ingenieros . No tenemos estadísticas finales sobre el tamaño de los cambios realizados, pero una estimación conservadora de las filas cambiadas (la suma de la diferencia absoluta entre inserciones y eliminaciones para cada CL) es aproximadamente 30,000 filas, que es aproximadamente el 20% de todo el código de front-end de DevTools .



El primer archivo que se exportará es compatible con Chrome 79, que se lanzó en la versión estable en diciembre de 2019. El último cambio para cambiar a la importación viene en Chrome 83, que se lanzó en la versión estable en mayo de 2020.



Somos conscientes de una regresión debida a la migración en Chrome estable. El autocompletado de fragmentos de código en la barra de comandos se rompió debido a una exportación predeterminada extraña . Ha habido varias otras regresiones, pero nuestros casos de prueba automatizados y los usuarios de Chrome Canary las han informado. Arreglamos errores antes de que pudieran convertirse en versiones estables de Chrome.



Puedes ver la historia completa registrándote aquí . No todos, pero la mayoría de CL están vinculados a este error.



¿Qué hemos aprendido?



  1. . , JavaScript ( ) , DevTools . , , , .
  2. — , . , , . , , , .
  3. (, ) . -. , Python Rollup.
  4. (~20% ), . , , . , .
  5. , . . , , , . , , — . , , .


imagen


, Level Up , - SkillFactory:





E







All Articles