佛山网站设计:避免自动内联代码的陷阱

2019.08.13 mf_web

86

内联是直接在HTML文档中包含文件内容的过程:CSS文件可以在style元素内部内联,JavaScript文件可以内联在script元素中:

<style>/* CSS contents here */</style><script>/* JS contents here */</script>

通过打印HTML输出中已有的代码,内联可避免渲染阻塞请求,并在呈现页面之前执行代码。因此,它有助于提高站点的感知性能(即页面变得可用的时间。)例如,我们可以使用在加载站点(大约14kb)时立即传送的数据缓冲区来内联在关键的风格,包括款式上面屏内容(如已经在先前的碎杂志网站完成),以及字体大小和布局的宽度和高度,以避免跳跃的布局重新绘制时数据的其余部分被输送。

但是,当过度使用时,内联代码也会对站点的性能产生负面影响:由于代码不可缓存,因此会反复向客户端发送相同的内容,并且无法通过Service Workers预缓存,或者从内容交付网络缓存和访问。此外,在实施内容安全策略(CSP)时,内联脚本被认为是不安全的。然后,它是一个明智的策略,内联CSS和JS的关键部分,使网站加载速度更快,但尽可能避免。

佛山网站设计为了避免内联,在本文中我们将探索如何将内联代码转换为静态资源:我们将其保存到磁盘(有效创建静态文件)并添加相应的<script>或<link>标记,而不是在HTML输出中打印代码加载文件。

让我们开始吧!

何时避免内联

如果某些代码必须内联,则没有神奇的方法可以确定,但是,当一些代码不能被内联时,它可能非常明显:当它涉及大量代码时,以及何时不需要它。

例如,WordPress网站内嵌JavaScript模板以呈现媒体管理器(可在媒体库页面下访问/wp-admin/upload.php),打印大量代码:

媒体库页面源代码的屏幕截图
WordPress媒体管理器内嵌的JavaScript模板。

占用一个完整的43kb,这段代码的大小是不可忽略的,因为它位于页面的底部,所以不需要立即。因此,通过静态资产代替服务或在HTML输出中打印代码会很有意义。

接下来让我们看看如何将内联代码转换为静态资产。

触发静态文件的创建

如果内容(要内联的内容)来自静态文件,那么除了简单地请求静态文件而不是内联代码之外,没有什么可做的。

但是,对于动态代码,我们必须计划如何/何时生成包含其内容的静态文件。例如,如果站点提供配置选项(例如更改颜色方案或背景图像),何时应生成包含新值的文件?我们有以下机会从动态代码创建静态文件:

  1. 根据要求
    当用户第一次访问内容时。

  2. 在更改
    时动态代码的源(例如配置值)已更改。

我们先请求考虑。第一次用户访问该站点时,我们说/index.html静态文件(例如header-colors.css)尚不存在,因此必须生成它。事件的顺序如下:

  1. 用户要求/index.html;

  2. 处理请求时,服务器会检查文件是否header-colors.css存在。由于它没有,它获取源代码并在磁盘上生成文件;

  3. 它返回对客户端的响应,包括标记 <link rel="stylesheet" type="text/css" href="/staticfiles/header-colors.css">

  4. 浏览器获取页面中包含的所有资源,包括header-colors.css;

  5. 到那时这个文件存在,所以它被提供。

然而,事件的顺序也可能不同,导致结果不令人满意。例如:

  1. 用户要求/index.html;

  2. 此文件已由浏览器(或某些其他代理或通过Service Workers)缓存,因此请求永远不会发送到服务器;

  3. 浏览器获取页面中包含的所有资源,包括header-colors.css。但是,此图像未在浏览器中缓存,因此请求将发送到服务器;

  4. 服务器尚未生成header-colors.css(例如,它刚刚重新启动);

  5. 它将返回404。

或者,我们可以header-colors.css在请求/index.html时生成,但在请求时/header-colors.css自己生成。但是,由于此文件最初不存在,请求已被视为404.即使我们可以破解它,改变标头以将状态代码更改为200,并返回图像的内容,这是一种可怕的做事方式,所以我们不会接受这种可能性(我们要比这更好!)

这只留下一个选项:在源文件发生变化后生成静态文件。

源更改时创建静态文件

请注意,我们可以从依赖于用户和依赖于站点的源创建动态代码。例如,如果主题允许更改站点的背景图像,并且该选项由站点的管理员配置,则可以在部署过程中生成静态文件。另一方面,如果站点允许其用户更改其配置文件的背景图像,则必须在运行时生成静态文件。

简而言之,我们有以下两种情况:

  1. 用户配置
    用户更新配置时必须触发该过程。

  2. 站点配置
    当管理员更新站点的配置时,或在部署站点之前,必须触发该过程。

如果我们独立考虑这两种情况,对于#2,我们可以在我们想要的任何技术堆栈上设计流程。但是,我们不希望实施两种不同的解决方案,而是一种可以解决这两种情况的独特解决方案。并且因为从#1生成静态文件的过程必须在运行的站点上触发,然后围绕该站点运行的相同技术堆栈设计该过程是令人信服的。

在设计流程时,我们的代码需要处理#1和#2的具体情况:

  • 版本控制
    必须使用“version”参数访问静态文件,以便在创建新静态文件时使先前文件无效。虽然#2可能只是与站点具有相同的版本,但#1需要为每个用户使用动态版本,可能保存在数据库中。

  • 生成的文件
    #2的位置为整个站点生成唯一的静态文件(例如/staticfiles/header-colors.css),而#1为每个用户创建静态文件(例如/staticfiles/users/leo/header-colors.css)。

  • 触发事件
    对于#1,静态文件必须在运行时执行,对于#2,它也可以作为我们的暂存环境中的构建过程的一部分执行。

  • 部署和分发
    #2中的静态文件可以无缝集成到站点的部署包中,不会带来任何挑战; 但是,#1中的静态文件不能,因此进程必须处理其他问题,例如负载均衡器后面的多个服务器(静态文件只能在1个服务器中创建,还是在所有服务器中创建,以及如何?)。

让我们接下来设计并实施该流程。对于要生成的每个静态文件,我们必须创建一个包含文件元数据的对象,从动态源计算其内容,最后将静态文件保存到磁盘。作为指导下面解释的用例,我们将生成以下静态文件:

  1. header-colors.css,使用保存在数据库中的值的某些样式

  2. welcomeuser-data.js,包含一个JSON对象,其中包含一些变量下的用户数据:window.welcomeUserData = {name: "Leo"};。

下面,我将描述为WordPress生成静态文件的过程,我们必须将该堆栈基于PHP和WordPress函数。可以通过加载执行短代码的特殊页面来触发在部署之前生成静态文件的功能,[create_static_files]如我在前一篇文章中所述。

进一步推荐阅读:制作服务工作者:案例研究

将文件表示为对象

我们必须将文件建模为具有所有相应属性的PHP对象,因此我们可以将文件保存在特定位置的磁盘上(例如,在/staticfiles/或下/staticfiles/users/leo/),并知道如何请求文件。为此,我们创建一个接口,Resource返回文件的元数据(文件名,目录,类型:“css”或“js”,版本和其他资源的依赖关系)及其内容。

interface Resource {
  function get_filename();
  function get_dir();
  function get_type();
  function get_version();
  function get_dependencies();
  function get_content();}

为了使代码可维护和可重用,我们遵循SOLID原则,为此我们为资源设置了一个对象继承方案,以逐步添加属性,从ResourceBase我们所有资源实现将继承的抽象类开始:

abstract class ResourceBase implements Resource {
  function get_dependencies() {
    // By default, a file has no dependencies
    return array();
  }}

在SOLID之后,只要属性不同,我们就会创建子类。如前所述,生成的静态文件的位置以及请求它的版本控制将根据有关用户或站点配置的文件而有所不同:

abstract class UserResourceBase extends ResourceBase {
  function get_dir() {
    // A different file and folder for each user
    $user = wp_get_current_user();
    return "/staticfiles/users/{$user->user_login}/";
  }
  function get_version() {
    // Save the resource version for the user under her meta data. 
    // When the file is regenerated, must execute `update_user_meta` to increase the version number
    $user_id = get_current_user_id();
    $meta_key = "resource_version_".$this->get_filename();
    return get_user_meta($user_id, $meta_key, true);
  }}abstract class SiteResourceBase extends ResourceBase {
  function get_dir() {
    // All files are placed in the same folder
    return "/staticfiles/";
  }
  function get_version() {
    // Same versioning as the site, assumed defined under a constant
    return SITE_VERSION;
  }}

最后,在最后一级,我们为我们想要生成的文件实现对象,通过函数添加文件名,文件类型和动态代码get_content:

class HeaderColorsSiteResource extends SiteResourceBase {
  function get_filename() {
    return "header-colors";
  }
  function get_type() {
    return "css";
  }
  function get_content() {
    return sprintf(
      "
        .site-title a {
          color: #%s;
        }
      ", esc_attr(get_header_textcolor())
    );
  }}class WelcomeUserDataUserResource extends UserResourceBase {
  function get_filename() {
    return "welcomeuser-data";
  }
  function get_type() {
    return "js";
  }
  function get_content() {
    $user = wp_get_current_user();
    return sprintf(
      "window.welcomeUserData = %s;",
      json_encode(
        array(
          "name" => $user->display_name        )
      )
    );
  }}

有了这个,我们将文件建模为PHP对象。接下来,我们需要将其保存到磁盘。

将静态文件保存到磁盘

通过语言提供的本机功能可以轻松地将文件保存到磁盘。在PHP的情况下,这是通过该功能完成的fwrite。另外,我们创建了一个实用程序类,ResourceUtils其中的函数提供了磁盘上文件的绝对路径,以及它相对于站点根目录的路径:

class ResourceUtils {
  protected static function get_file_relative_path($fileObject) {
    return $fileObject->get_dir().$fileObject->get_filename().".".$fileObject->get_type();
  }
  static function get_file_path($fileObject) {
    // Notice that we must add constant WP_CONTENT_DIR to make the path absolute when saving the file
    return WP_CONTENT_DIR.self::get_file_relative_path($fileObject);
  }}class ResourceGenerator {
  static function save($fileObject) {
    $file_path = ResourceUtils::get_file_path($fileObject);
    $handle = fopen($file_path, "wb");
    $numbytes = fwrite($handle, $fileObject->get_content());
    fclose($handle);
  }}

然后,每当源更改并且需要重新生成静态文件时,我们执行ResourceGenerator::save将表示该文件的对象作为参数传递。下面的代码重新生成并保存在磁盘上的文件“header-colors.css”和“welcomeuser-data.js”:

// When need to regenerate header-colors.css, execute:ResourceGenerator::save(new HeaderColorsSiteResource());// When need to regenerate welcomeuser-data.js, execute:ResourceGenerator::save(new WelcomeUserDataUserResource());

一旦它们存在,我们就可以将通过<script>和<link>标签加载的文件排入队列。

排队静态文件

排队静态文件与在WordPress中排队任何资源没什么不同:通过函数wp_enqueue_script和wp_enqueue_style。然后,我们简单地迭代所有对象实例并使用一个钩子或另一个钩子,这取决于它们的get_type()值是"js"或者"css"。

我们首先添加实用程序函数来提供文件的URL,并告诉类型为JS或CSS:

class ResourceUtils {
  // Continued from above...
  static function get_file_url($fileObject) {
    // Add the site URL before the file path
    return get_site_url().self::get_file_relative_path($fileObject);
  }
  static function is_css($fileObject) {
    return $fileObject->get_type() == "css";
  }
  static function is_js($fileObject) {
    return $fileObject->get_type() == "js";
  }}

类的实例ResourceEnqueuer将包含必须加载的所有文件; 当调用时,它的功能enqueue_scripts和enqueue_styles将做入队,通过执行相应的WordPress函数(wp_enqueue_script和wp_enqueue_style分别地):

class ResourceEnqueuer {
  protected $fileObjects;
  function __construct($fileObjects) {
    $this->fileObjects = $fileObjects;
  }
  protected function get_file_properties($fileObject) {
    $handle = $fileObject->get_filename();
    $url = ResourceUtils::get_file_url($fileObject);
    $dependencies = $fileObject->get_dependencies();
    $version = $fileObject->get_version();
    return array($handle, $url, $dependencies, $version);
  }
  function enqueue_scripts() {
    $jsFileObjects = array_map(array(ResourceUtils::class, 'is_js'), $this->fileObjects);
    foreach ($jsFileObjects as $fileObject) {
      list($handle, $url, $dependencies, $version) = $this->get_file_properties($fileObject);
      wp_register_script($handle, $url, $dependencies, $version);
      wp_enqueue_script($handle);
    }
  }
  function enqueue_styles() {
    $cssFileObjects = array_map(array(ResourceUtils::class, 'is_css'), $this->fileObjects);
    foreach ($cssFileObjects as $fileObject) {
      list($handle, $url, $dependencies, $version) = $this->get_file_properties($fileObject);
      wp_register_style($handle, $url, $dependencies, $version);
      wp_enqueue_style($handle);
    }
  }}

最后,我们ResourceEnqueuer使用表示每个文件的PHP对象列表来实例化一个类的对象,并添加一个WordPress钩子来执行入队:

// Initialize with the corresponding object instances for each file to enqueue$fileEnqueuer = new ResourceEnqueuer(
  array(
    new HeaderColorsSiteResource(),
    new WelcomeUserDataUserResource()
  ));// Add the WordPress hooks to enqueue the resourcesadd_action('wp_enqueue_scripts', array($fileEnqueuer, 'enqueue_scripts'));add_action('wp_print_styles', array($fileEnqueuer, 'enqueue_styles'));

就是这样:入队时,在客户端加载站点时会请求静态文件。我们已成功避免打印内联代码并加载静态资源。

接下来,我们可以应用多项改进来提高性能。

将文件捆绑在一起

即使HTTP / 2减少了捆绑文件的需求,它仍然使网站更快,因为文件的压缩(例如通过GZip)将更有效,并且因为浏览器(例如Chrome)具有更大的开销处理许多资源。

到目前为止,我们已将文件建模为PHP对象,这允许我们将此对象视为其他进程的输入。特别是,我们可以重复上述相同的过程,将同一类型的所有文件捆绑在一起,并提供捆绑版本而不是所有独立文件。为此,我们创建了一个函数get_content,它只是从每个资源中提取内容$fileObjects,然后再次打印,从而生成所有资源中所有内容的聚合:

abstract class SiteBundleBase extends SiteResourceBase {
  protected $fileObjects;
  function __construct($fileObjects) {
    $this->fileObjects = $fileObjects;
  }
  function get_content() {
    $content = "";
    foreach ($this->fileObjects as $fileObject) {
      $content .= $fileObject->get_content().PHP_EOL;
    }
    return $content;
  }}

我们可以bundled-styles.css通过为此文件创建一个类将所有文件捆绑到文件中:

class StylesSiteBundle extends SiteBundleBase {
  function get_filename() {
    return "bundled-styles";
  }
  function get_type() {
    return "css";
  }}

最后,我们像以前一样将这些捆绑文件排队,而不是所有独立资源。对于CSS,我们创建一个包含文件捆绑header-colors.css,background-image.css并font-sizes.css为我们简单的实例StylesSiteBundle与PHP对象为每个文件(同样地,我们可以创建JS包文件):

$fileObjects = array(
  // CSS
  new HeaderColorsSiteResource(),
  new BackgroundImageSiteResource(),
  new FontSizesSiteResource(),
  // JS
  new WelcomeUserDataUserResource(),
  new UserShoppingItemsUserResource());$cssFileObjects = array_map(array(ResourceUtils::class, 'is_css'), $fileObjects);$jsFileObjects = array_map(array(ResourceUtils::class, 'is_js'), $fileObjects);// Use this definition of $fileEnqueuer instead of the previous one$fileEnqueuer = new ResourceEnqueuer(
  array(
    new StylesSiteBundle($cssFileObjects),
    new ScriptsSiteBundle($jsFileObjects)
  ));

而已。现在我们将只请求一个JS文件和一个CSS文件而不是许多。

感知性能的最终改进涉及通过延迟加载那些不需要的资产来优先考虑资产。让我们接下来解决这个问题。

async/ deferJSs资源的属性

我们可以添加属性async,并defer在<script>标签,当JavaScript文件被下载,解析和执行的改变,为关键的JavaScript优先,推动家居尽可能晚的非关键,从而降低网站的明显的加载时间。

要实现此功能,遵循SOLID原则,我们应该创建一个包含函数和的新接口JSResource(继承自Resource)。然而,这将关闭最终支持这些属性的标签。因此,考虑到适应性,我们采用更开放的方法:我们只需在接口上添加一个通用方法,以便保持灵活性,以便为两者和标签添加任何属性(已存在或尚未发明):is_asyncis_defer<style>get_attributesResource<script><link>

interface Resource {
  // Continued from above...
  function get_attributes();}abstract class ResourceBase implements Resource {
  // Continued from above...
  function get_attributes() {
    // By default, no extra attributes
    return '';
  }}

WordPress没有提供一种简单的方法来为排队的资源添加额外的属性,所以我们以相当hacky的方式进行,添加一个钩子,通过函数替换标签内的字符串add_script_tag_attributes:

class ResourceEnqueuerUtils {
  protected static tag_attributes = array();
  static function add_tag_attributes($handle, $attributes) {
    self::tag_attributes[$handle] = $attributes;
  }
  static function add_script_tag_attributes($tag, $handle, $src) {
    if ($attributes = self::tag_attributes[$handle]) {
      $tag = str_replace(
        " src='${src}'>",
        " src='${src}' ".$attributes.">",
        $tag
      );
    }
    return $tag;
  }}// Initize by connecting to the WordPress hookadd_filter(
  'script_loader_tag', 
  array(ResourceEnqueuerUtils::class, 'add_script_tag_attributes'), 
  PHP_INT_MAX, 
  3);

我们在创建相应的对象实例时添加资源的属性:

abstract class ResourceBase implements Resource {
  // Continued from above...
  function __construct() {
    ResourceEnqueuerUtils::add_tag_attributes($this->get_filename(), $this->get_attributes());
  }}

最后,如果welcomeuser-data.js不需要立即执行资源,我们可以将其设置为defer:

class WelcomeUserDataUserResource extends UserResourceBase {
  // Continued from above...
  function get_attributes() {
    return "defer='defer'";
  }}

因为它是作为延迟加载的,所以稍后将加载脚本,提前用户可以与站点交互的时间点。关于性能提升,我们现在都准备好了!

在我们放松之前还有一个问题需要解决:当网站托管在多个服务器上时会发生什么?

处理负载均衡器后面的多个服务器

如果我们的站点托管在负载均衡器后面的多个站点上,并且重新生成了依赖于用户配置的文件,则处理该请求的服务器必须以某种方式将重新生成的静态文件上载到所有其他服务器; 否则,其他服务器将从那一刻开始提供该文件的陈旧版本。我们如何做到这一点?使服务器相互通信不仅复杂,而且可能最终证明不可行:如果站点在不同地区的数百台服务器上运行会发生什么?显然,这不是一种选择。

我提出的解决方案是添加一个间接级别:不是从站点URL请求静态文件,而是从云中的某个位置请求它们,例如从AWS S3存储桶请求。然后,在重新生成文件时,服务器将立即将新文件上载到S3并从那里提供服务。我之前的文章通过AWS S3在多个服务器之间共享数据中解释了此解决方案的实现。

结论

在本文中,我们认为内联JS和CSS代码并不总是理想的,因为必须将代码重复发送到客户端,如果代码量很大,则可能会对性能产生影响。作为一个例子,我们看到WordPress如何加载43kb的脚本来打印媒体管理器,它们是纯JavaScript模板,可以完美地作为静态资源加载。

因此,我们设计了一种通过将动态JS和CSS内联代码转换为静态资源来加快网站速度的方法,这可以增强多个级别的缓存(在客户端,服务工作者,CDN中),允许进一步将所有文件捆绑在一起成只有一个JS / CSS资源,以压缩所述输出(如通过GZip压缩)时改善的比率和避免同时处理多个资源(例如,在浏览器)浏览器中的开销,并且还允许添加属性async或defer到<script>标记以加快用户交互性,从而改善网站的明显加载时间。

作为一个有益的副作用,将代码拆分为静态资源也可以使代码更加清晰,处理代码单元而不是大量的HTML,这可以更好地维护项目。

我们开发的解决方案是在PHP中完成的,包括WordPress的一些特定代码,但是,代码本身非常简单,几乎没有几个接口定义属性和按照SOLID原则实现这些属性的对象,以及一个保存的功能文件到磁盘。这就是它。最终结果是干净,紧凑,可以直接重新创建任何其他语言和平台,并且不难引入现有项目 - 提供轻松的性能提升。

佛山网站设计

最新案例

寒枫总监

来电咨询

400-6065-301

微信咨询

寒枫总监

TOP