Node-構(gòu)建web應(yīng)用2

數(shù)據(jù)上傳

單純的頭部報(bào)文無(wú)法攜帶大量的數(shù)據(jù),在業(yè)務(wù)中郭宝,我們往往需要接收一些數(shù)據(jù)柜与,比如表單提交、文件提交惹想、JSON上傳、XML上傳等饵婆。

如果請(qǐng)求中還帶有內(nèi)容部分(如POST請(qǐng)求勺馆,它具有報(bào)頭和內(nèi)容)戏售,內(nèi)容部分需要用戶自行接收和解析,通過報(bào)頭的Transfer-EncodingContent-Length即可判斷請(qǐng)求中是否帶有內(nèi)容名草穆。

var hasBody = function(req) {
  return 'transfer-encoding' in req.headers || 'content-length' in req.headers;
};

表單數(shù)據(jù)

默認(rèn)的表單提交灌灾,請(qǐng)求頭中的Content-Type字段值為application/x-www-form-urlencoded,由于它的報(bào)文體內(nèi)容跟查詢字符串相同foo=bar&baz=val悲柱,因此可以直接使用queryString解析:

if (req.headers['content-type'] === 'application/x-www-form-urlencoded') {
  req.body = querystring.parse(req.rawBody); 
}

其他格式

常見的提交還有JSON和XML文件等锋喜,判斷它們都是依據(jù)Content-Type字段,其中JSON類型的值為application/json豌鸡,XML的值為application/xml嘿般。需要注意的是,在Content-Type中可能還附帶如下所示的編碼信息涯冠。

Content-Type: application/json; charset=utf-8

使用JSON.parse()解析JSON炉奴,使用第三方庫(kù)解析XML

var xml2js = require('xml2js');

var handle = function (req, res) {
  if (mime(req) === 'application/xml') {
    xml2js.parseString(req.rawBody, function (err, xml) { 
      if (err) {
        // 異常內(nèi)容,響應(yīng)Bad request 
        res.writeHead(400); 
        res.end('Invalid XML'); 
        return;
      }
      req.body = xml; 
      todo(req, res);
    }); 
  } else if (mime(req) === 'application/json') {
    try {
      req.body = JSON.parse(req.rawBody);
    } catch (e) {
      // 異常內(nèi)容蛇更,響應(yīng)Bad request 
      res.writeHead(400); 
      res.end('Invalid JSON'); 
      return;
    } 
  }
};

附件上傳

在前端HTML代碼中瞻赶,特殊表單與普通表單的差異在于該表單中可以含有file類型的控件,以及需要指定表單屬性enctype為multipart/form-data派任。

瀏覽器在遇到multipart/form-data表單提交時(shí)砸逊,構(gòu)造的請(qǐng)求報(bào)文與普通報(bào)文完全不同,首先它的報(bào)頭中最為特殊的如下所示:

Content-Type: multipart/form-data; boundary=AaB03x 
Content-Length: 18231

它代表本次提交的內(nèi)容是多部分構(gòu)成的掌逛,其中boundary=AaB03x指定的是每部分內(nèi)容的分界符师逸,AaB03x是隨機(jī)生成的一段字符串,報(bào)文體的內(nèi)容將通過在它前面添加--進(jìn)行分割豆混,報(bào)文結(jié)束時(shí)在它前后都加上--標(biāo)識(shí)結(jié)束篓像,另外Content-Length的值必須確保是報(bào)文體的長(zhǎng)度。

數(shù)據(jù)上傳與安全

內(nèi)存限制

在解析表單崖叫、JSON和XML部分遗淳,我們采取的策略是先保存用戶提交的所有數(shù)據(jù),然后再解析處理心傀,最后才傳遞給業(yè)務(wù)邏輯屈暗。這種策略存在的問題就是,僅僅適合數(shù)據(jù)量小的提交請(qǐng)求脂男,如果攻擊者每次提交1MB的內(nèi)容养叛,那么內(nèi)存很快就會(huì)被占光。

解決這個(gè)問題有兩個(gè)方案:

1.限制上傳內(nèi)容的大小宰翅,一旦超過限制弃甥,停止接受數(shù)據(jù),并響應(yīng)400狀態(tài)碼

2.通過流式解析汁讼,將數(shù)據(jù)導(dǎo)向到磁盤中淆攻,Node只保留文件路徑等小數(shù)據(jù)阔墩。

// 限制上傳數(shù)據(jù)量
var bytes = 1024;
function (req, res) {
  var received = 0;
  var len = req.headers['content-length'] ? parseInt(req.headers['content-length'], 10) : null;
  // 內(nèi)容超過長(zhǎng)度限制,返回請(qǐng)求實(shí)體過長(zhǎng)的狀態(tài)碼 
  if (len && len > bytes) {
    res.writeHead(413); res.end();
    return;
  }
  // limit
  req.on('data', function (chunk) {
    // 內(nèi)容長(zhǎng)度累積超過限制瓶珊。
    received += chunk.length;
    if (received > bytes) {
      // 停止接收數(shù)據(jù)啸箫,觸發(fā)end()
      req.destroy(); 
    }
  });
  handle(req, res); 
};

CSRF: Cross-Site Request Forgery

通常而言,用戶通過瀏覽器訪問服務(wù)器端的Session ID是無(wú)法被第三方知道的伞芹,但是CSRF的攻擊者并不需要知道Session ID就能讓用戶中招忘苛。

舉例:用戶C訪問網(wǎng)站A,輸入用戶名密碼成功通過驗(yàn)證唱较,此時(shí)網(wǎng)站A返回Cookie信息給瀏覽器扎唾,用戶可以成功發(fā)送請(qǐng)求到網(wǎng)站A,如果用戶未退出網(wǎng)站A南缓,被攻擊者引誘訪問了攻擊者擁有的網(wǎng)站B胸遇,網(wǎng)站B有一段自執(zhí)行代碼去訪問網(wǎng)站A,此時(shí)用戶與網(wǎng)站A的cookie還未失效西乖,那么就可以成功訪問狐榔,并且這次請(qǐng)求的參數(shù)是由攻擊者決定的坛增,這樣就能以你的名義操作你的賬號(hào)获雕。

解決方案:

  • 驗(yàn)證 HTTP Referer 字段

如果黑客要對(duì)網(wǎng)站實(shí)施 CSRF 攻擊,他只能在他自己的網(wǎng)站構(gòu)造請(qǐng)求收捣,當(dāng)用戶通過黑客的網(wǎng)站發(fā)送請(qǐng)求到后臺(tái)時(shí)届案,該請(qǐng)求的 Referer 是指向黑客自己的網(wǎng)站, 然而罢艾,這種方法并非萬(wàn)無(wú)一失楣颠。Referer 的值是由瀏覽器提供的。事實(shí)上咐蚯,對(duì)于某些瀏覽器童漩,比如 IE6 或 FF2已經(jīng)有一些方法可以篡改 Referer 值。即便是使用最新的瀏覽器春锋,黑客無(wú)法篡改 Referer 值矫膨,這種方法仍然有問題。因?yàn)?Referer 值會(huì)記錄下用戶的訪問來源期奔,有些用戶認(rèn)為這樣會(huì)侵犯到他們自己的隱私權(quán)侧馅,特別是有些組織擔(dān)心 Referer 值會(huì)把組織內(nèi)網(wǎng)中的某些信息泄露到外網(wǎng)中。

  • 在請(qǐng)求地址中添加 token 并驗(yàn)證

為每一個(gè)請(qǐng)求的用戶呐萌,在Session中賦予一個(gè)隨機(jī)值馁痴,在做頁(yè)面渲染的過程中,將這個(gè)token告知前端肺孤,使得前端才發(fā)送請(qǐng)求的時(shí)候會(huì)攜帶上token罗晕,后端收到請(qǐng)求判斷token是否是自己給前端的济欢,以此辨別該請(qǐng)求是否是偽造的。如果是動(dòng)態(tài)生成的HTML或者是使用前端框架渲染小渊,則需要前端程序員代碼中去處理攜帶上token船逮。

路由解析

MVC

MVC是一個(gè)分層模式,它的工作模式是:

1粤铭、路由解析挖胃,根據(jù)URL尋找到對(duì)應(yīng)的控制器和行為(Controller)

2、行為調(diào)用相關(guān)的模型梆惯,進(jìn)行數(shù)據(jù)操作(Model)

3酱鸭、數(shù)據(jù)操作結(jié)束后,調(diào)用視圖和相關(guān)數(shù)據(jù)進(jìn)行頁(yè)面渲染垛吗,輸出到客戶端凹髓。(View)

如何做到路由映射?

手工映射

var routes = [];
// Controller的集合
var use = function (path, action) {
  routes.push([path, action]);
};

function (req, res) {
  var pathname = url.parse(req.url).pathname;
  for (var i = 0; i < routes.length; i++) {
    var route = routes[i];
    if (pathname === route[0]) {
      // 根據(jù)URL尋找到對(duì)應(yīng)的控制器和行為
      var action = route[1];
      action(req, res);
      return;
    }
  }
  // 處理404請(qǐng)求
  handle404(req, res);
}

上面手工映射的問題有兩個(gè)怯屉,

1蔚舀、采用的是硬匹配,對(duì)于動(dòng)態(tài)路徑不方便處理锨络。/profile/jacksontian/profile/hoover兩個(gè)路徑是一樣的模型赌躺,會(huì)出現(xiàn)重復(fù)的代碼操作。

2羡儿、如果使用正則匹配還需要知道到底匹配到了什么礼患,例如上面的username,到底是jacksontian還是hoover掠归,將其匹配到的內(nèi)容抽取出來設(shè)置到req.params處缅叠。

自然映射

路由按照一種約定的方式自然而然地實(shí)現(xiàn)路由,例如:

/controller/action/param1/param2/param3

/user/setting/12/1987為例虏冻,它會(huì)按照約定去找controllers目錄下的user文件肤粱,將其require出來,調(diào)用這個(gè)文件模塊的setting方法厨相,而其余的值作為參數(shù)傳遞給這個(gè)方法领曼。

function (req, res) {
  var pathname = url.parse(req.url).pathname;
  var paths = pathname.split('/');
  var controller = paths[1] || 'index';
  var action = paths[2] || 'index';
  var args = paths.slice(3);
  var module;
  try {
    // require的緩存機(jī)制使得只有第一次是阻塞的
    module = require('./controllers/' + controller);
  } catch (ex) {
    handle500(req, res);
    return;
  }
  var method = module[action];

  if (method) {
    method.apply(null, [req, res].concat(args));
  } else {
    handle500(req, res);
  }
}

RESTful

REST的全稱是Representational State Transfer,中文含義為表現(xiàn)層狀態(tài)轉(zhuǎn)化领铐,它的設(shè)計(jì)哲學(xué)主要是將服務(wù)端提供的內(nèi)容實(shí)體看做一個(gè)資源悯森,并表示在URL上,對(duì)這個(gè)資源的操作绪撵,主要體現(xiàn)在HTTP請(qǐng)求方法上瓢姻,而不是原來的URL上。

對(duì)一個(gè)資源的增刪查改音诈,采用同一個(gè)URL幻碱,但是請(qǐng)求方式不同绎狭,后端根據(jù)不同的請(qǐng)求方式,做出不同的操作褥傍。

PUT /user/jacksontian
DELETE /user/jacksontian
GET /user/jacksontian
POST /user/jacksontian

過去設(shè)計(jì)資源的格式與后綴有很大的關(guān)聯(lián)儡嘶,在RESTful設(shè)計(jì)中,資源的具體格式由請(qǐng)求報(bào)頭中的Accept字段和服務(wù)器端的支持情況來決定恍风,如果客戶端同時(shí)接受JSON和XML格式的響應(yīng)蹦狂,那么它的Accept字段是:Accept: application/json,application/xml,服務(wù)器根據(jù)自己能夠響應(yīng)的格式做出響應(yīng)朋贬,在響應(yīng)中通過Content-Type字段告知客戶端是什么格式:Content-Type: application/json凯楔,所以RESTful設(shè)計(jì)就是,通過URL設(shè)計(jì)資源锦募、請(qǐng)求方法定義資源的操作摆屯、通過Accept決定資源的表現(xiàn)形式。

中間件

對(duì)于web應(yīng)用而言糠亩,我們希望不用接觸到這么多細(xì)節(jié)性的處理虐骑,為此我們引入了中間件(middleware)來簡(jiǎn)化和隔離這些基礎(chǔ)設(shè)施和業(yè)務(wù)邏輯之間的細(xì)節(jié),讓開發(fā)者能夠關(guān)注在業(yè)務(wù)的開發(fā)上赎线,以達(dá)到提升開發(fā)效率的目的廷没。

中間件用來幫我們完成基本功能,例如查詢字符串的解析氛驮,cookie和session的處理腕柜,并把處理結(jié)果反映在req上傳遞下去,由于Node異步的原因矫废,我們需要提供一種機(jī)制,在當(dāng)前中間件處理完成后砰蠢,通知下一個(gè)中間件執(zhí)行蓖扑。

var handle = function (req, res, stack) {
  var next = function () {
    // 從stack數(shù)組中取出中間件并執(zhí)行
    var middleware = stack.shift();
    if (middleware) {
      // 傳入next()函數(shù)自身,使中間件能夠執(zhí)行結(jié)束后遞歸
      middleware(req, res, next);
    }
  };
  // 啟動(dòng)執(zhí)行
  next();
}

這里帶來的疑問是台舱,像querystring律杠、cookie、session這樣基礎(chǔ)的功能中間件是否需要為每一個(gè)路由都進(jìn)行設(shè)置呢竞惋?如果都設(shè)置將會(huì)演變成如下的路由配置:

app.get('/user/:username', querystring, cookie, session, getUser);
app.put('/user/:username', querystring, cookie, session, updateUser);

這并不是一個(gè)好的設(shè)計(jì)柜去,我們需要將路由和中間件結(jié)合起來。

app.use(querystring);
app.use(cookie);
app.use(session);
app.get('/user/:username', getUser);
app.put('/user/:username', authorize, updateUser);
app.use = function (path) {
  var handle;
  // 如果第一參數(shù)是路徑
  if (typeof path === 'string') {
    handle = {
      path: pathRegexp(path),
      stack: Array.prototype.slice.call(arguments, 1)
    };
  } else {
    // 如果不是路徑拆宛,則相當(dāng)于是對(duì)所有路徑進(jìn)行中間件的處理 
    // 之后的所有請(qǐng)求都可以拿到處理后的結(jié)果嗓奢,用與不用,取決于業(yè)務(wù)邏輯浑厚。
    handle = {
      path: pathRegexp('/'),
      stack: Array.prototype.slice.call(arguments, 0)
    };
  }
  routes.all.push(handle);
};

異常處理

如果某一個(gè)中間件出現(xiàn)錯(cuò)誤該怎么辦股耽,我們需要為自己構(gòu)建的web應(yīng)用的穩(wěn)定性和健壯性負(fù)責(zé)根盒,應(yīng)該為next()方法添加err參數(shù),并捕獲中間件直接拋出的同步異常物蝙。

var handle = function (req, res, stack) {
  var next = function (err) {
    if (err) {
      return handle500(err, req, res, stack);
    }

    var middleware = stack.shift();
    if (middleware) {
      try {
        middleware(req, res, next);
      } catch (ex) {
        next(err);
      }
    }
  };
  // 啟動(dòng)執(zhí)行
  next();
};

由于異步方法的異常不能直接捕獲炎滞,因?yàn)楫惒秸{(diào)用都是直接返回,使用try/catch無(wú)法捕獲到請(qǐng)求階段的異常诬乞,所以需要在異步執(zhí)行完的回調(diào)中將異常手動(dòng)傳遞出來册赛。

var session = function (req, res, next) {
  var id = req.cookies.sessionid;
  store.get(id, function (err, session) {
    if (err) {
      // 將異常通過next()傳遞
      return next(err);
    }
    req.session = session;
    next();
  });
}

中間件與性能

使用中間件,我們可以發(fā)現(xiàn)業(yè)務(wù)邏輯往往是最后執(zhí)行震嫉,為了讓業(yè)務(wù)邏輯提早執(zhí)行击奶,盡早響應(yīng)給終端用戶,中間件的編寫和使用是需要一番考究的责掏,下面是兩個(gè)主要提升的點(diǎn):

編寫高效的中間件

編寫高效的中間件其實(shí)就是提升單個(gè)處理單元的處理速度柜砾,以盡早調(diào)用next()執(zhí)行后續(xù)邏輯,因?yàn)橹虚g件一旦匹配换衬,那么每個(gè)請(qǐng)求都會(huì)使該中間件執(zhí)行一次痰驱,哪怕只浪費(fèi)1毫秒的執(zhí)行時(shí)間,都會(huì)讓我們的QPS顯著下降(一秒內(nèi)可以處理的請(qǐng)求數(shù)量稱之為服務(wù)器的QPS)

常見的優(yōu)化方法有:

1瞳浦、使用已有API中高效的方法担映,而不是自己嘗試編寫。

2叫潦、緩存需要重復(fù)計(jì)算的結(jié)果

3蝇完、避免不必要的計(jì)算,比如HTTP報(bào)文體的解析矗蕊,對(duì)于GET方法完全不需要短蜕。

合理使用路由,避免不必要的中間件的執(zhí)行

合理的路由使得不必要的中間件不參與請(qǐng)求處理的過程傻咖,假設(shè)我們這里有一個(gè)靜態(tài)文件的中間件朋魔,它會(huì)對(duì)請(qǐng)求進(jìn)行判斷,如果磁盤上存在對(duì)應(yīng)文件卿操,就響應(yīng)對(duì)應(yīng)的靜態(tài)文件警检,否則就交給下游中間件處理。

var staticFile = function (req, res, next) {
  var pathname = url.parse(req.url).pathname;
  fs.readFile(path.join(ROOT, pathname), function (err, file) {
    if (err) {
      return next();
    }
    res.writeHead(200);
    res.end(file);
  });
};

如果我們以如下方式注冊(cè)路由app.use(staticFile);害淤,那么意味著對(duì)根路徑下所有的URL請(qǐng)求都會(huì)進(jìn)行判斷扇雕,對(duì)于這種情況,我們需要做的是提升匹配成功率app.use('/public', staticFile);窥摄,這樣只有/public路徑會(huì)匹配上镶奉,其他路徑就不會(huì)涉及到該中間件。

頁(yè)面渲染

內(nèi)容響應(yīng)

服務(wù)器端響應(yīng)的報(bào)文,最終都要被終端處理腮鞍,這個(gè)終端可能是命令行終端值骇,也可能是代碼終端,也可能是瀏覽器終端移国。服務(wù)器端的響應(yīng)從一定程度上決定了客戶端該如何處理響應(yīng)的內(nèi)容吱瘩。

瀏覽器通過Content-Type的值來決定采用不同的渲染方式,這個(gè)值我們簡(jiǎn)稱為MIME(Multipurpose Internet Mail Extensions)迹缀,最早用于電子郵件使碾,后來應(yīng)用到瀏覽器中,不同的文件類型具有不同的MIME值祝懂,為了方便獲知文件的MIME值票摇,社區(qū)有專有的mime模塊可以用來判斷文件類型。

var mime = require('mime');
mime.lookup('/path/to/file.txt'); // => 'text/plain'
mime.lookup('file.txt'); // => 'text/plain'
mime.lookup('.TXT'); // => 'text/plain'
mime.lookup('htm'); // => 'text/html

// JSON  application/json
// XML application/xml
// PDF application/pdf
// ...

除了MIME值外砚蓬,Content-Type的值還可以包含一些參數(shù)矢门,如字符集:

Content-Type: text/javascript; charset=utf-8

附件下載

在一些場(chǎng)景下,無(wú)論響應(yīng)的內(nèi)容是什么樣的MIME值灰蛙,需求中并不要求客戶端去打開它祟剔,只需彈出并下載它即可,Content-Disposition字段就是為了滿足這種需求摩梧,客戶端會(huì)根據(jù)它的值判斷是應(yīng)該將報(bào)文數(shù)據(jù)當(dāng)做即時(shí)瀏覽的內(nèi)容物延,還是可下載的附件,當(dāng)內(nèi)容值需即時(shí)查看時(shí)仅父,它的值為inline叛薯,當(dāng)數(shù)據(jù)可以存為附件時(shí),它的值為attachment笙纤,另外Content-Disposition還能通過參數(shù)指定保存時(shí)應(yīng)該使用的文件名耗溜。

Content-Disposition: attachment; filename="filename.ext"

如果我們要設(shè)計(jì)一個(gè)響應(yīng)附件下載的API,大致如下:

res.sendfile = function (filepath) {
  fs.stat(filepath, function(err, stat) {
    var stream = fs.createReadStream(filepath);
    // 設(shè)置內(nèi)容
    res.setHeader('Content-Type', mime.lookup(filepath));
    // 設(shè)置長(zhǎng)度
    res.setHeader('Content-Length', stat.size);
    // 設(shè)置為附件
    res.setHeader('Content-Disposition' 'attachment; filename="' + path.basename(filepath) + '"');
    res.writeHead(200);
    stream.pipe(res);
  });
};

響應(yīng)JSON

res.json = function (json) {
  res.setHeader('Content-Type', 'application/json');
  res.writeHead(200);
  res.end(JSON.stringify(json));
};

響應(yīng)跳轉(zhuǎn)

res.redirect = function (url) {
  res.setHeader('Location', url);
  res.writeHead(302);
  res.end('Redirect to ' + url);
};

視圖渲染

模板是帶有特殊標(biāo)簽的HTML片段粪糙,通過與數(shù)據(jù)的渲染强霎,將數(shù)據(jù)填充到這些特殊標(biāo)簽中,最后生成普通的帶數(shù)據(jù)的HTML片段蓉冈,通常我們將渲染方法設(shè)計(jì)為render(),參數(shù)就是模板路徑和數(shù)據(jù)轩触。

res.render = function (view, data) {
  res.setHeader('Content-Type', 'text/html');
  res.writeHead(200);
  // 實(shí)際渲染
  var html = render(view, data);
  res.end(html);
};

模板

為了使HTML與邏輯代碼分離開來寞酿,催生了一些服務(wù)器端動(dòng)態(tài)網(wǎng)頁(yè)技術(shù),如ASP脱柱、PHP伐弹、JSP。它們將動(dòng)態(tài)語(yǔ)言部分通過特殊的標(biāo)簽(ASP和JSP以<% %>作為標(biāo)志榨为,PHP則以<? ?>作為標(biāo)志)包含起來惨好,通過HTML和模板標(biāo)簽混排煌茴,將開發(fā)者從輸出HTML的工作中解脫出來,這種方法雖然一定程度上減輕了開發(fā)維護(hù)的難度日川,但是頁(yè)面還是充斥著大量的邏輯代碼蔓腐,這催生了MVC在動(dòng)態(tài)網(wǎng)頁(yè)技術(shù)中的發(fā)展,MVC將邏輯龄句、顯示回论、數(shù)據(jù)分離開來的方式,大大提高了項(xiàng)目的可維護(hù)性分歇。

模板技術(shù)雖然多種多樣傀蓉,但它的實(shí)質(zhì)就是將模板文件和數(shù)據(jù)通過模板引擎生成最終的HTML代碼,形成模板技術(shù)的也就如下4個(gè)要素:模板語(yǔ)言职抡、包含模板語(yǔ)言的模板文件葬燎、擁有動(dòng)態(tài)數(shù)據(jù)的數(shù)據(jù)對(duì)象、模板引擎缚甩。模板技術(shù)使得網(wǎng)頁(yè)中的動(dòng)態(tài)內(nèi)容和靜態(tài)內(nèi)容變得不互相依賴谱净,數(shù)據(jù)開發(fā)者和模板開發(fā)者只要約定好數(shù)據(jù)結(jié)構(gòu),兩者就不用互相影響了蹄胰。

但模板技術(shù)并不是什么神秘的技術(shù)岳遥,它干的實(shí)際上是拼接字符傳這樣和底層的活,做的就是替換特殊標(biāo)簽的技術(shù)裕寨,只是各種模板有著各自的優(yōu)缺點(diǎn)和技巧浩蓉。

模板安全

模板技術(shù)用來替換特殊標(biāo)簽然后形成新的HTML代碼,這樣就很容易形成XSS攻擊宾袜,例如將某個(gè)數(shù)據(jù)的值改為<script>alert("I am XSS.")</script>捻艳,如果渲染到頁(yè)面上,這段腳本就會(huì)被執(zhí)行庆猫,為了提高安全性认轨,大多數(shù)模板提供了轉(zhuǎn)義的功能,轉(zhuǎn)義就是將能形成HTML標(biāo)簽的字符轉(zhuǎn)換成安全的字符月培,這些字符主要有&嘁字、<>杉畜、"纪蜒、'

var escape = function (html) {
  return String(html)
    .replace(/&(?!\w+;)/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
};

集成文件系統(tǒng)

var cache = {};
var VIEW_FOLDER = '/path/to/wwwroot/views';

res.render = function (viewname, data) {
  if (!cache[viewname]) {
    var text;
    try {
      text = fs.readFileSync(path.join(VIEW_FOLDER, viewname), 'utf8');
    } catch (e) {
      res.end('模板文件錯(cuò)誤');
      return;
    }
    cache[viewname] = complie(text);
  }
  var complied = cache[viewname];
  res.writeHead(200, {'Content-Type': 'text/html'});
  var html = complied(data);
  res.end(html);
};

這個(gè)res.render()的實(shí)現(xiàn)中,雖然有同步讀取文件的情況此叠,但是由于采用了緩存纯续,只會(huì)在第一次讀取的時(shí)候造成整個(gè)進(jìn)程的阻塞,一旦緩存生效,將不會(huì)反復(fù)讀取文件猬错,其次窗看,緩存之前已經(jīng)進(jìn)行了編譯,也不會(huì)每次讀取都編譯倦炒,并且與文件系統(tǒng)集成了显沈,調(diào)用的時(shí)候只需要指定模板文件的名稱和數(shù)據(jù)即可完成渲染。

app.get('/aaa', function (req, res) {
  res.render('viewname', {});
});
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末析校,一起剝皮案震驚了整個(gè)濱河市构罗,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌智玻,老刑警劉巖遂唧,帶你破解...
    沈念sama閱讀 219,490評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異吊奢,居然都是意外死亡盖彭,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門页滚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來召边,“玉大人,你說我怎么就攤上這事裹驰∷砦酰” “怎么了?”我有些...
    開封第一講書人閱讀 165,830評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵幻林,是天一觀的道長(zhǎng)贞盯。 經(jīng)常有香客問我,道長(zhǎng)沪饺,這世上最難降的妖魔是什么躏敢? 我笑而不...
    開封第一講書人閱讀 58,957評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮整葡,結(jié)果婚禮上件余,老公的妹妹穿的比我還像新娘。我一直安慰自己遭居,他們只是感情好啼器,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評(píng)論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著俱萍,像睡著了一般镀首。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鼠次,一...
    開封第一講書人閱讀 51,754評(píng)論 1 307
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼腥寇。 笑死成翩,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的赦役。 我是一名探鬼主播麻敌,決...
    沈念sama閱讀 40,464評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼掂摔!你這毒婦竟也來了术羔?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤乙漓,失蹤者是張志新(化名)和其女友劉穎级历,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體叭披,經(jīng)...
    沈念sama閱讀 45,847評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡寥殖,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了涩蜘。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嚼贡。...
    茶點(diǎn)故事閱讀 40,137評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖同诫,靈堂內(nèi)的尸體忽然破棺而出粤策,到底是詐尸還是另有隱情,我是刑警寧澤误窖,帶...
    沈念sama閱讀 35,819評(píng)論 5 346
  • 正文 年R本政府宣布叮盘,位于F島的核電站,受9級(jí)特大地震影響贩猎,放射性物質(zhì)發(fā)生泄漏熊户。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評(píng)論 3 331
  • 文/蒙蒙 一吭服、第九天 我趴在偏房一處隱蔽的房頂上張望嚷堡。 院中可真熱鬧,春花似錦艇棕、人聲如沸蝌戒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)北苟。三九已至,卻和暖如春打瘪,著一層夾襖步出監(jiān)牢的瞬間友鼻,已是汗流浹背傻昙。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留彩扔,地道東北人妆档。 一個(gè)月前我還...
    沈念sama閱讀 48,409評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像虫碉,于是被迫代替她去往敵國(guó)和親贾惦。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容