GithubHelp home page GithubHelp logo

records's Introduction

records's People

Contributors

kangjs7854 avatar

Stargazers

 avatar

records's Issues

我讨厌dom节点

这两周由于维护一些年纪和我相仿的项目,写得我心力交瘁

一度丧失了对前端的热爱,沦为一个没有感情的切图仔,疯狂的产出页面和基本交互,都不需要我处理逻辑,彷佛穿越回了零几年。看着组里的java大佬们jsp写得风生水起,不亦乐乎,唯一的前端仔暗自神伤,独自忧伤。

其实项目需求很简单,就是展示商品列表,点击弹窗显示详情,tab栏切换不同频道。需求评审的时候我甚至在脑子里用vue和react各写了几遍,心想就这?然后大佬说要用jsp开发,主要是迭代维护之前的老项目,我就负责开发页面和基本交互,用经典的网页三剑客,html,css,js,开发完就把文件夹丢给java大佬处理业务逻辑。

我心想倒也简单,三下除以五写完了样式了,作为一个稍微有点代码洁癖的菜鸟,写原生js也不忘规范

function init() {
    switchBar()
    closeDialog()
    openDialog()
    handleSearch()
    handleLoadMore()
    
}

function switchBar(){
//balabala
}

function closeDialog(){
//balabala
}

function openDialog(){
//balabala
}

function handleSearch(){
//balabala
}

functuon handleLoadMore(){
//balabala
}

function initListDom(){
//balabala
}

init()

写完之后看着自己的代码,两个字,优雅~看来面向对象的编程**已经扎根于心,成为中级cv工程师指日可待,内心不禁窃喜,直到半天后java大佬捧着他的macbookpro过来找我,开口就是一句你这写得有问题啊,
没想到我和mbp第一次的邂逅这么的尴尬,想到这有点恼火,差点脱口而出”你在教我写代码?“

定睛一看,原来是之前写的批量插入dom节点的函数,在切换导航条时获取到的数据,也是直接插入到dom节点的后面,但是这个时候应该是要覆盖掉原来的节点,这还不简单,脱口而出”我记得原生js有一个repalceChild的方法,你可以试试“,然后过了五分钟java大佬又过来了,说第二个参数传啥,替不了。后来我百度了一番,时间滴滴答答的走,研发经理下班前过来看了一眼,意味深长的眼神让我脑子里一片空白”不会吧不会把,不会还没过试用期就要被辞退了嘛,这脸丢大了呀“。

不行,我一定要找到办法,于是开始了面向百度编程,替换节点不行那就删除掉再插...又感觉遍历li标签挨个删除又插入实在性能杀手,有悖优雅,遂弃之.......

晚上八点多的时候,办公司人都走光了,一片祥和,原本躁动的心也慢慢地平复下来,思路也清晰了,才想到可以使用拼接字符串的方式插入dom元素,这样视图上的内容就是根据数据的内容动态渲染,问题解决

function initListDom(){
    let liDomStr = ''
    listArr.forEach(el=>{
        liDomStr += `
            <li>
                ${el.tradeName}
            </li>
        `
    })
    listDom.innerHTML = liDomStr
}

思考

突然意识到,这不就是现代框架的一个理念嘛,避免操作dom节点去更新视图,用数据去驱动视图,传说中的UI=render(data)公式竟然被我不小心悟到了,想到这我就心满意足的下班了

后来我又写了一个jsp 的页面,还是一样的套路,三下除以五写完了,但是又感觉少了点什么,突然意识到是不是可以通过引用vue cdn连接的方式和我讨厌的dom节点说拜拜。 于是到了公司马上咨询了大佬是否可行,然后我把之前的两个页面改成了vue版本,发给他查看后,一番攀谈交心,毅然走上了改革开放新的道路。

到这故事就结束了,只是特别感慨,感谢尤大大开发了这么好用的框架

mobx-react-devtools

mobx的原理实际和vue很相似

都是通过代理数据对象的getter和setter

mobx会为每个组件创建一个Watcher,在数据的getter和setter上加钩子,当组件渲染的时候(例如,调用render方法)会触发getter,然后把这个组件对应的Watcher添加到getter相关的数据的依赖中(例如,一个Set)。当setter被触发时,就能知道数据发生了变化,然后同时对应的Watcher去重绘组件。

在mobx中需要将数据声明成可观察的对象(推荐使用装饰器的写法);要改变状态得通过action的装饰器,这里和vuex十分相似;通过@Inject可以在任意组件使用store的状态,我觉得这个api特别的好,不仅易于理解,而且使用起来十分方便。

在使用了上述几个api后就可以愉快的管理状态,再也不用烦恼组件间的状态处理。但是很快就遇到了调试上的麻烦,mobx将变量用proxy代理之后在控制台上输出的都是一个proxy对象,在chrom浏览器里调试变量也十分不方便。就需要使用到一些mobx的调试工具。

使用mobx-react-devtools

安装npm i mobx-react-devtools -D后在react中引入DevTools这个组件就可以使用。
这里注意版本兼容,由于mobx-react-devtools这个包不兼容mobx-react@^6以上的版本,需要手动安装低版本的对其兼容。否则就会报Cannot read property 'on' of undefined的错误。

小程序请求的封装

小程序原生的api有着大量的回调函数

当业务逻辑一复杂,很容易就陷入了回调地狱,可读性和可维护性都特别差。
好在小程序开发者工具支持es6转es5,我们利用es6的promise特性来封装网络请求

promise会返回两种状态,已完成和未完成,并且状态只能改变一次,所以我们可以把成功和失败的回调放到这两个状态中去执行

1.创建http.js文件

import md5 from './md5'
const app = getApp()
const WX = app.proxy()
module.exports = {
  /**
    * 默认post请求,将小程序的request封装成promise,解决回调嵌套过深的问题
    * @param {String} url 请求路径
    * @param {Object} params 请求参数
    * @param {String} token 身份认证信息标识
    */
  http(url, params) {
    //时间戳
    const requestTimestamp = new Date().getTime();
    //唯一的id
    const requestId = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
      let r = Math.random() * 16 | 0,
        v = c == 'x' ? r : (r & 0x3 | 0x8);
      return v.toString(16);
    })

    //生成签名
    const paramsArr = Object.keys(params)
    let signStr = ''
    //ASCII排序参数
    paramsArr.sort()
    paramsArr.forEach(el=>{
      signStr += `${el}=${params[el] || ''}&`
    })
    const key = '' //与服务端协商好的key
    //最掉最后一个参数的&
    signStr = signStr.slice(0,-1) + key
    const sign = md5(signStr).toUpperCase()

    const header = {
      'content-type': 'application/json',
      requestTimestamp,
      requestId,
      sign
    }

    //如果token存在,写入请求头
    const userInfo = wx.getStorageSync("userInfo")
    userInfo.token && Object.assign(header, { requestToken: userInfo.token })

    return WX.request({
      url,
      data: params,
      header,
      method: 'POST',
      dataType: 'json',
      responseType: 'text',
    }).then(res => {
      if (res.statusCode == 200) {
        return res.data
      }
      app.toast(res.statusCode)
      return res
    }).catch(err => {
      app.toast(res.statusCode)
      return err
    })
  }

}

2. 创建api.js文件夹,方便统一管理api

使用es6默认参数的新特性,在编码时像vscode这样的编译器会提供参数提醒等功能,方便了接口联调,不需要在接口文档和编辑器之间反复横跳


import { http } from './http'

const baseUrl = 'http://localhost:3000'//请求的域名

module.exports = {
    //测试
    test(params = { name }) {
        return http(baseUrl + "/api/test", params)
    },
    getUserInfo(params = { code }) {
        return http(baseUrl + "/api/wxLogin", params)
    },
}


使用

import api from "../../utils/api"

onLoad() {
        api.test({name:"kjs"}).then((res)=>{
            console.log(res);
        })
    },

加密

由于数据传输中容易被抓包导致数据泄露,被篡改,需要一些安全措施来保证数据传输的安全性

  1. 请求来源识别
  • Origin : 请求来源的域名
  • Referer : 请求来源是从哪个页面链接引用过来的
  • User-Agent : 请求来源的设备信息

由于这些信息在前端都可以修改,所以不是很可靠

  1. 身份验证

由于http是无状态的协议,通过一应一答的方式进行数据传输。但是遇到需要保存状态的时候,可以使用cookie,seesion,token作为客户端和服务端会话验证的凭证

  • cookie
    缺点:cookie容量比较小;api不灵活,需要自行封装;每次请求都会发送到服务端,造成性能困扰等问题;在http-only属性设置为fasle时容易遭受XSS攻击;容易遭受跨站请求攻击(CSRF)

  • seesion
    服务端生成,在验证通过后在seeion里保存信息,并将seesionId写入响应头返回给前端,之后再次请求时服务器会读取这个sessionId,与存储的用户信息进行验证匹配。
    缺点:大量的用户信息读写会给内存造成压力,并且面对服务器集群无法共享。

  • token
    token为一个身份标识的令牌,为当前比较流行的登录验证的方式,由于不需要把存储数据在服务端,减轻了服务端的压力。在身份信息认证成功后返回给前端保存,在每次请求的时候携带这个token标识身份。

感觉可以简单理解成一个接口通行证的令牌

  1. 加密
  • 对称加密
    加密和解密的私钥为同一个,计算速度快,但是一旦泄露就不安全了

  • 非对称加密
    由公钥和私钥成对组成,如果使用公钥加密,只有对应的私钥才能解密

  1. 加签
    通过生成数字签名来报证数据的安全性,这里比较常见的是使用md5算法。
    将需要提交的数据通过某种顺序排序组合再加上一个双方规定的用户密钥。
    当客户端发送的签名和服务端生成的签名一致则证明数据传输没问题。
    //生成签名
    const paramsArr = Object.keys(params)
    let signStr = ''
    //ASCII排序参数
    paramsArr.sort()
    paramsArr.forEach(el=>{
      signStr += `${el}=${params[el] || ''}&`
    })
    const key = '' //与服务端协商好的key
    //最掉最后一个参数的&
    signStr = signStr.slice(0,-1) + key
    const sign = md5(signStr).toUpperCase()


``

5. 时间戳
为了防止DDOS的攻击,增加时间戳的请求时间标识来让服务端判断,但是客户端的时间并不可靠
const requestTimestamp = new Date().getTime();

github第三方登录

使用第三方的授权登录非常的方便,可以获取该平台的用户数据信息

假设现在有一个网站需要github登录

授权步骤

  1. 在该网站通过点击跳转到github授权页面
    2.github授权页面提醒是否愿意授权
    3.授权成功重定向回该网站并携带一个code,这里可以理解为授权码
    4.授权码为一个临时凭证,还需要想github申请一个令牌才能访问用户信息。
    5.将该授权码传给服务端,服务端发送github的api验证是否过期
    6.验证成功返回令牌,拥有了令牌即可调取github接口获取用户信息,再由服务端返回给前端。

授权前登记

我们需要在github的网站上申请一个client_idclient_secret,作为应用的标识
点击 登记,填写信息后生成上述两密钥信息。github授权最棒的一点是也支持本地服务的域名,所以我们可以使用localhost来实验。

前端

前端就是在登录页面使用一个a标签链接到授权页面,记得携带上面申请的client_idredirect_uri,后者为授权成功后重定向的网站,这里我重定向回首页。

 window.location.href= 'https://github.com/login/oauth/authorize?client_id=50ab343567bd310005df&redirect_uri=http://127.0.0.1:9000'

授权成功重定向首页后会发现url上携带了code=xxxxxxxxx的信息。我们需要使用js截取获得该授权码,将其发送到服务端进行验证。

const code = this.getUrlParams('code')
if(!code) return
const res = await api.sendAuthCode(code)

后端

后端要做的就是验证该授权码是否过期,换取令牌,请求用户信息,保存并返回给前端展示

router.post('/auth', async (req, res, next) => {
    const { code } = req.body
    if (!code) return
    const clientID = '50ab343567bd310005df'
    const clientSecret = 'deea41faa0a55396c16f7679e16e61c2229f2f6a'
    //根据临时code换取令牌
    const tokenResponse = await axios({
        method: 'post',
        url: 'https://github.com/login/oauth/access_token?' +
            `client_id=${clientID}&` +
            `client_secret=${clientSecret}&` +
            `code=${code}`,
        headers: {
            accept: 'application/json'
        }
    });
    const accessToken = tokenResponse.data.access_token;
    if(!accessToken){
        return res.json(tokenResponse.data)
    }
    //使用令牌请求gitHub的接口
    const result = await axios({
        method: 'get',
        url: `https://api.github.com/user`,
        headers: {
            accept: 'application/json',
            Authorization: `token ${accessToken}`
        }
    });
    res.json(result.data)

    //存储用户数据到数据库
    const query = { id:result.data.id }
    const payload = { ...result.data }
    await authController.insert(query, payload)

})

小程序缓存封装

小程序中的存储api特别不友好,通过二次封装实现类似localStorage的缓存功能

/**
 * @param {String} key 缓存字段
 * @param {Object} value 缓存内容
 * @param {Number} expires 缓存时间/h
 */
export default class LocalCache {
    constructor() {
        this.now = new Date().getTime()
    }

    setItem(key, value, expires) {
        expires && Object.assign(value, {
            expiresTime: this.now + parseInt(Number(expires) * 60 * 60 * 1000)
        })
        wx.setStorageSync(key, value);
    }

    getItem(key) {
        const value = wx.getStorageSync(key)
        if (!value.expiresTime) return value
        return value.expiresTime >= this.now
            ? value
            : wx.removeStorageSync(key);
    }

    removeItem(key) {
        wx.removeStorageSync(key);
    }

    clearAll() {
        wx.clearStorageSync();
    }
}

使用

在app.js 引入并创建实例

//app.js
import LocalCache from './utils/cache'

App({
  localCache : new LocalCache()
})

测试代码

app.localCache.setItem('test',{name:'kjs'},1)

const data = app.localCache.getItem('test')

app.localCache.removeItem('test')
``

mongod.exe闪退

1. 主要是要在目录多配置一个mongo.conf文件

如果mongodb的目录没有data文件夹,记得新建一个,为下方dbpath的路径

#mongo.conf文件

# 新增的数据库的文件夹路径,根据自己的安装路径填写
dbpath=D:\Program Files\MongoDB\Server\4.2\data
# 新增的日志记录文件
logpath=D:\Program Files\MongoDB\Server\4.2\log\mongo.log
#错误日志采用追加模式
logappend=true
#启用日志文件,默认启用
journal=true
#这个选项可以过滤掉一些无用的日志信息,若需要调试使用请设置为false
quiet=true
#端口号 默认为27017
port=27017

2. 在bin目录打开cmd,安装mongodb服务

mongod.exe --config "//此处为第一步配置的mongo.conf文件的路径" --install 

3. 测试,启动服务

net start mongodb
//显示启动成功即可通过 http://localhost:27017/ 访问

4. 服务名无效的解决方法

  • 将data目录删除
  • 管理员模式打开cmd,进入mongodb的bin目录
  • 再次安装服务
mongod --config "//此处为第一步配置的mongo.conf文件的路径 + mongo.conf"  --install --serviceName "mongodb"
  • 再次启动服务
net start mongodb 

jest单元测试

为什么需要单元测试

老实说在之前我一直觉得测试就是点点点,通过重复繁琐的操作的查找bug的。平日里开发也少不了自测的环节,大部分时候通过点击的操作难以覆盖所有的情景和环节。而且确实实在繁琐,令人生厌,所以也想好好学习使用单元测试来解决这个问题,毕竟很多时候我们更愿意相信数据。人在主观上太容易陷入“应该没问题”这个误区,但是没有任何人能保证程序不出错误,我们只能通过测试来减少错误发生的概率。

安装及配置

框架使用的vue,所以用到了Vue Test Utils。现在主流的js测试运行期它都支持,于是我选用了jest来学习和使用。

  1. 我们要做的第一件事就是安装 Jest 和 Vue Test Utils
$ npm install --save-dev jest @vue/test-utils
  1. 我们需要在 package.json 中定义一个单元测试的脚本。
// package.json
{
  "scripts": {
    "test": "jest"
  }
}
  1. Jest 处理 *.vue 文件,需要安装和配置 vue-jest 预处理器
 // ...
  "jest": {
    "moduleFileExtensions": [
      "js",
      "json",
      // 告诉 Jest 处理 `*.vue` 文件
      "vue"
    ],
    "transform": {
      // 用 `vue-jest` 处理 `*.vue` 文件
      ".*\\.(vue)$": "vue-jest"
    }
  }
  1. 安装babel,处理es模块等高级的特性

babel新版本的名称有所不同,要注意区别。babel7以上的版本需要安装 ```` babel-core@^7.0.0-bridge.0 babel-jest```进行兼容

npm i - D @babel/core @babel/preset-env babel-core@^7.0.0-bridge.0 babel-jest
  1. 配置babel

创建.babelrc文件。在package.json添加jest配置文件,来告诉 Jest 用 babel-jest 处理 JavaScript 测试文件

//.babelrc
{
    "presets": [["@babel/preset-env", { "modules": false }]],
    "env": {
      "test": {
        "presets": [["@babel/preset-env", { "targets": { "node": "current" } }]]
      }
    }
  }


//package.json
{
  // ...
  "jest": {
    // ...
    "transform": {
      // ...
      // 用 `babel-jest` 处理 js
      "^.+\\.js$": "<rootDir>/node_modules/babel-jest"
    }
    // ...
  }
}

入门

  1. 创建test目录;Jest 将会递归的找到整个工程里所有 .spec.js.test.js扩展名的文件

  2. 新建一个测试组件

<template>
  <div>
    {{ count }}
    <button @click="increment">自增</button>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        count: 0
      }
    },

    methods: {
      increment() {
        this.count++
      }
    }
  }
</script>
  1. 新建一个测试用例
import Vue from 'vue'
import { shallowMount } from '@vue/test-utils'
import Counter from '../src/Counter.vue'

describe('Counter.vue', () => {
  it('increments count when button is clicked', async () => {
    const wrapper = shallowMount(Counter)
    wrapper.find('button').trigger('click')
    await Vue.nextTick()
    expect(wrapper.find('div').text()).toMatch('1')
  })
})

  1. 运行测试
npm run test

自定义小程序顶部导航条

这个自定义导航栏其实也就是一个自定义组件

然后在app.json全局引入该组件供项目使用

1. app.json 声明导航条自定义

这个时候小程序顶部只剩下右侧的胶囊菜单

 "window": {
    "navigationStyle":"custom"
  },

2. 几个概念

  • 胶囊菜单
    为小程序自带的菜单按钮,设为自定义导航条依旧存在
  • 顶部状态栏
    显示设备信息的状态栏,例如wifi、电量、时间等数据
  • 固定布局
    自定义导航条需要使用position:fixed固定在顶部,不使用该属性的话,移动端下拉导航条会移动;使用该属性的话,导航条下方的内容会被覆盖一部分,在这儿以固定布局为例子。
  • 导航条
    在这儿我把导航条理解为包含两个胶囊,标题的这一行。与上头的状态栏区分开来。

3. 需求

假设需求需要在导航条左侧放置一个 提供返回上一级页面 及 返回首页的胶囊按钮。

1) 实现左右两侧胶囊及标题的对齐
2)高度适配各个尺寸的移动端设备
3)提供更加灵活多变的返回操作

4. 思路

通过小程序的api获取胶囊及状态栏的位置信息,计算自定义导航条的高度及边距

5. 疑问

当导航条设为固定布局时,如何防止覆盖下方的内容?

这里要理解为什么会覆盖,由于position:fixed;top:0时,导航条的位置是脱离了文档流固定在屏幕窗口的最顶部。但是原本导航条下的内容会根据文档流渲染,因此覆盖了一部分在导航条之下。

解决方案也很简单,通过设置一个底部边距将下方内容撑开,这个边距为状态栏的高度和导航条的高度及上下边距即可。

6. 计算

通过微信的api获取胶囊和状态栏的位置信息

//获取右侧胶囊的的位置信息
 const capsulePosition = wx.getMenuButtonBoundingClientRect()

//获取设备信息
wx.getSystemInfo({
  success: (res) => {
    const {
      statusBarHeight,//顶部状态栏的高度
      windowWidth //屏幕宽度
    } = res

//存储到全局变量中方便使用
    this.globalData.navBarInfo = {
      height: capsulePosition.height //导航条的高度
      paddingTop: capsulePosition.top,//导航条的上边距
      paddingLeft: windowWidth - capsulePosition.right, //导航条的左边距
      paddingBottom: capsulePosition.top - statusBarHeight,//导航条的下边距,使用胶囊的上边距减去状态栏高度可得
    }
  },
})

布局 (navBar组件)

计算整个头部的高度,用于作为导航条的下边距撑开被覆盖的内容

//navBar.js
const app = getApp()
const { navBarInfo  } = app.globalData
//整个头部的高度 = 导航条的高度+上边距+下边距
const topHeight = navBarInfo.height + navBarInfo.paddingTop + navBarInfo.paddingBottom
 

讨厌的回调

场景,小程序的api大多数是异步采用回调函数的方式来处理成功或者失败的结果

假如你的项目要实现一个用户登录功能

  1. 调用wx.login()
    这个api获取用户临时凭证code
  2. 调用服务端的方法,假设有一个getOpenid()的函数
    服务端会根据这个code,小程序appid和密钥,请求对应的小程序凭证验证的api,返回用户的openid和seesionkey,再将这个结果返回给小程序保存起来。
  3. 调用服务端获取自身系统用户中心的方法,假设有一个getUserInfo()的函数

服务端根据小程序传过去的openid,在自己的用户中心进行用户的注册或检索,其实可以和第二步合在一起处理。具体还是根据公司业务来。

可以看到,要获取用户信息就需要调用三个api,而微信的api全是回调!

再假如我们获取用户信息后还要保存到本地,在调用wx.login时还需要检查登录态是否过期,防止重复调用产生不同的seesion_key导致某些接口报 session_key无效的错误。这就又多了两个回调函数,不管是开发还是后期维护都非常的不方便。

 wx.login({
            timeout:10000,
            success: (res) => {
                wx.request({
                    url: '/api/getOpenid',
                    data: {code: res.code},
                    header: {'content-type':'application/json'},
                    method: 'POST',
                    dataType: 'json',
                    responseType: 'text',
                    success: (result) => {
                        wx.request({
                            url: '/api/getUserInfo',
                            data: {},
                            header: {'content-type':'application/json'},
                            method: 'GET',
                            dataType: 'json',
                            responseType: 'text',
                            success: (res) => {
                                wx.setStorage({
                                    key: 'userInfo',
                                    data: res.data,
                                    success: (result) => {
                                        
                                    },
                                    fail: () => {},
                                    complete: () => {}
                                });
                                  
                            },
                            fail: () => {},
                            complete: () => {}
                        });
                          
                    },
                    fail: () => {},
                    complete: () => {}
                });
                  
            },
            fail: () => {},
            complete: () => {}
        });


包装成promise

小程序开发者工具内置es6转es5,所以使用es6的新特性完全没问题,我们可以利用promise来解决回调函数的问题

  /**
   * 传入小程序的api,转成promise
   * 第一版,传入整个小程序的api函数
   * @param {Function} fn 小程序的api 
   * @return {Function} 返回的promise函数
   */
  promisify: (fn) => {
    return (args = {}) => {
      return new Promise((resovled, rejected) => {
        args.success = res => {
          resovled(res)
        }
        args.fail = err => {
          rejected(err)
        }
        fn(args)
      })
    }
  },

通过把需要转换的小程序api传入promisify函数,返回一个promise对象

//使用
const wxLogin = promisify(wx.login)
const wxRequest = promisify(wx.request)
const wxSetStorage = promisify(wx.setStorage)
wxLogin()
  .then(res => {
    return wxRequest({
      url: "/api/getOpenid",
      data: { code: res.code },
      header: { 'content-type': 'application/json' },
      method: 'GET',
      dataType: 'json',
      responseType: 'text',
    })
  })
  .then(res => {
    return wxRequest({
      url: "/api/getUserInfo",
      data: { openid: res.openid },
      header: { 'content-type': 'application/json' },
      method: 'GET',
      dataType: 'json',
      responseType: 'text',
    })
  })
  .then(res => {
    return wxSetStorage({
      key: 'userInfo',
      data: res.data,
    })
  })
  .then(res=>{
    //弹窗提醒 or 做啥都行
  })

这样处理之后代码的逻辑和结构是不是清晰了很多,promise真香!

代理wx这个对象的api为promise

虽然上方的处理解决了回调函数的问题,但是每次都需要调用这个函数来包装小程序的api,比较麻烦

这里就需要使用es6的proxy,也就是vue3版本代理object.defindproperty实现数据监听的新方法。
其实整个proxy人如其名,就是代理的意思,不过他比劫持数据对象的get和set更加强大,不仅仅支持对象,还能
代理数组等。欸,小程序的api不都是定义在wx这个对象里面嘛,是不是突然有了思路

通过代理wx这个对象在使用其对应的api时,包上一层promisify函数,转成promsie对象,我们不就可以实现 和上面一样的功能,甚至更加优雅。

  /**
 * 将小程序的api代理成promise形式第二版
 * @return {Object} 返回一个proxy函数,代理wx这个对象
 * 然后当这个代理对象使用小程序api时,会把原生的api转成promise对象
 * @example const WX = proxy() 
 * WX.showToast({title:'666'}).then(res=>WX.showLoading())
 */
  proxy: () => {
    function promisify(fn) {
      return (args = {}) => {
        return new Promise((resovled, rejected) => {
          wx[fn]({
            ...args,
            success: resovled,
            fail: rejected,
          })
        })
      }
    }
    return new Proxy(wx, {
      get: (target, prop) => {
        return promisify(prop)
      }
    })

  }

由于小程序没有全局变量,例如window或者global之类的,我们只能退而求其次,在需要使用到的引入,可以把需
要这个proxy函数放到app.js中方便引用

//使用
const app = getApp()

const WX = app.proxy()
WX.login()
  .then(res => {
    return WX.request({
      url: "/api/getOpenid",
      data: { code: res.code },
      header: { 'content-type': 'application/json' },
      method: 'GET',
      dataType: 'json',
      responseType: 'text',
    })
  })
  .then(res => {
    return WX.request({
      url: "/api/getUserInfo",
      data: { openid: res.openid },
      header: { 'content-type': 'application/json' },
      method: 'GET',
      dataType: 'json',
      responseType: 'text',
    })
  })
  .then(res=>{
    return WX.setStorage({
      key: 'userInfo',
      data: res.data,
    })
  })
  .then(res=>{
    //弹窗提醒 or 做啥都行
  })

是不是更加优雅了呢!

使用 async await

虽然将api转换成promise的形式解决了回调嵌套过深导致的可读性可维护性都很差的问题,但是promise链式调用导致的调试困难还是难以避免,这里由于把wx这个对象的api的回调转换成了promise,我们可以使用async和await来处理,写法更加优雅,符合开发者的阅读开发习惯。

async function handleLogin() {
    const app = getApp()
    const WX = app.proxy()
    const res = await WX.login()

    const loginIngo = await WX.request({
        url: "/api/getOpenid",
        data: { code: res.code },
        header: { 'content-type': 'application/json' },
        method: 'GET',
        dataType: 'json',
        responseType: 'text',
    })

    const userInfo = await WX.request({
        url: "/api/getUserInfo",
        data: { openid: loginIngo.openid },
        header: { 'content-type': 'application/json' },
        method: 'GET',
        dataType: 'json',
        responseType: 'text',
    })

    await WX.setStorage({
        key: 'userInfo',
        data: userInfo,
    })

}

使用node开发restful风格api

为啥要选用node

  1. 不需要学习额外的语言特性;
  2. 扩展网络和数据库的知识面;
  3. 一个字,快!
    安装环境快,一键式无脑安装,不用担心各种繁琐配置;
    启动服务快,只需要短短几行代码就能生成一个web服务器;
    上手更快。

思路

通过下述步骤对使用node开发web服务有一个大概的认识

  1. 安装数据库,这里选用mongodb;
  2. 定义数据模式,即数据的结构和行为;
  3. 定义数据模型,对数据库进行操作需要通过该模型;
  4. 定义路由,在该路由路径被访问时,触发对应的函数,进行数据库操作。

其实很简单,就三步。通过一些代码层面的优化,甚至只需要我们实现第二步定义好我们所需要的数据结构之后就能生成restful风格的api。

restful风格的api?

宁静的数据接口?什么鬼?

  • 先说api,其实是一个很广的命题。这里我们理解的api是打通前后端的交互的数据接口。而在有一些语义下,它可以是你封装的一个工具函数方法。

  • restful风格我们可以这么理解

  1. 设计路径:在第2、3步定义好数据结构后,通过路径去访问该数据资源。
  2. 定义http动词:访问资源后,我们需要什么样的操作
    我们耳熟能详的增删改查其实就对应着 post、delete、put、get这四个http动词。
    例如:get /api/user/ => 查看/api/user这个路径下所有用户信息的资源

尝试开发

其实开发web服务,大部分操作就是围绕着操作数据库,而操作数据库肯定有额外的学习成本。毕竟这是很多前端er没接触过的。

就我个人而言,学习一个新的知识点,查阅官方文档无疑是最好的办法。

假设已经安装完了mongodb,我们要操作数据库还需要另一个东西:mongoose,翻译过来是猫鼬,又是一个不知道什么鸟东西,在这儿可以简单的理解,通过它可以更加方便的操作数据库。

其实官方的例子特别的好,跟着猫鼬一起云养猫
还记得前面将的思路和步骤嘛,首先我们要使用node的一个框架express生成项目模板

  1. 设计路径:在routes文件夹创建一个cat.js文件,这个就是我们要访问的资源;
  2. 定义数据模型:定义数据结构、生成数据模型;
  3. 定义路由:使用express框架的路由功能,根据http动词触发对应函数操作数据库;
const express = require('express');
const router = express.Router();
const mongoose = require("mongoose")

//猫的模式:假设每一只猫都有名字和颜色的属性。
const catSchema = mongoose.Schema({
  name: String,
  color:String
})
//猫的模型:通过传入猫的模式,我们有了一个模型,然后就可以根据该模型生成好多猫了!
const CatModel = mongoose.model('Cat', catSchema)

router.get('/cat', function (req, res, next) {
  //后来猫越来越多,就可以通过模型来查找小猫
  CatModel.find((err, allCats) => res.json(allCats))
});

router.post('/cat', ((req, res, next) => {
  //假设我们要增加一只小猫,猫咪的信息通过post请求的请求体传输到服务端。
  const catInfo = req.body //假如请求体内容为{name:'小白',color:'黑色'}
  //根据猫的模型生成的小猫就有了  名字为小白,颜色为黑色的属性。
  const lititleCat = new CatModel(catInfo)
  //小猫在调用save方法后保存到数据库
  lititleCat.save((err, saved) => res.json(saved))
}))

router.delete('/cat', ((req, res, next) => {
  const { id } = req.body
  //根据传入的猫的id,删除该猫的信息
  CatModel.findByIdAndRemove(id, (err, removed) => res.json(removed))
}))

router.put('/cat', ((req, res, next) => {
  const { id } = req.body
  //根据传入的猫的id,更新猫的信息
  CatModel.findByIdAndUpdate(id, { ...req.body }, { new: true }, (err, updated) => res.json(updated))
}))

这个时候启动服务,其实未生效,这是因为我们写了一大堆,并没有触发,需要在入口文件app.js引入我们的路由文件。

//引入猫的路由
const catRouter = require('./routes/cat');
//app就是我们的服务器,当服务器资源路径 /api被调用时,就会映射到我们猫的路由
app.use('/api', catRouter);
// localhoset:3000/api/cat  即是我们要请求猫的资源的 路径,最后通过http动词,触发对应的数据库操作,我们就生成了第一个符合restful风格的api。

进阶

  1. 假设我们要将猫的信息和它的饲养员的信息绑定在一起,就需要联表
  2. mongoose的语法充斥着各种回调,就需要async await来优化

接下来修改一下上面生成猫的例子

const express = require('express');
const router = express.Router();
const mongoose = require("mongoose")
const mongoose = require('mongoose')

+ const feederSchema = mongoose.Schema({
+  feederName: String,
+  cat: {
+    type: mongoose.Schema.Types.ObjectId,//联表查询必须这样的格式来存储对应表的_id
+    ref: 'Cat'//联表关系的表名,注意是生成模型的类,与模型名区分开
+  }
+ })

//猫的模式:假设每一只猫都有名字和颜色的属性。
const catSchema = mongoose.Schema({
  name: String,
  color:String
})

+ const feederModel = mongoose.model("Feeder", feederSchema)
//猫的模型:通过传入猫的模式,我们有了一个模型,然后就可以根据该模型生成好多猫了!
const CatModel = mongoose.model('Cat', catSchema)


router.get('/cat',  (req, res, next)=> {
  //后来猫越来越多,就可以通过模型来查找小猫
  CatModel.find((err, allCats) => res.json(allCats))
});

router.post('/cat', async((req, res, next) => {
   //假设我们要增加一只小猫,猫咪的信息通过post请求的请求体传输到服务端。
+  const { name, color, feederName } = req.body //假如请求体内容为{name:'小白',color:'黑色',feederName:"张三"}
+  const catInfo = { name, color } //猫的信息
+  const feederInfo = { feederName }  //饲养员信息
   //根据猫的模型生成的小猫就有了  名字为小白,颜色为黑色的属性。
   const lititleCat = new CatModel(catInfo)
   //小猫在调用save方法后保存到数据库
+  const { _id } = await lititleCat.save()
   //将小猫的id与用户的猫关联起来
+  const newFeeder = new feederModel({ ...feederInfo, cat: _id })
   //保存用户信息
+  await newFeeder.save()
   //通过populate()可以查询联表的属性,exec()可以更好的追踪堆栈,让查询的函数返回的是一个完整的promise对象
+  const data = await feederModel.find().populate("cat").exec()
+  res.json(data)
  
}))

router.delete('/cat', (req, res, next) => {
  const { id } = req.body
  //根据传入的猫的id,删除该猫的信息
  CatModel.findByIdAndRemove(id, (err, removed) => res.json(removed))
})

router.put('/cat', (req, res, next) => {
  const { id } = req.body
  //根据传入的猫的id,更新猫的信息
  CatModel.findByIdAndUpdate(id, { ...req.body }, { new: true }, (err, updated) => res.json(updated))
})

可以下载postman测试一下代码

优化

当你使用postman测试了几次代码之后你会发现每次post请求都新增重复数据,哪怕内容是没有变化的,mongoose会为保存时的数据添加唯一的_id标识。

按照正常的逻辑应该是

  1. 当没有这条数据时,插入一个新数据;
  2. 当数据存在时,根据新的内容更新它;
  3. 而判断该数据是否存在,我们需要一个作为唯一标识的查询条件

所以我们可以使用findOneAndUpdate这个api插入或更新我们的数据,修改post请求的代码

router.post('/cat', async((req, res, next) => {
  //假设我们要增加一只小猫,猫咪的信息通过post请求的请求体传输到服务端。
  const { name, color, feederName } = req.body //假如请求体内容为{name:'小白',color:'黑色',feederName:"张三"}
  const catInfo = { name, color } //猫的信息
  const feederInfo = { feederName }  //饲养员信息
  //这里根据猫的名称作为插入或更新的查询标识
+ const {_id} = await CatModel.findOneAndUpdate({name},catInfo,{ upsert: true, new: true, setDefaultsOnInsert: true }).exec()
  //这里根据饲养员的名称作为插入或更新的查询标识
+ await feederModel.findOneAndUpdate({feederName},{...feederInfo,cat:_id},{ upsert: true, new: true, setDefaultsOnInsert: true })
  //通过populate()可以查询联表的属性,exec()可以更好的追踪堆栈,让查询的函数返回的是一个完整的promise对象
  const data = await feederModel.find().populate("cat").exec()
  res.json(data)
 
}))

总结

当然,这些都是node最基础的操作,但是这些特别简单的东西,扩展了很多知识,通过断点调试你可以更加深刻地了解到http请求通信过程中的数据传输、数据库模型的操作等等,甚至你还会开始思考如何使用node做更多的东西。
例如:
快速的接口mock,在拟定好协议之后使用node开发数据接口,提高接口联调的效率;
甚至搭一个可视化的mock数据的平台,通过可编辑表格操作去生成数据接口;
又或者你可以新建一个自己的网站,使用node搭建自己的服务器等等等。

最后贴上自己的github地址:
https://github.com/kangjs7854/node-server-template
臭不要脸的求个star,万分感谢~

小程序授权那些事

小程序提供了非常丰富的api供开发者使用,部分接口需要经过用户授权同意才能调用

大部分的授权可以通过调用 wx.authorize 发起弹窗申请,部分授权只能通过用户点击来实现
例如:

  • 获取用户信息的授权需要通过button这个原生组件的点击事件的回调来触发
  • 获取用户手机号的权限则更加严格,不仅仅不需要使用原生button组件的点击事件回调,还只支持认证后的小程序
  • 获取后台位置信息的权限需要用户主动到设置页面,将“位置信息”选项设置为“使用小程序期间和离开小程序后”。
  • 涉及位置信息的授权还需要在app.json配置”permission“及”requiredBackgroundModes“

授权流程

  1. 在授权之前我们应该检查是否已经授权, 调用wx.getSetting 获取用户当前的授权状态。
  2. 若未授权调用wx.authorize进行授权
  3. 这里注意授权失败或被拒绝的情况下,调用wx.authorize会直接进入失败的回调,导致授权弹窗无法出现;这里可以使用对话框引导用户点击进入设置页进行授权;在需要后台位置的授权时就可以采用该方式,记得在app.json进行允许后台位置信息的配置。
  4. 可能有一些app.json的配置
  5. 由于小程序的限制,获取用户信息和手机号之类的授权,只能通过点击button组件触发,开发者可以自定义一个弹窗,在弹窗里加入原生的button组件,引导用户点击进行授权

封装一个授权的函数

使用到了之前把小程序api转换成promise的代理

async authorize(permissions) {
   const WX = app.proxy()
    try {
        const res = await WX.getSetting()
        const isAuth = res.authSetting['scope[permissions]']
        if (isAuth) return
        const authorizeInfo = await WX.authorize({ scope: 'scope[permissions]' })
        console.log(authorizeInfo);
    } catch (error) {
        //授权失败则引导至设置页面手动打开
        console.log(error);
        const modelInfo = await WX.showModal({
            title: '提示',
            content: '小程序需要您开启授权,请打开设置选项主动进行授权',
        })
        if (!modelInfo.confirm) return
        await WX.openSetting()
    }
},

配置

位置信息所需要的配置

//app.json
 "permission": {
    "scope.userLocation": {
      "desc": "你的位置信息将用于小程序定位"
    }
  },
  "requiredBackgroundModes": [
    "location"
  ]

自定义弹窗组件

加入原生button组件,完成授权操作

核心代码在 <button open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber">
通过设置对应的类型和回调获取授权

<view class="confirm-dialog" wx:if="{{isShowConfirm}}">
        <view class="inner">
            <text>我们需要您的手机号授权信息</text>
            <view class="confirm-footer">
                <view class="btn" bindtap="handleCancel">
                    拒绝
                </view>
                <button open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber">
                    确认
                </button>
            </view>
        </view>
    </view>

如何让vue中的watch停止观察

如何让vue中的watch停止观察

遇到过一道面试题,如何让vue中的watch停止观察?

第一次听到的时候楞了几秒,脑子里搜索那些看过的博客文章文档,好像是第一次遇到这样奇怪的题目。
为了不冷场就从vue双向数据绑定说起,说完面试官表示很不满意,随口说了一句:数据绑定的时候不是用get,set劫持了数据对象嘛,再把这个对象设为null,这个watch就不就失效了。

当时的我觉得哇好有道理,点到哈腰,“哇,原来如此,谢谢面试官指点,现在如同醍醐灌顶”。现在仔细一想我好像被他忽悠了,都设置为null,这个对象都不存在了啊,我又百度了一番,好像没有找到类似的题目,大家搜的都是“为什么我的watch失效了,为什么不能监听数据对象了?”之类的。

额...答案好像已经出来了,不就是网友百度的那些问题嘛?感觉自己好傻,一是没get到面试官的点,可能他一开始就是想考察一下watch的注意事项才这么提问。二是在我吹了一波双向数据绑定后他突发奇想回答了那个奇淫技巧,整的我云里雾里。

了解watch

  • 一般情况下我们定义一个watch对象监听数据
export default {
    data() {
        return {
            count: 1      
        }
    },
    watch: {
        count(newVal) {
            console.log('count 新值:'+newVal)
        }
    }
}
  • 其实还可以用函数式定义watch
export default {
    data() {
        return {
            count: 1      
        }
    },
    created() {
        this.$watch('count', function(){
            console.log('count 新值:'+newVal)
        })
    }
}

让watch失效

  1. $watch会返回一个取消观察的函数,通过调用它,我们可以取消watch观察
let unwatchFn = this.$watch('count', function(){
    console.log('count 新值:'+newVal)
})
this.count = 2 // log: count 新值:2
unwatchFn()
this.count = 3 // 没有打印
  1. 箭头函数定义watch

箭头函数会在预编译阶段绑定父级作用域的this,普通函数的this则是指向函数运行时所在的对象
因为js代码分为预编译和执行阶段,在预编译时遇到函数声明会提前进行解析,由于 let vm = new Vue({...}) 这句代码在预编译时还未执行,此时箭头函数绑定的this没有指向vue的实例,所以watch失效

3.由于Object.defineProperties的限制,在监听深层次对象时不添加deep属性watch会失效

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.