F. render.js

1/* Ejemplo de render en el cliente. No se usa import
2 * porque Firefox no lo soporta en los web workers. */
3
4invocaServicio("srv/lista.php")
5 .then(respuesta => {
6
7 const lista = respuesta.body
8
9 if (Array.isArray(lista)) {
10 // Genera el código HTML de la lista.
11 let render = ""
12 for (const modelo of lista) {
13 /* Codifica nombre y color para que cambie los caracteres especiales y
14 * el texto no se pueda interpretar como HTML. Esta técnica evita la
15 * inyección de código. */
16 const nombre =
17 typeof modelo.nombre === "string" ? htmlentities(modelo.nombre) : ""
18 const color =
19 typeof modelo.color === "string" ? htmlentities(modelo.color) : ""
20 render += /* html */
21 `<dt>${nombre}</dt>
22 <dd>${color}</dd>`
23 }
24
25 // Verifica si el código corre dentro de un web worker.
26 if (self instanceof WorkerGlobalScope) {
27 // Envía el render a la página que invocó este web worker.
28 self.postMessage({ resultado: render })
29 }
30 }
31 })
32 .catch(muestraErrorEnWorker)
33
34const ProblemDetails_InternalServerError = 500
35
36class ProblemDetails extends Error {
37
38 /**
39 * @param {number} status
40 * @param {string} title
41 * @param {string} [detail]
42 * @param {string} [type]
43 */
44 constructor(status, title, detail, type) {
45 super(title)
46 /** @readonly */
47 this.status = status
48 /** @readonly */
49 this.type = type
50 /** @readonly */
51 this.title = title
52 /** @readonly */
53 this.detail = detail
54 }
55
56}
57
58/**
59 * Muestra un error en la consola y en un cuadro de
60 * alerta el mensaje de una excepción.
61 * @param { ProblemDetails | Error | null } error descripción del error.
62 */
63function muestraErrorEnWorker(error) {
64 if (error === null) {
65 console.log("Error")
66 self.postMessage({ error: "Error" })
67 } else if (error instanceof ProblemDetails) {
68 let mensaje = error.title
69 if (error.detail) {
70 mensaje += `\n\n${error.detail}`
71 }
72 mensaje += `\n\nCódigo: ${error.status}`
73 if (error.type) {
74 mensaje += ` ${error.type}`
75 }
76 console.error(mensaje)
77 console.error(error)
78 self.postMessage({ error: mensaje })
79 } else {
80 console.error(error)
81 self.postMessage({ error: error.message })
82 }
83}
84
85const JsonResponse_OK = 200
86const JsonResponse_Created = 201
87const JsonResponse_NoContent = 204
88
89class JsonResponse {
90
91 /**
92 * @param {number} status
93 * @param {any} [body]
94 * @param {string} [location]
95 */
96 constructor(status, body, location) {
97 /** @readonly */
98 this.status = status
99 /** @readonly */
100 this.body = body
101 /** @readonly */
102 this.location = location
103 }
104
105}
106
107/**
108 * Espera a que la promesa de un fetch termine. Si
109 * hay error, lanza una excepción. Si no hay error,
110 * interpreta la respuesta del servidor como JSON y
111 * la convierte en una literal de objeto.
112 * @param { string | Promise<Response> } servicio
113 */
114export async function invocaServicio(servicio) {
115 let f = servicio
116 if (typeof servicio === "string") {
117 f = fetch(servicio, {
118 headers: { "Accept": "application/json, application/problem+json" }
119 })
120 } else if (!(f instanceof Promise)) {
121 throw new Error("Servicio de tipo incorrecto.")
122 }
123 const respuesta = await f
124 if (respuesta.ok) {
125 if (respuesta.status === JsonResponse_NoContent) {
126 return new JsonResponse(JsonResponse_NoContent)
127 }
128 const texto = await respuesta.text()
129 try {
130 const body = JSON.parse(texto)
131 if (respuesta.status === JsonResponse_Created) {
132 const location = respuesta.headers.get("location")
133 return new JsonResponse(JsonResponse_Created, body,
134 location === null ? undefined : location)
135 } else {
136 return new JsonResponse(JsonResponse_OK, body)
137 }
138 } catch (error) {
139 // El contenido no es JSON. Probablemente sea texto.
140 throw new ProblemDetails(ProblemDetails_InternalServerError,
141 "Problema interno en el servidor.", texto)
142 }
143 } else {
144 const texto = await respuesta.text()
145 try {
146 const { type, title, detail } = JSON.parse(texto)
147 throw new ProblemDetails(respuesta.status,
148 typeof title === "string" ? title : "",
149 typeof detail === "string" ? detail : undefined,
150 typeof type === "string" ? type : undefined)
151 } catch (error) {
152 if (error instanceof ProblemDetails) {
153 throw error
154 } else {
155 // El contenido no es JSON. Probablemente sea texto.
156 throw new ProblemDetails(respuesta.status, respuesta.statusText, texto)
157 }
158 }
159 }
160}
161
162/**
163 * Codifica un texto para que cambie los caracteres
164 * especiales y no se pueda interpretar como
165 * etiiqueta HTML. Esta técnica evita la inyección
166 * de código.
167 * @param { string } texto
168*/
169function htmlentities(texto) {
170 return texto.replace(/[<>"']/g, textoDetectado => {
171 switch (textoDetectado) {
172 case "<": return "<"
173 case ">": return ">"
174 case '"': return "&quot;"
175 case "'": return "&#039;"
176 default: return textoDetectado
177 }
178 })
179}
skip_previous skip_next