- 原生小程序全局状态管理方案
原生小程序状态管理: mpsm 具备vue+react开发体验 整合了【vue的属性监听watch、计算属性computed】和【 dvajs 的全局状态管理模式】,解决跨页通信问题, 提出【组件圈子】概念,摆脱各种父子、兄弟、姐妹、街坊邻居、远房表兄弟等组件间的通信浆糊困扰。 数据流 不管是页面间,还是组件间,嵌套组件内部,都可以通过简单的dispach来管理全局状态或圈子状态(局部)。 [图片] [图片] 使用介绍 完全兼容原生代码,已有的业务逻辑代码,即便不适配也可使用此库,不影响已有业务逻辑。 原生: [代码]Page()[代码] 变为 [代码]page()()[代码] 原生: [代码]Component()[代码] 变为 [代码]component()()[代码] 初始化 [代码]//app.js import {page} from './mpsm/index' import models from './models/index' page.init(models, {}, {}) [代码] 第二、三参数分别为页面和组件的公共options,会在每个页面或组件生效,对于同名的对象和函数,会进行合并处理。 页面或组件注册 小:即只需将Page、Component首字母小写。 尾巴:即尾部多调用一次: [代码]page({ //component // ... })() [代码] [代码]// 数据更新有两种方式 // 第一种,直接修改this.data, 需要更新时调用this.update() this.data.a = 1 this.update() // 第二种 this.$setData this. $setData({ list: list }) //第一种更新方式,根据需要自己确定数据更新的时机,调用this.update() //第二种更新方式,会进行批处理,待函数执行结束后统一批量更新,异步操作立即更新,类似react中的setState [代码] 方法 方法 说明 备注 page 用于注册页面 page()() component 用于注册组件 component()() dispatch 状态分发 参数object, {type, action, lazy}, 默认lazy: true, 懒更新,表示只更新当前展示页面的状态,其它页面待onShow触发后再更新 getComponentOps 获取公共组件配置选项 不包含函数,只是简单的JSON格式数据 getOps 获取公共页面配置选项 不包含函数,只是简单的JSON格式数据 subscribe 订阅单一数据源 参数: ‘userInfo/setup’ unsubscribe 取消订阅单一数据源 参数: ‘userInfo/setup’ 单一数据源的订阅取消 方法: unsubscribe, subscribe 取消订阅不使用dva的[代码]app.unmodel()[代码],同时,subscription 必须返回 unlisten 方法,用于取消数据订阅。 [代码]subscribe('userInfo/setup'); unsubscribe('userInfo/setup'); [代码] 页面注册、组件注册示例 [代码]import {dispatch, page, component} from '../../mpsm/index' page({ // 或者 component watch: { // 属性监听 isLogin(newState, oldState) { } }, computed: { // 计算属性 countComputed(data) { return data.count * 2 } }, data: { count: 2 }, onLoad() {}, login() { dispatch({ type: 'userInfo/save', payload: { isLogin: true } }) }, changeGroupState() { this.dispatch({ type: 'group/index-a-1', payload: { nameA: 'name' } }) }, })(({userInfo}) => {//订阅全局状态 return { isLogin: userInfo.isLogin } }, (groups) => {//订阅圈子状态,page为全局圈子,component为组件所在圈子 return { nameA: groups.nameA && groups.nameA.name || '--' } }) [代码] tips: dispatch用于分发全局状态,风格与dva保持一致, 不过多了一个lazy字段, 默认true(懒更新,只更新当前页面,其他页面onShow时再更新),false的话,就是全量更新; Page和Component实例内置this.dispatch方法,用于分发局部状态。 3、组件可监听page的生命周期函数,无需做版本兼容,只需将想要监听的函数名与page内一直即可,即 [代码]// 原生的pageLifetimes可监听的周期太少,且版本要求高,监听了onShow,就不需要监听show了,避免执行两次 component({ pageLifetimes: { onShow: function () { }, onHide: function () { }, onPageScroll: function () { }, }, })() [代码] 目前可监听的生命周期 [代码]['onShow', 'onHide', 'onResize', 'onPageScroll', 'onTabItemTap', 'onPullDownRefresh', 'onReachBottom'][代码] 有了这个功能,就可以编写很多自控组件了,比如吸顶效果的导航栏等。 models 全局状态 参考 dvajs [代码]export default { namespace: '', state: { }, subscriptions: { }, effects: { }, reducers: { } } [代码] model 方法 详情参阅 dva select、put、history tips: put只用于简单的model内部的action分发,未封装put.resolve等方法,可使用async/await实现同等效果; 依据小程序特性,history回调中两个参数分别为当前页面路由信息和上一页路由信息,为一个对象,格式如。 [代码] export default { subscriptions: { setup({dispatch, history, select}) { const callback = (current, prev) => { dispatch({ type: 'save', payload: { prev, // {route: 'pages/index/index',options: {id: 123}} current } }) } history.listen(callback) return () => history.unlisten(callback) } } } [代码] 组件圈子 传统的组件数据流 对于嵌套组件间的数据通信,往往存在所谓的父子组件关系,内层组件想要向外层组件或其它分支上的组件传递数据,往往通过外层的组件通过监听函数来接收并派发,层层传递,这种 这种层层转接的模式,繁琐且需要专门的函数去维护,不利于组件的拓展和移植。 [图片] 小程序的官方组件数据流 而对于小程序的原生组件系统来说,behaviors, relations, definitionFilter, triggerEvent, getRelationNodes等编写组件需要使用的属性或方法, 真的是辣眼睛!小程序官方组件系统基本丧失了作为组件的意义, 虽然它还在努力地更新升级, 时不时文档中出现[代码]"不推荐使用这个字段,而是使用另一个字段代替,它更加强大且性能更好"[代码]的字眼, 大家不要信!!! [图片] [图片] 什么叫组件?相对独立,具有明确约定的接口,依赖语境,可自由组合,可拓展,可独立部署,亦可复合使用!!!你说你除了相对独立,你说你还有啥,你说你还有个啥!!! [图片] 组件圈子数据流 对于一个组件来说,在最初编写时,不应该去关心自己会被挂载到哪,自己只需要为圈子提供数据, 甚至是数据键名也不用关心,放到圈子里,谁爱用谁用,无需回调函数来协助传递数据, 这才是作为组件可移植可复用的意义,在一个圈子里,不存在父子兄弟的概念,也没有哪个组件生来是给人当儿子的,组件间的往来只有最纯粹的数据通信。 如下图中的组件C与组件F之间的数据通信,一个提供数据,另一个直接去使用就行了,不管层级多少,直接通信,没有多余的步骤。 [图片] [代码] <!--组件a in page--> <a group-name="index-a-1" group-keys="{{ {name: 'nameA1'} }" group-data="{{ {a: 1} }}"></a> [代码] 只需给组件赋值一个[代码]group-name[代码]属性,便给组件分配了一个圈子, 组件内[代码]this.dipatch({})[代码]分发地[代码]payload[代码]便是该组件给圈子贡献的数据, 也是该组件对外约定的数据值。 [代码]group-keys[代码]是用于避免字段名称冲突的,示例中可将a贡献地数据键值更改为[代码]nameA1[代码], 而值不变,所以圈子中地组件很纯粹,只向所在圈子提供必要的数据值, 至于你将组件放置何处,数据给谁用,名称怎么修改,都与我无关。 [代码]group-data[代码]是组件所依赖的外部值,是一个内置属性,可在组件的[代码]watch[代码]配置中进行监听。 页面、组件实例对象 注意: 对配置中的函数,以及setData进行了简单封装,在页面注册或组件注册options的函数中, setData的数据并不会马上更新,而是合并收集,待当前函数执行完毕后, 先模拟计算出computed,与收集的结果合并,再diff,最后setData一次。 与react更新机制类似,对于函数中出现的异步操作,不会进行数据收集,而是直接模拟计算值,diff,接着setData 页面 对圈子状态的管理拥有最高权限 属性 属性 说明 备注 $groups 获取当前页面内的所有圈子状态值 object, 只读 [代码] this.$groups['index-a-1'] [代码] 方法 dispatch 用于强制更新某个圈子中的状态 [代码] this.dispatch({ type: 'data/index-a-1', payload: { nameA1: 'name' } }) [代码] 参数 说明 备注 type 格式: 更新类型/圈子名称,更新类型:data 或 group,data表示更新圈子数据的同时,也将payload中的数据更新至this.data, 即this.setData(payload) string payload 需要更新的状态值 object 组件 只更新所在圈子状态 属性 属性 说明 备注 $groups 获取所在页面内的所有圈子状态值 object, 只读 $group 获取所在圈子状态值 object, 只读 [代码] this.$group['nameA1'] [代码] 方法 dispatch 用于更新所在圈子中的状态 [代码] this.dispatch({ type: 'data', // group payload: { nameA1: 'name' } }) [代码] 参数 说明 备注 type 格式: 更新类型,更新类型:data 或 group,data表示更新圈子数据的同时,也将payload中的数据更新至this.data, 即this.setData(payload) string payload 需要更新的状态值 object tips 更新类型 之所以会有data更新类型,是因为组件提供的数据名称,有可能会被改写,所以组件不要去监听自己的数据,那是给外人用的。 有时候组件向圈子贡献了数据,但自身并不需要这些数据更新到data,所以会有group更新类型 状态更新机制 [图片] 定制diff 腾讯官方的westore库,为小程序定制了一个diff,本想用在自己库里, 但使用中发现diff结果不太对,他会将删除的属性值重置为null,我并不需要为null, 当去遍历对象的属性值,就会多出一个为null的属性,莫名其妙,删了就是删了,不必再保留, 并且其内部实现,在进行diff前先进行一次同步下key键,这个完全没必要,浪费性能。 westore的diff应该是用在全局状态新旧状态上,其结果违反setData的数据拼接规则, 也不符合原生数据更新的习惯, 所以我得自己写一个diff,先列出基本情况: 1、不改变原生setData的使用习惯; 2、明确diff使用的场景,针对具体场景定制:属性更新时需要diff,setData时需要diff; 3、diff返回结果的格式,setData会用到,需要满足 [代码]a.b[0].a[代码]这样的格式,另外如果属性变化,还需要根据键值调用watch; 4、参与对比的两个参数都有一个共同特点,就是都是拿局部变化的数据,去对比全量旧数据,属性的话,两者的键值对更是对等的,所以无需同步键值; 定制的diff: [代码] diff({ a: 1, b: 2, c: "str", d: { e: [2, { a: 4 }, 5] }, f: true, h: [1], g: { a: [1, 2], j: 111 } }, { a: [], b: "aa", c: 3, d: { e: [3, { a: 3 }] }, f: false, h: [1, 2], g: { a: [1, 1, 1], i: "delete" }, k: 'del' }) [代码] diff结果 [代码] const diffResult = { result: { a: 1, b: 2, c: "str", 'd.e[0]': 2, 'd.e[1].a': 4, 'd.e[2]': 5, f: true, g: { a: [1,2], j: 111 }, h: [1] }, rootKeys: { a: true, b: true, c: true, d: true, g: true, h: true, } } [代码] 记录rootKeys是因为属性监听不需要 'a.b’这样的格式,在diff中记录下来,便少了一次遍历筛选拆分。
2019-12-16 - 怎么解决顶部tab遮盖下拉刷新动画?
顶部tab选项卡用的vant的ui,原先下拉的时候会带动tab一起滚动[图片] 随后设置了tab的样式: position:fixed; top:0; left:0; right:0; width:100%; z-index:99999; 列表页面样式: padding-top:45px; 设置样式后,下拉时候tab不会随页面滚动,但是触发wx.startPullDownRefresh后,不会出现下拉刷新样式 [图片] 手动下拉页面,发现刷新动画在页面上方 尝试改动页面位置:page设置padding-top,margin-top都没效果 [图片]
2019-08-01 - 微信小程序如何去掉checkbox前面的方框?
微信小程序如何去掉checkbox前面的方框,我只想要这个复习框的功能,但是我不想要这个复选框前面的方框,应该怎么做呀 checkbox-group bindchange="checkboxChange"> <label class="checkbox" wx:for="{{items}}" wx:key="item"> <checkbox value="{{item.name}}" checked="{{item.checked}}">{{item.value}}</checkbox> </label> </checkbox-group>
2019-09-18 - 通过授权登录介绍小程序原生开发如何引入async/await、状态管理等工具
登陆和授权是小程序开发会遇到的第一个问题,这里把相关业务逻辑、工具代码抽取出来,展示我们如何引入的一些包使得原生微信小程序内也可以使用 async/await、fetch、localStorage、状态管理、GraphQL 等等特性,希望对大家有所帮助。 前端 目录结构 [代码]├── app.js ├── app.json ├── app.wxss ├── common │ └── api │ └── index.js ├── config.js ├── pages │ └── index │ ├── api │ │ └── index.js │ ├── img │ │ ├── btn.png │ │ └── bg.jpg │ ├── index.js │ ├── index.json │ ├── index.wxml │ └── index.wxss ├── project.config.json ├── store │ ├── action.js │ └── index.js ├── utils │ └── index.js └── vendor ├── event-emitter.js ├── fetch.js ├── fetchql.js ├── http.js ├── promisify.js ├── regenerator.js ├── storage.js └── store.js [代码] 业务代码 app.js [代码]import store from './store/index' const { loginInfo } = store.state App({ store, onLaunch() { // 打开小程序即登陆,无需用户授权可获得 openID if(!loginInfo) store.dispatch('login') }, }) [代码] store/index.js [代码]import Store from '../vendor/store' import localStorage from '../vendor/storage' import actions from './action' const loginInfo = localStorage.getItem('loginInfo') export default new Store({ state: { // 在全局状态中维护登陆信息 loginInfo, }, actions, }) [代码] store/action.js [代码]import regeneratorRuntime from '../vendor/regenerator'; import wx from '../vendor/promisify'; import localStorage from '../vendor/storage' import api from '../common/api/index'; export default { async login({ state }, payload) { const { code } = await wx.loginAsync(); const { authSetting } = await wx.getSettingAsync() // 如果用户曾授权,直接可以拿到 encryptedData const { encryptedData, iv } = authSetting['scope.userInfo'] ? await wx.getUserInfoAsync({ withCredentials: true }) : {}; // 如果用户未曾授权,也可以拿到 openID const { token, userInfo } = await api.login({ code, encryptedData, iv }); // 为接口统一配置 Token getApp().gql.requestObject.headers['Authorization'] = `Bearer ${token}`; // 本地缓存登陆信息 localStorage.setItem('loginInfo', { token, userInfo } ) return { loginInfo: { token, userInfo } } } } [代码] common/api/index.js [代码]import regeneratorRuntime from '../../vendor/regenerator.js' export default { /** * 登录接口 * 如果只有 code,只返回 token,如果有 encryptedData, iv,同时返回用户的昵称和头像 * @param {*} param0 */ async login({ code, encryptedData, iv }) { const query = `query login($code: String!, $encryptedData: String, $iv: String){ login(code:$code, encryptedData:$encryptedData, iv:$iv, appid:$appid){ token userInfo { nickName avatarUrl } } }` const { login: { token, userInfo } } = await getApp().query({ query, variables: { code, encryptedData, iv } }) return { token, userInfo } }, } [代码] pages/index/index.js [代码]import regeneratorRuntime from '../../vendor/regenerator.js' const app = getApp() Page({ data: {}, onLoad(options) { // 将用户登录信息注入到当前页面的 data 中,并且当数据在全局范围内被更新时,都会自动刷新本页面 app.store.mapState(['loginInfo'], this) }, async login({ detail: { errMsg } }) { if (errMsg === 'getUserInfo:fail auth deny') return app.store.dispatch('login') // 继续处理业务 }, }) [代码] pages/index/index.wxml [代码]<view class="container"> <form report-submit="true" bindsubmit="saveFormId"> <button form-type="submit" open-type="getUserInfo" bindgetuserinfo="login">登录</button> </form> </view> [代码] 工具代码 事件处理 vendor/event-emitter.js [代码]const id_Identifier = '__id__'; function randomId() { return Math.random().toString(36).substr(2, 16); } function findIndexById(id) { return this.findIndex(item => item[id_Identifier] === id); } export default class EventEmitter { constructor() { this.events = {} } /** * listen on a event * @param event * @param listener */ on(event, listener) { let { events } = this; let container = events[event] || []; let id = randomId(); let index; listener[id_Identifier] = id; container.push(listener); return () => { index = findIndexById.call(container, id); index >= 0 && container.splice(index, 1); } }; /** * remove all listen of an event * @param event */ off (event) { this.events[event] = []; }; /** * clear all event listen */ clear () { this.events = {}; }; /** * listen on a event once, if it been trigger, it will cancel the listner * @param event * @param listener */ once (event, listener) { let { events } = this; let container = events[event] || []; let id = randomId(); let index; let callback = () => { index = findIndexById.call(container, id); index >= 0 && container.splice(index, 1); listener.apply(this, arguments); }; callback[id_Identifier] = id; container.push(callback); }; /** * emit event */ emit () { const { events } = this; const argv = [].slice.call(arguments); const event = argv.shift(); ((events['*'] || []).concat(events[event] || [])).map(listener => self.emitting(event, argv, listener)); }; /** * define emitting * @param event * @param dataArray * @param listener */ emitting (event, dataArray, listener) { listener.apply(this, dataArray); }; } [代码] 封装 wx.request() 接口 vendor/http.js [代码]import EventEmitter from './event-emitter.js'; const DEFAULT_CONFIG = { maxConcurrent: 10, timeout: 0, header: {}, dataType: 'json' }; class Http extends EventEmitter { constructor(config = DEFAULT_CONFIG) { super(); this.config = config; this.ctx = wx; this.queue = []; this.runningTask = 0; this.maxConcurrent = DEFAULT_CONFIG.maxConcurrent; this.maxConcurrent = config.maxConcurrent; this.requestInterceptor = () => true; this.responseInterceptor = () => true; } create(config = DEFAULT_CONFIG) { return new Http(config); } next() { const queue = this.queue; if (!queue.length || this.runningTask >= this.maxConcurrent) return; const entity = queue.shift(); const config = entity.config; const { requestInterceptor, responseInterceptor } = this; if (requestInterceptor.call(this, config) !== true) { let response = { data: null, errMsg: `Request Interceptor: Request can\'t pass the Interceptor`, statusCode: 0, header: {} }; entity.reject(response); return; } this.emit('request', config); this.runningTask = this.runningTask + 1; let timer = null; let aborted = false; let finished = false; const callBack = { success: (res) => { if (aborted) return; finished = true; timer && clearTimeout(timer); entity.response = res; this.emit('success', config, res); responseInterceptor.call(this, config, res) !== true ? entity.reject(res) : entity.resolve(res); }, fail: (res) => { if (aborted) return; finished = true; timer && clearTimeout(timer); entity.response = res; this.emit('fail', config, res); responseInterceptor.call(this, config, res) !== true ? entity.reject(res) : entity.resolve(res); }, complete: () => { if (aborted) return; this.emit('complete', config, entity.response); this.next(); this.runningTask = this.runningTask - 1; } }; const requestConfig = Object.assign(config, callBack); const task = this.ctx.request(requestConfig); if (this.config.timeout > 0) { timer = setTimeout(() => { if (!finished) { aborted = true; task && task.abort(); this.next(); } }, this.config.timeout); } } request(method, url, data, header, dataType = 'json') { const config = { method, url, data, header: { ...header, ...this.config.header }, dataType: dataType || this.config.dataType }; return new Promise((resolve, reject) => { const entity = { config, resolve, reject, response: null }; this.queue.push(entity); this.next(); }); } head(url, data, header, dataType) { return this.request('HEAD', url, data, header, dataType); } options(url, data, header, dataType) { return this.request('OPTIONS', url, data, header, dataType); } get(url, data, header, dataType) { return this.request('GET', url, data, header, dataType); } post(url, data, header, dataType) { return this.request('POST', url, data, header, dataType); } put(url, data, header, dataType) { return this.request('PUT', url, data, header, dataType); } ['delete'](url, data, header, dataType) { return this.request('DELETE', url, data, header, dataType); } trace(url, data, header, dataType) { return this.request('TRACE', url, data, header, dataType); } connect(url, data, header, dataType) { return this.request('CONNECT', url, data, header, dataType); } setRequestInterceptor(interceptor) { this.requestInterceptor = interceptor; return this; } setResponseInterceptor(interceptor) { this.responseInterceptor = interceptor; return this; } clean() { this.queue = []; } } export default new Http(); [代码] 兼容 fetch 标准 vendor/fetch.js [代码]import http from './http'; const httpClient = http.create({ maxConcurrent: 10, timeout: 0, header: {}, dataType: 'json' }); function generateResponse(res) { let header = res.header || {}; let config = res.config || {}; return { ok: ((res.statusCode / 200) | 0) === 1, // 200-299 status: res.statusCode, statusText: res.errMsg, url: config.url, clone: () => generateResponse(res), text: () => Promise.resolve( typeof res.data === 'string' ? res.data : JSON.stringify(res.data) ), json: () => { if (typeof res.data === 'object') return Promise.resolve(res.data); let json = {}; try { json = JSON.parse(res.data); } catch (err) { console.error(err); } return json; }, blob: () => Promise.resolve(new Blob([res.data])), headers: { keys: () => Object.keys(header), entries: () => { let all = []; for (let key in header) { if (header.hasOwnProperty(key)) { all.push([key, header[key]]); } } return all; }, get: n => header[n.toLowerCase()], has: n => n.toLowerCase() in header } }; } export default (typeof fetch === 'function' ? fetch.bind() : function(url, options) { options = options || {}; return httpClient .request(options.method || 'get', url, options.body, options.headers) .then(res => Promise.resolve(generateResponse(res))) .catch(res => Promise.reject(generateResponse(res))); }); [代码] GraphQL客户端 vendor/fetchql.js [代码]import fetch from './fetch'; // https://github.com/gucheen/fetchql /** Class to realize fetch interceptors */ class FetchInterceptor { constructor() { this.interceptors = []; /* global fetch */ this.fetch = (...args) => this.interceptorWrapper(fetch, ...args); } /** * add new interceptors * @param {(Object|Object[])} interceptors */ addInterceptors(interceptors) { const removeIndex = []; if (Array.isArray(interceptors)) { interceptors.map((interceptor) => { removeIndex.push(this.interceptors.length); return this.interceptors.push(interceptor); }); } else if (interceptors instanceof Object) { removeIndex.push(this.interceptors.length); this.interceptors.push(interceptors); } this.updateInterceptors(); return () => this.removeInterceptors(removeIndex); } /** * remove interceptors by indexes * @param {number[]} indexes */ removeInterceptors(indexes) { if (Array.isArray(indexes)) { indexes.map(index => this.interceptors.splice(index, 1)); this.updateInterceptors(); } } /** * @private */ updateInterceptors() { this.reversedInterceptors = this.interceptors .reduce((array, interceptor) => [interceptor].concat(array), []); } /** * remove all interceptors */ clearInterceptors() { this.interceptors = []; this.updateInterceptors(); } /** * @private */ interceptorWrapper(fetch, ...args) { let promise = Promise.resolve(args); this.reversedInterceptors.forEach(({ request, requestError }) => { if (request || requestError) { promise = promise.then(() => request(...args), requestError); } }); promise = promise.then(() => fetch(...args)); this.reversedInterceptors.forEach(({ response, responseError }) => { if (response || responseError) { promise = promise.then(response, responseError); } }); return promise; } } /** * GraphQL client with fetch api. * @extends FetchInterceptor */ class FetchQL extends FetchInterceptor { /** * Create a FetchQL instance. * @param {Object} options * @param {String} options.url - the server address of GraphQL * @param {(Object|Object[])=} options.interceptors * @param {{}=} options.headers - request headers * @param {FetchQL~requestQueueChanged=} options.onStart - callback function of a new request queue * @param {FetchQL~requestQueueChanged=} options.onEnd - callback function of request queue finished * @param {Boolean=} options.omitEmptyVariables - remove null props(null or '') from the variables * @param {Object=} options.requestOptions - addition options to fetch request(refer to fetch api) */ constructor({ url, interceptors, headers, onStart, onEnd, omitEmptyVariables = false, requestOptions = {}, }) { super(); this.requestObject = Object.assign( {}, { method: 'POST', headers: Object.assign({}, { Accept: 'application/json', 'Content-Type': 'application/json', }, headers), credentials: 'same-origin', }, requestOptions, ); this.url = url; this.omitEmptyVariables = omitEmptyVariables; // marker for request queue this.requestQueueLength = 0; // using for caching enums' type this.EnumMap = {}; this.callbacks = { onStart, onEnd, }; this.addInterceptors(interceptors); } /** * operate a query * @param {Object} options * @param {String} options.operationName * @param {String} options.query * @param {Object=} options.variables * @param {Object=} options.opts - addition options(will not be passed to server) * @param {Boolean=} options.opts.omitEmptyVariables - remove null props(null or '') from the variables * @param {Object=} options.requestOptions - addition options to fetch request(refer to fetch api) * @returns {Promise} * @memberOf FetchQL */ query({ operationName, query, variables, opts = {}, requestOptions = {}, }) { const options = Object.assign({}, this.requestObject, requestOptions); let vars; if (this.omitEmptyVariables || opts.omitEmptyVariables) { vars = this.doOmitEmptyVariables(variables); } else { vars = variables; } const body = { operationName, query, variables: vars, }; options.body = JSON.stringify(body); this.onStart(); return this.fetch(this.url, options) .then((res) => { if (res.ok) { return res.json(); } // return an custom error stack if request error return { errors: [{ message: res.statusText, stack: res, }], }; }) .then(({ data, errors }) => ( new Promise((resolve, reject) => { this.onEnd(); // if data in response is 'null' if (!data) { return reject(errors || [{}]); } // if all properties of data is 'null' const allDataKeyEmpty = Object.keys(data).every(key => !data[key]); if (allDataKeyEmpty) { return reject(errors); } return resolve({ data, errors }); }) )); } /** * get current server address * @returns {String} * @memberOf FetchQL */ getUrl() { return this.url; } /** * setting a new server address * @param {String} url * @memberOf FetchQL */ setUrl(url) { this.url = url; } /** * get information of enum type * @param {String[]} EnumNameList - array of enums' name * @returns {Promise} * @memberOf FetchQL */ getEnumTypes(EnumNameList) { const fullData = {}; // check cache status const unCachedEnumList = EnumNameList.filter((element) => { if (this.EnumMap[element]) { // enum has been cached fullData[element] = this.EnumMap[element]; return false; } return true; }); // immediately return the data if all enums have been cached if (!unCachedEnumList.length) { return new Promise((resolve) => { resolve({ data: fullData }); }); } // build query string for uncached enums const EnumTypeQuery = unCachedEnumList.map(type => ( `${type}: __type(name: "${type}") { ...EnumFragment }` )); const query = ` query { ${EnumTypeQuery.join('\n')} } fragment EnumFragment on __Type { kind description enumValues { name description } }`; const options = Object.assign({}, this.requestObject); options.body = JSON.stringify({ query }); this.onStart(); return this.fetch(this.url, options) .then((res) => { if (res.ok) { return res.json(); } // return an custom error stack if request error return { errors: [{ message: res.statusText, stack: res, }], }; }) .then(({ data, errors }) => ( new Promise((resolve, reject) => { this.onEnd(); // if data in response is 'null' and have any errors if (!data) { return reject(errors || [{ message: 'Do not get any data.' }]); } // if all properties of data is 'null' const allDataKeyEmpty = Object.keys(data).every(key => !data[key]); if (allDataKeyEmpty && errors && errors.length) { return reject(errors); } // merge enums' data const passData = Object.assign(fullData, data); // cache new enums' data Object.keys(data).map((key) => { this.EnumMap[key] = data[key]; return key; }); return resolve({ data: passData, errors }); }) )); } /** * calling on a request starting * if the request belong to a new queue, call the 'onStart' method */ onStart() { this.requestQueueLength++; if (this.requestQueueLength > 1 || !this.callbacks.onStart) { return; } this.callbacks.onStart(this.requestQueueLength); } /** * calling on a request ending * if current queue finished, calling the 'onEnd' method */ onEnd() { this.requestQueueLength--; if (this.requestQueueLength || !this.callbacks.onEnd) { return; } this.callbacks.onEnd(this.requestQueueLength); } /** * Callback of requests queue changes.(e.g. new queue or queue finished) * @callback FetchQL~requestQueueChanged * @param {number} queueLength - length of current request queue */ /** * remove empty props(null or '') from object * @param {Object} input * @returns {Object} * @memberOf FetchQL * @private */ doOmitEmptyVariables(input) { const nonEmptyObj = {}; Object.keys(input).map(key => { const value = input[key]; if ((typeof value === 'string' && value.length === 0) || value === null || value === undefined) { return key; } else if (value instanceof Object) { nonEmptyObj[key] = this.doOmitEmptyVariables(value); } else { nonEmptyObj[key] = value; } return key; }); return nonEmptyObj; } } export default FetchQL; [代码] 将wx的异步接口封装成Promise vendor/promisify.js [代码]function promisify(wx) { let wxx = { ...wx }; for (let attr in wxx) { if (!wxx.hasOwnProperty(attr) || typeof wxx[attr] != 'function') continue; // skip over the sync method if (/sync$/i.test(attr)) continue; wxx[attr + 'Async'] = function asyncFunction(argv = {}) { return new Promise(function (resolve, reject) { wxx[attr].call(wxx, { ...argv, ...{ success: res => resolve(res), fail: err => reject(err) } }); }); }; } return wxx; } export default promisify(typeof wx === 'object' ? wx : {}); [代码] localstorage vendor/storage.js [代码]class Storage { constructor(wx) { this.wx = wx; } static get timestamp() { return new Date() / 1000; } static __isExpired(entity) { if (!entity) return true; return Storage.timestamp - (entity.timestamp + entity.expiration) >= 0; } static get __info() { let info = {}; try { info = this.wx.getStorageInfoSync() || info; } catch (err) { console.error(err); } return info; } setItem(key, value, expiration) { const entity = { timestamp: Storage.timestamp, expiration, key, value }; this.wx.setStorageSync(key, JSON.stringify(entity)); return this; } getItem(key) { let entity; try { entity = this.wx.getStorageSync(key); if (entity) { entity = JSON.parse(entity); } else { return null; } } catch (err) { console.error(err); return null; } // 没有设置过期时间, 则直接返回值 if (!entity.expiration) return entity.value; // 已过期 if (Storage.__isExpired(entity)) { this.remove(key); return null; } else { return entity.value; } } removeItem(key) { try { this.wx.removeStorageSync(key); } catch (err) { console.error(err); } return this; } clear() { try { this.wx.clearStorageSync(); } catch (err) { console.error(err); } return this; } get info() { let info = {}; try { info = this.wx.getStorageInfoSync(); } catch (err) { console.error(err); } return info || {}; } get length() { return (this.info.keys || []).length; } } export default new Storage(wx); [代码] 状态管理 vendor/store.js [代码]module.exports = class Store { constructor({ state, actions }) { this.state = state || {} this.actions = actions || {} this.ctxs = [] } // 派发action, 统一返回promise action可以直接返回state dispatch(type, payload) { const update = res => { if (typeof res !== 'object') return this.setState(res) this.ctxs.map(ctx => ctx.setData(res)) return res } if (typeof this.actions[type] !== 'function') return const res = this.actions[type](this, payload) return res.constructor.toString().match(/function\s*([^(]*)/)[1] === 'Promise' ? res.then(update) : new Promise(resolve => resolve(update(res))) } // 修改state的方法 setState(data) { this.state = { ...this.state, ...data } } // 根据keys获取state getState(keys) { return keys.reduce((acc, key) => ({ ...acc, ...{ [key]: this.state[key] } }), {}) } // 映射state到实例中,可在onload或onshow中调用 mapState(keys, ctx) { if (!ctx || typeof ctx.setData !== 'function') return ctx.setData(this.getState(keys)) this.ctxs.push(ctx) } } [代码] 兼容 async/await vendor/regenerator.js [代码]/** * Copyright (c) 2014-present, Facebook, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ var regeneratorRuntime = (function (exports) { "use strict"; var Op = Object.prototype; var hasOwn = Op.hasOwnProperty; var undefined; // More compressible than void 0. var $Symbol = typeof Symbol === "function" ? Symbol : {}; var iteratorSymbol = $Symbol.iterator || "@@iterator"; var asyncIteratorSymbol = $Symbol.asyncIterator || "@@asyncIterator"; var toStringTagSymbol = $Symbol.toStringTag || "@@toStringTag"; function wrap(innerFn, outerFn, self, tryLocsList) { // If outerFn provided and outerFn.prototype is a Generator, then outerFn.prototype instanceof Generator. var protoGenerator = outerFn && outerFn.prototype instanceof Generator ? outerFn : Generator; var generator = Object.create(protoGenerator.prototype); var context = new Context(tryLocsList || []); // The ._invoke method unifies the implementations of the .next, // .throw, and .return methods. generator._invoke = makeInvokeMethod(innerFn, self, context); return generator; } exports.wrap = wrap; // Try/catch helper to minimize deoptimizations. Returns a completion // record like context.tryEntries[i].completion. This interface could // have been (and was previously) designed to take a closure to be // invoked without arguments, but in all the cases we care about we // already have an existing method we want to call, so there's no need // to create a new function object. We can even get away with assuming // the method takes exactly one argument, since that happens to be true // in every case, so we don't have to touch the arguments object. The // only additional allocation required is the completion record, which // has a stable shape and so hopefully should be cheap to allocate. function tryCatch(fn, obj, arg) { try { return { type: "normal", arg: fn.call(obj, arg) }; } catch (err) { return { type: "throw", arg: err }; } } var GenStateSuspendedStart = "suspendedStart"; var GenStateSuspendedYield = "suspendedYield"; var GenStateExecuting = "executing"; var GenStateCompleted = "completed"; // Returning this object from the innerFn has the same effect as // breaking out of the dispatch switch statement. var ContinueSentinel = {}; // Dummy constructor functions that we use as the .constructor and // .constructor.prototype properties for functions that return Generator // objects. For full spec compliance, you may wish to configure your // minifier not to mangle the names of these two functions. function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} // This is a polyfill for %IteratorPrototype% for environments that // don't natively support it. var IteratorPrototype = {}; IteratorPrototype[iteratorSymbol] = function () { return this; }; var getProto = Object.getPrototypeOf; var NativeIteratorPrototype = getProto && getProto(getProto(values([]))); if (NativeIteratorPrototype && NativeIteratorPrototype !== Op && hasOwn.call(NativeIteratorPrototype, iteratorSymbol)) { // This environment has a native %IteratorPrototype%; use it instead // of the polyfill. IteratorPrototype = NativeIteratorPrototype; } var Gp = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(IteratorPrototype); GeneratorFunction.prototype = Gp.constructor = GeneratorFunctionPrototype; GeneratorFunctionPrototype.constructor = GeneratorFunction; GeneratorFunctionPrototype[toStringTagSymbol] = GeneratorFunction.displayName = "GeneratorFunction"; // Helper for defining the .next, .throw, and .return methods of the // Iterator interface in terms of a single ._invoke method. function defineIteratorMethods(prototype) { ["next", "throw", "return"].forEach(function(method) { prototype[method] = function(arg) { return this._invoke(method, arg); }; }); } exports.isGeneratorFunction = function(genFun) { var ctor = typeof genFun === "function" && genFun.constructor; return ctor ? ctor === GeneratorFunction || // For the native GeneratorFunction constructor, the best we can // do is to check its .name property. (ctor.displayName || ctor.name) === "GeneratorFunction" : false; }; exports.mark = function(genFun) { if (Object.setPrototypeOf) { Object.setPrototypeOf(genFun, GeneratorFunctionPrototype); } else { genFun.__proto__ = GeneratorFunctionPrototype; if (!(toStringTagSymbol in genFun)) { genFun[toStringTagSymbol] = "GeneratorFunction"; } } genFun.prototype = Object.create(Gp); return genFun; }; // Within the body of any async function, `await x` is transformed to // `yield regeneratorRuntime.awrap(x)`, so that the runtime can test // `hasOwn.call(value, "__await")` to determine if the yielded value is // meant to be awaited. exports.awrap = function(arg) { return { __await: arg }; }; function AsyncIterator(generator) { function invoke(method, arg, resolve, reject) { var record = tryCatch(generator[method], generator, arg); if (record.type === "throw") { reject(record.arg); } else { var result = record.arg; var value = result.value; if (value && typeof value === "object" && hasOwn.call(value, "__await")) { return Promise.resolve(value.__await).then(function(value) { invoke("next", value, resolve, reject); }, function(err) { invoke("throw", err, resolve, reject); }); } return Promise.resolve(value).then(function(unwrapped) { // When a yielded Promise is resolved, its final value becomes // the .value of the Promise<{value,done}> result for the // current iteration. result.value = unwrapped; resolve(result); }, function(error) { // If a rejected Promise was yielded, throw the rejection back // into the async generator function so it can be handled there. return invoke("throw", error, resolve, reject); }); } } var previousPromise; function enqueue(method, arg) { function callInvokeWithMethodAndArg() { return new Promise(function(resolve, reject) { invoke(method, arg, resolve, reject); }); } return previousPromise = // If enqueue has been called before, then we want to wait until // all previous Promises have been resolved before calling invoke, // so that results are always delivered in the correct order. If // enqueue has not been called before, then it is important to // call invoke immediately, without waiting on a callback to fire, // so that the async generator function has the opportunity to do // any necessary setup in a predictable way. This predictability // is why the Promise constructor synchronously invokes its // executor callback, and why async functions synchronously // execute code before the first await. Since we implement simple // async functions in terms of async generators, it is especially // important to get this right, even though it requires care. previousPromise ? previousPromise.then( callInvokeWithMethodAndArg, // Avoid propagating failures to Promises returned by later // invocations of the iterator. callInvokeWithMethodAndArg ) : callInvokeWithMethodAndArg(); } // Define the unified helper method that is used to implement .next, // .throw, and .return (see defineIteratorMethods). this._invoke = enqueue; } defineIteratorMethods(AsyncIterator.prototype); AsyncIterator.prototype[asyncIteratorSymbol] = function () { return this; }; exports.AsyncIterator = AsyncIterator; // Note that simple async functions are implemented on top of // AsyncIterator objects; they just return a Promise for the value of // the final result produced by the iterator. exports.async = function(innerFn, outerFn, self, tryLocsList) { var iter = new AsyncIterator( wrap(innerFn, outerFn, self, tryLocsList) ); return exports.isGeneratorFunction(outerFn) ? iter // If outerFn is a generator, return the full iterator. : iter.next().then(function(result) { return result.done ? result.value : iter.next(); }); }; function makeInvokeMethod(innerFn, self, context) { var state = GenStateSuspendedStart; return function invoke(method, arg) { if (state === GenStateExecuting) { throw new Error("Generator is already running"); } if (state === GenStateCompleted) { if (method === "throw") { throw arg; } // Be forgiving, per 25.3.3.3.3 of the spec: // https://people.mozilla.org/~jorendorff/es6-draft.html#sec-generatorresume return doneResult(); } context.method = method; context.arg = arg; while (true) { var delegate = context.delegate; if (delegate) { var delegateResult = maybeInvokeDelegate(delegate, context); if (delegateResult) { if (delegateResult === ContinueSentinel) continue; return delegateResult; } } if (context.method === "next") { // Setting context._sent for legacy support of Babel's // function.sent implementation. context.sent = context._sent = context.arg; } else if (context.method === "throw") { if (state === GenStateSuspendedStart) { state = GenStateCompleted; throw context.arg; } context.dispatchException(context.arg); } else if (context.method === "return") { context.abrupt("return", context.arg); } state = GenStateExecuting; var record = tryCatch(innerFn, self, context); if (record.type === "normal") { // If an exception is thrown from innerFn, we leave state === // GenStateExecuting and loop back for another invocation. state = context.done ? GenStateCompleted : GenStateSuspendedYield; if (record.arg === ContinueSentinel) { continue; } return { value: record.arg, done: context.done }; } else if (record.type === "throw") { state = GenStateCompleted; // Dispatch the exception by looping back around to the // context.dispatchException(context.arg) call above. context.method = "throw"; context.arg = record.arg; } } }; } // Call delegate.iterator[context.method](context.arg) and handle the // result, either by returning a { value, done } result from the // delegate iterator, or by modifying context.method and context.arg, // setting context.delegate to null, and returning the ContinueSentinel. function maybeInvokeDelegate(delegate, context) { var method = delegate.iterator[context.method]; if (method === undefined) { // A .throw or .return when the delegate iterator has no .throw // method always terminates the yield* loop. context.delegate = null; if (context.method === "throw") { // Note: ["return"] must be used for ES3 parsing compatibility. if (delegate.iterator["return"]) { // If the delegate iterator has a return method, give it a // chance to clean up. context.method = "return"; context.arg = undefined; maybeInvokeDelegate(delegate, context); if (context.method === "throw") { // If maybeInvokeDelegate(context) changed context.method from // "return" to "throw", let that override the TypeError below. return ContinueSentinel; } } context.method = "throw"; context.arg = new TypeError( "The iterator does not provide a 'throw' method"); } return ContinueSentinel; } var record = tryCatch(method, delegate.iterator, context.arg); if (record.type === "throw") { context.method = "throw"; context.arg = record.arg; context.delegate = null; return ContinueSentinel; } var info = record.arg; if (! info) { context.method = "throw"; context.arg = new TypeError("iterator result is not an object"); context.delegate = null; return ContinueSentinel; } if (info.done) { // Assign the result of the finished delegate to the temporary // variable specified by delegate.resultName (see delegateYield). context[delegate.resultName] = info.value; // Resume execution at the desired location (see delegateYield). context.next = delegate.nextLoc; // If context.method was "throw" but the delegate handled the // exception, let the outer generator proceed normally. If // context.method was "next", forget context.arg since it has been // "consumed" by the delegate iterator. If context.method was // "return", allow the original .return call to continue in the // outer generator. if (context.method !== "return") { context.method = "next"; context.arg = undefined; } } else { // Re-yield the result returned by the delegate method. return info; } // The delegate iterator is finished, so forget it and continue with // the outer generator. context.delegate = null; return ContinueSentinel; } // Define Generator.prototype.{next,throw,return} in terms of the // unified ._invoke helper method. defineIteratorMethods(Gp); Gp[toStringTagSymbol] = "Generator"; // A Generator should always return itself as the iterator object when the // @@iterator function is called on it. Some browsers' implementations of the // iterator prototype chain incorrectly implement this, causing the Generator // object to not be returned from this call. This ensures that doesn't happen. // See https://github.com/facebook/regenerator/issues/274 for more details. Gp[iteratorSymbol] = function() { return this; }; Gp.toString = function() { return "[object Generator]"; }; function pushTryEntry(locs) { var entry = { tryLoc: locs[0] }; if (1 in locs) { entry.catchLoc = locs[1]; } if (2 in locs) { entry.finallyLoc = locs[2]; entry.afterLoc = locs[3]; } this.tryEntries.push(entry); } function resetTryEntry(entry) { var record = entry.completion || {}; record.type = "normal"; delete record.arg; entry.completion = record; } function Context(tryLocsList) { // The root entry object (effectively a try statement without a catch // or a finally block) gives us a place to store values thrown from // locations where there is no enclosing try statement. this.tryEntries = [{ tryLoc: "root" }]; tryLocsList.forEach(pushTryEntry, this); this.reset(true); } exports.keys = function(object) { var keys = []; for (var key in object) { keys.push(key); } keys.reverse(); // Rather than returning an object with a next method, we keep // things simple and return the next function itself. return function next() { while (keys.length) { var key = keys.pop(); if (key in object) { next.value = key; next.done = false; return next; } } // To avoid creating an additional object, we just hang the .value // and .done properties off the next function object itself. This // also ensures that the minifier will not anonymize the function. next.done = true; return next; }; }; function values(iterable) { if (iterable) { var iteratorMethod = iterable[iteratorSymbol]; if (iteratorMethod) { return iteratorMethod.call(iterable); } if (typeof iterable.next === "function") { return iterable; } if (!isNaN(iterable.length)) { var i = -1, next = function next() { while (++i < iterable.length) { if (hasOwn.call(iterable, i)) { next.value = iterable[i]; next.done = false; return next; } } next.value = undefined; next.done = true; return next; }; return next.next = next; } } // Return an iterator with no values. return { next: doneResult }; } exports.values = values; function doneResult() { return { value: undefined, done: true }; } Context.prototype = { constructor: Context, reset: function(skipTempReset) { this.prev = 0; this.next = 0; // Resetting context._sent for legacy support of Babel's // function.sent implementation. this.sent = this._sent = undefined; this.done = false; this.delegate = null; this.method = "next"; this.arg = undefined; this.tryEntries.forEach(resetTryEntry); if (!skipTempReset) { for (var name in this) { // Not sure about the optimal order of these conditions: if (name.charAt(0) === "t" && hasOwn.call(this, name) && !isNaN(+name.slice(1))) { this[name] = undefined; } } } }, stop: function() { this.done = true; var rootEntry = this.tryEntries[0]; var rootRecord = rootEntry.completion; if (rootRecord.type === "throw") { throw rootRecord.arg; } return this.rval; }, dispatchException: function(exception) { if (this.done) { throw exception; } var context = this; function handle(loc, caught) { record.type = "throw"; record.arg = exception; context.next = loc; if (caught) { // If the dispatched exception was caught by a catch block, // then let that catch block handle the exception normally. context.method = "next"; context.arg = undefined; } return !! caught; } for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; var record = entry.completion; if (entry.tryLoc === "root") { // Exception thrown outside of any try block that could handle // it, so set the completion value of the entire function to // throw the exception. return handle("end"); } if (entry.tryLoc <= this.prev) { var hasCatch = hasOwn.call(entry, "catchLoc"); var hasFinally = hasOwn.call(entry, "finallyLoc"); if (hasCatch && hasFinally) { if (this.prev < entry.catchLoc) { return handle(entry.catchLoc, true); } else if (this.prev < entry.finallyLoc) { return handle(entry.finallyLoc); } } else if (hasCatch) { if (this.prev < entry.catchLoc) { return handle(entry.catchLoc, true); } } else if (hasFinally) { if (this.prev < entry.finallyLoc) { return handle(entry.finallyLoc); } } else { throw new Error("try statement without catch or finally"); } } } }, abrupt: function(type, arg) { for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; if (entry.tryLoc <= this.prev && hasOwn.call(entry, "finallyLoc") && this.prev < entry.finallyLoc) { var finallyEntry = entry; break; } } if (finallyEntry && (type === "break" || type === "continue") && finallyEntry.tryLoc <= arg && arg <= finallyEntry.finallyLoc) { // Ignore the finally entry if control is not jumping to a // location outside the try/catch block. finallyEntry = null; } var record = finallyEntry ? finallyEntry.completion : {}; record.type = type; record.arg = arg; if (finallyEntry) { this.method = "next"; this.next = finallyEntry.finallyLoc; return ContinueSentinel; } return this.complete(record); }, complete: function(record, afterLoc) { if (record.type === "throw") { throw record.arg; } if (record.type === "break" || record.type === "continue") { this.next = record.arg; } else if (record.type === "return") { this.rval = this.arg = record.arg; this.method = "return"; this.next = "end"; } else if (record.type === "normal" && afterLoc) { this.next = afterLoc; } return ContinueSentinel; }, finish: function(finallyLoc) { for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; if (entry.finallyLoc === finallyLoc) { this.complete(entry.completion, entry.afterLoc); resetTryEntry(entry); return ContinueSentinel; } } }, "catch": function(tryLoc) { for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; if (entry.tryLoc === tryLoc) { var record = entry.completion; if (record.type === "throw") { var thrown = record.arg; resetTryEntry(entry); } return thrown; } } // The context.catch method must only be called with a location // argument that corresponds to a known catch block. throw new Error("illegal catch attempt"); }, delegateYield: function(iterable, resultName, nextLoc) { this.delegate = { iterator: values(iterable), resultName: resultName, nextLoc: nextLoc }; if (this.method === "next") { // Deliberately forget the last sent value so that we don't // accidentally pass it on to the delegate. this.arg = undefined; } return ContinueSentinel; } }; // Regardless of whether this script is executing as a CommonJS module // or not, return the runtime object so that we can declare the variable // regeneratorRuntime in the outer scope, which allows this module to be // injected easily by `bin/regenerator --include-runtime script.js`. return exports; }( // If this script is executing as a CommonJS module, use module.exports // as the regeneratorRuntime namespace. Otherwise create a new empty // object. Either way, the resulting object will be used to initialize // the regeneratorRuntime variable at the top of this file. typeof module === "object" ? module.exports : {} )); [代码] 后端 [代码]const typeDefs = gql` # schema 下面是根类型,约定是 RootQuery 和 RootMutation schema { query: Query } # 定义具体的 Query 的结构 type Query { # 登陆接口 login(code: String!, encryptedData: String, iv: String): Login } type Login { token: String! userInfo: UserInfo } type UserInfo { nickName: String gender: String avatarUrl: String } `; const resolvers = { Query: { async login(parent, { code, encryptedData, iv }) { const { sessionKey, openId, unionId } = await wxService.code2Session(code); const userInfo = encryptedData && iv ? wxService.decryptData(sessionKey, encryptedData, iv) : { openId, unionId }; if (userInfo.nickName) { userService.createOrUpdateWxUser(userInfo); } const token = await userService.generateJwtToken(userInfo); return { token, userInfo }; }, }, }; [代码]
2019-04-21 - 构建npm失败,没有找到 node_modules 目录。
问题:微信开发工具刚支持npm时我安装成功过package,但是今天需要安装另外一些package时出现这个问题。此时我想是不是我项目的问题,于是我重新建了一个小程序项目来测试,情况如下: 新建小程序,选择“快速启动模版”。 在该项目根目录下运行命令:npm install --production vant-weapp,自动在项目根目录下创建了 node_modules 文件夹,与 package-lock.json 文件。 点击 工具-构建 npm,此时提示“没有找到 node_modules 目录。” package-lock.json 文件内容如下: { "requires": true, "lockfileVersion": 1, "dependencies": { vant-weapp": { "version": "0.4.9", "resolved": "http://registry.npm.taobao.org/vant-weapp/download/vant-weapp-0.4.9.tgz", "integrity": "sha1-wZExmnw1085cwzggP53LuGrywKk=" } } } 为何新建立的项目都能出现这个问题? 另外,这个提问网页当我进来输入完问题内容后再去选择的提问模块,我的提问内容全部消失???
2018-12-18