8web应用
1、基础功能
构建web应用考虑的基本功能 1、函数作为参数,传递给createServer()方法 2、请求方法处理 req.method 3、路径解析 req.url(静态文件、路径对应控制器) 4、查询字符串 ?foo=bar&baz=val 5、cookie
- 业务层面需要状态,区分身份
- 响应的cookie,在Set-Cookie中
- Path 影响的路径
- Expires(具体时间)、Max-Age(多久后) 过期时间
- HttpOnly 不允许浏览器修改
- Secure https才有效
- 性能:
- 减小cookie 域名相同都会带上
- 静态组件使用不同的域名
- 广告:
- 标识用户,投放广告 6、session
- 数据保留在服务器,安全
- 问题:
- 内存限制、进程之间内存不共享
- 解决:redis/memchaed
- 第三方redis问题:网络访问(但影响较小)
- 安全:
- 使用一个私钥,签名session,进行加密,响应式作对比
- 攻击者获取口令和签名
- 将客户端的独有信息与口令,然后签名 7、缓存
- 在get请求中
- 第一次请求:服务器内容
- 第二次请求:
- 本地文件进行检查,不确定是否可用
- 发起一次条件请求,
- 附带If-Modified-Since字段
- 问服务有没有更改?没有,304,使用本地
- 更改了,放弃本地
- If-Modified-Since 事件戳问题:
- 内容改动、时间戳没有改动
- 只精确到妙级别,更频繁的无法生效
- 使用Etag
- Etag
- if-None-Match/ETag
req.headers['if-None-Match']
res.setHeader("Etag",hash)
- 可以是根据文件内容生成散列值,进行对比
- 不发起请求
- 让浏览器明确地将内容缓存起来
- Expires
res.setHeader("Expires",toUTCString())
- 不足:前后端时间不一致
- Cache-Control头
res.setHeader("Cache-Control","max-age=10000000")
- max-age:倒计时方式计算过期
- 还能设置public、private、no-cache、no-store更精细选型
- 这两个字段基本对应了cookie的字段
- 8、Basic认证
- 检查报文头Authorization字段的内容
- 已废弃
- 摘要访问认证(改进Basic认证)
- oauth
- sso
- jwt
2、数据上传
报文体解析、Content-Type常见类型、安全:内存、CSRF
- node对报文体没有解析,自行解析
- 通过这两个字段:Transfer-Encoding(长度未知)或Content-Length(长度一致) 判断有没有内容
- 这两个字段互斥
- Transfer-Encoding:chunked 分成块逐个发送
- Content-Type 常见格式
- 接受处理思路:先接受内容,在解析格式
rawBody
是buffer拼接而成的- Content-Type: application/x-www-form-urlencoded (
<form>
表单)- 最简单:使用
querystring.parse(req.rawBody)
解析
- 最简单:使用
- Content-Type: application/json;charset=utf-8
- json:
JSON.parse(req.rawBody)
解析
- json:
- Content-Type: application/xml;charset=utf-8
- xml:库xml2js
xml2js.parse(req.rawBody)
- xml:库xml2js
- Content-Type: multipart/form-data; boundary=AaB03x (formdata)
- 提交文件:file类型的控件
- boundary=AaB03x指定的是每部分内容的分界符,随机生成的一段字符串
- 通过在它前面添加--进行分割,结束时在
前后边都加--
,标识结束 - 后边进行验证下
- 对于文件上传,上面这种处理思路变得不可接受,先处理格式,找到对应的处理方法
- 文件上传使用,模块formidable(8.2/upload)
- 安全
- 内存
- 限制上传大小 (Transfer-Encoding/Content-Length两个字段来控制大小)
- 导向磁盘
- CSRF(Cross-Site Request Forgery)
- 场景:
- 留言中添加访问接口代码,或者邮件中诱导客户点击链接
- 过程
- 网站a是正常的,登录用户,发起过请求
- 网站b(黑客的)中添加网站a的请求
- 攻击者诱导客户从a登录用户访问b
- 到达b自动向a发送一个请求
- 解决
- 为每个请求的用户,在Session中赋予一个随机值,_csrf
- 将这个值告诉前端
- 每次请求都带上
- 服务端存储的随机值进行对比
- 伪代码中无法获取这个随机值,发起请求,则失效
- 场景:
- 内存
3、路由解析
路由解析三种思路:文件路径、mvc(映射)、restful
- 文件路径
- 静态文件:url与路径一致
- 动态文件:(node不常见)
- 根据路径找到执行脚本,
- 根据文件后缀寻找文本解析器,执行脚本
- MVC
- URL路径原来可以跟具体脚本所在的路径没有任何关系
- 包括
- 控制器(Controller),一组行为的集合。
- 模型(Model),数据相关的操作和封装。
- 视图(View),视图的渲染。
- 思路
- 路由解析,根据URL寻找到对应的控制器和行为
- 行为调用相关的模型,进行数据操作
- 数据操作结束后,调用视图和相关数据进行页面渲染,输出到客户端
- 实现:如何根据URL做路由映射
1、手工映射(/8.3/handmap.js)
jsvar http = require("http"); var url = require("url"); var routes = []; var strict = true; var pathRegexp = function (path) { var keys = []; path = path .concat(strict ? "" : "/? ") .replace(/\/\(/g, "(?:/") .replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?(\*)?/g, function (_, slash, format, key, capture, optional, star) { slash = slash || ""; keys.push(key); return "" + (optional ? "" : slash) + "(?:" + (optional ? slash : "") + (format || "") + (capture || (format && "([^/.]+?)") || "([^/]+?)") + ")" + (optional || "") + (star ? "(/*)?" : ""); }) .replace(/([\/.])/g, "\\$1") .replace(/\*/g, "(.*)"); return { keys: keys, regexp: new RegExp("^" + path + "$") }; }; var use = function (path, action) { routes.push([pathRegexp(path), action]); }; exports.setting = function (req, res) { console.log("setting"); res.end("setting"); // TODO }; // use("/user/setting", exports.setting); // use("/setting/user", exports.setting); // 甚至 // use("/setting/user/jacksontian", exports.setting); use("/profile/:username", function (req, res) { var username = req.params.username; // TODO console.log(username); res.end(username); }); function handle404(req, res) { console.log("no page"); res.end("no page"); } function handle(req, res) { var pathname = url.parse(req.url).pathname; for (var i = 0; i < routes.length; i++) { var route = routes[i]; // 正则匹配 var reg = route[0].regexp; var keys = route[0].keys; var matched = reg.exec(pathname); if (matched) { // 抽取具体值 var params = {}; for (var i = 0, l = keys.length; i < l; i++) { var value = matched[i + 1]; if (value) { params[keys[i]] = value; } } req.params = params; var action = route[1]; action(req, res); return; } } // 处理404请求 handle404(req, res); } http.createServer(handle).listen(2000, "127.0.0.1");
- 添加一个路由表,路径对应action
- 将请求的路径,解析,找到对应路由,执行action
- 复杂的使用正则匹配
2、自然关联映射(/8.3/natureMap.js)
jsvar http = require("http"); var url = require("url"); function handle500(req, res) { console.log("node page"); } function handle(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的缓存机制使得只有第一次是阻塞的 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); } } http.createServer(handle).listen(2001, "127.0.0.1");
- 尽是路由不如无路由
- 实质是路由按一种约定的方式自然而然地实现了路由,而无须去维护路由映射
- 将如下路径进行了划分处理
- /user/setting/12/1987为例,
- 它会按约定去找controllers目录下的user文件,
- 将其require出来后,调用这个文件模块的setting()方法
- /12/1987作为参数传递
- php ci框架中应用十分广泛
- RESTful
- (Representational State Transfer) 表现层状态转化
- 将服务器端提供的内容实体看作一个资源
- 对这个资源的操作,主要体现在HTTP请求方法上,不是体现在URL上js
POST /user/jacksontian DELETE /user/jacksontian PUT /user/jacksontian GET /user/jacksontian
- 资源的具体格式由请求报头中的Accept字段和服务器端的支持情况来决定
Accept: application/json, application/xml
- 综述:
app.post('/user/:username',addUser)
- 通过URL设计资源
/user/:username
- 请求方法定义资源的操作
GET POST
- 通过Accept决定资源的表现形式
Accept: application/json, application/xml
- 具体实现
- routes改为method对应结构,app也改为method对应结果
- app.use()注册一个路由,填充到routes和app
- 根据req.url和req.method进行解析,找到对应路由以及处理函数
4、中间件
中间件实现思路
- 定义
- 将细节按照不同的功能“公共功能”(基础设施)和“业务逻辑”进行分离;
- 封装底层细节,为上层提供更方便服务
- 原理:尾触发
- 1、注册use的时候,将中间件push到路由对应stack[]中
- 2、url进行match路由的时候(接受请求),匹配到,将对应stack传到第三个参数,继续进行handle执行操作
- 3、执行的时候,从stack中shift出,判断有没有中间件,如果有则执行当前中间件
- 4、因为stack有可能有多个中间件,将stack传给当前执行中间件的第三个参数(递归)
- 5、中间件都改为,有第三个参数的模式,执行玩自己的内容,走第三个参数(尾调用)next,即下一个中间件
- 6、增加路由共同的中间件、各自的中间件
- 特点:职责单一、可扩展、可组合
- 性能:
- 1、编写高效中间件;
- 高效方法
jsperf.com
测试基准性能 - 缓存需要重复计算的结果
- 避免不必要的计算
- 高效方法
- 2、合理利用路由,避免不必要的中间件执行;
- 只有/public路径会匹配上,其余路径根本不会涉及该中间件
app.use('/public', staticFile);
- 1、编写高效中间件;
5、页面渲染
页面渲染需要考虑的因素以及实现原理
- 内容响应
- 响应报头中的 Content-*字段十分重要
- 1、MINI
- 2、下载附件
- Content-Disposition字段应声登场
- Content-Disposition: attachment; filename="filename.ext"
- 3、响应JSON
- 4、响应跳转
- 视图渲染
- 模板技术:
- 模板语言
- 包含模板语言的模板文件
- 拥有动态数据的数据对象
- 模板引擎
- 模版技术演化:
- 1.0:在CGI程序或servlet中输出HTML片段
- 2.0:动态网页技术,如ASP、PHP、JSP(模板极度依赖上下文)
- ASP、PHP、JSP而言,模板属于服务器端动态页面的内置功能
- 模板语言就是它们的宿主语言(VBScript、JScript、PHP、Java),
- 模板文件就是以.php、.asp、.jsp为后缀的文件,
- 模板引擎就是Web容器
- 3.0 脱离上下文环境,但依赖宿主语言
- PHPLIB Template和FastTemplate、
- Java的XSTL,以及Velocity、JDynamiTe、Tapestry
- 4.0 破局者是Mustache
- 给出了十多门编程语言的模板引擎实现
- 可移植
- 模板技术本质- 拼接字符串这样很底层的活
- 实现考虑:
- 基础模板引擎(实现思路):
js// 中间函数 var complie = function (str) { var tpl = str.replace(/<%=([\s\S]+? )%>/g, function(match, code) { return "' + obj." + code + "+ '"; }); tpl = "var tpl = '" + tpl + "'\nreturn tpl; "; // Function构造函数 // new Function ([arg1[, arg2[, ... argN]], ] functionBody) return new Function('obj, escape', tpl); }; var render = function (complied, data) { return complied(data); }; var tpl = 'Hello <%=username%>.'; console.log(render(complie(tpl), {username: 'Jackson Tian'})); // => Hello Jackson Tian.
- with应用 支持不同变量
- 模版安全
jsvar escape = function (html) { return String(html) .replace(/&(? ! \w+; )/g, '& ') .replace(/</g, '< ') .replace(/>/g, '> ') .replace(/"/g, '" ') .replace(/'/g, '' '); // IE下不支持'(单引号)转义 };
- 字符转义,html字符转为安全字符
- 支持逻辑 if else
- 集成文件系统 将tpl模版放在文件中
- 子模版:支持复用
- 布局视图:父模版的复用
- 性能
- 模板技术: