Manifest.JS: un framework strutturale front-end leggero
Ambito di revisione del codice
Il mio obiettivo con questa recensione è ricevere osservazioni generali e suggerimenti per migliorare l'efficienza / facilità di scrittura del front-end di un'applicazione Web con questo framework di base. Voglio concentrarmi su ciò che sembra che dovrebbe fare piuttosto che sui dettagli di ciò che può o non può fare involontariamente. Limita l'ambito a una panoramica generale, che dovrebbe aiutare a risparmiare tempo poiché è un pezzo di codice di buone dimensioni per una revisione.
L'attenzione dovrebbe essere rivolta alla velocità di sviluppo scalabile (gestibile, ristrutturabile), ai modelli di codice globali e alla progettazione del codice delle applicazioni risultanti.
"Ecco cosa sembra che tu stia cercando di ottenere, qui è dove sei riuscito, qui è dove ti manca, oppure ecco una modifica del quadro generale che potrebbe rendere il codice risultante più facile da leggere, mantenere e più veloce da sviluppare . "
Il problema che Manifest.JS intende risolvere
Progettando app web a pagina singola ho trovato due cose che non mi piacevano di ReactJS (almeno per la mia tipica scala di progetto):
- Ho dovuto scrivere più codice di quanto volessi per realizzare le cose di base
- Era noioso trasportare le informazioni attraverso l'app, dovevi essenzialmente passare un filo attraverso i componenti per ottenere informazioni dal punto A al punto B, il che rendeva il design strettamente accoppiato e difficile da ristrutturare in seguito
Mi sono sentito in questo modo anche per altri framework di app JS che ho provato. Quindi ho scritto due classi abbastanza semplici che lavorano insieme per creare un modello di sviluppo che preferivo. Il mio obiettivo era che questo mi permettesse di:
- Concentra tutto il processo di costruzione di ogni singolo componente di un'app in JS modulare senza doversi preoccupare molto di come ogni componente è collegato all'applicazione esterna.
- Non è necessario passare tra più file o lingue per modificare JavaScript, HTML e CSS per creare o mantenere una qualsiasi funzione dell'interfaccia utente.
Accoppiamento sciolto, separazione delle preoccupazioni, flusso di lavoro JS puro, struttura del progetto prevedibile, flusso di dati facile, non più funzionalità del necessario. Questo è l'obiettivo, che questo codice lo raggiunga o meno, non ne sono ancora sicuro.
Nota: JSX fa qualcosa di simile #2
, ma avere le due lingue in un file mi è sembrato un po 'strano, volevo che i miei file fossero un linguaggio uniforme piuttosto che JSX intessuto come con React.
Autocritiche:
Finora alcune autocritiche che ho considerato:
Quando si tratta di modulare un insieme di
Elements
in una classe, potrei fornire un unico modo stabilito per farlo in modo che ci sia un percorso chiaro per lo sviluppatore e nessuna libertà di sviluppare anti-pattern accidentali quando si decide come impacchettare i componenti in moduli modulari File.Il concatenamento è fantastico. Dovrei aggiornare
.use
per tornare inthis
modo da poter concatenare un'azione comeself.append(new InfoPage().use(subPage, { /* properties */ }).actions.select(true))
Creare la pagina informazioni, utilizzare il modello di pagina secondaria, passare proprietà univoche e selezionarlo per impostazione predefinita. Inoltre può farli
action
restituire inElement
modo che possano essere incatenati.
Componenti:
- Publisher.js : una semplice classe di passaggio di messaggi per implementare il pattern Pub Sub perché volevo essere in grado di inviare eventi separati dallo spazio dei nomi da qualsiasi posizione nell'app e leggerli altrove, come:
publisher.emit("header/select-nav/home", this)
epublisher.on("header/select-nav/" + name, data => {})
. Inoltre, supporto un terzobool
argomento per supportare l'invio e l'ascolto di eventi su un socket facoltativamente passato nel socket Socket.io, ad esempiolet publisher = new Publisher(io())
, in modo da poter gestire eventi locali e remoti allo stesso modo.
Utilizzo:
let publisher = new Publisher(io()) // or let publisher = new Publisher()
publisher.on("namespace1/subnamespace2/event-name", data => {}, false)
// third arg set to true tells the event handler to listen for Socket.io events
- Element.js : un wrapper per elementi HTML che facilita l'intera generazione e logica dell'HTML dell'app, quindi la logica associata a ciascun componente visibile dell'app è strettamente accoppiata ad essa, mentre tutti i componenti individualmente rimangono liberamente accoppiati tra loro . Ho anche in programma di aggiungere forse anche il supporto per la generazione di classi CSS localmente all'interno di ciascun componente.
Utilizzo:
new Element("div", { // create a div
id: "header", // set the id
traits: { publisher }, // assign data that will be inherited by any children
data: { name: "primary" }, // assign data to this element only
text: "My header text", // set text
innerHTML: false, // set innerHTML
classes: [], // assign classes
attributes: { // assign attributes
"contenteditable": "true"
},
styles: {}, // assign styles
actions: { // create actions, which will be wrapped in a
show: (self, arg1) => { // wrapper func and receive the Element as
self.clearStyle("opacity") // their first argument, followed by any
self.addClass("visible") // args passed with Element.action.name()
console.log("called with Element.actions.show(arg1)")
},
hide: self => {
self.removeClass("visible") // remove them
self.style("opacity", 0) // or set styles
}
},
callback: self => { // trigger as soon as the element is created
self.append(new Element("div", {
id: "important-icon",
classes: ["hidden", "header-icon"],
actions: {
select: () => {
self.addClass("visible")
self.removeClass("hidden")
self.updateText("Selected") // update text
}
},
ready: self => {
self.on("mouseover", evt => { // handle DOM events
self.addClass("highlight")
})
}
}))
},
ready: self => { // trigger after the element is appended to a parent
self.traits.publisher.on("events/header/" + self.data.name, data => {
self.select("#important-icon").actions.select();
// you could of course apply this listener to the icon itself,
// but the select feature is convenient in some cases
})
}
}).appendTo(document.body)
- Controller.js : la convalida dell'input durante il flusso di dati diventa sempre più importante quanto più grande diventa un'applicazione. Quindi dovrebbe essere una scelta, ovviamente, se vuoi usarlo, e l'ho reso disponibile e supportato per convalidare il flusso di dati sia all'interno dell'elemento che nel publisher. Non ho ancora codificato nel supporto per i publisher, ma funzionerà allo stesso modo con
Element
, conpublisher.use(controller)
. Ma volevo anche un passaggio per passare un input di blueprint a un insieme di elementi che richiedono le stesse proprietà, e ha senso che un controller sia in grado di sovrascrivere l'input corrente che lo attraversa per facilità di test / debug, quindi ho aggiunto uninsert
metodo, che (come vedrai nel codice) può e deve essere utilizzato per creare modelli di proprietà degli elementi.
Utilizzo:
let page = new Controller({
data: data => { // pass a function to validate data however you want
if (!data.name) return false
else return true
},
traits: true, // pass true to simply ensure a setting is passed
actions: "object", // pass a string to test against typeof
}).insert({ // and insert specific default data
traits: {
publisher
},
actions: {
select: self => {
let target = "header/select-nav/" + self.data.name.toLowerCase()
self.traits.publisher.emit(target, this)
self.addClass("visible")
}
},
ready: self => {
self.traits.publisher.emit("header/add-nav", self)
}
});
Element.js:
import Controller from "/js/classes/controller.js"
function isCyclic(obj) {
var seenObjects = [];
function detect(obj) {
if (obj && typeof obj === 'object') {
if (seenObjects.indexOf(obj) !== -1) {
return true;
}
seenObjects.push(obj);
for (var key in obj) {
if (obj.hasOwnProperty(key) && detect(obj[key])) {
//console.log(obj, 'cycle at ' + key);
return true;
}
}
}
return false;
}
return detect(obj);
}
function isObject(item) {
return item && typeof item === 'object' && !Array.isArray(item);
}
function isIterable(item) {
let type = false;
if (isObject(item)) type = 'obj';
else if (Array.isArray(item)) type = 'arr';
return type;
}
function mergeDeeper(source, target) {
let allProps = [];
let sourceProps;
let type;
let targetProps;
if (isObject(source)) {
sourceProps = Object.keys(source);
type = 'obj';
} else if (Array.isArray(source)) {
sourceProps = source;
type = 'arr';
} else {
return source;
}
if (isObject(target)) {
targetProps = Object.keys(target);
} else if (Array.isArray(target)) {
targetProps = target;
} else {
debugger
throw "target missing"
}
sourceProps.forEach(prop => {
allProps.push(prop);
});
targetProps.forEach(prop => {
allProps.push(prop);
});
allProps = [...new Set(allProps)];
let merged
if (type == 'obj') {
merged = {};
} else if (type == 'arr') {
merged = [];
}
allProps.forEach(prop => {
if (type == "obj") {
if (source[prop]) {
if (isIterable(source[prop])) {
if (isIterable(target[prop])) {
merged[prop] = mergeDeeper(source[prop], target[prop])
} else merged[prop] = source[prop]
} else {
merged[prop] = source[prop]
}
} else {
if (source[prop] !== undefined) {
merged[prop] = source[prop]
} else {
merged[prop] = target[prop]
}
}
} else {
let iterable = isIterable(prop);
if (iterable) {
let filler
if (iterable == "obj") filler = {};
else if (iterable == "arr") filler = [];
merged.push(mergeDeeper(prop, filler))
} else {
merged.push(prop)
}
}
})
return merged;
}
const collectChildSelectors = (elementWrapper, selectors) => {
elementWrapper.children.forEach(childWrapper => {
if (childWrapper.element.id) {
selectors[childWrapper.element.id] = childWrapper
}
if (childWrapper.selector) {
selectors[childWrapper.selector] = childWrapper
}
collectChildSelectors(childWrapper, selectors)
})
}
const applySettings = function(newSettings) {
if (!newSettings) throw "bad settings"
let settings = mergeDeeper(newSettings, {
text: false,
innerHTML: false,
classes: [],
actions: {},
data: {},
attributes: {},
styles: {},
traits: {},
id: false,
callback: false,
ready: false,
});
if (settings.id) {
this.element.id = settings.id
this.selector = settings.id
}
if (settings.text) this.element.textContent = settings.text
if (settings.innerHTML) this.element.innerHTML = settings.innerHTML
if (settings.selector) {
this.selector = settings.selector
this.selectors[settings.selector] = this;
}
settings.classes.forEach(className => this.element.classList.add(className))
Object.keys(settings.attributes).forEach(attributeName =>
this.element.setAttribute(attributeName,
settings.attributes[attributeName]))
Object.keys(settings.styles).forEach(styleName =>
this.element.style[styleName] = settings.styles[styleName])
Object.keys(settings.actions).forEach(actionName =>
this.actions[actionName] = () => settings.actions[actionName](this))
Object.keys(settings.data).forEach(propertyName =>
this.data[propertyName] = settings.data[propertyName])
Object.keys(settings.traits).forEach(propertyName =>
this.traits[propertyName] = settings.traits[propertyName])
if (settings.ready) this.ready = settings.ready
if (settings.callback) settings.callback(this);
}
export default class {
constructor(tag, settings) {
this.children = [];
this.data = {}
this.actions = {}
this.traits = {}
this.selectors = {}
this.element = document.createElement(tag)
applySettings.apply(this, [settings])
}
use(arg1, arg2) {
if (arg1 instanceof Controller) {
let controller = arg1;
let settings = arg2;
let mergedSettings = mergeDeeper(settings, controller.insertions);
controller.test(mergedSettings);
applySettings.apply(this, [mergedSettings])
} else if (arguments.length === 1) {
let settings = arg1;
applySettings.apply(this, [settings])
} else {
throw "bad settings passed to Element"
}
return this;
}
addEventListener(event, func) {
this.element.addEventListener(event, func)
}
delete() {
this.parent.removeChild(this.element)
}
style(styleName, value) {
this.element.style[styleName] = value
}
clearStyle(styleName) {
this.element.style[styleName] = ""
}
updateText(text) {
this.element.textContent = text
}
updateAttribute(attributeName, attributeContent) {
this.element.setAttribute(attributeName, attributeContent)
}
addClass(className) {
this.element.classList.add(className)
}
removeClass(className) {
this.element.classList.remove(className)
}
on(evt, func) {
this.element.addEventListener(evt, func)
}
select(id) {
let parts = id.split("#")
let selector = parts[parts.length - 1];
if (!this.selectors[selector]) debugger;
//throw "bad selector " + selector
return this.selectors[selector]
}
appendTo(elementWrapper) {
let element
if (elementWrapper.nodeName) element = elementWrapper
else {
element = elementWrapper.element
this.parent = element
collectChildSelectors(this, elementWrapper.selectors)
Object.keys(elementWrapper.traits).forEach(propertyName =>
this.traits[propertyName] = elementWrapper.traits[propertyName])
}
if (this.ready) this.ready(this)
element.appendChild(this.element)
return this
}
append(elementWrapper) {
let element
let wrapped = false
if (elementWrapper.nodeName) element = elementWrapper
else {
wrapped = true
element = elementWrapper.element
element.parent = this
if (element.id) this.selectors[element.id] = elementWrapper
if (elementWrapper.selector)
this.selectors[elementWrapper.selector] = elementWrapper
this.children.push(elementWrapper)
collectChildSelectors(elementWrapper, this.selectors)
Object.keys(this.traits).forEach(propertyName =>
elementWrapper.traits[propertyName] = this.traits[propertyName])
}
if (elementWrapper.ready) elementWrapper.ready(elementWrapper)
this.element.appendChild(element)
if (wrapped) return elementWrapper
}
}
Controller.js:
export default class {
constructor(settings) {
this.tests = {};
Object.keys(settings).forEach(key => {
let val = settings[key];
if (typeof val == "boolean") {
this.tests[key] = input => {
return input !== undefined
}
} else if (typeof val == "string") {
this.tests[key] = input => {
return typeof input === val
}
} else if (typeof val == "function") {
this.tests[key] = val;
}
})
}
test(obj) {
Object.keys(obj).forEach(key => {
if (!this.tests[key] || !this.tests[key](obj[key])) {
console.log("Controller test failed");
debugger;
}
});
}
insert(insertion) {
this.insertions = insertion;
return this;
}
}
Publisher.js
export default class {
constructor(socket) {
if (socket) this.socket = socket;
this.events = {};
}
on(command, func, socket = false) {
if (!this.events[command]) this.events[command] = [];
this.events[command].push(func);
if (socket && this.socket) socket.on(command, func);
}
emit(command, data = {}, socket = false) {
if (this.events[command]) {
this.events[command].forEach(func => func(data));
}
if (socket && this.socket) socket.emit(command, data);
}
}
Implementazione
app.js
:
import Publisher from "/js/classes/publisher.js"
import Controller from "/js/classes/controller.js"
let publisher = new Publisher(io())
import Header from "/js/classes/header/header.js"
import Home from "/js/classes/pages/home/home.js"
import News from "/js/classes/pages/news/news.js"
import Leaderboard from "/js/classes/pages/leaderboard/leaderboard.js"
import Account from "/js/classes/pages/account/account.js"
import Builder from "/js/classes/pages/builder/builder.js"
let header = new Header(publisher)
let page = new Controller({
data: true, // () => { } // validate the value however you choose
traits: true, // It's good to have this capability for debugging
actions: true, // or for if your boss wants all your data interfaces
ready: true // validated because he read it in a hip dev blog
}).insert({ // <- But insertion is the feature you'll be using
traits: { // more often to test input data, debug, and like with
publisher // this case, apply a single input object to multiple
}, // Elements
actions: {
select: self => {
let target = "header/select-nav/" + self.data.name.toLowerCase()
self.traits.publisher.emit(target, this)
self.addClass("visible")
}
},
ready: self => {
self.traits.publisher.emit("header/add-nav", self)
}
});
new Home().use(page, {
data: {
name: "Home",
iconPath: "/assets/home/home-1.png",
cornerPath: "/assets/corners/corner-1.png",
}
}).appendTo(document.body)
new News().use(page, {
data: {
name: "News",
iconPath: "/assets/news/news-1.png",
cornerPath: "/assets/corners/corner-5.png"
}
}).appendTo(document.body)
new Leaderboard().use(page, {
data: {
name: "Leaderboard",
iconPath: "/assets/leaderboard/leaderboard-1.png",
cornerPath: "/assets/corners/corner-3.png",
}
}).appendTo(document.body)
new Account().use(page, {
data: {
name: "Account",
iconPath: "./assets/profile/profile-1.png",
cornerPath: "/assets/corners/corner-4.png",
}
}).appendTo(document.body)
new Builder().use(page, {
data: {
name: "Builder",
iconPath: "./assets/builder/builder-1.png",
cornerPath: "/assets/corners/corner-2.png",
}
}).appendTo(document.body).actions.select()
/js/classes/pages/builder/builder.js
:
Qui ho usato una sorta di strana return
dichiarazione nel costruttore, puramente per scopi visivi perché mi piace usare new ModuleName()
nel file in cui è usato, al contrario di una chiamata di funzione, ma puoi farlo in entrambi i casi.
import Element from "/js/classes/element.js"
import NavBar from "/js/classes/pages/builder/nav-bar.js"
export default class {
constructor() {
return new Element("div", {
id: "builder",
classes: ["page"],
actions: {
select: self => {
let target = "header/select-nav/" + self.data.name.toLowerCase()
self.traits.publisher.emit(target, this)
self.addClass("visible")
}
},
ready: self => {
self.traits.publisher.emit("header/add-nav", self)
self.actions.select()
},
callback: self => {
self.append(new NavBar());
// add more elements
}
})
}
}
/js/classes/pages/header/header.js
import Element from "/js/classes/element.js"
import NavIcon from "./header-nav-icon.js"
export default class {
constructor(publisher) {
return new Element("div", {
id: "header",
traits: { publisher },
ready: self => {
self.append(new Element("div", {
selector: "title-wrapper",
classes: ["title-wrapper"],
ready: self => {
self.append(new Element("div", {
selector: "location-wrapper",
classes: ["location-wrapper"],
ready: self => {
self.traits.publisher.on("header/add-nav", data => {
self.append(new Element("div", {
selector: "location-item-wrapper",
classes: ["location-item-wrapper"],
ready: self => {
self.traits.publisher.on("header/select-nav/" +
data.data.name.toLowerCase(), data => {
self.addClass("visible")
});
self.append(new Element("div", {
id: data.data.name.toLowerCase() + "-nav",
classes: ["location-item", "heading"],
text: data.data.name
}))
self.append(new Element("img", {
classes: ["location-item-icon"],
attributes: {
"src": data.data.iconPath.split(".png")[0] + "-flat.png"
}
}))
self.append(new Element("img", {
selector: "corner",
classes: ["corner"],
attributes: {
"src": data.data.cornerPath
}
}))
}
}))
})
}
}))
self.append(new Element("div", {
selector: "sub-location-wrapper",
classes: ["sub-location-wrapper", "subheading"]
}))
}
}))
self.append(new Element("div", {
selector: "nav-wrapper",
classes: ["nav-wrapper", "center-contents"],
ready: self => {
self.traits.publisher.on("header/add-nav", data => {
console.log("header/add-nav, data", data.data)
console.log("adding nav-item")
self.append(new NavIcon().use({
data: data.data
}))
});
self.append(new Element("div", {
classes: ["title-bg-wrapper"],
ready: self => {
self.append(new Element("img", {
classes: ["title-bg-icon"],
attributes: {
"src": "./assets/header.png"
}
}))
self.append(new Element("div", {
classes: ["title-bg-text"],
innerHTML: "BIDRATE <br/> RENAISSANCE"
}))
}
}))
}
}))
}
}).appendTo(document.body)
}
}
Risposte
Da una breve recensione;
isCyclic
-> Vorrei prendere in considerazione gettareobj
nelJSON.stringify
e intercettare l'eccezione rilevantefunction detect
non è un bel nome, va bene per il contesto, ma potrebbe essere migliore//console.log(obj, 'cycle at ' + key);
<- commento negativoIl codice utilizza sia
var
eset
econst
, c'è un vero valore nell'analisi del codice e nell'uso soloset
/const
function isObject(item)
<- un nome icky poiché in realtà controlli se è un oggetto ma non un Array (che è anche un oggetto), quindi perché non puoi usare questa funzione inif (obj && typeof obj === 'object')
function isIterable(item) {
<- nome molto icky, il lettore assume restituisce un valore booleano, soprattutto con la prima linea esserefalse
, ma poi anche di tornareobj
oarr
, forse lo chiamanoiterableType
che il ritornoundefined
,'obj'
o'arr'
?Stai saltando le parentesi graffe
isIterable
, non dovrestidebugger
non appartiene al codice di produzioneQuesto
sourceProps.forEach(prop => { allProps.push(prop); }); targetProps.forEach(prop => { allProps.push(prop); });
potrebbe essere
allProps = allProps.concat(sourceProps).concat(targetProps);
Sai che solo Object e Array sono iterabili e che la proprietà è iterabile in questo modo
let filler if (iterable == "obj") filler = {}; else if (iterable == "arr") filler = [];
può essere
let filler = iterable=="obj"?{}:[];
Nel complesso vorrei documentarmi sull'operatore ternario, questo
if (source[prop] !== undefined) { merged[prop] = source[prop] } else { merged[prop] = target[prop] }
potrebbe essere cortocircuitato e più leggibile (per me);
merged[prop] = source[prop]?source[prop]:target[prop];
e in questo caso potrebbe anche essere ridotto a
merged[prop] = source[prop] || target[prop];
Il codice ha un uso incoerente del punto e virgola, molto fastidioso da leggere
Dovresti scegliere uno standard di denominazione / codifica e rispettarlo, prima
function
veniva usata la parola chiave e ora il codice passa a questo;const collectChildSelectors = (elementWrapper, selectors) => {
Non sono sicuro del motivo per cui non stai fornendo tutti i parametri possibili
addEventListener
addEventListener(event, func) { this.element.addEventListener(event, func) }
Fai quanto segue 5 volte con parametri diversi, questo potrebbe utilizzare una funzione di supporto per renderlo più leggibile;
Object.keys(settings.styles).forEach(styleName => this.element.style[styleName] = settings.styles[styleName])
Panoramica del quadro generale
Limita l'ambito a una panoramica generale, che dovrebbe aiutare a risparmiare tempo poiché è un pezzo di codice di buone dimensioni per una revisione.
Anche se ho collegato i moduli a un plunker, è ancora difficile per me determinare se il framework potrebbe aiutarmi con una SPA con codice limitato. Vedo molti metodi che accettano self
come primo (e spesso unico) parametro. Perché non possono operare this
? Il contesto non è vincolato correttamente per questo?
Ho creato un modulo emettitore di eventi per un'intervista. I requisiti suonano come il pattern Pub-Sub e ho implementato metodi simili al Publisher. I requisiti richiedevano un modo per avere un gestore "occasionale", nonché un modo per annullare la registrazione di una funzione gestore registrato. Potresti considerare di offrire tale funzionalità con il tuo modulo editore.
La linea di fondo è: se ritieni che questo framework ti consenta di scrivere meno codice di quanto potresti altrimenti con molti altri framework, vai avanti e usalo.
Feedback JS mirato
Ho notato che la const
parola chiave appare solo due volte nel tuo codice, per due espressioni di funzione, ad esempio collectChildSelectors
e applySettings
. Si consiglia di const
utilizzare la parola chiave predefinita per tutte le variabili e quindi, se è necessaria la riassegnazione, passare a using let
. Inoltre, evitare var
, a meno che non sia necessario qualcosa come una variabile globale, ma anche questo è disapprovato.
Alcune parti del codice vengono utilizzate ===
per confrontare i valori, ma altre utilizzano ==
. Una pratica consigliata è sempre quella di utilizzare un rigoroso confronto dei tipi.
Per una maggiore leggibilità, utilizzare uno stile di virgolette coerente per le stringhe letterali: virgolette singole o doppie ma non entrambe.
mergeDeeper()
potrebbe usare l'operatore spread invece di forEach () -> push for sourceProps
etargetProps
allProps.push(...sourceProps, ...targetProps)
Il nome della funzione isIterable
sembra alquanto strano dato che può restituire una stringa o un valore booleano. Forse sarebbe un nome più appropriato iterableType
- anche se restituisse, false
il chiamante saprebbe che il valore non è iterabile.