leungkaming / blog Goto Github PK
View Code? Open in Web Editor NEWDo a good habbit.
Do a good habbit.
在项目复杂的业务场景,有时候需要在前端跨域获取数据,这时候提供数据的服务就需要提供跨域请求的接口,通常是使用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')
})
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')
})
现在用到中间件有:
现在掌握了的API有:
现在掌握了的Node原生API有:
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 // 是否允许重写
}
)
为什么使用React-Redux?
定义这个参数用来监听Redux的store变化。
作用:顾名思义,把state完整copy一份到组件的props(map),返回为一个对象
// 将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 中的数据的基本方式。
定义这个参数用来分发action给Redux的reducer处理,生成新的state。
作用:顾名思义,有上述获取store的方法,自然就有更新store的方法。
// 组件通过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'));
特殊场景
参数:
// 举个例子
// 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并执行!
接着讨论connect的第三个参数。
接着讨论connect的第四个参数。如果指定这个参数,可以定制 connector 的行为。
由于本次开发不注意,直接把这张psd一屏切成背景图,导致1280*720分辨率下样式变形,令到表格往下掉。。。
2. 尽量不要设置overflow-y:hidden;可能会由于分辨率引起无法往下滚动查看剩余内容。
3. 高度尽量通过内容来撑开,不要设死。
4. 如果依旧给某个元素设置了固定高度,并且有背景图的话,要考虑不同的分辨率会存在拉伸,导致可能会出现“白条”的缺点 --- 解决方法是给body加上背景色;优点是减少网络请求。
结合 Redux中文实战文档去修改 #5 #4 #6 #8 的issue。
其实要说的是初始化数据流(store)。
window.location.href在微信浏览器上可能无法实现跳转,可以使用top,window.location处理。
下面先从服务端部分开始学习。
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}`)
})
使用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
回顾:上面2篇提到了用原生koa2实现路由,还提到了使用koa2的中间件---Koa-router实现路由。
这里要强调的是Koa2中的ctx只要打印就会发现:存在一个request字段和req字段,前者是经过context封装的请求对象,后者是经过context提供的Node.js原生HTTP请求对象;同理,存在一个response字段和res字段,前者是经过context封装的响应对象,后者是经过context提供的Node.js原生HTTP响应对象。
GET请求数据获取
// 因为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请求数据获取
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属性/方法
class SomeComponent extends React.Component {
render() {
return (
{/* 在反引号后面的字符串还是标点符号(包括空格)都会照旧在页面上展示出来 */}
<pre>{`
Hello ,
World .
`}</pre>
)
}
}
ReactDOM.render(
<SomeComponent />,
document.getElementById('content')
)
DOM上显示的效果是:
<pre>
Hello ,
World .
</pre>
今天想花一点时间总结下自己学习React的过程中,结合资料悟到的一些道理:
const Stateless = (props) => (
<div>{ this.props.text }</div>
)
<Stateless text="Hello World." />
graph LR
父组件State-->子组件Props
graph LR
父组件Methods-->子组件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}/>
);
}
}
// 伪代码
<div onClick={ this.changeState }>{ this.state.text }</div>
changeState () {
this.setState({
'text': 'Hello World.'
})
}
shoudComponentUpdate(nextProps, nextState) {
// 伪代码
return nextPropppps !== this.props;
}
通过设置类似上述合适条件,来防止绝大部分的无效重绘。
提到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)
用React + Redux + React-Route +Antd搭建了一个个人系统,暂时只有todoList的功能。
地址详见有道云笔记。
今天封装组件的时候遇到一个问题:就是明明外层组件已经给封装组件传值了,但是封装的组件好像没有接收到参数。
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>
接下来要对路由进行设计。
└── server # 后端代码目录
└── routers
├── admin.js # /admin/* 子路由
├── api.js # restful /api/* 子路由
├── error.js # /error/* 子路由
├── home.js # /home/子路由
├── work.js # /work/* 子路由
└── index.js # 子路由汇总文件 -- 会从这里引入所有子路由,最后再导出给app.js执行
以下先针对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
// ...
const routers = require('./routers/index')
// 初始化路由中间件
app.use(routers.routes()).use(routers.allowedMethods())
// 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等事件
需要设计一个前端由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/ # 前端源代码目录
停下来想想,发觉自己下班后的生活不是看动画片就是打游戏,好像也没有怎么把工作上或者学习上的一些事情给记录下来,本着这个出发点,也就有了以下的记录。
上周webpack3.0刚正式发布,老版本的 Webpack 需要==将每个模块包裹在单独的函数闭包中以实现模块系统==,而这些封装函数往往会使得浏览器中运行的 JavaScript 代码性能有所下降;而新版本的Webpack提供插件会将代码中所有的==模块作用域连接到单一闭包中==,从而保证了浏览器中的代码运行速度。
而 Webpack 3 中提供了如下的插件来允许开发者启用作用域提升特性来避免这种额外的性能损耗:
module.exports = {
plugins: [
new webpack.optimize.ModuleConcatenationPlugin()
]
};
由于在公司每个人都要有自己的OKR(大概就是一些季度计划),而自己总是每个季度都因为各种原因(其实是懒...)没有做到,所以这个季度(7~9月)的OKR打算认真做一做(稍作修改):
估时:
刚看到的一条评论,自我勉励下。
接触Vue也有一定时间了,到今天公众号推送的时候才发现有“Mixins”这个用法一直没用过,惭愧。。。TNT,于是赶紧用现有项目粗略试了一下。
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
// 不冲突时
//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!
// main.js 在入口文件
Vue.mixin({
mounted () {
console.log('global mixins mounted.')
}
})
// 某个组件
所有组件都会自动加上mounted方法
所以使用全局Mixins的时候需要认真考虑好场景。
之前学习React的时候跟着教程过了一下Redux,并没有花太多时间在它身上(加上脑容量只有512M,老是忘记),于是乎有了下面(重新学习)的系列(逃
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一样,永远只会有一个顶级组件,但却有无数的小组件。
整个应用的数据存储在这个唯一的对象中。
API:
// 使用Reducer初始化store【一般情况下】 => 如果出现多个Reducer的情况下需要用到combineReducer(合体!)
let store = createStore(reducer)
参考自http://www.jianshu.com/p/ba8654cb77b6
数据形式可以是数组、对象甚至是Immutable.js化的数据。
在书写代码前,最好规定好State的结构,怎么写?
// 一个应用程序可能有多个列表页的数据
{
homeList: [{}, {}],
paperList: [{}, {}]
// 其他的列表页数据
tagList: [{}, {}],
categoryList: [{}, {}]
}
{
// 文章相关的数据结构
artidles: {
// 首页列表
list: [{}, {}],
// Tag页列表
tagList: [{}, {}]
}
// 研究所相关的数据结构
papers: {
// 研究所列表页
list: [{}, {}]
}
}
根据以上Redux就应该创建两套数据:state.articles 和 state.papers。两套数据之间彼此独立,分别对应两套Reducer更便于维护。
// 承接上述横向改造后的数据
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中的值即可。
目的是:更灵活的对数据进行增删改查,并且避免冗余数据的问题。
参考自http://www.jianshu.com/p/ba8654cb77b6
Reducer是一个纯函数,根据传入的Action来改变对应Store里的State。核心:(oldState, action) => newState
// 伪代码
case 'INCREMENT':
return Object.assign({}, state, newState)
case 'DECREMENT':
return {...state, ...newState} // es6解构写法,把state和newState的键值都拷贝到一个对象上
default:
return state
// es6默认参数写法
function counter(state=0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
基于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)
higher-order reducer:高阶reducer,接受reducer作为参数的函数/返回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函数变体
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 // 永远给默认情况留值
}
// 如果有一份数据需要先由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
)
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点:
对最近几个项目做了总结,避免类似开发时再犯。
#7
在前面零散的Redux基础入门中,我们讨论的action都是同步调用的,每次action触发,state都会马上更新。下面讨论下异步调用action。
function add_todo (state) {
return {
type: 'ADD_TODO',
...state
}
}
// 另一种带上dispatch的写法:const add_todo = (state) => dispatch( { type: 'ADD_TODO', ...state } )
// actions.js
function requestPosts (state) {
return {
type: 'REQUEST_POSTS',
...state
}
}
function receivePosts (state) {
return {
type: 'RECEIVE_POSTS',
...state
}
}
// actions.js
function fetchPosts (state) {
return function (dispath) {
dispatch(requestPost( { name: 'ljm' } )); // 在thunk中调用同步action(变成thunk是由于返回的是一个函数)
dispatch(responsePost( { name: 'ljm' } )); // 同上
}
}
// 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的唯一方法。详情见:
9.25
// 该模块用于只取数组前几项,第一个参数是数组,第二个参数是选择第几个开始
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]
// 该模块用于只取数组后几项,第一个参数是数组,第二个参数是选择第几个开始
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]
// // 该模块用于使多层嵌套数组变为单层嵌套,参数为一个带着多层嵌套的数组
// flatten - 使...平坦
// implementation - 实现
var flatten = require('arr-flatten')
console.log(flatten(['a', ['b', ['c']], 'd', ['e']])) // [ 'a', 'b', 'c', 'd', 'e' ]
/* 该模块用于遍历数组每一项,并且每一项都调用传入的函数
* 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' ]
/* 原生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' ]
/* 该模块用于过滤数组,筛选条件就是根据返回的值 */
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']
// 该模块用于判断数组是否按照一定规则排序
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, 第二个参数为回调函数,返回一个倒序排列
/* 该模块用于对比参数之间的不同,默认是拿第一个跟第二个参数做对比,放在前面的做参照物
*/
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
// 该模块用于 将数组间多次引用的元素提取并去重,生成一个新数组
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. 跟原生数组的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']
// 该模块用于合并数组,等同于原生的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. 直接去重的话,生成新数组的同时会影响原数组
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']
// 该模块用于 将数组里面的每个带有某个属性的元素检索出来,用它们的值生成新的数组
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]
/* 该模块用于对比数组,如果数组之间存在相同的值则忽略,先来先到,最后返回一个合并后的新数组
*/
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]
// 该模块用于 将对象任意一项转换成数组,第一个参数是对象,第二个参数是回调函数,回调函数的第一个参数是每项对象的值,第二个参数是每项对象的键,第三个参数是整个对象
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']
// 该模块用于数组去重,生产的新数组并不会影响原数组
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)
// 该模块可以将给定范围的数字生成一个数组;只传一个参数,默认从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))
// 该模块用于根据指定参数生成数组;第一个参数为数字/字符串时,第二个参数表示循环次数;第一个参数为函数时,第二个参数表示循环次数
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
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
/* 该模块用于对 【文件流 / 图片流】 做类型校验
* 返回一个带着两个键值对的对象 => 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()
})
})
// 该模块用于 从文件内读取块 => 为什么不用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
关于最近发布的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>
在16.0.0没被 错误划分组件 所捕获的错误,将会导致整个React组件树的卸载。为什么会直接卸载而不是保留部分组件?
举例:像某些聊天产品,虽然应用崩溃了,但还是可以让用户发信息,这就可能导致用户发送了错误信息,所以显示部分UI远比不显示UI还要糟糕;而如果我们对某个对话框做了错误划分的话,那么依然可以通过UI引导用户去做相应操作。
前面介绍了服务器端相关配置,这里要介绍的是初始化数据库,也就是每个项目建立之前的必经之路。
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';
Redux architecture revolves around a strict unidirectional data flow.
官网原话:Redux架构是围绕严格的==单向数据流==。
数据流的生命周期遵循以下4点:
// 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)
一个http请求访问web服务静态资源,一般响应结果有三种情况:
// 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)
}
现在用到中间件有:
现在掌握了的API有:
现在掌握了的Node原生API有:
接下来的一个月要把心思都集中在学习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:能令普通函数转换成针对异步的函数,只要使用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.从上述案例可以得出:
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,我想把控制台中打印的贴出来:
{
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封装起来。里面主要的无非就是请求头的所有信息 和 响应头的所有信息,而服务端需要的也就只是这些而已。
简单点,说话的方式简单点。
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')
// action通常是一个对象,在异步时会是一个函数
{
type: 'userAction', // 肯定会有type属性
...state // 其他扩展属性;state是一个对象,这里用到es6解构语法,把key-value对拷贝到当前对象
}
在真实App,最聪明的做法就是给每个Action构建最好尽量带上index来标记。
{
type: 'userAction',
index: 5,
...state
}
传统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。
相关依赖
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
}
path.dirname(path) 获取当前路径的父级目录
util.inspect(object[, options]) 传入JS原始值或对象,返回字符串形式的传入参数
fs.mkdirSync 同步创建目录
通常初始化数据库要建立很多表,特别在项目开发的时候表的格式可能会频繁些变动,【这时候就必须把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脚本文件
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 }
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
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
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()
现在用到中间件有:
现在掌握了的API有:
现在掌握了的Node原生API有:
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 // 是否允许重写
}
)
上面简单学习过路由、请求数据获取还有静态资源加载相关后,接下来要聊聊cookie/session。
koa的ctx自带读取 和 写入 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')
遗憾的是跟post请求一样,koa并没有提供读取 和 写入session 的方法。只能通过自己实现或者通过第三方中间件实现:
// 示例代码等看到MySQL相关章节再补充
最近工作过程中,发现webpack打包出来的静态资源路径publicPath如果写死成http的话,那么网站切换成https就会导致资源无法访问。
那么是否存在一种办法,能让网站在http / https随意切换,也不影响静态资源的加载呢?
相对协议能完美解决这个问题。
// html
<img src="//domain.com/img/logo.png">
// 这个小技巧同样适用于 CSS :
.omg { background: url(//websbestgifs.net/kittyonadolphin.gif); }
从上一篇例子可以看出,如果一直依靠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.')
})
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.