<style scoped> .self-repair{ margin-top: 16px; } </style>
.self-repair>>> .van-button__text{ color: red; } .self-repair/deep/ .van-button__text{
color: red; }
<style module> .selfRepair{ margin-top: 16px; } </style>
<div :class="$style.selfRepair"> 提交 </div>
useEffect(()=>{ console.log('mount'); return ()=>{ console.log('did update done') } },[name])当name的值改变的时候,会重新mount,这个方法在项目中有很多处的应用,比如搜索表格数据,name即搜索条件,当name重新被赋值的时候即重新拉取表格数据,如果有多个搜索条件的时候也可以加到依赖中. 副作用函数里的return则是在页面被卸载时调用. useEffect相当于老版本class里的componentDidMount,componentDidUpdate,componentWillUnmount等生命周期的集合,这个hooks在项目中属于基本的应用,也是必须掌握的. memo 这个hook是针对组件的,减少组件的不必要更新. 子组件
const TextCell = memo(function(props:any) { console.log('我重新渲染了') return ( <p onClick={props.click}>ffff</p> ) }) 父组件 const fatherComponent = () => {
const [number,setNumber] = useState(0);
return( <div> 模块{number} <TextCell/> <Button onClick={()=>setNumber(number => number + 1)}>加加加</Button> </div> ) }在这里如果没有用到memo 每次父组件重新setNumber,子组件都会重新渲染一次,加上了后只会在初始化的时候渲染,减少了子组件渲染的次数,这个在之前老的class方法,要减少子组件的渲染,可以使用pureComponent,或者在生命周期 componentWillReciveProps方法里返回false即可. useCallback 这个hook主要是针对函数的,为什么会有这个hook呢? 比如: 子组件
const TextCell = memo(function(props:any) { console.log('我重新渲染了') return ( <p onClick={props.click}>ffff</p> ) }) 父组件 const fatherComponent = () => {
const [number,setNumber] = useState(0); const handleClick = useCallback(()=>{ console.log(33) },[])
return( <div> 模块{number} <TextCell click={handleClick}/> <Button onClick={()=>setNumber(number => number + 1)}>加加加</Button> </div> ) }这里如果不使用useCallback,哪怕子组件用memo包裹了 也还是会更新子组件,因为子组件的绑定的函数click在父组件更新的时候也会更新引用地址,导致子组件的更新,但是这个其实是没必要的更新,绑定的函数并不需要子组件更新,useCallback就是阻止这类没必要的更新而存在的. 如果是用class的方法的话,需要在constructor里绑定函数,会比较麻烦,不易阅读. 这里需要注意的是 如果是有参数需要传递,则需要这样写 <TextCell click={useCallback(()=>handleClick('传递的参数'),[])}/> useMemo useMemo会在页面初始化的时候执行一次,并把执行的结果缓存一份
const fatherComponent = () => { function expensiveFn() { let result = 0; for (let i = 0; i < 10000; i++) { result += i; } console.log(result) return result; } const base = useMemo(expensiveFn, [number]); return ( <div>{base}</div> ) }在这个地方如果没有使用useMemo而是 直接用 const base = expensiveFn(),则在页面每次刷新的时候都会重新调用这个方法,影响页面的性能,而如果使用上了useMemo,react会把expensiveFn的执行结果缓存一份,即使页面刷新了,也不会再执行expensiveFn函数.useCallback和useMemo的区别其实就是前者是缓存过程,而后者是缓存结果.这个在页面上有时候会应用到select里的选项数据源的上下级联动.这里的数据源一般不会改变. 最后总结: 这里就说这么多了,16版本的react在很多写法真的做了很多升级,不适应也是很正常的,老的方式在性能上其实也不会很差,如果追求性能极限以上的内容都是可以参考的,掌握以上的内容react新版本的功能便可以掌握70%了,剩余的一些用法其实并不常用,像自定义hook,useContext,useRecduer
brew install lrzsz
2.在本地/usr/local/bin/目录下保存iterm2-send-zmodem.sh 和iterm2-recv-zmodem.sh两个脚本
3.设置一下两个脚本的权限,一般 chmod 777 就行了
chmod 777 /usr/local/bin/iterm2-*
1.第一条
Regular expression: rz waiting to receive.\*\*B0100
Action: Run Silent Coprocess
Parameters: /usr/local/bin/iterm2-send-zmodem.sh
Instant: checked
2.第二条
Regular expression: \*\*B00000000000000
Action: Run Silent Coprocess
Parameters: /usr/local/bin/iterm2-recv-zmodem.sh
Instant: checked
var baseWidth = 1080,baseHeight = 2400; var height = device.height; var width = device.width; setScreenMetrics(baseWidth,baseHeight); threads.start(function(){ events.observeKey(); events.on("key_down", function(keyCode, events){ if(keyCode == keys.volume_up){ toastLog('停止脚本使用') exit(); } }); }); var canCapture = requestScreenCapture(); if(!canCapture){ toast('请授权截图!') exit() } launchApp('手机淘宝') sleep(3000); click(820,860); sleep(6000); let catImg = images.read('/sdcard/catImg.jpg'); if(catImg === null){ catImg = images.load('https://pic1.zhimg.com/80/v2-8a9af44ba48b553fbbdafd96475ad47c_1440w.jpeg'); // 图片不能为png格式的 无法加载到 catImg.saveTo('/sdcard/catImg.jpg'); } // let screenCaptureImg2 = images.grayscale(screenCaptureImg) // images.save(screenCaptureImg2,'/sdcard/screenCaptureImg2.jpg') sleep(2000); // 如果有11月11日见弹出框 则关闭 var eleven = text('好的,11月11日见').findOnce(); if(eleven){ click('好的,11月11日见'); sleep(2000) } click(942,1973); // 点击 赚喵币 sleep(1500) while(true){ var goText = text('去浏览').findOnce(); if(goText){ click(goText.bounds().centerX(),goText.bounds().centerY()); sleep(5000) swipe(baseWidth/3,baseHeight - 200,baseWidth/2,200,2000); sleep(13000); back(); sleep(2000); click('领取奖励'); sleep(1000) }else{ var goSearch = text('去搜索').findOnce(); if(goSearch){ click(goSearch.bounds().centerX(),goSearch.bounds().centerY()); sleep(5000) swipe(baseWidth/3,baseHeight - 200,baseWidth/2,200,2000); sleep(13000); back(); sleep(2000); click('领取奖励'); sleep(1000); }else{ var array = ['逛一逛"潮酷新品"(0/2)','逛一逛"潮酷新品"(1/2)','逛一逛"潮流趋势"(0/2)','逛一逛"潮流趋势"(1/2)','逛一逛"时尚配饰"(0/2)','逛一逛"时尚配饰"(1/2)','逛一逛"家居百货"(0/2)','逛一逛"家居百货"(1/2)']; var goTo = null; for(var i = 0;i<array.length;i++){ var a = text(array[i]).findOnce(); if(a){ goTo = a; break; } } if(goTo){ click(goTo.bounds().centerX(),goTo.bounds().centerY()); sleep(2000) swipe(baseWidth/3,baseHeight - 200,baseWidth/2,200,2000); sleep(15500); back(); sleep(2000); click('领取奖励'); sleep(1000); }else{ break } } } } toastLog('结束!'); click(974,650); // 点击右上角关闭 exit(); function readImg(name,url){ let catImg = images.read('/sdcard/'+name); if(catImg === null){ catImg = images.load(url); // 图片不能为png格式的 无法加载到 catImg.saveTo('/sdcard/'+name); }; return catImg }这段代码是用于淘宝收取喵币的,前后仅花了2小时不到,这一块内容还是比较简单的,主要之前写过类似的代码,有些经验了. 遇到的问题: 1.点击文字,由于不确定一些字体是图片还是纯文本,所以在调试的时候,都是凭感觉去试 2.活动的文本较多,有些地方情况无法用找图片的方式来定位要点击的位置,但是文字内容又不确定,所以只能把可能的文本内容情况给加上,比如这一块 var array = ['逛一逛"潮酷新品"(0/2)','逛一逛"潮酷新品"(1/2)','逛一逛"潮流趋势"(0/2)','逛一逛"潮流趋势"(1/2)','逛一逛"时尚配饰"(0/2)','逛一逛"时尚配饰"(1/2)','逛一逛"家居百货"(0/2)','逛一逛"家居百货"(1/2)']; 现在是把我可能遇到的情况都加上了,如果后面支付宝有增删改查的话,也需要跟着改一下逻辑... 3.手机兼容,由于自己只有一个手机,只能使用自己的手机来测试,虽然官网有说可以通过方法setScreenMetrics(baseWidth,baseHeight)来保持比例,但是自己没试过 也许还是不行呢. 4.还有各类的点击可能性,并没有完成覆盖,有些去完成的任务只能通过手点来获取喵币. 5.autojs是好早之前的框架,现在已经不维护了,有一些es6的语法是不支持的,比如const,let,尽量不用到新语法来写业务逻辑. 6.各种sleep 需要等待app内容加载完成,如果页面有不确定因素也是需要延长下一步的执行逻辑的. 总结: 小东西有大用处,在接下来的几天时间能够帮助我节省好多的时间,让自己感觉高大上一些,多探究一些小玩意,对自己以后的职业生涯也有帮助.. 另外有兴趣的可以加群一起玩转autojs...
.canvas-box { width: calc(100vw - 120px); height: 100vh; margin-left: 120px; position: relative; .text_warp{ .text-detail{ width: 300px; top: 50%; left: 50%; transform: translate(-50%,-50%) rotate(90deg); position: absolute; font-size: 50px; color: #333; white-space: nowrap; } } }这里没做旋转,只是把高宽控制了一下. 技术点2: 左方的文字区域则做了相应的旋转:
.layout-flex{ display: flex; justify-content: space-between; position: absolute; top: 0; left: 100px; width: 100vh; transform: rotate(90deg); transform-origin:left top; height: 120px; .read{ display: flex; line-height: 100px; font-size: 20px; .at-checkbox{ &:before{ display: none; } &:after{ display: none; } } .at-checkbox__option-wrap{ padding-right: 0; } } .btn-warp{ display: flex; .at-button + .at-button{ margin: 0 10px; } } } 旋转的时候记得配置一下origin,旋转原点,默认是中间的. 技术点3:js部分的代码
import Taro, {Component, Config} from '@tarojs/taro'; import {View, Button, Canvas, Text} from '@tarojs/components'; import {APP_URLS} from '@/utils/api/api_urls' import {upload} from '@/utils/api/httpRequest' import './index.scss' import { AtButton, AtCheckbox } from 'taro-ui'; let ctx = Taro.createCanvasContext('canvas', this); let startX = 0; let startY = 0; let canvasw = 0; let canvash = 0; export default class Signature extends Component { config = { navigationBarTitleText: '电子签名' } state = { isPaint: false, tempFilePath: '', checkedList: ['list1'], isShow: true, // 是否展示文字说明 trustorIndex: 0, } initCanvas() { ctx = Taro.createCanvasContext('canvas', this); ctx.setStrokeStyle('#000000'); ctx.setLineWidth(4); ctx.setLineCap('round'); ctx.setLineJoin('round'); } canvasStart = (e) => { if (startX !== 0) { this.setState({ isShow: false }) } startX = e.changedTouches[0].x startY = e.changedTouches[0].y ctx.beginPath() } canvasMove = (e) => { if (startX !== 0) { this.setState({ isPaint: true, isShow: false }) } let x = e.changedTouches[0].x let y = e.changedTouches[0].y ctx.moveTo(startX, startY) ctx.lineTo(x, y) ctx.stroke(); ctx.draw(true) startX = x startY = y } canvasEnd(e) { console.log('结束') } clearDraw = () => { startX = 0; startY = 0; ctx.clearRect(0, 0, canvasw, canvash); ctx.draw(true); this.setState({ isPaint: false, tempFilePath: '' }) } createImg() { if (!this.state.isPaint) { Taro.showToast({ title: '签名内容不能为空!', icon: 'none' }); return false; } // 生成图片 Taro.canvasToTempFilePath({ canvasId: 'canvas', success: async res => { console.log(res) }, fail(err) { console.log(err) } }) } uploadImage(filePath) { return new Promise((resolve, reject) => { let data = { url: APP_URLS.UPLOAD_FILES, filePath, name: 'file', formDate: {} } upload(data) .then(res => { console.log(res); resolve(res); }) .catch(e => { Taro.showToast({title: '材料上传失败,请稍后再试', icon: 'none'}) }) }) } // 获取 canvas 的尺寸(宽高) getCanvasSize() { const query = Taro.createSelectorQuery() query.select('#canvas').boundingClientRect(function (res) { canvasw = res.width canvash = res.height }) query.exec() } // 重新签名 afreshDraw = () => { this.setState({ canSign: true }) } componentDidMount() { this.getCanvasSize() this.initCanvas() } componentWillUnmount() { ctx = null } handleChange = (checkedList) => { this.setState({ checkedList }) } handleHideWarp = () => { this.setState({ isShow: false }) } render() { return ( <View className="signature"> <View className="canvas-box"> <Canvas id="canvas" canvasId="canvas" className="canvas" disableScroll={true} onTouchStart={this.canvasStart.bind(this)} onTouchMove={this.canvasMove.bind(this)} onTouchEnd={this.canvasEnd.bind(this)} onTouchCancel={this.canvasEnd.bind(this)} onClick={this.handleHideWarp} disable-scroll={true} > </Canvas> { this.state.isShow && <View className='text_warp' onTouchStart={this.handleHideWarp} onClick={this.handleHideWarp}> <Text className='text-detail'>请在此区域签名</Text> </View> } </View> <View className="layout-flex buttons"> <View className='read'> <AtCheckbox options={[ { value: 'list1', label: '', } ]} selectedList={this.state.checkedList} onChange={this.handleChange.bind(this)} /> <Text>我已阅读并同意《电子签名协议》</Text> </View> <View className='btn-warp'> <AtButton onClick={this.clearDraw}>重新签名</AtButton> <AtButton customStyle={{margin: '0 10px'}} type='primary' onClick={this.createImg.bind(this)}>确认签名</AtButton> </View> </View> </View> ); } } 全部copy到微信小程序里即可正常使用. 排坑: 坑1: 旋转的问题,有时候把握不好或者说空间想像能力不够丰富,导致旋转的时候花费了一些时间. 坑2: 上面的placeholder的问题.有想过直接在canvas里绘制出这几个文字,但是会引起其它的问题,比如要保证文字的垂直居中状态,这个时候就得计算文字的长度, 然后绘制到canvas上,这显然有些麻烦. 所以换了另外一种思路,直接一个div覆盖在最上方,开始绘制的时候给隐藏了,这种方式可能不适合所有项目. 坑3: 微信小程序在开发的时候使用真机预览的电子签名绘制的时候会有所卡顿,也不知道大家是否都一样,但是一旦上传到体验版本又是比较流畅的,这个无从去排查问题. 总结: 多尝试项目,可以让自己更好的成长
我们现在需要写一个 foo 函数,这个函数返回首次调用时的 Date 对象,注意是首次。
var t; function foo() { if (t) return t; t = new Date() return t; }
问题有两个,一是污染了全局变量,二是每次调用 foo 的时候都需要进行一次判断。
我们很容易想到用闭包避免污染全局变量。
var foo = (function() { var t; return function() { if (t) return t; t = new Date(); return t; } })();
然而还是没有解决调用时都必须进行一次判断的问题。
函数也是一种对象,利用这个特性,我们也可以解决这个问题。
function foo() { if (foo.t) return foo.t; foo.t = new Date(); return foo.t; }
依旧没有解决调用时都必须进行一次判断的问题。
不错,惰性函数就是解决每次都要进行判断的这个问题,解决原理很简单,重写函数。
var foo = function() { var t = new Date(); foo = function() { return t; }; return foo(); };
DOM 事件添加中,为了兼容现代浏览器和 IE 浏览器,我们需要对浏览器环境进行一次判断:
// 简化写法 function addEvent (type, el, fn) { if (window.addEventListener) { el.addEventListener(type, fn, false); } else if(window.attachEvent){ el.attachEvent('on' + type, fn); } }
问题在于我们每当使用一次 addEvent 时都会进行一次判断。
利用惰性函数,我们可以这样做:
function addEvent (type, el, fn) { if (window.addEventListener) { addEvent = function (type, el, fn) { el.addEventListener(type, fn, false); } } else if(window.attachEvent){ addEvent = function (type, el, fn) { el.attachEvent('on' + type, fn); } } }
当然我们也可以使用闭包的形式:
var addEvent = (function(){ if (window.addEventListener) { return function (type, el, fn) { el.addEventListener(type, fn, false); } } else if(window.attachEvent){ return function (type, el, fn) { el.attachEvent('on' + type, fn); } } })();
当我们每次都需要进行条件判断,其实只需要判断一次,接下来的使用方式都不会发生改变的时候,想想是否可以考虑使用惰性函数。
前言:
在平时开发中难免会使用到子组件的方法,在之前老的class实现方式中,可以通过ref直接获取到子组件的实例,然后调用子组件的方法,但是在函数组件中,却没有实例,你通过ref是无法直接获取到子组件的方法的.
解决方案:
在子组件中实例化函数组件:
const childComponent = React.forwardRef((props,ref) =>{ // useImperativeHandle 在react中引入 useImperativeHandle(ref,()=>{ return { reset:()=>{ form.resetFields() } } }) return ( <div>fff</div> ) })
在父组件中引用方式即与之前用class的方式一样了
背景:
一直想着在网上尝试爬虫服务,在去年的时候,由于自己服务器知识不足,导致自己对服务器运行puppeteer一直出错,后面也不了了之,这段时间又重新捡起来了,为了能够多学一些,多谁知一些.
排坑:
1.服务器无法正常启动无头chrome
await puppeteer.launch({headless:false});
哪怕把headless置为true,也一样无法正常启动
解决方案:
const browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']});
方案可以去github的issues里面搜索一下
加上链接:https://github.com/puppeteer/puppeteer/issues/290
2.ubuntu安装无头浏览器需要各种插件
apt-get install -yq gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
要想正常使用puppeteer需要安装以上插件.可能在其它环境下也是需要安装相同插件.
3.开发中遇到的问题
由于自己想走全套流程,所以也有用到mysql,学习mysql 从不熟悉到慢慢会一点,成就感还是有的.下面就碰到了一个坑:
数据库里已经存在的更新,没有存在的则新加,开始用了on duplicate key update 这个语法,
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '"15414675859332.html","a","b","c","d" on duplicate key update href=values(href),' at line 1
后面替换成 replace into
为什么替换,因为上面那个错误自己一直无法排查出来原因,不管在服务器上的mysql里输入同样的语法,还是在电脑端执行相同的语法均可以正常执行,但是一到服务上就不对了.
这个问题也是排查了很久,后面替换成 replace into后,才慢慢发现了问题:
'replace into headList (imgId,src,alt,href,imgType) values '
后面的values后面是没有值的,为什么没有值?
最开始的时候以为是异步原因,以为是mysql删除了,后面经过排查是puppeteer上的坑,应该说是自己的经验不足,在服务器上拉取数据的时候,由于网络慢的原因,导致有时候数据没有出来 但是js代码已经执行完了,引起values为空的问题,所以在服务器上使用puppeteer也要充分考虑到网络的因素,具体代码如下:
while(true){ await page.waitFor(1000); await page.waitFor('.m_txul>li .img'); const ele = await page.$$eval('.m_txul>li .img',((eles,imgType) =>{ const array = []; let str = ''; for(let i = 0;i<eles.length;i++){ const href = eles[i].href; const imgId = href.match(/.*?([\d]*.html)/); array.push({ src:eles[i].children[0].src, alt:eles[i].children[0].alt, href:eles[i].href, imgId, id:i }); if(i === eles.length - 1){ str+= `("${imgId[1]}","${eles[i].children[0].src}","${eles[i].children[0].alt}","${eles[i].href}","${imgType}")` }else{ str+= `("${imgId[1]}","${eles[i].children[0].src}","${eles[i].children[0].alt}","${eles[i].href}","${imgType}"),` } } return str }),urlObj.imgType); await page.evaluate(() => { return new Promise(async (resolve, reject) => { const height = document.querySelector('html').scrollHeight; const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); for (let i = 0; i < height; i+=100) { await delay(200) window.scroll(0, i); } resolve(); }) }); if(ele){ const sql = `replace into headList (imgId,src,alt,href,imgType) values ${ele}` // const sql = `insert into headList (imgId,src,alt,href,imgType) values ${ele} on duplicate key update href=values(href),src=values(src),alt=values(alt),imgType=values(imgType)` console.log(`更新次数:${num++}`); await exec(sql) } const bool = await page.$eval('.pagination>ul a:nth-last-child(2)',ele => { return ele.innerText === '下一页' }); if(!bool){ break; }else{ await page.click('.pagination>ul a:nth-last-child(2)'); } // num-- }
总结:
功能不多,但是对我来说是一个新东西,并且是一个非常值得学习的技能,虽然有一定的坑,但是还是很开心能够入门这个技能
No Data
No Data