最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

一文搞懂vue編譯器(DSL)原理

 更新時間:2023年05月04日 09:29:53   作者:Sir蘇  
本文主要介紹了一文搞懂vue編譯器(DSL)原理,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧

源碼下載

什么是DSL

DSL是領(lǐng)域特定語言的縮寫,與JavaScript這種通用語言編譯器相對,它只針對某一個特殊應(yīng)用場景工作

類似中英翻譯,它將源代碼翻譯為目標(biāo)代碼,其轉(zhuǎn)換的標(biāo)準(zhǔn)流程過程包括:詞法分析、語法分析、語義分析、中間代碼生成、優(yōu)化、目標(biāo)代碼生成等,此外,前述流程并非是嚴(yán)格必須的

vue中的DSL

  • 詞法+語法+語義分析
  • 生成token流
  • 生成模板ast
  • 將ast轉(zhuǎn)化為js ast
  • 將ast轉(zhuǎn)化為render函數(shù)
const code = ``
const tokens = tokenize(code) // 詞法+語法+語義分析,生成token流
const tAst = parse(tokens) // 生成ast
const jsAst = transform(tAst) // 將ast轉(zhuǎn)化為jsAst
const renderCode = generate(jsAst) // 將jsAst轉(zhuǎn)化為render函數(shù)

實現(xiàn)思路

  • ast結(jié)構(gòu)定義

首先我們要明確要生成的ast結(jié)構(gòu)是什么樣的,比如如下的模板,div和h1怎么表示,開標(biāo)簽中的屬性怎么區(qū)分,標(biāo)簽的內(nèi)容放在那里等等

<div>
  <h1 v-if="show">我愛前端</h1>
<div>

我們約定:ast是一個樹形結(jié)構(gòu),每一個節(jié)點對應(yīng)一個html元素,該節(jié)點使用ts定義如下:

interface AstNode{
  // 元素類型,是html原生還是vue自定義
  type:string; 
  // 元素名稱,是div還是h1 
  tag:string;
  // 子節(jié)點,h1是div的子節(jié)點
  children:AstNode[];
  // 開標(biāo)簽屬性內(nèi)容
  props:{
    type:string;
    name?:string;
    exp?:{
      ...
    }
    ...
  }[];
}
  • 詞法、語法、語義分析

在工程化中,webpack或vite會幫我們把用戶側(cè)的源代碼拉取過來,我們使用node的readFileSync來代替這一行為

const fs = require('node:fs')
const code =  fs.readFileSync('./vue.txt','utf-8')

有了源代碼,接下來要考慮的就是如何對源碼進行切分,這需要使用到有限狀態(tài)機,即伴隨著源碼的不斷輸入,解析器能夠自動的在不同的狀態(tài)間進行遷移的過程,而有限則意味著狀態(tài)的種類是可枚舉完的

1-模擬源碼不斷輸入

使用while+substring每次刪除一個字符可以模擬字符的輸入

function parse(code){
    while(code.length){
        code = code.substring(1)
    }
}

2-狀態(tài)遷移

我們根據(jù)html標(biāo)簽的書寫規(guī)則來定義狀態(tài)遷移的條件,當(dāng)遇到<時,將狀態(tài)從開始狀態(tài)標(biāo)記為標(biāo)簽開始;伴隨著while循環(huán)的執(zhí)行,首次遇到非空字符時,從標(biāo)簽開始狀態(tài)切換為標(biāo)簽名稱狀態(tài);當(dāng)遇到>時,再從標(biāo)簽名稱狀態(tài)切換為標(biāo)簽初始狀態(tài)。至此形成一個閉環(huán),我們在這一個閉環(huán)內(nèi)記錄下的狀態(tài)集合則稱之為一個token,如圖所示

5d5b306f3de8a2ee8446cba49c494a4.jpg

3-代碼實現(xiàn)

3.1 定義狀態(tài)機的狀態(tài)

const State = {
    // 初始
    initial:1,
    // 標(biāo)簽開始
    start:2,
    // 標(biāo)簽名稱
    startName:3,
    // 標(biāo)簽文本
    text:4,
    // 標(biāo)簽結(jié)束
    end:5,
    // 標(biāo)簽結(jié)束名稱
    endName:6
}

3.2 編寫輔助函數(shù),判斷是否是字符

const isAlpha = function(char){
    return /[a-zA-Z1-6]/.test(char)
}

3.3 實現(xiàn)tokenize函數(shù)

通過while循環(huán)依次取得每一個字符,當(dāng)遇到規(guī)則字符(如<或/或>)時,根據(jù)當(dāng)前所處的狀態(tài)進行狀態(tài)遷移,當(dāng)遷移回初始狀態(tài)時記錄一個token

function tokenize(code){
    let currentState = State.initial
    const tokens = []
    const chars = []
    while(code.length){
        const act = code[0]
        switch(currentState){
            case State.initial:
                if(act === '<'){
                    currentState = State.start
                }else if(isAlpha(act)){
                    currentState = State.text
                    chars.push(act)
                }
                break
            case State.start:
                if(isAlpha(act)){
                    currentState = State.startName
                    chars.push(act)
                }else if(act === '/'){
                    currentState = State.end
                }
                break
            case State.startName:
                if(isAlpha(act)){
                    chars.push(act)
                }else if(act === '>'){
                    // 切到初始狀態(tài),形式閉環(huán),記錄token
                    currentState = State.initial
                    tokens.push({
                        type:'tag',
                        name:chars.join('')
                    })
                    chars.length = 0
                }
                break
            case State.text:
                /**
                 * 1.<div></div>  act = i
                 * 2.<div>我愛前端</div> act = 愛
                 */
                if(isAlpha(act)){
                    chars.push(act)
                }else if(act === '<'){
                    currentState = State.start
                    tokens.push({
                        type:'text',
                        content:chars.join('')
                    })
                    chars.length = 0
                }
                break
            case State.end:
                // 當(dāng)遇到/才會切換到結(jié)束標(biāo)簽狀態(tài)
                if(isAlpha(act)){
                    currentState = State.endName
                    chars.push(act)
                }
                break
            case State.endName:
                if(isAlpha(act)){
                    chars.push(act)
                }else if(act === '>'){
                    currentState = State.initial
                    tokens.push({
                        type:'tagEnd',
                        name:chars.join('')
                    })
                    chars.length = 0
                }
                break
        }
        code = code.substring(1)
    }
    return tokens
}

運行結(jié)果如下:

tokenize執(zhí)行結(jié)果

  • 生成tAst

由于vue是在js下實現(xiàn)的編譯器,并不會創(chuàng)造新的運算符號,所以并不需要進行遞歸下降才能實現(xiàn)ast,我們只需要對上一步生成的tokens進行遍歷掃描即可

1-如何掃描

觀察我們生成的tokens,最先開始的div標(biāo)簽,最后結(jié)束,同時,后進入的h1標(biāo)簽是div標(biāo)簽的子節(jié)點

因此,我們需要初始化一個棧,當(dāng)遇到type為tag的標(biāo)簽時向棧頂壓入一個ast節(jié)點,并將其作為前一個棧頂節(jié)點的子節(jié)點,當(dāng)遇到type為tagEnd時則從棧頂彈出,標(biāo)識一次標(biāo)簽的完整匹配

2-代碼實現(xiàn)

2.1 初始化虛擬根節(jié)點

由于樹形結(jié)構(gòu)必存在根節(jié)點,而html則是多根的,因此我們在代碼里初始化一個根

const root = {
    type:'Root',
    children:[]
}
復(fù)制代碼

2.2 初始化棧

將虛擬根作為默認的棧頂,這樣在掃描實際的tokens時,就能默認作為其子節(jié)點了

const stack = [root]
復(fù)制代碼

2.3 創(chuàng)建節(jié)點

class Node{
    constructor(type,tag){
        this.type = type
        this.tag = tag
        this.children = []
    }
    setContent(content){
        this.content = content
    }
}
復(fù)制代碼

2.4 掃描

依次從tokens中取出,并判斷其type類型,如果是tag則作為子節(jié)點向原棧頂追加,如果是tagEnd則從棧頂彈出

while(tokens.length){
        const p = stack[stack.length - 1]
        const act = tokens.shift()
        switch(act.type){
            case 'tag':{
                const e = new Node('Element',act.name)
                p.children.push(e)
                stack.push(e)
                break
            }
            case 'text':{
                const e = new Node('text')
                e.setContent(act.content)
                p.children.push(e)
                break
            }
            case 'tagEnd':{
                stack.pop()
            }
        }
}

2.5 生成的ast如下

tAst結(jié)果

  • transform

現(xiàn)在,我們已經(jīng)完成了模板的ast化,接下來就是考慮如何將這個模板的ast轉(zhuǎn)化為js ast,這一過程我們稱之為transform,它定義了對ast節(jié)點操作的一系列方法

1-節(jié)點的訪問

節(jié)點操作的前提一定是先拿到這個節(jié)點,因此我們需要能夠遍歷到樹中的每一個節(jié)點

1.1 深度優(yōu)先遍歷

function transform(tAst){
    const children = tAst.children
    if(Array.isArray(children)){
        for(let i=0;i<children.length;i++){
            transform(children[i])
        }
    }
}

1.2 定義訪問操作

如果將訪問操作的代碼內(nèi)置到transform當(dāng)中,則該函數(shù)一定會又臭又長,且不易后續(xù)擴展,因此我們需要將該操作進行提取,ast的訪問應(yīng)該算是訪問者模式的典型應(yīng)用,不過為了保持和vue一致,咱們也采用函數(shù)回調(diào)的方式來實現(xiàn)

function transform(tAst,ctx){
    const act = tAst
    const transforms = ctx.nodeTransforms
    for(let i=0;i<transforms;i++){
        if(typeof transforms[i] === 'function'){
            transforms[i](act,ctx)
        }
    }
    ......
}

2-擴展ctx

在進行節(jié)點操作之前,我們還需要動態(tài)的給ctx掛載一些狀態(tài)信息,用以標(biāo)記當(dāng)前transform的運行狀態(tài),比如當(dāng)前運行的是哪一顆節(jié)點樹、當(dāng)前的節(jié)點樹的父節(jié)點是誰、當(dāng)前節(jié)點的兄弟節(jié)點是誰以及當(dāng)前節(jié)點樹是父節(jié)點的第幾個子節(jié)點

function transform(tAst,ctx){
    // 當(dāng)前的節(jié)點樹
    ctx.act = tAst
    const transforms = ctx.nodeTransforms
    for(let i=0;i<transforms.length;i++){
        if(typeof transforms[i] === 'function'){
            transforms[i](ctx.act,ctx)
        }
    }
    const children = ctx.act.children
    if(Array.isArray(children)){
        // 當(dāng)前節(jié)點的父節(jié)點
        ctx.parent = ctx.act
        for(let i=0;i<children.length;i++){
            // 當(dāng)前節(jié)點樹是父節(jié)點的第幾個子節(jié)點
            ctx.index = i
            // 當(dāng)前節(jié)點的兄弟節(jié)點
            ctx.siblings = [arr[i-1],arr[i+1]].map(v=>v||null)
            transform(children[i],ctx)
        }
    }
}

3-節(jié)點替換

至此,我們的transform的主框架就搭好了,要實現(xiàn)節(jié)點替換就只需要在nodeTransforms中增加處理函數(shù)即可,比如我們將h1標(biāo)簽替換為p標(biāo)簽

function _replaceNode(newNode){
    this.act = newNode
    this.parent.children[this.index] = newNode
}
function transformElement(node,ctx){
    if(node.type === 'Element'){
        switch(node.tag){
            case 'h1':
                _replaceNode.call(ctx,{
                    type:'Element',
                    tag:'p',
                    children:node.children
                })
                break
        }
    }
}

4-等待子節(jié)點處理完畢

目前的實現(xiàn)中,在對當(dāng)前節(jié)點進行處理時,其子節(jié)點一定還未被處理,但在實際需求中,往往需要等子節(jié)點處理完畢后再根據(jù)其執(zhí)行結(jié)果決定如何處理當(dāng)前節(jié)點,因此需要對transform進行改進

我們?yōu)閚odeTransforms設(shè)計一個返回值,該值是一個函數(shù),當(dāng)正向訪問結(jié)束后,使用該返回函數(shù)做反向遍歷即可

function transform(tAst,ctx){
    // 退出回調(diào)列表
    const cbs = []
    ...
    const cb = transforms[i](ctx.act,ctx)
    if(typeof cb === 'function'){
       cbs.push(cb)
    }
    ...
    // 退出
    let i = cbs.length
    while(i--){
        cbs[i]()
    }
}

5-生成js ast

由于我們最終的產(chǎn)物是一個render函數(shù),因此需要將模板ast轉(zhuǎn)換為js ast,以前文的模板為例

<div>
 <h1>123</h1>
</div>

其對應(yīng)的render函數(shù)如下

function render(){
  return h('div',h('h1','123'))
}

5.1 ast節(jié)點類型

在模板的ast節(jié)點定義時,我們把一個元素節(jié)點視為一個ast節(jié)點,而在JavaScript中,則為一條js語句等同于一個ast節(jié)點

觀察render函數(shù)的js代碼,不難發(fā)現(xiàn),其由函數(shù)定義、函數(shù)參數(shù)和函數(shù)返回值三部分構(gòu)成,同樣的,我們使用type來標(biāo)記其類型

另外,我們的目標(biāo)代碼是明確的,并非所有的js語句,因此,我們可以定義任何的type名稱來做專屬標(biāo)識,比如我就想使用Function來表示render函數(shù),使用ReturenCb來表示h函數(shù)......

本文,使用FunctionDecl+id.name標(biāo)識render函數(shù);params標(biāo)識render函數(shù)的參數(shù);body標(biāo)識render函數(shù)的函數(shù)體,由于函數(shù)體內(nèi)又可能存在多個js語句,因此它被設(shè)計為一個數(shù)組,最后使用ReturnStatement標(biāo)識return語句,其返回的是一個h函數(shù),而參數(shù)使用arguments標(biāo)記

{
  type:'FunctionDecl',
  id:{
      type:"Identifier",
      name:"render"
  },
  params:[],
  body:[
    {
      type:"ReturnStatement",
      return:{
        type:"CallExpression",
        callee:{
          type:"Identifier",
          name:"h"
        },
        arguments:[
          {
            type:"StringLiteral",
            value:"div"
          },
          {
            type:"ArrayExpression",
            elements:[
               //CallExpression類型,
              //CallExpression類型,
            ]
          }
        ]
      }
    }
  ]
}

5.2 定義類型生成器

編寫一個newType函數(shù)用于統(tǒng)一處理各種節(jié)點類型的生成

function _newType(type,value,arguments){
    const o = {
        type,
    }
    switch(type){
        case 'StringLiteral':
            o.value = value
            break
        case 'Identifier':
            o.name = value
            break
        case 'ArrayExpression':
            o.elements = value
            break
        case 'CallExpression':
            o.callee = _newType('Identifier',value)
            o.arguments = arguments
            break
    }
    return o
}

5.3 為tAst添加jsCode屬性收集當(dāng)前節(jié)點的轉(zhuǎn)換結(jié)果

5.4 重新實現(xiàn)transformElement函數(shù)

對當(dāng)前語句的處理必須等到子節(jié)點轉(zhuǎn)換完畢,因為只有此時jscode才是可用的

function transformElement(node,ctx){
    return ()=>{
        if(node.type === 'Element'){
        }
    }
}

從5.1的節(jié)點類型定義可以知道,每一個節(jié)點本質(zhì)上都是一個h函數(shù)

const callee = _newType('CallExpression','h',[
      _newType('StringLiteral',node.tag)
    // 參數(shù)二取決于子節(jié)點的數(shù)量,需要動態(tài)生成
])

生成參數(shù)二

node.children.length === 1 
            ? callee.arguments.push(node.children[0].jsCode)
            : callee.arguments.push(node.children.map(v=>v.jsCode))

最后將當(dāng)前節(jié)點的轉(zhuǎn)換結(jié)果掛載到j(luò)sCode

node.jsCode = callee

5.5 新增transformRoot函數(shù)

至此,我們已經(jīng)完成了對實際模板節(jié)點的轉(zhuǎn)化,即

<div>
 <h1>123</h1>
</div>

轉(zhuǎn)為了

h('div',[
  h('h1','123')
])

因此我們還需要處理生成render函數(shù),而這正好與我們在一開始生成的虛擬根節(jié)點相對應(yīng)

function transformRoot(node){
    return ()=>{
        if(node.type === 'Root'){
            node.jsCode = {
                type:"FunctionDecl",
                id:_newType("Identifier","render"),
                params:[],
                body:[
                    {
                        type:"ReturnStatement",
                        return:node.children[0].jsCode
                    }
                ]
            }
        }
    }
}

6-轉(zhuǎn)換結(jié)果如下

transform的最終轉(zhuǎn)換結(jié)果

  • 生成目標(biāo)代碼

我們在前一部分已經(jīng)為根節(jié)點添加了jsCode屬性,該屬性就是tAst所對應(yīng)的jsAst,因此我們只需要找到每一個節(jié)點并將他們轉(zhuǎn)化成字符串進行拼接就可以了

1-定義上下文

我們說了,代碼生成本質(zhì)上是在做字符串的拼接工作,為此我們將拼接時出現(xiàn)頻率較大的函數(shù)定義在上下文中以方便復(fù)用,其中的code就是我們最終生成的代碼的容器,而newLine則更多是為了生成代碼的可讀性

const ctx = {
    code: "",
    append(code) {
        this.code += code;
    },
    newLine(indent = 1) {
        this.code += "\n" + " ".repeat(indent * 2);
    },
};

2-定義類型生成函數(shù)

2.1 首先我們將每一種js節(jié)點類型所對應(yīng)的生成函數(shù)放到一個統(tǒng)一的genMap中

const genMap = new Map([
    ['FunctionDecl',genFunctionDecl],
    ['ReturnStatement',genReturnStatement],
    ['StringLiteral',genStringLiteral],
    ['CallExpression',genCallExpression],
    ['ArrayExpression',genArrayExpression]
])

2.2 對參數(shù)的遍歷生成單獨做一個genParams

function genParams(nodes,ctx) {
    nodes.forEach((v,i)=>{
        genMap.get(v.type)(v,ctx)
        if(i<nodes.length-1){
            ctx.append(',')
        }
    })
}

2.3 分別實現(xiàn)

分別對函數(shù)名稱、函數(shù)參數(shù)、函數(shù)體做生成,他們都在節(jié)點中有著一一對應(yīng)的節(jié)點屬性

2.3.1 genFunctionDecl

代碼實現(xiàn)

function genFunctionDecl(node, ctx) {
    ctx.append(`function ${node.id.name}(`);
    genParams(node.params, ctx);
    ctx.append("){");
    ctx.newLine();
    node.body.forEach((v) => genMap.get(v.type)(v, ctx));
    ctx.newLine(0)
    ctx.append('}')
}

生成結(jié)果

genFunctionDecl

2.3.2 genReturnStatement

該函數(shù)就是為code拼接return字符,至于真正的函數(shù)體,是由genCallExpression完成的

代碼實現(xiàn)

function genReturnStatement(node,ctx) {
    ctx.append('return ')
    genMap.get(node.return.type)(node.return,ctx)
}

生成結(jié)果

genReturnStatement

2.3.3 genCallExpression

代碼實現(xiàn)

function genCallExpression(node,ctx) {
    ctx.append(`${node.callee.name}(`)
    genParams(node.arguments,ctx)
    ctx.append(')')
}

生成結(jié)果

genCallExpression

2.3.4 genStringLiteral

代碼實現(xiàn)

function genStringLiteral(node,ctx) {
    ctx.append(`'${node.value}'`)
}

生成結(jié)果

genStringLiteral

2.3.5 genArrayExpression

目前在我們的示例中是不存在該類型的,因此我們將模板源碼做下調(diào)整

<div>
 <h1>123</h1>
 <h2>456</h2>
</div>

代碼實現(xiàn)

function genArrayExpression(node,ctx) {
    ctx.append('[')
    genParams(node.elements,ctx)
    ctx.append(']')
}

生成結(jié)果

genArrayExpression

3-最終的完整生成結(jié)果

代碼生成

 到此這篇關(guān)于一文搞懂vue編譯器(DSL)原理的文章就介紹到這了,更多相關(guān)vue編譯器DSL內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Vue.js前端框架之事件處理小結(jié)

    Vue.js前端框架之事件處理小結(jié)

    這篇文章主要介紹了Vue.js前端框架之事件處理小結(jié),本文給大家介紹了v-on 指令的基本用法,通過實例講解的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2021-04-04
  • 詳解vue3的沙箱機制

    詳解vue3的沙箱機制

    這篇文章主要介紹了vue3的沙箱機制的相關(guān)資料,幫助大家更好的理解和學(xué)習(xí)使用vue框架,感興趣的朋友可以了解下
    2021-04-04
  • Vue插件打包與發(fā)布的方法示例

    Vue插件打包與發(fā)布的方法示例

    插件功能開發(fā)完成后,若需要發(fā)布到公共組件庫中(例如:npmjs),需要對插件進行打包并發(fā)布,這篇文章主要介紹了Vue插件打包與發(fā)布的方法示例,感興趣的小伙伴們可以參考一下
    2018-08-08
  • 基于vue循環(huán)列表時點擊跳轉(zhuǎn)頁面的方法

    基于vue循環(huán)列表時點擊跳轉(zhuǎn)頁面的方法

    今天小編就為大家分享一篇基于vue循環(huán)列表時點擊跳轉(zhuǎn)頁面的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2018-08-08
  • vue2實現(xiàn)搜索結(jié)果中的搜索關(guān)鍵字高亮的代碼

    vue2實現(xiàn)搜索結(jié)果中的搜索關(guān)鍵字高亮的代碼

    這篇文章主要介紹了vue2實現(xiàn)搜索結(jié)果中的搜索關(guān)鍵字高亮的代碼,需要的朋友可以參考下
    2018-08-08
  • 深入了解Vue動態(tài)組件和異步組件

    深入了解Vue動態(tài)組件和異步組件

    這篇文章主要介紹了深入了解Vue動態(tài)組件和異步組件的相關(guān)資料,幫助大家更好的理解和學(xué)習(xí)vue組件的相關(guān)知識,感興趣的朋友可以了解下
    2021-01-01
  • Vue實現(xiàn)簡單購物車小案例

    Vue實現(xiàn)簡單購物車小案例

    這篇文章主要為大家詳細介紹了Vue實現(xiàn)簡單購物車小案例,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2021-10-10
  • axios請求的一些常見操作實戰(zhàn)指南

    axios請求的一些常見操作實戰(zhàn)指南

    axios是一個輕量的HTTP客戶端,它基于XMLHttpRequest服務(wù)來執(zhí)行 HTTP請求,支持豐富的配置,支持Promise,支持瀏覽器端和 Node.js 端,下面這篇文章主要給大家介紹了關(guān)于axios請求的一些常見操作,需要的朋友可以參考下
    2022-09-09
  • Vue監(jiān)聽使用方法和過濾器實現(xiàn)

    Vue監(jiān)聽使用方法和過濾器實現(xiàn)

    這篇文章主要介紹了Vue監(jiān)聽使用方法和過濾器實現(xiàn),過濾器為頁面中數(shù)據(jù)進行強化,具有局部過濾器和全局過濾器
    2022-06-06
  • Vue?項目中Echarts?5使用方法詳解

    Vue?項目中Echarts?5使用方法詳解

    這篇文章主要為大家介紹了Vue?項目中Echarts?5使用方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-11-11

最新評論

北碚区| 澄城县| 咸宁市| 南皮县| 莲花县| 长顺县| 仙游县| 集贤县| 柳林县| 井冈山市| 淳安县| 蚌埠市| 万盛区| 浮山县| 万源市| 彭山县| 临澧县| 南召县| 隆化县| 大渡口区| 永德县| 仙游县| 榆中县| 苏尼特右旗| 桐柏县| 闵行区| 洪泽县| 和硕县| 凭祥市| 齐齐哈尔市| 芦山县| 克山县| 玉林市| 清丰县| 静乐县| 莎车县| 固安县| 清水河县| 连云港市| 松江区| 扶风县|