在Jekyll博客里优雅地嵌入Github Gist

- hikerpig
#Jekyll#Github

Gist 是 Github 一个Snippet托管平台,也是全球秀代码和吵架的好地方。

例如我的一个虾米签到gist,官方提示的嵌入写法是这样的:

 <script src="https://gist.github.com/hikerpig/10013696.js"></script>

可以在markdown文档里直接插入这一句,jekyll把文章转成静态网页,用户打开后会加载。

不过这同步的script载入方式存在一点问题:

  1. 如果因为众所周知的某些时不时出现的“网络原因”导致此script载入失败,之后的文章内容都会停止加载的。这不,Github今天又撞墙了,以前在博客里贴的gist都挂掉了。
  2. 即便gist会加载成功,也有可能因为速度慢而阻碍完整文章的显示速度。

因此,我需要一种优雅地处理gist载入失败的策略。

如果你不存在第1点问题,那么只要给script标签加上一个异步加载的属性就行:

<script src="https://gist.github.com/hikerpig/10013696.js" async></script>

否则,就得多做点工作。

Jekyll内建模板支持

Jekyll文档里说明,使用Liquid的gist标签便可插入Github Gist内容。

在文章里需要使用的时候,用Liquid标签包裹起来:

{% raw %}
{% gist 10013696 %} 载入该gist id对应的代码片段
{% gist 10013696 xiami_casper.coffee %} 自定义gist显示的文件名
{% gist hikerpig/10013696 xiami_casper.coffee %}  私有gist
{% endraw %}

看看Jekyll源码里关于gist标签的实现, 发现它其实,就是帮我们减少了手写script标签的苦活。在html页面中加入script标签。

    def gist_script_tag(gist_id, filename = nil)
      if filename.empty?
        "<script src=\"https://gist.github.com/#{gist_id}.js\"> </script>"
      else
        "<script src=\"https://gist.github.com/#{gist_id}.js?file=#{filename}\"> </script>"
      end
    end

其实还是没法解决第1个问题.

Jekyll插件

这篇文章描述的插件扩展了Jekyll的gist标签。首先在plugins文件夹里添加gisttag.rb文件:

require 'cgi'
require 'digest/md5'
require 'net/https'
require 'uri'

module Jekyll
  class GistTag < Liquid::Tag
    def initialize(tag_name, text, token)
      super
      @text           = text
      @cache_disabled = false
      @cache_folder   = File.expand_path "../_gist_cache", File.dirname(__FILE__)
    end

    def render(context)
      if parts = @text.match(/([\d]*) (.*)/)
        gist, file = parts[1].strip, parts[2].strip
        script_url = script_url_for gist, file
        code       = get_cached_gist(gist, file) || get_gist_from_web(gist, file)
        html_output_for script_url, code
      else
        ""
      end
    end

    def html_output_for(script_url, code)
      code = CGI.escapeHTML code
      "<script src='#{script_url}'></script><noscript><pre><code>#{code}</code></pre></noscript>"
    end

    def script_url_for(gist_id, filename)
      "https://gist.github.com/#{gist_id}.js?file=#{filename}"
    end

    def get_gist_url_for(gist, file)
      "https://gist.github.com/raw/#{gist}/#{file}"
    end

    def cache(gist, file, data)
      cache_file = get_cache_file_for gist, file
      File.open(cache_file, "w") do |io|
        io.write data
      end
    end

    def get_cached_gist(gist, file)
      return nil if @cache_disabled
      cache_file = get_cache_file_for gist, file
      File.read cache_file if File.exist? cache_file
    end

    def get_cache_file_for(gist, file)
      bad_chars = /[^a-zA-Z0-9\-_.]/
      gist      = gist.gsub bad_chars, ''
      file      = file.gsub bad_chars, ''
      md5       = Digest::MD5.hexdigest "#{gist}-#{file}"
      File.join @cache_folder, "#{gist}-#{file}-#{md5}.cache"
    end

    def get_gist_from_web(gist, file)
      gist_url          = get_gist_url_for gist, file
      raw_uri           = URI.parse gist_url
      https             = Net::HTTP.new raw_uri.host, raw_uri.port
      https.use_ssl     = true
      https.verify_mode = OpenSSL::SSL::VERIFY_NONE
      request           = Net::HTTP::Get.new raw_uri.request_uri
      data              = https.request request
      data              = data.body
      cache gist, file, data unless @cache_disabled
      data
    end
  end

  class GistTagNoCache < GistTag
    def initialize(tag_name, text, token)
      super
      @cache_disabled = true
    end
  end
end

Liquid::Template.register_tag('gist', Jekyll::GistTag)
Liquid::Template.register_tag('gistnocache', Jekyll::GistTagNoCache)

在每一次jekyll build的时候都去gistcache文件夹检查gist id是否有对应的缓存内容,没有的话会下载并保存,且在页面内添加noscript标签显示gist全内容,这样一来使用不支持javascript的设备也能看得到gist内容。

JS前端实现法

JS界的合照狂人Ben Nadel大叔的Loading GitHub Gists After The Page Content Has Loaded采取了另一种方法:

静态文档中用一个placeholder填在gist应该出现的位置,使用jQuery.ajax读取gist内容,数据获取完毕以后再使用document.write写到文档里。如此的前端异步载入方式可以减少后台程序生成静态页面的大小。

其实还是没法解决第1个问题.

参考文章