前端3D应用开发—— ThreeJS入门与3D应用开发简介

前言

计算机 3D 图形的历史可以追溯到 20 世纪 60 年代,几乎和计算机本身的历史一样长。它被广泛应用于工程、教育、培训、建筑、金融、销售、市场、博彩、娱乐等各个领域。曾经 ,3D 图形只能用计算机软件渲染。如今,所有的计算机和移动设备都搭载了 3D 图形处理硬件,普通智能手机甚至有着比五年前的专业图形工作站更为优秀的图形处理能力。更重要的是现代 Web 浏览器也支持了 3D 渲染,相比昂贵的 3D专用渲染 件,浏览器显然更普遍,更易于获取,并且是免费的。

HTML5新型视觉媒介的核心是一系列先进的图形技术:WebGL、css3 3d,canvas。在接触WebGL领域的时候,积累了不少学习资料,分别从前端可视化、游戏、3D应用等不同方向展示WebGL的具体应用。他们有的自底向上,从计算机图形学开始到具体需求实现;有的自顶向下,从需求出发,一步步实现一个完整的应用。无论哪种方式,这一领域都需要漫长的耕耘,无法满足快速起步的目的。

目前这一系列主标题是前端3D应用开发,主要探讨如何从0到1落地一个3D项目。由于发现介绍原始的WebGL开发难度过大,内容枯燥,使用中心向两端的方式,根据实际情况向前或向后灵活展开,更为合适。当前安排如下:

  • 介绍Three.js及快速入门案例
  • Three.js简介

ThreeJS入门

以下内容整理自《Three.js开发指南》

获取源码的方式

1
git clone https://github.com/josdirksen/learning-threejs-third

代码需要运行在Web服务器,可以自行处理,也准备了一个在线资源

第一行代码

查看效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// once everything is loaded, we run our Three.js stuff.
function init() {

// create a scene, that will hold all our elements such as objects, cameras and lights.
var scene = new THREE.Scene();

// create a camera, which defines where we're looking at.
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);

// create a render and set the size
var renderer = new THREE.WebGLRenderer();
renderer.setClearColorHex();
renderer.setClearColor(new THREE.Color(0xEEEEEE));
renderer.setSize(window.innerWidth, window.innerHeight);

// show axes in the screen
var axes = new THREE.AxisHelper(20);
scene.add(axes);

// create the ground plane
var planeGeometry = new THREE.PlaneGeometry(60, 20);
var planeMaterial = new THREE.MeshBasicMaterial({color: 0xcccccc});
var plane = new THREE.Mesh(planeGeometry, planeMaterial);

// rotate and position the plane
plane.rotation.x = -0.5 * Math.PI;
plane.position.x = 15;
plane.position.y = 0;
plane.position.z = 0;

// add the plane to the scene
scene.add(plane);

// create a cube
var cubeGeometry = new THREE.BoxGeometry(4, 4, 4);
var cubeMaterial = new THREE.MeshBasicMaterial({color: 0xff0000, wireframe: true});
var cube = new THREE.Mesh(cubeGeometry, cubeMaterial);

// position the cube
cube.position.x = -4;
cube.position.y = 3;
cube.position.z = 0;

// add the cube to the scene
scene.add(cube);

// create a sphere
var sphereGeometry = new THREE.SphereGeometry(4, 20, 20);
var sphereMaterial = new THREE.MeshBasicMaterial({color: 0x7777ff, wireframe: true});
var sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);

// position the sphere
sphere.position.x = 20;
sphere.position.y = 4;
sphere.position.z = 2;

// add the sphere to the scene
scene.add(sphere);

// position and point the camera to the center of the scene
camera.position.x = -30;
camera.position.y = 40;
camera.position.z = 30;
camera.lookAt(scene.position);

// add the output of the renderer to the html element
document.getElementById("WebGL-output").appendChild(renderer.domElement);

// render the scene
renderer.render(scene, camera);
}
window.onload = init;

代码中首先定义了场景、摄像机和渲染器对象。场景是一个容器,示例中的立方体和圆球都会添加到场景中;摄像机决定了能够在场景中看到什么;渲染器会基于摄像机角度来计算渲染成什么样子。后面我们有机会了解这些概念是怎么一回事

第二部分添加了轴和平面。我们创建了坐标轴对象并设置粗细20,最后调用scene.add方法将轴添加到场景。平面创建分两步进行,首先定义大小,然后通过创建材质来设置外观,然后将大小和外观组合进Mesh对象并赋予给平面变量。材质的概念也是稍后要介绍的

以同样的方式加入方块和球,设置wireframe: true物体不会渲染为实体。最后一部分设置相机位置和lookAt方向。接下去还会使用光照、阴影、材质和动画来美化这个场景

添加材质、光源和阴影

基础材质不会对光源有任何反应,需要改为MeshLambertMaterial

1
2
3
4
5
var planeGeometry = new THREE.PlaneGeometry(60, 20);
var planeMaterial = new THREE.MeshLambertMaterial({color: 0xffffff});
var plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.receiveShadow = true;

1
2
3
4
5
6
// add spotlight for the shadows
var spotLight = new THREE.SpotLight(0xffffff);
spotLight.position.set(-40, 60, -10);
spotLight.castShadow = true;
scene.add(spotLight);

因为渲染阴影比较耗费性能,所以默认关闭,需要做如下修改:

1
2
3
4
5
 // create a render and set the size
var renderer = new THREE.WebGLRenderer();
renderer.setClearColor(new THREE.Color(0xEEEEEE, 1.0));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMapEnabled = true;

此外还需要明确指定哪个物体投射阴影,哪个接受阴影。

1
2
3
4
5
6
plane.receiveShadow = true;
//...
cube.castShadow = true;
//...
sphere.castShadow = true;

点击查看源码

Three.js简介

Three.js的作者Ricardo Cabello Miguel或者称呼为Mr.doob。Three.js一开始是用ActionScript编写的,2010年第一个使用SVG 和2DCanvas的版本诞生,几个月后WebGL发布了,便开始移植到 WebGL 上进行继续开发,从那时开始,Three.js逐渐强大 完善,并最终成为创建WebGL 3D 应用的最流行选择。

  • Three.js 隐藏了 WebGL 渲染中的底层细节。Three.js 简化了 WebGL API 的细节 将
    3D 场景表示为网格、材质、灯光(即图形开发者常用的各种对象类型)。

  • Three.js 非常强大。Three.js 不仅仅是 WebGL 的一个封装,它内置了许多可用千游戏开发、动画、演示、数据可视化、建模工具应用的非常有用的模型对象,还提供了用千特殊效果的后渲染机制。除了它本身的能力,Three.js 还拥有丰富的示例,你可以在你的项目中使用这些示例的代码。

  • Three.js 非常易用。Three.js 的 API 基千友好和易学的理念设计。随库提供的许多示例能够帮助你更好地入门。

  • Three.js 运行速度很快。Three.js 采用了 3D 图形的最佳大践,既保证了高性能,又不因此牺牲可用性。

  • Three.js 非常稳定。它包含完备的错误检查、异常和控制台警告,让开发者可以准确地跟踪程序问题。

  • Three.js 支持交互。WebGL 不具备原生的选中支持,这意味着你无法获取到鼠标经过了当前场景中的哪个物体。Three.js 提供了对选中的支持,使得为应用添加交互变得更加简单。

  • Three.js 支持数学计算。Three.js 包含强大易用的 3D 数学运算对象,如矩阵、映射和向匿。

  • Three.js 内置第三方文件格式支持。你可以利用 Three.js 提供的接口载入以文本格式存储的、用流行的建模工具导出的 3D 模型。Three.js 也定义了自己专属的 JSON 和二进制 3D 模型文件格式。

  • Three.js 是面向对象的。开发者基千设计优良的 JavaScript 对象编写代码,而不是仅仅去调用一些函数。

  • Three.js 是可扩展的。无论你希望为 Three.js 添加一些特性,还是定义自己的个性化
    Three.js,都是非常简单的事。如果现有的数据格式支持无法满足你的需求,那么你可以为特定的数据格式编写相应的插件。

  • Three.js 包含 2D Canvas、SVG、CSS 的渲染引擎。尽管 WebGL 已经非常流行,但它并未被所有的浏览器支持,这表示对某些应用来说,WebGL 并不是最好的选择。幸好 Three.js 还可以使用 2D Canvas 和 SVG 元素来渲染大部分的内容。当运行环境不支持 3D Canvas 的时候,这为你的代码提供了一个优雅的降级方案。

需要提醒大家的是,Three.js 也有一些不擅长的事情。比方说,Three.js 并不是一个游戏引擎。它缺乏一些游戏引擎所需的常用特性,如公告牌、头像、有限状态机以及物理引擎。 Three.js 也没有编写多人联机游戏所需的内置网络支持。你需要基千 Three.js 自行构建它,或是整合其他专用的代码库。同时 Three.js 也不是一个应用框架,它不具备一个框架应有的安装、卸载、事件处理和运行循环机制。

另外一个简单入门案例

WebGL入门

WebGL原生API是一种非常低等级的接口,就好像一个只有加法的计算器,尽管有许多诸如Three.js之类的图形库,如果想要掌控WebGL编程,依然需要坚持学习更多底层知识,因此自底向上的学习方式,可能会让你经历他人突飞猛进而自己仍然在画三角形的挫败感。不要放弃学习基础知识,也要灵活尝试成熟的框架。

WebGL的优势体现在Web和3D

什么是3D

3D 计算机图形(相对 2D 计算机图形而言)是使用三个维度来表示几何数据(通常使用笛卡儿坐标系)并将其存储在计算机中,用于计算和绘制成屏幕上 2D 图像的一类图形。这类图形可被存储起来随时浏览,也可用于实时显示。

WebGL坐标系统

默认情况下WebGL使用右手坐标系。

右手坐标系

WebGL对于使用左手还是右手坐标系这个问题是中立的,那为什么那么多书籍教程都将WebGL坐标系描述为右手呢?这是因为使用右手坐标系是个传统,当你开发自己的程序需要先确定使用的坐标系统,然后不再改变。这一点对于各类图形库同样成立,早期图形库大部分采用了右手坐标,时至今日右手坐标系已经成为了传统,以致成了GL图形语言的一部分。

那为什么会有疑问?如果所有人都接受同一个传统,就没问题了,但在特定场景下需要让WebGL选择一种坐标系完成运算,就需要知道它的默认设定,而默认设定并不总是右手坐标系!关于WebGL默认行为探索,比如隐藏面消除和裁剪坐标系统,可以参考**《WebGL编程指南》**附录D,目前,你可以认为WebGL就是右手坐标系。

变换矩阵

对于简单的变换,可以使用数学表达式来实现。但当情况变得复杂,你会发现表达式运算实际相当繁琐。比如旋转后平移,就需要两个表达式叠加,获得一个新等式,然后在顶点着色器中实现。也就是说每次要实现一个新的着色器,这很不科学。我们可以使用变换矩阵来完成这项工作。

向量的点乘和叉乘

相关资料B站

线性变换的本质

将矩阵和矢量相乘就获得一个新的矢量

[xyz]=[abcdefghi]×[xyz]\left[\begin{matrix} x^{'}\\y^{'}\\z^{'} \end{matrix} \right]=\left[ \begin{matrix} a & b & c\\ d & e&f\\ g & h & i \end{matrix} \right] \times \left[\begin{matrix} x\\y\\z \end{matrix} \right]

注意只有矩阵的列数与矢量的行数相等时,才可以将两者相乘

x=ax+by=czx^{'}= ax+by=cz

y=dx+ey+fzy^{'}= dx+ey+fz

z=gx+hy+izz^{'}= gx+hy+iz

变换矩阵:旋转

x=r(cosαcosβsinαsinβ)x^{'}= r(cos \alpha cos \beta-sin \alpha sin\beta)

y=r(sinαcosβcosαsinβ)y^{'}= r(sin \alpha cos \beta-cos \alpha sin\beta)

将3.2代入消除,最终可得

x=xcosβysinβx^{'}=xcos\beta -y sin\beta

y=xsinβ+ycosβy^{'}=xsin\beta +y cos\beta

二维旋转矩阵直观的理解方式是:三个基向量i,j,k :[1,0,0],[0,1,0],[0,0,1],因为线性变换的本质是基向量的变化,在二维平面中z轴方向不发生变化,而新的基向量的坐标其实是在原始坐标系的投影,我们直接想象如果逆时针旋转90度,可以直接想出原来的x轴到了原来y轴位置,原来y轴到了原来-x轴位置。单位向量的坐标就是[0,1,0],[-1,0,0],[0,0,1],是不是和上面矩阵值一致呢

相机、透视、视口、投影

3D系统通常使用相机(camera)概念来描述用户查看渲染好场景的观察点。相机定义了用户相对于场景位置朝向,它具备现实中相机的属性,如视野的尺寸,它定义了透视(perspective,即远处物体看起来小)。相机通常用一对矩阵表示。第一个矩阵定义相机位置和方向,类似变换矩阵。第二个矩阵表示3D到2D绘制坐标转换,称为投影矩阵(projection matrix)。这些细节一般工具都封装好了,只需要对准、拍摄。

下图描述了相机、视口、透视的概念。眼睛代表相机位置,x轴表示相机指向,两个矩形代表近剪裁平面和远剪裁平面。两个平面间定义了3D空间子集的范围,通称视锥体或视见体。只有位于视锥体中的物体才会真正被渲染。

谈谈对透视矩阵的理解:矩阵是一系列坐标转换,一个3D坐标系中的物体,我们以上帝视角来看,是绝对的坐标,不管远近,然而,因为视觉上的需求,需要做“透视”,即远近大小看起来不一致(想象一下火车头和火车尾)。即将绝对坐标转换为视觉坐标。

WebGL应用示例

WebGL就是个绘图库,类似Canvas 2D那样,是另外一种Canvas。为了在页面中渲染WebGL,应当执行以下步骤:

  • 创建一个Canvas元素
  • 获取Canvas上下文
  • 初始化视口
  • 创建一个或多个待渲染数据的缓冲
  • 创建一个或多个定义定点缓冲到屏幕空间转化规则的矩阵
  • 创建实现绘制算法的着色器
  • 使用参数初始化着色器
  • 绘制

Canvas元素及绘图上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function initWebGL(canvas) {

var gl = null;
var msg = "Your browser does not support WebGL, " +
"or it is not enabled by default.";
try
{
gl = canvas.getContext("experimental-webgl");
}
catch (e)
{
msg = "Error creating WebGL Context!: " + e.toString();
}

if (!gl)
{
alert(msg);
throw new Error(msg);
}

return gl;
}

视口

当你从canvas获取WebGL绘图上下文是,需要定义一个绘制区域的矩形边界。在WebGL中,这个矩形边界被称为视口。

1
2
3
4
function initViewport(gl, canvas)
{
gl.viewport(0, 0, canvas.width, canvas.height);
}

缓冲、缓冲数组和类型化数组

WebGL基于图元(primitive)进行绘制。图元是指不同类型的基本几何图形。WebGL的图元包括三角形、线、点。三角形是最常用的,通常使用两种形式存储:以数组形式存储的三角形和三角形带。图元以数组形式存储数据,这个数组称为缓冲(buffer),待绘制的顶点数据在缓冲中被定义。
下面展示如何创建单位(1X1)正方形顶点缓冲,我们使用三角形带,一个三角形带定义了一组连续的三角形,前三个顶点表示第一个三角形,后续三角形都与它前一个共用两个顶点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Create the vertex data for a square to be drawn
function createSquare(gl) {
var vertexBuffer;
vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
var verts = [
.5, .5, 0.0,
-.5, .5, 0.0,
.5, -.5, 0.0,
-.5, -.5, 0.0
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verts), gl.STATIC_DRAW);
var square = {buffer:vertexBuffer, vertSize:3, nVerts:4, primtype:gl.TRIANGLE_STRIP};
return square;
}

Float32Array是因为WebGL才被引入浏览器的,类型化数组可以普遍使用,访问速度更快,耗费的内存更小。

矩阵

在绘制之前,首先要创建一对矩阵。一个矩阵用于定义正方形在3D坐标系统中的位置(相对于相机),这个矩阵同时包含相机位置和模型位置,称为模型-视图矩阵。在例子中,沿着z轴负方向对正方形平移(当相机位置被移动,程序实际进行的处理是根据当前相机位置对整个场景进行平移)。第二个矩阵是投影矩阵,着色器使用它执行3D-2D坐标转换。示例中定义了一个45度视野的透视相机。
我们用glMatrix开源库来进行矩阵运算,现在我们放弃关心透视矩阵推导过程及背后的观察原理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var projectionMatrix, modelViewMatrix;

function initMatrices(canvas)
{
// Create a model view matrix with object at 0, 0, -3.333
modelViewMatrix = mat4.create();
mat4.translate(modelViewMatrix, modelViewMatrix, [0, 0, -3.333]);

// Create a project matrix with 45 degree field of view
projectionMatrix = mat4.create();
// perspective(out, fovy, aspect, near, far)
mat4.perspective(projectionMatrix, Math.PI / 4, canvas.width / canvas.height, 1, 10000);
}

透视矩阵推导

透视矩阵

着色器

一个着色器可以应用于多个对象,因此实际应用中,整个场景通常使用一个着色器,通过设置不同参数,在不同几何上复用。着色器通常有两部分:顶点着色器(vertex shader)和片元着色器(fragment shader)。顶点着色器负责将物体坐标转换为2D显示区坐标,片元着色器负责转换好的顶点最终颜色输出,基于颜色、纹理、光照、材质等数值输入。

相关推荐

canvas API

WebGL API

SVG

3Blue1BrownB站

华中科技大学-计算机图形学

奇舞团月影可视化课程源码

《交互式计算机图形学——基于WebGL的自顶向下方法(第七版)

本站收集在线示例

参考资料

跟月影学可视化

WebGL编程指南

Three.js开发指南

Introduction to Computer Graphics A Practical Learning Approach


1.1.4 校验码

码距

两个合法编码对应位之间比较,有多少位编码不同,又称为海明距离。比如10101和00110就是(从高到低1,4,5位不同,码距为3)

计算海明距离的一种方法,就是对两个位串进行异或(xor)运算,并计算出异或运算结果中1的个数。例如110和011这两个位串,对它们进行异或运算,其结果是:

110011=101110 \bigoplus 011=101

异或结果中含有两个1,因此110和011之间的海明距离就等于2。

为了使一个系统能检查和纠正一个差错,码间最小距离必须至少是“3”。最小距离为3时,或能纠正一个错,或能检二个错,但不能同时纠一个错和检二个错。编码信息纠错和检错能力的进一步提高需要进一步增加码字间的最小距离。

码距越大,纠错能力越强,但数据冗余也越大,即编码效率低了。

模二加法

这是一种二进制的运算,等同于“异或”运算。

规则是两个序列按位相加模二,即两个序列中对应位,相加,不进位,相同为0,不同为1。

奇偶校验码(Parity Codes)

是一种通过增加冗余位使得码字中"1"的个数恒为奇数或偶数的编码方法,它是一种检错码。在实际使用时又可分为垂直奇偶校验、水平奇偶校验和水平垂直奇偶校验等几种。因为其利用的是编码中1的个数的奇偶性作为依据,所以不能发现偶数位错误。(错偶数个奇偶性不变)

海明码(Hamming Code)

贝尔实验室Richard Hamming设计,在特定位置插入k个校验码,通过扩大码距来检错纠错。

n位数k校验位,则必须符合:

2k1n+k2^k-1\geq n+k

Pk,Pk1,...p1校验位: P_k, P_{k-1},...p_1

Dn1,Dn2,...D0数据位:D_{n-1},D_{n-2},...D_0

Hn+k,Hn+k1,...H1海明码:H_{n+k},H_{n+k-1},...H_1

Hj=Pi,j=2i1则:H_j=P_i,j=2^{i-1} 数据从低到高占据剩下的位置

海明码中的任何一位都是由若干个校验位来校验的:被校验的海明位的下标==所有参与校验该位的校验位的下标之和,校验位由自身校验。

编码过程

对于8位数,要4个校验位。

  • 1.确定D与P位置
Hamming D&P
H(12) D(7)
H(11) D(6)
H(10) D(5)
H(9) D(4)
H(8) P(4)
H(7) D(3)
H(6) D(2)
H(5) D(1)
H(4) P(3)
H(3) D(0)
H(2) P(2)
H(1) P(1)
  • 2.确定校验关系
海明码 分布 海明码下标 校验位组
H(12) D(7) 12=4+8 P3,P4
H(11) D(6) 11=1+2+8 P1,P2,P4
H(10) D(5) 10=2+8 P2,P4
H(9) D(4) 9=1+8 P1,P4
H(8) P(4) 8 P4
H(7) D(3) 7=1+2+4 P1,P2,P3
H(6) D(2) 6=2+4 P2,P3
H(5) D(1) 5=1+4 P1,P3
H(4) P(3) 4 P3
H(3) D(0) 3=1+2 P1,P2
H(2) P(2) 2 P2
H(1) P(1) 1 P1

根据每个校验位参与校验得

P1=D0D1D3D4D6P_1=D_0 \bigoplus D_1 \bigoplus D_3 \bigoplus D_4 \bigoplus D_6

P2=D0D2D3D5D6P_2=D_0 \bigoplus D_2 \bigoplus D_3 \bigoplus D_5 \bigoplus D_6

P3=D1D2D3D7P_3=D_1 \bigoplus D_2 \bigoplus D_3 \bigoplus D_7

P4=D4D5D6D7P_4=D_4 \bigoplus D_5 \bigoplus D_6 \bigoplus D_7

若使用奇校验,将校验位的偶校验值取反即可

  • 3.检测错误

G1=P1D0D1D3D4D6G_1=P_1 \bigoplus D_0 \bigoplus D_1 \bigoplus D_3 \bigoplus D_4 \bigoplus D_6

G2=P2D0D2D3D5D6G_2=P_2 \bigoplus D_0 \bigoplus D_2 \bigoplus D_3 \bigoplus D_5 \bigoplus D_6

G3=P3D1D2D3D7G_3=P_3 \bigoplus D_1 \bigoplus D_2 \bigoplus D_3 \bigoplus D_7

G4=P4D4D5D6D7G_4=P_4 \bigoplus D_4 \bigoplus D_5 \bigoplus D_6 \bigoplus D_7

如果采用偶校验,G4G3G2G1全为0则无错(奇校验全为1),且其十进制值指出了错误位置,如1010说明H10(D5)出错了,将其取反即可纠正错误。


hexo自动部署+gitlab

前期工作

之前入手了一台miniPC安装好了Ubuntu,并通过frp内网穿透部署好了gitlab。现在决定重拾荒废的Blog,并做成自动部署。

本文想达到的目的是在任意一台主机编写Blog,并能够做到提交后自动部署。

当前hexo版本5.0以上(安装主题的方式有所不同)

条件

  • 云服务器,公网IP+域名
  • 服务器自行安装好Nginx+git
  • 本地安装好node

开始

本地安装hexo及初始化

1
2
3
4
5
npm install -g hexo-cli

hexo init myblog
cd myblog
npm install

创建服务端账户

一般我们需要专门为服务创建不同用户及用户组,以便做好权限管理。
当前是以root用户登录服务器的

1
2
3
useradd blog
passwd blog #设置用户密码
usermod -a -G root blog #设置组

为了方便,直接将blog用户加到了root组里面

在服务器创建空的git仓库

届时会通过git的钩子方法自动将文件复制到静态文件夹

1
2
3
4
5
mkdir /home/blog/repo/
chown -R blog:root /home/blog/repo/
chmod -R 755 /home/blog/repo/
cd /home/blog/repo/
git init --bare hexo_static.git

既然创建了专用的用户账号,就把相应的资源放在用户目录下/home/blog/就是用户home目录,我们将文件都放到home目录的repo目录中

在服务器创建hexo静态文件夹

作为站点root使用Nginx做web服务器

1
mkdir /home/blog/repo/hexo

在本地连接服务器(实现免密连接)

可以先检查下是否已经有了

1
cat ~/.ssh/id_rsa.pub

没有就创建公开密钥认证所需的SSH Key

1
2
3
4
5
6
$ ssh-keygen -t rsa -C "your_email@example.com"
Generating public/private rsa key pair.
Enter file in which to save the key
(/Users/your_user_directory/.ssh/id_rsa): 按回车键
Enter passphrase (empty for no passphrase): 输入密码
Enter same passphrase again: 再次

实现免密登录

1
ssh-copy-id -i ~/.ssh/id_rsa.pub  blog@服务器ip或域名
  • 注意: ssh-copy-id 将key写到远程机器的 ~/ .ssh/authorized_key文件中

本地配置hexo的部署方式

1
2
cd myblog
vi _config.yml

找到deploy并修改

1
2
3
deploy:
type: 'git'
repo: 'blog@ip:/home/blog/repo/hexo_static.git'

此时已经可以通过hexo g -d命令构建并部署文件到git仓库了,但是还差一步

添加自动部署钩子

利用git的hooks,在push的时候自动复制文件到hexo目录

1
2
3
cd /home/blog/repo/hexo_static.git
cd ./hooks
vi post-receive
1
2
#!/bin/bash
git --work-tree=/home/blog/repo/hexo --git-dir=/home/blog/repo/hexo_static.git checkout -f

给文件加好执行权限

1
chmod +x post-receive

运行

有变动才能提交,先修改一下

1
hexo g -d

检查文件是否已经到/home/blog/repo/hexo目录了,如过没有,很有可能是访问权限问题,git仓库目录应当和hexo属于同一个用户

配置nginx

自行探索,提供一个简单参考

1
2
3
4
5
server{
listen 80;
server_name blog.sumshare.cn;
root /home/blog/repo/hexo;
}

使用gitlab仓库实现代码管理

以上已经实现自动部署,我们进一步的目的是为了实现多态设备随时写作+发布。这一步其实只要有个代码管理仓库不管是github还是gitlab或者gitee都能实现。

可能的问题

  1. hexo d时ERROR Deployer not found: git只需要npm install --save hexo-deployer-git
  2. nginx 访问403,这个也是权限问题引起的,检查一下nginx是以什么用户启动的,nginx一般会有nginx用户和用户组,酌情考虑将启动用户改成root。否则你需要重新规划以哪个用户进行hexo的部署

参考链接


vue实现input强制输入Number类型

需求

如果想要实现input输入Number类型,又不想用难看的type='number’怎么处理

1
2
<el-input style="width:200px" :maxlength="4" @keyup.native="isNumber(form,'recruitsCount')" v-model.number="form.recruitsCount" placeholder="请输入数字,不能为空"></el-input>

1
2
3
4
5
6
7
8
9
isNumber(obj,key) {
let isnumber = new RegExp('^[0-9]\\d*$')
if (!isnumber.test(obj[key])) {
obj[key] = null
} else {
return true
}
},

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>Document</title>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.js"></script>
</head>

<body>
<div id="app">
<input style="width:200px" :maxlength="4" @keyup="isNumber(form,'count')" v-model.number="form.count" placeholder="请输入数字,不能为空"></input>
</div>
</body>
<script type="text/javascript">
var app = new Vue({
el: '#app',
data: {
form: {
count: null
}

},
methods: {
isNumber(obj, key) {
let isnumber = new RegExp('^[0-9]\\d*$')
if (!isnumber.test(obj[key])) {
obj[key] = null
} else {
return true
}
}
}
})
</script>

</html>



说明

  • 配合.number修饰符实现纯数字输入
  • .nmber的特点是将字符串转为num型,如果转换结果是NaN,则原样输出。
  • 因此如果第一个字符输入非数字,那么就会变成字符串,根据强制类型转换规则,首字母是数字才能转换成功,如1f这种就会转换为1,因此就出现预想中的输入非数字转换为数字的效果。
  • 因此关键是第一个字符不能为非数字,配合keyUp事件,检测首字母是否是数字即可,检查全部也无所谓,因为keyup事件发生时,input事件未发生,input又经过类型转换,因此能完美运行

编程日志(08)—vue实现plupload分片上传
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
<!-- plupload封装 -->
<template>
<div class="supload">
<div class="clearfix list-picture" v-if="fileList.length>0&&listType=='picture'">
<div v-for="ff,index in fileList" class="pic-bx small">
<img :src="ff.showUrl">
<div class="cl-bx">
<i class="el-icon-close" @click="deleteFile(ff,index)"></i>
</div>
</div>
</div>
<div class="plupload clearfix" v-show="fileList.length<maxFile">
<template v-if="listType=='text'">
<div class="text_trigger">
<div :id="trigger_id" >
<i class="el-icon-document" ></i>
<span>{{triggerText}}(小于{{maxSize}}Mb)</span>
</div>
</div>
</template>
<template v-if="listType=='picture'">
<div class="fm-up" :id="trigger_id">
<div class="cc">
<i class="el-icon-circle-plus-outline"></i>
<p>点击选择</p>
<span>支持jpg/png格式</span>
<span>560px×400px</span>

</div>
</div>
</template>
</div>
<div class="pgs" v-if="progress>0&&listType=='text'">
<el-progress :text-inside="true" :stroke-width="20" :percentage="progress" status="success"></el-progress>
</div>

<div class="list-text" v-if="listType=='text'">
<template v-for="ff,index in fileList">
<div class="clearfix item-text">
<!-- <div class="progress" :style="{'width':ff.progress+'%'}"></div> -->
<div class="title">{{ff.name}}</div>
<div class="close" @click="deleteFile(ff,index)">删除</div>
</div>

</template>
</div>

</div>
</template>

<script>
/**
* 强制multi_selection为false使其只能逐个上传
*
*/
/**
* tip:
* 为了操作方便fileList以数组形式传入props,直接操作fileList可以实现数据双向绑定
* 并不建议这样去做,父子组件之间尽量保持单向数据流
* 因此可以暂不向外提供文件删除上传的回调入口
*/
/*
xlsx等文档可以在拓展过滤为zip的情况下可选的bug是因为xlsx本质是个压缩包,将xlsx拓展名改成zip再看看
*/
import plupload from 'plupload'
import {guid,getExtName} from '../../lib/util.js'

import config from '../../config/config.js'
export default {
data() {
return {
trigger_id:null, //触发按钮的ID属性
plup:null, // plupload实例对象
progress: 0, // 单文件上传进度
option: null
};
},
props: {
accept: {
type: String,
default: 'image/png,image/jpeg,audio/mp3'
},
// 支持的拓展名以逗号隔开jpg,gif,png,bmp,mp4
extensions:{
type: String,
default: 'mp4'
},
maxFile: {
type: Number,
default: 1
},
maxSize: {
type: Number,
default: 512 //mb
},
// 文件列表
fileList: {
default(){
return []
}
},
// text,picture 以picture形式呈现时,如果超过可上传数量,上传界面将消失
listType:{
default: 'text'
},
params:{
type:Object,
required:true
},
triggerText:{
default:'选择文件'
}
},
computed:{

},
components: {

},

methods: {
transGetUrl(url) {
return config.imgHost + url
},


deleteFile(ff,index){
this.fileList.splice(index,1)
},

},
watch:{
extensions(ne,olg){
this.option.filters.mime_types=[{
title: 'files', extensions: ne
}]

this.plup.destroy()
this.plup = new plupload.Uploader(this.option);
this.plup.init()

}

},
mounted(){
const $this = this;
this.option = {
runtimes: 'html5,flash,silverlight,html4',
url: config.downloadHost+'upload/pluploadUpload',
//url: '/preschool/upload/pluploadUpload',
browse_button: this.trigger_id,
chunk_size: '2048kb',
multi_selection:false,
filters: {
mime_types: [{ title: 'files', extensions: this.extensions }],
max_file_size: this.maxSize+'mb',
prevent_duplicates: true
},
multipart:true,
multipart_params:this.params,
init: {
FilesAdded: function(up, files) {
up.start();
},
UploadProgress: function(up, file) {
$this.progress = file.percent;
},
BeforeUpload: function(up,file) {},
FileUploaded: function(uploader,file,res){
// response,responseHeaders,status
if (res.status==200){
let result = JSON.parse(res.response);
if (result.code==0){
setTimeout(function(){
$this.progress = 0;
$this.fileList.push({
name:file.name,
// url: result.data.replace(/(.*)(pluploadDir)/g,config.imgHost),
url: result.data,
showUrl: $this.transGetUrl(result.data),
type: getExtName(result.data)
});
},500);
}
} else {
}
},
Error:function(up,err){},
Refresh: function(up){},
Destroy(up){},
OptionChanged:function(up,option_name,new_value,old_value){}
// Error: this.error
}
}
this.plup = new plupload.Uploader(this.option);
this.plup.init();
},
beforeMount(){
this.trigger_id = guid();
}
};
/**
* 额外参数传递:
* 1.设置multipart为true
* 为true时将以multipart/form-data的形式来上传文件,为false时则以二进制的格式来上传文件。
* 2. 设置multipart_params:{userId:1024}
* 方式二
* 设置init的BeforeUpload(up,file){up.setOption("multipart_params",{userId:1024})}
* 也可以up.setOption({'multipart_params':{userId:1030}});这样可以方便设置多个参数
* 像 up.setOption({'multipart':true,'multipart_params':{userId:1032}})
* 在某些情况下可能更方便
*/
</script>

调用方式

1
2
3
<plup-vue :file-list="fileListForAttach" list-type="text" :params="extendParamsForUploadComponent" :extensions="format" :maxSize="1024">
</plup-vue>

CSS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
/*上传组件*/
.supload{
line-height: 30px;
.pgs{
line-height:20px;width:600px;
}
.list-text{
line-height:20px;width:600px;
.item-text{
line-height: 30px;
height: 30px;
background: #ededed;
position: relative;
margin: 5px auto;
.progress{
position: absolute;
height: 100%;
width: 1%;
background: green;
top: 0;left: 0;
opacity: 0.2;
transition: width 0.5s;
-moz-transition: width 0.5s;
-webkit-transition: width 0.5s;
-o-transition: width 0.5s;

}
.title{
float: left;height: inherit;line-height: inherit;padding: 0 10px;
width: 500px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.close{
float: right;height: inherit;
line-height: inherit;padding: 0 10px;
cursor: pointer;
color:$colorRed
}
}
}
.list-picture {

.pic-bx{
float: left;
margin: 10px 10px 10px 0;
border:1px dashed #ededed;
img{width: 100%; }
position: relative;
}
.cl-bx{
width: 100%;height: 25px;background: rgba(1,1,1,0.5);
position: absolute;right: 0;top: 0;
text-align: right;
line-height: 25px;
i{
font-size: 15px;
color:#fff;
margin-right: 3px;
cursor: pointer;
}
}
.small{
width: 240px;height: 180px;overflow: hidden;
border-radius: 4px
}
}
.plupload{
.text_trigger{
display: inline-block;
position: relative;
color:$colorBlue;
cursor: pointer;
}

}

}
.fm-up{
width: 240px;height: 180px;
text-align: center;
cursor: pointer;
// border: 1px solid #03A9F4;
.cc{
height: 100px;width: 200px;
line-height: 20px;
margin: auto;
margin-top: 40px;
}
color:#dcdcdc;
border: 1px solid #dcdcdc;
i{font-size: 25px;}
border-radius: 4px;
p{line-height: 20px;}
span{font-size: 12px;line-height: 20px;}
}

编程日志(03)—圣杯布局、双飞翼布局

双飞翼布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
</head>
<style>
* {
margin: 0;
padding: 0
}

body {
min-width: 550px;
font-weight: bold;
font-size: 20px;
}

#header,
#footer {
background: rgba(29, 27, 27, 0.726);
text-align: center;
height: 60px;
line-height: 60px;
}

#container {
overflow: hidden;
}

.column {
text-align: center;
}

#left,
#right,
#center {
float: left;
/*三列高度撑开对齐,关键步*/
padding-bottom: 10000px;
margin-bottom: -10000px;
}

#center {
width: 100%;
background: rgb(206, 201, 201);
}

#left {
width: 200px;
margin-left: -100%;
background: rgba(95, 179, 235, 0.972);
}

#right {
width: 150px;
margin-left: -150px;
background: rgb(231, 105, 2);
}

.content {
margin: 0 150px 0 200px;
height: 400px;
}
</style>

<body>
<div id="header">#header</div>
<div id="container">
<div id="center" class="column">
<div class="content">
#center
</div>
</div>
<div id="left" class="column">#left</div>
<div id="right" class="column">#right</div>
</div>
<div id="footer">#footer</div>
</body>

</html>

image

说明

  • 三列布局,中间宽度自适应,两边定宽
  • 中间栏要在浏览器中优先展示渲染
  • 允许任意列的高度最高

查看源码及更多


pkg-node打包可执行文件工具

前言

最近在研发基于electron的桌面应用,其中涉及到外设交互部分需要使用dll实现,在经历了艰难的环境配置之后,最后由于electron内存管理问题导致应用崩溃使得electron+ffi实现动态链接库调用这条路无法走通。当时还剩下两种方案可以尝试:

  1. 自己创建dll封装原生dll调用,感觉成功几率很低。
  2. 写node addons编译成node插件,需要较强c++功底,难度较大,且最后成功概率不高。

为什么坚持了两个星期还不放弃是因为node+ffi这条路已经走通了,证明第三方的dll是没有问题的。所以试着能不能解决electron中出现的问题。

当electron不能实现的事实逐渐明朗,我开始转移方向。首先想到的是nwjs+ffi的方案,随后迅速想到为什么不使用node自己实现呢?将此部分功能单独分割成为第三方应用,通过某种通信方式实现(写缓存、数据库、http等)。最不济的情况是我为每台终端部署node服务(此应用是B2B,需要实施,不需要自己安装,这一点帮了大忙)

最好的方式是不需要安装node等一堆东西,直接是可执行文件。因此我开始寻找node打包成可执行文件的方法。

为什么要将node应用打包成可执行文件

  1. 部署方便,给客户演示和生产环境部署都很方便
  2. 保护源代码(很有用)
  3. 宿主机器其他应用对node版本可能有不同的要求
  4. 测试应用在不同node版本下兼容性

pkg打包的使用方法

将Node.js打包为可执行文件的工具有pkg、nexe、node-packer、enclose等,从打包速度、使用便捷程度、功能完整性来说,pkg是最优秀的。

安装

推荐采用局部安装

1
npm install pkg -D

用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
$ pkg -h

pkg [options] <input>

Options:

-h, --help output usage information
-v, --version output pkg version
-t, --targets comma-separated list of targets (see examples)
-c, --config package.json or any json file with top-level config
--options bake v8 options into executable to run with them on
-o, --output output file name or template for several files
--out-path path to save output one or more executables
-d, --debug show more information during packaging process [off]
-b, --build don't download prebuilt base binaries, build them
--public speed up and disclose the sources of top-level project

Examples:

– Makes executables for Linux, macOS and Windows
$ pkg index.js
– Takes package.json from cwd and follows 'bin' entry
$ pkg .
– Makes executable for particular target machine
$ pkg -t node6-alpine-x64 index.js
– Makes executables for target machines of your choice
$ pkg -t node4-linux,node6-linux,node6-win index.js
– Bakes '--expose-gc' into executable
$ pkg --options expose-gc index.js


项目的入口点是强制CLI参数。它可能是:

  • 输入文件的路径
  • package.json ,pkg会去寻找bin属性作为入口文件
  • 路径是一个目录地址,pkg会在目录下寻找package.json文件,然后如上

targets

pkg能同时为多个平台打包。使用-t 选项,定义由三部分组成,如node6-macos-x64:

  • nodeRange node${n} or latest
  • platform freebsd, linux, alpine, macos, win
  • arch x64, x86, armv6, armv7

native addons

如果说pkg还有哪儿还可以改进的地方,那就是无法自动打包二进制模块*.node文件。如果你的项目中引用了二进制模块,如sqlite3,那么你需要手动地将*.node文件复制到可执行文件同一目录,我通常使用命令cp node_modules/**/*.node .一键完成。但是,如果你要跨平台打包,例如在windows上打包linux版本,相应的二进制模块也要换成linux版本,通常需要你手动的下载或者编译。

后台运行与开机自启动

开机自启动原理就是将exe文件,通过命令注册到开机服务项。

SC Create 创建一个Windows系统服务
描述SC 是用于与服务控制管理器和服务进行通信的命令行程序。

命令行,有一点需要注意,等号和值之间要有空格,否则会报错

1
sc create test binPath="E:\node\app.exe" start=auto

可能遇到问题:

第一次打包的时候,会遇到下包很慢很可能超时的问题。到https://github.com/zeit/pkg-fetch/releases
下载对应的包,然后~/.pkg-cache/2.5/目录下,改名为fetched-v8.11.3-macos-x64(参考运行时下的包名字改)即可。
参考https://github.com/zeit/pkg/issues/419

windows 路径一般是用户/username/.pkg-cache

注册为系统服务

当我使用 sc create xx binPath= xx start= auto创建完系统服务后,发现启动报错,大致找了一下。

exe注册成服务了!但是启动服务的时候会报[错误1053:服务没有及时响应启动或控制请求.] ,我想应该是我的exe被服务启动后没有能回复一个服务的消息,不知道要在exe里面做什么样的操作才能不报这样的错误,不是所有的程序都能添加到服务里,要是专问的程序才行,程序里要相应控件,才会有返回值,返回告诉操作系统服务开启情况,QQ程序开发时就不是用来为作为服而设计的,所以不能当服务用

因此参考issues137找到一个nssm的工具,用法很简单。大致罗列一下

第一步:先将程序地址设置到环境变量,就把nssm.exe放到目录下好了。
第二步:nssm install servicename
第三部:按要求配置,注意设置日志输出文件和时间戳
第四步:进入服务列表进行设置,在恢复里面设置第一次失败重启(因为要做开机启动嘛),第二次也重启服务,第三次无操作,千万别设置重启电脑,之前试了一次把硬盘引导分区都破坏了,导致连BIOS都进不去,只好拆下硬盘,用其他电脑PE重做分区,还好是一个新的测试电脑。

参考


编程日志(02)—同一浏览器多用户登录session冲突问题

账号登录冲突

在后端程序中,往往将session和成功登录的用户id进行绑定来保持登录状态,这样前后端交互时就不用显示传递用户ID来判断是哪个用户,并通过设置session过期时间来保障账户的安全。

session的值会被后端设置在cookie,前端无需做什么特别操作,正常请求接口即可。由于session就相当于用户id,当同一个浏览器存在多个标签页,A页是A用户,B页是B用户,那么session绑定的其实是后登陆的那一个用户的id,因为这种绑定是在登陆接口处理的。这样的话假如请求的数据(比如查看个人信息)是基于session拿用户id的,就会导致A用户拿到B用户的数据。

一个临时思路

登陆成功后将用户id加密后存储到本地,在每个需要验证的页面加上这个参数,当用户刷新页面时与本地存储的值进行比较,不符合就跳转登陆页。(后登陆的用户会覆盖上一个用户的本地值,而url里的参数不会变所以会导致URL中获取的值和本地不一致)

由于项目使用的是vue,可以将在路由里设置钩子函数,在跳转路由前检查对应数据,路由是hash模式,获取url参数和本地存储的方法可以自己想办法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

router.beforeEach((to, from, next) => {
// 工具方法,获取url地址中traceid的值
let urltraceid = getPop('traceid');
// 获取localStorege中traceid的值
let localtraceid = getLocalStorage('traceid').traceid
console.log('url=%s,local=%s ', urltraceid, localtraceid)
// 先判断两个参数是否存在
if (urltraceid && localtraceid) {
if (urltraceid == localtraceid) {
next()
} else {
let url = window.location.href.replace(/[\?,\#]\S*/g, '');
window.location.href = url;
next({
path: "/"
});
}
}

})

在登陆的时候设置参数,跳转地址类似于下面

1
index.html?traceid=xx#/main

编程日志(01)--cocosCreator实现类似jQuery的ajax

为什么在cocosCreator中不使用axios等工具

cocosCreator版本:2.0.7

在游戏场景中少不了要使用网络交互,在以往的编程中,用jQuery实现或者axios实现并不是很难,从论坛的反馈来看,cocos对ES6的支持尤其是promise,经过长时间的更新似乎依然没有很好地完善,由于开发时间限制,这里就没有去体验这些说法直接使用回调函数避免兼容性问题原始实现ajax请求。

构造ajax方法

使用XMLHttpRequest实现ajax请求并不困难,这里需要注意的是针对IE10以下版本ActiveXObject的兼容性问题,目前个人没有去验证这一点,且项目本生要求支持基于vue的webview。下面是官方文档给的一个示例

1
2
3
4
5
6
7
8
9
10
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && (xhr.status >= 200 && xhr.status < 400)) {
var response = xhr.responseText;
console.log(response);
}
};
xhr.open("GET", url, true);
xhr.send();

我的目的是实现像JQuery一样使用$.ajax(),只需要定义好传参结构就可以了,且由于目前是自己使用,可以只给出核心功能,参数的检验可以之后完善。下面是完整代码,定义cc.global方便全局使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

cc.global = {
ajax: function(obj) {

let err = '';
var xhr = cc.loader.getXMLHttpRequest();
xhr.open(obj.type, obj.url);
if (obj.type.toUpperCase() == 'GET') {
xhr.setRequestHeader("Content-Type", "text/plain;charset=UTF-8");

} else if (obj.type.toUpperCase() == 'POST') {
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
}
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && (xhr.status >= 200 && xhr.status <= 207)) {
err = false;
} else {
err = true;
}
if (err) {
//错误处理
if (obj.error) {
obj.error()
}
} else {
if (obj.success) {
obj.success(JSON.parse(xhr.responseText))
}
}
};

xhr.send(obj.data ? cc.global.qsPrimitive(obj.data) : null);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
cc.global.ajax({
type: 'post',
url: '',
data: {
userCode:'ac',
userPwd:'pwd'
},
success: function(res) {
console.log(res)
}
})

非常简单,但是还有问题需要说明一下

qs的实现

根据post请求协议规定,post数据需要放在Request body中传输,四种常见的 POST 提交数据方式(application/x-www-form-urlencoded,multipart/form-data,application/json,text/xml),这些可以再研究一下。

在项目中主要用到的还是get请求和post的请求,他们直观的区别就在于GET参数是放在Query Params上,通过在地址后面拼接如下qs序列化的参数来传参,而POST不是说不可以将参数拼在地址里,关于这一点可以参考99%的人都理解错了HTTP中GET与POST的区别,只是有参数的长度限制以及规范使用。目前的项目中需要传递比较大的数据,因此需要将其放在在Request body中

1
?key1=value1&key2=value2

(大多数)浏览器通常都会限制url长度在2K个字节,而(大多数)服务器最多处理64K大小的url

不管是GET还是POST的x-www-form-urlencoded方式,都要求将参数qs序列化。

下面是从node querrystring模块源码扒出的简化的方法,核心只是一个循环语句只需注意一下中文编码的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 简单查询序列化
qsPrimitive: function(obj) {
var sep = sep || '&';
var eq = eq || '=';

// 假设value都是字符串
if (obj !== null && typeof obj === 'object') {
var keys = Object.keys(obj);
var len = keys.length;
var flast = len - 1;
var fields = '';
for (var i = 0; i < len; ++i) {
var k = keys[i];
var v = obj[k];
var ks = encodeURIComponent(k) + eq;
fields += ks + encodeURIComponent(v);
if (i < flast)
fields += sep;

}
return fields;
} else {
return '';

}

},

Zepto源码学习-核心篇

0.前言

Zepto源码1.2.0未压缩带注释约有1835行,之前是当做设计模式来阅读,并没有深入。且在当前前端环境下,JQuery的重要性大大降低了,从事开发工作大多用的是Angular、Vue等,并没有将jQuery用到精通。以训练为目的,尝试将Zepto源码讲的清楚一点

1.构建Zepto

下载源码并install构建

1
2
3
4
git clone https://github.com/madrobby/zepto.git
cd zepto
npm install
npm run dist

这部分内容可以通过阅读官方仓库指南获得,通过进一步分析package.json文件,可以看出是用coffee-script的cake方法做的构建文件make,但是源码并没有用coffee-script来写,所以不用担心。关于coffee-script,个人觉得如果闲的厉害可以去学学,否则不如去学点别的,或者多看一些源码。

2.模块分析

首先要明白一点,如果第一次看,逐行分析是没有意义的,必须先了解整体结构。先整体再局部,然后细化。
通过阅读手册或者make文件可以知道最终代码会包含zepto event ajax form ie几个模块。实际上,手册排版也大致是按照这些模块划分的。

中文手册

3.整体结构分析

如果对原型链有一定的了解和开发经验,将很容易理解zepto的源码结构,如果是新手,那么先了解原型链,再阅读源码,将会加深对原型链的理解。下面给出打包出的zepto.js整体结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(function(global, factory) {
if (typeof define === 'function' && define.amd)
define(function() {
return factory(global)
})
else
factory(global)
}(window, function(window) {
// Zepto核心
var Zepto = (function() {})()
window.Zepto = Zepto
// 一种短路运算,如果成立则执行后面的,否则不执行
window.$ === undefined && (window.$ = Zepto)
// 相应的,也可以这么写
// '$' in window || (window.$ = Zepto);
// 以下是其他Zepto模块
;
(function($) {})(Zepto)
return Zepto
}))

这段代码首先关注最外层的IIFE(立即执行函数表达式),针对AMD模块做了处理。这里可以拓展了解JS的模块化规范CommonJS、AMD等。其他要点已经在代码里做了注释

4.核心模块

上面已经介绍了整个代码的组织结构,接下来分析一下最重要的Zepto核心是怎么实现的。这里可以利用chrome调试工具逐行运行,观察代码的调用栈来了解运行过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script type="text/javascript" src="./dist/zepto.js">

</script>
</head>
<body>
<div class="z"></div>
</body>
<script type="text/javascript">
debugger
$('.z').html('x')
</script>
</html>

写一个简单的页面,从第一行设置断点,然后逐步运行。

4.1 核心模块结构

先看一下简单的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var Zepto = (function() {
function Z(dom, selector) {
var i, len = dom ? dom.length : 0
for (i = 0; i < len; i++) this[i] = dom[i]
this.length = len
this.selector = selector || ''
}
// `$.zepto.Z` swaps out the prototype of the given `dom` array
// of nodes with `$.fn` and thus supplying all the Zepto functions
// to the array. This method can be overridden in plugins.
zepto.Z = function(dom, selector) {
return new Z(dom, selector)
}
zepto.init = function(selector, context) {
// ...
return zepto.Z(dom, selector)
}
// `$` will be the base `Zepto` object. When calling this
// function just call `$.zepto.init, which makes the implementation
// details of selecting nodes and creating Zepto collections
// patchable in plugins.
$ = function(selector, context) {
return zepto.init(selector, context)
}
zepto.Z.prototype = Z.prototype = $.fn
$.zepto = zepto
return $
})()

4.2 zepto.Z和Z对象

这里有一个关键的对象zepto.Z,返回Z对象实例

1
2
3
4
5
6
function Z(dom, selector) {
var i, len = dom ? dom.length : 0
for (i = 0; i < len; i++) this[i] = dom[i]
this.length = len
this.selector = selector || ''
}

可以打断点各种试一试,其实是将选择器选中的dom数组转为对象

1
console.log('%o',$(document).length)

之所以能访问length是因为Z对象实例本身有这个属性

4.3 zepto.init

为了减轻阅读负担,先简化zepto.init函数。简而言之,zepto.init对selector参数,也就是$(selector,context)传入的第一个参数可能出现的情况编写了逻辑分支。看下面代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
zepto.init = function(selector,context){
var dom
if(!selector){
return zepto.Z()
} else if(typeof selector == 'string'){
// ...
} else if(isFunction(selector)){
return $(document).ready(selector)
} else if(zepto.isZ(selector)){
return selector
} else {
// ...
}
return zepto.Z(dom, selector)
}

首先主要逻辑有五条

  1. selector不存在则返回Z空对象
  2. selector是个string则进一步处理
  3. selector是个function则执行ready
  4. selector是Z实例则原样返回,可多层嵌套(((selector))验证
  5. else进一步处理

按照顺序本来应该分别阐述各个逻辑的实现细节,不过可以先放一放。目前我们依然不能够去深追细节,而应该继续从结构层面分析后续的内容。目前,我们只需有一个模糊概念:一切似乎都与Z对象有某种联系。

4.4 运行原理

到目前为止,我们分析了(selector)Z(selector)执行的逻辑从而接触了Z对象。接下去从(selector).html()执行过程进一步了解zepto核心的解构。

首先我们得思考$(selector)拿到的是一个Z对象实例,那么它为什么能够执行html()方法。

首先我们先看一下Z构造方法

1
2
3
4
5
6
function Z(dom, selector) {
var i, len = dom ? dom.length : 0
for (i = 0; i < len; i++) this[i] = dom[i]
this.length = len
this.selector = selector || ''
}

实例化后应当只有length,selector这两个属性,以及类似数组下标的dom节点,马上想到其他方法应该是委托给了原型。

1
zepto.Z.prototype = Z.prototype = $.fn

通过查看$.fn源码,果然发现了html()方法的定义,还有许多attr等也同样在这里定义,这样我们至少弄清楚了运行的流程,剩下的按部就班的阅读各个部分的实现细节就可以了。

1
console.log($(document).__proto__===$.fn)//true

最后看看html的实现

1
2
3
4
5
6
7
8
html: function(html){
return 0 in arguments ?
this.each(function(idx){
var originHtml = this.innerHTML
$(this).empty().append( funcArg(this, html, idx, originHtml) )
}) :
(0 in this ? this[0].innerHTML : null)
},

这里面另外又涉及了empty方法,append方法,说明各个方法之间并不是孤立的。建议用debug的方式追溯,这将覆盖绝大部分的源码,所以我们来看看.fn.fn以外的代码,将.fn放到最后来看。

4.5 $.extend()方法

查阅手册,$.extend()有两个用法

  • $.extend(target, [source, [source2, …]]) ⇒ target

  • $.extend(true, target, [source, …]) ⇒ target v1.0+

  • 通过源对象扩展目标对象的属性,源对象属性将覆盖目标对象属性。

  • 默认情况下为,复制为浅拷贝(浅复制)。如果第一个参数为true表示深度拷贝(深度复制)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 观察递归实现拷贝
function extend(target, source, deep) {
for (key in source) {
if (deep && (isPlainObject(source[key]) || isArray(source[key]))) {
if (isPlainObject(source[key]) && !isPlainObject(target[key])){
target[key] = {}
}
if (isArray(source[key]) && !isArray(target[key])){
target[key] = []
}
extend(target[key], source[key], deep)
} else if (source[key] !== undefined) {
target[key] = source[key]
}
}
}

// Copy all but undefined properties from one or more
// objects to the `target` object.
$.extend = function(target) {

// target 如果是布尔值则第二个参数是目标对象
var deep, args = slice.call(arguments, 1)
if (typeof target == 'boolean') {
deep = target
target = args.shift()
}
// 目标对象只有一个,源对象可以有多个
args.forEach(function(arg) {
extend(target, arg, deep)
})
// 即使传入时是布尔值,运行后已经被替换目标对象
return target
}

代码应当是很好理解的,顺便可以看看设计的两个工具方法

1
2
3
4
5
6
7
isArray = Array.isArray ||function(object){ return object instanceof Array }
function isPlainObject(obj) {
return isObject(obj) && !isWindow(obj) && Object.getPrototypeOf(obj) == Object.prototype
}

$.isArray = isArray
$.isPlainObject = isPlainObject

我们已经可以参考手册给出的方法顺序,逐个去研究一些工具方法,比如再看一个

  • $.parseJSON
  • $.trim
  • $.noop
1
2
3
4
5
6
7
8
9
10
11
if (window.JSON) $.parseJSON = JSON.parse

$.trim = function(str) {
return str == null ? "" : String.prototype.trim.call(str)
}

$.noop = function() {}




4.6 其他工具方法

到目前为止,我们已经大致了解Zepto是怎么运行的,随之代码覆盖面的扩大,出现了许多工具库方法,比如isObject,isWindow这些。这个时候可以按照源码顺序,尝试逐行理解代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 驼峰化
camelize = function(str){ return str.replace(/-+(.)?/g, function(match, chr){ return chr ? chr.toUpperCase() : '' }) }
$.camelCase = camelize

$.contains = document.documentElement.contains ?
function(parent, node) {
return parent !== node && parent.contains(node)
} :
function(parent, node) {
while (node && (node = node.parentNode))
if (node === parent) return true
return false
}

1
2
3
4
5
6
7
8
9
10
11
12
13
$.each = function(elements, callback){
var i, key
if (likeArray(elements)) {
for (i = 0; i < elements.length; i++)
if (callback.call(elements[i], i, elements[i]) === false) return elements
} else {
for (key in elements)
if (callback.call(elements[key], key, elements[key]) === false) return elements
}

return elements
}

1
2
3
4
// filter是数组原生filter
$.grep = function(elements, callback){
return filter.call(elements, callback)
}

差不多覆盖文档中所有的方法,只剩下–$.fn

4.6 $.fn

参考阅读