了解小程序登陆之前,我们写了解下小程序/公众号登录涉及到两个最关键的用户标识:
OpenId
是一个用户对于一个小程序/公众号的标识,开发者可以通过这个标识识别出用户。UnionId
是一个用户对于同主体微信小程序/公众号/APP的标识,开发者需要在微信开放平台下绑定相同账号的主体。开发者可通过UnionId,实现多个小程序、公众号、甚至APP 之间的数据互通了。wx.login
官方提供的登录能力
wx.checkSession
校验用户当前的session_key是否有效
wx.authorize
提前向用户发起授权请求
wx.getUserInfo
获取用户基本信息
以下从笔者接触过的几种登录流程来做阐述:
直接复用现有系统的登录体系,只需要在小程序端设计用户名,密码/验证码输入页面,便可以简便的实现登录,只需要保持良好的用户体验即可。
?提过,OpenId
是一个小程序对于一个用户的标识,利用这一点我们可以轻松的实现一套基于小程序的用户体系,值得一提的是这种用户体系对用户的打扰最低,可以实现静默登录。具体步骤如下:
小程序客户端通过 wx.login
获取 code
传递 code 向服务端,服务端拿到 code 调用微信登录凭证校验接口,微信服务器返回 openid
和会话密钥 session_key
,此时开发者服务端便可以利用 openid
生成用户入库,再向小程序客户端返回自定义登录态
小程序客户端缓存 (通过storage
)自定义登录态(token),后续调用接口时携带该登录态作为用户身份标识即可
如果想实现多个小程序,公众号,已有登录系统的数据互通,可以通过获取到用户 unionid 的方式建立用户体系。因为 unionid 在同一开放平台下的所所有应用都是相同的,通过 unionid
建立的用户体系即可实现全平台数据的互通,更方便的接入原有的功能,那如何获取 unionid
呢,有以下两种方式:
如果户关注了某个相同主体公众号,或曾经在某个相同主体App、公众号上进行过微信登录授权,通过 wx.login
可以直接获取 到 unionid
结合 wx.getUserInfo
和 这两种方式引导用户主动授权,主动授权后通过返回的信息和服务端交互 (这里有一步需要服务端解密数据的过程,很简单,微信提供了示例代码) 即可拿到
unionid
建立用户体系, 然后由服务端返回登录态,本地记录即可实现登录,附上微信提供的最佳实践:
调用 wx.login 获取 code,然后从微信后端换取到 session_key,用于解密 getUserInfo返回的敏感数据。
使用 wx.getSetting 获取用户的授权情况
获取到用户数据后可以进行展示或者发送给自己的后端。
unionid
形式的登录体系,在以前(18年4月之前)是通过以下这种方式来实现,但后续微信做了调整(因为一进入小程序,主动弹起各种授权弹窗的这种形式,比较容易导致用户流失),调整为必须使用按钮引导用户主动授权的方式,这次调整对开发者影响较大,开发者需要注意遵守微信的规则,并及时和业务方沟通业务形式,不要存在侥幸心理,以防造成小程序不过审等情况。 wx.login(获取code) ===> wx.getUserInfo(用户授权) ===> 获取 unionid
复制代码
因为小程序不存在 cookie
的概念, 登录态必须缓存在本地,因此强烈建议为登录态设置过期时间
值得一提的是如果需要支持风控安全校验,多平台登录等功能,可能需要加入一些公共参数,例如platform,channel,deviceParam等参数。在和服务端确定方案时,作为前端同学应该及时提出这些合理的建议,设计合理的系统。
openid
, unionid
不要在接口中明文传输,这是一种危险的行为,同时也很不专业。
经常开发和使用小程序的同学对这个功能一定不陌生,这是一种常见的引流方式,一般同时会在图片中附加一个小程序二维码。
借助 canvas
元素,将需要导出的样式首先在 canvas
画布上绘制出来 (api基本和h5保持一致,但有轻微差异,使用时注意即可)
借助微信提供的 canvasToTempFilePath
导出图片,最后再使用 saveImageToPhotosAlbum
(需要授权)保存图片到本地
根据上述的原理来看,实现是很简单的,只不过就是设计稿的提取,绘制即可,但是作为一个常用功能,每次都这样写一坨代码岂不是非常的难受。那小程序如何设计一个通用的方法来帮助我们导出图片呢?思路如下:
绘制出需要的样式这一步是省略不掉的。但是我们可以封装一个绘制库,包含常见图形的绘制,例如矩形,圆角矩形,圆, 扇形, 三角形, 文字,图片减少绘制代码,只需要提炼出样式信息,便可以轻松的绘制,最后导出图片存入相册。笔者觉得以下这种方式绘制更为优雅清晰一些,其实也可以使用加入一个type参数来指定绘制类型,传入的一个是样式数组,实现绘制。
结合上一步的实现,如果对于同一类型的卡片有多次导出需求的场景,也可以使用自定义组件的方式,封装同一类型的卡片为一个通用组件,在需要导出图片功能的地方,引入该组件即可。
class CanvasKit {
constructor() {
}
drawImg(option = {}) {
...
return this
}
drawRect(option = {}) {
return this
}
drawText(option = {}) {
...
return this
}
static exportImg(option = {}) {
...
}
}
let drawer = new CanvasKit('canvasId').drawImg(styleObj1).drawText(styleObj2)
drawer.exportImg()
复制代码
短链接
的方式来解决数据统计作为目前一种常用的分析用户行为的方式,小程序端也是必不可少的。小程序采取的曝光,点击数据埋点其实和h5原理是一样的。但是埋点作为一个和业务逻辑不相关的需求,我们如果在每一个点击事件,每一个生命周期加入各种埋点代码,则会干扰正常的业务逻辑,和使代码变的臃肿,笔者提供以下几种思路来解决数据埋点:
小程序的代码结构是,每一个 Page 中都有一个 Page 方法,接受一个包含生命周期函数,数据的 业务逻辑对象
包装这层数据,借助小程序的底层逻辑实现页面的业务逻辑。通过这个我们可以想到思路,对Page进行一次包装,篡改它的生命周期和点击事件,混入埋点代码,不干扰业务逻辑,只要做一些简单的配置即可埋点,简单的代码实现如下:
代码仅供理解思路
page = function(params) {
let keys = params.keys()
keys.forEach(v => {
if (v === 'onLoad') {
params[v] = function(options) {
stat() //曝光埋点代码
params[v].call(this, options)
}
}
else if (v.includes('click')) {
params[v] = funciton(event) {
let data = event.dataset.config
stat(data) // 点击埋点
param[v].call(this)
}
}
})
}
复制代码
这种思路不光适用于埋点,也可以用来作全局异常处理,请求的统一处理等场景。
对于特殊的一些业务,我们可以采取 接口埋点
,什么叫接口埋点呢?很多情况下,我们有的api并不是多处调用的,只会在某一个特定的页面调用,通过这个思路我们可以分析出,该接口被请求,则这个行为被触发了,则完全可以通过服务端日志得出埋点数据,但是这种方式局限性较大,而且属于分析结果得出过程,可能存在误差,但可以作为一种思路了解一下。
微信本身提供的数据分析能力,微信本身提供了常规分析和自定义分析两种数据分析方式,在小程序后台配置即可。借助小程序数据助手
这款小程序可以很方便的查看。
目前的前端开发过程,工程化是必不可少的一环,那小程序工程化都需要做些什么呢,先看下目前小程序开发当中存在哪些问题需要解决:
对于目前常用的工程化方案,webpack,rollup,parcel等来看,都常用与单页应用的打包和处理,而小程序天生是 “多页应用” 并且存在一些特定的配置。根据要解决的问题来看,无非是文件的编译,修改,拷贝这些处理,对于这些需求,我们想到基于流的 gulp
非常的适合处理,并且相对于webpack配置多页应用更加简单。所以小程序工程化方案推荐使用 gulp
通过 gulp 的 task 实现:
上述实现起来其实并不是很难,但是这样的话就是一份纯粹的 gulp 构建脚本和 约定好的目录而已,每次都有一个新的小程序都来拷贝这份脚本来处理吗?显然不合适,那如何真正的实现 小程序工程化
呢?
我们可能需要一个简单的脚手架,脚手架需要支持的功能:
微信小程序的框架包含两部分 View 视图层、App Service逻辑层。View 层用来渲染页面结构,AppService 层用来逻辑处理、数据请求、接口调用。
它们在两个线程里运行。
它们在两个线程里运行。
它们在两个线程里运行。
视图层和逻辑层通过系统层的 JSBridage 进行通信,逻辑层把数据变化通知到视图层,触发视图层页面更新,视图层把触发的事件通知到逻辑层进行业务处理。
补充
视图层使用 WebView 渲染,iOS 中使用自带 WKWebView,在 Android 使用腾讯的 x5 内核(基于 Blink)运行。
逻辑层使用在 iOS 中使用自带的 JSCore 运行,在 Android 中使用腾讯的 x5 内核(基于 Blink)运行。
开发工具使用 nw.js 同时提供了视图层和逻辑层的运行环境。
在 Mac下 使用 js-beautify 对微信开发工具 @v1.02.1808080代码批量格式化:
cd /Applications/wechatwebdevtools.app/Contents/Resources/package.nw
find . -type f -name '*.js' -not -path "./node_modules/*" -not -path -exec js-beautify -r -s 2 -p -f '{}' \;
复制代码
在 js/extensions/appservice/index.js
中找到:
267: function(a, b, c) {
const d = c(8),
e = c(227),
f = c(226),
g = c(228),
h = c(229),
i = c(230);
var j = window.__global.navigator.userAgent,
k = -1 !== j.indexOf('game');
k || i(), window.__global.getNewWeixinJSBridge = (a) => {
const {
invoke: b
} = f(a), {
publish: c
} = g(a), {
subscribe: d,
triggerSubscribeEvent: i
} = h(a), {
on: j,
triggerOnEvent: k
} = e(a);
return {
invoke: b,
publish: c,
subscribe: d,
on: j,
get __triggerOnEvent() {
return k
},
get __triggerSubscribeEvent() {
return i
}
}
}, window.WeixinJSBridge = window.__global.WeixinJSBridge = window.__global.getNewWeixinJSBridge('global'), window.__global.WeixinJSBridgeMap = {
__globalBridge: window.WeixinJSBridge
}, __devtoolsConfig.online && __devtoolsConfig.autoTest && setInterval(() => {
console.clear()
}, 1e4);
try {
var l = new window.__global.XMLHttpRequest;
l.responseType = 'text', l.open('GET', `http://${window.location.host}/calibration/${Date.now()}`, !0), l.send()
} catch (a) {}
}
复制代码
在 js/extensions/gamenaitveview/index.js
中找到:
299: function(a, b, c) {
'use strict';
Object.defineProperty(b, '__esModule', {
value: !0
});
var d = c(242),
e = c(241),
f = c(243),
g = c(244);
window.WeixinJSBridge = {
on: d.a,
invoke: e.a,
publish: f.a,
subscribe: g.a
}
},
复制代码
在 js/extensions/pageframe/index.js
中找到:
317: function(a, b, c) {
'use strict';
function d() {
window.WeixinJSBridge = {
on: e.a,
invoke: f.a,
publish: g.a,
subscribe: h.a
}, k.a.init();
let a = document.createEvent('UIEvent');
a.initEvent('WeixinJSBridgeReady', !1, !1), document.dispatchEvent(a), i.a.init()
}
Object.defineProperty(b, '__esModule', {
value: !0
});
var e = c(254),
f = c(253),
g = c(255),
h = c(256),
i = c(86),
j = c(257),
k = c.n(j);
'complete' === document.readyState ? d() : window.addEventListener('load', function() {
d()
})
},
复制代码
我们都看到了 WeixinJSBridge 的定义。分别都有 on
、invoke
、publish
、subscribe
这个几个关键方法。
拿 invoke
举例,在 js/extensions/appservice/index.js
中发现这段代码:
f (!r) p[b] = s, f.send({
command: 'APPSERVICE_INVOKE',
data: {
api: c,
args: e,
callbackID: b
}
});
复制代码
在 js/extensions/pageframe/index.js
中发现这段代码:
g[d] = c, e.a.send({
command: 'WEBVIEW_INVOKE',
data: {
api: a,
args: b,
callbackID: d
}
})
复制代码
简单的分析得知:字段 command
用来区分行为,invoke
用来调用 Native 的 Api。在不同的来源要使用不同的前缀。data
里面包含 Api 名,参数。另外 callbackID
指定接受回调的方法句柄。Appservice 和 Webview 使用的通信协议是一致的。
我们不能在代码里使用 BOM 和 DOM 是因为根本没有,另一方面也不希望 JS 代码直接操作视图。
在开发工具中 remote-helper.js
中找到了这样的代码:
const vm = require("vm");
const vmGlobal = {
require: undefined,
eval: undefined,
process: undefined,
setTimeout(...args) {
//...省略代码
return timerCount;
},
clearTimeout(id) {
const timer = timers[id];
if (timer) {
clearTimeout(timer);
delete timers[id];
}
},
setInterval(...args) {
//...省略代码
return timerCount;
},
clearInterval(id) {
const timer = timers[id];
if (timer) {
clearInterval(timer);
delete timers[id];
}
},
console: (() => {
//...省略代码
return consoleClone;
})()
};
const jsVm = vm.createContext(vmGlobal);
// 省略大量代码...
function loadCode(filePath, sourceURL, content) {
let ret;
try {
const script = typeof content === 'string' ? content : fs.readFileSync(filePath, 'utf-8').toString();
ret = vm.runInContext(script, jsVm, {
filename: sourceURL,
});
}
catch (e) {
// something went wrong in user code
console.error(e);
}
return ret;
}
复制代码
这样的分层设计显然是有意为之的,它的中间层完全控制了程序对于界面进行的操作, 同时对于传递的数据和响应时间也能做到监控。一方面程序的行为受到了极大限制, 另一方面微信可以确保他们对于小程序内容和体验有绝对的控制。
这样的结构也说明了小程序的动画和绘图 API 被设计成生成一个最终对象而不是一步一步执行的样子, 原因就是 Json 格式的数据传递和解析相比与原生 API 都是损耗不菲的,如果频繁调用很可能损耗过多性能,进而影响用户体验。
1.动画需要绑定在 data 上,而绘图却不用。你觉得是为什么呢?
var context = wx.createCanvasContext('firstCanvas')
context.setStrokeStyle("#00ff00")
context.setLineWidth(5)
context.rect(0, 0, 200, 200)
context.stroke()
context.setStrokeStyle("#ff0000")
context.setLineWidth(2)
context.moveTo(160, 100)
context.arc(100, 100, 60, 0, 2 * Math.PI, true)
context.moveTo(140, 100)
context.arc(100, 100, 40, 0, Math.PI, false)
context.moveTo(85, 80)
context.arc(80, 80, 5, 0, 2 * Math.PI, true)
context.moveTo(125, 80)
context.arc(120, 80, 5, 0, 2 * Math.PI, true)
context.stroke()
context.draw()
复制代码
Page({
data: {
animationData: {}
},
onShow: function(){
var animation = wx.createAnimation({
duration: 1000,
timingFunction: 'ease',
})
this.animation = animation
animation.scale(2,2).rotate(45).step()
this.setData({
animationData:animation.export()
})
}
})
复制代码
2.小程序的 Http Rquest 请求是不是用的浏览器 Fetch API?
知识点考察
wx.request
是不是遵循 fetch API 规范实现的呢?答案,显然不是。因为没有 Promise
WXML(WeiXin Markup Language)
Wxml编译器:Wcc 把 Wxml文件 转为 JS
执行方式:Wcc index.wxml
使用 Virtual DOM,进行局部更新
WXSS(WeiXin Style Sheets)
wxss编译器:wcsc 把wxss文件转化为 js
执行方式: wcsc index.wxss
亲测包含但不限于如下内容:
建议 Css3 的特性都可以做一下尝试。
rpx(responsive pixel): 可以根据屏幕宽度进行自适应。规定屏幕宽为 750rpx。公式:
const dsWidth = 750
export const screenHeightOfRpx = function () {
return 750 / env.screenWidth * env.screenHeight
}
export const rpxToPx = function (rpx) {
return env.screenWidth / 750 * rpx
}
export const pxToRpx = function (px) {
return 750 / env.screenWidth * px
}
复制代码
设备 | rpx换算px (屏幕宽度/750) | px换算rpx (750/屏幕宽度) |
---|---|---|
iPhone5 | 1rpx = 0.42px | 1px = 2.34rpx |
iPhone6 | 1rpx = 0.5px | 1px = 2rpx |
iPhone6 Plus | 1rpx = 0.552px | 1px = 1.81rpx |
可以了解一下 pr2rpx-loader 这个库。
使用 @import
语句可以导入外联样式表,@import
后跟需要导入的外联样式表的相对路径,用 ;
表示语句结束。
静态的样式统一写到 class 中。style 接收动态的样式,在运行时会进行解析,请尽量避免将静态的样式写进 style 中,以免影响渲染速度。
定义在 app.wxss 中的样式为全局样式,作用于每一个页面。在 page 的 wxss 文件中定义的样式为局部样式,只作用在对应的页面,并会覆盖 app.wxss 中相同的选择器。
截止20180810
小程序未来有计划支持字体。参考微信公开课。
小程序开发与平时 Web开发类似,也可以使用字体图标,但是 src:url()
无论本地还是远程地址都不行,base64 值则都是可以显示的。
将 ttf 文件转换成 base64。打开这个平台 transfonter.org/。点击 Add fonts 按钮,加载ttf格式的那个文件。将下边的 base64 encode 改为 on。点击 Convert 按钮进行转换,转换后点击 download 下载。
复制下载的压缩文件中的 stylesheet.css 的内容到 font.wxss ,并且将 icomoon 中的 style.css 除了 @font-face 所有的代码也复制到 font.wxss 并将i选择器换成 .iconfont,最后:
<text class="iconfont icon-home" style="font-size:50px;color:red">text>
复制代码
小程序提供了一系列组件用于开发业务功能,按照功能与HTML5的标签进行对比如下:
小程序的组件基于Web Component标准
使用Polymer框架实现Web Component
目前Native实现的组件有
cavnas
video
map
textarea
Native组件层在 WebView 层之上。这目前带来了一些问题:
cover-view
可以覆盖 cavnas video 等,但是也有一下弊端,比如在 cavnas 上覆盖 cover-view
,就会发现坐标系不统一处理麻烦截止20180810
包含但不限于:
小程序仍然使用 WebView 渲染,并非原生渲染。(部分原生)
服务端接口返回的头无法执行,比如:Set-Cookie。
依赖浏览器环境的 JS 库不能使用。
不能使用 npm,但是可以自搭构建工具或者使用 mpvue。(未来官方有计划支持)
不能使用 ES7,可以自己用babel+webpack自搭或者使用 mpvue。
不支持使用自己的字体(未来官方计划支持)。
可以用 base64 的方式来使用 iconfont。
小程序不能发朋友圈(可以通过保存图片到本地,发图片到朋友前。二维码可以使用B接口)。
获取二维码/小程序接口的限制。
小程序推送只能使用“服务通知” 而且需要用户主动触发提交 formId,formId 只有7天有效期。(现在的做法是在每个页面都放入form并且隐藏以此获取更多的 formId。后端使用原则为:优先使用有效期最短的)
小程序大小限制 2M,分包总计不超过 8M
转发(分享)小程序不能拿到成功结果,原来可以。链接(小游戏造的孽)
拿到相同的 unionId 必须绑在同一个开放平台下。开放平台绑定限制:
公众号关联小程序,链接
一个公众号关联的10个同主体小程序和3个非同主体小程序可以互相跳转
品牌搜索不支持金融、医疗
小程序授权需要用户主动点击
小程序不提供测试 access_token
安卓系统下,小程序授权获取用户信息之后,删除小程序再重新获取,并重新授权,得到旧签名,导致第一次授权失败
开发者工具上,授权获取用户信息之后,如果清缓存选择全部清除,则即使使用了wx.checkSession,并且在session_key有效期内,授权获取用户信息也会得到新的session_key
为了验证小程序对HTTP的支持适配情况,我找了两个服务器做测试,一个是网上搜索到支持HTTP2的服务器,一个是我本地起的一个HTTP2服务器。测试中所有请求方法均使用 wx.request
。
网上支持HTTP2的服务器:HTTPs://www.snel.com:443
在Chrome上查看该服务器为 HTTP2
在模拟器上请求该接口,请求头
的HTTP版本为HTTP1.1,模拟器不支持HTTP2
由于小程序线上环境需要在项目管理里配置请求域名,而这个域名不是我们需要的请求域名,没必要浪费一个域名位置,所以打开不验证域名,TSL 等选项请求该接口,通过抓包工具表现与模拟器相同
由上可以看出,在真机与模拟器都不支持 HTTP2,但是都是成功请求的,并且 响应头
里的 HTTP 版本都变成了HTTP1.1 版本,说明服务器对 HTTP1.1 做了兼容性适配。
本地新启一个 node 服务器,返回 JSON 为请求的 HTTP 版本
如果服务器只支持 HTTP2,在模拟器请求时发生了一个 ALPN
协议的错误。并且提醒使用适配 HTTP1
当把服务器的 allowHTTP1
,设置为 true
,并在请求时处理相关相关请求参数后,模拟器能正常访问接口,并打印出对应的 HTTP 请求版本
面试题:先授权获取用户信息再 login 会发生什么?
我们知道view部分是运行在webview上的,所以前端领域的大多数优化方式都有用。
我们知道view部分是运行在webview上的,所以前端领域的大多数优化方式都有用。
我们知道view部分是运行在webview上的,所以前端领域的大多数优化方式都有用。
代码包的大小是最直接影响小程序加载启动速度的因素。代码包越大不仅下载速度时间长,业务代码注入时间也会变长。所以最好的优化方式就是减少代码包的大小。
小程序加载的三个阶段的表示。
优化方式
首屏加载的体验优化建议
在构建小程序分包项目时,构建会输出一个或多个功能的分包,其中每个分包小程序必定含有一个主包,所谓的主包,即放置默认启动页面/TabBar 页面,以及一些所有分包都需用到公共资源/JS 脚本,而分包则是根据开发者的配置进行划分。
在小程序启动时,默认会下载主包并启动主包内页面,如果用户需要打开分包内某个页面,客户端会把对应分包下载下来,下载完成后再进行展示。
优点:
限制:
原生分包加载的配置 假设支持分包的小程序目录结构如下:
├── app.js
├── app.json
├── app.wxss
├── packageA
│ └── pages
│ ├── cat
│ └── dog
├── packageB
│ └── pages
│ ├── apple
│ └── banana
├── pages
│ ├── index
│ └── logs
└── utils
复制代码
开发者通过在 app.json subPackages 字段声明项目分包结构:
{
"pages":[
"pages/index",
"pages/logs"
],
"subPackages": [
{
"root": "packageA",
"pages": [
"pages/cat",
"pages/dog"
]
}, {
"root": "packageB",
"pages": [
"pages/apple",
"pages/banana"
]
}
]
}
复制代码
分包原则
引用原则
官方即将推出 分包预加载
独立分包
每次 setData 的调用都是一次进程间通信过程,通信开销与 setData 的数据量正相关。
setData 会引发视图层页面内容的更新,这一耗时操作一定时间中会阻塞用户交互。
setData 是小程序开发使用最频繁,也是最容易引发性能问题的。
避免不当使用 setData
避免不当使用onPageScroll
使用自定义组件
在需要频繁更新的场景下,自定义组件的更新只在组件内部进行,不受页面其他部分内容复杂性影响。
小程序的几个页面间,存在一些相同或是类似的区域,这时候可以把这些区域逻辑封装成一个自定义组件,代码就可以重用,或者对于比较独立逻辑,也可以把它封装成一个自定义组件,也就是微信去年发布的自定义组件,它让代码得到复用、减少代码量,更方便模块化,优化代码架构组织,也使得模块清晰,后期更好地维护,从而保证更好的性能。
但微信打算在原来的基础上推出的自定义组件 2.0,它将拥有更高级的性能:
目前小程序开发的痛点是:开源组件要手动复制到项目,后续更新组件也需要手动操作。不久的将来,小程序将支持npm包管理,有了这