- 爬虫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 - 小打卡 | 如何基于微信原生构建应用级小程序底层架构(下)
点击查看:小打卡 | 如何基于微信原生构建应用级小程序底层架构(上)装饰模式[图片] 可能有朋友会好奇装饰器和我们接下来要做的原生函数扩展有什么关系? 先看下装饰模式的定义:在不改变原函数的基础上,附加一些额外的功能 方便更好的理解 创建一个函数,然后将存在originF这个变量里,并对它重新赋值,最后执行,最后的执行直接结果是什么? hello world!! 这样做的好处是什么,我们对函数f添加了一个新的功能,但是我们没有对原函数进行任何的修改 那么对于小程序的应用 照猫画虎一下,同样的将小程序原生的Page函数存起来,再重新赋值,看下结果,发现Page执行的时候每次都会console这句话,我这里有两个未分包的页面,就执行了2次 [图片] 回归到刚开始需要解决的问题,“多个页面pv,uv如何监控,不可能每个页面都要手动收集”, pv怎么算,pv的意思是页面浏览量,也就是需要页面生成时,对应到小程序生命周期就是onLoad 刚才我们做到了每个Page执行时添加功能,但是怎么在onLoad时进行数据统计呢? 同样的可以用装饰函数修饰page里面的函数方法 [图1] 这个data就是我们实际在pages下写的业务逻辑对象,我们先拿到该页面的key名来进行遍历,首先排除掉非函数,拿到onLoad函数,这时候对它进行扩展,这时候每一个页面执行onLoad的时候都会console一次字符串,当然也可以替换为你想要的任何功能 [图2] 其实我们可以再优化一下,比如抽出一个对象,将你想要装饰的函数写入其中,如果原函数存在则进行装饰,如果不存在则直接赋值,这个抽出来的对象其实可以算是另一种继承方式的实现 它的意义在于[图片] 帮助我们开辟了一块公有空间,帮助我们扩展Page对象,并且可以劫持任意方法,在不修改原业务函数代码的情况下增加新的逻辑 其次也是最重要的是,我们完全不需要处理历史包袱 一个应用场景[图片] 左边是微信原生的分享方法 遇到的一些问题: 不便于扩展,多个分享策略时,代码块过大,并且分享的通用数据需要每个页面单独处理,例如统计回流信息需要的分享页面信息,事件信息 右边是依赖于我们刚刚开辟的公有空间里填写的公共方法,函数很简单,获得参数,获得页面分享策略,执行,顺便还做了obj转url的处理,避免了纯url书写长路径时的尴尬 思考[图片] 既然能扩展Page对象,那么App,Component甚至wx全局对象下的方法呢? 有兴趣的朋友可以下来想一下 跨页面 / 多页面事件通知[图片] 我这边简单提一下跨页面通知的问题,这个应该算是很多小程序开发者遇到的通用问题,我问过的一些人,大部分是使用下面这两种方法,一种是getCurrentPages 页面栈队列,二种onShow upData global里的存储的数据,不管哪一种在业务复杂的情况下都会引起一定的问题,比如第一种的多级入口,第二种的话属于无效判断和及时性 小打卡这边用的简版的event Bus,一个只有80行代码的订阅方法, 左图是一个简单的示例,大致上就是分两种角色,订阅者和发布者,中间依靠任务队列联系,每次发布者推送消息,订阅者都会收到通知和相应的数据 [图片] 其实单纯的event bus也有很多的问题,比如:业务复杂情况下过于频繁,对业务代码侵入频繁,可以想像一下到处都是on,off, emit场景 解绑需要树立规范,但是人总是会犯错,容易绑定后忘记解绑或重复绑定的问题,比较浪费内存,对性能也有消耗 结合公共空间我们已知可以对生命周期进行扩展的时候怎么去解决这个问题 其实订阅必然是和页面结合,因为在页面不存在的情况下,发送通知也不会有反馈, 如何证明页面存在自然是onLoad 和 onUnload 按照这个逻辑我们只需要在onLoad和onShow做些处理即可 [图1] 先看右侧业务代码,按照策略注册了一个函数集合,在执行onLoad时,自动将业务内的onRss函数遍历,自定绑定订阅,并推到一个缓存数组内 onUnload时遍历缓存,自动解绑 这时订阅与页面的生命周期强绑定,我们不再需要处理解绑事件,也不需要在业务内插入订阅代码,只需要管理好当前页面的订阅策略即可 技术选型[图片] 没有哪一种一定正确的方案,在选型的时候可能需要考虑到上面大致6种元素, 总之选择最适合自己团队方案非常重要 一些小贴士:开发要尽量避免过度设计, 应根据实际需求, 比如之前在写现在这个简版的event bus库的时候设计了很多奇奇怪怪的 功能,现在2年过去还没用到 小程序和浏览器,node本质上是相通的,可以多借鉴和参考 The End今天我的分享就到此结束,算是这段时间对小程序开发上的一点心得体会,谢谢 [图片]
2019-04-22 - 小打卡 | 如何基于微信原生构建应用级小程序底层架构(上)
[图片] 大家好,我是小打卡的前端负责人金轩正,今天分享的主题是如何基于微信原生构建应用级小程序底层架构,这个命题看上去好像有些大,不过不要紧,这次分享我把它拆一下,大致从 小程序原生开发面临的问题 小打卡整体架构演进 开发中摸索与实践 这三个方面来看这个讲一下 [图片] 小程序原生开发面临的问题[图片] ok,首先第一个方面原生开发遇到的问题 小程序从17年诞生2年来一直处于互联网风口,不过对于开发者而言的整个开发体验不是特别友好,在17-18年之间我和很多开发小程序的小伙伴们聊过,大多数的反馈可能分为下面大致几类,当然还有更多: 没有父类,无法使用继承挂载全局方法,扩展生命周期没有父类,无法使用继承挂载全局方法,扩展生命周期 不支持跨页面/多页面通讯 setData的性能瓶颈 代码包大小限制 1/2/4/8 M,没有npm包 代码发布流程繁琐 其根本原因是将刚刚诞生的小程序与已经非常成熟的React,vue,angular作对比,而没有将小程序作为一个新的生态来看待,当然这个是一种看待事物的进步,并不是倒退,我在这里说这句话的意思是有更多的问题需要我们开发者主动去解决问题,推动整个生态的前进与发展 [图片] 其实这里可能有些朋友会问,已经有很多优秀的框架已经解决了这些问题,那么为什么还要使用原生开发? 确实在这段时间内出现了很多优秀的解决方案,我们不用并不是因为情怀哈(当然还是有那么一丢丢) 更多的是下面几点: 历史包袱,改造成本过高 小打卡在小程序刚出现的时候就进入开发了,当时框架还不成熟,而且对创业公司来说时间和迭代效率高于一切,在人手不足,业务模式尚未形成,还处于探索阶段的情况下花费大量时间去做对产品影响较小, 甚至delay迭代速度事情不是很赚 减少与第三方沟通成本 高速迭代的情况下,将时间尽可能的覆盖于业务上,避免在整个开发-上线闭环上增加节点 避免开发黑盒,控制风险 虽然整个社区是非常活跃的,fixed一个问题同样是需要花费一定时间,但是很多时候需求是不会等你bug fixed 如非必要,勿增实体 即“简单有效原理”,这句话还是我去年刚来公司的时候和阿赖聊他所说过的 放在项目开发上我的理解是在架构层面要做的尽可能的薄,避免过度设计 这样才有足够的扩展性,灵活性,容错性 这些框架虽好,但是对我们当前业务来说可能过于复杂,比如跨端在之前的阶段还没有这方面需求,而像组件化小程序已经支持,自动化构建我们自己也是可以搭建的并不复杂 相信微信小程序团队 是真正的想把这件事情做好,而且做的是一个生态,不论是小程序对于反馈响应速度,和迭代速度非常给力,还是对开发者社区运营,比如是社区活跃与审核速度挂钩,社区周刊,优质个人和优质企业 对齐web标准,并且更加开放 [图片] 小打卡整体架构演进其实小打卡整个架构并非一蹴而就的,就像前面所说的如非必要,勿增实体,而是大量的实际开发中遇到的共同问题解决方案的集合题 [图片] 常规架构这个是微信小程序给出的快速开发模版的一个开发模式: server模块提供数据,App作为全局对象直连所有的业务模块,工具函数提供api处理业务模块的需求 优点: 整个模型非常简单,上手快,学习成本 低结构清晰,在业务不复杂的情况下可以快速开发 不瞒大家其实小打卡在最初的半年内基本都是这套模式。 当然是在业务不复杂的情况下,复杂情况下会出现哪些问题呢? App作为全局对象在有大量业务模块连接的情况下,代码很容易膨胀,在多人开发的时候问题非常明显,无论是fixed bug还是正常的业务开发都会造成麻烦 页面之间独立,缺少公共模块,唯一的工具函数又要尽可能保持单一职责来提供服务(小打卡当时就是因为这个问题导致很多工具函数内部存储直接修改外部状态,导致大量强耦函数合无法拆分) 业务层直连server层,未拆分数据层的情况下,基本不存在复用性 上面所述的问题,从我接手这个项目到真正的调整持续了挺长一段时间,主要是缺乏一个契机来进行优化 优化的转折点 [图片] 然后突然有一天产品同学跑过来说: 我们要有自己的核心数据仓库,我们要看实时数据 ok,涉及到数据采集的问题了,我这边从浅到深大概列了几项: 最基础的多个页面pv,uv如何监控,不可能每个页面都要手动收集 为了统计页面和事件的分享和回流的数据,需要在分享事件携带大量的参数 微信的wx.previewImage, wx.chooseImage 等api对于用户session的收集造成很大麻烦 我们先解决第一个问题,如何收集页面pv,uv 容易陷入的误区 [图片] 在解决问题之前,我们先说一下开发小程序容易进入的误区 App 和 Page 等函数工厂是微信原生提供,不可修改 小程序项目结构是基于App, Page, 工具函数三个模块构建的 小程序的全局存储只有globalData和本地缓存 其实产生这些误区最根本的原因是小程序没有提供在复杂业务逻辑下的开发范式,比如vue,react有自己的通用开发模版 如果保持这些观念来进行开发的话,很容易将路子走窄,并且难以解决一些实际上的问题, 其实不论小程序和传统web有多少不同, 本质上还是在js环境下开发 小打卡架构图解 [图片] 为了更好的方便理解后面的具体实现,我提前放了一张目前小打卡的架构图 首先很熟悉的server这一边垫了一个数据层,主要将数据层和业务层解耦,提高复用性,并且提供一些通用功能,比如返回格式化数据问题,参数校验,日志监控... 在App对象和业务层同样增加了一个全局模块,提供独立于业务和工具类,只提供api之间双向通讯的渠道 工具模块的话其实就是对业务层的增强,比如常见的请求模块,上传模块,路由拦截等等 业务模块的话基本除了增加Component和中间层外没有太大变化 这个图上可能有两块可能大家觉得比较怪异,一个是global里面的函数重载,还有一个是业务模块的中间层是什么? 函数重载其实就是修改微信提供的App, Page, Component函数,使其更符合我们的业务场景, 业务模块的中间层就是依赖于函数重载的扩展 其实小打卡的整套架构都是基于这两个模块,这两个模块赋予了更多的可能性,然而实现却十分的简单 点击查看:小打卡 | 如何基于微信原生构建应用级小程序底层架构(下)
2019-04-22 - 路由的封装
小程序提供了路由功能来实现页面跳转,但是在使用的过程中我们还是发现有些不方便的地方,通过封装,我们可以实现诸如路由管理、简化api等功能。 页面的跳转存在哪些问题呢? 与接口的调用一样面临url的管理问题; 传递参数的方式不太友好,只能拼装url; 参数类型单一,只支持string。 alias 第一个问题很好解决,我们做一个集中管理,比如新建一个[代码]router/routes.js[代码]文件来实现alias: [代码]// routes.js module.exports = { // 主页 home: '/pages/index/index', // 个人中心 uc: '/pages/user_center/index', }; [代码] 然后使用的时候变成这样: [代码]const routes = require('../../router/routes.js'); Page({ onReady() { wx.navigateTo({ url: routes.uc, }); }, }); [代码] query 第二个问题,我们先来看个例子,假如我们跳转[代码]pages/user_center/index[代码]页面的同时还要传[代码]userId[代码]过去,正常情况下是这么来操作的: [代码]const routes = require('../../router/routes.js'); Page({ onReady() { const userId = '123456'; wx.navigateTo({ url: `${routes.uc}?userId=${userId}`, }); }, }); [代码] 这样确实不好看,我能不能把参数部分单独拿出来,不用拼接到url上呢? 可以,我们试着实现一个[代码]navigateTo[代码]函数: [代码]const routes = require('../../router/routes.js'); function navigateTo({ url, query }) { const queryStr = Object.keys(query).map(k => `${k}=${query[k]}`).join('&'); wx.navigateTo({ url: `${url}?${queryStr}`, }); } Page({ onReady() { const userId = '123456'; navigateTo({ url: routes.uc, query: { userId, }, }); }, }); [代码] 嗯,这样貌似舒服一点。 参数保真 第三个问题的情况是,当我们传递的参数argument不是[代码]string[代码],而是[代码]number[代码]或者[代码]boolean[代码]时,也只能在下个页面得到一个[代码]string[代码]值: [代码]// pages/index/index.js Page({ onReady() { navigateTo({ url: routes.uc, query: { isActive: true, }, }); }, }); // pages/user_center/index.js Page({ onLoad(options) { console.log(options.isActive); // => "true" console.log(typeof options.isActive); // => "string" console.log(options.isActive === true); // => false }, }); [代码] 上面这种情况想必很多人都遇到过,而且感到很抓狂,本来就想传递一个boolean,结果不管传什么都会变成string。 有什么办法可以让数据变成字符串之后,还能还原成原来的类型? 好熟悉,这不就是json吗?我们把要传的数据转成json字符串([代码]JSON.stringify[代码]),然后在下个页面把它转回json数据([代码]JSON.parse[代码])不就好了嘛! 我们试着修改原来的[代码]navigateTo[代码]: [代码]const routes = require('../../router/routes.js'); function navigateTo({ url, data }) { const dataStr = JSON.stringify(data); wx.navigateTo({ url: `${url}?jsonStr=${dataStr}`, }); } Page({ onReady() { navigateTo({ url: routes.uc, data: { isActive: true, }, }); }, }); [代码] 这样我们在页面中接受json字符串并转换它: [代码]// pages/user_center/index.js Page({ onLoad(options) { const json = JSON.parse(options.jsonStr); console.log(json.isActive); // => true console.log(typeof json.isActive); // => "boolean" console.log(json.isActive === true); // => true }, }); [代码] 这里其实隐藏了一个问题,那就是url的转义,假如json字符串中包含了类似[代码]?[代码]、[代码]&[代码]之类的符号,可能导致我们参数解析出错,所以我们要把json字符串encode一下: [代码]function navigateTo({ url, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } // pages/user_center/index.js Page({ onLoad(options) { const json = JSON.parse(decodeURIComponent(options.encodedData)); console.log(json.isActive); // => true console.log(typeof json.isActive); // => "boolean" console.log(json.isActive === true); // => true }, }); [代码] 这样使用起来不方便,我们封装一下,新建文件[代码]router/index.js[代码]: [代码]const routes = require('./routes.js'); function navigateTo({ url, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } function extract(options) { return JSON.parse(decodeURIComponent(options.encodedData)); } module.exports = { routes, navigateTo, extract, }; [代码] 页面中我们这样来使用: [代码]const router = require('../../router/index.js'); // page home Page({ onLoad(options) { router.navigateTo({ url: router.routes.uc, data: { isActive: true, }, }); }, }); // page uc Page({ onLoad(options) { const json = router.extract(options); console.log(json.isActive); // => true console.log(typeof json.isActive); // => "boolean" console.log(json.isActive === true); // => true }, }); [代码] route name 这样貌似还不错,但是[代码]router.navigateTo[代码]不太好记,[代码]router.routes.uc[代码]有点冗长,我们考虑把[代码]navigateTo[代码]换成简单的[代码]push[代码],至于路由,我们可以使用[代码]name[代码]的方式来替换原来[代码]url[代码]参数: [代码]const routes = require('./routes.js'); function push({ name, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); const url = routes[name]; wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } function extract(options) { return JSON.parse(decodeURIComponent(options.encodedData)); } module.exports = { push, extract, }; [代码] 在页面中使用: [代码]const router = require('../../router/index.js'); Page({ onLoad(options) { router.push({ name: 'uc', data: { isActive: true, }, }); }, }); [代码] navigateTo or switchTab 页面跳转除了navigateTo之外还有switchTab,我们是不是可以把这个差异抹掉?答案是肯定的,如果我们在配置routes的时候就已经指定是普通页面还是tab页面,那么程序完全可以切换到对应的跳转方式。 我们修改一下[代码]router/routes.js[代码],假设home是一个tab页面: [代码]module.exports = { // 主页 home: { type: 'tab', path: '/pages/index/index', }, uc: { path: '/pages/a/index', }, }; [代码] 然后修改[代码]router/index.js[代码]中[代码]push[代码]的实现: [代码]function push({ name, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); const route = routes[name]; if (route.type === 'tab') { wx.switchTab({ url: `${route.path}`, // 注意tab页面是不支持传参的 }); return; } wx.navigateTo({ url: `${route.path}?encodedData=${dataStr}`, }); } [代码] 搞定,这样我们一个[代码]router.push[代码]就能自动切换两种跳转方式了,而且之后一旦页面类型有变动,我们也只需要修改[代码]route[代码]的定义就可以了。 直接寻址 alias用着很不错,但是有一点挺麻烦得就是每新建一个页面都要写一个alias,即使没有别名的需要,我们是不是可以处理一下,如果在alias没命中,那就直接把name转化成url?这也是阔以的。 [代码]function push({ name, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); const route = routes[name]; const url = route ? route.path : name; if (route.type === 'tab') { wx.switchTab({ url: `${url}`, // 注意tab页面是不支持传参的 }); return; } wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } [代码] 在页面中使用: [代码]Page({ onLoad(options) { router.push({ name: 'pages/user_center/a/index', data: { isActive: true, }, }); }, }); [代码] 注意,为了方便维护,我们规定了每个页面都必须存放在一个特定的文件夹,一个文件夹的当前路径下只能存在一个index页面,比如[代码]pages/index[代码]下面会存放[代码]pages/index/index.js[代码]、[代码]pages/index/index.wxml[代码]、[代码]pages/index/index.wxss[代码]、[代码]pages/index/index.json[代码],这时候你就不能继续在这个文件夹根路径存放另外一个页面,而必须是新建一个文件夹来存放,比如[代码]pages/index/pageB/index.js[代码]、[代码]pages/index/pageB/index.wxml[代码]、[代码]pages/index/pageB/index.wxss[代码]、[代码]pages/index/pageB/index.json[代码]。 这样是能实现功能,但是这个name怎么看都跟alias风格差太多,我们试着定义一套转化规则,让直接寻址的name与alias风格统一一些,[代码]pages[代码]和[代码]index[代码]其实我们可以省略掉,[代码]/[代码]我们可以用[代码].[代码]来替换,那么原来的name就变成了[代码]user_center.a[代码]: [代码]Page({ onLoad(options) { router.push({ name: 'user_center.a', data: { isActive: true, }, }); }, }); [代码] 我们再来改进[代码]router/index.js[代码]中[代码]push[代码]的实现: [代码]function push({ name, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); const route = routes[name]; const url = route ? route.path : `pages/${name.replace(/\./g, '/')}/index`; if (route.type === 'tab') { wx.switchTab({ url: `${url}`, // 注意tab页面是不支持传参的 }); return; } wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } [代码] 这样一来,由于支持直接寻址,跳转home和uc还可以写成这样: [代码]router.push({ name: 'index', // => /pages/index/index }); router.push({ name: 'user_center', // => /pages/user_center/index }); [代码] 这样一来,除了一些tab页面以及特定的路由需要写alias之外,我们也不需要新增一个页面就写一条alias这么麻烦了。 其他 除了上面介绍的navigateTo和switchTab外,其实还有[代码]wx.redirectTo[代码]、[代码]wx.navigateBack[代码]以及[代码]wx.reLaunch[代码]等,我们也可以做一层封装,过程雷同,所以我们就不再一个个介绍,这里贴一下最终简化后的api以及原生api的映射关系: [代码]router.push => wx.navigateTo router.replace => wx.redirectTo router.pop => wx.navigateBack router.relaunch => wx.reLaunch [代码] 最终实现已经在发布在github上,感兴趣的朋友可以移步了解:mp-router。
2019-04-26 - docker快速入门
1. 介绍docker是什么Docker使用go基于linux lxc(linux containers)技术实现的开源容器,诞生于2013年年初,最开始叫dotcloud公司,13年年底改名为docker inc。 2017年下载次数达到了百亿次,估值达13亿美元,通过对应用封装(Packaging)、分发(Distribution)、部署(Deployment)、运行(Runtime)全生命周期管理,达到“一次封装,到处运行” [图片] 为何使用docker?Docker直译码头工人,将各种大小和形状的物品装进船里。这对从事软件行业的人来说,听起来很熟悉,花了大量时间和精力把一个应用放在另一个应用里 [图片] docker出现之前,对不同环境的安装、配置、维护工作量很多,如部署,配置文件,crontab,依赖等等。 使用docker,无需关心环境,只需要一些配置就能构建镜像,而部署则用一条run命令 [图片] 虚拟机 vs 容器 虚拟机需要有额外的虚拟机管理应用和虚拟机操作系统层,操作系统层不仅占用空间而且运行速度也相对慢 docker容器是在本机操作系统层面上实现虚拟化,因此很轻量,速度接近原生系统速度 [图片] 虚拟机启动速度是分钟级别,性能较弱、内存和硬盘占用大,一个物理机最多跑几十个虚拟机,但它的隔离性比较好。 docker启停都是秒级实现,内存和硬盘占用非常小,单机支持上千个容器,在ibm服务器上可运行上万个容器 容器跟虚机相比,有着巨大的优势 [图片] docker优点 只关心应用:以往我们需要关心操作系统、软件、项目,有了docker我们可以只关心应用而不是操作系统,docker发展迅速,基于docker的paas平台也层出不穷,使得我们能更方便的使用docker 快速交付:docker可在秒级提供沙箱环境,开发,测试,运维使用完全相同的环境来部署代码 微服务:docker有助于将一个复杂系统分解,让用户用更离散的方式思考服务 离线开发:将服务编排在笔记本中移动办公,使用docker可在本机秒级别启动一个本地开发环境 降低调试成本:在测试和上线时产生无效的类、有问题的依赖、缺少的配置等问题,docker可让一个问题调试和环境重现变得更简单 CD:docker让持续交付实现变得更容易,特别是对于蓝绿部署就更简单。 第一版上线时,需要上第二版新功能,两个版本功能会有冲突,这时用docker实现蓝绿部署就非常方便了 如:可以部署两个版本同时在线,新版本测试没问题了把老版本流量切到新版本就可以了 迁移:可以很快的迁移到其他云或服务器 与传统虚拟机方式相比,容器化方式在很多场景下都是存在极为明显的优势。无论是开发、测试、运维都应该尽快掌握docker,尽早享受其带来的巨大便利 [图片] 容器化方式在很多场景下都有极大的优势。无论是开发、测试、运维都应该尽快掌握docker,尽早享受其带来的巨大便利 [图片] 概念再来了解docker非常关键的概念,这样才能理解docker容器整个生命周期 [图片] 概念—镜像 镜像(类)=文件系统+数据,我常常用开发语言中的类比作镜像,对象比作容器 镜像由多个层加上一些docker元数据组成,容器运行着由镜像定义的系统 [图片] 概念—容器容器(对象)=镜像运行实例 容器是镜像的运行实例,可以使用同一个镜像运行多个实例。如图所示,一个ubuntu docker镜像产生了三个ubuntu容器,docker利用容器运行和隔离应用 [图片] 从读写角度来说,镜像是只读的,容器是在镜像上添加了一层可读写的文件系统 [图片] [图片] 概念—层层=文件变更集合 像传统虚机应用,每个应用都需要拷贝一份文件副本,运行成百上千上磁盘空间会迅速耗光,而docker采用写时复制来减少磁盘空间,当一个运行中的容器要写入一个文件时,它会把该文件复制到新区域来记录这次的修改,在执行docker提交时将这次修改记录下并产生一个新的层。docker分层解决大规模使用容器时碰到的磁盘和效率问题 [图片] 概念—仓库docker借鉴了大量git优秀的经验。docker仓库分公有库和私有库,最大的公开仓库是docker hub,国内也有很多仓库源 [图片] 2. 创建第一个docker应用通过创建一个docker应用来看看docker是怎么方便使用的 创建docker镜像方式 创建docker有四种方式 [图片] 但最常用的docker命令+手工提交和Dockerfile的方式 [图片] 对于我们来说Dockerfile是最常用也是最有用的 “dockerfile” [图片] 那创建一个docker应用只需要三步:编写dockerfile、构建镜像、运行容器 编写dockerfile那我们就开始用dockerfile来创建一个应用 Dockerfile是包含一系列命令的文本文件,这个文件包含6条命令 1、FROM是使用php官方镜像,左边是镜像名字,右边是标签名字,标签名字不写默认是latest 2、声明维护人员 3、RUN运行一条linux命令,我们把php代码重定向到/tmp/index.php 4、EXPOSE声明要开放的端口 5、WORKDIR启动容器后默认目录 6、CMD容器启动后,默认执行的命令,相当于应用的入口,用php自带的webserver监听8000 [图片] 构建镜像使用docker build命令生成镜像,—tag指定镜像的名字,左边是名字,右边是标签,最后有个.表示在当前目录查找Dockerfile 可以看到,每个命令都会有个输入输出,输入是命令,输出是给到层的id,所以,基本上每个命令都会产生一个层 最后提示镜像构建成功,并打上镜像标签 [图片] 运行容器第三,使用docker run命令运行镜像,-p将容器的8000端口映射到本机8000端口,—name给容器起个名字 用curl对本机8000端口请求,服务器返回当前时间,说明我们构建的容器运行成功了 [图片] 请求本地8000端口,服务器返回当前时间 [图片] dockerfile常用命令其实Dockerfile常用命令就5个:from、add、run、workdir、cmd 创建docker应用步骤编写dockerfile 构建镜像 运行容器 使用docker应用步骤拉取镜像 运行容器 dockerfile最佳实践精简镜像用途 尽量让每个镜像的用途单一 选择合适基础镜像 选择以alpine、busybox等基础的镜像 busybox:号称操作系统里的瑞士军刀,只有……这么大,但却有一百多常用命令 如果你的目标是小而精,busybox是首选,因为它已经精简到没有bash,使用的是ash,一个兼容posix的shell [图片] Alpine:你的目标是小但是又有一些工具的话,可以选择alpine,它是一个面向安全的轻量linux发行版,它关注安全、性能和资源效能,比busybox功能更完善,还提供apk查询和安装软件包,大小只有2-3兆 [图片] 很多官方的镜像都有alpine的镜像,像刚刚使用的php镜像 [图片] 提供维护者信息 正确使用版本 使用明确的版本号,而非依赖于默认的latest,避免环境不一致导致的问题 [图片] 删除临时文件 如安装软件后的安装包,如上图2、3步骤 提高生成速度 如内容不变的指令尽量放在前面,这样可以复用 减少镜像层数 多条命令写在一起,使生成的镜像层数少,如上图2、3步骤 恰当使用multi-stage 保证最终生成镜像最小化 3. 常用命令search想使用一个镜像,用这个命令就可以了,默认按评分排序 official如果是ok表示是官方镜像 Auto标示它是否用dickerfile进行自动化镜像构建 [图片] pull一旦确定一个镜像,通过对其名称执行docker pull来下载 标签默认是latest,严格来讲,镜像的仓库名还应该添加仓库地址的,默认是registry.hub.docker.com Docker images命令查找下载的镜像 [图片] run使用docker run运行一个容器,it表示用交互式方式运行,最后表示要执行的命令 [图片] 其实更常用的方式是以后台方式来执行,这时用d参数在后台运行 运行后用exec命令进去到容器 [图片] tagDocker tag给镜像一个新tag名字 Docker images查看centos镜像,把centos:latest打上centos:yeedomliu,这时再看会有3个centos,latest和yeedomliu的镜像id是相同的 把centos:yeedomliu删除,再查看latest还会存在,最后用rmi命令删除latest就会真正把latest镜像删除掉 如果相同镜像存在多个标签,只有最后一次的rmi命令会真正删除镜像 [图片] psPs可以查看运行中的容器 [图片] rmi删除一个镜像,同一个镜像id的不同标签的镜像,使用rmi删除最后一个镜像才会真正删除这个镜像 [图片] rm删除docker容器,如果运行中的容器需要加-f [图片] diff容器启动后文件变化情况 [图片] logs查看容器运行后的日志 [图片] cp我们想从容器里面拷贝文件到宿主机,或相反的过程就可以用到cp命令 [图片] container prune随着使用docker时间越长,停止状态下的容器会越来越多,这些都会占据磁盘空间 [图片] image prune未被打标签的镜像可以用image prune命令清理 [图片] system prune/df如果你觉得刚刚两条命令执行起来麻烦,可以用docker system prune一条命令搞定 另外用system df查看docker磁盘空间 [图片] 4. 实战了解了docker基础知识后,可进入相对实战的环节 本地开发 常见问题 架构 优化 本地开发 我们的项目使用了很多服务,如redis/mysql/mongodb等等,如果一个个运行起来,还加上配置,容易出手,也比较麻烦 kitematic:与使用命令行管理本地容器相比,你更想使用图形工具对容器管理,官方推出的容器管理工具,通过它可以查找镜像、创建容器、配置、启停容器等管理 [图片] [图片] 这是配置容器端口和宿主机端口,目录,网络等映射界面 [图片] docker-composecompose定位是“定义和运行多个docker容器的应用”,前身fig,目前仍然兼容fig格式的模板文件。 一条命令可以把一个复杂的应用启动起来 日常工作中,经常碰到多个容器相互完成某项任务 [图片] docker-compose示例1 默认模板文件名叫docker-compose.yml,结构很简单,每个顶级元素为服务名称,次级信息为配置信息 这里使用了redis/mongodb/mysql/nginx镜像,分别给它们映射了本地目录、端口、密码等信息,nginx镜像需要使用redis/mysql等服务,用links命令连接进来 [图片] docker-compose示例2 如果在本地开发,每个项目都可以像之前说的那样配置,这里提供了另外一种做法 我把公共的资源在一开始就启动,每个项目里只启动nginx镜像并关联其它的服务即可 公共服务compose [图片] 项目compose [图片] 常见问题 主进程:docker启动第一个进程称主进程,就是id为1的进程,这个进程退出就意味着容器退出,所以想要使docker作为服务使用,这个进程是不能退出的 expose命令是声明暴露的端口,运行时用-P才会生效。一般ports命令是做真正的端口映射,比较常用 架构 安装了docker的主机,一般在一个私有网络上 1、调用docker客户端可以从守护进程获取信息或发送指令 2、docker守护进程使用http协议接收来自docker客户端的请求 3、私有docker注册中心存储docker镜像 4、docker hub是由docker公司运营的最大的公共注册中心 互联网上也存在其他公共的注册中心 调用 Docker客户端可以从守护进程获取信息或给它发送指令。守护进程是一个服务器,它使用 HTTP协议接收来自客户端的请求并返回响应。相应地,它会向其他服务发起请求来发送和接收镜像,使用的同样是 HTTP协议。该服务器将接收来自命令行客户端或被授权连接的任何人的请求。守护进程还负责在幕后处理用户的镜像和容器,而客户端充当的是用户与 REST风格 API之间的媒介。 理解这张图的关键在于,当用户在自己的机器上运行 Docker时,与其进行交互的可能是自己机器上的另一个进程,或者甚至是运行在内部网络或互联网上的服务。 [图片] 优化 使用小镜像:一般来说,使用小的镜像都相对比较优秀,如官方的镜像基本上都有基于alpine的镜像 事后清理:删除镜像里软件包或一些临时文件,减小镜像大小 命令写一行:多个命令尽量写在一起有助于减少层数,也会减少镜像的大小 脚本安装:使用脚本进行初始化时,可以有效减少dockerfile的命令,同时带来另外的问题,可读性不好并且构建镜像时缓存不了 扁平化镜像:构建镜像过程中,可能会涉及到一些敏感信息,或者用了上面的办法镜像依然很大,可以试试这个办法 docker export 容器名或容器id | docker import - 镜像标签 multi-stage:从docker 17.05版本开始,docker支持multi-stage(多阶段构建),特别适合编译型语言,如我在一个镜像下编译,在另外一个很小的系统运行,如下图,go项目在golang环境下编译,在alpine环境下运行 [图片]
2019-03-27 - mpvue+云开发,纯前端实现婚礼邀请函小程序
作者:黄彬 前言 感谢OnceLove提供的思路,借助他的小程序的界面UI风格,自己重新用mpvue实现了属于自己的婚礼邀请函,前前后后花了3天时间。 在这之前本人是没想过要自己实现这样一个项目,原因是后台那块是个麻烦事,所以当媳妇让我自己实现这个邀请函的时候,起初我是拒绝的。后面由于快要过年了,公司项目没有重大更新,趁着这段空闲时间,自己研究了下小程序自带的云开发,无需后台支持,前后端都可以自己来实现,下面我将一一介绍本项目的实现过程 ! 本小程序为婚礼正式使用的小程序,婚期将至,感兴趣的可以扫码体验本项目,沾沾喜气,欢迎大家体验,有什么问题可以在本文给我留言。 [图片] 源码地址: https://gitee.com/roberthuang123/wedding 准备工作 mpvue框架 mpvue官方文档 http://mpvue.com/mpvue/ 小程序·云开发 小程序·云开发文档 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/basis/getting-started.html 注意:使用mpvue前,首先你得先熟悉vue框架的基本使用 项目结构介绍 [图片] common目录: 放一些公共资源,如js,css,json components目录:组件相关的.vue文件都放在这里 pages目录:所有页面都放在这个目录 utils目录:使用mpvue时自动生成,可忽略 app.json文件: [代码]{ "pages": [ "pages/index/main", "pages/photo/main", "pages/map/main", "pages/greet/main", "pages/message/main" ], "window": { "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#fff", "navigationBarTitleText": "WeChat", "navigationBarTextStyle": "black" }, "tabBar": { "color": "#ccc", "selectedColor": "#ff4c91", "borderStyle": "white", "backgroundColor": "#ffffff", "list": [ { "pagePath": "pages/index/main", "iconPath": "static/images/1-1.png", "selectedIconPath": "static/images/1-2.png", "text": "邀请函" }, { "pagePath": "pages/photo/main", "iconPath": "static/images/2-1.png", "selectedIconPath": "static/images/2-2.png", "text": "甜蜜相册" }, { "pagePath": "pages/map/main", "iconPath": "static/images/3-1.png", "selectedIconPath": "static/images/3-2.png", "text": "酒店导航" }, { "pagePath": "pages/greet/main", "iconPath": "static/images/4-1.png", "selectedIconPath": "static/images/4-2.png", "text": "好友祝福" }, { "pagePath": "pages/message/main", "iconPath": "static/images/5-1.png", "selectedIconPath": "static/images/5-2.png", "text": "留言评论" } ] }, "requiredBackgroundModes": ["audio"] } [代码] App.vue文件:本人主要是为了增加项目更新后的提醒,所以在这个文件加了些相关内容,内容如下: [代码]<script>export default { onLaunch () { // 检测小程序是否有新版本更新 if (wx.canIUse('getUpdateManager')) { const updateManager = wx.getUpdateManager() updateManager.onCheckForUpdate(function (res){ // 请求完新版本信息的回调 if (res.hasUpdate) { updateManager.onUpdateReady(function () { wx.showModal({ title: '更新提示', content: '新版本已经准备好,是否重启应用?', success: function (res) { if (res.confirm) { // 新的版本已经下载好,调用 applyUpdate 应用新版本并重启 updateManager.applyUpdate() } } }) }) // 小程序有新版本,会主动触发下载操作(无需开发者触发) wx.getUpdateManager().onUpdateFailed(function () { // 当新版本下载失败,会进行回调 wx.showModal({ title: '提示', content: '检查到有新版本,下载失败,请检查网络设置', showCancel: false }) }) } }) } else { // 版本过低则无法使用该方法 wx.showModal({ title: '提示', confirmColor: '#5BB53C', content: '当前微信版本过低,无法使用该功能,请升级到最新微信版本后重试。' }) } }} </script> <style lang="stylus"> page height 100% image display block </style> [代码] main.js文件: [代码]import Vue from 'vue' import App from './App' Vue.config.productionTip = falseApp.mpType = 'app' wx.cloud.init({ env: '云开发环境ID' }) const app = new Vue(App) app.$mount() [代码] functions目录:主要放一些云函数,这里不清楚云函数的文章后面会提及 images目录:主要放一些静态资源图片 页面介绍 首页——邀请函 首页着重和大家讲解下背景音乐的实现方法 const audioCtx = wx.createInnerAudioContext() 首先,wx.createInnerAudioContext 接口获取实例 接着,通过实例的相关方法来实现音乐的播放与暂停功能 关于小程序音频相关文档 https://developers.weixin.qq.com/miniprogram/dev/api/InnerAudioContext.html 具体代码如下: [代码]<script> import IndexSwiper from 'components/indexSwiper' import tools from 'common/js/h_tools' const audioCtx = wx.createInnerAudioContext() export default { name: 'Index', components: { IndexSwiper }, data () { return { isPlay: true, list: [] } }, onShow () { const that = this that.isPlay = true that.getMusicUrl() }, methods: { audioPlay () { const that = this if (that.isPlay) { audioCtx.pause() that.isPlay = false tools.showToast('您已暂停音乐播放~') } else { audioCtx.play() that.isPlay = true tools.showToast('背景音乐已开启~') } }, getList () { const that = this const db = wx.cloud.database() const banner = db.collection('banner') banner.get().then(res => { that.list = res.data[0].bannerList }) }, getMusicUrl () { const that = this const db = wx.cloud.database() const music = db.collection('music') music.get().then(res => { let musicUrl = res.data[0].musicUrl audioCtx.src = musicUrl audioCtx.loop = true audioCtx.play() that.getList() }) } }, onShareAppMessage: function (res) { return { path: '/pages/index/main' } } } </script> [代码] 以上代码中使用到了云开发相关功能,文章后面会介绍,请大家稍安勿躁 相册页——就一个轮播图,这里就不过多介绍 地图页——这里着重讲一下地图标签map map标签 关于map组件的使用文档 这里讲一下标记点markers [代码]data () { return { // qqSdk: '', markers: [{ iconPath: '../../static/images/nav.png', id: 0, latitude: 30.08059, longitude: 115.93027, width: 50, height: 50 }] } } [代码] [代码]<template> <div class="map"> <image mode="aspectFit" class="head-img" src="../../static/images/t1.png"/> <map class="content" id="map" longitude="115.93027" latitude="30.08059" :markers="markers" scale="18" @tap="toNav"> </map> <div class="call"> <div class="left" @tap="linkHe"> <image src="../../static/images/he.png"/> <span>呼叫新郎</span> </div> <div class="right" @tap="linkShe"> <image src="../../static/images/she.png"/> <span>呼叫新娘</span> </div> </div> <image class="footer" src="../../static/images/grren-flower-line.png"/> </div> </template> [代码] 祝福页——也是云开发相关内容,后面会介绍 留言页——也是云开发相关内容,后面会介绍 云开发介绍 project.config.json文件: [代码]"cloudfunctionRoot": "static/functions/" [代码] 进行云开发首先我们需要找到上面这个文件,在上面这个json文件中加上上面这句 [代码]cloudfunctionRoot[代码] 用于指定存放云函数的目录 app.json文件: [代码]"window": { "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#fff", "navigationBarTitleText": "WeChat", "navigationBarTextStyle": "black" }, "cloud": true [代码] 增加字段 [代码]"cloud":true实现云开发能力的兼容性[代码] 开通云开发 在开发者工具工具栏左侧,点击 “云开发” 按钮即可开通云开发 云开发控制台 [图片] 数据库 云开发提供了一个 JSON 数据库 存储 云开发提供了一块存储空间,提供了上传文件到云端、带权限管理的云端下载能力,开发者可以在小程序端和云函数端通过 API 使用云存储功能。 云函数 云函数是一段运行在云端的代码,无需管理服务器,在开发工具内编写、一键上传部署即可运行后端代码。 下面开始讲解使用云开发的过程: 云能力初始化,在小程序端开始使用云能力前,需先调用 [代码]wx.cloud.init[代码] 方法完成云能力初始化 [代码]import Vue from 'vue' import App from './App' Vue.config.productionTip = false App.mpType = 'app' wx.cloud.init({ env: '云开发环境ID' }) const app = new Vue(App) app.$mount() [代码] 数据库的使用: 在开始使用数据库 API 进行增删改查操作之前,需要先获取数据库的引用。 以下调用获取默认环境的数据库的引用: [代码]const db = wx.cloud.database() [代码] 要操作一个集合,需先获取它的引用: [代码]const todos = db.collection('todos') [代码] 接下来是操作数据库的相关示例: 例:首页获取背景音乐资源 [代码]getMusicUrl () { const that = this const db = wx.cloud.database() const music = db.collection('music') music.get().then(res => { let musicUrl = res.data[0].musicUrl audioCtx.src = musicUrl audioCtx.loop = true audioCtx.play() that.getList() }) } [代码] 例:首页获取轮播图数组 [代码]getList () { const that = this const db = wx.cloud.database() const banner = db.collection('banner') banner.get().then(res => { that.list = res.data[0].bannerList }) } [代码] 例:祝福页,用户送上祝福存储用户 [代码]<script>import tools from 'common/js/h_tools' export default { name: 'Greet', data () { return { userList: [], openId: '', userInfo: '' } }, onShow () { const that = this that.getUserList() }, methods: { scroll (e) { console.log(e) }, sendGreet (e) { const that = this if (e.target.errMsg === 'getUserInfo:ok') { wx.getUserInfo({ success: function (res) { that.userInfo = res.userInfo that.getOpenId() } }) } }, addUser () { const that = this const db = wx.cloud.database() const user = db.collection('user') user.add({ data: { user: that.userInfo } }).then(res => { that.getUserList() }) }, getOpenId () { const that = this wx.cloud.callFunction({ name: 'user', data: {} }).then(res => { that.openId = res.result.openid that.getIsExist() } ) }, getIsExist () { const that = this const db = wx.cloud.database() const user = db.collection('user') user.where({ _openid: that.openId }).get().then(res => { if (res.data.length === 0) { that.addUser() } else { tools.showToast('您已经送过祝福了~') } }) }, getUserList () { const that = this wx.cloud.callFunction({ name: 'userList', data: {} }).then(res => { that.userList = res.result.data.reverse() }) } } } </script> [代码] 进入祝福页,首先我们需要获取送祝福的好友列表 [代码]getUserList () { const that = this wx.cloud.callFunction({ name: 'userList', data: {} }).then(res => { that.userList = res.result.data.reverse() }) } [代码] 这里用到了云函数,之所以用云函数是因为小程序端在获取集合数据时服务器一次默认并且最多返回 20 条记录,云函数端这个数字则是 100。 下面给大家看看云函数的使用方法: 上面我们讲过在project.config.json文件中配置云函数存放位置 下面是云函数messageList的index.js文件: [代码]const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() const MAX_LIMIT = 100 exports.main = async (event, context) => { // 先取出集合记录总数 const countResult = await db.collection('message').count() const total = countResult.total // 计算需分几次取 const batchTimes = Math.ceil(total / 100) // 承载所有读操作的 promise 的数组 const tasks = [] for (let i = 0; i < batchTimes; i++) { const promise = db.collection('message').skip(i * MAX_LIMIT).limit(MAX_LIMIT).get() tasks.push(promise) } // 等待所有 return (await Promise.all(tasks)).reduce((acc, cur) => ({ data: acc.data.concat(cur.data), errMsg: acc.errMsg })) } [代码] [图片] 使用云函数前,在开发者工具上,找到messageList目录,右键如图: [图片] 点击上传并部署:云端安装依赖(不上传node_modules) 得到如图的提示: [图片] 安装完点击完成就能使用当前云函数了,使用方法即: [代码]getUserList () { const that = this wx.cloud.callFunction({ name: 'userList', data: {} }).then(res => { that.userList = res.result.data.reverse() }) } [代码] 数组之所以要倒序是因为希望新祝福的的用户在最前面显示 接着是用户送上祝福的时候存储用户 这里我们用到了云函数获取用户信息, 当小程序端调用云函数时,云函数的传入参数中会被注入小程序端用户的 openid,开发者无需校验 openid 的正确性,因为微信已经完成了这部分鉴权,开发者可以直接使用该 openid 下面是云函数user的index.js文件: [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() // 云函数入口函数 exports.main = async (event, context) => { const wxContext = cloud.getWXContext() return { event, openid: wxContext.OPENID, appid: wxContext.APPID, unionid: wxContext.UNIONID } } [代码] 主要是为了获取当前操作用户的openid,获取当前用户的openid方法: [代码]getOpenId () { const that = this wx.cloud.callFunction({ name: 'user', data: {} }).then(res => { that.openId = res.result.openid that.getIsExist() }) } [代码] 接着判断当前用户是否已经存在于数据库中,即getIsExist()方法: [代码]getIsExist () { const that = this const db = wx.cloud.database() const user = db.collection('user') user.where({ _openid: that.openId }).get().then(res => { if (res.data.length === 0) { that.addUser() } else { tools.showToast('您已经送过祝福了~') } }) } [代码] 如果得到的数组长度为零则添加改用户到数据库,否则则提醒当前用户已经送过祝福 接下来介绍存储用户信息的方法,即addUser(): [代码]addUser () { const that = this const db = wx.cloud.database() const user = db.collection('user') user.add({ data: { user: that.userInfo } }).then(res => { that.getUserList() }) } [代码] 存入到数据库的信息是这样的: [图片] 有人要看数据库字段,其实很简单,这是首页轮播图的字段,见下图: [图片] 这是留言的评论,如图: [图片] 总结 在这里给大家讲解几点细节: 云开发是免费的; 数据库集合名首先必须要建立; 数据库集合为空时,这里本人没有处理细节,会报错,先通过使用自己的小程序写入一条数据,错误就可以解决了; 云函数新建后一定要创建并安装依赖; 如果担心有些评论不太友好希望有删除功能,可以给自己开放权限,在每条评论加一个删除按钮,仅自己使用的时候通过唯一的openid控制删除按钮为可见,其他人不可见,从而来管理留言列表; 数据库建立集合后记得将权限放开,如下图: [图片] 最后总结: 除了一些静态资源放在项目中,其他资源建议一律存储在云开发-存储管理中,这样的话,方便更换资源而不用再次提交等待小程序审核,更换后的资源可以立即生效; 我知道大家都喜欢拿来就能直接看到同样效果的东西,本项目因为涉及到后台数据,不可能完全把本人的环境放出来给大家使用,建议真的要实现相关功能或类似项目的小伙伴对照我的思路重新做一个属于自己的作品才最有意义。
2019-02-27 - 小程序原生高颜值组件库--ColorUI
[图片] 简介 ColorUI是一个Css类的UI组件库!不是一个Js框架。相比于同类小程序组件库,ColorUI更注重于视觉交互! 浏览GitHub:https://github.com/weilanwl/ColorUI [图片] 如何使用? 先下载源码包 → Github 引入到我的小程序 将 /demo/ 下的 colorui.wxss 和 icon.wxss 复制到小程序的根目录下 在 app.wxss 引入两个文件 [代码]@import "icon.wxss"; @import "colorui.wxss"; [代码] 使用模板全新开发 复制 /template/ 文件夹并重命名为你的项目,微信开发者工具导入为小程序就可以使用ColorUI了 体验沉浸式导航 [图片] App.js 获取系统参数并写入全局参数。 [代码]//App.js App({ onLaunch: function() { wx.getSystemInfo({ success: e => { this.globalData.StatusBar = e.statusBarHeight; let custom = wx.getMenuButtonBoundingClientRect(); this.globalData.Custom = custom; this.globalData.CustomBar = custom.bottom + custom.top - e.statusBarHeight; } }) } }) [代码] Page.js 页面配置获取全局参数。 [代码]//Page.js const app = getApp() Page({ data: { StatusBar: app.globalData.StatusBar, CustomBar: app.globalData.CustomBar, Custom: app.globalData.Custom } }) [代码] Page.wxml 页面构造导航。更多导航样式请下载Demo查阅 操作条组件。 [代码]<view class="cu-custom" style="height:{{CustomBar}}px;"> <view class="cu-bar fixed bg-gradual-pink" style="height:{{CustomBar}}px;padding-top:{{StatusBar}}px;"> <navigator class='action border-custom' open-type="navigateBack" delta="1" hover-class="none" style='width:{{Custom.width}}px;height:{{Custom.height}}px;margin-left:calc(750rpx - {{Custom.right}}px)'> <text class='icon-back'></text> <text class='icon-homefill'></text> </navigator> <view class='content' style='top:{{StatusBar}}px;'>操作条</view> </view> </view> [代码] 自定义系统Tabbar [图片] 按照官方 自定义 tabBar 配置好Tabbar (开发工具和版本库请使用最新版)。 使用ColorUI配置Tabbar只需要更改 Wxml 页的内容即可。 更多Tabbar样式请下载Demo查阅 操作条组件。 /custom-tab-bar/index.wxml [代码] <view class="cu-bar tabbar bg-white shadow"> <view class="action" wx:for="{{list}}" wx:key="index" data-path="{{item.pagePath}}" data-index="{{index}}" bindtap="switchTab"> <view class='icon-cu-image'> <image src='{{selected === index ? item.selectedIconPath : item.iconPath}}' class='{{selected === index ? "animation" : "animation"}}'></image> </view> <view class='{{selected === index ? "text-green" : "text-gray"}}'>{{item.text}}</view> </view> </view> [代码] 作者叨叨 ColorUI是一个高度自定义的Css样式库,包含了开发常用的元素和组件,元素组件之间也能相互嵌套使用。我也会不定期更新一些扩展到源码。 其实大家都在催我写文档,但这个库源码就在这,所见即所得,粘贴复制就可以得到你想要的页面。当然,文档我还是要写的,也希望大家多多提意见。 现在前端的开发方向基本都是奔着Js方向的,布局和样式大家讨论的有点少。以后我会在开发者社区多聊一聊关于开发中的布局和样式。 [图片] 感谢阅读。
2019-02-26