HouXingYi's blog

即使跑得再远,也逃不出自己本身


  • 首页

  • 标签

  • 分类

  • 归档

mongoDB常用操作

发表于 2017-10-21 | 更新于 2018-07-29 | 分类于 后端

一些mongoDB常用的操作

基本概念

mongoDB中的一些基本概念与SQL的基本概念是不同的,这是我们学习mongoDB的时候需要注意与区分的。

下面一个表格带我们来理解下mongoDB中的一些概念。

SQL概念 mongoDB概念 说明
database database 数据库
table collection 数据库表/集合
row document 数据记录行/文档
column field 数据字段/域
index index 索引
table joins 表连接,mongoDB不支持
primary key primary key 主键,mongoDB自动将_id字段设置为主键

通过下图,更加直观的理解mongoDB的一些概念

展示

数据库

一个mongoDB可以建立多个数据库

通过命令show dbs来展现当前有多少数据库,名字分别叫什么

文档(document)

在mongoDB中文档就是一组键值对(key-value)。MongoDB 的文档不需要设置相同的字段,并且相同的字段不需要相同的数据类型,这与关系型数据库有很大的区别,也是 MongoDB 非常突出的特点。

需要注意的点

  1. 文档中的键/值对是有序的。
  2. 文档中的值不仅可以是在双引号里面的字符串,还可以是其他几种数据类型(甚至可以是整个嵌入的文档)。
  3. MongoDB区分类型和大小写。
  4. MongoDB的文档不能有重复的键。
  5. 文档的键是字符串。除了少数例外情况,键可以使用任意UTF-8字符。

集合(collection)

集合就是mongoDB文档组,类似关系型数据库中的表。一个数据库中有多个集合,可以用命令show collections来查看。集合没有固定的结构,这意味着你在对集合可以插入不同格式和类型的数据,但通常情况下我们插入集合的数据都会有一定的关联性。

MongoDB 数据类型

下表为MongoDB中常用的几种数据类型。

SQL概念 mongoDB概念
String 字符串。存储数据常用的数据类型。在 MongoDB 中,UTF-8 编码的字符串才是合法的。
Integer 整型数值。用于存储数值。根据你所采用的服务器,可分为 32 位或 64 位。
Boolean 布尔值。用于存储布尔值(真/假)。
Double 双精度浮点值。用于存储浮点值。
Min/Max keys 将一个值与 BSON(二进制的 JSON)元素的最低值和最高值相对比。
Array 用于将数组或列表或多个值存储为一个键。
Timestamp 时间戳。记录文档修改或添加的具体时间。
Object 用于内嵌文档。
Null 用于创建空值。
Symbol 符号。该数据类型基本上等同于字符串类型,但不同的是,它一般用于采用特殊符号类型的语言。
Date 日期时间。用 UNIX 时间格式来存储当前日期或时间。你可以指定自己的日期时间:创建 Date 对象,传入年月日信息。
Object ID 对象 ID。用于创建文档的 ID。
Binary Data 二进制数据。用于存储二进制数据。
Code 代码类型。用于在文档中存储 JavaScript 代码。
Regular expression 正则表达式类型。用于存储正则表达式。

创建数据库

直接使用use命令创建数据库。如果数据库不存在,则创建数据库,否则切换到指定数据库。

1
use DATABASE_NAME

使用show dbs展示当前已创建的数据库。

use数据库之后db命令表示当前数据库

删除数据库

使用下列命令删除数据库

1
db.dropDatabase()

使用下列命令删除集合

1
db.collection.drop()

插入数据

MongoDB 使用 insert() 或 save() 方法向集合中插入文档,语法如下:

1
db.COLLECTION_NAME.insert(document)

document采用json语法

3.2 版本后还有以下几种语法可用于插入文档:

  • db.collection.insertOne():向指定集合中插入一条文档数据
  • db.collection.insertMany():向指定集合中插入多条文档数据

更新文档

update() 方法用于更新已存在的文档。语法格式如下:

1
2
3
4
5
6
7
8
9
db.collection.update(
<query>,
<update>,
{
upsert: <boolean>,
multi: <boolean>,
writeConcern: <document>
}
)

参数说明:

  • query : update的查询条件,类似sql update查询内where后面的。
  • update : update的对象和一些更新的操作符(如$,$inc…)等,也可以理解为sql update查询内set后面的
  • upsert : 可选,这个参数的意思是,如果不存在update的记录,是否插入objNew,true为插入,默认是false,不插入。
  • multi : 可选,mongodb 默认是false,只更新找到的第一条记录,如果这个参数为true,就把按条件查出来多条记录全部更新。
  • writeConcern :可选,抛出异常的级别。

删除文档

remove() 方法的基本语法格式如下所示:

1
2
3
4
db.collection.remove(
<query>,
<justOne>
)

如果你的 MongoDB 是 2.6 版本以后的,语法格式如下:

1
2
3
4
5
6
7
db.collection.remove(
<query>,
{
justOne: <boolean>,
writeConcern: <document>
}
)

参数说明:

  • query :(可选)删除的文档的条件。
  • justOne : (可选)如果设为 true 或 1,则只删除一个文档。
  • writeConcern :(可选)抛出异常的级别。

另:

remove() 方法已经过时了,现在官方推荐使用 deleteOne() 和 deleteMany() 方法。

查询文档

MongoDB 查询数据的语法格式如下:

1
db.collection.find(query, projection)
  • query :可选,使用查询操作符指定查询条件
  • projection :可选,使用投影操作符指定返回的键。查询时返回文档中所有键值, 只需省略该参数即可(默认省略)。

如果你需要以易读的方式来读取数据,可以使用 pretty() 方法,语法格式如下:

1
>db.col.find().pretty()

pretty() 方法以格式化的方式来显示所有文档。

总结

以上是一些最基本的mongoDB的常用操作,若想看一些详情的文档和功能可以看官方的文档和一些教程。

参考

mongoDB官方文档

mongoDB教程

mongoose基础

发表于 2017-10-20 | 更新于 2018-07-29 | 分类于 后端

在Nodejs开发中,我们一般使用mongoose来操作mongoDB数据库。下面我么来学习下mongoose常用的用法。

准备工作

那要使用它,首先你得装上node.js和mongodb并启动mongodb数据库

Github地址:https://github.com/Automattic/mongoose

API Docs:http://mongoosejs.com/docs/guide.html

概念理解

要学习mongoose首先要了解三个重要的概念,他们是Schema、Model、Document。它们的关系是Schema生成Model、Model创造Document。

Schema类似于定义表结构,但并不完全准确。它用于创建表时的数据定义(不仅仅可以定义文档的结构和属性,还可以定义文档的实例方法、静态模型方法、复合索引等),每个Schema会映射到mongodb中的一个collection。Schema不具备操作数据库的能力,主要用于定义结构。

Model是由Schema编译而成的构造器,具有抽象属性和行为,可以对数据库进行增删查改。Model所能增删查改的具体值是由Schema定义的,Schema定义以外的值则没有效果。在mongoDB中一个数据库有多个collections(由Schema定义结构),而每个collections有多个document(类似js对象一般的键值对)。

Model的每一个实例(instance)就是一个文档document。Document是由Model创建的实体,它的操作也会影响数据库。

连接数据库

在使用前首先连接数据库(若没有则创建之)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var mongoose = require('mongoose');

mongoose.connect('mongodb://localhost:27017/test');
//已连接
mongoose.connection.on('connected', function () {
console.log('已连接');
});
//连接错误
mongoose.connection.on('error',function (err) {
console.log('出错');
});
//断开连接
mongoose.connection.on('disconnected', function () {
console.log('断开连接');
});

其它事件可以自行查看:http://mongoosejs.com/docs/api.html#connection_Connection

定义Schema

Schema是mongoose中需要事先定义的数据模式,类似于表结构,不过mongoDB中不叫表,叫collections。

模型可以看作mysql中的数据表,属性可以看作是字段,当然这个类比并不十分正确。

每个Schema对应当前连接的数据库的一个collection,若不存在则会自动创建。

Schema不具备操作数据库的能力。

下面我们来看下如何创建Schema

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var mongoose = require('mongoose');
Schema = mongoose.Schema;

var blogSchema = new Schema({
title: String,
author: String,
body: String,
comments: [{ body: String, date: Date }],
date: { type: Date, default: Date.now },
hidden: Boolean,
meta: {
votes: Number,
favs: Number
}
});

type有下面这些,String、Number、Boolean、Array、Buffer、Date、ObjectId、Mixed类型

生成Model

模型Model是根据Schema编译出的构造器,或者称为类,通过Model可以实例化出文档对象document。

下面根据Schema构建一个Model

1
var Blog = mongoose.model('Blog', blogSchema);

Model实例化之后为document。

1
2
3
var blogInstance = new Blog({
...
});

常用数据库操作

新增

document.save()

model实例化后的document可用save新增文档

1
2
3
4
5
6
7
8
9
10
11
12
13
//实例化model
var blogInstance = new Blog({
...
});
//调用save方法即插入一个新的document
user.save(function (err, res) {
if (err) {
console.log("Error:" + err);
}
else {
console.log("Res:" + res);
}
});

model.create()

使用save()方法,需要先实例化为文档,再使用save()方法保存文档。而create()方法,则直接在模型Model上操作,并且可以同时新增多个文档

1
2
3
4
Blog.create({name:"xiaowang"},{name:"xiaoli"},function(err,doc1,doc2){
console.log(doc1);
console.log(doc2);
});

查询

查询文档有以下三种方法

1
2
3
find()
findById()
findOne()

find()
第一个参数表示查询条件,第二个参数用于控制返回的字段,第三个参数用于配置查询参数,第四个参数是回调函数,回调函数的形式为function(err,docs){}

1
Model.find(conditions, [projection], [options], [callback])

findById()

1
Model.findById(id, [projection], [options], [callback])

findOne()
该方法返回查找到的所有实例的第一个

1
Model.findOne([conditions], [projection], [options], [callback])

更新

更新文档有以下两种方法

1
2
update()
updateMany()

update()
第一个参数conditions为查询条件,第二个参数doc为需要修改的数据,第三个参数options为控制选项,第四个参数是回调函数

1
Model.update(conditions, doc, [options], [callback])

updateMany()
updateMany()与update()方法唯一的区别就是默认更新多个文档,即使设置{multi:false}也无法只更新第一个文档

1
Model.updateMany(conditions, doc, [options], [callback])

删除

更新文档有以下三种方法

1
2
3
remove()
findOneAndRemove()
findByIdAndRemove()

remove()

remove有两种形式,一种是文档的remove()方法,一种是Model的remove()方法。

下面介绍Model的remove()方法,该方法的第一个参数conditions为查询条件,第二个参数回调函数的形式如下function(err){}。

1
model.remove(conditions, [callback])

下面介绍文档的remove()方法,该方法的参数回调函数的形式如下function(err,doc){}

1
document.remove([callback])

findOneAndRemove()
model的remove()会删除符合条件的所有数据,如果只删除符合条件的第一条数据,则可以使用model的findOneAndRemove()方法

1
Model.findOneAndRemove(conditions, [options], [callback])

findByIdAndRemove()

1
Model.findByIdAndRemove(id, [options], [callback])

前后钩子

前后钩子即pre()和post()方法,又称为中间件,是在执行某些操作时可以执行的函数。中间件在schema上指定,类似于静态方法或实例方法等

可以在数据库执行下列操作时,设置前后钩子

1
2
3
4
5
6
7
8
9
10
11
init
validate
save
remove
count
find
findOne
findOneAndRemove
findOneAndUpdate
insertMany
update

pre()
以find()方法为例,在执行find()方法之前,执行pre()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var schema = new mongoose.Schema({ age:Number, name: String,x:Number,y:Number});  
schema.pre('find',function(next){
console.log('我是pre方法1');
next();
});
schema.pre('find',function(next){
console.log('我是pre方法2');
next();
});
var temp = mongoose.model('temp', schema);
temp.find(function(err,docs){
console.log(docs[0]);
})
/*
我是pre方法1
我是pre方法2
{ _id: 5972ed35e6f98ec60e3dc886,name: 'huochai',age: 27,x: 1,y: 2 }
*/

查询后处理

常用的查询后处理的方法如下所示

1
2
3
4
5
6
7
sort     排序
skip 跳过
limit 限制
select 显示字段
exect 执行
count 计数
distinct 去重

例子

1
2
3
4
//按照age从小到大排列
temp.find().sort("age").exec(function(err,docs){
console.log(docs);
});

文档验证

如果不进行文档验证,保存文档时,就可以不按照Schema设置的字段进行设置,分为以下几种情况

  1. 缺少字段的文档可以保存成功
  2. 包含未设置的字段的文档也可以保存成功,未设置的字段不被保存
  3. 包含字段类型与设置不同的字段的文档也可以保存成功,不同字段类型的字段被保存为设置的字段类型

而通过文档验证,就可以避免以上几种情况发生

文档验证在SchemaType中定义,格式如下

1
{ name: {type:String, validator:value} }

常用验证包括以下几种

1
2
3
4
5
6
7
required: 数据必须填写
default: 默认值
validate: 自定义匹配
min: 最小值(只适用于数字)
max: 最大值(只适用于数字)
match: 正则匹配(只适用于字符串)
enum: 枚举匹配(只适用于字符串)

参考

Mongoose基础入门

Mongoose介绍和入门

HTTP与TCP/IP网络协议基础知识与重点

发表于 2017-10-11 | 更新于 2018-07-29 | 分类于 基础

参考

一篇文章带你详解 HTTP 协议

一篇文章带你熟悉 TCP/IP 协议

图解HTTP

图解TCP/IP

HTTP协议重点

1.常用的HTTP方法

  • GET: 用于请求访问已经被URI(统一资源标识符)识别的资源,可以通过URL传参给服务器。
  • POST:用于传输信息给服务器,主要功能与GET方法类似,但一般推荐使用POST方式。
  • PUT: 传输文件,报文主体中包含文件内容,保存到对应URI位置。
  • HEAD: 获得报文首部,与GET方法类似,只是不返回报文主体,一般用于验证URI是否有效。
  • DELETE:删除文件,与PUT方法相反,删除对应URI位置的文件。
  • OPTIONS:查询相应URI支持的HTTP方法。

2.GET方法与POST方法的区别

  • 区别一:

    get重点在从服务器上获取资源,post重点在向服务器发送数据;
  • 区别二:

    get传输数据是通过URL请求,以field(字段)= value的形式,置于URL后,并用”?”连接,多个请求数据间用”&”连接,如http://127.0.0.1/Test/login.action?name=admin&password=admin,这个过程用户是可见的;

    post传输数据通过Http的post机制,将字段与对应值封存在请求实体中发送给服务器,这个过程对用户是不可见的;
  • 区别三:

    Get传输的数据量小,因为受URL长度限制,但效率较高;

    Post可以传输大量数据,所以上传文件时只能用Post方式;
  • 区别四:

    get是不安全的,因为URL是可见的,可能会泄露私密信息,如密码等;

    post较get安全性较高;
  • 区别五:

    get方式只能支持ASCII字符,向服务器传的中文字符可能会乱码。

    post支持标准字符集,可以正确传递中文字符

3.HTTP请求报文与响应报文格式

  • 请求报文包含三部分:
  1. 请求行:包含请求方法、URI、HTTP版本信息
  2. 请求首部字段
  3. 请求内容实体
  • 响应报文包含三部分:
  1. 状态行:包含HTTP版本、状态码、状态码的原因短语
  2. 响应首部字段
  3. 响应内容实体

4.常见的HTTP相应状态码

  • 返回的状态

    1xx:指示信息–表示请求已接收,继续处理

    2xx:成功–表示请求已被成功接收、理解、接受

    3xx:重定向–要完成请求必须进行更进一步的操作

    4xx:客户端错误–请求有语法错误或请求无法实现

    5xx:服务器端错误–服务器未能实现合法的请求


  • 常见状态码

    200:请求被正常处理

    204:请求被受理但没有资源可以返回

    206:客户端只是请求资源的一部分,服务器只对请求的部分资源执行GET方法,相应报文中通过Content-Range指定范围的资源。

    301:永久性重定向

    302:临时重定向

    303:与302状态码有相似功能,只是它希望客户端在请求一个URI的时候,能通过GET方法重定向到另一个URI上

    304:发送附带条件的请求时,条件不满足时返回,与重定向无关

    307:临时重定向,与302类似,只是强制要求使用POST方法

    400:请求报文语法有误,服务器无法识别

    401:请求需要认证

    403:请求的对应资源禁止被访问

    404:服务器无法找到对应资源

    500:服务器内部错误

    503:服务器正忙

TCP/IP协议重点

1.计算机网络体系结构分层

分层模型

2.数据处理流程

数据处理流程

3.TCP连接建立、释放时的握手过程  

三次握手

  • TCP 提供面向有连接的通信传输。面向有连接是指在数据通信开始之前先做好两端之间的准备工作。
  • 所谓三次握手是指建立一个 TCP 连接时需要客户端和服务器端总共发送三个包以确认连接的建立。在socket编程中,这一过程由客户端执行connect来触发。


    下面来看看三次握手的流程图:
    流程图
  • 第一次握手:客户端将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给服务器端,客户端进入SYN_SENT状态,等待服务器端确认。
  • 第二次握手:服务器端收到数据包后由标志位SYN=1知道客户端请求建立连接,服务器端将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给客户端以确认连接请求,服务器端进入SYN_RCVD状态。
  • 第三次握手:客户端收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给服务器端,服务器端检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,客户端和服务器端进入ESTABLISHED状态,完成三次握手,随后客户端与服务器端之间可以开始传输数据了。


    四次挥手
  • 四次挥手即终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。在socket编程中,这一过程由客户端或服务端任一方执行close来触发。
  • 由于TCP连接是全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭。


    下面来看看四次挥手的流程图:
    流程图
  • 中断连接端可以是客户端,也可以是服务器端。
  • 第一次挥手:客户端发送一个FIN=M,用来关闭客户端到服务器端的数据传送,客户端进入FIN_WAIT_1状态。意思是说”我客户端没有数据要发给你了”,但是如果你服务器端还有数据没有发送完成,则不必急着关闭连接,可以继续发送数据。
  • 第二次挥手:服务器端收到FIN后,先发送ack=M+1,告诉客户端,你的请求我收到了,但是我还没准备好,请继续你等我的消息。这个时候客户端就进入FIN_WAIT_2 状态,继续等待服务器端的FIN报文。
  • 第三次挥手:当服务器端确定数据已发送完成,则向客户端发送FIN=N报文,告诉客户端,好了,我这边数据发完了,准备好关闭连接了。服务器端进入LAST_ACK状态。
  • 第四次挥手:客户端收到FIN=N报文后,就知道可以关闭连接了,但是他还是不相信网络,怕服务器端不知道要关闭,所以发送ack=N+1后进入TIME_WAIT状态,如果Server端没有收到ACK则可以重传。服务器端收到ACK后,就知道可以断开连接了。客户端等待了2MSL后依然没有收到回复,则证明服务器端已正常关闭,那好,我客户端也可以关闭连接了。最终完成了四次握手。

总结

以上的都是些网络协议方面的老生常谈的基础知识,其中内容十分不完整和全面。主要是要有一些网络方面的基础概念,遇到问题的时候懂得要到哪里去查,一些细的方面的知识可以等到实际应用中去体会。

[译]一个100行内的现代js路由

发表于 2017-10-10 | 更新于 2018-07-29 | 分类于 js
1
2
原文:http://krasimirtsonev.com/blog/article/A-modern-JavaScript-router-in-100-lines-history-api-pushState-hash-url
作者:Krasimir

(这个js路由现在被放项目Navigo中。这里还有一篇你可能会感兴趣的文章Deep dive into client-side routing)

现今到处都是流行的单页面应用(SPA)。这样的应用需要一个坚实的路由机制。像Emberjs这样的框架确实是在建立在一个路由类上的。我不确定这是不是我喜欢的概念,但我确定的是AbsurdJS需要一个内置的路由。并且这个路由在所有东西齐全的前提下,应该小巧、简单。那就让我们来看看这样一个模块是怎么样的。

要求

这个路由应该符合下面几点

  1. 应该小于100行
  2. 支持像http://site.com#products/list这样的hash类型的url
  3. 也能使用History API
  4. 提供易用的API
  5. 不会自动的运行
  6. 可选择监听改变

单例模式

我决定把路由做成只有一个实例。这也许是个坏决定,因为我就有个项目需要多个路由,但要知道这个不是常有的应用。如果我们使用单例模式,我们就不需要把路由从一个对象传到另一个对象并且我们也不需要创建它。我们只需要一个实例,这样我们就可以自动的创建它。

1
2
3
4
5
var Router = {
routes: [],
mode: null,
root: '/'
}

这里有三个我们需要的属性。

  • routes-这个保存着当前已注册的路由
  • mode-根据我们使用的是history还是hash显示’hash’或者’history’
  • root-应用的根URL路径,只有当我们使用pushState的时候我们才需要

配置

我们需要一个方法来初始化路由。我们只需要传两个东西,但是最好在一个函数内做这些。

1
2
3
4
5
6
7
8
9
10
11
var Router = {
routes: [],
mode: null,
root: '/',
config: function(options) {
this.mode = options && options.mode && options.mode == 'history'
&& !!(history.pushState) ? 'history' : 'hash';
this.root = options && options.root ? '/' + this.clearSlashes(options.root) + '/' : '/';
return this;
}
}

只有当我们想要使用history模式并且支持pushState的时候,mode才会等于’history’。否则我们将使用hash。root默认设置为单斜线’/‘。

获得当前的URL

这是我们的路由的重要部分,因为这将会告诉我们现在在什么地方。

1
2
3
4
5
6
7
8
9
10
11
12
getFragment: function() {
var fragment = '';
if(this.mode === 'history') {
fragment = this.clearSlashes(decodeURI(location.pathname + location.search));
fragment = fragment.replace(/\?(.*)$/, '');
fragment = this.root != '/' ? fragment.replace(this.root, '') : fragment;
} else {
var match = window.location.href.match(/#(.*)$/);
fragment = match ? match[1] : '';
}
return this.clearSlashes(fragment);
}

在两个例子中我们都是用了全局的window.location对象。在’history’模式版本中我们需要去掉URL的根部分。我们也需要去掉所有的GET请求参数,我们用如下的正则表达式搞定-(/\?(.*)$/)。获取hash的值则更加简单点。注意clearSlashes函数的作用。它的用处是去除字符串开头和结尾的斜杠。这是有必要的,因为我们不能强迫开发者使用特定格式的URL。无论他传什么都会转成同样的值。

添加和修改路由

当我在制作AbsurdJS的时候,我总是尝试尽可能多的给开发者控制权。几乎所有的路由插件在执行路由的时候是用字符串路由。然而我更喜欢传一个正则表达式。这更加的灵活,因为有的时候我们也许会做很奇怪的匹配。

1
2
3
4
5
6
7
8
add: function(re, handler) {
if(typeof re == 'function') {
handler = re;
re = '';
}
this.routes.push({ re: re, handler: handler});
return this;
}

这个将routes数组进行填充。如果只有一个函数传进来,那么这个函数就会被作为handler,而默认的路由就是一个空字符串。注意这里大部分的函数return this。这可以让我们链式调用方法。

1
2
3
4
5
6
7
8
9
remove: function(param) {
for(var i=0, r; i<this.routes.length, r = this.routes[i]; i++) {
if(r.handler === param || r.re.toString() === param.toString()) {
this.routes.splice(i, 1);
return this;
}
}
return this;
}

只有我们传一个正则表达式或者handler传给add方法的时候,路由的删除才会调用。

1
2
3
4
5
6
flush: function() {
this.routes = [];
this.mode = null;
this.root = '/';
return this;
}

有的时候我们需要重新初始化类。那么这时候你就可以用上面的flush方法。

登记

好,现在我们已经有了增加和删除URL的API。我们也可以得到当前的地址了。下一步我们要比较已注册的入口。

1
2
3
4
5
6
7
8
9
10
11
12
check: function(f) {
var fragment = f || this.getFragment();
for(var i=0; i<this.routes.length; i++) {
var match = fragment.match(this.routes[i].re);
if(match) {
match.shift();
this.routes[i].handler.apply({}, match);
return this;
}
}
return this;
}

我们通过传入函数作为参数或者调用getFragment方法来获得fragment。接下来我们执行一个单纯的循环来遍历routes来查看匹配。只有当正则表达式没有匹配到的时候,变量match的值是null。否则match的值会像下面这样

1
["products/12/edit/22", "12", "22", index: 1, input: "/products/12/edit/22"]

这是一个包含了匹配到的数组和所有可记录的子串的类数组。这意味着如果我们shift第一个我们会获得URL动态部分的数组。举例:

1
2
3
4
5
6
7
8
9
10
11
Router
.add(/about/, function() {
console.log('about');
})
.add(/products\/(.*)\/edit\/(.*)/, function() {
console.log('products', arguments);
})
.add(function() {
console.log('default');
})
.check('/products/12/edit/22');

这段脚本输出

1
products ["12", "22"]

到此为止我们处理了动态URL。

监听变化

当然我们不能总是调用check方法。我们需要当地址栏改变的时候,我们能收到通知。我所说的改变甚至意味着点击浏览器上的后退按钮。如果你用过History API的话你就会知道有一个popstate事件。当URL改变的时候就会触发这个事件。然而我发现在一些浏览器中,当页面加载完的时候会分发这个事件。这个情况加上其他的一些问题让我转向了其他的解决方案。因为我希望甚至在模式被设置为hash也能进行监听,我决定采用setinterval。

1
2
3
4
5
6
7
8
9
10
11
12
13
listen: function() {
var self = this;
var current = self.getFragment();
var fn = function() {
if(current !== self.getFragment()) {
current = self.getFragment();
self.check(current);
}
}
clearInterval(this.interval);
this.interval = setInterval(fn, 50);
return this;
}

我们需要把最近的URL存下来这样我们才能用于对比下一个。

改变URL

最后我们需要一个函数能改变当前的地址并触发路由handler

1
2
3
4
5
6
7
8
9
navigate: function(path) {
path = path ? path : '';
if(this.mode === 'history') {
history.pushState(null, null, this.root + this.clearSlashes(path));
} else {
window.location.href = window.location.href.replace(/#(.*)$/, '') + '#' + path;
}
return this;
}

同样的,我们根据不同的模式做不同的事。如果History API可用,我们使用pushState。否则我们就采用window.location。

最终源码

以下是最终源码加一点例子

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
var Router = {
routes: [],
mode: null,
root: '/',
config: function(options) {
this.mode = options && options.mode && options.mode == 'history'
&& !!(history.pushState) ? 'history' : 'hash';
this.root = options && options.root ? '/' + this.clearSlashes(options.root) + '/' : '/';
return this;
},
getFragment: function() {
var fragment = '';
if(this.mode === 'history') {
fragment = this.clearSlashes(decodeURI(location.pathname + location.search));
fragment = fragment.replace(/\?(.*)$/, '');
fragment = this.root != '/' ? fragment.replace(this.root, '') : fragment;
} else {
var match = window.location.href.match(/#(.*)$/);
fragment = match ? match[1] : '';
}
return this.clearSlashes(fragment);
},
clearSlashes: function(path) {
return path.toString().replace(/\/$/, '').replace(/^\//, '');
},
add: function(re, handler) {
if(typeof re == 'function') {
handler = re;
re = '';
}
this.routes.push({ re: re, handler: handler});
return this;
},
remove: function(param) {
for(var i=0, r; i<this.routes.length, r = this.routes[i]; i++) {
if(r.handler === param || r.re.toString() === param.toString()) {
this.routes.splice(i, 1);
return this;
}
}
return this;
},
flush: function() {
this.routes = [];
this.mode = null;
this.root = '/';
return this;
},
check: function(f) {
var fragment = f || this.getFragment();
for(var i=0; i<this.routes.length; i++) {
var match = fragment.match(this.routes[i].re);
if(match) {
match.shift();
this.routes[i].handler.apply({}, match);
return this;
}
}
return this;
},
listen: function() {
var self = this;
var current = self.getFragment();
var fn = function() {
if(current !== self.getFragment()) {
current = self.getFragment();
self.check(current);
}
}
clearInterval(this.interval);
this.interval = setInterval(fn, 50);
return this;
},
navigate: function(path) {
path = path ? path : '';
if(this.mode === 'history') {
history.pushState(null, null, this.root + this.clearSlashes(path));
} else {
window.location.href = window.location.href.replace(/#(.*)$/, '') + '#' + path;
}
return this;
}
}

// configuration
Router.config({ mode: 'history'});

// returning the user to the initial state
Router.navigate();

// adding routes
Router
.add(/about/, function() {
console.log('about');
})
.add(/products\/(.*)\/edit\/(.*)/, function() {
console.log('products', arguments);
})
.add(function() {
console.log('default');
})
.check('/products/12/edit/22').listen();

// forwarding
Router.navigate('/about');

总结

这个路由大概90行。支持hash类型的URL和新的history API。如果你不想仅仅想使用路由这个功能而使用整个框架,这个就对你是有帮助的。

这个类是AbsurdJS的一部分。在这里可以查看文档。

入门正则表达式

发表于 2017-08-10 | 更新于 2018-07-29 | 分类于 基础

资料

正则表达式30分钟入门教程
《JavaScript 闯关记》之正则表达式

正则表达式是什么

在编写处理字符串的程序或网页时,经常会有查找符合某些复杂规则的字符串的需要。正则表达式就是用于描述这些规则的工具。换句话说,正则表达式就是记录文本规则的代码。

为什么要用正则表达式

  • 密码、账号等验证。
  • 看源码需要,写框架,后端编程,路由都离不开正则

元字符

常用的正则表达式元字符

例子加深理解

  1. 例子一
  • 正则表达式:\bhi\b.*\bLucy\b
  • 含义:先是一个单词hi,然后是任意个任意字符(但不能是换行),最后是Lucy这个单词
  • 知识点:
    1. \b:代表着单词的开头或结尾,也就是单词的分界处
    2. .:匹配除了换行符以外的任意字符
    3. *:它指定前边的内容可以连续重复使用任意次以使整个表达式得到匹配
  1. 例子二
  • 正则表达式:0\d\d-\d\d\d\d\d\d\d\d 或 0\d{2}-\d{8}
  • 含义:以0开头,然后是两个数字,然后是一个连字号“-”,最后是8个数字
  • 知识点:
    1. \d:匹配一位数字
    2. {n}:匹配必须连续重复匹配2次
  1. 例子三
  • 正则表达式:\ba\w*\b
  • 含义:a开头的单词——先是某个单词开始处(\b),然后是字母a,然后是任意数量的字母或数字(\w*),最后是单词结束处(\b)
  • 知识点:
    1. \w:匹配字母或数字或下划线或汉字等
    2. \s:匹配任意的空白符,包括空格,制表符(Tab),换行符,中文全角空格等(虽然这里没有)

还有一些元字符

  1. ^:匹配匹配字符串的开始
  2. $:匹配匹配字符串的结束
  3. {n,m}:重复的次数不能少于n次,不能多于m次
  4. \:标记为一个元字符、或一个原义字符、或一个 向后引用、或一个八进制转义符
  5. 重复:

    代码/语法 | 说明

    • | -
    • | 重复零次或更多次
    • | 重复一次或更多次
      ? | 重复零次或一次
      {n} | 重复n次
      {n,} | 重复n次或更多次
      {n,m} | 重复n到m次
  6. 字符类

    匹配没有预定义元字符的字符集合 : 只需要在方括号里列出它们就行了

    如:[aeiou]就匹配任何一个英文元音字母

一些稍微高级的话题

  1. 分枝条件:|类似于编程中的或,指的是有几种规则,如果满足其中任意一种规则都应该当成匹配
  2. 分组:用小括号来指定子表达式
  3. 反义:

    代码/语法 | 说明

    • | -
      \W | 匹配任意不是字母,数字,下划线,汉字的字符
      \S | 匹配任意不是空白符的字符
      \D | 匹配任意非数字的字符
      \B | 匹配不是单词开头或结束的位置
      [^x] | 匹配除了x以外的任意字符
      [^aeiou] | 匹配除了aeiou这几个字母以外的任意字符
  4. 后向引用:使用小括号指定一个子表达式后,匹配这个子表达式的文本(也就是此分组捕获的内容)可以在表达式或其它程序中作进一步的处理。默认情况下,每个分组会自动拥有一个组号。
    后向引用用于重复搜索前面某个分组匹配的文本。例如,\1代表分组1匹配的文本。

  5. 零宽断言:查找在某些内容(但并不包括这些内容)之前或之后的东西。
    (?=exp)也叫零宽度正预测先行断言,它断言自身出现的位置的后面能匹配表达式exp。比如\b\w+(?=ing\b),匹配以ing结尾的单词的前面部分(除了ing以外的部分),如查找I’m singing while you’re dancing.时,它会匹配sing和danc。
  6. 负向零宽断言:确保某个字符没有出现,但并不想去匹配它
  7. 贪婪与懒惰:当正则表达式中包含能接受重复的限定符时,通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能多的字符。有时,我们更需要懒惰匹配,也就是匹配尽可能少的字符。前面给出的限定符都可以被转化为懒惰匹配模式,只要在它后面加上一个问号?。
  8. 平衡组/递归匹配

[译]在10分钟内解释JavaScript Async/Await

发表于 2017-07-15 | 更新于 2018-07-29 | 分类于 翻译
1
2
原文:https://tutorialzine.com/2017/07/javascript-async-await-explained
作者:Danny Markov

在很长的一段时间里,JavaScript开发者不得不依靠回调函数去处理异步代码。结果,我们大部分都经历过回调地狱和遇到这样功能的恐怖。

幸运的是,接下来(或者我们应该说 .then())我们迎来了Promises. 他们提供了一个更组织化的回调方式,大多数社区迅速的转而使用它。

现在,随着最新版本的Async/Await出现,写JavaScript代码更爽了。

什么是Async/Await?

Async/Await是一个很久就令人期待的JavaScript功能,它让使用异步函数更加愉快和容易理解。它是基于Promises的并且和现存的所有基于Promises的API相兼容。

从async和await这两个名字来的这两个关键字将会帮助我们整理我们的异步代码。

Async - 声明一个异步函数(async function someName(){…})。

  • 自动将一个常规函数转化为一个Promise。
  • 当调用async函数的时候,它用函数内返回的任何值来解决(resolve)。
  • 异步函数可以使用await

Await-暂停执行async函数(var result = await someAsyncCall();)。

  • 当放在一个Promise前面执行,await强制剩下的代码等待直到那个Promise结束并且返回一个结果
  • Await只有和Promise一起使用才有用,和回调函数(普通函数)一起使用不会产生作用
  • Await只可以在async函数内部使用

这里有一个简单的例子,希望能帮你把事情理清楚:

假设我们想从我们的服务器中得到一些JSON文件。我们将写一个函数用axios库发送一个http GET 请求到https://tutorialzine.com/misc/files/example.json。 我们不得不等待服务器响应,所以很自然这个HTTP 请求是异步的。

下面我们可以看到相同的功能实现了两次。首先是用Promises,然后第二次用Async / Await。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Promise 实现方式
function getJSON(){

// 为了让函数阻塞我们手动创建了一个Promise
return new Promise( function(resolve) {
axios.get('https://tutorialzine.com/misc/files/example.json')
.then( function(json) {
// 从请求来的数据可以从.then中得到
// 我们使用resolve返回结果
resolve(json);
});
});

}

// Async/Await 实现方式
// async关键字将会自动创建一个新的Promise并返回。
async function getJSONAsync(){
// await关键词使我们免于写一个.then()
let json = await axios.get('https://tutorialzine.com/misc/files/example.json');
// GET请求的结果可以从json变量中得到
// 我们把结果就像一个普通的同步函数一样返回
return json;
}

很明显Async/Await版本的代码更短并且可读性更强。除了使用的语法,两个函数完全相同-他们都返回Promises并且都从axios得到JSON返回。我们可以像这样调用我们的async函数:

1
2
3
getJSONAsync().then( function(result) {
// Do something with result.
});

那么,Async/Await 让 Promises 过时了吗?

一点也不,当我们使用Async/Await其实底层还是在使用Promises。对Promises的良好理解在长远考虑下是对你是十分有帮助的并且也是高度推荐的。

这里甚至有一些情况Async/Await不能解决而我们不得不重新去寻求Promises的帮助。一种这样的场景是我们需要去调用多个独立的异步函数并等待他们所有完成。

假如我们尝试用async and await,以下就会发生:

1
2
3
4
5
6
async function getABC() {
let A = await getValueA(); // getValueA takes 2 second to finish
let B = await getValueB(); // getValueB takes 4 second to finish
let C = await getValueC(); // getValueC takes 3 second to finish
return A*B*C;
}

每一个await将会等待前一个返回一个结果。因为我们一次只执行一个调用那么整个函数从开始到结束将会花9秒的时间(2+4+3)。

这不是一个最佳的解决方案,因为A,B和C互相并不依赖。换句话来说我们在得到B的时候我们并不需要A的值。我们可以同时得到这些值以减去几秒钟的等待时间。

同时发送所有的requests我们需要Promise.all()。这将会保证我们在进行下一步的时候我们可以得到所有的结果,但是所有的异步函数将会平行的运行,而不是一个接一个。

1
2
3
4
5
6
async function getABC() {
// Promise.all() allows us to send all requests at the same time.
let results = await Promise.all([ getValueA, getValueB, getValueC ]);

return results.reduce((total,value) => total * value);
}

这种方式函数将会花费少的多的时间,在getValueB结束的时候getValueA和getValueC就已经结束了。我们将有效的减少执行的时间到最慢的请求而不是时间的总和。

处理Async/Await的错误

另一个关于Async/Await很棒的事是它允许我们用很棒的老式try/catch块去catch任何意外的错误。

1
2
3
4
5
6
7
8
9
async function doSomethingAsync(){
try {
// This async call may fail.
let result = await someAsyncCall();
}
catch(error) {
// If it does we will catch the error here.
}
}

任何我们写在try块中的等待的异步调用或者其他任何错误代码,catch都能解决他们引起的错误(error)。

如果情况需要,我们也可以在执行async函数的时候抓取错误。由于所有异步函数返回Promises,所以我们可以在调用它们时简单地包含.catch()事件处理程序。

1
2
3
4
5
6
7
8
9
10
11
// 不包含try/catch块的async函数
async function doSomethingAsync(){
// 这个async调用也许会失败
let result = await someAsyncCall();
return result;
}

// 我们catch错误在调用async函数的时候
doSomethingAsync().
.then(successHandler)
.catch(errorHandler);

重要的是选择一种你喜欢的错误处理的方式,并坚持使用它。同时使用try/catch和.catch()将很有可能导致一些问题。

浏览器支持

Async/Await已经支持大部分的主流浏览器。绝大多数的厂商将会识别你的async/await代码而不需要额外的库-除了IE11.

Node开发者只要node8或以上就可以享受到改进的异步流。它在今年晚些时候应该会变成LTS( Long Term Support )版本。

如果这兼容性不能满足你,也有许多像Babel和TypeScript和Nodejs库asyncawait这样的JS transpiler(源码转换器)提供他们自己的跨平台版本的功能。

结论

随着Async/Await ,JavaScript语言在代码可读性和易用性上向前迈进了一大步。能够编写类似于常规同步功能的异步代码将会受到JavaScript初学者和经验丰富的编程人员的赞赏。

理解JS的this关键字

发表于 2017-06-10 | 更新于 2018-07-29 | 分类于 前端

this是什么

this是JavaScript中一个关键字,this提供了一种优雅的方式来隐式传递一个对象引用,因此可以把API设计得更加简洁并且易于复用。
但是,在平常使用中经常会对this产生一些误解,比较常见的会认为this“指向自身”或者“指向函数的作用域”

指向自身

对于这个误解可以看下以下代码

1
2
3
4
5
6
7
8
function foo(){
this.a = 1;
}
foo.a = 0;
foo();
console.log(foo.a);//0
console.log(this.a);//1
console.log(window.a);//1

通过以上代码可以看到,this并不指向foo而是指向了window,可见this指向自身并不成立

指向函数的作用域

这也是个常见的误解,请看一下代码

1
2
3
4
5
6
7
8
function foo(){
var a = 1;
bar();
}
function bar(){
console.log( this.a );
}
foo();//undefined

这段代码,希望利用this向上查找到a,代替作用域的效果,但并没有成功。

this究竟是什么

通过以上一些例子,我们可以看到,this的指向飘忽不定,在不同的场景下,this会化身不同的对象。如果不了解this的机制就很难猜到this的指向。
那么this究竟是什么?
先上结论:实际上,this是在运行的时候进行绑定的,并不是在编写时绑定,它指向什么完全取决于函数在哪里被调用。

this绑定规则

要确定this的值,首先要看函数的调用位置。找到调用位置后,需要判断应用下面四条规则的哪一条

默认绑定

这条规则是无法应用其他规则时的默认规则,请看以下代码

1
2
3
4
5
function foo(){
console.log( this.a );
}
var a = 2;
foo();//2

在这里,foo()是直接进行调用,因此只能使用默认绑定,this指向全局对象。

隐式绑定

这种情况是函数是被某个上下文对象调用的,比如下面的代码

1
2
3
4
5
6
7
var o = {
a:3,
foo:function(){
console.log(this.a);
}
}
o.foo();

隐式丢失

隐式绑定一个常见的问题是被隐式绑定的函数会丢失绑定对象,比较常见的情况如下

1
2
3
4
5
6
7
8
9
10
11
12
function foo(){
console.log(this.a);
}
function doFoo(fn){
fn();
}
var obj = {
a : 2,
foo : foo
}
var a = "global";
doFoo(obj.foo);//global

这里没有如预想的应用隐式绑定,而是应用了默认绑定,将this绑定到全局对象上了,这种情况在应用隐式绑定的时候经常遇到。

显式绑定

显式绑定通过call()或apply()方法,用于显式的指定this的绑定对象

1
2
3
4
5
6
7
function foo(){
console.log(this.a);
}
var obj = {
a : 2
}
foo.call(obj);// 2

硬绑定

但是,显式绑定并没有解决我们之前提出的丢失绑定问题,但是显式绑定的一个变种可以解决这个问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function foo(something){
console.log( this.a,something );
return this.a + something;
}
function bind(fn,obj){
return function(){
return fn.apply(obj,arguments);
}
}
function doFoo(fn,arg){
fn(arg);
}
var obj = {
a:2
};
var bar = bind(foo,obj);
doFoo(bar,3);// 2 3

这里,我们创建了一个bind函数用于绑定函数与上下文对象,这种模式称为硬绑定。
ES5中提供了内置的bind方法

new绑定

在JS中,对函数使用new操作符的时候,我们称此函数为构造函数,或者更准确的说,对这个函数进行了“构造调用”。
使用new来调用函数,会自动执行下面的操作。

  1. 创建一个全新的对象
  2. 这个新对象会被执行[[Prototype]]连接(新对象的proto指向函数的原型)
  3. 这个新对象会绑定到函数调用的this
  4. 如果函数没有返回,自动返回这个新对象

new是最后一种可以影响函数调用时this绑定行为的方法,我们称之为new绑定

优先级

现在我们已经了解了函数调用中this绑定的四条规则,你需要做的就是找到函数的调用位置并判断应当应用哪条规则。如果某个调用位置可以应用多条,我们按照优先级应用这四条规则。

  1. 函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。
  2. 函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。
  3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。
  4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。

结束

一般情况下通过以上四种规则就可以理解大部分的this的使用情况。当然除了这些一般的情况下也有一些例外,比如ES6的箭头函数中的this会继承外层函数调用的this绑定。碰到这些例外情况就具体情况具体分析了。

毕业了

发表于 2017-05-20 | 更新于 2018-07-29 | 分类于 日常

留些纪念

毕业了,心里挺复杂的,但也不是悲伤,愿大家前程似锦。贴些照片留作纪念。

照片

memory

memory

memory

memory

memory

memory

memory

memory

memory

memory

memory

memory

memory

memory

memory

memory

memory

诶嘿!哪个是我呢!
memory

12
HouXingYi

HouXingYi

18 日志
8 分类
17 标签
© 2019 HouXingYi
由 Hexo 强力驱动 v3.7.1
|
主题 – NexT.Pisces v7.0.1