uni-app 编译时源码解析

周末在家里闲来无事,就想看一看 uni-app 是如何编译 vue 文件的,翻开 uni-app 源码,这个代码仓库内容非常庞大,但是今天我只对小程序编译感兴趣。翻开目录结构,我完全找不到东南西北,因为内容实在太多了,不知道哪一个文件夹才是我要找的。

每次看到这些目录结构就不想看下去了,闲着干点其他的事情不好吗?刷刷抖音,逛逛 b 站,但是这些都会让我无比的空虚,看完之后会觉得自己啥也没干,不是吗?于是我又犯贱地翻开了源码目录,哪怕每一次看一个文件夹,多来几次也能搜索到核心代码。将我的搜索结果列出来如下:

css 复制代码
├── global.d.ts                     全局类型文件
├── playground                      示例代码
├── shims-node.d.ts                 .d.ts类型文件
├── shims-uni-app.d.ts
├── shims-vue-runtime.d.ts
├── shims-vue.d.ts
├── size-check                      用来检测运行时源码体积是否超标
├── uni-api                         导出所有原生方法的定义
├── uni-app                         uni-app 开头的是 原生 app 源代码
├── uni-app-plus
├── uni-app-uts
├── uni-app-vite
├── uni-app-vue
├── uni-automator
├── uni-cli-shared                  uni-cli 开头的是 cli 工具的源代码
├── uni-cli-utils
├── uni-cloud                       uni 云服务
├── uni-components                  公共组件
├── uni-core
├── uni-h5                          uni-h5 开头的是 web 端的源代码
├── uni-h5-vite
├── uni-h5-vue
├── uni-i18n
├── uni-mp-alipay                   uni-app 开头的是小程序端源代码
├── uni-mp-baidu
├── uni-mp-compiler                 小程序编译时源码
├── uni-mp-core                     小程序运行时源码
├── uni-mp-jd
├── uni-mp-kuaishou
├── uni-mp-lark
├── uni-mp-qq
├── uni-mp-toutiao
├── uni-mp-vite
├── uni-mp-vue
├── uni-mp-weixin
├── uni-mp-xhs
├── uni-nvue-styler
├── uni-push
├── uni-quickapp-webview
├── uni-shared
├── uni-stacktracey
├── uni-stat
├── uni-uts-v1
├── uni-vue
├── uni-vue-devtools
├── uts
├── uts-darwin-arm64
├── uts-darwin-x64
├── uts-linux-x64-gnu
├── uts-linux-x64-musl
├── uts-win32-ia32-msvc
├── uts-win32-x64-msvc
└── vite-plugin-uni 

显而易见,本文要讨论的就是 uni-mp-compiler

编译器一般运行流程

众所周知,编译器一般的运行流程都是这样的:

graph TD Parse --> Transform ---> Generate

Parse阶段:先对源代码进行词法分析,分解为 token,然后对 token 进行语法分析,得到 ast,也就是抽象语法树

Transform阶段:把所有需要转换的情况穷举出来,然后遍历 ast 每一个节点进行对应的转换

Generate阶段:对转换后的 ast 节点进行遍历,转换回代码

显然 uni-app 的编译过程也遵循这三个步骤,这一点从目录结构中就能够看出来:

css 复制代码
├── ast.ts
├── codegen.ts
├── compile.ts                  Parse 阶段
├── decodeHtml.ts
├── errors.ts
├── identifier.ts
├── index.ts
├── namedChars.json
├── options.ts
├── parserOptions.ts
├── runtimeHelpers.ts
├── template
│   └── codegen.ts              Generate阶段
├── transform.ts            
└── transforms                  Transform阶段

Parse阶段

Parse 阶段直接引用的@vue/compiler-core这个包来进行解析,跟着我翻开这个包的源码,第一个映入眼帘的就是Tokenizer,它就是用来做词法分析的,利用到了状态机,状态机的三大要素就是状态、事件、响应

这里相当于穷举了所有的状态,并且写出了所有的状态转换逻辑,具体的业务逻辑就不再深入,感兴趣的可以自行阅读;

类比一下 babel,babel 的 Parser 也是使用状态机来做词法分析的:github1s.com/babel/babel...

语法分析阶段 babel 用的是 estree,这些了解一下即可;

总结一下:如果要编译 vue 文件直接使用@vue/compiler-core即可,如果只是编译 js 那么就可以用@babel/core

Transform阶段

上一个阶段已经拿到了抽象语法树,这一阶段的主要任务就是写业务逻辑,那就是把 vue 中的语法转化为小程序的语法,首先要转换 vue 的内置指令:

vText: 删除 vText 这个 props,然后追加到 children 中去

js 复制代码
const transformText = (node, _) => {
    if (!(0, uni_cli_shared_1.isElementNode)(node)) {
        return;
    }
    const dir = (0, compiler_core_1.findDir)(node, 'text');
    if (!dir) {
        return;
    }
    // remove v-text
    node.props.splice(node.props.indexOf(dir), 1);
    if (node.tagType !== 0 /* ElementTypes.ELEMENT */) {
        return;
    }
    node.isSelfClosing = false;
    node.children = [
        {
            type: 5 /* NodeTypes.INTERPOLATION */,
            loc: dir.exp.loc,
            content: dir.exp,
        },
    ];
};

vHtml:将 vHtml 这个 props 转换为 rich-text 这个子元素

js 复制代码
{
        tag: 'rich-text',
        type: 1 /* NodeTypes.ELEMENT */,
        tagType: 0 /* ElementTypes.ELEMENT */,
        props: [(0, uni_cli_shared_1.createBindDirectiveNode)('nodes', dir.exp || '')],
        isSelfClosing: true,
        children: [],
        codegenNode: undefined,
        ns: node.ns,
        loc: node.loc,
    };
    // 转换前的 props
    {
    "type": 7,
    "name": "html",
    "exp": {
        "type": 4,
        "content": "html.value",
        "isStatic": false,
        "constType": 0,
        "loc": {
            "start": {
                "column": 19,
                "line": 7,
                "offset": 164
            },
            "end": {
                "column": 23,
                "line": 7,
                "offset": 168
            },
            "source": "html"
        }
    },
    "modifiers": [],
    "loc": {
        "start": {
            "column": 11,
            "line": 7,
            "offset": 156
        },
        "end": {
            "column": 24,
            "line": 7,
            "offset": 169
        },
        "source": "v-html=\"html\""
    }
}
// 转换后
{
    "tag": "rich-text",
    "type": 1,
    "tagType": 0,
    "props": [
        {
            "type": 7,
            "name": "bind",
            "modifiers": [],
            "loc": {
                "source": "",
                "start": {
                    "line": 1,
                    "column": 1,
                    "offset": 0
                },
                "end": {
                    "line": 1,
                    "column": 1,
                    "offset": 0
                }
            },
            "arg": {
                "type": 4,
                "loc": {
                    "source": "",
                    "start": {
                        "line": 1,
                        "column": 1,
                        "offset": 0
                    },
                    "end": {
                        "line": 1,
                        "column": 1,
                        "offset": 0
                    }
                },
                "content": "nodes",
                "isStatic": true,
                "constType": 3
            },
            "exp": {
                "type": 4,
                "content": "html.value",
                "isStatic": false,
                "constType": 0,
                "loc": {
                    "start": {
                        "column": 19,
                        "line": 7,
                        "offset": 164
                    },
                    "end": {
                        "column": 23,
                        "line": 7,
                        "offset": 168
                    },
                    "source": "html"
                }
            }
        }
    ],
    "isSelfClosing": true,
    "children": [],
    "ns": 0,
    "loc": {
        "start": {
            "column": 5,
            "line": 7,
            "offset": 150
        },
        "end": {
            "column": 32,
            "line": 7,
            "offset": 177
        },
        "source": "<view v-html=\"html\"></view>"
    }
}

可能直接看这个 ast 有点疑惑,那么我来转换成代码:源码为<view v-html=\"html\"></view>,转换后<view><rich-text v-bind.nodes=\"html\"/></view>,是不是一目了然?

vSlot:插槽,又分为具名插槽和作用域插槽,具名插槽是这样转换的:

html 复制代码
// 转换前
<custom>
    <template v-slot:header/>
    <template v-slot:default/>
    <template v-slot:footer/>
</custom>

// 转换后
<custom u-s="{{['header','d','footer']}}" u-i="2a9ec0b0-0">
    <view slot="header"/>
    <view/>
    <view slot="footer"/>
</custom>

可以看到转换之后把 template 改成了 view 组件,然后标注了三个插槽的名称,后面就交给运行时处理了

作用域插槽就没有上面那么简单了,它不仅编译出来了 wxml 还编译出了 js,它使用 v-for 来实现作用域插槽:

html 复制代码
// 源代码
<custom>
    <template v-slot:default="slotProps">
        <view>{{ slotProps.item }}</view>
    </template>
</custom>

// 编译出来的 wxml
<custom u-s="{{['d']}}" u-i="2a9ec0b0-0">
    <view wx:for="{{a}}" wx:for-item="slotProps" wx:key="b" slot="{{slotProps.c}}">
        <view>{{slotProps.a}}</view>
    </view>
</custom>

// 编译出来的 js
(_ctx,_cache)=>{
    return {
        a: _w((slotProps,s0,i0)=>{
            return {
                a: _t(slotProps.item),
                b: i0,
                c: s0
            };
        }
        , {
            name: 'd',
            path: 'a',
            vueId: '2a9ec0b0-0'
        })
    }
}

vOn:和上面一样也会编译为两个文件,<view v-on:click="onClick"/>会被编译为<view bindtap="{{a}}"/>以及(_ctx, _cache) => { return { a: _o(_ctx.onClick) } }

vIf和 vFor都比较简单直接替换为 wx:if和wx:for就行了

Generate阶段

上面的示例除了第一个,我用的都是 Generate 之后的代码了,那么再来看看怎么从 ast 转化为代码呢。答案依然是穷举,把所有的情况都要列举出来然后一一转换。

可以看到是不断地循环然后执行了 genElement 最终把完整的 wxml 拼接出来了;

相关推荐
竣子好逑39 分钟前
uniapp 微信小程序 数据空白展示组件
微信小程序·小程序·uni-app
清风路遥7 小时前
【婚庆摄影小程序设计与实现】
微信小程序·毕业设计·springboot·课程设计
潜意识起点9 小时前
微信小程序 app.json 配置文件解析与应用
微信小程序·小程序·json
计算机徐师兄16 小时前
Java基于SSM框架的无中介租房系统小程序【附源码、文档】
java·微信小程序·小程序·无中介租房系统小程序·java无中介租房系统小程序·无中介租房微信小程序
源码哥_博纳软云16 小时前
JAVA智慧养老养老护理帮忙代办陪诊陪护小程序APP源码
java·开发语言·微信小程序·小程序·微信公众平台
孙 悟 空1 天前
uni-app:监听页面返回,禁用返回操作
前端·javascript·uni-app
美美的海顿1 天前
springboot基于Java的校园导航微信小程序的设计与实现
java·数据库·spring boot·后端·spring·微信小程序·毕业设计
罗狮粉 991 天前
docker部署微信小程序自动构建发布和更新
docker·微信小程序·notepad++
mosen8681 天前
uniapp中uni.scss如何引入页面内或生效
前端·uni-app·scss
lyz2468591 天前
uniapp popup弹窗组件的自定义使用方法
uni-app