360前端星计划0410

正则的三个应用场景

1.正则表达式的创建和使用

1.1创建正则表达式的两种方式

1.1.1使用正则表达式字面量

const reg = /[a-z]\d+[a-z]/i;
//[a-z]表示从小写字母a到小写字母z
//\d表示匹配数字0到数字9
//+表示\d字符重复一次到多次
//i表示忽略大小写

优点

  • 简单方便
  • 不需要考虑二次转义
    缺点
  • 子内容无法重复使用
  • 过长的正则导致可读性差

1.1.2使用 RegExp 构造函数

const alphabet = '[a-z]';
const reg = new RegExp(`${alphabet}\\d+${alphabet}`, 'i');
//传入两个参数,第一个是正则表达式的内容,第二个是正则表达式的修饰。包含属性 "g"、"i" 和 "m",分别用于指定全局匹配、区分大小写的匹配和多行匹配。

优点

  • 子内容可以重复使用
  • 可以通过控制子内容的粒度提高可读性
    缺点
  • 二次转义的问题非常容易导致 bug
//原意是想要匹配数字,但现在忘记对\进行转义,导致这个正则变成了对字符d的匹配
const reg = new RegExp(`\d+`);
reg.test('1'); // false
reg.test('ddd'); // true

1.2 正则表达式的常见用法

1.2.1 RegExp.prototype.test()

字符串的验证

const reg = /[a-z]\d+[a-z]/i;
reg.test('a1a'); // true
reg.test('1a1'); // false
reg.test(Symbol('a1a')); // TypeError

输入
要求输入字符串,如果输入的不是字符串类型,会尝试进行类型转换,转换失败会抛出 TypeError
输出
true 或者 false,表示匹配成功或失败

1.2.2 RegExp.prototype.source 和 RegExp.prototype.flags

对正则表达式进行反解

const reg = /[a-z]\d+[a-z]/ig;

reg.source; // "[a-z]\d+[a-z]"
reg.flags; // "gi"

get RegExp.prototype.source
返回当前正则表达式的模式文本的字符串
get RegExp.prototype.flags
es2015新增,返回当前正则表达式的修饰符的字符串,会对修饰符按照字母升序进行排序(gimsuy)

1.2.3 RegExp.prototype.exec() 和 String.prototype.match()

对字符串进行解析

const reg = /[a-z]\d+[a-z]/i;
//exec结果第0项是正则表达式匹配到的完整内容,第一项是匹配到结果的开始位置,第2项是输入的完整字符串,第3项是一个具名捕获组
reg.exec('a1a'); // ["a1a", index: 0, input: "a1a", groups: undefined]
reg.exec('1a1'); // null
'a1a'.match(reg); // ["a1a", index: 0, input: "a1a", groups: undefined]
'1a1'.match(reg); // null

输入
RegExp.prototype.exec 要求输入字符串,遇到非字符串类型会尝试转换

String.prototype.match 要求输入正则表达式,遇到其它类型会先尝试转成字符串,再以字符串为 source 创建正则表达式
输出
匹配成功,返回匹配结果
匹配失败,返回 null

const reg = /(a)/g;

reg.exec('a1a'); // ["a", "a", index: 0, input: "a1a", groups: undefined]
'a1a'.match(reg); // ["a", "a"]

当正则表达式含有 g 修饰符时,RegExp.prototype.exec 每次只返回一个匹配结果,数据格式和不含 g 修饰符相同。
String.prototype.match 会返回所有的匹配结果,数据格式会变为字符串数组。
由于 String.prototype.match 返回的数据格式不固定,因此大多数情况都建议使用 RegExp.prototype.exec

1.2.4 RegExp.prototype.lastIndex

主要结合g修饰符进行使用

const reg = /(a)/g;
const str = 'a1a';

reg.lastIndex; // 0
reg.exec('a1a'); // ["a", "a", index: 0, input: "a1a", groups: undefined]
reg.lastIndex; // 1
reg.exec('a1a'); // ["a", "a", index: 2, input: "a1a", groups: undefined]
reg.lastIndex; // 3
reg.exec('a1a'); // null
reg.lastIndex; // 0

当前正则表达式最后一次匹配成功的结束位置(也就是下一次匹配的开始位置)
注意:lastIndex 不会自己重置,只有当上一次匹配失败才会重置为 0 ,因此,当你需要反复使用同一个正则表达式的时候,请在每次匹配新的字符串之前重置 lastIndex!

1.2.5 String.prototype.replace()、String.prototype.search()、String.prototype.split()

replace()把当前字符串中可以匹配到的内容替换成新的内容
search()搜索正则表达式可以匹配到的内容在目标字符串中出现的位置

'a1a'.replace(/a/, 'b'); // 'b1a'
'a1a'.replace(/a/g, 'b'); // 'b1b'

'a1a'.search(/a/); // 0
'a1a'.search(/a/g); // 0

'a1a'.split(/a/); // ["", "1", ""]
'a1a'.split(/a/g); // ["", "1", ""]

2.场景一:正则与数据

2.1数值判断不简单

2.1.1 /[0-9]+/

[]
字符集,使用连字符 - 表示指定的字符范围,如果想要匹配连字符,需要挨着方括号放置,或进行转义
0-9 表示匹配从 0 到 9 的数字字符,常用的还有 a-z 匹配小写字母,\u4e00-\u9fa5 匹配汉字等
如果只是匹配数字,还可以使用字符集缩写 \d
+
限定符,匹配一个或多个​​​​​​​
这个正则的缺点
不是全字符匹配,存在误判,如 /[0-9]+/.test('a1') === true

2.1.2 /^\d+$/

^
匹配字符串开始位置,当结合 m 修饰符时,匹配某一行开始位置
$
匹配字符串结束位置,当结合 m 修饰符时,匹配某一行结束位置
这个正则的缺点

  • 不能匹配带符号的数值,如 +1,-2
  • 不能匹配小数,如 3.14159

2.1.3/^[+-]?\d+(.\d+)?$/

()
圆括号内是一个子表达式,当圆括号不带任何修饰符时,表示同时创建一个捕获组
?
? 在正则中有多种含义,作为限定符时,表示匹配零到一个
.

  • . 可以匹配除换行符之外的任意字符,当结合 s 修饰符时,可以匹配包括换行符在内的任意字符
  • 当匹配小数点字符时需要转义
    这个正则的缺点
  • 不能匹配无整数部分的小数,如 .123
  • 捕获组会带来额外的开销

2.1.4/^[+-]?(?:\d*.)?\d+$/

(?:)
创建一个非捕获组


限定符,匹配零个或多个
这个正则的缺点

  • 不能匹配无小数部分的数值,如 2.
  • 不能匹配科学计数法,如 1e2、3e-1、-2.e+4

2.2 完整的数值正则怎么写

2.2.1 完整的数值 token

https://drafts.csswg.org/css-syntax-3/#number-token-diagram

image.png

注意:这个 token 是 CSS 的 token,在 javascript 中,要多考虑一种情况

+'2.'; // 2
+'2.e1'; // 20

2.2.2 /^[+-]?(?:\d+.?|\d*.\d+)(?: e[+-]?\d+)?$/i

|
用来创建分支,当位于圆括号内时,表示子表达式的分支条件,当位于圆括号外时,表示整个正则表达式的分支条件
i 修饰符
表示匹配时忽略大小写,在这个例子中用于匹配科学计数法的 e,去掉 i 修饰符需要把 e 改为 [eE]

2.3 用正则处理数值

2.3.1数值的解析

下面的场景是想要实现一个函数找出所有的数字并返回

function execNumberList(str) {
   // ……
}

console.log(execNumberList('1.0px .2px -3px +4e1px')); // [1, 0.2, -3, 40]
console.log(execNumberList('+1.0px -0.2px 3e-1px')); // [1, -0.2, 0.3]
console.log(execNumberList('1px 0')); // [1, 0]
console.log(execNumberList('-1e+1px')); // [-10]

实现方法如下

const reg = /[+-]?(?:\d*\.)?\d+(?:e[+-]?\d+)?(?=px|\s|$)/gi;

function execNumberList(str) {
    reg.lastIndex = 0;
    let exec = reg.exec(str);
    const result = [];
    while (exec) {
        result.push(parseFloat(exec[0]));
        exec = reg.exec(str);
    }
    return result;
}

console.log(execNumberList('1.0px .2px -3px +4e1px')); // [1, 0.2, -3, 40]
console.log(execNumberList('+1.0px -0.2px 3e-1px')); // [1, -0.2, 0.3]
console.log(execNumberList('1px 0')); // [1, 0]
console.log(execNumberList('-1e+1px')); // [-10]

(?=expression)
正向肯定环视 / 顺序肯定环视 / 先行断言
用于匹配符合条件的位置

const reg = /[+-]?(?:\d*\.)?\d+(?:e[+-]?\d+)?(?=px|\s|$)/gi;

类似的语法还有:

(?!expression) 正向否定环视 / 顺序否定环视 / 先行否定断言

(?<=expression) 反向肯定环视 / 逆序肯定环视 / 后行断言,es2018 新增

(?

const reg = /[+-]?(?:\d*\.)?\d+(?:e[+-]?\d+)?(?=px|\s|$)/gi;

g
修饰符,表示全局匹配,用于取出目标字符串中所有符合条件的结果
需要注意的点

  • 按照 CSS 规范,只有数值为 0 才可以省略单位,这种情况没有必要靠正则来过滤
  • 这个例子中只验证了 px 单位,实际还存在 pt、em、vw 等单位,并且没有考虑百分比的情况
  • 实际工作中,要根据需求追加处理逻辑

2.2.2数值转货币格式

拿到一个数值转成货币格式,就是每3个数中间加一个逗号

function formatCurrency(str) {
   // ……
}

console.log(formatCurrency('1')); // 1
console.log(formatCurrency('123')); // 123
console.log(formatCurrency('12345678')); // 12,345,678

实现方法如下

const reg = /(\d)(?=(\d{3})+(,|$))/g;
function formatCurrency(str) {
   return str.replace(reg, '$1,');
}

console.log(formatCurrency('1')); // 1
console.log(formatCurrency('123')); // 123
console.log(formatCurrency('12345678')); // 12,345,678

上述代码的解析如下:

const reg = /(\d)(?=(?:\d{3})+(?:,|$))/g;
function formatCurrency(str) {
   return str.replace(reg, '$1,');
}

{n}
限定符,表示重复 n 次,n 必须是非负整数

类似的语法还有:

  • {n, m} 表示重复 n 到 m 次,n 和 m 都必须是非负整数,且 n <= m
  • {n,} 表示重复 n 次以上
const reg = /(\d)(?=(?:\d{3})+(?:,|$))/g;
function formatCurrency(str) {
   return str.replace(reg, '$1,');
}

& 表示本次完整的匹配,所以这段代码还可以改写为:

const reg = /\d(?=(?:\d{3})+(?:,|$))/g;
function formatCurrency(str) {
   return str.replace(reg, '$&,');
}

其它注意事项
环视中的圆括号也会生成捕获组,所以都要采用 (?:) 的非捕获组形式

在 es2018 以上的环境,还可以使用反向环视

3.场景二:正则与颜色

3.1颜色有多少种表示方式

3.1.1 16进制表示法

color: #rrggbb;
color: #rgb;
color: #rrggbbaa;
color: #rgba;

对应正则写法

const hex = '[0-9a-fA-F]';
const reg = new RegExp(`^(?:#${hex}{6}|#${hex}{8}|#${hex}{3,4})$`);

其他注意事项

  • 也可以使用 i 修饰符来匹配大小写,i 修饰符和 a-fA-F 要根据实际需求来做取舍
  • 还记得前面的问题吗?

3.1.2 rgb/rgba 表示法

color: rgb(r, g, b);
color: rgb(r%, g%, b%);
color: rgba(r, g, b, a);
color: rgba(r%, g%, b%, a);
color: rgba(r, g, b, a%);
color: rgba(r%, g%, b%, a%);

对应正则写法

const num = '[+-]?(?:\\d*\\.)?\\d+(?:e[+-]?\\d+)?';
const comma = '\\s*,\\s*';
const reg = new RegExp(`rgba?\\(\\s*${num}(%?)(?:${comma}${num}\\1){2}(?:${comma}${num}%?)?\\s*\\)`);

\n
反向引用,表示引用第 n 个捕获组
由于 r/g/b 必须同时为数值或百分比,所以 %? 只需要捕获一次,用 \1 来引用
\s
字符集缩写,用于匹配空白
需要注意的点

  • 按照规范,rgb(r,g,b,a) 和 rgba(r,g,b) 也是合法的
  • r/g/b 的值应该是 0~255 的整数,但是溢出或小数并不会报错
  • 当捕获组内的内容是可选的时候,一定要把问号写在捕获组内
    如果可选内容的圆括号不可省略,如(a|b|c)?,应该多嵌套一层:((?:a|b|c)?)

3.1.3 其他

/* hsl & hsla */
color: hsl(h, s%, l%);
color: hsla(h, s%, l%, a);
color: hsla(h, s%, l%, a%);

/* keywords */
color: red;
color: blue;
/* …… */

更多的颜色表示方法
https://www.w3.org/TR/css-color/

3.2使用正则处理颜色

3.2.1 16进制颜色的优化

要求实现当字符串中有两位相等的时候可以缩写成一个,实现如下

const hex = '[0-9a-z]';
const hexReg = new RegExp(`^#(?${hex})\\k(?${hex})\\k(?${hex})\\k(?${hex}?)\\k$`, 'i');
function shortenColor(str) {
    return str.replace(hexReg, '#$$$$');
}

console.log(shortenColor('#336600')); // '#360'
console.log(shortenColor('#19b955')); // '#19b955'
console.log(shortenColor('#33660000')); // '#3600'

对上述代码的解析

const hex = '[0-9a-z]';
const hexReg = new RegExp(`^#(?${hex})\\k(?${hex})\\k(?${hex})\\k(?${hex}?)\\k$`, 'i');
function shortenColor(str) {
    return str.replace(hexReg, '#$$$$');
}

(?)

  • es2018 新增,具名捕获组
  • 反向引用时的语法为 \k
  • 在 replace 中,使用 $ 来访问具名捕获组
  • 当应用 exec 时,具名捕获组可以通过 execResult.groups[key] 访问
const hex = '[0-9a-z]';
const hexReg = new RegExp(`^#(?${hex})\\k(?${hex})\\k(?${hex})\\k(?${hex}?)\\k$`, 'i');

hexReg.exec('#33660000');
// ["#33660000", "3", "6", "0", "0", index: 0, input: "#33660000", groups: {r: "3", g: "6", b: "0", a: "0"}]

4.场景三:正则与URL

4.1 用正则解析 URL

4.1.1 完整的 URL 规范

https://www.rfc-editor.org/rfc/rfc3986.html#section-3​​​​​​​

image.png

简单起见,scheme 部分只匹配 http 和 https ,忽略 userinfo 部分

4.1.2解析URL

实现一个函数,解析给定URL的协议、域名、hostname、端口、pathname、search、hash,实现如下:

const protocol = '(?https?:)';
const host = '(?(?[^/#?:]+)(?::(?\\d+))?)';
const path = '(?(?:\\/[^/#?]+)*\\/?)';
const search = '(?(?:\\?[^#]*)?)';
const hash = '(?(?:#.*)?)';
const reg = new RegExp(`^${protocol}\/\/${host}${path}${search}${hash}$`);
function execURL(url) {
    const result = reg.exec(url);
    if (result) {
        result.groups.port = result.groups.port || '';
        return result.groups;
    }
    return {
        protocol: '', host: '', hostname: '', port: '',
        pathname: '', search: '', hash: '',
    };
}

console.log(execURL('https://www.360.cn'));
console.log(execURL('http://localhost:8080/?#'));
console.log(execURL('https://image.so.com/view?q=360&src=srp#id=9e17bd&sn=0'));
console.log(execURL('this is not a url'));
const host = '(?(?[^/#?:]+)(?::(?\\d+))?)';
……
function execURL(url) {
    const result = reg.exec(url);
    if (result) {
        result.groups.port = result.groups.port || '';
        return result.groups;
    }
    return {
        protocol: '', host: '', hostname: '', port: '',
        pathname: '', search: '', hash: '',
    };
}

注意事项

  • port 捕获组可能为 undefined
  • 要考虑解析失败的情形

4.2 用正则解析 search 和 hash

4.2.1完整解析

解析URL,忽略第一个#或?,然后按键值对的方式返回,若一个&中间又多个等号,则只考虑第一个等号,实现如下

function execUrlParams(str) {
    str = str.replace(/^[#?&]/, '');
    const result = {};
    if (!str) {
        return result;
    }
    const reg = /(?:^|&)([^&=]*)=?([^&]*?)(?=&|$)/y;
    let exec = reg.exec(str);
    while (exec) {
        result[exec[1]] = exec[2];
        exec = reg.exec(str);
    }
    return result;
}

console.log(execUrlParams('#')); // { }
console.log(execUrlParams('##')); // { '#': '' }
console.log(execUrlParams('?q=360&src=srp')); // { q: '360', src: 'srp' }
console.log(execUrlParams('test=a=b=c&&==&a=')); // { test: 'a=b=c', '': '=', a: '' }

上述代码解析

const reg = /(?:^|&)([^&=]*)=?([^&]*?)(?=&|$)/y;

*?
? 可以跟在任何限定符之后,表示非贪婪模式(注意:这个例子其实不太恰当,使用贪婪模式效果是一样的)
y
es6 新增,粘连修饰符,和 g 修饰符类似,也是全局匹配。区别在于:

  1. y 修饰符每次匹配的结果必须是连续的
  2. y 修饰符在 match 时只会返回第一个匹配结果
    其它注意事项
    正则表达式如果可能匹配到空字符串,极有可能造成死循环,所以下面这段代码很重要:
 if (!str) {
        return result;
    }

4.2.2解析指定key

给定一个查询参数和key值,输出这个key对应的值,若有多个相同key则返回最后一个,实现如下:

function getUrlParam(str, key) {
    const result = [];
    let reg = new RegExp(`(?:^|&|#)(?:${key}=)([^&]*?)(?:&|$)`, 'g')
    let exec = reg.exec(str);
    while (exec) {
        result.push(exec[1]);
        exec = reg.exec(str);
    }
    let len = result.length;
    return len ? result[len - 1] : ''

}

5.如何用好正则表达式

  • 明确需求
  • 考虑全面
  • 反复测试

Node.js基础入门

1.什么是Node.js

Node.js是基于Chrome V8 JavaScript引擎构建的JavaScript运行时。

与JS的区别

  • 基于异步 I/O 相关接口
  • 基于 node_modules 和 require 的模块依赖
  • 提供 C++ addon API 与系统交互

Node.js 可以干什么?

  • Web 服务端:Web Server、爬虫
  • CLI 命令行脚本:webpack
  • GUI 客户端软件:VSCode、网易云音乐
  • IoT, 图像处理, 实时通讯,加密货币...
    node的一个小例子,爬取豆瓣上哪吒这个电影的名称、资源及描述
const puppeteer = require('puppeteer');
const url = 'https://movie.douban.com/subject/26794435';

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto(url);
  const film = await page.evaluate(() => {
    const title = $('h1 > span:first-child').text();
    const poster = $('#mainpic img').attr('src');
    const desc = $('span[property="v:summary"]').text().trim();
    return {title, poster, desc};
  });

  console.log(JSON.stringify(film, null, '  '));
  await browser.close();
})();

2.Node.js基础

首先去node 官网下载node

读写文件

//引入读写文件的模块
const fs = require('fs');
//读取文件的操作是一个异步的过程
fs.readFile('test.txt', (err, data) => {
    console.log(data);
});
console.log('read file content');

模块

  • 内置模块:编译进 Node 中,例如 http fs net process path 等
  • 文件模块:原生模块之外的模块,和文件(夹)一一对应
    使用内置模块
const fs = require('fs');
fs.readFile('a.text', (err, buffer) => {
  console.log(buffer);
})
const {readFile} = require('fs');
readFile('a.txt', (err, buffer) => {
  console.log(buffer);
})

使用文件模块

var circle = require('./circle.js');
console.log('半径为4的圆面积是:' + circle.area(4));

定义模块

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

模块加载

// 加载绝对路径文件
require('/foo/bar/a.js');

// 加载相对路径文件
require('../a.js');

// 加载无后缀的文件
require('../a');

// 加载外部模块
require('pkg-name');

模块类型

  • .js
  • .json
  • .node
  • .mjs//基于ESModul形式的一个模块
  • ...

模块路径查找

  • 绝对路径
  • 相对路径
    • 和当前路径处理为绝对路径
  • 模块/文件夹
    • 原生模块,直接读取缓存
    • [$NODE_PATH, ~/.node_modules,
      ./node_modules, ../node_modules, ...]
    • 解析 package.json,查找 main 属性,没有则使用 index.js
    • 如果未找到,则报错
      js模块解析
  • 通过 fs.readFileSync 同步拿到文件内容
  • 对内容进行包装
(function (exports, require, module, __filename, __dirname) {
    var circle = require('./circle.js');
    console.log('The area is ' + circle.area(4));
});
  • 通过 vm.runInThisContext 执行
  • 获取 module 对象的值作为模块的返回值

模块缓存

  • 模块加载后会将返回值缓存起来
  • 下次加载时直接读取缓存结果,避免文件 I/O 和解析时间
  • 导出对象缓存在 Module._cache 对象上

3.NPM

NPM包管理器

包管理

  • 一个package.json文件应该存在于包顶级目录下
  • 二进制文件应该包含在bin目录下
  • JavaScript代码应该包含在lib目录下
  • 文档应该在doc目录下
  • 单元测试应该在test目录下
    **# package.json
    **
star-plan npm init -y
Wrote to /Users/lizheming/star-plan/package.json:

{
  "name": "star-plan",
  "version": "1.0.0",//版本号
  "description": "",
  "main": "index.js",//入口文件字段
  "scripts": {//保存一些常用的脚本执行方式
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

还有重要的字段:
1. dependencies

2. devDependencies

3. peerDependencies

4. bundledDependencies

5. optionalDependencies
一些必要的依赖
bin:指定一些key及对应文件
registry:NPM的一个代理
包依赖

"dependencies": {
    "accepts": "^1.2.2",//接受大版本的更新
    "content-disposition": "~0.5.0",//只接受小版本的更新
    "cookies": "~0.7.0",
    "debug": "*",//接受任意版本的更新
    "delegates": "^1.0.0",
    "escape-html": "~1.0.1",
    "fresh": "^0.5.2",
    "only": "0.0.2",//只接受这个版本
    "parseurl": "^1.3.0",
    "statuses": "^1.2.0",
    "type-is": "^1.5.5",
    "vary": "^1.0.0"
  },

注意:可以使用 npm config set init.author.name 等命令修改初始化时的默认值

包依赖

  • 1.0.0 Must match version exactly
  • 1.0.0 Must be greater than version

  • =1.0.0 <1.0.0 <=1.0.0

  • ~1.0.0 "Approximately equivalent to version"
  • ^1.0.0 "Compatible with version"
  • 1.2.x 1.2.0, 1.2.1, etc., but not 1.3.0
    • Matches any version
  • version1 - version2 Same as >=version1 <=version2.

NPM存在的问题

  1. 速度问题
  2. 安全问题
    • 查看源码看 Star
    • https://snyk.io/ 外部的检测服务
    • npm audit

4.基于 Node.js 的 Web 开发

一个简单的小例子

//引入HTTP模块
const http = require('http');
//开启服务
const server = http.createServer((req, res) => {
  res.end('Hello World');
});
//监听3000端口
server.listen(3000);

进阶:Koa

const Koa = require('koa');
const app = new Koa();

// response,使用use进行一个中间的挂载
app.use(ctx => {
  ctx.body = 'Hello Koa';
});

app.listen(3000);

Koa源码

module.exports = class Application extends Emitter {
  ...
  
  listen() {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen.apply(server, arguments);
  }

  use(fn) {
    this.middleware.push(fn);
    return this;
  }
  
  callback() {
    const fn = compose(this.middleware);
    
    if (!this.listeners('error').length) this.on('error', this.onerror);

    const handleRequest = (req, res) => {
      res.statusCode = 404;
      const ctx = this.createContext(req, res);
      const onerror = err => ctx.onerror(err);
      const handleResponse = () => respond(ctx);
      onFinished(res, onerror);
      return fn(ctx).then(handleResponse).catch(onerror);
    };

    return handleRequest;
  }
  
  createContext(req, res) {
    const context = Object.create(this.context);
    const request = context.request = Object.create(this.request);
    const response = context.response = Object.create(this.response);
    context.app = request.app = response.app = this;
    context.req = request.req = response.req = req;
    context.res = request.res = response.res = res;
    request.ctx = response.ctx = context;
    request.response = response;
    response.request = request;
    context.originalUrl = request.originalUrl = req.url;
    context.cookies = new Cookies(req, res, {
      keys: this.keys,
      secure: request.secure
    });
    request.ip = request.ips[0] || req.socket.remoteAddress || '';
    context.accept = request.accept = accepts(req);
    context.state = {};
    return context;
  }
}

Koa的中间件

  • 逻辑分层
  • 路由处理
  • 数据解析、校验
  • 权限校验
  • Session、Cache
  • 数据库、Redis
  • 安全
    存在的问题
    Koa 无规范约束,不利于团队开发 。中间件繁多,质量参差不齐,选择困难
    针对以上问题,对Koa进行了一个企业级的封装,出现了ThinkJS


    image.png

    ThinkJS包含的模块如下


    image.png

TODO List 项目实战

项目样式如下

image.png

功能列表

  • TODO List 的页面
  • API
    • 获取 TOO 列表
    • 增加 TODO
    • 删除 TODO
    • 更新 TODO 状态
      数据表结构设计
CREATE TABLE `todo` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `desc` varchar(255) NOT NULL DEFAULT '',
  `status` tinyint(11) NOT NULL DEFAULT '0' COMMENT '0 是未完成,1是已完成',
  `createdAt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updatedAt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

安装ThinkJS的脚手架
输入以下命令

npm install -g think-cli

安装成功后输入如下命令可以看到ThinkJS的版本号

 thinkjs --version
2.2.8

输入以下命令创建项目

$ thinkjs new todo
$ cd todo
$ npm install

输入以下命令启动项目

simple-todo npm start

模板渲染
把模板文件修改成想要的样子

// src/controller/index.js
const Base = require('./base.js');
module.exports = class extends Base {
  indexAction() {
    return this.display();
  }
};

API开发
使用成熟的 RESTful API进行开发
RESTful 接口规范
1.每个 API 都对应一种资源或资源集合
2.使用 HTTP Method 来表示对资源的动作
3.使用 HTTP Status Code 来表示资源操作结果
实现此次页面所需要的API

  • GET /ticket 获取 ticket 列表
  • GET /ticket/:id 查看某个具体的 ticket
  • POST /ticket 新建一个 ticket
  • PUT /ticket/:id 更新 id 为 12 的 ticket
  • DELETE /ticket/:id 删除 id 为 12 的 ticekt
    通过以下命令创建API文件
simple-todo thinkjs controller -r ticket
//创建成功后返回的结果
   think-cli · Create: src/controller/rest.js
   think-cli · Create: src/controller/ticket.js
   think-cli · Create: src/logic/api/ticket.js

在src/config/router.js文件中进行路由配置

module.exports = [
  ['/ticket/:id?', 'rest'], // 配置 RESTful API 路由
]

路由解析

  • GET /api/todo 获取 TODO 列表,执行 getAction
  • GET /api/todo/:id 获取某个TODO的详细信息,执行 getAction
  • POST /api/todo 添加一个 TODO,执行 postAction
  • PUT /api/todo/:id 更新一个 TODO,执行 putAction
  • DELETE /api/todo/:id 删除一个 TODO,执行 deleteAction
    getAction
    获取数据
// src/controller/rest.js
async getAction() {
  let data;
  if (this.id) {
    const pk = this.modelInstance.pk;
    data = await this.modelInstance.where({ [pk]: this.id }).find();
    return this.success(data);
  }
  data = await this.modelInstance.select();
  return this.success(data);
}

postAction
数据的添加,把用户传过来的数据添加到数据库中

async postAction() {
    const pk = this.modelInstance.pk;
    const data = this.post();
    delete data[pk];
    if (think.isEmpty(data)) {
      return this.fail('data is empty');
    }
    const insertId = await this.modelInstance.add(data);
    return this.success({ id: insertId });
  }

deleteAction
删除数据库中对应id的数据

async deleteAction() {
  if (!this.id) {
    return this.fail('params error');
  }
  const pk = this.modelInstance.pk;
  const rows = await this.modelInstance.where({ [pk]: this.id }).delete();
  return this.success({ affectedRows: rows });
}

putAction
对数据进行更新操作

async deleteAction() {
  if (!this.id) {
    return this.fail('params error');
  }
  const pk = this.modelInstance.pk;
  const rows = await this.modelInstance.where({ [pk]: this.id }).delete();
  return this.success({ affectedRows: rows });
}

对数据库进行配置

// src/config/adapter.js
exports.model = {
  type: 'mysql',
  common: {
    logConnect: isDev,
    logSql: isDev,
    logger: msg => think.logger.info(msg)
  },
  mysql: {
    handle: mysql,
    database: 'todo',
    prefix: '',
    encoding: 'utf8',
    host: '127.0.0.1',
    port: '',
    user: 'root',
    password: 'root',
    dateStrings: true
  }
};

当以上操作完成后就可以对地址进行访问得到todo的列表
数据校验

  • 提供了 Logic 机制转门用来支持数据校验
  • 文件和 Action 与 Controller 一一对应
    具体代码如下
module.exports = class extends think.Logic {
  getAction() {
    this.rules = {
      id: {
        int: true
      }
    };
  }
  deleteAction() {
    this.rules = {
      id: {
        int: true,
        required: true,
        method: 'get'
      }
    };
  }
  putAction() {
    this.rules = {
      id: {
        int: true,
        required: true,
        method: 'get'
      },
      status: {
        int: true,
        required: true
      },
      desc: {
        required: true
      }
    };
  }
  postAction() {
    this.rules = {
      desc: {
        required: true
      }
    };
  }
};

经过上面的配置,当用户删除但是没有提供id的时候逻辑层就会进行判断并返回id不能为空。
数据库操作

  • 封装了 think.Model 类
  • 提供增删改查等操作
  • 支持关联模型查询
  • 自动分析数据表字段类型
  • 自动数据安全过滤
    通过以下命令来创建一个模型
const model = this.model(modeName);

查找规则如下:
根据模型名查找 src/model 下的模型文件

  • 文件存在,实例化对应的模型类
  • 文件不存在,实例化 think.Model 类
    通过以下代码可以创建一个模型类
module.exports = class TodoModel extends think.Model {
  getList () {
    // get list
  }
}

使用模型的好处

  • 简化代码、提高效率
  • 不用太懂 SQL 语句也能操作数据库
  • 避免手写 SQL 语句的安全风险

5.Node.js 的调试

  • 日志调试(通过console.log来调试)
  • 断点调试
    • node --inspect
    • vscode
    • ndb

断点调试

NodeJS 6.3+ 使用 node --inspect 参数启动可以在 Chrome 浏览器中调试,在 chrome://inspect 中可以发现你的项目并启动 devtool

Node 开发角色转换

  • 前端
    • 跟浏览器打交道,兼容性问题
    • 组件化
    • 加载速度、JS 执行性能、渲染性能
    • 错误监控
    • XSS、CSRF 等安全漏洞
  • 服务端
    • 数据库、Redis 等周边服务
    • 性能、内存泄露、CPU、机器管理
    • 服务监控、错误监控、流量监控、报警
    • SQL注入、目录遍历等安全漏洞

你可能感兴趣的:(360前端星计划0410)