wframe不是控件库,也不是UI库,她是一个微信小程序面向对象编程框架,代码只有几百行。她的主要功能是规范小程序项目的文件结构、规范应用程序初始化、规范页面加载及授权管理的框架,当然,wframe也提供了一些封装好了的函数库,方便开发者调用。
wframe目前已实现的核心功能:
1. 应用程序初始化自动从服务器获取配置,ajax成功后触发ready事件;
2. 每个页面对象可以配置是否requireLogin属性,如果需要登录,则每个页面在进入ready方法之前会自动完成授权、获取用户信息、服务器端登录;
3. 完成ajax全局封装:如果用户已经登录,则会自动在http-header添加token信息,如果session过期则会重新进入登录流程;
本文的阅读对象:想要自己搭建小程序框架的人,相信本文会给你提供一些思路。
我们为什么要开发wframe?
我们开发的小程序越来越多,小程序也越来越复杂,于是我们就想将每个小程序重复在写的那一部分代码提出来,变成一个公共的函数库,一个跟每个项目的业务逻辑完全不相关的函数库。除了在新项目中可以节省代码之外,有一些复杂的代码逻辑由于提到了公共的函数库,我们将其优化得更优雅、更健壮。
说wframe是一个函数库虽说也可以,但wframe更像一个框架。我们通常把一些静态方法、静态对象、仅处理页面内容的JS文件集称作函数库,比如jQuery;我们通常把处理了应用程序和页面生命周期,以及使用了大量的面向对象编程技术的JS文件集称作框架。因此,wframe其实是一个框架。
重要说明:wframe框架用到了大量的面向对象编程知识,比如实例、继承、覆写、扩展、抽象方法等等,因此对开发人员,特别是项目中的架构师,的面向对象编程能力有较高要求。
项目源码已上传到GitHub并会持续更新:https://github.com/leotsai/wframe
一、wframe项目结构
wframe的最核心的职责就是规范项目文件结构。
为什么需要规范呢?因为我们小程序越来越多,如果每个小程序的文件结构都不一样的话,那定是一件很难受的事。另外,wframe由于其框架的身份,其本职工作就是定义一个最好的文件结构,这样基于wframe创建的所有小程序都将自动继承wframe的优秀品质。
1. _core文件夹
wframe框架源码,与业务毫不相干,每个小程序都可以直接将_core文件夹复制到项目中,而当wframe更新版本时,所有小程序可以直接覆盖_core完成升级。用下划线“_”开头的目的有2个:
(a) 将此文件夹置顶;
(b) 标记此文件夹是一个特殊文件夹,本框架中还有其他地方也会用到下划线开头为文件夹/文件。
2. _demo文件夹
业务核心文件夹,比如定义一些扩展wframe框架的类,同时这些类又被具体的业务类继承使用,比如ViewModelBase等。
3. pages文件夹
与微信小程序官方文档定义一致:放置页面的地方。
4. app.js
程序主入口,只不过基于wframe的小程序的app.js跟官方的长得很不一样,我们定义了一个自己的Applicaiton类,然后再new的一个Application实例。稍后详解。
5. mvcApp.js
几乎每个js文件都会require引入的一个文件,因为这相当于是项目的静态入口,其包含了所有的静态函数库,比如对wx下面方法的封装、Array类扩展、Date类扩展、网络请求(ajax)封装等等。mvcApp.js几乎只定义了一个入口,其内部的很多对象、方法都是通过require其他JS引入的。因此,大多数情况下,我们只需要require引入mvcApp.js就够了。
写到这里,分享一个我们的编程思想:入口要少。小程序里有哪些入口:this、getApp()、wx、mvcApp。其实也就是每一行代码点号“.”前面的都叫代码入口。
我们还有另一个编程规范(强制):每个文件不能超过200行代码(最好不超100行)。这就是要求每个程序员必须学会拆分,拆分也是我们的另一个编程思想。通过拆分,每个JS文件职责清晰,极大的提高了代码阅读率。
二、详解
1. app.js和Application类详解
app.js定义了程序入口。
1 var mvcApp = require(\'mvcApp.js\'); 2 var Application = require(\'_core/Application.js\'); 3 4 function MvcApplication() { 5 Application.call(this); 6 this.initUrl = \'https://www.somdomain.com/api/client-config/get?key=wx_applet_wframe\'; 7 this.host = \'http://localhost:18007\'; 8 this.confgis = { 9 host: \'http://localhost:18007\', 10 cdn: \'https://images.local-dev.cdn.somedomain.com\' 11 }; 12 this.mock = true; 13 this.accessToken = null; 14 this.useDefaultConfigsOnInitFailed = false; 15 }; 16 17 MvcApplication.prototype = new Application(); 18 19 MvcApplication.prototype.onInitialized = function (configs) { 20 if (configs != null && configs !== \'\') { 21 this.configs = JSON.parse(configs); 22 this.host = this.configs.host; 23 } 24 }; 25 26 App(new MvcApplication());
可以看到app.js定义了一个MvcApplication类,继承自框架中的Application类,同时重写了父类的onInitialized方法。
下面是框架中的Application类:
1 var WebClient = require(\'http/WebClient.js\'); 2 var AuthorizeManager = require(\'weixin/AuthorizeManager.js\'); 3 var weixin = require(\'weixin.js\'); 4 5 6 function Application() { 7 this.initUrl = \'\'; 8 this.host = \'\'; 9 this.session = null; 10 this.initialized = false; 11 this.mock = false; 12 this.useDefaultConfigsOnInitFailed = false; 13 this.authorizeManager = new AuthorizeManager(); 14 this._userInfo = null; 15 this._readyHandlers = []; 16 }; 17 18 Application.prototype = { 19 onLaunch: function () { 20 var me = this; 21 if(this.initUrl === \'\'){ 22 throw \'please create YourOwnApplication class in app.js that inerits from Application class and provide initUrl in constructor\'; 23 } 24 var client = new WebClient(); 25 client.post(this.initUrl, null, function(result){ 26 if (result.success || me.useDefaultConfigsOnInitFailed){ 27 me.initialized = true; 28 me.onInitialized(result.success ? result.value : null); 29 me.triggerReady(); 30 } 31 else{ 32 weixin.alert(\'小程序初始化失败\', result.message); 33 } 34 }, \'初始化中...\'); 35 }, 36 onShow: function () { 37 38 }, 39 onHide: function () { 40 41 }, 42 onError: function () { 43 44 }, 45 onPageNotFound: function () { 46 47 }, 48 ready: function (callback) { 49 var me = this; 50 if (this.initialized === true) { 51 callback && callback(); 52 return; 53 } 54 this._readyHandlers.push(callback); 55 }, 56 triggerReady: function () { 57 for (var i = 0; i < this._readyHandlers.length; i++) { 58 var callback = this._readyHandlers[i]; 59 callback && callback(); 60 } 61 this._readyHandlers = []; 62 }, 63 onInitialized: function(configs){ 64 65 }, 66 getUserInfo: function(callback){ 67 var me = this; 68 if(this._userInfo != null){ 69 callback && callback(this._userInfo.userInfo); 70 return; 71 } 72 this.authorizeManager.getUserInfo(function(result){ 73 me._userInfo = result; 74 callback && callback(me._userInfo.userInfo); 75 }); 76 }, 77 getCurrentPage: function(){ 78 var pages = getCurrentPages(); 79 return pages.length > 0 ? pages[0] : null; 80 } 81 }; 82 83 module.exports = Application;
Applicaiton类(及其子类)在wframe框架中的主要工作:
1. 应用程序初始化的时候从服务器获取一个配置,比如服务器域名(实现域名实时切换)、CDN域名,以及其他程序配置信息;
2. 全局存储用户的授权信息和登陆之后的会话信息;
3. 全局mock开关;
4. 其他快捷方法,比如获取当前页面等。
Application类核心执行流程:
1. 应用程序初始化时首先从服务器获取客户端配置信息;
2. 获取完成之后会触发onInitialized方法(在子类中覆写)和ready方法。
2. PageBase类详解
PageBase类是所有页面都会继承的一个基类。先看代码:
1 console.log("PageBae.js entered"); 2 3 const app = getApp(); 4 5 function PageBase(title) { 6 this.vm = null; 7 this.title = title; 8 this.requireLogin = true; 9 }; 10 11 PageBase.prototype = { 12 onLoad: function (options) { 13 var me = this; 14 if (this.title != null) { 15 this.setTitle(this.title); 16 } 17 this.onPreload(options); 18 app.ready(function () { 19 if (me.requireLogin && app.session == null) { 20 app.getUserInfo(function (info) { 21 me.login(info, function (session) { 22 app.session = session; 23 me.ready(options); 24 }); 25 }); 26 } 27 else { 28 me.ready(options); 29 } 30 }); 31 }, 32 ready: function (options) { 33 34 }, 35 onPreload: function(options){ 36 37 }, 38 render: function () { 39 var data = {}; 40 for (var p in this.vm) { 41 var value = this.vm[p]; 42 if (!this.vm.hasOwnProperty(p)) { 43 continue; 44 } 45 if (value == null || typeof (value) === \'function\') { 46 continue; 47 } 48 if (value.__route__ != null) { 49 continue; 50 } 51 data[p] = this.vm[p]; 52 } 53 this.setData(data); 54 }, 55 go: function (url, addToHistory) { 56 if (addToHistory === false) { 57 wx.redirectTo({ url: url }); 58 } 59 else { 60 wx.navigateTo({ url: url }); 61 } 62 }, 63 goBack: function () { 64 wx.navigateBack({}); 65 }, 66 setTitle: function (title) { 67 this.title = title; 68 wx.setNavigationBarTitle({ title: this.title }); 69 }, 70 login: function (userInfo, callback) { 71 throw \'please implement PageBase.login method.\'; 72 }, 73 getFullUrl: function () { 74 var url = this.route.indexOf(\'/\') === 0 ? this.route : \'/\' + this.route; 75 var parts = []; 76 for (var p in this.options) { 77 if (this.options.hasOwnProperty(p)) { 78 parts.push(p + "=" + this.options[p]); 79 } 80 } 81 if (parts.length > 0) { 82 url += "?" + parts.join(\'&\'); 83 } 84 return url; 85 }, 86 isCurrentPage: function(){ 87 return this === getApp().getCurrentPage(); 88 } 89 }; 90 91 PageBase.extend = function (prototypeObject) { 92 var fn = new PageBase(); 93 for (var p in prototypeObject) { 94 fn[p] = prototypeObject[p]; 95 } 96 return fn; 97 }; 98 99 module.exports = PageBase;
由于微信小程序Application类的onLaunch不支持回调,也就是说,在wframe框架中,虽然我们在onLaunch时发起了ajax调用,但是程序并不会等待ajax返回就会立即进入Page对象的onLoad方法。这是一个非常重要的开发小程序的知识前提,但是官方文档并没有重要说明。
PageBase类的三个实例属性:
1. vm:即ViewModel实例,可以理解为官方文档中的Page实例的data属性;
2. title:页面标题
3. requireLogin:是否需要登录,如果设置为true,则页面onLoad执行后自动进入登录流程,登录完成后才会触发页面的ready方法;
PageBase类的实例方法:
1. onLoad:对应官方文档中的onLoad事件。wframe框架自动会处理requireLogin属性,处理完成后才触发ready方法;
2. ready:每个业务级页面的主入口,每个业务级页面都应该实现ready方法,而不一定实现onLoad方法;
3. onPreload:在执行onLoad之前执行的方法,不支持异步;
4. render:非常常用的方法,功能是将ViewModel(即data)呈现到页面上,在业务页面中直接使用this.render()即可将更新的数据呈现出来;
5. go:页面跳转,相比官方的wx.navigateTo简化了很多;
6. goBack:等于wx.navigateBack;
7. setTitle:直接设置页面标题;
8. login:可以理解成抽象方法,必须由子类实现,在我们demo中由业务级框架中的DemoPageBase实现;
9. getFullUrl:获取页面完整地址,包括路径和参数,便于直接跳转;
10. isCurrentPage:判断该页面实例是否在应用程序页面栈中处于当前页面,主要用于setInterval函数中判断用户是否已离开了页面;
3. DemoPageBase类详解
这是业务层级的框架内容。我们建议每个页面都继承自该类,这个类可以封装跟业务相关的很多逻辑,方便子类(业务页面)直接通过this调用相关方法。
在wframe的demo框架中,我们实现了PageBase类的抽象方法login。
这里请注意同目录的api.js文件。在我们的编码规范中,所有ajax访问都需要提到专门的api.js文件,通常与页面类处于同一目录,这是为了方便mock API。请看示例代码:
1 var mvcApp = require(\'../mvcApp.js\'); 2 3 var api = { 4 login: function (userInfo, code, callback) { 5 var data = mvcApp.serializeToKeyValues(userInfo) + "&code=" + code; 6 mvcApp.ajax.busyPost(\'/demo/api/login\', data, function(result){ 7 callback(result.value); 8 }, \'登陆中...\', true); 9 } 10 }; 11 if (getApp().mock) { 12 var api = { 13 login: function (userInfo, code, callback) { 14 setTimeout(function(){ 15 callback({ 16 token: \'98c2f1bd7beb3bef3b796a5ebf32940498cb5586ddb4a5aa8e\' 17 }); 18 }, 2000); 19 } 20 }; 21 } 22 23 module.exports = api;
4. 页面类的实现
请看pages/index目录下的文件列表:
1. IndexViewModel:该页面的ViewModel;
2. api.js:该页面所有ajax的封装;
3. index.js:页面入口;
4. index.wxml:HTML;
5. index.wxss:样式;
先看入口index.js,代码如下:
1 var mvcApp = require(\'../../mvcApp.js\'); 2 var DemoPageBase = require(\'../DemoPageBase.js\'); 3 var IndexViewModel = require(\'IndexViewModel.js\'); 4 5 function IndexPage() { 6 DemoPageBase.call(this, \'index\'); 7 }; 8 9 IndexPage.prototype = new DemoPageBase(); 10 11 IndexPage.prototype.onPreload = function(options){ 12 this.vm = new IndexViewModel(this); 13 this.render(); 14 }; 15 16 IndexPage.prototype.ready = function () { 17 var me = this; 18 this.vm.load(); 19 }; 20 21 IndexPage.prototype.goDetails = function (e) { 22 var item = e.target.dataset.item; 23 wx.navigateTo({ 24 url: \'/pages/details/details?id=\' + item.id 25 }); 26 }; 27 28 Page(new IndexPage());
index.js核心逻辑:继承自DemoPageBase,onPreload时设置了ViewModel,ready时(自动登录完成后)调用ViewModel的数据加载方法,完成。
5. ViewModel的实现
在微信小程序官方文档中,并没有提ViewModel的概念,这会导致一些稍微有点复杂的页面的data对象的处理变得很凌乱,更别说复杂页面的data处理,那根本无从维护。ViewModel的设计思想是专门用来封装视图数据的一层代码,不管是MVC,还是MVVM,ViewModel都是拆分数据层代码的最佳实践。因此,wframe框架强烈建议每个页面都建一个对应的ViewModel,封装数据结构,以及获取、处理数据。
在我们的编程思想中,ViewModel不仅仅是放数据的地方,更是封装业务逻辑的最佳位置之一。所以我们的ViewModel会很肥(fat model),会包含相关的很多业务逻辑处理。
如果项目需要,还可以封装一个DemoViewModelBase类,将其他页面ViewModel常用的方法封装进来,比如this.getUserName()等方法。
请看示例代码:
1 var api = require(\'api.js\'); 2 var mvcApp = require(\'../../mvcApp.js\'); 3 4 function IndexViewModel(page){ 5 this.users = []; 6 this.showLoading = true; 7 this.males = 0; 8 this.females = 0; 9 this.page = page; 10 }; 11 12 IndexViewModel.prototype.load = function(){ 13 var me = this; 14 api.getUsers(function(users){ 15 me.showLoading = false; 16 me.females = users._count(function(x){ 17 return x.gender === \'female\'; 18 }); 19 me.males = users._count(function (x) { 20 return x.gender === \'male\'; 21 }); 22 me.users = users._orderByDescending(null, function(first, second){ 23 if(first.gender === \'male\'){ 24 if(second.gender === \'male\'){ 25 return first.birthYear > second.birthYear; 26 } 27 return true; 28 } 29 if(second.gender === \'female\'){ 30 return first.birthYear > second.birthYear; 31 } 32 return false; 33 }); 34 me.page.render(); 35 }); 36 }; 37 38 module.exports = IndexViewModel;
api.js就不贴代码了,跟上一小节中的api.js一样的。html和css部分也忽略不讲。
至此,页面级实现就完成了。
下面,笔者再对wframe框架中的其他特殊部分进行特殊说明。继续。
6. pages/_authorize文件夹
这个文件夹定义了一个授权页面,这是因为新版小程序API强制要求用户自己点授权按钮才能弹出授权。这个虽然集成在wframe框架中,但是每个项目应该自行修改此页面的样式以符合项目UI设计。
这个目录下面只有一个_authorize.js值得贴一下代码,其实都非常简单:
1 var DemoPageBase = require(\'../DemoPageBase.js\'); 2 3 4 function AuthPage() { 5 DemoPageBase.call(this, \'auth\'); 6 this.requireLogin = false; 7 }; 8 9 AuthPage.prototype = new DemoPageBase(); 10 11 AuthPage.prototype.onPreload = function (options) { 12 this.returnUrl = decodeURIComponent(options.returnUrl); 13 }; 14 15 AuthPage.prototype.onGotUserInfo = function (event) { 16 var me = this; 17 if (event.detail.userInfo == null) { 18 return; 19 } 20 var app = getApp(); 21 app._userInfo = event.detail; 22 DemoPageBase.prototype.login.call(this, app._userInfo.userInfo, function () { 23 me.go(me.returnUrl, false); 24 }) 25 } 26 27 Page(new AuthPage())
请注意onPreload方法中对returnUrl的获取,以及获取用户授权信息后对DemoPageBase.login方法的调用。
7. _core文件夹其他文件详解
_core文件夹之前已经讲了Application和PageBase类。继续。
1. weixin.js
主要封装了toast、busy(增加延时功能)、alert、confirm方法,后期可能会增加更多常用方法的封装。代码如下:
1 var weixin = { 2 _busyTimer: null, 3 _busyDelay: 1500, 4 toast: function (message, icon) { 5 wx.showToast({ 6 title: message, 7 icon: icon == null || icon == \'\' ? \'none\' : icon 8 }); 9 }, 10 toastSuccess: function (message) { 11 this.toast(message, \'success\'); 12 }, 13 busy: function (option, delay) { 14 clearTimeout(this._busyTimer); 15 if (option === false) { 16 wx.hideLoading(); 17 return; 18 } 19 if (delay === 0) { 20 wx.showLoading({ 21 title: option, 22 mask: true 23 }); 24 } 25 else { 26
请发表评论