钉钉小程序开发遇到的问题以及解决方案(一)

2,587 阅读8分钟

本人从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'就能解决钉钉小程序拍照上传图片时闪退的问题(钉钉自身问题)

3.钉钉小程序登录不了,报错callInternalAPISync getStartupParams error(部分手机)

上次做的某个钉钉小程序小部分人的手机钉钉小程序登录不了,登录的接口都没调用到,就报错callInternalAPISync getStartupParams error了,开始以为是这部分人的账号有问题,于是用有问题的账号登录别人无问题的手机,发现能登录;然后怀疑是这部分人的手机问题,于是切换了别人的账号,总之都试过了,发现不是账号和手机的问题,是固定某些人在某些手机上才有问题,于是做了个骚操作,清除了钉钉的缓存和所有内存,钉钉会退出重新登录,问题解决(钉钉自身问题) 今天先说这三个遇到的问题,有问题的可以在下面留言,后续问题会继续上新,谢谢!!