卡牌大叔的博客

  • Home

  • Archives

前端技术原理

Posted on 2019-04-02 | Edited on 2019-06-03

reactNative 原理

说明:没有Andorid,ios,c++底层分析功力无法完全通透

React Native桥接器初探link link

  • React Native 渲染

    在 React 框架中,JSX 源码通过 React 框架最终渲染到了浏览器的真实 DOM 中,而在 React Native 框架中,JSX 源码通过 React Native 框架编译后,通过对应平台的 Bridge 实现了与原生框架的通信。如果我们在程序中调用了 React Native 提供的 API,那么 React Native 框架就通过 Bridge 调用原生框架中的方法。

JavaScriptCore是一个C++ vm。使用Apple提供的JavaScriptCore框架,你可以在Objective-C或者基于C的程序中执行Javascript代码,也可以向JavaScript环境中插入一些自定义的对象。JavaScriptCore从iOS 7.0之后可以直接使用。

因为 React Native 的底层为 React 框架,所以如果是 UI 层的变更,那么就映射为虚拟 DOM 后进行 diff 算法,diff 算法计算出变动后的 JSON 映射文件,最终由 Native 层将此 JSON 文件映射渲染到原生 App 的页面元素上,最终实现了在项目中只需要控制 state 以及 props 的变更来引起 iOS 与 Android 平台的 UI 变更。

编写的 React Native代码最终会打包生成一个 main.bundle.js 文件供 App 加载,此文件可以在 App 设备本地,也可以存放于服务器上供 App 下载更新,热更新就会涉及到 main.bundle.js 位置的设置问题。

  • 优点:

1.复用了 React 的思想,有利于前端开发者涉足移动端
2.能够利用 JavaScript 动态更新的特性,快速迭代
3.相比于原生平台,开发速度更快,相比于 Hybrid 框架,性能更好

  • 缺点
  1. 开发者依然要为 iOS 和 Android 两个平台提供两套不同的代。有组件是区分平台的,即使是共用组件,也会有平台独享的函数。
  2. 不能做到完全屏蔽 iOS 端或 Android端,前端开发者必须对原生平台有所了解。
  3. 由于 Objective-C 与 JavaScript 之间的切换存在固定的时间开销,所以性能必定不及原生。(比如目前的官方版本无法做到 UItableview(ListView) 的视图重用,因为滑动过程中,视图重用需要在异步线程中执行,速度太慢。这也就导致随着 Cell 数量的增加,占用的内存也线性增加。)
  • React Native 交互原理总结

Objective-C 有 JavaScript Core 框架用来执行 JavaScript 代码。
JavaScript 通过配置表生成类,方法,参数三个元素,放入消息队列,Objective-C获取之后,
就可以唯一确定要调用的是哪个Objective-C函数,然后调用

babel 原理,抽象语法树如何产生?

  1. 将整个代码字符串分割成 语法单元 数组, 最小语法单元包含。
  • 空白:JS中连续的空格、换行、缩进等
  • 注释:行注释或块注释
  • 字符串
  • 数字 16、10、8进制以及科学表达法等数字表达语法
  • 标识符,没有被引号括起来的的连续字符,包含字母,_,数字,常量 true,false,if , return, function
  • 运算符 +、-、*、/、>、<
  • 括号 ( ), 大扩招,冒号,分好,点,等等

拆分过程就是 一个字符一个字符地遍历,然后分情况讨论,整个实现方法就是顺序遍历和大量的条件判断。

1
2
3
4
5
6
if (1 > 0) {
alert("if \"1 > 0\"");
}

'if' ' ' '(' '1' ' ' '>' ' ' ')' ' ' '{'
'\n ' 'alert' '(' '"if \"1 > 0\""' ')' ';' '\n' '}'

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
function tokenizeCode(code){
const tokens = [];
for(let i=0;i<code.length;i++){
//从0开始,一个字符一个字符读取
let currentChar = code.charAt(i);
if(currentChar === ';'){
tokens.push({
type:'sep',
value:';'
})
continue;
}
if(currentChar === '('|| currentChar===')'){
tokens.push({
type:'parens',
value:currentChar
})
continue;
}
if(currentChar ==='}' || currentChar==='{'){
tokens.push({
type:'brace',
value:currentChar
});
continue;
}
if(currentChar ==='>' || currentChar==='<'){
tokens.push({
type:'operator',
value:currentChar
});
continue;
}
if(currentChar === '"' || currentChar ==='\''){
const token = {
type:'string',
value:currentChar
};
tokens.push(token);
const closer = currentChar;
let escaped = false;
//嵌套循环遍历,寻找字符串结尾
for(i++;i<code.length;i++){
currentChar = code.charAt(i);
token.value += currentChar;
if(escaped) {
escaped = false;
}else if( currentChar === '\\'){
escaped = true;
}else if( currentChar === closer) {
break;
}
}
continue;
}
if(/[0-9]/.test(currentChar)){
const token = {
type:'number',
value:currentChar
};
token.push(token);
for(i++;i<code.length;i++){
currentChar = code.charAt(i);
if(/[0-9\.]/.test(currentChar)){
token.value += currentChar;
}else{
i--;
break;
}
}
continue;
}
if(/[a-zA-Z\$\_]/.test(currentChar)){
const token = {
type:'identifer',
value:currentChar,
};
tokens.push(token)
for(i++;i<code.length;i++){
currentChar = code.charAt(i);
if(/[a-zA-Z0-9\$\_]/.test(currentChar)){
token.value +=currentChar;
}else{
i--;
break;
}
}
continue;
}
if(/\s/.test(currentChar)){
const token = {
type:'whitespace',
value:currentChar,
};
token.push(token);
for(i++;i<code.length;i++){
currentChar = code.charAt(i);
if(/\s]/.test(currentChar)){
token.value += currentChar;
}else{
i--;
break;
}
}
continue;
}
throw new Error('Unexpected '+currentChar);
}
return tokens;
}
  1. 语义分析:在分析结果的基础上分析 语法单元之间的关系

在编程语言的解析中有两个很相似但是又有区别的重要概念:

  • 表达式:每个表达式都会产生一个值,它可以放在任何需要一个值的地方
1
2
3
var a = (5 + 6) / 2; //表达式:(5 + 6) / 2
var b = (function(){ return 25;})(); //表达式: (function(){ return 25;})()
foo(a*b); //表达式:a*b
  • 语句: 是由“;(分号)”分隔的句子或命令。如果在表达式后面加上一个“;”分隔符,这就被称为“表达式语句”。它表明“只有表达式,而没有其他语法元素的语句”。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var num = 9; //声明、赋值语句
    vloop: //标签语句
    { //其实这里大括号可以不需要的,在这里我只想向大家展示一下这种代码块结构而已
    for(var i=1; i<10; i++) { //循环语句
    if(i==num){ //条件语句
    break vloop;
    }else{
    num = num - 1;
    }
    }
    }
    console.log(num); //表达式语句,输出:5
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
/*
* 如何语法分析
*
* stash()函数暂存当前的i,nextToken()读取一下个符号 token[i]
*
* 1, 判断if语句:
* 如果curToken.type为标识符,为if, nextToken()紧接着读取下一个token,
* 如果token类型不是空格,或者不是 '(', 抛出错误
* 后续statement.test = nextExpression() 的一个表达式是if的判断条件,递归找出if内的表达式
* nextToken()
* 如果后续token类型不是空格或者不是')',抛出错误
* statement.consequent = nextStatement() (无限递归)找出下一个语句是if成立时执行的语句
*
* 如果下一个符号是 else 就说明还存在 if 不成立时的逻辑
* if (curToken === 'identifier' && curToken.value === 'else') {
statement.alternative = nextStatement();
} else {
statement.alternative = null;
}
commit(); 上一个暂存点销毁,不再需要了。
return statement


2 判断如果是curToken.type === 'brace' && curToken.value ==="{"
* 认为是 { block代码块
* statement = { type:'BlockStatement',body:[],}
* statsh(); nextToken();
* if (curToken.type === 'brace' && curToken.value === '}') { commit(); //表示代码块结束}
* rewind(); //回到原来位置
statement.body.push(nextStatement()); //递归,寻找block代码块中的语句or表达式
* */

function parse (tokens) {
let i = -1;
let curToken;


function nextStatement(){
stash();
nextToken();

if(curToken.type === 'identifier' && curToken.value ==='if'){
//解析 if 语句
const statement = {
type:'IfStatement',
}

nextToken();
if (curToken.type !== 'parens' || curToken.value !== '(') {
throw new Error('Expected ( after if');
}

// 后续的一个表达式是 if 的判断条件
statement.test = nextExpression();
// 判断条件之后必须是 )
nextToken();
if(curToken.type!=='parens' || curToken.value!==')'){
throw new Error('Expected ) after if test expression');
}
statement.consequent = nextStatement();
if(curToken === 'identifier' && curToken.value ==='else'){
statement.alternative = nextStatement();
}else{
statement.alternative = null;
}
commit();
return statement;
}
if(curToken.type ==='brace' && curToken.value ==='{'){
// 以 { 开头表示是个代码块,我们暂不考虑JSON语法的存在
const statement = {
type: 'BlockStatement',
body: [],
};
while (i<tokens.length){
// 检查下一个符号是不是 }
stash();
nextToken();
if(curToken.type==='brace'&&curToken.value ==="}"){
commit();
break;
}
rewind();
statement.body.push(nextStatement());
}
commit();
return statement;
}

rewind();
const statement = {
type:'ExpressionStatement',
expression:nextExpression(),
};
if(statement.expression){
nextToken();
if (curToken.type !== 'EOF' && curToken.type !== 'sep') {
throw new Error('Missing ; at end of expression');
}
return statement;
}
}


// 读取下一个表达式
function nextExpression () {
nextToken();
if (curToken.type === 'identifier') {
const identifier = {
type: 'Identifier',
name: curToken.value,
};
stash();
nextToken();
if (curToken.type === 'parens' && curToken.value === '(') {
// 如果一个标识符后面紧跟着 ( ,说明是个函数调用表达式
const expr = {
type: 'CallExpression',
caller: identifier,
arguments: [],
};
stash();
nextToken();
if (curToken.type === 'parens' && curToken.value === ')') {
// 如果下一个符合直接就是 ) ,说明没有参数
commit();
} else {
// 读取函数调用参数
rewind();
while (i < tokens.length) {
// 将下一个表达式加到arguments当中
expr.arguments.push(nextExpression());
nextToken();
// 遇到 ) 结束
if (curToken.type === 'parens' && curToken.value === ')') {
break;
}
// 参数间必须以 , 相间隔
if (curToken.type !== 'comma' && curToken.value !== ',') {
throw new Error('Expected , between arguments');
}
}
}
commit();
return expr;
}
rewind();
return identifier;
}


if (curToken.type === 'number' || curToken.type === 'string') {
// 数字或字符串,说明此处是个常量表达式
const literal = {
type: 'Literal',
value: eval(curToken.value),
};
// 但如果下一个符号是运算符,那么这就是个双元运算表达式
// 此处暂不考虑多个运算衔接,或者有变量存在
stash();
nextToken();
if (curToken.type === 'operator') {
commit();
return {
type: 'BinaryExpression',
left: literal,
right: nextExpression(),
};
}
rewind();
return literal;
}

if (curToken.type !== 'EOF') {
throw new Error('Unexpected token ' + curToken.value);
}
}

function nextToken(){
do{
i++;
curToken = tokens[i] || { type:'EOF' }
}while (curToken.type==='whitespace');
}

const stashStack = [];

function stash(cb){
stashStack.push(i);
}

function rewind(){
i = stashStack.pop();
curToken = tokens[i];
}

function commit(){
stashStack.pop()
}
const ast = {
type:'Program',
body:[]
}
while (i<tokens.length){
const statement = nextStatement();
if(!statement){ break; }
ast.body.push(statement);
}
return ast;
}

webpack 原理

实现一个简单的webpack link

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
let mainFile = parse(config.entry, true) //babylon.parse 返回入口js文件的AST抽象语法树,依赖
return {
fileName,
dependence: getDependence(ast),
code: getTranslateCode(ast),
};

let queue = getQueue(mainFile) //分析递归主文件依赖,返回文件依赖的字面量树。

console.log('queue',queue);
return bundle(queue) ;//打包所有js到一个文件。

=====

const fs = require('fs');
const path = require('path');
const babylon = require('babylon');
const traverse = require('babel-traverse').default;
const {transformFromAst} = require('babel-core’);
const loaderUtils = require('loader-utils')

let config = {}


//fs.readFileSync获取源代码,在转换成ast之前执行loader
function getSource(modulePath){
try{
let rules = this.config.module.rules
const content = fs.readFileSync(modulePath, 'utf-8') //读取源文件
for(let i = 0;i<rules.length;i++){
let {test, use } = rules[i]
let len = use.length -1
if(test.test(modulePath)){
//递归处理所有loader
function loopLoader() {
let loader = require(use[len--])
let content = loader(content);
if(len>=0){
loopLoader()
}
}
loopLoader()
}
}
return content
}catch (e) {
throw new Error(`获取数据错误 : ${modulePath}`)
}
}

/**
* 获取文件,解析成ast语法
* @param filename
* @returns {*}
*/
function getAst (source) {
const content = source
return babylon.parse(content, {
sourceType: 'module',
});
}


function getDependence (ast) {
let dependencies = []
traverse(ast, {
ImportDeclaration: ({node}) => {
dependencies.push(node.source.value);
},
})
console.log('dependencies',dependencies);
return dependencies
}

/**
* 编译
* @param ast
* @returns {*}
*/
function getTranslateCode(ast) {
const {code} = transformFromAst(ast, null, {
presets: ['env']
});
console.log('code',code);
return code
}


/**
* 生成完整的文件依赖关系映射
* @param fileName
* @param entry
* @returns {{fileName: *, dependence, code: *}}
*/
function parse(fileName, entry) {
let filePath = fileName.indexOf('.js') === -1 ? fileName + '.js' : fileName
let dirName = entry ? '' : path.dirname(config.entry)
let absolutePath = path.join(dirName, filePath)
let source =this.getSource(absolutePath)
const ast = getAst(source)

return {
fileName,
dependence: getDependence(ast),
code: getTranslateCode(ast),
};
}


/**
* 获取深度队列依赖关系
* @param main
* @returns {*[]}
*/
function getQueue(main) {
let queue = [main]
for (let asset of queue) {
asset.dependence.forEach(function (dep) {
let child = parse(dep)
queue.push(child)
})
}
return queue
}


function bundle(queue) {
let modules = ''
queue.forEach(function (mod) {
modules += `'${mod.fileName}': function (require, module, exports) { ${mod.code} },`
})

const result = `
(function(modules) {
function require(fileName) {
const fn = modules[fileName];
const module = { exports : {} };
fn(require, module, module.exports);
return module.exports;
}
require('${config.entry}');
})({${modules}})
`;
// We simply return the result, hurray! :)
return result;
}


//option tinypack.config.js
function bundleFile(option) {
console.log(option)
config = option
let mainFile = parse(config.entry, true) //返回入口js文件的AST抽象语法树,依赖
let queue = getQueue(mainFile) //递归主文件依赖,返回文件依赖的字面量树。
console.log('queue',queue);
return bundle(queue) ;//打包所有js到一个文件。
}

module.exports = bundleFile


`

DIFF 算法实现 原理

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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
import {StateEnums,isString, move } from ‘./util’
import Element from ‘./element’

export default function diff(oldDomTree, newDomTree){
let patches = { } //记录差异
dfs(oldDomTree, newDomTree, 0, patches)
return patches
}
//树递归
//①,比较新旧节点,
function dfs(oldNode, newNode, index, patches){
let curPatches = [ ] //用于保存子树更改
// 需要判断三种情况
/*1.没有新的节点,什么都不做
2.新的节点 tagName和 key(可能没有) 和旧的相同,开始比较属性是否相同,如果不同则入栈属性改变状态,
(diffProps 比较属性。
* 遍历旧节点的属性key,如果旧节点属性key 新节点没有这个key,入栈属prop key (说明删除了)
* 遍历新节点尚属性key,若旧节点存在key,比较新旧vlue值,oldprops[key] !== 新节点props[key], 入栈 新节点key,新节点 value(说明添加了key)
若不存在oldProps[key], 入栈新 {prop:key,value:newProps}
)
开始遍历子树, diffChildren(oldNode.children, newNode.children, index, patches){
let { changes, list } = listDiff ( oldChild, newChild, index, patches )
//判断列表差异算法实现 Vitual Dom核心算法
/*
* 迭代oldList,找到oldlist的节点key,声明一个临时栈oldlistTemp,声明change栈
如果oldList节点存在的key newList不存在,说明oldList此节点key被删除,入栈null到oldKeyTemp里
如果都存在key相当说明 子树上存在相同节点,只用对比两个节点是否需要运动, 入栈key到oldKeyTemp里
* 迭代oldKeyTemp, 发现有null,入栈change.push({type:StateEnums.Remove, index:i})
* 迭代newlist,找到节点key,
如果存在oldKeyTemp[key],说明这个节点从旧到新没有发生操作,
但需要判断节点树层级,如果index!=i 入栈 changes.push({ type:StateEnums.Move, from:index,to:i }),移动节点
如果不存在oldkeyTemp.indexOf(key) === -1, 说明这个节点是个新节点,
入栈 changes.push({ type:StateEnums.Insert, node:item, index:i}) oldKeyTemp添加此key
listDiff方法返回 { changes, oldListTemp }

diffChildren中拿到change,list, 合并更改到patches里。 再次比较新旧节点,如果新节点存在字子节点,递归dfs重复上面过程
}
dfs返回所有节点的替换,删除,移动信息。


3.新的节点的tagName和 ‘key’ 和旧的不同,就替换所有树,不需遍历旧节点子元素,因为整个节点都被替换掉了,入栈节点替换状态

最后调用patch(root, diffs), 应用更新,由于更新用的都是无关dom操作的api,完成替换。
StateEnums.ChangeProps/remove/move/insert


*/
if(!newNode){ }
else if(newNode.tag===oldNode.tag && newNode.key === oldNode.key){
//判断属性是否变更
let props = diffProps(oldNode.props, newNode.props)
if(props.length) curPatches.push({ type:StateEnums.ChangeProps, props })
//遍历子树
diffChildren(oldNode.children, newNode.children, index, patches )
}else{
//节点不同,需要替换
curPatches.push({ type:StateEnums.Replace, node:newNode })
}
if(curPatches.length){
if(patches[index]){
patches[index] = patches[index].concat[curPatches]
}else{
patches[index] = curPatches
}
}
}

//判断属性的更改
/*
1 遍历旧节点的属性key,如果旧节点属性key 新节点没有这个key,入栈属prop key (说明删除了)
2 遍历新节点尚属性key,若旧节点存在key,比较新旧vlue值,oldprops[key] !== 新节点props[key], 入栈 新节点key,新节点 value(说明添加了key)
若不存在oldProps[key], 入栈新 {prop:key,value:newProps}
*/
function diffProps (oldProps, newProps){
let change = []
for(const key in oldProps) {
if(oldProps.hasOwnProperty(key) &&!newProps[key]){
change.push({ prop:key })
}
}
for(const key in newProps){
if(newProps.hasOwnProperty(key)){
const prop = newProps[key]
if(oldProps[key] && oldProps[key]!==newProps[key]){
change.push({ prop:key, value:newProps[key] })
}
}else if(!oldProps[key]){
change.push({ prop:key, value:newProps })
}
}
return change
}

//遍历子元素打标识,判断两个列表差异
/*
listDiff 拿到
*/

function diffChildren (oldChild,newChild,index,patches){
let { changes, list } = listDiff(oldChild, newChild, index, patches)
if(changes.length){
if(patches[index]){ //合并更改
patches[index] = patches[index].concat(changes)
}else{
patches[index] = changes
}
}

let last = null
oldChild &&
oldChild.forEach((item,i)=>{ //递归看是否仍然有子节点,然后进行dfs操作。
let child = item && item.children
if(child){
index = last && last.children? index+last.children.length+1:index+1
let keyIndex = list.indexOf(item.key)
let node = newChild[keyIndex]
// 只遍历新旧中都存在的节点,其他新增或者删除的没必要遍历
if(node){
dfs(item, node, index, patches)
}
}
else
index+=1

last = item
})
}

//判断列表差异算法实现 Vitual Dom核心算法
/* 1 迭代oldList,找到oldlist的节点key,声明一个临时栈oldlistTemp,声明change栈
如果oldList节点存在的key newList不存在,说明oldList此节点key被删除,入栈null到oldKeyTemp里
如果都存在key相当说明 子树上存在相同节点,只用对比两个节点是否需要运动, 入栈key到oldKeyTemp里

2 迭代oldKeyTemp, 发现有null,入栈change.push({type:StateEnums.Remove, index:i})

3 迭代newlist,找到节点key,
如果存在oldKeyTemp[key],说明这个节点从旧到新没有发生操作,
但需要判断节点树层级,如果index!=i 入栈 changes.push({ type:StateEnums.Move, from:index,to:i }),移动节点
如果不存在oldkeyTemp.indexOf(key) === -1, 说明这个节点是个新节点,
入栈 changes.push({ type:StateEnums.Insert, node:item, index:i}) oldKeyTemp添加此key

listDiff方法返回 { changes, oldListTemp }

*/
function listDiff (oldList, newList, index, patches){
let oldKeys = getKeys(oldList)
let newKeys = getKeys(newList)
let change = [ ]

let oldkeyTemp = [ ]
oldList &&
oldList.forEach(item=>{
let key = item.key
if(isString(item)) { key = item }
let index = newKeys.indexOf(key)
if( index === -1){
oldkeyTemp.push(null) //newList不存在key, 说明oldList此节点k被删除,入栈null
}
else oldkeyTemp.push(key) //把newlist, oldlist 都存在的key保留在数组里。
})
//遍历变更后的数组,因为删除数组元素是会更改索引的,所有从后往前删可以保证索引不变,
//找到null节点,入栈删除操作
let length = oldkeyTemp.length
for(let i = length-1; i>0;i--) {
if(!oldkeyTemp[i]){
oldkeyTemp.splice(i,1); //把空节点删掉。
changes.push({ type:StateEnums.Remove, index:i })
}
}
//遍历新的list, 判断是否有节点新增或移动, 同时也对 list 做节点新增和移动节点操作
newList &&
newList.forEach((item,i)=>{
let key = item.key
if(isString(item)) { key = item }
//寻找旧的children中是否含有当前节点
let index = oldkeyTemp.indexOf(key)
//如果没有找到新节点,说明此key是一个新的节点,需要插入
if(index === -1 || key== null){
changes.push({
type:StateEnums.Insert,
node:item,
index:i
})
oldListTemp.splice(i,0,key)
} else {
//找到了,需要判断是否需要移动
if(index!==i){
changes.push({
type:StateEnums.Move,
from:index,
to:i
})
move(list, index, i)
}
}
})
return { changes, oldListTemp }
}

function getKeys (list){
let keys = [ ]
let text
list && list.forEach(item=>{
let key
if(isString(item)) { key = [item] } //如果item本身为文本,将文本作为单数组 key
else if (item instanceof Element) { key = item.key }
keys.push(key)
})
return keys
}


//渲染差异 深度遍历树,将需要的变更操作取出来。 局部更新DOM
let index = 0

export default function patch(node,patchs){
let changes = patchs[index]
let childNodes = node && node.childNodes

if(!childNodes) index += 1
if(changes && changes.length && patchs[index]) { changeDom (node, changes) }
let last = null
if(childNodes && childNode.length){
childNodes.forEach((item,i) => {
index = last && last.children ? index +last.children.length+1:index+1
patch(item, patchs);
last = item
})
}
}

function changeDom ( node, changes, noChild) {
changes &&
changes.forEach(change =>{
let {type} = change
swtich(type) {
case StateEnums.ChangeProps:
let { props } = change
props.forEach( item => {
if(item.value) { node.setAttribute(item.prop, item.value) }
else { node.removeAttribute(item.prop) }
})
break;
case StateEnums.Remove:
node.childNodes[change.index].remove();
break;
case StateEnums.Insert:
let dom
if(isString(change.node)){
dom = document.createTextNode(change.node)
}else if( change.node instanceof Element){
dom = change.node.create()
}
node.insertBefore( dom, nodeChildNodes[change.index])
break;
case StateEnums.Move:
let fromNode = node.childNodes[change.from]
let toNode = node.childNodes[change.to]
let cloneFromNode = fromNode.cloneNode(true)
let cloneToNode = toNode.cloneNode(true)
node.replaceChild(cloneFromNode,toNode)
node.replaceChild(cloneToNode,fromNode)
break;
default:
break;
}

})

}


let test4 = new Element('div', { class: 'my-div' }, ['test4’],’key4')
let test5 = new Element('ul', { class: 'my-div' }, ['test5’],’key5')

let test1 = new Element('div', { class: 'my-div' }, [test4], ‘key1')
let test2 = new Element('div', { id: '11' }, [test5, test4], ‘key2')

let root = test1.render()

let patchs = diff(test1, test2)
console.log(patchs)

setTimeout(() => {
console.log('开始更新')
patch(root, patchs)
console.log('结束更新')
}, 1000)

diff的不足与待优化的地方


看图的 D,此时D不移动,但它的index是最大的,导致更新lastIndex=3,从而使得其他元素A,B,C的index<lastIndex,导致A,B,C都要去移动。
理想情况是只移动D,不移动A,B,C。因此,在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,会影响React的渲染性能。

viturl Dom实现

Virtual Dom实现

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
//创建节点  
//实现逻辑:
1 创建一个el空节点,遍历props,添加为el属性,如果存在key属性,添加key属性到le上
2 判断child参数是否存在,迭代child,判断child的每项是虚拟El节点则递归创建节点方法,不是El节点肯定为Text节点,
创建Text文本节点。 最后统一插入到el空节点下, 递归完成返回el节点

<ul id='list'>
<li class='item'>Item 1</li>
<li class='item'>Item 2</li>
<li class='item'>Item 3</li>
</ul>
var ul = el('ul', {id: ‘list’,class:’list'}, [
el('li', {class: 'item'}, ['Item 1']),
el('li', {class: 'item'}, ['Item 2']),
el('li', {class: 'item'}, ['Item 3'])
],'key')

export default class Element {
/**
* @param {String} tag 'div'
* @param {Object} props { class: 'item' }
* @param {Array} children [ Element1, 'text']
* @param {String} key option
*/
constructor(tag, props, children, key){
this.tag = tag
this.props = props
if(Array.isArray(children)){
this.children = children
}else if(isString(children)){
this.key = children
this.children = null
}
if(key) this.key = key
}
render(){
let root = this._createElement(
this.tag,
this.props,
this.children,
this.key
)
document.body.appendChild(root)
return root
}
create(){
return this._createElement(this.tag,this.props,this.children,this.key)
}
//创建节点
//实现逻辑:
1 创建一个el空节点,遍历props,添加为el属性,如果存在key属性,添加key属性到le上
2 判断child参数是否存在,迭代child,判断child的每项是虚拟El节点则递归创建节点方法,不是El节点肯定为Text节点,
创建Text文本节点。 最后统一插入到el空节点下, 递归完成返回el节点
_createElement(tag,props,child,key){
let el = document.createElement(el);
for(let key in props){
value = props[key];
el.setAttribute(key,value)
}
if(key){
el.setAttribute(‘key’,key)
}
if(child){
child.foreach(elem=>{
if(elem instanceOf Element){
let els = _createElement(elem.tag,elem.props,elem.child,elem.key)
}else{
let els = document.createTextNode(textNode)
}
el.appendChild(els)
})
}
return el;
}
}

Vue-router路由原理

目前在浏览器环境中这一功能的实现主要有两种方式:

利用URL中的hash(“#”) HashHistory

特点:

  • hash虽然出现在URL中,但不会被包括在HTTP请求中。它是用来指导浏览器动作的,对服务器端完全无用,因此,改变hash不会重新加载页面
  • 可以为hash的改变添加监听事件: window.addEventListener(“hashchange”, funcRef, false)
  • 每一次改变hash(window.location.hash),都会在浏览器的访问历史中增加一个记录

HashHistory中的push()方法:

1
2
3
4
5
6
7
8
9
10
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.transitionTo(location, route => {
pushHash(route.fullPath)
onComplete && onComplete(route)
}, onAbort)
}

function pushHash (path) {
window.location.hash = path
}

transitionTo()方法是父类中定义的是用来处理路由变化中的基础逻辑的,push()方法最主要的是对window的hash进行了直接赋值HashHistory.replace()

* replace()方法与push()方法不同之处在于,它并不是将新路由添加到浏览器访问历史的栈顶,而是替换掉当前的路由:
1
2
3
4
5
6
7
8
9
10
11
12
13
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.transitionTo(location, route => {
replaceHash(route.fullPath)
onComplete && onComplete(route)
}, onAbort)
}

function replaceHash (path) {
const i = window.location.href.indexOf('#')
window.location.replace(
window.location.href.slice(0, i >= 0 ? i : 0) + '#' + path
)
}

利用History interface在 HTML5中新增的方法 HTML5History

pushState(), replaceState()使得我们可以对浏览器历史记录栈进行修改:

两者优劣

调用history.pushState()相比于直接修改hash主要有以下优势:

  • pushState设置的新URL可以是与当前URL同源的任意URL;而hash只可修改#后面的部分,故只可设置与当前同文档的URL
  • pushState设置的新URL可以与当前URL一模一样,这样也会把记录添加到栈中;而hash设置的新值必须与原来不一样才会触发记录添加到栈中
  • pushState通过stateObject可以添加任意类型的数据到记录中;而hash只可添加短字符串
  • pushState可额外设置title属性供后续使用

history模式的一个问题

比如用户直接在地址栏中输入并回车,浏览器重启重新加载应用等。
hash模式仅改变hash部分的内容,而hash部分是不会包含在HTTP请求中的:

http://oursite.com/#/user/id // 如重新请求只会发送http://oursite.com/

故在hash模式下遇到根据URL请求页面的情况不会有问题。而history模式则会将URL修改得就和正常请求后端的URL一样

http://oursite.com/user/id

解决:
在 Vue 应用里面覆盖所有的路由情况,然后在给出一个 404 页面。或者,如果是用 Node.js 作后台,可以使用服务端的路由来匹配 URL,当没有匹配到路由的时候返回 404,从而实现 fallback。
react使用一个叫做connect-history-api-fallback 中间件解决

小程序的实现原理是什么?

小程序架构 http://www.bubuko.com/infodetail-1906089.html

generator 原理

Generator 是 ES6中新增的语法,和 Promise 一样,都可以用来异步编程

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
// 使用 * 表示这是一个 Generator 函数
// 内部可以通过 yield 暂停代码
// 通过调用 next 恢复执行
function* test() {
let a = 1 + 2;
yield 2;
yield 3;
}
let b = test();
console.log(b.next()); // > { value: 2, done: false }
console.log(b.next()); // > { value: 3, done: false }
console.log(b.next()); // > { value: undefined, done: true }
从以上代码可以发现,加上 *的函数执行后拥有了 next 函数,也就是说函数执行后返回了一个对象。每次调用 next 函数可以继续执行被暂停的代码。以下是 Generator 函数的简单实现
// cb 也就是编译过的 test 函数
function generator(cb) {
return (function() {
var object = {
next: 0,
stop: function() {}
};

return {
next: function() {
var ret = cb(object);
if (ret === undefined) return { value: undefined, done: true };
return {
value: ret,
done: false
};
}
};
})();
}
// 如果你使用 babel 编译后可以发现 test 函数变成了这样
function test() {
var a;
return generator(function(_context) {
while (1) {
switch ((_context.prev = _context.next)) {
// 可以发现通过 yield 将代码分割成几块
// 每次执行 next 函数就执行一块代码
// 并且表明下次需要执行哪块代码
case 0:
a = 1 + 2;
_context.next = 4;
return 2;
case 4:
_context.next = 6;
return 3;
// 执行完毕
case 6:
case "end":
return _context.stop();
}
}
});
}

前端工程化包含哪些内容?

Posted on 2019-04-02 | Edited on 2019-06-02

前端工程化的衡量标准?

  1. 从开发角度衡量工程化主要体现在“快”。
  2. 从测试角度衡量工程化主要体现在“快”和“准”。
  3. 从部署角度衡量工程化主要体现在“稳”。

开发层面 的构建问题

  1. ES规范与浏览器兼容性不一致[ES规范的转译]
  2. css 的弱编程能力[css预编译器支持,PostCSS 处理 hack后缀]
  3. 资源定位。
  4. 图片压缩/base64 内嵌/CSS Sprites[自动创建 CSS Sprites 图 ]
  5. 模块化组件化开发,
  6. 模块依赖分析和压缩打包[ JavaScript 模块化规范支持]。
  7. 增量更新于缓存
  8. 开发环境、测试环境和生产环境 区分

解决:脚手架制作(Yeoman.js,commander.js),对webpack进行二次封装( Fis,easywebpack) ,
整合一些webpack的一些插件和loader

协作层面

JavaScript部分逻辑依赖接口 API。
解决:webpack-dev-middleware ,动态构建,热更新HMR,Mock服务

部署层面

部分资源需要借助后端工程师部署。
解决:

  • 搭建本地工作流,测试沙箱,
  • 云平台工作流,gitlab,gitflow,WebHook
  • 继续集成持续交付(宙斯盾) 分支实现自动构建和测试。持续交付在持续集成的基础上 ,将集成并自动构建、测试通过的代码
    自动部署至测试或者仿真生产环境中 ,而生产环境的部署仍须人工操作

前端开发人员培养

基础素质

方向 node方向,多终端方向(web/手机), 可视化方向,

不论角色如何转变,前端工程师始终需要坚持如下两项原则。

于产品而言,须保证性能和体验。 于开发而言,须保证快速与严谨。

本地工作流

HMR工作流程

1> 修改源文件并保存后, webpack 监听到 Filesystem Event 事件并触发了重新构建行为
2> 构建完成之后, webpack将模块变动信息传递给 HMR Server。
3> HMRServer通过WebSocket发送Push信息告知HMRRuntime需要更新客 户端模块 , HMRRuntime 随后通过 HTTP 获取待更新模块的内容详情。
4> 最终, HMRRuntime 将更新的模块进行替换,在此过程中浏览器不会进行刷新。

HMR Sever 以何种形式的文件将信息传递给 Runtime, Runtime 又是如何替换模块内容井 且立即生效等 。 其中涉及 webpack 内 部细节以及浏览器原理的部分知识

缓存策略

  • 强制缓存
    Cach-control: no-cache/max-age=31536000(1年)
    expires = Sat, 14 Mar 2020 07:55:24 GMT

协商缓存
request if-none-match:<Etag值> response:Etag,

覆盖更新与增量更新都是建立在启用浏览器强制缓存策略的前提下的。
增量更新是目前被业界广泛使用的前端静态资源更新策略,普遍的实现方案是通过为文件名添加 hash指纹。
覆盖更新的缺陷较多且没有较好的解决方案

  • 覆盖更新:
    hash 指纹:通过既 定的数据摘要算法(目前使用 较广泛的是 md5)计算出文件的 hash值
    1
    2
    3
    <head>
    <link rel=” stylesheet” href=”main .home .css?v=858d5483”>
    </head> <body>

问题:第一,必须保证 html 文件与改动的静态文件同步更新,否则会出现资源不同步 的情况 。第二,不利于版本回滚。

  • 增量更新:
    1
    2
    3
    4
    5
    6
    <head>   
    <link rel=” stylesheet” href=”main.home.858d543.css”>
    </head>
    <body>
    <script type=”text/javascript” src="main.home .bbcdaf73 . js">
    </body>

可以将静态资源先于动态 HTML 部署, 此时静态资源没有引用入口,不会对线上环境产生影响

模块和组件的区别

模块是一个白盒,侧重的是对属性的封装, 重心在设计和开发阶段, 不关注运行时逻辑,组件是一个黑盒,内部的逻辑是不可见的 。
组件是一个可以独立部署的软件单元,面向的是运行时,侧重于产品的功能性。 模块可以理解为零件。

如轮胎上的螺丝钉,而组件则是轮胎,组件是具备某项完整功能的一个整体 。 具体到前端领域, 一个 button是一个模块, 一个包括多个button的导航栏是一个组件。

模块化开发的价值

  1. 避免命名冲突。
  2. 便于依赖管理。
  3. 利于性能优化。
    其一,虽然不使用模块化规范开发也可以 实现按需加载,但是配合模块化规范的依赖管理功能可以让按需加载的模块更加易 于管理.
    其二,使用模块化构建工具将同步的散列模块进行合并打包, 减少了客户端的 HTTP 请求数量,不仅提高了 Web 应用的解析速度,而且减小了服 务器的并发压力;
    其三 ,细粒度的模块划分搭配动态加载令 Web 应用程序的解析更顺畅
  4. 提高可维护性。

  5. 利于代码复用。

react-native笔记

Posted on 2019-03-02 | Edited on 2019-06-02

对ReactNative评价

  • 跨平台:目前React Native官方已经支持iOS、Android两个平台的移动设备,民间也有一些大牛在做macOS、tvOS,甚至UWP平台的适配。但由于不同平台特性不同,并不能一份代码在所有平台上直接运行,React Native的思想是「Learn once, write anywhere」,我们需要针对不同平台的特性写出不同的代码,尽量保持组件的高可复用性。

  • 性能:官方宣称性能堪比Native,实际使用中我们会发现几个问题,比如复杂视图渲染出View层级过多、ListView(等同于iOS上的UITableView)无重用机制、有些组件存在内存泄露。这就会导致在部分低端Android机型上的性能过差,复杂的、大型的应用会有明显性能问题。

  • 热更新:由于App Store应用商店发版迭代效率问题,热更新成为了iOS平台非常渴求的功能,可喜的是React Native的热更新能力非常好,通过将JavaScript代码部署到服务器中,运行过程中即可重新reload整个界面。

  • 学习成本:对于iOS开发者来讲,要了解相当数量的Web前端开发知识才可以进行开发,对于Web前端开发者来讲,对于原生性能调优则需要原生开发知识和经验,所以说学习成本略高。

  • 开发效率:Android和iOS平台可复用很多组件,仅部分代码需要各自平台分别维护,所以比开发两个平台原生应用效率要高得多。加上本身可动态渲染的能力,不用重新编译,Command⌘+R即可重新渲染界面,开发效率更是惊人地快。

React-Native 几大导航类型

  • createStackNavigator 提供APP屏幕之间切换的能力,它是以栈的形式还管理屏幕之间的切换,新切换到的屏幕会放在栈的顶部。
    组件中可以使 navigation.navigate(’routerModuleName’, {param} ) 来切换转场
  • createDrawerNavigator 侧边导航抽屉路由

    navigation.openDrawer( ) 打开侧边栏
    navigation.toggleDrawer( ) 切换侧边栏
    navigation.closeDrawer( ) 关闭侧边栏

  • createMaterialTopTabNavigator 顶部导航

  • createBottomTabNavigator 底部导航

如何开发一个换肤的功能?

1 开发ThemeFactory 包含主题列表,主题样式
2 开发一个Dao,用来持久化应用的主题,包含存,取数据,浏览器可以存储在locationStorage里,react-native存储在AsyncStorage里。
3 开发换肤组件html+css,组件选取盘,比如颜色名称,
4 开发action,配置action类型,THEME_CHANGE(主题更改),THEME_INIT(主题初始化), SHOW_THEME_VIEW(皮肤组件显示), 以及相对应的reducer.
5 用户前端点击换肤组件,触发选取组件事件,在事件里拿到主题的key, 调用Dao保存当前选取主题,解决用户离开应用再返回时候拿到配置
触发sTHEME_CHANGE action,reducer,把颜色key 保存到state里,用来做应用适时替换。
这样就完成了换肤功能。

redux/EventBut/DeviceEventEmitter不同点

redux 用来处理整个应用的共享数据,持久化数据,页面动作相关的数据

EventBus 可以用来处理多个页面之间的 简单的状态传递,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
EventBus.getInstance().addListener(EventTypes.favoriteChanged_trending, this.favoriteChangeListener = () => {
this.isFavoriteChanged = true;
});

EventBus.getInstance().addListener(EventTypes.bottom_tab_select, this.bottomTabSelectListener = (data) => {
if (data.to === 1 && this.isFavoriteChanged) {
this.loadData(null, true);
}
})

if (this.storeName === FLAG_STORAGE.flag_popular) {
EventBus.getInstance().fireEvent(EventTypes.favorite_changed_popular);
} else {
EventBus.getInstance().fireEvent(EventTypes.favoriteChanged_trending);
}

DeviceEventEmitter 用来解决单页面 组件之间的状态处理

1
2
3
4
5
6
this.timeSpanChangeListener = DeviceEventEmitter.addListener(EVENT_TYPE_TIME_SPAN_CHANGE, (timeSpan) => {
this.timeSpan = timeSpan;
this.loadData();
})

DeviceEventEmitter.emit(EVENT_TYPE_TIME_SPAN_CHANGE, tab);

项目结构

action

reducer

dispatch 一个action之后,进入reducer,处理完成之后统一返回给state

作为this.props的属性 返回给 this.props

1
2
3
4
5
6
7
8
9
10
const mapStateToProps = state => ({
popular: state.popular
});

const mapDispatchToProps = dispatch =>({
onLoadPopularData: (storeName, url) => dispatch(actions.onLoadPopularData(storeName,url))

})

const PopularTabPage = connect(mapStateToProps, mapDispatchToProps)(PopularTab)

这里的state.popular实际为 combineReducers里配置的popular名称以及总返回值。

AsyncStorage取代localStorage
import {AsyncStorage} from ‘react-native’

fetch取代 httpajaxrequest请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let url = `https://api.github.com/search/repositories?q=${this.searchKey}`;
console.log(url);
fetch(url)
.then(response=>{
return response.text() //格式为为text
}).then(responseText=>{
this.setState({
showText:responseText
})
}).catch(e=>{
this.setState({
showText:e.toString()
})
})

SafeAreaView 针对iPhone X设备齐刘海页面适配的组件
友盟 U-DPlus 互联网运营数据服务需要申请注册才能使用。集成ReactNative也是巨坑无比

====

react native debug模式加载100%之后空白页

  • “GET /debuggerWorker.js HTTP/1.1” 404 156 “http://localhost:8081/debugger-ui“
  • Downloading JavaScript bundle 100%
  1. “GET /debuggerWorker.js HTTP/1.1”的问题是由于我chrome的问题无法自动打开http://localhost:8081/debugger-ui的页面导致的。
  2. Downloading JavaScript bundle 100%加载之后一直空白页尝试了多次一直都这样。

最后我通过使用官方的react native debugger工具成功debug了

禁用黄色调试信息

constructor(props){
super(props);
console.disableYellowBox = true;
}

reduxifyNavigator 已经废弃


Failed to build iOS project

info
error Failed to build iOS project. We ran “xcodebuild” command but it exited with error code 65. To debug build logs further, consider building your app with Xcode.app, by opening Github_RN_test.xcodeproj
BUILD FAILED

原因:xcode编译未通过,说明ios 有问题,可以打开ios项目在xcode里build一下查看具体出错原因。
一般出现的是模块安装,删除过程中,react-native link过后的模块 出错,没有删除或者未添加。

react-native-vector-icons 出错


reason: react-native-vector-icons 安装后没有执行link 简单执行react-native link 并不能自动执行所有模块的link操作。
react-native link react-native-vector-icons

Navigator 没有用createAppContainer 出错

没有 react-native link

react-native 模块安装后没有 react-native link

不能多重项目嵌套运行

error: No bundle URL present Make sure you’re running a packager server or have included a .jsbundle file

  • 多半是一个项目嵌套多个项目,多个package.json
  • 本地开了翻墙工具

-Go back to the old version of React Native

  • Run “npm install -g react-native-git-upgrade”
  • Run “react-native-git-upgrade”

Loading dependency graph, done. 死住

  • 查看端口8081是否被占用
  • 在浏览器中输入http://localhost:8081/index.ios.bundle,查看是否有bundling的提示
  • 删除手机/模拟器上的App,重新执行run-ios
  • 下载react第三方库 https://github.com/facebook/react-native/tree/0.57-stable/scripts
  • 正常情况下,react-native run-ios打包完成,在模拟器上会马上安装demo APP,然后加载 Loading dependency graph, done,哪个部位出错就解决哪个部位。

解决react-native 8081端口占用问题
lsof -i:8081
kill pid pid

开发目录权限问题

gitclone下来的项目 react-native run-ios 会出现Metro Bundle EACCES: permission denied

  1. cd 你的文件夹路径的上一级目录。
  2. sudo chmod -R 777 你的文件夹名。
  3. 输入密码。
    4.成功
    link

boost_1_63_0.tar.gz等错误

【RN踩坑】React-native 0.45版本以上出现 boost_1_63_0.tar.gz等错误

link

nodejs中间件笔记

Posted on 2019-02-20 | Edited on 2019-06-02

=====

gm 图片处理工具

link
先读取(gm)图片后,可以先进行拼接(mosaic, compose, append),然后裁剪(crop),放缩(resize)到指定大小后,最后才保存(write)下来

formidable 模块

是一个用于处理文件、图片、视频等数据上传的模块,支持GB级上传数据处理,支持多种客户端数据提交。有极高的测试覆盖率,非常适合在生产环境中使用。
link

connect-history-api-fallback

解决react前端直接在浏览器输入地址访问报404错误的问题 link

express-winston winston 日志

Node.js最流行的日志框架之一,设计为一个简单通用的日志库,

express-session

session/cookie对比 link
中间件将会话数据存储在服务器上;它仅将会话标识(而非会话数据)保存在 cookie 中。
从1.5.0版本开始, express-session不再依赖cookie-parser,直接通过req/res读取/写入;默认存储位置内存存储(服务器端),

cookieParser 签名与解析

cookie设置与解析,cookie签名与解析,实现
link

config-lite 自动获取config

自动获取config不同环境下配置文件使用 https://segmentfault.com/a/1190000010099383

captchang 验证码

模块实现图片验证码 https://www.cnblogs.com/fangdong/p/6853124.html

bluebird 异步promise

一种更为优雅的Promise异步使用方式,还有async await

pm2 node进程管理工具

link
link

pm2 start 基本启动命令
pm2 show (appname|id) 查看详细状态信息
pm2 list 查看所有启动的进程列表
pm2 monit 监控每个 node 进程的 cpu 和内存使用情况
pm2 logs 显示所有进程的日志信息
pm2 web 监控运行这些进程的机器的状态
pm2 stop (id|all) 停止 指定/所有 进程
pm2 restart (id|all) 重启 指定/所有 进程
pm2 delete (id|all) * 杀死 指定/所有 进程

pm2 start server.js -i (number|max) 负载均衡

npm install -g pm2-web 实现监控可视化

cross-env能跨平台地设置及使用环境变量

大多数情况下,在windows平台下使用类似于: NODE_ENV=production的命令行指令会卡住,windows平台与POSIX在使用命令行时有许多区别(例如在POSIX,使用$ENV_VAR,在windows,使用%ENV_VAR%。。。)
cross-env让这一切变得简单,不同平台使用唯一指令,无需担心跨平台问题

nodemon 热更新工具

解决问题:修改代码后,需要重新启动 Express 应用,所做的修改才能生效。若之后的每次代码修改都要重复这样的操作,势必会影响开发效率,本文将详细介绍Nodemon,它会监测项目中的所有文件,一旦发现文件有改动,Nodemon 会自动重启应用

1
2
3
4
5
6
"scripts": {
"dev": "cross-env NODE_ENV=development nodemon --harmony index.js",
"check": "cross-env NODE_ENV=production nodemon --harmony index.js",
"test": "echo \"Error: no test specified\" && exit 1",
"deploy": "pm2 deploy production"
}

connect-history-api-fallback

单页面应用程序(SPA)通常使用一个web浏览器可以访问的索引文件,比如index.html,然后,在HTML5 History API的帮助下(react-router就是基于History API实现的),借助JavaScript处理应用程序中的导航。当用户单击刷新按钮或直接通过输入地址的方式访问页面时,会出现找不到页面的问题,因为这两种方式都绕开了History API,而我们的请求又找不到后端对应的路由,页面返回404错误。

connect-history-api-fallback中间件很好的解决了这个问题。具体来说,每当出现符合以下条件的请求时,它将把请求定位到你指定的索引文件(默认为/index.html)。
link

express-session connect-mongo

Express Session的使用
node exprss-session 和connect-mongo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let express = require('express');
let session = require('express-session');
let app = new express();
let MongoStore = require('connect-mongo')(session);
app.use(session({
secret:'keyboard cat', //加密字符串也可以写数组
resave:true, //强制保存session 建议设置成false
saveUninitialized:true, //强制保存未初始化的内容
rolling:true, //动态刷新页面cookie存放时间
cookie:{maxAge:10000}, //保存时效
store:new MongoStore({ //将session存进数据库 用来解决负载均衡的问题
url:'mongodb://127.0.0.1:27017/db06',
touchAfter:24*3600 //通过这样做,设置touchAfter:24 * 3600,您在24小时内
//只更新一次会话,不管有多少请求(除了在会话数据上更改某些内容的除外)
})
}))
app.get('/login',(req,res)=>{
req.session.userinfo = 'admin';
res.send('存储成功');
})

cookie-parser

Express使用进阶:cookie-parser中间件实现深入剖析
express中cookie的使用和cookie-parser的解读

Express function(req,res,next)

Node.js Express 里常常见到function(req, res, next)

req : request的缩写, 请求的数据Request
对象表示 HTTP 请求,包含了请求查询字符串,参数,内容,HTTP 头部等属性。
常用req.body.xx来表示POST的xx属性

res: response的缩写, 响应的数据
Response 对象表示 HTTP 响应,即在接收到请求时向客户端发送的 HTTP 响应数据。
我们常常用res.send() 传送HTTP响应 , res.render()渲染结果页面

next 前往下一个中间件
执行相同路径的下一个方法;

1
2
3
4
5
6
7
8
9
route.get("/a",function(req,res,next){
console.log("a");
next();
})
route.get("/a",function(req,res){
console.log("b");
res.send("a");
})
//a,b

Mongoose

link

chalk

日志中间件

config-lite

config-lite 是一个轻量的读取配置文件的模块。
config-lite 会根据环境变量(NODE_ENV)的不同从当前执行进程目录下的 config 目录加载不同的配置文件。
config-lite 支持 .js、.json、.node、.yml、.yaml 后缀的文件。

Example

config/default.js

1
module.exports = 'default';

config/test.js

1
module.exports = 'test';

config/production.js

1
module.exports = 'production';

====================================

1
2
3
node app

require('config-lite'); //=> 'default'
1
2
3
NODE_ENV=test node app

require('config-lite'); //=> 'test'
1
2
3
NODE_ENV=production node app

require('config-lite'); //=> 'production'

or:

`
NODE_ENV=production node app –host=localhost –port=3000

金融交易能力模型

Posted on 2018-12-03 | Edited on 2019-06-18

webpack/babel笔记

Posted on 2018-11-02 | Edited on 2019-06-02

babel 基础

link

babel-core

是作为babel的核心存在,负责转义ast,
import {tranform, transformFile, transformFileSync, transformFromAst } from ‘babel-core’

babel-cli

babel-cli是一个通过命令行对js文件进行换码的工具。
babel script.js

babel-node

这是一个支持ES6的js执行环境。

babel-register

底层改写了node的require方法,引入babel-register之后所有require并以.es6, .es, .jsx 和 .js为后缀的模块都会经过babel的转译。

1
2
3
4
5
6
7
8
9
10
11
//test.js
const name = 'shenfq';
module.exports = () => {
const json = {name};
return json;
};
//main.js
require('babel-register');
var test = require('./test.js'); //test.js中的es6语法将被转译成es5

console.log(test.toString()); //通过toString方法,看看控制台输出的函数是否被转译

babel-polyfill

polyfill在代码中的作用主要是用已经存在的语法和api实现一些浏览器还没有实现的api,对浏览器的一些缺陷做一些修补。例如Array新增了includes方法,箭头函数、解构赋值、class、Promise
.babelrc babel run command缩写

官方唯一推荐preset:babel-preset-env。

webpack 大致运行流程

  1. 获取配置参数 webpack.config.js
  2. 实例化Compiler, 通过run方法开启编译 //Compiler类,
  3. 根据入口文件, 创建依赖项, 并递归获取所有模块的依赖模块 //
  4. 通过loader去解析匹配到的模块
  5. 获取模板, 把解析好的数据套进不同的模板 // 切割模块儿模板,
  6. 输出文件到指定路径

如何编写一个Loader

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
// stylus-loader

const stylus = require('stylus')
function loader (source) {
let css = ''
stylus.render(source, (err, data) => {
if (!err) {
css = data
} else {
throw new Error(error)
}
})
return css
}
module.exports = loader

// style-loader

function loader (source) {
let script = `
let style = document.createElement('style')
style.innerHTML = ${JSON.stringify(source)}
document.body.appendChild(style)
`
return script
}
module.exports = loader

//使用:

// webpack.config.js
const path = require('path')
const root = path.join(__dirname, './')

const config = {
mode : 'development',
entry : path.join(root, 'src/app.js'),
output : {
path : path.join(root, 'dist'),
filename : 'bundle.js'
},
module : {
rules : [
{
test : /\.styl(us)?$/,
use : [ //root, loaders 路径名称
path.join(root, 'loaders', 'style-loader.js'),
path.join(root, 'loaders', 'stylus-loader.js')
]
}
]
}
}


module.exports = config

// app.js

const name = require('./js/moduleA.js')
require('./style/init.styl')


const oH1 = document.createElement('h1')
oH1.innerHTML = 'Hello ' + name
document.body.appendChild(oH1)

如何写一个webpack插件

1、一个 js 命名函数。
2、在原型链上存在一个 apply 方法。
3、为该插件指定一个 Webpack 的事件钩子函数。
4、使用 Webpack 内部的实例对象(Compiler 或者 Compilation)具有的属性或者方法。
5、当功能完成以后,需要执行 Webpack 的回调函数。

1
2
3
4
5
6
7
8
function myExampleWebpackPlugin( ){  }
myExampleWebpackPlugin.prototype.apply = function(Compiler){
Compiler.plugin(‘compilation’,function(compilation,callback){
console.log(‘this is an example plugin’);
callback( );
})
}
`

Compiler 对象: Compiler 对象代表了 Webpack 完整的可配置的环境。
该对象在 Webpack 启动的时候会被创建,同时该对象也会被传入一些可控的配置,如 Options、Loaders、Plugins。
当插件被实例化的时候,会收到一个 Compiler 对象,通过这个对象可以访问 Webpack 的内部环境。

Compilation 对象: Compilation 对象在每次文件变化的时候都会被创建,因此会重新产生新的打包资源。
该对象表示本次打包的模块、编译的资源、文件改变和监听的依赖文件的状态。

如何手写一个简单的webpack

link

clean-webpack-plugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const CleanWebpackPlugin = require('clean-webpack-plugin’); 不必每一次都有手动清除dist,自动清除。

purifycss-webpack css优化去重去无效代码
css打包优化去重去无效代码 https://blog.csdn.net/zhoulucky1993/article/details/85228990

const PurifyCSS = require("purifycss-webpack");

new PurifyCSS({
paths: glob.sync([
// 要做CSS Tree Shaking的路径文件
path.join(__dirname, ".html"),
path.join(__dirname, "src/*.js")
])
})

extract-text-webpack-plugin

该插件的主要是为了抽离css样式,防止将样式打包在js中引起页面样式加载错乱的现象。

glob-all , glob

在webpack中的使用。
link

读取某个文件夹下所有的文件。

1
2
3
4
5
6
7
glob("./src/components/**/*.js", function (er, files) {
console.log(files);
return files
});
// [ './src/components/index/index.js',
// './src/components/news/n.js',
// './src/components/news/news.js' ]

webpack3-webpack4升级

从webpack4开始官方移除了commonchunk插件,改用了optimization属性进行更加灵活的配置,这也应该是从V3升级到V4的代码修改过程中最为复杂的一部分,
下面的代码即是optimize.splitChunks 中的一些配置参考

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
module.exports = {
optimization: {
runtimeChunk: {
name: 'manifest'
},
minimizer: true, // [new UglifyJsPlugin({...})]
splitChunks:{
chunks: 'async’, //表示哪些代码需要优化,有三个可选值:initial(初始块)、async(按需加载块)、all(全部块),默认为async
minSize: 30000, //表示在压缩前的最小模块大小,默认为30000
minChunks: 1, //表示被引用次数,默认为1
maxAsyncRequests: 5, //按需加载时候最大的并行请求数,默认为5
maxInitialRequests: 3, //一个入口最大的并行请求数,默认为3
automaticNameDelimiter: ‘~’, //命名连接符
name: false, //拆分出来块的名字,默认由块名和hash值自动生成

cacheGroups: { //缓存组。缓存组的属性除上面所有属性外,还有test, priority, reuseExistingChunk
* test: 用于控制哪些模块被这个缓存组匹配到
* priority: 缓存组打包的先后优先级
* reuseExistingChunk: 如果当前代码块包含的模块已经有了,就不在产生一个新的代码块
vendor: {
name: 'vendor',
chunks: 'initial',
priority: -10,
reuseExistingChunk: false,
test: /node_modules\/(.*)\.js/,
default:true/false //开启禁用default缓存组
},
styles: {
name: 'styles',
test: /\.(scss|css)$/,
chunks: 'all',
minChunks: 1,
reuseExistingChunk: true,
enforce: true
}
}
}
}
}

chunks的取值是有initial, async, all。默认情况下是async,
initial , all 模式会将所有来自node_modules的模块分配到一个叫vendors的缓存组;所有重复引用至少两次的代码,会被分配到default的缓存组。
https://www.codercto.com/a/26564.html 关于配置splitChunk的一些介绍,不是很懂。

babel-polyfill和babel-runtime区别

babel-polyfill会有一定副作用,比如:
引入了新的全局对象:比如Promise、WeakMap等。
修改现有的全局对象:比如修改了Array、String的原型链等。
举个例子,我在项目中定义了跟规范不一致的Array.from()函数,同时引入了一个库(依赖babel-polyfill),
此时,这个库可能覆盖了自定义的Array.from()函数,导致出错。

babel-runtime存在的原因。它将开发者依赖的全局内置对象等,抽取成单独的模块,并通过模块导入的方式引入,避免了对全局作用域的修改(污染)。
因此,如果是开发库、工具,可以考虑使用 babel-runtime。

babel-plugin-transform-runtime和 babel-runtime 区别

link

babel-plugin-transform-runtime

用于构建过程的代码转换,babel-runtime是实际导入项目代码的功能模块。

1
2
3
4
var promise = new Promise;     ->  `npm bin`/babel index.js
=>
import _Promise from "babel-runtime/core-js/promise”; //babel-runtime
var promise = new _Promise();

babelrc 详解

babel 用法及其 .babelrc 的配置详解,想做前端架构,拒绝一知半解
如何写好.babelrc?Babel的presets和plugins配置解析

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
es2015(es6集合 )
{
"presets": [ //presets 其实就是一堆plugins的预设起到了方便的作用 如果不采用 presets 完全可以单独引用某个功能
["env",{
“targets”:{
“browsers”:[“last 2 version”,”safari>=7"]
}
}],
[“es2015”],
[“latest”],
[“react"]
],
"plugins": [
"transform-runtime” //babel-runtime是实际导入项目代码的功能模块。转码后额外require core-js的模块如promise class等等
"transform-es2015-classes”,
"transform-regenerator”,
"transform-runtime", {
"helpers": true,
"polyfill”:false, //设置为false可以优化代码
"regenerator": true,
"moduleName": "babel-runtime"
}

]
}

transform-runtime
var sym = Symbol();
var promise = new Promise( );
console.log(arr[Symbol.iterator]( ));

"use strict”;
var _getIterator2 = require("@babel/runtime-corejs2/core-js/get-iterator");
var _getIterator3 = _interopRequireDefault(_getIterator2);
var _promise = require("@babel/runtime-corejs2/core-js/promise");
var _promise2 = _interopRequireDefault(_promise);
var _symbol = require("@babel/runtime-corejs2/core-js/symbol”);
var _symbol2 = _interopRequireDefault(_symbol);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
var sym = (0, _symbol2.default)();
var promise = new _promise2.default();
console.log((0, _getIterator3.default)(arr));

`

webpack处理css文件

css-loader css-loader 中 importLoaders 选项的作用是,用于配置 css-loader 作用于 @import 的资源之前需要经过其他 loader 的个数。@ import 用于 css 源码中引用 其他模块的关键字,如果你的项目中确定不会涉及模块化,可以忽略此配置项。

style-loader 如果需要将编译后的 css文件独立导出,则须将 style-loader 替换为 extract-text­-webpack-plugin,

1
2
3
4
5
6
7
npm install css-loader style-loader --save-dev  
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
]

webpack处理图片文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
npm install url-loader --save-dev // 处理图片文件 
npm install file-loader -D //如果不正确
rules: [
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 8192,
name: path.posix.join('','img/[name].[hash:7].[ext]')
}
// 添加到这并且会按照文件大小, 或者转化为 base64, 或者单独作为文件
//在大小限制可以name属性/[name].[ext],会将我们的文件生成在设定的文件夹下。
},
]

实现Npm run dev

1
2
3
4
5
npm install webpack-dev-server -D //
"scripts": {
“build": "webpack”, //生成dict,build压缩后的文件
"dev": "webpack-dev-server --config ./webpack.config.js”//不压缩直接在线跑
}

react笔记

Posted on 2018-10-14 | Edited on 2019-06-02

React Link NavLink

使用 redux-devtools-extension 会踩到action无法派发的坑,

const composeEnhancers = window.REDUX_DEVTOOLS_EXTENSION_COMPOSE || compose;

import { createStore,compose } from ‘redux’;

const stroe =createStore(reducer,composeEnhancers());

link

如何在react框架下实现一个hashrouter

link

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<HashRouter>  //错误 HashRouter不能包含多个Route子节点
<Route path="/about"></Route>
<Route path="/topic"></Route>
</HashRouter>

<Router>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
</Router>
function Home() {
return (
<div>
<h2>Home<Link to={...}>{l}</Link></h2>
</div>
);
}


react-router4.0 简单介绍

已经不需要配置路由,一切皆组件。

  • react-router:提供了一些router的核心api,包括Router,Route,Switch等
  • react-router-dom:提供了BrowserRouter, HashRouter,Route,Link,NavLink

react-router-dom核心用法

  • HashRouter 和BrowserRouter
  • Route: path exact component render
  • NavLink Link
  • Switch
  • Redirect

jsx如何注释?

1
2
3
4
5
6
7
8
9
10
11
{ /*
<Menu theme="dark">
<SubMenu key="sub4" title={<span><Icon type="setting" />
<span>Navigation Three</span></span>}>
<Menu.Item key="1">Option 9</Menu.Item>
<Menu.Item key="2">Option 10</Menu.Item>
<Menu.Item key="3">Option 11</Menu.Item>
<Menu.Item key="4">Option 12</Menu.Item>
</SubMenu>
</Menu>
*/ }

ant Desigin的 一些优化方案 link

yarn add –save-dev less less-loadr
yarn add –save-dev axios react-router


初识React

如何在React项目中使用less?
link
npm run eject报错。如何解决?
git add .然后git commit -m “init” 然后再npm run eject
React里,jsx所有的变量,表达式都在一个大括号{ }里做的。
相比之下,Vue比较混乱。


创建react项目

  1. VSCode编辑器中使用快捷键ctrl+`打开终端
  2. 在终端输入create-react-app demo,自动创建名称为demo的项目
  3. cd demo进入demo项目中
  4. yarn start或yarn build运行项目
  5. 此时,一个React项目就搭建成功。使用VScode搭建React要简单很多,配置信息都可以省去。直接开始编写组件就可以

VueRouter原理

Posted on 2018-03-17 | Edited on 2019-02-19

Vue-router源码图

网络

Posted on 2018-03-09 | Edited on 2019-06-02

TCP报文

1
TCP头部重要字段
  • Sequence number 此序号保证了TCP传输的报文都是有序的,对端可以通过序号顺序拼接报文
  • Acknowledgement Number 此序号标识数据接收期望接收的下一个字节的编号是多少,同时也标识上一个序号的数据已经收到
  • Window Size 窗口大小,标识还能接收多少字节的数据,用于流量控制
  • 标识符
    • RUG=1 该字段表示 数据部分包含紧急信息,是一个高优先级数据报文
    • ACK=1 该字段 在TCP建立后传送的所有报文必须把ACK设置为1
    • PSH=1 表示接收端应该理解将数据push给应用层
    • RST=1 表示TCP连接出现严重的问题,可能需要重新建立TCP连接
    • SYN=1 SYN=1andACK=0表示当前报文段是一个连接的请求报文,SYN=1andACK=1表示当前报文是一个统一建立连接的应答报文
    • FIN=1 表示此此表文是一个释放连接的请求报文

TCP建立连接三次握手

  • 不管是客户端还是服务端,TCP连接建立后都能发送数据和接收数据,所以TCP是一个全双工协议
  • 期初两端都是closed状态,通信开始,双方都会创建TCB。服务器创建完TCB都进入LISTEN状态,等待客户端发送数据

第一次握手
客户端向服务端发送连接请求 SYN=1 报文段,该报文中包含自身的数据通讯初始序号,请求发送后,客户端进入 SYN-SENT 状态

第二次次握手
服务端接受到 SYN=1 报文段,如果同意连接,则会发送一个 SYN=1 and ACK=1 的应答,包含自身的数据通信初始序号,发送完进入 SYN-RECEIVED 状态

第三次次握手

  • 当客户端收到SYN=1 and ACK=1的同意连接的应答后,还要向服务端发送一个确认ACK=1 的确认报文,客户端发问后进入ESTABLISHED 状态。服务端接受这个应答后也进入 ESTABLISHED 状态,此时连接成功。

TCP为何建立连接需要三次握手,明明两次就可以

  • 背景,在建立连接中,任意一段掉线,TCP都会重复SYN包,一般会重试5次。TCP有超时重传机制
  • 主要为了防止出现失效的连接请求报文文段被服务端接受的情况,从而产生错误
  • 可以想象如下场景。客户端发送了一个连接请求 A,但是因为网络原因造成了超时,这时 TCP 会启动超时重传的机制再次发送一个连接请求 B。此时请求顺利到达服务端,服务端应答完就建立了请求,然后接收数据后释放了连接。
  • 假设这时候连接请求 A 在两端关闭后终于抵达了服务端,那么此时服务端会认为客户端又需要建立 TCP 连接,二次握手的情况下,服务端应答了该请求并进入 ESTABLISHED 状态。但是客户端其实是 CLOSED 的状态,那么就会导致服务端一直等待,造成资源的浪费。

TCP断开四次握手

UDP

特性:

  1. 面向无连接

    • 不需要向TCP 一样三次握手,想发数据就可以发
    • 不会对数据报文进行任何拆分和拼接操作
    • 发送端:应用层将数据传递给传输层的UDP协议,只会给数据增加一个UDP 头标识一下是 UDP 协议,然后就传递给网络层了 在接收端:网络层将数据传递给传输层,UDP 只取出IP报文头就传递给应用层,不会做任何拼接操作
  2. 不可靠性

    • 体现在无连接上,通信不需要建立连接,想发就发,不可靠
    • 收到什么数据就传递什么数据,不会备份数据,发送数据也不关心对方是否已经接收到数据了
    • 不判断网络环境,UDP 无阻塞控制,一直以恒定速度发送数据,不会对发送速率进行调整。弊端是网络条件不好的情况下可能会导致丢包。 对实时性要求高的场景就需要使用UDP而不是TCP
  3. 高效

    • UDP 头部开销小,只有8个字节,TCP二十个字节
    • 不可靠,不复杂,需要保证数据不丢失有序到达
    • 两个十六位的端口号,Source port分别为源端口,Destination port目标端口 length整个数据报文的长度
    • checksum整个数据报文的检测,用于发现头部信息和数据中的错误
  4. 传输方式
    UDP 支持一对一,一对多,多对多,多对一,提供单播,多播,广播的功能
  5. 适合应用场景
    实时性要求高的场景,直播,游戏,视频会议

##TCP UDP区别

  • 不需要向TCP 一样三次握手,想发数据就可以发
  • TCP 建立连接断开连接都需要先进行握手,TCP 传输数据过程中,通过各种算法保证数据可靠性。没有UDP高效

如何阅读源代码

Posted on 2018-02-17 | Edited on 2019-02-18

目的

当我们阅读面前的源码时,无非有以下几种目的:

  • 纯粹学习
  • 添加新功能
  • 重构旧代码
  • 修复他人的Bug

不同的目的会有不同的心情,会影响到工作的进展,像修复他人的Bug这种事情,类似于没被掰弯的男猿捏着鼻子给另外一个男人擦屁股,是很恶心的,很容易让人拒绝的。所以因这种目标而阅读源码,往往是欲拒还迎、欲说还休,效率较低。然而修复实际工作中帮别人修复Bug这种情形,十有八九你要遇到,无可逃避。所以,心理调试很重要。

为了学习去读源码,这是最愉快的最放松的。不过提醒一点,设定可检验的目标才会有收获,否则就会像走到大街上看见一美女擦肩而过那样,惊艳一下下,过后嘛关系嘛收获也没了。

其他的目的,重构旧代码、添加新功能,比帮别人擦屁股略强,因为他带有创造性,创造性的活动能给人带来强烈的愉悦,所以虽然这两种目的也有很多让人不爽的部分,不过想到我可以让一棵老树焕发青春,不爽也就慢慢弱下去了。

工具

工欲善其事必先利其器,这是亘古不变的道理。要很好的完成阅读源码的任务,我们大概需要下列这些工具:

  • SourceInsight,最好的源码浏览工具,它能维护符号库,动态显示上下文,还能绘制调用关系图,最好的,没有之一
  • 纸质笔记本,随时记录心得和疑惑,随时绘制各种图(类图、时序图、框图),比UML工具快,也比Visio快
  • 中性笔
  • 记事本、Notepad++、有道云笔记、为知笔记等,记录阅读源码过程中的关键点、心得体会、分析过程
  • Visio,用于绘制简单的框图,表述源码的模块划分、层次结构等
  • StartUML,用于最后绘制类图、时序图等,方便交流
  • 扫描全能王(CamScanner),一款可以通过拍照达到扫描效果的App,可以用它扫描你在纸质笔记本上写下的文字,绘制的框图,分享给其他人,如果你懒得用软件绘制图标,那手绘之后扫描成电子档就最适合你了

知识准备

前戏很重要,准备好了后面水到渠成快感不断,否则就会频频受挫直感道阻且长。

  • 业务基础,每一份有实际意义的源码都离不开业务,必须先对业务有概念
  • 技术基础,这个源码用什么语言,什么框架,什么第三方模块,都需要先有所了解
  • 文档,尽量找到业务、需求、概要、详细等文档,帮助会很大,然而,我们经常面临的情况是,只有源码,只有源码,只有源码,片言只字的文档也无,所以只好坚信——源码是最好的文档。这个心理门槛儿其实也容易过,你就想像着源码只是神仙姐姐的画像,看再多画像也不抵当面一眼效果强大——要么摧毁三观要么魂牵梦萦
  • 人,搞明白哪个程序员维护过这份代码,方便后面不懂时请教,有时人家点一下顶你自己瞎琢磨一天

运行与开发环境

  • 配置好开发环境,目的是为了调试,对有些程序员来讲,调试是弄明白软件内部机理的最好方法,按着F5、F10、F11、F9,一切都搞定了
  • 配置好运行环境,为使用软件、体验软件做准备,从用户角度,从外面看看软件到底是怎么回事,便于揣摩内部逻辑
    笔记
    在阅读源码的过程中,做笔记是必须的。我有这样的体会,因为代码不是自己写的,很难很快在脑子里刻下印记,经常是看着这里忘了那里,早上觉得弄懂了数据流向,中午吃个饭就忘了。所以,笔记就显得尤为重要。
  • 找到适合你的记录方式,小本本、软件皆可。用软件(Notepad++、有道笔记、为知笔记等)来记录有个坏处——必须切换屏幕,会在形式上中断代码阅读过程。所以我经常在紧张得不能中断时随手用笔写些断句残章在本子上,告一段落时梳理下用软件再记录。
  • 尽可能详细的记录,但不必看到什么记录什么,要间隔性的记录,比如弄明白了某个子模块的逻辑、某个类的作用、某些函数的调用关系时再记录,否则记录这个动作本身会打断思考
  • 每天工作结束,记录进度(弄明白的部分),记录疑问,记录第二天要弄明白什么东西,这样你的工作状态就入栈了,第二天来了很容易出栈,快速进入工作状态
  • 记录看到的优秀设计,提高审美,见贤思齐,自我成长

沧海遗珠

我在漫长的读码生涯里积攒了一些的经验,算是碎碎念,供参考:

  • 理清某一业务如何映射在代码执行流程上的,这点很关键。
  • 理清不同模块间的业务关系,代码调用关系,很关键
  • 调试是弄明白代码调用流程的最快方式,之一
  • 找出关键代码(代表实际对象的类、衔接不同模块的类、代表业务关键节点的类)
  • 分析日志可以帮助分析代码执行流程和业务流程
  • 先用已有的可运行软件,体验业务,琢磨你点这里一下点那里一下代码可能是怎么做出反应的
  • 阅读应该围绕目的,把实现目标放在第一位,比如修改Bug,如果有期限,在最后日期前搞定是第一要务,然后有时间就继续读源码或改进Bug修复方案,力求没有副作用和后遗症,再有时间就修修别人留下的破窗户(你也可以顺带鄙视下前任维护者)
  • 千万次的问,还记得前面说要弄明白谁维护过你要读的代码吧,别不好意思,问吧,问吧,问吧
  • 对着设计文档、接口文档或测试用例看代码
  • 心理调试,勿畏难,别放弃。我有时看代码,看两天也不知道看了个甚,一头雾水两眼发花是常有的事儿,有时真是觉得搞不定了,然而,这要么是你基础知识没准备好,要么是你找错了入口,要知道,任何一份代码,都有一条隐形的线串着,耐心点,总会找到。这样不行就那样,多换换角度,多换换方法,读不行,就调试,调试不行,就运行,运行不行,就研究日志。
  • 给自己设置小奖励,弄明白某个逻辑或某个模块的代码后奖励自己休息一下,5~10分钟,走出办公室转转,或者干脆在网上瞎逛一下,浏览自己喜欢的网站
  • 读不懂才要读,想不明白才要想,这是进步和成长的开始。那些阻挡你的蹂躏你的而杀又不死你的,终将帮助你成长让你变得更强大。
12

卡牌大叔

13 posts
6 tags
RSS
GitHub Weibo Twitter
© 2019 卡牌大叔
Powered by Hexo v3.8.0
|
Theme – NexT.Mist v7.0.0