Giter VIP home page Giter VIP logo

blog's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

blog's Issues

Electron 快速应用

最近使用 Electron 开发了一款知识管理客户端应用 - 布丁笔记,主要功能是用来管理自己的私人笔记和公开博客,支持一键上传 github,亦可同步 github issues

当然,关于布丁笔记如何实现的,我会在后续的文章中更新,今天主要是来聊一聊如何使用 Electron 快速开发一款客户端应用

介绍

正如我们使用的 web 技术一样,Electron 也是使用 JavascriptHTMLCSS 来快速简便地搭建一个跨平台的客户端应用

具体来说,Electron 是通过将 ChromiumNode.js 合并到同一个运行时环境中,并将其打包为 Mac,Windows 和 Linux 系统下的应用来实现这一目的的

所以,它可以利用 Chromium 的浏览器引擎来完成 UI 渲染,也可以利用 Node.js 实现服务端 JS 编程并可以直接操作文件和调用系统 API,甚至操作数据库,同时,还可以使用 Node 提供的 request 模块进行网络请求,无需考虑跨域

运行原理

Chromium

相当于 Chrome 的实验版,新功能会先在 Chromium 上验证,后续才应用在 Chrome 上

Node.js

Node.js 是非常强大的,既可以写后台的 CRUD,又可以做中间件,现在又可以写前端

Electron 中,主要运用其 PathfsCrypto 等模块,操作系统底层 API

系统API

内置原生应用程序接口,以支持 GUI,调用系统通知、打开系统文件夹等

主进程

Electron 有两种进程:主进程和渲染进程,两者之间可以进行进程间通信

主进程可以创建多个渲染进程;同时控制整个应用生命周期,包括启动、退出 APP 以及对 APP 做一些监听;主要用来处理原生应用逻辑,调用系统底层功能,可调用 Node API、Electron 主进程 API

一个 Electron 应用有且只有一个主进程,对应为 package.jsonmain 脚本的进程

渲染进程

渲染进程负责界面渲染,做一些界面交互,可以创建浏览器窗口,也可以调用 Node APIDOM APIElectron 渲染进程 API

不妨把渲染进程想像成一个浏览器窗口,它能存在多个并且相互独立,不同的是,它能调用 Node API

基础

生命周期

通过主进程的 app 模块控制整个应用的生命周期

比如当初始化时完成时触发 ready 事件

app.on('ready', () => {
  // 创建窗口、加载页面等操作
  createWindow()
})

当所有的窗口都关闭时会触发 window-all-closed 事件

app.on('window-all-closed', () => {
  if(process.platform !== 'darwin'){
      app.quit();     // 退出应用
  }
})

窗口

Electron 提供了一个 BrowserWindow 模块用于创建和控制浏览器窗口

通过关键字 new 实例化返回 win 对象

win = new BrowserWindow({
  width: 1100,         // 窗口宽度
  height: 800,        // 窗口高度
  minWidth: 900,
  minHeight: 600,
  title: '布丁笔记',
  fullscreen: false,  // 不允许全屏
  resizable: false    // 不允许改变窗口size,不然布局就乱了啊
})

窗口创建完是一片空白的,还需要通过 win.loadURL() 来加载要显示的页面

win.loadURL('app://./index.html')

shell 模块

使用系统默认应用管理文件和 URL,在主进程和渲染进程中都可以用到该模块

import { shell } from 'electron'

shell.moveItemToTrash(fullPath)   // 将文件删除至电脑回收站
shell.openExternal(`file://${postPath}.html`)    // 在默认浏览器中打开URL

打包应用

electron-builder 是基于 electron-packager 打包出来的程序再做安装处理,将项目打包成安装文件

npm install electron-builder -S    // 安装

electron-builder --win             // 打包

进程通信

渲染进程如果想进行原生的 GUI 操作,必须和主进程通讯,请求主进程来完成这些操作

在讲具体的通信过程前,我们先了解下 EventEmitter 类,它是 NodeJS 事件的基础,由 NodeJS 中的 events 模块导出

EventEmitter 的核心就是事件触发与事件监听器功能的封装。它实现了事件模型需要的接口,包括 addListenerremoveListener, emit 及其它工具方法. 同原生 JavaScript 事件类似, 采用了发布/订阅(观察者)的方式, 使用内部 _events 列表来记录注册的事件处理器

接下来提到的 ipcRendereripcMainonsend 进行监听和发送消息都是 EventEmitter 定义的相关接口

渲染进程向主进程通信

ipcRenderer 是一个 EventEmitter 的实例,可以使用其 sendsendSync 方法发送异步或同步的消息到主进程,同时可以接收主进程回复的消息

// 在渲染进程引入ipcRenderer
import { ipcRenderer } from 'electron'
ipcRenderer.send('sync-render', '我是来自渲染进程的异步消息')
const msg = ipcRenderer.sendSync('async-render', '我是来自渲染进程的同步消息')

注意:发送同步消息将会阻塞整个渲染进程,直到收到主进程的响应。所以我们在实际项目开发中应该尽可能的使用异步通信

ipcMain 模块也是 EventEmitter 类的一个实例,用于监听渲染进程传过来的消息并处理

ipcMain.on('sync-render', (event, data) => {
  console.log(data);
})

主进程向渲染进程通信

webContents 是一个事件发出者,它负责渲染并控制网页,也是 BrowserWindow 对象的属性

ipcMain 中的 event.sender,返回发送消息的 webContents 对象,所以包含着 send() 方法用于发送消息

ipcMain 接受消息的回调函数中,我们可以回应消息到渲染进程
使用 ipc 模块在进程之间发送异步消息时

ipcMain.on('sync-render', (event, data) => {
  console.log(data);
  event.sender.send('main-reply', '主进程收到了渲染进程的【异步】消息!')
})

渲染进程通过 ipcRenderer.on 监听

ipcRenderer.on('main-reply', (event, msg) => {
  console.log(msg);
})

使用 ipc 模块在进程之间发送同步消息时

ipcMain.on('async-render', function (event, arg) {
  event.returnValue = '主线程收到了渲染进程的同步消息!'
})

渲染进程间通信

Electron 并没有提供渲染进程之间相互通信的方式,我们可以在主进程中建立一个消息中转站

渲染进程之间通信首先发送消息到主进程,主进程的中转站接收到消息后根据条件进行分发

remote

remote 模块为渲染进程和主进程通信提供了一种简单方法

Electron 中,有许多模块只存在主进程中,想要调用这些模块的方法需要通过 ipc 模块向主进程发送消息,让主进程调用这些方法,而使用 remote 模块,可以在渲染进程中调用这些只存在于主进程对象的方法

import { remote } from 'electron'

const BrowserWindow = remote.BrowserWindow      // 访问主进程中的BrowserWindow模块

let win = new BrowserWindow()                  // 其他的跟主进程的操作都一样

win.setRepresentedFilename(this.post.localPath)
win.setDocumentEdited(true)
win.setTitle('布丁笔记')

数据持久存储

数据持久化有很多种方案,因为是前端浏览器,我们可以选择 localStorage, CookieindexDB 等等

考虑可靠性,持久化以及存储空间,还可以通过 Electron 写文件的方式,把数据写入到应用路径下

这样即使 app 被卸载了,只要数据没被清空,用户数据还在

通过 Electron app getPath 可以获得应用存储路径

import { app } from 'electron'
app.getPath('userData')

而对于文件是否写入的判断,以及进程间的数据共享,这里我推荐使用开源库 lowdb,以 key-value 的格式存储 json 文件

lowdb 是一个由 Lodash 支持的小型本地 JSON 数据库(支持 NodeElectron 和浏览器)

image

总结

  • Electron = Node.js + 谷歌浏览器 + 平常的 JS 代码生成的应用,最终打包成安装包,就是一个完整的客户端应用
  • Electron 分两个进程,主进程负责原生应用逻辑的创建以及底层功能的调用,渲染进程负责 UI 界面展示
  • 两个进程间是通过 iPCRenderiPCMain 方法,以及 remote 模块通信,前者是基于发布订阅机制,自定义事件的监听和触发进程间通信;后者类似于挂载在全局的属性上进行通信
  • Electron 相当于给 React 或者 Vue 生成的单页面应用套了一层壳,如果涉及到文件操作这类的复杂功能,就要依靠 Electron 的主进程,因为主进程可以直接调用 Node.jsAPI,还可以调用系统底层功能

参考

moment.js的简单应用

前言

最近做的前端项目遇到了处理日期时间的问题,要对数据库中的时间进行处理,由于原生的日期格式处理起来很繁琐,所以选用momentjs来对日期进行一些常规的处理

跟原生js为了完成一个简单的日期时间显示效果而要写一大堆处理函数相比,momentjs算是极其轻量级的操作时间的工具库,支持字符串、Date、时间戳以及数组等格式,它使得操作时间变得非常简单

使用心得

momentjs支持多个环境,可以在浏览器中引入momentjs文件即可

<script src="https://cdn.bootcss.com/moment.js/2.9.0/moment.js"></script>

也可以脱离浏览器的环境在nodejs中使用,但需要安装moment模块

npm install moment

然后加载模块即可

var moment = require('moment')

实例

创建

获得当前时间

moment() // 当前时间

相当于moment(new Date()) 此处会返回一个moment封装的日期对象

image

初始化一个指定的时间

moment('2018-01-21') // 2018-01-21 
moment(Date.now() - 24 * 60 * 60 * 1000) // 昨天
moment(new Date(2018, 01, 22)) // 2018-01-22
moment({year:2015,month:10,day:26,hour:22}) // 传入一个具有日期字段的对象

格式化

moment().format('YYYY年MM月DD日 HH:mm:ss') // 2018年01月22日 18:01:41
moment().format('YYYY-MM-DD HH:mm:ss') // 2018-01-22 18:02:35
moment().format('YYYY/MM/DD HH:mm:ss') // 2018/01/22 18:03:29
moment().format('hh:m:ss') // 06:2:32
moment().format('YYYY') // "2018"
moment().format('d') // 今天是星期几
moment().format('X') // 转换当前时间的Unix时间戳

转换为Date对象

moment().toDate() // Mon Jan 22 2018 18:11:55 GMT+0800 (**标准时间)
moment('2018-01-20').toDate() // Tue Jan 20 2015 00:00:00 GMT+0800 (**标准时间)
moment('2018-01-22 10:20:15').toDate() // Mon Jan 22 2018 10:20:15 GMT+0800 (**标准时间)
moment(1448896064621).toDate() //毫秒转日期

获取/设置时间信息

moment().second() // 秒
moment().second(Number) // 设置 秒 0 到 59

类似还可以设置分获得分设置小时获得小时

moment().date() // 日
moment().day() // 星期几
moment().dayOfYear() // 一年里的第几天
moment().week() // 一年里的第几周
moment().month() // 月-1
moment().quarter() // 一年里的第几个季度
moment().year() // 年
moment().daysInMonth() // 当前月有多少天

操作

moment('20160901', 'YYYYMMDD').fromNow() // 1 years ago

时间加减

moment().add(7, 'days') // 当前时间加上7天 第2个参数还可以是 'months', 'years' 等
moment().add(7, 'd') // 与上面一行代码的运行结果一样
moment().subtract(1, 'months') // 当前的月份减去1个月

moment().startOf('week') // 本周第一天
moment().startOf('hour') // 等效于moment().minutes(0).seconds(0).milliseconds(0)

开始结束时间

moment().endOf('year') // 将时间设置为今年的最后时刻
moment().startOf('month') // 将时间设置为这个月的开始
moment().endOf('week') // 将时间设置为本周的最后时刻

比较传入日期的最大值最小值

a = moment().add(7, 'days')
b = moment().subtract(1, 'days')
moment.max(a, b)
moment.min(a, b)

时间差(默认返回相差的毫秒数)

a = moment().add(7, 'days')
b = moment().subtract(1, 'days')
a.diff(b) // 86400000
a.diff(b, 'days') // 1 返回相差的天数

查询

早于

moment('2018-01-22').isBefore('2018-01-21') // true
moment('2018-01-22').isBefore('2018-02-23', 'year') // false
moment('2018-01-22').isBefore('2019-01-01', 'year') // true

是否相等

moment('2018-01-22').isSame('2018-01-22') // true
moment('2018-01-22').isSame('2017-12-31', 'year') // false
moment('2018-01-22').isSame('2018-01-01', 'year') // true

晚于

moment('2018-01-22').isAfter('2010-10-21') // true
moment('2018-01-22').isAfter('2018-01-01', 'year') // false
moment('2018-01-22').isAfter('2017-12-31', 'year') // true

是否在时间范围内

moment('2018-01-22').isBetween('2018-01-01', '2018-10-25') // true
moment('2018-01-22').isBetween('2017-01-01', '2018-01-28', 'year') // false
moment('2018-01-22').isBetween('2017-12-31', '2019-01-01', 'year') // true

moment().isLeapYear() // 是否是闰年

官网地址

移动端自适应rem的问题

前言

我们一般写移动端页面时,为了让页面适应各个尺寸,常常会给各个元素设一个相当单位

一般常用的css单位有px,em,rem

px: 多用于pc端,精准的描述元素大小,不随屏幕大小变化而变化,也可以说是一个绝对单位

em: 相对于父元素的大小变化而变化,在嵌套层次多的情况下会导致元素大小值偏大或偏小

rem: 全名也叫font size of the root element ,即相对于根元素的大小,常用来表示移动端元素大小

最近在开发移动端页面,针对rem谈谈其心得体会

rem使用方法

首先,在开发页面之前,我们需要给根元素赋一个font-size值,也就是rem基准值

<html  style="font-size: 37.5px;">...</html>

浏览器默认的根元素font-size值是16px

rem基准值计算

为了方便计算,我们可以设置一个按屏幕大小整数倍缩放的值,比如10倍

所以,对于iphone6的屏幕宽度375px,那么我们就可以将根元素的font-size设置为37.5px,缩小10个倍数

同样的,对于iPhone5屏幕的宽度320px,缩小10倍根元素的font-size就是32px

那缩放10倍是怎么定义的呢,其实这个值是随便定义的,假如不除以10,如果是除以100,这样在设置html的font-size时候会偏小,只有3.75px或者是3.2px等等,而

浏览器的font-size如果小于12px就显示不出效果了

假设根元素的字体设置的大小是37.5px(按iPhone6屏幕尺寸缩小10倍),设计稿的宽度是按iPhone6的屏幕宽度375px定义的,设计稿中p的大小是12px,那么p应该设置的相对字体大小是(12/37.5)rem

如果设计稿的宽度是750px,设计稿中p的大小是12px,那么p应该设置的相对字体大小是(12/75)rem

动态设置html的font-size

为了将各个不同屏幕宽度根元素的font-size都缩小到屏幕宽度的10倍,可以在js中动态计算出当前屏幕所适配的font-size的大小

document.getElementsByTagName('html')[0].style.fontSize = window.innerWidth / 10 + 'px';

document.documentElement.style.fontSize = innerWidth/10 + "px"

或者用媒体查询设置

@media (min-device-width : 375px) and (max-device-width : 667px) and (-webkit-min-device-pixel-ratio : 2){
      html{font-size: 37.5px;}
}

rem数值计算

考虑到元素很多很难换算的情况,我们可以用css构建工具scss

@function px2rem($px){
    $rem : 37.5px;
    @return ($px/$rem) + rem;
}

当我们写具体的数值时,就可以写成

height: px2rem(90px);
width: px2rem(90px);;

参考资料

运用UglifyJS压缩JS

初识UglifyJS

JS代码压缩在前端项目优化中是用的比较多的方法,特别是对于大量使用JS的Web应用,代码压缩可以是极大的减少代码的大小,加快传输效率,大大提升用户浏览的体验感

最近刚好学习了压缩JS文件的方法--UglifyJS,据说其不仅能压缩JS代码,还能优化JS,是基于node的

而且在所有的压缩工具中,UglifyJS在不改变JS语义的前提下可以提供最好的压缩率

只需要安装node,然后使用npm安装UglifyJS类库

在线压缩工具

下面我们可以看看具体的实例操作

全局安装

npm install uglify-js -g

命令行简单压缩JS文件

首先创建一个文件夹,将要压缩的js文件放入文件夹中,然后压缩

如下,我在demo_uglify项目文件夹中的src文件下压缩compile.js文件,将压缩文件存放到build文件夹下

uglifyjs src/compile.js -o build/compile-min.js

再运行如下代码,测试-m可选参数

uglifyjs src/compile.js -m -o  build/compile.min.js

如下截图

image

image

38k的是没有运行-m参数的,29k的是运行的

-m参数就是把变量名变成a,b,c,...

node实现批量压缩js函数

上面是使用的一个简单命令来压缩一个JS文件,接下来以编程的方式来压缩JS文件

即写一个.js文件,使用node命令执行该文件,.js中的代码需要调用UglifyJS的接口函数来执行压缩任务

所以需要先到github下载UglifyJS

 git clone git://github.com/mishoo/UglifyJS.git

解压后其目录结构如下

image

然后将uglify-js.jslib目录拷贝到前面创建的项目中

image

控制台输出压缩后代码

demo_uglifysrc文件中新建一个index.js,内容如下

取fs模块,fs模块是node的文件模块;接着取UglifyJS的两个模块;然后就是UglifyJS压缩流程

function buildOne (fileIn, fileOut) {
  var origCode = fs.readFileSync(fileIn, 'utf8')
  var ast = jsp.parse(origCode)  
  ast = pro.ast_mangle(ast)  
  ast = pro.ast_squeeze(ast)  

  var finalCode = pro.gen_code(ast)

  fs.writeFileSync(fileOut, finalCode, 'utf8')
}

buildOne('src/index.js', 'build/index-min.js')

最后在命令行中执行

node index.js

image

单个文件读取压缩输出

通过在上述测试,通过node环境可以输出压缩后的代码,那也可以通过函数直接读取源文件,压缩后输出到指定的目录

demo_uglify的src文件中新建一个test.js,写入下列代码,将src下的index.js压缩输出到build文件中

函数封装

function buildOne (fileIn, fileOut) {
  var origCode = fs.readFileSync(fileIn, 'utf8')
  var ast = jsp.parse(origCode)  
  ast = pro.ast_mangle(ast)  
  ast = pro.ast_squeeze(ast)  

  var finalCode = pro.gen_code(ast)

  fs.writeFileSync(fileOut, finalCode, 'utf8')
}

buildOne('src/index.js', 'build/index-min.js')
node test.js

这时就会在demo_uglifybuild目录中生成一个index-min.js

批量读取文件压缩输出

在上面单个文件读取压缩的代码中稍做修改,如下

function buildAll (fileIn, fileOut) {
    if (fileIn.length > 0) {
        var finalCode = []
        var origCode = ''
        var ast = ''
        
        for (var i = 0, len = fileIn.length; i < len; i++) {
            origCode = fs.readFileSync(fileIn[i], 'utf8')
            ast = jsp.parse(origCode)
            ast = pro.ast_mangle(ast)
            ast = pro.ast_squeeze(ast)

            finalCode.push(pro.gen_code(ast), ';')
        }
    }
    fs.writeFileSync(fileOut, finalCode, 'utf8')
}

buildAll(['index.js', 'compile.js'], 'build.min.js')
node test.js

这时会在demo_uglify目录中生成一个build.min.js

UglifyJS2

UglifyJS2是UglifyJS的重写,我们知道UglifyJS是JS开发通用的语法分析、代码压缩、代码优化的一个工具包。而对比UglifyJS,UglifyJS2则是把整个JS压缩过程,做了更进一步的细化

安装

npm install uglify-js -g

或者通过github下载源代码安装

git clone git://github.com/mishoo/UglifyJS2.git

命令操作

用UglifyJS2命令进行操作,合并两个文件,对变量名用字母替换,进行压缩,所有代码合并到一个函数

image

uglify_demo完整项目

React 的动画过渡库 - React-Transition-group

在开发中实现动画的方法有很多,不管是 react 还是 vue 都有开源的动画组件库来更加方便的动画实现效果

react 动画组件库是 React-Transition-group,在说这个组件库之前,我们先来看下借助 CSS3 实现最基本的动画样式

CSS3 实现动画样式

constructor(props) {
  super(props)
  this.state = {
    show: true
  }
  this.handleToggle = this.handleToggle.bind(this)
}

render() {
  return (
    <Fragment>
      <div className={this.state.show ? 'show' : 'hide'}>hello</div>
      <button onClick={this.handleToggle}>toggle</button>
    </Fragment>
  );
}

handleToggle() {
  this.setState({
    show: this.state.show ? false : true
  })
}
.show {
  opacity: 1;
  transition: all 1s ease-in;
}

.hide {
  opacity: 0;
  transition: all 1s ease-in;
}

这样在浏览器中通过控制 toggle,就可以看到 hello 若隐若现了,就借助了 CSS3 实现了最基本的动画样式

使用 react-transition-group 实现动画

在 github 中搜索 react-transition-group ,可以看到 star 最多的项目,就是 react-transition-group

先安装依赖 npm install react-transition-group --save

然后在项目中引入 import { CSSTransition } from 'react-transition-group'

可以看到官网的demo

classNames="fade" applies fade-enter, fade-enter-active, fade-enter-done, fade-exit, fade-exit-active, fade-exit-done, fade-appear, and fade-appear-active.

Transition

过渡组件 ( Transiton ) 允许您用一个简单的声明性 API 描述随着时间的推移从一个组件状态到另一个组件状态的转换

默认展示组件某个特定状态的样式,而不是创建渐变动画

<Transition
  in={this.state.show}
  timeout={1000}
  // unmountOnExit
>
  {state=>{
    if(state === 'entering' || state === 'entered')
      return <div className="on">{state}</div>
    else 
      return <div className="off">{state}</div>
  }}
</Transition>
<button onClick={this.handleAddItem}>toggle {`${this.state.show}`}</button>

Transition 中间传入一个函数,也就是属性 children ,并获得一个参数 state,

state 包含了内部组件的 transition 状态,分别有

  • entering
  • entered
  • exiting
  • exited
  • unmounted

上述 🌰 的意思可以理解为

你设置的时间是 1000ms,当 in 从 false 变成 true 的时候, 显示 <div className="on">{state}</div>enteringentered 的状态 ), 相反的,当 in 从 true 变成 false 的时候,1000ms 之后, 切换到 <div className="off">{state}</div>exitingexited 的状态 )

运行原理

从头来看,它其实就是一个状态机,跟动画没什么关系,所以它的名字也是叫 Transition

有点像路由,在不同的组件中选择一个渲染,唯一的区别是,它只有4个路由选项:

进入 ( in === true )时,url 是 entering,1000ms 后,url 变成 entered

退出 ( in ===false ) 时,url 是 exiting,1000ms 后,url 变成 exited

然后加上 css ease-in-out 就成了动画

CSSTransition

展示组件从状态到另一个状态的动态变化,需要定义 className 和相关样式

最常用的是用来动画一个组件的安装和卸载,但也可以用来描述在适当的过渡状态

可以将我们之前用 CSS3 写的样式修改为

render() {
  return (
    <Fragment>
      <CSSTransition
        in={this.state.show}
        timeout={1000}
        // 前缀名注意S
        classNames='fade'
      >
        <div>hello</div>
      </CSSTransition>
      <button onClick={this.handleToggle}>toggle</button>  
    </Fragment>
  )
}

handleToggle() {
  this.setState({
    show: this.state.show ? false : true
  })
}
.fade-enter {
  opacity: 0;
}

.fade-enter-active {
  opacity: 1;
  transition: opacity 1s ease-in;
}

/* 入场动画执行完毕后,保持状态 */
.fade-enter-done {
  opacity: 1;
}

.fade-exit {
  opacity: 1;
}

.fade-exit-active {
  opacity: 0;
  transition: opacity 1s ease-in;
}

.fade-exit-done {
  opacity: 0;
}

这样就可以实现和之前相同的动画效果了

咋一看虽然稍微复杂了点,但是它可以带给我们很多新的特效

比如参数 intrue or false,代表了是’淡入’状态,还是’淡出’状态

timeout 代表了整个的持续时间

unmountOnExit 属性,添加到代码中,会发现当我们点隐藏的时候,对应的 DOM 被移出了,点显示的时候,DOM 又出来了

借助 react-transition-group 这个库,实现起来非常简单

继续看它的文档,这个库提供了很多钩子函数

image

假设当这个 hello 显示出来之后,希望它的颜色能变成红色,现在实现就变得很简单了

只需要在入场动画结束之后,将 color 变成 red

.fade-enter-done {
  opacity: 1;
  color: red
}

还可以用 js 的方式来实现,怎么做呢?

CSSTransition 组件中添加一个钩子onEntered

<CSSTransition
  in={this.state.show}
  timeout={1000}
  classNames='fade'
  unmountOnExit
  onEntered={(el) => {el.style.color='blue'}}
>
  <div>hello</div>
</CSSTransition>

钩子和生命周期函数是一个东西,就是在某个时刻会自动执行的函数

onEntered 钩子什么时候会自动执行呢?就是当入场动画结束之后,就会被执行

el 就是指的内部的 div 元素

如果希望第一次展示的时候也有动画效果,应该怎么办呢?

同样也需要在 CSSTransition 组件中添加一个 appear={true} ,同时在入场动画的第一帧添加 fade-appear ,同时在入场动画的第二帧以及整个过程中添加 fade-appear-active

/* enter是入场前的刹那(点击按钮),appear指页面第一次加载前的一刹那(自动) */
.fade-enter, .fade-appear {
  opacity: 0;
}

/* enter-active指入场后到入场结束的过程,appear-active则是页面第一次加载自动执行 */
.fade-enter-active, .fade-appear-active {
  opacity: 1;
  transition: opacity 1s ease-in;
}

这些也就是 react-transition-group 这个库比较核心的内容,其他更复杂的方法可以查阅 Transition

TransitionGroup

如果要做多个元素的动画切换呢?

这个时候就要用到 TransitionGroup

TransitionGroup 实际上就是实现多个Transition 或者CSSTransition组合的效果

用来管理一些列组件的动画,例如列表

首先引入这个组件 import TransitionGroup from 'react-transition-group'

constructor(props) {
  super(props)
  this.state = {
    data: []
  }
  this.handleAddItem = this.handleAddItem.bind(this)
}

render() {
  return (
    <Fragment>
      <TransitionGroup>
      {
        this.state.data.map((item, index) => {
          return (
            <CSSTransition
              timeout={1000}
              classNames='fade'
              unmountOnExit
              onEntered={(el) => {el.style.color='blue'}}
              appear={true}
              key={index}
            >
              <div>{item}</div>
            </CSSTransition>
          )
        })
      }
      </TransitionGroup>
        <button onClick={this.handleAddItem}>toggle</button>
    </Fragment>
  )
}

handleAddItem() {
  this.setState((prevState) => {
    return {
      data: [...prevState.data, 'item']
    }
  })
}

这样配合 TransitionGroupCSSTransition 就可以进行多个元素或者组件切换这样的动画效果了

完整代码

transition 动画

通俗地解析background属性

我们在做项目的时候,通常会遇到设置背景图与屏幕大小相适应问题

而background中一些属性的取值是可以直接影响到背景图的呈现效果,包括CSS3当中新增的几个新的属性值:background-size,background-origin,background-clip,还有CSS1当中的background-position

之前在项目中达不到想要的背景图效果,总是将各种取值都试一遍,现在认真解析了一遍,每个值都要特点的效果,本篇文章主要是对上述几个属性做一下通俗易懂的解释

background-size

取值

background-size: auto || <length> || <percentage> || cover || contain

auto: 默认值,保持背景图原有高度和宽度

length: 具体像素值,改变背景图大小

percentage:百分值(0%〜100%),作用于块元素,背景图大小取决于所在块元素的宽度百分比

cover: 背景图放大到铺满整个容器,适应于图片尺寸小于元素容器

container:背景图缩小到铺满整个容器,适应于图片尺寸大于元素容器

当取值为length或percentage,可以设置一个值 第二个值相当于auto auto的值与第一个值相同

DEMO结构和效果

HTML Code

<div class = "test"></div>

先加上初步的效果

.test {
     background: url("./images/background_image.jpg") no-repeat;
     width: 800px;
     height: 450px;
     line-height: 450px;
     border: 1px solid #999999;
     margin: 30px;
 }

找了张(380px*300px)左右的图片来当作背景图片使用

image

background-size:auto

在上述.test{...}中增加一行

background-size:auto;

效果

image

效果等同于没加background-size效果一样

background-size:'length'

同样的增加一行,如

background-size: 550px 300px;

效果

image

从上张取值的效果图可以看出来,背景图片由(380px300px)变为(550px300px),已经变形失真

如果只取了一个值,比如background-size:350px ,相当于background-size:350px auto,此时auto的取值为350px/380px*300px

background-size:'percentage'

同样的增加一行,如

background-size:80% 50%;

效果

image

从这张效果图我们可以看出来,背景图大小并不是按背景图的百分比来缩放的,而是按装载背景图的容器元素的百分比计算的,也就是长800px(div.width)*80%,高450px(div.height)*50%

如果只有一个值时,比如background-size:50%,相当于background-size:50% auto,相当于background-size: 50%*800px(div.width) 50%*800px(div.width)/380px(背景图长度)*300px(背景图高度)

上述两种取值也可以搭配使用,如

background-size:47.5% 300px;

background-size:47.5% 300px;的取值实际上就是图片自身的大小(380px*300px)了,效果也等同于一个值background-size:47.5%;

效果

image

background-size: cover

同样的增加一行,如

background-size:cover;

效果

image

从上述效果图可以看到,背景图会放大到适合容器元素的尺寸,原则是背景图的width和height都需要等比例放大到填满容器,以至背景图在装载它的容器元素中占比大的一方向可能超出容器,比如上图中背景图的height明显超出了容器。和上一种取值background-size:100%的效果一样

background-size:contain

上面的cover取值是把背景图片放大到适合容器元素的尺寸,这时的contain刚好是跟cover相反,是把背景图片缩小到适合容器元素的尺寸

这里再重新定义一下容器元素的宽高

width:200px;
height:200px;
background-size:contain;

效果

image

前面已经介绍了背景图片的大小是380px300px,而现在将容器元素改为200px200px

从效果图中我们可以看到背景图等比例的缩小到适合元素容器的尺寸,以至背景图在超出它的容器元素较少的一方向可能与容器有间隙,比如上图中背景图的height超出了100px,width超出了180px,所以等比缩小后height与容器间多出了空隙

从上边的几个效果值可以看出来,cover、contain的值、或者只设置一个值,另一个值为auto时,都不会出现失真的情况,但会出现图片显示不全或者与容器元素出现留白的情况

所以我们在项目开发中合理的定义容器元素的尺寸大小,根据容器尺寸大小和背景图大小合理的选用background-size的取值

background-position

图片定位的问题,比较容易理解,但也有需要注意的点。接下来还是从取值讲起

取值

background-position:  <关键词> || <百分比> || <像素值> 

关键词:类似于background-position:top left

像素:类似于background-position: 0px 0px

百分比:类似于background-position: 0% 0%

DEMO结构和效果

上面前两种定位都是

将背景图左上角的原点放置到属性值位置

同样,在上述.test{...}中增加一行background-position: 50px 50px;

.test {
     background: url("./images/background_image.jpg") no-repeat;
     width: 800px;
     height: 450px;
     line-height: 450px;
     border: 1px solid #999999;
     margin: 30px;
     background-position: 50px 50px;
 }

background-position: 50px 50px

image

可以看到,规定的位置是"50px 50px",也就是图片的左上角的原点在那个位置上

同理,当取值background-position: top left ;

background-position: top left

image

但是第三种取值方法 百分比定位 就不是将背景图左上角的原点放置到属性值位置了,接下来我们设置background-position: 10% 10%;

background-position: x% y%

image

我们先将看百分比定位转换为像素值定位,那width和height的10%,转换为像素值就是

background-position: 80px 45px;

效果

image

对比这两张图,是不是效果很明显不一样,说明百分比定位有自己特有的定位原则

图片本身(x%,y%)的那个点,与背景区域的(x%,y%)的那个点重合

那有人好奇既然这样就直接用像素值定位好了,其实并不是

使用百分比设置的主要优势在于,当页面缩放的时候,背景图片也会跟着一起缩放

background-origin

上面讲到的background-position规定背景显示的位置,但是位置总要有个标准来确定吧,这个标准呢,就是由background-origin这个属性设置的

同样的,我们先来看标准的取值

取值

background-origin: padding-box || border-box || content-box;

padding-box:默认值,背景图相对于内边距框来定位(包括padding)

border-box: 背景图相对于边框盒来定位(包括padding+border)

content-box: 背景图相对于内容框来定位(只包括内容width/height里边的内容)

DEMO结构和效果

我们来看下实际的效果

DEMO

重新修改上述test元素样式

.test {
     background: url("./images/background_image.jpg") no-repeat;
     width: 400px;
     height: 400px;
     line-height: 400px;
     border: 10px dotted #999999;
     margin: 30px;
     background-size:100%;        
}

效果

image

上述是 background-origin的默认样式效果

background-origin: padding-box

在上述.test(...)中增加一行background-origin: padding-box;

image

可以看到跟默认效果一样,只包含了padding的宽度

background-origin: border-box

同样的将background-origin的属性值改为border-box

image

可以看到是包含了boder和padding的宽度的

background-origin: content-box

同样的将background-origin的属性值改为content-box

image

可以看到boder和padding都留白出来了

每种取值的图片效果都不一样,所以具体怎么用,还是得看项目情况了

先写这么多吧,background-clip这个属性目前用的还是不太多,就不赘述了~~~

react实现表单事件

相信使用过React.js的小伙伴都应该知道其数据双向绑定的灵活性,在处理表单,人机交互方面都具能发挥极大的优势,而且实现起来也比较方便

最近在项目中刚好用到了这系列操作,下面以类似于单选框、复选框和下拉框为例介绍他们在ant-design-pros中的具体实现方式

单选事件

在项目中我们经常会遇到类似单选事件的小功能,比如点击或悬浮的元素高亮,页面上有很多的item元素,要实现点击或者悬浮到哪个就哪个高亮

用jq实现的话,就是选中的元素给加个addClass的事件,添加active的样式,然后它的兄弟元素removeClass,去掉active样式

同样的,用react实现,我们可以用相同的思路,比如用一个currentIndex,通过它来判断是在哪个元素实现切换的,而currentIndex的值又可以通过选中元素的某个元素属性来动态改变

效果图

image

代码

class Radio extends PureComponent {
  state= {
    currentIndex: null,
    value: null
  }

  setCurrentIndex = (e) => {
    this.setState({
      currentIndex: parseInt(e.currentTarget.getAttribute('index'), 10)
    })
  }

  handleChange = (e) => {
    this.setState({
      value: e.target.value
    })
  }

  render () {
    const arr = ['京东超市', '天猫超市', '京东生鲜', '京东到家', '果蔬好', '盒马生鲜', '无人超市', '每日生鲜']
    const itemList = arr.map((item, index) => {
      return (
        <li 
          key={index} 
          className={`${styles.item} ${this.state.currentIndex === index ? styles.active : ''}`} 
          index={index} 
          onClick={this.setCurrentIndex}
        >
          {item}
        </li>
      )
    })
    return (
      <Card title="单选列表" bordered={false} className={styles.main}>
        <div className={styles.content}>
          <div className={styles.top}>
            <div className={styles.name}>点击或悬浮选中的li高亮:</div>
            <ul className={styles.list}>{itemList}</ul>
          </div>
          <div className={styles.bottom}>
            <div className={styles.name}>您的性别为:</div>
            <div className={styles.list}>
              <label className={styles.item}><input type="radio" name='gender' value="男生" onChange={this.handleChange}/>男生</label>
              <label className={styles.item}><input type="radio" name='gender' value="女生" onChange={this.handleChange}/>女生</label>
            </div>
            <div className={styles.sex}>性别: {this.state.value}</div>
          </div>
        </div>     
      </Card>
    )
  }
}

总的来说,就是生成这些li的时候给元素添加一个index标志位,然后通过相应的点击事件,把这个index用e.currentTarget.getAttribute('index')取出来,然后去设置currentTarget的值,再通过赋给选中的css的active样式就可以了

复选事件

在页面中,有时候会根据用户的一些操作更新属性值,但不同的对象间的操作就涉及到对多个值的状态管理,这些可变的状态通常都保存在组件的状态属性中,并且只能用setState()方法更新

复选事件和单选事件类似,也需要通过监听各个标签元素的点击或者悬浮等事件来实现

效果图

image

代码

class Checkbox extends PureComponent {
  state = {
    fruit: [],
    value: []
  }

  handleChange  = (e) => {
      let item = e.target.value
      let items = this.state.fruit.slice()
      let index = items.indexOf(item)
      index === -1 ? items.push(item) : items.splice(index, 1)
      this.setState({fruit: items})
  }

  onclickIcon = (e) => {
    let item = parseInt(e.currentTarget.getAttribute('index'), 10)
    let items = this.state.value.slice()

    let index = items.indexOf(item)
    index === -1 ? items.push(item) : items.splice(index,1)
    this.setState({
      value: items
    })
  }

  render() {
    const listArr = ['羽绒服', '裙子', '帽子', '围巾']
    return (
      <Card title="多选列表" bordered={false} className={styles.main}>
        <div className={styles.content}>
          <div className={styles.left}>
            <div className={styles.title}>Choose fruit : </div>
            <div className={styles.list}>
              <label className={styles.item}><input type="checkbox" name="fruit" value="apple"
                            onChange={this.handleChange}/>apple</label>
              <label className={styles.item}><input type="checkbox" name="fruit" value="banana"
                            onChange={this.handleChange}/>banana</label>
              <label className={styles.item}><input type="checkbox" name="fruit" value="pear"
                            onChange={this.handleChange}/>pear</label>
            </div>
            <div>Choosen : {this.state.fruit.join('-')}</div>
          </div> 
          <div className={styles.right}>    
            <div className={styles.tag}>点亮我的喜欢:</div>
            <ul className={styles.card}>
            {
              listArr.map((item,index) => {
                return (
                  <li 
                    key={index} 
                    className={styles.item} 
                    index={index} 
                    onClick={this.onclickIcon}
                  >
                    <svg 
                      className={`${styles.svg} ${this.state.value.indexOf(index) === -1? styles.icon : styles.iconColor}`} aria-hidden="true">
                      <use xlinkHref={this.state.value.indexOf(index) === -1? "#icon-lovetaoxin" : "#icon-shixintaoxin"}></use>
                    </svg>
                    <span to='#' target="_blank" className={styles.text}>{item}</span>
                  </li>
                )
              })
            }
            </ul> 
          </div> 
        </div>           
      </Card>
    )
  }
}

总的来说,我们通过在组件状态state中定义数组,存放选中的信息,主要是通过先在li元素中添加index标志位,然后通过相应事件的触发,把这个index用e.currentTarget.getAttribute('index')取出来,存放到state中定义的数组中

需要注意的是,在更新value时,必须使用setState函数,否则代码不会被重新渲染,在return中显示已选中的选项不会实时更新

下拉事件

下拉的实现方式和单选、复选有些类似,而且在ant-design中也有下拉Dropdown的组件,可以复用加以修改,可以直接看例子说明

效果图

image

代码

class NewDropdown extends PureComponent {
  state = {
    value: 'basketball'
  }

  handleChange = (e) => {
    this.setState({
      value: e.target.value
    })
  }

  handleMenuClick = ({ key }) => {
    if (key === 'radio') {
      router.push('/selector/radio');
      return;
    }
    if (key === 'checkbox') {
      router.push('/selector/checkbox');
      return;
    }
    if (key === 'dropdown') {
      return;
    }
  };

  render () {
    const menu = (
      <Menu className={styles.menu} selectedKeys={[]} onClick={this.handleMenuClick}>
        <Menu.Item key="radio">
          <Icon type="user" />
          <FormattedMessage id="menu.selector.radio" defaultMessage="radio" />
        </Menu.Item>
        <Menu.Item key="checkbox">
          <Icon type="setting" />
          <FormattedMessage id="menu.selector.checkbox" defaultMessage="checkbox" />
        </Menu.Item>
        <Menu.Item key="dropdown">
          <Icon type="close-circle" />
          <FormattedMessage id="menu.selector.dropdown" defaultMessage="dropdown" />
        </Menu.Item>
      </Menu>
    );
    return (
      <Card title="下拉列表" bordered={false} className={styles.main}>
        <div className={styles.content}>
          <div className={styles.left}>
            <label className={styles.select}>choose favorite sports:
              <select value={this.state.value} onChange={this.handleChange}>
                  <option value="running">running</option>
                  <option value="basketball">basketball</option>
                  <option value="skiing">skiing</option>
              </select>
            </label>
            <div className={styles.chosen}>chosen: {this.state.value}</div>
          </div>
          <div className={styles.right}>
            <Dropdown overlay={menu}>
              <span className={styles.action}>
                <span className={styles.name}>查看示例</span>
              </span>
            </Dropdown>
          </div>
        </div>
      </Card>
    )
  }
}

总的来说,是通过在组件状态中设置一个value值,并在return中的select标签中使用,上述实现的是一个默认选中的功能

在用react实现表单时,其原理都是相同的,利用一个组件状态state来存储选中信息,然后监听各个标签元素的点击、悬浮、切换等事件,并在响应函数中更新组件状态

vue与百度地图结合的BMap is not defined问题

首先,得确保首页index.html引入的百度地图的秘钥

<script type="text/javascript" src="//api.map.baidu.com/api?v=2.0&ak=秘钥"></script>

在网上百度了很多方法,也一一尝试了,下面介绍一种实践成功了的方法

百度api官网里的异步加载很像

首先跟入口js文件一样,新建一个map.js文件

export function MP(ak) {
  return new Promise(function (resolve, reject) {
    window.onload = function () {
      resolve(BMap)
    }
    var script = document.createElement("script");
    script.type = "text/javascript";
    script.src = "//api.map.baidu.com/api?v=2.0&ak="+ak+"&callback=init";
    script.onerror = reject;
    document.head.appendChild(script);
  })
}

然后在要用到百度地图的页面调用(ak 就是密钥)

<script>
import {MP} from './map.js'  
export default {
    data() {
        return {
            ak: '秘钥',
            ....
        }
    }
    mounted(){  
          MP(this.ak).then(BMap => {  
              //在此调用api  
          })
     } 
     ......
}
</script>

同样的,挂载其他外部类库有问题时也可以尝试这样做

具体可参考我的个人项目中的map.js百度地图初始化组件百度地图demo

ant-design-pro初步入门

前言

由于项目需求,最近开始学习了ant-design-pro框架,整个框架的业务功能代码主要在四个文件夹中,分别是:

routes:该文件夹下存放每个路由对应的页面组件代码。主要定义页面的基本结构和内容
components:组件文件夹。每个页面可能是由一些组件组成的,对于一些相对通用的组件,可以将该组件写入components文件夹中,并在routes文件夹中的文件引入来使用
models:用于组件的数据存储,接收请求返回的数据以及逻辑处理等
services:用于与后台交互、发送请求等

下面通过具体实践步骤来介绍页面创建的整个流程

实践

配置新的路由和菜单

路由的配置文件统一由src/common/router.js文件进行管理

通过添加一个新路由并在前端页面中增加一个菜单来对应路由

const routerConfig = {
    '/': {
      component: dynamicWrapper(app, [], () => import('../layouts/BasicLayout')),
    },
    '/dashboard/monitor': {
      component: dynamicWrapper(app, ['monitor'], () => import('../routes/Dashboard/NgMonitor')),
    },
    '/dashboard/workplace': {
      component: dynamicWrapper(app, ['monitor'], () => import('../routes/Dashboard/NgWorkSpace')),
    },
    '/testpage': {
      component: dynamicWrapper(app, ['monitor'], () => import('../routes/Test/Test')),
    },
}

路由/testpage对应的页面文件是Test/Test.js

然后在侧边栏中填写一项来对应到我们添加的路由中

const menuData = [{
  name: 'dashboard',
  icon: 'dashboard',  // https://demo.com/icon.png or <icon type="dashboard">
  path: 'dashboard',
  children: [{
    name: '分析页',
    path: 'analysis',
  }, {
    name: '监控页',
    path: 'monitor',
  }, {
    name: '工作台',
    path: 'workplace',
  }, {
    name: '测试页',
    path: 'testpage',
  }],
}, {
  // 更多配置
}];

创建一个页面

在src/routes下创建对应的js文件,如testpage.js

对应到我们添加的路由中

import React, { PureComponent } from 'react';
export default class Testpage extends PureComponent {
  render() {
    return (
      <h1>Hello World!</h1>
    );
  }
}

新增一个组件

如果页面相对复杂,需要引入一个组件

可以在components文件夹下创建一个组件,并在testpage.js引入并使用

如创建components/ImageWrapper/index.js

import React from 'react';
import styles from './index.less';    // 按照 CSS Modules 的方式引入样式文件。
export default ({ src, desc, style }) => (
  <div style="{style}" classname="{styles.imageWrapper}">
    <img classname="{styles.img}" src="{src}" alt="{desc}">
    {desc &amp;&amp; <div classname="{styles.desc}">{desc}</div>}
  </div>
);

然后修改pagetest.js

import React from 'react';
import ImageWrapper from '../../components/ImageWrapper';  // 注意保证引用路径的正确
export default () => (
  <imagewrapper src="https://os.alipayobjects.com/rmsportal/mgesTPFxodmIwpi.png" desc="示意图">;
)

增加service和module

假设pagetest.js页面需要发送请求

我们可以在组件加载时发送一个请求来获取数据

componentDidMount() {
  const { dispatch } = this.props;
  dispatch({
    type: 'project/fetchtagA',
  });
  dispatch({
    type: 'activities/fetchtagB',
  });
}

将会找到对应的models中的函数来发送请求

export default {
  namespace: 'pagetest',
  state: {
    ServicesA: [],
    ServicesB: [],
  },
  effects: {
    *fetchtagA(_, { call, put }) {
      const response = yield call(getServiceAList);
      yield put({
        type: 'ServiceAList',
        tagAServices: response.result,
      });
    },
    *fetchtagB(_, { call, put }) {
      const response = yield call(getServiceBList);
      yield put({
        type: 'ServiceBList',
        tagBServices: response.result,
      });
    },
  },
  reducers: {
    ServiceAList(state, action) {
      return {
        ...state,
        ServicesA: action.tagAServices,
      };
    },
    ServiceBList(state, action) {
      return {
        ...state,
        ServicesB: action.tagBServices,
      };
    },
  },
};

而真正发送请求的实际是service文件夹下的文件进行的

export async function getServiceAList() {
  return request('/api/get_service_list', {
    method: 'POST',
    body: {"type": "waiting"},
  });
}
export async function getServiceBList() {
  return request('/api/get_service_list', {
    method: 'POST',
    body: {"type": "current"},
  });
}

在routes文件夹中的pagetest.js中 ,我们可以直接在render部分使用应该得到的返回值进行渲染显示

const { pagetest, loading } = this.props;
// pagetest指的是相当于数据流
const { ServicesA, ServicesB } = pagetest;
// 从数据流中取出了具体的变量

上述就是快速创建一个页面的简单步骤

大白话解析 Redux 、 redux-thunk 、redux-saga 和 react-redux

Redux是什么

在介绍 redux 之前,我们先来了解下目前前端最火的框架之一 — react

react 官网上,我们可以看到其介绍是

用于构建用户界面的 JavaScript 库

也就是说 react 本质上是一个 JavaScript 的库,是创建UI接口的视图层框架

(图一)

image

如图一所示,假如蓝色组件需要和灰色组件通信,只使用 react 视图层框架,就需要调用父组件函数的形式通信,逐层往父级通信

但对于大型应用来说,这样实现基本不太可能,过多的组件会造成维护困难,那应该怎么做呢?

这个时候就应该在 react 视图层框架上配套一个数据层框架 — Redux ,结合应用

redux 要求我们把数据都放在 store 公共存储空间,当绿色组件想要去传递数据时,只需要改变 store 里边对应的数据,灰色区域会自动感知到 store 有变化,就会重新去 store 取数据,从而灰色组件就能得到新的数据

这样的操作流程对于深层次的组件是非常适用的,组件与组件之间的数据传递会变得非常简单

组件改变,修改数据,其他组件再来取值。这就是 Redux 的基础设计理念

Redux = Reducer + Flux

在讲解这个式子之前,我们先来看看 redux 的起源

react 在2013年开源的时候, facebook 团队除了放出 react 框架外,还放出了 flux 框架,是官方推出的最原始的辅助 react 使用的数据层框架

后来在使用时发现了 flux 的很多缺点,比如公共数据存储区域可以有很多个 store 组成,这样数据存储操作就可能存在一个数据依赖的问题

接着就有人对 flux 做了一个升级,也就是现在使用的 redux , redux 除了借鉴 flux 的设计理念外,还引入了一个新的概念 — Reducer

(图二)

image

图二就是 redux 的工作流程图,再次说明了其设计理念就是把所有数据放在 store 进行管理,一个组件改变了 store 里的数据内容,其他组件就能感知到 store 的变化,再来取数据,从而间接的实现了这些数据传递的功能

Redux的工作流程

根据图二的工作流程,可以举个实际的例子:假设 React Components 是借书的用户, Action Creactor 是借书时说的话(借什么书), Store 是图书馆管理员, Reducer 是记录本(借什么书,还什么书,在哪儿,需要查一下), state 是书籍信息

整个流程就是借书的用户需要先存在,然后需要借书,需要一句话来描述借什么书,图书馆管理员听到后需要查一下记录本,了解图书的位置,最后图书馆管理员会把这本书给到这个借书人

转换为代码是, React Components 需要获取一些数据, 然后它就告知 Store 需要获取数据,这就是就是 Action Creactor , Store 接收到之后去 Reducer 查一下, Reducer 会告诉 Store 应该给这个组件什么数据

Store的创建

我们已经了解到 redux 是解决数据传递问题的框架,把所有的数据都放在store中进行管理。所以 store 数据仓库是四个操作过程中最重要的,应该最先被创建

接下来我们用 redux 来编写简易的 TodoList , 通过具体的例子进行说明

假设你已经通过 create-react-app 创建了一个 react 项目, 这里首先直接引用 antd 进行布局,然后还得引入 redux

npm install redux --save

npm install antd --save

创建 Store 文件夹, 接着在文件夹中创建 index.js

import { createStore } from 'redux' // 引入一个第三方的方法

const store = createStore() // 创建数据的公共存储区域(管理员)

export default store

这样就已经把 redux 引入到项目中了,以及创建了一个 store 的公共数据区域,还需要一个记录本去辅助管理数据,也就是 reducer

继续创建一个 reducer.js ,这个文件需要返回一个函数,接收两个参数: state , action ;需要返回一个值,默认返回 state , state 可以理解为整个数据空间里存放的数据,可以设置一个默认值

const defaultState = {}

export default (state = defaultState, action) => {
  return state
}

现在记录本有了,那最后怎么把笔记本传给 store 呢?继续修改 index.js 的代码

import { createStore } from 'redux'
import reducer from './reducer'

const store = createStore(reducer)

export default store

这样就将 reducer 和 store建立了连接

我们已经创建了一个 store ,负责存储项目应用中的所有数据, 一个 reducer ,负责整个项目应用中的数据处理,并且把 reducer 传给了 store ,这样就可以知道在 reducer 中查看数据并做处理了

在创建 TodoList 组件时,有两项需要获取的数, inputValuelist ,可以在 defaultState 中对其设置初始值,由 reducer 来管理,由于 reducer 会传入到 store ,所以 store 也就知道数据空间里存在 inputValue 和 list 数据

const defaultState = {
  inputValue: '123',
  list: [1, 2]
}

组件的数据应该怎样从公用的数据空间获取呢?我们需要在 TodoList 组件中引入这个 store

import store from './store'

class TodoList extends Component {
  
  constructor(props) {
    super(props)
    console.log(store.getState())
  }

  render () {
    ...
  }
}

很明显,这个值能传到组件中,接着就可以在组件中进行页面的数据渲染了

render () {
  return (
    <div style={{marginTop: '10px', marginLeft: '10px'}}>
      <div>
        <Input value={this.state.inputValue} placeholder='todo info' style={{width: '300px', marginRight: '10px'}}></Input>
        <Button type="primary">提交</Button>
      </div>
      <List
        style={{marginTop: '10px', width: '300px'}}
        bordered
        dataSource={this.state.list}
        renderItem={item => (<List.Item>{item}</List.Item>)}
      />
    </div>
  )
}

简单概括 store 的创建就是:

  1. 需要引入 redux 方法,叫做 createStore

  2. 不能单单的创建 store ,需要在创建 store 的时候把 reducer 传递进来

那 reducer 里边存放的内容呢?

  1. reducer负责管理整个业务里边的数据,包括处理数据,存储数据等

  2. reducer 返回的必须是一个函数,这个函数里面接收两个参数,一个是 state ,另一个是 action

看到这里,我相信有的小伙伴会有疑问, store 里边应该存什么数据呢?我们在 state = defaultState 这里设置了这个仓库的默认数据是什么? action 有什么用呢?接着继续看下面的操作

Action 和 Reducer 的编写

上面已经完成 react 组件取数据这样一个过程,接下来我们继续改进:当 input 里的内容发生改变时, redux 里的数据 value 也可以相应发生变化

可以再次回到图二,应该先创建一个 action , action 是个对象的形式,里边需要有个 type 向 redux 描述需要做的操作, 然后把 value 值传递进去

组件创建完 action 后,接着就需要把这个 action 派发给 store

constructor(props) {
  super(props)
  this.state = store.getState()
  this.handleInputChange = this.handleInputChange.bind(this)
}

... ...

handleInputChange(e) {
  const action = {
    type: 'change_input_value',
    value: e.target.value
  }
  store.dispatch(action)
}

但是 store 并不知道怎么处理这个数据,需要去 reducer 进行查找,所以需要把当前 store 里存在的数据和接收的 action 转给 reducer , reducer 处理好了之后再转给 store

需要注意的是, react 里边的 store ,接收到 action 之后,会自动把之前的数据和 action 转发给 reducer

export default (state = defaultState, action) => {
  console.log(state, action)
  return state
}

state 是上一次 store 中的数据集合,action 是 dispatch 传过来的对象

reducer 已经接收到了数据,也拿到了 action ,接着就需要对这些数据进行处理,最后传给 store

export default (state = defaultState, action) => {
  if (action.type === 'change_input_value') {
    const newState = JSON.parse(JSON.stringify(state)) // 对之前的state做一次深拷贝
    newState.inputValue = action.value
    return newState
  }
  return state
}

注意, reducer 有一个限制, reducer 可以接收 state ,但是绝不能修改 state 。这就是为什么拿到 state 的时候需要去拷贝一份,再对拷贝的数据进行修改了

最后 reducer 返回的 newState 给了谁呢?从图二中我们可以发现, reducer 将处理的新数据传给了 store , store用新数据替换成老数据,那页面是怎么进行更新的呢?

这就需要用到 store 的另一个方法 - store.subscribe() ,意思是组件订阅了store,store里的数据只要发生改变,subscribe() 里边的函数就会执行

但是 subscribe() 里边的函数应该怎样写,才能让 store 一改变,页面就跟着变化呢?

constructor(props) {
  super(props)
  this.state = store.getState()
  this.handleInputChange = this.handleInputChange.bind(this)
  this.handleStoreChange = this.handleStoreChange.bind(this)
  store.subscribe(this.handleStoreChange) 
}

handleStoreChange() {
  this.setState(store.getState())
}

也就是说,当组件感知到 store 里的数据发生变化时,就去调用 store.getState() 方法,从 store 里重新取数据,然后调用 setState 方法,替换掉当前组件里的数据,这样组件里的数据就和 store 里边的数据同步了

接下来继续增加提交功能,当提交发生的时候, input 里的值需要存入公共数据里的 list,同样的逻辑

  1. 需要先给 button 绑定一个事件

  2. 创建一个 action (对象),指定一个类型,然后通过 dispatch 把 action 发给 store

  3. 然后 store 把之前 store 里的数据和 action 发给 reducer , reducer 这个函数接收到 state 和 action 之后会对数据做一些处理,会返回一个新的 state到 store

  4. 最后 store 会将新的 state 替换以前 store 的数据, react 组件会感知到 store 数据发生了变化,会从 store 里边重新取数据,更新组件的内容,页面就发生了变化

(TodoList.js)

<Button type="primary" onClick={this.handleBtnClick}>提交</Button>

handleBtnClick() {
  const action = {
    type: 'add_todo_item',
  }
  store.dispatch(action)
}

(reducer.js)

export default (state = defaultState, action) => {
  if (action.type === 'change_input_value') {
    const newState = JSON.parse(JSON.stringify(state))
    newState.inputValue = action.value
    return newState
  }
  if (action.type === 'add_todo_item') {
    const newState = JSON.parse(JSON.stringify(state))
    newState.list.push(newState.inputValue)
    newState.inputValue = ''
    return newState
  }
  return state
}

使用 redux 完成 todolist 删除功能

先在每个 item 上设置点击事件

 <List
  style={{marginTop: '10px', width: '300px'}}
  bordered
  dataSource={this.state.list}
  renderItem={(item, index) => (<List.Item onClick={this.handleItemDelete.bind(this, index)}>{item}</List.Item>)}
/>

接下来需要改变 store 里的数据,怎么改变呢?同样的,需要先创建一个 action ,然后传给 store

handleItemDelete(index) {
  const action = {
    type: 'delete_todo_item',
    index
  }
  store.dispatch(action)
}

store 接收到这个 action 之后,就会把之前的数据和这个 action 一起传给 reducer 进行处理

if (action.type === 'delete_todo_item') {
  const newState = JSON.parse(JSON.stringify(state))
  newState.list.splice(action.index, 1) // 找到对应的下标,删除即可
  return newState
}

actionTypes的拆分

如果经常操作 action ,可能会发现一个问题, action 的 type 这个字符串要是有一个字符写错了,程序就垮掉了,而且很难被排查出,那应该怎么避免这个问题呢?

可以在 store 文件夹中新建一个文件 actionTypes.js ,然后设置每个 action 字符串的常量,用这些常量分别替换掉 action 的 type 字符串

export const CHANGE_INPUT_VALUE = 'change_input_value'
export const ADD_TODO_ITEM = 'add_todo_item'
export const DELETE_TODO_ITEM = 'delete_todo_item'

这样抽离的目的是因为如果常量或者变量写错的时候,是能报出详细异常的,可以迅速定位到问题

使用 actionCreator 统一创建 action

回到图二 redux 的工作流程图,在派发 action 的时候, action 不应该在我们的组件里直接被定义,一般会通过 actionCreator 来统一的管理页面上所有的 action ,然后通过 actionCreator 来创建 action ,这是一个比较标准、正规的流程。怎么做呢?

在 store 文件夹下创建 actionCreator.js 的文件时,就可以创建一些方法

import { CHANGE_INPUT_VALUE, ADD_TODO_ITEM, DELETE_TODO_ITEM } from './actionTypes'

export const getInputChangeAction = (value) => ({
  type: CHANGE_INPUT_VALUE,
  value
})

然后在 TodoList.js 组件中引入这个文件,更新方法

handleInputChange(e) {
  const action = getInputChangeAction(e.target.value)
  store.dispatch(action)
}

之所以将 action 的创建放在 actionCreator 这样一个统一的文件进行管理,主要的目的是提高代码的可维护性,而且前端会有自动化的测试工具,如果把 action 都放在一个文件里边,做测试的时候也会非常方便

现在回到图二 redux 的流程图,是不是就非常清晰了~~~

如果要改变 store 里的数据,就要先去调用 actionCreator ,创建一个 action ,然后 store 把这个 action 派发出去,这样流程就完全一致了

组件精炼

  1. 实际项目中,最好将 UI 组件和容器组件拆分, UI 组件负责页面渲染,容器组件负责页面逻辑

  2. 当组件中只有一个 render 函数时,就可以定义成无状态组件

比如 TodoListUI 组件

class TodoListUI extends Component {
  render() {
    return (
      <div style={{marginTop: '10px', marginLeft: '10px'}}>
        // ... ...
      </div>
    )
  }
}

就可以修改成无状态组件

const TodoListUI = (props) => {
  return (
    <div style={{marginTop: '10px', marginLeft: '10px'}}>
      // ... ...
    </div>
  )
}

无状态组件的性能比较高,因为它就是一个函数,而 React 里边普通的组件是 JS 里边的一个类,这个类生成的对象里,还会有一些生命周期函数,所以它执行起来,既要执行生命周期函数,又要执行 render ,它要执行的东西远比函数执行的东西多的多,所以一个普通组件的性能是肯定赶不上无状态组件的

小结

Redux 设计和使用的三项原则

  1. 首先 store 要求必须是唯一的

  2. 只有 store 能够改变自己的内容

有的小伙伴可能会疑惑,明明是 reducer 对数据进行了整理,其实 reducer 只是将原有数据和新的 action 进行了整理,最终还是需要把新的 state 返回给 store , store 拿到 reducer 的数据,再对自己的数据进行更新

  1. reducer 必须是纯函数

纯函数指的是,给定固定的输入,就一定会有固定的输出,而且不会有任何副作用,如果一个函数里边有 ajax 等异步操作,或者与日期相关的操作之后,他都不是一个纯函数,副作用是指对传入的参数进行修改

Redux 中核心的 API

  1. createStore 可以帮助创建 store

  2. store.dispatch 帮助派发 action , action 会传递给 store

  3. store.getState 这个方法可以帮助获取 store 里边所有的数据内容

  4. store.subscrible 方法可以让让我们订阅 store 的改变,只要 store 发生改变, store.subscrible 这个函数接收的这个回调函数就会被执行

使用 Redux-thunk 中间件进行ajax请求发送

在讲解 Redux-thunk 这个中间件之前,我们先写一个在 react 中直接获取异步数据的例子

React中发送异步请求获取数据

先进行模拟数据的测试,假设你已经安装了 Charles ,打开 Charles ,然后找到 Tools 下面的 Mac Local ,选中,进行如图操作

image

image

image

当然,之前还得新建一个 list.json 文件,如

["hello", "dell", "lee"]

然后刷新页面,就可以看到打印出来请求的json数据了

image

接下来,就可以进行创建 action , store 派发 action 的操作了

首先在 actionCreator.js 里边创建一条 action 对象

export const initListAction = (data) => ({
  type: INIT_LIST_ACTION,
  data
})

然后在 actionTypes 中申明这个 action type 常量

export const INIT_LIST_ACTION = 'init_list_action'

接着在 TodoList 组件中实例化生成 action,并 dispatch action 到 store , store 再连同之前 store 的数据一同派发给 reducer

componentDidMount() {
  axios.get('/list.json').then((res) => {
    const data = res.data
    const action = initListAction(data)
    store.dispatch(action)
  })
}

reducer 对数据进行整理,最后将整理好的数据返回给 store

if (action.type === INIT_LIST_ACTION) {
  const newState = JSON.parse(JSON.stringify(state))
  newState.list = action.data
  return newState
}

使用 Redux-thunk 中间件进行ajax请求发送

上面的 TodoList 组件代码, list 在 componentDidMount 做了一个ajax数据的请求,咋一看可能没有什么问题,但是,如果我们把这种异步的请求,或者把一些非常复杂的逻辑都放在组件里进行实现时,这个组件会显得过于臃肿

所以遇到这种异步请求或者非常复杂的逻辑,最好是把它移出到其他页面进行统一的处理,可以移到哪里进行管理呢?

这个时候 Redux-thunk 这个中间件就显得至关重要了,它可以将这些异步请求或者是复杂的逻辑放到 action 去处理,那如何使用 Redux-thunk 这个中间件呢?

打开github,搜索 Redux-thunk ,star最多的项目,就是Redux-thunk

按照它的使用说明进行如下操作

import { createStore, applyMiddleware } from 'redux'
import reducer from './reducer'
import thunk from 'redux-thunk'

const store = createStore(
  reducer,
  applyMiddleware(thunk) // applyMiddleware可以使用中间件模块
) 

export default store

需要注意的是:

  1. 中间件是通过创建 redux 的 store 时使用的,所以这个中间件是指的 redux 中间件,而不是 react 中间件

  2. 原则上 action 返回的是一个对象,但当我们使用 redux-thunk 中间件后, action 就可以返回一个函数了,继而可以在函数里边进行异步操作,也就可以把 TodoList 获取数据的请求放入这个函数中了

接着操作,在 actionCreator 中创建 action 的函数,然后数据传给 store

那问题来了,怎么传呢?本质还是调用 dipatch 方法,但是现在 actionCreactor 这个文件里并没有 store 这个数据仓库,也就没有 dispatch 这个方法,怎么办呢?

实际上,当我们创建一个内容是函数的 action 时,返回的函数就会自动接收到 store.dispatch 这个方法,所以只要在返回的函数里调用 dispatch ,然后派发 action 就好了, store 判断接收的 action 是一个对象,就会接收并发送给 reducer 进行数据更新操作

export const getTodoList = () => {
  return (dispatch) => {
    axios.get('/list.json').then((res) => {
      const data = res.data
      const action = initListAction(data)
      dispatch(action)
    })
  }
}

在 TodoList 组件中引用这个创建内容是函数的 action

componentDidMount() {
  const action = getTodoList()
  store.dispatch(action) // 调用 store.dispatch()这个函数时,action这个函数就会被执行
}

有的小伙伴可能会有疑问,就一个ajax请求,放在 componentDidMount 会有影响吗?

考虑到后期代码量的增加,如果把异步函数放在组件的生命周期里,这个生命周期函数会变得越来越复杂,组件就会变得越来越大

所以,还是应该把这种复杂的业务逻辑或者异步函数拆分到一个地方进行管理,现在借助 redux-thunk ,就可以放在 actionCreactor 里边集中管理,除此之外,在做自动化测试的时候,测试 actionCreactor 这个方法,也会比测组件的生命周期函数要简单的多

到底什么是 Redux 中间件

(图三)

image

看到图三,我们先来回顾一个redux的标准流程:

view 到 redux 的过程中会派发一个 action , action 通过 Store 的 dispatch 方法,会派发给 store , store接收到 action ,再连同之前的 state 一起传给 reducer , reducer 返回一个新的数据给 store , store 就可以去改变自己的 state ,组件接收到新的 state 就可以重新渲染页面了

redux的中间件在这个流程里边,指的是谁和谁之间呢?指的是 action 和 store 中间

继续看图三,action 通过 dispatch 方法被传递给 store ,那么 action 和 store 之间是不是就是 dispatch 这个方法呢?实际上,我们说的中间件就是指的 dispatch 方法的一个封装,或者是对 dispatch 方法的一个升级

最原始的 dispatch 方法,接收到对象 action 后会传递给 store ,这就是没有中间件的情况

对 dispatch 方法做了一个升级后,也就是使用中间件时,再调用 dispatch 方法,如何给 dispatch 传递的仍然是个对象, dispatch 就会把这个对象传给 store ,跟之前的方法没有任何区别;但是假如传的是个函数,就不会直接传递给 store 了,会让这个函数先执行,然后执行完之后需要调用 store ,这个函数再去调用 store

dispatch方法会根据参数的不同,执行不同的事情,如果参数是对象,就直接传给store,如果是函数,那就把函数执行结束

所以,redux的中间件原理很简单,就是对 store 的 dispatch 方法做一个升级,既可以接收对象,又可以接收函数了,那是用什么方法进行的升级的呢?就是用 redux-thunk 这个中间件进行升级的

当然,redux的中间件还有 redux-log ,原理就是在派发 action 给 store 之前先 console.log 出来;还有 redux-saga ,接下来需要讲解的

Redux-saga 中间件的使用

redux-saga 也是做异步代码拆分的,可以完全替代 redux-thunk

在 github 中搜索 redux-saga ,翻到文档部分,根据文档进行如下操作

import { createStore, applyMiddleware } from 'redux'
import reducer from './reducer'
import createSagaMiddleware from 'redux-saga'

const sagaMiddleware = createSagaMiddleware() // 创建saga中间件

// 创建数据的公共存储区域
const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
) 

export default store

这里还需要在store中建一个单独的文件- saga.js

function* mySaga() {
  
}

export default mySaga

mySaga() 是 ES6 的 generator 函数

没有使用 redux-saga 时, action 只能给到 store , store 再把之前的数据和 action 给到 reducer ,所以我们只能在 reducer 里拿到 store 去做一些业务逻辑

需要注意的是,有了redux-saga之后, saga.js 也可以接收这个 action了

import { takeEvery } from 'redux-saga/effects'
import { GET_INIT_LIST } from './actionTypes'
import { initListAction } from './actionCreator'
import axios from 'axios'

function* getInitList() {
  axios.get('/list.json').then((res) => {
    const data = res.data
    const action = initListAction(data)
    console.log(action)
  })
}

// generator 函数
function* mySaga() {
  yield takeEvery(GET_INIT_LIST, getInitList) // takeEvery捕捉每一个派发出来的action type类型为GET_INIT_LIST的时候,就会执行getInitList方法
}

export default mySaga

上面的代码是什么意思呢?首先,当 TodoList 这个容器组件加载完成后,会派发一个 action ,因为之前在创建 store 时使用了 redux-saga 这个中间件,做了基础的配置,所以这个 action 派发出来之后,不仅仅 reducer 会接收到这个 action , saga 文件中 mySaga 这个函数也能接收到,刚好通过 takeEvery 这个函数声明,一旦接收到 GET_INIT_LIST 这样类型的 action ,就执行 getInitList 这个方法,所以就可以把异步逻辑写到这个方法里了

通过在异步函数中创建 action ,还需要把它派发出去,但是在 saga.js 这个文件中并没有 store 数据仓库,所以不能执行 store.dispatch(action) 这个操作,接下来我们会用到另一个方法 - put

继续看 github 上 redux-saga 的例子

在 generator 函数里边我们可以不用 promise 来请求异步数据,可以这么来写

import { takeEvery, put  } from 'redux-saga/effects'
import { GET_INIT_LIST } from './actionTypes'
import { initListAction } from './actionCreator'
import axios from 'axios'

function* getInitList() {
  const res = yield axios.get('/list.json')
  const action = initListAction(res.data)
  yield put(action)
}

function* mySaga() {
  yield takeEvery(GET_INIT_LIST, getInitList)
}

export default mySaga

整个执行流程就是:

  1. 首先在创建 store 的时候,根据官方文档的配置,需要把 redux-saga 的使用配置做好,这里需要注重的是:

    在引入 createSagaMiddleware 后,需要创建一个 createSagaMiddleware ,然后通过 applyMiddleware 使用这个中间件,接着创建 saga.js 这个文件,然后在 store 的 index 中引入这个文件,让这个文件通过 sagaMiddleware 来运行

  2. saga 里边要有一个 generator 函数,在这个 generator 函数里边写入一些逻辑,意思是当接收到 action 的类型是 GET_INIT_LIST 时,就执行 getInitList 方法,这个方法是一个 generator 函数,接着就可以在 getInitList 方法里进行数据的获取发送操作了

当我们获取ajax数据失败的时候,为了操作友好,�最后做下容错处理

function* getInitList() {
  try {
    const res = yield axios.get('/list.json')
    const action = initListAction(res.data)
    yield put(action)
  } catch(e) {
    console.log('list.json 网络请求失败')
  }
}

通过上面的实践可以发现, redux-saga 远比 redux-thunk 复杂的多, redux-saga 里边有非常多的api,我们只用了 takeEveryput ,文档中还有很多我们经常用到的 calltakeLatest

在处理大型项目时, redux-saga 是要优于 redux-thunk 的;但是从另一角度来说, redux-thunk 几乎没有任何 api ,特点就是在 action 里面返回的内容不仅仅是个对象,还可以是个函数

React-Redux 的使用

目前我们已经了解了 react 和 redux ,那 React-Redux 是什么呢?它是一个第三方的模块,可以在 react 中非常方便是使用 redux

重新来编写 todolist 功能,在 index 文件中引入 react-redux

import React from 'react'
import ReactDOM from 'react-dom'
import TodoList from './TodoList'
import { Provider } from 'react-redux'
import store from './store'

const App = (
  <Provider store={store}>
    <TodoList />
  </Provider>
)

ReactDOM.render(App, document.getElementById('root'))

Provider 实质是一个组件,是一个提供器,是 react-redux 的一个核心API,连接着 store , Provider 里边所有的组件,都有能力获取到 store 里边的内容

react-redux 的另一个核心方法叫做 connect ,接收三个参数,最后一个参数是连接的组件,前面两个是连接的规则

之前说 Provider 组件连接了 store , Provider 内部的组件有能力获取到 store ,是怎样获取的呢?就是通过 connect 这个方法获取到里面的数据的

意思是让 TodoList 组件和 store 进行连接,所以 connect 方法的意思是做连接,在做连接时需要有一定的方式和规则,就是用 mapStateToProps 方法来做关联,翻译为中文就是把 store 里的数据 inputValue 映射到组件 inputValue 这个位置,为组件的 props 的数据

import React, { Component } from 'react'
import { connect } from 'react-redux'

class TodoList extends Component {
  render () {
    return (
      <div>
        <div>
          <input value={this.props.inputValue} />
          <button>提交</button>
        </div>
        <ul>
          <li>Dell</li>
        </ul>
      </div>
    )
  }
}

const mapStateToProps = (state) => {
  return {
    inputValue: state.inputValue,

  }
}

export default connect(mapStateToProps, null)(TodoList)

如果需要对 store 的数据做修改,dispatch 是指的 store.dispatch ,可以通过 mapDispatchToProps 方法把 store.dispatch 挂载到props上,为什么呢?

因为想要改变 store 里的内容,就要调用 dispatch 方法, dispatch 方法被映射到了 props 上,所以就可以通过 this.props.dispatch 方法去调用了

import React, { Component } from 'react'
import { connect } from 'react-redux'

class TodoList extends Component {
  render () {
    return (
      <div>
        <div>
          <input value={this.props.inputValue} onChange={this.props.handleInputChange} />
          <button>提交</button>
        </div>
        <ul>
          <li>Dell</li>
        </ul>
      </div>
    )
  }
}

const mapStateToProps = (state) => {
  return {
    inputValue: state.inputValue
  }
}
const mapDispatchToProps = (dispatch) => {
  return {
    handleInputChange(e) {
      const action = {
        type: 'change_input_value',
        value: e.target.value
      }
      dispatch(action)
    }
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(TodoList)

现在在 input 里输入值的功能就完成了,那todolist的增加功能怎么实现呢?

(TodoList.js)

<button onClick={this.props.handleClick}>提交</button>

const mapDispatchToProps = (dispatch) => {
  return {
    handleInputChange(e) {
      const action = {
        type: 'change_input_value',
        value: e.target.value
      }
      dispatch(action)
    },

    handleClick() {
      const action = {
        type: 'add_todo_item'
      }
      dispatch(action)
    }
  }
} 

(reducer.js)

export default (state = defaultState, action) => {
  if (action.type === 'change_input_value') {
    const newState = JSON.parse(JSON.stringify(state))
    newState.inputValue = action.value
    return newState
  }
  if (action.type === 'add_todo_item') {
    const newState = JSON.parse(JSON.stringify(state))
    newState.list.push(newState.inputValue)
    newState.inputValue = ''
    return newState
  }
  return state
}

点击这个 button 的时候,会执行 handleClick 这个方法,这个方法会把创建出来的 action 传给 store ,再传给 reducer, reducer 接收到这个 action 之后,去处理数据,把新的数据返回出去,新的数据就包含列表项的新内容了,数据发生了改变,todolist 组件恰好又通过 connect 跟数据做了连接,所以这块是个自动的流程,数据一旦发生改变,这个组件自动就会跟的变

以前还需要 store.subscribe 做订阅,现在连订阅都可以不用了,页面自动跟随数据发生变化

这样写就实现了增加 item 的功能,后续还有一些功能的实现可以去我的 github 看完整代码

比如 item 的删除操作, action 要通过 actionCreator 来创建,同时,还需要把 action 的 type 字符串放在 actionType 里面进行管理等等

创建 TodoList 这个组件,正常来说都是 export default TodoList ,把这个组件导出出去,但是�现在 export defalut 出的东西是通过 connect 方法执行的结果,connect 方法做了一件什么事呢?

它把这些映射关系和业务逻辑集成到了 TodoList 这个 UI 组件之中,所以 connect 方法可以这样理解,TodoList 是一个 UI 组件,当你用 connect 把这个 UI 组件和一些数据和逻辑相结合时,返回的内容实际就是一个容器组件了,容器组件可以理解成数据处理包括派发这样的业务逻辑,对 UI 组件进行包装,去调用这些UI组件,数据和方法都准备好了

有的小伙伴可能在网上看到过这样的描述,react-redux 组件既有 UI 组件,又有容器组件。UI 组件就是 TodoList 这个东西,而容器组件就是 connect 方法返回的结果,或者说 connect 方法执行生成的内容

所以 export default 导出的内容就是 connect 方法执行的结果,是一个容器组件

代码

附完整版代码地址:redux-todolist

内含

react-redux 完整的 todolist 代码

redux-saga 完整的 todolist 代码

简单的 Webpack Plugin 编写

前言

前端技术发展迅速,各种框架工具库层出不穷,诸如'大前端'、'前后端分离'、'本地开发构建'等等热门词汇我们更是屡见不鲜,这些现代前端技术基石自然离不开 webpack ,webpack 的工作更离不开各类插件 Plugin 的支持

相信不少小伙伴在平时的项目开发中使用过 Webpack Plugins, 我的上篇文章 Webpack4 和 Babel 7 全套配置也有介绍搭配 Webpack Plugins 的配置过程,但对于自己开坑写 webpack ,可能是比较少的(包括我寄几),这两天抽空看了下 webpack 中文文档(v4.15.1),又逢读到 Webpack 原理-编写 Plugin 好文章一篇,豁然开朗,所以简单的介绍下插件开发的过程

例子中使用的插件我已经发到 npm 用来测试了

基本插件结构

webpack 插件是一个具有 apply 属性的 JavaScript 对象。apply 属性会被 webpack compiler 调用,并且 compiler 对象可在整个编译生命周期访问

class BasicPlugin{
  // 在构造函数中获取用户给该插件传入的配置
  constructor(options){
  }
  
  // Webpack 会调用 BasicPlugin 实例的 apply 方法给插件实例传入 compiler 对象
  apply(compiler){
    compiler.hooks.run.tap(pluginName, compilation => {
      // console.log("webpack 构建过程开始!")
    })
  }
}

// 导出 Plugin
module.exports = BasicPlugin

compiler hooktap 方法的第一个参数,应该是驼峰式命名的插件名称。建议为此使用一个常量,以便它可以在所有 hook 中复用

配置

const BasicPlugin = require('BasicPlugin.js')
module.export = {
  plugins:[
    new BasicPlugin(options),
  ]
}
  • Webpack 启动后,在读取配置的过程中会先执行 new BasicPlugin(options) 初始化一个 BasicPlugin 获得其实例
  • 在初始化 compiler 对象后,再调用 basicPlugin.apply(compiler) 给插件实例传入 compiler 对象
  • 插件实例在获取到 compiler 对象后,就可以通过 compiler hooktap 方法监听到 Webpack 广播出来的事件,hook 上绑定了事件名称,第二个参数是回调函数

Compiler 和 Compilation

在基本结构中比较重要的两个对象就是 CompilerCompilation ,它们是 PluginWebpack 之间的桥梁

Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例

Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象

Compiler 和 Compilation 的区别在于

Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译

事件流

webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果

插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理

Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作

Webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好,事件流机制应用了观察者模式

/**
* 广播出事件
* event-name 为事件名称,注意不要和现有的事件重名
* params 为附带的参数
*/
compiler.apply('event-name',params)

/**
* 监听名称为 event-name 的事件,当 event-name 事件发生时,函数就会被执行。
* 同时函数中的 params 参数为广播事件时附带的参数。
*/
compiler.hooks.event-name.tap('plugin-name',function(params) {
  
})

apply 方法是必须要有的,因为当我们使用一个插件时( new somePlugins({})),webpack 会去寻找插件的 apply 方法并执行

compiler.hooks.event-name.tap() 就相当于给 compiler 设置了事件监听,当监听到 event-name (Compilation)事件,应该做些相应操作,类似于 document.addEventListener

实战

下面写一个插件名为 LogWebpackPlugin 的例子,作用是 Webpack 在执行过程中打印一些日志,实现该插件非常简单,完整代码如下

class LogWebpackPlugin {

  constructor(doneCallback, emitCallback) {
    // 存下在构造函数中传入的回调函数
    this.emitCallback = emitCallback
    this.doneCallback = doneCallback
  }

  apply(compiler) {
    compiler.hooks.emit.tap('LogWebpackPlugin', () => {
      // 在 emit 事件中回调 emitCallback
      this.emitCallback()
    })
    compiler.hooks.done.tap('LogWebpackPlugin', () => {
      // 在 done 事件中回调 doneCallback
      this.doneCallback()
    })
    compiler.hooks.compilation.tap('LogWebpackPlugin', () => {
      // compilation('编译器'对'编译ing'这个事件的监听)
      console.log("The compiler is starting a new compilation...")
    })
    compiler.hooks.compile.tap('LogWebpackPlugin', () => {
      // compile('编译器'对'开始编译'这个事件的监听)
      console.log("The compiler is starting to compile...")
    })
  }
}

// 导出插件 
module.exports = LogWebpackPlugin

使用该插件的方法如下

npm i log-webpack-plugin --D
const logWebpackPlugin = require('log-webpack-plugin') 

module.exports = {
  plugins: [
    // 在初始化 logWebpackPlugin 时传入了两个参数,分别是在成功时的回调函数和模块完成转换的回调函数;
    new logWebpackPlugin(() => {
      // Webpack 模块完成转换成功
      console.log('emit 事件发生啦,所有模块的转换和代码块对应的文件已经生成好~')
    } , () => {
      // Webpack 构建成功,并且文件输出了后会执行到这里,在这里可以做发布文件操作
      console.log('done 事件发生啦,成功构建完成~')
    })
  ]
}

npm run build 编译结果

image

完整插件可见 log-webpack-plugin

页面间的传值方法及问题

在vue中官网中我们可以看到两种页面间传值的方法:query和params

query 跳转URL携带参数

首先在路由文件中定义类似的如下路由

{
      path:"/user",
      name:"user",
      component:user
    }

然后在页面中定义跳转路径和传值参数

this.$router.push({path:'/user',query:{userName:'ls',userId:'02'}});

浏览器会显示

http://localhost:8080/#/user?userName=ls&userId=02

我们已经了解了query传参会将参数都放到url中,所以当传递的值比较多的情况下,跳转的url会太长而受限制(取决于浏览器和服务器的限制)

所以只建议在参数较少的情况下使用query传值

params

参数不带入url中,可在参数较多时使用

同样的先在路由文件中定义类似的如下路由

{
      path:"/user",
      name:"user",
      component:user
    }

然后在页面中定义跳转路径和传值参数

this.$router.push({path:'/user',query:{userName:'ls',userId:'02'}});

问题来了,如果我们打印下面代码,结果肯定是 undefined

console.log(this.$route.params.userName )

注意,这是因为使用params传值,只能用name来引入路由,可见官网

正确的写法

this.$router.push({name:'user',query:{userName:'ls',userId:'02'}});

问题又来了,如果我们刷新页面,参数就不见了

注意,用params携带参数时,在注册路由时,需要在path后面加上/:参数名

{
      path:"/user/:userName/:userId",
      name:"user",
      component:user
    }

浏览器会显示

http://localhost:8080/#/user/ls/02

这样做虽然解决了上面的问题,但是同样会在url后面显示传入的参数值

sessionStorage 会话存储

优点:不仅可以解决传参较多问题,还可以传递一个完整的对象;对于临时数据,避免了参数值过期时间的设置

sessionStorage 是HTML5新增的一个会话存储对象,用于临时保存同一窗口(或标签页)的数据,在关闭窗口或标签页之后将会删除这些数据

存储数据

采用setItem()方法存储

sessionStorage.setItem('userName','ls'); 

通过属性方式存储
  
sessionStorage['userName'] = 'ls';

读取数据

通过getItem()方法取值

sessionStorage.getItem('userName')

通过属性方式取值

sessionStorage['userName']

存取对象

存储时,通过JSON.stringify()将对象转换为文本格式

读取时,通过JSON.parse()将文本转换回对象

let userInf = {
    userName: 'ls',
    userId: 02
}

// 存储值:将对象转换为Json字符串
sessionStorage.setItem('user', JSON.stringify(userInf))

// 取值时:把获取到的Json字符串转换回对象
var userJsonStr = sessionStorage.getItem('user')

userInf = JSON.parse(userJsonStr);
console.log(userEntity.userName); // => ls

对于永久存储的数据,本地存储传值可以用localStorage,方法与sessionStorage类似

聊聊 React 编程**

前阵子在部门内做了一个关于 React 的分享,为了更进一步地理解,也分享给有需要的小伙伴,做了如下整理 ^_^

image

Facebook 在 2013 年 5 月推出了全新的函数式编程,也就是全球范围内使用人数最多的前端框架 - React,也是目前最受欢迎的前端框架之一

现代框架与旧式框架的区别

React 是一个视图层框架,是用来解决数据和页面渲染的问题

同样的还要 VueAngular,这也是目前前端最受欢迎的三套框架,几乎是所有前端工程师必备的一项技能

所以我们在技术选型的时候,常常会产生困扰,究竟应该选择哪一门语言开发项目呢,比如

  • reactjs : 灵活性更大,处理大型业务时选择性更多一点
  • vuejs : api更多,实现功能更简单,但也因为api多,灵活性有一定限制

所以,在做复杂度比较高的项目时,大家倾向于 reactjs ,而面向用户端的一些复杂度不是特别高的项目时,用 vuejs 更简单

当然,vuejs 也可以做大型的项目,至于具体选什么框架,还需要取决于对框架的熟悉程度以及业务复杂度做一个权衡

而在几年前开发前端应用时,基本没这个困扰,因为大家开发都用 jQuery

image

但随着前端交互的复杂度越来越高,现代框架比如 reactvue 逐渐的替换掉了 jQuery,因为用现代框架来开发更容易维护

为什么会说变得容易维护呢?我们先来看看 reactjQuery 到底什么区别

image

我认为,他们之间编程**的最大区别,就是声明式命令式的区别

命令式

命令式编程,比如 jQuery,直接操作 DOM,告诉页面怎么挂载,怎么操作,整个程序有 70% 都是操作 DOM

<button class="red_btn" type="button">change</button> 
<script type="text/javascript"> 
  $(document).ready(function(){ 
    $("button").click(function(){ 
      if($("button").hasClass("red_btn")){ 
        $("button").removeClass("red_btn").addClass("green_btn") 
      } else {
        $("button").removeClass("green_btn").addClass("red_btn")
      }
    }) 
  })
</script>

例如上面的代码,点击一个按钮,切换 button 颜色。我们用 jQuery 这种命令式编程思路写,就是当前是什么颜色就让它变成另外一个颜色

但如果我们认真想想,其实这里面可以细分成两个行为,一个是对状态判断,另一个是操作 DOM。那声明式呢?

声明式

还是上面那个场景,我们用 React 提供的 JSX 语法来实现,当我们用 JSX 描述了映射关系之后,点击按钮事件时,只需要对颜色这个变量进行修改就可以完成需求了

handleChangeBtnColor() {
  this.setState({
    tag: !this.state.tag
  })
}

render() {
  return (
    <div>
      <button
        className={this.state.tag? 'red_btn' : 'green_btn'}
        type="button"
        onClick={this.handleChangeBtnColor}
      >
        change
      </button>
    </div>
  )
}

所以区别就出来了,用 react 来实现同样的需求,如果细分来看,我们在逻辑上只有状态这一个行为

jQuery 是两个行为,状态 + DOM 操作

那为什么 React 要用声明式来编程呢?

因为命令式编程是直接操作 DOM 节点,如果有多个事件,比如 100 个 button 上都有点击和取消事件,那频繁的事件操作会很大程度上产生 bug ,而且不可控,会有无法预期性的 bug 产生

而且声名式编程只需要我们操作数据就好,数据可能出现的几种情况我们能提前做好容错

声明式是通过描述状态与视图之间的映射关系来操作DOM,或者说具体点是用这样的映射关系来生成一个 DOM 节点插入到页面去

比如 React 提供的 JSX 和 Vue 中的模板语言

目的是为了实现声明式渲染的功能,本质上都是描述了 『状态』与『视图』之间的映射关系

状态与视图之间的映射关系,等同于 render 函数

在框架的内部,不论是 JSX 还是 Vue 的模板,最终会编译成 render 函数。

image

声明式渲染是现代框架的特性,也就是我们常说的数据驱动视图

这个特性跟声明式可以简化维护应用代码的复杂度有什么关系呢?

事实上,这个特性可以让我们把关注点只放在状态的维护上。这样一来,即使应用复杂后,我们管理代码的方式只在状态上,所有的视图操作都不用关心了,因为框架会帮我们自动去做,可以说大大降低代码维护的成本

what

状态

那我们所说的这个状态到底是什么呢?

  • 在现实中,状态是某一时刻看到或感受到的状况
  • 对于设计来说,状态是 UI 在交互过程中某一时刻的画面
  • 对于开发来说,状态是存储上下文中所用到的数据

image

React 官网上,我们可以看到其介绍是用于构建用户界面的 JavaScript,所以 React 本质上是一个创建 UI 接口的视图层框架

前面我们已经提到了 React 有个声明式**,它是数据到视图的一个静态映射,但在我们的实际项目中,并不是一个静态网页,还需要操作数据,网页的状态可能随时改变,那怎么才能让网页跟着状态一起改变呢?

响应式设计**

这就是 React 背后的响应式设计**

开发者只需要告诉 React 我们希望页面长什么样子,React 就会自动帮我们绘制界面

也就是说,我们只要操作数据,页面视图会自动作出响应,用户界面的展示完全取决于数据层

而且我们一切的操作都是基于内存之中,不会有较大的性能损耗,这就是 React 响应式编程的精髓,也是为何它叫作 ReactReact 在英文中是响应的意思

简单来说就是,不需要关注视图层,只需要关注数据层的变化

一个类 class 一定有一个构造函数 constructor,最优先被执行

constructor(props) {
  super(props)
}

constructor 接收 props 参数,super 指的是父类 Componentsuper(props) 方法指的是调用父类的构造函数

定义数据需要定义在状态里面 this.state

框架是怎么知道 Web 应用在运行时数据状态发生了变化呢? 这个问题是所有框架必须去解决的

不同的解决方案,导致的直接结果就是它所提供给用户的上层语法或 API 完全不一样,也是我们常对比的各个框架的使用区别

解决方案包括我们常说的 Virtual DOMdiff 算法对比

Virtual DOM 有兴趣的小伙伴可以查看我的另一篇博客 Virtual DOM 中那些你不知道的事,在这篇博客里有对 Virtual DOM 做一个详细的讲解

服务端渲染

既然提到了 Virtual DOM,这里就提一下 React 的价值 - nodejs 服务端渲染

因为有 Virtual DOM 的存在,React 可以很容易的将 Virtual DOM 转换为字符串,这便使我们可以只写一份 UI 代码,同时运行在 node 里和和浏览器里

var html = React.renderToString(el)

在 node 里将组件 HTML 渲染为一段 HTML 一句话即可,不过围绕 renderToString 还需要做一些准备工作

整个思路大致是:

  1. 从后台 server 或数据库等来源拉取数据
  2. 引入要渲染的 React 组件
  3. 调用 React.renderToString() 方法来生成 HTML
  4. 最后发送 HTML 和数据给浏览器

这就是 React 的服务端渲染,组件的代码前后端都可以复用

不仅如此,React 还能够用一套代码同时运行在浏览器和 node 里,而且能够以原生 App 的姿势运行在 iOS 和 Android 系统中,即拥有了 web 迭代迅速的特性,又拥有原生 App 的体验,也就是 React-Native

单向数据流

我认为使用 react最大好处在于功能组件化,遵守前端可维护的原则

react 是单向数据流,什么是单向数据流呢?

  • 数据主要从父节点传递到子节点( 通过 props ),即遵循从上到下的数据流向

  • 如果顶层( 父级 )的某个 props 改变了,react 会重渲染所有的子节点

image

通俗的理解是指用户访问 ViewView 发出用户交互的 Action,在 Action 里对 state 进行相应更新。state 更新后会触发 View 更新页面的过程。这样数据总是清晰的单向进行流动,便于维护并且可以预测

那为什么 react 要使用单向数据流呢?

实际上,单向数据流这种模式十分适合跟 react 搭配使用

它的主要**组件不会改变接收的数据。它们只会监听数据的变化,当数据发生变化时它们会使用接收到的新值,而不是去修改已有的值。当组件的更新机制触发后,它们只是使用新值进行重新渲染而已

消除了在多个地方同时管理状态,可能出现的数据不同步的情况,它只会在一个地方进行状态管理,减小了应用的复杂度,唯一的数据源将使得开发更加简单

需要注意的是,单向数据流并非单向绑定,甚至单向数据流与绑定没有任何关系

对于 react 来说,单向数据流( 从上到下 )与单一数据源这两个原则,限定了 react 中要想在一个组件中更新另一个组件的状态(类似于 vue 的平行组件传参,或者是子组件向父组件传递参数),需要进行状态提升

即将状态提升到他们最近的祖先组件中。子组件中 Change 了状态,触发父组件状态的变更,父组件状态的变更,影响到了另一个组件的显示(因为传递给另一个组件的状态变化了,这一点与 vue 子组件的 $emit() 方法很相似)

比如在做 list 删除时,为什么不可以直接把 list 传给子组件来改变 list?

因为父组件可以向子组件传值,但是子组件只能去使用这个值,不能去改变这个值

应该是父组件向子组件传递方法,子组件调用这个方法,传递一个数据,最终还是父组件自己来改变这个数据

Vue也是单向数据流,只不过能实现双向绑定,UI 控件提供了双向数据绑定的方式,在一些需要实时反应用户输入的场合会非常方便

但通常认为复杂应用中这种便利比不上引入状态管理带来的优势

所以无论是 vue 还是 react 其实还是提倡单向数据流去管理状态,这一点在 vuexredux 状态管理器上体现的很明显

虽然 vuereact 框架本身有自己状态管理,当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏

  • 多个视图依赖于同一状态
  • 来自不同视图的行为需要变更同一状态

所以就需要 vuexredux 来解决这个问题,redux 在我的另一篇博客大白话解析 Redux 、 redux-thunk 、redux-saga 和 react-redux 中介绍的很详细了,大家有兴趣可以去看看

注意:

单向数据流中的单向,指的是数据从父组件到子组件的这个流向叫单向

绑定单双向是指View层与Module层之间的映射关系

但我们通常也说双向数据绑定,带来双向数据流

数据( state )和视图( View )之间的双向绑定,ng 里的 ng-model 和 vue 里的 v-model

props

刚才我们提到了 props,怎么理解 props 呢?

propsproperty 的缩写,可以理解为 HTML 标签的 attribute

在组件内部,可以通过 this.props 来访问 propsprops 是组件唯一的数据来源

不可以使用 this.props 直接修改 props,因为 props 是只读的props 是用于整个组件树中传递数据和配置

PropTypes 与 DefaultProps

react 为我们提供了一套非常简单好用的属性校验机制,强校验:

// 对TodoItem的一些属性类型做校验
TodoItem.propTypes = {
  content: PropTypes.string.isRequired,
  deleteItem: PropTypes.func,
  index: PropTypes.number
}

// 设置TodoItem的一些默认属性
TodoItem.defaultProps = {
  content: 'hello world'
}

PropTypes 包含的校验类型包括基本类型、数组、对象、实例、枚举

state

React 的一大创新,就是把每一个组件都看成是一个状态机,组件内部通过 state 来维护组件状态的变化,这也是state 唯一的作用

每个组件都有属于自己的 statestateprops区别在于前者 ( state ) 只存在于组件内部,只能从当前组件调用 this.setState 修改 state 值( 不可以直接修改 this.state

一般我们更新子组件都是通过改变 state 值,更新子组件的 props 值从而达到更新

state 一般和事件一起使用,比如有一个简单的开关组件,开关状态会以文字的形式表现在按钮的文本上

首先需要在 render 方法中返回了一个 button 元素,给 button 注册了一个事件用来处理点击事件,在点击事件中对 state 的描述开关状态的字段,比如 on 取反,并执行 this.setState() 方法设置 on 字段的新值。一个开关组件就完成了

react 通过将事件处理器绑定到组件上来处理事件

react 事件本质上和原生 JS 一样,鼠标事件用来处理点击操作,表单事件用于表单元素变化等,react 事件的命名、行为和原生 JS 差不多,不一样的地方是 react 事件名区分大小写

事件的处理器需要由组件的使用者来提供,可以通过 props 将事件处理器传进来

image

这是一个 react 组件实现组件可交互所需的流程,render() 输出 Virtual DOMVirtual DOM 转为 DOM,再在 DOM 上注册事件,事件触发 setState() 修改数据,在每次调用 setState 方法时,react 会自动执行 render 方法来更新 Virtual DOM,如果组件已经被渲染,那么还会更新到 DOM 中去

setState 方法

新版的 setState 可以接收一个函数而不是一个对象了,需要有一个返回值 return
所以我们可以在项目中做一些优化,比如

handleInputChange(e) {
  this.setState({
    inputValue: e.target.value
  })
}

可以优化为

handleInputChange(e) {
  const value = e.target.value
  this.setState(() => ({
      inputValue: value
  }))
}

注意:当有 e.target.value 这种异步设置数据的时候,需要存在外层

handleBtnClick() {
  this.setState({
    list: [...this.state.list, this.state.inputValue],
    inputValue: ''
  })
  }

可以用 prevState,改为

handleBtnClick() {
  this.setState((prevState) =>({
    list: [...prevState.list, prevState.inputValue],
    inputValue: ''
  }))
}

handleDeleteItm(index) {
  const list = [...this.state.list] // 拷贝list数组
  list.splice(index,1)
  this.setState({
    list
  })
}

可以改为

handleDeleteItm(index) {
  this.setState((prevState) => {
    const list = [...prevState.list]
    list.splice(index,1)
    return {list}
  })
}

props 与 state

尽可能使用 props 当做数据源,state 用来存放状态值( 简单的数据 )

也就是说咱们通常用 props 传递大量数据,state 用于存放组件内部一些简单的定义数据

当组件的 state 或者 props 发生改变的时候,render 函数就会重新执行

当父组件的 render 函数被运行时,它的子组件的 render 都将重新被运行一次

单向数据流和单向数据绑定是什么区别呢

前面已经提到了单向数据流,需要按照它的顺序办事。比如我们假设有一个这样的生命周期:

  1. 从 data 里面读取数据
  2. ui 行为( 如果没有 ui 行为就停在这里等他有了为止 )
  3. 触发 data 更新
  4. 再回到步骤1

改了一个数,view 层不能反回头来找他来更新 view 层视图( 从步骤 2 跳回去 1 ),你得等下一个循环(转了一圈)的步骤 1 才能更新视图。react 就是这样子,你得 setState 触发更新,如果你 this.state = {...},是没用的,它一直不变

单向数据绑定,就是绑定事件,比如绑定 onInputonChangestorage 这些事件,只要触发事件,立刻执行对应的函数(代表 react)

双向数据绑定,我们一般是借用 js 底层的 Object.defineproperty ( 代表 Vue )

这是 Vue 双绑的核心**,view 层能让 model 层变了,model 层也能让 view 层变了

要判断是单向绑定还是双向绑定,只需要手动去控制台改一下那个核心绑定的数据,view 层的显示内容能马上变化的就是双绑,不能马上有变化的只是单向数据

想做到像 Vue 那样的极致双绑,能够在控制台改个数据就改变视图的,大概就只有 defineproperty(据说新版 vue 现在用 ES6proxy )和定时器轮询了

既然说到了数据流,那组件间是怎么进行通信的呢?

组件通信

一般来说,有两种通信方式

父子组件通信

react 中,最为常见的组件通信也就是父子了,一般情况是:

父组件更新组件状态 -----props-----> 子组件更新

另一种情况是

子组件更新父组件状态 -----需要父组件传递回调函数-----> 子组件调用触发

可能大家对于第二种子组件更新父组件状态的情况有些不理解

一般情况下,只能由父组件通过 props 传递数据给子组件,使得子组件得到更新

那么现在,我们想实现子组件更新父组件,就需要父组件通过 props 传递一个回调函数到子组件中,这个回调函数可以更新父组件子组件就是通过触发这个回调函数,从而使父组件得到更新

兄弟组件通信

当两个组件处于同一级时( 同处父级,或者同处子级 ),就称为兄弟组件

这里也有两种实现方式

方式一

按照React单向数据流方式,我们需要借助父组件进行传递,通过父组件回调函数改变兄弟组件的props

其实这种实现方式与子组件更新父组件状态的方式是大同小异的

方式一只适用于组件层次很少的情况,当组件层次很深的时候,整个沟通的效率就会变得很低

方式二

React官方给我们提供了一种上下文方式,可以让子组件直接访问祖先的数据或函数,无需从祖先组件一层层地传递数据到子组件中

但这种方法建议按需使用,可能会导致一些不可预期的错误。( 比如数据传递逻辑结构不清晰 )

组件划分

前面已经提到使用 react最大好处在于功能组件化,遵守前端可维护的原则

事实上,react 组件化开发原则是组件负责渲染 UI,组件的不同状态对应着不同 UI,通常遵循以下组件设计思路

  • 布局组件:仅仅涉及应用 UI 界面结构的组件,不涉及任何业务逻辑,数据请求及操作

  • 容器组件:负责获取数据,处理业务逻辑,通常在 render() 函数内返回展示型组件

  • 展示型组件:负责应用的界面 UI 展示

  • UI 组件:指抽象出的可重用的 UI 独立组件,通常是无状态组件

实际项目中,最好将 UI 组件和容器组件拆分, UI 组件负责页面渲染,容器组件负责页面逻辑

当组件中只有一个 render 函数时,就可以定义成无状态组件

无状态组件的性能比较高,因为它就是一个函数,而 React 里边普通的组件是 JS 里边的一个类,这个类生成的对象里,还会有一些生命周期函数,所以它执行起来,既要执行生命周期函数,又要执行 render ,它要执行的东西远比函数执行的东西多的多,所以一个普通组件的性能是肯定赶不上无状态组件的

生命周期函数

React的组件拥有一套清晰完整而且非常容易理解的生命周期机制

大体可以分为三个过程:初始化更新销毁

在组件生命周期中,随着组件的 props 或者 state 发生改变,它的 Virtual DOMDOM 表现也将有相应的变化

6721543734532_ pic

什么叫生命周期函数?生命周期函数指在某一时刻组件会自动调用执行的函数

// constructor 可以理解为一个生命周期函数,它是 ES6 的语法规定的。在组件一创建就会被调用,页面初始化 Initialization
constructor(props) {
  super(props)
  // 当组件的 state 或者 props 发生改变的时候,render 函数就会重新执行  
  this.state = {
    inputValue: 'hello',
    list: ['学习英文', '学习react']
  }
  this.handleInputChange = this.handleInputChange.bind(this)
  this.handleBtnClick = this.handleBtnClick.bind(this)
  this.handleDeleteItm = this.handleDeleteItm.bind(this)
}

// 在组件即将被挂载到页面的时候执行
componentWillMount() {
  console.log('componentWillMount')
}

render() {
  console.log('parent render')
  return (
    <div>......</div>
  )
}

// 组件被挂载到页面之后自动执行
componentDidMount() {
  console.log('componentDidMount')
}

// 当组件被更新之前,他会自动执行
shouldComponentUpdate() {
  console.log('shouldComponentUpdate')
  return true
}

// 组件被更新之前,它会自动执行,但是它在 shouldComponentUpdate 之后执行
// 如果 shouldComponentUpdate 返回true它才执行
// 如果返回 false ,这个函数就不会执行了
componentWillUpdate() {
  console.log('componentWillUpdate')
}

componentDidUpdate() {
  console.log('componentDidUpdate')
}

在子组件中

render() {
  console.log('child render')
  const { content } =  this.props
  return (
    <div onClick={this.handleClick}>
      {content}
    </div>
  )
}

handleClick() {
  const { deleteItem, index } = this.props
  deleteItem(index)
}

// 一个组件从父组件接受参数
// (只要父组件的render函数被重新执行了,子组件的这个生命周期函数就会被执行)
// 如果这个组件第一次存在于父组件中,不会执行
// 如果这个组件之前已经存在于父组件中,才会执行
componentWillReceiveProps() {
  console.log('child componentWillReceiveProps')
}

// 但这个组件即将被剔除时执行
componentWillUnmount() {
  console.log('child componentWillUnmount')
}

Mount 是指组件被挂载执行的过程,Updation 是指组件被更新执行的过程,什么情况发生更新呢?要么是 state 被更新,要么是 props 被更新,也就是数据发生变化的时候,页面会更新

需要注意的是,所有的生命周期函数都可以不存在,但是有一个生命周期函数必须得有,就是 render 函数

它的底层为什么会有这样的设定呢?原因就是组件是继承自 Component 这个组件的,React Component 这个组件里边默认内置了其他所有的生命周期函数,唯独没有内置 render 函数,所以对组件来说,render 是必须自己定义的,不然就会报错

React 生命周期函数的使用场景

上面讲了 React 的生命周期函数,有一个比较容易忽视的钩子函数 shouldComponentUpdate ,他的使用过程是怎样的呢?

我们先来做个测试,看下子组件的渲染过程,先把所有的生命周期函数都删除掉,只在子组件的 render 函数中留下 console.log('child render')

然后我们在 input 框中输入内容,在控制台可以看到这样的结果

image

也就是父组件 render 函数重新执行的时候,子组件的 render 函数也会跟着执行

这样的逻辑是没有问题的,但是它会带来性能上的损耗

父组件上的内容发生变化了,其实子组件的内容是没必要重新渲染的,而这样的机制会导致子组件要做很多无谓的渲染

那应该怎样做性能优化呢?

很简单,这个时候我们就可以利用生命周期函数 shouldComponentUpdate 来做性能优化了

shouldComponentUpdate 这个函数的意思是,当数据或者内容发生变化的时候,会先询问一下,组件是否要被真正的更新

shouldComponentUpdate(nextProps, nextState) {
  if(nextProps.content !== this.props.content) {
    return true
  } else {
    return false
  }
}

shouldComponentUpdate 一般会接收两个参数,一个是nextProps,另一个是 nextState

当一个组件要被更新的时候,props 要被更新成什么样呢?
nextProps 指的是接下来 props 要被变化成什么样,nextState 指的是接下来 state 要被变化成什么样

我们的组件 props 接收的 content ,如果 content 发生变化,这个组件才需要重新渲染,没有发生变化时,不需要发生渲染

这样就通过 shouldComponentUpdate 这个生命周期函数提升了组件的性能,可以避免一个组件做无谓的 render 操作

render 函数重新执行,就意味着 React 底层要生成一份 Virtual DOM ,和之前的 Virtual DOM 做比对,虽然 Virtual DOM 的比对比 Actual DOM 的比对要快的多,但是,如果能省略这个比对过程当然能节约更多的性能

性能优化

当然,React 当中有很多关于性能优化的点

  • 首先是 this.handleClick.bind(this) 这样的方法,如果要改变作用域的话,我们把作用域的修改放在 constructor 里边,这样可以保证整个程序里边这个函数的作用域绑定只会执行一次,而且可以避免组件的一些无谓渲染,所以,这样写代码,react 组件的性能会有所提升

  • 其次, react 的底层 setState 内置了性能提升的机制,是一个异步的函数,可以把多次数据的改变结合成一次来做,这样可以降低 Virtual DOM 的比对频率

  • 再者 react 的底层使用了 Virtual DOM 的概念,还有同层比对,还有 key 的概念,来提升 Virtual DOM 比对的速率,从而提升 react 的性能

  • 最后,也就是借助 shouldComponentUpdate 这个方法,可以提高 react 组件的性能,因为我们可以避免无谓的组件的 render 函数的运行

监听数据对象

由于之前用过一段时间的 Vue,在转到 React 开发的时候,可以明显的发现 React 并没有 Vue 可以 watch 数据对象的方法

React 是怎么检测数据对象的变化呢?

React 默认不是双向绑定的,它不监听数据对象,而是通过手动调用 setState() 方法来触发了 Virtual DOM 的更新,再用 diff 算法来进行 Virtual DOM 比较前后两个状态的不同,看看是哪个 DOM 节点更新了,然后针对性的更改变化了的 DOM 结构实现数据更新,渲染 Actual DOM

我们单纯的使用 React,状态发生变化,会触发组件生命周期中的如下方法:

componentWillUpdate(object nextProps, object nextState) 

componentDidUpdate(object prevProps, object prevState) 

但如果结合 Redux 使用,一般状态变化是由 Dispatch 引起的,我们可以在 Dispatch 的回调中执行相应的操作

image

函数式编程

react 把需要不断重复构建的 UI 抽象成了组件,它充分利用很多函数式的方法减少了冗余代码

可以说,函数式编程是 React 的精髓

那到底什么是函数式编程呢?

函数式编程或称函数程序设计,又称泛函编程,是一种编程范型,它将电脑运算视为数学上的函数计算,并且避免使用程序状态以及易变对象

比起命令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。

也就是说,函数式编程和命令式编程最大的区别是:

函数式编程关心数据的映射,而命令式编程关心解决问题的步骤

而且维护方便,面向测试的开发流程

一个高阶函数,它可以接收函数可以当参数,也可以当返回值,这就是函数式编程

像柯里化、装饰器模式、高阶组件,都是相通的,一个道理

举个简单的 🌰

function first () {
  console.log('zhangshan')
}
function second() {
  console.log('lisi')
}

现在想在每条 console 语句前后各加一条 console 语句,如果在每个函数都加上 console 语句,会产生不必要的耦合,所以高阶函数就派上了用场

function FuncWrapper(func) {
  return function () {
    console.log('before')
    func()
    console.log('after')
  }
}
var first = FuncWrapper(first)
var second = FuncWrapper(second)

我们写了一个函数 FuncWrapper,该函数接一个函数作为参数,将参数函数装饰了一层,返回出去,减少了代码耦合

在设计模式中称这种模式为装饰器或装饰者模式

React 中,高阶组件 HOC 就相当于这么一个 FuncWrapper,传入一个组件,返回被包装或者被处理的另一个组件

高阶组件

a higher-order component is a function that takes a component and returns a new component.

高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件

高阶组件就是一个没有副作用的纯函数

本质上是一个类工厂,先举个简单的 🌰

组件一:

import React from 'react'

export default class First extends React.Component {
  constructor (props) {
    super(props)
    this.changeHandle = this.changeHandle.bind(this)
  }
  changeHandle (value) {
    console.log(value)
  }
  render () {
    return (
      <div>
        <h2>zhangsan</h2>
        <input type="text" onchange={value => this.changeHandle(value)}/>
      </div>
    )
  }
}

组件二:

import React from 'react'

export default class Second extends React.Component {
  constructor (props) {
    super(props)
    this.changeHandle = this.changeHandle.bind(this)
  }
  changeHandle (value) {
    console.log(value)
  }
  render () {
    return (
      <div>
        <h2>lisi</h2>
        <input type="text" onchange={value => this.changeHandle(value)}/>
      </div>
    )
  }
}

有两个不相同的组件,但是有部分功能重合,比如 h2 标题的内容,changeHandle 函数,这样也就造成了代码的冗余

理解了高阶函数,再解决这类问题就不难了吧?接下来我们加入高阶组件解决这个问题

高阶组件:

import React, { Fragment } from 'react'

// 定义装饰器的外层函数
function HocUITest(name) {
  // 返回一个装饰器函数
  return function CompWrapper (Component) {
    return class WarpComponent extends React.Component {
      constructor (props) {
        super(props)
        this.handleChange = this.handleChange.bind(this)
      }

      handleChange (value) {
        console.log(value)
      }

      render () {
        return (
          <Fragment>
            <h2>{name}</h2>
            <Component handleChange={this.handleChange} {...this.props}></Component>
          </Fragment>
        )
      }
    }
  }
}
export default HocUITest

在高阶组件返回包装好的组件的时,我们将高阶组件的 props 展开并传入包装好的组件中,这是确保给高阶组件的 props 也能给到被包装的组件上

简化接下来的两个组件

组件一:

import React from 'react'
import HocUITest from './Test'

@HocUITest('zhangsan')
class First extends React.Component {
  constructor (props) {
    super(props)
    console.log(props)
  }
  render () {
    return (
      <div>
        <input type="text" onChange={value => this.props.handleChange(value)}/>
      </div>
    )
  }
}

// First = HocUITest('zhangsan')(First)

export default First

组件二:

import React from 'react'
import HocUITest from './Test'

@HocUITest('lisi')
export default class Second extends React.Component {
  constructor (props) {
    super(props)
    console.log(props)
  }
  render () {
    return (
      <div>
        <input type="text" onChange={value => this.props.handleChange(value)}/>
      </div>
    )
  }
}

高阶组件的用途很多,比如代码复用,逻辑抽象,抽离底层代码,渲染劫持,更改 state、更改 props 等等

包括我们经常用到的 react-reduxconnect 函数

reduxstateaction 创建函数,通过 props 注入给了 Component

你在目标组件 Component 里面可以直接用 this.props 去调用 redux stateaction 创建函数了

ConnectedComment = connect(mapStateToProps, mapDispatchToProps)(Component)

相当于

// connect是一个返回函数的函数(就是个高阶函数)
const enhance = connect(mapStateToProps, mapDispatchToProps)
// 返回的函数就是一个高阶组件,该高阶组件返回一个与Redux store
// 关联起来的新组件
const ConnectedComment = enhance(Component)

antdForm 组件也是一样的

const WrappedNormalLoginForm = Form.create()(NormalLoginForm)

上述高阶组件中我们用了 ES6 装饰器语法,@HocUITest('lisi') 就是一个装饰器,它修改了类的行为

也就是说,装饰器是一个对类进行处理的函数

需要注意的是,装饰器对类的行为的改变,是代码编译时发生的,而不是在运行时

这意味着,装饰器能在编译阶段运行代码

也就是说,装饰器本质就是编译时执行的函数

为了传递更多的参数,上面的装饰器函数外面又封装了一层函数

比如,我们实际开发时,ReactRedux 库结合使用时,常常需要写成下面这样

class MyComponent extends React.Component {}
export default connect(mapStateToProps, mapDispatchToProps)(MyComponent)

有了装饰器,就可以改写上面的代码

@connect(mapStateToProps, mapDispatchToProps)
export default class MyComponent extends React.Component {}

接下来我们主要说一下两种功能的 react 高阶组件:属性代理、反向继承

属性代理

高阶组件将它收到的 props 传递给被包装的组件,所叫属性代理

主要用来处理以下问题

  • 更改 props
  • 抽取 state
  • 通过 refs 获取组件实例
  • 将组件与其他原生 DOM 包装到一起

反向继承

为什么叫反向继承,是高阶组件继承被包装组件,按照我们想的被包装组件继承高阶组件

反向代理主要用来做渲染劫持

所谓的渲染劫持,就是最后组件所渲染出来的东西或者我们叫 React Element 完全由高阶组件来决定,通过我们可以对任意一个 React Elementprops 进行操作;我们也可以操作 React ElementChild

用过 React-Redux 的人可能会有印象,使用 connect 可以将 reactredux 关联起来,这里的 connect 就是一个高阶组件

ref 的使用

refreference 的简写,它是一个引用,在 React ,可以使用 ref 操作 DOM

<input
  id='insertArea'
  className='input'
  value={this.state.inputValue} 
  onChange={this.handleInputChange}
  ref={(input) => {this.input = input}}
/>

React 16 的新语法中,ref 应该等于一个函数(箭头函数)

ref={(input) => {this.input = input}} ,构造了一个 ref 引用,这个引用叫 this.input ,它指向 input 对应的 DOM 节点。所以 this.input 指向的就是 input 框的 DOM

所以下面的方法

handleInputChange(e) {
  const value = e.target.value
  this.setState(() => ({
      inputValue: value
  }))
}

可以改为

handleInputChange() {
    const value = this.input.value
    this.setState(() => ({
        inputValue: value
    }))
  }

应该尽量保持少操作 DOM ,setState 是异步函数,操作 DOM 时必须写到回调中

比如

<Fragment>
  <div>
    <label htmlFor='insertArea'>输入内容</label>
    <input
      id='insertArea'
      className='input'
      value={this.state.inputValue} 
      onChange={this.handleInputChange}
      ref={(input) => {this.input = input}}
    />
    <button onClick={this.handleBtnClick}>提交</button>
  </div>
  <ul ref={(ul) => {this.ul = ul}}>
    {this.getTodoItem()}
  </ul>
</Fragment>
 handleBtnClick() {
  this.setState((prevState) =>({
    list: [...prevState.list, prevState.inputValue],
    inputValue: ''
  }), () => {
    console.log(this.ul.querySelectorAll('div').length)
  })
}

ref 是帮助我们在 React 中直接获取 DOM 元素的时候使用的,一般情况下尽量避免使用 ref ,但是有的时候一些极其复杂的业务,比如动画的时候,不可避免的还是要用到 DOM 标签,怎么用呢,就用 ref 来获取 DOM 标签

注意,refsetState 合用的时候,DOM 的获取并不及时,原因是 setState 是异步的,如果希望页面更新之后再去获取 DOM ,需要把获取 DOM 的语法放在 setState 的第二个参数里边,它是一个回调函数

补充知识

开发环境搭建

快速搭建 React 的开发环境有两种方法

  1. 通过 CDN 引入 .js 文件来使用 React

  2. 使用 create-react-app 脚手架工具来编码

脚手架是前端开发过程中的一个辅助工具,自动构建一个大型项目的开发流程和目录,允许我们以一定方式实现 js 文件的相互引用,更方便的对项目进行管理

在脚手架的代码并不能直接运行,需要脚手架进行编译,编译出来的代码才可以被浏览器识别运行,一般会使用 webpackgulp 这样的工具

工程目录简介

image

  • yarn.lock - 项目依赖的安装包
  • package.json - node 的包文件,包含项目的介绍、项目依赖的包、指令供调用,让项目变成node的包
  • public favicon.ico - 项目左上角图标,index.html 模板
  • src index.js - 整个程序运行的入口文件

注意事项

state 不允许做任何改变,可以先拷贝 state 中的值

const list = [...this.state.list]

需要注意的是

  1. 在 JSX 语法中, {{}} 是表示 JS 表达式里的 JS 对象

  2. 转义的情况下,比如输入:<h1>hello</h1> 会显示 <h1>hello</h1>

    <li 
      key={index} 
      onClick={this.handleDeleteItm.bind(this,index)}
    >
      {item}
    </li>

不转义的情况下,比如输入:<h1>hello</h1> 会显示 hello

```jsx
<li 
  key={index} 
  onClick={this.handleDeleteItm.bind(this,index)}
  dangerouslySetInnerHTML={{__html: item}}
>
</li>
```
  1. React 中使用表单时,label 元素的 for 标签要替换成 htmlFor

    <label htmlFor='insertArea'>输入内容</label>
    <input
      id='insertArea'
      className='input'
      value={this.state.inputValue} 
      onChange={this.handleInputChange.bind(this)}
    />

使用 Charles 实现本地数据 mock

在前端开发代码的时候实际上和后端是分离的,也就需要在本地进行接口数据的模拟,这个时候就需要使用 Charles 进行接口数据的模拟

具体操作我在博客大白话解析 Redux 、 redux-thunk 、redux-saga 和 react-redux里边已经讲到了,有兴趣的小伙伴可以去看看

我们先 npm install axios --save 安装 axios

然后在程序中引入 axios: import axios from 'axios'

componentDidMount() {
  axios.get('/list.json')
    .then((res) => { 
      console.log('data',res.data) 
      this.setState(() => ({      
          list: [...res.data]
      }))
    })
    .catch(() => console.log('error'))
}

image

Charles 这个工具的原理是什么?

它可以抓到浏览器向外发送的请求,然后对一些请求做一些处理,比如说,抓取到请求的是http://localhost:3000/list.json

他有一个规则是,只要你请求的下面这个地址

image

就会把 Local path 这个本地文件的内容返回给你

所以 Charles 其实就是一个中间的代理服务器,可以抓取到浏览器的请求,如果有些接口是需要模拟的话,就可以使用 CharlesMap Local 这个功能去模拟数据

当然,用脚手架的话,我们也可以在 public 文件夹中 mock 请求

结尾

这篇文章主要从编程**入手剖析 React,包括如何快速构建组件和应用,让你快速了解 React 的编程原理

当然,对于构建大型应用,我们还需要结合 Reduxreact-routeraxios,大家有兴趣的话,都可以瞧瞧 ^_^

react 组件在 unmounted 后进行 setState 操作的报错处理

最近在项目中遇到一个这样的问题,在 react 组件 unmounted 之后 setState 会报错

我们先来看个简单的例子,主要是为了重现一下问题

class Child extends Component {
  state = {
    ballName: ''
  }

  componentDidMount() {
    setTimeout(() => {
      this.setState({
        ballName: 'Ball Zhang'
      }, 1000)
    })
  }

  render() {
    return <span>Hello! {this.state.ballName}</span>
  }
}
class Parent extends Component {
  state = {
    isShowed: true
  }

  componentDidMount() {
    setTimeout(()=> {
      this.setState({
        isShowed: false
      })
    }, 300)
  }

  render() {
    const message = this.state.isShowed ? <Child /> : 'Bye!'
    return (
      <div>
         <span>{ message }</span>
      </div>
    )
  }
}

举的例子比较简略,主要是为了说明问题

在 Parent 组件中,300ms 之后移除了 Welcome 组件,但在 Child 组件里 1000ms 之后会改变 Welcome 组件的状态。这时候 React 会报出如下错误:

Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component.

这种错误情况一般出现在 react 组件已经从 DOM 中移除,但组件的 setState 还未执行完

我们在 react 组件中发送一些异步请求或者进行异步操作时, 就有可能会出现这样的问题,这个例子的 setState 其实也是异步操作

再举个例子,我们在 componentDidMount 中发送异步请求,当请求成功返回数据,我们调用 setState 改变组件的状态。但是当请求到达之前, 我们更换了页面或者移除了组件,就会报这个错误。这是因为虽然组件已经被移除,但是请求还在执行, 所以会报setState() on an unmounted component的错误

解决办法

现在我们已经知道问题出现的原因了,那应该怎么来解决呢?

实质上是要求我们在 react 组件被移除之前终止 setState 操作就行了

回到我们之前的例子,可以这样来做:

一种方式在子组件上设置定时器,可随时取消

componentDidMount() {
  // 把 setTimeout 保存在 timer 里
  this.timer = setTimeout(() => {
    this.setState({
      name: 'Ball Zhang'
    })
  }, 1000)
}

// 在组件将要被移除的时候,清除 timer
componentWillUnmount() {
  clearTimeout(this.timer)
}

类似的在处理 ajax 请求的时候也是这个套路, 在 componentWillUnmount 方法中终止 ajax 请求即可

componentWillMount() {
  this.xhr = $.ajax({
    // 请求的细节
  })
}

componentWillUnmount() {
  this.xhr.abort()
}

另一种方法是在父组件移出子组件之前增加延时,保证子组件有足够的时间完成 setState

componentDidMount() {
    setTimeout(()=> {
      this.setState({
        isShowed: false
      })
    }, 2000)
  }

至于采用哪种方式来解决这个 bug ,还得根据具体情境来决定

Virtual DOM 中那些你不知道的事

虚拟DOM是啥?以及diff算法原理

虚拟 dom 相当于在 js 和真实 dom 中间加了一个缓存,利用 dom diff 算法避免了没有必要的 dom 操作,从而提高性能
原理:

  • 把树形结构按照层级分解,只比较同级元素
  • 给列表结构的每个单元添加唯一的 key 属性,方便比较
  • React 只会匹配相同 class 的 component(这里面的 class 指的是组件的名字)
  • 合并操作,调用 component 的 setState 方法的时候, React 将其标记为 dirty.到每一个事件循环结束, React 检查所有标记 dirty 的 component 重新绘制.
  • 选择性子树渲染。开发人员可以重写 shouldComponentUpdate 提高 diff 的性能

diff算法,DOM树对比过程

#46

为什么虚拟 dom 会提高性能?

  • DOM是浏览器中的概念,用js对象表示页面上的元素,并提供操作DOM对象的API
  • 虚拟DOM就是一个JS对象(数据+JXS模板),用一个js对象来描述真实的DOM

虚拟DOM提高性能,不是说不操作DOM,而是减少操作DOM的次数,减少回流和重绘
虚拟 dom 相当于在 js 和真实 dom 中间加了一个缓存,利用 dom diff 算法避免了没有必要的 dom 操作,从而提高性能

  • 用 JavaScript 对象结构表示 DOM 树的结构;
  • 然后用这个树构建一个真正的 DOM 树,插到文档当中
  • 当状态变更的时候,重新构造一棵新的对象树。
  • 然后用新的树和旧的树进行比较,记录两棵树差异
  • 把记录的差异应用到真正的 DOM 树上,视图就更新了

使用diff算法比较新旧虚拟DOM----即比较两个js对象不怎么耗性能,而比较两个真实的DOM比较耗性能,从而虚拟DOM极大的提升了性能

虚拟DOM的目的?

实现页面中DOM元素的高效更新

虚拟DOM会比真实DOM快吗?什么情况下用虚拟DOM好

虚拟DOM并不一定比原生操作DOM快。需不需要虚拟DOM,其实与框架的DOM操作机制有关
React 的基本思维模式是每次有变动就重新渲染整个应用。如果没有 Virtual DOM,简单来想就是直接重置 innerHTML。
比如,在一个大型列表所有数据都变了的情况下,重置 innerHTML 其实是一个还算合理的操作... 真正的问题是在 “全部重新渲染” 的思维模式下,即使只有一行数据变了,它也需要重置整个 innerHTML,这时候显然就有大量的浪费。

可以比较一下 innerHTML vs. Virtual DOM 的重绘性能消耗:

  • innerHTML: render html string O(template size) + 重新创建所有 DOM 元素 O(DOM size)
  • Virtual DOM: render Virtual DOM + diff O(template size) + 必要的 DOM 更新 O(DOM change)

Virtual DOM render + diff 显然比渲染 html 字符串要慢,但是!它依然是纯 js 层面的计算,比起patch简化的dom操作省下来的时间可观的多。
可以看到,innerHTML 的总计算量是和整个界面的大小相关,但 Virtual DOM 的计算量里面,只有 js 计算和界面大小相关,DOM 操作是和数据的变动量相关的。和 DOM 操作比起来,js 计算是极其快速的。
这才是为什么要有 Virtual DOM:它保证了
1)不管你的数据变化多少,每次重绘的性能都可以接受;
2) 你依然可以用类似 innerHTML 的思路去写你的应用。

这也是 React 厉害的地方。并不是说它比 DOM 快,而是说不管你数据怎么变化,我都可以以最小的代价来进行更新 DOM。 方法就是我在内存里面用新的数据重新生成一个虚拟 DOM 树,然后比较新旧 DOM,找出差异,再更新到 DOM 树上。这就是所谓的 diff 算法

  • 虚拟DOM减少了真实DOM的操作,当修改数据的时候,就是修改虚拟DOM产生全新的虚拟DOM。新旧虚拟DOM使用diff算法,得到patch(也就是需要修改的部分),然后将这个patch打到浏览器的DOM上(减少重绘和回流,从而达到性能优化的目的)
  • 每次DOM操作会引起重绘或者回流,频繁的真实DOM的修改会触发多次的排版和重绘相当耗性能。
    总之,一切为了减弱频繁的大面积重绘引发的性能问题,不同框架不一定需要虚拟DOM,关键看框架是否频繁会引发大面积的DOM操作

react是怎么工作的,怎么提高性能

主要还是说了下react的生命周期,还有shouldComponentUpdate这个函数,以及diff算法
#47

React 的工作原理

React 会创建一个虚拟 DOM(virtual DOM)。当一个组件中的状态改变时,React 首先会通过 "diffing" 算法来标记虚拟 DOM 中的改变,第二步是调节(reconciliation),会用 diff 的结果来更新 DOM

react 事件绑定

由于类的方法默认不会绑定this,因此在调用的时候如果忘记绑定,this的值将会是undefined。
绑定方式有以下几种:

  • 在构造函数中使用bind绑定this
    constructor(props) {
      super(props)
      this.handleClick = this.handleClick.bind(this)
    }
  • 在调用的时候使用bind绑定this
    <button onClick={this.handleClick.bind(this)}>
  • 在调用的时候使用箭头函数绑定this
    <button onClick={()=>this.handleClick()}>
  • 使用属性初始化语法绑定this(实验性)
      handleClick=()=>{
        console.log('this is:', this);
      }
      <button onClick={this.handleClick}>

比较:

  • 方式2和方式3都是在调用的时候再绑定this

    • 写法比较简单,当组件中没有state的时候就不需要添加类构造函数来绑定this
    • 每一次调用的时候都会生成一个新的方法实例,因此对性能有影响,并且当这个函数作为属性值传入低阶组件的时候,这些组件可能会进行额外的重新渲染,因为每一次都是新的方法实例作为的新的属性传递。
  • 方式1在类构造函数中绑定this,调用的时候不需要再绑定

    • 优点:只会生成一个方法实例,并且绑定一次之后如果多次用到这个方法也不需要再绑定
    • 缺点:即使不用到state,也需要添加类构造函数来绑定this,代码量多一点
  • 利用属性初始化语法(箭头函数声明),将方法初始化为箭头函数,因此在创建函数的时候就绑定了this(只会生成一个)

    • 优点:创建方法就绑定this,不需要在类构造函数中绑定,调用的时候不需要再作绑定。结合了方式1、方式2、方式3的优点
    • 需要用babel转译,且不能带参数,要不然还得使用方式二和三

方式1是官方推荐的绑定方式,也是性能最好的方式。方式2和方式3会有性能影响并且当方法作为属性传递给子组件的时候会引起重渲问题。方式4目前是最好的绑定方式,需要结合bable转译
this 的本质就是:this跟作用域无关的,只跟执行上下文有关

注意:只要是需要在调用的地方传参,就必须在事件绑定的地方使用bind或者箭头函数.又回到了方式二和方式三

生命周期

  • 初始化
    • 类的构造方法constructor():初始化props、state
  • 挂载
    • componentWillMount():组件即将被装载、渲染到页面上,整个生命周期中只会调用一次(在这里请求异步数据,render可能不会渲染到,因为componentWillMount执行后,render立马执行)
    • render():创建虚拟dom,进行diff算法,更新dom树
    • componentDidMount(): 组件被挂载到页面(渲染到 DOM 中)之后调用,整个生命周期只调用一次(可以异步请求数据,不建议使用setState函数,会触发额外的渲染,导致性能问题)
  • 更新
    • componentWillReceiveProps(nextprops):组件从父组件中接受了新的props(初始化时不调用。可以调用setState来更新组件状态,旧的属性可以通过this.props获取)
    • shouldComponentUpdate(nextprops,nextstate):组件接受到新属性或者新状态(组件接受新的props或state。返回布尔值,为true才更新组件。通过对比新旧数据避免生成新的dom树和旧的进行diff算法对比,从而优化性能。因为父组件render()调用会使得子组件render()也被执行)
    • componentWillUpdate(nextprops,nextstate): 组件更新之前(componentshouldupdate返回true)时调用,组件初始化时不调用(不能调用setState,会导致死循环)
    • render():创建虚拟dom,进行diff算法,更新dom树
    • componentDidUpdate():组件更新完成之后调用,组件初始化时候不调用(可以在这里获取doms)
  • 卸载
    • componentWillUnmount(): 组件即将被卸载时执行(在这里清除一些不需要的监听和计时器)

函数式编程,纯函数

React创建组件的方式

React 中有三种构建组件的方式
React.createClass()、ES6 class 和无状态函数

组件性能优化

shouldComponentUpdate(react 性能优化是哪个周期函数?)

这个方法用来判断是否需要调用 render 方法重新描绘 dom。
因为 dom 的描绘非常消耗性能,如果我们能在 shouldComponentUpdate 方法中能够写出更优化的 dom diff 算法,可以极大的提高性能。

pureComponent

不可变数据

key

等等优化方法,每一点的优点和缺点

如何设计一个好组件?容器组件和展示组件

合理划分组件,分为业务组件和技术组件

  • 根据组件的职责通常把组件分为 UI 组件和容器组件
  • UI 组件负责 UI 的呈现,容器组件负责管理数据和逻辑
  • 两者通过 React-Redux 提供 connect 方法联系起来

调用setState之后发生了什么

  • 首先,React 会将传入的参数对象与组件当前的状态合并,然后触发调和过程。(对比新的virtual dom树以及旧的virtual dom树,接着找出两者所不同的地方,根据不同的地方来修改现有的DOM)
  • 调和(reconciliation)的最终目标是以最有效的方式,根据这个新的状态来更新UI。为此,React将构建一个新的 React 元素树。(UI的对象表示)
  • 在得到元素树之后,为了弄清 UI 如何响应新的状态而改变,React 会将这个新树与上一个元素树相比较。自动计算出新的树与老树的节点差异,然后根据差异对界面进行最小化重渲染
  • 在差异计算算法中,React 能够相对精确地知道哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染

refs的作用

Refs 是 React 提供给我们的安全访问 DOM 元素或者某个组件实例的句柄。
我们可以为元素添加 ref 属性然后在回调函数中接受该元素在 DOM 树中的句柄,该值会作为回调函数的第一个参数返回

class CustomForm extends Component {
  handleSubmit = () => {
    console.log("Input Value: ", this.input.value)
  }
  render () {
    return (
      <form onSubmit={this.handleSubmit}>
        <input
          type='text'
          ref={(input) => this.input = input} />
        <button type='submit'>Submit</button>
      </form>
    )
  }
}

上述代码中的 input 域包含了一个 ref 属性,该属性声明的回调函数会接收 input 对应的 DOM 元素,我们将其绑定到 this 指针以便在其他的类函数中使用。
另外值得一提的是,refs 并不是类组件的专属,函数式组件同样能够利用闭包暂存其值:

function CustomForm ({handleSubmit}) {
  let inputElement
  return (
    <form onSubmit={() => handleSubmit(inputElement.value)}>
      <input
        type='text'
        ref={(input) => inputElement = input} />
      <button type='submit'>Submit</button>
    </form>
  )
}

如果你创建了类似于下面的 Twitter 元素,那么它相关的类定义是啥样子的?

<Twitter username='tylermcginnis33'>
  {(user) => user === null
    ? <Loading />
    : <Badge info={user} />}
</Twitter>

import React, { Component, PropTypes } from 'react'
import fetchUser from 'twitter'
// fetchUser take in a username returns a promise
// which will resolve with that username's data.
class Twitter extends Component {
  // finish this
}

回调渲染模式:这种模式中,组件会接收某个函数作为其子组件,然后在渲染函数中以 props.children 进行调用

这种模式的优势在于将父组件与子组件解耦和,父组件可以直接访问子组件的内部状态而不需要再通过 Props 传递,这样父组件能够更为方便地控制子组件展示的 UI 界面

譬如产品经理让我们将原本展示的 Badge 替换为 Profile,我们可以轻易地修改下回调函数

import React, { Component, PropTypes } from 'react'
import fetchUser from 'twitter'
class Twitter extends Component {
  state = {
    user: null,
  }
  static propTypes = {
    username: PropTypes.string.isRequired,
  }
  componentDidMount () {
    fetchUser(this.props.username)
      .then((user) => this.setState({user}))
  }
  render () {
    return this.props.children(this.state.user)
  }
}

何为 Children

  • 在JSX表达式中,一个开始标签(比如)和一个关闭标签(比如)之间的内容会作为一个特殊的属性props.children被自动传递给包含着它的组件
  • 这个属性有许多可用的方法,包括 React.Children.map,React.Children.forEach, React.Children.count, React.Children.only,React.Children.toArray

展示组件(Presentational component)和容器组件(Container component)之间有何不同

  • 展示组件关心组件看起来是什么。展示专门通过 props 接受数据和回调,并且几乎不会有自身的状态,但当展示组件拥有自身的状态时,通常也只关心 UI 状态而不是数据的状态
  • 容器组件则更关心组件是如何运作的。容器组件会为展示组件或者其它容器组件提供数据和行为(behavior),它们会调用 Redux actions,并将其作为回调提供给展示组件。容器组件经常是有状态的,因为它们是(其它组件的)数据源

类组件(Class component)和函数式组件(Functional component)之间有何不同

  • 类组件不仅允许你使用更多额外的功能,如组件自身的状态和生命周期钩子,也能使组件直接访问 store 并维持状态
  • 当组件仅是接收 props,并将组件自身渲染到页面时,该组件就是一个 '无状态组件(stateless component)',可以使用一个纯函数来创建这样的组件。这种组件也被称为哑组件(dumb components)或展示组件

(组件的)状态(state)和属性(props)之间有何不同

  • State 是一种数据结构,用于组件挂载时所需数据的默认值。State 可能会随着时间的推移而发生突变,但多数时候是作为用户事件行为的结果.它是私有的
  • Props(properties 的简写)则是组件的配置。props 由父组件传递给子组件,并且就子组件而言,props 是不可变的(immutable)。组件不能改变自身的 props,但是可以把其子组件的 props 放在一起(统一管理)。Props 也不仅仅是数据--回调函数也可以通过 props 传递

何为受控组件(controlled component)

  • 在 HTML 中,类似 <input>, <textarea><select> 这样的表单元素会维护自身的状态,并基于用户的输入来更新。当用户提交表单时,前面提到的元素的值将随表单一起被发送。
  • 但在 React 中会有些不同,包含表单元素的组件将会在 state 中追踪输入的值,并且每次调用回调函数时,如 onChange 会更新 state,重新渲染组件。一个输入表单元素,它的值通过 React 的这种方式来控制,这样的元素就被称为"受控元素"。
handleChangeValue = (e) => {
  this.setState({
    inputValue: e.target.value
  })
}

<Input 
  placeholder="请输入"
  value={inputValue.trim()}
  onChange={this.handleChangeValue}
  onPressEnter={this.handleSearch}
/>

高阶组件是什么和常见的高阶组件

高阶组件是一个以组件为参数并返回一个新组件的函数。
HOC 运行你重用代码、逻辑和引导抽象。
最常见的可能是 Redux 的 connect 函数和antD的Form.create()组件。
除了简单分享工具库和简单的组合,HOC 最好的方式是共享 React 组件之间的行为。如果你发现你在不同的地方写了大量代码来做同一件事时,就应该考虑将代码重构为可重用的 HOC

为什么建议传递给 setState 的参数是一个 callback 而不是一个对象

因为 this.props 和 this.state 的更新可能是异步的,不能依赖它们的值去计算下一个 state。而通过callback的第一个参数可以拿到上一次的prevState,此时的prevState也是合并了前面多次setState计算的结果

setState第二个参数支持回调函数,在回调里state是最新的。并且回调的执行时机在于state合并处理之后

怎么立即获取到修改后的state呢?可以通过 setState 中传递函数的方式及回调去实现

state = {
  count: 0
}

componentDidMount(){
  this.setState({ count: this.state.count + 1 })
  this.setState({ count: this.state.count + 1 })
  console.log("console: " + this.state.count) // 0
  this.setState(prevState => {
    console.log("console from func: " + prevState.count); // 1
    return {
      count: prevState.count + 1
    };
  }, ()=>{
    console.log('last console: '+ this.state.count) // 2
  })
}

如果只是通过回调去实现,只能立即获取上一步修改后的结果

state = {
  count: 0
}

componentDidMount(){
  this.setState({ count: this.state.count + 1 })
  this.setState({ count: this.state.count + 1 })
  console.log("console: " + this.state.count) // 0
  this.setState({ count: this.state.count + 1 }, () => {
    console.log("console from callback: " + this.state.count); // 1
  })
}

demo-01:

state = {
  count: 0
}

componentDidMount(){
  this.setState({ count: this.state.count + 1 })
  console.log("console: " + this.state.count) // 一:0
  this.setState({ count: this.state.count + 1 }, () => {
    console.log("console from callback: " + this.state.count); // 四:2
  })
  this.setState(prevState => {
    console.log("console from func: " + prevState.count); // 三:1
    return {
      count: prevState.count + 1
    };
  }, ()=>{
    console.log('last console: '+ this.state.count) // 五:2
  })
  console.log("console-end: " + this.state.count) // 二:0
}

demo-02:

state = {
  count: 0
}

componentDidMount(){
  this.setState({ count: this.state.count + 1 })
  this.setState({ count: this.state.count + 1 })
  console.log("console: " + this.state.count) // 一:0
  this.setState(prevState => {
    console.log("console from func: " + prevState.count); // 三:1
    return {
      count: prevState.count + 1
    };
  }, ()=>{
    console.log('last console: '+ this.state.count) // 四:1
  })
  this.setState({ count: this.state.count + 1 }, () => {
    console.log("console from callback: " + this.state.count); // 五:1
  })
  console.log("console-end: " + this.state.count) // 二:0
}

React 其实会维护着一个 state 的更新队列,每次调用 setState 都会先把当前修改的 state 推进这个队列,在最后,React 会对这个队列进行合并处理,然后去执行回调。根据最终的合并结果再去走下面的流程(更新虚拟dom,触发渲染)

setState为什么要设计成异步的?react的setState同步还是异步?

  • 保证内部的一致性:即使state是同步更新,props也不是。(你只有在父组件重新渲染时才能知道props)
  • 将state的更新延缓到最后批量合并再去渲染对于应用的性能优化是有极大好处的,如果每次的状态改变都去重新渲染真实dom,那么它将带来巨大的性能消耗

setState并不是真正意义上的异步操作,它只是模拟了异步的行为。React中会去维护一个标识(isBatchingUpdates),判断是直接更新还是先暂存state进队列。setTimeout以及原生事件都会直接去更新state,因此可以立即得到最新state。而合成事件和React生命周期函数中,是受React控制的,其会将isBatchingUpdates设置为 true,从而走的是类似异步的那一套

state = {
  count: 0
}

componentDidMount(){
  this.setState({ count: this.state.count + 1 })
  this.setState({ count: this.state.count + 1 })
  console.log("console: " + this.state.count) // 0
  setTimeout(() => { // setTimeout中调用
    console.log("setTimeout-first: " + this.state.count); // 1
    this.setState({ count: this.state.count + 1 });
    console.log("setTimeout-end: " + this.state.count); // 2
  }, 0)
}

(在构造函数中)调用 super(props) 的目的是什么

在 super() 被调用之前,子类是不能使用 this 的,在 ES6 中,子类必须在 constructor 中调用 super()。
传递 props 给 super() 的原因则是便于(在子类中)能在 constructor 访问 this.props

如何实现异步网络请求的?应该在 React 组件的何处发起 Ajax 请求,为什么

在 React 组件中,应该在 componentDidMount 中发起网络请求。
这个方法会在组件第一次“挂载”(被添加到 DOM)时执行,在组件的生命周期中仅会执行一次。
如果在之前发起请求,可能在组件挂载之前 Ajax 请求已经完成,如果是这样,也就意味着你将尝试在一个未挂载的组件上调用 setState,将不起作用。在 componentDidMount 中发起网络请求将保证这有一个组件可以更新了。

react16新特性

尤其理解time slice和suspense

在 React 当中 Element 和 Component 有何区别

简单地说,一个 React element 描述了你想在屏幕上看到什么。换个说法就是,一个 React element 是一些 UI 的对象表示。

一个 React Component 是一个函数或一个类,它可以接受输入并返回一个 React element t(通常是通过 JSX ,它被转化成一个 createElement 调用)

路由实现原理

react-router等一众路由插件实现的功能是更新页面的视图,但是却不重新请求页面,也就是说,其实,他们并没有实际进行了跳转,而是修改了页面的DOM并通过修改页面的URL来模拟跳转
在HTML5之前,页面路由只有hash模式,而HTML5中History对象的新增方法,带来了另一种模式:history模式

hash模式:
在HTML5之前,vue-router是通过修改URL的hash值来达到修改页面URL并生成历史记录,虽然不会重新请求页面,但会发现路径前总会有一个#,比如http://localhost:8080/#/b
因为没开启history模式的情况下,vue-router是通过hashchange事件来监听URL中hash的改变并通过修改hash来模拟路径的变化
hash模式最大的优点是兼容性强,可以兼容一众老式浏览器。而它最大的缺点是,页面URL中一直挂着一个难看的#

history模式:
HTML5发布后,又有了history模式
vue-router的history模式就是通过HTML5中History对象的pushState方法进行模拟的
HTML5还提供了一个popstate事件,当用户点击前进、后退按钮,或者调用back、forward、go方法时触发,可以监听URL的改变
不仅如此,还可以使用pushState的第一个参数进行传值,使用History的state属性进行取值。
但是目前只有兼容了HTML5的浏览器(IE10+)才能使用history模式

简述 flux **

Flux 的最大特点,就是数据的"单向流动"。

  • 用户访问 View
  • View 发出用户的 Action
  • Dispatcher 收到 Action,要求 Store 进行相应的更新
  • Store 更新后,发出一个"change"事件
  • View 收到"change"事件后,更新页面

React 项目用过什么脚手架

  • creat-react-app
  • umijs

redux 有什么缺点

  • 一个组件所需要的数据,必须由父组件传过来,而不能像 flux 中直接从 store 取。
  • 当一个组件相关数据更新时,即使父组件不需要用到这个组件,父组件还是会重新 render,可能会有效率影响,或者需要写复杂的 shouldComponentUpdate 进行判断

组件间通信

React key是干嘛的?

Keys 是 React 用于追踪哪些列表中元素被修改、被添加或者被移除的辅助标识

  • 在开发过程中,我们需要保证某个元素的 key 在其同级元素中具有唯一性
  • 在 React Diff 算法中 React 会借助元素的 Key 值来判断该元素是新近创建的还是被移动而来的元素,从而减少不必要的元素重渲染
  • 此外,React 还需要借助 Key 值来判断元素与本地状态的关联关系

说一下 redux ,redux、react-redux等原理

#34

  • redux 是一个应用数据流框架,主要是解决了组件间状态共享的问题,原理是集中式管理
  • 主要有三个核心方法,action,store,reducer

基本**

整个应用的 state 保持在一个单一的 store 中
改变应用 state 的唯一方式是在应用中触发 actions,然后为这些 actions 编写 reducers 来修改 state

store

Store 是一个 javascript 对象,它保存了整个应用的 state。与此同时,Store 也承担以下职责:

  • 允许通过 getState() 访问 state
  • 运行通过 dispatch(action) 改变 state
  • 通过 subscribe(listener) 注册 listeners
  • 通过 subscribe(listener) 返回的函数处理 listeners 的注销

action

Actions 是一个纯 javascript 对象,它们必须有一个 type 属性表明正在执行的 action 的类型。实质上,action 是将数据从应用程序发送到 store 的有效载荷

reducer

一个 reducer 是一个纯函数,该函数以先前的 state 和一个 action 作为参数,并返回下一个 state

Redux Thunk 的作用是什么

Redux thunk 是一个允许你编写返回一个函数而不是一个 action 的 actions creators 的中间件。如果满足某个条件,thunk 则可以用来延迟 action 的派发(dispatch),这可以处理异步 action 的派发(dispatch)

何为纯函数(pure function)

一个纯函数是一个不依赖于且不改变其作用域之外的变量状态的函数,这也意味着一个纯函数对于同样的参数总是返回同样的结果

combineReducers

combineReducers 函数主要用来接收一个对象,将参数过滤后返回一个函数。该函数里有一个过滤参数后的对象 finalReducers,遍历该对象,然后执行对象中的每一个 reducer 函数,最后将新的 state 返回

介绍Redux数据流的流程

view 到 redux 的过程中会派发一个 action , action 通过 Store 的 dispatch 方法,会派发给 store , store接收到 action ,再连同之前的 state 一起传给 reducer , reducer 返回一个新的数据给 store , store 就可以去改变自己的 state ,组件接收到新的 state 就可以重新渲染页面了

其中,redux有三个基本属性,Action,Reducer,Store

  • Action 是把数据从应用传到 store 的有效载体,也是 store 数据的唯一来源,用法是通过 store.dispatch() 把 action 传到 store。(Action 只是描述了有事情发生了这一事实,并没有指明应用如何更新 state。)
  • 这是 reducer 要做的事情。reducer 负责管理整个业务里边的数据,包括处理数据,存储数据等。刚刚说了store.dispatch() 会把 action 传到 store ,但是store并不知道怎么处理这个数据,需要去 reducer 进行查找,所以需要把当前 store 里存在的数据和接收的 action 转给 reducer , reducer 处理好了之后再转给 store。reducer 返回的必须是一个函数,这个函数里面接收两个参数,一个是 state ,另一个是 action。
  • 既然可以使用 action 来描述“发生了什么”,使用 reducers 来根据 action 更新 state 的用法。那Store 就是把它们联系到一起的对象,它负责存储项目应用中的所有数据,通过redux的createStore()方法创建store公共数据存储区域,然后把 reducer 传给 store,就可以在 reducer 中查看数据并做处理,const store = createStore(reducer)
    Store提供了一些方法。让我们很方便的操作数据:
  1. 维持应用的 state
  2. 提供 getState() 方法获取 store 的数据内容state
  3. 提供 dispatch(action) 方法派发 action ,更新 state
  4. 通过 subscribe(listener) 注册监听器,订阅 store 的改变,只要 store 发生改变, store.subscrible 这个函数接收的这个回调函数就会被执行

Redux如何实现多个组件之间的通信,多个组件使用相同状态如何进行管理

react-redux(Provider 传入到最外层组件store 在需要用到的地方 用 connect 获取 (mapStateToProps, mapDispatchToProps) 在页面中引用)
类似发布订阅模式, 一个地方修改了这个值, 其他所有使用了这个相同状态的地方也会更改

const App = (
  <Provider store={store}>
    <TodoList />
  </Provider>
)
  • Provider 是 react-redux 的一个核心API,连接着 store , Provider 里边所有的组件,都有能力获取到 store 里边的内容
  • react-redux 的另一个核心方法叫做 connect ,接收三个参数,最后一个参数是连接的组件,前面两个是连接的规则,分别是 mapStateToProps 和mapDispatchToProps方法。mapStateToProps翻译为中文就是把 store 里的数据state映射到组件props这个位置,为组件的 props 的数据, mapDispatchToProps可以把store.dispatch 挂载到props上。因为想要改变 store 里的内容,就要调用 dispatch 方法, dispatch 方法被映射到了 props 上,所以就可以通过 this.props.dispatch 方法去调用了。
  • 当我们在页面进行操作时,会执行某些事件方法,比如增删改查,会改变数据的状态,todolist 组件恰好通过 connect 跟数据做了连接,所以这块就变成了自动的流程,数据一旦发生改变,这个组件自动就会跟的变化。以前还需要 store.subscribe 做订阅,现在连订阅都可以不用了,页面自动响应数据发生变化
  • connect 方法可以这样理解,当你用 connect 把这个 UI 组件和一些数据和逻辑相结合时,返回的内容实际就是一个容器组件了,容器组件可以理解成数据处理包括派发这样的业务逻辑。所以 export default 导出的内容就是 connect 方法执行的结果,是一个容器组件
const mapStateToProps = (state) => {
  return {
    inputValue: state.inputValue
  }
}
const mapDispatchToProps = (dispatch) => {
  return {
    handleInputChange(e) {
      const action = {
        type: 'change_input_value',
        value: e.target.value
      }
      dispatch(action)
    }
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(TodoList)

多个组件之间如何拆分各自的state,每块小的组件有自己的状态,它们之间还有一些公共的状态需要维护,如何思考这块

在实际项目开发中,如果 reducer 存放过多的数据,可能会造成代码的不可维护,我们需要把总的 reducer 拆分为多个子的 reducer,然后再做一个整合。
一个全局的 reducer , 页面级别的 reducer , 然后redux 里有个 combineReducers 把所有的 reducer 合在一起,小组件的使用与全局的使用是分开的互不影响

import { combineReducers } from 'redux'
import headerReducer from '../common/header/store/reducer'

export default combineReducers({
  header: headerReducer
})

然后在页面中通过state.header.focused访问

使用过的Redux中间件

react-redux
React-Redux是 redux官方提供的 React 绑定库,可以在 react 中非常方便是使用 redux
React-redux有两个核心方法。一个是Provider,另一个是connect

redux-thunk(把action 返回的对象 换成一个异步函数) :

import { createStore, applyMiddleware } from 'redux'
import reducer from './reducer'
import thunk from 'redux-thunk'

const store = createStore(
  reducer,
  applyMiddleware(thunk) // applyMiddleware可以使用中间件模块
) 
export default store

因为reducer 必须是纯函数,是不能进行ajax这种异步操作的,而在组件中直接进行ajax异步操作或者复杂的业务逻辑处理,组件会显得过于臃肿。
有了redux-thunk,就可以将ajax异步请求或者是复杂的逻辑放到 action 去处理,原则上 action 返回的是一个对象,但当我们使用 redux-thunk 中间件后, action 就可以返回一个函数了,继而可以在函数里边进行异步操作,也就可以把组件中获取数据的请求放入这个函数中了

export const getTodoList = () => {
  return (dispatch) => {
    axios.get('/list.json').then((res) => {
      const data = res.data
      const action = initListAction(data)
      dispatch(action)
    })
  }
}

componentDidMount() {
  const action = getTodoList()
  store.dispatch(action) // 调用 store.dispatch()这个函数时,action这个函数就会被执行
}

redux-saga
虽然redux-thunk可以接受函数作为action,但函数的内部如果有多个异步操作,就需要为每一个异步操作都定义一个action,异步操作太为分散而不易维护
这个时候就可以用到redux-saga。 redux-saga也是做异步代码拆分的,可以完全替代 redux-thunk 。
要通过redux-saga的createSagaMiddleware()方法创建saga中间件,还需要有一个sagajs文件,这样通过sagaMiddleware.run(mySaga)来运行这个sagas文件,这样当组件dispatch一个action的时候,不仅reducer可以接收到,sagas文件中也可以接收到了

import { createStore, applyMiddleware } from 'redux'
import reducer from './reducer'
import createSagaMiddleware from 'redux-saga'
import mySaga from './sagas'

const sagaMiddleware = createSagaMiddleware() // 创建saga中间件
const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
) 
sagaMiddleware.run(mySaga)
export default store

sagajs导出的是个生成器函数,通过在生成器函数中使用takeEvery方法,捕捉派发出来的所有 action。takeEvery有两个参数,第一个参数时action type,第二个参数是函数,一旦接收到符合条件的 action type,就执行第二个参数方法,所以就可以把异步逻辑写到这个方法里集中处理了,这个方法中有一个put方法也可以派发action操作。类似于 redux 原始的 dispatch,都可以发出 action,且发出的 action 都会被 reducer 监听到

import { takeEvery, put  } from 'redux-saga/effects'
import axios from 'axios'

function* getInitList() {
  const res = yield axios.get('/list.json')
  const action = initListAction(res.data)
  yield put(action)
}
function* mySaga() {
  yield takeEvery(GET_INIT_LIST, getInitList)
}
export default mySaga

redux-saga 远比 redux-thunk 复杂的多, redux-saga 里边有非常多的api,我们只用了 takeEvery 、 put ,官方文档中还有很多我们经常用到的 call 、 takeLatest 等,在处理大型项目时, redux-saga 是要优于 redux-thunk 的;但是从另一角度来说, redux-thunk 几乎没有任何 api ,特点就是在 action 里面返回的内容不仅仅是个对象,还可以是个函数

只要使用了中间件,就需要使用applyMiddleware()函数,顾名思义,是运行中间件的函数。它是Redux 的原生方法,作用是将所有中间件组成一个数组,依次执行。
中间件就是指的 dispatch 方法的一个封装或者升级。(redux-thunk中间件:对 dispatch 方法做了封装之后,既可以接收对象,又可以接收函数了)所以中间件本质上只是拓展了store.dispatch方法

关于flex布局我们应该熟悉的

前言

CSS3最喜欢的新属性之一便是flex布局属性,用六个字概括便是简单、方便、快速

Flex 是 Flexible Box 的缩写,意为"弹性布局"

简单来说,就是通过给元素盒子设置一些属性,让元素盒子具有伸缩性

有别于之前的display+position+float布局,flex布局可以简便、完整、响应式地实现各种页面布局

设为 Flex 布局以后,子元素的float、clear和vertical-align属性将失效

概念

首先我们来看下下面这张图,这张图我相信了解过flex布局的人都应该见过

image

采用flex布局的弹性盒子,称为flex容器(flex container),简称“容器”

盒子里边的子元素称flex项目(flex item),简称“项目”

上图中我们还能看到容器默认存在两根轴:

水平的主轴(main axis)

垂直的交叉轴(cross axis)

主轴和交叉轴的位置是可以变换的,依赖于flex-direction,默认方向是row

属性

flex属性主要基于两块,一块是使用flex布局的盒子容器,另一块是其子元素

基于盒子容器

  • flex-direction : 主轴方向(子项目的排列方向)
  • flex-wrap : 换行 默认nowrap
  • flex-flow : flex-direction属性和flex-wrap属性的简写 默认row nowrap
  • justify-content : 项目在主轴上的对齐方向
  • align-items : 项目在交叉轴上如何对齐
  • align-content : 多根轴线的对齐方式

基于子元素项目

  • order : 项目的排列顺序
  • flex-grow : 项目的放大比例
  • flex-shrink : 项目的缩小比例
  • flex-basis : 项目占据的主轴空间
  • flex : flex-grow, flex-shrink 和 flex-basis的简写
  • align-self : 单个项目的对齐方式

接下来的内容主要是针对在项目中使用频率比较高的属性

flex-direction 主轴方向(子项目的排列方向)

默认值是row水平方向,起点在左

与之相反的就是column垂直方向,起点在上

还有两个属性值为row-reversecolumn-reverse,与row和column的区别就在于在于起点相反

有点类似于行内元素不换行与块级元素的换行

justify-content 项目在主轴上的对齐方向

justify-content 是一个非常重要的属性,它的5个属性值都是我们在项目中经常用到的,分别是:

  • flex-start(默认值):左对齐

  • flex-end:右对齐

  • center: 居中

  • space-between:两端对齐,项目之间的间隔都相等

  • space-around:每个项目两侧的间隔相等

align-items 项目在交叉轴上如何对齐

同justify-content一样,也是非常重要的属性,同样也有5个属性值,分别是:

  • flex-start:交叉轴的起点对齐

  • flex-end:交叉轴的终点对齐

  • center:交叉轴的中点对齐

  • baseline: 项目的第一行文字的基线对齐

  • stretch(默认值):如果项目未设置高度或设为auto,将占满整个容器的高度

demo

针对于上面的两种属性,写了个简单的demo,我们可以看下效果,也方便添加后面属性的对比

html
<div class="box">
    <div class="item"></div>
    <div class="item" id="item2"></div>
    <div class="item" id="item3"></div>
    <div class="item"></div>
</div>
css
.box {
    width: 700px;
    height: 400px;
    background-color: #EEB4B4;
    display: flex;
    justify-content: space-around;   
}
.item {
    width: 150px;
    height: 150px;
    font-size: 100px;
    border: 1px solid green;
    display: flex;
    justify-content: center;
    align-items: center;
}
效果

image

flex-grow 项目的放大比例

取值大致有三种情况:

默认值0,即使还存在剩余空间也不放大

所有子项目item取值都为1(或其他相同值),将等分剩余空间(如果有的话)

子项目item取值不相同,比如一个为2,其余都是1,那么属性值为2的item占据的剩余空间是属性值为1的2倍

demo

我们先来设置flex-grow属性值为1,效果如下:

.item{
  ...
  flex-grow: 1;
}

image

将id为item2和item3的flex-grow属性值设置为2

#item2,#item3 {
   flex-grow: 2;
}

image

flex-shrink 项目的缩小比例

这个属性的效果与flex-grow是相反的,取值也有三种情况:

默认为1,即如果空间不足,该项目将缩小

所有项目的flex-shrink属性都为1,当空间不足时,都将等比例缩小

子项目item取值不相同,比如一个项目的flex-shrink属性为0,其他项目都为1,则空间不足时,前者不缩小

demo

先增加子项目item的数量

.box {
    ...
    <div class="item">五</div>
    <div class="item">六</div>
     <div class="item">七</div>
}

然后看flex-shrink的默认值,也就是属性值为1的情况,效果如下:

image

将id为item2和item3的flex-shrink属性值设置为0

#item2,#item3 {
   flex-shrink: 0;
}

image

所以,从flex-growflex-shrink的默认值我们可以知道

flex弹性盒子在存在剩余空间的情况下是默认不具有伸缩性的,在不存在剩余空间的情况下是默认收缩的

flex-basis 定义项目占据的主轴空间(在有剩余空间的情况下)

默认值为auto,项目本身的大小,也就是说从子项目结束后那个位置开始分配项目空间

属性值为width或height属性一样的值(比如350px),代表项目将占据的空间,也就是说从子项目开始的位置分配项目空间

demo

再次将子项目item的数量设为四个,基于最上面的样式设置,给第二个item设置width:200pxflex-basis就用默认值

#item2 {
   width: 200px;
   height: 150px;
}

image

然后将子项目.item{...}添加样式flex-basis:170px;,设置具体的数值

image

可以看到是从子项目开始的位置,重新分配项目占据空间

flex flex-grow, flex-shrink 和 flex-basis的简写

同样按上面的样式设计,有四种取值情况:

默认值是0 1 auto,代表项目在有剩余空间的情况下,不具有伸缩性;在没有剩余空间的情况下,默认压缩,且从子项目结束后那个位置开始分配项目空间

image

属性值是auto (1 1 auto) ,从子项目结束的位置分配剩余空间

image

属性值是none (0 0 auto),不分配剩余空间

image

属性值是数值(比如1),这三个值分别是

image

从子项目开始的位置分配剩余空间,同flex-basis:auto的情况相反

image

通常在项目中,我们往往是在同级子项目item宽度或高度固定的情况下,让其他子项目占据剩余空间时使用flex:1

比如,在上述情况下我们只取两个子项目item,其中一个item宽度和高度固定,另一个item设置flex:1,如下;

html
<div class="box">
    <div class="item">一</div>
    <div class="item" id="item2">二</div>
</div>
css
.box {
    width: 700px;
    height: 400px;
    background-color: #EEB4B4;
    display: flex;
    justify-content: center;   
}
.item {
    width: 150px;
    height: 150px;
    font-size: 100px;
    border: 1px solid green;
    display: flex;
    justify-content: center;
    align-items: center;
}
.item2 {
    flex: 1;
}

image
可以看到设置了flex:1的子项目占据了所有的剩余空间,并始终从头布局(父元素的justify-content: center; 可以相当于未生效)

封装

在项目开发中,我们尽量做到自己封装好flex这些常用的布局方法,比如封装到一个全局样式中,然后在之后的页面css中引入全局样式,具体可以查阅我之前用flex实现的H5页面'头尾固定,中间滚动'的项目,在该项目的global.css中就是我对flex布局常用属性的兼容性的全局封装

.flex {
    display: flex;
    /* 兼容性 */
    display:-webkit-box;
    display: -moz-box;
    display: -ms-flexbox;
    display: -webkit-flex;
}

.direction-column {
    -webkit-box-orient:vertical;
    -webkit-box-direction:normal;
    -moz-box-orient:vertical;
    -moz-box-direction:normal;
    flex-direction: column;
    -webkit-flex-direction:column;
}

以上情况的处理往往是我们写单个网页时。在项目中常常用插件来自动实现兼容性的处理

但是为了方便,在项目中多次用flex布局时,我们也可以对齐进行封装,比如vue,可以参考博客如果说 Flexbox 之前的布局都是错的...

请求超时优化 异步数据加载

场景

页面上有多个box,每个box下都会请求数据内容

用户可以通过表单保存已选择的条件名和内容

目的

将首屏加载时同时请求数据的情况修改为首屏加载只加载上次用户保存的表单,其他box下的请求通过点击后异步加载

对首屏数据加载和异步数据加载设置超时请求处理,再次加载数据,渲染页面

方法

请求已保存条件数据的函数

function getData(url , requestData) {
    return new Promise(resolve,reject) => {
        $.get(url, requestData).done(function(data) {
            if (data.status ==1) {
                reject(new Error(data.message))
             } else {
                if (data.code === codeConfig.SUCCESS) {
                    resolve(data.data)
                } else if (data.code == null) {
                    resolve(data)
                }
             }
        }) 
   }
 }

promise请求数据超时处理

定义一个延时函数,用于给请求计时,返回promise

function timeout(delay) {
    return new Promise((resolve,reject) => {
        setTimeout(function(){
            reject("数据请求超时,请重试")
        },delay)
    })
}

在路由接口处通过Promise.race方法,Promise的all方法和race方法是相对的,all方法是[谁跑的慢,以谁为准执行回调],race则是[谁跑的快,以谁为准执行回调]。所以通过race给异步请求数据设置超时时间,超时后执行相应的操作

exports.list = function(type,identifier) {
    var timeout = commonUtil.timeout(5000)
    var promise = commonUtil.getData(url,{
        type:type,
        user:window.global_user_name
    })
    Promise
    .race([promise,timeout])
    .then (function(results){
       return results
     })
     .catch(function(reason){
        commonUtil.showMsg(reason)
     })
 }

模仿网络延时的方法

这里推荐一种模仿网络延时的方法:chrome开发者工具的network下有个throtting节流工具

150297072878

可以点Add自定义节流时间

150297073866

我测试时在promise延时函数中设置的时间为500ms,这里节流用的800ms

150297074452

设置好之后选中,Network会出现黄色的警告

15029707482

这样可以在首次加载时选择节流,点击已保存条件时会出现请求数据失败的情景,然后再去掉节流,点击重试按钮,就可以发出去请求,正常请求到数据了,然后就可以渲染到页面

在网络延时复现不了的情况下,很好的模拟了首次请求失败,点击刷新重新请求数据的情景

定制化React三级菜单组件的创建与封装

由于项目的需要,最近开发了一些定制化的react组件,这篇文章主要介绍下呼出三级菜单的实现原理,支持鼠标悬浮呼出和鼠标点击呼出

image

事件机制

在事件机制中,主要利用鼠标的一些事件来监听,具体如下:

利用onClick(鼠标点击),onMouseOver(鼠标进入),onMouseLeave(鼠标离开)来监听鼠标的变化,同时在state状态机中定义控制菜单出现或消失的状态标识,然后通过这些鼠标事件来改变相应的状态值

class Hovermenu extends PureComponent {
  constructor(props) {
    super(props)
    this.state = {
      show: true,
      currentIndex: null,
      currentApplicationIndex: null,
      currentTabIndex: null,
    }
    this.handleMouseOver = this.handleMouseOver.bind(this)
    this.handleTabMouseOut = this.handleTabMouseOut.bind(this)
    this.setcurrentIndex = this.setcurrentIndex.bind(this)
    this.setoverSvg = this.setoverSvg.bind(this)
    this.setleaveSvg = this.setleaveSvg.bind(this)
    this.handleClick = this.handleClick.bind(this)
    this.handleDataMenu = this.handleDataMenu.bind(this)
  }

  componentDidMount () {
  }

  handleMouseOver(e) { 
    this.setState({
      currentApplicationIndex: parseInt(e.currentTarget.getAttribute('index'), 10),
      currentTabIndex: parseInt(e.currentTarget.getAttribute('index'), 10)
    })
  }

  handleTabMouseOut() {
    this.setState({
      currentTabIndex : null,
      currentApplicationIndex: null
    })
  }

  setcurrentIndex(e) {
    this.setState({
      currentIndex : parseInt(e.currentTarget.getAttribute('index'), 10)
    })
  }

  setoverSvg() {
    this.setState({
      show : false
    })
  }

  setleaveSvg() {
    this.setState({
      show : true
    })
  }

  handleClick() {
    this.setState({
      currentIndex : null
    })
  }

  // 处理菜单数据 将第三级菜单抽象成JSX
  handleDataMenu(arr) {
    const { currentIndex } = this.state
    return (
      <ul className={styles.card}>
        {
          arr.map((item,index) => {
            return (
              <a href='#' target="_blank" className={styles.text} key={item.name}>
                <li key={item.name} className={`${styles.item} ${currentIndex === index? styles.active : ''}`} index={index} onClick={this.setcurrentIndex} onMouseOver={this.setoverSvg} onMouseLeave={this.setleaveSvg} onFocus={() => 0}>
                  {item.name}
                </li>
              </a>
            )
          })
        }
      </ul>
    )
  }

  render () {
    const { currentTabIndex, currentApplicationIndex } = this.state
    const menuData = this.props.menu
    const title = this.props.title ? this.props.title  : '下拉菜单'
    const { trigger }= this.props
    // 一级菜单name
    const applicationArr = menuData.map(item => {
      return item.name
    })

    // 二级三级菜单数组
    const tabArr = menuData.map(item => {
      return item.children
    })

    // 一级菜单JSX
    const applicationList = (
      applicationArr.map((item,index) => {
        if ( trigger === 'click' ) {
          return (
            <div key={item} className={`${styles.item} ${currentApplicationIndex === index? styles.active : ''}`} index={index} onClick={this.handleMouseOver} onFocus={() => 0}>
              <div className={styles.title}>{item}</div>
            </div>
          )
        } else {
          return (
            <div key={item} className={`${styles.item} ${currentApplicationIndex === index? styles.active : ''}`} index={index} onMouseOver={this.handleMouseOver} onFocus={() => 0}>
              <div className={styles.title}>{item}</div>
            </div>
          )
        }
      })
    )

    // 二三级菜单分层处理
    const tabList = (
      tabArr.map((item,index) => {
        return (
          <div key={item[0].id} className={`${styles.item} ${currentTabIndex === index? styles.layer : styles.hidden}`} index={index} onMouseOver={this.handleMouseOver} onMouseOut={this.handleTabMouseOut} onClick={this.handleClick} onFocus={() => 0} onBlur={() => 0}>
            <Tabs defaultActiveKey="1" onChange={this.callback}>
              {
                item.map((element) => {
                  return <TabPane key={element.id} tab={element.name}>{this.handleDataMenu(element.children)}</TabPane>
                })
              }
            </Tabs>
          </div>
        )
      })
    )

    return (
      <Card title={title} bordered={false} className={styles.main}>
        <div className={styles.content}>
        {applicationList}
        </div>
        {tabList}
      </Card>
    )
  }
}

export default Hovermenu

样式设置

除了事件机制控制状态变化外,我们还需要在样式中设置父类和子类的position值,父类值为relative,子类值为absolute,同时为使悬浮菜单在最前端显示,菜单的css中需要加入层级控制z-index(数值越大,越靠前端)

同时需要注意的是,在hover判断时,需要在其中通过控制display来控制显示与否

如下面的.hidden { display: none }

.main {
  position: relative;
  .content {
    ...
  }
  .layer {
    position: absolute;
    z-index: 1050;
    left: 0;
    top: 144px;
    width: 100%;
    background-color: #f0f2f5;
    box-shadow: 0 2px 4px 0 #D9D9D9;
    border-radius: 2px;
    :global {
      .ant-tabs-nav .ant-tabs-tab {
        color: #666;
      }
    }
    .card {
      ...
    }
  }
  .hidden {
    display: none;
  }
}

封装

可见我的另一篇博客通过npm发布的第一个React组件的实践过程

Github Repo

前端React的三级菜单组件

循环中的定时器问题

前言

通过定时器循环输出不同的值

问题

一提到循环,理所当然的就想到了for循环

比如下面的代码

for (var i =0; i < 3; i++) {   
    setTimeout(function() {
        console.log(i)
    }, 1000)
}

怎么才能在每次输出i之后再执行循环呢,因为for循环执行完毕后,才执行定时器,所以结果输出了3个3

定时器只是定时开启一个新的线程,不会暂停for循环

原因

JS众所周知是单线程的,可能有人会认为在上面的例子中会先阻塞等待定时器执行完成后再执行后面的循环

为了解决单线程的缺陷,引入了异步机制

异步机制主要是利用一个我们平时很少去关注的一个知识点-浏览器的多线程

可以参考博客第三小节浏览器的多线程

所以对于上面连续打印3个3的问题,由于变量 i 直接暴露在全局作用域内,当调用 console.log 函数开始输出时,这是循环已经结束

方法

最简单的,不能用for循环,那就用函数递归

(function f(i) {
     console.log(i)
    if (++i<3)
        setTimeout(function(){f(i)}, 1000)
})(0)

就可以依次输出出0,1,2

解决这种setTimeout变量控制的方法有多种,早在stackoverflow上就有大神解释了,可以参考

大体方法有三种:

第一种是为每个定时器处理函数创建不同的“i”变量副本

function doSetTimeout(i) {
  setTimeout(function() { console.log(i) }, 1000)
}

for (var i = 0; i < 3; i++)
  doSetTimeout(i)

这里通过定义一个函数来实现中介的作用,从而创建了变量的值的副本

由于setTimeout()是在该副本的上下文中调用的,所以它每次都有自己的私有的"i"以供使用

但该方法也有它的缺陷,像上述代码块几乎是同时输出了0,1,2三个值

因为设立一些时间间隔相同的连续的setTimeout()将导致所有延时处理程序同时被调用,设置timer(对setTimeout()的调用)几乎不消耗时间

也就是说,告诉系统“请在1000毫秒后调用此函数”将会被立即返回,因为在timer队列中安装延时请求的过程非常快

可以修改为

function doSetTimeout(i,j) {
    setTimeout(function() { console.log(i) }, j*1000)
}
for (var i = 0,j=1; i < 3; i++,j++)
doSetTimeout(i,j)

第二种方法是使用bind方法

for (var i = 0, j = 1; i < 3; i++, j++) {
    setTimeout(function() {
        console.log(this)
    }.bind(i), j * 1000)
}

该方法允许显式地指定函数调用时 this 所指向的值

bind()的作用类似call和apply,都是修改this指向

但是call和apply是修改this指向后函数会立即执行,失去了定时器的作用

而bind则是返回一个新的函数,会创建一个与原来函数主体相同的新函数,新函数中的this指向传入的对象

第三种方法是使用立即执行函数给setTimeout创建一个闭包

类似于上面提到的函数递归方法

for (var i = 0; i < 3; i++) {
    (function(index) {
        setTimeout(function() {console.log(index)}, i * 1000)
    })(i)
}

因为 Javascript 只有两种作用域,一是全局作用域,二是函数作用域,它是没有块级作用域的

所以闭包的出现就相当于利用一个匿名函数的壳模拟出一个块级作用域

上述代码块往匿名函数内部传的参数将会被拷贝一份,也就是说循环没执行一次就拷贝变量 i 的值到匿名函数内部

还可以使用es6中的let变量,也是利用的块级作用域原理

for (let i =0,j=1; i < 3; i++,j++) {   
   setTimeout(function() {
       console.log(i)
   }, j*1000)
}

页面间传值方法及问题

在vue中官网中我们可以看到两种页面间传值得方法:query和params

query 跳转URL携带参数

首先在路由文件中定义类似的如下路由

{
      path:"/user",
      name:"user",
      component:user
    }

然后在页面中定义跳转路径和传值参数

this.$router.push({path:'/user',query:{userName:'ls',userId:'02'}});

我们已经了解了query传参会将参数都放到url中,所以当传递的值比较多的情况下,跳转的url会太长而受限制(取决于浏览器和服务器的限制)

所以只建议在参数较少的情况下使用query传值

params

参数不带入url中,可在参数较多时使用

同样的先在路由文件中定义类似的如下路由

{
      path:"/user",
      name:"user",
      component:user
    }

然后在页面中定义跳转路径和传值参数

this.$router.push({path:'/user',query:{userName:'ls',userId:'02'}});

问题来了,如果我们打印下面代码,结果肯定是 undefined

console.log(this.$route.params.userName )

注意,这是因为使用params传值,只能用name来引入路由,可见官网

正确的写法

this.$router.push({name:'user',query:{userName:'ls',userId:'02'}});

问题又来了,如果我们刷新页面,参数就不见了

注意,用params携带参数时,在注册

百度地图上添加选择框实现交互的相关技术整理

主要目的

在百度地图上添加一个矩形选择框(后面我们就用选择刷这个称号来代替),最终是要实现选择刷与百度地图的交互操作,能在地图上拖动选择刷时,实时读取到刷内的地图图元数据

方法介绍

在实现选择刷的过程中,主要尝试了两种方法

方法一

用jquery ui中的选择框实现,引用draggable

主要步骤

  • 计算矩形框西北角相对于地图的位置
  • 根据像素距离获取剩下三个角的坐标
  • 根据矩形框的范围判断图元是否在矩形框内

实现流程

  1. 获取可视区域内的坐标:用getBounds()函数获取可视区域四个角的经纬度坐标,取西北角的坐标转换为可视区域坐标
  2. 使用drag(event,ui)事件获取选择刷相对地图的位置,当鼠标在拖放过程时触发
  3. 获取选择刷四个角的像素坐标,利用PixelToPoint()函数转换为经纬度坐标
  4. 根据Polygon()函数获取选择刷范围
  5. 引入GeoUtils,根据isPointInPolygon()函数判断点是不是在选择刷内
    6.将刷内的点存放在数组中,此数组就是刷内的地图图元数据

方法二

直接引用百度地图API上的函数

实现步骤

  • 将可拖拽点修改为可拖拽矩形框,拖拽结束后再响应事件
  • 计算出x轴和y轴上的分辨率
  • 根据分辨率和像素距离计算地图图元的经纬度
  • 最后根据矩形框的范围判断图元是否在矩形框内

实现流程

  1. 将百度地图中可拖拽的点修改为可拖拽的矩形框
  2. 修改选择刷的样式布局
  3. 分别计算出选择刷左下角和右上角的地理经纬度坐标和像素坐标
  4. 计算分辨率:实际距离(经纬度坐标差)/像素值(像素坐标差)
  5. 根据经度方向和纬度方向的分辨率分别换算出拖拽结束后选择刷四个角的经纬度
  6. 最后再将地图上图元数据的经纬度跟选择刷经纬度对比,过滤出符合条件的图元数据即可

实现结果

相关知识点

源代码var bounds = map.getBounds() 得到的是一堆aa、bb、cc之类的值,类似运行结果图:
146632002745

矩形bounds放入坐标系中查看经纬度,一目了然:
14663200839

Bounds四个角点经纬度值分别为:

  • 左上角点LTPoint: ( bounds.Cf.lng, bounds.uf.lat)  (bounds.cc, bounds.Zb)
  • 右上角点RTPoint: (bounds.uf.lng, bounds.uf.lat) (bounds.$b, bounds.Zb)
  • 左下角点LBPoint: (bounds.Cf.lng, bounds.Cf.lat)  (bounds.cc, bounds.bc)
  • 右下角点RBPoint: (bounds.uf,lng,bounds.Cf.lat) (bounds.$b,bounds.bc)

Bounds中含有获取两个角点坐标的实例方法:

  • 左上角点LTPoint:(bounds.getSouthWest().lng,bounds.getNorthEast().lat)

  • 右上角点RTPoint:bounds.getNorthEast()

  • 左下角点LBPoint:bounds.getSouthWest()

  • 右下角点RBPoint:(bounds.getNorthEast().lng,bounds.getSouthWest().lat)

获取矩形框四个角的坐标(交互)

根据drag(event,ui)事件,当鼠标在拖动过程中移动时触发:
146632013472

源代码

$(#draggable”).draggable({
   Drag:function(event,ui){
   Console.log(ui.position) //position是相对于父元素的位置
   Console.log(ui.offset) //offset是相对于浏览器窗口}
})

根据构造函数BMap.Pixel(ui.position.top,ui.position.left)创建像素点对象实例,像素坐标的坐标原点为地图区域的左上角。

百度地图使用经纬度地理坐标(lng,lat)和像素坐标(x,y)两种,同时有两种坐标转换的方法:

经纬度装换为地理坐标的方法为:

PointToPixel(point:point) 返回值:Pixel 描述:经纬度坐标转换为像素坐标

地理坐标转换为经纬度的方法为:

pixelToPoint(pixel:Pixel) 返回值:point 描述:像素坐标转换为经纬度坐标

判断点是否在多边形内

引入BMapLib.GeoUtils静态类库

<script type="text/javascript" src="http://api.map.baidu.com/library/GeoUtils/1.2/src/GeoUtils_min.js"></script>

源代码

146632018435

类BMapLib.GeoUtils方法介绍

首先根据矩形框每个角的坐标确定多边形范围:Var ply = new BMap.Polygon(polyPoints);再用forEach函数循环地图上存放的所有点数,将点逐个与矩形框范围作比较,判断结果为true时将该点存放在数组中

直接判断是否在矩形框的边线内

分别用覆盖点的纬度与东北点纬度、西南点纬度比较,覆盖点的经度与西南角经度、东北角经度比较

Python操作Mongodb

前言

MongoDB 是一个基于分布式文件存储的数据库,可以为为WEB 应用提供可扩展的高性能数据存储解决方案

为了更好熟悉数据库接口数据的存储,我们也需要了解mongodb是存入数据的整体流程

安装

Mongodb Coummuinity

PyCharm Coummuinity3.6

Python3.6(mac自带的是2.0版本的)

操作

启动MongoDB(macOS)

mongoDB的启动需要我们先开启服务器,然后再开启客户端

开启服务器:终端输入 sudo mongod(如果已经配置了环境可以不用加sudo)

image

开启客户端:在另一个终端中输入sudo mongo

image

出现这样的的界面就代表mongodb就启动成功了

可能启动的时候会报类似于没有权限的错误,可以自行百度,这里我就不加赘述了

PyCharm连接mongodb

首先在设置里边下载Mongo Explorer插件

image

然后选中如下图红框所示的按钮

image

搜索Mongo Plugin 安装即可

image

安装好之后我们重新回到图2的界面,可以看到我们已安装好的插件

image

然后在Pycharm界面就可以查看我们的mongodb数据库了

image

如上图代码所示,我们新建了一个数据库和表集合

在已经启动服务的客户端查看如下

image

O啦,到此基本的建数据库表流程就跑通了~

关于CSS3动画效果背后的实现

我相信很多前端开发人员都有用CSS3写过动画的经历,甚至创建过一些比较复杂的2D或3D动画效果

在这篇文章,主要通过案例来介绍一些常见的CSS3的动画属性

先来看下常见的CSS3的动画属性都有哪些

image

转换transform

.box {
     width: 200px;
     height: 200px;
     background-color: red;
     transform: translate(400px);
     transform: rotate(45deg);
     transform: scale(2);
}
.box:hover {
     transform: rotate(720deg) scale(2);
}
.....
<div class="box"></div>

过渡transition

.box {
     width: 200px;
     height: 200px;
     background-color: red;
     transition-property: all; 
     transition-delay: 1s;
     transition-duration: 4s;
     transition-timing-function: linear;
     /*transition: all 4s 1s linear*/
}
.box:hover {
     transform: rotate(720deg) scale(2);
}
.....
```html
<div class="box"></div>

animation动画

.box {
     width: 200px;
     height: 200px;
     background-color: red;
     animation-name: box;
     animation-duration: 4s;
     animation-delay: 1s;
     animation-timing-function: linear;
     animation-iteration-count: 1;
     animation-fill-mode: both;
     /*animation: box 4s 1s linear 1 both;*/
}
@keyframes box {
     0%{
         width: 0;
      }
      25%{
          width: 200px;
       }
       50%{
           width: 400px;
       }
       5%{
           width: 600px;
        }
        100%{
            width: 800px;
        }
 }
.box:hover {
     animation-play-state: paused;
}
.....
<div class="box"></div>

综合案例可以参考demo

移动端1px问题

前言

开发过移动端的应该都遇到过css里边写的1px实际上看着比1px粗,由于devicePixelRatio的存在,移动端永远无法使用border属性实现一个统一的1px细线。详细原因可以参考大漠的文章,也可以参考博客

1px细线的处理方法有多种,下面主要介绍现在主流的方法

伪元素:after

为什么说是主流方法呢?

可以参考现在比较受欢迎的移动端ui样式库,比如微信的WeUI button

image的1px border

如蚂蚁金服的(Ant Design Mobile组件库)[https://mobile.ant.design/components/button-cn/]

image

上面的图片如果看不清晰,我们可以具体分析下WeUI的实现原理

weui-btn:after {
    content: " ";
    width: 200%;
    height: 200%;
    position: absolute;
    top: 0;
    left: 0;
    border: 1px solid rgba(0, 0, 0, 0.2);
    -webkit-transform: scale(0.5);
    transform: scale(0.5);
    -webkit-transform-origin: 0 0;
    transform-origin: 0 0;
    box-sizing: border-box;
    border-radius: 10px;
}

我们可以看到通过伪元素构建的基本原理,是先将它的长宽放大到2倍, 边框宽度设置为1px, 再以transform缩放到50%,同时,元素本身使用position:relative相对定位,伪元素使用position:absolute绝对定位

vue数据变化侦测

关于vue的内部原理其实有很多个重要的部分,变化侦测,模板编译,virtualDOM,整体运行流程等

今天主要分析下变化侦测的原理

如何侦测变化

关于变化侦测首先要问一个问题,在 js 中,如何侦测一个对象的变化

学过js的都能知道,js中有两种方法可以侦测到变化,Object.defineProperty 和 ES6 的proxy

到目前为止vue还是用的 Object.defineProperty

在vue的官网中,有这样一段关于vue追踪变化的原理

把一个普通对象传给Vue实例作为它的data选项,Vue.js将遍历它的属性,用Object.defineProperty将它们转换为getter/setter
用户看不到getter/setters,但是在内部它们让Vue.js追踪依赖,在属性被访问和修改时通知变化
模板中每个指令/数据绑定都有一个对应的watcher对象,在计算过程中它把属性记录为依赖;之后当依 赖的setter被调用时,会触发watcher重新计算,也就会导致它的关联指令更新DOM

接下来是小段源码

image

可以看到defineproperty中给属性getter,setter的设置,一旦该属性被获取,便会添加依赖;同样的,一旦该属性被更改,便会发出通知

观察所有数据

知识点补充:

访问器属性不能直接定义,必须使用Object.defineProperty()来定义;包含一对getter函数和setter函数(这两个函数不是必须的)

在读取访问器属性时,会调用getter函数,这个函数负责返回有效的值;在写入访问器属性是,会调用setter函数并传入新值,这个函数负责决定如何处理数据

拿 Object.defineProperty来举例说明这个原理

image

所以,当对象下的访问器属性值发送改变之后(vue会将属性都转化为访问器属性),那么就会调用set函数,这时vue就可以通过这个set函数来追踪变化,调用相关函数来实现view视图的更新

每个组件实例都有相应的watcher实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会通知watcher重新计算,从而让致使它关联的组件得以更新

怎么观察

即在渲染的过程中就会调用对象属性的getter函数,然后getter函数通知watcher对象将之声明为依赖,依赖之后,如果对象属性发生变化,那么就会调用setter函数来通知watcher,watcher就会在重新渲染组件,以此来完成更新

总结起来一句话,getter中,收集依赖,setter中,触发依赖

依赖收集

首先想到的是每个key都有一个数组,用来存储当前key的依赖

实例中新增了对象数组model,用来存储被收集的依赖

然后在触发set触发时,循环model把收集到的依赖触发,递归侦测所有key

收集目标

收集谁,当数据变化通知谁?

watcher--变化侦测和更新view的一个纽带

watcher会主动将自己push到属性的依赖中,只有通过watcher读取属性的值才会收集依赖,模板是通过watcher读取属性的值的

总结

image

这是vue官网上的一张图,这张图其实非常清晰,就是一个变化侦测的原理图

getter到watcher有一条线,上面写着收集依赖,意思是说 getter 里收集 watcher,也就是说当数据发生 get 动作时开始收集 watcher

setter 到 watcher 有一条线,写着 Notify 意思是说在 setter 中触发消息,也就是当数据发生 set动作时,通知 watcher

Watcher 到 ComponentRenderFunction 有一条线,写着 Trigger re-render 意思很明显了

原生JS实现数据双向绑定

全面解析微信小程序开发之登录/授权

体验过小程序的童鞋应该都知道,最初进入一个新的小程序,会让我们进行一系列授权操作,同意过后就可以正常使用了,并且在下次进入的时候依然保持授权后的状态

那这一系列操作是怎么完成的呢?我们的授权登入态是怎么维护的呢?

下面就来说说我根据官方给出的方法来写出的维护登录态的方法吧

登录态维护

微信小程序的运行环境不是在浏览器下运行的,所以就不能用浏览器下那套cookie实现机制来维护登录态了,但我们可以仿浏览器来实现

官方文档给出了wx.login()的方法来获取用户登录态:

登录时序图

image

  1. 小程序在调用wx.login()时会获取到临时登录凭证code,然后将code发送到后台服务器

    前端具体实现如下(app.js)

    // 登录
    login() {
      return util.promisify(wx.login)().then(({ code }) => {
        return http.post('/oauth/login', {
        code,
        type: 'wxapp'
        })
      }).then(({ data }) => {
        return this.getUserInfo()
      })
    }
  2. 后台服务器再通过code去请求微信服务器,换取对应的用于用户唯一标识的openid和会话密钥(解密用户的敏感信息)session_key

    后端具体实现如下(server.js)

    axios.get('https://api.weixin.qq.com/sns/jscode2session', {
      params: {
        appid: config.appId,
        secret: config.appSecret,
        js_code: code,
        grant_type: 'authorization_code'
      }
    }).then(({data}) => {
      var openId = data.openid
      var user = userList.find(user => {
        return user.openId === openId
      })
  3. 后台服务器紧接着可以根据用户标识来生成自定义登录态,以便于维护后续业务逻辑中前后端交互时的身份

    需要注意的是,后台服务器在拿到session_key、openid等字段时,不应该直接作为用户的标识或者session的标识,而应该自己生成一个session登录态(可以参考登录时序图),在保证其安全性的角度上不设置较长的过期时间

    后端具体实现如下(server.js)

    var sessionMap = {}
    
    app
      .use(bodyParser.urlencoded({ extended: false }))
      .use(bodyParser.json())
      .use(session({
        secret: 'alittlegirl',
        resave: false,
        saveUninitialized: true
      }))
    
      .use((req, res, next) => {
        req.user = sessionMap[req.session.id] // 通过不同的session.id映射不同的user
        console.log(`req.url: ${req.url}`)
        if (req.user) {
          console.log(`wxapp openId`, req.user.openId)
        } else {
          console.log(`session`, req.session.id)
        }
        next()
      })
  4. session通过响应头派发到小程序客户端之后,可将其存储在storage中,然后每次发请求时再加在请求头上,用于后续通信的登录态校验

    前端具体实现如下(util.js)

    响应头拦截器

    http.interceptors.response.use(response => {
      var { headers, data, status } = response
      if (data && typeof data === 'object') {
        Object.assign(response, data)
        if (data.code !== 0) {
          return Promise.reject(new Error(data.message || 'error'))
        }
      }
      if (status >= 400) {
        return Promise.reject(new Error('error'))
      }
      var setCookie = headers['set-cookie'] || ''
      var cookie = setCookie.split('; ')[0]
      if (cookie) {
        var cookie = qs.parse(cookie)
        return util.promisify(wx.getStorage)({
          key: 'cookie'
        }).catch(() => { }).then(res => {
          res = res || {}
          var allCookie = res.allCookie || {}
          Object.assign(allCookie, cookie)
          return util.promisify(wx.setStorage)({
            key: 'cookie',
            data: allCookie
          })
        }).then(() => {
          return response
        })
      } else {
        return response
      }
    })

    请求头拦截器

    http.interceptors.request.use(config => {
      return util.promisify(wx.getStorage)({
        key: 'cookie'
      }).catch(() => { }).then(res => {
        if (res && res.data) {
          Object.assign(config.headers, {
            Cookie: qs.stringify(res.data, ';', '=')
          })
        }
        return config
      })
      return config
    })

以上就是整个登录态维护的实现方法,总结来说就是

  • 调用wx.login()得到code后请求服务器获取openidsession_key缓存到服务器当中

  • 其中生成一个随机数为keyvalueopenidsession_key

  • 然后返回到小程序通过wx.setStorage('token',得到的随机数key)存在小程序当中

  • 每当我们去请求服务器时带上token即可给服务器读取从而判断用户是否在登录

自定义登录态有效期

上述维护登录态的实现,我们已经了解到最关键的是生成与openidsession_key对应的key值,来保持用户唯一身份的识别

而我们每调用一次wx.login()时,就会更新用户的session_key,从而导致旧的session_key失效

所以我们必须明确只有在需要重新登录时才调用wx.login()

那怎么确定什么时候才是需要重新登录的呢?换句话说就是怎么知道session_key的有效期呢?

我们可以先看下官方文档

微信不会把session_key的有效期告知开发者。我们会根据用户使用小程序的行为对session_key进行续期。用户越频繁使用小程序,session_key有效期越长。

开发者在session_key失效时,可以通过重新执行登录流程获取有效的session_key。使用接口wx.checkSession()可以校验session_key是否有效,从而避免小程序反复执行登录流程。

当开发者在实现自定义登录态时,可以考虑以session_key有效期作为自身登录态有效期,也可以实现自定义的时效性策略。

也就是说小程序通过wx.login()接口获得的用户登录态是具有一定的时效性的,可以通过wx.checkSession()来判断登录态是否过期,从而决定是否更新登录态

前端具体实现如下(app.js)

onLaunch: function () {
  console.log('build time', date.formatTime(new Date()))
  util.promisify(wx.checkSession)().then(() => {
    console.log('session 有效')
    return this.getUserInfo()
  }).then((userInfo) => {
    console.log('登录成功', userInfo)
  }).catch(() => {
    console.log('自动登录失败')
    return this.login()
  }).catch(err => {
    console.log(`手动登录失败`)
  })
},

微信授权&获取用户数据

通过wx.login()实现登录态维护之后,我们就可以通过wx.getUserInfo()获取用户信息、wx.getPhoneNumber()获取手机号信息了

得到的用户数据包含两部分,一部分是敏感信息,需要从密文中解密出来,密文在encryptedData这个字段中,去请求后台服务器去解密然后就可以得到敏感信息并保存下来了;另一部分是不敏感的信息,在result的userInfo里

如下图所示

image

有一点需要注意的是,微信5月份出了最新的策略,这俩操作都需要用户主动点击点击按钮,完成统一授权后才能触发

wxml代码如下(index. wxml)

<view class="container">
  <view class="userinfo">
    <view wx:if="{{!userInfo.nickName}}">
      <button 
        open-type="getUserInfo" 
        bindgetuserinfo="bindUserInfo"> 
        获取头像昵称 
      </button>
    </view>
    <block wx:else>
      <image bindtap="bindViewTap" class="userinfo-avatar" src="{{userInfo.avatarUrl}}" mode="cover" bindtap='bindViewTap'></image>
      <text class="userinfo-nickname">{{userInfo.nickName}}</text>
    </block>
  </view>

  <view class="userinfo">
    <view wx:if="{{!userInfo.phoneNumber}}">
      <button
        open-type="getPhoneNumber" 
        bindgetphonenumber="bindPhoneNumber">
          获取手机号码
      </button>     
    </view>
    <text wx:else>{{userInfo.phoneNumber}}</text>
  </view> 
  <view class="usermotto">
    <text class="user-motto">{{motto}}</text>
  </view>
</view>

在实际项目开发中,我们每进行一次不同的授权操作,都会将微信服务器请求授权的用户数据返回到后台服务器,然后写入统一的用户信息接口,再返回给小程序

每次授权后前端也都需要进行用户信息更新,以便于将所有有关用户的信息都存于统一的接口,便于调用

如果每进行一次授权操作,都重新写一遍请求后台用户信息接口的逻辑,难免让代码显得冗余

那正确的操作是怎样的呢?

我们可以在入口app.js文件中声明调用的后台接口,将用户信息存入全局定义的数组中(暂且称之为userInfo)

前端代码如下(app.js)

// 获取用户信息
getUserInfo() {
  return http.get('/user/info').then(({ data }) => {
    if (data && typeof data === 'object') {
      this.globalData.userInfo = data
      // 延时函数
      if (this.userInfoReadyCallback) {
        this.userInfoReadyCallback(data)
      }
      return data
    }
    return Promise.reject(response)
  })
},

globalData: {
  userInfo: null
}

这样即使在授权过后,再次使用该小程序时,我们也能够从全局直接拿到用户信息,然后在各个页面调用全局数据即可

比如微信授权页面(index.js)

onLoad: function () {
  if (app.globalData.userInfo) {
    this.setData({
      userInfo: app.globalData.userInfo
    })
  }else if (this.data.canIUse){
    // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回
    // 所以此处加入 callback 以防止这种情况
    app.userInfoReadyCallback = res => {
      this.setData({
        userInfo: res
      })
    }
  } else {
    // 在没有 open-type=getUserInfo 版本的兼容处理
    wx.getUserInfo({
      success: res => {
        app.globalData.userInfo = res.userInfo
        this.setData({
          userInfo: res.userInfo
        })
      }
    })
  }
}

那前后端是怎么实现微信授权操作中从微信服务器读取数据然后存入后台数据库的呢?

获取用户信息(头像昵称等)

前端代码如下(index.js)

bindUserInfo: function(e) {
  var detail = e.detail
  if (detail.iv) {
    http.post('/user/bindinfo', {
      encryptedData: detail.encryptedData
      iv: detail.iv
      signature: detail.signature
    }).then(()=>{
      return app.getUserInfo().then(userInfo => {
        this.setData({
          userInfo: userInfo
        })
      })
    })
  }
}

服务端接口代码如下(server.js)

.post('/user/bindinfo', (req, res) => {
  var user = req.user
  if (user) {
    var {encryptedData, iv} = req.body
    var pc = new WXBizDataCrypt(config.appId, user.sessionKey)
    var data = pc.decryptData(encryptedData, iv)
    Object.assign(user, data)
    return res.send({
      code: 0
    })
  }
  throw new Error('用户未登录')
})

获取用户手机号

前端代码如下(inde.js)

bindPhoneNumber(e) {
  var detail = e.detail
  if (detail.iv) {
    http.post('/user/bindphone', {
      encryptedData: detail.encryptedData,
      iv: detail.iv
    }).then(() => {
      return app.getUserInfo().then(userInfo => {
        this.setData({
          userInfo: userInfo
        })
      })
    })
  }
}

服务端接口如下(server.js)

.post('/user/bindphone', (req, res) => {
  var user = req.user
  if (user) {
    var {encryptedData, iv} = req.body
    var pc = new WXBizDataCrypt(config.appId, user.sessionKey)
    var data = pc.decryptData(encryptedData, iv)
    Object.assign(user, data)
    return res.send({
      code: 0
    })
  }
  throw new Error('用户未登录')
})

代码

可见我的wxapp-authorization-demo

css的border法实现三角形

原理

先看下border的表现形式

#box{
  width:100px;
  height:100px;
  background:yellow;
  border-top: 20px solid red;
  border-right:20px solid black;
  border-bottom:20px solid green;
  border-left:20px solid blue;
}

150607240455

可以发现,border表现为梯形

所以,当我们减少box的宽高,一直降到0时,border就变成了三角形

所以我们就可以通过将元素宽高设置为0,而通过控制border来得到想要的三角形了

实现

只需要将不需要方向的border设置为透明(transparent),就可以用来实现三角形了

比如想实现下三角形,就将border-left,border-bottom,border-right设置为transparent即可

#box{
  width:0px;
  height:0px;
  border-top: 20px solid red;
  border-right:20px solid transparent;
  border-bottom:20px solid transparent;
  border-left:20px solid transparent;
}

150607295095

实现梯形也就简单了,比如

#box{
  width:100px;
  height:100px;
  border-top: 60px solid red;
  border-right:20px solid transparent;
  border-bottom:0px solid transparent;
  border-left:20px solid transparent;
}

150607308358

总而言之,通过调整border的大小和颜色/透明,就可以获得各种三角形和梯形了

那如果实现是带边框的三角形呢?如果是一个正方形,我们写边时,会用到边框

但这里的三角形本身就是border,不可能再给border添加border属性了,所以得用其他方法

比如叠加层,思路就是将两个三角形叠加在一起,外层三角形稍大一些,颜色设置成边框所需的颜色;内层三角形绝对定位在里面。整体就能形成带边框三角形的假象

实现

html:
<!-- 向上的三角形 -->
<div class="triangle_border_up">
    <span></span>
</div>

css:
/*向上*/
.triangle_border_up{
    width:0;
    height:0;
    border-width:0 30px 30px;
    border-style:solid;
    border-color:transparent transparent #333;/*透明 透明  灰*/
    margin:40px auto;
    position:relative;
}
.triangle_border_up span{
    display:block;
    width:0;
    height:0;
    border-width:0 28px 28px;
    border-style:solid;
    border-color:transparent transparent #fc0;/*透明 透明  黄*/
    position:absolute;
    top:0px;
    left:0px;
}

效果

150607373635

但是并不是预想的这样

150607385068

原因是,我们看到的三角形是边,而不是真的具有内容的区域,绝对定位(position:absolute),是根据相对定位父层内容的边界计算的

这个空的div,content的位置在中心,所以内部三角形是根据中心这个点来定位的

添加阴影可以看到

150607413038

所以,内部的三角形都是根据外部三角形实际内容的点来定位的,而非我们肉眼看到的三角形的边界定位

将上述span的

top:0px;
left:0px;

改为

top:1px;
left:-28px;

就阔以了!

web端导出csv文件

前言

导出文件,大部分方式还是服务端来处理的,通过服务端的方式生成文件,然后传递到客户端

但是,有时候可能就想使用web前端直接把页面上的内容导出来

前段时间做了一个多行多列导出页面的table表格,用的是a标签(支持firefox和Chrome)实现的(IE可以使用ActiveXObiect实现

忽视点

  • 如何分行,分列

    • 理论上,分列使用,号分割,分行用\n
    • 但是会出现列可以分开,但是不会换行的情况
    • 解决方法是,将生成的csv字符串使用encodeURIComponent编码
  • csv导出出现中文乱码(Excel打开)

    • 少了一个BOM头,\ufeff,加上\ufeff BOM头,csv="\ufeff"+csv
    • 页面的charset需设置成UTF-8
  • a的download属性可以指定下载的文件名及后缀,但是Chrome执行的时候会有bug

    • 之前写的a.href = "data:text/csv;charset=utf-8,\ufeff"+str;Chrome不理会
    • 改成如下代码,使用Blob和URL来封装和转换,问题解决
      var blob = new Blob([csv],{
         type: 'text/csv,charset=UTF-8'
      })

    var csvUrl = window.URL.createObjectURL(blob)
    var a = document.createElement('a')
    a.href = csvUrl

导出报表源码

exportData(list) {
    let tableHeader = [{
       type: 'theme',
       name: '报表名称'
      },{...},...]
     let tableBody = []
     for (let item of list) {
        let object = {theme:'',...}
        object.theme = item.title.replace('{DATE}','')+'('+item.info+')'
         ...
         for (let to of item.to) {
           object.to += to + ";\n"
         }
         ...
        tableBody.push({
           theme : object.theme || '',
          ...
       })
    }  
    var csv = '\ufeff'
    var keys= []
    tableHeader.forEach(function(item){
       csv+='"'+item.name+'",'
       keys.push(item.type)
   })
   csv=csv.replace(/\,$/, '\n')
   tableBody.forEach(function(item){
      keys.forEach(function(key){
        csv+='"'+item[key] +'",'
     })
     csv=csv.replace(/\,$/, '\n')
  })
  csv=csv.replace(/"null"/g, '""') 
  var  blob=new Blob([csv],{
     type:'text/csv,charset=UTF-8'
  })
  var csvUrl=window.URL.createObjectURL(blob)
  var a=document.createElement('a')
  var now=newDate()
  function to2(num) {
     returnnum>9?num:'0'+num
  }
  a.download='报表信息导出'+now.getFullYear() +to2((now.getMonth() +1)) +to2(now.getDate()) 
  +to2(now.getHours()) +to2(now.getMinutes()) +to2(now.getSeconds()) +'.csv'
   a.href=csvUrl
   document.body.appendChild(a)
   a.click()
   document.body.removeChild(a)
 }

注:tableHead存放表头,tableBody存放表格内容
download设置下载的文件名
点击click是下载文件

ant-design-pro踩坑

前言

字典表级联的需求我们在项目里边也是经常碰到,下一级选择栏依赖于上一级选择栏选中的结果,对于model中定义state,一定要用不同的key值代替不同的接口数据

问题

对于多次请求的接口,如果只用一个key值,下次接口数据render的时候,上次接口请求的数据就会被覆盖或者出现下次接口请求的数据不会更新的bug

实例

比如在routes文件夹下的组件中请求接口数据

const CreateForm = Form.create()(props => { //创建组件CreateForm,为Operation子组件
          
  const { getFieldDecorator } = props.form;
  const { optionsA, handleChangeA, handleChangeB, optionsB } = props; //获取父组件传入子组件的值
  ......
  const handleSelectChangeA = (id) => { 
    handleChangeA(id)  
  };
  const handleSelectChangeB = (id) => { 
    handleChangeB(id)  
  };
 return (...)
}

// 关联model中的数据,在render中直接用this.props可以获取state中的数据
@connect(({ operation }) => ({
   ...operation,
}))
@Form.create()

export default class Operation extends PureComponent {

  // 组件初始化请求数据
  componentDidMount() {
    const { dispatch } = this.props;
    const query = {
      level: 'a',
      pId: 0
    }
    dispatch({
      type: 'operation/fetch',
      payload: query,
    });
  }
    
  // handleChangeA为第一个select选中时处理数据的函数
  handleChangeA = id => {
    console.log('id', id)
    const { dispatch } = this.props;
    const params = {
      level: 'b',
      pId: id
    };
    dispatch({
      type: 'operation/fetch',
      payload: params,
    });  
  }

  // handleChangeB为第二个select选中时处理数据的函数
  handleChangeB = id => {
    console.log('id', id)
    const { dispatch } = this.props;
    const params = {
      level: 'c',
      pId: id
    };
    dispatch({
      type: 'operation/fetch',
      payload: params,
    });  
  }

  render() {
    ......

    // 将方法handleChange传入子组件CreateForm
    const parentMethods = {
      handleChangeA: this.handleChangeA,
      handleChangeB: this.handleChangeB,
    };

    //处理数据格式,过滤出选中的id和professionalName,用以传入子组件CreateForm
    const optionsA = (this.props.aTags || []).map(item => <Option key={item.id}>{item.professionalName}</Option>)
    const optionsB = (this.props.bTags || []).map(item => <Option key={item.id}>{item.professionalName}</Option>)
        
    return (
      <div className={styles.tabs}>
        <Tabs defaultActiveKey="2" onChange={callback} tabBarStyle={{ marginBottom: 24 }}>
          <TabPane tab="tab1" key="1">Content of Tab Pane 1</TabPane>
          <TabPane tab="tab2" key="2">
            <Collapse bordered={false} defaultActiveKey={['2']}>
              <Panel header="panel1" key="1">
              </Panel>
              <Panel header="panel2" key="2">
              <CreateForm {...parentMethods} optionsA={optionsA} optionsB={optionsB}/>
              </Panel>
              <Panel header="panel3" key="3">
              </Panel>
            </Collapse>
          </TabPane>
          <TabPane tab="tab3" key="3">Content of Tab Pane 3</TabPane>
        </Tabs>
     </div>
   );
  }
}

然后在model下对数据请求进行逻辑处理

export default {
  namespace: 'operation',
  state: {
    aTags: [],
    bTags: [],
    cTags: [],
  },
  effects: {
    *fetch({ payload }, { call, put }) {
      const { level } = payload;
      const levelMap = {
        'a': 'aTags',
        'b': 'bTags',
        'c': 'cTags',
      }
      const target = levelMap[level];
      const response = yield call(query,payload);
      const result = {
        [target]: response,
      }
      console.log('----', result);
      yield put({
        type: 'saveTag',
        payload: result,
      });
    },
  },
  reducers: {
    saveTag(state, action) {
      return {
        ...state,
        ...action.payload,
      };
    },
  },
}

这里的aTags: [],bTags: [],cTags: []分别存放三次请求的数据,而不是直接定义Tags来存放三次请求数据

类似于这种结果界面

image

flex 的 space-between 属性多行等分布局出现空格的友好处理

经常使用 flex 布局的小伙伴应该遇到过一个问题,就是当我们使用 space-between 属性等分布局时,在多行且不是整数时,中间就会空出一格,比如

我有一个宽度为 576 px , 有 6 个固定的 item宽为 130,如果等分布局,就会出现下面这种情况

image

这种情况应该怎么处理呢?我们可以通过手写一个函数来填充,比如每行有 num 个 item ,始终渲染 num 的整数个 item ,多出的用 null 填充

handleObj = (i) => (
    {id: i, hidden: true}
  )

// collection: 原数据list,是个数组 ; num: 每行需要展示的item数
handleShowContent = (collection, num) => { 
  const hiddenNum = Math.ceil(collection.length / num) * num - collection.length // 行数
  const newCollection = JSON.parse(JSON.stringify(collection))
  for (let i = 0; i < hiddenNum; i+=1) {
    newCollection.push(this.handleObj(collection[collection.length-1].id+i+1))
  }
  return newCollection
}
<div className={styles.list}>
  {
    (element || []).map(ele => (
      <div key={ele.id} className={`${styles.item} ${ele.hidden? styles.show : ''}`} />
    ))
  }
</div>
.list {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  padding: 12px;
  height: 200px;
  background-color: #fff;
}

.show {
  visibility: hidden;
}

.item {
  border:1px solid #e5e5e5;
  width:130px;
  height:80px;
}

处理完之后就能得到正常的展示

image

PM2—nodejs项目部署

背景

前段时间做了个生成图片的可视化查询系统,该项目的技术栈是vue+express+nodejs+mysql。目前Nodejs开发中有很多痛点,其中一个是修改完代码以后需要我们重启服务器才能看到效果。

开发阶段我们使用node file.js来启动Nodejs的单线程,但是任何异常都会导致整个服务中断,那怎样才能在异常后自动重启,保证服务一直在线呢。

接下来介绍本次在项目部署中用到的nodejs的部署方式-PM2。

简介

PM2=P(rocess)M(anager)2,是可以用于生产环境的Nodejs的进程管理工具。不仅可以保证服务不会中断一直在线,并且提供0秒reload功能,还有其他一系列进程管理、监控功能等,而且使用非常简单。

部署项目的目的是为了项目在服务器上持续运行,并且可以实时查看并检查服务器项目的运行情况。

PM2官方文档

实例

前期必备

  • node环境
  • npm

全局安装

npm install -g pm2

运行

pm2 start app.js 

start后的第一个参数,直接是nodejs的主程序即可

image

箭头处表示已开启的服务进程

查看应用的运行状态:pm2 list

查看日志:pm2 logs

重启应用:pm2 restart appId

停止应用:pm2 stop appId

停止所有应用:pm2 stop all

image

分层解析 Promise 的实现原理

今天写的这篇文章是关于 Promise 的,其实在谷歌一搜索,会出来很多关于Promise的文章,那为什么还要写这篇文章呢?

我相信一定有人用过 Promise ,但总有点似懂非懂的感觉,比如我们知道异步操作的执行是通过 then 来实现的,那后面的操作是如何得知前面异步操作完成的呢? Promise 具体是怎样实现的呢?

所以我写这篇文章的目的主要是从最基础的点开始剖析,一步一步来理解 Promise 的背后实现原理

也是因为最近自己的困惑,后面边看文章,边调试代码,以至于对 Promise 的理解又上升了一个台阶~

为什么会有 Promise 的产生

我们可以想象这样一种应用场景,需要连续执行两个或者多个异步操作,每一个后来的操作都在前面的操作执行成功之后,带着上一步操作所返回的结果开始执行

在过去,我们会做多重的异步操作,比如

doFirstThing((firstResult) => {
  doSecondThing(firstResult, (secondResult) => {
    console.log(`The secondResult is:` + secondResult)
  })
})

这种多层嵌套来解决一个异步操作依赖前一个异步操作的需求,不仅层次不够清晰,当异步操作过多时,还会出现经典的回调地狱

那正确的打开方式是怎样的呢?Promise 提供了一个解决上述问题的模式,我们先回到上面那个多层异步嵌套的问题,接下来转变为 Promise 的实现方式:

function doFirstThing() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('获取第一个数据')
      let firstResult = 3 + 4
      resolve(firstResult)
    },400)
  })
}

function doSecondThing(firstResult) {
  console.log('获取第二个数据')
  let secondResult = firstResult * 5
  return secondResult
}

doFirstThing()
  .then(firstResult => doSecondThing(firstResult))
  .then(secondResult => {
    console.log(`The secondResult Result: ${secondResult}`
  )})
  .catch(err => {
    console.log('err',err)
  })

可以看到结果就是我们预期得到的,需要注意的一点是,如果想要在回调中获取上个 Promise 中的结果,上个 Promise 中必须有返回结果

Promise 到底是什么

相信经过上面的应用场景,已经大致明白 Promise 的作用了,那它的具体定义是什么呢?

Promise 是对异步编程的一种抽象,是一个代理对象,代表一个必须进行异步处理的函数返回的值或抛出的异常

简单来说,Promise 主要就是为了解决异步回调的问题,正如上面的例子所示

可以将异步对象和回调函数脱离开来,通过 then 方法在这个异步操作上面绑定回调函数

用 Promise 来处理异步回调使得代码层析清晰,便于理解,且更加容易维护,其主流规范目前主要是 Promises/A+ ,下面介绍具体的API

状态和值

Promise 有3种状态: pending (待解决,这也是初始状态), fulfilled (完成), rejected (拒绝)

状态只能由 pending 变为 fulfilled 或由 pending 变为 rejected ,且状态改变之后不会再发生变化,会一直保持这个状态

Promise 的值是指状态改变时传递给回调函数的值

接口

Promise 唯一接口 then 方法,它需要2个参数,分别是 onResolvedonRejected

并且需要返回一个 promise 对象来支持链式调用

Promise 的构造函数接收一个函数参数,参数形式是固定的异步任务,接收的函数参数又包含 resolvereject 两个函数参数,可以用于改变 Promise 的状态和传入 Promise 的值

  1. resolve:将 Promise 对象的状态从 pending (进行中)变为 fulfilled (已成功)

  2. reject:将 Promise 对象的状态从 pending (进行中)变为 rejected (已失败)

  3. resolve 和 reject 都可以传入任意类型的值作为实参,表示 Promise 对象成功( fulfilled )和失败( rejected )的值

了解了 Promise 的状态和值,接下来,我们开始讲解 Promise 的实现步骤

Promise 是怎样实现的

我们已经了解到实现多个相互依赖异步操作的执行是通过 then 来实现的,那重新回到最开始的疑问,后面的操作是怎么得知异步操作完成了呢?了解过 Vue 的童鞋应该知道一种发布/订阅模式,就是后面有一个函数在一直监听着前面异步操作的完成。 Promise 的实现貌似也有点发布/订阅的味道,不过它有 then 的链式调用,且没有 on/emit 这种很明显的订阅/发布的东西,让实现变得看起来有点复杂

在讲解 Promise 实现之前,我们还是先简要提一下发布/订阅模式:首先有一个事件数组来收集事件,然后订阅通过 on 将事件放入数组, emit 触发数组相应事件

那 Promise 呢? Promise 内部其实也有一个数组队列存放事件, then 里边的回调函数就存放数组队列中。下面我们可以看下具体的实现步骤

实现 promise 雏形

( demo1 )

class Promise {
  constructor (executor) {
    this.value = undefined
    this.status = 'pending'
    executor(value => {
      this.status = 'resolve',
      this.value = value
    }, reason => {
      this.status = 'rejected'
      this.value = reason
    })
  }

  then(onResolved) {
    onResolved(this.value)
  }
}

// 测试
var promise = new Promise((resolve, reject) => {
  resolve('promise')
})

promise.then(value => {
  console.log('value',value)
})
promise.then(value => {
  console.log('value',value)
})

上述代码很简单,大致的逻辑是:

通过构造器 constructor 定义 Promise 的初始状态和初始值,通过 Promise 的构造函数接收一个函数参数 executor , 接收的函数参数又包含 resolvereject 两个函数参数,可以用于改变 Promise 的状态和传入 Promise 的值。

然后调用 then 方法,将 Promise 操作成功后的值传入回调函数

异步操作

相信有人会好奇,上述 Promise 实例中都是进行的同步操作,但是往往我们使用 Promise 都是进行的异步操作,那会出现怎样的结果呢?在上述例子上进行修改,我们用 setTimeout 来模拟异步的实现

// 测试
var promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('promise')
  },300)
})

会发现后面的回调函数中打印出来的值都是undefined

image

很明显,这种错误的造成是因为 then 里边的回调函数在实例化 Promise 操作 resolve 或 reject 之前就执行完成了,所以我们应该设定触发回调函数执行的标识,也就是在状态和值发生改变之后再执行回调函数

正确的逻辑是这样的:

  1. 调用 then 方法,将需要在 Promise 异步操作成功时执行的回调函数放入 children 数组队列中,其实也就是注册回调函数,类似于观察者模式

  2. 创建 Promise 实例时传入的函数会被赋予一个函数类型的参数,即 resolve ( reject ),它接收一个参数 value ,当异步操作执行成功后,会调用 resolve ( reject )方法,这时候其实真正执行的操作是将 children 队列中的回调一一执行

在 demo1 的基础上修改如下:

( demo2 )

class Promise {
  constructor (executor) {
    this.value = undefined
    this.status = 'pending'
    this.children = [] // children为数组队列,存放多个回调函数
    executor(value => {
      this.status = 'resolve',
      this.setValue(value)
    }, reason => {
      this.status = 'rejected'
      this.setValue(reason)
    })
  }

  then (onResolved) {
    this.children.push(onResolved)
  }

  setValue (value) {
    this.value = value
    this.children.forEach(child => {
      child(this.value)
    })
  }
}

// 测试
var promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('promise')
  },300)
})
promise.then(value => {
  console.log('value',value)
})

首先实例化 Promise 时,传给 promise 的函数发送异步请求,接着调用 promise 对象的 then 函数,注册请求成功的回调函数,然后当异步请求发送成功时,调用 resolve ( rejected )方法,该方法依次执行 then 方法注册的回调数组

实现 promise 开枝散叶

相信仔细的人应该可以看出来,then 方法应该能够支持链式调用,但是上面的初步实现显然无法支持链式调用

那怎样才能做到支持链式调用呢?其实实现也很简单:

then (onResolved) {
  this.children.push(onResolved)
  return this
}
// 测试
var promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('promise')
  },300)
})
promise.then(value1 => {
  console.log('value1',value1)
}).then(value2 => {
  console.log('value2',value2)
})

then 方法中加入 return this 实现了链式调用,但如果需要在 then 回调函数中返回一个值 value 或者 promise ,传给下一个 then 回调函数呢?

先来看返回值 value 的情况,比如:

// 测试
promise.then(value1 => {
  console.log('value1',value1)
  let value = 'promise2'
  return value
}).then(value2 => {
  console.log('value2',value2)
})

在 demo2 的基础上进行改造:

( demo3 )

then (onResolved) {
  var child = new Promise(() => {})
  child.onResolved = onResolved
  this.children.push(child)
  return this
}

setValue (value) {
  this.value = value
  this.children.forEach(child => {
    var ret = child.onResolved(this.value)
    this.value = ret
  })
}

原理就是在调用 Promise 对象的 then 函数时,注册所有请求成功的回调函数,后续在 setValue 函数中循环所有的回调函数,每次执行完一个回调函数就会更新 this.value 的值,然后将更新后的 this.value 传入下一个回调函数里,这样就解决了传值的问题

但这样也会出现一个问题,我们只考虑了串行 Promise 的情况下依次更新 this.value 的值,如果串行和并行一起呢?比如:

// 测试
// 串行
promise.then(value1 => {
  console.log('value1',value1)
  let value = 'promise2'
  return value
}).then(value2 => {
  console.log('value2',value2)
})

// 并行
promise.then(value1 => {
  console.log('value1',value1)
})

打印出来的结果最后一个 value1 为 undefined ,因为我们一直在改变 this.value 的值,并且在串行最后一个 then 回调函数中也显示设定返回值,默认返回 undefined

image

可见 return this 并行不通,继续在 demo3 的基础上改造 then 和 setValue 函数如下:

( demo4 )

then (onResolved) {
  var child = new Promise(() => {})
  child.onResolved = onResolved
  this.children.push(child)
  return child
}
setValue (value) {
  this.value = value
  this.children.forEach(child => {
    var ret = child.onResolved(this.value)
    child.setValue(ret)
  })
}

那如果 then 回调函数中返回一个 promise 呢?比如:

// 测试
promise.then(value1 => {
  console.log('value1',value1)
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('promise2')
    },200)
  })
}).then(value2 => {
  console.log('value2',value2)
})

image

很明显,打印出来的结果是个 Promise 。继续在 demo4 的基础上改造 setValue 函数

( demo5 )

setValue (value) {
  if (value && value.then) {
    value.then(realValue => {
      this.setValue(realValue)
    })
  } else {
    this.value = value
    this.children.forEach(child => {
      var ret = child.onResolved(this.value)
      child.setValue(ret)
    })
  }
}

在 setValue 方法里面,我们对 value 进行了判断,如果是一个 promise 对象,就会调用其 then 方法,形成一个嵌套,直到其不是 promise 对象为止

到目前为止,我们已经实现了 Promise 的主要功能—'开枝散叶',状态和值的有序更新

实现 promise 错误处理

上面所有列举到的 demo 都是在异步操作成功的情况下进行的,但异步操作不可能都成功,在异步操作失败时,状态为标记为 rejected ,并执行注册的失败回调

rejected 失败的错误处理也类似于 resolve 成功状态下的处理,接着在 demo5 的注册回调、处理状态上加入新的逻辑,在 Promise 上加入 resolvereject 静态函数

( demo6 )

class Promise {
  constructor (executor) {
    this.value = undefined
    this.status = 'pending'
    this.children = []
    executor(value => {
      this.setValue(value, 'resolved')
    }, reason => {
      this.setValue(reason, 'rejected')
    })
  }

  then (onResolved, onRejected) {
    var child = new Promise(() => {})
    this.children.push(child)
    Object.assign(child, {
      onResolved: onResolved || (value => value),
      onRejected: onRejected || (reason => Promise.reject(reason))
    })
    if (this.status !== 'pending') {
      child.triggerHandler(this.value, this.status)
    }
    return child
  }

  catch (onRejected) {
    return this.then(null, onRejected)
  }

  triggerHandler (parentValue, status) {
    var handler
    if (status === 'resolved') {
      handler = this.onResolved
    } else if (status === 'rejected') {
      handler = this.onRejected
    }
    this.setValue(handler(parentValue), 'resolved')
  }

  setValue (value, status) {
    if (value && value.then) {
      value.then(realValue => {
        this.setValue(realValue, 'resolved')
      }, reason => {
        this.setValue(reason, 'rejected')
      })
    } else {
      this.status = status
      this.value = value
      this.children.forEach(child => {
        child.triggerHandler(value, status)
      })
    }
  }

  static resolve (value) {
    return new Promise(resolve => {
      resolve(value)
    })
  }

  static reject (reason) {
    return new Promise((resolve, reject) => {
      reject(reason)
    })
  }
}

then 函数中有两个回调 handler, 分别是onResolvedonResolved ,表示成功执行的回调函数和是失败执行的回调函数,并设置默认值,保持链式连接

定义一个 triggerHandler 函数用来判断当前 child 的 status ,并触发自己的 handler ,执行回调函数,再次更新 status 和 value

setValue 函数同时设置 Promise 自己的状态和值,然后在重新设置新的状态之后循环遍历 children

为了更高效率的运行,在 then 函数中注册回调函数时加入状态判断,如果状态改变不为 pending ,说明 setValue 函数已经执行,状态已经发生了更改,就立马执行 triggerHandle r函数;如果状态为 pending ,则在 setValue 函数执行时再触发 triggerHandle `函数

Promise 中的 nextTick

Promise/A+规范要求 handler 执行必须是异步的, 具体可以参见标准 3.1 条

Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, or with a “micro-task” mechanism such as MutationObserver or process.nextTick. Since the promise implementation is considered platform code, it may itself contain a task-scheduling queue or “trampoline” in which the handlers are called

这里用 setTimeout 简单实现一个跨平台的 nextTick

function nextTick(func) {
  setTimeout(func)
}

然后使用 nextTick 包裹 triggerHandler

triggerHandler (status, parentValue) {
  nextTick(() => {
    var handler
    if (status === 'resolved') {
      handler = this.onResolved
    } else if (status === 'rejected') {
      handler = this.onRejected
    }
    this.setStatus('resolved', handler(parentValue))
  })
}

在 demo6 中我们实现了不管是异步还是同步都可以执行 triggerHandler ,那为什么要强制异步的要求呢?

主要是为了流程可预测,标准需要强制异步。可类比于经典的 image onload 问题

var image = new Image()
image.onload = funtion
image.src = 'url'

src 属性为什么需要写在 onload 事件后面?

因为 js 内部是按顺序逐行执行的,可以认为是同步的,给 image 赋值 src 时,去加载图片这个过程是异步的,这个异步过程完成后,如果有 onload ,则执行 onload

如果先赋值 src ,那么这个异步过程可能在你赋值 onload 之前就完成了(比如图片缓存),那么 onload 就不会执行

反之, js 同步执行确定 onload 赋值完成后才会赋值 src ,可以保证这个异步过程在 onload 赋值完成后才开始进行,也就保证了 onload 一定会被执行到

同样的,在Promise中,我们希望代码执行顺序是完全可以预测的,不允许出现任何问题

总结

上述 Promise 各个功能逻辑块的完整代码可见我的 github 分层实现Promise

需要注意的是:

  1. promise 里面的 then 函数仅仅是注册了后续需要执行的回调函数,真正的执行是在 triggerHandler 方法里

  2. then 和 catch 注册完回调函数后,返回的是一个新的 Promise 对象,以延续链式调用

  3. 对于内部 pending 、fulfilled 和 rejected 的状态转变,通过 handler 触发 resolve 和 reject 方法,然后在 setValue 中更改状态和值

Cookie保存用户登录状态

cookie的工作流

Cookie可以通过服务器进行设置,相当于服务器给用户贴的一个标签,用于跟踪用户的状态

HTTP协议本身是无状态的,而应用服务器想保存一些状态,cookie应运而生,由服务器颁发,通过服务器设置的cookie信息通过响应头返回给浏览器,浏览器将响应头中的cookie信息保存在本地,当下次向服务器发送HTTP请求时,就自动将保存的这些cookie信息添加到HTTP请求头中,传递给服务器

这样的交互,服务器就可以在cookie里记录一些用户相关的信息,比如是否登录,账号等,然后根据这些信息做一些动作,比如接下来的示例中的保存登录状态的实现,就利用cookie。还有一些电子商务网站,实现购物车时也可能用到cookie

使用cookie保持登入态

在看cookie保存用户登录状态的效果前,我们先看下header中的cookies信息记录

header中的cookies

首次登入

响应头信息中增加Set-Cookie参数,但请求头信息中没有Cookie参数

image

第一次登入后已经将cookie存到了内存中

image

非首次登入

如果再次点击登录请求按钮,且在没有清空cookie的情况下,发送不同于前一个用户的登录信息,就会出现以下头部信息

image

此时本地的cookie信息也得到了更新

image

请求头中的Cookie字段是浏览器发送给服务器的cookie信息,cookie的值是之前存入内存中的userInfo("userName":"zhangsan")。响应头中的Set-Cookie字段是服务器返回给浏览器的cookie信息(实际上,随着用户登录信息的更新,cookie的值又被重新设置了)

因为没有使用删除已有cookie的直接方法,所以在使用相同的路径(path)、域(domain)和安全选项(secure)的情况下,会再次设置原cookie

上面的登入状态,是当我们登录成功后,在这个页面刷新,页面没有保存登录状态。接下来的效果是,后台cookie保存了用户登录状态,登录后刷新页面直接显示首页信息

使用cookie保持登入态

效果demo主要是通过express+nodejs实现的

处理POST正文数据

我们在demo中使用了HTML表单来接收用户名和密码,当我们提交表单信息时,浏览器会把表单内的数据按一定的格式组织之后编码进body,POST到指定的服务器地址

用户名和密码,在服务器端,可以通过HTML元素的名字属性的值找出来

服务器解析表单数据这一过程,是用express的body-parser中间件,只需要简单的配置即可:

var bodyParser = require("body-parser") // 加载body-parser模块
...
// 应用中间件
app.use(bodyParser.urlencoded({extended:false}))
app.use(bodyParser.json())

express.Request对象req内有解析好的body,使用

app.post("/login",function(req,res,next){
    var userName = req.body.userName
    var password = req.body.password
    ...
}

就可以直接访问用户名和密码,也就是在HTML的input元素中的用户表单值,即建立关联

cookie

cookie存储的是一些key-value对。在express里,Request和Response都有cookie相关的方法。Request实例req的cookies属性,保存了解析出的cookie,如果浏览器没发送cookie,那这个cookies对象就是一个空对象

var Cookies=require('cookies')
app.use(function (req,res,next) {
    req.cookies=new Cookies(req,res)
    next();
})

实例化一个Cookies()方法,用来设置cookie,res.cookies.set('userInfo',JSON.stringify(...)),再通过res.cookies.get('userInfo')的方法来获取cookie

app.use(function (req,res,next) {
    req.cookies=new Cookies(req,res)
    console.log('cookies',typeof res.cookies.get('userInfo')) //返回 string
    //解析用户的cookie信息
    req.userInfo={};
    var cookiesUserInfo=res.cookies.get('userInfo')
    if(cookiesUserInfo){
        try{
            req.userInfo=JSON.parse(cookiesUserInfo)
        }catch(e){}
    }
    next()
})

第一次点击登录按钮post用户数据时,可以看到已经将用户基本信息写入响应头的set-Cookie了

app.post("/login",function(req,res,next){
    var userName = req.body.userName
    var password = req.body.password
    console.log("User name = "+ userName +",password is" + password)
    if(userName===''||password==='') {
        resoinseData.code = 1
        resoinseData.message = "用户名和密码不能为空!"
        res.json(resoinseData) // json格式返回给前端
        return
    }else {
        resoinseData.code = 200
        resoinseData.message = "登录成功!请再次刷新页面"
        resoinseData.userInfo = {
            _id:"5a40caa218013c1dd4eadcb0",
            userName:userName,
            password:password
        }
        req.cookies.set('userInfo',JSON.stringify(resoinseData.userInfo))
        res.json(resoinseData)
        return
    }
})

image

再次刷新页面时,请求头中已经出现了设置的cookie,用户进入免登入状态

app.get("/",function(req,res,next){
    if(req.userInfo.userName) {
        res.sendFile(path.resolve("index.html")) // 如果请求头中有cookie信息,则加载首页
    }else {
        res.sendFile(path.resolve("login.html")) // 如果请求头中没有cookie信息,则重新跳转登录页
    }  
})

image

再次点击退出按钮时,通过res.cookies.set('userInfo',null)会将先前设置的cookie信息删除,再次进入登入页

app.post("/loginout",function(req,res,next){
    req.cookies.set('userInfo',null)
    res.sendFile(path.resolve("login.html")) // 用户登出后再次跳转登录页
})

image

整个保持登入态的工作流:

用户登录 - 前端发送登录请求 - 后端保存用户cookie - 页面刷新 - 前端判断用户cookie存在 - 显示登录状态 - 用户退出 - 前端发送退出请求 - 后端清空用户cookie - 页面跳转登录页

完整的demo可以查看cookie-demo

highcharts实现图片导出

highcharts默认情况下,是支持将图表导出为图片或打印功能的

在图表的右上角有两个按钮,主要用到的是它的导出属性-exporting

而highcharts的命令行导出功能--Nodejs 导出服务器,是对于需要自动生成图表、纯后端生成图表图片、批量生成图表的情况

使用也很方便,只需要安装nodejs导出服务器,有两种方式,分别是

直接安装npm包

npm install highcharts-export-server -g 

源代码安装:

git clone https://github.com/highcharts/node-export-server
npm install
npm link

然后命令行导出,nodejs导出服务器运行方法

highcharts-export-server <arguments>

Nodejs 导出服务器以模块的形式在 Nodejs 程序中使用,简单的示例代码

加载导出模块

const exporter = require('highcharts-export-server');

导出配置

var exportSettings = {
  type: 'png',
  options: {
    title: {
      text: 'My Chart'
    },
    xAxis: {
      categories: ["Jan", "Feb", "Mar", "Apr", "Mar", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
    },
    series: [{
      type: 'line',
      data: [1, 3, 2, 4]
    }, {
      type: 'line',
      data: [5, 3, 4, 2]
    }]
  }
};

启动 Phantomjs 线程池

exporter.initPool();

执行导出

exporter.export(exportSettings, function(err, res) {
  //导出的结果包含在 res 中
  //如果导出结果不是 PDF 或 SVG,那么结果是 base64 编码的内容,可以通过 res.data 来获取
  //如果导出结果是 PDF 或 SVG,那么结果是文件名,可以通过 res.filename 来获取

  //当所有的操作完毕后,关掉线程池并推出程序
  exporter.killPool();
  process.exit(1);
});

谈谈js闭包

闭包问题一直是面试中提到频率最高的问题,那到底什么是闭包呢,百度一下,我们会看到各种层出不穷的答案

所以,闭包是什么呢?

闭包的定义

  • 比较权威的答案,在函数内部定义的变量或方法在函数外部被引用时,就形成了闭包

    但这种说法还是比较片面的,比如我们在函数内部定义方法,并在函数内部执行,也会形成闭包

    image

  • 红宝书中的定义,闭包是指有权访问另一个函数作用域中的变量的函数

    image

    之所以函数能够访问另一个函数作用域中的变量,即使这个内部函数(匿名函数)被返回了,而且是在其他地方被调用了,但它仍然可以访问外部函数中的变量 closure,是因为内部函数的作用域链中包含外部函数的作用域

    当某个函数被调用时,会创建一个执行环境及相应的作用域链

    《JavaScript权威指南》有提到,函数的执行依赖于变量作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的

    每个函数都有一个活动对象,在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,......直至作为作用域链终点的全局执行环境

    在函数执行过程中,为读取和写入变量的值,就需要在作用域链中查找变量

    但我觉得红宝书中的说法也不完全正确,我们可以看看开发者工具中的执行结果

    image

    所以说,闭包指向的是定义内部函数访问的变量的外部作用域,也可以说,闭包是代码块(函数)和创建该代码块的上下文中数据的结合

  • 官方定义,闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分

    我的理解是函数保留了对外部函数变量的引用,简单说就是引用了外部变量,这个变量没有被GC回收,仍存在于函数的作用域链中,这样的函数,叫做闭包

创建闭包的方式

创建闭包的常见方式,就是在一个函数内部创建另一个函数

方式有很多种,比如,可以是在函数内部 return 一个函数(如上),也可以是在函数定义一个内部变量函数,并在函数内部执行(如上),或者是定义一个全局变量函数,在全局执行

image

或者是在定义一个对象方法,在外部执行

image

闭包的作用

闭包很好地解决了变量过多的问题

我们先来看一个例子

<button>1</button>
<button>2</button>
<button>3</button>
var buttons = document.querySelectorAll('button')
for (var i = 0; i < buttons.length; i++) {
  !function(index){
    buttons[i].onclick = function() {
      alert(index + 1)
    }
  }(i)
}

通过立即执行函数形成的闭包很好的解决了button变量的问题,如果没有闭包,我们就需要写多个这样的button点击事件

而且,如果没有闭包,我们以这种方式来定义点击事件,会出现常见的问题

var buttons = document.querySelectorAll('button')
for (var i = 0; i < buttons.length; i++) {
  buttons[i].onclick = function() {
    console.log(i)
  }
}

这样,每个函数打印的i值其实都是3。因为每个函数的作用域链中都保存着全局的活动对象,所以它们引用的都是同一个变量i。当开始执行 onclick 事件时,变量i的值已经是3了,此时每个对象都引用着保存变量i的同一个变量对象,所以在每个函数内部i的值都是3

一篇文章教你快速了解 vuejs

前段时间一直在用 react 写项目,最近的新项目想用 vue 来写,发现大多数属性已经有点混淆了,于是花点时间重新梳理了下~

配合对应的 vue-demo 看更容易理解哦

vue 基础

在讲 vue 之前,我们先提一下框架和库的区别

— 类似于 jQuery 这样的库,是操作 DOM 元素,发起 ajax 请求等,中间还会用到一些其他的库,用来做模板引擎

框架 — 全方位功能齐全,请求获取数据,模板渲染,框架就是一个简易的 DOM 操作 + 发请求 + 模板引擎 + 路由功能

所以,框架就是更为全面的功能,库就是单一的层面

从代码层面上讲,一般使用库的代码,是调用某个函数,我们自己把控库的代码;而使用框架,是框架在帮我们运行我们编写好的代码

比如,框架运行时,一般会初始化自身的代码,然后执行你所编写的代码,最后释放一些资源,其中初始化代码和释放资源都是框架自身的功能

作为框架来讲,基本是大而全,而库就比较专一

从运行层面上讲,对于框架来说,是我们把代码给框架运行,库是我们调用库

插值表达式

  • {{表达式}},表达式可以是:
  1. 对象(不要连接3个{{ {name:'jack'} }})
  2. 字符串{{ 'xxx' }}
  3. 判断后的布尔值 {{ true }}
  4. 三元表达式 {{ true? '是正确':'错误' }}
  • 可以用于页面中简单粗暴的调试

  • 注意:必须在 data 这个函数中返回的对象中声明

什么是指令

  • 比如在 angular 中以 ng-xxx 开头的就叫做指令
  • vue 中以 v-xxx 开头的就叫做指令
  • 指令中封装了一些 DOM 行为,提供不同的功能,框架会进行相关 DOM 操作的绑定

vue 中常见的 v- 指令演示

* v-text 元素的 innerText 属性必须是双标签,只能用在双标签中,本质就是给元素的 innerText 赋值

  • v-html 元素的 innerHTML,本质就是给元素的 innerHTML 赋值
  • v-if 判断是否插入这个元素
  • v-else-if
    * v-else 移出和插入的问题,移出后是 <!---->
  • v-show 隐藏元素,如果确定隐藏,会给元素的 style 加上 display:none,本质是隐藏与否的问题

注意, v-if 如果值为 false,会留下一个 <!----> 作为标记,万一未来 v-if 的值是 true 了,就是这里插入元素,如果有 if else 就不需要单独留坑了

v-bind 使用

  • 给元素的属性赋值
  1. 可以给已经存在的属性赋值
  2. 也可以给自定义属性赋值
  • 语法:在元素上 v-bind:原属性名="变量||变量名"

  • 简写形式 :属性名=“变量名”

v-on 的使用

  • 处理自定义原生事件,给按钮添加 click 并让使用变量的样式改变
  • 在元素上 v-on:原生事件名=“给变量进行操作||函数名”
  • 简写形式:@原生事件名=“给变量进行操作”

value 的值是根据 value 内部的变量,点击按钮,改变了 vue 内部的变量,vue 察觉到页面中使用的变量更改了,所以重新渲染了视图的更改部分

小结:

* input 输入框中的 value 属性改变,显示就改变

  • input 元素 .value = myValue = 'abc'

  • vue 会实时监控 myValue 属性,当其改变,重新执行。将 vue 中的数据同步到页面,也是 v-bind 单向的功能

  • 同时,当用户的输入值发生改变时,vue 就知道了让页面中凸显一个元素出来。页面的改变,影响 vue

    这就是双向数据绑定(流),v-model="myValue",当元素的 value 值改变以后,就会给 vue 中的属性赋值

v-bind 可以给任何属性赋值,是从 vue 到页面的单向数据流
v-model 只能给具备 value 属性的元素进行双向数据绑定(必须使用的是有 value 属性的元素)

v-model

  • 双向数据绑定

    1. 视图改变影响数据 ( js )
    2. 数据 ( js ) 改变影响视图

v-bind 和 v-model 的区别

  • input v-model="name"

    1. 双向数据绑定页面对于 input 的 value 改变,能影响内存中 name 变量
    2. 内存 js 改变 name 的值,会影响页面重新渲染最新值
  • input :value="name"

    1. 单向数据绑定 内存改变影响页面改变
  • v-model: 其的改变影响其他v-bind:其的改变不影响其他

  • v-bind就是对属性的简单赋值,当内存中值改变,还是会触发重新渲染

v-for 的使用

  • 基本语法 v-for="item in arr"
  • 对象的操作 v-for="item in obj"
  • 如果是数组没有id
    v-for="(item,index) in arr" :class="index"
  • 各中 v-for 的属性顺序
    数组 item,index
    对象 value,key,index

关于对象内的 this

  • vue 已经把以前 this 是 window 或者事件对象的问题搞定了
  • methodsdata 本身是在同一个对象中的,所以在该对象中可以通过 this 随意取
  • this.xxxdata 中的值,this.xxxMethodmethods 中的方法

渲染组件-父使用子组件

  1. 创建子组件(对象)
    var Header = { template: '模板', data是一个函数,methods: 功能,components:子组件们}
  2. 在父组件中声明,根属性 components: {组件名: 组件对象}
  3. 在父组件要用的地方使用 <组件名></组件名>
    注意:在不同框架中,有的不支持大写字母,用的时候
    • 组件名 MyHeader
    • 使用 my-headers

父子组件传值(父传子)

  1. 父用子的时候通过属性传递
  2. 子要声明 props:['属性名']来接收
  3. 收到就是自己的了,随便使用
    • template 中直接用
    • 在 js 中 this 属性名用

小补充:常量传递直接用,变量传递加冒号

注册全局组件

  • 应用场景:多处使用的公共性功能组件,就可以注册成全局组件,减少冗余代码
  • 全局API:Vue.component('组件名',组件对象)

附加功能:过滤器&监视改动

  1. filter
    • 将数据进行添油加醋的操作
    • 过滤器分为两种
      • 组件内的过滤器(组件内有效)
      • 全局过滤器(所有组件共享)
    • 先注册,后使用
    • 组件内 filter:{过滤器名: 过滤器fn},最终 fn 内通过 return 产出最终的数据
    • 使用方式是 {{原有数据|过滤器名}}
    • 需求
      • 页面 input 框输入字符串,反字符串输出,按参数显示 label(中英文)
    • 过滤器 fn
      • 声明 function(data,argv1,argv2...){}
  2. watch 监视单个
  3. computed 监视多个
    * computed: {监视的业务名: function(){ return 显示的一些内容 }}
    • 使用 计算属性的名称

slot( 传递 DOM )

  • 内置的组件
  • slot 就是子组件里给DOM留下的坑
  • <子组件>DOM</子组件>
  • slot 动态的 DOM,props 是动态的数据

实质上,slot 其实就是父组件传递给 DOM 结构

vue提供的内置组件 <slot></slot>

<my-li>
  <button>111</button>
</my-li>

生命周期函数(钩子函数)

Vue 实例从创建到销毁的过程,就是生命周期

详细来说也就是从开始创建、初始化数据、编译模板、挂载 Dom、渲染→更新→渲染、卸载等一系列过程

  • beforeCreate
  • created
  • beforeMount
  • mounted
  • beforeUpdate
  • updated
  • beforeDestroy
  • destroyed
  1. beforeCreatecreated 钩子函数之间的生命周期

    进行初始化事件,进行数据的观测,在 created 的时候数据已经和 data 属性进行绑定
    需要注意的是,此时还是没有 el 选项

  2. created 钩子函数和 beforeMount 间的生命周期

    首先会判断对象是否有 el 选项如果有的话就继续向下编译,如果没有el选项,则停止编译也就意味着停止了生命周期,直到在该 vue 实例上调用 vm.$mount(el)

    如果 vue 实例对象中有 template 参数选项,则将其作为模板编译成 render 函数,如果没有 template 选项,则将外部 HTML 作为模板编译
    template 中的模板优先级要高于 outer HTML 的优先级

    render 函数选项 > template 选项 > outer HTML.

  3. beforeMountmounted 钩子函数间的生命周期

    给 vue 实例对象添加 $el 成员,并且替换掉挂在的 DOM 元素

    注意,beforeMount 方法中 $el 还未被创建,这期间 VUE 先后生成两份模板,但是在 beforeMount 之前只是虚拟的,并未真实存在

  4. mounted

id="app" 中的内容发生了变化

  1. beforeUpdate 钩子函数和 updated 钩子函数间的生命周期

    当 vue 发现 data 中的数据发生了改变,会触发对应组件的重新渲染,先后调用 beforeUpdateupdated 钩子函数

    beforeUpdate 时,可以监听到 data 的变化但是 view 层没有被重新渲染,view 层的数据没有变化。等到 updated 的时候 view 层才被重新渲染,数据更新

    需要注意的是,这里运行 console.log,在 beforeUpdated 钩子函数执行的时候里面的 DOM 元素就已经发生了变化,是因为还有 virtual DOM 这一层,实际上,我们打印 document.body.innerHTML,view 层是没有被重新渲染的

  2. beforeDestroydestroyed 钩子函数间的生命周期

    beforeDestroy 钩子函数在实例销毁之前调用
    destroyed 钩子函数在 Vue 实例销毁后调用

    需要注意的是,频繁的销毁和创建组件会影响页面性能,如果需要频繁的操作,我们可以使用 <keep-alive></keep-alive> 内置组件来控制组件的激活和停用,用法也就是包裹住需要销毁的组件的就好了

    <keep-alive>
        <test v-if="isExist"></test>
    </keep-alive>

    这个时候就用到了另一对钩子函数 activateddeactivated ,分别在组件被激活和被停用时触发

获取 DOM 元素

  • 救命稻草,document.querySelector

    • template 中标识元素 ref = "xxx"
    • 在要获取的时候,this.$refs.xxx 获取元素
      • 创建组件,装载 DOM,用户点击按钮
  • ref 在 DOM 上获取的是原生 DOM 对象

  • ref 在组件上获取的是组件对象

    • $el 是拿起 DOM
    • 这个对象就相当于我们平时玩的 this,也可以直接调用函数

Vue.nextTick 对异步函数的结果进行操作

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

需要了解的是 Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新

简单来说,Vue 在修改数据后,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新

//改变数据
`vm.message = 'changed'`

//想要立即使用更新后的 DOM。这样不行,因为设置 message 后 DOM 还没有更新
`console.log(vm.$el.textContent)` // 并不会得到 'changed'

//这样可以,`nextTick` 里面的代码会在 DOM 更新后执行,有一定的延时
Vue.nextTick(function(){
    console.log(vm.$el.textContent) //可以得到 'changed'
})

需要注意的是,createdmounted 阶段,如果需要操作渲染后的视图,要使用 nextTick 方法
比如刷新页面时在 mouted 事件中触发 input 元素的插入,并给 input 元素获取焦点

var App = {
  template: `<div>
    <input type="text" v-if="isShow" ref="input" />
  </div>`,

  data() {
    return {
      isShow: false
    }
  },
  mounted() {
    // 显示元素,并给与获取焦点
    this.isShow = true // 会触发 input 元素的插入,但这里并不会立马更新 DOM,最终代码更新完毕以后,vue才会根据实际的值,进行 DOM 的操作

    // 我们希望在 vue 真正渲染 DOM 到页面以后,才操作如下事件
    this.$nextTick(() => {
      this.$refs.input.focus()
    })
  },
}

注意 mounted 不会承诺所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以用 vm.$nextTick 替换掉 mounted

mounted: function () {
  this.$nextTick(function () {
    // Code that will run only after the
    // entire view has been rendered
  })
}

路由

路由原理

  • 传统开发方式 url 改变后 立刻发起请求 响应整个页面 渲染整个页面 可能会出现白屏的问题
  • SPA 锚点值改变后 不会发起请求 发起 ajax 请求 局部改变页面数据
    • 页面不跳转 用户体验更好

SPA

  • single page application ( 单页应用程序 )
  • 前端路由
    • 锚点值监视
    • ajax 获取动态数据
    • 核心点是锚点值
  • 前端框架 Vue / angular / react

命名路由

    1. 给路由对象一个名称 { name: 'home', path: '/home', component: Home}
    1. router-linkto 属性中描述这个规则
      • <router-link :to="{name: 'home'}"></router-link>
      • 通过名称找路由对象,获取其 path,生成自己的 href
  • 大大降低维护成本,锚点值改变只用 main.js 中改变 path 属性即可

小结

  • vue-router 使用步骤:1.引入 2.安装插件 3.创建路由实例 4.配置路由规则 5.将路由对象关联 vue 6.留坑
  • router-link to="/xxx" 命名路由
    1. 在路由规则对象中 加入 name 属性
    2. router-link:to="{name:"xxx"}"

vue-router 中的对象

  • $router 路由信息对象,只读对象
  • $router 路由操作对象,只写对象

配置 :to="{ name:'login', query:{id:1}} ,获取 this.$route.query.id,生成 <a href="/detail?id=1">

配置 :to="{ name:'register', params:{name:'abc'},获取 this.$route.params.id,生成 <a href="/detail/abc">

嵌套路由

  • 市面上所谓的用单页应用框架开发多页应用
    • 嵌套路由
  • 案例
    • 进入我的主页显示;电影、歌曲等多部分内容
  • 代码**
    • router-view 的部分
      • router-view 第一层中,包含一个 router-view
    • 每一个坑挖好了,要对应单独的组件

知识点介绍

  • 路由 meta 元数据 => meta 是对于路由规则是否需要验证权限的配置
    • 路由对象中和 name 属性同级 { meta:{isChecked: true} }
  • 路由钩子 => 权限控制的函数执行时期
    • 每次路由匹配后,渲染组件到 router-view 之前
    • router.beforeEach(function(to,from,next){ })

编程导航

  • 跳到指定的锚点,并显示页面 this.$router.push({name: 'xxx',query:{id:1},params:{name:'abc'} })

  • 配置规则 {name:'xxx',path:'/xxx/:name'}

  • 根据历史记录,前进或后退

    • this.$router.go(-1|1)
    • 1代表进一步,-1是退一步

本质是锚点值的变化

axios

这里极度推荐使用 axios 发请求,可以看下 axios中文文档

浏览器端发起 XMLHttpRequests 请求
node 端发起 http 请求
支持 Promise API
拦截请求和返回
转化请求和返回(数据)
取消请求
自动转化 json 数据
客户端支持抵御 XSRF(跨站请求伪造)

不像 jquery 那样嵌套,而是用 promise
只做 ajax 请求,不做其他的 DOM 之类的请求

聊聊 React 编程**

前阵子在部门内做了一个关于 React 的分享,为了更进一步地理解,也分享给有需要的小伙伴,做了如下整理 ^_^

image

Facebook 在 2013 年 5 月推出了全新的函数式编程,也就是全球范围内使用人数最多的前端框架 - React,也是目前最受欢迎的前端框架之一

现代框架与旧式框架的区别

React 是一个视图层框架,是用来解决数据和页面渲染的问题

同样的还要 VueAngular,这也是目前前端最受欢迎的三套框架,几乎是所有前端工程师必备的一项技能

所以我们在技术选型的时候,常常会产生困扰,究竟应该选择哪一门语言开发项目呢,比如

  • reactjs : 灵活性更大,处理大型业务时选择性更多一点
  • vuejs : api更多,实现功能更简单,但也因为api多,灵活性有一定限制

所以,在做复杂度比较高的项目时,大家倾向于 reactjs ,而面向用户端的一些复杂度不是特别高的项目时,用 vuejs 更简单

当然,vuejs 也可以做大型的项目,至于具体选什么框架,还需要取决于对框架的熟悉程度以及业务复杂度做一个权衡

而在几年前开发前端应用时,基本没这个困扰,因为大家开发都用 jQuery

image

但随着前端交互的复杂度越来越高,现代框架比如 reactvue 逐渐的替换掉了 jQuery,因为用现代框架来开发更容易维护

为什么会说变得容易维护呢?我们先来看看 reactjQuery 到底什么区别

image

我认为,他们之间编程**的最大区别,就是声明式命令式的区别

命令式

命令式编程,比如 jQuery,直接操作 DOM,告诉页面怎么挂载,怎么操作,整个程序有 70% 都是操作 DOM

<button class="red_btn" type="button">change</button> 
<script type="text/javascript"> 
  $(document).ready(function(){ 
    $("button").click(function(){ 
      if($("button").hasClass("red_btn")){ 
        $("button").removeClass("red_btn").addClass("green_btn") 
      } else {
        $("button").removeClass("green_btn").addClass("red_btn")
      }
    }) 
  })
</script>

例如上面的代码,点击一个按钮,切换 button 颜色。我们用 jQuery 这种命令式编程思路写,就是当前是什么颜色就让它变成另外一个颜色

但如果我们认真想想,其实这里面可以细分成两个行为,一个是对状态判断,另一个是操作 DOM。那声明式呢?

声明式

还是上面那个场景,我们用 React 提供的 JSX 语法来实现,当我们用 JSX 描述了映射关系之后,点击按钮事件时,只需要对颜色这个变量进行修改就可以完成需求了

handleChangeBtnColor() {
  this.setState({
    tag: !this.state.tag
  })
}

render() {
  return (
    <div>
      <button
        className={this.state.tag? 'red_btn' : 'green_btn'}
        type="button"
        onClick={this.handleChangeBtnColor}
      >
        change
      </button>
    </div>
  )
}

所以区别就出来了,用 react 来实现同样的需求,如果细分来看,我们在逻辑上只有状态这一个行为

jQuery 是两个行为,状态 + DOM 操作

那为什么 React 要用声明式来编程呢?

因为命令式编程是直接操作 DOM 节点,如果有多个事件,比如 100 个 button 上都有点击和取消事件,那频繁的事件操作会很大程度上产生 bug ,而且不可控,会有无法预期性的 bug 产生

而且声名式编程只需要我们操作数据就好,数据可能出现的几种情况我们能提前做好容错

声明式是通过描述状态与视图之间的映射关系来操作DOM,或者说具体点是用这样的映射关系来生成一个 DOM 节点插入到页面去

比如 React 提供的 JSX 和 Vue 中的模板语言

目的是为了实现声明式渲染的功能,本质上都是描述了 『状态』与『视图』之间的映射关系

状态与视图之间的映射关系,等同于 render 函数

在框架的内部,不论是 JSX 还是 Vue 的模板,最终会编译成 render 函数。

image

声明式渲染是现代框架的特性,也就是我们常说的数据驱动视图

这个特性跟声明式可以简化维护应用代码的复杂度有什么关系呢?

事实上,这个特性可以让我们把关注点只放在状态的维护上。这样一来,即使应用复杂后,我们管理代码的方式只在状态上,所有的视图操作都不用关心了,因为框架会帮我们自动去做,可以说大大降低代码维护的成本

what

状态

那我们所说的这个状态到底是什么呢?

  • 在现实中,状态是某一时刻看到或感受到的状况
  • 对于设计来说,状态是 UI 在交互过程中某一时刻的画面
  • 对于开发来说,状态是存储上下文中所用到的数据

image

React 官网上,我们可以看到其介绍是用于构建用户界面的 JavaScript,所以 React 本质上是一个创建 UI 接口的视图层框架

前面我们已经提到了 React 有个声明式**,它是数据到视图的一个静态映射,但在我们的实际项目中,并不是一个静态网页,还需要操作数据,网页的状态可能随时改变,那怎么才能让网页跟着状态一起改变呢?

响应式设计**

这就是 React 背后的响应式设计**

开发者只需要告诉 React 我们希望页面长什么样子,React 就会自动帮我们绘制界面

也就是说,我们只要操作数据,页面视图会自动作出响应,用户界面的展示完全取决于数据层

而且我们一切的操作都是基于内存之中,不会有较大的性能损耗,这就是 React 响应式编程的精髓,也是为何它叫作 ReactReact 在英文中是响应的意思

简单来说就是,不需要关注视图层,只需要关注数据层的变化

一个类 class 一定有一个构造函数 constructor,最优先被执行

constructor(props) {
  super(props)
}

constructor 接收 props 参数,super 指的是父类 Componentsuper(props) 方法指的是调用父类的构造函数

定义数据需要定义在状态里面 this.state

框架是怎么知道 Web 应用在运行时数据状态发生了变化呢? 这个问题是所有框架必须去解决的

不同的解决方案,导致的直接结果就是它所提供给用户的上层语法或 API 完全不一样,也是我们常对比的各个框架的使用区别

解决方案包括我们常说的 Virtual DOMdiff 算法对比

Virtual DOM 有兴趣的小伙伴可以查看我的另一篇博客 Virtual DOM 中那些你不知道的事,在这篇博客里有对 Virtual DOM 做一个详细的讲解

服务端渲染

既然提到了 Virtual DOM,这里就提一下 React 的价值 - nodejs 服务端渲染

因为有 Virtual DOM 的存在,React 可以很容易的将 Virtual DOM 转换为字符串,这便使我们可以只写一份 UI 代码,同时运行在 node 里和和浏览器里

var html = React.renderToString(el)

在 node 里将组件 HTML 渲染为一段 HTML 一句话即可,不过围绕 renderToString 还需要做一些准备工作

整个思路大致是:

  1. 从后台 server 或数据库等来源拉取数据
  2. 引入要渲染的 React 组件
  3. 调用 React.renderToString() 方法来生成 HTML
  4. 最后发送 HTML 和数据给浏览器

这就是 React 的服务端渲染,组件的代码前后端都可以复用

不仅如此,React 还能够用一套代码同时运行在浏览器和 node 里,而且能够以原生 App 的姿势运行在 iOS 和 Android 系统中,即拥有了 web 迭代迅速的特性,又拥有原生 App 的体验,也就是 React-Native

单向数据流

我认为使用 react最大好处在于功能组件化,遵守前端可维护的原则

react 是单向数据流,什么是单向数据流呢?

  • 数据主要从父节点传递到子节点( 通过 props ),即遵循从上到下的数据流向

  • 如果顶层( 父级 )的某个 props 改变了,react 会重渲染所有的子节点

image

通俗的理解是指用户访问 ViewView 发出用户交互的 Action,在 Action 里对 state 进行相应更新。state 更新后会触发 View 更新页面的过程。这样数据总是清晰的单向进行流动,便于维护并且可以预测

那为什么 react 要使用单向数据流呢?

实际上,单向数据流这种模式十分适合跟 react 搭配使用

它的主要**组件不会改变接收的数据。它们只会监听数据的变化,当数据发生变化时它们会使用接收到的新值,而不是去修改已有的值。当组件的更新机制触发后,它们只是使用新值进行重新渲染而已

消除了在多个地方同时管理状态,可能出现的数据不同步的情况,它只会在一个地方进行状态管理,减小了应用的复杂度,唯一的数据源将使得开发更加简单

需要注意的是,单向数据流并非单向绑定,甚至单向数据流与绑定没有任何关系

对于 react 来说,单向数据流( 从上到下 )与单一数据源这两个原则,限定了 react 中要想在一个组件中更新另一个组件的状态(类似于 vue 的平行组件传参,或者是子组件向父组件传递参数),需要进行状态提升

即将状态提升到他们最近的祖先组件中。子组件中 Change 了状态,触发父组件状态的变更,父组件状态的变更,影响到了另一个组件的显示(因为传递给另一个组件的状态变化了,这一点与 vue 子组件的 $emit() 方法很相似)

比如在做 list 删除时,为什么不可以直接把 list 传给子组件来改变 list?

因为父组件可以向子组件传值,但是子组件只能去使用这个值,不能去改变这个值

应该是父组件向子组件传递方法,子组件调用这个方法,传递一个数据,最终还是父组件自己来改变这个数据

Vue也是单向数据流,只不过能实现双向绑定,UI 控件提供了双向数据绑定的方式,在一些需要实时反应用户输入的场合会非常方便

但通常认为复杂应用中这种便利比不上引入状态管理带来的优势

所以无论是 vue 还是 react 其实还是提倡单向数据流去管理状态,这一点在 vuexredux 状态管理器上体现的很明显

虽然 vuereact 框架本身有自己状态管理,当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏

  • 多个视图依赖于同一状态
  • 来自不同视图的行为需要变更同一状态

所以就需要 vuexredux 来解决这个问题,redux 在我的另一篇博客大白话解析 Redux 、 redux-thunk 、redux-saga 和 react-redux 中介绍的很详细了,大家有兴趣可以去看看

注意:

单向数据流中的单向,指的是数据从父组件到子组件的这个流向叫单向

绑定单双向是指View层与Module层之间的映射关系

但我们通常也说双向数据绑定,带来双向数据流

数据( state )和视图( View )之间的双向绑定,ng 里的 ng-model 和 vue 里的 v-model

props

刚才我们提到了 props,怎么理解 props 呢?

propsproperty 的缩写,可以理解为 HTML 标签的 attribute

在组件内部,可以通过 this.props 来访问 propsprops 是组件唯一的数据来源

不可以使用 this.props 直接修改 props,因为 props 是只读的props 是用于整个组件树中传递数据和配置

PropTypes 与 DefaultProps

react 为我们提供了一套非常简单好用的属性校验机制,强校验:

// 对TodoItem的一些属性类型做校验
TodoItem.propTypes = {
  content: PropTypes.string.isRequired,
  deleteItem: PropTypes.func,
  index: PropTypes.number
}

// 设置TodoItem的一些默认属性
TodoItem.defaultProps = {
  content: 'hello world'
}

PropTypes 包含的校验类型包括基本类型、数组、对象、实例、枚举

state

React 的一大创新,就是把每一个组件都看成是一个状态机,组件内部通过 state 来维护组件状态的变化,这也是state 唯一的作用

每个组件都有属于自己的 statestateprops区别在于前者 ( state ) 只存在于组件内部,只能从当前组件调用 this.setState 修改 state 值( 不可以直接修改 this.state

一般我们更新子组件都是通过改变 state 值,更新子组件的 props 值从而达到更新

state 一般和事件一起使用,比如有一个简单的开关组件,开关状态会以文字的形式表现在按钮的文本上

首先需要在 render 方法中返回了一个 button 元素,给 button 注册了一个事件用来处理点击事件,在点击事件中对 state 的描述开关状态的字段,比如 on 取反,并执行 this.setState() 方法设置 on 字段的新值。一个开关组件就完成了

react 通过将事件处理器绑定到组件上来处理事件

react 事件本质上和原生 JS 一样,鼠标事件用来处理点击操作,表单事件用于表单元素变化等,react 事件的命名、行为和原生 JS 差不多,不一样的地方是 react 事件名区分大小写

事件的处理器需要由组件的使用者来提供,可以通过 props 将事件处理器传进来

image

这是一个 react 组件实现组件可交互所需的流程,render() 输出 Virtual DOMVirtual DOM 转为 DOM,再在 DOM 上注册事件,事件触发 setState() 修改数据,在每次调用 setState 方法时,react 会自动执行 render 方法来更新 Virtual DOM,如果组件已经被渲染,那么还会更新到 DOM 中去

setState 方法

新版的 setState 可以接收一个函数而不是一个对象了,需要有一个返回值 return
所以我们可以在项目中做一些优化,比如

handleInputChange(e) {
  this.setState({
    inputValue: e.target.value
  })
}

可以优化为

handleInputChange(e) {
  const value = e.target.value
  this.setState(() => ({
      inputValue: value
  }))
}

注意:当有 e.target.value 这种异步设置数据的时候,需要存在外层

handleBtnClick() {
  this.setState({
    list: [...this.state.list, this.state.inputValue],
    inputValue: ''
  })
  }

可以用 prevState,改为

handleBtnClick() {
  this.setState((prevState) =>({
    list: [...prevState.list, prevState.inputValue],
    inputValue: ''
  }))
}

handleDeleteItm(index) {
  const list = [...this.state.list] // 拷贝list数组
  list.splice(index,1)
  this.setState({
    list
  })
}

可以改为

handleDeleteItm(index) {
  this.setState((prevState) => {
    const list = [...prevState.list]
    list.splice(index,1)
    return {list}
  })
}

props 与 state

尽可能使用 props 当做数据源,state 用来存放状态值( 简单的数据 )

也就是说咱们通常用 props 传递大量数据,state 用于存放组件内部一些简单的定义数据

当组件的 state 或者 props 发生改变的时候,render 函数就会重新执行

当父组件的 render 函数被运行时,它的子组件的 render 都将重新被运行一次

单向数据流和单向数据绑定是什么区别呢

前面已经提到了单向数据流,需要按照它的顺序办事。比如我们假设有一个这样的生命周期:

  1. 从 data 里面读取数据
  2. ui 行为( 如果没有 ui 行为就停在这里等他有了为止 )
  3. 触发 data 更新
  4. 再回到步骤1

改了一个数,view 层不能反回头来找他来更新 view 层视图( 从步骤 2 跳回去 1 ),你得等下一个循环(转了一圈)的步骤 1 才能更新视图。react 就是这样子,你得 setState 触发更新,如果你 this.state = {...},是没用的,它一直不变

单向数据绑定,就是绑定事件,比如绑定 onInputonChangestorage 这些事件,只要触发事件,立刻执行对应的函数(代表 react)

双向数据绑定,我们一般是借用 js 底层的 Object.defineproperty ( 代表 Vue )

这是 Vue 双绑的核心**,view 层能让 model 层变了,model 层也能让 view 层变了

要判断是单向绑定还是双向绑定,只需要手动去控制台改一下那个核心绑定的数据,view 层的显示内容能马上变化的就是双绑,不能马上有变化的只是单向数据

想做到像 Vue 那样的极致双绑,能够在控制台改个数据就改变视图的,大概就只有 defineproperty(据说新版 vue 现在用 ES6proxy )和定时器轮询了

既然说到了数据流,那组件间是怎么进行通信的呢?

组件通信

一般来说,有两种通信方式

父子组件通信

react 中,最为常见的组件通信也就是父子了,一般情况是:

父组件更新组件状态 -----props-----> 子组件更新

另一种情况是

子组件更新父组件状态 -----需要父组件传递回调函数-----> 子组件调用触发

可能大家对于第二种子组件更新父组件状态的情况有些不理解

一般情况下,只能由父组件通过 props 传递数据给子组件,使得子组件得到更新

那么现在,我们想实现子组件更新父组件,就需要父组件通过 props 传递一个回调函数到子组件中,这个回调函数可以更新父组件子组件就是通过触发这个回调函数,从而使父组件得到更新

兄弟组件通信

当两个组件处于同一级时( 同处父级,或者同处子级 ),就称为兄弟组件

这里也有两种实现方式

方式一

按照React单向数据流方式,我们需要借助父组件进行传递,通过父组件回调函数改变兄弟组件的props

其实这种实现方式与子组件更新父组件状态的方式是大同小异的

方式一只适用于组件层次很少的情况,当组件层次很深的时候,整个沟通的效率就会变得很低

方式二

React官方给我们提供了一种上下文方式,可以让子组件直接访问祖先的数据或函数,无需从祖先组件一层层地传递数据到子组件中

但这种方法建议按需使用,可能会导致一些不可预期的错误。( 比如数据传递逻辑结构不清晰 )

组件划分

前面已经提到使用 react最大好处在于功能组件化,遵守前端可维护的原则

事实上,react 组件化开发原则是组件负责渲染 UI,组件的不同状态对应着不同 UI,通常遵循以下组件设计思路

  • 布局组件:仅仅涉及应用 UI 界面结构的组件,不涉及任何业务逻辑,数据请求及操作

  • 容器组件:负责获取数据,处理业务逻辑,通常在 render() 函数内返回展示型组件

  • 展示型组件:负责应用的界面 UI 展示

  • UI 组件:指抽象出的可重用的 UI 独立组件,通常是无状态组件

实际项目中,最好将 UI 组件和容器组件拆分, UI 组件负责页面渲染,容器组件负责页面逻辑

当组件中只有一个 render 函数时,就可以定义成无状态组件

无状态组件的性能比较高,因为它就是一个函数,而 React 里边普通的组件是 JS 里边的一个类,这个类生成的对象里,还会有一些生命周期函数,所以它执行起来,既要执行生命周期函数,又要执行 render ,它要执行的东西远比函数执行的东西多的多,所以一个普通组件的性能是肯定赶不上无状态组件的

生命周期函数

React的组件拥有一套清晰完整而且非常容易理解的生命周期机制

大体可以分为三个过程:初始化更新销毁

在组件生命周期中,随着组件的 props 或者 state 发生改变,它的 Virtual DOMDOM 表现也将有相应的变化

6721543734532_ pic

什么叫生命周期函数?生命周期函数指在某一时刻组件会自动调用执行的函数

// constructor 可以理解为一个生命周期函数,它是 ES6 的语法规定的。在组件一创建就会被调用,页面初始化 Initialization
constructor(props) {
  super(props)
  // 当组件的 state 或者 props 发生改变的时候,render 函数就会重新执行  
  this.state = {
    inputValue: 'hello',
    list: ['学习英文', '学习react']
  }
  this.handleInputChange = this.handleInputChange.bind(this)
  this.handleBtnClick = this.handleBtnClick.bind(this)
  this.handleDeleteItm = this.handleDeleteItm.bind(this)
}

// 在组件即将被挂载到页面的时候执行
componentWillMount() {
  console.log('componentWillMount')
}

render() {
  console.log('parent render')
  return (
    <div>......</div>
  )
}

// 组件被挂载到页面之后自动执行
componentDidMount() {
  console.log('componentDidMount')
}

// 当组件被更新之前,他会自动执行
shouldComponentUpdate() {
  console.log('shouldComponentUpdate')
  return true
}

// 组件被更新之前,它会自动执行,但是它在 shouldComponentUpdate 之后执行
// 如果 shouldComponentUpdate 返回true它才执行
// 如果返回 false ,这个函数就不会执行了
componentWillUpdate() {
  console.log('componentWillUpdate')
}

componentDidUpdate() {
  console.log('componentDidUpdate')
}

在子组件中

render() {
  console.log('child render')
  const { content } =  this.props
  return (
    <div onClick={this.handleClick}>
      {content}
    </div>
  )
}

handleClick() {
  const { deleteItem, index } = this.props
  deleteItem(index)
}

// 一个组件从父组件接受参数
// (只要父组件的render函数被重新执行了,子组件的这个生命周期函数就会被执行)
// 如果这个组件第一次存在于父组件中,不会执行
// 如果这个组件之前已经存在于父组件中,才会执行
componentWillReceiveProps() {
  console.log('child componentWillReceiveProps')
}

// 但这个组件即将被剔除时执行
componentWillUnmount() {
  console.log('child componentWillUnmount')
}

Mount 是指组件被挂载执行的过程,Updation 是指组件被更新执行的过程,什么情况发生更新呢?要么是 state 被更新,要么是 props 被更新,也就是数据发生变化的时候,页面会更新

需要注意的是,所有的生命周期函数都可以不存在,但是有一个生命周期函数必须得有,就是 render 函数

它的底层为什么会有这样的设定呢?原因就是组件是继承自 Component 这个组件的,React Component 这个组件里边默认内置了其他所有的生命周期函数,唯独没有内置 render 函数,所以对组件来说,render 是必须自己定义的,不然就会报错

React 生命周期函数的使用场景

上面讲了 React 的生命周期函数,有一个比较容易忽视的钩子函数 shouldComponentUpdate ,他的使用过程是怎样的呢?

我们先来做个测试,看下子组件的渲染过程,先把所有的生命周期函数都删除掉,只在子组件的 render 函数中留下 console.log('child render')

然后我们在 input 框中输入内容,在控制台可以看到这样的结果

image

也就是父组件 render 函数重新执行的时候,子组件的 render 函数也会跟着执行

这样的逻辑是没有问题的,但是它会带来性能上的损耗

父组件上的内容发生变化了,其实子组件的内容是没必要重新渲染的,而这样的机制会导致子组件要做很多无谓的渲染

那应该怎样做性能优化呢?

很简单,这个时候我们就可以利用生命周期函数 shouldComponentUpdate 来做性能优化了

shouldComponentUpdate 这个函数的意思是,当数据或者内容发生变化的时候,会先询问一下,组件是否要被真正的更新

shouldComponentUpdate(nextProps, nextState) {
  if(nextProps.content !== this.props.content) {
    return true
  } else {
    return false
  }
}

shouldComponentUpdate 一般会接收两个参数,一个是nextProps,另一个是 nextState

当一个组件要被更新的时候,props 要被更新成什么样呢?
nextProps 指的是接下来 props 要被变化成什么样,nextState 指的是接下来 state 要被变化成什么样

我们的组件 props 接收的 content ,如果 content 发生变化,这个组件才需要重新渲染,没有发生变化时,不需要发生渲染

这样就通过 shouldComponentUpdate 这个生命周期函数提升了组件的性能,可以避免一个组件做无谓的 render 操作

render 函数重新执行,就意味着 React 底层要生成一份 Virtual DOM ,和之前的 Virtual DOM 做比对,虽然 Virtual DOM 的比对比 Actual DOM 的比对要快的多,但是,如果能省略这个比对过程当然能节约更多的性能

性能优化

当然,React 当中有很多关于性能优化的点

  • 首先是 this.handleClick.bind(this) 这样的方法,如果要改变作用域的话,我们把作用域的修改放在 constructor 里边,这样可以保证整个程序里边这个函数的作用域绑定只会执行一次,而且可以避免组件的一些无谓渲染,所以,这样写代码,react 组件的性能会有所提升

  • 其次, react 的底层 setState 内置了性能提升的机制,是一个异步的函数,可以把多次数据的改变结合成一次来做,这样可以降低 Virtual DOM 的比对频率

  • 再者 react 的底层使用了 Virtual DOM 的概念,还有同层比对,还有 key 的概念,来提升 Virtual DOM 比对的速率,从而提升 react 的性能

  • 最后,也就是借助 shouldComponentUpdate 这个方法,可以提高 react 组件的性能,因为我们可以避免无谓的组件的 render 函数的运行

监听数据对象

由于之前用过一段时间的 Vue,在转到 React 开发的时候,可以明显的发现 React 并没有 Vue 可以 watch 数据对象的方法

React 是怎么检测数据对象的变化呢?

React 默认不是双向绑定的,它不监听数据对象,而是通过手动调用 setState() 方法来触发了 Virtual DOM 的更新,再用 diff 算法来进行 Virtual DOM 比较前后两个状态的不同,看看是哪个 DOM 节点更新了,然后针对性的更改变化了的 DOM 结构实现数据更新,渲染 Actual DOM

我们单纯的使用 React,状态发生变化,会触发组件生命周期中的如下方法:

componentWillUpdate(object nextProps, object nextState) 

componentDidUpdate(object prevProps, object prevState) 

但如果结合 Redux 使用,一般状态变化是由 Dispatch 引起的,我们可以在 Dispatch 的回调中执行相应的操作

image

函数式编程

react 把需要不断重复构建的 UI 抽象成了组件,它充分利用很多函数式的方法减少了冗余代码

可以说,函数式编程是 React 的精髓

那到底什么是函数式编程呢?

函数式编程或称函数程序设计,又称泛函编程,是一种编程范型,它将电脑运算视为数学上的函数计算,并且避免使用程序状态以及易变对象

比起命令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。

也就是说,函数式编程和命令式编程最大的区别是:

函数式编程关心数据的映射,而命令式编程关心解决问题的步骤

而且维护方便,面向测试的开发流程

一个高阶函数,它可以接收函数可以当参数,也可以当返回值,这就是函数式编程

像柯里化、装饰器模式、高阶组件,都是相通的,一个道理

举个简单的 🌰

function first () {
  console.log('zhangshan')
}
function second() {
  console.log('lisi')
}

现在想在每条 console 语句前后各加一条 console 语句,如果在每个函数都加上 console 语句,会产生不必要的耦合,所以高阶函数就派上了用场

function FuncWrapper(func) {
  return function () {
    console.log('before')
    func()
    console.log('after')
  }
}
var first = FuncWrapper(first)
var second = FuncWrapper(second)

我们写了一个函数 FuncWrapper,该函数接一个函数作为参数,将参数函数装饰了一层,返回出去,减少了代码耦合

在设计模式中称这种模式为装饰器或装饰者模式

React 中,高阶组件 HOC 就相当于这么一个 FuncWrapper,传入一个组件,返回被包装或者被处理的另一个组件

高阶组件

a higher-order component is a function that takes a component and returns a new component.

高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件

高阶组件就是一个没有副作用的纯函数

本质上是一个类工厂,先举个简单的 🌰

组件一:

import React from 'react'

export default class First extends React.Component {
  constructor (props) {
    super(props)
    this.changeHandle = this.changeHandle.bind(this)
  }
  changeHandle (value) {
    console.log(value)
  }
  render () {
    return (
      <div>
        <h2>zhangsan</h2>
        <input type="text" onchange={value => this.changeHandle(value)}/>
      </div>
    )
  }
}

组件二:

import React from 'react'

export default class Second extends React.Component {
  constructor (props) {
    super(props)
    this.changeHandle = this.changeHandle.bind(this)
  }
  changeHandle (value) {
    console.log(value)
  }
  render () {
    return (
      <div>
        <h2>lisi</h2>
        <input type="text" onchange={value => this.changeHandle(value)}/>
      </div>
    )
  }
}

有两个不相同的组件,但是有部分功能重合,比如 h2 标题的内容,changeHandle 函数,这样也就造成了代码的冗余

理解了高阶函数,再解决这类问题就不难了吧?接下来我们加入高阶组件解决这个问题

高阶组件:

import React, { Fragment } from 'react'

// 定义装饰器的外层函数
function HocUITest(name) {
  // 返回一个装饰器函数
  return function CompWrapper (Component) {
    return class WarpComponent extends React.Component {
      constructor (props) {
        super(props)
        this.handleChange = this.handleChange.bind(this)
      }

      handleChange (value) {
        console.log(value)
      }

      render () {
        return (
          <Fragment>
            <h2>{name}</h2>
            <Component handleChange={this.handleChange} {...this.props}></Component>
          </Fragment>
        )
      }
    }
  }
}
export default HocUITest

在高阶组件返回包装好的组件的时,我们将高阶组件的 props 展开并传入包装好的组件中,这是确保给高阶组件的 props 也能给到被包装的组件上

简化接下来的两个组件

组件一:

import React from 'react'
import HocUITest from './Test'

@HocUITest('zhangsan')
class First extends React.Component {
  constructor (props) {
    super(props)
    console.log(props)
  }
  render () {
    return (
      <div>
        <input type="text" onChange={value => this.props.handleChange(value)}/>
      </div>
    )
  }
}

// First = HocUITest('zhangsan')(First)

export default First

组件二:

import React from 'react'
import HocUITest from './Test'

@HocUITest('lisi')
export default class Second extends React.Component {
  constructor (props) {
    super(props)
    console.log(props)
  }
  render () {
    return (
      <div>
        <input type="text" onChange={value => this.props.handleChange(value)}/>
      </div>
    )
  }
}

高阶组件的用途很多,比如代码复用,逻辑抽象,抽离底层代码,渲染劫持,更改 state、更改 props 等等

包括我们经常用到的 react-reduxconnect 函数

reduxstateaction 创建函数,通过 props 注入给了 Component

你在目标组件 Component 里面可以直接用 this.props 去调用 redux stateaction 创建函数了

ConnectedComment = connect(mapStateToProps, mapDispatchToProps)(Component)

相当于

// connect是一个返回函数的函数(就是个高阶函数)
const enhance = connect(mapStateToProps, mapDispatchToProps)
// 返回的函数就是一个高阶组件,该高阶组件返回一个与Redux store
// 关联起来的新组件
const ConnectedComment = enhance(Component)

antdForm 组件也是一样的

const WrappedNormalLoginForm = Form.create()(NormalLoginForm)

上述高阶组件中我们用了 ES6 装饰器语法,@HocUITest('lisi') 就是一个装饰器,它修改了类的行为

也就是说,装饰器是一个对类进行处理的函数

需要注意的是,装饰器对类的行为的改变,是代码编译时发生的,而不是在运行时

这意味着,装饰器能在编译阶段运行代码。也就是说,装饰器本质就是编译时执行的函数。

为了传递更多的参数,上面的装饰器函数外面又封装了一层函数

比如,我们实际开发时,React 与 Redux 库结合使用时,常常需要写成下面这样

class MyComponent extends React.Component {}
export default connect(mapStateToProps, mapDispatchToProps)(MyComponent)

有了装饰器,就可以改写上面的代码

@connect(mapStateToProps, mapDispatchToProps)
export default class MyComponent extends React.Component {}

接下来我们主要说一下两种功能的 react 高阶组件:属性代理、反向继承

属性代理

高阶组件将它收到的 props 传递给被包装的组件,所叫属性代理

主要用来处理以下问题

  • 更改 props
  • 抽取 state
  • 通过 refs 获取组件实例
  • 将组件与其他原生 DOM 包装到一起

反向继承

为什么叫反向继承,是高阶组件继承被包装组件,按照我们想的被包装组件继承高阶组件

反向代理主要用来做渲染劫持

所谓的渲染劫持,就是最后组件所渲染出来的东西或者我们叫 React Element 完全由高阶组件来决定,通过我们可以对任意一个 React Elementprops 进行操作;我们也可以操作 React ElementChild

用过 React-Redux 的人可能会有印象,使用 connect 可以将 reactredux 关联起来,这里的 connect 就是一个高阶组件

ref 的使用

refreference 的简写,它是一个引用,在 React ,可以使用 ref 操作 DOM

<input
  id='insertArea'
  className='input'
  value={this.state.inputValue} 
  onChange={this.handleInputChange}
  ref={(input) => {this.input = input}}
/>

React 16 的新语法中,ref 应该等于一个函数(箭头函数)

ref={(input) => {this.input = input}} ,构造了一个 ref 引用,这个引用叫 this.input ,它指向 input 对应的 DOM 节点。所以 this.input 指向的就是 input 框的 DOM

所以下面的方法

handleInputChange(e) {
  const value = e.target.value
  this.setState(() => ({
      inputValue: value
  }))
}

可以改为

handleInputChange() {
    const value = this.input.value
    this.setState(() => ({
        inputValue: value
    }))
  }

应该尽量保持少操作 DOM ,setState 是异步函数,操作 DOM 时必须写到回调中

比如

<Fragment>
  <div>
    <label htmlFor='insertArea'>输入内容</label>
    <input
      id='insertArea'
      className='input'
      value={this.state.inputValue} 
      onChange={this.handleInputChange}
      ref={(input) => {this.input = input}}
    />
    <button onClick={this.handleBtnClick}>提交</button>
  </div>
  <ul ref={(ul) => {this.ul = ul}}>
    {this.getTodoItem()}
  </ul>
</Fragment>
 handleBtnClick() {
  this.setState((prevState) =>({
    list: [...prevState.list, prevState.inputValue],
    inputValue: ''
  }), () => {
    console.log(this.ul.querySelectorAll('div').length)
  })
}

ref 是帮助我们在 React 中直接获取 DOM 元素的时候使用的,一般情况下尽量避免使用 ref ,但是有的时候一些极其复杂的业务,比如动画的时候,不可避免的还是要用到 DOM 标签,怎么用呢,就用 ref 来获取 DOM 标签

注意,refsetState 合用的时候,DOM 的获取并不及时,原因是 setState 是异步的,如果希望页面更新之后再去获取 DOM ,需要把获取 DOM 的语法放在 setState 的第二个参数里边,它是一个回调函数

补充知识

开发环境搭建

快速搭建 React 的开发环境有两种方法

  1. 通过 CDN 引入 .js 文件来使用 React

  2. 使用 create-react-app 脚手架工具来编码

脚手架是前端开发过程中的一个辅助工具,自动构建一个大型项目的开发流程和目录,允许我们以一定方式实现 js 文件的相互引用,更方便的对项目进行管理

在脚手架的代码并不能直接运行,需要脚手架进行编译,编译出来的代码才可以被浏览器识别运行,一般会使用 webpackgulp 这样的工具

工程目录简介

image

  • yarn.lock - 项目依赖的安装包
  • package.json - node 的包文件,包含项目的介绍、项目依赖的包、指令供调用,让项目变成node的包
  • public favicon.ico - 项目左上角图标,index.html 模板
  • src index.js - 整个程序运行的入口文件

注意事项

state 不允许做任何改变,可以先拷贝 state 中的值

const list = [...this.state.list]

需要注意的是

  1. 在 JSX 语法中, {{}} 是表示 JS 表达式里的 JS 对象

  2. 转义的情况下,比如输入:<h1>hello</h1> 会显示 <h1>hello</h1>

    <li 
      key={index} 
      onClick={this.handleDeleteItm.bind(this,index)}
    >
      {item}
    </li>

不转义的情况下,比如输入:<h1>hello</h1> 会显示 hello

```jsx
<li 
  key={index} 
  onClick={this.handleDeleteItm.bind(this,index)}
  dangerouslySetInnerHTML={{__html: item}}
>
</li>
```
  1. React 中使用表单时,label 元素的 for 标签要替换成 htmlFor

    <label htmlFor='insertArea'>输入内容</label>
    <input
      id='insertArea'
      className='input'
      value={this.state.inputValue} 
      onChange={this.handleInputChange.bind(this)}
    />

使用 Charles 实现本地数据 mock

在前端开发代码的时候实际上和后端是分离的,也就需要在本地进行接口数据的模拟,这个时候就需要使用 Charles 进行接口数据的模拟

具体操作我在博客大白话解析 Redux 、 redux-thunk 、redux-saga 和 react-redux里边已经讲到了,有兴趣的小伙伴可以去看看

我们先 npm install axios --save 安装 axios

然后在程序中引入 axios: import axios from 'axios'

componentDidMount() {
  axios.get('/list.json')
    .then((res) => { 
      console.log('data',res.data) 
      this.setState(() => ({      
          list: [...res.data]
      }))
    })
    .catch(() => console.log('error'))
}

image

Charles 这个工具的原理是什么?

它可以抓到浏览器向外发送的请求,然后对一些请求做一些处理,比如说,抓取到请求的是http://localhost:3000/list.json

他有一个规则是,只要你请求的下面这个地址

image

就会把 Local path 这个本地文件的内容返回给你

所以 Charles 其实就是一个中间的代理服务器,可以抓取到浏览器的请求,如果有些接口是需要模拟的话,就可以使用 CharlesMap Local 这个功能去模拟数据

当然,用脚手架的话,我们也可以在 public 文件夹中 mock 请求

结尾

这篇文章主要从编程**入手剖析 React,包括如何快速构建组件和应用,让你快速了解 React 的编程原理

当然,对于构建大型应用,我们还需要结合 Reduxreact-routeraxios,大家有兴趣的话,都可以瞧瞧 ^_^

超级好用的流程库 - ggEditor

在一些中后台应用的开发中,有些业务往往需要用到流程设计器,比如:管理网络、审批流程等等...正是因为有多种业务都需要使用到流程图的功能,所以对于能快速产出一个流程设计器是非常必须的

工具选择

古话说的好,预先善其事,必先利其器。所以首先要做的就是选择一个合适的开源基础库,站在巨人的肩膀上,总是跑的更快

开源时代,社区的资源总是非常丰富,身为小程序媛的我漏出幸福的微笑😊

  • joint.js 支持多种交互式图表创建,但有收费版本和免费版本的区分,更丰富的功能可能就需要收费了
  • jsPlumb 是一套完全开源的流程图创建工具,上手简单,底层是基于 Canvas 技术,但唯一美中不足的是,基于 jQuery,所以对于大批量流程的操作,在性能上可能不能达到最佳
  • d3 d3 应该很多人都熟悉,非常好的可视化基础库,相应的,上手成本相对高一些
  • spritejs 同样是基于 Canvas,据说是月影大大一个人写的(✨✨眼),官网写的超好啦...
  • ggEditor 蚂蚁金服数据可视化团队的大神高力结合 react + g6 开源的流程库,它的前身是 g6Editor,也是他们团队开源的产品,官方说法是学习成本太高,停止了对外支持。底层也是 Canvas 技术,虽然 ggEditor 目前开源的文档不多,但是潜力无穷,使用起来非常便捷,上手也比较简单,貌似是去年开源的,社区相对还没那么活跃,所以它的使用依然是一个摸索的过程

最终,我选择了 ggEditor,因为它完全开源,使用很简单,很轻量级,而且天然基于 react,非常完美

快速构建

引用 ggEditor

我们可以按照 github 上 ggEditor 的安装步骤操作

启动之后的界面效果可以参考:http://ggeditor.com/demo/#/flow

我们可以看到它的官方 demo 功能是比较齐全的,基本上已经实现了一个元素拖拽及元素属性编辑的完整功能

属性解析

我们知道整个流程编辑器基本可以分为三部分,一部分是左侧的元素面板区,另一部分是右侧的元素属性配置区,再就是中间的流程展示区,每个流程元素上可以有相应的事件响应

所以这里主要介绍以上相应的属性

  • Item 节点配置

    • 其中 type 有 node 和 edge 两个值可选,node 就是节点,edge 是连接节点的连线,当我们初始化加载元素和保存导出元素数据时也是以这两种为key
    • shape 可选参数有:圆形 flow-circle | 圆角矩形 flow-rect | 菱形 flow-rhombus | 椭圆矩形 flow-capsule
    • src 可以引入一张图片作为当前节点的预览样式
    • 编辑器画板中的样式是由 model 决定,model 默认会继承 组件的 props.shapeprops.size,所以通常 model 只需配置 color、label。配置项如下:
      model: {
        color: '#333', // 节点主题色(选中颜色、激活颜色基于该值)
        size: [10, 10], // [x, y] 节点尺寸
        shape: 'cirle', // 图形:圆形 circle | 圆角矩形 rect | 菱形 rhombus | 椭圆矩形 capsule
        style: { // 关键形样式(可覆盖color的普通样式,但激活、选中依然无效,坑!)
          fill: 'red', // 填充背景
          stroke: 'blue' // 形状描边
        },
        label: { // 节点标签
          text: '开始节点', // 文本内容
          fill: 'green' // 文本颜色
        },
        index: 1 // 渲染层级
      }
  • Flow 编辑器配置
    在 组件上,最重要的是监听事件:
    js <Flow onNodeClick={(e) => { console.log(e); }}/>
    更多的我们可以参考页面事件 Page Events

    • 拖拽节点时,区分是新节点还是旧节点。可以用onDrop - 监听拖拽放置事件,如果是从元素面板区拖拽新节点到画布上,onDrop 返回的事件对象中 currentItemcurrentShape 都是 undefined。而如果是挪动旧节点的位置,这两个字段会记录拖动的图形图项。当然,也可以监听节点拖动结束事件 - onNodeDragEnd,这个事件只会在拖动画布上的节点时才触发

    • 锚点连线取消,比如有一个需求是当目标节点已经连线的时候,就取消连线。可以用 onAfterChange 事件,根据 item(type: 'edge') 或者 model(source:'xxxx') 对象中的参数进行判断。最后用结合异步函数和 <withPropsAPI> 组件取消连线:

      handleAddItem = (e) => {
        this.apiAction('undo')
      }
      
      apiAction = (command) => {
        const { propsAPI } = this.props
        setTimeout(() => {
          propsAPI.executeCommand(command)
        }, 0)
      }

      <withPropsAPI>是 ggEditor 自带的包装组件,同时它又自带 propsAPI 属性,更多的属性值我们可以参考 propsAPI

    • 保存数据。上面 <withPropsAPI> 提供的 propsAPI 属性中,包含了 save() 方法�。我们可以封装一个 SaveButton 组件,暴露一个 onSave 事件,然后用 <withPropsAPI> 包装该组件。具体可参考 demo

    • 自定义键盘操作。可以通过监听 onKeyDownonKeyUp 手动创建多个快捷命令

      handleKeyUp = e => {
        // 键盘抬起时重置记录的按键
        this.keysDown = ''
      }
      
      handleKeyDown = e => {
        // 拼接按键命令
        if (this.keysDown.length === 0) {
          this.keysDown = e.domEvent.key
        } else {
          this.keysDown += `+${e.domEvent.key}`
        }
      
        // 自定义键盘操作
        switch (this.keysDown) {
          case 'Meta+c':
            this.diyCopyCommand()
            break
          case 'Control+c':
            this.diyCopyCommand()
            break
          case 'Meta+v':
            this.diyPasteCommand()
            break
          case 'Control+v':
            this.diyPasteCommand()
            break
          default: break
        }
      }
      
      // 自定义复制
      diyCopyCommand = () => {
        const { propsAPI } = this.props
        let selected = propsAPI.getSelected()
        if (selected.length > 0) {
          this.commandAction('copy')
        }
      }
      
      // 自定义粘贴
      diyPasteCommand = () => {
        this.commandAction('paste')
      }

以上只是 ggEditor 的核心组件和功能。还有很多组件没有提交,比如 <Minimap><ContextMenu><Toolbar> ,我们可以具体去看项目 demo

其他的属性解析可以参考官网 API

自定义节点

如果 <Item> 自带的参数不满足需求,可以使用 <ReisterNode> 来封装自己的 <Item>

比如自定义一个 start-node,就可以自行封装一个 node 组件

具体可以参考 Issues

兼容 IE11

ggEditor 是基于 umijs 脚手架的, umijs不得不说,太简单粗暴,默认不支持IE,如果需要支持,需要我们开启配置

需要在 .umirc.js 中配置

targets: {
  ie: 11,
}

dva 版本问题

Chrome warning

image

IE bug

image

查看后发现都是指向同一个问题,因为 dva 的最新版本,具体可以看看升级dva最新版提示Warning: Please use require("dva").dynamic instead of require("dva/dynamic"). Support for the latter will be removed in the next major release.2.6.0-beta.4 版本新抛出警告

为了快速解决这个问题,我去瞅了眼 ant-design-pro,发现它三天前提交的,但是dva版本仍是用的低版本,非常 nice

以上,便可快速解决 IE11 白屏的问题

参考文档

在 React 项目中引入 GG-Editor 编辑可视化流程

常见问题

PS:终于用上自己写的博客系统写文章啦,体验还是非常八错的 😊😊

通过npm发布的第一个React组件的实践过程

前言

从开始接触react也有差不多四个月时间了,用的频繁了之后,也越发的“默契”。最近在项目中要开发一些定制化组件,写了些组件想要集中的管理,就打算把组件放在npm上,在网上查找资料学习了下,下面就是通过npm发布自己的第一个React组件的实现过程

注册

npm的注册就跟我们注册微博等其他社交账号一样,基本都是输入邮箱、用户名等等信息,十分简单也不耗时

注册网址:https://www.npmjs.com

新建项目

在github上新建一个项目(这里不做过多赘述,相信使用过github的盆友应该会很熟悉),然后git clone下来之后进入到项目目录,执行npm init,按提示输入信息,主要有

  • name:发布的名称
  • version:版本号
  • entry:入口文件

刚开始没填好没关系,后期也可以修改

具体封装实现

我在这个项目里面封装了一个react-hover-menu的组件

这是完成后的项目树:

image

配置package.json

npm相关信息

"name": "react-hover-menu",
"version": "1.0.3",
"description": "react hover or click menu component",
"main": "dist/bundle.js",
"files": [
  "dist"
],
"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "build": "webpack"
},
  • main: 这里是我们组件的入口文件。开发者在 import 我们的组件的时候会引入这里 export 的内容

  • files: 申明将要发布到 npm 的文件。如果省略掉这一项,所有文件包括源代码会被一起上传到 npm

  • scripts: 加入一个 build 指令来运行 webpack,此时运行的 webpack 是这个当前文件夹内安装的 webpack 而不是 global 的 webpack

依赖

"dependencies": {
  "antd": "^3.10.5",
  "prop-types": "^15.6.0",
  "react": "^16.2.0",
  "react-dom": "^16.6.1"
},
"devDependencies": {
  "babel-core": "^6.26.3",
  "babel-loader": "^7.1.2",
  "babel-plugin-import": "^1.11.0",
  "babel-preset-env": "^1.6.1",
  "babel-preset-react": "^6.24.1",
  "css-loader": "^0.28.11",
  "less": "^3.8.1",
  "less-loader": "^4.1.0",
  "postcss-loader": "^3.0.0",
  "react-hot-loader": "^4.3.12",
  "react-scripts": "^1.1.0",
  "style-loader": "^0.19.1",
  "webpack": "^3.10.0",
  "webpack-node-externals": "^1.6.0"
}

这些依赖是我这个组件需要用到的,具体依赖得根据具体项目需要添加

配置webpack(webpack.config.js)

  • 通过babel准换JSX和ES6的代码

  • 通过css-loader, sytle-loader 引入 css 到打包文件

  • 通过exclude:/src/, css-loader对antd的样式做处理

  • 通过style-loader,css-loader,postcss-loader,less-loader对less样式做处理

  • webpack-node-externals 可以避免把 node_modules 里面的依赖包引入打包文件

  • libraryTarget: "commonjs2" 使测试项目可以找到我们打包后的组件

const path = require('path')
const nodeExternals = require('webpack-node-externals')

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
    libraryTarget: 'commonjs2'
  },
  module: {
    rules: [
      {// 通过babel准换JSX和ES6的代码
        test: /(\.jsx|\.js)$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        query:
        {
          presets:["env", "react"],
          plugins: [
            [  "import",{libraryName: "antd", style: "css"}] // antd按需加载
          ]
        }
      },
      {// CSS处理
        test: /\.css$/,
        loader: "style-loader!css-loader?modules",
        exclude: /node_modules/,
      },
      {// antd样式处理
        test:/\.css$/,
        exclude:/src/,
        use:[
            { loader: "style-loader",},
            {
                loader: "css-loader",
                options:{
                    importLoaders:1
                }
            }
        ]
      },
      {// less样式处理
        test: /\.less$/,
        use: 
        [
          "style-loader", 
          {
            loader: 'css-loader', 
            options: {
              sourceMap: 1,
              modules:true
            }
          }, 
          {
            loader: "postcss-loader",
            options: {           // 如果没有options这个选项将会报错 No PostCSS Config found
              plugins: (loader) => [
                  require('autoprefixer')(), //CSS浏览器兼容
              ]
            }
          },
          {
            loader: "less-loader",
            options: {
              modifyVars: {
                'primary-color': '#1DA57A',
                'link-color': '#1DA57A',
                'border-radius-base': '2px',
              },
              javascriptEnabled: true,
            }
          }
        ]
      },
    ]
  },
  externals: [nodeExternals()]
}

配置babel(.babelrc)

  • transpile JSX and ES6 code
{
  "presets": ["react","env"],
  "plugins": ["react-hot-loader/babel",["import", { "libraryName": "antd", "libraryDirectory": "lib", "style": "css" }]],
  "env": {
    "production":{
      "preset":["react-optimize"]
    }
  }
}

测试

完成上述步骤后我们就可以在src文件夹里面完成自己的组件,这个时候就可以打包并link让测试项目引入这个打包后的组件进行测试

npm run build

npm link (可能会提示需要管理员权限)

cd [test project folder]

npm link react-hover-menu

创建测试项目

在本地做一个测试项目按上述步骤引入组件就可以看到成果了

使用create-react-app 创建一个 react 项目,并 link 我们刚才完成的组件:

create-react-app demo-menu

cd demo-menu

npm link react-hover-menu

link之后我们就可以直接在页面中引入组件

import HoverMenu from 'react-hover-menu'

发布到NPM

首先先登录npm

npm login

然后发布组件到npm

npm publish

取消发布

npm unpublish

更改版本

更改 package.json 里面的版本号并重新发布

出现的问题

解决引入antd样式无效问题

刚开始配置的时候,发现了一个如果同时引入css modules和按需引入antd,antd样式无效的问题

官网给出的按需加载解决方案,需要先安装babel-plugin-import

  • babel-loader的query/options字段加入
plugins: [
  [  "import",{libraryName: "antd", style: "css"}] // antd按需加载
]
  • webpack中plugins字段这样配置
module: {
  rules: [
    {// 通过babel准换JSX和ES6的代码
      test: /(\.jsx|\.js)$/,
      loader: 'babel-loader',
      exclude: /node_modules/,
      query:
      {
        presets:["env", "react"],
        plugins: [
          [  "import",{libraryName: "antd", style: "css"}] // antd按需加载
        ]
      }
    },
    {// CSS处理
      test: /\.css$/,
      loader: "style-loader!css-loader?modules",
      exclude: /node_modules/,
    }
  ]
}

测试后发现还是不行

解决办法

如果同时需要使用antd和css modules,处理样式时,需要分别处理

webpack加入以下处理

{// CSS处理
  test: /\.css$/,
  loader: "style-loader!css-loader?modules",
  exclude: /node_modules/,
},
{// antd样式处理
  test:/\.css$/,
  exclude:/src/,
  use:[
      { loader: "style-loader",},
      {
          loader: "css-loader",
          options:{
              importLoaders:1
          }
      }
  ]
},

引入less

由于后期要修改ant上的样式,根据官网上给出的提示,必须加装less格式

image

继续在webpack加入以下处理

{// less样式处理
  test: /\.less$/,
  use: 
  [
    "style-loader", 
    {
      loader: 'css-loader', 
      options: {
        sourceMap: 1,
        modules:true
      }
    }, 
    {
      loader: "postcss-loader",
      options: {           // 如果没有options这个选项将会报错 No PostCSS Config found
        plugins: (loader) => [
            require('autoprefixer')(), //CSS浏览器兼容
        ]
      }
    },
    {
      loader: "less-loader",
      options: {
        modifyVars: {
          'primary-color': '#1DA57A',
          'link-color': '#1DA57A',
          'border-radius-base': '2px',
        },
        javascriptEnabled: true,
      }
    }
  ]
},

好了,可以开森的npm组件了

Github Repo

查看我创建的前端React呼出菜单组件

ant-design-pro 动态菜单配置

Ant Design Pro 路由及菜单解析

我们先看下官网关于路由和菜单的介绍

路由和菜单是组织起一个应用的关键骨架,pro 中的路由为了方便管理,使用了中心化的方式,在 router.config.js 统一配置和管理。

整个路由和菜单的基本结构如官网所述:

路由管理 通过约定的语法根据在 router.config.js 中配置路由。

菜单生成 根据路由配置来生成菜单。菜单项名称,嵌套路径与路由高度耦合。

面包屑 组件 PageHeader 中内置的面包屑也可由脚手架提供的配置信息自动生成。

路由

目前脚手架中所有的路由都通过 router.config.js 来统一管理,在 umi 的配置中我们增加了一些参数,如 name,icon,hideChildrenInMenu,authority,来辅助生成菜单

菜单

菜单根据 router.config.js 生成,具体逻辑在 src/layouts/BasicLayout 中的 formatter 方法实现

动态菜单

看过Ant Design Pro 2.0的盆友应该知道,整个页面路由的配置都写在文件router.config.js中了,由于是提前配置再加载,所以它是静态的

但往往在我们的实际项目中,都存在需要动态配置路由的情况,比如题主最近在公司做的内部系统项目,就需要针对不同管理权限的人提供不同的菜单路由

在讲解动态菜单生成的具体步骤前,我们先来看下官网对于生成动态菜单方法的介绍

如果你的项目并不需要菜单,你可以直接在 BasicLayout 中删除 SiderMenu 组件的挂载。并在 src/layouts/BasicLayout 中 设置 const MenuData = []。

如果你需要从服务器请求菜单,可以将 menuData 设置为 state,然后通过网络获取来修改了 state。

这句话到底是什么意思了?接下来跟着具体的实例操作一遍,相信大家就明白了

实例介绍

  1. config/router.config.js中配置所有页面需要的路由参数

  2. src/layouts/BasicLayout.js中连接model与menuData数据

export default connect(({ menuTree, loading, global, setting }) => ({
  menuTree, // +
  loading: loading.effects['menuTree/getMenu'], // +
  collapsed: global.collapsed,
  layout: setting.layout,
  ...setting,
}))(BasicLayout);

请求model中的menuData数据

componentDidMount() {
  const { dispatch } = this.props;
  // +
  dispatch({
    type: 'menuTree/getMenu'
  })
  dispatch({
    type: 'user/fetchCurrent',
  });
  dispatch({
    type: 'setting/getSetting',
  })

在获取面包屑映射的时候配置states数据

getBreadcrumbNameMap() {
  const routerMap = {};
  const mergeMenuAndRouter = data => {
    data.forEach(menuItem => {
      if (menuItem.children) {
        mergeMenuAndRouter(menuItem.children);
      }
      // Reduce memory usage
      routerMap[menuItem.path] = menuItem;
    });
  };
  // +
  const {menuTree} = this.props
  const {menuData} = menuTree
  mergeMenuAndRouter(formatter(menuData));
  return routerMap;
}

在render中获取到menuData的数据

render() {
  const {
    menuTree, // +
    navTheme,
    layout: PropsLayout,
    children,
    location: { pathname },
  } = this.props;
  // +
  const { isMobile } = this.state;
  const {menuData} = menuTree
  const menuList = formatter(menuData)
  const isTop = PropsLayout === 'topmenu';

  const layout = (
    <Layout>
      {isTop && !isMobile ? null : (
        <SiderMenu
          logo={logo}
          Authorized={Authorized}
          theme={navTheme}
          onCollapse={this.handleMenuCollapse}
          menuData = {menuList} // +
          isMobile={isMobile}
          {...this.props}
        />
      )}
      <Layout
        style={{
          ...this.getLayoutStyle(),
          minHeight: '100vh',
        }}
      >
        <Header
          menuData={menuList} // +
          // ......
  1. src/models/menuTree.js中更新menuData数据
import { queryMenu } from '@/services/menuTree';
 
export default {
  namespace: 'menuTree',
 
  state: {
    menuData: [],
  },
 
  effects: {
    *getMenu(_, { call, put }) {
      const response = yield call(queryMenu);
      console.log('response',response)
      yield put({
        type: 'menuResult',
        payload: response[2].routes,
      });
    },
  },
 
  reducers: {
    menuResult(state, action) {
      return {
        ...state,
        menuData: action.payload,
      };
    },
  },
};
  1. src/services/menuTree.js中请求menuData数据
import request from '@/utils/request';
 
export async function queryMenu() {
  return request('/menu/getMenuTree');
}
  1. 我们使用mock数据来模拟请求的路由参数,在mock/menu.js中配置所需要显示的动态路由
export default {
  'GET /menu/getMenuTree':[
    // user
    {
      path: '/user',
      component: '../layouts/UserLayout',
      routes: [
        { path: '/user', redirect: '/user/login' },
        { path: '/user/login', component: './User/Login' },
        { path: '/user/register', component: './User/Register' },
        { path: '/user/register-result', component: './User/RegisterResult' },
      ],
    },
    // ......
  1. src/locales/zh-CN.jssrc/locales/en-US.js中配置相应路由的显示名称

以上就是Ant Design Pro 动态菜单的所有步骤了

都9102了,该实践 Docker 部署啦

最近用 Docker 完成了 eggjs 后端项目的部署,不得不感叹,Docker 真的是太好用了。不仅能够一键安装 mysql,省去了很多搭环境的事宜,而且可以直接把项目发布到 Docker 容器上进行测试,等项目需要正式上线时,就直接把做好的 Docker 镜像部署上去就好了,省去了很多项目部署上线的风险

Docker 是什么

Docker 是一个可以用来快速部署的轻量级虚拟技术,允许开发人员将自己的程序和运行环境一起打包,制作成一个 DockerImage (镜像),然后部署到服务器上,通过下载这个 Image 就可以将程序跑起来,省去了每次都安装各种依赖和环境的麻烦

Docker 镜像

操作系统分为内核和用户空间。对于 Linux 而言,内核启动后,会挂载 root 文件系统为其提供用户空间支持。而 Docker 镜像(Image),就相当于是一个 root 文件系统。除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)

Docker 容器

镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的 类 和 实例 一样,镜像是静态的定义,容器是镜像运行时的实体

仓库(Docker Registry)

镜像构建完成后,可以很容易的在当前宿主机上运行,但是,如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,Docker Registry 就是这样的服务

Docker 部署应用有什么优点

使用 Docker 容器部署应用快速方便,特别是应用较多时部署迁移等使用 Docker 会更方便。另外,在同一台服务器上不能同时运行多个 eggjs 应用,除非停止另外一个 eggjs 应用

更详细的介绍可以参考非官方 Docker 中文文档

Docker 的架构

为了后续更好的理解 Docker 命令的操作,我们先来大致理清下 Docker 的架构

image

上面这张图大致介绍了 Docker 的架构,中间是 host,也就是进行 Docker 操作的宿主机,宿主机上主要是运行 Docker Daemon 的核心程序,也就是负责做各种各样的操作,比如说下载 Docker 的镜像,比如说运行一个容器

那宿主机如何和 Docker Daemon 交互呢?实际上是通过在客户端用命令比如 buildrunput 交给 DaemonDaemon 来做实际的操作

右边的蓝的是互联网的 Sass 服务,叫做 registryDaemon 可以和 registry 交互,比如说 push 一个 Image,拖拉一个 Image,实际上是所有 Docker 用户共享 Docker 镜像的服务

image

简单来说,就是客户端和守护进程 Daemon 进行操作,把命令送给守护进程,守护进程来拖取镜像,运行容器,和远端的镜像仓库进行交互

项目部署实践

我这里已经用 eggjs 开发了一个后端项目,然后需要构建一个镜像,然后基于这个 Image 运行一个 container。从而快速实现部署

大体流程

  • 服务器安装好 Docker
  • 本地应用根目录编写好 Dockerfile 文件
  • 将整个应用一起上传到服务器目录下
  • 使用终端连接服务器执行命令构建 Docker Image
  • 基于镜像运行 container,部署成功

具体操作如下

创建 Dockerfile

如果不知道 Dockerfile 文件怎么写,可以直接到 github 上查找 eggjs / docker,就可以看到完整的 Dockerfile 文件,直接拷贝粘贴到项目路径下即可

# 拉取要创建的新镜像的 base image(基础镜像),类似于面向对象里边的基础类
FROM node:8.11.3-alpine

# 设置时区
ENV TIME_ZONE=Asia/Shanghai

# 在容器内运行命令
RUN \
  mkdir -p /usr/src/app \
  && apk add --no-cache tzdata \
  && echo "${TIME_ZONE}" > /etc/timezone \ 
  && ln -sf /usr/share/zoneinfo/${TIME_ZONE} /etc/localtime 

# 创建 docker 工作目录
WORKDIR /usr/src/app

# 拷贝,把本机当前目录下的 package.json 拷贝到 Image 的 /usr/src/app/ 文件夹下
COPY package.json /usr/src/app/

# 使用 npm 安装 app 所需要的所有依赖
RUN npm i

RUN npm i --registry=https://registry.npm.taobao.org

# 拷贝本地的所有文件到路径中去
COPY . /usr/src/app

# 暴露端口。如果程序是一个服务器,会监听一个或多个端口,可以用 EXPOSE 来表示这个端口
EXPOSE 7001

# 给容器指定一个执行入口
CMD npm run start

注意事项

同时,我们还可以在 eggjs 的官网看到应用部署的文档,有两点需要防止踩坑的地方

image

  • 第一点,因为我们的 Docker 已经是后台了,所以在部署的时候需要去掉 --daemon,不能运行在后台的后台
  • 第二点,我们需要加上 --port=7001,因为 Docker 里有环境变量 PORT,如果不加上,会默认使用 Docker 里的环境变量,而这个变量 PORT 的值是随机生成的数字,所以在正式部署的时候就会开启这个随机数生成的端口,从而报错

image

以上修改都是在 package.json 文件中

构建 Image

Dockerfile 创建完成后,我们就可以在 Dockerfile 文件所在的目录下运行下面的 Docker 命令来构建一个 Image

docker build -t webshare-backend .

通过 -t 的参数,给它一个标签 share,然后给出一个 .. 就是路径名 就是把这个路径底下的所有文件都送给 Docker Engine 让它来产生 Image

image

运行完最后会出现 successfully,代表构建成功

接着我们就可以通过 Docker Images 来查看是否真的生成了这个文件

image

的确是生成了一个新的 Image, 打了一个 taglatest,有一个 ImageId 和大小 size

接着就可以运行这个 Image

运行镜像

docker run -d -p 7001:7001 webshare-backend
  • -d 是 Demon 守护进程,代表容器会在后台运行
  • -p 7001:7001 是把端口暴露出来,p 是做端口映射的,右边的是程序本身的端口,左边是本地的 host 的端口,把本机的 7001 映射到 container 的7001,这样外网就能通过本机的 7001 访问我们的 web 了

image

返回了一个容器的 id,这样就成功的用 Dockerfile 的方式来构建了一个自己的 Image

通过运行 docker ps 可以查看容器是否启动成功

我们还可以用 curl localhost:7001 测试一下,会输出接口查询内容

上传镜像

可以通过 docker push 命令,把自己创建的镜像上传到仓库中来共享

  • 通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。我们可以通过 <仓库名>:<标签> 的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 latest 作为默认标签。
docker tag webshare-backend:latest 服务器ip/webshare-backend:1.0
  • 然后,登录镜像仓库
docker login -u 账号 -p 密码 服务器IP地址

后面显示 Login Succeeded,就是登录成功了

这儿可能第一次会报 Service Unavailable,需要去根路径的 .docker 目录下的 daemon.json 里添加信任

  • 登录成功后,就可以上传自己的镜像到 Docker 仓库
docker push 服务器IP/目录名/webshare-backend:1.0

更新应用

  • 通过 docker ps 命令列出运行的容器
  • docker stop xxx (CONTAINER ID ) 停止运行该容器
  • docker stop $(docker ps -a -q) 暂停所有运行的容器
  • docker rm xxxx 删除container
  • docker rmi 9a52e8ccdd7a(image id) 删除image
  • 提示无法删除的情况下,强制删除,docker rmi -f imageId
  • 按照上面的步骤重新构建镜像和启动容器

其他常用命令

  • 查看镜像构建工程,docker history webshare-backend
  • 列出所有的容器,docker ps -a
  • 查看具体的容器日志,docker logs xxx (CONTAINER ID )
  • docker cp src/. mycontainer:/targethostcontainer 之间拷贝文件

镜像分层

Dockerfile 中的每一行都产生一个新层,存在 Image 里的层是只读的(RO)

Image 被运行成为一个容器的时候,就会会产生一个新层,叫容器层 container layer,是可读可写的(RW)

分层的好处:如果有很多的容器和 Image,比如 A Image 有 10 层, B Image 有 7 层,他们之间可能有 5 层是共享的,那么无形之中,存储压力就会小很多

Volume( 数据卷)

提供独立于容器之外的持久化存储

因为在容器中的改动是不会被保存的,Volume 提供了比较方便的、可以持久化存储的一个技巧,比如说运行一个数据库容器,数据库的真正数据应该是被持久化的,Volume 是可以实现的,并且还可以提供给容器之间的共享数据

一篇文章告诉你谁才是最好用的 Javascript 请求库

在前端这个迅猛发展的领域,前端请求方法也是迭出不穷,从原生 XHRjQuery ajax,再到现在的 axiosfetch,那发请求的最佳实践是什么呢?这个答案我们在文章中会慢慢给出

在分析 axios 封装方法之前,我们先来看看 jQuery ajaxfetch 方法

jQuery ajax

jQuery ajax 是对原生 XHR 的封装,还支持 JSONP

$.ajax({
  type: 'POST',
  url: url,
  data: data,
  dataType: dataType,
  success: function() {},
  error: function() {}
})

但是随着 reactvue 等前端框架的兴起,jQuery 早已不复当年之勇

jQuery 整个项目太大,单纯使用 ajax 却要引入整个 jQuery 非常的不合理,于是便有了 fetch 的解决方案

fetch

fetch 号称是 ajax 的替代品,它的 API 是基于 Promise 设计的

而且 fetch 也是 whatwg 的标准,具体可以参考 fetch Living Standard

// 原生 XHR
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4 && xhr.status === 200) {
    console.log(xhr.responseText) // 从服务器获取数据
  }
}
xhr.send()

// fetch
fetch(url).then(response => {
  if (response.ok) {
    response.json()
  }
})
.then(data => console.log(data))
.catch(err => console.log(err))

再搭配上 async / await 将会让我们的异步代码更加优雅

async function test() {
  let response = await fetch(url)
  let data = await response.json()
  console.log(data)
}

fetch 相对于是更加底层的,很多情况下都需要我们再次封装,比如需要手动将参数拼接成 'name=test' 的格式,而 jquery ajax 已经封装好了

// jquery ajax
$.post(url, {name: 'test'})

// fetch
fetch(url, {
  method: 'POST',
  body: Object.keys({name: 'test'}).map((key) => {
    return `${encodeURIComponent(key)} = ${encodeURIComponent(params[key])}`
  }).join('&')
})

除此之外,fetch 还有很多问题,比如

问号

  • fetch 只对网络请求报错,对 400,500 都当做成功的请求,需要封装去处理

  • fetch 不支持取消(最大的问题),不支持超时控制,使用 setTimeoutPromise.reject 实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费

  • fetch 没有办法原生监测请求的进度,而 XHR 可以

具体问题可以参考 fetch 没有你想象的那么美fetch 使用的常见问题及解决方法

所以 fetch 并不是开箱即用的

axios

对比于 jQuery ajaxfetchaxios 的优点简直不要太多,它也是 vue 官网推荐使用的

axios 是一个基于 PromiseHTTP 库,可以用于浏览器和 nodejs 中,主要特性有

  • 从浏览器中创建 XMLHttpRequest
  • node.js 创建 http 请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求数据和响应数据
  • 取消请求
  • 自动转换 JSON 数据
  • 客户端支持防御 XSRF

axios 本质上也是对原生 XHR 的封装

axios.post('/upload', {
  userName: 'zhangsan'
})
.then(res => {
  console.log(res, res.data)
})
.catch(err => {
  console.log(err)
})

对于客户端支持防御 XSRF,实际上就是让你的每个请求都带一个从 cookie 中拿到的 key,根据浏览器同源策略,假冒的网站是拿不到你 cookie 中的 key,这样后台就可以轻松辨别出这个请求是否是用户在假冒网站上的误导输入,从而采取正确的策略

axios 既提供了并发的封装方法,也没有 fetch 的各种问题,而且体积也较小,当之无愧现在最应该选用的请求的方式

axios 支持同构, 对于既要支持 SPA 同时支持 SSR 的项目来说非常好用

具体的使用方法我们可以查看 axios 中文文档

下面是一些封装方法的详细使用

合并请求

let q1 = axios.post('/add', 'b=2')
let q2 = axios.post('/upload', 'a=1')

// 合并这两个请求,并处理其成功和失败
// 一般会用于两个分开的相关联的请求,缺一不可
axios.all([q1, q2])
.then(axios.spread((res1, res2) => {
  // 全部成功
  console.log(res1, res2)
}))
.catch(err => {
  // 其一失败
  console.log(err)
})

options 参数

// axios.headers = {} // 覆盖原本默认头
// axios.defalut.headers.accept = 'abc' // 走默认头,修改个例
            
// 请求一
axios.get('/', {
  params: {id: 1},

  // transformResponse 在传递给 then/catch 前,允许修改响应数据
  transformResponse: (data) => {
    console.log(data)

    // 就是 res.data,可以更改
    data = 'data changed'

    return data
  }
})
.then(res => {
  console.log(res, res.data)
})
.catch(err => {
  console.log(err)
})

// 请求二
axios.post('/upload', 'name=jack', {
  timeout: 1000,

  // transformRequest 允许在向服务器发送前,修改请求数据
  // 只能用在 'PUT', 'POST' 和 'PATCH' 这几个请求方法
  transformRequest: (data) => {
    // 加工请求体数据
    return 'name = rose'
  }
})
.then(res => {
  console.log(res, res.data)
})
.catch(err => {
  console.log(err)
})

取消请求

这里我们以上传文件为例,在 vuejs 中使用

choose file : <input type = "file" name = "file" @change = "changeFile" />
<button @click = "sendAjax">sendReq</button>
<button @click = "cancelAjax">cancelReq</button>
<button @click = "resumeAjax">ContinueSendlReq</button>
// 发送请求
sendAjax() {

  // 在请求前需要先生成独立的标识,才能更方便取消请求
  const CancelToken = axios.CancelToken // 获取取消标识
  const source = CancelToken.source() // 使用 CancelToken.source 工厂方法创建 cancel token
  // 保存起来方便取消调用
  this.source = source
            
  let fd = new FormData()
  fd.append('file', this.file)
  axios.post('/uploadfile', fd, {
    // 携带取消标识
    cancelToken: source.token,
    onUploadProgress: (progressEvent) => {
      console.log(progressEvent.loaded)
      console.log(progressEvent.total)

      // 保存最后上传大小
      this.loaded = progressEvent.loaded

      this.rate = (progressEvent.loaded / progressEvent.total) * 100
    }
  })
  .then(res => {
    console.log(res.data)
  })
  .catch(err => {
    console.log(err)
  })
},

// 取消请求
cancelAjax() {
  this.source.cancel() // 通过内部存好的方法做取消
},

// 继续请求
resumeAjax() {
  // 剪裁文件        开始        结尾
  const fileData = this.file.slice(this.loaded + 1, this.file.size)
  let fd = new FormData()
  fd.append('file', fileData)

  // 为了后续续传以后,再取消
  const CancelToken = axios.CancelToken // 获取取消标识
  const source = CancelToken.source() // 使用 CancelToken.source 工厂方法创建 cancel token
  this.source = source

  axios.post('/uploadfile', fd, {
    // 携带取消标识
    cancelToken: source.token,
    onUploadProgress: (progressEvent) => {
      // 保存最后上传大小
      this.loaded = progressEvent.loaded
      this.rate = (progressEvent.loaded / progressEvent.total) * 100
    }
  })
  .then(res => {
    console.log(res.data)
  })
  .catch(err => {
    console.log(err)
  })
},

// 上传文件
changeFile(e) {
  // console.log(e.target.files[0])
  this.file = e.target.files[0]
}

为了更方便的取消请求,我们在发送请求时,需要有独立的标识

取消请求大概来说分为三步

  1. 获取请求标识
  2. 使用 CancelToken.source 工厂方法创建 cancel token,保存起来方便取消调用
  3. 通过内部存好的方法做取消

拦截器

在请求或响应被 thencatch 处理前拦截它们

比如实现一个功能如下

  1. 在请求发起之前,show 一个 loading 出来
  2. 响应回来之后,关闭这个 loading

可以通过设置一个标识表示 loading 出现与否,默认是不出现,在在发送请求之前将 loading 标识设为 true,拿到响应数据之后,将 loading 标识设为 false

data() {
  return {
    isshow: false // loading默认不出现
  }
},

添加请求拦截器

// 配置请求拦截器  use 给请求之前做的事可以是多件,可以 use 多次
axios.interceptors.request.use((config) => {
  // 在发送请求之前做些什么
  console.log(config)

  this.isshow = true // loading 出现
  return config
}, (error) => {
  return Promise.reject(error)
})

添加响应拦截器

 axios.interceptors.response.use((res) => {
  // 对响应数据做点什么
  console.log(res) // res.config
  
  this.isshow = false // loading 消失
  return res
}, (error) => {
  return Promise.reject(error)
})

在拦截器中我们也可以实现一个类似 cookie 的机制,比如现在广泛使用的 jwt,通过客户端保存数据,再在请求中带回服务器

  1. 服务器发送 cookie 到客户端保存起来,在响应拦截器中完成
  2. 然后在请求之前,从本地获取 cookie,设置请求头拦截器

在服务端代码中设置响应头中包含 token: 'abc' 的信息

server.post('/token', function (req, res){
  console.log('body', req.body)
  // 给客户端一个标识
  res.set('token', 'abc')

  res.send({token: 'abc', msg: 'post请求成功'})
})

然后前端请求数据,通过响应拦截器将 token 存在本地,使用 localStorage

axios.interceptors.response.use((res) => {
              
  // 获取服务器的响应头
  if(res.headers.token) {
    var token = res.headers.token
    localStorage.setItem('token', token)
  }
  
  return res
}, (error) => {
  return Promise.reject(error)
})

然后再次发送请求时,通过请求拦截器从本地取出 token 放入请求头中

axios.interceptors.request.use((config) => {

  // 设置请求头,类似cookie
  var token = localStorage.getItem('token')
  if(token) {
    config.headers['token'] = token
  }

  return config
}, (error) => {
  return Promise.reject(error)
})

nodejs 使用

在 node 中,我们可能遇到需要向另一个服务器 post 数据的需求,这个时候如果使用 axios 会非常方便,毕竟有原生支持的 Promise 语法

const querystring = require('querystring')
axios.post('http://xxx.com/', querystring.stringify({ userName: 'zhangsan' }))

其他的方法使用这里就不做过多赘述了,具体的使用可以查看 axios 中文文档

所以,发请求的最佳实践是什么呢?综上,当之无愧现在最应该选用的请求的方式就是 axios

上述用到的所有代码完整版可以查看 axios-demo

elementUI默认样式修改不成功的问题

问题

filter.vue文件中的样式表中写入了如下样式

<style scoped>
.el-input__inner {
  background-color: #F5F5F5;
  border: none;
}
</scope>

但设置el-input的样式并没有效果。

image

image

原因和解决方法

是因为scoped的原因,去掉scoped就可以显示样式

但此时会污染全局样式,所以可以将该样式放在控制全局样式的文件里(比如app.css),或者在该样式表的外面再写一个全局样式

<style>
.el-input__inner {
  background-color: #F5F5F5;
  border: none;
}
</style>
<style scoped>
  ...
</style>

image

如果不希望所有的el-input都变成相同样式,可以给需要修改的el-input添加一个类

通俗地解析background属性-

我们在做项目的时候,通常会遇到设置背景图与屏幕大小相适应问题

而background中一些属性的取值是可以直接影响到背景图的呈现效果,包括CSS3当中新增的几个新的属性值:background-size,background-origin,background-clip,还有CSS1当中的background-position

之前在项目中达不到想要的背景图效果,总是将各种取值都试一遍,现在认真解析了一遍,每个值都要特点的效果,本篇文章主要是对上述几个属性做一下通俗易懂的解释

background-size

取值

background-size: auto || <length> || <percentage> || cover || contain

auto: 默认值,保持背景图原有高度和宽度

length: 具体像素值,改变背景图大小

percentage:百分值(0%〜100%),作用于块元素,背景图大小取决于所在块元素的宽度百分比

cover: 背景图放大到铺满整个容器,适应于图片尺寸小于元素容器

container:背景图缩小到铺满整个容器,适应于图片尺寸大于元素容器

当取值为length或percentage,可以设置一个值 第二个值相当于auto auto的值与第一个值相同

DEMO结构和效果

HTML Code

<div class = "test"></div>

先加上初步的效果

.test {
     background: url("./images/background_image.jpg") no-repeat;
     width: 800px;
     height: 450px;
     line-height: 450px;
     border: 1px solid #999999;
     margin: 30px;
 }

找了张(380px*300px)左右的图片来当作背景图片使用

image

background-size:auto

在上述.test{...}中增加一行

background-size:auto;

效果

image

效果等同于没加background-size效果一样

background-size:'length'

同样的增加一行,如

background-size: 550px 300px;

效果

image

从上张取值的效果图可以看出来,背景图片由(380px300px)变为(550px300px),已经变形失真

如果只取了一个值,比如background-size:350px ,相当于background-size:350px auto,此时auto的取值为350px/380px*300px

background-size:'percentage'

同样的增加一行,如

background-size:80% 50%;

效果

image

从这张效果图我们可以看出来,背景图大小并不是按背景图的百分比来缩放的,而是按装载背景图的容器元素的百分比计算的,也就是长800px(div.width)*80%,高450px(div.height)*50%

如果只有一个值时,比如background-size:50%,相当于background-size:50% auto,相当于background-size: 50%*800px(div.width) 50%*800px(div.width)/380px(背景图长度)*300px(背景图高度)

上述两种取值也可以搭配使用,如

background-size:47.5% 300px;

background-size:47.5% 300px;的取值实际上就是图片自身的大小(380px*300px)了,效果也等同于一个值background-size:47.5%;

效果

image

background-size: cover

同样的增加一行,如

background-size:cover;

效果

image

从上述效果图可以看到,背景图会放大到适合容器元素的尺寸,原则是背景图的width和height都需要等比例放大到填满容器,以至背景图在装载它的容器元素中占比大的一方向可能超出容器,比如上图中背景图的height明显超出了容器。和上一种取值background-size:100%的效果一样

background-size:contain

上面的cover取值是把背景图片放大到适合容器元素的尺寸,这时的contain刚好是跟cover相反,是把背景图片缩小到适合容器元素的尺寸

这里再重新定义一下容器元素的宽高

width:200px;
height:200px;
background-size:contain;

效果

image

前面已经介绍了背景图片的大小是380px300px,而现在将容器元素改为200px200px

从效果图中我们可以看到背景图等比例的缩小到适合元素容器的尺寸,以至背景图在超出它的容器元素较少的一方向可能与容器有间隙,比如上图中背景图的height超出了100px,width超出了180px,所以等比缩小后height与容器间多出了空隙

从上边的几个效果值可以看出来,cover、contain的值、或者只设置一个值,另一个值为auto时,都不会出现失真的情况,但会出现图片显示不全或者与容器元素出现留白的情况

所以我们在项目开发中合理的定义容器元素的尺寸大小,根据容器尺寸大小和背景图大小合理的选用background-size的取值

background-position

图片定位的问题,比较容易理解,但也有需要注意的点。接下来还是从取值讲起

取值

background-position:  <关键词> || <百分比> || <像素值> 

关键词:类似于background-position:top left

像素:类似于background-position: 0px 0px

百分比:类似于background-position: 0% 0%

DEMO结构和效果

上面前两种定位都是将背景图左上角的原点放置到属性值位置

同样,在上述.test{...}中增加一行background-position: 50px 50px;

.test {
background: url("./images/background_image.jpg") no-repeat;
width: 800px;
height: 450px;
line-height: 450px;
border: 1px solid #999999;
margin: 30px;
background-position: 50px 50px;
}

效果

image

可以看到,规定的位置是"50px 50px",也就是图片的左上角的原点在那个位置上

同理,当取值background-position: top left ;

效果

image

git多分支创建与代码合并

通常我们从github等远程版本库克隆一个全新的代码库,使用

git clone username@xxx:/xxx/xxx.git

默认的克隆下来是master分支,没有其他分支

我们可以使用git branch -a查看所有分支,包括本地和远程的

(如果是只查看本地分支,使用git branch命令,只查看远程分支,使用git branch -r命令)

在团队合作开发中,我们往往需要创建多个分支,各自在不同的分支上开发,然后再归并代码

建立远程分支

如果是团队管理者首先创建好分支,供团队成员使用

也就是本地给远程创建分支

可以直接先创建一个本地分支,假设分支名为dev

git branch dev

然后把本地dev分支提交到远程仓库

git push origin dev

接下来查看远程分支

git branch -a

远程分支创建好之后就是团队成员在各自分支上开发了

切换到远程分支并合并其他分支

首先在开发前,用git branch -a查看远程分支,然后切换到 origin/dev远程分支,并在本地新建分支 dev

git checkout -b dev origin/dev

同时建立了本地dev分支和远程dev分支的联系

然后在本地dev的分支上开发,开发完后push到远程分支上(省略了add commit这些操作)

git push origin dev

现在比如要合并其他人的代码分支,如dev2,可以

(在本地创建分支,然后拉取dev2的代码)

git checkout master
git branch dev2
git checkout dev2
git pull origin dev2
git add .
git commit -m "合并分支"

并不建议这种操作,因为是强行push了一个本地分支dev2到线上,并是不友好的关联

或者
(直接拉取一个与远程分支关联的本地分支)

git checkout -b dev2 origin/dev2

然后切换回dev分支,如果线上dev2分支有改动,合并线上分支dev2的代码

git checkout dev2
git pull origin dev2
git checkout dev 
git merge dev2

如果是更新dev的代码到dev2

git checkout dev
git pull origin dev
git checkout dev2
git merge dev

创建本地分支并更新到线上

git branch dev3
git push origin dev3

git fetch

如果我们拉取线上分支并与本地分支做对比,确认无误后再合并代码,可以使用

git fetch origin master:tmp
git diff tmp 
git merge tmp

上面代码也就是先从master上拉取了tmp的一个本地分支,然后查看tmp分支跟本地已有分支的改动,再合并tmp分支

类似于git pull 从远程获取最新版本并merge到本地

git pull origin master

如果线上临时创建了分支,需要查看远程分支的情况

也可以使用git fetch 把远程服务器上所有的更新都拉取下来

然后在本地使用git branch -a查看所有分支

删除分支

删除本地分支

git branch -r dev3

使用大写的D 强制删除,在有代码改动未提交的情况下删除该分支

  git branch -D dev3 

删除远程分支

git branch -r -d origin/dev3
git push origin :dev3

不提交commit切换分支

当我们同时开发两个功能块,用到两个分支的时候,可能需要随时切换

而当前分支有改动,没有开发完的前提下,不想提交代码进行commit的操作,切换分支会失败

如果开发完进行commit操作,也会将提交的改动带到另一分支,造成混乱

这种情况下,最好的解决办法就是在当前分支上执行git stash 命令,将当前分支存起来

执行后可以进行git status验证,看是否存储成功

接着就可以在主分支master上创建并切换到新的分支去开发另一个功能了,在新分支上的功能开发完成后,我们再切换回上一个分支

git stash list命令去查看我们“存储”的列表

然后用git stash pop命令恢复最近存储的列表,恢复的同时把 stash 存储列表的内容也删了

本地回滚

是指已经commit存储到了本地仓库,但未push到远程

git reset --hard   22f8aae

22f8aae为commit的版本号,可以用git lig查看

远程回滚

可以参考博客

bug fix

使用git的pull时报refusing to merge unrelated histories,push时报Push rejected: Push to origin/master was rejected

使用如下命令进行pull操作

git pull [你的远程仓库的地址] [要拉取的分支名] --allow-unrelated-histories

git pull [email protected]:lulujianglab/react-router-demo.git master --allow-unrelated-histories

Webpack4 和 Babel 7 全新配置

前阵子自己开坑写了个 npm 开源的交互组件,按照 webpack 的配置流程走了一遍,打包遇到了各种坑。根据命令行的报错逐个排查,发现 babel 升级到 7.x 之后有坑。为了更好的记录遇到的问题以及解决方案,这里以 webpack4.x 的全套配置流程为方式重新梳理一遍

webpack 和 babel 浅析

在梳理配置流程之前我们先来大致过一下 webpack 和 babel 的基础理论,如果想更详细的了解,可以去查阅一下 webpack 官方文档 Babel 官方文档

webpack

webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle

image

也就是说,一切静态资源,皆可打包。webpack 可以说是现代前端发展的基石

那为什么需要 webpack 来打包呢? webpack 到底发挥的是什么作用?

  • webpack 可以通过分析项目结构,找到 JavaScript 模块以及其它一些浏览器不能直接运行的扩展语言( Scss , TS 等),并将其打包为合适的格式以供旧浏览器使用

  • 以往的前端技术,是用jQuery、html、css等来开发静态页面;而现在的,我们讲的都是 MVVM 框架,数据驱动界面,webpack 可以将现在 js 开发中的各种新型有用的技术集合打包

在如今功能丰富的页面下,都有着复杂的 JavaScript 代码和一大堆依赖包,为了简化开发的复杂度,前端社区出现了很多有益于提高开发效率的方法

比如我们熟悉的模块化开发,把复杂的程序细化为小的文件;scss / less 等 css 预处理器

这些开发方式都需要额外的处理才能让浏览器识别,而手动处理又是非常繁琐的,这就为 webpack 这类工具的出现提供了需求

babel

Babel 是一个工具链,主要用于在旧的浏览器或环境中将 ECMAScript 2015+ 代码转换为向后兼容版本的 JavaScript 代码

Babel 可以转换 JSX 语法,可以将 ES6 的代码转换为 ES5

babel 的目的就是为了解决浏览器的自身对于 es 语言的差异性而带来的一款工具,有了 babel 就首先不用担心旧浏览器不支持 es 语言这件事,其实最重要的不是支持,而是解决差异性,这种差异性不仅介于浏览器之间,对于 node 这样的环境也会存在这样的问题,各个 node 版本对于 es 的支持,或者对于 es 的一些尚未提交的草案的支持都是不同的,所以不论是浏览器下还是 node 下都需要到使用 babel 的场景

尤其是现在 Javascript 主要是用 ES6 编写的,为了更好适应各种旧浏览器,我们需要进行转换处理,这个转换的步骤也就是 transpiling(转译)

webpack 不知道如何进行转换但是有 loader (加载器),也就是转译器

babel-loader 是一个 webpack 的 loader (加载器),用于将 ES6 及以上版本转译至 ES5

使用 loader 之前,我们需要安装一堆依赖项,尤其是:

  • babel-core
  • babel-loader
  • babel-preset-env 用于将 Javascript ES6 代码编译为 ES5

webpack 4 变化

这里主要看看我们用的比较多的两则变化:

webpack 4 既不必须定义 entry point(入口点) ,也不必须定义 output file(输出文件)

entry point (入口点):webpack 寻找开始构建 Javascript 包的文件
output file (输出文件):webpack 构建完成输出 Javascript 包的文件

但是从 webpack 4 开始,这两个属性不是必须定义的,它有一个默认值,会将 ./src/index.js 作为默认入口点, ./dist/main.js 作为默认输出模块包

当然,我们也可以覆盖默认 entry point(入口点) 和 默认 output(输出) ,只需要在 package.json 中配置它们

module.exports = {
  entry: './src/index.js', 
  output: { 
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js'
  }
}

或者

"scripts": {
  "dev": "webpack --mode development ./src/js/index.js --output ./dist/main.js",
  "build": "webpack --mode production ./src/js/index.js --output ./dist/main.js"
}

webpack 4 引入了 production(生产) 和 development(开发) 模式

拥有两个配置文件在 webpack 中是常见模式,但在 webpack 4 中,我们可以在没有一行配置的情况下完成,仅仅只需要在 package.json 中填充 script 部分

"scripts": {
  "dev": "webpack --mode development",
  "build": "webpack --mode production"
}

然后运行

npm run dev

打开 ./dist/main.js,会发现有一个没有压缩的 bundle(包)文件

接着运行

npm run build

再次打开 ./dist/main.js,没错,bundle(包)压缩了!

production mode (生产模式) 可以开箱即用地进行各种优化。 包括压缩,作用域提升,tree-shaking 等
development mode (开发模式) 针对速度进行了优化,仅仅提供了一种不压缩的 bundle

环境搭建

这里使用 npm 来安装 webpack

npm i webpack webpack-cli -g

在 webpack3 中, webpack 和 cli 是在同一个包中,但在第4版中,为了更好的管理,已经将两者分开

初始化项目和配置

npm init -y
npm i webpack webpack-cli - D

部署配置

修改我们项目的 package.json ,使用 npm run build 启动 webpack

"scripts": {
  "build": "webpack --mode production"
},
"devDependencies": {
  "webpack": "^4.28.3",
  "webpack-cli": "^3.2.1"
}

我们在项目实战中应该很熟悉,类似于 vue-cli 或者是 umijs 这样的脚手架在打包的时候都会通过配置自动生成项目,通常都是在 src 文件夹下进行项目的开发,然后运行 npm run build 打包并生成我们的 dist 文件

比如我们在 src 目录下新建一个 index.js 文件,写上

console.log('我是测试案例')

然后运行 npm run build ,会发现新增了一个 dist 目录,里面存放着 webpack 打包好后的 main.js 文件

配置流程

回想我们之前做过的项目,一般会打包 src 下的什么文件呢?

  • 发布时需要的 html, css, js
  • css 预编译器 stylus / less / sass
  • es6 的高级语法
  • react 的 jsx 语法
  • 图片资源 .png, .gif, .ico, .jpg
  • 文件间的 require
  • 别名 @ 等修饰符
  • webpack dev server

下面就跟着上述几点来依次在 webpack 的 webpack.config.js 中进行配置,以 commonJS 模块化机制向外输出

module.exports = {}

html 配置

这里就不使用默认配置了,自行定义好入口 entry 和出口 output 。通俗的理解就是,webpack 相当于一个工厂,进入相当于把各种各样的原料放入我们的工厂,然后工厂进行一系列的打包操作,将打包好的东西向外输出,就可以出售了(上线)

module.exports = {
  entry: './src/index.js', //入口文件
  output: {  // 出口定义
    path: path.resolve(__dirname, 'dist'), // 输出文件的目标路径
    filename: '[name].js' // 文件名[name].js默认
  }
}

HTML 打包需要安装插件 html-webpack-plugin,它将会创建一个 index.html 文件,有了这个插件,我们就可以不用手动创建一个 index.html 文件了

npm i html-webpack-plugin -D

然后在 webpack.config.js 中引入

const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  // ...
  plugins: [  // 插进的引用, 压缩,分离美化
    new HtmlWebpackPlugin({  // 将模板html的头部和尾部添加css和js模板
      file: 'index.html' // 输出文件的文件名称,默认为index.html
    }),
  ],
}

配置好后,在终端输入 npm run build 后 webpack 将 html 打包好并且自动将 js 引进来了

<body>
  <script type="text/javascript" src="main.js"></script>
</body>

在 dist 目录下启动本地服务器测试一下,运行 http-server,然后在浏览器中打开一个 ip 地址的 8080 端口,就可以在浏览器中看到我们的 Hello World 了,也就是我们上线的页面

css 配置

在我们的项目应用中,往往为了提高编程效率,使用一些预编译器,比如 stylus 、less 、sass 等,这里主要以 less 和 css 为例进行说明

在 src 下新建 index.html 、main.css 、style.less 文件

<body>
  <p class="title">Hello Webpack-4-cli !/p>
</body>
body {
  background: gray;
}
.title {
  color: red;
}

在 index.js 中引入

import style from "./main.css"
import style2 from "./style.less"

然后安装文件 css-loader 、 sass-loader 和 sass

npm i css-loader less less-loader style-loader -D

因为想让 css 在 dist 目录下和 HTML 分离,所以这是还需引入 extract-text-webpack-plugin

npm i extract-text-webpack-plugin -D

需要注意的是,这里有一个坑,因为 extract-text-webpack-plugin 最新版本为 3.0.2 ,这个版本还没有适应 webpack 4 的版本

解决办法:使用 4.0 beta 版

npm i extract-text-webpack-plugin@next -D

会下载到 [email protected],安装好后,我们开始配置webpack.config.js文件

但是即便是使用 [email protected] 代替,还是会有一些问题,所以可以用另一个插件来代替,webpack4 可以使用 mini-css-extract-plugin 这个插件来单独打包 css

npm i mini-css-extract-plugin -D
const MiniCssExtractPlugin = require('mini-css-extract-plugin') // 打包的css拆分,将一部分抽离出来  
module.exports = {
  // ...
  module: { // 模块的相关配置
    rules: [ // 根据文件的后缀提供一个loader,解析规则
      {
        test: /\.(css|less)$/,
        use: [MiniCssExtractPlugin.loader, "css-loader", 'less-loader']
      },
  ]},
  plugins: [
    new HtmlWebpackPlugin({
      file: 'index.html',
      template: 'src/index.html' // 本地模板文件的位置,支持加载器(如handlebars、ejs、undersore、html等)
    }),
    new MiniCssExtractPlugin({ // [name] 默认  也可以自定义name  声明使用
      filename: "[name].css",
      chunkFilename: "[id].css"
    }),
  ],
}

打包之后 http-server , 发现我们的样式已经生效了,.less 的文件也被编译到 .css 文件中了,并且 css 部分已经从 html 中分离出来

js 配置

为了让更多的旧浏览器适应 es6 的开发语法,需要引入 babel 来把 es6 的代码编译为 es5 。在根目录下新建 .babelrc

{"presets": ["env"]}

在 index.js 中添加代码

const arr = [1, 2, 3]
const iAmJavascriptES6 = () => console.log(...arr)
window.iAmJavascriptES6 = iAmJavascriptES6

然后安装文件

npm i babel-loader babel-core  abel-preset-env -D

接下来在 webpack.config.js 中配置

module.exports = {
  // ...
  module: { // 模块的相关配置
    rules: [ // 根据文件的后缀提供一个loader,解析规则
      {
        test: /\.js$/,
        exclude: /node_modules/, // 不匹配选项(优先级高于test和include)
        use: 'babel-loader'
      },
  ]}
  // ...
}

这里会出现一个大坑,因为我们的 babel 已经升级到了一个大版本 - 7.x

Error: Cannot find module '@babel/core'
babel-loader@8 requires Babel 7.x (the package '@babel/core').
If you'd like to use Babel 6.x ('babel-core'), you should install 'babel-loader@7'.

没找到 @babel/core ,这里把 babel-core 卸载,并安装 @babel/core

npm un babel-core
npm i -D @babel/core

同理,也需要将 babel-preset-env 卸载后重新安装,最终安装好的文件是

"devDependencies": {
  "@babel/core": "^7.2.2",
  "@babel/plugin-transform-runtime": "^7.2.0",
  "@babel/preset-env": "^7.2.3",
  "@babel/runtime": "^7.2.0",
  "babel-loader": "^8.0.5",
}

babel 舍弃了以前的 babel-- 的命名方式,改成了 @babel/-

修改完依赖和 .babelrc 文件后就能正常启动项目了

{
  "presets": ["@babel/preset-env"],
  "plugins": [
    "@babel/plugin-transform-runtime"
  ]
}

为 React 项目配置 webpack 4

安装 React :

npm i react react-dom --D

因为前面已经安装了 babel-loader@babel/core@babel/preset-env , 所以只需安装babel-preset-react , 同上面一样的情况,在 babel 7 中,我们需要安装 @babel/preset-react

npm i @babel/preset-react --D
  • babel-loader : 使用 Babel 转换 JavaScript依赖关系的 Webpack 加载器
  • @babel/core : 即 babel-core,将 ES6 代码转换为 ES5
  • @babel/preset-env : 即 babel-preset-env,根据您要支持的浏览器,决定使用哪些 transformations / plugins 和 polyfills,例如为旧浏览器提供现代浏览器的新特性
  • @babel/preset-react : 即 babel-preset-react,针对所有 React 插件的 Babel 预设,例如将 JSX 转换为函数

再次强调:babel 7 使用了 @babel 命名空间来区分官方包,因此以前的官方包 babel-xxx 改成了 @babel/xxx

修改 webpack.config.js 和 .babelrc 文件

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/, // 不匹配选项(优先级高于test和include)
        use: 'babel-loader'
      },
  ]}
  // ...
}
{
  "presets": ["@babel/preset-env", "@babel/preset-react"],
  "plugins": [
    "@babel/plugin-transform-runtime"
  ]
}

然后在 src 目录下新建一个 App.js 的 react 组件

import React from "react"
import ReactDOM from "react-dom"
const App = () => {
  return (
    <div>
      <p>Hello React!</p>
    </div>
  )
}
export default App
ReactDOM.render(<App />, document.getElementById("app"))

接下来在 index.js 文件中 import(导入) 组件

import App from "./App"

在 index.html 中添加代码

<body>
  <p class="title">Hello Webpack-4-cli !</p>
  <div id="app"></div>
</body>

再次构建 npm run build 即可

如果想详细了解可以查阅这篇文章 React 教程:如何使用 webpack 4 和 Babel 构建 React 应用(2018)

图片资源的配置

在src目录下新建一个assets文件,里面放置图片

安装依赖文件

npm i file-loader -D

然后在 webpack.config.js 中添加配置

module.exports = {
  // ...
  module: {
    rules: [
      { // 图片loader
        test: /\.(png|jpg|gif)$/,
        use: [
          {
            loader: 'file-loader' // 根据文件地址加载文件
          }
        ]
      }
  ]}
  // ...
}

然后在 index.js 文件中引入

import img from './assets/star.png'

var oImg = new Image()
// oImg.src = require('./assets/star.png') // 当成模块引入图片
oImg.src = img
document.getElementById('image').appendChild(oImg)

然后在 index.html 中添加代码

<body>
  <p class="title">Hello Webpack-4-cli !</p>
  <div id="app"></div>
  <div id="image"></div>
</body>

别名(@)的配置

在 src 目录下新建一个文件夹 utils , 然后在 utils 中新建文件 format.js

module.exports = function format(chars) {
  return chars.toUpperCase()
}

如果我们要在 src/index.js 中引入这个 format.js 文件呢,理所当然是不是想这么写

import * as format from './utils/format'

这样写确实没错,但如果想使用别名代替呢?比如在 vue-cli 项目中常用的 @ 一个文件路径,其意思就是在 src 目录下,同样的方法,我们可以在 webpack.config.js 中添加配置

module.exports = {
  // ...
  resolve: { // 解析模块的可选项
    // modules: [ ]//模块的查找目录 配置其他的css等文件
    extensions: [".js", ".json", ".jsx", ".less", ".css"], // 用到文件的扩展名
    alias: { // 模快别名列表
      utils: path.resolve(__dirname,'src/utils')
    }
  },
  // ...
}

就可以在 index.js 中修改引入方式了

const format = require('utils/format')

CopyWebpackPlugin 插件和 webpack.ProvidePlugin 插件

  • 使用 CopyWebpackPlugin 可以将 src 下其他的不需要打包的文件直接复制到 dist 目录下
  • 使用 webpack.ProvidePlugin 可以在全局引用 lodash 这类的工具库,省去了import

在 webpack.config.js 中添加配置

module.exports = {
  // ...
  plugins: [
    new CopyWebpackPlugin([
      { from:'src/assets/redStar.png',to: 'redStar.png' }
    ]),
    new webpack.ProvidePlugin({
        '_': 'lodash'  // 引用 webpack
    })
  ],
  // ...
}

webpack dev server

想象一下,如果我们对代码进行更改,都需要 npm run dev,会不会远远降低我们的开发效率

如果使用 webpack dev server 配置,浏览器会自动启动你的应用程序

每次更改文件时,它都会自动刷新浏览器的窗口,比如 vue-cli 中,我们启动监听 npm run dev 时,可以监控我们 src 下文件的改动,那是怎么做到的呢?

在 webpack 里其实创建了一个 node 进程,通过 webpack-dev-server 其内部封装了一个 node 的 express 模块

首先安装包

npm i webpack-dev-server --D

接下来打开 package.json 并调整 scripts

"scripts": {
  "dev": "webpack-dev-server --mode development --open",
  "build": "webpack --mode production"
}

然后在 webpack.config.js 中添加配置

module.exports = {
  // ...
  devServer: { // 服务于webpack-dev-server  内部封装了一个express 
    port: '8888',
    before(app) {
      app.get('/api/test.json', (req, res) => {
        res.json({
          code: 200,
          message: 'Hello World'
        })
      })
    }
  }
  // ...
}

启动监听 npm run dev ,就可以看到 webpack dev server 在浏览器中启动了应用项目

完整代码链接

webpack-4-cli

学习资料

手写一个webpack4.0配置
从零配置到生产发布(2018)
使用 Webpack 4 和 Babel 7 从头开始创建 React 应用程序
babel 7.x 和 webpack 4.x 配置 vue 项目
阮一峰 Webpack 教程

css实现自定义复选框样式

前言

浏览器自带的checkbox复选框不怎么美观,而且复选框在不同的浏览器上显示的样式又有很大的差异

目的

用一些css代码来自定义checkbox的显示方式,比如框中的√颜色变为绿色

方法

通过appearance去修改css默认样式

html:
<input type="checkbox" id="awesome" />
<label for="awesome"></label>

css:
input[type="checkbox"]{
  -webkit-appearance: none;
  top: 5px;
  float: left;
  position: relative;
  vertical-align:middle;
  margin-top:0;
  background:#fff;
  border:#dedede solid 1px;
  border-radius: 3px;
  min-height: 14px;
  min-width: 14px;
}
input[type="checkbox"]:focus {
  outline: none;
}
input[type=checkbox]:checked::after{
  content: '';
  top: 1px;
  left: 1px;
  position: absolute;
  vertical-align: middle;
  margin-top: 0;
  background: transparent;
  border: #85de82 solid 2px;
  border-top: none;
  border-right: none;
  height: 4px;
  width: 8px;
  -moz-transform: rotate(-45deg);
  -ms-transform: rotate(-45deg);
  -webkit-transform: rotate(-45deg);
  transform: rotate(-45deg);
}

原理比较简单,通过css的appearance属性去修改css默认样式

appearance是css3的新属性,所有主流浏览器都不支持appearance,所以让chrome支持用-webkit-appearance

  • appearance:none为去除浏览器默认checkbox样式。然后再通过border、height、width等属性重绘一个灰色边框

  • 去掉input元素获得焦点后的轮廓

  • 然后通过伪元素after来模拟选中状态;::after设置在选中input对象后发生的内容,必须和content属性一起使用;通过绝对定位在之前checkbox里生成一个宽8像素高4像素的绿色边框,然后去掉上边框和右边框,再逆时针旋转45度就可以模拟成√的样式

利用label标签的模拟功能

html:
<input type="checkbox" id="awesome" />
<label for="awesome"></label>

css:
input[type="checkbox"] + label::before {
  content: ' ';
  display: inline-block;
  vertical-align: .2em;
  width: .8em;
  height: .8em;
  margin-right: .2em;
  border-radius: .2em;
  background: silver;
  text-indent: .15em;
  line-height: .65;
}
input[type="checkbox"]:checked + label::before {
  content: '\2713';
  color: #85df82;
}
input[type="checkbox"] {
  position: absolute;
  clip: rect(0,0,0,0);
}

主要思路:

label的for属性可以关联一个具体的input元素,即使这个input本身不可被用户可见,有个与它对应的label后,我们就可以直接通过和label标签交互来替代原生的input(比如相邻选择符(E+F),伪元素after,before,可以直接利用html的默认checkbox)

一句话概括就是隐藏原生input,样式定义的过程留给label

  • 通过关系选择符选择紧贴在input元素之后的label元素,然后通过before伪元素设置在label对象前的内容样式(这里自定义一个灰色边框,背景白色,通过inline-block定位来并排显示)

  • 然后模拟选中状态,content:‘\2713’是模拟√

  • 然后通过css的clip属性裁剪绝对定位的checkbox元素

使用自定义图片来实现checkbox的显示

html:
        <input  type="checkbox"  id="checkbox01" />
       <label  for="checkbox01"></label>
 
css:
/* 隐藏checkbox */
input[type='checkbox'] {
display: none;
}
/* 对label进添加背景图片*/
label {
  display: inline-block;
  width: 60px;
  height: 60px;
  position: relative;
  background: url(unchecked.png);
  background-repeat: no-repeat;
}
 input[type='checkbox']:checked + label {
  width: 60px;
  height: 60px;
  position: relative;
  background: url(checked.png);
  background-repeat: no-repeat;
   }
  • 首先删除掉input元素

  • 然后对label添加背景图片,也就是未选中时呈现的图片

  • 再通过关系选择符选择紧贴在checked状态之后的label元素,再添加一张选中状态的背景图片
    (因为label本身是没有点击后被选中的状态的,checkbox被隐藏后,这个状态只能手动模拟)

ESLint使用心得

介绍

简单来说,ESLint是一个避免低级错误和统一代码风格的工具.

比如:在Javascript应用中,我们很难找到多余或者忘记声明的变量和方法,或者少空格,多字符。那怎样才能分析JS代码,找到bug并在一定程度上保证JS语法的正确性和规范性呢?

已经出现的解决这类问题的工具有很多,这篇文章要介绍的是这类工具中最高大上的---ESLint

ESLint具有有以下几个特性:

  • 可插拔--所有的东西都是可以随意关闭的;
  • 可配置--任意的rule都是独立的,开启后可自定义代码规范,细化配置;
  • 自由--没有特定的coding style,可以自己配置
  • 多插件--引用共享配置或自定义规则

ESLint提供检验的范围:

  • 语法错误校验
  • 不重要或丢失的标点符号,如分号
  • 多写火少些的空格
  • 没法运行到的代码块
  • 未被使用的参数提醒
  • 漏掉的结束符,如}
  • 检查变量的命名,如是否申明

安装

  1. 进入项目目录中,需要项目中没有package.json文件,需要先创建一个
npm init

局部安装ESlint

npm install eslint --save-dev
  1. 设置一个配置文件,在回答些许问题之后会生成一个.eslintrc.*文件.
    我一般选用的javascript文件,也就是使用.eslintrc.js文件并导出一个包含配置的对象.
eslint --init

当询问到代码风格时,通常默认的是standard style

  1. 在项目目录下执行如下命令来使用ESLint
eslint yourfile.js 

配置

可以被配置的信息主要分为3类:

-Environments:你的 javascript 脚步将要运行在什么环境(如:nodejs,browser,commonjs等)中

-Globals:执行代码时脚步需要访问的额外全局变量

-Rules:开启某些规则,也可以设置规则的等级

指定执行环境

在配置文件中可自由的指定JS执行环境,如下面的代码表示在浏览器中运行

env: {
    browser: true,
  }

指定全局变量

可以在配置文件或注释中指定额外的全局变量

globals: {
    $: true,
    _: true
  }

指定规则

-"off" 或者 0:关闭规则

-"warn" 或者 1:打开规则,并且作为一个警告

-"error" 或者 2:打开规则,并且作为一个错误

配置文件来配置

'rules': {
    'arrow-parens': 0,
    'generator-star-spacing': 0,
    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
  }

注释来配置

/* eslint-disable no-alert, no-console */
/* eslint-enable no-alert, no-console */

使用方法

命令行

eslint yourfile.js  

编辑器

在VSCode中配合相应的插件

image

vue脚手架

在package.json的script中添加注释行
如下代码表示检验src路径下所有带.js .vue扩展名的文件

"lint": "eslint --ext .js,.vue src"

然后命令行运行npm run lint,就会直接显示错误

image

Generator / yield 与 async / await

异步编程对JS的重要性应该是不言而喻了的,作为单线程的编程语言,如果没有异步编程,体验不要太酸爽

比如我们有一个读取文件并进行处理的任务。如果有异步编程,先是向操作系统发出请求,要求读取文件。然后,程序会去执行其他任务,等到操作系统返回文件,再接着处理文件的任务

相反的,如果没有异步函数,上述编程就变成了同步的执行方式。由于是连续执行,不能插入其他任务,所以操作系统从硬盘读取文件的这段时间,程序只能干等着

Promise

从 Promise 开始,JavaScript 就在引入新功能来实现更简单的方法处理异步编程,帮助我们远离回调地狱

回调地狱也就是多个回调函数嵌套带来的问题。比如读取文件A之后,再读取文件B

fs.readFile(fileA, function (err, data) {
  fs.readFile(fileB, function (err, data) {
    // ...
  })
})

很明显,在读取多个文件的情况下,代码就会出现多重嵌套

一个很明显的特点是,代码不是纵向发展,而是横向发展的,造成的结果是代码混乱,无法管理

为了解决这个问题,就出现了 Promise

注意,它不是新的语法功能,而是一种新的写法,这里就不具体介绍它的实现了,有兴趣的小伙伴可以翻阅我的另一篇博客分层解析 Promise 的实现原理

Promise 允许将回调函数的横向加载,改成纵向加载

回到上面那个读取文件的问题,用 Promise 实现就会简洁很多

var readFile = require('fs-readfile-promise')

readFile(fileA)
.then(function(data){
  console.log(data.toString())
})
.then(function(){
  return readFile(fileB)
})
.then(function(data){
  console.log(data.toString())
})
.catch(function(err) {
  console.log(err)
})

Promise 提供 then 方法加载回调函数,catch方法捕捉执行过程中抛出的错误

但 Promise 也有它的缺点,比如代码冗余,原来的函数需要被 Promise 包装,这样不管什么操作,都会出现一堆 then ,语义也不是明确

那有没有更好的写法呢?

Generator / yield

ES6 推出了 Generator / yield 两个关键字,使用 Generator 可以很方便的帮助我们建立一个处理 Promise 的解释器,比较重要的是

第一,Generator (生成器)函数最大的特点就是可以交出函数的执行权(即暂停执行),yield 表达式就是暂停的标志

第二,控制 Generator 函数的执行/暂停的方法是 next() 方法,它是由 Generator 函数返回的一个迭代器提供的

接下来我们对这两点进行详细说明

执行过程

生成器函数的语法为 function*,在其函数体内部可以使用 yield 关键字

看到这里,有的小伙伴可能会有疑问,生成器函数也是函数,那他和普通函数有什么区别呢?我们先来看一个简单的例子

假设你已经引入了fetch文件 <script src="https://cdn.bootcss.com/fetch/2.0.4/fetch.js"></script>

function* gen(num){
  console.log('first')
  var result = yield num + 2
  console.log('second')
  return result
}

var g = gen(1)

当我们这样执行代码的时候,会发现控制台根本没有输出,为什么呢?这也就是生成器函数和普通函数的区别,它可以交出执行权,即暂停执行。yield 表达式就是暂停标志,再次强调上述第一点

那应该怎样让暂停的代码继续执行了,也就是上述第二点,可以通过 next 方法。执行结果是一个包含 value 和 done 属性的对象

接着上面的 demo,我们执行 console.log(g.next()),就会出现

// first
// { value: 3, done: false }

接着再执行 console.log(g.next())

// second
// { value: undefined, done: true }

上面代码一共调用了两次 next 方法

第一次调用, Generator 函数开始执行,直到遇到第一个 yield 表达式为止。next 方法返回一个对象,它的 value 属性就是当前 yield 表达式的值,done 属性的值 false,表示遍历还没有结束

第二次调用,Generator 函数从上次 yield 表达式停下的地方,一直执行到下一个 yield 表达式或者 return 语句(如果没有,就执行到函数结束)。next 方法返回的对象的 value 属性就是当前 yield 表达式的值或者 return 语句后面的表达式的值(如果都没有,则 value 属性的值为 undefined ),done 属性的值 true ,表示遍历已经结束。

next 方法的作用是分阶段执行 Generator 函数。每次调用 next 方法,会返回一个对象,表示当前阶段的信息( value 属性和 done 属性)。value 属性是 yield 语句后面表达式的值,表示当前阶段的值;done 属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段

数据交换和错误处理

我们比较熟悉的是 Generator 函数可以暂停执行和恢复执行,这也是它能封装异步任务的根本原因

除此之外,还有两个作为异步编程完整方案的特性:函数体内外的数据交换和错误处理机制

数据交换

Generator 函数向外输出数据:next 方法返回值的 value 属性

Generator 函数体内输入数据: 通过 next 方法接受参数

还是上面的例子,修改一下代码

function* gen(num){
  console.log('first')
  var result = yield num + 2
  console.log('second')
  return result
}

var g = gen(1)
console.log(g.next())
console.log(g.next(2))

控制台就打印出了

// first
// { value: 3, done: false }
// second
// { value: 2, done: true }

上面代码中,第一个 next 方法的 value 属性,返回表达式 num + 2 的值(3)。第二个 next 方法带有参数 2,这个参数可以传入 Generator 函数,作为上个阶段异步任务的返回结果,被函数体内的变量 result 接收。因此,这一步的 value 属性,返回的就是 2(变量 result 的值)

错误处理机制

Generator 函数内部还可以部署错误处理代码,捕获函数体外抛出的错误

function* gen(x){
  try {
    var y = yield x + 2
  } catch (e){ 
    console.log(e)
  }
  return y
}

var g = gen(1)
g.next()
g.throw('出错了'

上面代码的最后一行,Generator 函数体外,使用指针对象的 throw 方法抛出的错误,可以被函数体内的 try ... catch 代码块捕获。这意味着,出错的代码与处理错误的代码,实现了时间和空间上的分离,这对于异步编程无疑是很重要的

执行异步任务

手动执行

手动执行其实就是用 then 方法,层层添加回调函数。接下来我们看一个实际的异步例子

function* gen(){
  var result1 = yield fetch('https://api.github.com/users/github')
  var result2 = yield fetch('https://api.github.com/users/github')
  console.log('result1',result1)
  console.log('result2',result2)
  return result2
}
// 手动执行Generator 函数
var g = gen()
g.next(1).value.then(function(data){
  console.log('第一次执行g.next()',data)
  g.next(data).value.then(function(data){
    console.log('第二次执行g.next(),并将data传给result1',data)
    return data.json()
  }).then(function(data){
    console.log('解析出第二次结果的json数据',data)
    console.log('将data传入给result2',g.next(data))
    console.log('已执行结束',g.next())
  })
})

上面代码中,Generator 函数封装了一个异步操作,该操作先读取一个远程接口,然后从 JSON 格式的数据解析信息

首先执行 Generator 函数,获取遍历器对象,然后使用 next 方法,执行异步任务的第一阶段。由于 Fetch 模块返回的是一个 Promise 对象,因此要用 then 方法调用下一个next 方法

假如我们同时执行的异步任务相互不依赖,这里用一个同步任务来模拟异步任务执行快的过程,如下

function* gen(num){
  var result1 = yield fetch('https://api.github.com/users/github')
  var result2 = yield num + 2
  console.log('result1',result1)
  console.log('result2',result2)
  return result2
}

var g = gen(1)
g.next().value.then(function(data){
  console.log('data',data)
})
console.log(g.next('result1'))
console.log(g.next('result2'))

打印出来的结果

image

相信大家已经对 next 方法的运行逻辑明白的差不多了,大致也就是

  1. 每调用一次 g.next() 方法都会暂停执行 yield 后面的操作,并将 yield 后面的表达式的值作为返回对象中 value 的值

  2. 下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式

  3. 如果没有再遇到新的 yield 表达式,就一直运行到函数结束,直到 return 语句为止,并将 return 语句后面的表达式的值,作为返回的对象的 value 属性值

需要注意的是,yield 表达式后面的表达式,只有当调用 next 方法、内部指针指向该语句时才会执行

可以看到,虽然 Generator 函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段),那应该怎样自动化异步任务的流程管理呢

Generator 函数就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。

两种方法可以做到这一点。

  1. 回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。

  2. Promise 对象。将异步操作包装成 Promise 对象,用 then 方法交回执行权。

这里就不着重讲回调函数的方法了,有兴趣的小伙伴可以参考 Thunk 函数的含义和用法

基于 Promise 对象的自动执行

function* gen(){
  var result = yield fetch('https://api.github.com/users/github')
  console.log('result',result)
  return result
}
function run(gen){
  var g = gen()

  function next(data){
    var result = g.next(data)
    if (result.done) return result.value
    result.value.then(function(data){
      return data.json()
    }).then(function(data){
      next(data)
    })
  }

  next()
}

run(gen)

上面代码中,只要 Generator 函数还没执行到最后一步,next 函数就调用自身,以此实现自动执行

co 函数库

co 模块是 nodejs 社区著名的TJ大神写的一个小工具,用于 Generato r函数的自动执行。co 是上面那个自动执行器的扩展,官网上面有具体的 demo

image

源码解析可以参考co 函数库的含义和用法

首先,co 函数接受 Generator 函数作为参数,返回一个 Promise 对象

function co(gen) {
  var ctx = this

  return new Promise(function(resolve, reject) {
  })
}

在返回的 Promise 对象里面,co 先检查参数 gen 是否为 Generator 函数。如果是,就执行该函数,得到一个内部指针对象;如果不是就返回,并将 Promise 对象的状态改为 resolved

function co(gen) {
  var ctx = this

  return new Promise(function(resolve, reject) {
    if (typeof gen === 'function') gen = gen.call(ctx)
    if (!gen || typeof gen.next !== 'function') return resolve(gen)
  })
}

接着,co 将 Generator 函数的内部指针对象的 next 方法,包装成 onFulefilled 函数。这主要是为了能够捕捉抛出的错误

function co(gen) {
  var ctx = this

  return new Promise(function(resolve, reject) {
    if (typeof gen === 'function') gen = gen.call(ctx)
    if (!gen || typeof gen.next !== 'function') return resolve(gen)

    onFulfilled()
    function onFulfilled(res) {
      var ret
      try {
        ret = gen.next(res)
      } catch (e) {
        return reject(e)
      }
      next(ret)
    }    
  })
}

最后,就是关键的 next 函数,它会反复调用自身

function next(ret) {
  if (ret.done) return resolve(ret.value)
  var value = toPromise.call(ctx, ret.value)
  if (value && isPromise(value)) return value.then(onFulfilled, onRejected)
  return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
        + 'but the following object was passed: "' + String(ret.value) + '"'))
    }
})

> 上面代码中,next 函数的内部代码,一共只有四行命令

> 第一行,检查当前是否为 Generator 函数的最后一步,如果是就返回。

> 第二行,确保每一步的返回值,是 Promise 对象。

> 第三行,使用 then 方法,为返回值加上回调函数,然后通过 onFulfilled 函数再次调用 next 函数。

> 第四行,在参数不符合要求的情况下(参数非 Thunk 函数和 Promise 对象),将 Promise 对象的状态改为 rejected,从而终止执行。

下面是一个 Generator 函数,用于依次读取两个文件

var fs = require('fs')

var readFile = function (fileName){
  return new Promise(function (resolve, reject){
    fs.readFile(fileName, function(error, data){
      if (error) reject(error)
      resolve(data)
    })
  })
}

var gen = function* () {
  var f1 = yield readFile('/etc/fstab')
  var f2 = yield readFile('/etc/shells')
  console.log(f1.toString())
  console.log(f2.toString())
}

var co = require('co')
co(gen)

co模块可以让你不用编写Generator函数的执行器。Generator函数只要传入co函数,就会自动执行。co函数返回一个Promise对象,因此可以用then方法添加回调函数。

co(gen).then(function () {
  console.log('Generator 函数执行完成')
})

co模块的原理:其实就是将两种自动执行器(Thunk函数和Promise对象),包装成一个模块。使用co的前提条件是,Generator函数的yield命令后面,只能是Thunk函数或Promise对象。如果数组或对象的成员,全部都是Promise对象,也可以使用co(co v4.0版以后,yield命令后面只能是Promise对象,不再支持Thunk函数)

co 支持并发的异步操作,即允许某些操作同时进行,等到它们全部完成,才进行下一步。这时,要把并发的操作都放在数组或对象里面

async/await

在 ES7 ,得到了 Generator/yield 这样的语法,可以让我们以接近编写同步代码的方式来编写异步代码(无需使用.then()或者回调函数)

目前,它仍处于提案阶段,但是转码器Babel和regenerator都已经支持

async函数可以说是目前异步操作最好的解决方案,是对Generator函数的升级和改进

是什么

async 函数就是 Generator 函数的语法糖

注意:await只能用在async函数中

上面那个读取两个文件的例子,写成 async 函数,就是下面这样

var asyncReadFile = async function (){
  var f1 = await readFile('/etc/fstab')
  var f2 = await readFile('/etc/shells')
  console.log(f1.toString())
  console.log(f2.toString())
}

可以发现,async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await

优点

  1. 内置执行器。Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。
var result = asyncReadFile()
  1. 更好的语义。 async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果

  2. 更广的适用性。 co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)

async 函数的实现

async 函数的实现,就是将 Generator 函数和自动执行器,包装在一个函数里

async function fn(args){
  // ...
}

// 等同于

function fn(args){ 
  return spawn(function*() {
    // ...
  })
}

所有的 async 函数都可以写成上面的第二种形式,其中的 spawn 函数就是自动执行器

async 函数的用法

同 Generator 函数一样,async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句

var url = 'https://api.github.com/users/github'

async function getUrl(url) {
  var result1 = await fetch(url)
  var result2 = await fetch(url)
  console.log('result1',result1)
  console.log('result2',result2)
  return result2.json()
}

!async function() {
  var result = await getUrl(url)
  console.log(result)
}()

// getUrl(url).then(function(data){
//   console.log('data',data)
// })

控制台打印结果为

image

需要注意的是

  1. await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try...catch 代码块中
var url = 'https://api.github.com/users/github'

async function getUrl(url) {
  try {
    await fetch(url)
  } catch (err) {
    console.log(err)
  }
}

!async function() {
  var result = await getUrl(url)
  console.log(result)
}()
  1. await 命令只能用在 async 函数之中,如果用在普通函数,就会报错

  2. 如果这两个是独立的异步操作,可以让它们同时触发

let one = await getOne()
let two = await getTwo()

let [foo, bar] = await Promise.all([getOne(), getTwo()])

参考文献

Generator 函数的含义与用法

co 函数库的含义和用法

async 函数的含义和用法

Generator 函数的语法

前端 DES 加密实践

由于项目中需要对用户登录的密码进行加密处理,特意学习了下,网上有很多 DES 加密的 js 代码,也有一些 java 解密不成功的,问题大都是 解密出来乱码的问题

今天分享的是前端 js 的 DES 加密 - 对称加密,后端使用 java 进行 DES 解密的,这里就不做介绍了

JS端加密过程处理中,比较重要的对 key 的设定以及 DES 加密模式的配定

  1. key 的处理:通过创建指定的 key ,key 必须是 16 位 / 24 位 / 32 位中的一种,常见的加密 Key 24 位的 16 进制

  2. DES 加密模式:常见的两种加密方式是 ECB 和 CBC 模式

我们可以直接采用谷歌的 crypto-js

根据它的文档介绍,需要先安装 crypto-js 依赖

npm install crypto-js

然后引入 crypto-js const CryptoJS = require("crypto-js")

在 github 上,我们可以看到其文档相当详细

image

根据文档介绍我们就可以写出具体的demo了

JS 端的 Demo ,通过DES加密,Base64 编码,需要引入封装好的 DES加密解密算法

// DES加密用户密码
encryptByDES(message) {
  const now = new Date()
  const day = now.getDate() < 10? `0${now.getDate()}` : now.getDate()
  const username = 'zhangsan'
  const firstName = JSON.parse(JSON.stringify(username)).charAt(0)
  const secondName = JSON.parse(JSON.stringify(username)).charAt(1)
  // 秘钥 key
  const key = `${firstName}HAH_${secondName}${day}`
  // 把私钥转换成24位的16进制的字符串,不足24位自动以0(最小位数是0)补齐,如果多余24位,则截取前24位,后面多余则舍弃掉
  const keyHex = CryptoJS.enc.Utf8.parse(key);
  // 加密模式为ECB padding为PKcs7
  const encrypted = CryptoJS.DES.encrypt(message, keyHex, {
      mode: CryptoJS.mode.ECB, // ECB 模式
      padding: CryptoJS.pad.Pkcs7 // padding 处理
  });
  // 加密出来是一个16进制的字符串
  return encrypted.toString();
}

encryptByDES('123456')

这样就对 '123456' 密码进行了 DES 加密,key 在这里设置的是 'zHAH_h03', '03' 表示的当天的日期

加密后的结果类似于

1JQhSMa4Mhk=

参考资料

https://blog.csdn.net/bob_Xing_Yang/article/details/80417383

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.