Git Product home page Git Product logo

niexia.github.io's Introduction

Hi there 👋

  • 🔭 I’m currently working on TikTok Ads Interface and Platform.
  • 💬 We are looking for Frontend Engineers,to apply please send a resume to this email.

niexia.github.io's People

Contributors

niexia avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Forkers

web-logs2

niexia.github.io's Issues

如何设计产品:写商业计划书

科技公司的产品经理到底做些什么事情,他们是如何设计产品,把产品从构想变成现实?接下来学习一个产品从 0~1 转变的一个过程。

这个部分其实也分为三个部分:

  • 首先是设计产品,写商业计划书
  • 第二点就是说服领导争取资源。在你的商业计划书完成之后,要展现给领导,让他们批钱投资你的项目
  • 那第三点就是,带领团队实现产品。当领导们觉得你是个人才,你做出了一个优秀的产品计划书之后,他们批了钱,你就会有一个团队,来帮你一起把这个产品给实现出来

这次主要讨论第一部分:如何设计产品写商业计划书。

商业计划书要回答的问题

任何产品都是从想法开始的,然后当你如果有了一个产品想法的时候,你就需要用商业计划书的形式把这个产品写出来,而这个商业计划书著重围绕要回答大概 5 个关于这个产品的问题:

  1. 第一你这个产品的顾客是谁?
  2. 第二你这个产品到底是什么东西?
  3. 第三你这个产品和市面上的其他产品相比有什么不同的地方
  4. 第四你这个产品可以给这个公司赚多少钱
  5. 第五你这个产品需要多少的投资

要实现一个好的商业计划书,需要把刚才说的那 5 个问题回答的非常透彻。我们就用 UberEats 做个例子,来探讨一下这 5 个问题怎么回答。 在这之前,你也许会有疑问,所以产品的 idea(创意) 产品的想法到底是哪里来的?其实这个我觉得是在做产品经理初期,很多人会想的问题。

首先是这样子的,当你刚刚开始做产品经理的时候,一般你是不用考虑太多这方面的问题的。你刚开始做的时候,你老板会给你设定一个非常有限的范围,告诉你你要做什么,比如说亚马逊购物车按钮应该设计什么样子的颜色?应该放在哪里?你可能会做很多测试来选择一个最好的方案。在这个阶段你还不需要太多的自己来思考产品的想法,而慢慢的当你走到产品经理的后期,你就会需要自己设想出很多产品想法。在这个过程中首先你会有经经验的积累,你会慢慢知道如何来思考产品,如何来形成好的产品想法。其次就是观察生活,体验生活,用顾客的思维把自己想象成你是顾客,哪里的体验不够好,你希望有什么样子更好的体验,把这样子的想法整理出来,转换为一个新的产品想法。

产品的顾客是谁

回到 UberEats 这个案例,假设现在这个 APP 还没有出来,我们正在设想这个 idea(创意) 我们在思考什么样子一个问题,那就是有好多小伙伴在家里面特别懒、特别宅,不想要去餐馆,但是又想在家里面可以吃到各个餐馆的美食、拉面、西安名吃、港式点心。这样子的人群,他有这样子一个需求,但是没有办法得到满足。现在的市场可能就是他们必须要打电话去那个餐馆,问问那个餐馆有没有送货?能不能送到他家?然后可能要现金付费,要信用卡付费,特别不方便,那我们可能想要解决他们这样子一个人群的问题。

选择完这样子一个针对人群之后,我们要知道这个顾客群到底有多大,如果只有 100 个人,我设计出一个 APP 出来可能这个钱都挣不回来。如果有 100 万 1000 万的这样子的群体,那我可能要上市要发财了。**所以我们之后就要做很多的市场调查,到底有多少这样子的人会对这样子的一个产品有需求? **

如何做这样子调查?其实有很多种方式:比如说你可以找一个调研公司,帮你随机选一批符合你要求的人进来做调研,了解到底有多少人有这样子的需求。你也可以在网上找各方面的数据,现在有多少餐馆是提供外卖服务的?外卖收入是多少?占他们全部收入的多少。你也可以调研这些餐馆,他们在做外卖服务过程中有什么样子的困难?比如说是收费上的困难是营销方面的困难?你可以在这个调研过程中,了解你的产品中比较重要的一些 feature(功能), 一些功能是什么样子的。

当我们做完这个调研以后,知道我们这个顾客群体大概是哪些人以及有多大,这个产品的顾客到底是谁的问题。

产品到底是什么东西

第二是这个产品本身是什么东西,如果我来写 UberEats 是一个什么样的产品,我就会写,这是一个餐饮运输服务的 APP 顾客只需要按一个按钮,就把全程的美食都可以在 30 分钟之内送货上门。这里文案可能是简洁夸张一点,必须把这个顾客这个好处给体现出来,才可以让投资者看到这个东西的好处,主要就是体现于顾客的好处。

然后你在说这个产品的时候,你还需要描写到底顾客是怎么样去体验这个东西的,包括要 log in(登陆)这个产品,然后你可以选那个餐餐厅,然后你可以就是说根据那个餐厅的 review(评价) 或者是餐厅的那些 rating(评分),然后去决定我是不是要在这个餐厅点,然后我可以看见那些菜的那些图片,然后可以把那些菜加到我们 shopping cart(购物车)里面,然后我最后可以加自己的 payment(付款方式) 然后再去付账,然后去定这个 Order(订单)。 整个这个过程是需要把它写下来,让那些读者、那些投资者,在看完你这个商业计划书以后脑袋里面有非常非常清晰的印象:这个产品到底是一个什么样子的轮廓,这就是这个产品问题是需要怎么样回答。

当然这个过程中有一个非常重要的概念叫做 minimum viable product - MVP (最简可行产品)。 什么意思?也就是当你最开始第一个 launch(发布)你的产品的时候。你的产品要长什么样子?

大家都知道 UberEats 在刚上去的时候,不像现在有这么多的功能,可以在超市上购物、可以点评餐馆之类的。这个产品最开始发布的时候是什么样子的?需要有哪些功能?这就是 MVP而这个 MVP 和你最开始调研所得到的消费群体,以及他们的需求是密切相关的。比如说我们最开始调查出来消费者的需求是,我要在家里面就能吃到我想要吃的餐馆的食品。他们已经有一个想指定的餐馆想要点,这样子的话可能最开始我们不需要做 review feature(评价功能),我们不需要可以让他们看到不同餐馆的评价是什么样子的,只要他们能够点到他们想要餐馆就可以了。而如果最开始的需求是,我想要吃到西雅图大家评价最好吃的餐馆,那这个 review feature(评价功能)可能就非常重要了,就必须要在我们的 MVP 产品中实现。所以最开始调研也 inform(预知)、定义了我们的 MVP 产品要做到什么样子的程度。

这个时候大家可能要问一个问题,就是那为什么不一开始就把产品所有的 feature 都做出来?

如果你刚开始的商业计划书就写得非常宏大,你要 1000 万的资金,要 100 个人头来给你做这个产品的话,领导不一定会批的,因为这个产品对公司的成本来说太高了。所以定义这个 MVP 到底我们最低做到什么样的程度,能实现什么样的效果,其实非常重要,决定你之后能不能拿到资源的一件事情。太大你拿不到资源,太小领导觉得不 exciting(令人兴奋)、不刺激、不兴奋, 也不会给你批。选择一个合适的、最好的 MVP 至关重要。这也是区分一个很厉害的 PM(产品经历)和一个很 ok(普通)的 PM 的一个很大的分水岭。因为一个很厉害的 PM,一方面他们会给领导一个非常非常宏大的画面,但是另外一方面他们也会给领导一个非常 practical starting point(现实的起点)。他们会告诉领导我们从这个出发点是怎么样,最终可以到达这个很宏大的画面。既给他们一个很大的 picture(画面)
然后又告诉他们我们现在开始就可以做什么东西,这是一个 PM 的一个必备的技能。

就比如说我要实现全世界人民在家就能吃到所有美食,而它的 MVP 可能就是我现在点麦当劳,麦当劳可以送到我家,没错!

产品和市面上的其他产品相比有什么不同的地方

这第三个问题就是说你做的这个产品和市面上其他的产品有一些什么样的不同? 现在这个时代是没有公司想做那种 me too product(一样的产品),就是说别人做的那个东西,然后跑过去跟他做一模一样的东西。因为消费者不会选你的产品,因为你没有不同的地方,我们叫做 differentiation(产品差异化)。

那如果我是写 UberEats 这个商业计划书的人,我会说 UberEats 和市面上其他产品的很大的不同,是 UberEats 可以让顾客点完这个餐以后,用最快的速度把这个餐送到顾客手中。而 UberEats 可以达到最快的速度,是因为 UBER(优步)他有世界上最庞大的司机群体。不管你随时随地点餐,我们都会有一个很近很近的司机,而这个司机可以在很高效的时间内把这个顾客点的餐,送给这个顾客。

然后我会尽量去做研究,把它数字到说每一个单我会比其他的竞争对手快多少?是快两分钟还是快 10 分钟。然后我也会搞清楚消费者到底会多么 value(看重)快这 10 分钟甚至是 20 分钟。这样也可以搞清楚我这个产品和其他产品比,差异到底有多大。往往回答这个差异化问题,是决定你这个产品能不能拿到大佬投资的一个关键性的问题。因为如果你这个产品一旦有很强的 differentiation(差异),你把这个产品发布以后就会有很多顾客用你的。很多顾客用你这个产品,也就是说你给这个公司带来的收益也就会相当大。

当然这个 differentiation factors(差异因素)其实可以从很多方面来体现的,不一定是送餐的 speed(速度),还可以从你这个平台上 selection(选品)。比如说我这个平台有鼎泰丰,其他平台没有,那我们这在这个 selection(选品)方面有很大优势。可能那些鼎泰风格顾客就会只会用我们的。然后你的顾客群体可能是不一样的,比如说什么饭团、外卖、熊猫外卖之类的,他们针对**人的群体,那可能很多**小伙伴只想要选择**的餐厅,他们只会上这样子的 APP ,所以这就是这些华人 APP 的 differentiating factor(差异因素) 与众不同的点。他们不一定是送餐最快的,他们不一定是 APP 设计最好的,他们也不一定是餐馆选择范围最广的,但是他们针对的群体非常的固定,他们的语言非常的有针对性。

另外你看这个差异的时候,你不仅要看这个差异给这个顾客带来多大的 benefit(好处),你还要看其他的竞争对手如果想去 copy(复制)你这个差异会有多难。比如说我们刚刚讲的那个例子 UberEats 那个司机群体,你是需要花很多时间才可以有一个那么大的司机群体,所以说别人去复制你这个差异就会很难。如果说我这个 APP 可能就专门去 target(对准)华人群体讲中文,可能那些英文的 APP 把它翻译成中文,这个就没有那么那么难,对比你要去建一个很大的司机群体。所以说 differentiation(差异)本身也要看 Barrier to Entry(进入壁垒),就说你这个差异是别人很难复制的,还是别人很容易复制的,而这个问题往往是在大脑决定给不给你项目投资最重要的一个问题之一!

可以给这个公司赚多少钱,需要多少的投资

那第 4 个问题也就是我们之前讲的,你这个产品到底会给这个公司带来多少收益,以及这个公司需要出多少钱才能把这个产品给开发出来。

那这里就和公司里面的 finance partner(财务搭档)合作,做这样子的一个计算。当然你可能会遇到非常优秀的 finance partner(财务搭档) ,他们可能也非常喜欢你的这个项目,然后很认真的把这些计划帮你算好,做出一个很详细的财务报表。你也可能会碰到一个不是特别积极工作的 partner(搭档) ,这个时候可能你就需要自己发挥你的想象力,发挥你天才的数学头脑,把这样子的一个报表给做出来。这也是为什么说就是 pm 其实就像万金油,因为 pm 什么东西都要做。如果你碰到一个很给力的 finance partner(财务搭档) ,你告诉他我一个这样的产品,你帮我算一下他以后可以达到多大。可能那个给力的 finance partner(财务搭档) 基本上已经算出来了。但如果那个 finance partner(财务搭档)不给力的话,你就需要去做这个 finance partner(财务搭档)的事情。然后你做完这个 finance partner(财务搭档)这个事情,你还要需要跟那个 finance partner review(审阅) 。因为你只有拿到这个财务搭档的认可以后,你才能说这个东西做完了。这个时候如果你是从商业分析师 BA(business analyst)转过来的产品经理,就有非常大的优势了,因为这个对你来说就是小意思。

前面讲那几个方面是一个商业计划书主要需要回答的几个问题。当然有很多其他的问题,比如说你这个产品本身它有什么样子的风险?比如说你是怎么准备去发布这个产品?还有就是说很多类似其他的问题,根据产品本身不同也会有衍生出不同的问题。所以一个优秀的商业计划书,是需要把那些可能出现的问题全部给 cover(概括)到的。

就拿 UberEats 来说,你可能要考虑到,如果司机在送货过程中受伤了怎么办? 如果餐馆食品质量安全问题导致顾客拉肚子怎么办? 这些都是你在产品计划书里面需要 address(阐述) ,让领导知道你已经思考到了这些方方面面细节的问题。因为你没有思考到当你呈现给领导,领导发现了这个问题,那他对你这个项目投资的可能性就会降低很多,

总结

现在来总结产品商业计划书需要包括的几个比较重要的问题:首先第一点 你的顾客群体是谁?有多大?他们的需求是什么? 第二点你的产品到底长什么样子?用户体验是什么样子?第三点一个产品跟市面上其他的产品比有什么样子的不同? 第 4 点你这个产品需要多少投资,会给公司带来多少收益?那第 5 点就是一些其他的问题。

script 标签 - async & defer

看一下这张图,已经说明了普通 script,async 和 defer。

image

普通 script

一个普通的 script 标签如下:

<script src="foo.js"></script>

当解析 HTML 遇到 <script> 的时候:

  1. 停止解析;
  2. 请求下载 foo.js(如果它是外部引入的,不是内联脚本内联脚本) ;
  3. 执行 foo.js
  4. 执行完成之后,继续解析 HTML

async

<script src="foo.js" async></script>

添加 async 之后,解析遇到这个 <script> 时:

  1. 并行的请求下载 foo.js,不停止解析;
  2. 下载完成之后,停止解析,立即执行下载的脚本;
  3. 执行完成之后,继续解析。

值得注意的是,async 脚本会在脚本加载后立即执行,因此不能保证执行顺序(最后包含的脚本可能会在第一个脚本文件之前执行)。

defer

<script src="foo.js" defer></script>

添加 defer 之后,解析遇到这个 <script> 时:

  1. 并行的请求下载 foo.js,不停止解析;
  2. 只有解析完成之后,在 DOMContentLoaded 实践前执行。

async 不同,defer 保证按照它们在页面中出现的顺序执行。

总结

  • async 会阻止 document 的解析;
  • defer 会在 DOMContentLoaded 前依次执行;
  • async 则是下载完立即执行,不一定是在 DOMContentLoaded 前;
  • async 因为顺序无关,所以很适合像 Google Analytics 这样的无依赖脚本;

参考

费曼学习法

费曼学习法的灵感源于诺贝尔物理奖获得者理查德·费曼(Richard Feynman),运用费曼技巧,你只需花上 20 分钟就能深入理解知识点,而且记忆深刻,难以遗忘。

知识有两种类型,我们绝大多数人关注的都是错误的那类

  1. 第一类知识注重了解某个事物的名称
  2. 第二类知识注重了解某件事物

这可不是一回事儿。著名的诺贝尔物理学家理查德•费曼(Richard Feynman)能够理解这二者间的差别,这也是他成功最重要的原因之一。事实上,他创造了一种学习方法,确保他会比别人对事物了解的更透彻。

理查德•费曼

965年获得诺贝尔物理学奖,美籍犹太人。理论物理学家,量子电动力学创始人之一,纳米技术之父。因其对量子电动物理学的贡献获得诺贝尔物理学奖。他被认为是爱因斯坦之后最睿智的理论物理学家,也是第一位提出纳米概念的人。

什么是费曼学习法

费曼学习法可以简化为四个单词:Concept (概念)、Teach (教给别人)、Review (回顾)、Simplify (简化)

费曼学习法的四个步骤

该技巧主要包含四步:

1. 把它教给一个小孩子

拿出一张白纸,在上方写下你想要学习的主题。想一下,如果你要把它教给一个孩子,你会讲哪些,并写下来。这里你的教授对象不是你自己那些聪明的成年朋友,而是一个8岁的孩子,他的词汇量和注意力刚好能够理解基本概念和关系。

许多人会倾向于使用复杂的词汇和行话来掩盖他们不明白的东西。问题是我们只在糊弄自己,因为我们不知道自己也不明白。另外,使用行话会隐藏周围人对我们的误解。

当你自始至终都用孩子可以理解的简单的语言写出一个想法(提示:只用最常见的单词),那么你便迫使自己在更深层次上理解了该概念,并简化了观点之间的关系和联系。如果你努力,就会清楚地知道自己在哪里还有不明白的地方。这种紧张状态很好——预示着学习的机会到来了

2. 回顾

在第一步中,你不可避免地会卡壳,忘记重要的点,不能解释,或者说不能将重要的概念联系起来。

这一反馈相当宝贵,因为你已经发现了自己知识的边缘。懂得自己能力的界限也是一种能力,你刚刚就确定了一个!
这是学习开始的地方。现在你知道自己在哪里卡住了,那么就回到原始材料,重新学习,直到你可以用基本的术语解释这一概念。

认定自己知识的界限,会限制你可能犯的错误,并且在应用该知识时,可以增加成功的几率。

3. 将语言条理化,简化

现在你手上有一套自己手写笔记,检查一下确保自己没有从原材料中借用任何行话。将这些笔记用简单的语言组织成一个流畅的故事。将这个故事大声读出来,如果这些解释不够简单,或者听起来比较混乱,很好,这意味着你想要理解该领域,还需要做一些工作。

4. 传授(可选)

如果你真的想确保你的理解没什么问题,就把它教给另一个人(理想状态下,这个人应该对这个话题知之甚少,或者就找个 8 岁的孩子)。检测知识最终的途径是你能有能力把它传播给另一个人

这不仅是学习的妙方,还是窥探不同思维方式的窗口,它让你将想法撕开揉碎,从头重组。这种学习方法会让你对观点和概念有更为深入的理解。重要的是,以这种方式解决问题,你可以在别人不知道他们自己在说什么的情况下,理解这个问题。

费曼的方法直观地认为智力是一个增长的过程,这与 Carol Dweck 的研究非常吻合,Carol Dweck 精确地描述了停滞型思维(fixed mindset)和成长型思维(growth mindset)之间的区别

Generator与异步编程

一个 Generator

Generator 可以看作是一个状态机,保存了多个内部状态,执行 Generator 函数会返回一个遍历器对象,它可以用来遍历 Generator 内部的每个状态。

调用 Generator 之后,函数并没有执行,而是返回一个指向内部状态的指针对象,也就是遍历器对象。每次调用遍历器对象的 next 方法,内部指针从函数头部或者上次执行停下来开始执行,直到遇到 yield 表达式或者 return 语句为止。

调用 next 方法返回的是一个有着 value 和 done 两个属性的对象。value 属性表示当前状态的值,是 yield 或者 return 后面那个表达式的值;done 属性是一个布尔值,表示是否遍历结束。

const generator1 = function* () {
  yield 1;
  yield 2;
 return 3;
};
const iterator1 = generator1();

console.log(iterator1.next()); // {value: 1, done: false}
console.log(iterator1.next()); // {value: 2, done: false}
console.log(iterator1.next()); // {value 3, done: true}
console.log(iterator1.next()); // {value: undefined, done: true}

yield 表达式

yield 是暂停标志,遍历器的 next 方法运行的逻辑如下

  1. 遇到 yield 表达式,就暂停执行后面的操作,并将紧跟在 yield 后面的那个表达式的值,作为返回的对象的 value 属性值。
  2. 下一次调用 next 方法时,再继续往下执行,直到遇到下一个 yield 表达式。
  3. 如果没有再遇到新的 yield 表达式,就一直运行到函数结束,直到 return 语句为止,并将 return 语句后面的表达式的值,作为返回的对象的 value 属性值。
  4. 如果该函数没有 return 语句,则返回的对象的 value 属性值为 undefined。

需要注意的是,yield 表达式后面的表达式,只有当调用 next 方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。

function* gen() {
  yield  123 + 456;
}

表达式 123 + 345 并不会立即求值,只会在 next 将指针移动到这一句时才执行。

yield 语句的返回值

const generator2 = function* () {
  const result = yield 3.141592;
  console.log(result);
};

const iterator2 = generator2();
const next = iterator2.next();

iterator2.next(next.value.toFixed(2)); //3.14

Generator 的自动执行

const generator3 = function* () {
  yield console.log(1);
  yield console.log(2);
  console.log(3);
};

const run1 = myGenerator => {
  const myIterator = myGenerator();
  let next;
  do {
    next = myIterator.next();
  } while (!next.done)
};

run1(generator3);
// 1
// 2
// 3

Generator 的特点

  • 在 iterator.next() 被调用之前,generator 暂停在 yield 语句上,不会往下执行。串行执行,继续执行的时间点可控。
  • yield 语句可以有输出(返回值)。每一个异步单元都有一个(或多个)输出。

定个小目标

基于 Promise 的 2 步串行异步编程

const generator4 = function* () {
  const author = yield Authors.findOne({
    where: {name: 'Curry'}
  });
  const books = yield Books.find({
    where: {authorId: author.id}
  });
}

编写自动执行器

  1. 触发迭代器 next 方法的时机:在 promise 的 then 中 iteration.next();
  2. 为 yield 语句注入返回值:把 resolve 值绑定到 iteration.next 的参数。
const run2 = myGenerator => {
  const myIterator = myGenerator();
  let next = myIterator.next();
  const loop = () => {
    if (!next.done) {
      next.value.then(data => {
        next = myIterator.next(data);
        loop();
      });
    }
  }
  loop();
};

const generator5 = function* () {
  const a = yield new Promise((resolve, reject) => {
    setTimeout(resolve.bind(null, 1), 1000);
  });
  console.log(a);
  const b = yield new Promise((resolve, reject) => {
    setTimeout(resolve.bind(null, 2), 2000);
  });
  console.log(b);
};

run2(generator5);

加入错误处理

const run3 = myGenerator => {
  const myIterator = myGenerator();
  return new Promise((resolve, reject) => {
    let next;
    try {
      next = myIterator.next();
    } catch (e) {
      reject(e);
    }
    const loop = () => {
      if (!next.done) {
        next.value.then(data => {
          try {
            next = myIterator.next(data);
          } catch (e) {
            reject (e);
          }
          loop();
        }).catch (reject);
      } else {
        resolve(next.value);
      }
   }
   loop();
  });
};

如何看“前端已死”

现象

前端已死,我们的出路究竟在哪里?
也谈“前端已死”
大家都在说工作很难找,发现前端的坑变少了。

原因

任何行业的发展都会经历发展、稳定和衰落的阶段,前端行业也不例外。目前互联网进入平稳期,岗位的需求也变得平稳,新人不断进入,必然在某个节点前端也达到饱和。对于那些没有竞争力的人来说,将会面临就业困境。

破局

寄希望于大环境的好转不能解决问题,更重要是提升个人的核心竞争力。核心竞争力通俗来说就是能干别人干不了的活,能做别人不能做的事,能给团队创造更大的价值。

所以对个人的要求是“要有为团队创造更大价值的意识”。不要浑浑噩噩的被动执行,等需求、写代码、上线。做事要精益求精,努力把当下的事情做好,也主动去做那些工作以为对团队有帮助的事情。

一个陷阱是,把使用工具的能力作为核心竞争力。想具有区分度,需要“更广的广度”,例如图像处理、Node 开发;“更深的深度”,前端基本功扎实积累深厚,熟悉各种 API ,最佳实践信手拈来;“产品意识和设计审美”,开发的产品体验非常好,干活很细。

未来

学习技术不应该只是为了追求短期的收益和前途,重要不是学了什么而是学得怎样,心无旁骛专注自身。

前端这个职业本身不会消失,因为人机交互行为一直存在。人工智能的兴起也会对前端产生影响,如果安于现状则是危机,如果勤于学习,则是机遇,让你产出更加高效。

总之,学习的能力是最核心的竞争力。

Timeboxing: 一个简单而强大的技术来提高你的生产力

原文链接 https://www.spica.com/blog/timeboxing

Timeboxing 是一种非常简单和流行的时间管理技术,可以帮助你更好地控制你的时间表。

这也是一种非常有用的技术,可以练习自律,并以最重要的任务为先的方式来安排你的日程。

有了 Timeboxing,你还可以更注意你在某项任务上花了多少时间,以避免过度

什么是 timeboxing?

timeboxing 的意思很简单,就是你打开日历,输入一个你将来要花在某项任务上的时间块

你不是完成任务之前一直工作(个人承担一项任务并一直工作到完成),而是主动决定你要花多少时间在上面,什么时候(甚至在哪里)

timeboxing 意味着在你的日历中为某项任务设定一个固定的时间量。这就像在你的日历中安排一个会议。你选择日期,开始和结束时间,定义所需的结果,并在你的日历中保留时间。

一旦你预留了一盒时间,你就应该像对待一个预定的会议一样对待它--不能迅速重新安排,当你在进行 timeboxing 的任务时不能分心,等等。

对于较大的任务,你可以提前保留几个时间块。有了这样的方法,你就可以完全控制你的时间表和优先事项

timeboxing 的最大好处

如果您使用 timeboxing 时间管理技术,会有许多不同的好处。以下是主要好处:

  • 你可以更轻松地“强迫自己”开始执行你拖延的任务,或者你知道这些任务对您来说很难,
  • 你可以更轻松地对在特定任务上花费的时间和时间设置严格的限制,这样你就可以更好地组织自己,
  • 如果你确保在限定时间内完成任务时没有人打扰您或分散您的注意力,你可以大大提高您的工作效率和注意力,
  • 这是处理完美主义和任何过度处理和过度执行任务的好方法,
  • 你可以使用 timeboxing 来计划早上最重要的事情,它可以帮助你总体上计划更好的工作节奏。

使用 timeboxing 方法,你可以避免延迟交付、低质量以及过度执行和过度处理任务。如你所知,时间过得很快,通过 timeboxing 你可以很好地控制它,确保它不会失控地飞走。

使用 timeboxing 开始你拖延的任务

有时你不得不做一个你真的不喜欢的任务,或因其他原因而拖延。timeboxing 可以帮助你解决这个问题。

我们人类有一个非常有趣的(有时是有益的)心理现象,那就是在你开始一项活动后,你想完成它。例如,你可能已经发现自己一直在看一部电影,即使它是一部糟糕的电影,但不知为何你就是不能关掉电视。

相反,你可能有一个任务,你就是不能开始工作,但在你开始工作几分钟后,你就忘记了挣扎,欣然接受了任务。

timeboxing 的概念是,你在日历提醒你该完成任务的确切时刻,开始做一项任务

如果你知道你必须处理一项你拖延的任务,如果有必要,你设置10个闹钟,提醒你真正开始工作

一旦你开始工作,你很快就会忘记之前存在的所有紧张和阻力。一个真正有用的方法。如果你有一个更大的任务,你可以,例如,把大任务切成许多迷你任务,需要1-2个小时来完成,你只需给第一个迷你任务设定时间

这样一来,你就会开始为大任务工作,即使你只迈出了一小步,你也会对自己和你的生产力感觉好很多。

更重要的是,你会更轻松地继续完成大任务,因为你已经迈出了第一步。另外,你可以在你的日历上划定一个时间段,开始处理这个大任务,在几个小时的时间段内进行工作,并尝试尽可能地达到目的。

你可能会遇到的情况是,你陷入了流程中,做的事情比你计划的多得多。你只需确保你不会做得太过火或越过为完成整个任务所规定的时间限制

Timeboxing 将会帮你设置严格的限制

每项任务需要的时间与你投入的时间完全相同。如果你决定在一项任务上花两个小时而不是10个小时,你可能要以更集中的方式工作,确保在任务的重要部分工作,遗漏一些细节,等等。

如果你是一个完美主义者,在没有任何时间限制的情况下完成一项任务,那么你可能需要永远完成一项任务,或者至少比它可能需要的时间多很多

在没有对投入的时间进行严格限制的情况下开始一项任务,也没有考虑到这项任务到底有多重要,以及它对你的价值创造会产生什么影响,这就意味着要以一种完全被动和无组织的方式工作,而不是以一种高度有组织的方式主动工作

关于明确定义产出的清单还可以包括什么被认为是足够好的产出,以及何时开始工作最合理。

帕金森定律指出,"工作的扩展是为了填补完成工作的时间"。有了 timeboxing,你就为你在一项任务上花费的时间设置了最大的限制。
timeboxing 是帮助你在开始工作前战略性地回答所有这些问题的好方法。

当你在你的日历上划分时间时,你只需花一两分钟时间,考虑你所掌握的关于任务的所有事实,并粗略估计一项任务应该花多少时间。

然后,你在看板上贴上便利贴,并在日历上保留一个时间块。就是这么简单。

用Timeboxing更好地组织你的日历

使用Timeboxing,你可以非常好地组织和规范你的日历。特别是有两种非常有用的方式来使用Timeboxing:决定你什么时候花多少时间在电子邮件上;以及你什么时候有多少会议。

例如,你可以在你的日历上写上,你在工作日开始时有30分钟,结束时有30分钟用于发送电子邮件。

你可以决定每天最多召开两次30分钟的会议,但周三除外,因为周三你没有会议,而周五你有更长的会议,而且更多。

你只要填满这些空档,直到你用完为止。你用剩下的时间在流程中处理最重要的任务。

主要的想法是,通过Timeboxing,你可以完全控制你的时间表,你提前考虑你将把时间花在什么地方,并确保你真的把时间花在重要的事情上。

通过Timeboxing,你在日历中设置了一些关于任务的严格限制,你应该确保你永远不会跨越这些限制

以同样的方式,你可以给许多不同的事情设定Timeboxing,比如午餐时间,以确保你的身体在需要的时候得到所有的营养,你可以给一些活动设定Timeboxing,你也可以把这些活动组合在一起,比如跑腿、销售会议、销售电话,或者其他。

你也可以给所有不同类型的活动设定Timeboxing,如头脑风暴,执行任务,有一小时的个人权力用于阅读,花时间与你的孩子在一起,甚至有一个无干扰的一天或无干扰的一周。

Timeboxing架是一个如此简单和有效的方法,你可以在许多不同的方面使用它。

为会议设定时Timeboxing,对无益的会议进行限制

会议通常是对时间的巨大浪费,但不时地,它们仍然可以成为真正的工作,特别是当你需要人们就某件事情达成一致,或需要集体的脑力来解决一个问题。

如果你决定要开一个会议,而且不是一个有创意或有凝聚力的会议,Timeboxing可以帮助你设定一个严格的限制,以阻止会议变成一个浪费时间的活动。

在召集会议时,你应该向所有被邀请者发送严格的开始和停止时间,以及会议的主要预期产出和议程。

对会议结束的时间有一个严格的期限,将有助于保持人们的注意力,防止他们在无益的讨论中流走。

有时间限制的会议的一个例子也是与你自己的早晨计划会议,它不应该花费你超过15分钟。之后你也可以和你的团队做同样的事情,只是要确保每个人在会议期间都是站着的,这样就真的只需要15分钟了。

Timeboxing的主要好处是提高对完成一项任务所需的专门时间的认识。

敏捷软件开发中的Timeboxing

在敏捷软件开发中,有几种类型的会议是固定的,而且基本上是预先设定好时间的--贯穿整个项目期,没有例外。

它们以相同的时间间隔和相同的议程类型发生。这也是实践中时间框的一种方式。

敏捷软件开发中的分时会议有。

  • 每天的Scrum(限制:15分钟)--每天15分钟的同步会议,每天在同一时间发生,通常在同一地点。
  • 冲刺计划(限制:2小时) - 这是一个计划会议,团队决定哪些任务将在下一个冲刺阶段完成,通常在7天内完成。
  • 冲刺 review(限时:1小时)--在冲刺 review 中,团队 review 已经完成了哪些工作,还没有完成哪些工作,增加了哪些工作,以及从冲刺中删除的工作。
  • 冲刺回顾 - 这是冲刺结束时的一次会议(或两次不同的会议),团队在会上回顾哪些工作做对了,哪些可以做得更好。

所有这些会议都是在敏捷团队的日历中提前进行的。它提供了结构、稳定性和良好的工作动态。

但更重要的是,每个会议都有非常严格的时间限制,它可以持续多长时间。例如,每天的Scrum不应该超过15分钟。这也是在日历中预定会议框中保留的时间。

哪些应用程序可以帮助您使用timeboxing?

您不需要任何特殊工具来使用timeboxing时间管理技术。你所需要的只是一个像谷歌日历或任何其他工具。在日历中,您只需为一项任务预留特定的时间,就像您安排会议一样。

您所要做的就是确保在时间到来时开始执行任务,并在时间用完时停止执行任务。

有些人使用闹钟或计时器作为开始和停止工作的提醒。如果你觉得它有用,一定要试试。其他人将timeboxing与番茄技术相结合,这意味着他们将 timeboxed time 段调整为特定的时间间隔。

番茄工作法建议将任务分解为间隔,传统上为 25 分钟,中间有短暂的休息时间。

与timeboxing一起使用的有益工具也是时间跟踪器。使用一个简单易用的时间跟踪器,您可以更准确地监控您在不同任务上花费了多少时间,即使它们是预先设定的 timeboxed。然后,您可以获得详细的报告和统计数据,甚至可以简化您的计费。

享受使用timeboxing技术!这比用待办事项清单来管理你的生活要有效得多。

其他参考文档

理解 scrollTop,offsetTop 和 clientTop 等

先看一下这样图

image

scrollX

image

  • scrollTop、scrollLeft

一个元素的内容垂直滚动和水平滚动的像素值。scrollTop 可以读取或设置这个元素的顶部到视口可见内容(的顶部)的距离。当一个元素的内容没有产生垂直方向的滚动条,那么 scrollTop 的值是 0scrollLeft 可以读取或设置元素滚动条到元素左边的距离。当一个元素的内容没有产生水平方向的滚动条,那么 scrollLeft 的值是 0

  • scrollWidth、scrollHeight

为只读属性。scrollWidthscrollHeight 属性指包含滚动条在内的该元素的视觉面积。视口之外隐藏的部分也被包含在里面。那么 document 对象的 scrollWidthscrollHeight 属性就是网页的大小,意思就是滚动条滚过的所有长度和宽度。

offsetX

image

  • offsetTop、offsetLeft

为只读属性。offsetTop 返回当前元素相对于其 offsetParent 元素的顶部的距离。offsetLeft 返回当前元素相对于其 offsetParent 元素的左边界的距离。

注意offsetTopoffsetLeft 不包含 offsetParentborder

  • offsetWidth、offsetHeight

为只读属性。offsetWidth 返回一个元素的布局宽度,offsetWidth 包含元素的 borderpaddingcontent 的宽度和竖直方向滚动条 scrollBar(如果存在)。offsetHeight 返回一个元素的布局高度,offsetHeight 包含元素的 borderpaddingcontent 的高度和水平方向滚动条 scrollBar(如果存在)。

clientX

image

  • clientTop、clientLeft

为只读属性。clientTop 返回一个元素顶部边框的宽度,不包括外边距和内边距,即 border-top-widthclientLeft 返回一个元素左边框的宽度,不包括外边距和内边距,即 border-left-width

  • clientWith、clientHeight

为只读属性。clientWith 表示元素内部的宽度,即 content 宽度和 padding。但是不包含外边距、边框和垂直滚动条(如果有)。clientHeight 表示元素内部的高度,即 content 高度和 padding。但是不包含外边距、边框和水平滚动条(如果有)。内联元素的 clientWidthclientHeight0

总结

如果一个元素没有滚动条,那么它的 scrollWidthclientWidth 应该是相等的。scrollHeightclientHeight 也相等。而 offsetWidth clientWidth 相比,offsetWidth 还包含了 borderoffsetHeightclientWidth 也类似。

应用

  1. 滚动到顶部
element.scrollTop = 0
  1. 元素是否滚动到底
element.scrollHeight - element.scrollTop === element.clientHeight
  1. 获取元素的绝对位置。

元素的绝对位置,指该元素的左上角相对于整张网页左上角的坐标

这个绝对位置需要通过计算才能得到。每个元素都有 offsetTopoffsetLeft 属性,表示该元素左上角和父容器(offsetParent 对象)左上角的距离。所以,只要将这两个值累加,就可以得到元素的绝对位置。

function getElementLeft(element) {
  var actualLeft = element.offsetLeft;
  var current = element.offsetParent;

  while (current !== null) {
    actualLeft += current.offsetLeft;
    current = current.offsetParent;
  }

  return actualLeft;
}

function getElementTop(element) {
  var actualTop = element.offsetTop;
  var current = element.offsetParent;

  while (current !== null) {
    actualTop += current.offsetTop;
    current = current.offsetParent;
  }

  return actualTop;
}
  1. 获取网页元素的相对位置

网页元素的相对位置,指该元素左上角相对于浏览器窗口左上角的坐标

有了绝对位置之后,获得相对坐标就容易了,只要将绝对坐标减去滚动条滚动的距离就可以了。滚动条滚动条的垂直距离,是 document 对象的 scrollTop;滚动条滚动的水平距离是 documentscrollLeft 属性。对上面的方法改写一下

function getElementViewLeft(element) {
  var actualLeft = element.offsetLeft;
  var current = element.offsetParent;

  while (current !== null) {
    actualLeft += current.offsetLeft;
    current = current.offsetParent;
  }

  if (document.compatMode == "BackCompat") {
    var elementScrollLeft = document.body.scrollLeft;
  } else {
    var elementScrollLeft = document.documentElement.scrollLeft;
  }

  return actualLeft - elementScrollLeft;
}

function getElementViewTop(element) {
  var actualTop = element.offsetTop;
  var current = element.offsetParent;

  while (current !== null) {
    actualTop += current.offsetTop;
    current = current.offsetParent;
  }

  if (document.compatMode == "BackCompat") {
    var elementScrollTop = document.body.scrollTop;
  } else {
    var elementScrollTop = document.documentElement.scrollTop;
  }

  return actualTop - elementScrollTop;
}

除了这种方法之外,还有一种快速的方法,那就是使用 getBoundingClientRect() 方法。它返回一个对象,其中包含了 leftrighttopbottom 四个属性,分别对应了该元素的左上角右下角相对于浏览器窗口(viewport)左上角的距离。

所以,网页元素的相对位置就是

var X = this.getBoundingClientRect().left;
var Y = this.getBoundingClientRect().top;

再加上滚动的距离,就可以得到绝对位置

var X = this.getBoundingClientRect().left + document.documentElement.scrollLeft;
var Y = this.getBoundingClientRect().top + document.documentElement.scrollTop;

参考

跨域

通过 XHR 实现的 Ajax 请求时有一个主要限制,来源于浏览器的同源策略。同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。

所以默认情况下,XHR 对象只能访问与包含它的页面位于同一个域中的资源。这种安全策略可以预防某些恶意行为。但是,实现合理的跨域请求对开发某些浏览器引用程序也是至关重要的。

同源的定义

如果两个页面的协议,端口(如果有指定)和主机都相同,则两个页面具有相同的源。我们也可以把它称为“协议/主机/端口 tuple”,或简单地叫做“tuple"。

下表给出了相对 http://store.company.com/dir/page.html 同源检测的示例:

URL 结果 原因
http://store.company.com/dir2/other.html 成功 只有路径不同
http://store.company.com/dir/inner/another.html 成功 只有路径不同
https://store.company.com/secure.html 失败 不同协议 ( https和http )
http://store.company.com:81/dir/etc.html 失败 不同端口 ( http:// 80是默认的)
http://news.company.com/dir/other.html 失败 不同域名 ( news和store )

同源策略限制了一下行为:

  • Cookie、LocalStorage 和 IndexDB 无法读取
  • DOM 和 JS 对象无法获取
  • Ajax 请求发送不出去

实现跨域请求

CORS(Cross-Origin Resource Sharing,跨域源资源共享)

CORS 是 W3C 的一个工作草案,定义了在必须访问跨域资源时,浏览器与服务器应该如何沟通。CORS 背后的基本**,就是使用自定义的 HTTP 头部让浏览器与服务器进行沟通,从而决定请求或者响应是否应该成功,还是应该失败

整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
因此,实现 CORS 通信的关键是服务器。只要服务器实现了CORS 接口,就可以跨源通信。

两种请求方式

请求方式分两种:一种是简单请求,另一种是非简单请求。只要满足下面条件就是简单请求:

  • 请求方式为:HEAD、POST 或者 GET;
  • http 头信息不超出一下字段:Accept、Accept-Language 、 Content-Language、 Last-Event-ID、 Content-Type(限于三个值:application/x-www-form-urlencoded、multipart/form-data、text/plain)。

区分这两种请求方式是因为浏览器对这这两种方式的处理方式是不同的。

简单请求

对于简单的请求,在发送时,浏览器发现它是一个 CORS 请求,就会在就是在头信息之中,增加一个 Origin 字段,其中包含请求页面的源信息(协议、域名和端口),以便服务器根据这个头部信息来决定是否给予响应。

例如:

GET /cors HTTP/1.1
Origin: http://www.nczonline.net

如果服务器认为这个请求可以接受,就在 Access-Control-Allow-Origin 头部中回发相同的源信息(如果是公共资源,可以回发“*”),例如:

Access-Control-Allow-Origin: http://www.nczonline.net

如果没有 Access-Control-Allow-Origin 这个头部,或者有这个头部但源信息不匹配,浏览器就会驳回请求。正常情况下,浏览器会处理请求。注意,请求和响应默认是都不包含 cookie 信息的。

所以即使跨域了,但是请求还是发送了,只是在处理响应时,浏览器发现跨域,才驳回的

非简单请求

CORS 通过 Preflight Request 的透明服务器验证机制支持开发人员使用自定义头部、GET 或 POST 之外的方法、不同类型的主体内容。

这些非简单的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为“预检请求”(preflight request)。使用 OPTIONS 方法,发送一下头部:

  • Origin:与简单的请求相同
  • Access-Control-Request-Method:请求自身使用的方法
  • Access-Control-Request-Header:(可选)自定义的头部信息,多个头部以逗号分隔。

例如,以下是一个带有自定义头部 NCZ 的使用 POST 方法发送请求时的 Preflight 请求:

Origin: http://www.nczonline.net
Access-Control-Request-Method: POST
Access-Control-Request-Header: NCZ

发送这个请求之后,服务器可以决定是否允许这种类型的请求。服务器通过在相应中发送如下头部与浏览器进行沟通。

  • Access-Control-Allow-Origin:与简单请求相同;
  • Access-Control-Allow-Methods:允许的方法,多个方法以逗号隔开;
  • Access-Control-Allow-Headers:允许的头部,多个头部以逗号分隔
  • Access-Control-Max-Age:应该将这个 Preflight 请求缓存多长时间(以秒计算)

例如:

Access-Control-Allow-Origin: http://www.nczonline.net
Access-Control-Allow-Methods: POST,GET
Access-Control-Allow-Headers: NCZ
Access-Control-Max-Age: 1728000

Preflight 请求结束后,会按照指定的响应时间缓存结果。在此有效时间内,不用发出另一条预检请求。

一旦服务器通过了"预检"请求,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样,会有一个 Origin 头信息字段。

如果浏览器认定,服务器不同意预检请求,会触发一个错误,被 XMLHttpRequest 对象的 onerror 回调函数捕获。

带凭据的请求

默认情况下,跨源请求不提供凭据(cookie、HTTP 认证及客户端 SSL 证明等)。通过将 withCredentials 属性设置为 true,可以指定某个请求应该发送数据。如果服务器接受带凭据的请求,会用以下 HTTP 头部来响应:

Access-Control-Allow-Credentials: true

如果发送的是带凭据的请求,但服务器的响应没有包含这个头部,那么浏览器就不会把响应交给 JavaScript(于是,responseText 中将是空字符串,status 的值为 0,而且会调用 onerror() 事件处理程序)。另外,服务器还可以在 Preflight 响应中发送这个 HTTP 头部,表示允许源发送带凭据的请求。

接下来,看一下其他的跨域技术。

在 CORS 出现以前,要实现跨域 AJax 通信破费周折。开发人员想出了一下方法,利用 DOM 中能够执行跨域请求的功能,在不依赖 XHR 对象的情况下也能发送某种请求。虽然 CORS 已经无处不在,但是开发人员自己发明的这些技术仍然被广泛应用,毕竟这样不需要修改服务器代码

图像 Ping

使用 标签,我们知道,一个网页可以从任何网页中加载图像,不用担心跨域。这也是在线广告跟踪浏览量的主要方式。可以动态的创建图像,使用它们的 onload 和 onerror 时间处理程序来确定是否收到了响应。

动态创建图像经常用于图像 Ping图像 Ping 是与服务器进行简单、单向的跨域通信的一种方式。请求的数据是通过查询字符串的形式发送的。而响应是任意内容,通常是图像像素,或 204 响应。通过图像 Ping,浏览器得不到任何具体的数据,但是通过监听 onload 和 onerror 事件,它能知道响应是什么时候接收到的。

var img = new Image();
img.onload = img.onerror = function() {
  alert("Done!");
}
img.src = "http://www.example.com/text?name=Nicholas";

这里创建了一个 Image 实例,然后给 onload 和 onerror 事件绑定同一个函数,这样无论什么响应,只要请求完成,都能得到通知。

请求从 src 属性设置的那一刻开始,这个例子在请求中发送了一个 name 参数。

图像 Ping 最常用于跟踪用户点击。图像 Ping 有两个主要的缺点:一是只能发送 GET 请求,二是无法访问服务器的响应文本。因此,图像 Ping 只能用于浏览器与服务器的单向通信。

JSONP

JSONP 是 JSON with padding(填充式 JSON 或参数式 JSON) 的简写,是一种应用 JSON 的新方法。JSONP 看起来和 JSON 差不多,只不过是被包含在函数调用中的 JSON。例如像这样:

callback({"name": "Nicholas"})

JSONP 由两个部分组成:回调函数数据

  • 回调函数:当响应到来时因该在页面中调用的函数,函数的名字一般在请求中指定。
  • 数据:就是传入回调函数中的 JSON 数据。

JSONP 是通过动态 <script> 元素来使用的,使用时可以为 src 属性制定一个跨域 URL。这里的 <script> 元素和 <img> 元素类似,都有能力不受限制从其他域中加载资源。因为 JSONP 是有效的 JavaScript 代码,所以在请求完成后,即在 JSONP 响应加载到页面中以后,就会立即执行。

来看一个例子:

function handleResponse(response) {
  console.log(response.name);
}
var script = document.createElement("script");
// 设置为跨域的 URL,并指定回调函数为 handleResponse
script.src = "http://freegeoip.net/json/?callback=handleResponse";
document.body.insertBefore(script);

因为指定了回调函数,所以返回的响应可以根据它来构造,将 JSON 数据包含到函数调用中,当请求完成后,立即执行,完成了请求。例如返回下面的代码:

handleResponse({"name": "foo"});

JSONP 之所以非常流行,是因为它简单易用。和图像 Ping 相比,优点在于能够直接访问页面的响应文本。不过 JSONP 也有两点不足:

  • 首先,JSONP 从其他域中加载代码执行。如果其他域不安全,很可能会在响应中夹带一些恶意代码,而此时除了完全放弃 JSONP 调用之外,没有办法追究。因此使用的不是自己运维的 Web 服务时,一定要保证它的安全。
  • 其次,要确定 JSONP 请求是否失败并不容易。虽然 HTML5 给 <script> 元素新增 onerror 事件处理程序,但目前还没有得到任何浏览器支持。为此,我们可能不得不使用定时器来检测指定时间内是否接收到了响应。

代理

代理服务器解决跨域的思路,是利用代理服务器对浏览器页面的请求进行转发,因为同源策略的限制只存在在浏览器中,到了服务器端就没有这个限制了。

相关配置再补充吧 =_=

  • node 代理跨域
  • nginx 代理

参考

exec、test 和 match

exec、test 和 match

我们知道 exectest 都是 RegExp 实例的方法,看看如何使用它们,另外 execmatch 有什么区别?

exec

exec() 专门为捕获组而设计的。

exec 接受一个参数,即要应用模式的字符串,然后返回包含第一个匹配项信息的数组,没有任何匹配项时返回 null

返回的虽然是个数组,但是包含两个额外的属性:indexinput

  1. index:表示匹配项在字符串中的位置。
  2. input:表示正在应用正则表达式的字符串。

在数组中,第一项是整个模式匹配的字符串,其他项是与模式中的捕获组匹配的字符串(如果没有捕获组,则数组只包含一项)

var str = 'mom and dad and bady';
var pattern = /mom( and dad( and bady)?)?/gi;

var result = pattern.exec(str);

console.log(result.index) // 0
console.log(result.input) // "mom and dad and bady"
console.log(result[0])    // "mom and dad and bady"
console.log(result[1])    // " and dad and bady"
console.log(result[2])    // " and bady"

注意对于 exec 而言,即使子啊模式中设置了全局标志 g,它每次只返回一个匹配项,每次调用 exec 都会在字符串中继续查找新的匹配项,直至字符串的末尾。在不设置全局标志的情况下,在同一个字符串上多次调用 exec 始终返回第一个匹配项的信息。

var str = "cat,bat,eat"
var pattern1 = /.at/

var result = pattern1.exec(str);
console.log(result.index)       // 0
console.log(result[0])          // cat
console.log(pattern1.lastIndex) // 0

var result = pattern1.exec(str);
console.log(result.index)       // 0
console.log(result[0])          // cat
console.log(pattern1.lastIndex) // 0

var str = "cat,bat,eat"
var pattern2 = /.at/g

var result = pattern2.exec(str);
console.log(result.index)       // 0
console.log(result[0])          // cat
console.log(pattern2.lastIndex) // 3

var result = pattern2.exec(str);
console.log(result.index)       // 4
console.log(result[0])          // bat
console.log(pattern2.lastIndex) // 7

test

test 方法接受一个字符串参数,在模式与该参数匹配的情况下返回 true;否则返回 false

如果只想知道目标字符串与某个模式是否皮撇,但是不需要知道其文本内容的情况下,使用这个方法很方便。因此 test 常用来做判断。

var str = "0933-2331-9732"
var pattern = /\d{4}-\d{4}-\d{4}/

if (pattern.test(str)) {
  console.log('号码格式正确');
}

exec 和 match 的区别

说到 exec() 很容易就想到字符串的一个方法 match(),它们之间有什么区别呢?

var str = "cat,bat,eat"
var pattern = /.at/

pattern.exec(str)
str.match(pattern)

它们的区别有 2 点:首先这两个方法属于不同的类,另外重要的一点是跟 g 有关

  • 没有 g 的情况下,它们返回的结果是一致的
  • 设置了 g 之后,exec 只返回第一个匹配项,而 match 会所有匹配项组成的一个数组,同时,返回的数组不再带有 indexinput 属性。
var str = "cat,bat,eat"
var pattern = /.at/

pattern.exec(str)  // ['cat']
str.match(pattern) // ['cat']

javascript-regexp-exec1

var str = "cat,bat,eat"
var pattern = /.at/g

pattern.exec(str)  // ['cat']
str.match(pattern) // ['cat','bat,'eat']

javascript-regexp-exec2

浏览器页面渲染机制

浏览器的内核是指支持浏览器运行的最核心的程序,分为两个部分的,一是渲染引擎,另一个是 JS 引擎。目前市面上常见的浏览器内核可以分为这四种:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)。这里面大家最耳熟能详的可能就是 Webkit 内核了。

页面加载过程

页面的加载过程,主要有五个步骤:

  • DNS 查询
  • TCP 连接
  • HTTP 请求即响应
  • 服务器响应
  • 客户端渲染

浏览器得到的其实就是一堆 HMTL 格式的字符串,因为只有 HTML 格式浏览器才能正确解析,这是 W3C 标准的要求。最后将它们渲染出来。

浏览器的渲染过程

把 HTML、CSS、JavaScript 等数据作为输入,经过渲染模块的处理,最终输出为屏幕上的像素。

渲染模块在执行过程中会分为很多子节点,接下来看看:

构建 DOM 树

无法直接理解和使用 HTML,所以需要将 HTML 转换为浏览器能够理解的结构——DOM 树。

javascript-browser-render-eg1

样式计算(Recalculate Style)

计算出 DOM 节点中每个元素的具体样式,这个阶段大体可分为三步来完成:

  1. 把 CSS 转换为浏览器能够理解的结构,会把获取到的 CSS 文本都转成 styleSheets,可以在控制台输入 document.styleSheets 查看。

javascript-browser-render-eg2

  1. 转换样式表中的属性值,使其标准化:
body { font-size: 20px }
p {color:blue;}
span  {display: none}

/* 标准化为 */
body { font-size: 20px }
p {color:rgb(0, 128, 0);}
span  {display: none}
  1. 计算出 DOM 树中每个节点的具体样式
body { font-size: 20px }
p {color:blue;}
span  {display: none}
div {font-weight: bold;color:red}
div  p {color:green;}
<body>
  <p><span>重点介绍</span>渲染流程</p>
  <div>
    <p>green</p>
    <div>red</div>
  </div>
</body>

那就就会得到这样的树:

javascript-browser-render-eg3

布局阶段

有 DOM 树和 DOM 树中元素的样式,接下来就要计算可见元素的位置,这就是布局。

  1. 创建布局树,构建一棵只包含可见元素的布局树。

javascript-browser-render-eg4

  1. 布局计算,计算布局树中节点的坐标位置,并将计算结果写回到布局树中。

分层

有了布局树,接下来并不是直接绘制,而是需要先分层。因为页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等。为了显示这些效果,需要为特定节点生成专用图层,并生成一棵对应的图层树(Layer Tree)。就类似于 PS 中的图层。

javascript-browser-render-eg5

图层绘制

在完成图层树的构建之后,渲染引擎会对图层树中的每个图层进行绘制。

栅格化(raster)

当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程。

用户最先看到的页面是视口(viewport)部分,把图层分成块,合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。

所谓栅格化,是指将图块转换为位图。

通常栅格化的过程还需要 GPU 加速生成,使用 GPU 生成位图的过程叫做快速栅格化或者 GPU 栅格化。GPU 生成的位图块保存在 GPU 的内存中。

合成和显示

所有图块都被光栅化,合成线程就会生成一个绘制图块的命令 DrawQuad,然后将该命令提交给浏览器进程。浏览器进程根据 DrawQuad 命令,将页面内容绘制到内存中,最后将内容显示到屏幕上。

这就是整个完整的流程,从 HTML 到 DOM、样式计算、布局、图层、图层绘制、栅格化、合成和显示。

javascript-browser-render-eg6

最后再看看重排和重绘

重排(reflow)和重绘(repaint)对性能优化有很多的影响,这里理解它们的影响范围,更详细的可以看浏览器的回流和重绘

重排 - 更新了元素的几何属性

通过 JavaScript 或者 CSS 修改元素的几何位置属性,例如改变元素的宽度、高度等,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排。

javascript-browser-render-eg7

无疑,重排需要更新完整的渲染流水线,所以开销也是最大的。

重绘 - 更新了元素的绘制属性

比如通过 JavaScript 更改某些元素的背景颜色,那么布局阶段将不会被执行,因为并没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个过程就叫重绘。

javascript-browser-render-eg8

相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。

参考

了解前端模块化

作为一个前端工程师,当我们需要用到类似 ElementUI , lodash 之类的库的时候,只需要执行 npm install,然后引入就可以开始愉快的写代码了。它们都极大地提高了我们的工作效率,但是这一切是从什么开始的呢?

这些都要从 Modular design (模块化设计) 说起。

说到模块化,我们经常能关联出以下这些熟悉的名词,当然有一些是比较老的方式了,你甚至没有用过。什么原因导致了区别于旧规范而产生出来的新的规范?也许我们可以从它们之间的区别,或者说改变中体会到它们的新意味着什么。

  • IIFE [Immediately Invoked Function Expression]
  • Common.js
  • AMD
  • CMD
  • ES6 Module

IIFE

IIFEImmediately Invoked Function Expression(立即调用函数表达式) 的缩写。它是一个在定义时就会立即执行的 JavaScript 函数。

(function () {
    statements
})();

这是一个被称为自执行匿名函数的设计模式,主要包含两部分。

  1. 第一部分是包围在 圆括号运算符 () 里的一个匿名函数,这个匿名函数拥有独立的词法作用域。这不仅避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域。
  2. 第二部分再一次使用 () 创建了一个立即执行函数表达式,JavaScript 引擎到此将直接执行函数

最开始,我们对于模块化的概念,是从文件开始区分的。在一个简易的项目中,我们的编程习惯是通过一个 HTML 文件加上若干个 JavaScript 文件来区分不同模块的,就像下面这样:

|--index.html
|--footer.js
|--header.js
|--main.js

然后简单的看看里面的内容

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>index</title>
  <script src="header.js"></script>
  <script src="main.js"></script>
  <script src="footer.js"></script>
</head>
<body>
  
</body>
</html>

其他三个 JavaScript 文件

在不同的 js 文件中我们定义不同的变量

// header.js
var header = '这是头部';

// main.js
var main = '这是内容';

// footer.js
var footer = '这是底部';

像这样通过不同文件来声明变量的方式,实际上没有做到将变量区分开来。因为它们都绑定到了全局 window 对象上,我们尝试将它们在控制台中输出验证一下:

window.header
// "这是头部"
window.main
// "这是内容"
window.footer
// "这是底部"

这简直是异常噩梦,你可能还没有意识到这会导致什么严重的后果。现在我们试着改一下 footer.js,给 header 变量进行赋值:

// footer.js
var footer = '这是底部';
header = '头部改变了';

然后再将 window.header 打印出来,它已经被改变了:

window.header
// "头部改变了"

想想这是多么可怕,因为我们根本无法知道和预料在什么时候什么地方,某个之前定义的变量被改变了。

也就是说简单的通过文件是不能将变量区分的。

那么,重要的是我们应该怎么解决这问题?我们都知道,JavaScript 具有函数作用域的概念,也就是说,我们可以使用一个函数将这些变量包裹起来,那么这些变量就不会直接被声明到 window 对象上了:

现在我们把 header.js 修改成:

function createHeader() {
  var header = '这是头部';
}

createHeader();

现在我们在 window 里面找不到 header,因为它们被隐藏在了 createHeader 中,但是 createHeader 仍旧污染了我们的 window

window.header
// undefined
window.createHeader
// ƒ createHeader() {
//   var header = '这是头部';
// }

也就是说这个方案并不是很完美,怎么改进呢?

答案就是 IIFE,我们可以定义一个立即执行的匿名函数来解决这个问题:

(function() {
  var header = '这是头部';
})()

因为是一个匿名的函数,执行完后很快就会被释放,这种机制不会污染全局对象。

虽然看起来有些麻烦,但它确实解决了我们将变量分离开来的需求,不是吗?然而在今天,几乎没有人会用这样方式来实现模块化编程。

后来又发生了什么呢?

CommonJS

CommonJS 是以在浏览器环境之外构建 JavaScript 生态系统为目标而产生的项目,比如在服务器和桌面环境中。

这个项目最开始是由 Mozilla 的工程师 Kevin Dangoor 在2009年1月创建的,当时的名字是 ServerJS。

我在这里描述的并不是一个技术问题,而是一件重大的事情,让大家走到一起来做决定,迈出第一步,来建立一个更大更酷的东西。 —— Kevin Dangoor's What Server Side JavaScript needs

2009年8月,这个项目改名为 CommonJS,以显示其 API 的更广泛实用性。CommonJS 是一套规范,它的创建和核准是开放的。这个规范已经有很多版本和具体实现。CommonJS 并不是属于 ECMAScript TC39 小组的工作,但 TC39 中的一些成员参与 CommonJS 的制定。

CommonJS 规范是为了解决 JavaScript 的作用域问题而定义的模块形式,可以使每个模块它自身的命名空间中执行。该规范的主要内容是,模块必须通过 module.exports 导出对外的变量或接口,通过 require() 来导入其他模块的输出到当前模块作用域中。

一个简单的例子:

// moduleA.js
module.exports = function( value ){
    return value * 2;
}
// moduleB.js
var multiplyBy2 = require('./moduleA');
var result = multiplyBy2(4);

值得注意的是,这里所说的 CommonJS 是一套通用的规范,与之对应的有非常多不同的实现。

这里,我们关注 Node.js 的实现。

Node.js Modules

Node 模块采用 CommonJS 模块规范。

每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类都是私有的,对其他文件不可见。

// example.js
var x = 5;
var addX = function (value) {
  return value + x;
};
module.exports.x = x;
module.exports.addX = addX;

// main.js
var example = require('./example.js');
console.log(example.x); // 5
console.log(example.addX(1)); // 6

上面代码中,变量 x 和函数 addX,是当前文件 example.js 私有的,其他文件不可见。如果想在多个文件分享变量,必须定义为 global 对象的属性。

global.warning = true;

上面定义的 warning 变量,可以被所有文件读取。当然,这样写法是不推荐的。

根据 CommonJS 的规定,在每个模块内部:

  • module 变量代表当前模块,这个变量是一个对象。
  • exportsmodule.exports 属性是对外的接口,加载某个模块,其实就是加载该模块的 module.exports 属性。
  • require 方法用于加载模块。

CommonJS 模块具有以下特点

  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
  • 模块加载的顺序,按照其在代码中出现的顺序。
  • CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。

module 对象

Node 内部提供一个 Module 构建函数。所有模块都是 Module 的实例。

function Module(id, parent) {
  this.id = id;
  this.exports = {};
  this.parent = parent;
  // ...

每个模块内部,都有一个 module 对象,代表当前模块。它有以下属性。

  • module.id 模块的识别符,通常是带有绝对路径的模块文件名。
  • module.filename 模块的文件名,带有绝对路径。
  • module.loaded 返回一个布尔值,表示模块是否已经完成加载。
  • module.parent 返回一个对象,表示调用该模块的模块。
  • module.children 返回一个数组,表示该模块要用到的其他模块。
  • module.exports 表示模块对外输出的值。

module.exports 属性

module.exports 属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports 变量。

exports 变量

为了方便,Node 为每个模块提供一个 exports 变量,指向 module.exports。这等同在每个模块头部,有一行这样的命令。

var exports = module.exports;

造成的结果是,在对外输出模块接口时,可以向exports对象添加方法。

exports.area = function (r) {
  return Math.PI * r * r;
};

exports.circumference = function (r) {
  return 2 * Math.PI * r;
};

值得注意的是不能直接将 exports 变量指向一个值,因为这样等于切断了exportsmodule.exports 的联系。

下面两种写法都是无效的:

// 无效,因为 exports 不再指向 module.exports 了
exports = function(x) {console.log(x)}; 
// 无效, hello 函数是无法对外输出的,因为module.exports被重新赋值了。
exports.hello = function() {
  return 'hello';
};
module.exports = 'Hello world';

require

require 命令的基本功能是,读入并执行一个 JavaScript 文件,然后返回该模块的 exports 对象。如果没有发现指定模块,会报错。

// example.js
var invisible = function () {
  console.log("invisible");
}

exports.message = "hi";
exports.say = function () {
  console.log(message);
}

运行下面的命令,可以输出exports对象。

var example = require('./example.js');
example
// {
//   message: "hi",
//   say: [Function]
// }

require 是怎么实现的?这样的方式有什么弊端?

每个模块实例都有一个 require 方法。

Module.prototype.require = function(path) {
  return Module._load(path, this);
};

由此可知,require 并不是全局性命令,而是每个模块提供的一个内部方法,也就是说,只有在模块内部才能使用 require 命令。另外,require 其实内部调用 Module._load 方法。

Module._load = function(request, parent, isMain) {
  // 1. 检查 Module._cache,是否缓存之中有指定模块
  // 2. 如果缓存之中没有,就创建一个新的 Module 实例
  // 3. 将它保存到缓存
  // 4. 使用 module.load() 加载指定的模块文件,
  //    读取文件内容之后,使用 module.compile() 执行文件代码
  // 5. 如果加载/解析过程报错,就从缓存删除该模块
  // 6. 返回该模块的 module.exports
};

上面的第4步,采用module.compile()执行指定模块的脚本:

Module.prototype._compile = function(content, filename) {
  var self = this;
  var args = [self.exports, require, self, filename, dirname];
  return compiledWrapper.apply(self.exports, args);
};

上面的代码基本等同于下面的形式:

(function (exports, require, module, __filename, __dirname) {
  // 模块源码
});

也就是说,模块的加载实质上就是,注入 exports、require、module 三个全局变量,然后执行模块的源码,然后将模块的 module.exports 变量的值输出。

Module._compile 方法是同步执行的,所以 Module._load 要等它执行完成,才会向用户返回module.exports 的值。

看一下 require 的简易实现,

function require(/* ... */) {
  const module = { exports: {} };
  ((module, exports) => {
    // 模块代码在这。在这个例子中,定义了一个函数。
    function someFunc() {}
    exports = someFunc;
    // 此时,exports 不再是一个 module.exports 的快捷方式,
    // 且这个模块依然导出一个空的默认对象。
    module.exports = someFunc;
    // 此时,该模块导出 someFunc,而不是默认对象。
  })(module, module.exports);
  return module.exports;
}

回答刚才的问题:

  • require 是怎么实现的?

执行 require 的时候,创建一个 module 实例,将它注入并执行模块源码,最后将 module.exports 返回。也就是将被引用的 module 拷贝一份到当前 module 中。

  • 这样的方式有什么弊端?

CommonJS 这一标准的初衷是为了让 JavaScript 在多个环境下都实现模块化,但是 Node.js 中的实现依赖了 Node.js 的环境变量:moduleexportsrequireglobal,浏览器没法用啊,所以后来出现了 Browserify 这样的实现。

说完了服务端的模块化,接下来我们聊聊,在浏览器这一端的模块化,又经历了些什么呢?

RequireJS & AMD(Asynchronous Module Definition)

在浏览器环境下,如果也使用 CommonJS,会存在什么问题呢?上面说到 CommonJS 规范加载模块是同步的。在 require() 的实现中,你已经发现这其实是一个复制的过程,将被 require 的内容,赋值到一个 module 对象的属性上,然后返回这个对象的 exports 属性。

由于 Node.js 主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以 CommonJS 规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块
,如果还是使用 CommonJS,则可能导致阻塞,使得我们后面的步骤无法进行。

所以在浏览器环境下,模块化必须使用异步的方式。

在这样的背景下,RequireJS 出现了。

RequireJS 是一个工具库,主要用于客户端的模块管理。它可以让客户端的代码分成一个个模块,实现异步或动态加载,从而提高代码的性能和可维护性。它的模块管理遵守 AMD 规范(Asynchronous Module Definition)。

RequireJS 就是为了解决这两个问题:

  1. 实现js文件的异步加载,避免网页失去响应;
  2. 管理模块之间的依赖性,便于代码的编写和维护。

RequireJS 的基本**是,通过 define 方法,将代码定义为模块;通过 require 方法,实现代码的模块加载。

require.js 的加载

使用 require.js 的第一步,是先去官方网站下载最新版本。下载后,假定把它放在js子目录下面,就可以加载了:

<script src="js/require.js" defer async="true" ></script>

加载 require.js 以后,下一步就要加载我们自己的代码了。假定我们自己的代码文件是 main.js,也放在js目录下面。那么,只需要写成下面这样就行了:

<script src="js/require.js" data-main="js/main"></script>

data-main 属性的作用是,指定网页程序的主模块。在上例中,就是 js 目录下面的 main.js,这个文件会第一个被 require.js 加载。由于 require.js 默认的文件后缀名是 .js,所以可以把main.js 简写成 main

下面来看看 main.js 的内容。

如果我们的代码不依赖任何其他模块,那么可以直接写入javascript代码。

// main.js
alert('这是AMD');

但这样的话,就没必要使用 require.js 了。真正常见的情况是,主模块依赖于其他模块,这时就要使用 AMD 规范定义的的 require() 函数。

// main.js
require(['moduleA', 'moduleB', 'moduleC'], function (moduleA, moduleB, moduleC){
 // some code here
});

require()函数接受两个参数:

  1. 第一个参数是一个数组,表示所依赖的模块,上例就是['moduleA', 'moduleB', 'moduleC'],即主模块依赖这三个模块;
  2. 二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用。加载的模块会以参数形式传入该函数,从而在回调函数内部就可以使用这些模块。

下面,我们看一个实际的例子。

假定主模块依赖 jquery、underscore 这两个模块,则 main.js 可以这样写:

require(['jquery', 'underscore'], function ($, _){
 // some code here
});

require.config()

上一节最后的示例中,主模块的依赖模块是['jquery', 'underscore']。默认情况下,require.js 假定这三个模块与 main.js 在同一个目录,文件名分别为 jquery.jsunderscore.js,然后自动加载。

使用 require.config() 方法,我们可以对模块的加载行为进行自定义。require.config() 就写在主模块(main.js)的头部。参数就是一个对象,这个对象的 paths 属性指定各个模块的加载路径。

require.config({
  paths: {
    "jquery": "jquery.min",
    "underscore": "underscore.min"
  }
})

上面的代码给出了三个模块的文件名,路径默认与 main.js 在同一个目录(js子目录)。如果这些模块在其他目录,比如 js/lib 目录,则有两种写法。一种是逐一指定路径。另一种则是直接改变基目录(baseUrl)。

// 写法1
require.config({
 paths: {
  "jquery": "lib/jquery.min",
  "underscore": "lib/underscore.min"
 }
});

// 写法2
require.config({
  baseUrl: "js/lib",
 paths: {
  "jquery": "jquery.min",
  "underscore": "underscore.min"
 }
});

如果某个模块在另一台主机上,也可以直接指定它的网址,例如:

require.config({
 paths: {
  "jquery": "https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min"
 }
});

define 定义模块

模块必须采用特定的define()函数来定义。如果一个模块不依赖其他模块,那么可以直接定义在define()函数之中。

// math.js
define(function (){
 var add = function (x,y){
  return x+y;
  };
 return {
  add: add
 };
});

// 加载方法如下
// main.js
require(['math'], function (math){
 alert(math.add(1,1));
});

如果这个模块还依赖其他模块,那么define()函数的第一个参数,必须是一个数组,指明该模块的依赖性。

define(['myLib'], function(myLib){
 function foo(){
  myLib.doSomething();
 }
 return {
  foo : foo
 };
});

当require()函数加载上面这个模块的时候,就会先加载myLib.js文件。

通过上面的语法说明,我们会发现一个很明显的问题,在使用 RequireJS 声明一个模块时,必须指定所有的依赖项 ,这些依赖项会被当做形参传到 factory 中,对于依赖的模块会提前执行(在 RequireJS 2.0 也可以选择延迟执行),这被称为:依赖前置。

这会带来什么问题呢?

加大了开发过程中的难度,无论是阅读之前的代码还是编写新的内容,也会出现这样的情况:引入的另一个模块中的内容是条件性执行的。

SeaJS & CMD(Common Module Definition)

针对 AMD 规范中可以优化的部分,CMD 规范出现了,而 SeaJS 则是它的具体实现之一,与 AMD 十分相似。

CMD 规范的前身是Modules/Wrappings规范。

SeaJS 更多地来自 Modules/2.0 的观点,同时借鉴了 RequireJS 的不少东西,比如将 Modules/Wrappings 规范里的 module.declare 改为 define 等。SeaJS 遵循的CMD(Common Module Definition)。

定义模块

在 CMD 规范中,一个模块就是一个文件。代码的书写格式如下:

define(factory)
  • factory 为对象、字符串时,表示模块的接口就是该对象、字符串。
  • factory 为函数时,表示是模块的构造方法。执行该构造方法,可以得到模块向外提供的接口。factory默认会传入三个参数:require、exports 和 module。
// factory 为对象
define({ "foo": "bar" });

// factory 为函数
define(function(require, exports, module) {
  // 模块代码
});

factory 的参数使用:

// 所有模块都通过 define 来定义
define(function(require, exports, module) {
  // 通过 require 引入依赖,获取模块 a 的接口
  var a = require('./a');

  // 调用模块 a 的方法
  a.doSomething();

  // 通过 exports 对外提供接口foo 属性
  exports.foo = 'bar';

  // 对外提供 doSomething 方法
  exports.doSomething = function() {};

  // 错误用法!!!
  exports = {
    foo: 'bar',
    doSomething: function() {}
  };

  // 正确写法,通过module.exports提供整个接口
  module.exports = {
    foo: 'bar',
    doSomething: function() {}
  };
});

与 AMD 的主要区别

// AMD 的一个例子,当然这是一种极端的情况
define(["header", "main", "footer"], function(header, main, footer) { 
    if (xxx) {
      header.setHeader('new-title')
    }
    if (xxx) {
      main.setMain('new-content')
    }
    if (xxx) {
      footer.setFooter('new-footer')
    }
});

 // 与之对应的 CMD 的写法
define(function(require, exports, module) {
    if (xxx) {
      var header = require('./header')
      header.setHeader('new-title')
    }
    if (xxx) {
      var main = require('./main')
      main.setMain('new-content')
    }
    if (xxx) {
      var footer = require('./footer')
      footer.setFooter('new-footer')
    }
});

我们可以很清楚的看到,CMD 规范中,只有当我们用到了某个外部模块的时候,它才会去引入,这回答了我们上一小节中遗留的问题,这也是它与 AMD 规范最大的不同点:CMD 推崇依赖就近 + 延迟执行

我们能够看到,按照 CMD 规范的依赖就近的规则定义一个模块,会导致模块的加载逻辑偏重,有时你并不知道当前模块具体依赖了哪些模块或者说这样的依赖关系并不直观。按需执行依赖虽然避免浪费,但是require 时才解析的行为对性能有影响。

而且对于 AMD 和 CMD 来说,都只是适用于浏览器端的规范,而 Node.js module 仅仅适用于服务端,都有各自的局限性。

ECMAScript6 Module

ECMAScript6 标准增加了 JavaScript 语言层面的模块体系定义,作为浏览器和服务器通用的模块解决方案它可以取代我们之前提到的 AMDCMD , CommonJS

它凭借什么做到这一点呢?

  • 与 CommonJS 一样,具有紧凑的语法,对循环依赖以及单个 exports 的支持。
  • 与 AMD 一样,直接支持异步加载和可配置模块加载。

除此之外,它还有更多的优势:

  • 语法比 CommonJS 更紧凑。
  • 结构可以静态分析(用于静态检查,优化等)。
  • 对循环依赖的支持比 CommonJS 好。

如果你想搞清楚 ES6 Module,理解它的设计目标是很有帮助的,它的主要目标是:

  • 默认导出是被推荐的
  • 静态模块结构
  • 支持同步和异步加载
  • 支持模块之间的循环依赖关系

参考资料

如何思考新的产品想法

产品经理是做什么的?如何去做产品?

产品经理

产品经理主要做两件事:

  1. 根据用户需求,定义产品,争取资源
    有什么新的产品想法,然后把这种把这种想法表达出来,得到大家的支持(领导、合作伙伴等),然后拿到钱(资源)去最求这个想法。

  2. 和技术团队一起实现和改进产品
    和技术团队合作,把这个产品想法,或者已有的产品做得更好。

这就是产品经理做的两件事情。接下来讨论这些事情怎么做。

如何思考新的产品

对于一个产品来说,产品经理需要回答三个问题

  1. 顾客,定义顾客群体以及顾客痛点
    这个产品的顾客是谁,以及这些顾客有什么痛点。

  2. 产品,定义产品,解决客户痛点
    你这个产品到底是什么,以及如何清晰的解决客户痛点。

  3. 产品差异化,挖掘产品特点
    你的产品和市面上的产品有什么不同,顾客为什么选择你的产品,而不是选择市面上其他的产品。

接下来通过一些例子来理解产品经理的思维模式,以及需要做一些什么样的研究,然后把一个新的产品推荐给领导或潜在的投资人。

定义用户及用户痛点

比如说 Uber,当时 Uber 刚发布的时候解决什么用户问题?什么样的人是他的顾客?

最开始,在北美打车存在几个痛点:

  • 要打电话预约。比如外面下雨想打车回家,需要先给出租车公司打电话,然后他们立马给你预约一辆车到你前面。
  • 等待时间长。这个等待的过程可能就要 10-20 分钟
  • 价格贵。出租车人工成本高
  • 入行成本高。成为出租车司机你还需要去考相关的证件。

用户 1:乘客,用户 2:司机。

正是因为这些限制,导致顾客在约出租车司机、打车的费用上有很大的痛点。当顾客想从一个地方去另一个地方的时候,面临很高的出租车费用和等待时间成本。

根据用户痛点 定义产品

接下来讨论一下产品,这个时候我们就从产品经理的角度去看产品怎么做,产品怎么样一一对应的去解决客户的问题。

Uber 当时做了一些假设,就是当这个人想约出租车司机的时候,旁边会有一个私家车司机,他们那其实要去同样的地方,但是他们不知道对方的存在,所以他们不能说我们一起坐车去那个地方对不对。

他们做了这样的假设,但是其实解决这个问题的方法有很多种,这个时候就说到思维模式

就是说我们如何帮这个顾客和另外一个想去一样地方的出租车司机联系起来,然后一起去同一个地方。

或者他还可以说,自己开一个出租车公司,然后在晚上大家吃完饭要打车等场景不断去优化流程,然后自己再买一些出租车,自己去做。这也是一个方法。

所以有很多种方法。

怎么样从顾客的痛点到解决方案,这中间其实有一个桥梁,这就是思维模式

作为产品经理,要在表达这种思维模式上有很强的技能,这样你的领导才能审视你这个思维模式,理解你的思维模式是不是有道理的。他明白了你的思维模式后,才能理解你的产品。

如果你只是说,这是问题,然后这是产品。那么哪怕你的想法再好,哪怕想法再正确,你的领导也很难理解这个产品是什么东西。

在产品经理这个角色中 mental model 是一个很重要的部分。

对比同类产品找到产品特色

在我们讨论顾客以及他们的痛点,也讨论了产品的思维模式以及这个产品是怎么解决顾客的痛点的前提下,我们还需要知道怎么去探究这个产品的成功性。

大家会不会用这个产品?

想回答大家会不会用我们的产品,就要了解市面上有没有类似的产品,以及说,我们这个产品跟市面上类似的产品会有什么样的不同(在功能上)。这就是你的 Differentiating Factor

  • 远程交通

当时已经有 Lyft 存在了,但是它解决的事远程顾客和远程司机的问题。例如从西雅图 —— 波特兰,left 联系的是这样的顾客。但是在短途旅程中,比如在市内,其实是没有一个非常好的解决方案。

  • 短途通行

这个时候 Uber 就进入这个领域,帮助短途顾客连接短途私人司机就是一个非常非常新的想法。它围绕这个想法做的功能,就成为它的
Differentiating Factor。这就是为什么顾客选择他,而不是 Lyft 的原因。

回到现在 2023,假设我们想做一款跟 Uber 类似的产品放到市场上,怎样才能把我们的产品做的和 Uber 不一样呢?

我们可以考虑几点

  • 你可以选择不同的代步工具:比如摩托车
  • 然后可以选不同人群:女性可以选择女司机
  • 其次选择话题:例如可以根据需求选择不同背景的司机,例如可以选择一个有互联网背景的司机,这样坐车的时候可以聊天。

当我们想到这一系列的功能,我们的思维模式是什么?是怎么想去想这些东西的?怎么找到这些想法的?

基本上会从自己的需求,以及想到身边人可能也有类似的需求,并且结合我们对 Uber 这个产品的理解上,然后想出我们可以做一些什么事情。这就是 Differentiating Factor。

然后呢,假如我们那做出这样一个产品,然后这个产品和 Uber 的唯一区别就是可以选择话题。

这就到产品经常说的两个概念:

  1. Pain Killer:止疼药,你这个新的产品是可以像止疼药一样把你身上那个疼止住的。(解决用户痛点)
  2. Vitamin:维他命,你这个新的产品,实际上像维他命一样,吃到身体里,确实感觉不到他有多好,但是长年累月,你可以能感受到一点点变化。(改善用户体验)

这就是一个新的产品,它是 Pain Killer 还是 Vitamin 的一个区别。

所以我们前面的这些想法,其实是一个 Vitamin,主要是改善用户体验。

另外我们还可以从 Uber 的角度去看待这个问题,什么事情会让 Uber 不想去做这个功能?如果你发想 Uber 是很容易就可以做出这个功能的话,那其实我们在 offer 的这个新产品,有这个新功能,也不能有效阻止 Uber 进入这个新的 market。

所以说 Differentiating Factor 很重要,它直接关系到这个顾客会用你的产品还是现有的产品。一般大家会说:

  1. 产品有什么样的特色
  2. 产品的特色能改善用户体验(比现在的产品好在哪)
  3. 利用产品已有的资源来增强竞争力。例如如果我们去做一个货物配送,那么 Uber 或者一个随机的 Startup 公司哪个会容易成功?Uber 已经有自己的司机群体,这样转化到这个产品上,把东西送到顾客手里的时间就可以更短。

参考

Blog 升级 👨🏻‍💻⛽️

最近看 antfu.me 感觉体验很好,接下做一下琢磨一下对 blog 进行升级,提供更完善和友好的体验,主要包括:

  • 框架升级 Vue 3 + Vite:当前 blog 只是简单的通过接口获取数据之后做个渲染,SEO 效果并不好,另外要做功能扩展的时候成本比较高。 Vue 3 也带来更好的性能,同时也是一个新的尝试。
  • 更好的阅读体验:当前 blog 缺少更好的代码展示效果和目录引导不利于阅读。
  • 移动端适配: 手机端也能用。
  • 暗黑模式:跟随系统或者自由切换。

当然还是会通过 issue 这种方式来记录博客,相对方便一点,也可以使用评论功能进行讨论。

具体方案完成之后回来补充...

Infinite-scroll 无限滚动

InfiniteScroll 无限滚动,也就是滚动到底部时,加载更多的数据。

实现原理

无限滚动的原理就是,用户滚动,当滚动条到底的时候就加载。所以很关键的一点是要知道滚动条和底部的距离。

先了解三个概念:

  • scrollHeight:只读属性,是一个元素内容高度的度量,包括由于溢出导致的视图中不可见内容。
  • scrollTop:可以获取或设置一个元素的内容垂直滚动的像素数。
  • clientHeight:只读属性,对于没有定义CSS或者内联布局盒子的元素为0,否则,它是元素内部的高度(单位像素),包含内边距,但不包括水平滚动条、边框和外边距。

javascript-InfiniteScroll-eg1

通过计算就可以的得到滚动条到底部的距离:

distance = Element.scrollHeight - Element.scrollTop - Element.clientHeight

element 的 InfiniteScroll 无限滚动

可以试一下如何使用 InfiniteScroll,然后 element 的源码是怎么实现的:

export default {
  name: 'InfiniteScroll',
  inserted(el, binding, vnode) {
    const cb = binding.value;

    const vm = vnode.context;
    // only include vertical scroll
    const container = getScrollContainer(el, true);
    const { delay, immediate } = getScrollOptions(el, vm);
    const onScroll = throttle(delay, handleScroll.bind(el, cb));

    el[scope] = { el, vm, container, onScroll };

    if (container) {
      container.addEventListener('scroll', onScroll);

      if (immediate) {
        const observer = el[scope].observer = new MutationObserver(onScroll);
        observer.observe(container, { childList: true, subtree: true });
        onScroll();
      }
    }
  },
  unbind(el) {
    const { container, onScroll } = el[scope];
    if (container) {
      container.removeEventListener('scroll', onScroll);
    }
  }
};

通过指令的方式来实现的,inserted 钩子在被绑定元素插入父节点时调用,首先通过 const container = getScrollContainer(el, true); 获取滚动的元素,这个方法定义在 element-ui/src/utils/dom

export const isScroll = (el, vertical) => {
  if (isServer) return;

  const determinedDirection = vertical !== null || vertical !== undefined;
  const overflow = determinedDirection
    ? vertical
      ? getStyle(el, 'overflow-y')
      : getStyle(el, 'overflow-x')
    : getStyle(el, 'overflow');

  return overflow.match(/(scroll|auto)/);
};

export const getScrollContainer = (el, vertical) => {
  if (isServer) return;

  let parent = el;
  while (parent) {
    if ([window, document, document.documentElement].includes(parent)) {
      return window;
    }
    if (isScroll(parent, vertical)) {
      return parent;
    }
    parent = parent.parentNode;
  }

  return parent;
};

从该元素开始循环判断父元素是否定义了 overflow 样式,来确定滚动容器 container

接下来就会监听滚动容器的滚动事件。滚动的时候就执行 onScroll 方法,为了优化性能,这个方法是节流函数 throttle 执行之后返回的,所以实际上执行的是 handleScroll。这个等会再分析。

先看下面的代码对 immediate 的判断,如果是 true,即立即执行,则创建一个 new MutationObserver(onScroll) 实例,并监听容器,内容改变之后会执行回调函数 onScroll

为什么这么处理呢?

因为无限滚动是通过滚动加载实现的,如果初始状态下内容无法撑满容器,就无法出现滚动条,那就会造成后面无法滚动加载数据了!这个默认就是 true,保证加载到出现滚动条。

const onScroll = throttle(delay, handleScroll.bind(el, cb));
// ...
if (container) {
  container.addEventListener('scroll', onScroll);

  if (immediate) {
    const observer = el[scope].observer = new MutationObserver(onScroll);
    observer.observe(container, { childList: true, subtree: true });
    onScroll();
  }
}

handleScroll 的实现如下:

const handleScroll = function(cb) {
  const { el, vm, container, observer } = this[scope];
  const { distance, disabled } = getScrollOptions(el, vm);

  if (disabled) return;

  const containerInfo = container.getBoundingClientRect();
  if (!containerInfo.width && !containerInfo.height) return;

  let shouldTrigger = false;

  if (container === el) {
    // be aware of difference between clientHeight & offsetHeight & window.getComputedStyle().height
    const scrollBottom = container.scrollTop + getClientHeight(container);
    shouldTrigger = container.scrollHeight - scrollBottom <= distance;
  } else {
    const heightBelowTop = getOffsetHeight(el) + getElementTop(el) - getElementTop(container);
    const offsetHeight = getOffsetHeight(container);
    const borderBottom = Number.parseFloat(getStyleComputedProperty(container, 'borderBottomWidth'));
    shouldTrigger = heightBelowTop - offsetHeight + borderBottom <= distance;
  }

  if (shouldTrigger && isFunction(cb)) {
    cb.call(vm);
  } else if (observer) {
    observer.disconnect();
    this[scope].observer = null;
  }

};

看一下主要流程,首先会判断 disabled,如果是 true 直接返回。可以结合官方实例,实际开发中,如果正在加载数据,那么就可以将 disabled 设置为 true,避免多次触发。

根据容器不同,判断方法有些差异:

  1. 如果滚动容器是绑定的元素本身,那就通过前面说明的那种方式:
Element.scrollHeight - Element.scrollTop - Element.clientHeight === 0

判断是否已经到底部了。这里是判断是否小于等于 distance,它提供了一个配置项,距离底部 distance 的时候就可以触发回调了。

const scrollBottom = container.scrollTop + getClientHeight(container);
shouldTrigger = container.scrollHeight - scrollBottom <= distance;
  1. 如果滚动容器不是元素本身,那判断就会麻烦一些:

javascript-InfiniteScroll-eg2

当鼠标往下滚的时候,el 就会向上,heightBelowTop - offsetHeight + borderBottom 其实就是 el 底部到 container 的距离,它和 distance 含义其实是一样的。

javascript-InfiniteScroll-eg3

如果满足条件就会执行回调 cb.call(vm)。同时,上面说过设置 immediate 会立即加载,加载完成之后移除 observer

if (shouldTrigger && isFunction(cb)) {
  cb.call(vm);
} else if (observer) {
  observer.disconnect();
  this[scope].observer = null;
}

参考

MVC, MVP 和 MVVP

看了几篇 MVC 的 MVVM 的文章,感觉一些背景和区分都不太清楚,一部分还主要是根据一些实际的开发反过来硬套 MVC。不过最后根据其中一些大佬们整理的文章,也总算有一点点头绪,这里记录一下

在架构设计中,我们面对的模式主要有:

  • MVC
  • MVP
  • MVVM

这三种模式都是把所有的实体归类到了下面三种分类中的一种:

  • Models(模型)- 数据层,或者负责处理数据的数据接口层。
  • Views(视图) - 展示层(GUI)
  • Controller/Presenter/ViewModel(控制器/展示器/视图模型) - 它是 Model 和 View 之间的胶水或者说是中间人。一般来说,当用户对 View 有操作时它负责去修改相应 Model;当 Model 的值发生变化时它负责去更新对应 View。

通过以上的分类:

  • 更容易理解
  • 方便重用
  • 可以独立进行测试

MVC 架构

由来

在1979年,经典MVC模式被提出。

在当时,人们一直试图将纯粹描述思维中的对象与跟计算机环境打交道的代码隔离开来,而Trygve Reenskaug在跟一些人的讨论中,逐渐剥离出一系列的概念,最初是Thing、Model、View、Editor。后来经过讨论定为Model、View和Controller。作者自言“最难搞的就是给这些架构组件起名字”。

因为当时的软件环境跟现在有很大不同,所以经典MVC中的概念很难被现在的工程师理解。比如经典 MVC中说:“view 永远不应该知道用户输入,比如鼠标操作和按键。”对一个现代的软件工程师来说,这听上去相当不可思议:难道监听事件不需要类似这样的代码吗?

view.onclick = ……

但是想想在70年代末,80年代初,我们并没有操作系统和消息循环,甚至鼠标的光标都需要我们的 UI 系统来自行绘制,所以我们面对的应该是类似下面的局面:

mouse.onclick = ……

mouse.onmove = ……

当鼠标点击事件发生后,我们需要通过 view 的信息将点击事件派发到正确的 view 来处理。假如我们面对的是鼠标、键盘驱动这样的底层环境,我们就需要一定的机制和系统来统一处理用户输入并且分配给正确的 view 或者 model 来处理。这样也就不难理解为什么经典 MVC 中称”controller是用户和系统之间的链接”。

因为现在的多数环境和UI系统设计思路已经跟1979年完全不同,所以现代一些喜好生搬硬套的“MVC”实现者常常会认为controller的输入来自view,以至于画出model、view、controller之间很奇葩的依赖关系:

image

我们来看看Trygve Reenskaug自己画的图:

image

值得一提的是,其实MVC的论文中,还提到了”editor”这个概念。因为没有出现在标题中,所以editor声名不著。MVC论文中推荐controller想要根据输入修改view时,从view中获取一个叫做editor的临时对象,它也是一种特殊的controller,它会完成对view和view相关的model的修改操作。

控件系统

MVC是一种非常有价值的架构思路,然而时代在变迁,随着以windows系为代表的WIMP(window、icon、menu、pointer)风格的应用逐渐成为主流,人们发现,view和controller某些部件之间的局部性实际上强于controller内部的局部性。于是一种叫做控件(control)的预制组件开始出现了。

控件本身带有一定的交互功能,从MVC的视角来看,它既包含view,又包含controller,并且它通过“属性”,来把用户输入暴露给model。

controller的输入分配功能,则被操作系统提供的各种机制取代:

  • 指针系统:少数DOS时代过来的程序员应该记得,20年前的程序中的“鼠标箭头”实际上是由各个应用自己绘制的,以MVC的视角来看,这应当属于一个”PointerView”的职责范畴。但是20世纪以后,这样的工作基本由操作系统的底层UI系统来实现了。
  • 文本系统:今天我们几乎不需要再去关心文本编辑、选中、拖拽等逻辑,对web程序员可以尝试自己用canvas写一个文本编辑框来体验一下上个时代程序员编写程序的感受。你会发现,选中、插入/覆盖模式切换、换行、退格、双击、拖拽等逻辑异常复杂,经典MVC模式中通常使用TextView和TextEditor配合来完成这样的工作,但是今天几乎找不到需要我们自己处理这些逻辑的场景。
  • 焦点系统:焦点系统通过响应鼠标、tab键等消息来使得控件获得操作系统级唯一的焦点状态,所有的键盘事件通常仅仅会由拥有焦点的控件来响应。在没有焦点系统的时代,操作系统通常是单任务的,但是即使是单一应用,仍然要自己管理多个controller之间的优先权和覆盖逻辑,焦点系统不但从技术上,也从交互设计的角度规范化了UI的输入响应,而最妙的是,焦点系统是对视觉障碍人士友好的,现在颇多盲人用读屏软件都是强依赖焦点系统的。

所以时至今日,MVC,尤其是其中 controller 的功能已经意义不大,若是在控件系统中,再令所有用户输入流经一个 controller 则可谓不伦不类、本末倒置。MVVM 的提出者,微软架构师John Gossman曾言:“我倾向于认为它(指controller)只是隐藏到后台了,它仍然存在,但是我们不需要像是1979年那样考虑那么多事情了”

MVP

由来

1996年,Taligent公司的CTO,Mike Potel 在一篇论文中提出 Model-View-Presenter 的概念。

在这个时期,主流的 view 的概念跟经典MVC中的那个“永远不应该知道用户输入”的view有了很大的差别,它通常指本文中所述的控件,此时在Mike眼中,输入已经是由view获得的了:

image

Model-View-Presenter是在MVC的基础上,进一步规定了Controller中的一些概念而成的:

image

对,所以,不论你按照 Mike 还是 Trygve 的理解方式,MVP和MVC的依赖关系图应该是一模一样的!因为Mike的论文里说了“we refer to this kind(指应用程序全局且使用interactor, command以及selection概念的) of controller as a presenter”。presenter它就是一种controller啊!

标记语言和 MVVM

随着 20 世纪初 web 的崛起,HTML 跟 JS 这样标记语言+程序语言的组合模式开始变得令人注目。逐渐推出的Flex、Sliverlight、QT、WPF、JSF、Cocoa等UI系统不约而同地选择了标记语言来描述界面。

在这样的架构中,view(或者说叫控件),不但是从依赖关系上跟程序的其他部件解耦,而且从语言上跟其它部分隔离开来。

标记语言的好处是,它可以由非专业的程序员产生,通过工具或者经过简单培训,一些设计师可以直接产生用标记语言描述的UI。想要突破这个限制使得view跟其它部分异常耦合可能性也更低。

然而这样的系统架构中,MVC和MVP模式已经不能很好地适用了。微软架构师John Gossman在WPF的XAML模式推出的同时,提出了MVVM的概念。

WPF得MVVM正式说明了它的view的概念跟MVC中的view的概念的区别。

image

在MVVM模式中,数据绑定是最重要的概念,在MVC和MVP中的view和model的互相通讯,被以双向绑定的方式替代,这进一步把逻辑代码变成了声明模式。

参考

JavaScript 手写代码

new

/**
 * new 操作符
 * 
 * 1.创建(或者说构造)一个全新的对象;
 * 2.这个新对象会被执行 [[Prototype]] 链接;
 * 3.这个新对象会被绑定到函数调用的 this;
 * 4.如果函数没有返回其他新对象,那么 new 表达式中的函数调用会自动返回这个新对象。
 *
 * @param {Function} fn 构造函数
 */
function myNew(fn) {
  var obj = {};
  obj.__proto__ = fn.prototype;
  var args = [].slice.call(arguments, 1);
  var result = fn.apply(obj, args);
  return typeof result === 'object' && result !== null ? result : obj;
}
// test
function Foo(name, age) {
  this.name = name;
  this.age = age;
}
function Baz(name, age) {
  this.name = name;
  this.age = age;
  return {a: 1};
}
myNew(Foo, 1, 2); // {name: 1, age: 2}
myNew(Baz, 1, 2); // {a: 1}

JSON.stringfy

/**
 * JSON.stringfy
 * 
 * 语法格式:JSON.stringify(value[, replacer [, space]])
 * 
 * @param {*} obj 要序列化的参数
 */
function jsonStringfy(obj) {
  let type = typeof obj;
  if (type !== 'object') {
    if (/function|undefined|symbol/.test(type)) return undefined;
    if (type === 'string') obj = `"${obj}"`;
    return String(obj);
  } else {
    let json = [];
    let isArr = Array.isArray(obj);
    for (let k in obj) {
      let v = obj[k];
      let type = typeof v;
      if (/function|undefined|symbol/.test(type)) continue;
      if (type === 'string') {
        v = `"${v}"`;
      } else if (type === 'object') {
        v = jsonStringfy(v);
      }
      json.push((isArr ? "" : `"${k}":`) + String(v));
    }
    // String(json) 数组转为字符串,然后再用 "[]" 或 "{}" 包裹
    return (isArr ? "[" : "{") + String(json) + (isArr ? "]" : "}")
  }
}

JSON.parse

/**
 * JSON.parse
 *
 * @param {*} str 反序列化的字符串
 * @returns 返回结果
 */

// eval
function jsonParse(str) {
  return eval(`(${str})`)
}

// Function
function jsonParse(str) {
  return (new Function('return ' + jsonStr))();
}

call

/**
 * call
 *
 * @param {*} context 
 * @param {*} args 
 * @returns 
 */
Function.prototype.call1 = function (context = window, ...args) {
  if (this === Function.prototype) return undefined;
  context.fn = this;
  let result = context.fn(...args);
  delete context.fn;
  return result;
}

apply

/**
 * apply
 *
 * @param {*} context 
 * @param {*} args 
 * @returns 
 */
Function.prototype.apply1 = function (context = window, args) {
  if (this === Function.prototype) return undefined;
  context.fn = this;
  let result;
  if (Array.isArray(args)) {
    result = context.fn(...args);
  } else {
    result = context.fn();
  }
  delete context.fn;
  return result;
}

bind

/**
 * bind
 *
 * @param {*} context 
 * @param {*} args1
 * @returns 
 */

Function.prototype.bind1 = function (context, ...args1) {
  if (typeof this !== 'function') {
    throw new Error('Not a function');
  }
  let fn = this;
  function ToBind(...args2) {
    return fn.apply(
      this instanceof F ? this : context || window,
      args1.concat(args2)
    )
  }
  function F() {};
  F.prototype = fn.prototype;
  ToBind.prototype = new F();
  return ToBind;
}

继承

/**
 * 实现一个继承
 */
function Parent(name) {
  this.name = name;
}
Parent.prototype.sayName = function() {
  console.log('name:', this.name);
}
function Children(name, age) {
  Parent.call(this, name);
  this.age = age;
}
function create(obj) {
  function F() {};
  F.prototype = obj;
  return new F();
}
Children.prototype = create(Parent.prototype);
// 或者直接只用 Object.create()
// Children.prototype = Object.create(Parent.prototype);
Children.prototype.sayAge = function() {
  console.log('age:', this.age);
}
Object.defineProperty(Children.prototype, 'constructor', {
  value: Children,
  enumerable: false,
  writable: true,
  configurable: true,
})
var child = new Children('Foo', 20);
child.sayAge(); // 20
child.sayName(); // 'Foo'

柯里化

function curry(fn) {
  let args = Array.prototype.slice.call(arguments, 1);
  return function () {
    let innerArgs = Array.prototype.slice.call(arguments);
    return fn.apply(this, args.concat(innerArgs));
  }
}

// 1. 实现 multi(2)(3)(4), multi(2,3,4), multi(2)(3,4)
function multiFn(a, b, c) {
  return a * b * c;
}
function curry1(fn) {
  let length = fn.length;
  let args1 = Array.prototype.slice.call(arguments, 1);
  return function () {
    let args2 = Array.prototype.slice.call(arguments);
    let args = args1.concat(args2);
    if (args.length >= length) {
      return fn.apply(this, args);
    } else {
      return curry1(fn, ...args);
    }
  }
}
let multi = curry1(multiFn);
multi(2,3,4);
multi(2)(3)(4);

// 2. 实现 multi(2)(3)(4)(), multi(2)(3)(), multi(2)(3)(4)(5)(), multi(2,3,4,5)()
function multiFn() {
  let args = Array.prototype.slice.call(arguments);
  return args.reduce((pre, cur) => pre * cur);
}
function curry2(fn) {
  let args1 = Array.prototype.slice.call(arguments, 1);
  return function () {
    let arg2 = Array.prototype.slice.call(arguments);
    if (arguments.length === 0) {
      return fn.apply(this, args1.concat(arg2));
    } else {
      return curry2(fn, ...args1.concat(arg2));
    }
  }
}
let multi = curry2(multiFn);
multi(2)(3)(4)();

Promise

/**
 * Promise
 */

// 简单版
const PENDING = "pending"
const FULFILLED = "fulfilled"
const REJECTED = "rejected"

function Promise(f) {
  this.state = PENDING;
  this.value = undefined;
  this.reason = undefined;

  const resolve = value => {
    if (value === this) {
      throw new TypeError('Can not fulfill itself');
    }
    if (value instanceof Promise) {
      return value.then(resolve, reject);
    }
    setTimeout(() => {
      if (this.state === PENDING) {
        this.state = FULFILLED;
        this.value = value;
      }
    })
  }
  
  const reject = reason => {
    setTimeout(() => {
      if (this.state === PENDING) {
        this.state = REJECTED;
        this.reason = reason;
      }
    })
  }

  try {
    f(resolve, reject)
  } catch (error) {
    reject(error)
  }
}

Promise.prototype.then = function(onFulfilled, onRejected) {
  if (this.state === FULFILLED) {
    onFulfilled(this.value);
  } else if (this.state === REJECTED) {
    onRejected(this.reason);
  }
}
var promise = new Promise((resolve, reject) => resolve(1))
promise.then(v => console.log(v)); // 1

// 完整版
const isFunction = obj => typeof obj === 'function'
const isObject = obj => !!(obj && typeof obj === 'object')
const isThenable = obj => (isFunction(obj) || isObject(obj)) && 'then' in obj
const isPromise = promise => promise instanceof Promise

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

function Promise(f) {
  this.result = null
  this.state = PENDING
  this.callbacks = []

  let onFulfilled = value => transition(this, FULFILLED, value)
  let onRejected = reason => transition(this, REJECTED, reason)

  let ignore = false
  let resolve = value => {
    if (ignore) return
    ignore = true
    resolvePromise(this, value, onFulfilled, onRejected)
  }
  let reject = reason => {
    if (ignore) return
    ignore = true
    onRejected(reason)
  }

  try {
    f(resolve, reject)
  } catch (error) {
    reject(error)
  }
}

Promise.prototype.then = function(onFulfilled, onRejected) {
  return new Promise((resolve, reject) => {
    let callback = { onFulfilled, onRejected, resolve, reject }

    if (this.state === PENDING) {
      this.callbacks.push(callback)
    } else {
      setTimeout(() => handleCallback(callback, this.state, this.result), 0)
    }
  })
}

const handleCallback = (callback, state, result) => {
  let { onFulfilled, onRejected, resolve, reject } = callback
  try {
    if (state === FULFILLED) {
      isFunction(onFulfilled) ? resolve(onFulfilled(result)) : resolve(result)
    } else if (state === REJECTED) {
      isFunction(onRejected) ? resolve(onRejected(result)) : reject(result)
    }
  } catch (error) {
    reject(error)
  }
}

const handleCallbacks = (callbacks, state, result) => {
  while (callbacks.length) handleCallback(callbacks.shift(), state, result)
}

const transition = (promise, state, result) => {
  if (promise.state !== PENDING) return
  promise.state = state
  promise.result = result
  setTimeout(() => handleCallbacks(promise.callbacks, state, result), 0)
}

const resolvePromise = (promise, result, resolve, reject) => {
  if (result === promise) {
    let reason = new TypeError('Can not fulfill promise with itself')
    return reject(reason)
  }

  if (isPromise(result)) {
    return result.then(resolve, reject)
  }

  if (isThenable(result)) {
    try {
      let then = result.then
      if (isFunction(then)) {
        return new Promise(then.bind(result)).then(resolve, reject)
      }
    } catch (error) {
      return reject(error)
    }
  }

  resolve(result)
}

防抖函数

/**
 * debounce
 *
 * @param {Function} fn 进行防抖处理的函数
 * @param {Number} wait 延迟时间
 * @param {Boolean} immediate 是否立即执行
 * @returns
 */
function debounce(fn, wait, immediate) {
  let timer = null;
  return function() {
    if (timer) clearTimeout(timer);
    if (immediate && !timer) fn.apply(this, arguments);
    timer = setTimeout(() => {
      fn.apply(this, arguments);
    }, wait)
  }
}

节流函数

/**
 * throttle
 *
 * @param {*} fn 函数
 * @param {*} interval 间隔时间
 * @returns
 */
function throttle(fn, interval) {
  let last = 0;
  let timer = null;
  return function() {
    let now = Date.now();
    let delay = interval - (now - last);
    if (delay <= 0) {
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      fn.apply(this, arguments);
    } else {
      timer = setTimeout(() => {
        fn.apply(this, arguments);
      }, delay)
    }
  }
}

深拷贝

/**
 * 深拷贝
 *
 * @param {*} obj
 * @returns
 */
function deepClone(obj) {
  if (typeof obj !== 'object' || obj === null) return obj;
  let res = Array.isArray(obj) ? [] : {};
  for (let k in obj) {
    if (typeof obj[k] === 'object') {
      res[k] = deepClone(obj[k]);
    } else {
      res[k] = obj[k];
    }
  }
  return res;
}

instanceof

/**
 * instanceOf
 *
 * @param {*} obj 对象
 * @param {*} ctor 构造函数
 */
function instanceOf(obj, ctor) {
  let proto = obj.__proto__
  let prototype = ctor.prototype
  while (true) {
    if (proto === null) return false
    if (proto === prototype) return true
    proto = proto.__proto__
  }
}

参考

如何提升自己

前端已死中介绍,在现在环境中重要的是提升自己的核心竞争力,这其中最重要的是学习能力。

那你该怎么学习?学习什么?你是怎样思考这些问题的?

相关原理和思维模型

一般来说,超过别人一般来说就是两个维度:

  • 在认知、知识和技能上
  • 在领导力上

首先,我们对一个事物的了解是从“认识”开始的,然后经过书本、教程、学校把“零碎的认知”转换成”系统的知识“,要把支持转换成技能,就需要训练和实践,这样才能完成从:认知 ——> 知识 ——> 技能 的转换。

认知

认知是指“透过**、经验和感官获得知识和理解的心理行为或过程”。是我们了解和理解世界的方式,它影响着我们的学习、决策、思考和行为。

要提升“认知”,需要在 3 个方面努力:

1. 信息渠道

试想如果别人的信息源没有你的好,那么,这些看不见信息源的人,只能接触得到二手信息甚至三手信息,只能获得被别人解读过的信息,这些信息被三传两递后必定会有错误和失真。

只能被“喂养”。

2. 信息质量

信息质量主要表现在两个方面,一个是信息中的燥音,另一个是信息中的质量等级。

你天天看的都是垃圾,你的**和认识也只有垃圾。所以,如果你的信息质量并不好的话,你的认知也不会好,而且你还要花大量的时间来进行有价值信息的挖掘和处理。

3. 信息密度

优质的信息,密度一般都很大,因为这种信息会逼着你去干这么几件事:

  • 搜索并学习其关联的知识
  • 沉思和反省
  • 亲手去推理、验证和实践……

一般来说,经验性的文章会比知识性的文章会更有这样的功效。比如,类似于像 Effiective C++/Java,设计模式,Unix编程艺术,算法导论等等这样的书就是属于这种密度很大的。

通过提高自己的认知能力,我们能够更好地解决问题和面对挑战。

知识

要提升“知识”,需要在 3 个方面努力:

1. 知识树(图)

任何知识,只在点上学习不够的,需要在面上学习,这叫系统地学习,这需要我们去总结并归纳知识树或知识图,一个知识面会有多个知识板块组成,一个板块又有各种知识点,一个知识点会导出另外的知识点,各种知识点又会交叉和依赖起来,学习就是要系统地学习整个知识树(图)

对于树,根基是非常重要的,要需要基础。对于陌生地方,地图(知识树)是非常重要的,没有地图只会迷路、走冤枉路。

2. 知识缘由

了解知识的缘由和前世今生,可以帮助我们更好地理解和掌握知识,而不只是单纯地靠记忆。

对于一些操作性的知识(不需要了解由来的),我把其叫操作知识,就像一些函数库一样,这样的知识只要学会查文档就好了。

能够知其然,知其所以然的人自然会比识知识到表皮的人段位要高很多。

3. 方法套路

学习的目的不是为了找到答案,而是为了找到解题方法和思路,掌握更高级的方法和解题思路是提高自己的关键。

技能

要提升“技能”,需要在 3 个方面努力:

1. 精益求精

不仅仅是重复训练,而是在每次训练中总结经验,寻找更好的方法。

用相同的方法重复,那你只不过在搬砖罢了。

2. 让自己犯错

犯错是有利于成长的,这是因为出错会让人反思,反思更好的方法,反思更完美的方案,总结教训,寻求更好更完美的过程,是技能升级的最好的方式。

当然,千万不要同一个错误重复地犯。

3. 找高手切磋

找高手切磋,通过和高手切磋来感受高手的技艺和方法,寻找新的技能提升途径。

领导力

要有领导力或是影响力这个事并不容易,这跟你的野心有多大,好胜心有多强 ,你愿意付出多少很有关系。

1. 识别自己的特长和天赋

首先,每个人DNA都可能或多或少都会有一些比大多数人厉害的东西(当然,也可能没有)。如果你有了,那么在你过去的人生中就一定会表现出来了,就是那种大家遇到这个事会来请教你的寻求你帮助的现象。

如果有特长和天赋,要扩大自己的优势,不要进入会限制自己优势的领域。

2. 识别自己的兴趣

没有天赋也没有问题,还有兴趣点,都说兴趣是最好的老师。兴趣驱动的事总是会比那些被动驱动的更好。

这里说明一下兴趣,真正的兴趣不是那种三天热度的东西,而是那种,你愿意为之付出一辈子的事,是那种无论有多大困难有多难受你都要死磕的事,这才是“真兴趣”,这也就是你的“好胜心”所在。

3. 建立高级的习惯和方法

没有天赋没有野心,也还是可以跟别人拼习惯拼方法的,只要你有一些比较好的习惯和方法,那么你一样可以超过大多数人。

在习惯上你要做到比较大多数人更自律,更有计划性,更有目标性。比如,每年学习一门新的语言或技术,并可以参与相关的顶级开源项目,每个月训练一个类算法,掌握一种算法,每周阅读一篇英文论文,并把阅读笔记整理出来……自律的是非常可怕的。

你还需要在方法上超过别人,你需要满世界的找各种高级的方法。其中包括,思考的方法,学习的方法、时间管理的方法、沟通的方法这类软实力的,还有,解决问题的方法(trouble shooting 和 problem solving),设计的方法,工程的方法,代码的方法等等硬实力的,一开始照猫画虎,时间长了就可能会自己发明或推导新的方法。

4. 勤奋努力执着坚持

如果上面三件事你都没有也没有能力,那还有最后一件事了,那就是勤奋努力了,就是所谓的“一万小时定律”了。

很多东西都是死的,只要肯花时间就有一天你会搞懂的。

参考

一个自动补全的 VSCode 插件

Visual Studio Code 是由微软开发的一款免费、跨平台的文本编辑器。由于其卓越的性能和丰富的功能,它很快就受到了大家的喜爱。同时,它强大的扩展插件也给开发带来极大的便利。

在开发过程中,会逐渐形成一些常用或者公共的代码片段,这个时候就可以通过 VSCode 的自动补全来提高我们的效率了,自动补全的代码就是一些使用频率最高的,通过这种方式最能帮助我们提高编码的速度。

在 VSCode 上配置 Snippets

VSCode 本身就支持代码片段自动补全的配置。按下快捷键 shift+ctrl+p 显示所有命令,然后输入 Snippets,选择“首选项:配置用户代码判断”。

20191111134216

20191111134319

接下来会提示配置那种语言的代码判断,这里以 javascript.json 为例。

20191111135013

每个代码片段都必须包含:

  • prefix: 前缀是用来触发代码段的
  • body:片段的主体内容,将会被扩展插入
  • description:说明描述

另外 $1, $2 用于制表符(Tab)切换停止, $0 是最后一个位置, 还有 ${1:label}, ${2:another}用于配置占位符。

{
    // Place your snippets for javascript here. Each snippet is defined under a snippet name and has a prefix, body and 
    // description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
    // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. Placeholders with the 
    // same ids are connected.
    // Example:
    // "Print to console": {
    //    "prefix": "log",
    //     "body": [
    //         "console.log('$1');",
    //          "$2"
    //     ],
    //     "description": "Log output to console"
    // }
    "for of": {
        "prefix": "forof",
        "body": [
            "for (let v of ${1:arr}) {",
            "\t${2://body}",
           "}"
        ],
        "description": "for of"
    }
}

然后配置了一个 for of,保存之后,在一个 .js 文件里边输入 forof 这个时候出现我们配置的代码片段了,选择就可以自动补全了!

20191111135043

很方便,但是还存在一些问题:

  • 在自己的 VSCode 中配置的,不方便共享;
  • 换个环境就需要重新配置。

这就需要 VSCode 扩展插件了

自定一个 VSCode 自动补全插件

接下来,将以一个简单的代码片段自动补全插件为例,看看整个过程

环境准备

  • 安装脚手架,官方推荐使用的脚手架工具 Yeoman 和 Generator-code
npm install -g yo generator-code
  • 安装打包和发布工具 vsce
npm install -g vsce

初始化项目,并选择配置

输入 yo code 进行初始化,接下来会提示你选择一些配置,主要包括插件类型、名称和描述等等。不同的插件类型会对应不同的模板,这里选择 Code Snippets

yo code

20191111154005

生成的代码结构如下

├── .vscode
	├── launch.json  # 插件加载和调试的配置
├── snippets
	├── snippets.json  # 代码片段
├── CHANGELOG.md  # 变更记录
├── package.json  # 声明当前插件相关信息
├── README.md  # 插件使用说明
└── vsc-extension-quickstart.md

20191111140247

自定义代码片段并调试

通过上面的例子,我们已经知道配置规则了。这里我还新建一个 .vue 的自动补全:

"contributes": {
    "snippets": [
        {
            "language": "javascript",
            "path": "./snippets/snippets.json"
        },
        {
            "language": "vue",
            "path": "./snippets/vue.json"
        }
    ]
}

然后配置如下:

  • snippets/snippets.json
{
  "forEach": {
    "prefix": "fe",
    "body": [
      "${1:array}.forEach(function(item) {",
      "\t${2:// body}",
      "});"
    ],
    "description": "Code snippet for \"forEach\""
  },
  "for of": {
        "prefix": "forof",
        "body": [
            "for (let v of ${1:arr}) {",
            "\t${2://body}",
           "}"
        ],
        "description": "for of"
    }
}
  • snippets/vue.json
{
  "templateLang": {
    "prefix": "templateLang",
    "body": [
      "<template lang=\"$1\">",
      "\t<div$2>",
      "\t\t$0",
      "\t</div>",
      "</template>"
    ],
    "description": "template element"
  },
  "script": {
    "prefix": "script",
    "body": [
      "<script>",
      "export default {",
      "\tname: '$1',",
      "\tdata() {",
      "\t\treturn {",
      "\t\t\t$2",
      "\t\t}",
      "\t},",
      "\tmethods: {",
      "\t\t",
      "\t}",
      "}",
      "</script>"
    ],
    "description": "script element"
  },
  "styleLang": {
    "prefix": "styleLang",
    "body": [
      "<style lang=\"$1\">",
      "\t$0",
      "</style>"
    ],
    "description": "style element with lang attribute"
  }
}

接下来进行调试,这块的说明在 vsc-extension-quickstart.md 都有说明了。按下 F5 会打开一个新的窗口:

20191111140704

然后在新窗口中新建 .js.vue 文件,输入刚才定义的代码片段的 prefix,这个时候就会出现提示了!

20191111140736
20191111140809

如果你又修改了配置的代码片段,这是需要按下 ctrl+r 重新加载才生效。

打包发布

  • 打包

输入命令 vsce package 进行打包,打包完成之后就会生成一个 .vsix 的安装包,如果进作为团队或者内网使用,那么手动安装即可,无需发布到 VSCode 插件市场。

注意,直接打包会发生一些错误:

20191111132836

  1. Missing publisher name:如果不发布,则直接在 package.json 加上这个即可:
{
    "description": "common code snippets",
    "publisher": "test"
    "version": "0.0.1",
}
  1. 没有编辑 README.md:需要更改这个文件

  2. 没有 repository 地址:不发布的话这个可以不配置,若需要发布,则加上 github 上对应的地址。

然后就可以打包成功了:

20191111153158

接着可以选择安装这个扩展:

20191111153247

  • 发布

发布需要发布者账号,所以你需要前往 marketplace 注册。注册之后需要创建一个 organization,然后申请 Personal Access Tokens 。详细申请细节见 Publishing Extension
输入命令 vsce publish 进行发布。

这是发布之后的地址 Self Snippets,以及代码地址 self-snippets

参考

nginx 配置说明

try_files

try_files 是 nginx 中 http_core 核心模块所带的指令,主要是能替代一些 rewrite 的指令,提高解析效率。

语法规则

  • 格式1:try_files file ... uri;
  • 格式2:try_files file ... = code;

含义

Checks the existence of files in the specified order and uses the first found file for request processing; the processing is performed in the current context. The path to a file is constructed from the fileparameter according to the root and alias directives. It is possible to check directory’s existence by specifying a slash at the end of a name, e.g. “$uri/”. If none of the files were found, an internal redirect to the uri specified in the last parameter is made.

  1. 按指定的 file 顺序查找存在的文件,并使用第一个找到的文件进行请求处理
  2. 查找路径是按照给定的 rootalias 为根路径来查找的
  3. 如果给出的 file 都没有匹配到,则重新请求最后一个参数给定的 uri,就是新的 location 匹配
  4. 如果是格式 2,如果最后一个参数是 = 404 ,若给出的 file 都没有匹配到,则最后返回 404 的响应码

例子

location /images/ {
  root /opt/html/;
  try_files $uri $uri/ /images/default.gif; 
}

比如请求 127.0.0.1/images/test.gif 会依次查找

  1. 文件 /opt/html/images/test.gif
  2. 文件夹 /opt/html/images/test.gif/ 下的 index 文件
  3. 请求 127.0.0.1/images/default.gif
  4. 其他注意事项:try-files 如果不写上 $uri/,当直接访问一个目录路径时,并不会去匹配目录下的索引页,即访问 127.0.0.1/images/ 不会去访问 127.0.0.1/images/index.html

为什么学习 Node.js

我之前学习 Node 的犹如蜻蜓点水一般,现在都谈不上对 Node 有了解,所以又重新想了一下这个问题,没有找到一些比较满意的答案,不过有些收获的这里总结一下。

为什么使用 Node.js

原文 Why The Hell Would I Use Node.js? A Case-by-Case Tutorial

作者 TOMISLAV CAPAN

正如维基百科中说明的一样

Node.js 是能够在服务器端运行 JavaScript 的开放源代码、跨平台运行环境。Node.js 采用 Google 开发的 V8 运行代码,使用事件驱动、非阻塞和异步输入输出模型等技术来提高性能,可优化应用程序的传输量和规模。

在本 Node.js 指南中,我不仅会讨论这些优势是如何实现的,还会讨论为什么您可能想要使用 Node.js 以及为什么不使用一些经典的 Web 应用程序模型作为示例。

它是如何工作的

Node.js 的主要**:使用非阻塞、事件驱动的 I/O,在面对跨分布式设备运行的数据密集型实时应用程序时保持轻量级和高效。

它的真正含义是,Node.js并不是一个将主导 Web 开发世界的银弹新平台。相反,它是一个满足特定需求的平台。

理解这一点是绝对必要的。您绝对不想将 Node.js 用于 CPU 密集型操作;事实上,将它用于繁重的计算几乎会抵消它的所有优势。Node 真正闪耀的地方在于构建快速、可扩展的网络应用程序,因为它能够以高吞吐量处理大量同时连接,这相当于高可扩展性

它在后台的工作方式非常有趣。与传统的 Web 服务技术相比,每个连接(请求)产生一个新线程,占用系统 RAM 并最终最大化可用 RAM 量,Node.js 在单线程上运行,使用非阻塞 I/ O 调用,允许它支持在事件循环中保持的数万个并发连接。

image

快速计算:假设每个线程可能附带 2 MB 内存,在具有 8 GB RAM 的系统上运行使我们理论上最多可以有 4,000 个并发连接(计算取自 Michael Abernethy 的文章“到底什么是 Node .js?”,于 2011 年在 IBM developerWorks 上发表;不幸的是,该文章不再可用),加上线程之间上下文切换的成本。这就是您通常在传统 Web 服务技术中处理的场景。通过避免所有这些,Node.js 实现了超过 100 万并发连接和超过60 万并发 websockets 连接的可扩展性水平。

当然,存在在所有客户端请求之间共享单个线程的问题,这是编写 Node.js 应用程序的潜在陷阱。首先,繁重的计算可能会阻塞 Node 的单线程并导致所有客户端出现问题(稍后会详细介绍),因为传入请求将被阻塞,直到所述计算完成。其次,开发人员需要非常小心,不要让异常冒泡到核心(最顶层)Node.js 事件循环,这将导致 Node.js 实例终止(有效地使程序崩溃)。

用于避免异常冒泡到表面的技术是将错误作为回调参数传递回调用者(而不是像在其他环境中那样抛出它们)。即使某些未处理的异常设法冒泡,也已经开发了工具来监视 Node.js 进程并执行必要的崩溃实例恢复(尽管您可能无法恢复用户会话的当前状态),最常见的是Forever 模块,或者使用不同的方法与外部系统工具upstart和monit,甚至只是 upstart。

NPM:节点包管理器

在讨论 Node.js 时,绝对不应忽略的一件事是使用 NPM对包管理的内置支持,NPM是每个 Node.js 安装默认附带的工具。NPM 模块的**与Ruby Gems的**非常相似:一组公开可用、可重用的组件,可通过在线存储库轻松安装获得,具有版本和依赖项管理。

完整的打包模块列表可以在npm 网站上找到,或者使用 npm CLI 工具访问,该工具自动随 Node.js 安装。模块生态系统对所有人开放,任何人都可以发布自己的模块,这些模块将列在 npm 存储库中。

今天一些最有用的 npm 模块是:

  • express - Express.js — 或简称 Express — 一个受 Sinatra 启发的 Node.js 网络开发框架,也是当今大多数 Node.js 应用程序的事实上的标准。
  • hapi - 一个非常模块化且易于使用的以配置为中心的框架,用于构建 Web 和服务应用程序
    connect
  • Connect 是 Node.js 的可扩展 HTTP 服务器框架,提供称为中间件的高性能“插件”集合;作为 Express 的基础。
  • socket.io和sockjs - 当今两个最常见的 websockets 组件的服务器端组件。
  • pug(以前称为Jade)- 受 HAML 启发的流行模板引擎之一,这是 Express.js 中的默认设置。
  • mongodb和mongojs - MongoDB 包装器,为 Node.js 中的 MongoDB 对象数据库提供 API。
  • redis -Redis 客户端库。
  • lodash (underscore, lazy.js) - JavaScript 实用工具带。Underscore 发起了这场比赛,但被两个对手之一推翻,主要是因为更好的性能和模块化实现。
    永远- 可能是确保给定节点脚本连续运行的最常用实用程序。面对任何意外故障,让您的 Node.js 进程保持在生产环境中。
  • bluebird - 功能齐全的 Promises/A+ 实现,具有非常好的性能
  • moment - 用于解析、验证、操作和格式化日期的 JavaScript 日期库。

应该在何处使用 Node.js 的示例

聊天

聊天应用程序确实是 Node.js 的最佳示例:它是一个轻量级、高流量、数据密集型(但处理/计算量低)的应用程序,可跨分布式设备运行。它也是一个很好的学习用例,因为它很简单,但它涵盖了您将在典型 Node.js 应用程序中使用的大多数范例。

让我们试着描述它是如何工作的。

在最简单的例子中,我们的网站上有一个单独的聊天室,人们可以在那里以一对多(实际上是所有人)的方式交换消息。例如,假设我们在网站上有三个人都连接到我们的留言板。

在服务器端,我们有一个简单的 Express.js 应用程序,它实现了两件事:

  1. 一个GET /请求处理程序,它为包含留言板和“发送”按钮的网页提供服务,以初始化新的消息输入,以及
  2. 一个 websockets 服务器,它监听 websocket 客户端发出的新消息。

在客户端,我们有一个 HTML 页面,其中设置了几个处理程序,一个用于“发送”按钮单击事件,它接收输入消息并将其发送到 websocket,另一个用于侦听在 websockets 客户端上(即其他用户发送的消息,服务器现在希望客户端显示这些消息)新传入的消息。

当其中一个客户端发布消息时,会发生以下情况:

  1. 浏览器通过 JavaScript 处理程序捕获“发送”按钮单击,从输入字段(即消息文本)中获取值,并使用连接到我们服务器的 websocket 客户端(在网页初始化时初始化)发出 websocket 消息。
  2. websocket 连接的服务器端组件接收消息并使用广播方法将其转发给所有其他连接的客户端。
  3. 所有客户端都通过在网页中运行的 websockets 客户端组件接收作为推送消息的新消息。然后他们选择消息内容并通过将新消息附加到板来就地更新网页。

image

这是最简单的例子。要获得更强大的解决方案,您可以使用基于 Redis 存储的简单缓存。或者在更高级的解决方案中,一个消息队列来处理消息到客户端的路由,以及一个更强大的传递机制,它可以覆盖临时连接丢失或为离线时注册客户端存储消息。但无论您做出何种改进,Node.js 仍将在相同的基本原则下运行:对事件做出反应、处理许多并发连接并保持用户体验的流畅性。

API ON TOP OF AN OBJECT DB

尽管 Node.js 在实时应用程序中非常出色,但它非常适合从对象数据库(例如 MongoDB)读取数据。JSON 存储数据允许 Node.js 在没有阻抗不匹配和数据转换的情况下运行。

使用 Node.js,您可以简单地使用 REST API 返回您的 JSON 对象以供客户端使用。此外,在从数据库读取或写入时,您无需担心在 JSON 和其他任何内容之间进行转换(如果您使用的是 MongoDB)。总之,您可以通过跨客户端、服务器和数据库使用统一的数据序列化格式来避免多次转换的需要。

排队输入

如果您正在接收大量并发数据,您的数据库可能会成为瓶颈。如上所示,Node.js 本身可以轻松处理并发连接。但是因为数据库访问是一个阻塞操作(在这种情况下),我们遇到了麻烦。解决方案是在数据真正写入数据库之前确认客户端的行为。

使用这种方法,系统可以在重负载下保持其响应能力,这在客户端不需要确认数据写入成功时特别有用。典型的例子包括:用户跟踪数据的记录或写入,批量处理,直到以后才使用;以及不需要立即反映的操作(例如更新 Facebook 上的“喜欢”计数),其中最终一致性(在 NoSQL 世界中经常使用)是可以接受的。

数据通过某种缓存或消息队列基础设施(如 RabbitMQ 或 ZeroMQ)排队,并由单独的数据库批量写入过程或计算密集型处理后端服务消化,这些服务在性能更好的平台上为此类任务编写。类似的行为可以用其他语言/框架实现,但不能在相同的硬件上实现,并具有相同的高吞吐量。

image

简而言之:使用 Node,您可以将数据库写入推到一边并稍后处理它们,就好像它们成功一样继续。

数据流

在更传统的 Web 平台中,HTTP 请求和响应被视为孤立事件;事实上,它们实际上是流。可以在 Node.js 中利用这种观察来构建一些很酷的功能。例如,可以在文件仍在上传时对其进行处理,因为数据是通过流传入的,我们可以以在线方式对其进行处理。这可以用于实时音频或视频编码,以及不同数据源之间的代理

代理人

Node.js 很容易用作服务器端代理,它可以以非阻塞方式处理大量同时连接。它对于代理具有不同响应时间的不同服务或从多个源点收集数据特别有用。

一个例子:考虑一个服务器端应用程序与第三方资源通信,从不同来源提取数据,或者将图像和视频等资产存储到第三方云服务。

尽管确实存在专用代理服务器,但如果您的代理基础设施不存在或者您需要本地开发的解决方案,则使用 Node 可能会有所帮助。我的意思是,您可以使用 Node.js 开发服务器为资产和代理/存根 API 请求构建客户端应用程序,而在生产中,您将使用专用代理服务(nginx、HAProxy 等)处理此类交互)

可以使用 Node.js 的地方

服务器端 Web 应用程序

Node.js 和 Express.js 也可用于在服务器端创建经典的 Web 应用程序。然而,尽管可能,Node.js 将携带呈现的 HTML 的这种请求-响应范式并不是最典型的用例。有赞成和反对这种方法的论据。以下是一些需要考虑的事实:

优点:

  • 如果您的应用程序没有任何 CPU 密集型计算,您可以使用 Javascript 自上而下构建它,如果您使用 JSON 存储对象数据库(如 MongoDB),甚至可以向下构建到数据库级别。这大大简化了开发(包括招聘)。
  • 爬虫会收到一个完全渲染的 HTML 响应,这比单页应用程序或运行在 Node.js 之上的 websockets 应用程序更对 SEO 友好。

缺点:

  • 任何 CPU 密集型计算都会阻止 Node.js 响应,因此线程平台是更好的方法。或者,您可以尝试扩展计算 [*]。
  • 将 Node.js 与关系数据库一起使用仍然很痛苦(有关更多详细信息,请参见下文)。如果您正在尝试执行关系操作,请帮自己一个忙并选择任何其他环境,如 Rails、Django 或 ASP.Net MVC。

[*] 这些 CPU 密集型计算的替代方案是创建一个具有后端处理的高度可扩展的 MQ 支持环境,以保持 Node 作为前端“职员”以异步处理客户端请求。

不应该使用 Node.js 的地方

繁重的服务器端计算/处理

当涉及到繁重的计算时,Node.js 并不是最好的平台。不,您绝对不想在 Node.js 中构建斐波那契计算服务器。一般来说,任何 CPU 密集型操作都会取消 Node 通过其事件驱动的非阻塞 I/O 模型提供的所有吞吐量优势,因为当线程被您的数字运算占用时,任何传入请求都将被阻塞 - 假设您正在尝试在响应请求的同一个 Node 实例中运行计算。

如前所述,Node.js 是单线程的,仅使用一个 CPU 内核。在多核服务器上添加并发性时,Node 核心团队正在以集群模块的形式完成一些工作 [参考:http://nodejs.org/api/cluster.html]。您还可以通过 nginx在反向代理后面非常轻松地运行多个 Node.js 服务器实例

使用集群,您仍然应该将所有繁重的计算卸载到在更合适的环境中编写的后台进程,并让它们通过像 RabbitMQ 这样的消息队列服务器进行通信。

尽管您的后台处理最初可能在同一台服务器上运行,但这种方法具有非常高的可扩展性的潜力。这些后台处理服务可以轻松地分发到单独的工作服务器,而无需配置前端 Web 服务器的负载。

当然,您也可以在其他平台上使用相同的方法,但是使用 Node.js,您可以获得我们讨论过的高请求/秒吞吐量,因为每个请求都是一个非常快速有效地处理的小任务。

结论

我们已经从理论到实践讨论了 Node.js,从它的目标和抱负开始,到它的最佳点和陷阱结束。当人们遇到 Node 问题时,几乎总是归结为这样一个事实:阻塞操作是万恶之源——99% 的 Node 滥用都是直接后果。

请记住:Node.js 的创建从来不是为了解决计算扩展问题。它的创建是为了解决 I/O 扩展问题,它做得非常好。

为什么要使用 Node.js?如果您的用例不包含 CPU 密集型操作,也不访问任何阻塞资源,您可以利用 Node.js 的优势并享受快速且可扩展的网络应用程序。

如何正确学习 node.js

原文 如何正确的学习Node.js

作者 狼叔

git 常用操作

记录一些“常用”的使用方式

.gitignore

有些时候,你必须把某些文件放到 git 工作目录中,但又不能提交它们,例如打包生成的 dist 文件。在 git 工作区的根目录下创建一个特殊的 .gitignore 文件,然后把要忽略的文件名填进去,git 就会自动忽略这些文件。

这里可以看一下 element 的 .gitignore

node_modules
.DS_Store
npm-debug.log
yarn-debug.log
yarn-error.log
lerna-debug.log
npm-debug.log.*
yarn-debug.log.*
yarn-error.log.*
lerna-debug.log.*
lib
.idea
.vscode
examples/element-ui
examples/pages/en-US
examples/pages/zh-CN
examples/pages/es
examples/pages/fr-FR
fe.element/element-ui
.npmrc
coverage
waiter.config.js
build/bin/algolia-key.js
.envrc

忽略原则

  • 忽略操作系统自动生成的文件;
  • 忽略编译生成的中间文件、可执行文件等,也就是如果一个文件是通过另一个文件自动生成的,那自动生成的文件就没必要放进版本库;
  • 忽略你自己的带有敏感信息的配置文件,比如存放口令的配置文件。

文件提交不了

如果配置了 .gitignore 之后,发现有某个文件添加不进去,原因是被忽略了。例如你现在要添加 node_modules,那么就会提示已经被忽略,当然可以通过加上 -f 来强制添加。

$ git add node_modules
The following paths are ignored by one of your .gitignore files:
node_modules
Use -f if you really want to add them.

另外还可以通过 git check-ignore 检查被哪条定义的规则忽略了:

$ git check-ignore -v node_modules
.gitignore:2:node_modules/      node_modules

已经提交过的如何忽略

之前在提交代码时,.gitignore 没有填写完整,例如导致 npm run build 生成的 dist 文件被提交了。然后每次重新打包项目,dist 都会更新,将 dist 填写到.gitignore 规则中,但是运行 git status 的时候,依然能看到这些文件。

这时因为 .gitignore 文件只能作用于 Untracked Files,也就是那些从来没有被 git 记录过的文件(自添加以后,从未 addcommit 过的文件)。而 dist 已经被我们提交过来,git 已经追踪。这时候再修改 .gitignore 是无效的。

解决方法是:

git rm -r --cached dist

然后更新 .gitignore 忽略掉目标文件,最后再提交即可。

查看 commit 中修改的文件

  • git log --name-status:每次修改的文件列表、状态(新增编辑等)

image

  • git log --name-only:每次修改的文件列表

image

  • git log --stat:每次修改的文件列表和文件修改的统计

image

  • git show:最近的一次修改的文件具体内容

image

  • git show -x: 查看最近几次修改的文件具体内容,例如查看最近两次修改内容 git show -2

image

  • git show commitid:查看某个 commit id 对应修改的文件具体内容,例如 git show 4f5e3f4

image

  • git whatchanged:每次修改的文件列表

image

  • git whatchanged --stat:每次修改的文件列表和文件修改的统计

image

参考

Netflix 和摇滚明星

Netflix 高薪

在最近的一次大会上,Netflix 的 CEO 对于“高薪雇佣”给出了自己的观点:Netflix 成立的前几年处于迅速发展时期,需要招纳更多软件工程师,公司很快意识到,开发成功的引擎是一项人才密集型工作,需要市场上最顶尖的雇员。

在硅谷,大多数卓越人才效力于谷歌、苹果和 Facebook,这些技术巨头也为他们开出了很高的薪酬。Netflix 手头的现金不足,难以吸引他们离开自己当前的工作岗位。但作为一名工程师,Netflix 的 CEO 很熟悉早在 1968 年就诞生于软件领域的概念——摇滚明星原则(特指实力超强的工程师)。

摇滚明星

摇滚明星原则来自加利福尼亚圣莫尼卡某地下室中进行的一项著名研究。当天清晨 6 点 30 分,有 9 位开发实习生进入了部署着数十台计算机的房间。每个人都拿到一个草纸信封,其中装有他们需要在 120 分钟之内完成的一系列编码与调试任务。

研究人员已开始在预计,最强程序员的工作效率应该可以达到最差程序员的 2~3 倍。但是事实证明,于后者相比,前者的编码速度可以达到 20 倍,调试速度为 25 倍,程序执行速度则是 10 倍

自这项研究发表以来,整个软件行业都受到了冲击,不少经理人开始探索某些程序员为什么能有能力带来远超其他同行价值与创造结构。

结合当时 Netflix 有限的资金与待完成项目,他们有这清晰的选择:雇用 10 到 15 名普通的工程师,或者把所有预算都用来招揽 1 名摇滚明星。

多年以来,Netflix 发现最顶尖的程序员所带来的价值回报远不止 10 倍,他们的创造力大约是普通程序员的 100 倍。

人们经常引用比尔·盖茨的名言:“一位出色的车床操作员,薪酬可以达到普通车床操作员的数倍;但 一位出色软件开发者的价值,则可以达到普通软件开发者的 10000 倍。”

为创意买单

Netflix 开始考虑这种模式在软件行业之外的应用。摇滚明星级别的工程师比其他同行更有价值应该并非编程工作所独有。这是因为他们更富创造力,能够发现其他人无法理解甚至无法察觉的概念与模式

这些顶尖人才拥有灵活的视角,当人们普遍陷入思维定势时,他们总有办法走出来找到新的、更全面的审视方式。而这也正是一切创意工作都最需要的核心技能。当时担任 Netflix 公司人才总监的 Patty McCord 也由此开始了 Netflix 对于摇滚明星原则的探索之路,开始将工作划分为运营与创意两大类别。

如果希望聘请某人担任运营职位,那么好的员工可能会创造出两倍的价值,但这类职位本身能够实现的价值是有上限的,因此对于运营类角色,支付行业平均薪酬就可以获得非常理想的企业运作效果。

那时是 2003 年,Netflix 资金紧张而且工作压力极大。必须认真思考怎么把这些有限的资金利用好。

最终,Netflix 决定

  • 一切运营角色,只要其工作水平存在明确的上限,公司就只支付相当于市场平均水平的薪酬。
  • 但对于一切创造性工作,Netflix 愿意为人才市场的最顶端员工开出天价工资,而不是把这笔钱花在十几名甚至更多普通员工身上,这也会让公司的劳动力结构更加精简。我们可以依靠一位顶尖人才搞定很多普通人才做得完的工作,但也需要为此付出极高的薪酬。

这也帮助确立了 Netflix 公司日后雇佣员工的基本方式,事实证明这一思路非常成功,整个公司的创新速度与产出都得到了成倍增长。

Netflix 公司的 CEO 还发现,精干的劳动力团队也拥有其他优势,人力管理一直是项老大难问题,需要企业付出大量精力,而管理绩效不佳的员工尤其困难,也往往更加耗时

通过保持组织小型化与团队精简化,每位经理需要管理的人员更少,业务产出反而有所提升。当这些精益团队中的每位成员都非常出色时,经理的规划与指引将更加得心应手、员工处理工作更高效,最终帮助 Netflix 在发展道路上走得更平稳、更顺遂。

参考链接

工作上的一些思考

正好是国庆,睡了个懒觉之后,刷微博是看到了玉伯,点进去把他最近的一些微博都看了,主要是看了一些总结。然后很像知道他对于技术或者产品的一些思考,所以去 github 上看了他的博客,越来越觉得做技术的不要仅仅局限于代码里,要学会协同共赢,要融入业务、关注场景,去做产品而不是项目。以下是从博客了提取或总结出来的,希望也可以给你带来一些思考或解答你的疑问。

需求沟通

作为工程师,有时候会为了细枝末节的特殊 UI 需求而不惜把组件库自己实现一遍,而这样做的原因是因为需求写的就是这样。殊不知有些时候,这都是 UI 设计师的无心之念。当然这样实现也没有问题,但是效率可能不是太高了。

懂得沟通和博弈,学会思辩去看、去接需求。一个能轻松互怼的团队,可以成就彼此,并能做出更好的产品来。

在会议上讨论越激烈的地方,往往越是没有想清楚的地方,达成的结论也是需要再次确认的。真正的思考在会后,要在会议上听取大家的想法,但是在会后自己也要做深度的思考和判断。

技术和产品

技术和产品是可以合二为一的,技术往里钻,本身就是要用做产品的眼光去做,要用产品心态去做,同时要有持久坚持的能力。

如何做基础类产品

基础类产品特指基础技术类产品,比如类库、框架、开发者工具、内部平台等。基础类产品,做好不容易。

  1. 去做产品,而不是做项目

想通过一两个项目来造就一个基础技术类产品,基本是不可能的。项目最多是种下一棵小树苗,而树苗的长大,更需要平平淡淡的日常工作。平时一点一滴的灌溉,不断施肥修整,树苗才能长大成树。

产品永远没有“做完了”。

  1. 学会欣赏,而不是颠覆

看见现有系统的缺点是很容易的,但是看见缺点之后,经常忽略掉它背后的大量优点。对于复杂系统来说,保持现状往往是最正确的选择。当然,这不代表不去改进,或者不能去颠覆。

要解决掉现有系统的不足,更要延续现有系统的有点,否则很容易做出看似勇敢但是路漫漫的决策。

  1. 少做、做好、做通

要懂得说不,有所放弃,有所坚持。

少做不是为了多做,是为了不做。保持适当的慢,才能确保以后的快。

坚持做少,才有机会做好。好不光在技术上,还要与场景紧密结合,真正服务好使用者。

做通是在做好的基础上,能用技术驱动创新,能触类旁通,能由点及面。由深度带来广度,以一渗百。同是平台化、体系化。

  1. 顺势而为,而不是逆流而上

对技术人员来说,逆流而上很吸引人,但是充满着危险。顺势而为是保持对业业界的关注。技术变化很快,新**、新潮流未必代表什么。但是对社区保持适量的关注,往往能够节省团队的时间。

比如,对于前端来说,我们可以继续使用 Ant 或 Maven 等工具方案。但是如果能看到 Node.js 的兴起,能适时跟进 Grunt 等社区,那我们的很多工作都可以省掉。通过成熟的方案,稍加定制,就可以达成目标。

顺势可以走的更快,不光速度快,心情也愉快。

  1. 追求小而美,做好生态圈

很多基础类产品,做出来相对容易,但是维护起来可是一场噩梦。你所在的公司,是否会有一两个系统,常年需要那么一两个固定的人员或团队持续维护?

好的产品成年之后,应该能自我前行。

小而美是事物的形态。小意味着成本、可替换性和可维护性等方面的优势;美意味着功能的完备与稳定。就如 shell 的命令一样,一旦成熟之后,可以十几年甚至永远不用再更新。

生态圈的形成,可以让良币驱除劣币,适者生存。一旦生态圈形成之后,产品就有了生命,就活跃灵动起来了。

开源精神

  1. 拿来主义

懂得从现有成熟开源项目中去挑选符合自己需求的项目,直接拿来用。程序员容易翻一个错误,就是什么东西都想自己造,或者对别人造的,浅尝辄止判断别人的不行。

真正的拿来主义,需要一颗谦卑的心,在的过程中,需要去看文档,甚至去读源码,这些过程,对于程序员的技能增长都很有帮助。很多程序员技能的提升,并非写的代码太少,而是看的代码不够多。懂得去看、去理解、去用,是买入开源世界的第一步。

  1. 参与比主导更重要

开源世界里永远没有完美的项目。当你学会了拿来主义之后,在使用开源项目时,肯定会遇到 bug,或者特性不满足。这时,你可以自己新开一个项目,也可以参与到该开源项目中去,帮助作者一起来完善。绝大多说项目来说,参与进去帮助完善是更明智的选择。参与的过程中,你的功力往往也会大增。不管是技术上的进步,还包括英语读写能力。在人性沟通上,你也会收获很多,这是无价的财富。

  1. 重视社区

除了代码,还有文档、测试用例、issue 管理、版本发布、升级策略等等。尽可能让社区活跃起来,社区形成之后,开源才活起来,否则就是死开源。

前端开发中重要的是什么

前端开发是距离用户最近的编程工作。一个优秀的前端工程师,一定要对产品有爱。如果做的产品自己都不怎么用,那么你对很多交互细节就有可能缺乏深思,它们会在你的潜意识里被忽略掉。但是,如果你自己也用这个产品,那么那就不仅仅是在编码了,你同时还是 PD、测试等角色,这种感觉是很奇妙的。

全栈

有这么一批人,他们对软件开发的很多层未必精通,但对每一层都很熟悉,他们对软件技术充满热情,这种人就是所谓的全栈工程师。

这里的每一层指的是:

  1. 服务器、网络和运维。
  2. 数据模型。
  3. 业务逻辑。
  4. API 层、Action 层和 MVC。
  5. UI 层。
  6. 用户体验。
  7. 理解用户与商业需求。

如果对以上 7 层都很熟悉,同时精通一二,就是全栈工程师了。这样看来,不管是前端还是后端工程师,只要有追妻,除了自己工作重点对应的层之外,对其他的层也会有些了解。单栈工程师是很好很少的。

回到全栈工程师的定义,可以分解为三点:

  1. 精通若干层。
  2. 熟悉所有层。
  3. 对软件技术充满热情。

第 3 点很重要,未必刻意让自己熟悉所有层,但是能对软件技术充满热情,那么遇到陌生的领域,就会有能力去快速学习,从而慢慢地自然的熟悉所有层,莫名成为的全栈工程师。

全栈工程师不是给自己设限,而是永远保持激情和学习欲望。另外全栈不会违背社会的分工。在软件开发领域,分工依旧是提高效率的重要手段。但是分工之后,还有影响效率的一个重要因素是分工是否合理

全栈视角可以让我们重新去审视、去思考各个角色适合去做什么,从而有可能促进更合理的分工协作。一旦发现了更合适的分工,需要对分工做出调整时,全栈就是一种自然而然的要求了。

眼中的技术高手

  1. 真正的语言高手

真正的语言高手不多,可能我们自己这辈子都成不了语言高手。JavaScript(纯语言,不包括 DOM)高手,在国内屈指可数。周爱民、白露飞、老赵、winter、月影、hax 等等。还有一些低调的隐士,这些人读 ECMAScript 规范就像啃瓜子一样轻松。你我等闲之辈,除了佩服之外,只能去谈恋爱。

工作中,我们需要语言高手吗?肯定的说,需要。可是,我们需要大量的语言高手吗?除了特殊岗位,我相信很多公司不需要。

  1. 我们的价值

Douglas Crockford 的 JS 能力很可能不及 winter,但 Douglas 规范并布道了 JSON 格式,天下留名,惠泽全球。

Jeremy Ashkenas 的 JS 能力可能还不如老赵,但 Jeremy 用很裸的代码写就了 Backbone,至少影响了一万人,给各个公司创造的价值总额很可能过千万美刀。

我么需要语言高手,我们需要这种精神,但是这仅仅是很小很小的领域。在这个领域里,永远有比你更聪明的人。

具体对 JavaScript 语言来说。搞清楚数据类型、作用域、闭包、原型链等基本概念,足矣。再深入进去,对绝大部分人来说,除了满足心理上的优越感,对实际工作没有任何实质的帮助。

语言的本质和互联网一样,只是工具,是剪刀、石头、布。让张小泉去研究怎么做剪刀就好,我们用好剪刀,去剪出各种窗花,更有意思。还有一个有趣的事实是,张小泉会造剪刀,但是剪不好窗花。

从这种思维中跳出来,天大地大。永远不要妄自菲薄,每个人身上都背负着独特的使命。去努力寻找自己的,不要老盯着别人的,否则就会成为观众。

另外还有很多吐槽的点:

  • 源码只是很小很小的一部分。直接读源码往往无法领会类库框架的精髓。不读源码用心去用,用时间去体味,偶尔针对性的看看源码,往往更能掌握一个类库框架的真谛。
  • 对社区的贡献还有很多。用心的 bug 提交、pull request、一个认真的评论,这些都是有价值的。
  • 一个 Java 高手如果说他会原生的 Java,那一定会遭到很多人的围观。语言之外的领域知识,才真正造就了高手。对于前端来说,会原生 JS 只能打 20 分,另外 40 分需要你深入使用 CSS、DOM、HTML5 等领域知识。还有 20 分需要你对业务需求、架构设计等有真正的运用,这已经 80 分了。剩下的 20 分,只有两个字:“勤奋”。

成为技术大牛

知识、导师和环境。

有了互联网,对技术人员来说,这个三个条件还是比较容易具备的。带上甄别的眼睛,网络上充满着知识,比如很多优秀的书籍,各种技术文档和博客,只要用心,你就能发现。

通过互联网,也将各个公司的技术高手直接变为我们的导师。比如很多优秀的开源社区,只要你懂得合理提问,就会有技术大拿很热心的帮助你。

作为程序员,可以通过 github、StackOverflow 等,和大家一起讨论、解决问题。

除了互联网,在现实工作中,也可以尽量创造者三个条件。去一个你认可的团队,去接近你心中的大牛,只要迈出第一步,就不会太难。只要多那么一点点勇气和决断。

努力赶路吧自己。

Ajax

2005 年 Jesse James Garrett 发表了一篇在线文章 “Ajax:A new Approach to Web application”。他在这篇文章中介绍了一种新的技术,就叫 Ajax( Asynchronous Javascript + XML)。这一技术能够解决向服务器发送额外的数据请求而无需卸载页面,会带来更好的体验。

Ajax 的核心就是 XMLHttpRequest 对象(简称 XHR),这是微软首先引入的一个特性,其他的浏览器提供商后来都提供了相同的实现。XHR 为想服务器发送请求和解析服务器响应提供了流畅的接口。能够以异步的方式从服务器取得更多的信息,意味着用户单击之后,可以不必刷新页面也能取得数据。

如果提交数据之后,页面刷新了,那得多烦。

接下看看怎么实现一个 Ajax。

XMLHttpRequest 对象

使用 XHR 对象时,要调用的第一方法就是 open()

open

open() 方法接收 3 个参数:

  1. 要发送的请求类型(get、post 等);
  2. 请求的 URL;
  3. 表示是否是异步发送的布尔值。

例如:

var xhr = new XMLHttpRequest();
xhr.open('get', 'example.php', false);

这段代码会启动一个针对 example.php 的 GET 请求。这里还需要注意两点:一是 URL 相对于执行代码的当前页面(当然也可以使用绝对路径);二是调用 open() 方法并不会真正的发送请求,而只是启动一个请求以备发送

关于第一点,浏览器规定,只能向同一个域中使用相同端口和协议的 URL 发送请求。如果 URL 与启动请求的页面有任何差别,都会引发安全错误,也就是跨域了

要发送特定的请求,还需要调用 send() 方法。

send

要发送上面的请求,需要这样调用:

var xhr = new XMLHttpRequest();
xhr.open('get', 'example.php', false); // 发送的是同步请求
xhr.send(null);

send() 方法接收一个参数:请求主体发送的数据。如果不需要通过请求主体发送数据,则必须传入 null,因为这个参数对有些浏览器来说是必须的。

调用 send() 之后,请求就会被分派到服务器。

这里发送的是一个同步请求,JavaScript 代码会等到服务器响应之后再继续执行。

HXR 的属性

收到响应之后,响应的数据会自动填充到 XHR 对象的属性,相关属性有:

  • responseText:作为响应主体被返回的文本;
  • responseXML:如果响应的内容类型是“text/html”或“application/xml”,这个属性中将保存包含着响应数据的 XML DOM 文档;
  • status:响应的 HTTP 状态;
  • statusText:响应的状态说明。

接收到响应之后,应该先检查 status 属性,以确定响应已经成功返回。一般来说,可以将状态码为 200 作为成功的标志。此时 responseText 属性也已经包含响应内容。此外,状态码 304 表示请求资源并没有修改,可以直接使用浏览器中缓存的版本。

var xhr = new XMLHttpRequest();
xhr.open('get', 'example.php', false); // 发送同步请求
xhr.send(null);
if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
  alert(xhr.responseText);
} else {
  alert("Request was unsuccessful:" + xhr.status);
}

异步请求

前面这样发送同步请求没有问题,但是大多数情况下,我们还是发送异步请求,才能让 JavaScript 继续执行而不必等待响应。可以通过检查 XHR 对象的 readyState 属性,该属性表示请求/响应过程的当前活动阶段。这个属性可取值如下:

  • 0:未初始化。尚未调用 open() 方法;
  • 1:启动。已经调用 open() 方法,但尚未调用 send() 方法;
  • 2:发送。已经调用 send() 方法,但尚未收到响应;
  • 3:接收。已经接受到部分响应数据;
  • 4:完成。已经接受到全部响应数据,而且已经可以在客户端使用了。

知道 readyState 的值发生变化,就会触发一次 readystatechange 事件。所以我们可以通过这个事件来检查状态变化后的 readyState 的值。只要 readyState 等于 4 就说明所有的数据已经准备就绪了。

不过,必须在调用 open() 方法之前指定 onreadystatechange 事件的处理方法才能确保跨浏览器兼容性。看一个例子:

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4) { // 状态只能通过 xhr 本身获取,事件没有 event 对象
    if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
      alert(xhr.responseText);
    } else {
      alert("Request was unsuccessful:" + xhr.status);
    }
  }
};
xhr.open('get', 'example.php', true); // 发送异步请求
xhr.send(null);

实现一个 Ajax

通过上面的知识,可以自己实现一个简单的 Ajax 请求的方法了。

function success(text) {
  var textarea = document.getElementById('test-response-text');
  textarea.value = text;
}

function fail(code) {
  var textarea = document.getElementById('test-response-text');
  textarea.value = 'Error code: ' + code;
}

var request = new XMLHttpRequest(); // 新建 XMLHttpRequest 对象

request.onreadystatechange = function () { // 状态发生变化时,函数被回调
  if (request.readyState === 4) { // 成功完成
    // 根据 HTTP 状态判断响应结果
    if (request.status === 200) {
      // 成功,通过 responseText 拿到响应的文本
      return success(request.responseText);
    } else {
      // 失败,根据响应码判断失败原因
      return fail(request.status);
    }
  } else { // HTTP请求还在继续
    // ...
  }
}

// 发送请求:
request.open('get', 'example.php', true); 
request.send(null);

参考

整理前端的重要博客

Q&A 你是否知道 JS

你是否知道 JS

解答和理解一些“奇怪”的 JavaScript 问题,可能不是很常用,但是也可以帮助我们理解这么语言。

  1. 浏览器控制台上会打印什么?
var a = 10;
function foo() {
    console.log(a); // ??
    var a = 20;
}
foo();

A: undefined

使用 var 关键字声明的变量在 JavaScript 中会被提升,并在内存中分配值为 undefined。但初始化是在赋值的地方。另外,var 声明的变量是函数作用域,而 letconst 是块作用域。实际的过程是下面这样

var a = 10;
function foo() {
  var a; // var声明被提升到函数的顶部
  console.log(a); // undefined
  a = 20; // 实际这个时候才初始化
}
  1. 如果我们使用 let 或 const 代替 var,输出是否相同?
var a = 10;
function foo() {
    console.log(a); // ??
    let a = 20;
}
foo();  

A:ReferenceError: a is not defined

ES6 明确规定,如果区块中存在 letconst 命令,这个区块对这些命令声明的变量,从一开始就形成封闭的作用域。凡是在声明之前就使用这些变量,就会报错。

总之,在代码块内,使用 let 命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死去”(temporal dead zone 简称 TDZ)。

if (true) {
  // TDZ开始
  tmp = 'abc'; // ReferenceError
  console.log(tmp); // ReferenceError
  
  let tmp; // TDZ 结束
  console.log(tmp); // undefined

  tmp = 123;
  console.log(tmp); // 123
}
  1. “newArray”中有哪些元素?
var array = [];
for(var i = 0; i <3; i++) {
 array.push(() => i);
}
var newArray = array.map(el => el());
console.log(newArray); // ? 

A:[3, 3, 3]

在 for 循环的头部声明带有 var 关键字的变量会为该变量创建单个绑定(存储空间),再看一下 for 循环

var array = [];
for (var i = 0; i < 3; i++) {
  // 箭头函数中的每个 i 最终都指向同一个
  // 所以最后输出的都是3
  array.push(() => i);
}

如果使用 let,则在每次循环中都创建一个新的变量

var array = [];
for (let i = 0; i < 3; i++) {
  // 箭头函数中的i都指向不同的绑定
  // 所以,返回的是当时的值
  array.push(() => i);
}

这里还可以使用闭包

let array = [];
for (var i = 0; i < 3; i++) {
  array[i] = (function (x) {
    return function() {
      return x;
    }
  })(i)
}
  1. 如果我们在浏览器控制台中运行'foo'函数,是否会导致堆栈溢出错误?
function foo() {
  setTimeout(foo, 0); // 是否存在堆栈溢出错误?
};    

A:不会溢出

JavaScript 并发模型基于“事件循环”。

浏览器的主要组件包括:调用堆栈、事件循环、任务队列和 Web API。像 setTimeoutsetIntervalPromise 这样的全局函数不是 JavaScript 的一部分,而是 Web API 的一部分。

image

调用栈是后进先出(LIFO)的,引擎每次从堆栈中取出一个函数,然后从上往下依次执行代码。每当遇到一些异步代码,例如 setTimeout,它就把它交给 Web API,因此,等到事件触发时,callback 就会被添加到任务队列中。

事件循环(Event Loop)不断的检查任务队列(Task Queue),并按照它们的顺序一次处理一个 callback。**每当调用栈(call stack)为空时,事件循环就获取一个 callback 放入到调用栈中进行处理。

请记住,如果调用栈不是空的,事件循环不会将任何回调推入堆栈。

现在看看这道题

  • foo() 会将函数放入调用栈(call stack)
  • 执行的时候遇到 setTimeout,然后会将它的回调函数 foo 传给 Web API 并从函数中返回,此时调用栈为空。
  • 定时器被设置为 0,foo 将被添加到任务队列
  • 由于调用栈时空的,事件循环会选择 callback foo,将其放到调用栈中处理
  • 进程再次重复,堆栈不会溢出。
  1. 如果在控制台中运行以下函数,页面(选项卡)的 UI 是否仍然响应?
function foo() {
  return Promise.resolve().then(foo);
};  

A:不会响应

实际上任务队列并非只有一个,我们可以有多个任务队列。由浏览器选择其中一个队列并处理其中的回调。

setTimout 属于宏任务Promise 属于微任务。

主要区别是,宏任务在单个周期中一次一个的推入堆栈,但是微任务总是在执行后返回到事件循环之前清空。因此,你已处理条目的速度向这个队列添加条目,那么你就永远在处理微任务。只要当微任务队列为空时,事件循环才会渲染页面。

function foo() {
  return Promise.resolve().then(foo);
}

每次调用 foo 都会在微任务队列上添加另一个 foo 回调,因此事件循环无法继续处理其他事情(滚动,单击等),直到该队列为空,所以会阻止渲染。

  1. 我们能否以某种方式为下面的语句使用展开运算而不导致类型错误?
var obj = { x: 1, y: 2, z: 3 };
[...obj]; // TypeError 

展开语法和 for...of 遍历的时候,该数据结构需要部署了 Symbol.iterator,即需要具有 iterator 接口。Object 不具有该接口,它是不可迭代的。

要想它是迭代的,这意味者需要它(或者它的原型链)上必须带有 Symbol.iterator

var obj = {x: 1, y: 2, z: 3};
obj[Symbol.iterator] = function*() {
  yield 1;
  yield 2;
  yield 3;
}
[...obj]; // [1, 2, 3]
  1. 运行以下代码片段时,控制台上会打印什么?
var obj = { a: 1, b: 2 };
Object.setPrototypeOf(obj, {c: 3});
Object.defineProperty(obj, 'd', { value: 4, enumerable: false });

// what properties will be printed when we run the for-in loop?
for(let prop in obj) {
    console.log(prop);
}  

A:a,b,c

for-in 循环遍历对象本身的可枚举属性以及对象从其原型继承的属性。可枚举属性是可以在 for-in 循环期间包含和访问的属性。

var obj = { a: 1, b: 2 };
var descriptor = Object.getOwnPropertyDescriptor(obj, "a");
console.log(descriptor.enumerable); // true
console.log(descriptor);
// { value: 1, writable: true, enumerable: true, configurable: true }
var obj = { a: 1, b: 2 }; //a,b 都是 enumerables 属性

// 将{c:3}设置为'obj'的原型,for-in 循环也迭代 obj 继承的属性
// 所以c可以被访问。
Object.setPrototypeOf(obj, { c: 3 });

// 我们在'obj'中定义了另外一个属性'd'
// 但是 将'enumerable'设置为false。 这意味着d不是可枚举的。
Object.defineProperty(obj, "d", { value: 4, enumerable: false });

for (let prop in obj) {
  console.log(prop);
}
// a, b, c
  1. xGetter() 会打印什么值?
var x = 10;
var foo = {
  x: 90,
  getX: function() {
    return this.x;
  }
};
foo.getX(); // prints 90
var xGetter = foo.getX;
xGetter(); // prints ?

A:10

var xGetter = foo.getX; xGetter 是 foo.getX 的一个引用。执行 xGetter,这是 this 指向全局作用域window,所以输出 10。要想获取 foo.x 可以使用 bindcall 或者 apply

xGetter.bind(foo)(); // 90
xGetter.call(foo); // 90
xGetter.apply(foo); // 90
  1. 下面代码输出结果?
var a = 10;
a.pro = 10;
console.log(a.pro + a);

var b = 'hello';
b.pro = 'world';
console.log(b.pro + b);

A:输出结果是 NaN undefinedhello

JavaScript 引擎内部在处理对某个基本类型 a 进行形如 a.pro 的操作时,会在内部临时创建一个对应的包装类型(对数字类型来说就是Numbe r类型)的临时对象,并把对基本类型的操作代理到对这个临时对象身上,使得对基本类型的属性访问看起来像对象一样。但是在操作完成后,临时对象就销毁了,下次再访问时,会重新建立临时对象,当然就会返回 undefined 了。

  1. 下面代码的输出结果?
var f = 1;
if (!f) {
  var a = 10;
}
function fn() {
  var b = 20;
  c = 30;
}

fn();
console.log(a);
console.log(c);
console.log(b);

A:输出结果是 undefined, 30, Uncaught ReferenceError: b is not defined

1、没有用 var 声明的是全局变量,即便在函数内部;当然在严格模式下会报错
2、var 声鸣的变量,只有在 function 内部新声明的才是局部变量,在 if,while,for 等声明的变量其实是全局变量(除非本身在function内部)
3、因为变量提升,虽然 if 块的内容没执行,但是预解析阶段会执行var a,只是没有赋值而已,因此打印 a 是 undefined 而打印 b 会报错

  1. 下面的代码的输出结果?
var length = 10;
function fn() {
  console.log(this.length);
}
var obj = {
  length: 5,
  method: function(fn) {
    fn();
    arguments[0]();
  }
}

obj.method(fn, 1);

A:输出结果是 10, 2

第一次输出 10 应该没有什么异议,这里的 this 指向 window,第二个调用 arguments[0]() 相当于执行 arguments 调用方法,this 指向 arguments,而这里传了两个参数,故输出 arguments 长度为2。

  1. 下面的代码的输出结果?
function fn(a) {
  console.log(a);
  var a = 2;
  function a() {};
  console.log(a);
}
fn(1)

A:输出结果是 function a(){}, 2

知道预解析阶段,变量声明和函数声明会提前,且变量名和函数名同名时,函数优先级高于变量,会覆盖变量,或者说,函数提升要比变量提升的优先级要高一些,且不会被变量声明覆盖,但是会被变量赋值之后覆盖。因此第一个输出的是f a(){ },继续执行,会执行a=2,再输出的时候就会输出2。

  1. a 在什么情况下,控制台可以输出 hello world?
a = ?
if (a == 1 && a == 2 && a == 3) {
  console.log('hello world');
}

A:这里需要知道,比较相等时,会发生类型转换。如果是对象,则会调用对象的 valueOf() 方法,得到一个返回值,如果返回值是一个基本类型,则按照转换规则继续转换比较;否则调用对象的 toString() 方法,然后依照前面的规则转换返回的字符串。

a = {
  num: 0,
  valueOf: function() {
    return ++this.num;
  }
}

// 或者
a = {
  num: 0,
  toString: function() {
    return ++this.num;
  }
}

// 或者
a = {
  num: 0,
  valueOf: function() {
    return {};
  },
  toString: function() {
    return ++this.num;
  }
}

参考

浏览器的回流和重绘

在讨论回流和重绘之前,先了解一下:

  1. 浏览器使用流式布局模型
  2. 浏览器会把 HTML 解析成 DOM,把 CSS 解析成 CSSOMDOMCSSOM合并就产生了 Render Tree
  3. 有了 Render Tree,就可以每个节点对应的样式,然后计算他们在页面的大小和位置,最后把节点绘制在页面上。
  4. 由于浏览器使用流式布局,对 Render Tree 的计算通常只需要遍历一遍就可以完成了,但是 table 及其内部的元素除外。
    他们可能需要多次计算,通常需要3倍于同等元素的时间,这也是为什么要避免使用 table 布局的原因之一。

我们可能已经从一些资料上了解到,当一个元素的可见性 visibility 发生改变的时候,重绘也随之发生,但是不影响布局。类似的属性还有 outlinebackground-color等。重绘的代价是高昂的,因为浏览器必须验证 DOM 树上其他节点元素的可见性。而回流更是影响性能的主要原因是它涉及部分页面或者整个页面的布局。

回流(重排)

Render Tree 中部分或者全部元素的尺寸、结构或某些属性发生改变时,浏览器重新渲染部分或者全部文档的过程称为回流。

ex:

<body>
  <div class="error">
    <h1>我的账户</h1>
    <p><strong>错误:</strong>账户不存在...</p>
    <h2>忘记账户?</h2>
    <ol>
      <li>第一步</li>
      <li>第二步</li>
    </ol>
  </div>
</body>

上面示例中,对段落标签(<p>)会留将会引发强烈的回流,因为它是一个子节点,这也导致祖先的回流(div.errorbody 视浏览而定)。此外,<h5><ol>也会有简单的回流,因为它们在 DOM 中的回流元素之后。

会导致回流的操作

  • 页面首次渲染
  • 浏览器窗口大小发生改变
  • 元素尺寸或位置发生改变
  • 元素内容的变化(文字数量或图片大小等等)
  • 元素字体大小变化
  • 脚本操作 DOM(添加或删除可见 DOM 元素等)
  • 操作 class 属性
  • 激活 CSS 伪类(例如::hover
  • 查询某些属性或者调用某些方法

常用且会导致回流的属性和方法

  • clientWidthclientHeightclientTopclientLeft
  • offsetWidthoffsetHeightoffsetTopoffsetLeft
  • scrollWidthscrollHeightscrollTopscrollLeft
  • getComputedStyle()
  • getBoundingClientRect()
  • scrollTo()
  • scrollIntoView()scrollIntoViewIfNeeded()

重绘

当页面中元素的样式改变并不影响它在文档流中的位置时(例如:colorbackground-colorvisibility 等)浏览器会将新样式赋给元素并重新绘制它,这个过程称为重绘

性能影响

回流的代价比重绘高。回流必将引起重绘,重绘不一定会引起回流。

有时候即使仅仅回流一个单一的元素,它的父元素以及任何跟随它的元素也会产生回流。现代浏览器对频繁的回流和重绘进行了优化。

浏览器会维护一个队列,把所有引起回流和重绘的操作放入队列中,如果队列中的任务数量或者时间间隔达到一个阈值,浏览器就会将队列清空,进行一次批处理,这样就可以把多次回流和重绘变成一次。

当你访问下面的属性或者方法的时候,浏览器会立即清空队列

  • widthheight
  • clientWidthclientHeightclientTopclientLeft
  • offsetWidthoffsetHeightoffsetTopoffsetLeft
  • scrollWidthscrollHeightscrollTopscrollLeft
  • getComputedStyle()
  • getBoundingClientRect()

因为队列中可能会有影响这些属性或方法返回值的操作,即使你希望获取的信息可能与队列中的操作引发的改变无关,浏览器也会强行清空队列,确保拿到的值是最精确的。

如何避免回流或者将它们对性能的影响降到最低

CSS的使用

  1. 如果想设定元素的样式,通过改变元素的 class 名,并尽可能在 DOM 树的最末端改变 class
  2. 避免设置多项内联样式。
  3. 将动画效果应用到 position 属性为 absolutefixed 的元素上。
  4. 权衡平滑和速度。
  5. 避免使用 table 布局。
  6. 避免使用 CSS 的 JavaScript 表达式(例如 calc() )。

尽可能在 DOM 树的最末端改变 class

回流可以自上而下,或自下而上的回流的信息传递给周围的节点。回流是不可避免的,但可以减少其影响。尽可能在 DOM 树的里面改变 class,可以限制了回流的范围,使其影响尽可能少的节点。例如,你应该避免通过改变对包装元素类去影响子节点的显示。面向对象的 CSS 始终尝试获得它们影响的类对象(DOM 节点或节点),但在这种情况下,它已尽可能的减少了回流的影响,增加性能优势。

避免设置多项内联样式

我们都知道与 DOM 交互很慢。我们尝试在一种无形的 DOM 树片段组进行更改,然后整个改变应用到 DOM 上时仅导致了一个回流。同样,通过 style 属性设置样式导致回流。避免设置多级内联样式,因为每个都会造成回流,样式应该合并在一个外部类,这样当该元素的 class 属性可被操控时仅会产生一个 reflow。

动画效果应用到position属性为absolute或fixed的元素上

动画效果应用到position属性为absolute或fixed的元素上,它们不影响其他元素的布局,所它他们只会导致重新绘制,而不是一个完整回流。这样消耗会更低。

牺牲平滑度换取速度

Opera 还建议我们牺牲平滑度换取速度,其意思是指您可能想每次 1 像素移动一个动画,但是如果此动画及随后的回流使用了100% 的 CPU,动画就会看上去是跳动的,因为浏览器正在与更新回流做斗争。动画元素每次移动 3 像素可能在非常快的机器上看起来平滑度低了,但它不会导致 CPU 在较慢的机器和移动设备中抖动。

避免使用table布局

避免使用 table 布局。可能您需要其它些避免使用 table 的理由,在布局完全建立之前,table 经常需要多个关口,因为 table 是个和罕见的可以影响在它们之前已经进入的DOM 元素的显示的元素。想象一下,因为表格最后一个单元格的内容过宽而导致纵列大小完全改变。这就是为什么所有的浏览器都逐步地不支持 table 表格的渲染。然而有另外一个原因为什么表格布局时很糟糕的主意,根据Mozilla,即使一些小的变化将导致表格(table)中的所有其他节点回流。

避免使用 CSS 的 JavaScript 表达式

这项规则较过时,但确实是个好的主意。主要的原因,这些表现是如此昂贵,是因为他们每次重新计算文档,或部分文档、回流。正如我们从所有的很多事情看到的:引发回流,它可以每秒产生成千上万次。

JavaScript的使用

  1. 避免频繁操作样式,最好一次性重写 style 属性,或者将样式列表定义为 class 并一次性更改 class 属性。
  2. 避免频繁操作 DOM,创建一个 documentFragment,在它上面应用所有 DOM 操作,最后再把它添加到文档中。
  3. 也可以先为元素设置 display: none,操作结束后再把它显示出来。因为在 display 属性为 none 的元素上进行的 DOM 操作不会引发回流和重绘。
  4. 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
  5. 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

参考

Webpack实现多用户配置

多用户配置

在开发中,如果项目代码中,面对多个客户,现在需要根据不同客户打包的不同的代码,需要怎么配置呢?

平常的开发中一些需求也是比较类似的,例如下面这段代码,根据不同环境,配置不同的接口地址。它们都是需要根据环境变量来做不同的配置,从而实现打包的时候进行区分。

let baseurl;
if (process.env.NODE_ENV === 'development') {
    baseurl = 'http://api.development.com';
} else {
    baseurl = 'http://api.production.com';
}
export default baseurl;

webpack的模式

开发环境(development)和生产环境(production)的构建目标差异很大。在开发环境中,我们需要具有强大的、具有实时重新加载(live reloading)或热模块替换(hot module replacement)能力的 source map 和 localhost server。而在生产环境中,我们的目标则转向于关注更小的 bundle,更轻量的 source map,以及更优化的资源,以改善加载时间。由于要遵循逻辑分离,通常为每个环境编写彼此独立的 webpack 配置

虽然,以上我们将生产环境和开发环境做了略微区分,但是,请注意,我们还是会遵循不重复原则(Don't repeat yourself - DRY),保留一个“通用”配置。为了将这些配置合并在一起,我们将使用一个名为 webpack-merge 的工具。通过“通用”配置,我们不必在环境特定(environment-specific)的配置中重复代码。

webpack4配置mode

提供 mode 配置选项,告知 webpack 使用相应模式的内置优化mode的默认值是production

  1. 只在配置中提供mode选项:
module.exports = {
  mode: 'production'
};
  1. 从 CLI 参数中传递:
webpack --mode=production
选项 描述
development 会将 process.env.NODE_ENV 的值设为 development。启用 NamedChunksPlugin 和 NamedModulesPlugin。
production 会将 process.env.NODE_ENV 的值设为 production。启用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin 和 UglifyJsPlugin.
none 不选用任何默认优化选项

可以看到mode实际就是设置process.env.NODE_ENV的值。

webpack 3.12.0,以上的配置方法都是不适用的。

const devWebpackConfig = merge(baseWebpackConfig, {
  mode: 'development',
  module: {
    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
  },
  ...
// 报错信息
Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema.
 - configuration has an unknown property 'mode'. These properties are valid:
...

使用第二种方式,同样没有成功

node_modules/.bin/webpack --mode=development
...
Unknown argument: mode

在webpack3的配置

许多 library 将通过与 process.env.NODE_ENV 环境变量关联,以决定 library 中应该引用哪些内容。例如,当不处于生产环境中时,某些 library 为了使调试变得容易,可能会添加额外的日志记录(log)和测试(test)。其实,当使用 process.env.NODE_ENV === 'production' 时,一些 library 可能针对具体用户的环境进行代码优化,从而删除或添加一些重要代码。

webpack3需要使用 webpack 内置的 DefinePlugin 为所有依赖定义这个变量:

webpack.dev.conf.js

+ const webpack = require('webpack');
  const merge = require('webpack-merge');
  const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
  const common = require('./webpack.common.js');

  module.exports = merge(common, {
    devtool: 'cheap-module-source-map',
    plugins: [
      new UglifyJSPlugin({
        sourceMap: true
      }),
      new webpack.DefinePlugin({
        'process.env': {
          'NODE_ENV': JSON.stringify('production')
        }
      })
    ]
  })

如果您正在使用像 react 这样的 library,那么在添加此 DefinePlugin 插件后,你应该看到 bundle 大小显著下降。还要注意,任何位于 /src 的本地代码都可以关联到 process.env.NODE_ENV 环境变量,所以以下检查也是有效的:

  import { cube } from './math.js';

  if (process.env.NODE_ENV !== 'production') {
    console.log('Looks like we are in development mode!');
  }

  function component() {
    var element = document.createElement('pre');

    element.innerHTML = [
      'Hello webpack!',
      '5 cubed is equal to ' + cube(5)
    ].join('\n\n');

    return element;
  }

  document.body.appendChild(component());

技术上讲,NODE_ENV 是一个由 Node.js 暴露给执行脚本的系统环境变量。通常用于决定在开发环境与生产环境(dev-vs-prod)下,服务器工具、构建脚本和客户端 library 的行为。然而,与预期不同的是,无法在构建脚本 webpack.config.js 中,将 process.env.NODE_ENV 设置为 "production",请查看 #2537。因此,例如 process.env.NODE_ENV === 'production' ? '[name].[hash].bundle.js' : '[name].bundle.js' 这样的条件语句,在 webpack 配置文件中,无法按照预期运行。

也就是说,在 webpack.config.js 里面我们是无法拿到 process.env.NODE_ENV 的。但是可以换种方式来设置配置信息,可以参考 issue 里这个解决方法

DefinePlugin

DefinePlugin 允许创建一个在编译时可以配置的全局常量。这可能会对开发模式和发布模式的构建允许不同的行为非常有用。如果在开发构建中,而不在发布构建中执行日志记录,则可以使用全局常量来决定是否记录日志。这就是 DefinePlugin 的用处,设置它,就可以忘记开发和发布构建的规则。

new webpack.DefinePlugin({
  // Definitions...
});

用法

每个传进 DefinePlugin 的键值都是一个标志符或者多个用 . 连接起来的标志符。

  • 如果这个值是一个字符串,它会被当作一个代码片段来使用。
  • 如果这个值不是字符串,它会被转化为字符串(包括函数)。
  • 如果这个值是一个对象,它所有的 key 会被同样的方式定义。
  • 如果在一个 key 前面加了 typeof,它会被定义为 typeof 调用。

这些值会被内联进那些允许传一个代码压缩参数的代码中,从而减少冗余的条件判断。

new webpack.DefinePlugin({
  PRODUCTION: JSON.stringify(true),
  VERSION: JSON.stringify('5fa3b9'),
  BROWSER_SUPPORTS_HTML5: true,
  TWO: '1+1',
  'typeof window': JSON.stringify('object')
});
console.log('Running App version ' + VERSION);
if(!BROWSER_SUPPORTS_HTML5) require('html5shiv');

process.env

process 对象是一个 global (全局变量),提供有关信息,控制当前 Node.js 进程。作为一个对象,它对于 Node.js 应用程序始终是可用的,故无需使用 require()。

返回用户的环境信息,返回的对象类似如下:

{
  TERM: 'xterm-256color',
  SHELL: '/usr/local/bin/bash',
  USER: 'maciej',
  PATH: '~/.bin/:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin',
  PWD: '/Users/maciej',
  EDITOR: 'vim',
  SHLVL: '1',
  HOME: '/Users/maciej',
  LOGNAME: 'maciej',
  _: '/usr/local/bin/node'
}

在[这篇文章](https://www.jianshu.com/p/ce8f405935b9),列出了一些设置这些环境信息的方法,不同的环境存在这差异。

设置NODE_ENV这样的环境变量

  1. 通过DefinePlugin设置

src/下都可以获取到,但是webpack.config.js获取不到

  1. 直接写在js文件里面,类似与build.js
// build.js
'use strict'
require('./check-versions')()
process.env.NODE_ENV = 'production'

配置

实现步骤

  1. 由一个变量标识当前是那个客户
  2. npm run dev或者npm run build之前,根据不同客户生成对应的路由文件
  3. 这样执行或者打包就可以根据路由的配置打包不同的文件,而那些没有使用的文件将不会被包含进来

标识客户

  1. 直接通过DefinePlugin来配置的话,例如
new webpack.DefinePlugin({
  CLIENT: 'C1'
});

这样这个 CLIENT 只能在 src/ 下面的文件获取到,无法在构建的时候进行判断。同时不方便扩展,当多个用户的时候就需要多个配置文件。

  1. process.env.CLIENT = 'C1',同时不方便扩展。

如果上面第二种方法不是直接写死,而是在执行命令的时候直接输入客户,然后代码里面直接判断即可,这样就解决第一步的问题。这就需要cross-env。

cross-env

cross-env(跨Win/Linux平台设置 process.env值)

安装

npm install --save-dev cross-env

在 package.json 文件中设置不同的运行脚本,比如:

script:{
    "start": "node build/dev-server.js",
    "buildStag": "cross-env cross-env NODE_ENV=stag  node build/dev-server.js",
    "buildProd": "cross-env cross-env NODE_ENV=production  node build/dev-server.js",
}
//根据上述脚本即可设置不同的 NODE_ENV 值,在文件中设置值或者加载不同的设置文件,以上配置在NODE项目中可正常运行

在vue项目中的使用

在使用Vue Cli构建的项目中,需要在 process.env 设置其他变量名进行使用,如:BUILD_ENV 在 package.json 的 script 字段中作如下配置:

"scripts": {
    "start": "cross-env BUILD_ENV=dev node build/dev-server.js",
    "dev": "cross-env BUILD_ENV=dev  node build/dev-server.js",
    "build": "cross-env BUILD_ENV=dev node build/build.js",
    "buildDev": "cross-env BUILD_ENV=dev  node build/build.js",
    "buildStag": "cross-env BUILD_ENV=stag  node build/build.js",
    "buildProd": "cross-env BUILD_ENV=prod  node build/build.js",
    "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run",
    "e2e": "node test/e2e/runner.js",
    "test": "npm run unit && npm run e2e",
    "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs"
  },

由于搭配webpack重新设置了 process.env 的值,需要在webpack.dev.conf.js 及 webpack.prod.conf.js 文件中

webpack.dev.conf.js
new webpack.DefinePlugin({
    'process.env': config.dev.env,
    'process.env.BUILD_ENV': JSON.stringify(process.env.BUILD_ENV)//增加此行
})

webpack.prod.conf.js
new webpack.DefinePlugin({
    'process.env': env,
    'process.env.BUILD_ENV': JSON.stringify(process.env.BUILD_ENV)
})

即在该插件设置并暴露出 process.env 对象后,再增加process.env.BUILD_ENV 字段并进行赋值。

此时,可在前端JS文件中通过 process.env.BUILD_ENV 获得 package.json 命令中设置的值,进行其他操作,

比如,引入不同环境的配置文件:
在config文件夹中增加 buildConfig 文件夹,其中新建以下3个文件:

  1. dev.config.js
  2. stag.config.js
  3. prod.config.js,

在每个文件中采用module.exports的方式导出变量,如:

module.exports = {
    BASE_URL: 'https://dev-api.greigreat.com',
    BASE_STATIC_URL:'https://static1.greigreat.com/'
}

在其他文件中使用

//引入环境配置文件
//process.env.BUILD_ENV 为 webpack中的DefinePlugin暴露出的环境变量
const buildConfig = require('./buildConfig/' + process.env.BUILD_ENV + '.config')
export default {
    baseUrl: buildConfig.BASE_URL,
    baseStaticUrl: buildConfig.BASE_STATIC_URL
}

是的!!!这样就可以根据客户名称然后获取客户对应的配置!!!

  • package.json
"scripts": {
  "dev": "npm run build:router && webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
  "dev:C1": "cross-env CLIENT=C1 npm run dev", // 设置CLIENT=C1并执行
  "dev:C2": "cross-env CLIENT=C2 npm run dev", // 设置CLIENT=C2并执行
  "start": "npm run dev",
  "unit": "jest --config test/unit/jest.conf.js --coverage",
  "test": "npm run unit",
  "build:router": "node build/bin/build-router.js", // 创建路由
  "build": "npm run build:router && node build/build.js",
  "build:C1": "cross-env CLIENT=C1 npm run build", // 设置CLIENT=C1并打包
  "build:C2": "cross-env CLIENT=C2 npm run build" // 设置CLIENT=C1并打包
},
  • 不同客户的配置
// build/clients/C1/router.js

var CLIENT_IMPORT = {
  Loading: '@/views/C1/Loading' // 不同客户路径不一样
}
module.exports = {
  CLIENT_IMPORT
}

// build/clients/C2/router.js

var CLIENT_IMPORT = {
  Loading: '@/views/C2/Loading' // 不同客户路径不一样
}
module.exports = {
  CLIENT_IMPORT
}
  • 标准版本的配置
// 组件以及对应地址
var STANDARD_IMPORT = {
  Home: '@/components/home/home',
  NoFound: '@/views/NoFound',
  Loading: '@/views/Loading',
  Step: '@/views/Step',
  verify: '@/views/verify'
};

// 默认的路由
var STANDARD_DEFAULT = {
  Home: `{
      path: '/',
      name: '首页',
      component: Home
    }`,
  NoFound: `{
      path: '*',
      component: NoFound,
    }`
}

// 页面路由
var STANDARD_VIEWS = {
  notice: `{
    path: '/notice',
    name: 'notice',
    component: Home,
    children: [{
      path: 'loading',
      name: 'loading组件',
      component: Loading,
    }]
  }`,
  navigation: `{
    path: '/navigation',
    name: 'navigation',
    component: Home,
    children: [{
      path: 'step',
      name: 'step组件',
      component: Step,
    }]
  }`,
  verify: `{
    path: '/verify',
    name: 'verify',
    component: Home,
    children: [{
      path: 'verify',
      name: '官方文档验证',
      component: verify,
    }]
  }`
}

module.exports = {
  STANDARD_IMPORT,
  STANDARD_DEFAULT,
  STANDARD_VIEWS
}
  • 生成路由
var fs = require('fs');
var render = require('json-templater/string');
var path = require('path');
var endOfLine = require('os').EOL;
var { STANDARD_IMPORT, STANDARD_DEFAULT, STANDARD_VIEWS } = require('../clients/STANDARD/router.js');
// 根据客户获取对应的配置文件
var CLIENT = process.env.CLIENT || "";
if (CLIENT) {
  var { CLIENT_IMPORT } = require(`../clients/${CLIENT}/router.js`);
} else {
  var CLIENT_IMPORT = {};
}

var OUTPUT_PATH = path.join(__dirname, '../../src/router/index.js');
var IMPORT_TEMPLATE = 'const {{name}} = resolve => require([\'{{path}}\'], resolve);';
var MAIN_TEMPLATE = `
// 由build/bin/build-router.js自动生成
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)

{{include}}

// 默认路由
const router = new Router({
  routes: [
    {{default}}
  ]
})

// 动态路由,并用于生成菜单
export const routesMenu = [
  {{views}}
]

router.addRoutes(routesMenu);
export default router;
`;

var includeTemplate = [];
var defaultTemplate = [];
var viewsTemplate = [];

// include
var includeObject = Object.assign({}, STANDARD_IMPORT, CLIENT_IMPORT);
for (let name of Object.keys(includeObject)) {
  includeTemplate.push(render(IMPORT_TEMPLATE, {
    name: name,
    path: includeObject[name]
  }))
}

// default
for (let name of Object.keys(STANDARD_DEFAULT)) {
  defaultTemplate.push(STANDARD_DEFAULT[name])
}

// views
for (let name of Object.keys(STANDARD_VIEWS)) {
  viewsTemplate.push(STANDARD_VIEWS[name])
}

var template = render(MAIN_TEMPLATE, {
  include: includeTemplate.join(endOfLine),
  default: defaultTemplate.join(','),
  views: viewsTemplate.join(',')
}) 

fs.writeFileSync(OUTPUT_PATH, template); 
console.log('[build router] DONE', OUTPUT_PATH);

使用

npm install

// 标准版
npm run dev
npm run build

// 客户C1版
npm run dev:C1
npm run build:C1

// 客户C2版
npm run dev:C1
npm run build:C2

npm run build --report

参考

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.