Skip to content

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)解析
    • Content-Type: application/xml;charset=utf-8
      • xml:库xml2js xml2js.parse(req.rawBody)
    • 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)

        js
        var 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)

        js
        var 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);

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应用 支持不同变量
      • 模版安全
      js
      var escape = function (html) {
        return String(html)
          .replace(/&(? ! \w+; )/g, '&amp; ')
          .replace(/</g, '&lt; ')
          .replace(/>/g, '&gt; ')
          .replace(/"/g, '&quot; ')
          .replace(/'/g, '&#039; '); // IE下不支持&apos;(单引号)转义
      };
      • 字符转义,html字符转为安全字符
      • 支持逻辑 if else
      • 集成文件系统 将tpl模版放在文件中
      • 子模版:支持复用
      • 布局视图:父模版的复用
      • 性能