OSDN Git Service

Plugin::JavaScriptLoader: fix encoding and gzipped content related problem
[newslash/newslash.git] / src / newslash_web / lib / Newslash / Plugin / JavaScriptLoader.pm
index 971a3ef..2196e12 100644 (file)
@@ -1,80 +1,42 @@
 package Newslash::Plugin::JavaScriptLoader;
-use Mojo::Base 'Mojolicious::Plugin';
+use Mojo::Base 'Newslash::Plugin::Preprocessor';
 
 use File::Spec;
-use File::Basename qw(fileparse);
-use Digest::MD5 qw(md5_hex);
-use IPC::Open3;
-use Symbol qw(gensym);
-use IO::Select;
-use POSIX ":sys_wait_h";
-use Mojo::Util qw(dumper);
+use File::Basename qw(basename);
 use Encode;
+use Mojo::JSON qw(decode_json encode_json);
+use List::Util qw(any);
+use Mojo::File qw(path);
 
-use constant KVS_PREFIX => "JSL:";
-use constant DEFAULT_MEM_EXPIRE => 60;
-use constant DEFAULT_KVS_EXPIRE => 60;
+use constant CACHE_KEY => "JavaScriptLoader";
+use constant JS_CONTENT_TYPE => "application/javascript";
 
-sub _kvs {
-    return shift->{app}->kvs;
-}
-
-sub _cache {
-    return shift->{app}->cache;
-}
-
-sub set_cache {
-    my ($self, $key, $value) = @_;
-    my $mem_expire = $self->{conf}->{mem_expire};
-    my $kvs_expire = $self->{conf}->{kvs_expire};
-    $mem_expire = DEFAULT_MEM_EXPIRE if !defined $mem_expire;
-    $kvs_expire = DEFAULT_KVS_EXPIRE if !defined $kvs_expire;
-
-    $self->_kvs->set(KVS_PREFIX . $key, $value, $kvs_expire);
-    $self->_cache->set(KVS_PREFIX . $key, $value, $mem_expire);
-}
+sub _generate_content {
+    my ($self, $pathname) = @_;
+    $pathname = $self->_strip_md5_from_path($pathname);
 
-sub get_cache {
-    my ($self, $key) = @_;
-    my $expire = $self->{conf}->{mem_expire};
-    $expire = DEFAULT_MEM_EXPIRE if !defined $expire;
-
-    my $value = $self->_cache->get(KVS_PREFIX . $key);
-    return $value if defined $value;
-
-    $value = $self->_kvs->get(KVS_PREFIX . $key);
-    if (defined $value) {
-        $self->_cache->set(KVS_PREFIX . $key, $value, $expire);
-        return $value;
+    my $js_content;
+    if ($self->{bundle_config}->{$pathname}) {
+        # target is bundle, so pack them
+        $js_content = $self->_pack_files($pathname);
     }
-
-    # no cache found
-    return;
-}
-
-sub get_content {
-    my ($self, $basename) = @_;
-    my $content = $self->get_cache($basename);
-    return $content if defined $content;
-
-    $content = $self->_load_js($basename);
-    if (defined $content) {
-        $self->set_cache($basename, $content);
-        $self->set_cache($content->{path}, $content);
-        return $content
+    else {
+        # target is single file, load it
+        $js_content = $self->_load_js($pathname);
     }
-    return;
-}
 
-sub cached_basedir {
-    return "/js_cache";
+    if (!defined $js_content) {
+        $self->{app}->log->error("JavaScriptLoader: generate contentfailed: $pathname");
+        return;
+    }
+    return $self->_build_content_object($js_content, $pathname, JS_CONTENT_TYPE);
 }
 
 sub _load_js {
-    my ($self, $basename) = @_;
+    my ($self, $rel_pathname) = @_;
 
     # check if file exists
-    my $abs_path = $self->_basename_to_absolute_path($basename);
+    my $abs_path = $self->_get_absolute_path($rel_pathname);
     return if (!-f $abs_path || !-r $abs_path);
 
     # load file
@@ -82,106 +44,189 @@ sub _load_js {
     my $js_body = do { local($/); <$fh> } ;
     close($fh);
 
-    my $md5;
-    if (utf8::is_utf8($js_body)) {
-        $md5 = md5_hex(Encode::encode('utf8', $js_body));
+    return $js_body;
+}
+
+sub _pack_files {
+    my ($self, $pathname) = @_;
+    $self->{app}->log->debug("JavaScriptLoader: packing to $pathname...");
+    my $base_dir = $self->_get_absolute_path($self->{conf}->{source_directory});
+
+    my $config = $self->{bundle_config}->{$pathname};
+    if (!$config) {
+        $self->{app}->log->error("JavaScriptLoader: no settings to pack $pathname...");
+        return;
     }
-    else {
-        $md5 = md5_hex($js_body);
+
+    my @result;
+    my %done;
+    for my $target (@{$config->{prior}}) {
+        my $path = path($base_dir, $target);
+        next if $done{$path->basename};
+        $self->{app}->log->debug("JavaScriptLoader: pack $target ($path)");
+        push @result, Encode::decode_utf8($path->slurp);
+        #my $js;
+        #my $fh = FileHandle->new;
+        #if ($fh->open($path, "<:utf8")) {
+        #    $js = do { local $/; <$fh> };
+        #    $fh->close;
+        #}
+        #else {
+        #    $self->{app}->log->error("JavaScriptLoader: cannot open $path");
+        #}
+        #push @result, $js;
+        $done{$target} = 1;
     }
-    my $path;
-    if ($self->{mode} eq "development") {
-        my $basedir = $self->{conf}->{source_directory};
-        $basedir =~ s/^public//g;
-        $path = "$basedir/$basename";
+
+    for my $target (path($base_dir)->list->each) {
+        my $basename = $target->basename;
+        next if $target->to_abs !~ $config->{target};
+        next if ( any { $target->to_abs =~ m/$_/ } $config->{exclude} );
+        next if $done{$basename};
+
+        $self->{app}->log->debug("JavaScriptLoader: pack $basename ($target)");
+        push @result, Encode::decode_utf8($target->slurp);
+        #my $js;
+        #my $fh = FileHandle->new;
+        #if ($fh->open($target, "<:utf8")) {
+        #    $js = do { local $/; <$fh> };
+        #    $fh->close;
+        #}
+        #else {
+        #    $self->{app}->log->error("JavaScriptLoader: cannot open $target");
+        #}
+        #push @result, $js;
+        $done{$basename} = 1;
     }
-    else {
-        my ($base, $dir, $ext) = fileparse($basename, qw/\.[^.]*/);
-        $dir = "" if $dir eq "./";
-        my $basedir = $self->cached_basedir;
-        $path = "$basedir/$dir${base}.$md5$ext";
+
+    my $joined = join("\n", @result);
+    $self->{app}->log->debug("JavaScriptLoader: packing to $pathname done.");
+
+    my @sources = keys %done;
+    $self->{bundled} ||= {};
+    for my $src (@sources) {
+        $self->{bundled}->{$src} = 1;
     }
 
-    my $result = { md5 => $md5,
-                   content => $js_body,
-                   path => $path,
-                   type => "text/javascript",
-                 };
-    return $result;
+    return $joined;
 }
 
-sub _basename_to_absolute_path {
-    my ($self, $basename) = @_;
-    my $base_dir = $self->{conf}->{base_directory};
-    if (!$base_dir) {
-        $base_dir = $self->{app}->home;
-    }
-    if (!$base_dir) { # fallback
-        $base_dir = "./";
+
+sub _load_bundle_config {
+    my ($self) = @_;
+
+    # load config file
+    if ($self->{conf}->{config_file}) {
+        my $abs_path = $self->{conf}->{config_file};
+        if ($abs_path !~ m|^/|) {
+            $abs_path = path($self->{app}->home, $abs_path)->to_abs;
+        }
+
+        # read bundle config
+        if (-r $abs_path) {
+            my $json_body = Encode::decode_utf8(path($abs_path)->slurp);
+            $self->{bundle_config} = decode_json($json_body);
+            #$self->load_all_bundles;
+        }
+        else {
+            $self->{app}->log->warn("JavaScriptLoader: cannot read config file: $self->{conf}->{config_file} ($abs_path)");
+        }
     }
-    my $src_dir = $self->{conf}->{source_directory};
-    my $path = File::Spec->catdir($base_dir, $src_dir, $basename);
-    $path = File::Spec->rel2abs($path);
-    return $path;
 }
 
-sub _split_md5 {
-    my ($self, $path) = @_;
-    if ($path =~ m/^(.+)\.([0-9a-f]+)(\.\w+)$/) {
-        return ($1 . $3, $2);
+sub load_all_bundles {
+    my $self = shift;
+    for my $target (keys %{$self->{bundle_config}}) {
+        my $content = $self->get_content($target, {reload => 1});
     }
-    return ($path, "");
+}
+
+sub _is_bundled {
+    my ($self, $target) = @_;
+    my $pool = $self->{bundled} // {};
+    return $pool->{$target};
 }
 
 sub register {
     my ($self, $app, $conf) = @_;
 
-    my $c = $app->config->{JavaScriptLoader};
-    if (!$c) {
-        $app->log->error("JavaScriptLoader: no config settings found!");
-        return;
-    }
+    # set default config value
+    my $cnf = $app->config->{JavaScriptLoader} ||= {};
+    $cnf->{source_directory} //= "public/js";
+    $cnf->{mode} //= "development";
+    $cnf->{config_file} //= "public/js/bundle-config.json";
+    $cnf->{use_bundle} //= 1;
+    $cnf->{use_compression} ||= 0;
 
-    $self->{conf} = {%$conf, %$c};
+    $self->{conf} = {%$conf, %$cnf};
     $self->{app} = $app;
     $self->{mode} = $self->{conf}->{mode} || $app->mode;
+    $self->{compress} = $cnf->{use_compression};
+    $self->{bundled} = {};
 
     if (!$self->{conf}->{source_directory}) {
         $app->log->warn("JavaScriptLoader: no source_directory given!");
     }
+    $self->_load_bundle_config;
 
-    if ($app->config->{TT2Renderer}) {
-        my $tt2r = $app->config->{TT2Renderer}->{self};
-        $tt2r->add_NS_function('load_js', sub {
-                                   my ($c, $name) = @_;
-                                   my $content = $self->get_content($name);
-                                   if ($content) {
-                                       return "<script src='$content->{path}'></script>";
-                                   }
-                                   $self->{app}->log->warn("JavaScriptLoader: cannot load $name");
-                                   return '';
-                               });
-    }
+    $app->helper('load_js', sub {
+                     my ($c, $name) = @_;
+                     # check if target is bundled
+                     return '' if $self->_is_bundled($name);
+
+                     my $path = $self->get_md5_path($name);
+                     if (!$path) {
+                         $self->{app}->log->error("JavaScriptLoader: cannot load $name");
+                         return '';
+                     }
+                     return "<script src='$path'></script>";
+                 });
 
     my $r = $app->routes;
-    $r->any('/js_cache/*content_path' => sub {
+    my $base_url = $self->{conf}->{base_url};
+    if (substr($base_url, -1, 1) ne "/") {
+        $base_url = $base_url . "/";
+    }
+
+    $r->any("${base_url}*content_path" => sub {
                 my $c = shift;
-                my $path = $c->stash('content_path');
+                my $path = $base_url . $c->stash('content_path');
+
                 if ($path) {
-                    my $content = $self->get_cache($path);
-                    if (!$content) {
-                        # no cache, so try to generate
-                        my ($basename, $md5) = $self->_split_md5($path);
-                        $content = $self->get_content($basename);
-
-                        # check md5
-                        if ($content && $content->{md5} ne $md5) {
-                            $content = "";
-                        }
-                    }
+                    my $content = $self->get_content($path);
                     if ($content) {
+                        # check Etag
+                        if ($c->req->headers->if_none_match
+                            && $content->{md5}
+                            && $c->req->headers->if_none_match eq "\"$content->{md5}\"") {
+                            $c->rendered(304);
+                            return;
+                        }
                         $c->res->headers->content_type($content->{type});
-                        $c->res->body(encode_utf8($content->{content}));
+                        my $output = $content->{content};
+
+                        # check compressable
+                        if ($self->{compress}
+                            && $content->{type} =~ m{^(text/|application/json|application/javascript)}
+                            && ($c->req->headers->accept_encoding // '') =~ /gzip/i
+                            && $content->{gz_content}
+                           ) {
+                            # use gzip compression
+                            # Add header
+                            $c->res->headers->append(Vary => 'Accept-Encoding');
+                            $c->res->headers->content_encoding('gzip');
+
+                            $output = $content->{gz_content};
+                        }
+
+                        # add Etag
+                        $c->res->headers->content_type($content->{type});
+                        if ($content->{md5}) {
+                            $c->res->headers->etag("\"$content->{md5}\"");
+                        }
+
+                        # send result
+                        $c->res->body($output);
                         $c->rendered(200);
                         return;
                     }
@@ -192,6 +237,4 @@ sub register {
     $app->helper(javascript_loader => sub { state $javascript_loader = $self; });
 }
 
-
-
 1;