对js对象创建、原型、原形链、继承及”类”的理解,由于其间各种复杂的关系及掌握程度不够,个人经常需要翻资料确认,于是找时间整理了下当时的理解,并以此文形式记录下来。

面向对象

oop,面向对象编程。主要有以下几大特征:

  • 类,具有相同结构和行为的集合。
  • 封装,将操作封闭,对外提供接口供使用。
  • 继承,子类具有基类(父类)的特征,还可以有自己的新的特性。
  • 多态,一个功能可以有多种不同用法,或者说一个子类可以将父类某个属性替换,具体的包括重写(override, 方法名称与参数相同)和重载(overload, 方法名称相同,参数不同)。

JavaScript对象

js对象,除“null”外,其他的可以看做是属性的无序集合,每个属性都是一个名/值对,且其拥有一个连接到原型对象的隐藏连接。“null”可以理解成不是对象的对象。函数,也是对象。js对象是动态的,可以新增和删除属性,通过引用而非值来操作对象。每个属性还有与之相关的一些其他值,称为“属性特性(property attribute)”:

  • 可写(writable attribute),表明是否可以设置该属性的值;
  • 可枚举(enumerable attribute),表明是否可以通过for/in循环返回该属性;
  • 可配置(configurable attribute),表明是否可以删除或修改该属性。

这三个特性是在ES5之后可以加以配置,在ES5之前,通过代码给对象创建的属性都是可写的、可枚举的和可配置的。除了包含属性之外,每个对象还拥有三个相关的对象特性(object attribute):

  • 对象的原型(prototype)指向另外一个对象,本对象的属性继承自它的原型对象;
  • 对象的类(class)是一个标识对象类型的字符串;
  • 对象的扩展标记(extensible flag)指明了是否可以向该对象添加新属性。

js对象的属性包括两类:自有属性(own property)及继承属性(inherited property)。

原型

原型是对象的内部属性,可以将其理解为指向构造函数原型对象的内部指针,默认不可访问,部分浏览器实现可以通过.__proto__访问(eg.Chrome),我们可以结合.__proto__来加深理解。
先说我知道的几个没有原型的对象:

  • null没有原型;
  • Object.prototype没有原型(或者说原型为null)。

然后介绍几种获取对象的原型属性的方法:

  • 所有通过对象直接量创建的对象都具有同一个原型对象—Object.prototype
  • 通过关键字new和构造函数调用创建的对象,它的原型就是构造函数的prototype属性的值。即,通过new Date()创建的对象,其原型就是Date.prototype;
  • 通过Object.create()创建的对象使用第一个参数(可以是null)作为它们的原型;

每个函数对象在创建时会带有一个prototype属性,它的值是一个拥有constructor属性且值即为该函数本身的对象。
当一个函数对象被创建时,Function构造器产生的函数对象会运行类似下面的代码:

1
this.prototype = {constructor:this, __proto__:...};

新函数对象被赋予一个prototype属性,其值包含一个constructor属性且属性值为该新函数对象本身。

    var C = function(){
        this.key = 'key';
    };
    C.prototype.constructor === C;    // true

function

上面的概念说完了,可以利用chrome的console面板,通过以下代码加深理解:

    null.__proto__    // Uncaught TypeError: Cannot read property '__proto__' of null
    Object.prototype.__proto__    // null
    var o = {};
    o.__proto__ === Object.prototype    // true
    var A = function(){};
    A.__proto__ === Function.prototype    // true
    A.prototype.constructor === A    // true
    A.prototype.__proto__ === Object.prototype    // true
    var a = new A();
    a.__proto__ === A.prototype    // true
    var b = Object.create(A);
    var c = Object.create(A.prototype);
    var empty = Object.create(null);
    b.__proto__ === A    // true 注意此处,对象b的原型为A
    c.__proto__ === A.prototype    // true 注意此处,对象c的原型为A.prototype
    empty.__proto__    // undefined 注意跟上面对象o的区别

__proto__

原形链

如上面所述,几乎每个对象都有原型(__proto__)。如果其原型不为null,且是以构造函数形式创建的对象,其原型指向该构造函数的prototype属性。该构造函数的prototype属性也有原型(__proto__),如果不为null,则接着往上引用,直到原型为null。如下代码所示:

    var A = function(){};
    var a = new A();
    a.__proto__ === A.prototype;    // true
    A.prototype.__proto__ === Object.prototype;    // true
    Object.prototype.__proto__ === null;    // true
像上面这样通过`原型(__proto__)`一直往上可以串联起来就是原型链。

创建对象

可以通过对象字面量、new及(ES5)Object.create()方法来创建对象。 + 对象字面量创建方式很简单,`var a = {}`; + 通过关键字`new`创建,也可以叫做通过`构造函数`创建,比如:`var date = new Date()`; + 通过ES5新提供的方法,`Object.create()`创建。其接收两个参数,第一个参数是当前要创建对象的原型,第二个参数表示当前要创建对象的特性约束,即是否可写、可配置等。下面代码简单介绍下使用及细节:
    var a = Object.create(null);
    a.__proto__        // undefined 此时a是一个连原型都没有的空对象
    var b = Object.create({});
    b.__proto__        // Object {}, b是一个普通的空对象,其有原型,指向空的Object`{}`
    var c = Object.create(Object.prototype);
    c.__proto__ === Object.prototype    // true

上面介绍的是创建对象的最基础方法,这里稍微扩展一些,介绍几种关于创建对象的设计模式:

  • 工厂模式,此模式抽象了创建具体对象的过程,用函数来封装以特定接口创建对象的细节,如下所示:
      function create(name,age){
          var o = new Object();
          o.name = name;
          o.age = age;
          return o;
      }
      var p1 = create('li',20);
      var p2 = create('wu',30);
    
  • 构造函数模式,上面已经涉及,平常也经常用到,不举例子了;
  • 原型模式,此方式将属性写在构造函数的prototype上面,实用性不强,一般不用;
  • 原型模式与构造函数组合使用,此模式将共享的属性和方法写在prototype上,构造函数里面用于自定义属性,比较通用。

继承

js实现继承的方式很多,下面我介绍的只是用的比较多的一种,可以叫做:组合继承。其将原形链和借用构造函数技术组合到一块,发挥各自的长处实现的一种继承模式。
其背后的思路是使用原形链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承,如下面例子:

    function Man(name){
        this.name = name;
        this.sex = 'male';
    }
    Man.prototype.myName = function(){
        console.log(this.name);
    };

    function Son(name,age){
        // 继承属性
        Man.call(this,name);
        this.age = age;
    }
    Son.prototype = Object.create(Man.prototype);
    Son.prototype.myAge = function(){
        console.log(this.age);
    }
    Son.prototype.constructor = Son;    // 由于上面更改了Son.prototype的值,将其中的constructor值也改变了,因此此处必须将其写回
    var li = new Son('li',20);
    li.myName();    // li
    li.myAge();        // 20
    li.sex;        // male

最后

继承、创建对象,还有其他的一些形式,目前我不大常用,所以没有在上文提及,有兴趣的可以自行了解。

参考资料

《JavaScript语言精粹》
《JavaScript权威指南(第6版)》
《JavaScript高级程序设计(第2版)》等



money☜☜☜ wechat 『『『 reward 点击扫码打赏 ~~~ ^_^ 』』』alipay ☞☞☞ money



之前经常会看到一些日常比较少见的CSS用法,事后想起要用的时候找半天,特意留一文记录,有待日后持续更新。

  • font-variant: small-caps

      desc:    指定文本为小型大写字母;
      tips:    使用此样式,大写字母显示保持不变,小写字母为小一号的大写字母了。
      usage:    this IS demo
    
  • -webkit-filter: drop-shadow(rgb(102, 153, 153) 20px 0px);

      desc:    可以利用CSS3滤镜实现给图片换颜色;
      tips:    目前需要比较新版本浏览器支持;
      usage:    可参考文章:张鑫旭blog:PNG格式小图标的CSS任意颜色赋色技术
    
  • color

      这里主要是一点提示,CSS颜色可以用名称、16进制数字或者rgb等多种方式表示,如果用一些比较少见的名称表示的话,你要留意是否其他浏览器支持。
      有一点我之前没有注意到的是,纯绿色,并不是:`green`--#008000--,    而是:`lime`--#00ff00--
    


money☜☜☜ wechat 『『『 reward 点击扫码打赏 ~~~ ^_^ 』』』alipay ☞☞☞ money



上面一篇文章简单介绍了gulp的相关知识及在项目中如何使用gulp进行自动化构建,本文简单介绍下如何搭配gulp做一些其他的逻辑处理,只是提供一种思路,不表示可以完全套用。

如果没有合适的插件怎么办?

通过npm安装的gulp插件,都是先想想自己要的需求,然后上npm社区找,万一找不到或者自己不会找,怎么办?

  • 假设一个需求
    我们先假设一个简单的需求:每次发布指令生成的文件带上当前时间信息。
  • 处理思路
    拿到这个需求第一反应可能还是先上社区找一下,没找到,怎么办?试着变通一下,如果没找到,那我自己写一个插件岂不是也可以。
    想法很好,也可以行得通,目前有through2插件可以帮助我们写gulp插件,但是写一个插件对一个gulp初学者来说可能还是比较难的事情,那么我们换一个思路。
    我们可以尝试在每次gulp.dest前,先给最终要生成的文件重命名,添加时间戳,这样可以保证正常情况下每次构建后的文件跟之前文件名都不一样。
  • 实现
    简单的根据时间戳重命名,单个文件还好,多个文件的话,由于很多异步操作,记录时间的时刻必须比较特别,选用执行命令的那一刻可能更好些。

有些时候,开发版指令可能需求简单些,或者单个task的需求简单些,这时可以用命令行参数搭配使用来方便构建。

搭配命令行参数

命令行参数可以用process.argv获取,拿到的第三个参数,可以表示为当前task的参数。
因此,我们可以拿这个来做简单的区分,如果是特殊命令,可以选择携带一个特殊的参数,这样不用每次都单独写任务来表示。

使用多彩的控制台输出

npm社区有不少模块可以帮助生成多彩的控制台输出,虽然这对项目构建没有实际的作用,但是可以很清楚的区分当前使用的任务。

替换多个输出的文件名

如果有需求是替换某个文件夹中的所有文件名,此需求与上面的需求类似,可以一起进行。
具体代码可以自己琢磨,可以使用path模块的path.join方法来实现。

最后

由于自己对这方面也不是很熟悉,只是根据自己的经验提供一个解决问题的思路,希望可以举一反三,而不是滥用。



money☜☜☜ wechat 『『『 reward 点击扫码打赏 ~~~ ^_^ 』』』alipay ☞☞☞ money



gulp,主要用来前端项目开发过程中自动构建项目,自身的api比较简单,一般搭配着多种插件来使用,以最大限度满足项目的构建需要。
gulp跟grunt、webpack的功能差不多,grunt算是gulp的前辈吧,稍早先用grunt的可能比较多点,webpack目前比较火的原因可能也跟其与react天生一对有关,哥俩好。
本文暂不涉及grunt及webpack的知识,主要介绍两个主题,前一部分先简单介绍gulp的工作原理,后一部分将介绍我是怎么使用gulp的,可以概括为:用gulp构建angular项目中我主要涉及的需求、实现思路及插件。

流(stream)

在使用gulp之前,你先要了解下其工作原理,gulp核心思想之一便是:流。
通过gulp.src方法,将文件以流的形式返回、输出。

管道(pipe)

管道可以看成是流的运输工具吧,通过.pipe方法,可以将输入的流流转到他处进行其他操作,比如最简单的,将其写入文件。

dest

大部分构建任务的最后一步处理可能都是写文件,或者写文件夹。通过gulp.dest方法,将初始文件进行相应的处理后,写入一个新文件(文件夹)中,或者覆盖原有文件。

task

gulp.task可以用来定义一个任务,第一个参数为任务名称,第二个参数表示当前任务要执行的动作,为一个函数。

基本构建需求

先说一般情况下最简单前端项目构建需求,可能包括js合并、压缩、混淆,css合并、压缩,然后支持开发和发布两套指令。
先介绍一个插件:gulp-load-plugins,它可以根据package.json文件,识别gulp插件,在代码中调用后,会将以gulp为前缀的gulp插件添加到返回的对象列表中,根据后面的名字驼峰表示法可以拿到此插件的导出对象。这个插件的好处在于如果代码中用到的gulp插件太多,可以不用一个个的写require代码来引用插件模块。

  • 文件合并
    gulp-cancat插件可以用于合并多个文件;
  • 文件压缩、混淆
    文件压缩的插件可能比较多,可以根据需要自己选择,目前js处理,选用 gulp-uglify,其可对js文件进行压缩及混淆两步处理;
    css文件,选用gulp-minify-css来进行压缩处理;
  • 两套指令
    工作当中的指令是能够同时支持项目开发需要及发布需要,比较合适的方案是结合if条件判断及gulp-if插件俩个配合来实现。

进一步扩展

拿一个简单的angular项目来举例,需求可就不止上面这些。

  • js依赖注入
    使用gulp-ng-annotate插件可以方便在angular项目开发过程中动态依赖注入;
  • 重命名
    使用gulp-rename插件可以重命名文件;
  • 生成.map文件
    使用gulp-sourcemaps插件可以生成.map文件,可以用于混淆后的js文件快速定位问题;
  • less文件编译
    使用gulp-less插件可以对项目中的.less文件进行编译成正常可使用的.css文件;
  • 自动添加浏览器前缀
    使用gulp-autoprefixer,可以给样式表动态自动添加浏览器前缀;
  • 动态引用资源
    使用gulp-inject,可以给指定的入口文件,如index.html,动态插入引用的资源文件,由于最终引用的资源文件可能不时的在更换文件名或者版本号来避免浏览器缓存等,因此使用动态插入引用既可以避免出错又提高工作效率;
  • html打包js
    使用gulp-angular-templatecache,可以将项目中用到的html文件打包为js文件,生成的js文件包含html模版文件的cache,使用时只需要写入依赖即可;

最后

上述这些插件的合理使用可以基本服务一个angular项目的正常构建需要,但是当项目日益庞大,所需要的需求日加复杂的情况下,可能就需要在使用已有的插件搭配工作下进行自己的一些逻辑处理,下文将再对此做一个简单的思路介绍。



money☜☜☜ wechat 『『『 reward 点击扫码打赏 ~~~ ^_^ 』』』alipay ☞☞☞ money



本文主题是使用node来搭建最简单的web服务器,其后可以自己根据需要深入了解,目前在开发过程中可以用来模拟与服务器进行简单的交互,比如返回的资源控制等。
早先不使用web服务器的情况下想要在浏览器端访问本地资源,可以利用firefox浏览器,其可以自己启动一个小型web服务器。
为了让刚接触node的人也能大体看懂,本文的代码我将尽量简化。

准备

首先,需要安装nodejs,这个可以去官网下载,目前我本地安装的v0.12版本。
安装完成后可以通过命令行测试安装是否成功,输入:node -v,应该会显示当前安装node版本号
本文中用到的模块,都是nodejs核心模块,不需要从外部下载,如果有需要,可以使用以下命令安装:npm install xxx

开始

下一步,新建js文件,可以命名为server.js,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
var http = require('http');
var url = require('url');
var path = require('path');
var fs = require('fs');
var dir, arg = process.argv[2] || ''; // 命令行第三个参数,用来接收目录,可为空,相对当前server.js文件的目录名称
// 比如使用命令 node server debug,意思就是debug文件夹与server.js文件同级
// 且你想以debug文件夹启动web服务
http.createServer(function (req, res) {
var pathname = __dirname + url.parse(req.url).pathname;
dir = dir ? dir : pathname; // 记住dir(目录)
pathname = dir ? pathname.replace(dir, dir + arg + '/') : pathname; // 替换文件静态路径
if (path.extname(pathname) == "") {
pathname += "/";
}
if (pathname.charAt(pathname.length - 1) == "/") {
pathname += "index.html"; // 入口文件,此处默认index.html
}
fs.exists(pathname, function (exists) {
if (exists) {
switch (path.extname(pathname)) {
case ".html":
res.writeHead(200, {"Content-Type": "text/html"});
break;
case ".js":
res.writeHead(200, {"Content-Type": "text/javascript"});
break;
case ".css":
res.writeHead(200, {"Content-Type": "text/css"});
break;
case ".gif":
res.writeHead(200, {"Content-Type": "image/gif"});
break;
case ".jpg":
res.writeHead(200, {"Content-Type": "image/jpeg"});
break;
case ".png":
res.writeHead(200, {"Content-Type": "image/png"});
break;
default:
res.writeHead(200, {"Content-Type": "application/octet-stream"});
}
// res可以自己添加信息来简单交互 比如可以修改点header信息 或者修改返回的资源数据
fs.readFile(pathname, function (err, data) {
res.end(data);
});
}
else {
res.writeHead(404, {"Content-Type": "text/html"});
res.end("<h1>404 Not Found</h1>");
}
});
}).listen(8085, "127.0.0.5"); // 服务器端口
console.log("server running at http://127.0.0.5:8085/");

启动

node安装完成及上述server.js文件也新建好之后。将其与你要访问的文件夹放在一起,可以放同层或者直接下层。比如,如果你要访问d:\test\debug文件夹。
你可以先将当前文件放入同层或者直接下,然后输入如下命令启动web服务:

1
2
3
4
先打开`cmd`,进入server文件所在目录,比如是`test`目录;
然后输入:`node server debug`(同层), 或者`node server`(子层),
此时会提示`server running at http://127.0.0.5:8085/`, 表示启动服务成功;
最后打开浏览器,进入:`127.0.0.5:8085`,即可访问此资源。

最后

简单解释下上面的代码。

  • 首先最上面的require表示需要用到那几个模块,先引用一下;
  • arg表示输入的命令行的第三个参数,上面是做了手动截取;
  • createServer方法表示创建一个http服务,以函数为参数,本文代码中传入了一个匿名函数;
    • req,表示http request(请求)对象,其携带着来自客户端此次http请求的相关信息,例如请求method、请求query参数、请求header头信息等;
    • res,表示http response(返回)对象,用来给客户端返回请求资源用,可以手动添加信息,例如返回的数据、返回的头信息等、返回的code等;
    • fs,表示文件资源对象,具体可以访问nodejs官网的api
    • path,表示资源路径对象,具体可以访问nodejs官网的api
  • listen表示创建的服务监听,一旦访问了此端口,将进入此前的匿名函数回调中,将资源返回给客户端。


money☜☜☜ wechat 『『『 reward 点击扫码打赏 ~~~ ^_^ 』』』alipay ☞☞☞ money



一般的angular项目都是单页应用,但是当项目逐渐庞大时,可能会需要拆分多个应用来具体实现。近期花时间将原有的项目结构进行改造,以便进行多应用开发,下图是我设计的初版项目目录结构,希望可以给有需要的人一点帮助。

structure



money☜☜☜ wechat 『『『 reward 点击扫码打赏 ~~~ ^_^ 』』』alipay ☞☞☞ money



以下皆为个人原创见解,不当之处烦请指正。

首先,我有些话想说在前面。现在网上的各种开放的资源,很多都是有访客模式,即不用登录也能浏览或者操作,但是现今时代没有登录功能或者登录页面的网站,几乎绝迹了。
说实话,登录页面除了验证信息作用外,其他的都不是主要的,因此可能很多人觉得没有必要花很多时间在这上面。
我也同意上述观点!
但是呢,既然做了,当然希望做的更好点啦,废话不说,下面进入主题。

吐槽

各大公司的网站,做的已经非常棒了,至少我是很佩服。
但是有时候会遇到一点小瑕疵,比如说下面几个例子,我平时也会拿出来吐槽下,用以慰藉下像我这种技术渣幼小的心灵。

  • JD
  1. JD的登录页面,如果没输入用户名和密码,或者只输入密码,点击登录按钮,会请求服务器拿两个log.gif文件,我并不知道这个文件是干嘛用的,为什么要这么做。然后当输入了用户名再删去,也会发出请求。不是应该只有都输了,才发出请求的吗???
  2. 第二点,就是点击右下角扫码登录后,按钮居然跑下面来了,居然不是在同一个地方,每次点完都要移动鼠标才能点回去,这体验太差了吧。
  3. 第三点,从普通登录切换扫码登录,拿一个code图片,我能理解,但是为什么切回来的时候,还要拿一次吖???
  4. 第四点,登录页面没有做自适应处理,缩放后出现滚动条。
  • BD
    BD的登录弹框,每次修改完密码再失焦,会请求一次publicKey。然后从扫码登录页面切换回来,也还会再请求一次gif文件。但至少切换的按钮,位置没变,不需要再移动鼠标。也没有做自适应处理,缩放屏幕出现滚动条。
  • ALI
  1. TB的登录页面做了自适应,有一个767的分界。屏宽小于767px,会让登录窗口居中显示,去掉背景。
    但是关于TB登录页面的自适应做的,也有问题。当屏宽超过767px时,由于其左边的背景看着像是广告图片,图片的宽度设置为屏宽100%,而整个页面内容区域宽度也是固定的,目前是1200px,当屏宽在767与1200之间时,还是出现了横向滚动条。
    关键的来了,由于TB登录框的position是relative,此时被固定在右侧了,你要看到首先得拖动滚动条,一拖滚动条,背景就跟不过来了,就会很容易出现我看到登录框整个背景是白色的!
    或者部分背景是白色的!没有背景!或者说脱离背景图片了!这个看着视觉效果太差了感觉。
  2. 第二点,当我输入用户名和密码后,不管我有没有拖动验证条,都会去执行登录请求服务器。而且我很奇怪的是,登录失败也要整个页面重新刷新一次!!!
    我的天呐,我不知道为什么需要重新刷新一次,而且我感觉好像是有意为之,因为登录失败再加载页面很慢,还有专门的加载进度圈圈。。。

界面

吐槽完了,说下我自己写的渣渣页面吧。由于h5很多元素用的不多,语义性可能有点乱,但是大致思想在那。
首先界面,主要是三块,header、section及footer,大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!--login header-->
<header class = "login-header">
<!--login logo-->
<div class = "login-logo">
...
</div>
</header>
<!--login content-->
<section class = "login-content">
<!--login background-->
<div class = "login-bg">
...
</div>
<!--login layout-->
<div class = "login-layout">
<div class = "login-box-wrap">
...
</div>
</div>
</section>
<!--login footer-->
<footer class = "login-footer">
...
</footer>

header部分现在是放一个logo区域。
footer部门现在是放一些友链啥的。
section是主要内容区域。内容区域放一个可以独立出来的弹出框,因为有些网站,像BD,可能不光有登录页面,还有登录弹框。
单独拿出来可以保证此页面可以复用,比如项目是用ng开发的时候,写成指令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div class = "login-box">
<div class = "login-box-content">
<!--login quick-->
<div class = "login-quick">
...
</div>
<!--login static form-->
<form class = "login-static" submit = "signIn();">
... (账户)
... (密码)
... (验证码)
<!--login button-->
<div class = "login-btn">
<button type = "submit" class = "btn btn-success">登 录</button>
</div>
</form>
</div>
</div>

自适应&视效(样式)

可能不算视效啦,只是不知道怎么描述。
首先,内容区域,即section部分,我设置position为relative,相对定位。宽度100%。背景图片宽度100%,绝对定位。
添加媒介查询,屏宽小于768px时,背景不要了。

1
2
3
4
5
6
7
8
9
.login-content {
width: 100%;
position: relative;
}
@media (min-width: 768px)
.login-bg{
position: absolute;
width: 100%;
}

然后登录内容区域,宽度先固定1200px,相对定位。先保证在大屏幕情况下,正常显示,再去滚动条。

1
2
3
4
5
6
7
.login-layout{
width: 1200px;
margin: 0 auto;
overflow: hidden;
position: relative;
transition: width .25s linear;
}

注意最后一行,我还加了点别的东西–transition,这个小过渡主要是变化窗体宽度的时候触发。
有人会说我width固定了,也触发不了呀,下面还有一点东西。

1
2
3
4
@media (max-width: 1200px) and (min-width: 768px)
.login-layout {
width: 100%!important;
}

上面这句样式的作用就是用来动态改变登录内容区域宽度的。这句样式个人感觉还是蛮重要的,可以保证屏宽在这两者之间时,内容区域宽度动态变化,这样做的好处就在于里面的子元素相对其定位时,感觉上子元素所处的位置没有发生变化。
接下来看下登录框的样式。

1
2
3
4
5
.login-box-wrap {
position: absolute;
top: 40px;
right: 15%;
}

相对父元素(此处为登录内容区域),加一个top和right,以保证在宽度变化的时候,视觉上看登录框所处位置不变。
以上样式生效后,可以保证屏宽在768px以下时,只居中显示登陆框,在1200px以上时,正常显示。在两者之间时,屏宽有变化则登录窗体也跟着一起变化,不出现滚动条,背景不丢失,且登录框所处位置相对固定。

登录(功能)

h5提供一些基础的表单校验功能,其中有一条–必填检验(required)。而且允许用户自定义一些校验提示,还支持正则匹配。此特性规定,在校验不合法的情况下,不允许用户提交表单。
因此我在form内利用这些新知识,可以避免在没有输入用户及密码的情况下,进行表单提交,请求后台服务器。我不知道其他网站为何要在未输入用户名密码的情况下请求后台服务器,我觉得在我看来没有这个必要。

1
2
3
4
5
6
<input type = "text"
class = "form-control" required autofocus = "autofocus"
autocomplete="on" placeholder = "手机号/会员名/邮箱"
oninvalid = "setCustomValidity('请输入账号')"
oninput = "setCustomValidity('')"
/>

‘autofocus’属性可以让当前元素在页面初始化的时候,自动获取焦点。
‘autocomplete’设置为on可以让浏览器记住输入内容。
‘oninvalid’属性可以自己设定输入非法时的提示语。
‘oninput’属性表示输入时要执行的操作。
‘placeholder’属性表示占位时显示的字符。

这些都是普通的h5新特性,可以添加你们适用的属性来满足需求,个人感觉还比较方便。

安全

目前登录机制是利用cookie。有时候可能需要跳转到当前系统,即利用单点登录验证身份,此时是请求服务器产生一个唯一的一次可用的authId,携带这个authId的即视为合法用户。登录安全这块内容东西太多,做的时候需要考虑仔细点,以上就是登录页面我个人的看法。



money☜☜☜ wechat 『『『 reward 点击扫码打赏 ~~~ ^_^ 』』』alipay ☞☞☞ money



本文为Steve Souders的《High Performance Web Sites》读书笔记,主要介绍14条用于前端优化网站性能的建议。

Rule 1: Make Fewer HTTP Requests(减少HTTP请求)

  • Image Maps
    在看到这个名词之前,甚至没听说过它。稍微了解了下,知道大概就是可以用一张图片当成地图,然后点击图片上某一点,触发相应的事件。用到的场景可以是电商的服装广告上,比如一张模特图片,点击衣服,跳转衣服链接,点击鞋子,跳转鞋子链接。也可以是导航为图片的情况,点击导航对应的信息,相应执行动作。
    使用图片地图可以减少多张图片进行多次HTTP请求。
    example for no
    example for yes
  • CSS Sprites
    这个比较常用在图片图标情况下。
    example for yes
  • Inline Images
    可以使用“data:”内联图片,不用额外再去请求图片。
    example for html yes
    example for css yes
  • Combined Scripts and Stylesheets
    可以使用一些工具,如webpack/gulp,将脚本合并,样式表合并。虽然两者还可以合并,但不建议这么做。
    example for no
    example for yes

Rule 2: Use a Content Delivery NetWork(使用内容分发网络)

  • CND
    先要了解下CND–内容分发网络。它是一组分布在多个不同地理位置的Web服务器,可以更加有效的向用户发布内容。个人感觉最简单的解释就是可以让用户就近请求资源。

Rule 3: Add an Expires Header(添加Expires头)

  • Expires
    Web服务器使用Expires来告诉客户端该资源在某时间之前是有效的。客户端在服务端没有更新资源的情况下,可以在这段时间内一直使用之前请求到的资源的副本。
  • Max-Age & mod_expires
    使用Expires有一些限制,比如需要客户端和服务端的时钟需要严格同步,然后还要经常检查过期日期,一旦过期,还得提供一个新的日期。
    在HTTP1.1种,引入了Cache-Control。这时我们可以换一种方式,使用Cache-Control的Max-Age替代Expires。它表示资源可以被缓存多长时间,以s为单位。
    为了兼容,可以同时指定这两个响应头。HTTP规范规定,如果这两者同时存在,Max-Age将重写Expires。
  • Empty Cache vs Primed Cache
    如果你的页面中可缓存资源都在缓存中,则缓存是“完整的”。如果你页面中的资源没有放在缓存中,则缓存为“空”。这两者使用场景不一样,使用时,视情况而定。
  • More Than Just Images
    图片添加Expires很正常,我们也应该给其他不经常变化的资源添加Expires,比如脚本、样式表等。
  • Revving FileNames
    如果像上面那样给资源添加Expires,那么怎样让其获取更新呢?一般的实现是替换文件名,加一个版本信息后缀。

Rule 4: Gzip Components(压缩组件)

  • How Cpmpression Works
    Web客户端可以通过HTTP请求中的Accept-Encoding头来标识对压缩的支持。如果Web服务器看到请求中的这个头,就会使用客户端列出来所支持的一种压缩方法来响应。
    Web服务器通过响应中的Content-Encoding头来通知Web客户端。gzip是目前最流行和最有效的压缩方法,还有一种较普遍的压缩格式为deflate。支持deflate的浏览器也支持gzip,但支持gzip的浏览器却不一定支持deflate。

Rule 5: Put Stylesheets at the Top(将样式表放在顶部)

  • Progressive Rendering
    将样式表放在文档底部时,浏览器会延迟显示任何可视化的组件,这带来了很糟糕的用户体验,有时甚至会带来“白屏”现象。一般我们把样式表都放在文档的顶部。
  • Flash of Unstyled Content
    “白屏”现象源自于浏览器的行为。“FOUC”–“无样式内容的闪烁”,简单说就是当样式加载完成,某组件会突然加载上某个样式,出现闪烁现象。
    “白屏”现象是浏览器在尝试修改前端工程师所犯的错误–延迟呈现。
  • What’s a Frontend Engineer to do?
    如何避免“白屏”和“FOUC”现象呢?
    最佳的解决方案是遵循HTML规范,将样式表放在顶部。如果你的样式表不要求呈现页面,可以想办法在文档加载完毕后动态的加载进来,即“加载后下载”。

Rule 6: Put Scripts at the Bottom(将脚本放在底部)

  • Parallel Downloads
    对响应时间影响最大的是页面中组件的数量。浏览器可并行下载组件数量有限,可能4个,可能8个。修改浏览器设置可能可以增加并行下载数量。但这个取决于你的带宽和CPU速度,过多的并行下载可能反而会降低性能。
  • Scripts Block Downloads
    在下载脚本时,浏览器会阻塞并行下载。其中一个原因就是为了保证脚本能够按照正确的顺序执行。另一个原因是脚本可能使用document.write来修改页面内容,因此浏览器会等待,以确保页面能够恰当的布局。
  • Worst Case: Scripts at the Top
    如果将脚本放在页面顶部,页面中的所有东西都位于脚本之后,那么整个页面的呈现和下载都会被阻塞,直至脚本加载完毕。
  • Best Case: Scripts at the Bottom
    将脚本放在页面的底部是最佳的方案,既不会阻止页面内容的呈现,还可以让页面中的可视化组件尽早下载。
  • Putting it in Perspective
    如果某个脚本下载等待时间很长,但我们不想等待这么长时间。那么可以使用Defferred(延迟)脚本。DEFER属性表明脚本不包含document.write,浏览器可以继续呈现。

Rule 7: Avoid CSS Expressions(避免CSS表达式)

  • Working Around the Problem
    如果需要动态改变样式,可以使用事件处理器。

Rule 8: Make JavaScript and CSS External(使用外部JavaScript和CSS)

  • Inline
    内联快一些。
  • Component Reuse
    如果你的网站中的每个页面或者多个页面都使用了相同的JavaScript和CSS,使用外部文件可以提高这些组件的重用性,这时,使用外部文件更加具有优势。
  • Post-Onload Download
    我们希望为主页内联JavaScript和CSS,但又能为所有后续页面查看提供外部文件。这时可以通过在主页加载完成后动态下载外部组件来实现(通过onload事件),这能够将外部文件放到浏览器的缓存中以便用户接下来访问其他页面。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <script>
    function doOnload(){
    setTimeout(downloadComponents(),1000);
    }
    function downloadComponents(){
    downloadJS('http://xxx/xxx.js');
    downloadCSS('http://xxx/xxx.css');
    }
    function downloadJS(url){
    var elem = document.createElement('script');
    elem.src = url;
    document.body.appendChild(elem);
    }
    function downloadCSS(url){
    var elem = document.createElement('link');
    elem.rel = 'stylesheet';
    elem.type = 'text/css';
    elem.href = url;
    document.body.appendChild(elem);
    }
    window.onload = doOnload;
    </script>

Rule 9: Reduce DNS lookups(减少DNS查找)

  • DNS Caching and TTLs
    DNS查找可以被缓存起来以提高性能。
    在用户请求一个主机名之后,DNS信息会留在操作系统的DNS缓存中,之后对于该主机名的请求将无需进行过多的DNS查找。
    很多浏览器拥有其自己的缓存,和操作系统的缓存相分离。只要浏览器在其缓存中保留了DNS记录,它就不会麻烦操作系统来请求这个记录。

Rule 10: Minify JavaScript(精简JavaScript)

  • Minification
    精简,是从代码中移除不必要的字符以减少其大小,所有的注释以及不必要的空白字符将被移除。
  • Obfuscation
    混淆,同精简一样,它也会移除注释和空白,但同时它也会改写代码,它会使用更短的变量和函数名字替换原有内容。
  • Gzip and Minification
    使用gzip压缩文件,通常可以使文件大小减小70%,压缩比精简更能减小文件的大小。而且,混淆压缩与精简压缩,效率几乎一样。但使用精简可以避免混淆带来的额外风险。
  • Minifying CSS
    优化CSS,最大的潜在可能是,合并相同的类、移除不使用的类等。目前的解决方案是移除注释和空白,并进行一些直观的优化,比如使用缩写(‘#ccc’代替’#cccccc’)和移除不必要的字符串,如用’0’代替’0px’。

Rule 11: Avoid Redirects(避免重定向)

  • Types of Redirects
    重定向请求的响应中会包含一个范围在3xx的状态码。’304 Not Modified’并不是真的重定向–它是用来响应条件GET请求,避免下载已经存在于浏览器缓存中的数据。状态码’306’已经被废弃。
  • How Redirects Hurt Performance
    第一次请求URL时,接收到3xx重定向,然后请求此时的新url,而且可能会多次重定向,此时直到请求到预期想请求的资源已经经过了好几次http请求,影响性能。

Rule 12: Remove Duplicate Scripts(移除重复脚本)

  • Duplicate Scripts Hurt Performance
    重复脚本损伤性能的方式有两种–不必要的HTTP请求和执行JavaScript所浪费的时间。

Rule 13: Configure ETags(配置ETag)

  • What’s an ETag
    实体标签,是Web服务器和浏览器用于确认缓存组件的有效性的一种机制,在HTTP1.1中引入。
  • Last Modified Date
    服务器通过Last-Modified响应头来返回组件的最新修改日期,下一次请求资源时,浏览器会使用If-Modified-Since头将最新修改日期传回到服务器以进行比较。
    如果服务器上组件的最新修改日期与浏览器传回的值匹配,会返回一个304响应,而不会传送当前浏览器请求的资源。
  • The Problem with ETags
    当浏览器从一台服务器上获取了原始组件,之后又向另外一台不同的服务器发起GET请求时,ETag是不会匹配的,这对于使用服务器集群来处理请求的网站来说,是很常见的一种情况。

Rule 14: Make Ajax Cacheable(使Ajax可缓存)

  • Optimizing Ajax Requests
    当用户发起主动Ajax请求时,用户可能仍须等待,要改善性能,可以优化这些请求。
  • Caching Ajax in the Real World
    一个例子,Yahoo Mail。当用户启动Yahoo Mail的Ajax版本时,它会下载前三个Email消息的正文,这是一个聪明的主动Ajax请求,用户很有可能单击这些Email消息中的一个或几个,
    它们已经被下载到客户端,这样用户无需进行任务Ajax请求就能看到他们的Email消息。


money☜☜☜ wechat 『『『 reward 点击扫码打赏 ~~~ ^_^ 』』』alipay ☞☞☞ money



在JavaScript社区,CommonJS规范的提出,是模块化方面最重要的一笔。

CommonJS的模块实现

CommonJS的模块引用使用require,如下:

1
var http = require('http');

Node的模块实现

在Node中引入模块,需要经过下面3个步骤:

  • 路径分析;
  • 文件定位;
  • 编译执行。

主要用require()方法来查找模块。

AMD规范

AMD全称Asynchronous Module Definition,即“异步模块定义”。其模块引用方式如下:

1
define(id?,dependencies?,factory);

其中,id及依赖是可选的。其与CommonJS方式相似的地方在于factory的内容就是实际代码的内容,下面是一个简单的例子:

1
2
3
4
5
6
7
define(function(){
var exports = {};
exports.sayHello = function(){
alert('hello');
};
return exports;
});

CMD规范

CMD由国内大神玉伯提出,与AMD类似,区别主要在于定义模块和依赖引入的地方。AMD需要在声明模块时指定所有的依赖,通过形参传递依赖到模块内容中:

1
2
3
define(['dep1','dep2'],function(dep1,dep2){
return function(){};
});

与AMD相比,CMD更接近Node对CommonJS规范的定义:

1
define(factory);

在依赖部分,CMD支持动态引入,如下:

1
2
3
define(function(require,exports,module){
// Todo
});

require、exports、module通过形参传递给模块,在需要依赖模块时,随时调用require引入即可。

兼容多种模块规范

为了让同一个模块可以运行在前后端,在开发过程中需要考虑兼容前后端问题,以下代码演示如何将hello()方法定义到不同的运行环境中,它能够兼容AMD、CMD、Node以及常见的浏览器环境中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
;
(function (name, definition) {
// 检测上下文环境是否为AMD或CMD
var hadDefine = typeof define === 'function',
// 检测上下文环境是否为Node
hasExports = typeof module !== 'undefined' && module.exports;
if (hadDefine) {
// AMD/CMD
define(definition);
}
else if (hasExports) {
// Node
module.exports = definition();
}
else {
// 将模块的执行结果挂在全局变量(window)中
this[name] = definition();
}
})('hello', function () {
var hello = function () {};
return hello;
});

以上信息主要摘自书籍《深入浅出Nodejs》,有兴趣的朋友可以自己尝试前台、后台模块引入的异同,或者尝试这几种模块引入方式的异同。



money☜☜☜ wechat 『『『 reward 点击扫码打赏 ~~~ ^_^ 』』』alipay ☞☞☞ money



最近的新项目中,用户登录需要采用cookie来记住用户,校验身份。

在header中携带authId登录

在之前老的项目里,没有采用cookie来记录用户登录状态,而是在请求的header中携带一个身份标识来校验,大致方案如下:

  • 客户端使用post请求提交user、password给服务端进行登录操作;
  • 服务端校验用户是否合法,如果合法将产生一个唯一的身份标识authId,返回给客户端,客户端将此authId存放本地(如localStorage);
  • 客户端在每次需要校验身份的请求中,往header中加入这个authId;
  • 服务端检测当前的authId是否有效,有效则表示当前用户合法,允许操作;
  • 客户端用户登出的时候,发送一个delete请求,告诉服务端用户注销,同时删除本地的authId信息;
  • 服务端收到注销请求后,删除当前的authId数据。

上面的方案,如果其他客户端知道了这个authId后,可以在其他客户端模拟身份,不安全,因此弃用。

用cookie记住用户

新项目中,将采用此文即将介绍的方案–利用cookie来记住用户。主要流程是:

  • 客户端使用post请求提交user、password给服务端进行登录操作;
  • 服务端校验用户是否合法,如果合法将产生一个唯一的身份标识authId,以cookie的形式返回给客户端;
  • 客户端再次请求服务端时,会携带此前已经拿到的cookie给服务端,服务端校验是否合法,合法则可以继续操作;
  • 客户端用户登出的时候,发送一个delete请求,告诉服务端用户注销,服务端删除登录标识。
    整个过程可以用下面这张图简单表示:
    cookie传递流程图

前台用angular搭建单页客户端应用,后台用node搭建服务器,数据存放在mongodb中,这三个技术及cookie基础知识本文不做介绍,感兴趣的同学可以自行了解。
以下的代码都是最简单的get/post请求,但也是最核心的部分,其他有关登录的繁琐操作,感兴趣的同学可以自行补充。

从开始–>结束,遇到的问题

首先,我用的是最基础的post请求,服务端也只是简单的返回数据,部分简单但比较核心的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// client
$http({
method : 'POST',
url : 'http://127.0.0.1:8888/rest/user',
data : {name: 'xxx',password:'***'}
}).success(function (data) {
console.log('login success,data is:'+data);
}).error(function (data) {
console.log('login error');
}).then(function () {
console.log(arguments);
});
// server
var cookie = "authId=" + authId;
res.setHeader('Content-Type', 'application/json;charset=utf-8');
res.setHeader('Set-Cookie', cookie + ';Max-Age=3600;HttpOnly=false;Path=/;');
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end();

查看chrome调试,发现虽然服务端的cookie推过来了,但整体出了问题,提示如下:
不能跨域

1
XMLHttpRequest cannot load http://127.0.0.1:8888/rest/user. Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:62427' is therefore not allowed access.

分析问题后,发现原因是来自客户端的请求不能跨域访问服务端的请求,请求的资源header中没有携带允许跨越请求的信息。根据这个提示,我们把服务端的代码稍加改进后,如下:

1
2
3
4
5
6
7
8
9
10
11
// server
var cookie = "authId=" + authId;
res.setHeader('Content-Type', 'application/json;charset=utf-8');
res.setHeader('Set-Cookie', cookie + ';Max-Age=3600;HttpOnly=false;Path=/;');
// 添加允许跨越的头信息
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end();

解释下上面代码什么意思,第一句主要是允许来自任何域的请求访问,第二句是允许哪些类型的请求访问。加上后再次运行,提示如下:
没有携带支持Content-Type

1
XMLHttpRequest cannot load http://127.0.0.1:8888/rest/user. Request header field Content-Type is not allowed by Access-Control-Allow-Headers in preflight response.

原因是来自客户端的请求中,Content-Type头字段,在服务端的响应信息的头中,没有携带,再次修改代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// server
var cookie = "authId=" + authId;
res.setHeader('Content-Type', 'application/json;charset=utf-8');
res.setHeader('Set-Cookie', cookie + ';Max-Age=3600;HttpOnly=false;Path=/;');
// 添加允许跨越的头信息
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
// 添加支持Content-Type允许的头信息
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end();

再次运行代码,发现没有错误提示,但是当我们再次请求服务器时,发现客户端的请求并没有携带cookie信息,这显然不是我们想要的效果:
没有携带cookie的请求

在查阅了一段时间后了解到,客户端是会默认携带cookie给服务端的,但是当客户端的请求是跨域请求时,由于跨域请求本身就有风险,而携带给cookie同样有风险。
因此在进行跨域访问时,客户端不会将服务端返回的cookie携带。此时,我们需要同时在客户端和服务端都设置“withCredentials”为true,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// client
$http({
method : 'POST',
url : 'http://127.0.0.1:8888/rest/user',
withCredentials: true
data : {name: 'xxx',password:'***'}
}).success(function (data) {
console.log('login success,data is:'+data);
}).error(function (data) {
console.log('login error');
}).then(function () {
console.log(arguments);
});
// server
var cookie = "authId=" + authId;
res.setHeader('Content-Type', 'application/json;charset=utf-8');
res.setHeader('Set-Cookie', cookie + ';Max-Age=3600;HttpOnly=false;Path=/;');
// 添加允许跨越的头信息
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
// 添加支持Content-Type允许的头信息
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
// 设置已携带凭证为true
//res.setHeader('Access-Control-Allow-Credentials', true);
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end();

运行后,发现又有错误提示,如下:
允许跨越不能设置泛型“*”

1
XMLHttpRequest cannot load http://127.0.0.1:8888/rest/user. Response to preflight request doesn't pass access control check: A wildcard '*' cannot be used in the 'Access-Control-Allow-Origin' header when the credentials flag is true. Origin 'http://localhost:62427' is therefore not allowed access.

分析错误后发现,原因是当设置了已携带凭证参数为true时,允许跨域请求的源不能设置为泛型的“*”,因此我们再次修改代码如下:(最终代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// client
$http({
method : 'POST',
url : 'http://127.0.0.1:8888/rest/user',
withCredentials: true
data : {name: 'xxx',password:'***'}
}).success(function (data) {
console.log('login success,data is:'+data);
}).error(function (data) {
console.log('login error');
}).then(function () {
console.log(arguments);
});
// server
var cookie = "authId=" + authId;
res.setHeader('Content-Type', 'application/json;charset=utf-8');
res.setHeader('Set-Cookie', cookie + ';Max-Age=3600;HttpOnly=false;Path=/;');
// 添加允许跨越的头信息
// res.setHeader('Access-Control-Allow-Origin', '*');
// 用当前的客户端origin来取代泛型的“*”
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:62427');
res.setHeader('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
// 添加支持Content-Type允许的头信息
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
// 设置已携带凭证为true
res.setHeader('Access-Control-Allow-Credentials', true);
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end();

此时,第一次请求服务端时,服务端返回cookie信息,以后每次客户端请求服务端,客户端的header中都会携带cookie信息,效果如下图:
成功

最后

以上就是在使用cookie记住用户身份时遇到的一些问题及简单解决方法,一般在angular应用中,可能使用较多的是resoure进行http通信,此时只要在GET/POST/PUT/DELETE等请求的参数中,将“withCredentials”设置为true即可。



money☜☜☜ wechat 『『『 reward 点击扫码打赏 ~~~ ^_^ 』』』alipay ☞☞☞ money