你的位置:首页 > Java教程

[Java教程]像VUE一样写微信小程序

像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: List54 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.w

官方DEMO:

  1.  1 project 2  3 ├── pages 4  5 | ├── index 6  7 | | ├── index.json index 页面配置 8  9 | | ├── index.js index 页面逻辑10 11 | | ├── index.w12 13 | | └── index.wxss index 页面样式表14 15 | └── log16 17 |  ├── log.json log 页面配置18 19 |  ├── log.w20 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  可复用组件 a10 11 | | └── com_b.wpy  可复用组件 b12 13 | ├── pages    页面文件夹(完整页面)14 15 | | ├── index.wpy  页面 index16 17 | | └── page.wpy  页面 page18 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 properties26 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文件提取分析并编译成小程序所要求的w

wepy编译过程

拆解过程核心代码

  1.  1 //wepy自定义属性替换成小程序标准属性过程 2  3 return content.replace(/<([\w-]+)\s*[\s\S]*?(\/|<\/[\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 //按 34  35 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( { 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 // 拆解提取w154 155 (() => {156 157 if (rst.template.type !== 'w) {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/211164 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   //调用w176 177   let compilerConfig = config.compilers[rst.template.type];178 179 180 181   // 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 = 'w;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(/import\s*([\w\-\_]*)\s*from\s*['"]([\w\-\_\.\/]*)['"]/ig, (match, com, path) => {212 213  coms[com] = path;214 215  });216 217 218 219 let match = rst.script.code.match(/[\s\r\n]components\s*=[\s\r\n]*/);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('\r\n');226 227 try {228 229  if (components) {230 231   rst.template.components = new Function(`${vars}\r\nreturn ${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}\r\n${vars}\r\nreturn ${components}`);244 245  }246 247 })();248 249 ...

     

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

  1.  1 //compile-template.js 2  3 ... 4  5 //将拆解处理好的w 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 //处理成微信小程序所需的w 32  33 compile {}) { 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: 'w, 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页,将是一件非常爽的事情。

 

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