- 小程序wxRequest封装一个无痛刷新token方法?
当token过期后就去获取新的token,但要阻止同一个页面多个异步请求,在请求到新的token前,把请求任务放在队列,新token生成后再执行任务队列的请求,大概咋写啊,我怎么写都不对,就是怎么回调任务队列的请求 这是我现在的代码,大概思路是这样子,但是好像没有效果 var BASE_URL = 'https://www.****.com' // 定义一个flag 判断是否刷新Token中 let isRefreshing = false; // 保存需要重新发起请求的队列 let retryRequests = []; class Request { /** * Request请求方法 * @param {String} url 链接 * @param {Objece} params 参数 * @param {Boolean} isToken 是否携带token * @return {Promise} 包含抓取任务的Promise */ getApi(url, params, isToken, method) { if (isToken === undefined) isToken = true; if (method === undefined) method = 'GET'; let token = wx.getStorageSync('logintoken') || ''; let that = this; return new Promise((resolve, reject) => { //请求封装 wx.request({ url: `${BASE_URL}${url}`, method: method, data: params, header: { 'Content-Type': 'application/json', "token": token, }, success: res => { if (res.statusCode == 200) { const code = res.data.code || 200; if (code == 200) { resolve(res); } else if (code == 10245) { // 返回10245表示token过期 多个异步请求 token 刷新处理 console.log('token过期了') console.log('isRefreshing 状态',isRefreshing) console.log('retryRequests 数组',retryRequests) if(!isRefreshing){ // 改变flag状态,表示正在刷新Token中 不让其他请求进来 isRefreshing = true // 去获取新token getApp().GetLogin().then(res=>{ // 遍历执行需要重新发起请求的队列 // retryRequests.forEach(cb => cb()) // retryRequests =[] }) .finally(() => { // 请求完成后重置flag isRefreshing = false; }) }else{ // 正在刷新token,返回一个未执行resolve的promise // 把promise 的resolve 保存到队列的回调里面,等待刷新Token后调用 // 原调用者会处于等待状态直到 队列重新发起请求,再把响应返回,以达到用户无感知的目的(无痛刷新) // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行 // retryRequests.push(()=>that(url, params, isToken, method)); } } else { wx.showToast({ title: res.data.msg || '服务器出错', icon: 'none', duration: 1200, mask: true }); reject(res.data); } } else { wx.showToast({ title: '[' + res.statusCode + '] 服务器出错,请重试', icon: 'none', duration: 1200 }); reject(res); } }, fail: err => { console.log(err) wx.showToast({ title: '网络错误', icon: 'none', duration: 1200 }); reject(err); }, complete: () => { } }); }); } } let request = new Request(); module.exports = { request: request.getApi, url: BASE_URL }
2022-03-31 - 一个通用request的封装
小程序内置了[代码]wx.request[代码],用于向后端发送请求,我们先来看看它的文档: wx.request(OBJECT) 发起网络请求。使用前请先阅读说明。 OBJECT参数说明: 参数名 类型 必填 默认值 说明 最低版本 url String 是 - 开发者服务器接口地址 - data Object/String/ArrayBuffer 否 - 请求的参数 - header Object 否 - 设置请求的 header,header 中不能设置 Referer。 - method String 否 GET (需大写)有效值:OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT - dataType String 否 json 如果设为json,会尝试对返回的数据做一次 JSON.parse - responseType String 否 text 设置响应的数据类型。合法值:text、arraybuffer 1.7.0 success Function 否 - 收到开发者服务成功返回的回调函数 - fail Function 否 - 接口调用失败的回调函数 - complete Function 否 - 接口调用结束的回调函数(调用成功、失败都会执行) - success返回参数说明: 参数 类型 说明 最低版本 data Object/String/ArrayBuffer 开发者服务器返回的数据 - statusCode Number 开发者服务器返回的 HTTP 状态码 - header Object 开发者服务器返回的 HTTP Response Header 1.2.0 这里我们主要看两点: 回调函数:success、fail、complete; success的返回参数:data、statusCode、header。 相对于通过入参传回调函数的方式,我更喜欢promise的链式,这是我期望的第一个点;success的返回参数,在实际开发过程中,我只关心data部分,这里可以做一下处理,这是第二点。 promisify 小程序默认支持promise,所以这一点改造还是很简单的: [代码]/** * promise请求 * 参数:参考wx.request * 返回值:[promise]res */ function requestP(options = {}) { const { success, fail, } = options; return new Promise((res, rej) => { wx.request(Object.assign( {}, options, { success: res, fail: rej, }, )); }); } [代码] 这样一来我们就可以使用这个函数来代替wx.request,并且愉快地使用promise链式: [代码]requestP({ url: '/api', data: { name: 'Jack' } }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); [代码] 注意,小程序的promise并没有实现finally,Promise.prototype.finally是undefined,所以complete不能用finally代替。 精简返回值 精简返回值也是很简单的事情,第一直觉是,当请求返回并丢给我一大堆数据时,我直接resolve我要的那一部分数据就好了嘛: [代码]return new Promise((res, rej) => { wx.request(Object.assign( {}, options, { success(r) { res(r.data); // 这里只取data }, fail: rej, }, )); }); [代码] but!这里需要注意,我们仅仅取data部分,这时候默认所有success都是成功的,其实不然,wx.request是一个基础的api,fail只发生在系统和网络层面的失败情况,比如网络丢包、域名解析失败等等,而类似404、500之类的接口状态,依旧是调用success,并体现在[代码]statusCode[代码]上。 从业务上讲,我只想处理json的内容,并对json当中的相关状态进行处理;如果一个接口返回的不是约定好的json,而是类似404、500之类的接口异常,我统一当成接口/网络错误来处理,就像jquery的ajax那样。 也就是说,如果我不对[代码]statusCode[代码]进行区分,那么包括404、500在内的所有请求结果都会走[代码]requestP().then[代码],而不是[代码]requestP().catch[代码]。这显然不是我们熟悉的使用方式。 于是我从jquery的ajax那里抄来了一段代码。。。 [代码]/** * 判断请求状态是否成功 * 参数:http状态码 * 返回值:[Boolen] */ function isHttpSuccess(status) { return status >= 200 && status < 300 || status === 304; } [代码] [代码]isHttpSuccess[代码]用来决定一个http状态码是否判为成功,于是结合[代码]requestP[代码],我们可以这么来用: [代码]return new Promise((res, rej) => { wx.request(Object.assign( {}, options, { success(r) { const isSuccess = isHttpSuccess(r.statusCode); if (isSuccess) { // 成功的请求状态 res(r.data); } else { rej({ msg: `网络错误:${r.statusCode}`, detail: r }); } }, fail: rej, }, )); }); [代码] 这样我们就可以直接resolve返回结果中的data,而对于非成功的http状态码,我们则直接reject一个自定义的error对象,这样就是我们所熟悉的ajax用法了。 登录 我们经常需要识别发起请求的当前用户,在web中这通常是通过请求中携带的cookie实现的,而且对于前端开发者是无感知的;小程序中没有cookie,所以需要主动地去补充相关信息。 首先要做的是:登录。 通过[代码]wx.login[代码]接口我们可以得到一个[代码]code[代码],调用后端登录接口将code传给后端,后端再用code去调用微信的登录接口,换取[代码]sessionKey[代码],最后生成一个[代码]sessionId[代码]返回给前端,这就完成了登录。 [图片] 具体参考微信官方文档:wx.login [代码]const apiUrl = 'https://jack-lo.github.io'; let sessionId = ''; /** * 登录 * 参数:undefined * 返回值:[promise]res */ function login() { return new Promise((res, rej) => { // 微信登录 wx.login({ success(r1) { if (r1.code) { // 获取sessionId requestP({ url: `${apiUrl}/api/login`, data: { code: r1.code, }, method: 'POST' }) .then((r2) => { if (r2.rcode === 0) { const { sessionId } = r2.data; // 保存sessionId sessionId = sessionId; res(r2); } else { rej({ msg: '获取sessionId失败', detail: r2 }); } }) .catch((err) => { rej(err); }); } else { rej({ msg: '获取code失败', detail: r1 }); } }, fail: rej, }); }); } [代码] 好的,我们做好了登录并且顺利获取到了sessionId,接下来是考虑怎么把sessionId通过请求带上去。 sessionId 为了将状态与数据区分开来,我们决定不通过data,而是通过header的方式来携带sessionId,我们对原本的requestP稍稍进行修改,使得它每次调用都自动在header里携带sessionId: [代码]function requestP(options = {}) { const { success, fail, } = options; // 统一注入约定的header let header = Object.assign({ sessionId: sessionId }, options.header); return new Promise((res, rej) => { ... }); } [代码] 好的,现在请求会自动带上sessionId了; 但是,革命尚未完成: 我们什么时候去登录呢?或者说,我们什么时候去获取sessionId? 假如还没登录就发起请求了怎么办呢? 登录过期了怎么办呢? 我设想有这样一个逻辑: 当我发起一个请求的时候,如果这个请求不需要sessionId,则直接发出; 如果这个请求需要携带sessionId,就去检查现在是否有sessionId,有的话直接携带,发起请求; 如果没有,自动去走登录的流程,登录成功,拿到sessionId,再去发送这个请求; 如果有,但是最后请求返回结果是sessionId过期了,那么程序自动走登录的流程,然后再发起一遍。 其实上面的那么多逻辑,中心思想只有一个:都是为了拿到sessionId! 我们需要对请求做一层更高级的封装。 首先我们需要一个函数专门去获取sessionId,它将解决上面提到的2、3点: [代码]/** * 获取sessionId * 参数:undefined * 返回值:[promise]sessionId */ function getSessionId() { return new Promise((res, rej) => { // 本地sessionId缺失,重新登录 if (!sessionId) { login() .then((r1) => { res(r1.data.sessionId); }) .catch(rej); } } else { res(sessionId); } }); } [代码] 好的,接下来我们解决第1、4点,我们先假定:sessionId过期的时候,接口会返回[代码]code=401[代码]。 整合了getSessionId,得到一个更高级的request方法: [代码]/** * ajax高级封装 * 参数:[Object]option = {},参考wx.request; * [Boolen]keepLogin = false * 返回值:[promise]res */ function request(options = {}, keepLogin = true) { if (keepLogin) { return new Promise((res, rej) => { getSessionId() .then((r1) => { // 获取sessionId成功之后,发起请求 requestP(options) .then((r2) => { if (r2.rcode === 401) { // 登录状态无效,则重新走一遍登录流程 // 销毁本地已失效的sessionId sessionId = ''; getSessionId() .then((r3) => { requestP(options) .then(res) .catch(rej); }); } else { res(r2); } }) .catch(rej); }) .catch(rej); }); } else { // 不需要sessionId,直接发起请求 return requestP(options); } } [代码] 留意req的第二参数keepLogin,是为了适配有些接口不需要sessionId,但因为我的业务里大部分接口都需要登录状态,所以我默认值为true。 这差不多就是我们封装request的最终形态了。 并发处理 这里其实我们还需要考虑一个问题,那就是并发。 试想一下,当我们的小程序刚打开的时候,假设页面会同时发出5个请求,而此时没有sessionId,那么,这5个请求按照上面的逻辑,都会先去调用login去登录,于是乎,我们就会发现,登录接口被同步调用了5次!并且后面的调用将导致前面的登录返回的sessionId过期~ 这bug是很严重的,理论上来说,登录我们只需要调用一次,然后一直到过期为止,我们都不需要再去登录一遍了。 ——那么也就是说,同一时间里的所有接口其实只需要登录一次就可以了。 ——也就是说,当有登录的请求发出的时候,其他那些也需要登录状态的接口,不需要再去走登录的流程,而是等待这次登录回来即可,他们共享一次登录操作就可以了! 解决这个问题,我们需要用到队列。 我们修改一下getSessionId这里的逻辑: [代码]const loginQueue = []; let isLoginning = false; /** * 获取sessionId * 参数:undefined * 返回值:[promise]sessionId */ function getSessionId() { return new Promise((res, rej) => { // 本地sessionId缺失,重新登录 if (!sessionId) { loginQueue.push({ res, rej }); if (!isLoginning) { isLoginning = true; login() .then((r1) => { isLoginning = false; while (loginQueue.length) { loginQueue.shift().res(r1); } }) .catch((err) => { isLoginning = false; while (loginQueue.length) { loginQueue.shift().rej(err); } }); } } else { res(sessionId); } }); } [代码] 使用了isLoginning这个变量来充当锁的角色,锁的目的就是当登录正在进行中的时候,告诉程序“我已经在登录了,你先把回调都加队列里去吧”,当登录结束之后,回来将锁解开,把回调全部执行并清空队列。 这样我们就解决了问题,同时提高了性能。 封装 在做完以上工作以后,我们都很清楚的封装结果就是[代码]request[代码],所以我们把request暴露出去就好了: [代码]function request() { ... } module.exports = request; [代码] 这般如此之后,我们使用起来就可以这样子: [代码]const request = require('request.js'); Page({ ready() { // 获取热门列表 request({ url: 'https://jack-lo.github.io/api/hotList', data: { page: 1 } }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); // 获取最新信息 request({ url: 'https://jack-lo.github.io/api/latestList', data: { page: 1 } }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); }, }); [代码] 是不是很方便,可以用promise的方式,又不必关心登录的问题。 然而可达鸭眉头一皱,发现事情并不简单,一个接口有可能在多个地方被多次调用,每次我们都去手写这么一串[代码]url[代码]参数,并不那么方便,有时候还不好找,并且容易出错。 如果能有个地方专门记录这些url就好了;如果每次调用接口,都能像调用一个函数那么简单就好了。 基于这个想法,我们还可以再做一层封装,我们可以把所有的后端接口,都封装成一个方法,调用接口就相对应调用这个方法: [代码]const apiUrl = 'https://jack-lo.github.io'; const req = { // 获取热门列表 getHotList(data) { const url = `${apiUrl}/api/hotList` return request({ url, data }); }, // 获取最新列表 getLatestList(data) { const url = `${apiUrl}/api/latestList` return request({ url, data }); } } module.exports = req; // 注意这里暴露的已经不是request,而是req [代码] 那么我们的调用方式就变成了: [代码]const req = require('request.js'); Page({ ready() { // 获取热门列表 req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); // 获取最新信息 req.getLatestList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); } }); [代码] 这样一来就方便了很多,而且有一个很大的好处,那就是当某个接口的地址需要统一修改的时候,我们只需要对[代码]request.js[代码]进行修改,其他调用的地方都不需要动了。 错误信息的提炼 最后的最后,我们再补充一个可轻可重的点,那就是错误信息的提炼。 当我们在封装这么一个[代码]req[代码]对象的时候,我们的promise曾经reject过很多的错误信息,这些错误信息有可能来自: [代码]wx.request[代码]的fail; 不符合[代码]isHttpSuccess[代码]的网络错误; getSessionId失败; … 等等的一切可能。 这就导致了我们在提炼错误信息的时候陷入困境,到底catch到的会是哪种[代码]error[代码]对象? 这么看你可能不觉得有问题,我们来看看下面的例子: [代码]req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); [代码] 假如上面的例子中,我想要的不仅仅是[代码]console.log(err)[代码],而是想将对应的错误信息弹窗出来,我应该怎么做? 我们只能将所有可能出现的错误都检查一遍: [代码]req.getHotList({ page: 1 }) .then((res) => { if (res.code !== 0) { // 后端接口报错格式 wx.showModal({ content: res.msg }); } }) .catch((err) => { let msg = '未知错误'; // 文本信息直接使用 if (typeof err === 'string') { msg = err; } // 小程序接口报错 if (err.errMsg) { msg = err.errMsg; } // 自定义接口的报错,比如网络错误 if (err.detail && err.detail.errMsg) { msg = err.detail.errMsg; } // 未知错误 wx.showModal({ content: msg }); }); [代码] 这就有点尴尬了,提炼错误信息的代码量都比业务还多几倍,而且还是每个接口调用都要写一遍~ 为了解决这个问题,我们需要封装一个方法来专门做提炼的工作: [代码]/** * 提炼错误信息 * 参数:err * 返回值:[string]errMsg */ function errPicker(err) { if (typeof err === 'string') { return err; } return err.msg || err.errMsg || (err.detail && err.detail.errMsg) || '未知错误'; } [代码] 那么过程会变成: [代码]req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { const msg = req.errPicker(err); // 未知错误 wx.showModal({ content: msg }); }); [代码] 好吧,我们再偷懒一下,把wx.showModal也省去了: [代码]/** * 错误弹窗 */ function showErr(err) { const msg = errPicker(err); console.log(err); wx.showModal({ showCancel: false, content: msg }); } [代码] 最后就变成了: [代码]req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch(req.showErr); [代码] 至此,一个简单的wx.request封装过程便完成了,封装过的[代码]req[代码]比起原来,使用上更加方便,扩展性和可维护性也更好。 结尾 以上内容其实是简化版的[代码]mp-req[代码],介绍了[代码]mp-req[代码]这一工具的实现初衷以及思路,使用[代码]mp-req[代码]来管理接口会更加的便捷,同时[代码]mp-req[代码]也提供了更加丰富的功能,比如插件机制、接口的缓存,以及接口分类等,欢迎大家关注mp-req了解更多内容。 以上最终代码可以在这里获取:req.js。
2020-08-04 - 小程序app.onLaunch与page.onLoad异步问题的最佳实践
场景: 在小程序中大家应该都有这样的场景,在onLaunch里用wx.login静默登录拿到code,再用code去发送请求获取token、用户信息等,整个过程都是异步的,然后我们在业务页面里onLoad去用的时候异步请求还没回来,导致没拿到想要的数据,以往要么监听是否拿到,要么自己封装一套回调,总之都挺麻烦,每个页面都要写一堆无关当前页面的逻辑。 直接上终极解决方案,公司内部已接入两年很稳定: 1.可完美解决异步问题 2.不污染原生生命周期,与onLoad等钩子共存 3.使用方便 4.可灵活定制异步钩子 5.采用监听模式实现,接入无需修改以前相关逻辑 6.支持各种小程序和vue架构 。。。 //为了简洁明了的展示使用场景,以下有部分是伪代码,请勿直接粘贴使用,具体使用代码看Github文档 //app.js //globalData提出来声明 let globalData = { // 是否已拿到token token: '', // 用户信息 userInfo: { userId: '', head: '' } } //注册自定义钩子 import CustomHook from 'spa-custom-hooks'; CustomHook.install({ 'Login':{ name:'Login', watchKey: 'token', onUpdate(token){ //有token则触发此钩子 return !!token; } }, 'User':{ name:'User', watchKey: 'userInfo', onUpdate(user){ //获取到userinfo里的userId则触发此钩子 return !!user.userId; } } }, globalData) // 正常走初始化逻辑 App({ globalData, onLaunch() { //发起异步登录拿token login((token)=>{ this.globalData.token = token //使用token拿用户信息 getUser((user)=>{ this.globalData.user = user }) }) } }) //关键点来了 //Page.js,业务页面使用 Page({ onLoadLogin() { //拿到token啦,可以使用token发起请求了 const token = getApp().globalData.token }, onLoadUser() { //拿到用户信息啦 const userInfo = getApp().globalData.userInfo }, onReadyUser() { //页面初次渲染完毕 && 拿到用户信息,可以把头像渲染在canvas上面啦 const userInfo = getApp().globalData.userInfo // 获取canvas上下文 const ctx = getCanvasContext2d() ctx.drawImage(userInfo.head,0,0,100,100) }, onShowUser() { //页面每次显示 && 拿到用户信息,我要在页面每次显示的时候根据userInfo走不同的逻辑 const userInfo = getApp().globalData.userInfo switch(userInfo.sex){ case 0: // 走女生逻辑 break case 1: // 走男生逻辑 break } } }) 具体文档和Demo见↓ Github:https://github.com/1977474741/spa-custom-hooks 祝大家用的愉快,记得star哦
2023-04-23 - 小程序中如何实现并发控制?
小程序中如何实现并发控制? 一、性能之网络请求数 wx.request、wx.uploadFile、wx.downloadFile 的最大并发限制是 10 个; 小程序中,短时间内发起太多请求会触发小程序并发请求数量的限制同时太多请求也可能导致加载慢等问题,应合理控制请求数量,甚至做请求的合并等。 上传多图使用Promise.all并发处理,很容易触发限制,导致请求失败。 故需做并发控制。实现并发控制此处将对p-limit和async-pool进行研究 二、p-limit并发控制的实现 2.1 p-limit的使用 [代码]const limit = pLimit(1); const input = [ limit(() => fetchSomething('foo')), limit(() => fetchSomething('bar')), limit(() => doSomething()) ]; // 一次只运行一个promise const result = await Promise.all(input); console.log(result); [代码] pLimit(concurrency) 返回一个[代码]limit[代码]函数。 concurrency(Number): 表示并发限制,最小值为1,默认值为Infinity limit(fn, …args) 返回通过调用[代码]fn(...args)[代码]返回的promise fn(Function): 表示Promise-returning/async function args: 表示传递给fn的参数 limit.activeCount 当前正在运行的promise数量 limit.pendingCount 等待运行的promise数量(即它们的内部fn尚未被调用)。 limit.clearQueue() 丢弃等待运行的pending promises。 2.2 p-limit 的实现 要想理解p-limit必须先掌握浏览器中event-loop的执行顺序: [代码]console.log('script start') async function async1() { await async2() console.log('async1 end') } async function async2() { console.log('async2 end') } // 以上两个async函数可改写为以下代码 // new Promise((resolve, reject) => { // console.log('async2 end') // // Promise.resolve() 将代码插入微任务队列尾部 // // resolve 再次插入微任务队列尾部 // resolve(Promise.resolve()) // }).then(() => { // console.log('async1 end') // }) // 如果 await 后面跟着 Promise 的话,async1 end 需要等待三个 tick 才能执行到 async1() setTimeout(function() { console.log('setTimeout') }, 0) new Promise(resolve => { console.log('Promise') resolve() }) .then(function() { console.log('promise1') }) .then(function() { console.log('promise2') }) console.log('script end') // script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout [代码] 先介绍下宏任务和微任务具体有哪些内容, 微任务包括 [代码]process.nextTick[代码] ,[代码]promise[代码] ,[代码]MutationObserver[代码],其中 [代码]process.nextTick[代码] 为 Node 独有。 宏任务包括 [代码]script[代码] , [代码]setTimeout[代码] ,[代码]setInterval[代码] ,[代码]setImmediate[代码] ,[代码]I/O[代码] ,[代码]UI rendering[代码]。 Event Loop 执行顺序如下所示: 首先执行同步代码,这属于宏任务 当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执行 执行所有微任务 当执行完所有微任务后,如有必要会渲染页面 然后开始下一轮 Event Loop,执行宏任务中的异步代码,也就是 [代码]setTimeout[代码] 中的回调函数 [代码]function pLimit(concurrency) { if(!((Number.isInteger(concurrency)||concurrency===Number.POSITIVE_INFINITY)&&concurrency>0)) { throw new TypeError('Expected `concurrency` to be a number from 1 and up'); } // 用一个queue队列维护所有Promise异步函数 const queue=[]; let activeCount=0; const next=() => { // 某异步函数完成后,需要将activeCount-- activeCount--; if(queue.length>0) { // 再次从队列中出队并执行异步函数,activeCount维持在concurrency queue.shift()(); } }; const run=async (fn,resolve,args) => { // activeCount++; // 进一步将fn封装为异步函数并运行 const result=(async () => fn(...args))(); // 此处返回generator函数 的resolve值,即Promise.all resolve(result); try { // 等待result异步函数完成(例如某请求完成) await result; } catch {} next(); }; const enqueue=(fn,resolve,args) => { queue.push(run.bind(undefined,fn,resolve,args)); // setTimeout(()=>{ // // 正在运行的Promise数量activeCount始终不大于concurrency,从而达到控制并发的目的 // if(activeCount<concurrency&&queue.length>0) { // // 队列出队并执行改函数 // queue.shift()(); // } // },0); (async () => { // 这个函数需要等到下一个微任务再比较 `activeCount` 和 `concurrency` // 因为 `activeCount` 在 run 函数出列和调用时异步更新。 // if 语句中的比较也需要异步进行,以获取 `activeCount` 的最新值。 await Promise.resolve(); // 正在运行的Promise数量activeCount始终不大于concurrency,从而达到控制并发的目的 if (activeCount < concurrency && queue.length > 0) { // 队列出队并执行改函数 queue.shift()(); } })(); }; const generator = (fn,...args) => new Promise(resolve => { enqueue(fn,resolve,args); }); Object.defineProperties(generator,{ // 正在运行的Promise数量 activeCount: { get: () => activeCount, }, // 等待运行的Promise数量 pendingCount: { get: () => queue.length, }, // 清空queue队列中的异步函数 clearQueue: { value: () => { while(queue.length!=0) { pueue.shift(); } }, }, }); return generator; } [代码] 三、asyncPool并发控制的实现 async-pool 这个库提供了 ES7 和 ES6 两种不同版本的实现,在分析其具体实现之前,我们来看一下它如何使用。 3.1 asyncPool 的使用 [代码]function asyncPool(poolLimit, array, iteratorFn){ ... } [代码] 该函数接收 3 个参数: [代码]poolLimit[代码](Number):表示限制的并发数; [代码]array[代码](Array):表示任务数组; [代码]iteratorFn[代码](Function):表示迭代函数,用于实现对每个任务项进行处理,该函数会返回一个 Promise 对象或异步函数。 [代码]asyncPool[代码]在有限的并发池中运行多个promise-returning & async functions。一旦其中一个承诺被拒绝,它就会立即拒绝。当所有 Promise 完成时,它就会resolves。它尽快调用迭代器函数(在并发限制下)。例如: [代码]const timeout = i => new Promise(resolve => setTimeout(() => resolve(i), i)); await asyncPool(2, [1000, 5000, 3000, 2000], timeout); // Call iterator (i = 1000) // Call iterator (i = 5000) // Pool limit of 2 reached, wait for the quicker one to complete... // 1000 finishes // Call iterator (i = 3000) // Pool limit of 2 reached, wait for the quicker one to complete... // 3000 finishes // Call iterator (i = 2000) // Itaration is complete, wait until running ones complete... // 5000 finishes // 2000 finishes // Resolves, results are passed in given array order `[1000, 5000, 3000, 2000]`. [代码] 通过观察以上的注释信息,我们可以大致地了解 [代码]asyncPool[代码] 函数内部的控制流程 3.2 asyncPool 实现 [代码]async function asyncPool(poolLimit, array, iteratorFn) { const ret = []; // 存储所有的异步任务 const executing = []; // 存储正在执行的异步任务 for (const item of array) { // 调用iteratorFn函数创建异步任务 const p = Promise.resolve().then(() => iteratorFn(item, array)); ret.push(p); // 保存新的异步任务 // 当poolLimit值小于或等于总任务个数时,进行并发控制 if (poolLimit <= array.length) { // 当任务完成后,从正在执行的任务数组中移除已完成的任务 const e = p.then(() => executing.splice(executing.indexOf(e), 1)); executing.push(e); // 保存正在执行的异步任务 if (executing.length >= poolLimit) { await Promise.race(executing); // 等待较快的任务执行完成 } } } return Promise.all(ret); } [代码] 在以上代码中,充分利用了 [代码]Promise.all[代码] 和 [代码]Promise.race[代码] 函数特点,再结合 ES7 中提供的 [代码]async await[代码] 特性,最终实现了并发控制的功能。利用 [代码]await Promise.race(executing);[代码] 这行语句,我们会等待 正在执行任务列表 中较快的任务执行完成之后,才会继续执行下一次循环。 四、实践运用 使用p-limit [代码]const limit=pLimit(5); const fn=() => { return axios({ method: 'get', url: 'https://www.fastmock.site/mock/883c2b5177653e4ef705b7ffdc680af1/daily/story', }) .then(function(response) { return response.data; }); }; const input=[]; for(let i=0;i<50;i++) { input.push(limit(fn)) } Promise.all(input).then(res => { console.log('res',res) }); [代码] [图片] 使用asyncPool [代码]const fn=() => { return axios({ method: 'get', url: 'https://www.fastmock.site/mock/883c2b5177653e4ef705b7ffdc680af1/daily/story', }) .then(function(response) { return response.data; }); } const input=[]; for(let i=0;i<50;i++) { input.push(fn); } const timeout= f => new Promise(resolve => setTimeout(() => resolve(f()))); asyncPool(5,input,timeout).then(res => { console.log('res',res); }) [代码] [图片]
2021-12-09 - 基于小程序请求接口 wx.request 封装的类 axios 请求
Introduction wx.request 的配置、axios 的调用方式 ----------------源码戳我--------------- ---------------demo 戳我-------------- feature 支持 wx.request 所有配置项 支持 axios 调用方式 支持 自定义 baseUrl 支持 自定义响应状态码对应 resolve 或 reject 状态 支持 对响应(resolve/reject)分别做统一的额外处理 支持 转换请求数据和响应数据 支持 请求缓存(内存或本地缓存),可设置缓存标记、过期时间 use app.js @onLaunch [代码] import axios form 'axios' axios.creat({ header: { content-type': 'application/x-www-form-urlencoded; charset=UTF-8' }, baseUrl: 'https://api.baseurl.com', ... }); [代码] page.js [代码]axios .post("/url", { id: 123 }) .then((res) => { console.log(response); }) .catch((err) => { console.log(err); }); [代码] API [代码] axios(config) - 默认get axios(url[, config]) - 默认get axios.get(url[, config]) axios.post(url[, data[, config]]) axios.cache(url[, data[, config]]) - 缓存请求(内存) axios.cache.storage(url[, data[, config]]) - 缓存请求(内存 & local storage) axios.creat(config) - 初始化定制配置,覆盖默认配置 [代码] config 默认配置项说明 [代码]export default { // 请求接口地址 url: undefined, // 请求的参数 data: {}, // 请求的 header header: "application/json", // 超时时间,单位为毫秒 timeout: undefined, // HTTP 请求方法 method: "GET", // 返回的数据格式 dataType: "json", // 响应的数据类型 responseType: "text", // 开启 http2 enableHttp2: false, // 开启 quic enableQuic: false, // 开启 cache enableCache: false, /** 以上为wx.request的可配置项,参考 https://developers.weixin.qq.com/miniprogram/dev/api/network/request/wx.request.html */ /** 以下为wx.request没有的新增配置项 */ // {String} baseURL` 将自动加在 `url` 前面,可以通过设置一个 `baseURL` 便于传递相对 URL baseUrl: "", // {Function} (同axios的validateStatus)定义对于给定的HTTP 响应状态码是 resolve 或 reject promise 。如果 `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),promise 将被 resolve; 否则,promise 将被 reject validateStatus: undefined, // {Function} 请求参数包裹(类似axios的transformRequest),通过它可统一补充请求参数需要的额外信息(appInfo/pageInfo/场景值...),需return data transformRequest: undefined, // {Function} resolve状态下响应数据包裹(类似axios的transformResponse),通过它可统一处理响应数据,需return res transformResponse: undefined, // {Function} resolve状态包裹,通过它可做接口resolve状态的统一处理 resolveWrap: undefined, // {Function} reject状态包裹,通过它可做接口reject状态的统一处理 rejectWrap: undefined, // {Boolean} _config.useCache 是否开启缓存 useCache: false, // {String} _config.cacheName 缓存唯一key值,默认使用url&data生成 cacheName: undefined, // {Boolean} _config.cacheStorage 是否开启本地缓存 cacheStorage: false, // {Any} _config.cacheLabel 缓存标志,请求前会对比该标志是否变化来决定是否使用缓存,可用useCache替代 cacheLabel: undefined, // {Number} _config.cacheExpireTime 缓存时长,计算缓存过期时间,单位-秒 cacheExpireTime: undefined, }; [代码] 实现 axios.js [代码]import Axios from "./axios.class.js"; // 创建axios实例 const axiosInstance = new Axios(); // 获取基础请求axios const { axios } = axiosInstance; // 将实例的方法bind到基础请求axios上,达到支持请求别名的目的 axios.creat = axiosInstance.creat.bind(axiosInstance); axios.get = axiosInstance.get.bind(axiosInstance); axios.post = axiosInstance.post.bind(axiosInstance); axios.cache = axiosInstance.cache.bind(axiosInstance); axios.cache.storage = axiosInstance.storage.bind(axiosInstance); [代码] Axios class 初始化 defaultConfig 默认配置,即 defaults.js axios.creat 用户配置覆盖默认配置 注意配置初始化后 mergeConfig 不能被污染,config 需通过参数传递 [代码]constructor(config = defaults) { this.defaultConfig = config; } creat(_config = {}) { this.defaultConfig = mergeConfig(this.defaultConfig, _config); } [代码] 请求别名 axios 兼容 axios(config) 或 axios(url[, config]); 别名都只是 config 合并,最终都通过 axios.requst()发起请求; [代码] axios($1 = {}, $2 = {}) { let config = $1; // 兼容axios(url[, config])方式 if (typeof $1 === 'string') { config = $2; config.url = $1; } return this.request(config); } post(url, data = {}, _config = {}) { const config = { ..._config, url, data, method: 'POST', }; return this.request(config); } [代码] 请求方法 _request 请求配置预处理 实现 baseUrl 实现 transformRequest(转换请求数据) [代码] _request(_config = {}) { let config = mergeConfig(this.defaultConfig, _config); const { baseUrl, url, header, data = {}, transformRequest } = config; const computedConfig = { header: { 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', ...header, }, ...(baseUrl && { url: combineUrl(url, baseUrl), }), ...(transformRequest && typeof transformRequest === 'function' && { data: transformRequest(data), }), }; config = mergeConfig(config, computedConfig); return wxRequest(config); } [代码] wx.request 发起请求、处理响应 实现 validateStatus(状态码映射 resolve) 实现 transformResponse(转换响应数据) 实现 resolveWrap、rejectWrap(响应状态处理) [代码]export default function wxRequest(config) { return new Promise((resolve, reject) => { wx.request({ ...config, success(res) { const { resolveWrap, rejectWrap, transformResponse, validateStatus, } = config; if ((validateStatus && validateStatus(res)) || ifSuccess(res)) { const _resolve = resolveWrap ? resolveWrap(res) : res; return resolve( transformResponse ? transformResponse(_resolve) : _resolve ); } return reject(rejectWrap ? rejectWrap(res) : res); }, fail(res) { const { rejectWrap } = config; reject(rejectWrap ? rejectWrap(res) : res); }, }); }); } [代码] 请求缓存的实现 默认使用内存缓存,可配置使用 localStorage 封装了 Storage 与 Buffer 类,与 Map 接口一致:get/set/delete 支持缓存标记&过期时间 缓存唯一 key 值,默认使用 url&data 生成,无需指定 [代码] import Buffer from '../utils/cache/Buffer'; import Storage from '../utils/cache/Storage'; import StorageMap from '../utils/cache/StorageMap'; /** * 请求缓存api,缓存于本地缓存中 */ storage(url, data = {}, _config = {}) { const config = { ..._config, url, data, method: 'POST', cacheStorage: true, }; return this._cache(config); } /** * 请求缓存 * @param {Object} _config 配置 * @param {Boolean} _config.useCache 是否开启缓存 * @param {String} _config.cacheName 缓存唯一key值,默认使用url&data生成 * @param {Boolean} _config.cacheStorage 是否开启本地缓存 * @param {Any} _config.cacheLabel 缓存标志,请求前会对比该标志是否变化来决定是否使用缓存,可用useCache替代 * @param {Number} _config.cacheExpireTime 缓存时长,计算缓存过期时间,单位-秒 */ _cache(_config) { const { url = '', data = {}, useCache = true, cacheName: _cacheName, cacheStorage, cacheLabel, cacheExpireTime, } = _config; const computedCacheName = _cacheName || `${url}#${JSON.stringify(data)}`; const cacheName = StorageMap.getCacheName(computedCacheName); // return buffer if (useCache && Buffer.has(cacheName, cacheLabel)) { return Buffer.get(cacheName); } // return storage if (useCache && cacheStorage) { if (Storage.has(cacheName, cacheLabel)) { const data = Storage.get(cacheName); // storage => buffer Buffer.set( cacheName, Promise.resolve(data), cacheExpireTime, cacheLabel ); return Promise.resolve(data); } } const curPromise = new Promise((resolve, reject) => { const handleFunc = (res) => { // do storage if (useCache && cacheStorage) { Storage.set(cacheName, res, cacheExpireTime, cacheLabel); } return res; }; this._request(_config) .then((res) => { resolve(handleFunc(res)); }) .catch(reject); }); // do buffer Buffer.set(cacheName, curPromise, cacheExpireTime, cacheLabel); return curPromise; } [代码]
2020-07-03 - 不止于小程序,云开发的多场景应用
不止于小程序,云开发的多场景应用——黄映焜 [视频]
2021-09-22 - 服务商的加急审核
为了保证第三方服务商(以下简称“第三方”)的重要核心业务不受审核排队影响,提升提审体验,小程序平台已对优质的第三方开放一定的加急额度。 如何查询加急额度 接口查询: 第三方可通过 微信官方文档-开放平台-第三方平台-代小程序实现业务-加急审核 申请的接口查询当月平台分配的提审限额和剩余可提审次数。 小程序快速查询: 进入【小程序服务商助手】小程序,可直接查询当月平台分配的提审限额和剩余可提审次数。 [图片] 如何操作加急 接口加急: 有加急次数的第三方可以通过 微信官方文档-开放平台-第三方平台-代小程序实现业务-加急审核接口 对已经提审的小程序进行加急操作。 小程序一键加急: 进入【小程序服务商助手】小程序,点击审核专区,对排队中的小程序进行单个或批量一键加急。 [图片] [图片] [图片] 那么,如何获得更多加急额度,加急审核的适用场景有哪些?通过视频,了解更多: [视频]
2021-11-26 - 微信小程序this.setData如何修改对象、数组中的值
在微信小程序的前端开发中,使用this.setData方法修改data中的值,其格式为 this.setData({ '参数名1': 值1, '参数名2': 值2 )} 需要注意的是,如果是简单变量,这里的参数名可以不加引号。 经过测试,可以使用3种方式对data中的对象、数组中的数据进行修改。 假设原数据为: data: { user_info:{ name: 'li', age: 10 }, cars:['nio', 'bmw', 'wolks'] }, 方式一: 使用['字符串'],例如 this.setData({ ['user_info.age']: 20, ['cars[0]']: 'tesla' }) 方式二: 构造变量,重新赋值,例如 var temp = this.data.user_info temp.age = 30 this.setData({ user_info: temp }) var temp = this.data.cars temp[0] = 'volvo' this.setData({ cars: temp }) 方式三: 直接使用字符串,此种方式之前不可以,现在可以了,估计小程序库升级了。 注意和第一种方法的对比,推荐还是使用第一种方法。 this.setData({ 'user_info.age': 40, 'cars[0]': 'ford' }) 完整代码: Page({ /** * 页面的初始数据 */ data: { user_info:{ name: 'li', age: 10 }, cars:['nio', 'bmw', 'wolks'] }, change_data: function(){ console.log('对象-修改前:', this.data.user_info) this.setData({ ['user_info.age']: 20 }) console.log('对象-修改后1:', this.data.user_info) var temp = this.data.user_info temp.age = 30 this.setData({ user_info: temp }) console.log('对象-修改后2:', this.data.user_info) this.setData({ 'user_info.age': 40 }) console.log('对象-修改后3:', this.data.user_info) console.log('数组-修改前:', this.data.cars) this.setData({ ['cars[0]']: 'tesla' }) console.log('数组-修改后1:', this.data.cars) var temp = this.data.cars temp[0] = 'volvo' this.setData({ cars: temp }) console.log('数组-修改后2:', this.data.cars) this.setData({ 'cars[0]': 'ford' }) console.log('数组-修改后3:', this.data.cars) } }) 效果: [图片]
2020-08-26 - 关于虚拟支付的汇总整理
本文场景 在iso端做支付,虚拟支付问题是绕不过去,没有处理好,轻则封禁搜索,重则永久封号,所以对待这个问题不可谓不慎重 本文内容本文主要汇总社区相关的几个经典帖子,特别是带有官方回复的帖子,进行截图,最后做一个总结 截图一 [图片] 截图二 [图片] 截图三 [图片] 截图四 [图片] 参考文章 为什么“小鹅通”的小程序可以做虚拟支付?? - 微信开放社区 https://developers.weixin.qq.com/community/develop/doc/84ef0895eb9e4261b114a88d73dc7621 所谓的小程序IOS不允许虚拟支付到底限制的是谁?? - 微信开放社区 https://developers.weixin.qq.com/community/develop/article/doc/000246265d01201064ea17bc65b813 实名举报手机充值小程序虚拟支付可以正常使用?? - 微信开放社区 https://developers.weixin.qq.com/community/develop/doc/00066aedd70a907b38ea5043856400 跑腿小程序,支付跑腿费,属于虚拟支付吗?可以在ios里进行支付吗?? - 微信开放社区 https://developers.weixin.qq.com/community/develop/doc/0002e8ec24cbd0f29cba2e28156400 虚拟业务指南请收好。? - 微信开放社区 https://developers.weixin.qq.com/community/develop/doc/000cc6c0b383a047c7798e0045b409 本文总结关于虚拟支付的整理 关于一个支付业务到底是不是虚拟支付,在有参考的情况,可以根据官方的 上面社区的锅巴同学整理的非常详细,给出了官方明确定义为非虚拟支付的6种情况 以下内容摘录自锅巴同学的社区回帖内容 小程序在线直播课程,充值加油卡,手机流量等这6种,不算小程序虚拟支付,请参照以下6种情况即可,因为你不符合这6种情况,所以被判定为违规 ① 小程序在线课程直播。用户先买课程,后续在线上安排老师在小程序直播。补充选择教育-在线视频课程类目 ② 线上报名活动,线下培训的类型 ③ 充值加油卡加油,涉及预付卡销售服务,补充商家自营-预付卡销售类目 ④ 充值手机流量,补充IT科技-电信运营商类目 ⑤ 悬赏问答功能,需选择社交红包-社交红包类目,并完成新商户号申请后,再提交代码审核。 ⑥ 微信支付充值积分,签到积分等,积分兑换实物商品,兑换成功后,会直接给用户寄过去。有实际服务存在 最后总结下 你所认为的虚拟支付可能并不是虚拟支付,你所强调的非虚拟支付也有可能被定义为虚拟支付,所以一切以官方口径为依据。
2020-09-07 - 于 Next.js 和云开发 CMS 的内容型网站应用实战开发
作者简介 董沅鑫,云开发 CloudBase 团队研发工程师,侧重于前端工程化、node 服务开发,业余时间出没在 xin-tan.com。 本文目录 [图片] 引言随着腾讯云云开发能力的日渐完善,有经验的工程师已经可以独立完成一个产品的开发和上线。但网上云开发相关的实战文章非常少,很多开发者清楚云开发的能力,但是不清楚如何在现有的开发体系下引入云开发。 [图片] 本文从云开发团队开发者+能力使用者的角度,以云开发官网 (http://cloudbase.net/) 的搭建思路为例,分享云开发结合流行框架与工具的实战经验。 涉及到的知识点有: 云开发:扩展能力(CMS 扩展)静态托管云数据库CloudBase CLI 工具React 框架:Next.jsCI 自动构建总览系统设计图: [图片] 背景介绍随着云开发团队业务的迅猛发展,团队需要一个官网来更直观、更即时地向开发者们展示云开发的相关能力,包括但不限于工具链、SDK、技术文档等。 同时,为了降低开发者的上手成本,积累业界的优秀实战经验,官网也承载着营造社区氛围、聚合重要资料、增强用户黏度的重要任务。 我们最初使用 VuePress 作为静态网站工具,遇到了一些痛点: 问题 1: 每次更新内容,都需要配合 git。运营同学对 git 不熟悉问题 2: 学习资料方面的内容更新过于频繁,“污染”了 git 记录问题 3: 内容和网站代码耦合问题 4: 缺少可视化的内容编辑工具我们使用「CMS 扩展」、「云开发基础能力」、「Next.js」、「CI 工具」,很好地解决了以上问题。在实现网站内容动态化的同时,保证了 SEO,运营同学也可以通过 CMS 对内容进行可视化管理。 安装 CMS进入云开发扩展能力控制台,根据引导,安装 CMS 内容管理系统。 在进行扩展程序配置的时候,有两种账号:管理员账号和运营者账号。管理员账号权限更高,可以创建新的数据集合;而运营者账号只能在已有的数据集合上进行增删改的操作。 [图片] 注意:安装时间有些长,请耐心等待安装成功后,云数据库会自动创建 3 个集合,分别是 [代码]tcb-ext-cms-contents[代码]、[代码]tcb-ext-cms-users[代码]、[代码]tcb-ext-cms-webhooks[代码],存放 CMS 系统配置信息以及内容数据。会自动创建 3 个云函数,分别是 [代码]tcb-ext-cms-api[代码]、[代码]tcb-ext-cms-init[代码]、[代码]tcb-ext-cms-auth[代码],封装了初始化、身份验证以及数据流的相关逻辑。 进入「静态网站托管」,可以看到 CMS 系统的静态文件已经自动部署到tcb-cms/目录下了: [图片] 点击上方的「基础配置」,就可以查看到域名信息。 [图片] 在浏览器中访问对应链接(http://pagecounter-d27cfe-1255463368.tcloudbaseapp.com/tcb-cms/)即可看到 CMS 系统: [图片] 到此为止,无任何开发成本,一个 CMS 内容管理系统就正式上线了~ 使用 CMS 创建动态内容对于动态化的数据内容,我们将其划分为不同的模块。每个内容模块,对应 CMS 系统的一个数据集合。例如「云开发官网」-「社区页」中,推荐好课的内容就是动态的。 [图片] 从图中可以看到,每节课程有着多个属性。而在云数据库中,每节课程就对应一个文档,课程属性就对应文档的字段。字段类型与含义如下: name<string>: 课程名称 time<number>: 课程时间 cover<string>: 课程封面 url<string>: 课程链接 level<0 | 1 | 2>: 课程难度 以管理员身份登录 CMS 系统,在「内容设置页」新建内容。在 CMS 中,支持多种高级数据类型,例如 url、图片、markdown、富文本、标签数组、邮箱、网址等,并对这些类型进行了智能识别和更友好地展示。 注意:CMS 自带图床功能。当数据类型是「图片」时,图片会自动上传到当前云开发环境下的云存储中。图片信息以 [代码]cloud://[代码] 开头的特殊链接,存放在数据集合中。新建内容时,默认情况下,CMS 会自动填充 4 个字段:name、order、createTime、updateTime。可以根据自身需要,对不需要的字段进行删除。 建议:保留 order 字段,它可以被用作数据排序。对运营者来说,数据的 order 的值越大,在 CMS 系统中展示的位置越靠前;对开发者来说,可以根据 order 来进行排序搜索。从而保证了体验和逻辑的一致性。根据字段创建集合后,CMS 系统左侧会看到「推荐好课」。它对应的内容被保存在云数据库的[代码]recommend-course[代码](创建时指定)集合中,它的字段信息保存在云数据库的[代码]tcb-ext-cms-contents[代码](CMS 初始化时创建)集合中。 [图片] 按照设定添加新的课程内容后,再次进入「推荐好课」,如下所示: [图片] 图片、链接等内容,更友好地展示给运营者。 项目搭建按照 Next.js Docs 的指引,创建 Next.js 项目: npm i --save next react react-dom axios 因为我们要将网站部署到「静态托管」上,所以要使用 Next.js 的静态导出功能。[代码]package.json[代码] 中的打包脚本更新为: "scripts": { "dev": "next", "build": "next build && next export", "start": "next start" } 为了快速部署静态网站,以及发布云函数。需要全局安装 [代码]@cloudbase/cli[代码]: npm install -g @cloudbase/cli 安装后,添加两个脚本: [代码]deploy:hosting[代码]: 将 Next.js 的静态导出文件部署到「静态托管」[代码]deploy:function[代码]: 发布项目中的云函数 "scripts": { "deploy:hosting": "npm run build && cloudbase hosting:deploy out -e jhgjj-0ae4a1", "deploy:function": "echo y | cloudbase functions:deploy --force" } 注意:准备两个云环境,防止静态部署时文件覆盖。envId 为 [代码]jhgjj-0ae4a1[代码] 的云环境只用于部署 Next.js 的静态导出文件。envId 为 [代码]pagecounter-d27cfe[代码] 的云环境用来部署 CMS 系统。获取 CMS 内容配合 CloudBase 的 Node 端 SDK-[代码]@cloudbase/node-sdk[代码],我们可以在 Next.js 的 [代码]getStaticProps()[代码] 方法中读取到云数据库中的数据。 为了使逻辑更清晰,我们将获取外部数据的方法统一封装到单独文件中。以获取「推荐课程」为例: // provider.js const cloudbase = require("@cloudbase/node-sdk"); const config = { secretId: "your secretId", // 前往「腾讯云控制台」-「访问密钥」获取 secretKey: "your secretKey", // 前往「腾讯云控制台」-「访问密钥」获取 env: "your envid" // 前往「腾讯云控制台」-「云开发 CloudBase」获取 }; const app = cloudbase.init(config); /** * 获取云数据库数据 */ async function getCourses() { const db = app.database(); const result = await db.collection("recommend-course").get(); if (result.code) { throw new Error( `获取「推荐课程」失败, 错误码是${result.code}: ${result.message}` ); } return result.data.map(item => { if (item.createTime instanceof Date) { item.createTime = item.createTime.toLocaleString(); } if (item.updateTime instanceof Date) { item.updateTime = item.updateTime.toLocaleString(); } item.cover = getBucketUrl(item.cover); // 处理云存储的特殊链接 return item; }); } 前文有讲到,CMS 自带图床功能,拖拽上传的图片会被存储在同一环境下的云存储中,并且获取图片的链接存放在集合中。云存储的链接是以 cloud:// 开头的特殊链接,需要在前端进行识别和特殊处理。 举个 🌰,图片的存储链接是:[代码]cloud://pagecounter-d27cfe.7061-pagecounter-d27cfe-1255463368/uploads/1589990230404.png[代码]。将其转成可访问的 http 链接:[代码]https://7061-pagecounter-d27cfe-1255463368.tcb.qcloud.la/uploads/1589990230404.png[代码]。 转换思路是:识别 envid 后面的信息,将其与tcb.qcloud.la域名重新拼接即可: // provider.js /** * 获取云存储的访问链接 * @param {String} url 云存储的特定url */ function getBucketUrl(url) { if (!url.startsWith("cloud://")) { return url; } const re = /cloud:\/\/.*?\.(.*?)\/(.*)/; const result = re.exec(url); return `https://${result[1]}.tcb.qcloud.la/${result[2]}`; } 注意:云存储的「权限设置」应为:所有用户可读,仅创建者及管理员可写。否则链接无法访问。推荐:除了自带的图床功能,开发者可以根据自身需求使用其他稳定图床服务,例如微博图床。如果使用其他图床,对应字段类型不能设置为「图片」,可以是「字符串」或者「超链接」。目前为止,我们使用 SDK 获取了云数据库数据,剩下要做的就是将其注入到 Next.js 页面组件的 props 上: // pages/index.js const HomePage = ({ courses }) => { return ( // 尽情使用数据吧... ) } export async function getStaticProps() { const { getCourses } = require('./../provider') return { props: { courses: await getCourses() } } } export default HomePage 打开浏览器,进入 http://localhost:3000/ ,可以看到效果如下: [图片] 进入 view-source:http://localhost:3000/ ,可以看到网页的 html 源码中包含了课程数据,解决了 SEO 的问题: [图片] 注意:Next.js 的一些方法是双端都会运行的,但 [代码]getStaticProps()[代码] 只会在 server 端运行自动构建与部署目前为止,开发工作基本结束。执行 [代码]npm run build[代码] 命令,网站静态文件被打包到了 [代码]out/[代码] 目录下: [图片] 执行[代码]npm run deploy:hosting[代码]将[代码]out/[代码] 目录下的文件上传到「静态网站托管」。访问静态网站托管的链接:https://jhgjj-0ae4a1.tcloudbaseapp.com/ ,效果如下: [图片] 借助成熟的 CI 工具,例如 Travis、Circle 等,可以定时触发构建工作。如此一来,内容和开发彻底分离。 在构建发布的时候,需要用到 CloudBase CLI 工具。在 CI 工具中,不再使用[代码]cloudbase login[代码]进行交互式输入登录,而是使用密钥登录:[代码]cloudbase login --apiKeyId $TCB_SECRET_ID --apiKey $TCB_SECRET_KEY[代码] 。 注意:前往 云 API 密钥 获得 [代码]TCB_SECRET_ID[代码] 和 [代码]TCB_SECRET_KEY[代码] 的值在 CI 工具的控制台中,配置[代码]TCB_SECRET_ID[代码] 和 [代码]TCB_SECRET_KEY[代码]。并为[代码]package.json[代码]新添加一个脚本: "scripts": { "login": "echo N | cloudbase login --apiKeyId $TCB_SECRET_ID --apiKey $TCB_SECRET_KEY" } 总结来说,CI 构建的流程是: tcb 密钥登录:[代码]npm run login[代码]获取最新数据,导出静态文件:[代码]npm run build[代码]发布到「静态网站托管」:[代码]npm run deploy:function[代码]如果数据需要紧急修改上线,可以在本地或者 CI 工具控制台,手动触发构建。 最后借助云开发 CMS,可以实现评论系统、预约系统、发布博客等各式各样的内容模板,从而快速搭建网站。在现有开发体系下,合理运用云开发,使得人力成本、开发成本以及运维成本大幅度降低。真正实现前后端一把梭,构成“闭环”。 本文实战仅是抛砖引玉,涉及了云开发能力的一部分,还有更多好玩的东西等待你的探索,比如使用云函数实现 SSR、托管后端服务、图像服务、各端 SDK 等。 探索能力,发散思路,以更低成本开发高可用的 web 服务,云开发绝对是你最好的选择!
2020-09-14 - 民宿小程序
民宿小程序开发系列一 谈谈开发的波折 经过半年的闭关开发,民宿小程序正式对外发布上线,我谈下做这个小程序的感受 第一点:由于我之前对酒店旅游民宿这种产品缺乏相关的业务经验,小程序主体逻辑结构几经改版,依次经过以下几种设计方案 1、酒店=>套餐 2、酒店=>套餐=>房间 3、酒店=>房间=>套餐 最后终于定下来了, 接近半年时间,我的周末是跟合伙人在星巴克度过的,蹭网蹭糖,我喜欢吃星巴克的颗粒包装的糖,撕开就往嘴里抿的那种,到了夏天,特别是午后四五点钟,抬头望天空,虽然阳光很刺眼,但是星巴克的空调不受控制的温度放的很低,每次去都要带上母后给我手工缝制的厚棉袄,才能熬过去这种冷气 第二点,关于支付,我也算摸着石头过河,也是中间做过一次大的迭代优化,这个在我的文章单独写过 小程序支付逻辑梳理? - 微信开放社区 https://developers.weixin.qq.com/community/develop/article/doc/000ae8b299c828acfe6a2607c5b813 1 由于采用了小程序云开发,所以没有一个业务后台,对合伙人来说非常不方便,所以云开发CMS一推出,我便是第一批用户, 现在整个云开发CMS后台都已配置完成,遇到了几个问题,但是总体能满足日常运营需要 云开发内容管理系统CMS使用初体验二? - 微信开放社区 https://developers.weixin.qq.com/community/develop/article/doc/000064ba07cd28cceb6ab908e52013 后面会出该系列的第二篇文章,具体讲解,民宿小程序 数据库业务设计,算是给这段时间的一个总结,因为整个小程序业务非常重,整体都是由我一人完成,没有文档的话,基本就是那种写完代码就忘,每次都要看代码会议的状态,累~~~ 2 3
2020-06-01 - 微信开发者工具 1.02.2003121 RC 更新说明
下载地址Windows 64 、 Windows 32 、 macOS 1、支持 API Mock 新增API Mock功能支持模拟 API 的返回内容,让开发者更方便开发小程序,更多详情可移步至:API Mock文档。 [图片] 2、编辑器支持重命名多个文件编辑器支持在同级目录下同时重命名多个同名文件,方便对 Page/Component 文件进行重命名。 [图片] 3、支持显示灰度中的基础库、下发测试基础库新增显示灰度中的基础库以及基础库支持的客户端版本。 [图片] [图片] 同时新增推送按钮,将选定版本的基础库下发到客户端上,推送结果可以在开发版小程序的调试面板中查看。 [图片] 微信客户端对开发版的小程序打开调试,可以看到测试版基础库的生效时间。 [图片] [图片] 注意: 该功能只能推送到登录到开发者工具的微信号的手机上。 会影响到手机上所有的小程序。4、模拟器支持终止模拟器是工具的主要功能之一,如果小程序/小游戏的业务代码中出现死循环、复杂运算、频繁调用某些 API 的情况下都会导致工具出现卡顿或者 CPU 占用比较高的情况。模拟器新增终止按钮,点击后可以暂时终止模拟器运行,节省系统资源占用。 [图片] 5、打开项目时展示 Loading 状态工具增加开启加载 loading 弹窗,显示加载状态情况。 [图片] 6、CLI/HTTP V2 更新CLI & HTTP 接口升级 v2 版本,在 v2 版本中,旧版命令仍然可以使用,但已废弃并会在未来移除,请使用 v2 命令。v2 版本增加了云开发管理操作支持、优化命令形式、增加细致状态提示、支持长时间命令执行、支持国际化(中英文选择)等。详细文档。 [图片] 7、优化云控制台用户访问统计和监控图表的数据展示支持按照近 7 天、近 30 天以及自定义时间段来筛选 DAU。 [图片] 8、数据库备份回档云开发已自动开启数据库备份,并于每日凌晨自动进行一次数据备份,最长保存 7 天的备份数据。如有需要,开发者可在云控制台上通过新建回档任务将集合回档(还原)至指定时间点。详情。 [图片] 9、优化模拟器工具栏展示工具栏机型及显示比例菜单合并,网络模拟调整到模拟操作下。 [图片] 10、编辑器支持小游戏项目的 API 代码补全在小游戏项目,可以看到为小游戏提供的代码补全(部分 Canvas API 尚未提供)。 [图片]
2020-03-13 - (4)获取用户信息
背景 我们发现大部分小程序都会使用 [代码]wx.getUserInfo[代码] 接口,来获取用户信息。原本设计这个接口时,我们希望开发者在真正需要用户信息的情况下才去调取这个接口,但很多开发者会直接调用这个接口,导致用户在使用小程序的时候产生困扰,归结起来有几点: 开发者在小程序首页直接调用 [代码]wx.getUserInfo[代码] 进行授权,弹框获取用户信息,会使得一部分用户点击“拒绝”按钮。 在开发者没有处理用户拒绝弹框的情况下,用户必须授权头像昵称等信息才能继续使用小程序,会导致某些用户放弃使用该小程序。 用户没有很好的方式重新授权,尽管我们增加了[代码]设置[代码]页面,可以让用户选择重新授权,但很多用户并不知道可以这么操作。 此外,我们发现开发者默认将 [代码]wx.login[代码] 和 [代码]wx.getUserInfo[代码] 绑定使用,这个是由于我们一开始的设计缺陷和实例代码导致的([代码]wx.getUserInfo[代码] 必须通过 [代码]wx.login[代码] 在后台生成 [代码]session_key[代码]后才能调用)。同时,我们收到开发者的反馈,希望用户进入小程序首页便能获取到用户的 [代码]unionId[代码],以便识别到用户是否以前关注了同主体公众号或使用过同主体的App 。 为了解决以上问题,针对获取用户信息我们更新了三个能力: 1.使用组件来获取用户信息 2.若用户满足一定条件,则可以用[代码]wx.login[代码] 获取到的[代码]code[代码]直接换到[代码]unionId[代码] 3.[代码]wx.getUserInfo[代码] 不需要依赖 [代码]wx.login[代码] 就能调用得到数据 获取用户信息组件介绍 [代码][代码] 组件变化: [代码]open-type [代码]属性增加 [代码]getUserInfo[代码] :用户点击时候会触发 [代码]bindgetuserinfo[代码] 事件。 新增事件 [代码]bindgetuserinfo[代码] :当 [代码]open-type[代码]为 [代码]getUserInfo[代码] 时,用户点击会触发。可以从事件返回参数的 [代码]detail[代码] 字段中获取到和 [代码]wx.getUserInfo[代码] 返回参数相同的数据。 示例: [代码]<button open-type="getUserInfo" bindgetuserinfo="userInfoHandler"> Click me button>[代码]和 [代码]wx.getUserInfo[代码] 不同之处在于: 1.API [代码]wx.getUserInfo[代码] 只会弹一次框,用户拒绝授权之后,再次调用将不会弹框; 2.组件 [代码][代码][代码][代码] 由于是用户主动触发,不受弹框次数限制,只要用户没有授权,都会再次弹框。 通过获取用户信息的组件,就可以解决用户再次授权的问题。 直接获取unionId开发者申请 [代码]userinfo[代码] 授权主要为了获取 [代码]unionid[代码],我们鼓励开发者在不骚扰用户的情况下合理获得[代码]unionid[代码],而仅在必要时才向用户弹窗申请使用昵称头像。为此,凡使用“获取用户信息组件”获取用户昵称头像的小程序,在满足以下全部条件时,将可以静默获得 [代码]unionid[代码]: 1.在微信开放平台下存在同主体的App、公众号、小程序。 2.用户关注了某个相同主体公众号,或曾经在某个相同主体App、公众号上进行过微信登录授权。 这样可让其他同主体的App、公众号、小程序的开发者快速获得已有用户的数据。 不依赖登录的用户信息获取某些工具类的轻量小程序不需要登录行为,但是也想获取用户信息,那么就可以在 [代码]wx.getUserInfo[代码] 的时候加一个参数 [代码]withCredentials: false[代码] 直接获取到用户信息,可以少一次网络请求。 这样可以在不给用户弹窗授权的情况下直接展示用户的信息。 最佳实践 1.调用 [代码]wx.login[代码] 获取 [代码]code[代码],然后从微信后端换取到 [代码]session_key[代码],用于解密 [代码]getUserInfo[代码]返回的敏感数据。 2.使用 [代码]wx.getSetting[代码] 获取用户的授权情况 1) 如果用户已经授权,直接调用 API [代码]wx.getUserInfo[代码] 获取用户最新的信息; 2) 用户未授权,在界面中显示一个按钮提示用户登入,当用户点击并授权后就获取到用户的最新信息。 3.获取到用户数据后可以进行展示或者发送给自己的后端。 One More Thing 除了获取用户方案介绍之外,再聊一聊很多初次接触微信小程序的开发者所不容易理解的一些概念: 1.关于OpenId和UnionId [代码]OpenId[代码] 是一个用户对于一个小程序/公众号的标识,开发者可以通过这个标识识别出用户。 [代码]UnionId[代码] 是一个用户对于同主体微信小程序/公众号/APP的标识,开发者需要在微信开放平台下绑定相同账号的主体。开发者可通过[代码]UnionId[代码],实现多个小程序、公众号、甚至APP 之间的数据互通了。 同一个用户的这两个 ID 对于同一个小程序来说是永久不变的,就算用户删了小程序,下次用户进入小程序,开发者依旧可以通过后台的记录标识出来。 2.关于 getUserInfo 和 login 很多开发者会把 [代码]login[代码] 和 [代码]getUserInfo[代码] 捆绑调用当成登录使用,其实 [代码]login[代码] 已经可以完成登录,[代码]getUserInfo[代码] 只是获取额外的用户信息。 在 [代码]login[代码] 获取到 [代码]code[代码] 后,会发送到开发者后端,开发者后端通过接口去微信后端换取到 [代码]openid[代码] 和[代码]sessionKey[代码](现在会将 [代码]unionid[代码] 也一并返回)后,把自定义登录态 [代码]3rd_session[代码]返回给前端,就已经完成登录行为了。而 [代码]login[代码] 行为是静默,不必授权的,用户不会察觉。 [代码]getUserInfo[代码] 只是为了提供更优质的服务而存在,比如展示头像昵称,判断性别,开发者可通过 [代码]unionId[代码] 和其他公众号上已有的用户画像结合来提供历史数据。因此开发者不必在用户刚刚进入小程序的时候就强制要求授权。 可以在官方的文档中看到 [代码]login[代码] 的最佳实践: [图片] Q & A Q1: 为什么 login 的时候不直接返回 openid,而是要用这么复杂的方式来经过后台好几层处理之后才能拿到? A: 为了防止坏人在网络链路上做手脚,所以小程序端请求开发者服务器的的请求都需要二次验证才是可信的。因为我们采取了小程序端只给 [代码]code[代码] ,由服务器端拿着 [代码]code[代码] 和 [代码]AppSecrect[代码] 去微信服务器请求的方式,才会给到开发者对应的[代码]openId[代码] 和用于加解密的 [代码]session_key。[代码] Q2: 既然用户的[代码]openId[代码] 是永远不变的,那么开发者可以使用[代码]openId[代码] 作为用户的登录态么? A: 不行,这是非常危险的行为。因为 [代码]openId[代码] 是不变的,如果有坏人拿着别人的 [代码]openId[代码] 来进行请求,那么就会出现冒充的情况。所以我们建议开发者可以自己在后台生成一个拥有有效期的 [代码]第三方session[代码] 来做登录态,用户每隔一段时间都需要进行更新以保障数据的安全性。 Q3: 是不是用户每次打开小程序都需要重新[代码]login[代码]? A: 不必,可以将登录态存入[代码]storage[代码]中,用户再次登录就可以拿[代码]storage[代码] 里的登录态做正常的业务请求,只有当登录态过期了之后才需要重新[代码]login[代码] 。这样子做一则可以减少用户等待时间,二则可以减少网络带宽。 目前微信的[代码]session_key[代码] 有效期是三天,所以建议开发者设置的登录态有效期要小于这个值。
2018-08-17 - 自制图标和引入iconfont
先看效果图: [图片] 以上的UI并不需要美工,程序员自己就能实现。具体步骤如下: 1、找现成的图标去这里:https://icomoon.io/ [图片] 2、点击Generage Font F[图片] 3、下载font文件,下载后解压成icomoon文件夹,找到/fonts/icomoon.ttf,接下来要用到它; 4、生成 @font-face去这里:https://transfonter.org 第一步Add fonts时,选择/fonts/icomoon.ttf 第二步打开Base64开关; 第三步点击转换; 第四步下载生成的文件; [图片] 5、将在icomoon生成的文件style.css和在transfonter生成的文件stylesheet.css中的内容编辑合并,生成小程序中的一个wxss文件: [图片] 6、在其他wxss里引用该icon.wxss文件: @import "./icon.wxss"; 7、在wxml里的用法: <icon class="ico icon-chevron-right" /> 8、结束。
2020-01-06 - 小程序同层渲染原理剖析
众所周知,小程序当中有一类特殊的内置组件——原生组件,这类组件有别于 WebView 渲染的内置组件,他们是交由原生客户端渲染的。原生组件作为 Webview 的补充,为小程序带来了更丰富的特性和更高的性能,但同时由于脱离 Webview 渲染也给开发者带来了不小的困扰。在小程序引入「同层渲染」之前,原生组件的层级总是最高,不受 [代码]z-index[代码] 属性的控制,无法与 [代码]view[代码]、[代码]image[代码] 等内置组件相互覆盖, [代码]cover-view[代码] 和 [代码]cover-image[代码] 组件的出现一定程度上缓解了覆盖的问题,同时为了让原生组件能被嵌套在 [代码]swiper[代码]、[代码]scroll-view[代码] 等容器内,小程序在过去也推出了一些临时的解决方案。但随着小程序生态的发展,开发者对原生组件的使用场景不断扩大,原生组件的这些问题也日趋显现,为了彻底解决原生组件带来的种种限制,我们对小程序原生组件进行了一次重构,引入了「同层渲染」。 相信已经有不少开发者已经在日常的小程序开发中使用了「同层渲染」的原生组件,那么究竟什么是「同层渲染」?它背后的实现原理是怎样的?它是解决原生组件限制的银弹吗?本文将会为你一一解答这些问题。 什么是「同层渲染」? 首先我们先来了解一下小程序原生组件的渲染原理。我们知道,小程序的内容大多是渲染在 WebView 上的,如果把 WebView 看成单独的一层,那么由系统自带的这些原生组件则位于另一个更高的层级。两个层级是完全独立的,因此无法简单地通过使用 [代码]z-index[代码] 控制原生组件和非原生组件之间的相对层级。正如下图所示,非原生组件位于 WebView 层,而原生组件及 [代码]cover-view[代码] 与 [代码]cover-image[代码] 则位于另一个较高的层级: [图片] 那么「同层渲染」顾名思义则是指通过一定的技术手段把原生组件直接渲染到 WebView 层级上,此时「原生组件层」已经不存在,原生组件此时已被直接挂载到 WebView 节点上。你几乎可以像使用非原生组件一样去使用「同层渲染」的原生组件,比如使用 [代码]view[代码]、[代码]image[代码] 覆盖原生组件、使用 [代码]z-index[代码] 指定原生组件的层级、把原生组件放置在 [代码]scroll-view[代码]、[代码]swiper[代码]、[代码]movable-view[代码] 等容器内,通过 [代码]WXSS[代码] 设置原生组件的样式等等。启用「同层渲染」之后的界面层级如下图所示: [图片] 「同层渲染」原理 你一定也想知道「同层渲染」背后究竟采用了什么技术。只有真正理解了「同层渲染」背后的机制,才能更高效地使用好这项能力。实际上,小程序的同层渲染在 iOS 和 Android 平台下的实现不同,因此下面分成两部分来分别介绍两个平台的实现方案。 iOS 端 小程序在 iOS 端使用 WKWebView 进行渲染的,WKWebView 在内部采用的是分层的方式进行渲染,它会将 WebKit 内核生成的 Compositing Layer(合成层)渲染成 iOS 上的一个 WKCompositingView,这是一个客户端原生的 View,不过可惜的是,内核一般会将多个 DOM 节点渲染到一个 Compositing Layer 上,因此合成层与 DOM 节点之间不存在一对一的映射关系。不过我们发现,当把一个 DOM 节点的 CSS 属性设置为 [代码]overflow: scroll[代码] (低版本需同时设置 [代码]-webkit-overflow-scrolling: touch[代码])之后,WKWebView 会为其生成一个 [代码]WKChildScrollView[代码],与 DOM 节点存在映射关系,这是一个原生的 [代码]UIScrollView[代码] 的子类,也就是说 WebView 里的滚动实际上是由真正的原生滚动组件来承载的。WKWebView 这么做是为了可以让 iOS 上的 WebView 滚动有更流畅的体验。虽说 [代码]WKChildScrollView[代码] 也是原生组件,但 WebKit 内核已经处理了它与其他 DOM 节点之间的层级关系,因此你可以直接使用 WXSS 控制层级而不必担心遮挡的问题。 小程序 iOS 端的「同层渲染」也正是基于 [代码]WKChildScrollView[代码] 实现的,原生组件在 attached 之后会直接挂载到预先创建好的 [代码]WKChildScrollView[代码] 容器下,大致的流程如下: 创建一个 DOM 节点并设置其 CSS 属性为 [代码]overflow: scroll[代码] 且 [代码]-webkit-overflow-scrolling: touch[代码]; 通知客户端查找到该 DOM 节点对应的原生 [代码]WKChildScrollView[代码] 组件; 将原生组件挂载到该 [代码]WKChildScrollView[代码] 节点上作为其子 View。 [图片] 通过上述流程,小程序的原生组件就被插入到 [代码]WKChildScrollView[代码] 了,也即是在 [代码]步骤1[代码] 创建的那个 DOM 节点对应的原生 ScrollView 的子节点。此时,修改这个 DOM 节点的样式属性同样也会应用到原生组件上。因此,「同层渲染」的原生组件与普通的内置组件表现并无二致。 Android 端 小程序在 Android 端采用 chromium 作为 WebView 渲染层,与 iOS 不同的是,Android 端的 WebView 是单独进行渲染而不会在客户端生成类似 iOS 那样的 Compositing View (合成层),经渲染后的 WebView 是一个完整的视图,因此需要采用其他的方案来实现「同层渲染」。经过我们的调研发现,chromium 支持 WebPlugin 机制,WebPlugin 是浏览器内核的一个插件机制,主要用来解析和描述embed 标签。Android 端的同层渲染就是基于 [代码]embed[代码] 标签结合 chromium 内核扩展来实现的。 [图片] Android 端「同层渲染」的大致流程如下: WebView 侧创建一个 [代码]embed[代码] DOM 节点并指定组件类型; chromium 内核会创建一个 [代码]WebPlugin[代码] 实例,并生成一个 [代码]RenderLayer[代码]; Android 客户端初始化一个对应的原生组件; Android 客户端将原生组件的画面绘制到步骤2创建的 [代码]RenderLayer[代码] 所绑定的 [代码]SurfaceTexture[代码] 上; 通知 chromium 内核渲染该 [代码]RenderLayer[代码]; chromium 渲染该 [代码]embed[代码] 节点并上屏。 [图片] 这样就实现了把一个原生组件渲染到 WebView 上,这个流程相当于给 WebView 添加了一个外置的插件,如果你有留意 Chrome 浏览器上的 pdf 预览,会发现实际上它也是基于 [代码]<embed />[代码] 标签实现的。 这种方式可以用于 map、video、canvas、camera 等原生组件的渲染,对于 input 和 textarea,采用的方案是直接对 chromium 的组件进行扩展,来支持一些 WebView 本身不具备的能力。 对比 iOS 端的实现,Android 端的「同层渲染」真正将原生组件视图加到了 WebView 的渲染流程中且 embed 节点是真正的 DOM 节点,理论上可以将任意 WXSS 属性作用在该节点上。Android 端相对来说是更加彻底的「同层渲染」,但相应的重构成本也会更高一些。 「同层渲染」 Tips 通过上文我们已经了解了「同层渲染」在 iOS 和 Android 端的实现原理。Android 端的「同层渲染」是基于 chromium 内核开发的扩展,可以看成是 webview 的一项能力,而 iOS 端则需要在使用过程中稍加注意。以下列出了若干注意事项,可以帮助你避免踩坑: Tips 1. 不是所有情况均会启用「同层渲染」 需要注意的是,原生组件的「同层渲染」能力可能会在特定情况下失效,一方面你需要在开发时稍加注意,另一方面同层渲染失败会触发 [代码]bindrendererror[代码] 事件,可在必要时根据该回调做好 UI 的 fallback。根据我们的统计,目前同层失败率很低,也不需要太过于担心。 对 Android 端来说,如果用户的设备没有微信自研的 [代码]chromium[代码] 内核,则会无法切换至「同层渲染」,此时会在组件初始化阶段触发 [代码]bindrendererror[代码]。而 iOS 端的情况会稍复杂一些:如果在基础库创建同层节点时,节点发生了 WXSS 变化从而引起 WebKit 内核重排,此时可能会出现同层失败的现象。解决方法:应尽量避免在原生组件上频繁修改节点的 WXSS 属性,尤其要尽量避免修改节点的 [代码]position[代码] 属性。如需对原生组件进行变换,强烈推荐使用 [代码]transform[代码] 而非修改节点的 [代码]position[代码] 属性。 Tips 2. iOS 「同层渲染」与 WebView 渲染稍有区别 上文我们已经了解了 iOS 端同层渲染的原理,实际上,WebKit 内核并不感知原生组件的存在,因此并非所有的 WXSS 属性都可以在原生组件上生效。一般来说,定位 (position / margin / padding) 、尺寸 (width / height) 、transform (scale / rotate / translate) 以及层级 (z-index) 相关的属性均可生效,在原生组件外部的属性 (如 shadow、border) 一般也会生效。但如需对组件做裁剪则可能会失败,例如:[代码]border-radius[代码] 属性应用在父节点不会产生圆角效果。 Tips 3. 「同层渲染」的事件机制 启用了「同层渲染」之后的原生组件相比于之前的区别是原生组件上的事件也会冒泡,意味着,一个原生组件或原生组件的子节点上的事件也会冒泡到其父节点上并触发父节点的事件监听,通常可以使用 [代码]catch[代码] 来阻止原生组件的事件冒泡。 Tips 4. 只有子节点才会进入全屏 有别于非同层渲染的原生组件,像 [代码]video[代码] 和 [代码]live-player[代码] 这类组件进入全屏时,只有其子节点会被显示。 [图片] 总结 阅读本文之后,相信你已经对小程序原生组件的「同层渲染」有了更深入的理解。同层渲染不仅解决了原生组件的层级问题,同时也让原生组件有了更丰富的展示和交互的能力。下表列出的原生组件都已经支持了「同层渲染」,其他组件( textarea、camera、webgl 及 input)也会在近期逐步上线。现在你就可以试试用「同层渲染」来优化你的小程序了。 支持同层渲染的原生组件 最低版本 video v2.4.0 map v2.7.0 canvas 2d(新接口) v2.9.0 live-player v2.9.1 live-pusher v2.9.1
2019-11-21 - kbone,十分钟让 Vue 项目同时支持小程序
什么是kbone 微信小程序开发过程中,许多开发者会遇到 小程序 与 Web 端一起的需求,由于 小程序 与 Web 端的运行环境不同,开发者往往需要维护两套类似的代码,这对开发者来说比较耗费力气,并且会出现不同步的情况。 为了解决上述问题,微信小程序推出了同构解决方案 [代码]kbone[代码] 来解决此问题。 那么,[代码]kbone[代码] 要怎么使用呢?这里我们将通过一个 [代码]todo[代码] 的例子来跟大家讲解。 基本结构 首先,我们来看下一个基本的 kbone 项目的目录结构(这里的 [代码]todo[代码] 是基于 [代码]Vue[代码] 的示例,[代码]kbone[代码] 也有 [代码]React[代码],[代码]Preact[代码],[代码]Omi[代码] 等版本,详情可移步 kbone github)。 因为 kbone 是为了解决 小程序 与 Web 端的问题,所以每个目录下的配置都会有两份(小程序 与 Web 端各一份) [图片] 入口 不管是 小程序 端还是 Web 端,都需要入口文件。在 [代码]src/index[代码] 目录下,[代码]main.js[代码] 为 Web 端用主入口,[代码]main.mp.js[代码] 则为 小程序 端用主入口。 当然,Web 端会比 小程序 多一个入口页面,即 [代码]index.html[代码](位于根目录下)。 [图片] 下面两段代码分别是 小程序端 入口与 Web 端入口的代码,可以看到 小程序端的入口代码封装在 [代码]createApp[代码] 函数里面(这里固定即可),内部会比 Web 端多一个创建 [代码]app[代码] 节点的操作,其他的基本就是一致的。 [代码]// 小程序端入口 import Vue from 'vue' import todo from './todo.vue' export default function createApp() { // 创建app节点用于绑定 const container = document.createElement('div') container.id = 'app' document.body.appendChild(container) return new Vue({ el: '#app', render: h => h(todo) }) } [代码] [代码]// web端入口 import Vue from 'vue' import todo from './todo.vue' new Vue({ el: '#app', render: h => h(todo) }) [代码] todo.vue 在上面的入口图可以看到,源码目录中,除了入口文件分开之前,页面文件就是共用的了,这里直接使用 Vue 的写法即可,不用做特殊的适应。 配置 写完代码之后,我们要怎么跑项目呢?这时,配置就派上用场啦。 Web 端配置为正常的 Vue 配置,小程序端配置与 Web 端配置的唯一不同就是需要引入 [代码]mp-webpack-plugin[代码] 插件来将 Vue 组件转化为小程序代码。 [图片] 构建代码 接着,我们需要构建代码,让代码可以运行到各自的运行环境中去。构建完成后,生产代码会位于 dist 目录中。 [代码]// 构建 web 端代码 // 目标代码在 dist/web npm run build // 构建小程序端代码 // 目标代码在 dist/mp npm run mp [代码] 小程序端 的构建会比 Web 端的构建多一个步骤,就是 npm 构建。 进入 [代码]dist/mp[代码] 目录,执行 [代码]npm install[代码] 安装依赖,用开发者工具将 [代码]dist/mp[代码] 目录作为小程序项目导入之后,点击工具栏下的 [代码]构建 npm[代码],即可预览效果。 效果 最后,我们来看一下 todo 的效果。kbone 初体验,done~ todo 代码可到 kbone/demo13 自提。 [图片] 最后 如果你想了解更多 kbone 相关的使用及详情,可移步 kbone github。 如有疑问,可到 Kbone小主页 发帖沟通。
2020-04-22 - 【开箱即用】分享几个好看的波浪动画css效果!
以下代码不一定都是本人原创,很多都是借鉴参考的(模仿是第一生产力嘛),有些已忘记出处了。以下分享给大家,供学习参考!欢迎收藏补充,说不定哪天你就用上了! 一、第一种效果 [图片] [代码]//index.wxml <view class="zr"> <view class='user_box'> <view class='userInfo'> <open-data type="userAvatarUrl"></open-data> </view> <view class='userInfo_name'> <open-data type="userNickName"></open-data> , 欢迎您 </view> </view> <view class="water"> <view class="water-c"> <view class="water-1"> </view> <view class="water-2"> </view> </view> </view> </view> //index.wxss .zr { color: white; background: #4cb4e7; /*#0396FF*/ width: 100%; height: 100px; position: relative; } .water { position: absolute; left: 0; bottom: -10px; height: 30px; width: 100%; z-index: 1; } .water-c { position: relative; } .water-1 { background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjYwMHB4IiBoZWlnaHQ9IjYwcHgiIHZpZXdCb3g9IjAgMCA2MDAgNjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCAzLjQgKDE1NTc1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT53YXRlci0xPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9IuaIkSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9Ii0iIHNrZXRjaDp0eXBlPSJNU0FydGJvYXJkR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMjEuMDAwMDAwLCAtMTMzLjAwMDAwMCkiIGZpbGwtb3BhY2l0eT0iMC4zIiBmaWxsPSIjRkZGRkZGIj4KICAgICAgICAgICAgPGcgaWQ9IndhdGVyLTEiIHNrZXRjaDp0eXBlPSJNU0xheWVyR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyMS4wMDAwMDAsIDEzMy4wMDAwMDApIj4KICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0wLDcuNjk4NTczOTUgTDQuNjcwNzE5NjJlLTE1LDYwIEw2MDAsNjAgTDYwMCw3LjM1MjMwNDYxIEM2MDAsNy4zNTIzMDQ2MSA0MzIuNzIxMDUyLDI0LjEwNjUxMzggMjkwLjQ4NDA0LDcuMzU2NzQxODcgQzE0OC4yNDcwMjcsLTkuMzkzMDMwMDggMCw3LjY5ODU3Mzk1IDAsNy42OTg1NzM5NSBaIiBpZD0iUGF0aC0xIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==") repeat-x; background-size: 600px; -webkit-animation: wave-animation-1 3.5s infinite linear; animation: wave-animation-1 3.5s infinite linear; } .water-2 { top: 5px; background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjYwMHB4IiBoZWlnaHQ9IjYwcHgiIHZpZXdCb3g9IjAgMCA2MDAgNjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCAzLjQgKDE1NTc1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT53YXRlci0yPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9IuaIkSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9Ii0iIHNrZXRjaDp0eXBlPSJNU0FydGJvYXJkR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMjEuMDAwMDAwLCAtMjQ2LjAwMDAwMCkiIGZpbGw9IiNGRkZGRkYiPgogICAgICAgICAgICA8ZyBpZD0id2F0ZXItMiIgc2tldGNoOnR5cGU9Ik1TTGF5ZXJHcm91cCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTIxLjAwMDAwMCwgMjQ2LjAwMDAwMCkiPgogICAgICAgICAgICAgICAgPHBhdGggZD0iTTAsNy42OTg1NzM5NSBMNC42NzA3MTk2MmUtMTUsNjAgTDYwMCw2MCBMNjAwLDcuMzUyMzA0NjEgQzYwMCw3LjM1MjMwNDYxIDQzMi43MjEwNTIsMjQuMTA2NTEzOCAyOTAuNDg0MDQsNy4zNTY3NDE4NyBDMTQ4LjI0NzAyNywtOS4zOTMwMzAwOCAwLDcuNjk4NTczOTUgMCw3LjY5ODU3Mzk1IFoiIGlkPSJQYXRoLTIiIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDMwMC4wMDAwMDAsIDMwLjAwMDAwMCkgc2NhbGUoLTEsIDEpIHRyYW5zbGF0ZSgtMzAwLjAwMDAwMCwgLTMwLjAwMDAwMCkgIj48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==") repeat-x; background-size: 600px; -webkit-animation: wave-animation-2 6s infinite linear; animation: wave-animation-2 6s infinite linear; } .water-1, .water-2 { position: absolute; width: 100%; height: 60px; } .back-white { background: #fff; } @keyframes wave-animation-1 { 0% { background-position: 0 top; } 100% { background-position: 600px top; } } @keyframes wave-animation-2 { 0% { background-position: 0 top; } 100% { background-position: 600px top; } } .user_box { display: flex; z-index: 10000 !important; opacity: 0; /* 透明度*/ animation: love 1.5s ease-in-out; animation-fill-mode: forwards; } .userInfo_name { flex: 1; vertical-align: middle; width: 100%; margin-left: 5%; margin-top: 5%; font-size: 42rpx; } .userInfo { flex: 1; width: 100%; border-radius: 50%; overflow: hidden; max-height: 50px; max-width: 50px; margin-left: 5%; margin-top: 5%; border: 2px solid #fff; } [代码] 二、第二种效果 [图片] [代码]//index.wxml <view class="waveWrapper waveAnimation"> <view class="waveWrapperInner bgTop"> <view class="wave waveTop" style="background-image: url('https://s2.ax1x.com/2019/09/26/um8g7n.png')"></view> </view> <view class="waveWrapperInner bgMiddle"> <view class="wave waveMiddle" style="background-image: url('https://s2.ax1x.com/2019/09/26/umGZ38.png')"></view> </view> <view class="waveWrapperInner bgBottom"> <view class="wave waveBottom" style="background-image: url('https://s2.ax1x.com/2019/09/26/umGuuQ.png')"></view> </view> </view> //index.wxss .waveWrapper { overflow: hidden; position: absolute; left: 0; right: 0; height: 300px; top: 0; margin: auto; } .waveWrapperInner { position: absolute; width: 100%; overflow: hidden; height: 100%; bottom: -1px; background-image: linear-gradient(to top, #86377b 20%, #27273c 80%); } .bgTop { z-index: 15; opacity: 0.5; } .bgMiddle { z-index: 10; opacity: 0.75; } .bgBottom { z-index: 5; } .wave { position: absolute; left: 0; width: 500%; height: 100%; background-repeat: repeat no-repeat; background-position: 0 bottom; transform-origin: center bottom; } .waveTop { background-size: 50% 100px; } .waveAnimation .waveTop { animation: move-wave 3s; -webkit-animation: move-wave 3s; -webkit-animation-delay: 1s; animation-delay: 1s; } .waveMiddle { background-size: 50% 120px; } .waveAnimation .waveMiddle { animation: move_wave 10s linear infinite; } .waveBottom { background-size: 50% 100px; } .waveAnimation .waveBottom { animation: move_wave 15s linear infinite; } @keyframes move_wave { 0% { transform: translateX(0) translateZ(0) scaleY(1) } 50% { transform: translateX(-25%) translateZ(0) scaleY(0.55) } 100% { transform: translateX(-50%) translateZ(0) scaleY(1) } } [代码] 三、第三种效果 [图片] [代码]//index.wxml <view class="container"> <image class="title" src="https://ftp.bmp.ovh/imgs/2019/09/74bada9c4143786a.png"></image> <view class="content"> <view class="hd" style="transform:rotateZ({{angle}}deg);"> <image class="logo" src="https://ftp.bmp.ovh/imgs/2019/09/d31b8fcf19ee48dc.png"></image> <image class="wave" src="wave.png" mode="aspectFill"></image> <image class="wave wave-bg" src="wave.png" mode="aspectFill"></image> </view> <view class="bd" style="height: 100rpx;"> </view> </view> </view> //index.wxss image{ max-width:none; } .container { background: #7acfa6; align-items: stretch; padding: 0; height: 100%; overflow: hidden; } .content{ flex: 1; display: flex; position: relative; z-index: 10; flex-direction: column; align-items: stretch; justify-content: center; width: 100%; height: 100%; padding-bottom: 450rpx; background: -webkit-gradient(linear, left top, left bottom, from(rgba(244,244,244,0)), color-stop(0.1, #f4f4f4), to(#f4f4f4)); opacity: 0; transform: translate3d(0,100%,0); animation: rise 3s cubic-bezier(0.19, 1, 0.22, 1) .25s forwards; } @keyframes rise{ 0% {opacity: 0;transform: translate3d(0,100%,0);} 50% {opacity: 1;} 100% {opacity: 1;transform: translate3d(0,450rpx,0);} } .title{ position: absolute; top: 30rpx; left: 50%; width: 600rpx; height: 200rpx; margin-left: -300rpx; opacity: 0; animation: show 2.5s cubic-bezier(0.19, 1, 0.22, 1) .5s forwards; } @keyframes show{ 0% {opacity: 0;} 100% {opacity: .95;} } .hd { position: absolute; top: 0; left: 50%; width: 1000rpx; margin-left: -500rpx; height: 200rpx; transition: all .35s ease; } .logo { position: absolute; z-index: 2; left: 50%; bottom: 200rpx; width: 160rpx; height: 160rpx; margin-left: -80rpx; border-radius: 160rpx; animation: sway 10s ease-in-out infinite; opacity: .95; } @keyframes sway{ 0% {transform: translate3d(0,20rpx,0) rotate(-15deg); } 17% {transform: translate3d(0,0rpx,0) rotate(25deg); } 34% {transform: translate3d(0,-20rpx,0) rotate(-20deg); } 50% {transform: translate3d(0,-10rpx,0) rotate(15deg); } 67% {transform: translate3d(0,10rpx,0) rotate(-25deg); } 84% {transform: translate3d(0,15rpx,0) rotate(15deg); } 100% {transform: translate3d(0,20rpx,0) rotate(-15deg); } } .wave { position: absolute; z-index: 3; right: 0; bottom: 0; opacity: 0.725; height: 260rpx; width: 2250rpx; animation: wave 10s linear infinite; } .wave-bg { z-index: 1; animation: wave-bg 10.25s linear infinite; } @keyframes wave{ from {transform: translate3d(125rpx,0,0);} to {transform: translate3d(1125rpx,0,0);} } @keyframes wave-bg{ from {transform: translate3d(375rpx,0,0);} to {transform: translate3d(1375rpx,0,0);} } .bd { position: relative; flex: 1; display: flex; flex-direction: column; align-items: stretch; animation: bd-rise 2s cubic-bezier(0.23,1,0.32,1) .75s forwards; opacity: 0; } @keyframes bd-rise{ from {opacity: 0; transform: translate3d(0,60rpx,0); } to {opacity: 1; transform: translate3d(0,0,0); } } [代码] wave.png(可下载到本地) [图片] 在这个基础上,再加上js的代码,即可实现根据手机倾向,水波晃动的效果 wx.onAccelerometerChange(function callback) 监听加速度数据事件。 [图片] [代码]//index.js Page({ onReady: function () { var _this = this; wx.onAccelerometerChange(function (res) { var angle = -(res.x * 30).toFixed(1); if (angle > 14) { angle = 14; } else if (angle < -14) { angle = -14; } if (_this.data.angle !== angle) { _this.setData({ angle: angle }); } }); }, }); [代码] 四、第四种效果 [图片] [代码]//index.wxml <view class='page__bd'> <view class="bg-img padding-tb-xl" style="background-image:url('http://wx4.sinaimg.cn/mw690/006UdlVNgy1g2v2t1ih8jj31hc0p0qej.jpg');background-size:cover;"> <view class="cu-bar"> <view class="content text-bold text-white"> 悦拍屋 </view> </view> </view> <view class="shadow-blur"> <image src="https://raw.githubusercontent.com/weilanwl/ColorUI/master/demo/images/wave.gif" mode="scaleToFill" class="gif-black response" style="height:100rpx;margin-top:-100rpx;"></image> </view> </view> //index.wxss @import "colorui.wxss"; .gif-black { display: block; border: none; mix-blend-mode: screen; } [代码] 本效果需要引入ColorUI组件库
2019-09-26 - 借助实时数据推送快速制作在线对战五子棋小游戏丨实战
1 项目概述 游戏开发,尤其是微信小游戏开发,是最近几年比较热门的话题。 本次「云开发」公开课,将通过实战「在线对战五子棋」,一步步带领大家,在不借助后端的情况下,利用「小程序 ✖ 云开发」,独立完成一款微信小游戏的开发与上线。 2 任务目标 根据项目初始框架,阅读教程的同时,逐步完成棋盘绘制、音乐播放、玩家对战、输赢判定等功能,最终实现一个可以快乐玩耍的在线对战五子棋。 在这个过程中,会了解到 Serverless 的一些概念,并且实际应用它们,比如:云数据库、云存储、云函数、增值能力。除了这些基本功能,还准备了更多的硬核概念与落地实践,比如:实时数据库、聚合搜索、权限控制。 完成开发后,上传并且设置为体验版,欢迎邀请更多人来体验。 3 准备工作 从 TencentCloudBase/tcb-game-gomoku 中下载代码到本地: [代码]git clone https://github.com/TencentCloudBase/cloudbase-examples.git cd /cloudbase-examples/minigame/tcb-demo-gomoku [代码] 切换课程专用的 [代码]minigame-study[代码] 分支: [代码]git checkout minigame-study [代码] 4 实战任务 4.1 创建云开发与小游戏环境 1、打开微信 IDE,点击左侧的小游戏,选择右侧的导入项目,导入之前下载的「在线对战五子棋」的目录,AppID 修改为你已经注册好的小游戏 AppID。 [图片] 2、进入后,点击上方的云开发按钮。如果之前没有开通过云开发,需要开通云开发,新开通的话需要等待 10 ~ 20 分钟。 [图片] 3、进入「云开发/数据库」,创建新的集合,新集合的名称是[代码]rooms[代码]。 [图片] 4、进入「云开发/存储」,点击“上传文件”。上传的内容是[代码]/static/[代码]下的[代码]bgm.mp3[代码] 和 [代码]fall.mp3[代码]。之后的代码中会通过云存储的接口,请求文件的临时 url,这样做的目的是减少用户首次进入游戏加载的静态资源。 [图片] 4.2 准备配置文件 创建配置文件: [代码]cp miniprogram/shared/config.example.js miniprogram/shared/config.js [代码] 将关键字段的信息,换成自己账号的信息即可: [图片] 4.3 创建云开发接口 打开 [代码]miniprogram/shared/cloud.js[代码],在里面初始化云开发能力,并且对外暴露云数据库以及聚合搜索的 API。 [图片] 4.4 获取云存储资源的链接 为了减少用户首屏加载的静态资源,音乐资源并没有放在[代码]miniprogram[代码]目录下,而是放在了云存储中,通过调用云存储的 api 接口,来返回静态资源的临时链接。 在 [代码]miniprogram/modules/music.js[代码]中,会调用资源接口,获取资源链接: [图片] [代码]getTempFileURL[代码]函数属于云开发相关,因此放在了 [代码]miniprogram/shared/cloud.js[代码]中。这里只需要临时链接[代码]tempFileURL[代码]属性,其它返回值直接过滤调即可。 为了方便外面调用,promise 内部不再用 reject 抛错。对于错误异常,返回空字符串。这样,加载失败的资源不会影响正常资源的加载和 Promise.all 中逻辑进行。 [图片] 4.5 游戏进入与身份判断 根据前面的流程图我们可以看到,游戏玩家的身份是分为 owner 与 player。它们的含义如下: owner:玩家进入游戏后,查找是否有空闲房间,如果不存在空闲房间,那么就会主动创建新的空闲房间。那么对于新创建的房间,玩家就是 owner。 player:玩家进入游戏后,查找是否有空闲房间,如果存在空闲房间,那么就加入空闲房间。那么对于空闲房间,玩家就是 player。 判断的依据就是 [代码]judgeIdentity[代码] 方法中,读取云数据库集合中的 rooms 的记录。如果存在多个空闲房间,需要选取创建时间最近的一个房间。因此,这里需要用到「聚合搜索」的逻辑。 聚合搜索的条件,在这里有 3 个: 标记人数的字段,是否为 1 创建时间倒叙排序 只选择 1 个 [图片] 4.6 创建新房间 在上述的身份判断函数逻辑中,如果聚合搜索查询的结果为空,说明没有空闲房间,玩家需要作为 owner 来创建新的房间,等待其它玩家加入。 创建房间的逻辑就是将约定好的字段,放进云数据库的记录中。这些字段有: roomid<[代码]String[代码]>: 6 位房间号,唯一 nextcolor<[代码]"white" | "black"[代码]>: 下一步是白棋/黑棋走 chessmen<[代码]String[代码]>: 编码后的棋盘数据 createTimestamp<[代码]String[代码]>: 记录创建时间戳,精确到 ms people<[代码]Number[代码]>: 房间人数 是的,你可能注意到了,这里需要保证 roomid 是不重复的。因此本地生成的随机 roomid,需要先调用云数据库的查询接口,检测是否存在。如果存在,那么递归调用,重新生成随机字符串。 [图片] 4.7 监听玩家进入 对于 owner 身份来说,除了要创建新房间,还需要在创建后监听 player 身份的玩家进入游戏。 对于 player 身份的玩家进入游戏后,会更新记录中的 people 字段(1 => 2)。这时候就需要利用「实时数据库」的功能,监听远程记录的 people 字段变化。 代码实现上,调用[代码]watch[代码]方法,并且传递[代码]onChange[代码]函数参数。一旦有任何风吹草动,都可以在[代码]onChange[代码]回调函数中获得。对于传递给回调函数的参数,有两个比较重要: docChanges<[代码]Array[代码]>: 数组中的每一项对应每条记录的变化类型,变化类型有 init、update、delete 等。 docs<[代码]Array[代码]>: 数组中的每一项对应每条记录的当前数据。 [图片] 4.8 越权更新字段 对于 player 身份来说,进入房间后,既不需要「创建新房间」,也不需要「监听玩家进入」。但需要更新记录的 people 字段。由于记录是由 owner 身份的玩家创建的,而云数据库只有以下 4 种权限: 所有用户可读,仅创建者可读写 仅创建者可读写 所有用户可读 所有用户不可读写 以上 4 种权限,并没有「所有用户可读写」。因此,对于越权读写的情况,需要通过调用云函数来以“管理员”的权限实现。在 [代码]cloudfunction[代码] 中创建 [代码]updateDoc[代码] 云函数,接收前端传来的 collection、docid、data 字段。对于 data 字段来说,就是数据记录的最新更新数据。 [图片] 在小游戏中,通过[代码]wx.cloud.callFunction[代码]来调用云函数。传入的 data 字段指明被调用的云函数,传入的 data 字段可以在云函数的回调函数的 event 参数中访问到(如上图所示)。 [图片] 4.9 落子更新逻辑 不论对于 player 还是 owner 身份,都需要处理落子的逻辑。落子逻辑中,下面的两种情况是属于无效落子: 点击位置已经有棋子 对方还未落子,目前依然处于等待情况 对于以上两种情况,处理的逻辑分别是: 棋盘状态保存在内部类中,调用落子的函数,会返回是否成功的字段标识 只有监听到远程棋盘更新后,才会打开本地的锁,允许落子;落子后,会重新上锁 [图片] 落子成功后,要在本地判断是否胜利。如果胜利,需要调用退出的逻辑。但无论是否胜利,都要将本地的最新状态更新到云端。 [图片] 4.10 监听远程棋盘更新 不论对于 player 还是 owner 身份的玩家,都需要监听远程棋盘的更新逻辑。当远程棋盘字段更新时,本地根据最新的棋盘状态,重绘整个棋盘。并且进行输赢判定,如果可以判定输赢,则退出游戏;否则,打开本地的锁,玩家可以落子。 因为不同身份均需要监听,因此这一块的监听逻辑可以复用。不同的是,两种身份的监听启动时间不一样。owner 身份需要等待 player 身份玩家进入游戏后才开启棋盘监听;player 身份是更新了 people 字段后,开启棋盘监听。 在监听逻辑中,需要判断远程更新的字段是否是 chessmen,这是通过前面提及的 dataType 来实现的。还徐哟啊判断记录中的 nextcolor 字段是否和本地的 color 一样,来决定是否打开本地的锁。 [图片] 如果上述的两个条件均满足,则执行更新本地棋盘、判定输赢、打开本地锁的逻辑。 [图片] 4.11 游戏结束与退出 每次需要判定输赢的地方,如果可以判定输赢,那么都会走到游戏退出逻辑。退出的逻辑分为 2 个部分,第 1 个是给用户提示,第 2 个是调用云函数清空记录。 第 1 个逻辑中用户提示,需要判定用户胜负状态: [图片] 第 2 个逻辑中清除记录的原因是为了方便调试,对于真正的业务场景,一般不会删除历史数据,方便问题定位。同时,这也是一个越权操作,需要调用云函数来实现。 [图片] 6. 课程完整源码 https://github.com/TencentCloudBase/cloudbase-examples/tree/master/minigame/tcb-demo-gomoku 7. 联系我们 更多云开发使用技巧及 Serverless 行业动态,扫码关注我们~ [图片]
02-19 - 开源微信小程序 BookChat 后端程序 BookStack v2.0 发布,功能类似 GitBook 的文档系统
程序介绍 BookStack,分享知识,共享智慧!知识,因分享,传承久远! BookStack 是基于 Mindoc、使用Go语言的Beego框架开发的功能类似GitBook和看云的在线文档管理系统,实现了文档采集、导入、电子书生成以及版本控制等强大的文档功能,并推出了配套的开源微信小程序 BookChat。 升级日志 [代码]BookStack[代码] 配套微信小程序 [代码]BookChat[代码] 接口实现,累计 [代码]20+[代码] 个API接口 修复删除项目时误删默认封面的bug HTML内容处理,以兼容微信小程序[代码]rich-text[代码]组件对HTML内容的渲染 增加开源书籍和文档收录提交入口,以及收录管理 内容采集增强和优化 书籍在发布的时候,自动把非站内图片自动采集下来 书籍页增加小程序码,提供小程序阅读入口,打通PC端与移动端一体化阅读浏览 增加评论审核与管理功能 横幅管理 支持 [代码]epub[代码] 导入(感谢 @wenfengand的PR) 隐藏附件管理入口(因为不依赖于此管理附件) 管理后台增加根据用户名、昵称、邮箱和角色对用户进行检索和管理的功能 增加[代码]作者[代码]角色,用于控制普通用户创建项目权限,有效控制不良项目对网站资源的占用 增加微信小程序配置项(在 [代码]app.conf[代码] 文件中) 新增微信小程序配置项如下: [代码]# 微信小程序 appid appId="" # 微信小程序appSecret appSecret="" # 是否限制API请求,也就是如果不是上述配置的微信小程序的appId请求的接口,则直接拒绝 limitReferer=false # 是否显示小程序阅读码(需要配置了appScecret才会生效) showWechatCode = false # 比如你将static目录下的所有静态资源都放到了专门的服务器上,那么这个域名就行用来访问你的静态资源的域名。否则建议填写web网站的域名 # 如果您部署了微信小程序,则该值一定要填写 static_domain= [代码] 如果不喜欢现在 BookStack 的目录展现形式,可根据 @cnspray 在 Gitee issues 上的回复 进行修改 程序升级 本次升级,数据库表结构有新增和调整,部署时,务必先执行如下命令升级数据库表 [代码]./BookStack install [代码] 详细 安装部署文档 相关地址 BookStack 官网 书栈网:https://www.bookstack.cn BookStack 开源地址 Gitee(码云)开源: https://gitee.com/truthhun/BookStack GitHub 开源: https://github.com/TruthHun/BookStack BookStack 配套微信小程序 BookChat 开源地址 Gitee(码云)开源:https://gitee.com/truthhun/BookChat GitHub 开源:https://github.com/truthhun/BookChat 配套微信小程序 BookChat 小程序码 [图片] 微信扫码体验一下,相信你一定会喜欢,并且想要给[代码]BookChat[代码]项目一个[代码]Star[代码]
2019-08-13 - 分享一个新的小程序开发框架:wxmp
wxmp 一个 简单,轻量,贴近原生 的微信小程序开发框架,让小程序的开发变得更加便捷。 兼容小程序基础库 v2.0.1+。 功能 单文件组件,告别烦人的 [代码].wxml[代码], [代码].wxss[代码], [代码].js[代码], [代码].json[代码] 计算属性 使用 推荐使用标准模板 standard-wxmp,其中 [代码]script[代码] 使用 ES6,[代码]style[代码] 使用 Stylus,[代码]config[代码] 使用 YAML,[代码]template[代码] 使用 Pug。 为什么会有这个项目 在这个项目之前,我们使用 mpvue 作为开发框架。长期使用以后,发现 mpvue 存在一些问题,其中最严重的问题是不够贴近原生。 小程序有自己的运行模型,这套模型和 Vue 并不相同,使用 Vue 的观念来开发小程序长期来看只会带来困扰。 mpvue 将小程序的接口全部屏蔽,这使得出错以后问题的排查变得十分困难。 我们期望有一个框架,能够 拥有像 Vue 一样便捷的开发体验 尽可能贴近小程序原生,因为如果想开发某个平台上最好的程序,唯一的方法是使用这个平台的原生开发 wxmp 并不复杂,实现也非常简单,但是它可以大幅增加我们的开发效率。 单文件组件 每个页面文件或者组件文件由四部分组成: [代码]template[代码], [代码]style[代码], [代码]config[代码], [代码]script[代码], 分别编译到小程序对应的 [代码]wxml[代码], [代码]wxss[代码], [代码]json[代码], [代码]js[代码]。 如下是一个页面的示例: [代码]<template> .home .home__main _loading _reserve-area </template> <script> definePage({ onLoad() { _showToast("Hello, World") }, }) </script> <config> usingComponents: _loading: components/_loading _reserve-area: components/_reserve-area </config> <style> .home height: 100vh display: flex flex-direction: column .home__main flex: 1 </style> [代码] definePage && defineComponent wxmp 定义了两个全局方法 [代码]definePage[代码] 和 [代码]defineComponent[代码] 用于取代小程序标准的 [代码]Page[代码] 和 [代码]Component[代码] 构造器。 新的构造器在原生的基础上增加了对 [代码]computed[代码] 配置项的支持,同时,[代码]definePage[代码] 中使用 [代码]methods[代码] 属性组织用户自定义方法,[代码]Page[代码] 构造器原生是不支持的。 [代码]definePage({ onLoad() ... ... data: { name: "wxmp", age: 1, }, computed: { info(data) { return `${ data.name } ${ data.age }` }, }, methods: { ... }, }) [代码] [代码]defineComponent({ attached() ... ... properties: { num1: { type: Number, value: 1, }, num2: { type: Number, value: 1, }, }, computed: { total(data) { return data.num1 + data.num2 }, }, // Component 构造器原生支持 methods 属性 methods: { ... }, }) [代码] 计算属性 & this.$set 在 [代码]definePage[代码] 和 [代码]defineComponent[代码] 中可以使用 [代码]computed[代码] 属性指定计算属性,wxmp 内部会追踪计算属性与数据属性之间的关系。 同时,wxmp 提供 [代码]this.$set[代码] 函数用于更新数据,该函数会自动计算计算属性的新值然后调用 [代码]setData[代码]。 注意,定义计算属性时,对数据属性的访问通过参数 [代码]data[代码] 实现。 [代码]definePage({ data: { a: 1, b: 1, }, computed: { c(data) { return data.a + data.b }, }, methods: { dataChange() { this.$set({ a: 2 }) // a 和 c 都会更新 }, }, }) [代码] 使用自定义解析引擎 可以在 Webpack 中配置不同的部分由什么 Loader 进行处理。 [代码]{ loader: "wxmp", options: { script: [{ loader: "babel-loader" }], template: [ { loader: "pug-plain-loader", options: { doctype: null, // 解决小程序中闭合标签不兼容的问题 }, }, ], style: [{ loader: "stylus-loader" }], config: [{ loader: "yaml-loader" }], }, }, [代码] 注意: [代码]script[代码] 必须提供 [代码]config[代码] 必须使用 [代码]yaml-loader[代码] 由于 [代码]pug[代码] 会将 [代码]div(wx:if="{{ 2 > 1 }}")[代码] 转成 [代码]{{ 2 > 1 }}[代码], 无法被小程序识别,因此 wxmp 内部使用正则 [代码]/="{{/g[代码] 将 [代码]="{{[代码] 替换成 [代码]!=\"{{[代码] , 以关闭 pug 转义 语法高亮 Sublime 下载 wxmp.sublime-syntax 文件,放入 Sublime 的 Packages/User 目录中。 打开 Command Palette,输入 [代码]Set Syntax: wxmp[代码] 即可。 VS Code 首先安装 Vue 的插件 [代码]vetur[代码]。 编辑 [代码]setting.json[代码] [代码]"vetur.grammar.customBlocks": { "config": "yaml", "template": "pug", "style": "stylus" }, "files.associations": { "*.wxmp": "vue" }, "vetur.validation.template": false, "vetur.validation.style": false, "vetur.validation.script": false [代码] 运行 [代码]Vetur: Generate grammar from vetur.grammar.customBlocks[代码],然后重启 VS Code。 Authors @cj1128 @clmystes License [图片] Released under the MIT license
2019-07-19 - 从0到1开发一个小程序cli脚手架(一)--创建页面/组件模版篇
github地址:https://github.com/jinxuanzheng01/xdk-cli 原文地址:https://www.yuque.com/docs/share/c6dddfdf-5a18-4024-8604-5e619cb9845d cli工具是什么? 在正文之前先大致描述下什么是cli工具,cli工具英文名command-line interface,也就是命令行交互接口,比较典型的几个case例如,create-react-app,vue-cli,具体可以去百度一下,下面gif是小打卡目前用的一套自动化发布工具🔧 [图片] 可以看到整个发布流程大致是以选择或默认项的形式实现,大致分析下面几步 选择打包形式 开发模式/debug模式/发布模式 设置版本号 填写发布信息 选择环境 是否提交版本commit 是不是非常无脑?是不是再也不用担心线上发错环境了?有了它就算不同项目间,就算一天发n次版本还需要担心什么呢? 当然除了简单的发布功能还,还可以做很多的事情,比如创建page/component模版等一些更多有趣的事情 为了节约版面就不贴图了,具体可以看下仓库 https://github.com/jinxuanzheng01/xdk-cli(目前该工具是从小打卡现有的cli库中抽离的部分功能) 明确痛点 也就是我为什么要做这么一个工具,其实最开始我只是为了解决一个问题,就是在整个发布流程中需要人工去改动/确认发布环境和版本信息,大致可以想象下把线下环境发布到线上的尴尬处境 后续发现从cli角度触发,很多东西都变得简单了,大致列了下: 环境变量切换(线上环境,线下环境) 创建启动模版,包括页面,组件 自动化发布 … 准备工作 本文会以快速创建页面模版文件为例教你怎么快速撸一个属于自己的cli工具 如果觉得自己做比较麻烦,可以clone下我的仓库自己改装下 需要了解的三方库 中间会用到一些第三方库 commander, 一个解析命令行命令和参数工具 inquirer,常用交互式命令行用户界面的集合 chalk,美化你的终端输出样式 fuzzy,字符串模糊匹配的插件,根据输入关键词进行模糊匹配 json-format,json美化/格式化工具 其他的一些小知识:比如path模块,fs模块,大家可以去node官网自行查看:https://nodejs.org/api/ 搭建开发环境 创建一个空文件夹,并且npm初始化, 并且创建一个index.js页面,这个index.js将作为你整个包的入口文件 [代码]npm init -y [代码] 安装上述的三方包,当然也可以后续按需安装,这样更能清楚每个包是做什么的 [代码] npm install @moyuyc/inquirer-autocomplete-prompt commander chalk commander fuzzy inquirer json-format --save [代码] 在package.json里添加bin字段, 将自定义的命令软连到全局环境,同时执行npm link创建链接,这里如果报错{code EACCES,errno:13,…},是因为权限不足,可以尝试sudo npm link [代码] "bin": { "cli-demo": "./index.js" } [代码] 在入口文件,index.js 行首加入一行[代码]#!/usr/bin/env node[代码]指定当前脚本由node.js进行解析 [代码]#!/usr/bin/env node // 指定运行环境 // 输出文本 console.log('Hello World!!!'); [代码] 这时可以在命令行中执行[代码]cli-demo[代码]验收一下成果了 [图片] ok,可以看到当在全局状态下输入自定义命令时,正确运行了入口文件,也就意味着的开发玩具已经搭建完成 Let‘ Go 整理逻辑 以快速创建页面模版文件为例,就需要考虑需要哪些逻辑: 设置页面名称 找到已有模版文件 copy到项目中 修改app.json 识别命令行 在刚才的[代码]Hello World!!![代码]环节,已经可以正确识别cli-demo,但是需要在一个cli工具中集成更多功能,可能需要有不同的执行策略,以git为例:[代码]git clone, git status,git push[代码],所以需要识别不同的命令和参数, 是时候就需要用到[代码]commander[代码]这个第三方包帮助解析命令行参数了,当然你也可以自己撸一个lib,本质上还是方便解析[代码]process.argv[代码] index.js (本质上这个js就是一个路由) [代码]#!/usr/bin/env node const version = require('./package').version; // 版本号 /* = package import -------------------------------------------------------------- */ const program = require('commander'); // 命令行解析 /* = task events -------------------------------------------------------------- */ const createProgramFs = require('./lib/create-program-fs'); // 创建项目文件 /* = config -------------------------------------------------------------- */ // 设置版本号 program.version(version, '-v, --version'); /* = deal receive command -------------------------------------------------------------- */ program .command('create') .description('创建页面或组件') .action((cmd, options) => createProgramFs(cmd)); /* 后续可以根据不同的命令进行不同的处理,可以简单的理解为路由 */ // program // .command('build [cli]') // .description('执行打包构建') // .action((cmd, env) => callback); /* = main entrance -------------------------------------------------------------- */ program.parse(process.argv) [代码] 这时候当键入[代码]cli-demo create[代码]时会自动执行createProgramFs createProgramFs.js [代码]module.exports = function () { console.log('Hi, create-program-fs.js'); }; [代码] 命令行输入 cli-demo create [图片] 可以看到已经成功的开辟出了一块独立的业务模块,后续就只需要依据需求填补相应的内容即可 创建交互命令 收到执行命令,这个时候按第一张图,是需要开始一系列QA(当然你也可以不做交互式,直接配置命令行参数),<br />引入三方包 [代码]inquirer[代码],来指定问题队列 [代码]const question = [ // 选择模式使用 page -> 创建页面 | component -> 创建组件 { type: 'list', name: 'mode', message: '选择想要创建的模版', choices: [ 'page', 'component', ] }, // 设置名称 { type: 'input', name: 'name', message: answer => `设置 ${answer.mode} 名称 (e.g: index):`, }, ]; module.exports = function() { // 问题执行 inquirer.prompt(question).then(answers => { console.log(answers); }); }; [代码] [图片]、 可以看到通过一系列QA交互,实际输出拿到的是一个json对象,第一步已完成 创建模版文件 创建一个存放模版文件的文件夹template,并准备好你希望的模版 [图片] 项目中使用模版文件 为了方便阅读,下面的代码,需要明确下面变量的定义, Config.dir_root = 命令行执行目录 Config.root = cli项目根目录 Config.appRoot = 小程序项目路径 Config.template = 模版目录 这里有两个点,一个是执行路径的问题,另一个是分包的问题,具体如下: 执行路径 这里一定要弄明白**__dirname, process.cwd()**的区别,同时还有一些小程序是自己搭的gulp/webpack,可能小程序项目是在src目录下,一定要分清楚 __dirname: 被执行js文件的绝对路径,一般在index.js执行时缓存起来作为项目的全局路径,比如找到template文件夹就会使用 [代码]${__dirname}/template[代码] process.cwd():当前命令行运行时的工作目录,比如在/Users/xuan/Documents/cli-demo 如果当前项目在src,或其他文件夹里怎么办?可以提供一个给用户项目中的配置文件,类似于gulpfile.js或是webpack.config.js的形式,内容例如(具体可以看git仓库) [代码]module.exports = { // 小程序路径 app: './src', // 模版文件夹 template: './template' }; [代码] 可以看到对象中app属性,可以指定你当前小程序项目的路径 分包 因为小程序的分包机制会导致页面实际路径与在主包的路径不相符,例如: 主包:pages/index/index 分包:pages/main_module/pages/habit_enlist/habit_enlist 解决这个问题一方面是要有页面创建要有一定的规范,统一格式,另一方面需要根据规则解析app.json,<br />上面的主包,分包路径差不多是我目前使用的规范 解析app.json [代码]// 获取app.json function getAppJson() { let appJsonRoot = path.join(Config.appRoot, '/app.json'); try { return require(appJsonRoot); }catch (e) { Log.error(`未找到app.json, 请检查当前文件目录是否正确,path: ${appJsonRoot}`); process.exit(1); // 异常退出 } } // 解析app.json let parseAppJson = () => { // app Json 原文件 let appJson = __Data__.appJson = getAppJson(); // 获取主包页面 appJson.pages.forEach(path => __Data__.appPagesList[getPathSubSting(path)] = ''); // 获取分包,页面列表 appJson.subPackages.forEach(item => { __Data__.appModuleList[getPathSubSting(item.root)] = item.root; item.pages.forEach(path => __Data__.appPagesList[getPathSubSting(path)] = item.root); }); }; // __Data__.appPagesList = 小程序全部页面 // __Data__.appModuleList = 小程序全部分包页面 // item结构 {util_module: 'pages/util_module/'},这么定义结构是为了方便后续取数 [代码] question队列里,增加删选分包的选项 [代码] // 设置page所属module { type: 'autocomplete', name: 'modulePath', message: 'Set page ownership module', choices: [], suggestOnly: false, source(answers, input) { // none 代表放在主包 return Promise.resolve(fuzzy.filter(input, ['none', ...Object.keys(__Data__.appModuleList)]).map(el => el.original)); }, filter(input) { if (input === 'none') { return ''; } return __Data__.appModuleList[input]; }, when(answer) { return answer.mode === 'page'; } } [代码] autocomplete类型本质上是个列表,但是可以进行模糊查询,非常方便,像小打卡有接近30个分包的情况下效果尤为明显 [图片] 有了文件名,有了分包路径,有了可供copy的模版,接下来就很简单了,把模版文件塞进项目就可以了,下面是一串从仓库里copy的代码,利用async/await很方便的写出一维代码,基本上的流程: 获取路径 -> 校验 -> 获取文件信息 -> 复制文件 -> 修改app.json -> 输出结果信息 [代码]async function createPage(name, modulePath = '') { // 获取模版文件路径 let templateRoot = path.join(Config.template, '/page'); if (!Util.checkFileIsExists(templateRoot)) { Log.error(`未找到模版文件, 请检查当前文件目录是否正确,path: ${templateRoot}`); return; } // 获取业务文件夹路径 let page_root = path.join(Config.appRoot, modulePath, '/pages', name); // 查看文件夹是否存在 let isExists = await Util.checkFileIsExists(page_root); if (isExists) { Log.error(`当前页面已存在,请重新确认, path: ` + page_root); return; } // 创建文件夹 await Util.createDir(page_root); // 获取文件列表 let files = await Util.readDir(templateRoot); // 复制文件 await Util.copyFilesArr(templateRoot, `${page_root}/${name}`, files); // 填充app.json await writePageAppJson(name, modulePath); // 成功提示 Log.success(`createPage success, path: ` + page_root); } [代码] 扩展 一个基本的快速创建页面模版的cli工具就这样完成,但是有可能需要更多的一些功能 自定义模版 比如说每个项目的模版都有可能不太一样,很大程度上需要根据项目进行定制,这时候可能就需要前文提到的给用户开放config文件的插槽了 项目中的config: [代码]// xdk.config.js module.exports = { // 小程序路径 app: './', // 模版文件夹 template: './template' }; // create-program-fs.js module.exports = function() { // 校验:当前是否存在配置文件 let customConfPath = `${Config.dir_root}/xdk.config.js`; if (!Util.checkFileIsExists(customConfPath)) { Log.error('当前项目尚未创建xdk.config.js文件'); return; } // 获取用户配置项 let {app, template = ''} = require(customConfPath); // 小程序目录 Config.appRoot = path.resolve(path.join(Config.dir_root, app)); // 模版文件目录(默认使用cli提供的默认模版,当config文件有设置template路径时,使用自定义路径) !!template && (Config.template = path.resolve(path.join(Config.dir_root, template)))); // 问题执行 inquirer.prompt(question).then(answers => { console.log(answers); }); }; [代码] 发布的npm仓库 目前从开发到调试本质上是在本地提供服务,利用npm link提供软连接到全局PATH,<br />其实也可以直接发到npm上,让其他使用的该cli的成员一建安装,比如npm install -g xxxxxxx 教程的话百度,google有很多,作者表示很懒,遇到问题下面留言吧。。 最后 可以看到整个功能逻辑相对于平时写的复杂的业务逻辑来说相对简单,主要是工具库的一些使用方面的东西,中间的难点可能就是node中概念性的一些东西,然而这些多看一下文档基本就可以解决 顺便预告下后续的话可能会更新一些如何利用cli工具做到自动化发布,版本号控制,环境变量切换,自动生成文档等一系列有趣的功能 下文地址: 《从0到1开发一个小程序cli脚手架(二) --版本发布/管理篇》
2019-08-05 - javascript浮点数存储计算
双精度数的存储方法 js中不区分整数和浮点数,统一按双精度浮点数存储(就是c++里的double)。使用64位固定长度 符号位S : 第一位 表示正负,0正 指数位E: 中间的11位 尾数位M:后面的52位 [图片] 公式遵循科学计数法规范,十进制中M∈(0,10) 二进制就(0,2),所以M的整数部分只能是1,可以舍去省下一位。 那么4.5 二进制100.1 科学计数法位 1.001*2^2 则s=0,m=001,e= 2(e的二进制表示下面解释一下)。 指数有正负,E表示的二进制在(0-2^11)之间 因此就就取中间值 1023 为 0 。 1024 则为1,1022为-1。 大数溢出的问题 可以看出来M只有52位,所以一直到2^52 -1 都不会有溢出,即 1.111(.后52个1)*2^52。 然后+1成2^53, 其实M位已经开始溢出了一位0,1.00(.后52个0)*2^53 。 但是因为进位后舍弃的是0,并未影响2^53的准确度。 这时候就比较明朗了,2^53+1 最后的一位1依然被舍弃, 所以 2^53+1= 2^53。 2^53+2 则又正常。 可以得出个规律: (2^53, 2^54)之间的数 每两个精确一个,而且是只有偶数可以精确 (2^54, 2^55)之间的数 每4个精确一个,而且是只有4的倍数可以精确 …… 以2的更高次幂的继续跳 浮点误差 [代码]0.1===0.10000000000000001 //true 0.1===0.1000000000000001 //false [代码] 0.1 转换为二进制后,0.00011001100……本身就是无限循环的。这个例子其实还是受限于M的位数, 2^53 约等于10^16. 所以0.1000000000000001十六位的时候仍然正确判断,十七位就溢出 认为他们相等了。 [代码]0.1+0.2 //0.30000000000000004 [代码] [代码]// 0.1 和 0.2 都转化成二进制后再进行运算 0.00011001100110011001100110011001100110011001100110011010 + 0.0011001100110011001100110011001100110011001100110011010 = 0.0100110011001100110011001100110011001100110011001100111 // 转成十进制正好是 0.30000000000000004 [代码] 上面讲到0.1转为二进制后,0.00011001100……是1100无限循环,而js只能有53位的尾数存储,所以其实存储的0.1 是接近0.1000000000000000055511的数 那么 为什么会出现下面的情况 [代码]0.1 //0.1 [代码] 其实就是第一个例子下讲到的2^53 约等于10^16,所以以十进制表示的时候,最多就16位,多余的部分就舍去。 [代码]0.1.toPrecision(22) //0.1000000000000000055511 [代码]
2019-04-01