gae上のアプリに対してService Workerを登録する

やりたいこと

gae上で動いているアプリに対してService Workerを登録します。

やること

  • service worker用のjsファイル sw.js を登録すること。
  • sw.js ファイルのレスポンスヘッダーに Service-Worker-Allowed:/ を登録すること。

手順

1. sw.jsを読み込むには、app.yaml にてstaticファイル以下に配置した sw.js を読み込むようにします。

    - url: /sw.js
      static_files: static/js/pathTo/sw.js
      upload: static/js/pathTo/sw.js
      expiration: 0m

参考:

stackoverflow.com

2. Service-Worker-Allowed: /sw.js のレスポンスヘッダーに付与します。

// r *http.Request
r.Request.Header.Set("Service-Worker-Allowed", "/")

これを static/ 配下のroutingにセットします。

ちなみにFWのginなら以下のように書けます。

// main.go
r := gin.New()
g := r.Group("/static")
g.Use(func(c *gin.Context) {
    c.Header("Service-Worker-Allowed", "/")
    c.Next()
})
g.Static("", "./static")

これでgae上でservice workerを使うための sw.js を登録することが出来ます。

追記

20180307 GAE上で動かしているアプリ上にservice-workerを登録するためのjsを仕込むには、service-worker登録用のjsをどうサーブするのかを考えないと行けないのですが、以下の場合に置いてGAE上のデプロイで sw.js が404になってしまうという事象に遭遇しました。

  1. app.yamlファイルでsw.jsをサーブするpathを指定する。上記例では/sw.js としているところ。
  2. routingでstaticでファイルをサーブする箇所を指定して Service-Worker-Allowed ヘッダーを登録する。

skip_files.yamlやignore設定してない状態で sw.js が上がるものかと思っていましたが、サーブ元が同じファイルをyamlとroutingの2箇所で同じ指定をしているとそのファイルにアクセスできませんでした。(多分上がっていない?)

これを解決するために

  • routingでのstaticファイルのサーブ先の指定されている場合は、yamlファイルでのstaticファイルのサーブ先の指定しない。(yamlの設定はいらない)

ということがわかりました。正常に sw.js がアップロードされました。

参考にしたstackoverflowのエントリーはroutingでstaticファイルをサーブするなどを考慮していない純粋なGAEでのstatic file のサーブ方法だったので問題なかったですが、プロジェクトによってはstaticファイルのサーブをroutingで管理している場合もあると思うので、プロジェクトの状況に応じてsw.jsのサーブ方法を修正したほうが良いかもしれません。

サンプルコードには上げてしまったのですが、gin上で g.Static("", "./static") でroutingにおいてstaticファイルのサーブ先を指定している場合は、yamlファイルでの設定は特にしなくても良く、ヘッダー情報だけ追加してService-Workerが動くpathを制限すればよかったということがわかりました。

routingでstaticファイルのサーブをしている場合と、yamlで指定する場合において、何かしら競合が起きると正常に動作しないということがわかりました。

おまけ

ginでのstaticファイルの指定方法について深掘りしてみました。

ginのStaicファイルの登録メソッドの定義は以下。

// Static serves files from the given file system root.
// Internally a http.FileServer is used, therefore http.NotFound is used instead
// of the Router's NotFound handler.
// To use the operating system's file system implementation,
// use :
//     router.Static("/static", "/var/www")
func (group *RouterGroup) Static(relativePath, root string) IRoutes {
    return group.StaticFS(relativePath, Dir(root, false))
}
  • relativePath: 相対パス上記コードではGroupで /static を指定してしまっているので特に意識しなくていい
  • Dir(root, false): ProjectRootのディレクトリから対象ディレクトリまでの絶対パスを指定する。trueにすると http.Dir() と同じ動作をします。

以下のコードを見るとわかるけど true のときは File, error を返すようになります。 falseのときは File のみ返します。

// @gin/fs

type (
    onlyfilesFS struct {
        fs http.FileSystem
    }
    neuteredReaddirFile struct {
        http.File
    }
)

// Dir returns a http.Filesystem that can be used by http.FileServer(). It is used interally
// in router.Static().
// if listDirectory == true, then it works the same as http.Dir() otherwise it returns
// a filesystem that prevents http.FileServer() to list the directory files.
func Dir(root string, listDirectory bool) http.FileSystem {
    fs := http.Dir(root)
    if listDirectory {
        return fs
    }
    return &onlyfilesFS{fs}
}
// @http/fs

// A Dir implements FileSystem using the native file system restricted to a
// specific directory tree.
//
// While the FileSystem.Open method takes '/'-separated paths, a Dir's string
// value is a filename on the native file system, not a URL, so it is separated
// by filepath.Separator, which isn't necessarily '/'.
//
// An empty Dir is treated as ".".
type Dir string

func (d Dir) Open(name string) (File, error) {
    if filepath.Separator != '/' && strings.IndexRune(name, filepath.Separator) >= 0 ||
        strings.Contains(name, "\x00") {
        return nil, errors.New("http: invalid character in file path")
    }
    dir := string(d)
    if dir == "" {
        dir = "."
    }
    f, err := os.Open(filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name))))
    if err != nil {
        return nil, err
    }
    return f, nil
}

refs:

http - The Go Programming Language