2023年2月

在学习微信小程序开发过程中,一部分的难点是前端逻辑的处理,也就是对前端JS的代码编辑;一部分的难点是前端界面的设计展示;本篇随笔基于一个豆瓣电影接口的小程序开源项目进行重新调整,把其中遇到的相关难点和改进的地方进行讨论介绍,希望给大家提供一个参考的思路,本篇随笔是基于前人小程序的项目基础上进行的改进,因此在开篇之前首先对原作者的辛劳致敬及感谢。

1、豆瓣电影接口的小程序项目情况

豆瓣电影接口提供了很多相关的接口给我们使用,豆瓣电影接口的API地址如下所示:
https://developers.douban.com/wiki/?title=movie_v2

在GitHub的开源库里面,可以搜索到很多关于豆瓣电影接口的小程序,我本篇随笔是基于
weapp-douban-movie
这个小程序进行的改造处理,后来找到了原作者的项目地址:
wechat-weapp-movie
,原作者对版本做了一次升级,后来我对照我的调整和作者最新版本的源码,发现有些地方改造的思路有些类似,如对于URL地址外放到统一的配置文件中的处理,不过还是有很多地方改造不同。

本篇随笔的改造方案是基于小程序项目
weapp-douban-movie
的,因此对比的代码也是和这个进行比较,不知道这个版本是不是原作者的旧版本,不过这个版本对文件目录的区分已经显得非常干净利落了,对电影信息的展示也统一到了模板里面,进行多次的重复利用,整体的布局和代码都做的比较好,看得出是花了不少功夫进行整理优化的了。

小程序主界面效果如下所示:

小程序源码目录结构如下所示:

不过每个人都有不同的经验和看法,对于开发小程序来说,我侧重于使用配置文件减少硬编码的常量,使用Promise来优化JS代码的使用,将获取和提交JSON数据的方法封装到辅助类,以及使用地理位置接口动态获取当前城市名称和坐标等等。

本篇随笔下面的部分就是介绍使用这些内容进行代码优化的处理过程。

1、使用配置文件定义常量内容

我们在使用任何代码开发程序的时候,我们都是非常注意一些变量或常量的使用,如果能够统一定义那就统一定义好了,这种在小程序的JS代码里面也是一样,我们尽可能抽取一些如URL,固定参数等信息到独立的配置文件中,这样在JS代码引入文件,使用变量来代替

例如原来的config.js文件里面,只是定义了一个地址和页面数量的大小常量,如下所示

module.exports ={
city:
'杭州',
count:
20}

原来的小程序代码在获取待映的电影内容时候,部分源码如下所示

其他页面JS代码也和这个类似,头部依旧有很多类似这样URL地址,这个是我希望统一到config.js文件的地方,另外这个调用的函数是使用回调函数的处理方式,如下所示。

douban.fetchFilms.call(that, url, config.city, that.data.start, config.count)

其实我认为这里面既然是定义的外部函数,那么这里面的url, city, config.city, config.cout都不需要这里,在封装函数内部使用这些常量即可,因此可以对他们进行改造,如下我们统一抽取各个文件里面的URL,以及一些常见变量到config.js里面。

下面代码是我优化整理后的配置参数信息。

module.exports ={
city:
'',
location:
'0,0',
count:
20,

coming_soon_url:
'https://api.douban.com/v2/movie/coming_soon',
in_theaters_url:
'https://api.douban.com/v2/movie/in_theaters',
top_url:
'https://api.douban.com/v2/movie/top250',
search_url:
'https://api.douban.com/v2/movie/search?tag=',
detail_url:
'https://api.douban.com/v2/movie/subject/', //?id= celebrity_url: 'https://api.douban.com/v2/movie/celebrity/',
baidu_ak:
'6473aa8cbc349933ed841467bf45e46b',
baidu_movie:
'https://api.map.baidu.com/telematics/v3/movie',

hotKeyword: [
'功夫熊猫', '烈日灼心', '摆渡人', '长城', '我不是潘金莲', '这个杀手不太冷', '驴得水', '海贼王之黄金城', '西游伏妖片', '我在故宫修文物', '你的名字'],
hotTag: [
'动作', '喜剧', '爱情', '悬疑'],
}

上面的配置文件config.js里面,我统一抽取了各个页面的URL地址、关键词和标签(hotKeyword和hotTag)、城市及地址(city和location后面动态获取)、页面数量count等参数信息。

另外由于部分参数统一通过config.js获取,就不需要再次在调用的时候传入了,因此简化调用代码的参数传入,如下代码所示。

douban.fetchComming(that, that.data.start)

对于原先的代码

douban.fetchFilms.call(that, url, config.city, that.data.start, config.count)

简化的虽然不多,但是尽可能的保持干净简单的接口是我们的目标,而且这里把常规的URL等参数提取到函数里面,更加符合我们编码的习惯。

这里定义的
douban.fetchComming(that, that.data.start)
使用了Promise来简化代码,传入的that参数是因为需要在函数体里面设置该页面里面的Data等处理。

关于Promise的相关处理,我们在下面进行介绍。

2、使用Promise来优化JS代码

关于Promise的好处和如何使用Promise插件介绍,我在随笔《
在微信小程序的JS脚本中使用Promise来优化函数处理
》中已有介绍,我很喜欢使用这种Promise的风格代码,而且可以定义一些常用的辅助类来提高代码的重用。在我参考的这个豆瓣电影小程序还是使用常规回调的函数,对比原作者最新版本的
wechat-weapp-movie
小程序,也依旧使用回调函数模式来处理,有点奇怪为什么不引入Promise插件来开发。

原来的小程序,电影接口的相关处理,统一在fetch.js里面进行处理,这里封装对各种豆瓣API接口的调用。

这里我们来看看原来程序没有采用Promise的回调函数处理代码

var config = require('./config.js')var message = require('../../component/message/message')

module.exports
={
fetchFilms:
function(url, city, start, count, cb) {var that = this if(that.data.hasMore) {
wx.request({
url: url,
data: {
city: config.city,
start: start,
count: count
},
method:
'GET',
header: {
"Content-Type": "application/json,application/json"},
success:
function(res){if(res.data.subjects.length === 0){
that.setData({
hasMore:
false,
})
}
else{
that.setData({
films: that.data.films.concat(res.data.subjects),
start: that.data.start
+res.data.subjects.length,
showLoading:
false})
}
wx.stopPullDownRefresh()
typeof cb == 'function' &&cb(res.data)
},
fail:
function() {
that.setData({
showLoading:
false})
message.show.call(that,{
content:
'网络开小差了',
icon:
'warning',
duration:
3000})
}
})
}
},

这个函数是一个通用的函数,用来获取待映、热映、top250口碑的记录信息,不过它把参数抛给调用者传入,因此显得调用比较复杂一些,我们经过使用Promise优化代码处理,并对接口的参数进行简化,代码改造如下所示。

var config = require('./config.js')var message = require('../../component/message/message')var app = getApp()//获取应用实例
module.exports={//待映
    fetchComming : function(page, start) {return this.fetchFilms(page, config.coming_soon_url, config.city, start, config.count);
},
//热映 fetchPopular : function(page, start) {return this.fetchFilms(page, config.in_theaters_url, config.city, start, config.count);
},
//top250口碑 fetchTop : function(page, start) {return this.fetchFilms(page, config.top_url, config.city, start, config.count);
},
//通用的热映、待映的获取方式 fetchFilms: function(page, url, city, start, count) {return new Promise((resolve, reject) =>{var that =page;var json ={city: city, start: start, count: count };var type = "json";//特殊设置,默认是application/json if(that.data.hasMore) {
app.utils.get(url, json, type).then(res
=>{if(res.subjects.length === 0){
that.setData({
hasMore:
false,
})
}
else{
that.setData({
films: that.data.films.concat(res.subjects),
start: that.data.start
+res.subjects.length,
showLoading:
false})
}
wx.stopPullDownRefresh();

resolve(res);
})
}
})
},

最终的请求接口参数只有两个,一个是页面对象,一个是请求的起始位置,如下代码所示

function(page, start)

另外我们使用了代码

app.utils.get(url, json, type)

来对wx.request方法的统一封装,直接使用工具类里面的方法即可获取结果,不需要反复的、臃肿的处理代码。这就是我们使用Promise来优化JS,并抽取常用代码到工具类里面的做法。

我们再来对比一下获取电影详细信息的接口函数封装,原来代码如下所示。

    fetchFilmDetail: function(url, id, cb) {var that = this;
wx.request({
url: url
+id,
method:
'GET',
header: {
"Content-Type": "application/json,application/json"},
success:
function(res){
that.setData({
filmDetail: res.data,
showLoading:
false,
showContent:
true})
wx.setNavigationBarTitle({
title: res.data.title
})
wx.stopPullDownRefresh()
typeof cb == 'function' &&cb(res.data)
},
fail:
function() {
that.setData({
showLoading:
false})
message.show.call(that,{
content:
'网络开小差了',
icon:
'warning',
duration:
3000})
}
})
},

我改造后的函数代码如下所示。

    //获取电影详细信息
    fetchFilmDetail: function(page, id) {return new Promise((resolve, reject) =>{var that =page;var url = config.detail_url +id;var type = "json";//特殊设置,默认是application/json
        app.utils.get(url, {}, type).then(res =>{
that.setData({
filmDetail: res,
showLoading:
false,
showContent:
true});

wx.setNavigationBarTitle({
title: res.title
});
wx.stopPullDownRefresh();

resolve(res);
});
})
},

通过对fetch.js函数代码的改造处理,可以看到调用的JS代码参数减少了很多,而且页面也不用保留那么多连接等参数常量信息了。

    onLoad: function() {var that = thisdouban.fetchComming(that, that.data.start)
},

3、使用地理位置接口动态获取当前城市名称和坐标

原来程序使用硬编码的方式设置当前城市,如下脚本所示

module.exports ={
city:
'杭州',
count:
20}

不过我们不同地方的人员使用的时候,这个城市名称肯定需要变化的,因此可以使用微信的地理位置接口动态获取当前位置信息,然后写入到配置文件里面即可。

//获取当前位置信息
functiongetLocation (type) {return new Promise((resolve, reject) =>{
wx.getLocation({ type: type, success: resolve, fail: reject })
})
}
//根据坐标获取城市名称 function getCityName (latitude = 39.90403, longitude = 116.407526) {var data = { location: `${latitude},${longitude}`, output: 'json', ak: '6473aa8cbc349933ed841467bf45e46b'};var url = 'https://api.map.baidu.com/' + 'geocoder/v2/';var type = 'json';return this.get(url, data, type).then(res =>res.result.addressComponent.city);
}

然后我们在app.js里面编写代码,在app启动的时候,动态获取城市名称、坐标信息然后写入配置文件即可,这里使用的还是Promise的函数调用实现。

const utils  = require('./comm/script/util.js')
const config
= require('./comm/script/config.js')

App({
onLaunch:
function() {
utils.getLocation()
.then(res
=>{
const { latitude, longitude }
=res;
config.location
= `${longitude},${latitude}`;//当前坐标 console.log(`currentLocation : ${config.location}`);returnutils.getCityName(latitude, longitude)
})
.then(name
=>{
config.city
= name.replace('市', ''); //当前城市名称 console.log(`currentCity : ${config.city}`)
})
.
catch(err =>{
config.city
= '广州'console.error(err)
})
},
...

最后呈上改造过代码的运行界面,还是保留原来的功能正常使用。

以上就是我对这个小程序进行不同方面的调整思路和经验总结,希望大家有所收益或者建议,感谢阅读支持。

在微信开发中,我一直强调需要建立一个比较统一的Web API接口体系,以便实现数据的集中化,这样我们在常规的Web业务系统,Winform业务系统、微信应用、微信小程序、APP等方面,都可以直接调用基于JSON数据格式的Web API接口,在我之前的几篇随笔中,对这方面都有一定的介绍,本篇继续这个主题,细致深入的阐述如何在接口和源码的基础上整合Web API、微信后台管理及前端微信小程序的应用方案。

1、基于Web API的微信开发框架

首先我们各个业务模块,都应该围绕着Web API进行展开,如果是都部署在同一个服务器或者局域网内的系统,考虑到开发的复杂性,退而求其次,也可以基于同一个数据库进行开发。

总体而言,我们是以Web API为核心进行的应用框架构建的,如下图所示。

在项目场景中,我们这里的微信后台管理系统,是一个独立维护微信后台数据的管理系统,对于操作微信相关API所需要的接口调用凭证(token),我们可以通过Web API接口获得,这样保证各个平台(如Winform界面、其他Web界面),操作的接口token都保持一致

而后台管理系统,我们通过下面的来了解整体功能,整个后台管理系统使用了Bootstrap的框架进行前端处理。

2、整合Web API、微信后台管理及前端微信小程序应用

首先我们在Web API平台上,创建一个AccountController的MVC控制器来提供对应的API接口,实现对账号相关的信息查询,接口访问凭证的获取等业务。

    /// <summary>
    ///微信公众号、小程序、企业号的账号配置/// </summary>
    public class AccountController : BusinessController<Account, AccountInfo>

然后增加获取token的方法

        /// <summary>
        ///获取公众号/企业号/小程序操作的访问令牌AccessToken/// </summary>
        /// <param name="accountId">账号ID</param>
        /// <returns></returns>
[HttpGet]public string GetAccessTokenByAccount(stringaccountId)
{
var result =MyMemoryCache.GetAccessTokenByAccount(accountId);returnresult;
}

这个token的生成,是存储在内存缓存里面的,定期刷新的,这样我们可以避免频繁的请求接口凭证token,可以统一生成给各个业务系统使用 。

        /// <summary>
        ///获取公众号(或企业号)操作的访问令牌AccessToken/// </summary>
        /// <param name="accountId">账号ID</param>
        /// <returns></returns>
        public static string GetAccessTokenByAccount(stringaccountId)
{
var key = string.Format("{0}_{1}", System.Reflection.MethodBase.GetCurrentMethod().Name, accountId);var token = MemoryCacheHelper.GetCacheItem<string>(key, delegate()
{
string result = "";
AccountInfo info
=MyMemoryCache.GetAccountByID(accountId);if (info != null)
{
if (info.AccountType ==AccountType.企业号.ToString())
{
//获取微信企业号操作的访问令牌AccessToken ICorpBasicApi baseBLL = newCorpBasicApi();
result
=baseBLL.GetAccessToken(info.CorpID, info.CorpSecret);
}
else{//小程序、订阅号、服务器号共享一个逻辑//获取微信操作的访问令牌AccessToken WHC.Weixin.Interface.IBasicApi baseBLL = newWHC.Weixin.API.BasicApi();
result
=baseBLL.GetAccessToken(info.AppID, info.AppSecret);
}
}
returnresult;
},
new TimeSpan(0, 5, 0)//5分钟过期 );returntoken;
}

解决了token的统一生成和存储外,我们就可以在各个不同的系统中使用这个token 接口获取并使用来操作微信对应接口了。

                //客户端调试和服务端应用统一采用一个AccessToken的方式//避免客户端测试的时候,替换更新了服务端的AccessToken,从而导致服务端的AccessToken无效。
                HttpHelper helper = newHttpHelper();var tokenWebSiteUrl = string.Format("https://www.youdomain.com/api/Framework/Account/GetAccessTokenByAccount?accountId={0}", accountId);var result =helper.GetHtml(tokenWebSiteUrl);if(!string.IsNullOrEmpty(result))
{
result
= result.Trim('"');
}
Console.WriteLine(
"通过Web API 获取到的Token为:" +result);this.token = result;

关于微信后台管理系统,这个是对微信相关数据,如账号配置、菜单、事件、权限控制、业务数据定义等方面的综合管理,通过基于Bootstrap的MVC的技术进行Web端的数据维护,如下界面所示。

系统登录后,通过水平菜单进行后台功能管理。

系统支持多微信账号的接入管理和使用,同时支持订阅号、公众号、企业号、小程序的账号配置。

在系统中管理菜单,并通过把菜单提交到服务器上,实现菜单的动态配置和生成,能够为我们系统适应各种的需要,实现灵活的处理。

在微信服务账号的门户上,菜单的表现效果如下所示。

为了更有效管理订阅用户以及分组信息,我们可以从微信服务器上获取相关的信息,供我们了解关注的用户信息,也可以为后续的群发消息做准备。

基于Bootstrap的Web前端,我们处理H5页面起来也是得心应手,非常方便。

在我前面的一些案例中,都利用了We UI样式来进行很多微信H5页面的功能设计,包括微信支付页面、签到页面等等。如微信支付页面如下所示:



以及签到页面效果如下所示。


基于微信的H5页面,我们后台管理系统整合了一个实际的设备维修案例的微信应用场景,该需求主要围绕固定资产(如医疗设备)的微信应用展开,包括录入及查询资产信息、资产盘点、设备的维修保养、日常巡检、维修、计量检测等事务。

整个案例微信端应用采用的是H5页面以及微信的JSDK进行相关的接口开发,符合微信的界面风格。后端管理就是本后台管理系统。



设备盘点和设备计量如下所示:

预防性维护和设备计量界面如下所示。

3、微信小程序的后台和前端的应用整合

例如对于我们开发的《公司企业展示》小程序,适合展示公司信息,以及公司的产品信息,在这个应用中我们是整合了后台管理系统和微信前端开发实现的。

我们在微信后台管理系统中,实现商品数据的后台管理,数据数据我们分为几种类型,方便在前端界面展示。

商品编辑界面包括对基础信息的修改、封面和Banner图片的维护、以及商品多个展示图片、商品详细介绍的内容维护,如下界面所示。

除了商品的封面图片以及Banne图片外,我们在小程序的商品详细界面里面,需要在顶端展示多个可以滚动的图片效果,那么我们需要维护商品的图片,如下界面所示。

当然商品的详细信息需要一个富文本的编辑器来进行图片文字的编辑处理,如下界面所示。

上面介绍了管理后台的数据维护,我们就是基于上面的数据模型,在小程序上实现商品数据的展示的。

下图是小程序的商品展示首图,其中包括了顶部Banner栏目、中间的商品分类、底部的商品信息展示几部分。

而详细部分内容,则是需要滚动展示商品的多个图片,另外还需要展示详细的HTML内容,HTML内容的展示使用富文本转化插件wxParse即可实现,这部分在随笔《
在微信小程序中使用富文本转化插件wxParse
》有详细的使用介绍。

在微信小程序开发中,我们可以根据不同的业务场景,开发不同的业务应用,可以基于自身域名服务接口,也可以基于第三方的域名接口进行处理(如果被禁用除外),本篇随笔介绍使用小程序来实现我博客(http://wuhuacong.cnblogs.com)的文章阅读功能,这个小程序主要用来介绍使用介绍基于Javascript的正则表达式的处理应用,和常规在C#里面使用正则表达式有一些差异,因此可以作为后续使用正则表达式处理业务数据的一个练兵吧。

1、Request接口合法域名配置

一般情况下,我们知道微信的Request请求是需要配置合法的域名的,这种安全性可以是微信拦截有潜在危险的或者不喜欢的域名接口,Request合法域名配置界面如下所示。

一般情况下,我们在上面增加合法域名即可,这样小程序发布后,就可以顺利通过检查并获取数据了,本篇随笔由于想读取博客园个人博客园的文章,因此需要配置博客园的域名,不过很不幸,博客园的域名上了黑名单被禁用了。

如果我们在开发环境,我们可以通过不包含对合法域名的检验处理,不过在开发环境必须取消勾选“不校验”。

2、小程序功能设计

首先我们来看看主体界面的效果图,然后在进行分析具体的功能实现,具体界面效果如下所示。

博客文章列表内容如下所示:

文章详细界面效果如下所示:

这些文章直接都是从博客园页面中获取,并通过Javascript的正则表达式进行提取,然后展示在小程序上的,对于HTML内容的展示我们还是使用了WxParse的这个HTML解析组件,具体功能和使用过程可以参考我之前的随笔《
在微信小程序中使用富文本转化插件wxParse
》进行详细了解。对于Javascript函数的封装,我们还是使用比较方便的Promise进行封装处理,具体知识可以参考我随笔《
在微信小程序的JS脚本中使用Promise来优化函数处理
》进行详细了解。

一般我们也准备把公用方法提取出来,放到工具类Utils/util.js里面,配置统一放到utils/config.js里面,这样方便小程序的模块化处理。

项目的文件目录如下所示。

在Utils/util.js里面,我们封装了wx.request的获取内容方法如下所示。

//封装Request请求方法
function request(url,method,data = {},type='application/json'){
wx.showNavigationBarLoading();
return new Promise((resove,reject) =>{
wx.request({
url: url,
data: data,
header: {
'Content-Type': type},
method: method.toUpperCase(),
//OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT success: function(res){
wx.hideNavigationBarLoading()
resove(res.data)
},
fail:
function(msg) {
console.log(
'reqest error',msg)
wx.hideNavigationBarLoading()
reject(
'fail')
}
})
})
}

而在Config.js里面,我们主要定义好一些常用的参数,如URL等

在列表页面,我们主要是展示文字标题和日期等信息,而列表是需要滚动翻页的,因此我们使用微信界面组件
scroll-view
来展示,具体界面代码如下所示。

<blockwx:if="{{showLoading}}">
    <viewclass="loading">玩命加载中…</view>
</block>
<blockwx:else>
    <scroll-viewscroll-y="true"style="height: {{windowHeight}}rpx"scroll-top="{{scrollTop}}"bindscroll="scroll"bindscrolltolower="scrolltolower">
        <viewclass="blog">
            <blockwx:for="{{blogs}}"wx:for-index="blogIndex"wx:for-item="blogItem"wx:key="blog">
                <viewdata-id="{{blogItem.id}}"catchtap="viewBlogDetail"class="flex box box-lr item">
                  <viewclass="flex item_left">
                    <view><textclass="title">{{blogItem.title}}</text></view>
                    <view><textclass="sub_title">{{blogItem.date}}</text></view>
                  </view>
                </view> 
            </block>
            <blockwx:if="{{hasMore}}">
                <viewclass="loading-tip">拼命加载中…</view>
            </block>
            <blockwx:else>
                <viewclass="loading-tip">没有更多内容了</view>
            </block>
        </view>
    </scroll-view>
</block>

通过绑定向下滑动的事件 bindscrolltolower="scrolltolower" 我们可以实现列表内容的滚动刷新。

我们通过前面介绍的封装Request方法,可以获取到HTML内容,如下函数所示。

  //获取博客文章列表
  getList:function(start =1) {return new Promise((resolve, reject) =>{var that = this;var data ={};var type = "text/html";var url = config.mainblog_url +start;if(that.data.hasMore) {
app.utils.get(url, data, type).then(res
=> {

通过指定type = "text/html",并且传入对应的起始位置,可以获取到对应页面的内容。

在博客园里面,【我的随笔】里面的标准URL地址为:http://www.cnblogs.com/wuhuacong/p/?page=n,其中 n 是当前的页码。

页面上的页码效果如下所示:

分析页面源码,可以看到页码标签的源码如下所示。

因此我们对HTML源码进行正则表达式的匹配即可获取对应的内容(关键是获取多少页,为后面循环获取文章列表做准备)。

关于正则表达式的测试,建议使用RegExBuilder(http://www.jb51.net/softs/389196.html)进行测试,其实我之前倾向于使用The Regulator 2.0这个很好的程序测试正则表达式,不过这个软件经常性的不能启动使用。

不过后来测试使用RegExBuilder,也觉得非常不错,其中勾选ECMAScript是为了我们在Javascript使用正则表达式的选项,毕竟和在C#里面使用正则标识还是有一些差异,如不支持单行选择,以及一些微小差异。

对于在Javascript中使用正则标识,建议大家温习下下面几篇随笔,有所帮助:


深入浅出的javascript的正则表达式学习教程


使用javascript正则表达式实现遍历html字符串


JavaScript之正则表达式

以及一个常见的坑,在HTML内容匹配的时候,不支持.*这种很普通的模式,这种由于不能选择单行模式导致的,变通的方式是使用[\s\S]来实现匹配所有字符处理。

可以参考文章:
https://stackoverflow.com/questions/1068280/javascript-regex-multiline-flag-doesnt-work
了解下。

另外对于Javascript的正则书写,经常看到i,g,m的结束符

修饰符   描述
i (ignore case)
执行对大小写不敏感的匹配。
g (global search)
执行全局匹配(查找所有匹配而非在找到第一个匹配后停止)。
m (multiline)
执行多行匹配。

它的意思你参考文章了解:

https://zhidao.baidu.com/question/620656820875333772.html

Javascript的正则匹配处理,
支持正则表达式的String对象的方法
可以使用
search()方法、
match()方法、
replace()方法、
split()方法、


RegExp对象方法包括:

test()方法、
exec()方法。

具体Javascript的正则表达式使用,可以好好学习下《
深入浅出的javascript的正则表达式学习教程
》,就很清晰了。

例如对于我们这篇小程序,我们获取页码的js代码如下所示。

  var reg = /共(\d{0,})页/g;var pageNum = reg.exec(html)[1];//console.log(pageNum);
that.setData({
end:pageNum,
//设置最大页码 });

在获取每篇随笔文章的标题、URL、日期等信息,我编写了一个正则表达式来匹配,如下所示。

var regContent=/class=\"postTitl2\">.*?href=\"(.*?)\">(.*?)<\/a>.*?class=\"postDesc2\">(.*?)\s*阅读:.*?<\/div>/igm;

正则表达式的内容,在使用前,一定需要在这个工具上测试,测试通过了我们再在代码上使用,减少调试错误的时间。

下面的测试结果如下所示。

获取文章列表信息的小程序js代码如下所示。

在之前介绍的列表展示界面代码里面,我们绑定了单击连接的事件,如下界面所示标注所示。

这个事件就是触发导航到详细界面,如下所示,我们把URL作为id传入到详细的界面里面。

  viewBlogDetail: function(e) {var data =e.currentTarget.dataset;var url =data.id;//console.log(url);
wx.navigateTo({
url:
"../details/details?id=" +data.id
})
},

在文章详细界面展示里面,界面的代码如下所示。

<importsrc="../../utils/wxParse/wxParse.wxml" />
<viewclass="flex box box-lr item">
    <viewclass="flex item_left">
        <view>
            <textclass="title">{{detail.title}}</text>
        </view>
    </view>
</view>
<viewclass="page">
    <viewclass="page__bd">
        <viewclass="weui-article">
            <viewclass="weui-article__p">
                <templateis="wxParse"data="{{wxParseData:article.nodes}}"/>
             </view>
        </view>
    </view>
</view>

这里引入了WxParse作为HTML内容解析的组件,我们在页面代码顶部引入组件代码

<importsrc="../../utils/wxParse/wxParse.wxml" />

具体处理的内容在JS代码里面,我们的JS代码如下所示。

  //获取文章详细内容
getDetail(url) {var that = this;var type = "text/html";
app.utils.get(url, {}, type).then(res
=>{//console.log(res); var html =res;var regContent = /id=\"cb_post_title_url\".*?>([\s\S]*?)<\/a>[\s\S]*?id=\"cnblogs_post_body\">([\s\S]*?)<\/div><div\s*id=\"MySignature\">/igmvarmatchArr;if ((matchArr =regContent.exec(html))) {var detail ={
id: url,
title : matchArr[
1], //titile content : matchArr[2], //content };

that.setData({
detail:detail
});
WxParse.wxParse(
'article', 'html', detail.content, that, 5);
};
});
},

其中的Javascript的正则表达式如下:

var regContent = /id=\"cb_post_title_url\".*?>([\s\S]*?)<\/a>[\s\S]*?id=\"cnblogs_post_body\">([\s\S]*?)<\/div><div\s*id=\"MySignature\">/igm

我们在工具上测试,得到相关的效果后再在代码上使用。

最后就可以获得详细的展示效果了,文章详细界面效果如下所示:

在我们开发系统界面,包括Web和Winform的都一样,主要的界面就是列表展示主界面,编辑查看界面,以及一些辅助性的如导入界面,选择界面等,其中列表展示主界面是综合性的数据展示界面,一般往往需要对记录进行合理的分页,集成各种增删改查的按钮等功能。随着开发项目的需求变化,对数据记录分页展示、排序等功能都是常态的要求,因此在代码生成工具中调整了主列表界面的列表展示插件为Bootstrap-table插件,本篇随笔主要介绍在代码生成工具Database2Sharp中集成对Bootstrap-table插件的分页及排序支持功能。

1、Web界面列表分页处理

1)常规分页方式

最开始的Web界面列表分页,使用较为常规Bootstrap Paginator分页模式,内容生成以HTML组合方式,先设置表头,然后获取分页列表数据,遍历生成相关的HTML数据,增加到页面上,这种方式也是比较高效的处理方式,如我在本系列开始的随笔《
基于Metronic的Bootstrap开发框架经验总结(2)--列表分页处理和插件JSTree的使用
》介绍中一样。有时候为了业务数据的快速查询,也会在左侧放置一个树列表方便查询分类,界面如下所示。

这种方式可控性非常好,而且可以对HTML代码进行完全的控制,非常适合在自定义界面中展示一些数据,如我之前介绍过的图标分页展示界面 一样,完全是自定义的内容展示,图标界面如下所示。

2)Bootstrap-table插件分页

使用常规的分页方式界面可控性非常方便,不过随着不同项目的一些特殊要求,对表头排序的需求也是非常强烈的,上面的分页处理方式无法实现表头的排序功能,因此引入了使用非常广泛的Bootstrap-Table插件,该插件应用很广、功能非常强大,可以通过属性配置实现很细致的功能控制。Bootstrap-table插件提供了非常丰富的属性设置,可以实现查询、分页、排序、复选框、设置显示列、Card View视图、主从表显示、合并列、国际化处理等处理功能,而且该插件同时也提供了一些不错的扩展功能,如移动行、移动列位置等一些特殊的功能。

因此我对这个插件进行了使用研究并进行总结,这个插件的详细使用可以参考我的随笔《
基于Metronic的Bootstrap开发框架经验总结(16)-- 使用插件bootstrap-table实现表格记录的查询、分页、排序等处理
》进行了解。这个插件界面展示也是非常美观的。

这个插件最显著的特点就是完美支持客户端或者服务器的数据列排序处理,单击表头就可以实现排序操作。

2、在代码生成工具Database2Sharp中集成对Bootstrap-table插件的分页及排序支持

我们的代码生成工具Database2Sharp是为了框架开发服务的,不管是Winform还是Web开发,都可以基于数据库的基础上进行框架代码的快速生成,以及界面的代码生成,本次调整的代码生成工具功能,在列表界面代码中增加了对Bootstrap-table插件分页的支持,使得我们开发Bootstrap框架的界面代码更加丰富、快捷。

在代码生成工具Database2Sharp上,我们先使用Enterprise Library代码增量生成主体框架的框架代码。

然后在使用Bootstrap的Web界面代码生成功能,如下可以在工具栏界面中选择。

选择数据库和表后,可以进行界面代码(包括控制器代码、视图界面代码)两部分,其中视图分为两种模式,一种是利用Bootstrap-table插件的分页及排序(index.cshtml),一种是常规的Bootstrap Paginator分页处理(index2.cshtml)。

老客户可以继续使用index2.cshtml的样式,也可以使用最新的Bootstrap-table插件的分页及排序方式(index.cshtml)。

生成的界面分为HTML部分和JS部分,都是比较紧密联系的两部分,我们进行一定的调整即可实现丰富的界面排版。

部分的JS代码(展示分页部分处理)如下所示。

列表数据的显示列,默认是以数据库的字段进行生成,我们在生成后可以进行一定的调整,可以合理显示我们关注的数据。

当然生成的界面代码还有很多其他的JS代码,如编辑、查看的代码和控件对应,导入、导出等代码的处理,都是一并生成的,我们根据需要进行一定的裁剪调整即可完成整个界面的处理了,极大的提高开发效率。

在我们的很多框架或者项目应用中,缓存在一定程度上可以提高程序的响应速度,以及减轻服务器的承载压力,因此在一些地方我们都考虑引入缓存模块,这篇随笔介绍使用开源缓存框架CacheManager来实现数据的缓存,在微信开发框架中,我们有一些常用的处理也需要应用到缓存,因此本随笔以微信框架为例介绍缓存的实际使用,实际上,在我们很多框架中,如混合式开发框架、Web开发框架、Bootstrap开发框架中,这个模块都是通用的。

1、框架的缓存设计

在我们的微信开发框架中,缓存作为数据库和对外接口之间的一个分层,提供数据的缓存响应处理,如下结构所示是Web API层对缓存的架构设计。

在缓存的处理中,我侧重于使用CacheManager,这个缓存框架是一个集大成者,关于CacheManager 的介绍,我们可以回顾下我之前的随笔《
.NET缓存框架CacheManager在混合式开发框架中的应用(1)-CacheManager的介绍和使用
》。

CacheManager是一个以C#语言开发的开源.Net缓存框架抽象层。它不是具体的缓存实现,但它支持多种缓存提供者(如Redis、Memcached等)并提供很多高级特性。
CacheManager 主要的目的使开发者更容易处理各种复杂的缓存场景,使用CacheManager可以实现多层的缓存,让进程内缓存在分布式缓存之前,且仅需几行代码来处理。
CacheManager 不仅仅是一个接口去统一不同缓存提供者的编程模型,它使我们在一个项目里面改变缓存策略变得非常容易,同时也提供更多的特性:如缓存同步、并发更新、序列号、事件处理、性能计算等等,开发人员可以在需要的时候选择这些特性。

CacheManager的GitHub源码地址为:
https://github.com/MichaCo/CacheManager
,如果需要具体的Demo及说明,可以访问其官网:
http://cachemanager.michaco.net

2、在微信框架中整合CacheManager 缓存框架

在使用CacheManager 缓存的时候,我们可以直接使用相关对象进行处理,首先需要定义一个类来进行初始化缓存的设置,然后进行调用,调用的时候可以使用IOC的方式构建对象,如下代码所示创建一个自定义的缓存管理类

    /// <summary>
    ///基于CacheManager的接口处理/// </summary>
    public classCacheManager : ICacheManager
{
/// <summary> ///ICacheManager对象/// </summary> public ICacheManager<object> Manager { get; set; }/// <summary> ///默认构造函数/// </summary> publicCacheManager()
{
//初始化缓存管理器 Manager = CacheFactory.Build("getStartedCache", settings =>{
settings
.WithSystemRuntimeCacheHandle(
"handleName")
.And
.WithRedisConfiguration(
"redis", config =>{
config.WithAllowAdmin()
.WithDatabase(
0)
.WithEndpoint(
"localhost", 6379);
})
.WithMaxRetries(
100)
.WithRetryTimeout(
50)
.WithRedisBackplane(
"redis")
.WithRedisCacheHandle(
"redis", true)
;
});
}
}
}

然后在Autofac的配置文件中配置缓存的相关信息,如下文件所示。

如果直接使用Autofac的构造类来处理,那么调用缓存处理的代码如下所示。

            //通过AutoFac工厂获取对应的接口实现
            var cache = AutoFactory.Instatnce.Container.Resolve<ICacheManager>();if (cache != null)
{
accountInfo
= cache.Manager.Get(key) asAccountInfo;if (accountInfo == null)
{
var value = BLLFactory<Account>.Instance.FindByID(accountId);var item = new CacheItem<object>(key, value, ExpirationMode.Absolute, TimeSpan.FromMinutes(TimeOut_Minutes));
cache.Manager.Put(item);

accountInfo
= cache.Manager.Get(key) asAccountInfo;
}
}

如果为了使用方便,我们还可以对这个辅助类进行进一步的封装,以便对它进行统一的调用处理即可。

    /// <summary>
    ///基于.NET CacheManager的缓存管理,文档参考:http://cachemanager.michaco.net/documentation
    /// </summary>
    public classCacheManagerHelper
{
/// <summary> ///锁定处理变量/// </summary> private static readonly object locker = new object();/// <summary> ///创建一个缓存的键值,并指定响应的时间范围,如果失效,则自动获取对应的值/// </summary> /// <typeparam name="T">对象类型</typeparam> /// <param name="key">对象的键</param> /// <param name="cachePopulate">获取缓存值的操作</param> /// <param name="expiration">失效的时间范围</param> /// <param name="mode">失效类型</param> /// <returns></returns> public static T GetCacheItem<T>(string key, Func<T>cachePopulate, TimeSpan expiration,string region = "_", ExpirationMode mode = ExpirationMode.Sliding) where T :class{
CacheItem
<object> outItem = null;//通过AutoFac工厂获取对应的接口实现 var cache = AutoFactory.Instatnce.Container.Resolve<ICacheManager>();if (cache != null)
{
if (cache.Manager.Get(key, region) == null)
{
lock(locker)
{
if (cache.Manager.Get(key, region) == null)
{
//Add、Put差异,Add只有在空值的情况下执行加入并返回true,Put总会替换并返回True//如果按下面的方式加入,那么会留下历史丢弃的键值: cache.Manager.Put(key, value); var value =cachePopulate();var item = new CacheItem<object>(key, region, value, mode, expiration);
cache.Manager.Put(item);
}
}
}
return cache.Manager.Get(key, region) asT;
}
else{throw new ArgumentNullException("AutoFac配置参数错误,请检查autofac.config是否存在ICacheManager的定义");
}
}
}

不过由于官方已经提供了一个类似上面的代码逻辑的TryGetOrAdd方法,这个方法的定义如下所示。

TryGetOrAdd(String, String, Func<String, String, TCacheValue>, out TCacheValue)

Tries to either retrieve an existing item or add the item to the cache if it does not exist. The
valueFactory will be evaluated only if the item does not exist.

Declaration
boolTryGetOrAdd(stringkey, stringregion, Func<string, string, TCacheValue> valueFactory, outTCacheValue value)
Parameters
Type Name Description
String key

The cache key.

String region

The cache region.

Func
<
String
,
String
, TCacheValue>
valueFactory

The method which creates the value which should be added.

TCacheValue value

The cache value.

Returns
Type Description
Boolean

True
if the operation succeeds,
False
in case there are too many retries or the
valueFactory returns null.

我们根据这个参数的定义,可以进一步简化上面的辅助类代码。

                cache.Manager.TryGetOrAdd(key, region, (_key, _region) =>{var value =cachePopulate();var item = new CacheItem<object>(key, region, value, mode, expiration);returnitem;
},
outoutItem);return outItem as T;

整个类的代码如下所示

    /// <summary>
    ///基于.NET CacheManager的缓存管理,文档参考:http://cachemanager.michaco.net/documentation
    /// </summary>
    public classCacheManagerHelper
{
/// <summary> ///创建一个缓存的键值,并指定响应的时间范围,如果失效,则自动获取对应的值/// </summary> /// <typeparam name="T">对象类型</typeparam> /// <param name="key">对象的键</param> /// <param name="cachePopulate">获取缓存值的操作</param> /// <param name="expiration">失效的时间范围</param> /// <param name="mode">失效类型</param> /// <returns></returns> public static T GetCacheItem<T>(string key, Func<T>cachePopulate, TimeSpan expiration,string region = "_", ExpirationMode mode = ExpirationMode.Sliding) where T :class{
CacheItem
<object> outItem = null;//通过AutoFac工厂获取对应的接口实现 var cache = AutoFactory.Instatnce.Container.Resolve<ICacheManager>();if (cache != null)
{
cache.Manager.TryGetOrAdd(key, region, (_key, _region)
=>{var value =cachePopulate();var item = new CacheItem<object>(key, region, value, mode, expiration);returnitem;
},
outoutItem);return outItem asT;
}
else{throw new ArgumentNullException("AutoFac配置参数错误,请检查autofac.config是否存在ICacheManager的定义");
}
}
}

这样代码就简化了不少,而且不用自己控制读取的线程锁了,下面代码是使用辅助类实现缓存的添加及获取处理。

        /// <summary>
        ///为避免频繁的对数据库检索,提高获取账号信息的速度///我们把账号信息根据ID缓存起来,方便快速使用,提高效率。/// </summary>
        public static AccountInfo GetAccountByID(stringaccountId)
{
AccountInfo accountInfo
= null;#region 使用.NET CacheManager缓存 //正常情况下access_token有效期为7200秒,这里使用缓存设置短于这个时间即可 var key = "GetAccountByID_" +accountId;
accountInfo
= CacheManagerHelper.GetCacheItem<AccountInfo>(key, () =>{return BLLFactory<Account>.Instance.FindByID(accountId);
}, TimeSpan.FromMinutes(TimeOut_Minutes));
returnaccountInfo;
}

通过这样的辅助类封装,我们可以在需要缓存的函数里面,统一使用辅助类对数据进行缓存或者读取缓存的操作。

我们也可以直接使用Autofac构建的缓存管理进行操作,如在小程序里面,我们对用户敏感数据的解密处理函数,如下所示。

        /// <summary>  
        ///根据微信小程序平台提供的解密算法解密数据/// </summary>  
[HttpGet]public SmallAppUserInfo Decrypt(string encryptedData, string iv, stringthirdkey)
{
SmallAppUserInfo userInfo
= null;//通过AutoFac工厂获取对应的接口实现 var cache = AutoFactory.Instatnce.Container.Resolve<ICacheManager>();if (cache != null)
{
//从缓存里面,获取对应的SessionKey var sessionkey =cache.Manager.Get(thirdkey);if (sessionkey != null)
{
//对用户身份加密数据进行解析,获取包含openid等属性的完整对象 IBasicApi api = newBasicApi();
userInfo
=api.Decrypt(encryptedData, iv, sessionkey.ToString());
}
}
returnuserInfo;
}