hello-world's People
hello-world's Issues
前端的各种日期操作【值得收藏】 - 掘金
前言
虽然现在处理日期方面已经有了很成熟的也很好用的库,例如(momentjs和date-fns),但是在实际开发中,我们有时候可能并不需要整个库。
所以我就在下面整理了在前端开发时对日期时间的各种操作,也算是比较全的了。其中一部分来自自己,还有一部分来源于我们万能的网友~
获取当前时间戳
var timestamp = Date.parse(new Date()); //精确到秒
var timestamp = (new Date()).valueOf(); //精确到毫秒
var timestamp = new Date().getTime(); //精确到毫秒
var timestamp = +new Date();复制代码
获取指定时间戳
var timestamp = (new Date(" 2019/10/24 08:00:00")).getTime();
var timestamp = (new Date(" 2019-10-24 08:00:00")).getTime();复制代码
获取当前时间的前一天/后一天的时间戳
var timestamp = +new Date() - 24*60*60*1000;
var timestamp = +new Date() + 24*60*60*1000;复制代码
今日零点时间戳
var timestamp = new Date(new Date().toLocaleDateString()).getTime();复制代码
今日最晚时间 23:59:59的时间戳
let timestamp = new Date(new Date().toLocaleDateString()).getTime()+24*60*60*1000-1;复制代码
获取当前时间的n天后的时间戳
/**
* @param {number} n 天数
* @returns {Number} 返回值为时间毫秒值
*/
function toNextTimes(n){
let timestamp = +new Date() + n * 86400000;
return timestamp;
}复制代码
本周第一天
/***
* @return {*} WeekFirstDay 返回本周第一天的时间
*/
function showWeekFirstDay(){
let Nowdate=new Date();
let WeekFirstDay=new Date(Nowdate-(Nowdate.getDay()-1)*86400000);
return WeekFirstDay;
}复制代码
本周最后一天
/***
* @return {*} WeekLastDay 返回本周最后一天的时间
*/
function showWeekLastDay(){
let Nowdate=new Date();
let WeekFirstDay=new Date(Nowdate-(Nowdate.getDay()-1)*86400000);
let WeekLastDay=new Date((WeekFirstDay/1000+6*86400)*1000);
return WeekLastDay;
}复制代码
本月第一天
/***
* @return {*} MonthFirstDay 返回本月第一天的时间
*/
function showMonthFirstDay(){
let Nowdate=new Date();
let MonthFirstDay=new Date(Nowdate.getFullYear(),Nowdate.getMonth());
return MonthFirstDay;
}复制代码
本月最后一天
/***
* @return {*} MonthLastDay 返回本月最后一天的时间
*/
function showMonthLastDay(){
let Nowdate=new Date();
let MonthNextFirstDay=new Date(Nowdate.getFullYear(),Nowdate.getMonth()+1);
let MonthLastDay=new Date(MonthNextFirstDay-86400000);
return MonthLastDay;
}复制代码
日期转时间戳
/**
* @param {String} time - 日期字符串,如'2018-8-8','2018,8,8','2018/8/8'
* @returns {Number} 返回值为时间毫秒值
*/
function timeToTimestamp (time) {
let date = new Date(time);
let timestamp = date.getTime();
return timestamp;
}
复制代码
格式化当前时间
/***
* @return {string} timeText 返回系统时间字符串
*/
function getdataTimeSec() {
let time = new Date();
let weekDay;
let year = time.getFullYear();
let month = time.getMonth() + 1;
let day = time.getDate();
//获取时分秒
let h = time.getHours();
let m = time.getMinutes();
let s = time.getSeconds();
//检查是否小于10
h = check(h);
m = check(m);
s = check(s);
let now_day = time.getDay();
switch (now_day) {
case 0: {
weekDay = "星期日"
}
break;
case 1: {
weekDay = "星期一"
}
break;
case 2: {
weekDay = "星期二"
}
break;
case 3: {
weekDay = "星期三"
}
break;
case 4: {
weekDay = "星期四"
}
break;
case 5: {
weekDay = "星期五"
}
break;
case 6: {
weekDay = "星期六"
}
break;
case 7: {
weekDay = "星期日"
}
break;
}
let timeText = year + "年" + month + "月" + day + "日 " + " " weekDay + " " + h + ":" + m +":" + s;
return timeText
}复制代码
返回指定时间戳之间的时间间隔
/**
* @param {*} startTime 开始时间的时间戳
* @param {*} endTime 结束时间的时间戳
* @return {string} str 返回时间字符串
*/
function getTimeInterval(startTime, endTime) {
let runTime = parseInt((endTime - startTime) / 1000);
let year = Math.floor(runTime / 86400 / 365);
runTime = runTime % (86400 * 365);
let month = Math.floor(runTime / 86400 / 30);
runTime = runTime % (86400 * 30);
let day = Math.floor(runTime / 86400);
runTime = runTime % 86400;
let hour = Math.floor(runTime / 3600);
runTime = runTime % 3600;
let minute = Math.floor(runTime / 60);
runTime = runTime % 60;
let second = runTime;
let str = '';
if (year > 0) {
str = year + '年';
}
if (year <= 0 && month > 0) {
str = month + '月';
}
if (year <= 0 && month <= 0 && day > 0) {
str = day + '天';
}
if (year <= 0 && month <= 0 && day <= 0 && hour > 0) {
str = hour + '小时';
}
if (year <= 0 && month <= 0 && day <= 0 && hour <= 0 && minute > 0) {
str = minute + '分钟';
}
if (year <= 0 && month <= 0 && day <= 0 && hour <= 0 && minute <= 0 && second > 0) {
str += second + '秒';
}
str += '前';
return str;
}
复制代码
按类型格式化日期
/**
* @param {*} date 具体日期变量
* @param {string} dateType 需要返回类型
* @return {string} dateText 返回为指定格式的日期字符串
*/
function getFormatDate(date, dateType) {
let dateObj = new Date(date);
let month = dateObj.getMonth() + 1;
let strDate = dateObj.getDate();
let hours = dateObj.getHours();
let minutes = dateObj.getMinutes();
let seconds = dateObj.getSeconds();
if (month >= 1 && month <= 9) {
month = "0" + month;
}
if (strDate >= 0 && strDate <= 9) {
strDate = "0" + strDate;
}
if (hours >= 0 && hours <= 9) {
hours = "0" + hours
}
if (minutes >= 0 && minutes <= 9) {
minutes = "0" + minutes
}
if (seconds >= 0 && seconds <= 9) {
seconds = "0" + seconds
}
let dateText = dateObj.getFullYear() + '年' + (dateObj.getMonth() + 1) + '月' + dateObj.getDate() + '日';
if (dateType == "yyyy-mm-dd") {
dateText = dateObj.getFullYear() + '-' + (dateObj.getMonth() + 1) + '-' + dateObj.getDate();
}
if (dateType == "yyyy.mm.dd") {
dateText = dateObj.getFullYear() + '.' + (dateObj.getMonth() + 1) + '.' + dateObj.getDate();
}
if (dateType == "yyyy-mm-dd MM:mm:ss") {
dateText = dateObj.getFullYear() + '-' + month + '-' + strDate + ' ' + hours + ":" + minutes + ":" + seconds;
}
if (dateType == "mm-dd MM:mm:ss") {
dateText = month + '-' + strDate + ' ' + hours + ":" + minutes + ":" + seconds;
}
if (dateType == "yyyy年mm月dd日 MM:mm:ss") {
dateText = dateObj.getFullYear() + '年' + month + '月' + strDate + '日' + ' ' + hours + ":" + minutes + ":" + seconds;
}
return dateText;
}复制代码
判断是否为闰年
/**
* @param {number} year 要判断的年份
* @return {boolean} 返回布尔值
*/
function leapYear(year) {
return !(year % (year % 100 ? 4 : 400));
}复制代码
返回两个年份之间的闰年
/**
* @param {number} start 开始年份
* @param {number} end 结束年份
* @return {array} arr 返回符合闰年的数组
*/
function leapYears(start, end) {
let arr = [];
for (var i=start; i<end; i++) {
if ( leapYear(i) ) {
arr.push(i)
}
}
return arr
}复制代码
判断时间格式是否有效
/**
* 短时间,如 (10:24:06)
* @param {string} str 需要验证的短时间
* @return {boolean} 返回布尔值
*/
function isTime(str) {
var a = str.match(/^(\d{1,2})(:)?(\d{1,2})\2(\d{1,2})$/);
if (a == null) { return false; }
if (a[1] >= 24 || a[3] >= 60 || a[4] >= 60) {
return false
}
return true;
}
/**
* 短日期,形如 (2019-10-24)
* @param {string} str 需要验证的短时间
* @return {boolean} 返回布尔值
*/
function strDateTime(str){
var result = str.match(/^(\d{1,4})(-|\/)(\d{1,2})\2(\d{1,2})$/);
if (result == null) return false;
var d = new Date(result[1], result[3] - 1, result[4]);
return (d.getFullYear() == result[1] && d.getMonth() + 1 == result[3] && d.getDate() == result[4]);
}
/**
* 长日期时间,形如 (2019-10-24 10:24:06)
* @param {string} str 需要验证的短时间
* @return {boolean} 返回布尔值
*/
function strDateTime(str){
var result = str.match(/^(\d{4})(-|\/)(\d{1,2})\2(\d{1,2}) (\d{1,2}):(\d{1,2}):(\d{1,2})$/);
if (result == null) return false;
var d = new Date(result[1], result[3] - 1, result[4], result[5], result[6], result[7]);
return (d.getFullYear() == result[1] && (d.getMonth() + 1) == result[3] && d.getDate() == result[4] && d.getHours() == result[5] && d.getMinutes() == result[6] && d.getSeconds() == result[7]);
}复制代码
验证日期大小
/**
* 例:"2019-10-24" 和 "2019-10-25"
* @param {string} d1需要验证的日期1
* @param {string} d2需要验证的日期2
* @return {boolean} 返回布尔值
*/
function compareDate(d1, d2) {
return ((new Date(d1.replace(/-/g, "\/"))) < (new Date(d2.replace(/-/g, "\/"))));
}复制代码
验证一个日期是不是今天
/**
* @param {string} val 需要验证的日期
* @return {boolean} 返回布尔值
*/
function isToday(val){
return new Date().toLocaleDateString() == new Date(val).toLocaleDateString();
}复制代码
验证传入的日期是否是昨天
/**
* @param {string} val 需要验证的日期
* @return {boolean} 返回布尔值
*/
function isYesterday(val) {
var today = new Date();
var yesterday = new Date(now - 1000 * 60 * 60 * 24);
var test = new Date(val);
if (yesterday.getYear() === test.getYear() && yesterday.getMonth() === test.getMonth() && yesterday.getDate() === test.getDate()) {
return true;
} else {
return false;
}
}复制代码
设置几天后的日期
/**
* @param {string} date 起始日期
* @param {number} day 向后的天数
* @return {string} 返回想要得到的日期
*/
function convertDate (date, day) {
let tempDate = new Date(date);
tempDate.setDate(tempDate.getDate()+day);
let Y = tempDate.getFullYear();
let M = tempDate.getMonth()+1 < 10 ? '0'+(tempDate.getMonth()+1) : tempDate.getMonth()+1;
let D = tempDate.getDate() < 10 ? '0'+(tempDate.getDate()) : tempDate.getDate();
let result = Y + "-" + M + "-" + D
return result;
}复制代码
写在最后
若上面函数有错误,或者还有工作中遇到,但是上面没有写到的,欢迎指出来~
职场薪酬倒挂的背后,是所有人的利益最大化
这是仙人JUMP的第69篇原创
1
上周写了一篇银行管培生的职业指导文章后,后台关于职场的咨询留言突然暴增,我拉出来用关键词统计了一下,与职场有关的留言大概1万7千条。
看得我差点尿裤子,难怪各路导师们这么赚钱,拜各种不当人的老板所赐,这个市场需求有点猛。
由于问题过多,一个一个回答真的是弄死我也答不完,所以我决定不定时找一些比较有共性的问题,总结性讲一些职场有关的内容。
这些内容我不保证一定是对的,也不保证一定适合你,毕竟每个人所处的环境和个人的利益诉求都是完全不同的,如果有一套方案适合所有人,那么只能说明这个方案本身是骗子。
事先说明,我只是从我自己的职业经历来进行相关的分析阐述,各种观点甚至可能是歪理邪说,并且讲话会比较尖锐一点。
希望大家可以自动过滤,找到对自己有所帮助的内容。
关键还是,大家看着开心就行。
通过对留言的分析(很简单的SQL技能),我发现很多朋友对工资少或者工作压力大并不是特别在意(或者说已经习惯了),反倒是一个很有趣的职场现象被提及的特别多。
用专业术语讲,这东西叫薪酬倒挂,工作强度与收益不匹配。
用大白话讲,就是新招进来的人,普遍工资都比老员工高,但是工作产出和效率往往不如老员工。
甚至还出现过很多应届生工资倒挂老员工的现象,看着就特别憋屈。
于是老员工们就开始很不满意了,觉得自己被压榨了,天天期望着老板有一天能够良心发现给自己涨工资,明明自己做的比新人又好又熟练,特别委屈。
别幻想了朋友们,老板要是有良心,母猪都能上树。
严格来说,一个合格的老板,是没有所谓【良心】的,有的只是投入产出比(ROI)和KPI,一切行为都是围绕着这东西服务的。
而且所谓的薪酬倒挂,其实是非常正常的现象,甚至可以说薪酬不倒挂才有鬼了。
或许你觉得不可思议,没关系,听我慢慢讲。
2
为什么绝大多数公司宁肯给新人更高的工资也不愿意给老员工加薪?
表面看起来,这是一个非常典型的不患多寡而患不均的问题,公司非常愚蠢,导致员工心生不满,最后不好好干活让公司没法利益最大化。
与薪酬倒挂一起出现的,往往还有薪酬保密这个制度,员工们都恨死这写东西了,本来世界已经这么难了,还有这么多幺蛾子。
薪酬倒挂+薪酬保密,听起来很沙雕的东西如果非常流行,那么答案只有一个。
那就是这东西确实有用。
我们都知道,一家公司追求的一定是利益最大化,不管公司做出什么决策,都是要实现这个效果的。
对员工好,让员工拼命干活,是一种方法。
对员工不好,压榨员工,也是一种方法。
当一个公司出现大规模薪酬倒挂的时候,除了小概率是老板自己沙雕,大多数情况都是管理者认为这么做的好处比坏处多。
薪酬倒挂,是公司管理者利益最大化的具象体现。
需要注意的一点是,我这里说的是利益最大化是公司管理者,包括高层,中层以及基层管理者,而非是公司本身。
实际上公司本身玩儿倒挂本身没什么好处,买菜九毛九,买人一块一,省钱也不是这么省的。
但是架不住各路管理者不顾公司利益,给自己打算盘。
实际上真正影响各位职场体验的,正是这群天天接触的管理者们。
他们沙雕,才是真的沙雕。
3
为什么管理者会为了自己的利益而容许甚至鼓励薪酬倒挂现象存在呢?
那我们先来看,管理者的核心利益诉求是什么?
钱,权力,当然权力最终也是会被兑换成钱的,我讲话就是这么赤裸裸。
OK,我们可以知道管理者的核心利益诉求是先掌握权力,然后利用权力来牟利。
那么问题来了,什么是掌握权力最快的方法?
可能大多数没有当过管理者的人觉得是好好干,干出成绩,获得身边所有人的认可,然后升职获得更多权力。
这个答案严格来说不能说错,但是稍微带了一些幻想主义。
在现实职场中,很多管理者权力扩大,是靠自己管理人数的增加来实现的。
别笑,我知道这东西很荒谬,但又确实存在,尤其是大公司和外企。
下属越多,管理者的权力就越大。
大公司里通行的潜规则是,同样的一份工作,只有你一个人做,你就是累死的驴,你抓着3个人一起做,你就是主管;你带着5到10个人一起做,你就是经理;你带着20个人一起做,你就是总监;你带着一大堆人,你就是可以独立事业部总经理。
你管的人越多,你在公司的权重就越大,并且由于人多了一定会有内斗,作为仲裁者的你,反而地位更加稳固,在上层眼里也更加有价值。
那么问题来了,如何让自己快速把人搞来给自己管?如何才能快速撬动权力膨胀?
对了,你想到了,招人,而且是快速招人
很多空降管理者最爱干的事情就是招人,不招人他们都不知道干什么了。
还美其名曰领导能力,非常搞笑。
那么如何才能快速招人呢?
你又想到了,加钱。
不加钱,是很难快速招到人的,所以很多新来的人薪酬高于老人,并不意外。
那还有人说了,老人万一干的不爽离职了怎么办?
朋友,老人离开后的坑,显然一个新人是很难直接顶上的,那就刚好可以找借口再多招几个人,反正锅都往离职人员头上扣。
放老人走,新申请多个HC多招几个人扩大权力,很多管理者喜欢玩这个套路,毕竟符合规则,操作简单,又能把自己的利益最大化,额外增加的成本反正是算在公司头上的,又不用他自己出。
即使从大局上对公司成本会有浪费,但是对于管理者自己是大赚特赚,so why not?
当第一目的是增加管理人数的时候,很多沙雕操作就有了内在合理性。
你以为管理者不懂,实际上他们比谁都懂。
只不过大家的利益不一致而已,人都是趋利避害的。
哪有什么头脑问题,归根究底都是屁股问题
4
即使排除管理者因为自身权力扩张作妖,即使是从不给自己惹麻烦的角度来看,允许薪酬倒挂的存在都是非常高性价比的行为。
对,是高性价比,是不给自己惹麻烦。
在绝大多数公司,给老员工调薪都是一件非常非常麻烦而且性价比不高的行为,一般来说不是天地良心级别的管理者,没理由给自己惹一身腥。
很多管理者的核心生存逻辑不是创造价值,而是不出错,不惹麻烦,默默等待对手出错。
确实是怂归怂,但是很多时候熬到对手挂掉,也是一种战略。
给老员工调薪就是很容易搞出幺蛾子的一件事。
一般来说,大公司有完善的加薪体系,每次加薪多少,普调多少,公司都有明确规定。
在大公司里,争取大幅度内部调薪的难度要高于申请新的高薪HC,因为这本质上是走了两个不同的流程。
帮你申请超出常规的调薪幅度往往需要层层审批报备并且要老板做出担保;
而申请一个高薪HC(head count,等于岗位)只要差不多符合市场定价(稍微有个波动也无所谓)就OK了。
所以如果你不是不可取代的员工(这种人公司有10%就很不错了,绝大多数人都是自以为不可替代),那么对你的上级来说,为你强出头申请高额调薪,是一件非常具有风险的事情。
因为如果特批给了你高新,如果你后续无法产出更多或者不是不可替代,那么他就比较被动了。
从性价比来说,申请一个新HC才是更好的选择,因为后者更简单,而且可以塞自己人进来,每个人都是趋利避害的,干嘛要去做困难的事情呢?
反正又不是没你不行。
记住了,大公司里,你只是老板算的人头数。
或许你说了,万一我特别优秀呢,就是不可或缺呢?
朋友,你对于世界的残酷和职场的阴暗一无所知。
如果给你一个人大幅调薪了,那么其他人怎么想?是不是还要给其他人大幅调薪?搞成按闹分配吗?
任何一个合格的管理者都不容许这种情况的出现,人力成本剧增是一回事,更重要的是自己的权威被挑战,自己管理者的资格被质疑,这个非常要命。
所以杀鸡儆猴也是一种策略,我要树立自己的宁可去外面花贵的钱也不愿意屈服的形象,因为这次退一步,就要退更多步,最终导致人心不平衡,团队失控。
所以宁可从外面招一个高薪的,首先是避免出现单一老人大幅涨薪导致团队内心不满,毕竟人都是不患贫而患不均的;
其次就是即使大家有不满,也可以团结起来孤立这个新人,反而会增加这个团队的稳定性;
如果新人表现出了超强的能力,那么大家其实也就没啥不服了;
如果新人能力不太行,大不了在试用期就可以合理地低成本地干掉新人让老人觉得满意,团队也就没有怨言了。
就是立一个靶子在那里,至于是当做正面教材还是反面教材,老板不管怎样都可以玩得转。
在同一个行业里,公司待遇多数都是大差不差的,能拿到高额offer的人总是少数。
绝大多数人,其实权衡下来跳槽的风险和获得的工资涨幅未必都是完全匹配的。
总有人怀念公司的氛围不走,
总有人太懒只要公司不是太过分不走,
总有人觉得大家都脸熟好办事不走,
总有人觉得自己还要学习不走,
总有人觉得换工作麻烦不走,
总有人有妻儿老小不走,
总有人家里离得近不走,
那么走的一些人刚好还可以促进公司新鲜血液流动,领导者还可以趁机多招几个人扩大职权,皆大欢喜。
对管理者来说,这是可控的。
可控,很重要。
5
如果抛开别的乱七八糟,只谈价格的话,薪酬倒挂也是一种正常的经济现象。
因为老人新人的薪酬计算方式就是用了不同的统计方法。
给老人的薪资涨幅,对应的是公司制度一年一年普调的,不参考具体数字,只参考涨幅百分比;
给新人的薪资是按照当前市场价的,不参考与自己老人的对比,只参考当前的市场价开到多少才能招来人;
不是说公司非得给跳槽的人高工资,而是公司开出了高工资别人才有心思跳槽过来。
所以这两个价格存在错位非常正常,况且市场价也不是总比内部价要高的。
有的时候市场大环境不好的时候给毕业生的薪资低了特别多,毕业生进来了也和老人做一样的工作,比老人更能加班更能付出。
当然新人比老人低的时候,可能大家都不比比了。
很讽刺的是,其实新人比老人高,可能更符合市场规律。
这时候有的老人肯定不开心了,我也想跳槽,也想成为别人口里的【新人】。
很好,这个态度是积极的,但是很多人面临的现实情况是,其实很多老人真的是没有新任优秀,他们最大的优势不是能力。
而是比较融洽的同事人脉,以及对于公司内部流程的熟悉程度。
换句话说,他之所以在这里能发挥作用,不是因为个人能力,而是因为他比别人更熟悉这套流程机制,更能通过卖脸来获得资源。
这在各类大型企业特别特别常见,这样的老人非常非常多。
他们不是有10年经验,他们是2年的经验用了10年。
而且有一说一,真正的大公司,其实是【去能力化】的。
就是说,在大公司里,往往会有一套完美的公司人才体制,是倾向于把工作细致化,规范化,简单化,并辅以各种流程指导和老员工帮带,让新人可以快速上手,岗位可以快速更替·。
很多外资公司就是这种玩法,各种JD上要求很高大上,实际上进来之后就是重复性简单工作。
这样做的好处就是公司不会因为少了任何一个人就出现重大危机,一个干了很多年的人和应届毕业生在这个机制里面都是差不多的水准。
只有绝对的稳定化,才能让公司走得更远,现在很多互联网公司也逐渐有了这样的趋势,很多岗位都是细致到只有本公司有,其他公司没有,让你跳无可跳,况且东家给的薪水也还好,你就成了被圈养的羊,只能被拔毛,无法反抗。
这种情况下不是你想不想走了,而是你还能不能走,你还有什么资本和能力与公司谈判?你哪里来的溢价权?
很多人都是年轻的时候怼天怼地,一过30岁怂成了狗。
说穿了还是被圈养了。
保持饥饿并不是一句空话,兽性不可丢。
6
讲完现实状况和原理,再讲讲如何应对。
不管你面对的是什么情况,我认为都只有一种策略。
那就是当你在同一家公司工作2年(如果涨薪太难看,1年也可)的时候,如果工资涨幅没有达到你的预期或者说学到的知识和能力让你觉得未来也没有变现空间,那么就该考虑动了。
不是说让你一定要跳槽,实际上跳槽也要看大环境的,有的时候就是狗着最聪明。
我的意思是,最起码每年都要去同业面一遍试,拿一遍offer,不管你跳还是不跳,起码你要知道市场价,也要知道自己的价值,说不定你拿了一圈offer发现价格还不如自家,或者拿不到满意的offer,那说明公司没有亏待你,你就值这个价,摆正心态,好好上班,现在是市场经济。
这也有助于你了解市场的最新需求,知道自己的努力方向。
记住即使被别的公司拒绝了,也尽量向面试官或者hr诚恳请教自己被拒绝的原因,多数人在没有利害关系的前提下是愿意和你分享一些看法的,这些看法都有利于你完善自我。
另外还有就是别把简历到处瞎投,记得保护好自己面试这件事儿,也不要过于频繁请假,省的被现有Leader发现。
那种在一家公司痴守多年没涨工资的人,我觉得主要责任在个人,不在公司。
你自己都不去争取不去市场询价不去比对,你就指望公司看穿你的善良的内心然后被你感动给你发个大奖?天真!
公司的第一要义是盈利,不是员工福利,一切员工福利和员工关怀的本质都是激发员工积极性或者让员工有归属感有稳定性,本质上还是希望员工为公司创造价值!
我们出来工作就是要钱的,所谓职业规划,说白了就是看短期的钱还是看长期的钱,归根结底重点还是在钱。
没有财务自由,就没有**自由,所以我认为各位追求钱是非常正当的事情,我自己也是这样的。
大大方方谈钱,不丢人。
当然,虽然大方向如此,但是也要考虑一下跳槽频率,不是说瞎JB跳就能让你更强。
工作和赚钱是一辈子的事情,需要放长线钓大鱼,不要死盯着眼前的一点点利益而忽视其他,选择跳槽平台的前提一定是这家公司能对你再下次跳槽的时候有足够的背书起码不会拖你后腿,不要盲目为了一点钱去一些乱七八糟的公司,有的你后悔。
跳槽时间尽量控制在2年(一年半算是最短的极限了)到3年一跳最好,频繁跳槽是在给简历抹黑,因为你的下一家会认为你缺乏稳定性,且存在隐形缺陷。
短时间跳槽一次两次可以说是公司SB,短时间跳槽多次,一定是求职者也有某些缺陷,或者存在沟通问题,或者缺乏稳定性。
反正公司也会考虑自己公司不是天选之子,可能无法接待大神。
一个缺乏稳定性,或者存在隐患的人,无法持续为公司创造价值。
而不能创造价值的人,没有公司会给他高价。
这一切,本质上就是一笔交易。
看清楚交易的本质,为自己谋利,才是最理智的行为。
仙人JUMP
长按左侧二维码关注!
你将感受到一个放飞自我的灵魂
且每篇文章都有惊喜
-----------------------
感谢你的阅读,下面是1个抽奖链接按钮,11月3日晚上19点开奖,一共1888元,666个红包,感谢大家的支持。
感谢大家一直以来的阅读、在看和转发,点我参与抽奖!点我参与抽奖!
【上个班都不让人安心,真魔幻】
vscode + vim 全键盘操作高效搭配方案
vscode-vim
vscode-vim 是一款 vim 模拟器,它将 vim 的大部分功能都集成在了 vscode 中,你可以将它理解为一个嵌套在 vscode 中的 vim。
由于该 vim 是被模拟的的非真实 vim,所以原生 vim 中有些功能它并不支持,如宏录制功能,但这依然不妨碍 vscode-vim 插件的优秀。
其实在 vscode 的扩展商店中,还有一个 vscode neovim 的插件也十分不错,但是相较于 vscode-vim 来说依然存在一些让我难以接受的缺点,比如 visual 模式下的选择并非真正的 vscode 选择等,基于种种原因我还是考虑使用 vscode-vim,尽管它可能占用了更大的内存。
安装使用
我们需要在插件商店中搜索 vim 插件,然后安装:
安装完成之后,需要做一些基础配置。
1)仅 MAC 用户,关闭 MAC 的重复键:
$ defaults write com.microsoft.VSCode ApplePressAndHoldEnabled -bool false
2)设置相对行号,在 settings.json 中添加上以下配置项:
"editor.lineNumbers": "relative",
3)关闭自动传参建议,使用按键手动触发:
"editor.parameterHints.enabled": false,
4)控制资源管理器中的键盘导航无法自动触发:
"workbench.list.automaticKeyboardNavigation": false,
基础配置项
vscode-vim 插件由于是一款模拟器,所以它的配置文件是放在 settings.json 文件中,而不是 vimrc 文件中,个人也并不推荐将配置放在 vimrc 文件中,因为这会导致多端同步变的复杂,尽管这款插件可以支持从 vimrc 文件中读取配置。
下面是一些我会在使用 vscode-vim 插件时配置的 vim 选项,直接放入到 settings.json 文件中即可:
{
// 绑定vim前导键
"vim.leader": "<space>",
// 启用easymotion插件
"vim.easymotion": true,
// 启用系统粘贴板作为vim寄存器
"vim.useSystemClipboard": true,
// 由vim接管ctrl+any的按键,而不是vscode
"vim.useCtrlKeys": true,
// 突出显示与当前搜索匹配的所有文本
"vim.hlsearch": true,
// 普通模式下的非递归按键绑定
"vim.normalModeKeyBindingsNonRecursive": [],
// 插入模式下的非递归按键绑定
"vim.insertModeKeyBindings": [],
// 命令模式下的非递归按键绑定
"vim.commandLineModeKeyBindingsNonRecursive": [],
// 可视模式下的非递归按键绑定
"vim.operatorPendingModeKeyBindings": [],
// 下面定义的按键将交由vscode进行处理,而不是vscode-vim插件
"vim.handleKeys": {
"<C-a>": false,
"<C-f>": false
}
}
热键配置项
基本上 vim 的所有模式你都可以配置在下面的 4 个选项中:
// 普通模式下的非递归按键绑定
"vim.normalModeKeyBindingsNonRecursive": [],
// 插入模式下的非递归按键绑定
"vim.insertModeKeyBindings": [],
// 命令模式下的非递归按键绑定
"vim.commandLineModeKeyBindingsNonRecursive": [],
// 可视模式下的非递归按键绑定
"vim.operatorPendingModeKeyBindings": [],
// 下面定义的按键将交由vscode进行处理,而不是vscode-vim插件
"vim.handleKeys": {
"<C-a>": false,
"<C-f>": false
}
一个简单的例子,在 INSERT 模式下使用 jj 退回到 NORMAL 模式:
"vim.insertModeKeyBindings": [
{
"before": [
"j",
"j"
],
"after": [
"<Esc>"
]
},
],
下面的示例将演示如何在 NORMAL 模式下按下快捷键执行 COMMAND 的命令,如按下后,取消高亮搜索:
"vim.normalModeKeyBindingsNonRecursive": [
{
"before": [
"<C-n>"
],
"commands": [
":nohl"
]
},
],
除此之外,你也可以定义在按下一些按键后,调用 vscode 下的命令 API,比如在 NORMAL 模式下按下gc 后,调用 vscode 的全局命令:
"vim.normalModeKeyBindingsNonRecursive": [
{
"before": [
"<leader>",
"g",
"c"
],
"commands": [
"workbench.action.showCommands"
]
}
],
注意!leader 键只在代码编辑区域生效,它无法做到全 vscode 生效。
针对一些刚接触 vscode 不久的朋友,可能不知道怎么拿到 vscode 的热键映射命令,其实你可以从 vsocde 的键盘快捷键中复制命令 ID 获得。
自用热键方案
下面是个人自用的 vim+vscode 全键盘热键方案,对于非代码编辑区的热键将其定义在 keybindings.json 中,对于代码编辑区且属于 vim 的热键将其定义在 settings.json 文件中。
我目前使用的是 mac 平台,所以 cmd 按键会比 ctrl 按键更加常用,如果是 windows 或者 linux 平台,则将 cmd 替换为 ctrl 按键即可:
cmd + g c : 显示命令面板
cmd + g s : 打开设置页面
cmd + g k : 打开热键映射
cmd + g m : 打开一个目录
cmd + g f : 打开一个文件
cmd + g h : 打开最近记录
cmd + g n : 新建vscode实例
cmd + g q : 关闭vscode示例
cmd + f n : 新建文件
cmd + f o : 打开文件
cmd + f e : 另存为文件
cmd + f s : 保存文件
cmd + f w : 保存所有文件
cmd + f q : 关闭文件
cmd + f a : 关闭所有文件
cmd + n [ : 切换侧边栏显示状态
cmd + n 1 : 显示文件资源管理器
cmd + n 2 : 显示TODO Tree
cmd + n 3 : 显示全局搜索
cmd + n 4 : 显示debug
cmd + n 5 : 显示版本控制
cmd + n 6 : 显示SQL Tools
cmd + n 7 : 显示Docker
cmd + n 8 : 显示测试
cmd + n 9 : 显示插件商店
cmd + p ] : 切换面板显示状态
cmd + p 1 : 显示问题
cmd + p 2 : 显示输出
cmd + p 3 : 显示终端
cmd + p 4 : 显示调试控制台
cmd + p 5 : 显示SQL CONSOLE
以下是编辑区域操作控制方案:
cmd + q :关闭当前选项卡或分屏
cmd + e :聚焦在第一个选项卡中
cmd + , :切换到上一个选项卡
cmd + . :切换到下一个选项卡
cmd + w s :拆分一个上下分屏
cmd + w v :拆分一个左右分屏
cmd + w k :将光标向上移动1屏
cmd + w j :将光标向下移动1屏
cmd + w h :将光标向左移动1屏
cmd + w l :将光标向右移动1屏
代码控制区域:
cmd + h : 触发帮助提示
cmd + j : 触发参数提示
cmd + k : 触发建议提示
cmd + n : 移动到下一个建议
cmd + p : 移动到上一个建议
tab : 选择下一个建议
enter : 选择当前建议
cmd + alt + l : 格式化代码(个人习惯)
cmd + = : 放大字体
cmd + - : 缩小字体
在 settings.json 中配置的代码控制区域热键方案:
jj : 退出INSERT模式
zz : 切换代码折叠(原生vim的zz不是切换折叠)
H :跳转行首、取代^
L :跳转行尾、取代$
g[ : 跳转到上一个问题
g] : 跳转到下一个问题
我的配置文件
以下是 settings.json 文件中关于代码编辑区的一些操作配置项:
"vim.normalModeKeyBindingsNonRecursive": [
{
"before": [
"H"
],
"after": [
"^"
]
},
{
"before": [
"L"
],
"after": [
"$"
]
},
{
"before": [
"z",
"z",
],
"commands": [
"editor.toggleFold"
]
},
{
"before": [
"g",
"[",
],
"commands": [
"editor.action.marker.prevInFiles"
]
},
{
"before": [
"g",
"]",
],
"commands": [
"editor.action.marker.nextInFiles"
]
},
],
// 插入模式下的非递归按键绑定
"vim.insertModeKeyBindings": [
{
"before": [
"j",
"j"
],
"after": [
"<Esc>"
]
},
],
// 命令模式下的非递归按键绑定
"vim.commandLineModeKeyBindingsNonRecursive": [],
// 可视模式下的非递归按键绑定
"vim.operatorPendingModeKeyBindings": [],
以下是 keybindings.json 文件,基本的键位设置就如同上面看到的一样,但是做了一些额外的限制条件,比如和以及等 vscode 原生命令在代码编辑区域中非 NORMAL 模式下是不生效的:
[
// --- 全局命令
// 显示命令面板
{
"key": "cmd+g c",
"command": "workbench.action.showCommands"
},
// 打开设置页面
{
"key": "cmd+g s",
"command": "workbench.action.openSettings"
},
// 打开热键映射
{
"key": "cmd+g k",
"command": "workbench.action.openGlobalKeybindings"
},
// 打开一个目录
{
"key": "cmd+g m",
"command": "workbench.action.files.openFolder"
},
// 打开一个文件
{
"key": "cmd+g f",
"command": "workbench.action.files.openFile"
},
// 打开最近记录
{
"key": "cmd+g h",
"command": "workbench.action.openRecent"
},
// 新建vscode实例
{
"key": "cmd+g n",
"command": "workbench.action.newWindow"
},
// 关闭vscode实例
{
"key": "cmd+g q",
"command": "workbench.action.closeWindow"
},
// --- 文件命令
// 新建文件
{
"key": "cmd+f n",
"command": "welcome.showNewFileEntries",
},
// 打开文件
{
"key": "cmd+f o",
"command": "workbench.action.files.openFileFolder"
},
// 另存为文件
{
"key": "cmd+f e",
"command": "workbench.action.files.saveAs"
},
// 保存文件
{
"key": "cmd+f s",
"command": "workbench.action.files.save"
},
// 保存所有文件
{
"key": "cmd+f w",
"command": "workbench.action.files.saveAll"
},
// 关闭文件
{
"key": "cmd+f q",
"command": "workbench.action.closeActiveEditor"
},
// 关闭所有文件
{
"key": "cmd+f a",
"command": "workbench.action.closeAllEditors"
},
// -- 侧边栏命令
// 切换侧边栏显示状态
{
"key": "cmd+n [",
"command": "workbench.action.toggleSidebarVisibility"
},
// 显示文件资源管理器
{
"key": "cmd+n 1",
"command": "workbench.files.action.focusFilesExplorer"
},
// 显示TODO Tree
{
"key": "cmd+n 2",
"command": "todo-tree-view.focus"
},
// 显示全局搜索
{
"key": "cmd+n 3",
"command": "workbench.action.replaceInFiles",
},
// 显示debug
{
"key": "cmd+n 4",
"command": "workbench.view.debug",
"when": "viewContainer.workbench.view.debug.enabled"
},
// 显示版本控制
{
"key": "cmd+n 5",
"command": "workbench.view.scm",
"when": "workbench.scm.active"
},
// 显示SQL Tools
{
"key": "cmd+n 6",
"command": "workbench.view.extension.sqltoolsActivityBarContainer"
},
// 显示Docker
{
"key": "cmd+n 7",
"command": "workbench.view.extension.dockerView"
},
// 显示测试
{
"key": "cmd+n 8",
"command": "workbench.view.testing.focus"
},
// 显示插件商店
{
"key": "cmd+n 9",
"command": "workbench.view.extensions",
"when": "viewContainer.workbench.view.extensions.enabled"
},
// --- 面板命令
// 切换面板显示状态
{
"key": "cmd+p [",
"command": "workbench.action.togglePanel"
},
// 显示问题
{
"key": "cmd+p 1",
"command": "workbench.panel.markers.view.focus"
},
// 显示输出
{
"key": "cmd+p 2",
"command": "workbench.action.output.toggleOutput",
"when": "workbench.panel.output.active"
},
// 显示终端
{
"key": "cmd+p 3",
"command": "workbench.action.terminal.toggleTerminal",
"when": "terminal.active"
},
// 显示调试控制台
{
"key": "cmd+p 4",
"command": "workbench.debug.action.toggleRepl",
"when": "workbench.panel.repl.view.active"
},
// 显示SQL CONSOLE
{
"key": "cmd+p 5",
"command": "workbench.view.extension.sqltoolsPanelContainer"
},
// --- 编辑区命令
// 关闭当前选项卡或分屏
{
"key": "cmd+q",
"command": "workbench.action.closeActiveEditor"
},
// 聚集在第一个选项卡中
{
"key": "cmd+e",
"command": "workbench.action.focusFirstEditorGroup"
},
// 切换到上一个选项卡
{
"key": "cmd+,",
"command": "workbench.action.previousEditor"
},
// 切换到下一个选项卡
{
"key": "cmd+.",
"command": "workbench.action.nextEditor"
},
// 拆分一个上下分屏
{
"key": "cmd+w s",
"command": "workbench.action.splitEditorDown"
},
// 拆分一个左右分屏
{
"key": "cmd+w v",
"command": "workbench.action.splitEditor"
},
// 将光标向上动1屏
{
"key": "cmd+w k",
"command": "workbench.action.focusAboveGroup"
},
// 将光标向下动1屏
{
"key": "cmd+w j",
"command": "workbench.action.focusBelowGroup"
},
// 将光标向左移动1屏
{
"key": "cmd+w h",
"command": "workbench.action.focusLeftGroup"
},
// 将光标向右移动1屏
{
"key": "cmd+w l",
"command": "workbench.action.focusRightGroup"
},
// --- 代码编辑命令
// 触发帮助提示
{
"key": "cmd+h",
"command": "editor.action.showHover",
"when": "editorTextFocus"
},
// 触发参数提示
{
"key": "cmd+j",
"command": "editor.action.triggerParameterHints",
"when": "editorHasSignatureHelpProvider && editorTextFocus"
},
{
"key": "cmd+j",
"command": "closeParameterHints",
"when": "editorFocus && parameterHintsVisible"
},
// 触发建议提示
{
"key": "cmd+k",
"command": "editor.action.triggerSuggest",
"when": "editorHasCompletionItemProvider && textInputFocus && !editorReadonly"
},
{
"key": "cmd+k",
"command": "hideSuggestWidget",
"when": "suggestWidgetVisible && textInputFocus"
},
// 移动到下一个建议
{
"key": "cmd+n",
"command": "selectNextSuggestion",
"when": "suggestWidgetMultipleSuggestions && suggestWidgetVisible && textInputFocus"
},
// 移动到上一个建议
{
"key": "cmd+p",
"command": "selectPrevSuggestion",
"when": "suggestWidgetMultipleSuggestions && suggestWidgetVisible && textInputFocus"
},
// 格式化代码
{
"key": "cmd+alt+l",
"command": "editor.action.formatDocument",
"when": "editorHasDocumentFormattingProvider && editorTextFocus && !editorReadonly && !inCompositeEditor"
},
// 放大字体
{
"key": "cmd+=",
"command": "editor.action.fontZoomIn"
},
// 缩小字体
{
"key": "cmd+-",
"command": "editor.action.fontZoomOut"
},
// --- window 用户删除以下重映射,这里是为MAC用户准备的
{
"key": "cmd+r",
"command": "extension.vim_ctrl+r",
"when": "editorTextFocus && vim.active && vim.use<C-r> && !inDebugRepl"
},
{
"key": "ctrl+r",
"command": "-extension.vim_ctrl+r",
"when": "editorTextFocus && vim.active && vim.use<C-r> && !inDebugRepl"
},
{
"key": "cmd+a",
"command": "extension.vim_ctrl+a",
},
{
"key": "ctrl+a",
"command": "-extension.vim_ctrl+a",
"when": "editorTextFocus && vim.active && vim.use<C-a> && !inDebugRepl"
},
{
"key": "cmd+x",
"command": "extension.vim_ctrl+x",
},
{
"key": "ctrl+x",
"command": "-extension.vim_ctrl+x",
"when": "editorTextFocus && vim.active && vim.use<C-x> && !inDebugRepl"
},
{
"key": "cmd+u",
"command": "extension.vim_ctrl+u",
"when": "editorTextFocus && vim.active && vim.use<C-u> && !inDebugRepl"
},
{
"key": "ctrl+u",
"command": "-extension.vim_ctrl+u",
"when": "editorTextFocus && vim.active && vim.use<C-u> && !inDebugRepl"
},
{
"key": "cmd+d",
"command": "extension.vim_ctrl+d",
"when": "editorTextFocus && vim.active && vim.use<C-d> && !inDebugRepl"
},
{
"key": "ctrl+d",
"command": "-extension.vim_ctrl+d",
"when": "editorTextFocus && vim.active && vim.use<C-d> && !inDebugRepl"
},
{
"key": "cmd+i",
"command": "extension.vim_ctrl+i",
"when": "editorTextFocus && vim.active && vim.use<C-i> && !inDebugRepl"
},
{
"key": "ctrl+i",
"command": "-extension.vim_ctrl+i",
"when": "editorTextFocus && vim.active && vim.use<C-i> && !inDebugRepl"
},
{
"key": "cmd+o",
"command": "extension.vim_ctrl+o",
"when": "editorTextFocus && vim.active && vim.use<C-o> && !inDebugRepl"
},
{
"key": "ctrl+o",
"command": "-extension.vim_ctrl+o",
"when": "editorTextFocus && vim.active && vim.use<C-o> && !inDebugRepl"
},
// --- 取消一些vim插件的额外功能
{
"key": "cmd+a",
"command": "-extension.vim_cmd+a",
"when": "editorTextFocus && vim.active && vim.use<D-a> && !inDebugRepl && vim.mode != 'Insert'"
},
{
"key": "alt+cmd+down",
"command": "-extension.vim_cmd+alt+down",
"when": "editorTextFocus && vim.active && !inDebugRepl"
},
{
"key": "alt+cmd+up",
"command": "-extension.vim_cmd+alt+up",
"when": "editorTextFocus && vim.active && !inDebugRepl"
},
{
"key": "cmd+c",
"command": "-extension.vim_cmd+c",
"when": "editorTextFocus && vim.active && vim.overrideCopy && vim.use<D-c> && !inDebugRepl"
},
{
"key": "cmd+v",
"command": "-extension.vim_cmd+v",
"when": "editorTextFocus && vim.active && vim.use<D-v> && vim.mode == ''CommandlineInProgress' !inDebugRepl' || editorTextFocus && vim.active && vim.use<D-v> && !inDebugRepl && vim.mode == 'SearchInProgressMode'"
},
{
"key": "cmd+d",
"command": "-extension.vim_cmd+d",
"when": "editorTextFocus && vim.active && vim.use<D-d> && !inDebugRepl"
},
{
"key": "cmd+left",
"command": "-extension.vim_cmd+left",
"when": "editorTextFocus && vim.active && vim.use<D-left> && !inDebugRepl && vim.mode != 'Insert'"
},
{
"key": "cmd+right",
"command": "-extension.vim_cmd+right",
"when": "editorTextFocus && vim.active && vim.use<D-right> && !inDebugRepl && vim.mode != 'Insert'"
},
// --- 取消或更改一些vscode键位
// cmd+a全选功能在非INSERT模式下不生效
{
"key": "cmd+a",
"command": "editor.action.selectAll",
"when": "vim.mode != 'Normal' && vim.mode != 'Visual' && vim.mode != 'VisualLine' && vim.mode != 'VisualBlock' && vim.mode != 'CommandlineInProgress'"
},
{
"key": "cmd+a",
"command": "-editor.action.selectAll"
},
// cmd+c或者cmd+v功能在非INSERT模式下不生效
{
"key": "cmd+c",
"command": "-editor.action.clipboardCopyAction"
},
{
"key": "cmd+v",
"command": "-editor.action.clipboardPasteAction"
},
{
"key": "cmd+c",
"command": "-execCopy"
},
{
"key": "cmd+c",
"command": "execCopy",
"when": "vim.mode != 'Normal' && vim.mode != 'Visual' && vim.mode != 'VisualLine' && vim.mode != 'VisualBlock' && vim.mode != 'CommandlineInProgress'"
},
{
"key": "cmd+v",
"command": "-execPaste",
},
{
"key": "cmd+v",
"command": "execPaste",
"when": "vim.mode != 'Normal' && vim.mode != 'Visual' && vim.mode != 'VisualLine' && vim.mode != 'VisualBlock' && vim.mode != 'CommandlineInProgress'"
},
]
资源管理配置
默认的资源管理配置只包含了上下左右移动等基础命令,所以我们需要手动添加新增、删除、剪切、刷新等操作命令,它们仅在资源管理器中生效:
j : 向下移动
k : 向上移动
space : 打开文件或目录
// 手动新增:
i : 新增文件
o : 新增目录
r : 刷新目录
a : 重命名文件或目录
d : 删除文件或目录
x : 剪切文件或目录
y : 复制文件或目录
p : 粘贴文件或目录
在 keybindings.json 文件中加入以下配置项:
// --- 资源管理器中对文件或目录的操作
// 新建文件
{
"key": "i",
"command": "explorer.newFile",
"when": " explorerViewletVisible && filesExplorerFocus && !explorerResourceIsRoot && !explorerResourceReadonly && !inputFocus "
},
// 新建目录
{
"key": "o",
"command": "explorer.newFolder",
"when": " explorerViewletVisible && filesExplorerFocus && !explorerResourceIsRoot && !explorerResourceReadonly && !inputFocus "
},
// 刷新资源管理器
{
"key": "r",
"command": "workbench.files.action.refreshFilesExplorer",
"when": " explorerViewletVisible && filesExplorerFocus && !explorerResourceIsRoot && !explorerResourceReadonly && !inputFocus "
},
// 重命名文件或目录
{
"key": "a",
"command": "renameFile",
"when": " explorerViewletVisible && filesExplorerFocus && !explorerResourceIsRoot && !explorerResourceReadonly && !inputFocus "
},
// 删除文件或目录
{
"key": "d",
"command": "deleteFile",
"when": " explorerViewletVisible && filesExplorerFocus && !explorerResourceIsRoot && !explorerResourceReadonly && !inputFocus "
},
// 剪切文件或目录
{
"key": "x",
"command": "filesExplorer.cut",
"when": "explorerViewletVisible && filesExplorerFocus && !explorerResourceIsRoot && !explorerResourceReadonly && !inputFocus"
},
// 复制文件或目录
{
"key": "y",
"command": "filesExplorer.copy",
"when": "explorerViewletVisible && filesExplorerFocus && !explorerResourceIsRoot && !inputFocus"
},
// 粘贴文件或目录
{
"key": "p",
"command": "filesExplorer.paste",
"when": "explorerViewletVisible && filesExplorerFocus && !explorerResourceReadonly && !inputFocus"
},
输入法切换
如果你在 INSERT 模式下使用中文输入法进行编辑,当 ESC 到 NORMAL 模式下后,它将依然保持中文输入法,这会使我们需要频繁的使用 ctrl+shift 或者 cmd+space 进行输入法切换,非常麻烦。
为了解决这个问题,你必须先在你的计算机上安装一个im-select脚本(根据官方文档来看,貌似只有 mac 和 windows 平台下存在这个问题):
$ curl -Ls https://raw.githubusercontent.com/daipeihust/im-select/master/install_mac.sh | sh
这个脚本有 2 个作用,当你输入 im-select 后它将获取当前输入法,当你输入 im-select xxx 后它将切换至 xxx 输入法。
首先你需要先切换到英文输入法中到终端执行 im-select 命令,并把结果保存下来:
$ im-select
com.apple.keylayout.ABC
然后再到 settings.json 中加入以下配置项即可完成输入法在 INSERT 模式以及 NORMAL 模式下的自动切换:
// 自动切换输入法
"vim.autoSwitchInputMethod.enable": true,
"vim.autoSwitchInputMethod.defaultIM": "com.apple.keylayout.ABC", // 这里输入你刚刚获得的英文输入法名称
"vim.autoSwitchInputMethod.obtainIMCmd": "/usr/local/bin/im-select",
"vim.autoSwitchInputMethod.switchIMCmd": "/usr/local/bin/im-select {im}"
vsocde-vim 中内置了很多插件,使用它们可以快速的进行某一些操作。
1)vim-esaymotion 插件,使用它之前你必须在 settings.json 中加入下面这一行:
"vim.easymotion": true,
它的作用是通过以下的按键组合,你可以快速的定位到任何你想修改的行中:
<leader><leader>s<char>
2)vim-surround 插件,如果你想修改、或者删除单引号和双引号,它带给你的体验将是无与伦比的,以下是它的语法:
ds<existing>
cs<existing><desired>
示例如下:
# 删除以下的[]
[1, 2, 3] -> ds[
# 将以下的[]修改为()
[1, 2, 3] -> cs[(
3)vim-commentary 插件,该插件能够快速的利用键盘进行行或者块的注释,它内部其实是调用了 vscode 的注释 API:
gcc :行注释
gCC :块注释
除此之外,vscode-vim 插件也额外提供了一些非常多的快捷键用于编辑操作:
gd : 跳转到函数定义或引用处,搭配cmd+i/cmd+o查看源码很方便
gh : 触发帮助提示
gb : 开启多光标模式,选中和当前单词相同的单词
有一些朋友可能和我一样安装了google-translate插件或者comment-translate插件来查看源代码注释的翻译结果,这个时候你将会遇到一些问题。
如一次性选择翻译的文本太长,vscode 的 hover 无法支持 scroll 的快捷键滚动操作,你只能通过鼠标滚轮进行 hover 窗口的滚动:
该问题在 vscode 的 github 上多次被提到,这对全键盘操作用户来说是非常不友好的,可以参见#69836。
对此有一个折中的解决方案,安装一个Doc View插件,将翻译结果放在侧边栏中,这样我们就有足够的空间来显示翻译结果了:
另外一个问题就是关于 Error 的提示信息,你可以自定义一些热键来快速查看 Error,或者跟我一样安装一个Error Lens插件。
如下图所示,有了该插件我们就能快速的查看到一些问题的提示信息了:
配合上面定义的 g[或者 g]热键,就可以快速的定位到某个错误并进行修改。
为什么不选择使用 vim 来搭建一个 IDE 呢?
- 太麻烦、懒得折腾,vscode 足够强大…
为什么不选择使用 vscode neovim 插件呢?
- visual 模式不是 vscode 的真正选择,多端同步比 vscode-vim 麻烦是我没选择它的主要原因
vim 真的很好用吗?
- 使用 vim 能治好你的 vim 崇拜症,但是 vim 并不是必须的
此外,网上搜了很多资料无一例外都是比较浅显的介绍了一下 vsocode-vim 插件就完了,但是真正让你能够搭配出一套全键盘使用方案的文章少之又少,所以这里就将我的折腾历程发了出来,很显然它比单纯的折腾 vim 要快很多。
参考文档:
据说 99% 的人不知道 vue-devtools 还能直接打开对应组件文件?本文原理揭秘
据说 99% 的人不知道 vue-devtools 还能直接打开对应组件文件?本文原理揭秘
- 前言
你好,我是若川[1],微信搜索「若川视野」关注我,专注前端技术分享,一个愿景是帮助 5 年内前端开阔视野走向前列的公众号。欢迎加我微信
ruochuan12
,长期交流学习。
这是
学习源码整体架构系列
之 launch-editor 源码(第九篇)。学习源码整体架构系列文章 (有哪些必看的 JS 库):jQuery、underscore、lodash、sentry、vuex、axios、koa、redux。
本文仓库地址[2]:
git clone [https://github.com/lxchuan12/open-in-editor.git](https://github.com/lxchuan12/open-in-editor.git)
,本文最佳阅读方式,克隆仓库自己动手调试,容易吸收消化。
要是有人说到怎么读源码,正在读文章的你能推荐我的源码系列文章,那真是无以为报啊。
我的文章尽量写得让想看源码又不知道怎么看的读者能看懂。我都是推荐使用搭建环境断点调试源码学习,哪里不会点哪里,边调试边看,而不是硬看。正所谓:授人与鱼不如授人予渔。
阅读本文后你将学到:
- 如何解决该功能报错问题
- 如何调试学习源码
launch-editor-middleware、launch-editor
等实现原理
1.1 短时间找不到页面对应源文件的场景
不知道你们有没有碰到这样的场景,打开你自己(或者你同事)开发的页面,却短时间难以找到对应的源文件。
这时你可能会想要是能有点击页面按钮自动用编辑器打开对应文件的功能,那该多好啊。
而vue-devtools
提供了这样的功能,也许你不知道。我觉得很大一部分人都不知道,因为感觉很多人都不常用vue-devtools
。
open-in-editor
你也许会问,我不用vue
,我用react
有没有类似功能啊,有啊,请看 react-dev-inspector[3]。你可能还会问,支持哪些编辑器呀,主流的 vscode、webstorm、atom、sublime
等都支持,更多可以看这个列表 Supported editors[4]。
本文就是根据学习尤大写的 launch-editor[5] 源码,本着知其然,知其所以然的宗旨,探究 vue-devtools
「在编辑器中打开组件」功能实现原理。
1.2 一句话简述其原理
code path/to/file
一句话简述原理:利用nodejs
中的child_process
,执行了类似code path/to/file
命令,于是对应编辑器就打开了相应的文件,而对应的编辑器则是通过在进程中执行ps x
(Window
则用Get-Process
)命令来查找的,当然也可以自己指定编辑器。
1.3 打开编辑器无法打开组件的报错解决方法
而你真正用这个功能时,你可能碰到报错,说不能打开这个文件。
`Could not open App.vue in the editor.
To specify an editor, specify the EDITOR env variable or add "editor" field to your Vue project config.
`
控制台不能打开编辑器的错误提示
这里说明下写这篇文章时用的是
Windows
电脑,VSCode
编辑器,在Ubuntu
子系统下使用的终端工具。同时推荐我的文章使用 ohmyzsh 打造 windows、ubuntu、mac 系统高效终端命令行工具,用过的都说好。
解决办法也简单,就是这句英文的意思。
1.3.1 方法一:先确保在终端能用命令打开你使用的编辑器,文中以VSCode
为例
如果你的命令行本身就不能运行code
等命令打开编辑器,那肯定是报错的。这时需要把VSCode
注入到命令行终端中。注入方法也简单。我的交流群里有小伙伴提供了mac
电脑的截图。
mac
电脑在 VSCode
command + shift + p
,Windows
则是 ctrl + shift + p
。然后输入shell
,选择安装code
。如下图:
Install 'code' command in PATH
这样就能在终端中打开VSCode
的了。
如果能在终端打开使用命令编辑器能打开,但实际上还是报错,那么大概率是没有识别到你的编辑器。那么可以通过方法二设置指定编辑器。
1.3.2 方法二:具体说明编辑器,在环境变量中说明指定编辑器
在vue
项目的根目录下,对应本文则是:vue3-project
,添加.env.delelopment
文件,其内容是EDITOR=code
。这里重点说明下,我的 vue-cli
版本是4.5.12
,好像在vue-cli 3.5
及以上版本才支持自定义EDITOR
这样的环境变量。
# .env.development # 当然,我的命令行终端已经有了 code 这个命令。 EDITOR=code
不用指定编辑器的对应路径(
c/Users/lxchu/AppData/Local/Programs/Microsoft VS Code/bin/code
),因为会报错。为什么会报错,因为我看了源码且试过。因为会被根据空格截断,变成c/Users/lxchu/AppData/Local/Programs/Microsoft
,当然就报错了。
也有可能你的编辑器路径有中文路径导致报错,可以在环境变量中添加你的编辑器路径。
如果你通过以上方法,还没解决报错问题。欢迎留言,或者加我微信 ruochuan12
交流。毕竟电脑环境不一,很难保证所有人都能正常执行,但我们知道了其原理,就很容易解决问题。
接下来我们从源码角度探究「在编辑器中打开组件」功能的实现原理。
2. vue-devtools Open component in editor 文档
探究原理之前,先来看看vue-devtools
官方文档。
vuejs/vue-devtools[6]文档
Open component in editor
To enable this feature, follow this guide[7].
这篇指南中写了在Vue CLI 3
中是开箱即用。
Vue CLI 3 supports this feature out-of-the-box when running vue-cli-service serve.
也详细写了如何在Webpack
下使用。
# 1. Import the package: var openInEditor = require('launch-editor-middleware') # 2. In the devServer option, register the /__open-in-editor HTTP route: devServer: { before (app) { app.use('/__open-in-editor', openInEditor()) } } # 3. The editor to launch is guessed. You can also specify the editor app with the editor option. See the supported editors list. # 用哪个编辑器打开会自动猜测。你也可以具体指明编辑器。这里显示更多的支持编辑器列表 openInEditor('code') # 4. You can now click on the name of the component in the Component inspector pane (if the devtools knows about its file source, a tooltip will appear). # 如果 `vue-devtools` 开发者工具有提示点击的组件的显示具体路径,那么你可以在编辑器打开。
同时也写了如何在Node.js
中使用等。
Node.js
You can use the launch-editor[8] package to setup an HTTP route with the/__open-in-editor
path. It will receive file as an URL variable.
查看更多可以看这篇指南[9]。
3. 环境准备工作
熟悉我的读者,都知道我都是推荐调试看源码的,正所谓:哪里不会点哪里。而且调试一般都写得很详细,是希望能帮助到一部分人知道如何看源码。于是我特意新建一个仓库 open-in-editor[10] git clone https://github.com/lxchuan12/open-in-editor.git
,便于大家克隆学习。
安装vue-cli
npm install -g @vue/cli # OR yarn global add @vue/cli
node -V # v14.16.0 vue -V # @vue/cli 4.5.12 vue create vue3-project # 这里选择的是 vue3、vue2 也是一样的。 # Please pick a preset: Default (Vue 3 Preview) ([Vue 3] babel, eslint) npm install # OR yarn install
这里同时说明下我的 vscode 版本。
code -v 1.55.2
前文提到的Vue CLI 3
中开箱即用和Webpack
使用方法。
vue3-project/package.json
中有一个debug
按钮。
debug 示意图
选择第一项,serve vue-cli-service serve
。
我们来搜索下'launch-editor-middleware'
这个中间件,一般来说搜索不到node_modules
下的文件,需要设置下。当然也有个简单做法。就是「排除的文件」右侧旁边有个设置图标「使用 “排查设置” 与“忽略文件”」,点击下。
其他的就不赘述了。可以看这篇知乎回答:vscode 怎么设置可以搜索包含 node_modules 中的文件?[11]
这时就搜到了vue3-project/node_modules/@vue/cli-service/lib/commands/serve.js
中有使用这个中间件。
4. vue-devtools 开箱即用具体源码实现
接着我们来看Vue CLI 3
中开箱即用具体源码实现。
// vue3-project/node_modules/@vue/cli-service/lib/commands/serve.js // 46 行 const launchEditorMiddleware = require('launch-editor-middleware') // 192 行 before (app, server) { // launch editor support. // this works with vue-devtools & @vue/cli-overlay app.use('/__open-in-editor', launchEditorMiddleware(() => console.log( `To specify an editor, specify the EDITOR env variable or ` + `add "editor" field to your Vue project config.\n` ))) // 省略若干代码... }
点击vue-devtools
中的时,会有一个请求,http://localhost:8080/__open-in-editor?file=src/App.vue
,不出意外就会打开该组件啦。
open src/App.vue in editor
接着我们在launchEditorMiddleware
的具体实现。
5. launch-editor-middleware
看源码时,先看调试截图。
debug-launch
在launch-editor-middleware
中间件中作用在于最终是调用 launch-editor
打开文件。
`// vue3-project/node_modules/launch-editor-middleware/index.js
const url = require('url')
const path = require('path')
const launch = require('launch-editor')
module.exports = (specifiedEditor, srcRoot, onErrorCallback) => {
// specifiedEditor => 这里传递过来的则是 () => console.log() 函数
// 所以和 onErrorCallback 切换下,把它赋值给错误回调函数
if (typeof specifiedEditor === 'function') {
onErrorCallback = specifiedEditor
specifiedEditor = undefined
}
// 如果第二个参数是函数,同样把它赋值给错误回调函数
// 这里传递过来的是 undefined
if (typeof srcRoot === 'function') {
onErrorCallback = srcRoot
srcRoot = undefined
}
// srcRoot 是传递过来的参数,或者当前 node 进程的目录
srcRoot = srcRoot || process.cwd()
// 最后返回一个函数, express 中间件
return function launchEditorMiddleware (req, res, next) {
// 省略 ...
}
}
`
上一段中,这种切换参数的写法,在很多源码中都很常见。为的是方便用户调用时传参。虽然是多个参数,但可以传一个或者两个。
可以根据情况打上断点。比如这里我会在launch(path.resolve(srcRoot, file), specifiedEditor, onErrorCallback)
打断点。
// vue3-project/node_modules/launch-editor-middleware/index.js module.exports = (specifiedEditor, srcRoot, onErrorCallback) => { // 省略上半部分 return function launchEditorMiddleware (req, res, next) { // 根据请求解析出 file 路径 const { file } = url.parse(req.url, true).query || {} // 如果没有文件路径,则报错 if (!file) { res.statusCode = 500 res.end(`launch-editor-middleware: required query param "file" is missing.`) } else { // 否则拼接路径,用 launch 打开。 launch(path.resolve(srcRoot, file), specifiedEditor, onErrorCallback) res.end() } } }
6. launch-editor
跟着断点来看,走到了launchEditor
函数。
`// vue3-project/node_modules/launch-editor/index.js
function launchEditor (file, specifiedEditor, onErrorCallback) {
// 解析出文件路径和行号列号等信息
const parsed = parseFile(file)
let {fileName} = parsed
const {lineNumber, columnNumber} = parsed
// 判断文件是否存在,不存在,直接返回。
if (!fs.existsSync(fileName)) {
return
}
// 所以和 onErrorCallback 切换下,把它赋值给错误回调函数
if (typeof specifiedEditor === 'function') {
onErrorCallback = specifiedEditor
specifiedEditor = undefined
}
// 包裹一层函数
onErrorCallback = wrapErrorCallback(onErrorCallback)
// 猜测当前进程运行的是哪个编辑器
const [editor, ...args] = guessEditor(specifiedEditor)
if (!editor) {
onErrorCallback(fileName, null)
return
}
// 省略剩余部分,后文再讲述...
}
`
6.1 wrapErrorCallback 包裹错误函数回调
onErrorCallback = wrapErrorCallback(onErrorCallback)
这段的代码,就是传递错误回调函数,wrapErrorCallback
返回给一个新的函数,wrapErrorCallback
执行时,再去执行 onErrorCallback(cb)。
我相信读者朋友能看懂,我单独拿出来讲述,主要是因为这种包裹函数的形式在很多源码里都很常见。
这里也就是文章开头终端错误图Could not open App.vue in the editor.
输出的代码位置。
// vue3-project/node_modules/launch-editor/index.js function wrapErrorCallback (cb) { return (fileName, errorMessage) => { console.log() console.log( chalk.red('Could not open' + path.basename(fileName) + 'in the editor.') ) if (errorMessage) { if (errorMessage[errorMessage.length - 1] !== '.') { errorMessage += '.' } console.log( chalk.red('The editor process exited with an error:' + errorMessage) ) } console.log() if (cb) cb(fileName, errorMessage) } }
6.2 guessEditor 猜测当前正在使用的编辑器
这个函数主要做了如下四件事情:
- 如果具体指明了编辑器,则解析下返回。
- 找出当前进程中哪一个编辑器正在运行。
macOS
和Linux
用ps x
命令
windows
则用Get-Process
命令 - 如果都没找到就用
process.env.VISUAL
或者process.env.EDITOR
。这就是为啥开头错误提示可以使用环境变量指定编辑器的原因。 - 最后还是没有找到就返回
[null]
,则会报错。
const [editor, ...args] = guessEditor(specifiedEditor) if (!editor) { onErrorCallback(fileName, null) return }
``// vue3-project/node_modules/launch-editor/guess.js
const shellQuote = require('shell-quote')
const childProcess = require('child_process')
module.exports = function guessEditor (specifiedEditor) {
// 如果指定了编辑器,则解析一下,这里没有传入。如果自己指定了路径。
// 比如 c/Users/lxchu/AppData/Local/Programs/Microsoft VS Code/bin/code
// 会根据空格切割成 c/Users/lxchu/AppData/Local/Programs/Microsoft
if (specifiedEditor) {
return shellQuote.parse(specifiedEditor)
}
// We can find out which editor is currently running by:
// ps x
on macOS and Linux
// Get-Process
on Windows
try {
// 代码有删减
if (process.platform === 'darwin') {
const output = childProcess.execSync('ps x').toString()
// 省略
} else if (process.platform === 'win32') {
const output = childProcess
.execSync('powershell -Command"Get-Process | Select-Object Path"', {
stdio: ['pipe', 'pipe', 'ignore']
})
.toString()
// 省略
} else if (process.platform === 'linux') {
const output = childProcess
.execSync('ps x --no-heading -o comm --sort=comm')
.toString()
}
} catch (error) {
// Ignore...
}
// Last resort, use old skool env vars
if (process.env.VISUAL) {
return [process.env.VISUAL]
} else if (process.env.EDITOR) {
return [process.env.EDITOR]
}
return [null]
}
``
看完了 guessEditor 函数,我们接着来看 launch-editor
剩余部分。
6.3 launch-editor 剩余部分
以下这段代码不用细看,调试的时候细看就行。
`// vue3-project/node_modules/launch-editor/index.js function launchEditor(){ // 省略上部分... if ( process.platform === 'linux' && fileName.startsWith('/mnt/') && /Microsoft/i.test(os.release()) ) { // Assume WSL / "Bash on Ubuntu on Windows" is being used, and // that the file exists on the Windows file system. //
os.release()` is "4.4.0-43-Microsoft" in the current release
// build of WSL, see: microsoft/WSL#423 (comment)
// When a Windows editor is specified, interop functionality can
// handle the path translation, but only if a relative path is used.
fileName = path.relative('', fileName)
}
if (lineNumber) {
const extraArgs = getArgumentsForPosition(editor, fileName, lineNumber, columnNumber)
args.push.apply(args, extraArgs)
} else {
args.push(fileName)
}
if (_childProcess && isTerminalEditor(editor)) {
// There's an existing editor process already and it's attached
// to the terminal, so go kill it. Otherwise two separate editor
// instances attach to the stdin/stdout which gets confusing.
_childProcess.kill('SIGKILL')
}
if (process.platform === 'win32') {
// On Windows, launch the editor in a shell because spawn can only
// launch .exe files.
_childProcess = childProcess.spawn(
'cmd.exe',
['/C', editor].concat(args),
{stdio: 'inherit'}
)
} else {
_childProcess = childProcess.spawn(editor, args, { stdio: 'inherit'})
}
_childProcess.on('exit', function (errorCode) {
_childProcess = null
if (errorCode) {
onErrorCallback(fileName, '(code' + errorCode + ')')
}
})
_childProcess.on('error', function (error) {
onErrorCallback(fileName, error.message)
})
}
``
这一大段中,主要的就是以下代码,用子进程模块。简单来说子进程模块有着执行命令的能力。
`const childProcess = require('child_process')
if (process.platform === 'win32') {
// On Windows, launch the editor in a shell because spawn can only
// launch .exe files.
_childProcess = childProcess.spawn(
'cmd.exe',
['/C', editor].concat(args),
{stdio: 'inherit'}
)
} else {
_childProcess = childProcess.spawn(editor, args, { stdio: 'inherit'})
}
`
行文至此,就基本接近尾声了。原理其实就是利用nodejs
中的child_process
,执行了类似code path/to/file
命令。
7. 总结
这里总结一下:首先文章开头通过提出「短时间找不到页面对应源文件的场景」,并针对容易碰到的报错情况给出了解决方案。其次,配置了环境跟着调试学习了vue-devtools
中使用的尤大写的 yyx990803/launch-editor[12]。
7.1 一句话简述其原理
我们回顾下开头的原理内容。
code path/to/file
一句话简述原理:利用nodejs
中的child_process
,执行了类似code path/to/file
命令,于是对应编辑器就打开了相应的文件,而对应的编辑器则是通过在进程中执行ps x
(Window
则用Get-Process
)命令来查找的,当然也可以自己指定编辑器。
最后还能做什么呢。
可以再看看 umijs/launch-editor[13] 和 react-dev-utils/launchEditor.js[14] 。他们的代码几乎类似。
也可以利用Node.js
做一些提高开发效率等工作,同时可以学习child_process
等模块。
也不要禁锢自己的思维,把前端禁锢在页面中,应该把视野拓宽。
Node.js
是我们前端人探索操作文件、操作网络等的好工具。
如果读者朋友发现有不妥或可改善之处,再或者哪里没写明白的地方,欢迎评论指出。另外觉得写得不错,对您有些许帮助,可以点赞、评论、转发分享,也是对我的一种支持,万分感谢。如果能关注我的前端公众号:「若川视野」,就更好啦。
参考资料
[1]
若川: https://lxchuan12.gitee.io
[2]
本文仓库地址: https://github.com/lxchuan12/open-in-editor.git
[3]
最近组建了一个江西人的前端交流群,如果你也是江西人可以加我微信 ruochuan12 拉你进群。
················· 若川出品 ·················
今日话题
之前发的阅读量惨淡,所以现在主要改了标题,还有根据读者一些反馈修改更新,所以重新编辑发布下原创,争取投稿到一些大号。欢迎在下方留言~ 欢迎分享、收藏、点赞、在看我的公众号文章~
一个愿景是帮助 5 年内前端人走向前列的公众号
可加我个人微信 ruochuan12,长期交流学习
推荐阅读
若川知乎问答:2 年前端经验,做的项目没什么技术含量,怎么办?
若川视野
建议工作 5 年内的前端人关注。我是若川,《学习源码整体架构系列》作者,知乎、掘金等平台的文章累计超过百万阅读。致力于前端开发经验分享。愿景:帮助 5 年内的前端人开阔视野不断成长,走在前列。
55 篇原创内容
公众号
点击上方卡片关注我、加个星标,或者查看源码等系列文章。
学习源码整体架构系列、年度总结、JS 基础系列
https://mp.weixin.qq.com/s/bvhg_7AaJxfa7PSl3wKHOA
vue2.x项目报错 vuex.esm.js sub is not function-pudn.com
aiyang1214878408 于 2022-05-12 18:48:59
场景
一般在 vue2.x 项目中我们会使用 Vue DevTools 插件来帮助我们开发,但是今天项目突然出现 bug,调用 vuex 的 dispatch 方法时突然出现报错,如下:
TypeError: sub is not a function
at eval (vuex.esm.js?2f62:422:1)
at Array.forEach (<anonymous>)
at Store.dispatch (vuex.esm.js?2f62:422:1)
at Store.boundDispatch [as dispatch] (vuex.esm.js?2f62:332:1)
at eval (list.vue?51fc:432:1)
排查原因发现是使用 Vue DevTools 插件的问题,因为我的浏览器现在安装了 Vue DevTools2.x 和 3.x 的版本,这次出问题主要是在 vue.2x 的项目中使用 Vue.js devtools 6.0.0 beta 15(3.x)版本引起的。
后面发现不仅是调 vuex 可能报错,页面之间跳转可能也会引发异常.
解决方法:
- 关闭或者重启 Vue.js devtools 6.0.0 beta 15(3.x)版本
- 在 Vue 2 内置插件中添加了开启设置 Legacy Actions
第一种方法有手就行,这里主要是说下第二张方法
在 Vue 2 内置插件中添加了开启设置 Legacy Actions
- 首先打开控制台找到自己的 vue 插件,然后点击右边的三个小圆点
-
点击左边的 vue2, 就会出现右边的红色框框起来的东西,最后点击打开里面的 Legacy Actions 按钮
注意:我之前 vue3.x 版本用的是 devtools 6.0.0 beta 15,发现是没有第三步的 Legacy Actions 按钮,所以我去仓库拉了一个新的 3.x 版本的,这样就有这个配置了。(现在我用的是 Vue.js devtools 6.1.4)
本内容为 PUDN 经合法授权发布,文章内容为作者独立观点,不代表 PUDN 立场,未经允许不得转载。
https://www.pudn.com/news/627e2c4b5981aa38efad852f.html
跟着动画学习TCP三次握手和四次挥手
TCP三次握手和四次挥手的问题在面试中是最为常见的考点之一。很多读者都知道三次和四次,但是如果问深入一点,他们往往都无法作出准确回答。
本篇尝试使用动画来对这个知识点进行讲解,期望读者们可以更加简单地地理解TCP交互的本质。
TCP 三次握手
TCP 三次握手就好比两个人在街上隔着50米看见了对方,但是因为雾霾等原因不能100%确认,所以要通过招手的方式相互确定对方是否认识自己。
张三首先向李四招手(syn),李四看到张三向自己招手后,向对方点了点头挤出了一个微笑(ack)。张三看到李四微笑后确认了李四成功辨认出了自己(进入estalished状态)。
但是李四还有点狐疑,向四周看了一看,有没有可能张三是在看别人呢,他也需要确认一下。所以李四也向张三招了招手(syn),张三看到李四向自己招手后知道对方是在寻求自己的确认,于是也点了点头挤出了微笑(ack),李四看到对方的微笑后确认了张三就是在向自己打招呼(进入established状态)。
于是两人加快步伐,走到了一起,相互拥抱。
我们看到这个过程中一共是四个动作,张三招手--李四点头微笑--李四招手--张三点头微笑。其中李四连续进行了2个动作,先是点头微笑(回复对方),然后再次招手(寻求确认),实际上可以将这两个动作合一,招手的同时点头和微笑(syn+ack)。于是四个动作就简化成了三个动作,张三招手--李四点头微笑并招手--张三点头微笑。这就是三次握手的本质,中间的一次动作是两个动作的合并。
我们看到有两个中间状态,syn_sent和syn_rcvd,这两个状态叫着「半打开」状态,就是向对方招手了,但是还没来得及看到对方的点头微笑。syn_sent是主动打开方的「半打开」状态,syn_rcvd是被动打开方的「半打开」状态。客户端是主动打开方,服务器是被动打开方。
-
syn_sent: syn package has been sent
-
syn_rcvd: syn package has been received
TCP 数据传输
TCP 数据传输就是两个人隔空对话,差了一点距离,所以需要对方反复确认听见了自己的话。
张三喊了一句话(data),李四听见了之后要向张三回复自己听见了(ack)。
如果张三喊了一句,半天没听到李四回复,张三就认为自己的话被大风吹走了,李四没听见,所以需要重新喊话,这就是tcp重传。
也有可能是李四听到了张三的话,但是李四向张三的回复被大风吹走了,以至于张三没听见李四的回复。张三并不能判断究竟是自己的话被大风吹走了还是李四的回复被大风吹走了,张三也不用管,重传一下就是。
既然会重传,李四就有可能同一句话听见了两次,这就是「去重」。「重传」和「去重」工作操作系统的网络内核模块都已经帮我们处理好了,用户层是不用关心的。
张三可以向李四喊话,同样李四也可以向张三喊话,因为tcp链接是「双工的」,双方都可以主动发起数据传输。不过无论是哪方喊话,都需要收到对方的确认才能认为对方收到了自己的喊话。
张三可能是个高射炮,一说连说了八句话,这时候李四可以不用一句一句回复,而是连续听了这八句话之后,一起向对方回复说前面你说的八句话我都听见了,这就是批量ack。但是张三也不能一次性说了太多话,李四的脑子短时间可能无法消化太多,两人之间需要有协商好的合适的发送和接受速率,这个就是「TCP窗口大小」。
网络环境的数据交互同人类之间的对话还要复杂一些,它存在数据包乱序的现象。同一个来源发出来的不同数据包在「网际路由」上可能会走过不同的路径,最终达到同一个地方时,顺序就不一样了。操作系统的网络内核模块会负责对数据包进行排序,到用户层时顺序就已经完全一致了。
TCP 四次挥手
TCP断开链接的过程和建立链接的过程比较类似,只不过中间的两部并不总是会合成一步走,所以它分成了4个动作,张三挥手(fin)——李四伤感地微笑(ack)——李四挥手(fin)——张三伤感地微笑(ack)。
之所以中间的两个动作没有合并,是因为tcp存在「半关闭」状态,也就是单向关闭。张三已经挥了手,可是人还没有走,只是不再说话,但是耳朵还是可以继续听,李四呢继续喊话。等待李四累了,也不再说话了,超张三挥了挥手,张三伤感地微笑了一下,才彻底结束了。
上面有一个非常特殊的状态time_wait
,它是主动关闭的一方在回复完对方的挥手后进入的一个长期状态,这个状态标准的持续时间是4分钟,4分钟后才会进入到closed状态,释放套接字资源。不过在具体实现上这个时间是可以调整的。
它就好比主动分手方要承担的责任,是你提出的要分手,你得付出代价。这个后果就是持续4分钟的time_wait
状态,不能释放套接字资源(端口),就好比守寡期,这段时间内套接字资源(端口)不得回收利用。
它的作用是重传最后一个ack报文,确保对方可以收到。因为如果对方没有收到ack的话,会重传fin报文,处于time_wait状态的套接字会立即向对方重发ack报文。
同时在这段时间内,该链接在对话期间于网际路由上产生的残留报文(因为路径过于崎岖,数据报文走的时间太长,重传的报文都收到了,原始报文还在路上)传过来时,都会被立即丢弃掉。4分钟的时间足以使得这些残留报文彻底消逝。不然当新的端口被重复利用时,这些残留报文可能会干扰新的链接。
4分钟就是2个MSL,每个MSL是2分钟。MSL就是maximium segment lifetime
——最长报文寿命。这个时间是由官方RFC协议规定的。至于为什么是2个MSL而不是1个MSL,我还没有看到一个非常满意的解释。
四次挥手也并不总是四次挥手,中间的两个动作有时候是可以合并一起进行的,这个时候就成了三次挥手,主动关闭方就会从fin_wait_1
状态直接进入到time_wait
状态,跳过了fin_wait_2
状态。
总结
TCP状态转换是一个非常复杂的过程,本文仅对一些简单的基础知识点进行了类比讲解。关于TCP的更多知识还需要读者去搜寻相关技术文章进入深入学习。如果读者对TCP的基础知识掌握得比较牢固,高级的知识理解起来就不会太过于吃力。
关于TCP的更多文章,还请关注微信公众号「码洞」进行订阅,后续我会持续更新更多细节。
网页外链用了 target="_blank",结果悲剧了 - 掘金
今天给大家分享一个 Web 知识点。如果你有过一段时间的 Web 开发经验,可能已经知道了。不过对于刚接触的新手来说,还是有必要了解一下的。
我们知道,网页里的a
标签默认在当前窗口跳转链接地址,如果需要在新窗口打开,需要给 a
标签添加一个target="_blank"
属性。
<a href="http://kaysonli.com/" target="_blank">1024译站</a>
复制代码
顺便提下一个有意思的现象,很早之前我就发现,国外网站倾向于在当前页跳转,而国内网站喜欢打开新窗口。不信你们可以去验证下。我不知道这是交互设计上的文化差异,还是技术上的开发习惯。
当然,这两种方式各有优缺点。当前页跳转显得操作比较有连贯性,不会贸然打断用户的注意力,也会减少浏览器的窗口(tab 页)数量。但是对于需要反复回到初始页面的场景来说,就很麻烦了。比如搜索结果页面,通常需要查看对比几个目标地址,保留在多个窗口还是比较方便。
今天要说的不只是用户体验上的差别,而是涉及安全和性能。
安全隐患
如果只是加上target="_blank"
,打开新窗口后,新页面能通过window.opener
获取到来源页面的window
对象,即使跨域也一样。虽然跨域的页面对于这个对象的属性访问有所限制,但还是有漏网之鱼。
这是某网页打开新窗口的页面控制台输出结果。可以看到window.opener
的一些属性,某些属性的访问被拦截,是因为跨域安全策略的限制。
即便如此,还是给一些操作留下可乘之机。比如修改window.opener.location
的值,指向另外一个地址。你想想看,刚刚还是在某个网站浏览,随后打开了新窗口,结果这个新窗口神不知鬼不觉地把原来的网页地址改了。这个可以用来做什么?钓鱼啊!等你回到那个钓鱼页面,已经伪装成登录页,你可能就稀里糊涂把账号密码输进去了。
还有一种玩法,如果你处于登录状态,有些操作可能只是发送一个GET
请求就完事了。通过修改地址,就执行了非你本意的操作,其实就是 CSRF 攻击。
性能问题
除了安全隐患外,还有可能造成性能问题。通过target="_blank"
打开的新窗口,跟原来的页面窗口共用一个进程。如果这个新页面执行了一大堆性能不好的 JavaScript 代码,占用了大量系统资源,那你原来的页面也会受到池鱼之殃。
解决方案
尽量不使用target="_blank"
,如果一定要用,需要加上rel="noopener"
或者rel="noreferrer"
。这样新窗口的window.openner
就是null
了,而且会让新窗口运行在独立的进程里,不会拖累原来页面的进程。当然,有些浏览器对性能做了优化,即使不加这个属性,新窗口也会在独立进程打开。不过为了安全考虑,还是加上吧。
我特意用自己的博客网站 1024 译站 试了一下,点击里面的外链打开新页面,window.openner
都是null
。查看页面元素发现,a
标签都加上了rel="noreferrer"
。博客是用 Hexo 生成的,看来这种设置已经成了基本常识了。
另外,对于通过window.open
的方式打开的新页面,可以这样做:
var yourWindow = window.open();
yourWindow.opener = null;
yourWindow.location = "http://someurl.here";
yourWindow.target = "_blank";
复制代码
看到这个颇有气质的 logo,不来关注下吗?
[VS Code]跟我一起在Visual Studio Code 添加自定义snippet(代码段),附详细配置_开发工具_猫科龙-CSDN博客
日志:
- 2019.09.06 VSCode 自 v1.38 起,引入了新的变量「CURRENT_SECONDS_UNIX」;自 v1.33 起,引入了新的变量「WORKSPACE_NAME」。本次更新即旨于介绍这些新变量,同时评论显示,很多朋友都困惑于如何打印特殊字符如「$」,本次同时加入对这部分的介绍;
- 2019.01.19 VSCode 自 v1.30 起,开始支持注释变量(comment variables),这些变量会随着当前语言而进行适应;自 v1.28 起,开始支持工作目录专属代码片(Project level snippets)及多前缀(multiple prefixes)特性。本次更新即旨于介绍这些新特性。另外笔者觉得中英文混杂的排版很扎眼睛,所以移除了大量中英文混杂的语句;
- 2018.07.16 VSCode 自 v1.25 起,开始支持占位符转换(placeholder transformations)了,其用于在进行占位符跳转时(1→2)对当前占位符(1)适用正则替换。新特性听起来和变量转换很像,它们的区别在于占位符转换适用于占位符,而变量转换适用于变量。前者更灵活,后者更省心。本次更新即旨于介绍这个新特性,并再次对排版进行适应性调整;
- 2018.05.13 VSCode 自 v1.20 起,开始支持更多变量,其用于读取剪贴板内容及插入当前日期。本次更新即旨于介绍这些新变量。此外,本次更新还移除了一些废话,并对排版进行了微调;
- 2017.10.11 VSCode 自 v1.17 起,其代码片引擎开始支持变量转换(variable transformations)特性,变量的值可以经过格式化处理后,再插入预定的位置。这是一个很强大的特性;自 v1.15 起,开始支持 Choice 了。本次更新即旨于介绍这些新特性。
推广:
- 「VS Code」Visual Studio Code 菜鸟教程:从入门到精通。你能找到的最好的 VSC 教程。
- 「VS Code」如何在 Visual Studio Code 中通过跳板机连接远程服务器:Remote-SSH 篇。你能找到的最好的 VSC SSH 教程。
前记:今天试着用了下 Atom,发现 Atom 居然预装了 CLANG 的 snippets,而且远比 VSCode 的已有拓展「C/C++ Snippets」中的丰富!身为 VSCode 的死忠粉,我决定立马把 Atom 的 C snippets 搬到 VSCode 上来。
既然你点开了这个页面,那就说明要么你不知道 VSCode 上已有拓展「C/C++ Snippets」,要么你对这个拓展不甚满意。对于后者,本文将为你介绍如何在 VSCode 上设置 snippets,并为你提供一套可以直接用的 C 语言 snippets。
1. 代码片简介
snippet[ˈsnɪpɪt],或者说「code snippet」,也即代码片,指的是能够帮助输入重复代码模式,比如循环或条件语句,的模板。通过 snippet ,我们仅仅输入一小段字符串,就可以在代码片引擎的帮助下,生成预定义的模板代码,接着我们还可以通过在预定义的光标位置之间跳转,来快速补全模板。
当然,看图更易懂。下图将 aja
补全为 JQuery 的 ajax() 方法,并通过光标的跳转,快速补全了待填键值对:
2. 代码片配置流程
首先,进入代码片设置文件,这里提供了三种方法:
- 通过快捷键「Ctrl + Shift + P」打开命令窗口(All Command Window),输入「snippet」,点选「首选项:配置用户代码片片段」;
- 点击界面最左侧竖栏(也即活动栏)最下方的齿轮按钮,在弹出来的菜单中点选「用户代码片段」;
- 按下「Alt」键切换菜单栏,通过文件 > 首选项 > 用户代码片段;
接着,在设置文件里补全代码片。以 C 语言为例,选中后你将打开一个设置文件,c.json,在文件头部你会看见一个注释,这其实是一个示例和对它的介绍。你可以试着将第 7~14 行反注释掉(选中后 Ctrl + “/”),从而尝试使用它。了解过「json」就不会对此感到奇怪。
{
"Print to console": {
"prefix": "log",
"body": [
"console.log('$1');",
"$2"
],
"description": "Log output to console"
}
}
这个示例中定义了一个描述为「Print to console」的代码片,其功能为:在 IntelliSense 中输入 log
并选中对应代码片后,可将原文本替换为 console.log('');
。效果预览如下:
3. 代码片详细介绍
3.1 语法结构
在第二章中,你已经接触到了代码片最简单的功能,而 VSCode 的代码片引擎所能做的远不止这些。本章将以官方教程1为本,详细地为大家介绍代码片。
代码片由四部分组成:
-
prefix:前缀。代码片从 IntelliSense 中呼出的「关键字」;
注:支持 N:1,数组中的每一项都能作为本条代码片的前缀。
-
scope: 域。代码片适用的「语言模式」;
注:可选,但只有「全局代码片」才能使用。不填代表适用于所有语言模式。
-
body:主体。代码片的「布局与控制」;
注:每个字符串表示一行。
-
description:描述。代码片在 IntelliSense 中的「介绍」。
注:可选。未定义的情况下直接显示对象名,上例中将显示
Print to console
。
3.2 Prefix 部分
前缀部分没有什么好介绍的,唯一值得注意的是,前缀支持 N:1,也即允许多条前缀对应同一条代码片。在使用时,只需将前缀定义为数组即可,数组中的每一个前缀都能对应本代码片。下面就是一个很简单的示例。
{
"prefix": [ "header", "stub", "copyright"],
"body": "Copyright. Foo Corp 2028",
"description": "Adds copyright...",
"scope": "javascript,typescript"
}
3.3 Scope 部分
前缀部分没有什么好介绍的,不过在引入了域的概念之后,会不由自主地想起一些问题,比如如何让同一条代码片根据语言进行微调,从而取得良好的可移植性。一个很容易想到的例子就是注释风格了。
某公司希望所有代码文件的头部都有公司的版权声明,但 python 风格的注释是 #
而 C 风格的注释是 //
,在每个语言的设置文件下都定义类似但注释风格不同的代码段显然会引入巨大的冗余。对此,VSCode 提供的解决方案是提供一些在不同语言下表现不同的变量。下一小节中已经更新了相关介绍。
3.4 Body 部分
3.4.1 基本结构
Body 部分可以使用特殊语法结构,来控制光标和要插入的文本,其支持的基本结构如下:
-
Tabstops:制表符
用「Tabstops」可以让编辑器的指针在 snippet 内跳转。使用
$1
,$2
等指定光标位置。这些数字指定了光标跳转的顺序。特别地,$0
表示最终光标位置。相同序号的「Tabstops」被链接在一起,将会同步更新,比如下列用于生成头文件封装的 snippet 被替换到编辑器上时,光标就将同时出现在所有$1
位置。"#ifndef $1" "#define $1" "#end // $1"
-
Placeholders:占位符
「Placeholder」是带有默认值的「Tabstops」,如
${1:foo}
。「placeholder」文本将被插入「Tabstops」位置,并在跳转时被全选,以方便修改。占位符还可以嵌套,例如${1:another ${2:placeholder}}
。比如,结构体的代码片主体可以这样写:
struct ${1:name_t} {\n\t$2\n};
作为「Placeholder」的
name_t
一方面可以提供默认的结构名称,另一方面可以作为输入的提示。 -
Choice:可选项
「Choice」是提供可选值的「Placeholder」。其语法为一系列用逗号隔开,并最终被两个竖线圈起来的枚举值,比如
${1|one,two,three|}
。当光标跳转到该位置的时候,用户将会被提供多个值(one 或 two 或 three)以供选择。 -
Variables:变量
使用
$name
或${name:default}
可以插入变量的值。当变量未赋值时(如),将插入其缺省值或空字符串。 当varibale
未知(即,其名称未定义)时,将插入变量的名称,并将其转换为「Placeholder」。可以使用的「Variable」如下:TM_SELECTED_TEXT
:当前选定的文本或空字符串;
注:选定后通过在命令窗口点选「插入代码片段」插入。TM_CURRENT_LINE
:当前行的内容;TM_CURRENT_WORD
:光标所处单词或空字符串
注:所谓光标一般为文本输入处那条闪来闪去的竖线,该项可定制。单词使用 VSCode 选词(Word Wrap)器选择。你最好只用它选择英文单词,因为这个选择器明显没有针对宽字符优化过,它甚至无法识别宽字符的标点符号。TM_LINE_INDEX
:行号(从零开始);TM_LINE_NUMBER
:行号(从一开始);TM_FILENAME
:当前文档的文件名;TM_FILENAME_BASE
:当前文档的文件名(不含后缀名);TM_DIRECTORY
:当前文档所在目录;TM_FILEPATH
:当前文档的完整文件路径;WORKSPACE_NAME
:当前工作目录的名称(而非完整路径);CLIPBOARD
:当前剪贴板中内容。
此外,还有一些用于插入当前时间的变量,这里单独列出:
CURRENT_YEAR
: 当前年份;CURRENT_YEAR_SHORT
: 当前年份的后两位;CURRENT_MONTH
: 格式化为两位数字的当前月份,如 02;CURRENT_MONTH_NAME
: 当前月份的全称,如 July;CURRENT_MONTH_NAME_SHORT
: 当前月份的简称,如 Jul;CURRENT_DATE
: 当天月份第几天;CURRENT_DAY_NAME
: 当天周几,如 Monday;CURRENT_DAY_NAME_SHORT
: 当天周几的简称,如 Mon;CURRENT_HOUR
: 当前小时(24 小时制);CURRENT_MINUTE
: 当前分钟;CURRENT_SECOND
: 当前秒数;CURRENT_SECONDS_UNIX
:Unix 时间戳。
此外,还有一些用于插入行/块注释的变量,其将根据当前文件的语言模式自动调整:
BLOCK_COMMENT_START
块注释上半段,输出示例:- PHP:
/*
- HTML:
<!--
- PHP:
BLOCK_COMMENT_END
块注释下半段,输出示例:- PHP:
*/
- HTML:
-->
- PHP:
LINE_COMMENT
行注释,输出示例:- PHP:
//
- HTML:
<!-- -->
- PHP:
注:这些都是变量名,不是宏,在实际使用的时要加上
$
符。
3.4.2 变量转换
变量转换可将变量的值格式化处理后插入预定的位置。
语法结构
我们可以通过 ${var_name/regular_expression/format_string/options}
插入格式化后的代码片。显然,「variable transformations」由 4 部分构成:
var_name
:变量名;regular_expression
:正则表达式;format_string
:格式串;options
:正则表达式匹配选项。
其中正则表达式的写法和匹配选项部分不在本篇博文的讲解范围之内,具体内容请分别参考 javascript 有关 RegExp(pattern [, flags])
构造函数中的 pattern
及 flags
参数项的说明2。
本文只对 format_string
部分进行详细介绍。
format_string 部分
根据其 EBNF 范式,我们可以知道 format_string
其实是 format
或 text
的线性组合:
text
:也即没有任何作用的普通文本,你甚至可以使用汉字;format
:格式串,分为 7 种:$sn
:表示插入匹配项;${sn}
:同$sn
;${sn:/upcase}
或${sn:/downcase}
或${sn:/capitalize}
:表示将匹配项变更为「所有字母均大写/所有字母均小写/首字母大写其余小写」后,插入;${sn:+if}
:表示当匹配成功时,并且捕捉括号捕捉特定序号的捕捉项成功时,在捕捉项位置插入「if」所述语句;${sn:?if:else}
:表示当匹配成功,并且捕捉括号捕捉特定序号的捕捉项成功时,在捕捉项位置插入「if」所述语句;否则当匹配成功,但当捕捉括号捕捉特定序号的捕捉项失败时,在捕捉项位置插入「else」所述语句;${sn:-else}
:表示当匹配成功,但当捕捉括号捕捉特定序号的捕捉项失败时,在捕捉项位置插入「else」所述语句;${sn:else}
:同${sn:-else}
。
format
的后三条理解起来可能比较困难。这里我们以倒数第三条为例进行说明。假设我们有一个「make.c」文件,我们有这么一条 snippet: "body": "${TM_FILENAME/make.c(pp|\+\+)?/${1:?c++:clang}/}"
。整个模式串匹配成功,但是捕捉括号捕捉后缀名中的 pp 或 ++ 失败,因此判断条件在捕捉括号的位置插入捕捉失败时应插入的字符串,也即「clang」。
注:
- 其中
sn
表示捕捉项的序号- 其中
if
表示捕捉项捕捉成功时替换的文本- 其中
else
表示捕捉项捕失败时替换的文本
案例分析
下面笔者再介绍一个简单的例子,帮助大家理解「variable transformations」。
假设有一个名为「make.c」的文件中,并且我们已经定义如下 snippet。
"#ifndef HEADER … #define … #endif":{
"prefix": "defheader",
"body": "#ifndef ${1:${TM_FILENAME/(.*)\\.C$/${1:/upcase}_H/i}} \n#define $1 \n${2:header content}\n#endif\t// $1"
}
这段 snippet 将生成下图所示代码:
其中最复杂的模式为:${1:${TM_FILENAME/(.*)\\.C$/${1:/upcase}_H/i}}
,我们将之拆解为如下五部分:
${1:...}
:嵌套的placeholder
;${TM_FILENAME/.../.../.}
:「variable transformations」中的「var_name」,表示带后缀的文件名;${.../(.*)\\.C$/.../.}
:「variable transformations」中的「regular_expression」,表示匹配任意以「.C」为后缀的字符串;${.../.../${1:/upcase}_H/.}}
:「variable transformations」中的「format_string」,表示将第一个捕捉项替换为大写的,并添加「_H」的后缀;${.../.../.../i}
:「variable transformations」中的「options」,表示无视大小写。
3.4.3 占位符转换
语法结构
我们可以通过 ${int/regular_expression/format_string/options}
插入格式化后的代码片。显然,与变量转换,「placeholder transformations」也由 4 部分构成:
int
:占位符相应光标序号;regular_expression
:正则表达式;format_string
:格式串;options
:正则表达式匹配选项。
上述全部内容我们都在前文介绍过了,因此此处不做赘述。我们唯一需要关注的是转换触发的时机:占位符转换将在进行占位符跳转(假设 1→2)的时候自动适用到当前占位符(1)。
案例分析
假设我们已经这样的 Snippet:
"HelloWorld": {
"prefix": "say_hello",
"body": "${1} ${2} -> ${1/Hello/Hallo/} ${2/World/Welt/}"
}
那么我们在两个制表位同时键入 Hello 并跳转的时候,第一个制表位依然保持 Hello 不变,而第二个制表位(占位符)被替换为 Hallo。键入 Welt 亦然。效果图:
4. 代码片语法定义
官网给出了 snippet 的 EBNF 范式的正则文法。
any ::= tabstop | placeholder | choice | variable | text
tabstop ::= '$' int
| '${' int '}'
| '${' int transform '}'
placeholder ::= '${' int ':' any '}'
choice ::= '${' int '|' text (',' text)* '|}'
variable ::= '$' var | '${' var '}'
| '${' var ':' any '}'
| '${' var transform '}'
transform ::= '/' regex '/' (format | text)+ '/' options
format ::= '$' int | '${' int '}'
| '${' int ':' '/upcase' | '/downcase' | '/capitalize' '}'
| '${' int ':+' if '}'
| '${' int ':?' if ':' else '}'
| '${' int ':-' else '}' | '${' int ':' else '}'
regex ::= JavaScript Regular Expression value (ctor-string)
options ::= JavaScript Regular Expression option (ctor-options)
var ::= [_a-zA-Z] [_a-zA-Z0-9]*
int ::= [0-9]+
text ::= .*
注意,作普通字符使用时,$
, }
和 \
可使用 \
(反斜杠)转义,比如评论中有朋友问到,应如何打印 this.$message.success('xx')
,所采用的代码片为 this.\\$message.success('xx')
,其中 string 表达式里的「\」实际被转义为「\」,「$」再被引擎处理为「$」。
5. 代码片设置文件
我们在第二章中就已经理解了代码片设置文件的概念,但当时这并不是我们的核心关注点。现在,你已经对代码片有了深刻的理解了,你可能会面临着这样新的需求,也即只为某工程,某语言,或所有语言单独配置代码片的进阶要求。这里就将为你这样的需求提供解决思路。
5.1 Project level snippets
当你使用 VSCode 打开一个文件夹时,这个文件夹就成了所谓的 Project 或 Workspace。所以,如果你希望单独为某工程配置 snippet 时,你首先应该打开一个目录。在打开目录之后,你只需按照第二章中介绍的方法,在进入代码片设置文件时点选「新建"xxx"文件夹的代码片段文件」。VSCode 会使用 GUI 引导着你在当前工程下的「.vscode」中新建一个「*.code-snippets」的文件,这就是当前工作目录的设置文件。
在呼出代码片的时候,IntelliSense 会注明哪些代码片是「Workspace Snippet」。如:
6. 一些建议
默认情况下 snippet 在 IntelliSense 中的显示优先级并不高,而且在 IntelliSense 中选择相应 snippet 需要按「enter」键,这对于手指短的人来说并不是什么很好的体验。
所幸,VSCode 意识到了这一点,并为我们提供了改进的方式。我们可以在 VSCode 的用户设置(「Ctrl+P」在输入框中写「user settings」后点选)中,检索代码片,然后根据提示修改代码片的相关设置。
我们可以设置在 IntelliSense 中优先显示代码片,并可以通过「TAB」补全。
修改后设置文件中会多出这两行:
"editor.snippetSuggestions": "top",
"editor.tabCompletion": true
注:v1.28 之后,
editor.tabCompletion
得到了增强。现在 TAB 能补全所有前缀了,而非仅 snippet。另外,在插入非代码片的前缀 之后,可以使用 TAB 向下切换别的建议,或使用 Shit + TAB 向上切换。
附录
说好的附录。
另,我对 Atom 的 C snippet3 作了部分修改,使之更适合我的习惯,若有兴致你可自行修改,反正也不难。
{
"#ifndef … #define … #endif":{
"prefix": "def",
"body": "#ifndef ${1:SYMBOL}\n#define $1 ${2:value}\n#endif\t// ${1:SYMBOL}"
},
"#include <>":{
"prefix": "Inc",
"body": "#include <${1:.h}>"
},
"#include \"\"":{
"prefix": "inc",
"body": "#include \"${1:.h}\""
},
"#pragma mark":{
"prefix": "mark",
"body": "#if 0\n${1:#pragma mark -\n}#pragma mark $2\n#endif\n\n$0"
},
"main()":{
"prefix": "main",
"body": "int main(int argc, char const *argv[]) {\n\t$1\n\treturn 0;\n}"
},
"For Loop":{
"prefix": "for",
"body": "for (${1:i} = 0; ${1:i} < ${2:count}; ${1:i}${3:++}) {\n\t$4\n}"
},
"Define and For Loop":{
"prefix": "dfor",
"body": "size_t ${1:i};\nfor (${1:i} = ${2:0}; ${1:i} < ${3:count}; ${1:i}${4:++}) {\n\t$5\n}"
},
"Header Include-Guard":{
"prefix": "once",
"body": "#ifndef ${1:SYMBOL}\n#define $1\n\n${2}\n\n#endif /* end of include guard: $1 */\n"
},
"Typedef":{
"prefix": "td",
"body": "typedef ${1:int} ${2:MyCustomType};"
},
"Typedef Struct":{
"prefix": "tst",
"body": "typedef struct ${1:StructName} {\n\t$2\n}${3:MyCustomType};"
},
"Do While Loop":{
"prefix": "do",
"body": "do {\n\t$0\n} while($1);"
},
"While Loop":{
"prefix": "while",
"body": "while ($1) {\n\t$2\n}"
},
"fprintf":{
"prefix": "fprintf",
"body": "fprintf(${1:stderr}, \"${2:%s}\\\\n\", $3);$4"
},
"If Condition":{
"prefix": "if",
"body": "if ($1) {\n\t$2\n}"
},
"If Else":{
"prefix": "ife",
"body": "if ($1) {\n\t$2\n} else {\n\t$3\n}"
},
"If ElseIf":{
"prefix": "iff",
"body": "if ($1) {\n\t$2\n} else if ($3) {\n\t$4\n}"
},
"If ElseIf Else":{
"prefix": "iffe",
"body": "if ($1) {\n\t$2\n} else if ($3) {\n\t$4\n} else {\n\t$5\n}"
},
"Switch Statement":{
"prefix": "sw",
"body": "switch ($1) {\n$2default:\n\t${3:break;}\n}$0"
},
"case break":{
"prefix": "cs",
"body": "case $1:\n\t$2\n\tbreak;\n$0"
},
"printf":{
"prefix": "printf",
"body": "printf(\"${1:%s }\\n\", $2);$3"
},
"scanf":{
"prefix": "scanf",
"body": "scanf(\"${1:%s}\\n\", $2);$3"
},
"Struct":{
"prefix": "st",
"body": "struct ${1:name_t} {\n\t$2\n};"
},
"void":{
"prefix": "void",
"body": "void ${1:name}($2) {\n\t$3\n}"
},
"any function":{
"prefix": "func",
"body": "${1:int} ${2:name}($3) {\n\t$5\n\treturn ${4:0};\n}"
},
"write file":{
"prefix": "wf",
"body": "FILE *${1:fp};\n${1:fp} = fopen (\"${2:filename.txt}\",\"w\");\nif (${1:fp}!=NULL)\n{\n\tfprintf(${1:fp},\"${3:Some String\\\\n}\");\n\tfclose (${1:fp});\n}"
},
"read file":{
"prefix": "rf",
"body": "FILE *${1:fp};\n${1:fp} = fopen (\"${2:filename.txt}\",\"r\");\nif (${1:fp}!=NULL)\n{\n\tfscanf(${1:fp},\"${3:Some String\\\\n}\", ${3:&var});\n\tfclose (${1:fp});\n}",
"description": "read file opeartion including fopen, fscanf and fclose."
}
}
-
https://code.visualstudio.com/Docs/customization/userdefinedsnippets ↩︎
-
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/RegExp ↩︎
-
https://github.com/atom/language-c/blob/master/snippets/language-c.cson ↩︎
[VS Code]跟我一起在Visual Studio Code 添加自定义snippet(代码段),附详细配置_开发工具_猫科龙-CSDN博客
巧用 CSS 实现酷炫的充电动画 - 掘金
循序渐进,看看只使用 CSS ,可以鼓捣出什么样的充电动画效果。
画个电池
当然,电池充电,首先得用 CSS 画一个电池,这个不难,随便整一个:
欧了,勉强就是它了。有了电池,那接下来直接充电吧。最最简单的动画,那应该是用色彩把整个电池灌满即可。
方法很多,代码也很简单,直接看效果:
有内味了,如果要求不高,这个勉强也就能够交差了。通过蓝色渐变表示电量,通过色块的位移动画实现充电的动画。但是总感觉少了点什么。
增加阴影及颜色的变化
如果要继续优化的话,需要添加点细节。
我们知道,低电量时,电量通常表示为红色,高电量时表示为绿色。再给整个色块添加点阴影的变化,呼吸的感觉,让充电的效果看起来确实是在动。
知识点
到这里,其实只有一个知识点:
- 使用 filter: hue-rotate() 对渐变色彩进行色彩过渡变换动画
我们无法对一个渐变色直接进行 animation ,这里通过滤镜对色相进行调整,从而实现了渐变色的变换动画。
上述例子完整的 Demo: CodePen Demo -- Battery Animation One
添加波浪
ok,刚刚算一个小里程碑,接下来再进一步。电量的顶部为一条直线有点呆呆的感觉,这里我们进行改造一下,如果能将顶部直线,改为波浪滚动,效果会更为逼真一点。
改造之后的效果:
使用 CSS 实现这种波浪滚动效果,其实只是用了一种障眼法,具体的可以我早期写的这篇文章:
知识点
这里的一个知识点就是上述说的使用 CSS 实现简易的波浪效果,通过障眼法实现,看看图就明白了:
上述例子完整的 Demo: CodePen Demo -- Battery Animation Two
OK,到这,上述效果加上数字变化已经算是一个比较不错的效果了。当然上面的效果看上去还是很 CSS 的,就是一眼看到就觉得用 CSS 是可以做到的。
使用强大的 CSS 滤镜实现安卓充电动画效果
那下面这个呢?
用安卓手机的同学肯定不陌生,这个是安卓手机在充电的时候的效果。看到这个我就很好奇,使用 CSS 能做到吗?
经过一番尝试,发现使用 CSS 也是可以很好的模拟这种动画效果:
上述 Gif 录制的效果图是完全使用 CSS 模拟的效果。
上述例子完整的 Demo: HuaWei Battery Charging Animation
知识点
拆解一下知识点,最主要的其实是用到了 filter: contrast()
以及 filter: blur()
这两个滤镜,可以很好的实现这种融合效果。
单独将两个滤镜拿出来,它们的作用分别是:
filter: blur()
: 给图像设置高斯模糊效果。filter: contrast()
: 调整图像的对比度。
但是,当他们“合体”的时候,产生了奇妙的融合现象。
先来看一个简单的例子:
仔细看两圆相交的过程,在边与边接触的时候,会产生一种边界融合的效果,通过对比度滤镜把高斯模糊的模糊边缘给干掉,利用高斯模糊实现融合效果。
当然,这种效果在之前的文章也多次提及过,更具体的,可以看看:
颜色的变换
当然,这里也是可以加上颜色的变换,效果也很不错:
上述例子完整的 Demo: HuaWei Battery Charging Animation
容易忽视的点
通过调节 filter: blur()
及 filter: contrast()
属性的值,动画效果其实会有很大程度的变化,好的效果需要不断的调试。当然,经验在其中也是发挥了很重要的作用,说到底还是要多尝试。
最后
本文给出的几个充电动画,效果渐进增强,本文只指出了最核心的知识点。但是在实际输出的过程中有很多小细节是本文没有提及的,感兴趣的同学还是应该点进 Demo 好好看看源码或者自己动手实现一遍。
更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。
好了,本文到此结束,希望对你有帮助 :)
如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。
可能是最全的 “文本溢出截断省略” 方案合集 - 掘金
本文首发于政采云前端团队博客: 可能是最全的 “文本溢出截断省略” 方案合集
前言
在我们的日常开发工作中,文本溢出截断省略是很常见的一种需考虑的业务场景细节。看上去 “稀松平常” ,但在实现上却有不同的区分,是单行截断还是多行截断?多行的截断判断是基于行数还是基于高度?这些问题之下,都有哪些实现方案?他们之间的差异性和场景适应性又是如何?凡事就怕较真,较真必有成长。本文试图通过编码实践,给出一些答案。
先来点基础的,单行文本溢出省略
核心 CSS 语句
- overflow: hidden;(文字长度超出限定宽度,则隐藏超出的内容)
- white-space: nowrap;(设置文字在一行显示,不能换行)
- text-overflow: ellipsis;(规定当文本溢出时,显示省略符号来代表被修剪的文本)
优点
- 无兼容问题
- 响应式截断
- 文本溢出范围才显示省略号,否则不显示省略号
- 省略号位置显示刚好
短板
- 只支持单行文本截断
适用场景
- 适用于单行文本溢出显示省略号的情况
Demo
<style>
.demo {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>
<body>
<div class="demo">这是一段很长的文本</div>
</body>
复制代码
示例图片
进阶一下,多行文本溢出省略(按行数)
○ 纯 CSS 实现方案
核心 CSS 语句
- -webkit-line-clamp: 2;(用来限制在一个块元素显示的文本的行数, 2 表示最多显示 2 行。 为了实现该效果,它需要组合其他的WebKit属性)
- display: -webkit-box;(和 1 结合使用,将对象作为弹性伸缩盒子模型显示 )
- -webkit-box-orient: vertical;(和 1 结合使用 ,设置或检索伸缩盒对象的子元素的排列方式 )
- overflow: hidden;(文本溢出限定的宽度就隐藏内容)
- text-overflow: ellipsis;(多行文本的情况下,用省略号“…”隐藏溢出范围的文本)
优点
- 响应式截断
- 文本溢出范围才显示省略号,否则不显示省略号
- 省略号显示位置刚好
短板
- 兼容性一般: -webkit-line-clamp 属性只有 WebKit 内核的浏览器才支持
适用场景
- 多适用于移动端页面,因为移动设备浏览器更多是基于 WebKit 内核
Demo
<style>
.demo {
display: -webkit-box;
overflow: hidden;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
</style>
<body>
<div class='demo'>这是一段很长的文本</div>
</body>
复制代码
示例图片
○ 基于 JavaScript 的实现方案
优点
- 无兼容问题
- 响应式截断
- 文本溢出范围才显示省略号,否则不显示省略号
短板
- 需要 JS 实现,背离展示和行为相分离原则
- 文本为中英文混合时,省略号显示位置略有偏差
适用场景
- 适用于响应式截断,多行文本溢出省略的情况
Demo
当前仅适用于文本为中文,若文本中有英文,可自行修改
<script type="text/javascript">
const text = '这是一段很长的文本';
const totalTextLen = text.length;
const formatStr = () => {
const ele = document.getElementsByClassName('demo')[0];
const lineNum = 2;
const baseWidth = window.getComputedStyle(ele).width;
const baseFontSize = window.getComputedStyle(ele).fontSize;
const lineWidth = +baseWidth.slice(0, -2);
// 所计算的strNum为元素内部一行可容纳的字数(不区分中英文)
const strNum = Math.floor(lineWidth / +baseFontSize.slice(0, -2));
let content = '';
// 多行可容纳总字数
const totalStrNum = Math.floor(strNum * lineNum);
const lastIndex = totalStrNum - totalTextLen;
if (totalTextLen > totalStrNum) {
content = text.slice(0, lastIndex - 3).concat('...');
} else {
content = text;
}
ele.innerHTML = content;
}
formatStr();
window.onresize = () => {
formatStr();
};
</script>
<body>
<div class='demo'></div>
</body>
复制代码
示例图片
再进阶一步,多行文本溢出省略(按高度)
○ 多行文本溢出不显示省略号
核心 CSS 语句
- overflow: hidden;(文本溢出限定的宽度就隐藏内容)
- line-height: 20px;(结合元素高度,高度固定的情况下,设定行高, 控制显示行数)
- max-height: 40px;(设定当前元素最大高度)
优点
- 无兼容问题
- 响应式截断
短板
- 单纯截断文字, 不展示省略号,观感上较为生硬
适用场景
- 适用于文本溢出不需要显示省略号的情况
Demo
<style>
.demo {
overflow: hidden;
max-height: 40px;
line-height: 20px;
}
</style>
<body>
<div class='demo'>这是一段很长的文本</div>
</body>
复制代码
示例图片
○ 伪元素 + 定位实现多行省略
核心 CSS 语句
-
position: relative; (为伪元素绝对定位)
-
overflow: hidden; (文本溢出限定的宽度就隐藏内容)
-
position: absolute;(给省略号绝对定位)
-
line-height: 20px; (结合元素高度,高度固定的情况下,设定行高, 控制显示行数)
-
height: 40px; (设定当前元素高度)
-
::after {} (设置省略号样式)
优点
-
无兼容问题
-
响应式截断
短板
-
无法识别文字的长短,无论文本是否溢出范围, 一直显示省略号
-
省略号显示可能不会刚刚好,有时会遮住一半文字
适用场景
- 适用于对省略效果要求较低,文本一定会溢出元素的情况
Demo
<style>
.demo {
position: relative;
line-height: 20px;
height: 40px;
overflow: hidden;
}
.demo::after {
content: "...";
position: absolute;
bottom: 0;
right: 0;
padding: 0 20px 0 10px;
}
</style>
<body>
<div class='demo'>这是一段很长的文本</div>
</body>
复制代码
示例图片
○ 利用 Float 特性,纯 CSS 实现多行省略
核心 CSS 语句
-
line-height: 20px;(结合元素高度,高度固定的情况下,设定行高, 控制显示行数)
-
overflow: hidden;(文本溢出限定的宽度就隐藏内容)
-
float: right/left;(利用元素浮动的特性实现)
-
position: relative;(根据自身位置移动省略号位置, 实现文本溢出显示省略号效果)
-
word-break: break-all;(使一个单词能够在换行时进行拆分)
优点
-
无兼容问题
-
响应式截断
-
文本溢出范围才显示省略号,否则不显示省略号
短板
- 省略号显示可能不会刚刚好,有时会遮住一半文字
适用场景
- 适用于对省略效果要求较低,多行文本响应式截断的情况
Demo
<style>
.demo {
background: #099;
max-height: 40px;
line-height: 20px;
overflow: hidden;
}
.demo::before{
float: left;
content:'';
width: 20px;
height: 40px;
}
.demo .text {
float: right;
width: 100%;
margin-left: -20px;
word-break: break-all;
}
.demo::after{
float:right;
content:'...';
width: 20px;
height: 20px;
position: relative;
left:100%;
transform: translate(-100%,-100%);
}
</style>
<body>
<div class='demo'>
<div class="text">这是一段很长的文本</div>
</div>
</body>
复制代码
示例图片
原理讲解
有 A、B、C 三个盒子,A 左浮动,B、C 右浮动。设置 A 盒子的高度与 B 盒子高度(或最大高度)要保持一致
-
当的 B 盒子高度低于 A 盒子,C 盒子仍会处于 B 盒子右下方。
-
如果 B 盒子文本过多,高度超过了 A 盒子,则 C 盒子不会停留在右下方,而是掉到了 A 盒子下。
-
接下来对 C 盒子进行相对定位,将 C 盒子位置向右侧移动 100%,并向左上方向拉回一个 C 盒子的宽高(不然会看不到哟)。这样在文本未溢出时不会看到 C 盒子,在文本溢出时,显示 C 盒子。
收,大道归简,能力封装
凡重复的,让它单一;凡复杂的,让它简单。
每次都要搞一坨代码,太麻烦。这时候你需要考虑将文本截断的能力,封装成一个可随时调用的自定义容器组件。市面上很多 UI 组件库,都提供了同类组件的封装,如基于 Vue 的 ViewUI Pro,或面向小程序提供组件化解决能力的 MinUI 。
结语
本文介绍了几种目前常见的文本截断省略的方案,各有利弊,各位同学可根据实际开发情况及需求选择方案。如果你还知道更好其他实现方案,欢迎在评论区留下宝贵评论。
参考文章
招贤纳士
政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 50 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。
如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“ 5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 [email protected]
推荐阅读
在Mac上使用多输入法?尝试用AppleScript+自动操作实现快捷切换到指定输入法。 - 知乎
https://zhuanlan.zhihu.com/p/404763045
如果您是 Windows 用户,应该会注意到在 Windows 系统中可以设置按快捷键切换到指定输入法。然而在 Mac 上,系统并未自带这个功能。因此会给使用多国语言输入的用户带来麻烦。
例如笔者的一位朋友正在学习法语,他习惯于用电脑制作笔记,有时也要和法语角的语伴们在聊天软件上互动和解惑。在这种多语言混合输入的情况下,Mac 自带的两个输入法快捷键就显得有些力不从心了。
笔者在迁移到 Mac 系统之后也遇到了这样的问题。在互联网上寻求到的解决方案几乎都是付费选项。(例如 Alfred、KM、快速切换输入法)
于是笔者开始尝试使用 Apple 自带的脚本和自动操作功能解决这个问题。
1、下载脚本 Inputsource
几经检索,笔者找到了一个可以获取当前输入法信息的脚本Inputsource。解压后记录 inputsource 文件的位置。例如笔者将其放置在 / usr/local/lib 中,在终端中输入以下命令:
/usr/local/lib/Inputsource
就会返回你当前输入法的名称。例如:
"com.apple.inputmethod.Kotoeri.RomajiTyping.Japanese"(*日语输入法*)
"com.apple.keylayout.ABC"(*英语输入法*)
"com.apple.inputmethod.SCIM.ITABC"(*简体拼音*)
利用这个脚本,我们就可以获取当前系统输入法
2、简单的 AppleScript 语句
在 MacOS 中自带有脚本编辑器。在聚焦中输入脚本编辑器,就能打开应用,界面如下:
在闪烁光标处输入 AppleScript 语句,点击右上角的 键编译,播放键运行。
如本文第一图所示,mac 自带的输入法切换快捷键 control+option+space 可以切换至输入法库的下一个输入法,那么我们只要不断调用此快捷键,再调用 Inputsource 返回当前输入法,直至输入法为我们想要的输入法就 ok 了。
经查询,空格键在苹果 keycode 表中的值为 49。由此写如下代码:
set nowInput to (do shell script "/usr/local/lib/Inputsource")
repeat until nowInput = "com.apple.inputmethod.Kotoeri.RomajiTyping.Japanese"
tell application "System Events"
key code 49 using {option down, control down}
end tell
set nowInput to (do shell script "/usr/local/lib/Inputsource")
end repeat
这是将现在的输入法切换为 Apple 自带的日语输入法的代码。
3、加入快速操作
至此,我们已经写好了需要的代码,接下来我们需要把它加入到 Apple 的 “自动操作” 中以设置快捷键调用。在聚焦中搜索“自动操作”,选择左下角的新建文稿,然后选取文稿类型为快速操作(这样才能在服务菜单中增加快捷键)
然后在左上角搜索运行 AppleScript,并将其拖拽到工作区 (图片右下角提示处)。
在框中删除 “Your Script goes here” 这一行,将你编辑好的代码加入到这里(记得在源代码的基础上每行都要增加一个缩进),然后点击编译。
结果如图所示:
保存此快速操作,取一个你想要的名字(例如我这里取名为日语输入法)然后点击显示屏左上角的苹果标志,打开系统偏好设置,选择键盘,
选择快捷键—服务,找到通用分类,下面就会有你编辑好的快速操作。勾选你想要使用的快速操作,然后双击对应操作,就可以按键设置快捷键。(记得设置快捷键的时候要防范快捷键冲突。)
4、处理权限问题
由于笔者能力所限,并不清楚如何为所有应用添加调用 System Events 发送按键的权限,因此第一次在某个应用中使用快捷键时,总会弹出无法发送按键的错误。此时,打开系统偏好设置,点击安全性与隐私,点击辅助功能选项,点击小加号,然后把你想要使用的应用赋予权限。(注意:这是笔者个人的解决办法,赋予未知应用或被恶意程序注入的应用可能会导致系统中毒,请谨慎赋予权限。)
5、加快运行速度
在笔者多次尝试后,发现此方法切换输入法不够快,有时候会导致输入有片刻延迟。猜测是反复调用 Inputsource 脚本消耗时间过长。由于笔者个人的输入法库按照如下顺序排列:
简体拼音——日语罗马字——ABC
于是尝试只调用一次 Inputsource,然后根据当前的输入法选择键入次数来切换输入法,切换输入法速度有明显提升。具体代码示例如下(以切换到日语输入法为例):
set nowInput to (do shell script "/usr/local/lib/Inputsource")
if nowInput = "com.apple.keylayout.ABC" then
tell application "System Events"
key code 49 using {option down, control down}
key code 49 using {option down, control down}
end tell
else if nowInput = "com.apple.inputmethod.SCIM.ITABC" then
tell application "System Events"
key code 49 using {option down, control down}
end tell
end if
如此修改以后,可以实现较快的输入法切换。但并不能达到如同系统自带的中 / 英键一样的切换速度,启动自动操作仍然有一个较小的延迟。这也是这种处理方法的一个问题。
https://zhuanlan.zhihu.com/p/404763045
2021年不可错过的34种JS优化技巧
作者丨 Atit
译者丨王者
转载丨前端之巅
开发者总是在学习新东西,而跟上这些技术的变化不应该比之前更难。我写这篇文章的目的是介绍 JavaScript 的一些最佳实践,作为前端开发人员,掌握了这些最佳实践会让我们在 2021 年的工作变得更轻松。
你可能做了很长时间的 JavaScript 开发,但有时候你可能没有使用最新的 JavaScript 特性或技巧,这些特性和技巧可以在不需要编写额外代码的情况下解决你的问题。它们可以帮助你写出干净且优化的 JavaScript 代码。此外,如果你在 2021 年准备去参加面试,可以参考本文的内容。
- 带有多个条件的 if 语句
把多个值放在一个数组中,然后调用数组的 includes 方法。
//longhand
if (x === 'abc' || x === 'def' || x === 'ghi' || x ==='jkl') {
//logic
}
//shorthand
if (['abc', 'def', 'ghi', 'jkl'].includes(x)) {
//logic
}
- 简化 if true...else
对于不包含大逻辑的 if-else 条件,可以使用下面的快捷写法。我们可以简单地使用三元运算符来实现这种简化。
// Longhand
let test: boolean;
if (x > 100) {
test = true;
} else {
test = false;
}
// Shorthand
let test = (x > 10) ? true : false;
//or we can use directly
let test = x > 10;
console.log(test);
如果有嵌套的条件,可以这么做。
let x = 300,
test2 = (x > 100) ? 'greater 100' : (x < 50) ? 'less 50' : 'between 50 and 100';
console.log(test2); // "greater than 100"
- 声明变量
当我们想要声明两个具有相同的值或相同类型的变量时,可以使用这种简写。
//Longhand
let test1;
let test2 = 1;
//Shorthand
let test1, test2 = 1;
- null、undefined 和空值检查
当我们创建了新变量,有时候想要检查引用的变量是不是为非 null 或 undefined。JavaScript 确实有一个很好的快捷方式来实现这种检查。
// Longhand
if (test1 !== null || test1 !== undefined || test1 !== '') {
let test2 = test1;
}
// Shorthand
let test2 = test1 || '';
-
null 检查和默认赋值
let test1 = null,
test2 = test1 || '';
console.log("null check", test2); // output will be "" -
undefined 检查和默认赋值
let test1 = undefined,
test2 = test1 || '';
console.log("undefined check", test2); // output will be ""
一般值检查
let test1 = 'test',
test2 = test1 || '';
console.log(test2); // output: 'test'
另外,对于上述的 4、5、6 点,都可以使用?? 操作符。
如果左边值为 null 或 undefined,就返回右边的值。默认情况下,它将返回左边的值。
const test= null ?? 'default';
console.log(test);
// expected output: "default"
const test1 = 0 ?? 2;
console.log(test1);
// expected output: 0
- 给多个变量赋值
当我们想给多个不同的变量赋值时,这种技巧非常有用。
//Longhand
let test1, test2, test3;
test1 = 1;
test2 = 2;
test3 = 3;
//Shorthand
let [test1, test2, test3] = [1, 2, 3];
- 简便的赋值操作符
在编程过程中,我们要处理大量的算术运算符。这是 JavaScript 变量赋值操作符的有用技巧之一。
// Longhand
test1 = test1 + 1;
test2 = test2 - 1;
test3 = test3 * 20;
// Shorthand
test1++;
test2--;
test3 *= 20;
- if 判断值是否存在
这是我们都在使用的一种常用的简便技巧,在这里仍然值得再提一下。
// Longhand
if (test1 === true) or if (test1 !== "") or if (test1 !== null)
// Shorthand //it will check empty string,null and undefined too
if (test1)
注意:如果 test1 有值,将执行 if 之后的逻辑,这个操作符主要用于 null 或 undefinded 检查。
- 用于多个条件判断的 && 操作符
如果只在变量为 true 时才调用函数,可以使用 && 操作符。
//Longhand
if (test1) {
callMethod();
}
//Shorthand
test1 && callMethod();
- for each 循环
这是一种常见的循环简化技巧。
// Longhand
for (var i = 0; i < testData.length; i++)
// Shorthand
for (let i in testData) or for (let i of testData)
遍历数组的每一个变量。
function testData(element, index, array) {
console.log('test[' + index + '] = ' + element);
}
[11, 24, 32].forEach(testData);
// logs: test[0] = 11, test[1] = 24, test[2] = 32
- 比较后返回
我们也可以在 return 语句中使用比较,它可以将 5 行代码减少到 1 行。
// Longhand
let test;
function checkReturn() {
if (!(test === undefined)) {
return test;
} else {
return callMe('test');
}
}
var data = checkReturn();
console.log(data); //output test
function callMe(val) {
console.log(val);
}
// Shorthand
function checkReturn() {
return test || callMe('test');
}
- 箭头函数
//Longhand
function add(a, b) {
return a + b;
}
//Shorthand
const add = (a, b) => a + b;
更多例子:
function callMe(name) {
console.log('Hello', name);
}
callMe = name => console.log('Hello', name);
- 简短的函数调用
我们可以使用三元操作符来实现多个函数调用。
// Longhand
function test1() {
console.log('test1');
};
function test2() {
console.log('test2');
};
var test3 = 1;
if (test3 == 1) {
test1();
} else {
test2();
}
// Shorthand
(test3 === 1? test1:test2)();
- switch 简化
我们可以将条件保存在键值对象中,并根据条件来调用它们。
// Longhand
switch (data) {
case 1:
test1();
break;
case 2:
test2();
break;
case 3:
test();
break;
// And so on...
}
// Shorthand
var data = {
1: test1,
2: test2,
3: test
};
data[something] && data[something]();
- 隐式返回
通过使用箭头函数,我们可以直接返回值,不需要 return 语句。
//longhand
function calculate(diameter) {
return Math.PI * diameter
}
//shorthand
calculate = diameter => (
Math.PI * diameter;
)
- 指数表示法
// Longhand
for (var i = 0; i < 10000; i++) { ... }
// Shorthand
for (var i = 0; i < 1e4; i++) {
- 默认参数值
//Longhand
function add(test1, test2) {
if (test1 === undefined)
test1 = 1;
if (test2 === undefined)
test2 = 2;
return test1 + test2;
}
//shorthand
add = (test1 = 1, test2 = 2) => (test1 + test2);
add() //output: 3
- 延展操作符简化
//longhand
// joining arrays using concat
const data = [1, 2, 3];
const test = [4 ,5 , 6].concat(data);
//shorthand
// joining arrays
const data = [1, 2, 3];
const test = [4 ,5 , 6, ...data];
console.log(test); // [ 4, 5, 6, 1, 2, 3]
我们也可以使用延展操作符进行克隆。
//longhand
// cloning arrays
const test1 = [1, 2, 3];
const test2 = test1.slice()
//shorthand
// cloning arrays
const test1 = [1, 2, 3];
const test2 = [...test1];
- 模板字面量
如果你厌倦了使用 + 将多个变量连接成一个字符串,那么这个简化技巧将让你不再头痛。
//longhand
const welcome = 'Hi ' + test1 + ' ' + test2 + '.'
//shorthand
const welcome = `Hi ${test1} ${test2}`;
- 跨行字符串
当我们在代码中处理跨行字符串时,可以这样做。
//longhand
const data = 'abc abc abc abc abc abc\n\t'
+ 'test test,test test test test\n\t'
//shorthand
const data = `abc abc abc abc abc abc
test test,test test test test`
- 对象属性赋值
let test1 = 'a';
let test2 = 'b';
//Longhand
let obj = {test1: test1, test2: test2};
//Shorthand
let obj = {test1, test2};
- 将字符串转成数字
//Longhand
let test1 = parseInt('123');
let test2 = parseFloat('12.3');
//Shorthand
let test1 = +'123';
let test2 = +'12.3';
- 解构赋值
//longhand
const test1 = this.data.test1;
const test2 = this.data.test2;
const test2 = this.data.test3;
//shorthand
const { test1, test2, test3 } = this.data;
- 数组 find 简化
当我们有一个对象数组,并想根据对象属性找到特定对象,find 方法会非常有用。
const data = [{
type: 'test1',
name: 'abc'
},
{
type: 'test2',
name: 'cde'
},
{
type: 'test1',
name: 'fgh'
},
]
function findtest1(name) {
for (let i = 0; i < data.length; ++i) {
if (data[i].type === 'test1' && data[i].name === name) {
return data[i];
}
}
}
//Shorthand
filteredData = data.find(data => data.type === 'test1' && data.name === 'fgh');
console.log(filteredData); // { type: 'test1', name: 'fgh' }
- 条件查找简化
如果我们要基于不同的类型调用不同的方法,可以使用多个 else if 语句或 switch,但有没有比这更好的简化技巧呢?
// Longhand
if (type === 'test1') {
test1();
}
else if (type === 'test2') {
test2();
}
else if (type === 'test3') {
test3();
}
else if (type === 'test4') {
test4();
} else {
throw new Error('Invalid value ' + type);
}
// Shorthand
var types = {
test1: test1,
test2: test2,
test3: test3,
test4: test4
};
var func = types[type];
(!func) && throw new Error('Invalid value ' + type); func();
- indexOf 的按位操作简化
在查找数组的某个值时,我们可以使用 indexOf() 方法。但有一种更好的方法,让我们来看一下这个例子。
//longhand
if(arr.indexOf(item) > -1) { // item found
}
if(arr.indexOf(item) === -1) { // item not found
}
//shorthand
if(~arr.indexOf(item)) { // item found
}
if(!~arr.indexOf(item)) { // item not found
}
按位 () 运算符将返回 true(-1 除外),反向操作只需要!。另外,也可以使用 include() 函数。
if (arr.includes(item)) {
// true if the item found
}
- Object.entries()
这个方法可以将对象转换为对象数组。
const data = { test1: 'abc', test2: 'cde', test3: 'efg' };
const arr = Object.entries(data);
console.log(arr);
/** Output:
[ [ 'test1', 'abc' ],
[ 'test2', 'cde' ],
[ 'test3', 'efg' ]
]
**/
- Object.values()
这也是 ES8 中引入的一个新特性,它的功能类似于 Object.entries(),只是没有键。
const data = { test1: 'abc', test2: 'cde' };
const arr = Object.values(data);
console.log(arr);
/** Output:
[ 'abc', 'cde']
**/
- 双重按位操作
// Longhand
Math.floor(1.9) === 1 // true
// Shorthand
~~1.9 === 1 // true
- 重复字符串多次
为了重复操作相同的字符,我们可以使用 for 循环,但其实还有一种简便的方法。
//longhand
let test = '';
for(let i = 0; i < 5; i ++) {
test += 'test ';
}
console.log(str); // test test test test test
//shorthand
'test '.repeat(5);
- 查找数组的最大值和最小值
const arr = [1, 2, 3];
Math.max(…arr); // 3
Math.min(…arr); // 1
- 获取字符串的字符
let str = 'abc';
//Longhand
str.charAt(2); // c
//Shorthand
str[2]; // c
- 指数幂简化
//longhand
Math.pow(2,3); // 8
//shorthand
2**3 // 8
原文链接:
推荐阅读:
* [大文件上传如何做断点续传](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247499152&idx=1&sn=daed0c35ae7d63dd342b16b31b89cda2&chksm=97c6603ea0b1e9285f7ded75e0cadf6436281e1f06dfec6c0414f8b5b3cecfcb3f202ba6c629&scene=21#wechat_redirect)
* [微信:5 月 20 日后不再提供小程序打开 App 服务](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247498526&idx=1&sn=debbfcaa6ec256ae7d64429900cac0f0&chksm=97c666b0a0b1efa682e74936bda666370575886ef37f0ca731f86e18398435181492052f5e0d&scene=21#wechat_redirect)
* [一文搞懂单点登录三种情况的实现方式](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247498639&idx=1&sn=1386ea609c31345e380f21f12cb4dc05&chksm=97c66621a0b1ef3777f00e35035e4b097404f64bc39b17fbb225b2fee9ec3eab7be2d4e5863c&scene=21#wechat_redirect)
* [终于有人把 Nginx 说清楚了,图文详解!](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247498339&idx=1&sn=6c42ff6f859d3e02c360e56b585d9816&chksm=97c667cda0b1eedbb665a83f66f8c264a257763b77a03e00c87d0d1d8373c0c1ef3b65c3b193&scene=21#wechat_redirect)
* [推荐 130 个令你眼前一亮的网站,总有一个用得着](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247498262&idx=1&sn=886534663bb8209d4d9ad32232eb3d5d&chksm=97c667b8a0b1eeaea90f83e91505d40efd4c606aca7e1f8061697f569bc91f8dba3497bc7a85&scene=21#wechat_redirect)
* [深入浅出 33 道 Vue 99% 出镜率的面试题](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247498262&idx=2&sn=bc792931ee337d3e7cb73bdb8fa4599e&chksm=97c667b8a0b1eeae0915411943ab1d50ca0f4847882d56020d23b4b9308021af8326e1ffb631&scene=21#wechat_redirect)
**VUE中文社区**
编程技巧 **·** 行业秘闻 **·** 技术动向
https://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247499235&idx=3&sn=1d347a1824d8185f3fbec28d1fddbc07&chksm=97c6604da0b1e95b7cba5a4056ba19ebd1bba30f7ef8ef38029291c48f990232fd9f04211ea0&mpshare=1&scene=24&srcid=0531LsfgfLQPs0b7Q5Q9gEtk&sharer_sharetime=1622425554185&sharer_shareid=bc6a9591e78ea361aa62f4212113d179#rd
如何防止他人恶意调试你的web程序
1 前言
看到社区很多都在讨论如何调试, 如何高级的调试, 以及一些调试的奇技淫巧, 今天我想和大家聊聊,怎么禁止调试, 禁止他人调试我们的程序
为什么会有这篇文章呢, 源自一次我寻找盗版电影的遭遇, 一次好奇心的驱使下, 由于很多这种平台都是只做搬运, 不做存储, 因为存储盗版电影向他人提供是违法的, 特别是那种刚出的新电影! 当时好奇想通过看某站的控制台, 想了解一下他们是怎么是通过啥接口, 怎么请求, 请求来的格式啥样的, 抱着这样的好奇心, 开始了我的奇妙之旅...
看完本篇文章你将学会
我无法断定你能学到什么, 但是以下是我希望你能从本篇文章中学到的:
- 如何简单的防止你的程序被他人恶意调试
- 逆向思维学会如何更好的调试
2 具体实现
防止调试的方法, 这里我们主要是通过不断debugger
的方法来疯狂输出断点, 让控制台打开后程序就无法正常执行
我们都知道debugger
只有在控制台被打开的时候才会执行, 所以后面的所有方法都是围绕着这一特性来进行, 废话不多说, 我将通过以下几个案例向你们展示道高一尺魔高一丈的道理, 先上代码:
方法一:
(() => { function block() { setInterval(() => { debugger; }, 50); } try { block(); } catch (err) {} })();
通过上方的代码我们可以看到, 在页面中打开控制台后, 会有以下结果:
需要在这里说明以下几点:
- 程序被
debugger
阻泄了, 我们无法像以往一样在 Source Tab 中的对应 js 代码处添加断点调试, 无法调试程序的执行逻辑. 在程序异常复杂且被混淆后的代码是异常难读的! 通常我们会在 source 的左边加上breakpoint
来让程序每次走到加点的地方停下来, 以便让我们查看一些变量的值或是步骤的流程逻辑 (如下图所示)
- 我们都知道, 第一次打开控制台是看不到 network tab 中的任何请求的, 所以我们想通过 network tab 来查看网页都做了哪些请求, 也是看不到的, 当我们打开控制台就会出
debugger
阻挡我们, 我们可以通过下面的解决方法来处理, 或者是用抓包工具来查看具体的请求
大家可以先不看解决方法, 想想如果是你, 这个时候怎么突破这个屏障呢? 第一次遇到这种情况我也是很懵, 不知道咋处理, 后面发现问题简直不要太简单, 我们可以带着疑问来看:
对于第一个示例, 我们如何解决?(绕过它)
答案是: 禁止断点
可以看到很简单, 在 Chrome 控制台的 Source Tab 页点击 Deactivate breakpoints 按钮或者按下 Ctrl + f8(如下图所示)。但是对于控制台不熟悉的小伙伴, 很难会想到这里去.
但是, 难道这篇文章就这样结束了? 那我可顶不住小伙伴们的 "就这?" 😂
其实, 上面的解决方法并没有帮我们解决根本问题,我们需要做的是调试, 上面虽然把debugger
都去掉了, 但是我们也无法在通过点击每一行代码左边的行号添加 breakpoint
了, 所以根本性的问题, 并没有解决, 只是去除了那碍眼的疯狂 debugger, 我们还是得另辟蹊径
方法二:
对对应的代码行, 通过添加logpoint
为 false,然后按回车后刷新网页,发现成功跳过无限 debugger, 于是我们就可以愉快的自由调试了~
对应的还有一种方法
即通过add script ignore list
来添加需要忽略执行代码行或文件
可以看到, 我们也可以通过删除 script ignore list 里已添加的忽略代码, 恢复初始状态
但是, 你这么聪明, 那人家不得想想对策?
对于上面的第一个方法 🎈
将setInterval(() => {debugger;}, 50);
写在一行中, 你即使通过添加logpoint
为 false, 也没用, 仍然是疯狂 debugger, 即使你可能想到, 通过左下角的代码格式化, 来格式一下setInterval(() => {debugger;}, 50);
将它变成多行的, 也是没用的, 仍然会在刷新后重新弹 debugger
(() => { function block() { setInterval(() => {debugger;}, 50); } try { block(); } catch (err) {} })();
对于第二个方法, 我们对代码进行如下改造😬
(() => { function block() { setInterval(() => { Function("debugger")(); }, 50); } try { block(); } catch (err) {} })();
我们可以通过将debugger
改写成Function("debugger")();
的形式, 来应对; Function 构造器生成的 debugger 会在每一次执行时开启一个临时 js 文件, 哈哈~ 对方表示好无奈 😅
于是会有以下结果
这无限套娃, 真够狠的, 我们要坚信正义最后总会胜利, 不能给想非法调试我们程序的人机会, 所以我们要把各种情况都考虑周全,可以说这种方法是最恨的, 但是这还不算完~ (好家伙~ 😀 想非法调试我程序, 那你就得战胜我)
强化以上方法
上面的代码由于没有加密混淆, 多少可能还是会被别人读一些, 那么我们加密混淆看看是啥样的
好家伙, 你这咋读?
eval(function(c,g,a,b,d,e){d=String;if(!"".replace(/^/,String)){for(;a--;)e[a]=b[a]||a;b=[function(f){return e[f]}];d=function(){return"\\w+"};a=1}for(;a--;)b[a]&&(c=c.replace(new RegExp("\\b"+d(a)+"\\b","g"),b[a]));return c}('(()=>{1 0(){2(()=>{3("4")()},5)}6{0()}7(8){}})();',9,9,"block function setInterval Function debugger 50 try catch err".split(" "),0,{}));
格式化后的样子
我们继续对代码进行改造, 让对方尽量的难以识别我们的代码
将Function("debugger").call()
改成(function(){return false;})["constructor"]("debugger")["call"]();
并且, 添加条件, 当窗口外部宽高, 和内部宽高的差值大于一定的值, 我把 body 里的内容全部清空掉, 看你还能不能操作我的按钮啊啥的~ 哈哈哈 😀
需要特别说明的是: 像 toG 的项目或者是一些为了保护自己的版权又或者是一些比较敏感的项目, 出于安全的考虑在部署到生产环境后最好是不让别人调试的, 当然, 前端所做的也就那么一些, 需要前后端一起配合, 便可以很好的对项目或者数据进行私密的保护 🧡
最后: 附上这份未混淆的来之不易的的代码 (记得混淆后使用哦~) 😢 一定要记得点赞加关注~ 原创太不容易了.
你可以把它当作你的工具函数, 在需要不让别人轻易调试的项目中引用
(() => { function block() { if ( window.outerHeight - window.innerHeight > 200 || window.outerWidth - window.innerWidth > 200 ) { document.body.innerHTML = "检测到非法调试, 请关闭后刷新重试!"; } setInterval(() => { (function () { return false; } ["constructor"]("debugger") ["call"]()); }, 50); } try { block(); } catch (err) {} })();
3 推荐一个调试页面的小技巧
说了那么多的防止被人调试, 那么最后也说一个本人觉得眼前一亮的调试样式的方法
通过给style
标签添加style="display: block"
,contenteditable
两个属性实现在页面中便捷的调试样式
复制下方代码到你的 html 文件中, 玩一下~
`<!DOCTYPE html>
4 最后
我所知道的禁止调试的方法就只有如上所述, 但是肯定还有很多好玩的, 小伙伴们可以在评论区留言, 一起共同学习~
推荐阅读:
* [调试技巧:如何快速知道页面上所有元素的轮廓跟位置!](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247501249&idx=1&sn=15cb8bb060d25aa03aab9052060a2699&chksm=97c6586fa0b1d179cda1f1bf638bb6b8a49a1cbcbada369674430424da040497c3eb43bf0c4f&scene=21#wechat_redirect)
* [25个 Vue 技巧,开发了5年了,才知道还能这么用](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247500344&idx=1&sn=ff35e3962b7c421b20cc8e2ff115cc90&chksm=97c65f96a0b1d680c05917f97383d2fed903a885e00e160e8743c56910dd265d4b1fe6aeef81&scene=21#wechat_redirect)
* [史上最全 Vue 前端代码风格指南](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247500706&idx=1&sn=d6bb0422516b9fd721b035defc02adb7&chksm=97c65e0ca0b1d71aad68e458d78757e997db6cf37204c54853dacd043c4de84140f4225bcdc1&scene=21#wechat_redirect)
* [2021, 九款值得推荐的VUE3 UI框架](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247500786&idx=1&sn=085a6af1c950f47269c07c92f723a6e6&chksm=97c65e5ca0b1d74afdd3c12872d8e0285eab23f516919bfb4c31988cd6d87ea8a0bd984b481b&scene=21#wechat_redirect)
* [推荐 130 个令你眼前一亮的网站,总有一个用得着](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247498262&idx=1&sn=886534663bb8209d4d9ad32232eb3d5d&chksm=97c667b8a0b1eeaea90f83e91505d40efd4c606aca7e1f8061697f569bc91f8dba3497bc7a85&scene=21#wechat_redirect)
* [深入浅出 33 道 Vue 99% 出镜率的面试题](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247498262&idx=2&sn=bc792931ee337d3e7cb73bdb8fa4599e&chksm=97c667b8a0b1eeae0915411943ab1d50ca0f4847882d56020d23b4b9308021af8326e1ffb631&scene=21#wechat_redirect)
**VUE中文社区**
编程技巧 **·** 行业秘闻 **·** 技术动向
Excel如何跨工作表动态引用数据(合并汇总必备)
http://www.360doc.com/content/19/0824/12/30583536_856778219.shtml
在进行一些合并、汇总工作中,经常碰到的一个问题是有一堆格式类似的不同工作表,希望能有一张汇总表显示其中的一些数据,又不想一个个手动 link。而想用公式拉时又会发现工作表名无法作为变量随之移动。
这里介绍一个常用函数: indirect(以下都不解释函数原理,反正写了也没人看 ^ ^)
*但这种方法 Link 的话无法通过追溯公式直接定位到对应单元格,推荐使用建立工作表的超链接替代
内容分为三块
1、Indirect 的简单使用
2、Indirect 配合 match/Lookup 使用
3、其他配套事项
1、Indirect 的简单使用
简单来说,当使用 indirect 跨工作表引用时,规律如下:=indirect(“工作表名!单元格区域”)
*indirect 也可以跨工作簿,但一定要工作簿维持打开才可以保持引用有效,没啥用就不介绍了。跨工作簿大量 & 维持链接的引用至今没有很好的解决方法。
观察规律,发现括号里头尾各有一个双引号,如果这时非动态引用一个名为 C. 现金的工作表的 B2 单元格,公式如下:\=INDIRECT(''C. 现金'!B2') 即工作表名部分变为单引号 工作表名 单引号
则动态引用时工作表名部分如下:双引号 & 单元格位置 & 双引号,即如图所示:
这时就达成了最简单的跨工作表动态引用的效果。(注意 Indirect 引用工作表名里不能有空格)
2、Indirect 配合 match/Lookup 使用
不过这是在知道我们要引用 B2 单元格的情况下,那如果这个单元格的位置在每个工作表里都不确定呢?比如我们需要引用每张工作表的合计数。该合计数在不同表中列数相同但行数不同。
这时有两种方法,match 函数与 lookup 函数。这两个函数都是用来查找的,前者返回查找的单元格的位置,后者返回单元格的内容。
比如在两个工作表里有一行结果 & 数字,分别为第二行和第五行,对应值分别为 5 和 10.
使用 Match Indirect 引用则返回对应行号:=MATCH($A$1,INDIRECT(A3&'!A:A'),0)
然后使用 index indirect 引用行号以及工作表名与相应的列则返回结果对应的数据:\=INDEX(INDIRECT('''&$A3&''!B:B'),$C3)
注意:Match 函数的最后一个变量写 0 就会查找第一个匹配的值。此时数列可以以任意顺序排列。
如果使用 Lookup 函数则公式如下:\=LOOKUP(1,0/(INDIRECT(A3&'!A:A')=$A$1),INDIRECT(A3&'!B:B'))
原理就不解释了,这时候可以避开 lookup 函数对应列必须升序排列的缺陷并且直接得到结果,但这仅适用该列只有一个 “结果” 的情况。 当存在多个 “结果” 时,由于 Lookup 使用二分法查找,故不会像 match 一样返回第一个匹配的值。
此时,就可以根据你的需求,随心所欲地获取你想要的单元格的数据了,包括可以实现引用倒数几行等等。
3、其他配套事项
所以我一般碰到一个需要 1、合并多张工作簿中的工作表 - 2、重命名工作表 - 3、做一张汇总表 的时候一般步骤如下:
1、利用 VBA(网上有很多代码 or 插件) 把多个工作簿里的工作表合并到一个工作簿里
2、利用 VBA 获取现有的工作表名
3、这样会在 A 列按 sheet 顺序列示现有工作表名,在旁边写上重命名成什么
4、利用 VBA 重命名工作表
5、按照上述关于 indirect 的指引增加汇总工作表
6、增加工作表超链接:\=HYPERLINK('#''&A3&''!A1',A3)
以上就是我个人对于这类汇总工作常见需求的应对方法,如果有更方便的欢迎讨论。此外,其实网上也有很多 excel 插件汇总了各种批量合并批量改名等等功能,我自己也装了。不过由于付费、安全等等因素,有时候不一定适用所有人,VBA 函数则是微软默认功能,不会存在突然用不了了的问题。
以上函数由于展开来讲内容太长了,其实能够复制然后对应着自己的需求改下公式就行了。
最后之前有个小伙伴后台问关于 Power query 合并重复的问题,因为过了 48 小时就没法再回复了,就直接在这里回复:
我不清楚你用的是 2013 还是 2016 版本,根据我 2013 的经验来说,如果是从文件夹合并,这时会自动进行如你发的那个专栏文章所说的删除其他列并拓展,比如此时我从文件夹加载一堆表,选择合并和编辑,在尚未进行任何其他操作的情况下,左侧默认选择其他查询:
右侧显示如下:
不用进行任何其他操作,自动就是把几个工作表按顺序拼起来的样子了。我不确定你说的问题是由于版本不同造成的还是其他原因。
在看点一下 大家都知道
http://www.360doc.com/content/19/0824/12/30583536_856778219.shtml
设计模式看了又忘,忘了又看?
设计模式汇总
耗时了 5 个月,终于把设计模式一整个系列写完。其实设计模式这一系列文章网上已经有很多非常好、非常优秀的文章,为什么要写呢?
一方面是为了学得更扎实,印象中设计模式学习了 2 遍,记得牢的基本就那几个众所周知的,反思前面 2 次学习过程,缺少了思考的过程,没有把知识消化掉转化成自己的,就像动物一样,吃进去的东西没有消化只能排出。
另一方面是利用这个学习过程,学会把知识用文字表达出来,也把这份知识分享给各位同道中人。
没有期望说这系列的每篇文章都对你有意义,这要求太高了,我远没有这个能力,但是如果能有一篇文章让你看完就把这个设计模式都记住了,那这系列文章的目标就达到了。
这里整理了这个系列文章汇总,有关注公众号的同学可以直接点击菜单【设计模式】看所有文章,没有关注的同学可以收藏这篇汇总文章。划重点:这一系列文章已经整理成 PDF 电子版,在公众号后台回复【**设计模式**】即可获取,
或者扫下面二维码。
六大原则
单一职责原则(方法:修改名字还是密码?接口:洗碗、买菜还是倒垃圾?类:注册、登录和注销)
里氏替换原则(我儿来自新东方烹饪)
依赖倒置原则(抠门的饭店老板)
接口隔离原则(小伙子的作坊)
迪米特法则(手机上看电子书)
开闭原则(社保这点事)
五大创建型模式
创建型模式:单例模式(小明就只有 1 辆车)
创建型模式:工厂方法(小明家的车库)
创建型模式:抽象工厂(宝马车就得用宝马轮胎和宝马方向盘)
创建型模式:建造者模式(汤这么煲)
创建型模式:原型模式(复印书籍)
十一大行为型模式
行为型模式:模板方法(运动鞋制造过程)
行为型模式:中介者模式(租房找中介)
行为型模式:命令模式(技术经理分配任务)
行为型模式:责任链模式(面试过五关斩六将)
行为型模式:策略模式(洗衣模式)
行为型模式:迭代器模式(听歌这件事)
行为型模式:观察者模式(朋友圈)
行为型模式:状态模式(P2P借款状态流程)
行为型模式:备忘录模式(你的发布平台好用么?)
行为型模式:解释器模式(SQL 解析)
行为型模式:访问者模式(宴请领导人)
七大结构型模式
结构型模式:适配器模式(你用过港式插座转换器么?)
结构型模式:桥接模式(IOS、Android 二分天下)
结构型模式:组合模式(程序猿组织架构)
结构型模式:装饰模式(夏天到了,吃碗龟苓膏解解暑)
结构型模式:外观模式(你需要一个技术组长)
结构型模式:享元模式(还记得童年的蜡笔画么?)
结构型模式:代理模式(你我都知道的这道墙)
后台回复『大礼包』获取 Java、Python、IOS 等教程
加个人微信获取架构师、机器学习等教程
LieBrother
**生活不止代码,还有诗和远方!**没时间解释了,快长按左边二维码关注我们~
Vue中容易被忽视的知识点 - 掘金
前言
Vue的学习成本和难度不高,这除了和框架本身的设计理念有关之外,还和Vue完善的官方文档有着密不可分的关系,相信有很多的开发者和我一样,很长时间没有仔细去看过官方文档了,最近花时间仔细看了一下官方文档,将里面一些容易忽视的点整理出来和大家分享。
容易忽视的点
箭头函数的使用
ES6的普及使得箭头函数的使用更加频繁,但是在Vue中不要在选项属性或者回调上使用箭头函数,举个例子:
new Vue({
el: '#app',
data: {
show: true
},
created: () => {
console.log(this.show)
},
})
将created钩子写成箭头函数,这里的this将不再指向Vue对象,在浏览器中将会指向window对象,这是因为箭头函数并没有this,this会作为变量一直向上级词法作用域查找,直到找到为止
指令动态参数
Vue从2.6.0开始,可以用方括号括起来的JavaScript表达式作为一个指令参数,举个例子:
<div id="app">
<input v-on:[event] = "doSomething">
<button v-on:click="event = 'focus'">change</button>
</div>
new Vue({
el: '#app',
data() {
return {
event: 'input'
}
},
methods: {
doSomething () {
console.log('sss')
}
},
})
这里将input的事件监听设置为一个动态的参数event,默认是监听点击事件,当点击change的时候,改为监听focus事件,动态参数预期会求出一个字符串,异常情况下值为null,null值可以用于移除绑定,任何其他非字符串类型的值都会触发一个警告
template中使用方法
methods中提供的方法大多数时候都是用来给其他方法调用的,但是它其实也可以像computed计算属性一样直接写在模版里,举个例子:
<div id="app">{{reversedMessage('hello')}}</div>
var app = new Vue({
el: '#app',
methods: {
reversedMessage: function (message) {
return message.split('').reverse().join('')
}
},
})
有了computed计算属性,为什么还要用methods呢?计算属性是基于响应式依赖进行缓存的,只在相关依赖发生改变时才会重新求值,而methods每次调用都会重新计算,调用methods时可以传参,进行指定计算,但是computed不行,这在遍历数组时十分有用
用key管理可复用元素
Vue会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。这么做会使 Vue变得非常快,举个例子:
<div id="app">
<template v-if="loginType === 'username'">
<label>Username</label>
<input placeholder="Enter your username">
</template>
<template v-else>
<label>Email</label>
<input placeholder="Enter your email address">
</template>
<button @click="change">change</button>
</div>
var app = new Vue({
el: '#app',
data() {
return {
loginType: 'username'
}
},
methods: {
change () {
this.loginType = this.loginType === 'username' ? 'email' : 'username'
}
}
})
上面代码中切换loginType将不会清除用户已经输入的内容,因为两个模版使用了相同的元素,如果不想复用也很简单,只需要添加一个具有唯一值的key属性即可:
<template v-if="loginType === 'username'">
<label>Username</label>
<input key="username" placeholder="Enter your username">
</template>
<template v-else>
<label>Email</label>
<input key="email" placeholder="Enter your email address">
</template>
<button @click="change">change</button>
现在切换,每次都会重新渲染,但是label元素还是会被复用,因为它没有加唯一key值
v-if与v-for一起使用
Vue的风格指南不推荐同时使用v-if与v-for,当项目中的eslint继承了@vue/standard时,同时使用就会编译报错,但是可以通过在模版上加进行忽略,同时当它们处于同一节点,v-for的优先级比v-if更高,这意味着v-if 将分别重复运行于每个v-for循环中
对象变更检测
在Vue中对于已经创建的实例,不允许动态添加根级别的响应式属性,但是我们知道可以通过Vue.set(object, propertyName, value)方法向嵌套对象添加响应式属性,那如果需要为已有对象赋值多个新属性呢?举个例子:
<div id="app">{{user.name}}-{{user.age}}-{{user.sex}}</div>
var app = new Vue({
el: '#app',
data() {
return {
user: {
name: 'xxx'
}
}
},
created() {
this.user = Object.assign({}, this.user, {
age: 18,
sex: 'name'
})
},
})
可以用Object.assign为这个对象重新赋值,这样就能添加多个新的响应式属性
内联方法访问原始DOM事件
有时在模版中调用方法时,我们需要向方法中传参数,但是同时又要传递原始的DOM事件,怎么处理呢?举个例子:
<div id="app">
<button @click="share('share info', $event)">share</button>
</div>
var app = new Vue({
el: '#app',
data() {
return {
user: {
name: 'xxx'
}
}
},
methods: {
share (info, event) {
console.log(info, event)
}
},
})
如例子所示,可以用特殊变量$event把它传入方法
once、passive事件修饰符
Vue中提供了多个事件修饰符, once、passive是后面新增的两个,once用于限定事件只触发一次,passive用于修饰的事件发生后立即触发,用于提升移动端性能
表单输入修饰符
.lazy
在默认情况下,v-model在每次input事件触发后将输入框的值与数据进行同步,可以添加lazy修饰符,从而转变为使用change事件进行同步,举个例子:
<input placeholder="lazy" v-model.lazy="msg" @input="input" @change="change">
.number
如果想自动将用户的输入值转为数值类型,可以给v-model添加number修饰符,这通常很有用,因为即使在type="number"时,HTML输入元素的值也总会返回字符串。如果这个值无法被 parseFloat()解析,则会返回原始的值,举个例子:
<input placeholder="number" v-model.number="age" @input="input">
.trim
如果要自动过滤用户输入的首尾空白字符,可以给v-model添加trim修饰符,举个例子:
<input placeholder="trim" v-model.trim="trim" @input="input">
子组件替换/合并已有的特性
在Vue中对于绝大多数特性来说,从外部提供给组件的值会替换掉组件内部设置好的值。所以如果传入type="text"就会替换掉 type="date"并把它破坏!庆幸的是,class和 style特性会稍微智能一些,即两边的值会被合并起来,从而得到最终的值,举个例子:
<div id="app">
<base-input type="text" class="out"></base-input>
</div>
Vue.component('base-input', {
template: `<input type="date" placeholder="replace" class="default">`
})
new Vue({
el: '#app',
})
在上例中input的type值为date,class为deafault,在使用子组件时,向子组件中传入type="text" class="out",此时input的type值会被替换为text,class值会被合并为"default out",那么如果想要禁用属性继承怎么办呢?可以在组件的选项中设置inheritAttrs:false,举个例子:
Vue.component('base-input', {
inheritAttrs: false,
template: `<input type="date" placeholder="replace" class="default">`
})
但是inheritAttrs:false选项不会影响style和class的绑定,因此style和class还是会合并
.sync修饰符
在有些情况下,可能需要对一个 prop进行“双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以修改父组件,且在父组件和子组件都没有明显的改动来源,因此Vue提供了sync修饰符,举个例子:
<div id="app">
<span>{{title}}</span>
<text-document v-bind:title.sync="title"></text-document>
</div>
Vue.component('text-document', {
props: ['title'],
template: `<button @click="change">change</button>`,
methods: {
change () {
this.$emit('update:title', 'change')
}
},
})
new Vue({
el: '#app',
data() {
return {
title: 'default'
}
}
})
当调用this.$emit('update:title', 'change'),父组件中的title就会改变
总结
这篇文章对Vue中一些容易忽视的点进行了简单的总结,希望看完之后能对大家有所帮助。 如果有错误或不严谨的地方,欢迎批评指正,如果喜欢,欢迎点赞
不定宽溢出文本适配滚动
作者:chokcoco
在日常布局当中,肯定经常会遇到文本内容超过容器的情况。非常常见的一种解决方案是超出省略。
但是,有的时候,由于场景的限制,可能会出现在一些无法使用超出打点省略的方法的场景,譬如在导航栏中:
这种情况下,在容器定宽但是文本又溢出且不能换行的情况下,我们就需要寻求另外的解决方案。
hover 时弹出框提示
一种可行的方案是在 hover 的时候,弹出一个文本框展示全文,最简单的就是在文本标签下添加 title 属性,填充我们需要的内容:
1. `<nav>`
2. `<p title="溢出文本1 溢出文本2 溢出文本3 溢出文本4">溢出文本1 溢出文本2 溢出文本3 溢出文本4</p>`
3. `</nav>`
当然,这种方法简单但是可能缺乏点用户体验。
本文将简单介绍在文本长度不确定,容器长度也不确定的情况下,任意长度的文本实现 hover 状态下,从左向右,滚动到文本末端,再滚动回初始位置,如此反复,像是这样:
容器定宽,文本不定宽
我们先假设一下,我们的容器的宽度如果是固定的,但是不确定每条文本的宽度。
像是这样:
1. `<div class="wrap">`
2. `<p title="我的宽度是正常宽度">我的宽度是正常宽度</p>`
3. `<p class="scroll" title="我的宽度是溢出了一小部分">我的宽度是溢出了一小部分</p>`
4. `<p class="scroll" title="我的宽度是溢出了溢出了很大一部分">我的宽度是溢出了溢出了很大一部分</p>`
5. `</div>`
1. `.wrap {`
2. `position: relative;`
3. `width: 150px;`
4. `overflow: hidden;`
5. `}`
7. `p {`
8. `white-space: nowrap;`
9. `}`
使用 inline-block 获取实际文本的宽度
由于 <p>
标签的宽度为父元素的 100%,如果是这样,我们很难进行下面的操作。我们首先需要拿到实际文本的宽度,这里可以借助 inline-block 的特性,做到这一点,我们改进下我们的 CSS:
1. `p {`
2. `+ display: inline-block;`
3. `white-space: nowrap;`
4. `}`
这样,当前 <p>
标签的实际宽度,其实就是整个文本元素的宽度。
Tips:这里没有使用 display: inline 是因为下文我们需要让 p 元素滚动起来需要用到 transform,但是 transform 是无法作用在内联元素之上的。具体可以参考规范:transformable element
算出滚动距离,进行滚动
这样,我们有了父元素的宽度 150px,文本的宽度。那么很容易得到需要滚动的距离:
需要滚动的距离 S = 溢出的文本元素的宽度 - 父元素的宽度
这样,我们只需要找到一个可以表示并且当前文本宽度是变量值即可。即是 -- transoform。
由于在使用 transform: translate() 进行位移的时候,如果使用百分比表示,那么百分比的基准元素是元素本身,也就是如果我们 transform: translate(100%, 0),其实表示的就是向右移动一个元素本身宽度的距离。
那么我们可以借助 calc 非常容易的拿到我们上述的需要滚动的距离 S -- transform: translate(calc(-100% + 150px), 0),嵌入动画中:
1. `p:hover {`
2. `animation: move 1.5s infinite alternate linear;`
3. `}`
5. `@keyframes move {`
6. `0% {`
7. `transform: translate(0, 0);`
8. `}`
9. `100% {`
10. `transform: translate(calc(-100% + 150px), 0);`
11. `}`
12. `}`
至此,对于任意超出容器宽度的文本,我们都可以轻松的完成上述的效果。
不定宽文字跑马灯来回滚动展示 -- 父容器定宽,子元素不定宽:https://codepen.io/Chokcoco/pen/QWyoMrx
父容器不定宽度
当然,还没完。
如果父容器的宽度也是不固定的,或因为者 calc 兼容性问题无法使用上述方法。那么,我们要做的就是,在一段固定的 CSS 代码中,既能运动当前元素的宽度,也能位移父容器的宽度。
正巧,CSS 还真能完成上述要求,我们改造一下 animation 的代码:
1. `@keyframes move {`
2. `0% {`
3. `left: 0;`
4. `transform: translate(0, 0);`
5. `}`
6. `100% {`
7. `left: 100%;`
8. `transform: translate(-100%, 0);`
9. `}`
10. `}`
transform: translate(-100%, 0) 能够向左位移自身宽度的 100%
left: 100% 能够实现向右位移父容器宽度的 100%
使用 margin-left 替换 left 也是一样可以实现的,使用百分比表示的 margin-left 位移的基准也是父元素的宽度。
这样,不论父容器宽度如何,文本元素宽度如何,都可以实现对溢出文本适配滚动展示。
不定宽文字跑马灯来回滚动展示 -- 父容器不定宽,子元素不定宽:https://codepen.io/Chokcoco/pen/oNbVGrd
部分不足之处
无法判断文本长度是否超出父元素宽度
当然,上述方案并非完美的方案,如果我们希望只针对本文长度溢出的情况,hover 的时候才进行滚动,这一点在使用纯 CSS 的情况下是无法实现的。
我们无法通过 CSS 去判断当前元素长度是否大于父元素长度再选择性的进行动画。毕竟 CSS 只是负责样式,不控制行为。所以实际使用中,可能还是需要借助 JavaScript 简单判断,然后通过一个 class 进行控制。
动画闪烁
在父容器不定宽度的情况下,由于需要同时对两个属性进行动画,并且位移的方向是相反的,所以动画看上去会有一点闪烁。这个暂时没有找到特别好的解决方案。
最后
- 欢迎加我微信 (winty230),拉你进技术群,长期交流学习...
- 欢迎关注「前端 Q」, 认真学前端,做个专业的技术人...
辞退补偿你不知道的事
我怀孕时虽然因为 hr 失误,损失了五千多生育津贴。
但那一年我过得确实是又滋润又嚣张,各种迟到早退休假。
主要是身体不好先兆流产卧床仨月,所以老板也额外关照了。
同时作为工作多年的老员工,就算公司有心辞退我,也赔不起辞退补偿。
还不如再养几个月,生完还能继续做牛做马。
要知道辞退一个孕妇,不仅要给 2n 的非法辞退补偿,还要继续为其缴纳社保,直到哺乳期结束,得不偿失。
最近由于双减,很多教培行业的员工 “被辞退” 了。
我就一个建议,辞职申请书绝对不能写,辞退补偿的钱一定要要,不给补偿就仲裁,光脚的不怕穿鞋的。
正常情况下,公司辞退你,需要给 n+1 个月的工资作为补偿。
N 是指工作年限,工作 3 年就是 3 个月,不满 1 年的按 1 年算,不满半年的按半年算,所以工作 3 年 7 个月要给 4 个月的补偿。
按照劳动法的规定,公司想要赶咱们走人,必须提前一个月通知。
如果没有提前通知你多给一个月的工资,这一个月的工资也叫代通知金,也就是 “+1” 的含义。
但是像我当时是孕妇,如果被辞退就属于 “非法辞退”,那么辞退补偿就要翻倍到 2N 了。
还有一点需要注意:离职补偿金对应的月收入不是基本工资,而是 “劳动合同解除或者终止前十二个月的平均工资”。
比如我月薪 1 万,年终奖 6 万,那么就要按照每个月 1.5 万,来给我计算辞退补偿。
辞退补偿也是有上限的,最多就给 12 年,金额最高是当地上一年社平工资的 3 倍。
举个例子
我月薪 5 万,工作两年,需要给我 2+1=3 个月 15 万的辞退补偿。
去年北京的社平工资是 8 千多,我们按 9 千来算,3 倍就是 2.7 万,一年就是 32.4 万。
虽然每月补偿 5 万超过了 2.7 万,但总额没有超过 32.4 万就是可以的。
被辞退不仅有辞退补偿,还能领失业金。
前提都是 “被辞退”,离职手续中千万别写 “个人原因”。
最后,分享几个资深 hr 总结的常见辞退补偿情况:
1、合同到期不签了,要给辞退补偿吗?
分两种情况:
员工不签约,没有辞退补偿;
企业不签约,n 倍辞退补偿。
2、公司不辞退,逼你辞职怎么办?
现在的 “调岗” 已经被玩坏了,成了企业变相辞退员工的手段。
调岗必须经得员工同意、签订书面协议才生效。
并且需要满足四个条件:
的确处于企业生产经营需要;
调岗后的工资水平和原岗位基本相当;
不得具有侮辱性和惩罚性;
不违反法律和行政法规。
比如让一个程序员去做保洁,明显具有侮辱性,员工可以拒绝。
企业如果执意调岗,那么恭喜你,你可以以此为由要求双倍补偿了。
3、“被旷工” 了怎么办?
你们的领导 “体谅” 你离家远,特批上下班不用打卡,什么时候来都行。
结果三天之后就以旷工的名义开除你,或者 HR 口头告知要把你辞退,不用来上班了。
如果之后真的不来上班,那就中计了。
三天之后同样会收到因为旷工被开除的通知。
诸如此类,套路满满。
那我们应该应对呢?
首先,不轻信任何不会留下痕迹的通知;
其次,即使被通知不用来上班了,在没有办离职手续前要一直按时上下班。
打不了卡,可以在公司门口来一张美美的自拍、可以给同事发送工作邮件、可以做好工作日志。
证明自己来上班了,并且干活了。
然后,觉着风向不对时,警惕领导与你的谈话,可以录个音。
最后,实在不行,咱就仲裁诉讼。
4、孕期被辞退怎么补偿?
如果是孕期、产期、哺乳期,那么恭喜你,辞退补偿的钱更多了。
辞退一个孕产妇不仅要给 2n 的补偿,还要负担她到哺乳期结束的社保,剩余津贴等。
不过虽然有劳动法护身,三期(孕期、产期、哺乳期)员工也不是完全不能开。
比如严重违纪、严重失职等情况,企业是可以辞退的,并且没有离职补偿。
关于辞退补偿,其实就这几点:
1、坚决不写辞职申请,等着被辞退;
2、发没有协商一致之前,坚持上班并留痕;
3、不明白就答 12333 进行咨询或者找八姐;
4、整理好自己手里的证据(劳动合同、工资条、考勤证据、工作日志、公司的通知、聊天记录、录音录像等);
5、向劳动监察部门举报;
6、申请劳动争议调解;
7、调解不成可以申请劳动仲裁;
8、对仲裁结果不满可向法院提起诉讼。
据我所知类似的仲裁,大概率是会偏向劳动者的,所以放心去仲裁诉讼。
加八姐微信防失联,还能在换工作时给你出出主意。
公众号、网页文章「404」之前,这 7 种方法帮你备份 - 少数派
我们在网页、公众号上阅读的文章往往都具有「在线」属性。也就是说,如果文章一旦被删除,你就无法再阅读它了。无论是网站被屏蔽、平台关停,还是作者自主删除,都可能导致文章失效。
这时候,一份能够永久保存的副本可以让你不用担心文章被删,随时查阅。本文将介绍 7 种不同方法帮你将文章永久保存,让你可以随时翻阅想读的文章。
保存到本地
为文章保留一个本地副本是最直接、也是最安全的一种做法。只要你的硬盘不坏,文章就会被永久保存。将文章保存到本地有三种好用的方法:保存为 PDF、生成网页存档及发送至 Kindle。你可以根据自己的喜好选择合适的方法。
保存为 PDF
将网页保存为 PDF 是一个普适性最高,同时也相对方便的方法。现在的浏览器大多都拥有将网页保存为 PDF 的插件,有些浏览器甚至将其作为内置功能。在电脑上,你也可以通过浏览器的打印功能将网页直接「打印」成 PDF 文件。
以 iOS 13 上的 Safari 为例,你有两种方法将网页保存成 PDF。最简单的方式就是在网站上截图,随后选择「整页」并调整范围即可。即使不选择「整页」,系统也会在你截图后自动生成一个 PDF 文档保存在 iCloud Drive 的「下载项」里。
第二种方法则是通过「万能」的分享菜单实现。在 Safari 中点击分享按钮,在弹出的菜单中选择「选项」,就可以选择 PDF 并将其直接分享转存至其它应用。
虽然在 iOS 上将网页保存为 PDF 的操作并不复杂,但偶尔会出现网页无法完全加载的情况。在一些长篇幅文章,或是配图较多的文章中尤为明显。这时候你也可以利用 滚动截屏 App 来实现网页的存档,生成为长截图后再利用快捷指令将它导出成 PDF。
滚动截图
滚动截图的好处就是在保存公众号文章时,不需要跳转至浏览器,可以直接在微信里实现截图。并且它的操作也相对便捷、直观,不需要手动进行设置就能获得很好的效果。
在 Android 平台上,Chrome、Opera 等浏览器都支持在「分享」菜单中将网页保存为 PDF。并且 Android 对滚动截图拥有良好支持,在大部分国内厂商定制的系统上,你甚至不需要安装第三方 App 就能做到滚动截图。
网页存档
虽然保存为 PDF 既便捷、效果也不错,但 PDF 有一定的弊端:比如图片形式的 PDF 无法复制和检索文字、无法根据屏幕大小自适应调整等。网页存档则能在保持便捷和良好阅读效果的前提下,改善这些问题,毕竟网页存档是直接将整个网页保存下来的。
网页存档支持自适应
在绝大部分的操作系统里,你都可以在浏览器中使用 Ctrl(⌘ Command)+ S 或分享菜单来生成网页存档。在 Windows 和 Android 可以生成 .mhtml 文件,而在 macOS 和 iOS 中则会是 .webarchive 文件。两者的原理相同,都是将网页上的各元素打包成一个文件。
两种不同格式
需要注意的是,Windows 默认情况下会将网页保存为 .html 格式,并生成一个包含媒体素材的文件夹。你需要手动将格式修改为「网页(单个文件)」,才能生成单个 .mthml 文件。
虽然网页存档能够做到自适应屏幕尺寸、具有高度可编辑性、支持 GIF 动图等特点,但它的体积往往也要大于 PDF 文件。并且在一些动态页面里,在不同的设备上观看虽然能够实现文字的自适应效果,但图片和一些特殊元素则可能出现排版错误的现象。
无论是保存为 PDF 还是生成网页存档,我都建议你先将需要保存的内容浏览一遍,确保图片加载完毕再保存。
发送至 Kindle
如果你有一台 Kindle,你也可以尝试将文章发送到 Kindle 设备来进行保存和阅读。
将文章发送至 Kindle 其实并不难,你首先需要在微信中关注「亚马逊 Kindle 服务号」并绑定你的 Kindle 邮箱。当你看到想要推送的公众号文章时,点击右上角的菜单按钮,并选择「亚马逊 Kindle 服务号」即可。除此之外,服务号还支持发送文字、图片到 Kindle。
如果你想将网页内容发送到 Kindle,也可以将网页链接粘贴到服务号的对话框中。此外,也有不少第三方工具支持以更丰富的形式和更优秀的排版将网页文章发送至 Kindle,比如少数派先前介绍过的 Klip.me、Doocer 等。
你所发送至 Kindle 设备的内容都会被保存在你的亚马逊云空间里,就算你在 Kindle 上删掉了,也可以随时找回。当然,即使你没有 Kindle,也可以通过 Kindle 应用来阅读。
在线服务帮你忙
生成本地副本虽然方便又安全,但如果你想随时在多平台阅读文章,这样的方式自然是不适合的。下面介绍的这 4 种在线服务可以帮你把文章保存在一个安全的第三方平台,让你可以随时查阅。
云笔记
市面上有不少云笔记服务,其中 OneNote 和印象笔记作为普适性最广、用户满意度也相对较高的云笔记工具,是用于保存文章的好选择。
在桌面端,OneNote 与印象笔记都支持网页剪藏的浏览器插件,其中 OneNote 还支持在保存文章前进行内容过滤,可以选择单独仅保存文章,或是整页保存。印象笔记也支持在保存文章前进行批注等操作,体验不相上下。
OneNote Web Clipper
但在移动端的表现,OneNote 就不如印象笔记了。通过分享菜单保存到 OneNote 的网页很大可能只会保存一个网页链接,而不是整个页面。印象笔记在移动端的体验就做得更好,浏览器分享转存的体验流畅,还支持通过微信公众号和微博来保存文章。
在保存文章后,OneNote 强大的可编辑性让它在批注体验上要好于印象笔记。在触屏设备上,你还可以通过手写的方式来批注。如果你习惯手写批注,那么对于需要精读的文章,OneNote 或许能给你更好的体验。
在微信公众号、微博等我们常用的国内平台上,OneNote 就显得力不从心。如果你想保存微信公众号的文章,只需要关注「我的印象笔记」服务号并绑定账号就能实现快速保存。通过服务号保存的文章会生成速读摘要,还能帮你计算文章字数、图片数和阅读时间。
至于微博和今日头条等平台的文章,两者都是以「保存网页」的形式来转存的,因此差别不大。虽然印象笔记支持通过 @我的印象笔记 来快速保存微博,但仅限于微博,而不包括微博文章。
仅支持保存微博
总的来说,OneNote 与印象笔记两者在保存文章的体验上各有千秋。考虑到价格因素,保存网页方面我更推荐完全免费的 OneNote,如果你更需要保存公众号文章,又或者你已经订阅了印象笔记,那么印象笔记或许更适合你。
Notion
作为一个「瑞士军刀」式的工具,Notion 自然也可以用来保存网页文章。Notion 既有官方的 Web Clipper 插件,也有第三方的插件可供选择。其中 Notion 官方插件仅支持网页剪藏,而第三方插件支持保存前编辑等功能。
在移动平台,你可以直接通过分享菜单将网页保存到 Notion。不过这种方法偶尔会出现一些问题,比如只保存了链接而没保存内容,或是内容保存不全。虽然 Notion 剪藏在移动端的体验还有待改善,但如果你是一个 Notion 用户,不妨将网页剪藏也加入你的 Notion 资料库中。
稍后读工具
许多稍后读工具都支持将文章离线保存,Pocket 和 Instapaper 是稍后读工具中最具名气的两个服务,两者都支持以简洁美观的排版保存文章内容。
左:Pocket,右:Instapaper
在 Instapaper 中,只要你完成了「保存」这个操作,无论是浏览器插件、书签还是移动应用,Instapaper 就会为那篇文章保存一份当时的副本并保存在服务器上,即使文章立即被删除或修改也不会受到影响,你可以在任何时候重新下载回来。
Pocket 则略有不同,你所保存到 Pocket 的文章会在你打开 Pocket 后加载并保存至本地。也就是说,如果你在设备 A 上将一篇文章保存至 Pocket 并下载到本地后,文章就被删除了,那么你将无法在设备 B 上重新下载回来。
只有在订阅高级版后,你才能享有永久保存文章副本的权利,并且这也是需要你先行下载才能在后台自动上传,相比于 Instapaper 来说,稍显麻烦。
在排版方面,两者都支持在本地进行字体、字号的调整。Pocket 还支持报告文本的排版问题,Pocket 团队会及时进行更新。此外,Pocket 还支持为文章添加标签,而 Instapaper 则是以文件夹形式实现文章的过滤筛选。
两者在免费版的体验上表现都很出色,支持保存无限数量的文章、多平台同步等功能。Instapaper 免费支持永久保存副本这一点显然比 Pocket 更能吸引用户,而 Pocket 的语音朗读功能则吸引了不少喜欢「听文章」的人。在高级版方面,两者都支持全文搜索、无限标注等功能。
Internet Archive
上面提到的方法只适用于「保存现有文章」的情况,如果你想保存的文章已经被删除了,别急,Internet Archive 的 Wayback Machine 也许可以帮到你。你只需要输入链接即可浏览历史快照,找到文章未被删除的时间点保存即可。
这种方法虽然可以帮你抓取已经被删除的文章,但局限还是比较大的。首先你必须知道文章的具体链接。其次,你所访问的站点必须具有一定的名气才能被 Internet Archive 截取更多快照,比如一些个人博客的文章就不适合通过此方法来找回。这种方法更适合用来保存浏览器书签中失效的网页,公众号文章基本不适用于这种方法。
我该选择什么方法
对于偏爱本地存储的人来说,如果你追求还原原始阅读感受,那么生成网页存档让你可以在不同设备间享受更加原汁原味的浏览体验。保存为 PDF 则更适合那些喜欢批注的人。如果你有 Kindle,也可以将它利用起来,毕竟「Send to Kindle」的体验还是非常不错的。
如果你喜欢在线服务,那么免费也能做好事的 OneNote、Notion 和 Instapaper 都是不错的选择。如果你愿意为印象笔记和 Pocket 付费,那么它们能给你带来更好的阅读体验。而 Internet Archive 作为一种补救措施,虽然拥有一定的局限性,但还是可以为你找回一些已经不见的文章。
对于公众号或者网页文章,你会怎么保存它们?欢迎在评论区和我们分享。
> 下载少数派 客户端、关注 少数派公众号 ,发现更多新酷应用 🆒
> 年度回顾、好物推荐……更多精彩尽在 少数派 2019 年度盘点 🎉
vue2.x项目报错 vuex.esm.js sub is not function-pudn.com
aiyang1214878408 于 2022-05-12 18:48:59
场景
一般在 vue2.x 项目中我们会使用 Vue DevTools 插件来帮助我们开发,但是今天项目突然出现 bug,调用 vuex 的 dispatch 方法时突然出现报错,如下:
TypeError: sub is not a function
at eval (vuex.esm.js?2f62:422:1)
at Array.forEach (<anonymous>)
at Store.dispatch (vuex.esm.js?2f62:422:1)
at Store.boundDispatch [as dispatch] (vuex.esm.js?2f62:332:1)
at eval (list.vue?51fc:432:1)
排查原因发现是使用 Vue DevTools 插件的问题,因为我的浏览器现在安装了 Vue DevTools2.x 和 3.x 的版本,这次出问题主要是在 vue.2x 的项目中使用 Vue.js devtools 6.0.0 beta 15(3.x)版本引起的。
后面发现不仅是调 vuex 可能报错,页面之间跳转可能也会引发异常.
解决方法:
- 关闭或者重启 Vue.js devtools 6.0.0 beta 15(3.x)版本
- 在 Vue 2 内置插件中添加了开启设置 Legacy Actions
第一种方法有手就行,这里主要是说下第二张方法
在 Vue 2 内置插件中添加了开启设置 Legacy Actions
- 首先打开控制台找到自己的 vue 插件,然后点击右边的三个小圆点
3. 点击左边的 vue2, 就会出现右边的红色框框起来的东西,最后点击打开里面的 Legacy Actions 按钮
注意:我之前 vue3.x 版本用的是 devtools 6.0.0 beta 15,发现是没有第三步的 Legacy Actions 按钮,所以我去仓库拉了一个新的 3.x 版本的,这样就有这个配置了。(现在我用的是 Vue.js devtools 6.1.4)
本内容为 PUDN 经合法授权发布,文章内容为作者独立观点,不代表 PUDN 立场,未经允许不得转载。
https://www.pudn.com/news/627e2c4b5981aa38efad852f.html
前端装逼技巧 108 式(一)—— 打工人
你在拼多多到处找人砍价,他在滴滴打车求人助力,我在电子厂拧螺丝拧到凌晨,我们都有光明的未来!早安,打工人!
本文因涉及大量代码,因此建议读者在 PC 上阅读更佳!
楔子
作为一名拥有钢铁般意志的前端打工人,装逼是不可能的,这辈子都不可能装逼。如果真要装逼,那就大家一起装逼,毕竟前端要讲武德嘛,要耗子尾汁。遂决定写下前端装逼技巧 108 式,供诸君茶余饭后一乐,甚至时不时秀个*操作,为打工的生活增添一抹亮色。
因作为打工人,时间、精力有限,目前大纲只有约 50 式,还望诸君有好的知识点私信或者在评论区留言,大家共同装逼、共同迎接打工人的光明未来!
文章风格所限,引用资料部分,将在对应小节末尾标出。
第一式:子曰,公欲装逼好,工具少不了
代码太丑陋,carbon 来相救:把你的代码转换为精美图片进行分享
carbon
本文为便于代码复制,将奉行不首先装逼的原则,尽量减少此装逼利器的使用。
第二式:console
调试万金油,学会开车更上头
console.log()
在前端调试中的地位自不必赘述,其实一代车神也对其五体投地,不信诸君细看(如真有不解其意者,建议发扬不耻下问的求知精神,问问你旁边的同事):
console.log
是的,以上图片是由console.log()
完成的,我没有骗你,贴出代码以证清白,为便于诸君控制台开车,此处我们忘掉第一式:
// 在此提醒,为免于生成丑陋的锯齿背景图片,请注意空格的个数,并保证console面板的宽度。
为什么会这样呢?想必你还记得其他语言中的print()
。占位符是print()
的专属吗?不,他们在console.log()
中同样适用:
•%s
:字符串 •%d
:整数 •%i
:整数 •%f
:浮点数 •%o
:obj 对象(DOM)•%O
:obj 对象 •%c
:CSS 样式
console.log()
可以通过以上这些特有的占位符进行信息的加工输出。是的,你可能已经明白,上面代码的玄机就在四个%c
,第一个创建神秘而性感的纯黑背景;第二个给 “FBI WARNING” 加上红色的背景;第三个恢复纯黑的性感;第四个配上白色的文字,如此,大事已成。
明白了以上原理,诸君就可以自由发挥,展示你们强大的 css 实力了,甚至还可以输出 gif 背景图,在装逼的路上更上几层楼。不装了,我是 css 渣渣。
console.log(
孤蓬
那么,我们是否可以超越度娘,在官网控制台完成精美的招聘文案投送呢?
拓展:console
对象都有哪些方法?
console
参考资料:小蝌蚪日记:通过 console.log 高仿 FBI Warning[1]
Using the F12 Tools Console to View Errors and Status[2]
console.log 也能插图!!![3]
第三式:芙蓉面,杨柳腰,无物比妖娆 —— 让你看清 UI 的轮廓
•UI 轮廓哪里寻,outline
属性来帮您。
html * {
outline: 1px solid red;``}
UCloud
• 解析与思考 • 这里没有使用 border 的原因是 border 会增加元素的大小但是 outline 不会;• 通过这个技巧不仅能帮助我们在开发中迅速了解元素所在的位置,还能帮助我们方便地查看任意网站的布局;• 所有浏览器都支持 outline 属性; outline (轮廓)是绘制于元素周围的一条线,位于边框边缘的外围,可起到突出元素的作用;• 轮廓线不会占据空间,也不一定是矩形(比如 2D 转换等)。• 去掉 Chrome 浏览器中输入框以及其它表单控件获得焦点时的带颜色边框
input {
outline: none;``}
• 通过一个开关实现任意网页开启关闭 outline•Chrome 右上角三个点⇒书签⇒书签管理器⇒右上角三个点⇒「添加新书签」;• 名称随意,粘贴以下代码到网址中;• 然后我们就可以在任意网站上点击刚才创建的书签,内部会判断是否存在调试的 style。存在的话就删除,不存在的话就添加,通过这种方式我们就能很方便的通过这个技巧查看任意网页的布局了。
javascript: (function () {
var elements = document.body.getElementsByTagName('*');
var items = [];
for (var i = 0; i < elements.length; i++) {
if (
elements[i].innerHTML.indexOf('html * { outline: 1px solid red }') != -1
) {
items.push(elements[i]);
}
}
if (items.length> 0) {
for (var i = 0; i < items.length; i++) {
items[i].innerHTML = '';
}
} else {
document.body.innerHTML +=
'<style>html * { outline: 1px solid red }</style>';
}``})();
参考资料:很好用的 UI 调试技巧
第四式:角声寒,夜阑珊,又改需求。难,难,难!—— 类型转换助你不带脏字的骂产品、优雅的夸自己
•(!(~+[])+{})[--[~+""][+[]]*[~+[]]+~~!+[]]+({}+[])[[~!+[]*~+[]]]
:sb•([][[]]+[])[+!![]]+([]+{})[!+[]+!![]]
:nb•(+!![]*([]+{})+[]+{})[+[]]+([]+{})[!+[]+!![]]
:Nb
图解:取类型转换得到的字符串里的字母进行拼凑(看懂了原理,其实我们完全可以尝试写的更简练一些)
nb
插件:zhuangbility,一个可以逆向操作,输入文字,返回操作符的 npm 插件[4]
第五式:a == 1 && a == 2 && a == 3
,那你可以实现a === 1 && a === 2 && a === 3
吗?
•a == 1 && a == 2 && a == 3
:
// 当然,你也可以把 count 作为属性放在 a 对象上 ``let count = 1;``let a = {
valueOf: function () {
return count++;
},``};``console.log(a == 1 && a == 2 && a == 3); // true
• 对象在转换基本类型时,会调用该对象上 valueOf
或 toString
这两个方法,该方法的返回值是转换为基本类型的结果 • 具体调用什么取决于内置的 toPrimitive
调用结果 •a === 1 && a === 2 && a === 3
:
let count = 1;
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。同时,该 API 也是 Vue 2.x 数据绑定实现的核心,Vue 在 3.x 版本之后改用 Proxy 进行实现,本系列文章后续会进行简单讨论。
原理可参考:[译] 在 JS 中,如何让 (a===1 && a===2 && a === 3)(严格相等) 的值为 true?[5] 深入浅出 Object.defineProperty()[6] ECMAScript7 规范中的 ToPrimitive 抽象操作[7]
第六式:最近有点儿火的 Web Components 可能并不是小鲜肉
•html 很宽松,浏览器也可以识别不规则、不合法标签(元素)(如<custom-label>Web Components</custom-label>
会展示 "Web Components"。);• 自定义继承自HTMLElement
的类,称为自定义元素的类;• 经过window.customElements.define
API 定义和注册自定义元素,使得不合法标签(自定义元素)与自定义元素的类关联,实现合法化;• 通过模板标签<template>
简化类的定义过程并添加样式;• 通过自定义元素的attachShadow()
方法开启 Shadow DOM(这部分 DOM 默认与外部 DOM 隔离,内部任何代码都无法影响外部),隐藏自定义元素的内部实现;• 添加事件监听、进行组件化封装等。
参考资料:Web Components 入门实例教程 - 阮一峰[8] Window.customElements[9] Web Components[10]
第七式:Windows 环境变量设置其实可以很简单
使用 Windows 系统电脑进行开发的小伙伴也许经常会碰到需要手动设置环境变量的情况,其实设置环境变量也可以很简单:
```sh
参考资料:Windows 使用 cmd 命令行查看、修改、删除与添加环境变量[11]
第八式:1.toFixed()
、1.0.toFixed()
和1..toFixed()
,究竟哪个写法是对的?
在数字字面量中,1.xxxxx
这样的语法是浮点数表示法。所以1.toFixed()
这样的语法在 JavaScript 中会报错,这个错误来自于浮点数的字面量解析过程,而不是 “. 作为存取运算符” 的处理过程。在 JavaScript 中,浮点数的小位数是可以为空的,因此 “1.” 和“1.0”将作为相同的浮点数被解析出来。所以会出现:
1 === 1; // true
既然 “1.” 表示的是浮点数,那么 “1..toFixed” 表示的就是该浮点数字面量的 “.toFixed” 属性。当是数字字面量时,可通过类似Number(1).toFixed()
创建基本包装类型(显示装箱),然后就可以进行属性和方法的添加、读取(或者可借助小括号把字面量括起来,告诉浏览器引擎这是一个整体)。
• 装箱:将基本数据类型转换为对应的引用类型的操作(装箱又分为隐式装箱和显式装箱);• 拆箱:把引用类型转换成基本数据类型。
基本类型不能有属性和方法,当给它们添加属性的时候系统会自动进行包装类并销毁:
var num = 1;
参考拓展:谈谈 JavaScript 中装箱和拆箱[12]
第九式:typeof 不靠谱,我们又该如何判断类型?
•typeof
之殇:我们应该都知道,使用 typeof
可以准确判断除 null
以外的基本类型,以及 function
、symbol
类型;null
会被 typeof
判断为 object
。• 在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null 的类型标签是 0,typeof null 也因此返回 "object";• 在 ES 6 之前,typeof 总能保证对任何所给的操作数返回一个字符串。即便是没有声明的标识符,typeof 也能返回'undefined'。使用 typeof 永远不会抛出错误。但在加入了块级作用域的 let 和 const 之后,在其被声明之前对块中的 let 和 const 变量使用 typeof 会抛出一个 ReferenceError。块作用域变量在块的头部处于 “暂存死区”,直至其被初始化,在这期间,访问变量将会引发错误。• 以前经常拿来判断数组的instanceof
是怎么实现的:使用 a instanceof B
判断的是 a 是否为 B 的实例,即 a 的原型链上是否存在 B 构造函数(ES6 之后可以通过Array.isArray()
来判断是否是数组)。
// L 表示左表达式,R 表示右表达式 ``const customInstanceof = (L, R) => {
if (typeof L !== 'object') return false;
while (true) {
// 已经遍历到了最顶端
if (L === null) return false;
// 利用原型链进行判断
if (R.prototype === L.__proto__) return true;
L = L.__proto__;
}``};``customInstanceof([], Array); // true
•constructor 为什么不是我们的选择?•constructor 属性是可以被修改的,会导致检测出的结果不正确;• 除了undefined
和null
,其他类型的变量均能使用constructor
判断出类型。
let bool = true;``bool.constructor == Boolean; //true``let num1 = 1;``num1.constructor == Number; //true``let num2 = new Number();``num2.constructor == Number; //true``// constructor 属性是可以被修改的 ``num2.constructor = Object;``num2.constructor == Number; //false``let str = 'hello world';``str.constructor == String; //true
•Object.prototype.toString 竟如此万能?
Object.prototype.toString.call(123);``//"[object Number]"``Object.prototype.toString.call('str');``//"[object String]"``Object.prototype.toString.call(true);``//"[object Boolean]"``Object.prototype.toString.call({});``//"[object Object]"``Object.prototype.toString.call([]);``//"[object Array]"``// 定义 getType 方法,用来判断类型 ``getType = (obj) => {
return Object.prototype.toString.call(obj).slice(8, -1);``};``getType(12n); // BigInt``getType(Symbol()); // Symbol``getType(() => {}); // Function``getType(); // Undefined``getType(null); // Null``getType(NaN); // Number
资料参考:typeof[13] The history of “typeof null”[14]
第十式:十进制二进制互转,真的不用那么麻烦
• 使用NumberObject.toString(radix)
十进制转二进制:
// 如有补齐位数的需求,可通过判断返回值的长度在前面加 0``let num = 10;``console.log(num.toString(2)); // 1010
• 使用parseInt(string, radix);
二进制转十进制:
let num = 1010101;``console.log(parseInt(num, 2)); // 85
•Tips:由于以上代码都使用 let 定义了 num 变量,除了刷新页面外,该如何在控制台分别执行呢?只需把代码放在一对花括号之间即可(块级作用域)。
第十一式:没有加减乘除,如何比较正整数字符串的大小?
在接手的部分项目中,存在需要前端拼接 Elasticsearch 查询语句的情况,好不容易会了点 Elasticsearch,却发现问题并没有那么简单:金额数量区间查询你告诉我存储的是字符串?那岂不是会出现1<3000<5
的情况?天啦噜,不要逗我好吗?
那么,在不改动 ES 的情况下,如何通过正则查询来实现正整数字符串大小的比较呢?直接说思路:数位更多或者从高位开始比,数值更大即是更大的数【一时间没想到更好的解法,有更好的解法欢迎留言或者私信】。
// 通过正则表达式从字符串数组中筛选出大于某个数值的字符串类型数据
详细 Elasticsearch 列表页搜索公共方法实现可以查看我的这篇[15]笔记。
第十二式:如何让页面和你说话?—— TTS(Text To Speech)
在项目中需要对 ajax 请求返回的消息进行语音播报,str 为需要播报的信息(适应于错误信息语音提示等场景):
//语音播报
• 参数解释:•lan:固定值 zh。语言选择, 目前只有中英文混合模式,填写固定值 zh•ie: 编码方式 •spd:语速,取值 0-9,默认为 5 中语速 •text:合成的文本,使用 UTF-8 编码。小于 512 个中文字或者英文数字。(文本在百度服务器内转换为 GBK 后,长度必须小于 1024 字节)•React Native Text-To-Speech library for Android and iOS[16]• 用语音控制自己的网站 annyang[17]:A tiny JavaScript Speech Recognition library that lets your users control your site with voice commands.annyang has no dependencies, weighs just 2 KB, and is free to use and modify under the MIT license。
第十三式:失焦事件与点击事件冲突怎么办?
• 场景:• 下拉框中 blur 与 click 冲突;• 输入框 blur 与下方可点击浮沉 click 冲突:输入值时下方出现浮层,输入框失去焦点时,浮层隐藏;点击浮层条目触发搜索并隐藏浮层;• 问题:点击浮层时,由于失焦事件先触发,浮层隐藏逻辑执行,导致浮层的 onClick 事件逻辑无法执行
失焦事件与点击事件冲突
// 点击弹窗条目进行搜索
• 解决:• 方法一:给失焦事件设置延迟触发
onBlur = () => {
if (this.state.keyword) {
setTimeout(() => {
this.setState({visible: false});
}, 300);
}``};
• 方法二:使用 onMouseDown 替代 onClick•mousedown 事件:当鼠标指针移动到元素上方,并按下鼠标按键时,会发生 mousedown 事件,所以它会先于失焦事件执行。•mouseup 事件:当在元素上放松鼠标按钮时,会发生 mouseup 事件。
第十四式:不用加减乘除如何做加法——位运算让你的代码更高效
•JavaScript 位运算符
JavaScript 位运算符
位运算是基于二进制的,如何快速获得二进制可参考第十式。
• 不用加减乘除做加法
function add(a, b) {
let sum;
let add1;
while (b != 0) {
// 异或
sum = a ^ b;
// 与 左移
add1 = (a & b) << 1;
a = sum;
b = add1;
}
return a;``}
•JS 按位运算符的妙用:• 使用&
运算符判断一个数的奇偶(只需记住 0 和 1 与 1 进行&
运算的结果即可):•偶数 & 1 = 0
•奇数 & 1 = 1
• 使用~~,>>,<<,>>>,|
来取整:•~~Math.PI
:3(按位取反再取反)•Math.PI>>0
,Math.PI<<0
,Math.PI>>>0
:3(按位左移或者右移 0 位,>>>
不可用于负数)•Math.PI|0
:3,按位异或 • 使用<<,>>
来计算乘除:• 整数左移 n 位相当于乘 2 的 n 次方;• 右移相当于除以 2 的 n 次方,再向下取整 • 利用^
来完成比较两个数是否相等:!(a ^ b)
• 使用^
来完成值交换:参考第十五式 • 使用&,>>,|
来完成 rgb 值和 16 进制颜色值之间的转换 •16 进制颜色值转 RGB:
function hexToRGB(hex) {
hex = hex.replace('#', '0x');
let r = hex >> 16;
let g = (hex>> 8) & 0xff;
let b = hex & 0xff;
return 'rgb(' + r + ',' + g + ',' + b + ')';``}``hexToRGB('#cccccc'); // rgb(204,204,204)
•RGB 转 16 进制颜色值:
function RGBToHex(rgb) {
let rgbArr = rgb.split(/[^\d]+/);
let color = (rgbArr[1] << 16) | (rgbArr[2] << 8) | rgbArr[3];
return '#' + color.toString(16);``}``RGBToHex('rgb(204,204,204)'); // #cccccc
参考资料:JavaScript 位运算符[18]
第十五式:无聊的脑筋急转弯,不借助第三个变量交换 a,b 两个变量值的 N 种方法
• 方法一:加减
a = a + b;``b = a - b;``a = a - b;
• 方法二:位运算
a ^= b;``b ^= a;``a ^= b;
• 方法三:对象或者数组
a = {a, b};``b = a.a;``a = a.b;``// a = [a, b];``// b = a[0];``// a = a[1];
• 方法四:ES 6 解构赋值
[a, b] = [b, a];
• 方案五:运算符优先级
a = [b, (b = a)][0];
参考资料: 不借助第三个变量交换 a,b 两个变量值[19]
第十六式:如何在浏览器当前页面打开并操作另一个 tab 页
if (window.customeWindow) {
参考资料:BRAFT EDITOR 富文本编辑器预览[20]
第十七式:产品说要按照中文拼音顺序排序?
• 使用 stringObject.localeCompare(target)
方法实现中文按照拼音顺序排序
var array = ['上海', '北京', '杭州', '广东', '深圳', '西安'];
参考资料:String.prototype.localeCompare()[21]
• 一个对象数组按照另一个数组排序
sortFunc = (propName, referArr) => (prev, next) =>
第十八式:这段代码为什么会报错,说好的分号可以省略呢?
console.log(123)[(12, 2)].filter((item) => item > 3);
• 分号推断:编译原理里的分号推断,作用是在编程的时候,让程序员省略掉不必要的分号;•JavaScript 有着自动分号插入的机制 (Automatic Semicolon Insertion),简称 ASI(ASI 只是表示编译器正确理解了程序员的意图,并没有真的插入分号);• 浏览器引擎的 Parser(负责将 JS 源码转换为 AST)总是优先将换行符前后的符号流当作一条语句解析(带换行的多行注释与换行符是等效的);• 所以在 Parser 眼里,以上代码是这样的:•console.log(123)[12,2].filter(item => item > 3)
,console.log(123)
没有返回值,既undefined
;•[12,2]
中的方括号被视为读取console.log(123)
返回值中的属性2
,类似于根据下标取数组中的元素;• 为什么是取属性2
呢,因为12,2
是个逗号表达式,表达式的值是最右边的 “2”,如此以来,上面的报错信息就很好理解了。• 不能省略的分号:•for 循环头部的分号 • 作为空语句存在的分号 • 以 [、(、+、-、和 /
五个字符开头的语句之前的分号
资料参考:备胎的自我修养——趣谈 JavaScript 中的 ASI (Automatic Semicolon Insertion)[22]
引用链接
[1]
小蝌蚪日记:通过 console.log 高仿 FBI Warning: https://segmentfault.com/a/1190000022866520
[2]
Using the F12 Tools Console to View Errors and Status: https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/samples/gg589530(v=vs.85)?redirectedfrom=MSDN
[3]
console.log 也能插图!!!: https://xie.infoq.cn/article/a8b8c170ca942b2a995d1e8e1
[4]
zhuangbility,一个可以逆向操作,输入文字,返回操作符的 npm 插件: https://www.npmjs.com/package/zhuangbility
[5]
[译] 在 JS 中,如何让 (a===1 && a===2 && a === 3)(严格相等) 的值为 true?: https://juejin.cn/post/6844903725442531341
[6]
深入浅出 Object.defineProperty(): https://www.jianshu.com/p/8fe1382ba135
[7]
ECMAScript7 规范中的 ToPrimitive 抽象操作: https://segmentfault.com/a/1190000016325587
[8]
Web Components 入门实例教程 - 阮一峰: http://www.ruanyifeng.com/blog/2019/08/web_components.html
[9]
Window.customElements: https://developer.mozilla.org/zh-CN/docs/Web/API/Window/customElements
[10]
Web Components: https://developer.mozilla.org/zh-CN/docs/Web/Web_Components
[11]
Windows 使用 cmd 命令行查看、修改、删除与添加环境变量: https://www.cnblogs.com/springsnow/p/12610417.html
[12]
谈谈 JavaScript 中装箱和拆箱: https://juejin.im/post/6844903859765133320
[13]
typeof: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/typeof
[14]
The history of “typeof null”: https://2ality.com/2013/10/typeof-null.html
[15]
这篇: https://king-hcj.github.io/2020/11/12/elasticsearch/#elasticsearch%E5%88%97%E8%A1%A8%E6%90%9C%E7%B4%A2%E5%85%AC%E5%85%B1%E6%96%B9%E6%B3%95%E5%B0%81%E8%A3%85
[16]
React Native Text-To-Speech library for Android and iOS: https://github.com/ak1394/react-native-tts
[17]
annyang: https://github.com/TalAter/annyang
[18]
JavaScript 位运算符: https://www.w3school.com.cn/js/js_bitwise.asp
[19]
不借助第三个变量交换 a,b 两个变量值: https://blog.csdn.net/web_hwg/article/details/75045689
[20]
BRAFT EDITOR 富文本编辑器预览: https://braft.margox.cn/demos/preview
[21]
String.prototype.localeCompare(): https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare
[22]
备胎的自我修养——趣谈 JavaScript 中的 ASI (Automatic Semicolon Insertion): https://segmentfault.com/a/1190000002955405
[input[type=number]下能输入e、+、-的解决方案_王一诺Eno的博客-CSDN博客_input type=number 可以输入e
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
在我们开发过程中,难免遇到这样的需求,需要一个输入框,并且只能输入数字,不能输入其他任何字符,这时候各种正则校验替换真的是苦不堪言~ 有时候弄得焦头烂额还要被嘲讽 le se … 接下来给大家介绍个神奇的解决方案,走过路过的朋友,有钱的捧个钱场,没钱的捧个人场,点个赞什么的我最喜欢啦;废话说多了,下边请看:
输入框 input[type=number]
首先为何输入框 input[type=number]能输入 e 呢? 究竟这个 e 是何方神圣,查阅资料得知,原来 e = 2.71828…
解决:
在 inpu原生事件中把非数字的按键过滤掉,具体操作如下:
onKeypress="return (/[\d]/.test(String.fromCharCode(event.keyCode)))"
<input v-model="goPage"
onKeypress="return (/[\d]/.test(String.fromCharCode(event.keyCode)))"
:max="9999999" type="number" placeholder="请输入"></input>
惊不惊喜?意不意外?是不是解决啦 ^ _ ^
作者就是喜欢买一送一!
输入框有自带的上下箭头,即 input[type=number]实际上为步距输入框也就是计数器… 会有自带的样式,顺带下面附上解决方案…
CSS 去默认样式
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
}
input[type="number"]{
-moz-appearance: textfield;
}
// vue的scope下
/deep/ input::-webkit-outer-spin-button,
/deep/ input::-webkit-inner-spin-button {
-webkit-appearance: none;
}
/deep/ input[type="number"]{
-moz-appearance: textfield;
}
总结:没有什么坑解决不了滴,如果对你有帮助,请点个关注或者喜欢喔
楼主博客安静 Eno
楼主github
https://blog.csdn.net/weixin_40755688/article/details/95794638
你可能不知道,前端这6个有用的技术可以这么酷!
为了让大家编程更轻松一些,本挑选一些有用的但相对比较少见有用的技巧。废话不多说,开车了。
1. 快速隐藏
要隐藏一个 DOM 元素,不需要 JavaScript。一个原生的 HTML 属性就足以隐藏。其效果类似于添加一个style display: none;
。
<p hidden> 该段落在页面上是不可见的,它对 HTML 是隐藏的。</p>
不过,这个技巧对伪元素不起作用。
2. 迅速定位
熟悉 inset` CSS 属性吗?它是 `top`、`left`、`right` 和
bottom的缩写版本。与简写的
margin和
padding`类似,我们可以在一行中设置一个元素的所有偏移量。
`// Before
div {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
// After
div {
position: absolute;
inset: 0;
}
`
3. 前端测网速
Chrome 浏览器提供了原始的 APInavigator.connection.downlink
可以访问用户当前网络环境的网络带宽。
navigator.connection.downlink;
connection.downlink
返回的并不是用户当前环境的展示的网络传输速度,而是当前网络的带宽,官方说法是:返回以Mb/s
为单位的有效带宽,并保留该值为25kb/s
的最接近的整数倍。
例如,我在我家里 Chrome 浏览器控制台跑一下 navigator.connection.downlink 这段语句,结果返回的是10
, 表示下载带宽是10M
的。
具体场景看这篇文章:https://www.zhangxinxu.com/wordpress/2021/04/navigator-connection-downlink/
5. 禁止拉动刷新
CSS overscroll-behavior
属性允许开发人员在达到内容的顶部 / 底部时覆盖浏览器的默认溢出滚动行为。使用该案例包括禁用移动设备上的 “拉动到刷新” 功能,消除过度滚动发光和橡皮筋效果,并防止页面内容在模态 / 叠加层下滚动
body {overscroll-behavior-y: contain;}
这个属性对于组织模态窗口内的滚动也非常有用 -- 它可以防止主页面在到达边界时拦截滚动。
具体场景参考这篇文章:https://www.zhangxinxu.com/wordpress/2020/01/css-overscroll-behavior/
6. 禁止插入文字
当用户在浏览器用户界面发起 “粘贴” 操作时,会触发 paste 事件。
有时间,我想禁止用户从某个地方复制的文本粘贴到输入框中。通过监听paste
事件并调用其方法preventDefault()
,这可以很容易做到。
`<input type="text">
<script> const input = document.querySelector('input'); input.addEventListener("paste", function(e){ e.preventDefault() }) </script>`
~ 完,我是小智,整理这篇文章时,正在生病中,准备去会所嫩模放松一下。
作者:Shadeed 译者:前端小智 来源:dev 原文:https://dev.to/ra1nbow1/6-useful-frontend-techniues-that-you-may-not-know-about-47hd
**推荐阅读:**
- 【干货】私藏的这些高级工具函数,你拥有几个?
- 25 个 Vue 技巧,开发了 5 年了,才知道还能这么用
- 史上最全 Vue 前端代码风格指南
- 终于有人把 Nginx 说清楚了,图文详解!
- 推荐 130 个令你眼前一亮的网站,总有一个用得着
- 深入浅出 33 道 Vue 99% 出镜率的面试题
VUE 中文社区
编程技巧 · 行业秘闻 · 技术动向
![](https://mmbiz.qpic.cn/mmbiz_jpg/1NOXMW586utOUfibtYibJdGGY4H4QciaQ8DaVztiaP4aUmmfOjK4xJbJ1DqOuI3P3IWYvFy7Mtiaib8gnHujDCquadAA/640?wx_fmt=jpeg)
打造vuecli3+element后台管理系统(五)几个小技巧,让你的后台系统在不同版本浏览器兼容性更好
打造 vuecli3+element 后台管理系统(五)几个小技巧,让你的后台系统在不同版本浏览器兼容性更好
当你的后台系统写好,给测试大大验收的时候。会发现他甩了一堆兼容性 bug 给你,在 ie 中打不开页面啦、在 360 浏览器火狐 ie 布局混乱啦、输入框怎么有难看的黄色背景啦、电话输入框怎么有丑陋的箭头啦、字体溢出了啦等等等等...
在开发时我们都习惯在 google 浏览器进行调试,总所周知,google 浏览器的对 css3、html5、es6 等的支持是完全没有问题的,所以我们会忽略了其实在其他浏览器对这些新特性不够支持的问题,下面介绍我在开发后台系统中使用的一些插件和一些小技巧,来让你的后台系统尽可能多的向下向外兼容多版本浏览器~
一、用 rem 代替 px
许多后台系统都要求要做成响应式的,虽然我们用的 elementUI 框架已经在响应式上面做了出色的处理,但是也只能解决一部分的问题。所以我们需要使用 rem。
1.1 什么是 rem
rem 是 CSS3 新增的相对长度单位,是指相对于根元素 html 的 font-size 计算值的大小。简单可理解为屏幕宽度的百分比。
但是!但是!问题来了,那就是我们其实用 px 开发习惯了,要改成 rem 一时半会缓不过来,加上还要换算是吧。所以用 rem 还挺烦的。接下来主角就登场了,安利大家几个插件,能够将你项目中的 px 转换成 rem,还可以自定义换算基数等。
1.2 使用 lib-flexible & px2rem 自动转换 px 为 rem,解决响应式问题
1.2-1 引入 lib-flexible 和 px2rem
npm install --save lib-flexible
npm install --save-dev px2rem-loader postcss-plugin-px2rem
复制代码
postcss-plugin-px2rem 是为了在使用 less 或者 sass 的情况下也可以正常转换
1.2-2 删除或注释 index.html 中的 <meta name="viewport" content="width=device-width,initial-scale=1.0">
标签
使用 lib-flexible 插件,他会自动生成 meta name="viewport" 的标签,所以我们需要把原来有的删除掉。自动生成标签之后,lib-flexible 会自动设置 html 的 font-size 为屏幕宽度除以 10,也就是 1rem 等于 html 根节点的 font-size,如果你的设计稿宽度是 750px,那 font-size 就会被设置为 75px
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- <meta name="viewport" content="width=device-width,initial-scale=1.0"> -->
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>vuecli3-ele-admin-template</title>
</head>
<body>
<noscript>
<strong>We're sorry but vuecli3-ele-admin-template doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
复制代码
1.2-3 入口文件引入 lib-flexible
在 main.js 全局引入 lib-flexible
// 使用lib-flexible来解决移动端适配
import 'lib-flexible'
复制代码
1.2-4 新增配置
在 vue.config.js 新增 px2rem 的配置
const path = require('path')
function resolve(dir) {
return path.join(__dirname, dir)
}
module.exports = {
publicPath: '/',
outputDir: 'dist', // 输出文件目录
assetsDir: 'assets', // 静态资源文件夹
productionSourceMap: false,
devServer: {
port: 9566, // 端口号
open: true,
proxy: null // 设置代理
},
// 新增内容
css: {
loaderOptions: {
sass: { // 如果用的是less就改成less
javascriptEnabled: true
},
postcss: {
plugins: [
require('postcss-plugin-px2rem')({
rootValue: 54, // 换算基数,默认100,自行根据效果调整。
mediaQuery: false, // (布尔值)允许在媒体查询中转换px。
minPixelValue: 3 // 设置要替换的最小像素值默认0,这里表示大于3px会被转rem。
})
]
}
}
},
// 新增结束
chainWebpack: config => {
// 新增内容
config.module
.rule('css')
.test(/\.css$/)
.oneOf('vue')
.resourceQuery(/\?vue/)
.use('px2rem')
.loader('px2rem-loader')
.options({
remUnit: 54
})
// 新增结束
config.module
.rule('svg')
.exclude.add(resolve('src/icons'))
.end()
config.module
.rule('icons')
.test(/\.svg$/)
.include.add(resolve('src/icons'))
.end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: 'icon-[name]'
})
}
}
复制代码
1.2-5 康康 postcss-plugin-px2rem 的配置项
官方文档给出的配置项有这么多:
{
rootValue: 100,
unitPrecision: 5,
propWhiteList: [],
propBlackList: [],
exclude:false,
selectorBlackList: [],
ignoreIdentifier: false,
replace: true,
mediaQuery: false,
minPixelValue: 0
}
复制代码
我们目前只用到了三个 那么这些都是啥意思呢,一起来康康
- rootValue 转换基数,类型可以是 Number 也可以是 Object,默认是 100
- 如果你传的是一个 Object,例如
{px: 50, rpx: 100}
,那么就意味着,在换算的时候,如果遇上单位是 px 那换算基数是 50,如果遇上 rpx 那么换算基数是 100
- 如果你传的是一个 Object,例如
- unitPrecision 单位精度,Number 类型,简单的说就是转换之后的 rem 要保留几位小数,默认保留 5 位的哈
- propWhiteList 转换的白名单,Array 类型,里面包含了可以被转换的 css 属性
- 默认是个空数组哦, 意思就是不用呗,就是说 css 里所有的属性,都可以进行转换
- 里面的属性值必须精确匹配,使用了白名单之后,意味着只有白名单里头的属性可以转换了。感觉这个白名单会比较少用到的说
- propWhiteList 转换的黑名单,和白名单相反嘛,黑名单里头的 css 属性是不会进行转换的
- exclude 排除的文件夹,是正则表达式。比如说写了
/(node_module)/
,就是说 (node_module) 中的样式文件不进行替换,这文件夹里能有啥,就是你引的插件嘛。排除这个文件夹的意思就是不对你引入的 UI 框架的样式进行单位转换。 - selectorBlackList 选择器黑名单,Array 类型。和上面那个属性黑名单大同小异,不同的是这里忽略转换的依据是 css 选择器。
- 如果是['body'],数组元素是单纯的字符串,意思就是排除 class 为 body 的转换,即忽略. body 下的所有属性的转换。
- 如果是[/^body$/]酱的,数组元素是一个正则表达式,他排除的就是 body 标签,即忽略 body 标签下的所有属性的转换。
- ignoreIdentifier 默认是 false,也可以是 String 类型,当是某个 css 属性名的时候,意思就是忽略这个属性的转换。如果你想忽略的属性只有那么一个,而不是一连串的时候,就可以用它。当启用这个的时候,replace 会自动变成 true 的哈
- replace 默认是 true,Boolean 类型,表示直接替换包含 REM 的规则,而不是添加回调函数
- mediaQuery Boolean 类型,默认是 false,表示是否允许在 @media 查询中进行转换
- minPixelValue Number 类型,表示开始转换的最小值,默认是 0,意思就是大于 0px 的长度都进行转换
细心的小伙伴发现我这里的 rootValue 转换基数设置的是 54,为什么涅?你运行项目,然后 F12,会发现根元素 html 的 font-size 是 54px。为什么!为什么明明前面说的是宽度除以 10 啊,我特喵的 pc 端宽度是 1080 啊,不应该是 font-size:108px 么???
想知道答案的小伙伴就要去看看伟大的 lib-flexible 的源码啦,lib-flexible 里头有这么一段代码:
function refreshRem(){
var width = docEl.getBoundingClientRect().width;
if (width / dpr > 540) {
width = 540 * dpr;
}
var rem = width / 10;
docEl.style.fontSize = rem + 'px';
flexible.rem = win.rem = rem;
}
复制代码
pc 端的 dpr 是 1,width / dpr 肯定是大于 540 的,所以 lib-flexible 会默认使用 540px 这个宽度,然后将屏幕宽度除以 10 作为 rem 值,所以 1rem = 54px。所以我们将 rootValue 转换基数设为 54 刚刚好
康康添加了 lib-flexible & px2rem 之后,页面在移动端的显示效果如何:
好的,十分优秀。
二、解决低版本 ie 打不开页面的问题
当你写完后台系统之后,毛闷台了喔,在线上把代码一拉一部署,测试那边说了,你这个页面我浏览器打不开啊!你过去之后发现他用的 ie 不知道 6 还是 7 还是 8 测得你的网站。那么问题来了,为什么会打不开呢?
原因就是你的项目里头用了 es6 的 promise,ie 低版本对这个的支持不是特别好,这个问题很好解决,只需要引入两个插件就可以了。对本身代码没有其他影响。
2.1 引入 es6-promise & 和 babel-polyfill 依赖包
npm install --save es6-promise babel-polyfill
复制代码
2.2 在入口文件 main.js 引入
// 解决低版本浏览器不支持promise问题
import 'babel-polyfill'
import Es6Promise from 'es6-promise'
Es6Promise.polyfill()
复制代码
2.3 在 vue.config.js 新增配置
// 。。。此处省略n个字符。。。
config.module
.rule('icons')
.test(/\.svg$/)
.include.add(resolve('src/icons'))
.end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: 'icon-[name]'
})
// 新增配置
config.entry.app = ['babel-polyfill', './src/main.js']
// 新增结束
}
}
复制代码
三、使用 autoprefixer 让 css 属性自动增加兼容前缀
很多时候,像 flexBox 或者 transform 这样样式,在不同浏览器下面有不同的写法,正常来说我们每次用到其中一个的时候都需要写这么长一大串:
<style lang="scss">
.flex-box {
display: -webkit-box;
display: -moz-box;
display: -webkit-flex;
display: -moz-flex;
display: -ms-flexbox;
display: flex;
-webkit-justify-content: center;
-webkit-box-pack: center;
-moz-justify-content: center;
-moz-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-align-items: center;
-webkit-box-align: center;
-moz-align-items: center;
-moz-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-direction: normal;
-webkit-box-orient: vertical;
-webkit-flex-direction: column;
-moz-flex-direction: column;
-moz-box-orient: vertical;
-ms-flex-direction: column;
flex-direction: column;
}
</style>
复制代码
晕,我只是想要用一下 flex 布局啊。。
经过评论区小伙伴的提醒,这里有一个很棒的插件可以使用,他可以自动给你的项目增加兼容前缀,需要添加的浏览器兼容前缀由你自由配置。我们只需:
3.1 安装 autoprefixer 依赖
cnpm install --save-dev autoprefixer
复制代码
3.2 在 vue.config.js 引入
// ...省略前面省略
css: {
loaderOptions: {
sass: { // 如果用的是less就改成less
javascriptEnabled: true
},
postcss: {
plugins: [
// 新增内容
require('autoprefixer')({}),
// 新增结束
require('postcss-plugin-px2rem')({
rootValue: 54, // 换算基数,默认100,自行根据效果调整。
mediaQuery: false, // (布尔值)允许在媒体查询中转换px。
minPixelValue: 3 // 设置要替换的最小像素值默认0,这里表示大于3px会被转rem。
})
]
}
}
},
// ...省略后面省略
复制代码
3.3 在 package.json 中指定 browserslist 关键字
在 package.json 中新增
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8",
"iOS >= 8",
"Firefox >= 20",
"Android >= 4.4"
]
复制代码
接下来就是见证奇迹的时刻,重新 npm run serve 一下,你就发现所有兼容样式前面都加上前缀啦~
关于之前的手写 mixin 的方法,我还是建议大家能够多封装,不管是业务代码上还是样式代码上,这样可以增加代码的复用率,让你的代码看起来更加轻盈。像一些使用率比较多的样式块,可以使用 mixin 封装起来,需要时 include 就行啦,也是十分方便的。
举个栗子
-
在 styles 的 mixin.scss 文件里声明一些常用的样式块
/* 背景自适应容器大小 */
@mixin bgCover($url) {
background-image: url($url);
background-repeat: no-repeat;
background-size: cover;
background-position: 0 center;
}@mixin noData($url) {
width: 100%;
font-size: 14px;
text-align: center;
color: #666;
line-height: 60px;
}
复制代码 -
在需要使用到 mixin 样式的页面引入,用 mixin 替换样式块
<style lang="scss"> @import '~@/styles/mixin'; .no-data { @include noData; } </style>复制代码
四、覆盖默认样式
很多标签都有一些奇奇怪怪的默认样式,在不同的浏览器下面默认样式还不一样,为了统一性。我们需要覆盖掉默认样式。其实这一块,elementUI 已经考虑到了,在 styles 目录下面的 index.scss 文件就是用来覆盖默认样式的。有需要覆盖掉的默认样式,可以在里面已有代码的基础上再新增。分享两个典型的:
4.1 覆盖掉 input type=number 时的箭头
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
}
input[type="number"] {
-moz-appearance: textfield;
}
复制代码
4.2 改变 Placeholder 文字的颜色
input::-moz-placeholder{color:rgb(204, 204, 204)} //Firefox
input::-webkit-input-placeholder{color:rgb(204, 204, 204)} //Chrome,Safari
input:-ms-input-placeholder{color:rgb(204, 204, 204)} // ie
textarea::-moz-placeholder{color:rgb(204, 204, 204)} //Firefox
textarea::-webkit-input-placeholder{color:rgb(204, 204, 204)} //Chrome,Safari
textarea:-ms-input-placeholder{color:rgb(204, 204, 204)} // ie
复制代码
五、通过 meta 标签控制浏览器内核
国产浏览器大多是双内核,甚至更多,例如 360 浏览器、QQ 浏览器之类。这些浏览器一般会有一个 Chromium 内核(极速模式。Chromiu 就是 Chrome 使用的内核。);一个 IE 内核(IE 模式);有的甚至还有一个修改过的 IE 内核(兼容模式)。
我们创建的项目,默认有一个控制切换浏览器内核的 meta 标签<meta http-equiv="X-UA-Compatible" content="IE=edge">
运行网站的时候强制切换为该浏览器所拥有的最高版本 IE 内核,所以在 qq 浏览器或者 360 浏览器里头打开项目,会发现浏览器用的是 IE 模式或者兼容模式。
因为项目是用 chrome 调试的,所以在 Chromium 内核下拥有最优体验,我们需要用代码让浏览器能够改变一下模式。具体做法就是:
修改 public 目录下的 index.html 模板文件。
新增 meta 标签,告诉浏览器优先使用何种内核
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<!-- 新增内容 -->
<!-- 强制Chromium内核,作用于360浏览器、QQ浏览器等国产双核浏览器 -->
<meta name="renderer" content="webkit"/>
<!-- 强制Chromium内核,作用于其他双核浏览器 -->
<meta name="force-rendering" content="webkit"/>
<!-- 如果有安装 Google Chrome Frame 插件则强制为Chromium内核,否则强制本机支持的最高版本IE内核,作用于IE浏览器 -->
<meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1"/>
<!-- 新增结束 -->
<!-- <meta name="viewport" content="width=device-width,initial-scale=1.0"> -->
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>vuecli3-ele-admin-template</title>
</head>
<body>
<noscript>
<strong>We're sorry but vuecli3-ele-admin-template doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
复制代码
清一下浏览器缓存,重新运行项目,就会发现在 360 浏览器、QQ 浏览器下已经变成 Chromium 内核的极速模式了~ 美得很~
暂时就先想到这些啦,后面想到我再继续补充~~ 还有很多细节的东西没有详细写出来,我这里贴一下项目地址,有兴趣的可以看一看哦~
解锁更酷的「网上冲浪」姿势,近期上架的这 10 款 Chrome 扩展值得一试 - 少数派
距离我上次推荐 Chrome 新扩展已经过去了四个月时间,商店里也出现了一大波新选手。在本文中,我将介绍其中 10 款实力玩家,包括新标签页、剪藏工具、外语学习、时间追踪、下载增强等,希望你能找到自己心仪的扩展。
DashOne:用微件搭起高效信息中心
作为 Android 的特色亮点之一,桌面微件(Widget)可以帮助我们在不打开应用的情况下了解最新动向、完成各种操作,而 DashOne 则将其搬到了你的 Chrome 新标签页上。
DashOne 预置了天气、新闻、笔记、书签、快速启动及多种效率工具,允许你阅读 RSS 订阅文章、查看 Gmail 未读邮件、速览 GitHub 通知、处理 Todoist 任务,甚至玩一局打砖块或连连看,还支持自由排列和暗色模式。
你可以在 Chrome 网上应用店 获取 DashOne,免费用户仅可以添加 10 个微件,如果你想添加更多,需要订阅 3 美元/月的专业版。
几枝:高颜值的新标签页面,还能帮你涨知识
不论是为了提高自己的姿势水平,还是单纯想在聊天时彰显文采,多背几首诗词总是没错的。
而几枝这款扩展,就会在你每次打开新标签页时展示一句经典古诗词,配合层叠涌动的波浪或气泡背景,婉约如江南山水,让你的 Chrome 与众不同。
如果你想了解更多,只需点击页面中间的词句,就可以调用 Google、百度等引擎搜索相关背景知识,探索古韵之美。
你可以在 Chrome 网上应用店、Firefox Add-ons 和 GitHub 免费获取几枝。
Web Clipper:把好看的内容「剪辑」下来
正如其名,Web Clipper 的主要功能是将网页上的内容「剪辑」下来,并一键保存至数种云笔记服务。
与印象笔记剪藏等工具相比,Web Clipper 支持智能提取、手动框选及自由编辑选中区域,还允许你将网页转换为 Markdown 格式,满足多种需求。目前,Web Clipper 已支持 Bear、GitHub、Notion、OneNote、有道云笔记和语雀 6 种在线笔记服务,值得一试。
你可以在 Chrome 网上应用店 和 GitHub 免费获取 Web Clipper,Firefox 版本也正在开发中。
TenWords:「冲浪」背词两不误
我曾经介绍过不少划词翻译扩展,而 TenWords 可能是最特别的那个。
只需点击工具栏上的图标进入学习模式,你就可以通过鼠标轻点查询不认识的单词释义,并标注生词为「已掌握」或「不认识」,在 TenWords 的练习页面中温习。TenWords 支持定时提醒、进度管理、单词卡片及学习成就统计等,帮助你快速掌握一门新语言。
除了英语外,TenWords 还针对法语、德语、俄语等数种语言做了相应优化,适用范围更广。
你可以在 Chrome 网上应用店 免费获取 TenWords,由于网络原因,登录账户可能需要科学手段,敬请留意。
Dualsub:让 YouTube 显示多语言字幕
要想掌握一门新语言,观看外语原版视频是快速提升自己的好方法,为了兼顾初学者和进阶用户的需要,不少字幕组都会提供双语版本的字幕文件,便于对照学习。
其实,不少 YouTube 视频也会提供多种语言版本的字幕,但默认只能显示一种,为了解决这一问题,Dualsub 诞生了。它支持同时显示两种不同语言的字幕,还允许你将字幕以 ASS 文件的形式保存至本地,啃生肉更轻松。
你可以在 Chrome 网上应用店 和 Firefox Add-ons 免费获取 Dualsub。
Summary Box:用 AI 快速「消化」外语文章
在信息高度碎片化、呈爆炸式增长的今天,「一目十行」几乎成了快速汲取新知的必修课。不过,我们可以比较轻松地精炼母语文章的中心**,却很难高效阅读外语新闻。
比起在脑中一个个单词逐词翻译这种效率低下的外语阅读方式,Summary Box 可以利用 AI 技术帮助我们一键生成所选文章的要点梗概,还支持自动提取正文内容,是速览英语文章的好帮手。
你可以在 Chrome 网上应用店 免费获取 Summary Box。
Worklog Tracker:网页浏览也能轻松量化
时间量化管理是一项容易被忽视、却相当重要的提升效率法门,可以帮助我们厘清工作时长分配,更好地安排自己的一天。
Worklog Tracker 就是一款相当出色的时间记录扩展,它支持在本地创建并追踪工作动态,自动同步至 Toggl、Jira、Keen 和 Yandex.Tracker 等主流服务。你还可以利用它导入及导出不同时间追踪平台中的个人数据,在多种服务间自由切换。
你可以在 Chrome 网上应用店 和 Firefox Add-ons 免费获取 Worklog Tracker。
DownThemAll!:老牌下载工具现已支持 Chrome
DownThemAll! 是 Firefox 平台的老牌下载增强扩展,近期移植到了 Chrome 上。
与原生下载管理器相比,DownThemAll! 支持自动嗅探网页上的元素,并按照预设规则进行分类。你可以轻松筛选当前页面上所有的链接、图片、音频、视频、文件等,并一键下载至本地。此外,DownThemAll! 还允许你创建自定义规则,利用正则表达式过滤要下载的项目。
你可以在 Chrome 网上应用店 和 Firefox Add-ons 免费获取 DownThemAll!。
FasterChrome:巧为 Chrome 浏览「加速」
Chrome 之所以能够在短短数年时间内超越 IE,独霸浏览器市场,很大程度上是凭借其无出其右的响应速度,而 FasterChrome 这款扩展想让它变得更快。
FasterChrome 的原理十分简单,当你的鼠标指针停留在一个链接上超过 65 毫秒时,FasterChrome 就会假定你即将点击它,并在后台预先加载。由于正常人从移动鼠标到手指按下间隔 300 毫秒以上,两者之间的时间差就会造成网速更快的错觉。
你可以在 Chrome 网上应用店 免费获取 FasterChrome。
ClearURLs:自动净化追踪字段
点击广告或其它链接跳转至第三方网站时,你或许会注意到地址栏后缀着一串看似无意义的字符,其主要作用是标识你的身份,包括设备信息、用户资料等,便于追踪和定位。
如果你比较在意自己的隐私安全,或许会希望删去这些跟踪字段,而 ClearURLs 就能派上用场。它能够自动净化主流网站的追踪字段,还支持提交自定义模板,保护你的个人数据。
你可以在 Chrome 网上应用店 和 Firefox Add-ons 免费获取 ClearURLs。
以上就是本次 Chrome 扩展插件推荐的全部内容,哪些是你已经在用的?又有哪些你觉得不错的新扩展本文还没有覆盖到?欢迎在评论区留言分享,我们下期推荐再见~
关联阅读:标签页管理、隐私保护、划词搜索…… 这 10 款新扩展让你的 Chrome 焕然一新
> 想知道自己都错过了哪些「宝藏」扩展插件?来「无扩展,不 Chrome」专题一次补个够 🐱🏍
> 下载少数派 客户端、关注 少数派公众号 ,了解更多实用 Chrome 插件 🌐
> 特惠、好用的硬件产品,尽在 少数派 Pi Store 商店 🛒
『 Vue小Case 』- Vue Prop中的 null vs undefined - 掘金
前言:本文将引入两个Vue中比较特殊的使用场景,带领大家熟悉一下
null
和undefined
的区别,然后再分析一下Vue中是怎么对Props做校验的,最后给出大佬是怎么解释的。
一直以来,笔者在使用Vue时,习惯于在需要表示prop属性未定义时,使用undefined
,而不是null
。因为“undefined
才是没有值,null
是有值,但是值为空的对象(注意不是空对象{}
)”。
基于这一习惯,笔者规避掉了很多问题,对此也没有深究。
直到最近,参与项目的一些同学习惯于指定null
为初始值,我也没有强制性统一。根本原因是,我自己也觉得没什么,每个人都有自己的习惯,只要大的风格不出偏差就可以接受,毕竟项目紧嘛。
直到今天遇到了下面的场景,我才发现,原来这么做会有一些奇怪的小问题。
一、场景再现
1.1 场景一:prop的值是null
,会使用default的值吗?
阅读以下代码,你觉得有问题吗?
HTML:
<div id="app">
<list :items="null"></list>
</div>
JS:
Vue.component('list', {
template: '<div>{{ typeof items }} {{ items.join(',') }}</div>',
props: {
items: {
type: Array,
default() {
return [1, 2, 3]
},
}
}
})
new Vue({
el: '#app',
});
上述的代码执行之后有问题吗?停顿5s思考一下,1、2、3、4、5。
好,我们来看下示例代码,这段代码执行之后会报错(记得开启控制台查看错误,因为是JS执行错误)。因为items
最终的值是null
,而所以没法对null
执行join()
拼接。那为什么prop值为null
时,default
中指定的值没有生效呢?
如果你愿意,可以把null
换成undefined
,你会发现正如你期望的那样,default
生效了。
1.2 场景二:prop设置了required
,传null
可以吗?
既然prop的值为null
时,default
不会生效,那我们能否通过required
强制prop必填呢?
HTML保持场景一不变。JS做如下改动:
Vue.component('list', {
template: '<div>{{ typeof items }} {{ items.join(',') }}</div>',
props: {
items: {
type: Array,
required: true,
}
}
})
new Vue({
el: '#app',
});
同样停顿5s思考一下,1、2、3、4、5,好。我们看下 示例代码,这段代码执行时依然会报错。
Vue会给出警告,信息如下:
[Vue warn]: Invalid prop: type check failed for prop \"items\". Expected Array, got Null.
(found in component <list>)
从警告内容可以看出null
通过了required: true
的验证,但是没有通过类型校验,最后浏览器执行报错。如果这时候,你再次尝试将null
改成undefined
,你会发现依然行不通,会有类似的错误。
好了,至此,我们已经看完了我所想展示的示例。可以总结为以下两个疑问:
- 当值为
null
的时候,为什么default
中定义的值没有生效?(显然,当值为undefined的时候,默认值是会生效的) - 当值为
null
/undefined
的时候,为什么required: ture
的校验似乎通过了,但是类型校验反倒没有通过?(显然,required: false
的时候,null
和undefined
是都能通过类型校验的。这点文档中有提到。)
在详细解释为什么之前,我们先来熟悉下一个历史悠久的问题:“null
和undefined
的区别是什么?”
二、null和undefined的区别
在设计之初,JavaScript是这样区分的:null
是一个表示"无"的对象,转为数值时为0
;undefined
是一个表示"无"的原始值,转为数值时为NaN
。
Number(null) // 0
5 + null // 5
Number(undefined) // NaN
5 + undefined // NaN
不得不吐槽,真是坑呀
但后来被证明这并不可行。目前,null
和undefined
基本是同义的,只有一些细微的差别。
null
表示没有对象,即此处不应该有值。虽然其表示不应该有值,但它是有值的,值是一个空的对象(注意不是{}
)。用法如下:
- 作为函数的参数,表示该函数的参数不是对象。
- 作为对象原型链的终点。
Object.getPrototypeOf(Object.prototype)
// nul
undefined
表示缺少值,就是此处应该有一个值,但是还没有定义。
- 变量被声明了,但没有赋值时,就等于
undefined
。 - 调用函数时,应该提供的参数没有提供,该参数等于
undefined
。 - 对象没有赋值的属性,该属性的值为
undefined
。 - 函数没有返回值时,默认返回
undefined
。
var i;
i // undefined
function f(x){console.log(x)}
f() // undefined
var obj = new Object();
obj.p // undefined
var ret = f();
ret // undefined
三、Vue中的Prop校验
好了,我们已经再次熟悉了一下null
和undefined
的区别。下面就让我们一起看下,Vue中是如何进行Prop校验以及如何对待null
和undefined
。
笔者阅读了Vue中关于Prop校验的代码,总结出了如下图所示的校验流程:
从图中第三步可以看出,只有prop的值为undefined
时,才会去获取default
中的值。这解释了第一部分两个奇怪现象的第一个。
代码片段如下:
// check default value
if (value === undefined) {
value = getPropDefaultValue(vm, prop, key)
// since the default value is a fresh copy,
// make sure to observe it.
const prevShouldObserve = shouldObserve
toggleObserving(true)
observe(value)
toggleObserving(prevShouldObserve)
}
从图中第四步可以看出,当required: true
时,只有不传属性时,才会提示'Missing required prop
。当required:false
时,null
和undefined
都会通过校验。其他情况,则都会进行类型校验。这解释了第一部分两个奇怪现象的第二个。
代码片段如下:
// required为true,且不传属性
if (prop.required && absent) {
warn(
'Missing required prop: "' + name + '"',
vm
)
return
}
// required为false,null和undefined校验通过
if (value == null && !prop.required) {
return
}
// 其他情况校验type
let type = prop.type
// ...
if (type) {
// type校验逻辑...
}
好了,我们通过Vue源码中的逻辑解释了为什么会出现第一部分中的奇怪现象。但可能还是没有理解为什么。下面我们继续来看一下大佬们的解释。
四、大佬们的解释
引用一下尤雨溪在issue#6768中的回答,部分内容及翻译如下:
null
indicates the value is explicitly marked as not present and it should remain null.
null
表示显式地标记值为未指定(是个空值),所以我们保留null
。(这里解释了为什么不对null
应用default
)
undefined
indicates the value is not present and a default value should be used if available.
undefined
表示值没有指定,如果有默认值,则使用默认值。
required: true
indicates neithernull
orundefined
are allowed (unless a default is used)
required: true
表示在default没有应用的情况下,null
和undefined
都不允许。注意:还有一个小前提,就是给属性指定了类型
(此外,这句话隐式包含了required和default是可以共存的,先应用default,再判断
required: true
)
在这一评论中,尤雨溪还解释了为什么要这么设计。虽然这样存在歧义,但是这和null
与undefined
在语言本身中的含义是统一的,如果改变的会造成更多的混乱。
五、总结
比较赞同尤雨溪的解释,虽然JavaScript语言本身存在一定的设计缺陷,但我们对这些缺陷表示知道,并不轻易hack,不然就会出现更多的混淆。与语言本身保持统一是一种更好的方式。
最后,虽然本文标题是《Vue Prop中的 null vs undefined》,但其中频繁涉及到了required和default这两个概念。所以想借此机会和大家一起明确一下Vue中这两个值的具体含义。
required: true
表示要求传入该属性,即template中要有该属性。只要有,不管值是什么,都可校验通过(没通过是类型校验的事情)。- default会在value值为
undefined
时生效,不管是因为没有传入属性还是属性的值就是undefined
。所以当你希望触发默认值的时候,一定要使用undefined
。
参考链接
前端使用puppeteer 爬虫生成《React.js 小书》PDF并合并 - 知乎
写于2018年08月29日
现在同步发布在知乎
1、puppeteer
是什么?
puppeteer
: Google
官方出品的 headless
Chrome
node
库
[puppeteer github仓库](https://zhuanlan.zhihu.com/p/89892479/%3C/code%3Eh%3Ccode%3Ettps:/%3C/code%3E/github.com/GoogleChrome/puppeteer)
[puppeteer API](https://zhuanlan.zhihu.com/p/89892479/%3C/code%3Eh%3Ccode%3Ettp%3C/code%3Es://pptr.dev/)
官方介绍:
您可以在浏览器中手动执行的大多数操作都可以使用
Puppeteer
完成!
生成页面的屏幕截图和
抓取SPA
并生成预渲染内容(即“SSR
”)。
自动化表单提交,UI
测试,键盘输入等。
创建最新的自动化测试环境。使用最新的JavaScript
和浏览器功能直接在最新版本的Chrome
中运行测试。
捕获时间线跟踪 您的网站,以帮助诊断性能问题。
测试Chrome
扩展程序。
2、爬取网站生成PDF
2.1 安装 puppeteer
# 安装 puppeteer
# 可能会因为网络原因安装失败,可使用淘宝镜像
# npm install -g cnpm --registry=https://registry.npm.taobao.org
npm i puppeteer
# or "yarn add puppeteer"
2.2 《React.js
小书》简介
a href="http://huziketang.mangojuice.top/books/react/">《React.js小书》简介
关于作者@胡子大哈
这是⼀本关于 React.js 的⼩书。 因为⼯作中⼀直在使⽤React.js
,也⼀直以来想总结⼀下⾃⼰关于React.js
的⼀些 知识、经验。于是把⼀些想法慢慢整理书写下来,做成⼀本开源、免费、专业、简单 的⼊⻔级别的⼩书,提供给社区。希望能够帮助到更多React.js
刚⼊⻔朋友。
下图是《React.js
小书》部分截图:
2.3 一些可能会用到的 puppeteer API
// 新建 reactMiniBook.js, 运行 node reactMiniBook.js 生成pdf
const puppeteer = require('puppeteer');
(async () => {
// 启动浏览器
const browser = await puppeteer.launch({
// 无界面 默认为true,改成false,则可以看到浏览器操作,目前生成pdf只支持无界面的操作。
// headless: false,
// 开启开发者调试模式,默认false, 也就是平时F12打开的面版
// devtools: true,
});
// 打开一个标签页
const page = await browser.newPage();
// 跳转到页面 http://huziketang.mangojuice.top/books/react/
await page.goto('http://huziketang.com/books/react/', {waitUntil: 'networkidle2'});
// path 路径, format 生成pdf页面格式
await page.pdf({path: 'react.pdf', format: 'A4'});
// 关闭浏览器
await browser.close();
})();
知道这启动浏览器打开页面关闭浏览器主流程后,再来看几个API
。
const args = 1;
let wh = await page.evaluate((args) => {
// args 可以这样传递给这个函数。
// 类似于 setTimeout(() => {console.log(args);}, 3000, args);
console.log('args', args); // 1
// 这里可以运行 dom操作等js
// 返回通过dom操作等获取到的数据
return {
width: 1920,
height: document.body.clientHeight,
};
}, args);
// 设置视图大小
await page.setViewport(wh);
// 等待2s
await page.waitFor(2000);
// 以iPhone X执行。
const devices = require('puppeteer/DeviceDescriptors');
const iPhone = devices['iPhone X'];
await page.emulate(iPhone);
2.4 知道了以上这些API
后,就可以开始写主程序了。
简单说下:实现功能和主流程。从上面React.js小书
截图来看。
1、打开浏览器,进入目录页,生成0. React 小书 目录.pdf
2、跳转到1. React.js 简介
页面,获取左侧所有的导航a
链接的href
,标题。
3、用获取到的a链接数组
进行for
循环,这个循环里主要做了如下几件事:
3.1 隐藏左侧导航,便于生成
3.2 给**React.js简介**
等标题 加上序号,便于查看
3.3 设置docment.title
加上序号, 便于在页眉中使用。
3.4 隐藏 传播一下知识也是一个很好的选择 这一个模块(因为页眉页脚中设置了书的链接等信息,就隐藏这个了)
3.5 给 分页 上一节,下一节加上序号,便于查看。
3.6 最末尾声明下该
3.7 返回宽高,用于设置视图大小
3.8 设置视图大小,创建生成
4、关闭浏览器
具体代码:可以查看这里爬虫生成《React.js小书》的pdf每一小节的代码
// node 执行这个文件
// 笔者这里是:
node src/puppeteer/reactMiniBook.js
即可生成如下图:每一小节(0-46小节)的pdf
生成这些后,那么问题来了,就是查看时总不能看一小节,打开一小节来看,这样很不方便。
于是接下来就是合并这些pdf
成为一个pdf
文件。
3、合并成一个PDF文件 pdf-merge
起初,我是使用在线网站Smallpdf,合并PDF
。合并的效果还是很不错的。这网站还是其他功能。比如word
转pdf
等。
后来找到社区提供的一个npm
package
pdf merge。 (毕竟笔者是写程序的,所以就用代码来实现合并了)
这个pdf-merge
依赖 pdftk
安装 PDFtk
Windows
下载并安装
笔者安装后,重启电脑才能使用。
Debian, Ubuntu 安装
笔者在Ubuntu系统安装后,即可使用。
apt-get install pdftk
使用例子
const PDFMerge = require('pdf-merge');
const files = [
`${__dirname}/1.pdf`,
`${__dirname}/2.pdf`,
];
// Buffer (Default)
PDFMerge(files)
.then((buffer) => {...});
// Stream
PDFMerge(files, {output: 'Stream'})
.then((stream) => {...});
// 笔者这里使用的是这个
// Save as new file
PDFMerge(files, {output: `${__dirname}/3.pdf`})
.then((buffer) => {...});
知道这些后,可以开始写主程序了。
简单说下主流程
1、读取到生成的所有pdf
文件路径,并排序(0-46)
2、判断下输出文件夹是否存在,不存在则创建
3、合并这些小节的pdf
保存到新文件 React小书(完整版)-作者:胡子大哈-时间戳.pdf
具体代码:可以查看这里爬虫生成《React.js小书》的pdf合并pdf的代码
**最终合并的pdf
文件可供下载。**百度网盘链接: https://pan.baidu.com/s/107WNpJqHbBPxAGsXS51u5A 提取码: m4nd。github下载链接:React小书(完整版)-作者:胡子大哈。
本想着还可以加下书签和页码,没找到合适的生成方案,那暂时先不加了。如果读者有好的方案,欢迎与笔者交流。
小结
1、puppeteer
是Google
官方出品的 headless
Chrome
node
库,可以在浏览器中手动执行的大多数操作都可以使用Puppeteer
完成。总之可以用来做很多有趣的事情。
2、用 puppeteer
生成每一小节的pdf
,用依赖pdftk
的pdf-merge
npm
包, 合并成一个新的pdf
文件。或者使用Smallpdf等网站合并。
3、a href="http://huziketang.mangojuice.top/books/react/">《React.js小书》,推荐给大家。爬虫生成pdf
,应该不会对作者@胡子大哈有什么影响。作者写书服务社区不易,尽可能多支持作者。
最后推荐几个链接,方便大家学习 puppeteer
。
puppeteer入门教程
Puppeteer 初探之前端自动化测试
爬虫生成ES6标准入门 pdf
大前端神器安利之 Puppeteer
puppeteer API中文文档
关于
作者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。
个人博客
掘金专栏,欢迎关注~
[segmentfault前端视野专栏](https://zhuanlan.zhihu.com/p/89892479/htt%3C/code%3Eps://segmentfault.com/blog/lxchuan12),开通了**前端视野**专栏,欢迎关注~
[知乎前端视野专栏](https://zhuanlan.zhihu.com/lxchuan12),开通了**前端视野**专栏,欢迎关注~
[github blog](https://link.zhihu.com/?target=https%3A//github.com/lxchuan12/blog),相关源码和资源都放在这里,求个star^_^~
微信公众号 若川视野
可能比较有趣的微信公众号,长按扫码关注。也可以加微信 lxchuan12
,注明来源,拉您进【前端视野交流群】。
如何解决VSCode Vim中文输入法切换问题?-autohotkey
https://www.zhihu.com/question/303850876/answer/1181682863
vscodevim 做的真烂…… 折腾半天,给的方法都不行……
既然用的是 win 了,vscodeVim 不行 那么就换一种思路,用外部的程序改变他。
现在需要的是 在编辑模式下可能是中文,当退出编辑模式为 Normal 模式的时候,将输入法换成英文,这样能避免在 normal 模式下 按 hh jjkl 等命令时 中文输入法的候选框问题。
所以可以这样理解 ,按 esc 以后 , 将输入法切换成 英语输入法, 就能避免中文的输入框弹出来
win 下有个好用的 工具叫 autohotkey , 首先在官网下载 ahk ,
点击第一 个就行了,下载当前版本,v2 好像是 语法变了……
这个安装包很小,就 3mb 大小
然后安装 我这里已经安装过了就不截图了……
接着创建一个记事本文件
编辑一下 输入以下代码
;限定在vscode 程序里面发挥作用
#IfWinActive ahk_exe Code.exe ;vscode 的exe 名字叫做Code.exe
Esc::
sendinput,{Esc}
PostMessage, 0x50, 0, 0x4090409, , A ;切换为英文0x4090409=67699721
return
#If
保存,接着将 .txt 的后缀改成 .ahk
然后 双击运行他 ,后台就会有一个 程序 图标出现
然后 你在 ,vscode 里面 将输入法 切换成中文后按 esc 键 ,他就会自动帮你把输入法切换成英文输入法了!
备注: 我的键盘设置是这样的,理论上这样 就不会有问题……
如果 你想 把他切换成 别的键盘,只要 把
PostMessage, 0x50, 0, 0x4090409, , A ;切换为英文0x4090409
这个 改成 对应的代码 就行了, 具体 的代码 从哪里来我忘记了…… 好像是注册表的 键盘码……
同理,如果你不想按 esc 键 ,想按别的键 比如 ctrl+[ 键 切换 输入法 为 英语 ,只要 把 esc 改成 ^[ 就可以 ,这个 ahk 有帮助文档 ,里面有 写,我就不在这里贴了
最后 在说一句 vsvim 真不好用!但是 win 下 gvim 我也不会配…… 现在用的是 ideavim ……
https://www.zhihu.com/question/303850876/answer/1181682863
基于elementUI改造成自己的UI库 - 掘金
规矩先介绍一下本文内容,由于一些项目对于UI控件的定制化要求比较高,但是又不想全部ui自己完全实现,我就想到了这个方法,将element修改一下变成自己的UI控件库,可以通过修改elementui的源码并发布到自己的github上,这样在vue项目中使用的时候跟其他依赖包一样下载下来就好了,使用方法跟elementui完全一致。
这种方法也是存在弊端的,例如element版本升级了或者是修复了bug,你这边如果还想同步代码,那么你就需要根据官网修改的记录去修改一遍你项目里相应的代码。
运行环境:
vue: 2.6.11
vue-cli:2.9.6 (使用这个脚手架构建的vue项目)
element-ui:2.13.0(基于这个版本修改成自己ui库)
一、新建构建一个vue项目
这个简单,我就简单罗列一下主要过程
1、初始化一个vue项目
2、运行vue项目
vue run dev
项目能正常运行起来就行了
二、下载element-ui源码,并修改。
1、下载element-ui源码
下载成功后目录结构
目录介绍:
build:放置webpack的配置文件。
examples:放置element api的页面文档。
packages:放置element的组件(css样式放置在这个目录下theme-chalk下)。
src/directives:放置自定义指令。
src/locale:放置语言的配置文件。
src/mixins:放置组件用的混合文件。
src/transitions:放置动画配置文件。
src/utils:放置用到工具函数文件。
src/index.js:组件注册的入口文件。
test:测试文件。
types:这个文件里放了typescript的数据类。
2、运行element-ui项目
npm install //安装依赖
npm run dev:play //项目运行在8085端口
其实运行的就是examples/play/index.vue的页面,这样就可以在我们更改源码的时候方便我们进行调试。
3、开始修改项目名称
把所有的element-ui替换成chen-ui
替换过后再次运行项目发现运行不起来了,很正常,意料之中。
咱们先看一下是什么问题
(此处省略一张图片,其实是我忘记截图了)
大概的意思就是找不到某些组件,但是不要慌,你把项目关掉重新打开,在运行 一般就好了
4、运行npm run dist
这一步主要是生成lib目录,这个目录里的代码才是其他项目使用时实际引用的代码
之后我们上传到自己的git上
ao···
对了 不要忘了把 .gitignore文件中的lib去掉,这样才能把lib目录下的文件提交上去
5、项目中引用
1.首先在package.json文件中加入
"chen-ui": "git+https://github.com/chenmeng012/chen-ui.git",
前面是chen-ui项目名,后面是项目所在git地址。
2.在main.js中全局引入chen-ui,按需引入的方式和element-ui的用法一致。我只里采用全局引入的方式
import Vue from 'vue';
import App from './App';
import router from './router';
import ChenUI from 'chen-ui';
import 'chen-ui/lib/theme-chalk/index.css';
Vue.config.productionTip = false;
Vue.use(ChenUI);
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})
复制代码
3.在组件中使用chen-ui
修改helloWorld.vue文件
<template>
<div class="hello">
<el-button @click="dialogVisible = true">显示DIALOG</el-button>
<el-dialog
title="提示"
:visible.sync="dialogVisible"
width="30%"
:before-close="handleClose">
<span>这是一段信息</span>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="dialogVisible = false">确 定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data () {
return {
dialogVisible: false,
msg: 'Welcome to Your Vue.js App'
}
},
methods: {
handleClose(done) {
this.$confirm('确认关闭?')
.then(_ => {
done();
})
.catch(_ => {});
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped></style>
复制代码
运行项目,看一下效果
好了,项目正常运行,并且chen-ui能够正常使用。
6.配置自动更新chen-ui命令
首先在vue-demo项目的build 目录下新建update-chen-ui.js、update-chen-ui.bat、update-chen-ui.sh三个文件
注:这三个文件可以不用新建,可以手动删除chen-ui之后再cnpm i chen-ui就好了,但是如果你们项目使用了jenkins打板机,这样配置的话会很方便,当然还得配置pom.xml文件等,这里就不细讲了。
const os = require("os");
let exec = require('child_process').exec;
function isWindows() {
return os.platform() === 'win32';
}
let cmd = null;
if (isWindows()) {
cmd = "cd build && update-chen-ui.bat";
} else {
cmd = "./build/update-chen-ui.sh";
}
exec(cmd, (err, stdout) => {
if (err) {
console.error(err);
}
if (stdout) {
console.log(stdout);
}
});
复制代码
windows系统
@echo off
chcp 65001
cd ../node_modules
rd /s /q [email protected]@chen-ui
if %errorlevel%==0 (
echo 清除[email protected]@chen-ui成功!
) else (
echo 清除[email protected]@chen-ui失败!
)
rd /s /q chen-ui
if %errorlevel%==0 (
echo 清除chen-ui成功!
) else (
echo 清除chen-ui失败!
)
cd ..
cnpm i chen-ui
if %errorlevel%==0 (
echo 安装chen-ui成功!
) else (
echo 安装chen-ui失败!
)
复制代码
注:bat这个文件不要用webstorm编辑会运行不了(出现这个问题可能是我的webstorm有问题,具体还没找到原因),要用记事本或者是notepad++进行编译。
Linux系统
#!/usr/bin/env bash
echo "清理旧的chen-ui安装..."
cd ./node_modules
rm -rf [email protected]@chen-ui
if [ $? -ne 0 ]; then
echo "删除 node_modules/[email protected]@chen-ui 失败"
fi
rm -rf chen-ui
if [ $? -ne 0 ]; then
echo "删除 node_modules/chen-ui 失败"
fi
echo "更新chen-ui..."
cd ..
cnpm i chen-ui
if [ $? -ne 0 ]; then
echo "执行chen-ui失败"
else
echo "更新chen-ui OK"
fi
复制代码
之后在package.json中加入一个scripts命令
update:ui": "node ./build/update-ctx-ui.js",
那这样就可以执行npm命令就可以自动更新chen-ui了
7.接下来修改chen-ui代码,达到定制ui控件的目的。
再来看一下chen-ui的目录
其实咱们主要修改的是package下的代码
为了更好的调试代码,咱们先将chen-ui这个项目运行起来
npm run dev:play
文章上面介绍过,项目运行的主页面是在examples/play/index.vue文件,打开这个文件,发现他是默认引入了一个input组件。咱们先引入一个select组件。
页面也能正常运行
接下来我要实现一个功能,我的项目中用到的一个功能。这个功能是在select下拉选项的最下面增加一个按钮用于做一些操作。
咱们先找到select组件,package/select
要实现的功能具体修改代码在package/select/select.vue文件
先来在template增加html部分
我用截图吧,上下文代码太多,不好表现,修改的代码我都用选中来表示
接下来是computed
接下来是props
最后是methods
vue文件也就修改完了。
接下来添加样式
样式文件都在packages/theme-chalk/src下
咱们找到select-dropdown.scss文件,添加样式
样式文件也就修改完了。
之后在 examples/play/index.vue使用一下
运行一下项目,看一下效果
可以 可以,可以运行,并且功能也就实现了。
8.更新vue-demo中的chen-ui
在chen-ui项目中执行 npm run dist
用来更行lib文件,也就是其他项目引用的真正的代码。
在更新之前,别忘了先把chen-ui的代码提交到git上!!
在vue-demo项目中执行npm run update:ui
更新完成后咱们试一下在项目中是否可以直接使用
修改一下HelloWorld.vue文件
<template>
<div class="hello">
<el-select v-model="value" placeholder="请选择"
:bottom-button="{title:'新建',visible:true,closeAfterClick:false}"
@bottom-button-click="handleClick">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data () {
return {
options: [{
value: '选项1',
label: '黄金糕'
}, {
value: '选项2',
label: '双皮奶'
}, {
value: '选项3',
label: '蚵仔煎'
}, {
value: '选项4',
label: '龙须面'
}, {
value: '选项5',
label: '北京烤鸭'
}],
value: ''
}
},
methods: {
handleClick() {
this.$message('点击了新增');
}
}
}
</script>
<style scoped></style>
复制代码
看一下页面效果
也是好使的
三、赠送的内容
再追加点内容吧!!!
咱们看到element-ui中有这样的一个组件scrollbar,也就是滚动条,这个在element-ui中没有公布出去的一个组件,正常情况下咱们是不能使用的。现在就把它公布出去,让其他项目可以引用这个组件。
1.在types目录下新建一个scrollbar.d.ts文件,文件内容如下
import {ElementUIComponent} from "./component";
export declare class ElScrollbar extends ElementUIComponent{}
复制代码
2.将原来的element-ui.d.ts改名为chen-ui.d.ts,之后修改代码
之后提交,再在vue-demo中更新chen-ui
修改一下HelloWorld.vue文件
<template>
<div class="hello">
<el-scrollbar style="height: 200px">
<div v-for="item in scrollArr">
{{item.title}}
</div>
</el-scrollbar>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data () {
return {}
},
computed: {
scrollArr() {
let arr = [];
for(let i = 0; i < 50; i++) {
arr.push({title: `项目${i}`})
}
return arr
}
},
methods: {}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped></style>
复制代码
现在在项目中也是可以使用scrollbar了
但是有一个小bug在上面这张图的下面红框中有一个横向滚动条的框,这个是scrollbar样式有点问题,他是在package/theme-chalk/src/scrollbar
在chen-ui项目中别忘记npm run dist!
其实scrollbar组件还可以传入参数,我就不介绍了,这个通过源码,实践一下就能知道参数的作用了。
这样再更新chen-ui就好了。我就不再截图了。
上面简单的介绍了两种个简单的例子,形目中可能还会有其他的需求,这就需要自己去看源码,根据自己的需求去修改源码。
我也只是提供一种思路,其实还可以将package整个文件夹复制到自己的项目中去使用,当成自己的组件使用,当然还得修改代码,并且添加一些配置,还是比较麻烦的,有时间我再专门写一篇关于此类问题的文章吧。
小白一枚,不喜勿喷。当然文中可能存在不足和错误,还请各位大佬指正,万分感谢!!
QuickTime Player录制只带系统声音的视频【适用M1】 - 简书
MACM1 用户方法
1 安装所需软件 - Blackhole
https://existential.audio/blackhole/
brew install blackhole-2ch
2 安装完成后打开 “midi” 音频设备,点击左下⻆➕ 号创建多输出设备
3 勾选 BlackHole 2ch
4 切换主设备 BlackHole 2ch
补充: 确认音频设备 - BlackHole 2ch「格式与主声道是否需要调整」
录屏前注意:
1 把声音输出选择 - 多输出设备
2 录屏选项的⻨克⻛也要修改 为 - BlackHole 2ch
总结:安装软件「软件自下也可私信」- 配置软件 - 开启录屏 - 选择录屏声音的所需项
版权所属转载请注明出处
QuickTime Player录制只带系统声音的视频【适用M1】 - 简书
mac m1上系统内录方法BlackHole代替soundflower录音(附安装包)_小手琴师的博客-CSDN博客_m1 mac 录音
如何用 QuickTime 对 Mac 屏幕录制,而且录上声音? - 知乎
https://www.jianshu.com/p/142038d267e0
保存网页时“丢三落四”?8k Star 的开源扩展,一键完美保存完整网页
保存网页时 “丢三落四”?8k Star 的开源扩展,一键完美保存完整网页
【导语】:用浏览器自带的网页另存功能时,经常出现丢失图片,而且还会保存一堆的关联文件。最近 GitHub 上有一个热门开源工具,可以完美解决这些问题。
简介
SingleFile 是一个浏览器扩展,以及 CLI 工具,可快速将完整的网页保存成单一 HTML 文件。
它兼容 Chrome、Firefox(桌面和移动端)、Edge、Vivaldi、Brave、Waterfox、Yandex 和 Opera 等主流浏览器。
项目地址:
https://github.com/gildas-lormeau/SingleFile
安装
SingleFile 可以安装在:
-
Firefox: https://addons.mozilla.org/firefox/addon/single-file
-
Firefox 移动端:https://blog.mozilla.org/addons/2020/09/29/expanded-extension-support-in-firefox-for-android-nightly/
-
Chrome: https://chrome.google.com/extensions/detail/mpiodijhokgodhhofbcjdecpffjipkle
-
Microsoft Edge: https://microsoftedge.microsoft.com/addons/detail/efnbkdcfmcmnhlkaijjjmhjjgladedno
-
也可以通过手动下载 zip 文件,解压到磁盘上并且按照以下说明手动安装:https://github.com/gildas-lormeau/SingleFile/archive/master.zip
-
Firefox: https://extensionworkshop.com/documentation/develop/temporary-installation-in-firefox/
-
Chrome: https://developer.chrome.com/extensions/getstarted#manifest
简单使用
等到页面完全加载后,单击扩展工具栏中的 SingleFile 按钮以保存页面,在处理页面时再次单击该按钮以取消该操作。
-
通过右键单击扩展工具栏或网页上的 SingleFile 按钮打开菜单,可以保存:
-
当前 tab 的内容
-
选中的内容
-
选中的 frame
-
也可以一键处理多个 tab 并保存:
-
选中的 tab
-
未固定的 tab
-
所有的 tab
-
在菜单中选择 "Annotate and save the page...":
-
可以高亮文本
-
添加注释
-
删除内容
-
如果启用自动保存,页面每次加载后都会自动保存页面
-
文件下载后保存路径是浏览器配置中的下载文件夹
SingleFile 的命令行界面
SingleFile 可以通过命令行启动,它通过 Node.js 作为注入网页的独立脚本运行。
使用 Docker 安装
- 从 Docker Hub 安装
docker pull capsulecode/singlefile docker tag capsulecode/singlefile singlefile
- 手动安装
git clone --depth 1 --recursive https://github.com/gildas-lormeau/SingleFile.git cd SingleFile/cli docker build --no-cache -t singlefile .
- 运行
docker run singlefile "[https://www.wikipedia.org](https://www.wikipedia.org/)"
- 运行并将结果重定向到文件中
docker run singlefile "https://www.wikipedia.org" > wikipedia.html
手动安装
-
确保已经安装了 Chrome 或 Firefox,并且可以通过 PATH 环境变量找到可执行文件
-
安装 Node.js
-
下载安装 SingleFile 有以下 3 种方法:
-
全局下载和安装
npm install -g "gildas-lormeau/SingleFile#master"
- 手动下载并解压
unzip master.zip . cd SingleFile-master npm install cd cli
- git 源码安装
git clone --depth 1 --recursive https://github.com/gildas-lormeau/SingleFile.git cd SingleFile npm install cd cli
运行
- 语法:
single-file <url> [output] [options ...]
- 查看帮助:
single-file --help
-
例子
-
保存页面内容到指定文件
single-file https://www.wikipedia.org wikipedia.html
- 保存 list-urls.txt 文件中的 url 列表
single-file --urls-file=list-urls.txt
与用户脚本集成
可以在 SingleFile 保存页面之前或之后执行用户脚本。
- 当 SignleFile 作为:
- 扩展使用时,从选项页面导出设置、编辑 JSON 文件、替换 userScriptEnabled: false 为 userScriptEnabled: true,并在 SingleFile 中导入修改后的文件来启用隐藏选项。
- CLI 工具使用时,使用选项 --browser-script 将脚本路径传递给 SingleFile。
- 在用户脚本中分发自定义事件:
dispatchEvent(new CustomEvent("single-file-user-script-init"));
- 在用户脚本中监听自定义事件 single-file-on-before-capture-request,这个监听函数会在页面保存前被调用:
addEventListener("single-file-on-before-capture-request", () => { console.log("The page will be saved by SingleFile"); });
- 在用户脚本中监听自定义事件 single-file-on-after-capture-request,这个监听函数会在页面保存后被调用:
addEventListener("single-file-on-after-capture-request", () => { console.log("The page has been processed by SingleFile"); });
- 例子,这个脚本会在保存页面之前从页面中删除图像,并在处理页面后恢复:
`(() => {
const elements = new Map();
const removedElementsSelector = "img";
dispatchEvent(new CustomEvent("single-file-user-script-init"));
addEventListener("single-file-on-before-capture-request", () => {
document.querySelectorAll(removedElementsSelector).forEach(element => {
const placeHolderElement = document.createElement(element.tagName);
elements.set(placeHolderElement, element);
element.parentElement.replaceChild(placeHolderElement, element);
});
});
addEventListener("single-file-on-after-capture-request", () => {
Array.from(elements).forEach(([placeHolderElement, element]) => {
placeHolderElement.parentElement.replaceChild(element, placeHolderElement);
});
elements.clear();
});
})();
`
- EOF -
推荐阅读 点击标题可跳转
2、看完微软大神写的求平均值代码,我意识到自己还是 too young 了
3、响应乌克兰 “制裁” 呼吁:Oracle 和 SAP 暂停俄罗斯业务,GitHub 限制俄罗斯获取相应技术
关注「程序员的那些事」加星标,不错过圈内事
点赞和在看就是最大的支持❤️
https://mp.weixin.qq.com/s/xA0vDiqmaHKKybUFd7MH_Q
这15个Vue自定义指令,让你的项目开发爽到爆
这15个Vue自定义指令,让你的项目开发爽到爆
原创 Kayson 1024译站 2019-10-11
受 AngularJS 的启发,Vue 内置了一些非常有用的指令(比如v-html 和 v-once等),每个指令都有自身的用途。完整的指令列表可以在这里查看.
这还没完,更棒的是可以开发自定义指令。Vue.js 社区因此得以通过发布自定义指令npm 包,解决了无数的代码问题。
以下就是我最喜欢的 Vue.js 自定义指令列表。不用说,这些指令为我的项目开发节省了大量时间!😇
- V-Hotkey
仓库地址: https://github.com/Dafrok/v-hotkey
Demo: 戳这里 https://dafrok.github.io/v-hotkey
安装: npm install --save v-hotkey
这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟:
<template>
<div
v-show="show"
v-hotkey="{
'esc': onClose,
'ctrl+enter': onShow
}"
>
Press `esc` to close me!
</div>
</template>
<script>
export default {
data() {
return {
show: true
}
},
methods: {
onClose() {
this.show = false
},
onShow() {
this.show = true
},
}
}
</script>
- V-Click-Outside
仓库地址: https://github.com/ndelvalle/v-click-outside
Demo: https://codesandbox.io/s/zx7mx8y1ol?module=%2Fsrc%2Fcomponents%2FHelloWorld.vue
安装: npm install --save v-click-outside
你想要点击外部区域关掉某个组件吗?用这个指令可以轻松实现。这是我每个项目必用的指令之一,尤其在弹框和下拉菜单组件里非常好用。
<template>
<div
v-show="show"
v-click-outside="onClickOutside"
>
Hide me when a click outside this element happens
</div>
</template>
<script>
export default {
data() {
return {
show: true
};
},
methods: {
onClickOutside() {
this.show = false;
}
}
};
</script>
说明: 你也可以通过双击外部区域来触发,具体用法请参考文档。
- V-Clipboard
仓库地址: https://github.com/euvl/v-clipboard
安装: npm install --save v-clipboard
这个简单指令的作者是Yev Vlasenko ,可以用在任何静态或动态元素上。当元素被点击时,指令的值会被复制到剪贴板上。用户需要复制代码片段的时候,这个非常有用。
<button v-clipboard="value">
Copy to clipboard
</button>
- Vue-ScrollTo
仓库地址: https://github.com/rigor789/vue-scrollTo
Demo: https://vue-scrollto.netlify.com/
安装: npm install --save vue-scrollto
这个指令监听元素的点击事件,然后滚动到指定位置。我通常用来处理文章目录跳转和导航跳转。
<span v-scroll-to="{
el: '#element', // 滚动的目标位置元素
container: '#container', // 可滚动的容器元素
duration: 500, // 滚动动效持续时长(毫秒)
easing: 'linear' // 动画曲线
}"
>
Scroll to #element by clicking here
</span>
说明: 也可以通过代码动态设置,具体看文档。
- Vue-Lazyload
仓库地址: https://github.com/hilongjw/vue-lazyload
Demo: http://hilongjw.github.io/vue-lazyload/
安装: npm install --save vue-lazyload
图片懒加载,非常方便。
<img v-lazy="https://www.domain.com/image.jpg">
- V-Tooltip
仓库地址: v-tooltip
Demo: available here
安装: npm install --save v-tooltip
几乎每个项目都会用到 tooltip。这个指令可以给元素添加响应式的tooltip,并可控制显示位置、触发方式和监听事件。
<button v-tooltip="'You have ' + count + ' new messages.'">
说明: 还有一个比较流行的tooltip插件vue-directive-tooltip.
- V-Scroll-Lock
仓库地址: https://github.com/phegman/v-scroll-lock
Demo: https://v-scroll-lock.peterhegman.com/
安装: npm install --save v-scroll-lock
基于 body-scroll-lock 开发,这个指令的作用是在打开模态浮层的时候防止下层的元素滚动。
<template>
<div class="modal" v-if="opened">
<button @click="onCloseModal">X</button>
<div class="modal-content" v-scroll-lock="opened">
<p>A bunch of scrollable modal content</p>
</div>
</div>
</template>
<script>
export default {
data () {
return {
opened: false
}
},
methods: {
onOpenModal () {
this.opened = true
},
onCloseModal () {
this.opened = false
}
}
}
</script>
- V-Money
仓库地址: https://github.com/vuejs-tips/v-money
Demo: https://vuejs-tips.github.io/v-money/
安装: npm install --save v-money
如果你需要在输入框里加上货币前缀或后缀、保留小数点位数或者设置小数点符号——不用找了,就是它!一行代码搞定这些需求:
<template>
<div>
<input v-model.lazy="price" v-money="money" /> {{price}}
</div>
</template>
<script>
export default {
data () {
return {
price: 123.45,
money: {
decimal: ',',
thousands: '.',
prefix: '$ ',
precision: 2,
}
}
}
}
</script>
- Vue-Infinite-Scroll
仓库地址: https://github.com/ElemeFE/vue-infinite-scroll
安装: npm install --save vue-infinite-scroll
无限滚动指令,当滚动到页面底部时会触发绑定的方法。
<template>
<!-- ... -->
<div
v-infinite-scroll="onLoadMore"
infinite-scroll-disabled="busy"
infinite-scroll-distance="10"
></div>
<template>
<script>
export default {
data() {
return {
data [],
busy: false,
count: 0
}
},
methods: {
onLoadMore() {
this.busy = true;
setTimeout(() => {
for (var i = 0, j = 10; i < j; i++) {
this.data.push({ name: this.count++ });
}
this.busy = false;
}, 1000);
}
}
}
</script>
- Vue-Clampy
仓库地址: vue-clampy.
安装: npm install --save @clampy-js/vue-clampy
这个指令会截断元素里的文本,并在末尾加上省略号。它是用clampy.js实现的。
<p v-clampy="3">Long text to clamp here</p>
<!-- displays: Long text to...-->
- Vue-InputMask
仓库地址: vue-inputmask
安装: npm install --save vue-inputmask
当你需要在输入框里格式化日期时,这个指令会自动生成格式化文本。基于Inputmask library 开发。
<input type="text" v-mask="'99/99/9999'" />
- Vue-Ripple-Directive
仓库地址: vue-ripple-directive
安装: npm install --save vue-ripple-directive
Aduardo Marcos 写的这个指令可以给点击的元素添加波纹动效。
<div v-ripple class="button is-primary">This is a button</div>
- Vue-Focus
仓库地址: vue-focus
安装: npm install --save vue-focus
有时候,用户在界面里操作,需要让某个输入框获得焦点。这个指令就是干这个的。
<template>
<button @click="focused = true">Focus the input</button>
<input type="text" v-focus="focused">
</template>
<script>
export default {
data: function() {
return {
focused: false,
};
},
};
</script>
- V-Blur
仓库地址: v-blur
Demo: 戳这里
安装: npm install --save v-blur
假设你的页面在访客没有注册的时候,有些部分需要加上半透明遮罩。用这个指令可以轻松实现,还可以自定义透明度和过渡效果。
<template>
<button
@click="blurConfig.isBlurred = !blurConfig.isBlurred"
>Toggle the content visibility</button>
<p v-blur="blurConfig">Blurred content</p>
</template>
<script>
export default {
data () {
return
blurConfig: {
isBlurred: false,
opacity: 0.3,
filter: 'blur(1.2px)',
transition: 'all .3s linear'
}
}
}
}
};
</script>
- Vue-Dummy
仓库地址: vue-dummy
Demo: available here
安装: npm install --save vue-dummy
开发 app 的时候,偶尔会需要使用假文本数据,或者特定尺寸的占位图片。用这个指令可以轻松实现。
<template>
<!-- the content inside will have 150 words -->
<p v-dummy="150"></p>
<!-- Display a placeholder image of 400x300-->
<img v-dummy="'400x300'" />
</template>
最后
欢迎补充更多好用的 Vue 自定义指令。
输入框input只能输入数字和小数点_赵敬是个程序媛的博客-CSDN博客_input只能输入数字和小数
只允许输入数字 (整数:小数点不能输入)
<input type="text" onkeyup="value=value.replace(/[^\d]/g,'')" >
允许输入小数 (两位小数)
<input type="text" onkeyup="value=value.replace(/^\D*(\d*(?:\.\d{0,2})?).*$/g, '$1')" >
允许输入小数 (一位小数)
<input type="text" onkeyup="value=value.replace(/^\D*(\d*(?:\.\d{0,1})?).*$/g, '$1')" >
开头不能为 0,且不能输入小数
<input type="text" onkeyup="value=value.replace(/[^\d]/g,'').replace(/^0{1,}/g,'')" >
只能输入数字或小数且第一位不能是 0 和点且只能有一个点
<input type="text" onkeyup="value=value.replace(/[^1-9]{0,1}(\d*(?:\.\d{0,2})?).*$/g, '$1')" >
https://blog.csdn.net/weixin_42171955/article/details/98734640
一份 ElementUI 问题清单
↓推荐关注↓
一份 ElementUI 问题清单
1、form 下面只有一个 input 时回车键刷新页面
原因是触发了表单默认的提交行为,给 el-form 加上 @submit.native.prevent 就行了。
<el-form inline @submit.native.prevent> <el-form-item label="订单号"> <el-input v-model="query.orderNo" :placeholder="输入订单号查询" clearable @keyup.enter.native="enterInput" /> </el-form-item></el-form>
2、表格固定列最后一行显示不全
这种情况有时在宽度刚好处于临界值状态时会出现。因为固定列是独立于表格 body 动态计算高度的,出现了固定列高度小于表格高度所以造成最后一行被遮挡。
// 设置全局.el-table__fixed-right { height: 100% !important;}
3、气泡确认框文档里的 confirm 事件不生效
版本:element-ui: "2.13.2", vue: "2.6.10"
// 将confirm改为onConfirm@onConfirm="onDeleteOrder(row.id)"
4、输入框用正则限制但绑定值未更新
看到项目里有下面这么一段代码:
<el-input v-model="form.retailMinOrder" placeholder="请输入" onkeyup="value=value.replace(/[^\d.]/g,'')" />
这样做虽然输入框的显示是正确的,但绑定的值是没有更新的,将 onkeyup 改为 oninput 即可。
-
PS:经评论区的兄弟指正,输入中文后 v-model 会失效,下面的方式更好一点:
<el-input v-model="form.retailMinOrder" placeholder="请输入" @keyup.native="form.retailMinOrder=form.retailMinOrder.replace(/[^\d.]/g,'')"/>
5、去除 type="number" 输入框聚焦时的上下箭头
/* 设置全局 */.clear-number-input.el-input::-webkit-outer-spin-button,.clear-number-input.el-input::-webkit-inner-spin-button { margin: 0; -webkit-appearance: none !important;} .clear-number-input.el-input input[type="number"]::-webkit-outer-spin-button,.clear-number-input.el-input input[type="number"]::-webkit-inner-spin-button { margin: 0; -webkit-appearance: none !important;}.clear-number-input.el-input { -moz-appearance: textfield;} .clear-number-input.el-input input[type="number"] { -moz-appearance: textfield;}
<el-input type="number" class="clear-number-input" />
6、只校验表单其中一个字段
在一些用户注册场景中,提交整个表单前有时候我们会做一些单独字段的校验,例如发送手机验证码,发送时我们只需要校验手机号码这个字段,可以这样做:
this.$refs['form'].validateField('mobile', valid => { if (valid) { // 发送验证码 }})
如果需要多个参数,将参数改为数组形式即可。
7、弹窗重新打开时表单上次的校验信息未清除
有人会在 open 时在 $nextTick 里重置表单,而我选择在关闭时进行重置。
<el-dialog @close="onClose"> <el-form ref="form"> </el-form></el-dialog>// 弹窗关闭时重置表单onClose() { this.$refs['form'].resetFields()}
8、表头与内容错位
网上也有其他一些办法,但我记得对我没什么作用,后来我是用下面这个办法:
// 全局设置.el-table--scrollable-y .el-table__body-wrapper { overflow-y: overlay !important;}
9、表单多级数据结构校验问题
<el-form :model="form"> <el-form-item label="部门" prop="dept"></el-form-item> <el-form-item label="姓名" prop="user.name"></el-form-item></el-form>
rules: { 'user.name': [{ required: true, message: '姓名不能为空', trigger: 'blur' }]}
10、表格跨分页多选
看到项目里有小伙伴手动添加代码去处理这个问题,其实根据文档,只需加上 row-key 和 reserve-selection 即可。
<el-table row-key="id"> <el-table-column type="selection" reserve-selection></el-table-column></el-table>
11、根据条件高亮行并去除默认 hover 颜色
<el-table :row-class-name="tableRowClassName"></el-table>tableRowClassName({ row }) { return row.status === 2 ? 'highlight' : ''}// 设置全局.el-table .highlight { background-color: #b6e8fe; &:hover > td { background-color: initial !important; } td { background-color: initial !important; }}
12、表单不想显示 label 但又想显示必填星号怎么办
// label给个空格即可<el-form> <el-table> <el-table-column label="名称"> <template> <el-form-item label=" "> <el-input placeholder="名称不能为空" /> </el-form-item> </template> </el-table-column> </el-table></el-form>
13、table 内嵌 input 调用 focus 方法无效
<el-table> <el-table-column label="名称"> <template> <el-input ref="inputRef" /> </template> </el-table-column></el-table>// 无效this.$refs['inputRef'].focus()this.$refs['inputRef'][0].focus()this.$refs['inputRef'].$el.children[0].focus()// 有效<el-input id="inputRef" />document.getElementById('inputRef').focus()
14、表格内容超出省略
看到有小伙伴在代码里自己手动去添加 CSS 来实现,害,又是一个不看文档的反面例子,其实只要加个 show-overflow-tooltip 就可以了,还自带 tooltip 效果,不香吗?
<el-table-column label="客户名称" prop="customerName" show-overflow-tooltip></el-table-column>
15、el-tree 展开 / 收起所有节点
<el-tree ref="tree"></el-tree>expandTree(expand = true) { const nodes = this.$refs['tree'].store._getAllNodes() nodes.forEach(node => { node.expanded = expand })}
16、哪天想起什么或遇到什么再更新。。。
作者:8 号的凌晨 4 点
- EOF -
推荐阅读 点击标题可跳转
1、2.3 万 Star 的国产装机神器,制作可启动 U 盘
2、TypeScript 高级类型入门手册:附大量代码实例(收藏!)
3、悄悄学习 Doris,偷偷惊艳所有人 :Apache Doris 四万字小总结
关注「程序员的那些事」加星标,不错过圈内事
程序员的那些事
日常分享程序员相关的精选文章和热点资讯;外加每天一张程序员减压的 IT 趣图,笑的有高度;还有难得一见的程序员相亲,一不小心就脱单了 :)
180 篇原创内容
公众号
点赞和在看就是最大的支持❤️
https://mp.weixin.qq.com/s/nMb2soSaSEovxji7zDIsfA
react不使用eject的配置方法(config-overrides复现vue项目全部配置)_桃饱の店-CSDN博客_config-overrides
react 不使用 eject 的配置方法(config-overrides 复现 vue 项目全部配置)_桃饱の店 - CSDN 博客_config-overrides
基础依赖
暴露全部配置 eject 十分不友好,我们基于 customize-cra 和 react-app-rewired 进行自定义配置:
yarn add -D customize-cra react-app-rewired
之后修改 package.json
的 scripts
部分:
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"_eject": "react-scripts eject"
},
注意前三个命令只是把原来的 react-scripts
替换为了 react-app-rewired
,最后 eject 为了安全我加了下划线(你也可以不修改最后一行 eject )。
下面就可以开始自定义配置了。
config-overrides.js
根目录下建立一个配置重写文件 config-overrides.js
,刚刚安装的依赖就会注入 react 内帮我们 override 相应的配置。
总体把握
我们开一个新项目,使用 eject 暴露所有配置,会发现根目录下生成了 config
目录,下面有 react 的所有配置文件:
很多教程都只讲如何配置 webpack.config.js
,那其他文件怎么配置呢?
自定义配置 react 打包生成目录
在 config-overrides.js
写入如下内容:
const path = require('path')
function resolve(dir) {
return path.join(__dirname, dir)
}
module.exports = {
paths: function (paths) {
paths.appBuild = resolve('./dist')
return paths
}
}
- 先导入了
path
依赖并写了resolve()
函数用来补全路径,path.resolve()
这个函数也可以补全,不过在('/a', '/b')
情况下,只有path.join()
是可以补全为/a/b
的。 - 之后导出我们要重写的配置对象,键就是你要重写的配置文件名,要重写
paths.js
,那就是paths
这个键下进行重写。
之后 build 打包生成的内容就会在 ./dist
下而不是默认的 ./build
下。
查看所有 paths.js 配置
如果你要查看所有 paths.js
的配置,第一种方法你重开一个 react 项目使用 eject 去查看有什么选项,第二种方法你在里面打印即可:
module.exports = {
paths: function (paths) {
paths.appBuild = resolve('./dist')
console.log(paths)
return paths
}
}
我们执行 yarn start
就可以看到所有配置了:
这些配置和 eject 暴露全部配置后生成的 paths.js
是一样的:
配置 webpack
根据上面的经验,我们配置 webpack 只需要如下写法即可:
module.exports = {
webpack: override()
}
下面几个例子简单说明。
配置路径 @ 快捷前缀
在 customize-cra 里给我们提供了一些封装好的 api ,我们直接使用就可以:
const {
override,
addWebpackAlias
} = require('customize-cra')
const path = require('path')
function resolve(dir) {
return path.join(__dirname, dir)
}
module.exports = {
webpack: override(
addWebpackAlias({
'@': resolve('src')
})
}
这里使用了 addWebpackAlias()
这个 api ,十分方便。
完整 api 请查看:官方 api 文档
配置 less 编译器
先安装基本依赖:
yarn add -D less less-loader react-app-rewire-less react-app-rewire-less-modules
之后配置 config-overrides.js
:
const {
override,
addWebpackAlias
} = require('customize-cra')
const path = require('path')
function resolve(dir) {
return path.join(__dirname, dir)
}
module.exports = {
webpack: override(
addLessLoader()
}
这样就可以打开 less-loader 了。
在 less 中添加全局变量
如果我们直接在项目入口 index.jsx
或 app.jsx
导入 .less
是不能识别到定义的变量的(样式可以),我们要在 less-loader 选项中导入全局资源:
module.exports = {
webpack: override(
addLessLoader({
additionalData: `@import "${ resolve('./src/assets/css/variable.less') }";`
})
}
官方对 addLessLoader()
这个 api 的参数定义是 loaderOptions ,也就是说里面可以传 less-loader 的配置对象。
less-loader 可配置项:webpack-contrib / less-loader
注:正如 vue-cli 在 css 相关 所说,该 @import "...";
导入语句结尾必须要有分号!
查看所有 webpack 配置
customize-cra 给我们提供的 api 不多,但是如同之前配置 paths.js
所说,我们可以用一个函数配置全部 webpack 的配置:
const fs = require('fs')
module.exports = {
webpack: override(
(config) => {
if (process.env.NODE_ENV === "production") {
}
console.log(config)
fs.writeFileSync(`./config-${process.env.NODE_ENV}.json`, JSON.stringify(config))
}
)
}
注:该函数只能传入一次。
下面说几个实用配置。
随心所欲配置法
如果你在上面一步中将 config
打印到了控制台,你会发现插件都无法显示内部配置:
但是输出到文件会发现,丢失了插件名字,成了一个个对象:
好心的 OptimizeCssAssetsWebpackPlugin 还给我们准备了插件说明,但是 TerserPlugin 却只是一个配置对象,没有任何说明,我们也不知道哪个对象是哪个插件的。
我们可以使用构造函数名字解决这个问题:
function invade(target, name, callback) {
target.forEach(
item => {
if (item.constructor.name === name) {
callback(item)
}
}
)
}
生产环境去除 console.log
有了上面的 invade()
函数,我们可以这样写:
module.exports = {
webpack: override(
(config) => {
if (process.env.NODE_ENV === "production") {
invade(config.optimization.minimizer, 'TerserPlugin', (e) => {
e.options.extractComments = false
e.options.terserOptions.compress.drop_console = true
})
}
}
)
}
关闭 sourceMap
react 官方很好心的给我们准备了这个环境变量,在 eject 后的 webpack.config.js
可以看到:
在根目录建立 .env.production
:
GENERATE_SOURCEMAP = false
关闭生产环境 devtool
module.exports = {
webpack: override(
(config) => {
if (process.env.NODE_ENV === "production") {
config.devtool = false;
}
)
}
漂亮的打包 js/css 文件名
默认打包后文件名带 .chunk
结尾,去掉即可:
module.exports = {
webpack: override(
(config) => {
if (process.env.NODE_ENV === "production") {
config.output.chunkFilename = config.output.chunkFilename.replace('.chunk', '')
invade(config.plugins, 'MiniCssExtractPlugin', (e) => {
e.options.chunkFilename = e.options.chunkFilename.replace('.chunk', '')
})
}
)
}
runtime 内联策略
有关 runtime 为何要内联,简单的说就是避免几 K 的文件还要单独加载一次,提高加载速度,你可以查找一些其他资料深入学习。
需要说的一点是,react 官方给我们提供了 process.env.INLINE_RUNTIME_CHUNK
选项,但是他不会配合 babel ,生成的 runtime 内联到 index.html
后仍然在 js
文件夹下生成,这是不友好的。
我们使用 script-ext-html-webpack-plugin :
yarn add -D script-ext-html-webpack-plugin
配置:
const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin')
module.exports = {
webpack: override(
(config) => {
if (process.env.NODE_ENV === "production") {
config.plugins.push(
new ScriptExtHtmlWebpackPlugin(
{
inline: /runtime\..*\.js$/
})
)
config.optimization.runtimeChunk = 'single'
}
)
}
注:不知道有没有细心的人会疑惑,customize-cra 给我们提供了 addWebpackPlugin()
的快捷 api 啊,为啥还要 push ,这里不详细说明了,有兴趣可以去查一下该 api 的代码,也是做了 config.push()
,在我们自定义的 (config) => { }
函数里用了是不生效的,因为没有 config
对象给他了,只能在外面用。
外面怎么用?
module.exports = {
webpack: override(
process.env.NODE_ENV === "production" ? addWebpackPlugin(new ScriptExtHtmlWebpackPlugin(...)) : null
)
}
应该没有人会这么用。
之后再打包就会发现,runtime 内联到了 index.html
,并且在 js
文件夹下不再生成了。
打包 splitChunks 分块策略
这里直接把 vue 的搬过来:
module.exports = {
webpack: override(
(config) => {
if (process.env.NODE_ENV === "production") {
config.optimization.splitChunks = {
chunks: 'all',
cacheGroups: {
libs: {
name: 'chunk-libs',
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: 'initial'
},
commons: {
name: 'chunk-commons',
test: resolve('src/components'),
minChunks: 3,
priority: 5,
reuseExistingChunk: true
}
}
}
}
)
}
非常易懂,不详细说明了。
prefetch 策略
在预加载和懒加载上也有一定的学问,可以先学习这篇文章:
《[译] React 16.6 懒加载 (与预加载) 组件》
在 webpack 4 中这么用即可:
() => import(
"./pages/home")
有关 preload 请参考 这里 ,不需要 preload 。
其他
能参照 vue 进行的优化配置还有很多,比如 svg 的配置,自行参照搬过来即可。
还有两个没有说到:
-
我如何知道 react 内置了什么插件:请打印
config.plugins
-
react 打包后路由块的文件名不好看:请配置
webpackChunkName
:() => import( "./pages/home")
之后就会打包出来
home.contenthash.js
。
总结
本文全部配置如下:
const {
override,
addLessLoader,
addWebpackAlias
} = require('customize-cra')
const path = require('path')
const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin')
function invade(target, name, callback) {
target.forEach(
item => {
if (item.constructor.name === name) {
callback(item)
}
}
)
}
function resolve(dir) {
return path.join(__dirname, dir)
}
module.exports = {
paths: function (paths) {
paths.appBuild = resolve('./dist')
return paths
},
webpack: override(
addWebpackAlias({
'@': resolve('src'),
}),
addLessLoader({
additionalData: `@import "${ resolve('./src/assets/css/variable.less') }";`
}),
(config) => {
if (process.env.NODE_ENV === "production") {
config.devtool = false;
config.output.chunkFilename = config.output.chunkFilename.replace('.chunk', '')
invade(config.optimization.minimizer, 'TerserPlugin', (e) => {
e.options.extractComments = false
e.options.terserOptions.compress.drop_console = true
})
invade(config.plugins, 'MiniCssExtractPlugin', (e) => {
e.options.chunkFilename = e.options.chunkFilename.replace('.chunk', '')
})
config.optimization.splitChunks = {
chunks: 'all',
cacheGroups: {
libs: {
name: 'chunk-libs',
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: 'initial'
},
commons: {
name: 'chunk-commons',
test: resolve('src/components'),
minChunks: 3,
priority: 5,
reuseExistingChunk: true
}
}
}
config.plugins.push(
new ScriptExtHtmlWebpackPlugin(
{
inline: /runtime\..*\.js$/
})
)
config.optimization.runtimeChunk = 'single'
}
return config
}
)
}
这不是一个完美的配置,还有一些细节和 svg 内联没有追究,有时间可以自行参照配一下,但基本上复现了在 vue 中我们的配置。
《vue-cli 创建项目后优化更多配置(三)》
https://blog.csdn.net/qq_21567385/article/details/108383083
ElementUI之Message功能拓展 - 掘金
在最近项目开发中,接口错误信息是在拦截器统一处理,在一次产品大大验收过程中,由于服务器没有重启完成,导致前端弹出一推错误提示语,产品大大对于提示语的交互效果提出了一系列的建议。由于项目使用了ElementUI
框架,加上本人喜欢投(xin)机(shou)取(nian)巧(lai),于是去查看ElementUI
Message
的源码,根据实际需求自定义了Message
功能。
场景描述
- 场景一:限制页面同时展示消息提示语的最大数量(优先展示后插入的提示语)
- 场景二:根据不同情况可以优先显示新/旧消息提示语
- 场景三:如果超出了最大显示数量,则剩余的消息以队列的显示依次展示
实现方案
场景一
-
功能描述
- 根据设置的最大数量,如果存储的实例列表
instances
长度超出最大限制数则销毁之前的消息实例instance
(调用Message方法创建消息提示语会返回当前消息的一个实例),否则保存新建实例instance
到实例列表instances
中 - 如果消息提示语消失,需要从实例列表
instances
中移除当前实例instance
,确保页面显示消息数量与instances
列表长度统一
- 根据设置的最大数量,如果存储的实例列表
-
代码实现
新建ZMessage
构造函数import { Message } from 'element-ui' function ZMessage (options) { if (!(this instanceof ZMessage)) { return new ZMessage(options) } this.init(options) } 复制代码
静态配置项和实例列表
ZMessage.config = { max: 0, } ZMessage.instances = [] 复制代码
定义创建消息和监听实例消失事件方法
ZMessage.prototype.setMessage = function (options) { const instance = Message(options) instance.$watch('visible', val => { ZMessage.instances = ZMessage.instances.filter(item => item !== instance) }) ZMessage.instances.push(instance) } 复制代码
定义移除消息实例方法
ZMessage.prototype.prototype.removeMessages = function () { const { instances, config: { max } } = ZMessage ZMessage.instances = instances.filter((instance, index) => { if (index < instances.length - max + 1) { instance && instance.close() return false } return true }) } 复制代码
初始化消息
ZMessage.prototype.init = function (options) { const { max } = ZMessage.config if (max > 0 && ZMessage.instances.length >= max) { this.removeMessages() : } if (ZMessage.instances.length < max || !max) { this.setMessage(options) } } 复制代码
场景二
-
功能描述
- 在场景一的基础上新增优先取消息还是旧消息的标志操作
-
代码实现 静态配置项和实例列表
ZMessage.config = { max: 0, showNewest: true } 复制代码
初始化
ZMessage.prototype.init = function (options) { const { max, showNewest } = ZMessage.config if (max > 0 && ZMessage.instances.length >= max && showNewest) { this.removeMessages() } if (ZMessage.instances.length < max || !max) { this.setMessage(options) } } 复制代码
场景三
-
功能描述
- 在场景一场景二基础上添加是否使用队列方式存储未展示消息的实例,如果超出了最大限制数则创建消息实例的容器存储到消息队列
queue
中 - 监听是否有消息消失,如果有则从消息队列
queue
中取出第一个容器,创建消息实例
- 在场景一场景二基础上添加是否使用队列方式存储未展示消息的实例,如果超出了最大限制数则创建消息实例的容器存储到消息队列
-
代码实现
静态配置项和消息容器队列ZMessage.config = { max: 0, showNewest: true, isQueue: false } ZMessage.queue = [] 复制代码
生成队列
ZMessage.prototype.saveToQueue = function (options) { return () => { this.setMessage(options) } } 复制代码
初始化
ZMessage.prototype.init = function (options) { const { max, isQueue, showNewest } = ZMessage.config if (max > 0 && ZMessage.instances.length >= max && showNewest && !isQueue) { this.removeMessages() } if (ZMessage.instances.length >= max && isQueue) { ZMessage.queue.push(this.saveToQueue(options)) } else if (ZMessage.instances.length < max || !max) { this.setMessage(options) } } 复制代码
获取消息实例和添加事件监听
ZMessage.prototype.setMessage = function (options) { const instance = Message(options) instance.$watch('visible', val => { ZMessage.instances = ZMessage.instances.filter(item => item !== instance) if (ZMessage.config.isQueue && ZMessage.queue.length) { ZMessage.queue.shift()() } }) ZMessage.instances.push(instance) } 复制代码
最后一步
添加不同消息类型功能静态方法
const messageTypes = ['success', 'warning', 'error', 'info']
messageTypes.forEach(type => {
ZMessage[type] = options => {
let opts = options
if (typeof options === 'string') {
opts = {
message: options
}
}
return new ZMessage({ ...opts, type })
}
})
复制代码
完整代码
import { Message } from 'element-ui'
const messageTypes = ['success', 'warning', 'error', 'info']
function ZMessage (options) {
if (!(this instanceof ZMessage)) {
return new ZMessage(options)
}
this.init(options)
}
ZMessage.queue = []
ZMessage.instances = []
ZMessage.config = {
max: 0,
isQueue: false,
showNewest: true
}
ZMessage.setConfig = function (config = {}) {
ZMessage.config = { ...ZMessage.config, ...config }
}
ZMessage.close = Message.close
ZMessage.closeAll = Message.closeAll
messageTypes.forEach(type => {
ZMessage[type] = options => {
let opts = options
if (typeof options === 'string') {
opts = {
message: options
}
}
return new ZMessage({ ...opts, type })
}
})
ZMessage.prototype.init = function (options) {
const { max, isQueue, showNewest } = ZMessage.config
if (max > 0 && ZMessage.instances.length >= max && showNewest && !isQueue) {
this.removeMessages()
}
if (ZMessage.instances.length >= max && isQueue) {
ZMessage.queue.push(this.saveToQueue(options))
} else if (ZMessage.instances.length < max || !max) {
this.setMessage(options)
}
}
ZMessage.prototype.removeMessages = function () {
const {
instances,
config: { max }
} = ZMessage
ZMessage.instances = instances.filter((instance, index) => {
if (index < instances.length - max + 1) {
instance && instance.close()
return false
}
return true
})
}
ZMessage.prototype.setMessage = function (options) {
const instance = Message(options)
instance.$watch('visible', val => {
ZMessage.instances = ZMessage.instances.filter(item => item !== instance)
if (ZMessage.config.isQueue && ZMessage.queue.length) {
ZMessage.queue.shift()()
}
})
ZMessage.instances.push(instance)
}
ZMessage.prototype.saveToQueue = function (options) {
return () => {
this.setMessage(options)
}
}
export default ZMessage
import Vue from 'vue'
import ZMessage from 'path/to/ZMessage.js'
ZMessage.setConfig({ max: 1, isQueue: false, showNewest: true })
Vue.prototype.$message = ZMessage
复制代码
小结
希望看完本篇文章能对你拓展ElementUI框架的Message组件功能有所帮助。
文中如有错误,欢迎在评论区指正,如果这篇文章有帮助到你,欢迎点赞和关注。
参考资料
可组合的 Vue
2021 年 5 月来自全球各地的 Vue.js 开发者齐聚线上,一起见证了 VueConf 2021 杭州的成功举办。
在大会上,Vue.js 核心团队成员,VueUse 作者,全职开源工程师 Anthony Fu 通过远程接入的方式给大家带来了《可组合的 Vue》 主题演讲。分享的内容主要包括 Vue Composition API 底层原理介绍,对于编写优质的组合式函数技巧与模式。
以下是他在 VueConf 2021 分享的视频和 PPT,欢迎大家观看。
推荐阅读:
* [一文搞懂Web常见的攻击方式](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247499306&idx=1&sn=9fb499afbb28b1069cfdb4d1362dfed8&chksm=97c66384a0b1ea92d2c92d7b3206d881eb8ec900c086cec5cba9633f18307cd0ca3709cc3e5c&scene=21#wechat_redirect)
* [Vue3 在编译优化方面做的努力 | HcySunYang](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247499282&idx=1&sn=2b84ede7de921450a98622856f2fccaf&chksm=97c663bca0b1eaaac7a62b69fe74dcc4100ad47f9128f760cecd18b8e9e49cceab7e1953809d&scene=21#wechat_redirect)
* [\[视频\]尤雨溪谈Vue3 生态进展和计划](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247499247&idx=1&sn=bd984e65c5d547b671ba1b8c1d14286f&chksm=97c66041a0b1e9576874eac6ebd784237dc7fe4f29bafd814f7bfa4e5aaa17bc96788a9fe2e5&scene=21#wechat_redirect)
* [一文搞懂单点登录三种情况的实现方式](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247498639&idx=1&sn=1386ea609c31345e380f21f12cb4dc05&chksm=97c66621a0b1ef3777f00e35035e4b097404f64bc39b17fbb225b2fee9ec3eab7be2d4e5863c&scene=21#wechat_redirect)
* [终于有人把 Nginx 说清楚了,图文详解!](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247498339&idx=1&sn=6c42ff6f859d3e02c360e56b585d9816&chksm=97c667cda0b1eedbb665a83f66f8c264a257763b77a03e00c87d0d1d8373c0c1ef3b65c3b193&scene=21#wechat_redirect)
* [推荐 130 个令你眼前一亮的网站,总有一个用得着](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247498262&idx=1&sn=886534663bb8209d4d9ad32232eb3d5d&chksm=97c667b8a0b1eeaea90f83e91505d40efd4c606aca7e1f8061697f569bc91f8dba3497bc7a85&scene=21#wechat_redirect)
* [深入浅出 33 道 Vue 99% 出镜率的面试题](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247498262&idx=2&sn=bc792931ee337d3e7cb73bdb8fa4599e&chksm=97c667b8a0b1eeae0915411943ab1d50ca0f4847882d56020d23b4b9308021af8326e1ffb631&scene=21#wechat_redirect)
**VUE中文社区**
编程技巧 **·** 行业秘闻 **·** 技术动向
前端项目集成 stylelint - 掘金
github
代码 & demo 在 github stylelint-guide
依赖安装
可以使用 yarn
或者 npm
。
yarn add stylelint stylelint-order stylelint-config-standard
# or
npm install stylelint stylelint-order stylelint-config-standard --save-dev
复制代码
创建 stylelint.config.js
你可以自定义你的验证规则来适应你的团队规范。
// basic rules
module.exports = {
extends: 'stylelint-config-standard',
ignoreFiles: ['**/*.js', '**/*.md'],
plugins: ['stylelint-order'],
rules: {
'at-rule-no-unknown': [
true,
{
ignoreAtRules: ['function', 'if', 'each', 'include', 'mixin']
}
],
'no-empty-source': null,
'rule-empty-line-before': 'never',
'at-rule-empty-line-before': null,
'no-missing-end-of-source-newline': null,
'selector-list-comma-newline-after': null,
'font-family-no-missing-generic-family-keyword': null,
'indentation': 2,
// ...
}
}
复制代码
重新排序你的 CSS
属性
你可能注意到上面的代码有个 plugin
选项 stylelint order
。 它可以根据你设定的 CSS
顺序来重新排序。 你可以在 rules
选项中添加对应的规则,例如:
rules: {
"order/order": [
"declarations",
"custom-properties",
"dollar-variables",
"rules",
"at-rules"
],
// 根据 Andy Ford 的 "Order of the Day: CSS Properties"
// 并且可以将 CSS 属性进行分组
"order/properties-order": [{
groundName: "Display & Flow",
emptyLineBefore: "never",
properties: [
"display",
"visibility",
"float",
"clear",
]
},
{
groundName: "Positioning",
emptyLineBefore: "never",
properties: [
"position",
"top",
"right",
"bottom",
"left",
"z-index",
"transform",
]
},
{
groupName: "Flex",
emptyLineBefore: "never",
properties: [
"flex",
"flex-direction",
"flex-grow",
"flex-shrink",
"flex-basis",
"flex-wrap",
"justify-content",
"align-items"
]
},
{
groupName: "Dimensions",
emptyLineBefore: "never",
properties: [
"width",
"min-width",
"max-width",
"height",
"min-height",
"max-height",
"overflow",
],
},
{
groupName: "Margins, Padding, Borders, Outline",
emptyLineBefore: "never",
properties: [
"margin",
"margin-top",
"margin-right",
"margin-bottom",
"margin-left",
"padding",
"padding-top",
"padding-right",
"padding-bottom",
"padding-left",
"border-radius",
"border",
"border-top",
"border-right",
"border-bottom",
"border-left",
"border-width",
"border-top-width",
"border-right-width",
"border-bottom-width",
"border-left-width",
"border-style",
"border-top-style",
"border-right-style",
"border-bottom-style",
"border-left-style",
"border-color",
"border-top-color",
"border-right-color",
"border-bottom-color",
"border-left-color",
"outline",
"list-style",
"table-layout",
"border-collapse",
"border-spacing",
"empty-cells",
],
},
{
groundName: "Typographic Styles",
emptyLineBefore: "never",
properties: [
"font",
"font-family",
"font-size",
"line-height",
"font-weight",
"text-align",
"text-indent",
"text-transform",
"text-decoration",
"letter-spacing",
"word-spacing",
"white-space",
"vertical-align",
"color",
]
},
{
groupName: "Backgrounds",
emptyLineBefore: "never",
properties: [
"background",
"background-color",
"background-image",
"background-repeat",
"background-position",
]
},
{
groundName: "Opacity, Cursors, Generated Content, Transition",
emptyLineBefore: "never",
properties: [
"opacity",
"cursor",
"content",
"quotes",
"transition"
]
},
{
...
}
]
}
复制代码
使用 yarn
或者 npm
脚本
"scripts": {
"lint": "yarn run lint:css && yarn run lint:basecss",
"lint:css": "stylelint src/**/*.vue --fix",
"lint:basecss": "stylelint src/pages/**/*.less --custom-syntax ./node_modules/postcss-less --fix"
}
复制代码
这里我使用了 postcss-less
这个包来指定校验 less
文件的语法 如果你需要校验 less
文件,需要安装 postcss
包
yarn add postcss
# or
npm add postcss
复制代码
使用 husky
在预提交时校验代码
// package.json
"husky": {
"hooks": {
"pre-commit": "yarn run lint",
}
},
复制代码
现在你可能会觉得自己的 CSS 看起来舒服多了~
Enjoy it 🎉
https://juejin.cn/post/6844903799094509575
如何解决VSCode Vim中文输入法切换问题? -im-select
https://www.zhihu.com/question/303850876/answer/2263584870
折腾了多次后实现。
前言
使用 vim
过程会频繁切换状态,在 insert 模式下,主力使用的中文输入法,比如搜狗输入法、微软拼音输入法等,而在 normal 模式下敲命令都是需要英文状态的,为了切换到 normal 模式时输入法能自动切换为英文状态,vscodevim插件推荐使用im-select。
在中文输入法状态下,鼠标点击搜狗输入法状态栏的 “中”,或者 Windows 工具栏底下的 “中”,或者按 Shift
键,这三者等效,都是切换搜狗输入法的中 / 英状态。但据网上所查,Windows 版的 im-select
做不到这一点,实际上它是通过切换的语言包实现的中英切换,故照搬vscodevim插件推荐的配置并不起作用。
语言包和键盘准备
大多数人的电脑上只安装了中文语言包,故首先要安装英文语言包。
在 Windows 设置》时间和语言》语言下,找到首选语言,点 + 号添加语言,选择英语(美国)或者英语(英国)都行,只需要基本包即可,手写识别、语音识别可以不需要。等待安装完成,界面是这样的。
中英两种语言包
点击中文语言包的 “选项” 按钮,可以添加键盘,搜狗拼音输入法就在这里,这个键盘实际就是输入法,直译反而不好理解。如果需要,还可以继续添加微软拼音、微软五笔等等。
中文语言键盘(输入法)
验证
im-select
本意是利用一个命令行程序在终端自由选择输入法,im 代表 “Input Method” 这给vscodevim插件提供了自动切换支持。
首先安装 im-select
,下载地址 (https://github.com/daipeihust/im-select/release),将压缩包下载下来解压,其中包含了源码和编译好的可执行文件 im-select.exe
,在 out 子目录下。也可以在这里只选择 im-select.exe
文件下载下来。将 im-select.exe
放到自己习惯的路径下,比如我的路径是 “D:\PortableApp\bin”,建议把这个路径添加到 Windows 的环境变量中,减少后续验证的麻烦。
打开 Windows 终端,输入命令 “im-select”,可以看到输出了当前输入法的编号,在搜狗或微软拼音输入法下(无论中英文)是 2052。鼠标悬停语言栏语言状态图标,可以看到提示 “要切换输入法,请按 Windows 键 + 空格键”(这里又被翻译成输入法),按 Win
+ 空格,,语言被切换到英文,重复上述命令,输出为 1033,说明 “im-select” 命令取到了当前语言包的编号。
任务栏语言状态提示
再输入命令 “im-select 2052” 发现语言包被切换到了中文,输入法(键盘)被切换到中文语言包前次使用的输入法,前次是搜狗就恢复到搜狗,前次是微软拼音就恢复到微软拼音;再输入命令“im-select 1033”,语言包被切换为英文,英文下的输入法(键盘),只有一种。
> im-select
2052
> im-select
1033
> im-select 2052
> im-select 1033
总结,命令 “im-select 2052” 和“im-select 1033”,作用和按快捷键 Win
+ 空格是等效的。至此,验证了 im-select 确实可以切换中英输入法。
vscodevim 配置
参照vscodevim插件推荐,在 vscode 中填入如下配置,{im} 不需要换成 2052,im-select 路径填为实际安装路径,注意路径符号需要两个反斜杠,前一个是转义符。在使用 vscodevim 过程中,切换模式,插件会自动调用 im-select 切换中英输入法。
"vim.autoSwitchInputMethod.enable": true,
"vim.autoSwitchInputMethod.defaultIM": "1033",
"vim.autoSwitchInputMethod.obtainIMCmd": "D:\\PortableApp\\bin\\im-select.exe",
"vim.autoSwitchInputMethod.switchIMCmd": "D:\\PortableApp\\bin\\im-select.exe {im}",
相关阅读
- 伪码人专栏目录导航 持续更新...
- 微软 WSL——Linux 桌面版未来之光
- WSLg:为 WSL 增光添彩
- Python 自动操作 GUI 神器 PyAutoGUI
- silaoA 的博客. https://silaoa.github.io
如本文对你有帮助,或内容引起极度舒适,欢迎分享转发与留言交流
►本文为原创文章,如需转载请私信知乎账号 silaoA 或联系公众号伪码人(We_Coder)。
都看这里了,不妨点个赞再走呗
https://www.zhihu.com/question/303850876/answer/2263584870
4 个技巧,让 Windows 10 和 iOS 也能无缝协作 - 少数派
4 个技巧,让 Windows 10 和 iOS 也能无缝协作 - 少数派
苹果生态的设备协作一直是它的优势,隔空投送(AirDrop) 配合 接力(Handoff)让你可以在 Mac 和 iOS 上轻松地进行传输文件、发送网址和同步剪切板等操作。但假如你用的是一台 Windows 10 电脑和一台 iPhone,有没有办法获得类似的体验呢?
作为一个双平台的用户,在这篇文章里我将分享几个实用的小技巧,想办法让你的 Windows 10 和 iOS 实现「无缝协作」。
文件传输:Windows 和 iOS 也有「隔空投送」
隔空投送是苹果设备之间无线传输文件的一个实用功能。而到了 Windows 10 和 iOS 上,许多人会使用微信、QQ 等 IM 工具来发送文件。这个方法固然简单,但操作起来终究不像隔空投送一键发送这样方便。
隔空投送之所以好用,是因为它可以在手机与电脑的任何地方直接发送,在手机上用分享菜单,在电脑上直接右键点击,这种不需要跳转的方式效率无疑是最高的。
现在市面上有很多第三方的文件传输工具,少数派之前也介绍了不少。我在尝试了几乎所有 Windows 和 iOS 平台间的传输文件工具后,最终选择了 Send Anywhere。
> 拓展阅读:手机和电脑之间如何高效传文件?10 款跨平台传输应用和服务横评
使用 Send Anywhere 需要在 iOS 和 Windows 上安装相应的客户端。在 iOS 上,Send Anywhere 不需要打开客户端导入文件,直接利用分享菜单就能发送。以相册为例,第一次使用时,选择希望分享的照片,然后在分享菜单里找到 Send Anywhere,点击后 Send Anywhere 会生成一个 6 位的分享码,然后在电脑上输入后配对传输。之后 Send Anywhere 会记住你选择的设备,就不需要每次都输入分享码了。
Windows 上同样也非常简单,右键点击希望分享的文件,然后点击「Share with Send Anywhere」(或直接选择希望分享的设备)。手机会收到通知,点击通知接收文件即可。
许多同类工具虽然有类似的功能,但都需要在分享菜单中选择「在 XXX 中打开」导入文件发送。而 Send Anywhere 能够实现类似隔空投送的效果,一键发送。
其他同类工具很多都需要打开后再传输
需要注意的是,当你使用 6 位分享码传输时,Send Anywhere 采用的是局域网点对点传输,文件将直接发送到对应的设备上。而如果你在传输时选择了设备或采用链接分享,那么文件将上传到 Send Anywhere 的服务器中保存 48 小时(之后会自动删除)。
不过,假如你有大量传输文件的需要,或者需要传输体积较大的文件,更推荐使用 iOS 文件 App 里连接 SMB 服务的方式进行传输 。这种方法需要一定的动手能力,在 Windows 上设置好共享文件夹后,当两台设备处于同一局域网时,就可以直接进行传输。少数派 Power+ 付费栏目对此有一篇详细的介绍,感兴趣的话可以阅读一下。
> 付费栏目:媲美 AirDrop,如何让 iOS 与 Windows 便捷地互传文档
发送网址:让 Safari 和 Edge 也能「接力」
Windows 10 上默认的浏览器是 Microsoft Edge,iOS 上的默认浏览器是 Safari,要把手机上查看的网页发送到电脑上,除了复制粘贴到聊天工具,还有更快一步的方法。
在 App Store 里搜索下载「Continue on PC」,这是微软开发的一款用来共享网页的小工具。
在 Safari 里打开想要分享的网页,在分享菜单里选择 Continue on PC,登录你在 Windows 10 电脑上登录的微软账号,选择你的电脑,分享成功后电脑端就会自动用 Edge 打开你分享的网页。
除了 Edge,Chrome 和 Firefox 也都支持类似的功能。不过你需要在 iOS 上下载 Chrome 和 Firefox 浏览器,并且在电脑和手机上都要登录一样的 Google 和 Firefox 账号。相比 Edge,两者除了支持从手机发送到电脑,还支持从电脑发送到手机。
剪切板同步:Windows 也能有「通用剪切板」
支持 Windows 和 iOS 同步的剪切板同步工具,之前少数派介绍过 几款,另外一些国产的输入法也都支持云剪切板功能。这些方法的问题在于你不能像苹果的通用剪切板一样在 Windows 上复制后直接到手机上粘贴内容,还是需要进入 App 或切换输入法来粘贴。
如果你想在 Windows 和 iOS 上实现类似「通用剪切板」的效果,推荐你试试 Bark。利用 iOS 的推送通知,它可以把你在 Windows 上复制的文字内容推送到你的手机上,并自动帮你复制到剪切板中。整个过程只需要在 iOS 上下载 Bark 的客户端,然后在 Windows 上下载一个 脚本,进行简单的配置后即可使用。
具体的操作方法,请参考少数派之前的 Bark 介绍文章:
> 拓展阅读:定制推送内容、同步 Windows 剪贴板…… 把 iOS 推送通知玩出花:Bark
如果你需要双向的剪切板传送,那 MFiles 这款工具可以帮到你。它是一个支持多平台文件传输的小工具,还拥有剪切板同步的功能。
首先你需要在 Windows 和 iOS (¥18)上下载应用,并进行配对。从电脑或手机上复制一段文字,点击 Windows 客户端里的「传送粘贴板」或 iOS 客户端上的「传送 - 传送剪切板」,即可传送剪切板中的内容,直接在另一台设备上粘贴就行了。
> 拓展阅读:让 Android 用上隔空投送,你可以试试全平台的 MFiles
Office 文件接力:在电脑上继续编辑手机上的文档
工作里经常需要跟 Office 三件套打交道的人不在少数。现在很多人都习惯用手机查看别人发过来的 Word、Excel 或 PowerPoint 文件,但假如需要编辑文档,还是电脑更方便一些。如果你有将 iOS 上打开的 Office 文件同步到 Windows 上继续编辑的需求,下面这个技巧可以帮到你。
在 iOS 的 App Store 里下载新版 Office 应用,它可以帮你 iOS 上打开和编辑 Word、Excel 或 PowerPoint 文件。用它打开你需要编辑的 Office 文档,然后将它保存到你的 OneDrive 上(Office 365 用户拥有 1TB 的 OneDrive 空间,另外每个 Windows 账号则都会附赠 5GB 的 OneDrive 免费空间)。
回到你的 Windows 电脑,打开系统设置,在「隐私」中找到「活动历史记录」,开启里面的选项。
然后按下 Win+TAB,打开任务视图。这时候你会发现在下面出现了一条时间线,在这里能够看到你最近保存和打开过的文件。之后你只要在手机上打开或编辑过 OneDrive 里的 Office 文件,你都能在这里看到记录。点击时间线里的文档就可以继续在电脑上进行编辑。相比文件传输或者使用其他网盘,这种方式更加接近「接力」的使用体验,两边都会自动同步你的修改记录,保证你随时随地都可以开始工作。
以上就是我经常使用的 4 个 iOS 和 Windows 10 协作的技巧。除此之外,一些电脑厂商还会提供专门的同步工具来帮你进行协作,比如 Dell 的 Dell Mobile Connect 就可以让你在 Windows 电脑上查看 iPhone 的通知、短信,还能直接在电脑上用手机打电话,如果有需要的话不妨试试看。
> 更多实用的 Windows 10 小知识,尽在「真正好用的 Windows 10,从这里开始」专题
> 下载少数派 客户端、关注 少数派公众号,了解更多 iOS 和 Windows 使用技巧 🐱🏍
https://sspai.com/post/59329
一个让 git clone 提速几十倍的小技巧
不知道大家有没有遇到比较大的项目,git clone 很慢很慢,甚至会失败的那种。大家会怎么处理的呢?
可能会考虑换一个下载源,可能会通过一些手段提高网速,但是如果这些都试过了还是比较慢呢?
今天我就遇到了这个问题,我需要把 typescript 代码从 gitlab 下载下来,但是速度特别慢:
git clone https://github.com/microsoft/TypeScript ts
等了很久还是没下载完,于是我加了一个参数:
git clone https://github.com/microsoft/TypeScript --depth=1 ts
这样速度提高了几十倍,瞬间下载完了。
加上 --depth 会只下载一个 commit,所以内容少了很多,速度也就上去了。
而且下载下来的内容是可以继续提交新的 commit、创建新的分支的。不影响后续开发,只是不能切换到历史 commit 和历史分支。
我用我的一个项目测试过,我首先下载了一个 commit:
然后做一下改动,之后 git add、commit、push,能够正常提交:
创建新分支也能正常提交。唯一的缺点就是不能切换到历史 commit 和历史分支。
在一些场景下还是比较有用的:当需要切换到历史分支的时候也可以计算需要几个 commit,然后再指定 depth,这样也可以提高速度。
大家有没有想过,这样能行的原理是什么?
git 原理
git 是通过一些对象来保存信息的:
- glob 对象存储文件内容
- tree 对象存储文件路径
- commit 对象存储 commit 信息,关联 tree
以一个 commit 为入口,关联的所有的 tree 和 blob,就是这个 commit 的内容。
commit 之间相互关联,而 head、branch、tag 等是指向具体 commit 的指针。可以在 .git/refs 下看到。这样就基于 commit 实现了分支、tag 等概念。
git 就是通过这三个对象来实现的版本管理和分支切换的功能,所有 objects 可以在 .git/objects 下看到。
这就是 git 的原理。
主要理解 blob、tree、commit 这三个 object,还有 head、tag、branch、remote 等 ref。
能下载单个 commit 的原理
我们知道了 git 是通过某一个 commit 做为入口来关联所有的 object,那如果我们不需要历史自然就可以只下载一个 commit。
这样依然基于那个 commit 创建新的 commit,关联新的 blob、tree 等。但是历史的 commit、tree、blob 因为都没有下载下来所以无法切回去,相应的 tag、branch 等指针也不行。这就是我们下载了单个 commit 却依然可以创建新的分支、commit 等的原理。
总结
遇到大的 git 项目的时候,可以通过添加 --depth 参数使得速度极大提升,历史 commit 越多,下载速度提升越大。
而且下载下来的项目依然可以进行后续开发,可以创建新的 commit 和新的分支、tag,只是不能切换到历史 commit、分支、tag。
我们梳理了 git 的原理:通过 tree、blob、commit 这三个 object 来存储文件和提交信息,通过 commit 之间的关联来实现分支、标签等功能。commit 是入口,关联所有的 tree 和 blob。
我们下载了一个 commit,就是下载了他关联的所有 tree、blob,还有一些 refs (包括 tag、branch 等),这就是 --depth 的原理。
希望大家在不需要切换到历史 commit 和分支的场景下可以用这个技巧来提升大项目的 git clone 速度。
推荐阅读:
* [大文件上传如何做断点续传](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247499152&idx=1&sn=daed0c35ae7d63dd342b16b31b89cda2&chksm=97c6603ea0b1e9285f7ded75e0cadf6436281e1f06dfec6c0414f8b5b3cecfcb3f202ba6c629&scene=21#wechat_redirect)
* [](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247499152&idx=1&sn=daed0c35ae7d63dd342b16b31b89cda2&chksm=97c6603ea0b1e9285f7ded75e0cadf6436281e1f06dfec6c0414f8b5b3cecfcb3f202ba6c629&scene=21#wechat_redirect)[Vue3 在编译优化方面做的努力 | HcySunYang](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247499282&idx=1&sn=2b84ede7de921450a98622856f2fccaf&chksm=97c663bca0b1eaaac7a62b69fe74dcc4100ad47f9128f760cecd18b8e9e49cceab7e1953809d&scene=21#wechat_redirect)
* [\[视频\]尤雨溪谈Vue3 生态进展和计划](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247499247&idx=1&sn=bd984e65c5d547b671ba1b8c1d14286f&chksm=97c66041a0b1e9576874eac6ebd784237dc7fe4f29bafd814f7bfa4e5aaa17bc96788a9fe2e5&scene=21#wechat_redirect)
* [一文搞懂单点登录三种情况的实现方式](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247498639&idx=1&sn=1386ea609c31345e380f21f12cb4dc05&chksm=97c66621a0b1ef3777f00e35035e4b097404f64bc39b17fbb225b2fee9ec3eab7be2d4e5863c&scene=21#wechat_redirect)
* [终于有人把 Nginx 说清楚了,图文详解!](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247498339&idx=1&sn=6c42ff6f859d3e02c360e56b585d9816&chksm=97c667cda0b1eedbb665a83f66f8c264a257763b77a03e00c87d0d1d8373c0c1ef3b65c3b193&scene=21#wechat_redirect)
* [推荐 130 个令你眼前一亮的网站,总有一个用得着](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247498262&idx=1&sn=886534663bb8209d4d9ad32232eb3d5d&chksm=97c667b8a0b1eeaea90f83e91505d40efd4c606aca7e1f8061697f569bc91f8dba3497bc7a85&scene=21#wechat_redirect)
* [深入浅出 33 道 Vue 99% 出镜率的面试题](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247498262&idx=2&sn=bc792931ee337d3e7cb73bdb8fa4599e&chksm=97c667b8a0b1eeae0915411943ab1d50ca0f4847882d56020d23b4b9308021af8326e1ffb631&scene=21#wechat_redirect)
**VUE中文社区**
编程技巧 **·** 行业秘闻 **·** 技术动向
https://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247499297&idx=3&sn=3c0e737d5c471c4d7a58002570ffed2e&chksm=97c6638fa0b1ea99a041457f5cdd40a3f5f669ab2c83cb3fc7cf20e9d9c57b90c6b0bca0ad5f&mpshare=1&scene=24&srcid=0604QvdGNuApp1FYy9aQGqIl&sharer_sharetime=1622775900952&sharer_shareid=bc6a9591e78ea361aa62f4212113d179#rd
快到飞起!一键搞定 GitHub 下载加速!
公众号关注 “GitHubDaily”
设为 “星标”,每天带你逛 GitHub!
大家好,我是小 G。
作为一名经常分享各类 GitHub 优质开源项目的博主,我经常会在后台收到类似以下私信:
GitHub 下载速度很慢,请问该如何解决呢?
借着周末这个空档,今天跟大家好好聊一聊。
众所周知,GitHub 作为目前全球最大的代码托管平台,已成为开发者日常不可分割的开发工具之一。
不过由于某种原因,使得 GitHub 在国内的下载速度一直被开发者所诟病。
下面,给大家分享 3 种可用的解决方案,让你一键搞定 GitHub 下载加速。
Fast Git
首先要介绍的是 FastGit,一个非官方的 GitHub 加速镜像,于 2020 年 3 月 28 号成立,由 5 名开发者参与维护,日常使用颇为稳定,操作起来也很便捷。
下面拿 Linux 仓库作为示例,给大家做下讲解。
在正常情况下,当我们需要 clone GitHub 上仓库代码时,会运行类似以下命令:
git clone https://github.com/torvalds/linux
而使用 FastGit 镜像加速,则只需要将 GitHub 域名,替换为 FastGit 的域名即可,就像下面这样:
git clone https://hub.fastgit.org/torvalds/linux
嫌麻烦的话,也可以选择对 Git 进行全局设置,使用 FastGit 替换 GitHub 的指向链接,设置命令如下:
git config --global url."https://hub.fastgit.org/".insteadOf "https://github.com/"
git config protocol.https.allow always
对于常用的 Git 命令行操作,FastGit 已基本满足,唯一的缺点,是暂时不支持用 SSH 克隆代码仓库。
另外一点,是关于 GitHub Web 端的操作与访问。
GitHub 基础的 Web 页面操作,其实 FastGit 已提供了很好的支持,在访问某个代码仓库时,只需要同上面命令行一样,将 https://github.com/ 这一域名,替换为 https://hub.fastgit.org/ 域名即可实现快速访问。
出于对安全性的考虑,FastGit 禁用了 Web 端的 Cookie 与 Session 等敏感权限,这就意味着,在利用 FastGit 访问 Web 代码库时,开发者只能以游客身份访问,而不能登录进行操作。
想更进一步了解 FastGit 的操作与使用,可查看其官方文档或 GitHub 仓库:
https://doc.fastgit.org/zh-cn/guide.html
GitHub 增强
作为一名油猴脚本的老用户,小 G 经常会用它来安装一些比较实用的脚本工具,而「GitHub 增强」便是其中之一。
这款工具的主要作用,是能在 GitHub 页面上的 Git Clone/SSH、Release、Raw、Code(ZIP) 等地方,为你添加一个高速下载的选项。以及在项目列表页,添加单文件快捷下载的指向链接。
就像下面这样:
在 GitHub 主页下载代码包👇
对单个 GitHub 文件进行下载👇
除此之外,作者还开源了其它一些比较实用的油猴脚本,但因为不是本篇文章的内容核心,所以在此按下不表。
感兴趣的同学,可前往其 GitHub 仓库一窥究竟:
https://github.com/XIU2/UserScript
「GitHub 增强」油猴脚本安装地址:
https://greasyfork.org/zh-CN/scripts/412245
Fast GitHub
对于不常用油猴脚本的同学,也不必惊慌,下面推荐一款浏览器插件:Fast GitHub。
在安装之后,它会在 GitHub 主页新增一个「加速」按钮,点击之后,便会出现 CNPMJS、FastGit、Cloudflare Workers 3 种可选下载加速通道,让你可以快速下载项目代码。
效果如下:
该插件支持 Chrome、Safari、Edge、Firefox 等主流浏览器,大家可安心使用。
GitHub 地址:
https://github.com/fhefh2015/Fast-GitHub
总结
上述提到的 3 种 GitHub 加速方案,从使用的便捷性及扩展性讲,小 G 更加倾向于第一种,即用 FastGit 作为 GitHub 的镜像,一劳永逸搞定 GitHub 下载加速,相信这也可以从根本上解决你当下的困境。
---
如果你想了解更多关于 GitHub 的使用技巧或 GitHub 项目,可以关注一下我们的代码仓库,里面总结了自 2018 - 2020 年以来,GitHubDaily 在多个平台分享的数千个开源项目:
https://github.com/GitHubDaily/GitHubDaily
今天的分享到此结束,我们下期再见,Respect!
GitHubDaily
专注于分享 GitHub 上知名的 Python、Java、Web、AI、数据分析等多个领域的优质学习资源、开源项目及开发者工具,为 GitHub 开发者提供优质编程资讯。
163 篇原创内容
[译] 在 async/await 中更好的处理错误 - 掘金
原文链接:Better error handling with async/await,by Sobio Darlington
本篇文章介绍在使用 async
/await
语法时,一种更好的处理错误的方式。在此之前,大家也需要先了解下 Promise 的工作原理。
从回调地狱到 Promise
回调地狱(callback Hell),也称为“末日金字塔(Pyramid of Doom)”,是在开发者代码中看到的一种反模式(anti-pattern),这种异步编程方式并不明智。- Colin Toh
由于回调函数的嵌套,回调地狱 会使你的代码向右排布而不是垂直向下排版。
为了更直观的反映回调函数,这里举了一个例子。
用户资料案例 1
let user;
let friendsOfUser;
getUser(userId, function(data) {
user = data;
getFriendsOfUser(userId, function(friends) {
friendsOfUser = friends;
getUsersPosts(userId, function(posts) {
showUserProfilePage(user, friendsOfUser, posts, function() {
});
});
});
});
复制代码
Promise
Promise 是 ES2015(即俗称的 ES6)引入的一个语言特性,用来更好的处理异步操作,避免回调地狱的出现。
下例中使用 Promise 的 .then
链来解决回调地狱问题。
用户资料案例 2
let user;
let friendsOfUser;
getUser().then(data => {
user = data;
return getFriendsOfUser(userId);
}).then(friends => {
friendsOfUser = friends;
return getUsersPosts(userId);
}).then(posts => {
showUserProfilePage(user, friendsOfUser, posts);
}).catch(e => console.log(e));
复制代码
Promise 的处理方式更加干净和可读。
async/await Promise
async
/await
是一种特殊的语法,可以用更简洁的方式处理 Promise。
在 funtion
前加 async
关键字就能将函数转换成 Promise。
所有的 async 函数的返回值都是 Promise。
例子
async function add(a, b) {
return a + b;
}
add(1, 3).then(result => console.log(result));
复制代码
使用 async
/await
,可以让“用户资料案例 2”看起来更棒。
用户资料案例 3
async function userProfile() {
let user = await getUser();
let friendsOfUser = await getFriendsOfUser(userId);
let posts = await getUsersPosts(userId);
showUserProfilePage(user, friendsOfUser, posts);
}
复制代码
等等!有个问题
在“用户资料案例 3”中,如果有一个 Promise reject 了,就会抛出 Unhandled promise rejection
异常。
在此之前写的代码都没有考虑 Promise reject 的情况。未处理的 reject Promise 过去会以静默的方式失败,这可能会使调试成为噩梦。
不过现在,Promise reject 时会抛出一个错误了。
- Google Chrome 抛出的错误:
VM664:1 Uncaught (in promise) Error
- Node 抛出的错误则类似这样:
(node:4796) UnhandledPromiseRejectionWarning: Unhandled promise rejection (r ejection id: 1): Error: spawn cmd ENOENT
[1] (node:4796) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code
。
No promise should be left uncaught(每个 Promise 都要使用
.catch
处理). - Javascript
注意,“用户资料案例 2”中的 .catch
方法。如果没有写 .catch
块的话,JavaScript 会在 Promise reject 的时候抛出 Unhandled promise rejection
错误。
处理“用户资料案例 3”中的问题比较容易。只要使用 try...catch
块包装下 await
语句就能避免 Unhandled promise rejection
错误了。
用户资料案例 4
async function userProfile() {
try {
let user = await getUser();
let friendsOfUser = await getFriendsOfUser(userId);
let posts = await getUsersPosts(userId);
showUserProfilePage(user, friendsOfUser, posts);
} catch(e) {
console.log(e);
}
}
复制代码
问题解决了!
但是错误处理还可以再优雅点吗?
我怎么知道报错是来自哪一个异步请求的呢?
可以在异步请求上使用 .catch
方法来处理错误。
用户资料案例 5
let user = await getUser().catch(e => console.log('Error: ', e.message));
let friendsOfUser = await getFriendsOfUser(userId).catch(e => console.log('Error: ', e.message));
let posts = await getUsersPosts(userId).catch(e => console.log('Error: ', e.message));
showUserProfilePage(user, friendsOfUser, posts);
复制代码
上面的解决方案将处理来自请求的单个错误,但是会混合使用多种模式。应该有一种更干净的方法来使用 async
/await
而不使用 .catch
方法(嗯,如果你不介意的话,可以这样做)。
我的更好的 async/await 错误处理方案
用户资料案例 6
const handle = (promise) => {
return promise
.then(data => ([data, undefined]))
.catch(error => Promise.resolve([undefined, error]));
}
async function userProfile() {
let [user, userErr] = await handle(getUser());
if(userErr) throw new Error('Could not fetch user details');
let [friendsOfUser, friendErr] = await handle(
getFriendsOfUser(userId)
);
if(friendErr) throw new Error('Could not fetch user\'s friends');
let [posts, postErr] = await handle(getUsersPosts(userId));
if(postErr) throw new Error('Could not fetch user\'s posts');
showUserProfilePage(user, friendsOfUser, posts);
}
复制代码
这里使用了一个工具函数 handle
,如此就可以避免 Unhandled promise rejection
报错,还能细粒度的处理错误。
解释
handle
函数接受一个 Promise 对象作为参数,并总是 resolve 它,以 [data|undefined, Error|undefined]
的形式返回结果。
- 如果 Promise resolve 了,
handle
函数返回[data, undefined]
; - 如果 Promise reject 了,
handle
函数返回[undefined, Error]
。
类似的解决方案
- 如何更轻松的处理
async
/await
中的错误,Jesse Warden - NPM 包 - await-to-js
结论
async
/await
的语法很简洁,但你还是要处理异步函数里的抛出的错误。
除非你实现了自定义错误类(custom error classes),否则很难处理 Promise.then
链中的 .catch
错误处理。
使用 handle
工具函数,我们可以避免 Unhandled promise rejection
报错,还能细粒度的处理错误。
(正文完)
广告时间(长期有效)
我有一位好朋友开了一间猫舍,在此帮她宣传一下。现在猫舍里养的都是布偶猫。如果你也是个爱猫人士并且有需要的话,不妨扫一扫她的【闲鱼】二维码。不买也不要紧,看看也行。
(完)
Vue实战中的一些小魔法
作者:EasyMoment23
juejin.cn/post/6966495817792389150
路由懒加载可以让我们的包不需要一次把所有的页面的加载进来,只加载当前页面的路由组件就行。
举个🌰,如果这样写,加载的时候会全部都加载进来。
const router = new VueRouter({ routes:[ { path: '/', name: 'Home', component: Home }, { path: '/about', name: 'About', component: About } ]})
所以,应该避免上面的写法,尽量使用懒加载
懒加载写法, 结合 webpack 的 import 食用
const router = new VueRouter({ routes:[ { path: '/', name: 'Home', component: () => import(/* webpackChunkName: "home" */ '../views/Home.vue') }, { path: '/about', name: 'About', component: () => import(/* webpackChunkName: "about" */ '../views/About.vue') } ]})
应该所有同学都知道,vue 初始化的时候会将 data 里面的数据都搞成响应式数据吧。但是,我们在写业务逻辑的时候会有些数据一初始化就永远不会改变,它根本就不需要被 vue 做成响应式数据,因此我们应该将这些不用改变的数据通过 Object.freeze 方法冻结它,避免 vue 初始化的时候,做一些无用的操作。
🌰
export default { data:()=>({ list:Object.freeze([{title:'我永远不需要改变,我不需要响应式'}]) })}
异步组件可以让我们在需要一些组件时才将它加载进来,而不是一初始化就加载进来,这跟路由懒加载时一个概念。
🌰
export default { components:{ AsyncComponent:()=>import(/* webpackChunkName: "AsyncComponent" */ './Async') }}
异步组件还有一种比较完善的写法
🌰
export default { components:{ AsyncComponent:()=>({ component:import(/* webpackChunkName: "AsyncComponent" */ './Async'), delay:200, // 延迟几毫秒,默认200 timeout:3000, // 加载几毫米之后就超时,触发error组件 loading:LoadingComponent, // 组件未加载回来前显示 error:ErrorComponent // 组件超时时显示 }) }}
我猜还有很多同学,在 computed 属性中通过 this.xxx 去拿 data 里面的数据,和 methods 里面的方法吧,或许还会通过 this.route 去获取路由里面的数据吧。其实,我们可以避免这些丑陋的 this, 它甚至会给我们带来看不见的性能问题。实现上,我们通过 this 能访问到的数据,在 computed 的第一个参数上都能结构出来。
🌰
export default { haha({$attrs,$route,$store,$listeners,$ref}){ // 还能结构很多属性,可自行打印康康 return }}
为什么要避免 v-if 和 v-for 在同一个元素上同时使用呢?因为在 vue 的源码中有一段代码时对指令的优先级的处理,这段代码是先处理 v-for 再处理 v-if 的。所以如果我们在同一层中一起使用两个指令,会出现一些不必要的性能问题,比如这个列表有一百条数据,再某种情况下,它们都不需要显示,当 vue 还是会循环这个 100 条数据显示,再去判断 v-if,因此,我们应该避免这种情况的出现。
不好的🌰
<h3 v-if="status" v-for="item in 100" :key="item">{{item}}</h3>
好的🌰
<template v-if="status" > <h3 v-for="item in 100" :key="item">{{item}}</h3> </template>
如果你想要在父组件控制一个子组件的显示隐藏,是不是还在传一个 prop 和一个自定义方法,这样会很麻烦,不妨试一试 sync 修饰符。
🌰
// 父组件 template> <div> <Toggle :show.sync = 'show'></Toggle> </div></template>//Toggle 组件<template> <div> <div v-if="show"> 展示和隐藏组件 </div> <button @click="test">隐藏组件</button> </div></template><script>export default { props:['show'], methods: { test(){ this.$emit('update:show',false) } }}</script>
listeners 可能很多同学没怎么去使用,其实它们让我们对一些组件库的组件二次封装,非常好用的。
简单介绍一下它们两个:
attr 里面。
listeners 里面。
这里举一个对 ElementUI 的 Tabel 组件简单的二次封装的🌰
<el-table v-bind="$attrs" v-on="$listeners"> <template v-for="item in column"> <el-table-column v-bind="item" /> </template></el-table><script>export default { props:{ column:{ type:Array, required:true } }}<script>
v-model 上有 3 个比较好用的修饰符不知到大家有没有用过,一个是 lazy, 一个是 number, 一个是 trim。
lazy: 可以将 @input 事件变成 @blur 事件
number:只能输入数字值
trim: 清空两边的空格
🌰
//lazy <input v-model.lazy="msg" /> //number <input v-model.number="msg" /> //trim <input v-model.trim="msg" />
如果想在一个自定义的 Input 组件上使用 v-model,那么就要在子组件,介绍一个 value,和触发 input 事件,v-model 的默认语法糖就是这两个东西的组合。
🌰
// 父组件<template> <div> <CustomInput v-model='msg' /> </div></template>//CustomInput<template> <div> <input type="text" :value="value" @input="test"> </div></template><script>export default { props:['value'], methods: { test(e){ this.$emit('input',e.target.value) } },}</script>
但是,如果组件里面不是 input, 而是一个 checkbox 或者一个 radio 呢? 我不想接受一个 value 和 input 事件,我想接收一个更加语义化的 checked 和 change 事件,那该怎么办?
🌰
// 父组件不需改变...//CustomInput<template> <div> <input type="checkbox" :checked="checked" @change="test"> </div></template><script> props:['checked'], model:{ props:'checked', event:'change' }, methods: { test(e){ this.$emit('change',e.target.checked) } }}</script>
有些时候我们在操作一下页面的滚动行为,那么我们第一时间就会想到 scrollTop。其实我们还有第二个选择就是 VueRouter 给我们提供的 scrollBehavior 钩子。
🌰
const router = new VueRouter({ routes:[...] , scrollBehavior(to,from,position){ // position参数可自行打印康康,点击浏览器左右箭头会触发 return{ // 这里可以返回很多参数,下面简单列就几个,详情自己康康官网 x:100, y:100, selector:#app, offset:200, //等等 } }})
有时候我们想在子组件上面监听一些事件,比如 click,但是不论你怎么点,它都没反应,为什么呢?
🌰
<template> <div> <Child @click="test"></Child> </div></template><script> methods:{ test(){} }</script>
因为这样写 vue 会认为,你自定义了一个 click 事件,要在子组件通过 $emit('click') 触发才行。如果我就是要在父组件触发呢?那就要用到 native 修饰符了。
🌰
<template> <div> <Child @click.native="test"></Child> </div></template><script> methods:{ test(){} }</script>
keep-alive 可以帮助我们在切换组件的时候,保留上一个组件不被销毁,它在管理后台系统中比较常用。
🌰
<keep-alive> <router-view></router-view></keep-alive>
推荐阅读:
* [大文件上传如何做断点续传](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247499152&idx=1&sn=daed0c35ae7d63dd342b16b31b89cda2&chksm=97c6603ea0b1e9285f7ded75e0cadf6436281e1f06dfec6c0414f8b5b3cecfcb3f202ba6c629&scene=21#wechat_redirect)
* [微信:5 月 20 日后不再提供小程序打开 App 服务](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247498526&idx=1&sn=debbfcaa6ec256ae7d64429900cac0f0&chksm=97c666b0a0b1efa682e74936bda666370575886ef37f0ca731f86e18398435181492052f5e0d&scene=21#wechat_redirect)
* [一文搞懂单点登录三种情况的实现方式](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247498639&idx=1&sn=1386ea609c31345e380f21f12cb4dc05&chksm=97c66621a0b1ef3777f00e35035e4b097404f64bc39b17fbb225b2fee9ec3eab7be2d4e5863c&scene=21#wechat_redirect)
* [终于有人把 Nginx 说清楚了,图文详解!](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247498339&idx=1&sn=6c42ff6f859d3e02c360e56b585d9816&chksm=97c667cda0b1eedbb665a83f66f8c264a257763b77a03e00c87d0d1d8373c0c1ef3b65c3b193&scene=21#wechat_redirect)
* [推荐 130 个令你眼前一亮的网站,总有一个用得着](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247498262&idx=1&sn=886534663bb8209d4d9ad32232eb3d5d&chksm=97c667b8a0b1eeaea90f83e91505d40efd4c606aca7e1f8061697f569bc91f8dba3497bc7a85&scene=21#wechat_redirect)
* [深入浅出 33 道 Vue 99% 出镜率的面试题](http://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247498262&idx=2&sn=bc792931ee337d3e7cb73bdb8fa4599e&chksm=97c667b8a0b1eeae0915411943ab1d50ca0f4847882d56020d23b4b9308021af8326e1ffb631&scene=21#wechat_redirect)
**VUE中文社区**
编程技巧 **·** 行业秘闻 **·** 技术动向
https://mp.weixin.qq.com/s?__biz=MzIyMDkwODczNw==&mid=2247499212&idx=2&sn=3dbb283696e4b32160f1e58e4807c2e8&chksm=97c66062a0b1e974f7d0e761782365ef4cc4b277a448d8f76605286070d1078e3c33f9f6228c&mpshare=1&scene=24&srcid=0530xzGWm6pcQx7fwzTtvJop&sharer_sharetime=1622338134817&sharer_shareid=bc6a9591e78ea361aa62f4212113d179#rd
用生动有趣的 emoij 美化你的 commit log
周日了,就不甩长篇大论的技术文章了,来点轻松有趣的吧。
先来看看下面仓库的 commit log
(来自 Antd
仓库),使用了很多 emoij
表情。
并不是程序员喜欢故意卖萌,而是添加了 emoji
表情的提交记录真的能包含很多有用信息,而且阅读体验非常棒。
使用效果
对的,每个 emoji
都是有自己具体含义的,比如下面常用的几个:
-
🎉:初次提交
-
🎨:改进代码结构/代码格式
-
🐛:修复bug
-
...
下面是我自己的仓库的提交情况:
使用方法
每个 emoji
都有自己对应的代码,你只需要在编写 commit log
时带上这个代码就可以了,比如:
-
🎉:初次提交 对应代码:
:tada:
-
🎨:改进代码结构/代码格式 对应代码:
:art:
-
🐛:修复bug 对应代码:
:bug:
-
...
使用参考
初次使用当然记不住这么多代码和含义啦,大家如果要使用的话可以参考下面的网站:
https://gitmoji.carloscuesta.me/
网站中详细列举了所有 emoji
对应的含义,你可以自行选择取舍。
如果你有好的建议,或新的好玩的 emoji
欢迎贡献给官方。
https://github.com/carloscuesta/gitmoji/
参考资料
Docker · EvineDeng/jd-base Wiki
脚本可以干什么
git_pull.sh
- 自动更新 lxk0301 的京东薅羊毛脚本;
- 自动更新我的 shell 脚本;
- 自动删除失效的定时任务,并发送通知;
- 自动添加新的定时任务,并发送通知;
- 检测配置文件模板
config.sh.sample
是否升版,如有升版,发出通知; - 其他还有若干功能,查看 本文件 注释即可看到。
export_sharecodes.sh
从已经产生的日志中导出互助码,注意:是已经产生的日志。
rm_log.sh
自动按设定天数(config.sh 中设置的)删除旧日志。
jd.sh
自动按 crontab.list 设定的时间去跑各个薅羊毛脚本,需要后本脚本后面提供 js 脚本名称。
操作流程
-
安装好 docker(中文教程),然后创建容器:
注 1:如果是旁路由,建议用
--network host \
代替-p 5678:5678 \
这一行。注 2:如果想要看到 lxk0301 大佬的 js 脚本,并且重新部署也不影响自己添加的额外脚本,可以增加一行
-v / 你想存放的路径 / jd/scripts:/jd/scripts \
,不过这会增加占用约 50M 空间,并且会在创建时自动克隆 lxk0301 的 js 脚本。注 3:容器本身默认会在启动时自动启动挂机程序(就一个 jd_crazy_joy_coin),如不想自动启动,请增加一行
-e ENABLE_HANGUP=false \
。注 4:容器本身默认会在启动时自动启动控制面板,如不想自动启动,请增加一行
-e ENABLE_WEB_PANEL=false \
。注 5:如果想从 gitee 更新脚本,请使用
evinedeng/jd:gitee
镜像代替evinedeng/jd:github
。注 6:群晖和威联通用户,以及其他非 root 用户的,请在 ssh 登陆后,切换为 root 用户再运行:
sudo -i
。docker run -dit \ -v /你想存放的路径/jd/config:/jd/config `# 配置保存目录,冒号左边请修改为你想存放的路径` \ -v /你想存放的路径/jd/log:/jd/log `# 日志保存目录,冒号左边请修改为你想存放的路径` \ -p 5678:5678 \ --name jd \ --hostname jd \ --restart always \ evinedeng/jd:github
-
请在创建后使用
docker logs -f jd
查看创建日志,直到出现容器启动成功...
字样才代表启动成功(不是以此结束的请更新镜像),按Ctrl+C
退出查看日志。 -
访问
http://<ip>:5678
(ip 是指你 Docker 宿主机的局域网 ip),初始用户名:admin
,初始密码:adminadmin
,请登陆后务必修改密码,并在线编辑config.sh
和crontab.list
,其中config.sh
可以对比修改,如何修改请仔细阅读各文件注释。如未启用控制面板自动启动功能,请运行docker exec -it jd node /jd/panel/server.js
来启动,使用完控制面板后Ctrl+C
即可结束进程。如无法访问,请从防火墙、端口转发、网络方面着手解决。实在无法访问,就使用 winscp 工具 sftp 连接进行修改。 -
只有 Cookie 是必填项,其他根据你自己需要填。编辑好后,如果需要启动挂机程序(目前只有一个疯狂的 JOY 需要挂机),请重启容器:
docker restart jd
。在创建容器前 config.sh 中就有有效 Cookie 的,无需重启容器。 -
如何自动更新 Docker 容器
安装
containrrr/watchtower
可以自动更新容器,它也是一个容器,但这个容器可以监视你安装的所有容器的原始镜像的更新情况,如有更新,它将使用你原来的配置自动重新部署容器。部署containrrr/watchtower
最简单的方式如下:docker run -d \ --name watchtower \ -v /var/run/docker.sock:/var/run/docker.sock \ containrrr/watchtower
你也可以访问 https://containrrr.dev/watchtower/ 获取更详细的部署说明,包括如何避开某些容器不让它自动更新,如何发更新容器后发送通知,设置检测时间等等。
如果多容器并发
多个容器并发,建议使用 docker-compose 安装,但如果你平台无法正常安装 docker-compose,或者你不想用 docker-compose,按上述操作流程中方式部署不同名称不同映射路径的容器也是可以的,看你个人需要。
如需使用 docker-compose,请前往 这里 下载最新版本的文件,放在本机 /usr/local/bin
下,并重命名为docker-compose
。
然后参考本仓库的 docker-compose.yml 准备好你自己的docker-compose.yml
,然后部署:
## cd 到docker-compose.yml的存放路径下
docker-compose up -d
如何更新配置文件
访问http://<ip>:5678
并编辑保存好即可,其他啥也不用干,容器也不用重启。其中config.sh
改完立即生效,crontab.list
会在下一次任何定时薅羊毛任务启动时更新。config.sh
可以通过控制面板的对比工具对比修改。
如未启用控制面板自动启动功能,请运行docker exec -it jd node /jd/panel/server.js
来启动,使用完控制面板后Ctrl+C
即可结束进程。如无法访问,请从防火墙、端口转发、网络方面着手解决。
也可以不通过控制面板,而是通过 sftp 连接修改,你自己的配置文件config.sh
可对照仓库中sample/config.sh.sample
修改。
如何重置控制面板用户名和密码
docker exec -it jd bash jd resetpwd
如何添加其他脚本
本环境基于 node,所以也只能跑 js 脚本。你可以把你的后缀为.js
的脚本放在你映射的config
或映射的scripts
下即可。比如你放了个test.js
,可以在你的crontab.list
中添加如下的定时任务:
15 10 * * * bash jd test # 如果不需要准时运行或RandemDelay未设置
15 10 * * * bash jd test now # 如果设置了RandemDelay但又需要它准时运行
识别顺序:1. /jd/scripts、2. /jd/scripts/backUp、3. /jd/config
,如果一个脚本在多个目录下均存在,以先找到的为准。
如果急你就运行一下docker exec -it jd crontab /jd/config/crontab.list
更新定时任务即可,如果不急就等着程序自己添加进定时任务。
注意:在 crontab.list 中,你额外添加的任务不能以 “jd_”、“jr_”、“jx_” 开头,以 “jd_”、“jr_”、“jx_” 开头的任务如果不在https://github.com/LXK9301/jd_scripts 和 https://github.com/shylocks/Loon 这两个仓库中,那么这个任务会被删除。
其他说明:
-
如果你额外加的脚本要用到环境变量,直接在你的
config.sh
文件最下方按以下形式添加好变量即可(单引号或双引号均可):export 变量名1="变量值1" export 变量名2="变量值2" export 变量名3="变量值3"
-
如果你额外添加的脚本要用到 lxk0301 大佬仓库中的
sendNotify.js
来发送通知,或者要用到jdCookie.js
来处理 Cookie,建议你直接放在容器内的/jd/scripts
文件夹下,按以下命令复制进容器(如果没有映射/jd/scripts
出来的话,重新部署容器后要再次运行):docker cp /宿主机上脚本存放路径/test.js jd:/jd/scripts
如何手动运行脚本
-
手动 git pull 更新脚本
docker exec -it jd bash git_pull
-
手动删除指定时间以前的旧日志
docker exec -it jd bash rm_log
-
手动导出所有互助码
docker exec -it jd bash export_sharecodes
-
手动启动挂机程序(容器会在启动时立即启动挂机程序,所以你想重启挂机程序,你也可以重启容器,而不采用下面的方法。 )
docker exec -it jd bash jd hangup
然后挂机脚本就会一直运行。如需查看挂机脚本日志,请输入
docker exec -it jd pm2 monit
或docker exec -it jd pm2 logs
查看。因挂机程序日志过多,不再记录在 log 文件中。 -
手动执行薅羊毛脚本,用法如下 (其中
-it
后面的jd
为容器名,bash
后面的jd
为命令名,xxx
为 lxk0301 大佬的脚本名称),不支持直接以node xxx.js
命令运行:docker exec -it jd bash jd xxx # 如果设置了随机延迟并且当时时间不在0-2、30-31、59分内,将随机延迟一定秒数 docker exec -it jd bash jd xxx now # 无论是否设置了随机延迟,均立即运行
如果你忘记了命令也不要紧,只要你记得命令
jd
就行,输入后会提示你:
脚本名不记得也不要紧,输错了也会提示你的:
https://github.com/EvineDeng/jd-base/wiki/Docker
iPhone 为什么不加大内存? - 知乎
这实际上是一个技术问题,涉及到iOS与安卓两个操作系统的设计原理。
首先,安卓机上大内存,这是个**特色,也就是说只有**国内销售的安卓机才会上这么大的内存,海外版的安卓机主流内存基本和苹果差不了太多。图我就不贴了,大家可以自己去查一下,华为的P30,海外版的128G硬盘的型号只配4G或6G内存,而**版的P30,64G硬盘的型号却配上了8G内存;海外版的安卓机,128G硬盘配4G内存比比皆是,简直不要太多,但在国内,你几乎找不到一款128G硬盘配4G内存的安卓机。这样乍一看好像我们得了便宜,可其实,这是国产安卓生态的无奈之举。
先说国内安卓机为什么上大内存吧。举一个例子,比如我们常用的微信这个APP,首先你打开微信登录账号后,你手机上的微信APP就会和腾讯的服务器保持一个长久的连接,你发一条消息给你朋友,这条消息不是直接从你的手机上发到他的手机上的,而是你的手机将消息发给了腾讯的服务器,然后腾讯的服务器再发给你朋友。这个时候就有一个问题,如果你朋友手机上的微信APP处在关闭状态下,那么即使腾讯服务器把消息发过去了,他也收不到,因为他的手机上微信APP已经完全关闭了,已经和腾讯服务器断开连接了,只有当他再次打开微信的时候,连接恢复,他才能收到那条信息。也就是说,想要及时收到消息,那么微信这个APP就不能完全关闭,需要留下一些线程在后台一直保持运行,一直和腾讯的服务器保持接连,这样一旦收到消息,系统才能及时启动消息机制提醒你。
所以,国内安卓系统下,每一个APP,都必须在后台(也就是内存里)留下一些线程随时准备接收外来信息,也就是说你刚打开手机还啥都没干,你手机上所有APP的一部分线程已经悄悄启动,进驻内存,而这些后台启动的线程是无法关闭的,因为是合法的,关闭的话用户就无法及时收到消息。另外,不要小看这些后台线程的数量,一个APP在后台里留十几二十MB确实不多,但100个APP(64G硬盘的手机就能装100个APP)加起来就要吃掉将近2G的内存!
因此,国内安卓机的内存容量必须跟着硬盘容量递增,硬盘容量越大,能下的APP就越多,就越需要更大的内存来保证这些APP留在后台的线程正常运行。
再说iOS,它为啥就不需要大内存呢?因为苹果有自己的服务器(谷歌实际上也有自己的服务器,但国内不能用,下面会说)。iOS的这个系统,必须搭配苹果公司的服务器才能正常运行。每一台iOS设备在联网的时候,iOS系统首先会和苹果的服务器建立一个长连接。
还是微信那个例子,在iOS上,如果微信处在打开状态下,那么逻辑还是和上述安卓的一样,差距是体现在关闭微信后。如果你iPhone上的微信APP关闭了,会和腾讯的服务器断开,而这个时候,你朋友发了一条消息给你,这条消息先发给了腾讯的服务器,这时腾讯服务器发现你手机上的微信APP没有和服务器建立连接,那么它会将这条消息转头发给苹果的服务器,然后苹果的服务器收到后会发送给你的iOS系统,系统收到就能及时做出提醒。这样实际上是多了一个步骤,但带来的好处是你手机上的微信APP可以完全关闭,不需要留下多余的线程来接收消息,因为苹果的服务器会一直保持接收消息,然后将消息发给手机系统,系统再做出提醒。
这样一来,苹果手机的内存就不需要跟着硬盘容量一起递增,因为即使APP完全关闭,也不影响及时接收消息。再加上iOS系统实际上不支持真正意义上的后台运行(仅持后台听歌、下载、导航等有限几个操作,如果一个APP进入后台,10分钟内没被换到前台来,那么系统就会将它关闭,只留下临死前的一个截图,所谓的墓碑机制),所以苹果手机的这些内存绝大部分时间内其实只为一个APP(当前这个)服务,一个APP使用4G内存,真的是绰绰有余,有余到多的不能再多了。
插个题外话,都说苹果手机安全,注重隐私,也有这个原因,苹果手机上的APP,如果关闭了,那就是真的全部线程都关闭了;而安卓,由于每一个APP都必须在后台留一些线程一直运行,有的无良APP甚至手机厂商,就不仅仅会留下接收消息的线程,还会留下偷偷开启摄像头或者麦克风的线程,去收集用户数据,而这些后台的的线程,由于是系统代码层面的东西,用户根本没可能去关闭。
最后,说一下海外版安卓机为什么也不上这么大的内存,因为海外版安卓机可以使用谷歌的服务器(前段时间闹的沸沸扬扬的安卓停止对华为授权事件,实际上就是谷歌不让海外版华为连接谷歌的服务器了),而使用了谷歌的服务器,推送机制就变得和iOS一样了,就不需要有APP自己的线程留在后台了,谷歌的服务器会帮APP们接收信息。
而由于众所周知的原因,国内安卓用户是无法连接谷歌的服务器的,那么,APP要想及时接收消息,开发者也只能使用常驻后台这个方法,而让APP线程常驻在后台,需要大量的内存支持,那国内的安卓机就只好加大内存。那么,谁来为这些多出来的内存买单呢?羊毛都出在羊身上,当然还是用户,还是消费者。这就是我们国内安卓用户的悲催之处,我们不仅要忍受隐私的泄露,还要为这样的行为买单。
目前,国内的APP驻留后台已经把国内的整个安卓生态搞的乌烟瘴气、乱七八糟。其实好多大厂想过解决方案,比如小米推出自己的推送服务,华为也有自己的推送服务,但全都收效甚微。因为没什么开发者去配合他们,一来因为开发者适配的话需要一个厂一个厂去适配,提升了开发成本和维护成本,二来APP开发商也不愿放弃常驻后台带来的利益。
目前比较值得期待的,是安卓推送联盟,这个联盟是由工信部牵头的,运行机制和iOS的推送原理差不多,如果能全面铺开,可以解决上述问题,但这一套现在还在测试阶段,还是个美好的愿望,日后能不能全面铺开还是前途未卜。
所以,苹果不上大内存,真不是厨子抠门,因为没必要了,国内安卓机12G内存都打不过苹果4G内存,为什么?因为12G内存里,很大一部分是留给国产APP常驻后台用的。
就是这样。
CSS 奇思妙想边框动画
今天逛博客网站 -- shoptalkshow,看到这样一个界面,非常有意思:
觉得它的风格很独特,尤其是其中一些边框。
嘿嘿,所以来一篇边框特辑,看看运用 CSS,可以在边框上整些什么花样。
border 属性
谈到边框,首先会想到 border
,我们最常用的莫过于 solid
,dashed
,上图中便出现了 dashed
。
除了最常见的 solid
,dashed
,CSS border 还支持 none
,hidden
, dotted
, double
, groove
, ridge
, inset
, outset
等样式。除去 none
,hidden
,看看所有原生支持的 border 的样式:
基础的就这些,如果希望实现一个其他样式的边框,或者给边框加上动画,那就需要配合一些其他属性,或是脑洞大开。OK,一起来看看一些额外的有意思的边框。
边框长度变化
先来个比较简单的,实现一个类似这样的边框效果:
这里其实是借用了元素的两个伪元素。两个伪元素分别只设置上、左边框,下、右边框,通过 hover
时改变两个伪元素的高宽即可。非常好理解。
div {
position: relative;
border: 1px solid #03A9F3;
&::before,
&::after {
content: "";
position: absolute;
width: 20px;
height: 20px;
}
&::before {
top: -5px;
left: -5px;
border-top: 1px solid var(--borderColor);
border-left: 1px solid var(--borderColor);
}
&::after {
right: -5px;
bottom: -5px;
border-bottom: 1px solid var(--borderColor);
border-right: 1px solid var(--borderColor);
}
&:hover::before,
&:hover::after {
width: calc(100% + 9px);
height: calc(100% + 9px);
}
}
CodePen Demo -- width border animation
接下来,会开始加深一些难度。
虚线边框动画
使用 dashed
关键字,可以方便的创建虚线边框。
div {
border: 1px dashed #333;
}
当然,我们的目的是让边框能够动起来。使用 dashed
关键字是没有办法的。但是实现虚线的方式在 CSS 中有很多种,譬如渐变就是一种很好的方式:
div {
background: linear-gradient(90deg, #333 50%, transparent 0) repeat-x;
background-size: 4px 1px;
background-position: 0 0;
}
看看,使用渐变模拟的虚线如下:
好,渐变支持多重渐变,我们把容器的 4 个边都用渐变表示即可:
div {
background:
linear-gradient(90deg, #333 50%, transparent 0) repeat-x,
linear-gradient(90deg, #333 50%, transparent 0) repeat-x,
linear-gradient(0deg, #333 50%, transparent 0) repeat-y,
linear-gradient(0deg, #333 50%, transparent 0) repeat-y;
background-size: 4px 1px, 4px 1px, 1px 4px, 1px 4px;
background-position: 0 0, 0 100%, 0 0, 100% 0;
}
效果如下:
OK,至此,我们的虚线边框动画其实算是完成了一大半了。虽然 border-style: dashed
不支持动画,但是渐变支持呀。我们给上述 div 再加上一个 hover
效果,hover
的时候新增一个 animation
动画,改变元素的 background-position
即可。
div:hover {
animation: linearGradientMove .3s infinite linear;
}
@keyframes linearGradientMove {
100% {
background-position: 4px 0, -4px 100%, 0 -4px, 100% 4px;
}
}
OK,看看效果,hover 上去的时候,边框就能动起来,因为整个动画是首尾相连的,无限循环的动画看起来就像是虚线边框在一直运动,这算是一个小小的障眼法或者小技巧:
这里还有另外一个小技巧,如果我们希望虚线边框动画是从其他边框,过渡到虚线边框,再行进动画。完全由渐变来模拟也是可以的,如果想节省一些代码,使用 border
会更快一些,譬如这样:
div {
border: 1px solid #333;
&:hover {
border: none;
background:
linear-gradient(90deg, #333 50%, transparent 0) repeat-x,
linear-gradient(90deg, #333 50%, transparent 0) repeat-x,
linear-gradient(0deg, #333 50%, transparent 0) repeat-y,
linear-gradient(0deg, #333 50%, transparent 0) repeat-y;
background-size: 4px 1px, 4px 1px, 1px 4px, 1px 4px;
background-position: 0 0, 0 100%, 0 0, 100% 0;
}
}
由于 border 和 background 在盒子模型上位置的差异,视觉上会有一个很明显的错位的感觉:
要想解决这个问题,我们可以把 border
替换成 outline
,因为 outline
可以设置 outline-offset
。便能完美解决这个问题:
div {
outline: 1px solid #333;
outline-offset: -1px;
&:hover {
outline: none;
}
}
最后看看运用到实际按钮上的效果:
上述 Demo 完整代码如下:
CodePen Demo -- dashed border animation
其实由于背景和边框的特殊关系,使用 border 的时候,通过修改
background-position
也是可以解决的,就是比较绕。关于背景和边框的填充关系,可以看这篇文章:条纹边框的多种实现方式
渐变的其他妙用
利用渐变,不仅仅只是能完成上述的效果。
我们继续深挖渐变,利用渐变实现这样一个背景:
div {
position: relative;
&::after {
content: '';
position: absolute;
left: -50%;
top: -50%;
width: 200%;
height: 200%;
background-repeat: no-repeat;
background-size: 50% 50%, 50% 50%;
background-position: 0 0, 100% 0, 100% 100%, 0 100%;
background-image: linear-gradient(#399953, #399953), linear-gradient(#fbb300, #fbb300), linear-gradient(#d53e33, #d53e33), linear-gradient(#377af5, #377af5);
}
}
注意,这里运用了元素的伪元素生成的这个图形,并且,宽高都是父元素的 200%
,超出则 overflow: hidden
。
接下来,给它加上旋转:
div {
animation: rotate 4s linear infinite;
}
@keyframes rotate {
100% {
transform: rotate(1turn);
}
}
看看效果:
最后,再利用一个伪元素,将中间遮罩起来,一个 Nice 的边框动画就出来了 (动画会出现半透明元素,方便示意看懂原理):
上述 Demo 完整代码如下,这个效果我最早见于这位作者 -- Jesse B
CodePen Demo -- gradient border animation
改变渐变的颜色
掌握了上述的基本技巧之后,我们可以再对渐变的颜色做一些调整,我们将 4 种颜色变成 1 种颜色:
div::after {
content: '';
position: absolute;
left: -50%;
top: -50%;
width: 200%;
height: 200%;
background-color: #fff;
background-repeat: no-repeat;
background-size: 50% 50%;
background-position: 0 0;
background-image: linear-gradient(#399953, #399953);
}
得到这样一个图形:
同样的,让它旋转一起,一个单色追逐的边框动画就出来了:
CodePen Demo -- gradient border animation 2
Wow,很不错的样子。不过如果是单线条,有个很明显的缺陷,就是边框的末尾是一个小三角而不是垂直的,可能有些场景不适用或者 PM 接受不了。
那有没有什么办法能够消除掉这些小三角呢?有的,在下文中我们再介绍一种方法,利用 clip-path
,消除掉这些小三角。
conic-gradient
的妙用
再介绍 clip-path
之前,先讲讲角向渐变。
上述主要用到了的是线性渐变 linear-gradient
。我们使用角向渐变 conic-gradient
其实完全也可以实现一模一样的效果。
我们试着使用 conic-gradient
也实现一次,这次换一种暗黑风格。核心代码如下:
.conic {
position: relative;
&::before {
content: '';
position: absolute;
left: -50%;
top: -50%;
width: 200%;
height: 200%;
background: conic-gradient(transparent, rgba(168, 239, 255, 1), transparent 30%);
animation: rotate 4s linear infinite;
}
}
@keyframes rotate {
100% {
transform: rotate(1turn);
}
}
效果图和示意图如下,旋转一个部分角向渐变的图形,中间的部分使用另外一个伪元素进行遮罩,只漏出线条部分即可:
CodePen Demo -- Rotating border 3
clip-path
的妙用
又是老朋友 clip-path
,有意思的事情它总不会缺席。
clip-path
本身是可以进行坐标点的动画的,从一个裁剪形状变换到另外一个裁剪形状。
利用这个特点,我们可以巧妙的实现这样一种 border 跟随效果。伪代码如下:
div {
position: relative;
&::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 2px solid gold;
animation: clippath 3s infinite linear;
}
}
@keyframes clippath {
0%,
100% {
clip-path: inset(0 0 95% 0);
}
25% {
clip-path: inset(0 95% 0 0);
}
50% {
clip-path: inset(95% 0 0 0);
}
75% {
clip-path: inset(0 0 0 95%);
}
}
效果图与示意图一起:
CodePen - clip-path border animation
这里,因为会裁剪元素,借用伪元素作为背景进行裁剪并动画即可,使用 clip-path
的优点了,切割出来的边框不会产生小三角。同时,这种方法,也是支持圆角 border-radius
的。
如果我们把另外一个伪元素也用上,实际实现一个按钮样式,可以得到这样的效果:
CodePen - clip-path border animation 2
overflow
的妙用
下面这个技巧利用 overflow 实现。实现这样一个边框动画:
为何说是利用 overflow
实现?
贴个示意图:
CodePen Demo -- 巧用 overflow 及 transform 实现线条 hover 效果
两个核心点:
- 我们利用
overflow: hidden
,把原本在容器外的一整个元素隐藏了起来 - 利用了
transform-origin
,控制了元素的旋转中心
发现没,其实几乎大部分的有意思的 CSS 效果,都是运用了类似技巧:
简单的说就是,我们看到的动画只是原本现象的一小部分,通过特定的裁剪、透明度的变化、遮罩等,让我们最后只看到了原本现象的一部分。
border-image
的妙用
利用 border-image
,我们也可以实现一些有意思的边框动画。关于 border-image
,有一篇非常好的讲解文章 -- border-image 的正确用法,本文不对基本定义做过多的讲解。
如果我们有这样一张图:
便可以利用 border-image-slice
及 border-image-repeat
的特性,得到类似的边框图案:
div {
width: 200px;
height: 120px;
border: 24px solid;
border-image: url(image-url);
border-image-slice: 32;
border-image-repeat: round;
}
在这个基础上,可以随便改变元素的高宽,如此便能扩展到任意大小的容器边框中:
CodePen Demo -- border-image Demo
接着,在这篇文章 -- How to Animate a SVG with border-image 中,还讲解了一种利用 border-image
的边框动画,非常的酷炫。
与上面例子不一样的是,我们只需要让我们的图案,动起来,就是我们需要这样一个背景图(掘金不支持 SVG 动图,原图地址):
那么,我们也就能得到运动的边框图,代码完全一样,但是,边框是运动的:
CodePen Demo -- Dancing Skull Border
border-image
使用渐变
border-image
除了贴图引用 url
之外,也是可以直接填充颜色或者是渐变的。
之前也有一篇关于 border-image
的文章 -- 巧妙实现带圆角的渐变边框
我们可以利用 border-image
+ filter
+ clip-path
实现渐变变换的圆角边框:
.border-image-clip-path {
width: 200px;
height: 100px;
border: 10px solid;
border-image: linear-gradient(45deg, gold, deeppink) 1;
clip-path: inset(0px round 10px);
animation: huerotate 6s infinite linear;
filter: hue-rotate(360deg);
}
@keyframes huerotate {
0% {
filter: hue-rotate(0deg);
}
100% {
filter: hue-rotate(360deg);
}
}
CodePen Demo -- clip-path、border-image 加 filter 实现圆角渐变边框
最后
本文介绍了一些我认为比较有意思的边框动画小技巧,当然 CSS 产生还有非常多有意思的效果,限于篇幅,不一一展开。
本文到此结束,希望对你有帮助 :),想 Get 到最有意思的 CSS 资讯,千万不要错过我的 iCSS 公众号 😄
更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。
如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。
https://juejin.cn/post/6918921604160290830#heading-10
【js】手动实现一个es6模板字符串 | 累了歇一歇,保肝护肝人人做起
首先写两个字符串,然后使用 es6 模板字符串试一下
let obj = {
name: 'lily',
age: 90
}
let str1 = `${obj.name}的年龄是${obj.age}`;
console.log(str1);
写一个普通字符串,内容和模板字符串一样
let obj = {
name: 'lily',
age: 90
}
let str2 = '${obj.name}的年龄是${obj.age}';
创建一个命名函数replacefunc
,用于匹配到对应的模板,并进行替换。
在replacefunc
函数内创建一个正则//
,对需要匹配的${
和}
进行匹配,并添加转译,在花括号中间添加分组()
,对不能出现结束的花括号进行匹配[^}]
,^
为取反,并至少有一个字符+
,添加全局匹配g
。
后指定字符串方法replace
的第二个函数参数。
match 为正则匹配到的字符串,
key 为分组匹配到的字符串,
循环两次是因为匹配到两次符合正则的字符串。
最后则用eval()
还原,取到对应作用域内的变量。
let obj = {
name: 'lily',
age: 90
}
let str2 = '${obj.name}的年龄是${obj.age}';
function replacefunc(desc) {
return desc.replace(/\$\{([^}]+)\}/g, function (match, key) {
console.log(match);
console.log(key);
console.log(eval(key));
return eval(key);
})
}
console.log(replacefunc(str2));
用途:可以在模板引擎中用到。
本文作者: haise
本文地址: https://www.shifeng1993.com/2019/03/05/js_temp_str/
版权声明: 转载请注明出处!
https://www.shifeng1993.com/2019/03/05/js_temp_str/
一份超级详细的Vue-cli3.0使用教程 | 前端进阶积累
一份超级详细的Vue-cli3.0使用教程
在vue-cli 2.X的时候,也写过一篇类似的文章,在八月份的时候vue-cli已经更新到了3.X,新版本的脚手架,功能灰常强大,试用过后非常喜欢,写篇教程来帮助各位踩一下坑。
主要内容:
- 零配置启动/打包一个
.vue
文件 - 详细的搭建过程
- 重点推荐:使用图形化界面创建/管理/运行项目
安装:
卸载旧版本:
如果你事先已经全局安装了旧版本的vue-cli
(1.x 或 2.x),你需要先卸载它:
npm uninstall vue-cli -g
Node版本要求:
3.x需要在Node.js
8.9或更高版本(推荐8.11.0+),点击这里可以安装node
大多数人都安装过了node,使用下面的命令行查询你的node版本:
node -v
如果你的版本不够,可以使用下面的命令行来把Node版本更新到最新的稳定版:
npm install -g n // 安装模块 这个模块是专门用来管理node.js版本的
n stable // 更新你的node版本
mac下,更新版本的时候,如果提示你权限不够:
sudo n stable // 我就遇到了
安装vue-cli:
npm install -g @vue/cli // 安装cli3.x
vue --version // 查询版本是否为3.x
如果cli3.x用的不舒服,cli3也能使用2.x模板:
npm install -g @vue/cli-init // 安装这个模块
// 就可以使用2.x的模板:vue init webpack my-project
零配置启动/打包一个.vue
文件:
安装扩展:
npm install -g @vue/cli-service-global
安装完扩展之后,可以随便找个文件夹建一个如下方示例的.vue文件,然后跑起来:
vue serve App.vue // 启动服务
vue build App.vue // 打包出生产环境的包并用来部署
如下图,只需一个.vue文件,就能迅速启动一个服务:
如图所示,服务启动的时候回生成一个node_modules
包,稍微测试了一下,服务支持ES6语法和热更新,打包的时候会生成一个dist
文件夹。(新建一个test.vue文件也只有一个node_modules
/dist
文件夹)
这是个很棒的功能,用于开发一个库、组件,做一些小demo等都是非常适合的!
第一次创建项目:
1. 命令行:
vue create hello-cli3
-
hello-cli3是文件夹名字,如果不存在会自动创建文件夹,如果存在会安装到那个文件夹中。
-
相比2.x的时候需要自己手动创建一个文件夹,这里也算是一个小优化吧。
2. 选择模板:
-
一开始只有两个选项:
default
(默认配置)和Manually select features
(手动配置)默认配置只有
babel
和eslint
其他的都要自己另外再配置,所以我们选第二项手动配置。 -
在每次选择手动配置之后,会询问你是否保存配置,也就是图片中的
koro
选项,这样以后我们在进行创建项目的时候只需使用原先的配置就可以了,而不用再进行配置。
3. 选择配置:
-
根据你的项目需要来选择配置,空格键是选中与取消,A键是全选
? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection) // 检查项目所需的功能:(按<space>选择,<a>切换所有,<i>反转选择) >( ) TypeScript // 支持使用 TypeScript 书写源码 ( ) Progressive Web App (PWA) Support // PWA 支持 ( ) Router // 支持 vue-router ( ) Vuex // 支持 vuex ( ) CSS Pre-processors // 支持 CSS 预处理器。 ( ) Linter / Formatter // 支持代码风格检查和格式化。 ( ) Unit Testing // 支持单元测试。 ( ) E2E Testing
4. 选择css预处理器:
-
如果你选择了Css预处理器选项,会让你选择这个
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): // 选择CSS预处理器(默认支持PostCSS,Autoprefixer和CSS模块): > SCSS/SASS LESS Stylus
5. 是否使用路由的history
模式:
-
这里我建议选No,这样打包出来丢到服务器上可以直接使用了,后期要用的话,也可以自己再开起来。
-
选yes的话需要服务器那边再进行设置。
Use history mode for router? (Requires proper server setup for index fallback in production) // 路由使用history模式?(在生产环境中需要适当的服务器设置以备索引)
6. 选择Eslint代码验证规则:
> ESLint with error prevention only
ESLint + Airbnb config
ESLint + Standard config
ESLint + Prettier
7. 选择什么时候进行代码规则检测:
-
建议选保存就检测,等到commit的时候,问题可能都已经积累很多了。
-
之前写了篇VsCode保存时自动修复Eslint错误推荐一下。
? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection) >( ) Lint on save // 保存就检测 ( ) Lint and fix on commit // fix和commit时候检查
8. 选择e2e测试:
? Pick a E2E testing solution: (Use arrow keys)
❯ Cypress (Chrome only)
Nightwatch (Selenium-based)
9. 把babel,postcss,eslint这些配置文件放哪:
-
通常我们会选择独立放置,让package.json干净些
? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? (Use arrow keys) > In dedicated config files // 独立文件放置 In package.json // 放package.json里
10. 是否保存配置:
Save this as a preset for future projects? (Y/n) // 是否记录一下以便下次继续使用这套配置
// 选保存之后,会让你写一个配置的名字:
Save preset as: name // 然后你下次进入配置可以直接使用你这次的配置了
11. 下载依赖
12. webpack配置的目录不见了:
一起来看一下新项目的结构(下图),会发现2.x的webpack配置的目录不见了,也就是没有build、config这两个文件夹了:
-
这种方式的优势对小白来说非常友好,不会一上来就两个文件夹,一堆文件,看着脑袋都大了。
-
然后在引用
抄别人的配置的时候,也非常方便,直接将文件复制过来就好了。 -
在自定义一下webpack的配置,我们需要在根目录新建一个
vue.config.js
文件,文件中应该导出一个对象,然后进行配置,详情查阅官方文档// vue.config.js module.exports = { // 选项... }
-
还有一些小变动像:static文件夹改为public了,router文件夹变成了单个文件之类的(我之前一直这么做,嘿嘿)。
13.启动项目:
使用图形化界面创建/管理/运行项目:
启动图形化界面
vue ui
创建项目和导入项目:
项目管理:
当我们点击hello -cli3项目,就会进入项目管理的界面
1. 仪表盘:
2. vue-cli3.x插件:
-
vue-cli3的插件功能,详情了解官方文档
-
cli3插件安装的过程:
3. 项目依赖
4. 项目配置
5. 任务:
-
serve 运行项目,点击直接运行,再也不用输入命令了!
-
可以清楚的看到各个模块用了多久,方便我们针对性的进行优化:
-
build 打包项目:这里主要展示了图表的功能,比以前2.x生成报告更加直观,超级棒!
6. 其他
结语
可以说很认真了,希望大家看完能够有些收获,赶紧试试新版的vue-cli吧!
以上2018.11.10
参考资料:
点个Star支持我一下~
最后更新时间: 8/2/2019, 12:38:18 PM
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.