一、正则表达式的创建和使用
1.创建正则表达式的两种方式
1.1使用正则表达式字面量
const reg = /[a-z]\d+[a-z]/i;//\d是数字,+代表出现一次或多次,/i忽略大小写
优点:
简单方便
不需要考虑二次转义
缺点:
子内容无法重复使用
过长的正则导致可读性差
1.2使用 RegExp 构造函数
const alphabet = '[a-z]';
const reg = new RegExp(`${alphabet}\\d+${alphabet}`, 'i');//\\d二次转义
优点
子内容可以重复使用
可以通过控制子内容的粒度提高可读性
缺点
二次转义的问题非常容易导致 bug
例如:
const reg = new RegExp(`\d+`);
reg.test('1'); // false
reg.test('ddd'); // true
2.常见用法
2.1RegExp.prototype.test()
输入:要求输入字符串,如果输入的不是字符串类型,会尝试进行类型转换,转换失败会抛出 TypeError
输出:true 或者 false,表示匹配成功或失败
const reg = /[a-z]\d+[a-z]/i;
reg.test('a1a'); // true
reg.test('1a1'); // false
reg.test(Symbol('a1a')); // TypeError
2.2RegExp.prototype.source 和 RegExp.prototype.flags
①返回当前正则表达式的模式文本的字符串
②es2015新增,返回当前正则表达式的修饰符的字符串,会对修饰符按照字母升序进行排序(gimsuy)
const reg = /[a-z]\d+[a-z]/ig;
reg.source; // "[a-z]\d+[a-z]"
reg.flags; // "gi"
2.3RegExp.prototype.exec() 和 String.prototype.match()(常用)
输入:RegExp.prototype.exec 要求输入字符串,遇到非字符串类型会尝试转换
String.prototype.match 要求输入正则表达式,遇到其它类型会先尝试转成字符串,再以字符串为 source 创建正则表达式
输出:匹配成功,返回匹配结果
匹配失败,返回 null
const reg = /[a-z]\d+[a-z]/i;
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
注意:当正则表达式含有 g 修饰符时,RegExp.prototype.exec 每次只返回一个匹配结果,数据格式和不含 g 修饰符相同。
String.prototype.match 会返回所有的匹配结果,数据格式会变为字符串数组。
const reg = /(a)/g;
reg.exec('a1a'); // ["a", "a", index: 0, input: "a1a", groups: undefined]
'a1a'.match(reg); // ["a", "a"]
2.4RegExp.prototype.lastIndex
当前正则表达式最后一次匹配成功的结束位置(也就是下一次匹配的开始位置)
注意:lastIndex 不会自己重置,只有当上一次匹配失败才会重置为 0 ,因此,当你需要反复使用同一个正则表达式的时候,请在每次匹配新的字符串之前重置 lastIndex!
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
2.5String.prototype.replace()、String.prototype.search()、String.prototype.split()
'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", ""]
二、场景一:数值
1.数值判断不简单
1.1/[0-9]+/
[]:字符集,使用连字符 - 表示指定的字符范围,如果想要匹配连字符,需要挨着方括号放置,或进行转义0-9 表示匹配从 0 到 9 的数字字符,常用的还有 a-z 匹配小写字母,\u4e00-\u9fa5 匹配汉字等,如果只是匹配数字,还可以使用字符集缩写 \d。
+:限定符,匹配一个或多个
缺点:不是全字符匹配,存在误判,如 /[0-9]+/.test(‘a1’) === true
1.2/^\d+$/
以^开始 $ 结束
缺点:
不能匹配带符号的数值,如 +1,-2
不能匹配小数,如 3.14159
1.3/^ [+ -]?\d+(.\d+)?$/
():圆括号内是一个子表达式,当圆括号不带任何修饰符时,表示同时创建一个捕获组。
?:表示0个或1个
.:. 可以匹配除换行符之外的任意字符,当结合 s 修饰符时,可以匹配包括换行符在内的任意字符,当匹配小数点字符时需要转义。
缺点:
不能匹配无整数部分的小数,如 .123
捕获组会带来额外的开销
1.4/^ [+ -]?(?:\d*.)?\d+$/
(?: ):创建一个非捕获组
*:表示0个或多个
缺点:
不能匹配无小数部分的数值,如 2.
不能匹配科学计数法,如 1e2、3e-1、-2.e+4
2.完整的数值正则怎么写
2.1完整的数值 token
2.2/^ [+ -]?(?:\d+.?|\d*.\d+)(?: e[±]?\d+)?$/i
|:用来创建分支,当位于圆括号内时,表示子表达式的分支条件,当位于圆括号外时,表示整个正则表达式的分支条件
思考:缺点是什么?
个人思考:没有分组,内容太冗长。
答案是:数字的表示方法还有二进制、八进制、十六进制。
3.用正则处理数值
3.1数值的解析
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):
正向肯定环视 / 顺序肯定环视 / 先行断言
用于匹配符合条件的位置
类似的语法还有:
(?!expression) 正向否定环视 / 顺序否定环视 / 先行否定断言
(?<=expression) 反向肯定环视 / 逆序肯定环视 / 后行断言,es2018 新增
(?
g:表示全局匹配,用于取出目标字符串中所有符合条件的结果
注意:按照 CSS 规范,只有数值为 0 才可以省略单位,这种情况没有必要靠正则来过滤。
这个例子中只验证了 px 单位,实际还存在 pt、em、vw 等单位,并且没有考虑百分比的情况。
实际工作中,要根据需求追加处理逻辑。
3.2数值转货币格式
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
{n}:限定符,表示重复 n 次,n 必须是非负整数
类似的语法还有:
{n, m} 表示重复 n 到 m 次,n 和 m 都必须是非负整数,且 n <= m
{n,} 表示重复 n 次以上
$n:用于 replace 的字符串中,表示第 n 个捕获组,n 可以从 1 到 9
$& 表示本次完整的匹配
代码也可以写成:
const reg = /\d(?=(?:\d{3})+(?:,|$))/g;
function formatCurrency(str) {
return str.replace(reg, '$&,');
}
在 es2018 以上的环境,还可以使用反向环视
const reg = /(?<=\d)(?=(?:\d{3})+(?:,|$))/g;
function formatCurrency(str) {
return str.replace(reg, ',');
}
注意:环视中的圆括号也会生成捕获组,所以都要采用 (? 的非捕获组形式。
三、场景二:颜色
1.颜色有多少种表示方式
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 要根据实际需求来做取舍。
1.2rgb/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)?)
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;
/ …… */
2.使用正则处理颜色
2.1十六进制颜色的优化
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'
(?< key>):es2018 新增,具名捕获组
反向引用时的语法为 \k
在 replace 中,使用 $ 来访问具名捕获组
当应用 exec 时,具名捕获组可以通过 execResult.groups[key] 访问
四、场景三:URL
1.用正则表示URL
1.1完整的 URL 规范
1.2解析URL
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'));
注意:
port 捕获组可能为 undefined
要考虑解析失败的情形
2.用正则解析 search 和 hash
2.1完整解析
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: '' }
*?: 可以跟在任何限定符之后,表示非贪婪模式
y:es6 新增,粘连修饰符,和 g 修饰符类似,也是全局匹配。区别在于:
y 修饰符每次匹配的结果必须是连续的
y 修饰符在 match 时只会返回第一个匹配结果
注意:要考虑到空串的情况
== 课后习题==
function getUrlParam(url, key) {
var reg = new RegExp("(?:^|&|#)" + key + "=([^&]+)", "gmi");
if (reg.test(url)) return RegExp.$1;
return "";
}
console.log(getUrlParam('?nothing', 'test')); // ''
console.log(getUrlParam('#a=1&aa=2&aaa=3', 'a')); // '1'
console.log(getUrlParam('&b=1&a=1&b=2', 'b')); // '2'
console.log(getUrlParam('a=1&b=2&c=&d', 'c')); // ''
console.log(getUrlParam('&d==', 'd')); // '='
总结:
明确需求
考虑全面
反复测试
一、什么是 Node.js
1.定义
Node.js® is a JavaScript runtime built on Chrome’s V8 JavaScript engine.
2.与 JavaScript 的区别
基于异步 I/O 相关接口
基于 node_modules 和 require 的模块依赖
提供 C++ addon API 与系统交互
3.Node.js 可以干什么?
Web 服务端:Web Server、爬虫
CLI 命令行脚本:webpack
GUI 客户端软件:VSCode、网易云音乐
IoT, 图像处理, 实时通讯,加密货币…
爬虫例子:
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();
})();
二、Node.js 基础
1.读写文件
const fs = require('fs');
fs.readFile('test.txt', (err, data) => {
console.log(data);
});
console.log('read file content');
2.模块
内置模块:编译进 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);
})
文件模块:原生模块之外的模块,和文件(夹)一一对应
// app.js
var circle = require('./circle.js');
console.log('半径为4的圆面积是:' + circle.area(4));
定义模块
// circle.js
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
模块路径查找:
绝对路径
相对路径:和当前路径处理为绝对路径
模块/文件夹:
原生模块,直接读取缓存
[$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 对象上
三、NPM
1.包管理器
一个package.json文件应该存在于包顶级目录下
二进制文件应该包含在bin目录下
JavaScript代码应该包含在lib目录下
文档应该在doc目录下
单元测试应该在test目录下
2.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"
}
3.包依赖
"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"
},
4.npm的问题
速度问题
安全问题
四、基于 Node.js 的 Web 开发
1.demo
const http = require('http');
const server = http.createServer((req, res) => {
res.end('Hello World');
});
server.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 无规范约束,不利于团队开发
中间件繁多,质量参差不齐,选择困难
2.TODO List 项目实战
源码:https://github.com/lizheming/simple-todo
2.1功能列表
TODO List 的页面
API:
获取 TOO 列表
增加 TODO
删除 TODO
更新 TODO 状态
2.2数据表设计
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;
2.3创建项目
$ thinkjs new todo
$ cd todo
$ npm install
2.4模板渲染
// src/controller/index.js
const Base = require('./base.js');
module.exports = class extends Base {
indexAction() {
return this.display();
}
};
2.5RESTful 接口规范
每个 API 都对应一种资源或资源集合
使用 HTTP Method 来表示对资源的动作
使用 HTTP Status Code 来表示资源操作结果
2.6RESTful API
GET /ticket 获取 ticket 列表
GET /ticket/:id 查看某个具体的 ticket
POST /ticket 新建一个 ticket
PUT /ticket/:id 更新 id 为 12 的 ticket
DELETE /ticket/:id 删除 id 为 12 的 ticekt
2.7创建 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
2.8配置路由
// src/config/router.js
module.exports = [
['/ticket/:id?', 'rest'], // 配置 RESTful API 路由
]
2.9路由解析
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
2.10
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
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 putAction() {
if (!this.id) {
return this.fail('params error');
}
const pk = this.modelInstance.pk;
const data = this.post();
delete data[pk];
if (think.isEmpty(data)) {
return this.fail('data is empty');
}
const rows = await this.modelInstance.where({ [pk]: this.id }).update(data);
return this.success({ affectedRows: rows });
}
模型的好处
简化代码、提高效率
不用太懂 SQL 语句也能操作数据库
避免手写 SQL 语句的安全风险
五、Node.js 的调试
日志调试
断点调试
node --inspect
vscode
ndb
NodeJS 6.3+ 使用 node --inspect
参数启动可以在 Chrome 浏览器中调试,在 chrome://inspect
中可以发现你的项目并启动 devtool
Node 开发角色转换
前端:
跟浏览器打交道,兼容性问题
组件化
加载速度、JS 执行性能、渲染性能
错误监控
XSS、CSRF 等安全漏洞
服务端:
数据库、Redis 等周边服务
性能、内存泄露、CPU、机器管理
服务监控、错误监控、流量监控、报警
SQL注入、目录遍历等安全漏洞