Code Monkey home page Code Monkey logo

blog's Introduction

Hi there 👋

Ranto's Dev Card

blog's People

Contributors

runtu4378 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

blog's Issues

计算机与网络发展的历史概述

前言
计算机的发明和普及为人类社会的发展提供了极大的助力,而网络的发明更是使人类突破了地域的限制,进入了知识共享的时代。计算机和网络这两者究竟是如何诞生和慢慢演变成我们现在的样子的呢。

计算机的发展阶段

批处理

这个阶段的计算机体积很大,为了能够让更多的人使用计算机,采用了批处理系统。用户事先将用户程序和数据装入到卡带或者磁带中,又计算机按照一定的顺序读取,并进行处理。此时的计算机操作相当复杂,有事程序运行的时间较长,需要排队处理不同的用户程序。此时的计算机尚未是一个便于普通人是用的工具。

分时系统

继批处理系统之后,20世纪60年代出现了分时系统,分时系统的原理是多个终端与同一个计算机连接,允许多个用户同时使用一台计算机。此时的计算机造价亦然十分昂贵,不过分时系统的出现实现了一人一机,觉醒了以解放生产力为目的的个人计算机的需求。同时,分时系统中每个终端与计算机的连接线路形成一个星型结构,网络(通信)与计算机之间的关系开始浮出水面,计算机开始向普及化的方向发展。

最初计算机之间的通信

20世纪70年代,计算机体积趋于小型化,价格也开始下降,企业开始使用计算机。在计算机间通信技术诞生之前,计算机之间的信息交流是通过外部存储介质将数据从一台计算机转移到另外一台计算机上的,不过这种通信速度大大影响了工作效率,人们开始研究计算机之间的通信技术。

计算机网络的产生

20世纪70年代的初期,人们便开始研究基于分组交换技术的计算机网络,在初期,计算机网络限制于同厂商生产的计算机之间,这导致企业想要实现内部联网,就只能购买同一厂商的计算机。到了80年代,一种能互联多种计算机的网络随之产生。这时候还有一个重要的发明便是窗口系统,窗口系统使计算机的可用性大大增加,而且可以同时执行多个程序,窗口系统和网络的紧密结合使得企业内部可以围绕计算机组成便捷的工作局域网环境。

互联网的普及

进入20世纪90年代,一种能够连接各种多种异构计算机之间的链接开始成熟。使得搭建网络环境的成本降低,诸如电子邮件和万维网等技术逐渐发展,使计算机从单机模式的工具变成访问互联网的工具,互联网开始迅速普及。

以互联网技术为中心的时代

互联网技术开始向各种技术渗透,能够接入互联网的除了计算机,扩展到了手机、家用电器、其他移动设备等,互联网开始进入我们生活的各方面。

从“单纯建立连接”到“安全建立连接”

互联网的普及为现代人提供了一个高度便捷的网络环境,也因此成为了一个国家基础建设中重要的一环。但同时互联网的便捷性也为人们的生活带来了负面问题。比如计算机病毒的侵害、信息泄露、网络欺诈等利用互联网的犯罪行为也日渐增多。
在互联网普及的初期,人们更关注单纯的连接性,以不受任何限制地建立互联网连接为最终目的。然而现在,人们不再满足与单纯的建立连接,而是更为追求安全建立连接为目的。

《图解TCP/IP》第五版读书摘要

图书信息
书名:《图解TCP/IP》
作者:【日】竹下隆史 村山公保 荒井透 䓭田幸雄
出版社:人民邮电出版社

此读书摘要记录我对该书内容的浓缩和提取。

第一章 网络基础知识

计算机网络出现的背景

计算机的普及与多样化
从独立模式到网络互联模式
从计算机通信到信息通信
计算机网络的作用

计算机与网络发展得 7 个阶段

批处理
分时系统
计算机之间的通信
计算机网络的产生
互联网的普及
以互联网技术为中心的时代
从“单纯建立连接”到“安全建立连接”
手握金刚钻的TCP/IP

前端知识整理

前言
此文主要是是为了准备面试而整理,通过整理前端的一系列知识,以我熟悉的文字和思路诉诸文字。

参考内容来源

前端开发面试题


整体思路

前端是一个年轻的行业,行的行业标准、框架、库都在不断的更新,加上前端的使用环境从pc端慢慢发展到移动端也是带来了很多变化,正如赫门在2015深JS大会上的《前端服务化之路》主题演讲中说的一句话:“每18至24个月,前端都会难一倍”,这些变化使前端的能力更加丰富、创造的应用也会更加完美。所以关注各种前端技术,跟上快速变化的节奏,也是身为一个前端程序员必备的技能之一。

以下是我对前端知识体系的整理思路:

  • 前端行业的立足点(扮演的角色、担任的工作);
  • 基础知识点(HTML、CSS、JavaScript);
  • 标准和实现(浏览器)带来的变化;
  • 著名的库和工具以及它们为前端带来的影响;

前端的发展历程

了解有限,这里只从web前端应该担任的角色的设计初衷来探究web前端的功能,其他角度还有开发模式的变迁等方面。

参考文章
你了解Web前端起源与发展阶段吗
Web 开发模式演变历史和趋势

此部分未完待续…

前端基础知识点

由于由浅入深的考虑,把前端基础知识点的范围局限在HTML、CSS和JavaScript还有与之相关的基础的网络知识。

那么下面简单概括一下各个知识点的内容。

HTML+CSS: 了解Web标准、浏览器的差异和运行机制、HTML和CSS的基础知识。
JavaScript: 数据类型、对象、闭包、事件、DOM、正则表达式。
其他: 响应式、移动端注意事项、自动化构建、HTTP、Web安全、事件委托、跨域。

Javascript 如何工作:内存管理 + 如何处理4种常见的内存泄漏

前言
翻译自How JavaScript works: memory management + how to handle 4 common memory leaks

几个星期前,我们开展了一系列针对深入研究 Javascript 和它是如何工作的:我们认为通过了解 Javascript 的构建模块和它们是如何在一起运行的话,你就可以编写更好的代码和程序。

这个系列的第一篇文章是关注于提供 引擎、运行时机制(runtime)、调用堆栈的概述,第二篇文章仔细研究了 Google 的 V8 引擎的内部实现,V8 引擎内部实现和如何编写更好的代码的 5 条 tips

在这第三篇文章里,我们会讨论更重要的问题以至于越来越多的程序员忽视这个问题——内存管理,因为我们日常使用的编程语言正变得越来越成熟和复杂。我们同样会提供几种方法来解决 Javascript 中的内存泄漏,这是我们在开发 SessionStack 中所遵循的,这能令 SessionStack 不会发生内存泄漏或者在我们所继承的网页应用中导致内存消耗增加。

概括

比如 C 语言这样的编程语言,会提供底层的内存管理函数,比如 malloc()free()。这些函数能够让开发者明确的从系统中分配和释放内存。

对比之下,Javascript 在目标(对象,字符等)被创建时分配,并且“自动”的在它们不再被使用的时候释放内存,这个过程称为垃圾回收。这看似“自动”的释放资源的性质带来了混乱,给 Javascript(或者其他高级语言)的开发者带来错误的印象——他们可以不考虑内存管理,这错的离谱。

即使使用高级语言进行开发,开发者都应该拥有内存管理的认识(至少是最基本的理解)。有时他们提出一些内存管理方面的问题(例如是 Bug 或者是垃圾回收器的实现限制之类的)当他们需要正确的理解和处理内存管理问题(或者是为了找到一种最低成本或者最少代码债务的正确方式来处理它)。

内存的生命周期

无论你使用什么开发语言,内存的生命周期基本上都是一样的:

分配内存 -> 使用内存 -> 释放内存

下面是关于每个阶段发生了什么的概述:

  • 分配内存——内存是由操作系统分配给你的程序来使用的。在低级语言(如C)中,这是一项开发者需要处理的明确操作。然而在高级语言为了照顾开发者,它会自动处理这件事。
  • 使用内存——就是你的正在使用刚刚系统为你分配的内存的时候。当你在使用代码中定义的变量的时候,实际执行的是变量对应的内存的读写操作。
  • 释放内存——这时候你应该释放那些你不需要的内存来解除它们的占用以至于这些内存能够被复用。跟分配内存相似的是,释放内存在低级语言中也是一项明确的操作。

想快速获得调用堆栈和内存堆的概念概括的话,可以阅读我们的 第一篇文章

什么是内存?

在直接跳跃到讲 Javascript 的内存之前,我们会简要的讨论一下内存一般来说指的是什么,和它是如何运作的。

在硬件层级,计算机内存是由大量的触发器(flip flops)组成的,每个触发器包含若干的晶体管并且能够存储一比特(1 bit)的数据。每个触发器都可以被一个唯一的识别符作为寻址依据,所以我们才能读写它们。所以从概念上我们可以把计算机的所有内存抽象为一个超长的由比特(bits)组成的数组,我们可以在这数组上进行读写操作。

作为人类,我们没有概念也不擅长在比特维度上进行计算,我们将它聚合成更高一级的维度——能够用来保存数字的字节(8 比特组成 1 字节)在字节之上是单词(有些单词需要16字节,有些需要32位字节)。

这些内存上面保存了很多东西:

1.所有程序所使用到的变量或者是其他数据。
2.程序的代码,包括操作系统的。

编译器和操作系统负责了大部分的内存管理工作,但我们建议你还是需要了解一下在底层发生了些什么。

当你编译你的代码的时候,编译器会检查你所声明的数据类型并事前计算你需要使用多少内存。程序所需被分配的内存数量被称为堆栈空间。在堆栈空间中需要添加被函数被调用到的变量的时候,它们会被添加到已经存在的内存的最上面,当它们被释放的时候,遵循 LIFO 原则(Last In First Out 后进先出),例如下面的运算:

int n;  // 4字节
int x[4]; // 长度为4的数组,每个元素4字节
double m; // 8字节

编译器可以马上计算出代码需要:
4 + 4 × 4 + 8 = 28 bytes 的内存

这是按照现在的 integers 和 doubles 的尺寸规定所计算出的结果,在大概 20 年前,integers 一般是 2 字节的,而 double 是 4 字节的。你的代码是不需要依赖于现在的基本数据类型的尺寸规定的。

编译器会插入与操作系统进行相互作用的代码,并请求必要数量的堆栈来保存你的变量。

在上面的例子,编译器知道每一个变量在内存中的地址。事实上,当我们写入变量 n 的时候,在内部会被翻译为比如内存地址 4127963之类的内存地址。

注意当我们尝试去访问 x[4] 的时候,我们会访问到一部分分配到 m 中的数据内容。这是因为我们访问了一个不存在数组中的数据————在数组的最后一个元素 x[3] 后面 4 bytes 的 x[4] 地址,这会访问到(或者写覆盖)到部分 m 的 bits,这会给后面的程序运行带来一些不可预知的结果。

内存分布示意图

当函数调用另一个函数的时候,每个函数都会在堆栈中创建属于他们的块。用来保存它们的局部变量,同时有一个程序计数器,记录它在执行时的位置。

动态分配

不幸的是,当我们不知道在编译时一个变量需要分配多少内存的时候,事情看上去并非这么简单。比如我们想要进下下面这样的操作的时候:

int n = readInput(); // 读取用户输入

...

// 创建包括 n 的元素的数组

在这种情况下,编译器并不知道要为 n 分配多少内存因为这取决于运行时用户输入的值。

因此无法从堆栈为这个变量分配空间,所以我们的程序需要在运行的时候向操作系统请求正确的内存空间。这些空间是从系统的堆空间中分配出来的。静态分配和动态分配这两种内存的区别总结基本总结在下面的表格中:

静态分配 动态分配
  • 编译时要分配的尺寸必须是已知的
    - 编译的时候执行
    - 从栈(stack)中分配
    - FIFO(First in,Last out) | - 编译时要分配的尺寸必须是未知的
    - 运行的时候执行
    - 从堆(heap)中分配
    - 没有特殊的分配方式

想要全面了解如何动态分配内存,我们需要花多点时间在指针上面,这会稍微偏离我们这篇文章的主题。如果你想要学习指针相关的知识的话,可以在评论里面反映,我们会在将来的文章中增加关于指针的知识。

Javascript 中的内存分配

现在我们会解释 Javascript 如何执行第一步(分配内存)。

Javascript 减轻了开发者内存分配的责任——Javascript 自己完成变量分配,伴随着变量声明。

var n = 374; // 为数字分配内存
var S = 'sessionstack'; // 为字符分配内存

var o = {
  a: 1,
  b: null,
}; // 为对象及其包含的值分配内存

var a = [1, null, 'str']; // (类似对象)为数组及其包含的值分配内存

function f(a) {
  return a + 3;
} // 分配函数空间(能被调用的对象)

// 函数表达式同样被视为对象进行内存分配
someElement.addEventListener('click', function() {
  someElement.style.backgroundColor = 'blue';
}, false);

一些调用回调为对象的变量同样也会进行分配:

var d = new Data(); // 分配 Date 对象

var e = document.createElement('div'); // 分配 DOM 元素

函数可以分配为新值或者新对象:

var s1 = 'sessionstack';
var s2 = s1.substr(0, 3); // s2 是一个新字符串
// 因为字符串是不会变化的
// Javascript 可能不会分配新的内存
// 只保存 [0, 3] 的范围

var a1 = ['str1', 'str2'];
var a2 = ['str3', 'str4'];
var a3 = al.concat(a2);
// 由四个元素组成的新数组
// 由 a1 和 a2 的元素所组成

Javascript 中的内存使用

Javascript 中通过读取和写入来使用已分配的内存。

它发生在读取或者是写入变量的时候,或者是某些对象的属性乃至你为函数传入的变量。

释放那些不再被需要的内存

大部分关于内存管理的问题是在这个阶段出现的。

最艰难的任务是如何分辨某个已分配内存不会再被使用。这通常需要开发者在代码中明确提出某部分内存不会再使用而可以被释放,

高等语言会内嵌一种名为垃圾回收器的程序,他的任务是追踪内存分配而且判断这些已分配的内存在什么时候不会再被使用,从而释放这段内存。

不幸的是,这个过程只能得出一个近似值,因为判断某部分内存在什么时候被需要这个问题是不可解的(无法通过一个算法来解决)

大部分垃圾回收器通过手机那些不会再被访问到的内存来达到目的。例如,所有指向此处的变量都超出了使用范围。然而,这只是可被回收的内存空间的一个近似值。因为在任何时候,一个内存地址都可能还有变量仍然指向此处,但这些变量可能不会再被范围而已。

垃圾回收

由于寻找某个变量空间“不再被需要”的问题是不可判定的。垃圾回收是对一般情况实现的一种解决方案。这一章节会解释一些理解垃圾回收的算法和局限性所必需的的知识。

内存引用

垃圾回收的算法所依赖的主要概念之一是内存引用

在内存管理中,如果一个对象访问另个一对象,则称前者引用另一个对象(可以是显式的或者是隐式的)。举个例子,一个 Javascript 对象会引用它的 原型属性(隐式引用)和它的属性的值(显式引用)。

在我们讨论的这些情况下,“对象”的含义从传统意义上的“Javascript 对象”扩展到更广泛的概念上,包括函数领域乃至词法范围

词法范围定义了如何在嵌套函数中解析变量名称:即使父函数已返回,内部函数也包含父函数的作用域。

引用计数和垃圾回收

这是最简单的垃圾回收算法。一个对象是根据它的“可回收性”来决定是否要被回收——如果没有任何单位引用它。

分析一下下面的代码:

var o1 = {
  o2: {
    x: 1
  }
};

// 两个对象被创建
// o2 被 o1 在它的第一个属性被引用
// 没有任何垃圾可以被回收

var o3 = o1; // o3 是第二个对 o1 所指向的对象进行引用的变量

o1 = 1; // 现在,原来被 o1 所引用的对象只剩下了一个引用,也就是上面的 o3

var o4 = o3.o2; // 引用对象的 o2 属性,这个对象现在有 2 个引用,一个作为属性,一个被变量所引用

o3 = '374'; // 原来被 o3 所引用的那个对象现在被引用数为 0,它现在可以被清理掉了,即使如此,它的属性 o2 仍然被 o4 所引用,所以这个对象现在还不能被清理。

o4 = null; // 带有 o2 这个对象为属性的对象,现在被引用数为 0,这可以被清理了。

循环带来的问题

在面临循环的时候,垃圾回收算法可能会面临局限。在下面的例子中,两个对象先后创建,并且相互引用,形成了一种循环。它们会在函数调用完毕之后其实已经离开作用域,是无用且可被释放的变量。然而,垃圾回收算法中的引用计数器这时候判断它们至少还仍有一个引用,两者都不能被释放。

function f() {
  var o1 = {};
  var o2 = {};
  o1.p = o2;  // o1 引用 o2
  o2.p = o1;  // o2 引用 o1
}

f();

引用关系图

标记和扫描算法

为了判断对象是否被需要,这个算法判断对象是否可达。

标记和扫描算法一般经历以下三个步骤:

1.根:通常来说,根是那些定义为全局变量并被引用的变量。举个例子在 Javascript 中,能被视为根变量的是 window 对象。类似的在 NodeJS 中的根变量为 global。垃圾回收器会建立一个列表来记录所有根变量。
2.算法随后会扫描所有根变量以及它们的属性,将它们标记为激活状态(意味着它们不是垃圾)。任何在根级别但不被访问到的变量将被标记为垃圾。
3.最后,垃圾回收器将所有标记为垃圾(没有被标记为激活状态的变量)的变量空间释放返回给系统。

1_wvtok3bv0ngu95mpxk9cng

这个算法相比于前一个更好,因为判断原因由“对象为零引用”推广到这个对象不能被访问。明显的一个反例是循环引用的情况。

截止到 2012 年,所有的现在浏览器都装载了具备标记和扫描算法的垃圾回收器。在过去几年里在 Javascript 领域里面关于垃圾回收(代码、增量、并发、并行垃圾收集)的改进都是对该算法的改进(标记扫描算法),但没有改进垃圾收集算法本身,也没有该变判断一个对象是否可达这个目标。

在这篇文章中,你可以跟进有关垃圾回收的更详细信息,也包括标记和扫描算法以及其优化。

循环不再是问题

在上面最近一个例子中,在函数调用完之后,两个互相引用的对象不被根级别的任何对象所引用。所以,它们会被垃圾回收器视为不可达的对象。

image

所以即使这两个对象还在相互引用,它们相对于根是不可达的。

垃圾收集器的直观行为

虽然垃圾收集器很方便,但它们也是有由于设计目的所限制的局限之处。其中之一是“非确定性”,换言之,垃圾收集器是不可预料的。你无法告诉它什么时候执行垃圾回收。这意味着有时候程序会占用比它实际所需要的更多的内存。在这种情况下,尤其是那些对流畅性要求很高的程序,短暂停会尤其明显。尽管非决定论意味着人们无法确定何时进行收集,大多数垃圾收集实现共享在分配过程中执行收集过程的通用模式。如果没有分配动作需要执行,大部分垃圾收集器会保持空闲状态,同时考虑以下情况:

1.一大组分配被执行。

2.大部分这些元素(或者是全部)被标记为不可达(假设我们将这些变量链接到内存地址的引用标记为 null)

3.没有进一步的分配被执行。

在这种情况下,大部分垃圾收集器不会再进行深一步的内存收集。换言之,尽管可能还存在可收集的不可达的引用,不过收集器并不会发现这些引用。这些并非内存泄漏但仍然导致了高于预期的内存占用的表现。

什么是内存泄漏

内存泄漏指的是某个应用曾经使用过但在之后不会被使用到的一段内存空间,这段内存空间没有被返回给系统或者是可用内存池。

image

编程语言支持以不同的方式来进行内存管理。然后,某段内存是否被占用是一个不可判定的问题。换言之,只有开发者能够明确说明某个变量所占用的内存空间是否能够释放。

某些编程语言提供了可帮助开发人员执行此操作的功能。其他开发语言则希望开发人员能够完全清楚一段内存何时未被使用。维基百科有关手动自动内存管理的好文章。

Javascript 常见的四种内存泄漏

1.全局变量

Javascript 以一种有趣的方式来对待未声明的变量:当一个未声明的变量被引用时,这个变量会在全局对象中被创建。在浏览器中,全局对象是 window,这意味着:

function foo() {
  bar = 'some text';
}

相当于:

function foo() {
  window.bar = 'some text';
}

bar 变量的预期作用是一个在 foo 函数里面使用的函数,不过却导致了一个冗余的全局变量被定义了,这只是由于你忘记了使用 var 来进行变量的声明。上面这种情况带来的后果可能不那么糟糕,但你应该能想象到比这糟糕得多的情况。

你也可以通过使用 this 来意外的创建一个全局变量:

function foo() {
  this.var1 = '潜在的意外全局性变量';
}

// foo 被调用,此时的 this 指向的是全局变量 window 而不是 undefined
foo();

你可以通过在 Javascript 的文件开头添加 use strict; 来避免这种情况发生,这能够将 Javascript 转换到更严格的编译模式,防止意外的全局变量的创建。

变量意外全局化当然是个问题,然而,更多的时候,你的代码会被显式定义的全局变量污染,这些变量根据定义不能被垃圾收集器所收集。特别要注意那些用来存储临时变量和处理大量信息的全局变量。如果你必须这么做的话你大可以使用全局变量来存储这些数据,不过记得要在使用完这些数据之后将它们分配为空或者是重新分配它们

2.被遗忘的定时器或者是回调

我们以经常在 Javascript 中使用到的 setInterval 为例。

提供观察者和接受回调的其他工具的库通常会确保所有对回调的引用在其实例无法访问时变得无法访问。看看下面这个不怎么罕见的例子:

var serverData = loadData();
setInterval(function() {
  var renderer = document.getElementById('renderer');
  if (renderer) {
    renderer.innerHTML = JSON.stringify(serverData);
  }
}, 5000); // 每 5 秒会执行一次

上面的代码片段显示了使用引用不再需要的节点或数据的定时器的后果。

renderer 对象可能会在某些点被替换或删除,这会使得间隔处理程序封装的块变得冗余。如果发生这种情况,处理程序及其依赖物都不会被当做垃圾被收集,因为间隔需要先停止(请记住,它仍然处于活动状态)。事实是 serverData 确实被存储了,加载数据的进程也不会被回收。

在使用观察者模式时,您需要确保你在调用完它们的时候提供一个明确的调用来将其删除(令观察者不再需要,或者该对象将变得无法到达)。

辛运的是,大部分现代浏览器会帮你完成这个工作:即使您忘记删除侦听器,一旦观察到的对象变得无法访问,它们会自动收集观察者处理程序。低版本的一些浏览器无法处理这些情况。(辣鸡IE6)

尽管如此,最好的做法是一旦对象变得过时,就移除它的监听器。看下面的例子:

var element = document.getElementById('launch-button');
var counter = 0;

function onClick(event) {
  counter += 1;
  element.innerHTML = 'text' + counter;
}

element.addEventListener('click', onClick);

// 其他操作

element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);

// 现在,当 element 离开作用域时,即使是在低版本浏览器中,element 和 onClick 都会当做垃圾收集,

在那些可以检测周期并适当地处理监听器的现代浏览器中,你不再需要在节点不可达之前调用 removeEventListener

如果你使用的是 jQuery 的 API 的话(其他库或者框架也支持)你也可以在节点过时之前删除侦听器。即使应用程序在较旧的浏览器版本下运行,该库也会确保没有内存泄漏。

3.闭包

Javascript 开发的其中一个关键点是闭包:一个可以访问外部(封闭)函数变量的内部函数。由于JavaScript运行时的实现细节,有可能以下列方式泄漏内存:

var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing) // originalThing 的引用
      console.log('hi');
  };

  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log("message");
    }
  };
};

setInterval(replaceThing, 1000);

replaceThing 被调用,theThing 会获得一个由长数组和和一个新闭包(someMethod)组成的对象。然而,originalThing 被变量 unused 所引用的闭包函数所引用了(之前调用的 replaceThing 函数里面的 theThing 变量)。需要记住的是:一旦在同一个父范围内为闭包创建了闭包范围,范围将被共享。

在这种情况下,为 someMethod 创建的闭包范围会和 unused 的闭包范围共享。unused 拥有对 originalThing 的引用。即使 unused 从未被调用过,someMethod 会通过 theThingreplaceThing 以外的空间被调用(全局空间的某处)。而且当 someMethodunused 共享闭包范围的时候,originalThing 引用着 unused 使它保持激活状态(两个关闭包之间的整个共享闭包范围)。这阻止了 unused 被收集。

在上面的例子中,为 someMethod 创建的闭包空间和 unused 共享了,而 unused 引用了 originalThingsomeMethod 可以通过 theThingreplaceThing 作用域之外被调用,即使事实上 unused 从未被调用过。事实上由于作用域共享,在 someMethod 被调用时,unused 也进行了 originalThing 的引用。

所有这些都可能导致相当大的内存泄漏。当上面的代码片段一遍又一遍地运行时,您可能会发现内存使用量激增。当垃圾收集器运行时,其大小不会缩小。一个闭包的关联链表被创建(在这个例子中根变量是 theThing 变量),并且每个闭包范围都会间接引用大数组。

Meteor团队发现了这个问题,他们有一篇很好的文章,详细描述了这个问题。

4.DOM 以外引用

有些情况下开发人员在数据结构中存储 DOM 节点。假设你想快速更新表格中几行的内容。如果您在字典或数组中存储对每个DOM行的引用,将有两个对同一个 DOM 元素的引用:一个在 DOM 树中,另一个在字典中。如果你决定移除这些行,你需要记住使两个引用无法访问。

var elements = {
  button: document.getElementById('button'),
  image: document.getElementById('image')
};
function doStuff() {
  elements.image.src = 'http://example.com/image_name.png';
}
function removeImage() {
  // image 是 body 下直属的子元素
  document.body.removeChild(document.getElementById('image'));
  // 在这时候,在全局变量 elements 中依然保存着对 #button 的引用,换言之,这个 button 元素依然存在于内存中不能被垃圾回收掉
}

在涉及DOM树内的内部节点或叶节点时,还有一个额外考虑因素需要考虑。如果您在代码中保留对表格单元格(<td>标记)的引用,然后你决定从 DOM 中删除该表格并保留对该特定单元格的引用,你可能会发生一个严重的内存泄漏。你可能会认为垃圾回收器会释放除该单元格外的所有内容。然而,情况并非如此。由于单元格是表格的子节点,并且子节点保持对其父母的引用,对表格单元格的这种单一引用会将整个表格保留在内存中。

我们在 SessionStack 尝试遵循这些最佳实践来编写正确处理内存分配的代码,原因如下:

当您将 SessionStack 集成到您的生产Web应用程序,它开始记录一切:所有 DOM 更改,用户交互,JavaScript 异常,堆栈跟踪,网络请求失败,调试消息等。通过 SessionStack,您可以将发生的问题作为视频重播,并查看发生在用户身上的一切。所有这些都必须在您的网络应用程序没有额外性能影响的情况下进行。

由于用户可以重新加载页面或导航您的应用,所有观察员,拦截器,变量分配等。都必须正确处理,所以 SessionStack 不应该导致任何内存泄漏,或者不会增加我们集成的 Web 应用程序的内存消耗。

有一个免费的计划,所以你可以尝试一下

image

资源

Less自动编译环境指南

本文是关于选择什么工具搭建自动编译less文件为指定格式的样式文件到指定目录,和Less生产环境的一些使用技巧。

需求

今天开发小程序的时候遇到一种违反 DRY 原则的情况(Don’t Repeat Yourself)。那就是我想使用基础类来构建我的样式文件,但是小程序的开发框架不支持直接使用 less,然后小程序每个页面的样式文件是和 wxml 文件绑定在同一目录的,我要每个页面分别先写 less 再去编译的话略为繁琐,且不好管理;我也可以选择将所有样式写在根样式文件中去,但是这样也不好。

所以我需要的结果是能够在工作目录中编写 less 文件,然后将不同源文件的 less 文件自动编译到指定的目录和文件名(xxx.wxss),然后还要做到能够基础类样式复用。


过程

解决问题花了一个下午的时间,我在 Less 中文手册中发现了四个 GUI 编译工具,其中:

  • koala 的缺点是不能够自定义输出文件的后缀名,但可以自定输出目录并自动监听文件更改。
  • Codekit 缺点是没有 win 版…
  • SimpleLess 可以自定输出目录、监听文件更改、自定输出后缀,但缺点是有 bug,会将文中某行的注释提到第一行,暂无解决方法。
  • WinLess 这个就是我最后的选择,自定输出目录、监听文件更改、自定后缀这些功能都有,而且没有SimpleLess 的 bug,还有的其他好处我在下一章说。

使用技巧

WinLess 还有两个好处是我发现的惊喜。

切换Less引擎

WinLess 可以切换不同的 Less 引擎来编译,因为它自带的引擎版本比较低,不支持 @import (keyword) "filename"; 这样的语法,所以可以切换成我们在 npm 全局安装的 Less 编译引擎,这样问题就解决了。

启动全局引擎设置


自定编译参数

WinLess 的默认安装路径为 C:\Program Files (x86)\Mark Lagendijk\WinLess 下面简单说说文件目录结构:

┝node_modules
┃ ┝.bin
┃ ┃ ┝...
┃ ┃ ┝npm.cmd(使用程序自带引擎时执行的node命令)
┃ ┃ ┕...
┃ ┝less(使用程序自带引擎的引擎文件)
┃ ┕less-plugin-clean-css(自带引擎的插件)
┝less.cmd(使用全局Less引擎时执行的命令)
┝...
┕WinLess.exe(程序入口文件)

上面的文件目录告诉我们可以从两个方向来调教这款软件:

  • 更换自带引擎的引擎文件、插件等
  • 使用全局引擎、自己配置参数

命令行模式参数-文档


Less使用技巧

reference 关键字

在 Less 文档中通过关键字 reference 的作用是引用目标 Less 文件但是不输出它。

例子:

<!-- base.less -->
.import-css {
    font-size: 10px;
}
<!-- tar.less -->
@import (reference) 'base.less';
.demo {
    color: red;
    .import-css;
}
<!-- tar.css -->
.demo {
    color: red;
    font-size: 10px;
}

注:@import 关键字和 (reference) 之间要有一个空格,不然无法生效。

Javascript跨域知识整理

引用出处:
浅谈跨域以及WebService对跨域的支持
浅谈WEB跨域的实现(前端向)

写在前面

跨域问题是 javascript 在同源策略下产生的,javascript 只能访问和操作自己所在域的资源(相同协议、主机名、端口号)。

在以前,前端代码和后端代码混杂在一起,javascript 直接调用系统里面统一的 Httphandler 这样是不存在跨域问题的。但随着现在技术的发展,多客户端、前后端分离等开发环境使得 javascript 使用到跨域获取数据的情况越来越多,所以,下面来说说 javascript 跨域要怎么实现。

需要说明的是,同源策略是 javascript 里面的限制,其他语言如 c#、java 是可以调用其他 webserver 的。

传统的跨域解决方案是 JSONP(JSON with padding) 和 CORS(Cross-origin resource sharing),其中,JSONP 需要客户端和服务端一起配合,CORS 则只需要在服务端进行处理就可以了。


JSONP

在同源策略下,某个服务器是没有办法获取到服务器以外的数据的,iframe,img,script等标签是例外,这些标签可以通过 src 属性去请求其他服务器上的资源,JSONP 就是利用到了 script 标签的 src 来调用跨域的请求。

举例如下:服务端和客户端约定传一个名为 callback 的参数来使用 JSONP 功能,比如常规的请求地址如下:

http://www.example.net/sample.aspx

该地址的返回结果可能是一个单纯的 json 字符串,比如:

{ foo: 'bar' }

当加上 callback 的参数之后,即开启 JSONP 功能的时候。

http://www.example.net/sample.aspx?callback=mycallback

服务端会使用传入的 callback 参数对返回的数据进行处理:

mycallback({ foo: 'bar' })

这样子就能够获取到跨域的数据同时使用本域的回调函数进行处理。

JSONP 跨域方式比较方便,也支持各种比较老的浏览器,但是缺点很明显,只支持 GET 的方式提交,GET 方式对请求的参数长度有限制,有些情况下可能不满足需求。


CORS(Cross-origin resource sharing)同域安全策略

CORS 同域安全策略是 W3C 在05年提出的跨域资源请求机制,它要求在当前域的相应报头添加 Access-Control-Allow-Origin 标签,从而允许该域的站点访问当前域上的资源。

服务器端在返回的相应报头中添加 Access-Control-Allow-Origin 的标签,该标签的值可为 URL 或 * ,星号表示允许所有域访问当前域。

// 服务器端配置
require("http").createServer(function(req,res){
  //报头添加Access-Control-Allow-Origin标签,值为特定的URL或“*”
  //“*”表示允许所有域访问当前域
  res.setHeader("Access-Control-Allow-Origin","*");  
  res.end("OK");
}).listen(1234);

对应域的客户端只需要正常的发出 GET 或 POST 请求即可获取到返回的 OK 信息。

// 浏览器端使用
$.ajax({
    url:"http://127.0.0.1:1234/",
    success:function(data){
        $("div").text(data)
    }
})

要注意的是,CORS 默认只支持 GET/POST 这两种 http 请求类型,要开启 PUT/DELETE 这两种方式的话需要在响应报头添加 Access-Control-Allow-Methods 标签。

// 服务器端配置
require("http").createServer(function(req,res){
  res.setHeader("Access-Control-Allow-Origin","http://127.0.0.1");
  res.setHeader(
    "Access-Control-Allow-Methods",
    "PUT, GET, POST, DELETE, HEAD, PATCH"
  );
  res.end(req.method+" "+req.url);
}).listen(1234);

不过 CORS 的缺点是对浏览器的版本有要求,不是所有版本的浏览器都支持,

CORS兼容情况


XDR-IE8

IE8 不支持 OCRS,不过它引入了 XDR特性来实现 CORS 的部分规范(IE11 不再支持 XDR 特性),由于 XDR 使用得很少,具体介绍可查看原文


HTML5解决方案

WebSocket

WebSocket protocol 是 HTML5 一种新的协议,实现了浏览器和服务器的全双工通信,也允许跨域通讯,是 server push 技术的一种很好体现。

// 客户端-原生 webSocket 实现
var ws = new WebSocket('ws://127.0.0.1:8080/url'); //新建一个WebSocket对象,注意服务器端的协议必须为“ws://”或“wss://”,其中ws开头是普通的websocket连接,wss是安全的websocket连接,类似于https。
ws.onopen = function() {
    // 连接被打开时调用
};
ws.onerror = function(e) {
    // 在出现错误时调用,例如在连接断掉时
};
ws.onclose = function() {
    // 在连接被关闭时调用
};
ws.onmessage = function(msg) {
    // 在服务器端向客户端发送消息时调用
    // msg.data包含了消息
};
// 这里是如何给服务器端发送一些数据
ws.send('some data');
// 关闭套接口
ws.close();

服务端继续使用 Node.js 来编写,并使用 socket.io 模块辅助,它封装了 webSocket 接口,并对不支持 webSocket 的浏览器提供了向下兼容(如替代为 Flash Socket/Comet)。

服务端要先安装 socket.io 模块

// 服务端脚本
var io = require('socket.io');
var server = require("http").createServer(function(req,res){
    res.writeHead(200, { 'Content-type': 'text/html'});
}).listen(1234);
io.listen(server).on('connection', function (client) {
    client.on('message', function (msg) { //监听到信息处理
        console.log('Message Received: ', msg);
        client.send('服务器收到了信息:'+ msg);
    });
    client.on("disconnect", function() { //断开处理
        console.log("Server has disconnected");
    })
});

但 socket.io 提供的接口与原生的有所区别:

<!-- 客户端-socket.io 实现 -->
<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>socket.io</title>
    <script src="jq.js"></script>
    <script src="https://cdn.socket.io/socket.io-1.3.4.js"></script>
</head>
<body>
Incoming Chat:
<ul></ul>
<br/>
<input type="text" />
<script>
    $(function () {
        var iosocket = io.connect('http://127.0.0.1:1234/'),
                $ul = $("ul"),
                $input = $("input");
        iosocket.on('connect', function() {  //接通处理
            $ul.append($('<li>连上啦</li>'));
            iosocket.on('message', function(message) {  //收到信息处理
                $ul.append($('<li></li>').text(message));
            });
            iosocket.on('disconnect', function() { //断开处理
                $ul.append('<li>Disconnected</li>');
            });
        });
        $input.keypress(function (event) {
            if (event.which == 13) { //回车
                event.preventDefault();
                iosocket.send($input.val());
                $input.val('');
            }
        });
    });
</script>
</body>
</html>

webSocket 在使用 socket.io 来解决兼容之后,能够很好地摆脱无状态的 http 连接,更好的处理连接断开、数据错误等情况。

React+Redux+webpack+Express应用环境搭建实战

文章来源:
es6环境搭建
Webpack傻瓜指南(三)和React配合开发

本文目的

使用webpack对项目工程进行打包监听,实现文件模块化打包、项目文件变动自动更新页面,生成本地服务器进行页面显示和调试。

进阶部分补充如何将Webpack自动更新页面和后台数据处理Express结合进行全栈开发。


配置流程

生成项目目录

项目目录结构如下:

┍client(项目源文件目录)
┃ ┝actions(action文件目录)
┃ ┝components(UI组件文件目录)
┃ ┝containers(容器组件文件目录)
┃ ┝utils(公用资源模块目录)
┃ ┝reducers(reducer文件目录)
┃ ┕index.jsx(项目入口)
┝build(编译文件存放目录,可不创建)
┝node_modules(模块仓库)
┝.babelrc(babel配置文件)
┝package.json(项目描述文件)
┕webpack.config.js(webpack配置文件)

创建文件夹,命令行输入指令 npm init 生成项目描述文件,一路回车,然后便生成了 package.json 文件。随后建立 app 目录及 components 目录、utils 目录。

安装webpack及配置

输入指令 npm install --save-dev webpack html-webpack-plugin 安装 webpack 及插件,然后在根目录新建 webpack.config.js 文件,输入以下内容保存。

输入指令 webpack-dev-server -v,如无显示对应版本信息的话需要全局安装 webpack 的开发工具,指令为 npm install -g webpack-dev-server

// webpack.config.js
var path = require('path');
var webpack = require('webpack');
var HtmlwebpackPlugin = require('html-webpack-plugin');
var ROOT_PATH = path.resolve(__dirname);
var APP_PATH = path.resolve(ROOT_PATH, 'client');
var BUILD_PATH = path.resolve(ROOT_PATH, 'build');
module.exports= {
  entry: {
    client: path.resolve(APP_PATH, 'index.jsx')
  },
  output: {
    path: BUILD_PATH,
    filename: 'bundle.js'
  },
  //enable dev source map
  devtool: 'eval-source-map',
  //enable dev server
  devServer: {
    contentBase: APP_PATH,
    compress: true,
    port: 9000,
    hot: true,
    watchContentBase: true
  },
  //babel重要的loader在这里
  module: {},
  resolve: {
    extensions: ['.js', '.jsx', ".scss", ".less"],
    alias: {}
  },
  plugins: [
    new HtmlwebpackPlugin({
      title: 'My first react app'
    })
  ]
}

以上配置将 app 目录下的 index.jsx 设置为入口文件文件,设置根目录下的 build 目录设置为打包文件输出目录(使用 build 命令时才会生成文件,平时预览的时候编译的 bundle.js 文件是在内存中的)。然后还定义了 dev 工具运行的目录为 app 目录(默认入口文件为 index.jsx)当运行 webpack-dev-server 命令的时候会启动本地服务器的 9000 接口进行页面输出。

resolve 选项的 extensions 配置为常用文件后缀,进行设置之后引入这些类型的文件可以不输入后缀名。alias 配置项为对常用路径设置别名,这样有助于减少系统检索的时间。如:

// 设置前
import bootstraps from '../node_modules/bootstrap/less/bootstrap.less';
// 配置项为
alias: {
  "Bootstrap": path.resolve(__dirname, 'node_modules/bootstrap/less/bootstrap.less')
}
// 设置后可以这样子引入
import bootstrap from 'Bootstrap';

安装React+Redux并配置

输入命令 npm install --save-dev react react-dom react-redux redux,安装 react 和 redux。

由于 react 和 redux 会用到 es6 的语法,所以需要安装 babel 相关插件负责在打包的时候将其转换为 es5 的语法。

输入命令 npm install --save-dev babel-core babel-loader babel-preset-es2015 babel-preset-stage-2 babel-preset-react 安装相关插件。

然后在项目根目录建立 .babelrc 作为 babel 的配置文件,window 用户可在命令行中输入命令 cd .>.babelrc 创建这样一个不带文件名的文件。在 .babelrc 文件中输入以下内容。

  "presets": [
    "es2015",
    "react",
    "stage-2"
  ],
  "plugins": []
}

之后还需在 webpack.config.js 文件中修改 module 配置项,添加 loaders 属性,使其支持渲染 .jsx 为后缀的使用 es6 语法的文件。修改结果如下:

...
//babel重要的loader在这里
module: {
  loaders: [
    {
      test: /\.jsx?$/,
      loader: 'babel-loader',
      include: APP_PATH      
    }
  ]
},
...

其目的是使用 babel-loader 插件对后缀为 jsx 的文件进行语法转换和渲染。

webpack对其他格式的文件的渲染配置

项目中常会用到的文件类型还有 .less|.sass 的样式脚本文件、.eot|.ttf 之类的 iconfont 文件和图片文件。下面对 webpack 进行配置,使之支持。

输入命令 npm install --save-dev file-loader url-loader css-loader style-loader 安装对应插件。

由于我使用 less 对我的 css 文件进行管理,所以我额外安装了 less less-loader node-less 选用 sass 的可以安装 sass sass-loader node-sass 插件。

然后修改webpack.config.js中的module配置项下面的loaders属性,修改结果如下:

...
//babel重要的loader在这里
module: {
  loaders: [
    {
      test: /\.jsx?$/,
      loader: 'babel-loader',
      include: APP_PATH      
    },
    {
      test: /\.scss?$/,
      loaders: ['style-loader', 'css-loader', 'sass-loader']
    },
    {
      test: /\.less?$/,
      loaders: ['style-loader', 'css-loader', 'less-loader']
    },
    {
      test: /\.(gif|jpg|png|woff|svg|eot|ttf)\??.*$/, 
      loader: 'url-loader?limit=50000&name=[path][name].[ext]'
    }
  ]
},
...

测试

在 app 目录下的 index.jsx 文件中添加如下代码:

import React from 'react';
import ReactDOM from 'react-dom';
import {createStore} from 'redux';
let normalVal = 0
let counter = (state = normalVal, action) => {
  switch (action.type){
    case 'ADD':
      return state + 1;
    case 'DOWN':
      return state - 1;
    default:
      return state;
  }
}
let store = createStore(counter);
class App extends React.Component{
    constructor() {
        super();
    }
    render() {
        //JSX here!
        return (
          <div className="container">
            <section className="jumbotron">
              <h3 className="jumbotron-heading">React+Redux+webpack</h3>
              <div>{store.getState()}</div>
              <button onClick={() => store.dispatch({type:'ADD'})}>+</button>
              <button onClick={() => store.dispatch({type:'DOWN'})}>-</button>
            </section>
          </div>
        )
    }
};
const app = document.createElement('div');
document.body.appendChild(app);
ReactDOM.render(<App />, app);
store.subscribe(() => ReactDOM.render(<App />, app))

可以选用为 react 定制的 bootstrap 模块,npm install --save react-bootstrap 使用方式参考这两篇文章 React-BootStrap 框架快速体验上手官方文档

修改 package.json 中的 script 选项,将其修改为:

...
"scripts": {
  "dev": "webpack-dev-server --progress --profile --colors --hot",
  "build": "webpack --progress --profile --colors",
  "test": "karma start"
},
...

然后子啊命令行中输入命令 npm run dev,编译成功无报错的话打开浏览器,输入地址 http://127.0.0.1:9000 查看效果。


安装并配置Express

Express 是基于 Node.js 的后台框架,我选择使用它来作为后端框架的原因是我不会用其他方式。

具体的介绍不多说,这里是中文官网链接。

运行命令 npm install --save-dev express 即可安装 Express。

现在我们来调整一下上面建立的文件目录结构,增加一个后台入口和后台文件存放目录。

┍client(项目源文件目录)
┃ ┝actions(action文件目录)
┃ ┝components(UI组件文件目录)
┃ ┝containers(容器组件文件目录)
┃ ┝utils(公用资源模块目录)
┃ ┝reducers(reducer文件目录)
┃ ┕view(对应后台的view视图文件的入口文件存放目录)
┃   ┝index1.jsx(view1入口文件)
┃   ┕index2.jsx(view2入口文件)
┝server(服务器文件目录)
┃ ┝views(view文件存放目录)
┃ ┕routes(route文件存放目录)
┝build(编译文件存放目录,可不创建)
┝node_modules(模块仓库)
┝app.js(后台入口文件)
┝.babelrc(babel配置文件)
┝package.json(项目描述文件)
┕webpack.config.js(webpack配置文件)

以上目录结构便是调整后的样子。目录文件依次建立好之后,在根目录的 app.js 文件中键入如下内容。

var express = require('express');
var app = express();
app.get('/', function (req, res) {
  res.send('Hello World!');
});
var server = app.listen(3000, function () {
  var port = server.address().port;
  console.log('Example app listening at Port %s', port);
});

然后在命令行中输入指令 node app.js 当命令行输出 Example app listening at Port 3000 的时候,在浏览器中输入地址,如果能够在浏览器看到 Hello World!,那就代表 Express 环境搭好了。


Express结合Webpack的全栈自动刷新

文章来源
Express结合Webpack的全栈自动刷新
入门 Webpack,看这篇就够了

Redux+React 实现的 flux 前端框架(下文统称前端框架)实现了对状态的自洽管理和页面更新生成。但是作为传统的 C/S 结构,我们还需要安全封闭的后台环境来实现对核心逻辑的封装和数据库的保存。前端框架通过 post 接口向后台调用需要更新的数据,然后通过客户端的缓存文件实现页面更新和显示。

那么,在单人开发的时候,前端框架的本地服务器预览和后台的本地服务器模拟实现交接要如何做到呢,这里来整理一下。

前端框架的预览使用的是 webpack-dev-server 插件,该插件其实是使用了 express 生成了一个小型的本地页面来实行页面显示。我们只需要通过后台端的 express 启动文件中启动 webpack-dev-middlewarewebpack-hot-middleware 这两个插件来替代 webpack-dev-server 的功能就可以了。

npm install --save-dev webpack-dev-middleware webpack-hot-middleware

安装完毕之后我们可以直接使用上面为 webpack-dev-server 所使用的设置文件生成一份副本 webpack.config.dev.js 来作为 webpack-dev-middleware 的配置文件。

// webpack.config.dev.js
var path = require('path');
var webpack = require('webpack');
var HtmlwebpackPlugin = require('html-webpack-plugin');
var ROOT_PATH = path.resolve(__dirname);
var APP_PATH = path.resolve(ROOT_PATH, 'client');
var Mod_PATH = path.resolve(ROOT_PATH, 'node_modules');
var publicPath = 'http://localhost:3000/';
var hotMiddlewareScript = 'webpack-hot-middleware/client?reload=true';
var devConfig = {
    entry: {
        page1: ['./client/views/index1', hotMiddlewareScript],
        page2: ['./client/views/index2', hotMiddlewareScript]
    },
    output: {
        filename: './[name]/bundle.js',
        path: path.resolve('./public'),
        publicPath: publicPath,
    },
    devtool: 'source-map',
    module: {
        loaders: [
        {
          test: /\.(gif|jpg|png|woff|svg|eot|ttf)\??.*$/, 
          loader: 'url-loader?limit=50000&name=[path][name].[ext]'
        }, {
          test: /\.(js|jsx)$/,
          loader: 'babel-loader',
          include: APP_PATH      
        },{
          test: /\.css?$/,
          loaders: ['style-loader', 'css-loader']
        },{
          test: /\.less?$/,
          loaders: ['style-loader', 'css-loader', 'less-loader']
        },]
    },
    resolve: {
      extensions: ['.js', '.jsx', ".scss", ".less"],
      // 设置常用模块别名,优化webpack检索时间
      alias: {
        // 'jquery': 'client/utils/javascript/jquery-3.1.1.min.js',
        'react': path.join(Mod_PATH, 'react/dist/react.min.js'),
        'react-dom': path.join(Mod_PATH, 'react-dom/dist/react-dom.min.js'),
        'react-redux': path.join(Mod_PATH, 'react-redux/dist/react-redux.min.js'),
        'redux': path.join(Mod_PATH, 'redux/dist/redux.min.js'),
        'react-bootstrap': path.join(Mod_PATH, 'react-bootstrap/dist/react-bootstrap.min.js'),
      }
    },
    plugins: [
        // new webpack.OccurenceOrderPlugin(),
        new webpack.HotModuleReplacementPlugin(),
        new webpack.NoEmitOnErrorsPlugin(),
        new HtmlwebpackPlugin({
          title: 'My first react app',
        }),
        new webpack.ProvidePlugin({
          $:"jquery",
          jQuery:"jquery",
          "window.jQuery":"jquery"
        })
    ]
};
module.exports = devConfig;

要注意上面这份配置文件中的 devServer 配置项被去掉了,因为和预览服务器相关的属性设置被放到了中间件调用的参数当中去。

和热启动有关的还有一个插件 react-transform-hmr 下面介绍安装及配置方法。

npm install --save-dev babel-plugin-react-transform react-transform-hmr

安装完毕之后配置 .babelrc 文件:

{
  "presets": ["react", "es2015"],
  "env": {
    "development": {
    "plugins": [["react-transform", {
       "transforms": [{
         "transform": "react-transform-hmr",
         
         "imports": ["react"],
         
         "locals": ["module"]
       }]
     }]]
    }
  }
}

现在使用 React 的时候,可以热加载模块了。

配置文件设置完成了之后就要到后台入口文件 app.js 中设置和中间件有关的代码了,不过要先安装模板引擎 ejs npm install --save-dev ejs

var express = require('express');
var app = express();
var bs = require('browser-sync').create();
//nodeJs模板引擎,选用ejs(需要安装),如下配置才可正常使用.html文件作为view入口
app.engine('.html', require('ejs').__express);
//change the template main catelog
app.set('views',__dirname+'server/views/');
app.set('view engine','html')
// 设置以开发模式启动项目
app.locals.env = process.env.NODE_ENV || 'dev';
app.locals.reload = false;
var isDev = process.env.NODE_ENV !== 'production';
// 检查当前是否以开发模式启动项目,若是的话,启用webpack中间件生成预览服务器
if(isDev){
    const webpack = require('webpack');
    const webpackDevMiddleware = require('webpack-dev-middleware');
    const webpackHotMiddleware = require('webpack-hot-middleware');
    const config = require('./webpack.config.dev.js');
    const compiler=webpack(config);
    app.use(webpackDevMiddleware(compiler,{
      publicPath:config.output.publicPath,
      noInfo: false,
      stats:{
        colors:true
      }
    }));
    app.use(webpackHotMiddleware(compiler));
    app.get('/', function (req, res) {
      res.render('./index.html');
    });
    app.listen(3000,function(){
        console.log("App (dev) is running on port 3000.");
    })
}

env.NODE_ENV 是当前项目的运行环境参数,可通过命令行命令 set NODE_ENV = development 来设置,也可以在代码中通过 app.locals.env = development 来设置。上面代码的意思便是在开发模式中启动 webpack-dev 本地服务器进行本地预览,关于开发模式和上线模式之间的参数设置技巧可以看下这篇文章入门 Webpack,看这篇就够了

上面设置了在启动 Express 服务器的同时启动 webpack-dev 的本地预览服务器并实现在前端框架文件有改动的时候热启动重启插件或者刷新页面。

下面来说说如何监听后台文件更改和自动刷新。常用的思路是 supervisor 或者 browser-sync 这里使用 browser-sync

npm install --save-dev browser-sync

安装好插件之后,修改我们刚才的 app.js 文件:

var express = require('express');
var app = express();
var bs = require('browser-sync').create();
//nodeJs模板引擎,选用ejs(需要安装),如下配置才可正常使用.html文件作为view入口
app.engine('.html', require('ejs').__express);
//change the template main catelog
app.set('views',__dirname+'server/views/');
app.set('view engine','html')
// 设置以开发模式启动项目
app.locals.env = process.env.NODE_ENV || 'dev';
app.locals.reload = false;
var isDev = process.env.NODE_ENV !== 'production';
// 检查当前是否以开发模式启动项目,若是的话,启用webpack中间件生成预览服务器
if(isDev){
    const webpack = require('webpack');
    const webpackDevMiddleware = require('webpack-dev-middleware');
    const webpackHotMiddleware = require('webpack-hot-middleware');
    const config = require('./webpack.config.dev.js');
    const compiler=webpack(config);
    app.use(webpackDevMiddleware(compiler,{
      publicPath:config.output.publicPath,
      noInfo: false,
      stats:{
        colors:true
      }
    }));
    app.use(webpackHotMiddleware(compiler));
    app.get('/', function (req, res) {
      res.render('./index.html');
    });
    app.listen(3000,function(){
        bs.init({
          open: false,
          ui: false,
          notify: false,
          proxy: 'loaclhost:3000',
          files: ['./client/**'],
          port: 8080
        })
        console.log("App (dev) is going to be running on port 8080 (by browsersync).");
    })
}
...
"scripts": {
  "dev": "webpack-dev-server --progress --profile --colors --hot",
  "build": "webpack --progress --profile --colors",
  "test": "karma start",
  "kaifa": "set NODE_ENV = development && node app.js",
  "pd": "set NODE_ENV = production && node app.js"
},
...
"devDependencies": {
  "babel-core": "^6.23.1",
  "babel-loader": "^6.3.2",
  "babel-plugin-react-transform": "^2.0.2",
  "babel-preset-es2015": "^6.22.0",
  "babel-preset-react": "^6.23.0",
  "babel-preset-stage-2": "^6.22.0",
  "browser-sync": "^2.18.8",
  "css-loader": "^0.26.1",
  "ejs": "^2.5.6",
  "express": "^4.14.1",
  "file-loader": "^0.10.0",
  "html-webpack-plugin": "^2.28.0",
  "jquery": "^3.1.1",
  "less": "^2.7.2",
  "less-loader": "^2.2.3",
  "node-less": "^1.0.0",
  "react": "^15.4.2",
  "react-dom": "^15.4.2",
  "react-redux": "^5.0.2",
  "react-transform-hmr": "^1.0.4",
  "redux": "^3.6.0",
  "style-loader": "^0.13.1",
  "url-loader": "^0.5.7",
  "webpack": "^2.2.1",
  "webpack-dev-middleware": "^1.10.1",
  "webpack-hot-middleware": "^2.17.0"
}
...

最后贴出 package.json 里面的 script 配置和依赖列表,dev 命令为启动 webpack-dev-server 的本地预览服务器,kaifa 命令为启动 browser-sync 本地服务器。

大概的流程便是这样,不过关于前后台结合的具体原理的流程我还有不理解的地方,慢慢会继续更新的。教程文档各有不同,框架接口也在不断更新调整,要多逼自己去看官方文档和亲手测试。

博客书写格式简要

记录下我以后博客的书写格式,以免忘记以及慢慢完善。
部分内容参考自阮一峰-中文技术文档的写作规范

段落

层次

文章最多只能有两级目录,第一级是 ## 大小的,第二级是 ### 大小的。

若二级目录下还有分节需要,尽量使用项目列表

格式

一级段落之间使用分割线进行区分 ***


文本

全角中文和半角英文之前,应有一个半格空格分隔。

错误:本文介绍Window系统。
正确:本文介绍 Window 系统。

全角中文字符与半角阿拉伯数字之间,有没有半角空格都可,但必须保证风格统一,不能两种风格混杂。

正确:2011年5月15日,我订购了5台笔记本电脑与10台平板电脑。
正确:2011 年 5 月 15 日,我订购了 5 台笔记本电脑与 10 台平板电脑。

半角的百分号,视同阿拉伯数字。

英文单位若不翻译,单位前的阿拉伯数字与单位间不留空格。

错误:一部容量为 16 GB 的智能手机
正确:一部容量为 16GB 的智能手机

半角英文字符和半角阿拉伯数字,与全角标点符号之间不留空格。

错误:他的电脑是 MacBook Air 。
正确:他的电脑是 MacBook Air。


句子

尽量不使用被动语态,改为使用主动语态。

错误:假如此软件尚未被安装,
正确:假如尚未安装这个软件,

不使用非正式的语言风格。

错误:Lady Gaga 的演唱会真是酷毙了,从没看过这么给力的表演!!!
正确:无法参加本次活动,我深感遗憾。

用对“的”、“地”、“得”。

她露出了开心的笑容。
(形容词+的+名词)

她开心地笑了。
(副词+地+动词)

她笑得很开心。
(动词+得+副词)

使用代词时(比如“其”、“该”、“此”、“这”等词),必须明确指代的内容,保证只有一个含义。

错误:从管理系统可以监视中继系统和受其直接控制的分配系统。
正确:从管理系统可以监视两个系统:中继系统和受中继系统直接控制的分配系统。

名词前不要使用过多的形容词。

错误:此设备的使用必须在接受过本公司举办的正式的设备培训的技师的指导下进行。
正确:此设备必须在技师的指导下使用,且指导技师必须接受过由本公司举办的正式设备培训。

单个句子的长度尽量保持在 20 个字以内;20~29 个字的句子,可以接受;30~39 个字的句子,语义必须明确,才能接受;多于 40 个字的句子,在任何情况下都不能接受。

错误:本产品适用于从由一台服务器进行动作控制的单一节点结构到由多台服务器进行动作控制的并行处理程序结构等多种体系结构。
正确:本产品适用于多种体系结构。无论是由一台服务器(单一节点结构),还是由多台服务器(并行处理结构)进行动作控制,均可以使用本产品。

同样一个意思,尽量使用肯定句表达,不使用否定句表达。

错误:请确认没有接通装置的电源。
正确:请确认装置的电源已关闭。

避免使用双重否定句。

错误:没有删除权限的用户,不能删除此文件。
正确:用户必须拥有删除权限,才能删除此文件。

nginx 安装及使用技巧

第一步:安装 epel 库

sudo yum install epel-release

第二步:安装 nginx

sudo yum install nginx

安装好之后 nginx 应该会自动添加到系统变量中,可以用指令测试一下 nginx 的是不是安装好了和获取配置文件位置。

nginx -t

如果能够正常显示 nginx 的配置文件路径的话,代表 nginx 已经成功安装,但这时候它还没被启动,你需要通过以下命令来启动它:

systemctl start nginx

以下是一些常用的 nginx 命令

作用 命令
测试 nginx 配置是否正确 nginx -t
设置 nginx 为自启动 systemctl enable nginx
启动 nginx systemctl start nginx
重启 nginx nginx -s reload

网络协议简述

协议是什么?

一句话简述:协议是计算机之间实现通信所定义的规则,计算机之间按照这种事先达成的约定实现通信。

例:计算机网络体系结构

网络体系结构 包含协议 主要用途
TCP/IP IP、ICMP、TCP、UDP、HTTP、TELNET、SNMP、SMTP… 互联网、局域网
IPX/SPX(NetWare) IPX、SPX、NPC… 个人电脑局域网
AppleTalk DDP、RTMP、AEP、ATP、ZIP… 苹果公司现有产品的局域网
DECnet DPR、NSP、SCP… 前DEC小型机

协议有很多种,每一种协议都明确的界定了它的行为规范,协议规定了使用某种媒介进行通讯的具体方式和通讯过程中遇到异常的处理方式。

计算机之间的协议的实现一般使用分组交换的方式。


分组交换是什么?

分组交换是指将大数据分为一个个叫做包的较小单位进行传输的方法。分组交换的灵感是来源于生活中邮寄包裹的行为。我们在邮寄包裹的时候需要提交寄件人地址、收件人地址等信息;同样的,在通信过程中,也会为每个包附上源主机地址、目标主机地址、分组序号等信息放到分包数据的前端、称为“报文首部”。

通信协议会规定报文首部应该写入那些信息、应该如何处理这些信息,接收端主机同样根据这个协议复原数据。


协议由谁制定?

协议的诞生和标准化一开始并没有得到重视,之后由于计算机之间的通信的需求,各家计算机厂商开始研发他们自己的网络产品和网络系统来实现计算机通信。各个厂商之间的使用的网络产品并不相同,所以它们的计算机之间无法实现通信。

为了实现异构计算机之间的通信,ISO制订了一个国际标准OSI,对通信系统进行了标准化。虽然OSI协议并没有得到普及,但是OSI参考模型在网络协议的制定中发挥了很大的作用。

现在流行使用的TCP/IP体系是由IETF所制定的,以当时的大学等研究机构和计算机行业作为中心力量,推进了TCP/IP的标准化进程。使之成为了互联网通信的标准和相关的生产行业所应用的标准。


协议的分层思路

协议的分层是指将计算机之间的通信行为以功能为角度进行层次划分,例如OSI参考模型将通信协议的必要功能分为了7层,能够使一些复杂的协议简单化。在OSI模型中,每个分层都接收它下一层所提供的特定服务,并负责为它的上一层提供特定服务,上下层之间进行交互时所遵循的约定叫做“接口”,同一层之间交互时遵循的约定叫做“协议”。

OSI参考模型是一个理想化的模型,将网络功能划分为七层。这样的好处是各层独立使用,即使某层出现问题也不会波及整个系统,通过细分通信功能,使各层各司其职,每个分层的协议易于实现,灵活性和扩展性都很强。不过分层太细的缺点也是明显的,那就是过分模块化,每个分层共同执行的重复操作占据大量的资源等。

下面通过一个简单的情景模拟来理解分层思路,有两位同学A君和B君,他们通过手机进行通话,那么A君B君也就是人类就是第一层,手机可以笼统的划分为第二层。A君B君使用手机提供的拨号程序和话筒(接口)来使用手机提供的服务,而A君B君之间通话使用的语言和语法就是他们之间通信使用的协议。同样的,手机之间将语音信息和电子信息进行转换的规则就是他们之间使用的协议。


OSI参考模型

分层名称 功能
应用层 针对特定应用的协议
表示层 设备固有数据格式和网络标准数据格式的转换
会话层 通信管理,负责决定何时建立和断开通信连接
传输层 管理两个节点(互联的网络终端)之间的通信传输。负责保证可靠传输(确保数据能够可靠的传送到目标地址)
网路层 地址管理和路由选择
数据链路层 互联设备之间传送和识别数据帧
物理层 以“0”和“1”代表电压的高低、灯光的闪灭。界定连接器和网线的规格。

解决 roadhog 单页应用提取公共模块的问题

前言
roadhog 在打包单页应用文件的时候如果采取异步加载模块文件的话,每个模块里面应用到的一些公用库(antd、moment等)会在每个生成的异步模块文件里面重复应用到,导致所有文件的总体积增加了很多(因为公用库被重复引用了)本文描述的是如何解决这个问题

相关仓库版本

  • roadhog: 1.1.1

问题描述

在 cpbs PC 端开发过程中,发现在经过 roadhog build --debug --analyze 命令打包之后的文件体积很大,具体内容如下所示:

Compiled successfully in 25.6s.

File sizes after gzip:

  1.56 MB    dist\index.js
  1.27 MB    dist\role.async.js
  1.26 MB    dist\privilegeAudit.async.js
  1.23 MB    dist\merchant.async.js
  1.23 MB    dist\agent.async.js
  1.23 MB    dist\merchantAudit.async.js
  1.23 MB    dist\agentAudit.async.js
  1.23 MB    dist\merchantReview.async.js
  1.23 MB    dist\agentReview.async.js
  1.22 MB    dist\user.async.js
  1.18 MB    dist\privilege.async.js
  1.15 MB    dist\transactionTotal.async.js
  1.14 MB    dist\statisticRemit.async.js
  1.14 MB    dist\statisticContrast.async.js
  1.14 MB    dist\transactionFlow.async.js
  239.48 KB  dist\login.async.js
  17.2 KB    dist\index.css
  3.93 KB    dist\error.async.js
  1.7 KB     dist\antd.js

Analyze result is generated at dist/stats.html.

roadhog 将每个 model 都打包为一个单独的异步调用的 js 文件,这些文件即使在 gzip 压缩之后每个都需要平均 1M 的体积,在查看打包的具体内容 (stats.html)之后,发现体积过大的这些包都对一些共同使用到的组件进行了打包,而非引用模块部分的代码体积只有 300k 左右:

模块打包文件示意图

经过分析,发现体积过大的原因有以下几点:

  • moment 模块被重复引用
  • moment 模块引用了多余的本地化文件(约300k)
  • antd 重复引用到的模块,如 TableButtonForm 等被各个模块重复打包,增加了体积

解决思路

  • 将体积大的公用的模块进行打包(momentantd部分使用频率高的模块)
  • 移除 moment 模块没有使用到的语言包

本文只讲述如何将公共模块抽离出来作为一个单独文件


解决过程

使用 CommonsChunkPlugin 将公用模块抽离到 ventor.js

修改 roadhog 的配置文件:

// .roadhogrc.js
  ...
  "entry": "./src/index.js",
  ...

在根目录添加 webpack.config.js,输入以下内容:

const webpack = require('webpack')

module.exports = function wp(webpackConfig, env) {
  // 对roadhog默认配置进行操作,比如:
  if (env === 'production') {
    // 上线环境使用分包打包方式  
    webpackConfig.entry = {
      index: './src/index.js',
      vendor: [
        'moment',
        'mockjs',
        'lodash',
        'react',
        'react-dom',
        'react-helmet',
      ],
    }
    // webpackConfig.plugins.push(new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/))
    webpackConfig.plugins.push(new webpack.optimize.CommonsChunkPlugin({
      name: ['vendor'],
      minChunks: Infinity,
    }))
  }

  return webpackConfig
}

至此,完成了将除 antd 之外的公用模块的抽离

使用 CommonsChunkPluginantd 的高频模块抽离到 antd.js

修改 webpack.config.js为:

const webpack = require('webpack')

module.exports = function wp(webpackConfig, env) {
  // 对roadhog默认配置进行操作,比如:
  if (env === 'production') {
    // 上线环境使用分包打包方式  
    webpackConfig.entry = {
      index: './src/index.js',
      vendor: [
        'moment',
        'mockjs',
        'lodash',
        'react',
        'react-dom',
        'react-helmet',
      ],
      antd: [
        'antd/lib/button',
        'antd/lib/icon',
        'antd/lib/table',
        'antd/lib/date-picker',
        'antd/lib/form',
        'antd/lib/modal',
        'antd/lib/grid',
        'antd/lib/input',
      ],
    }
    // webpackConfig.plugins.push(new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/))
    webpackConfig.plugins.push(new webpack.optimize.CommonsChunkPlugin({
      name: ['vendor', 'antd'],
      minChunks: Infinity,
    }))
  }

  return webpackConfig
}

修改 index.htmlindex.ejs

/src 目录下的文件进行如下调整:

|-index.ejs
|-index.js
|-index.less
|-router.js

各个文件的配置为:

<!-- index.ejs -->
<!DOCTYPE html>
<html lang="zh-cn">
<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">
</head>
<body>
<div id="root"></div>
</body>
</html>
import 'babel-polyfill'
import dva from 'dva'
import createHistory from 'history/createBrowserHistory'
import createLoading from 'dva-loading'
import { message } from 'antd'
import moment from 'moment'
import 'moment/locale/zh-cn'
import './index.less'
import './styles/lib/animate.css'    // 引入全局样式动画库
import './themes/index.less'

moment.locale('zh-cn')
const ERROR_MSG_DURATION = 3 // 3 秒

// 1. Initialize
const app = dva({
  history: createHistory(),
  onError(error) {
    console.error(error)
    message.error(error.message, ERROR_MSG_DURATION)
  },
})

// 2. Plugins
app.use(createLoading({ effects: true }))

// 3. Model

// 4. Router
app.router(require('./router'))

// 5. Start
app.start('#root')

index.lessrouter.js 的内容这里不赘述

经过以上配置之后,index.ejs 能够根据 roadhog 里面的 entry 配置自动引入所有入口文件。

注意 CommonsChunkPlugin 配置里面的 name 的数组顺序会影响到 entry 的引用顺序,如果出现 webpack 未定义的错误,尝试改变这个属性值的顺序,因为 CommonsChunkPlugin 是按照抽离顺序将 webpack 的全局对象放到了 entry 的最后一个入口文件里面。


解决成果

经过上面的处理之后,打包的个文件体积为:

Compiled successfully in 21.1s.

File sizes after gzip:

  1.41 MB    dist\antd.js
  596.22 KB  dist\index.js
  556.53 KB  dist\role.async.js
  549.17 KB  dist\privilegeAudit.async.js
  526.47 KB  dist\merchant.async.js
  524.27 KB  dist\agent.async.js
  520.04 KB  dist\merchantAudit.async.js
  519.58 KB  dist\agentAudit.async.js
  516.8 KB   dist\merchantReview.async.js
  516.34 KB  dist\agentReview.async.js
  508.46 KB  dist\user.async.js
  474.88 KB  dist\privilege.async.js
  443.19 KB  dist\transactionTotal.async.js
  434.33 KB  dist\statisticRemit.async.js
  433.45 KB  dist\statisticContrast.async.js
  429.59 KB  dist\transactionFlow.async.js
  279.46 KB  dist\vendor.js
  112.09 KB  dist\login.async.js
  17.2 KB    dist\index.css
  3.92 KB    dist\error.async.js

Analyze result is generated at dist/stats.html.

可以看到被抽离了公用模块之后的各模块的体积基本减少到了 500k(具体抽离哪些模块还可以再精准研究),代码包的整体面积无疑是减少了很多的。


后续报道

发现在使用多入口方式的时候无法触发热更新,所以在开发环境之下还是使用旧有的打包方式,只在线上环境使用抽离式打包,解决方式就是在 webpack.config.js 里面添加一个环境变量判断,详细见上面代码。

const webpack = require('webpack')

module.exports = function wp(webpackConfig, env) {
  // 对roadhog默认配置进行操作,比如:
  if (env === 'production') {
    // ...do what you want
  }

  return webpackConfig
}

资料来源

roadhog - 支持 vendor 的配置 #370

浏览器工作原理

介绍

浏览器的主要功能

浏览器的主要功能是将用户选择的网络资源(HTML、图像、视频等)呈现出来,用户通过输入URI(Uniform Resource Identifier)来指定网络资源的位置,浏览器将其显示在浏览器窗口中。

浏览器的主要组件

浏览器的主要组件包括:

1.用户界面 - 包括地址栏、后退/前进按钮、书签栏等,在多年的浏览器竞争中,浏览器的用户界面组成趋于稳定和统一,也有可以自行定制用户界面的方式。

2.浏览器引擎 - 用来查询及操作渲染引擎的接口。

3.渲染引擎 - 用来显示请求的内容。例如,如果请求内容为HTML,渲染引擎负责解析HTML及CSS,并将解析后的结果显示在浏览器窗口。

4.网络 - 用来完成网络调用,例如http请求。

5.UI后端 - 用于绘制类似组合选择框和对话框等基本组件,具有不特定与某个平台的通用接口,底层使用操作系统的用户接口。

6.JS解释器 - 用于解释执行JS代码。

7.数据存储 - 处于持久层。浏览器需要在硬盘中存储类似Cookie等数据,HTML5定义了webDatabase,这是一种轻量级的客户端存储技术。

浏览器组件层次图

注:不同于大部分浏览器,Chrome为每个Tab都分配了各自的渲染引擎,每个Tab都是独立的进程。


渲染引擎

渲染引擎的职责是渲染,在浏览器窗口显示所请求的内容。

默认情况下是显示HTML、XML文档及图片的,可借助插件的显示其他数据(如PDF等文档),这里主要讲述渲染引擎的最主要用途–显示HTML文档既其之上的CSS和图片。

渲染引擎简介

主流浏览器中,Firefox使用的渲染引擎是Geoko–Mozilla自主研发的渲染引擎,Safari和Chrome是用的渲染引擎都是Webkit。Webkit是一款开源渲染引擎,本来是为Linux平台研制的后来由Apple移植到Mac以及WIndow平台上。

渲染主流程

渲染引擎在接收到网络传回的文档时,按照下面流程来显示页面。

解析HTMl,生成DOM树 > 构建Render树 > 布局Render树 > 绘制Render树

1.渲染引擎开始解析HTML,将标签转化为内容树中的DOM节点。

2.解析外部CSS文件和Style标签中的样式信息,这些信息与html中的可见性指令将被用作与DOM节点一起构造Render树。Render树是由一些包含颜色和大小等属性的矩形组成,它们将被按照正确的顺序显示到屏幕上。

3.Render树构造好之后,将会执行布局过程。先是确定每个节点在屏幕上准确的坐标,下一步就是绘制,遍历Render树,使用UI后端层绘制每个节点。

以上过程理论上是逐步完成的,但是为了更好的用户体验,渲染引擎会尽可能早的将内容显示到屏幕上,所以它的处理方式是不会等到所有html都解析完再构造和布局Render树,它会解析完一部分内容就显示这一部分的内容,同时可能还在通过后台网络下载其他内容。

Geoko内核渲染引擎工作流图

Webkit内核渲染引擎工作流图

以上是两种内核的工作流程图。不难发现,尽管Webkit和Geoko的术语略有不同,但是它们的主要流程基本相同。

1.Geoko称可见的格式化元素组成的树为Frame树,Webkit称之为Render树。

2.Geoko将元素的定位成为回流(Reflow),Webkit称之为布局(Layout)。

3.Geoko是把样式文件和字段从html中筛选出来的,而Webkit则是同时进行解析html文档和样式文件的解析操作。

下面讨论流程中的各个阶段。


解析与DOM树构建

解析(Prasing - general)

解析是渲染引擎工作流中一个非常重要的过程,解析一个文档即将其转换为具有一定意义的结构–编码所能够理解和使用的东西。解析的结果一般是表达文档结构的节点树、称为解析树或者语法树。

例如,解析“2+3-1”这个表达式,可能返回这样一棵树。

数学表达式节点树

文法(Grammars)

解析基于文档所使用的语法规则–文档的语言或者格式。可被解析的格式必须具有由词汇及语法规则组成的特定的文法,称为上下文无关文法。人类语言不具有这一特性,因此不能被一般的解析技术所解析。

解析器-词法分析器(Parser-Lexer combination)

解析可以分为两个子过程–语法分析和词法分析。

词法分析就是将输入分解为符号,符号是语言的词汇表–基本有效单元的集合。对人类语言来说,它相当于我们字典中出现的所有单词。

语法分析指对语言应用语法规则。

解析器一般将工作分配给两个组件–词法分析器(有时也叫分词器)负责将输入分解为合法的符号,解析器则根据语言的语法规则分析文档结构,从而构建解析树。词法分析器也负责如何跳过空白和换行之类的无关字符。

源文档到解析树

解析过程是迭代的,解析器从词法分析器处取到一个新的符号,会试着用这个符号匹配一条语法规则,如果有,则这个符号对应的节点将被添加到解析树上,然后解析器接着请求另一个符号。如果没有匹配到规则,解析器将在内部保存该符号,并从词法分析器取下一个符号,直至所有内部保存的符号能够匹配一项语法规则。如果最终没有找到匹配的语法规则,解析器将抛出一项异常,这意味文档无效或者是包含语法错误。

转换(Translation)

很多时候,解析树并不是最终结果。解析一般在转换中使用–将输入文档转换为另一种格式。例如编译,编译器在将一段代码编译为机器码的时候,先将源码解析为解析树,然后将解析树转换为一个机器码文档。

编译流程图

FLux架构知识总结

本文来源:Flux 架构入门教程

写在前面

前端技术最近很热门的React,由于它只负责UI层,如果搭建大型应用的话,需要配合使用前端框架,也就是必须学习使用两套东西:React + 前端框架。

Facebook官方使用的Flux框架,如何在使用React的基础上,使用Flux架构组织和安排代码是我们要掌握的。

React的基础教程:《React入门教程》


Flux是什么

Flux是一种软件架构**,专门解决软件的结构问题,它和MVC、MVVM架构是同一类东西,优点是更加简单和清晰,Flux架构有很多种实现,这里介绍Facebook官方实现。


基本概念

先了解一下Flux的基本概念,Flux把一个程序分为四个部分。

  • Action:动作,视图层发出的消息,如点击。
  • Dispatcher:派发器,用来接收Actions,执行回调函数。
  • Store:数据层,用来存放应用的状态,一旦发生变动,就提醒View要更新页面。
  • View:视图层

Flux架构示意图

Flux架构的最大特点是数据的单项流动。

  1. 用户访问 View
  2. View 发出用户的的 Action
  3. Dispatcher收到用户的 Action,要求 Store 进行相应的处理
  4. Store 处理完之后,发出要求 View 进行更新的事件
  5. View 收到事件之后,更新页面

Facebook的Flux实现

来源:facebook/flux

简介

使用React和单向数据流的应用构造体系

Flux数据流示意图

开始使用

指引和例子

更多资源和APi文档

Flux如何工作

Flux概念

详细概念

环境要求

Flux更像是一个设计范式而不是具体框架,也没有什么重度依赖的环境。不过,我们通常使用 EventEmitter 来作为Stores的基础、React 作为 View,剩下的Dispatcher 部分是不那么容易在其他地方找到现成的模块,所以我们在这里提供了我们现成的模块来完善你的Flux工具箱。

安装Flux

Flux已经作为一个npm模块发布了,所以你可以把它添加到你的 package.json 文档中或者运行 npm install flux 安装。我们提供的 Dispatcher 你可以通过 Flux.Dispatcher 的方式调用,或者你可以这样进行引用:

const Dispatcher = require('flux').Dispatcher;

你可以浏览一下Dispatcher的API和例子

Flux实用工具

我们也提供了一些基础工具类来帮助您开始使用Flux。这些工具类是为一个简单的Flux应用开发的简单方法而已,并不是成型的框架,所以可能不能够解决所有应用场景,其他开发者还许多优秀的Flux框架能够完全满足您的需求。

import {ReduceStore} from 'flux/utils';
class CounterStore extends ReduceStore<number> {
  getInitialState(): number {
    return 0;
  }
  reduce(state: number, action: Object): number {
    switch (action.type) {
      case 'increment':
        return state + 1;
      case 'square':
        return state * state;
      default:
        return state;
    }
  }
}

这里可以查看更多的实例文档

克隆我们的代码并运行Flux

本页面的代码克隆到本地,进入 flux 目录并运行 npm install

通过运行 Gulp-based 自动编译出 Flux.js 文件,你可以将它引用到您的模块当中。

比如像这样子引用Dispatcher:

const Dispatcher = require('path/to/this/directory/Flux').Dispatcher;

编译同时会在 lib 目录生成压缩版本的 Dispatcherinvariant 模块,你可以直接引用这些模块,或者尽你所便将它们复制到你想要的目录下面,就像flux-todomvc的Demo和flux-chat的Demo那样。

加入我们的Flux讨论组

联系我们

Hexo使用手册(Window系统)

前言
本文是我对自己使用Hexo全过程的整理,包括安装、配置、主题设置、博文编写等方面的内容。

Hexo 简介

Hexo是一款简洁快速的博客框架,依赖于Node.js实现博客页面的渲染和生成,使用git一键部署到Github Page上,Hexo官网上面有简单的使用文档和接口文档可供阅读。


Hexo 安装

环境搭建

步骤 作用 方法
安装Node.js 安装Hexo和作为Hexo的部署环境 去官网安装对应版本即可
安装git 提交Hexo的代码到github代码仓库 上同
申请github账号并建立Github Page代码仓库 作为Hexo的摆放空间 这里不详述

以上步骤都完成之后就可以进入下一步骤,进行Hexo的安装了

安装流程

笔者安装的hexo版本为3.2.2、node的版本为4.4.3、电脑系统是Win10

建立工作目录

在本地建立一个存放Hexo的文件夹,同时这个文件夹也是你博客内容生成的“工厂”,存放Hexo编译(或者称为转换)前的文件。

安装Hexo

在工作目录中运行命令行(可以按着Shift键点击右键,在右键菜单中选择“在此处打开命令行”)输入下列指令安装Hexo(如果已经安装过Hexo的略过此步)。

npm install hexo-cli -g

初始化Hexo

在上文的命令行中输入以下指令,在工作目录下初始化Hexo。

hexo init

安装成功

在命令行中输入以下指令看是否能获取到hexo的帮助信息,若能,则Hexo初始化成功了。

hexo help

Hexo配置

工作目录根目录下的_config.yml是Hexo的配置文件,下文不会详述,具体每个配置项的作用和可选值请参看官方文档

个人配置

以下列出一些建议修改的配置项,

配置项 作用 建议值
title 网页标题
author 网页作者
new_post_name 新建文章时自动生成的文件名的命名规律 :year-:month-:day-:title.md

发布配置

需要将Hexo生成的静态博客文件上传到Github Page空间的话,我们需要修改配置文件中_cofing.yml的“deploy”类选项。

还需要安装一个模块 hexo-deployer-git

npm install --save hexo-deployer-git
# Deployment
## Docs: https://hexo.io/docs/deployment.html
deploy:
  type: git
  repo: https://github.com/Runtu4378/Runtu4378.github.io.git
  branch: master

主题安装及配置

Hexo有许多不同程序员制作的风格各异的主题,官网也有一个主题页面,这里列出我安装并配置Next的过程。

预览页面
主题文档

主题安装

工作目录下面的这个文件夹是存放主题的文件夹\themes\,在这个文件夹下有一个Hexo自带的默认表情“landscape”,我们安装其他的过程就是把这个主题的内容的文件夹放到这个主题文件夹下,再修改Hexo配置文件_config.yml中的theme字段为主题名,应用主题。

那么我们回到博客工作目录的根目录,在命令行中把主题的文件下载到主题文件夹中,执行以下指令。

git clone https://github.com/iissnan/hexo-theme-next themes/next

然后修改Hexo配置文件_config.yuml中的theme的值为next。

hexo clean
hexo generate
hexo server

然后在浏览器中输入localhost:4000/即可看到主题应用之后的效果。

主题配置

各个主题的配置各有异同,不过主要是几种主要配置:分类标签评论类插件搜索类插件图床类配置等几类配置的组合,现有的主题一般是这些功能的排列组合。

在Hexo中我进行了标签功能的配置 以下分步说明

  1. 修改主题配置文件
    主题配置文件是位于主题内容文件夹中的_config.yml,本文即是themes/next/_config.yml,找到其中的“menu”类别配置中的tags项,将其设置为/tags。这样在博客的菜单栏即会出现”标签“这个选项。
  2. 生成标签分页
    在命令行中输入指令:
hexo new page tags

然后会生成标签页的分页页面(这里的具体实现我也不清楚),这样的话,在写博文的时候添加了标签的页面就会在标签分页中自动规整了。

命令介绍

Hexo提供在博客生成方面的指令,具体使用可以查看命令文档

我把博客的日常使用概括为编写博文和发布博文,下面演示一次“Hello world”博文编写流程。

编写新博文

hexo new "Hello world"

键入以上指令,hexo会在source/_posts/路径下自动生成一个名为Hello world.md的文件,这个文件即是这篇博文的物理文件了,我们使用Markdown格式编写好这问文件之后,在静态生成页面的阶段会把这份文件转化为html文件。

打开这个md文件,下面是一个典型的文件格式。

---
title: Helloworld
date: 2016-09-29 14:34:37
tags:
- tag1
- tag2
---
具体内容

tag项下面列的是该文章所属的标签,date项即是文章的书写日期,我们编辑好博文的文章内容之后可以在本地预览,也可发布到云端仓库查看效果

本地预览效果

写完博文之后,最好先在本地服务器预览一下效果,依次输入以下指令打开本地服务器预览效果。

$ hexo clean
$ hexo g
$ hexo s

此时,使用浏览器打开相应的地址即可预览。

发布新博文

发布博文的前提是已经进行过提交选项的设置并已在云端有一个发布空间,例如我使用的Github Page空间,键入以下指令:

hexo clean
hexo d -g

clean 指令是清除之前的缓存文件和已经生成的静态文件。
d -g 这条指令就是在 delpoy 之前进行 generate 操作,即是先生成静态文件,然后提交变更到云端。

至此,“Hello world”这篇博文成功发布到云端了,继续享受你的Hexo之旅吧。

使用npm-script便捷本地预览和发布博文

部分内容来源阮一峰-npm scripts 使用指南

每次本地预览和提交博文都需要繁琐的输入多条指令的日子不好过吧,来使用 npm script 解放双手吧。

打开 hexo 项目根目录下的 package.json 文件,添加或将 scripts 配置项修改为下文所示:

...
"scripts": {
  "local": "hexo clean && hexo g && hexo s",
  "net": "hexo clean && hexo d -g"
},
...

这样子修改的目的是使你在命令行中输入 npm run local 来代替 hexo cleanhexo ghexo s 三次命令的依次输入。

修改之后,输入 npm run local 即能启动本地预览,输入 npm run net 即能提交博文更改到网络空间中。

进阶的配置还能够安装 open 模块,npm install --save open。该模块的功能是在默认浏览器打开指定网页。

即可将 package.json 文件的 scripts 配置项修改为:

...
"scripts": {
  "local": "hexo clean && hexo g && hexo s && node local.js",
  "net": "hexo clean && hexo d -g && node net.js'"
},
...

然后在根目录下新建两个 js 文件,内容如下:

// local.js
var open= require('open');
open('http://127.0.0.1:4000/');
// 由于 hexo s 会阻塞脚本命令的执行 所以暂时只能先打开浏览器再打开服务器 等服务器打开后刷新页面即可
// net.js
var open= require('open');
open('http://runtu4378.github.io/');

这样就可以在实现上面便捷输入连串指令的基础上增加一步,打开本地预览的网址或者是网络空间博客地址。


静态资源文件夹

Hexo提供了使用静态资源文件夹的相关设置资源文件夹|Hexo,提供了通过 source/images 来统一放置图片和使用与博文相同名字的文章资源文件夹来组织化管理图片资源的方式。

其中文章资源文件夹的方式是此章节说明的重点,方便不喜欢用图床来管理和使用图片的用户。

首先,在 _config.yml 文件中将 post_asset_folder 设置为 true

开启了资源文件夹之后,Hexo 会在你每次使用 hexo new [layout] <title> 指令的时候在相同路径下创建一个与文章同名的文件夹。可以通过相对路径的方式来引用图片(将图片和博文视为在同一目录)。

不过我按照文档中的使用并无法实现(我的HExo版本是3.2.2),找了很久,终于找到了一个解决的方法–在 hexo 中无痛使用本地图片

这个方法的重点是在原有的hexo基础上手动添加一个插件 hexo-asset-image

$ npm install https://github.com/CodeFalling/hexo-asset-image --save

安装了这个插件之后,就可以实现官方文档宣称的静态资源文件夹的功能了。假设以下是其中一篇博文的文档结构:

MacGesture2-Publish
├── apppicker.jpg
├── logo.jpg
└── rules.jpg
MacGesture2-Publish.md

需要注意的是,在md文件中需要按照以下方式才可以正常引用本地图片 ![logo](/logo.jpg)


使用 hexo-admin 进行博客管理

使用 hexo-admin 插件能够提供一个对博文进行管理的管理后台。

安装方法

npm i --save hexo-admin

安装完毕之后,输入以下命令启动 hexo 本地服务器

hexo server -d

启动完毕会有 Hexo is running at http://localhost:4000/. 的提示,此时打开上述地址即是 hexo 的预览地址。

浏览器输入 http://localhost:4000/admin 进入管理后台,进入如下菜单 Settings -> Settings -> Setup authentification here 进入登录账号的设定。

设置好登录账号、密码之后将如下所示的一系列配置项复制到 hexo 根目录的 _config.yml 文件中,即可生效。

# hexo-admin authentification
admin:
  username: username
  password_hash: $2a$10$L.XAIqIWgTc5S1zpvV3MEu7/rH34p4Is/nq824smv8EZ3lIPCp1su
  secret: my super secret phrase

前端预览 PDF 的实现方式

结论
1.PDF 的预览行为是浏览器插件(浏览器厂商实现的)所实现的,浏览器对 Content-Typeapplication/pdf 的 GET 请求的默认行为是下载文件
2.要实现可靠的,表现一致的网页端对 PDF 预览,只能使用 js 插件实现

后端提供PDF内容的方式

前端一般是通过调用后端接口去获取 PDF 文件(无论是静态 PDF 文件或是动态的 PDF 文件),不论请求类型,后端返回 PDF 内容通常有以下两种内容格式:

  • 二进制: 此时响应的类型 Content-Typeapplication/pdf ,浏览器对此类型的响应的默认处理方式为下载,在使用了预览插件的浏览器会不下载文件而是触发预览行为
  • Base64: 此时响应的类型可能是 text/html,因为此时的 Base64 编码是放在返回体中的,Base64的处理方式实际是后台将 PDF 文件的二进制内容转换为 Base64 编码,此时的文件预览和上述二进制的处理方式基本是一样的,区别在于你需要告诉浏览器这串字符的格式是 Base64,需要将其转换为 PDF,在字符前面增加这串字符即可 data:application/pdf;base64,

利用浏览器自带插件实现 PDF 预览

缺点

  • 不是所有浏览器都有实现 PDF 文件预览,没有实现预览的浏览器会触发 PDF 文件的下载行为
  • 浏览器实现预览的效果各不相同,而且和浏览器的版本有关(可能有些版本的预览功能有 bug)

优点

  • 成本低,适合作为渐进增强
  • 简单,不依赖于前端

如何开启或关闭浏览器的 PDF 预览功能

比如在 Chrome 浏览器,可以在下列菜单设置 PDF 预览功能的开关

设置 - 高级 - 内容设置 -PDF文档

实现示例

在新窗口打开 PDF 并进行预览或下载

<a href="/helloWorld.pdf" target="_blank">新窗口打开 PDF</a>

在页面内嵌 iframe 中打开 PDF 或进行下载

<iframe
  src="/helloWorld.pdf"
  width="100%"
  height="500px"
>
     
  This browser does not support PDFs. Please download the PDF to view it: <a href="/helloWorld.pdf">Download PDF</a>

</iframe>

在页面内嵌 iframe 中预览 Base64 编码格式的 PDF

<iframe
  src="data:application/pdf;base64,other base64 code"
  width="100%"
  height="500px"
>
        
  This browser does not support PDFs. Please download the PDF to view it: <a href="data:application/pdf;base64,other base64 code">Download PDF</a>
            
</iframe>

注意事项


利用 PDF.js 插件实现 PDF 预览

PDF.js 是 mozilla 开源的 JS 库,大概实现原理是利用算法将 PDF 转换为 js 数据结构然后利用 canvas 在网页中复现

缺点

  • 不支持低版本IE
  • 体积约 3m (gzip前)

优点

  • mozilla 出品,现在仍在更新
  • 不依赖浏览器的PDF预览插件,大概实现原理是利用算法将 pdf 转换为数据结构,然后在 canvas 上输出
  • 提供若干 API 接口,可以自行封装换页等插件

实现示例

<!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>editor</title>
  <script src="/static/pdfjs/pdf.js"></script>
</head>

<body>
  <canvas id="the-canvas"></canvas>
</body>

</html>
const pdfjsLib = window['pdfjs-dist/build/pdf']
// The workerSrc property shall be specified.
pdfjsLib.GlobalWorkerOptions.workerSrc = '/static/pdfjs/pdf.worker.js'

function initPDF () {
  cosnt url = '/helloWorld.pdf'  // pdf 文件的地址
  const loadingTask = pdfjsLib.getDocument({
    url: url,
  }) // 创建加载文件对象
  loadingTask.promise.then(function (pdf) {
    // pdf 对象是文件加载完成后转换得来的,保存了文件内容以及相关的信息,如 pdf.numPages 能获取到 pdf 的页数
    const num = 0  // 渲染第一页
    pdf.getPage(num).then(function(page) {
      console.log(`Page ${num} loaded`);
      
      var scale = 1.5 // 放大倍数
      var viewport = page.getViewport(scale);
      const canvas = document.getElementById('the-canvas')
      const ctx = canvas.getContext('2d')
      // Prepare canvas using PDF page dimensions
      canvas.height = viewport.height;
      canvas.width = viewport.width;

      // Render PDF page into canvas context
      var renderContext = {
        canvasContext: ctx,
        viewport: viewport,
      }
      var renderTask = page.render(renderContext);  // 生成 canvas 渲染任务
      renderTask.then(function () {
        console.log('Page rendered')  // 渲染完毕
      });
    });
  }).catch(function (reason) {
    console.error('Error: ' + reason)
  });
}

更多示例请见 mozilla/PDF.js 示例


参考资料

利用虚拟机实现 jenkins + PHP 项目自动部署

前言
记录一次利用虚拟机(两台)实现本地的 jenkins 环境搭建、PHP 代码运行环境搭建以及使用 jenkins 实现 PHP 代码的自动部署

环境搭建

分为 jenkins 环境搭建和 PHP 环境搭建,jenkins 环境搭建在器一台虚拟机上面(虚拟机1),搭建过程详见:Jenkins环境搭建

下面简单说一下 PHP 环境的搭建,这里使用 OneinStack 来进行搭建,具体方式详见 安装 - OneinStack

Git 安装

在虚拟机 1 中(安装 Jenkins 的机器)需要安装 git 才能够拉取代码

这里笔者的虚拟机安装的是 CetOS 自带 git

python 环境搭建

因为自动部署代码有使用到 python 编写,所以同时也要安装 python 环境

具体代码:

sudo yum install -y gcc libffi-devel python-devel openssl-devel

接下来是 python 的 pip 环境安装、需要先行安装 epel-release

yum install -y epel-release
yum install -y python-pip

升级 pip

pip install --upgrade pip

absible 安装

yum install -y ansible

输入 ansible --version 能够获取到 ansible 版本号即为安装成功


自动部署思路

自动部署包括机器管理、环境检查、代码部署、服务管理、错误通知等内容。

这里先实现最简单的几步:机器管理、代码部署和服务管理,实现通过 jenkins 对 PHP 代码进行拉取和部署、重启服务等。

具体方式是利用 jenkins 进行自动部署过程的触发,拉取 PHP 代码,然后触发 jenkins 所在机器(虚拟机1)上面的 ansible 的 playbook 任务,连接到本地安装了 PHP 环境的机器(虚拟机2)进行代码推送和服务重启等任务。


实施过程

安装 jenkins 插件

由于整个自动部署过程是从 jenkins 上面进行触发的(这里不选用代码管理库提交代码即通知 jenkins 进行构建的方式),所以我们需要先创建 jenkins 上面的任务,任务会从两个地方拉取代码(PHP 项目代码和部署脚本代码),然后触发部署过程。

要实现从多个地方拉取代码需要安装以下插件:[Multijob plugin]、[Multiple SCMs plugin]。

点击首页右侧的 “系统管理” - “管理插件” - “可选插件” 里面进行安装,勾选“安装完成后重启Jenkins(空闲时)”,即可。等待 jenkins 重启完成之后重新登陆 jenkins 即可进行下一步操作。

代码库登录秘钥管理

点击首页右侧的 “Credentials” 进去秘钥管理界面。按照下图方式添加一个对应代码库的域名(如github.com)

添加域名

接下来是为代码空间添加登录秘钥了(用来拉取代码的账号名和密码)

添加秘钥

添加及配置 jenkins 任务

接下来就是要添加 jenkins 任务了,点击首页右侧的“新建” - “构建一个自由风格的软件项目”

在“源码管理一栏” - “Multiple SCMs” 去添加要拉取的 PHP代码以及部署脚本代码

需要注意的是要额外设置一个 “Additional Behaviours":“Check out to a sub-directory” 来定义拉取后的代码放到什么目录

任务设置

设置完成之后点击保存,然后返回去到任务操作界面,点击“立即构建”如无意外(代码库秘钥不通过、或者虚拟机 1 的 git 没有安装或者异常等)的话,会构建成功,此时点击“工作空间”即能够看到对应的两个代码库的代码已经被正确放到对应的文件夹中

工作空间

部署脚本设计

完成代码的拉取之后,就到了部署脚本的设计环节,部署过程使用 ansible 的话,需要通过 ansible 解决以下问题:

  • 远程机器的管理
  • 部署过程的自动化、流程化

解决css-loader不同文件拥有相同的hash:base64的bug

前言
解决 css-loader 在启用 cssModules 的时候处理相同文件名的文件的时候会产生相同的 [hash:base64] 的 bug

bug 描述

css-loader 在启动了 modules 的时候,默认会将类名转换为 [hash:base64],出于开发方便的考虑,我们一般会配置转换规则 localIdentName[local]___[hash:base64],这样同时显示了类名和哈希串,便利了开发也实现了样式隔离。但是会出现一种情况,有时候相同文件名里面的相同样式所生成的哈希串会是一样的,这是为什么呢?

// webpack 配置
{
  test: /\.less$/,
  include: 'src',
  use: [
    {
      loader: 'style-loader',
    },
    {
      loader: 'css-loader',
      options: {
        modules: true,
        localIdentName: '[local]___[hash:base64]',
      },
    },
    {
      loader: 'less-loader',
    },
  ],
}
// /page1 下面的文件以及样式
index.less -> .main {}
// /page2 下面的文件以及样式
index.less -> .main {}
/* 生成的哈希值 */
/* /page1/index.less */
.main___3JR3AQ6dNZLxjhld7RKqmr
/* /page2/index.less */
.main___3JR3AQ6dNZLxjhld7RKqmr

原因

相关 issue
Same file name but different directory, after build hash is same.(use css module)
英文好的哥们可以直接进去看

大概的原因是 css-laoder 的实现里面是根据引用文件时的相对路径来作为文件夹的路径,上述两个文件在他们各自的根目录下被引用时文件名(以及路径)是一样的,所以生成的哈希串是一样的(这里的哈希串是以唯一的路径作为唯一的依据而非文件内容)。

解决方法是给 css-loader 添加一个参数 context 来作为判断文件路径的上下文(这个参数没有在文档中被列出,可能是 hack 出来的)比如是源代码的根路径 src,这样上面两个文件的路径就分别变为 /page1/index.less/page2/index.less 了。(我用的是 path 模块包装过的绝对路径,而不是下面写的相对路径)

// webpack 配置
{
  test: /\.less$/,
  include: 'src',
  // loader: 'style!css?minimize&-autoprefixer!postcss!less',
  use: [
    {
      loader: 'style-loader',
    },
    {
      loader: 'css-loader',
      options: {
        modules: true,
        context: 'src', // 修复不同文件拥有相同[hash:base64]的bug-https://github.com/webpack-contrib/css-loader/issues/464
        localIdentName: '[path]_[name]_[local]___[hash:base64]',
      },
    },
    {
      loader: 'less-loader',
    },
  ],
}

思考

这个问题大概耗费了一个上午的事件去解决,考虑过是不是被其他模块影响、模块有没有 bug、也看了下源码、查了下社区,但是都没有深入去钻,导致了时间白白流逝。

后面优化了一下关键字,从 [hash:base64] no unique,到 different file same [hash:base64] localIdentName 等关键字,才终于找到了相关的讨论,解决这问题的哥们正是从源码中发现了设计者的上述的设计误区。

更加有趣的是,没找到解决问题前,localIdentName 配置项中的 [path] 在相同相对路径相同文件名里面被输出的都是空(/),在设置了引用上下文之后,[path]便能被正确解析出来了。

所以,以后遇到问题,最好能后沉下心来,试着从反思到的这几点着手解决

  • 整理模块的使用方式和阅读文档,但有无错误使用
  • 试着查看源码,找出出错部分的逻辑设计与文档设计预期是否相同
  • 试着调整关键字查找相关讨论,沉下心挑战英文环境的不适应
  • 解决完问题记得要记录和整理

如何编写具有代码提示的代码,如何配置 vscode 的目录别名

本文解决问题
1.如何优化 vscode 的代码提示功能(如结合 webpack 的 alias)
2.如何编写具有代码提示的代码

相关版本
vscode: 1.24.0


使用 jsdoc 优化 javascript 的代码提示

jsdoc 是一种通过注释来为 javascript 代码增加代码提示的规范,大概示例如下:

class Dom {
  /**
   * 初始化组件
   * @param {Object} attr 组件的参数
   * @param {string} attr.defaultProps 默认data
   * @param {string} attr.renderFunc 组件的渲染函数
   * @param {string} attr.dataMerge data的后置处理函数
   */
  constructor({
    defaultProps,
    renderFunc,
    dataMerge,
  }) {
    // something...
  }
}

在类 Dom 的构造函数上面的注释就是使用 jsdoc 规范书写的对这个函数的“注释”了,现在很多 IDE 都有内置了对 jsdoc 的支持,体现在能够格式化的显示这个类的内容,函数的参数列表、使用函数时进行自动补全等:

参数列表

_20180918174857

自动补全

_20180918174912

通过上述操作就能够很直观的将我们写的代码都纳入代码提示里面了

相关链接
Use JSDoc
@param 格式文档


配置 vscode 的目录别名

实现代码提示之后就可以愉快的撸码了,然而在实际使用中我们可能用到了能够提供文件目录别名的开发工具(如 webpack 的 resolve.alias),通过别名引入的模块 IDE 一般会因为找不到实际的物理路径而无法初始化代码提示,这里以 vscode 为例,看看怎么为 IDE 配置目录路径别名

首先,在项目文件夹(workspace)的根路径创建 jsconfig.json,关于为什么要创建 jsconfig.json

jsconfig.json 的内容:

{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"],
      "@js/*": ["src/utils/js/*"]
    },
  },
  "exclude": ["node_modules", "dist"],
  "include": ["src"]
}

通过 compilerOptions.paths 的定义就能够告诉 IDE,在寻找文件的时候遇到某些关键字的时候如何识别别名

const DOM = require('@js/dom')

const dom = new DOM({
  // something...
})

定义了 jsconfig.json 之后在上面的代码中就能够正常获取到代码提示啦,如果发现没有生效,可以试下重启 vscode


其他链接
Visual Studio Code使用typings拓展自动补全功能

Jenkins环境搭建

安装

简介

Jenkins 是一个开源软件项目,是基于 Java 开发的一种持续集成工具,用于监控持续重复的工作,旨在提供一个开放易用的软件平台,使软件的持续集成变成可能。

java安装

首先我们需要准备 Java 环境,使用下面命令来安装 Java:

yum -y install java-1.8.0-openjdk-devel

Jenkins 安装

为了使用 Jenkins 仓库,我们要执行以下命令:

sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo
sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key

如果您以前从 Jenkins 导入过 key,那么 rpm --import 将失败,因为您已经有一个 key。请忽略,继续下面步骤。


安装

接着我们可以使用 yum 安装 Jenkins:

yum -y install jenkins

启动jenkins

启动jenkins

启动 Jenkins 并设置为开机启动:

systemctl start jenkins.service
chkconfig jenkins on

测试访问

Jenkins 默认运行在 8080端口。

稍等片刻,打开 http://localhost:8080 测试访问。


jenkins 操作权限配置

jenkins 安装好之后,还需要对 jenkins 进行权限配置,使 jenkins 能够以 root 用户运行

修改 jenkins 配置文件

vi /etc/sysconfig/jenkins

对如下内容进行修改

JENKINS_USER="jenkins"
# 修改为
JENKINS_USER="root"

修改 jenkins 相关文件夹用户权限

chown -R root:root /var/lib/jenkins
chown -R root:root /var/cache/jenkins
chown -R root:root /var/log/jenkins

重启 jenkins 服务并检查效果

# 重启Jenkins(若是其他方式安装的jenkins则重启方式略不同)
service jenkins restart
# 查看Jenkins进程所属用户
ps -ef | grep jenkins
# 若显示为root用户,则表示修改完成

进入 Jenkins

管理员密码

登入 Jenkins 需要输入管理员密码,按照提示,我们使用以下命令查看初始密码:

cat /var/lib/jenkins/secrets/initialAdminPassword

复制密码,填入,进入 Jenkins。

定制 Jenkins

我们选择默认的 install suggested plugins 来安装插件。

创建用户

请填入相应信息创建用户,然后即可登入 Jenkins 的世界

注意事项:在创建默认管理员的时候需要填写邮箱,不然会创建失败

实验完成

恭喜,您已完成 搭建 Jenkins 环境搭建 实验。
有关 Jenkins 的使用实践,请继续关注后续实验。

虚拟机相关设置

如果你是在本地的虚拟机试行安装 jenkins 的话,有时候会遇到想要在宿主机器通过局域网 ip 访问虚拟机中的 jenkins 的情况。

你需要解决两个问题:

  • 虚拟机与宿主机器实现局域网互通并获取虚拟机的局域网 ip
  • 进行虚拟机(centOS)的防火墙设置,使宿主机器能够访问对应端口

虚拟机与宿主机器实现局域网互通

这里以 VMware Workstation Pro 里运行的虚拟机为例,对虚拟机的网卡进行设置,设置为**“桥接模式”**

设置截图

如果你安装的是图形化界面的 centOS 的话,应该会自动更新网络设置,此时你打开终端,输入以下命令查看 ip 设置:

centOS 7 以下:

ifconfog

centOS 7

ip addr

局域网中的 ip 地址(centOS 7)

这里笔者的宿主机器的局域网 ip 为 192.168.31.200 所以由上图可知,虚拟机与宿主机器进行局域网互通已基本成功。

[相关资料来源]
手把手教你设置局域网访问虚拟机内服务器
VMware虚拟机实现局域网互通

这里要提及一下,非图形化界面的 centOS 要自行配置:

第一步

依旧是 VMware 的虚拟机的网卡设置,将其设置为**“桥接模式”**

第二步

进去虚拟机系统(centOS 7)中,进行网络设置

cd /etc/sysconfig/network-scripts/
ls

查看当前目录下是否有 ifcfg-ens33 (不同系统不同机型可能不一样),可以通过以下命令查看当前应用的是哪个网络设置

ip addr

然后对配置文件进行编辑

vi ifcfg-ens33

文件修改效果

这里笔者宿主机器的网关地址是 192.168.31.1

然后保存文件,重启网络

# vim 操作
esc
:wq
systemctl restart network.service
ip addr

查看当前系统的 ip 地址是否已经与宿主机器在同一个网关下面了。

[相关资料来源]
Centos7虚拟机桥接模式

虚拟机防火墙设置

虚拟机系统和宿主机器实现网路连通而且虚拟机已经安装好 jenkins 之后,宿主机器可以通过 虚拟机局域网ip:8080 的方式来访问 jenkins,如果出现无内容返回的情况的话,一般是虚拟机的防火墙设置导致的。

centOS 7 的防火墙模块使用的是 firewall 而非 iptables,(可以改用 iptables)具体方法百度,这里讲述对 firewall 添加 8080 端口的放行规则的方法。

查看防火墙运行状态

firewall-cmd --state

如果为 running 的话即为防火墙当前正在运行

添加 8080 端口放行

firewall-cmd --permanent --zone=public --add-port=8080/tcp

重启防火墙

firewall-cmd --reload

此时应该就能够在宿主机器中访问到 jenkins 的界面啦。


卸载 jenkins

service jenkins stop
yum clean all
yum -y remove jenkins
rm -rf /var/cache/jenkins
rm -rf /var/lib/jenkins/

[相关资料来源]
CentOS7 Firewall 简单设置
CentOS7 Firewall防火墙配置用法详解
CentOS 7.0关闭默认防火墙启用iptables防火墙

express-session 文档翻译

文档原文
express-session版本:1.15.1

安装

该模块是通过 npm 仓库进行管理的 Node.js 模块,可使用 npm install 命令进行安装:

$ npm install express-session

接口

var session = require('express-session');

session(options)

根据传入的 options 创建 session 的实例。

注意 保存到 cookie 里面的数据并非 session 本身,只是 session ID,session 的内容保存在服务端。

注意 1.5.0 版本之后, cookie-parser 中间件不再是本模块的依赖环境,模块直接在 req/res 中读写 cookies。新版本中如果 cookie-parser 中的 secret 配置项和本模块中的 secret 配置项不一致的话会引发异常。

警告 本模块使用的默认服务器端存储介质是 MemoryStore (内存),这并不是为生产环境设计的。在很多情况下都会自动释放内存,也不能存储大量的数据,这意味着经常会出现异常。

想了解可以使用哪些介质来存储 Session 的话,可以查阅 与 session 兼容的存储介质

Options

express-session 接受以下这些配置项。

cookie

设置保存 session ID 的 cookie 对象的属性,默认值是:

{ path: '/', httpOnly: true, secure: false, maxAge: null }

以下是该配置项的一些可配置项。

cookie.domain-默认域名

指定 Domain Set-Cookie 属性的默认属性,不配置默认域名的话,大部分客户端会只在当前路径应用 cookie。

cookie.expires-期限

指定 Expires Set-Cookie 配置项的值为 Date 对象,在默认情况下该值为空,大部分浏览器会将这类 cookie 视为 非持续性cookie 并且在一些默认行为例如关闭浏览器的行为下删除该 cookie。

注意 如果 expires 和 maxAge 属性都有设置了值,最后被定义的那个配置项为生效项。

注意 expires 这个配置项不应该被直接定义,大部分情况下使用 maxAge 配置项来替代。

cookie.httpOnly

指定 HttpOnly Set-Cookie 配置项的值为 布尔 值。当其为真,HttpOnly 值被设置,反之不被设置。默认该值为真。

注意 注意当该值设置为 true 的时候,兼容模式的客户端将不被允许使用客户端的 javascript 来通过 document.cookie 访问 cookie。

cookie.maxAge

指定该值的值为数字(单位毫秒)以计算 Expires Set-Cookie 属性。该值生效的原理是通过服务器的当前系统时间来计算出 Expires 的值。默认情况下该值没有内容。

注意 如果 expires 和 maxAge 属性都有设置了值,最后被定义的那个配置项为生效项。

cookie.path

指定 Path Set-Cookie 的值,该值默认为 /,域名的根目录。

cookie.sameSite

指定 SameSite Set-cookie 的值为 boolean 或者 string。

  • true 将 SameSite 属性值设置为 Strict
  • false 将不启用 SameSite 属性
  • lax 将 SameSite 属性值设置为 Lax
  • strict 和 true 效果一样。

关于 SameSite-cookies 安全机制的更多信息可以查看下面两个文章:

再见,CSRF:讲解set-cookie中的SameSite属性
https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-4.1.1

注意 这个属性还没有得到很普遍的支持,将来也有可能会改变,这意味着大部分浏览器在适用该属性之前会忽略这个配置项。

cookie.secure

指定 Secure Set-Cookie 配置项的属性为 boolean 值,当为真时,启用 Secure 属性,反之不启用该属性。默认情况不启用 Secure 属性。

注意 当把 Secure 属性设置为真时,如果兼容客户端没有使用 https 协议进行连接的时候,将不能把 cookie 发回到服务器。

需要一提的是 Secure: true 是推荐属性,不过这需要和一个支持 https 的站点配合使用。https 属性对于 Secure cookie 来说是必须的。当启用该属性的时候,如果你使用 http 来进入你的网站的时候,cookie 将不会被保存。如果你的 node.js 是挂载在使用 secure: true 属性的代理下面的时候,你需要设置 express 的 trust proxy 属性:

var app = express()
app.set('trust proxy', 1) // trust first proxy 
app.use(session({
  secret: 'keyboard cat',
  resave: false,
  saveUninitialized: true,
  cookie: { secure: true }
}))

想要只在产品上线的时候使用 secure cookie,在生产环境下不是用的话,可以使用基于 NODE_ENV 全局配置变量的代码逻辑:

var app = express()
var sess = {
  secret: 'keyboard cat',
  cookie: {}
}
 
if (app.get('env') === 'production') {
  app.set('trust proxy', 1) // trust first proxy 
  sess.cookie.secure = true // serve secure cookies 
}
 
app.use(session(sess))

cookie.secure 配置项也可以设置为 auto,这样会自动适配当前链接的安全性,不过要小心当该网站同时使用 http 和 https 的时候,当 cookie 在 https 下被设置的时候,它在 http 的连接方式的时候会不可见。这在我们使用 express 的 trust proxy 设置的时候简化关于生产环境和上线环境的配置很有用。

genid

产生一个新 session ID 的函数,返回一个可以用作 session ID 的字符串。当你想在生成 session ID 的时候将某些值附加到 req 里,把 req作为第一个参数传入到该函数。

默认使用 uid-safe 库里面的值来生成 session ID。

注意 你应该为你的 session 生成一些具有唯一性的 ID,以避免冲突。

app.use(session({
  genid: function(req) {
    return genuuid() // use UUIDs for session IDs 
  },
  secret: 'keyboard cat'
}))

name

在响应头中返回的保存 session ID 的 cookie 名。默认为 connect.sid。

注意 如果你有多个网络应用同时运行在同一个域名下,你可能需要甄别彼此保存 session 的 cookie 值,这时最好的方法是为不同的网络应用使用不同的 name 来保存 session ID。

proxy

当使用 Secure cookie 的时候信任 reverse proxy。

默认值是 undefined。

  • true 使用 X-Forwarded-Proto 头部
  • false 所有头部将被忽略,只有使用 TLS/SSL 连接的时候才允许连接。
  • undefined 使用 express 的 trust proxy 设置

resave

强制将接收到的 session 保存到存储介质中,即使在本次请求过程中没有进行过更改。根据你使用的存储介质的不同,这可能会是必选配置项,不过有一种竞争条件是当客户端传来了两个并发请求都修改到 cookie 的时候,先修改到的 cookie 会被后修改到的 cookie 给覆盖,即使这个覆盖可能并不会改变原来的 cookie (这种情况的表现也跟你使用的存储介质是什么有关)。

该配置项默认值为 true,但是该选项的默认配置已经被弃用,默认值在将来版本可能会变更,所以最好按照你的需要去手动设置该值,通常情况下,你可能需要将其设置为 false。

我要怎么样才能知道这个选项对于我所使用的存储介质来说是不是必要的呢?最好的办法是去检查你使用的存储介质有没有实现 touch 方法。如果有,你可以放心的将之设为 resave: false。如果它没有实现 touch 方法并且会为你的 session 设置到期日期的话,你最好将该值设为 resave: true。

注意 关于竞争条件(race condition)

rolling

强制在每一个响应头中设置存储 session ID 的 cookie。失效日期的设置会根据 maxAge 属性重算。

该属性的默认值为 false。

注意 当本属性设置为 true 而 saveUninitialized 设置为 false 的时候,未初始化的 session 的 cookie 将不会在响应的时候被保存。

saveUninitialized

强制保存将那些未“初始化”的 session。session 在它被新建但是还没有设置属性的时候是被视为“未初始化”。设置为 false 对于实现登录 session 的时候很有用,并可以减少存储空间的使用比例,同时也是遵循了在设置 cookie 之前需要得到允许的原则,同时也有利于处理客户端在没有 session 的情况下发出多个并发请求的情况。

该选项的默认值为 true,但使用默认选项已经被弃用,默认值在将来版本可能会变更,所以最好按照你的需要去手动设置该值。

注意 如果你使用的 session 是和 PassportJS 进行关联的话,Passport 会在用户进行验证之后为 session 添加一个空的 Passport 对象。这会被视为为 session 进行了初始化,从而引发该 session 的保存。这在 PassportJS 0.3.0 中已经被修复。

secret

必填项

这是用来为保存 session ID 的 cookie 加密的秘钥,该选项可以是一个秘钥字符串,也可以是一个保存复数秘钥的数组。如果传入的是秘钥数组的话,只有第一个元素会被用来加密保存 session ID 的cookie,不过所有的元素都会在校验请求中的 cookie 的时候被使用。

store

session 保存的实例,默认是保存在内存中。

unset

控制如何处理关于 req.session 的取消设置请求(通过 delete 方法,或者将之设置为 null之类的)

该选项的默认值为 keep。

  • ‘destroy’ session 在请求结束的时候会被销毁(删除)
  • ‘keep’ 存储空间中的 session 会被保留,不过在请求过程中对其作出的更改将被忽略且不被保存。

req.session

存储或者访问 session,通过请求的参数进行调用 req.session,通常使用 json 作为数据格式,下面的例子是保存用户的视图计数器。

app.use(session({ secret: 'keyboard cat', cookie: { maxAge: 60000 }}))
 
// Access the session as req.session 
app.get('/', function(req, res, next) {
  var sess = req.session
  if (sess.views) {
    sess.views++
    res.setHeader('Content-Type', 'text/html')
    res.write('<p>views: ' + sess.views + '</p>')
    res.write('<p>expires in: ' + (sess.cookie.maxAge / 1000) + 's</p>')
    res.end()
  } else {
    sess.views = 1
    res.end('welcome to the session demo. refresh!')
  }
})

Session.regenerate(callback)

调用本方法来初始化 session,当本方法执行完毕时,新的 SID 和 session 实例会初始化到 req.session 然后执行回调函数。

1
2
3
req.session.regenerate(function(err) {
  // will have a new session here 
})

Session.destroy(callback)

删除 session 并且解除 req.session 属性。当执行完成时,执行回调函数。

req.session.destroy(function(err) {
  // cannot access session here 
})

Session.reload(callback)

从存储介质中重新读取 session 数据并且挂载在 req.session 属性中,执行完成时执行回调函数。

req.session.reload(function(err) {
  // session updated 
  // 应该是异步方法,所以在回调中才能读到新的有效的session
})

Session.save(callback)

将 session 保存回存储介质中,将内存中的 session 内容替换到存储介质中的对应内容(虽然存储介质可能会做点其他事情,查看你所使用的存储介质的文档看看有哪些额外的行为)

如果 session 被修改过的话,这个方法会在 http 的响应逻辑的结尾被自动调用。(尽管这个行为会被中间体的构造函数中的多个选项影响到)所以这个方法通常不需要手动调用。

有几个情况手动调用本函数是有用的,比如 websockets 中的长持续请求。

req.session.save(function(err) {
  // session saved 
})

Session.touch()

更新 session 的 .maxAge 属性,通常这个函数是没有必要手动调用的,session 中间件会为你处理这个事情。

req.session.id

每个 session 都有一个唯一的 ID 与它关联,本属性包含了 session ID 并且无法被改写。

req.session.cookie

每个 session 都有一个唯一的 cookie 对象,我们可以为不同的用户改变他们的 session cookie。例如我们可以设置 req.session.cookie.expires 为 false 以使 cookie 只在用户代理的持续时间内保留。

Cookie.maxAge

req.session.cookie.maxAge 会以毫秒为单位返回 cookie 的剩余时间,以使我们可以根据情况调整它的 .expires 属性,以下两条语句的作用是相同的。

var hour = 3600000
req.session.cookie.expires = new Date(Date.now() + hour)
req.session.cookie.maxAge = hour

假设 maxAge 属性被设置为 60000(一分钟),在 30 秒之后,除非本次请求结束了,该属性的值会更新为 30000,也就是在那个时候自动调用了 req.session.touch() 的原因。

req.session.cookie.maxAge // => 30000

req.SessionID

访问 req.SessionID 属性能够访问当前所加载的 session 的 ID,这是一个只读属性,只有在这个 session 被创建或是被加载了之后才能够获取到。


Session Store Implementation - session 存储介质的实现方案

Session 存储方案必须是以 EventEmitter 为骨架,并且实现了一些具体的方法,下面是要实现的一些方法列表,分别有标注为 必须实现、推荐实现 和 可选实现。

  • 必须实现:这种方法是模块会一直都调用到的方法。
  • 推荐实现:这种方法是如果存储介质有实现的话就会调用到的方法。
  • 可选实现:这种方法模块不一定会调用,但是能够帮助用户实现呈现统一的存储介质。

例如 connect-redis 的实现:

store.all(callback)

可选实现

该可选实现能够获取存储中的所有 session 并以数组形式返回。回调函数的形式应该为:callback(error, sessions)

store.destroy(sid, callback)

必须实现

该必须实现是通过传入的 sid 来删除存储介质中的 session 的。当 session 被删除了之后,回调的形式应该为 callback(error)。

store.clear(callback)

可选实现

该可选实现是用于将存储介质中的所有 session 都删除。回调函数的形式应该为:callback(error)。

store.length(callback)

可选实现

该可选实现是用于获取存储介质中 session 的数量,回调函数的形式应该为:callback(error, len)。

store.get(sid, callback)

必须实现

该必须实现是根据传入的 sid 获取指定的 session,回调函数的形式应该为:callback(err, session)。

如果找不到指定的 session 的话,返回的 session 参数值为 null 或者 undefined (并且无发生错误信息 error),一种特殊情况是当发生 error.code === 'ENOENT' 时,返回将是 callback(null, bull)。

store.set(sid, session, callback)

必须实现

该必须实现是根据传入的 sid 和 session 来更新存储介质中对应的 session 。回调函数的形式应该为:callback(error)。

store.touch(sid, session, callback)

推荐实现

该推荐实现是根据传入的 sid 和 session 来更新指定 session 的状态,回调函数的形式应该为:callback(error)。

当存储介质打算自动删除那些空闲的 session 的时候,该方法可以用来标记指定的 session,告诉存储介质该 session 是激活状态,将其定时器复位。


兼容的 session 存储介质

以下的列表是跟本模块兼容的实现了作为 session 存储介质。

工具收集

FastStone Capturecn

功能齐全的截图、取色、的小工具
使用教程留待后续

Name: ZRQX
Code: FOZRJILDQIYCHCIHSSQN

http-server

快速启动本地服务器的 npm 模块,适合进行打包文件预览

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.