Skip to content
微信公众号

设计思路与原理

设计思路

Mars 框架的设计思路是将跨多端的应用拆分为逻辑层和视图层,逻辑层采用同一套核心运行时进行数据驱动以及生命周期管理,视图层使用同一套模板语法,经过编译转换为特定平台的视图语言。

框架原理

考虑到学习成本、生态完善程度以及在多端上的扩展性、业务场景等原因,我们选择了 Vue 技术栈,采用 Vue 单文件组件和模板语法来书写组件代码,引入标准基础组件和 API 规范和标准生命周期规范。

在此开发规范之上,基于 Vue 的模板语法和基础组件来构建视图层,基于 Vue 数据驱动及标准生命周期规范来构建逻辑层,实现多端运行。框架总体原理图如下:

小程序运行时原理图:

编译和构建:

H5运行时原理图:

代码结构

Mars仓库地址为:https://github.com/max-team/Mars,其中代码结构如下:

docs                // 文档目录
packsges
    |- mars-build   // 编译相关代码
    |- mars-core    // 运行时代码
    |- mars-cli     // CLI 代码
    |- mars-cli-template  // CLI Service 代码
    |- mars-api     // 适配 H5 的 API 代码

模版

如果我们对比小程序和Vue模版,会发现他们与html语法是十分相似的。区别只在于标签上属性值的写法,相互之间通过编译是可以转化的。我们可以在编译阶段由Vue模版编译到小程序模版。

Vue模版如下:

html
<!--index.wxml-->
<template class="container">
    <view class="userinfo">
        <button v-if="!hasUserInfo&&canIUse" open-type="getUserInfo">获取头像昵称</button>
        <block v-else>  
            <image @tap="bindViewTap" class="userinfo-avatar" src=""/>
            <text class="userinfo-nickname">{{userInfo.nickName}}</text>
        </block>
    </view>
    <view class="usermotto">
        <text class="user-motto">{{motto}}</text>
    </view>
</template>

小程序的模版内容如下:

html
<!--index.wxml-->
<view class="container">
    <view class="userinfo">
        <button wx:if="{{!hasUserInfo&&canIUse}}" open-type="getUserInfo">获取头像昵称</button>
        <block wx:else>  
            <image bindtap="bindViewTap" class="userinfo-avatar" src=""/>
            <text class="userinfo-nickname">{{userInfo.nickName}}</text>
        </block>
    </view>
    <view class="usermotto">
        <text class="user-motto">{{motto}}</text>
    </view>
</view>

逻辑

逻辑部分,小程序与Vue在书写方式上有很大差异,他们的逻辑代码在各自的运行时中执行。并且逻辑部分用户书写的灵活度是很大的,没有办法通过编译将Vue的逻辑编译成小程序的逻辑去执行。那该怎么办呢,我们不如换一种思路。Vue运行时和Vue组件的逻辑在生产中都是以JS代码执行的,在小程序提供的环境中是可以执行的。我们可以让Vue运行时也可以在小程序中执行,这样开发者编写的Vue逻辑代码也可以在小程序中执行了。

Vue逻辑如下

js
<script>
    export default{
        data(){},
        methods:{
            bindViewTap(){},
            getUserInfo(){}
        },
        mounted(){}
    }
</script>

小程序的逻辑如下:

js
Page({
    data:{},
    bindViewTap: function(){},
    onLoad: function(){},
    getUserInfo: function(){}
})

数据

数据部分是最简单的,因为数据是以JS对象的形式存在的,在小程序和Vue中是相同的。

通过对视图、逻辑和数据这三个部分的分析,我们可以使用以下思路来使用Vue开发小程序。

首先将Vue template部分编译成小程序的模版,之后在小程序逻辑部分运行整个Vue的运行时,以及开发者编写的逻辑代码。最后Vue数据发生变化时同步给小程序,触发视图刷新。

我们需要在编译阶段产出 .wxml、.css、.js以及.json文件,在template部分需要将v-bind等语法转换成小程序使用的格式。样式内容则可以直接提取出来作为css文件。我们会在Vue中规定一个字段作为配置,这部分配置会提取出来作为.json文件。

而对于js部分,由于我们的逻辑执行在Vue中,因此只需要用到小程序的生命周期,在生命周期中执行Vue运行时以及业务逻辑代码就可以了。

例如,Vue单文件组件内容如下:

vue
<template>
    <view class="home-wrap">
        <navigator :url="item.bookApi" v-for="(item,index) in bookList">
            <book :poster="item.poster"></book>
        </navigator>
    </view>
</template>
<script>
import Book from 'components/Book/index';
export default{
    config:{
        navigationBarTitleText: "标题"
    },
    data(){},
    components:{
        book:Book
    }
}
</script>
<style>
    .home-wrap{
        width:100vw;
        height:100vh;
    }
</style>

编译成小程序的组件内容如下:

html
<!--wxml模板内容-->
 <view class="home-wrap">
        <navigator url="{{item.bookApi}}" v-for="(item,index) in bookList">
            <book poster="{{item.poster}}" compId="{{ (compId ? compId : '$root') + ',0' }}"></book>
        </navigator>
</view>
css
 /*wxss样式内容*/
 .home-wrap{
        width:100vw;
        height:100vh;
    }
js
/*json配置内容*/
{
        "navigationBarTitleText": "标题",
        "usingComponents":{"book":"../../components/Book/index"}
},
js
//js逻辑内容
import {createPage} from "../../mars-core/index"
import Book from "../../components/Book/index.vue"
Page(createPage({
    data(){},
    components:{
        book:Book
    }
}))

为了执行Vue运行时以及业务逻辑代码,我们需要在小程序中创建Vue实例,Vue在生产环境中是以JS代码来运行的。因此我们可以直接将Vue引入,然后在小程序onLoad阶段new一个Vue实例出来。

js
import Vue from 'vue'

Page({
    onLoad(){
        const vm = new Vue(options)
        this.$vue = vm
    }
})

但是要注意,Vue正常是要执行在浏览器中的,在执行时会进行DOM操作完成页面渲染,在小程序中我们需要将Vue进行DOM操作的部分删掉。做到这里模版已经有了,样式也有了,创建了Vue实例后逻辑也可以执行了,但到目前为止,小程序与Vue也没有真正联系上。

通过之前的分析我们了解到,小程序与Vue之间是通过数据来联系的,Vue中执行逻辑,修改数据,将数据变化同步给小程序,触发试图更新。因此,我们现在要做的就是在每次Vue中更新视图时,把数据修改同步给小程序,那么如何知道Vue中的逻辑执行造成了视图刷新了呢?

我们可以使用Vue的updated钩子函数。

js
const vueMixin = {
    updated(){
        setData(vm,this)
    }
}

updated钩子函数会在数据发生变化导致视图刷新后触发。我们可以在其中调用小程序的setData方法,来将变化后的数据同步给小程序,现在我们在Vue和小程序之间建立了联系。但这个联系还是单向的,Vue的数据变化可以修改小程序的视图。但小程序中用户的操作还不能传递给Vue进行处理。用户的操作体现在tap等事件中,由于我们所有的逻辑都在Vue中,因此需要让Vue接管小程序的事件处理。

我们可以在小程序的模版中去设置一个代理函数handleProxy,在这个事件代理函数中,调用Vue实例中的事件处理函数,触发开发者编写的业务处理逻辑。这样用户的操作通过事件代理传递给Vue进行处理,Vue处理过程中会修改数据,触发VirtualDom的更新,VirtualDom更新后会触发updated钩子函数,我们在updated钩子函数中将数据变化同步给小程序,使得小程序视图更新,完成了整个用户操作响应流程。

现在我们已经完成了Vue与小程序结合的整体结构,视图绘制发生在小程序中,业务逻辑运行在Vue中,小程序与Vue用事件和数据来进行通信。

组件机制原理也是一样的,视图依旧由小程序组件来绘制,业务逻辑运行在Vue组件中,小程序组件与Vue组件通过事件和数据来进行通信。

但是这么做的前提是我们需要将小程序组件与Vue组件关联起来,在我们创建Vue实例时有两种选择,一种是我们只在小程序根组件也就是Page中去创建Vue实例,Vue会继续创建组件实例。

在这种情况下小程序组件和Vue组件的创建分别是同时进行的,那么我们就需要将小程序组件与Vue组件之间进行关联匹配,否则他们之间的通信也就无从谈起了。

那么如何匹配呢?我们可以给每个组件都标记一个唯一的ID,然后通过ID来进行匹配。标记的方法就是从根组件开始,将根组件标记为$root,那么它的子组件就是$root.0,$root.1等,不同层级间使用.来分割,其中列表循环特殊对待,我们使用横线来标记循环项。

例如root.1中有一个循环列表,这个循环列表中渲染了一个组件这个组件自身ID位root.1.0,然后循环产生的第一个子组件就是root.1.0-0,第二个就是root.1.0-1,这样我们通过ID给每个组件增加了标记,将相同ID的小程序组件与Vue组件匹配在一起。

另一种是我们去掉Vue创建组件实例的逻辑,自己在每个小程序组件创建时new一个Vue实例,但如果这么做,我们需要自己维护Vue各个实例间的父子关系。

本站总访问量次,本站总访客数人次
Released under the MIT License.