如何渲染 Svelte 服务器端?

How to render Svelte Server-side?

我可以使用 Svelte 以某种方式生成“初始”HTML 文件吗?

我正在使用 Django、Webpack 和 Tailwindcss。我想在我的前端使用 Svelte,但我不想放弃使用服务器端渲染(Django 模板)带来的速度。如果我最初展示的是一个 bootstrap HTML 页面,它拉入了 bundle.js 并且 Svelte 在客户端构建了 DOM,那么浏览器只会在之后才开始下载图像JS 文件已加载。

将此与最初呈现的 HTML 已经包含图像链接的情况进行比较,浏览器开始将它们与 JS 一起下载,从而加快页面加载速度。

我不想使用 Sapper 作为我的应用服务器,我想继续使用 Django。

我觉得,没有多大意义。您可以为每个页面定义一个模板,它有一个静态的、预呈现的部分,然后是一个由 svelte 应用程序控制的动态内容区域。

但是所有导航都会加载一个新模板,它可能不再是单端应用程序。您可能会为不同的页面模板编写不同的 svelte 应用程序(rollup 可以做到)。

我认为,这样做会损失很多性能,在那种情况下,我宁愿选择 angular,然后选择 svelte(或 react 或 vue)。

挑战在于在 Django 应用程序和 Svelte 组件之间共享状态(道具)。

要从组件中获取 HTML 代码:

require('svelte/register')
const MyComponent = require('./MyComponent.svelte').default

const { html } = MyComponent.render({ ...props... })

如果组件没有属性,您可以编译和缓存 HTML 模板(甚至可以在运行前)。

如果您想动态发送道具,例如基于数据库中的数据,那么您需要在运行时这样做。这意味着在服务器端执行 JS。如果你缓存结果,性能不会差。

如果你不能缓存,那么使用 Django 来提高性能将被否定,因为无论如何你都会执行 Svelte,所以不妨使用 Svelte 来完成整个服务器端工作,然后使用 Django 作为后端服务器。

根据 this blog post 您执行以下说明:

In the following post I will show how to make use of server-side rendering in Svelte.

Most of the time you will probably run your Svelte code client-side, but there are scenarios where you can benefit from server-side Svelte rendering.

SEO In my opinion the primary use case for server-side rendering is SEO. Doing an initial rendering on the server will make your website much more crawler accessible. Crawlers typically support some JavaScript execution, but a complex JavaScript application is unlikely to be indexed correctly.

My experience is that applications that make ajax calls to fetch data are particularly challenging to index. The crawler might successfully render the initial static part of the application, but the data will be missing since the crawler won't make the necessary ajax calls.

Doing the initial rendering on the server allows crawlers to download the application as fully constructed html. There is no need to execute JavaScript on the client to compose the initial view since the view was already built on the server.

Server-side vs Client-side Technically you could build a Svelte application without any client side components, but it would be completely static. Basically the application would behave much like an old server-side PHP application. Great for crawlers, but real users typically expect a richer user experience.

This is where server-side meets client-side.

Once the server generated html is fully rendered in the browser, we can start a client-side counterpart of the application. The client-side version picks up where the server side application left off.

Both versions of the application may use the same Svelte components, but it's important to understand that they execute independently of each other. The server-side version does not know about the client-side version and vice versa.

It's also important to understand that there is no default state sharing between client and server (e.g. data property).

Article Carousel I decided to use server side Svelte to build an article carousel for the landing page of my blog. The idea is to use a Svelte component to cycle through articles in four of my article categories.

The carousel should load instantly on page load, so I decided to render the initial view on the server. Once the page has loaded I start the client-side counterpart of the Svelte component to dynamically cycle through the articles.

I am not known for css or design skills, so don't expect a pretty UI, but here's how I wired everything up.

I start by creating an Article component that will be used to render the articles in the carousel. The component should probably be split into two components, but keeping it as one for the purposes of this blog post.

    <div class="row">
      <span class="slide-show-card col-sm-3">
        <h4>Angular</h4>
        <h5><a class="slide-show-link" href="{{angularUrl}}">{{angularTitle}}</a></h5>
        <div class="label label-success slide-show-count">Viewed {{angular.viewCount}} times</div>
        <div>{{angularIntro}}</div>
      </span>
      <span class="slide-show-card col-sm-3">
        <h4>JavaScript</h4>
        <h5><a class="slide-show-link" href="{{javascriptUrl}}">{{javascriptTitle}}</a></h5>
        <div class="label label-success slide-show-count">Viewed {{javascript.viewCount}} times</div>
        <div>{{javascriptIntro}}</div>
      </span>
      <span class="slide-show-card col-sm-3">
        <h4>Svelte</h4>
        <h5><a class="slide-show-link" href="{{svelteUrl}}">{{svelteTitle}}</a></h5>
        <div class="label label-success slide-show-count">Viewed {{svelte.viewCount}} times</div>
        <div>{{svelteIntro}}</div>
      </span>
      <span class="slide-show-card col-sm-3">
        <h4>React</h4>
        <h5><a class="slide-show-link" href="{{reactUrl}}">{{reactTitle}}</a></h5>
        <div class="label label-success slide-show-count">Viewed {{react.viewCount}} times</div>
        <div>{{reactIntro}}</div>
      </span>
     </div>
    <script>
    
    class ArticleService {
      constructor() {
        this.articles = [];
        this.index = {
          'angular': -1,
          'javascript': -1,
          'svelte': -1,
          'react': -1
        };
      }
    
      getArticles() {
        return fetch('/full-article-list-json')
               .then((data) => {
                  return data.json();
               })
               .then((articles) => {
                  this.articles = articles;
                  return articles;
               });
      }
    
      getNextArticle(key) {
        this.index[key] = (this.index[key] + 1) % this.articles[key].length;
        let a = this.articles[key][this.index[key]];
        return a;
      }
     }
    
      let articleService = new ArticleService();
    
       export default {
         onrender() {
           let update = () => {
                       this.set({angular: articleService.getNextArticle('angular')});
                       this.set({javascript: articleService.getNextArticle('javascript')});
                       this.set({svelte: articleService.getNextArticle('svelte')}); 
                       this.set({react: articleService.getNextArticle('react')}); 
          };
    
          articleService.getArticles()
                        .then((articles) => {
                           update();
                           if(typeof global === 'undefined') {
                             document.querySelector('#articles').innerHTML = '';
                             document.querySelector('#articles-client-side').style.display =
"block";
                           }
                        })
                        .then(() => {
                           setInterval(() => {
                             articleService.getArticles()
                                           .then((articles) => {
                                              update();
                                           });  
                           }, 5000);  
                        });
        },
    
         data () {
           return {}
         },
    
         computed: {
           angularTitle: angular => angular.title || '',
           angularIntro: angular => angular.intro || '',
           angularUrl: angular => `viewarticle/${angular.friendlyUrl}`,
           
           javascriptTitle: javascript => javascript.title || '',
           javascriptIntro: javascript => javascript.intro || '',
           javascriptUrl: javascript => `viewarticle/${javascript.friendlyUrl}`,
     
           svelteTitle: svelte => svelte.title || '',
           svelteIntro: svelte => svelte.intro || '',
           svelteUrl: svelte => `viewarticle/${svelte.friendlyUrl}`,
     
           reactTitle: react => react.title || '',
           reactIntro: react => react.intro || '',
           reactUrl: react => `viewarticle/${react.friendlyUrl}`,
         }
      }
    </script>

Server Let's take a look at the server code.

The first thing we have to do is activate the compiler by requiring svelte/ssr/register

require( 'svelte/ssr/register' );

Next we have to require the component html file to get a handle to the component.

We then call the render method and pass it an initial data object. The data object is a standard Svelte data object.

app.get('/', function(req, res) {
      var component = require('./app/article-show/Articles.html');
      var articles = component.render({
                                  angular: articles.angular,
                                  svelte: articles.svelte,
                                  react: articles.react,
                                  javascript: articles.javascript
                              });
      res.render('index.html', {articles: articles});
});

After calling render we get back a fully rendered component. We now have to pass this to the node view engine.

In my case I am using Express with Mustache, so I can just pass the component as an object to the render method. Then in my index.html page I use Mustache view syntax with triple curly braces to render the component on the page like so.

{{{articles}}} Client What we have so far is enough to render the initial view of my component, but it won't support cycling through new articles every few seconds.

To achieve this we have to start up a client-side version of the Article component.

The client side version is loaded as a standard Svelte client-side component.

import articles from './articles';

var articlesSection = new articles({
  target: document.querySelector( 'main' ),
  data: {angular: {}, javascript: {}, svelte: {}, react: {}}
});

Once the client-side version is activated we will have two components in the DOM. As soon as the client-side version is ready to take over I wipe out the server-side version.

There might be a more elegant way to do this, but I simply clear out the server generated DOM element and flip a style on the client-side version.

Navigation In addition to the article carousel I also built my main navigation as a Svelte server side component. The nav is a pure server side component with no client side counterpart.

The navigation is largely static, but it supports dynamic styling of the active nav item. Here is the nav component code:

<div class="nav col-md-2 hidden-sm hidden-xs">
  <a class="navLink" href="/"><div class="navBox {{home}}">Home</div></a>
  <a class="navLink" href="/most-popular"><div class="navBox {{mostpopular}}">Most Popular</div></a>
  <a class="navLink" href="/most-recent"><div class="navBox {{mostrecent}}">Most Recent</div></a>
  <a class="navLink" href="/articleList/angular"><div class="navBox {{angular}}">Angular</div></a>
  <a class="navLink" href="/articleList/react"><div class="navBox {{react}}">React</div></a>
  <a class="navLink" href="/articleList/aurelia"><div class="navBox {{aurelia}}">Aurelia</div></a>
  <a class="navLink" href="/articleList/javascript"><div class="navBox {{javascript}}">JavaScript</div></a>
  <a class="navLink" href="/articleList/nodejs"><div class="navBox {{nodejs}}">NodeJS</div></a>
  <a class="navLink" href="/articleList/vue"><div class="navBox {{vue}}">Vue</div></a>
  <a class="navLink" href="/articleList/svelte"><div class="navBox {{svelte}}">Svelte</div></a>
  <a class="navLink" href="/articleList/mongodb"><div class="navBox {{mongodb}}">MongoDB</div></a>
  <a class="navLink" href="/articleList/unittesting"><div class="navBox {{unittesting}}">UnitTesting</div></a>
  <a class="navLink" href="/articleList/dotnet"><div class="navBox {{dotnet}}">.Net</div></a>
  <a class="navLink" href="/questions"><div class="navBox {{questions}}">Q&A</div></a>
  <a class="navLink" href="/full-article-list"><div class="navBox {{all}}">All</div></a>
</div>
<script>
  export default {
    data () {
      return {}
    },
  }
</script>

Because the nav is so static, there is no reason to regenerate the server side html for every request. As an optimization I have decided to cache the different variations of the nav. The only difference between the different versions of the nav is that the "active" nav items style is applied to different elements.

Here is some basic caching logic that ensures that we only generate each nav version once.

function getNavComponent(key) {
  key = key || 'home';
  key = key.replace('-', '');

  if(!navCache[key]) {
    navCache[key] = createNavComponent(key);
  }

  return navCache[key];
} 

function createNavComponent(key) {
  var nav = require('./app/article-show/Navigation.html');
  var data = {};
  data[key] = 'activeNav';
  return nav.render(data);
}

Demo If you want to test out my server-side vs client-side view you can find it here.

I also loaded my site in Google web master tools to compare a crawler's view of the component to a user's view

As you can tell from the screenshot my component looks pretty good to crawlers after adding server side rendering.

Left side is the crawler view and the right side is the user view.