Letzten Monat bat die Designerin Helen Tran Leute, fünf Designerinnen zu nennen, die ihrer Meinung nach für die Branche wertvoll sind.
Ich brauche deine Hilfe.
Nenne fünf Designerinnen, die deiner Meinung nach für unsere Branche sehr wertvoll und wichtig sind.— Helen Tran (@tranhelen) 26. April 2017
Innerhalb weniger Tage generierte der Tweet über 373 Antworten, die 636 Frauen namentlich mit ihren Twitter-Benutzernamen erwähnten. Einige Leute scherzten, dass der Thread ein großartiger Ort sei, um potenzielle Mitarbeiter zu finden, und mir wurde klar, dass ich ein anderes Bedürfnis hatte: Ich versuche niemanden anzuwerben, aber ich würde gerne Frauen finden, denen ich folgen kann, die in Bereichen, an denen ich interessiert bin, oder in denen ich mehr lernen möchte, hervorragend sind. Da viele Leute ihren Jobtitel oder Fachbereich in ihre Twitter-Beschreibungen aufnehmen, erkannte ich, dass ich ein selbstberichtetes Filtersystem erstellen könnte, indem ich jede Beschreibung nach Schlüsselwörtern durchsuche und dann die Profile in einem sortierbaren Verzeichnis anzeige.

Ich beschloss, den Thread als Ausgangspunkt zu nehmen und eine eigenständige Website zu erstellen: Women Who Design. Hier ist, wie ich sie gebaut habe.
Erste Schritte
Zuerst musste ich
- den Twitter-Benutzernamen jeder Person, auch bekannt als „Handle“, aus dem ursprünglichen Thread in einer Liste aufzeichnen.
- die Projekt-Dateistruktur einrichten.
- die Twitter REST API dazu bringen, Profilinformationen von einem gegebenen Handle zurückzugeben.
- eine Datenbank auswählen, um jedes Handle und seine entsprechenden Profilinformationen zu speichern.
Der erste Schritt, das Sammeln der Handles, war der einfachste. Mit einer guten Playlist im Hintergrund verbrachte ich etwa eine Stunde damit, den Twitter-Thread durchzugehen und jeden Handle in eine Tabelle einzugeben, die ich dann als JSON-Datei namens `designers.json` exportierte.
Zu diesem Zeitpunkt initialisierte ich mein Git-Repository und richtete eine grundlegende Dateistruktur ein
- index.html
- app.js
- styles.css
- designers.json
Ganz oben in meiner `app.js`-Datei importierte ich alle Designer aus dem ursprünglichen Twitter-Thread.
var designers = require('./designers.json');
Als nächstes registrierte ich meine App bei Twitter, um mit der REST API zu arbeiten.

Ich entschied mich, das Projekt als schreibgeschützte Anwendung zu konfigurieren, da ich nur den GET users/show Endpunkt nutzen wollte, der Benutzerprofilinformationen liefert.

Dann installierte ich über die Kommandozeile eine asynchrone Client-Bibliothek für Twitter (auch Twitter genannt), um Anfragen an die API stellen zu können.
npm install twitter
Um die Bibliothek in meinem Projekt nutzen zu können, musste ich sie ebenfalls oben in meiner `app.js`-Datei importieren.
var twitter = require('twitter');
Laut der Dokumentation der Client-Bibliothek musste ich nach dem Import von `"twitter"` meinen Consumer-Key, das Consumer-Secret und ein Bearer-Token meiner App in meine `.js`-Datei eintragen.
var client = new Twitter({
consumer_key: '',
consumer_secret: '',
bearer_token: ''
});
Der Key und das Secret waren leicht in meinem Twitter-App-Dashboard zu finden, aber das Bearer-Token erforderte einen zusätzlichen Schritt. Ich führte den folgenden Befehl in der Kommandozeile aus, um das Bearer-Token zu erhalten, indem ich die Variablen mit meinen Anmeldeinformationen aus dem Dashboard füllte, und fügte dann das Ergebnis zur obigen Client-Variable hinzu.
curl -u "$CONSUMER_KEY:$CONSUMER_SECRET" \
--data 'grant_type=client_credentials' \
'https://api.twitter.com/oauth2/token'
Die Client-Bibliothek bot auch eine praktische Komfortfunktion zum Stellen von Anfragen, also fügte ich sie meiner app.js-Datei mit einem Hinweis hinzu, sie später auszufüllen. Laut der Dokumentation des GET users/show Endpunkts müsste ich jedes Handle aus meiner Liste an den Parameter `"screen_name"` übergeben, um die gesuchten Profilinformationen zu erhalten.
client.get('users/show', {'screen_name': handle}, function(error, response) {
if (!error) {
console.log(response);
// do stuff here later!
}
});
Wenn alles korrekt durchgeführt wurde, könnte die Antwort ungefähr so aussehen
{
"id": 2244994945,
"id_str": "2244994945",
"name": "TwitterDev",
"screen_name": "TwitterDev",
"location": "Internet",
"profile_location": null,
"description": "...",
Schließlich musste ich eine Datenbank auswählen, um die Profile zu speichern. Ich entschied mich für die Realtime Database von Firebase, da es sich um eine NoSQL-Datenbank handelt, die JSON als Speicherformat verwendet. Ich installierte `firebase` und `firebase-admin` über npm und importierte sie dann oben in meiner `app.js`-Datei neben allem anderen.
var firebase = require('firebase');
var admin = require('firebase-admin');
Um das Schreiben und Lesen aus der Datenbank zu ermöglichen, musste ich Firebase mit einem speziell generierten privaten Schlüssel eines „Service-Kontos“ authentifizieren. Ich generierte den Schlüssel im Reiter „Service Accounts“ meiner Firebase-Einstellungen und fügte den entsprechenden Code unterhalb meiner übrigen Konfigurationen ein.
var serviceAccount = {
"type": "service_account",
"project_id": process.env.WWD_FIREBASE_PROJECT_ID,
"private_key_id": process.env.WWD_FIREBASE_PRIVATE_KEY_ID,
"private_key": process.env.WWD_FIREBASE_PRIVATE_KEY,
"client_email": process.env.WWD_FIREBASE_CLIENT_EMAIL,
"client_id": process.env.WWD_FIREBASE_CLIENT_ID,
"auth_uri": "https://#/o/oauth2/auth",
"token_uri": "https://#/o/oauth2/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": process.env.WWD_FIREBASE_CLIENT_CERT_URL
};
Arbeiten mit den Daten
Puh! Liste aufgezeichnet: erledigt. App registriert: erledigt. Dateistruktur eingerichtet: erledigt. Datenbank eingerichtet: erledigt. Zeit, die API tatsächlich zu nutzen.
Ich habe mir die Beispielantwort in der Dokumentation des GET users/show Endpunkts angesehen und entschieden, dass ich den Namen des Benutzers, das Handle (in der Dokumentation als `screen_name` bezeichnet), den Standort, die Beschreibung und die URL des Profilbilds speichern muss. Außerdem wollte ich die Profilfarbe, die jeder Benutzer auf Twitter eingestellt hat, speichern, um sie als Hervorhebungsfarbe für Schaltflächen, Links und andere Akzente zu verwenden. Daher fügte ich den folgenden Code in die Komfortfunktion der Client-Bibliothek ein, die ich in meiner `app.js`-Datei gespeichert hatte.
var name = response.name;
var handle = response.screen_name;
var description = response.description;
var imageUrl = response.profile_image_url_https;
var location = response.location;
var profileColor = response.profileColor;
Um Situationen zu vermeiden, in denen Unterschiede in der Großschreibung zu zwei Instanzen desselben Profils führen würden (wie z. B. @julesforrest vs. @JulesForrest), habe ich die Variable für das Handle in Kleinbuchstaben umgewandelt.
handle = handle.toLowerCase();
Dann bemerkte ich, dass die zurückgegebenen Profilbilder viel zu klein waren, um sie auf einem Desktop-Display sinnvoll nutzen zu können. Durch Herumspielen mit der Bild-URL fand ich einen größeren Bildpfad, der für meine Bedürfnisse besser funktionierte.
imageUrl = response.profile_image_url_https.replace("_normal", "_400x400");
Links verknüpfen
Leute haben oft URLs, @Handles, #Hashtags und E-Mail-Adressen in ihren Profilbeschreibungen, aber leider gab die Twitter-API jede Beschreibung als einfachen String zurück. Glücklicherweise fand ich ein Tool namens Autolinker, das jeden String durchsuchte und an den entsprechenden Stellen Anker-Tags erstellte.
Um es zum Laufen zu bringen, installierte ich es über npm, dann importierte ich es oben in meiner app.js-Datei.
npm install autolinker --save
var Autolinker = require( 'autolinker' );
Die grundlegende Verwendung sah ziemlich einfach aus und es gab einige Optionen, die als Objekt an den zweiten Parameter übergeben werden konnten, wie z. B. das Öffnen jedes Links in einem neuen Fenster oder das Hinzufügen einer Klasse zu jedem Anker-Tag.
var linkedText = Autolinker.link( textToAutolink[, options] );
Ich wollte jedoch die benutzerdefinierte Profilfarbe jedes Benutzers (zuvor aus der API-Antwort gespeichert) als Inline-Stil auf den Links dieses Benutzers hinzufügen, was das Schreiben einer benutzerdefinierten Ersetzungsfunktion erforderte. Während benutzerdefinierte Ersetzungsfunktionen einige ziemlich komplexe Konfigurationen verarbeiten können, habe ich das Beispiel aus der Dokumentation nur leicht angepasst, um die Inline-Farbstile hinzuzufügen und jeden Link in einem neuen Fenster zu öffnen. Es ist wichtig zu beachten, dass das soziale Netzwerk für Erwähnungen und Hashtags oben im Objektparameter angegeben werden muss, um sie richtig zu verlinken, was aus der Dokumentation nicht sofort ersichtlich war.
description = Autolinker.link( description, {
mention: 'twitter',
hashtag: 'twitter',
replaceFn : function( match ) {
switch( match.getType() ) {
case 'url' :
var tag = match.buildTag();
tag.setAttr( 'style', 'color: #' + profileColor );
return tag;
case 'mention' :
var mention = match.getMention();
return `<a href="https://twitter.com/${mention}" target="blank" style="color: #${profileColor}">@${mention}</a>`;
case 'email' :
var email = match.getEmail();
return `<a href="mailto:"${email}" target="blank" style="color: #${profileColor}">${email}</a>`;
case 'hashtag' :
var hashtag = match.getHashtag();
return `<a href="https://twitter.com/hashtag/${hashtag}" target="blank" style="color: #${profileColor}">#${hashtag}</a>`;
}
}
});
Frustrierenderweise tauchten jedoch die von Twitter verkürzten t.co-URLs als Text des Anker-Tags anstelle der beschreibenden URL auf.

Nach mehreren Stunden Debugging bemerkte ich schließlich, dass die t.co-URLs und *nicht* die beschreibenden URLs die ganze Zeit in den ursprünglichen Strings waren, die von der API zurückgegeben wurden. Nach erneuter Prüfung der Beispielantwort fand ich ein `description.urls`-Objekt, das ich vorher übersehen hatte, und gab es ebenfalls aus, wobei ich den t.co-URL-Text durch den entsprechenden beschreibenden URL-Text ersetzte.
var descriptionUrls = response.entities.description.urls;
if (descriptionUrls.length != 0) {
for (var i = 0; i < descriptionUrls.length; ++i) {
description = description.replace(descriptionUrls[i].url, `${descriptionUrls[i].display_url}`);
}
}
Nach Filtern suchen
Menschen profitieren am meisten von Twitter, wenn sie Personen folgen, die in irgendeiner Weise für sie relevant sind. Daher musste es für die Verzeichnisnutzer einfach sein, die Profile nach Position oder Fachgebiet zu sortieren. Zum Beispiel würde ich als jemand, der sich für Front-End-Entwicklung interessiert, gerne herausfinden, welche Frauen im Verzeichnis sich selbst als Entwicklerinnen bezeichnen.

Das Hinzufügen dieser sortierbaren Filter war der letzte und wichtigste Schritt, bevor ich jedes Profil nach Firebase schreiben konnte, und ich habe viel Zeit damit verbracht, über die richtige Vorgehensweise nachzudenken. Ich wusste, dass ich ein selbstberichtetes Filtersystem erstellen könnte, indem ich jede Profilbeschreibung nach Schlüsselwörtern durchsuche, aber viele der Begriffe, die die Leute in ihren Beschreibungen verwendeten, überschnitten sich (Product Designer und UX Designer, Entwickler und Ingenieur). Letztendlich entschied ich, dass es wichtig war, die genauen Begriffe zu verwenden, die die Leute zur Beschreibung ihrer selbst benutzten, auch wenn dies mehr Klicks für den Verzeichnisnutzer bedeutete.
Um die Filterkategorien auszuwählen, suchte ich nach Begriffen, die insgesamt am häufigsten in den Beschreibungen vorkamen, und schrieb eine Funktion, um nach Begriffen zu suchen und entsprechende Tags in ein Array zu pushen. Ich plante, das Array später im Frontend zu verwenden, um die Filterung zu ermöglichen.
var designerTagsArray = [];
function addDesignerTags(handle, searchTerm, tag) {
if ((description.toUpperCase()).includes(searchTerm) === true) {
designerTagsArray.push(tag);
};
};
addDesignerTags(handle, "PRODUCT DESIGN", "product ");
addDesignerTags(handle, "LEAD ", "lead ");
addDesignerTags(handle, "MANAGER", "manager ");
// etc, etc
Für bestimmte Begriffe wie „Direktor“ musste ich eine benutzerdefinierte Suche durchführen, um ähnliche Phrasen mit erheblich unterschiedlichen Bedeutungen wie Art Director oder Creative Director auszusortieren.
if ((description.toUpperCase()).includes("DIRECTOR") === true) {
if ((description.toUpperCase()).includes("ART DIRECTOR") === true) {
// do nothing
} else if ((description.toUpperCase()).includes("CREATIVE DIRECTOR") === true) {
// do nothing
}
else {
designerTagsArray.push("director");
};
};
Als die Filterung abgeschlossen war, stringifizierte ich das Array und entfernte alle zusätzlichen Zeichen.
designerTagsArray = JSON.stringify(designerTagsArray);
designerTagsArray = designerTagsArray.replace(/[^\w\s]/gi, '');
Schreiben nach Firebase
Es war Zeit, alle Daten aus der ursprünglichen Liste hochzuladen. Zuerst erstellte ich ein neues Objekt, um alle Informationen zu speichern, die ich nach Firebase schreiben musste.
var designerProfile = new Object();
Alle Elemente zugewiesen
designerProfile.name = name;
designerProfile.handle = handle;
designerProfile.description = description;
designerProfile.imageUrl = imageUrl;
designerProfile.imageUrlMobile = imageUrlMobile;
designerProfile.profileColor = profileColor;
designerProfile.designerTags = designerTagsArray;
Und schrieb eine Funktion, um sie einem Objekt hinzuzufügen, das ich auf Firebase `display` nannte.
function writeToFirebase(handle, designerProfile) {
firebase.database().ref('display/' + handle).set({
designerProfile
});
console.log(handle + " has been written to Firebase!");
};
writeToFirebase(handle, designerProfile);
Bis zu diesem Zeitpunkt war der gesamte Code, den ich geschrieben hatte, in der ursprünglichen Komfortfunktion der Twitter-Client-Bibliothek enthalten.
client.get('users/show', {'screen_name': handle}, function(error, response) {
if (!error) {
console.log(response);
// log relevant data
// lowercase handle
// adjust image URL
// add links to descriptions
// search and add tags
// write to Firebase
}
});
Ich wickelte die Komfortfunktion in eine Funktion namens `getProfileInfo` ein, die ein Twitter-Handle als Parameter akzeptierte.
var getProfileInfo = function(handle) {
client.get('users/show', {'screen_name': handle}, function(error, response) {
if (!error) {
console.log(response);
// log relevant data
// lowercase handle
// adjust image URL
// add links to descriptions
// search for tags
// write to Firebase
}
});
};
Dann schrieb ich eine Schleife, um jedes Handle aus der JSON-Datei der ursprünglichen Liste zu durchlaufen, die ich zuvor oben in meiner app.js-Datei importiert hatte.
for (var i = 0; i < designers.length; ++i) {
getProfileInfo(designers[i].handle);
};
Schließlich führte ich das Skript über die Kommandozeile mit Node aus und alle Profilinformationen wurden in Firebase angezeigt.
node app.js

Das Frontend
Während ich die Datenprobleme löste, arbeitete ich auch an einem einfachen Frontend, das Profile aus der Datenbank las und sie mit jQuery in HTML aufbaute. Ich erstellte auch eine „Über uns“- und eine „Nominierungs“-Seite mit einem Formular auf der Nominierungsseite, um neue Einreichungen zu erfassen.

Um das Formular zum Laufen zu bringen, schnappte ich mir den eingegebenen Text aus jedem der Eingabefelder und fügte ihn einem neuen Firebase-Objekt hinzu, das ich für die spätere Überprüfung `submit` nannte.
var designerDatabase = firebase.database();
$('#designer-submission').submit(function(event){
event.preventDefault();
var handle = $('#handle').val();
var reason = $('#reason').val();
designerDatabase.ref('submissions/' + handle).set({
handle: handle,
reason: reason
});
$('#handle').val('');
$('#reason').val('');
});
Alles in allem endete ich mit einer clientseitigen `.js`-Datei, drei `.html`-Dateien, einer Logo-`.svg`-Datei und einer `.css`-Datei.
Live schalten
Als die grundlegenden Interaktionen alle kodiert waren, beschloss ich, dass es Zeit war, das Projekt auf Heroku hochzuladen. Da der größte Teil der App auf serverseitigem Node basierte, benötigte ich ein Tool namens Express.js, um es als tatsächliche Website zu veröffentlichen. Dazu musste ich meine `package.json`-Datei einrichten.
npm init
Nachdem ich eine Reihe von Fragen zu Name und Versionsnummer meiner App beantwortet hatte, wurde ich aufgefordert, einen Einstiegspunkt anzugeben, den ich als Standard beließ: `index.js`.
entry point: (index.js)
Dann installierte ich Express.
npm install express --save
Danach richtete ich meine index.js-Datei ein, die so aussah.
var express = require('express');
var app = express();
app.use(express.static('public'));
app.get('/', function (req, res) {
res.sendFile('index.html');
});
app.listen(process.env.PORT || 3000, function () {
console.log('Example app listening on port 3000!');
});
Um alle meine clientseitigen Dateien ordnungsgemäß bereitzustellen, verschob ich sie alle in einen Ordner namens `public`. Ich richtete ein entferntes Heroku-Repository ein und schob den Code hoch.
git push heroku master
Anpassen der Backend-Struktur
Sobald alles andere funktionierte, musste ich mein Setup etwas ändern, da ich die Funktion `getProfileInfo` sowohl zum Aktualisieren bestehender Profile als auch zum Schreiben komplett neuer Profile für über die Website eingereichte Designer verwenden wollte.
Ich habe meine app.js-Datei verworfen und die Funktion `getProfileInfo` als Modul-Export namens `getprofileinfo.js<code>` gespeichert, um sie in zwei neu erstellten Skripten zu verwenden: `display.js` und submit.js. Dann importierte ich das Modul oben in das display-Skript und verwendete das Heroku-Scheduler-Add-on, um es alle 24 Stunden auszuführen, um die Daten bestehender Profile auf Firebase zu aktualisieren.
var getProfileInfo = require('./getprofileinfo.js');
function getDisplayedDesigners() {
firebase.database().ref('display/').on('value', function (results) {
var allDisplayedDesigners = results.val();
for (var designer in allDisplayedDesigners) {
getProfileInfo(designer);
};
});
}
getDisplayedDesigners();

Das submit-Skript war etwas anders. Ich wollte die Einreichungen manuell prüfen, um Trolleinreichungen oder unzulässige Personen zu entfernen, dann automatisch die verbleibenden Einreichungen zum display-Objekt hinzufügen und sie aus dem submit-Objekt entfernen. Ich musste auch Personen berücksichtigen, die möglicherweise das @-Symbol im Handle-Feld des Formulars bei der Einreichung angaben.
var getProfileInfo = require('./getprofileinfo.js');
function getSubmittedDesigners() {
firebase.database().ref('submissions/').on('value', function (results) {
var allSubmittedDesigners = results.val();
for (var designer in allSubmittedDesigners) {
var handle = designer;
if (handle.includes("@") === true) {
handle = handle.replace("@", "");
getProfileInfo(handle);
firebase.database().ref('submissions/' + "@" + handle).remove();
} else {
getProfileInfo(handle);
firebase.database().ref('submissions/' + handle).remove();
};
};
});
}
getSubmittedDesigners();
Mit diesem Setup konnte ich das submit.js in der Kommandozeile ausführen und alle zulässigen Einreichungen auf einmal verarbeiten.
node submit.js
Launch!
Am 15. Mai startete ich offiziell meine allererste App: Women Who Design. In den ersten 24 Stunden verzeichnete sie 15.000 Besucher und 1.000 neue Nominierungen, was ziemlich aufregend war. Mit einer solchen Resonanz hatte ich jedoch nicht gerechnet, daher arbeite ich jetzt an einigen wichtigen Frontend- und Performance-Upgrades, um den Website-Traffic und das Profilvolumen zu bewältigen. In der Zwischenzeit freue ich mich, dass die Leute die Website nutzen, um unglaublich talentierte Frauen in der Designbranche zu finden und zu nominieren. Bleiben Sie dran!
Danke fürs Aufschreiben! Ich kann es kaum erwarten zu sehen, was als nächstes kommt.
Jules, das ist ein wirklich lausiger Kommentar, aber großartige Arbeit an diesem Beitrag. Ich experimentiere gerade mit dem Einrichten der Grundlagen einer Progressive Web App mit dieser Art von Dingern, und die Schritte und Details, die du durchlaufen hast, haben mich erkennen lassen, dass ich die Dinge komplett falsch angegangen bin.
Ich würde gerne einen Nachfolgeartikel über die Verbesserungen auf der Performance-Seite sehen.
Ich würde auch gerne sehen, welche Performance-Verbesserungen Sie vorgenommen haben. Danke, dass Sie sich die Zeit für die großartige Ausarbeitung genommen haben!
Das ist einfach großartig, tolle Arbeit Jules! Das zeigt wirklich, wie einfach es ist, selbst eine Web-App zu bauen.
Manchmal (meistens?) sind die einfachsten Ideen die besten! Es war eine fantastische Idee und gut umgesetzt, danke für die ausführliche Beschreibung!
Hallo Jules, vielen Dank fürs Schreiben. Ich habe deinem Tutorial gefolgt (zugegebenermaßen nicht ganz) und endlich meine erste Node-App mit Firebase erstellt!
Dies ist eine unterlegene, aber inspirierte Version der von dir erstellten Website: https://designers-who-code.herokuapp.com/
Danke dafür!
Ich habe nur eine Frage zu Autolinker: Ich bin auf einen seltsamen Bug gestoßen, bei dem die Erwähnungs-/Hashtag-Links nicht mehr automatisch verlinkt werden, wenn ich die `replaceFn`-Methode hinzufüge. Seltsam, oder?
Wow, das sieht großartig aus, Andric. Schön, dass du das Tutorial hilfreich fandest. Schau dir nochmal den Abschnitt zu Autolinker an, er wurde aktualisiert :)
Ich hoffe, das behebt das Problem!
Danke für diese großartige Zusammenfassung, Jules. Es hat mir wirklich geholfen, viele Zusammenhänge zu verstehen!