- 开发者工具那些你未必知道的console命令
build 编译 相当于点击“编译”按钮 [图片] preview 预览 [图片] upload 上传代码 [图片] cleanAppCache 清除应用缓存 清除完需要重新编译 [图片] showRequestInfo 查看请求过链接的信息 检查证书一大利器 [图片] showSystemInfo 显示当前开发者工具占用内存及其他信息 [图片] checkProxy 检测目标网址是否启用代理 [图片] openToolsLog 打开开发者工具日志目录 openPlugin 打开插件目录 openVendor 打开供应商目录 更多详细的命令使用 help 进行获取 [图片]
2019-11-08 - 小程序使用防抖函数的简单方法
废话不多说,上代码: Page构造器内部使用,不需要使用外部模块。 [代码]onLoad: function (options) { console.log(options); this.debounce = this.debounce();// 防抖函数,在此处初始化 // 若不初始化,函数主体不执行 } // debounce函数,就是事件触发的函数,名字可以随意取名 debounce : function () { var timeOut = null; return () => { clearTimeout(timeOut); timeOut = setTimeout(() => { // 事件函数中要执行的代码块 // 改写原函数异常方便、简洁 }, 300); } } [代码] 如果这个有问题,欢迎指点。
2019-11-12 - 开发者工具真机调试常见报错解答
以下整理了常见的真机调试控制台报错,以及建议的解决方法。 1、扫描二维码出现 “-50003” 的错误 出现此错误时,可以尝试在工具上登出开发者账户,切换开发者账号,重开项目再试一次。如果不能解决,可以在此贴评论,我们会后台排查原因。 2、控制台出现报错,第一条报错信息是 “Cannot read 'toLowerCase' of undefined” 这是一个底层模块的导致的问题,部分机器会出现。由于出现频率较低,此问题仍在排查修复中。如果出现此错误,可以尝试退出所有项目、关闭工具,然后重新启动电脑。如果不能解决,可以尝试先将工具手动完全卸载,重新启动,然后再安装最新的版本。 3、控制台出现报错,第一条报错信息是 “define is not defined” 这是微信客户端上的某个新改动引起的。最新的 RC、Nightly 工具已经修复了这个问题,可以尝试下载安装再试一次: Nightly 版本: https://developers.weixin.qq.com/miniprogram/dev/devtools/nightly.html RC 版本: https://developers.weixin.qq.com/miniprogram/dev/devtools/rc.html 4、控制台出现报错,第一条报错信息是 “Cannot assign to readonly property xxx of xxx #.<object>” 这是微信客户端上的某个新改动引起的。最新的 RC、Nightly 工具已经修复了这个问题,可以尝试下载安装再试一次: Nightly 版本: https://developers.weixin.qq.com/miniprogram/dev/devtools/nightly.html RC 版本: https://developers.weixin.qq.com/miniprogram/dev/devtools/rc.html 5、控制台出现报错 “Async stack traces in debugger are not available on 32bit platforms” 如果操作系统是 64 位的,请尝试下载安装 64 位的工具。如果操作系统是 32 位的,可以忽略这个报错。 6、控制台报错 “TimeoutOverflowWarning” 请排查项目代码是否存在 setTimeout 的时间参数存在类似 Infinity 等很大的数字。 7、右侧面板出现警告 “Error sync-0” 的问题 如果不影响小程序的加载,可以忽略此问题。如果小程序无法加载,请确认将微信客户端和工具都升级到了最新的版本。 如果开发者有其他问题,可以在此提出。
2019-07-11 - 一个通用request的封装
小程序内置了[代码]wx.request[代码],用于向后端发送请求,我们先来看看它的文档: wx.request(OBJECT) 发起网络请求。使用前请先阅读说明。 OBJECT参数说明: 参数名 类型 必填 默认值 说明 最低版本 url String 是 - 开发者服务器接口地址 - data Object/String/ArrayBuffer 否 - 请求的参数 - header Object 否 - 设置请求的 header,header 中不能设置 Referer。 - method String 否 GET (需大写)有效值:OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT - dataType String 否 json 如果设为json,会尝试对返回的数据做一次 JSON.parse - responseType String 否 text 设置响应的数据类型。合法值:text、arraybuffer 1.7.0 success Function 否 - 收到开发者服务成功返回的回调函数 - fail Function 否 - 接口调用失败的回调函数 - complete Function 否 - 接口调用结束的回调函数(调用成功、失败都会执行) - success返回参数说明: 参数 类型 说明 最低版本 data Object/String/ArrayBuffer 开发者服务器返回的数据 - statusCode Number 开发者服务器返回的 HTTP 状态码 - header Object 开发者服务器返回的 HTTP Response Header 1.2.0 这里我们主要看两点: 回调函数:success、fail、complete; success的返回参数:data、statusCode、header。 相对于通过入参传回调函数的方式,我更喜欢promise的链式,这是我期望的第一个点;success的返回参数,在实际开发过程中,我只关心data部分,这里可以做一下处理,这是第二点。 promisify 小程序默认支持promise,所以这一点改造还是很简单的: [代码]/** * promise请求 * 参数:参考wx.request * 返回值:[promise]res */ function requestP(options = {}) { const { success, fail, } = options; return new Promise((res, rej) => { wx.request(Object.assign( {}, options, { success: res, fail: rej, }, )); }); } [代码] 这样一来我们就可以使用这个函数来代替wx.request,并且愉快地使用promise链式: [代码]requestP({ url: '/api', data: { name: 'Jack' } }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); [代码] 注意,小程序的promise并没有实现finally,Promise.prototype.finally是undefined,所以complete不能用finally代替。 精简返回值 精简返回值也是很简单的事情,第一直觉是,当请求返回并丢给我一大堆数据时,我直接resolve我要的那一部分数据就好了嘛: [代码]return new Promise((res, rej) => { wx.request(Object.assign( {}, options, { success(r) { res(r.data); // 这里只取data }, fail: rej, }, )); }); [代码] but!这里需要注意,我们仅仅取data部分,这时候默认所有success都是成功的,其实不然,wx.request是一个基础的api,fail只发生在系统和网络层面的失败情况,比如网络丢包、域名解析失败等等,而类似404、500之类的接口状态,依旧是调用success,并体现在[代码]statusCode[代码]上。 从业务上讲,我只想处理json的内容,并对json当中的相关状态进行处理;如果一个接口返回的不是约定好的json,而是类似404、500之类的接口异常,我统一当成接口/网络错误来处理,就像jquery的ajax那样。 也就是说,如果我不对[代码]statusCode[代码]进行区分,那么包括404、500在内的所有请求结果都会走[代码]requestP().then[代码],而不是[代码]requestP().catch[代码]。这显然不是我们熟悉的使用方式。 于是我从jquery的ajax那里抄来了一段代码。。。 [代码]/** * 判断请求状态是否成功 * 参数:http状态码 * 返回值:[Boolen] */ function isHttpSuccess(status) { return status >= 200 && status < 300 || status === 304; } [代码] [代码]isHttpSuccess[代码]用来决定一个http状态码是否判为成功,于是结合[代码]requestP[代码],我们可以这么来用: [代码]return new Promise((res, rej) => { wx.request(Object.assign( {}, options, { success(r) { const isSuccess = isHttpSuccess(r.statusCode); if (isSuccess) { // 成功的请求状态 res(r.data); } else { rej({ msg: `网络错误:${r.statusCode}`, detail: r }); } }, fail: rej, }, )); }); [代码] 这样我们就可以直接resolve返回结果中的data,而对于非成功的http状态码,我们则直接reject一个自定义的error对象,这样就是我们所熟悉的ajax用法了。 登录 我们经常需要识别发起请求的当前用户,在web中这通常是通过请求中携带的cookie实现的,而且对于前端开发者是无感知的;小程序中没有cookie,所以需要主动地去补充相关信息。 首先要做的是:登录。 通过[代码]wx.login[代码]接口我们可以得到一个[代码]code[代码],调用后端登录接口将code传给后端,后端再用code去调用微信的登录接口,换取[代码]sessionKey[代码],最后生成一个[代码]sessionId[代码]返回给前端,这就完成了登录。 [图片] 具体参考微信官方文档:wx.login [代码]const apiUrl = 'https://jack-lo.github.io'; let sessionId = ''; /** * 登录 * 参数:undefined * 返回值:[promise]res */ function login() { return new Promise((res, rej) => { // 微信登录 wx.login({ success(r1) { if (r1.code) { // 获取sessionId requestP({ url: `${apiUrl}/api/login`, data: { code: r1.code, }, method: 'POST' }) .then((r2) => { if (r2.rcode === 0) { const { sessionId } = r2.data; // 保存sessionId sessionId = sessionId; res(r2); } else { rej({ msg: '获取sessionId失败', detail: r2 }); } }) .catch((err) => { rej(err); }); } else { rej({ msg: '获取code失败', detail: r1 }); } }, fail: rej, }); }); } [代码] 好的,我们做好了登录并且顺利获取到了sessionId,接下来是考虑怎么把sessionId通过请求带上去。 sessionId 为了将状态与数据区分开来,我们决定不通过data,而是通过header的方式来携带sessionId,我们对原本的requestP稍稍进行修改,使得它每次调用都自动在header里携带sessionId: [代码]function requestP(options = {}) { const { success, fail, } = options; // 统一注入约定的header let header = Object.assign({ sessionId: sessionId }, options.header); return new Promise((res, rej) => { ... }); } [代码] 好的,现在请求会自动带上sessionId了; 但是,革命尚未完成: 我们什么时候去登录呢?或者说,我们什么时候去获取sessionId? 假如还没登录就发起请求了怎么办呢? 登录过期了怎么办呢? 我设想有这样一个逻辑: 当我发起一个请求的时候,如果这个请求不需要sessionId,则直接发出; 如果这个请求需要携带sessionId,就去检查现在是否有sessionId,有的话直接携带,发起请求; 如果没有,自动去走登录的流程,登录成功,拿到sessionId,再去发送这个请求; 如果有,但是最后请求返回结果是sessionId过期了,那么程序自动走登录的流程,然后再发起一遍。 其实上面的那么多逻辑,中心思想只有一个:都是为了拿到sessionId! 我们需要对请求做一层更高级的封装。 首先我们需要一个函数专门去获取sessionId,它将解决上面提到的2、3点: [代码]/** * 获取sessionId * 参数:undefined * 返回值:[promise]sessionId */ function getSessionId() { return new Promise((res, rej) => { // 本地sessionId缺失,重新登录 if (!sessionId) { login() .then((r1) => { res(r1.data.sessionId); }) .catch(rej); } } else { res(sessionId); } }); } [代码] 好的,接下来我们解决第1、4点,我们先假定:sessionId过期的时候,接口会返回[代码]code=401[代码]。 整合了getSessionId,得到一个更高级的request方法: [代码]/** * ajax高级封装 * 参数:[Object]option = {},参考wx.request; * [Boolen]keepLogin = false * 返回值:[promise]res */ function request(options = {}, keepLogin = true) { if (keepLogin) { return new Promise((res, rej) => { getSessionId() .then((r1) => { // 获取sessionId成功之后,发起请求 requestP(options) .then((r2) => { if (r2.rcode === 401) { // 登录状态无效,则重新走一遍登录流程 // 销毁本地已失效的sessionId sessionId = ''; getSessionId() .then((r3) => { requestP(options) .then(res) .catch(rej); }); } else { res(r2); } }) .catch(rej); }) .catch(rej); }); } else { // 不需要sessionId,直接发起请求 return requestP(options); } } [代码] 留意req的第二参数keepLogin,是为了适配有些接口不需要sessionId,但因为我的业务里大部分接口都需要登录状态,所以我默认值为true。 这差不多就是我们封装request的最终形态了。 并发处理 这里其实我们还需要考虑一个问题,那就是并发。 试想一下,当我们的小程序刚打开的时候,假设页面会同时发出5个请求,而此时没有sessionId,那么,这5个请求按照上面的逻辑,都会先去调用login去登录,于是乎,我们就会发现,登录接口被同步调用了5次!并且后面的调用将导致前面的登录返回的sessionId过期~ 这bug是很严重的,理论上来说,登录我们只需要调用一次,然后一直到过期为止,我们都不需要再去登录一遍了。 ——那么也就是说,同一时间里的所有接口其实只需要登录一次就可以了。 ——也就是说,当有登录的请求发出的时候,其他那些也需要登录状态的接口,不需要再去走登录的流程,而是等待这次登录回来即可,他们共享一次登录操作就可以了! 解决这个问题,我们需要用到队列。 我们修改一下getSessionId这里的逻辑: [代码]const loginQueue = []; let isLoginning = false; /** * 获取sessionId * 参数:undefined * 返回值:[promise]sessionId */ function getSessionId() { return new Promise((res, rej) => { // 本地sessionId缺失,重新登录 if (!sessionId) { loginQueue.push({ res, rej }); if (!isLoginning) { isLoginning = true; login() .then((r1) => { isLoginning = false; while (loginQueue.length) { loginQueue.shift().res(r1); } }) .catch((err) => { isLoginning = false; while (loginQueue.length) { loginQueue.shift().rej(err); } }); } } else { res(sessionId); } }); } [代码] 使用了isLoginning这个变量来充当锁的角色,锁的目的就是当登录正在进行中的时候,告诉程序“我已经在登录了,你先把回调都加队列里去吧”,当登录结束之后,回来将锁解开,把回调全部执行并清空队列。 这样我们就解决了问题,同时提高了性能。 封装 在做完以上工作以后,我们都很清楚的封装结果就是[代码]request[代码],所以我们把request暴露出去就好了: [代码]function request() { ... } module.exports = request; [代码] 这般如此之后,我们使用起来就可以这样子: [代码]const request = require('request.js'); Page({ ready() { // 获取热门列表 request({ url: 'https://jack-lo.github.io/api/hotList', data: { page: 1 } }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); // 获取最新信息 request({ url: 'https://jack-lo.github.io/api/latestList', data: { page: 1 } }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); }, }); [代码] 是不是很方便,可以用promise的方式,又不必关心登录的问题。 然而可达鸭眉头一皱,发现事情并不简单,一个接口有可能在多个地方被多次调用,每次我们都去手写这么一串[代码]url[代码]参数,并不那么方便,有时候还不好找,并且容易出错。 如果能有个地方专门记录这些url就好了;如果每次调用接口,都能像调用一个函数那么简单就好了。 基于这个想法,我们还可以再做一层封装,我们可以把所有的后端接口,都封装成一个方法,调用接口就相对应调用这个方法: [代码]const apiUrl = 'https://jack-lo.github.io'; const req = { // 获取热门列表 getHotList(data) { const url = `${apiUrl}/api/hotList` return request({ url, data }); }, // 获取最新列表 getLatestList(data) { const url = `${apiUrl}/api/latestList` return request({ url, data }); } } module.exports = req; // 注意这里暴露的已经不是request,而是req [代码] 那么我们的调用方式就变成了: [代码]const req = require('request.js'); Page({ ready() { // 获取热门列表 req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); // 获取最新信息 req.getLatestList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); } }); [代码] 这样一来就方便了很多,而且有一个很大的好处,那就是当某个接口的地址需要统一修改的时候,我们只需要对[代码]request.js[代码]进行修改,其他调用的地方都不需要动了。 错误信息的提炼 最后的最后,我们再补充一个可轻可重的点,那就是错误信息的提炼。 当我们在封装这么一个[代码]req[代码]对象的时候,我们的promise曾经reject过很多的错误信息,这些错误信息有可能来自: [代码]wx.request[代码]的fail; 不符合[代码]isHttpSuccess[代码]的网络错误; getSessionId失败; … 等等的一切可能。 这就导致了我们在提炼错误信息的时候陷入困境,到底catch到的会是哪种[代码]error[代码]对象? 这么看你可能不觉得有问题,我们来看看下面的例子: [代码]req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); [代码] 假如上面的例子中,我想要的不仅仅是[代码]console.log(err)[代码],而是想将对应的错误信息弹窗出来,我应该怎么做? 我们只能将所有可能出现的错误都检查一遍: [代码]req.getHotList({ page: 1 }) .then((res) => { if (res.code !== 0) { // 后端接口报错格式 wx.showModal({ content: res.msg }); } }) .catch((err) => { let msg = '未知错误'; // 文本信息直接使用 if (typeof err === 'string') { msg = err; } // 小程序接口报错 if (err.errMsg) { msg = err.errMsg; } // 自定义接口的报错,比如网络错误 if (err.detail && err.detail.errMsg) { msg = err.detail.errMsg; } // 未知错误 wx.showModal({ content: msg }); }); [代码] 这就有点尴尬了,提炼错误信息的代码量都比业务还多几倍,而且还是每个接口调用都要写一遍~ 为了解决这个问题,我们需要封装一个方法来专门做提炼的工作: [代码]/** * 提炼错误信息 * 参数:err * 返回值:[string]errMsg */ function errPicker(err) { if (typeof err === 'string') { return err; } return err.msg || err.errMsg || (err.detail && err.detail.errMsg) || '未知错误'; } [代码] 那么过程会变成: [代码]req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { const msg = req.errPicker(err); // 未知错误 wx.showModal({ content: msg }); }); [代码] 好吧,我们再偷懒一下,把wx.showModal也省去了: [代码]/** * 错误弹窗 */ function showErr(err) { const msg = errPicker(err); console.log(err); wx.showModal({ showCancel: false, content: msg }); } [代码] 最后就变成了: [代码]req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch(req.showErr); [代码] 至此,一个简单的wx.request封装过程便完成了,封装过的[代码]req[代码]比起原来,使用上更加方便,扩展性和可维护性也更好。 结尾 以上内容其实是简化版的[代码]mp-req[代码],介绍了[代码]mp-req[代码]这一工具的实现初衷以及思路,使用[代码]mp-req[代码]来管理接口会更加的便捷,同时[代码]mp-req[代码]也提供了更加丰富的功能,比如插件机制、接口的缓存,以及接口分类等,欢迎大家关注mp-req了解更多内容。 以上最终代码可以在这里获取:req.js。
2020-08-04 - request封装
fetch.js [代码]const api = 'www.qq.com' export const Fetch = ({ url = '', data = {}, header = { "content-type": "application/json" }, method = 'GET', api = Api }) => { return new Promise((resolve, reject) => { wx.request({ url: api + url, header: header, method: method, data: data, success: res => { // 成功时的处理 if (res.data.error == 0) { resolve(res.data); } else { reject(res.data); } }, fail: err => { reject(err); } }) }) } [代码] api.js [代码]import { Fetch } from './fetch.js'; export const PostMiniList = data => { return Fetch({ url: '/post/post_mini_list.json', data: data, method: 'POST', header: { 'content-type': 'application/x-www-form-urlencoded' } }) } export const GetMiniList = data => { return Fetch({ url: '/get/get_mini_list.json', data: data }) } [代码] index.js [代码]import { PostMiniList, GetMiniList } from './api.js'; PostMiniList({ a:1, b:2 }).then( res => { // 成功处理 }, err => { // 失败处理 } ) GetMiniList({ a:1, b:2 }).then( res => { // 成功处理 }, err => { // 失败处理 } ) [代码] 把所有api放在api.js里统一管理,利用promise使我们只关注返回的结果
2019-05-06 - 爬虫vs反爬虫
爬虫介绍 爬虫简单介绍就是一个获取数据的途径。有时我们需要进行数据分析等操作,都会将别人网站中现成的数据放入我们自己本地数据库内,这时候,我们可以使用爬虫来实现。 网站的重要资料、信息财产被轻易窃取,是不能随便泄漏的。我们就应该使用反爬虫技术。本文将依次先将常见的反爬虫技术,与对应的爬虫技巧。 爬虫原理 一般我们访问网络资源都是通过uri。我们要获取的信息,一般有两种常见形式。json或html。html一般是后端服务器渲染后返回的,json是服务器直接返回给前端,然后前端自己在将数据渲染到页面上。如果是json类型的话,可以直接请求这个uri,或者等待前端渲染完毕再从html获取。获取到html信息后,通过dom操作,即可获得对应内容。爬取手机app的内容时,需要使用抓包工具:Fiddler,Charles,Wireshark。 最简单的爬虫,用shell就可以实现。复杂的爬虫,甚至需要用到机器学习分析。 注意:大部分情况爬虫是没有法律问题的,只有网站明确声明了禁止使用网络爬虫和转载商业化时(付费知识)时,爬虫才会触犯法律 为了更快的创建一只爬虫,请先安装Postman,Chrome等软件,用于发送请求,并可以查看详细请求和响应头部和内容 如果有反爬虫需要,请一定要记录服务器请求日志。分析日志才能找出潜在爬虫,即使没有及时对应的解决的方案,也可以暂时将其拉入黑名单。 爬虫流程 [图片] 整体流程的关键在于将网上的数据通过机器方式自动获得。没有反爬虫机制的 uri,还可以使用分布式爬虫,开启多线程,快速爬取信息。遇到了反爬虫机制,一般来说用上对应的反反爬虫方法就行了。大部分网站仅仅是对游客有访问限制,如果不想注册账号,基本都可以使用代理方式解决。也可以在登录之后复制登录的完整请求头解决。但是对于某些数据会有验证码拦截。这时我们就要将网站使用的验证码分类并在网上找到对应的解决方案。 查看请求 使用Chrom浏览器(也可以用别的,这里只用Chrome做例子)访问我们要爬取的页面。通过F12进入开发者工具,切换到network面板下(没有数据,请刷新再次请求) 找到要获取数据的URI [图片] 请求所携带的请求头都在这里,完整附带请求信息,可以充分模拟浏览器。 [图片] 直接通过Chrome将请求信息导出,在自己的终端下尝试请求。 由于Google页面携带的cookie内容较多,而且hk站点下编码不是常见的UTF8格式,会出现乱码,这里就以百度为例。 [代码]curl 'https://www.baidu.com/' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:67.0) Gecko/20100101 Firefox/67.0' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' -H 'Accept-Language: en-US,en;q=0.5' --compressed -H 'DNT: 1' -H 'Connection: keep-alive' -H 'Cookie: BAIDUID=606890F9A814F1194EEA6EC7D743CC84:FG=1; BIDUPSID=606890F9A814F1194EEA6EC7D743CC84; PSTM=1553581103; delPer=0; BD_HOME=0; H_PS_PSSID=26524_1461_21106_28722_28557_28697_28584_26350_28604_28606; BD_UPN=133252; BDORZ=B490B5EBF6F3CD402E515D22BCDA1598' -H 'Upgrade-Insecure-Requests: 1' -H 'Pragma: no-cache' -H 'Cache-Control: no-cache' [代码] 添加-o 将本次请求的response保存为文件。某些页面可能会返回乱码,是因为开启了gzip或者编码需要重新设置,部分网页编码非UTF8。 直接访问 这种网站一般数据都是完全公开的,可以算是0反爬虫。对于这种网站,直接请求即可。这种爬虫最好做,所以一般搜索引擎也最好搜索。很多网站即使内部有许多反爬虫机制,但是首页还是会为了SEO,一般不会反爬虫。 鉴别浏览器 通过浏览器进行网站访问时,都会携带user-agent信息。而在本地请求时,并不会携带这些信息。同时浏览器会保持一个session会话,而一般的request模块中,并不能携带session。我们可以通过session存放标记或者判断header部分,来鉴别爬虫。 爬虫方案 对于session的判断,使用带session的模块。比如python的requests或者使用headless。 对于header的判断,可以在浏览器的请求内复制完整的请求头。或者直接使用headless 判断token token一般不用于自己的主站。访问过很多网站,我只见过掘金在使用。token的目的不是反爬虫,而是防止用户的账号密码泄露。 先简单讲讲token步骤 客户端第一次登录 服务器确认登录并返回有期限的合法token 客服端进行请求时携带token 服务器判断token识别合法用户,然后在执行请求 爬虫方案 一般服务器都是使用sesssion的,但如果确实使用了token的。我们可以分析接口,获取token,并在请求时携带token即可。 限制ip访问频率 一般而言限制ip访问,不只是为了减少爬虫的数量,更是为了防止DDOS之类攻击。因为同一ip的大量访问,将会占用服务器的大量资源。如果触发的还是一个费时的操作,将会导致服务器来不及处理其他正常用户的请求。可以直接通过nginx自动让网站都有一个拦截ip频繁访问。 其实大部分网站还是会设置一定的阈值的,可能是1小时的访问次数,又或者每日,每个游客用户的访问次数。 爬虫方案 通过添加定时器:随机几秒延迟,不超过服务器的阈值,简单模拟人的访问频率。 使用代理服务器:大部分语言的请求,都提供了proxy的api。使用代理后,就可避开ip的限制了。所以我们反爬虫时尽力用非ip方式判断是不是用了proxy在爬取服务器 使用网站登录信息:部分网站是对游客有所限制,对于登录的用户会有更多访问次数。所以可以利用这点提高频率。 验证码 验证码一直是一个反人类的玩意。但是他设计的目的是为了反机器。验证码和破解验证码有着很长的战争历史。大部分后端语言都有快速部署数字验证码的组件。 爬虫方案 最简单的方破解法就是使用登录信息或者用代理更换ip,不过部分情况还是会遇到不可避免的验证码的。 使用第三方平台接入,手工在线解验证码。 使用机器学习配合headless,破解验证码 滑动验证码 [图片] 现在经常出现的滑动验证码,使用的是极验提供的验证码。通过滑动确实让用户的操作成本降低了。 爬虫方案 网上出现了不少的破解案例。这里推荐使用headless将通过代码能让鼠标执行操作验证码。 图片文字验证码 [图片] 爬虫方案 这种验证码比上面的要求更高,提高的用户的成本。使用的比较少,但是破解方法还是有的,就是使用OCR或者机器学习。 前端动态渲染 现代前端日益强大,许多事情在前端都可以完成。为了减轻服务器的压力。有些网站已经从传统的JSP,PHP渲染转为,Java,PHP提供接口,由前端自行渲染。 爬虫方案 这时候,我们可以再次在Network面板中查看获取信息的api接口。但是部分时候,数据会比较复杂,晦涩。甚至还有加密信息在返回的数据之内。其实我们也可能让数据直接在网页上自己渲染出来,就是通过headless等无头浏览器。实现 图片代替文字,字体映射 这种反爬方案比较高级,单一般用户体验也不会很高,成本也不低。遇到还是换个网站比较好。 headless无头浏览器 Node.js Python Puppeteer、phantomjs、Splash Selenium 更多headless 使用headless基本上算是终极解决方案了,用代码的方式去执行一个no GUI的浏览器。包括鼠标的移动,点击,拖动等。而且还能自动携带Session和Cookie信息,不过headless设计的初衷其实是前端自动化测试… [代码]const puppeteer = require('puppeteer'); async function getPage() { //创建实例 const browser = await puppeteer.launch(); //新建页面 const page = await browser.newPage(); await page.goto("https://juejin.im"); //等待1秒 await page.waitFor(1000); //截屏 await page.screenshot({path: `preview.png`}); //关闭实例 await browser.close(); } getPage(); [代码] 这段代码可以将SPA页面加载出来,查看效果可以使用api提供的screenshot截屏功能 [图片] 注意:使用headless需要加入渲染页面的性能,会导致爬虫性能极速下降(毕竟本来设计目的不是爬虫) 总结 其实为什么反爬虫没过多久都能被破解呢?其实主要原因是浏览器的用户信息的透明的,我们可以通过浏览器就可以看到开发的前端源代码。即使使用了各种技术,只要有前端,耐心的分析还是可以破解的。就算不行,我也可以通过headless确实以浏览器方式进行访问。即使是App也可以通过抓包,分析api,进行爬虫。 其实做反爬虫就好像是图灵测试,通过一系列的方法来辨别当前访问者是人还是机器。但是这个测试又不能使用户感到返感。
2019-03-26 - 深入解析JS的异步机制
1. JavaScript定义 JavaScript 是一种单线程编程语言,这意味着同一时间只能完成一件事情。也就是说,JavaScript 引擎只能在单一线程中处理一次语句。 优点:单线程语言简化了代码编写,因为你不必担心并发问题,但这也意味着你无法在不阻塞主线程的情况下执行网络请求等长时间操作。 缺点:当从 API 中请求一些数据。根据情况,服务器可能需要一些时间来处理请求,同时阻塞主线程,让网页无法响应。 2. 异步运行机制 CallBack,setTimeOut,ajax 等都是通过**事件循环(event loop)**实现的。 2.1 什么是Event Loop? 主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。 2.2 流程整体示意图 [图片] 2.3 总结异步运行到整体机制 主线程在运行的时候,将产生堆(heap)和栈(stack),栈中的代码会调用各种外部API,它们将在"任务队列"中根据类型不同,分类加入到相关任务队列中,如各种事件等。只要栈中的代码执行完毕,主线程就会去读取"任务队列",根据任务队列的优先级依次执行那些事件所对应的回调函数。这就是整体的事件循环。 2.4 任务队列的优先级 微任务队列中的所有任务都将在宏队列中的任务之前执行。也就是说,事件循环将首先在执行宏队列中的任何回调之前清空微任务队列。 ** 举例: ** [代码] console.log('Script start'); setTimeout(() => { console.log("setTimeout 1"); }, 0); setTimeout(() => { console.log("setTimeout 2"); }, 0); new Promise ((resolve, reject) => { resolve("Promise 1 resolved"); }) .then(res => console.log(res)) .catch(err => console.log(err)); new Promise ((resolve, reject) => { resolve("Promise 2 resolved"); }) .then(res => console.log(res)) .catch(err => console.log(err)); console.log('Script end'); [代码] 运行结果是: Script start Script end Promise 1 Promise 2 setTimeout 1 setTimeout 2 通过上述例子可以看到无论宏队列的位置在何方,只要微队列尚未清空,一定会先清空微队列后,在去执行宏队列。下面介绍微队列任务中比较典型的几个API,通过相关举例,让你更深入理解JS的异步机制。 3. 微任务队列 3.1 Promise(ES6) Promise,就是一个对象,用来传递异步操作的消息。 3.1.1 基础用法: [代码] var promise = new Promise(function(resolve, reject) { //异步处理逻辑 //处理结束后,调用resolve返回正常内容或调用reject返回异常内容 }) promise.then(function(result){ //正常返回执行部分,result是resolve返回内容 }, function(err){ //异常返回执行部分,err是reject返回内容 }) .catch(function(reason){ //catch效果和写在then的第二个参数里面一样。另外一个作用:在执行resolve的回调时,如果抛出异常了(代码出错了),那么并不过报错卡死JS,而是会进入到这个catch方法中,所以一般用catch替代then的第二个参数 }); [代码] 缺点: 无法取消 Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。再次,当处于 Pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。 优点: Promise能够简化层层回调的写法,Promise的精髓是“状态”,用维护状态、传递状态的方式来使得回调函数能够及时调用,它比传递callback函数要简单、灵活的多。 3.1.2 用法注意点 - 顺序: [代码] new Promise((resolve, reject) => { resolve(1); console.log(2); }).then(r => { console.log(r); }); [代码] 运行结果是: 2 1 说明: 立即 resolved 的 Promise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务。也就是resolve(1)和console.log(2)是属于同步任务,需要全部执行完同步任务后,再去循环到resolve的then中。 3.1.3 用法注意点 - 状态: [代码] const p1 = new Promise(function (resolve, reject) { setTimeout(() => reject(new Error('fail')), 3000); }); const p2 = new Promise(function (resolve, reject) { setTimeout(() => resolve(p1), 1000); }); const p3 = new Promise(function (resolve, reject) { setTimeout(() => resolve(new Error('fail')), 1000); }); p2 .then(result => console.log("1:", result)) .catch(error => console.log("2:",error)); p3 .then(result => console.log("3:", result)) .catch(error => console.log("4:",error)); [代码] 运行结果是: 3: Error: fail at setTimeout (async.htm:182) 2: Error: fail at setTimeout (async.htm:174) 说明: p1是一个 Promise,3 秒之后变为rejected。p2和p3的状态是在 1 秒之后改变,p2 resolve方法返回的是 p1, p3 resolve方法返回的是 抛出异常。但由于p2返回的是另一个 Promise,导致p2自己的状态无效了,由p1的状态决定p2的状态。所以后面的then语句都变成针对后者(p1)。又过了 2 秒,p1变为rejected,导致触发catch方法指定的回调函数。 而p3返回的是自身的resolve,所以触发then中指定的回调函数。 3.1.4 用法注意点 - then链的处理: [代码] var p1 = p2 = new Promise(function (resolve){ resolve(100); }); p1.then((value) => { return value*2; }).then((value) => { return value*2; }).then((value) => { console.log("p1的执行结果:",value) }) p2.then((value) => { return value*2; }) p2.then((value) => { return value*2; }) p2.then((value) => { console.log("p2的执行结果:",value) }) [代码] 运行结果是: p2的执行结果: 100 p1的执行结果: 400 说明: p2写法中的 then 调用几乎是在同时开始执行的,而且传给每个 then 方法的 value 值都是 100。而p1中写法则采用了方法链的方式将多个 then 方法调用串连在了一起,各函数也会严格按照 resolve → then → then → then 的顺序执行,并且传给每个 then 方法的 value 的值都是前一个promise对象通过 return 返回的值。 ###3.1.4 用法注意点 - catch的处理: [代码] var p1 = new Promise(function (resolve, reject){ reject("test"); //throw new Error("test"); 效果同reject("test"); //reject(new Error("test")); 效果同reject("test"); resolve("ok"); }); p1 .then(value => console.log("p1 then:", value)) .catch(error => console.log("p1 error:", error)); p2 = new Promise(function (resolve, reject){ resolve("ok"); reject("test"); }); p2 .then(value => console.log("p2 then:", value)) .catch(error => console.log("p2 error:", error)); [代码] 运行结果是: p2 then: ok p1 error: test 说明: Promise 的状态一旦改变,就永久保持该状态,不会再变了。不会即抛异常又会正常resolve。 3.2 async/await(ES7) 3.2.1 async基础用法: async 用于申明一个 function 是异步的,返回的是一个 Promise 对象。 [代码] async function testAsync() { return "hello async"; } var result = testAsync(); console.log("1:", result); testAsync().then(result => console.log("2:", result)); async function mytest() { //"hello async"; } var result1 = mytest(); console.log("3:", result1); [代码] 运行结果是: 1: Promise {<resolved>: “hello async”} 3: Promise {<resolved>: undefined} 2: hello async 说明: async返回的是一个Promise对象,可以用 then 来接收,如果没有返回值的情况下,它会返回 Promise.resolve(undefined),所以在没有 await 的情况下执行 async 函数,它会立即执行,并不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。 3.2.2 await基础用法: await 只能出现在 async 函数中,用于等待一个异步方法执行完成(实际等的是一个返回值,强调 await 不仅仅用于等 Promise 对象,它可以等任意表达式的结果)。 [代码] function getMyInfo() { return Promise.resolve("hello 2019!"); } async function testAsync() { return "hello async"; } async function mytest() { return Promise.reject("hello async"); } async function test() { try { const v1 = await getMyInfo(); console.log("getV1"); const v2 = await testAsync(); console.log("getV2"); const v3 = await mytest(); console.log(v1, v2, v3); } catch (error) { console.log("error:", error); } } test(); [代码] 运行结果是: getV1 getV2 error: hello async 说明: await等到的如果是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。 放心,这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。 3.2.3 async/await的优势: 很多情况下,执行下一步操作是需要依赖上一步的返回结果的,如果当嵌套层次较多的时候,(举例3层的时候): [代码] const getRequest = () => { return promise1().then(result1 => { //do something return promise2(result1).then(result2 => { //do something return promise3(result1, result2) }) }) } [代码] 从上例可以看到嵌套内容太多。此时如果用async写法,可写成如下: [代码] const getRequest = async () => { const result1 = await promise1(); const result2 = await promise2(result1); return promise3(result1, result2); } [代码] 说明: async / await 使你的代码看起来像同步代码,它有效的消除then链,让你的代码更加简明,清晰。 总结:以上就是对JS的异步机制及相关应用的整体总结,如有需要欢迎交流~
2019-03-12 - mpvue开发音频类小程序踩坑和建议
前言 这是我第一次开发小程序,开发的产品是音频类的,在大佬的建议下采用了[代码]mpvue[代码],一周时间把功能都做出来,由于不太熟悉mpvue和微信小程序,足足用了一周时间来改bug才出来一个能用的版本,在这里整理分享下我开发时遇到的一些问题和给出一些建议。 [图片] 在[代码]Linux[代码]上开发小程序 在公司电脑装了双系统,日常用的是[代码]Ubuntu[代码]系统,Linux或Mac的开发环境对前端相对来说会友好一些。微信小程序官方的开发者工具只有[代码]Windows[代码]和[代码]Mac[代码]版本,所以这就尴尬了。 不过还好,发现已经有大神在GitHub上做了Linux的支持,推荐给大家:Linux微信web开发者工具。 根据教程安装使用即可,使用时就用[代码]./bin/wxdt[代码]命令打开。不过用了几天后面觉得不太方便,就索性切回Windows系统用官方最新的版本了。 封装wx.request为Promise [代码]wx.request[代码]用于发起http请求,但平时习惯了Promise的写法,所以还是封装一下这个方法为Promise的形式。 我看很多小程序会使用fly这个库。 但个人觉得发起请求不需要那么强大的功能,小程序本身就应该是一个轻量级的东西,引入一个库可能会导致项目打包变大,可能让小程序更卡,所以本着能自己写就自己写吧的心态,索性自己封装一下算了。 在[代码]src/utils[代码],新建一个[代码]request.js[代码]: [代码]const apiUrl = 'https://your server.com/api/' const request = (apiName, reqData, isShowLoading = true) => { // 某些请求可能不需要显示loading if (isShowLoading) { wx.showLoading({ title: '正在努力加载中', mask: true }) } return new Promise(function (resolve, reject) { wx.request({ url: apiUrl + apiName, method: 'POST', data: reqData, header: { 'content-type': 'application/json' // 默认值 }, success (res) { if (res.data.code === 0) { // 与后端约定code=0时才是正常的 resolve(res) } else { reject(res) } }, fail (err) { reject(err) }, complete (res) { wx.hideLoading() } }) }) } export default request [代码] 当然这是个简化版的,我实际项目中还会在初始化时加入一些[代码]token[代码]之类的参数,大家能看明白是这样封装成Promise的就可以啦。 使用vant-weapp 小程序已经支持了npm安装,但不太会弄。还是按网上方法,将项目clone下来放进static目录下。 [代码]git clone https://github.com/youzan/vant-weapp.git [代码] 然后将[代码]vant-weapp[代码]的[代码]dist[代码]目录拷贝到项目的static目录下(尽可能精简,删掉一些奇奇怪怪的如[代码].github[代码]的东西,所以直接使用dist目录),改名为[代码]vant[代码](也可以不改名)。全局使用时,可以在[代码]app.json[代码]引入: [代码] "usingComponents": { "van-button": "/static/vant/button/index", "van-field": "/static/vant/field/index" }, [代码] 注意:需要打开微信开发者工具中的ES6转ES5功能 一开始以为使用起来和web端的没啥差别,但没想到那么麻烦。比如:在vue中是可以使用[代码]v-model[代码]的,但在mpvue中的小程序中不能使用,只能 [代码]<van-field :value="password" type="password" @change="pwdChange" input-class="myClass" /> [代码] 而且不能随意灵活添加class修改组件的样式,需要vant组件支持提供外部样式才可修改,比如上面的[代码]van-field[代码]是通过[代码]input-class[代码]来添加样式控制的,很不方便。而且某些内部样式由于没有外部样式表,根本改不了。 综上: 在微信小程序使用第三方组件库不太方便,样式修改比较麻烦,如果产品是有UI设计时,尽量不使用,有时候自己实现样式可能更快,而且项目体积更小。 使用vuex mpvue官方的快速模板中是将vuex放在[代码]counter[代码] 这个page目录下,可能习惯了vue官方写法的很多同学(包括我)不太喜欢,所以最好就改为vuex官方的写法。 在src目录下建一个[代码]store[代码]的文件夹,分别建以下文件: [图片] 项目不太复杂时不建议使用modules,使用起来比较麻烦。 贴一下[代码]index.js[代码]的代码,其他的[代码]actions.js[代码],[代码]getters.js[代码]按官方的写法就好啦。 [代码]import Vue from 'vue' import Vuex from 'vuex' import * as actions from './actions' import * as getters from './getters' import state from './state' import mutations from './mutations' import createLogger from 'vuex/dist/logger' Vue.use(Vuex) const debug = process.env.NODE_ENV !== 'production' export default new Vuex.Store({ actions, getters, state, mutations, strict: debug, plugins: debug ? [createLogger()] : [] }) [代码] [代码]vuex/dist/logger[代码]是vuex在开发环境可以自动打印日志的工具,debug比较方便,建议使用。 然后在[代码]src/main.js[代码]引入: [代码]import Vue from 'vue' import App from './App' import store from '@/store' Vue.config.productionTip = false App.mpType = 'app' Vue.prototype.$store = store const app = new Vue({ store }) app.$mount() [代码] 这样就可以在项目中正常使用啦,完全支持[代码]mapState[代码],[代码]mapActions[代码],[代码]mapGetters[代码]的写法,比如在[代码]pages/index/index.vue[代码]中使用: [代码]<script> import { mapState, mapActions } from 'vuex' export default { computed: { ...mapState(['myAudio']) }, methods: { ...mapActions(['myActions']) }, created () { this.myActions() //调用vuex中的方法 } } </script> [代码] 踩坑指南 其实大多数坑可能是mpvue的,很多情况也是自己不熟悉小程序生命周期导致的一些奇奇怪怪的bug。 mpvue是支持小程序原生组件的 mpvue会将[代码]div[代码]编译为小程序中的[代码]view[代码]。一开始我不了解,以为用了mpvue后就不能使用小程序原生支持的组件了,比如[代码]swiper[代码],[代码]scroll-view[代码]等,小程序是支持的,可以放心使用哈哈。 npm run build后样式丢失 本来在开发环境正常的,然后准备发版[代码]npm run build[代码]后发现样式丢失了。然后重新[代码]npm start[代码]排查问题,样式还是丢失的。内心此时是mmp的:npm run build丢失就算了,我没改什么东西重新npm start后为什么还是丢失,之前还是正常的呀? 刚开始怀疑是缓存什么的问题,删掉的dist目录,重启开发者工具,甚至重启电脑都试了一下,这是我遇到的超级诡异的问题之一。 冷静下来想到:之前的版本是正常的,一定是新版本引入了什么导致了打包样式的丢失。于是回滚版本一个个build排查问题,最后找到了原因:在一个page中引入了其他page,即在页面中import另一个页面。 在我这里的具体例子是:我在[代码]pages/index/index.vue[代码] 中想做底部共用一个tabbar,页面根据tabbar的值来显示对应的子级页面:[代码]pages/page1/index.vue[代码]和[代码]pages/page2/index.vue[代码]。 所以我将这两个页面当做子组件来引入了:[代码]import Page1 from '@/pages/page1'[代码],一开始没有问题,等重启项目,或者build后就发现样式丢失了。 这可能是mpvue打包机制的一个限制,即[代码]页面不能将另一个页面当子组件来引用[代码],否则会导致样式丢失。 背景音频的src无法读取 项目中希望用户退出小程序后依然能播放音频,所以用到了背景音频的api: wx.getBackgroundAudioManager()。 [代码]this.audio = wx.getBackgroundAudioManager() this.audio.src = 'http://ws.stream.qqmusic.qq.com/M500001VfvsJ21xFqb.mp3?guid=ffffffff82def4af4b12b3cd9337d5e7&uin=346897220&vkey=6292F51E1E384E061FF02C31F716658E5C81F5594D561F2E88B854E81CAAB7806D5E4F103E55D33C16F3FAC506D1AB172DE8600B37E43FAD&fromtag=46' this.audio.title = '此时此刻' //注意必填 this.audio.epname = '此时此刻' this.audio.singer = '许巍' this.audio.coverImgUrl = 'http://y.gtimg.cn/music/photo_new/T002R300x300M000003rsKF44GyaSk.jpg?max_age=2592000' [代码] [代码]title[代码]和[代码]src[代码]赋值后会直接播放音频,后面的几个属性建议也填上,因为播放背景音频时微信是有个界面需要封面图和歌手名称等的。 如果想要获取当前正在播放的音频src,本来以为通过[代码]this.audio.src[代码]来获取就可以了但是有bug。 在开发者工具中是可以正常获取的,即开发时是没问题的,但在真机上返回的是[代码]undefined[代码],因此不能用[代码]this.audio.src[代码]来获取当前播放的音频url,得用一个变量来存这个数据。 直接使用音频的currentTime可能渲染不及时 currentTime用于显示当前的播放进度,但我用在子组件中时经常更新不及时,打印是正常的,但试图渲染不及时,有时候需要点击一下才能重新渲染,这可能是mpvue使用时才会遇到。 所以建议还是项目自身维护一套背景音频的变量比较好一点,比如放在[代码]vuex[代码]中。监听[代码]BackgroundAudioManager.onTimeUpdate()[代码]方法每次赋值到自身维护的变量中。 音频的onCanplay方法不一定每个音频都会触发 一开始我监听在[代码]onCanplay[代码]方法,将音频的时长信息[代码]duration[代码]赋值到vuex中存起来,但发现[代码]onCanplay[代码]有时候是不会触发的,比如重新赋值src播放下一首时,很尴尬。 所以不要太依赖onCanplay这个方法,还好目前直接使用[代码]audio.duration[代码]好像不会出现像上面的[代码]currentTime[代码]渲染不及时的问题,所以就这样用着先。 音频播放结束,即onStop后,不能再通过audio.play()的方法重新播放,得重新赋值src 正常来说,音频播放结束后,音频的src是不变的,再次[代码]play()[代码]应该是可以的。但在小程序中偏偏不行,得重新赋值src才能重新播放,这应该是小程序的一个bug。。。 所以需要判断一下暂停和停止的情况,用不同的办法播放。正常来说,音频暂停时[代码]currentTime[代码]是不为0的,而结束时[代码]currentTime[代码]会为0。 所以可以通过[代码]currentTime[代码](最好是自己维护的变量)来判断暂停和停止的情况:如果currentTime不为0,表示是暂停的情况,可以用[代码]play()[代码],如果小于等于0,则重新赋值src播放: [代码]if (currentTime) { this.audio.play() } else { this.audio.src = 'xx.mp3' } [代码] mpvue不支持直接在template上直接绑定函数 这个是mpvue文档上有写的,不过一开始并不是很理解,也踩坑了,所以在这里提一下,避免不知道的同学踩坑找半天。 [代码]<template> <div v-for="(item, index) in list" :key="index">{{ formatItem(item) }}</div> </template> <script> export default { data () { return{ list: [1, 2, 3] } }, methods: { formatItem (item) { return `我是${item}` } } } </script> [代码] 上面的代码应该是日常vue中比较常用的,就是将数据传参给方法做一些处理,这个在mpvue中是不支持的,会被编译成一个空字符串。 小程序中可放心使用css3的一些特性 比如高斯模糊 [代码]filter: blur(50px); [代码] 如果要使用动画,尽量用[代码]css[代码]动画代替[代码]wx.createAnimation[代码] 在实际使用时,[代码]wx.createAnimation[代码]做动画其实很卡,性能很差,所以在需要使用动画时,建议尽量使用css做动画。 在小程序中是支持css动画的,[代码]transition[代码],[代码]animation[代码],[代码]@keyframes[代码]这些特性都支持。 比如做一个div一直旋转的动画,大家可以对比一下两个版本: [代码]wx.createAnimation[代码]版本 原理:通过setInterval()不断更新div的旋转位置 [代码]<template> <div class="cover" :animation="animationData"></div> </template> <script> export default { data () { return { animationData: '', animation: '', rotateCount: 0, timer: '' } }, components: { }, methods: { startRotate () { this.timer = setInterval(() => { this.rotateAni(++this.rotateCount) }, 100) }, rotateAni (n) { if (!this.animation) { return } // 每100毫秒旋转10度 this.animation.rotate(10 * n).step() this.animationData = this.animation.export() } }, onShow () { // 页面从隐藏到显示时才执行 if (!this.animation) { this.animation = wx.createAnimation() this.startRotate() } }, onReady () { // 第一次初始化时会执行 if (!this.animation) { this.animation = wx.createAnimation() this.starRotate() } }, onHide () { // 页面隐藏时会执行,避免频繁的setData操作,将定时器停掉 this.timer && clearInterval(this.timer) }, beforeDestroy () { // 页面卸载,也停掉定时器 this.timer && clearInterval(this.timer) } } </script> <style scoped lang="scss"> .cover { left: 20px; bottom: 70px; border-radius: 50%; background: #fff; position: absolute; width: 50px; height: 50px; background: rgba(0, 0, 0, 0.2); box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.2); border: 1px solid rgba(255, 255, 255, 0.5); overflow: hidden; z-index: 10000; } </style> [代码] 使用css的[代码]@keyframes[代码]做旋转动画 [代码]<template> <div class="cover" :style="coverStyle"></div> </template> <script> export default { } </script> <style scoped lang="scss"> // 定义一个动画名为 rotate @keyframes rotate { 0%, 100% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .cover { left: 20px; bottom: 70px; border-radius: 50%; background: #fff; position: absolute; width: 50px; height: 50px; background: rgba(0, 0, 0, 0.2); box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.2); border: 1px solid rgba(255, 255, 255, 0.5); overflow: hidden; z-index: 10000; // 使用动画 animation: rotate 4s linear infinite; } </style> [代码] 用js写的动画需要控制好setInterval的间隔时间和旋转角度,比较难调。而用css写动画很简单,性能比js好,代码量也很少。 使用css动画时建议开启硬件加速 为了动画更流畅,想尽办法做优化,虽然不知道有没效果,反正用了再说[手动滑稽]。 可以用will-change和transform: translate3d(0,0,0)开启硬件加速。我也不太会用,具体用法大家自行百度Google。 [代码]will-change: auto; transform: translate3d(0, 0, 0); [代码] iPhoneX需要底部导航条预留34px(68rpx)的高度。 由于小程序中不能设置[代码]viewport-fit=cover[代码],所以也就没有web中的安全区域说法,目前主流的做法是通过[代码]wx.getSystemInfoSync()[代码]判断是否是ipx,若是则给页面底部撑高34px。 [代码]const res = wx.getSystemInfoSync() if (res.model.indexOf('iPhone X') >= 0) { this.isIpx = true } [代码] 注意是用[代码]res.model.indexOf('iPhone X')[代码],在开发者工具的iPhone X中,model是全等于[代码]iPhone X[代码]的,但在真机中往往拿到的值是[代码]iPhone X GZxxx[代码],即后面可能会带一串东西,所以用[代码]indexOf[代码]才是比较稳的,而且对[代码]iPhone XR[代码]等机型也适用。 由于还有其他安卓机的全面屏,不太可能一一判断,而且某些安卓全面屏是没有用iPhone底部的工具条的(不存在冲突的情况),所以我们只判断iPhone X的情况就可以了,其他全面屏就不需要给底部预留了。 至于全面屏布局的适配,需要用[代码]flex[代码]布局或者获取屏幕宽高来慢慢调了,建议最好用flex布局自适应处理。 for循环中的子组件click事件无法触发 [代码]Page -> 父组件 -> 子组件[代码],在子组件click后[代码]$emit[代码]一个事件出来,发现无法触发。 这个bug一开始没有出现,但偶然[代码]npm run build[代码]出现的,然后排查原因,后面即使回滚所有版本再npm start也还会出现。好像不触发则已,一发就不可收拾,这又是一个大坑,搜issue和加群问人,当晚下班回家研究到1点多都没有解决。 第二天继续研究,感觉可能是框架的原因,最后尝试升级一下mpvue版本,没想到就正常了。直接使用quick-strat项目的[代码]mpvue[代码]版本是 2.0.0,[代码]mpvue[代码]和[代码]mpvue-template-compiler[代码]升级到最新[代码]2.0.6[代码]就解决了。 事后查看mpvue版本记录,果然是框架本身原因,并且找到了issue。 npm run build后代码报错,再build一次可能报另一些错 解决: 没找到原因,可能是引入vant导致的,打包时丢失了部分文件。多build几次,或者重启下小程序开发者工具就正常了。 mpvue中created() 钩子会在页面初始化时全部一起触发,尽量不要用 小程序生命周期的理解 进入已销毁的page组件时依次触发: onLoad,onShow,onReady,beforeMount,mounted 第一次进入已销毁的子组件时依次触发: onLoad,onReady,beforeMount,mounted 第二次进入已销毁的子组件时依次触发: onLoad,onShow,onReady 再次进入 未被销毁的page组件、子组件时只触发: onShow mpvue文档中建议尽量不要使用小程序的生命周期,这个应该是为了让项目更好地适应支付宝小程序和头条小程序等,所以才这样建议大家尽量不要使用某一个小程序自身的api。 如果你们的小程序只是微信小程序(不考虑兼容其他平台小程序),我建议直接用小程序的生命周期,而不要用mpvue的生命周期,坑太多了。比如mpvue的created周期,初始化时所有的page都会执行,所以created这个周期是不能用了。 onUnload不触发 小程序中与平常web开发不同的是,它的页面会被缓存。举个例子: 从[代码]page1[代码]跳转到[代码]page2[代码],再从[代码]page2[代码]返回[代码]page1[代码],此时的[代码]page1[代码]还没销毁,不会触发[代码]onLoad[代码]再重新渲染,而是直接使用之前的数据。从性能上来说,单纯的返回不应该再请求api获取数据重新渲染,这是对的,符合我们的预期。 而有时候,从[代码]page2[代码]返回[代码]page1[代码]时,我们希望[代码]page1[代码]是重新获取数据渲染的。比如在[代码]page2[代码]做了一个退出登录的操作,此时再返回[代码]page1[代码]时,还是会看到之前的数据。实际上我们的预期是:由于已经退出登录了,[代码]page1[代码]的数据应该被销毁了。 在平常的web开发中,遇到上面的问题,我们可能是不管缓存,每次返回[代码]page1[代码]都再次请求api渲染最新的数据,牺牲掉部分性能从而保证逻辑的正确性。 在mpvue中我也尝试这样干了:想在[代码]page1[代码]的[代码]onUnload()[代码]生命周期中销毁数据,但是没有成功。即使在[代码]page2[代码]退出登录时,采用[代码]wx.reLaunch()[代码]重新刷一遍,[代码]page1[代码]的[代码]onUnload()[代码]生命周期也没有执行。所以[代码]onUnload()[代码]是有可能不执行的,建议慎用。 最后还是得想办法做到在[代码]page2[代码]控制[代码]page1[代码]的数据销毁或保留。想到这里,[代码]vuex[代码]就不自觉浮现在眼前了,如果page1的数据是通过vuex来控制的,那么我在page2就可以用vuex来灵活管理其他页面的数据了。 如果page2做退出登录操作时,就让page1的数据销毁,如果是不退出登录正常返回,page1的数据还是正常,做到灵活控制。 个人平时web开发很少用[代码]vuex[代码],因为项目比较简单不用那么复杂的全局数据传递。但在小程序中,建议全局使用[代码]vuex[代码]来控制所有数据(当然是得根据需求来用)。 总结 第一次开发小程序就直接上了mpvue,可能有些坑已经很多同学总结过了,有些坑可能是不熟悉而导致的,但自己没有去踩过一遍可能不够深刻。 有两种坑会比较难啃: 框架本身的问题,如mpvue2.0.0出现的子组件无法触发事件的问题。 开发者工具和真机运行环境不一致导致的坑。 遇到真机和开发者工具不一致的情况,可按以下步骤排查: 有可能是缓存,可以杀掉之前的版本再跑起来 手机微信版本太低,可能api不支持,用[代码]wx.canIUse[代码]打印一下 手机端某些属性不支持读取,比如上面的[代码]this.audio.src[代码],可以在真机打印调试一下 代码在手机端运行有报错,可以在手机端开启调试,看一下log 微信设计上的坑,百度下是否有相关的案例和解决办法 而遇到mpvue框架的问题可以: 去搜一下[代码]mpvue[代码]的issue看有没相关解决办法 尽量使用最新版本的框架,可能某些问题已经修复了的。实在解决不了的,建议想办法绕过,换一种方法来实现。 希望对大家有所帮助。
2019-03-12