个人案例
- 健身之窗
健身之窗
健身之窗扫码体验
- 小技巧!CSS 整块文本溢出省略特性探究
今天的文章很有意思,讲一讲整块文本溢出省略打点的一些有意思的细节。 文本超长打点 我们都知道,到今天(2020/03/06),CSS 提供了两种方式便于我们进行文本超长的打点省略。 感兴趣的小伙伴点击链接,了解详情~ http://github.crmeb.net/u/yi 对于单行文本,使用单行省略: { width: 200px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } [图片] 而对于多行文本的超长省略,使用 [代码]-webkit-line-clamp[代码] 相关属性,兼容性也已经非常好了: { width: 200px; overflow : hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; } [图片] CodePen Demo -- inline-block 实现整块的溢出打点 问题一:超长文本整块省略 基于上述的超长打点省略方案之下,会有一些变化的需求。譬如,我们有如下结构: Sb Coco FEUIUX Designer前端工程师 [图片] 对于上述超出的情况,我们希望对于超出文本长度的整一块 -- 前端工程师,整体被省略。 如果我们直接使用上述的方案,使用如下的 CSS,结果会是这样,并非我们期待的整块省略: .person-card__desc { width: 200px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } [图片] 将 [代码]display: inline[代码] 改为 [代码]display: inline-block[代码] 实现整块省略 这里,如果我们需要实现一整块的省略,只需要将包裹整块标签元素的 [代码]span[代码] 的 [代码]display[代码] 由 [代码]inline[代码] 改为 [代码]inline-block[代码] 即可。 .person-card__desc span { display: inline-block; } [图片] 这样,就可以实现,基于整块的内容的溢出省略了。完整的 Demo,你可以戳这里: CodePen Demo - 整块超长溢出打点省略 问题二:iOS 不支持整块超长溢出打点省略 然而,上述方案并非完美的。经过实测,上述方案在 iOS 和 Safari 下,没能生效,表现为这样: [图片] 查看规范 - CSS Basic User Interface Module Level 3 - text-overflow,究其原因,在于 [代码]text-overflow[代码] 只能对内联元素进行打点省略。(Chrome 对此可能做了一些优化,所以上述非 iOS 和 Safari 的场景是正常的) 所以猜测是因为经过了 [代码]display: inline-block[代码] 的转化后,已经不再是严格意义上的内联元素了。 解决方案,使用多行省略替代单行省略 当然,这里经过试验后,发现还是有解的,我们在开头还提到了一种多行省略的方案,我们将多行省略的代码替换单行省略,只是行数 [代码]-webkit-line-clamp: 2[代码] 改成一行即可 [代码]-webkit-line-clamp: 1[代码]。 .person-card__desc { width: 200px; white-space: normal; overflow : hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; } .person-card__desc span { display: inline-block; } 这样,在 iOS/Safari 下也能完美实现整块的超长打点省略: [图片] CodePen Demo -- iOS 下的整块超长溢出打点省略方案 值得注意的是,在使用 [代码] -webkit-line-clamp[代码] 的方案的时候,一定要配合 [代码]white-space: normal[代码] 允许换行,而不是不换行。这一点,非常重要。 这样,我们就实现了全兼容的整块的超长打点省略了。 当然,[代码] -webkit-line-clamp[代码] 本身也是存在一定的兼容性问题的,实际使用的时候还需要具体去取舍。 最后 好了,本文到此结束,一个简单的 CSS 小技巧,希望对你有帮助 :) 感兴趣的小伙伴点击链接,了解详情~ http://github.crmeb.net/u/yi 作者:chokcoco
2021-03-15 - 登录系统实现
对于前端来说,登录就是把用户信息提交上去,后续就不用前端去担心了。但是做过一个登陆sdk的项目,发现这里边的逻辑不是那么简单。下面是我对登陆的一些理解分享给大家,感兴趣的小伙伴点击链接,了解详情~ http://github.crmeb.net/u/yi session & JWT[代码]http[代码]协议是无状态的,它不能以状态来区分和管理请求和响应。也就是说,如果用户通过账号和密码来进行用户认证后,在下次请求时,用户还需要在再次进行用户认证。因为根据[代码]http[代码]协议,服务端并不知道是哪个用户发起的请求。为了识别当前的用户,服务端与客户端需要约定某个标识表示当前的用户 session为了识别是哪个用户发出的请求,需要在服务端存储一份用户登录的信息,这份登录信息会在响应传递给客户端进行存储,当下次请求的时候客户端会携带登录信息请求服务端,服务端就能够区分请求是哪个用户发起的 下面是示意图: [图片] 在[代码]session[代码]方案中,请求服务端时会携带[代码]session_id[代码],服务端会通过当前的[代码]session_id[代码],去查询数据库当前session是否有效,如果有效后续请求就能够标识当前用户。 如果当前的[代码]session[代码]是无效的或者是不存在的,客户端需要重定向到登录页面,或者提示没有登录 下面是对应的代码: const express = require('express'); const session = require('express-session') const redis = require('redis') const connect = require('connect-redis') const bodyParser = require('body-parser') const app = express(); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })) const RedisStore = connect(session); const client = redis.createClient({ host: '127.0.0.1', port: 6397 }) app.use(session({ store: new RedisStore({ client, }), secret: 'sec_id', resave: false, saveUninitialized: false, cookie: { secure: true, httpOnly: true, maxAge: 1000 * 60 * 10 } })) app.get('/', (req, res) => { sec = req.session; if (sec.user) { res.json({ user: sec.user }) } else { res.redirect('/login') } }) app.post('/login', (req, res) => { const {pwd, name } = req.body; // 这里为了简便,就写简单点 if (pwd === name) { req.session.user = req.body.name; res.json({ message: 'success' }) } }) 当请求[代码]/[代码]接口的时候,会判断当前[代码]session[代码]是否存在。如果存在,就返回对应的信息;如果不存在,则会重定向到[代码]/login[代码]页面。这个页面登录成功以后,就会设置[代码]session[代码] 上面代码中只考虑了单个服务的场景,但是业务中往往是多个服务,服务域名不一样,由于[代码]cookie[代码]不能跨域,所以[代码]session[代码]的共享会存在一定问题 [图片] 例如有上面场景中,用户首先请求服务[代码]Auth Server[代码],然后生成[代码]session[代码]。当用户再次请求服务[代码]feedback Server[代码]时,由于[代码]session[代码]不共享,就导致服务B拿不到登陆态,就需要重新登录。 session的缺点 [代码]session[代码]用于解决鉴权,存在一些缺点: 多集群支持: 当网站采用集群部署的时候,会遇到多台web服务器之间如何做[代码]session[代码]共享的问题。因为[代码]session[代码]是由单个服务创建,处理请求的服务器可能不是创建[代码]session[代码]的服务器,那么该服务器就无法拿到之前放入到session中的登录凭证之类的信息 性能差: 当流量高峰期时,由于每个请求的用户信息都需要存储在数据库中,对资源会是一种负担 低扩展性:当扩容服务端的时候,[代码]session store[代码]也需要扩容。这会占用额外的资源和增加复杂性 JWT 在[代码]session[代码]服务中,服务器需要维护用户的[代码]session[代码]对象,要么前置一个服务,要么每个服务都从存储层中获取[代码]session[代码]信息,请求量大的时候IO压力大。 相比于[代码]session[代码]服务,把用户信息存放在客户端,每次请求的时候随[代码]cookie[代码]或[代码]http[代码]头部渠道发送到服务器上,就可以让服务器变成无状态的存在,从而减轻服务器的压力。 [图片] 相比于浏览器,[代码]Native App[代码]设置[代码]cookie[代码]没有那么容易,所以服务端需要采用另外一种认证方式。在登录后,服务端会根据登录信息生成一个[代码]token[代码]值,后续的请求客户端请求会携带[代码]token[代码]值进行登录校验。 感兴趣的小伙伴点击链接,了解详情~ http://github.crmeb.net/u/yi [代码]jwt[代码]主要由三部分构成: 头部信息([代码]header[代码])、消息体([代码]payload[代码])和签名([代码]signature[代码]) 头信息指定了[代码]JWT[代码]的签名算法 header = { alg: "HS256", type: "JWT" } [代码]HS256[代码]表示使用了 [代码]HMAC-SHA256[代码] 来生成签名 消息体包含了[代码]JWT[代码]的意图: payload = { "loggedInAs": "admin", "iat": 1422779638 } 未签名的令牌由[代码]base64url[代码]编码的头信息和消息体拼接而成,签名则通过私有的[代码]key[代码]计算而成: key = 'your_key' unsignedToken = encodeBase64(header) + "." + encodeBase64(payload) signature = HAMC-SHA256(key, unsignedToken) 最后在未签名的令牌尾部拼接上[代码]base64url[代码]编码的签名就是JWT了: token = encodeBase64(header) + '.' + encodeBase64(payload) + '.' + encodeBase64(signature) 具体实现 首先创建[代码]app.js[代码],用于获取请求参数,还有监听端口等等 // app.js require('dotenv').config(); const express = require('express'); const bodyParser = require('body-parser') const cookieParser = require('cookie-parser'); const router = require('./router'); const app = express(); app.use(bodyParser.json()) app.use(cookieParser); app.use(bodyParser.urlencoded({ extended: true })) router(app); app.listen(3001, () => { console.log('server start') }) [代码]dotenv[代码]主要用于配置环境变量,创建[代码].env[代码]文件,下面是本示例的配置: ACCESS_TOKEN_SECRET=swsh23hjddnns ACCESS_TOKEN_LIFE=1200000 然后注册[代码]login[代码]接口,这个接口提交用户信息到[代码]server[代码],后端会用这些信息生成对应的[代码]token[代码],可以直接返回给客户端或者设置[代码]cookie[代码] // user.js const jwt = require('jsonwebtoken') function login(req, res) { const username = req.body.username; const payload = { username, } const accessToken = jwt.sign(payload, process.env.ACCESS_TOKEN_SECRET, { algorithm: "HS256", expiresIn: process.env.ACCESS_TOKEN_LIFE }) res.cookie('jwt', accessToken, { secure: true, httpOnly: true, }) res.send(); } 当登录成功以后直接设置客户端的[代码]cookie[代码] 下次请求的时候,服务端直接获取用户的[代码]jwt cookie[代码],判断当前[代码]token[代码]是否是有效的: //middleware.js const jwt = require('jsonwebtoken'); exports.verify = function(req, res, next) { const accessToken = req.cookies.jwt; try { jwt.verify(accessToken, process.env.ACCESS_TOKEN_SECRET); next(); } catch (error) { console.log(error); return res.status(401).send(); } } 相对于session的方式,jwt具有以下优势: 扩展性好:在分布式部署场景下,session需要数据共享,而jwt不需要 无状态: 不需要在服务端存储任何状态 jwt也存在一些缺点: 无法废弃: 在签发后,在到期之前会始终有效,无法中途废弃。 性能差: session方案中,cookie需要携带的sessionId是一个很短的字符串。但是由于jwt是无状态的,需要携带一些必要的信息,体积会比较大。 安全性:jwt中的payload是base64编码的,没有加密,因此不能存储敏感数据 续签: 传统的cookie续签方案都是框架自带的,session有效期30分钟,30分钟内如果有访问,有效期被刷新至30分钟。如果要改变jwt的有效时间,就需要签发新的jwt。一种方案是每次请求都更新jwt,这样性能太差了;第二种方案为每个jwt设置过期时间,每次访问刷新jwt的过期时间,就失去了jwt无状态的优势了。 session和jwt的适用场景 适合适用jwt的场景: 有效期短只希望被使用一次 例如在请求服务A的时候,服务A会颁发一个很短过期时间的JWT给浏览器,浏览器可以当前的jwt去请求服务B,服务B则可以通过校验JWT来判断当前用户是否有权操作。 由于jwt具有无法废弃的特性,单点登录和会话管理非常不适合用jwt。 单点登录(SSO) [代码]sso[代码]通常处理的是一个公司的不同应用间的访问登录问题。如企业应用有很多业务子系统,只需要登录一个系统,就可以实现不同子系统间的跳转,而避免了登录操作。 这里举个例子进行说明: 子系统[代码]A[代码]统一到[代码]passport[代码]域名登录,并且在[代码]passport[代码]域名下种上cookie,然后把token加入到url中,重定向到子系统A 回到子系统A后,使用token再次去[代码]passport[代码]验证,如果验证通过返回必要的信息生成系统A的session 当系统A下次请求的时候会当前服务已有[代码]session[代码],不会再去[代码]passport[代码]去权限校验 当访问系统B的时候,由于系统B不存在[代码]session[代码],所以会重定向到[代码]passport[代码]域名,[代码]passport[代码]域名下面已经有cookie了,所以不需要登录,直接把token加入到url中,重定向到子系统B,后续流程和A一样 实现原理 以腾讯为例,腾讯旗下有多个域名,例如: cd.qq.com、tencent.com、jd.cm、music.qq.com 在[代码]cd.qq.com[代码]和[代码]music.qq.com[代码],我们可以设置[代码]cookie[代码]的[代码]domian[代码]为[代码]qq.com[代码]实现[代码]cookie[代码]的共享。 但是如[代码]cd.qq.com[代码]、[代码]tencent.com[代码]二级域名不一致,让所有的域名都能共享一个[代码]cookie[代码]。所以希望有一个通用的服务去承载这个登录服务。例如在腾讯有这样一个域名: [代码]passport.tencent.com[代码]用于专门登录服务的承载。这个时候[代码]cd.qq.com[代码]和[代码]tencent.com[代码]的登录登出都由[代码]sso[代码]([代码]passport.baidu.com[代码])来实现 具体实现 成功登录[代码]SSO[代码]会生成[代码]token[代码]跳转到源页面,此时[代码]SSO[代码]已经有登录状态,但是子系统仍然没有登录态。子系统需要通过[代码]token[代码]设置当前子系统的登录态,并通过当前的[代码]token[代码]请求[代码]passport[代码]服务获取用户的基本信息。 下面主要讲三个部分 [代码]passport[代码]: 登录服务,域名为[代码]passport.com[代码] [代码]system[代码]: 子系统,监听端口[代码]3001[代码]为系统[代码]A[代码],监听端口[代码]3002[代码]为系统[代码]B[代码],域名分别为[代码]a.com[代码]、[代码]b.com[代码] passport服务 [代码]passport[代码]主要有以下几个功能: 统一登录服务获取用户信息校验当前的[代码]token[代码]是否是有效的感兴趣的小伙伴点击链接,了解详情~ http://github.crmeb.net/u/yi 首先实现登录页面的一些逻辑: // passport.js import express from 'express'; import session from 'express-session'; import bodyParser from 'body-parser'; import cookieParser from 'cookie-parser'; import connect from 'connect-redis'; import redis from '../redis'; const app = express(); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); app.use(cookieParser()); app.set('view engine', 'ejs'); app.set('views', `${__dirname}/views`); const RedisStore = connect(session); app.use( session({ store: new RedisStore({ client: redis, }), secret: 'token', resave: false, saveUninitialized: false, cookie: { secure: true, httpOnly: true, maxAge: 1000 * 60 * 10, }, }) ); app.get('/', (req, res) => { const { token } = req.cookies; if (token) { const { from } = req.query; const has_access = await redis.get(token); if (has_access && from) { return res.redirect(`https://${from}?token=${token}`); } // 如果不存在便引导至登录页重新登录 return res.render('index', { query: req.query, }); } return res.render('index', { query: req.query, }); }) app.port('/login', (req, res) => { const { name, pwd, from } = req.body; if (name === pwd) { const token = `${new Date().getTime()}_${ name}`; redis.set(token, name); res.cookie('token', token); if (from) { return res.redirect(`https://${from}?token=${token}`); } } else { console.log('登录失败'); } }) [代码]/[代码]接口首先判断[代码]passport[代码]是否已经有登录成功的[代码]token[代码],如果存在就在去存储中查找当前[代码]token[代码]是否是有效的。如果有效并且参数中携带[代码]from[代码]参数,那么就跳转到原页面并且把生成的[代码]token[代码]值带回到原页面。 下面是[代码]passport[代码]页面的样式: [图片] 登录接口需要做的就是登录成功后设置[代码]passport[代码]域名的[代码]token[代码],然后重定向到之前的页面 子系统实现 import express from 'express'; import axios from 'axios'; import session from 'express-session'; import bodyParser from 'body-parser'; import connect from 'connect-redis'; import cookieParser from 'cookie-parser'; import redisClient from "../redis"; import { argv } from 'yargs'; const app = express(); const RedisStore = connect(session); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); app.use(cookieParser('system')); app.use(session({ store: new RedisStore({ client: redisClient, }), secret: 'system', resave: false, name: 'system_id', saveUninitialized: false, cookie: { httpOnly: true, maxAge: 1000 * 60 * 10 } })) app.get('/', async (req, res) => { const { token } = req.query; const { host } = req.headers; // 如果本站已经存在凭证,便不需要去passport鉴权 if (req.session.user) { return res.send('user success') } // 如果没有本站信息,有没有token,便去passport登录鉴权 if (!token) { return res.redirect(`http://passport.com?from=${host}`) } const {data} = await axios.post('http://127.0.0.1:3000/check',{ token, }) // 验证成功 if (data?.code === 0) { const user = data?.user; req.session.user = user; } else { // 验证失败 return res.redirect(`http://passport.com?from=${host}`) } return res.send('page has token') }) app.listen(argv.port, () => { console.log(argv.port); }) 首先判断当前子系统是否已经登录了,如果当前系统[代码]session[代码]已经存在,就返回[代码]user success[代码]。如果没有登录并且[代码]url[代码]上携带[代码]token[代码]参数,就需要跳转到[代码]passport.com[代码]登录。 如果[代码]token[代码]存在,并且当前子系统没有登录,就需要使用当前页面的[代码]token[代码]去请求[代码]passport[代码]服务,判断这个[代码]token[代码]是否有效的,如果有效就返回相应的信息,并且设置[代码]session[代码]。 这里系统[代码]A[代码]和系统[代码]B[代码]只是监听的接口不同,所以在启动参数中添加变量获取启动端口 passport鉴权服务 app.get('/check', (req, res) => { const { token } = req.query; if (!token) { return res.json({ code: 1 }) } const user = await redis.getAsync(token); if (user) { return res.json({ code: 0, user, }) } else { return res.redirect('passport.com') } }) [代码]check[代码]接口就是判断请求服务的[代码]token[代码]是否是有效的,如果有效就返回对应的用户信息,如果无效就重定向到passport.com重新登录 OAuth [代码]OAuth[代码]协议被广泛应用于第三方授权登录中,借助第三方登录可以让用户规避再次登录的问题。 以[代码]github[代码]授权为例,讲解[代码]OAuth[代码]的授权过程: 访问服务[代码]A[代码],服务[代码]A[代码]没有登录,可以通过[代码]github[代码]第三方登录点击[代码]github[代码],跳转到认证服务器。然后询问是否授权授权完成后,会重定向到服务A的一个路径,并且携带参数[代码]code[代码]服务[代码]A[代码]通过[代码]code[代码]去请求[代码]github[代码],获取到[代码]token[代码]值通过[代码]token[代码]值,再去请求[代码]github[代码]资源服务器获取到你想要的的数据 首先去github-auth申请一个[代码]auth[代码]应用,例如以下: [图片] 执行后会得到对应的[代码]client_id[代码]和[代码]client_secret[代码]。下面是具体的授权代码(启动服务就不写,大同小异): import { AuthorizationCode } from 'simple-oauth2'; const config = { client: { id: 'client_id', secret: 'client_secret' }, auth: { tokenHost: 'https://github.com', tokenPath: '/login/oauth/access_token', authorizePath: '/login/oauth/authorize' } } const client = new AuthorizationCode(config); const authorizationUri = client.authorizeURL({ redirect_uri: 'http://localhost:3000/callback', scope: 'notifications', state: '3(#0/!~' }); app.set('view engine', 'ejs'); app.set('views', `${__dirname}/views`); app.get('/auth', (_, res) => { res.redirect(authorizationUri) }) 上面使用了[代码]simple-oauth2[代码]用于[代码]oauth2[代码]的讲解,当访问[代码]localhost:3000/auth[代码]的时候,服务会自动跳转到[代码]github[代码]的认证地址下面是具体的地址 https://github.com/login/oauth/authorize?response_type=code&client_id=86f4138f17d0c3033ca4&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&scope=notifications&state=3(%230%2F!~ 复制代码 当点击授权后会重定向到[代码]localhost:3000/callback[代码],并且[代码]url[代码]上携带参数[代码]code[代码]。下面是服务端的处理函数 async function getUserInfo(token) { const res = await axios({ method: 'GET', url: 'https://api.github.com/user', headers: { Authorization: `token ${token}` } }) return res.data; } app.get('/callback', async (req, res) => { const { code } = req.query; console.log(code); // 获取token const options = { code, } try { const access = await client.getToken(options); const resp = await getUserInfo(access.token.access_token); return res.status(200).json({ token: access.token, user: resp, }); } catch (error) { } }) 根据[代码]url[代码]上参数[代码]code[代码]获取到[代码]token[代码],然后根据这个[代码]token[代码]去请求[代码]github api[代码]服务,获取到用户信息,通常网站会根据当前获取到的用户信息完成注册、加session等一系列操作。上面代码中,把用户请求数据简单返回给返回给前端,下面是最后返回给前端的数据格式: [图片] 最后就实现了第三方的登录授权 感兴趣的小伙伴点击链接,了解详情~ http://github.crmeb.net/u/yi 作者:B_Cornelius
2021-03-16 - 微信小程序云开发教程-云函数操作数据库-增、查
本小节开始,我们将学习云函数如何操作数据库,现在我们开始学习如何向云数据库中新增一条数据。 [图片] 右边是往云数据库新增一条数据的核心代码。 第一步,我们需要实例化数据库连接; 第二步,把要新增的数据用json对象表示; 第三步,指定要将数据增加到哪个集合,也就是哪个数据表,我们这里要增加到todos集合。 第四步,使用add操作,为data赋值; 第五步,操作成功后,返回值res中包含了数据新增成功后,其在集合中的_id。 是不是很简单呀?下面请根据教学视频进行学习和操作 下面学习如何通过云函数从云数据库中查找单条数据 [图片] 右边是往云数据库查找一条数据的核心代码。 第一步,我们需要实例化数据库连接; 第二步,指定要从哪个集合查找,也就是哪个数据表,我们这里要查找的是todos集合。 第三步,将要查找数据的_id传入doc中; 第四步,使用get操作; 第五步,操作成功后,返回值res.data中包含了一条记录,也就是一个对象,请注意,只是一条。 是不是很简单呀?下面请根据教学视频进行学习和操作 下面我们将学习如何通过云函数从云数据库中查找多条数据 [图片] 右边是往云数据库查找多条数据的核心代码。 第一步,我们需要实例化数据库连接; 第二步,指定要从哪个集合查找,也就是哪个数据表,我们这里要查找的是todos集合。 第三步,将要查询条件组装成一个json对象并传入到where中; 第四步,使用get操作; 第五步,操作成功后,返回值res.data中包含了多条记录,请注意,这是一个数据,该数据中包含多个对象。大家需要注意查找多条数据和单条数据在返回值的区别,所以,我们建议大家都使用查找多条数据方法,如果要查找单条数据,只需要在查询条件的代码里,增加一个_id字段即可。
2020-08-31 - 新能力|云开发 VSCode 插件 Cloudbase Toolkit 正式发布
什么是 Cloudbase Toolkit Tencent CloudBase Toolkit 是云开发的 VS Code(Visual Studio Code)插件。该插件可以让您更好地在本地进行云开发项目开发和代码调试,并且轻松将项目部署到云端。 [图片] Cloudbase Toolkit 将项目创建、函数上传、函数更新、函数本地调试等功能集成在 VSCode 的本地调试环境中,开发者可以通过简单的点击,完成云函数的更新、上传、同步等功能。 和 Cloudbase Cli 相比,Cloudbase Toolkit 能够在 VSCode 中完成各种函数操作,在不影响开发者开发流程,不打断开发者开发节奏的同时,完成项目开发,帮助开发者完成工作任务。 应用场景 Cloudbase Toolkit 可以实现在 VSCode 中完成云开发项目的创建、云函数和静态托管文件的部署等,方便开发者快速完成项目开发,提升工作效率。 通过 Tencent CloudBase Toolkit 插件,您可以: 在本地快速创建云开发项目 从多种模板快速创建云函数 同步云端的云函数列表,并下载函数代码到本地 部署云函数到云端,并进行云端安装依赖 对云函数进行管理,如删除云函数、查看云函数详细信息 增量更新云函数文件 删除云端的云函数文件 部署静态托管文件到云端 关于应用插件进行云函数debug调试可以查看文档 https://docs.cloudbase.net/vscode/debug.html?from=10004 如何使用 Cloudbase Toolkit Cloudbase Toolkit 现已发布至 VSCode 官方插件市场,你可以在 VSCode 中直接搜索 Tencent Cloudbase Toolkit 来进行安装,也可以访问 VSCode 的官方市场页面进行安装和使用。 [图片] 快速开始 Step1 安装 运行 VS Code IDE 并打开插件市场 在搜索框中输入:单击搜索框下方列表中的 Tencent CloudBase Toolkit 插件查看详情并选择【install】。 Step2 创建环境 如果您已经开通了云开发服务,并创建了相关环境,可以跳过此步骤。 登录腾讯云官方账号 打开腾讯云官网,注册腾讯云账号,然后登录账号。如有账号,可直接登录创建环境。 开通云开发 进入云开发主页,授予相关权限开通使用。 创建环境 点击新建环境,输入环境名称,选择按量计费模式,点击立即开通,等待服务开通完成后继续进行下面的操作。 Step3 配置 单击左侧导航栏的图标,打开已安装好的 Tencent CloudBase Toolkit 插件: [图片] 点击登录: [图片] CloudBase Toolkit 提供了两种登录方式,您可以通过腾讯云 - 云开发控制台登录,也可以使用腾讯云访问秘钥登录。 [图片] 登录成功后,在 VS Code 窗口的右下方会有”登录成功“的提示。同时,如果当前项目下没有检测到 cloudbaserc 配置文件,CloudBase Toolkit 会提示您创建云开发项目,或创建配置文件。 创建云开发项目会拉取云端模块创建全新的项目,而创建配置文件只会在当前目录下生成 cloudbaserc 配置文件。 [图片] 随后,你就可以使用 CloudBase Toolkit 进行项目的创建了: [图片] 更多文档 Cloudbase Toolkit 官方文档链接:https://docs.cloudbase.net/vscode/intro.html?from=10004 Cloudbase Toolkit 本地调试函数文档:https://docs.cloudbase.net/vscode/debug.html?from=10004 VSCode 官方市场链接:https://marketplace.visualstudio.com/items?itemName=tencentcloud.cloudbase-toolkit 云开发(CloudBase)是云端一体化的后端云服务,采用 serverless 架构,免去了移动应用构建中繁琐的服务器搭建和运维。同时云开发提供的静态托管、命令行工具(CLI)、Flutter SDK 等能力极大的降低了应用开发的门槛。使用云开发可以快速构建完整的小程序/小游戏、H5、Web、移动 App 等应用。 产品文档:https://cloud.tencent.com/product/tcb 技术文档:https://cloudbase.net 技术交流加Q群:601134960 最新资讯关注微信公众号【腾讯云云开发】
2020-09-14 - 小程序直播从开通到开播全过程——开发篇
本文因为社区编辑器markdown功能暂有问题,格式上比较混乱,大家将就看吧: 目前小程序支持的直播方式有两种,一种是纯原生方案(小程序提供推流拉流服务器,主播端和收播端页面都已提供好,你直接使用即可),一种是自己搭建推流服务器(只是使用小程序端提供的live-pusher和live-player组件而已,里面的直播页面和功能都自己独立开发!),这里说的是第一种方案: 一、准备工作 1、一个已经申请开通和正常使用的实实在在的小程序 PS:如果开通了直播功能,但是没有审核上架成功过,直播间分享出去的二维码点击会提示页面不存在!!!原因很简单,因为你新开发的直播页面正式版的小程序上并没有新加进去,必须要提审上架到正式版才能生效! 二、小程序直播准入门槛 微信小程序直播功能准入要求(官方文档链接>>) 一、类目要求: 1. 小程序开发者为国内非个人主体开发者; 2. 小程序开发者为下述类目品类,类目具体信息可参考《微信小程序开放的服务类目》: 1)电商平台:电商平台 2)商家自营:百货、食品、初级食用农产品、酒/盐、图书报刊/音像/影视/游戏/动漫、汽车/其他交通 工具的配件、服装/鞋/箱包、玩具/母婴用品(不含食品)、家电/数码/手机、美妆/洗护、珠宝/饰品/眼镜 /钟表、运动/户外/乐器、鲜花/园艺/工艺品、家居/家饰/家纺、汽车内饰/外饰、办公/文具、机械/电子 器件、电话卡销售、预付卡销售、宠物/农资、五金/建材/化工/矿产品; 3)教育:培训机构、教育信息服务、学历教育(学校)、驾校培训、教育平台、素质教育、婴幼儿教 育、在线教育、教育装备、出国移民、出国留学、特殊人群教育、在线视频课程; 4)金融业:证券/期货投资咨询、保险; 5)出行与交通:航空、地铁、水运、城市交通卡、打车(网约车)、顺风车(拼车)、出租车、路况、 路桥收费、加油/充电桩、城市共享交通、高速服务、火车、公交、长途客运、停车、代驾、租车; 6)房地产:房地产、物业管理、房地产经营、装修/建材; 7)生活服务:丽人、宠物(非医药类)、宠物医院/兽医、环保回收/废品回收、摄影/扩印、婚庆服务、 搬家公司、百货/超市/便利店、家政、营业性演出票务、生活缴费; 8)IT科技:硬件与设备、基础电信运营商、电信业务代理商、软件服务提供商、多方通信; 9)餐饮:餐饮服务场所/餐饮服务管理企业、点餐平台、外卖平台、点评与推荐、菜谱、餐厅排队; 10)旅游:旅游线路、旅游攻略、旅游退税、酒店服务、公寓/民宿、门票、签证、出境WiFi、景区服 务; 11)汽车:养车/修车、汽车资讯、汽车报价/比价、车展服务、汽车经销商/4S店、汽车厂商、汽车预售 服务; 12)体育:体育场馆服务、体育赛事、体育培训、在线健身 二、运营要求: 1、主体下小程序近半年没有严重违规 2、小程序近90天存在支付行为 以上2个运营条件和类目同时满足的前提下,下面3个条件满足其一即可 3、主体下公众号累计粉丝数大于100 4、主体下小程序近7日dau大于100 5、主体在微信生态内近一年广告投放实际消耗金额大于1w 以上准入要求于 2020 年 02 月 24 日进行公示生效。为营造良好健康的微信生态,腾讯公司有权对《微信 小程序直播功能准入要求》不时予以调整并公布,请予以关注。 腾讯公司 tip:如果你的小程序刚刚满足上面门槛,请T+2后刷新再试试。 三、进入小程序后台直播,创建直播间 [图片] 如果你的小程序满足了第二点。小程序后台会有一个直播的入口(没有的话自己找找原因) 点击进入后->创建直播间 按提示操作(要输入主播人的微信号,对方初次使用要活体检测+实名认证)即可成功创建直播间。(注意点:开播时间最早不能早于当前时间10分钟后) 创建成功后,会有一个开播码。注意这个开播码是给主播用的,主播开播的入口小程序码。主播可以扫码进入直播间开播。 [图片] 四、小程序端开发 完成上面3步算是完成主播端的配置了,接下来是收播端(观看直播的小程序端)的开发了。这个是要小程序开发者完成的。所以下面操作都在小程序开发端完成。下面就简单介绍开发逻辑和顺序,具体的要用到的API和接口都不细说,在后面相关链接里面可以点击官方链接查看!(小程序直播 | 微信开放文档)https://developers.weixin.qq.com/miniprogram/dev/framework/liveplayer/live-player-plugin.html) (1)引入直播插件(直接按官方介绍文档操作) 正常引入后开发者工具会弹出这个窗口,如果不弹出请认真,静下心来按照官方文档检查自己的引入代码: [图片] (2)开发后端(如果你没有小程序端自建直播列表和直播间入口的需求2、3、4都可以跳过,届时你的小程序直播间可以用分享方式进入) 后端目前官方只提供了2个接口。一个是获取直播间列表,一个是获取直播间直播完后的相关回放信息,其中第一个接口必须先完成。就是获取到直播间列表,列表里面有带返回直播间的roomid,小程序端必须需要接收到这方面的返回才能接下来的开发。 (3)进入直播页面 引入直播插件后并对接第二步的后端接口后,你可以直接编码进入直播页面了。像进入普通页面一样,可以通过wxml里面的navigator url="xxxx"的方式和js里的wx.navigateTo跳转页面代码进入直播页面。但是他这个url比较特殊,是下面这样的格式: url: `plugin-private://${provider}/pages/live-player-plugin?room_id=${roomId}&custom_params=${encodeURIComponent(JSON.stringify(customParams))}` provider:插件appid(1)小步里面获取到的 rommId:直播间id(2)小步里面获取列表后里面的roomId customParams:自定义的进入页面参数。(根据需要自己定义的传入直播间收播页面的参数) 进入直播间收播页面后的开发量为0,因为这个是由直播间插件接管并完成相关功能。 (4)几个注意点: 4.1、后端获取直播间列表接口几个跟官方文档介绍不一致的地方 [图片] 4.2、 livePlayer.getLiveStatus获取直播间状态这个API官方介绍:首次获取立马返回直播状态,往后间隔1分钟或更慢的频率去轮询获取直播状态。实际使用过程中建议也这么干,如果需要轮询直播间状态,建议间隔时间1分钟以上,如果少于这个值,基本上就是卡在这里后面的代码都不执行了。还有,有时候即使超过1分钟后再轮询,也会偶发性出现获取不到卡住的情况。解决方法,大家可以看看开发者工具里面的本地Storage相关的值,然后后面怎么做你懂的。。 4.3订阅组件subscribe的样式问题。不多说,你懂的,你加上去就能看到效果 4.4后端接口每日调用次数限制的问题。要做好相关缓存到本地的架构设计。 4.5运营上一定要注意,按要求直播。别整那些没用的,很容易被禁播的。 (5)回放功能开发 1.0.4版本后支持0开发的回放功能了。参考后面新增的专门介绍回放功能的使用教程。 五、跑路 这里的跑路是指代码写累了,带上口罩和吉娃娃去公园跑一圈路回来继续码。 最新:1.0.4版本后的回放功能说明,回放功能是这样的 1、后台开启该直播间的回放功能 [图片] 2、收播端还是原来的直播入口进行回放,小程序端是 plugin-private://${liveplayId}/pages/live-player-plugin?room_id=${roomId}&custom_params=${encodeURIComponent(JSON.stringify(customParams))}` 这里的页面链接,链接到回放页面。获取分享方式,分享出去的直播页面,点击后进入回放。 [图片] 还有一个口,点击原来的分享链接后的直播完成页面,也有一个查看回放的入口,如上图。 Tip:如果刚刚直播完可能需要稍等生成回放视频后再次进入相关页面才能看到回放。 相关链接: 小程序直播 | 微信开放文档(开发必看,而且要熟读,基本有所有你要的开发资料) https://developers.weixin.qq.com/miniprogram/dev/framework/liveplayer/live-player-plugin.html 微信小程序直播功能准入要求 | 微信开放文档 https://developers.weixin.qq.com/miniprogram/product/live/access-requirement.html “小程序直播”接入指引 | 微信开放社区 https://developers.weixin.qq.com/community/develop/doc/0008ce654c4450244c1a7e5de5b801?highLine=%25E7%259B%25B4%25E6%2592%25AD%2520%25E6%25B1%25BD%25E8%25BD%25A6 相关知识科普: 小程序直播单日直播上限是50场,同时直播上限50场,单场的直播时长上限是12小时。
2020-06-23 - Node Koa2 mysql搭建后端服务器
前言 前段时间公司要开发自己的一套博客系统,网上找了很多源码,看了下都比较复杂,因为是公司内自己的技术博客,不需要很复杂,索性自己搭一套后端服务,找一套博客前端模版即可实现,话不多说开始搭建历程吧。 开发环境 node.js v8.0.0+ mysql ^2.1.0 koa ^2.7.0 redis ^3.0.2 sequelize ^5.21.5 准备工作 项目中用到了es6 es7的一些语法 比如promise,async await ,还有熟悉mysql sequlize的一些语法 安装 mysql 到官网 https://www.mysql.com/downloads 下载对应版本,并安装数据库 安装 Sequelize 安装sequlize npm install sequelize --save 安装 mysql2 npm install mysql2 --save 下载redis 具体操作不一一说明了,网上有教程 https://www.jianshu.com/p/bb7c19c5fc47 目录结构 先上图 [图片] app/controllers 控制器处理业务逻辑 app/models 逻辑实现的方法 app/schema 定义sequlize模型,也就是表模型 bin/www 项目启动文件 config/env 不同开发环境配置文件 config/config 读取config/env配置文件 config/constants 定义一些枚举 config/redis redis配置文件 config/secretKeys session redis一些密钥 middleware 项目所需中间件 models sequlize 初始化连接池文件 public 项目静态文件如 js css等 routers 路由文件 utils 公共工具包或方法 views 视图页面 .eslintrc.js eslint 检查文件 app.js 入口文件 使用 Sequelize 初始化连接池 [代码]'use strict'; const fs = require('fs'); const path = require('path'); const Sequelize = require('sequelize'); const basename = path.basename(__filename); const config = require('../config/config'); const db = {}; let sequelize; sequelize = new Sequelize(config.db.database, config.db.username, config.db.password, config.db); sequelize.authenticate().then(()=> { console.log('db success') }).catch(() => { console.log('db error') }) sequelize.sync({alter: true}) db.sequelize = sequelize; db.Sequelize = Sequelize; module.exports = db; [代码] 配置 config config 通过config 可以读取不同环境的env文件 [代码]/** * @description 配置文件 * @author Tony */ process.env.NODE_CONFIG_DIR = __dirname + '/env'; const config = require('config'); module.exports = config [代码] 配置redis [代码]/** * @description 连接redis的方法 * @author Tony */ const redis = require('redis') const config = require('./config') const redisClient = redis.createClient(config.redisStore); redisClient.auth(config.redisStore.pass, function() { console.log('Redis client connected'); }); redisClient.on("error", function(err) { console.log("Error " + err); }); /** * redis 操作插入 * @param {*} key * @param {*} val * @param {*} timeout */ function set(key,val,timeout = 60*60) { if(typeof val === 'object') { val = JSON.stringify(val) } redisClient.set(key,val) redisClient.expire(key,timeout) } /** * redis 查找 * @param {*} key */ function get(key) { const promise = new Promise((resolve,reject)=> { redisClient.get(key,(err,val)=> { if(err) { reject(err) return } if(val == null) { resolve(null) return } try { resolve(JSON.parse(val)) } catch (error) { resolve(val) } }) }) return promise } module.exports = { redisClient, set, get } [代码] 配置密钥文件 [代码]/** * @description 密钥常量 * @author tony */ module.exports = { CRYPTO_SECRET_KEY: 'xxxxxxxx', SESSION_SECRET_KEY: 'xxxxxxxxx' } [代码] 上面都是配置文件,下面开始真正的开发啦 1. 定义数据表模型 [代码]/** * 定义user schema */ module.exports = function(sequelize, DataTypes) { return sequelize.define("user", { userName: { type: DataTypes.STRING, allowNull: false, comment: '用户名' }, password: { type: DataTypes.STRING, allowNull: false, comment: '密码' }, nickName: { type: DataTypes.STRING, allowNull: true, comment: '昵称' } { freezeTableName: true } ); }; [代码] 2. 定义models逻辑用到的方法 [代码]const db = require('../../models/index') const Sequelize = db.sequelize const Op = db.Sequelize.Op const User = Sequelize.import('../schema/user') const { formatUser } = require('./_format') User.sync({ force: false }) class UserModel{ /** * 获取用户信息 * @param {*} userName * @param {*} password */ static async getUerInfo(userName,password){ const whereOpt = { userName } if(password) { Object.assign(whereOpt,{ password }) } const result = await User.findOne({ where: whereOpt, attributes: ['id','userName','nickName','picture','city'] }) if(result == null) { return result } const formatRes = formatUser(result.dataValues) return formatRes } /** * 创建用户 * @param {*} data */ static async createUser(data){ return await User.create(data) } } module.exports = UserModel [代码] 3. 定义控制器 [代码]/** * @description 用户逻辑处理 * @author Tony */ const userModel = require('../models/user') const { SuccessModel,ErrorModel} = require('../../utils/ResModel') const doCrypto = require('../../utils/cryp') class UserControler { /** * 判断用户是否存在 * @param {*} ctx */ static async isExist(ctx){ try { const { userName } = ctx.request.body const userInfo = await userModel.getUerInfo(userName) if(userInfo) { ctx.body = new SuccessModel(userInfo) } else { ctx.body = new ErrorModel({ errno: 10003, message: '用户名已存在' }) } } catch (error) { return Promise.reject(error) } } /** * 注册 * @param {*} ctx */ static async register(ctx) { try { const { userName,nickName,password,gender } = ctx.request.body const userInfo = await userModel.getUerInfo(userName) if(userInfo) { ctx.body = new ErrorModel({ errno: 10003, message: '用户名已存在' }) } const result = await userModel.createUser({ userName, nickName, password : doCrypto(password), gender }) ctx.body = new SuccessModel(result) } catch (error) { ctx.body = new ErrorModel({ errno: 10000, message: '注册失败' }) } } /** * 登录 * @param { } ctx */ static async login(ctx) { const { userName,password } = ctx.request.body const userInfo = await userModel.getUerInfo(userName,doCrypto(password)) if(!userInfo) { ctx.body = new ErrorModel({ errno: 10004, message: '用户名或密码不存在' }) } if(ctx.session.userInfo == null) { ctx.session.userInfo = userInfo } ctx.body = new SuccessModel(userInfo) } } module.exports = UserControler [代码] 4. 定义路由 [代码]/** * @description user API 路由 * @author Tony */ const router = require('koa-router')() const UserController = require('../../app/controllers/user') const userValidate = require('../../utils/validator/user') const { genValidator } = require('../../middleware/validator') router.prefix('/api/user') // 用户名是否存在 router.post('/isExist',UserController.isExist) router.post('/register',genValidator(userValidate),UserController.register) //登录 router.post('/login',UserController.login) //返回router module.exports = router [代码] 入口文件 [代码]const Koa = require('koa') const app = new Koa() const views = require('koa-views') const json = require('koa-json') const onerror = require('koa-onerror') const bodyparser = require('koa-bodyparser') const logger = require('koa-logger') const session = require("koa-generic-session") const redisStore = require('koa-redis') const jwtKoa = require('koa-jwt') const config = require('./config/config') const { SECRET } = require('./config/constants') const { SESSION_SECRET_KEY } = require('./config/secretKeys') // 错误处理 onerror(app) // 中间件 app.use(bodyparser({ enableTypes:['json', 'form', 'text'] })) app.use(json()) app.use(logger()) app.use(require('koa-static')(__dirname + '/public')) app.use(views(__dirname + '/views', { extension: 'ejs' })) // session 配置 app.keys = [ SESSION_SECRET_KEY ] app.use(session({ key: 'koa.sid', // cookie name 默认koa.sid prefix: 'koa:sess', // redis key 前缀 默认 koa:sess cookie: { path: '/', httpOnly: true, maxAge: 24 * 60 * 60 * 1000 // 单位 ms }, store: redisStore({ all: `${config.redisStore.host}:${config.redisStore.port}` }) })) // logger app.use(async (ctx, next) => { const start = new Date() await next() const ms = new Date() - start console.log(`${ctx.method} ${ctx.url} - ${ms}ms`) }) const userViewRouter = require('./routes/view/user') const userAPIRouter = require('./routes/api/user') app.use(userViewRouter.routes(), userViewRouter.allowedMethods()) app.use(userAPIRouter.routes(), userAPIRouter.allowedMethods()) // error-handling app.on('error', (err, ctx) => { console.error('server error', err, ctx) }); module.exports = app [代码] 项目启动 npm run start 项目编译 npm run build 接口测试 启动项目后可以通过postman等一些测试工具测试 项目部署 项目可通过nginx pm2等进行部署,具体操作可网上查询,今天先分享到这,一些细节代码没有贴出来,还请见谅。
2020-04-28 - 微信小程序码获取-从频繁失败到成功率100%
早期实现方案 1. 方案实现 通过微信的appSecret获取小程序accessToken并缓存 微信小程序上很多操作都需要使用accessToken,比如用户授权手机号,当然也包括获取小程序码 通过微信提供的api获取到对应的小程序码,由于http接口直接返回的是图片本身,所以考虑将图片上传七牛服务器并获取图片链接,最后使用图片的链接来展示或保存小程序码 2. 方案优点 由于上传了小程序码,对于一些跳转固定页面和参数的码可以将图片链接存到数据库,以供用户下次分享使用,无需重复获取 3. 存在的问题 稳定性很差,获取小程序码的失败率比较高,甚至会出现一个时间段内完全获取不到码的情况 接口效率不好,由于每次都会存在图片上传,而且上传本身又比较耗时,导致服务器压力巨大且频繁出现慢接口,可能会影响到项目中的其他服务 改造后方案 1. 方案实现 获取小程序码后不再上传七牛,直接通过图片流的方式返回给前端 2. 方案优点 取消了图片的上传操作,接口效率大幅提升,提高了小程序码的获取成功率,也减轻了服务的压力 3. 存在的问题 依旧存在小程序码获取失败的情况 4. 问题排查 经排查日志发现是accessToken失效导致,缓存的accessToken失效时间远比微信规定的失效时间短,那究竟又是什么情况会导致accessToken失效呢?经讨论和实验发现以下三点: 我们微信的appSecret授权给第三方网站使用(比如阿拉丁),他们也有获取小程序码的服务,运营可以通过阿拉丁获取小程序码,这就会导致阿拉丁使用我们的appSecret获取accessToken,以至于我们缓存中的accessToken失效。 后端缓存中的accessToken存入和获取的逻辑存在缺陷,每当从缓存读取accessToken时,若缓存不命中,则通过微信api获取新的accessToken然后再存入缓存,这个逻辑容易导致缓存穿透,即当多个请求都没有命中缓存时,只有一个线程能通过微信api拿到新的accessToken,其他线程都拿不到。 当一个accessToken存在时间比较长时,手动调用微信api获取小程序码,会看到微信的api也会存在概率获取不到码的情况,但是一个全新生成的accessToken则不会有这种情况,至少在10分钟之内非常稳定。 最终的方案 1. 方案实现 通知运营不要再使用阿拉丁的生成小程序码的功能,若有这方面需求可以找技术帮忙获取。 缓存中的accessToken有效时间缩短至5分钟,保证每次使用的accessToken都能稳定获取小程序码。 修改accessToken的获取机制,由定时器来获取accessToken并更新缓存,定时器每4分钟执行一次,以确保每个请求都能命中缓存,若定时器出现异常,则回退之前的逻辑(请求没有命中缓存,通过微信api重新获取accessToken)。 最终效果 这一个方案上线后,线上再也没有出现小程序码没有获取成功的情况,观察日志也没再出现获取失败的情况,目前已经两周保持100%成功率了。 [图片]
2019-06-04 - 小肥羊社区帖子精华整理
前几天有偿征集了两个方案,现在把代码公布下,这个问题非常考验思维逻辑能力 问题描述 目前有一个数组,长度为1000,如何把这么大的数据量,渲染到swiper中,保证swiper左右滑动时,丝滑般流畅 使用场景 一般会用于在线答题小程序答题过程中,有时候题库大,顺序答题会存在这个问题,所以说需求来自真实项目场景。 1、 https://developers.weixin.qq.com/s/3dsCzam176gk 2、 https://developers.weixin.qq.com/s/DUzMc6mw73g6 第二个问题:解决wx.previewImage不能对应显示(总显示第一张) wx.preview索引问题 https://developers.weixin.qq.com/community/develop/article/doc/000004d972c3a8b33369dda8d51813 https://developers.weixin.qq.com/s/kSKrV5mZ7Egq 非常感谢参与的同学 1 第三个问题:使用tcb-router管理路由 https://developers.weixin.qq.com/community/develop/article/doc/00000a8eaec7e86b9f48cfeb051c13 2 第四个问题:weui内置扩展库使用步骤 https://developers.weixin.qq.com/s/1GPX7jmG78dz https://developers.weixin.qq.com/community/develop/article/doc/000224381788e8e5bb89426f55e413 3 第五个问题:算法生成云开发集合_id https://developers.weixin.qq.com/community/minihome/doc/000a0e0be54568e8ac0aab28256800 第六个问题 关于 swiper 中 数据过多时 滑动卡顿掉帧的解决办法? - 微信开放社区 https://developers.weixin.qq.com/community/develop/article/doc/000e665d0bcc78861d8a7dcef5b413 4
2020-07-09 - 请问小程序跳转斗鱼直播房间应该怎么填写路径?
我通过“获取小程序页面小程序码”取得了路径,但是填写进wx.navigateToMiniProgram后跳转还是斗鱼小程序的首页呢?有朋友做过吗? wx.navigateToMiniProgram({ appId: 'wxca1e7ba3fe18ff12', path: '/pages/room.html?roomId=4796577&is_vertical=1', success(res) { // 打开成功 } })
2019-11-03 - 分享一个我自己的小程序创业项目,请多指教/:@)/:@)/:@)
善食者联盟,一款基于商品保质期“动态定价”模式的小程序电商平台,商品按天计算价值,每天降价,越临近保质期越便宜。 商品价值(元/天)=零售价➗保质期天数。 ps:我是自己创业,前端是通过百度自学的,在这期间感谢社区里的小伙伴,不厌其烦的回答我提出的各种低级问题。 [图片] 商品详情页,同一商品可添加不同批次,用户可根据自身对保质期的偏好进行选择。 [图片] 营养成分表、致敏原信息提示及配料表展示 [图片] 寻味记板块:用户可以自由分享家乡特色美食,春节后会开通特产橱窗。 [图片] 知味(食品安全互动问答板块) [图片] 欢迎大家下面留言一起讨论关于商业模式、ui设计、前端开发的各种问题。
2020-01-08 - 你不知道的小程序系列之生命周期执行顺序
再次开始之前先问几个问题: 你是否知道[代码]Page[代码]生命周期 与 [代码]pagelifetimes[代码] 生命周期执行顺序? 你是否知道[代码]behaviors[代码]中的生命周期与组件生命周期执行顺序? 你是否知道[代码]Page[代码]生命周期 与 组件[代码]pagelifetimes[代码]生命周期执行顺序? 要回答上面的问题,首先我们看看小程序生命周期有哪些: App onLaunch onShow onHide Page onLoad onShow onReady onHide onUnload Component created attached ready moved detached 想一下加载一个页面(包含组件)的加载顺序,按照直觉小程序加载顺序应该是这样的加载顺序(以下列子中[代码]Component[代码]都是同步组件): App(onLaunch) -> Page(onLoad) -> Component(created) 但其实并不然,小程序的加载顺序是这样的: 首先执行 [代码]App.onLaunch[代码] -> [代码]App.onShow[代码] 其次执行 [代码]Component.created[代码] -> [代码]Component.attached[代码] 再执行 [代码]Page.onLoad[代码] -> [代码]Page.onShow[代码] 最后 执行 [代码]Component.ready[代码] -> [代码]Page.onReady[代码] 其实也不难理解微信这么设计背后的逻辑,我们先看下官方的的生命周期: [图片] 可以看到,在页面[代码]onLoad[代码]之前会有页面[代码]create[代码]阶段,这其中就包含了组件的初始化,等组件初始化完成之后,才会执行页面的[代码]onLoad[代码], 之后页面[代码]ready[代码]事件也是在组件[代码]ready[代码]之后才触发的。 下面我们来看看 [代码]Behavior[代码], [代码]Behavior[代码] 与 [代码]Vue[代码]中的 [代码]mixin[代码] 类似,猜想下其中的执行顺序: Behavior.created => Component.created 测试下来和预期相符,其实在[代码]Vue[代码]的文档中有一段这样的描述: 另外,混入对象的钩子将在组件自身钩子之前调用。 这样的设计和主流设计保持一致。接下来我们看看 [代码]pageLifetimes[代码],有[代码]show[代码]和[代码]hide[代码]生命周期对应页面的展示与隐藏,预期的执行顺序: pageLifetime.show => Page.onShow 测试下来也和预期相符,那么我们可以推断出如下的结论: 当页面中包含组件时,组件的生命周期(包括pageLifetimes)总是优先于页面,[代码]Behaviors[代码]生命周期优先于组件的生命周期。但其实有个例外:页面退出堆栈,当页面[代码]unload[代码]时会执行如下顺序: Page.onUnload => Component.detached 看了以上的分析你应该知道了答案,最后做个总结(demo): [图片] 最后的最后布置个作业 异步组件(异步渲染的组件,通常是通过if条件判断是否渲染)的生命周期执行顺序是怎样的,pagelifetimes会不会执行?
2020-01-10 - 云端定时触发器调用订阅消息,为什么会返回604101错误?
我在本地测试云函数时,使用subscribeMessage.send没有遇到问题,但是设置定时触发器及云端测试调用后,就遇到了和以下链接一样的问题: https://developers.weixin.qq.com/community/develop/doc/000a265699494018f459acb705b800?highLine=subscribeMessage.send%253Afail%2520Invalid%2520request%2520param 具体报错为Error: errCode: -501007 invalid parameters | errMsg: subscribeMessage.send:fail Invalid request param 根据帖子里的一些回复,下载了最新的nightly build开发工具,上传云函数和定时触发器后,又发生了更加令人费解的错误: "errCode":-604101,"errMsg":"system error" 为什么本地测试正常,在云端频频报错呢?
2019-11-15 - 做了一个颜色选择器
edit at 11/12 代码传到了:https://github.com/eclipseglory/zasi-components , DEMO演示在文章结尾 小程序没有提供color-picker类似的组件,只能自己做。 可传统的RGB颜色选择器,真的腻了,而且在手机上也不是很操作,就跑网上搜了一圈,发现有一种圆环形的(基于HSV)我很喜欢: [图片] 我自诩对canvas2d和webgl很熟悉,做个这玩意儿很轻松,开始做!没想到痛苦开始了。 从上周5开始,一共做了三个版本: 1.纯canvas版本 2.canvas+组件版本 3.纯组件版本 纯canvas版本这个版本做了整整一天! [图片] 由于canvas绘制性能问题,特别是因为没有requestAnimationFrame可以调用,别说在真机上测试特别不流畅,就是在模拟器上也小卡小卡的。而且,在纯的canvas进行触摸定位等事件响应处理,计算起来太麻烦,bug不断,只能放弃了。 混合版本因为wxs模块是提供requestAnimationFrame接口的,所以我就想,使用canvas作为底部颜色环,上面就直接用view作为指针,这样,事件触发和处理比起纯canvas要简单得多,而且还能利用rAF回调页面接口去绘制其他canvas。 的确,我的想法得到了证实,这个混合版本比起第一个要流畅得多! 可就要完工的时候,我却发现,在真机上,cover-view的鼠标事件有很大问题,坐标值飘忽不定,也就是说拖动指针会发生鬼畜般的抖动!加上我不知道怎么debug到wxs模块中,于是跟个sb一样fix,找了半天也没找到问题在哪儿,直到我搜索时,返现有人也遇到和我一样的问题,我才安心了:这是小程序的问题。 动手改!既然cover-view有不行,那就不用它。 实际上canvas在该组件中的作用无非就是绘制一个圆环而已,如果我利用离屏canvas事先画好,然后保存成图片,再用image加载它,这样就可以避免使用canvas来显示圆环了,也就可以不用cover-view放到其顶部! 想法是好的,可是到了真机上,绘制保存出来的图片时好时坏: [图片] 只能放弃,又耽误我一天。 无canvas版本刚才说了,canvas在该组件中的作用,仅仅是绘制一个颜色环而已,除此之外真没什么用。 那我就用css模拟一个类似圆环就好了,精确到每一度一个颜色一点意义没有。 所以就利用css的background-image属性,做了4个四分之一圆弧,然后拼在一起,得到了一个彩色原版,再用一个小的view遮挡,让它们只露出一部分,圆环就做好了。 之前的代码都不用改,直接用新作的圆环views替换canvas的标签即可。主体框架和功能,不到一天就完成了,不得不说,比起纯的canvas绘制,要方便太多太多。 这是截图: [图片] 代码片段这里是 演示DEMO,要使用的话,复制里面的组件出来用就好。 有些代码我混淆过,但不耽误使用。 有问题找我
2019-11-12 - 【开箱即用】分享几个好看的波浪动画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 - 如何写一个自己的脚手架 - 一键初始化项目
如何写一个自己的脚手架 - 一键初始化项目 介绍 脚手架的作用:为减少重复性工作而做的重复性工作 即为了开发中的:编译 es6,js 模块化,压缩代码,热更新等功能,我们使用[代码]webpack[代码]等打包工具,但是又带来了新的问题:初始化工程的麻烦,复杂的[代码]webpack[代码]配置,以及各种配置文件,所以就有了一键生成项目,0 配置开发的脚手架 本文项目代码地址 本文以我司的脚手架工具 简化之后为基础 本系列分 3 篇,详细介绍如何实现一个脚手架: 一键初始化项目 0 配置开发环境与打包 一键上传服务器 首先说一下个人的开发习惯 在写功能前我会先把调用方式写出了,然后一步一步的从使用者的角度写,现将基础功能写好后,慢慢完善 例如一键初始化项目功能 我期望的就是 在命令行执行输入 [代码]my-cli create text-project[代码],回车后直接创建项目并生成模板,还会把依赖都下载好 我们下面就从命令行开始入手 创建项目 [代码]my-cli[代码],执行 [代码]npm init -y[代码]快速初始化 bin [代码]my-cli[代码]: 在 [代码]package.json[代码] 中加入: [代码]{ "bin": { "my-cli": "bin.js" } } [代码] [代码]bin.js[代码]: [代码]#!/usr/bin/env node console.log(process.argv); [代码] [代码]#!/usr/bin/env node[代码],这一行是必须加的,就是让系统动态的去[代码]PATH[代码]目录中查找[代码]node[代码]来执行你的脚本文件。 命令行执行 [代码]npm link[代码] ,创建软链接至全局,这样我们就可以全局使用[代码]my-cli[代码]命令了,在开发 [代码]npm[代码] 包的前期都会使用[代码]link[代码]方式在其他项目中测试来开发,后期再发布到[代码]npm[代码]上 命令行执行 [代码]my-cli 1 2 3[代码] 输出:[代码][ '/usr/local/bin/node', '/usr/local/bin/my-cli', '1', '2', '3' ][代码] 这样我们就可以获取到用户的输入参数 例如[代码]my-cli create test-project[代码] 我们就可以通过数组第 [2] 位判断命令类型[代码]create[代码],通过第 [3] 位拿到项目名称[代码]test-project[代码] commander [代码]node[代码]的命令行解析最常用的就是[代码]commander[代码]库,来简化复杂[代码]cli[代码]参数操作 (我们现在的参数简单可以不使用[代码]commander[代码],直接用[代码]process.argv[3][代码]获取名称,但是为了之后会复杂的命令行,这里也先使用[代码]commander[代码]) [代码]#!/usr/bin/env node const program = require("commander"); const version = require("./package.json").version; program.version(version, "-v, --version"); program .command("create <app-name>") .description("使用 my-cli 创建一个新的项目") .option("-d --dir <dir>", "创建目录") .action((name, command) => { const create = require("./create/index"); create(name, command); }); program.parse(process.argv); [代码] [代码]commander[代码] 解析完成后会触发[代码]action[代码]回调方法 命令行执行:[代码]my-cli -v[代码] 输出:[代码]1.0.0[代码] 命令行执行: [代码]my-cli create test-project[代码] 输出:[代码]test-project[代码] 创建项目 拿到了用户传入的名称,就可以用这么名字创建项目 我们的代码尽量保持[代码]bin.js[代码]整洁,不将接下来的代码写在[代码]bin.js[代码]里,创建[代码]create[代码]文件夹,创建[代码]index.js[代码]文件 [代码]create/index.js[代码]中: [代码]const path = require("path"); const mkdirp = require("mkdirp"); module.exports = function(name) { mkdirp(path.join(process.cwd(), name), function(err) { if (err) console.error("创建失败"); else console.log("创建成功"); }); }; [代码] [代码]process.cwd()[代码]获取工作区目录,和用户传入项目名称拼接起来 (创建文件夹我们使用[代码]mkdirp[代码]包,可以避免我们一级一级的创建目录) 修改[代码]bin.js[代码]的[代码]action[代码]方法: [代码]// bin.js .action(name => { const create = require("./create") create(name) }); [代码] 命令行执行: [代码]my-cli create test-project[代码] 输出:[代码]创建成功[代码] 并在命令行所在目录创建了一个[代码]test-project[代码]文件夹 模板 首先需要先列出我们的模板包含哪些文件 一个最基础版的[代码]vue[代码]项目模板: [代码]|- src |- main.js |- App.vue |- components |- HelloWorld.vue |- index.html |- package.json [代码] 这些文件就不一一介绍了 我们需要的就是生成这些文件,并写入到目录中去 模板的写法后很多种,下面是我的写法: 模板目录: [代码]|- generator |- index-html.js |- package-json.js |- main.js |- App-vue.js |- HelloWorld-vue.js [代码] [代码]generator/index-html.js[代码] 模板示例: [代码]module.exports = function(name) { const template = ` { "name": "${name}", "version": "1.0.0", "description": "", "main": "index.js", "scripts": {}, "devDependencies": { }, "author": "", "license": "ISC", "dependencies": { "vue": "^2.6.10" } } `; return { template, dir: "", name: "package.json" }; }; [代码] [代码]dir[代码]就是目录,例如[代码]main.js[代码]的[代码]dir[代码]就是[代码]src[代码] [代码]create/index.js[代码]在[代码]mkdirp[代码]中新增: [代码]const path = require("path"); const mkdirp = require("mkdirp"); const fs = require("fs"); module.exports = function(name) { const projectDir = path.join(process.cwd(), name); mkdirp(projectDir, function(err) { if (err) console.error("创建失败"); else { console.log(`创建${name}文件夹成功`); const { template, dir, name: fileName } = require("../generator/package")(name); fs.writeFile(path.join(projectDir, dir, fileName), template.trim(), function(err) { if (err) console.error(`创建${fileName}文件失败`); else { console.log(`创建${fileName}文件成功`); } }); } }); }; [代码] 这里只写了一个模板的创建,我们可以用[代码]readdir[代码]来获取目录下所有文件来遍历执行 下载依赖 我们平常下载[代码]npm[代码]包都是使用命令行 [代码]npm install / yarn install[代码] 这时就需要用到 [代码]node[代码] 的 [代码]child_process.spawn[代码] api 来调用系统命令 因为考虑到跨平台兼容处理,所以使用 cross-spawn 库,来帮我们兼容的操作命令 我们创建[代码]utils[代码]文件夹,创建[代码]install.js[代码] [代码]utils/install.js[代码]: [代码]const spawn = require("cross-spawn"); module.exports = function install(options) { const cwd = options.cwd || process.cwd(); return new Promise((resolve, reject) => { const command = options.isYarn ? "yarn" : "npm"; const args = ["install", "--save", "--save-exact", "--loglevel", "error"]; const child = spawn(command, args, { cwd, stdio: ["pipe", process.stdout, process.stderr] }); child.once("close", code => { if (code !== 0) { reject({ command: `${command} ${args.join(" ")}` }); return; } resolve(); }); child.once("error", reject); }); }; [代码] 然后我们就可以在创建完模板后调用[代码]install[代码]方法下载依赖 [代码]install({ cwd: projectDir }); [代码] 要知道工作区为我们项目的目录 至此,解析 cli,创建目录,创建模板,下载依赖一套流程已经完成 基本功能都跑通之后下面就是要填充剩余代码和优化 优化 当代码写的多了之后,我们看上面[代码]create[代码]方法内的回调嵌套回调会非常难受 [代码]node 7[代码]已经支持[代码]async,await[代码],所以我们将上面代码改成[代码]Promise[代码] 在[代码]utils[代码]目录下创建,[代码]promisify.js[代码]: [代码]module.exports = function promisify(fn) { return function(...args) { return new Promise(function(resolve, reject) { fn(...args, function(err, ...res) { if (err) return reject(err); if (res.length === 1) return resolve(res[0]); resolve(res); }); }); }; }; [代码] 这个方法帮我们把回调形式的[代码]Function[代码]改成[代码]Promise[代码] 在[代码]utils[代码]目录下创建,[代码]fs.js[代码]: [代码]const fs = require(fs); const promisify = require("./promisify"); const mkdirp = require("mkdirp"); exports.writeFile = promisify(fs.writeFile); exports.readdir = promisify(fs.readdir); exports.mkdirp = promisify(mkdirp); [代码] 将[代码]fs[代码]和[代码]mkdirp[代码]方法改造成[代码]promise[代码] 改造后的[代码]create.js[代码]: [代码]const path = require("path"); const fs = require("../utils/fs-promise"); const install = require("../utils/install"); module.exports = async function(name) { const projectDir = path.join(process.cwd(), name); await fs.mkdirp(projectDir); console.log(`创建${name}文件夹成功`); const { template, dir, name: fileName } = require("../generator/package")(name); await fs.writeFile(path.join(projectDir, dir, fileName), template.trim()); console.log(`创建${fileName}文件成功`); install({ cwd: projectDir }); }; [代码] 结语 关于进一步优化: 更多功能与健壮 例如指定目录创建项目,目录不存在等情况 [代码]chalk[代码]和[代码]ora[代码]优化[代码]log[代码],给用户更好的反馈 通过[代码]inquirer[代码]问询用户得到更多的选择:模板[代码]vue-router[代码],[代码]vuex[代码]等更多初始化模板功能,[代码]eslint[代码] 更多的功能: 内置 webpack 配置 一键发布服务器 其实要学会善用第三方库,你会发现我们上面的每个模块都有第三方库的身影,我们只是将这些功能组装起来,再结合我们的想法进一步封装 虽然有[代码]vue-cli[代码],[代码]create-react-app[代码]这些已有的脚手架,但是我们还是可能在某些情况下需要自己实现脚手架部分功能,根据公司的业务来封装,减少重复性工作,或者了解一下内部原理
2019-09-26 - 巧用云调用,实现【共享名片夹】小程序
原创: 锋少 一、前言 从一个较早的小程序开发者到第一批使用小程序·云开发的开发者,这期间一直在关注关于小程序各方面的更新,同时也用小程序·云开发做了几款产品,其中包括上次分享的随手记Lite小程序,比较上次,这次分享的技术点相对更加全面和实用一些。 涉及的技术点有: 数据上传、数据更新、分页读取、数据删除,AI智能名片识别读取。 单图上传、多图上传,图片URL获取,带参小成码生成。 下发模板消息,云调用使用。 二、主要功能 创建电子名片:信息存储,图片上传,名片读取(AI智能名片识别) 转发电子名片:专属名片海报(带参小程序码生成) 电子名片被访问:下发模板消息(云调用) 三、功能实现 3.1、准备工作 1、注册微信小程序账号: 方式一:直接注册(https://mp.weixin.qq.com/wxopen/waregister?action=step1) 方式二:已经有微信公众号(已认证)朋友可以直接【登录公众号】 -> 【小程序管理】 -> 【添加】->【快速注册并认证小程序】,注册完成后,找到小程序的 AppID和 AppSecret [图片] 2、下载微信开发者工具、创建项目 ,打开开发者工具,键入项目目录、项目名称、刚才的 AppID,此时项目创建成功,然后点击开发者工具上方的【云开发】开通云开发。 3.2功能实现一:【创建电子名片】 信息存储,图片上传,名片读取(AI智能名片识别) 1.功能简要描述 对于一个名片的小程序,第一步肯定是创建电子名片,除此之外,可以用传统信息录入的方式创建名片,同时也支持纸质名片的识别读取,快速创建名片,这里本地需要导入 [代码]mapping.js[代码]框架,接下来以纸质名片识别为例。 2.核心代码 [代码] // 上传名片后获取零时链接 getTempFileURL() { wx.cloud.getTempFileURL({ fileList: [{ fileID: this.data.fileID, }], }).then(res => { console.log('获取成功', res); if (res.fileList.length) { this.setData({ coverImage: res.fileList[0].tempFileURL }, () => { this.parseNameCard(); }); } else { Toast('获取图片地址失败'); } }).catch(err => { Toast('获取图片地址失败'); }); }, // 读取名片 parseNameCard() { wx.cloud.callFunction({ name: 'parseCard', data: { url: this.data.coverImage } }).then(res => { if (res.result.data.length == 0) { Toast('解析失败,请上传【纸质名片】或【手动创建】'); return; } let data = this.transformMapping(res.result.data); wx.setStorageSync("parseCardData", data) Toast('解析成功'); }).catch(err => { console.error('解析失败,请上传【纸质名片】或【手动创建】', err); Toast('解析失败,请上传【纸质名片】或【手动创建】'); }); }, // 名片数据解析 transformMapping(data) { let record = {}; let returnData = []; data.map((item) => { let name = null; if (mapping.hasOwnProperty(item.item)) { name = mapping[item.item]; // 写入英文名 item.name = name; } return item; }); // 过滤重复的字段 data.forEach((item) => { if (!record.hasOwnProperty(item.item)) { returnData.push(item); record[item.item] = true; } }); return returnData; }, [代码] 3.3功能实现二:【转发电子名片】 专属名片海报(带参小程序码生成) 1.功能简要描述:转发电子名片有两种方式。 1.以小程序的形式直接转发给好友或微信群。 2.生成专属名片海报分享到朋友圈长按进入对应的电子名片页面。名片海报上除了有对应用户的姓名之外,还有专属的名片小程序码,效果如下: [图片] 2.核心代码 [代码]const cloud = require('wx-server-sdk') const axios = require('axios') var rp = require('request-promise'); cloud.init() // 云函数入口函数,小程序端传过来页面和名片id exports.main = async (event, context) => { console.log(event) try { const resultValue = await rp('https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=appid&secret=secret') const token = JSON.parse(resultValue).access_token; const response = await axios({ method: 'post', url: 'https://api.weixin.qq.com/wxa/getwxacodeunlimit', responseType: 'stream', params: { access_token: token, }, data: { page: event.page, width: 300, scene: event.id, }, }); return await cloud.uploadFile({ cloudPath: 'xcxcodeimages/' + Date.now() + '.png', fileContent: response.data, }); } catch (err) { console.log('>>>>>> ERROR:', err) } } [代码] 3.4功能实现三:【电子名片被访问】 下发模板消息(云调用) 1.功能简要描述 用户名片被访问的时候,用户者会收到【客户来访提醒】的模板消息,同时提醒用户完善名片信息。 2.核心代码 [代码]const cloud = require('wx-server-sdk') cloud.init() exports.main = async (event, context) => { try { const result = await cloud.openapi.templateMessage.send({ touser: event.toUser, page: "pages/index/index", data: { keyword1: { value: event.visitDate }, keyword2: { value: "刚刚有人深度访问了您的名片,经常完善名片信息,更容易被查找和访问。" }, }, templateId: 'templateId', formId: event.formId, }) return result } catch (err) { throw err } } [代码] 四、总结 和传统的小程序 + WEB后台开发模式比起来,云开发在精力和人力上真的是节省了很多,这能使开发者将大部分精力和时间放到功能的开发上。 云开发上线时间不算太长,但逐步有新的功能开放出来,比如云控制台数据的导入导出、云调用等,希望小程序·云开发开放出更多的接口和功能… 五、项目预览 [图片]
2019-05-05 - 【周刊-1】三年大厂面试官-面试题精选及答案
前言 在阿里和腾讯工作了6年,当了3年的前端面试官,把期间我和我的同事常问的面试题和答案汇总在我 Github 的 Weekly-FE-Interview 中。希望对大家有所帮助。 如果你在bat面试的时候遇到了什么不懂的问题,欢迎给我提issue,我会把题目汇总并将面试要点和答案写好放在周刊里,大家一起共同进步和成长,助力大家进入自己理想的企业。 项目地址是:https://github.com/airuikun/Weekly-FE-Interview 常见面试题精选 以下是十道大厂一面的时候常见的面试题,如果全部理解并且弄透,在一面或者电话面的时候基本上能中1~2题。小伙伴可以先不急着看答案,先自己尝试着思考一下和自己实现一下,然后再看答案。 第 1 题:http的状态码中,499是什么?如何出现499,如何排查跟解决 解析:第 1 题:http的状态码中,499是什么?如何出现499,如何排查跟解决 第 2 题:讲解一下HTTPS的工作原理 解析:第 2 题:讲解一下HTTPS的工作原理 第 3 题:讲解一下https对称加密和非对称加密。 解析:第 3 题:讲解一下https对称加密和非对称加密 第 4 题:如何遍历一个dom树 解析:第 4 题:如何遍历一个dom树 第 5 题:new操作符都做了什么 解析:第 5 题:new操作符都做了什么 第 6 题:手写代码,简单实现call 解析:第 6 题:手写代码,简单实现call 第 7 题:手写代码,简单实现apply 解析:第 7 题:手写代码,简单实现apply 第 8 题:手写代码,简单实现bind 解析:第 8 题:手写代码,简单实现bind 第 9 题: 简单实现项目代码按需加载,例如import { Button } from ‘antd’,打包的时候只打包button 解析:第 9 题: 简单实现项目代码按需加载,例如import { Button } from ‘antd’,打包的时候只打包button 第 10 题:简单手写实现promise 解析:第 10 题:简单手写实现promise 结语 本人还写了一些前端进阶知识的文章,如果觉得不错可以点个star。 blog项目地址是:https://github.com/airuikun/blog 我是小蝌蚪,腾讯高级前端工程师,跟着我一起每周攻克几个前端技术难点。希望在小伙伴前端进阶的路上有所帮助,助力大家进入自己理想的企业。
2019-04-08 - 适配刘海屏和全面屏的一些小心得
今年开始各路刘海和全面屏手势的手机已经开始霸占市场,全面屏和刘海屏的适配也必须提上日程。 相信大家也一定会有第一次将未适配的小程序放到全面屏或刘海屏手机上的尴尬体验。 尤其是在导航栏设置为custom时,标题与胶囊对不齐简直逼死强迫症。。 微信官方也没有出一个官方的指导贴帮助开发者。 这里仅总结一下个人关于这个问题的一些处理方式,如有疏漏烦请指正补充。 适配的关键在两个位置即额头和下巴,头不用说自然是关于刘海的。 小程序的头的高度主要分为2个部分 1.statusBarHeight 该值可以在app onLaunch 调用wx.getSystemInfoSync() 获取到 a)刘海 高度44 [图片] b)无刘海 ios高度20 安卓各不相同 [图片] 2.胶囊高度 即下图高度 [图片] 在查阅社区问答后了解到小程序给到的策略是ios在模拟器下统一是44px,ios在真机下统一是40px(感谢指正@bug之所措 ),而安卓下统一是48px,因此我们又可以在wx.getSystemInfoSync() 中获取到系统之后得到胶囊高度。 总的导航栏高度即这两个高度之合。本人项目中是将导航做成组件并给到slot,方便各个页面配置。 开发者工具 1.02.1810190 及以上版本支持在 app.json 中声明 usingComponents 字段,在此处声明的自定义组件视为全局自定义组件,在小程序内的页面或自定义组件中可以直接使用而无需再声明。 目前小程序还支持在单个页面配置custom,也可以配合使用~ 另一个需要关注的则是底部,参考的文章是 https://www.jianshu.com/p/a1e8c7cf8821 重点是在于在全面屏的手机的底部需要流出34px的空白给到全面屏返回手势操作,此外由于全面屏屏幕圆边还可能使一些按钮或功能无法正常使用。 那么首先如何判断是否是全面屏呢?个人的做法是判断屏幕高度是否大于750,iphone的plus系列高度在736,正好在这个范围之内,当然750不一定准确,如果出现疏漏烦请补充。 涉及到底部的主要是弹出的操作菜单、tabBar和底部定位的按钮等。这里做了一个简单的代码片段。 https://developers.weixin.qq.com/s/fnU0n8mv7o5M 希望能够帮助到大家,也欢迎交流~
2019-01-03 - 社区经验分享与问题总结
1. 插件开发中,使用自定义组件需用到相对路径 "usingComponents":{ "alert":"../../components/alert/alert" } 注:选择自定义组件需要加上in(this) wx.createSelectorQuery().in(this).select('.xxxxx') 2. 获取小程序码相关 通过接口生成的小程序码page必须是已经发布的 生成小程序码后可在客户端开发工具中,通过二维码编译进行测试scene参数 [图片] 3. swiper组件current不重置问题(bug) 如:通过arr=[1,2]遍历swiper-item组件,当swiper滑动到current=1时,setData({arr:[2]});此时swiper会出现空白。 原因:current未重置为0,需自己去设置current。 https://developers.weixin.qq.com/community/develop/doc/00066c8beacfa05f24d7d144056800 4. 模板消息相关 时效性:1次支付可下发3条,1次提交表单可下发1条。(7天内有效) 对应性:发送消息的对象openId和formId是匹配的。 获取方式:发起支付或表单提交 5. 分包加载大小限制问题:使用分包加载时,如果在分包中使用插件,插件大小只会算在分包大小2MB与整包8MB内,不算入主包2MB。(之前算在了主包内,目前已修复)。 6. 获取unionId(包括openId)流程(前提:小程序 或 其主体公众号 与 微信开发平台账号关联) 开放平台关联同主体的公众号且 用户已经关注公众号: wx.login()=> 获取到code,后端通过appid+appserect+code,拿到openId+ session_key+unionId 开放平台关联小程序: 用户授权后通过wx. getUserInfo(需要授权)获取iv、encryptedData,然后解密(需要用到上面的 session_key),appid+ session_key + encryptedData + iv解密 得到unionId、openId及用户信息 [图片] 注:如果再次获取code会导致之前的session_key过期 7. h5与小程序跳转问题 公众号=>小程序:公众号自定义菜单可配置跳转到小程序 小程序=>h5:webview(需配置业务域名、webview不支持个人账号) 注:目前不支持h5与小程序的直接跳转 8. canvas原生组件覆盖自定义弹层的解决方案 用css样式控制器显示或隐藏,如hidden 纯显示性的canvas可以生成图片之后展示 9. 自定义弹层背景滚动问题 方法一:打开的函数中,如果自定义弹框当前显示,则isScroll设为true,否则设为false <scroll-viewclass="scanInvoice_content" height="100%"scroll-y="{{isScroll}}"> //设置Page的overflow-y属性值为hidden </scroll-view> 方法二:事件捕获,顶层加上catchtouchmove 10. 小程序图片分享截取变形或显示不全:保持分享的图片是5:4。 11. 授权问题 wx.authorize可以对除scope.userinfo之外的权限进行授权,scope.userinfo需要用<button open-type="getUserInfo"/>组件进行授权。 统一小程序下的用户拒绝授权之后会直接进入失败回调,这种情况可使用wx.openSetting引导用户授权。(用户手动删除小程序才会重新提示授权) 12. 数据绑定是双括号内只能是data里面的变量或者wxs里声明的函数。 13.不要用wx.request去访问微信接口,官方限制且不能把[代码]api.weixin.qq.com[代码]配置为服务器域名 基础库版本:v2.4.0 欢迎更新指正
2018-12-28 - 利用云函数绕过域名校验和HTTPS配置,实现内网加端口访问
闲来无事,无意中发现云函数中的request网络请求可以不用配置校验域名和https,也就是说可以通过云函数封装一个请求通用函数来处理没有域名和https的网络请求(甚至包括内网穿透,可以用非80端口进行实验)。 适用场景: A、没有域名或使用局域网(直接使用IP访问); B、使用花生壳动态域名解析(内网穿透); C、有域名但不想申请配置HTTPS(懒人); D、连自己的服务器都没有,接口直接使用开源或者第三方接口且不能添加域名校验的情况(空壳); E、不愿意直接在小程序中直接暴露自己逻辑API实际请求地址的(安全); ······ 具体步骤如下: 1、给项目添加云函数支持(https://developers.weixin.qq.com/miniprogram/dev/wxcloud/basis/getting-started.html) 2、新建名为“proxy”的云函数,配置支持request-promise [代码]// package.json[代码][代码]{[代码][代码] [代码][代码]"name"[代码][代码]: [代码][代码]"proxy"[代码][代码],[代码][代码] [代码][代码]"version"[代码][代码]: [代码][代码]"1.0.0"[代码][代码],[代码][代码] [代码][代码]"description"[代码][代码]: [代码][代码]""[代码][代码],[代码][代码] [代码][代码]"main"[代码][代码]: [代码][代码]"index.js"[代码][代码],[代码][代码] [代码][代码]"scripts"[代码][代码]: {[代码][代码] [代码][代码]"test"[代码][代码]: [代码][代码]"echo \"Error: no test specified\" && exit 1"[代码][代码] [代码][代码]},[代码][代码] [代码][代码]"author"[代码][代码]: [代码][代码]""[代码][代码],[代码][代码] [代码][代码]"license"[代码][代码]: [代码][代码]"ISC"[代码][代码],[代码][代码] [代码][代码]"dependencies"[代码][代码]: {[代码][代码] [代码][代码]"wx-server-sdk"[代码][代码]: [代码][代码]"latest"[代码][代码],[代码][代码] [代码][代码]"request"[代码][代码]: [代码][代码]"latest"[代码][代码],[代码][代码] [代码][代码]"request-promise"[代码][代码]: [代码][代码]"latest"[代码][代码] [代码][代码]}[代码][代码]}[代码][代码]// 云函数入口文件index.js[代码] [代码]const cloud = require([代码][代码]'wx-server-sdk'[代码][代码])[代码][代码]const rq = require([代码][代码]'request-promise'[代码][代码])[代码][代码]cloud.init()[代码][代码]// 云函数入口函数[代码][代码]// event为小程序调用的时候传递参数,包含请求参数uri、headers、body[代码][代码]exports.main = async (event, context) => {[代码][代码] [代码][代码]return[代码] [代码]await rq({[代码][代码] [代码][代码]method: [代码][代码]'POST'[代码][代码],[代码][代码] [代码][代码]uri: event.uri,[代码][代码] [代码][代码]headers: event.headers ? event.headers : {},[代码][代码] [代码][代码]body: event.body[代码][代码] [代码][代码]}).then(body => {[代码][代码] [代码][代码]return[代码] [代码]body[代码][代码] [代码][代码]}).[代码][代码]catch[代码][代码](err => {[代码][代码] [代码][代码]return[代码] [代码]err[代码][代码] [代码][代码]})[代码][代码]}[代码]3、在小程序中调用云函数请求数据请求 [代码]onLoad: [代码][代码]function[代码][代码](){[代码][代码] [代码][代码]// 初始化[代码][代码] [代码][代码]wx.cloud.init()[代码][代码]},[代码][代码]onGetItemList: [代码][代码]function[代码][代码](){[代码][代码] [代码][代码]wx.cloud.callFunction({[代码][代码] [代码][代码]name: [代码][代码]'proxy'[代码][代码],[代码][代码] [代码][代码]data: {[代码][代码] [代码][代码]// http域名 https域名 第三方域名 非验证域名 IP[:prot] 内网IP或花生壳域名[代码][代码] [代码][代码]uri: [代码][代码]'http://192.168.1.100:8081'[代码][代码],[代码][代码] [代码][代码]headers: {[代码][代码] [代码][代码]'Content-Type'[代码][代码]: [代码][代码]'application/json'[代码][代码] [代码][代码]},[代码][代码] [代码][代码]body: {[代码][代码] [代码][代码]uid: 1[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}).then(res => {[代码][代码] [代码][代码]console.log(res)[代码][代码] [代码][代码]const data = res.result[代码][代码] [代码][代码]console.log(data)[代码][代码] [代码][代码]// do something[代码][代码] [代码][代码]})[代码][代码]}[代码]然后你会发现你已经无所不能了。 个人见解,如有不妥之处,望各位大神指正!~
2018-12-03 - 分享一个小游戏开发神器-Annie2x,附上案例下载后可直接预览
在这里给大家分享一个小游戏开发神器-Annie2x. Annie2x是一款Flash插件,直接通过Flash来开发小游戏,简单逆天。 如果你会Flash或者以前用Flash做页游,做动画,那Annie2x基本上就是0难度了,直接上手。 如果你在Flash使用过CreateJS开发过H5,那基本上Annie2x基本上就是0难度了,直接上手。 并且Annie2x比CreateJS好用太多了。 如何使用呢,去Annie2x官网下载插件(http://annie2x.com)。然后在他的论坛有一个专门讲安装的教程 http://ask.annie2x.com/article/5。 照着来就行了。 不说了,直接上图: [图片] [图片] [图片] [图片] 具体制作可以去官方论坛,或者官方的QQ里了解,那里有更多学习资源。 上手非常快,之前用laya egret 还有cocos做一个项目,要做动画效果的时候真的是好麻烦。 有了Annie2x,像做swf一样开发小游戏,真的是太爽了。 上面这个游戏我只用了半天时间就把以前的Flash游戏转换成小游戏了。这里放出源码,大家直接下载后解压用微信开发者工具打开就能预览效果了,见证下这个神奇的开发工具吧。 源码地址:https://pan.baidu.com/s/1Igcyc7pAKijoezfCfm3I7A 密码:9x1j
2018-08-08 - 下载文件后缀名为unknown的解决方法
貌似这个问题有很多玩家遇到,微信官方说法是根据服务器响应的header中的Content-Type来决定下载到本地的文件的后缀的, 但是这个特性支持的特别不好,如果下载后,文件后缀名为.unknown就不好搞了。 怎么解决的呢? 思路: 利用wx.downloadFile下载文件,获取到tempFilePath临时文件。这个临时文件路径不要改动。 利用wx.getFileSystemManager获取到FileSystemManager对象,利用该对象的saveFile方法,把临时文件保存为本地文件,保存成功后,其success函数回调会返回savedFilePath本地路径。这个路径会把上面的临时文件移动到这个本地路径中,但是后缀名仍然为unknown。这一步的目的是把临时文件保存为本地文件。 本地文件已经有了,我们就可以对本地文件进行任意的操作。利用FileSystemManager对象的copyFile,把上面的后缀为unknown的本地文件,复制到另外的本地文件。这个时候你可以任意定义复制到的文件的后缀。 有一点坑的是:上面第一步、第二步中的文件的路径(tempFilePath、savedFilePath)我们都是知道的,但是第三部中的复制到的目标路径需要开发者自己定义路径。这个时候我们需要用到wx.env.USER_DATA_PATH常量。 这个常量是微信为每个小程序小游戏搞得目录地址,在这个路径下面你可以新建、删除等文件或者文件夹。有这个知识储备,你可以先调用FileSystemManager对象的mkdir方法,在wx.env.USER_DATA_PATH常量路径下新建一个文件夹,然后你再调用第三步的copyFile就可以了。 注: 第二步和第三步可以合并。 wx.downloadFile下载文件可以利用filePath指定下载的目标路径,这样可以省略上面的第二步操作,直接利用这个filePath进行第三部的复制操作,把后缀为unknown的filePath文件,复制到指定的文件后缀的目标文件中,然后就可以使用该文件了。 下面的代码是用TypeScript写的,但是逻辑思路是一样的。 (这是随便写的一个demo,代码有点乱堆,各位大佬轻喷~) [代码]private beginLoad(): void {[代码][代码] [代码][代码]var[代码] [代码]that = [代码][代码]this[代码][代码];[代码][代码] [代码][代码]if[代码] [代码](Laya.Browser.onWeiXin) {[代码][代码] [代码][代码]var[代码] [代码]fileMgr: FileSystemManager = wx.getFileSystemManager();[代码] [代码] [代码] [代码] //利用access方法判断文件是否可用[代码] [代码] [代码][代码]fileMgr.access({[代码][代码] [代码][代码]path: wx.env.USER_DATA_PATH + [代码][代码]'/music/music.wav'[代码][代码],[代码][代码] [代码][代码]success: [代码][代码]function[代码][代码](res) {[代码][代码] [代码][代码]console.log([代码][代码]'access res:'[代码][代码], res);[代码][代码] [代码][代码]AudioMgr.playMusic(wx.env.USER_DATA_PATH + [代码][代码]'/music/music.wav'[代码][代码]);[代码][代码] [代码][代码]},[代码] [代码] //失败,不可用,则下载文件到本地[代码] [代码] [代码][代码]fail: [代码][代码]function[代码][代码](res) {[代码][代码] [代码][代码]console.log([代码][代码]'access file fail.'[代码][代码], res);[代码][代码] [代码][代码]wx.downloadFile({[代码][代码] [代码][代码]url: [代码][代码]'http://fjdx.sc.chinaz.com/Files/DownLoad/sound1/201808/10453.wav'[代码][代码],[代码][代码] [代码][代码]header: [代码][代码]''[代码][代码],[代码][代码] [代码][代码]filePath: [代码][代码]''[代码][代码],[代码][代码] [代码][代码]success: [代码][代码]function[代码][代码](res) {[代码][代码] [代码][代码]console.log([代码][代码]'down load file success.'[代码][代码], res);[代码][代码] [代码][代码]that.saveMusicFile(res.tempFilePath);[代码][代码] [代码][代码]},[代码][代码] [代码][代码]fail: [代码][代码]function[代码][代码](res) {[代码][代码] [代码][代码]console.log([代码][代码]'down load file fail.'[代码][代码], res);[代码][代码] [代码][代码]},[代码][代码] [代码][代码]});[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码] //保存临时文件到本地路径 [代码] [代码][代码]private saveMusicFile(tempPath): void {[代码][代码] [代码][代码]var[代码] [代码]fileMgr: FileSystemManager = wx.getFileSystemManager();[代码] [代码] //这一步是把保存的文件列表删除掉。本地文件最大50M。很容易超过限制[代码] [代码] [代码][代码]fileMgr.getSavedFileList({[代码][代码] [代码][代码]success: [代码][代码]function[代码][代码](savedFiles) {[代码][代码] [代码][代码]console.log([代码][代码]'saved files:'[代码][代码], savedFiles);[代码][代码] [代码][代码]var[代码] [代码]fileList = savedFiles.fileList as Array<any>;[代码][代码] [代码][代码]for[代码][代码]([代码][代码]var[代码] [代码]i=0; i<fileList.length; i++) {[代码][代码] [代码][代码]fileMgr.removeSavedFile({[代码][代码] [代码][代码]filePath: fileList[i].filePath,[代码][代码] [代码][代码]success: [代码][代码]function[代码][代码](r) {[代码][代码] [代码][代码]console.log([代码][代码]'remove save file. success.'[代码][代码], r);[代码][代码] [代码][代码]},[代码][代码] [代码][代码]fail: [代码][代码]function[代码][代码](r) {[代码][代码] [代码][代码]console.log([代码][代码]'remove save file.'[代码][代码], r);[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码][代码] [代码][代码]}[代码][代码] [代码][代码]},[代码][代码] [代码][代码]fail: [代码][代码]function[代码][代码](savedFiles) {[代码][代码] [代码][代码]console.log([代码][代码]'saved files fail.'[代码][代码], savedFiles);[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码] [代码] [代码] [代码] //把临时文件保存到本地文件中[代码] [代码] [代码][代码]fileMgr.saveFile({[代码][代码] [代码][代码]tempFilePath: tempPath,[代码][代码] [代码][代码]success: [代码][代码]function[代码][代码](data) {[代码][代码] [代码][代码]console.log([代码][代码]'save file:'[代码][代码], data);[代码] [代码] [代码] [代码] //新建dir目录,把保存的本地文件,复制到指定的文件夹下[代码] [代码] [代码][代码]fileMgr.mkdir({[代码][代码] [代码][代码]dirPath: wx.env.USER_DATA_PATH + [代码][代码]'music'[代码][代码],[代码][代码] [代码][代码]success: [代码][代码]function[代码][代码](res) {[代码][代码] [代码][代码]console.log([代码][代码]'mkdir success:'[代码][代码], res);[代码] [代码] [代码] [代码] //复制文件到目标文件夹下指定的后缀的文件中[代码] [代码] [代码][代码]fileMgr.copyFile({[代码][代码] [代码][代码]srcPath: data.savedFilePath,[代码][代码] [代码][代码]destPath: wx.env.USER_DATA_PATH + [代码][代码]'/music/music.wav'[代码][代码],[代码][代码] [代码][代码]success: [代码][代码]function[代码][代码](result) {[代码][代码] [代码][代码]console.log([代码][代码]'copy file :'[代码][代码], result);[代码] [代码] //复制成功后,就可以进行后续逻辑处理了[代码] [代码] [代码][代码]AudioMgr.playMusic(wx.env.USER_DATA_PATH + [代码][代码]'/music/music.wav'[代码][代码]);[代码][代码] [代码][代码]},[代码][代码] [代码][代码]fail: [代码][代码]function[代码][代码](result) {[代码][代码] [代码][代码]console.log([代码][代码]'copy file fail>'[代码][代码], result);[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码][代码] [代码][代码]},[代码][代码] [代码][代码]fail: [代码][代码]function[代码][代码](res) {[代码][代码] [代码][代码]console.log([代码][代码]'mkdir fail res:'[代码][代码], res);[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码][代码] [代码][代码]},[代码][代码] [代码][代码]fail: [代码][代码]function[代码][代码](data) {[代码][代码] [代码][代码]console.log([代码][代码]'save file fail:'[代码][代码], data);[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码][代码] [代码][代码]}[代码]
2018-08-14 - 在setData1024KB限制下如何做到无限翻页列表交互?
最近在做一个翻页交互,遇到点setData的坑,最后想了个办法给绕过去了,但我不知道各位有没有更好的办法,在这里分享下我的处理办法; 例如我有个列表,这个列表的总数据量不确定有多少,经我们产品交代,大促期间,至少会有1000条,从小程序的开发文档里可以看到setData对数据的限制是1024KB,因为之前我看文档时没有注意到这一点,所以我一开始在做分页的时候就用了错误的方法,上代码: wxml: [代码] [代码] [代码]<[代码][代码]sku-item[代码] [代码] [代码][代码]wx:for[代码][代码]=[代码][代码]"{{skuList}}"[代码][代码] [代码][代码]skuData[代码][代码]=[代码][代码]"{{item}}"[代码][代码] [代码][代码]actId[代码][代码]=[代码][代码]"{{actId}}"[代码][代码] [代码][代码]wx:key[代码][代码]=[代码][代码]"{{item.skuid}}"[代码][代码]/>[代码] [代码] [代码] js: [代码] [代码] [代码]data: {[代码] [代码] [代码][代码]skuList: [][代码][代码]},[代码][代码]loadMore: [代码][代码]function[代码][代码]() {[代码][代码] [代码][代码]request([代码][代码]function[代码][代码](res) {[代码][代码] [代码][代码]this[代码][代码].setData({[代码][代码] [代码][代码]skuList: [代码][代码]this[代码][代码].data.skuList.concat(res.list)[代码][代码] [代码][代码]});[代码][代码] [代码][代码]})[代码][代码]}[代码] [代码] [代码] 好,这样写,问题就出来了,当数据量慢慢的累积起来,就会触发1024KB阈值,后面的数据就算能取回来,也set不进去了。 于是我想了一个解决方案: 把之前的渲染流程拆成两步来做,第1步: 先想办法把每页的坑位给渲染出来,于是我搞了一个skuPage组件, 在这个组件中来单独做每页的sku渲染: wxml: [代码] [代码] [代码]<[代码][代码]sku-item[代码] [代码] [代码][代码]wx:for[代码][代码]=[代码][代码]"{{skuList}}"[代码][代码] [代码][代码]skuData[代码][代码]=[代码][代码]"{{item}}"[代码][代码] [代码][代码]actId[代码][代码]=[代码][代码]"{{actId}}"[代码][代码] [代码][代码]wx:key[代码][代码]=[代码][代码]"{{item.skuid}}"[代码][代码]/>[代码] [代码] [代码] js: [代码] [代码] [代码]data: {[代码] [代码] [代码][代码]skuList: [][代码][代码]},[代码][代码]methods: {[代码][代码] [代码][代码]setListData: [代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]let _this = [代码][代码]this[代码][代码];[代码][代码] [代码][代码]this[代码][代码].setData({[代码][代码] [代码][代码]skuList: app.globalData.skuList[代码][代码] [代码][代码]});[代码][代码] [代码][代码]app.globalData.skuList = [];[代码][代码] [代码][代码]}[代码][代码]},[代码][代码]ready: [代码][代码]function[代码][代码]() {[代码][代码] [代码][代码]this[代码][代码].setListData();[代码][代码]}[代码] [代码] [代码] 在外部页面中调用skuPage组件: wxml: [代码] [代码] [代码]<[代码][代码]sku-page[代码] [代码] [代码][代码]class[代码][代码]=[代码][代码]"sku-page"[代码][代码] [代码][代码]wx:for[代码][代码]=[代码][代码]"{{pageWrapCount}}"[代码][代码] [代码][代码]wx:key[代码][代码]=[代码][代码]"{{index}}"[代码][代码] [代码][代码]actId[代码][代码]=[代码][代码]"{{actId}}"[代码][代码]/>[代码] [代码] [代码] js: [代码] [代码] [代码]data: {[代码] [代码] [代码][代码]pageWrapCount: [][代码][代码]},[代码][代码]loadMore: [代码][代码]function[代码][代码]() {[代码][代码] [代码][代码]app.globalData.skuList = res.list;[代码][代码] [代码][代码]request([代码][代码]function[代码][代码](res) {[代码][代码] [代码][代码]this[代码][代码].setData({[代码][代码] [代码][代码]pageWrapCount: [代码][代码]this[代码][代码].data.pageWrapCount.concat([1])[代码][代码] [代码][代码]});[代码][代码] [代码][代码]})[代码][代码]}[代码] [代码] [代码] 好,这样我们把取回的数据先不要立马set进去,而是把它先丢在global里,然后让坑位+1,这样skuPage就会新增一个,就会触发skuPage的ready钩子函数,这个时候再在skuPage的ready钩子中,从global中把list取过来丢给skuPage组件的skuList,让skuPage组件去渲染,这样就能绕开setData的1024KB上线,因为每次针对于坑位来说 [代码] [代码] [代码]this[代码][代码].setData({[代码] [代码] [代码][代码]pageWrapCount: [代码][代码]this[代码][代码].data.pageWrapCount.concat([1])[代码][代码]});[代码] [代码] [代码] 我只对pageWrapCount数组push(1); 就算有1024KB限制,那也远远足够了,除了此种办法外,各位还有更好的办法么?欢迎共享经验
2018-05-25 - 小程序更改checkbox和radio默认样式
1、checkbox checkbox .wx-checkbox-input{ border-radius:50%; width:20px;height:20px; } checkbox .wx-checkbox-input.wx-checkbox-input-checked{ border-color:#F0302F !important; background:#F0302F !important; } checkbox .wx-checkbox-input.wx-checkbox-input-checked::before{ border-radius:50%; width:20px; height:20px; line-height:20px; text-align:center; font-size:15px; color:#fff; background:transparent; transform:translate(-50%, -50%) scale(1); -webkit-transform:translate(-50%, -50%) scale(1); } 2、radio radio .wx-radio-input{ border-radius:50%; width:20px;height:20px; } radio .wx-radio-input.wx-radio-input-checked{ border-color:#F0302F !important; background:#F0302F !important; } radio .wx-radio-input.wx-radio-input-checked::before{ border-radius:50%; width:20px; height:20px; line-height:20px; text-align:center; font-size:15px; color:#fff; background:transparent; transform:translate(-50%, -50%) scale(1); -webkit-transform:translate(-50%, -50%) scale(1); } 如果上面的代码对您有帮助,麻烦抖一抖小手点下赞,谢谢
2018-06-29 - 小程序整个支付流程接口(包含后台chuanshu乱码的处理)
1,获取:openid接口; ,2,完成整套支付流程接口 // 用code换取openId openIdUrl: `https://developers.weixin.qq.com/`, getOpenId: function (code) { var that = this; wx.request({ url: openIdUrl+"openid", data: { code: code }, method: 'GET', success: function (res) { console.log('返回openId') console.log(res.data) that.generateOrder(res.data.openid) }, fail: function () { // fail }, complete: function () { // complete } }) }, // 生成支付订单的接口 paymentUrl: openIdUrl+`payment`, /**生成商户订单 */ generateOrder: function (openid) { var that = this //统一支付 wx.request({ url: openIdUrl+'wxpay', method: 'GET', data: { openid: openid, total_fee: '5', // body: '支付测试', // attach: '真假酒水' body: 'zhifuceshi', attach: 'zhenjiajiushui' }, success: function (res) { console.log(res) var pay = res.data //发起支付 var timeStamp = pay[0].timeStamp; console.log("timeStamp:" + timeStamp) var packages = pay[0].package; console.log("package:" + packages) var paySign = pay[0].paySign; console.log("paySign:" + paySign) var nonceStr = pay[0].nonceStr; console.log("nonceStr:" + nonceStr) var param = { "timeStamp": timeStamp, "package": packages, "paySign": paySign, "signType": "MD5", "nonceStr": nonceStr }; that.pay(param) }, }) }, 注:中文乱码,tomcat 容器,配置 /* 支付 */ pay: function (param) { console.log("支付") console.log(param) wx.requestPayment({ timeStamp: param.timeStamp, nonceStr: param.nonceStr, package: param.package, signType: param.signType, paySign: param.paySign, success: function (res) { // success console.log("支付") console.log(res) wx.navigateBack({ delta: 1, // 回退前 delta(默认为1) 页面 success: function (res) { wx.showToast({ title: '支付成功', icon: 'success', duration: 2000 }) }, fail: function () { // fail }, complete: function () { // complete } }) }, fail: function (res) { // fail console.log("支付失败") console.log(res) }, complete: function () { // complete console.log("pay complete") } }) } 成功跳转版 // pages/pay/pay.js var app = getApp(); var openid; Page({ data: {}, onLoad: function (options) { // 页面初始化 options为页面跳转所带来的参数 }, /* 微信支付 */ wxpay: function () { var that = this //登陆获取code wx.login({ success: function (res) { console.log(res.code) //获取openid that.getOpenId(res.code) } }); }, getOpenId: function (code) { var that = this; wx.request({ url:openIdUrl+ "openid", data: { code: code }, success: function (res) { console.log('返回openId') console.log(res.data) that.generateOrder(res.data) }, fail: function () { // fail }, complete: function () { // complete } }) }, /**生成商户订单 */ generateOrder: function (openid) { var that = this //统一支付 wx.request({ url: openIdUrl+'wxpay', method: 'GET', data: { openid: openid, total_fee: '1', body: '50套亲测小程序源码', attach: '行业源码' }, success: function (res) { console.log(res) var pay = res.data //发起支付 var timeStamp = pay[0].timeStamp; console.log("timeStamp:" + timeStamp) var packages = pay[0].package; console.log("package:" + packages) var paySign = pay[0].paySign; console.log("paySign:" + paySign) var nonceStr = pay[0].nonceStr; console.log("nonceStr:" + nonceStr) var param = { "timeStamp": timeStamp, "package": packages, "paySign": paySign, "signType": "MD5", "nonceStr": nonceStr }; that.pay(param) }, }) }, /* 支付 */ pay: function (param) { console.log("支付") console.log(param) wx.requestPayment({ timeStamp: param.timeStamp, nonceStr: param.nonceStr, package: param.package, signType: param.signType, paySign: param.paySign, success: function (res) { // success //付款成功,这里可以写你的业务代码 console.log("支付") console.log(res) // 写入订单号,更新订单状态 // 下单成功,跳转到订单管理界面 wx.redirectTo({ url: "/pages/order-list/index" }); wx.navigateBack({ delta: 1, // 回退前 delta(默认为1) 页面 success: function (res) { wx.showToast({ title: '支付成功', icon: 'success', duration: 2000 }); }, fail: function () { // fail }, complete: function () { // complete } }) }, fail: function (res) { // fail console.log("支付失败") console.log(res) }, complete: function () { // complete console.log("pay complete") } }) } })
2018-07-03