Giter Site home page Giter Site logo

blog's Issues

0719

话题

回顾:上面2篇提到了用原生koa2实现路由,还提到了使用koa2的中间件---Koa-router实现路由。
这里要强调的是Koa2中的ctx只要打印就会发现:存在一个request字段和req字段,前者是经过context封装的请求对象,后者是经过context提供的Node.js原生HTTP请求对象;同理,存在一个response字段和res字段,前者是经过context封装的响应对象,后者是经过context提供的Node.js原生HTTP响应对象

请求数据获取

GET请求数据获取

  • 获取GET请求数据是从request对象拿的,而由于Koa把request跟response都封装到ctx里面了,所以我们直接从ctx里拿就可以了;
  • request对象中的query方法或querystring方法,query返回是格式化好的参数对象,querystring返回的是请求字符串。
// 因为ctx默认对request的API有直接引用的方式
ctx.query => 返回如 { a:1, b:2 }
ctx.queryString => 返回如 a=1&b=2
// 从上下文的request对象中获取
ctx.request.query => 返回如 { a:1, b:2 }
ctx.request.queryString => 返回如 a=1&b=2

POST请求数据获取

  • 遗憾的是,Koa并没有把POST请求数据封装到request对象拿到的,需要从ctx的req字段 调取 原生Node.js中的请求对象req来获取POST请求数据;
  • 处理顺序:先将POST表单数据解析成queryString(例如:a=1&b=2&c=3),再将query string 解析成JSON格式(例如:{"a":"1", "b":"2", "c":"3"})。
var koa = require('koa')
var app = new koa()

app.use( async ( ctx ) => {
  if ( ctx.url === '/' && ctx.method === 'GET' ) {
    // 当GET请求时候返回表单页面
    let html = `
      <h1>koa2 request post demo</h1>
      <form method="POST" action="/">
        <p>userName</p>
        <input name="userName" /><br/>
        <p>nickName</p>
        <input name="nickName" /><br/>
        <p>email</p>
        <input name="email" /><br/>
        <button type="submit">submit</button>
      </form>
    `
    ctx.body = html
  } else if ( ctx.url === '/' && ctx.method === 'POST' ) {
    // 1. 当POST请求的时候,解析POST表单里的数据,并显示出来【重点】
    // 依赖下面原生node方法监听
    let postData = await parsePostData( ctx )
    // 等到解析完POST请求的数据再写在页面上
    console.log('写在页面的数据', postData)
    ctx.body = postData
  } else {
    // 其他请求显示404
    ctx.body = '<h1>404!!! o(╯□╰)o</h1>'
  }
})

// 2. 由于是POST请求,所以需要用到node原生req的addListener方法来监听POST参数
// 依赖下面解析函数
function parsePostData( ctx ) {
  return new Promise((resolve, reject) => {
    try {
      let postdata = "";
      // 传递的是流
      ctx.req.addListener('data', (data) => {
        console.log('我是post请求的数据', data)
        postdata += data
      })
      ctx.req.addListener("end",function(){
        let parseData = parseQueryStr( postdata )
        resolve( parseData )
      })
    } catch ( err ) {
      reject(err)
    }
  })
}

// 3. 由于监听到的数据是流的形式,所以必须要把流(本身这里就是一个字符串)转换成json
function parseQueryStr( queryStr ) {
  let queryData = {}
  console.log('解析前', queryStr)
  let queryStrList = queryStr.split('&')
  console.log( queryStrList )
  for (  let [ index, queryStr ] of queryStrList.entries()  ) {
    let itemList = queryStr.split('=')
    queryData[ itemList[0] ] = decodeURIComponent(itemList[1])
  }
  return queryData
}

app.listen(3000)
console.log('[demo] request post is starting at port 3000')

koa-bodyparser中间件

由于上述遇到POST请求的时候需要处理参数是一件十分麻烦的事,所以就有了整合上述功能的中间件koa-bodyparser。

// koa2对应的版本
npm install --save koa-bodyparser@3

放弃上面使用原生node的req.addListener方法,改用中间件

// 使用ctx.body解析中间件
const Koa = require('koa')
const app = new Koa()
const bodyParser = require('koa-bodyparser')

// 使用ctx.body解析中间件
app.use(bodyParser())

app.use( async ( ctx ) => {

  if ( ctx.url === '/' && ctx.method === 'GET' ) {
    // 当GET请求时候返回表单页面
    let html = `
      <h1>koa2 request post demo</h1>
      <form method="POST" action="/">
        <p>userName</p>
        <input name="userName" /><br/>
        <p>nickName</p>
        <input name="nickName" /><br/>
        <p>email</p>
        <input name="email" /><br/>
        <button type="submit">submit</button>
      </form>
    `
    ctx.body = html
  } else if ( ctx.url === '/' && ctx.method === 'POST' ) {
    // 当POST请求的时候,中间件koa-bodyparser解析POST表单里的数据,并默认以json格式显示出来
    let postData = ctx.request.body
     console.log(postData)
    ctx.body = postData
  } else {
    // 其他请求显示404
    ctx.body = '<h1>404!!! o(╯□╰)o</h1>'
  }
})

app.listen(3000)
console.log('[demo] request post is starting at port 3000')

总结

现在用到中间件有:
koa-router => 简便处理ctx.request.url的过程
koa-bodyparser => 简便处理请求类型为POST的参数
现在掌握了的API有:
ctx.url 获取当前url
ctx.request 获取请求对象
ctx.request.query / ctx.query 获取请求参数(格式化)
ctx.request.queryString / ctx.queryString 获取请求参数(字符串)
ctx.req 调用原生node属性/方法
ctx.req.addListener('data', (data) => {})
ctx.req.addListener('end', () => {}) // 上述两者都用于处理POST请求参数
ctx.response 获取响应对象
ctx.res 调用原生node属性/方法

2018.03.08

fs.creadStream 和 fs.readFile有什么区别

  1. createReadStream是给你一个ReadableStream,你可以听它的'data',一点一点儿处理文件,用过的部分会被GC(垃圾回收),所以占内存少;
  2. readFile是把整个文件全部读到内存里,占内存最多
  • 那问题来了,什么时候用合适的方法?
  // 1. readFile => 小文件就算全部内容读入内存,也不会有多大影响;但对于体积较大的二进制文件,比如:音频、视频文件。继续使用此方法就会导致内存”爆仓“,理想的方法应该是读一部分,写一部分,不管文件有多大,只要时间允许,总会处理完成,这里就需要用到流的概念。
  var fs = require('fs')
  fs.readFile('../draw.js', 'utf8', (err, data) => {
    if (err) {
      throw new Error(err)
    } else {
      console.log(data)
    }
  })

  /*
  * 2. 流的使用场景:
  * http responses request
  * fs read write streams
  * zlib streams
  * tcp sockets
  * child process stdout and stderr
  */

  // http request response
  var http = require('http')
  http.createServer((req, res) => {
    var stream = fs.createReadStream('../draw.js')
    stream.on('open', () => {
      console.log('流打开了')
      stream.pipe(res)
    })
    
    stream.on('error', () => {
      console.log('流有问题了')
      res.end(err)
    })
  }).listen(8080) 

  // fs read write streams
  var fs = require('fs')
  varr readStream = fs.createReadStream('../lottery.html')
  var writeStream = fs.createWriteStream('../hello-copy.html')
  readStream.on('data', (chunk) => { // 当有数据流出时,写入数据  
    writeStream.write(chunk)
  })

  readStream.on('end', () => { // 当没有数据时,关闭数据流
    writeStream.end()
  })

  // 上面的写法有一些问题,【如果写入的速度跟不上读取的速度,有可能导致数据丢失】。正常的情况应该是,写完一段,再读取下一段,如果没有写完的话,就让读取流先暂停,等写完再继续。代码修改为:
  var fs = require('fs');
  var path = require('path')

  const fileName = '../hello.js'
  const fileStat = fs.statSync(fileName)
  const totatlSize = fileStat.size
  let passSize = 0

  var readStream = fs.createReadStream(fileName); // 创建可读流可能会有乱码问题,第二个参数带上一个键值为encoding: 'utf-8'的对象即可
  var writeStream = fs.createWriteStream('../hello-copy.js');  
    
  readStream.on('data', function(chunk) { // 当有数据流出时,写入数据  
    passSize += chunk.length
    if (writeStream.write(chunk) === false) { // 如果没有写完,暂停读取流  
          readStream.pause();  // 相比起readFile完全没得暂停,直接全部读入内存,差距:)
      }  
  });  
    
  writeStream.on('drain', function() { // 写完后,继续读取  
      readStream.resume();  // 还可以恢复读取,正 :) 
  });  
    
  readStream.on('end', function() { // 当没有数据时,关闭数据流  
      writeStream.end();  
  });  

  // 复制进度
  setTimeout(() => {
    let lastSize = totatlSize - passSize
    let percent = (passSize / totatlSize) * 100
    console.log(`复制的文件为${path.extname(fileName).split('.')[1]}类型, 总大小为:${totatlSize},过去了:${passSize},剩余${lastSize},完成进度为:${percent}`)
  }, 500)

readStream.pipe(writeStream) // 嫌上面麻烦的,还可以这样玩喔,pipe相当于自动调用data,end等事件

0727

话题

提到NodeJS,肯定要说说模板引擎。以下先举例ejs,相关的还有handlebars。
Koa使用模板必须经过模板引擎,所以要安装依赖~

# 安装koa模板使用中间件
npm install --save koa-views

# 安装ejs模板引擎
npm install --save ejs

一般将模板放在view目录下面

// ./view/index.ejs
<!DOCTYPE html>
<html>
<head>
    <title><%= title %></title>
</head>
<body>
    <h1><%= title %></h1>
    <p>EJS Welcome to <%= title %></p>
</body>
</html>
// index.js
const Koa = require('koa')
const views = require('koa-views')
const path = require('path')
const app = new Koa()

// 加载模板引擎
app.use(views(path.join(__dirname, './view'), {
  extension: 'ejs'
}))

app.use( async ( ctx ) => {
  let title = 'hello koa2'
  // 用于渲染模板引擎,第一个参数是表示模板名字;第二个参数是可选选项,用于传去模板对应位置<%= %>
  await ctx.render('index', {
    title
  })
})

app.listen(3000)

0830

前言

下面先从服务端部分开始学习。

入口文件 - app.js

const Koa = require('koa')
const app = new Koa()

// modules
const path = require('path')
const convert = require('koa-convert') // 用于兼容koa1的第三方模块在koa2环境使用
const static = require('koa-static') // 指定静态资源位置
const views = require('koa-views') // 模板引擎
const bodyParser = require('koa-bodyparser') // 方便获取Post的data
const logger = require('koa-logger') // 日志记录
const session = require('koa-session-minimal') // 会话
const mysqlStore = require('koa-mysql-session')  // 用于连接koa跟mysql的会话模块

// database
const config = require('../config')

// routes
const routers = require('./routers/index')

// MySQL存储session相关配置
const sessionMysqlConfig = {
  user: config.database.USERNAME,
  password: config.database.PASSWORD,
  database: config.database.DATABASE,
  host: config.database.HOST
}

// 配置session中间件
app.use(session({
  key: 'USER_SID',
  store: new mysqlStore(sessionMysqlConfig)
}))

// 配置log中间件
app.use(convert(logger()))

// 配置获取Post Data的中间件
app.use(bodyParser())

// 配置静态资源 
app.use(convert(static(
  path.join(__dirname, '../static')
)))

// 配置服务端渲染引擎所使用的模板信息
app.use(views(path.join(__dirname, './views'), {
  extension: 'ejs'
}))

// 配置路由的中间件
app.use(routers.routes()).use(routers.allowedMethods())

app.listen(config.port, () => {
  console.log(`Listening to ${config.port}`)
})

每天一包

每天一包

Array

9.25

  1. array-first
// 该模块用于只取数组前几项,第一个参数是数组,第二个参数是选择第几个开始
var first = require('array-first')

console.log(first([1, 2, 3, 4]))  // 1

console.log(first([1, 2, 3, 4], 1)) // 1

console.log(first([1, 2, 3, 4], 3)) // [1, 2, 3]
  1. array-last
// 该模块用于只取数组后几项,第一个参数是数组,第二个参数是选择第几个开始
var last = require('array-last')

console.log(last([1, 2, 3]))  // 3

console.log(last([1, 2, 3], 1)) // 3

console.log(last([1, 2, 3], 2)) // [2, 3]
  1. arr-flatten
// // 该模块用于使多层嵌套数组变为单层嵌套,参数为一个带着多层嵌套的数组
// flatten - 使...平坦
// implementation  - 实现
var flatten = require('arr-flatten')

console.log(flatten(['a', ['b', ['c']], 'd', ['e']])) // [ 'a', 'b', 'c', 'd', 'e' ]
  1. array-each
/* 该模块用于遍历数组每一项,并且每一项都调用传入的函数
 * 1. 等同于lodash的_.each / _.forEach
 * 2. 没返回
 */
var each = require('array-each')

var temp = []
each([1, 2, 3, 4, 5], (item, index)=>{
  temp.push(`I am element ${item}`)
})
console.log(temp) // [ 'I am element 1','I am element 2','I am element 3','I am element 4','I am element 5' ]
  1. array-map
/* 原生JS的map方法相对较慢,而且相关数组map库都专注于浏览器的兼容,导致既臃肿又满足不到非浏览器的需求。
 * 1. 所以这个插件的实现会使在node.js运行中更轻更快
 * 2. 并且该插件通过映射返回一个新的数组,区别于each
 */
var map = require('arr-map')
const arr = ['a', 'b', 'c']

var result = map(arr, (item, index)=>{
  item = item + 's'
  return item
})
console.log(result) // [ 'as', 'bs', 'cs' ]
  1. arr-filter
/* 该模块用于过滤数组,筛选条件就是根据返回的值 */
const filter = require('arr-filter')
let result = []

result = filter([1, 2, 'd', 3, {a: 'I am a'}, 'c'], (item)=>{
  return typeof item === 'string'
})

console.log(result) // ['d', 'c']
  1. is-sorted
// 该模块用于判断数组是否按照一定规则排序
const sorted = require('is-sorted')

console.log(sorted([1, 2, 3]))  // true

console.log(sorted([3, 1, 2]))  // false

console.log(sorted([3, 2, 1], (a, b)=>b-a))  // true, 第二个参数为回调函数,返回一个倒序排列
  1. arr-diff
/* 该模块用于对比参数之间的不同,默认是拿第一个跟第二个参数做对比,放在前面的做参照物
 */
const diff = require('arr-diff')

const a = ['a', 'b', 'c', 1]
const b = ['a', 'c', 'h']

console.log(diff(a, b)) // ['b', 1]
console.log(diff(b, a)) // ['h']

9.26

  1. array-intersection
// 该模块用于 将数组间多次引用的元素提取并去重,生成一个新数组
const intersection = require('array-intersection')
console.log(intersection(['a', 'a', 'c']))  // ['a', 'c']
console.log(intersection(['a', 'b', 'c'], ['b', 'c', 'd', 'e']))  // ['b', 'c']
console.log(intersection(['a', 'a', 'c'], ['a', 'b', 'c'], ['b', 'c'])) // ['c']
  1. arr-reduce
/** 这个模块用于遍历数组每个元素,让第一个元素跟第二个元素相加得到的结果和第三个比较,以此类推,得到总值
 *  1. 跟原生数组的reduce用法差不多,只是更快了
 *  2. reduce的参数有三个:第一个是数组,第二个是回调函数,第三个是初始值;如果指定了初始值,则让初始值跟第一个元素相加得到的结果和第二个比较,以此类推
 */
const reduce = require('arr-reduce')

// 普通用法
let temp = reduce([1, 2, 3, 4, 5], (prev, curr)=>{
  return prev + curr
})
console.log(temp) // 15

// 初始值
let temp2 = reduce([1, 2, 3, 4, 5], (prev, curr)=>{
  return prev + curr
}, 6)
console.log(temp2)  // 21

// 遍历数组每一项跟初始数组通过数组合并的方法结合再返回
let temp3 = reduce(['b', 'c'], (prev, curr)=>{
  return prev.concat(curr)
}, ['a'])
console.log(temp3)  // ['a', 'b', 'c']
  1. array-union
// 该模块用于合并数组,等同于原生的concat,还自带去重
const union = require('arr-union')

// 一般用法
const result = union(['a'], ['b', 'c'], ['d', 'e', 'f'])
console.log(result) // [ 'a', 'b', 'c', 'd', 'e', 'f' ]

// 自带去重
const result2 = union(['a'], ['a', 'b', 'c'], ['d', 'b', 'e', 'f'])
console.log(result2) // [ 'a', 'b', 'c', 'd', 'e', 'f' ]
  1. array-unique
// 该模块用于去重
// 1. 直接去重的话,生成新数组的同时会影响原数组
var unique = require("array-unique")
var arr = ['a', 'b', 'c', 'c'];
console.log(unique(arr)) //=> ['a', 'b', 'c'] 
console.log(arr)         //=> ['a', 'b', 'c', 'c'] 

var unique2 = require("array-unique").immutable;
var arr2 = ['a', 'b', 'c', 'c'];
console.log(unique2(arr2)) //=> ['a', 'b', 'c'] 
console.log(arr2)         //=> ['a', 'b', 'c', 'c'] 
  1. arr-pluck
// 该模块用于 将数组里面的每个带有某个属性的元素检索出来,用它们的值生成新的数组
const pluck = require('arr-pluck');
const arr = [{'a': 1, 'b': 2, 'c': 3}, {'a': 0, 'b': 6}];
// 把带有属性名为a的元素检索出来,用它们的值生成新的数组
console.log(pluck(arr, 'a'))  // [1, 0]
  1. array-xor
/* 该模块用于对比数组,如果数组之间存在相同的值则忽略,先来先到,最后返回一个合并后的新数组
 */
const xor = require('array-xor')

const a = [1, 2, 3, 5, 9]
const b = [3, 4]
const c = [3, 5, 9, 4]

console.log(xor(a)) // [1, 2, 3, 5, 9]
console.log(xor(a, b)) // [1, 2, 5, 9, 4]
console.log(xor(a, c)) // [1, 2, 4]
console.log(xor(a, b, c)) // [1, 2, 3]
  1. collection-map => 自由定制数组
// 该模块用于 将对象任意一项转换成数组,第一个参数是对象,第二个参数是回调函数,回调函数的第一个参数是每项对象的值,第二个参数是每项对象的键,第三个参数是整个对象
const col = require('collection-map')

/*
 * foo a { a: 'foo', b: 'bar', c: 'baz' }
 * bar b { a: 'foo', b: 'bar', c: 'baz' }
 * baz c { a: 'foo', b: 'bar', c: 'baz' }
 */
let result = col({a: 'foo', b: 'bar', c: 'baz'}, (val, key, obj)=>{
  return val
})
console.log(result) // [ 'foo', 'bar', 'baz' ]

let result2 = col({a: 'foo', b: 'bar', c: 'baz'}, (val, key, obj)=>{
  return key
})
console.log(result2)  // ['a', 'b', 'c']
  1. dedupe
// 该模块用于数组去重,生产的新数组并不会影响原数组
const dedupe = require('dedupe')
const a = [1, 2, 2, 3, 6]
const b = dedupe(a)
console.log(a, b) // [1, 2, 2, 3, 6][1, 2, 3, 6]

// 对数组内对象元素进行去重
const c = [{a: 2}, {a: 1}, {a: 1}, {a: 1}]
const d = dedupe(c)
console.log(c, d)

// 还能对数组内带有特定键名的对象元素进行去重
const e = [{a: 2, b: 1}, {a: 1, b: 2}, {a: 1, b: 3}, {a: 1, b: 4}]
const f = dedupe(e, value => value.a)
console.log(e, f)
  1. array-range
// 该模块可以将给定范围的数字生成一个数组;只传一个参数,默认从0到参数之间的范围;传两个参数,从第一个参数到第二个参数之间的范围。
const range = require('array-range')
console.log(range(3)) // [ 0, 1, 2 ]
console.log(range(1, 10)) // [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]

// 多用于函数式编程,例如es6
console.log(range(3).map( x => x*x )) // [ 0, 1, 4 ]
console.log(range(1, 10).filter( x => x%2===0 )) // [ 2, 4, 6, 8 ]

// 什么都不传,默认返回空数组
console.log(range())

// 接受负数
console.log(range(-3, 3))
  1. filled-array
// 该模块用于根据指定参数生成数组;第一个参数为数字/字符串时,第二个参数表示循环次数;第一个参数为函数时,第二个参数表示循环次数
const filledArray = require('filled-array')

// 字符串
console.log(filledArray('ljm', 3))  // [ 'ljm', 'ljm', 'ljm' ]
// 数字
console.log(filledArray(23, 3)) // [23, 23, 23]
// 函数
console.log(filledArray((item) => {
  return (++item % 3 ? item : 'nomatch') || item
}, 10)) // [ 1, 2, 'nomatch', 4, 5, 'nomatch', 7, 8, 'nomatch', 10 ]

9.27

qs

该包专门用于转换url参数。但有更多奇淫技巧。

  1. qs.parse(string, [options]),参数:第一个参数是字符串,例如'a=1&b=2';第二个参数为可选选项
var qs = require('qs');
console.log(qs.parse('a=1&b=2')); // { a: '1', b: '2' }
console.log(qs.stringify({ a: '1', b: '2' })); // 'a=1&b=2'

1. 参数转换为对象

// 可以多层嵌套
console.log(qs.parse('foo[bar]=baz'))  // { foo: { bar: baz } }
console.log(qs.parse('foo[bar][baz]=foobarbaz'))  // { foo: { bar: { baz: 'foobarbaz' } } }
// 但嵌套只转换前3层,大于3层以上是不会进行转换的;但可以通过添加可选选项depth进行嵌套层数的挑选
console.log(qs.parse('a[b][c][d][e][f][g][h][i]=j', {depth: 1}))  // { a: { b: { c: [Object] } } }
console.log(qs.parse('a[b][c][d][e][f][g][h][i]=j'))  // { a: { b: { c: [Object] } } }

// 可以转换uri encoded格式的字符串
console.log(qs.parse('a%5Bb%5D=c')) // { a: { b: 'c' } }

// 限制转换参数
console.log(qs.parse('a=b&c=d', { parameterLimit: 1 })) // { a: 'b' }

// 是否转换带符号的字符串
console.log(qs.parse('?a=b&c=d', { ignoreQueryPrefix: true })) // { 'a': 'b', c: 'd' }
console.log(qs.parse('?a=b&c=d', { ignoreQueryPrefix: false })) // { '?a': 'b', c: 'd' }

// 以某个分隔符作为划分界限
console.log(qs.parse('a=b;c=d', { delimiter: ';' }))  // { a: 'b', c: 'd' }
// 还能用来做正则过滤
console.log(qs.parse('a=b;c=d,e=f', { delimiter: /[;,]/ })) // { a: 'b', c: 'd', e: 'f' }

// 开启小标点模式
console.log(qs.parse('a.b=c', { allowDots: true })) // { a: { b: 'c' } },等同于a[b]=c

2. 参数转换为数组
// 对比于a[b]=c => a{ b: c },既然没有键名,那就会被转换成数组
console.log(qs.parse('a[]=b&a[]=c'))  // { a: [ 'b', 'c' ] }

// 键名只要是数字,也会被转换成数组;并且数字表示转换后数组的排列顺序
console.log(qs.parse('a[1]=c&a[0]=b'))  // { a: [ 'b', 'c' ] }
// 键名是数字,但是由于qs限制数组里的索引最大不能超过20,所以21会被转换成对象的键名 => 注意这种情况只要有一个参数存在,其余参数也会被转换成 键名为索引 的对象
console.log(qs.parse('a[21]=b')) // { a: { 21: 'b' } } 
console.log(qs.parse('a[21]=b&a[10]=c')) // { a: { '10': 'c', '21': 'b' } }

// 正常情况下
console.log(qs.parse('a[1]=b')) // {a: ['b']}
// 限制转换参数
console.log(qs.parse('a[1]=b', { arrayLimit: 0 })) // {a: { 1: 'b'}}

// 参数的键名既是数字,又是变量的话,则统一当成对象处理
console.log(qs.parse('a[0]=b&a[b]=c'))  // { a: {0: b, b: c} }

// 创建对象数组
console.log(qs.parse('a[][b]=c')) // { a: [{b: c}] }

// 值不填,也会被转换成数组
console.log(qs.parse('a[]=&a[]=b')) // { a: [ '', 'b' ] }

// 
console.log(qs.parse('a=b', { decoder: (str)=>{
  console.log(str)  // 依次分别是a 和 b
  return str
} })) // {a: 'b'}

// 2. qs.stringify(object, [options])
// 正常转换单层对象
console.log(qs.stringify( {a: 'b'} )) // a=b

// 转换多层对象,默认是会对括号进行uri加密
console.log(qs.stringify({a: {b: 'c'}}))  // a%5Bb%5D=c
// 转换多层对象,不对括号进行uri加密
console.log(qs.stringify({a: {b: 'c'}}, { encode: false }))  // a[b]=c

// 多层嵌套
console.log(qs.stringify({ a: { b: { c: 'd', e: 'f' } } }, {encode: false}))  // a[b][c]=d&a[b][e]=f

// false表示对嵌套都做uri加密处理
console.log(qs.stringify({ a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] }, { encodeValuesOnly: false })) // a=b&c%5B0%5D=d&c%5B1%5D=e%3Df&f%5B0%5D%5B0%5D=g&f%5B1%5D%5B0%5D=h
// true表示对嵌套不做uri加密处理
console.log(qs.stringify({ a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] }, { encodeValuesOnly: true })) // a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h

// 自定义加密处理规则,encode如果设置为false则自定义规则不生效
let result = qs.stringify({ a: { b: 'c' } }, { encoder: function (str) {
  // Passed in values `a`, `b`, `c`
  console.log(str)  // 依次分别是a[b] 和 'c'
  return str// Return encoded string
}})
console.log(result) // a[b] = c

// 如何将转换成的数组索引去掉
console.log(qs.stringify({ a: ['b', 'c', 'd'] }, { encode: false }))  // a[0]=b&a[1]=c&a[2]=d
console.log(qs.stringify({ a: ['b', 'c', 'd'] }, { indices: false })) // a=b&a=c&a=d

// 通过指定格式转换
console.log(qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices', encode: false })) // a[0]=b&a[1]=c
console.log(qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets', encode: false }))  // a[]=b&a[]=c
console.log(qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat', encode: false }))  // a=b&a=c

// 转换成小数点
console.log(qs.stringify({ a: { b: { c: 'd', e: 'f' } } }, { allowDots: true }))  // a.b.c=d&a.b.e=f

// 空值
console.log(qs.stringify({ a: '' }))  // a=
console.log(qs.stringify( { a: [] } ))  // ''
console.log(qs.stringify( { a: {} } ))  // ''
console.log(qs.stringify( { a: [{}] } ))  // ''
console.log(qs.stringify( { a: { b: []} } ))  // ''
console.log(qs.stringify( { a: { b: {}} } ))  // ''
console.log(qs.stringify({ a: null, b: undefined }))  // a=

// 加入请求标记
console.log(qs.stringify({ a: 'b', c: 'd' }, { addQueryPrefix: true })) // ?a=b&c=d

// 0929
// 转换成以某个操作符做间隔的字符串,替代&
console.log(qs.stringify({ a: 'b', c: 'd' }, { delimiter: ';' })) // a=b;c=d

// 可通过serialzeDate 自定义转换时间规则
console.log(qs.stringify({ a: new Date(7) })) // a=1970-01-01T00%3A00%3A00.007Z
console.log(qs.stringify({ a: new Date(7) }, { serializeDate: function (d) { console.log(d);return d.getTime(); } })) // a=7

// 可通过自定义规则进行排序
// srcStr.localeCompare(tarStr) srcStr < tarStr,返回-1;srcStr > tarStr,返回大于0的数;srcStr = tarStr,返回0
function alphabeticalSort(a, b) {
  console.log(a, b) // 分别代表各自键名,用键名进行比较
  return a.localeCompare(b);
}
console.log(qs.stringify({ a: 'c', z: 'y', b : 'f' }, { sort: alphabeticalSort }))  // a=c&b=f&z=y

// 可通过过滤规则进行筛选转换后的字符串,一般传递的是一个跟在最终生成字符串存在的数组,也可以传递的是一个函数
/* prefix - 键名 value - 值
 * 首先将整个对象看成prefix,然后遍历对象的每个键名作为prefix,对应的键值作为value,最后通过return代表每次遍历的拼接字符串方式
 */
function filterFunc(prefix, value) {
  console.log(`Filter:key: ${prefix}, value: ${value}`)
  if (prefix == 'b') {
      // Return an `undefined` value to omit a property.
      return;
  }
  if (prefix == 'e[f]') {
      return value.getTime();
  }
  if (prefix == 'e[g][0]') {
      return value * 2;
  }
  return value;
}
// 通过encodeValuesOnly不对结果进行uri加密
console.log(qs.stringify({ a: 'b', c: 'd', e: { f: new Date(123), g: [2] } }, { filter: filterFunc, encodeValuesOnly: true }))  // a=b&c=d&e[f]=123&e[g][0]=4
console.log(qs.stringify({ a: 'b', c: 'd', e: 'f' }, { filter: ['a', 'e'] })) // a=b&e=f,只过滤跟数组元素吻合的键值对
console.log(qs.stringify({ a: ['b', 'c', 'd'], e: 'f' }, { filter: ['a', 0, 2], encodeValuesOnly: true }))  // a[0]=b&a[2]=d,只过滤跟数组元素吻合的键值对

// 3. 处理空值
// 转成字符串时:null或者空值 会被当成空字符串
console.log(qs.stringify({a: null, b: ''})) // a=&b=
// 使用strictNullHandling,能把值为null转换后的值 去除=
console.log(qs.stringify({a: null, b: ''}, { strictNullHandling: true })) // a&b=

// 转成对象时:键值不存在或者等于空 会被当成空字符串
console.log(qs.parse('a&b=')) // {a: '', b: ''}
// 使用strictNullHandling,能把值为null转换后的值 去除=
console.log(qs.parse('a&b=', { strictNullHandling: true })) // { a: null, b: '' }

// 忽略值为null的进行转换
console.log(qs.stringify({ a: 'b', c: null}, { skipNulls: true }))  // a=b

// 运用特定模块进行加密转换
console.log(qs.stringify({ a: 'こんにちは!' }, { encoder: encoder }))
// 运用特定模块进行解密转换
console.log(qs.parse('a=%82%B1%82%F1%82%C9%82%BF%82%CD%81I', { decoder: decoder })) // { a: 'こんにちは!' }

// 对空格进行RFC 3986 或者 RFC 1738的加密,默认使用RFC 3986加密
console.log(qs.stringify({ a: 'b c' })) // a=b%20c
console.log(qs.stringify({ a: 'b c' }, { format : 'RFC3986' })) // a=b%20c
console.log(qs.stringify({ a: 'b c' }, { format : 'RFC1738' })) // a=b+c

file-type

/* 该模块用于对 【文件流 / 图片流】 做类型校验
 * 返回一个带着两个键值对的对象 => ext: 支持的文件类型 mime: 支持的媒体类型;如果不匹配会返回null
 * 支持的文件类型如下:(不包含svg,针对判断是否为svg的文件类型可以使用这个模块:https://github.com/sindresorhus/is-svg)
 * jpg
 * png
 * gif
 * webp
 * flif
 * cr2
 * tif
 * bmp
 * jxr
 * psd
 * zip
 * tar
 * rar
 * gz
 * bz2
 * 7z
 * dmg
 * mp4
 * m4v
 * mid
 * mkv
 * webm
 * mov
 * avi
 * wmv
 * mpg
 * mp3
 * m4a
 * ogg
 * opus
 * flac
 * wav
 * amr
 * pdf
 * epub
 * exe
 * swf
 * rtf
 * woff
 * woff2
 * eot
 * ttf
 * otf
 * ico
 * flv
 * ps
 * xz
 * sqlite
 * nes
 * crx
 * xpi
 * cab
 * deb
 * ar
 * rpm
 * Z
 * lz
 * msi
 * mxf
 * mts
 * wasm
 * blend
 * bpg
 * docx
 * pptx
 * xlsx
 */
// Node.js核心模块
const fs = require('fs')

// 第三方模块
const readChunk = require('read-chunk');  // 跟fs.readFileSync / fs.readFile有什么分别
const fileType = require('file-type');
const http = require('http')

// 使用第三方模块同步读取文件(应该是封装了fs.read*方法)
const buffer = readChunk.sync('../unicorn.jpg', 0, 4100);
console.log(fileType(buffer)) // { ext: 'jpg', mime: 'image/jpeg' },ext表示后缀,mime表示媒体类型
// 使用核心模块同步读取文件
const file = fs.readFileSync('../unicorn.jpg')
console.log(fileType(file)) // { ext: 'jpg', mime: 'image/jpeg' },ext表示后缀,mime表示媒体类型

const url = 'http://assets-cdn.github.com/images/spinners/octocat-spinner-32.gif';
http.get(url, res => {
   res.once('data', chunk => {
      console.log(chunk)  // <Buffer 47 49 46 38 39 61 20 00 20 00 a2 07 00 82 82 82 b3 b3 b3 f8 f8 f8 e2 e2 e2 99 99 99 cc cc cc ca ca ca ff ff ff 21 ff 0b 4e 45 54 53 43 41 50 45 32 2e ... >
      console.log(fileType(chunk)) // {ext: 'gif', mime: 'image/gif'}
      res.destroy()
   })
})

read-chunk

// 该模块用于 从文件内读取块 => 为什么不用fs.readFile?因为
const readChunk = require('read-chunk')

/* 直接readChunk.sync得出的结果是一个Buffer类型的数据。
 * 如果是单一Buffer(只读文件,而不是服务端监听并接收客户端请求的参数),则可以通过toString()方法来把Buffer数据转换成字符串输出;
 * 如果是流类型的Buffer(服务端监听并接收客户端请求的参数),则需要通过string_decoder这个模块来把流类型的Buffer数据转换成字符串输出,详情:https://stackoverflow.com/questions/12121775/convert-streamed-buffers-to-utf8-string;
 * 参数:filepath 文件路径 position 开始读的位置 length读取多少个数量的bytes
 */
// 1. readChunk(filepath, position, length) 返回一个Promise形式的Buffer
console.log(readChunk('./test.txt', 1, 3))  // Promise { <pending> }

// 2. readChunk.sync(filepath, position, length) 返回Buffer
console.log(readChunk.sync('./test.txt', 1, 3))  // <Buffer 65 6c 6c>
console.log(readChunk.sync('./test.txt', 1, 3).toString('utf8'))  // ell

余下的有:
学习资源:
https://github.com/node-modules
https://github.com/parro-it/awesome-micro-npm-packages
https://github.com/sindresorhus/awesome-nodejs
跟markdown相关的有:
https://github.com/jonschlinkert/remarkable;
比较出名的有:
ioredis
redis

0717

接下来的一个月要把心思都集中在学习Node上,组里选择了Koa这个Node框架,所以后续的issue都用于记录学习的过程。

话题

节选自https://chenshenhai.github.io/koa2-note/note/start/quick.html

环境

简单带过,无非就是安装Koa2

npm install koa // 默认是Koa2

示例代码:

const Koa = require('koa')
const app = new Koa()  // 区别于Koa1
// ctx集request跟response于一身
app.use( async ( ctx ) => {
  ctx.body = 'hello koa2'
})
app.listen(3000)

快速理解async / await

async:能令普通函数转换成针对异步的函数,只要使用async声明过就能使用await;
await:遇到await则等await后的函数执行完成再接着执行自身代码,解决了异步函数不停回调的问题。

const Koa = require('koa')
const app = new Koa()

//  洋葱圈的执行顺序:从外到里,从里到外
function getSyncTime() {
  return new Promise((resolve, reject) => {
    try {
      let startTime = new Date().getTime()
      setTimeout(() => {
        let endTime = new Date().getTime()
        let data = endTime - startTime
        console.log(3)
        resolve( data )
      }, 500)
    } catch ( err ) {
      reject( err )
    }
  })
}

async function getSyncData() {
  console.log('2: before')
  let time = await getSyncTime()
  console.log('2: after')
  let data = `endTime - startTime = ${time}`
  return data
}

async function getData() {
  console.log('1: before')
  let data = await getSyncData()
  console.log('1: after')
  console.log( data )
}

getData()

app.use( async ( ctx ) => {
  ctx.body = 'hello koa2'
})

app.listen(3000)

输出结果为:1:before,2:before,3,2:after,1:after.从上述案例可以得出:

  • 可以让异步逻辑用同步写法实现
  • 最底层的await必须返回的是Promise对象
  • 可以通过多层 async function 的同步写法代替传统的callback嵌套

聊聊中间件

  • 中间件肯定是一个函数
  • 客户端发起的请求,只要流经服务端,都会走中间件
  • 中间件有许多用途:有记录日志,有返回处理过的数据,还有各种。。。
const Koa = require('koa')
const app = new Koa()

/** async封装过的函数只要用在Koa2,都肯定会有2个参数:ctx 和 next
 *   - ctx封装了request和response
 *   - 就像上面提到的:最底层的await必须返回的是Promise对象,而执行next这个函数返回的就是Promise对象
 */
app.use( async ( ctx, next ) => {
  console.log(ctx)
  //  console.log(next())
  ctx.body = 'hello koa2'
  await next()
})
app.listen(3000)

说说ctx

上面提到的ctx,我想把控制台中打印的贴出来:

{ 
  request: { 
    method: 'GET',
    url: '/',
    // 请求头的所有信息
    header: {
      host: 'localhost:3000',
      connection: 'keep-alive',
      'cache-control': 'max-age=0',
      'upgrade-insecure-requests': '1',
      'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36',
      accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
      'accept-encoding': 'gzip, deflate, sdch, br',
      'accept-language': 'zh-CN,zh;q=0.8',
      cookie: '_ga=GA1.1.936162926.1487390889' 
    }
  },
  // 响应头信息
  response: { status: 404, message: 'Not Found', header: {} },
  app: { subdomainOffset: 2, proxy: false, env: 'development' },
  originalUrl: '/',
  req: '<original node req>',
  res: '<original node res>',
  socket: '<original node socket>' 
}

所谓的ctx其实就是对原生Node的Request和Response封装起来。里面主要的无非就是请求头的所有信息 和 响应头的所有信息,而服务端需要的也就只是这些而已。

怎么写路由

简单点,说话的方式简单点。

  • 上面的例子都是中间件没有做任何过滤(也就是所谓的路由规则),就直接把Hello,Koa2打印在页面上。
  • 实际应用上是怎么做呢?具体看一个简单例子:
const Koa = require('koa')
const app = new Koa()

app.use( async ( ctx, next ) => {
  let url = ctx.request.url  // 把请求的地址通过body写在浏览器上
  ctx.body = `hello koa2! And my request url is: ${url}`
})

app.listen(3000)

上面是一个最简单的默认路由。

定制化你的路由

const Koa = require('koa')
const fs = require('fs')
const app = new Koa()

/**
 * 用Promise封装异步读取文件方法
 * @param  {string} page html文件名称
 * @return {promise}      
 */
function render( page ) {
  return new Promise(( resolve, reject ) => {
    let viewUrl = `./view/${page}`
    fs.readFile(viewUrl, "binary", ( err, data ) => {
      if ( err ) {
        reject( err )
      } else {
        resolve( data )
      }
    })
  })
}

/**
 * 根据URL获取HTML内容
 * @param  {string} url koa2上下文的url,ctx.url
 * @return {string}     获取HTML文件内容
 */
async function route( url ) {
  let view = '404.html'
  switch ( url ) {
    case '/':
      view = 'index.html'
      break
    case '/index':
      view = 'index.html'
      break
    case '/todo':
      view = 'todo.html'
      break
    case '/404':
      view = '404.html'
      break
    default:
      break
  }
  let html = await render( view )
  return html
}

app.use( async ( ctx ) => {
  let url = ctx.request.url
  let html = await route( url )
  ctx.body = html
})

app.listen(3000)
console.log('[demo] route-simple is starting at port 3000')

0707

工作

今天封装组件的时候遇到一个问题:就是明明外层组件已经给封装组件传值了,但是封装的组件好像没有接收到参数。
1. 后来同事提到在封装组件(以下统称组件)中用watch来监听组件的变量是否变化即可,之后发现原本把写在created的逻辑移到watch里写是可行的,但在created中是不可行的。 废话,肯定不行,都不是在监听变量的。。。
2. 其实下面的情况是遇到过的,只是忘记了。。。
2. 由于之前封装组件都没遇到过这个情况,所以这个可以列入特殊场景。顺带一提,其实除了watch还有计算属性可以监听到组件的变量是否变化。回到正题,通过watch得到的结论是:组件的变量没有发生改变不是由于之前推测的外部组件没传值,而可能是v-show跟v-if对生命周期的影响。
3. 默认组件的代码外面是用v-show包住的,v-show有个特点就是它会在DOM看到,处于显示/隐藏状态;而v-if有个特点就是它要么在DOM插入,要么从DOM移除。引申一点就是:v-if的改变会不停触发生命周期;v-show则只会触发一次。
结果:封装组件需要对外部传入的参数进行处理,应该尽可能使用计算属性,其次才是watch;把处理逻辑写在created这类生命周期是错的,因为生命周期无论v-if还是v-show都只会走一次,从一开始我的关注点就是错的。。。生命周期与监听变量,两个概念是分开的,还是写太少的锅。。。
处理方法:

<template>
  <div>
    <div v-show="test">打开蚊帐</div>
  </div>
</template>

<style>

</style>

<script>
// 这个是公共组件
  export default {
    props: ['btn'],
    computed: {
      test () {
        console.log(this.btn)
        return this.btn  // 根据外层参数来动态改变局部组件的变量
      }
    },
    data () {
      return {
        switchBtn: this.btn
      }
    }
  }
</script>

0711

工作

使用flexbox后,里面的元素又用相对定位时,尽管在浏览器的模拟器下查看是并没异常,以下错误样式:

// less 伪代码
   .rank {
     display: flex;
     flex-direction: column;
     align-items: center;
     justify-content: center;  // 开始排查的时候以为是漏了这个,结果真机还是出现子元素全部偏左
     position: relative;
     margin: 30px 0;
     height: 145px;
     background: url('../assets/images/46.png') no-repeat;
     background-size: 100% 100%;
     .rank-content1 {
      // 归根到底是这里用了相对定位,想其居中只在外层用flex是不够的,因为相对定位的居中不受外层flexbox的居中属性影响。所以这里居中是需要加上加上left: 0 和 right: 0。
      // 这里设置子元素上下间距除了用相对定位,还可以用margin
      position: absolute;  
      top: 25%;
      // margin-top: 5px; 上下间距可用px/rem,左右间距可用百分比
      color: #ff7c11;
     }
     .rank-content2 {
      position: absolute;
      top: 48%;
     }
     .rank-content3 {
      position: absolute;
      top: 70%;
     }
   }

归纳到这里 #7

0718

话题

从上一篇例子可以看出,如果一直依靠ctx.request.url去手动处理路由,将会写很多代码:体现在需要先从url地址上获取url参数,然后判断该参数是以什么名字结尾的(通过switch判断),接着通过读取方法读取该url对应的html文件内容,最后再返回到客户端。

所以就有人封装了koa-router这个中间件来简化我们上述的操作。

var koa = require('koa')
var router = require('koa-router')
var fs = require('fs')
var app = new koa()

// 初始化路由 - 主页
let home = new router()
home.get('/', async (ctx) => {
  let html = `
    <ul>
      <li><a href="/page/helloworld">/page/helloworld</a></li>
      <li><a href="/page/404">/page/404</a></li>
    </ul>
  `
  ctx.body = html
})

// 初始化路由 - 子页面
let page = new router()
// **链式写法 -> /404 跟 /helloworld 是同级的,父级都为/page**
page.get('/404', async (ctx) => {
  ctx.body = '404 page!'
}).get('/helloworld', async (ctx) => {
  ctx.body = 'helloworld page!'
})

// 汇总路由
// **凡是上述初始化的路由最后都需要将内容返回到客户端,体现在ctx.body**
let rootRouter = new router()
// **这里的allowedMethods()貌似是用来允许特殊请求的,例如: options这类**
rootRouter.use('/', home.routes(), home.allowedMethods())
rootRouter.use('/page', page.routes(), page.allowedMethods())

// 链式写法 => 意味着每个中间件执行完,最后返回的都是app本身
// app.use(router.routes(), router.allowedMethods()) 另一种写法
app.use(rootRouter.routes()).use(rootRouter.allowedMethods())

app.listen(3000, () => {
  console.log('U r listening port 3000.')
})

1011

工作

最近工作过程中,发现webpack打包出来的静态资源路径publicPath如果写死成http的话,那么网站切换成https就会导致资源无法访问。

那么是否存在一种办法,能让网站在http / https随意切换,也不影响静态资源的加载呢?
相对协议能完美解决这个问题。

  1. 如果你的网站同时准备了 https 资源和 http 资源,那么,可以使用相对协议可以帮助你实现当网站引入的都是 http 资源,网站域名更换为 https 后的无缝切换。
  2. 相对协议是什么?简而言之,就是将URL的协议(http、https)去掉,只保留//及后面的内容。这样,在使用https的网站中,浏览器会通过https请求URL,否则就通过http发送请求。
// html
<img src="//domain.com/img/logo.png">
// 这个小技巧同样适用于 CSS :
.omg { background: url(//websbestgifs.net/kittyonadolphin.gif); }

0825

原生Koa实现JSONP

在项目复杂的业务场景,有时候需要在前端跨域获取数据,这时候提供数据的服务就需要提供跨域请求的接口,通常是使用JSONP的方式提供跨域接口。

var koa = require('koa')
var app = new koa()

app.use(async (ctx) => {
  if (ctx.method === 'GET' && ctx.url.split('?')[0] === '/getData.jsonp') {
    // 获取jsonp的callback参数
    let callbackName = ctx.query.callback || 'callback'
    console.log(callbackName)
    let returnData = {
      success: true,
      data: {
        text: 'this is a jsonp api',
        time: new Date().getTime()
      }
    }

    // jsonp的script字符串
    let jsonpStr= `;${callbackName}(${JSON.stringify(returnData)})`
    
    // 用text/javascript让请求支持跨域获取
    ctx.type = 'text/javascript'

    ctx.body = jsonpStr
  } else {
    ctx.body = 'hello jsonp'
  }
})

app.listen(3000, ()=>{
  console.log('run on 3000 port')
})

中间件实现JSONP

const Koa = require('koa')
const jsonp = require('koa-jsonp')
const app = new Koa()

// 使用中间件
app.use(jsonp())

app.use( async ( ctx ) => {

  let returnData = {
    success: true,
    data: {
      text: 'this is a jsonp api',
      time: new Date().getTime(),
    }
  }

  // 直接输出JSON
  ctx.body = returnData
})

app.listen(3000, () => {
  console.log('[demo] jsonp is starting at port 3000')
})

总结

现在用到中间件有:

  • koa-router => 简便处理ctx.request.url的过程
  • koa-bodyparser => 简便处理请求类型为POST的参数
  • koa-static => 简便处理模块之间引用静态资源路径的问题
  • koa-views => 模板引擎
  • koa-jsonp => JSONP

现在掌握了的API有:

  1. ctx.url 获取当前url
  2. ctx.request 获取请求对象
  • ctx.request.query <===> ctx.query 获取请求参数(格式化)
  • ctx.request.queryString <===> ctx.queryString 获取请求参数(字符串)
  1. ctx.req 调用原生node请求属性/方法
  • ctx.req.addListener('data', (data) => {})
  • ctx.req.addListener('end', () => {}) // 上述两者都用于处理POST请求参数
  1. ctx.response 获取响应对象
  2. ctx.res 调用原生node响应属性/方法
  • ctx.res.writeHead(statusCode, reason) 自定义响应头的状态码和原因
  • ctx.res.write(content, 'binary') 以二进制格式输出内容,第二个参数可变
  • ctx.res.end() 结束响应

现在掌握了的Node原生API有:

  1. fs
  • fs.existsSync(filepath/directorypath) 同步判断该路径下的文件或目录是否存在
  • fs.statSync(directorypath).isDirectory() 同步判断该路径指的是否为目录
  • fs.readFileSync 同步读文件 / fs.readFile 异步读文件
  • fs.mkdirSync 同步创建目录 / fs.mkdir 异步创建文件
  1. path
  • path.join(path1, path2) 将path1和path2拼接起来,返回完整路径
  • path.resolve(path1, path2) 同上
  • path.extname 用于截取请求url的后缀
  • path.dirname(path) 获取当前路径的父级目录
  1. util
  • util.inspect(object[, options]) 传入JS原始值或对象,返回字符串形式的传入参数
  1. cookie
    // koa的ctx自带读取 和 写入 cookie的方法
  • ctx.cookies.get(name, [options]) 读取cookie
  • ctx.cookies.set(name, value, [options]) 写入cookie
ctx.cookies.set('name', 'ljm', {
    domain: 'localhost',  // 写cookie所在的域名
    path: '/index',       // 写cookie所在的路径
    maxAge: 10 * 60 * 1000, // cookie有效时长
    expires: new Date('2017-02-15'),  // cookie失效时间
    httpOnly: false,  // 是否只用于http请求中获取
    overwrite: false  // 是否允许重写
  }
)
  1. session
    // 遗憾的是跟post请求一样,koa并没有提供读取 和 写入session 的方法。只能通过自己实现或者通过第三方中间件实现:
  • koa-session-minimal 适用于koa2 的session中间件,提供存储介质的读写接口 。
  • koa-mysql-session 为存储介质koa-session-minimal中间件提供MySQL数据库的session数据读写操作。

0703

数据流

Redux architecture revolves around a strict unidirectional data flow.

官网原话:Redux架构是围绕严格的==单向数据流==。

数据流的生命周期遵循以下4点:

  1. store.dispatch(action)
  • 能够在React应用的任何地方调用这个方法,无论是组件还是异步回调还是定时器。
  1. Redux会调用所有你定义的Reducer函数
  • 初始化Reducer,前面提到过。
  • 只会计算下一个状态,并且是可预测的:相同的参数进来,相同的输出出去。
  • 期间不应有任何API调用和路由跳转。
  1. 一个根Reducer永远对应一个状态树
  • 但Reducer可以有多个,对应的State也有多个。
// combineReducer
function todos(state = [], action) {
  // Somehow calculate it...
  return nextState
}

function visibleTodoFilter(state = 'SHOW_ALL', action) {
  // Somehow calculate it...
  return nextState
}

// 多个Reducer合并成一个
let todoApp = combineReducers({
  todos,
  visibleTodoFilter
})

var store = createStore(todoApp)
  1. 通过根Reducer返回整个Redux的完整状态树
  • 一个起点发散开来。

0802

React16.0.0 beta

关于最近发布的beta版,新引入了一个概念:错误划分。

之前版本的React一直是遇到错误就会导致整个应用崩溃,为了解决这类问题,16.0.0将会有一个“错误分界线”的概念,在任何组件树的任何地方都能接收到JS的错误,打印并反馈对应UI,而不会是一串报错。为此,React新增了一个生命周期来处理:componentDidCatch(error, info)。

// 简化了
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  componentDidCatch(error, info) {
    // Display fallback UI
    this.setState({ hasError: true });
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>NO,NO,NO!Something went wrong.</h1>;
    }
    // 判断是否将当前组件做成插槽
    if (this.props.children) {
      return this.props.children
    }
    // 没做插槽的话默认返回文案
    return <h1>Hey,buddy!</h1>
  }
}

带插槽的样子

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>
  • componentDidCatch()生命周期方法其实跟JS里面的try...catch...很像,只不过前者是给组件用的。
  • 建议16.0.0正式发布后,尽量声明一个错误划分的组件,并在应用中使用。
  • 错误划分组件是不能在内部捕捉到自身的错误,只会把错误传递到最接近的错误划分组件。
  • 错误划分的粒度是取决于使用者,可以把顶级路由包起来,也可以把小组件包起来,以避免它们能够在应用稳定运行。

没被捕获的错误

在16.0.0没被 错误划分组件 所捕获的错误,将会导致整个React组件树的卸载。为什么会直接卸载而不是保留部分组件?

举例:像某些聊天产品,虽然应用崩溃了,但还是可以让用户发信息,这就可能导致用户发送了错误信息,所以显示部分UI远比不显示UI还要糟糕;而如果我们对某个对话框做了错误划分的话,那么依然可以通过UI引导用户去做相应操作。

0720~0721(0726补充)

话题

一个http请求访问web服务静态资源,一般响应结果有三种情况:

  • 访问文件,例如后缀为js,css,png,jpg,gif
  • 访问静态目录
  • 找不到资源,抛出404错误
// index.js
const Koa = require('koa')
const app = new Koa()
const path = require('path')
const content = require('./util/content') // 读取请求内容
const mimes = require('./util/mimes') // 读取文件类型

// 静态资源目录相对于当前入口文件index.js的路径 => 但如果在其他模块调用这个入口文件的方法时,就不能直接使用这个路径,而需要把它处理成绝对路径再提供给别的模块(下面就有多个地方用path.resolve / path.join 来处理)。
const staticPath = './static'

// 直接返回请求内容的类型
function parseMime (url) {
  // path.extname 这个是node原生用于截取请求url后缀的方法
  let extName = path.extname(url)
  // str.slice(x) 从第x位开始截取
  extName = extName ? extName.slice(1) : 'unknown'
  // 拿着url后缀给引入的mimes对象的键值匹配来返回
  return mimes[extName]
}

// 为什么要这样写?因为koa2所有跟请求响应相关的操作都封装到ctx
app.use(async (ctx) => {
  // 静态资源的绝对路径(转换成绝对路径后,再提供给别的模块)
  let fullStaticPath = path.join(__dirname, staticPath)
  
  // 获取静态资源内容,优先(1)根据请求的url来在(2)静态资源目录里面找对应的资源文件
  let _content = await content(ctx, fullStaticPath)

  // 解析请求内容的类型 => 目的就是为了以对应的输出格式把内容输出出来
  let _mime = parseMime(ctx.url)

  // 如果有对应的文件类型,就配置上下文的类型 => 其实就是保存
  if (_mime) {
    ctx.type = _mime
  }

  // 输出静态资源内容
  if (_mime && _mime.indexOf('image/') !== -1) {
    // 图片需要直接改用原生node方法来输出二进制数据 => 后续查查究竟是什么类型才会以二进制输出
    ctx.res.writeHead(200, "U request is image's type")
    ctx.res.write(_content, 'binary')
    ctx.res.end()
  } else {
    // 其他类型则以流输出
    ctx.body = _content
  }
})

app.listen(3000)
console.log('[demo] request post is starting at port 3000')

专门用于获取静态资源内容的模块,(1)根据请求的url来在(2)静态资源目录里面找对应的资源文件

// content.js
const path = require('path')
const fs = require('fs')
// 引入读取目录内容方法
const dir = require('./dir')
// 引入读取文件内容方法
const file = require('./file')

/**
 * 获取静态资源内容
 * @param  {object} ctx koa上下文
 * @param  {string} 静态资源目录在本地的绝对路径
 * @return  {string} 请求获取到的本地内容
 */
async function content( ctx, fullStaticPath ) {

  // 将静态资源路径和请求的URL拼接 => 得到的就是该 请求在静态资源的完整路径
  let reqPath = path.join(fullStaticPath, ctx.url)

  // 判断完整请求路径是否为存在目录或者文件【同步】
  let exist = fs.existsSync( reqPath )

  // 返回请求内容, 默认为空
  let content = ''

  if( !exist ) {
    // 如果请求路径不存在,返回404
    content = '404 Not Found! o(╯□╰)o!'
  } else {
    // 判断访问地址是文件夹还是文件 => fs.statSync
    let stat = fs.statSync( reqPath )

    if( stat.isDirectory() ) {
      // 如果为目录,则读取目录内容 => 默认读取该目录的index.html
      // 将目录下的所有文件都以<ul><li></li></ul>这样的列表遍历出来
      content = dir( ctx.url, reqPath )
    } else {
      // 如果为文件,则读取文件内容
      content = await file( reqPath )
    }
  }
  return content
}

module.exports = content

专门用于处理目录的模块,把对应目录的所有内容遍历出来,无论目录还是文件

// dir.js
const url = require('url')
const fs = require('fs')
const path = require('path')

// 遍历读取目录内容方法
const walk = require('./walk')

/**
 * 封装目录内容
 * @param  {string} url 当前请求的上下文中的url,即ctx.url
 * @param  {string} reqPath 请求静态资源的完整本地路径
 * @return {string} 返回目录内容,封装成HTML
 */
function dir ( url, reqPath ) {
  // 遍历读取当前目录下的文件、子目录的模块
  let contentList = walk( reqPath )

  let html = `<ul>`
  for ( let [ index, item ] of contentList.entries() ) {
    // console.log(`${url === '/' ? '' : url}/${item}`) // => /css /js /image /index.html
    html = `${html}<li><a href="${url === '/' ? '' : url}/${item}">${item}</a></li>` 
  }
  html = `${html}</ul>`

  return html
}

module.exports = dir

遍历读取目录内容(子目录,文件名)的模块,其实跟上述的dir是一体的,只是解耦抽出来了。

// walk.js
const fs = require('fs')
const mimes = require('./mimes')

/**
 * 遍历读取目录内容(子目录,文件名)
 * @param  {string} reqPath 请求资源的绝对路径
 * @return {array} 目录内容列表
 */
function walk( reqPath ){
  // node原生方法--同步读取目录下的所有文件,返回数组
  let files = fs.readdirSync( reqPath );
  let dirList = [], fileList = [];
  for( let i=0, len=files.length; i<len; i++ ) {
    let item = files[i];
    // 将带'.'的文件名拆分开,例如index.html => ['index', 'html']
    let itemArr = item.split("\.");
    // 通过判断itemArr的数组长度是否大于1,来定位其是文件还是目录;如果是文件,则把其后缀放进mimes模块判断;如果是目录,则赋值为undefined
    let itemMime = ( itemArr.length > 1 ) ? itemArr[ itemArr.length - 1 ] : "undefined";
    if( typeof mimes[ itemMime ] === "undefined" ) {
      // 如果是undefined,则把当前项归去 目录列表
      dirList.push( files[i] );
    } else {
      // 如果带后缀(即文件),则把当前项归去 文件列表
      fileList.push( files[i] );
    }
  }

  // 最后将传入的reqPath参数所在的 所有目录和文件 合并后返回
  let result = dirList.concat( fileList );
  return result;
};

module.exports = walk;

检查媒体类型的模块

// mimes.js
// 媒体类型
let mimes = {
  'css': 'text/css',
  'less': 'text/css',
  'gif': 'image/gif',
  'html': 'text/html',
  'ico': 'image/x-icon',
  'jpeg': 'image/jpeg',
  'jpg': 'image/jpeg',
  'js': 'text/javascript',
  'json': 'application/json',
  'pdf': 'application/pdf',
  'png': 'image/png',
  'svg': 'image/svg+xml',
  'swf': 'application/x-shockwave-flash',
  'tiff': 'image/tiff',
  'txt': 'text/plain',
  'wav': 'audio/x-wav',
  'wma': 'audio/x-ms-wma',
  'wmv': 'video/x-ms-wmv',
  'xml': 'text/xml'
}

module.exports = mimes

专门用于处理文件的模块,把对应文件直接用node原生方法读出来

// file.js
const fs = require('fs')

/**
 * 读取文件方法
 * @param  {string} 文件本地的绝对路径
 * @return {string|binary} 
 */
function file ( filePath ) {
 // node原生方法--以二进制同步读取
 let content = fs.readFileSync(filePath, 'binary' )
 return content
}

module.exports = file

静态资源中间件

为了方便在模块间互相调用,需要顾及到路径的问题,所以使用中间件koa-static来指定静态资源目录。

const Koa = require('koa')
const path = require('path')
const static = require('koa-static')

const app = new Koa()

// 静态资源目录对于相对入口文件index.js的路径
const staticPath = './static'

app.use(static(
  path.join( __dirname,  staticPath)
))


app.use( async ( ctx ) => {
  ctx.body = 'hello world'
})

app.listen(3000)
console.log('[demo] static-use-middleware is starting at port 3000')

总结

es6:
array.entries();

返回一个迭代器,虽然在控制台打印是一个空对象,但是只要用于for...of...里面就可以自动迭代。
用法:

// 可以手动调用entries.next()控制迭代器迭代
var entries = ["a", "b", "c"].entries();
/*
  entries.next().value == [0, "a"]
  entries.next().value == [1, "b"]
  entries.next().value == [2, "c"] 
*/

// 也可以自动让其迭代
for (let [index, item] of ['a', 'b', 'c'].entries()) {
  console.log(item)
}

现在用到中间件有:

  • koa-router => 简便处理ctx.request.url的过程
  • koa-bodyparser => 简便处理请求类型为POST的参数
  • koa-static => 简便处理模块之间引用静态资源路径的问题

现在掌握了的API有:

  1. ctx.url 获取当前url
  2. ctx.request 获取请求对象
  • ctx.request.query / ctx.query 获取请求参数(格式化)
  • ctx.request.queryString / ctx.queryString 获取请求参数(字符串)
  1. ctx.req 调用原生node请求属性/方法
  • ctx.req.addListener('data', (data) => {})
  • ctx.req.addListener('end', () => {}) // 上述两者都用于处理POST请求参数
  1. ctx.response 获取响应对象
  2. ctx.res 调用原生node响应属性/方法
  • ctx.res.writeHead(statusCode, reason) 自定义响应头的状态码和原因
  • ctx.res.write(content, 'binary') 以二进制格式输出内容,第二个参数可变
  • ctx.res.end() 结束响应

现在掌握了的Node原生API有:

  1. fs
  • fs.existsSync(filepath/directorypath) 同步判断该路径下的文件或目录是否存在
  • fs.statSync(directorypath).isDirectory() 同步判断该路径指的是否为目录
  • fs.readFileSync 同步读文件
  1. path
  • path.join(path1, path2) 将path1和path2拼接起来,返回完整路径
  • path.resolve(path1, path2) 同上
  • path.extname 用于截取请求url的后缀

0801

实现简单上传文件

相关依赖

npm install busboy --save

启动文件

// app.js
const Koa = require('koa')
const path = require('path')
const app = new Koa()

//  关键代码
const { uploadFile } = require('./util/upload')

app.use( async ( ctx ) => {

  if ( ctx.url === '/' && ctx.method === 'GET' ) {
    // 当GET请求时候返回表单页面
    let html = `
      <h1>koa2 upload demo</h1>
      <form method="POST" action="/upload.json" enctype="multipart/form-data">
        <p>file upload</p>
        <span>userName:</span><input name="userName" type="text" /><br/>
        <span>picName:</span><input name="picName" type="text" /><br/>
        <input name="file" type="file" /><br/><br/>
        <button type="submit">submit</button>
      </form>
    `
    ctx.body = html

  } else if ( ctx.url === '/upload.json' && ctx.method === 'POST' ) {
    // 上传文件请求处理
    let result = { success: false }
    let serverFilePath = path.join( __dirname, 'upload-files' )

    // 上传文件事件
    // 等待uploadFile返回结果再执行下面代码;像前面提到的,最后一个await必须返回一个Promise对象
    result = await uploadFile( ctx, {
      fileType: 'album', // common or album
      path: serverFilePath
    })

    ctx.body = result
  } else {
    // 其他请求显示404
    ctx.body = '<h1>404!!! o(╯□╰)o</h1>'
  }
})

app.listen(3000)
console.log('[demo] upload-simple is starting at port 3000')

关键代码

// uploadFile.js
const inspect = require('util').inspect
const path = require('path')
const os = require('os')
const fs = require('fs')
const Busboy = require('busboy')

/**
 * 同步创建文件目录
 * @param  {string} dirname 目录绝对地址
 * @return {boolean}        创建目录结果
 */
function mkdirsSync( dirname ) {
  if (fs.existsSync( dirname )) {
    return true
  } else {
    // 如果当前目录不存在,则往上一级查找
    if (mkdirsSync( path.dirname(dirname)) ) {
      // 然后以当前目录创建并命名一个新的目录
      fs.mkdirSync( dirname )
      return true
    }
  }
}

/**
 * 获取上传文件的后缀名
 * @param  {string} fileName 获取上传文件的后缀名
 * @return {string}          文件后缀名
 */
function getSuffixName( fileName ) {
  let nameList = fileName.split('.')
  return nameList[nameList.length - 1]
}

/**
 * 上传文件
 * @param  {object} ctx     koa上下文
 * @param  {object} options 文件上传参数 fileType文件类型, path文件存放路径
 * @return {promise}         
 */
function uploadFile( ctx, options) {
  let req = ctx.req
  let res = ctx.res
  let busboy = new Busboy({headers: req.headers})

  // 获取类型
  // 默认如果没传入文件类型,则统一把资源文件放到common目录下
  let fileType = options.fileType || 'common'
  let filePath = path.join( options.path,  fileType)
  let mkdirResult = mkdirsSync( filePath )

  return new Promise((resolve, reject) => {
    console.log('文件上传中...')
    let result = { 
      success: false,
      formData: {},
    }

    // 解析请求文件事件
    busboy.on('file', function(fieldname, file, filename, encoding, mimetype) {
      console.log(fieldname, file, filename, encoding, mimetype, 1)
      // 随机生成字符串加上当前文件的后缀,生成新的加密文件名
      let fileName = Math.random().toString(16).substr(2) + '.' + getSuffixName(filename)
      // 改造后的文件所在的目录
      let _uploadFilePath = path.join( filePath, fileName )
      // 保存路径,同上
      let saveTo = path.join(_uploadFilePath)

      // 文件保存到指定路径
      file.pipe(fs.createWriteStream(saveTo))

      // 文件写入事件结束
      file.on('end', function() {
        result.success = true
        result.message = '文件上传成功'

        console.log('文件上传成功!')
        resolve(result)
      })
    })

    // 解析表单中其他字段信息
    busboy.on('field', function(fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) {
      console.log(fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype, 2)
      console.log('表单字段数据 [' + fieldname + ']: value: ' + inspect(val));
      result.formData[fieldname] = inspect(val);
    });

    // 解析结束事件
    busboy.on('finish', function( ) {
      console.log('文件上传结束')
      resolve(result)
    })

    // 解析错误事件
    busboy.on('error', function(err) {
      console.log('文件上出错')
      reject(result)
    })

    req.pipe(busboy)
  })
} 

module.exports =  {
  uploadFile
}

API

path.dirname(path) 获取当前路径的父级目录
util.inspect(object[, options]) 传入JS原始值或对象,返回字符串形式的传入参数
fs.mkdirSync 同步创建目录

0826

前言

需要设计一个前端由react,后端由koa2搭建的后台管理系统。

框架设计

├── database # 数据库初始化目录
│ ├── index.js # 初始化入口文件
│ ├── sql/ # sql脚本文件目录
│ └── util/ # 工具操作目录
├── package.json
├── config.js # 配置文件
├── server # 后端代码目录
│ ├── app.js # 后端服务入口文件
│ ├── codes/ # 提示语代码目录
│ ├── controllers/ # 操作层目录(mvc--c)- 执行服务端模板渲染,json接口返回数据,页面跳转
│ ├── models/ # 数据模型model层目录(mvc--m)- 数据模型层 执行数据操作
│ ├── routers/ # 路由目录
│ ├── services/ # 业务层目录 - 实现数据层model到操作层controller的耦合封装
│ ├── utils/ # 工具类目录
│ └── views/ # 模板目录(mvc--v)
└── static # 前端静态代码目录
| ├── build/ # webpack编译配置目录
| ├── dist/ # 编译后前端代码目录&静态资源前端访问目录
| └── src/ # 前端源代码目录

0627

停下来想想,发觉自己下班后的生活不是看动画片就是打游戏,好像也没有怎么把工作上或者学习上的一些事情给记录下来,本着这个出发点,也就有了以下的记录。


最近

  • 上周webpack3.0刚正式发布,老版本的 Webpack 需要==将每个模块包裹在单独的函数闭包中以实现模块系统==,而这些封装函数往往会使得浏览器中运行的 JavaScript 代码性能有所下降;而新版本的Webpack提供插件会将代码中所有的==模块作用域连接到单一闭包中==,从而保证了浏览器中的代码运行速度。

  • 而 Webpack 3 中提供了如下的插件来允许开发者启用作用域提升特性来避免这种额外的性能损耗:

module.exports = {  
  plugins: [
    new webpack.optimize.ModuleConcatenationPlugin()
  ]
};

个人规划

由于在公司每个人都要有自己的OKR(大概就是一些季度计划),而自己总是每个季度都因为各种原因(其实是懒...)没有做到,所以这个季度(7~9月)的OKR打算认真做一做(稍作修改):

  • 从Vue + Koa的项目开始做起,从侧面学习Node
  • 对比学习Redux以及Mobx
  • Webpack2官方文档撸完
  • 学习一门后端语言(Go/Python),达到入门
  • 余下根据实际情况安排

估时:

  • 第一件花费时间为2周 暂没有固定时间
  • 第二件可以每天安插1小时做,先由Redux开始
  • 第三件可以每天安插半小时做,例如: 入口、出口、加载器、插件等
  • 第四件根据同学提供的视频进行学习(以前三件优先级为准)

0628

工作碰到的

在React中使用pre标签

class SomeComponent extends React.Component {
   render() {
        return (
          {/* 在反引号后面的字符串还是标点符号(包括空格)都会照旧在页面上展示出来 */}
          <pre>{`
            Hello   ,   
            World   .
          `}</pre>
        )
    }
}

ReactDOM.render(
  <SomeComponent />, 
  document.getElementById('content')
)

DOM上显示的效果是:

<pre>
            Hello   ,
            World   .
</pre>

话题

今天想花一点时间总结下自己学习React的过程中,结合资料悟到的一些道理:

  • 组件化的粒度:大到一个页面,小到一个div,都可以被称为组件。但在设计前必须要把整个页面的布局做好组件的划分(个人习惯写在纸上),好多次自己封装组件的时候都会把一些不是逻辑的元素集合 / 使用次数整个程序只出现一到两次的元素集合 也划分成组件,导致代码冗余并且显得没必要,尽量避免不必要的封装。
  • 组件内部状态State:内部状态跟UI是挂钩的。一般组件内部都有自身的状态,除非是无状态组件 =>没有内部状态State,没有ref,没有生命周期函数,若不是需要控制生命周期函数的话建议多使用无状态组件,获得更好的性能。
const Stateless = (props) => (
  <div>{ this.props.text }</div>
)

<Stateless text="Hello World." />
  • 外部属性Props:从外部传入来的参数,可以是组件 / 值 / 函数。组件可以根据传入的外部参数进行UI上的切换 => 既然外部参数可以传入==组件==,那就引入了组件嵌套的概念。
  • 父子组件间的通信:在不使用Redux / Mobx的情况下,父子组件间通信过程:
  • 1. 子组件使用父组件的状态:
graph LR
父组件State-->子组件Props
  • 2. 子组件调用父组件的方法:
graph LR
父组件Methods-->子组件Props
  • 3. 父组件使用子组件的方法 (无法做到,因为React是单向数据流,只能由父组件传给子组件)
    :子组件Methods => 父组件
  • 4. 父组件准备好子组件要用的状态从 props 传入,既然子组件的状态是由外部传入,换句话说,父组件的方法就能控制子组件的状态
class ParentComponent extends Component {
   constructor(props) {
      super(props);
      this.state = {
        open: false;
      }
   }

   toggleChildMenu() {
      this.setState({
        open: !this.state.open
      });
   }

   render() {
      return (
         <div>
           <button onClick={this.toggleChildMenu.bind(this)}>
              Toggle Menu from Parent
           </button>
           <ChildComponent open={this.state.open} />
         </div>
       );
    }
}

class ChildComponent extends Component {
    render() {
      return (
         <Drawer open={this.props.open}/>
      );
    }
}
  • 虽然React是单向数据流,但是依旧可以通过绑定控制状态,从而影响状态:
// 伪代码
<div onClick={ this.changeState }>{ this.state.text }</div>

changeState () {
  this.setState({
    'text': 'Hello World.'
  })
}
  • 生命周期:众多的生命周期来预测并控制整个组件的状态流向。【验证组件是否更新可以采用生命周期】
  • React并不像Vue一样拥有双向数据绑定,但在不考虑性能基础上,也能做到类似 双向绑定的效果 => 就是上述提到的给元素绑定事件来修改状态。

概念

  • VDOM(虚拟DOM):顾名思义,不是真的2333,O。O。虚拟DOM相对于真实DOM,它是用户发起修改DOM操作时才存在的预处理阶段,只有满足一定条件下虚拟DOM才会渲染成真实DOM。考虑到其多了一步differ计算的过程,可能调用到内存,所以肯定会造成一定的性能损失,只是考虑到其带来的工程性的提升,这点性能损失不足为道。上述提及的differ计算只是VDOM的其中一点损耗性能缺点,如果任由用户不做限制的不停更新VDOM,这里面所引起的不适当重绘也会使性能显著下降,所以建议书写组件的时候不要忽略掉生命周期中的shouldComponentUpdate,这个方法的参数是nextProps和nextState,返回的是一个bool值,React会根据这个返回值来允许或阻止组件重绘。
shoudComponentUpdate(nextProps, nextState) {
  // 伪代码  
  return nextPropppps !== this.props;
}

通过设置类似上述合适条件,来防止绝大部分的无效重绘。

关于上面提及到的组件间状态传递,下一篇博文更新。

参考地址

0710

话题

React-Redux

为什么使用React-Redux?

  • 不使用:首先需要把store传给根组件,作为其props上的属性;然后根组件把自身props上的store属性再传递给其子组件,作为其子组件props上的属性;子组件也嵌套了子组件的话,那就继续传给对应子组件的props属性。。。
  • 以上就需要在每个组件的逻辑上都加上一句this.state.store = this.props.store.getState(),把从store获取到的state传给下一个组件。大大增加了代码的冗余性,所以才衍生了这个React-Redux插件,统一在插件进行处理。

provider

connect

  • 格式:connect(mapStateToProps, mapDispatchToProps, mergeProps, options)
  • 使用方法:const wrappedComponentClass = connect(mapStateToProps, mapDispatchToProps, mergeProps, options)(ComponentClass)

mapStateToProps

定义这个参数用来监听Redux的store变化。
作用:顾名思义,把state完整copy一份到组件的props(map),返回为一个对象

  1. 任何时候只要Redux的store发生变化,mapStateToProps这个函数就会被调用。
  2. 该函数一般情况下会返回对象,这个对象会跟连接组件的props进行合并。
  3. 如果省略了这个参数,组件将不会监听Redux的store。
  4. 第二个参数可选,用来表示被链接的组件props;设置后,每次组件接收到新的props,都会触发该mapStateToProps函数。
  5. 为了更好地去控制渲染的性能,mapStateToProps这个函数有可能返回函数,但在绝大多数的应用中不会用到。
  6. 当然,你不必将 state 中的数据原封不动地传入组件,可以根据 state 中的数据,动态地输出组件需要的(最小)属性,减少不必要的性能消耗。
// 将state里的count变量copy一份到组件的props
const mapStateToProps = function (state, ownProps) {
    return {
        count: state.count
    }
}

// 有时候可能需要组件自身的ownProps来判断state需要取哪些
// state为:{userList: [{id: 0, name: 'leung'}]
const mapStateToProps = function (state, ownProps) {
    return {
        count: _.find(state.userList, {ownProps.userId})
    }
}

export default mapStateToProps

这就是 组件获取 Redux store 中的数据的基本方式。

mapDispatchToProps

定义这个参数用来分发action给Redux的reducer处理,生成新的state。
作用:顾名思义,有上述获取store的方法,自然就有更新store的方法。

  1. Redux的 action 的变化不会触发该函数,默认 action 在组件的生命周期中是固定的。
  2. 该函数一般情况下会返回对象,这个对象会跟连接组件的props进行合并。
  3. 如果省略了这个参数,默认情况下,dispatch会注入到连接组件的props中。
  4. 第二个参数可选,用来表示被链接的组件props;设置后,每次ownProps变化的时候,该函数也会被调用。
// 组件通过props的事件方法来分发action
const mapDispatchToProps = function (dispatch, ownProps) {
    return {
        increase: () => dispatch({type: 'increase'}),
        decrease: () => dispatch({type: 'decrease'})
    }
}

export default mapDispatchToProps

暂停一下

根据上述简单介绍React-Redux,可以得出目前我们的连接组件props属性有这些值:

// es6解构
// 分别对应:获取state的count变量,分发事件increase / decrease;全部都只需要通过this.props即可调用。
const { count, increase, decrease } = this.props

具体代码:

import mapStateToProps from './mapStateToProps'
import mapDispatchToProps from './mapDispatchToProps'

class MyComp extends Component {
  render(){
    // 这三个变量都是上述mapStateToProps, mapDispatchToProps映射到组件的props上的,通过解构赋值抽离出来
    const {count, increase, decrease} = this.props;
    return (
      <div>
        <div>计数:{this.props.count}次</div>
        <button onClick={increase}>增加</button>
        <button onClick={decrease}>减少</button>
      </div>
    )
  }
}

const Comp = connect(mapStateToProps, mapDispatchToProps)(MyComp);

ReactDOM.render(Comp, document.getElementById('app'));

特殊场景

  • *如果上面的MyComp组件里面还有一个SonComp组件,这个组件需要用到mapDispatchToProps的action creators,而我们既不想让这个组件察觉到Redux的存在,又不希望把Redux的store和dispatch传给它。那么就该对mapDispatchToProps这个函数再封装。
  • 上述场景就要用到bindActionCreators这个API。

bindActionCreators(actionCreators, dispatch)

参数:

  • actionCreators (func / obj)
  • dispacth
    返回:
    传入的是对象,返回的就是对象,只不过这个对象中的每个函数值都可以直接调用dispatch,连接组件就不用像之前this.props.increase()来分发action,而是this.props.increase;传入的是函数,返回的就是函数。
// 举个例子
// TodoActionCreators.js
export function addTodo(text) {
  return {
    type: 'ADD_TODO',
    text
  };
}

export function removeTodo(id) {
  return {
    type: 'REMOVE_TODO',
    id
  };
}
import TodoActionCreators from './TodoActionCreators'

class TodoListContainer extends React.Component {
  componentDidMount () {
    // 只要使用了React-Redux默认就会注入
    let { dispatch } = this.props
    // 这里其实就可以直接分发了
    dispatch( TodoActionCreators.addTodo('Use Redux') )
  }  
  render () {
    // 这里的场景就是上述提到的:分发action需要带到下一个组件中,又不想让它发现Redux存在。
    let { todos, dispatch } = this.props
    let boundActionCreators = bindActionCreators(TodoActionCreators, dispatch)
    return (
      <TodoList 
        todos = { todos }
        {/* 在子组件里,可以完全不知道Redux的存在。*/}
        { ...boundActionCreators  }
       />
    )
  }
}

重点!这里boundActionCreators的值是:addTodo: dispatch( {type: 'ADD_TODO', text} ), removeTodo: dispatch( {type: 'REMOVE_TODO', id} )。相当于给addTodo: {type: 'ADD_TODO', text}, removeTodo: {type: 'REMOVE_TODO', id}的所有键值加上dispatch并执行!

mergeProps

接着讨论connect的第三个参数。

  1. 该函数会等前面mapStateToProps, mapDispatchToProps执行完后,把返回结果和连接组件的props传入来。
  2. 传进来有什么用?有时候如果你需要等待连接组件的props来判断是否把store的state合并到连接组件,此时就是合适时候。
  3. 如果省略该参数,默认返回Object.assign({}, ownProps, stateProps, dispatchProps)的结果

options

接着讨论connect的第四个参数。如果指定这个参数,可以定制 connector 的行为。

  • [pure = true] (Boolean): 如果为 true,connector 将执行 shouldComponentUpdate 并且浅对比 mergeProps 的结果,避免不必要的更新,前提是当前组件是一个“纯”组件,它不依赖于任何的输入或 state 而只依赖于 props 和 Redux store 的 state。默认值为 true。
  • [withRef = false] (Boolean): 如果为 true,connector 会保存一个对被包装组件实例的引用,该引用通过 getWrappedInstance() 方法获得。默认值为 false。

0901

前言

前面介绍了服务器端相关配置,这里要介绍的是初始化数据库,也就是每个项目建立之前的必经之路。

初始化表

CREATE TABLE   IF NOT EXISTS  `user_info` (
  `id` int(11) NOT NULL AUTO_INCREMENT, # 用户ID
  `email` varchar(255) DEFAULT NULL,    # 邮箱地址
  `password` varchar(255) DEFAULT NULL, # 密码
  `name` varchar(255) DEFAULT NULL,     # 用户名
  `nick` varchar(255) DEFAULT NULL,     # 用户昵称
  `detail_info` longtext DEFAULT NULL,  # 详细信息
  `create_time` varchar(20) DEFAULT NULL,   # 创建时间
  `modified_time` varchar(20) DEFAULT NULL, # 修改时间
  `level` int(11) DEFAULT NULL, # 权限级别
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

# 插入默认信息
INSERT INTO `user_info` set name='admin001', email='[email protected]', password='123456';

0714

工作

  • 写官网之类的大型门户页面需要考虑到某些页面是否要做SEO,如果要做SEO的话就必须要做静态化,意思就是要有大体的结构,体现在使用类似avalon这类框架的时候,不能把整个页面都用avalon初始化。
  • 看了一下Avalon,大概到ms-text

业余

用React + Redux + React-Route +Antd搭建了一个个人系统,暂时只有todoList的功能。
地址详见有道云笔记。

0629

几句话

刚看到的一条评论,自我勉励下。

  • 偷懒救不了前端新人
  • 投机救不了前端新人
  • 浮躁救不了前端新人
  • 功利救不了前端新人
  • 懒惰救不了前端新人
  • 狭隘救不了前端新人
  • 上面这些救不了任何不努力的新人

工作碰到的

在Vue中使用Mixins

接触Vue也有一定时间了,到今天公众号推送的时候才发现有“Mixins”这个用法一直没用过,惭愧。。。TNT,于是赶紧用现有项目粗略试了一下。

  • 一般情况下,如果有多个页面都重复用同一块代码,可以把该块代码抽取成Mixins模块,再引入进来。

export default class Test extends React.Component {} 写错了(逃

// mixins (作为模块导出)
export default const commonMixins = {
    created () {
        console.log('from mixins')
    }    
}

// 某组件(伪代码)
import { commonMixins } from './mixins'
export default {
    data () {
        return {}
    },
    mixins: [commonMixins]  // 此处引用Mixins
}

// 控制台打印
> from mixins
  • 但使用时需要注意,生命周期方法发生冲突的时候,Mixins会首先被调用,然后是组件,但最后组件的会覆盖Mixins的,所以最后同样的输出会打印2次。
// 不冲突时
//mixin
const hi = {
  mounted() {
    console.log('hello from mixin!')
  }
}

//vue instance or component
new Vue({
  el: '#app',
  mixins: [hi],
  mounted() {
    console.log('hello from Vue instance!')
  }
});

//Output in console
> hello from mixin!
> hello from Vue instance!
// 冲突时
//mixin
const hi = {
  methods: {
    sayHello: function() {
      console.log('hello from mixin!')
    }
  },
  mounted() {
    this.sayHello()  // 方法两边都存在,发生冲突
  }
}

//vue instance or component
new Vue({
  el: '#app',
  mixins: [hi],
  methods: {
    sayHello: function() {
      console.log('hello from Vue instance!')
    }
  },
  mounted() {
    this.sayHello()  // 方法两边都存在,发生冲突
  }
})

// Output in console
> hello from Vue instance!
> hello from Vue instance!
  • 如果是基于构建的Vue项目,还可以全局设置Mixins,但真的不建议这样用,后果是所有的组件都会自动执行Mixins。
// main.js 在入口文件
Vue.mixin({
  mounted () {
    console.log('global mixins mounted.')
  }
})

// 某个组件
所有组件都会自动加上mounted方法

所以使用全局Mixins的时候需要认真考虑好场景。


话题

之前学习React的时候跟着教程过了一下Redux,并没有花太多时间在它身上(加上脑容量只有512M,老是忘记),于是乎有了下面(重新学习)的系列(逃

Redux是什么

graph LR
View层-->Action
graph LR
Action-->触发Dispath
graph LR
触发Dispath-->所有的Reducer都会接收State和Action
graph LR
Reducer判断Action.type-->返回新State
graph LR
State改变-->重绘View层

还有什么需要补充

// 官网原话
1.Redux doesn't have a Dispatcher or support many stores. Instead, there is just a single store with a single root reducing function. 
2.As your app grows, instead of adding stores, you split the root reducer into smaller reducers independently operating on the different parts of the state tree. This is exactly like how there is just one root component in a React app, but it is composed out of many small components.

1.Redux并不支持拥有多个stores,相反,永远只会有一个store跟一个顶级Reducer函数,也可以理解成一个state对应一个Reducer函数。
2.当应用逐渐扩展的时候,用户需要将顶级Reducer函数拆分成多个小的Reducer函数,用这些Reducers函数来操作state树对应的不同部分,而不是增加多个stores。就像React的App一样,永远只会有一个顶级组件,但却有无数的小组件。

Store

整个应用的数据存储在这个唯一的对象中。

API:

  • subscribe 监听 => 尽量使用React-Redux代替(后续会介绍)
  • dispatch 分发Action
  • getState 获取状态
// 使用Reducer初始化store【一般情况下】 => 如果出现多个Reducer的情况下需要用到combineReducer(合体!)
let store = createStore(reducer)

State

参考自http://www.jianshu.com/p/ba8654cb77b6

数据形式可以是数组、对象甚至是Immutable.js化的数据。
在书写代码前,最好规定好State的结构,怎么写?

  • 尽量将Redux的state从UI的state区分开来【必须健壮灵活,意为不要单纯为了给UI状态而设置状态】。
  • 多级组件嵌套的时候,尽量把state解耦不要写在同一个组件上。
  1. 假设现在有一套原始数据State
// 一个应用程序可能有多个列表页的数据
{
    homeList: [{}, {}],
    paperList: [{}, {}]
    // 其他的列表页数据
    tagList: [{}, {}],
    categoryList: [{}, {}]
}
  1. 横向改造(其实就是思考这几个列表有什么共通点,比如数据类别)
{
    // 文章相关的数据结构
    artidles: {
        // 首页列表
        list: [{}, {}],
        // Tag页列表
        tagList: [{}, {}]
    }
    // 研究所相关的数据结构
    papers: {
        // 研究所列表页
        list: [{}, {}]
    }
}

根据以上Redux就应该创建两套数据:state.articles 和 state.papers。两套数据之间彼此独立,分别对应两套Reducer更便于维护。

  1. 纵向改造(遇到多重嵌套的数据,学会把每一层都抽到一个对象里)
// 承接上述横向改造后的数据
papers: [{
   id: xxx,
    title: xxx,
    questions: [{
        id: xxx,
        title: xxx,
        sequence: xxx,
        options: [{
            id: xxx,
            title: xxx,
            sequence: xxx
        }]
    }] 
}]

假设我要每次都要利用options这个数组的数据去更新下拉列表,那么就必须走三层循环。这时就需要数据扁平化,把每层循环都抽离出来。

// 扁平化之后的数据
papers: [id1, id2]  // 关联部分
papersHash: {
    id1: {
        id: xxx,
        title: xxx,
        questions: [id1, id2]  // 关联部分
    }
}
questionsHash: {
    id1: {
        id: xxx,
        title: xxx,
        sequence: xxx,
        options: [id1, id2]  // 关联部分
    }
}
optionsHash: {
    id1: {
        id: xxx,
        title: xxx,
        sequence: xxx
    }
}

扁平化之后,数据被抽离到一个hash对象中,==根据key-value 键值对可以轻松获取指定对象的值,而关联部分只存储id作为索引。==
比如前面的场景,我们想要更新某个option,很简单,直接根据id更新optionsHash中的值即可。

目的是:更灵活的对数据进行增删改查,并且避免冗余数据的问题。

Reducer

参考自http://www.jianshu.com/p/ba8654cb77b6

Reducer是一个纯函数,根据传入的Action来改变对应Store里的State。核心:(oldState, action) => newState

  1. 以下事情千万不能在reducer里做:
  • 修改它的旧状态
  • 不要在里面有回调api和转换路由
  • 调用非纯函数,例如:Date.now() / Math.random() ==> 不可以改变原状态,但可以映射一个新状态返回
  • Redux初始化的时候会dispatch一个初始化action,用于调用所有的Reducer,输出结果统一是:undefined, { type: '@@redux/INIT' }。分别对应reducer的参数(state, action),而由于应用初始化时,state还没初始化,因此它的值是undefined。
  • 必须贯彻Reducer返回的是新state,类似深拷贝,永远是提供一个新的对象进行拷贝并返回
  • 必须永远针对默认情况都返回state
  // 伪代码
  case 'INCREMENT':
    return Object.assign({}, state, newState)
  case 'DECREMENT':
    return {...state, ...newState}  // es6解构写法,把state和newState的键值都拷贝到一个对象上
  default:
    return state
  1. 单个Reducer写法
// es6默认参数写法
function counter(state=0, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
}
  1. 多个Reducer写法(合体2333)

基于Redux的初始化函数createStore只能Redux提供了combineReducers()方法给我们将多个Reducer合并成一个根Reducer。

import { createStore, combineReducers } from 'redux'

// 假设上述创建了2个Reducer,分别叫:counter,displayer
var reducer = combineReducers({
    counter,
    displayer
})

// 通过combineReducers函数合并reducer,生成大store
// 在大store里面就可以访问所有reducer的私有状态state了
var store = createStore(reducer)
  1. 扩展

higher-order reducer:高阶reducer,接受reducer作为参数的函数/返回reducer作为返回值的函数。

  • 书写Reducer函数时尽量精减功能函数(CASE),抽离无关功能函数到另一个Reducer函数
// 抽离前,所有代码都揉到slice reducer中,不够清晰
function appreducer(state = initialState, action) {
    switch (action.type) {
        case 'ADD_TODO':
            ...
            ...
            return newState;
        case 'TOGGLE_TODO':
            ...
            ...
            return newState;
        default:
            return state;
    }
}

// 抽离后,将所有的state处理逻辑放到单独的函数中,reducer的逻辑格外清楚
function addTodo(state, action) {
    ...
    ...
    return newState;
}
function toggleTodo(state, action) {
    ...
    ...
    return newState;
}
function appreducer(state = initialState, action) {
    switch (action.type) {
        case 'ADD_TODO':
            return addTodo(state, action);
        case 'TOGGLE_TODO':
            return toggleTodo(state, action);
        default:
            return state;
    }
}
  • 功能函数(CASE)还能通过别的方式实现
// case函数变体
const ACTION_TYPES_HANDLER = {
  'ADD_TODO': (state={}, action) => {items: 'ljm'},
  'DEL_TODO': (state={}, action) => {items: 'user'}
}
// 
function reducer (state={}, action) {
  // 判断action.types是否存在于变体
  const condition = ACTION_TYPES_HANDLER[action.type]
  return condition ? condition(state, action) : state   // 永远给默认情况留值
}
  • 抽离工具函数 / 公共方法
  • 多个reduer间共享数据
// 如果有一份数据需要先由combineReducer处理完,再交给另外的reducer使用,可以定义一个crossReducer,但实际上传递给createStore初始化的时候只有rootReducer。
function crossReducer(state, action) {
  switch (action.type) {
    // 处理指定的action
    case UPDATE_COMMENTS:
      // 这里的state是combineReducer最终返回传入来的
      return Object.assign({}, state, {
        name: 'ljm'
      })
    default:
      return state;
  }
}

// 合成reducer
let combinedReducer = combineReducers({
    entities: entitiesreducer,
    increment: incrementReducer,
    papers: papersReducer
});

// 在其他reducer处理完成后,再进行crossReducer的操作
function rootReducer(state, action) {
    let tempstate = combinedReducer(state, action),
        finalstate = crossReducer(tempstate, action);
    return finalstate;
}

// 初始化Store
let store = createStore(rootReducer)

上述实现了combineReducer与crossReducer之间共享数据。当然,可以使用reduce-reducers这个插件来简化上面的rootReducer:

import reduceReducers from 'reduce-reducers';

export const rootReducer = reduceReducers(
    combineReducers({
        entities: entitiesreducer,
        increment: incrementReducer,
        papers: papersReducer
    }),
    crossReducer
)
  • 减少Reducer的样板代码(去除重复代码),
    现成的比较好的方案,比如:redux-actions
  1. 补充
    返回新状态可以有这两种写法~
function reducer (state, action) => {
  switch (action.type) {
    case 'TEST':
      return {
        ...state
      }
  }
}
function reducer (state, action) => {
  switch (action.type) {
    case 'TEST':
      // 对象的拷贝用法,把第二个参数和第三个参数同时拷贝到第一个参数空对象里。后续不作说明。
      return Object.assign({}, state, {
        completed: true
      })
  }
}

下一篇接着说明以下3点:

Action

#6

Midware

React-redux(Provider 和 Connect)

0726

话题

上面简单学习过路由、请求数据获取还有静态资源加载相关后,接下来要聊聊cookie/session。

cookie

koa的ctx自带读取 和 写入 cookie的方法:

  • ctx.cookies.get(name, [options]) 读取cookie
  • ctx.cookies.set(name, value, [options]) 写入cookie
const Koa = require('koa')
const app = new Koa()

app.use( async ( ctx ) => {

  if ( ctx.url === '/index' ) {
    ctx.cookies.set(
      'cid', 
      'hello world',
      {
        domain: 'localhost',  // 写cookie所在的域名
        path: '/index',       // 写cookie所在的路径
        maxAge: 10 * 60 * 1000, // cookie有效时长
        expires: new Date('2017-02-15'),  // cookie失效时间
        httpOnly: false,  // 是否只用于http请求中获取
        overwrite: false  // 是否允许重写
      }
    )
    ctx.body = 'cookie is ok'
  } else {
    ctx.body = 'hello world' 
  }

})

app.listen(3000)
console.log('[demo] cookie is starting at port 3000')

session

遗憾的是跟post请求一样,koa并没有提供读取 和 写入session 的方法。只能通过自己实现或者通过第三方中间件实现:

  • 如果session数据量很小,可以直接存在内存中
  • 如果session数据量很大,则需要存储介质存放session数据(MySQL) => 所谓的会话通常是登录用户后涉及到的,当用户关闭当前窗口则需要重新登录,生成新的会话信息。

数据库存储方案

  1. 将session存放在MySQL数据库中
  2. 过程需要用到的中间件
  • koa-session-minimal 适用于koa2 的session中间件,提供存储介质的读写接口 。
  • koa-mysql-session 为存储介质koa-session-minimal中间件提供MySQL数据库的session数据读写操作。
  • 将sessionId和对应的数据存到数据库 【到这里为止是写入】
  1. 将数据库的存储的sessionId存到页面的cookie中
  2. 根据cookie的sessionId去获取对应的session信息 【到这里为止是获取】
// 示例代码等看到MySQL相关章节再补充

10.23

工作

window.location.href在微信浏览器上可能无法实现跳转,可以使用top,window.location处理。

0705

待整理

结合 Redux中文实战文档去修改 #5 #4 #6 #8 的issue。

  1. Reducer深入
    区分于传统的Flux 和 Vuex,前面两者都是存在一个固定的state在某个位置,即"有状态的 store";但在Redux中,每次调用Reducer都需要传入一个新的参数,该参数会跟旧的 state 一起拷贝到新对象并返回,而该对象就是我们新的store。得出:Redux的state是存在于每次调用的Reducer上,即"无(稳定)状态的 store"。

话题

异步数据流

其实要说的是初始化数据流(store)。

  • 默认情况下,Redux的store是只支持同步数据流的,即createStore(),并没有使用任何中间件。
  • 而如果要异步调用action creator,则需要通过applyMidware(thunkMiddleware)来增强createStore(),变成applyMidware(thunkMiddleware)(createStore)
  • 多个异步action creators之间返回的函数thunk,可以多层嵌套,意味着不断返回函数,但最后一定要返回一个同步action creator对象,来将处理流程带回同步方式。

0704

工作

对最近几个项目做了总结,避免类似开发时再犯。
#7

话题

在前面零散的Redux基础入门中,我们讨论的action都是同步调用的,每次action触发,state都会马上更新。下面讨论下异步调用action。

  • 异步调用action需要用到redux-thunk来实现
  • 调用同步action creator返回的是一个对象;调用异步action creator返回的是一个函数,参数是dispatch / getState,也可以把当前action creator看成是thunk
  • 异步调用action返回的函数会通过redux-thunk执行,在这个函数内部可以包括网络请求等异步API。
  1. 首先定义一个同步action creator
function add_todo (state) {
  return {
    type: 'ADD_TODO',
    ...state
  }
}

// 另一种带上dispatch的写法:const add_todo = (state) => dispatch( { type: 'ADD_TODO', ...state } )
  1. 然后定义两个同步action creator
// actions.js
function requestPosts (state) {
  return {
    type: 'REQUEST_POSTS',
    ...state
  }
}

function receivePosts (state) {
  return {
    type: 'RECEIVE_POSTS',
    ...state
  }
}
  1. 接着定义中间件redux thunk
// actions.js
function fetchPosts (state) {
  return function (dispath) {
    dispatch(requestPost( { name: 'ljm' } ));  // 在thunk中调用同步action(变成thunk是由于返回的是一个函数)
    dispatch(responsePost( { name: 'ljm' } ));  // 同上
  }
}
  1. 如何使用?
// index.js 入口文件
import thunkMiddleware from 'redux-thunk'  // 导入中间件
import { createStore, applyMiddleware } from 'redux'  // 引入调用中间件函数
import { fetchPosts } from './actions'
import rootReducer from './reducers'

const loggerMiddleware = createLogger()

const store = createStore(
  rootReducer,
  applyMiddleware(
    thunkMiddleware // 初始化store的时候带上,就能允许我们调用异步action creator时用到dispatch了
  )
)

store.dispatch( add_todo({ name: 'ljm' }) ); // dispatch同步action creator返回的对象
store.dispatch(fetchPosts('reactjs')).then(() => console.log(store.getState())); // dispatch异步action creator返回的函数,成功后就会打印当前store的state

redux-thunk 并不是解决异步API调用action的唯一方法。详情见:

0630

Action

// action通常是一个对象,在异步时会是一个函数
{
    type: 'userAction', // 肯定会有type属性
    ...state // 其他扩展属性;state是一个对象,这里用到es6解构语法,把key-value对拷贝到当前对象
}

在真实App,最聪明的做法就是给每个Action构建最好尽量带上index来标记。

{
  type: 'userAction',
  index: 5,
  ...state
}

Action Creator

传统Flux的Action Creator:

function actionCreater (text) {
  const temp = {
    type: 'USER_ACTION',
    ...text
  }
  dispatch(temp)
}

但是Redux的Action Creator是这样:

// action creator
function addTodo (text) {
  return {
    type: 'USER_ACTION',
    ...text
  }
}

// 虽然不像Flux,但是可以模拟Flux做自动dispatch,唯一只做的就是调用actionCreater
const actionCreater = text => { dispatch(addTodo(text)) }

// 调用
actionCreater({
  name: 'ljm'
})

其实到最后也是调用这个函数返回的对象,用于传去Reducer。

0824

通常初始化数据库要建立很多表,特别在项目开发的时候表的格式可能会频繁些变动,【这时候就必须把sql文件跟逻辑代码解耦出来】,保留项目的sql脚本文件,然后每次需要重新建表,则执行建表初始化程序(db.js)就行。

目录规划

├── index.js # 程序入口文件
├── sql # sql脚本文件目录
│ ├── data.sql
│ └── user.sql
└── util # 工具代码
├── db.js # 封装的mysql模块方法
├── get-sql-content-map.js # 获取sql脚本文件内容
├── get-sql-map.js # 获取所有sql脚本文件
└── walk-file.js # 遍历sql脚本文件

关于操作SQL的步骤

  1. 遍历sql目录下的sql文件
  2. 解析所有sql文件的内容
  3. 执行sql文件

执行sql文件模块---db.js

const Koa = require('koa')
const cors = require('koa-cors') 
const app = new Koa()

const mysql = require('mysql')
const pool = mysql.createPool({
  host: '127.0.0.1',
  user: 'root',
  password: '********',
  database: 'sys'
})

// 在数据库中进行会话池操作
let query = (sql, values) => {
  return new Promise((resolve, reject) => {
    pool.getConnection((err, connection) => {
      if (err) {
        reject(err)
      } else {
        connection.query(sql, values, (err, rows) => {
          // connected! 
          if (err) {
            reject(err)
          } else {
            resolve(rows)
          }
          connection.release() // ok
        })
      }
      
    })
  })
}

module.exports = { query }

解析所有sql文件的内容---get-sql-content-map.js

const fs = require('fs')
const getSqlMap = require('./get-sql-map')

let sqlContentMap = {}

/**
 * 读取sql文件内容
 * @param  {string} fileName 文件名称
 * @param  {string} path     文件所在的路径
 * @return {string}          脚本文件内容
 */
function getSqlContent( fileName,  path ) {
  // 以二进制形式来读取sql文件
  let content = fs.readFileSync( path, 'binary' )
  // 创建一个以文件名称为键名,值为sql文件内容的对象
  sqlContentMap[ fileName ] = content
}

/**
 * 封装所有sql文件脚本内容
 * @return {object} 
 */
function getSqlContentMap () {
  let sqlMap = getSqlMap()
  for( let key in sqlMap ) {
    // sqlMap[key]是当前sql所在路径,key是当前sql的名称
    getSqlContent( key, sqlMap[key] )
  }

  return sqlContentMap
}

module.exports = getSqlContentMap

遍历sql目录下的sql文件---get-sql-map.js

const fs = require('fs')
const walkFile = require('./walk-file')

/**
 * 获取sql目录下的文件目录数据
 * @return {object} 
 */
function getSqlMap () {
  let basePath = __dirname
  basePath = basePath.replace(/\\/g, '\/')
  let pathArr = basePath.split('\/')
  pathArr = pathArr.splice( 0, pathArr.length - 1 )
  // 搞这么多就是为了获取相对于当前js文件的上一级目录下的sql目录位置。。。
  basePath = pathArr.join('/') + '/sql/'
  
  // 只匹配sql后缀的文件
  let fileList = walkFile( basePath, 'sql' )
  return fileList
}

module.exports = getSqlMap

纯属拆分作为一个遍历模块---walk-file.js

const fs = require('fs')

/**
 * 遍历目录下的文件目录
 * @param  {string} pathResolve  需进行遍历的目录路径
 * @param  {string} mime         遍历文件的后缀名
 * @return {object}              返回遍历后的目录结果
 */
const walkFile = function(  pathResolve , mime ){
  // 遍历目录下的内容
  let files = fs.readdirSync( pathResolve )
  let fileList = {}

   for( let [ i, item] of files.entries() ) {
    let itemArr = item.split('\.')
    
    // 筛选当前文件的后缀 与 传入的文件后缀名进行匹配
    let itemMime = ( itemArr.length > 1 ) ? itemArr[ itemArr.length - 1 ] : 'undefined'
    let keyName = item + ''
    if( mime === itemMime ) {
      fileList[ item ] =  pathResolve + item
    }
  }

  return fileList
}

module.exports = walkFile

入口文件

const fs = require('fs');
const getSqlContentMap = require('./util/get-sql-content-map');
const { query } = require('./util/db');


// 打印脚本执行日志
const eventLog = function( err , sqlFile, index ) {
  if( err ) {
    console.log(`[ERROR] sql脚本文件: ${sqlFile} 第${index + 1}条脚本 执行失败 o(╯□╰)o !`)
  } else {
    console.log(`[SUCCESS] sql脚本文件: ${sqlFile} 第${index + 1}条脚本 执行成功 O(∩_∩)O !`)
  }
}

// 获取所有sql脚本内容
let sqlContentMap = getSqlContentMap()
console.log(sqlContentMap)

// 执行建表sql脚本
const createAllTables = async () => {
  for( let key in sqlContentMap ) {
    let sqlShell = sqlContentMap[key]
    let sqlShellList = sqlShell.split(';')

    for ( let [ i, shell ] of sqlShellList.entries() ) {
      if ( shell.trim() ) {
        let result = await query( shell )
        if ( result.serverStatus * 1 === 2 ) {
          eventLog( null,  key, i)
        } else {
          eventLog( true,  key, i) 
        }
      }
    }
  }
  console.log('sql脚本执行结束!')
  console.log('请按 ctrl + c 键退出!')

}

createAllTables()

总结

现在用到中间件有:

  • koa-router => 简便处理ctx.request.url的过程
  • koa-bodyparser => 简便处理请求类型为POST的参数
  • koa-static => 简便处理模块之间引用静态资源路径的问题
  • koa-views => 模板引擎

现在掌握了的API有:

  1. ctx.url 获取当前url
  2. ctx.request 获取请求对象
  • ctx.request.query / ctx.query 获取请求参数(格式化)
  • ctx.request.queryString / ctx.queryString 获取请求参数(字符串)
  1. ctx.req 调用原生node请求属性/方法
  • ctx.req.addListener('data', (data) => {})
  • ctx.req.addListener('end', () => {}) // 上述两者都用于处理POST请求参数
  1. ctx.response 获取响应对象
  2. ctx.res 调用原生node响应属性/方法
  • ctx.res.writeHead(statusCode, reason) 自定义响应头的状态码和原因
  • ctx.res.write(content, 'binary') 以二进制格式输出内容,第二个参数可变
  • ctx.res.end() 结束响应

现在掌握了的Node原生API有:

  1. fs
  • fs.existsSync(filepath/directorypath) 同步判断该路径下的文件或目录是否存在
  • fs.statSync(directorypath).isDirectory() 同步判断该路径指的是否为目录
  • fs.readFileSync 同步读文件 / fs.readFile 异步读文件
  • fs.mkdirSync 同步创建目录 / fs.mkdir 异步创建文件
  1. path
  • path.join(path1, path2) 将path1和path2拼接起来,返回完整路径
  • path.resolve(path1, path2) 同上
  • path.extname 用于截取请求url的后缀
  • path.dirname(path) 获取当前路径的父级目录
  1. util
  • util.inspect(object[, options]) 传入JS原始值或对象,返回字符串形式的传入参数
  1. cookie
    // koa的ctx自带读取 和 写入 cookie的方法
  • ctx.cookies.get(name, [options]) 读取cookie
  • ctx.cookies.set(name, value, [options]) 写入cookie
ctx.cookies.set('name', 'ljm', {
    domain: 'localhost',  // 写cookie所在的域名
    path: '/index',       // 写cookie所在的路径
    maxAge: 10 * 60 * 1000, // cookie有效时长
    expires: new Date('2017-02-15'),  // cookie失效时间
    httpOnly: false,  // 是否只用于http请求中获取
    overwrite: false  // 是否允许重写
  }
)
  1. session
    // 遗憾的是跟post请求一样,koa并没有提供读取 和 写入session 的方法。只能通过自己实现或者通过第三方中间件实现:
  • koa-session-minimal 适用于koa2 的session中间件,提供存储介质的读写接口 。
  • koa-mysql-session 为存储介质koa-session-minimal中间件提供MySQL数据库的session数据读写操作。

0905

接下来要对路由进行设计。

结构

└── server # 后端代码目录
└── routers
├── admin.js # /admin/* 子路由
├── api.js # restful /api/* 子路由
├── error.js # /error/* 子路由
├── home.js # /home/子路由
├── work.js # /work/* 子路由
└── index.js # 子路由汇总文件 -- 会从这里引入所有子路由,最后再导出给app.js执行

restful API

以下先针对restful API的子路由进行概述。

const router = require('koa-router')
const userInfoController = require('./../controllers/user-info')

// 所谓的控制层就是当服务端接收用户请求时所要响应的操作
const routers = router.get('/user/getUserInfo.json', userInfoController.getLoginUserInfo)
                                    .post('/user/signIn.json', userInfoController.signIn)
                                   .post('/user/signUp.json', userInfoController.signUp)
module.exports = routers

汇总路由

// 汇总
const router = require('koa-router')

// 引入各部分子路由
const home = require('./home')
const api = require('./api')
const admin = require('./admin')
const work = require('./work')
const error = require('./error')

router.use('/', home.routes(), home.allowedMethods())
router.use('/api', api.routes(), api.allowedMethods())
router.use('/admin', admin.routes(), admin.allowedMethods())
router.use('/work', work.routes(), work.allowedMethods())
router.use('/error', error.routes(), error.allowedMethods())

module.exports = router

app.js中是如何调用

// ...
const routers = require('./routers/index')

// 初始化路由中间件
app.use(routers.routes()).use(routers.allowedMethods())

切图注意事项

PC

  1. 一定要注意PC端在不同分辨率下会存在样式问题,所以一定要用工程化的**去划分每个页面的所有部分。例如:psd看似是一屏过的页面,也要把它划分一头,一尾还有中间部分。

20170704

由于本次开发不注意,直接把这张psd一屏切成背景图,导致1280*720分辨率下样式变形,令到表格往下掉。。。
2. 尽量不要设置overflow-y:hidden;可能会由于分辨率引起无法往下滚动查看剩余内容。
3. 高度尽量通过内容来撑开,不要设死。
4. 如果依旧给某个元素设置了固定高度,并且有背景图的话,要考虑不同的分辨率会存在拉伸,导致可能会出现“白条”的缺点 --- 解决方法是给body加上背景色;优点是减少网络请求。

移动端

  1. 移动端可以看场合使用overflow-y:hidden; 但注意不同机型下是否有影响
  2. 移动端横轴使用百分比,纵轴使用rem或px。
  3. 高度尽量通过内容来撑开,不要设死。
  4. 如果依旧给某个元素设置了固定高度,并且有背景图的话,要考虑不同的机型会存在拉伸,导致可能iphone6的元素高度300px是显示正常的,但到了iphone5屏幕会收窄,背景有可能就会变成280px,而元素高度还是固定的300px,那么就会出现“白条”的缺点 --- 解决方法是给body加上背景色;优点是减少网络请求。
  5. 要是用了图片来代替背景占位的话,只需要给该图片设置固定宽高即可;缺点是会增加网络请求。
  6. 移动端如果想要背景图布满整个屏幕,用background-size: 100%;即可,一般background-size: 100% 100%;在正方形的素材才会用(使用后X,Y轴会明显拉伸),注意background-size: cover虽然也可以实现,但是前者在ipad的尺寸下也可以自然铺满,后者是不可以的。
  7. 如何切自带PS效果的图标做png(例如:发光、模糊)
  • 老步骤
  • 然后把所有无关的视图层勾去掉
  • 接着选中该两个图层右键转换为智能对象
    clipboard
  • 最后辅助线选项 => 视图 => 对齐到

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.