大狗哥传奇

  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 搜索

未命名

发表于 2020-12-02
.red { color: red; font-size: 16px; } .grey { color: rgb(138, 143, 138); } .mermaid { background-color: white !important; } code { font-family: "PT Mono", consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; overflow-wrap: break-word; word-wrap: break-word; padding: 2px 4px; color: #555; background: #eee; border-radius: 3px; font-size: 14px; } grey { color: rgb(138, 143, 138); } hi { font-family: "PT Mono", consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; overflow-wrap: break-word; word-wrap: break-word; padding: 2px 4px; background: rgb(255, 232, 25); border-radius: 3px; font-size: 14px; } hi3 { font-size: 14px; line-height: 20px; color: #333; margin: 200px 0; font-weight: bold; } red { color: red; padding: 2px 4px; } trs { font-family: "PT Mono", consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; overflow-wrap: break-word; word-wrap: break-word; /* padding: 2px 4px; */ color: #555; background: #eee; border-radius: 3px; font-size: 14px; } empty { height: 14px; display: block; }

自动机理论

发表于 2020-11-26 更新于 2020-12-04

定义

  • 非确定型有穷自动机(NFA),在某状态下,对于指定的输入,存在多个转移状态。一般情况下,通过某种算法可转换为 DFA

  • 上下文无关文法 描述程序设计语言的结构以及相关集合的重要记号,用来构造编译器的语法分析部件。

  • 字母表 符号的有穷非空集合。用Σ\SigmaΣ表示

  • 串(有时候称为单词)是从某个字母表中选择的符号的有穷序列。例如 01101 是从二进制字母表Σ={0,1}\Sigma = \lbrace 0,1\rbraceΣ={0,1}中选出的串

  • 空串 出现 0 次符号的串,记做ε\varepsilonε

  • 串的长度 这个串中的符号数,记做∣ω∣\vert \omega \vert∣ω∣,例如∣ε∣=0\vert \varepsilon \vert = 0∣ε∣=0

  • 字母表的幂 定义Σk\Sigma^{k}Σk是长度为 k 的串的集合。串的每个符号都属于Σ\SigmaΣ。无论是什么字母表Σ0={ε}\Sigma^{0}=\lbrace\varepsilon\rbraceΣ0={ε}。字母表上所有的串的集合约定为Σ∗\Sigma^{*}Σ∗,排除空串的集合约定为Σ+\Sigma^{+}Σ+

  • 串的连接 设定 x 和 y 都是串,于是 xy 表示 x 和 y 的连接

  • 集合表示法 {ω∣ω\lbrace\omega\vert\omega{ω∣ω的语义描述}\rbrace}。比如{ω∣ω\lbrace\omega\vert\omega{ω∣ω包含相同个数的 0 或 1}\rbrace},还可以把ω\omegaω换成某个带参数的表达式{0n1n∣n≥1}\lbrace0^n1^n\vert n \ge1\rbrace{0n1n∣n≥1}

  • 当两个状态机交互时,当其状态处于(i,x)(i,x)(i,x),对于一个合法的输入ZZZ,可使i→ji \rightarrow ji→j,x→yx \rightarrow yx→y,那么我们可以认为(i,x)→(j,y)(i,x) \rightarrow (j,y)(i,x)→(j,y)是可达的

确定型有穷自动机(DFA)

确定型有穷自动机 包含若干个状态的集合,若干个输入符,当有输入时,控制权将由一个状态转移到另一个状态。在任意状态下,对于指定的输入,其转移是唯一的。

  1. 一个有穷状态集合,记做QQQ,

  2. 初始状态记做q0q_0q0​,

  3. 终止状态记做FFF

  4. 一个有穷输入集合,记做Σ\SigmaΣ

  5. 状态转移函数,记做δ\deltaδ,以一个状态qqq和一个输入符号aaa作为参数,返回一个状态ppp。δ(q,a)=p;p∈Q\delta(q,a) = p;p\in Qδ(q,a)=p;p∈Q

  6. 扩展状态转移函数,记做δ^\hat{\delta}δ^,以一个状态qqq和一个输入串ω\omegaω作为参数,返回一个状态ppp。
    • δ^(q,ε)=q\hat{\delta}(q,\varepsilon)=qδ^(q,ε)=q
    • 假定ω=xa\omega = xaω=xa,a 是最后的输入,δ^(q,ω)=δ(δ^(q,x),a)\hat{\delta}(q,\omega)=\delta(\hat{\delta}(q,x),a)δ^(q,ω)=δ(δ^(q,x),a)

DFA 的定义: A=(Q,Σ,δ,q0,F)A = (Q,\Sigma,\delta,q_0,F)A=(Q,Σ,δ,q0​,F)

对于所有 DFA,我们可以定义为 L(A)={ω∣δ^(q0,ω)∈F}L(A) = \lbrace \omega|\hat{\delta}(q_0,\omega) \in F\rbraceL(A)={ω∣δ^(q0​,ω)∈F}

例如{ω∣ω\lbrace\omega|\omega{ω∣ω出现 01 字符串}\rbrace}可能出现三个状态

  1. q0q_0q0​ 未遇到 01,且最后一个不为 0,δ(q0,0)=q1,δ(q0,1)=q0\delta(q_0,0)=q_1,\delta(q_0,1)=q_0δ(q0​,0)=q1​,δ(q0​,1)=q0​
  2. q1q_1q1​ 未遇到 01,且最后一个为 0,δ(q1,0)=q1,δ(q0,1)=q2\delta(q_1,0)=q_1,\delta(q_0,1)=q_2δ(q1​,0)=q1​,δ(q0​,1)=q2​
  3. q2q_2q2​ 已遇到 01,可接受任意 0 或 1,δ(q2,0)=q2,δ(q2,1)=q2\delta(q_2,0)=q_2,\delta(q_2,1)=q_2δ(q2​,0)=q2​,δ(q2​,1)=q2​

其 DFA 表达式: A=({q0,q1,q2},{0,1},δ,q0,q2)A = (\lbrace q_0,q_1,q_2\rbrace,\lbrace 0,1\rbrace,\delta,q_0,{q_2})A=({q0​,q1​,q2​},{0,1},δ,q0​,q2​)

对 DFA 的细节描述难以阅读,通常使用两种方式来更好的描述

  1. 状态转移图

       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
  2. 状态转移表,一个状态和输入组成的δ\deltaδ的表

    0 1
    →q0\rightarrow q_0→q0​ q1q_1q1​ q0q_0q0​
    q1q_1q1​ q1q_1q1​ q2q_2q2​
    q2q_2q2​ q2q_2q2​ q2q_2q2​

使用扩展转移函数,对一个串 0011 进行计算

  1. δ^(q0,ε)=q0\hat{\delta}(q_0,\varepsilon)=q_0δ^(q0​,ε)=q0​

  2. δ^(q0,0)=δ(δ^(q0,ε),0)=δ(q0,0)=q1\hat{\delta}(q_0,0)=\delta(\hat{\delta}(q_0,\varepsilon),0) =\delta(q_0,0) = q_1δ^(q0​,0)=δ(δ^(q0​,ε),0)=δ(q0​,0)=q1​

  3. δ^(q0,00)=δ(δ^(q0,0),0)=δ(q1,0)=q1\hat{\delta}(q_0,00)=\delta(\hat{\delta}(q_0,0),0) =\delta(q_1,0) = q_1δ^(q0​,00)=δ(δ^(q0​,0),0)=δ(q1​,0)=q1​

  4. δ^(q0,001)=δ(δ^(q0,00),1)=δ(q1,1)=q2\hat{\delta}(q_0,001)=\delta(\hat{\delta}(q_0,00),1) =\delta(q_1,1) = q_2δ^(q0​,001)=δ(δ^(q0​,00),1)=δ(q1​,1)=q2​

  5. δ^(q0,0011)=δ(δ^(q0,001),1)=δ(q2,1)=q2\hat{\delta}(q_0,0011)=\delta(\hat{\delta}(q_0,001),1) =\delta(q_2,1) = q_2δ^(q0​,0011)=δ(δ^(q0​,001),1)=δ(q2​,1)=q2​

这种方式的处理,非常适合我们用递归函数进行计算。

非确定型有穷自动机(NFA)

与确定型有穷自动机类似,包含若干个状态,若干个输入符,状态转移函数,终止状态。唯一不同的是,δ\deltaδ的结果可能是零个、一个、或多个状态的集合。

  1. 一个有穷状态集合,记做QQQ,

  2. 初始状态记做q0q_0q0​,

  3. 终止状态记做FFF

  4. 一个有穷输入集合,记做Σ\SigmaΣ

  5. 状态转移函数,记做δ\deltaδ,以一个状态qqq和一个输入符号aaa作为参数,返回一个状态ppp。δ(q,a)={p1,p2,⋯ ,pk};{p1,p2,⋯ ,pk}∈Q\delta(q,a) = \lbrace p1,p2,\cdots,p_k \rbrace;\lbrace p1,p2,\cdots,p_k \rbrace\in Qδ(q,a)={p1,p2,⋯,pk​};{p1,p2,⋯,pk​}∈Q

  6. 扩展状态转移函数,记做δ^\hat{\delta}δ^,以一个状态qqq和一个输入串ω\omegaω作为参数,返回一个状态ppp。
    • δ^(q,ε)=q\hat{\delta}(q,\varepsilon)=qδ^(q,ε)=q
    • 假定ω=xa\omega = xaω=xa,δ^(q,x)={p1,p2,⋯ ,pk}\hat{\delta}(q,x)=\lbrace p_1,p_2,\cdots,p_k \rbraceδ^(q,x)={p1​,p2​,⋯,pk​},那么δ^(q,ω)=⋃i=1kδ(pi,a)={r1,r2,⋯ ,rm}\hat{\delta}(q,\omega)=\displaystyle\bigcup_{i=1}^k\delta(p_i,a)=\lbrace r_1,r_2,\cdots,r_m \rbraceδ^(q,ω)=i=1⋃k​δ(pi​,a)={r1​,r2​,⋯,rm​}

对于 NFA,可以定义为 L(A)={ω∣δ^(q0,ω)∩F}≠∅L(A) = \lbrace \omega|\hat{\delta}(q_0,\omega) \cap F\rbrace \ne \emptysetL(A)={ω∣δ^(q0​,ω)∩F}​=∅

例如,{{0,1}∣\lbrace \lbrace0,1\rbrace|{{0,1}∣以 01 结尾}\rbrace}

其状态转移图如下

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 已经开始了,一条弧线从q0q_0q0​指向q1q_1q1​,我们可以看到 0 有两条弧线,另一条指向q0q_0q0​。NFA 会同时走这两条线。当在q1q_1q1​状态时,它会检查下一个符号是否为 1,如果是则会进入状态q2q_2q2​。当在q2q_2q2​状态时,如还有其他输入,这条路线就终结掉了。

00101 的处理过程如下 自动机理论_NFA.png

其状态转移表如下

0 1
→q0\rightarrow q_0→q0​ {q0,q1}\lbrace q_0,q_1\rbrace{q0​,q1​} {q0}\lbrace q_0\rbrace{q0​}
q1q_1q1​ ∅\emptyset∅ {q2}\lbrace q_2\rbrace{q2​}
* q2q_2q2​ ∅\emptyset∅ ∅\emptyset∅

使用扩展转移函数的处理过程如下

  1. δ^(q0,ε)={q0}\hat{\delta}(q_0,\varepsilon)=\lbrace q_0\rbraceδ^(q0​,ε)={q0​}

  2. δ^(q0,0)=δ(q0,0)={q0,q1}\hat{\delta}(q_0,0)=\delta(q_0,0) = \lbrace q_0,q_1\rbraceδ^(q0​,0)=δ(q0​,0)={q0​,q1​}

  3. δ^(q0,00)=δ(q0,0)∪δ(q1,0)={q0}∪{q2}={q0,q1}\hat{\delta}(q_0,00)=\delta(q_0,0) \cup \delta(q_1,0)= \lbrace q_0\rbrace \cup \lbrace q_2\rbrace=\lbrace q_0,q_1\rbraceδ^(q0​,00)=δ(q0​,0)∪δ(q1​,0)={q0​}∪{q2​}={q0​,q1​}

  4. δ^(q0,001)=δ(q0,1)∪δ(q1,1)={q0}∪{q2}={q0,q2}\hat{\delta}(q_0,001)=\delta(q_0,1) \cup \delta(q_1,1)= \lbrace q_0\rbrace \cup \lbrace q_2\rbrace=\lbrace q_0,q_2\rbraceδ^(q0​,001)=δ(q0​,1)∪δ(q1​,1)={q0​}∪{q2​}={q0​,q2​}

  5. δ^(q0,0010)=δ(q0,0)∪δ(q2,0)={q0,q1}∪∅={q0,q1}\hat{\delta}(q_0,0010)=\delta(q_0,0) \cup \delta(q_2,0)= \lbrace q_0,q_1\rbrace \cup \emptyset=\lbrace q_0,q_1\rbraceδ^(q0​,0010)=δ(q0​,0)∪δ(q2​,0)={q0​,q1​}∪∅={q0​,q1​}

  6. δ^(q0,00101)=δ(q0,1)∪δ(q1,1)={q0}∪{q2}={q0,q2}\hat{\delta}(q_0,00101)=\delta(q_0,1) \cup \delta(q_1,1)= \lbrace q_0\rbrace \cup \lbrace q_2\rbrace=\lbrace q_0,q_2\rbraceδ^(q0​,00101)=δ(q0​,1)∪δ(q1​,1)={q0​}∪{q2​}={q0​,q2​}

NFA 与 DFA 的转换

通常来说,构造 NFA 比构造 DFA 更容易,每一个用 NFA 描述的语言也能用 DFA 来描述。这个可以用子集构造来证明。

子集构造从一个 NFA N=(Qn,Σ,δn,q0,Fn)N = (Q_n,\Sigma,\delta_n,q_0,F_n)N=(Qn​,Σ,δn​,q0​,Fn​)开始,转换为 D=(Qd,Σ,δd,{q0},Fd)D = (Q_d,\Sigma,\delta_d,\lbrace q_0 \rbrace,F_d)D=(Qd​,Σ,δd​,{q0​},Fd​)

  1. 两个自动机的输入字母表是相同的
  2. D 的起始状态为仅包含 N 的起始状态的长度为 1 的集合
  3. QdQ_dQd​是QnQ_nQn​子集的集合,即幂集合。假如QnQ_nQn​有 n 个状态,那么QdQ_dQd​有2n2^n2n状态,通常不是所有的状态都是从q0q_0q0​可达的,这些状态可以丢弃,所以实际上QdQ_dQd​的状态要远远小于2n2^n2n
  4. FDF_DFD​所有满足S∩Fn≠∅S\cap F_n \neq \emptysetS∩Fn​​=∅的QnQ_nQn​的子集的集合 S,也就是说,FDF_DFD​是QNQ_NQN​状态子集中至少包含一个FNF_NFN​的集合。
  5. 对于S⊆QnS \subseteq Q_nS⊆Qn​的集合 S 中每个状态来说,其对应的每个属于Σ\SigmaΣ的输入符号aaa的转移函数为: δD(S,a)=⋃p∈SδN(p,a)\delta_D(S,a)= \bigcup_{p \in S} \delta_N(p,a)δD​(S,a)=p∈S⋃​δN​(p,a)

示例,我们以接受所有 01 结尾的串的 NFA 向 DFA 转换,由于Qn={q0,q1,q2}Q_n = \lbrace q_0,q_1,q_2\rbraceQn​={q0​,q1​,q2​},所以子集构造产生一个带有23=82^3 = 823=8(并非所有状态都是有意义的)种状态的 DFA

0 1
∅\emptyset∅ ∅\emptyset∅ ∅\emptyset∅
→{q0}\rightarrow \lbrace q_0\rbrace→{q0​} {q0,q1}\lbrace q_0,q_1\rbrace{q0​,q1​} {q0}\lbrace q_0\rbrace{q0​}
{q1}\lbrace q_1\rbrace{q1​} ∅\emptyset∅ {q2}\lbrace q_2\rbrace{q2​}
∗{q2}*\lbrace q_2\rbrace∗{q2​} ∅\emptyset∅ ∅\emptyset∅
{q0,q1}\lbrace q_0,q_1\rbrace{q0​,q1​} {q0,q1}\lbrace q_0,q_1\rbrace{q0​,q1​} {q0,q2}\lbrace q_0,q_2\rbrace{q0​,q2​}
∗{q0,q2}*\lbrace q_0,q_2\rbrace∗{q0​,q2​} {q0,q1}\lbrace q_0,q_1\rbrace{q0​,q1​} {q0}\lbrace q_0\rbrace{q0​}
∗{q1,q2}*\lbrace q_1,q_2\rbrace∗{q1​,q2​} ∅\emptyset∅ {q2}\lbrace q_2\rbrace{q2​}
∗{q0,q1,q2}*\lbrace q_0,q_1,q_2\rbrace∗{q0​,q1​,q2​} {q0,q1}\lbrace q_0,q_1\rbrace{q0​,q1​} {q0,q2}\lbrace q_0,q_2\rbrace{q0​,q2​}

上述表格的详细证明如下(列举 2、5 两行):

  • δD({q0},0)=δN(q0,0)={q0,q1}\delta_D(\lbrace q_0\rbrace,0) = \delta_N(q_0,0) = \lbrace q_0,q_1\rbraceδD​({q0​},0)=δN​(q0​,0)={q0​,q1​}

  • δD({q0},1)=δN(q0,1)={q0}\delta_D(\lbrace q_0\rbrace,1) = \delta_N(q_0,1) = \lbrace q_0\rbraceδD​({q0​},1)=δN​(q0​,1)={q0​}

  • δD({q0,q1},0)=δN(q0,0)∪δN(q1,0)={q0,q1}∪∅={q0,q1}\delta_D(\lbrace q_0,q_1\rbrace,0) = \delta_N(q_0,0) \cup \delta_N(q_1,0) = \lbrace q_0,q_1\rbrace \cup \emptyset = \lbrace q_0,q_1\rbraceδ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}\delta_D(\lbrace q_0,q_1\rbrace,1) = \delta_N(q_0,1) \cup \delta_N(q_1,1) = \lbrace q_0\rbrace \cup \lbrace q_2\rbrace= \lbrace q_0,q_2\rbraceδD​({q0​,q1​},1)=δN​(q0​,1)∪δN​(q1​,1)={q0​}∪{q2​}={q0​,q2​}

我们给这 8 中状态设计新的名字,如AAA表示∅\emptyset∅,BBB表示{q0}\lbrace q_0 \rbrace{q0​}等。

0 1
A A A
→B\rightarrow B→B E B
C A D
∗D*D∗D A A
E E F
∗F*F∗F E B
∗G*G∗G A D
∗H*H∗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

我们需要证明这个子集构造是正确的。

在读入输入符号序列ω\omegaω后,所构造的 DFA 处于这样一个状态,这个状态时 NFA 在读ω\omegaω后所处的状态的集合。由于 DFA 的接受状态是至少包含一个 NFA 接受状态FnF_nFn​的集合,因为包含 NFA 的可接受状态FnF_nFn​,因此这个集合也是被 NFA 接受的。于是我们可以得出结论,这个 DFA 和 NFA 接受完全相同的语言。

我们来证明 若D=(Qd,Σ,δd,{q0},Fd)D = (Q_d,\Sigma,\delta_d,\lbrace q_0 \rbrace,F_d)D=(Qd​,Σ,δd​,{q0​},Fd​)是从N=(Qn,Σ,δn,q0,Fn)N = (Q_n,\Sigma,\delta_n,q_0,F_n)N=(Qn​,Σ,δn​,q0​,Fn​)子集构造出来的,那么L(D)=L(N)L(D) = L(N)L(D)=L(N),这也就是证明,对于输入字母表ω\omegaω ,δ^({q0},ω)=δ^(q0,ω)\hat{\delta}(\lbrace q_0\rbrace,\omega) = \hat{\delta}(q_0,\omega)δ^({q0​},ω)=δ^(q0​,ω)

  1. 当∣ω∣=0|\omega| =0∣ω∣=0,即ω=ε\omega = \varepsilonω=ε,根据δ^\hat{\delta}δ^的定义,δ^({q0},ε)\hat{\delta}(\lbrace q_0\rbrace,\varepsilon)δ^({q0​},ε) δ^(q0,ε)\hat{\delta}(q_0,\varepsilon)δ^(q0​,ε)的结果都为{q0}\lbrace q_0 \rbrace{q0​}

  2. 假定∣ω∣=n+1|\omega| = n + 1∣ω∣=n+1 ,ω=xa\omega = xaω=xa,∣x∣=n|x| = n∣x∣=n,δ^({q0},x)=δ^(q0,x)\hat{\delta}(\lbrace q_0\rbrace,x) = \hat{\delta}(q_0,x)δ^({q0​},x)=δ^(q0​,x)成立,两个集合状态 N 为{p1,p2,⋯ ,pk}\lbrace p_1,p_2,\cdots,p_k\rbrace{p1​,p2​,⋯,pk​}。

    那么对于 NFA:

    δ^N(q0,ω)=⋃i=1kδN(pi,a)\hat{\delta}_N(q_0,\omega)=\displaystyle\bigcup_{i=1}^k\delta_N(p_i,a)δ^N​(q0​,ω)=i=1⋃k​δN​(pi​,a)

    通过子集构造的定义我们可以得出

    δD({p1,p2,⋯ ,pk},a)=⋃i=1kδN(pi,a)\delta_D(\lbrace p_1,p_2,\cdots,p_k\rbrace,a)=\displaystyle\bigcup_{i=1}^k\delta_N(p_i,a)δD​({p1​,p2​,⋯,pk​},a)=i=1⋃k​δN​(pi​,a)

    又因 δD({q0},x)={p1,p2,⋯ ,pk}\delta_D(\lbrace q_0\rbrace,x)= \lbrace p_1,p_2,\cdots,p_k\rbraceδD​({q0​},x)={p1​,p2​,⋯,pk​}

    δD^({q0},w)=δD(δD^({q0},x),a)=δD({p1,p2,⋯ ,pk},a)=⋃i=1kδN(pi,a)\hat{\delta_D}(\lbrace q_0\rbrace,w) = \delta_D(\hat{\delta_D}(\lbrace q_0\rbrace,x),a) =\delta_D( \lbrace p_1,p_2,\cdots,p_k\rbrace,a)=\displaystyle\bigcup_{i=1}^k\delta_N(p_i,a)δD​^​({q0​},w)=δD​(δD​^​({q0​},x),a)=δD​({p1​,p2​,⋯,pk​},a)=i=1⋃k​δN​(pi​,a)

文本搜索

识别 web 和 ebay 出现

通过 NFA 来实现 自动机理论_web_ebay.png

对终止状态[4,8]增加了回到1的转移,以支持 web 和 ebay 出现在中间位置。

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
//识别单词web和ebay出现的NFA

//状态转移函数
var delta = {
1: (a) => {
if (a == "w") return [1, 2];
else if (a == "e") return [1, 5];
else return [1];
},
2: (a) => (a === "e" ? [3] : []),
3: (a) => (a === "b" ? [4] : []),
4: (a) => [1, 4],
5: (a) => (a === "b" ? [6] : []),
6: (a) => (a === "a" ? [7] : []),
7: (a) => (a === "y" ? [8] : []),
8: (a) => [1, 8],
};

//所有状态
Qn = [1, 2, 3, 4, 5, 6, 7, 8];
//初始状态
q0 = 1;
//终止状态
F = [4, 8];

//扩展状态转移函数
function hat_delat(q0, w) {
if (typeof w === "string") {
w = w.split("");
}
//ε的总是返回当前状态
if (w.length === 0) {
return [q0];
}
let result = [];
//ω=xa,将结果并集
hat_delat(q0, w.slice(0, -1)).forEach((q) => {
let a = w[w.length - 1];
let next = delta[q](a);
//去重合并
next
.filter((item) => !result.includes(item))
.forEach((item) => result.push(item));
});
return result;
}

console.log(hat_delat(q0, "webay"));
console.log(hat_delat(q0, "aebay"));
console.log(hat_delat(q0, "abc"));

通过子集构造的方式来转换为 DFA,省略[4,8]的1出口 自动机理论_ndf_dfa.png

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
//状态转移函数
var delta = {
1: (a) => {
if (a == "w") return [1, 2];
else if (a == "e") return [1, 5];
else return [1];
},
2: (a) => (a === "e" ? [3] : []),
3: (a) => (a === "b" ? [4] : []),
4: (a) => [],
5: (a) => (a === "b" ? [6] : []),
6: (a) => (a === "a" ? [7] : []),
7: (a) => (a === "y" ? [8] : []),
8: (a) => [],
};

//所有状态
Qn = [1, 2, 3, 4, 5, 6, 7, 8];
//初始状态
q0 = 1;
//终止状态
F = [4, 8];

//由NFA状态子集构成的DFA状态集合
DFA = {};

//输入字母表为26个字母
E = "qwertyuiopasdfghjklzxcvbnm".split("");

//为Array添加equal方法,判断内部元素是否相同
Array.prototype.equal = function (other) {
if (this.length == other.length) {
let equal = true;
for (let i = 0; i < other.length; i++) {
if (this[i] !== other[i]) {
equal = false;
break;
}
}
return equal;
} else {
return false;
}
};
//为Array添加去重函数,若内部元素为数组,则根据内部数组元素是否完全相同来去重
Array.prototype.unique = function () {
let temp = [];
for (let i = 0; i < this.length; i++) {
let insert = true;
var item = this[i];
if (Array.isArray(item)) {
for (let j = 0; j < temp.length; j++) {
if (temp[j].equal(item)) {
insert = false;
}
}
} else {
insert = temp.indexOf(item) == -1;
}
if (insert) {
temp.push(item);
}
}
return temp;
};

//子集构造
function sub_construct(state_arr) {
if (state_arr.length === 0) {
return;
}
let trans = DFA[state_arr];
//已经计算过的子集不在计算
if (trans == undefined) {
trans = [];
E.forEach((a) => {
//根据子集构造的定义,DFA子集构造的转移函数,对于特定输入,state_arr的所有状态的转移函数结构的集合

let dfa_of_state_arr = [];
state_arr.forEach((state) => {
dfa_of_state_arr = dfa_of_state_arr.concat(delta[state](a));
});
trans.push(dfa_of_state_arr.sort().unique());
});

trans = trans.sort().unique();
DFA[state_arr] = trans;
//检查是否触发了新的DFA状态,如果有,则继续向前查找
trans.forEach((dfa_of_state_arr) => {
sub_construct(dfa_of_state_arr);
});
}
}

sub_construct([q0]);
console.log(DFA);

//子集构造完成后,我们就可以构建出扩展转移函数
function hal_delta(q_arr, w) {
if (typeof w === "string") {
w = w.split("");
}
if (w.length === 0) {
return q_arr;
}
let result = [];
// ω=xa,将结果并集
hal_delta(q_arr, w.slice(0, -1)).forEach((q) => {
let a = w[w.length - 1];
let next = delta[q](a);
//去重合并
next.forEach((item) => result.push(item));
});
return result.sort().unique();
}

console.log(hal_delta([q0], "webay"));

上述输出结果为,与例图完美对应

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"1": [[1], [1, 2], [1, 5]],
"1,2": [[1], [1, 2], [1, 3, 5]],
"1,3,5": [[1], [1, 2], [1, 4, 6], [1, 5]],
"1,4,6": [[1], [1, 2], [1, 5], [1, 7]],
"1,5": [[1], [1, 2], [1, 5], [1, 6]],
"1,6": [[1], [1, 2], [1, 5], [1, 7]],
"1,7": [[1], [1, 2], [1, 5], [1, 8]],
"1,8": [[1], [1, 2], [1, 5]]
}

//扩展转移函数的输出
[(1, 8)]

互斥量与条件变量

发表于 2020-11-26 更新于 2020-12-02

凡涉及到内存共享、共享文件以及共享任何资源的情况都会引起竞争条件。我们需要阻止多个进程同时读写共享数据,换言之,我们需要的是互斥,即已某种手段确定当一个进程在使用一个共享变量或文件时,其他进程不能做同样的操作。

避免竞争条件的问题也可以用一种抽象的方式进行描述。一个进程的一部分时间做内部计算或另外一些不会引起竞争条件的操作,另一部分时间可能需要访问共享内存或共享文件,或执行另外一些可能导致竞争条件的操作。我们把对共享内存进行访问的程序片段称作临界区。如果我们能够适当的安排,使得两个进程不可能同时处于临界区,就能够避免竞争条件。

一个好的解决方案,需要满足以下四个条件:

  1. 任何两个进程不能同时出去其临界区
  2. 不应对 CPU 的速度和数量做任何假设
  3. 临界区外运行的进程不得阻塞其他进程
  4. 不得使进程无期限等待进入临界区

原子操作:是指一组相关联的操作要么都不间断地执行,要么都不执行。 信号量:使用一个整型变量来累计唤醒次数。对一信号量执行 down 操作,则是检查其值是否大于 0,若是则减一,否则进程将睡眠,检测数值、修改变量值以及可能的睡眠操作需要确保是原子操作。up 操作对信号量的值增 1。如果一个或多个进程在该信号上睡眠,无法完成一个先前的 down 操作,则有系统选择其中的一个并允许该进程完成它的 down 操作。于是,对一个有进程在其上睡眠的信号量执行了一次 up 操作之后,该信号量的值仍旧是 0,但在其上睡眠的进程却少了一个。信号量的值增 1 和唤醒其他进程操作也是不可分割的,是原子操作。 互斥量:一个可以处于两态之一的变量,解锁和加锁

wireshark

发表于 2020-11-24 更新于 2020-12-02

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
2
3
ip.len le 1500
ip.len le 02734
ip.len le 0x5dc

布尔类型的值可以使用1(true),0(false)

网络地址可以使用:,.,-分割

1
2
3
4
eth.dst == ff:ff:ff:ff:ff:ff
eth.dst == ff-ff-ff-ff-ff-ff
eth.dst == ffff.ffff.ffff
ip.addr == 192.168.0.1

可以比较字符串

1
2
http.request.uri == "https://www.wireshark.org/"
udp contains 81:60:03

过滤器表达式组合语法

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
2
3
4
5
6
7
8
9
10
eth.src[0:3] == 00:00:83
eth.src[2] == 83

tcp.port in {80 443 8080}
# 等效于
tcp.port == 80 || tcp.port == 443 || tcp.port == 8080

tcp.port in {443 4430..4434}
#不等效于,因为port指代的可能是source,也可能是destination,而下述的每个判断是分别进行的
tcp.port == 443 || (tcp.port >= 4430 && tcp.port <= 4434)

mrcp

发表于 2020-11-18 更新于 2020-12-02 分类于 ivr

概述

MRCP 是一个标准、统一、可扩展的媒体资源控制协议,主要用来语音识别、TTS 合成、录音、 声纹识别(确认是否为某一类群体),声纹认证。MRCP 是一个框架,同时也是一个协议。该框架定义了它的网络基本组件及相互关系。它使用 SIP 协议来控制会话管理,使用 RTP 进行媒体流传输。它的协议定义了它如何控制媒体资源的过程。 MRCP 是基于文本的协议,与 HTTP、SIP 的结构类似

结构

MRCP 允许客户端请求和控制网路上指定的媒体服务,使用媒体服务的客户端通常包括:

  • IVR 平台
  • 高级媒体网关
  • 基于 IP 的媒体服务端

客户端一般需要有

  1. 定位和发现媒体资源服务端
  2. 与媒体资源服务建立消息 channel
  3. 远程控制媒体资源

MRCP 通过 SIP URI 来确认媒体资源的 IP 地址,当找到合适的媒体资源服务,SIP 会在客户端和服务端建立两个消息管道,一个用来发送和接收音频流(又称媒体会话),另一个是客户端用来发送控制请求到媒体资源服务端、服务端响应并返回事件给客户端(又称控制会话,MRCP 协议建立在控制会话上) mrcp_结构.png

媒体资源类型

类型 描述
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
2
3
4
5
6
7
8
9
10
11
12
client                    TTS
| |
| SPEAK |
|----------------------->|
| |
| 200(IN-PROGRESS) |
|<-----------------------|
| |
|SPEAK-COMPLETE(COMPLETE)|
|<-----------------------|
| |
| |

其请求报文大致如下

1
2
3
4
5
6
7
8
9
MRCP/2.0 380 SPEAK 14321
Channel-Identifier: 43b9ae17@speechsynth
Content-Type: application/ssml+xml
Content-Length: 253

<?xml version="1.0" encoding="UTF-8"?>
<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis">
<emphasis>Good afternoon</emphasis> Anne.<break/> You have one voice message, two e-mails, and three faxes waiting for you.
</speak>

MRCP 的请求报文大体上可以分为三个部分

  1. 请求行,每个字段以空格分隔,以换行(CRLF)结束,每个字段分别表示MRCP版本、请求报文长度、请求事件、请求id
  2. 请求头 header-name:header-value CRLF格式,请求头结尾一定会有一个空行
  3. 请求报文
1
2
3
MRCP/2.0 119 14321 200 IN-PROGRESS
Channel-Identifier: 43b9ae17@speechsynth
Speech-Marker: timestamp=857206027059

MRCP 的返回报文类似请求报文,返回行每个字段分别代表MRCP版本、返回报文长度、请求id、返回码、当前request状态。 IN-PROGRESS 状态一般表示 audio 流正在向客户端发送。

1
2
3
4
MRCP/2.0 157 SPEAK-COMPLETE 14321 COMPLETE
Channel-Identifier: 43b9ae17@speechsynth
Speech-Marker: timestamp=861500994355
Completion-Cause: 000 normal

当服务端执行到 COMPLETE 状态时,表明不在接收指定的 request 请求,则会返回一个包含 COMPLETE 的响应报文。此时返回行每个字段分别代表MRCP版本、返回事件、返回报文长度、请求id、当前request状态。

1
2
3
MRCP/2.0 111 START-OF-INPUT 32121 IN-PROGRESS
Channel-Identifier: 23af1e13@speechrecog
Input-Type: speech

其他有返回事件的返回行格式也是这样。

SIP in MRCP

MRCP 使用 SIP 协议在客户端与服务端进行通信。标准的三步INVITE-200 OK-ACK握手用来建立 media session 和 control session 连接。BYE-200 OK用来关闭连接。使用SDP offer/answer模型来进行协商

以下是 control channel 的一些 SDP 片段

1
2
3
4
5
6
c=IN IP4 10.0.0.1
m=application 9 TCP/MRCPv2
a=setup:active
a=connection:new
a=resource:speechsynth
a=cmid: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
2
3
4
5
6
c=IN IP4 10.0.0.22
m=application 43251 TCP/MRCPv2
a=setup:passive
a=connection:new
a=channel:43b9ae17@speechsynth
a=cmid:1
  • m 这里就指定了将要使用的端口号

示例:

一个标准的单媒体资源请求服务 mrcp_single_media.png

INVITE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
INVITE sip:mrcpv2@example.com SIP/2.0
Via: SIP/2.0/UDP host1.example.com;branch=z9hG4bKabc Max-Forwards: 70
To: <sip:mrcpv2@example.com>
From: <sip:client@example.com>;tag=12425
Call-ID: 43fb8aec@host1.example.com
CSeq: 1 INVITE
Contact: <sip:client@host1.example.com> Content-Type: application/sdp
Content-Length: 264

v=0
o=client 2890844527 2890844527 IN IP4 host1.example.com
s=-
c=IN IP4 host1.example.com
t=0 0
m=audio 5324 RTP/AVP 0
a=rtpmap:0 PCMU/8000
a=recvonly
a=mid:1
m=application 9 TCP/MRCPv2
a=setup:active
a=connection:new
a=resource:speechsynth
a=cmid:1
  • 第一个 m 行建立 media 流
  • 第二个 m 行连接 control session

200 OK

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
SIP/2.0 200 OK
Via: SIP/2.0/UDP host1.example.com;branch=z9hG4bKabc
;received=192.168.1.11
Max-Forwards: 70
To: <sip:mrcpv2@example.com>;tag=98452
From: <sip:client@example.com>;tag=12425
Call-ID: 43fb8aec@host1.example.com
CSeq: 1 INVITE
Contact: <sip:mrcpv2@host100.example.com>
Content-Type: application/sdp
Content-Length: 274

v=0
o=server 31412312 31412312 IN IP4 host100.example.com
s=-
c=IN IP4 host100.example.com
t=0 0
m=audio 4620 RTP/AVP 0
a=rtpmap:0 PCMU/8000
a=sendonly
a=mid:1
m=application 9001 TCP/MRCPv2
a=setup:passive
a=connection:new
a=channel:153af6@speechsynth
a=cmid:1

ACK

1
2
3
4
5
6
7
8
ACK sip:mrcpv2@host100.example.com SIP/2.0
Via: SIP/2.0/UDP host1.example.com;branch=z9hG4bK214 Max-Forwards: 70
To: <sip:mrcpv2@example.com>;tag=98452
From: <sip:client@example.com>;tag=12425
Call-ID: 43fb8aec@host1.example.com
CSeq: 1 ACK
Contact: <sip:mrcpv2@host1.example.com>
Content-Length: 0

添加和删除 media 资源 mrcp_add_remove.png

INVITE

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
INVITE sip:mrcpv2@host100.example.com SIP/2.0
Via: SIP/2.0/UDP host1.example.com;branch=z9hG4bK452 Max-Forwards: 70
To: <sip:mrcpv2@example.com>;tag=98452
From: <sip:client@example.com>;tag=12425
Call-ID: 43fb8aec@host1.example.com CSeq: 2 INVITE
Contact: <sip:client@host1.example.com>
Content-Type: application/sdp
Content-Length: 367

v=0
o=client 2890844527 2890844528 IN IP4 host1.example.com
s=-
c=IN IP4 host1.example.com
t=0 0
m=audio 5324 RTP/AVP 0
a=rtpmap:0 PCMU/8000
a=sendrecv
a=mid:1
m=application 9 TCP/MRCPv2
a=setup:active
a=connection:existing
a=resource:speechsynth
a=cmid:1
m=application 9 TCP/MRCPv2
a=setup:active
a=connection:existing
a=resource:recorder
a=cmid:1
  • 新申请一个 recorder 服务。SDP 全量更新 control session 和 media session 的,所以之前建立的 control session 是需要体现在当前的 SDP 内容中,当我们使用 recorder 服务,media 资源就需要保持双向连接(sendrecv),control session 的 connection 使用了现有的 control session 的 TCP 连接

200 OK

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
SIP/2.0 200 OK
Via: SIP/2.0/UDP host1.example.com;branch=z9hG4bK452;received=192.168.1.1
To: <sip:mrcpv2@example.com>;tag=98452
From: <sip:client@example.com>;tag=12425
Call-ID: 43fb8aec@host1.example.com CSeq: 2 INVITE
Contact: <sip:client@host1.example.com>
Content-Type: application/sdp
Content-Length: 387

v=0
o=client 31412312 31412313 IN IP4 host100.example.com
s=-
c=IN IP4 host100.example.com
t=0 0
m=audio 4620 RTP/AVP 0
a=rtpmap:0 PCMU/8000
a=sendrecv
a=mid:1
m=application 9001 TCP/MRCPv2
a=setup:passive
a=connection:existing
a=channel:153af6@speechsynth
a=cmid:1
m=application 9001 TCP/MRCPv2
a=setup:passive
a=connection:existing
a=channel: 153af6@recorder
a=cmid:1

ACK

1
2
3
4
5
6
7
8
ACK sip:mrcpv2@host100.example.com SIP/2.0
Via: SIP/2.0/UDP host1.example.com;branch=z9hG4bK554
Max-Forwards: 70
To: <sip:mrcpv2@example.com>;tag=98452
From: <sip:client@example.com>;tag=12425
Call-ID: 43fb8aec@host1.example.com
CSeq: 2 ACK
Contact: <sip:mrcpv2@host1.example.com> Content-Length: 0

INVITE

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
INVITE sip:mrcpv2@host100.example.com SIP/2.0
Via: SIP/2.0/UDP host1.example.com;branch=z9hG4bK763 Max-Forwards: 70
To: <sip:mrcpv2@example.com>;tag=98452
From: <sip:client@example.com>;tag=12425
Call-ID: 43fb8aec@host1.example.com
CSeq: 3 INVITE
Contact: <sip:client@host1.example.com>
Content-Type: application/sdp
Content-Length: 367

v=0
o=client 2890844527 2890844529 IN IP4 host1.example.com
s=-
c=IN IP4 host1.example.com
t=0 0
m=audio 5324 RTP/AVP 0
a=rtpmap:0 PCMU/8000
a=recvonly
a=mid:1
m=application 9 TCP/MRCPv2
a=setup:active
a=connection:existing
a=resource:speechsynth
a=cmid:1
m=application 0 TCP/MRCPv2
a=setup:active
a=connection:existing
a=resource:recorder
a=cmid:1
  • application 后的端口被设置 0,表示禁用

200 OK

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
SIP/2.0 200 OK
Via: SIP/2.0/UDP host1.example.com;branch=z9hG4bK763
;received=192.168.1.1
To: <sip:mrcpv2@example.com>;tag=98452 From: <sip:client@example.com>;tag=12425 Call-ID: 43fb8aec@host1.example.com CSeq: 3 INVITE
Contact: <sip:client@host1.example.com> Content-Type: application/sdp Content-Length: 384

v=0
o=client 31412312 31412314 IN IP4 host100.example.com s=-
c=IN IP4 host100.example.com
t=0 0
m=audio 4620 RTP/AVP 0
a=rtpmap:0 PCMU/8000
a=sendonly
a=mid:1
m=application 9001 TCP/MRCPv2
a=setup:passive
a=connection:existing
a=channel:153af6@speechsynth
a=cmid:1
m=application 0 TCP/MRCPv2
a=setup:passive
a=connection:existing
a=channel: 153af6@recorder
a=cmid:1

ACK

1
2
3
4
5
6
7
8
ACK sip:mrcpv2@host100.example.com SIP/2.0
Via: SIP/2.0/UDP host1.example.com;branch=z9hG4bK432
Max-Forwards: 70
To: <sip:mrcpv2@example.com>;tag=98452
From: <sip:client@example.com>;tag=12425
Call-ID: 43fb8aec@host1.example.com
CSeq: 3 ACK
Contact: <sip:mrcpv2@host1.example.com> Content-Length: 0

查询服务端支持的功能

OPTIONS

1
2
3
4
5
6
7
8
9
10
OPTIONS sip:mrcpv2@example.com SIP/2.0
Via: SIP/2.0/UDP host1.example.com;branch=z9hG4bKab3
Max-Forwards: 70
To: <sip:mrcpv2@example.com>
From: <sip:client@example.com>;tag=21342
Call-ID: 12fa3421@host1.example.com
CSeq: 21342 OPTIONS
Contact: <sip:client@host1.example.com>
Accept: application/sdp
Content-Length: 0

200 OK

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
SIP/2.0 200 OK
Via: SIP/2.0/UDP host1.example.com;branch=z9hG4bKab3
Max-Forwards: 70
To: <sip:mrcpv2@example.com>;tag=32421
From: <sip:client@example.com>;tag=21342
Call-ID: 12fa3421@host1.example.com
CSeq: 21342 OPTIONS
Contact: <sip:client@host99.example.com>
Allow: INVITE, BYE, OPTIONS, ACK, CANCEL
Content-Type: application/sdp
Content-Length: 288

v=0
o=Server 289123140 289123140 IN IP4 host99.example.com
s=-
t=0 0
m=application 0 TCP/MRCPv2
a=resource:speechsynth
a=resource:speechrecog
a=resource:recorder
m=audio 0 RTP/AVP 0 8 96
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:96 telephone-event/8000
a=fmtp:96 0-15

control session

MRCP 消息有三种:request、response、event。每种消息都含有一个起始行(各种类型的消息起始行有差异),一个或多个消息头,以 CRLF 间隔,格式为header-name:header-value,消息体报文一个有消息头定义长度和格式的报文。 mrcp_control_session.png

request

起始行格式:MRCP/2.0 message-length method-name requestid

  • 协议版本
  • 整个报文的长度,包括起始行
  • 请求的服务
    • SPEACK 语音合成服务
    • RECOGNIZE 语音识别服务
  • 32 位自增 int 来区分不同的请求

示例

1
2
3
4
5
6
7
8
MRCP/2.0 267 SPEAK 10000
Channel-Identifier: 43b9ae17@speechsynth
Content-Type: application/ssml+xml
Content-Length: 150

<?xml version="1.0" encoding="UTF-8"?> <speak version="1.0"
xmlns="http://www.w3.org/2001/10/synthesis"> Hello world!
</speak>

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
2
3
MRCP/2.0 109 START-OF-INPUT 10000 IN-PROGRESS
Channel-Identifier: 43b9ae17@speechrecog
Input-Type: dtmf

highlightjs

发表于 2020-11-05 更新于 2020-12-02 分类于 front-end

高亮代码块,官网文档

1
npm install -D -S highlight.js
1
2
3
4
5
6
7
8
9
10
11
import hljs from "highlight.js";
var obj = { a: 1, b: "2" };
//优化json格式
var json = JSON.stringify(obj, null, 4);

var lang = "json";
//得到高亮后的html文本
var html = hljs.highlight(lang, json).value;

//写入对应的dom元素上
document.getElementById("#id").innerHTML = html;

mockjs

发表于 2020-11-05 更新于 2020-12-02 分类于 front-end

使用 mockjs 来模拟 api 接口返回报文

1
2
#安装
npm install -S -D mockjs

在项目目录下新建mock/index.js,并在main.js中引入

1
2
3
4
5
6
7
8
9
10
11
12
// main.js

require("./mock");

//mock/index.js

import Mock from "mockjs";

Mock.mock("/api", {
name: "li",
age: 12,
});

mockjs 代理地址支持正则表达式

1
Mock.mock(/\/api\/.*/, { foo: "bar" });

mockjs 支持使用一个函数来返回模拟数据

1
2
3
4
5
6
//options中包含请求url,type,请求body等信息
Mock.mock(/\/api\/.*/, function (options) {
return {
foo: "bar";
}
});

axios

发表于 2020-11-04 更新于 2020-12-02 分类于 front-end

安装

1
npm install --save axios

示例

官方文档

基础示例

1
2
3
4
5
6
7
8
9
const axios = require("axios");
axios({
url: "http://localhost:5000",
method: "post",
data: "{}",
headers: { "Content-tyle": "application/json" },
}).then((res) => {
console.log(res);
});

其参数值可以为

  • data 请求数据
  • method 请求访问
  • url 请求地址

post 请求

1
2
3
4
5
6
7
8
9
10
11
12
const axios = require("axios");

axios
.post("http://localhost:5000", {
name: 1,
})
.then((res) => {
console.log(res);
});
.catch((err)=>{
console.log(err)
})

我们可以为请求设定一个具有指定配置项的实例

1
2
3
4
5
const instance = axios.create({
url: "http://localhost:5000",
timeout: 1000,
headers: { "Content-tyle": "application/json" },
});

然后这个 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
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
//axios/index.js
import axios from "axios";

axios.defaults.baseURL =
env === "development"
? "/api"
: window.location.protocol + "//" + window.location.host; // 配置axios请求的地址
axios.defaults.headers.post["Content-Type"] = "application/json; charset=utf-8";
axios.defaults.crossDomain = true;
axios.defaults.withCredentials = true; //设置cross跨域 并设置访问权限 允许跨域携带cookie信息

//配置发送请求前的拦截器 可以设置token信息
axios.interceptors.request.use(
(config) => {
// 这里配置全局loading
if (!/\.json/.test(config.url)) {
$("#screen").show(); // 这个div控制loading动画,项目中有对json的请求,所以这里区分是否是json文件
}
return config;
},
(error) => {
return Promise.reject(error);
}
);

// 配置响应拦截器
axios.interceptors.response.use(
(res) => {
$("#screen").hide(); // loading结束
return Promise.resolve(res.data); // 这里直接返回data, 即接口返回的所有数据
},
(error) => {
$("#screen").hide();
tooltip("", "连接错误!", "error");
// 判断是否登录失效,按照实际项目的接口返回状态来判断
if (error.toString().includes("776")) {
window.location.href = window.location.origin + "/#/login";
}
return Promise.reject(error);
}
);
export default axios;

最后在 main.js 里面引入

1
2
3
4
import VueAxios from "vue-axios"; // 报错的话则npm安装依赖
import axios from "./axios";

Vue.use(VueAxios, axios);

Webpack-dev-server 的 proxy 用法

在开发环境中,可以将 axios 的请求通过 proxy 进行转发

  • 最简单的用法示例

    1
    2
    3
    4
    5
    6
    7
    mmodule.exports = {
    devServer: {
    proxy: {
    "/api": "http://localhost:3000",
    },
    },
    };

    请求到/api/xxx现在会被代理到请求http://localhost:3000/api/xxx

  • 如果想要代理多个路径到同一个地址,可以使用一个或多个具有 context 属性的对象构成的数组

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    mmodule.exports = {
    devServer: {
    proxy: [
    {
    context: ["/api", "/auth"],
    target: "http://localhost:3000",
    },
    ],
    },
    };
  • 如果你不想传递/api,可以重写路径

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    mmodule.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
    16
    mmodule.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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var bodyFormData = new FormData();
bodyFormData.append("userName", "Fred");
axios({
method: "post",
url: "myurl",
data: bodyFormData,
headers: { "Content-Type": "multipart/form-data" },
})
.then(function (response) {
//handle success
console.log(response);
})
.catch(function (response) {
//handle error
console.log(response);
});

webpack

发表于 2020-10-29 更新于 2020-12-02

快速入门

官方入门文档

1
2
3
4
5
mkdir webpack-demo
cd webpack-demo
npm init -y
npm install webpack webpack-cli --save-dev
npm install --save lodash

新增两个文件

1
2
3
4
5
6
webpack-demo
|- package.json
|- /dist
|- index.html
|- /src
|- index.js
1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html>
<head>
<title>Getting Started</title>
</head>

<body>
<script src="main.js"></script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
import _ from "lodash";

function component() {
const element = document.createElement("div");

// Lodash, now imported by this script
element.innerHTML = _.join(["Hello", "webpack"], " ");

return element;
}
document.body.appendChild(component());

执行打包命令

1
2
# npx 类似package.json中的scripts,可直接运行
npx webpack

也可以在 package.json 中新增 script,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"name": "webpack-demo",
"version": "1.0.0",
"description": "",
"private": true,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^5.3.0",
"webpack-cli": "^4.1.0"
},
"dependencies": {
"lodash": "^4.17.20"
}
}

那么我们可以直接使用如下命令进行打包

1
npm run build

当不指定配置文件时,就使用了默认的配置(npx webpack --config webpack.config.js)

1
2
3
4
5
6
7
8
9
const path = require("path");

module.exports = {
entry: "./src/index.js",
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist"),
},
};

如需要指定配置文件,可在 package.json 中指定相关配置文件

命令成功执行后,就会将所有文件打包到 dist 目录,我们打开 index.html,如果一切正常的话,我们可以在浏览器上看到Hello webpack字样

我们可以使用npx webpack serve启动 web 服务,默认会在 8080 端口上启动

1
2
npm i webpack-dev-server -S -D
npx webpack serve

加载其他资源文件

webpack.config.js 中的配置,在 module 对象的 rules 属性中可以指定一系列的 loaders,每一个 loader 都必须包含 test 和 use 两个选项,这段配置的意思是说,当 webpack 编译过程中遇到 require()或 import 语句导入一个后缀名为.css 的文件是,先将它通过 css-loader 转换,在通过 style-loader 转换,然后继续打包。use 选项的值可以是数组或字符串,如果是数组,它的编译顺序是从后往前

修改一下项目结构

修改dist/index.html

1
2
3
4
5
6
7
8
9
10
<!doctype html>
<html>
<head>
<title>Asset Management</title>
</head>
<body>
- <script src="main.js"></script>
+ <script src="bundle.js"></script>
</body>
</html>

修改webpack.config.js

1
2
3
4
5
6
7
8
9
10
 const path = require('path');

module.exports = {
entry: './src/index.js',
output: {
- filename: 'main.js',
+ filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};

导入 css 文件

1
npm install --save-dev style-loader css-loader

修改webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 const path = require('path');

module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
+ module: {
+ rules: [
+ {
+ test: /\.css$/,
+ use: [
+ 'style-loader',
+ 'css-loader',
+ ],
+ },
+ ],
+ },
};

新增src/style文件

1
2
3
.hello {
color: red;
}

修改src/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  import _ from 'lodash';
+ import './style.css';

function component() {
const element = document.createElement('div');

// Lodash, now imported by this script
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+ element.classList.add('hello');

return element;
}

document.body.appendChild(component());
1
npx webpack serve --config webpack.config.js

导入图片

1
npm install --save-dev file-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
const path = require('path')

module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
]
},
+ {
+ test: /\.(png|svg|jpg|gif)$/,
+ use: [
+ 'file-loader',
+ ],
+ },
]
}
}

新增图片src/icon.png 修改src/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  import _ from "lodash";
import "./style.css";
+ import Icon from "./icon.png";

function component() {
const element = document.createElement("div");

// Lodash, now imported by this script
element.innerHTML = _.join(["Hello", "webpack"], " ");
element.classList.add("hello");

+ // Add the image to our existing div.
+ const myIcon = new Image();
+ myIcon.src = Icon;

+ element.appendChild(myIcon);

return element;
}

document.body.appendChild(component());

修改src/style.css

1
2
3
4
  .hello {
color: red;
+ background: url('./icon.png');
}

导入数据

1
npm install --save-dev csv-loader xml-loader

webpack.config.js

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
 const path = require('path');

module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
],
},
{
test: /\.(png|svg|jpg|gif)$/,
use: [
'file-loader',
],
},
+ {
+ test: /\.(csv|tsv)$/,
+ use: [
+ 'csv-loader',
+ ],
+ },
+ {
+ test: /\.xml$/,
+ use: [
+ 'xml-loader',
+ ],
+ },
+ ],
},
};

增加一些数据

src/data.xml

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<note>
<to>Mary</to>
<from>John</from>
<heading>Reminder</heading>
<body>Call Cindy on Tuesday</body>
</note>

src/data.csv

1
2
3
4
to,from,heading,body
Mary,John,Reminder,Call Cindy on Tuesday
Zoe,Bill,Reminder,Buy orange juice
Autumn,Lindsey,Letter,I miss you

src/index.js

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
  import _ from 'lodash';
import './style.css';
import Icon from './icon.png';
+ import Data from './data.xml';
+ import Notes from './data.csv';

function component() {
const element = document.createElement('div');

// Lodash, now imported by this script
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
element.classList.add('hello');

// Add the image to our existing div.
const myIcon = new Image();
myIcon.src = Icon;

element.appendChild(myIcon);

+ console.log(Data);
+ console.log(Notes);

return element;
}

document.body.appendChild(component());

类似的数据文件还可以使用

src/data.toml

1
2
3
4
5
6
7
title = "TOML Example"

[owner]
name = "Tom Preston-Werner"
organization = "GitHub"
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
dob = 1979-05-27T07:32:00Z

src/data.yaml

1
2
3
4
5
6
7
8
title: YAML Example
owner:
name: Tom Preston-Werner
organization: GitHub
bio: |-
GitHub Cofounder & CEO
Likes tater tots and beer.
dob: 1979-05-27T07:32:00.000Z

src/data.json5

1
2
3
4
5
6
7
8
9
10
11
{
// comment
"title": "JSON5 Example",
"owner": {
"name": "Tom Preston-Werner",
"organization": "GitHub",
"bio": "GitHub Cofounder & CEO\n\
Likes tater tots and beer.",
"dob": "1979-05-27T07:32:00.000Z"
}
}

需要安装相关插件

1
npm install toml yamljs json5 --save-dev

输出管理

通过 html-webpack-plugin自动生成 index.html,并引入相关资源 通过 clean-webpack-plugin 自动清理 dist 目录

1
2
npm install --save-dev html-webpack-plugin
npm install --save-dev clean-webpack-plugin

示例

src/print.js

1
2
3
export default function printMe() {
console.log("I get called from print.js!");
}

src/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  import _ from 'lodash';
+ import printMe from './print.js';

function component() {
const element = document.createElement('div');
+ const btn = document.createElement('button');

element.innerHTML = _.join(['Hello', 'webpack'], ' ');

+ btn.innerHTML = 'Click me and check the console!';
+ btn.onclick = printMe;
+
+ element.appendChild(btn);

return element;
}

document.body.appendChild(component());

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  const path = require('path');
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
+ const { CleanWebpackPlugin } = require('clean-webpack-plugin');



module.exports = {
- entry: './src/index.js',
+ entry: {
+ app: './src/index.js',
+ print: './src/print.js',
+ },
+ plugins: [
+ new CleanWebpackPlugin(),
+ new HtmlWebpackPlugin({
+ title: 'Output Management',
+ }),
+ ],
output: {
- filename: 'bundle.js',
+ filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};

统一输出 css 文件

使用插件mini-css-extract-plugin

1
npm install --save-dev mini-css-extract-plugin

webpack.config.js

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
  const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
+ const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const path = require('path')

module.exports = {
entry: {
app: './src/index.js',
print: './src/print.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
plugins: [
new CleanWebpackPlugin(),
+ new MiniCssExtractPlugin({
+ filename: 'main.css'
+ }),
new HtmlWebpackPlugin({
title: 'the output management'
}
)
],
module: {
rules: [
{
test: /\.css$/,
+ use: [MiniCssExtractPlugin.loader, 'css-loader']

}
]
}
}

重新编译后在 dist 目录下生成了 main.css,且 index.html 中引入了 main.css

1
npm run build

vue

发表于 2020-10-23 更新于 2020-12-02 分类于 frontend

双向绑定

通过构造函数 Vue 就可以创建一个 Vue 的根示例,并启动 Vue 应用,Vue 实例需要挂载一个 DOM 元素,它可以是 HTMLElement,也可以是 CSS 选择器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Vue 测试实例 - 菜鸟教程(runoob.com)</title>
<script src="https://unpkg.com/vue/dist/vue.min.js"></script>
</head>

<body>
<div id="app">
<p>{{ message }}</p>
</div>

<script>
var app = new Vue({
el: "#app",
data: {
message: "Hello Vue.js!",
},
});

console.log(app);
console.log(app.message);
</script>
</body>
</html>

上述 Vue 实例的构造器中的成员变量可以通过app.$el,app.$data的方式去访问,对于 data 数据,可以直接使用app.message去访问。在 dom 上以 {{}} 包含的内容与 app 进行双向绑定。当我们在 console 控制台修改app.messege的值时,页面也随着刷新 message 的内容

双向绑定实现原理

异步更新队列

Vue 在观察到数据变化时并不是直接更新 DOM,而是开启一个队列,并缓冲同一事件循环中发生的所有数据改变。在缓冲时会去除重复数据,从而避免不必要的计算和 DOM 操作。然后在下一个事件循环 tick 中,Vue 刷新队列并执行实际(已去重的)工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div id="app">
<div id="div" v-if="showDiv">这是一段文本</div>
<button @click="getText">获取div内容</button>
</div>
<script>
var app = new Vue({
el: "#app",
data: {
showDiv: false,
},
methods: {
getText: function () {
this.showDiv = true;
//在执行this.showDiv = true时,div仍然没有被创建出来,直到下一个Vue事件循环时,才开始创建。$nextTick就是用来直到什么时候DOM更新完成的
this.$nextTick(function () {
var text = document.getElementById("div").innerHTML;
console.log(text);
});
},
},
});
</script>

生命周期

每个 Vue 示例创建时,都会经历一系列的初始化过程,同时也会调用相应的生命周期钩子。比较常用的生命周期有

  • created 示例创建完成后调用,次阶段完成了数据的观测等,但尚未挂载,$el 还不可用,需要初始化一些处理数据时会比较有用
  • mounted el 挂载到实例上后调用
  • beforeDestroy 实例销毁前调用。主要是解绑一些使用 addEventListener 监听的事件等。

这些钩子与 el 和 data 类似,作为选项写入 Vue 实例

1
2
3
4
5
6
7
8
9
10
11
12
13
var app = new Vue({
el: "#app",
data: {
message: "Hello Vue.js!",
},
created: function () {
console.log(this.message);
console.log(this.$el); //undefined
},
mounted: function () {
console.log(this.$el);
},
});

手动挂载实例

Vue 提供了 Vue.extend 和$mount两个方法将 vue 实例挂载到一个 dom 上,即如果 Vue 实例在实例化时它没有收到 el 选项,它就处于“未挂载”状态,没有关联的 DOM 元素。可以使用$mount()手动挂载一个未挂载的实例。这个方法返回实例自身,因而可以链式调用其他实例方法。

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
<div id="app">
<my-component></my-component>
</div>
<div id="app2">
<my-component></my-component>
</div>
<div id="app3">
<my-component></my-component>
</div>
<script type="text/x-template" id="my-component">
<div>
<h1>h1</h1>
<h1>h2</h1>
</div>
</script>

<script>
var app = Vue.extend({
components: {
"my-component": {
template: "#my-component",
},
},
});
new app().$mount("#app");
new app().$mount("#app2");
new app({
el: "#app3",
});
</script>

插值与表达式

  1. {{}} 使用双大括号(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'
    },
    });
  2. {{}} 支持使用管道符|来对数据进行过滤,过滤的规则是自定义了,通过选项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('#');
    }
    }
    });
  3. 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
    8
    computed:{
    set: function (value) {
    this.number = parseInt(value.substring(1))
    },
    get: function () {
    return '$' + this.number
    }
    }

指令

  1. 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>
  2. 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

  3. v-html 输出 HTML

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <span v-html="link">}</span>
    <script>
    new Vue({
    el: "#app",
    data: {
    link: '<a href="#">一个链接</a>',
    },
    });
    </script>
  4. 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>
  5. 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" />
  6. 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>
  7. v-once 值渲染一次,包括元素和组件的所有子节点(包括 v 指令等)。首次渲染后,不再随数据的变化重新渲染

  8. v-pre 跳过编译

    1
    <span v-pre> {{这里面不会被编译}}</span>
  9. v-show 进行 CSS 属性切换,是否可见

自定义指令

例如注册一个 v-focus 的指令,用于在<input>元素初始化时自动获取焦点

1
2
3
4
5
6
7
8
9
10
11
12
13
//全局注册
Vue.directive("focus", {
//指令选项
});
//局部注册
var app = new Vue({
el: "#app",
directives: {
focus: {
//指令选项
},
},
});

自定义指令由几个钩子函数组成的,每个都是可选的

  • 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
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
<div id="app">
<input v-focus:arg.foo.bar="1+1" />
</div>

<script>
Vue.directive("focus", {
//指令选项
inserted: function (el, binding, vnode) {
el.focus();
console.log(el);
//<input>
console.log(JSON.stringify(binding));
/**
{
"name": "focus",
"rawName": "v-focus:arg.foo.bar",
"value": 2,
"expression": "1+1",
"arg": "arg",
"modifiers": { "foo": true, "bar": true },
"def": {}
}
*/
}

},
});
var app = new Vue({
el: "#app",
});
</script>

自定义指令也可以传入一个 JavaScript 对象字面量

1
<input v-focus="{msg:'hello',count:100}" />

自定义插件

注册插件

1
2
3
4
5
6
7
8
9
// 在Vue内部实际上会调用`MyPlugin.install(Vue)`
Vue.use(MyPlugin);

new Vue({
//... options
});

插件也可以带上自定义参数;
Vue.use(MyPlugin, { someOption: true });

自定义插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
MyPlugin.install = function (Vue, options) {
// 1. 注册Vue全局函数
Vue.prototype.myGlobalMethod = function () {
}

// 2. 添加全局指令
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
}
...
})

// 3. 注入一些组件选项,对所有组件都会有效
Vue.mixin({
created: function () {
// some logic ...
}
...
})

// 4. 注册实例函数
Vue.prototype.$myMethod = function (methodOptions) {
// some logic ...
}
}

绑定 class 的几种方式

v-bind 中的表达式最终会被解析为字符串,Vue 对 v-bind 增强了一些功能,提供了多重语法

  1. 对象语法

    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>

    也可以指定绑定到一个对象上

  2. 数组语法

    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
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 阻止单击事件冒泡-->
<a @click.stop="handle"></a>
<!-- 提交事件不重载页面-->
<form @submit.prevent="handle"></form>
<!-- 修饰符可以串联-->
<a @click.stop.prevent="handle"></a>
<!-- 添加事件侦听器使用事件捕获模式-->
<a @click.capture="handle"></a>
<!-- 仅当事件在元素本身(而不是子元素)触发时触发回调-->
<a @click.self="handle"></a>
<!-- 只触发一次,组件同样使用-->
<a @click.once="handle"></a>
<!-- 只有在keyCode是enter时调用submit-->
<a @keyup.enter="submit"></a>

按键

keyCode

  • .enter
  • .tab
  • .delete
  • .esc
  • .space
  • .up
  • .down
  • dblclick 双击

按键可以组合,或和鼠标一起配合使用

1
2
3
4
<!-- 按下shift时点击-->
<p @click.shift="handle">{{text1}}</p>
<!-- 按下shift和enter,一起时触发-->
<input @keyup.shift.enter="handle" />

$emit

使用$emit来触发自定义事件,使用$on来监听子组件的事件。

例如

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
<div id="app">
<p>总数:{{total}}</p>
<li-div @increase="handleGetTotal" @reduce="handleGetTotal"></li-div>
</div>

<script>
var app = new Vue({
el: "#app",
data: {
total: 0,
},
methods: {
handleGetTotal: function (total) {
this.total = total;
},
},

components: {
"li-div": {
template: '<div>\
<button @click="handleIncrease"> +1</button>\
<button @click="handleReduce"> -1</button>\
</div>',
data: function () {
return {
counter: 0,
};
},
methods: {
handleIncrease: function () {
this.counter++;
this.$emit("increase", this.counter);
},
handleReduce: function () {
this.counter--;
if (this.counter <script 0) {
this.counter = 0;
}
this.$emit("reduce", this.counter);
},
},
},
},
});
</script>

v-model语法糖,v-model绑定的是一个数据,接收一个 value 属性,,在其有新的 value 时会触发 input 事件。反之亦然。 等价于<input :value='someVariable' @input='someHandle'>

所以上述事例也可以写成如下

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
<div id="app">
<p>总数:{{total}}</p>
<li-div v-model="total"></li-div>
</div>

<script>
var app = new Vue({
el: "#app",
data: {
total: 0,
},
methods: {
handleGetTotal: function (total) {
this.total = total;
},
},

components: {
"li-div": {
template: '<div>\
<button @click="handleIncrease"> +1</button>\
<button @click="handleReduce"> -1</button>\
</div>',
data: function () {
return {
counter: 0,
};
},
methods: {
handleIncrease: function () {
this.counter++;
this.$emit("input", this.counter);
},
handleReduce: function () {
this.counter--;
if (this.counter <script 0) {
this.counter = 0;
}
this.$emit("input", this.counter);
},
},
},
},
});
</script>

不同组件互相通信

建议通过一个空的 vue 实例作为中央事件总线来实现。

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
<div id="app">
<button @click="click">click</button>
<l1></l1>
<l2></l2>
</div>

<script>
var bus = new Vue();
var template = function (name) {
return {
template: "<div>" + name + ":{{message}}</div>",
data: function () {
return {
message: "",
};
},
mounted: function () {
var _this = this;
bus.$on("on-message", function (msg) {
_this.message = msg;
});
},
};
};
var l1 = template("l1");
var l2 = template("l2");
var app = new Vue({
el: "#app",
methods: {
click: function () {
bus.$emit("on-message", "来自app的消息");
// 可以直接使用this.$children查看当前子组件的实例,子组件也可以使用this.$parent访问到父组件
//当给子标签定义ref时可以使用this.$refs.ref去访问子标签的Vue实例
},
},
components: { l1, l2 },
});
</script>

slot

当在子组件内使用特殊的<slot>元素就可以为这个子组件开启一个slot(插槽),在父组件的模板里,插入在子组件模板标签内的所有内容将替代子组件内的<slot>标签以及它的内容,slot可以指定一个 name,其会加载父组件的子组件模板标签内中属性slot值相同的内容。没有指定slot值的标签都作为默认匿名 slot

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
<div id="app">
<li_slot>
<div style="color:red">默认slot-1</div>
<div slot="s1" style="color:green">名为s1的slot</div>
<div slot="s2" style="color:pink">名为s2的slot</div>
<div slot="s2" style="color:pink">另一个名为s2的slot</div>
<div style="color:red">默认slot-2</div>
</li_slot>
</div>

<script>
var app = new Vue({
el: "#app",
components: {
li_slot: {
template: '<div>\
<slot></slot>\
<slot name="s1"></slot>\
<slot name="s2"></slot>\
<slot name="s2">若没有s2的slot,则显示这段文本</slot>\
<slot name="s3">若没有s3的slot,则显示这段文本</slot>\
<slot></slot>\
</div>',
},
},
});
</script>
vue_输出效果如下.png
vue_输出效果如下.png

作用域插槽(template)是一种特殊的 slot,使用一个可以复用的模板替换已渲染元素,该模板可以定义 scope,定义一个临时变量作用域,模板内部可以使用scope_name.msg的语法访问子组件插槽的数据 msg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div id="app">
<li-slot>
<template scope="scope_name">
<p>来自父组件的内容</p>
<p>{{scope_name.msg}}</p>
</template>
</li-slot>
</div>

<script>
var app = new Vue({
el: "#app",
components: {
"li-slot": {
template: '<div>\
<slot msg="来自子组件的内容"></slot>\
</div>',
},
},
});
</script>

作用域插槽更具有代表性的用例是列表组件

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
<div id="app">
<list-slot :books="books">
<template slot="book" scope="scope_name">
<li>{{scope_name.bookName}}</li>
</template>
</list-slot>
</div>

<script>
var app = new Vue({
el: "#app",
data: {
books: [
{ name: "Vue.js实战" },
{ name: "java实战" },
{ name: "python实战" },
],
},
components: {
"list-slot": {
props: {
books: {
type: Array,
default: function () {
return [];
},
},
},
template: '<ul>\
<slot name="book" v-for="book in books" :book-name="book.name"></slot>\
</ul>',
},
},
});
</script>

我们可以使用$slots访问默认作用域的 slot 渲染后的标签,$scopedSlots访问所有作用域的标签

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
<div id="app">
<li_slot>
<template scope="scope1">
<div style="color:red" scope="scope_name">默认slot</div>
</template>
<div slot="s1" style="color:green">名为s1的slot</div>
<div slot="s2" style="color:pink">名为s2的slot</div>
</li_slot>
</div>

<script>
var app = new Vue({
el: "#app",
components: {
li_slot: {
template: '<div>\
<slot></slot>\
<slot name="s1"></slot>\
<slot name="s2">若没有s2的slot,则显示这段文本</slot>\
<slot name="s3">若没有s3的slot,则显示这段文本</slot>\
</div>',
mounted: function () {
console.log(this.$slots);
console.log(this.$scopedSlots);
},
},
},
});
</script>
vue_slot.png
vue_slot.png

watch

监听 prop 或 data 的改变,当他们发生变化时触发 watch 配置的函数,此时对于节点以及渲染完成,

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
<div id="app">
<input-number v-model="value"></input-number>
</div>

<script type="text/x-template" id="input-number">
<div class="input-number">
</div>
</script>
<script>
Vue.component("input-number", {
template: "#input-number",
props: {
value: {
type: Number,
default: 0,
},
},
watch: {
value: function (newVal, oldVal) {
console.log("component newVal", newVal);
console.log("component oldVal", oldVal);
},
},
});
</script>
<script>
var app = new Vue({
el: "#app",
data: {
value: 5,
},
watch: {
value: function (newVal, oldVal) {
console.log("newVal", newVal);
console.log("oldVal", oldVal);
},
},
});
</script>

watch 对第一次赋值是不响应的,可通过设置immediate来让其执行。watch 一般仅监听属性的重新赋值,对于对象属性的内部值的操作时不响应的,可通过deep来使其生效

1
2
3
4
5
6
7
8
9
10
11
12
<script>

watch: {
value: {
handler:function (newVal, oldVal) {
},
immediate:true, //第一次赋值时即响应
deep:true //改变obj内的属性时也生效

}
},
</script>

watch 可以监听 vuex 中存储的内容

1
2
3
4
5
6
7
<script>

watch: {
'$store.state.value': function(newVal){
}
},
</script>

表单

单选按钮

当那个单选按钮被选中时,picked 即为对应绑定的数据的值,同一个v-model单选具有排斥性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div id="app">
<input type="radio" v-model="picked" :value="v1" />
<input type="radio" v-model="picked" :value="v2" />
<label>单选</label>
<p>{{picked}}</p>
<p>{{value}}</p>
</div>

<script>
var app = new Vue({
el: "#app",
data: {
picked: false,
v1: "123",
v2: "456",
},
});
</script>

组件

定义组件

组件最外层仅能有一个标签

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
<div id="app">
<my-component></my-component>
<my-component></my-component>
<my-component></my-component>
</div>
<script>
//全局组件
Vue.component("my-component", {
template: '<div @click="obj.counter++">{{counter}}</div>',
data: function () {
return {
counter: 0,
};
},
});
//示例组件
var app = new Vue({
el: "#app",
components: {
"my-component": {
template: '<div @click="obj.counter++">{{counter}}</div>',
//es6 语法 等同于 data:function(){}
data() {
return {
counter: 0,
};
},
},
},
});
</script>

异步组件

Vue 允许将组件定义为一个工厂函数,动态解析组件。工厂函数接收一个 resolve(component)回调,也可以使用 reject(reason)指示加载失败

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div id="app">
<lazy-component></lazy-component>
</div>
<script>
var app = new Vue({
el: "#app",
components: {
"lazy-component": function (resolve, reject) {
setTimeout(
//模拟延迟
function () {
resolve({
template: "<div >异步组件</div>",
});
},
3000
);
},
},
});
</script>

嵌套组件

嵌套的组件需要定义在全局组件上,必须设定一个条件来限制递归数量

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
<div id="app">
<child :count="1"></child>
</div>

<script>
Vue.component("child", {
props: {
count: {
type: Number,
default: 1,
},
},
template: '<div>\
<div>\
count:{{count}}\
</div>\
<div>\
<child :count="count+1" v-if= "count < 3" ></child>\
</div>\
</div > ',
});
var app = new Vue({
el: "#app",
});
</script>

动态组件

Vue 提供了一个特殊的元素<component>用来挂载不同的组件,使用is特性来选择要挂载的组件。通常is特性绑定的是组件的名称,组件也可以直接绑定对象。

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
<div id="app">
<component :is="currentView"></component>
<button @click="handleChangeView('A')">切换到A</button>
<button @click="handleChangeView('B')">切换到B</button>
<button @click="handleChangeView('C')">切换到C</button>
<button @click="handleChangeView('D')">切换到D</button>
</div>

<script>
var app = new Vue({
el: "#app",
data: {
currentView: "comA",
},
methods: {
handleChangeView: function (component) {
this.currentView = "com" + component;
if (!this.$options.components[this.currentView]) {
// 直接绑定一个组件对象
this.currentView = {
template: "<div>备用组件<div>",
};
}
},
},
components: {
comA: {
template: "<div>组件A<div>",
},
comB: {
template: "<div>组件B<div>",
},
comC: {
template: "<div>组件C<div>",
},
},
});
</script>

X-templates

可以使用这种模板更好的属性模板代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script src="vue.min.js"></script>
<div id="app">
<my-component></my-component>
</div>
<script type="text/x-template" id="my-component">
<div>
<h1>h1</h1>
<h1>h2</h1>
</div>
</script>

<script>
var app = new Vue({
el: "#app",
components: {
"my-component": {
template: "#my-component",
},
},
});
</script>

使用 props 传递数据

在组件内可以通过声明 props 访问在父类标签中的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div id="app">
<li-div msg="来自父组件的数据"></li-div>
<li-div :bind-msg="msg"></li-div>
</div>

<script>
var app = new Vue({
el: "#app",
data:{
msg:'来自组件的动态数据'
}
components: {
"li-div": {
template: "<div >{{msg}} {{bindMsg}}</div>",
props: ["msg","bindMsg"],
},
},
});
</script>

HTML 特性不区分大小写,当使用 DOM 模板时,驼峰命名的 props 名称要转换为短横分隔命名

1
2
3
4
5
6
<li-div msg-text="来自父组件的数据"></li-div>
<script>
// ...
props: ["msgText"];
template: "<div> {{msgText}}</div>";
</script>

父组件数据变化时传递给子组件默认是单向数据流,即父组件的数据更新会传递给子组件,但反过来不行。

示例:当我们输入内容时,子组件内容跟着变动,而父组件不变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div id="app">
<p style="color: green;">{{msg}}</p>
<li-div :msg="msg"></li-div>
</div>

<script>
var app = new Vue({
el: "#app",
data: {
msg: "fater",
},
components: {
"li-div": {
template: '<div><input :value="msg" v-model="msg"><p>{{msg}}</p><div>',
props: ["msg"],
},
},
});
</script>

在 JavaScript 中对象和数组式引用类型的,指向同一个内存空间,所以 props 中是对象和数组式,在子组件内改变是会影响符组件的。

示例:当我们输入内容时,子组件内容跟着变动,而父组件也会跟着变动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div id="app">
<p style="color: green;">{{obj.msg}}</p>
<li-div :obj="obj"></li-div>
</div>

<script>
var app = new Vue({
el: "#app",
data: { obj: { msg: "fater" } },
components: {
"li-div": {
template:
'<div><input :value="obj.msg" v-model="obj.msg" /><p>{{obj.msg}}</p><div>',
props: ["obj"],
},
},
});
</script>

数据验证

对于组件的 props,我们可以对其属性进行数据验证,

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
new Vue({
el: "#app",
components: {
"li-div": {
props: {
//必须是数字类型
propA: Number,
//必须是数字类型或字符串
propB: [Number, String],
//布尔类型,如果没有定义,默认值是true
propC: {
type: Boolean,
default: true,
},
//数字 ,必传
propD: {
type: Number,
required: true,
},
// 如果是数组或对象,默认值必须由函数返回
propE: {
type: Array,
default: function () {
return [];
},
},
//自定义一个验证函数
propF: {
validator: function (data) {
return value > 10;
},
},
},
},
},
});

验证的 type 可以是

  • String
  • Number
  • Boolean
  • Object
  • Array
  • Function

默认情况下含有静态值(非双向绑定的数据)属性的类型为 String,当我们需要自动推断类型时,可以使用v-bind

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div id="app">
<!-- <li-div counter="100"></li-div> -->
<li-div v-bind:counter="100"></li-div>
</div>

<script>
var app = new Vue({
el: "#app",
data: { obj: { msg: "fater" } },
components: {
"li-div": {
template: "<div>{{counter}}<div>",
props: {
counter: {
type: Number,
},
},
},
},
});
</script>

type 也可以是一个自定义构造器,使用 instanceof 检测 当 prop 验证失败时,在开发版本下会在控制台抛出一条警告

mixins

mixins: 是一种分发 Vue 组件中可复用功能的非常灵活的方式。混入对象可以包含任意组件选项。当组件使用 mixins 时,所有 mixins 的选项将被组合到该组件本身的选项,类似预编译过程。

  1. 组件的数据不共享,仅拷贝一份,数组和对象采用的是深拷贝,而非拷贝引用。
  2. 对于值为对象的选项如 methods,components 等,选项会被合并,键冲突的组件会覆盖混入对象的。
  3. 值为函数的选项,如 created,mounted 等,就会被合并调用,混合对象里的钩子函数在组件里的钩子函数之前调用

webpack 过程

使用.vue单文件组件的构建模式,需要 webpack 并使用 vue-loader 对.vue 格式的文件进行处理。一个.vue 文件一般包括 3 部分,即<template>,<script>,<style>

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div>你好:{{name}}</div>
</template>
<script>
export default {
props: {
name: {
type: String,
default: "",
},
},
};
</script>
<style scoped>
div {
color: #f60;
}
</style>

示例中的 style 标签使用了 scoped 属性,表示当前的 CSS 只在这个组件有效,如果不加,那么 div 的样式会应用到整个项目。<style>还可以结合 CSS 预编一起使用,比如使用 Less 处理可以写成<style lang='less'>

使用.vue 文件需要安装vue-loader、vue-style-loader等加载器并做配置。因为要使用 ES6 语法,还需要安装babel和bable-loader等加载器。使用 npm 逐个安装以下依赖

使用vue-cli

1
2
3
4
5
6
# vue2版本对应的cli
npm install -g --save-dev vue-cli


#项目名必须全部小写,安装过程中注意`vue-router`选择`Y`,项目安装失败,尝试重新安装`vue-cli`
vue init webpack vue-demo

安装后,package.json 文件内容如下

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
{
"name": "vue-demo",
"version": "1.0.0",
"description": "A Vue.js project",
"private": true,
"scripts": {
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"start": "npm run dev",
"test": "npm run unit",
"lint": "eslint --ext .js,.vue src test/unit",
"build": "node build/build.js"
},
"dependencies": {
"vue": "^2.5.2",
"vue-router": "^3.0.1"
},
"devDependencies": {
"autoprefixer": "^7.1.2",
"babel-core": "^6.22.1",
"babel-eslint": "^8.2.1",
"babel-helper-vue-jsx-merge-props": "^2.0.3",
"babel-loader": "^7.1.1",
"babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-plugin-transform-vue-jsx": "^3.5.0",
"babel-preset-env": "^1.3.2",
"babel-preset-stage-2": "^6.22.0",
"chalk": "^2.0.1",
"copy-webpack-plugin": "^4.0.1",
"css-loader": "^0.28.0",
"eslint": "^4.15.0",
"eslint-config-standard": "^10.2.1",
"eslint-friendly-formatter": "^3.0.0",
"eslint-loader": "^1.7.1",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-node": "^5.2.0",
"eslint-plugin-promise": "^3.4.0",
"eslint-plugin-standard": "^3.0.1",
"eslint-plugin-vue": "^4.0.0",
"extract-text-webpack-plugin": "^3.0.0",
"file-loader": "^1.1.4",
"friendly-errors-webpack-plugin": "^1.6.1",
"html-webpack-plugin": "^2.30.1",
"node-notifier": "^5.1.2",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"ora": "^1.2.0",
"portfinder": "^1.0.13",
"postcss-import": "^11.0.0",
"postcss-loader": "^2.0.8",
"postcss-url": "^7.2.1",
"rimraf": "^2.6.0",
"semver": "^5.3.0",
"shelljs": "^0.7.6",
"uglifyjs-webpack-plugin": "^1.1.1",
"url-loader": "^0.5.8",
"vue-loader": "^13.3.0",
"vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.5.2",
"webpack": "^3.6.0",
"webpack-bundle-analyzer": "^2.9.0",
"webpack-dev-server": "^2.9.1",
"webpack-merge": "^4.1.0"
},
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
},
"browserslist": ["> 1%", "last 2 versions", "not ie <= 8"]
}

使用npm run dev即可启动一个 dev 服务

路由

定义

单页面应用指的是只有一个主页面,通过动态替换 DOM 内容并同步修改 url 地址,来模拟多页面应用的效果。切换页面的功能直接由前台脚本来完成,而不是后端渲染完毕后前端只负责显示。

前端路由就是一个前端不同页面的状态管理器,可以不向后台发送请求而直接通过前端技术实现多个页面效果。vue 中的 vue-router,react 的 react-router 均是对这种功能的实现。

实现原理可参考这里

vue-route

我们通过 vue-cli 初始化项目后,main.js 的内容如下

1
2
3
4
5
6
7
8
9
10
11
12
import Vue from "vue";
import App from "./App";
import router from "./router";

Vue.config.productionTip = false;

new Vue({
el: "#app",
router,
components: { App },
template: "<App/>",
});

首先页面会渲染成类似如下

1
2
3
4
5
6
<html>
<head> </head>
<body>
<app></app>
</body>
</html>

这里的<app>即是new Vue()中可选参数 template 所生产的标签, 然后根据 components 中定义的 App 组件,将 app 渲染成 app.vue 中的内容

1
2
3
4
5
6
7
8
9
<html>
<head> </head>
<body>
<div id="app">
<img src="./assets/logo.png" />
<router-view />
</div>
</body>
</html>

其中<router-view>路由视图以用来挂载路由,默认挂载/,那么我们根据main.js中引入的 router,找到其对应的 js 文件。

src/router/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Vue from "vue";
import Router from "vue-router";
import HelloWorld from "@/components/HelloWorld";

Vue.use(Router);

export default new Router({
routes: [
{
path: "/",
name: "HelloWorld",
component: HelloWorld,
},
],
});

根据/路径配置的组件,我们将<router-view>渲染成 HelloWorld 组件的内容

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
<html>
<head> </head>
<body>
<div id="app">
<img src="./assets/logo.png" />
<div class="hello">
<h1>{{ msg }}</h1>
<h2>Essential Links</h2>
<ul>
<li>
<a href="https://vuejs.org" target="_blank"> Core Docs </a>
</li>
<li>
<a href="https://forum.vuejs.org" target="_blank"> Forum </a>
</li>
<li>
<a href="https://chat.vuejs.org" target="_blank">
Community Chat
</a>
</li>
<li>
<a href="https://twitter.com/vuejs" target="_blank"> Twitter </a>
</li>
<br />
<li>
<a href="http://vuejs-templates.github.io/webpack/" target="_blank">
Docs for This Template
</a>
</li>
</ul>
<h2>Ecosystem</h2>
<ul>
<li>
<a href="http://router.vuejs.org/" target="_blank"> vue-router </a>
</li>
<li>
<a href="http://vuex.vuejs.org/" target="_blank"> vuex </a>
</li>
<li>
<a href="http://vue-loader.vuejs.org/" target="_blank">
vue-loader
</a>
</li>
<li>
<a href="https://github.com/vuejs/awesome-vue" target="_blank">
awesome-vue
</a>
</li>
</ul>
</div>
</div>
</body>
</html>

路由跳转

router-link是用来动态切换router-view显示内容组件。

示例

我们新建一个组件src/components/link.vue

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
<template>
<div class="hello">
<h1>{{ msg }}</h1>
</div>
</template>

<script>
export default {
name: "link",
data() {
return {
msg: "测试一下路由链接",
};
},
};
</script>

<style scoped>
h1 {
font-weight: normal;
color: palevioletred;
}

a {
color: red;
}
</style>

在 router 配置中配置 link.vue 的请求路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  import Vue from "vue";
import Router from "vue-router";
import HelloWorld from "@/components/HelloWorld";
+ import link from "@/components/link";

Vue.use(Router);

export default new Router({
routes: [
{
path: "/",
name: "HelloWorld",
component: HelloWorld,
},
+ {
+ path: "/link",
+ name: "link",
+ component: link,
+ },
],
});

修改src/App.vue

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
  <template>
<div id="app">
- <img src="./assets/logo.png">
+ <div>
+ <img src="./assets/logo.png">
+ </div>
+ <div>
+
+ <router-link to="/">主页</router-link>
+ <router-link to="/link">测试router-link</router-link>
+ </div>
<router-view />
</div>
</template>
<script>
export default {
name: 'App'
}
</script>

<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

当我们点击由router-link渲染出来的<a>链接,其下面的router-view对应的元素将会被切换至对应的在路由中配置的组件

我们也可以使用 js 方法来实现跳转

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
  <template>
<div id="app">
<div>
<img src="./assets/logo.png">
</div>
<div>

<router-link to="/">主页</router-link>
<router-link to="/link/123">测试router-link</router-link>
<router-link to="/404">404</router-link>
+ <button @click="handleRouter">按钮点击跳转</button>
</div>
<router-view />
</div>
</template>
<script>
export default {
name: 'App',
+ methods: {
+ handleRouter() {
+ this.$router.push('/link/4567')
+ }
+ }
}
</script>

$router还有其他一些方法($router为 main.js 中声明的 router 变量)

  • replace 它不会向 history 添加新记录,而是替换掉当前的 history 记录。
  • go 类似于 window.history.go(),在 history 记录中向前或后退多少步

路由配置相关

配置默认页面,可以在路由列表的最后新加一项,当访问路径不存在时,重定向到首页。当所有路由都不满足时,就会直接加载最后一项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  export default new Router({
routes: [
{
path: '/',
name: 'HelloWorld',
component: HelloWorld
},
{
path: '/link/:id',

name: 'link',
component: link
},
+ {
+ path: '*',
+ redirect: '/'
+ }
]
})

动态路由

路由列表的 path 也可以带参数,

1
2
3
4
{
path:'/link/:id',
component:link
}

当我们访问/link/1234时,我们可以通过this.$route.params.id的方式取出该参数值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  <template>
<div class="hello">
<h1>{{ msg }}</h1>
+ <h1>{{ $route.params.id}}</h1>
</div>
</template>

<script>
export default {
name: 'link',
data() {
return {
msg: '测试一下路由链接'
}
},
+ mounted() {
+ console.log(this.$route)
+ }
}
</script>

路由钩子

router 提供了导航钩子 beforeEach 和 afterEach,它们会在路由即将改变前和改变后触发。

钩子函数有三个参数

  • to 即将进入的目标的路由对象,在对应的目标页面我们可以用this.$route取出当前路由对象,也就是 to
  • from 当前导航即将离开的路由对象
  • next 调用该方法后,才能进入下一个钩子。next 中还可以设置参数,。设置为 false,可以取消导航,设置为具体的路径可以导航到指定的页面。典型的用法是校验客户是否登录。

例如我们将页面的 title 实时更改为组件的名称

src/main/js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  import Vue from 'vue'
import App from './App'
import router from './router'

Vue.config.productionTip = false

+ router.beforeEach((to, from, next) => {
+ console.log('to', to)
+ console.log('from', from)
+ window.document.title = to.name || "hello"
+ next()
+ })
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})

命名视图

<router-view>可指定 name,来使用指定 name 的组件,路由的路径,可以对应多个组件,默认使用的是 name 为 default 的组件

src/App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div id="app">
<router-view />
<router-view name="input1" />
<router-view name="input2" />
</div>
</template>

<script>
export default {
name: "App",
};
</script>

router/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import Vue from "vue";
import Router from "vue-router";
import input1 from "@/components/input1";
import input2 from "@/components/input2";

Vue.use(Router);

export default new Router({
routes: [
{
path: "/",
name: "HelloWorld",
components: {
default: input1,
input1,
input2,
},
},
],
});

src/components/input1.vue 和 input2.vue,基本一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div class="hello">
input1
<input v-model="value" />
<h1>{{ value}}</h1>
</div>
</template>

<script>
export default {
data() {
return {
value: "",
};
},
};
</script>

当访问/时,三个router-view分别会被渲染为对应的组件。

二级路由

App.vue

1
2
3
4
5
6
7
<html>
<body>
<div id="app">
<router-view />
</div>
</body>
</html>

Home.vue

1
2
3
4
<div>
<router-link to="/child">
<router-view />
</div>

Child

1
<div>the child</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import Vue from "vue";
import Router from "vue-router";
import Home from "@/components/Home";
import Child from "@/components/Child";

export default new Router({
routes: [
{
path: "/",
name: "Home",
component: Home,
children: [
{
path: "child",
component: Child,
},
],
},
],
});

二级路由和命令视图结合

可参考这个demo

app.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<!-- 上下布局 -->
<div id="home">
<router-view class="view header" name="header"></router-view>
<!-- 左右布局 -->
<div class="flex_left_right">
<router-view class="view menu" name="menu"></router-view>
<router-view class="view content" name="content"></router-view>
</div>
</div>
</template>

<script>
export default {};
</script>

router/index.js,省略其他组件的代码

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
import Vue from "vue";
import Router from "vue-router";
import home from "@/pages/home";
import header from "@/components/header";
import menu from "@/components/menu";
import content from "@/components/content";
import list from "@/components/list";
import detail from "@/components/detail";

Vue.use(Router);

export default new Router({
routes: [
{
path: "/",
redirect: "/home/menu",
},
{
path: "/home",
name: "home",
component: home,
children: [
{
path: "menu",
components: {
header: header,
menu: menu,
content: content,
},
children: [
{
path: "list/:index",
name: "list",
component: list,
},
{
path: "list/:index/detail/:detailIndex",
name: "detail",
component: detail,
},
],
},
],
},
{
path: "/detailNoMenu",
name: "detailNoMenu",
component: detail,
},
],
});

其他

在 vue 代码中,我们可以使用

1
2
3
4
5
6
7
8
export default {
computed: {
func() {
console.log(this.$router); //在main.js注册进入Vue的router
console.log(this.$router.options); //在route/index.js中export的实例
},
},
};

使用router-link或使用$router.push时,可以传递 obj,指定跳转到命令的路由中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import Vue from "vue";
import Router from "vue-router";
import Home from "@/components/Home";
import Child from "@/components/Child";

export default new Router({
routes: [
{
path: "/",
name: "Home",
component: Home,
children: [
{
path: "child",
component: Child,
},
],
},
],
});

//App.vue params为路由url参数
this.#router.push({ name: "Home", params: { index: x } });

Vuex

1
npm install --save-dev vuex

新增文件src/store/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

export default new Vuex.Store({
//组件数据,只能读取,不能手动改变
state: {
count: 0,
},
//改变store中数据的唯一途径就是显式提交mutations,mutations可以接受两个参数,第二个参数可以是数字、字符串或对象等类型
mutations: {
increment(state, n = 1) {
state.count += n;
},
},
});

//调用mutations
this.$store.commit("increment");
this.$store.commit("increment", 100);

mutations 也可以接受一个包含 type 的对象

1
2
3
4
5
6
7
8
9
10
mutations: {
increment(state, params) {
state.count += params.count;
},
},
//调用mutations
this.$store.commit({
type:'increment',
count:10
})

vuex 还有三个选项可以使用:getters,actions,modules

getters 一般用来对数据进行处理

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
export default new Vuex.Store({
state: {
list: [1, 5, 7, 10, 30],
},
//getters还可以把getters作为第二个参数
getters: {
filteredList: (state) => {
return state.list.filter((item) => item < 10);
},
listCount: (state, getters) => {
return getters.filteredList.length;
},
//getters可以定义函数式方法,外部调用可以传递参数
filteredListUnderMax(state) {
return function (max) {
return state.list.filter((item) => item < max);
};
},
},
});

//则我们在别的组件中就可以使用
export default {
computed: {
list() {
return this.$store.getters.filteredList;
},
listCount() {
return this.$store.getters.listCount;
},
filter() {
return this.$store.getters.filteredListUnderMax(10);
},
},
};

actions 类似 mutations,但是是异步执行的

modules,它用来将 store 分割到不同的模块中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const moduleA = {
state: {},
mutations: {},
actions: {},
getters: {
//module 中的 mutaion 和 getter 还可以接受第三个参数,来访问根节点
sumCount(state,getters,rootState){
...
}
},
};
const moduleB = {
state: {},
mutations: {},
actions: {},
getters: {},
};

const store = new Vue.Store({
modules: {
a: moduleA,
b: moduleB,
},
});
12…15

lijun

与其感慨路难行,不如马上就出发
145 日志
30 分类
117 标签
© 2019 – 2020 lijun