Loading...

Node Sass 报错:Error: Node Sass does not yet support your current environment: OS X 64-bit with Unsupported

MILLEROS 发布于 2020-03-17

## 前言 最近在对博客代码进行更新时,遇到Node Sass的编译报错,一直找不到原因: ![bc615b61-3e9a-4899-900f-21860af1787e.jpg](http://qiniu.milleros.com/20-03-17/bc615b61-3e9a-4899-900f-21860af1787e.png) 一开始在网上找了很多方法,其中包括: ```js npm rebuild node-sass ``` 但是并没有什么用,还是出现了`make`错误: ![make错误](http://qiniu.milleros.com/20-03-17/4dae1814-9e9d-4753-b1f0-0e14847b91c9.png) 可以看到我这里`node`版本是`v12.16.1`,根据日志看,总感觉是这个`node-gyp`模块不支持这么高版本的node,所以我采用了下面的方法。 ## 解决办法 根据上面说的,我怀疑是`node-gyp`和高版本的node不兼容,那我就降级好了 ### 使用n模块查一下11版本 ```bash $ n lsr 11 11.15.0 11.14.0 11.13.0 11.12.0 11.11.0 11.10.1 11.10.0 11.9.0 11.8.0 11.7.0 11.6.0 11.5.0 11.4.0 11.3.0 11.2.0 11.1.0 11.0.0 ``` 干脆降级到11的最新版本算了 ### 降级到node v11.15.0 ```bash $ sudo n install 11.15.0 ``` 安装成功后,切换到11.15.0 ```bash $ n use 11.15.0 $ node -v v11.15.0 ``` ## 重装模块 ```bash $ rm -rf ./node_modules $ npm install ``` 事情解决了!!! 看来`要用就用最新`还是风险很大的 ## 参考资料 除了这个暴力方法,如果你不想降级node的话,可以尝试一下这里面的方法 [gyp ERR! stack Error: `make` failed with exit code: 2](https://github.com/nodejs/node-gyp/issues/1694 "gyp ERR! stack Error: `make` failed with exit code: 2")
继续阅读

FINDING YOUR PASSION

MILLEROS 发布于 2020-03-08

>这篇文章是中英文对照翻译版本,原文为英文,中文为本人(Milleros)自行翻译。 ## 前言 **Remember back when you were a kid? You would just do things. You never thought to yourself, "What are the relative merits of learning baseball versus football?" You just ran around the playground and played baseball and football. You built sand castles and played tag and asked silly questions and looked for bugs and dug up grass and pretended you were a sewer monster.** 还能回想起来小时候的你吗?那些你做过的事情。那时的你可不会去思考“学习棒球和橄榄球哪个更好?”你只是在操场上跑来跑去打着橄榄球,打着棒球。堆沙堡、老鹰捉小鸡、问傻问题、捉虫子、拔草抑或是假装自己是个污污怪。 >**tag** 又称为it, tig, tiggy, tips, tick, chasey 或者 touch and go,类似于国内的老鹰抓小鸡游戏,通过触碰玩家使其出局 >**sewer monster** 《下水道怪物》,一部卡通片 **Nobody told you to do it, you just did it. You were led merely by your curiosity and excitement.** 没有人教过你这些游戏,驱使你的只是你的好奇心罢了。 **And the beautiful thing was, if you hated baseball, you just stopped playing it. There was no guilt involved. There was no arguing or debate. You either liked it or you didn't.** 在这之中,最美好的地方莫过于:你讨厌橄榄球,那你可以随心所欲的选择不去碰它,你不带有任何的愧疚;也不带有任何的疑虑和矛盾,只是你喜欢与否。 **And if you loved looking for bugs, you just did that. There was no second-level analysis of, "Well, is looking for bugs really what I should be doing with my time as a child? Nobody else wants to look for bugs, does that mean there's something wrong with me? How will looking for bugs affect my future prospects?"** 你做着你喜欢的事,更不会去考虑“Emmm,捉虫子确定是我现在应该干的吗?为什么其他人都不喜欢抓虫子,这是不是意味着我有问题?这会影响我未来的路吗?” **There was no bullshit. If you liked something, you just did it.** 明白了吧!这里面没有任何的狗屁道理,你只是喜欢这么干而已。 ## "HOW DO I FIND MY PASSION?" 我如何找到我生活的热情 **Today, I received approximately the 11,504th email this year from a person telling me that they don't know what to do with their life. And like all of the others, this person asked me if I had any ideas of what they could do, where they could start, where to "find their passion."** 今天我收到了今年以来大概第11504封邮件,来自一位不知道自己生活该何去何从的人。和其他人一样,他很迫切的希望知道我是否对他们有什么建议,他们能从什么地方开始,获得他们对生活的激情。 **And of course, I didn't respond. Why? Because I have no fucking clue. If you don't have any idea what to do with yourself, what makes you think some jackass with a website would? I'm a writer, not a fortune teller.** 像往常一样,我并没有回复他。为什么?因为我也没有任何线索,如果一个人自己都不知道自己要干什么,那你凭什么相信一个只有他自己网站的蠢驴(作者自嘲)会知道?我清楚的知道自己是个作家,不是个算命先生。 **But more importantly, what I want to say to these people is this: that's the whole point — "not knowing" is the whole fucking point. Life is all about not knowing, and then doing something anyway. All of life is like this. All of it. And it's not going to get any easier just because you found out you love your job cleaning septic tanks or you scored a dream gig writing indie movies.** 总体来说,我最想对这些人说的就是:"未知"就是重点,生活本来就是在未知中探索,任何人究其一生都是,它不会因为你发现自己喜欢清理化粪池的工作,或是得到了一份写电影的梦想工作而变得容易。 **The common complaint among a lot of these people is that they need to "find their passion."** 几乎所有人都抱怨,“找到生活的激情”是他们起步前的唯一目标。 **I call bullshit. You already found your passion, you're just ignoring it. Seriously, you're awake 16 hours a day, what the fuck do you do with your time? You're doing something, obviously. You're talking about something. There's some topic or activity or idea that dominates a significant amount of your free time, your conversations, your web browsing, and it dominates them without you consciously pursuing it or looking for it.** 胡扯,其实你已经找到了你的激情,只是你忽略了它的存在而已。仔细想想,你每天十六个小时都是醒着的,那你每天都会干些什么呢?你肯定会做些什么事情,最明显的,你会和别人聊些什么。可能是一些热点话题、或者是某些活动和想法占据了你大量的时间,抑或是你任何形式的交谈、网页浏览,这些简简单单的事情都主导者你的热情,而无需你主动去追求或寻找(你做这些事的热情)。 **It's right there in front of you, you're just avoiding it. For whatever reason, you're avoiding it. You're telling yourself, "Oh well, yeah, I love comic books but that doesn't count. You can't make money with comic books."** 不可否认的是,此刻它就在你的面前,但是你忽略了它,无论是什么原因,你都熟视无睹。你只会告诉自己:“我很喜欢漫画,但是这有什么用,因为我并不能通过看漫画来赚钱。” **Fuck you, have you even tried?** 去你的,累不累? **The problem is not a lack of passion for something. The problem is productivity. The problem is perception. The problem is acceptance.** 所以问题的核心不在于你是否对某种事物缺乏热情,而是你能否行动起来,能否能感知到它,然后是能否去接受它。 **The problem is the, "Oh, well that's just not a realistic option," or "Mom and Dad would kill me if I tried to do that, they say I should be a doctor," or "That's crazy, you can't buy a BMW with the money you make doing that."** 我们常常会遇到这些描述:“哦,这个选择不符合实际”、“爸妈肯定不会同意我这么做,他们说我应该当个医生”、“疯了吧,你不能用你赚到的钱买宝马”。 **The problem isn't passion. It's never passion.** 问题不是没有激情,而是永远都不热情。 **It's priorities.** 这才是首先要明白的。 **And even then, who says you need to make money doing what you love? Since when does everyone feel entitled to love every fucking second of their job? Really, what is so wrong with working an okay, normal job with some cool people you like and then pursuing your passion in your free time on the side? Has the world turned upside-down or is this not suddenly a novel idea to people?** 即便如此,谁说你必须要通过喜欢的事情来赚钱呢?你觉得每个人都会热爱自己工作的每一秒吗?与你喜爱的人一起做稀松平常的工作,然后在业余时间追求自己喜爱的事物有什么不好吗?是世道变了还是人们都复古了? **Look, here's another slap in the face for you: every job sucks sometimes. There's no such thing as some passionate activity that you will never get tired of, never get stressed over, never complain about. It doesn't exist. I am living my dream job (which happened by accident, by the way. I never in a million years planned on this happening; like a kid on a playground I just went and tried it), and I still hate about 30% of it. Some days more.** 另一个打脸的方面是:任何工作都有恶心的时候。没有任何事情是让你充满热情的,都会厌倦、要承受压力、都会抱怨。像我现在做着自己梦想中的工作(顺便说一下,这是偶然的。我当时从未计划过它的发生;只是像婴儿学步一样尝试它),但我依然对它有着三分的厌恶,有些时候更多。 **Again, that's just life.** 重申一下,这就是生活! ![FINDING%20YOUR%20PASSION/Untitled.png](http://qiniu.milleros.com/20-03-08/1a54db9b-876d-4b48-b724-d0615f8b697d.png) **The issue here is, once again, expectations. If you think you're supposed to be working 70-hour work weeks and sleeping in your office like Steve Jobs and loving every second of it, you've been watching too many shitty movies. If you think you're supposed to wake up every single day dancing out of your pajamas because you get to go to work, then you've been drinking the Kool-Aid. Life doesn't work like that. It's just unrealistic. There's a thing most of us need called balance.** 还有一个问题就是:期望值。如果你认为自己应该每周干70小时的活,像乔布斯(Steve Jobs,苹果创始人)那样,起居都在办公室,享受着其中的每一秒,那说明你电影看多了。又或者是你不想去上班而每天早上起来跳雪舞(Snow dance,这里原文是Dancing out of your pajamas,穿着睡衣跳舞),还喝着酷儿(Kool-Aid,一种碳酸饮料,原文drinking the Kool-Aid,这里意指"热衷于跳雪舞")。生活不是这样的,因为这样太理想了,我们大多数人都需要有自己的对事物的衡量能力。 > **Snow dance** 雪舞是外国人的一种仪式,这种仪式通常是人们希望能在冬季带来雪,从而避免第二天要上学或上班。 > **Drinking the Kool-Aid** 喝酷儿,泛指和可乐,意思是热衷于做某事 ## YOUR PASSION IS RIGHT IN FRONT OF YOU 你生活的热情就在你的眼前 **I have a friend who, for the last three years, has been trying to build an online business selling whatever. It hasn't been working. And by not working, I mean he's not even launching anything. Despite years of "work" and saying he's going to do this or that, nothing actually ever gets done.** 我有个朋友,在过去三年里,他一直尝试做一个线上卖场卖些什么东西。但实际上根本没达成,因为他什么也没做。我的意思是他什么都没干,尽管进行了他多年的"运作",并不断说自己要做这个做那个,但实际上啥都没做。 **What does get done is when one of his former co-workers comes to him with a design job to create a logo or design some promotional material for an event. Holy shit, he's all over that like flies on fresh cow shit.** 他唯一做了的事情就是他的一位前同事来找他设计了一个LOGO还有一些活动促销材料。就像新鲜牛粪上的黄色苍蝇一样(译者认为这里应该是牛粪上苍蝇来代指他朋友在创建线上卖场的工作上总是说得多做得少,就像牛粪上飞着的苍蝇一样,光嗡嗡,又不落下去)。 > **Flies on fresh cow shit** 新鲜牛粪上的苍蝇,指的是北半球一种常见的苍蝇之一,通体黄色,生命周期短。 **And he does a great job! He stays up to 4:00 AM losing himself working on it and loving every second of it.** 他做的很好!在凌晨的四点还沉浸在设计LOGO的工作中,他热爱其中的每一秒! **But then two days later it's back to, "Man, I just don't know what I'm supposed to do."** 但是仅仅两天之后(译者注:帮朋友做完了事情),就变成了“兄弟,我不知道要做啥了”。 **I meet so many people like him. He doesn't need to find his passion. His passion already found him. He's just ignoring it. He just refuses to believe it's viable. He is just afraid of giving it an honest-to-god try.** 我见过很多像他这样的人。他们根本就不需要去寻找热情,因为热情已经找到他们了。他们只不过是忽略了它们罢了,他们不相信自己做的是可行的,同样的他们害怕付出真正的努力(到头来却一无所有)。 **It's like a nerdy kid walking onto a playground and saying, "Well, bugs are really cool, but NFL players make more money, so I should force myself to play football every day," and then coming home and complaining that he doesn't like recess.** 这就像一个书呆子走到操场边说:“抓飞虫虽然很酷,但是却不如国榄联(NFL,The National Football League,美国国家橄榄球联盟)球员来得赚钱,所以我要强迫自己每天去打橄榄球。”回到家还不停抱怨说自己并不喜欢休息(以此衬托出自己要每天打橄榄球的决心)。 **And that's bullshit. Everybody likes recess. The problem is that he's arbitrarily choosing to limit himself based on some bullshitty ideas he got into his head about success and what he's supposed to do.** 真是不可理喻,每个人都喜欢舒适惬意,问题在于他居然如此果断的给自己定标准,就因为他脑子里那些关于成功的小九九。 **Another email I get all the time is from people wanting advice on how to become a writer.** 除此之外,我收到的另外一种邮件是希望获得我关于如何成为作家的建议。 **And my answer is the same: I have no fucking idea.** 我的答案同样如此:我并没有什么高见! **As a kid, I would write short stories in my room for fun. As a teenager, I would write music reviews and essays about bands I loved and then show them to nobody. Once the internet came around, I spent hours upon hours on forums writing multi-page posts about inane topics – everything from guitar pickups to the causes of the Iraq War.** 当我还是个孩子的时候,我会在在房间里写一些小故事来取乐。十几岁的时候就写一些关于我喜欢的乐队音乐的评论和文章,虽然并不公开。当互联网出现之后,我通常会花上几小时在论坛对一些事情泛泛而谈,从吉他拾音器到伊拉克战争的起因。 **I never considered writing as a potential career. I never even considered it a hobby or passion. To me, the things I wrote about were my passion: music, politics, philosophy. Writing was just something I did because I felt like it.** 我从未考虑过写作是一种潜在的职业,我甚至从未认为它是一种爱好或激情。对我来说,我写的东西才是我的激情:音乐、政治或是哲学。写作只是一件因为我喜欢才做的事情。 **And when I had to go looking for a career I could fall in love with, I didn't have to look far. In fact, I didn't have to look at all. It chose me, in a way. It was already there. Already something I was doing every day, since I was a kid, without even thinking about it.** 而且当我也到了不得不去寻找自己喜爱的工作时,我从没看的很远。实际上我完全不需要看,因为它在某种程度上选择了我,他就在我的眼前。从小到大,他都是我每天都在做的事情,根本不需要过多的考虑。 **Because here's another point that might make a few people salty: If you have to look for what you're passionate about, then you're probably not passionate about it at all.** 除此之外还有一个可能会让一些人觉得很逗的点就是:你不得不去寻找自己所热衷的事物,但是根本就不没有激情去寻找。 **If you're passionate about something, it will already feel like such an ingrained part of your life that you will have to be reminded by people that it's not normal, that other people aren't like that.** 如果你对某件事充满了热情,那么它会让你觉得这就是你生活中根深蒂固的一部分,但可惜却是你需要别人来提醒你,这是不正常的,因为别人都不是这样的(译者注:指的是别人不像你一样喜欢那件事)。 **It didn't occur to me that writing 2,000-word posts on forums was something nobody else considered fun. It never occurred to my friend that designing a logo is something that most people don't find easy or fun. To him, it's so natural that he can't even imagine it being otherwise. And that's why it's probably what he really should be doing.** 我从没有想过在论坛上写个2000字的帖子会不会没人觉得有趣。就像我的朋友没想过设计一个LOGO对于大多数人来说是无趣的。但是对他来说,这是很自然的一件事,他没有想过(如果自己不是一个设计师的话在设计LOGO这件事上)会有什么不同。这就是为什么他可能真的应该这么做(指的是他设计LOGO到凌晨4点)。 **A child does not walk onto a playground and say to herself, "How do I find fun?" She just goes and has fun.** 孩子们根本不需要跑到操场旁问:“我如何从中找到快乐?”他只需要跑上去玩就行了。 **If you have to look for what you enjoy in life, then you're not going to enjoy anything.** 如果你走上了必须要寻找自己喜欢的生活的路,那么你将不会享受生活中的任何事情。 **And the real truth is that you already enjoy something. You already enjoy many things. You're just choosing to ignore them.** 事实就是你已经在享受某些东西,甚至是享受过了,但是你却选择忽视它们。 ## 原文 [SCREW FINDING YOUR PASSION - Mark Manson](https://markmanson.net/screw-finding-your-passion) ## 参考资料 1. [Tag(game) 老鹰捉小鸡游戏](https://en.wikipedia.org/wiki/Tag_(game)) 2. [Aaahh!!! Real Monsters 下水道怪物(卡通动画/游戏)](https://en.wikipedia.org/wiki/Aaahh!!!_Real_Monsters) 3. [Snow dance 跳雪舞](https://en.wikipedia.org/wiki/Snow_dance) 4. [Scathophaga stercoraria 一种黄色苍蝇](https://www.notion.so/milleros/FINDING-YOUR-PASSION-72d74c95a6ac4d92aa33d7aba576362a)
继续阅读

在创业公司上班的一些感受

MILLEROS 发布于 2020-03-07

## 前言 其实想要辞职这件事,我已经考虑很久了。一年来,它就像日夜拍涌着船儿的浪涛,时大时小,时而波涛汹涌,时而风平浪静;浪大的时候,我就是孤舟;浪小的时候,我就是巨轮。但是真的某一刻,浪花战胜了一次孤帆,于是次数越来越多,船儿越来越小,摇晃的愈发的厉害。 我工作在一家创业公司,总共的人数都不超过20人;我加入时,正值公司的鼎盛时期,登记在案的人数时15人;那时候人人都充满干劲,作为刚刚从象牙塔走出来的孩子,我对一切都充满了好奇,觉得自己能胜任所有工作。 下面我讲讲创业公司的一些共同点吧,算是对我以后找工作的警醒,也算是对看到这篇文章又恰巧和你相似的人有些许帮助。 ## 创业公司特性 ### 瓶颈低,难积累 其实刚产生这种念头是在工作了大概半年之后,那会发现自己在这个公司记得技术瓶颈已经到了,小公司,无非就是全部用轮子,项目种类很繁杂,涉及各种行业,让我们这些开发人员很难有积累,基本上都是做一种,下次就换一种了,在上次项目里积累到和项目相关行业里面的知识变得毫无用处。 ### 变质的扁平化管理 另一个就是小公司追求“**扁平化管理**”,按理说扁平化管理应该是真的会让效率提升数倍,因为减少了层层汇报的麻烦。 > 扁平化管理是指通过减少管理层次、压缩职能部门和机构、裁减人员,使企业的决策层和操作层之间的中间管理层级尽可能减少,以便使企业快速地将决策权延至企业生产、营销的最前线,从而为提高企业效率而建立起来的富有弹性的新型管理模式。 它摒弃了传统的金字塔状的企业管理模式的诸多难以解决的问题和矛盾。 但是很多公司做不好这个事情,像我们公司,经常是开发人员一对多,对多个人进行汇报/接受任务安排,这在很多情况下是有冲突的,按照A的意见做了,然后B又说A的不行,让你按照他来,这种情况往往是开发人员最糟心了。 ### 需求变动大,能用就行 小公司不知道是不是为了活下去,基本上都会走上质低价高,也就是收的钱和办的事不对等;他们追求的都是**变性的高效**,通常都是**能用就行**;很多人用到很难用的产品,肯定会骂这个程序员,写的什么玩意,但不是的,**程序员能决定某个功能是否好用,但是是有条件的;** ### **班门弄斧的考核标准** 很多创业公司会以**BUG率来做绩效考核的标准,**其实这本身没问题,BUG率越低说明这个程序员写的越好,那理应拿到更多的奖励;那大家就会问,那为什么这个产品还是这么难用,那不就说明真的是程序员的问题了吗?他不好好写代码,就想着偷懒;其实不是的,因为问题就出在: 1. 一个BUG是怎么定义的 2. BUG率是通过什么方式计算出来的 在这两个问题上,创业公司往往都是做不好的,一个是**统计混乱**,二个是**分工混乱**;第一个统计混乱就是没有人去专门负责这种统计的问题,所以这些工作往往会被推给测试工程师,那测试工程师又不懂代码,他只知道点点点,有问题了,那就算一个;第二个分工混乱就是指没有专门的人来负责这个考核的事情,这些事情往往会被委托给某个“领导”做审查,而这些所谓的“领导”通常情况下也是大致看一下总数字,然后看一下总页面,做个除法,以此算出平均每个页面的bug数量: **BUG率 = BUG总数/程序员经手页面总数** 很显然,这样就是有问题的,因为对于程序员来说,很多时候一个所谓的BUG并不都是因为自己的原因导致的,有可能是设计的时候有缺陷、原型图逻辑有问题、后端接口有问题等等;那在这种情况下,用这个公式一算,**所有的黑锅都让程序员背了**,搁谁谁都难受。 所以在这种畸形的考核标准下,程序员就总结出一条定理来了: **多做多错,少做少错** 一般程序员在做某些产品的功能的时候,也会反复去使用,看下那里是用户体验差的,如果差那他就多写些代码来改善体验,那多写些代码又可能会出现某些BUG,例如逻辑错误,那**测试那边直接就把你这优化里面的BUG算成整个项目的BUG**,你觉得如果你作为程序员,下次还会去做什么优化吗?**答案显然是否**! ### 画饼充不了饥 其实资本家最喜欢的就是画饼了,这无关创业公司与否。但是创业公司画的饼,不大,但是也落实不到你身上; 创业公司的饼一般是**多又小**,因为这里面夹杂的很多是他们的畅想和展望,有些都是天马行空,完全没实践的东西。例如我司去年开年给我们画个饼:**每个月项目结束后都会进行一次聚餐,当作项目的总结会。**实际上呢,要么是做了一次两次的样子,要么是“还在路上”,这些饼往往是无声无息的就没了。人最怕的就是给了期望,然后又把别人的期望耗尽。我觉得我也能理解创业公司的艰辛,也不要求什么每月聚餐,顶不住聚餐的开销,我也能理解,但是最起码和大家表达一下歉意也好,毕竟自己许出来的承诺没遵守,道个歉总归是拉拢人心的,大家也不是不理解。 所以这种有头没尾的“大饼”,谁都不爱吃,只会让自己的员工觉得公司言而无信,不可信,**当然这些都是做人方面不咋地,作为员工的我们,不能将这些情绪带到工作中,觉得他们这样对我们,那我们工作也松懈;** **这是不对的!** 因为画饼并没有对我们造成什么损失,如果人家工资照样发,那我们的活也应该照样干。 ### 没有明确的岗位职责/方向 创业公司在用人方面也是抱有“能用就行”的态度,为什么这么说,这和岗位职责/方向有啥关系?别着急,请听我娓娓道来。 创业公司在用人上,一般都是让一个人发挥多方面的能力,但是又不深入某些领域,比如说你作为网页设计师,你会UI设计,这是你最基本的能力,同时你恰巧又会一些视频剪辑的能力,那公司需要一个宣传片,就会让你帮忙剪辑;看起来不错,你可能会觉得刚好能让你多学点知识,我不否认多学知识这一点,但是有没有想过,时间长了,你被安排做得和UI设计不相关的工作越来越多,一个人的精力是有限的,那你花在设计上的时间肯定会相应的减少,这样时间久了,你就会变得知道很多,但是又啥都不懂(指一些深入的东西);最后,恭喜你,**你从“全职设计师”成为了“全干设计师”**;大小繁杂事都是你来干。 ## 有害之处 其实看完了上面的特性,就可以总结出,创业公司特性里面,“**没有明确的岗位职责/方向**”对我们的职业生涯影响是最大的;你可以在现在的创业公司里呼风唤雨,做啥啥都行,但是一旦你换了个公司,那情况就大不相同了,可能公司要你专门做某个领域,而你指挥皮毛,用不了多久你就会顶不住,甚至工作都找不到。 ## 结语 如果你和我一样,目前身在一家创业公司,那我希望你能保持自己的职业方向,当感受到了瓶颈就立马跳槽,不要犹豫,因为我们还年轻,有更多的时间去拼搏;也正是因为我们还年轻,没有房贷、车贷的压力,所以换工作也来的更加的如鱼得水。
继续阅读

APICloud跳转国内地图APP

MILLEROS 发布于 2020-03-06

## 前言 最近遇到一个项目需求,就是需要Android/IOS平台跳转到地图App进行导航,随即整理了这篇文章,供大家参考,也供自己做记录。 ## 需要用到的方法 ### 跳转APP 在APIcloud平台上,已经为我们提供了跳转APP的方法,那就是openApp方法: //iOS中的使用方法如下: api.openApp({ iosUrl: 'weixin://', //打开微信,其中weixin为微信的URL Scheme appParam: { appParam: 'app参数' } }); api.openApp({ iosUrl: 'app-settings:' //打开应用设置界面,支持iOS 8及以上系统 }); //Android中的使用方法如下: api.openApp({ androidPkg: 'android.intent.action.VIEW', mimeType: 'text/html', uri: 'http://www.baidu.com' }, function(ret, err) { if (ret) { alert(JSON.stringify(ret)); } else { alert(JSON.stringify(err)); } }); 可见[官方文档](https://docs.apicloud.com/Client-API/api#25) ### 检测本机是否安装了指定应用 在进行APP跳转的时候,我们不确定本机是否安装了什么软件,所以我们要先检测一下本机安装了哪些地图APP,以便我们能正确的显示可用列表。那么在这里检测已安装的APP则需要用到appInstalled方法: //异步返回结果: api.appInstalled({ appBundle: 'xxx' }, function(ret, err) { if (ret.installed) { //应用已安装 } else { //应用未安装 } }); //同步返回结果: var installed = api.appInstalled({ sync: true, appBundle: 'xxx' }); if (installed) { //应用已安装 } else { //应用未安装 } 可见[官方文档](https://www.notion.so/milleros/APIcloud-APP-5dc7825a578a4225ba393e7c6f3b8784) **但是检测本机已经安装了哪些APP并没有那么简单,特别是在IOS平台上,需要配置某些参数才能成功检测** 首先在检测前,我们需要知道被检测的appBundle; > 在安卓平台上,appBundle就是应用包名,例如支付宝包名为com.eg.android.AlipayGphone;在IOS平台上,appBundle就是URL Scheme,一个应用只能有一个包名,但是可以有多个URL Scheme,例如支付宝在IOS上的URL Scheme就是alipays:// 下面我列出一些地图APP的安卓包名和URL Scheme: 1. 高德地图 com.autonavi.minimap iosamap:// 2. 百度地图 com.baidu.BaiduMap baidumap:// 3. 谷歌地图 com.google.android.apps.maps comgooglemaps:// 4. 腾讯地图 com.tencent.map qqmap:// 5. 苹果地图 无安卓应用 [http://maps.apple.com/](http://maps.apple.com/) **在安卓平台上我们通过包名检测是否安装某个APP是不受限制的,但是在IOS上,我们还需要额外的配置参数** 因为我使用的是APIcloud平台开发APP,所以直接在应用根目录下的res文件夹内添加**Info.plist**文件: ![APICloud%20APP/8ff40440-f235-4cac-aed3-0c4d47c4e3e0.png](http://qiniu.milleros.com/8ff40440-f235-4cac-aed3-0c4d47c4e3e0.png) 然后在**Info.plist**添加下列内容: <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>LSApplicationQueriesSchemes</key> <array> <!-- 高德地图 --> <string>iosamap</string> <!-- 百度地图 --> <string>baidumap</string> <!-- 谷歌地图 --> <string>comgooglemaps</string> <!-- 腾讯地图 --> <string>qqmap</string> </array> </dict> </plist> 如果你是IOS原生应用开发者,那么你也可以在项目目录中找到**Info.plist**文件,添加上地图参数就可以了。 在APIcloud平台上,添加完**Info.plist**文件后,还需要在根目录下的**config.xml**文件内添加下列内容: <preference name="querySchemes" value="iosamap,baidumap,comgooglemaps,qqmap,sgmap" /> 要注意是否有相同name的选项(name="querySchemes"),有则在末尾处添加上面的内容,否则直接粘贴上面那句话上去就行了。 **IOS配置好这些内容后,我们会发现检测还是没用,安装了的也返回未安装,这是因为这些配置需要在云编译后的环境内才能使用(正式版本、测试版本);** 以上内容都完成后,我们就可以进行跳转APP的操作了,在跳转APP的时候,安卓和IOS也是天差地别,下面我会娓娓道来 ## 进行应用跳转 在IOS平台上我们可以直接使用URL Scheme进行跳转,但是**在Android平台上,我们不能直接使用包名进行跳转,否则虽然能跳过去,但是APP可能对我们传入的参数无任何反应**。 ### 安卓平台 api.openApp({ androidPkg: 'android.intent.action.VIEW', uri: 'bdapp://map/marker' // 打开百度地图标注 }) 可以看到此处我们**通过安卓的intentVIEW来打开我们想要的应用**。 ### IOS平台 api.openApp({ iosUrl: 'baidumap://map/marker', //打开百度地图标注 appParam: { title: '一些参数' } }) 下面也不卖关子了,直接把我写好的跳转代码贴出来 ### 地图跳转参考代码 var _pageParams = { lon: 114.062261, lat: 22.53913, gcj02lon: 114.108153, gcj02lat: 22.561602, address: '广东省深圳市福田区益田路5033号' }; // 跳转至高德地图 function toAMap() { // 参数文档地址:https://lbs.amap.com/api/amap-mobile/guide/android/route var _params = { ios: { iosUrl: 'iosamap://viewMap', appParam: { sourceApplication: 'com.milleros.blog', poiname: _pageParams.address, lat: _pageParams.gcj02lat, lon: _pageParams.gcj02lon, dev: 0 } }, android: { androidPkg: 'android.intent.action.VIEW', uri: 'androidamap://viewMap?lat=' + _pageParams.gcj02lat + '&lon=' + _pageParams.gcj02lon + '&poiname=' + _pageParams.address + '&dev=0&t=0' } }; api.openApp(_params[_systemType]); }; // 跳转至百度地图 function toBMap() { // 参数文档地址:https://lbsyun.baidu.com/index.php?title=uri/api/ios var _params = { ios: { iosUrl: 'baidumap://map/marker', appParam: { src: 'com.ltkj.hkscaacpa', coord_type: 'bd09ll', title: _pageParams.address, content: _pageParams.address, location: (_pageParams.lat + ',' + _pageParams.lon) } }, android: { androidPkg: 'android.intent.action.VIEW', uri: 'bdapp://map/marker?coord_type=bd09ll&title=' + _pageParams.address + '&location=' + _pageParams.lat + ',' + _pageParams.lon + '&src=com.h2389629950.eeg' } }; api.openApp(_params[_systemType]); }; // 跳转至谷歌地图 function toGMap() { // 参数文档地址:https://developers.google.com/maps/documentation/urls/ios-urlscheme var _params = { ios: { iosUrl: 'comgooglemaps://map/navi', appParam: { q: _pageParams.address, center: (_pageParams.gcj02lat + ',' + _pageParams.gcj02lon) } }, android: { androidPkg: 'android.intent.action.VIEW', uri: 'https://maps.google.com/maps?q=' + _pageParams.gcj02lat + ',' + _pageParams.gcj02lon + '(' + _pageParams.address + ')' } }; api.openApp(_params[_systemType]); }; // 跳转至腾讯地图 function toQQMap() { // 参数文档地址:https://lbs.qq.com/uri_v1/guide-mobile-poiMarker.html var _params = { ios: { iosUrl: 'qqmap://map/marker', appParam: { marker: 'coord:' + (_pageParams.gcj02lat + ',' + _pageParams.gcj02lon) + ';title:' + _pageParams.address + ';addr:' + _pageParams.address + '&referer=OB4BZ-D4W3U-B7VVO-4PJWW-6TKDJ-WPB77' } }, android: { androidPkg: 'android.intent.action.VIEW', uri: 'qqmap://map/marker?marker=coord:' + (_pageParams.gcj02lat + ',' + _pageParams.gcj02lon) + ';title:' + _pageParams.address + ';addr:' + _pageParams.address + '&referer=OB4BZ-D4W3U-B7VVO-4PJWW-6TKDJ-WPB77' } }; api.openApp(_params[_systemType]); }; // 跳转至苹果地图并导航 function toAppleMap() { // 参数文档地址:https://developer.apple.com/library/archive/featuredarticles/iPhoneURLScheme_Reference/MapLinks/MapLinks.html var _params = { ios: { iosUrl: 'http://maps.apple.com/', appParam: { q: _pageParams.address } } }; api.openApp(_params[_systemType]); }; 可以看到我的假数据里面还有两个gcj02开头的经纬度 var _pageParams = { lon: 114.062261, lat: 22.53913, gcj02lon: 114.108153, gcj02lat: 22.561602, address: '广东省深圳市福田区益田路5033号' }; 是因为不同的地图提供方,使用的坐标系统可能是不同的,下面是这次涉及到的地图APP的经纬度系统: 1. 高德地图 GCJ-02 2. 百度地图 BD09 3. 谷歌地图 GCJ-02 4. 腾讯地图 GCJ-02 5. 苹果地图 GCJ-02 ## 进行地图坐标系的转换 因为坐标系的不同会导致一样的经纬度数字在不同地图中表现不一致,所以这里我提供了一个网上搜集过来的转换工具(coordtransform.js) 使用方法: //国测局坐标(火星坐标,比如高德地图在用),百度坐标,wgs84坐标(谷歌国外以及绝大部分国外在线地图使用的坐标) //百度经纬度坐标转国测局坐标 var bd09togcj02 = coordtransform.bd09togcj02(116.404, 39.915); //国测局坐标转百度经纬度坐标 var gcj02tobd09 = coordtransform.gcj02tobd09(116.404, 39.915); //wgs84转国测局坐标 var wgs84togcj02 = coordtransform.wgs84togcj02(116.404, 39.915); //国测局坐标转wgs84坐标 var gcj02towgs84 = coordtransform.gcj02towgs84(116.404, 39.915); console.log(bd09togcj02); console.log(gcj02tobd09); console.log(wgs84togcj02); console.log(gcj02towgs84); //result //bd09togcj02: [ 116.39762729119315, 39.90865673957631 ] //gcj02tobd09: [ 116.41036949371029, 39.92133699351021 ] //wgs84togcj02: [ 116.41024449916938, 39.91640428150164 ] //gcj02towgs84: [ 116.39775550083061, 39.91359571849836 ] coordtransform.js文件源码: /** * Created by Wandergis on 2015/7/8. * 提供了百度坐标(BD09)、国测局坐标(火星坐标,GCJ02)、和WGS84坐标系之间的转换 * coordtransform.js */ //UMD魔法代码 // if the module has no dependencies, the above pattern can be simplified to (function(root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define([], factory); } else if (typeof module === 'object' && module.exports) { // Node. Does not work with strict CommonJS, but // only CommonJS-like environments that support module.exports, // like Node. module.exports = factory(); } else { // Browser globals (root is window) root.coordtransform = factory(); } }(this, function() { //定义一些常量 var x_PI = 3.14159265358979324 * 3000.0 / 180.0; var PI = 3.1415926535897932384626; var a = 6378245.0; var ee = 0.00669342162296594323; /** * 百度坐标系 (BD-09) 与 火星坐标系 (GCJ-02)的转换 * 即 百度 转 谷歌、高德 * @param bd_lon * @param bd_lat * @returns {*[]} */ var bd09togcj02 = function bd09togcj02(bd_lon, bd_lat) { var bd_lon = +bd_lon; var bd_lat = +bd_lat; var x = bd_lon - 0.0065; var y = bd_lat - 0.006; var z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * x_PI); var theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * x_PI); var gg_lng = z * Math.cos(theta); var gg_lat = z * Math.sin(theta); return [gg_lng, gg_lat] }; /** * 火星坐标系 (GCJ-02) 与百度坐标系 (BD-09) 的转换 * 即谷歌、高德 转 百度 * @param lng * @param lat * @returns {*[]} */ var gcj02tobd09 = function gcj02tobd09(lng, lat) { var lat = +lat; var lng = +lng; var z = Math.sqrt(lng * lng + lat * lat) + 0.00002 * Math.sin(lat * x_PI); var theta = Math.atan2(lat, lng) + 0.000003 * Math.cos(lng * x_PI); var bd_lng = z * Math.cos(theta) + 0.0065; var bd_lat = z * Math.sin(theta) + 0.006; return [bd_lng, bd_lat] }; /** * WGS84转GCj02 * @param lng * @param lat * @returns {*[]} */ var wgs84togcj02 = function wgs84togcj02(lng, lat) { var lat = +lat; var lng = +lng; if (out_of_china(lng, lat)) { return [lng, lat] } else { var dlat = transformlat(lng - 105.0, lat - 35.0); var dlng = transformlng(lng - 105.0, lat - 35.0); var radlat = lat / 180.0 * PI; var magic = Math.sin(radlat); magic = 1 - ee * magic * magic; var sqrtmagic = Math.sqrt(magic); dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * PI); dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * PI); var mglat = lat + dlat; var mglng = lng + dlng; return [mglng, mglat] } }; /** * GCJ02 转换为 WGS84 * @param lng * @param lat * @returns {*[]} */ var gcj02towgs84 = function gcj02towgs84(lng, lat) { var lat = +lat; var lng = +lng; if (out_of_china(lng, lat)) { return [lng, lat] } else { var dlat = transformlat(lng - 105.0, lat - 35.0); var dlng = transformlng(lng - 105.0, lat - 35.0); var radlat = lat / 180.0 * PI; var magic = Math.sin(radlat); magic = 1 - ee * magic * magic; var sqrtmagic = Math.sqrt(magic); dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * PI); dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * PI); var mglat = lat + dlat; var mglng = lng + dlng; return [lng * 2 - mglng, lat * 2 - mglat] } }; var transformlat = function transformlat(lng, lat) { var lat = +lat; var lng = +lng; var ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng)); ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0; ret += (20.0 * Math.sin(lat * PI) + 40.0 * Math.sin(lat / 3.0 * PI)) * 2.0 / 3.0; ret += (160.0 * Math.sin(lat / 12.0 * PI) + 320 * Math.sin(lat * PI / 30.0)) * 2.0 / 3.0; return ret }; var transformlng = function transformlng(lng, lat) { var lat = +lat; var lng = +lng; var ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng)); ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0; ret += (20.0 * Math.sin(lng * PI) + 40.0 * Math.sin(lng / 3.0 * PI)) * 2.0 / 3.0; ret += (150.0 * Math.sin(lng / 12.0 * PI) + 300.0 * Math.sin(lng / 30.0 * PI)) * 2.0 / 3.0; return ret }; /** * 判断是否在国内,不在国内则不做偏移 * @param lng * @param lat * @returns {boolean} */ var out_of_china = function out_of_china(lng, lat) { var lat = +lat; var lng = +lng; // 纬度3.86~53.55,经度73.66~135.05 return !(lng > 73.66 && lng < 135.05 && lat > 3.86 && lat < 53.55); }; return { bd09togcj02: bd09togcj02, gcj02tobd09: gcj02tobd09, wgs84togcj02: wgs84togcj02, gcj02towgs84: gcj02towgs84 } })); ## 参考资料 1. [js 百度、高德、谷歌、火星、wgs84(2000)地图坐标相互转换的JS实现](https://blog.csdn.net/xialong_927/article/details/100208002) 2. [高德地图、腾讯地图、谷歌中国区地图与百度地图坐标系](https://www.notion.so/milleros/APIcloud-APP-5dc7825a578a4225ba393e7c6f3b8784) 3. [各种国内地图坐标系总结](https://blog.csdn.net/m0_37738114/article/details/80452485)
继续阅读

如何用Vue打造一个爆款应用

MILLEROS 发布于 2019-12-24

## 前言 在9102年,没有前端不会用vue,甚至很多后端都开始接手使用vue来搭建后台管理系统,或者一些简单的页面,Vue以他的轻量和高效赢得了大家的青睐。 **开篇先来个滑稽,防止你以为这是个正经文章** ![http://qiniu.milleros.com/emoji/huaji.jpeg](http://qiniu.milleros.com/emoji/huaji.jpeg) 正是因为Vue的普及,越来越多的项目使用Vue作为前端框架,比如[bilibili](https://www.bilibili.com/),除此之外,因为移动端(手机)的普及程度还有对HTML、es的支持程度向来都是走在前列的,很多普通岗位上的程序员搭建页面的时候也会首选Vue,哪怕是一个很小的问卷调查。 今天我们就来看看,**如何用Vue打造一个爆款应用。** ![http://qiniu.milleros.com/19-12-24/vRkDZiVLcIExVCIzvmuY4oxiIoo5EmdD.png](http://qiniu.milleros.com/19-12-24/vRkDZiVLcIExVCIzvmuY4oxiIoo5EmdD.png) ## 起步 ![http://qiniu.milleros.com/19-12-24/Untitled.png](http://qiniu.milleros.com/19-12-24/Untitled.png) 我们可以看到,Vue文档一打开就已经标明了最好要了解HTML、CSS和JS的中级知识,但是我们也可以选择学习,因为Vue文档写的已经很详尽了,就算你不会前面说的那些中级知识,那么你也可以依葫芦画瓢做出自己想要的页面,但是花费的时间也许会更加的长。 > 为什么会要求需要中级知识,是因为了解这些知识,我们可以在代码的编写还有项目架构的搭建上,表现的更加的专业,写出质量更好的代码。 当然,看过Vue文档的,都知道我前面说的都是废话,什么专业的架构,质量更好的代码,都是虚的,只要**有Vue在手,天下我有**。 看到这里,大家肯定着急了,这说了半天也没看到你说到底要如何打造一个爆款的Vue应用,不会是一篇水文吧,别着急别着急,大家稍安勿躁,下面我会分情景给大家详细的说一下,各种常见情境下怎么科学的使用Vue。 > 这篇文章里面写的例子都是我在最近的公司项目代码中发现的,得提前说明一下就是,这个项目是个烂尾项目,之前的开发团队甩手不干了,然后转到我们组来的,这些Vue都是一个敢作敢当的新人加进去的。 ## 检索 这里的场景大概是这样的,在一个弹窗中,有很多分类选项,这些选项可以使用弹窗顶部的搜索框来检索包含特定关键词的选项,检索过程不是本地的,而是要调用接口。 我们先来看看下面这个实现方式,我觉得真的是完美(**大概瞟一眼就可以了,看代码后面的解释就行**)**。** <transition-group name="fade"> <template v-if="!is_searching"> <div v-for="(value, ikey) in list" class="schoolbox" :key="ikey"> <p class="type mt-1">{{ikey}}</p> <div class="row"> <div v-for="(v,i) in value" @click="check(v.name,v.id)" class="col-6 pb-2"> <div class="label"> <if condition="in_cn()"> <p>{{v.name}}</p> <p>{{v.english_name}}</p> <else /> <p>{{v.english_name}}</p> <p>{{v.name}}</p> </if> </div> </div> </div> </div> </template> </transition-group> var vm = new Vue({ el: '#form', data: { search_name: '', list: [], school: '', school_id: '', show_qt: false, is_searching: true, statusText: '加载中...', timer: null }, methods: { check(name, id) { alertify.closeAll(); this.show_qt = false; this.school = name; this.school_id = id; }, showSchQt() { alertify.closeAll(); this.show_qt = true; $('#school_name').blur(); $('#school_name1').show(); setTimeout(function(){ $('#school_name').val('其他学校'); $('#school_name1').focus(); },500); }, search() { var that = this; this.timer && clearTimeout(this.timer); this.timer = setTimeout(function() { that.is_searching = true; $.ajax({ url: 'path/to/get/data', type: 'post', dataType: 'json', data: { name: vm.search_name }, success: function(msg) { vm.is_searching = false; vm.statusText = '搜索中...'; vm.list = msg.data; resizeScroll(); }, error: function(msg) { layer.alert('网络错误!', { icon: 2 }) } }) }, 500); } } }); 首先我们分析一下这个页面为什么可以这么勇敢的引入Vue,我总结了以下优势: 1. 页面原本使用JQ,没有用过Vue 2. Vue可以对输入框做双向绑定 3. Vue可以对元素直接进行方法绑定 4. Vue内对数据做修改,会自动同步到Dom树上 我们看看,首先第一个页面原本只使用JQuery来做页面的DOM操作以及渲染,**那为了不破坏/夹杂代码到以前的JQ代码中,那当然是引入新的Vue啦**,这样我后面修改起来也方便,知道我的代码是哪些; 第二个,Vue可以对输入框做双向绑定,那**我就来个双向绑定,那多舒服,省去了JQ的各种绑定监听,方便!** 第三个,使用Vue直接做方法的绑定,那多舒服啊,写好所有的方法在methods里面,对着Dom元素就是一顿绑定,**特别是配合列表渲染,每个元素都能直接绑定带参数的事件,渲染代码都省了,舒服!** 第四个,那就更舒服了,换做用JQ,我没修改一次就得调用JQ对Dom元素做一次修改,**那我现在直接修改Vue里面的对应数据就可以了,反正他是自动的**。 现在我们看完了第一个Vue的例子,对Vue的强大还有方便,已经有了初步的认识,**我们看到了Vue的适应性,就算是原先没有引入Vue的页面,哪怕是之前用的是JQ,也能直接引入,根本不存在冲突,方便就Vans了。** ## 选择单选项 这个功能场景是这样的,在退款方式中,有集中选项,当选择其中一种之后,要将页面中的对应退款方式的表单展示出来,下面是业务代码: ![http://qiniu.milleros.com/19-12-24/Untitled%201.png](http://qiniu.milleros.com/19-12-24/Untitled%201.png) ![http://qiniu.milleros.com/19-12-24/Untitled%202.png](http://qiniu.milleros.com/19-12-24/Untitled%202.png) 针对这个业务场景,我总结了下面的优点: 1. 页面原本使用JQ, balabala(忘记了可以看看案例一第一条) 2. Vue可以对元素直接进行方法绑定 3. Vue内对数据做修改,会自动同步到Dom树上 4. 代码量少 第一个我就不说了,案例一已经讲过了; 第二个我们看看,这里业务要求对退款方式进行单选,**那肯定是每个选项绑定一个处理方法啦**,只要把参数传进去,在存储一下当前选中的是哪个就好了,**展示的时候直接交给优点3**,那多方便啊。 “什么?你说这是大炮打蚊子大材小用啊,我看你是想多了,你看看我**这里代码量明显就少啊**,你看看你**用JQ写,肯定比我多出几十行来**!” ![http://qiniu.milleros.com/19-12-24/Untitled%203.png](http://qiniu.milleros.com/19-12-24/Untitled%203.png) 我搜索了一下整个项目代码,找到了十处位置用了Vue ![http://qiniu.milleros.com/19-12-24/Untitled%204.png](http://qiniu.milleros.com/19-12-24/Untitled%204.png) 基本上每处都是像上面两个例子一样,属于**Vue轻量化用到极致**,JQuery都自愧不如。 看完Vue之后,我还看到了很多对ES新特性的致敬,使用大量的ES6新特性。 “什么?你说兼容性啊,我在我电脑上都是好好地啊,你看一点问题都没有,你看你的电脑上不是也没问题吗?” 这个图是我在项目中搜索出来使用**let关键字**的搜索截图。 ![http://qiniu.milleros.com/19-12-24/Untitled%205.png](http://qiniu.milleros.com/19-12-24/Untitled%205.png) 这个项目是用于给别人报名培训机构用的,可以线上管理自己的课程等操作。那**使用的人肯定都很有钱**,**怎么会用辣鸡windows上的IE7/8**呢,所以**兼容性肯定就不是问题**啦,**let**它能保证变量的作用域,这样就减少了变量相互之间的污染,多方便,**比var好用多了**,用就vans了**。** ![http://qiniu.milleros.com/19-12-24/Untitled%206.png](http://qiniu.milleros.com/19-12-24/Untitled%206.png) 看完这篇文章,相信大家对爆品Vue应用都有了自己的认识,总结下来,Vue带给我们的就是轻量、方便;取代JQuery完全不是问题,**用Vue一时爽,一直用一直爽**。 ## 结语 看完前面说的,是不是觉得Vue真的是这个世界上最好用的前端框架,集成事件绑定、数据绑定、渲染于一身的神级框架,尤雨溪他简直就是个天才,大神。 **至于用Vue用到这程度的,写出来的产品那肯定是爆品了,还爆炸了。** > 尤雨溪是大神我赞同,但是用Vue用成这样的人,你真是个天才。 ![http://qiniu.milleros.com/19-12-24/Untitled%207.png](http://qiniu.milleros.com/19-12-24/Untitled%207.png)
继续阅读

[前端查漏补缺01]什么是CSS-BFC?

MILLEROS 发布于 2019-12-18

## 前言 最近在忙着刷面试题,看到了很多未曾接触过的概念,今天接触到了一个叫做BFC的概念,想想工作一年多了,才知道这个概念,挺丢人的,所以抓紧研究一下这个叫做BFC的东西到底是什么。 好了,废话不多说,进入正题。 ## BFC介绍 BFC就是css布局的一个概念,是一块区域,一个环境。按照我的理解,**我把它理解成JS里面的作用域**,但是又不完全相同。反正就是一个区域的划分。 这说的有点让人懵逼,我们接着往下看。 BFC(Block formatting context)直译为"块级格式化上下文"。**它是一个独立的渲染区域**,只有Block-level box(我们平常说的盒子模型)参与, 它规定了内部的Block-level Box如何布局,**并且与这个区域外部毫不相干**。 我们常说的文档流其实分为定位流、浮动流和普通流三种。而普通流其实就是指BFC中的FC。 FC是formatting context的首字母缩写,直译过来是格式化上下文,它是页面中的一块渲染区域,有一套渲染规则,决定了其子元素如何布局,以及和其他元素之间的关系和作用。 常见的FC有BFC、IFC(行级格式化上下文),还有GFC(网格布局格式化上下文)和FFC(自适应格式化上下文),这里就不再展开了。 通俗一点的方式解释: BFC 可以简单的理解为**元素具有一套默认的渲染规则(属性)**,且这个属性(规则)**不能被使用这个语言的开发者修改**,使用了这种渲染规则(属性)的元素对外会表现出特定的渲染方式。 ## BFC的主要内容 ### 触发条件 满足下列条件之一就可触发BFC: 1. 根元素,即HTML元素 2. float的值不为none 3. overflow的值不为visible 4. display的值为inline-block、table-cell、table-caption 5. position的值为absolute或fixed ### 布局规则 1. 内部的Box会在垂直方向,一个接一个地放置。 2. Box垂直方向的距离由margin决定。属于同一个BFC的两个相邻Box的margin会发生重叠 3. 每个元素的margin box的左边, 与包含块border box的左边相接触(对于从左往右的格式化,否则相反)。即使存在浮动也是如此。 4. BFC的区域不会与float box重叠。 5. BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此。 6. 计算BFC的高度时,浮动元素也参与计算 ### 作用 1. 自适应两栏布局 2. 可以阻止元素被浮动元素覆盖 3. 可以包含浮动元素——清除内部浮动 4. 分属于不同的BFC时可以阻止margin重叠 ## BFC规则解析 ### 内部的Box会在垂直方向,一个接一个地放置 上文定义中提到过的块级盒:block-level box,在这里解析一波: ![block-level-box](http://qiniu.milleros.com/19-8-16/45563203.jpg 'block-level-box') 我们平常说的盒子是由margin、border、padding、content组成的,实际上每种类型的四条边定义了一个盒子,分别是分别是**content box、padding box、border box、margin box**,这四种类型的盒子一直存在,即使他们的值为0.决定块盒在包含块中与相邻块盒的垂直间距的便是margin-box。 提示:Box之间的距离虽然也可以使用padding来控制,但是此时实际上还是属于box内部里面,而且使用padding来控制的话就不能再使用border属性了。 布局规则1就是我们平常div一行一行块级放置的样式,大家想一下就知道了,这里就不展开了。 ### Box垂直方向的距离由margin决定。属于同一个BFC的两个相邻Box的margin会发生重叠 ![规则2](http://qiniu.milleros.com/19-8-16/6b0fc0e3d34f94875d35.gif '规则2') 上文提到过,决定块盒在包含块中与相邻块盒的垂直间距的便是margin-box。,上面的栗子就是这种情况。 演示中css属性设置:上面的box:margin-bottom: 100px;下面的box:margin-top: 100px;(他们是同一侧的margin,所以会发生margin重叠的情况,两个div的距离实际上只有100px。) ### 阻止margin重叠 当两个相邻块级子元素分属于不同的BFC时可以阻止margin重叠 操作方法:给其中一个div外面包一个div,然后通过触发外面这个div的BFC,就可以阻止这两个div的margin重叠 ```html <div class="aside"></div> <div class="text"> <div class="main"></div> </div> <style> .aside { margin-bottom: 100px; /*margin属性*/ width: 100px; height: 150px; background: #f66; } .main { margin-top: 100px; /*margin属性*/ height: 200px; background: #fcc; } .text { /*盒子main的外面包一个div,通过改变此div的属性使两个盒子分属于两个不同的BFC,以此来阻止margin重叠*/ overflow: hidden; /*此时已经触发了BFC属性。*/ } </style> ``` ![阻止margin重叠](http://qiniu.milleros.com/19-8-16/Snipaste_2019-08-16_18-19-50.png '阻止margin重叠') ### 每个元素的margin box的左边, 与包含块border box的左边相接触(对于从左往右的格式化,否则相反)。即使存在浮动也是如此 ```html <div class="par"> <div class="child"></div> <!-- 给这两个子div加浮动,浮动的结果,如果没有清除浮动的话,父div不会将下面两个div包裹,但还是在父div的范围之内。 --> <div class="child"></div> </div> ``` 解析:给这两个子div加浮动,浮动的结果,如果没有清除浮动的话,父div不会将下面两个div包裹,但还是在父div的范围之内,左浮是子div的左边接触父div的borderbox的左边,右浮是子div接触父div的borderbox右边,除非设置margin来撑开距离,否则一直是这个规则。 ### 可以包含浮动元素——清除内部浮动 给父divpar加上 overflow: hidden; 清除浮动原理:触发父div的BFC属性,使下面的子div都处在父div的同一个BFC区域之内,此时已成功清除浮动。 ![清除浮动](http://qiniu.milleros.com/19-8-16/dfe63a3d19cae8adf5fa.gif '清除浮动') 还可以向同一个方向浮动来达到清除浮动的目的,清除浮动的原理是两个div都位于同一个浮动的BFC区域之中。 ### BFC的区域不会与float box重叠 ```html <div class="aside"></div> <div class="text"> <div class="main"></div> </div> <style> .aside { width: 100px; height: 150px; float: left; background: #f66; } .main { height: 200px; overflow: hidden; /*触发main盒子的BFC*/ background: #fcc; } .text { width: 500px; } </style> ``` 上面aside盒子有一个浮动属性,覆盖了main盒子的内容,main盒子没有清除aside盒子的浮动。只做了一个动作,就是**触发自身的BFC**,然后就**不再被aside盒子覆盖了**。所以**BFC的区域不会与float box重叠**。 ![float box](http://qiniu.milleros.com/19-8-16/0e2c7b710c4a13111120.gif '') ### 自适应两栏布局 ![自适应两栏布局](http://qiniu.milleros.com/19-8-16/304255779293ba4c2082.gif '') 还是上面的代码,此时BFC的区域不会与float box重叠,因此**会根据包含块(父div)的宽度,和aside的宽度,自适应宽度。** ## BFC 与 Layout IE 作为浏览器中的奇葩,当然不可能按部就班的支持 BFC 标准,于是乎 IE 中有了 Layout 这个东西。**Layout 和 BFC 基本是等价的**,为了处理 IE 的兼容性,在需要触发 BFC 时,我们除了需要用触发条件中的 CSS 属性来触发 BFC,还需要针对 IE 浏览器使用 zoom: 1 来触发 IE 浏览器的 Layout。 有趣的文本: ```css .par { margin-top: 3rem; border: 5px solid #fcc; width: 300px; } .child { border: 5px solid #f66; width: 100px; height: 100px; float: left; } ``` ![环绕文字](http://qiniu.milleros.com/19-8-16/216207666aa8bef15115 '') 这里两个div被撑开,是因为父div被p标签撑开了,并不是因为清除浮动的原因,从下面这张图片可以清楚的知道。 ![焕然文字](http://qiniu.milleros.com/19-8-16/5f7dc07585ae6c512bb8 '') 其实以上的几个例子都体现了BFC布局规则第五条———— **BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此** ### 文本环绕float ```html <div style="float: left; width: 100px; height: 100px; background: #000;"> </div> <div style="height: 200px; background: #AAA;"> <div style=" width: 30px; height: 30px; background: red;"></div> <p>content</p> <p>content</p> <p>content</p> <p>content</p> <p>content</p> </div> ``` ![文本环绕](http://qiniu.milleros.com/19-8-16/c02b2396d987f4d7439a.jpg '') 问题:为什么 div 的左上角被覆盖了,而文本却没有被覆盖,float不是应该跟普通流不在一个层级吗?是因为float属性不生效吗? **解决:** float的定义和用法: float 属性定义元素在哪个方向浮动。以往这个属性总应用于图像,**使文本围绕在图像周围**,不过在 CSS 中,**任何元素都可以浮动**。浮动元素会生成一个块级框,而不论它本身是何种元素。 ![解决](http://qiniu.milleros.com/19-8-16/5994ed11ebc3e4b971db.gif '') 从上图可以看到,float属性确实生效,将float隐藏后,下面还有一个红色的div,这个div是被黑色div所覆盖掉的。**div会被float覆盖,而文本却没有被float覆盖**,是因为**float当初设计的时候**就是**为了使文本围绕在浮动对象的周围**。 ## 总结 其实说起来BFC在我们的日常开发中经常会遇到,只是我们没有去总结而已。当然总结并且熟练运用这些概念,可以让我们在编写代码是更加的高效。 参考转载: [布局概念-关于CSS-BFC深入理解](https://juejin.im/post/5909db2fda2f60005d2093db '')
继续阅读

APP上架到App Store/Google Play Store

MILLEROS 发布于 2019-12-17

## 前言 第一次上架APP到App Store,遇到了很多问题,但是好在都有惊无险的解决并成功上架了,在这之中有很多问题是经验不足造成的这次我写下这篇文章来总结一下,供以后自己或者看这篇文章的人参考,能少走点弯路就少走点弯路,毕竟时间宝贵啊!!! ## 根据上架平台划分 这里先给这篇文章做个区分,这样可以让有需要的人直接找到要上架的平台对应的流程。 ## Google Play Store **要上架到Play Store的前提是我们需要科学上网工具**,因为整个Play Store网站是被墙的状态,具体科学上网的步骤可参考网上现有的教程(自建/购买服务),整个流程我会按照必备账号、必备条件、必备文件、必备资料填写、发布流程来分类,有需要可自行在目录中点击跳转。 ### 必备账号 1. Google 账号 如果没有谷歌账号则需要先注册一个账号,注册地址是[https://accounts.google.com/](https://accounts.google.com/),有两种选项,一个是个人类型,一个是商业类型,需要根据实际情况进行区分创建 ![http://qiniu.milleros.com/19-12-17/Untitled.png](http://qiniu.milleros.com/19-12-17/Untitled.png) ### 必备条件 1. 账号支付了开发者费用 登录谷歌账号并进入[Google Play Console](https://play.google.com/apps/publish/signup/),如果没有支付开发者费用则不能发布,甚至连控制台都进不去 未支付开发者费用的界面: ![http://qiniu.milleros.com/19-12-17/Untitled%201.png](http://qiniu.milleros.com/19-12-17/Untitled%201.png) ### 必备文件 1. apk 包文件 2. 软件图标文件ICON(png格式,512*512像素) 3. 主题图片(JPG/PNG格式,1024*500像素),用于展示在APP详情页的主题背景处 ### 必备资料 1. APP名称 2. 一句话介绍(限定长度80字符) 3. 完整说明(限定4000字符) 4. 隐私政策网址 5. 截屏图片 **APP名称**一般商店都不允许同名,所以最好多想几个APP名字,防止和别人名字相同取不了,影响发布流程; **隐私政策网址**审核的时候会用到,可以使用网上的模板自行拟定一份。 **截屏图片**所有设备至少需要两张,不同设备类型每种最多八张。 ![http://qiniu.milleros.com/19-12-17/Untitled%202.png](http://qiniu.milleros.com/19-12-17/Untitled%202.png) 另外还可以准备一些宣传视频: ![http://qiniu.milleros.com/19-12-17/Untitled%203.png](http://qiniu.milleros.com/19-12-17/Untitled%203.png) ### 发布流程 在准备好上述资料后,我们可以进入正式发布的流程,**这里假设大家已经做好了测试任务,保证可以直接发布。** #### **创建APP** 进入[Google Play Console](https://play.google.com/apps/publish),点击右上角的创建新应用程序 ![http://qiniu.milleros.com/19-12-17/Untitled%204.png](http://qiniu.milleros.com/19-12-17/Untitled%204.png) 需要选择预设语言,这里要注意的是选择语言,一般选择了什么语言则后面写介绍信息的时候也最好用相应的语言 ![http://qiniu.milleros.com/19-12-17/Untitled%205.png](http://qiniu.milleros.com/19-12-17/Untitled%205.png) #### **填写商店资料** 建立好APP之后我们自动进入填写资料的页面,根据我们准备好的资料进行填写,这里填写的资料都是会在商店中展示。 这里有两个选项比较特殊: ![http://qiniu.milleros.com/19-12-17/Untitled%206.png](http://qiniu.milleros.com/19-12-17/Untitled%206.png) **填写标记和内容分级选项的时候记得先保存一下填写过的资料** **标记**是指你的APP一般属于什么分类,例如艺术、博客等分类; **内容分级**是用于对APP进行适用年龄进行分级,例如16+,表示适用于16周岁以上的人群使用,这种具体的数字是平台会在我们填写完一个问卷之后自动生成的,我们到时候直接套用就好了 #### **上传应用程序包** 点击左侧的应用程序版本 ![http://qiniu.milleros.com/19-12-17/Untitled%207.png](http://qiniu.milleros.com/19-12-17/Untitled%207.png) 填写完整了的选项右侧的完成图标会变为绿色 点击正式版的管理按钮 ![http://qiniu.milleros.com/19-12-17/Untitled%208.png](http://qiniu.milleros.com/19-12-17/Untitled%208.png) 建立新版本 ![http://qiniu.milleros.com/19-12-17/Untitled%209.png](http://qiniu.milleros.com/19-12-17/Untitled%209.png) 这里**有个非常非常重要的东西需要注意一下**,一进入的时候会**有个Google Play Signing计划**,我们一定要选择不加入,因为这个计划**会给你的包改签名**,还有一些其**他对包的修改**,会导致**APP上架过程中、过程后出现很多不必要的问题**,因为太严格了,关键是**这东西加入了就无法取消了;** 如果你**不小心手快点到了,或者不信邪去点了加入**,那么就需要退到我们一开始选择创建APP的页面**把这个APP删除再重新创建**,麻烦的就是资料都要再填一遍了。 ![http://qiniu.milleros.com/19-12-17/Untitled%2010.png](http://qiniu.milleros.com/19-12-17/Untitled%2010.png) 我们选择不采用-确定退出 ![http://qiniu.milleros.com/19-12-17/Untitled%2011.png](http://qiniu.milleros.com/19-12-17/Untitled%2011.png) 显示已停用 ![http://qiniu.milleros.com/19-12-17/Untitled%2012.png](http://qiniu.milleros.com/19-12-17/Untitled%2012.png) 然后在这里上传我们准备好的APK文件 ![http://qiniu.milleros.com/19-12-17/Untitled%2013.png](http://qiniu.milleros.com/19-12-17/Untitled%2013.png) 版本名称可以不用填写,上传APK包成功后自动就会填写上去了 在“这个版本有什么新功能?”这里填上一些你APP的功能就好了,注意别把<lang></lang>标签删除了 ![http://qiniu.milleros.com/19-12-17/Untitled%2014.png](http://qiniu.milleros.com/19-12-17/Untitled%2014.png) 然后点击右下角保存一下,再进行下一步操作 #### **设置价格以及销售区域等** 根据是否付费设置收费规则,然后设置国家/地区,**要注意这里默认是全部国家/区域都不选中的**,我们要点击红圈内的可用来全选全部区域,或者根据具体情况在列表里选择对应的国家/地区。 ![http://qiniu.milleros.com/19-12-17/Untitled%2015.png](http://qiniu.milleros.com/19-12-17/Untitled%2015.png) 然后在是否包含广告内容点击不包含(**根据具体情况设置,偷奸耍滑会导致应用审核不通过**) ![http://qiniu.milleros.com/19-12-17/Untitled%2016.png](http://qiniu.milleros.com/19-12-17/Untitled%2016.png) 到最后有两个协议要同意一下,勾选上点击保存就好了 ![http://qiniu.milleros.com/19-12-17/Untitled%2017.png](http://qiniu.milleros.com/19-12-17/Untitled%2017.png) #### 设置目标对象和内容 点击左侧菜单到应用程序内容 ![http://qiniu.milleros.com/19-12-17/Untitled%2018.png](http://qiniu.milleros.com/19-12-17/Untitled%2018.png) 点击开始 ![http://qiniu.milleros.com/19-12-17/Untitled%2019.png](http://qiniu.milleros.com/19-12-17/Untitled%2019.png) 根据实际情况勾选 ![http://qiniu.milleros.com/19-12-17/Untitled%2020.png](http://qiniu.milleros.com/19-12-17/Untitled%2020.png) 然后点击下一步、下一步到了最后点击提交就好了 ![http://qiniu.milleros.com/19-12-17/Untitled%2021.png](http://qiniu.milleros.com/19-12-17/Untitled%2021.png) #### 进入发布阶段 进入发布阶段的时候我们确保这些带完成按钮的选项都变成了绿色完成状态, ![http://qiniu.milleros.com/19-12-17/Untitled%2022.png](http://qiniu.milleros.com/19-12-17/Untitled%2022.png) 点击应用程序版本,再点击正式版后面的编辑版本 ![http://qiniu.milleros.com/19-12-17/Untitled%2023.png](http://qiniu.milleros.com/19-12-17/Untitled%2023.png) 然后点击右下角的审核,等他转完圈,会提示是不是有错误/警告,有错误的话就一定要解决掉才能发布,警告有些可以忽略有些不能忽略 ![http://qiniu.milleros.com/19-12-17/Untitled%2024.png](http://qiniu.milleros.com/19-12-17/Untitled%2024.png) 如果没有错误则继续点击右下角的按钮进入下一步,直到完成发布 之后可以点击资讯主页查看应用的发布状态 ![http://qiniu.milleros.com/19-12-17/Untitled%2025.png](http://qiniu.milleros.com/19-12-17/Untitled%2025.png) **至此Google Play Store的发布流程结束** 下面是App Store的发布流程 ## App Store 上架App Store**最好也要有科学上网**,因为没有的话访问会比较慢,或者是上传ipa包的时候直接就断连了。同样的整个流程我会按照必备账号、必备条件、必备文件、必备资料填写、发布流程来分类,有需要可自行在目录中点击跳转。 ### 必备账号 1. Apple ID 如果没有Apple ID的话需要在[Apple Developer](https://developer.apple.com/account/)自己创建一个账号,账号是通用的,所有Apple相关的服务都可以用 开发者有两种不同的身份,注意注册的时候要区分好 ![http://qiniu.milleros.com/19-12-17/Untitled%2026.png](http://qiniu.milleros.com/19-12-17/Untitled%2026.png) ### 必备条件 1. 账号支付了开发者费用 如果没支付费用,那你左边菜单栏就像这个一样 ![http://qiniu.milleros.com/19-12-17/Untitled%2027.png](http://qiniu.milleros.com/19-12-17/Untitled%2027.png) 那就要点击2处的链接或者直接进入[这里注册](https://developer.apple.com/programs/enroll/)给钱。 注册成功的界面是这样的,红色提示是说没绑定支付方式供自动支付 ![http://qiniu.milleros.com/19-12-17/Untitled%2028.png](http://qiniu.milleros.com/19-12-17/Untitled%2028.png) ### 开发必备文件 1. 测试版本的mobileprovision证书文件 2. 测试版本的p12证书文件 3. 推送p12证书文件(如果需要推送的话) 这里的mobileprovision和p12都是文件后缀,具体申请流程可以看这里; 关于推送的证书文件,Apple规定推送p12文件可以用正式版本的推送文件,所以我们可以只申请一个正式版的推送证书。 ### 打包正式包必备文件 1. 正式版本的mobileprovision证书文件 2. 正式版本的p12证书文件 3. 推送p12证书文件(如果需要推送的话) ### 发布必备文件/软件 1. Xcode软件(MAC平台) 2. [Appuploader](http://www.applicationloader.net/appuploader/download.php)(MAC/PC平台) 3. APP的ipa正式包 4. 屏幕截图 IOS App屏幕截图Apple要求必须要有两种,一种是6.5英寸(iPhone XS MAX)及以上新发布的机型截图,还有一种是普通的截图,5.5英寸设备截图,具体的尺寸在这里,如果没有XS MAX以上设备,那就可以找个iPhone X的截图对照这个表修改一下分辨率,保存的时候保存为jpg文件。更加详细的设备分辨率对照表可以查看[这里](https://help.apple.com/app-store-connect/#/devd274dd925)。 ![http://qiniu.milleros.com/19-12-17/Untitled%2029.png](http://qiniu.milleros.com/19-12-17/Untitled%2029.png) ### 发布必备资料 1. App名称 2. 隐私政策网址 3. 价格时间表:收费/免费 4. 销售范围:可全选所有区域/指定的某一个区域 5. 宣传文本:APP的一句话介绍 6. APP的详细介绍 7. 搜索你app的关键词、可以设置多个,多个关键词用英文状态下的逗号隔开 8. 技术支持网址:可以设置为公司网站个人网站 9. 版权所属 10. 有登录功能的需要提供一个测试账号 11. 联系信息:姓名,联系电话(区号+号码),用于审核的时候遇到问题,审核人员可以联系到我们 ### 发布流程 准备好所有资料后,就可以进入到发布流程中了。 #### 进入App Store Connect 进入[开发者后台官网](https://developer.apple.com/account/),点击右侧的App Store Connect ![http://qiniu.milleros.com/19-12-17/Untitled%2030.png](http://qiniu.milleros.com/19-12-17/Untitled%2030.png) 点击My Apps ![http://qiniu.milleros.com/19-12-17/Untitled%2031.png](http://qiniu.milleros.com/19-12-17/Untitled%2031.png) 点击+号创建 ![http://qiniu.milleros.com/19-12-17/Untitled%2032.png](http://qiniu.milleros.com/19-12-17/Untitled%2032.png) 我们进入之后到了这个界面 ![http://qiniu.milleros.com/19-12-17/Untitled%2033.png](http://qiniu.milleros.com/19-12-17/Untitled%2033.png) 我们先把App Information里面的内容填完,然后进入Pricing and Availability填写一下价格时间表 ![http://qiniu.milleros.com/19-12-17/Untitled%2034.png](http://qiniu.milleros.com/19-12-17/Untitled%2034.png) 然后点击左侧IOS APP下面的那一行,进去填写信息 ![http://qiniu.milleros.com/19-12-17/Untitled%2035.png](http://qiniu.milleros.com/19-12-17/Untitled%2035.png) 注意填写资料的时候,Build还有General App Information里面的信息先不填写,因为这里等我们上传了ipa包文件之后自动就会填充了。 ![http://qiniu.milleros.com/19-12-17/Untitled%2036.png](http://qiniu.milleros.com/19-12-17/Untitled%2036.png) #### 使用xcrun上传IPA文件 **这种方式只适合账号没开启[两步验证](https://support.apple.com/en-us/HT204915)的时候使用,如果开启了两步验证,则使用专用密码校验/上传包** > 两步验证就是当我们登录的时候会要求输入六位动态密码,有可能是短信,也有可能是登录了我们iCloud账号的Apple设备上的动态密码弹窗 ![http://qiniu.milleros.com/19-12-17/Untitled%2037.png](http://qiniu.milleros.com/19-12-17/Untitled%2037.png) 两步验证输入验证码 上传IPA文件需要用到**终端.app**,Xcode升级11之后,Apple为了更好的统一管理,发现打包上传的时候发现tools工具中没有Application Loader选项,推荐使用 xcrun altoos 或者 xcodebuild 上传应用程序。 使用xcrun 工具,如果以前没使用过或者没有这样添加操作为提示 xcrn: command not found,跟着下图的操作来一遍就行了 ![http://qiniu.milleros.com/19-12-17/Untitled%2038.png](http://qiniu.milleros.com/19-12-17/Untitled%2038.png) 在开发者中心去创建密钥apiKey 和apiIssuer ID,去到用户和访问 ![http://qiniu.milleros.com/19-12-17/Untitled%2039.png](http://qiniu.milleros.com/19-12-17/Untitled%2039.png) 打开秘钥页面 ![http://qiniu.milleros.com/19-12-17/Untitled%2040.png](http://qiniu.milleros.com/19-12-17/Untitled%2040.png) 在Active里面如果有有效的秘钥则直接点击下载下来,然后放在/Users/你的mac用户名/private_keys文件夹里面,如果没有的话就直接创建一个。 #### 打开终端,上传ipa包 **验证: xcrun altool --validate-app -f xxx.ipa -t ios --apiKey xxx --apiIssuer xxx --verbose** **上传: xcrun altool --upload-app -f xxx.ipa -t ios --apiKey xxx --apiIssuer xxx --verbose** 我们可以先验证一下ipa包,验证成功后会提示No errors Validate 上传成功会提示No errors uploading 如果验证/上传的时候提示了这种认证失败的错误,则有可能是你的账号开启了两步验证,那就不能用这种方式上传了 ![http://qiniu.milleros.com/19-12-17/Untitled%2041.png](http://qiniu.milleros.com/19-12-17/Untitled%2041.png) #### 使用专用密码提交IPA文件 要申请专用密码我们要进入[Apple ID官网](https://appleid.apple.com/#!&page=signin)登录,然后点击此处的生成密码 ![http://qiniu.milleros.com/19-12-17/Untitled%2042.png](http://qiniu.milleros.com/19-12-17/Untitled%2042.png) 输入一个标签之后点击创建,得到一个专用密码,**先别着急关了窗口** 我们要打开终端,使用以下命令 **验证: xcrun altool --validate-app -f xxx.ipa -t ios -u 你的Apple ID -p 刚刚生成的专用密码 --verbose** **上传: xcrun altool --upload-app -f xxx.ipa -t ios -u 你的Apple ID -p 刚刚生成的专用密码 --verbose** 我们可以先验证一下ipa包,验证成功后会提示No errors Validate 上传成功会提示No errors uploading 上传过程会很慢,经常出现网络问题,耐心等待,我上传了两三次才成功。 #### 上传成功后,在App Store Connect Activity会出现一个process状态的包 ![http://qiniu.milleros.com/19-12-17/Untitled%2043.png](http://qiniu.milleros.com/19-12-17/Untitled%2043.png) 完成后会显示为这样 ![http://qiniu.milleros.com/19-12-17/Untitled%2044.png](http://qiniu.milleros.com/19-12-17/Untitled%2044.png) 然后我们回到包管理界面,点击提交审核,就行了,提交前请检查好各项内容,否则会导致审核不通过或者无法提交审核。 ![http://qiniu.milleros.com/19-12-17/Untitled%2045.png](http://qiniu.milleros.com/19-12-17/Untitled%2045.png) ## 结语 至此,App的上传与发布到App Store/Google Play Store的教程就完成了,在你操作这全系列的过程中,可能还会遇到很多其他问题,请不要着急,**放心大胆地去操作,因为我们的每一步操作都是可撤销的,所以出现问题就多试几次,**上网寻找一下解决方案,这在很大程度上也会帮助我们锻炼自己解决问题的能力。 ### App开发/发布证书的生成 **这里我使用的是Mac平台,所以很多东西都操作比较方便,如果您使用的是windows平台,可以直接参考[AppUploader操作流程](http://www.applicationloader.net/blog/zh/2443.html)使用AppUploader操作证书生成/上传ipa文件** #### **创建App ID** 登录成功后选择Certificates, Identifiers & Profiles,在左侧菜单选择Identifiers,然后点击添加按钮 ![http://qiniu.milleros.com/19-12-17/Untitled%2046.png](http://qiniu.milleros.com/19-12-17/Untitled%2046.png) 选择App IDs,点击右上角的Continue按钮 ![http://qiniu.milleros.com/19-12-17/Untitled%2047.png](http://qiniu.milleros.com/19-12-17/Untitled%2047.png) 在Bundle ID处选择Explicit,填写自己项目的ID,例如京东APP的包名是com.jingdong.app.mall ![http://qiniu.milleros.com/19-12-17/Untitled%2048.png](http://qiniu.milleros.com/19-12-17/Untitled%2048.png) 如果应用需要使用推送功能,在下面的Capabilities列表中勾选上Push Notifications项,点击Continue ![http://qiniu.milleros.com/19-12-17/Untitled%2049.png](http://qiniu.milleros.com/19-12-17/Untitled%2049.png) 确认信息无误后点击Register,完成创建。 ![http://qiniu.milleros.com/19-12-17/Untitled%2050.png](http://qiniu.milleros.com/19-12-17/Untitled%2050.png) #### **申请请求证书,导入钥匙串** 不管是申请开发(Development)证书还是发布(Distribution)证书,都需要使用证书请求(.certSigningRequest)文件。 打开Mac上的钥匙串访问,选择左上角>钥匙串访问>证书助理>从证书颁发机构请求证书 ![http://qiniu.milleros.com/19-12-17/Untitled%2051.png](http://qiniu.milleros.com/19-12-17/Untitled%2051.png) 填写好资料,选择存储到磁盘,直接放在桌面就好了,这样等下好找,或者放到你指定的文件夹里面。 ![http://qiniu.milleros.com/19-12-17/Untitled%2052.png](http://qiniu.milleros.com/19-12-17/Untitled%2052.png) 成功之后可以得到一个这个文件 ![http://qiniu.milleros.com/19-12-17/Untitled%2053.png](http://qiniu.milleros.com/19-12-17/Untitled%2053.png) 进入[Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/certificates/list)页面,点击加号创建一个证书,这个证书要安装在你的Mac电脑上 ![http://qiniu.milleros.com/19-12-17/Untitled%2054.png](http://qiniu.milleros.com/19-12-17/Untitled%2054.png) 根据内容提示选择对应的选项,开发的就选择开发的,正式的就选择正式的 如果是个人或公司账号,选择iOS Distribution (App Store and Ad Hoc),如果是企业账号,则选择In-House and Ad Hoc,点击Continue ![http://qiniu.milleros.com/19-12-17/Untitled%2055.png](http://qiniu.milleros.com/19-12-17/Untitled%2055.png) 三种证书的区别 ![http://qiniu.milleros.com/19-12-17/Untitled%2056.png](http://qiniu.milleros.com/19-12-17/Untitled%2056.png) 然后点击下一步,这里选择上传刚刚我们在电脑上生成的那个文件 ![http://qiniu.milleros.com/19-12-17/Untitled%2057.png](http://qiniu.milleros.com/19-12-17/Untitled%2057.png) 点击下一步完成之后,去到证书的详情页点击下载 ![http://qiniu.milleros.com/19-12-17/Untitled%2058.png](http://qiniu.milleros.com/19-12-17/Untitled%2058.png) 下载下来是个这个文件 ![http://qiniu.milleros.com/19-12-17/Untitled%2059.png](http://qiniu.milleros.com/19-12-17/Untitled%2059.png) 双击这个证书,就可以把它安装在电脑上了,若弹出安装提示,选择安装到“登录”,在钥匙串中找到安装的证书,若提示此证书是由未知颁发机构签名的,请[下载Apple Worldwide Developer Relations Certification Authority](http://developer.apple.com/certificationauthority/AppleWWDRCA.cer)证书进行安装。 在左边选择“登录”和“我的证书”,找到证书,在证书上面点击鼠标右键,然后在菜单中选择导出证书 ![http://qiniu.milleros.com/19-12-17/Untitled%2060.png](http://qiniu.milleros.com/19-12-17/Untitled%2060.png) 在弹出页面中指定证书名,点击存储,然后输入证书密码(千万要记得),点击好,生成p12格式证书。 ![http://qiniu.milleros.com/19-12-17/Untitled%2061.png](http://qiniu.milleros.com/19-12-17/Untitled%2061.png) 至此,证书生成流程演示完毕,必备文件列表中还有一些其他类型的证书文件,可以参考本流程生成对应的文件,都是大同小异的。 ![http://qiniu.milleros.com/19-12-17/Untitled%2062.png](http://qiniu.milleros.com/19-12-17/Untitled%2062.png) ### 参考资料 1. [iOS之从创建(Development、Distribution)证书到发布 ——— 真的真心瓜子](https://www.jianshu.com/p/304ec98842e1) 2. [Xcode 11 使用xcrun altool 密钥上传ipa包 ——— YOrange](https://juejin.im/post/5da3cae951882555a84302f7) 3. [记Google Play应用签名计划带来的问题 ——— cp_Mark](https://blog.csdn.net/cp_Mark/article/details/80674533) 4. [2019苹果APP真机测试及上架App Store流程 ——— qiutianbiao](http://www.applicationloader.net/blog/zh/2443.html)
继续阅读

第一次参加LiveHouse

MILLEROS 发布于 2019-12-15

## 前言 昨天去参加了一次艾热的Live House演唱,因为家属的原因,第一次主动去看、听说唱。讲真其实全程很难融入进去,但是律动的音乐还是很有感染力的。最起码听不清歌词也能跟着晃一晃。 ## 初来乍到 Live house被设置在了嘿吼小镇,在赤湾地铁站出来还要走段路,就是在左炮台山脚下,还是挺远的。接近嘿吼小镇的时候,有种柳暗花明的感觉,因为是一转角就从冷清转为人声鼎沸。 ![LiveHouse/DDB1D44B-233F-4C67-8906-2A501998E409.jpeg](http://qiniu.milleros.com/DDB1D44B-233F-4C67-8906-2A501998E409.jpeg) 一开始怕走错了,然后看到有个嘿吼的牌子才确定没错。 入眼就是大片的人,估算了一下大概有一两千人在现场等候。一开始我还在担心,这么多人在这里聚集,他要怎么安检,然后就看到摆了三台传动扫描机在队伍后面。 讲真在大陆参与者中大型活动还是挺放心的,检查流程大致都是先验票,然后过安检机器,然后就是查身份证。过程加起来虽然繁琐,但是安全性很有保障。 对了,入场的时候还让把水杯扔掉,我一开始还以为他们要恰烂饭(在场内单独设置高价饮料售卖区域),然后过完安检发现原来是没有检查液体的机器装置,可能也是为了防止有人携带危险品。 ## 等待入场 深圳已经进入冬天有段时间了,虽然一直冷热反复无常,但是不可改变的早晚温度差还是挺寒人的。不过人多,现场除了阵阵的凉意之外就只有嘈杂带来的燥热了。 现场的观众真的是走在时髦的前沿,女生男生都是打扮的漂漂亮亮的。 大概看了一下现场男生的平均身高,发现好多人都比我高了一个头,女生的话高个子比较少,大部分像我家属那般高,那我应该不会受太大影响,家属那边我就比较担忧了。 ## 入场等候 到了七点就准时放大家入场了,但是门小人多,大家都想挤进去,然后大门那边就挤死人,大家都想去抢前排,虽然最前面的已经被提前入场的人占据了,但是能前一点那当然好。 最后我和家属到达的位置在距离舞台十米左右。看的还是挺清晰的。 现场的灯光很炫,各种射灯啥的都往脸上糊,还有RGB加成。 ![LiveHouse/8997CF12-E1BA-414C-8540-FE0185ABE6E3.jpeg](http://qiniu.milleros.com/8997CF12-E1BA-414C-8540-FE0185ABE6E3.jpeg) 这一等就直接让大家等了一个小时,因为是八点开始,我们七点就入场了,在那站着,然后听放出来的音乐,站到五十多分的时候还是挺烦躁的,想着等下要是晚了还不锤死他。 中途去上了次洗手间,那人是真的多,不过大家也还挺体谅的,一般从身边路过都会主动让一下。 人群中还固定散落着许多安保人员,这点让人真的放心安全问题。 上完厕所回来的时候刚好开始,然后找不到家属,当时内心真的是慌的一匹。因为我到了大概的位置,就让她打开手机锁屏举起来,然后我就有惊无险的找到她了。 ## 演出开始 没想到的是,艾热他说自己已经在深圳生活了两年了,开场就说了很多鼓励大家的话,让我觉得还是挺正能量的,可能是我之前对说唱歌手有误解,还是说艾热只是特例独行的说唱歌手,这个我不得而知,因为现在近距离接触过的也只有艾热一人。 ![LiveHouse/997B0511-7F9F-4396-AADB-B73132CC4B33.jpeg](http://qiniu.milleros.com/997B0511-7F9F-4396-AADB-B73132CC4B33.jpeg) 第一次参加真的感觉效果还是蛮震撼的,不过不知道是不是我个人的原因,我总听不清楚歌手的吐词,可能是音响系统出来的声音很嘈杂导致的,很多时候都只能听个响;或者也有可能是我事先没听过他的歌,所以不知晓他的歌词,所以就会很难听懂。 ![LiveHouse/2720A555-4522-4DB0-BDB8-295E0409CCBA.jpeg](http://qiniu.milleros.com/2720A555-4522-4DB0-BDB8-295E0409CCBA.jpeg) 不过今晚艾热唱的情歌比较多,说唱歌曲感觉还是有点少的,后面他解释说想让大家更多的了解他创作的多样性。 ![LiveHouse/8D7A0F8B-2D36-4DCA-8D62-F5C8613FB107.jpeg](http://qiniu.milleros.com/8D7A0F8B-2D36-4DCA-8D62-F5C8613FB107.jpeg) ## 演出嘉宾 这次艾热总共请了两个嘉宾来助场,一个是王以太,一个是大傻。这几年我比较熟悉大傻,因为家属很喜欢吴亦凡,所以之前会和她一起看中国新说唱,里面大傻就是吴亦凡战队的,所以我看了一点,也听过他的歌,还是蛮喜欢的一个歌手。不过不是很喜欢他的大花臂,像黄老板那样的,我不喜欢。 ![LiveHouse/158417CC-874D-4681-9FB9-002740D0BE38.jpeg](http://qiniu.milleros.com/158417CC-874D-4681-9FB9-002740D0BE38.jpeg) 大傻的现场还是燥的,蛮喜欢这种氛围的。王以太的话我没咋关注,因为熟悉的他唱的歌,他没唱,他说要留在自己演唱会唱,估计一个是不想抢了艾热的风头,一个是为自己拉票。 ![LiveHouse/A84F89F2-FFC2-4FC8-85ED-6D08C01D0077.jpeg](http://qiniu.milleros.com/A84F89F2-FFC2-4FC8-85ED-6D08C01D0077.jpeg) 毕竟千人千面,不好说大家都能理解,所以还是选择不唱比较好。 两个助场嘉宾出场后,我感觉还是艾热和大傻比亲密一点,因为他俩互动很多,而且也能看出来好兄弟的情谊很浓。 ![LiveHouse/794D15BA-A6BB-43A6-87A5-E965951ACA27.jpeg](http://qiniu.milleros.com/794D15BA-A6BB-43A6-87A5-E965951ACA27.jpeg) 这里拍了一张大家举起闪光灯的照片,人太矮了,没得办法,这个质量算好了。 ## 演出结束 经历了三个小时后的站立,感觉腿都要断了,但是大家还是热情不减,后面手机都快没电了,我没怎么拿出来拍照了,拍照是真的耗电,这次拍的视频比较多,这里不好放就不放置了。 结束的时候已经十点多了,不过回去地铁站还是有地铁可以乘坐的。 ## 结语 这一次Live House下来,还是挺有感觉的,最起码整个人是真的得到了放松,因为脑子里只有站着、听歌,不过嗨就完事了。 经过这次活动,我蛮期待我下次能去草莓音乐节听听现场的新裤子,九连真人,到时候视经济情况再定。
继续阅读

Tailwind.css的使用----基于nuxt.js

MILLEROS 发布于 2019-09-24

## 安装Tailwind 在终端项目文件夹下使用npm或yarn安装 ```bash npm install tailwindcss --save-dev # or yarn add -D tailwindcss ``` ## 创建Tailwind配置文件(不是必须的) 在终端使用npx生成 ```bash npx tailwind init ``` 成功后可以在项目根目录内看到tailwind.config.js ```javascript // tailwind.config.js module.exports = { theme: { extend: {} }, variants: {}, plugins: [] } ``` ## 引入tailwind 在assets/css内创建tailwind.css文件,并引入 ```css @tailwind base; @tailwind components; @tailwind utilities; ``` ## 在nuxt.config.js内配置postcss 首先我们要在nuxt.config.js内引入path模块 ```js const path = require('path'); ``` 然后配置postcss ```js // 引入我们刚刚创建的tailwind.css css: [ '~assets/css/tailwind.css', ], // 配置postcss build: { postcss: { plugins: { tailwindcss: path.resolve(__dirname, './tailwind.config.js') } } } ``` ## 使用 ```html <div class="bg-purple text-white sm:bg-green md:bg-blue md:text-yellow lg:bg-red xl:bg-orange"> Test </div> ``` ## 参考资料 - <a href="https://regenrek.com/posts/how-to-use-tailwind-css-1.0.1-in-nuxt/" target="_blank">How to use Tailwind CSS 1.0.1 in Nuxt</a> - <a href="https://tailwindcss.com/docs/installation" target="_blank">Tailwindcss.com</a>
继续阅读

勇于承认自己的不足和错误

MILLEROS 发布于 2019-08-28

“一个有勇气承认自己错误的人,也可以得到某种满足感。这不仅是消除罪恶感和自我辩护的气氛,而且有利于解决实质性问题。” ----《卡耐基沟通的艺术与处世智慧》 我记得从很小的时候开始,我就比较喜欢与人争论错误。 在我大致四五岁时,坊间流行养蚕,一只只肥肥的蚕虫是当时和小伙伴课余时间作对比最大的乐趣。但是当时家里经济困难,我自己手上是没有蚕儿的,所以那时候特别喜欢去邻居家玩耍,当时小伙伴家放在茶几上的蚕卵,一颗一颗的产在纸板上,听小伙伴说里面有些都是坏的,随即用手去挤破某些蚕卵;当时的我比较不懂事,也跟着挤破。后来我回到家被小伙伴的哥要求赔偿了,当时的我根本就不承认这事是我干的,以至于在后面很长一段时间内我都是上学被堵路的。 现在想起,虽然我还是很气愤当时小伙伴哥哥的做法,但是确实是我自己的问题,如果我当时用于承认错误,情景会不会是另一番呢。不得而知。 后面慢慢长大其实我有了一些对自己的思考,当时从深圳回去老家读高三的时候,在回去前在我心里萦绕许久的不是如何适应老家的高三生活,而是怎么向他们展示“大城市”出来的孩子的品行,这样的行为让现在的我很不齿,但是得承认在那段时间内,这样的想法对我的改变确实很大---有可能并没有在学业上有什么帮助。 在高三的那个班级里,有个同学很喜欢向他人炫耀自己所拥有的东西,以至于大家都很不喜欢和他玩耍,毕竟在那时候的环境里,大家都是家境不太好,要么是留守儿童,要么就是真的家庭困难的。他这样的做法很让人反感,台下就没安静过。 后来有次办理举行元旦晚会的安排,他因为是班干部的原因,被要求上台说两句话。大家其实很不情愿,毕竟大家当时都不喜欢他。 “在过去的一段时间里,我都比较倾向于向人展示自己的东西,这段时间我考虑了很多,这确实是我的问题。” 台下的人纷纷应和,我看到台上的他其实是有点尴尬的。后来下台我问他为什么要这么做,他只说了一句话: “说出来的感觉太好了,我憋了很久,原来承认自己的不足,看到大家了解了更多的我,我很高兴。” 这完全不是我编写的故事,试想一下,如果当时他还是照例讲一些希望大家和谐相处的意见,或者是指责大家对他的偏见,那样一点好处都没有,只能是当时愉快了一下自己---当众打那些不喜欢我的人的脸,多爽啊。因为这个事情确实也让我有了很多的想法。 在上大学期间,我比较喜欢自己安安静静捣鼓东西,而且当时我有一个怪癖---如果有人打扰到我做事情,我会毫不犹豫的对他倾泻我的怒火,通常是很短很暴躁的怒火,当然伤害也是很大的。 有一次我正在做网站建设方面的工作,我感到我的身后突然被拍了一下,一瞬间我的怒火就来了,但是我忍住了,但是又来了一下,我顿时就忍不住了,转身就是一顿臭骂,后面发现是我的同学来找我去玩,他很严肃的和我说“你就是喜欢这样随意发泄你的怒火,我就找你玩一下,并没有打扰你工作的意思,你就乱扣帽子”,那一次之后的很长一段时间,那个同学都没有来找过我了,我一直在反思自己,我一开始觉得就是他的问题,他不该那样打扰我,我发泄自己的情绪一点问题都没有。 在后面的一次课上,班级很多人迟到,任课老师在课堂上大发雷霆,大家都不敢说话,但是很多人在群里说老师大题小做,或者是其他一些话指责老师不对。后来老师气消了,大大方方的承认自己的问题所在,并保证后面不再这样了,并且向同学们说明迟到确实不好。然后我看到群里开始出现了很多人对老师的理解,并且课堂上同学们也表现出了更大的积极性,后面的课堂也极少出现迟到的情况了。 我说这个事情不是想说明“打一巴掌给颗糖有奇效”的战略奇效。我是想通过我的做法和老师的做法做一个对比。试想一下如果当时老师和我的做法一样,死不承认自己的问题,把所有的责任都推到对方身上,那样同学们只会越来越厌恶这个老师。而且迟到的问题也根本不会得到解决。 一个有勇气承认自己错误的人,也可以得到某种满足感。这不仅是消除罪恶感和自我辩护的气氛,而且有利于解决实质性问题。 前一段时间在网络上闹得沸沸扬扬的cxk事件,大致情况是一个“奶油练习生cxk”成为了NBA形象大使,这让很多篮球爱好者都很愤慨,觉得这是对篮球事业的侮辱,篮球事业不能被一个“娘炮”所代表。这件事情随后蔓延到全网络,越来越多人加入了对cxk的口诛笔伐,从而衍生出了一个流行词汇“鸡你太美”,而在这个网络舆论下,cxk背后的团队居然还对当时的某个网站发出了律师函,觉得这一切的负面问题都是这些网站传播导致的。这一操作再次引爆了全网络。 与之对比的是,当时的另一个明星wyf,原先也是流量起家,因为“大碗宽面”的rap,而被全网人做成了鬼畜视频。他并没有像cxk这般到处律师函,而是发布了单曲《大碗宽面》,来自嘲这件事。 其实这两个例子不能说是承认自己的错误,只能说是认识自己的不足,并且接受大众的指责和不满,但是这两手不同的操作确实导致了两种不同的效果---而且是差距如此之大。 **“用争斗的方法,你永远不会得到满足;但用让步的方法,你的收获将比你期望的更多。”这个道理人人都懂,只是实行起来往往会因为我们的瞬时情绪以及其他一些看法而造成困难。但是不管如何,积极承认自己的错误、不足只会给你加分。**
继续阅读

outlook 已知的兼容问题

MILLEROS 发布于 2019-08-27

## OutLook的宣言是不离不弃 是不是很讨厌为Email代码兼容Outlook? 太遗憾了!虽然光都有尽头,但对Outlook的代码兼容没有。 为了应付Email的怪癖,我们花了很多时间测试,确保我们搞定了所有Outlook的坑洼沟洄。在这个指导中,我们会分享一下数年来我们应付这种烦人的邮件客户端的编程经验,主要包括四个部分: 1. Outlook必知的17个tricks 2. 移除table间距的3种办法 3. 移除Outlook2013图片间距 4. Outlook 2007、2010中的CSS padding ## 1. Outlook必知的17个tricks 为Outlook设计就像追逐行踪飘忽的大白鲸。我们花费了数小时研究,跟踪缺陷和故障,努力在一个不完善的环境中实现像素级别的完美展示。虽然Outlook时最难啃的骨头,但是搞定它仍是当务之急。 下面是17条改善Outlook上Email的建议: ### 1.1 忽略图片的margin和padding Outlook 2007-2013不支持image的margin与padding样式,必要的时候尝试**hspace**和**vspace** ```html <img src="http://www.emailonacid.com/example.jpg" align="left" vspace="10" hspace="10" /> ``` 或者直接在图片内预留好间隙(要是遇到屏幕适配就麻烦大了)。 ### 1.2 文本不自动折行 对于table中的文字,比如 aaaaaaaaaaaaaaaaa ,如果希望它们自动折行,需要这样 ```html <td style="word-break:break-all;"> ``` ### 1.3 Outlook自动为table cell 添加1px border 如果table使用了背景颜色可以看到td有1px的白边,在内嵌的style中加入样式 ```css table td { border-collapse: collapse; } ``` 或者使用内联样式 ```html <td style="border-collapse: collapse;">... </td> ``` ### 1.4 Outlook有时忽略link的样式 如果a标签没有href属性,那么Outlook 2007和 2013将不支持其内联样式 ```html <a style="font-size: 20px; color: #004990;">Shop Flooring </a> ``` 这种没有作用,应该修改为 ```html <a href="http://www.test.com" style="font-size: 20px; color: #004990;">Shop Flooring </a> ``` ### 1.5 用table实现重要的间距 Email中最安全的呈现间距方式就是使用table ```html <table cellpadding="2" cellspacing="2" border="0"> <tr> <td valign="top">•</td> <td>Test</td> </tr> <tr> <td valign="top">1.</td> <td>Test</td> </tr> </table> ``` ### 1.6 使用cellpadding和cellspacing做间距 不要在table上使用margin和padding,cellpadding和cellspacing是比较安全的方式,可以达到padding和margin的效果 如果使用table的align属性 ```html <table align="left"> ``` 情况会有些小复杂,后续提到 注意;对table的对齐属性要额外的小心,你永远不知道这会对其渲染引擎(Word rending engine)造成什么影响。因为它会有可能使用自己的定位,而不是用我们设定的对齐属性 ### 1.7 Outlook有时会移除padding Outlook 2007 和 2010 会把div和h1~h6转换为p并包裹上span,这个会把html的容器从块元素转换为内联元素 ```html <h2 style="font-size: 15px; font-weight: bold; margin: 0; padding: 17px 0 0 0;"> NEW FASHIONS </h2> ``` 会被解析为 ```html <p class= style='mso-outline-level:3'> <b> <span style='font-size:11.5pt'> NEW FASHIONS<o:p></o:p> </span> </b> </p> ``` 一个解决办法就是把padding加到td上 ```html <td style="padding: 17px 0 0 0"> <h2 style="font-size: 15px; font-weight: bold; margin: 0"> NEWFASHIONS </h2> </td> ``` 另外一个解决办法就是不使用div和h1~h6,使用p ### 1.8 使用 MSO 控制行高 在使用行高前添加mso-line-height-rule:exactly ```html <td style="mso-line-height-rule:exactly; line-height:50px;"> ``` 这只在块元素上有效,所以想在font或span中用就洗洗睡了吧,这只是微软的CSS属性,对其他客户端无效 ### 1.9 Outlook按字面解释 确保td的**rowspan**属性 被设置了正确的值,大部分浏览器对错误设着的rowspan宽大处理了,但是Outlook 07 和10 会按照你声明的值解释渲染,如果声明多了就凉了,即使table中没有那么多也一样渲染 ### 1.10 Outlook有时忽略width和height 带有width和height的块元素可能显示和预期不一致,如果水平和垂直空间时由email内容决定的,但是有时候自定义的空间和对齐不好使,所以为了最好效果,可以使用透明的图片占位或者table cell的height属性来设置宽高。 ### 1.11 td有最矮2px的限制 在Outlook 2007和2010中,td最小高度被限制为2px,如果使用td里面是1px的透明gif和背景颜色,还会出现一个水平条 ### 1.12 背景图片需要使用vml 在Outlook 2007、2010、2013中背景图片不使用VML的话是不起作用的。VML是一个基于XML的过时的二维矢量图形文件格式,是Office Open XML标准的一部分。这个问题很难搞定,可以看看[这篇博客](https://www.emailonacid.com/blog/article/email-development/emailology_vector_markup_language_and_backgrounds/ 'Vector Markup Language and Backgrounds')看看这么使用VML搞定背景图片 也可以使用[Bulletproof background images](https://backgrounds.cm/)搞定。 ### 1.13 Outlook把一些div转换为p Outlook经常会把div转换为p,这个我们真心不理解什么时候和为什么,貌似是个误无解的问题。 ### 1.14 Outlook使用word渲染引擎 Outlook使用text boundaries 和page breaks略微有些不同,Text boundaries被用来在打印页面的时候分离页面元素。并不输出HTML代码 根据我们研究,我们了解到text boundaries 最大可以拉伸到23.7英寸高(1790px),超过了就会自动插入分割线然后创建新的text boundary。因此如果email中的table的高度高于1790px,它就会变成多个 最好的解决办法就是不要搞这么高的table,每次添加新的table都会创建新的text boundry,只要table不超过1790px就不会创建多个text boundry 使用table嵌套的时候要注意内层的table别超过了,适当拆分一下,重构比较麻烦,设计的时候注意一下最好。 ### 1.15 太高的图片会被自动剪切 **在Outlook中图片最高1728px,超过1728 的部分会被截掉**(怎么会有人这么丧心病狂在Email中放这么高的图片) 我们也见过Outlook自动缩小图片,使其最高位1827px 我们建议你剪裁图片,堆叠在一起 ### 1.16 定义了尺寸的图片可能渲染不正确 拉伸图片可能不会正确的渲染,所有的图形应该使用属性来设置宽高大小。不要依赖于CSS定义图片尺寸,这点对于Email很重要 ```html <!--有效--> <img src="cid:ALogo" width="500" height="200"> <!--无效--> <img src="cid:ALogo" style="width: 500px;height: 200px"> <!--无效--> <img src="cid:ALogo" width="500px" height="200px"> ``` ### 1.17 带动画的gif不支持 Outlook不支持带动画的gif,只会展示第一帧,这也太TM丧心病狂了,gif图都不支持啊!!!! ## 2. 移除table间距的3种办法 当我们尝试左右并列table或者上下堆叠table的时候会发现Outlook 2007 和 2010 有些小问题,如果发现不想要的间距,可以看看下面的方法 **步骤1** 在css中添加border-collapse属性 ```html <style type="text/css"> table {border-collapse: collapse;} </style> ``` **步骤2** border、cellpadding、cellspacing设为0 ```html <table border="0" cellpadding="0" cellspacing="0"> ``` **步骤3** 如果为table添加了align属性则还需要做些事情 在这种情况下,可能会发现table间十分大的间距,下面说一下解决办法 1. 为td添加bgcolor属性,根据你的布局选颜色 2. 为table添加1px的border,颜色和td一样 3. 为了适应border,减少table的宽度2px 4. 把第一个td的内容用p包裹,添加样式mso-table-lspace:0;mso-table-rspace:0; ```html <style type="text/css"> table { border-collapse: collapse; } </style> <table border="0" width="600" cellspacing="0" cellpadding="0"> <tr> <td> <table align="left" width="198" border="0" cellpadding="0" cellspacing="0" style="border:1px solid #00CC99"> <tr> <td bgcolor="#00CC99"> <p style="mso-table-lspace:0;mso-table-rspace:0;">Content in 1</p> </td> </tr> <tr> <td bgcolor="#00CC99">Content in 1</td> </tr> </table> <table align="left" width="198" border="0" cellpadding="0" cellspacing="0" style="border:1px solid #33FFFF"> <tr> <td bgcolor="#33FFFF"> <p style="mso-table-lspace:0;mso-table-rspace:0;">Content in 2</p> </td> </tr> <tr> <td bgcolor="#33FFFF">Content in 2</td> </tr> </table> <table align="left" width="198" border="0" cellpadding="0" cellspacing="0" style="border:1px solid #993366"> <tr> <td bgcolor="#993366"> <p style="mso-table-lspace:0;mso-table-rspace:0;">Content in 3</p> </td> </tr> <tr> <td bgcolor="#993366">Content in 3</td> </tr> </table> </td> </tr> </table> ``` 关于这种方法的说明 这个问题只会在在table的align被设置为left|right的时候发生 只需要把第一个td的内容用p包裹 如果table cells有不同颜色就得再套一层table了 这种方法对于image堆叠不起作用,最好把图片做成一个这种情况 ## 3. 移除Outlook2013图片间距 Outlook2013填了一些坑,但又挖了新坑。堆叠的图片会显示大概10px的间距(WDNMD 这简直就是巨坑啊)。 这个问题只在图片高度小于20px的时候出现,开心的是通过简单的小技巧就可以解决:为td设置和图片高度一样的行高 虽然作者说不知道为什么,我们可以大胆猜测一下,image时行内元素,Outlook为td设置了1.3之类的行高,这样上下就会有间距,设了行高就行了,也有人提到行高设为0或者image设为display:block都能解决 ```html <td width="600" height="80" style="line-height: 80px;"> <img height="80" src="http://www.website.com/images/Nature_01.jpg" width="600" /> </td> ``` ## 4. Outlook 2007、2010中的CSS padding 针对Outlook设计的人都很清楚已经被table绑架了,只能用很少的css,如果认为仅仅是这样,那你就太年轻了,Outlook会把你的代码转换为微软的word代码。 比如说很重要的就是Outlook经常去掉div和h1~h6,换为p和span,有时候保留外部的container有时候删除 ```html <h2 style="font-size: 15px; font-weight: bold; margin: 0; padding: 17px 0 0 0;">NEW FASHIONS</h2> <div style="font-size: 15px; font-weight: bold; margin: 0; padding: 17px 0 0 0;">NEW FASHIONS</div> <p style="font-size: 15px; font-weight: bold; margin: 0; padding: 17px 0 0 0;">NEW FASHIONS</p> ``` 上面的代码被更换为 ```html <style> p.MsoNormal, li.MsoNormal, div.MsoNormal { mso-style-unhide: no; mso-style-qformat: yes; mso-style-parent: ""; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: "Times New Roman", "serif"; mso-fareast-font-family: "Times New Roman"; mso-fareast-theme-font: minor-fareast; } h2 { mso-style-priority: 9; mso-style-unhide: no; mso-style-qformat: yes; mso-style-link: "Heading 2 Char"; mso-margin-top-alt: auto; margin-right: 0in; mso-margin-bottom-alt: auto; margin-left: 0in; mso-pagination: widow-orphan; mso-outline-level: 2; font-size: 18.0pt; font-family: "Times New Roman", "serif"; mso-fareast-font-family: "Times New Roman"; mso-fareast-theme-font: minor-fareast; } p { mso-style-noshow: yes; mso-style-priority: 99; mso-margin-top-alt: auto; margin-right: 0in; mso-margin-bottom-alt: auto; margin-left: 0in; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: "Times New Roman", "serif"; mso-fareast-font-family: "Times New Roman"; mso-fareast-theme-font: minor-fareast; } </style> <h2 style='margin:0in;margin-bottom:.0001pt'> <span style='font-size:11.5pt; mso-fareast-font-family:"Times New Roman"'> NEW FASHIONS <o:p></o:p> </span> </h2> <div> <p class=MsoNormal> <b> <span style='font-size:11.5pt;mso-fareast-font-family:"Times New Roman"'> NEW FASHIONS <o:p></o:p> </span> </b> </p> </div> <p style='margin:0in;margin-bottom:.0001pt'> <b> <span style='font-size:11.5pt'> NEW FASHIONS <o:p></o:p> </span> </b> </p> ``` 反正我是没眼看了(小声哔哔)。 转换之后本来对块元素设置的css到了span上,很多就不好使了,为了避免作者建议使用margin,其实我觉得还是老老实实使用table得了 原文地址: https://www.emailonacid.com/blog/article/email-development/tips_and_tricks_outlook_07-13/
继续阅读

使用NodeJS爬取知乎热榜

MILLEROS 发布于 2019-08-01

之前用Python写过一些小爬虫,但是作为前端,我还是对Nodejs比较熟悉,由于最近大量的APP下架,想看很多咨询都看不来,遂便想着利用Nodejs写一个爬虫,爬取我想要看的网站的热门内容。 **本文所述的内容需建立在您对Nodejs有一定了解,能独立编写一些简单的请求响应功能,高级部分则要求更高** 废话不多说,具体流程请看下面 ## 1. 准备工作 在工作开始之前我们需要准备以下几个条件: ### 1.1 基础软件 - Nodejs v8.9.3 - npm v6.7.0 > 以上软件的安装可以查看Nodejs官网 ### 1.2 npm软件包 - koa //Nodejs的框架,和express类似 - iconv-lite //用来转换网站字符编码 - axios //用来请求目标网站/接口 - cheerio //用来解析目标网站返回的html页面,使得可以在服务端类似客户端用jQuery操作DOM一样操作解析后的html元素 > 关于以上的软件包具体的使用方法可以去google搜索官方文档查看 ## 2. 开始 ### 2.1 安装依赖 首先我们建立一个项目文件夹NodejsFetchZhihu.com ```bash $ mkdir NodejsFetchZhihu.com $ cd NodejsFetchZhihu.com/ $ npm init -y //初始化此文件夹为Nodejs项目文件夹,生成一个package.json 文件 $ npm install koa iconv-lite axios cheerio --save //安装1.2内的依赖 ``` ### 2.2 建立http服务 新建文件server.js ```jsvascript const Koa = require('koa'); const app = new Koa(); app.use(async ctx => { //此拦截器表示所有请求进来我们都返回hello World ctx.body = 'Hello World'; }); console.log('站点建立在http://localhost:3000上'); app.listen(3000); ``` ### 2.3 运行 到这里,我们已经初步建立了一个可访问的http站点 ```bash $ node server.js //运行 站点建立在http://localhost:3000上 ``` 访问 http://localhost:3000 我们就可以看到页面返回了hello World ![hello world](http://qiniu.milleros.com/19-8-1/45563202.png "hello world") ## 3. 爬取 开始前我们要先打探好我们要爬取的网页的地址,例如我们此刻要爬取的是知乎热榜,那么可以从下图看到知乎热榜的页面地址是 https://www.zhihu.com/hot ![爬取](http://qiniu.milleros.com/19-8-1/Snipaste_2019-08-01_10-46-31.png "知乎热榜") ### 3.1 修改中间件并爬取 我们刚刚server.js设定的所有请求都会直接返回“hello world”,现在将它修改成请求知乎热榜页面 > 此处我们使用axios请求目标地址,具体文档请参考axios官网 ```javascript const Koa = require('koa'); const axios = require('axios'); const app = new Koa(); app.use(async ctx => { //此拦截器表示所有请求进来我们都返回hello World let _res = await axios.get('https://www.zhihu.com/hot').then((res) => { return res }); ctx.body = JSON.stringify(_res.data); }); console.log('站点建立在http://localhost:3000上'); app.listen(3000); ``` 运行后我们可以看到我们的站点返回了以下内容 ![返回结果](http://qiniu.milleros.com/19-8-1/Snipaste_2019-08-01_10-55-54.png "返回结果") 我们可以看到返回结果里面有一些登录字样,这说明知乎热榜是需要登录才能查看的,所以这里我们需要设定以下axios的请求头 我们将代码修改成下面所示 ```javascript const Koa = require('koa'); const axios = require('axios'); const app = new Koa(); app.use(async ctx => { //此拦截器表示所有请求进来我们都返回hello World let _headers = {}; _headers.cookie = '你的cookie'; let _res = await axios.get('https://www.zhihu.com/hot', { headers: _headers }).then((res) => { return res }); ctx.body = JSON.stringify(_res.data); }); console.log('站点建立在http://localhost:3000上'); app.listen(3000); ``` 这里涉及到一个概念,就是**网站cookie**,但是在这我就不赘述这个是什么,以及原理,我们只需要知道这个cookie是用来记录我们是否登录了zhihu.com的标记。 那么怎么获取cookie呢?这里我们首先**打开并登录**https://www.zhihu.com/hot 然后打开开发者工具,切换到Network面板,随便点击一个请求(如果没有则刷新一下页面),在右侧的headers里面往下翻可以看到我们的cookie ![cookie 在哪里](http://qiniu.milleros.com/19-8-1/Snipaste_2019-08-01_11-03-39.png '查看cookie') 复制这段cookie,粘贴到代码里面的“你的cookie”处,然后保存运行。 ![返回结果](http://qiniu.milleros.com/19-8-1/Snipaste_2019-08-01_11-09-08.png '返回结果') 我们可以看到,这里的红线处已经有了知乎热榜上的内容了 ### 3.2 解析网页结果 首先我们先分析知乎热榜的页面结构: ![知乎热榜页面结构](http://qiniu.milleros.com/19-8-1/Snipaste_2019-08-01_11-35-40.png '知乎热榜') 从上图我们可以看到,知乎热榜,用来显示热榜数据的div类名叫做**HotList-list**,单个知乎热榜信息的条目类名是**HotItem**,所以我们先获取条目列表 ```javascript const cheerio = require("cheerio"); //用于解析html文本 //取基本数据 const _list = $('html').find('HotList-list'); ``` 然后遍历条目,假设这里我们有需求取知乎热榜记录中的标题和热度 ```javascript let _dataTmp = []; //下发到客户端的数据 await _list.each((_index, _el) => { const _item = $(_el); let _itemTmp = {}; //存储类目值 let _title = _item.find('.HotItem-title').text().replace(/[\r\n]/g, ''); //标题 let _Hot = _item.find('.HotItem-metrics').text().replace(/[\r\n]/g, ''); //热度 _itemTmp = { title: _title, hot: _Hot }; _dataTmp[_index] = _itemTmp; //下发到客户端的数据 }); ``` 查看完整代码 ```javascript const Koa = require('koa'); const axios = require('axios'); const cheerio = require("cheerio"); const app = new Koa(); app.use(async ctx => { //此拦截器表示所有请求进来我们都返回hello World let _headers = {}; _headers.cookie = '_zap=0b7d5fde-a8b3-4d26-93d6-292ee4aa6488; d_c0="AKDnag8Xcg6PTg2vbEVtmCLLSwxAuwaqJKw=|1540954624"; _xsrf=a5bf2345-7d9d-4e96-82bf-02c7dcb4c9f2; l_n_c=1; q_c1=a47cb58a45fc4185a896490e8ffbd9a1|1563335283000|1546479842000; n_c=1; client_id="MzE2OTQ2Njc0Mw==|1563335331|411d415eb2357dc431e6aa3d30f5a4e8946c467a"; accountcallback=%7B%22status%22%3A%22new%22%2C%22fullname%22%3A%22MillerOS%22%2C%22type%22%3A%22sina%22%7D; tst=h; tshl=; l_cap_id="N2U0ZTNhMjQ3MzIzNGJjNjk1YWQ3OGI1YmRjMThkZWI=|1563516259|3bb9087cb4582ced6da3bf6c94c8b1c4f326e234"; r_cap_id="YzMxYzNiYTg4NzY4NDFlODk1MGY4NTExYmVmNWVlYjE=|1563516259|704737df69afd1df6913062aeb7fdf7155a387ab"; cap_id="MDYzZWIyNzI2ZGIxNDgzZjgzZTcyYTZiNmIyMDg0ZWQ=|1563516259|522c5e94cde2b2e6e4bdbdb5516d26d8e938c89b"; capsion_ticket="2|1:0|10:1563516306|14:capsion_ticket|44:ZjExOTE5NWYyYmQyNDZkMTg2YWIxZjBlOGFhYTJhZmY=|1969f6465d2e739d0d865b28577abcee45b3c54964bec458122582b7a837f8f0"; z_c0="2|1:0|10:1563516317|4:z_c0|92:Mi4xdkUwSUNBQUFBQUFBb09kcUR4ZHlEaVlBQUFCZ0FsVk5uYXNlWGdEYmg5bjJjeGNpZmFMandXVmxHdm9CSW5HN1lR|1b4b01dbfd08aba411b467447ca18ed7334e0a1d2a5e5fb7cd0e1032fa7cc473"; tgw_l7_route=060f637cd101836814f6c53316f73463'; let _res = await axios.get('https://www.zhihu.com/hot', { headers: _headers }).then(async (res) => { const $ = cheerio.load(res.data); //格式化数据 //取基本数据 const _list = $('html').find('.HotList-list .HotItem'); let _dataTmp = []; //下发到客户端的数据 await _list.each((_index, _el) => { const _item = $(_el); let _itemTmp = {}; //存储类目值 let _title = _item.find('.HotItem-title').text().replace(/[\r\n]/g, ''); //标题 let _Hot = _item.find('.HotItem-metrics').text().replace(/[\r\n]/g, ''); //热度 _itemTmp = { title: _title, hot: _Hot }; _dataTmp[_index] = _itemTmp; //下发到客户端的数据 }); return _dataTmp }); ctx.body = JSON.stringify(_res); }); console.log('站点建立在http://localhost:3000上'); app.listen(3000); ``` 保存并运行,查看结果 ![运行结果](http://qiniu.milleros.com/19-8-1/Snipaste_2019-08-01_11-49-29.png '运行结果') 可以看到,在这里我们已经获取到了知乎热榜内的数据,那么拥有这些数据,我们就可以放在自己的网站上,轻松的查看这些内容,亦或者是发送到自己的邮箱。 ## 4. 结语 关于爬虫的学问还有非常深入的,爬虫作为当今领域不可或缺的角色,不管是搜索引擎,还是资讯app,很多地方都又这爬虫的身影,作为这个时代的开发者,我们编写爬虫的时候要注意几个问题 ### 4.1 合法性 不爬取存储触犯法律网站的内容,且如果是需要另外展示出来,最好注明一下内容的来源,某些明确不让爬取的网页就不要爬取了。敏感信息自己看看就行,不要呈现给他人。 ### 4.2 资源 爬取内容时应该注意频率,不能高频检索目标网站,这样不仅会造成目标网站的资源被大大的浪费,而且我们的爬虫也极其容易被目标网站限制 ### 4.3 优质性 如果我们的爬虫爬取的数据是为了提供给我们自己的用户使用,则应该保证爬取内容的优质性,低俗内容就过滤掉为好,要不然咳咳,不就成了**百毒**那样了吗? ## 5. 附件 完整的项目代码可以参考这里 https://github.com/MillerO/NodejsFetchZhihu.com
继续阅读

密钥权限错误,Permissions too open

MILLEROS 发布于 2019-04-25

## 权限错误 今天在刚组装好的新电脑上用U盘里的密钥登录服务器,见鬼的是遇到了权限错误的问题,并且一直解决不了。一直像如下一样提示我密钥权限错误,无法读取: ```bash @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @ WARNING: UNPROTECTED PRIVATE KEY FILE! @ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ Permissions for 'test.pem' are too open. It is required that your private key files are NOT accessible by others. This private key will be ignored. Load key "test.pem": bad permissions root@youdomain.com: Permission denied (publickey,gssapi-keyex,gssapi-with-mic). ``` 后面谷歌了一堆的解决办法依然是没有办法。后面发现是在U盘里面的密钥文件夹一直是**只读状态**,后面单独把密钥拷贝到桌面就解决了这个问题。 为了方便大家在以后遇到这个问题好解决,我在这里详细说一下具体的方案。 ## 解决方法 解决方法这里要分为两种,一种是密钥文件的所属文件夹的权限问题,另一种是密钥文件的权限问题,这里我为大家做一下区分,方便快速定位原因。 ### 文件的权限问题 密钥文件的权限问题一般是会提示具体的权限过大的: ```bash @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @ WARNING: UNPROTECTED PRIVATE KEY FILE! @ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ Permissions 0644 for 'test.pem' are too open. It is required that your private key files are NOT accessible by others. This private key will be ignored. bad permissions: ignore key: test.pem Permission denied (publickey,password). ``` 例如此处提示了**权限0644过大**的问题 ```bash Permissions 0644 for 'test.pem' are too open ``` 遇到此类问题我们一般的做法是降低权限,例如此处权限为0644,我们则将此文件权限设置为0600: ```bash chmod 600 ./test.pem ``` 如果依然提示权限过大则降为500/400即可解决。 ### 文件夹权限问题 我此次遇到的就是文件夹权限问题,而且见鬼的是那个在U盘里的文件夹我右键更改掉只读权限关闭属性窗口后依然会回到只读状态,索性我就把它拷贝出来了。 遇到密钥文件夹权限问题的时候,一般不会提示具体的权限值,而是直接说你这个密钥文件权限太大了,读取不到,被忽略了: ```bash @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @ WARNING: UNPROTECTED PRIVATE KEY FILE! @ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ Permissions for 'test.pem' are too open. It is required that your private key files are NOT accessible by others. This private key will be ignored. Load key "test.pem": bad permissions root@youdomain.com: Permission denied (publickey,gssapi-keyex,gssapi-with-mic). ``` 这种时候则只需要把密钥文件拷贝到另外一个可以访问到的目录即可,如果此时依然不能读取到,且提示了具体的权限值,则回到本文的文件权限问题一栏进行解决。 ## 结语 遇到文件不能读取问题的时候,不管是密钥文件也好还是其他文件也罢,首先我们应该检查文件权限,然后是是否是只读状态,然后确定不是文件问题再检查此文件所在的文件夹,包括所有父级文件夹的权限是否都是合理的,以此来定位具体的问题在哪里。
继续阅读

使用docker-compose部署WordPress

MILLEROS 发布于 2019-04-24

## 1. 前言 Docker这个词接触过很久了,但是与之对应的Docker程序却一直未曾瞻仰过,这次因为需要对公司内现有的协作工具,例如git等程序统一迁移到新的服务器上,但是因为之前一直是直接在环境内安装程序,导致现在迁移起来十分麻烦,需要考虑各种兼容性的问题。 除此之外还考虑到现有的后端程序员小伙伴会使用不同版本的开发环境,导致合并代码时会有很多版本的兼容性问题,故此考虑使用docker-compose来编写docker-compose.yml文件统一开发环境,同时也可以使用统一开发环境部署更多不同语言的开发环境而不受干扰。 ## 2. 准备工作 ### 2.1 安装docker 关于docker的安装这里就不再赘述,如果对docker的安装还不太了解可参考[安装 Docker | Docker中文文档](https://docs.docker-cn.com/engine/installation/) ### 2.2 关于docker-compose 此处我们使用的是docker-compose安装,这里说一下为什么要使用docker-compose安装环境。 在基础的docker中,我们也可以进行docker内程序的安装与卸载等功能,但是我们操作的时候得一个一个命令单个单个的程序进行安装,很麻烦,而且如果遇到需要迁移某些服务时,我们得提前下线相关服务,此时如果我们的docker进程的实例命名不合理的话,很容易因为时间过长都分不清哪些实例才是我们需要下架的,这时docker-compose就派上用场了。 使用过Nodejs的同学应该知道,在应用程序运行之前,我们先要安装应用程序的依赖,而此时我们只需要执行npm install即可,是因为在当前目录下存在一个叫做package.json的文件,此文件记录了所有此项目需要的依赖以及版本,npm install的本质就是告诉npm去这个文件内找需要的依赖,然后下载下来。而这里的docker-compose就类似于npm的安装效用,npm需要package.json,docker-compose就需要docker-compose.yml。当然两者是不可以对比的,npm属于包管理器。 这里提到的docker-compose.yml使用的是yaml语言,关于这个语言的格式不了解请查看[基本语法](https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html)。 ### 2.3 安装docker-compose 这里我是用的是CentOS,所以我使用的是yum进行包管理,如果你和我一样使用yum进行包管理,可直接使用下面的命令进行安装: ```bash yum install docker-compose ``` 安装完成后可直接使用docker-compose命令查看是否安装成功。 ### 2.4 编写docker-compose.yml 为了防止以后因为项目过多而导致yml配置文件难以区分,我们应该为每一个docker项目分配单独的目录 ```bash $ mkdir wordpress $ cd wordpress $ touch docker-compose.yml vim docker-compose.yml ``` 我们需要在打开的文档内输入以下内容: ```yaml version: '3.3' services: db: image: mysql:5.7 volumes: - db_data:/var/lib/mysql restart: always environment: MYSQL_ROOT_PASSWORD: somewordpress MYSQL_DATABASE: wordpress MYSQL_USER: wordpress MYSQL_PASSWORD: wordpress wordpress: depends_on: - db image: wordpress:latest ports: - "8000:80" restart: always environment: WORDPRESS_DB_HOST: db:3306 WORDPRESS_DB_USER: wordpress WORDPRESS_DB_PASSWORD: wordpress WORDPRESS_DB_NAME: wordpress volumes: db_data: {} ``` 关于此处的各配置意思可参考[Docker Compose 配置文件详解](https://www.jianshu.com/p/c02e2127b9d7)。 我们此处需要修改的是environment内的参数 - MYSQL_ROOT_PASSWORD:MYSQL内Root用户的密码 - MYSQL_DATABASE:此时需要为wordpress提供的数据库名称 - MYSQL_USER:提供给wordpress使用的用户 - MYSQL_PASSWORD:提供给wordpress使用的用户的密码 - WORDPRESS_DB_HOST:提供给wordpress使用的数据库的服务地址 - WORDPRESS_DB_USER:在MYSQL内设置的提供给wordpress使用的数据库用户 - WORDPRESS_DB_PASSWORD:在MYSQL内设置的提供给wordpress使用的数据库用户的用户密码 - WORDPRESS_DB_NAME:在MYSQL内设置的提供给wordpress使用的数据库名称 此处我们也可以对ports进行修改,8000:80的意思就是将wordpress需要使用的80端口映射到服务器主机的8000端口上。 ## 3. 安装 说了这么多终于到了安装步骤,其实前面编辑好了docker-compose.yml文件之后,一切事情都简单了,我们只需要在docker-compose.yml所在的目录执行以下命令即可进行安装。 ```bash $ docker-compose up -d Creating network "wordpress_default" with the default driver Creating wordpress_db_1 ... done Creating wordpress_db_1 ... Creating wordpress_wordpress_1 ... done ``` 出现以上情况则说明服务启动成功,我们可以使用下面的命令进行服务的管理,当然是要在当前docker-compose.yml文件所在的目录内运行。 ```bash $ docker-compose start //启动服务,前提是您docker-compose.yml内的服务是已经存在于docker内了 $ docker-compose stop //停止服务,前提是您docker-compose.yml内的服务是已经存在于docker内了 $ docker-compose down //停止并删除服务组,但是服务组的数据还会被保存,下次启用依然可以使用旧的数据 $ docker-compose down --volumes //停止并删除服务组,数据也会一并删除,不可恢复 ``` 前面提到需要确认服务组是否存在与docker的进程内,我们可以使用下面的命令来查看: ```bash docker ps -a ``` 同样的我们也可使用 ```bash docker stop/start [实例名/ID] ``` 来停止或开启服务。 ## 4. 结语 关于Docker的更多功能需要我们的不断探索,我也是处于新人阶段,很多东西并不是特别理解,也许会有地方出错,欢迎指正。
继续阅读

JavaScript操作符

MILLEROS 发布于 2019-04-01

## 1. 一元操作符 只能操作一个值的操作符叫做一元操作符。 ### 1.1 递增和递减操作符 递增和递减操作符一般是连用两个一元操作符来完成操作。递增和递减操作符又分为**前置递增/递减**或**后置递增/递减**。 #### 1.1.1 前置递增/递减 ```javascript var age = 22; console.log(--age); //21 ``` #### 1.1.2 后置递增/递减 ```javascript var age = 22; console.log(age--); //21 ``` #### 1.1.3 在运算中运用递增或递减运算符 在实际的等式运算中对**变量**可以进行任意的一元运算,但是对于实际数值使用运算则可能会出错。 ```javascript var num1 = 4; console.log(num1-- + 1); //5 console.log(num1); // 3 console.log(--num1 + 1); //3 console.log(--4 + 1); // Uncaught ReferenceError: Invalid left-hand side expression in prefix operation console.log(4-- + 1); // Uncaught ReferenceError: Invalid left-hand side expression in prefix operation console.log(1 + 4--); // Uncaught ReferenceError: Invalid left-hand side expression in prefix operation ``` 这里有两处地方值得我们注意,第一处是在代码的第二行和第四行处,为何前置和后置递减操作分别运算加一操作会得出两个不同的结果。 首先第二行后置递减,此处的运算使用的num1值是num1执行后置递减前的值。 然后第四行前置递减,此处的运算使用的num1值是num1执行第二行后置递减接着再执行第四行前置递减后的值。 第二处值得我们注意的是直接对数值进行递增或递减操作是会报错的。 ### 1.2 运用一元操作符进行数值转换 我们除了可以在等式运算中使用一元运算符之外,还可以在JavaScript中对几乎任何值进行一元操作,不过在对**非数值**应用一元操作符时,该操作符会像Number()转型函数一样对这个值执行转换。 换句话说,布尔值false和true将被转换为0和1,字符串值会被按照一组特殊的规则进行解析,而对象则是先调用他们的valueOf()和toString()方法,再转换得到的值。 ```javascript var s1 = "01"; var s2 = "1.1"; var s3 = "z"; var b = false; var f = 1.1; var o = { valueOf:function(){ return -1; } }; s1 = + s1; //数值1 s2 = + s2; //数值1.1 s3 = +s3 //NaN b = +b; //数值0 f = +f; //数值1.1 o = +o; //数值-1 ``` ## 2. 位操作符 位操作符作用在最基本的二进制上,也就是**内存**中,此处的位指的就是内存中二进制各数值的位置得来的。ECMAScript中的所有数值都是以**[IEEE-754 64](http:/https://en.wikipedia.org/wiki/Double-precision_floating-point_format/ "IEEE-754 64")**位格式存储,但操作符并不直接操作64位的值,而是先将64位转换成32位的整数,然后再执行操作。 对于有符号的整数,32位中的前31位用于表示整数的值,第32位用于表示数值的符号:0表示正数,1表示负数。这个标识符好的位叫做**符号位**,符号位的值军订了其他位数值的格式。 正数以纯二进制格式存储,31位中的每一位都表示2的幂。第一位(位0)表示2º,以此类推。32位中表示数值时,没有用到的位使用0填充。例如 18的二进制表示:**00000000000000000000000000010010**,或者简洁表示为**10010**。拥有五个有效位五位本身军订了实际的值。 | 1 | 0 | 0 | 1 | 0 | | ------------ | ------------ | ------------ | ------------ | ------------ | | 2^4x1 | 2³x0 | 2²x0 | 2¹x0 | 2ºx0 | | 16 | 0 | 0 | 2 | 0 | | | | | 合计 | 18 | 负数同样以二进制码存储,但使用的格式是**二进制补码**。计算一个数值的二进制补码,需要三个步骤: 1. 求这个数值绝对值的二进制码,例如-18,先求18的二进制码。 2. 求二进制反码,也就是将0替换为1,将1替换为0。 3. 得到的二进制反码加1。 根据三个步骤求-18的二进制码: 0000 0000 0000 0000 0000 0000 0001 0010 求二进制反码,0与1互换 1111 1111 1111 1111 1111 1111 1110 1101 二进制反码加1 1111 1111 1111 1111 1111 1111 1110 1110 此时便就得到了二进制表示的-18。 但是在我们在JavaScript中计算二进制的时候,负数的二进制并不是如实显示上述的计算结果,例如-18的二进制在JavaScript中显示的是: ```javascript var num = -18; console.log(num.toString(2)); // "-10010",也就是直接在18的二进制码前面加上了负号。 ```
继续阅读

Let’s Encrypt Certbot 报错

MILLEROS 发布于 2019-03-18

关于给网站添加https的必要性在这里就不再赘述了,合理的添加https可以防止很多安全问题,其中就包括恶心的运营商广告注入问题。 现在普遍流行的https提供商非**[Let’s Encrypt](https://letsencrypt.org/ "Let’s Encrypt")**莫属了,根据其提供的**Certbot**工具,我们可以轻松、优雅的为我们的域名创建Nginx/Apache证书;工具为我们生成的证书有效期限均为**90天**,所以我们每过90天便需要对我们的证书进行更新。在更新证书的过程中,我们经常会遇到各种各样的问题。 以下是我个人遇到的一些问题汇总,长期更新。 ## 更新域名证书 1. Client with the currently selected authenticator does not support any combination of challenges that will satisfy the CA. 出现这种错误一般情况下是所使用的**客户端未更新**或者**Nginx/Apache服务未及时停止**所致应对措施如下: **更新Certbot**: ```bash yum upgrade certbot //使用yum安装的certbot时 apt-get upgrade certbot //使用apt-get安装的certbot时 ... //使用手动安装方法时 wget https://dl.eff.org/certbot-auto //下载wget工具 chmod a+x ./certbot-auto //修改权限 ./certbot-auto --help //测试打开certbot帮助 ``` **Nginx/Apache服务未及时停止** ```bash //Apache sudo certbot --authenticator standalone --installer apache -d example.com -d www.example.com --pre-hook "systemctl stop apache2" --post-hook "systemctl start apache2" //Nginx sudo certbot --authenticator standalone --installer nginx -d example.com -d www.example.com --pre-hook "systemctl stop nginx" --post-hook "systemctl start nginx" ``` ## 关于证书的维护问题 服务器如果缺少日常维护,经常会多出很多无用的问题,例如当前的certbot续期出现问题时,我们执行某些命令更新证书时常常会出现同一域名出现了多个证书,实际有用的只有一个,此时我们就需要删除无用的证书以及对应的Nginx/Apache配置文件。 ### 从证书中删除域名 删除证书的时候,需要删除archive中的文件和live中的符号链接,同时还需要删除证书更新的配置文件: ```bash rm -rf /etc/letsencrypt/live/www.example.com/ rm -rf /etc/letsencrypt/archive/www.example.com/ rm -rf /etc/letsencrypt/renewal/www.example.com.conf ``` <font color=red>这里要谨慎使用**rm -rf**命令,因为在输入文件路径时我们可能误按回车键,如果**使用了-rf指令**啧我们会在**不收到任何提示的情况下删除某个文件**</font> 删除了旧的证书,我们就不用在每次自动更新证书时选择需要更新的证书,也会减少多种因为Nginx/Apache配置文件与证书文件不对应而产生的问题。
继续阅读

整理的一些前端安全问题

MILLEROS 发布于 2019-02-25

# Web security 1.0 主要整理了前端的一些常见的安全问题。 ## JS逻辑代码 ### 1. XSS攻击 主要表现为注入代码,在客户端渲染数据时,**应该无条件相信用户的输入是不可信的**,也就是说当渲染用户输入的数据时,应当提前剔除可执行代码或者直接进行转义。 XSS攻击通常是下面的攻击形式: - 使用vue等框架渲染时 ```html <div class="content" v-html="data.content"></div> <!-- 🈲直接渲染用户的输入为html --> <div class="content">{{data.content}}</div> <!-- ✅使用带有转义作用的渲染方式 --> <script> data(){ return { content:"<script>alert('您被XSS攻击了')</script>" } } </script> ``` - 使用JS、JQ渲染数据时 ```html <div class="content"></div> <script> var _incomeMsg = '<script>alert('您被XSS攻击了')</script>'; document.getElementByClass('content')[0].innerHtml = _incomeMsg; // 🈲直接渲染用户的输入为html document.getElementByClass('content')[0].innerText = _incomeMsg; // ✅使用带有转义作用的渲染方式 $('.content').append(_incomeMsg);// 🈲直接渲染用户的输入为html $('.content').after(_incomeMsg);// 🈲直接渲染用户的输入为html $('.content').before(_incomeMsg);// 🈲直接渲染用户的输入为html $('.content').text(_incomeMsg);// ✅使用带有转义作用的渲染方式 </script> <!-- 执行exec等函数 --> <script> function html2Escape(sHtml) { return sHtml.replace(/[<>&"]/g,function(_index){ return {'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[_index]; }); } var _url = "https://lingtaikj.com?username=<script>alert('您被XSS攻击了')</script>" var param = /=(.+)$/.exec(_url); var value = decodeURIComponent(param[1]); $('.content').append('<a class="appendLink">' + _url + '</a>');// 🈲直接渲染用户的输入为html $('.content').append('<a class="appendLink">' + html2Escape(_url) + '</a>');// ✅使用函数将html标签转换为实体编码 </script> ``` ### 2. CSRF攻击 CSFR跨站请求伪造攻击,主要利用不合理的POST或GET请求来达成某种目的。 #### 2.1 GET请求 某些时候,一些后端工程师为了方便,会将某些post请求写成GET请求,殊不知这样是违反HTTP标准的,同时也容易被黑客所利用: ```php <?php // 从cookie中获取用户名,看似稳妥 $username = $_COOKIE['username']; $productId = $_GET['pid']; // 这里进行购买操作 //store_into_database($username, $productId); ?> <meta charset="utf-8" /> <?php echo $username . '买入商品:' . $productId; ?> ``` **此时我们直接使用浏览器访问目标端口即可达成请求** #### 2.2 POST请求 虽然我们此时按照了HTTP标准来编写请求接口,但是此时还是可以破解的。 ```php <?php $username = $_COOKIE['username']; // 换为post了,可以规避黑客直接的提交 $productId = $_POST['pid']; // 这里进行购买操作 //store_into_database($username, $productId); ?> <meta charset="utf-8" /> <?php echo $username . '买入商品:' . $productId; ?> ``` 此时在html网页中可以伪造一个表单等待用户点击。 ```html <!DOCYTPE HTML> <html> <head> <meta charset="utf-8" /> <script src="https://ss0.bdstatic.com/5aV1bjqh_Q23odCf/static/superman/js/lib/jquery-1.10.2_d88366fd.js"></script> </head> <body> <button id="clickme">点我看相册</button> <script> $('#clickme').on('click', function () { // 用户再不知情的情况下,提交了表单,服务器这边也会以为是用户提交过来的。 $('#myform').submit(); }); </script> <form id="myform" style="display:none;" target="myformer" method="post" action="http://myhost:8082/lab/xsrflab/submit.php"> <input type="hidden" name="pid" value="1"> </form> <iframe name="myformer" style="display:none;"></iframe> </body> </html> ``` #### 2.3 如何防范两种请求潜在的CSRF攻击 除了要**遵守HTTP标准来编写接口**以外,我们可以在请求时给请求加上验证码,如果验证码填写不对,则请求无效,但是缺点是降低用户体验,用户每次请求都需要输入验证码,这是极其不友好的。 另外一种方法是在POST等敏感请求上加上二次验证,即在每个请求上加上页面token验证,当用户每次访问某个页面时,生成一个token下发给用户,当用户请求时验证token是否有效,这样当黑客在使用CSRF时则不会带有网站生成的请求Token,此连接操作自然是无效的。 ### 3. 网络劫持 #### 3.1 HTTP劫持 很多的时候,我们的网站不是直接就访问到我们的服务器上的,中间会经过很多层代理,如果在某一个环节,数据被中间代理层的劫持者所截获,他们就能获取到使用你网站的用户的密码等保密数据。比如,我们的用户经常会在各种饭馆里面,连一些奇奇怪怪的wifi,如果这个wifi是黑客所建立的热点wifi,那么黑客就可以结果该用户收发的所有数据。这里,建议站长们网站都**使用https进行加密**。这样,就算网站的数据能被拿到,黑客也无法解开。 如果你的网站还没有**进行https加密**的化,则在表单提交部分,最好进行**非对称加密**--即**客户端加密,只有服务端能解开**。这样中间的劫持者便无法获取加密内容的真实信息了。 #### 3.2 iframe劫持 此种表现为自己的网站被其他网站利用iframe嵌套了,如果我们的网站因为某些原因被第三方网站嵌套了,第三方网站可以将嵌套我们网站的iframe透明度设为0覆盖在当前网页之上,然后利用背景诱导用户点击,那么此时就有可能发生不可预料的风险。 **解决方法** ##### 3.2.1 防网页被嵌套 为了防止网站被钓鱼,可以使用window.top来防止你的网页被iframe. ```javascript //禁止任意形式的被嵌套 if(window != window.top){ window.top.location.href = correctURL; } //同域名的网页可嵌套 if (top.location.host != window.location.host) {   top.location.href = window.location.href; } ``` ##### 3.2.2 X-Frame-Options X-Frame-Options是一个响应头,主要是描述服务器的网页资源的iframe权限。目前的支持度是IE8+ ``` X-Frame-Options: DENY 拒绝任何iframe的嵌套请求 X-Frame-Options: SAMEORIGIN 只允许同源请求,例如网页为 foo.com/123.php,則 foo.com 底下的所有网页可以嵌入此网页,但是 foo.com 以外的网页不能嵌入 X-Frame-Options: ALLOW-FROM http://s3131212.com 只允许指定网页的iframe请求,不过兼容性较差Chrome不支持 ``` ##### 3.3.3 CSP页面防护 和X-Frames-Options一样,都需要在服务器端设置好相关的Header. CSP 的作用, 真的是太大了,他能够极大的防止你的页面被XSS攻击,而且可以制定js,css,img等相关资源的origin,防止被恶意注入。不过兼容性不好。 ``` Content-Security-Policy: default-src 'self' ``` ##### 3.2.2 sandbox 主要用于**防止自己的网页嵌套他人网站**时的安全问题 ``` <iframe sandbox src="..."> ... </iframe> ``` sandbox还忠实的实现了“Secure By Default”原则,也就是说,如果你只是添加上这个属性而保持属性值为空,那么浏览器将会对iframe实施史上最严厉的调控限制,基本上来讲就是除了允许显示静态资源以外,其他什么都做不了。比如不准提交表单、不准弹窗、不准执行脚本等等,连Origin都会被强制重新分配一个唯一的值,换句话讲就是iframe中的页面访问它自己的服务器都会被算作跨域请求。 另外,sandbox也提供了丰富的配置参数,我们可以进行较为细粒度的控制。一些典型的参数如下: ``` allow-forms:允许iframe中提交form表单 allow-popups:允许iframe中弹出新的窗口或者标签页(例如,window.open(),showModalDialog(),target=”_blank”等等) allow-scripts:允许iframe中执行JavaScript allow-same-origin:允许iframe中的网页开启同源策略 ``` ### 4. 控制台注入代码 通常此种攻击方式较为罕见,主要表现为黑客利用不懂代码的小白,诱骗他们在控制台粘贴执行某些代码,从而达到窃取用户信息的目的。 **解决方案** 在控制台显著标识警示信息。可参考[天猫官网](https://www.tmall.com),打开控制台查看**安全警告** ### 5. 钓鱼网站 一般钓鱼网站会伪造成和正常网站,此时提示用户输入账号密码,提交之后用户对方便获得了用户的账号密码等信息。 一般是在原网站上发布钓鱼网站的外链,诱导用户点击。 **解决办法** 在网站内所有不可信的外链均执行页面unload提示,或者将所有的不可信外链改成拦截后需要用户确认为新窗口打开。 ## Cookie安全 Cookie主要是用来维护用户的登录状态的,同时有可能会存储其他敏感信息;当我们网页中的Cookie被他人,例如黑客利用XSS攻击或者其他方式获取了您的Cookie,那么他将可以**利用您的Cookie以您的身份登录网站**并进行操作。 - 解决办法 要解决用户的Cookie可能被窃取的问题,通常的做法是后端工程师在处理登录注册操作时**将用户的cookie设置为HttpOnly** ```php setcookie("Session", "sessionKey", NULL, NULL, NULL, NULL, TRUE/*httpOnly*/); ```
继续阅读

简单的Python爬虫爬取网页信息

MILLEROS 发布于 2018-06-25

### 前言 从一开始使用Nodejs编写爬虫爬去网页信息开始,就对爬虫感兴趣了,以为可以做很多低级但是繁琐的事情,当然,这里的低级是一方面是因为以我现在的阶段不需要爬取太多深层次的东西,另一方面则是爬虫的编写水平还跟不上,当然肯定是后者才是重点hhhh。 ### 爬虫的原理 编写网页爬虫一般是对页面进行解析,现在百度一搜索爬虫,最多的就是爬取豆瓣电影等网站的爬虫代码。 ### 爬虫代码 ```python # encoding=utf-8 import requests import time from bs4 import BeautifulSoup _downLoadUrl = 'https://so.gushiwen.org/mingju/' _count = 0 _all = 200 def getMs(): millis = int(round(time.time() * 1000)) return millis def downLoadPage(url): headers = { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.80 Safari/537.36' } data = requests.get(url,headers=headers).content return data def getList(doc): soup = BeautifulSoup(doc,'html.parser') _list = soup.find('div',class_='sons') _pageCount = 0 for _item in _list.find_all('div',class_='cont'): _pageCount += 1 _detail = '[' + str(_pageCount) + '/' + str(_count) + ']' + _item.get_text() # print(_detail) file = open("gushiwen.txt","a") file.write(_detail) file.close() # global _all # _all = int(soup.find('label',id_='sumPage').get_text()) #获取所有页数 page = soup.find('a',attrs={"class":"amore", "href": True}) #获取下一页 if page: return _downLoadUrl + page['href'].replace('/mingju/','') return None def main(): url = _downLoadUrl _start = getMs() while url: global _count _count +=1 doc = downLoadPage(url) url = getList(doc) print('第' + str(_count) + '页已经爬取完成,还剩' + str(_all - _count) + '页待爬取!') if (url == None): _end = getMs() print('爬取完成,耗时' + str(_end - _start) + '毫秒!') if __name__ == '__main__': main() ```
继续阅读

Nuxt.js -- Vue.js 单页应用的服务端渲染

MILLEROS 发布于 2018-05-15

## 简介 Vuejs现在几乎成了单页应用的代名词,但作为单页应用,在搜索引擎的友好程度上显然是不行的,当网站爬虫爬取到vue应用时如果没有做过特殊的处理,爬虫通常是爬取不到页面的主要内容的,因为vue单页应用通常使用的都是前端渲染,而爬虫爬取通常是不执行JS的;所以在种种情况下Vue的服务端渲染显然是作为对SEO优化最好的选择,因此Nuxt.js应运而生。 ## Nuxt.js Nuxt.js 是一个基于 Vue.js 的通用应用框架,它预设了利用 Vue.js 开发 服务端渲染(SSR, Server Side Render) 的应用所需要的各种配置,同时也可以一键生成静态站点。 作为框架,Nuxt.js 为 客户端/服务端 这种典型的应用架构模式提供了许多有用的特性,例如异步数据加载、中间件支持、布局支持等。区别于其他 vue SSR 框架,Nuxt.js 有以下比较明显的特性。 - 自动代码分层 - 强大的路由功能,支持异步数据(路由无需额外配置) - HTML头部标签管理(依赖 vue-meta 实现) - 内置 webpack 配置,无需额外配置 ## 起步 ### 项目创建 使用基于vue-cli的脚手架工具 ```bash $ vue init nuxt-community/starter-template <project-name> ``` > 通过此命令行命令初始化一个nuxt项目。 ### 目录结构 ```bash ├─assets 资源目录,未编译的静态资源如less、js ├─components 组件目录,例如页面共用的头部,底部 ├─layouts 布局目录,例如所有页面都分成三部分,上中下 ├─middleware 用于存放应用的中间件,允许您定义一个自定义函数运行在一个页面或一组页面渲染之前。 ├─node_modules ├─pages 页面目录,放置页面除去组件后的主要部分 ├─index.vue ├─.... ├─plugins 插件 例如jq,swiper等插件的文件 ├─static 静态文件目录 ├─store vuex store ``` > vuex store : [vuex状态树](https://vuex.vuejs.org/zh-cn/ "vuex状态树") ### 配置项 ```javascript // nuxt.config.js 文件配置 const path = require('path') module.exports = { // Headers of the page head: { title: '默认共用title', //对应HTML<title /> meta: [ //对应HTML<meta> { charset: 'utf-8' }, { 'http-equiv': 'pragma', content: 'no-cache' }, { 'http-equiv': 'cache-control', content: 'no-cache' }, { 'http-equiv': 'expires', content: '0' }, { content: 'telephone=no', name: 'format-detection' } ], // html head 中创建 script 标签 script: [ { innerHTML: require('./assets/js/flexible_nuxt'), type: 'text/javascript', charset: 'utf-8'} ], // 不对<script>标签中内容做转义处理 __dangerouslyDisableSanitizers: ['script'] }, // Global CSS css: ['~/assets/css/reset.css', '~/assets/css/main.less'], // Global env env: { __ENV: process.env.__ENV }, build: { vendor: ['axios'], postcss: [ require('postcss-px2rem')({ remUnit: 75 }) ], extend (config, ctx) { if (ctx.isClient) { // 拓展 webpack 配置 config.entry['polyfill'] = ['babel-polyfill'] config.module.rules.push({ enforce: 'pre', test: /\.(js|vue)$/, loader: 'eslint-loader', exclude: /(node_modules)/ }) // 添加 alias 配置 Object.assign(config.resolve.alias, { 'utils': path.resolve(__dirname, 'utils') }) } } }, plugins: [{src: '~plugins/toast', ssr: false}, {src: '~plugins/dialog', ssr: false}] } ``` #### 头部标签配置 Nuxt.js 通过 vue-meta 实现头部标签管理,在 nuxt.config.js 中的 head 配置。所有的页面都会走这个配置,如果想要修改某一页面的title,可以在 pages/yourpage.vue 文件下,添加如下配置,这时该页面的标题就变成了“This is Miller”,其余页面还保持原有标题不变。 ```javascript <script> export default { head () { return { title:'This is Miller' } } } //do something </script> ``` > 在config header配置中, __dangerouslyDisableSanitizers: ['script'] 主要是为了不对**innerHTML**中内容做转义处理 ### 路由 Nuxt.js 依据 pages 目录结构,自动生成 vue-router 模块的路由配置。 详情可查看[Nuxt.js官网路由板块](https://zh.nuxtjs.org/guide/routing "Nuxt.js官网") ### 布局 Nuxt.js布局方式如下图所示: ![Nuxt.js布局方式](http://qiniu.milleros.com/18-5-15/59212371.jpg "Nuxt.js布局方式") 布局配置文件存储在layouts文件夹下,默认布局为default.vue: ```html <template> <div id="milleros"> <AppHeader /> <nuxt/> <AppFooter /> <AppLoading /> </div> </template> ``` 这里&lt;nuxt/&gt;是一个页面的主体,所有/pages/\*.vue中的内容都会插入到此标签中; &lt;AppHeader /&gt;、&lt;AppFooter /&gt;、&lt;AppLoading /&gt;为conponents组件,是所有页面共用的组件。 所以layouts布局就是配置不同组件和页面主体的结构。 我们可以根据不同的页面定制不同的layouts布局: ```html <script> export default { layout: 'demo_layout', //do something } </script> ``` ### vuex 在根目录创建 store 目录,就会默认引用 vuex 模块,除此之外,还进行了以下的操作: 1)将 vuex 模块 加到 vendors 构建配置中去; 2)设置 Vue 根实例的 store 配置项。 Nuxt.js 支持两种使用 store 的方式: - 普通方式:store/index.js 返回一个 Vuex.Store 实例 - 模块方式:store 目录下的每个 .js 文件会被转换成为状态树指定命名的子模块 (当然,index 是根模块,相当于设置了namespaced: true) Nuxt.js提供了模块方式的简单写法:使用状态树模块化的方式,store/index.js 不需要返回 Vuex.Store 实例,直接将 state、mutations 和 actions 暴露出来即可。示例如下: ```javascript export const state = () => ({ accesstoken: '' }) export const mutations = { setAccesstoken (state, accesstoken) { state.accesstoken = accesstoken } } ``` ### asyncData **asyncData方法是服务端渲染的重点!!!** 因为**asyncData**可以在组件(**仅限于页面组件**)每次加载之前被调用。而且**可以再服务端或路由调用之前被调用** Nuxt.js 会将 asyncData 返回的数据融合组件 data 方法返回的数据一并返回给当前组件。 ###### 详细查看[异步数据](https://zh.nuxtjs.org/guide/async-data "异步数据")
继续阅读

记一次服务器文件被恶意篡改的解决过程

MILLEROS 发布于 2018-05-02

## 前言 本人也是个linux小白,下面的内容是记录一次服务器找出源文件被篡改的原因的过程。 最近遇到公司服务器文件被篡改的情况,主要表现就是公司官网主页的SEO信息被替换成了广告,一开始以为只是普通的劫持,但是后来上到服务器发现是源文件直接被篡改了,然后找了一圈服务器登录记录以及命令执行记录都没发现有可以目标存在,后来又怀疑是设置了定时任务,然而也并没有发现。在没有明确发现可以目标的情况下,而且下午就放五一小长假了就没去深究,直接把文件改回来了。 刚放完假的第一天回来发现文件再次被篡改,篡改时间为5月1日下午两点十几分,这就让我有点头疼了,但是一圈检查下来还是没有发现任何记录,怀疑是被删掉了或者干脆就是自动某个自动脚本修改的,因为修改之后的源文件编码不对了,原先是utf-8,现在直接就是GBK了。 本来想查看文件修改记录的,但是发现服务器上并没有发现任何监控程序启动,所以只能将文件再次改回去,然后使用最基础的[auditd官网](http://people.redhat.com/sgrubb/audit/ "auditd官网")来监控整个被篡改过文件的目录。 ## auditd 使用[auditd监控文件](https://milleros.com/details/5ae9803435cd6b1d5749b02e "auditd监控文件")变化记录。 到了这里我们其实已经可以先设定一个设计规则了,然后等待上述说到的文件夹被篡改后查看审计报告后在做计划了。 ## 等待 等待所监控的文件再次被修改 ## 文件再次被篡改 服务器首页文件在修正后并且添加了监控的情况下再次被篡改,使用命令 ```bash $ sudo ausearch -f /var/www/html ``` 并未发现存在文件修改记录,猜测文件并非从服务器端直接被修改,且注入的文字为HTML实体编码,修改后的文件编码被改变。 ## 猜测为注入攻击 ### 查看网站访问记录 #### 发现可疑访问 查看apache日志发现每次文件被篡改都显示有http://www.link114.cn/ 的访问 #### whois反查 通过whois反查查到以下注册人信息 - 名称:**陈镇威** - 邮箱:**520vigo@163.com** - 域名列表:hackerjs.cn、link114.cn、link114.com.cn、netloss.cn、showdownload.org 其中link114.cn为买链帮手,判断是此人所为 #### 查到个人博客 通过google查到此邮箱所关联的网易博客,但最近的博客更新时间为2008年,无法找到更多有用信息 ### 返回梳理发生过程 通过对文件从修改正确到再次被篡改的过程中梳理得出了我从一开始就忽略的问题——文件夹权限问题 通过ls查看**/var/www/**文件夹权限为777,其下所有文件夹几乎均为权限777,这就会导致一个问题,那就是任何人都可以使用工具向我们的服务器内**HTTP PUT**文件,而且脚本为最高权限。 ### 调整文件、文件夹权限 将**/var/www/**下的所有文件/文件夹所属用户和组修改为apache,修改权限为755 后来测试发现项目上传文件夹无法上传,将权限修改为**drwxrwx---**后可以上传了。 ## 总结 通过这次的教训,深刻认识到了服务器文件夹权限的重要性,往严重了说,这次对方修改meta信息算是轻得了,严重了都可以造成整个服务器被格盘或者干脆被当成肉机还不知情。
继续阅读

使用auditd监控文件/文件夹修改记录

MILLEROS 发布于 2018-05-02

### 什么是auditd auditd(或 auditd 守护进程)是Linux审计系统中用户空间的一个组件,其负责将审计记录写入磁盘。说白了就是开启的一个守护进程根据指定的规则来监控系统的某些情况变化。 ### 安装audited 我当前使用的操作系统是Centos 6,发现audited默认安装了,要查看audited是否安装了只需输入以下代码 ```bash $ sudo auditctl -v ``` 显示了版本信息说明已安装 ```bash auditctl version 2.4.5 ``` 否则就使用apt-get或者yum、rpm等包管理工具安装 ```bash $ sudo yum install auditd ``` 安装完毕后默认以下的工具也会存在 - auditctl : 即时控制审计守护进程的行为的工具,比如如添加规则等等。 - /etc/audit/audit.rules : 记录审计规则的文件。 - aureport : 查看和生成审计报告的工具。 - ausearch : 查找审计事件的工具 - auditspd : 转发事件通知给其他应用程序,而不是写入到审计日志文件中。 - autrace : 一个用于跟踪进程的命令。 - /etc/audit/auditd.conf : auditd工具的配置文件。 安装完后记得查看auditd**是否启动**了 ```bash $ sudo service auditd status ``` 否则使用以下命令**启动** ```bash $ sudo service auditd start ``` 使用以下命令**停止** ```bash $ sudo service auditd stop ``` 首次安装 auditd 后, 审计规则是空的: ```bash $ sudo auditctl -l No rules ``` ### 如何使用auditd #### Audit 文件和目录访问审计 我们使用审计工具的一个基本的需求是监控文件和目录的更改。使用auditd工具,我们可通过如下命令来配置(注意,以下命令需要root权限)。 ##### 文件审计 ```bash $ sudo auditctl -w /var/www/html/index.html -p rwxa ``` 选项 : - **-w path :** 指定要监控的路径,上面的命令指定了监控的文件路径 /etc/passwd,必须是以‘/’开头的绝对路径 - **-p :** 指定触发审计的文件/目录的访问权限 - **rwxa :** 指定的触发条件,r 读取权限,w 写入权限,x 执行权限,a 属性(attr) ##### 目录审计 使用类似的命令来对目录进行审计,如下: ```bash $ sudo auditctl -w /var/www/html/ ``` 以上命令将监控对 **/var/www/html/** 目录 的所有访问。 现在,可以运行 auditctl -l 命令即可查看所有已配置的规则。 ```bash $ auditctl -l -w /var/www/html/index.html -p rwxa -w /var/www/html/ -p rwxa ``` #### 查看审计日志 添加规则后,我们可以查看 auditd 的日志。使用 **ausearch** 工具可以查看auditd日志。 我们已经添加规则监控 /var/www/html/ 文件。现在可以使用 ausearch 工具的以下命令来查看审计日志了。 ```bash $ sudo ausearch -f /var/www/html/ ---- time->Wed May 2 11:20:31 2018 type=PATH msg=audit(1525231231.453:528): item=1 name="/var/www/html/.index.html.swp" inode=929483 dev=fc:01 mode=0100644 ouid=0 ogid=0 rdev=00:00 nametype=DELETE type=PATH msg=audit(1525231231.453:528): item=0 name="/var/www/html/" inode=921826 dev=fc:01 mode=040777 ouid=0 ogid=0 rdev=00:00 nametype=PARENT type=CWD msg=audit(1525231231.453:528): cwd="/var/www/html/" type=SYSCALL msg=audit(1525231231.453:528): arch=c000003e syscall=87 success=yes exit=0 a0=ea54a0 a1=1 a2=1 a3=0 items=2 ppid=15009 pid=15413 auid=0 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=pts4 ses=54129 comm="vim" exe="/usr/bin/vim" key=(null) ``` 配置项: - **-f 设定ausearch 调出 /var/www/html** 解读输出结果: - **time** : 审计时间 - **name** : 审计对象 - **cwd** : 当前路径 - **syscall** : 相关的系统调用 - **auid** : 审计用户ID - **uid 和 gid** : 访问文件的用户ID和用户组ID - **comm** : 用户访问文件的命令 - **exe** : 上面命令的可执行文件路径 以上审计日志显示文件曾被vim(/usr/bin/vim)改动过。 #### 查看审计报告 一旦定义审计规则后,它会自动运行。过一段时间后,我们可以看看auditd是如何帮我们跟踪审计的。 Auditd提供了另一个工具叫 aureport 。从名字上可以猜到, aureport 是使用系统审计日志生成简要报告的工具。 我们已经配置auditd去跟踪/var/www/html。auditd参数设置后一段时间后,audit.log 文件就创建出来了。 生成审计报告,我们可以使用aureport工具。不带参数运行的话,可以生成审计活动的概述。 ```bash $ sudo aureport Summary Report ====================== Range of time in logs: 05/02/2018 11:18:36.200 - 05/02/2018 12:31:43.069 Selected time for report: 05/02/2018 11:18:36 - 05/02/2018 12:31:43.069 Number of changes in configuration: 4 Number of changes to accounts, groups, or roles: 0 Number of logins: 2 Number of failed logins: 1 Number of authentications: 4 Number of failed authentications: 6 Number of users: 1 Number of terminals: 8 Number of host names: 3 Number of executables: 4 Number of commands: 2 Number of files: 123 Number of AVC's: 0 Number of MAC events: 0 Number of failed syscalls: 790 Number of anomaly events: 0 Number of responses to anomaly events: 0 Number of crypto events: 33 Number of integrity events: 0 Number of virt events: 0 Number of keys: 0 Number of process IDs: 176 Number of events: 1878 ``` 如上,报告包含了大多数重要区域的信息。 上图可以看出有 1 次授权失败。 使用aureport,我们可以深入查看这些信息。 使用以下命令查看授权失败的详细信息: ```bash $ sudo aureport -au Authentication Report ============================================ # date time acct host term exe success event ============================================ 1. 05/02/2018 11:42:11 root 14.*.79.* ssh /usr/sbin/sshd no 1161 2. 05/02/2018 11:42:17 root 14.*.79.* ssh /usr/sbin/sshd no 1162 3. 05/02/2018 11:42:17 root 14.*.79.* ssh /usr/sbin/sshd no 1163 4. 05/02/2018 11:42:22 root 14.*.79.* ssh /usr/sbin/sshd yes 1164 5. 05/02/2018 11:42:22 root 14.*.79.* ssh /usr/sbin/sshd yes 1167 6. 05/02/2018 12:31:30 root 14.*.79.* ssh /usr/sbin/sshd no 1866 7. 05/02/2018 12:31:38 root 14.*.79.* ssh /usr/sbin/sshd no 1867 8. 05/02/2018 12:31:38 root 14.*.79.* ssh /usr/sbin/sshd no 1868 9. 05/02/2018 12:31:42 root 14.*.79.* ssh /usr/sbin/sshd yes 1869 10. 05/02/2018 12:31:42 root 14.*.79.* ssh /usr/sbin/sshd yes 1872 ``` 从上图可以看出,由一个用户在特定的时间(从上一次登录成功的时间开始计算)授权失败。 如果我们想看所有账户修改相关的事件,可以使用**-m**参数。 ```bash $ sudo aureport -m Account Modifications Report ================================================= # date time auid addr term exe acct success event ================================================= <no events of interest were found> ``` 我这里因为刚开启auditd服务所以并未有记录存在。 #### Auditd 配置文件 我们已经添加如下规则: - $ sudo auditctl -w /var/www/html/index.html -p rwxa - $ sudo auditctl -w /var/www/html/ 现在,如果确信这些规则可以正常工作,我们可以将其添加到 **/etc/audit/audit.rules** 中使得规则永久有效。以下介绍如何将他们添加到/etc/audit/audit.rules中去。 ```bash $ sudo vim /etc/audit/audit.rules # This file contains the auditctl rules that are loaded # whenever the audit daemon is started via the initscripts. # The rules are simply the parameters that would be passed # to auditctl. # First rule - delete all -D # Increase the buffers to survive stress events. # Make this bigger for busy systems -b 320 # Feel free to add below this line. See auditctl man page -w /var/www/html/index.html -p rwxa -w /var/www/html/ ``` 编辑完后保存,然后重启auditd守护进程 ```bash $ sudo service auditd restart ``` ------------ 参考文档: - [Auditd - Linux 服务器安全审计工具](https://www.linuxidc.com/Linux/2015-02/113665.htm "Auditd - Linux 服务器安全审计工具")
继续阅读

linux NAT端口转发实现nodejs网站https访问

MILLEROS 发布于 2018-04-30

### 什么是certbot? > 在开始这个教程前我们必须要了解的就是什么是certbot。 > certbot是Let's Encrypt为了用户方便生成https证书而开发的自动化程序,通过certbot我们可以生成四个文件分别用于为https加密所用。 ### 安装certbot ###### 首先最保险的方法是你需要上[Certbot的官网](htthttps://certbot.eff.org/p:// "Certbot的官网")来选择您的服务器对应的操作系统版本 ![cerbot](http://qiniu.milleros.com/18-4-30/79269480.jpg "cerbot") 我这里使用的语言是nodejs,操作系统是CentOS7,接着下面会出现教程教你如何安装certbot: ![certbot安装方法](http://qiniu.milleros.com/18-4-30/6033161.jpg "certbot安装方法") 这里官网提供的方法是使用yum来安装Certbot ```bash $ sudo yum install certbot ``` 但是在CentOS7上,使用yum安装的certbot很可能会出现所安装的certbot和pyOpenSSL版本不对的情况(什么都别说,我就遇到了,可是把我折腾苦了),那么什么是版本不对呢?出现以下错误就说明你的yum安装的pyOpenSSL不对: ```bash Traceback (most recent call last): File "/usr/bin/certbot", line 9, in <module> load_entry_point('certbot==0.13.0', 'console_scripts', 'certbot')() File "/usr/lib/python2.7/site-packages/pkg_resources.py", line 378, in load_entry_point return get_distribution(dist).load_entry_point(group, name) File "/usr/lib/python2.7/site-packages/pkg_resources.py", line 2566, in load_entry_point return ep.load() File "/usr/lib/python2.7/site-packages/pkg_resources.py", line 2260, in load entry = __import__(self.module_name, globals(),globals(), ['__name__']) File "/usr/lib/python2.7/site-packages/certbot/main.py", line 17, in <module> from certbot import client File "/usr/lib/python2.7/site-packages/certbot/client.py", line 10, in <module> from acme import client as acme_client File "/usr/lib/python2.7/site-packages/acme/client.py", line 31, in <module> requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3() # type: ignore File "/usr/lib/python2.7/site-packages/requests/packages/urllib3/contrib/pyopenssl.py", line 112, in inject_into_urllib3 _validate_dependencies_met() File "/usr/lib/python2.7/site-packages/requests/packages/urllib3/contrib/pyopenssl.py", line 147, in _v alidate_dependencies_met raise ImportError("'pyOpenSSL' module missing required functionality. "ImportError: 'pyOpenSSL' module missing required functionality. Try upgrading to v0.14 or newer. ``` ### 使用pip安装绕过yum安装的版本不对问题 错误提示使用v0.14或者更新版本,先看看pip中的版本: ```bash $ pip search certbot ``` 显示版本为0.14.0; #### 卸载yum安装的certbot和pyOpenSSL: ```bash $ yum remove certbot pyOpenSSL ``` #### 升级pip(如果不是最新版本,pip会提示你升级后再使用): ```bash $ pip install –upgrade pip ``` ###### pip安装certbot: pip install certbot。 碰到了Python.h或者pyconfig.h找不到的错误,于是安装Python的devel包: yum install -y python-devel;再次运行命令,提示找不到opensslv.h头文件,于是安装OpenSSL的devel包: yum install -y openssl-devel;再次运行pip install certbot命令,成功安装certbot;pip安装certbot: pip install certbot。 碰到了Python.h或者pyconfig.h找不到的错误,于是安装Python的devel包: yum install -y python-devel;再次运行命令,提示找不到opensslv.h头文件,于是安装OpenSSL的devel包: yum install -y openssl-devel;再次运行pip install certbot命令,成功安装certbot; #### 测试certbot是否可用: certbot certificates。输出正常,说明pip安装了最新版的certbot,并且能正确运行。 ### 生成https证书 前面安装完certbot之后就到了最重要的地方了,就是生成https证书。 首先运行下列命令 ```bash $ certbot certonly ``` 会出现两种选择,我们选择第二种 ```bash How would you like to authenticate with the ACME CA? ------------------------------------------------------------------------------- 1: Spin up a temporary webserver (standalone) 2: Place files in webroot directory (webroot) ------------------------------------------------------------------------------- Select the appropriate number [1-2] then [enter] (press 'c' to cancel): ``` 接着这里我们需要输入自己的域名:www.mydomain.com (注意这里有一个空格) mydomain.com,我们这里绑定的是两个域名,一个是带www的,一个是没有www的,如果你只需要其中一个的话这里可以只写一个 ```bash Please enter in your domain name(s) (comma and/or space separated) (Enter 'c'to cancel): ``` 这里要求我们输入网站的根目录 ```bash Select the webroot for milleros.com: ------------------------------------------------------------------------------- 1: Enter a new webroot ------------------------------------------------------------------------------- Press 1 [enter] to confirm the selection (press 'c' to cancel): ``` 如果前面输入域名的时候您输入的是两个则这里会再次提示输入网站根目录: ```bash Select the webroot for yourdomain: ------------------------------------------------------------------------------- 1: Enter a new webroot 2: /root/www/static(你前面输入过的网站根目录) ------------------------------------------------------------------------------- ``` 如果根目录一致的话我们就选择2. 输入完根目录之后certbot会自动为我们生成证书,生成成功会有以下提示: ```bash Waiting for verification... Cleaning up challenges Generating key (2048 bits): /etc/letsencrypt/keys/0000_key-certbot.pem Creating CSR: /etc/letsencrypt/csr/0000_csr-certbot.pem IMPORTANT NOTES: - Congratulations! Your certificate and chain have been saved at /etc/letsencrypt/live/xxx.ml/fullchain.pem. Your cert will expire on 2017-06-07. To obtain a new or tweaked version of this certificate in the future, simply run certbot again. To non-interactively renew *all* of your certificates, run "certbot renew" - If you like Certbot, please consider supporting our work by: Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate Donating to EFF: https://eff.org/donate-le ``` ##### 注意:这里我遇到过一个很坑爹的问题,就是输入了网站根目录之后certbot提示我生成证书时认证失败: ```bash Failed authorization procedure. example.com (http-01): urn:acme:error:unauthorized :: The client lacks sufficient authorization :: Invalid response from http://example.com/.well-known/acme-ch <head><title>404 Not Found</title></head> <body bgcolor="white"> <center><h1>404 Not Found</h1></center> <hr><center>", www.example.com (http-01): urn:acme:error:unauthorized :: The client lacks sufficient authorization :: Invalid response from http://www.example.com/.well-known/acme-challenge/k <head><title>404 Not Found</title></head> <body bgcolor="white"> <center><h1>404 Not Found</h1></center> <hr><center>" IMPORTANT NOTES: - If you lose your account credentials, you can recover through e-mails sent to example@example.com. - The following errors were reported by the server: Domain: example.com Type: unauthorized Detail: Invalid response from http://example.com/.well-known/acme-challenge/wGNv57IGJjHQ9wyzzALktpNaPzfnTtN3m7u3QuO4p40: "<html> <head><title>404 Not Found</title></head> <body bgcolor="white"> <center><h1>404 Not Found</h1></center> <hr><center>" Domain: www.example.com Type: unauthorized Detail: Invalid response from http://www.example.com/.well-known/acme-challenge/kFJ0CSuKOdgcT2xmciB4GGNCcnUPoIbpQmA9jOII_Bk: "<html> <head><title>404 Not Found</title></head> <body bgcolor="white"> <center><h1>404 Not Found</h1></center> <hr><center>" To fix these errors, please make sure that your domain name was entered correctly and the DNS A record(s) for that domain contain(s) the right IP address. - Your account credentials have been saved in your Certbot configuration directory at /etc/letsencrypt. You should make a secure backup of this folder now. This configuration directory will also contain certificates and private keys obtained by Certbot so making regular backups of this folder is ideal. ``` 这里我研究了很久,网上很多人说可能是网站根目录下生成的文件夹.well-known 权限不够,但是我去根目录下看了,连那个文件夹都没看到,所以确定肯定不是权限问题,于是我在分析了一下错误信息里面包含404 not found的错误信息,可能是路由的问题,于是我在重复certbot生成https证书的过程中到了输入根目录的时候在结尾对加了一级/public(我用来存放客户端代码的文件夹),发现果然可以用了,顺利生成了https证书; ### 创建https服务器 经过刚刚的certbot操作,我们已经生成了如下四个文件: - cert.pem - chain.pem - fullchain.pem - privkey.pem 首先我们需要在原先的http服务器上做如下改动: ```javascript var express = require('express'); var fs = require('fs'); var http = require('http'); var https = require('https'); //引入刚刚生成的https文件 var privateKey = fs.readFileSync('/path/to/privkey.pem', 'utf-8'); var certificate = fs.readFileSync('/path/to/cert.pem', 'utf-8'); var ca = fs.readFileSync('/path/to/fullchain.pem', 'utf-8'); var credentials = {key: privateKey, cert: certificate ,ca:ca}; //实例化 expressvar app = express(); //创建http以及https服务 var httpServer = http.createServer(app); var httpsServer = https.createServer(credentials, app); //监听端口 httpServer.listen(8321); console.log('http server start on port 8321'); httpsServer.listen(8322); console.log('http server start on port 8322'); //默认的http协议端口是80端口,https端口是443端口。 ``` 大功告成 然后我们可以直接使用app来进行各种路由操作,现在就大功告成啦: ### 只有https访问 如果您不想要http访问,只想要https访问,只需要修改我们之前的两条规则: ```javascript //var httpServer = http.createServer(app); //之前的创建http服务器 var httpServer = http.createServer(function(req,res){ res.writeHead(301, {'Location': 'https://milleros.com/'}); res.end(); }); //将http请求重定向至https var httpsServer = https.createServer(credentials, app); ``` 上面这段代码的重定向存在一点问题,就是当我们访问的地址不是主页时,而且请求http时则会跳转回https的主页,这显然不是我们想要的结果,所以我们把原先的代码改成这样 ```javascript var express = require('express'); var fs = require('fs'); var http = require('http'); var https = require('https'); //引入刚刚生成的https文件 var privateKey = fs.readFileSync('/path/to/privkey.pem', 'utf-8'); var certificate = fs.readFileSync('/path/to/cert.pem', 'utf-8'); var ca = fs.readFileSync('/path/to/fullchain.pem', 'utf-8'); var credentials = {key: privateKey, cert: certificate ,ca:ca}; //实例化 expressvar app = express(); //创建http以及https服务 var httpServer = http.createServer(app); var httpsServer = https.createServer(credentials, app); //监听端口 httpServer.listen(8321); console.log('http server start on port 8321'); httpsServer.listen(8322); console.log('http server start on port 8322'); //默认的http协议端口是80端口,https端口是443端口。 //新增一个路由来拦截请求 app.use(function(req,res,next){ console.log(req.protocol); if(req.protocol != "https"){ res.writeHead(301, {'Location': 'https://milleros.com' + req.path}); res.end(); } next();//保证请求不被阻塞 }); ``` 这样就可以保证,网站中的每一个http请求都可以被重定向至https了。 ### 结语 linux是一个很复杂的操作系统(至少我这么认为),在我们平时进行的操作中经常会遇到各种各样的问题,有些问题在网络上我们可以很轻易的找到解决问题的方法,但是往往很多时候我们遇到的问题是很特殊的,网上几乎很难找到解决的办法,这个时候我们不应该着急,而应该静下心来仔细捋一捋思路,找找可能会导致问题出现的地方。 正是因为linux的复杂性,很多时候我们误操作会导致很多不可预料的问题,像这次我就是因为pyOpenSSL版本不对的问题在寻求很多方法未果之后选择了对系统自带的Python进行升级,正是因为我这一疯狂的想法从而让我误删除了site-packages,这一误操作直接导致的就是yum和pip无法使用,从而让我不得不重置了整个linux系统。 虽然对linux的误操作很容易造成不可估量的错误,但我还是鼓励大家大胆尝试,毕竟只有不停地跌倒才会有更多爬起来的经验,才能让我们更加的强大!
继续阅读

Ueditor + HighLightJs实现代码块高亮

MILLEROS 发布于 2018-04-30

百度Ueditor Ueditor是百度的一款富文本编辑器,目前在国内大多数网站都会使用到它,而今天我们就来看看,像我们这种三句不离代码的码农对于代码高亮来说是再基本不过的功能了,那么何为代码高亮呢? 普通代码显示 举例一段html代码 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Milleros的官方网站</title> </head> <body> <p>欢迎来到milleros的官方网站</p> </body> </html> 这是一段普通的未加任何修饰的html代码段,像这样的代码存在于网页上,我相信如果是我,我连看的勇气都没有。 高亮代码显示 还是上面那段代码,但是现在加上高亮功能 ```html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Milleros的官方网站</title> </head> <body> <p>欢迎来到milleros的官方网站</p> </body> </html> ``` 可以看到上面的代码的可读性变高了许多。 如何使用 使用方法很简单,当我们使用Ueditor编辑文章时可以在插入代码时点击Ueditor工具栏上的 snipaste_20170528_123657.png 来选择对应的语言,如果工具栏没有这个选项的话就说明您的编辑器没有默认配置这个功能,这个时候您需要对Ueditor的配置文件ueditor.config.js进行相应功能的配置。选择对应语言之后Ueditor编辑器内会出现专门用于放置代码块的pre标签 snipaste_20170528_124333.png 接着需要在我们需要渲染代码文章的页面引入两个文件 样式文件: ```html <link rel="stylesheet" type="text/css" href="/ueditor/third-party/SyntaxHighlighter/shCoreDefault.css"> ``` 脚本文件: ```html <script type="text/javascript" src="/ueditor/third-party/SyntaxHighlighter/shCore.js"></script> ``` 激活高亮: ```javascript 当上述文件均被引入之后,需要在页面js内写入下面的代码来执行代码高亮 SyntaxHighlighter.all(); 提示:代码高亮的执行需要在DOM加载完之后再执行,也就是需要执行代码高亮的代码已经被渲染到页面上之后再执行代码高亮。 ``` 代码高亮主题 官方主题分类 对于这种视觉上优化的功能,没有主题切换肯定是不“人道”的,百度Ueditor使用的代码高亮工具是第三方的SyntaxHighlighter高亮工具,从它的官网可以看到官方提供了自己的主题 snipaste_20170528_124928.png 官方主题使用 官方对于主题的应用,显然也考虑到了简单易用的原则;对于上述的主题样式我们可以使用 ```html <link type="text/css" rel="Stylesheet" href="PATH/Styles/shThemeDjango.css"/> ``` 来更换对应的主题文件。 主题文件 对于我们使用的Ueditor,因为是使用第三方库的原因,可能会存在主题文件不完整的问题存在,像楼主使用的是nodejs版的Ueditor,而内置的主题文件只有默认样式,所以我们需要把缺失的主题文件放到对应的文件夹内。具体的位置需要大家自己去看下 Ueditor的目录结构。
继续阅读

有些人25岁就死了,75岁才埋下

MILLEROS 发布于 2018-04-30

在很小很小的时候,我并不懂什么是理想,什么是梦想。 - “你的理想是什么” - “我长大后想当警察,为民除害“ 相信这个情景在我们是孩童时代经常会遇到的,那时候的我们总是带着小小的天真坚定说出自己可能并不了解的理想。 初中,依稀记得每次写到有关理想的作文,我总是写着自己要当警察的理想,但是随着我每次对梦想的探讨,都会让我加深对自己理想的动摇,因为我发现,每一次写一篇文章下来,总是绞劲脑汁都无法深入到这个理想的精髓。 高中是我动摇最大的时代,那三年让我彻底的摒弃了自己坚持了从小学到初中结束的理想,因为此刻的我发现,理想就是理想,是理想的生活,是理想中的理想。 我的大学南门有一排写字间,是学校划分给即将毕业的学生创业孵化的摇篮。记得我第一次来到这里的时候是得知自己被录取后,第二天就和妈妈来到了学校,那时候第一眼见到这一排的创业公司,觉得特别吃惊,因为在这时候的我看来,这些把自己的梦想亲自塑造出来的人该是多么的厉害。 某一个晚上,和舍友的闲聊 - “我刚刚去创意园了“ - “你去创意园干啥“ - “我有一个师兄在那边开了自己的公司,我就过去坐一坐了“ - “那你师兄是做啥的“ - “他开的是买衣服的“ - “卖衣服!?“ - “对,实体店加淘宝店,他以前是玩乐队的,他说以前深职的乐队特别多,玩摇滚的也很多,听他说当时还专门出了一个深职的专辑,不过没有发行,他卖的衣服都是自己设计的“ 昨天晚上 - “那位师兄不做了,他打算自己去找工作了”; 我当时很吃惊(还是这个词),吃惊之后就是满满的无奈。 每个人活着都有自己想做的事,可能对于我来说,大学就是提供给了我最好的时机做自己最喜欢的事,当然对于大多数的大学生来说同样也是如此,但是每每在我们努力朝着自己的方向前进的时候,深深的无力感总会压迫我们脆弱得一塌糊涂的坚持,也许某一天就会垮下去,接受社会的冲刷,让我们认清楚,不管理想有多么的伟大,最后终究会沦为生活的玩偶。这个时候我们或许是25岁了吧… ------------ 有些人25岁就死了,75岁才埋下。
继续阅读