2016-07-12 7 views
7

Mein Kunde verlangt eine funktionsreiche, clientseitig gerenderte Web-App, die gleichzeitig 100/100 in Google PageSpeed ​​Insights und rendert beim ersten Laden mit einem leeren Cache sehr schnell. Sie möchte die gleiche Seite sowohl als Web-App als auch als Zielseite nutzen und jede Suchmaschine mit guter SEO die gesamte Seite durchsuchen.Ergebnis 100 bei Google PageSpeed ​​Insights mit Meteor (dh einer Web-App-Zielseite)

Ist dies mit Meteor möglich? Wie kann es gemacht werden?

Antwort

23

Ja, dies ist möglich und einfach mit Meteor 1.3, ein paar zusätzliche Pakete und ein kleiner Hack.

Siehe bc-real-estate-math.com für ein Beispiel. (Diese Seite punktet nur 97, weil ich die Bilder nicht skaliert habe und Analytics und FB Tracking haben kurze Cache-Lebensdauern)

Traditionell war eine clientseitige gerenderte Plattform wie Meteor beim ersten Laden mit einem leeren Cache aufgrund der große Javascript-Nutzlast. Das serverseitige Rendering (mit React) der ersten Seite löst das fast, außer dass Meteor out-of-the-box kein asynchrones Javascript oder Inline-CSS unterstützt und somit Ihren ersten Rendervorgang verlangsamt und Ihren Google PageSpeed ​​Insights-Score zunichte macht Sie könnten über diese Metrik, es wirkt sich auf die AdWord-Preise meiner Kunden und damit optimieren ich für sie.

Dies ist, was Sie mit dieser Antwort des Setup erreichen können:

  • Sehr schnelle Time-to-first-Render auf leeren Cache, wie 500ms
  • Nein "Blitz von styled content"
  • Ergebnis 100/100 auf Google Pagespeed Insights
  • Die Nutzung von WebFont ohne Ihre page Speed ​​
  • Voll SEO Kontrolle punkten Tötung einschließlich Seitentitel und Meta-
  • Perfekte Integration mit Google Analytics und Facebook Pixel, die genau jede Seite Ansicht unabhängig von server- oder clientseitige Rendering
  • Google-Suchroboter und andere Crawler sehen alle Seiten echte HTML sofort ohne Ausführen von Skripts aufzeichnet
  • Griffe nahtlos #hash URLs Teile einer Seite
  • Verwenden eine kleine Anzahl (wie < 30) von Symbol Schrift Zeichen zu blättern, ohne Anfragen Hinzufügen oder verletzen Geschwindigkeit punkten
  • Maßstab bis zu jeder Größe von Javascript ohne impactin g Zielseitenerfahrung
  • Alle regulären awesomeness einer vollen Meteor web-app

Was dieses Setup kann nicht erreicht werden:

  • Große monolithische CSS-Frameworks startet Ihre Pagespeed Score zu töten und Verlangsamen Sie die Zeit bis zum ersten Rendern. Bootstrap ist ungefähr so ​​groß, wie Sie können, bevor Sie anfangen, Probleme zu sehen
  • Sie können einen Flash-of-Falsch-Schriftart nicht vermeiden und dennoch 100/100 PageSpeed ​​beibehalten. Das erste Rendern wird die websichere Schriftart des Clients sein, das zweite Rendern wird die Schriftart verwenden, die Sie zuvor zurückgestellt haben.

Wesentlichen, was passieren kann, ist,:

  • Kunde eine URL innerhalb Ihrer Website anfordert
  • Server sendet eine komplette HTML-Datei mit Inline-CSS, asynchrones Javascript zurück, und latente Fonts
  • Client fordert Bilder (wenn vorhanden) und Server sendet sie
  • Der Client kann nun die Seite rendern
  • Latente Schriftarten (falls vorhanden) ankommen und Seite könnte
  • Javascript Mutterschiff Nutzlast kommt in dem Hintergrund
  • Meteor bootet und Sie haben einen voll funktionsfähigen Web-App mit allen Glocken und Trillerpfeifen neu rendern und keine First-Load-Strafe
  • Solange Sie geben Sie dem Benutzer ein paar Zeilen Text zu lesen und ein schönes Bild zu Blick auf, werden sie nie den Übergang von statischen HTML Seite zu ausgewachsenen Web- App

Wie dies zu erreichen

I verwendet Meteor 1.3 und diese zusätzlichen Pakete:

  • reagieren
  • reagieren-dom
  • reagieren-Router
  • reagieren-Router-ssr
  • reagieren-helm
  • postcss
  • autoprefixer
  • Meteor-Knoten-Stubs

spielt Reagieren schön mit serverseitige Rendering, habe ich anderes Rendering-Engine nicht versucht. react-helmet wird verwendet, um einfach die jeder Seite sowohl Client- als auch Server-Seite hinzuzufügen und zu ändern (zB erforderlich, um den Titel jeder Seite festzulegen). Ich benutze den Autoprefixer, um alle herstellerspezifischen Präfixe zu meinem CSS/SASS hinzuzufügen, die für diese Übung sicherlich nicht benötigt werden.

Die meisten der Website ist dann ziemlich einfach nach den Beispielen in der react-Router, Reac-Router-ssr und Reaktion-Helm-Dokumentation. Weitere Informationen dazu finden Sie in den Dokumenten dieser Pakete.

Zunächst einmal eine sehr wichtige Datei, die in einem gemeinsamen Meteor-Verzeichnis (dh nicht in einem Server oder Client-Ordner) sein sollte. Dieser Code richtet das serverseitige React-Rendering, das <head>-Tag, Google Analytics, Facebook-Tracking ein und scrollt zu #hash-Ankern.

import { Meteor } from 'meteor/meteor'; 
import { ReactRouterSSR } from 'meteor/reactrouter:react-router-ssr'; 
import { Routes } from '../imports/startup/routes.jsx'; 
import Helmet from 'react-helmet'; 

ReactRouterSSR.Run(
    Routes, 
    { 
    props: { 
     onUpdate() { 
     hashLinkScroll(); 
     // Notify the page has been changed to Google Analytics 
     ga('send', 'pageview'); 
     }, 
     htmlHook(html) { 
     const head = Helmet.rewind(); 
     html = html.replace('<head>', '<head>' + head.title + head.base + head.meta + head.link + head.script); 
     return html;  } 
    } 
    }, 
    { 
    htmlHook(html){ 
     const head = Helmet.rewind(); 
     html = html.replace('<head>', '<head>' + head.title + head.base + head.meta + head.link + head.script); 
     return html; 
    }, 
    } 
); 

if(Meteor.isClient){ 
    // Google Analytics 
    (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 
    (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 
    m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 
    })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); 

    ga('create', 'UA-xxxxx-1', 'auto', {'allowLinker': true}); 
    ga('require', 'linker'); 
    ga('linker:autoLink', ['another-domain.com']); 
    ga('send', 'pageview'); 

    // Facebook tracking 
    !function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod? 
    n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n; 
    n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0; 
    t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window, 
    document,'script','https://connect.facebook.net/en_US/fbevents.js'); 

    fbq('init', 'xxxx'); 
    fbq('track', "PageView"); 
    fbq('trackCustom', 'LoggedOutPageView'); 
} 


function hashLinkScroll() { 
    const { hash } = window.location; 
    if (hash !== '') { 
    // Push onto callback queue so it runs after the DOM is updated, 
    // this is required when navigating from a different page so that 
    // the element is rendered on the page before trying to getElementById. 
    setTimeout(() => { 
     $('html, body').animate({ 
      scrollTop: $(hash).offset().top 
     }, 1000); 
    }, 100); 
    } 
} 

So werden die Routen eingerichtet. Beachten Sie die Titelattribute, die später an den Helm übergeben werden, um den Inhalt festzulegen.

import React from 'react'; 
import { Router, Route, IndexRoute, browserHistory } from 'react-router'; 

import App from '../ui/App.jsx'; 
import Homepage from '../ui/pages/Homepage.jsx'; 
import ExamTips from '../ui/pages/ExamTips.jsx'; 

export const Routes = (
    <Route path="/" component={App}> 
    <IndexRoute 
     displayTitle="BC Real Estate Math Online Course" 
     pageTitle="BC Real Estate Math Online Course" 
     isHomepage 
     component={Homepage} /> 
    <Route path="exam-preparation-and-tips"> 
     <Route 
     displayTitle="Top 3 Math Mistakes to Avoid on the UBC Real Estate Exam" 
     pageTitle="Top 3 Math Mistakes to Avoid on the UBC Real Estate Exam" 
     path="top-math-mistakes-to-avoid" 
     component={ExamTips} /> 
    </Route> 
); 

App.jsx --die äußere Anwendungskomponente. Beachten Sie das <Helmet>-Tag, das einige Meta-Tags und den Seitentitel basierend auf Attributen der bestimmten Seitenkomponente festlegt.

import React, { Component } from 'react'; 
import { Link } from 'react-router'; 
import Helmet from "react-helmet"; 

export default class App extends Component { 

    render() { 
    return (
     <div className="site-wrapper"> 
      <Helmet 
      title={this.props.children.props.route.pageTitle} 
      meta={[ 
       {name: 'viewport', content: 'width=device-width, initial-scale=1'}, 
      ]} 
      /> 

      <nav className="site-nav">... 

Ein Beispiel Seite Komponente:

import React, { Component } from 'react'; 
import { Link } from 'react-router'; 

export default class ExamTips extends Component { 
    render() { 
    return (
     <div className="exam-tips blog-post"> 
     <section className="intro"> 
      <p> 
      ... 

Wie latente Schriftarten hinzuzufügen.

Diese Schriftarten werden nach dem anfänglichen Rendern geladen und verzögern daher nicht die Zeit bis zum ersten Rendern. Ich glaube, das ist die einzige Möglichkeit, Webfonts zu verwenden, ohne den PageSpeed-Score zu reduzieren. Es führt jedoch zu einer kurzen Fehlzeichung. Setzen Sie dieses in einer Skriptdatei im Client enthalten:

WebFontConfig = { 
    google: { families: [ 'Open+Sans:400,300,300italic,400italic,700:latin' ] } 
}; 
(function() { 
    var wf = document.createElement('script'); 
    wf.src = 'https://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js'; 
    wf.type = 'text/javascript'; 
    wf.async = 'true'; 
    var s = document.getElementsByTagName('script')[0]; 
    s.parentNode.insertBefore(wf, s); 
})(); 

Wenn Sie einen exzellenten Service wie fontello.com und Hand-pick verwenden nur die Symbole, die Sie tatsächlich benötigen, können Sie sie in Ihre Inline <head> CSS einbinden und erhalten Symbole auf erst rendern, ohne auf eine große Schriftdatei zu warten.

Der Hack

, die fast genug, aber das Problem ist, dass unsere Skripte, CSS und Schriften synchron geladen werden, und die machen verlangsamt und unsere Page Speed ​​Score zu töten. Leider, soweit ich das beurteilen kann, unterstützt Meteor 1.3 offiziell keine Möglichkeit, CSS einzubinden oder den Skript-Tags das async-Attribut hinzuzufügen. Wir müssen ein paar Zeilen in 3 Dateien des Core-Boilerplate-Generator-Pakets hacken.

~/.meteor/packages/vorformulierten-Generator/.1.0.8.4n62e6 ++ o + web.browser + web.cordova/o/vorformulierten-generator.js

... 
Boilerplate.prototype._generateBoilerplateFromManifestAndSource = 
    function (manifest, boilerplateSource, options) { 
    var self = this; 
    // map to the identity by default 
    var urlMapper = options.urlMapper || _.identity; 
    var pathMapper = options.pathMapper || _.identity; 

    var boilerplateBaseData = { 
     css: [], 
     js: [], 
     head: '', 
     body: '', 
     meteorManifest: JSON.stringify(manifest), 
     jsAsyncAttr: Meteor.isProduction?'async':null, // <------------ !! 
    }; 

    .... 

     if (item.type === 'css' && item.where === 'client') { 
     if(Meteor.isProduction){ // <------------ !! 
      // Get the contents of aggregated and minified CSS files as a string 
      itemObj.inlineStyles = fs.readFileSync(pathMapper(item.path), "utf8");; 
      itemObj.inline = true; 
     } 
     boilerplateBaseData.css.push(itemObj); 
     } 
... 

~ /.meteor/packages/boilerplate-generator/.1.0.8.4n62e6++os+web.browser+web.cordova/os/packages/boilerplate-generator/boilerplate_web.browser.html

<html {{htmlAttributes}}> 
<head> 
    {{#each css}} 
    {{#if inline}} 
     <style>{{{inlineStyles}}}</style> 
    {{else}} 
     <link rel="stylesheet" type="text/css" class="__meteor-css__" href="{{../bundledJsCssUrlRewriteHook url}}"> 
    {{/if}} 
    {{/each}} 
    {{{head}}} 
    {{{dynamicHead}}} 
</head> 
<body> 
    {{{body}}} 
    {{{dynamicBody}}} 

    {{#if inlineScriptsAllowed}} 
    <script type='text/javascript'>__meteor_runtime_config__ = JSON.parse(decodeURIComponent({{meteorRuntimeConfig}}));</script> 
    {{else}} 
    <script {{../jsAsyncAttr}} type='text/javascript' src='{{rootUrlPathPrefix}}/meteor_runtime_config.js'></script> 
    {{/if}} 

    {{#each js}} 
    <script {{../jsAsyncAttr}} type="text/javascript" src="{{../bundledJsCssUrlRewriteHook url}}"></script> 
    {{/each}} 

    {{#each additionalStaticJs}} 
    {{#if ../inlineScriptsAllowed}} 
     <script type='text/javascript'> 
     {{contents}} 
     </script> 
    {{else}} 
     <script {{../jsAsyncAttr}} type='text/javascript' src='{{rootUrlPathPrefix}}{{pathname}}'></script> 
    {{/if}} 
    {{/each}} 
</body> 
</html> 

Jetzt zählen die Anzahl der Zeichen in diesen 2 Dateien, die Sie bearbeitet haben und eingeben die neuen Werte im Längenfeld dieser Einträge Dateien in ~/.meteor/packages/vorformulierten-Generator/.1.0.8.4n62e6 ++ o + web.browser + web.cordova/os.json

Löschen Sie dann den Ordner project/.meteor/local, um Meteor zu zwingen, das neue Kernpaket zu verwenden und Ihre App neu zu starten (das Warmladen funktioniert nicht). Sie werden nur die Änderungen im Produktionsmodus sehen.

Dies ist offensichtlich ein Hack und wird brechen, wenn Meteor aktualisiert. Ich hoffe, dass ich mit dem Posting und dem Interesse daran, auf einen besseren Weg hinarbeiten werde.

Dinge zu tun, wäre zu verbessern:

  • den Hack vermeiden.Erhalten Sie MDG offiziell async Skript und Inline- CSS auf flexible Weise
  • zulassen granulare Kontrolle darüber, welche CSS inline und die aufzuschieben
  • zulassen granulare Kontrolle über die JS asyn und die Synchronisierung und die Unterstützung Inline .
+0

Ein relevantes GitHub Problem für Meteor vorgeschlagenen Änderungen an den Textvorschlag Diskussion: https://github.com/meteor/meteor/pull/3860 – Noland

+0

Ich untersuche auch wenn dieses Paket den Hack vermeiden kann: https://atmospherejs.com/meteorhacks/inject-initial – Noland

+0

Hey @Noland - interessante Post. Können Sie mir sagen, wie behandeln Sie Ihre DDP-Daten wie in Abonnements in react-Router-ssr? – TJR

Verwandte Themen