本人从2019年5月开始开发公司的钉钉小程序(企业内部应用),一切都是摸索中滚爬,钉钉小程序api文档不全也不详细,大部分是抄袭微信小程序,言归正传,开始钉钉小程序遇到的第一个问题!
1.钉钉小程序的store状态管理和跨页传值通讯
开发钉钉小程序之前本人一直是vue的开发者,所以开发钉钉小程序时想用vue里面的store,钉钉页面与页面直接的传值,页面与孙子组件的传值,用setData简直是绕晕我!小程序通过页面或组件各自的 setData 再加上各种父子、祖孙、姐弟、姑姑与堂兄等等组件间的通讯会把程序搞成一团浆糊,如果再加上跨页面之间的组件通讯,会让程序非常难维护和调试。虽然市面上出现了许多技术栈编译转小程序的技术,但是我觉没有戳中小程序的痛点。小程序不管从组件化、开发、调试、发布、灰度、回滚、上报、统计、监控和最近的云能力都非常完善,小程序的工程化简直就是前端的典范。而开发者工具也在持续更新,可以想象的未来,组件布局的话未必需要写代码了。而且据统计,开发小程序使用最多的技术栈是使用小程序本身的开发工具和语法,所以最大的痛点只剩下状态管理和跨页通讯。 钉钉小程序并没有vue的store,只能去看下微信小程序有没有类似的,毕竟微信小程序出来的久,Westore - 微信小程序解决方案,github地址附上https://github.com/Tencent/westore 但是钉钉小程序并不能直接使用,因为钉钉和微信的生命周期有所不同,下载微信的Westore后需要修改部分代码才能使用在钉钉里面,直接附上修改后能使用在钉钉的westore代码:
diff.js
const ARRAYTYPE = '[object Array]'
const OBJECTTYPE = '[object Object]'
const FUNCTIONTYPE = '[object Function]'
export default function diff(current, pre) {
const result = {}
syncKeys(current, pre)
_diff(current, pre, '', result)
return result
}
function syncKeys(current, pre) {
if (current === pre) return
const rootCurrentType = type(current)
const rootPreType = type(pre)
if (rootCurrentType == OBJECTTYPE && rootPreType == OBJECTTYPE) {
//if(Object.keys(current).length >= Object.keys(pre).length){
for (let key in pre) {
const currentValue = current[key]
if (currentValue === undefined) {
current[key] = null
} else {
syncKeys(currentValue, pre[key])
}
}
//}
} else if (rootCurrentType == ARRAYTYPE && rootPreType == ARRAYTYPE) {
if (current.length >= pre.length) {
pre.forEach((item, index) => {
syncKeys(current[index], item)
})
}
}
}
function _diff(current, pre, path, result) {
if (current === pre) return
const rootCurrentType = type(current)
const rootPreType = type(pre)
if (rootCurrentType == OBJECTTYPE) {
if (rootPreType != OBJECTTYPE || Object.keys(current).length < Object.keys(pre).length && path !== '') {
setResult(result, path, current)
} else {
for (let key in current) {
const currentValue = current[key]
const preValue = pre[key]
const currentType = type(currentValue)
const preType = type(preValue)
if (currentType != ARRAYTYPE && currentType != OBJECTTYPE) {
if (currentValue != pre[key]) {
setResult(result, (path == '' ? '' : path + ".") + key, currentValue)
}
} else if (currentType == ARRAYTYPE) {
if (preType != ARRAYTYPE) {
setResult(result, (path == '' ? '' : path + ".") + key, currentValue)
} else {
if (currentValue.length < preValue.length) {
setResult(result, (path == '' ? '' : path + ".") + key, currentValue)
} else {
currentValue.forEach((item, index) => {
_diff(item, preValue[index], (path == '' ? '' : path + ".") + key + '[' + index + ']', result)
})
}
}
} else if (currentType == OBJECTTYPE) {
if (preType != OBJECTTYPE || Object.keys(currentValue).length < Object.keys(preValue).length) {
setResult(result, (path == '' ? '' : path + ".") + key, currentValue)
} else {
for (let subKey in currentValue) {
_diff(currentValue[subKey], preValue[subKey], (path == '' ? '' : path + ".") + key + '.' + subKey, result)
}
}
}
}
}
} else if (rootCurrentType == ARRAYTYPE) {
if (rootPreType != ARRAYTYPE) {
setResult(result, path, current)
} else {
if (current.length < pre.length) {
setResult(result, path, current)
} else {
current.forEach((item, index) => {
_diff(item, pre[index], path + '[' + index + ']', result)
})
}
}
} else {
setResult(result, path, current)
}
}
function setResult(result, k, v) {
const t = type(v)
if (t != FUNCTIONTYPE) {
//if (t != OBJECTTYPE && t != ARRAYTYPE) {
result[k] = v
// } else {
// result[k] = JSON.parse(JSON.stringify(v))
// }
}
}
function type(obj) {
return Object.prototype.toString.call(obj)
}
create.js
import diff from './diff'
let originData = null
let globalStore = null
let fnMapping = {}
const ARRAYTYPE = '[object Array]'
const OBJECTTYPE = '[object Object]'
const FUNCTIONTYPE = '[object Function]'
export default function create(store, option) {
let updatePath = null
if (arguments.length === 2) {
if (!originData) {
originData = JSON.parse(JSON.stringify(store.data))
globalStore = store
store.instances = {}
store.update = update
store.push = push
store.pull = pull
store.add = add
store.remove = remove
store.originData = originData
store.env && initCloud(store.env)
extendStoreMethod(store)
}
getApp().globalData && (getApp().globalData.store = store)
//option.data = store.data
const onLoad = option.onLoad
walk(store.data)
// 解决函数属性初始化不能显示的问题,要求必须在data中声明使用
// 这段代码是同步store.data到option.data,只有经过walk方法后store.data中的函数才能变成属性,才能被小程序page方法渲染
if (option.data && Object.keys(option.data).length > 0) {
updatePath = getUpdatePath(option.data)
syncValues(store.data, option.data)
}
option.onLoad = function (e) {
this.store = store
this._updatePath = updatePath
rewriteUpdate(this)
store.instances[this.route] = []
store.instances[this.route].push(this)
onLoad && onLoad.call(this, e)
syncValues(store.data, this.data)
this.setData(this.data)
}
// 解决执行navigateBack或reLaunch时清除store.instances对应页面的实例
const onUnload = option.onUnload
option.onUnload = function () {
onUnload && onUnload.call(this)
store.instances[this.route] = []
}
Page(option)
} else {
const didMount = store.didMount
const pure = store.pure
const componentUpdatePath = getUpdatePath(store.data)
store.didMount = function () {
if (pure) {
this.store = { data: store.data || {} }
this.store.originData = store.data ? JSON.parse(JSON.stringify(store.data)) : {}
walk(store.data || {})
rewritePureUpdate(this)
} else {
this.page = getCurrentPages()[getCurrentPages().length - 1]
this.store = this.page.store
this._updatePath = componentUpdatePath
syncValues(this.store.data, store.data)
walk(store.data || {})
this.setData.call(this, this.store.data)
rewriteUpdate(this)
this.store.instances[this.page.route].push(this)
}
didMount && didMount.call(this)
}
Component(store)
}
}
function syncValues(from, to){
Object.keys(to).forEach(key=>{
if(from.hasOwnProperty(key)){
to[key] = from[key]
}
})
}
function getUpdatePath(data) {
const result = {}
dataToPath(data, result)
return result
}
function dataToPath(data, result) {
Object.keys(data).forEach(key => {
result[key] = true
const type = Object.prototype.toString.call(data[key])
if (type === OBJECTTYPE) {
_objToPath(data[key], key, result)
} else if (type === ARRAYTYPE) {
_arrayToPath(data[key], key, result)
}
})
}
function _objToPath(data, path, result) {
Object.keys(data).forEach(key => {
result[path + '.' + key] = true
delete result[path]
const type = Object.prototype.toString.call(data[key])
if (type === OBJECTTYPE) {
_objToPath(data[key], path + '.' + key, result)
} else if (type === ARRAYTYPE) {
_arrayToPath(data[key], path + '.' + key, result)
}
})
}
function _arrayToPath(data, path, result) {
data.forEach((item, index) => {
result[path + '[' + index + ']'] = true
delete result[path]
const type = Object.prototype.toString.call(item)
if (type === OBJECTTYPE) {
_objToPath(item, path + '[' + index + ']', result)
} else if (type === ARRAYTYPE) {
_arrayToPath(item, path + '[' + index + ']', result)
}
})
}
function rewritePureUpdate(ctx) {
ctx.update = function (patch) {
const store = this.store
const that = this
return new Promise(resolve => {
//defineFnProp(store.data)
if (patch) {
for (let key in patch) {
updateByPath(store.data, key, patch[key])
}
}
let diffResult = diff(store.data, store.originData)
let array = []
if (Object.keys(diffResult).length > 0) {
array.push( new Promise( cb => that.setData(diffResult, cb) ) )
store.onChange && store.onChange(diffResult)
for (let key in diffResult) {
updateByPath(store.originData, key, typeof diffResult[key] === 'object' ? JSON.parse(JSON.stringify(diffResult[key])) : diffResult[key])
}
}
Promise.all(array).then( e => resolve(diffResult) )
})
}
}
function initCloud(env) {
wx.cloud.init()
globalStore.db = wx.cloud.database({
env: env
})
}
function push(patch) {
return new Promise(function (resolve, reject) {
_push(update(patch), resolve, reject)
})
}
function _push(diffResult, resolve) {
const objs = diffToPushObj(diffResult)
Object.keys(objs).forEach((path) => {
const arr = path.split('-')
const id = globalStore.data[arr[0]][parseInt(arr[1])]._id
const obj = objs[path]
if (globalStore.methods && globalStore.methods[arr[0]]) {
Object.keys(globalStore.methods[arr[0]]).forEach(key => {
if (obj.hasOwnProperty(key)) {
delete obj[key]
}
})
}
globalStore.db.collection(arr[0]).doc(id).update({
data: obj
}).then((res) => {
resolve(res)
})
})
}
function update(patch) {
return new Promise(resolve => {
//defineFnProp(globalStore.data)
if (patch) {
for (let key in patch) {
updateByPath(globalStore.data, key, patch[key])
}
}
let diffResult = diff(globalStore.data, originData)
if (Object.keys(diffResult)[0] == '') {
diffResult = diffResult['']
}
const updateAll = matchGlobalData(diffResult)
let array = []
if (Object.keys(diffResult).length > 0) {
for (let key in globalStore.instances) {
globalStore.instances[key].forEach(ins => {
if(updateAll || globalStore.updateAll || ins._updatePath){
// 获取需要更新的字段
const needUpdatePathList = getNeedUpdatePathList(diffResult, ins._updatePath)
if (needUpdatePathList.length) {
const _diffResult = {}
for (let _path in diffResult) {
if (needUpdatePathList.includes(_path)) {
_diffResult[_path] = typeof diffResult[_path] === 'object' ? JSON.parse(JSON.stringify(diffResult[_path])) : diffResult[_path]
}
}
array.push( new Promise(cb => {
ins.setData.call(ins, _diffResult, cb)
}) )
}
}
})
}
globalStore.onChange && globalStore.onChange(diffResult)
for (let key in diffResult) {
updateByPath(originData, key, typeof diffResult[key] === 'object' ? JSON.parse(JSON.stringify(diffResult[key])) : diffResult[key])
}
}
Promise.all(array).then(e=>{
resolve(diffResult)
})
})
}
function matchGlobalData(diffResult) {
if(!globalStore.globalData) return false
for (let keyA in diffResult) {
if (globalStore.globalData.indexOf(keyA) > -1) {
return true
}
for (let i = 0, len = globalStore.globalData.length; i < len; i++) {
if (includePath(keyA, globalStore.globalData[i])) {
return true
}
}
}
return false
}
function getNeedUpdatePathList(diffResult, updatePath){
const paths = []
for(let keyA in diffResult){
if(updatePath[keyA]){
paths.push(keyA)
}
for(let keyB in updatePath){
if(includePath(keyA, keyB)){
paths.push(keyA)
}
}
}
return paths
}
function includePath(pathA, pathB){
if(pathA.indexOf(pathB)===0){
const next = pathA.substr(pathB.length, 1)
if(next === '['||next === '.'){
return true
}
}
return false
}
function rewriteUpdate(ctx) {
ctx.update = update
}
function updateByPath(origin, path, value) {
const arr = path.replace(/]/g, '').replace(/\[/g, '.').split('.')
let current = origin
for (let i = 0, len = arr.length; i < len; i++) {
if (i === len - 1) {
current[arr[i]] = value
} else {
current = current[arr[i]]
}
}
}
function pull(cn, where) {
return new Promise(function (resolve) {
globalStore.db.collection(cn).where(where || {}).get().then(res => {
extend(res, cn)
resolve(res)
})
})
}
function extend(res, cn) {
res.data.forEach(item => {
const mds = globalStore.methods[cn]
mds && Object.keys(mds).forEach(key => {
Object.defineProperty(item, key, {
enumerable: true,
get: () => {
return mds[key].call(item)
},
set: () => {
//方法不能改写
}
})
})
})
}
function add(cn, data) {
return globalStore.db.collection(cn).add({ data })
}
function remove(cn, id) {
return globalStore.db.collection(cn).doc(id).remove()
}
function diffToPushObj(diffResult) {
const result = {}
Object.keys(diffResult).forEach(key => {
diffItemToObj(key, diffResult[key], result)
})
return result
}
function diffItemToObj(path, value, result) {
const arr = path.replace(/]/g, '').replace(/\[/g, '.').split('.')
const obj = {}
let current = null
const len = arr.length
for (let i = 2; i < len; i++) {
if (len === 3) {
obj[arr[i]] = value
} else {
if (i === len - 1) {
current[arr[i]] = value
} else {
const pre = current
current = {}
if (i === 2) {
obj[arr[i]] = current
} else {
pre[arr[i]] = current
}
}
}
}
const key = arr[0] + '-' + arr[1]
result[key] = Object.assign(result[key] || {}, obj)
}
function extendStoreMethod() {
globalStore.method = function (path, fn) {
fnMapping[path] = fn
let ok = getObjByPath(path)
Object.defineProperty(ok.obj, ok.key, {
enumerable: true,
get: () => {
return fnMapping[path].call(globalStore.data)
},
set: () => {
console.warn('Please using store.method to set method prop of data!')
}
})
}
}
function getObjByPath(path) {
const arr = path.replace(/]/g, '').replace(/\[/g, '.').split('.')
const len = arr.length
if (len > 1) {
let current = globalStore.data[arr[0]]
for (let i = 1; i < len - 1; i++) {
current = current[arr[i]]
}
return { obj: current, key: arr[len - 1] }
} else {
return { obj: globalStore.data, key: arr[0] }
}
}
function walk(data) {
Object.keys(data).forEach(key => {
const obj = data[key]
const tp = type(obj)
if (tp == FUNCTIONTYPE) {
setProp(key, obj)
} else if (tp == OBJECTTYPE) {
Object.keys(obj).forEach(subKey => {
_walk(obj[subKey], key + '.' + subKey)
})
} else if (tp == ARRAYTYPE) {
obj.forEach((item, index) => {
_walk(item, key + '[' + index + ']')
})
}
})
}
function _walk(obj, path) {
const tp = type(obj)
if (tp == FUNCTIONTYPE) {
setProp(path, obj)
} else if (tp == OBJECTTYPE) {
Object.keys(obj).forEach(subKey => {
_walk(obj[subKey], path + '.' + subKey)
})
} else if (tp == ARRAYTYPE) {
obj.forEach((item, index) => {
_walk(item, path + '[' + index + ']')
})
}
}
function setProp(path, fn) {
const ok = getObjByPath(path)
fnMapping[path] = fn
Object.defineProperty(ok.obj, ok.key, {
enumerable: true,
get: () => {
return fnMapping[path].call(globalStore.data)
},
set: () => {
console.warn('Please using store.method to set method prop of data!')
}
})
}
function type(obj) {
return Object.prototype.toString.call(obj)
}
store.js
export default {
data: {
cooperativeList: []
},
}
使用方法:
页面.js中使用: 给状态管理里面的store赋值: let cooperativeList = [];
rows.forEach( (item, index) => {
cooperativeList.push({});
for(var proper in item){
cooperativeList[index][proper] = item[proper].value;
}
})
this.store.data.cooperativeList = cooperativeList;
this.update();
this.store.data.cooperativeList = cooperativeList;
this.update(); 即可 在其他页面中不用重复调用接口就可以使用:
2.钉钉小程序拍照上传图片时闪退(部分手机)
上次做的钉钉小程序有上传图片的功能,部分手机(同事的部分苹果手机)拍照或者直接上传图片后小程序退出。微信也有类似bug,也百度下解决方法,附上微信社区小程序拍照上传图片时闪退的链接: developers.weixin.qq.com/community/d… 但是微信社区并没有解决这个问题。只能自己一个个摸索,dd.chooseImage方法一句一句的尝试,发现只有注释sizeType: 'compressed'就能解决钉钉小程序拍照上传图片时闪退的问题(钉钉自身问题)