自动机理论
定义
非确定型有穷自动机(NFA),在某状态下,对于指定的输入,存在多个转移状态。一般情况下,通过某种算法可转换为 DFA
上下文无关文法 描述程序设计语言的结构以及相关集合的重要记号,用来构造编译器的语法分析部件。
字母表 符号的有穷非空集合。用Σ表示
串(有时候称为单词)是从某个字母表中选择的符号的有穷序列。例如 01101 是从二进制字母表Σ={0,1}中选出的串
空串 出现 0 次符号的串,记做ε
串的长度 这个串中的符号数,记做∣ω∣,例如∣ε∣=0
字母表的幂 定义Σk是长度为 k 的串的集合。串的每个符号都属于Σ。无论是什么字母表Σ0={ε}。字母表上所有的串的集合约定为Σ∗,排除空串的集合约定为Σ+
串的连接 设定 x 和 y 都是串,于是 xy 表示 x 和 y 的连接
集合表示法 {ω∣ω的语义描述}。比如{ω∣ω包含相同个数的 0 或 1},还可以把ω换成某个带参数的表达式{0n1n∣n≥1}
当两个状态机交互时,当其状态处于(i,x),对于一个合法的输入Z,可使i→j,x→y,那么我们可以认为(i,x)→(j,y)是可达的
确定型有穷自动机(DFA)
确定型有穷自动机 包含若干个状态的集合,若干个输入符,当有输入时,控制权将由一个状态转移到另一个状态。在任意状态下,对于指定的输入,其转移是唯一的。
一个有穷状态集合,记做Q,
初始状态记做q0,
终止状态记做F
一个有穷输入集合,记做Σ
状态转移函数,记做δ,以一个状态q和一个输入符号a作为参数,返回一个状态p。δ(q,a)=p;p∈Q
- 扩展状态转移函数,记做δ^,以一个状态q和一个输入串ω作为参数,返回一个状态p。
- δ^(q,ε)=q
- 假定ω=xa,a 是最后的输入,δ^(q,ω)=δ(δ^(q,x),a)
DFA 的定义: A=(Q,Σ,δ,q0,F)
对于所有 DFA,我们可以定义为 L(A)={ω∣δ^(q0,ω)∈F}
例如{ω∣ω出现 01 字符串}可能出现三个状态
- q0 未遇到 01,且最后一个不为 0,δ(q0,0)=q1,δ(q0,1)=q0
- q1 未遇到 01,且最后一个为 0,δ(q1,0)=q1,δ(q0,1)=q2
- q2 已遇到 01,可接受任意 0 或 1,δ(q2,0)=q2,δ(q2,1)=q2
其 DFA 表达式: A=({q0,q1,q2},{0,1},δ,q0,q2)
对 DFA 的细节描述难以阅读,通常使用两种方式来更好的描述
状态转移图
graph LR; begin( )-->|Start|q0 q0-->|0|q1 q0-->|1|q0 q1-->|0|q1 q1-->|1|q2 q2-->|0,1|q2 style begin color:white,fill:white,stroke:white
状态转移表,一个状态和输入组成的δ的表
0 1 →q0 q1 q0 q1 q1 q2 q2 q2 q2
使用扩展转移函数,对一个串 0011 进行计算
δ^(q0,ε)=q0
δ^(q0,0)=δ(δ^(q0,ε),0)=δ(q0,0)=q1
δ^(q0,00)=δ(δ^(q0,0),0)=δ(q1,0)=q1
δ^(q0,001)=δ(δ^(q0,00),1)=δ(q1,1)=q2
δ^(q0,0011)=δ(δ^(q0,001),1)=δ(q2,1)=q2
这种方式的处理,非常适合我们用递归函数进行计算。
非确定型有穷自动机(NFA)
与确定型有穷自动机类似,包含若干个状态,若干个输入符,状态转移函数,终止状态。唯一不同的是,δ的结果可能是零个、一个、或多个状态的集合。
一个有穷状态集合,记做Q,
初始状态记做q0,
终止状态记做F
一个有穷输入集合,记做Σ
状态转移函数,记做δ,以一个状态q和一个输入符号a作为参数,返回一个状态p。δ(q,a)={p1,p2,⋯,pk};{p1,p2,⋯,pk}∈Q
- 扩展状态转移函数,记做δ^,以一个状态q和一个输入串ω作为参数,返回一个状态p。
- δ^(q,ε)=q
- 假定ω=xa,δ^(q,x)={p1,p2,⋯,pk},那么δ^(q,ω)=i=1⋃kδ(pi,a)={r1,r2,⋯,rm}
对于 NFA,可以定义为 L(A)={ω∣δ^(q0,ω)∩F}=∅
例如,{{0,1}∣以 01 结尾}
其状态转移图如下
graph LR; begin( )-->|Start|q0 q0-->|0|q1 q0-->|0,1|q0 q1-->|1|q2 style begin color:white,fill:white,stroke:white
当接受到 0 时,NFA 会猜测最后的 01 已经开始了,一条弧线从q0指向q1,我们可以看到 0 有两条弧线,另一条指向q0。NFA 会同时走这两条线。当在q1状态时,它会检查下一个符号是否为 1,如果是则会进入状态q2。当在q2状态时,如还有其他输入,这条路线就终结掉了。
00101 的处理过程如下
其状态转移表如下
0 | 1 | |
---|---|---|
→q0 | {q0,q1} | {q0} |
q1 | ∅ | {q2} |
* q2 | ∅ | ∅ |
使用扩展转移函数的处理过程如下
δ^(q0,ε)={q0}
δ^(q0,0)=δ(q0,0)={q0,q1}
δ^(q0,00)=δ(q0,0)∪δ(q1,0)={q0}∪{q2}={q0,q1}
δ^(q0,001)=δ(q0,1)∪δ(q1,1)={q0}∪{q2}={q0,q2}
δ^(q0,0010)=δ(q0,0)∪δ(q2,0)={q0,q1}∪∅={q0,q1}
δ^(q0,00101)=δ(q0,1)∪δ(q1,1)={q0}∪{q2}={q0,q2}
NFA 与 DFA 的转换
通常来说,构造 NFA 比构造 DFA 更容易,每一个用 NFA 描述的语言也能用 DFA 来描述。这个可以用子集构造
来证明。
子集构造从一个 NFA N=(Qn,Σ,δn,q0,Fn)开始,转换为 D=(Qd,Σ,δd,{q0},Fd)
- 两个自动机的输入字母表是相同的
- D 的起始状态为仅包含 N 的起始状态的长度为 1 的集合
- Qd是Qn子集的集合,即幂集合。假如Qn有 n 个状态,那么Qd有2n状态,通常不是所有的状态都是从q0可达的,这些状态可以丢弃,所以实际上Qd的状态要远远小于2n
- FD所有满足S∩Fn=∅的Qn的子集的集合 S,也就是说,FD是QN状态子集中至少包含一个FN的集合。
- 对于S⊆Qn的集合 S 中每个状态来说,其对应的每个属于Σ的输入符号a的转移函数为: δD(S,a)=p∈S⋃δN(p,a)
示例,我们以接受所有 01 结尾的串的 NFA 向 DFA 转换,由于Qn={q0,q1,q2},所以子集构造产生一个带有23=8(并非所有状态都是有意义的)种状态的 DFA
0 | 1 | |
---|---|---|
∅ | ∅ | ∅ |
→{q0} | {q0,q1} | {q0} |
{q1} | ∅ | {q2} |
∗{q2} | ∅ | ∅ |
{q0,q1} | {q0,q1} | {q0,q2} |
∗{q0,q2} | {q0,q1} | {q0} |
∗{q1,q2} | ∅ | {q2} |
∗{q0,q1,q2} | {q0,q1} | {q0,q2} |
δD({q0},0)=δN(q0,0)={q0,q1}
δD({q0},1)=δN(q0,1)={q0}
- δD({q0,q1},0)=δN(q0,0)∪δN(q1,0)={q0,q1}∪∅={q0,q1}
δD({q0,q1},1)=δN(q0,1)∪δN(q1,1)={q0}∪{q2}={q0,q2}
0 | 1 | |
---|---|---|
A | A | A |
→B | E | B |
C | A | D |
∗D | A | A |
E | E | F |
∗F | E | B |
∗G | A | D |
∗H | E | F |
从状态 B 开始,只能到达状态 B、E 和 F。其余五种状态都是从初始状态不可达的,也可以不出现在表中。如果向下面这样在子集合中执行惰性求值
,通常就能避免以指数时间步骤为每个状态子集合构造转移表项目。一般情况下,我们可以从初始状态 A 开始计算可到达状态,若有新的可达状态,我们继续计算,直到没有新的可达状态。
graph LR; begin( )-->|Start|B B-->|0|E B-->|1|B E-->|0|E E-->|1|F F-->|0|E F-->|1|B style begin color:white,fill:white,stroke:white
我们需要证明这个子集构造是正确的。
在读入输入符号序列ω后,所构造的 DFA 处于这样一个状态,这个状态时 NFA 在读ω后所处的状态的集合。由于 DFA 的接受状态是至少包含一个 NFA 接受状态Fn的集合,因为包含 NFA 的可接受状态Fn,因此这个集合也是被 NFA 接受的。于是我们可以得出结论,这个 DFA 和 NFA 接受完全相同的语言。
我们来证明 若D=(Qd,Σ,δd,{q0},Fd)是从N=(Qn,Σ,δn,q0,Fn)子集构造出来的,那么L(D)=L(N),这也就是证明,对于输入字母表ω ,δ^({q0},ω)=δ^(q0,ω)
当∣ω∣=0,即ω=ε,根据δ^的定义,δ^({q0},ε) δ^(q0,ε)的结果都为{q0}
假定∣ω∣=n+1 ,ω=xa,∣x∣=n,δ^({q0},x)=δ^(q0,x)成立,两个集合状态 N 为{p1,p2,⋯,pk}。
那么对于 NFA:
δ^N(q0,ω)=i=1⋃kδN(pi,a)
通过子集构造的定义我们可以得出
δD({p1,p2,⋯,pk},a)=i=1⋃kδN(pi,a)
又因 δD({q0},x)={p1,p2,⋯,pk}
δD^({q0},w)=δD(δD^({q0},x),a)=δD({p1,p2,⋯,pk},a)=i=1⋃kδN(pi,a)
文本搜索
识别 web 和 ebay 出现
通过 NFA 来实现
对终止状态[4,8]
增加了回到1
的转移,以支持 web 和 ebay 出现在中间位置。
1 | //识别单词web和ebay出现的NFA |
通过子集构造的方式来转换为 DFA,省略[4,8]
的1
出口
1 | //状态转移函数 |
上述输出结果为,与例图完美对应
1 | { |
互斥量与条件变量
凡涉及到内存共享、共享文件以及共享任何资源的情况都会引起竞争条件。我们需要阻止多个进程同时读写共享数据,换言之,我们需要的是互斥,即已某种手段确定当一个进程在使用一个共享变量或文件时,其他进程不能做同样的操作。
避免竞争条件的问题也可以用一种抽象的方式进行描述。一个进程的一部分时间做内部计算或另外一些不会引起竞争条件的操作,另一部分时间可能需要访问共享内存或共享文件,或执行另外一些可能导致竞争条件的操作。我们把对共享内存进行访问的程序片段称作临界区。如果我们能够适当的安排,使得两个进程不可能同时处于临界区,就能够避免竞争条件。
一个好的解决方案,需要满足以下四个条件:
- 任何两个进程不能同时出去其临界区
- 不应对 CPU 的速度和数量做任何假设
- 临界区外运行的进程不得阻塞其他进程
- 不得使进程无期限等待进入临界区
原子操作:是指一组相关联的操作要么都不间断地执行,要么都不执行。 信号量:使用一个整型变量来累计唤醒次数。对一信号量执行 down 操作,则是检查其值是否大于 0,若是则减一,否则进程将睡眠,检测数值、修改变量值以及可能的睡眠操作需要确保是原子操作。up 操作对信号量的值增 1。如果一个或多个进程在该信号上睡眠,无法完成一个先前的 down 操作,则有系统选择其中的一个并允许该进程完成它的 down 操作。于是,对一个有进程在其上睡眠的信号量执行了一次 up 操作之后,该信号量的值仍旧是 0,但在其上睡眠的进程却少了一个。信号量的值增 1 和唤醒其他进程操作也是不可分割的,是原子操作。 互斥量:一个可以处于两态之一的变量,解锁和加锁
wireshark
wireshark 显示过滤器表达式是根据协议
+.
+属性
来判断的。
基础使用的官方文档
全部的协议属性列表见官方文档
各种协议的wiki 地址
过滤器表达式基本语法
english | c-like | desc |
---|---|---|
eq | == | ip.src=10.0.0.5 |
ne | != | ip.src!=10.0.0.5 |
gt | > | frame.len>10 |
lt | < | frame.len<10 |
ge | >= | frame.len>=10 |
le | <= | frame.len<=10 |
contains | sip.TO contains "a123" | |
matches | ~ | 使用正则,http.host matches "acme.(org|com)" |
bitewire_and | & | 二进制 and 运算结果不为 0, tcp.flags & 0x02 |
数字值可以使用十进制、八进制、十六进制
1 | ip.len le 1500 |
布尔类型的值可以使用1
(true),0
(false)
网络地址可以使用:
,.
,-
分割
1 | eth.dst == ff:ff:ff:ff:ff:ff |
可以比较字符串
1 | http.request.uri == "https://www.wireshark.org/" |
过滤器表达式组合语法
english | c-like | desc |
---|---|---|
and | && | ip.src==10.0.0.5 and tcp.flags.fin |
or | || | ip.src==10.0.0.5 or ip.src=10.0.0.6 |
xor | ^^ | tr.src==10.0.0.5 xor tr.dst==10.0.0.5 |
not | ! | not tcp |
[...] | 子序列比较 | |
in | 是否在集合中 |
一些示例
1 | eth.src[0:3] == 00:00:83 |
mrcp
概述
MRCP 是一个标准、统一、可扩展的媒体资源控制协议,主要用来语音识别、TTS 合成、录音、 声纹识别(确认是否为某一类群体),声纹认证。MRCP 是一个框架,同时也是一个协议。该框架定义了它的网络基本组件及相互关系。它使用 SIP 协议来控制会话管理,使用 RTP 进行媒体流传输。它的协议定义了它如何控制媒体资源的过程。 MRCP 是基于文本的协议,与 HTTP、SIP 的结构类似
结构
MRCP 允许客户端请求和控制网路上指定的媒体服务,使用媒体服务的客户端通常包括:
- IVR 平台
- 高级媒体网关
- 基于 IP 的媒体服务端
客户端一般需要有
- 定位和发现媒体资源服务端
- 与媒体资源服务建立消息 channel
- 远程控制媒体资源
MRCP 通过 SIP URI 来确认媒体资源的 IP 地址,当找到合适的媒体资源服务,SIP 会在客户端和服务端建立两个消息管道,一个用来发送和接收音频流(又称媒体会话),另一个是客户端用来发送控制请求到媒体资源服务端、服务端响应并返回事件给客户端(又称控制会话,MRCP 协议建立在控制会话上)
媒体资源类型
类型 | 描述 |
---|---|
basicsynth | 基础 tts,仅支持 SSML 标准的一些子集 |
speechsynth | tts ,支持完整的 SSML |
dtmfrecog | DTMF 识别 |
sppechrecog | 语音识别 |
recorder | 录音 |
speakverify | 语音验证 |
报文
MRCP 消息分为三种类型,request、response、event。服务端接收到从客户端发送的 request 请求后,解析执行后返回一个 response 响应。response 包含一个三位数字的响应码(与 HTTP 的类似),另外还有包含当前 request 的状态(PENDING
,IN-PROGRESS
,COMPLETE
)
- PENDING 表明客户端的 request 请求已经到达服务器,并添加到服务器的 FIFO 的处理队列中。
- IN-PROGRESS 表明客户端的 request 请求处于执行过程中。
- COMPLETE 表明客户端的 request 请求已经完成,没有后续消息
PENDING 和 IN-PROGRESS 都表明请求还未结束,还在等待其他 event 事件。通过 对特定 events 的响应服务端与客户端进行数据交互。
例如一个标准的 tts 播报过程
1 | client TTS |
其请求报文大致如下
1 | MRCP/2.0 380 SPEAK 14321 |
MRCP 的请求报文大体上可以分为三个部分
- 请求行,每个字段以空格分隔,以换行(CRLF)结束,每个字段分别表示
MRCP版本
、请求报文长度
、请求事件
、请求id
- 请求头
header-name:header-value CRLF
格式,请求头结尾一定会有一个空行 - 请求报文
1 | MRCP/2.0 119 14321 200 IN-PROGRESS |
MRCP 的返回报文类似请求报文,返回行每个字段分别代表MRCP版本
、返回报文长度
、请求id
、返回码
、当前request状态
。 IN-PROGRESS 状态一般表示 audio 流正在向客户端发送。
1 | MRCP/2.0 157 SPEAK-COMPLETE 14321 COMPLETE |
当服务端执行到 COMPLETE 状态时,表明不在接收指定的 request 请求,则会返回一个包含 COMPLETE 的响应报文。此时返回行每个字段分别代表MRCP版本
、返回事件
、返回报文长度
、请求id
、当前request状态
。
1 | MRCP/2.0 111 START-OF-INPUT 32121 IN-PROGRESS |
其他有返回事件的返回行格式也是这样。
SIP in MRCP
MRCP 使用 SIP 协议在客户端与服务端进行通信。标准的三步INVITE-200 OK-ACK
握手用来建立 media session 和 control session 连接。BYE-200 OK
用来关闭连接。使用SDP offer/answer
模型来进行协商
以下是 control channel 的一些 SDP 片段
1 | c=IN IP4 10.0.0.1 |
SDP 是客户端请求服务端报文体重的一部分内容,比如包含在 INVITE 消息中。上述的例子,MRCP 向媒体服务器申请一个语音合成服务。
- c 行表示 IP 地址。
- m control session 包含一个或多个 m 行,其第一个字段为 application(对于 media session ,其值应为 audio)。每个 m 行代表一个 media 资源。
TCP/MRCPv2
表示使用 TCP 进行数据传输。端口号仅为0
或9
,0
表示禁用,9
表示暂未确定,将有服务端来确定。m 行下面的 a 行是对当前 m 行的属性进行设定。 - setup 客户端总是初始化为 active,服务端总是为 passive
- connection 是否新建 TCP 连接(new),还是使用 已经存在的 TCP 连接(existing)
- resource 请求的媒体资源类型
- cmid control channel 到 media 流的一个标识,多个 contrl channel 可以使用同一个 cmid,这标识同一通会话可以多次使用同一个 media 流。
以下是 SIP 200 Ok 的响应报文的一些片段
1 | c=IN IP4 10.0.0.22 |
- m 这里就指定了将要使用的端口号
示例:
一个标准的单媒体资源请求服务
INVITE
1 | INVITE sip:mrcpv2@example.com SIP/2.0 |
- 第一个 m 行建立 media 流
- 第二个 m 行连接 control session
200 OK
1 | SIP/2.0 200 OK |
ACK
1 | ACK sip:mrcpv2@host100.example.com SIP/2.0 |
添加和删除 media 资源
INVITE
1 | INVITE sip:mrcpv2@host100.example.com SIP/2.0 |
- 新申请一个 recorder 服务。SDP 全量更新 control session 和 media session 的,所以之前建立的 control session 是需要体现在当前的 SDP 内容中,当我们使用 recorder 服务,media 资源就需要保持双向连接(sendrecv),control session 的 connection 使用了现有的 control session 的 TCP 连接
200 OK
1 | SIP/2.0 200 OK |
ACK
1 | ACK sip:mrcpv2@host100.example.com SIP/2.0 |
INVITE
1 | INVITE sip:mrcpv2@host100.example.com SIP/2.0 |
- application 后的端口被设置 0,表示禁用
200 OK
1 | SIP/2.0 200 OK |
ACK
1 | ACK sip:mrcpv2@host100.example.com SIP/2.0 |
查询服务端支持的功能
OPTIONS
1 | OPTIONS sip:mrcpv2@example.com SIP/2.0 |
200 OK
1 | SIP/2.0 200 OK |
control session
MRCP 消息有三种:request、response、event。每种消息都含有一个起始行(各种类型的消息起始行有差异),一个或多个消息头,以 CRLF 间隔,格式为header-name:header-value
,消息体报文一个有消息头定义长度和格式的报文。
request
起始行格式:MRCP/2.0 message-length method-name requestid
- 协议版本
- 整个报文的长度,包括起始行
- 请求的服务
- SPEACK 语音合成服务
- RECOGNIZE 语音识别服务
- 32 位自增 int 来区分不同的请求
示例
1 | MRCP/2.0 267 SPEAK 10000 |
Channel-Identifier 是所有 request 都必须的,与 SDP channel 的值是相同的
response
起始行格式为:MRCP/2.0 message-length request-id status-code request-state
- 协议版本号
- 整体报文长度,包括起始行
- 请求 ID
- 类似 HTTP 状态码
- 请求状态,PENDING、IN-PROGRESS、COMPLETE
示例
1 | MRCP/2.0 79 10000 200 IN-PROGRESS Channel-Identifier: 43b9ae17@speechsynth |
event
起始行格式:MRCP/2.0 message-length event-name request-id request-state
- 协议版本号
- 整体报文长度,包括起始行
- 事件
- 请求 ID
- 请求状态,PENDING、IN-PROGRESS、COMPLETE
不同的媒体资源支持不同的事件
示例
1 | MRCP/2.0 109 START-OF-INPUT 10000 IN-PROGRESS |
highlightjs
高亮代码块,官网文档
1 | npm install -D -S highlight.js |
1 | import hljs from "highlight.js"; |
mockjs
使用 mockjs 来模拟 api 接口返回报文
1 | #安装 |
在项目目录下新建mock/index.js
,并在main.js
中引入
1 | // main.js |
mockjs 代理地址支持正则表达式
1 | Mock.mock(/\/api\/.*/, { foo: "bar" }); |
mockjs 支持使用一个函数来返回模拟数据
1 | //options中包含请求url,type,请求body等信息 |
axios
安装
1 | npm install --save axios |
示例
基础示例
1 | const axios = require("axios"); |
其参数值可以为
- data 请求数据
- method 请求访问
- url 请求地址
post 请求
1 | const axios = require("axios"); |
我们可以为请求设定一个具有指定配置项的实例
1 | const instance = axios.create({ |
然后这个 instance 就可以直接调用下述方法
axios#request(config)
axios#get(url[, config])
axios#delete(url[, config])
axios#head(url[, config])
axios#options(url[, config])
axios#post(url[, data[, config]])
axios#put(url[, data[, config]])
axios#patch(url[, data[, config]])
axios#getUri([config])
vue-cli2 中 axios 全局配置
1 | //axios/index.js |
最后在 main.js 里面引入
1 | import VueAxios from "vue-axios"; // 报错的话则npm安装依赖 |
Webpack-dev-server 的 proxy 用法
在开发环境中,可以将 axios 的请求通过 proxy 进行转发
最简单的用法示例
1
2
3
4
5
6
7mmodule.exports = {
devServer: {
proxy: {
"/api": "http://localhost:3000",
},
},
};请求到
/api/xxx
现在会被代理到请求http://localhost:3000/api/xxx
如果想要代理多个路径到同一个地址,可以使用一个或多个具有 context 属性的对象构成的数组
1
2
3
4
5
6
7
8
9
10mmodule.exports = {
devServer: {
proxy: [
{
context: ["/api", "/auth"],
target: "http://localhost:3000",
},
],
},
};如果你不想传递
/api
,可以重写路径1
2
3
4
5
6
7
8
9
10
11mmodule.exports = {
devServer: {
proxy: [
'/api':{
target: "http://localhost:3000",
pathRewrite:{'^/api',''},//原请求路径将被正则替换后加入到target地址后
secure:false,//默认情况下,不接受https,设置为false即可
},
],
},
};请求到
/api/xxx
现在会被代理到请求http://localhost:3000/xxx
使用 bypass 选项通过函数判断是否需要绕过代理,返回 false 或路径来跳过代理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16mmodule.exports = {
devServer: {
proxy: [
'/api':{
target: "http://localhost:3000",
bypass:function(req,res,proxyOptions){
if(req.header.accept.indexOfI('html')!== -1){
console.log('skipping proxy from browser request.')
return '/index.html';//return false
}
}
},
],
},
};
代理过程可能遇到的一些问题,对于有些 target 地址,可能需要登录,从而将页面重定向(302)到登录页面,那么我们就需要保证请求时带上对应的 token
提交 form 表单数据
1 | var bodyFormData = new FormData(); |
webpack
快速入门
1 | mkdir webpack-demo |
新增两个文件
1 | webpack-demo |
1 | <!DOCTYPE html> |
1 | import _ from "lodash"; |
执行打包命令
1 | # npx 类似package.json中的scripts,可直接运行 |
也可以在 package.json 中新增 script,
1 | { |
那么我们可以直接使用如下命令进行打包
1 | npm run build |
当不指定配置文件时,就使用了默认的配置(npx webpack --config webpack.config.js
)
1 | const path = require("path"); |
如需要指定配置文件,可在 package.json 中指定相关配置文件
命令成功执行后,就会将所有文件打包到 dist 目录,我们打开 index.html,如果一切正常的话,我们可以在浏览器上看到Hello webpack
字样
我们可以使用npx webpack serve
启动 web 服务,默认会在 8080 端口上启动
1 | npm i webpack-dev-server -S -D |
加载其他资源文件
webpack.config.js 中的配置,在 module 对象的 rules 属性中可以指定一系列的 loaders,每一个 loader 都必须包含 test 和 use 两个选项,这段配置的意思是说,当 webpack 编译过程中遇到 require()或 import 语句导入一个后缀名为.css 的文件是,先将它通过 css-loader 转换,在通过 style-loader 转换,然后继续打包。use 选项的值可以是数组或字符串,如果是数组,它的编译顺序是从后往前
修改一下项目结构
修改dist/index.html
1 | <!doctype html> |
修改webpack.config.js
1 | const path = require('path'); |
导入 css 文件
1 | npm install --save-dev style-loader css-loader |
修改webpack.config.js
1 | const path = require('path'); |
新增src/style
文件
1 | .hello { |
修改src/index.js
1 | import _ from 'lodash'; |
1 | npx webpack serve --config webpack.config.js |
导入图片
1 | npm install --save-dev file-loader |
1 | const path = require('path') |
新增图片src/icon.png
修改src/index.js
1 | import _ from "lodash"; |
修改src/style.css
1 | .hello { |
导入数据
1 | npm install --save-dev csv-loader xml-loader |
webpack.config.js
1 | const path = require('path'); |
增加一些数据
src/data.xml
1 | <?xml version="1.0" encoding="UTF-8"?> |
src/data.csv
1 | to,from,heading,body |
src/index.js
1 | import _ from 'lodash'; |
类似的数据文件还可以使用
src/data.toml
1 | title = "TOML Example" |
src/data.yaml
1 | title: YAML Example |
src/data.json5
1 | { |
需要安装相关插件
1 | npm install toml yamljs json5 --save-dev |
输出管理
通过 html-webpack-plugin
自动生成 index.html,并引入相关资源 通过 clean-webpack-plugin
自动清理 dist 目录
1 | npm install --save-dev html-webpack-plugin |
示例
src/print.js
1 | export default function printMe() { |
src/index.js
1 | import _ from 'lodash'; |
webpack.config.js
1 | const path = require('path'); |
统一输出 css 文件
1 | npm install --save-dev mini-css-extract-plugin |
webpack.config.js
1 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') |
重新编译后在 dist 目录下生成了 main.css,且 index.html 中引入了 main.css
1 | npm run build |
vue
双向绑定
通过构造函数 Vue 就可以创建一个 Vue 的根示例,并启动 Vue 应用,Vue 实例需要挂载一个 DOM 元素,它可以是 HTMLElement,也可以是 CSS 选择器
1 | <!DOCTYPE html> |
上述 Vue 实例的构造器中的成员变量可以通过app.$el
,app.$data
的方式去访问,对于 data 数据,可以直接使用app.message
去访问。在 dom 上以 {{}} 包含的内容与 app 进行双向绑定。当我们在 console 控制台修改app.messege
的值时,页面也随着刷新 message 的内容
Vue 在观察到数据变化时并不是直接更新 DOM,而是开启一个队列,并缓冲同一事件循环中发生的所有数据改变。在缓冲时会去除重复数据,从而避免不必要的计算和 DOM 操作。然后在下一个事件循环 tick 中,Vue 刷新队列并执行实际(已去重的)工作
1 | <div id="app"> |
生命周期
每个 Vue 示例创建时,都会经历一系列的初始化过程,同时也会调用相应的生命周期钩子。比较常用的生命周期有
- created 示例创建完成后调用,次阶段完成了数据的观测等,但尚未挂载,$el 还不可用,需要初始化一些处理数据时会比较有用
- mounted el 挂载到实例上后调用
- beforeDestroy 实例销毁前调用。主要是解绑一些使用 addEventListener 监听的事件等。
这些钩子与 el 和 data 类似,作为选项写入 Vue 实例
1 | var app = new Vue({ |
手动挂载实例
Vue 提供了 Vue.extend 和$mount两个方法将 vue 实例挂载到一个 dom 上,即如果 Vue 实例在实例化时它没有收到 el 选项,它就处于“未挂载”状态,没有关联的 DOM 元素。可以使用$mount()手动挂载一个未挂载的实例。这个方法返回实例自身,因而可以链式调用其他实例方法。
1 | <div id="app"> |
插值与表达式
{{}} 使用双大括号(Mustache 语法),双向绑定的数据,{{}} 内部可以使用 js 运算
1
2
3
4
5
6
7
8
9
10
11
12
13
14<div id="app">
{{number/10}}
{{isOK?'确定':'取消'}}
{{text.split(',').reverse().join('|')}}
</div>
<script>
new Vue({
el: "#app",
data: {
number:100,
isOK:false,
text:'123,456'
},
});{{}} 支持使用管道符
|
来对数据进行过滤,过滤的规则是自定义了,通过选项filters
来设置,过滤器可以接受参数,过滤器可以串联1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19<div id="app">
{{text|split}}
<!-- 串联-->
{{text|split|split}}
<!-- arg1 arg2 分别为过滤器的第二个 第三个参数-->
{{text|split('arg1','arg2')}}
</div>
<script>
new Vue({
el: "#app",
data: {
text:'123,456'
},
filters:{
split:function(value){
return value.split(',').join('#');
}
}
});computed 通过 Vue 选项 computed 的计算属性获取数据,每一个计算属性都包含一个 getter 和 setter,默认只使用 getter,当 getter 中所依赖的任何数据变化时,当前 getter 会被自动触发。相对于直接使用 methods,computed 只有在源数据更新后才会被重新调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<div id="app">
{{text}}
</div>
<script>
new Vue({
el: "#app",
data: {
number:100
},
computed:{
text:function(){
return '$'+this.number
}
}
});当我们对
app.number
进行赋值时,页面上 text 的值也会跟着变化,但是对 text 的值进行改变,number
的值不会变化, 当我们指定其 setter 就可以了1
2
3
4
5
6
7
8computed:{
set: function (value) {
this.number = parseInt(value.substring(1))
},
get: function () {
return '$' + this.number
}
}
指令
v-bind
动态更新 html 元素上的属性,可以使用语法糖:
1
2
3
4
5
6
7
8
9
10<a v-bind:href="url"> 链接</a>
<!--语法糖-->
<a :href="url"> 链接</a>
<script>
new Vue({
data: {
url: "http://example.com",
},
});
</script>v-for
迭代的数据也是双向绑定的,对数组或对象进行操作时会触发渲染遍历数组,
需要注意的是直接通过索引去设置值是无法被 Vue 检测到的,也不会触发视图更新 1
2
3
4
5
6
7
8
9
10
11<ul>
<li v-for="book in books">{{ book.name}}</li>
<li v-for="(book,index) in books">{{ index}} - {{book.name}}</li>
</ul>
<script>
new Vue({
data: {
books: [{ name: "v1" }, { name: "v2" }],
},
});
</script>遍历对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14<ul>
<li v-for="(value,key,index) in user">{{ index}} - {{key}}:{{value}}</li>
</ul>
<script>
new Vue({
data: {
user: {
name: "hello",
gender: "man",
age: 23,
},
},
});
</script>迭代整数
1
<li v-for="n in 10">{{ n}}</li>
若我们需要操作数组,可使用数组自带的方法区操作, 例如
- pop 删除末项
- push 添加一项
- shift 删除第一项
- unshift 添加第一项
- splice 截取/修改/删除数组元素
- sort 对数组排序
- reverse 取反
也可以通过直接修改数组的引用 或使用
Vue.set
、this.$set
v-html
输出 HTML1
2
3
4
5
6
7
8
9<span v-html="link">}</span>
<script>
new Vue({
el: "#app",
data: {
link: '<a href="#">一个链接</a>',
},
});
</script>v-if
、v-else-if
(必须紧跟 v-if)、v-else
(b 必须紧跟 v-if 或 v-else-if) 表达式为正,当前元素、组件所有子节点将被渲染,否则全部移除1
2
3<p v-if="status ===1">当status为1时显示这行</p>
<p v-else-if="status ===2">当status为2时显示这行</p>
<p v-else>否则显示这行</p>v-model
绑定表单数据,也是双向绑定的,对于中文输入法,只有在回车后才会触发更新1
2
3
4
5
6
7
8
9
10
11
12
13<input v-model="input" />
<p style="color: red;">{{input}}</p>
<!-- 使用@input可以在输入中文时实时刷新-->
<input @input="inputHandle" />
<script>
//...
methods:{
inputHandle:function(e){
this.input = e.target.value
}
}
</script>v-model 可使用修饰符控制数据同步的机制
- .lazy v-model 默认在 input 事件中同步输入框的数据,使用
.lazy
会转变在 change 事件中同步 - .number 将输入转换为 Number 类型
- .trim 过滤首位空格
示例
1
2
3<input v-model.lazy="value" />
<input v-model.number="value" />
<input v-model.trim="value" />- .lazy v-model 默认在 input 事件中同步输入框的数据,使用
v-on
绑定事件监听器,可以使用语法糖:
,可以用.native
修饰符表示监听的是一个原生事件。1
2
3
4
5
6
7
8
9
10
11
12<a v-on:click="log"> 链接</a>
<!--语法糖-->
<a @click="log"> 链接</a>
<script>
new Vue({
methods: {
log: function (event) {
console.log(event); //event为触发的事件
},
},
});
</script>v-once
值渲染一次,包括元素和组件的所有子节点(包括 v 指令等)。首次渲染后,不再随数据的变化重新渲染v-pre
跳过编译1
<span v-pre> {{这里面不会被编译}}</span>
v-show
进行 CSS 属性切换,是否可见
自定义指令
例如注册一个 v-focus 的指令,用于在<input>
元素初始化时自动获取焦点
1 | //全局注册 |
自定义指令由几个钩子函数组成的,每个都是可选的
- bind 只调用一次,指令在第一次绑定元素时调用,用这个钩子函数可以定义一个绑定时指定一次的初始化动作
- inserted 被绑定元素插入父节点时调用
- update 被绑定元素所在的模板更新时调用,而不论绑定值是否变化,通过比较更新前后的绑定值,可以忽略不必要的模板更新
- componentUpdated 被绑定元素所在模板完成一次更新周期时调用
- unbind 只调用一次,指令与元素解绑时调用
每个钩子函数都一次有几个参数可用
- el 指定绑定的元素,可以用来直接操作 DOM
binding 一个对象,包含以下属性
- name 指令名,不包含 v-前缀
- value 指令绑定的值,例如
v-my-directive="1+1"
,value 的值就是 2 - oldValue 指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用,无论值是否改变都可用
- expression 绑定值的字符串形式,例如
v-my-directive="1+1"
,expression 的值就是1+1
- arg 传给指令的参数,例如
v-my-directive:foo
,arg 的值是 foo - modifiers 一个包含修饰符的对象,例如
v-my-directive.foo.bar
,修饰符对象 modifiers 的值是{foo:true,bar:ture}
- vnode Vue 编译生成的虚拟节点
oldVnode 上一个虚拟节点仅在 update 和 componentUpdated 钩子中可用 根据需求在不同的钩子函数内完成逻辑代码,例如 v-focus,我们希望在元素插入父节点时调用,那用到的最好是 inserted。示例如下
1 | <div id="app"> |
自定义指令也可以传入一个 JavaScript 对象字面量
1 | <input v-focus="{msg:'hello',count:100}" /> |
自定义插件
注册插件
1 | // 在Vue内部实际上会调用`MyPlugin.install(Vue)` |
自定义插件
1 | MyPlugin.install = function (Vue, options) { |
绑定 class 的几种方式
v-bind 中的表达式最终会被解析为字符串,Vue 对 v-bind 增强了一些功能,提供了多重语法
对象语法
1
2
3
4
5
6
7
8
9<div :class="{'class1':isActive1,'class2':isActive2}"></div>
<script>
new Vue({
data: {
isActive1: true,
isActive2: false,
},
});
</script>上述渲染后的页面元素为
<div class="class1"></div>
也可使用 computed
1
2
3
4
5
6
7
8
9
10
11
12
13<div :class="classes"></div>
<script>
new Vue({
computed: {
classes: function () {
return {
class1: this.isActive1,
class2: this.isActive2,
};
},
},
});
</script>也可以指定绑定到一个对象上
数组语法
1
2
3
4
5
6
7
8
9<div :class="[c1,c2]"></div>
<script>
new Vue({
data: {
c1: 'class1'
c2: 'class2'
}
});
</script>
事件
修饰符
修饰符
. stop . prevent . capture . self . once
1 | <!-- 阻止单击事件冒泡--> |
按键
keyCode
- .enter
- .tab
- .delete
- .esc
- .space
- .up
- .down
- dblclick 双击
按键可以组合,或和鼠标一起配合使用
1 | <!-- 按下shift时点击--> |
$emit
使用$emit
来触发自定义事件,使用$on
来监听子组件的事件。
例如
1 | <div id="app"> |
v-model
语法糖,v-model
绑定的是一个数据,接收一个 value 属性,,在其有新的 value 时会触发 input 事件。反之亦然。 等价于<input :value='someVariable' @input='someHandle'>
所以上述事例也可以写成如下
1 | <div id="app"> |
不同组件互相通信
建议通过一个空的 vue 实例作为中央事件总线来实现。
1 | <div id="app"> |
slot
当在子组件内使用特殊的<slot>
元素就可以为这个子组件开启一个slot
(插槽),在父组件的模板里,插入在子组件模板标签内的所有内容将替代子组件内的<slot>
标签以及它的内容,slot
可以指定一个 name,其会加载父组件的子组件模板标签内中属性slot
值相同的内容。没有指定slot
值的标签都作为默认匿名 slot
1 | <div id="app"> |
scope_name.msg
的语法访问子组件插槽的数据 msg
1 | <div id="app"> |
作用域插槽更具有代表性的用例是列表组件
1 | <div id="app"> |
我们可以使用$slots
访问默认作用域的 slot 渲染后的标签,$scopedSlots
访问所有作用域的标签
1 | <div id="app"> |
watch
监听 prop 或 data 的改变,当他们发生变化时触发 watch 配置的函数,此时对于节点以及渲染完成,
1 | <div id="app"> |
watch 对第一次赋值是不响应的,可通过设置immediate
来让其执行。watch 一般仅监听属性的重新赋值,对于对象属性的内部值的操作时不响应的,可通过deep
来使其生效
1 | <script> |
watch 可以监听 vuex 中存储的内容
1 | <script> |
表单
单选按钮
当那个单选按钮被选中时,picked 即为对应绑定的数据的值,同一个v-model
单选具有排斥性
1 | <div id="app"> |
组件
定义组件
组件最外层仅能有一个标签
1 | <div id="app"> |
异步组件
Vue 允许将组件定义为一个工厂函数,动态解析组件。工厂函数接收一个 resolve(component)回调,也可以使用 reject(reason)指示加载失败
1 | <div id="app"> |
嵌套组件
嵌套的组件需要定义在全局组件上,必须设定一个条件来限制递归数量
1 | <div id="app"> |
动态组件
Vue 提供了一个特殊的元素<component>
用来挂载不同的组件,使用is
特性来选择要挂载的组件。通常is
特性绑定的是组件的名称,组件也可以直接绑定对象。
1 | <div id="app"> |
X-templates
可以使用这种模板更好的属性模板代码
1 | <script src="vue.min.js"></script> |
使用 props 传递数据
在组件内可以通过声明 props 访问在父类标签中的属性
1 | <div id="app"> |
HTML 特性不区分大小写,当使用 DOM 模板时,驼峰命名的 props 名称要转换为短横分隔命名
1 | <li-div msg-text="来自父组件的数据"></li-div> |
父组件数据变化时传递给子组件默认是单向数据流,即父组件的数据更新会传递给子组件,但反过来不行。
示例:当我们输入内容时,子组件内容跟着变动,而父组件不变
1 | <div id="app"> |
在 JavaScript 中对象和数组式引用类型的,指向同一个内存空间,所以 props 中是对象和数组式,在子组件内改变是会影响符组件的。
示例:当我们输入内容时,子组件内容跟着变动,而父组件也会跟着变动
1 | <div id="app"> |
数据验证
对于组件的 props,我们可以对其属性进行数据验证,
1 | new Vue({ |
验证的 type 可以是
- String
- Number
- Boolean
- Object
- Array
- Function
默认情况下含有静态值(非双向绑定的数据)属性的类型为 String,当我们需要自动推断类型时,可以使用v-bind
1 | <div id="app"> |
type 也可以是一个自定义构造器,使用 instanceof 检测 当 prop 验证失败时,在
mixins
mixins: 是一种分发 Vue 组件中可复用功能的非常灵活的方式。混入对象可以包含任意组件选项。当组件使用 mixins 时,所有 mixins 的选项将被组合到该组件本身的选项,类似预编译过程。
- 组件的数据不共享,仅拷贝一份,数组和对象采用的是深拷贝,而非拷贝引用。
- 对于值为对象的选项如 methods,components 等,选项会被合并,键冲突的组件会覆盖混入对象的。
- 值为函数的选项,如 created,mounted 等,就会被合并调用,混合对象里的钩子函数在组件里的钩子函数之前调用
webpack 过程
使用.vue
单文件组件的构建模式,需要 webpack 并使用 vue-loader 对.vue 格式的文件进行处理。一个.vue 文件一般包括 3 部分,即<template>
,<script>
,<style>
例如:
1 | <template> |
示例中的 style 标签使用了 scoped 属性,表示当前的 CSS 只在这个组件有效,如果不加,那么 div 的样式会应用到整个项目。<style>
还可以结合 CSS 预编一起使用,比如使用 Less 处理可以写成<style lang='less'>
使用.vue 文件需要安装vue-loader
、vue-style-loader
等加载器并做配置。因为要使用 ES6 语法,还需要安装babel
和bable-loader
等加载器。使用 npm 逐个安装以下依赖
使用vue-cli
1 | # vue2版本对应的cli |
安装后,package.json 文件内容如下
1 | { |
使用npm run dev
即可启动一个 dev 服务
路由
定义
单页面应用指的是只有一个主页面,通过动态替换 DOM 内容并同步修改 url 地址,来模拟多页面应用的效果。切换页面的功能直接由前台脚本来完成,而不是后端渲染完毕后前端只负责显示。
前端路由就是一个前端不同页面的状态管理器,可以不向后台发送请求而直接通过前端技术实现多个页面效果。vue 中的 vue-router,react 的 react-router 均是对这种功能的实现。
实现原理可参考这里
vue-route
我们通过 vue-cli 初始化项目后,main.js 的内容如下
1 | import Vue from "vue"; |
首先页面会渲染成类似如下
1 | <html> |
这里的<app>
即是new Vue()
中可选参数 template 所生产的标签, 然后根据 components 中定义的 App 组件,将 app 渲染成 app.vue 中的内容
1 | <html> |
其中<router-view>
路由视图以用来挂载路由,默认挂载/
,那么我们根据main.js
中引入的 router,找到其对应的 js 文件。
src/router/index.js
1 | import Vue from "vue"; |
根据/
路径配置的组件,我们将<router-view>
渲染成 HelloWorld 组件的内容
1 | <html> |
路由跳转
router-link
是用来动态切换router-view
显示内容组件。
示例
我们新建一个组件src/components/link.vue
1 | <template> |
在 router 配置中配置 link.vue 的请求路径
1 | import Vue from "vue"; |
修改src/App.vue
1 | <template> |
当我们点击由router-link
渲染出来的<a>
链接,其下面的router-view
对应的元素将会被切换至对应的在路由中配置的组件
我们也可以使用 js 方法来实现跳转
1 | <template> |
$router
还有其他一些方法($router
为 main.js 中声明的 router 变量)
- replace 它不会向 history 添加新记录,而是替换掉当前的 history 记录。
- go 类似于 window.history.go(),在 history 记录中向前或后退多少步
路由配置相关
配置默认页面,可以在路由列表的最后新加一项,当访问路径不存在时,重定向到首页。
1 | export default new Router({ |
动态路由
路由列表的 path 也可以带参数,
1 | { |
当我们访问/link/1234
时,我们可以通过this.$route.params.id
的方式取出该参数值
1 | <template> |
路由钩子
router 提供了导航钩子 beforeEach 和 afterEach,它们会在路由即将改变前和改变后触发。
钩子函数有三个参数
- to 即将进入的目标的路由对象,在对应的目标页面我们可以用
this.$route
取出当前路由对象,也就是 to - from 当前导航即将离开的路由对象
- next 调用该方法后,才能进入下一个钩子。next 中还可以设置参数,。设置为 false,可以取消导航,设置为具体的路径可以导航到指定的页面。典型的用法是校验客户是否登录。
例如我们将页面的 title 实时更改为组件的名称
src/main/js
1 | import Vue from 'vue' |
命名视图
<router-view>
可指定 name,来使用指定 name 的组件,路由的路径,可以对应多个组件,默认使用的是 name 为 default 的组件
src/App.vue
1 | <template> |
router/index.js
1 | import Vue from "vue"; |
src/components/input1.vue
和 input2.vue,基本一致
1 | <template> |
当访问/
时,三个router-view
分别会被渲染为对应的组件。
二级路由
App.vue
1 | <html> |
Home.vue
1 | <div> |
Child
1 | <div>the child</div> |
1 | import Vue from "vue"; |
二级路由和命令视图结合
可参考这个demo
app.vue
1 | <template> |
router/index.js
,省略其他组件的代码
1 | import Vue from "vue"; |
其他
在 vue 代码中,我们可以使用
1 | export default { |
使用router-link
或使用$router.push
时,可以传递 obj,指定跳转到命令的路由中
1 | import Vue from "vue"; |
Vuex
1 | npm install --save-dev vuex |
新增文件src/store/index.js
1 | import Vue from "vue"; |
mutations 也可以接受一个包含 type 的对象
1 | mutations: { |
vuex 还有三个选项可以使用:getters
,actions
,modules
getters 一般用来对数据进行处理
1 | export default new Vuex.Store({ |
actions 类似 mutations,但是是异步执行的
modules,它用来将 store 分割到不同的模块中。
1 | const moduleA = { |