wframe不是控制項庫,也不是UI庫,她是一個微信小程式面向對象編程框架,代碼只有幾百行。她的主要功能是規範小程式項目的文件結構、規範應用程式初始化、規範頁面載入及授權管理的框架,當然,wframe也提供了一些封裝好了的函數庫,方便開發者調用。 wframe目前已實現的核心功能: 1. 應用程式初始 ...
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 this._busyTimer = setTimeout(function () { 27 wx.showLoading({ 28 title: option, 29 mask: true 30 }); 31 }, delay == null ? this._busyDelay : delay); 32 } 33 }, 34 alert: function (title, content, callback) { 35 content = content == undefined ? '' : content; 36 wx.showModal({ 37 title: title, 38 content: content, 39 showCancel: false, 40 confirmText: "確定", 41 success: res => { 42 callback && callback(); 43 } 44 }); 45 }, 46 confirm: function (title, content, buttons) { 47 var buttonList = []; 48 for (var p in buttons) { 49 if (buttons.hasOwnProperty(p)) { 50 buttonList.push({ 51 text: p, 52 handler: buttons[p] 53 }) 54 } 55 } 56 content = content == undefined ? '' : content; 57 wx.showModal({ 58 title: title, 59 content: content, 60 showCancel: true, 61 cancelText: buttonList[0].text, 62 confirmText: buttonList[1].text, 63 success: res => { 64 if (res.confirm) { 65 buttonList[1].handler && buttonList[1].handler(); 66 } else if (res.cancel) { 67 buttonList[0].handler && buttonList[0].handler(); 68 } 69 } 70 }); 71 } 72 }; 73 74 module.exports = weixin;View Code
2. extensions/ArrayExtensions.js
一大堆數組擴展方法,非常常用,非常好用。引入mvcApp的業務層代碼均可直接使用。代碼如下:
1 var ArrayExtensions = {}; 2 3 Array.prototype._each = function (func) { 4 for (var i = 0; i < this.length; i++) { 5 var item = this[i]; 6 var result = func(i, item); 7 if (result === false) { 8 return; 9 } 10 } 11 }; 12 13 Array.prototype._sum = function (propertyOrFunc) { 14 var total = 0; 15 var isFunc = typeof (propertyOrFunc) == "function"; 16 this._each(function (i, item) { 17 if (isFunc) { 18 total += propertyOrFunc(item); 19 } else { 20 var value = item[propertyOrFunc]; 21 if (value != undefined) { 22 value = value * 1; 23 if (!isNaN(value)) { 24 total += value; 25 } 26 } 27 } 28