• 像VUE一样写微信小程序-深入研究wepy框架


    像VUE一样写微信小程序-深入研究wepy框架

     

    微信小程序自发布到如今已经有半年多的时间了,凭借微信平台的强大影响力,越来越多企业加入小程序开发。 小程序于M页比相比,有以下优势: 

    1、小程序拥有更多的能力,包括定位、录音、文件、媒体、各种硬件能力等,想象空间更大 

    2、运行在微信内部,体验更接近APP

    3、在过度竞争的互联网行业中,获取一个有效APP用户的成本已经非常高了,小程序相比APP更加轻量、即用即走, 更容易获取用户

    开发对比

    从开发角度来讲,小程序官方封装了很多常用组件给开发带来很多便利性,但同时也带来很多不便: 

    1、小程序重新定义了DOM结构,没有window、document、div、span等,小程序只有view、text、image等 封装好的组件,页面布局只能通过这些基础组件来实现,对开发人员来讲需要一定的习惯转换成本 

    2、小程序不推荐直接操作DOM(仅仅从2017年7月开始才可以获取DOM和部分属性),如果不熟悉MVVM模式的开发者, 需要很高的学习成本

    3、小程序没有cookie,只能通过storage来模拟各项cookie操作(包括http中的setCookie也需要自行处理)

    wepy

    笔者团队最近开发了多个微信小程序,为了弥补小程序各项不足和延续开发者VUE的开发习惯,团队在开发初期 就选用了wepy框架,该框架是腾讯内部基于小程序的开发框架,设计思路基本参考VUE,开发模式和编码风 格上80%以上接近VUE,开发者可以以很小的成本从VUE开发切换成小程序开发,相比于小程序,主要优点如下:

    1、开发模式容易转换 wepy在原有的小程序的开发模式下进行再次封装,更贴近于现有MVVM框架开发模式。框架在开发过程中参考了 一些现在框架的一些特性,并且融入其中,以下是使用wepy前后的代码对比图。

    官方DEMO代码:

    1.  1 /index.js
       2 
       3 //获取应用实例
       4 
       5 var app = getApp()
       6 
       7 Page({
       8 
       9    data: {
      10 
      11        motto: 'Hello World',
      12 
      13        userInfo: {}
      14 
      15    },
      16 
      17    //事件处理函数
      18 
      19    bindViewTap: function() {
      20 
      21        console.log('button clicked')
      22 
      23    },
      24 
      25    onLoad: function () {
      26 
      27        console.log('onLoad')
      28 
      29    }
      30 
      31 })

    基于wepy的实现:

    1.  1 import wepy from 'wepy';
       2 
       3  
       4 
       5 export default class Index extends wepy.page {
       6 
       7  
       8 
       9    data = {
      10 
      11        motto: 'Hello World',
      12 
      13        userInfo: {}
      14 
      15    };
      16 
      17    methods = {
      18 
      19        bindViewTap () {
      20 
      21            console.log('button clicked');
      22 
      23        }
      24 
      25    };
      26 
      27    onLoad() {
      28 
      29        console.log('onLoad');
      30 
      31    };
      32 
      33 }

    2.真正的组件化开发 小程序虽然有标签可以实现组件复用,但仅限于模板片段层面的复用,业务代码与交互事件 仍需在页面处理。无法实现组件化的松耦合与复用的效果。

    wepy组件示例

    1.  1 // index.wpy
       2 
       3 <template>
       4 
       5    <view>
       6 
       7        <panel>
       8 
       9            <h1 slot="title"></h1>
      10 
      11        </panel>
      12 
      13        <counter1 :num="myNum"></counter1>
      14 
      15        <counter2 :num.sync="syncNum"></counter2>
      16 
      17        <list :item="items"></list>
      18 
      19    </view>
      20 
      21 </template>
      22 
      23 <script>
      24 
      25 import wepy from 'wepy';
      26 
      27 import List from '../components/list';
      28 
      29 import Panel from '../components/panel';
      30 
      31 import Counter from '../components/counter';
      32 
      33  
      34 
      35 export default class Index extends wepy.page {
      36 
      37  
      38 
      39    config = {
      40 
      41        "navigationBarTitleText": "test"
      42 
      43    };
      44 
      45    components = {
      46 
      47        panel: Panel,
      48 
      49        counter1: Counter,
      50 
      51        counter2: Counter,
      52 
      53        list: List
      54 
      55    };
      56 
      57    data = {
      58 
      59        myNum: 50,
      60 
      61        syncNum: 100,
      62 
      63        items: [1, 2, 3, 4]
      64 
      65    }
      66 
      67 }
      68 
      69 </script>

    3.支持加载外部NPM包 小程序较大的缺陷是不支持NPM包,导致无法直接使用大量优秀的开源内容,wepy在编译过程当中,会递归 遍历代码中的require然后将对应依赖文件从node_modules当中拷贝出来,并且修改require为相对路径, 从而实现对外部NPM包的支持。如下图:

    4.单文件模式,使得目录结构更加清晰 小程序官方目录结构要求app必须有三个文件app.json,app.js,app.wxss,页面有4个文件 index.json,index.js,index.wxml,index.wxss。而且文 件必须同名。 所以使用wepy开发前后开发目录对比如下:

    官方DEMO:

    1.  1 project
       2 
       3 ├── pages
       4 
       5 |   ├── index
       6 
       7 |   |   ├── index.json  index 页面配置
       8 
       9 |   |   ├── index.js    index 页面逻辑
      10 
      11 |   |   ├── index.wxml  index 页面结构
      12 
      13 |   |   └── index.wxss  index 页面样式表
      14 
      15 |   └── log
      16 
      17 |       ├── log.json    log 页面配置
      18 
      19 |       ├── log.wxml    log 页面逻辑
      20 
      21 |       ├── log.js      log 页面结构
      22 
      23 |       └── log.wxss    log 页面样式表
      24 
      25 ├── app.js              小程序逻辑
      26 
      27 ├── app.json            小程序公共设置
      28 
      29 └── app.wxss            小程序公共样式表

    使用wepy框架后目录结构:

    1.  1 project
       2 
       3 └── src
       4 
       5    ├── pages
       6 
       7    |   ├── index.wpy    index 页面配置、结构、样式、逻辑
       8 
       9    |   └── log.wpy      log 页面配置、结构、样式、逻辑
      10 
      11    └──app.wpy           小程序配置项(全局样式配置、声明钩子等)

    5.默认使用babel编译,支持ES6/7的一些新特性。

    6.wepy支持使用less

    默认开启使用了一些新的特性如promise,async/await等等

    如何开发

    快速起步

    安装

    1. 1 npm install wepy-cli -g

    脚手架

    1. 1 wepy new myproject

    切换至项目目录

    1. 1 cd myproject

    实时编译

    1. 1 wepy build --watch

    目录结构

    1.  1 ├── dist                   微信开发者工具指定的目录
       2 
       3 ├── node_modules
       4 
       5 ├── src                    代码编写的目录
       6 
       7 |   ├── components         组件文件夹(非完整页面)
       8 
       9 |   |   ├── com_a.wpy      可复用组件 a
      10 
      11 |   |   └── com_b.wpy      可复用组件 b
      12 
      13 |   ├── pages              页面文件夹(完整页面)
      14 
      15 |   |   ├── index.wpy      页面 index
      16 
      17 |   |   └── page.wpy       页面 page
      18 
      19 |   └── app.wpy            小程序配置项(全局样式配置、声明钩子等)
      20 
      21 └── package.json           package 配置

    wepy和VUE在编码风格上面非常相似,VUE开发者基本可以无缝切换,因此这里仅介绍两者的主要区别:

    1.二者均支持props、data、computed、components、methods、watch(wepy中是watcher), 但wepy中的methods仅可用于页面事件绑定,其他自定义方法都要放在外层,而VUE中所有方法均放在 methods下

    2.wepy中props传递需要加上.sync修饰符(类似VUE1.x)才能实现props动态更新,并且父组件再 变更传递给子组件props后要执行this.$apply()方法才能更新

    3.wepy支持数据双向绑定,子组件在定义props时加上twoway:true属性值即可实现子组件修改父组 件数据

    4.VUE2.x推荐使用eventBus方式进行组件通信,而在wepy中是通过$broadcast,$emit,$invoke 三种方法实现通信

    1.  1 · 首先事件监听需要写在events属性下:
       2 
       3 ``` bash
       4 
       5 import wepy from 'wepy';
       6 
       7 export default class Com extends wepy.component {
       8 
       9    components = {};
      10 
      11    data = {};
      12 
      13    methods = {};
      14 
      15    events = {
      16 
      17        'some-event': (p1, p2, p3, $event) => {
      18 
      19              console.log(`${this.name} receive ${$event.name} from ${$event.source.name}`);
      20 
      21        }
      22 
      23    };
      24 
      25    // Other properties
      26 
      27 }
      28 
      29 ```
      30 
      31 · $broadcast:父组件触发所有子组件事件
      32 
      33  
      34 
      35 · $emit:子组件触发父组件事件
      36 
      37  
      38 
      39 · $invoke:子组件触发子组件事件

    5.VUE的生命周期包括created、mounted等,wepy仅支持小程序的生命周期:onLoad、onReady等

    6.wepy不支持过滤器、keep-alive、ref、transition、全局插件、路由管理、服务端渲染等VUE特性技术

    wepy原理研究

    虽然wepy提升了小程序开发体验,但毕竟最终要运行在小程序环境中,归根结底wepy还是需要编译成小程序 需要的格式,因此wepy的核心在于代码解析与编译。

    wepy项目文件主要有两个: wepy-cli:用于把.wpy文件提取分析并编译成小程序所要求的wxml、wxss、js、json格式 wepy:编译后js文件中的js框架

    wepy编译过程

    拆解过程核心代码

    1.   1 //wepy自定义属性替换成小程序标准属性过程
        2 
        3 return content.replace(/<([w-]+)s*[sS]*?(/|</[w-]+)>/ig, (tag, tagName) => {
        4 
        5    tagName = tagName.toLowerCase();
        6 
        7    return tag.replace(/s+:([w-_]*)([.w]*)s*=/ig, (attr, name, type) => { // replace :param.sync => v-bind:param.sync
        8 
        9        if (type === '.once' || type === '.sync') {
       10 
       11        }
       12 
       13        else
       14 
       15            type = '.once';
       16 
       17        return ` v-bind:${name}${type}=`;
       18 
       19    }).replace(/s+@([w-_]*)([.w]*)s*=/ig, (attr, name, type) => { // replace @change => v-on:change
       20 
       21        const prefix = type !== '.user' ? (type === '.stop' ? 'catch' : 'bind') : 'v-on:';
       22 
       23        return ` ${prefix}${name}=`;
       24 
       25    });
       26 
       27 });
       28 
       29  
       30 
       31 ...
       32 
       33 //按xml格式解析wepy文件
       34 
       35 xml = this.createParser().parseFromString(content);
       36 
       37 const moduleId = util.genId(filepath);
       38 
       39 //提取后的格式
       40 
       41 let rst = {
       42 
       43    moduleId: moduleId,
       44 
       45    style: [],
       46 
       47    template: {
       48 
       49        code: '',
       50 
       51        src: '',
       52 
       53        type: ''
       54 
       55    },
       56 
       57    script: {
       58 
       59        code: '',
       60 
       61        src: '',
       62 
       63        type: ''
       64 
       65    }
       66 
       67 };
       68 
       69 //循环拆解提取过程
       70 
       71 [].slice.call(xml.childNodes || []).forEach((child) => {
       72 
       73    const nodeName = child.nodeName;
       74 
       75    if (nodeName === 'style' || nodeName === 'template' || nodeName === 'script') {
       76 
       77        let rstTypeObj;
       78 
       79  
       80 
       81        if (nodeName === 'style') {
       82 
       83            rstTypeObj = {code: ''};
       84 
       85            rst[nodeName].push(rstTypeObj);
       86 
       87        } else {
       88 
       89            rstTypeObj = rst[nodeName];
       90 
       91        }
       92 
       93  
       94 
       95        rstTypeObj.src = child.getAttribute('src');
       96 
       97        rstTypeObj.type = child.getAttribute('lang') || child.getAttribute('type');
       98 
       99        if (nodeName === 'style') {
      100 
      101            // 针对于 style 增加是否包含 scoped 属性
      102 
      103            rstTypeObj.scoped = child.getAttribute('scoped') ? true : false;
      104 
      105        }
      106 
      107  
      108 
      109        if (rstTypeObj.src) {
      110 
      111            rstTypeObj.src = path.resolve(opath.dir, rstTypeObj.src);
      112 
      113        }
      114 
      115  
      116 
      117        if (rstTypeObj.src && util.isFile(rstTypeObj.src)) {
      118 
      119            const fileCode = util.readFile(rstTypeObj.src, 'utf-8');
      120 
      121            if (fileCode === null) {
      122 
      123                throw '打开文件失败: ' + rstTypeObj.src;
      124 
      125            } else {
      126 
      127                rstTypeObj.code += fileCode;
      128 
      129            }
      130 
      131        } else {
      132 
      133            [].slice.call(child.childNodes || []).forEach((c) => {
      134 
      135                rstTypeObj.code += util.decode(c.toString());
      136 
      137            });
      138 
      139        }
      140 
      141  
      142 
      143        if (!rstTypeObj.src)
      144 
      145            rstTypeObj.src = path.join(opath.dir, opath.name + opath.ext);
      146 
      147    }
      148 
      149 });
      150 
      151 ...
      152 
      153 // 拆解提取wxml过程
      154 
      155 (() => {
      156 
      157    if (rst.template.type !== 'wxml' && rst.template.type !== 'xml') {
      158 
      159        let compiler = loader.loadCompiler(rst.template.type);
      160 
      161        if (compiler && compiler.sync) {
      162 
      163            if (rst.template.type === 'pug') { // fix indent for pug, https://github.com/wepyjs/wepy/issues/211
      164 
      165                let indent = util.getIndent(rst.template.code);
      166 
      167                if (indent.firstLineIndent) {
      168 
      169                    rst.template.code = util.fixIndent(rst.template.code, indent.firstLineIndent * -1, indent.char);
      170 
      171                }
      172 
      173            }
      174 
      175            //调用wxml解析模块
      176 
      177            let compilerConfig = config.compilers[rst.template.type];
      178 
      179  
      180 
      181            // xmldom replaceNode have some issues when parsing pug minify html, so if it's not set, then default to un-minify html.
      182 
      183            if (compilerConfig.pretty === undefined) {
      184 
      185                compilerConfig.pretty = true;
      186 
      187            }
      188 
      189            rst.template.code = compiler.sync(rst.template.code, config.compilers[rst.template.type] || {});
      190 
      191            rst.template.type = 'wxml';
      192 
      193        }
      194 
      195    }
      196 
      197    if (rst.template.code)
      198 
      199        rst.template.node = this.createParser().parseFromString(util.attrReplace(rst.template.code));
      200 
      201 })();
      202 
      203  
      204 
      205 // 提取import资源文件过程
      206 
      207 (() => {
      208 
      209    let coms = {};
      210 
      211    rst.script.code.replace(/imports*([w-\_]*)s*froms*['"]([w-\_./]*)['"]/ig, (match, com, path) => {
      212 
      213        coms[com] = path;
      214 
      215    });
      216 
      217  
      218 
      219    let match = rst.script.code.match(/[s
      ]componentss*=[s
      ]*/);
      220 
      221    match = match ? match[0] : undefined;
      222 
      223    let components = match ? this.grabConfigFromScript(rst.script.code, rst.script.code.indexOf(match) + match.length) : false;
      224 
      225    let vars = Object.keys(coms).map((com, i) => `var ${com} = "${coms[com]}";`).join('
      ');
      226 
      227    try {
      228 
      229        if (components) {
      230 
      231            rst.template.components = new Function(`${vars}
      return ${components}`)();
      232 
      233        } else {
      234 
      235            rst.template.components = {};
      236 
      237        }
      238 
      239    } catch (e) {
      240 
      241        util.output('错误', path.join(opath.dir, opath.base));
      242 
      243        util.error(`解析components出错,报错信息:${e}
      ${vars}
      return ${components}`);
      244 
      245    }
      246 
      247 })();
      248 
      249 ...

    wepy中有专门的script、style、template、config解析模块 以template模块举例:

    1.   1 //compile-template.js
        2 
        3 ...
        4 
        5 //将拆解处理好的wxml结构写入文件
        6 
        7 getTemplate (content) {
        8 
        9    content = `<template>${content}</template>`;
       10 
       11    let doc = new DOMImplementation().createDocument();
       12 
       13    let node = new DOMParser().parseFromString(content);
       14 
       15    let template = [].slice.call(node.childNodes || []).filter((n) => n.nodeName === 'template');
       16 
       17  
       18 
       19    [].slice.call(template[0].childNodes || []).forEach((n) => {
       20 
       21        doc.appendChild(n);
       22 
       23    });
       24 
       25    ...
       26 
       27    return doc;
       28 
       29 },
       30 
       31 //处理成微信小程序所需的wxml格式
       32 
       33 compileXML (node, template, prefix, childNodes, comAppendAttribute = {}, propsMapping = {}) {
       34 
       35    //处理slot
       36 
       37    this.updateSlot(node, childNodes);
       38 
       39    //处理数据绑定bind方法
       40 
       41    this.updateBind(node, prefix, {}, propsMapping);
       42 
       43    //处理className
       44 
       45    if (node && node.documentElement) {
       46 
       47        Object.keys(comAppendAttribute).forEach((key) => {
       48 
       49            if (key === 'class') {
       50 
       51                let classNames = node.documentElement.getAttribute('class').split(' ').concat(comAppendAttribute[key].split(' ')).join(' ');
       52 
       53                node.documentElement.setAttribute('class', classNames);
       54 
       55            } else {
       56 
       57                node.documentElement.setAttribute(key, comAppendAttribute[key]);
       58 
       59            }
       60 
       61        });
       62 
       63    }
       64 
       65    //处理repeat标签
       66 
       67    let repeats = util.elemToArray(node.getElementsByTagName('repeat'));
       68 
       69    ...
       70 
       71  
       72 
       73    //处理组件
       74 
       75    let componentElements = util.elemToArray(node.getElementsByTagName('component'));
       76 
       77    ...
       78 
       79    return node;
       80 
       81 },
       82 
       83  
       84 
       85 //template文件编译模块
       86 
       87 compile (wpy){
       88 
       89    ...
       90 
       91    //将编译好的内容写入到文件
       92 
       93    let plg = new loader.PluginHelper(config.plugins, {
       94 
       95        type: 'wxml',
       96 
       97        code: util.decode(node.toString()),
       98 
       99        file: target,
      100 
      101        output (p) {
      102 
      103            util.output(p.action, p.file);
      104 
      105        },
      106 
      107        done (rst) {
      108 
      109            //写入操作
      110 
      111            util.output('写入', rst.file);
      112 
      113            rst.code = self.replaceBooleanAttr(rst.code);
      114 
      115            util.writeFile(target, rst.code);
      116 
      117        }
      118 
      119    });
      120 
      121 }

    编译前后文件对比

    wepy编译前的文件:

    1.  1 <scroll-view scroll-y="true" class="list-page" scroll-top="{{scrollTop}}" bindscrolltolower="loadMore">
       2 
       3    <!-- 商品列表组件 -->
       4 
       5    <view class="goods-list">
       6 
       7      <GoodsList :goodsList.sync="goodsList" :clickItemHandler="clickHandler" :redirect="redirect" :pageUrl="pageUrl"></GoodsList>
       8 
       9    </view>
      10 
      11 </scroll-view>

    wepy编译后的文件:

    1.  1 <scroll-view scroll-y="true" class="list-page" scroll-top="{{scrollTop}}" bindscrolltolower="loadMore">
       2 
       3  <view class="goods-list">
       4 
       5    <view  wx:for="{{$GoodsList$goodsList}}" wx:for-item="item" wx:for-index="index" wx:key="{{item.infoId}}" bindtap="$GoodsList$clickHandler" data-index="{{index}}" class="item-list-container{{index%2==0 ? ' left-item' : ''}}">
       6 
       7      <view class="item-img-list"><image src="{{item.pic}}" class="item-img" mode="aspectFill"/></view>
       8 
       9      <view class="item-desc">
      10 
      11        <view class="item-list-title">
      12 
      13          <text class="item-title">{{item.title}}</text>
      14 
      15        </view>
      16 
      17        <view class="item-list-price">
      18 
      19          <view wx:if="{{item.price && item.price>0}}" class="item-nowPrice"><i></i>{{item.price}}</view>
      20 
      21          <view wx:if="{{item.originalPrice && item.originalPrice>0}}" class="item-oriPrice">¥{{item.originalPrice}}</view>
      22 
      23        </view>
      24 
      25        <view class="item-list-local"><view>{{item.cityName}}{{item.cityName&&item.businessName?' | ':''}}{{item.businessName}}    </view>
      26 
      27      </view>
      28 
      29      </view>
      30 
      31        <form class="form" bindsubmit="$GoodsList$sendFromId" report-submit="true" data-index="{{index}}">
      32 
      33          <button class="submit-button" form-type="submit"/>
      34 
      35        </form>
      36 
      37      </view>
      38 
      39    </view>
      40 
      41  </view>
      42 
      43 </scroll-view>

    可以看到wepy将页面中所有引入的组件都直接写入页面当中,并且按照微信小程序的格式来输出 当然也从一个侧面看出,使用wepy框架后,代码风格要比原生的更加简洁优雅

    以上是wepy实现原理的简要分析,有兴趣的朋友可以去阅读源码(https://github.com/wepyjs/wepy)。 综合来讲,wepy的核心在于编译环节,能够将优雅简洁的类似VUE风格的代码,编译成微信小程序所需要的繁杂代码。

    wepy作为一款优秀的微信小程序框架,可以帮我们大幅提高开发效率,在为数不多的小程序框架中一枝独秀,希望有更多的团队选择wepy。

    PS:wepy也在实现小程序和VUE代码同构,但目前还处在开发阶段,如果未来能实现一次开发,同时产出小程序和M页,将是一件非常爽的事情。

    如果你喜欢我们的文章,关注我们的公众号和我们互动吧。

  • 相关阅读:
    常用数据绑定控件详解
    BookList
    BUG:TreeView: NodeCheck Event Does Not Occur
    SQL Server中TEXT类型操作
    Quote:软件开发工程师的经验之谈
    SQL字符串处理函数大全
    Summary 2009 Target 2010
    读取库中的所有表名 列名
    使用大值数据类型
    sql 修改列名及表名 sp_rename
  • 原文地址:https://www.cnblogs.com/zhuanzhuanfe/p/7412536.html
Copyright © 2020-2023  润新知