Browse Source

caching and gzipping

master
mort 8 years ago
parent
commit
6f061dbc38
6 changed files with 91 additions and 55 deletions
  1. 1
    0
      .gitignore
  2. 2
    1
      conf.json.example
  3. 40
    7
      lib/context.js
  4. 7
    4
      lib/loader.js
  5. 37
    39
      server.js
  6. 4
    4
      templates/head.html

+ 1
- 0
.gitignore View File

@@ -3,3 +3,4 @@ node_modules
npm-debug.log
imgs
!imgs/.placeholder
.currentRun

+ 2
- 1
conf.json.example View File

@@ -24,5 +24,6 @@
"dir": {
"imgs": "imgs"
},
"debug": false
"debug": false,
"maxRuns": 9999
}

+ 40
- 7
lib/context.js View File

@@ -1,5 +1,6 @@
var formidable = require("formidable");
var crypto = require("crypto");
var zlib = require("zlib");
var preprocess = require("./preprocess.js");

var sessions = {};
@@ -8,7 +9,10 @@ function templatify(str, args, ctx, env) {
env.url = ctx.req.url;

str = preprocess(str, {
session: ctx.session,
session: function(key) {
ctx.cachable = false;
return ctx.session[key];
},
arg: function(key) {
return ctx.htmlEntities(args[key]);
},
@@ -22,6 +26,8 @@ function templatify(str, args, ctx, env) {
return str;
}

var cache = {};

module.exports = function(options) {
this.req = options.req;
this.res = options.res;
@@ -30,6 +36,11 @@ module.exports = function(options) {
this.db = options.db;
this.conf = options.conf;
this.query = this.req.url.split("?")[1] || "";
this.shouldGzip = /gzip/.test(this.req.headers["accept-encoding"]);

this.cachable = true;
this.statusCode = 200;
this.headers = {};
if (this.conf.debug)
this.startTime = new Date();

@@ -70,7 +81,7 @@ module.exports = function(options) {
}

module.exports.prototype = {
end: function(str) {
_end: function(str) {
if (this.conf.debug) {
var ms = (new Date().getTime() - this.startTime.getTime());
console.log(
@@ -82,17 +93,32 @@ module.exports.prototype = {
conole.log(this.req.url);
}

if (this.statusCode)
this.res.writeHead(this.statusCode);

this.res.writeHead(this.statusCode, this.headers);
this.res.end(str);
},

end: function(str, alreadyGzipped) {
if (!this.shouldGzip)
return this._end(str);

this.setHeader("Content-Encoding", "gzip");

if (alreadyGzipped)
return this._end(str);

zlib.gzip(str, function(err, res) {
if (err)
throw err;

this._end(res);
}.bind(this));
},

succeed: function(obj) {
obj = obj || {};
obj.success = true;

this.res.setHeader("Content-Type", "application/json");
this.setHeader("Content-Type", "application/json");

this.end(JSON.stringify(obj));
},
@@ -101,7 +127,7 @@ module.exports.prototype = {
console.log("Sending error to client:");
console.trace(err);

this.res.setHeader("Content-Type", "application/json");
this.setHeader("Content-Type", "application/json");

obj = {};
obj.success = false;
@@ -122,6 +148,9 @@ module.exports.prototype = {
if (!str)
throw new Error("No such view: "+name);

if (name === "404")
this.setStatus(404);

return templatify(str, args, this, {view: name});
},

@@ -149,6 +178,10 @@ module.exports.prototype = {
this.statusCode = code;
},

setHeader: function(key, val) {
this.headers[key] = val;
},

login: function(username, id) {
this.session.loggedIn = true;
this.session.username = username;

+ 7
- 4
lib/loader.js View File

@@ -1,4 +1,5 @@
var fs = require("fs");
var zlib = require("zlib");
var minify = require("./minify.js");
var includeHtml = require("./includeHtml.js");

@@ -22,18 +23,20 @@ exports.load = function(endpoints, conf) {
//If it doesn't end with .node.js, it's a regular text file and will
//just be served as is
} else {
res.endpoints[i] = fs.readFileSync(conf.webroot+"/"+ep, "utf8");
var str = fs.readFileSync(conf.webroot+"/"+ep, "utf8");

//If it's an HTML file, we minify it
if (!conf.minify) {
//Don't minify unless the conf tells us to
} else if (/\.html$/.test(ep)) {
res.endpoints[i] = minify.html(res.endpoints[i]);
str = minify.html(str);
} else if (/\.js$/.test(ep)) {
res.endpoints[i] = minify.js(res.endpoints[i]);
str = minify.js(str);
} else if (/\.css$/.test(ep)) {
res.endpoints[i] = minify.css(res.endpoints[i]);
str = minify.css(str);
}

res.endpoints[i] = str;
}

//Errors will usually be because an endpoint doesn't exist

+ 37
- 39
server.js View File

@@ -2,6 +2,7 @@ var http = require("http");
var https = require("https");
var fs = require("fs");
var domain = require("domain");
var zlib = require("zlib");
var loader = require("./lib/loader.js");
var pg = require("pg");
var Context = require("./lib/context.js");
@@ -53,10 +54,30 @@ var endpoints = {
"/api/account_change_password": "api/account_change_password.node.js"
}

//We cache static resources for a long time. However, we want to invalidate
//the browser's cache whenever a file updates. Therefore, we append
//a number to all static files, and the number increases every time we start
//the server. currentRun is that number.
var currentRun;
try {
currentRun = parseInt(fs.readFileSync(".currentRun", "utf8"));
} catch (err) {
if (err.code === "ENOENT")
currentRun = 0;
else
throw err;
}
currentRun = (currentRun >= conf.maxRuns ? 0 : currentRun);
currentRun = (currentRun || 0) + 1;
conf.web.currentRun = currentRun.toString();
fs.writeFileSync(".currentRun", currentRun, "utf8");

var loaded = loader.load(endpoints, conf);

var db = new pg.Client(conf.db);

var gzipCache = {};

//Function to run on each request
function onRequest(req, res) {
var ctx = new Context({
@@ -80,13 +101,27 @@ function onRequest(req, res) {
if (typeof ep == "function") {
ep(ctx);
} else {
ctx.end(ep);

//Cache content for a year
ctx.setHeader("Cache-Control", "public, max-age=31536000");

//Gzip and such
if (ctx.shouldGzip && gzipCache[req.url]) {
ctx.end(gzipCache[req.url], true);
} else if (ctx.shouldGzip) {
zlib.gzip(ep, function(err, res) {
gzipCache[req.url] = res;
ctx.end(res, true);
});
} else {
ctx.end(ep);
}
}
}

//Initiate a postgresql client
db.connect(function() {
//Create HTTP or HTTPS server
var server;
if (conf.use_https) {
@@ -109,40 +144,3 @@ if (!conf.debug) {
console.trace(err);
});
}

function command(tokens) {
switch(tokens[0]) {

//Reload configuration
case "reload-conf":
var c = JSON.parse(fs.readFileSync("conf.json"));
for (var i in c)
conf[i] = c[i];
break;

//Reload HTML
case "reload-html":
var l = loader.load(endpoints, conf);
for (var i in l)
loaded[i] = l[i];
break;

//Reload everything
case "reload":
command(["reload-conf"]);
command(["reload-html"]);
break;

default: return false;
}
return true;
}

process.stdin.on("data", function(line) {
var tokens = line.toString().split(/\s+/);
if (command(tokens)) {
return console.log(tokens[0]+" completed successfully.");
} else {
return console.log("Command not found: "+tokens[0]);
}
});

+ 4
- 4
templates/head.html View File

@@ -2,11 +2,11 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">

<link rel="stylesheet" href="/global.css">
<link rel="stylesheet" href="/global.css?{{conf#currentRun}}">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<link rel="stylesheet" href="/{{env#view}}/style.css">
<link rel="stylesheet" href="/{{env#view}}/style.css?{{conf#currentRun}}">

<script src="https://code.jquery.com/jquery-2.1.4.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
<script src="/global.js"></script>
<script src="/{{env#view}}/script.js"></script>
<script src="/global.js?{{conf#currentRun}}"></script>
<script src="/{{env#view}}/script.js?{{conf#currentRun}}"></script>

Loading…
Cancel
Save