• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    迪恩网络公众号

[原创] 分享我们自己搭建的微信小程序开发框架——wframe及设计思想详解 ...

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

 

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            

鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
热门推荐
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap