前端森林

前端森林 查看完整档案

上海编辑  |  填写毕业院校  |  填写所在公司/组织 github.com/Cosen95 编辑
编辑

个人动态

前端森林 收藏了文章 · 2019-01-09

koa2 使用passport权限认证中间件

做后端系统避免不了要做权限认证,比如本地用户登录,第三方登录。
权限认证的思路也极其简单,不外乎就是登录,登出,路由守护三部分。

那么有没有现成的轮子可用呢?答案是肯定的,node发展了这么迅速,各种npm包层出不穷,总有那么几款厉害的。
今天要讲的权限认证中间件那就是:passport

passport目前有很多已经写好的登录策略,比如github登录,微信登录,Facebook登录,google等等。

官网 http://passportjs.org/docs/

官网是英文的,英文差的话不建议看了,去找个demo撸起来才是正确的学习思路。

通过一阵摸索,本文决定记录下koa2具体的使用步骤。

安装包

koa2中使用的是 koa-passport 这个包。
本地验证用的是 passport-local这个策略

npm install -S koa-passport

代码

先来看代码,稍后再做解释。
这里使用 passport-local 策略(本地权限认证)为例子。
因为passport使用之前要定义策略及序列化与反序列化操作,所以把 passport 的配置及策略写到一个文件passport.js

定义策略

// passport.js
const passport = require('koa-passport')
var LocalStrategy = require('passport-local').Strategy


// 序列化ctx.login()触发
passport.serializeUser(function(user, done) {
  console.log('serializeUser: ', user)
  done(null, user.id)
})
// 反序列化(请求时,session中存在"passport":{"user":"1"}触发)
passport.deserializeUser(async function(id, done) {
  console.log('deserializeUser: ', id)
  var user = {id: 1, username: 'admin', password: '123456'}
  done(null, user)
})
// 提交数据(策略)
passport.use(new LocalStrategy({
  // usernameField: 'email',
  // passwordField: 'passwd'
}, function(username, password, done) {
  console.log('LocalStrategy', username, password)
  var user = {id: 1, username: username, password: password}
  done(null, user, {msg: 'this is a test'})
  // done(err, user, info)
}))


module.exports = passport

记得文件末 module.exports = passport 导出 passport

入口载入

然后在 koa 入口 app.js 中载入 passport.js 文件

const passport = require('./passport')

并在适当位置(看下边 app.js)使用passport中间件

app.use(passport.initialize())
app.use(passport.session())

passport 中间件需要用到 session ()所以,你的app.js入口文件类似这样

// app.js
const Koa = require('koa')
const bodyParser = require('koa-bodyparser')
const static = require('koa-static')
const session = require('koa-session')
const RedisStore = require('koa-redis')
const app = new Koa()


const passport = require('./libs/passport')
const baseConf = require('./config/base')
const redisConf = require('./config/redis')

// 基础中间件
app.use(async (ctx, next) => {
  const start = Date.now()
  await next()
  const ms = Date.now() - start
  console.log(`${ctx.method} ${ctx.status} ${ctx.url} - ${ms} ms`)
})


app.keys = ['123456']
app.use(bodyParser())
app.use(session({
  cookie: {secure: false, maxAge:86400000},
  store: RedisStore(redisConf.session)
}, app))


app.use(passport.initialize())
app.use(passport.session())

var router = require('./routes')

router.all('404', '*', ctx => {
  ctx.status = 404
  ctx.body = '404'
})

app.use(router.routes())
app.use(router.allowedMethods())

var log = require('./libs/log')
app.on('error', (err, ctx) => {
  log.error(`${ctx.method} ${ctx.url}`, 'Error: ')
  log.error(err)
  console.log(err)
})

app.listen(baseConf.port)
console.log('listening on ' + baseConf.port)
module.exports = app

编写路由

编写路由及守护中间件。

  • 登录
POST /login
router.post('/login', ctx => {
  // 会调用策略
  return passport.authenticate('local',
    function(err, user, info, status) {
      ctx.body = {user, err, info, status}
      return ctx.login({id: 1, username: 'admin', password: '123456'})
    })(ctx)
})
  • 登出
GET /logout
router.get('/logout', ctx => {
  ctx.logout()
  ctx.body = {auth: ctx.isAuthenticated(), user: ctx.state.user}
})
  • 路由守护中间件

比如你/api/*的路由需要用户认证才能访问

router.use('/api/*', (ctx, next) => {
   if(ctx.isAuthenticated()) {
     next()
   } else {
    ctx.status = 401
    ctx.body = {
      msg: 'auth fail'
    }
  }
})

到这里,本地权限认证基本完成了,post请求 /login 并且提交表单username,和 password即可登录一个用户。

/logout 退出当前登录。

解释

使用 passport 这个中间件,必须了解其运行步骤和知识点。

passport 以策略来扩展验证,什么是策略呢?

比如:本地策略,github登录策略,微信登录策略

passport 中间件使用前,需要注册策略,及实习序列化与反序列化操作。

序列化

通过 passport.serializeUser 函数定义序列化操作。

// 序列化
passport.serializeUser(function(user, done) {
  done(null, user.id)
})

在调用 ctx.login() 时会触发序列化操作。

反序列化

通过 passport.deserializeUser 函数定义反序列化操作。

// 反序列化
passport.deserializeUser(async function(id, done) {
  console.log('deserializeUser: ', id)
  var user = {id: 1, username: 'admin', password: '123456'}
  done(null, user)
})

在请求时,session中如果存在 "passport":{"user":"xxx"}时会触发定义的反序列化操作。

注册策略

// 策略
passport.use(new LocalStrategy({
  // usernameField: 'email',
  // passwordField: 'passwd'
}, function(username, password, done) {
  var user = {id: 1, username: username, password: password}
  done(null, user)
}))

在使用 passport.authenticate('策略', ...) 的时候,会执行策略

其他

app.use(passport.initialize()) 会在请求周期ctx对象挂载以下方法与属性

  • ctx.state.user 认证用户
  • ctx.login(user) 登录用户(序列化用户)
  • ctx.isAuthenticated() 判断是否认证

github

另外附上github的认证代码

安装包

npm install -S passport-github

passport.js载入

var GitHubStrategy = require('passport-github').Strategy

passport.js 增加代码

passport.use(new GitHubStrategy({
    clientID: githubConf.clientId,
    clientSecret: githubConf.secret,
    callbackURL: githubConf.callback
  },
  function(accessToken, refreshToken, profile, done) {
    // console.log(accessToken, refreshToken, profile)
    return done(null, {accessToken, refreshToken, profile})
  }
))

添加两个路由

// 调用授权页面
router.get('/auth/github', ctx => {
  return passport.authenticate('github', {scope: ['user:email']})(ctx)
})
// 授权回调得到code
router.get('/auth/github/callback', async ctx => {
  return passport.authenticate('github', (err, user, info, status) => {
    ctx.body = {err, user, info, status}
    return ctx.login(user)
  })(ctx)
})

以上例子只是模拟,并没有涉及数据库的操作,具体的实现还需要自己按照业务需求实现。

passport使用session来维护会话。对于token验证的来说,并不能用,所以要实现token验证的话还需要另外编写策略才行。

更多详细用法,请自行到官网查看文档。

查看原文

前端森林 发布了文章 · 2018-11-25

socket踩坑实录

socket简述

socket(双工协议)
网络中的两个程序,通过一个双向的连接来实现数据的交换,我们把连接的一端称为socket

图片描述

socket特性

自带连接保持

可以实现双向通信

socket分类

基于TCP的socket

基于UDP的socket

基于RawIP的socket

基于链路层的socket

socket实战

socketClient.js

var net = require('net');
var hostname = '127.0.0.1';
var port = 9000;

var client = new net.Socket();
client.setEncoding('utf-8');

// 与服务端建立连接
client.connect(port,hostname,function() {
    client.write('你好啊');
})

client.on('data',function(data) {
    // console.log(data);
    say()

})

// 逐行读取
const readline = require('readline');
// 建立通信标准
var r1 = readline.createInterface({
    input:process.stdin, //input标准
    output:process.stdout //out 标准
})

function say(){
    r1.question('请输入:',function(inputStr) {
        if(inputStr === 'bye') {
            client.destroy()
            console.log('客户端的socket退出')
        } else {
            client.write(inputStr + '\n')
        }
    })
}

socketServer.js

var net = require('net');
var clientServer = net.createServer();
var clientMap = new Object;

// 加一个标识
var i = 0;

// 接收客户端的连接
clientServer.on('connection',function(client) {
    console.log('亲 客户端发送过来一条信息');
    client.name = i++;
    clientMap[client.name] = client;
    client.on("data",function(data) {
        console.log(`客户端传来${data}`)
        client.write('服务端发送再见')
        broadcast(data,client)
    })
})

function broadcast(data,client){
    for (key in clientMap) {
        clientMap[key].write(client.name+"说:"+data)
    }
}


clientServer.listen(9000);

websocketClient.js

var ws = new WebSocket('ws://127.0.0.1:3000');

// 初始化
ws.onopen = function(){
    ws.send('你好哇')
}

ws.onmessage = function(event) {
    var chatRoom = document.getElementById('chatRoom');
    chatRoom.innerHTML += '<br />' + event.data;
}

websocketServer.js

var WebsocketServer = require('ws').Server;

var wss = new WebsocketServer({
    port: 3000
})

var clientMap = new Object;
var i = 0;
wss.on('connection',function(client) {
    console.log('亲,服务端发送过来一条信息');
    client.on('message',function(data) {
        client.name = i++;
        clientMap[client.name] = client;
        broadcast(data,client)
    })
})

function broadcast(data,client) {
    for (key in clientMap) {
        clientMap[key].send(client.name+'说:'+data)
    }
}

文章持续更新中~~~~

查看原文

赞 3 收藏 2 评论 0

前端森林 赞了文章 · 2018-11-15

#关于setTimeout的妙用

关于setTimeout的妙用

定义

在指定的延迟时间之后调用一个函数或执行一个代码片段

这个是setTimeout最主要的功能,但也是很坑的地方,首先javascript其实是运行在单线程的环境下,意味者定时器会在未来的某个时间支持,但是具体的执行的时间并不能够很准确的运行,容易受到其它时间的影响,比如DOM的计算、事件的执行、各种函数的执行和运算。但是我们可以利用延迟执行的一些特性来达到我们想要的功能。

    window.setTimeout(callbask, delay, [param1, param2, ...])

函数节流

程序运行过程中对某个函数调用次数很多导致界面不断回流或者重排,这时候就需要对这个函数进行节流,比如监听touchmove、scroll事件等,可以限定函数在单位时间内只执行一次

    var throttle = function(method, context) {
        clearTimeout(method.tId);
        method.tId = setTimeout(function(){
            method.call(context);
        },100);
    }

控制执行

点击事件是会产生冒泡,执行的顺序是当前点击元素->父元素->...->document,一直往上执行到document。如果对当前元素的点击事件处理加上setTimeout的话,那么执行顺序就会改变为父元素->...->document->当前元素。这样就可以控制冒泡事件的执行顺序

    var parent = document.getElementById("parent");
    var child = document.getElementById("child");

    document.body.addEventListener("click", function(e) {
        console.log("body");

    }, false);

    parent.addEventListener("click", function(e) {
        console.log("parent");
    }, false);
    
    child.addEventListener("click", function(e) {
        setTimeout(function() {
            console.log("child");
    
        }, 0);
    }, false);
    
    /*
    *parent
    *body
    *child
    */

构建一个轮询任务

javacsript中已经有一个开启轮询的方法,就是setInterval,但是这种存在一个问题就是执行间隔往往就不是你希望的间隔时间

比如有个轮询任务间隔是100ms,但是执行方法的时间需要450ms,那么在200ms、300ms、400ms本来是计划中执行任务的时间,浏览器发现第一个还未执行完,那么就会放弃2、3、4次的任务执行,并且在500ms之后再次执行任务,这样的话,其实再次执行的间隔就只有50ms。使用setTimeout构造轮询能保证每次轮询的间隔

    setTimeout(function(){
        alert(1);
        setTimeout(arguments.callee, 100);
    },100)

分块处理耗时计算

浏览器给javascript的运行规定了内存和时间,假如javascript运行的时间超过了规定时间,那么浏览器就会''报错''

    for(var i = 0; i < arr.length; i++){
        //耗时很久的嵌套循环或者运算
        doSomeing(arr[i]);
    }

假如在循环中做的处理的逻辑不是同步的话,那么可以把逻辑''分片''执行

    sertTimeout(function(){
        var item = arr.shift();
        dosomeing(item);
        if(arr.length <0 ){
            setTimeout(arguments.callee, 100);
        }
    }, 100);

延迟JS引擎的调用

GUI渲染线程和JS引擎的执行之间是互斥,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎执行完毕时立即被执行。这样的结果会导致新内容很晚才能渲染完成,很多时候我们需要让界面先渲染完成之后在去调用JS引擎来执行,那么可以使用setTimeout来延迟JS引擎的调用

    var div = document.createElement('div');
    div.setAttribute('style', 'background-color:#233; width:100px; height:100px');
    document.body.appendChild(div);
    
    setTimeout(function() {
        alert(1);
    }, 100);
查看原文

赞 4 收藏 4 评论 1

前端森林 发布了文章 · 2018-11-12

NodeJS踩坑实录

nodejs的常用api

  • url 主要是配置一系列和路径相关的信息
url.parse(urlString[, parseQueryString[, slashesDenoteHost]]) 将一个URL字符串解析为URL对象
urlString: 解析的路径字符串
parseQueryString: 返回是布尔类型,主要用来解析query的
slashesDenoteHost: 返回是布尔类型,当你不确定你的请求协议时,辅助帮助你进行解析

url.format(urlObj,parseObj,slashesObj) 将url对象转换为字符串
与parse参数相反

url.resolve(from, to) 将基础路径和后缀路径转换成目标路径
from 解析时相对的基本URL
to 要解析的超链接 URL
值得注意的是基本路径要在路径最后添加'/',否则合并会找到你最近的'/'并替换
const url = require('url');
url.resolve('/one/two/three', 'four');         // '/one/two/four'
url.resolve('http://example.com/', '/one');    // 'http://example.com/one'
url.resolve('http://example.com/one', '/two'); // 'http://example.com/two'
  • queryString 为查询字符串提供扩展
querystring 模块提供了一些实用函数,用于解析与格式化 URL 查询字符串
querystring.parse(str,con,seq)
str 要解析的 URL 查询字符串
con用于界定查询字符串中的键值对的子字符串。默认为 '&'
seq 用于界定查询字符串中的键与值的子字符串。默认为 '='

querystring.stringify(obj,con,seq)
obj 要序列化成 URL 查询字符串的对象
con 用于界定查询字符串中的键值对的子字符串。默认为 '&'
seq 用于界定查询字符串中的键与值的子字符串。默认为 '='

querystring.escape(str) 相当于encodeURI 将Asc编码转换成utf-8
对给定的str进行 URL编码
该方法是提供给 querystring.stringify()使用的,通常不直接使用

querystring.unescape(str) 相当于decodeURI 将utf-8转换成ASc
对给定的str进行解码
该方法是提供给 querystring.parse()使用的,通常不直接使用
  • events - 事件触发器

大多数 Node.js 核心 API 构建于惯用的异步事件驱动架构,其中某些类型的对象(又称触发器,Emitter)会触发命名事件来调用函数(又称监听器,Listener)

当 EventEmitter 对象触发一个事件时,所有绑定在该事件上的函数都会被同步地调用

例子,一个简单的 EventEmitter 实例,绑定了一个监听器。 eventEmitter.on() 方法用于注册监听器,eventEmitter.emit() 方法用于触发事件。

const Eventemitter = require("events")

class Player extends Eventemitter {}

const player = new Player()

//使用 eventEmitter.on() 注册监听器时,监听器会在每次触发命名事件时被调用
player.on("change",(track) => {
    console.log(`node事件机制`,${track})
})


//使用 eventEmitter.once() 可以注册最多可调用一次的监听器。 当事件被触发时,监听器会被注销,然后再调用
//player.once("change",(track) => {
//    console.log(`node事件机制`,${track})
//})

player.emit("change","react")
player.emit("change","vue")

传参数与this到监听器
eventEmitter.emit() 方法可以传任意数量的参数到监听器函数。 当监听器函数被调用时,this 关键词会被指向监听器所绑定的 EventEmitter 实例

const myEmitter = new MyEmitter();
myEmitter.on('event', function(a, b) {
  console.log(a, b, this, this === myEmitter);
  // 打印:
  //   a b MyEmitter {
  //     domain: null,
  //     _events: { event: [Function] },
  //     _eventsCount: 1,
  //     _maxListeners: undefined } true
});
myEmitter.emit('event', 'a', 'b');

emitter.removeAllListeners([eventName])
移除全部监听器或指定的 eventName 事件的监听器。

  • fs - 文件系统

fs 模块提供了一些接口用于以一种类似标准 POSIX 函数的方式与文件系统进行交互

所有的文件系统操作都有同步和异步两种形式

异步形式的最后一个参数都是完成时的回调函数。 传给回调函数的参数取决于具体方法,但回调函数的第一个参数都会保留给异常。 如果操作成功完成,则第一个参数会是 null 或 undefined

fs.Stats
fs.Stats 对象提供了一个文件的信息
stats.isDirectory() 如果 fs.Stats 对象表示一个文件系统目录,则返回 true
stats.isFile() 如果 fs.Stats 对象表示一个普通文件,则返回 true

fs.mkdir(path[, options], callback)
异步地创建目录。 完成回调只有一个可能的异常参数

// 创建 /temp/a/apple 目录,不管 `/temp` 和 /temp/a 目录是否存在。
fs.mkdir('/temp/a/apple', (err) => {
  if (err) throw err;
});

fs.writeFile(file, data[, options], callback)
异步地写入数据到文件,如果文件已经存在,则覆盖文件。 data 可以是字符串或 buffer

fs.writeFile('temp.js', 'keep study', (err) => {
  if (err) throw err;
  console.log('文件已保存!');
});

fs.appendFile(path, data[, options], callback)
异步地追加数据到文件,如果文件不存在则创建文件。 data 可以是字符串或 Buffer

fs.appendFile('temp.js', '追加的数据', (err) => {
  if (err) throw err;
  console.log('数据已追加到文件');
});

fs.readFile(path[, options], callback)
异步地读取一个文件的全部内容

fs.readFile('/etc/passwd', (err, data) => {
  if (err) throw err;
  console.log(data);
});

回调有两个参数 (err, data),其中 data 是文件的内容。

如果未指定字符编码,则返回原始的 buffer。

如果 options 是一个字符串,则它指定了字符编码。例子:

fs.readFile('/etc/passwd', 'utf8', callback);

fs.readdir(path[, options], callback)
读取目录的内容。 回调有两个参数 (err, files),其中 files 是目录中文件名的数组,不包含 '.' 和 '..'。
options 参数用于传入回调的文件名。 它可以是一个字符串,指定字符编码。 也可以是一个对象,其中 encoding 属性指定字符编码。 如果 encoding 设为 'buffer',则返回的文件名会是 Buffer 对象。

fs.rmdir(path, callback)
删除目录

fs.readFileSync(path[, options])
同步读取文件

fs.readdirSync(path[, options])
同步读取目录

fs.unlink(path, callback)
解除关系(也即删除文件)

readFileSync和unlink结合实现删除一个目录及其目录下的文件的例子:

const fs = require('fs');

fs.readdirSync("logs").map((file) => {
    fs.unlink(`logs/${file}`,() => {
        console.log("删除成功")
    })
})

fs.rmdir("logs", (err)=> {
    console.log("确定要删除吗?")
})

fs.watch(filename[, options][, listener])
监视 filename 的变化,filename 可以是一个文件或一个目录。

如果提供的 options 是一个字符串,则它指定了 encoding。 否则 options 应该传入一个对象。

监听器回调有两个参数 (eventType, filename)。 eventType 可能是 'rename' 或 'change',filename 是触发事件的文件的名称。

在大多数平台,当目录中一个文件出现或消失时,就会触发 'rename' 事件。

fs.createReadStream(path[, options])

fs.createWriteStream(path[, options])

const fs = require('fs');

const ws = fs.createWriteStream('./demo.txt');

const tid = setInterval(() => {
    const num = parseInt(Math.random()*10);
    if (num < 8) {
        ws.write(num + '');
    } else {
        clearInterval(tid);
        ws.end()
    }
},200)

ws.on('finish', () => {
    console.log('done');
})

fs 解决回调地狱问题

const fs = require('fs');
const promisify = require('util').promisify;

const read = promisify(fs.readFile);

// read('./promisify.js').then(data => {
//     console.log(data.toString());
// }).catch(ex => {
//     console.log(ex)
// })

async function test() {
    try {
        const content = await read('./promisify.js');
        console.log(content.toString());
    } catch (ex) {
        console.log(ex);
    }
}

test();
  • path path 模块提供了一些工具函数,用于处理文件与目录的路径

path.normalize()
path.normalize() 方法会规范化给定的 path,并解析 '..' 和 '.' 片段。

当发现多个连续的路径分隔符时(如 POSIX 上的 / 与 Windows 上的 或 /),它们会被单个的路径分隔符(POSIX 上是 /,Windows 上是 )替换。 末尾的多个分隔符会被保留。

如果 path 是一个长度为零的字符串,则返回 '.',表示当前工作目录。
例如,在 POSIX 上:

path.normalize('/foo/bar//baz/asdf/quux/..');
// 返回: '/foo/bar/baz/asdf'

在 Windows 上:

path.normalize('C:\\temp\\\\foo\\bar\\..\\');
// 返回: 'C:\\temp\\foo\\'

path.join([...paths])
path.join() 方法使用平台特定的分隔符把全部给定的 path 片段连接到一起,并规范化生成的路径。

长度为零的 path 片段会被忽略。 如果连接后的路径字符串是一个长度为零的字符串,则返回 '.',表示当前工作目录。
如果任一路径片段不是一个字符串,则抛出 TypeError

例子:

path.join('/foo', 'bar', 'baz/asdf', 'quux', '..');
// 返回: '/foo/bar/baz/asdf'

path.join('foo', {}, 'bar');
// 抛出 'TypeError: Path must be a string. Received {}'

path.resolve([...paths])
path.resolve() 方法会把一个路径或路径片段的序列解析为一个绝对路径。

给定的路径的序列是从右往左被处理的,后面每个 path 被依次解析,直到构造完成一个绝对路径。 例如,给定的路径片段的序列为:/foo、/bar、baz,则调用 path.resolve('/foo', '/bar', 'baz') 会返回 /bar/baz

如果没有传入 path 片段,则 path.resolve() 会返回当前工作目录的绝对路径
如果任何参数不是一个字符串,则抛出 TypeError
例子:

path.resolve('/foo/bar', './baz');
// 返回: '/foo/bar/baz'

path.resolve('/foo/bar', '/tmp/file/');
// 返回: '/tmp/file'

path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif');
// 如果当前工作目录为 /home/myself/node,
// 则返回 '/home/myself/node/wwwroot/static_files/gif/image.gif'

path.basename(path[, ext])
path <string>
ext <string> 可选的文件扩展名

path.basename() 方法返回一个 path 的最后一部分,类似于 Unix 中的 basename 命令
例子:

path.basename('/foo/bar/baz/asdf/quux.html');
// 返回: 'quux.html'

path.basename('/foo/bar/baz/asdf/quux.html', '.html');
// 返回: 'quux'

path.extname(path)
path.extname() 方法返回 path 的扩展名,即从 path 的最后一部分中的最后一个 .(句号)字符到字符串结束。 如果 path 的最后一部分没有 . 或 path 的文件名(见 path.basename())的第一个字符是 .,则返回一个空字符串。

如果 path 不是一个字符串,则抛出 TypeError
例子:

path.extname('index.html');
// 返回: '.html'

path.extname('index.coffee.md');
// 返回: '.md'

path.extname('index.');
// 返回: '.'

path.extname('index');
// 返回: ''

path.extname('.index');
// 返回: ''

path.dirname(path)
path.dirname() 方法返回一个 path 的目录名,类似于 Unix 中的 dirname 命令
例子:

path.dirname('/foo/bar/baz/asdf/quux');
// 返回: '/foo/bar/baz/asdf'

path.parse(path)
path.parse() 方法返回一个对象,对象的属性表示 path 的元素
返回的对象有以下属性:

dir <string>
root <string>
base <string>
name <string>
ext <string>
例如,在 POSIX 上:

path.parse('/home/user/dir/file.txt');
// 返回:
// { root: '/',
//   dir: '/home/user/dir',
//   base: 'file.txt',
//   ext: '.txt',
//   name: 'file' }

在 Windows 上:

path.parse('C:\\path\\dir\\file.txt');
// 返回:
// { root: 'C:\\',
//   dir: 'C:\\path\\dir',
//   base: 'file.txt',
//   ext: '.txt',
//   name: 'file' }

path.format(pathObject)

pathObject <Object>
dir <string>
root <string>
base <string>
name <string>
ext <string>

path.format() 方法会从一个对象返回一个路径字符串。 与 path.parse() 相反。

当 pathObject 提供的属性有组合时,有些属性的优先级比其他的高:

如果提供了 pathObject.dir,则 pathObject.root 会被忽略
如果提供了 pathObject.base 存在,则 pathObject.ext 和 pathObject.name 会被忽略

例如,在 POSIX 上:

// 如果提供了 `dir`、`root` 和 `base`,则返回 `${dir}${path.sep}${base}`。
// `root` 会被忽略。
path.format({
  root: '/ignored',
  dir: '/home/user/dir',
  base: 'file.txt'
});
// 返回: '/home/user/dir/file.txt'

// 如果没有指定 `dir`,则 `root` 会被使用。
// 如果只提供了 `root` 或 `dir` 等于 `root`,则平台的分隔符不会被包含。
// `ext` 会被忽略。
path.format({
  root: '/',
  base: 'file.txt',
  ext: 'ignored'
});
// 返回: '/file.txt'

// 如果没有指定 `base`,则 `name` + `ext` 会被使用。
path.format({
  root: '/',
  name: 'file',
  ext: '.txt'
});
// 返回: '/file.txt'

在 Windows 上:

path.format({
  dir: 'C:\\path\\dir',
  base: 'file.txt'
});
// 返回: 'C:\\path\\dir\\file.txt'

path.sep
提供了平台特定的路径片段分隔符:

Windows 上是 \
POSIX 上是 /
例如,在 POSIX 上:

'foo/bar/baz'.split(path.sep);
// 返回: ['foo', 'bar', 'baz']

在 Windows 上:

'foo\\bar\\baz'.split(path.sep);
// 返回: ['foo', 'bar', 'baz']

path.delimiter
提供平台特定的路径分隔符:

Windows 上是 ;
POSIX 上是 :
例如,在 POSIX 上:

console.log(process.env.PATH);
// 输出: '/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin'

process.env.PATH.split(path.delimiter);
// 返回: ['/usr/bin', '/bin', '/usr/sbin', '/sbin', '/usr/local/bin']

在 Windows 上:

console.log(process.env.PATH);
// 输出: 'C:\Windows\system32;C:\Windows;C:\Program Files\node\'

process.env.PATH.split(path.delimiter);
// 返回: ['C:\\Windows\\system32', 'C:\\Windows', 'C:\\Program Files\\node\\']

path.win32
path.win32 属性提供了 path 方法针对 Windows 的实现

path.posix
path.posix 属性提供了 path 方法针对 POSIX 的实现

__dirname__filename总是返回文件的绝对路径
process.cwd()总是返回执行node命令所在文件夹
./:在require方法中总是相对于当前文件所在文件夹
在其他地方和process.cwd()一样,相对于node启动文件夹
  • Buffer (缓冲)

在 ECMAScript 2015 引入 TypedArray 之前,JavaScript 语言没有读取或操作二进制数据流的机制。 Buffer 类被引入作为 Node.js API 的一部分,使其可以在 TCP 流或文件系统操作等场景中处理二进制数据流。

TypedArray 现已被添加进 ES6 中,Buffer 类以一种更优化、更适合 Node.js 用例的方式实现了 Uint8Array API。

Buffer 类的实例类似于整数数组,但 Buffer 的大小是固定的、且在 V8 堆外分配物理内存。 Buffer 的大小在被创建时确定,且无法调整。

Buffer 类在 Node.js 中是一个全局变量,因此无需使用 require('buffer').Buffer。

Buffer.alloc(size[, fill[, encoding]])
size <integer> 新建的 Buffer 期望的长度
fill <string> | <Buffer> | <integer> 用来预填充新建的 Buffer 的值。 默认: 0
encoding <string> 如果 fill 是字符串,则该值是它的字符编码。 默认: 'utf8'

分配一个大小为 size 字节的新建的 Buffer 。 如果 fill 为 undefined ,则该 Buffer 会用 0 填充。
例子:

const buf = Buffer.alloc(5);

// 输出: <Buffer 00 00 00 00 00>
console.log(buf);

如果同时指定了 fill 和 encoding ,则会调用 buf.fill(fill, encoding) 初始化分配的 Buffer 。

const buf = Buffer.alloc(11, 'aGVsbG8gd29ybGQ=', 'base64');

// 输出: <Buffer 68 65 6c 6c 6f 20 77 6f 72 6c 64>
console.log(buf);

调用 Buffer.alloc() 会明显地比另一个方法 Buffer.allocUnsafe() 慢,但是能确保新建的 Buffer 实例的内容不会包含敏感数据。

Buffer.allocUnsafe(size)
分配一个大小为 size 字节的新建的 Buffer 。 如果 size 大于 buffer.constants.MAX_LENGTH 或小于 0,则抛出 [RangeError] 错误。 如果 size 为 0,则创建一个长度为 0 的 Buffer。

以这种方式创建的 Buffer 实例的底层内存是未初始化的。 新创建的 Buffer 的内容是未知的,且可能包含敏感数据。 可以使用 buf.fill(0) 初始化 Buffer 实例为0。

const buf = Buffer.allocUnsafe(10);

// 输出: (内容可能不同): <Buffer a0 8b 28 3f 01 00 00 00 50 32>
console.log(buf);

buf.fill(0);

// 输出: <Buffer 00 00 00 00 00 00 00 00 00 00>
console.log(buf);

Buffer.byteLength(string[, encoding])
返回一个字符串的实际字节长度。 这与 String.prototype.length 不同,因为那返回字符串的字符数

Buffer.isBuffer(obj)
如果 obj 是一个 Buffer 则返回 true ,否则返回 false。

Buffer.concat(list[, totalLength])
list <Array> 要合并的 Buffer 或 Uint8Array 实例的数组
totalLength <integer> 合并时 list 中 Buffer 实例的总长度

返回一个合并了 list 中所有 Buffer 实例的新建的 Buffer 。

如果 list 中没有元素、或 totalLength 为 0 ,则返回一个新建的长度为 0 的 Buffer 。

const buf1 = Buffer.alloc(10);
const buf2 = Buffer.alloc(14);
const buf3 = Buffer.alloc(18);
const totalLength = buf1.length + buf2.length + buf3.length;

// 输出: 42
console.log(totalLength);

const bufA = Buffer.concat([buf1, buf2, buf3], totalLength);

// 输出: <Buffer 00 00 00 00 ...>
console.log(bufA);

// 输出: 42
console.log(bufA.length);

buf.length
返回 buf 在字节数上分配的内存量。

const buf = Buffer.alloc(1234);

// 输出: 1234
console.log(buf.length);

buf.toString([encoding[, start[, end]]])
encoding <string> 解码使用的字符编码。默认: 'utf8'
start <integer> 开始解码的字节偏移量。默认: 0
end <integer> 结束解码的字节偏移量(不包含)。 默认: buf.length

根据 encoding 指定的字符编码解码 buf 成一个字符串。 start 和 end 可传入用于只解码 buf 的一部分。

const buf1 = Buffer.allocUnsafe(26);

for (let i = 0; i < 26; i++) {
  // 97 是 'a' 的十进制 ASCII 值
  buf1[i] = i + 97;
}

// 输出: abcdefghijklmnopqrstuvwxyz
console.log(buf1.toString('ascii'));

// 输出: abcde
console.log(buf1.toString('ascii', 0, 5));

buf.fill(value[, offset[, end]][, encoding])
value <string> | <Buffer> | <integer> 用来填充 buf 的值。
offset <integer> 开始填充 buf 前要跳过的字节数。默认: 0。
end <integer> 结束填充 buf 的位置(不包含)。默认: buf.length。
encoding <string> 如果 value 是一个字符串,则这是它的字符编码。默认: 'utf8'。

buf.equals(otherBuffer)
如果 buf 与 otherBuffer 具有完全相同的字节,则返回 true,否则返回 false。

const buf1 = Buffer.from('ABC');
const buf2 = Buffer.from('414243', 'hex');
const buf3 = Buffer.from('ABCD');

// 输出: true
console.log(buf1.equals(buf2));

// 输出: false
console.log(buf1.equals(buf3));

buf.indexOf(value[, byteOffset][, encoding])
buf 中 value 首次出现的索引,如果 buf 没包含 value 则返回 -1
value <string> | <Buffer> | <Uint8Array> | <integer> 要搜索的值
byteOffset <integer> buf 中开始搜索的位置。默认: 0
encoding <string> 如果 value 是一个字符串,则这是它的字符编码。 默认: 'utf8'

const buf = Buffer.from('this is a buffer');

// 输出: 0
console.log(buf.indexOf('this'));

// 输出: 2
console.log(buf.indexOf('is'));

// 输出: 8
console.log(buf.indexOf(Buffer.from('a buffer')));

// 输出: 8
// (97 是 'a' 的十进制 ASCII 值)
console.log(buf.indexOf(97));

// 输出: -1
console.log(buf.indexOf(Buffer.from('a buffer example')));

buf.copy(target[, targetStart[, sourceStart[, sourceEnd]]])
target <Buffer> | <Uint8Array> 要拷贝进的 Buffer 或 Uint8Array。
targetStart <integer> target 中开始拷贝进的偏移量。 默认: 0
sourceStart <integer> buf 中开始拷贝的偏移量。 默认: 0
sourceEnd <integer> buf 中结束拷贝的偏移量(不包含)。 默认: buf.length
拷贝 buf 的一个区域的数据到 target 的一个区域,即便 target 的内存区域与 buf 的重叠。

buf 解决中文字符串乱码问题
const StringDecoder = require('string_decoder').StringDecoder;
const decoder = new StringDecoder('utf8');
const buf = Buffer.from('中文字符串!');

//for (let i = 0; i < buf.length; i+= 5) {
//    const b = Buffer.allocUnsafe(5);
//    buf.copy(b,0,i);

//    console.log(b.toString()); 
//}

for (let i = 0; i < buf.length; i+= 5) {
    const b = Buffer.allocUnsafe(5);
    buf.copy(b,0,i);

    console.log(decoder.write(b)); 
}
  • process - 进程

process 对象是一个全局变量,它提供当前 Node.js 进程的有关信息,以及控制当前 Node.js 进程。 因为是全局变量,所以无需使用 require()。

process.stdin
process.stdin 属性返回连接到 stdin (fd 0)的流。 它是一个net.Socket(它是一个Duplex流),除非 fd 0指向一个文件,在这种情况下它是一个Readable流。
process.stdout
process.stdout 属性返回连接到 stdout (fd 1)的流。 它是一个net.Socket (它是一个Duplex流), 除非 fd 1 指向一个文件,在这种情况下它是一个[可写][]流。
例1: 将输入流数据输出到输出流,即输出到终端。

process.stdin.pipe(process.stdout);

Cookie和Session

Cookie

● HTTP是无状态协议。简单地说,当你浏览了一个页面,然后转到同一个网站的另一个页面,服务器无法认识到,这是同一个浏览器在访问同一个网站。每一次的访问,都是没有任何关系的。
那么世界就乱套了,比如我上一次访问,登陆了,下一次访问,又让我登陆,不存在登陆这事儿了。
● Cookie是一个简单到爆的想法:当访问一个页面的时候,服务器在下行HTTP报文中,命令浏览器存储一个字符串;浏览器再访问同一个域的时候,将把这个字符串携带到上行HTTP请求中。

● 第一次访问一个服务器,不可能携带cookie。 必须是服务器得到这次请求,在下行响应报头中,携带cookie信息,此后每一次浏览器往这个服务器发出的请求,都会携带这个cookie。

特点
● cookie是不加密的,用户可以自由看到;
● 用户可以删除cookie,或者禁用它
● cookie可以被篡改
● cookie可以用于攻击
● cookie存储量很小。未来实际上要被localStorage替代,但是后者IE9兼容。

express中的cookie,你肯定能想到。 res负责设置cookie, req负责识别cookie。
图片描述

cookie实例

var express  = require('express');
var cookieParser = require('cookie-parser');
 
var app = express();
//使用cookie必须要使用cookie-parser中间件
app.use(cookieParser());

app.get("/",function(req,res){
    res.send("猜你喜欢" + req.cookies.mudidi);
});

//查询一个地方的攻略,URL语法: http://127.0.0.1/gonglue?mididi=北京
//此时北京就能记录在cookie中
app.get("/gonglue",function(req,res){
    //得到get请求,用户查询的目的地
    var mudidi = req.query.mudidi;
    //记录用户喜好
    //先读取用户的喜好,然后把新的数据push进入数组,然后设置新的cookie
    var mudidiarry = req.cookies.mudidi || [];
    mudidiarry.push(mudidi);
    //maxAge在Express中以毫秒为单位
    res.cookie("mudidi",mudidiarry,{maxAge: 900000, httpOnly: true});
    res.send(mudidi + "旅游攻略");
});

app.listen(3000);

Session

session依赖cookie,当一个浏览器禁用cookie的时候,登陆效果消失; 或者用户清除了cookie,登陆也消失。
session比cookie不一样在哪里呢? session下发的是乱码,并且服务器自己缓存一些东西,下次浏览器的请求带着乱码上来,此时与缓存进行比较,看看是谁。
所以,一个乱码,可以对应无限大的数据。
任何语言中,session的使用,是“机理透明”的。他是帮你设置cookie的,但是足够方便,让你感觉不到这事儿和cookie有关。
session实例

var express = require("express");
var app = express();
var session = require("express-session");

app.use(session({
    secret: 'keyboard cat',
    resave: false,
    saveUninitialized: true
}));

app.get("/",function(req,res){
    if(req.session.login == "1"){
        res.send("欢迎" + req.session.username);
    }else{
        res.send("没有成功登陆");
    }
});

app.get("/login",function(req,res){
    req.session.login = "1";    //设置这个session
    req.session.username = "考拉";
    res.send("你已经成功登陆");
});

app.listen(3000);

加密(MD5加密)

MD5加密是函数型加密。就是每次加密的结果一定相同,没有随机位。
特点:
● 不管加密的文字,多长多短,永远都是32位英语字母、数字混合。
● 哪怕只改一个字,密文都会大变。
● MD5没有反函数破解的可能,网上的破解工具,都是通过字典的模式,通过大量列出明-密对应的字典,找到明码。两次加密网上也有对应的字典。所以我们不要直接用一层md5,这样对黑客来说和明码是一样。

MD5常用于作为版本校验。可以比对两个软件、文件是否完全一致。

node中自带了一个模块,叫做crypto模块,负责加密。

首先创建hash,然后update和digest:

var crypto = require("crypto");
module.exports = function(mingma){
    var md5 = crypto.createHash('md5');
    var password = md5.update(mingma).digest('base64');
    return password;
}

node框架之express

关于express

一个基于node的开发框架
express,不会在node上进行2次抽象,而是在node本身上提供扩展
完全由路由和中间件组成,从本质上来说express就是根据路由对前后端进行桥接
中间件(middleware)指的就是一个函数,可以访问请求对象,也可以访问响应对象,可以访问请求和响应循环中的下一个中间件

中间件的特性

  • 可以执行任何代码
  • 可以访问请求或者响应对象
  • 可以终结请求响应流程
  • 可以调用堆栈中的下一个中间件(next)

安装指令

npm init
npm i express -g
npm i express-generator -g //专门供windows使用
切换到指定项目目录
npm i express --save-dev
npm i express-generator --save-dev

Hello world~~

var express = require("express");
var app = express();

app.get("/",(req,res) => {
    res.write("this is homePage")
})

app.get("/login",(req,res) => {
    res.send("this is loginPage")
})


var server = app.listen(3000,"127.0.0.1",() => {
    console.log("express hello_world")
})

路由响应方法

方法描述
res.download()提示下载文件
res.end()终结响应处理流程
res.json()发送JSON响应
res.jsonp()发送一个支持JSONP的JSON格式的响应
res.redirect()重定向请求
res.send()发送各种类型的响应
res.sendFile()以八位字节流的形式发送文件
res.sendStatus()设置响应状态代码并将其以字符串形式作为响应体的一部分发送。

express 脚手架

利用express中内置的脚手架构建项目
在指定目录下 express -e project_name
cd project_name
npm install
npm install

图片描述

body-parser 帮助对请求体进行解析
cookie-parser 给cookie提供解析
debug 帮助在控制台上输出debug信息
ejs javascript 一个模板引擎
morgan 帮助在控制台上反馈request的信息
serve-favicon 主要是为了解决初始化请求favicon图标问题
app.set(event, str) 设定一些参数
__dirname 绝对路径
app.use(path,callback) 接受中间件并执行
res.sendFile(absolutePath--绝对路径)

ejs语法

<%= %> 输出内容标签
<%- %> 输出html内容标签
<% %> 流程标签 可以执行js代码
<%# %> 注释标签
<%- include(path) %> 引入标签

express实战

利用express+mongodb实现一个从登录、注册、注销到评论页、详情页的微型前后端电商平台,具体代码见github:https://github.com/Jack-cool/expressPro

app.js

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var session = require('express-session');
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var commentRouter = require('./routes/comment');
// var async = require('async');
var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use(session({
  secret: 'recommend 128 bytes random string', // 对session id 相关的cookie 进行签名
  cookie: { maxAge: 20*60*1000 }, // // 设置 session 的有效时间,单位毫秒
  resave: true,
  saveUninitialized: true // 是否保存未初始化的会话
}))
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/comment', commentRouter);


// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

routes/index.js

var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res, next) {
  res.render('index', { title: 'Home', username: req.session.username });
});

router.get('/register', function(req, res, next) {
  res.render('register', { title: '注册页' });
});

router.get('/login', function(req, res, next) {
  res.render('login', { title: '登录页'})
});

router.get('/logout', function(req,res,next) {
  // req.session.username = undefined;
  // res.redirect('/');
  req.session.destroy(function(err) {
    if (err) {
      console.log(err);
    } else {
      res.redirect("/");
    }
  })
});

router.get('/comment', function(req,res,next) {
  res.render('comment', { title: '评论页' })
})

module.exports = router;

routes/users.js

var express = require('express');
var MongoClient = require('mongodb').MongoClient;
var DB_CONNECT_STR = "mongodb://localhost:27017/users"

var router = express.Router();

/* GET users listing. */
router.get('/', function(req, res, next) {
  res.send('respond with a resource');
});

router.post('/register',function(req,res){
  // res.send('注册成功');
  console.log('注册信息',req.body);
  var username = req.body.reg_name;
  var nickname = req.body.reg_nickname;
  var pwd = req.body.reg_pwd;
  var insertData = function(db,callback) {
    // 关联集合
    var data = [{ username:username,nickname:nickname,pwd:pwd }]
    var conn = db.collection('front');
    conn.insert(data,function(err,result) {
      if (err) {
        console.log(err)
      } else {
        callback(result);
      }
    })
  }
  // 连接数据库
  MongoClient.connect(DB_CONNECT_STR,function(err,db) {
    if (err) {
      console.log('连接数据库失败');
    } else {
      console.log('连接数据库成功');
      insertData(db,function(result) {
        res.send('哈哈,你已经注册成功了呦~~~')
        db.close();
      })
    }
  })
})

router.post('/login', function(req,res,next) {
  var username = req.body.log_name;
  var nickname = req.body.log_nickname;
  var pwd = req.body.log_pwd;

  var findData = function(db,callback) {
    // 查询条件
    var data = { username:username,nickname:nickname,pwd:pwd }
    // 关联集合
    var conn = db.collection('front');
    conn.find(data).toArray(function(err,result) {
      if (err) {
        console.log(err);
      } else {
        callback(result);
      }
    })
  }
  // 连接数据库
  MongoClient.connect(DB_CONNECT_STR,function(err,db) {
    if (err) {
      console.log('连接数据库失败');
    } else {
      findData(db,function(result) {
        console.log('登陆数据~~',result);
        if (result.length > 0) {
          req.session.username = result[0].username;
          res.redirect('/');
        } else {
          res.send('登陆失败');
        }
        db.close();
      })
    }
  })
})

module.exports = router;

routes/comment.js

var express = require('express');
var async = require('async');
var MongoClient = require('mongodb').MongoClient;
var DB_CONNECT_STR = "mongodb://localhost:27017/users";
var router = express.Router();

/* GET home page. */
router.post('/save', function(req, res, next) {
  // res.send('发布成功');
  console.log('评论信息',req.body);
  var title = req.body.comment_title;
  var content = req.body.comment_content;
  // var insertData = function(db,callback) {
  //   // 关联集合
  //   var data = [{ title:title,content:content }];
  //   var conn = db.collection('comments');
  //   conn.insert(data,function(err,result) {
  //     if (err) {
  //       console.log(err);
  //     } else {
  //       callback(result);
  //     }
  //   })
  // } 

  var updateData = function(db,callback) {
    var conn = db.collection('comments');
    var ids = db.collection('ids');
    async.waterfall([function(callback){
      ids.findAndModify({name:'comments'},[["_id","desc"]],{$inc:{id:1}},function(err,result){
        callback(null,result.value.id)
      })
    },function(id,callback){
      var data = [{uid:id,title:title,content:content,username:req.session.username}];
      conn.insert(data,function(result){
        callback(result)
      })
    }],function(err,result){
      if (err) {
        console.log(err);
      } else {
        callback(result);
      }
    })
  }


  MongoClient.connect(DB_CONNECT_STR,function(err,db) {
    if (err) {
      console.log('连接数据库失败');
    } else {
      console.log('连接数据库成功');
      // insertData(db,function(result) {
      //   res.send('嘻嘻嘻,你发布成功了呦~~~');
      //   db.close();
      // })
      updateData(db,function(result) {
        // res.send('嘻嘻嘻,你发布成功了呦~~~');
        res.redirect('/comment/list');
        db.close();
      })
    }
  })
});

router.get('/list',function(req,res) {
  var findData = function(db,callback) {
    var conn = db.collection('comments');
    conn.find({}).toArray(function(err,result) {
      if (err) {
        console.log(err);
      } else {
        callback(result);
      }
    })
  }
  MongoClient.connect(DB_CONNECT_STR,function(err,db){
    if (err) {
      console.log(err);
    } else {
      findData(db,function(result) {
        if (result.length > 0) {
          console.log('评论列表页信息',result);
          res.render('list',{ title:'列表页',list:result });
        } else {
          res.send('亲,没有评论信息~~');
        }
      })
    }
  })
})

router.get('/detail',function(req,res) {
  // res.send('列表页');
  var uid = parseInt(req.query.uid);
  MongoClient.connect(DB_CONNECT_STR,function(err,db) {
    var conn = db.collection('comments');
    conn.find({uid:uid}).toArray(function(err,result) {
      if (err) {
        console.log(err);
      } else {
        console.log('详情页信息',result);
        res.render('detail',{ title:'详情页',mes:result });
      }
    })
  })
})

module.exports = router;

node框架之koa2

文档持续更新中~~~

欢迎关注我的微信公众号~前端森林

前端森林.jpeg

查看原文

赞 11 收藏 13 评论 0

前端森林 发布了文章 · 2018-11-11

mongodb使用总结

图片描述

MongoDB 是一个基于分布式文件存储的数据库。由 C++ 语言编写。旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。

MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。是世界上最大的nosql(not only sql)数据库。执行mongodb数据库需要mongod环境和mongo环境。

数据库

传统数据库技术回顾

概念:存储数据的仓库我们称为数据库。数据库分为非关系型数据库和关系型数据库。关系型数据库(Oracle,mysql,db2,...)往往以表结构的形式进行存储。存储数据用txt就行了,为什么要有数据库?
  • 理由1:数据库有行、列的概念,数据有关系,数据不是散的

老牌数据库,比如MySQL、SQLServer、Oracle、Access。这些数据库,我们管他们叫结构型数据库。为什么?因为每个表中,都有明确的字段、每行记录,都有这些字段。不能有的行有,有的行没有。
图片描述

  • 理由2:数据库能够提供非常方便的接口,让增删改查变得简单

我们的老牌数据库,都无一例外的使用SQL语言,管理数据库。
SQL就是structure query language
比如,查询所有女生:

SELECT * FROM users WHERE sex = '女';

再比如,查询所有女生,并且年龄20~24之间,并且在北京:

SELECT * FROM users WHERE sex = '女' AND age < 24 AND age >= 20 AND address = '北京'
  • 理由3:数据库不能自己玩儿,要给向PHP、.net、jsp等语言提供接口

用PHP这些语言,能够向数据库中增删改查。
老牌数据库,都是结构型数据库,现在出了什么问题?
比如,我们现在想往一个已经有1000条数据的数据库中增加一个字段‘是否为本地常住居民’
图片描述
之前已经存在的数据,实际上不需要增加这个字段,因为这些用户已经填写完毕表单了。不需要完善个人信息了。我们的想法是在今后注册的用户,需要添加‘是否为本地常住居民’信息。但是,我们刚才说了,所谓的字段,是表的一个结构。所有的行都必须拥有,不能有的行有这个字段,有的行没有这个字段。
可想而知,大数据时代,数据库中有100万条数据都算少的。我们如果要动字段,太耗费时间。

非结构型数据库NoSQL应运而生。
NoSQL是个怪胎,无法挑战老牌数据库,但是在大数据时代有自己的意义。

NoSQL

非结构型数据库。没有行、列的概念。用JSON来存储数据。
集合就相当于‘表’,文档就相当于‘行’
图片描述
文档就是JSON
图片描述

因此,我们总结NOSQL数据库在以下的这几种情况下比较适用:1.数据模型比较简单;2.需要灵活性更强的系统;3.对数据库性能要求较高;4.不需要高度的数据一致性;5.对于给定key,比较容易映射复杂值的环境。
NoSQL不是银弹,没有资格挑战老牌数据库,但是在某些特定情况下,还是适合的。

mongodb和mysql的区别

  • 前者非关系型数据库,后者是关系型数据库
  • mongodb中是以集合的形式来充当mysql中的表结构
  • mongodb中的数据是以文档的形式进行存储

图片描述

mongodb的特点

mongodb的优点

  • 面向文档存储的数据库(BSON的数据格式)

图片描述

  • 有丰富的查询指令
  • 支持索引
  • 具有分片系统
  • 无模式

mongodb的缺点

  • 占用的空间比较大
  • 不支持事务
  • 对于windows来说,它不支持32位的系统

mongodb常用指令

  • show dbs 查看当前所有数据库

图片描述

  • use database_name 创建数据库
  • db 查询当前使用的数据库
  • db.stats() 查询当前使用的数据库信息
  • db.dropDatabase() 删除当前数据库

图片描述

  • db.help() 获取查询帮助
  • db.database_name.help() 获取指定数据库查询帮助
  • db.collection_name.find() 查询集合的信息
  • db.createCollection(coll_name,options) 创建集合
  • db.getCollectionNames() 查询所有集合
  • db.getCollection(coll_name) 查询某一个特定集合
  • db.coll_name.drop() 对集合的删除
  • db.printCollectionStats() 打印当前数据库中所有集合的状态
  • db.coll_name.insert/insertMany/save/insertOne 添加一条/多条数据
  • db.coll_name.update(query,info,con,muti) 修改数据(query: 查询的条件;info: 要更新的信息;con: 给异步操作提供扩展;muti: 返回布尔类型 默认false)(这里涉及到几个特殊属性$inc$set 前者为相加后者为设置)
  • db.coll_name.remove(query) 删除数据(query 删除的条件)

图片描述

  • 对数据的查询
  • db.coll_name.find() 查询所有信息
  • db.coll_name.find({"age": 18}) 查询某一条信息
  • db.coll_name.find({age: {$gt: 22}}) gt大于某一条件
  • db.coll_name.find({age: {$lt: 22}}) lt小于某一条件
  • db.coll_name.find({age: {$gte: 22}}) gt大于等于某一条件
  • db.coll_name.find({age: {$lte: 22}}) lte小于等于某一条件
  • db.coll_name.find({title: /好/}) 模糊查询
  • 集合与集合之间的关联
  • async.waterfall([task],callback) (task:代表执行的函数组,下一个函数总会继承上一个函数的返回值;callback:回调函数)
  • coll_name.findAndModify(data,sort,update,callback) (data:查询的条件;sort:对返回的查询结果进行排序;update:对查询的结果进行更新;callback:回调函数)

mongodb术语概念

sql术语/概念mongodb术语/概念解释/说明
databasedatabase数据库
tablecollection数据库表/集合
rowdocument数据记录行/文档
columnfield数据字段/域
indexindex索引
table joins表连接,mongodb不支持
primary keyprimary key主键,mongodb自动将_id字段设置为主键

mongodb的封装(DAO)

把常用的增删改查,都封装成为module。
开发DAO:J2EE开发人员使用数据访问对象(DAO)设计模式把底层的数据访问逻辑和高层的商务逻辑分开,实现DAO模式能够更加专注于编写数据访问代码。
使用我们自己的DAO模块,来实现数据库插入。代码变得简单。
//这个模块里面封装了所有对数据库的常用操作
var MongoClient = require('mongodb').MongoClient;
var settings = require("../settings.js");
//不管数据库什么操作,都是先连接数据库,所以我们可以把连接数据库
//封装成为内部函数
function _connectDB(callback) {
    var url = settings.dburl;   //从settings文件中,都数据库地址
    //连接数据库
    MongoClient.connect(url, function (err, db) {
        if (err) {
            callback(err, null);
            return;
        }
        callback(err, db);
    });
}

//插入数据
exports.insertOne = function (collectionName, json, callback) {
    _connectDB(function (err, db) {
        db.collection(collectionName).insertOne(json, function (err, result) {
            callback(err, result);
            db.close(); //关闭数据库
        })
    })
};

//查找数据,找到所有数据。args是个对象{"pageamount":10,"page":10}
exports.find = function (collectionName, json, C, D) {
    var result = [];    //结果数组
    if (arguments.length == 3) {
        //那么参数C就是callback,参数D没有传。
        var callback = C;
        var skipnumber = 0;
        //数目限制
        var limit = 0;
    } else if (arguments.length == 4) {
        var callback = D;
        var args = C;
        //应该省略的条数
        var skipnumber = args.pageamount * args.page || 0;
        //数目限制
        var limit = args.pageamount || 0;
        //排序方式
        var sort = args.sort || {};
    } else {
        throw new Error("find函数的参数个数,必须是3个,或者4个。");
        return;
    }

    //连接数据库,连接之后查找所有
    _connectDB(function (err, db) {
        var cursor = db.collection(collectionName).find(json).skip(skipnumber).limit(limit).sort(sort);
        cursor.each(function (err, doc) {
            if (err) {
                callback(err, null);
                db.close(); //关闭数据库
                return;
            }
            if (doc != null) {
                result.push(doc);   //放入结果数组
            } else {
                //遍历结束,没有更多的文档了
                callback(null, result);
                db.close(); //关闭数据库
            }
        });
    });
}

//删除
exports.deleteMany = function (collectionName, json, callback) {
    _connectDB(function (err, db) {
        //删除
        db.collection(collectionName).deleteMany(
            json,
            function (err, results) {
                callback(err, results);
                db.close(); //关闭数据库
            }
        );
    });
}

//修改
exports.updateMany = function (collectionName, json1, json2, callback) {
    _connectDB(function (err, db) {
        db.collection(collectionName).updateMany(
            json1,
            json2,
            function (err, results) {
                callback(err, results);
                db.close();
            });
    })
}

exports.getAllCount = function (collectionName,callback) {
    _connectDB(function (err, db) {
        db.collection(collectionName).count({}).then(function(count) {
            callback(count);
            db.close();
        });
    })
}

项目中使用mongodb

切换到指定项目 npm init生成package.json
npm install mongodb -g 全局安装
npm install mongodb --save-dev 局部安装

mongodb.js

var Mongodb = require("mongodb")
// 连接到mongodb的服务端口
var server = new Mongodb.Server("localhost",27017,{auto_reconnect:true})
//创建数据库
var db = new Mongodb.Db('cloud',server,{safe:true})
//连接数据库
db.open((err,db) => {
    if(err) {
        console.log('连接数据库失败')
    } else {
        console.log('连接数据库成功')
    }
})
查看原文

赞 1 收藏 1 评论 0

前端森林 收藏了文章 · 2018-11-06

讲讲PWA

一、背景

文章2017 前端大事件和趋势回顾,2018 何去何从?中提到了2017年前端值得关注的十大事件,其中就提到了PWA。

大家都知道Native app体验确实很好,下载到手机上之后入口也方便。它也有一些缺点:

  • 开发成本高(ios和安卓)
  • 软件上线需要审核
  • 版本更新需要将新版本上传到不同的应用商店
  • 想使用一个app就必须去下载才能使用,即使是偶尔需要使用一下下

而web网页开发成本低,网站更新时上传最新的资源到服务器即可,用手机带的浏览器打开就可以使用。但是除了体验上比Native app还是差一些,还有一些明显的缺点

  • 手机桌面入口不够便捷,想要进入一个页面必须要记住它的url或者加入书签
  • 没网络就没响应,不具备离线能力
  • 不像APP一样能进行消息推送

那么什么是PWA呢?

二、What's PWA?

PWA全称Progressive Web App,即渐进式WEB应用。

一个 PWA 应用首先是一个网页, 可以通过 Web 技术编写出一个网页应用. 随后添加上 App Manifest 和 Service Worker 来实现 PWA 的安装和离线等功能
解决了哪些问题?

  • 可以添加至主屏幕,点击主屏幕图标可以实现启动动画以及隐藏地址栏
  • 实现离线缓存功能,即使用户手机没有网络,依然可以使用一些离线功能
  • 实现了消息推送

它解决了上述提到的问题,这些特性将使得 Web 应用渐进式接近原生 App。

三、PWA的实现

3.1 Manifest实现添加至主屏幕

index.html

<head>
  <title>Minimal PWA</title>
  <meta name="viewport" content="width=device-width, user-scalable=no" />
  <link rel="manifest" href="manifest.json" />
  <link rel="stylesheet" type="text/css" href="main.css">
  <link rel="icon" href="/e.png" type="image/png" />
</head>

manifest.json

{
  "name": "Minimal PWA", // 必填 显示的插件名称
  "short_name": "PWA Demo", // 可选  在APP launcher和新的tab页显示,如果没有设置,则使用name
  "description": "The app that helps you understand PWA", //用于描述应用
  "display": "standalone", // 定义开发人员对Web应用程序的首选显示模式。standalone模式会有单独的
  "start_url": "/", // 应用启动时的url
  "theme_color": "#313131", // 桌面图标的背景色
  "background_color": "#313131", // 为web应用程序预定义的背景颜色。在启动web应用程序和加载应用程序的内容之间创建了一个平滑的过渡。
  "icons": [ // 桌面图标,是一个数组
    {
    "src": "icon/lowres.webp",
    "sizes": "48x48",  // 以空格分隔的图片尺寸
    "type": "image/webp"  // 帮助userAgent快速排除不支持的类型
  },
  {
    "src": "icon/lowres",
    "sizes": "48x48"
  },
  {
    "src": "icon/hd_hi.ico",
    "sizes": "72x72 96x96 128x128 256x256"
  },
  {
    "src": "icon/hd_hi.svg",
    "sizes": "72x72"
  }
  ]
}
Manifest参考文档:https://developer.mozilla.org/zh-CN/docs/Web/Manifest

可以打开网站https://developers.google.cn/web/showcase/2015/chrome-dev-summit查看添加至主屏幕的动图。

如果用的是安卓手机,可以下载chrome浏览器自己操作看看

3.2 service worker实现离线缓存

3.2.1 什么是service worker

Service Worker 是 Chrome 团队提出和力推的一个 WEB API,用于给 web 应用提供高级的可持续的后台处理能力。

Service Workers 就像介于服务器和网页之间的拦截器,能够拦截进出的HTTP 请求,从而完全控制你的网站。

最主要的特点

  • 在页面中注册并安装成功后,运行于浏览器后台,不受页面刷新的影响,可以监听和截拦作用域范围内所有页面的 HTTP 请求。
  • 网站必须使用 HTTPS。除了使用本地开发环境调试时(如域名使用 localhost)
  • 运行于浏览器后台,可以控制打开的作用域范围下所有的页面请求
  • 单独的作用域范围,单独的运行环境和执行线程
  • 不能操作页面 DOM。但可以通过事件机制来处理
  • 事件驱动型服务线程
为什么要求网站必须是HTTPS的,大概是因为service worker权限太大能拦截所有页面的请求吧,如果http的网站安装service worker很容易被攻击

浏览器支持情况

浏览器支持情况详见: https://caniuse.com/#feat=serviceworkers

生命周期

当用户首次导航至 URL 时,服务器会返回响应的网页。

  • 第1步:当你调用 register() 函数时, Service Worker 开始下载。
  • 第2步:在注册过程中,浏览器会下载、解析并执行 Service Worker ()。如果在此步骤中出现任何错误,register() 返回的 promise 都会执行 reject 操作,并且 Service Worker 会被废弃。
  • 第3步:一旦 Service Worker 成功执行了,install 事件就会激活
  • 第4步:安装完成,Service Worker 便会激活,并控制在其范围内的一切。如果生命周期中的所有事件都成功了,Service Worker 便已准备就绪,随时可以使用了!
chrome://serviceworker-internals 来了解当前浏览器中所有已安装Service Worker的详细情况

3.2.2 HTTP缓存与service worker缓存

  • HTTP缓存

Web 服务器可以使用 Expires 首部来通知 Web 客户端,它可以使用资源的当前副本,直到指定的“过期时间”。反过来,浏览器可以缓存此资源,并且只有在有效期满后才会再次检查新版本。
使用 HTTP 缓存意味着你要依赖服务器来告诉你何时缓存资源和何时过期。

  • service worker缓存

Service Workers 的强大在于它们拦截 HTTP 请求的能力
进入任何传入的 HTTP 请求,并决定想要如何响应。在你的 Service Worker 中,可以编写逻辑来决定想要缓存的资源,以及需要满足什么条件和资源需要缓存多久。一切尽归你掌控!

3.2.3 实现离线缓存

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello Caching World!</title>
  </head>
  <body>
    <!-- Image -->
    <img data-original="/images/hello.png" />                 
    <!-- JavaScript -->
    <script async data-original="/js/script.js"></script>     
    <script>
      // 注册 service worker
      if ('serviceWorker' in navigator) {           
        navigator.serviceWorker.register('/service-worker.js', {scope: '/'}).then(function (registration) {
          // 注册成功
          console.log('ServiceWorker registration successful with scope: ', registration.scope);
        }).catch(function (err) {                   
          // 注册失败 :(
          console.log('ServiceWorker registration failed: ', err);
        });
      }
    </script>
  </body>
</html>
注:Service Worker 的注册路径决定了其 scope 默认作用页面的范围。
如果 service-worker.js 是在 /sw/ 页面路径下,这使得该 Service Worker 默认只会收到 页面/sw/ 路径下的 fetch 事件。
如果存放在网站的根路径下,则将会收到该网站的所有 fetch 事件。
如果希望改变它的作用域,可在第二个参数设置 scope 范围。示例中将其改为了根目录,即对整个站点生效。

service-worker.js

var cacheName = 'helloWorld';     // 缓存的名称  
// install 事件,它发生在浏览器安装并注册 Service Worker 时        
self.addEventListener('install', event => { 
/* event.waitUtil 用于在安装成功之前执行一些预装逻辑
 但是建议只做一些轻量级和非常重要资源的缓存,减少安装失败的概率
 安装成功后 ServiceWorker 状态会从 installing 变为 installed */
  event.waitUntil(
    caches.open(cacheName)                  
    .then(cache => cache.addAll([    // 如果所有的文件都成功缓存了,便会安装完成。如果任何文件下载失败了,那么安装过程也会随之失败。        
      '/js/script.js',
      '/images/hello.png'
    ]))
  );
});
  
/**
为 fetch 事件添加一个事件监听器。接下来,使用 caches.match() 函数来检查传入的请求 URL 是否匹配当前缓存中存在的任何内容。如果存在的话,返回缓存的资源。
如果资源并不存在于缓存当中,通过网络来获取资源,并将获取到的资源添加到缓存中。
*/
self.addEventListener('fetch', function (event) {
  event.respondWith(
    caches.match(event.request)                  
    .then(function (response) {
      if (response) {                            
        return response;                         
      }
      var requestToCache = event.request.clone();  //          
      return fetch(requestToCache).then(                   
        function (response) {
          if (!response || response.status !== 200) {      
            return response;
          }
          var responseToCache = response.clone();          
          caches.open(cacheName)                           
            .then(function (cache) {
              cache.put(requestToCache, responseToCache);  
            });
          return response;             
    })
  );
});
注:为什么用request.clone()和response.clone()
需要这么做是因为request和response是一个流,它只能消耗一次。因为我们已经通过缓存消耗了一次,然后发起 HTTP 请求还要再消耗一次,所以我们需要在此时克隆请求
Clone the request—a request is a stream and can only be consumed once.

3.2.4 调试相关

chrome浏览器打开https://googlechrome.github.io/samples/service-worker/basic/index.html,这是一个实现了service worker离线缓存功能的网站,打开调试工具

介绍一个图中的1.和2.

  1. 勾选可以模拟网站离线情况,勾选后network会有一个黄色警告图标,该网站已经离线。此时刷新页面,页面仍然能够正常显示
  2. 当前service worker的scope。它能够拦截https://googlechrome.github.i...,同样也能够拦截https://googlechrome.github.i...*/*.html下的请求
调试面板具体代表的什么参看https://x5.tencent.com/tbs/guide/serviceworker.html的第三部分

3.3 serice worker实现消息推送

  • 步骤一、提示用户并获得他们的订阅详细信息
  • 步骤二、将这些详细信息保存在服务器上
  • 步骤三、在需要时发送任何消息
不同浏览器需要用不同的推送消息服务器。以 Chrome 上使用 Google Cloud Messaging<GCM> 作为推送服务为例,第一步是注册 applicationServerKey(通过 GCM 注册获取),并在页面上进行订阅或发起订阅。每一个会话会有一个独立的端点(endpoint),订阅对象的属性(PushSubscription.endpoint) 即为端点值。将端点发送给服务器后,服务器用这一值来发送消息给会话的激活的 Service Worker (通过 GCM 与浏览器客户端沟通)。

步骤一和步骤二
index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Progressive Times</title>
    <link rel="manifest" href="/manifest.json">                                      
  </head>
  <body>
    <script>
      var endpoint;
      var key;
      var authSecret;
      var vapidPublicKey = 'BAyb_WgaR0L0pODaR7wWkxJi__tWbM1MPBymyRDFEGjtDCWeRYS9EF7yGoCHLdHJi6hikYdg4MuYaK0XoD0qnoY';
      // 方法很复杂,但是可以不用具体看,知识用来转化vapidPublicKey用
      function urlBase64ToUint8Array(base64String) {                                  
        const padding = '='.repeat((4 - base64String.length % 4) % 4);
        const base64 = (base64String + padding)
          .replace(/\-/g, '+')
          .replace(/_/g, '/');
        const rawData = window.atob(base64);
        const outputArray = new Uint8Array(rawData.length);
        for (let i = 0; i < rawData.length; ++i) {
          outputArray[i] = rawData.charCodeAt(i);
        }
        return outputArray;
      }
      if ('serviceWorker' in navigator) {
        navigator.serviceWorker.register('sw.js').then(function (registration) {
          return registration.pushManager.getSubscription()                            
            .then(function (subscription) {
              if (subscription) {                                                      
                return;
              }
              return registration.pushManager.subscribe({                              
                  userVisibleOnly: true,
                  applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
                })
                .then(function (subscription) {
                  var rawKey = subscription.getKey ? subscription.getKey('p256dh') : '';
                  key = rawKey ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) : '';
                  var rawAuthSecret = subscription.getKey ? subscription.getKey('auth') : '';
                  authSecret = rawAuthSecret ?
                    btoa(String.fromCharCode.apply(null, new Uint8Array(rawAuthSecret))) : '';
                  endpoint = subscription.endpoint;
                  return fetch('./register', {                                         
                    method: 'post',
                    headers: new Headers({
                      'content-type': 'application/json'
                    }),
                    body: JSON.stringify({
                      endpoint: subscription.endpoint,
                      key: key,
                      authSecret: authSecret,
                    }),
                  });
                });
            });
        }).catch(function (err) {
          // 注册失败 :(
          console.log('ServiceWorker registration failed: ', err);
        });
      }
    </script>
  </body>
</html>

步骤三 服务器发送消息给service worker

app.js

const webpush = require('web-push');                 
const express = require('express');
var bodyParser = require('body-parser');
const app = express();
webpush.setVapidDetails(                             
  'mailto:contact@deanhume.com',
  'BAyb_WgaR0L0pODaR7wWkxJi__tWbM1MPBymyRDFEGjtDCWeRYS9EF7yGoCHLdHJi6hikYdg4MuYaK0XoD0qnoY',
  'p6YVD7t8HkABoez1CvVJ5bl7BnEdKUu5bSyVjyxMBh0'
);
app.post('/register', function (req, res) {           
  var endpoint = req.body.endpoint;
  saveRegistrationDetails(endpoint, key, authSecret); 
  const pushSubscription = {                          
    endpoint: req.body.endpoint,
    keys: {
      auth: req.body.authSecret,
      p256dh: req.body.key
    }
  };
  var body = 'Thank you for registering';
  var iconUrl = 'https://example.com/images/homescreen.png';
  // 发送 Web 推送消息
  webpush.sendNotification(pushSubscription,          
      JSON.stringify({
        msg: body,
        url: 'http://localhost:3111/',
        icon: iconUrl
      }))
    .then(result => res.sendStatus(201))
    .catch(err => {
      console.log(err);
    });
});
app.listen(3111, function () {
  console.log('Web push app listening on port 3111!')
});

service worker监听push事件,将通知详情推送给用户

service-worker.js

self.addEventListener('push', function (event) {
 // 检查服务端是否发来了任何有效载荷数据
  var payload = event.data ? JSON.parse(event.data.text()) : 'no payload';
  var title = 'Progressive Times';
  event.waitUntil(
    // 使用提供的信息来显示 Web 推送通知
    self.registration.showNotification(title, {                           
      body: payload.msg,
      url: payload.url,
      icon: payload.icon
    })
  );
});
扩展知识:service worker的更新

总结

PWA的优势

  • 可以将app的快捷方式放置到桌面上,全屏运行,与原生app无异
  • 能够在各种网络环境下使用,包括网络差和断网条件下,不会显示undefind
  • 推送消息的能力
  • 其本质是一个网页,没有原生app的各种启动条件,快速响应用户指令

PWA存在的问题

  • 支持率不高:现在ios手机端不支持pwa,IE也暂时不支持
  • Chrome在中国桌面版占有率还是不错的,安卓移动端上的占有率却很低
  • 各大厂商还未明确支持pwa
  • 依赖的GCM服务在国内无法使用
  • 微信小程序的竞争

尽管有上述的一些缺点,PWA技术仍然有很多可以使用的点。

  • service worker技术实现离线缓存,可以将一些不经常更改的静态文件放到缓存中,提升用户体验。
  • service worker实现消息推送,使用浏览器推送功能,吸引用户
  • 渐进式开发,尽管一些浏览器暂时不支持,可以利用上述技术给使用支持浏览器的用户带来更好的体验。

参考文档

查看原文

前端森林 赞了文章 · 2018-11-06

讲讲PWA

一、背景

文章2017 前端大事件和趋势回顾,2018 何去何从?中提到了2017年前端值得关注的十大事件,其中就提到了PWA。

大家都知道Native app体验确实很好,下载到手机上之后入口也方便。它也有一些缺点:

  • 开发成本高(ios和安卓)
  • 软件上线需要审核
  • 版本更新需要将新版本上传到不同的应用商店
  • 想使用一个app就必须去下载才能使用,即使是偶尔需要使用一下下

而web网页开发成本低,网站更新时上传最新的资源到服务器即可,用手机带的浏览器打开就可以使用。但是除了体验上比Native app还是差一些,还有一些明显的缺点

  • 手机桌面入口不够便捷,想要进入一个页面必须要记住它的url或者加入书签
  • 没网络就没响应,不具备离线能力
  • 不像APP一样能进行消息推送

那么什么是PWA呢?

二、What's PWA?

PWA全称Progressive Web App,即渐进式WEB应用。

一个 PWA 应用首先是一个网页, 可以通过 Web 技术编写出一个网页应用. 随后添加上 App Manifest 和 Service Worker 来实现 PWA 的安装和离线等功能
解决了哪些问题?

  • 可以添加至主屏幕,点击主屏幕图标可以实现启动动画以及隐藏地址栏
  • 实现离线缓存功能,即使用户手机没有网络,依然可以使用一些离线功能
  • 实现了消息推送

它解决了上述提到的问题,这些特性将使得 Web 应用渐进式接近原生 App。

三、PWA的实现

3.1 Manifest实现添加至主屏幕

index.html

<head>
  <title>Minimal PWA</title>
  <meta name="viewport" content="width=device-width, user-scalable=no" />
  <link rel="manifest" href="manifest.json" />
  <link rel="stylesheet" type="text/css" href="main.css">
  <link rel="icon" href="/e.png" type="image/png" />
</head>

manifest.json

{
  "name": "Minimal PWA", // 必填 显示的插件名称
  "short_name": "PWA Demo", // 可选  在APP launcher和新的tab页显示,如果没有设置,则使用name
  "description": "The app that helps you understand PWA", //用于描述应用
  "display": "standalone", // 定义开发人员对Web应用程序的首选显示模式。standalone模式会有单独的
  "start_url": "/", // 应用启动时的url
  "theme_color": "#313131", // 桌面图标的背景色
  "background_color": "#313131", // 为web应用程序预定义的背景颜色。在启动web应用程序和加载应用程序的内容之间创建了一个平滑的过渡。
  "icons": [ // 桌面图标,是一个数组
    {
    "src": "icon/lowres.webp",
    "sizes": "48x48",  // 以空格分隔的图片尺寸
    "type": "image/webp"  // 帮助userAgent快速排除不支持的类型
  },
  {
    "src": "icon/lowres",
    "sizes": "48x48"
  },
  {
    "src": "icon/hd_hi.ico",
    "sizes": "72x72 96x96 128x128 256x256"
  },
  {
    "src": "icon/hd_hi.svg",
    "sizes": "72x72"
  }
  ]
}
Manifest参考文档:https://developer.mozilla.org/zh-CN/docs/Web/Manifest

可以打开网站https://developers.google.cn/web/showcase/2015/chrome-dev-summit查看添加至主屏幕的动图。

如果用的是安卓手机,可以下载chrome浏览器自己操作看看

3.2 service worker实现离线缓存

3.2.1 什么是service worker

Service Worker 是 Chrome 团队提出和力推的一个 WEB API,用于给 web 应用提供高级的可持续的后台处理能力。

Service Workers 就像介于服务器和网页之间的拦截器,能够拦截进出的HTTP 请求,从而完全控制你的网站。

最主要的特点

  • 在页面中注册并安装成功后,运行于浏览器后台,不受页面刷新的影响,可以监听和截拦作用域范围内所有页面的 HTTP 请求。
  • 网站必须使用 HTTPS。除了使用本地开发环境调试时(如域名使用 localhost)
  • 运行于浏览器后台,可以控制打开的作用域范围下所有的页面请求
  • 单独的作用域范围,单独的运行环境和执行线程
  • 不能操作页面 DOM。但可以通过事件机制来处理
  • 事件驱动型服务线程
为什么要求网站必须是HTTPS的,大概是因为service worker权限太大能拦截所有页面的请求吧,如果http的网站安装service worker很容易被攻击

浏览器支持情况

浏览器支持情况详见: https://caniuse.com/#feat=serviceworkers

生命周期

当用户首次导航至 URL 时,服务器会返回响应的网页。

  • 第1步:当你调用 register() 函数时, Service Worker 开始下载。
  • 第2步:在注册过程中,浏览器会下载、解析并执行 Service Worker ()。如果在此步骤中出现任何错误,register() 返回的 promise 都会执行 reject 操作,并且 Service Worker 会被废弃。
  • 第3步:一旦 Service Worker 成功执行了,install 事件就会激活
  • 第4步:安装完成,Service Worker 便会激活,并控制在其范围内的一切。如果生命周期中的所有事件都成功了,Service Worker 便已准备就绪,随时可以使用了!
chrome://serviceworker-internals 来了解当前浏览器中所有已安装Service Worker的详细情况

3.2.2 HTTP缓存与service worker缓存

  • HTTP缓存

Web 服务器可以使用 Expires 首部来通知 Web 客户端,它可以使用资源的当前副本,直到指定的“过期时间”。反过来,浏览器可以缓存此资源,并且只有在有效期满后才会再次检查新版本。
使用 HTTP 缓存意味着你要依赖服务器来告诉你何时缓存资源和何时过期。

  • service worker缓存

Service Workers 的强大在于它们拦截 HTTP 请求的能力
进入任何传入的 HTTP 请求,并决定想要如何响应。在你的 Service Worker 中,可以编写逻辑来决定想要缓存的资源,以及需要满足什么条件和资源需要缓存多久。一切尽归你掌控!

3.2.3 实现离线缓存

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello Caching World!</title>
  </head>
  <body>
    <!-- Image -->
    <img data-original="/images/hello.png" />                 
    <!-- JavaScript -->
    <script async data-original="/js/script.js"></script>     
    <script>
      // 注册 service worker
      if ('serviceWorker' in navigator) {           
        navigator.serviceWorker.register('/service-worker.js', {scope: '/'}).then(function (registration) {
          // 注册成功
          console.log('ServiceWorker registration successful with scope: ', registration.scope);
        }).catch(function (err) {                   
          // 注册失败 :(
          console.log('ServiceWorker registration failed: ', err);
        });
      }
    </script>
  </body>
</html>
注:Service Worker 的注册路径决定了其 scope 默认作用页面的范围。
如果 service-worker.js 是在 /sw/ 页面路径下,这使得该 Service Worker 默认只会收到 页面/sw/ 路径下的 fetch 事件。
如果存放在网站的根路径下,则将会收到该网站的所有 fetch 事件。
如果希望改变它的作用域,可在第二个参数设置 scope 范围。示例中将其改为了根目录,即对整个站点生效。

service-worker.js

var cacheName = 'helloWorld';     // 缓存的名称  
// install 事件,它发生在浏览器安装并注册 Service Worker 时        
self.addEventListener('install', event => { 
/* event.waitUtil 用于在安装成功之前执行一些预装逻辑
 但是建议只做一些轻量级和非常重要资源的缓存,减少安装失败的概率
 安装成功后 ServiceWorker 状态会从 installing 变为 installed */
  event.waitUntil(
    caches.open(cacheName)                  
    .then(cache => cache.addAll([    // 如果所有的文件都成功缓存了,便会安装完成。如果任何文件下载失败了,那么安装过程也会随之失败。        
      '/js/script.js',
      '/images/hello.png'
    ]))
  );
});
  
/**
为 fetch 事件添加一个事件监听器。接下来,使用 caches.match() 函数来检查传入的请求 URL 是否匹配当前缓存中存在的任何内容。如果存在的话,返回缓存的资源。
如果资源并不存在于缓存当中,通过网络来获取资源,并将获取到的资源添加到缓存中。
*/
self.addEventListener('fetch', function (event) {
  event.respondWith(
    caches.match(event.request)                  
    .then(function (response) {
      if (response) {                            
        return response;                         
      }
      var requestToCache = event.request.clone();  //          
      return fetch(requestToCache).then(                   
        function (response) {
          if (!response || response.status !== 200) {      
            return response;
          }
          var responseToCache = response.clone();          
          caches.open(cacheName)                           
            .then(function (cache) {
              cache.put(requestToCache, responseToCache);  
            });
          return response;             
    })
  );
});
注:为什么用request.clone()和response.clone()
需要这么做是因为request和response是一个流,它只能消耗一次。因为我们已经通过缓存消耗了一次,然后发起 HTTP 请求还要再消耗一次,所以我们需要在此时克隆请求
Clone the request—a request is a stream and can only be consumed once.

3.2.4 调试相关

chrome浏览器打开https://googlechrome.github.io/samples/service-worker/basic/index.html,这是一个实现了service worker离线缓存功能的网站,打开调试工具

介绍一个图中的1.和2.

  1. 勾选可以模拟网站离线情况,勾选后network会有一个黄色警告图标,该网站已经离线。此时刷新页面,页面仍然能够正常显示
  2. 当前service worker的scope。它能够拦截https://googlechrome.github.i...,同样也能够拦截https://googlechrome.github.i...*/*.html下的请求
调试面板具体代表的什么参看https://x5.tencent.com/tbs/guide/serviceworker.html的第三部分

3.3 serice worker实现消息推送

  • 步骤一、提示用户并获得他们的订阅详细信息
  • 步骤二、将这些详细信息保存在服务器上
  • 步骤三、在需要时发送任何消息
不同浏览器需要用不同的推送消息服务器。以 Chrome 上使用 Google Cloud Messaging<GCM> 作为推送服务为例,第一步是注册 applicationServerKey(通过 GCM 注册获取),并在页面上进行订阅或发起订阅。每一个会话会有一个独立的端点(endpoint),订阅对象的属性(PushSubscription.endpoint) 即为端点值。将端点发送给服务器后,服务器用这一值来发送消息给会话的激活的 Service Worker (通过 GCM 与浏览器客户端沟通)。

步骤一和步骤二
index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Progressive Times</title>
    <link rel="manifest" href="/manifest.json">                                      
  </head>
  <body>
    <script>
      var endpoint;
      var key;
      var authSecret;
      var vapidPublicKey = 'BAyb_WgaR0L0pODaR7wWkxJi__tWbM1MPBymyRDFEGjtDCWeRYS9EF7yGoCHLdHJi6hikYdg4MuYaK0XoD0qnoY';
      // 方法很复杂,但是可以不用具体看,知识用来转化vapidPublicKey用
      function urlBase64ToUint8Array(base64String) {                                  
        const padding = '='.repeat((4 - base64String.length % 4) % 4);
        const base64 = (base64String + padding)
          .replace(/\-/g, '+')
          .replace(/_/g, '/');
        const rawData = window.atob(base64);
        const outputArray = new Uint8Array(rawData.length);
        for (let i = 0; i < rawData.length; ++i) {
          outputArray[i] = rawData.charCodeAt(i);
        }
        return outputArray;
      }
      if ('serviceWorker' in navigator) {
        navigator.serviceWorker.register('sw.js').then(function (registration) {
          return registration.pushManager.getSubscription()                            
            .then(function (subscription) {
              if (subscription) {                                                      
                return;
              }
              return registration.pushManager.subscribe({                              
                  userVisibleOnly: true,
                  applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
                })
                .then(function (subscription) {
                  var rawKey = subscription.getKey ? subscription.getKey('p256dh') : '';
                  key = rawKey ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) : '';
                  var rawAuthSecret = subscription.getKey ? subscription.getKey('auth') : '';
                  authSecret = rawAuthSecret ?
                    btoa(String.fromCharCode.apply(null, new Uint8Array(rawAuthSecret))) : '';
                  endpoint = subscription.endpoint;
                  return fetch('./register', {                                         
                    method: 'post',
                    headers: new Headers({
                      'content-type': 'application/json'
                    }),
                    body: JSON.stringify({
                      endpoint: subscription.endpoint,
                      key: key,
                      authSecret: authSecret,
                    }),
                  });
                });
            });
        }).catch(function (err) {
          // 注册失败 :(
          console.log('ServiceWorker registration failed: ', err);
        });
      }
    </script>
  </body>
</html>

步骤三 服务器发送消息给service worker

app.js

const webpush = require('web-push');                 
const express = require('express');
var bodyParser = require('body-parser');
const app = express();
webpush.setVapidDetails(                             
  'mailto:contact@deanhume.com',
  'BAyb_WgaR0L0pODaR7wWkxJi__tWbM1MPBymyRDFEGjtDCWeRYS9EF7yGoCHLdHJi6hikYdg4MuYaK0XoD0qnoY',
  'p6YVD7t8HkABoez1CvVJ5bl7BnEdKUu5bSyVjyxMBh0'
);
app.post('/register', function (req, res) {           
  var endpoint = req.body.endpoint;
  saveRegistrationDetails(endpoint, key, authSecret); 
  const pushSubscription = {                          
    endpoint: req.body.endpoint,
    keys: {
      auth: req.body.authSecret,
      p256dh: req.body.key
    }
  };
  var body = 'Thank you for registering';
  var iconUrl = 'https://example.com/images/homescreen.png';
  // 发送 Web 推送消息
  webpush.sendNotification(pushSubscription,          
      JSON.stringify({
        msg: body,
        url: 'http://localhost:3111/',
        icon: iconUrl
      }))
    .then(result => res.sendStatus(201))
    .catch(err => {
      console.log(err);
    });
});
app.listen(3111, function () {
  console.log('Web push app listening on port 3111!')
});

service worker监听push事件,将通知详情推送给用户

service-worker.js

self.addEventListener('push', function (event) {
 // 检查服务端是否发来了任何有效载荷数据
  var payload = event.data ? JSON.parse(event.data.text()) : 'no payload';
  var title = 'Progressive Times';
  event.waitUntil(
    // 使用提供的信息来显示 Web 推送通知
    self.registration.showNotification(title, {                           
      body: payload.msg,
      url: payload.url,
      icon: payload.icon
    })
  );
});
扩展知识:service worker的更新

总结

PWA的优势

  • 可以将app的快捷方式放置到桌面上,全屏运行,与原生app无异
  • 能够在各种网络环境下使用,包括网络差和断网条件下,不会显示undefind
  • 推送消息的能力
  • 其本质是一个网页,没有原生app的各种启动条件,快速响应用户指令

PWA存在的问题

  • 支持率不高:现在ios手机端不支持pwa,IE也暂时不支持
  • Chrome在中国桌面版占有率还是不错的,安卓移动端上的占有率却很低
  • 各大厂商还未明确支持pwa
  • 依赖的GCM服务在国内无法使用
  • 微信小程序的竞争

尽管有上述的一些缺点,PWA技术仍然有很多可以使用的点。

  • service worker技术实现离线缓存,可以将一些不经常更改的静态文件放到缓存中,提升用户体验。
  • service worker实现消息推送,使用浏览器推送功能,吸引用户
  • 渐进式开发,尽管一些浏览器暂时不支持,可以利用上述技术给使用支持浏览器的用户带来更好的体验。

参考文档

查看原文

赞 285 收藏 296 评论 5

前端森林 赞了文章 · 2018-11-06

关于谷歌下载扩展程序

从安装Chrome浏览器后,就点过多次应用商店或者想下载谷歌插件,都是要翻墙才能访问,现在有了谷歌访问助手,可以不翻墙也能访问应用商店,下载插件了,Vue.js devtools,React Developer Tools,都好用了,爽

1.下载

首先先访问谷歌访问助手这个链接,将谷歌访问助手下载

2.步骤

  • 先下载谷歌浏览器对应的文件
clipboard.png
  • 将文件解压
clipboard.png

得到下面这两个文件
clipboard.png

  • 点击谷歌浏览器中右上角的省略号,点击更多工具里的扩展程序,打开后,将解压后里的.crx文件拖到这个页面,后点击添加扩展程序就可以了
clipboard.png
  • 记得打开开发者模式
clipboard.png
  • 他是免费的,不过会有这样的提醒,你点击永久激活后,按照他的提示,添加就可以永久激活了
clipboard.png

3.现在就可以访问应用商店下载插件了

正在努力学习中,若对你的学习有帮助,留下你的印记呗(点个赞咯^_^)
查看原文

赞 38 收藏 27 评论 8

前端森林 赞了文章 · 2018-11-06

18个基于 HTML5 Canvas 开发的图表库

如今,HTML5 可谓如众星捧月一般,受到许多业内巨头的青睐。很多Web开发者也尝试着用 HTML 5 来制作各种各样的富 Web 应用。HTML 5 规范引进了很多新特性,其中之一就是 Canvas 元素。HTML 5 Canvas 提供了通过 JavaScript 绘制图形的方法,非常强大。今天,本文收集了一些非常好的基于 HTML 5 Canvas 的图表方案推荐给大家。

 

1. Visualize

HTML5 Canvas Graphing Solutions Every Web Developers Must Know

Visualize通过Javascript从结构化的HTML表格获取数据并借助HTML5 Canvas把数据转换为可视化的图表。

2. rGraph

[HTML5 Canvas Graphing Solutions Every Web Developers Must Know

rGraph是一个基于HTML5 canvas的图表库。借助HTML5的特性可以生成各种类型的图表,例如饼图、条形图、圆环图、甘特图、雷达图等。

3. iGrapher

HTML5 Canvas Graphing Solutions Every Web Developers Must Know

iGrapher是一个免费的基于Web的,分析和预测股票、货币和商品的市场走势的可视化工具。

4. Function Plotter

HTML5 Canvas Graphing Solutions Every Web Developers Must Know

由Ed Mackey开发的函数图形绘制工具,使用Canvas绘制二维数学函数图形。

5. Building HTML5 Canvas Bar Graph

HTML5 Canvas Graphing Solutions Every Web Developers Must Know

使用HTML5 Canvas元素和Javascript绘制条形图。

6. HTML5 Graph Slider

HTML5 Canvas Graphing Solutions Every Web Developers Must Know

一个动态图形查看器,可通过JavaScript接收数据并即时更新到图表中。

7. Graph.tk

HTML5 Canvas Graphing Solutions Every Web Developers Must Know

Graph.tk是一个开源的图形工具,可以以不同的风格呈现函数曲线。

8. Ticker Plot

HTML5 Canvas Graphing Solutions Every Web Developers Must Know

Ticker plot也是一个开源项目,使用股票符号绘制走势图。

9. HumbleFinance

HTML5 Canvas Graphing Solutions Every Web Developers Must Know

类似于Google的Flash财经图表工具,HumbleFinance是一个基于HTML5 canvas的图表项目。

10. Charting HTML5

HTML5 Canvas Graphing Solutions Every Web Developers Must Know

一个HTML5 canvas实验项目,看看它用于呈现图表的特性。

11. Graphr

HTML5 Canvas Graphing Solutions Every Web Developers Must Know

Graphr是一个使用JavaScript编写的计算器,由Richard Ye开发。

12. Snazzy Animated Pie Chart with HTML5 and jQuery

HTML5 Canvas Graphing Solutions Every Web Developers Must Know

基于HTML5特性开发的饼图工具,拥有漂亮的动画效果。

13. AwesomeJS

HTML5 Canvas Graphing Solutions Every Web Developers Must Know

AwesomeChartJS是一个简单的Javascript库,可用于创建基于HTML 5 Canvas元素的图表。

14. jsGraph

HTML5 Canvas Graphing Solutions Every Web Developers Must Know

jsGraph是一个轻量的javascript库,用于结合HTML5呈现图表。

15. Facebook Privacy

HTML5 Canvas Graphing Solutions Every Web Developers Must Know

基于Prototype开发的可视化数据图表。

16. ASK KEN

HTML5 Canvas Graphing Solutions Every Web Developers Must Know

ASK KEN™是一个序列的节点连接图,可以让你通过可视化导航链接到Freebase服务提供的主题。

17. CanvasXpress

HTML5 Canvas Graphing Solutions Every Web Developers Must Know

CanvasXpress是另外一个结合HTML5特性开发的跨浏览器图表库,兼容主流浏览器。

18. ZingChart

HTML5 Canvas Graphing Solutions Every Web Developers Must Know

如果前面那些开源和免费的图表方案都满足不了你的项目,可以试试ZingChart,这是首个可以以Flash和HTML5
Canvas两种方案呈现图表的图表库。

参考资料

1. Canvas Cheat Sheet

PDF 和 PNG 格式的 HTML5 Canvas 速查手册。

2. How to draw with HTML5 Canvas

如何使用 HTML5 Canvas 元素进行绘图。

3. Mozilla Canvas Tutorial

Mozilla提供的 HTML5 Canvas 元素教程。

4. HTML5 Canvas: The Basics

一步一步学习如何使用 HTML5 Canvas 元素。

5. W3Schools HTML5 Canvas

W3Schools 详细介绍了HTML5 Canvas,附带了基本的例子。

6. HTML5, Flex and Silverlight Charts: A Test of Performance

这篇文章对比了几种图表实现方案的性能:HTML5 charts vs. Flex charts vs.
Silverlight charts

7. HTML5 Canvas Element Guide

非常好的 HTML5 canvas 元素教程。

8. 5 Clever Uses of the Canvas Tag

5个基于 HTML5 canvas 元素实现的非常棒的应用。

9. Canvas Demos

使用 HTML5 canvas 元素实现的游戏、工具和其它应用程序的示例及教程。

 

原文 HTML5 Canvas Graphing Solutions Every Web Developers Must Know
编译 梦想天空
 

查看原文

赞 17 收藏 73 评论 0

前端森林 发布了文章 · 2018-10-25

yarn的使用及与npm的对比

大家在平时的开发中,一定对npm不陌生,它是node自带的包管理器。通过npm可以安装项目对应需要的依赖,但

是时间久了就会发现npm的速度实在是太慢了,今天给大家推荐一个同样为包管理工具,但比npm好用的----yarn

什么是yarn?

Yarn 是 Facebook, Google, Exponent 和 Tilde 开发的一款新的 JavaScript 包管理工具。就像我们可以从官方文档了解那样,它的目的是解决这些团队使用 npm 面临的少数问题,即:

1.安装的时候无法保证速度/一致性
2.安全问题,因为 npm 安装时允许运行代码

Yarn 同样是一个从 npm 注册源获取模块的新的 CLI 客户端。注册的方式不会有任何变化 —— 你同样可以正常获取与发布包。

yarn的特点及优势

速度超快

Yarn 缓存了每个下载过的包,所以再次使用时无需重复下载。 同时利用并行下载以最大化资源利用率,因此安装速度更快。

超级安全

在执行代码之前,Yarn 会通过算法校验每个安装包的完整性。

超级可靠

使用详细、简洁的锁文件格式和明确的安装算法,Yarn 能够保证在不同系统上无差异的工作。

离线模式

如果你以前安装过某个包,再次安装时可以在没有任何互联网连接的情况下进行。

确定性

不管安装顺序如何,相同的依赖关系将在每台机器上以相同的方式安装。

网络性能

Yarn 有效地对请求进行排队处理,避免发起的请求如瀑布般倾泻,以便最大限度地利用网络资源。

相同的软件包

从 npm 安装软件包并保持相同的包管理流程。

网络弹性

重试机制确保单个请求失败并不会导致整个安装失败。

扁平模式

将依赖包的不同版本归结为单个版本,以避免创建多个副本。

使用方法

初始化一个新项目

yarn init

添加依赖包

yarn add [package]
yarn add [package]@[version]
yarn add [package]@[tag]

将依赖项添加到不同依赖项类别中

分别添加到 devDependencies、peerDependencies 和 optionalDependencies 类别中:

yarn add [package] --dev
yarn add [package] --peer
yarn add [package] --optional

升级依赖包

yarn upgrade [package]
yarn upgrade [package]@[version]
yarn upgrade [package]@[tag]

移除依赖包

yarn remove [package]

安装项目的全部依赖

yarn

yarn与npm的不同

语法的不同

yarnnpm
yarn initnpm init
yarnnpm install
yarn global add xxx@x.x.xnpm install xxx@x.x.x -g
yarn add xxx@x.x.xnpm install xxx@x.x.x --save
yarn add xxx@x.x.x --devnpm install xxx@x.x.x --save-dev
yarn remove xxxnpm uninstall xxx --save(-dev)
yarn run xxxnpm run xxx
查看原文

赞 1 收藏 1 评论 0

前端森林 收藏了文章 · 2018-10-19

Windows命令行工具cmder配置

简介

cmder是一个增强型命令行工具,不仅可以使用windows下的所有命令,更爽的是可以使用linux的命令,shell命令。

下载

官网地址:http://cmder.net/

clipboard.png

下载的时候,会有两个版本,分别是mini与full版;唯一的差别在于有没有内建msysgit工具,这是Git for Windows的标准配备;全安装版 cmder 自带了 msysgit, 压缩包 23M, 除了 git 本身这个命令之外, 里面可以使用大量的 linux 命令;比如 grep, curl(没有 wget); 像vim, grep, tar, unzip, ssh, ls, bash, perl 对于爱折腾的Coder更是痛点需求。

安装

直接解压到某个目录就可以了,点击Cmder.exe运行。

clipboard.png

配置环境变量

在系统变量添加

  • 变量名: CMDER_HOME
  • 变量值: 安装绝对路径

clipboard.png
最后在Path添加一条斜体文字
%CMDER_HOME%

clipboard.png

添加 cmder 到右键菜单

配置环境变量后,在管理员权限的终端输入以下语句。
Win8或者Win10可以直接 win+x 再按 a 键进入。

Cmder.exe /REGISTER ALL

clipboard.png

配置好后,任意文件夹右键

clipboard.png

新标签打开个管理员权限终端

快捷键 Ctrl + t 后勾选

clipboard.png

clipboard.png

设置

快捷键:win + alt + p
或者在右下角图标,右击

clipboard.png

设置bash作为默认开启的选项

clipboard.png

解决中文乱码问题

之前在网找了好多方法,可是都解决不了,很多人在在Environment里添加set LANG=zh_CN.UTF-8来解决中文乱码的问题,可是我用这个方法并没有成功,可能是环境的原因吧,我的系统是win10的。
最后找到解决办法:
Settings->Startup->Environment 添加
set LANG=zh_CN.UTF-8
set LC_ALL=zh_CN.utf8

clipboard.png

重启Cmder,发现使用ls,中文正确显示了。

clipboard.png

更改背景

clipboard.png

更换主题

内置了几款不错的主题,当然如果你觉得不合适,当然也支持自己设定。

clipboard.png

常用功能介绍

clipboard.png

如上图示编号的部分说明如下:

1, Cmder常用快捷键

  • 利用Tab,自动路径补全;
  • 利用Ctrl+T建立新页签;利用Ctrl+W关闭页签;
  • 利用Ctrl+Tab切换页签;
  • Alt+F4:关闭所有页签
  • Alt+Shift+1:开启cmd.exe
  • Alt+Shift+2:开启powershell.exe
  • Alt+Shift+3:开启powershell.exe (系统管理员权限)
  • Ctrl+1:快速切换到第1个页签
  • Ctrl+n:快速切换到第n个页签( n值无上限)
  • Alt + enter: 切换到全屏状态;
  • Ctr+r 历史命令搜索

2, 可在视窗内搜寻画面上出现过的任意关键字。

3, 新增页签按钮。

4, 切换页签按钮。

5, 锁定视窗,让视窗无法再输入。

6, 切换视窗是否提供卷轴功能,启动时可查询之前显示过的内容。

7, 按下滑鼠左键可开启系统选单,滑鼠右键可开启工具选项视窗。 Win+Alt+P :开启工具选项视窗。

查看原文

前端森林 收藏了文章 · 2018-10-09

系统认识JavaScript正则表达式

版权声明

转载请告知并注明来源作者
作者唐金健
网络昵称御焱
掘金知乎思否专栏优雅的前端

一、正则表达式简介

1、什么是正则表达式

正则表达式,又称规则表达式。(英语:Regular Expression,在代码中常简写为regex、regexp或RE),计算机科学的一个概念。正则表达式通常被用来检索、替换那些符合某个模式(规则)的文本。

简单的说,就是按照某种规则去匹配符合条件的字符串。

2、可视化正则表达式工具

Regexper:https://regexper.com/

二、RegExp对象

实例化RegExp的两种方式。

两种方式定义RegExp对象。

1、字面量

let reg = /[a-z]{3}/gmi;
let reg = /[a-z]{3}/g;
let reg = /[a-z]{3}/m;
let reg = /[a-z]{3}/i;

标志

  • g global 代表全局搜索。如果不添加,搜索到第一个匹配停止。
  • m Multi-Line 代表多行搜索。
  • i ignore case 代表大小写不敏感,默认大小写敏感。

2、构造函数

let reg = new RegExp('\\bis\\b', 'g');

因为JavaScript字符串中\属于特殊字符,需要转义。

三、元字符

把元字符当作转义字符。

正则表达式有两种基本字符类型组成。

  • 原义文本字符
  • 元字符

1、原义文本字符

表示原本意义上是什么字符,就是什么字符。

2、元字符

是在正则表达式中有特殊含义的非字母字符。
* + ? $ ^ . | \ ( ) { } [ ]

字符含义
\t水平制表符
\v垂直制表符
\n换行符
\r回车符
\0空字符
\f换页符
\cX控制字符,与X对应的控制字符(Ctrl + X)

类似于转义字符。

四、字符类

表示符合某种特性的字符类别。

使用元字符[]可以构建一个简单的类。
所谓类是指符合某些特性的对象,一个泛指,而不是某个字符。

例子

表达式[abc]把字符abc归为一类,表达式可以匹配这一类中的任意一个字符。

// replace() 方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。
'a1b2c3d4e5'.replace(/[abc]/g, '0');  //010203d4e5

字符类取反

我们想要替换不是abc中任意一个字符的字符。

// 元字符 ^ 创建一个 反向类/负向类
'abcdefg'.replace(/[^abc]/g, '0');  //abc0000

五、范围类

匹配这一个范围内的字符。

如果我们想要匹配数字0-9,那么我们可能会这样写[0123456789]
如果我们想要匹配26个字母,那么我们可能会这样写[abcdefghijklmnopqrstuvwxyz]
这样略显麻烦,所以才会有范围类。

例子

// 替换所有数字
'a1c2d3e4f5'.replace(/[0-9]/g, 'x');  //axcxdxexfx
// 替换所有小写字母
'a1c2d3e4f5'.replace(/[a-z]/g, 'x');  //x1x2x3x4x5
// []组成的类内部是可以连写的。替换所有大小写字母
'a1C2d3E4f5G6'.replace(/[a-zA-Z]/g, '*');  //*1*2*3*4*5*6

疑问

如果我想替换数字,并且连带-符号也一起替换呢?

// 替换所有数字和横杠
'2018-5-21'.replace(/[0-9-]/g, '*');  //*********

六、预定义类

一些已经定义的类,可以直接使用。
字符等价类含义
.[^\r\n]除了回车、换行之外的所有字符
\d[0-9]数字字符
\D[^0-9]非数字字符
\s[\t\n\x0B\r]空白符
\S[^\t\n\x0B\r]非空白符
\w[a-zA-Z_0-9]单词字符(字母、数字、下划线)
\W[^a-zA-Z_0-9]非单词字符

例子

替换一个 ab + 数字 + 任意字符 的字符串

// 写法1
'ab0c'.replace(/ab[0-9][^\r\n]/g, 'TangJinJian');  //TangJianJian
// 写法2
'ab0c'.replace(/ab\d./g, 'TangJinJian');  //TangJianJian

七、单词边界

字符含义
^以xxx开始(不在中括号内时的含义)
$以xxx结束
\b单词边界
\B非单词边界

例子

我想替换的字符串,属于那种只在开头出现的。

'YuYan is a boy, YuYan'.replace(/^YuYan/g, 'TangJinJian');  //TangJinJian is a boy, YuYan

我想替换的字符串,属于那种只在结尾出现的。

'YuYan is a boy, YuYan'.replace(/YuYan$/g, 'TangJinJian');  //YuYan is a boy, TangJinJian

单词边界例子。

// 替换所有is为0
'This is a man'.replace(/is/g, '0');  //Th0 0 a man
// 替换所有is前面带有单词边界的字符串
'This is a man'.replace(/\bis/g, '0');  //This 0 a man
// 替换所有is前面没有单词边界的字符串
'This is a man'.replace(/\Bis\b/g, '0');  //Th0 is a man

八、量词

用来处理连续出现的字符串。
字符含义
?出现零次或一次(最多出现一次)
+出现一次或多次(至少出现一次)
*出现零次或多次(任意次)
{n}出现n次
{n,m}出现n到m次
{n,}至少出现n次

我想替换字符串中连续出现10次的数字为*

'1234567890abcd'.replace(/\d{10}/, '*');  //*abcd

我想替换字符串中的QQ号码。

'我的QQ是:10000'.replace(/[1-9][0-9]{4,}/, '19216811');  //我的QQ是:19216811

九、贪婪模式

尽可能多的匹配。

有这样的一种场景下的正则表达式,/\d{3,6}/该替换3个数字还是6个数字呢,4、5个数字?

// 贪婪模式会尽可能的往多的方面去匹配
'123456789'.replace(/\d{3,6}/, 'x');  //x789
'123456789'.replace(/\d+/, 'x');  //x
'123456789'.replace(/\d{3,}/, 'x');  //x

十、非贪婪模式

尽可能少的匹配。

如果我们想要最低限度的替换呢?

// 非贪婪模式使用 ? 尽可能的往少的方面去匹配
'12345678'.replace(/\d{3,6}?/g, 'x');  //xx78
'123456789'.replace(/\d{3,6}?/g, 'x');  //xxx

因为有g标志,会匹配这段字符串里所有符合规则的字符串。
第一个规则/\d{3,6}?/g12345678中有两个符合条件的字符串,是123456。所以替换结果是xx78
第二个规则/\d{3,6}?/g123456789中有三个符合条件的字符串,是123456789。所以替换结果是xxx

十一、分组

括号里的一些规则,分为一组。

我想替换连续出现3次的字母数字

//没有分组的情况下,后面的量词,只是表示匹配3次数字。
'a1b2d3c4'.replace(/[a-z]\d{3}/g, '*');  //a1b2d3c4
//有分组的情况下,分组后面的量词,表示符合这个分组里规则的字符串,匹配3次。
'a1b2d3c4'.replace(/([a-z]\d){3}/g, '*');  //*c4

1、或

分组里有两种规则,只要满足其中一种即可匹配。

//我想把ijaxxy和ijcdxy都替换成*
'ijabxyijcdxy'.replace(/ij(ab|cd)xy/g, '*');  //**

2、反向引用

可以把分组视为变量,来引用。

//我想把改变年月日之间的分隔符
'2018-5-22'.replace(/(\d{4})-(\d{1,2})-(\d{1,2})/g, '$1/$2/$3');  //2018/5/22
//我想替换日期,并且更改顺序
'2018-5-22'.replace(/(\d{4})-(\d{1,2})-(\d{1,2})/g, '$2/$3/$1');  //5/22/2018

3、忽略分组

忽略掉分组,不捕获分组,只需要在分组内加上?:

// 忽略掉匹配年的分组后,匹配月的分组变成了$1,日的分组变成了$2
'2018-5-22'.replace(/(?:\d{4})-(\d{1,2})-(\d{1,2})/g, '$1/$2/$3');  //5/22/$3

十二、前瞻

正则表达式从文本头部向尾部开始解析,文本尾部方向,称为“前”。
前瞻就是在正在表达式匹配到规则的时候,向前检查是否符合断言,后顾/后瞻方向相反。
JavaScript不支持后顾。
符合和不符合特定断言称为肯定/正向匹配和否定/负向匹配。
名称正则含义
正向前瞻exp(?=assert)
负向前瞻exp(?!assert)
正向后顾exp(?<=assert)JavaScript不支持
负向后顾exp(?<!assert)JavaScript不支持

例子

有这样一个单词字符+数字格式的字符串,只要满足这种格式,就把其中的单词字符替换掉。

'a1b2ccdde3'.replace(/\w(?=\d)/g, '*');  //*1*2ccdd*3

有这样一个单词字符+非数字格式的字符串,只要满足这种格式,就把前面的单词字符替换掉。

'a1b2ccdde3'.replace(/\w(?!\d)/g, '*');  //a*b*****e*

十三、RegExp对象属性

global是否全文搜索,默认false
ignore case是否大小写敏感,默认是false
multiline多行搜索,默认值是false
lastIndex是当前表达式匹配内容的最后一个字符的下一个位置。
source正则表达式的文本字符串。

let reg1 = /\w/;
let reg2 = /\w/gim;

reg1.global;  //false
reg1.ignoreCase;  //false
reg1.multiline;  //false

reg2.global;  //true
reg2.ignoreCase;  //true
reg2.multiline;  //true

十四、RegExp对象方法

1、RegExp.prototype.test()

用来查看正则表达式与指定的字符串是否匹配。返回truefalse
let reg1 = /\w/;
reg1.test('a');  //true
reg1.test('*');  //false

加上g标志之后,会有些区别。

let reg1 = /\w/g;
// 第一遍
reg1.test('ab');  //true
// 第二遍
reg1.test('ab');  //true
// 第三遍
reg1.test('ab');  //false
// 第四遍
reg1.test('ab');  //true
// 第五遍
reg1.test('ab');  //true
// 第六遍
reg1.test('ab');  //false

实际上这是因为RegExp.lastIndex。每次匹配到之后,lasgIndex会改变。
lastIndex是正则表达式的一个可读可写的整型属性,用来指定下一次匹配的起始索引。

let reg = /\w/g;
// 每次匹配到,就会把lastIndex指向匹配到的字符串后一个字符的索引。
while(reg.test('ab')) {
    console.log(reg.lastIndex);
}
// 1
// 2

reg.lastIndex初始时为0,第一个次匹配到a的时候,reg.lastIndex1。第二次匹配到b的时候,reg.lastIndex2

let reg = /\w\w/g;
while(reg.test('ab12cd')) {
  console.log(reg.lastIndex);
}
// 2
// 4
// 6

reg.lastIndex初始时为0,第一个次匹配到ab的时候,reg.lastIndex2。第二次匹配到12的时候,reg.lastIndex4。第三次匹配到cd的时候,reg.lastIndex6

let reg = /\w/g;
// 匹配不到符合正则的字符串之后,lastIndex会变为0。
while(reg.test('ab')) {
    console.log(reg.lastIndex);
}
console.log(reg.lastIndex);
reg.test('ab');
console.log(reg.lastIndex);
// 1
// 2
// 0
// 1

所以,这就是为什么reg.test('ab')再多次执行之后,返回值为false的原因了。

let reg = /\w/g;
reg.lastIndex = 2;
reg.test('ab');  //false

每次匹配的起始位置,是以lastIndex为起始位置的。上述例子,一开始从位置2开始匹配,位置2后面没有符合正则的字符串,所以为false

2、RegExp.prototype.exec()

在一个指定字符串中执行一个搜索匹配。返回一个搜索的结果数组或null

非全局情况

let reg = /\d(\w)\d/;
let ts = '*1a2b3c';
let ret = reg.exec(ts);  //ret是结果数组
// reg.lastIndex肯定是0,因为没有g标志。 没有g标志的情况下,lastIndex被忽略。
console.log(reg.lastIndex + '\t' + ret.index + '\t' + ret.toString());
console.log(ret);
// 0  1 1a2,a
// ["1a2", "a"]

返回数组是有以下元素组成的:

  • 第一个元素是与正则表达式相匹配的文本。
  • 第二个元素是reg对象的第一个子表达式相匹配的文本(如果有的话)。
  • 第二个元素是reg对象的第二个子表达式相匹配的文本(如果有的话),以此类推。
// 子表达式就是分组。
let reg = /\d(\w)(\w)(\w)\d/;
let ts = '*1a2b3c';
let ret = reg.exec(ts);
console.log(reg.lastIndex + '\t' + ret.index + '\t' + ret.toString());
console.log(ret);  //输出结果数组
// 0  1 1a2b3,a,2,b
// ["1a2b3", "a", "2", "b"]

全局情况

let reg = /\d(\w)(\w)(\w)\d/g;
let ts = '*1abc25def3g';
while(ret = reg.exec(ts)) {
    console.log(reg.lastIndex + '\t' + ret.index + '\t' + ret.toString());
}
// 6  1 1abc2,a,b,c
// 11 6 5def3,d,e,f

第一次匹配的是1abc21abc2的后一个字符的起始位置是6,所以reg.lastIndex6
1abc2的第一个字符的起始位置是1,所以ret.index1

第二次匹配的是5def35def3的后一个字符的起始位置是11,所以reg.lastIndex11
5def3的第一个字符的起始位置是6,所以ret.index6

十五、字符串对象方法

1、String.prototype.search()

执行正则表达式和String对象之间的一个搜索匹配。
方法返回第一个匹配项的index,搜索不到返回-1
不执行全局匹配,忽略g标志,并且总是从字符串的开始进行检索。

我想知道Jin字符串的起始位置在哪里。

'TangJinJian'.search('Jin');  //4
'TangJinJian'.search(/Jin/);  //4

search方法,既可以通过字符串,也可以通过正则描述字符串来搜索匹配。

2、String.prototype.match()

当一个字符串与一个正则表达式匹配时, match()方法检索匹配项。
提供RegExp对象参数是否具有g标志,对结果影响很大。

非全局调用的情况

如果RegExp没有g标志,那么match只能在字符串中,执行一次匹配。
如果没有找到任何匹配文本,将返回null
否则将返回一个数组,其中存放了与它找到的匹配文本有关的信息。

let reg = /\d(\w)\d/;
let ts = '*1a2b3c';
let ret = ts.match(reg);
console.log(ret.index + '\t' + reg.lastIndex);
console.log(ret);
// 1  0
// ["1a2", "a"]

非全局情况下和RegExp.prototype.exec()方法的效果是一样的。

全局调用的情况

我想找到所有数字+单词+数字格式的字符串。

let reg = /\d(\w)\d/g;
let ts = '*1a2b3c4e';
let ret = ts.match(reg);
console.log(ret.index + '\t' + reg.lastIndex);
console.log(ret);
// undefined  0
// ["1a2", "3c4"]

全局情况下和RegExp.prototype.exec()方法的区别。在于,没有了分组信息。
如果我们不使用到分组信息,那么使用String.prototype.match()方法,效率要高一些。而且不需要写循环来逐个所有的匹配项获取。

3、String.prototype.split()

使用指定的分隔符字符串将一个String对象分割成字符串数组。
'a,b,c,d'.split(/,/);  //["a", "b", "c", "d"]
'a1b2c3d'.split(/\d/);  //["a", "b", "c", "d"]
'a1b-c|d'.split(/[\d-|]/);  //["a", "b", "c", "d"]

4、String.prototype.replace()

返回一个由替换值替换一些或所有匹配的模式后的新字符串。模式可以是一个字符串或者一个正则表达式, 替换值可以是一个字符串或者一个每次匹配都要调用的函数。

常规用法

'TangJinJian'.replace('Tang', '');  //JinJian
'TangJinJian'.replace(/Ji/g, '*');  //Tang*n*an

以上两种用法,是最常用的,但是还不能精细化控制。

精细化用法

我想要把a1b2c3d4中的数字都加一,变成a2b3c4d5

'a1b2c3d4'.replace(/\d/g, function(match, index, orgin) {
    console.log(index);
    return parseInt(match) + 1;
});
// 1
// 3
// 5
// 7
// a2b3c4d5

回调函数有以下参数:

  • match第一个参数。匹配到的字符串。
  • group第二个参数。分组,如果有n个分组,则以此类推n个group参数,下面两个参数将变为第2+n3+n个参数。没有分组,则没有该参数。
  • index第三个参数。匹配到的字符串第一个字符索引位置。
  • orgin第四个参数。源字符串。

我想把两个数字之间的字母去掉。

'a1b2c3d4e5f6'.replace(/(\d)(\w)(\d)/g, function(match, group1, group2, group3, index, orgin) {
  console.log(match);
  return group1 + group3;
});
// 1b2
// 3d4
// 5f6
// a12c34e56
查看原文

前端森林 赞了文章 · 2018-10-09

系统认识JavaScript正则表达式

版权声明

转载请告知并注明来源作者
作者唐金健
网络昵称御焱
掘金知乎思否专栏优雅的前端

一、正则表达式简介

1、什么是正则表达式

正则表达式,又称规则表达式。(英语:Regular Expression,在代码中常简写为regex、regexp或RE),计算机科学的一个概念。正则表达式通常被用来检索、替换那些符合某个模式(规则)的文本。

简单的说,就是按照某种规则去匹配符合条件的字符串。

2、可视化正则表达式工具

Regexper:https://regexper.com/

二、RegExp对象

实例化RegExp的两种方式。

两种方式定义RegExp对象。

1、字面量

let reg = /[a-z]{3}/gmi;
let reg = /[a-z]{3}/g;
let reg = /[a-z]{3}/m;
let reg = /[a-z]{3}/i;

标志

  • g global 代表全局搜索。如果不添加,搜索到第一个匹配停止。
  • m Multi-Line 代表多行搜索。
  • i ignore case 代表大小写不敏感,默认大小写敏感。

2、构造函数

let reg = new RegExp('\\bis\\b', 'g');

因为JavaScript字符串中\属于特殊字符,需要转义。

三、元字符

把元字符当作转义字符。

正则表达式有两种基本字符类型组成。

  • 原义文本字符
  • 元字符

1、原义文本字符

表示原本意义上是什么字符,就是什么字符。

2、元字符

是在正则表达式中有特殊含义的非字母字符。
* + ? $ ^ . | \ ( ) { } [ ]

字符含义
\t水平制表符
\v垂直制表符
\n换行符
\r回车符
\0空字符
\f换页符
\cX控制字符,与X对应的控制字符(Ctrl + X)

类似于转义字符。

四、字符类

表示符合某种特性的字符类别。

使用元字符[]可以构建一个简单的类。
所谓类是指符合某些特性的对象,一个泛指,而不是某个字符。

例子

表达式[abc]把字符abc归为一类,表达式可以匹配这一类中的任意一个字符。

// replace() 方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。
'a1b2c3d4e5'.replace(/[abc]/g, '0');  //010203d4e5

字符类取反

我们想要替换不是abc中任意一个字符的字符。

// 元字符 ^ 创建一个 反向类/负向类
'abcdefg'.replace(/[^abc]/g, '0');  //abc0000

五、范围类

匹配这一个范围内的字符。

如果我们想要匹配数字0-9,那么我们可能会这样写[0123456789]
如果我们想要匹配26个字母,那么我们可能会这样写[abcdefghijklmnopqrstuvwxyz]
这样略显麻烦,所以才会有范围类。

例子

// 替换所有数字
'a1c2d3e4f5'.replace(/[0-9]/g, 'x');  //axcxdxexfx
// 替换所有小写字母
'a1c2d3e4f5'.replace(/[a-z]/g, 'x');  //x1x2x3x4x5
// []组成的类内部是可以连写的。替换所有大小写字母
'a1C2d3E4f5G6'.replace(/[a-zA-Z]/g, '*');  //*1*2*3*4*5*6

疑问

如果我想替换数字,并且连带-符号也一起替换呢?

// 替换所有数字和横杠
'2018-5-21'.replace(/[0-9-]/g, '*');  //*********

六、预定义类

一些已经定义的类,可以直接使用。
字符等价类含义
.[^\r\n]除了回车、换行之外的所有字符
\d[0-9]数字字符
\D[^0-9]非数字字符
\s[\t\n\x0B\r]空白符
\S[^\t\n\x0B\r]非空白符
\w[a-zA-Z_0-9]单词字符(字母、数字、下划线)
\W[^a-zA-Z_0-9]非单词字符

例子

替换一个 ab + 数字 + 任意字符 的字符串

// 写法1
'ab0c'.replace(/ab[0-9][^\r\n]/g, 'TangJinJian');  //TangJianJian
// 写法2
'ab0c'.replace(/ab\d./g, 'TangJinJian');  //TangJianJian

七、单词边界

字符含义
^以xxx开始(不在中括号内时的含义)
$以xxx结束
\b单词边界
\B非单词边界

例子

我想替换的字符串,属于那种只在开头出现的。

'YuYan is a boy, YuYan'.replace(/^YuYan/g, 'TangJinJian');  //TangJinJian is a boy, YuYan

我想替换的字符串,属于那种只在结尾出现的。

'YuYan is a boy, YuYan'.replace(/YuYan$/g, 'TangJinJian');  //YuYan is a boy, TangJinJian

单词边界例子。

// 替换所有is为0
'This is a man'.replace(/is/g, '0');  //Th0 0 a man
// 替换所有is前面带有单词边界的字符串
'This is a man'.replace(/\bis/g, '0');  //This 0 a man
// 替换所有is前面没有单词边界的字符串
'This is a man'.replace(/\Bis\b/g, '0');  //Th0 is a man

八、量词

用来处理连续出现的字符串。
字符含义
?出现零次或一次(最多出现一次)
+出现一次或多次(至少出现一次)
*出现零次或多次(任意次)
{n}出现n次
{n,m}出现n到m次
{n,}至少出现n次

我想替换字符串中连续出现10次的数字为*

'1234567890abcd'.replace(/\d{10}/, '*');  //*abcd

我想替换字符串中的QQ号码。

'我的QQ是:10000'.replace(/[1-9][0-9]{4,}/, '19216811');  //我的QQ是:19216811

九、贪婪模式

尽可能多的匹配。

有这样的一种场景下的正则表达式,/\d{3,6}/该替换3个数字还是6个数字呢,4、5个数字?

// 贪婪模式会尽可能的往多的方面去匹配
'123456789'.replace(/\d{3,6}/, 'x');  //x789
'123456789'.replace(/\d+/, 'x');  //x
'123456789'.replace(/\d{3,}/, 'x');  //x

十、非贪婪模式

尽可能少的匹配。

如果我们想要最低限度的替换呢?

// 非贪婪模式使用 ? 尽可能的往少的方面去匹配
'12345678'.replace(/\d{3,6}?/g, 'x');  //xx78
'123456789'.replace(/\d{3,6}?/g, 'x');  //xxx

因为有g标志,会匹配这段字符串里所有符合规则的字符串。
第一个规则/\d{3,6}?/g12345678中有两个符合条件的字符串,是123456。所以替换结果是xx78
第二个规则/\d{3,6}?/g123456789中有三个符合条件的字符串,是123456789。所以替换结果是xxx

十一、分组

括号里的一些规则,分为一组。

我想替换连续出现3次的字母数字

//没有分组的情况下,后面的量词,只是表示匹配3次数字。
'a1b2d3c4'.replace(/[a-z]\d{3}/g, '*');  //a1b2d3c4
//有分组的情况下,分组后面的量词,表示符合这个分组里规则的字符串,匹配3次。
'a1b2d3c4'.replace(/([a-z]\d){3}/g, '*');  //*c4

1、或

分组里有两种规则,只要满足其中一种即可匹配。

//我想把ijaxxy和ijcdxy都替换成*
'ijabxyijcdxy'.replace(/ij(ab|cd)xy/g, '*');  //**

2、反向引用

可以把分组视为变量,来引用。

//我想把改变年月日之间的分隔符
'2018-5-22'.replace(/(\d{4})-(\d{1,2})-(\d{1,2})/g, '$1/$2/$3');  //2018/5/22
//我想替换日期,并且更改顺序
'2018-5-22'.replace(/(\d{4})-(\d{1,2})-(\d{1,2})/g, '$2/$3/$1');  //5/22/2018

3、忽略分组

忽略掉分组,不捕获分组,只需要在分组内加上?:

// 忽略掉匹配年的分组后,匹配月的分组变成了$1,日的分组变成了$2
'2018-5-22'.replace(/(?:\d{4})-(\d{1,2})-(\d{1,2})/g, '$1/$2/$3');  //5/22/$3

十二、前瞻

正则表达式从文本头部向尾部开始解析,文本尾部方向,称为“前”。
前瞻就是在正在表达式匹配到规则的时候,向前检查是否符合断言,后顾/后瞻方向相反。
JavaScript不支持后顾。
符合和不符合特定断言称为肯定/正向匹配和否定/负向匹配。
名称正则含义
正向前瞻exp(?=assert)
负向前瞻exp(?!assert)
正向后顾exp(?<=assert)JavaScript不支持
负向后顾exp(?<!assert)JavaScript不支持

例子

有这样一个单词字符+数字格式的字符串,只要满足这种格式,就把其中的单词字符替换掉。

'a1b2ccdde3'.replace(/\w(?=\d)/g, '*');  //*1*2ccdd*3

有这样一个单词字符+非数字格式的字符串,只要满足这种格式,就把前面的单词字符替换掉。

'a1b2ccdde3'.replace(/\w(?!\d)/g, '*');  //a*b*****e*

十三、RegExp对象属性

global是否全文搜索,默认false
ignore case是否大小写敏感,默认是false
multiline多行搜索,默认值是false
lastIndex是当前表达式匹配内容的最后一个字符的下一个位置。
source正则表达式的文本字符串。

let reg1 = /\w/;
let reg2 = /\w/gim;

reg1.global;  //false
reg1.ignoreCase;  //false
reg1.multiline;  //false

reg2.global;  //true
reg2.ignoreCase;  //true
reg2.multiline;  //true

十四、RegExp对象方法

1、RegExp.prototype.test()

用来查看正则表达式与指定的字符串是否匹配。返回truefalse
let reg1 = /\w/;
reg1.test('a');  //true
reg1.test('*');  //false

加上g标志之后,会有些区别。

let reg1 = /\w/g;
// 第一遍
reg1.test('ab');  //true
// 第二遍
reg1.test('ab');  //true
// 第三遍
reg1.test('ab');  //false
// 第四遍
reg1.test('ab');  //true
// 第五遍
reg1.test('ab');  //true
// 第六遍
reg1.test('ab');  //false

实际上这是因为RegExp.lastIndex。每次匹配到之后,lasgIndex会改变。
lastIndex是正则表达式的一个可读可写的整型属性,用来指定下一次匹配的起始索引。

let reg = /\w/g;
// 每次匹配到,就会把lastIndex指向匹配到的字符串后一个字符的索引。
while(reg.test('ab')) {
    console.log(reg.lastIndex);
}
// 1
// 2

reg.lastIndex初始时为0,第一个次匹配到a的时候,reg.lastIndex1。第二次匹配到b的时候,reg.lastIndex2

let reg = /\w\w/g;
while(reg.test('ab12cd')) {
  console.log(reg.lastIndex);
}
// 2
// 4
// 6

reg.lastIndex初始时为0,第一个次匹配到ab的时候,reg.lastIndex2。第二次匹配到12的时候,reg.lastIndex4。第三次匹配到cd的时候,reg.lastIndex6

let reg = /\w/g;
// 匹配不到符合正则的字符串之后,lastIndex会变为0。
while(reg.test('ab')) {
    console.log(reg.lastIndex);
}
console.log(reg.lastIndex);
reg.test('ab');
console.log(reg.lastIndex);
// 1
// 2
// 0
// 1

所以,这就是为什么reg.test('ab')再多次执行之后,返回值为false的原因了。

let reg = /\w/g;
reg.lastIndex = 2;
reg.test('ab');  //false

每次匹配的起始位置,是以lastIndex为起始位置的。上述例子,一开始从位置2开始匹配,位置2后面没有符合正则的字符串,所以为false

2、RegExp.prototype.exec()

在一个指定字符串中执行一个搜索匹配。返回一个搜索的结果数组或null

非全局情况

let reg = /\d(\w)\d/;
let ts = '*1a2b3c';
let ret = reg.exec(ts);  //ret是结果数组
// reg.lastIndex肯定是0,因为没有g标志。 没有g标志的情况下,lastIndex被忽略。
console.log(reg.lastIndex + '\t' + ret.index + '\t' + ret.toString());
console.log(ret);
// 0  1 1a2,a
// ["1a2", "a"]

返回数组是有以下元素组成的:

  • 第一个元素是与正则表达式相匹配的文本。
  • 第二个元素是reg对象的第一个子表达式相匹配的文本(如果有的话)。
  • 第二个元素是reg对象的第二个子表达式相匹配的文本(如果有的话),以此类推。
// 子表达式就是分组。
let reg = /\d(\w)(\w)(\w)\d/;
let ts = '*1a2b3c';
let ret = reg.exec(ts);
console.log(reg.lastIndex + '\t' + ret.index + '\t' + ret.toString());
console.log(ret);  //输出结果数组
// 0  1 1a2b3,a,2,b
// ["1a2b3", "a", "2", "b"]

全局情况

let reg = /\d(\w)(\w)(\w)\d/g;
let ts = '*1abc25def3g';
while(ret = reg.exec(ts)) {
    console.log(reg.lastIndex + '\t' + ret.index + '\t' + ret.toString());
}
// 6  1 1abc2,a,b,c
// 11 6 5def3,d,e,f

第一次匹配的是1abc21abc2的后一个字符的起始位置是6,所以reg.lastIndex6
1abc2的第一个字符的起始位置是1,所以ret.index1

第二次匹配的是5def35def3的后一个字符的起始位置是11,所以reg.lastIndex11
5def3的第一个字符的起始位置是6,所以ret.index6

十五、字符串对象方法

1、String.prototype.search()

执行正则表达式和String对象之间的一个搜索匹配。
方法返回第一个匹配项的index,搜索不到返回-1
不执行全局匹配,忽略g标志,并且总是从字符串的开始进行检索。

我想知道Jin字符串的起始位置在哪里。

'TangJinJian'.search('Jin');  //4
'TangJinJian'.search(/Jin/);  //4

search方法,既可以通过字符串,也可以通过正则描述字符串来搜索匹配。

2、String.prototype.match()

当一个字符串与一个正则表达式匹配时, match()方法检索匹配项。
提供RegExp对象参数是否具有g标志,对结果影响很大。

非全局调用的情况

如果RegExp没有g标志,那么match只能在字符串中,执行一次匹配。
如果没有找到任何匹配文本,将返回null
否则将返回一个数组,其中存放了与它找到的匹配文本有关的信息。

let reg = /\d(\w)\d/;
let ts = '*1a2b3c';
let ret = ts.match(reg);
console.log(ret.index + '\t' + reg.lastIndex);
console.log(ret);
// 1  0
// ["1a2", "a"]

非全局情况下和RegExp.prototype.exec()方法的效果是一样的。

全局调用的情况

我想找到所有数字+单词+数字格式的字符串。

let reg = /\d(\w)\d/g;
let ts = '*1a2b3c4e';
let ret = ts.match(reg);
console.log(ret.index + '\t' + reg.lastIndex);
console.log(ret);
// undefined  0
// ["1a2", "3c4"]

全局情况下和RegExp.prototype.exec()方法的区别。在于,没有了分组信息。
如果我们不使用到分组信息,那么使用String.prototype.match()方法,效率要高一些。而且不需要写循环来逐个所有的匹配项获取。

3、String.prototype.split()

使用指定的分隔符字符串将一个String对象分割成字符串数组。
'a,b,c,d'.split(/,/);  //["a", "b", "c", "d"]
'a1b2c3d'.split(/\d/);  //["a", "b", "c", "d"]
'a1b-c|d'.split(/[\d-|]/);  //["a", "b", "c", "d"]

4、String.prototype.replace()

返回一个由替换值替换一些或所有匹配的模式后的新字符串。模式可以是一个字符串或者一个正则表达式, 替换值可以是一个字符串或者一个每次匹配都要调用的函数。

常规用法

'TangJinJian'.replace('Tang', '');  //JinJian
'TangJinJian'.replace(/Ji/g, '*');  //Tang*n*an

以上两种用法,是最常用的,但是还不能精细化控制。

精细化用法

我想要把a1b2c3d4中的数字都加一,变成a2b3c4d5

'a1b2c3d4'.replace(/\d/g, function(match, index, orgin) {
    console.log(index);
    return parseInt(match) + 1;
});
// 1
// 3
// 5
// 7
// a2b3c4d5

回调函数有以下参数:

  • match第一个参数。匹配到的字符串。
  • group第二个参数。分组,如果有n个分组,则以此类推n个group参数,下面两个参数将变为第2+n3+n个参数。没有分组,则没有该参数。
  • index第三个参数。匹配到的字符串第一个字符索引位置。
  • orgin第四个参数。源字符串。

我想把两个数字之间的字母去掉。

'a1b2c3d4e5f6'.replace(/(\d)(\w)(\d)/g, function(match, group1, group2, group3, index, orgin) {
  console.log(match);
  return group1 + group3;
});
// 1b2
// 3d4
// 5f6
// a12c34e56
查看原文

赞 17 收藏 27 评论 0

前端森林 发布了文章 · 2018-10-09

正则表达式真的很6,可惜你不会写

本文旨在用最通俗的语言讲述最枯燥的基本知识。

1.元字符
2.重复限定符
3.分组
4.转义
5.条件或
6.区间

正则表达式在几乎所有语言中都可以使用,无论是前端的 JavaScript、还是后端的 Java、c#。他们都提供相应的接口 / 函数支持正则表达式。

但很神奇的是:无论你大学选择哪一门计算机语言,都没有关于正则表达式的课程给你修,在你学会正则之前,你只能看着那些正则大师们,写了一串外星文似的字符串,替代了你用一大篇幅的 if else 代码来做一些数据校验。

下面,我尝试用一种比较通俗点的方式讲一下正则,让你能在读完之后,能自己写出一些简单的正则,再不济,能看到别人写的正则,那也不错了。

元字符

万物皆有缘,正则也是如此,元字符是构造正则表达式的一种基本元素。

我们先来记几个常用的元字符:

元字符说明
.匹配除换行符以外的任意字符
\w匹配字母或数字或下划线或汉字
\s匹配任意的空白符
\d匹配数字
\b匹配单词的开始或结束
^匹配字符串的开始
$匹配字符串的结束

有了元字符之后,我们就可以利用这些元字符来写一些简单的正则表达式了,
比如:

匹配有abc开头的字符串:

\babc或者^abc

匹配8位数字的QQ号码:

^\d\d\d\d\d\d\d\d$

匹配1开头11位数字的手机号码:

^1\d\d\d\d\d\d\d\d\d\d$

重复限定符

有了元字符就可以写不少的正则表达式了,但细心的你们可能会发现:别人写的正则简洁明了,而我写的正则是由一堆乱七八糟而且重复的元字符组成的。正则没提供办法处理这些重复的元字符吗?

答案是有的!

为了处理这些重复问题,正则表达式中一些重复限定符,把重复部分用合适的限定符替代,下面我们来看一些限定符:

语法说明
*重复零次或更多次
+重复一次或更多次
?重复零次或一次
{n}重复n次
{n,}重复n次或更多次
{n,m}重复n到m次

有了这些限定符之后,我们就可以对之前的正则表达式进行改造了,比如:

匹配8位数字的QQ号码:

^\d{8}$

匹配1开头11位数字的手机号码:

^1\d{10}$

匹配银行卡号是14~18位的数字:

^\d{14,18}$

匹配以a开头的,0个或多个b结尾的字符串

^ab*$

分组

从上面的例子(4)中看到,限定符是作用在与他左边最近的一个字符,那么问题来了,如果我想要 ab 同时被限定那怎么办呢?

正则表达式中用小括号 () 来做分组,也就是括号中的内容作为一个整体。

因此当我们要匹配多个 ab 时,我们可以这样。

^(ab)*

转义

我们看到正则表达式用小括号来做分组,那么问题来了:

如果要匹配的字符串中本身就包含小括号,那是不是冲突?应该怎么办?

针对这种情况,正则提供了转义的方式,也就是要把这些元字符、限定符或者关键字转义成普通的字符,做法很简答,就是在要转义的字符前面加个斜杠,也就是即可。如:要匹配以 (ab) 开头:

^(\(ab\))*

条件或

回到我们刚才的手机号匹配,我们都知道:国内号码都来自三大网,它们都有属于自己的号段,比如联通有 130/131/132/155/156/185/186/145/176 等号段,假如让我们匹配一个联通的号码,那按照我们目前所学到的正则,应该无从下手的,因为这里包含了一些并列的条件,也就是“或”,那么在正则中是如何表示“或”的呢?

正则用符号 | 来表示或,也叫做分支条件,当满足正则里的分支条件的任何一种条件时,都会当成是匹配成功。

那么我们就可以用或条件来处理这个问题:

^(130|131|132|155|156|185|186|145|176)\d{8}$

区间

实际是有的。

正则提供一个元字符中括号 [] 来表示区间条件。

  • 限定 0 到 9 可以写成 [0-9]
  • 限定 A-Z 写成 [A-Z]
  • 限定某些数字 [165]

那上面的正则我们还改成这样:

^((13[0-2])|(15[56])|(18[5-6])|145|176)\d{8}$

反义

前面说到元字符的都是要匹配什么什么,当然如果你想反着来,不想匹配某些字符,正则也提供了一些常用的反义元字符:

反义元字符说明
\W匹配任意不是字母或数字或下划线或汉字的字符
\S匹配任意不是空白符的字符
\D匹配任意非数字的字符
\B匹配不是单词的开始或结束的字符
[^x]匹配除了x以外的任意字符
[^axios]匹配除了axios这几个字母以外的任意字符

欢迎关注我的微信公众号~前端森林

前端森林.jpeg

查看原文

赞 5 收藏 3 评论 0

前端森林 发布了文章 · 2018-09-28

web性能优化

几乎所有的开发者都会面临着开发的网站存在加载问题,想要加快网页的加载速度。前端的页面更需要在性能优化上下功夫,只有这样才能实现更好的用户体验。本文从构建、浏览器渲染、缓存、PWA、服务端优化等多方面,梳理前端性能优化的技术点、综合分析技术的原理,根据不同的业务场景选择合适的性能优化点进行应用,最终为你的网站带来显著的速度提升和整体性能提升。

先看一下早期CS架构的开发与部署过程

图片描述

再看一下现在BS架构的开发与部署过程

图片描述

从地址栏输入url到页面渲染出来,整个过程经历了什么(一道经典的面试题。。)让我们用一张图来了解整个过程:

图片描述

从上图这个请求过程,我们可以思考一下其中一些潜在的性能优化点:

  • dns是否可以通过缓存减少dns查询时间?
  • 网络请求的过程走最近的网络环境?
  • 相同的静态资源是否可以缓存?
  • 能否减少请求http的请求大小?
  • 减少http请求数量?
  • 服务端渲染?

综上,我们得出深入理解http请求过程是前端性能优化的核心

资源合并与压缩

html压缩

图片描述

HTML代码压缩就是压缩这些在文本文件中有意义,但是在HTML中不显示的字符,包括空格制表符换行符等,还有一些其他意义的字符,如HTML注释也可以被压缩。

HTML代码压缩的意义:
以google为例,google的流量,占到整个互联网的40%,2016年全球网络流量达到1.3ZB(1ZB=10^9TB),那么google在2016年的流量就是1.3ZB*40%,如果google每1MB请求减少一个字节,每年可以节省流量近500TB

那么如何进行HTML的压缩呢?

  • 使用在线网站进行压缩
  • nodejs提供了html-minifier工具
  • 后端模版引擎渲染压缩

css及js压缩

图片描述

如何进行css压缩:

  • 使用在线网站进行压缩
  • 使用html-minifier对html中的css进行压缩
  • 使用clean-css对css进行压缩

图片描述

如何进行js压缩和混乱:

  • 使用在线网站进行压缩
  • 使用html-minifier对html中对js进行压缩
  • 使用uglify2对js进行压缩

文件合并

图片描述

但是文件合并也存在它自己的问题:

  • 首屏渲染问题
  • 缓存失效问题

解决方案:

  • 公共库合并
  • 不同页面的合并
  • 随机应变,根据业务场景作出抉择,选出最优方案

如何进行文件合并:

  • 使用在线网站进行文件合并
  • 使用nodejs实现文件合并

图片相关的优化

一张JPG图片的解析过程

图片描述

png8、png24、png32之间的区别

区别主要还是文件大小和色彩的丰富程度

  • png8:256色 + 支持透明
  • png24:2^24色 + 不支持透明
  • png32:2^24色 + 支持透明

每种图片格式都有自己的特点,针对不同的业务场景选择不同的图片格式很重要。

不同格式图片常用的业务场景

先看一下四种常用图片格式的各自特点吧:

  • jpg有损压缩,压缩率高,不支持透明
  • png支持透明,浏览器兼容好
  • webp压缩程度更好,在ios webview有兼容性问题
  • svg矢量图,代码内嵌,相对较小,图片样式相对简单的场景

根据各自不同的特点得到适用的业务场景分别为:

  • jpg:大部分不需要透明图片的业务场景
  • png:大部分需要透明图片的业务场景
  • webp:安卓全部
  • svg:图片样式相对简单的业务场景,如icon

图片压缩几种方法-雪碧图、Image inline

css雪碧图

图片描述

image inline

图片描述

矢量图

  • 使用svg进行矢量图的绘制
  • 使用iconfont解决icon问题

在安卓下使用webp

webp的优势体现在它具有更优的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量;同时具备了无损和有损的压缩模式、Alpha透明以及动画的特性,在JPEG和PNG上的转化效果都非常优秀、稳定和统一。

css 和 js 的装载与执行

先用一张图来理解html页面加载渲染的过程:

图片描述

html渲染过程中有以下特点:

顺序执行、并发加载

(1)词法分析 (2)并发加载 (3)并发上限

是否阻塞

  • css阻塞:(1)css head中阻塞页面的渲染 (2)css阻塞js的执行 (3)css不阻塞外部脚本的加载
  • js阻塞:(1)直接引入的js阻塞页面的渲染 (2)js不阻塞资源的加载 (3)js顺序执行,阻塞后续js逻辑的执行

依赖关系
引入方式

懒加载与预加载

懒加载

  • 图片进入可视区域之后请求资源
  • 对于电商等图片很多,页面很长的业务场景适用
  • 减少无效资源的加载
  • 并发加载的资源过多会阻塞js的加载,影响网站的正常使用

预加载

  • 图片等静态资源在使用之前的提前请求
  • 资源使用到时能从缓存中加载,提升用户体验
  • 页面展示的依赖关系维护

重绘与回流

浏览器存储

缓存优化

查看原文

赞 5 收藏 3 评论 1

前端森林 发布了文章 · 2018-09-26

lodash常用工具函数总结

lodashunderscore是现在非常流行的两个javascript库,提供了一套函数式编程的实用功能。lodash是一套工具库,内部封装了很多字符串、数组、对象等常见数据类型的处理函数。

下面介绍一下常用的函数

_.chunk(array, [size=1])

定义

将数组(array)拆分成多个 size 长度的区块,并将这些区块组成一个新数组。 如果array 无法被分割成全部等长的区块,那么最后剩余的元素将组成一个区块。

参数

array (Array): 需要处理的数组
[size=1] (number): 每个数组区块的长度

返回

(Array): 返回一个包含拆分区块的新数组(相当于一个二维数组)。

示例

_.chunk(['a', 'b', 'c', 'd'], 2);
// => [['a', 'b'], ['c', 'd']]
 
_.chunk(['a', 'b', 'c', 'd'], 3);
// => [['a', 'b', 'c'], ['d']]

_.compact(array)

定义

创建一个新数组,包含原数组中所有的非假值元素。例如false, null, 0, "", undefined, 和 NaN 都是被认为是“假值”。

参数

array (Array): 待处理的数组

返回

(Array): 返回过滤掉假值的新数组

示例

_.compact([0, 1, false, 2, '', 3]);
// => [1, 2, 3]

_.drop(array, [n=1])

定义

创建一个切片数组,去除array前面的n个元素。(n默认值为1。)

参数

array (Array): 要查询的数组。
[n=1] (number): 要去除的元素个数。

返回

(Array): 返回array剩余切片。

示例

_.drop([1, 2, 3]);
// => [2, 3]
 
_.drop([1, 2, 3], 2);
// => [3]
 
_.drop([1, 2, 3], 5);
// => []
 
_.drop([1, 2, 3], 0);
// => [1, 2, 3]

_.indexOf(array, value, [fromIndex=0])

定义

返回首次 value 在数组array中被找到的 索引值, 如果 fromIndex 为负值,将从数组array尾端索引进行匹配。

参数

array (Array): 需要查找的数组。
value (*): 需要查找的值。
[fromIndex=0] (number): 开始查询的位置。

返回

(number): 返回 值value在数组中的索引位置, 没有找到为返回-1。

示例

_.indexOf([1, 2, 1, 2], 2);
// => 1
 
// Search from the `fromIndex`.
_.indexOf([1, 2, 1, 2], 2, 2);
// => 3

_.uniq(array)

定义

创建一个去重后的array数组副本。只有第一次出现的元素才会被保留。

参数

array (Array): 要检查的数组。

返回

(Array): 返回新的去重后的数组。

示例

_.uniq([2, 1, 2]);
// => [2, 1]

_.filter(collection, [predicate=_.identity])

定义

遍历 collection(集合)元素,返回 predicate(断言函数)返回真值 的所有元素的数组。 predicate(断言函数)调用三个参数:(value, index|key, collection)。

参数

collection (Array|Object): 一个用来迭代的集合。
[predicate=_.identity] (Array|Function|Object|string): 每次迭代调用的函数。

返回

(Array): 返回一个新的过滤后的数组。

示例

var users = [
  { 'user': 'barney', 'age': 36, 'active': true },
  { 'user': 'fred',   'age': 40, 'active': false }
];
 
_.filter(users, function(o) { return !o.active; });
// => objects for ['fred']
 
// The `_.matches` iteratee shorthand.
_.filter(users, { 'age': 36, 'active': true });
// => objects for ['barney']
 
// The `_.matchesProperty` iteratee shorthand.
_.filter(users, ['active', false]);
// => objects for ['fred']
 
// The `_.property` iteratee shorthand.
_.filter(users, 'active');

_.reject(collection, [predicate=_.identity])

定义

_.filter的反向方法;此方法 返回 predicate(断言函数) 不 返回 true(真值)的collection(集合)元素(非真)。

参数

collection (Array|Object): 用来迭代的集合。
[predicate=_.identity] (Array|Function|Object|string): 每次迭代调用的函数。

返回

(Array): 返回过滤后的新数组

示例

var users = [
  { 'user': 'barney', 'age': 36, 'active': false },
  { 'user': 'fred',   'age': 40, 'active': true }
];
 
_.reject(users, function(o) { return !o.active; });
// => objects for ['fred']
 
// `_.matches` 迭代简写
_.reject(users, { 'age': 40, 'active': true });
// => objects for ['barney']
 
// `_.matchesProperty` 迭代简写
_.reject(users, ['active', false]);
// => objects for ['fred']
 
// `_.property` 迭代简写
_.reject(users, 'active');
// => objects for ['barney']
查看原文

赞 1 收藏 1 评论 0

前端森林 收藏了文章 · 2018-09-26

async/await封装fetch

基本操作

一个基本的fetch操作很简单。就是通过fetch请求,返回一个promise对象,然后在promise对象的then方法里面用fetch的response.json()等方法进行解析数据,由于这个解析返回的也是一个promise对象,所以需要两个then才能得到我们需要的json数据。

  fetch('http://example.com/movies.json')
  .then(function(response) {
    return response.json();
  })
  .then(function(myJson) {
    console.log(myJson);
  });

为何不能直接使用基本操作

fetch规范与jQuery.ajax()主要有两种方式的不同:
1、当接收到一个代表错误的 HTTP 状态码时,比如400, 500,fetch不会把promise标记为reject, 而是标记为resolve,仅当网络故障时或请求被阻止时,才会标记为 reject。
2、默认情况下,fetch 不会从服务端发送或接收任何 cookies, 如果站点依赖于用户 session,则会导致未经认证的请求(要发送 cookies,必须设置 credentials 选项)。

从这里可以看出来,如果我们要在fetch请求出错的时候及时地捕获错误,是需要对response的状态码进行解析的。又由于fetch返回的数据不一定是json格式,我们可以从header里面Content-Type获取返回的数据类型,进而使用正确的解析方法。

使用async/awiait的原因

Promise 将异步操作规范化.使用then连接, 使用catch捕获错误, 堪称完美, 美中不足的是, then和catch中传递的依然是回调函数, 与心目中的同步代码不是一个套路.

为此, ES7 提供了更标准的解决方案 — async/await. async/await 几乎没有引入新的语法, 表面上看起来, 它就和alert一样易用。

var word = '123',
    url = 'https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su?wd='+word+'&json=1&p=3';
(async ()=>{
  try {
    let res = await fetch(url, {mode: 'no-cors'});//等待fetch被resolve()后才能继续执行
    console.log(res);//fetch正常返回后才执行
    return res;//这样就能返回res不用担心异步的问题啦啦啦
  } catch(e) {
    console.log(e);
  }
})();

代码

解析结果值

检查返回值的状态: 上面提到了,因为fetch不会自己reject,所以我们只能够通过抛出错误帮一下它啦。301和302是重定向的状态码,这个时候页面需要跳转一下,通过window.location实现是不是很perfer呢。

    checkStatus(response) {//检查响应状态
        if(response.status >= 200 && response.status < 300) {//响应成功
            return response;
        }
        if(response.status === 301 || response.status === 302) {//重定向
            window.location = response.headers.get('Location');
        }
        const error = new Error(response.statusText);
        error.data = response;
        throw error;
    }

判断用哪个fetch的解析函数:这里通过headers的Content-Type判断使用哪个解析方法,因为解析也是异步的,所以还是用async/await让程序停在那里慢慢解析。

    async parseResult(response) {//解析返回的结果
        const contentType = response.headers.get('Content-Type');
        if(contentType != null) {
            if(contentType.indexOf('text') > -1) {
                return await response.text()
            }
            if(contentType.indexOf('form') > -1) {
                return await response.formData();
            }
            if(contentType.indexOf('video') > -1) {
                return await response.blob();
            }
            if(contentType.indexOf('json') > -1) {
                return await response.json();
            }
        }
        return await response.text();
    }

为了调用比较好看吧,写多一个processResult去调用者两个方法,然后在fetch的then里面就只需要用这个去得到结果啦。

    async processResult(response) {
        let _response = this.checkStatus(response)
        _response = await this.parseResult(_response);
        return _response;
    }
fetch请求后台代码

把请求后台的代码都写在_request里面,然后get和post里面就封装一下参数。

    async _request(url, init, headers = {}) {
        try {
            let options = _.assign(
                    {
                        credentials: 'include',//允许跨域
                    },
                    init
            );
            options.headers = Object.assign({}, options.headers || {}, headers || {});
            let response = await fetch(url, options);
            response = await this.processResult(response);//这里是对结果进行处理。包括判断响应状态和根据response的类型解析结果
            return response;
        } catch(error) {
            throw error;
            return null;
        }
    }

    async get(api, data = {}, headers = {}, config = {}) {
        const query = _.isEmpty(data) ? '' : `json=${encodeURIComponent(JSON.stringify(data))}`;
        return await this._request(`${api}?${query}`, headers, {}, config);
    }

    async post(api, data = {}, headers = {}, config = {}) {//通过headers决定要解析的类型
        const _headers = {
            'Content-Type': 'application/x-www-form-urlencoded',
            ...headers,
        };
        let formBody = null;
        if(_headers['Content-Type'] && _headers['Content-Type'].indexOf('application/x-www-form-urlencoded')>-1) {
            formBody = new URLSearchParams();
            for(let k in data) {//遍历一个对象
                if(typeof(data[k]) === 'object') {
                    formBody.append(k, JSON.stringify(data[k]));
                } else {
                    formBody.append(k, data[k]);
                }
            }
        }
        return await this._request(
                api,
                {
                    method: 'POST',
                    headers: _headers,
                    body: formBody,
                },
                {},
                config,
        )
    }

how to use

把上面这些代码到写在一个http类里面

    import 'isomorphic-fetch'
    import 'es6-promise'
    import _ from 'lodash';
    class http {
         checkStatus(response) {}
         async parseResult(response) {}
         async processResult(response) {}
         async _request(url, init, headers = {}) {}
         async get(api, data = {}, headers = {}, config = {}) {}
         async post(api, data = {}, headers = {}, config = {}) {}
     }  
       
    let http = new Http();
    export default http;
    

然后调用的时候

    import http from '../../common/http'
    getData() {
        //form类型
        http.post('/api/submitComment', {a: 'hhhh'}).then((data) => {
            console.log(data);//输出返回的数据
        })
        //json类型
        http.post('/api/submitComment', {a: 'hhhh'}, {'content-type': 'application/json'
        }).then((data) => {
            console.log(data);//输出返回的数据
        })
    }
查看原文

前端森林 收藏了文章 · 2018-09-26

fetch,终于认识你

fetch和XMLHttpRequest

如果看网上的fetch教程,会首先对比XMLHttpRequest和fetch的优劣,然后引出一堆看了很快会忘记的内容(本人记性不好)。因此,我写一篇关于fetch的文章,为了自己看着方便,毕竟工作中用到的也就是一些很基础的点而已。

fetch,说白了,就是XMLHttpRequest的一种替代方案。如果有人问你,除了Ajax获取后台数据之外,还有没有其他的替代方案?

这是你就可以回答,除了XMLHttpRequest对象来获取后台的数据之外,还可以使用一种更优的解决方案fetch

如何获取fetch

到现在为止,fetch的支持性还不是很好,但是在谷歌浏览器中已经支持了fetch。fetch挂在在BOM中,可以直接在谷歌浏览器中使用。

查看fetch的支持情况:fetch的支持情况

当然,如果不支持fetch也没有问题,可以使用第三方的ployfill来实现只会fetch:whatwg-fetch

fetch的helloworld

下面我们来写第一个fetch获取后端数据的例子:

// 通过fetch获取百度的错误提示页面
fetch('https://www.baidu.com/search/error.html') // 返回一个Promise对象
  .then((res)=>{
    return res.text() // res.text()是一个Promise对象
  })
  .then((res)=>{
    console.log(res) // res是最终的结果
  })

是不是很简单?可能难的地方就是Promise的写法,这个可以看阮一峰老师的ES6教程来学习。

说明一点,下面演示的GET请求或POST请求,都是采用百度中查询到的一些接口,可能传递的有些参数这个接口并不会解析,但不会影响这个接口的使用。

GET请求

GET请求初步

完成了helloworld,这个时候就要来认识一下GET请求如何处理了。

上面的helloworld中这是使用了第一个参数,其实fetch还可以提供第二个参数,就是用来传递一些初始化的信息。

这里如果要特别指明是GET请求,就要写成下面的形式:

// 通过fetch获取百度的错误提示页面
fetch('https://www.baidu.com/search/error.html', {
    method: 'GET'
  })
  .then((res)=>{
    return res.text()
  })
  .then((res)=>{
    console.log(res)
  })

GET请求的参数传递

GET请求中如果需要传递参数怎么办?这个时候,只能把参数写在URL上来进行传递了。

// 通过fetch获取百度的错误提示页面
fetch('https://www.baidu.com/search/error.html?a=1&b=2', { // 在URL中写上传递的参数
    method: 'GET'
  })
  .then((res)=>{
    return res.text()
  })
  .then((res)=>{
    console.log(res)
  })

POST请求

POST请求初步

与GET请求类似,POST请求的指定也是在fetch的第二个参数中:

// 通过fetch获取百度的错误提示页面
fetch('https://www.baidu.com/search/error.html', {
    method: 'POST' // 指定是POST请求
  })
  .then((res)=>{
    return res.text()
  })
  .then((res)=>{
    console.log(res)
  })

POST请求参数的传递

众所周知,POST请求的参数,一定不能放在URL中,这样做的目的是防止信息泄露。

// 通过fetch获取百度的错误提示页面
fetch('https://www.baidu.com/search/error.html', {
    method: 'POST',
    body: new URLSearchParams([["foo", 1],["bar", 2]]).toString() // 这里是请求对象
  })
  .then((res)=>{
    return res.text()
  })
  .then((res)=>{
    console.log(res)
  })

其实除了对象URLSearchParams外,还有几个其他的对象,可以参照:常用的几个对象来学习使用。

设置请求的头信息

在POST提交的过程中,一般是表单提交,可是,经过查询,发现默认的提交方式是:Content-Type:text/plain;charset=UTF-8,这个显然是不合理的。下面咱们学习一下,指定头信息:

// 通过fetch获取百度的错误提示页面
fetch('https://www.baidu.com/search/error.html', {
    method: 'POST',
    headers: new Headers({
      'Content-Type': 'application/x-www-form-urlencoded' // 指定提交方式为表单提交
    }),
    body: new URLSearchParams([["foo", 1],["bar", 2]]).toString()
  })
  .then((res)=>{
    return res.text()
  })
  .then((res)=>{
    console.log(res)
  })

这个时候,在谷歌浏览器的Network中查询,会发现,请求方式已经变成了content-type:application/x-www-form-urlencoded

通过接口得到JSON数据

上面所有的例子中都是返回一个文本,那么除了文本,有没有其他的数据类型呢?肯定是有的,具体查询地址:Body的类型

由于最常用的是JSON数据,那么下面就简单演示一下获取JSON数据的方式:

// 通过fetch获取百度的错误提示页面
fetch('https://www.baidu.com/rec?platform=wise&ms=1&rset=rcmd&word=123&qid=11327900426705455986&rq=123&from=844b&baiduid=A1D0B88941B30028C375C79CE5AC2E5E%3AFG%3D1&tn=&clientWidth=375&t=1506826017369&r=8255', { // 在URL中写上传递的参数
    method: 'GET',
    headers: new Headers({
      'Accept': 'application/json' // 通过头指定,获取的数据类型是JSON
    })
  })
  .then((res)=>{
    return res.json() // 返回一个Promise,可以解析成JSON
  })
  .then((res)=>{
    console.log(res) // 获取JSON数据
  })

强制带Cookie

默认情况下, fetch 不会从服务端发送或接收任何 cookies, 如果站点依赖于维护一个用户会话,则导致未经认证的请求(要发送 cookies,必须发送凭据头).

// 通过fetch获取百度的错误提示页面
fetch('https://www.baidu.com/search/error.html', {
    method: 'GET',
    credentials: 'include' // 强制加入凭据头
  })
  .then((res)=>{
    return res.text()
  })
  .then((res)=>{
    console.log(res)
  })

简单封装一下fetch

最后了,介绍了一大堆内容,有没有发现,在GET和POST传递参数的方式不同呢?下面咱们就来封装一个简单的fetch,来实现GET请求和POST请求参数的统一。

/**
 * 将对象转成 a=1&b=2的形式
 * @param obj 对象
 */
function obj2String(obj, arr = [], idx = 0) {
  for (let item in obj) {
    arr[idx++] = [item, obj[item]]
  }
  return new URLSearchParams(arr).toString()
}

/**
 * 真正的请求
 * @param url 请求地址
 * @param options 请求参数
 * @param method 请求方式
 */
function commonFetcdh(url, options, method = 'GET') {
  const searchStr = obj2String(options)
  let initObj = {}
  if (method === 'GET') { // 如果是GET请求,拼接url
    url += '?' + searchStr
    initObj = {
      method: method,
      credentials: 'include'
    }
  } else {
    initObj = {
      method: method,
      credentials: 'include',
      headers: new Headers({
        'Accept': 'application/json',
        'Content-Type': 'application/x-www-form-urlencoded'
      }),
      body: searchStr
    }
  }
  fetch(url, initObj).then((res) => {
    return res.json()
  }).then((res) => {
    return res
  })
}

/**
 * GET请求
 * @param url 请求地址
 * @param options 请求参数
 */
function GET(url, options) {
  return commonFetcdh(url, options, 'GET')
}

/**
 * POST请求
 * @param url 请求地址
 * @param options 请求参数
 */
function POST(url, options) {
  return commonFetcdh(url, options, 'POST')
}
GET('https://www.baidu.com/search/error.html', {a:1,b:2})
POST('https://www.baidu.com/search/error.html', {a:1,b:2})
查看原文

前端森林 发布了文章 · 2018-09-25

async await 封装 axios

es6 的promise 逐步解决了层层回调的问题,es8的async await让异步变成了同步的写法。请求的封装可以让对接口请求的处理变得简单。

common/api/index.js

import axios from 'axios'
const qs = require('qs')
const api = {
  async get (url, data) {
    try {
      let res = await axios.get(url, {params: data})
      res = res.data
      return new Promise((resolve) => {
        if (res.code === 0) {
          resolve(res)
        } else {
          resolve(res)
        }
      })
    } catch (err) {
      alert('服务器出错')
      console.log(err)
    }
  },
  async post (url, data) {
    try {
      let res = await axios.post(url, qs.stringify(data))
      res = res.data
      return new Promise((resolve, reject) => {
        if (res.code === 0) {
          resolve(res)
        } else {
          reject(res)
        }
      })
    } catch (err) {
      // return (e.message)
      alert('服务器出错')
      console.log(err)
    }
  },
}
export { api }

上述代码采用try catch语法处理请求,捕获异常。使用示例如下:

import { api } from 'common/api'

  export default {
    data () {
      return {
        userInfo: [],
      }
    },
    created () {
      this.getUserInfo()
    },
    methods: {
      async getUserInfo () {
        let {res} = await api.get('/api/userInfo')
        console.log(res)
        this.userInfo = res
      }
    },
  }
查看原文

赞 0 收藏 0 评论 0

前端森林 赞了文章 · 2018-09-25

JavaScript常用数组操作方法,包含ES6方法

一、concat()

concat() 方法用于连接两个或多个数组。该方法不会改变现有的数组,仅会返回被连接数组的一个副本。

var arr1 = [1,2,3];
var arr2 = [4,5];
var arr3 = arr1.concat(arr2);
console.log(arr1); //[1, 2, 3]
console.log(arr3); //[1, 2, 3, 4, 5]

二、join()

join() 方法用于把数组中的所有元素放入一个字符串。元素是通过指定的分隔符进行分隔的,默认使用','号分割,不改变原数组。

var arr = [2,3,4];
console.log(arr.join());  //2,3,4
console.log(arr);  //[2, 3, 4]

三、push()

push() 方法可向数组的末尾添加一个或多个元素,并返回新的长度。末尾添加,返回的是长度,会改变原数组。

var a = [2,3,4];
var b = a.push(5);
console.log(a);  //[2,3,4,5]
console.log(b);  //4
push方法可以一次添加多个元素push(data1,data2....)

四、pop()

pop() 方法用于删除并返回数组的最后一个元素。返回最后一个元素,会改变原数组。

var arr = [2,3,4];
console.log(arr.pop()); //4
console.log(arr);  //[2,3]

五、shift()

shift() 方法用于把数组的第一个元素从其中删除,并返回第一个元素的值。返回第一个元素,改变原数组。

var arr = [2,3,4];
console.log(arr.shift()); //2
console.log(arr);  //[3,4]

六、unshift()

unshift() 方法可向数组的开头添加一个或更多元素,并返回新的长度。返回新长度,改变原数组。

var arr = [2,3,4,5];
console.log(arr.unshift(3,6)); //6
console.log(arr); //[3, 6, 2, 3, 4, 5]
tip:该方法可以不传参数,不传参数就是不增加元素。

七、slice()

返回一个新的数组,包含从 start 到 end (不包括该元素)的 arrayObject 中的元素。返回选定的元素,该方法不会修改原数组。

var arr = [2,3,4,5];
console.log(arr.slice(1,3));  //[3,4]
console.log(arr);  //[2,3,4,5]

八、splice()

splice() 方法可删除从 index 处开始的零个或多个元素,并且用参数列表中声明的一个或多个值来替换那些被删除的元素。如果从 arrayObject 中删除了元素,则返回的是含有被删除的元素的数组。splice() 方法会直接对数组进行修改。

var a = [5,6,7,8];
console.log(a.splice(1,0,9)); //[]
console.log(a);  // [5, 9, 6, 7, 8]
var b = [5,6,7,8];
console.log(b.splice(1,2,3));  //[6, 7]
console.log(b); //[5, 3, 8]

九、写错了(已删除)

十、sort 排序

按照 Unicode code 位置排序,默认升序

var fruit = ['cherries', 'apples', 'bananas'];
fruit.sort(); // ['apples', 'bananas', 'cherries']

var scores = [1, 10, 21, 2];
scores.sort(); // [1, 10, 2, 21]

十一、reverse()

reverse() 方法用于颠倒数组中元素的顺序。返回的是颠倒后的数组,会改变原数组。

var arr = [2,3,4];
console.log(arr.reverse()); //[4, 3, 2]
console.log(arr);  //[4, 3, 2]

十二、indexOf 和 lastIndexOf

都接受两个参数:查找的值、查找起始位置
不存在,返回 -1 ;存在,返回位置。indexOf 是从前往后查找, lastIndexOf 是从后往前查找。
indexOf

var a = [2, 9, 9];
a.indexOf(2); // 0
a.indexOf(7); // -1

if (a.indexOf(7) === -1) {
  // element doesn't exist in array
}

lastIndexOf

var numbers = [2, 5, 9, 2];
numbers.lastIndexOf(2);     // 3
numbers.lastIndexOf(7);     // -1
numbers.lastIndexOf(2, 3);  // 3
numbers.lastIndexOf(2, 2);  // 0
numbers.lastIndexOf(2, -2); // 0
numbers.lastIndexOf(2, -1); // 3

十三、every

对数组的每一项都运行给定的函数,每一项都返回 ture,则返回 true

function isBigEnough(element, index, array) {
  return element < 10;
}    
[2, 5, 8, 3, 4].every(isBigEnough);   // true

十四、some

对数组的每一项都运行给定的函数,任意一项都返回 ture,则返回 true

function compare(element, index, array) {
  return element > 10;
}    
[2, 5, 8, 1, 4].some(compare);  // false
[12, 5, 8, 1, 4].some(compare); // true

十五、filter

对数组的每一项都运行给定的函数,返回 结果为 ture 的项组成的数组

var words = ["spray", "limit", "elite", "exuberant", "destruction", "present", "happy"];

var longWords = words.filter(function(word){
  return word.length > 6;
});
// Filtered array longWords is ["exuberant", "destruction", "present"]

十六、map

对数组的每一项都运行给定的函数,返回每次函数调用的结果组成一个新数组

var numbers = [1, 5, 10, 15];
var doubles = numbers.map(function(x) {
   return x * 2;
});
// doubles is now [2, 10, 20, 30]
// numbers is still [1, 5, 10, 15]

十七、forEach 数组遍历

const items = ['item1', 'item2', 'item3'];
const copy = [];    
items.forEach(function(item){
  copy.push(item)
});

ES6新增新操作数组的方法

1、find():

传入一个回调函数,找到数组中符合当前搜索规则的第一个元素,返回它,并且终止搜索。

const arr = [1, "2", 3, 3, "2"]
console.log(arr.find(n => typeof n === "number")) // 1

2、findIndex():

传入一个回调函数,找到数组中符合当前搜索规则的第一个元素,返回它的下标,终止搜索。

const arr = [1, "2", 3, 3, "2"]
console.log(arr.findIndex(n => typeof n === "number")) // 0

3、fill():

用新元素替换掉数组内的元素,可以指定替换下标范围。

arr.fill(value, start, end)

4、copyWithin():

选择数组的某个下标,从该位置开始复制数组元素,默认从0开始复制。也可以指定要复制的元素范围。

arr.copyWithin(target, start, end)
const arr = [1, 2, 3, 4, 5]
console.log(arr.copyWithin(3))
 // [1,2,3,1,2] 从下标为3的元素开始,复制数组,所以4, 5被替换成1, 2
const arr1 = [1, 2, 3, 4, 5]
console.log(arr1.copyWithin(3, 1)) 
// [1,2,3,2,3] 从下标为3的元素开始,复制数组,指定复制的第一个元素下标为1,所以4, 5被替换成2, 3
const arr2 = [1, 2, 3, 4, 5]
console.log(arr2.copyWithin(3, 1, 2)) 
// [1,2,3,2,5] 从下标为3的元素开始,复制数组,指定复制的第一个元素下标为1,结束位置为2,所以4被替换成2

5、from

将类似数组的对象(array-like object)和可遍历(iterable)的对象转为真正的数组

const bar = ["a", "b", "c"];
Array.from(bar);
// ["a", "b", "c"]

Array.from('foo');
// ["f", "o", "o"]

6、of

用于将一组值,转换为数组。这个方法的主要目的,是弥补数组构造函数 Array() 的不足。因为参数个数的不同,会导致 Array() 的行为有差异。

Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]
Array.of(7);       // [7]
Array.of(1, 2, 3); // [1, 2, 3]

Array(7);          // [ , , , , , , ]
Array(1, 2, 3);    // [1, 2, 3]

7、entries() 返回迭代器:返回键值对

//数组
const arr = ['a', 'b', 'c'];
for(let v of arr.entries()) {
  console.log(v)
}
// [0, 'a'] [1, 'b'] [2, 'c']

//Set
const arr = new Set(['a', 'b', 'c']);
for(let v of arr.entries()) {
  console.log(v)
}
// ['a', 'a'] ['b', 'b'] ['c', 'c']

//Map
const arr = new Map();
arr.set('a', 'a');
arr.set('b', 'b');
for(let v of arr.entries()) {
  console.log(v)
}
// ['a', 'a'] ['b', 'b']

8、values() 返回迭代器:返回键值对的value

//数组
const arr = ['a', 'b', 'c'];
for(let v of arr.values()) {
  console.log(v)
}
//'a' 'b' 'c'

//Set
const arr = new Set(['a', 'b', 'c']);
for(let v of arr.values()) {
  console.log(v)
}
// 'a' 'b' 'c'

//Map
const arr = new Map();
arr.set('a', 'a');
arr.set('b', 'b');
for(let v of arr.values()) {
  console.log(v)
}
// 'a' 'b'

9、keys() 返回迭代器:返回键值对的key

//数组
const arr = ['a', 'b', 'c'];
for(let v of arr.keys()) {
  console.log(v)
}
// 0 1 2

//Set
const arr = new Set(['a', 'b', 'c']);
for(let v of arr.keys()) {
  console.log(v)
}
// 'a' 'b' 'c'

//Map
const arr = new Map();
arr.set('a', 'a');
arr.set('b', 'b');
for(let v of arr.keys()) {
  console.log(v)
}
// 'a' 'b'

10、includes

判断数组中是否存在该元素,参数:查找的值、起始位置,可以替换 ES5 时代的 indexOf 判断方式。indexOf 判断元素是否为 NaN,会判断错误。

var a = [1, 2, 3];
a.includes(2); // true
a.includes(4); // false

JavaScript字符串操作方法大全,包含ES6方法

查看原文

赞 132 收藏 113 评论 9