From efe0162e5438388e692b0eb4ce8ee4fd5954b2f4 Mon Sep 17 00:00:00 2001 From: fanghl Date: Fri, 3 Nov 2023 11:42:21 +0800 Subject: [PATCH] Site updated: 2023-11-03 11:42:21 --- content.json | 2 +- css/main.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/content.json b/content.json index 0d25c9fb..8990d627 100644 --- a/content.json +++ b/content.json @@ -1 +1 @@ -{"meta":{"title":"Fanghongliang's blog","subtitle":"Science && Truth","description":"站在巨人的肩膀上","author":"fanghl","url":"http://fanghl.top","root":"/"},"pages":[{"title":"404","date":"2019-09-19T12:36:20.000Z","updated":"2023-11-03T03:29:00.555Z","comments":true,"path":"404/index.html","permalink":"http://fanghl.top/404/index.html","excerpt":"","text":"404"},{"title":"Abort","date":"2019-09-19T11:40:28.000Z","updated":"2023-11-03T03:29:00.562Z","comments":true,"path":"about/index.html","permalink":"http://fanghl.top/about/index.html","excerpt":"","text":"我为何而生 <伯特兰·罗素> 有三种情感,单纯而强烈,支配着我的一生:对爱情的渴望,对知识的追求,以及对人类苦难不可遏制的同情。这些感情如阵阵巨风,挟卷着我在漂泊不定的路途中东飘西荡,飞越苦闷的汪洋大海,直抵绝望的边缘。 我之所以追寻爱情,首先,爱情使人心醉神迷,如此美妙的感觉,以致使我时常为了体验几小时爱的喜悦,而宁愿献出生命中其它一切;其次,爱情可以解除孤独,身历那种可怕孤寂的人的战栗意识,会穿过世界的边缘,直望入冰冷死寂的无底深渊;最后,置身于爱的结合,我在一个神秘缩影中看到了圣贤与诗人们所预想的天堂。这正是我所追寻的,尽管它对于人类的生活或许太过美好,却是我的最终发现。 我也以同样的热情追求知识。我渴望理解人类的心灵,渴望知道星辰为何闪耀,我还试图领略毕达哥拉斯关于哪些数字在变迁之上保持着永恒的智慧。在这一方面,我取得了一点成果,但并不算多。 爱情与知识,尽其可能,引领着我通往天堂;然而怜悯总是把我带回现实。那些痛苦的呼唤在我内心深处回响。饥饿中的孩子,被压迫和折磨的人们,给子女造成重担的无助老人,以及孤独、贫穷和痛苦的整个世界,都是对人类理想生活的嘲讽。我渴望能减少这些不幸,但无能为力,这也是我的痛苦。 这就是我的一生。我发现人生是值得的;而且如果能够再有一次这样的机会,我会欣然接受。"},{"title":"分类","date":"2019-08-19T07:11:42.000Z","updated":"2023-11-03T03:29:00.562Z","comments":false,"path":"categories/index.html","permalink":"http://fanghl.top/categories/index.html","excerpt":"","text":""},{"title":"标签","date":"2019-09-19T11:49:32.000Z","updated":"2023-11-03T03:29:00.562Z","comments":true,"path":"tags/index.html","permalink":"http://fanghl.top/tags/index.html","excerpt":"","text":""}],"posts":[{"title":"electron","slug":"electron","date":"2023-02-13T06:13:23.000Z","updated":"2023-11-03T03:29:00.557Z","comments":true,"path":"2023/02/13/electron/","link":"","permalink":"http://fanghl.top/2023/02/13/electron/","excerpt":"","text":"12 Electron本篇文章将结合官方文档以及实际线上每天万人使用的一款开播工具源码片段综合阐述下Electron 的开发经验和技巧 Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。 嵌入 Chromium 和 Node.js 到 二进制的 Electron 允许您保持一个 JavaScript 代码代码库并创建 在Windows上运行的跨平台应用 macOS和Linux——不需要本地开发经验。 开发环境 您需要安装 node.js 建议安装最新的LTS版本 因为 Electron 将 Node.js 嵌入到其二进制文件中,你应用运行时的 Node.js 版本与你系统中运行的 Node.js 版本无关。 Electron 应用程序遵循与其他 Node.js 项目相同的结构。 首先创建一个文件夹并初始化 npm 包。 12mkdir my-electron-app && cd my-electron-appnpm init init初始化命令会提示您在项目初始化配置中设置一些值 为本教程的目的,有几条规则需要遵循:. entry point 应为 main.js.. author 与 description 可为任意值,但对于应用打包是必填项。这里官网只是作为一个demo来规划文件结果,实际开发环境中,Electron应用很可能是在原有业务的网页版项目中 ,比如一个网页版直播页面,使用React + next 实现。现在要实现开播工具桌面端饮用,一般直接选择在原有的项目文件中直接新建文件夹开始,那么入口文件很可能不是根目录下 main.js,这点我们通过 package.json 的配置 main: ‘xxx’可以解决. 至于说 Electron 是 “Web网页” 套了桌面端的壳,那为什么我们还要使用 Electron呢? 因为Web应用无法拿到操作系统的权限,这对于我们解决一些问题十分关键。 12345// package.json{ \"description\": \"www.2339.com\", \"main\": \"app/main.js\", // 这里配置了项目的入口文件} 按装 Electron包 并创建执行脚本 1234567891011121314yarn add --dev electron// 配置 package.json 4种环境,一般学习只需要配置一种即可,打包命令后续也在这里配置{ \"scripts\": { \"electron:dev\": \"electron .\", \"electron:staging\": \"cross-env NODE_ENV=development ELECTRON=1 MODE=staging nextron -p 9401 .\", \"electron:grey\": \"electron .\", \"electron:production\": \"electron .\", }}// 命令启动 dev 环境yarn electron:dev 运行主进程任何 Electron 应用程序的入口都是 main 文件。 这个文件控制了主进程,它运行在一个完整的Node.js环境中,负责控制您应用的生命周期,显示原生界面,执行特殊操作并管理渲染器进程(稍后详细介绍)。执行期间,Electron 将依据应用中 package.json配置下main字段中配置的值查找此文件,您应该已在应用脚手架步骤中配置。 创建页面在可以为我们的应用创建窗口前,我们需要先创建加载进该窗口的内容。 在Electron中,各个窗口显示的内容可以是本地HTML文件,也可以是一个远程url。 在窗口中打开您的页面现在您有了一个页面,将它加载进应用窗口中。 要做到这一点,你需要 两个Electron模块: . app 模块,它控制应用程序的事件生命周期。. BrowserWindow 模块,它创建和管理应用程序 窗口。因为主进程运行着 Node.js,您可以在 main.js 文件头部将它们导入作为 CommonJS 模块: 专有名词 窗口 预加载脚本 渲染器 主进程每个 Electron 应用都有一个单一的主进程,作为应用程序的入口点。 主进程在 Node.js 环境中运行,这意味着它具有 require 模块和使用所有 Node.js API 的能力主进程的主要目的是使用 BrowserWindow 模块创建和管理应用程序窗口。BrowserWindow 类的每个实例创建一个应用程序窗口,且在单独的渲染器进程中加载一个网页。 您可从主进程用 window 的 webContent 对象与网页内容进行交互。 应用程序生命周期主进程还能通过 Electron 的 app 模块来控制您应用程序的生命周期。 该模块提供了一整套的事件和方法,可以让您用来添加自定义的应用程序行为 (例如:以编程方式退出您的应用程序、修改应用程序坞,或显示一个关于面板) app 模块 BrowserWindow 模块BrowserWindow 类的每个实例创建一个应用程序窗口,且在单独的渲染器进程中加载一个网页。 您可从主进程用 window 的 webContent 对象与网页内容进行交互。由于 BrowserWindow 模块是一个 EventEmitter, 所以您也可以为各种用户事件 ( 例如,最小化 或 最大化您的窗口 ) 添加处理程序。当一个 BrowserWindow 实例被销毁时,与其相应的渲染器进程也会被终止。 CommonJS 模块 Preload 脚本预加载(preload)脚本包含了那些执行于渲染器进程中,且先于网页内容开始加载的代码 。 这些脚本虽运行于渲染器的环境中,却因能访问 Node.js API 而拥有了更多的权限。预加载脚本可以在 BrowserWindow 构造方法中的 webPreferences 选项里被附加到主进程。因为预加载脚本与浏览器共享同一个全局 Window 接口,并且可以访问 Node.js API,所以它通过在全局 window 中暴露任意 API 来增强渲染器,以便你的网页内容使用。 下面是渲染进程中选取用户操作系统文件中的一张图片即打开系统文件夹,选取一张图片 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191// 在 preload 脚本中增加全局消息处理机制 // preload.jswindow.electron = { message: { send: (payload) => { return ipcRenderer.send('message', payload); }, on: (handler) => { return ipcRenderer.on('message', handler); }, off: (handler) => { return ipcRenderer.off('message', handler); }, },};// lib/qtClient 资源文件夹// 封装一个全局公用方法const clientEvent = new EventEmitter();const inti = (cb) => { // PC项目和electron项目在同一工程中 if (!browser.electronClient) { console.info( '======================\\r\\n非 electron 环境\\r\\n======================', ); return; } // 监听主进程事件 window.electron.message.on((event, msg) => { if (msg.id) { clientEvent.emit(msg.id, msg); } else { clientEvent.emit(msg.action, msg); } clientEvent.emit('all', msg); }); cb && cb();}// call 消息封装const call = ({ action, data = {}, onSuccess = (msg) => {}, onFail = (msg) => {},}) => { // 同上,区分electron环境 if (!browser.electronClient) { return; } if (!action) { console.error('action is required'); return; } const options = { id: utils.getRandomString(), action, data, }; clientEvent.once(options.id, (msg) => { if (msg.code === 1) { onSuccess(msg); } else { onFail(msg); } }); if (typeof data === 'function') { options.data = {}; } if (action !== 'getSystemInfo') { log(`call【${action}】 options:`, options); } window.electron.message.send(options);};const qtClient = { init, call}export default call// 封装具体的业务需求-选取图片// app-client.jsconst selectImage = (onSuccess) => { qtClient.call({ action: 'selectImage', onSuccess, });};// 具体业务环境中使用// eg 主播开播合流时往视频区域添加一张图片const handleAddImage = () => { appClient.selectImage((msg) => { const { filePath, fileName, ext, width, height } = msg.data; // 返回有文件的具体信息,文件名、路径等 // 处理你的业务 // 比如: 把文件信息在通过则 zego 等三方传递出去,最终在直播流中成功添加一张图片或Gif })}// -----------------分割线--------------// 以上代码是渲染进程处理的事// 渲染进程向主进程发送了想要打开用户文件夹获取图片的消息, ipcRenderer// 以下将是主进程中监听渲染进程的消息,并作出处理 ipcMain// main.tsimport message from './message';message.init()// message.ts// 处理所有的主进程消息 // 打开文件夹,并选取图片文件 const selectImage = (event, message) => { dialog .showOpenDialog({ title: '选择图片', properties: ['openFile'], filters: [ { name: 'Images', extensions: ['jpg', 'png', 'jpeg', 'gif', 'bmp'] }, ], }) .then(async ({ canceled, filePaths, bookmarks }) => { if (filePaths.length) { const filePath = filePaths[0]; const { width, height } = imageSize(filePath); // const {fileTypeFromFile} = await import('file-type') // const fileType = await fileTypeFromFile(filePath); const [fileName, fileExt] = filePath .replace(/\\\\/gi, '/') .split('/') .pop() .split('.'); // const ext = fileType?.ext || fileExt; const ext = fileExt; responseSuccess(event, message, { data: { filePath: filePaths[0], fileName, width, height, ext: ext.toLowerCase(), }, }); } });};const actions = { selectImage,}const init = () => { ipcMain.on('message', (event, message) => { // event.sender.send('message', message); if (message.action !== 'getSystemInfo') { log.info('[ipcMain message]', message); } if (actions[message.action]) { actions[message.action](event, message); } else { log.warn('action not found', message.action); } });}; 至此,一个完善的渲染进程-主进程通信框架就搭建完毕,后续其他的通信需求直接扩展上即可。 虽然预加载脚本与其所附着的渲染器在共享着一个全局 window 对象,但您并不能从中直接附加任何变动到 window 之上,因为 contextIsolation 是默认的。语境隔离(Context Isolation)意味着预加载脚本与渲染器的主要运行环境是隔离开来的,以避免泄漏任何具特权的 API 到您的网页内容代码中。 取而代之,我们將使用 contextBridge 模块来安全地实现交互 darwinDarwin 是MacOSX 操作环境, 即苹果电脑的操作系统 打包工具electron-builder CLI命令行接口 devDependencies开发环境需要的额外依赖,您的应用需要运行 Electron API,因此这听上去可能有点反直觉。 实际上,打包后的应用本身会包含 Electron 的二进制文件,因此不需要将 Electron 作为生产环境依赖。 原生 API为了使 Electron 的功能不仅仅限于对网页内容的封装,主进程也添加了自定义的 API 来与用户的作业系统进行交互。 Electron 有着多种控制原生桌面功能的模块,例如菜单、对话框以及托盘图标。","categories":[{"name":"code","slug":"code","permalink":"http://fanghl.top/categories/code/"}],"tags":[{"name":"electron","slug":"electron","permalink":"http://fanghl.top/tags/electron/"}]},{"title":"直播相关Live【hybrid】","slug":"page","date":"2022-01-01T08:46:39.000Z","updated":"2023-11-03T03:29:00.560Z","comments":true,"path":"2022/01/01/page/","link":"","permalink":"http://fanghl.top/2022/01/01/page/","excerpt":"","text":"序言直播间一些复杂功能的实现和总结,包括但不限于 IM 及时通讯消息、融云、融信 IM 私聊消息、推拉流、WebSocket 推送、Hybrid 与 H5 的桥接通信、mobx-state-tree | React 重构 远古 JQ 代码、 PC 直播间深度链接至移动端、用户极验证(验证非脚本或机器人)、进场特效、用户头像挂坠、炫彩昵称、用户财富、等级、身份标签组件化、拖拽等组件应用、 动画SVGASvga 是常见的一种直播间动画播放格式,其特性这里不做过多解释,今天总结一下在 H5 端 和 PC 端 播放 Svga 动画的全过程。封装公用方法以及优化、避坑。 动画播放在用户送礼、礼物预览等场景下使用频繁,封装一个公用方法在项目 lib 十分必要。这里使用 【svgaplayerweb】 封装方法: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172export function retryPromise(promiseFn, retriesLeft = 5, interval = 1500) { return new Promise((resolve, reject) => { promiseFn() .then(resolve) .catch((error) => { setTimeout(() => { if (retriesLeft === 1) { reject(error); return; } retryPromise(promiseFn, retriesLeft - 1, interval).then( resolve, reject ); }, interval); }); });}/** * 加载svga动画 * @param {String} id dom-id * @param {src} src svga文件路径 * @param {Number} loops 循环次数,默认为0无限循环 * @param {Function} onFinished 完成回调 * @param {Boolean} isPlayNow 是否立即播放,默认true */const svgaModules = {};export function svgaPlayer(option) { const { id, src, loops = 0, onFinished, clearsAfterStop = true, fillMode = \"Backward\", isPlayNow = true, } = option; if (!id) { return; } const play = () => { const { svga } = svgaModules; const player = new svga.Player(`#${id}`); const parser = new svga.Parser(`#${id}`); parser.load(src, (videoItem) => { player.loops = loops; player.clearsAfterStop = clearsAfterStop; player.fillMode = fillMode; player.setVideoItem(videoItem); player.onFinished(() => { if (onFinished) { onFinished(); } }); isPlayNow && player.startAnimation(); }); }; if (svgaModules.svga) { play(); return; } retryPromise(() => import(\"svgaplayerweb\")).then((svga) => { svgaModules.svga = svga; play(); });} 以上封装可以满足常见的动画播放场景。 Tab 点击动画场景现在的 H5 交互越来越体现用户至上,在 tab 的点击上,设计师更想要用户点击 某个 tab 播放该 tab 的动画状态,常见的有点击 hybrid 页面根 tab,该 tab 会抖动动画或者无衔接播放一个小动画,这个设计师给的 svga 动画,实现只需要上述公用方法 【isPlayNow】 参数,默认只让动画加载而不立即播放,展示第一帧,效果和静态 tab 一样,待点击时在 修改 【isPlayNow】 为 true,播放动画即可实现上述效果。 VAPvap [video-animation-player] 是腾讯企鹅电竞推出的开源 mp4 播放库,具体移步 GitHub 仓库,该库适配三端,这里只总结下 Web 端 封装公用方法: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475// vap-player.jsimport Vap from \"video-animation-player\";import { request } from \"lib\";let vap = null;const play = (opt) => { let { container, src, configUrl, imgUser, textUser, width = \"100%\", height = \"100%\", fontStyle, loop = false, fps = 24, mute = true, precache = false, accurate = false, onStop = () => {}, } = opt; const fn = (config) => { if (vap) { vap.destroy(); } vap = new Vap( Object.assign( {}, { container, src, config, width, height, fontStyle, imgUser, textUser, loop, fps: config.info.fps || fps, precache, mute, accurate, } ) ) .on(\"error\", (e) => { vap.destroy(); onStop(); }) .on(\"ended\", () => { vap.destroy(); onStop(); }); }; request.api.get(configUrl).then((res) => { fn(res.data); });};const destroy = () => { if (vap) { vap.destroy(); }};export default { play, destroy };// 引用import VapPlayer from \"lib\";VapPlayer.play(options); 使用 vap 在部分机型导致动画效果模糊锯齿的解决办法: Dom 容器的宽高扩大 400%,再缩小 ‘transform: scale(0.25);’ 缩小四倍。 Chrome不支持obs虚拟摄像头解决方法直播平台一般都会有自己对应的开播工具,比如YY、虎牙助手、OBS等。采用OBS采集视频流,会使用虚拟摄像头,Chrome有时候会不支持虚拟摄像头,解决办法为: 浏览器默认未允许虚拟摄像头的使用,在出Chrome的设置中打开对应的隐私配置即可,具体步骤可以Google。 IM 及时通讯消息待整理 推拉流dsBridge 桥接通信socket 监听发布CSS炫彩昵称五颜六色的炫彩昵称CSS实现 1234<!-- html --><WrapColorName> <div className=\"name\">用户昵称哇哈哈</div></WrapColorName> 1234567891011121314151617181920212223242526272829/* 这里使用 styled-components 写法,可以改成其他CSS框架写法,语法不变 */const maskedAnimation = keyframes` from { background-position: 0 0; } to { background-position: -200% 0; }`;const WrapColorName = styled.div` background-image: -webkit-linear-gradient( left, #f70000, #ff891c 14.3%, #ffe719 28.6%, #33e97c 43%, #1dd5ff 57%, #ec80ff 71.4%, #ff43dc 86%, #ff43cb 100% ); -webkit-text-fill-color: transparent; -webkit-background-clip: text; -webkit-background-size: 200% 100%; -webkit-animation: ${maskedAnimation} 3s infinite linear;` 上面的demo 可以直接在本地跑,主要利用 webkit-background-size 的位置偏移和 webkit-animation 背景光束实现炫彩动画文案,上述案例可以直接在 演示地址 查看","categories":[{"name":"默认","slug":"默认","permalink":"http://fanghl.top/categories/默认/"}],"tags":[{"name":"默认","slug":"默认","permalink":"http://fanghl.top/tags/默认/"}]},{"title":"MST","slug":"MST","date":"2021-07-13T06:42:57.000Z","updated":"2023-11-03T03:29:00.556Z","comments":true,"path":"2021/07/13/MST/","link":"","permalink":"http://fanghl.top/2021/07/13/MST/","excerpt":"","text":"序言React的组件化开发确实对于大型项目非常有用,大型项目的数据状态管理写乱了也是头疼和难以维护的,这里记录一下项目中使用的状态管理MST(mobx-state-tree) 预备知识 MobX:这是MST的核心,MST中存储的响应式“状态”都是MobX的Observable React:使用React来测试MST的功能非常简单 TypeScript:后文中会使用TS来编写示例代码,TS强大的智能提示和类型检查,有助于快速掌握MST的API 安装MST依赖MobX。项目中执行yarn add mobx mobx-state-tree即可完成安装。MobX有两个版本,新版本需要浏览器Proxy支持,一些老旧的浏览器并不支持,需要兼容老浏览器的请安装mobx@4:yarn add mobx@4 mobx-state-tree。 结构 使用MST来维护状态,首先需要让MST知道,这个状态的结构是什么样的。MST内建了一个类型机制。通过类型的组合就可以定义出整个状态的形状。并且,在开发环境下,MST可以通过这个定义好的形状,来判断状态的值和形状与其对应的类型是否匹配,确保状态的类型与预期一致,这有助于在开发时及时发现数据类型的问题: MST提供的一个重要对象就是types,在这个对象中,包含了基础的元类型(primitives types),如string、boolean、number,还包含了一些复杂类型的工厂方法和工具方法,常用的有model、array、map、optional等。model是一个types中最重要的一个type,使用types.model方法得到的就是Model,在Model中,可以包含多个type或者其他Model。一个Model可以看作是一个节点(Node),节点之间相互组合,就构造出了整棵状态树(State Tree)。MST可用的类型和类型方法非常多,这里不一一列举,可以在这里查看完整的列表。完成Model的定义后,可以使用Model.create方法获得Model的实例。Model.create可以传入两个参数,第一个是Model的初始状态值,第二个参数是可选参数,表示需要给Model及子Model的env对象(环境配置对象),env用于实现简单的依赖注入功能。 PropsViewsviews是Model中一系列衍生数据或获取衍生数据的方法的集合,类似Vue组件的computed计算属性。 需要注意的是,定义views时有两种选择,使用getter或者不使用。使用getter时,衍生数据的值会被缓存直到依赖的数据发送变化。而不使用时,需要通过方法调用的方式获取衍生数据,无法对计算结果进行缓存。尽可能使用getter,有助于提升应用的性能。 Actions从名字上可以看出来,上面四位都是生命周期方法,可以使用他们在Model的各个生命周期执行一些操作: 除了通常意义上用来更新状态的actions外,在model.actions方法中,还可以设置一些特殊的actions: afterCreateafterAttachbeforeDetachbeforeDestroy 从名字上可以看出来,上面四位都是生命周期方法,可以使用他们在Model的各个生命周期执行一些操作: 1234567const Model = types .model(...) .actions(self => ({ afterCreate () { // 执行一些初始化操作 } })); 异步Action、Flow异步更新状态是非常常见的需求,MST从底层支持异步action。 1234567891011121314151617181920const model = types .model(...) .actions(self => ({ // async/await async getData () { try { const data = await api.getData(); ... } catch (err) { ... } ... }, // promise updateData () { return api.updateData() .then(...) .catch(...); } })); 若使用Promise、async/await来编写异步Action,在异步操作之后更新状态时,代码执行的上下文会脱离action,导致状态在action之外被更新而报错。这里有两种解决办法: 将更新状态的操作单独封装成action 编写一个runInAction的action在异步操作中使用 123456789101112131415161718192021222324252627282930313233343536373839// 方法1const Model = types .model(...) .actions(self => ({ setLoading (loading: boolean) { self.loading = loading; }, setData (data: any) { self.data = data; }, async getData () { ... self.setLoading(true); // 这里因为在异步操作之前,直接赋值self.loading = true也ok const data = await api.getData(); self.setData(data); self.setLoading(false); ... } })); // 方法2const Model = types .model(...) .actions(self => ({ runInAction (fn: () => any) { fn(); }, async getData () { ... self.runInAction(() => self.loading = true); const data = await api.getData(); self.runInAction(() => { self.data = data; self.loading = false; }); ... } })); 方法1需要额外封装N个action,比较麻烦。方法2封装一次就可以多次使用。但是在某些情况下,两种方法都不够完美:一个异步action被分割成了N个action调用,无法使用MST的插件机制实现整个异步action的原子操作、撤销/重做等高级功能。为了解决这个问题,MST提供了flow方法来创建异步action: 1234567891011121314151617181920import { types, flow } from 'mobx-state-tree';const model = types .model(...) .actions(self => { const getData = flow(function * () { self.loading = true; try { const data = yield api.getData(); self.data = data; } catch (err) { ... } self.loading = false; }); return { getData }; }) 使用flow方法需要传入一个generator function,在这个生成器方法中,使用yield关键字可以resolve异步操作。并且,在方法中可以直接给状态赋值,写起来更简单自然。 快照 Snapshotsnapshot即“快照”,表示某一时刻,Model的状态序列化之后的值。这个值是标准的JS对象。 使用getSnapshot方法获取快照: 使用applySnapshot方法可以更新Model的状态: Volatile State在MST中,props对应的状态都是可持久化的,也就是可以序列化为标准的JSON数据。并且,props对应的状态必须与props的类型相匹配。如果需要在Model中存储无需持久化,并且数据结构或类型无法预知的动态数据,可以设置为Volatile State。 Volatile State的值也是Observable,但是只会响应引用的变化,是一个非Deep Observable。 选择正确的types类型types.string定义一个字符串类型字段。 types.number定义一个数值类型字段。 types.boolean定义一个布尔类型字段。 types.integer定义一个整数类型字段。注意,即使是TypeScript中也没有“整数”这个类型,在编码时,传入一个带小数的值TypeScript也无法发现其中的类型错误。如无必要,请使用types.number。 types.Date定义一个日期类型字段。这个类型存储的值是标准的Date对象。在设置值时,可以选择传入数值类型的时间戳或者Date对象。 types.null定义一个值为null的类型字段。 types.undefined定义一个值为undefined的类型字段。复合类型 types.model定义一个对象类型的字段。 types.array定义一个数组类型的字段。types.array(types.string);types.array(types.model); types.map定义一个map类型的字段。该map的key都为字符串类型,map的值都为指定类型。map可用set、 get进行取赋值 1234567891011121314151617181920export enum PopupType { Rule = '规则', Award = '奖励', Buy = '购买', Tip = '提示',}popup: types.map(types.boolean);const showPopup = (type: PopupType) => { self.popup.set(type: true)}const hidePopup = (type: PopupType) => { self.popup.set(type, false);};// 具体使用{popup.get(PopupType.Rule) && <PopupRule />} types.optional可选类型,根据传入的参数,定义一个带有默认值的可选类型。types.optional是一个方法,方法有两个参数,第一个参数是数据的真实类型,第二个参数是数据的默认值。types.optional(types.number, 1);上面的代码定义了一个默认值为1的数值类型。注意,types.array或者types.map定义的类型自带默认值(array为[],map为{}),也就是说,下面两种定义的结果是一样的: 1234567// 使用types.optionaltypes.optional(types.array(types.number), []);types.optional(types.map(types.number), {});// 不使用types.optionaltypes.array(types.number);types.map(types.number); 如果要设置的默认值与types.array或types.map自带的默认值相同,那么就不需要使用types.optional。 types.custom如果想控制类型更底层的如序列化和反序列化、类型校验等细节,或者根据一个class或interface来定义类型,可以使用types.custom定义自定义类型。 1234567891011121314151617181920class Decimal { ...}const DecimalPrimitive = types.custom<string, Decimal>({ name: \"Decimal\", fromSnapshot(value: string) { return new Decimal(value) }, toSnapshot(value: Decimal) { return value.toString() }, isTargetType(value: string | Decimal): boolean { return value instanceof Decimal }, getValidationMessage(value: string): string { if (/^-?\\d+\\.\\d+$/.test(value)) return \"\" // OK return `'${value}' doesn't look like a valid decimal number` }}); types.union实际开发中也许会遇到这样的情况:一个值的类型可能是字符串,也可能是数值。那我们就可以使用types.union定义联合类型: types.union(types.number, types.string);联合类型可以有任意个联合的类型。 types.literal字面值类型可以限制存储的内容与给定的值严格相等。比如使用types.literal(‘male’)定义的状态值只能为’male’。实际上,上面提到过的types.null以及types.undefined就是字面值类型: const NullType = types.literal(null);const UndefinedType = types.literal(undefined);搭配联合类型,可以这样定义一个性别类型:const GenderType = types.union(types.literal(‘male’), types.literal(‘female’)); types.enumeration枚举类型可以看作是联合类型以及字面值类型的一层封装,比如上面的性别可以使用枚举类型来定义:const GenderType = types.enumeration(‘Gender’, [‘male’, ‘female’]);方法的第一个参数是可选的,表示枚举类型的名称。第二个参数传入的是字面值数组。在TypeScript环境下,可以这样搭配TypeScript枚举使用: 123456enum Gender { male, female}const GenderType = types.enumeration<Gender>('Gender', Object.values(Gender)); types.maybe定义一个可能为undefined的字段,并自带默认值undefined。 123types.maybe(type)// 等同于types.optional(types.union(type, types.literal(undefined)), undefined) types.maybeNull与types.maybe类似,将undefined替换成了null。 123types.maybe(type)// 等同于types.optional(types.union(type, types.literal(null)), null) types.frozenfrozen意为“冻结的”,types.frozen方法用来定义一个immutable类型,并且存放的值必须是可序列化的。 当数据的类型不确定时,在TypeScript中通常将值的类型设置为any,而在MST中,就需要使用types.frozen定义。 12awardPosition: types.frozen(),notices: types.array(types.frozen()), 在MST看来,使用types.frozen定义类型的状态值是不可变的,所以会出现这样的情况: 12model.anyData = {a: 1, b: 2}; // ok, reactivemodel.anyData.b = 3; // not reactive 也就是只有设置一个新的值给这个字段,相关的observer才会响应状态的更新。而修改这个字段内部的某个值,是不会被捕捉到的!! types.late滞后类型有时候会出现这样的需求,需要一个Model A,在A中,存在类型为A本身的字段。 如果这样写 1234const A = types .model('A', { a: types.maybe(A), // 使用mabe避免无限循环 }); 会提示Block-scoped variable ‘A’ used before its declaration,也就是在A定义完成之前就试图使用他,这样是不被允许的 这个时候就需要使用types.late: 1234const A = types .model('A', { a: types.maybe(types.late(() => A)) }); types.late需要传入一个方法,在方法中返回A,这样就可以避开上面报错的问题。 types.refinement提纯类型 types.refinement可以在其他类型的基础上,添加额外的类型校验规则。 比如需要定义一个email字段,类型为字符串但必须满足email的标准格式,就可以这样做: 12345const EmailType = types.refinement( 'Email', types.string, (snapshot) => /^[a-zA-Z_1-9]+@\\.[a-z]+/.test(snapshot), // 校验是否符合email格式); 实操MST (mobx-state-tree),顾名思义是React用于管理状态的状态树结构,根据每个组件构建单独的状态树结构,一般建议状态树结构和接口或者推送保持一致的数据结构,便于更新维护。在请求接口和推送时,只需要更新对应的state,其他的页面级别的渲染等交给 observer 监听的组件。简单使用流程如下⬇️ {.line-numbers}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175// 1 - 封装useContext/*** * * 创建总的store文件夹 * ./store * ./store/index.ts * ./store/room.ts */// 1-1 ./store/index.tsimport { useContent, createContext } from 'react'import { types, Instance, onSnapshot, applySnapshot} from 'mobx-state-tree'import room from './room'const RootModel = types.modal('RootModel', { room: room.Modal, // 还可以扩展相关其他的store // users: users.Modal,}).action(self => { return {}})let rootStore;export const initStore = ({roomId, allRoomInfo}) => { const _rootStore = rootStore ?? RootModal.create({ room: assign({}, room.initStore, { roomId: roomId, allRoomInfo, }) //users: users.initState, }) if ( typeof window !== 'undefined' ) { window.__store = _rootStore } if (typeof window === 'undefined' ) return _rootStore if (!rootStore) { rootStore = _rootStore // onSnapshot(rootStore, snapshot => console.log(\"stage Snapshot: \", snapshot)); } return rootStore;}export type RootInstance = Instance<typeof RootStore>const RootStoreContext = createContext<null || RootInstance>(null);export const Provider = RootStoreContext.provider;export const useStore = () => { const store = useContext(RootStoreContext); if (store === null) { throw new Error('Store cannot be null, please add a context provider'); } return store;}export const getStore = () => { return rootStore;}// 创建具体store(room.ts)文件import { flow, types } from 'mobx-state-tree'import { getStore } from './index'const initState = { roomId: 0, allRoomInfo: {}}const PkModel = types.model('PkModel', {})const AllRoomInfoModel = types.model('AllRoomInfoModel', { _id: '', pk: types.maybeNull(PkModel)}).views(self => { return {}}).actions(self => { return {}})export const Model = types .model({ roomId: types.number, allRoomInfo: AllRoomInfoModel, }) .views(self => { return { get hasId() => { return self._id !== '' }, get isRoomOwner() { let userId = getGlobalStore().userId; return userId && userId === self.roomId; } } }) .volatile(self=> { return {} }) .actions(self => { const update = flow(function* () { let res; try { res = yield request.api.get('****/api/room/${self.roomID}') self.allRoomInfo = res.data } }), const setRoomInfo = (data) => { self.allRoomInfo = data }, const stopLive = () => { ... } return { update, setRoomInfo, stopLive, } })export default { initState, Modal,}// 3-组件应用import { observer } from 'mobx-react'import { useStore } from '../store'const BeHeadTip = observer(() => { const { room } = useStore() const { roomId, allRoomInfo, hasId} = room return ( !hsdId ? '当前房间未在开播哟~' : <StyledBeHead id={ roomId }>组件内容</StyledBeHead> )})const StyledBeHead = styled.div` position: relative;`// 4-接口、推送更新store// 4-1 接口const loadInfo = () => { request.api.get('**/api/info_v2').then(res => { setRoomInfo(res.data) })}export default { loadInfo }// 4-2 推送const handle = (socket) => { socket.on('room_info', (msg) => { setRoomInfo(res.data) })} 封装以上的创造store,可以简单封装下逻辑,让程序员更专注于model的构建 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778// utils/create-store.tsimport { Instance } from 'mobx-state-tree'import { createContext, useContext } from 'react'const createStore = ( Model: any, initialState: any ) => { type IStore = Instance<typeof Model> const store = Model.create(initialState) const StoreContext = createContext<null | IStore>(null) if (typeof window !== 'undefined') { if (window['__Act' + Model.name]) { window['__Act' + Model.name + random(100000, 10000)] = store } else { window['__Act' + Model.name] = store } } const useStore = ():IStore => { const currentStore = useContext(StoreContext) if (currentStore === null ) { throw new Error(`${Model.name} Store cannot be null, please add a context provider`) } return currentStore; } return { store, Provider: StoreContext.Provider, useStore, }}export default createStore;//具体文件使用// **/store.tsximport { createStore } from '@utils/create-store'export const Model = types .model('Model', { currentTab: '', list: [], }) .action((self) => { const setCurrTab = (tab: string) => { self.currentTab = tab } return { setCurrTab } })interface IModelSnapshotIn extend SnapshotIn<typeof Model>{}// const initState: IModelSnapshotIn = {}const initState = { currentTab: 'home', list: []}const { store, Provider, useStore } = createStore(Model, initState)export { store, Provider, useStore }// 页面引用store// **/index.tsximport { useStore } from './store'const { currentTab, setCurrTab } = useStore() 总结在整个程序执行中,我们只需要控制数据状态的更新,以及在MST中处理好数据的逻辑,暴露出直接可以使用的方法。例如 hasId, MST中的 views 相当于 Vue 中的 计算属性,根据依赖值的变化,计算最新的结果。数据的更新只能在 actions 中暴露的方法中去实现。在面对具有非常复杂状态的大型项目时,可以提高开发效率。","categories":[{"name":"默认","slug":"默认","permalink":"http://fanghl.top/categories/默认/"}],"tags":[{"name":"默认","slug":"默认","permalink":"http://fanghl.top/tags/默认/"}]},{"title":"么么直播-总结","slug":"summary-meme","date":"2021-06-07T11:25:24.000Z","updated":"2023-11-03T03:29:00.561Z","comments":true,"path":"2021/06/07/summary-meme/","link":"","permalink":"http://fanghl.top/2021/06/07/summary-meme/","excerpt":"","text":"序言简单记录一下在么么直播遇到的问题以及解决办法,项目技术栈跨度比较大,有 6-7 年前 jquery 项目,也有 React + MST(mobx state tree) + SSR 项目,还有纯 React 项目,遇到的问题比较广泛,记录一下常见的问题,帮助学习。 React 的更新机制Ref 的一些用法受控组建和非受控组件命名空间Canvas 实现弹幕组件 实现弹幕的核心是 Canvas 的 measureText()方法,该方法可以计算出画布上字体的宽度,由于弹幕的内容一般是由 相对固定的图片加未知长度的文案构成,渲染复杂的单条弹幕首先需要解决弹幕总长度,拿到了总长度,那么不管是总体的弹幕背景还是图片文案的未知都能 准确无误的渲染出来,React 可以把功能做成一个组件,一次完成,多次复用,这里我简单列举两种弹幕的实现,一种是普通的弹幕,构成是背景色 + 用户头像 + 相对固定的文案(比如抽奖弹幕,头像 + XXX 在 VVV 活动中 抽中了 AAAA x 99 次), 一种是特殊弹幕,比如春节期间产品上线了祈福送礼需求,用户发送祝福语,然后立即在屏幕上弹幕形式出现,每条祝福语弹幕的背景样式不同,🈶️ 新春对联、燕子高飞、柳树纸条等,切每个用户输入的祝福语长度取决于用户自己,有时候一条弹幕就几个字,有的有几十字,知道每条弹幕的长度有两个用处,一是弹幕的背景位置渲染,而是弹幕采用四行并存的形式,那么弹幕插入哪一行也取决于哪一行的弹幕稀疏程度,话不多少,上图上代码: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477// common 弹幕的引用文件export function getDevicePixelRatio(): number { // Fix fake window.devicePixelRatio on mobile Firefox const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1 if (window.devicePixelRatio !== undefined && !isFirefox) { return window.devicePixelRatio } else if (window.matchMedia) { const mediaQuery = (v: string, ov: string) => { return ( '(-webkit-min-device-pixel-ratio: ' + v + '),' + '(min--moz-device-pixel-ratio: ' + v + '),' + '(-o-min-device-pixel-ratio: ' + ov + '),' + '(min-resolution: ' + v + 'dppx)' ) } if (window.matchMedia(mediaQuery('1.5', '3/2')).matches) { return 1.5 } if (window.matchMedia(mediaQuery('2', '2/1')).matches) { return 2 } if (window.matchMedia(mediaQuery('0.75', '3/4')).matches) { return 0.7 } } return 1}// barrage-spring.tsximport { cancelAnimation } from '@utils/media'import { getDevicePixelRatio } from '@core/client'import { max } from '@utils/tool' const roundRect = function (ctx, left, top, width, height, r) { const pi = Math.PI; ctx.beginPath(); ctx.arc(left + r, top + r, r, -pi, -pi / 2); ctx.arc(left + width - r, top + r, r, -pi / 2, 0); ctx.arc(left + width - r, top + height - r, r, 0, pi / 2); ctx.arc(left + r, top + height - r, r, pi / 2, pi); ctx.closePath(); } const circleImg = function (ctx, img, l, t, r){ const d = 2 * r ctx.save(); ctx.beginPath() ctx.arc(l + r + 30, t + r - 3, r, 0, 2 * Math.PI) ctx.closePath() ctx.clip(); ctx.drawImage(img, l + 30, t - 3, d, d); ctx.restore(); } const leftBg = function (ctx, leftImg, left, top, width, height) { ctx.save() ctx.drawImage(leftImg, left, top, width, height) // height 94 ctx.restore(); } const middleBg = function (ctx, img, left, top, width, height) { ctx.save() ctx.drawImage(img, left+112, top, width, height) ctx.restore(); } const rightBg = function (ctx, img, left, top, width, height) { ctx.save() ctx.drawImage(img, left, top, width, height) ctx.restore(); } // const preImg = (url, callback, options) => { // const {x, y, w, h} = options // const img = new Image() // img.src = testLeft || url // if (img.complete) { // callback.call(img, x, y, w, h) // return // } // img.onload = () => { // callback.call(img, x, y, w, h) // } // } const getPxRatio = () => { const c = document.createElement(\"canvas\"), ctx = c.getContext(\"2d\"), dpr = getDevicePixelRatio() || 1, bsr = ctx['webkitBackingStorePixelRatio'] || ctx['mozBackingStorePixelRatio'] || ctx['msBackingStorePixelRatio'] || ctx['oBackingStorePixelRatio'] || ctx['backingStorePixelRatio'] || 1; return dpr / bsr; } export class Barrage { constructor(option) { const { canvasId, onPalyFinish } = option const pixelRatio = max([getPxRatio(), 2]) this.pxRatio = pixelRatio; // 缩放倍数,1会糊 this.canvas = document.getElementById(canvasId); this.canvas.width = this.w = this.canvas.width * pixelRatio; this.canvas.height = this.h = this.canvas.height * pixelRatio; this.drawing = true this.finishCount = 0 this.ctx = this.canvas.getContext('2d'); this.style = { // 弹幕样式 height: 47 * pixelRatio, // 弹幕高度 // 旧版高度29 imgBgWidth: 56 * pixelRatio, // 背景图片的宽度(左、右) fontSize: 12 * pixelRatio, // 字体大小 marginBottom: 20 * pixelRatio, // 弹幕 margin-bottom paddingX: 15 * pixelRatio, // 弹幕 padding x avatarWidth: 20 * pixelRatio, // 头像宽度 ellipsisMaxWidth: 100 * pixelRatio, offsetRight: 56 * pixelRatio // 右背景偏移量 } this.ctx.font = this.style.fontSize + 'px PingFangSC-Regular'; this.onPalyFinish = onPalyFinish this.barrageList = []; // 弹幕列表 this.rowStatusList = []; // 记录每行是否可插入,防止重叠。 行号为可插入 false为不可插入 let rowLength = Math.floor(this.h / (this.style.height + this.style.marginBottom)); for (var i = 0; i < rowLength; i++) { this.rowStatusList.push(i) } } shoot(value) { const { height, avatarWidth, fontSize, marginBottom, paddingX, ellipsisMaxWidth } = this.style; const { img, sortArr, t1, t2, t3, t4 } = value; const ellipsisT2 = this.getEllipsisText(t2) let row = this.getRow(); let color = '#7C0102'; let offset = this.pxRatio; let offsetNew = 30 let w_0 = paddingX; // 头像开始位置 let w_1 = w_0 + avatarWidth + 8 + offsetNew + 10; // t1文字开始位置 let w_2 = w_1 + Math.ceil(this.ctx.measureText(t1).width) + 8; // t2文字开始位置 let w_3 = w_2 + Math.ceil(this.ctx.measureText(ellipsisT2).width) + 8; // t3文字开始位置 let w_4 = w_3 + Math.ceil(this.ctx.measureText(t3).width) + 8; // t4文字开始位置 let w_5 = w_4 + Math.ceil(this.ctx.measureText(t4).width) + paddingX + 8; // 弹幕总长度 let barrage = { color, row, offset, top: row * (height + marginBottom), left: this.w, width: [w_0, w_1, w_2, w_3, w_4, w_5], value, ellipsisT2, } this.barrageList.push(barrage); } draw() { if (!this.drawing) { return } if (!!this.barrageList.length) { this.ctx.clearRect(0, 0, this.w, this.h); for (let i = 0, barrage; barrage = this.barrageList[i]; i++) { // 弹幕滚出屏幕,从数组中移除 if (barrage.left + barrage.width[5] <= -25) { this.barrageList.splice(i, 1); this.finishCount ++; i--; continue; } // 弹幕完全滚入屏幕,当前行可插入 if (!barrage.rowFlag) { if ((barrage.left + barrage.width[5]) < this.w - 45) { // this.rowStatusList[barrage.row] = barrage.row; barrage.rowFlag = true; } } barrage.left -= barrage.offset; this.drawBarrage(barrage); } } this.reqAnimeId = requestAnimationFrame(this.draw.bind(this)); } restartDraw() { this.drawing = true; this.draw() } clearDraw() { this.drawing = false cancelAnimation(this.reqAnimeId) } drawBarrage(barrage) { const { height, avatarWidth, fontSize, ellipsisMaxWidth } = this.style; const { value: { img, sortArr, t1, t3, t4,}, ellipsisT2, color, row, left, top, offset, width, } = barrage; // 画框子 // roundRect(this.ctx, left, top, width[5], height, height / 2, avatarWidth) // this.ctx.fillStyle = 'rgba(0,0,0,0.45)'; // this.ctx.fill(); // -- 画左边背景 leftBg(this.ctx, sortArr[0], left , top, this.style.imgBgWidth, height) middleBg(this.ctx, sortArr[1], left , top, width[2]-width[1], height) rightBg(this.ctx, sortArr[2], left + width[5]- this.style.offsetRight , top, this.style.imgBgWidth, height ) // left, top, width[1], height // 画头像 // circleImg(this.ctx, img, left + width[0], top + (height - avatarWidth) / 2, avatarWidth/2) circleImg(this.ctx, sortArr[3], left + width[0], top + (height - avatarWidth) / 2, avatarWidth/2) // 新的top偏移量 15 const offsetYNew = -4 const paddingTop = (height - fontSize) / 2 - 2 this.ctx.fillStyle = color; this.ctx.fillText(t1, left + width[1], top + fontSize + paddingTop + offsetYNew); this.ctx.fillStyle = '#CFFCFC'; this.ctx.fillText(ellipsisT2, left + width[2], top + fontSize + paddingTop); this.ctx.fillStyle = color; this.ctx.fillText(t3, left + width[3], top + fontSize + paddingTop); this.ctx.fillStyle = '#FFFF33'; this.ctx.fillText(t4, left + width[4], top + fontSize + paddingTop); } getRow() { let emptyRowList = this.rowStatusList.filter(d => /\\d/.test(d)); // 找出可插入行 let row = emptyRowList[Math.floor(Math.random() * emptyRowList.length)]; // 随机选一行 this.rowStatusList[row] = false; return row; } haveEmptyRow() { let emptyRowList = this.rowStatusList.filter(d => /\\d/.test(d)); // 找出可插入行 return !!emptyRowList.length; } getEllipsisText(text) { const { ellipsisMaxWidth: maxWidth } = this.style if (this.ctx.measureText(text).width <= maxWidth) { return text } const textArr = text.split('');//当前剩余的字符串 for (let m = 1; m <= textArr.length; m++) { if (this.ctx.measureText(textArr.slice(0, m)).width > maxWidth) { return textArr.slice(0, m).join('') + '...' } } } } // danmu.tsx 弹幕组件,可以直接调用 <danmu /> import React, { useEffect, useRef } from 'react' import styled from 'styled-components' import { defAvatarNew } from '@constants' import request from '@core/request' // import { Barrage } from '@utils/barrage' import { Barrage } from '@pages/act_spring_festival/barrage-spring' import { choice } from '@utils/tool' import { actions } from './config' export default (props: any) => { const storeRef = useRef<any>({ timer: 0, finishCount: 0, barrage: null, }) useEffect(() => { fetchData() }, []) useEffect(() => { const barrage = new Barrage({ canvasId: 'act-spring-barrage' }) barrage.draw() storeRef.current.barrage = barrage return () => { barrage.clearDraw() clearTimeout(storeRef.current.timer) } }, []) const fetchData = () => { request(actions.wishList).then((resData) => { const items = resData || [] if (items && items[0]) { shootBarrage(items[0]) setTimeout(() => shootBarrage(items[1]), 500) startBarrage(2, items) } }) } const startBarrage = (activeIndex: number, source: any[]) => { const { timer, barrage } = storeRef.current clearTimeout(timer) storeRef.current.timer = setTimeout(() => { let flag = false if (source[activeIndex]) { flag = shootBarrage(source[activeIndex]) } if (barrage.finishCount && barrage.finishCount >= source.length - 3) { barrage.finishCount = 0 fetchData() } startBarrage(!flag ? activeIndex : activeIndex + 1, source) }, choice([1000, 1800])) } const shootBarrage = (currentItem: any) => { const barrage = storeRef.current.barrage if (!barrage.haveEmptyRow() || !currentItem) { return false } const { pic = defAvatarNew, wish = '' } = currentItem const data = { t1: wish, t2: '', t3: '', t4: '', } // --一起初始化背景图 const imgConf1 = [ 'https://img.sumeme.com/28/4/1612232722204.png', 'https://img.sumeme.com/8/0/1612232705544.png', 'https://img.sumeme.com/14/6/1612232681614.png', pic, ] const imgConf2 = [ 'https://img.sumeme.com/32/0/1612232775520.png', 'https://img.sumeme.com/32/0/1612232762208.png', 'https://img.sumeme.com/54/6/1612232743414.png', pic, ] const imgConf3 = [ 'https://img.sumeme.com/27/3/1612232818395.png', 'https://img.sumeme.com/48/0/1612232804656.png', 'https://img.sumeme.com/32/0/1612232789280.png', pic, ] // const imgConf4 = [ // 'https://img.sumeme.com/25/1/1612232866777.png', // 'https://img.sumeme.com/32/0/1612232846432.png', // 'https://img.sumeme.com/33/1/1612232833441.png', // pic, // ] const imgConfAll = [imgConf1, imgConf2, imgConf3] const imgArray = choice(imgConfAll) const receiveArray: any[] = [] // let $myContent = document.getElementById(\"myContent\"); // let [imgW, imgH] = [300, 300]; // let Canvas = document.createElement('canvas'); // let ctx = Canvas.getContext(\"2d\"); // let scaleBy = 2; // Canvas.width = imgW * scaleBy; // Canvas.height = imgH * scaleBy; imgArray.forEach((e: any, idx: number) => { const img = new Image() img.src = e img.setAttribute('crossOrigin', 'Anonymous') img.addEventListener('load', () => { // ctx.drawImage(img, 0, 0, imgW * scaleBy, imgH * scaleBy); img.id = 'img' + idx receiveArray.push(img) // 将绘制的img节点收集到数组里,这里的顺序可能和imgArray的顺序不一样 if (receiveArray.length === imgArray.length) { // 所有图片load并绘制完成 const sortArr = new Array() receiveArray.forEach((ex) => { // 将所有绘制图片按imgArray顺序排序 sortArr[ex.id.split('img')[1]] = ex }) barrage.shoot({ sortArr, ...data, }) // sortArr.forEach(ex2 => { // $myContent.appendChild(ex2) // }) } }) }) // const img = new Image() // img.setAttribute('crossOrigin', 'anonymous') // const data = { // t1: wish, // t2: '', // t3: '', // t4: '', // } // img.onload = () => { // barrage.shoot({ // img, // ...data, // }) // } // img.onerror = () => { // barrage.finishCount++ // } // let pic1 = 'https://img.sumeme.com/27/3/1611904142299.png' // console.log('---', pic) // img.src = pic1 return true } return ( <StyledBarrage> <canvas id=\"act-spring-barrage\" height=\"250px\" /> </StyledBarrage> ) } export const StyledBarrage = styled.div` position: absolute; bottom: -120px; width: 750px; height: 550px; /* z-index: 99999; */ canvas { width: 100%; height: 100%; } ` 抽空把这个弹幕写个 demo ,光干巴巴的文字是在难以理解啊 Node 的版本控制 这个一般使用 nvm 或者 n 命令 移动端和 H5 的桥接通信 使用 jsBridge 进行 H5 和移动端的通信。具体后面整理一下。 直播礼物的动画播放队列实现Video 播放 mp4 的注意点 react 中播放 mp4 格式,会有一些 iOS 机型的兼容问题,不如 iOS 不能自动播放等 12345678910111213141516171819202122//React中播放mp4的情况,一帮情况下播放GIF或者SVGA// 代码如下<div className=\"video-box\" dangerouslySetInnerHTML={{ __html: ` <video id=\"entry-video\" poster=\"https://img.sumeme.com/16/0/1624613307152.png\" autoPlay x-webkit-airplay=\"allow\" x5-video-player-type=\"h5\" webkit-playsinline playsinline muted style=\"object-fit:fill\" > <source src=\"https://img.sumeme.com/swf/Render6-16.mp4\" type=\"video/mp4\"> </video> `, }}/> poster属性可以在视频未加载完成前展示一张封面图片,视频加载后自动播放视频。hooks 封装其实相比自己封装有针对性的 hooks 外,阿里的 ahooks3.0 也可以使用,功能还是值得期待的React 中挂载滑动函数抽奖一些 CSS12345678910111213141516171819202122232425262728-webkit-tap-highlight-color: rgba(0,0,0,0)// 解决iOS和iPad设备上点击状态出现默认蓝色高亮,很常见& + & {}取巧,选择非第一个开始的所有同类型元素font-size: 0;父元素设置改属性可以有效解决行内元素的默认间距,例如spanpadding-bottom: 6%;ol li:before { content:counter(sectioncounter) "、"; counter-increment:sectioncounter;}//有序元素的符号替换展示.gift-show-area[gift-id='7777'] {}//可以这样查找元素background-image: linear-gradient(135deg, red, blue);background-clip: text;-webkit-background-clip: text;color: transparent;// 文字颜色渐变::-webkit-input-placeholder// placeholder样式次修改 复杂表格项合并rowSpan colspan 行列合并 按需加载优化项目已经很庞大的情况下,还要考虑hybrid的体验情况,需要进行项目优化,按需加载比较适合某些场景下,这里采用《react-intersection-observer》中的hooks useInView 来判断元素是否在可视窗口内。进而判断是否渲染该元素。 123456789101112131415161718192021222324252627282930313233// 这里封装一个图片按需加载的公用组建import React from 'react'import { useInView, IntersectionOptions } from 'react-intersection-observer'import styled from 'styled-components' const StyledImg = styled.div` width: 100%; height: 100%;`type Props = IntersectionOptions & { src: string title?: string className?: string children?: any}const LazyImg = (props: Props) => { const { src, title, className, children, ...configProps } = props const [res, inView] = useInView({ triggerOnce: true, rootMargin: '20px 0', ...configProps, }) return ( <StyledImg ref={ref} className={className} title={title}> {children} {inView && <img src={src}/>} </StyledImg> )}export default LazyImg Modal封装不论是PC页面,还是hybrid原生页面,都需要大量形形色色的弹窗来通知用户处理业务逻辑,封装几种常见的Modal CSS 点九图最近年中和周年庆开始,铺天盖地的活动。UI 设计的风格和一往不太一样,举一个栗子: 在投票页面中,每个被投票的主播都是单独的一张特殊背景图包裹,该容器可能会根据被投票人的信息长短不一,不规则背景边框图也要自动适应。类似这样的需求,一般有这么几种方法实现: 三段图重复就是把不规则的背景图切成三段。头部、中间部分、底部,中间部分利用背景图的 repeat 来自适应,缺点就是不灵活,需要找 UI 切图,里面内容的间距控制不精准 点九图点九图是移动端的一种做法,就是一张图切四刀,四个角不伸缩,保持原图比例。四条边进行伸缩,中间的部分用来填充,一共九个部分,所以称点九图。CSS3 也可以实现点九图,且效果不错,举个例子:写一个业务组件,只用来做 wrap 包裹,用点九图,这样其他的同样式的组件都可以复用。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091import React from 'react'import styled from 'styled-components'import { StyledBaseWrap } from '../styled'type Props = { children: any title?: string // title?: string // headerType?: 'icon' | 'pureString'}export default (props: Props) => { const { children, title = '' } = props if (!children) { return null } return ( <StyledBaseWrap> <i className=\"bg\" /> { title && ( <div className=\"title-bg-box\"> <i className=\"title-bg\" /> {/* <p className=\"you-she\">{title}</p> */} <div className=\"you-she\">{title}</div> </div> ) } {children} </StyledBaseWrap> )}const StyledBaseWrap = styled.div` position: relative; width: 680px; margin: 0 auto; min-height: 320px; & > * { margin: 0 auto; } .bg { position: absolute; left: 0; right: 0; top: 0; bottom: 0; display: block; border-style: solid; border-width: 130px 340px 190px 340px; border-image-source: url(${require('./images/bg_wrap.png')}); border-image-slice: 130 340 190 340 fill; border-image-width: 1; border-image-repeat: repeat; } .title-bg-box { position: absolute; min-width: 380px; top: -20px; margin: 0 auto; font-size: 32px; left: 50%; transform: translateX(-50%); padding: 0 100px; box-sizing: border-box; div { position: relative; height: 52px; line-height: 52px; white-space: nowrap; } } .title-bg { position: absolute; left: 0; right: 0; top: 0; bottom: 0; display: block; border-style: solid; border-width: 24px 172px 24px 172px; border-image-source: url(${require('./images/title_bg.png')}); border-image-slice: 24 172 24 172 fill; border-image-width: 1; border-image-repeat: repeat; margin: 0 auto; }` 使用 ‘border-image-slice’ 属性来完成点九图,它接受 4 个参数,分别在图片的上右下左切一刀,把图片分为 9 个部分,一中心,四个角,四个边。伸缩只会让边进行伸缩,所以需要调整切的位置,尽量在规则的地方下刀。此时,若父容器的宽高未给定,则完全由内容撑开宽高,上面栗子中,宽度做了限制,高度未限制,传入的 children 会撑开点九图组件的高度,做到每个子组件高度根据内容自适应,但整体的样式不会发生变化。 Hybrid全面屏H5页面在iOS和安卓应用,通常是移动端给了屏幕空间,用来展示H5页面,不包含顶部电池栏tab,新的沉浸式体验则需要H5页面也要控制电池区域,铺满整个移动端屏幕。 每个手机设备的电池区域高度不尽相同,且设备的dpi也不一致,iOS是相对固定的22像素,安卓则是五花八门,这里需要桥接通信拿到移动端的“tab高度”和设备dpi,有这两个参数,H5页面则可以实现统一的全面屏幕沉浸式体验。 dpi :当前显示设备的物理像素分辨率与CSS像素分辨率之比,需要进行转化为H5的px单位,基本算法分为: 123456789101112131415// res 为桥接通信移动端返回的数据// statusBarHeight为状态栏移动端高度if (res.data.statusBarHeight) { if (client.instance.inIOsNative() ) { statusBarHeight.setValue(res.data.statusBarHeight) } else { statusBarHeight.setValue(res.data.statusBarHeight / window.devicePixelRatio) }}// react<HeaderBar style={{ paddingTop: `${statusBarHeight.value}px`, ...style }} className={`${className} `}> iOS的高度不需要额外转换,一般iOS机型返回都是22px,安卓则需要除以dpi得到CSS像素。 拿到最终的状态栏高度,进行app-header的布局,基本tab栏高度一般为 88 像素,再加上状态栏(电池栏)的高度,如果整个头部整体需要fixed布局,全局则增加padding-top 取巧实现。整个H5页面总体分为两个区域,tab栏和content内容页,一般tab栏使用纯色背景,内容页则有时候会使用渐变色,此时,content的高度无法确定,则整体页面使用 flex布局,tab栏使用 shrink: 0 ; 禁止缩放,content则使用 flex: 1; 自动填充满视口,这样,内容页的渐变色则和tab页无缝衔接。 hybrid touch bar判断js判断当前手机是否有touch bar,如果存在 touch bar ,则App全局头部添加样式类名,后续业务只需根据对应CSS标识处理不同的样式。 解决: iOS 手机屏幕底部存在白线(操作栏),会遮挡页面的一部分,常见的页面底部会存在用户点击按钮或其他UI,操作栏会降低用户的体验。判断iOS存在 touch bar ,则增加全局样式,在对应子业务中修改样式即可避免。 123456789101112131415<!-- App全局样式(app.js) -->if ( /iphone/gi.test(navigator.userAgent) && window.devicePixelRatio && window.devicePixelRatio >= 2 && window.screen.height >= 812) { document.querySelector('html').classList.add('fix-bottom');}<!-- 具体子业务 -->.fix-bottom & { padding-bottom: 60px;} iframe 跨域通信在对接一些第三方的游戏时,采用 App客户端 嵌入 Hybrid H5前端,前端对接第三方。比如,抖音直播间火起来的弹幕游戏和其他趣味游戏,在直播间接入这些三方游戏,在关闭游戏、支付等方面,涉及三端跨域通信。 这里有一个案例,游戏是第三方的,但游戏内充值兑换货币的页面是我们的。游戏最终需要在客户端、Web端、桌面端(window app, 主要是开播工具)三端展示。这里页面互相嵌套,但本质就是子父页面通信。 // 父页面监听单个事件 window.addEventListener('message', _handleMsg) const _handleMsg = (event) => { // doSomeThing } // 子页面发送事件 // window.parent.postMessage('你的参数, '*') 第二个参数即解决跨域问题,也可填写父窗口的域名 window.parent.postMessage('你的参数, 'https://0.0.0.9200') // window.parent 返回父窗口 // window.top 返回最顶层窗口,不一定是父窗口 const closeWebView = () => { window.parent.postMessage( { type: 'webViewEvent', source: 'xxx', event: 'close', }, '*', ); }; 自定义hooks 在手写较多的 useEffect 的时候,就应该抽一个 hooks 出来。 React的官网原话也有: 如果你发现自己经常需要手动编写 Effect,那么这通常表明你需要为组件所依赖的通用行为提取一些 自定义 Hook。","categories":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/categories/Programming/"}],"tags":[{"name":"么么","slug":"么么","permalink":"http://fanghl.top/tags/么么/"}]},{"title":"ES6","slug":"ES6","date":"2021-04-13T11:18:29.000Z","updated":"2023-11-03T03:29:00.555Z","comments":true,"path":"2021/04/13/ES6/","link":"","permalink":"http://fanghl.top/2021/04/13/ES6/","excerpt":"","text":"node版本管理不同项目交叉开发时,可能会出现node版本的冲突,常见有 nvm 插件来解决,但 nvm 并不是全自动,需要手动切换版本 nvm use … ,这里采用社区提供的 avn (avn)[https://github.com/wbyoung/avn],思路为: 项目根目录下创建 .node-version文件,以 server 格式约定好 Node 版本,如: 9.8.0,在CD到项目目录时,avn会自动切换到制定的Node版本。 n命令n命令来管理node的不同版本,基本为 123456789101112131415161718192021222324252627282930313233$ npm i -g n$ n ls-remote --all // 查看所有可安装的版本$ n <version> // 安装node,ex: n 10.15.0$ n ls // 本地已安装的版本sudo n run <version> // 使用某个版本n // 直接n 也可以选择版本sudo n rm <version> // 删除多余的版本``` 简单说下优缺点吧 优点: 方便,快速,相对avn来说, n命令安装方便,一条命令解决,avn则需要改IP地址或者科学上网等, 缺点: 这版本是全局且手动的,自己的项目可以用用,要是大型开发项目,建议用nvm编写 script 启动命令来控制版本,否则项目之间可能出现node版本冲突。 PS: n 命令大部分都需要使用 sudo 打开 ## 数组移除false类型 简单记录一个数组的移除 false 方法 ```javascriptconst arr = [1, "str", false, NaN, undefined, null]const res = arr.filter(Boolean)// Reactconst hideSomething ?: booleanconst options: any = [ hideSomething !== true && { otherOption: 'test' }, { normalOptions: 'normal' },].filter(Boolean) 解构附值log123const { log, warn } = consolelog('something')","categories":[{"name":"默认","slug":"默认","permalink":"http://fanghl.top/categories/默认/"}],"tags":[{"name":"默认","slug":"默认","permalink":"http://fanghl.top/tags/默认/"}]},{"title":"Amis","slug":"amis","date":"2021-02-24T12:11:31.000Z","updated":"2023-11-03T03:29:00.557Z","comments":true,"path":"2021/02/24/amis/","link":"","permalink":"http://fanghl.top/2021/02/24/amis/","excerpt":"","text":"Aims系列的高级用法Amis是百度开源的一套企业级管理系统,一个低代码前端框架,它使用 JSON 配置来生成页面,可以减少页面开发工作量,极大提升效率。 Ovine一个支持使用 Json 构建完整管理系统 UI 的框架,基于 Amis 二次开发。 相关文档依赖 ReactAmisstyled-componentimmerfont-awesome icon使用在Ovien项目中,icon的使用很简单也非常丰富,在[font-awesome](https://fontawesome.dashgame.com/)中选取适合你的icon名称即可 1icon: 'fa fa-${icon_name}'onFakeRequest应用amis对后端或者说中台转换依赖比较强,返回的API数据一般是后端组装好的JSON数据,直接渲染,但是对于其他后端不是很强大的项目来说,onFakeRequest应用就很有必要,它实现了假请求,在假请求里根据返回的数据拼接我们需要的schema,最后返回出去进行渲染,如下动态渲染 动态渲染动态渲染目前只能在 Service 容器中实现,核心思路是由后台返回需要的数据,再加以拼接返回一个Schema进行渲染而成。常见需求为: 某一个管理页面的行的每一项不是固定的,是根据其他配置页数据来渲染的,例如直播道具的使用情况,道具并不是事先就约定好的,是可配置的,想要渲染道具的使用数据表就得动态渲染表头。表头的数据接口只返回了简单的标识,未返回schema节点,我们在 onFakeRequest 里拼接schema返回渲染,具体看代码: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134schemaApi: {$preset: 'apis.tabConf',url: 'fakeRequest',onFakeRequest: async (source) => { // 假请求(Ovien实现) const confData = await app.request({ url: 'GET api/v1/apps/meme/hot-card', domain: 'flint', }) const cardConf = confData.data.data['hot-card']['hot-cards'] || {} const colConf = [ { name: 'nickname', label: '用户昵称(ID)', type: 'tpl', tpl: '${nickname}(ID:${_id})' } ] const selectConf = [] Object.keys(cardConf).map((key) => { const item = cardConf[key] const colOpt = { label: `${item['gift-name']}(未使用)` || '-', name: key, type: 'tpl', // tpl: `<%= (data.remain[${key}] || '-') + '/' + (data.total[${key}] || '-') %>`, tpl: `<%= (data.remain[${key}] || '/') %>`, } const selectOpt = { label: `${item['gift-name']} (${item['hot-value']}热度/张,生效${item.duration}分钟)`, value: key } selectConf.push(selectOpt) colConf.push(colOpt) return true }) // 二次弹窗内容 const retrieveCard = { api: { url: 'GET hotcard/del.json', data: { user_id: '$_id', gift_id: '$giftSelect', num: '$delNum', } }, type: 'form', horizontal: { left: 'col-sm-3', right: 'col-sm-8', }, controls: [ { type: 'select', label: '热度卡类型', name: 'giftSelect', required: true, options: selectConf, }, { type: 'number', name: 'delNum', label: '回收数量', description: '请输入每人回收热度卡的数量', required: true, // min: 1, precision: 0, }, ] } const retrieve: any = { type: 'action', label: '回收热度卡', level: 'danger', // visibleOn: '!data.status', actionType: 'dialog', dialog: { title: '回收热度卡', body: retrieveCard, }, } colConf.push(retrieve) const schemaNode = { type: 'lib-crud', syncLocation: false, // api: '$preset.apis.remainList', primaryField: '_id', perPageField: 'size', pageField: 'page', perPageAvailable: [50, 100, 200], defaultParams: { size: 50, }, api: { url: 'GET hotcard/remain.json' }, // source: '$rows', headerToolbar: [ { type: 'columns-toggler', align: 'left', }, // { // $preset: 'actions.add', // align: 'right', // }, ], footerToolbar: ['statistics', 'switch-per-page', 'pagination'], columns: colConf, filter: { type: 'form', title: '搜索', controls: [ { type: 'text', name: 'user_id', value: '', placeholder: '输入用户ID搜索', }, { type: 'submit', className: 'm-l', label: '搜索', }, ] }, } source.data = schemaNode return source}},","categories":[{"name":"默认","slug":"默认","permalink":"http://fanghl.top/categories/默认/"}],"tags":[{"name":"默认","slug":"默认","permalink":"http://fanghl.top/tags/默认/"}]},{"title":"React开发总结","slug":"React","date":"2020-11-11T08:33:03.000Z","updated":"2023-11-03T03:29:00.557Z","comments":true,"path":"2020/11/11/React/","link":"","permalink":"http://fanghl.top/2020/11/11/React/","excerpt":"","text":"传送门 React Mobx Mobx-state-tree React-Hooks Typescript VAP react-query SVGR polished immerjs 序言小程序和Vue要告一段落了,接下来要进行 React 、typescript、 Express 开发了。项目主要是一个2013年的直播项目,当时采用的是 Express \\ Ejs 实现的,现在V2版本一直陆续再往React、typescript方面重构。V2版本采用最新的React系列开发。知识点包括以上传送门等。 React","categories":[{"name":"React","slug":"React","permalink":"http://fanghl.top/categories/React/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"一些三方库","slug":"一些三方库","date":"2020-10-07T07:45:08.000Z","updated":"2023-11-03T03:35:02.534Z","comments":true,"path":"2020/10/07/一些三方库/","link":"","permalink":"http://fanghl.top/2020/10/07/一些三方库/","excerpt":"","text":"粘贴复制clipboard.js 现代化的拷贝文字,不依赖 flash, 不依赖其他框架,gzip 压缩后只有 3kb 大小 12yarn add clipboard npm install clipboard --save 123456789101112131415161718import ClipboardJS from 'lib/clipboard.min';useEffect(() => { const clipboard = new ClipboardJS('id'); clipboard.on('success', (e) => { UiToast.text('复制成功'); e.clearSelection(); }); clipboard.on('error', (e) => { UiToast.fail('复制失败'); # doSomeThing() }); return () => { clipboard.destroy(); };}, [codeStr.value]); React 拖拽","categories":[{"name":"三方库","slug":"三方库","permalink":"http://fanghl.top/categories/三方库/"}],"tags":[{"name":"React","slug":"React","permalink":"http://fanghl.top/tags/React/"}]},{"title":"Vue","slug":"Vue","date":"2020-09-27T09:07:25.000Z","updated":"2023-11-03T03:29:00.557Z","comments":true,"path":"2020/09/27/Vue/","link":"","permalink":"http://fanghl.top/2020/09/27/Vue/","excerpt":"","text":"Vue响应式原理Vue的数据响应式原理主要基于 Object.defineProperty() 函数来劫持数据变化,以及使用发布-订阅者模式达到更新数据的做法。首先要设置一个监听器Observer,用来监听所有的属性,当属性变化时,就需要通知订阅者Watcher,看是否需要更新.因为属性可能是多个,所以会有多个订阅者,故我们需要一个消息订阅器Dep来专门收集这些订阅者,并在监听器Observer和订阅者Watcher之间进行统一的管理.以为在节点元素上可能存在一些指令,所以我们还需要有一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令初始化成一个订阅者Watcher,并替换模板数据并绑定相应的函数,这时候当订阅者Watcher接受到相应属性的变化,就会执行相对应的更新函数,从而更新视图. 整理上面的思路,我们需要实现三个步骤,来完成双向绑定: 实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。 实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。 实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。 let {log} = console function defineReactive(data,key,val) { observe(val); //递归遍历所有的属性 Object.defineProperty(data,key,{ enumerable:true, //当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。 configurable:true, //当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中 get() { return val; }, set(newVal) { val = newVal; log(`属性${key}已被监听,现在值为\"${newVal.toString()}\"`) } }) } function observe(data) { if(!data || typeof data !== 'object') { return; } Object.keys(data).forEach(function(key){ //遍历每一个数据 defineReactive(data,key,data[key]); }); } let data = { user1: { name: '' }, user2: '' }; observe(data); data.user1.name = '约翰'; // 属性name已经被监听了,现在值为:“约翰” data.user2 = '鲍勃'; // 属性book2已经被监听了,现在值为:“鲍勃” 通过上面的代码,我们模拟了Vue实例的数据监听实现过程,这里也很好的解释了为什么页面需要的数据字段必须得在Vue实例挂载前就要注册在 data 对象中,不能动态的在data中设置数据字段,或者说动态的在 data 中设置字段,该字段是不能双向绑定的。原因就在于Vue实例挂载时,已经遍历了data 并为data中每个值都执行了Object.defineProperty(),而之后data中的数据自然就不会监听。","categories":[{"name":"Vue","slug":"Vue","permalink":"http://fanghl.top/categories/Vue/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"服务器部署-flask","slug":"flask-deploy","date":"2020-09-17T03:57:54.000Z","updated":"2023-11-03T03:29:00.558Z","comments":true,"path":"2020/09/17/flask-deploy/","link":"","permalink":"http://fanghl.top/2020/09/17/flask-deploy/","excerpt":"","text":"前言centos 系列的云服务器一般自带Python3.6,使用如下命令查看Python是否提前安装 whereis python 服务器的基本配置可参考之前的 服务器部署-Node 一文 工具: 云服务器 Xshell Navicat Postman 虚拟环境本地测试跑通的项目,生成 requirement.txt 或 Pipfile文件,通过Xshell导入到服务器目录下,使用 pipenv install 创建虚拟环境,并安装依赖。 安装依赖异常这里常见的报错为安装超时 timeout , pip 可以指定安装源。 阿里云 http://mirrors.aliyun.com/pypi/simple/ 中国科技大学 https://pypi.mirrors.ustc.edu.cn/simple/ 豆瓣(douban) http://pypi.douban.com/simple/ 清华大学 https://pypi.tuna.tsinghua.edu.cn/simple/ 中国科学技术大学 http://pypi.mirrors.ustc.edu.cn/simple/ 临时指定安装源 pip install -i https://pypi.tuna.tsinghua.edu.cn/simple 永久指定安装源 linux 修改 ~/.pip/pip.conf (没有就创建一个), 内容如下 [global]index-url = https://pypi.tuna.tsinghua.edu.cn/simple windows 直接在user目录中创建一个pip目录,如:C:\\Users\\xx\\pip,新建文件pip.ini [global]index-url = https://pypi.tuna.tsinghua.edu.cn/simple 服务器运行依赖都已经导入后,尝试运行项目的入口文件,如果报错,请先解决错误。 如果没有错误,此时的项目运行在 localhost:5000 端口,我们在外部是无法访问的,我们修改项目入口文件host: 12if __name__ == \"__main__\": app.run(host=\"0.0.0.0\", port=5000, debug=True) 来监听所有的端口请求。 端口安全组项目监听0.0.0.0,却在外面还是无法访问!使用 nmap your ipAddress 来查看开放的接口,这里5000端口是不存在的,eg: nmap 108.16.12.26 5000端口未被开放,可以使用防火墙命令开启5000端口,防火墙命令见 服务器部署-Node 开启了防火墙之后,如果可以访问5000端口了,那么恭喜你。如果防火墙开启5000端口或者关闭防火墙依然无法访问,那么这里需要去云服务器找到安全组 -> 创建安全组 -> 允许访问所有端口,之后在创建好的安全组,点击进去看到 关联云服务器 ,选择你的服务器实例即可。如此,我们便可以在外网访问到程序 gunicorn gevevnt可以访问项目后,当关闭 Xshell 后,项目仍然无法被访问,所以所以这里使用 gunicorn 来做持久访问。 安装gunicorn gevevnt, gevent 对Windows兼容不是很好。 pip install gunicorn gevevnt 安装好之后,在项目根目录建立 gunicorn.conf.py 内容如下: 123workers = 5 # 定义同时开启的处理请求的进程数量,根据网站流量适当调整worker_class = "gevent" # 采用gevent库,支持异步处理请求,提高吞吐量bind = "0.0.0.0:5000" 使用命令部署项目 gunicorn run:app -c gunicorn.conf.py # run:app 这里的 run 是你项目入口文件 启动后,关闭 Xshell 后依旧可以访问 重启和关闭 先查看进程 pstree -ap|grep gunicorn 得到的结果包含正在运行的进程和我们之前配置的线程数,这里操作的是进程pid。 kill -9 pid # 关闭进程 kill -HUP pid # 重启进程 docker容器部署 docker安装出现了报错 “Problem: package docker-ce-3:19.03.8-3.el7.x86_64 requires containerd.io >= 1.2.2-3, but none of the providers can be installed”解决办法 在这里 启动docker service docker start 创建 Dockerfile 文件 123456789FROM python:3.6WORKDIR /project/PythonProjectCOPY requirements.txt ./RUN pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simpleCOPY . .CMD ["gunicorn", "ginger:app", "-c", "./gunicorn.conf.py"] 构建 docker 镜像 (时间较长) docker build -t ‘testflask’ . 完成镜像后,使用如下命令查看 docker images 会发现存在一个 ‘testflask’镜像存在 配置阿里云镜像仓库 在阿里云dockerhub 点击这里 , 注册账号,他会生成一个阿里云镜像加速链接,(不适用国外的dockerhub,原因你懂的,网络问题), 将这个 加速链接 配置在我们的服务器上 /etc/docker/daemon.json 注意: 不存在 /etc/docker/daemon.json 则创建该文件!并复制链接进去 <!-- /etc/docker/daemon.json --> { "registry-mirrors": ["https:********.liyuncs.com"] } 重新加载服务配置文件 systemctl daemon-reload 重启Docker systemctl restart docker 查看本地镜像 docker inages 推送镜像到阿里云镜像仓库 docker tag 70517a163731 registry.cn-hangzhou.aliyuncs.com/命名空间/仓库名称:[镜像版本号]docker push registry.cn-hangzhou.aliyuncs.com/命名空间/仓库名称:[镜像版本号] 运行testginger 使我们的 docker images,端口映射前面是容器的端口、后面是项目暴露处的端口,相当于一层代理。 docker run -d -p 8080:5000 testginger此时,使用 nmap your id address 查看服务器端接口占用,8080是开启的。 日志 docker logs [options]docker logs –tail=”10” CONTAINER ID 重新build docker build -t ginger:test . 删除镜像初次部署时,我们可能会创造多个镜像,待成功部署后,我们可以删除多有的无用镜像容器。 docker rmi -f image_id 命令 查看正在运行的镜像容器 docker ps 查看所有存在的镜像容器 docker ps -a 停止镜像容器 docker stop image_id","categories":[{"name":"默认","slug":"默认","permalink":"http://fanghl.top/categories/默认/"}],"tags":[{"name":"默认","slug":"默认","permalink":"http://fanghl.top/tags/默认/"}]},{"title":"服务器部署-Node","slug":"server","date":"2020-06-09T03:28:31.000Z","updated":"2023-11-03T03:29:00.561Z","comments":true,"path":"2020/06/09/server/","link":"","permalink":"http://fanghl.top/2020/06/09/server/","excerpt":"","text":"配置服务器购买的服务器,属于一个空壳子,安装我们需要的Git、node等程序,使用Xshell进行控制。 Git连接仓库 生成秘钥,并将公钥注册在Git仓库即可 cd ~/.ssh 安装MySQL 目录切换至root下, cd ~ 下载与安装mysql, 步骤如下 安装MySQL官方的yum repository: wget -i -c http://dev.mysql.com/get/mysql57-community-release-el7-10.noarch.rpm 下载rpm包 yum -y install mysql57-community-release-el7-10.noarch.rpm 安装MySQL服务 yum -y install mysql-community-server,需要等待一段时间,最后出现 complete! 启动MySQL服务 systemctl start mysqld.service 查看MySQL状态 systemctl status mysqld.service 获取初始密码 grep "password" /var/log/mysqld.log,复制下来,待会修改密码要用到 登录MySQL mysql -u root -p,执行后输入刚刚复制的密码即可登录成功,成功后会展示MySQL的版本信息,这里是5.7.X 执行MySQL语句 set global validate_password_policy=0;set global validate_password_length=1;之后才可以修改密码(这里是5.7版本的修改密码方法) 修改MySQL密码 set password for root@localhost = password('123456'); root是用户名,可自定义。 退出使用新密码重新登录 配置mysql,在etc/目录下,编辑(不存在则新建)my.cnf文件,重启MySQL 配置用户,使得root用户在外网也可以访问到mysql, # mysql -h 127.0.0.1 -u root # mysql>use mysql; # mysql>update user set host = '%' where user ='root'; # mysql>select host, user from user; # mysql>flush privileges; 重启mysql服务 service mysqld restart; mysql异常集合 运行程序mysql报错 (node:6280) UnhandledPromiseRejectionWarning: SequelizeDatabaseError: Illegal mix of collations (latin1_swedish_ci,IMPLICIT) and (utf8mb4_unicode_ci,COERCIBLE) for,原因为sequelize创建的mysql默认字符集是 latin1 而不是 utf8,更改即可。 root用户丢失表现: 输入密码页无法访问mysql.user里面,root用户由于未知原因不在了,重新建立root用户 忽略密码登录- vim etc/my.cnf skip-grant-tables 创建root用户 create user ‘root’@’localhost’ identified by ‘123456’; 此步骤可能会报以下错误: 没报错的跳过(直接到权限那一步),用以下方法解决: ERROR 1290 (HY000): The MySQL server is running with the –skip-grant-tables option so it cannot execute this statement 解决: flush privileges; 再次创建root用户 create user ‘root’@’localhost’ identified by ‘123456’; 由于可能存在root用户,导致会报错,删除用户在创建 drop user ‘root’@’localhost’;create user ‘root’@’localhost’ identified by ‘123456’; 赋予root用户全部权限 GRANT ALL PRIVILEGES ON . TO ‘root’@’%’ IDENTIFIED BY ‘123456’flush privileges; 1396 报错 flush privileges;drop user ‘dl’@’%’;create user ‘dl’@’%’ identified by ‘123’; nmap使用该命令查看服务器开放的端口,查看3306端口是否防火墙中允许访问。 yum -y install nmapnmap 192.168.1.56 telnet检查我们的IP是否可以ping同 例如telnet 192.168.157.129 80 防火墙端口express程序默认运行在 3000 端口,云服务器默认是没有开放 3000 端口的,需要我们手动开启 3000 端口,命令如下: firewall-cmd --zone=public --add-port=3000/tcp --permanent重启防火墙firewall-cmd --reload 如报错FirewallD is not running,则开启防火墙systemctl start firewalld 使用nmap查看现在的端口情况,3000端口已经开启。nmap yourIpAddress pm2进程管理本地服务在我们关闭命令窗口后会停止服务,使用pm2 管理我们的服务,本地调试好之后传到git,Xshell中直接拉取最新代码,进入项目根目录,使用pm2管理进程,即使我们关闭xshell,服务依然还在跑。1、pm2需要全局安装npm install -g pm22、进入项目根目录2.1 启动进程/应用 pm2 start bin/www 或 pm2 start app.js 2.2 重命名进程/应用 pm2 start app.js –name wb123 2.3 添加进程/应用 watch pm2 start bin/www –watch 2.4 结束进程/应用 pm2 stop wwwb 2.5 结束所有进程/应用 pm2 stop all 2.6 删除进程/应用 pm2 delete www 2.7 删除所有进程/应用 pm2 delete all 2.8 列出所有进程/应用 pm2 list 2.9 查看某个进程/应用具体情况 pm2 describe www 2.10 查看进程/应用的资源消耗情况 pm2 monit 2.11 查看pm2的日志 pm2 logs 2.12 若要查看某个进程/应用的日志,使用 pm2 logs www 2.13 重新启动进程/应用 pm2 restart www 2.14 重新启动所有进程/应用 pm2 restart all 2.15 监听修改,自动重启 pm2 start xxx –watch nginx代理安装nginx,链接: https://www.cnblogs.com/shiyuelp/p/11945882.html路径: /usr/local/nginx/sbin配置文件修改:/usr/local/nginx/config注意点: nginx默认在80端口,而服务器默认不开放80端口。需要手动打开80端口。查看nginx是否启动成功: ps aux|grep nginx;检查IP是否可以ping通: telnet 106.13.4.74 80;启动nginx: /usr/local/nginx/sbin/nginx重启nginx: /usr/local/nginx/sbin/nginx -s reopen关闭nginx: /usr/local/nginx/sbin/nginx -s stop 配置文件可以自己修改 域名映射域名映射这块其实没必要单独再买一个域名,如果自己有域名的话,可以用一个二级域名来代替。这里以阿里云的域名为例。在阿里云的域名解析设置中,添加一个记录,主机记录那里就是填写我们二级域名的地方,记录值填写服务器的IP地址,添加完成后即可使用域名访问服务器了。可以直接在浏览器输入刚刚配置二级域名,看看是否有nginx欢迎页即可。 redis","categories":[{"name":"server","slug":"server","permalink":"http://fanghl.top/categories/server/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"Python-flask","slug":"flask","date":"2020-06-05T08:36:31.000Z","updated":"2023-11-03T03:29:00.559Z","comments":true,"path":"2020/06/05/flask/","link":"","permalink":"http://fanghl.top/2020/06/05/flask/","excerpt":"","text":"Base pointpycharm中打开debug模式:在终端中设置: set FLASK_ENV=development (windows)export FLASK_ENV=developm (mac)flask run开启debug会 激活调试器。 激活自动重载。 打开 Flask 应用的调试模式。 创建虚拟环境 python -m venv env 激活虚拟环境(win) $ env\\Scripts\\activate然后在虚拟环境中安装flask等 依赖 生成依赖文件 pip freeze > requirements.txt 安装依赖 pip install -r requirements.txt flask_scriptflaks_script通过命令的方式操作flask,跑起来开发版服务器、设置数据库,定时任务等。 1234567891011121314# manage.pyfrom flask_script import Managerapp = Flask(__name__)manager = Manager(app)def hello(): passmanager.add_command('hello', hello())# 装饰符命令@manager.commanddef hello(): pass 通过自定义方法操作flask,使用方法为: $ python manage.py hello Blueprint蓝图模块,帮助我们对于整体项目的分割,利于后续的管理和拓展。主要分为设置蓝图,注册蓝图,路由使用蓝图三部分。 1234567891011121314151617# @/api/user/controllers.pyfrom flask import Blueprint user = Blueprint('user', __name__)\"\"\"创建用户\"\"\"@user.route('/register', methods= ['get'])def user_register(): pass# @__init__.py def create_app(): app = Falsk(__name__) from om_core.api.user.controllers import user app.register_blueprint(user, url_prefix='/api/users') 至此,URL使用 localhost:5000/api/users/register 就可以访问注册路由。 flask-SQLAlchemy 查询结果一般使用db.session.query()来查询结果,结果返回一个list,多条数据处理需要用到遍历,单挑数据则可以使用一下方式获得值并返回 user = db.session.query(User).filter_by(name=’’liming).all() 数据接收 post json格式 data = json.loads(request.get_data(as_text = True))name = data.get(‘name’) get URL拼接 param = request.args[‘param’] 新增数据并提交数据库 user = User(name=”xxx”, dender=0)db.session.add(user)db.session.commit() 更新数据 user = db.session.query(User).filter(name=param.get(‘name’)).first()user.attr = param.get(‘attr’)db.session.commit() Faker生成大量的模拟数据 pip install Faker 12345678910111213from faker import Fakerfaker = Faker('zh-CN') # 默认美国for item in range(100): fname = faker.name() faddress = faker.address() fint = faker.pyint() user = User(name=fname, address=faddress, xxx=fint) db.session.add(user)try: db.session.commit()except: db.session.rollback() flask_restplus pip install flask-restplus from flask_restplus import Api Flask-RESTPlus提供的主要创建对象就是资源。资源创建于Flask可插入视图(pluggable view)之上,使得我们可以通过在资源上定义方法来很容易地访问多个HTTP方法。 flask_cors解决跨域 from flask_cors import CORS if Config.FLASK_ENV == ‘DEVELOPMENT’: CORS(app, supports_credentials=True) namedtuplenamedtuple是继承自tuple的子类。namedtuple创建一个和tuple类似的对象,而且对象拥有可访问的属性。 用户权限细粒度管理用户权限是一个常见的业务,这里使用scope模块在 token 进行验证时判断用户的接口访问权限,并且自定义的 scope 可以进行权限的合并与筛选,进行普通用户和管理员权限区分。scope 提供两套权限机制,api级别和蓝图级别,粒度粗细可以自己选择,十分灵活。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596# scope.pyclass Scope: allow_api = [] # api 级别的权限控制 粒度细 allow_module = [] # 蓝图级别权限控制 粒度粗 forbidden = [] # 权限筛选 def __add__(self, other): # 管理员合并普通用户权限 # 运算符重载 self.allow_api = self.allow_api + other.allow_api # set 去重 self.allow_api = list(set(self.allow_api)) # 红图级别权限相加 self.allow_module = self.allow_module + other.allow_module self.allow_module = list(set(self.allow_module)) # 逆向筛选权限 self.forbidden = self.forbidden + other.forbidden self.forbidden = list(set(self.forbidden)) return selfclass UserScope(Scope): allow_api = ['v1.user+get_user'] allow_module = [] forbidden = ['v1.user+super_get_user', 'v1.user+super_delete_user'] def __init__(self): self + AdminScope()class AdminScope(Scope): # allow_api = ['v1.user+super_get_user', # 'v1.user+super_delete_user'] allow_module = ['v1.user'] def __init__(self): # 排除 筛选视图函数 # self + UserScope() pass def is_in_scope(scope, endpoint): # scope() # globals # 反射 # token 内部携带权限参数,直接判断权限 # v1.red_name + view_func scope = globals()[scope]() splits = endpoint.split('+') red_name = splits[0] if endpoint in scope.forbidden: return False if endpoint in scope.allow_api: return True if red_name in scope.allow_module: return True else: return False# token_auth.py from app.libs.scope import is_in_scopeauth = HTTPBasicAuth()User = namedtuple('User', ['uid', 'ac_type', 'scope'])@auth.verify_passworddef verify_password(token, password): user_info = verify_auth_token(token) if not user_info: return False else: g.user = user_info return Truedef verify_auth_token(token): s = Serializer(current_app.config['SECRET_KEY']) try: data = s.loads(token) except BadSignature: raise AuthFailed(msg='token is invalid', error_code=1002) except SignatureExpired: raise AuthFailed(msg='token is expired', error_code=1003) uid = data['uid'] ac_type = data['type'] scope = data['scope'] # 视图函数 allow = is_in_scope(scope, request.endpoint) print('验证token参数--', uid, ac_type, scope, request.endpoint) if not allow: # 在这里拦截不同权限用户 raise Forbidden() return User(uid, ac_type, scope)","categories":[{"name":"Python","slug":"Python","permalink":"http://fanghl.top/categories/Python/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"Express","slug":"express","date":"2020-05-22T06:19:11.000Z","updated":"2023-11-03T03:29:00.558Z","comments":true,"path":"2020/05/22/express/","link":"","permalink":"http://fanghl.top/2020/05/22/express/","excerpt":"","text":"express文档 : https://www.expressjs.com.cn/guide/using-middleware.htmlsequelize文档: https://github.com/demopark/sequelize-docs-Zh-CN/blob/master/Readme.md ORM(sequelize)ORM(Object Relational Mapping)对象关系映射,减小操作层的代码量,直接方便的操作数据库。使用前,确保sequelize已经安装 12npm install --save sequelizenpm install --save mysql2 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127/** path: @src/models/message.js*/var Sequelize = require('sequelize')var sequelize = new Sequelize( 'nodesql', //database name 'root', //database user '123456', //database password { 'dialect': 'mysql', 'host': 'localhost', 'port': 3306 })//表模型var Message = sequelize.define('message', { id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true, }, userName: { type: Sequelize.STRING(32), }, content: { type: Sequelize.TEXT }})const User = sequelize.define('user', { id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true, }, userName: { type: Sequelize.STRING(32), }, age: { type: Sequelize.INTEGER }, gender: { type: Sequelize.INTEGER }, address: { type: Sequelize.STRING(32) }})Message.sync(); //创建表User.sunc()module.exports = {Message, User};/** path: @src/route/index.js* desc: 在路由中完成增删改查*/var express = require('express');var router = express.Router();var Message = require('../models/message.js')const setToken = require('../utils/middwares/jwt.js')const {log} = console//REST API//用户登录router.post('/login', async (req, res, next) => { const user = {} let {userName} = req.body let data = await User.findOne({ where: { userName: userName } }) // log('查询结果返回:', JSON.stringify(data, null, 2)) if(!data) { //不存在用户则创建用户 user.userName = userName data = await User.create(user) } setToken.setToken(data.id).then(token => { //返回用户信息及token return res.json({data: {data, token}}) })})//查找某内容router.get('/getOne', (req, res, next) => { if(!req.data) { return res.json({ msg:'token invalid' }) } Message.findAll().then(data => { log('查找数据res', JSON.stringify(data, null, 2)) res.json({ errcode: 0, data }) })})//删除一个用户router.get('/del_user', async (req, res, next) => { let {userName} = req.query let result = await User.findOne({ where: {userName,} }) if(!result) { return res.json({msg: '不存在该用户', errcode: 601}) } let data = await result.destroy() return res.json({msg: '删除成功', errcode: 0})})//更新用户信息router.post('/rich_user_info', async (req, res, next) => { let data = req.body let result = await User.findOne({ where: {userName: data.userName} }) //用数据表的result字段来匹配前端上传的字段!前端随便传参,我只过滤有用的 Object.keys(result.toJSON()).map(item => { data[item] ? result[item] = data[item] : '' }) await result.save() res.json({msg: 'succ', data: result})}) 至此,基本的sequlize就可以跑起来了。 原生SQL方法sequelize的确方便,但他的查询语句较为繁琐,这里我们还可以使用原生mysql语句。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263/*** path: utils/dbConfig.js* 数据库配置*/mysql = { host: 'localhost', user: 'root', password: '123456', database: 'nodesql'}module.exports = mysql;/*** path: @/db.js* 手动连接数据库*/let mysql = require('mysql')let dbConfig = require('./utils/dbConfig')const {log} = consolemodule.exports = { query: function(sql, params, callback) { let conn = mysql.createConnection(dbConfig) conn.connect(function(err) { if(err) { log('数据库连接失败') throw err } conn.query(sql, params, function(err, res, fields) { if(err) { log('数据库操作失败') throw err } callback && callback(res); conn.end(err => { if(err) { log('数据库关闭失败') throw err } }) }) }) }}/*** path: @/route/index.js* 路由使用*/router.get('/user', (req, res, next) => { let {id} = req.query const sql = `select * from user where id = ${id}` //复杂SQL语句 db.query(sql, [], function(result, fields) { let data = JSON.parse(JSON.stringify(result)) data1 = req.requestTime res.json({ status: 0, data, data1 }) })}) ORM和原生SQL语句之间并不冲突,合理选择使用即可。两个一起用也可以 JWT(token验证)jwt(jsonwebtoken)验证,前后端验证的一种方法。express实现jwt验证 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119// 安装 express-jwt$ npm i express-jwt --save /** path: @/util/jwt.js* 封装分发token和验证token函数*/var jwt = require('jsonwebtoken');var signkey = 'secret123456'; //随机串const setToken = function(userid){ return new Promise ((resolve, reject) => { const token = jwt.sign({ _id: userid }, signkey,{ expiresIn:'36h' }); resolve(token); })}const verToken = function (token) { return new Promise((resolve, reject) => { var info = jwt.verify(token.split(' ')[1], signkey); if(info) { resolve(info); } else { reject(info) } })}module.exports = { verToken, setToken,}/*** path: @/routes/index.js* 首先在login接口分发token*/var express = require('express');var router = express.Router();var {Message, User} = require('../models/test.js')const jwt = require('jsonwebtoken')const setToken = require('../utils/middwares/jwt.js')// 登录apirouter.post('/login', async (req, res, next) => { const user = {} let {account, password} = req.body //小程序这里应接受code,去换取openID存储 let data = await User.findOne({ where: { account: account, } }) // console.log('查询结果返回:', JSON.stringify(data, null, 2)) if(!data) { //不存在该用户,则创建用户 user.account = account data = await User.create(user) } setToken.setToken(data.id).then(token => { return res.json({data: {data, token}}) })})//请求内容router.get('/getOne', (req, res, next) => { if(!req.data) { //验证token的中间件成功后,把验证结果放置在req.data中 return res.json({ msg:'token invalid', errcode: 600, }) } Message.findAll().then(result => { res.json({ errcode: 0, result, }) })})/*** path: @/app.js* 配置token验证中间件*/var jwt = require('jsonwebtoken');var verToken = require('./utils/middwares/jwt.js')app.use(function(req, res, next) { var token = req.headers['authorization'] if(!token) { return next(); } else { verToken.verToken(token).then((data) => { req.data = data; return next(); }).catch((error)=>{ return next(); }) //上为封装方法,下为直接调用,都可以使用 // let info = jwt.verify(token.split(' ')[1], 'secret123456'); // req.data = info; // next() }})//过滤不需要token的路由app.use(expressJwt({ secret: 'secret123456' // 签名的密钥 或 PublicKey}).unless({ path: ['/login',] // 指定路径不经过 Token 解析}))//当token失效返回提示信息app.use(function(err, req, res, next) { if (err.status == 401) { return res.status(401).send({msg: 'token invalid'}); }}); 至此,token验证就可以跑起来了。在发送http时,headers中配置 Authorization: 'Bearer ${token}'即可,当然还可以继续再次封装。 middare(中间件)中间件用来处理后端服务,对前端的路由请求进行过滤处理。express本来就是服务加中间件的集合,不同的中间件构成了完整的api逻辑处理。应用级中间件绑定在APP内,路由中间件绑定在路由,除此之外,还有内置中间件,错误处理中间件等。不带有路由限制的中间件是会被所有路由执行的。 12app.use(middareFun) //所有请求都会触发app.use('/user/:id', middareFun) ///user路径请求触发 这里我们优化了上面的 token 验证中间件。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354/*** path: @/utils/middares/paramsVerify.js*/var verToken = require('./jwt.js')//请求时间const requestTime = function(req, res, next) { req.requestTime = Date.now() next()}//解析tokenconst tokenVerify = function(req, res, next) { var token = req.headers['authorization']; if(!token){ return next(); } else { verToken.verToken(token).then((data)=> { req.data = data; return next(); }).catch((error)=>{ return next(); }) }};//这里可以一个一个导出,也可以直接写在数组内,导出数组即可(二选一)。const middArr = [ requestTime = function() {}, tokenVerify = function() {},]module.exports = { requestTime, tokenVerify, middArr,}/*** path: @/app.js* 两种注册方式二选一即可*/let paramsVerify = require('./utils/middwares/paramsVerify.js')//1.注册paramsVerify文件中所有的中间件(单个导出式)let middwareArr = []for(let i=0; i<Object.keys(paramsVerify).length; i++) { let item = paramsVerify[Object.keys(paramsVerify)[i]] middwareArr.push(item) //干嘛不直接导出数组了?!en //app.use(item) }app.use(middwareArr)//2.导出数组式app.use(paramsVerify.middArr) //完事, 至此,中间的剥离优化完整。 封装log在调试中,可以封装一个log用来替代 console.log 123456789101112131415161718192021222324252627282930/*** path: @/utils/log.js**/Date.prototype.Format = function (fmt) {var o = { \"M+\": this.getMonth() + 1, \"d+\": this.getDate(), \"h+\": this.getHours(), \"m+\": this.getMinutes(), \"s+\": this.getSeconds(), \"q+\": Math.floor((this.getMonth() + 3) / 3), \"S\": this.getMilliseconds()};if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + \"\").substr(4 - RegExp.$1.length));for (var k in o) if (new RegExp(\"(\" + k + \")\").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : ((\"00\" + o[k]).substr((\"\" + o[k]).length)));return fmt;}function log() { const show = true //也可以和开发环境挂钩控制日志输出 if (show) { console.log(`[${new Date().Format(\"yyyy-MM-dd hh:mm:ss\")}] `, ...arguments) }}module.exports = log 跨域在搭建完后端api后,需要在前端调试。无论是小程序还是vue的webapp,访问本地连接会出现跨域问题(小程序得在开发工具上关闭域名检测,小程序默认https),如图:此时需要在app.js内加入允许跨域访问 123456789101112131415161718192021222324252627282930313233/** desc: 以下跨域代码未处理 OPTION 形式,在request不携带自定义参数如 token 等headers内的配置时,是可以跑起来的,一旦在request内设置headers内容,请求会被 OPTION 挂起,跨域失败!*///设置允许跨域访问该服务.(此代码为坑)app.all('*', function (req, res, next) { res.header('Access-Control-Allow-Origin', '*'); //Access-Control-Allow-Headers ,可根据浏览器的F12查看,把对应的粘贴在这里就行 res.header('Access-Control-Allow-Headers', 'Content-Type'); res.header('Access-Control-Allow-Methods', '*'); res.header('Content-Type', 'application/json;charset=utf-8'); next();})/** desc: 跨域* desc: 大胆copy* path: @/app.js*/app.use((req, res, next) => { // 设置是否运行客户端设置 withCredentials // 即在不同域名下发出的请求也可以携带 cookie res.header(\"Access-Control-Allow-Credentials\",true) // 第二个参数表示允许跨域的域名,* 代表所有域名 res.header('Access-Control-Allow-Origin', '*') res.header('Access-Control-Allow-Methods', 'GET, PUT, POST, OPTIONS') // 允许的 http 请求的方法 // 允许前台获得的除 Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma 这几张基本响应头之外的响应头 res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With') if (req.method == 'OPTIONS') { res.sendStatus(200) } else { next() }}) 此时,我们的vue前端使用axios携带token就可以请求到服务端接口了 morgan日志日志,记录服务器的操作行为,这里使用 morgan 把日志记录在本地文件保存。 /** * path: @/utils/morgan.js * desc: 封装一个morgan */ var path = require('path'); var fs = require('fs') var morgan = require('morgan'); var FileStreamRotator = require('file-stream-rotator') var logDirectory = path.join(__dirname, 'log') fs.existsSync(logDirectory) || fs.mkdirSync(logDirectory) morgan.token('timeStamp', function(req, res){ let date = new Date() let logHour = date.getHours() < 10 ? `0${date.getHours()}` : date.getHours() let logMinute = date.getMinutes() < 10 ? `0${date.getMinutes()}` : date.getMinutes() let logSecond = date.getSeconds() < 10 ? `0${date.getSeconds()}` : date.getSeconds() let logDate = `${logHour}:${logMinute}:${logSecond}` return logDate }); // 自定义format,其中包含自定义的token morgan.format('joke', '[joke :timeStamp] :remote-addr :remote-user :method :url HTTP/:http-version :status :res[content-length] - :response-time ms'); // create a rotating write stream var accessLogStream = FileStreamRotator.getStream({ date_format: 'YYYYMMDD', filename: path.join(logDirectory, 'access-%DATE%.log'), frequency: 'daily', verbose: false }) module.exports = {morgan, accessLogStream} /* * path: @/app * 引用这个日志系统 */ app.use(morgan('joke')); //服务器输入实时日志 app.use(morgan('short', {stream: accessLogStream})); //记录日志在文件中 至此,我们可以在 /utils 路径下看到创建的 log 文件夹,日志已经根据 file-stream-rotator 插件做了分割,每一天的日志集约在一个文件。以防日志过多导致混乱。 本项目git地址在这里, 欢迎star 本项目部署服务器文档在这里","categories":[{"name":"node","slug":"node","permalink":"http://fanghl.top/categories/node/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"vue-wx-h5","slug":"vue-wx-h5","date":"2019-12-30T02:47:44.000Z","updated":"2023-11-03T03:29:00.561Z","comments":true,"path":"2019/12/30/vue-wx-h5/","link":"","permalink":"http://fanghl.top/2019/12/30/vue-wx-h5/","excerpt":"","text":"微信公众号总结由于微信的约定,在ios端无法购买虚拟产品,安卓则没有限制。为了解决ios用户也可以享受购买虚拟服务的问题,可适用公众号H5支付来解决ios端支付问题 准备由于仅仅是为ios用户解决支付问题,故此H5页面内容很简单,登录、拉取支付列表、支付即可。H5采用vue+jq实现,未适用vue-cli。(就俩页面,没必要)。页面结构如下: 鉴权需求鉴权,是微信提供的H5授权方式,一般采用第三方授权,授权成功获取code,用code获取acces_token、unionID等,由于H5是小程序ios支付的延伸,故此需要unionID来判断用户唯一性!鉴权必不可少。index.html文件中首先导入jsapi 鉴权网页授权与小程序不同,网页是第三方网页授权,然后授权信息在重定向链接中(redirect_uri)返回,重定向链接我设置为index.html页面。在 created() 钩子中,去鉴权获取code 123456789101112131415161718192021222324252627282930313233created: function() { const token = window.sessionStorage.getItem('token') //解决刷新问题 if(token) { this.getCircleList() return } let c = this.getQueryCode('code') this.code = c if(this.code) { this.postData(c) } else { this.getUserCode() } },//鉴权getUserCode() { let redirect_uri = 'http://test.********.cn/projectName/' redirect_uri = encodeURI(redirect_uri) let url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${this.appId}&redirect_uri=${redirect_uri}&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirec` window.location.href = url},//获取鉴权成功后codegetQueryCode(variable) { let query = window.location.search.substring(1) let v = query.split(\"&\") for(let i = 0; i < v.length; i++) { let pair = v[i].split('=') if(pair[0] == variable) { return pair[1] } } return null}, 重点在于 getUserCode 鉴权函数,有人会在鉴权链接后面再加一个参数:&connect_redirect=1 我第一次使用的链接就是加了该参数的,我只替换了APPID和重定向地址,结果一直报错,坑货。第二,重定向链接不能是本地连接,得是外网可以访问的链接。 支付页面中点击单个商品,进行支付购买。具体的支付过程和小程序支付一样,只不过这里多了wx.config 的应用。 1234567891011121314151617181920212223242526272829303132333435wx.config({ debug: false, appId: that.appId, timestamp: timeStamp1, nonceStr, signature, jsApiList: ['chooseWXPay'] })wx.ready(function() { wx.chooseWXPay({ timestamp: timeStamp1, nonceStr, package: package1, signType, paySign, success: function (res) { that.showSuccTip = true that.clickStatus = true that.getCircleList() setTimeout(() => { that.showSuccTip = false }, 1000*3) }, cancel() { that.clickStatus = true }, fail(res) { alert('支付失败,稍候再试') that.clickStatus = true }, }); })wx.error(function(res) { console.log('config error : ', res)}) 页面通信 - ajax没有使用axios,使用ajax通信,封装ajax通信 123456789101112131415161718192021222324httpAjax (obj) { $.ajax({ url: obj.url, data: obj.data && obj.type === \"POST\" ? JSON.stringify(obj.data) : obj.data, type: obj.type ? obj.type : 'GET', contentType: 'application/json', beforeSend: function(xhr) { if (obj.token) { xhr.setRequestHeader('Authorization', '******** ' + obj.token); } }, dataType: 'json', success: function (res) { if (typeof obj.success === 'function') { obj.success(res) } }, error: function (res) { if (typeof obj.error === 'function') { obj.error(res) } } })}, 页面效果这个效果,想到了使用jq解决,vue可能有更简单的方法,但没试过! 123456789101112131415161718data: { clickId: 0, }//列表按钮携带自身idshowDetail(id) { if(id == this.clickId) return $('#'+this.clickId).addClass('contentBox') $('#'+id).removeClass('contentBox') $('#'+id).find(\"[name='bigCircle']\").removeClass('greyLine') $('#'+this.clickId).find(\"[name='bigCircle']\").addClass('greyLine') $('#'+id).find(\"[name='smallCircle']\").addClass('hited') $('#'+this.clickId).find(\"[name='smallCircle']\").removeClass('hited') this.clickId = id}, 采坑杂谈坑真的有点多,尤其是第一次搞得话。各种配置文件、微信公众平台里面的白名单,安全域名配置等等,token的传递坑了好久,跨域,没用vue-cli ,打开页面不能右击打开浏览器预览,使用 anywhere 插件来把路径转化为 http/HTTPS链接,后期测试直接把文件拉倒xshell服务器里面去测,要不Git分支被污染的不忍直视。 ios兼容ios端用户支付成功回调函数里面 alert 并不会被执行,Android则无影响。所以支付成功的提示自己写一个 alert 就可以。 刷新都快上线了,产品进入点了一个刷新,页面卡死报错!原因是鉴权返回的 code 一次性有效!,刷新时,链接其实没变,但是code已经过期了。解决: 页面刷新不影响逻辑,此时我们只需要token即可,故此把token存储在sessionStorage 里面即可避免页面刷新问题。","categories":[{"name":"Vue","slug":"Vue","permalink":"http://fanghl.top/categories/Vue/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"wepy-封装","slug":"wepy-wxminiPro","date":"2019-12-16T06:48:03.000Z","updated":"2023-11-03T03:29:00.561Z","comments":true,"path":"2019/12/16/wepy-wxminiPro/","link":"","permalink":"http://fanghl.top/2019/12/16/wepy-wxminiPro/","excerpt":"","text":"request封装目录结构: 目录: /utils/base.js 123456789101112131415161718192021222324252627282930313233343536373839404142import wepy from 'wepy'import qiniuyun = from '@/utils/qiniuUploader'const dev = falseconst baseUrl = dev ? 'https://xxxx' : 'https://test/xxx'//上传图片: const uploadImg = (imageURL, uptokenURL) { return new Promise((resolve, reject) => { qiniuyun.upload(imageURL, res => { resolve(res) }, error => { reject(error) }, { region: 'ECN', domain: 'https://xxxx', uptoken: uptokenURL }) }) }//请求封装const wxRequest = async (params = {}, url, method,) => { let token = params.token || '' if(params.getToken) { token = wepy,getStorageSync('token') } let res = await wepy.request({ url, method: methos || 'GET', data: params.data || {}, header: Object.assign({ 'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': `str**Token ${token}` }, params.header || {}) }) return res}module.exports = { wxRequest, baseUrl, uploadImg,}; 在 base.js 中封装请求,在api文件中则可以直接拿来使用目录: utils/api.js 12345678910import base from '@/utils/base'//登录、获取基本信息const login = (params) => base.wxRequest(params, `${base.baseUrl}/login/`, 'POST')const getUserInfo = (params) => base.wxRequest(params, `${base.baseUrl}/user/`,)module.exports = { login, getBaseInfo,} 统一管理维护所有的接口api, 在页面中,直接使用具体的api 1234567891011121314151617181920212223242526272829/** src/pages/index.wpy*/import api from '@/utils/api'//获取用户基本信息onShow() { this.getUserInfo(params)}//带参数,同步写法getUserInfo(params) { api.getUserInfo({ data: { data: data1 }, getToken: true }).then(res => { const data = res.data ... this.$apply() })}//不带参数,异步写法async getUserInfo() { let res = await api.getUserInfo({getToken: true}) if(res.statusCode === 200) { //doSth }} Mixins提出公共方法,方便全局调用 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121/**path: scr/mixins/common*name: common*/import wepy from 'wepy'export default class commonMixins extends wepy.mixins { //本地存储 saveData (k, v) { wepy.setStorage({ key: k, value: v }) } saveDataS(k, v){ wepy.setStorageSync(k, v || '') } getDataS(k){ let res = wepy.getStorageSync(k); return res } // 图片预览 preImg(c, u){ wepy.previewImage({ current: c, urls: u }) } // 提示框 toast(title,icon,dura){ wepy.showToast({ title: title, icon: icon, duration: dura || 1500 }) } //页面滚动 pageScro(num){ wepy.pageScrollTo({ scrollTop: num, duration: 0 }); } // 页面跳转 nav(url){ this.$navigate({ url: url }) } swi(url){ wepy.switchTab({ url: url }) } log() { const show = true if (show) { console.log(`[${new Date().Format(\"yyyy-MM-dd hh:mm:ss\")}] `, ...arguments) } } // 发送formid(已废弃) postFormId(id){ let arr = wepy.getStorageSync('form_ids') || []; arr.push(id); wepy.setStorageSync('form_ids', arr) } // showModal modal(data) { wx.showModal({ content: data.content, showCancel: data.cancel || false, confirmText: data.confirm || '知道了', }) } //粘贴板 setClipboardData(data, succFun) { wx.setClipboardData({ data: 'data', success(res) { wx.getClipboardData({ success (res) { succFun } }) } }) } // 页面顶部title setNavTitle(title) { wx.setNavigationBarTitle({ title: title }) } //跳转小程序 navToMini(appId) { wx.navigateToMiniProgram({ appId, }) } //加载框 loading(title) { wx.showLoading({ title: title || \"加载中...\" }) } //隐藏加载框 hideLoading() { wx.hideLoading({}) } //banner跳转 bannerJump(url) { if(url.slogan == 1) { this.swi(url.autoResponse1) return } this.nav(url.autoResponse1) } //返回上一个页面 navBack() { wx.navigateBack({}) }} 使用mixins ,在页面中引入,config中声明,即可在页面使用 this 调用 12345import commonMixin from '@/mixins/common'import req from '@/mixins/req' //页面配置中: mixins = [commonMixin, req] 常量配置数据前端配置数据抽离出来单独放,便于维护。 12345678910111213141516171819202122232425262728/***path: src/utils/configData.js*/const education = [ '高中及以下', '专科', '本科', '硕士', '博士',]//微信号码正则const wxreg=/^[a-zA-Z]{1}[-_a-zA-Z0-9]{5,19}$/;// banner轮播配置const swiperConfig = { autoplay: true, interval: 3500, duration: 500, circular: true, indicatorDdots: true, indicatorColor: 'rgba(255,255,255,1)', indicatorActiveColor: '#FF8356',}module.export = { education, wxreg, swiperConfig,} 分包小程序未超过2M大小,无需分包。超过2M,则采用分包,单包不超过2M,总计不超过16M。 分享普通分享略过。带shareTicket的分享,且需要记录分享群的信息时: 12345678910111213141516171819/**path: src/app.wpy**/async onShow(ops) { const that = this; // 判断是否是群点击进入 console.log('APP show : ', ops) that.scene = ops.scene; if (ops.scene === 1044 && ops.shareTicket !== undefined){ that.shareTicket = ops.shareTicket; }}//页面async onShow() { if(that.$parent.scene === 1044){ await that.touchGroup(); }} 由分享进入小程序某个页面,需先判断缓存中是否存在 token , 若不存在 token , 则先请求登录接口,到后端换取 token , 再请求其他api 。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455//某分享链接进入的页面async onShow() { const that = this if( !wepy.getStorageSync('token')) { await that.getLogin() } that.getUserInfo()}//mixins - reqasync getLogin(){ await wepy.login().then(async (res) => { let result = await this.timeOut(circleApi.login({ data: { app_id: this.$parent.globalData.appId, code: res.code } })); if(res.statusCode === 200){ wepy.setStorage({ key: \"token\", data: res.data.token }) wepy.setStorage({ key: 'user_id', data: res.data.user_id }) } })}//超时处理async timeOut(fn){ let that = this; let res = await Promise.race([this.test(), fn]).then((data) => { return data }); if(res === 'timeOut'){ that.toast('请检查网络或者重新试一下', 'none' ,1500); let status = { statusCode: 300 }; return status }else { return res }}// 请求超时处理test() { let promise = new Promise((resolve, reject) => { setTimeout(() => { resolve('timeOut') },20000) }); return promise;} formid (已废弃)小程序给用户发送模板消息需要消耗 fromID, 新版的则是授权。这里记录一下fromID的处理。发送一条模板消息需耗费一个 fromIDfromID的存储应该是用户本次使用完小程序,然后把收集到的fromID一次性发送到后端 123456789101112131415161718192021222324252627282930313233/**path: src/app.wpy**/onHide(){ let form_ids = wepy.getStorageSync('form_ids'); configApi.postFormId({ data: { form_ids: form_ids }, getToken: true }); wepy.setStorageSync('form_ids', [])}// mixins - common// 存储formid postFormId(id){ let arr = wepy.getStorageSync('form_ids') || []; arr.push(id); wepy.setStorageSync('form_ids', arr) }// 页面表单产生 fromID <form @submit=\"submit\" report-submit=\"true\"> <button class=\"publicBox\" hover-class=\"none\" form-type=\"submit\"> <image class=\"publicImg\" src=\"../images/common/bigButton.png\" ></image> <view class=\"publicText\">发布</view> </button></form>submit(e){ this.postFormId( e.detail.formId )}, 当用户的表单提交行为产生了 fromID 时, 统一进行本地存储,在用户沙雕该小程序时再统一提交全部fromid 小程序登录广播登录广播可以解决很多同步问题,在app内,执行登录获取token,在token还未拿到时,首页的接口不能去执行,需要等待后端返回token后才可执行,这里有两种方式实现:method1: async/awaitapp内会执行登录,首页也onload内判断token是否存在,不存在则重新登录(同步),在登录成功后在执行业务。 123456789101112// APPonLaunch() { this.getLogin() }//index.wpyasync onLoad() { if(!wepy.getStorageSync('token)) { await this.getLogin() } // 执行业务} 以上方法在有大量新用户分享进入小程序的场景下很实用,但是不存在token的用户(新用户)通常会请求两次登录。优化如下:利用广播,广播页面告知APP内登录是否成功,成功后各页面再去执行业务。code address: https://github.com/fanghongliang/Tools/blob/master/broadcast.js 小程序跳转路径携带对象参数Object小程序在跳转页面路径时会把query参数String化,也不能携带对象参数,当然我们可以通过localStorage、globalData来解决参数携带问题。但当不确定的对象参数需要传递到下一个页面时,可以使用对象参数传递,使用encodeURIComponent封装URL即可解决code address: https://github.com/fanghongliang/Tools/blob/master/urlHelper.js 小程序使用第三方UI库当需要使用第三方UI库时,优先考虑第三方库的适配性。这里wepy 版本为 1.7.3 ,使用kai-ui 采坑经历如下:kai-ui: https://www.npmjs.com/package/kai-ui/v/1.2.2npm引入之后要确保项目 /dist/npm 目录下,存在 kai-ui 文件夹,如没有,删除dist目录,重新编译。步骤一:在root目录app文件内,style中引入 @import '../node_modules/kai-ui/src/less/index';步骤二:在页面中直接引入你需要的组件, import loading from 'kai-ui/Loading' components = { loading, } 之后就可以使用 组件了,此时!可能会出现 [Error] TypeError: Cannot read property 'dir' of null 报错! 解决方法: 直接引用,不要在 components 内注册,亲测有效(坑)","categories":[{"name":"wepy框架封装","slug":"wepy框架封装","permalink":"http://fanghl.top/categories/wepy框架封装/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"Emmet","slug":"Emmet","date":"2019-10-11T05:53:19.000Z","updated":"2023-11-03T03:29:00.555Z","comments":true,"path":"2019/10/11/Emmet/","link":"","permalink":"http://fanghl.top/2019/10/11/Emmet/","excerpt":"","text":"Emmet功能快速编辑前端HTML标签,以及编辑器标签自动闭合功能编辑器安装插件:Auto Close TagVscode编辑器中,【设置】中打开 Emmet相关配置 Emmet初始化12! => tabhtml:5 标签id/class/属性12div.test#testidp.test-class{这里是p文本} 嵌套< : 子节点+ : 兄弟节点^ : 父节点 1div.aim-class>div.son-class^div.brother1-class+div.brother2-class 分组() : 分组分组内的标签在层级上视为整体 1div>(div>div>a)+div>p{test text} 隐式标签直接通过 类 或 ID 生成标签可以省略掉div,即输入.item即可生成<div class="item"></div>隐式标签集合: 1234li:用于ul和ol中tr:用于table、tbody、thead和tfoot中td:用于tr中option:用于select和optgroup中 乘法* : 重复指令$ : 自增符号 12div*5ul>li$*3 CSS缩写12w100 => width: 100pxh10p => height: 10% 单位别名列表:p 表示%e 表示 emx 表示 ex 更多参考: https://blog.csdn.net/comphoner/article/details/79670148","categories":[{"name":"html","slug":"html","permalink":"http://fanghl.top/categories/html/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"designMode","slug":"designMode","date":"2019-09-23T06:37:55.000Z","updated":"2023-11-03T03:29:00.557Z","comments":true,"path":"2019/09/23/designMode/","link":"","permalink":"http://fanghl.top/2019/09/23/designMode/","excerpt":"","text":"设计模式五大设计原则","categories":[{"name":"designMode","slug":"designMode","permalink":"http://fanghl.top/categories/designMode/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"Hexo建站","slug":"hexo","date":"2019-09-19T12:43:17.000Z","updated":"2023-11-03T03:29:00.559Z","comments":true,"path":"2019/09/19/hexo/","link":"","permalink":"http://fanghl.top/2019/09/19/hexo/","excerpt":"","text":"序言NEXT主题: http://theme-next.iissnan.com/getting-started.html参考文章: https://blog.csdn.net/sinat_37781304/article/details/82729029next参考: https://www.jianshu.com/p/5e56839ef917步骤: 12345678安装Git安装Node.js安装HexoGitHub创建个人仓库生成SSH添加到GitHub将hexo部署到GitHub设置个人域名发布文章 安装$ npm install hexo-cli -g安装后检查是否安装成功:$ hexo -v成功后进行初始化:$ hexo init myBlog安装组件: 1234$ cd myBlog$ npm install``` 至此,新建完成!目录会存在以下结构: node_modules: 依赖包public:存放生成的页面scaffolds:生成文章的一些模板source:用来存放你的文章themes:主题** _config.yml: 博客的配置文件** 1查看刚刚创建的hexo博客: $ hexo g$ hexo s 123456789打开localhost:4000就可以看到啦#### 创建GitHub仓库创建一个和你用户名相同的仓库,后面加.github.io,只有这样,将来要部署到GitHub page的时候,才会被识别,也就是xxxx.github.io,其中xxx就是你注册GitHub的用户名#### hexo部署到GitHub将hexo和GitHub关联起来,也就是将hexo生成的文章部署到GitHub上,打开站点配置文件 _config.yml,翻到最后,修改为 deploy: type: git repo: https://github.com/YourgithubName/YourgithubName.github.io.git branch: master 1此时需要安装deploy-git ,也就是部署的命令,这样你才能用命令部署到GitHub $ npm install hexo-deployer-git –save 1然后: $ hexo clean$ hexo generate$ hexo deploy $ hexo clean$ hexo d -g 1234567891011121314151617181920212223242526272829303132333435其中 deploy 时会要求输入 username 和 password (git账户密码) 之后,打开 http://yourname.github.io 这个网站就可以看到你的博客了!!#### 发布博客和线上GitHub关联后,新增一篇博客: `$ hexo new post <your blogName>` 编辑好文章发布部署: `$ hexo d -g `清除缓存: `$ hexo clean`#### 本地启Hexo动服务`$ hexo s || hexo serve` 默认端口:4000,修改端口号: `$ hexo serve -p 5000`#### 草稿#### 页面丰富##### 公益404:`$ hexo new page 404` 进入刚才生成的 \\source\\404\\index.md 添加: ```html<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8"> <title>404</title> </head> <body> <script type="text/javascript" src="//qzonestyle.gtimg.cn/qzone/hybrid/app/404/search_children.js" charset="utf-8"></script> </body></html> tags/标签页$ hexo new page tags配置tags页面,进入到刚才生成的 \\source\\tags\\index.md文件,添加type字段: 123title: 标签date: 2019-09-19 19:49:32type: "tags" 在文章中设置对应的标签即可: 1tags: [xxx,xxx,xxx] 分类页面/categories$ hexo new page categories配置categories页面,进入到刚才生成的 \\source\\categories\\index.md文件,添加type字段: 1234title: 分类date: 2019-08-19 15:11:42type: "categories"comments: false 设置每篇博客的 categories:categories: xxx 功能点字数统计、时长主题配置文件 _config.yml 中打开 wordcount 统计功能即可 123456post_wordcount: item_text: true wordcount: true #字数统计 min2read: true #阅读时长 totalcount: true separated_meta: true 配置之后还是没出现字数统计和阅读时长,可能是因为未安装 hexo-wordcount 插件,安装即可:$ npm insatll --save hexo-wordcount重启服务,OK 站内搜索安转插件npm install hexo-generator-searchdb --savehexo站点配置文件_config.yml,任意位置手动添加: 12345search: path: search.xml field: post format: html limit: 10000 修改主题(next)配置文件_config.yml,启用local_search 1234local_search: enable: true trigger: auto top_n_per_article: 1 ok GitHub page 404已经部署好的hexo,在我们修改了git仓库的属性之后(我是切换了仓库的公开和私有属性),再次访问域名就会 page 404,检查了仓库文件和本地hexo配置文件,均未改动。 解决: 在线上git仓库的setting中,选择theme-> Custom domain,在这里重新输入你的域名,保存即可,重新打开域名,页面已经回来了。hexo的域名绑定是双向的! hugo博客框架: go语言编写。多线程编译。速度快","categories":[{"name":"Hexo","slug":"Hexo","permalink":"http://fanghl.top/categories/Hexo/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"Charts","slug":"charts","date":"2019-09-17T02:33:17.000Z","updated":"2023-11-03T03:29:00.557Z","comments":true,"path":"2019/09/17/charts/","link":"","permalink":"http://fanghl.top/2019/09/17/charts/","excerpt":"","text":"序言antv蚂蚁官网: https://antv.alipay.com/zh-cn/index.html G2G2引入 CDN:<script src="https://gw.alipayobjects.com/os/lib/antv/g2/3.4.10/dist/g2.min.js"></script> NPM:$ npm install @antv/g2 --saveimport G2 from '@antv/g2' 本地脚本:<script src="./g2.js"></script> G2封装实战一个G2实例只能创建一个图表,若需要多个图表,可以封装G2 实例: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687/** * G2 * charths函数 * warn:数据格式相同可复用一个函数,否则请重新另创图表函数 */// utils/charts.jsexport function chartInstance( point, chartData, config={} ) { const [axisX, axisValue, axisY] = Object.keys(chartData[0]) const chart = new G2.Chart({ container: point, //point挂载点ID forceFit: config.width ? false : true, //表宽自适应配置 height: config.height || 600, width: config.width || null, //若配置forceFit,则width不生效 }); chart.source(chartData, { axisX: { range: [0, 1], min: 0, max: 100 } }); chart.tooltip({ crosshairs: { type: 'line' } }); chart.axis(axisY, { label: { formatter: function formatter(val) { return val + '¥'; } }, title: { textStyle: { fontSize: 12, // 文本大小 textAlign: 'center', // 文本对齐方式 fill: '#999', // 文本颜色 } }, line: { lineDash: [3, 3] } }); chart.line().position(`${axisX}*${axisY}`).color(`${axisValue}`).shape('smooth'); //平滑曲线图 chart.point().position(`${axisX}*${axisY}`).color(`${axisValue}`).size(4).shape('circle').style({ stroke: '#fff', lineWidth: 1 }); chart.render(); return chart}//showData.vueimport { chartInstance} from '@/utils/charts'data() { return{ chartIncome:'', chartData2: [{}], }},mounted() { this.chartIncome = chartInstance('c1', this.chartData2) //返回值很重要,关乎数据变动 this.chartRegister = chartInstance('c2', this.chartData2) this.chartActive = chartInstance('c3', this.chartData2) },methods: { //切换数据 onChangeIncome(e) { const dateChange = e.target.value switch(dateChange) { case 'a': this.chartIncome.changeData(this.chartData2) break case 'b': this.chartIncome.changeData(this.chartData3) break case 'c': this.chartIncome.changeData(this.chartData) break } },}","categories":[{"name":"Antv-G2","slug":"Antv-G2","permalink":"http://fanghl.top/categories/Antv-G2/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"javascript","slug":"js","date":"2019-09-12T08:25:21.000Z","updated":"2023-11-03T03:29:00.560Z","comments":true,"path":"2019/09/12/js/","link":"","permalink":"http://fanghl.top/2019/09/12/js/","excerpt":"","text":"Array属性:Array.length 方法:Array.from()从类数组对象或者可迭代对象中创建一个新的数组实例 123<!-- 数组去重 -->const arr = [1,3,5,56,3,2,1]const res = Array.from(new Set(arr)) Array.isArray()用来判断某个变量是否是一个数组对象。 12const obj = {'key': 'value'}Array.isArray(obj) // false Array.of()根据一组参数来创建新的数组实例,支持任意的参数数量和类型 Array instance属性:Array.prototype.constructor返回值 Array Array.prototype.length返回值长度 方法:修改器方法:下面的这些方法会改变调用它们的 对象自身 的值 123456789Array.prototype.copyWithin()Array.prototype.fill()Array.prototype.pop()Array.prototype.push()Array.prototype.reverse()Array.prototype.shift()Array.prototype.sort()Array.prototype.splice()Array.prototype.unshift() 访问方法:以下方法不会改变调用它们的对象的值,只会返回一个新的数组或者返回一个其它的期望值。 12345678Array.prototype.concat()Array.prototype.includes()Array.prototype.join()Array.prototype.slice()Array.prototype.toString()Array.prototype.toLocaleString()Array.prototype.indexOf()Array.prototype.lastIndexOf() 迭代方法: 1234567Array.prototype.forEach()Array.prototype.every()Array.prototype.some()Array.prototype.filter()Array.prototype.map()Array.prototype.reduce()Array.prototype.reduceRight() String属性:String.prototype 方法:String.fromCharCode()通过一串 Unicode 创建字符串 String instance属性:String.prototype.constructor返回值 String String.prototype.length字符串长度 方法: 123456789101112131415161718192021222324252627282930String.prototype.charAt()String.prototype.charCodeAt()String.prototype.codePointAt()String.prototype.concat()String.prototype.includes()String.prototype.endsWith()String.prototype.indexOf()String.prototype.lastIndexOf()String.prototype.localeCompare()String.prototype.match()String.prototype.normalize()String.prototype.padEnd()String.prototype.padStart()String.prototype.repeat()String.prototype.replace()String.prototype.search()String.prototype.slice()String.prototype.split()String.prototype.startsWith()String.prototype.substr()String.prototype.substring()String.prototype.toLocaleLowerCase()String.prototype.toLocaleUpperCase()String.prototype.toLowerCase()String.prototype.toUpperCase()String.prototype.toString()String.prototype.trim()String.prototype.trimLeft()String.prototype.trimRight()String.prototype.valueOf() ObjectObject构造函数方法1234567891011121314151617181920Object.assign()Object.create()Object.defineProperty()Object.defineProperties()Object.entries()Object.freeze()Object.getOwnPropertyDescriptor()Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Object.getPrototypeOf()Object.is()Object.isExtensible()Object.isFrozen()Object.isSealed()Object.keys()Object.values()Object.preventExtensions()Object.seal()Object.setPrototypeOf()delete obj.property","categories":[{"name":"Javascript","slug":"Javascript","permalink":"http://fanghl.top/categories/Javascript/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"Git","slug":"Git","date":"2019-09-11T03:43:41.000Z","updated":"2023-11-03T03:29:00.556Z","comments":true,"path":"2019/09/11/Git/","link":"","permalink":"http://fanghl.top/2019/09/11/Git/","excerpt":"","text":"Git实用命令总结总结了部分开发中常用的git操作命令,根据实际业务遇到的情况梳理。包括但不限于分支新建、分支合并、分支合并冲突、dist文件冲突、版本回退等。 命名规范 标记 解释 feat 新功能 fix 修补bug doc 文档 style 格式,不影响代码 refactor 重构,不添加新功能,也非修补bug test 增加测试 chore 构建过程或辅助工程变动 scope 用于说明commit影响的范围 问题解决修改文件大小写Windows对大小写不敏感,git对大小写不敏感,需要修改文件名的大小写,实际修改了git却不会生效的,解决: 1. 复制此文件到其他地方备份 2. 删除项目中的该文件 3. 提交代码 4. 重新添加此文件到项目 5. 提交代码,over git实用命令git stash想要切换分支,本地却已经做了改变。切换会报错,此时可以使用 git stash保存当前的修改,等处理完其他分支事务在回来‘取出保存’的修改即可。 命令 作用说明 git stash 保存当前工作区和暂存区的修改 git stash save ‘注释信息’ 作用同上,加上了注释信息方便区分 git stash list 查看保存列表 git stash pop 恢复最近一次保存并删掉保存列表的记录,只恢复工作区 git stash pop –index 与上面命令的效果一样但是还会恢复暂存区! git stash pop stash@{序号} 恢复保存列表里指定的保存记录,并把恢复的记录从保存列表中删除 git stash apply 恢复最近的保存记录但不会删除保存列表里面对应的记录 git stash drop 删除保存列表里面最近一条保存记录。后面加 stash@{序号}可以删除指定的保存记录 git stash clear 删除保存列表里面所以保存记录(清空保存列表) git stash 分支名 stash@{序号} 修改了文件,此次修该使用了 git stash 保存,然后继续修改了该文件,此时再用 git stash pop 或 git stash apply 恢复之前的保存,可能会出现冲突。此时使用该命令 git stash 分支名 stash@{序号} 会创建一个分支然后在创建的分支上把保存的记录恢复出来,避免冲突。 PS : git stash 保存的修改可以跨分支应用。例如:在 develop分支上做了修改, *git stash保存,切换到 *master 分支,使用 pop 或 apply 拉出来保存,这样就可以把 develop 分支上修改的内容迁移到 master 上,解决冲突可能会遇到。 git撤回文件 命令 作用说明 git reset HEAD 放弃暂存区的修改(已经add,未commit) git checkout – * 放弃本地修改(未commit) git reset –soft HEAD^ 撤销commit 撤销线上仓库的commit适用于错误的push后没有他人再次push 12$ git reset --soft <commitHash>$ git push --force git revert撤销几次之前的commit,又需要保留该commit之后的提交时,需要用到 revert 123$ git log $ git revert commit_hash$ git push git reset 本质即把指针 HEAD 指向某一个 commitgit revert 本质不算是回滚,是反做,反向操作commit。正常情况下,每一次操作文件后会让 Git 时间线往前走一步,revert反向操作某一个commit 记录,并生成一个新的 commit 来反做 配置别名长命令的别名配置 1$ git config --global alias.st status 一个比较实用的别名配置↓ 1git config --global alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit" 每个仓库的Git配置文件都放在.git/config文件中 1$ cat .git/config 12345[branch "master"] remote = originq merge = refs/heads/master[alias] last = log -1 //命令作用区域 配置别名也可以直接修改这个文件↑ 新建分支作业修改bug、添加新功能等,在主分支上开一个新分支进行作业,待作业完成后,再合并回主分支,并删掉新开的分支。 方法一:1234master分支$ git checkout master $ git checkout -b xqcircle origin/xqcircle //创建新分支并关联在远程同名分支上$ git push origin HEAD //把该分支推送到远程,即可以在git仓库看到 方法二:1234$ git branch dev $ git branch -a$ git branch -b branch_name //本地先创建该分支$ git push --set-upstream origin branch_name //本地分支推送到远程同名分支,且本地分支会自动track该分支 拉取分支远程存在分支,本地没有该分支,用以下命令拉下来 1$ git checkout --track origin/branch_name 合并回主分支12$ git checkout master$ git merge branch_name 删除远程分支12$ git branch -r -d origin/branch_name $ git push origin :branch_name 删除本地分支12$ git branch -d branch_name$ git branch -D branch_name 标签tag一个版本上线定义一个版本标签,方便快速回退该版本。 123git tag //列出所有taggit tag -r //查看远端所有分支git tag -l 'v2.0.1' //过滤tag 新建tag1git tag xqCircle-v2.0.0 查看tag,commit号1git show tagName 给某个commit打上tag1git tag -a v1.0.0 commitId -m 'my tag' 推送tag到服务器12git push origin tagName //推送某个具体taggit push origin --tags //推送本地所有tags 切换到某个tag跟分支一样,可以直接切换到某个tag去。这个时候不位于任何分支,处于游离状态 1git checkout xqCircle-v2.0.1 切换到某tag并新建分支1git checkout -b branchName tagName 删除tag12git tag -d xqCircle-v2.0.1 //本地删除git push origin :refs/tags/xqCircle2.0.1 //远端删除 port 22 fail“connect to host github.com port 22: Connection timed out fatal: Could not read from remote repository.”这个报错,优先解决本地是否正确的链接上线上仓库 git remote set-url origin git@yourGitUrlHeregit@yourGitUrlHere 为线上github的仓库访问地主","categories":[{"name":"Git","slug":"Git","permalink":"http://fanghl.top/categories/Git/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"Python与JavaScript对照学习总结点","slug":"learnPython","date":"2019-08-28T08:16:41.000Z","updated":"2023-11-03T03:29:00.560Z","comments":true,"path":"2019/08/28/learnPython/","link":"","permalink":"http://fanghl.top/2019/08/28/learnPython/","excerpt":"","text":"序言Python: 廖雪峰-Python传送门: https://www.liaoxuefeng.com/wiki/1016959663602400命令行打开 .py 文件;Python交互环境,执行Python代码 缩进方式进行代码格式!!4格空格缩进大小写敏感 数据类型: 整数 浮点数 字符串 布尔值 =》 and, or, not 空值 None 列表 字典 变量: 常量,即不能变的变量,指针指向不变 类const,常量一般为变量名全部大写 全局变量:全局变量globalData的定义,在读完了教程之后也没发现关于全局变量的定义,基于js的思想,在js中,全局变量取决于该变量定义的位置,传统web编程中,未添加定义符号(var, let, const)的变量都可被称为”全局变量”,即使是 var 也存在一个变量提升的问题。Python中全局变量一般有两种方式: 声明式关键字 global 定义变量法。可以直接进行全局变量声明。global OLD_URL 模块法模块法和js大同小异,js中没有关键字 global ,但是模块引用也是非常好用的,在中大型项目中,我们把项目中用到的常量单独提取出来放置在js文件中。在通过 import 来导入使用。Python中也是如此。 除法: / 浮点数除法,即便是两个整数相除,结果也是浮点数 // 地板除,两个整数的除法仍然是整数,结果只取整数 10 // 3 =》 3 % 求模取余 字符串编码: ord() 数获取字符的整数表示 chr() 函数把编码转换为对应的字符Python的字符串类型是str,在内存中以Unicode表示Python对bytes类型的数据用带b前缀的单引号或双引号表示 x = b’abc’ encode() 方法可以编码为指定的bytes decode() 方法 把bytes变为str如果bytes中只有一小部分无效的字节,可以传入errors=’ignore’忽略错误的字节: b’\\xe4\\xb8\\xad\\xff’.decode(‘utf-8’, errors=’ignore’) len() 方法计算str的字符数1个中文字符经过UTF-8编码占用3个字节,而1个英文字符只占用1个字节。 坚持 utf-8 编码 文件开头写上: 12#!/usr/bin/env python3# -*- coding: utf-8 -*- 格式化: Python与C一致,都采用 % 实现%运算符就是用来格式化字符串的。在字符串内部,%s表示用字符串替换,%d表示用整数替换'age: %s. nmae: %s' %(25, fhl) format() 格式化,比较麻烦 list 和 tuple : list : 列表。类数组 Arrayclassmates = ['xaioming', 'xiaohua', 'xiaoliu']获取最后一个元素 classmates[len(classmates) - 1] 或者 classmates[-1] list 方法:append(content) 末尾插入insert(index, content) 插入指定位置pop() 删除末尾元素pop(index) 删除指定位置元素 tuple 元组,有序列表一旦初始化,不能更改。没有append、insert方法,其他和list一致因为不能更改,故更为安全,能用tuple代替list就尽量用tuple!t = (1,)t = ('str', 23, ['a']) 条件判断: 123456if age >= 18: print('成年人')elif age >= 1: print('幼儿期')else: print('婴儿期') int() 转化为整数函数 循环 for…in循环 while 循环 range() 生成一个整数序列,再通过list()函数可以转换为list break 退出循环 配合if使用 continue 跳过循环 配合if使用 使用dict和set:dictionary字典,其他语言叫map,使用key-value存储,也就是js的对象。json的话,本质是字符串,也可以类比吧。d = {'name': 'fhl', 'age': 22, }PS: 区别点: js的对象可以使用 . 方法调用,Python目前只能 d[‘age’]或者 d.get(‘age’,-1)获取存储的值删除一个key,可以用 pop(key)dictionary是空间换时间的方法,list时间换空间 set同js中的key同根同源,存储key的集合,不存储value,且key不重复!!!创建set,需要一个list作为输入集合 add(key) 添加元素到set中,重复添加不会有效果 remove(key) 删除元素 set可看做成无序、无重复元素的集合,因此,两个set可以做数学意义上的交集、并集等1234s1 = set([1, 3, 54])s2 = set([2, 3, 4])s1 & s2 # 并 ,单个& s1 | s2 # | 或,单个| 函数自带函数(内置函数): abs() 、max()数据类型转换函数: int() float() str() bool() 参考js的数据转换函数 String() Number() Boolean() 定义函数:关键字: def, 依次写出函数名、括号、括号中的参数和冒号:,然后,在缩进块中编写函数体,函数的返回值用return语句返回类比js: 关键字function ,后面都一致,然后把 冒号 换成 js中的 大括号 即可 12345def my_abs(num): if x >= 10: return x else: return -x 执行函数: 相比于Python,js的函数执行比较简单,定义完函数后,直接用函数名字加一对小括号就可以调用当前函数,但Python貌似得先导入该函数,才可以调用 12from test import my_absmy_abs(-1) pass关键字pass语句啥都不做,就和0一样,用来占位的。让程序可以跑起来。可以理解为斗地主时,你的牌大不过上家的,你就可以大吼一声: pass/过,让单线程的斗地主可以走下去,而不至于卡在这里,让队友喷你 Python函数返回值js函数没有return语句时,返回的是 undefined。Python返回的是 None,在存在返回多个值的情况下。Python返回的是tuple,一个tuple可以被好多个变量接收,具体场景参考ES6的解构赋值,一毛一样。 默认参数没啥好讲的,和ES6函数默认参数一毛一样,默认参数在后,必填参数在前 可变参数参数前面添加 * ,在函数内部,参数numbers接收到的是一个tuple,因此,函数代码完全不变。但是,调用该函数时,可以传入任意个参数,包括0个参数,有点意思奥,即便我们的传参是list或者tuple,也可以在参数前加 * ,使得list或者 tuple 变为可变参数传进去 关键字参数**ky可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple。而关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。关键字参数有什么用?它可以扩展函数的功能。比如,在person函数里,我们保证能接收到name和age这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求 12345678def person(name, age, **kw): if 'city' in kw: # 有city参数 pass if 'job' in kw: # 有job参数 pass print('name:', name, 'age:', age, 'other:', kw) 参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。 总结: *args是可变参数,args接收的是一个tuple; **kw是关键字参数,kw接收的是一个dict。 递归函数:尾递归优化1234def fact(n): if n==1: return 1 return n * fact(n - 1) 以上代码是Python的递归代码,额,可能耦合性较高,函数内部使用了当前函数的名字,这高度耦合,在js里面我们可以通过 arguments.callee解决调用自身的问题,减小耦合。以下是js代码: 123456function fact(n) { if( n == 1 ) { return 1 } return n * arguments.callee(n-1)} 高级特性:切片 slice操作符slice切片操作符,简单来说,就是给js的slice()函数做了一个语法糖,其他都一致 12L[0:5] #[start:end]但不包括end,L为list或tupleL[-2:-1] #倒数第一个元素索引为-1 所有数,每5个取一个 123456L = list(range(100))L[:10] # 前十个数L[:-10] # 后十个数 L[:10:2] # 前10个数,每两个取一个L[::5] # 所有数,每5个取一个L[:] # 赋值list 切片也可以对字符串使用,不需要单独的类似substring()方法 迭代定义: 循环遍历list或者tuple,叫做迭代。迭代通过 for…in…来完成Python可以迭代一切可迭代的东西判断一个对象是否是可迭代对象呢? 12from collections import Iterableisinstance('abc', Iterable) # 判断str是否是可迭代的对象,返回Boolean 列表生成式就是简化了复杂列表生成的繁琐步骤 1234567L = [] # 实现一个 1*1, 2*2,....10*10的列表for x in range(1, 11): # 传统方法 L.append( x * x )[x*x for x in range(1, 10)] # 列表生成器[x*x for x in range(1, 10) if x % 2 == 0 ] # 还可以加上 if 判断[m+n for m in 'abc' for n in 'xyz'] # 双层循环 接下来,这行代码可能会让jser稍微羡慕一下,那就是操作文件 12import os # 拿到了当前目录的所有文件夹[d for d in os.listdir('.')] js不能操作文件的,当然表亲 node.js是可以的dict的items() 方法可以同时迭代key和value,那么: 12d = {'x': 'A', 'y': 'B', 'z': 'C'}[k + '=' + v for k, v in d.items() ] 12d = {'Hello', 'WorLD'} # 把list中所有的字符串小写[s.lower() for s in d] 生成器列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生generator 列表生成式的 [] 改成 () 即可创建生成器12L = [x * x for x in range(10) ] #列表生成式g = (x * x for x in range(10) ) # g是一个generator、next()方法打印值 generator保存的是算法,每次调用next(g),就计算出g的下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出StopIteration的错误一般,generator用for来循环,不用next() 12for n in g: print(n) 如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator:1234567def fib(max): n, a, b = 0, 0, 1 while n < max: yield b a, b = b, a + b n = n + 1 return 'done' PS::: enerator和函数的执行流程不一样。函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行用for循环调用generator时,发现拿不到generator的return语句的返回值。如果想要拿到返回值,必须捕获StopIteration错误,返回值包含在StopIteration的value中: 1234567g = fib()while True: try: x = next(g) except StopIteration as e: print('Generator return value:', e.value) break 迭代器可以直接作用于for循环的数据类型有以下几种: 一类是集合数据类型,如list、tuple、dict、set、str等; 一类是generator,包括生成器和带yield的generator function。 这些可以直接作用于for循环的对象统称为可迭代对象:Iterable。 可以使用isinstance()*判断一个对象是否是 *Iterable 对象 而生成器不但可以作用于for循环,还可以被next()函数不断调用并返回下一个值,直到最后抛出StopIteration错误表示无法继续返回下一个值了。 可以被next()函数调用并不断返回下一个值的对象称为迭代器:Iterator。 可以使用 isinstance() 判断一个对象是否是 Iterator 对象: 1isinstance([], Iterator) 生成器都是Iterator对象,但list、dict、str虽然是Iterable,却不是Iterator。 把list、dict、str等Iterable变成 Iterator 可以使用 iter() 函数: 1isinstance(iter([]), Iterator) # True PS: Python的for循环本质上就是通过不断调用next()函数实现的 函数式编程高阶函数函数名是指向函数的变量(同js ),即函数本身可以被变量指着,在变量引用也是可以的一个函数接受另一个函数作为参数,这种函数称为高阶函数(同js) map/reducemap 和 js 的功能一致,即都是为 Iterable 的全部元素应用一种规则。这个规则一般是一个函数。不过语法上稍有不同,js的map是Array的一个方法。 python: map()函数接收两个参数,一个是函数,一个是Iterable,map将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。 12345def f(x): return x*xr = map(f, [1, 2, 3, 4]) # 返回的是list每项的平方list(map(str, [1, 2, 32, 56]) # 把每一项变为字符串 123456789//js实现const arr = [1, 2, 3, 4]const r = arr.map(function(item) { return item*item})// ES6const r = arr.map(item => { return iten*item}) 为什么js的map就只是Array的一个方法呢?我个人觉得。js的数据结构并没有Python那么灵活,因为js的for循环只能循环Array和Object,而反观Python就比较多了,本质来说,就是 Iterable 数据结构js只有Array和Object。而Python有很多,除了list和tuple,还有string也算,等等 reducereduce把一个函数作用在一个序列[x1, x2, x3, …]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算,其效果就是: 1reduce(f,[x1, x2, x3]) = f(f(f(x1, x2,x3))) #三个f关系?其实这就说明了reduce()这个函数的作用了。 比如说序列求和: 1234from functools import reduce def add(x, y): return x + yreduce(add, [1, 3, 5, 7, 9]) #25 filter()和map()类似,filter()也接收一个函数和一个序列。和map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素。 例如,在一个list中,删掉偶数,只保留奇数,可以这么写: 12def is_odd(n): return n % 2 == 1 js中的filter同样是Array的一个方法,用于过滤数组并返回一个新的数组 1234const arr = [5, 16, 35, 15, 48]const r = arr.filter(item => { //返回r是一个大于18的数组 item >= 18}) 相比于js来说。Python的filter和map类似,都可以作用于Iterable数据类型 sorted()排序内置函数,用法和js的sort()类似。但是js的sort()方法在未传参数的情况下,默认按照字符编码的顺序进行排序。12L = [1, 5, 15, 25, 8]r = sorted(L) #1,5,8,15,25 js的sort(): 12345let arr = [1, 5, 15, 25, 8]arr.sort() // 1,15,25,5,8arr.sort(function(a, b){ //1,5,8,15,25 return a - b}) 看下sorted()的强大:可以传入三个参数,第一就是排序的list,第二个是key的规则,第三个是反转倒叙: 1sorted(['bov','lv','hln', 'Zomp'], key = str.lower, reverse = True) PS : sorted()也是一个高阶函数。用sorted()排序的关键在于实现一个映射函数 返回函数简单看了下返回函数的定义,里面提到了闭包,这个js里面也经常提到的操作。简单说,闭包就是函数嵌套函数,内部的函数保存了外层函数的变量等参数,在外部函数被销毁后,内部函数依旧可以拿到外部函数的传参。这个概念js和Python没大的区别。深层次理解的话,参考另外一篇博客: https://www.cnblogs.com/fanghl/p/11417906.html 匿名函数关键字: lambda只能有一个表达式,不用写return,返回值就是表达式的结果 1234list(map(lambda x: x * x, [1, 3, 5, 7] ))#lambda x: 相当于:def f(x): return x * x python的匿名函数和js的匿名函数不太一样,但作用大都相似,不用担心函数名冲突等等,简化写法等。js里面的匿名函数已经升级到了ES6箭头函数模式,简单方便: 123456item => { return item * item}(x, y) => { return x + y} js里面匿名函数用的较少,一般都是用了箭头函数替代了。匿名函数的使用场景我也想不出多少,但在定时器中使用较多: 1234567891011function test() { setTimeout(function() { console.log(1) }, 1000 * 2)}// 不过一般都箭头简化了function test() { setTimeout(() => { console.log(1) }, 1000 * 2)} 装饰器代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)函数也是一个对象,而且函数对象可以被赋值给变量,所以,通过变量也能调用该函数name 函数对象的该属性可以拿到函数的定义时名字没看懂,等懂了再回来写 偏函数个人理解又是一个语法糖!减少一些函数的繁杂写法关键模块:functools partial 123import functoolsint2 = functolls.partial(int, base = 2)# 创造一个偏函数int2,来进行2进制的转化,base是int内置函数固有的参数 模块模块是一组Python代码的集合,可以使用其他模块,也可以被其他模块使用创建自己的模块时,要注意:模块名要遵循Python变量命名规范,不要使用中文、特殊字符;模块名不要和系统模块名冲突,最好先查看系统是否已存在该模块,检查方法是在Python交互环境执行import abc,若成功则说明系统存在此模块 作用域__xxx__ 变量是特殊变量,可以被直接引用,但是有特殊用途_xxx或__xxx这样的函数或者变量是非公开的(private),不应该被直接引用 第三方模块安装 pip:安装第三方模块工具安装命令:pip install xxx 参照 npm 或者 yarn 包管理工具 安装常用模块 在使用Python时,我们经常需要用到很多第三方库,例如,上面提到的Pillow,以及MySQL驱动程序,Web框架Flask,科学计算Numpy等。用pip一个一个安装费时费力,还需要考虑兼容性。我们推荐直接使用Anaconda,这是一个基于Python的数据处理和科学计算平台,它已经内置了许多非常有用的第三方库,我们装上Anaconda,就相当于把数十个第三方模块自动安装好了,非常简单易用。Anaconda官网: https://www.anaconda.com/download/国内镜像: https://pan.baidu.com/s/1kU5OCOB#list/path=%2Fpub%2Fpython 面向对象编程 OOP面向过程处理学生的成绩表,为了表示一个学生的成绩,面向过程的程序可以用一个dict表示 12std1 = {'name': 'bob', 'score': 95}std1 = {'name': 'ming', 'score': 59} 处理学生成绩通过函数实现,打印学生成绩: 12def print_score(std): print('%s: %s' % (std['name'], std['score'])) 面向过程,顾名思义,关心的是程序下一步怎么走?这个过程如何保持正确的走法。而面向对象,即万物皆对象,我们要考虑学生这个对象,然后直接创建这个对象,需要什么功能直接调用这个对象上面的方法即可,不用管过程! 面向对象1234567891011121314class Student(object): def __init__(self, name, score): self.name = name self.score = score def print_score(self): print('%s: %s' % (self.name, self.score))xiaoming = Student('xiaoming', 95)xiaohong = Student('xiaohong', 59)xiaoming.print_score()xiaohong.print_score() 奥,语言都是相通的,JavaScript的class和Python思路都是一毛一样啊,不过js中class还有继承、super()、constructor()等等,Python应该也有的,往下继续学习。PS : js中定义的Class,创建实例需要 new 关键字 类和实例 创建类class + className + (继承自某个类) 12class Students(object): def __init__(self, xxx, xx1, xx2): 1234567891011121314class Student extends Person{ //js实现 super() //继承基类的属性 constructor(name, age) { //自己的属性 this.speed = 30 this.name = name this.age = age } otherMethods() { //挂载到Student的原型链上 doSth... }, otherMethods1() { //挂载到Student的原型链上 doOther... }} 创建实例12# pyxiaoming = Student(arg) 12// jsconst xioaming = new Student(arg) 看到这,终于深刻体会到了为啥class一定要首字母大写!js里面可能体会不深,因为有 new 关键字在class之前,而py里面,如果不区分,那么很容易搞混class 和 function 特殊方法__init__ 方法的第一个参数永远是self,表示创建的实例本身,因此,在init方法内部,就可以把各种属性绑定到self,因为self就指向创建的实例本身。diff:和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self,并且,调用时,不用传递该参数。 访问限制外部代码还是可以自由地修改一个实例的name、score属性: 12xiaoming = Student('xiaoming', 95)xioaming.score = 12 #可以修改实例的属性 大招来了!!!如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__这个大招js里面可没有啊在Python中,实例的变量名如果以__开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问 1234class Student(object): __init__(self, name, age): self.__name = name self__age = age 这样在外部就无法访问到内部的私有变量了,只能调用内部方法来访问。·····~真的是这样的吗?(手动滑稽.jpg),当然不是啦,之所有我们无法从外部访问到name,是因为Python解释器把该变量变成了 _Studentname ,所以呢,我们可以通过 短线 加 类名 加变量名继续来访问该私有变量吃饱了撑着了吗?哈哈 继承和多态有点C++的感觉了,毕竟js是没有显示的多态的~ 等等,我理解完了,凉凉打脸。Python的多态指的是基类和子类拥有同样的方法时,子类覆盖基类………………emmmm,按照js来说,这就是原型链的查找基本原理啊,先在自己内部找,找不到了就顺着原型链往上查找,一毛一样的……….把py中的class理解为一种数据结构,这个数据结构和py自带的list,tuple,dict 没有任何区别。那么 isinstance() 不就可以用了 12345678xiaoming = Student('xiaoming')isinstance(xiaoming, Student) # True isinstance可以理解为派生class Pupil(Student): passxiaoxioa = Pupil()isinstance(xiaoxiao, Student) # True 隔代的也算派生哦 鸭子模型 动态语言的鸭子类型特点决定了继承不像静态语言那样是必须的。 获取对象信息 type()判断对象类型,js中使用 typeof() , js判断字符串还可以更为准确的使用Object.prototype.toString().call()题外话了,js的type判断基本类型好用,其他就还是用 instanceof()这点上,JS和py还是高度相似的!!type() ===== typeof() ===== 基本类型isinstance() ==== instanceof() ===== 判断对象 dir()要获得一个对象的所有属性和方法,可以使用dir()函数,它返回一个包含字符串的list,比如,获得一个str对象的所有属性和方法仅仅把属性和方法列出来是不够的,配合getattr()、setattr()以及hasattr(),我们可以直接操作一个对象的状态, js中同理。setAttribute()、getAttribute()、hasAttribute() 实例属性和类属性总结一句话,就是class的类属性各个实例都会访问到,实例的实例属性各自相互独立。可以把这两个概念理解为 JS 基类中的方法,子类都会顺着原型链找到并访问到。 12class Student(object): school = 'hantaiMiddleSchool' #所有实例都可以访问到 面向对象高级编程__slots__slots的作用就是动态给class添加属性的一个约束,否则在class类建立完毕后,运行代码的时候动态随意绑定属性不就乱套了,需要一个约束,职责就是 slotsslots 英文: 插槽。在Vue中使用的广泛,可以理解为在这预先给你留了个位置,以后想用的时候可以用,没有留这个位置的话,以后相用都用不了,可以理解为图书馆同学帮你占座 1234567#pythonclass Student(object): __slots__ = ('name', 'age')>>> s = Student()>>> s.name = 'xiaoming' # 可以绑定成功>>> s.score = 98 # 会报错,“唉,这位置有人了,你坐不了(手动滑稽.jpg)” PS : 使用slots要注意,slots定义的属性仅对当前类实例起作用,对继承的子类是不起作用 @property解决问题前言:给Class绑定属性时,直接把属性暴露出去,写起来简单,调用起来简单,但是没办法检查参数,导致可以随便修改值。这不合理解决1: 123456789101112131415#Python#Python设置set、get方法来控制属性解决验证问题class Student(onject): def get_score(self): return self.score def set_scsoe(self, value): if not isinstance(value, int): raise ValueError('score must be int') if value < 0 or value > 100: raise ValueError('score must be 0-100') self.score = value 但是,上面的调用方法又略显复杂,没有直接用属性这么直接简单有没有既能检查参数,又可以用类似属性这样简单的方式来访问类的变量呢?对于追求完美的Python程序员来说,这是必须要做到的!还记得装饰器(decorator)可以给函数动态加上功能吗?对于类的方法,装饰器一样起作用。Python内置的@property装饰器就是负责把一个方法变成属性调用的:总结起来:语法糖,本质没变,但是简化了调用的繁琐程度 12345678910111213141516171819class Student(object): @property def score(self): return self.score @score.setter # 可读可写属性 def score(self, valule): if not isinstance(value, int): raise ValueError('score must be int') if value < 0 or value > 100: raise ValueError('score must be 0-100') self.score = value @property #只读属性 def age(self): return 23#这样,就依旧可以使用属性的.调用方法访问属性、设置属性值了 总结:@property广泛应用在类的定义中,可以让调用者写出简短的代码,同时保证对参数进行必要的检查,这样,程序运行时就减少了出错的可能性 多重继承关键字:一个子类可以拥有多个基类、mixinclass需要新的功能,只需要在继承一个基类就可以了,通常,主线都是单一继承下来的 * 为了更好地看出继承关系,我们把主线之外需要继承的基类命名为 Mixin,这样的的设计通常称为 *Mixin 12345678class Person(object): passclass ChineseMixin(Person): passclass ChinaPuple(Person, ChineseMixin): pass 定制类重点:前后双下划线的变量是特殊变量,py有特殊用途的!!__slots____len__()__str____repr____iter____getitem____setitem____getattr____call__上面罗列的方法未查看相关作用,以后需要用到在查看不迟,就最后一个 __call__ ,在js 中调用自身的有 arguments.callee() 枚举类…持续更新…….","categories":[{"name":"Python","slug":"Python","permalink":"http://fanghl.top/categories/Python/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"小程序项目自我总结","slug":"summary201907","date":"2019-08-22T08:16:41.000Z","updated":"2023-11-03T03:29:00.561Z","comments":true,"path":"2019/08/22/summary201907/","link":"","permalink":"http://fanghl.top/2019/08/22/summary201907/","excerpt":"","text":"序言小程序: 相亲小红圈+tool: wepy 传送门:[ wepy ] https://tencent.github.io/wepy/document.html#/[ Vue ] https://cn.vuejs.org/[ ES6 ] http://es6.ruanyifeng.com/[ git ] https://www.liaoxuefeng.com/ 七月份结束,项目上线,回过头来整理一下项目,项目为wepy1.7.0后版本开发的小程序,配套的后台前端使用ant-design-vue开发,上传测试服工具使用Xshell6。 内容 wepy构建工程具体可在wepy官网中查看,此处不多介绍。 1234$ wepy init standard my-project /**创建项目*/$ cd my-project /*进入项目目录*/$ npm install /**安装依赖*/$ wepy build --watch /*运行工程并监控项目修改自动刷新*/ wepy属于类Vue写法,要在wepy中使用异步操作(async/await)需要在工程的app.way入口文件中constructor函数中注册: 12345constructor () { super() this.use('requestfix') this.use('promisify') /*←手动添加这个*/} 生命周期:应用生命周期 属性 type 描述 触发时机 onLaunch Function 生命周期函数–监听小程序初始化 用户首次打开小程序,触发 onLaunch(全局只触发一次) onShow Function 生命周期函数–监听小程序显示 当小程序启动,或从后台进入前台显示,会触发 onShow onHide Function 生命周期函数–监听小程序隐藏 当小程序从前台进入后台,会触发 onHide 页面生命周期 属性 type 描述 触发时机 onLoad Function 监听页面加载,一个页面只会调用一次 小程序注册完成后,加载页面,触发onLoad方法,参数可以获取wx.navigateTo和wx.redirectTo及中的 query参数 onReady Function 监听页面初次渲染完成,代表页面已经准备妥当,可以和视图层进行交互 首次显示页面,会触发onReady方法,渲染页面元素和样式,一个页面只会调用一次 onShow Function 监听页面显示,当redirectTo或navigateBack的时候调用 当小程序有后台进入到前台运行或重新进入页面时,触发onShow方法。 onHide Function 监听页面隐藏,当navigateTo或底部tab切换时调用 当小程序后台运行或跳转到其他页面时,触发onHide方法 onUnload Function 监听页面卸载 当使用重定向方法wx.redirectTo(OBJECT)或关闭当前页返回上一页wx.navigateBack(),触发onUnload。 版本更新版本更新代码,一般较为固定,直接复制在 onLaunch 生命周期内 123456789101112131415161718192021222324if(wx.canIUse('getUpdateManager')){ const updateManager = wx.getUpdateManager(); updateManager.onCheckForUpdate((res) => { if(res.hasUpdate){ updateManager.onUpdateReady((res) => { wx.showModal({ title: '更新提示', content: '新版本已经准备好,是否重启应用?', success: function (res) { if(res.confirm){ updateManager.applyUpdate() } } }) }) updateManager.onUpdateFailed(function () { wx.showModal({ title: '已经有新版本了哟~', content: '新版本已经上线啦~,请您删除当前小程序,重新搜索打开哟~' }) }) } })} 组件组件复用说明: 组件复用,在 同一个页面,一个组件多次复用且每次传入不同的数据源,但是表现出来的数据源全部一致,其他的数据并没有渲染上原因: 组件名一致导致wepy认为是一模一样的组件,如此数据源也不会变动解决: 组件异名化 123456import CompA from 'path'components = { CompB: CompA, CompC: CompA,} 这样,既可以重复运用组件 文字换行小程序文字换行: 1<text>第一行\\n第二行\\n第三行\\n</text> 定时器 页面业务逻辑有需要用到倒计时功能,如下图。 在页面中有需要用到倒计时或者其他定时器任务时,新建的定时器在卸载页面时一定要清除掉,有时候页面可能不止一个定时器需求,在卸载页面(onUnload钩子函数)的时候一定要清除掉当前不用的定时器定时器用来做倒计时效果也不错,初始时间后台获取,前端处理,后台直接在数据库查询拿到的标准时间(数据库原始时间,T分割),前端需要正则处理一下这个时间: 123456let overTimeStr = data.over_time.split('T')let time1 = overTimeStr[0].replace(/-/g,\",\")let time2 = overTimeStr[1].replace(/:/g,',')let overTime = time1+ ',' + time2let overTimeArr = overTime.split(',')this.countDownCtrl( overTimeArr, 0 ); 最终把时间分割为[年,月, 日, 时, 分, 秒]的数组,(如果后端已经把时间处理过了那就更好了),然后把该数组传递给倒计时函数 123456789101112131415161718192021222324252627282930313233countDownCtrl( time, group ) { let deadline = new Date()//免费截止时间,月的下从0开始 deadline.setFullYear(time[0], time[1]-1, time[2]) deadline.setHours(time[3], time[4], time[5]) let curTimeJudge = new Date().getTime() let timeJudge = deadline.getTime()-curTimeJudge let remainTimeJudge = parseInt(timeJudge/1000) if( remainTimeJudge < 0) { log('倒计时已经过期') return; } this.interva1 = setInterval(() => { let curTime = new Date().getTime() let time = deadline.getTime()-curTime //剩余毫秒数 let remainTime = parseInt(time/1000) //总的剩余时间,以秒计 let day = parseInt( remainTime/(24*3600) )//剩余天 let hour = parseInt( (remainTime-day*24*3600)/3600 )//剩余小时 let minute = parseInt((remainTime-day*24*3600-hour*3600)/60)//剩余分钟 let sec = parseInt(remainTime%60)//剩余秒 hour = hour < 10 ? '0' + hour : hour; minute = minute < 10 ? '0' + minute : minute sec = sec < 10 ? '0' + sec : sec let countDownText = hour+ \":\" +minute+ \":\" +sec if( group === 0) { //个人业务逻辑,因为一个页面有两个倒计时需求,代码复用区分 this.countDown = countDownText; } else if( group === 1 ) { this.countDownGroup = countDownText } this.$apply() }, 1000 ); } 至此,倒计时效果处理完毕,PS:终止时间一定要大于currentDate,否则显示会出现异常(包括但不限于倒计时闪烁、乱码等) 最后,退出该页面去其他页面时,一定要在页码卸载钩子中清除倒计时!!! 123onUnload() { clearInterval(this.interva1);} 组件传值组件传值和Vue有点细微区别,Vue强调父组件的数组和对象不要直接传到子组件使用,应为子组件可能会修改这个data,如图: 但是,wepy中,有时候确实需要把一个对象传递到子组件使用,单个传递对象属性过于繁琐,而且!!!如果单个传递对象的属性到子组件,如果该属性是一个数组,则子组件永远会接收到 undefined 。此时最好用整个对象传值替代单个对象属性逐个传值的方法,且一定要在传值时加入 .sync 修饰符,双向传值绑定。确保从接口拿到的数据也能实时传递到子组件,而非 undefined :circleMembersList.sync="circleMembersList" 阻止组件的点击事件传播解决: 添加函数 catchtap=”funcName” 即可,funcName可为空函数,也可以直接不写 token判断 小程序调试时,有时候会出现首次打开无内容(拿不到数据)的状态,需要“杀死”小程序再打开才能看到数据内容,其中可能的原因之一便是 token 的失效。在与后台交互的时候,token必不可少。尤其是在小程序分享出去的链接,由其他用户点开分享链接进入小程序内部,此时更是要判断token,token的判断一般选在 onShow()钩子执行而不在 onLoad()钩子内执行。若不存在token,则应该执行登录去拿取token,再进行业务逻辑 1234567891011onShow() { const that = this; if( !wepy.getStorageSync('token') ) { wepy.login().then(async (res) => { if(res.code) { let code = res.code; await that.login(code) } }); } } formid 微信提供了服务通知,即在你支付、快递等行为时,微信会直接给你发一个服务通知(模板消息)来提醒,每次提醒都会消耗该用户存储的formID,formID为消耗品,用一个少一个,只有通过用户的表单提交行为才可以积攒formID 12345678<form @submit=\"submitForm\" report-submit=\"true\"> <button form-type=\"submit\" class=\"editCard\" @tap = \"goModifiPage('editFormTab')\">修改</button></form>//js方法submitForm(e) { this.postFormId( e.detail.formId ) // 向后端传输formid} 支付 准备: crypto-js.js && md5.js 微信支付流程为: 前端点击支付按钮拉起支付 ==》 准备加密数据 ==》 调用后端接口,传入需要的加密数据 ==》 后端验证加密数据,再返回加密数据 ==》 前端拿到后端加密数据(时间戳、内容、签名),对时间戳和内容进行本地签名,再判断本地签名和后端签名是否一致,若不一致,直接返回,退出支付,支付失败!若一致,对刚刚后台返回的content(内容)进行解析,拿到所需订单数据,前端拉起微信支付,参数传入刚刚解析数据 ===》 得到支付结果 success or fail !结束 12345678/** * 签名函数 Sign */function sign(timestamp, content) { var raw = timestamp + salt + content var hash = CryptoJS.SHA256(raw).toString() return CryptoJS.MD5(hash).toString()} 前端点击支付按钮: 123456789101112131415161718192021222324252627// 单独支付接口alonePay(arg) { const that = this; if( that.buttonClicked === false ) return; //防止重复多次拉起支付 that.buttonClicked = false; let mode = 1; //业务需求,我有五种不同模式支付 let appId = this.$parent.globalData.appId; let content; let sign; const timeStamp = new Date().Format(\"yyyy-MM-dd hh:mm:ss\").toString(); let code = wepy.getStorageSync('code'); wepy.login().then((res) => { //获取最新的code,可能这里没必要,具体和后端商量 if(res.code) { let code = res.code; log('code', code) wepy.setStorage({ key: \"code\", data: code }) } }).then( res => { content = `mode=${mode}&app_id=${appId}` sign = Sign.sign(timeStamp,content); }).then(res => { that.goCirclePay( that.circle_id, timeStamp, sign, content, mode ) })}, 支付函数: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879// 支付函数,我前端有五种支付情况(单独、自己发起拼团、拼别人团、ios、免费五种支付),所以单独抽出支付,每次调用支付函数goCirclePay( circle_id, timestamp, sign, content, mode) { const that = this; circleApi.goCirclePay({ data: { circle_id, timestamp, sign, content }, getToken: true }).then( res => { log('支付res:', res) let data = res.data const SignServer = data.sign const timeStampServer = data.timestamp let contentServer = data.content const SignLocal = Sign.sign(timeStampServer,contentServer); if( mode === 0 && data.status === \"success\") { that.nav('/pages/circleDetail?circle_id=' + that.circle_id) return; } if( SignLocal !== SignServer ) { log('签名不一致!') wx.showToast({ title: \"您已经支付过了\", duration: 1500, image: \"../images/common/icon_wxchat.png\", }) return } let contentArr = contentServer.split('&') const timeStamp = contentArr[0].split('=')[1]; const nonceStr = contentArr[1].split('=')[1]; let index = contentArr[2].indexOf(\"=\"); const package1 = contentArr[2].slice(index+1) const signType = contentArr[3].split('=')[1]; const paySign = contentArr[4].split('=')[1]; wepy.requestPayment({ timeStamp, nonceStr, package: package1, signType, paySign }).then(res => { return new Promise(resolve => { setTimeout(() => { resolve() }, 1000) }) }).then(res => { //支付后promise,这里有成功和fail两种,fail在catch捕获,这里直接开始写支付success后的业务代码 that.buttonClicked = true; let groupFormIdGet; circleApi.getGroupFormId({ ////获取getGroupFormId data: { circle_id: that.circle_id }, getToken: true }).then( res => { // let data = res.data that.group_form_id = data.group_form_id groupFormIdGet = data.group_form_id if( mode === 1) { that.nav(`/pages/paySuccess?circle_id=${that.circle_id}&shareLink=${that.shareLink}`) } else if( mode === 2) { that.nav(`/pages/paySuccess?circle_id=${that.circle_id}&group_form_id=${groupFormIdGet}`) } that.$apply() //脏值检查触发 }) }).catch(res => { log('支付失败', res) that.buttonClicked = true; }) })} 图片上传(七牛云) 更多图床网站请见我博客: https://www.cnblogs.com/fanghl/p/11419914.html 图片上传服务器采用七牛云服务,在app.wpy内小程序触发的时候,请求七牛云拿到token存为全局变量。 123456789101112//app.wpyonLaunch() { //other code *** // 七牛云,获取七牛云token wepy.request({ url: 'https://****************/qiniu_token/', header:{'content-type': 'application/json'}, }).then((res) => { this.globalData.qiniuToken = res.data.token });} 导入七牛云文件import qiniuyun from '@/utils/qiniuUploader' base.js代码: 1234567891011121314// 上传图片 base.jsconst uploadImg = (imageURL, uptokenURL) => { return new Promise((resolve, reject) => { qiniuyun.upload(imageURL, (res) => { resolve(res); }, (error) => { reject(error); }, { region: 'ECN', domain: '填入域名', uptoken: uptokenURL }); }); } 页面结构 12345678910111213141516<!-- 上传生活照 --> <view class=\"baseInfoTip\" style=\"border: 0\">上传生活照 <view class=\"imgUploadText\">(最多9张)</view> <view class=\"leftOriginLine\"></view> </view> <view class=\"uploadImgBox\"> <repeat for=\"{{images}}\" index=\"index\" item=\"item\" key=\"index\"> <view class=\"itemBox\"> <image class=\"imgItem\" src=\"{{item}}\" mode=\"aspectFill\"></image> <image class=\"imgItemCancel\" id=\"{{index}}\" src=\"../images/common/icon_cardImg_cancel.png\" @tap.stop=\"cancelUploadImg\"></image> </view> </repeat> <view class=\"itemBox\" @tap=\"addImg\" wx:if=\"{{!addImgCtrl}}\"> <image class=\"imgItem\" src=\"../images/common/icon_addImg.png\"></image> </view> </view> 上传图片业务: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455// 从相册选择照片上传addImg(){ const that = this; if( that.buttonClicked === false ) return; that.buttonClicked = false; wepy.chooseImage({ count:9 - that.images.length, sizeType: 'compressed', }).then(async(res1) => { that.buttonClicked = true; that.toast('上传图片中...','loading'); let filePath = res1.tempFilePaths; for(let i = 0;i < filePath.length;i++){ let imgSrc= res1.tempFilePaths[i]; let imgType = imgSrc.substring(imgSrc.length-3); let imgSize = res1.tempFiles[i].size; if(imgSize > 2000000 || imgType === 'gif'){ that.toast('该图片格式错误!请重新选择一张', 'none', 3000); continue } let res = await base.uploadImg(filePath[i], that.$parent.globalData.qiniuToken); that.images.push(res.imageURL); log('image长度:', that.images.length) log('image:', that.images) if( that.images.length >= 9) { that.addImgCtrl = true } if(that.images.length > 9){ that.images = that.images.slice(0,9) } if(that.images.length >0 && that.config.fImages){ that.config.progress = that.config.progress + parseFloat(that.config.getConfigs.lifepicweight*100); that.config.fImages = false } that.$apply(); // 上传用户头像列表 that.userInfo.photos = that.images if(i === filePath.length -1){ wepy.hideToast(); } } }).catch((res) => { if(res.errMsg === \"chooseImage:fail:system permission denied\"){ that.toast('请打开微信调用摄像头的权限', 'none', 3500) } })},// 取消图片上传cancelUploadImg(e) { if( this.images.length < 10 ) { this.addImgCtrl = false } let index = e.target.id this.images.splice(index, 1)}, 微信消息聊天布局微信聊天框整体布局特点有: 接收方和发送方消息分别位于屏幕的左右两侧、最新的消息一定是在屏幕最底部、进入消息dialog页面一定是显示的最新消息,即页面滑动在最底部。这三个基本特征构成了微信聊天页面的布局原则。先看效果图 (非最终效果):↓布局思路:flex反向布局 1234567891011121314151617<!-- 格式化代码 --><view class=\"msgBox\" id=\"msgBox\"> <repeat for=\"{{talkContent}}\" key=\"index\" item=\"item\"> <view class=\"msgItem {{item.send_user === configData.send_user ? 'msgItemReverse' : ''}}\"> <image class=\"adverseHeadimg\" src=\"{{item.send_user === configData.send_user ? configData.user_img : talkAimerInfo.headimg}}\" mode=\"aspectFill\"> </image> <text class=\"textBox {{item.send_user == configData.send_user ? 'textGreen' : ''}}\" selectable=\"true\"> {{item.message}} </text> </view> <view class=\"timeTip\" wx:if=\"{{item.send_user != configData.send_user}}\"> {{item.create_time}} </view> </repeat></view> 1234.msgBox{ display: flex; /*整体消息框flex布局,纵向取反布局*/ flex-direction: column-reverse;} 12345678.msgItem{ /*消息item样式*/ position: relative; display: flex; flex-direction: row;}.msgItemReverse{ /*对方的消息样式,flex行取反布局*/ flex-direction: row-reverse;} 保持页面始终滑动在最底部函数 12345678910pageScrollToBottom( msgLength ) { //在页面需要进行变化时调用 wx.createSelectorQuery().select('#contentBox').boundingClientRect(function(rect){ // 使页面滚动到底部 log('rect', rect) wx.pageScrollTo({ scrollTop: rect.bottom + msgLength*60, duration: 80 }) log('msgBox的下边界坐标: ', msgLength ) }).exec() } 自己发送的消息数据可以直接压入本地数组 talkContent 内,Unshift()进入,得到“负负得正”效果,即数据反,布局反即可得到从底部排列的布局。对方的消息从服务器拉下来的时候,放入 talkContent 内前 reverse() 一下即可 聊天页面input顶起页面相关聊天input点击后,默认为顶起页面,也可以关闭默认选择不顶起。但是不顶起页面其实是input脱离当前page,会出现键盘上方没有我们的输入框!因为键盘不顶起页面,故不会影响之前的布局,输入框一般都在页面最底部。解决: wx.onKeyboardHeightChange 监听键盘高度,严重不推荐input自身函数bindkeyboardheightchange,因为bindkeyboardheightchange 在手势上划隐藏键盘时Android是不会被触发的!!!思路: adjust-position = "false"设置不顶起页面,在手动把内容展示view 的高度减少键盘的高度!在键盘拉起时,内容高度减少键盘的高度,在键盘隐藏式,回复原高度。最后的效果和微信原生聊天一样!效果: 优化:在减少高度的同时,把内容页面滑到最底部,以展示最新消息! 12345678910<input class=\"inputContent\" type=\"text\" value=\"{{userInputContent}}\" bindinput = \"InputBlur\" adjust-position = \"{{false}}\" hold-keyboard = true confirm-hold = true confirm-type = 'done' @tap=\"onInpueChange\"> 123456789101112131415161718onInpueChange() { const that = this that.scrollBottom() wx.onKeyboardHeightChange(res => { that.log(res.height) that.scrollView.height = res.height *2 + 20 that.$apply() })}// 页面滚动到底部scrollBottom(){ const that = this; that.scrollTopValue++; setTimeout(function() { that.scrollTop = that.scrollTopValue; that.$apply() }, 300);} CSS注意点CSS持续补充中……word-break: break-all; //换行文字,英文溢出-webkit-overflow-scrolling: touch; //ios端启用硬件加速,解决ios端滑动粘手catchtouchmove='true' //模态框中添加,禁止页面滑动circleDynamic:last-of-type //特定类circleDynamic中最后一个元素:nth-of-type(1/odd/even) //选择特定元素下第几个元素 123 /* CSS 吸顶 */position: sticky; top: 0; async/await异步编程的终极解决方案,在小程序内拿取code或者login时会用到,await可理解为求值!async可理解为搭配await的语法,如果异步函数去掉await,返回的一般是 promise 对象,需要手动去reject 和 resolve 。 123456789101112if( !wepy.getStorageSync('token') ) { wepy.login().then(async (res) => { if(res.code) { let code = res.code; await that.login(code) wepy.setStorage({ key: \"code\", data: code }) } }); } else {} ios/android机型区别由于微信小程序的运行规范限制等,一些在 Android 上可以存在的业务需求并不能原封不动在 ios 端运行,否则小心 封号警告 (此处手动滑稽.jpg),所以一般采取两个系统的用户进入某一个页面,展现不同的内容。判断机型:在 app.wpy 入口文件中,onlaunch 生命周期内判断机型并保存到全局变量即可 12345678getSystemInfo() { const that = this; wx.getSystemInfo({ success(res) { that.globalData.userPlatform = res.platform; } })} 分包微信小程序官方限制小程序代码大小不得超过 2M ,在业务逻辑较多的情况下,查过2M后,我们可以采用分包加载。 1234567891011121314151617181920//app.wpyconfig = { pages: [ 'basePage1', 'basePage2', 'basePage3', ], subPackages: [ { root: 'dirName', //通常结构和 pages: [ 'subPage1', 'subPage2', ] } ]}//页面使用:this.nav(`/dirName/pages/subPage1`) canvas生成海报小程序分享至朋友圈的海报制作过程,海报内容为动态获取,内容根据每个用户生成不同的海报 如图,除了背景使用本地图片,其他的所有内容均为动态获取,且每次获取的不尽相同。 图片先从网络图片下载到本地才可以渲染,如果开发工具可以正常显示,而真机无法绘制,那么请先检查你的 downloadFile 域名!!! 绘制圆角-直角图片 图片保存模糊 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788//绘制文字 drawTxt(fontSize, color, content, x, y, bold=false) { this.ctx.save() this.ctx.setFontSize(fontSize) this.ctx.setFillStyle(color) this.ctx.fillText(content, x, y) if (bold) { this.ctx.fillText(content, x, y + 0.3) this.ctx.fillText(content, x + 0.3, y) } this.ctx.setTextBaseline('middle') this.ctx.restore() }//绘制圆角图片(默认)-矩形图片 getRectWithRadius(ctx, x, y, w, h, r, c, borderArgs = []) { // r > 0 则默认绘制圆角图片, r = 0 ,则绘制矩形 //绘制圆形 则 r = 1/2 w || 1/2h //绘制任意直角 则 borderArgs = [leftTop, rightTop, rightBottom, leftBottom]控制 let rate = this.rate let b = borderArgs ctx.beginPath() ctx.moveTo(x / rate, y / rate) b && b[0] ? '' : ctx.arc((x + r) / rate, (y + r) / rate, r / rate, Math.PI, 1.5 * Math.PI) b && b[1] ? ctx.lineTo((x + w) / rate, y / rate) : ctx.lineTo((x + w - r) / rate, y / rate) b && b[1] ? '' : ctx.arc((x + w - r) / rate, (y + r) / rate, r / rate, 1.5 * Math.PI, 0) b && b[2] ? ctx.lineTo((x + w) / rate, (y + h) / rate) : ctx.lineTo((x + w) / rate, (y + h - r) / rate) b && b[2] ? '' : ctx.arc((x + w - r) / rate, (y + h - r) / rate, r / rate, 0, 0.5 * Math.PI) b && b[3] ? ctx.lineTo((x) / rate, (y + h) / rate) : ctx.lineTo((x + r) / rate, (y + h) / rate) b && b[3] ? '' : ctx.arc((x + r) / rate, (y + h - r) / rate, r / rate, 0.5 * Math.PI, Math.PI) ctx.closePath() if (c) { ctx.fillStyle = c ctx.fill() } }// 保存海报 savePosterToLocal() { const that = this // 获取用户是否开启用户授权相册 wx.getSetting({ success(res) { // 如果没有则获取授权 if (!res.authSetting['scope.writePhotosAlbum']) { wx.authorize({ scope: 'scope.writePhotosAlbum', success() { that.saveCanvas() }, fail() { that.toast('右上角开启授权', 'none') wx.openSetting() } }) } else { that.saveCanvas() } } }) }//canvas保存至相册 saveCanvas() { // 1-把画布转化成临时文件 const that = this wx.canvasToTempFilePath({ x: 0, y: 0, width: 560, // 画布的宽 height: 996, // 画布的高 destWidth: 1080 * 750 / wx.getSystemInfoSync().windowWidth, destHeight: 1920 * 750 / wx.getSystemInfoSync().windowWidth, canvasId: 'poster', success(res) { wepy.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success(res2) { that.toast('保存至相册成功', 'none') }, fail() { that.toast('保存失败,稍后再试', 'none') } }) }, fail() { that.toast('保存失败,稍后再试', 'none') } }) } 多张图片下载网络图片下载成本地图片,且全部下载完全后才可以绘制canvas 1234567891011121314151617181920212223242526272829let promise1 = new Promise(function(resolve,reject){ wx.getImageInfo({ src: that.posterData.matchmaker.qrcord, success:function(res){ imgResource.qrcord = res.path resolve(res); }, fail:function(res){ reject(res); } }) })let promise2 = new Promise(function(resolve,reject){ wx.getImageInfo({ src: that.posterData.matchmaker.qrcord, success:function(res){ imgResource.qrcord = res.path resolve(res); }, fail:function(res){ reject(res); } })})Promise.all([promise1, promise2, promise3]).then(res => { //开始绘制 const ctx = wx.createCanvasContext('poster') that.ctx = ctx} 异常显示有时候Android手机会显示不完canvas,宽度异常,ios却没有该异常现象。其中一种可能就是设置canvas标签的宽高单位不一致,canvas内单位同一位 px ,把常用的 rpx 替换为 px <canvas canvas-id="poster" style="width: 280px; height: 498px;"></canvas> touchmove/onPageScroll动画效果如图,右下角的按钮,一般会做这样的效果,当用户滑动列表时,该按钮向下滑动并隐藏,当用户停止滑动且页面亦停止滑动(非用户手指脱离屏幕)时,该按钮再从页面底部滑出。 touchmove、 onPageScroll 思路1: 第一种想法是touchstart时, 触发下滑动画,touchend时触发上划动画 缺点: tap点击事件也会先触发start 和 end 事件,故点击屏幕也会触发动画,且屏幕抖动,pass 思路2: touchmove 时触发动画,touchend时上划动画, 缺点:虽然避免了点击就触发动画,但效果不佳,手指离开屏幕,页面还在滑动,动画已触发。 最终解: 只用 touchmove 来判断用户滑动列表,再用 onPageScroll 配合 超时器 来处理页面停止滑动。 代码: 12345678910111213141516171819202122//template<view calss=\"{{isSlide ? 'down-slide-hide' : 'up-slide-hide'}}\"></view>// datadata: { isSlide: false, timer: null,}//methodstouchmove(e) { this.isSlide = true this.$apply()},onPageScrool(e) { const that = this clearTimeout(that.timer) that.timer = setTimeout(() => { that.isSlide = false that.$apply() }, 400)} 页面一直处于滑动时,超时器不会生效,只有在页面停止滑动后,超时器才生效,程序执行 录音 录音没有难度,上传语音采用七牛云服务,拿到临时路径经过七牛云拿到网络路径,在传给自己服务器。这里实现了一个圆环进度条(canvas),在录音时配合录音时长展示 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748//绘制进度条圆环startRocord() { ... this.countInterval()}drawCircle(step) { !this.ctx && this.ctx = wx.createCanvasContext('progressBar') let ctx = this.ctx // ctx.clearRect(0, 0, 120, 120) //清除画布,(多次重复绘制需要。但绘制步伐过快会导致闪屏) // ctx.draw() ctx.setLineWidth(10) ctx.setStrokeStyle('#FF5757') ctx.setLineCap('round') ctx.beginPath() ctx.arc(60, 60, 55, -Math.PI/2, step*Math.PI - Math.PI/2, false) ctx.stroke() ctx.draw()}//进度绘制定时器countInterval() { this.countTimer = setInterval(() => { if (this.count <= 600) { //绘制步伐,这里0.1秒绘制,很丝滑 this.drawCircle(this.count / (600/2)) this.count++ this.$apply() } else { clearInterval(this.countTimer) this.count = 0 } }, 1000 * 0.1);}//录音实例 initRecord() { !this.RM && (this.RM = wx.getRecorderManager()) let RM = this.RM RM.onStop(async res => { //监听录音结束, res会返回录音信息(临时文件路径、时长、文件大小) //七牛云上传临时路径 let result = await base.uploadImg(res.tempFilePath, wepy.$instance.globalData.qiniuToken) ... }) RM.start({ duration: 60*1000, format: 'mp3', })} 播音 createInnerAudioContext 用来播放各个用户语音,可以随时切换不同用户的语音播放,安卓规规矩矩没问题 ios播放异常 ios用户切换语音时会播放第一个音频,但随后的语音却不会播放,实例已经销毁,但貌似对ios无效。解决:单例模式创建实例,在销毁实例后,在将变量手动清空,可以解决ios新建播音实例无效问题。 12345678910111213141516171819202122232425262728293031323334353637383940//dataIAC: null,isAudition: false,/*** src: 音频 cubicle:播放开关 index: 用户索引(不同音频) currentUser:当前需要播放的用户 lastUser: 上一个播放的用户 isAuditionStatus: 播放状态(未播放,正在播放)* handleSound 监听新老用户播音,若IAC正在播音,此时继续点击同一用户,则暂停当前音频,若IAC未在播音,则播音当前用户。若点击不同用户,则暂停当前正在播音的用户,播放新用户录音。*/async handleSound(src, cubicle, index) { if (this.currentUser !== index) { this.lastUser = this.currentUser this.currentUser = index this.changeUser = true } if (this.changeUser) { this.IAC && this.IAC.destroy() this.IAC = null //ios这点不仅需要destroy实例,还要手动清空变量 this.lastUser != -1 && (this.recommUserList[this.lastUser].isAuditionStatus = 0) !this.IAC && (this.IAC = wx.createInnerAudioContext()) let IAC = this.IAC IAC.src = src IAC.play() IAC.onPlay(() => { this.recommUserList[this.currentUser].isAuditionStatus = wepy.$instance.globalData.isPlaying = 1 this.$apply() }) IAC.onStop(() => { this.recommUserList[this.currentUser].isAuditionStatus = wepy.$instance.globalData.isPlaying = 0 this.$apply() }) IAC.onEnded(() => { this.recommUserList[this.currentUser].isAuditionStatus = wepy.$instance.globalData.isPlaying = 0 this.$apply() }) this.changeUser = false this.$apply() } else { cubicle ? this.IAC.play() : this.IAC.stop() }} 不管是录音还是播音,在页面卸载(onUnload)的时候清除掉实例或者初始化,有canvas也要清除画布 华为-textarea-层级异常华为部分机型,对小程序的textarea标签支出并不友好,其textarea的内容以及 placeholder 内容恨天高,无法通过程序控制,甚至小程序官方说 canvas 的层级是最高的,但也没高过textarea! 问题: 盖在textarea上面的弹框会被textarea的内容穿掉盖不住,并且点击事件直接穿透 解决:在拉起盖在textarea上面的弹框(或组件)时,用 view 标签重写模仿 textarea 样式,并把 textarea 关闭掉。 持续更新…….","categories":[{"name":"工作总结","slug":"工作总结","permalink":"http://fanghl.top/categories/工作总结/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"firstBlog","slug":"firstBlog","date":"2019-08-19T07:02:47.000Z","updated":"2023-11-03T03:29:00.558Z","comments":true,"path":"2019/08/19/firstBlog/","link":"","permalink":"http://fanghl.top/2019/08/19/firstBlog/","excerpt":"","text":"#既昨天搞崩GitHub博客,再次坚强的尝试 既昨天搞崩GitHub博客,再次坚强的尝试既昨天搞崩GitHub博客,再次坚强的尝试####既昨天搞崩GitHub博客,再次坚强的尝试 #####既昨天搞崩GitHub博客,再次坚强的尝试 1alert('hello world')","categories":[{"name":"test","slug":"test","permalink":"http://fanghl.top/categories/test/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"Hello World","slug":"hello-world","date":"2019-08-15T12:43:17.000Z","updated":"2023-11-03T03:29:00.559Z","comments":true,"path":"2019/08/15/hello-world/","link":"","permalink":"http://fanghl.top/2019/08/15/hello-world/","excerpt":"","text":"Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub. Quick StartCreate a new post1$ hexo new \"My New Post\" More info: Writing Run server1$ hexo server More info: Server Generate static files1$ hexo generate More info: Generating Deploy to remote sites1$ hexo deploy More info: Deployment","categories":[],"tags":[]}]} \ No newline at end of file +{"meta":{"title":"Fanghongliang's blog","subtitle":"Science && Truth","description":"站在巨人的肩膀上","author":"fanghl","url":"http://fanghl.top","root":"/"},"pages":[{"title":"404","date":"2019-09-19T12:36:20.000Z","updated":"2023-11-03T03:29:00.555Z","comments":true,"path":"404/index.html","permalink":"http://fanghl.top/404/index.html","excerpt":"","text":"404"},{"title":"Abort","date":"2019-09-19T11:40:28.000Z","updated":"2023-11-03T03:29:00.562Z","comments":true,"path":"about/index.html","permalink":"http://fanghl.top/about/index.html","excerpt":"","text":"我为何而生 <伯特兰·罗素> 有三种情感,单纯而强烈,支配着我的一生:对爱情的渴望,对知识的追求,以及对人类苦难不可遏制的同情。这些感情如阵阵巨风,挟卷着我在漂泊不定的路途中东飘西荡,飞越苦闷的汪洋大海,直抵绝望的边缘。 我之所以追寻爱情,首先,爱情使人心醉神迷,如此美妙的感觉,以致使我时常为了体验几小时爱的喜悦,而宁愿献出生命中其它一切;其次,爱情可以解除孤独,身历那种可怕孤寂的人的战栗意识,会穿过世界的边缘,直望入冰冷死寂的无底深渊;最后,置身于爱的结合,我在一个神秘缩影中看到了圣贤与诗人们所预想的天堂。这正是我所追寻的,尽管它对于人类的生活或许太过美好,却是我的最终发现。 我也以同样的热情追求知识。我渴望理解人类的心灵,渴望知道星辰为何闪耀,我还试图领略毕达哥拉斯关于哪些数字在变迁之上保持着永恒的智慧。在这一方面,我取得了一点成果,但并不算多。 爱情与知识,尽其可能,引领着我通往天堂;然而怜悯总是把我带回现实。那些痛苦的呼唤在我内心深处回响。饥饿中的孩子,被压迫和折磨的人们,给子女造成重担的无助老人,以及孤独、贫穷和痛苦的整个世界,都是对人类理想生活的嘲讽。我渴望能减少这些不幸,但无能为力,这也是我的痛苦。 这就是我的一生。我发现人生是值得的;而且如果能够再有一次这样的机会,我会欣然接受。"},{"title":"标签","date":"2019-09-19T11:49:32.000Z","updated":"2023-11-03T03:29:00.562Z","comments":true,"path":"tags/index.html","permalink":"http://fanghl.top/tags/index.html","excerpt":"","text":""},{"title":"分类","date":"2019-08-19T07:11:42.000Z","updated":"2023-11-03T03:29:00.562Z","comments":false,"path":"categories/index.html","permalink":"http://fanghl.top/categories/index.html","excerpt":"","text":""}],"posts":[{"title":"electron","slug":"electron","date":"2023-02-13T06:13:23.000Z","updated":"2023-11-03T03:29:00.557Z","comments":true,"path":"2023/02/13/electron/","link":"","permalink":"http://fanghl.top/2023/02/13/electron/","excerpt":"","text":"12 Electron本篇文章将结合官方文档以及实际线上每天万人使用的一款开播工具源码片段综合阐述下Electron 的开发经验和技巧 Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。 嵌入 Chromium 和 Node.js 到 二进制的 Electron 允许您保持一个 JavaScript 代码代码库并创建 在Windows上运行的跨平台应用 macOS和Linux——不需要本地开发经验。 开发环境 您需要安装 node.js 建议安装最新的LTS版本 因为 Electron 将 Node.js 嵌入到其二进制文件中,你应用运行时的 Node.js 版本与你系统中运行的 Node.js 版本无关。 Electron 应用程序遵循与其他 Node.js 项目相同的结构。 首先创建一个文件夹并初始化 npm 包。 12mkdir my-electron-app && cd my-electron-appnpm init init初始化命令会提示您在项目初始化配置中设置一些值 为本教程的目的,有几条规则需要遵循:. entry point 应为 main.js.. author 与 description 可为任意值,但对于应用打包是必填项。这里官网只是作为一个demo来规划文件结果,实际开发环境中,Electron应用很可能是在原有业务的网页版项目中 ,比如一个网页版直播页面,使用React + next 实现。现在要实现开播工具桌面端饮用,一般直接选择在原有的项目文件中直接新建文件夹开始,那么入口文件很可能不是根目录下 main.js,这点我们通过 package.json 的配置 main: ‘xxx’可以解决. 至于说 Electron 是 “Web网页” 套了桌面端的壳,那为什么我们还要使用 Electron呢? 因为Web应用无法拿到操作系统的权限,这对于我们解决一些问题十分关键。 12345// package.json{ \"description\": \"www.2339.com\", \"main\": \"app/main.js\", // 这里配置了项目的入口文件} 按装 Electron包 并创建执行脚本 1234567891011121314yarn add --dev electron// 配置 package.json 4种环境,一般学习只需要配置一种即可,打包命令后续也在这里配置{ \"scripts\": { \"electron:dev\": \"electron .\", \"electron:staging\": \"cross-env NODE_ENV=development ELECTRON=1 MODE=staging nextron -p 9401 .\", \"electron:grey\": \"electron .\", \"electron:production\": \"electron .\", }}// 命令启动 dev 环境yarn electron:dev 运行主进程任何 Electron 应用程序的入口都是 main 文件。 这个文件控制了主进程,它运行在一个完整的Node.js环境中,负责控制您应用的生命周期,显示原生界面,执行特殊操作并管理渲染器进程(稍后详细介绍)。执行期间,Electron 将依据应用中 package.json配置下main字段中配置的值查找此文件,您应该已在应用脚手架步骤中配置。 创建页面在可以为我们的应用创建窗口前,我们需要先创建加载进该窗口的内容。 在Electron中,各个窗口显示的内容可以是本地HTML文件,也可以是一个远程url。 在窗口中打开您的页面现在您有了一个页面,将它加载进应用窗口中。 要做到这一点,你需要 两个Electron模块: . app 模块,它控制应用程序的事件生命周期。. BrowserWindow 模块,它创建和管理应用程序 窗口。因为主进程运行着 Node.js,您可以在 main.js 文件头部将它们导入作为 CommonJS 模块: 专有名词 窗口 预加载脚本 渲染器 主进程每个 Electron 应用都有一个单一的主进程,作为应用程序的入口点。 主进程在 Node.js 环境中运行,这意味着它具有 require 模块和使用所有 Node.js API 的能力主进程的主要目的是使用 BrowserWindow 模块创建和管理应用程序窗口。BrowserWindow 类的每个实例创建一个应用程序窗口,且在单独的渲染器进程中加载一个网页。 您可从主进程用 window 的 webContent 对象与网页内容进行交互。 应用程序生命周期主进程还能通过 Electron 的 app 模块来控制您应用程序的生命周期。 该模块提供了一整套的事件和方法,可以让您用来添加自定义的应用程序行为 (例如:以编程方式退出您的应用程序、修改应用程序坞,或显示一个关于面板) app 模块 BrowserWindow 模块BrowserWindow 类的每个实例创建一个应用程序窗口,且在单独的渲染器进程中加载一个网页。 您可从主进程用 window 的 webContent 对象与网页内容进行交互。由于 BrowserWindow 模块是一个 EventEmitter, 所以您也可以为各种用户事件 ( 例如,最小化 或 最大化您的窗口 ) 添加处理程序。当一个 BrowserWindow 实例被销毁时,与其相应的渲染器进程也会被终止。 CommonJS 模块 Preload 脚本预加载(preload)脚本包含了那些执行于渲染器进程中,且先于网页内容开始加载的代码 。 这些脚本虽运行于渲染器的环境中,却因能访问 Node.js API 而拥有了更多的权限。预加载脚本可以在 BrowserWindow 构造方法中的 webPreferences 选项里被附加到主进程。因为预加载脚本与浏览器共享同一个全局 Window 接口,并且可以访问 Node.js API,所以它通过在全局 window 中暴露任意 API 来增强渲染器,以便你的网页内容使用。 下面是渲染进程中选取用户操作系统文件中的一张图片即打开系统文件夹,选取一张图片 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191// 在 preload 脚本中增加全局消息处理机制 // preload.jswindow.electron = { message: { send: (payload) => { return ipcRenderer.send('message', payload); }, on: (handler) => { return ipcRenderer.on('message', handler); }, off: (handler) => { return ipcRenderer.off('message', handler); }, },};// lib/qtClient 资源文件夹// 封装一个全局公用方法const clientEvent = new EventEmitter();const inti = (cb) => { // PC项目和electron项目在同一工程中 if (!browser.electronClient) { console.info( '======================\\r\\n非 electron 环境\\r\\n======================', ); return; } // 监听主进程事件 window.electron.message.on((event, msg) => { if (msg.id) { clientEvent.emit(msg.id, msg); } else { clientEvent.emit(msg.action, msg); } clientEvent.emit('all', msg); }); cb && cb();}// call 消息封装const call = ({ action, data = {}, onSuccess = (msg) => {}, onFail = (msg) => {},}) => { // 同上,区分electron环境 if (!browser.electronClient) { return; } if (!action) { console.error('action is required'); return; } const options = { id: utils.getRandomString(), action, data, }; clientEvent.once(options.id, (msg) => { if (msg.code === 1) { onSuccess(msg); } else { onFail(msg); } }); if (typeof data === 'function') { options.data = {}; } if (action !== 'getSystemInfo') { log(`call【${action}】 options:`, options); } window.electron.message.send(options);};const qtClient = { init, call}export default call// 封装具体的业务需求-选取图片// app-client.jsconst selectImage = (onSuccess) => { qtClient.call({ action: 'selectImage', onSuccess, });};// 具体业务环境中使用// eg 主播开播合流时往视频区域添加一张图片const handleAddImage = () => { appClient.selectImage((msg) => { const { filePath, fileName, ext, width, height } = msg.data; // 返回有文件的具体信息,文件名、路径等 // 处理你的业务 // 比如: 把文件信息在通过则 zego 等三方传递出去,最终在直播流中成功添加一张图片或Gif })}// -----------------分割线--------------// 以上代码是渲染进程处理的事// 渲染进程向主进程发送了想要打开用户文件夹获取图片的消息, ipcRenderer// 以下将是主进程中监听渲染进程的消息,并作出处理 ipcMain// main.tsimport message from './message';message.init()// message.ts// 处理所有的主进程消息 // 打开文件夹,并选取图片文件 const selectImage = (event, message) => { dialog .showOpenDialog({ title: '选择图片', properties: ['openFile'], filters: [ { name: 'Images', extensions: ['jpg', 'png', 'jpeg', 'gif', 'bmp'] }, ], }) .then(async ({ canceled, filePaths, bookmarks }) => { if (filePaths.length) { const filePath = filePaths[0]; const { width, height } = imageSize(filePath); // const {fileTypeFromFile} = await import('file-type') // const fileType = await fileTypeFromFile(filePath); const [fileName, fileExt] = filePath .replace(/\\\\/gi, '/') .split('/') .pop() .split('.'); // const ext = fileType?.ext || fileExt; const ext = fileExt; responseSuccess(event, message, { data: { filePath: filePaths[0], fileName, width, height, ext: ext.toLowerCase(), }, }); } });};const actions = { selectImage,}const init = () => { ipcMain.on('message', (event, message) => { // event.sender.send('message', message); if (message.action !== 'getSystemInfo') { log.info('[ipcMain message]', message); } if (actions[message.action]) { actions[message.action](event, message); } else { log.warn('action not found', message.action); } });}; 至此,一个完善的渲染进程-主进程通信框架就搭建完毕,后续其他的通信需求直接扩展上即可。 虽然预加载脚本与其所附着的渲染器在共享着一个全局 window 对象,但您并不能从中直接附加任何变动到 window 之上,因为 contextIsolation 是默认的。语境隔离(Context Isolation)意味着预加载脚本与渲染器的主要运行环境是隔离开来的,以避免泄漏任何具特权的 API 到您的网页内容代码中。 取而代之,我们將使用 contextBridge 模块来安全地实现交互 darwinDarwin 是MacOSX 操作环境, 即苹果电脑的操作系统 打包工具electron-builder CLI命令行接口 devDependencies开发环境需要的额外依赖,您的应用需要运行 Electron API,因此这听上去可能有点反直觉。 实际上,打包后的应用本身会包含 Electron 的二进制文件,因此不需要将 Electron 作为生产环境依赖。 原生 API为了使 Electron 的功能不仅仅限于对网页内容的封装,主进程也添加了自定义的 API 来与用户的作业系统进行交互。 Electron 有着多种控制原生桌面功能的模块,例如菜单、对话框以及托盘图标。","categories":[{"name":"code","slug":"code","permalink":"http://fanghl.top/categories/code/"}],"tags":[{"name":"electron","slug":"electron","permalink":"http://fanghl.top/tags/electron/"}]},{"title":"直播相关Live【hybrid】","slug":"page","date":"2022-01-01T08:46:39.000Z","updated":"2023-11-03T03:29:00.560Z","comments":true,"path":"2022/01/01/page/","link":"","permalink":"http://fanghl.top/2022/01/01/page/","excerpt":"","text":"序言直播间一些复杂功能的实现和总结,包括但不限于 IM 及时通讯消息、融云、融信 IM 私聊消息、推拉流、WebSocket 推送、Hybrid 与 H5 的桥接通信、mobx-state-tree | React 重构 远古 JQ 代码、 PC 直播间深度链接至移动端、用户极验证(验证非脚本或机器人)、进场特效、用户头像挂坠、炫彩昵称、用户财富、等级、身份标签组件化、拖拽等组件应用、 动画SVGASvga 是常见的一种直播间动画播放格式,其特性这里不做过多解释,今天总结一下在 H5 端 和 PC 端 播放 Svga 动画的全过程。封装公用方法以及优化、避坑。 动画播放在用户送礼、礼物预览等场景下使用频繁,封装一个公用方法在项目 lib 十分必要。这里使用 【svgaplayerweb】 封装方法: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172export function retryPromise(promiseFn, retriesLeft = 5, interval = 1500) { return new Promise((resolve, reject) => { promiseFn() .then(resolve) .catch((error) => { setTimeout(() => { if (retriesLeft === 1) { reject(error); return; } retryPromise(promiseFn, retriesLeft - 1, interval).then( resolve, reject ); }, interval); }); });}/** * 加载svga动画 * @param {String} id dom-id * @param {src} src svga文件路径 * @param {Number} loops 循环次数,默认为0无限循环 * @param {Function} onFinished 完成回调 * @param {Boolean} isPlayNow 是否立即播放,默认true */const svgaModules = {};export function svgaPlayer(option) { const { id, src, loops = 0, onFinished, clearsAfterStop = true, fillMode = \"Backward\", isPlayNow = true, } = option; if (!id) { return; } const play = () => { const { svga } = svgaModules; const player = new svga.Player(`#${id}`); const parser = new svga.Parser(`#${id}`); parser.load(src, (videoItem) => { player.loops = loops; player.clearsAfterStop = clearsAfterStop; player.fillMode = fillMode; player.setVideoItem(videoItem); player.onFinished(() => { if (onFinished) { onFinished(); } }); isPlayNow && player.startAnimation(); }); }; if (svgaModules.svga) { play(); return; } retryPromise(() => import(\"svgaplayerweb\")).then((svga) => { svgaModules.svga = svga; play(); });} 以上封装可以满足常见的动画播放场景。 Tab 点击动画场景现在的 H5 交互越来越体现用户至上,在 tab 的点击上,设计师更想要用户点击 某个 tab 播放该 tab 的动画状态,常见的有点击 hybrid 页面根 tab,该 tab 会抖动动画或者无衔接播放一个小动画,这个设计师给的 svga 动画,实现只需要上述公用方法 【isPlayNow】 参数,默认只让动画加载而不立即播放,展示第一帧,效果和静态 tab 一样,待点击时在 修改 【isPlayNow】 为 true,播放动画即可实现上述效果。 VAPvap [video-animation-player] 是腾讯企鹅电竞推出的开源 mp4 播放库,具体移步 GitHub 仓库,该库适配三端,这里只总结下 Web 端 封装公用方法: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475// vap-player.jsimport Vap from \"video-animation-player\";import { request } from \"lib\";let vap = null;const play = (opt) => { let { container, src, configUrl, imgUser, textUser, width = \"100%\", height = \"100%\", fontStyle, loop = false, fps = 24, mute = true, precache = false, accurate = false, onStop = () => {}, } = opt; const fn = (config) => { if (vap) { vap.destroy(); } vap = new Vap( Object.assign( {}, { container, src, config, width, height, fontStyle, imgUser, textUser, loop, fps: config.info.fps || fps, precache, mute, accurate, } ) ) .on(\"error\", (e) => { vap.destroy(); onStop(); }) .on(\"ended\", () => { vap.destroy(); onStop(); }); }; request.api.get(configUrl).then((res) => { fn(res.data); });};const destroy = () => { if (vap) { vap.destroy(); }};export default { play, destroy };// 引用import VapPlayer from \"lib\";VapPlayer.play(options); 使用 vap 在部分机型导致动画效果模糊锯齿的解决办法: Dom 容器的宽高扩大 400%,再缩小 ‘transform: scale(0.25);’ 缩小四倍。 Chrome不支持obs虚拟摄像头解决方法直播平台一般都会有自己对应的开播工具,比如YY、虎牙助手、OBS等。采用OBS采集视频流,会使用虚拟摄像头,Chrome有时候会不支持虚拟摄像头,解决办法为: 浏览器默认未允许虚拟摄像头的使用,在出Chrome的设置中打开对应的隐私配置即可,具体步骤可以Google。 IM 及时通讯消息待整理 推拉流dsBridge 桥接通信socket 监听发布CSS炫彩昵称五颜六色的炫彩昵称CSS实现 1234<!-- html --><WrapColorName> <div className=\"name\">用户昵称哇哈哈</div></WrapColorName> 1234567891011121314151617181920212223242526272829/* 这里使用 styled-components 写法,可以改成其他CSS框架写法,语法不变 */const maskedAnimation = keyframes` from { background-position: 0 0; } to { background-position: -200% 0; }`;const WrapColorName = styled.div` background-image: -webkit-linear-gradient( left, #f70000, #ff891c 14.3%, #ffe719 28.6%, #33e97c 43%, #1dd5ff 57%, #ec80ff 71.4%, #ff43dc 86%, #ff43cb 100% ); -webkit-text-fill-color: transparent; -webkit-background-clip: text; -webkit-background-size: 200% 100%; -webkit-animation: ${maskedAnimation} 3s infinite linear;` 上面的demo 可以直接在本地跑,主要利用 webkit-background-size 的位置偏移和 webkit-animation 背景光束实现炫彩动画文案,上述案例可以直接在 演示地址 查看","categories":[{"name":"默认","slug":"默认","permalink":"http://fanghl.top/categories/默认/"}],"tags":[{"name":"默认","slug":"默认","permalink":"http://fanghl.top/tags/默认/"}]},{"title":"MST","slug":"MST","date":"2021-07-13T06:42:57.000Z","updated":"2023-11-03T03:29:00.556Z","comments":true,"path":"2021/07/13/MST/","link":"","permalink":"http://fanghl.top/2021/07/13/MST/","excerpt":"","text":"序言React的组件化开发确实对于大型项目非常有用,大型项目的数据状态管理写乱了也是头疼和难以维护的,这里记录一下项目中使用的状态管理MST(mobx-state-tree) 预备知识 MobX:这是MST的核心,MST中存储的响应式“状态”都是MobX的Observable React:使用React来测试MST的功能非常简单 TypeScript:后文中会使用TS来编写示例代码,TS强大的智能提示和类型检查,有助于快速掌握MST的API 安装MST依赖MobX。项目中执行yarn add mobx mobx-state-tree即可完成安装。MobX有两个版本,新版本需要浏览器Proxy支持,一些老旧的浏览器并不支持,需要兼容老浏览器的请安装mobx@4:yarn add mobx@4 mobx-state-tree。 结构 使用MST来维护状态,首先需要让MST知道,这个状态的结构是什么样的。MST内建了一个类型机制。通过类型的组合就可以定义出整个状态的形状。并且,在开发环境下,MST可以通过这个定义好的形状,来判断状态的值和形状与其对应的类型是否匹配,确保状态的类型与预期一致,这有助于在开发时及时发现数据类型的问题: MST提供的一个重要对象就是types,在这个对象中,包含了基础的元类型(primitives types),如string、boolean、number,还包含了一些复杂类型的工厂方法和工具方法,常用的有model、array、map、optional等。model是一个types中最重要的一个type,使用types.model方法得到的就是Model,在Model中,可以包含多个type或者其他Model。一个Model可以看作是一个节点(Node),节点之间相互组合,就构造出了整棵状态树(State Tree)。MST可用的类型和类型方法非常多,这里不一一列举,可以在这里查看完整的列表。完成Model的定义后,可以使用Model.create方法获得Model的实例。Model.create可以传入两个参数,第一个是Model的初始状态值,第二个参数是可选参数,表示需要给Model及子Model的env对象(环境配置对象),env用于实现简单的依赖注入功能。 PropsViewsviews是Model中一系列衍生数据或获取衍生数据的方法的集合,类似Vue组件的computed计算属性。 需要注意的是,定义views时有两种选择,使用getter或者不使用。使用getter时,衍生数据的值会被缓存直到依赖的数据发送变化。而不使用时,需要通过方法调用的方式获取衍生数据,无法对计算结果进行缓存。尽可能使用getter,有助于提升应用的性能。 Actions从名字上可以看出来,上面四位都是生命周期方法,可以使用他们在Model的各个生命周期执行一些操作: 除了通常意义上用来更新状态的actions外,在model.actions方法中,还可以设置一些特殊的actions: afterCreateafterAttachbeforeDetachbeforeDestroy 从名字上可以看出来,上面四位都是生命周期方法,可以使用他们在Model的各个生命周期执行一些操作: 1234567const Model = types .model(...) .actions(self => ({ afterCreate () { // 执行一些初始化操作 } })); 异步Action、Flow异步更新状态是非常常见的需求,MST从底层支持异步action。 1234567891011121314151617181920const model = types .model(...) .actions(self => ({ // async/await async getData () { try { const data = await api.getData(); ... } catch (err) { ... } ... }, // promise updateData () { return api.updateData() .then(...) .catch(...); } })); 若使用Promise、async/await来编写异步Action,在异步操作之后更新状态时,代码执行的上下文会脱离action,导致状态在action之外被更新而报错。这里有两种解决办法: 将更新状态的操作单独封装成action 编写一个runInAction的action在异步操作中使用 123456789101112131415161718192021222324252627282930313233343536373839// 方法1const Model = types .model(...) .actions(self => ({ setLoading (loading: boolean) { self.loading = loading; }, setData (data: any) { self.data = data; }, async getData () { ... self.setLoading(true); // 这里因为在异步操作之前,直接赋值self.loading = true也ok const data = await api.getData(); self.setData(data); self.setLoading(false); ... } })); // 方法2const Model = types .model(...) .actions(self => ({ runInAction (fn: () => any) { fn(); }, async getData () { ... self.runInAction(() => self.loading = true); const data = await api.getData(); self.runInAction(() => { self.data = data; self.loading = false; }); ... } })); 方法1需要额外封装N个action,比较麻烦。方法2封装一次就可以多次使用。但是在某些情况下,两种方法都不够完美:一个异步action被分割成了N个action调用,无法使用MST的插件机制实现整个异步action的原子操作、撤销/重做等高级功能。为了解决这个问题,MST提供了flow方法来创建异步action: 1234567891011121314151617181920import { types, flow } from 'mobx-state-tree';const model = types .model(...) .actions(self => { const getData = flow(function * () { self.loading = true; try { const data = yield api.getData(); self.data = data; } catch (err) { ... } self.loading = false; }); return { getData }; }) 使用flow方法需要传入一个generator function,在这个生成器方法中,使用yield关键字可以resolve异步操作。并且,在方法中可以直接给状态赋值,写起来更简单自然。 快照 Snapshotsnapshot即“快照”,表示某一时刻,Model的状态序列化之后的值。这个值是标准的JS对象。 使用getSnapshot方法获取快照: 使用applySnapshot方法可以更新Model的状态: Volatile State在MST中,props对应的状态都是可持久化的,也就是可以序列化为标准的JSON数据。并且,props对应的状态必须与props的类型相匹配。如果需要在Model中存储无需持久化,并且数据结构或类型无法预知的动态数据,可以设置为Volatile State。 Volatile State的值也是Observable,但是只会响应引用的变化,是一个非Deep Observable。 选择正确的types类型types.string定义一个字符串类型字段。 types.number定义一个数值类型字段。 types.boolean定义一个布尔类型字段。 types.integer定义一个整数类型字段。注意,即使是TypeScript中也没有“整数”这个类型,在编码时,传入一个带小数的值TypeScript也无法发现其中的类型错误。如无必要,请使用types.number。 types.Date定义一个日期类型字段。这个类型存储的值是标准的Date对象。在设置值时,可以选择传入数值类型的时间戳或者Date对象。 types.null定义一个值为null的类型字段。 types.undefined定义一个值为undefined的类型字段。复合类型 types.model定义一个对象类型的字段。 types.array定义一个数组类型的字段。types.array(types.string);types.array(types.model); types.map定义一个map类型的字段。该map的key都为字符串类型,map的值都为指定类型。map可用set、 get进行取赋值 1234567891011121314151617181920export enum PopupType { Rule = '规则', Award = '奖励', Buy = '购买', Tip = '提示',}popup: types.map(types.boolean);const showPopup = (type: PopupType) => { self.popup.set(type: true)}const hidePopup = (type: PopupType) => { self.popup.set(type, false);};// 具体使用{popup.get(PopupType.Rule) && <PopupRule />} types.optional可选类型,根据传入的参数,定义一个带有默认值的可选类型。types.optional是一个方法,方法有两个参数,第一个参数是数据的真实类型,第二个参数是数据的默认值。types.optional(types.number, 1);上面的代码定义了一个默认值为1的数值类型。注意,types.array或者types.map定义的类型自带默认值(array为[],map为{}),也就是说,下面两种定义的结果是一样的: 1234567// 使用types.optionaltypes.optional(types.array(types.number), []);types.optional(types.map(types.number), {});// 不使用types.optionaltypes.array(types.number);types.map(types.number); 如果要设置的默认值与types.array或types.map自带的默认值相同,那么就不需要使用types.optional。 types.custom如果想控制类型更底层的如序列化和反序列化、类型校验等细节,或者根据一个class或interface来定义类型,可以使用types.custom定义自定义类型。 1234567891011121314151617181920class Decimal { ...}const DecimalPrimitive = types.custom<string, Decimal>({ name: \"Decimal\", fromSnapshot(value: string) { return new Decimal(value) }, toSnapshot(value: Decimal) { return value.toString() }, isTargetType(value: string | Decimal): boolean { return value instanceof Decimal }, getValidationMessage(value: string): string { if (/^-?\\d+\\.\\d+$/.test(value)) return \"\" // OK return `'${value}' doesn't look like a valid decimal number` }}); types.union实际开发中也许会遇到这样的情况:一个值的类型可能是字符串,也可能是数值。那我们就可以使用types.union定义联合类型: types.union(types.number, types.string);联合类型可以有任意个联合的类型。 types.literal字面值类型可以限制存储的内容与给定的值严格相等。比如使用types.literal(‘male’)定义的状态值只能为’male’。实际上,上面提到过的types.null以及types.undefined就是字面值类型: const NullType = types.literal(null);const UndefinedType = types.literal(undefined);搭配联合类型,可以这样定义一个性别类型:const GenderType = types.union(types.literal(‘male’), types.literal(‘female’)); types.enumeration枚举类型可以看作是联合类型以及字面值类型的一层封装,比如上面的性别可以使用枚举类型来定义:const GenderType = types.enumeration(‘Gender’, [‘male’, ‘female’]);方法的第一个参数是可选的,表示枚举类型的名称。第二个参数传入的是字面值数组。在TypeScript环境下,可以这样搭配TypeScript枚举使用: 123456enum Gender { male, female}const GenderType = types.enumeration<Gender>('Gender', Object.values(Gender)); types.maybe定义一个可能为undefined的字段,并自带默认值undefined。 123types.maybe(type)// 等同于types.optional(types.union(type, types.literal(undefined)), undefined) types.maybeNull与types.maybe类似,将undefined替换成了null。 123types.maybe(type)// 等同于types.optional(types.union(type, types.literal(null)), null) types.frozenfrozen意为“冻结的”,types.frozen方法用来定义一个immutable类型,并且存放的值必须是可序列化的。 当数据的类型不确定时,在TypeScript中通常将值的类型设置为any,而在MST中,就需要使用types.frozen定义。 12awardPosition: types.frozen(),notices: types.array(types.frozen()), 在MST看来,使用types.frozen定义类型的状态值是不可变的,所以会出现这样的情况: 12model.anyData = {a: 1, b: 2}; // ok, reactivemodel.anyData.b = 3; // not reactive 也就是只有设置一个新的值给这个字段,相关的observer才会响应状态的更新。而修改这个字段内部的某个值,是不会被捕捉到的!! types.late滞后类型有时候会出现这样的需求,需要一个Model A,在A中,存在类型为A本身的字段。 如果这样写 1234const A = types .model('A', { a: types.maybe(A), // 使用mabe避免无限循环 }); 会提示Block-scoped variable ‘A’ used before its declaration,也就是在A定义完成之前就试图使用他,这样是不被允许的 这个时候就需要使用types.late: 1234const A = types .model('A', { a: types.maybe(types.late(() => A)) }); types.late需要传入一个方法,在方法中返回A,这样就可以避开上面报错的问题。 types.refinement提纯类型 types.refinement可以在其他类型的基础上,添加额外的类型校验规则。 比如需要定义一个email字段,类型为字符串但必须满足email的标准格式,就可以这样做: 12345const EmailType = types.refinement( 'Email', types.string, (snapshot) => /^[a-zA-Z_1-9]+@\\.[a-z]+/.test(snapshot), // 校验是否符合email格式); 实操MST (mobx-state-tree),顾名思义是React用于管理状态的状态树结构,根据每个组件构建单独的状态树结构,一般建议状态树结构和接口或者推送保持一致的数据结构,便于更新维护。在请求接口和推送时,只需要更新对应的state,其他的页面级别的渲染等交给 observer 监听的组件。简单使用流程如下⬇️ {.line-numbers}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175// 1 - 封装useContext/*** * * 创建总的store文件夹 * ./store * ./store/index.ts * ./store/room.ts */// 1-1 ./store/index.tsimport { useContent, createContext } from 'react'import { types, Instance, onSnapshot, applySnapshot} from 'mobx-state-tree'import room from './room'const RootModel = types.modal('RootModel', { room: room.Modal, // 还可以扩展相关其他的store // users: users.Modal,}).action(self => { return {}})let rootStore;export const initStore = ({roomId, allRoomInfo}) => { const _rootStore = rootStore ?? RootModal.create({ room: assign({}, room.initStore, { roomId: roomId, allRoomInfo, }) //users: users.initState, }) if ( typeof window !== 'undefined' ) { window.__store = _rootStore } if (typeof window === 'undefined' ) return _rootStore if (!rootStore) { rootStore = _rootStore // onSnapshot(rootStore, snapshot => console.log(\"stage Snapshot: \", snapshot)); } return rootStore;}export type RootInstance = Instance<typeof RootStore>const RootStoreContext = createContext<null || RootInstance>(null);export const Provider = RootStoreContext.provider;export const useStore = () => { const store = useContext(RootStoreContext); if (store === null) { throw new Error('Store cannot be null, please add a context provider'); } return store;}export const getStore = () => { return rootStore;}// 创建具体store(room.ts)文件import { flow, types } from 'mobx-state-tree'import { getStore } from './index'const initState = { roomId: 0, allRoomInfo: {}}const PkModel = types.model('PkModel', {})const AllRoomInfoModel = types.model('AllRoomInfoModel', { _id: '', pk: types.maybeNull(PkModel)}).views(self => { return {}}).actions(self => { return {}})export const Model = types .model({ roomId: types.number, allRoomInfo: AllRoomInfoModel, }) .views(self => { return { get hasId() => { return self._id !== '' }, get isRoomOwner() { let userId = getGlobalStore().userId; return userId && userId === self.roomId; } } }) .volatile(self=> { return {} }) .actions(self => { const update = flow(function* () { let res; try { res = yield request.api.get('****/api/room/${self.roomID}') self.allRoomInfo = res.data } }), const setRoomInfo = (data) => { self.allRoomInfo = data }, const stopLive = () => { ... } return { update, setRoomInfo, stopLive, } })export default { initState, Modal,}// 3-组件应用import { observer } from 'mobx-react'import { useStore } from '../store'const BeHeadTip = observer(() => { const { room } = useStore() const { roomId, allRoomInfo, hasId} = room return ( !hsdId ? '当前房间未在开播哟~' : <StyledBeHead id={ roomId }>组件内容</StyledBeHead> )})const StyledBeHead = styled.div` position: relative;`// 4-接口、推送更新store// 4-1 接口const loadInfo = () => { request.api.get('**/api/info_v2').then(res => { setRoomInfo(res.data) })}export default { loadInfo }// 4-2 推送const handle = (socket) => { socket.on('room_info', (msg) => { setRoomInfo(res.data) })} 封装以上的创造store,可以简单封装下逻辑,让程序员更专注于model的构建 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778// utils/create-store.tsimport { Instance } from 'mobx-state-tree'import { createContext, useContext } from 'react'const createStore = ( Model: any, initialState: any ) => { type IStore = Instance<typeof Model> const store = Model.create(initialState) const StoreContext = createContext<null | IStore>(null) if (typeof window !== 'undefined') { if (window['__Act' + Model.name]) { window['__Act' + Model.name + random(100000, 10000)] = store } else { window['__Act' + Model.name] = store } } const useStore = ():IStore => { const currentStore = useContext(StoreContext) if (currentStore === null ) { throw new Error(`${Model.name} Store cannot be null, please add a context provider`) } return currentStore; } return { store, Provider: StoreContext.Provider, useStore, }}export default createStore;//具体文件使用// **/store.tsximport { createStore } from '@utils/create-store'export const Model = types .model('Model', { currentTab: '', list: [], }) .action((self) => { const setCurrTab = (tab: string) => { self.currentTab = tab } return { setCurrTab } })interface IModelSnapshotIn extend SnapshotIn<typeof Model>{}// const initState: IModelSnapshotIn = {}const initState = { currentTab: 'home', list: []}const { store, Provider, useStore } = createStore(Model, initState)export { store, Provider, useStore }// 页面引用store// **/index.tsximport { useStore } from './store'const { currentTab, setCurrTab } = useStore() 总结在整个程序执行中,我们只需要控制数据状态的更新,以及在MST中处理好数据的逻辑,暴露出直接可以使用的方法。例如 hasId, MST中的 views 相当于 Vue 中的 计算属性,根据依赖值的变化,计算最新的结果。数据的更新只能在 actions 中暴露的方法中去实现。在面对具有非常复杂状态的大型项目时,可以提高开发效率。","categories":[{"name":"默认","slug":"默认","permalink":"http://fanghl.top/categories/默认/"}],"tags":[{"name":"默认","slug":"默认","permalink":"http://fanghl.top/tags/默认/"}]},{"title":"么么直播-总结","slug":"summary-meme","date":"2021-06-07T11:25:24.000Z","updated":"2023-11-03T03:29:00.561Z","comments":true,"path":"2021/06/07/summary-meme/","link":"","permalink":"http://fanghl.top/2021/06/07/summary-meme/","excerpt":"","text":"序言简单记录一下在么么直播遇到的问题以及解决办法,项目技术栈跨度比较大,有 6-7 年前 jquery 项目,也有 React + MST(mobx state tree) + SSR 项目,还有纯 React 项目,遇到的问题比较广泛,记录一下常见的问题,帮助学习。 React 的更新机制Ref 的一些用法受控组建和非受控组件命名空间Canvas 实现弹幕组件 实现弹幕的核心是 Canvas 的 measureText()方法,该方法可以计算出画布上字体的宽度,由于弹幕的内容一般是由 相对固定的图片加未知长度的文案构成,渲染复杂的单条弹幕首先需要解决弹幕总长度,拿到了总长度,那么不管是总体的弹幕背景还是图片文案的未知都能 准确无误的渲染出来,React 可以把功能做成一个组件,一次完成,多次复用,这里我简单列举两种弹幕的实现,一种是普通的弹幕,构成是背景色 + 用户头像 + 相对固定的文案(比如抽奖弹幕,头像 + XXX 在 VVV 活动中 抽中了 AAAA x 99 次), 一种是特殊弹幕,比如春节期间产品上线了祈福送礼需求,用户发送祝福语,然后立即在屏幕上弹幕形式出现,每条祝福语弹幕的背景样式不同,🈶️ 新春对联、燕子高飞、柳树纸条等,切每个用户输入的祝福语长度取决于用户自己,有时候一条弹幕就几个字,有的有几十字,知道每条弹幕的长度有两个用处,一是弹幕的背景位置渲染,而是弹幕采用四行并存的形式,那么弹幕插入哪一行也取决于哪一行的弹幕稀疏程度,话不多少,上图上代码: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477// common 弹幕的引用文件export function getDevicePixelRatio(): number { // Fix fake window.devicePixelRatio on mobile Firefox const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1 if (window.devicePixelRatio !== undefined && !isFirefox) { return window.devicePixelRatio } else if (window.matchMedia) { const mediaQuery = (v: string, ov: string) => { return ( '(-webkit-min-device-pixel-ratio: ' + v + '),' + '(min--moz-device-pixel-ratio: ' + v + '),' + '(-o-min-device-pixel-ratio: ' + ov + '),' + '(min-resolution: ' + v + 'dppx)' ) } if (window.matchMedia(mediaQuery('1.5', '3/2')).matches) { return 1.5 } if (window.matchMedia(mediaQuery('2', '2/1')).matches) { return 2 } if (window.matchMedia(mediaQuery('0.75', '3/4')).matches) { return 0.7 } } return 1}// barrage-spring.tsximport { cancelAnimation } from '@utils/media'import { getDevicePixelRatio } from '@core/client'import { max } from '@utils/tool' const roundRect = function (ctx, left, top, width, height, r) { const pi = Math.PI; ctx.beginPath(); ctx.arc(left + r, top + r, r, -pi, -pi / 2); ctx.arc(left + width - r, top + r, r, -pi / 2, 0); ctx.arc(left + width - r, top + height - r, r, 0, pi / 2); ctx.arc(left + r, top + height - r, r, pi / 2, pi); ctx.closePath(); } const circleImg = function (ctx, img, l, t, r){ const d = 2 * r ctx.save(); ctx.beginPath() ctx.arc(l + r + 30, t + r - 3, r, 0, 2 * Math.PI) ctx.closePath() ctx.clip(); ctx.drawImage(img, l + 30, t - 3, d, d); ctx.restore(); } const leftBg = function (ctx, leftImg, left, top, width, height) { ctx.save() ctx.drawImage(leftImg, left, top, width, height) // height 94 ctx.restore(); } const middleBg = function (ctx, img, left, top, width, height) { ctx.save() ctx.drawImage(img, left+112, top, width, height) ctx.restore(); } const rightBg = function (ctx, img, left, top, width, height) { ctx.save() ctx.drawImage(img, left, top, width, height) ctx.restore(); } // const preImg = (url, callback, options) => { // const {x, y, w, h} = options // const img = new Image() // img.src = testLeft || url // if (img.complete) { // callback.call(img, x, y, w, h) // return // } // img.onload = () => { // callback.call(img, x, y, w, h) // } // } const getPxRatio = () => { const c = document.createElement(\"canvas\"), ctx = c.getContext(\"2d\"), dpr = getDevicePixelRatio() || 1, bsr = ctx['webkitBackingStorePixelRatio'] || ctx['mozBackingStorePixelRatio'] || ctx['msBackingStorePixelRatio'] || ctx['oBackingStorePixelRatio'] || ctx['backingStorePixelRatio'] || 1; return dpr / bsr; } export class Barrage { constructor(option) { const { canvasId, onPalyFinish } = option const pixelRatio = max([getPxRatio(), 2]) this.pxRatio = pixelRatio; // 缩放倍数,1会糊 this.canvas = document.getElementById(canvasId); this.canvas.width = this.w = this.canvas.width * pixelRatio; this.canvas.height = this.h = this.canvas.height * pixelRatio; this.drawing = true this.finishCount = 0 this.ctx = this.canvas.getContext('2d'); this.style = { // 弹幕样式 height: 47 * pixelRatio, // 弹幕高度 // 旧版高度29 imgBgWidth: 56 * pixelRatio, // 背景图片的宽度(左、右) fontSize: 12 * pixelRatio, // 字体大小 marginBottom: 20 * pixelRatio, // 弹幕 margin-bottom paddingX: 15 * pixelRatio, // 弹幕 padding x avatarWidth: 20 * pixelRatio, // 头像宽度 ellipsisMaxWidth: 100 * pixelRatio, offsetRight: 56 * pixelRatio // 右背景偏移量 } this.ctx.font = this.style.fontSize + 'px PingFangSC-Regular'; this.onPalyFinish = onPalyFinish this.barrageList = []; // 弹幕列表 this.rowStatusList = []; // 记录每行是否可插入,防止重叠。 行号为可插入 false为不可插入 let rowLength = Math.floor(this.h / (this.style.height + this.style.marginBottom)); for (var i = 0; i < rowLength; i++) { this.rowStatusList.push(i) } } shoot(value) { const { height, avatarWidth, fontSize, marginBottom, paddingX, ellipsisMaxWidth } = this.style; const { img, sortArr, t1, t2, t3, t4 } = value; const ellipsisT2 = this.getEllipsisText(t2) let row = this.getRow(); let color = '#7C0102'; let offset = this.pxRatio; let offsetNew = 30 let w_0 = paddingX; // 头像开始位置 let w_1 = w_0 + avatarWidth + 8 + offsetNew + 10; // t1文字开始位置 let w_2 = w_1 + Math.ceil(this.ctx.measureText(t1).width) + 8; // t2文字开始位置 let w_3 = w_2 + Math.ceil(this.ctx.measureText(ellipsisT2).width) + 8; // t3文字开始位置 let w_4 = w_3 + Math.ceil(this.ctx.measureText(t3).width) + 8; // t4文字开始位置 let w_5 = w_4 + Math.ceil(this.ctx.measureText(t4).width) + paddingX + 8; // 弹幕总长度 let barrage = { color, row, offset, top: row * (height + marginBottom), left: this.w, width: [w_0, w_1, w_2, w_3, w_4, w_5], value, ellipsisT2, } this.barrageList.push(barrage); } draw() { if (!this.drawing) { return } if (!!this.barrageList.length) { this.ctx.clearRect(0, 0, this.w, this.h); for (let i = 0, barrage; barrage = this.barrageList[i]; i++) { // 弹幕滚出屏幕,从数组中移除 if (barrage.left + barrage.width[5] <= -25) { this.barrageList.splice(i, 1); this.finishCount ++; i--; continue; } // 弹幕完全滚入屏幕,当前行可插入 if (!barrage.rowFlag) { if ((barrage.left + barrage.width[5]) < this.w - 45) { // this.rowStatusList[barrage.row] = barrage.row; barrage.rowFlag = true; } } barrage.left -= barrage.offset; this.drawBarrage(barrage); } } this.reqAnimeId = requestAnimationFrame(this.draw.bind(this)); } restartDraw() { this.drawing = true; this.draw() } clearDraw() { this.drawing = false cancelAnimation(this.reqAnimeId) } drawBarrage(barrage) { const { height, avatarWidth, fontSize, ellipsisMaxWidth } = this.style; const { value: { img, sortArr, t1, t3, t4,}, ellipsisT2, color, row, left, top, offset, width, } = barrage; // 画框子 // roundRect(this.ctx, left, top, width[5], height, height / 2, avatarWidth) // this.ctx.fillStyle = 'rgba(0,0,0,0.45)'; // this.ctx.fill(); // -- 画左边背景 leftBg(this.ctx, sortArr[0], left , top, this.style.imgBgWidth, height) middleBg(this.ctx, sortArr[1], left , top, width[2]-width[1], height) rightBg(this.ctx, sortArr[2], left + width[5]- this.style.offsetRight , top, this.style.imgBgWidth, height ) // left, top, width[1], height // 画头像 // circleImg(this.ctx, img, left + width[0], top + (height - avatarWidth) / 2, avatarWidth/2) circleImg(this.ctx, sortArr[3], left + width[0], top + (height - avatarWidth) / 2, avatarWidth/2) // 新的top偏移量 15 const offsetYNew = -4 const paddingTop = (height - fontSize) / 2 - 2 this.ctx.fillStyle = color; this.ctx.fillText(t1, left + width[1], top + fontSize + paddingTop + offsetYNew); this.ctx.fillStyle = '#CFFCFC'; this.ctx.fillText(ellipsisT2, left + width[2], top + fontSize + paddingTop); this.ctx.fillStyle = color; this.ctx.fillText(t3, left + width[3], top + fontSize + paddingTop); this.ctx.fillStyle = '#FFFF33'; this.ctx.fillText(t4, left + width[4], top + fontSize + paddingTop); } getRow() { let emptyRowList = this.rowStatusList.filter(d => /\\d/.test(d)); // 找出可插入行 let row = emptyRowList[Math.floor(Math.random() * emptyRowList.length)]; // 随机选一行 this.rowStatusList[row] = false; return row; } haveEmptyRow() { let emptyRowList = this.rowStatusList.filter(d => /\\d/.test(d)); // 找出可插入行 return !!emptyRowList.length; } getEllipsisText(text) { const { ellipsisMaxWidth: maxWidth } = this.style if (this.ctx.measureText(text).width <= maxWidth) { return text } const textArr = text.split('');//当前剩余的字符串 for (let m = 1; m <= textArr.length; m++) { if (this.ctx.measureText(textArr.slice(0, m)).width > maxWidth) { return textArr.slice(0, m).join('') + '...' } } } } // danmu.tsx 弹幕组件,可以直接调用 <danmu /> import React, { useEffect, useRef } from 'react' import styled from 'styled-components' import { defAvatarNew } from '@constants' import request from '@core/request' // import { Barrage } from '@utils/barrage' import { Barrage } from '@pages/act_spring_festival/barrage-spring' import { choice } from '@utils/tool' import { actions } from './config' export default (props: any) => { const storeRef = useRef<any>({ timer: 0, finishCount: 0, barrage: null, }) useEffect(() => { fetchData() }, []) useEffect(() => { const barrage = new Barrage({ canvasId: 'act-spring-barrage' }) barrage.draw() storeRef.current.barrage = barrage return () => { barrage.clearDraw() clearTimeout(storeRef.current.timer) } }, []) const fetchData = () => { request(actions.wishList).then((resData) => { const items = resData || [] if (items && items[0]) { shootBarrage(items[0]) setTimeout(() => shootBarrage(items[1]), 500) startBarrage(2, items) } }) } const startBarrage = (activeIndex: number, source: any[]) => { const { timer, barrage } = storeRef.current clearTimeout(timer) storeRef.current.timer = setTimeout(() => { let flag = false if (source[activeIndex]) { flag = shootBarrage(source[activeIndex]) } if (barrage.finishCount && barrage.finishCount >= source.length - 3) { barrage.finishCount = 0 fetchData() } startBarrage(!flag ? activeIndex : activeIndex + 1, source) }, choice([1000, 1800])) } const shootBarrage = (currentItem: any) => { const barrage = storeRef.current.barrage if (!barrage.haveEmptyRow() || !currentItem) { return false } const { pic = defAvatarNew, wish = '' } = currentItem const data = { t1: wish, t2: '', t3: '', t4: '', } // --一起初始化背景图 const imgConf1 = [ 'https://img.sumeme.com/28/4/1612232722204.png', 'https://img.sumeme.com/8/0/1612232705544.png', 'https://img.sumeme.com/14/6/1612232681614.png', pic, ] const imgConf2 = [ 'https://img.sumeme.com/32/0/1612232775520.png', 'https://img.sumeme.com/32/0/1612232762208.png', 'https://img.sumeme.com/54/6/1612232743414.png', pic, ] const imgConf3 = [ 'https://img.sumeme.com/27/3/1612232818395.png', 'https://img.sumeme.com/48/0/1612232804656.png', 'https://img.sumeme.com/32/0/1612232789280.png', pic, ] // const imgConf4 = [ // 'https://img.sumeme.com/25/1/1612232866777.png', // 'https://img.sumeme.com/32/0/1612232846432.png', // 'https://img.sumeme.com/33/1/1612232833441.png', // pic, // ] const imgConfAll = [imgConf1, imgConf2, imgConf3] const imgArray = choice(imgConfAll) const receiveArray: any[] = [] // let $myContent = document.getElementById(\"myContent\"); // let [imgW, imgH] = [300, 300]; // let Canvas = document.createElement('canvas'); // let ctx = Canvas.getContext(\"2d\"); // let scaleBy = 2; // Canvas.width = imgW * scaleBy; // Canvas.height = imgH * scaleBy; imgArray.forEach((e: any, idx: number) => { const img = new Image() img.src = e img.setAttribute('crossOrigin', 'Anonymous') img.addEventListener('load', () => { // ctx.drawImage(img, 0, 0, imgW * scaleBy, imgH * scaleBy); img.id = 'img' + idx receiveArray.push(img) // 将绘制的img节点收集到数组里,这里的顺序可能和imgArray的顺序不一样 if (receiveArray.length === imgArray.length) { // 所有图片load并绘制完成 const sortArr = new Array() receiveArray.forEach((ex) => { // 将所有绘制图片按imgArray顺序排序 sortArr[ex.id.split('img')[1]] = ex }) barrage.shoot({ sortArr, ...data, }) // sortArr.forEach(ex2 => { // $myContent.appendChild(ex2) // }) } }) }) // const img = new Image() // img.setAttribute('crossOrigin', 'anonymous') // const data = { // t1: wish, // t2: '', // t3: '', // t4: '', // } // img.onload = () => { // barrage.shoot({ // img, // ...data, // }) // } // img.onerror = () => { // barrage.finishCount++ // } // let pic1 = 'https://img.sumeme.com/27/3/1611904142299.png' // console.log('---', pic) // img.src = pic1 return true } return ( <StyledBarrage> <canvas id=\"act-spring-barrage\" height=\"250px\" /> </StyledBarrage> ) } export const StyledBarrage = styled.div` position: absolute; bottom: -120px; width: 750px; height: 550px; /* z-index: 99999; */ canvas { width: 100%; height: 100%; } ` 抽空把这个弹幕写个 demo ,光干巴巴的文字是在难以理解啊 Node 的版本控制 这个一般使用 nvm 或者 n 命令 移动端和 H5 的桥接通信 使用 jsBridge 进行 H5 和移动端的通信。具体后面整理一下。 直播礼物的动画播放队列实现Video 播放 mp4 的注意点 react 中播放 mp4 格式,会有一些 iOS 机型的兼容问题,不如 iOS 不能自动播放等 12345678910111213141516171819202122//React中播放mp4的情况,一帮情况下播放GIF或者SVGA// 代码如下<div className=\"video-box\" dangerouslySetInnerHTML={{ __html: ` <video id=\"entry-video\" poster=\"https://img.sumeme.com/16/0/1624613307152.png\" autoPlay x-webkit-airplay=\"allow\" x5-video-player-type=\"h5\" webkit-playsinline playsinline muted style=\"object-fit:fill\" > <source src=\"https://img.sumeme.com/swf/Render6-16.mp4\" type=\"video/mp4\"> </video> `, }}/> poster属性可以在视频未加载完成前展示一张封面图片,视频加载后自动播放视频。hooks 封装其实相比自己封装有针对性的 hooks 外,阿里的 ahooks3.0 也可以使用,功能还是值得期待的React 中挂载滑动函数抽奖一些 CSS12345678910111213141516171819202122232425262728-webkit-tap-highlight-color: rgba(0,0,0,0)// 解决iOS和iPad设备上点击状态出现默认蓝色高亮,很常见& + & {}取巧,选择非第一个开始的所有同类型元素font-size: 0;父元素设置改属性可以有效解决行内元素的默认间距,例如spanpadding-bottom: 6%;ol li:before { content:counter(sectioncounter) "、"; counter-increment:sectioncounter;}//有序元素的符号替换展示.gift-show-area[gift-id='7777'] {}//可以这样查找元素background-image: linear-gradient(135deg, red, blue);background-clip: text;-webkit-background-clip: text;color: transparent;// 文字颜色渐变::-webkit-input-placeholder// placeholder样式次修改 复杂表格项合并rowSpan colspan 行列合并 按需加载优化项目已经很庞大的情况下,还要考虑hybrid的体验情况,需要进行项目优化,按需加载比较适合某些场景下,这里采用《react-intersection-observer》中的hooks useInView 来判断元素是否在可视窗口内。进而判断是否渲染该元素。 123456789101112131415161718192021222324252627282930313233// 这里封装一个图片按需加载的公用组建import React from 'react'import { useInView, IntersectionOptions } from 'react-intersection-observer'import styled from 'styled-components' const StyledImg = styled.div` width: 100%; height: 100%;`type Props = IntersectionOptions & { src: string title?: string className?: string children?: any}const LazyImg = (props: Props) => { const { src, title, className, children, ...configProps } = props const [res, inView] = useInView({ triggerOnce: true, rootMargin: '20px 0', ...configProps, }) return ( <StyledImg ref={ref} className={className} title={title}> {children} {inView && <img src={src}/>} </StyledImg> )}export default LazyImg Modal封装不论是PC页面,还是hybrid原生页面,都需要大量形形色色的弹窗来通知用户处理业务逻辑,封装几种常见的Modal CSS 点九图最近年中和周年庆开始,铺天盖地的活动。UI 设计的风格和一往不太一样,举一个栗子: 在投票页面中,每个被投票的主播都是单独的一张特殊背景图包裹,该容器可能会根据被投票人的信息长短不一,不规则背景边框图也要自动适应。类似这样的需求,一般有这么几种方法实现: 三段图重复就是把不规则的背景图切成三段。头部、中间部分、底部,中间部分利用背景图的 repeat 来自适应,缺点就是不灵活,需要找 UI 切图,里面内容的间距控制不精准 点九图点九图是移动端的一种做法,就是一张图切四刀,四个角不伸缩,保持原图比例。四条边进行伸缩,中间的部分用来填充,一共九个部分,所以称点九图。CSS3 也可以实现点九图,且效果不错,举个例子:写一个业务组件,只用来做 wrap 包裹,用点九图,这样其他的同样式的组件都可以复用。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091import React from 'react'import styled from 'styled-components'import { StyledBaseWrap } from '../styled'type Props = { children: any title?: string // title?: string // headerType?: 'icon' | 'pureString'}export default (props: Props) => { const { children, title = '' } = props if (!children) { return null } return ( <StyledBaseWrap> <i className=\"bg\" /> { title && ( <div className=\"title-bg-box\"> <i className=\"title-bg\" /> {/* <p className=\"you-she\">{title}</p> */} <div className=\"you-she\">{title}</div> </div> ) } {children} </StyledBaseWrap> )}const StyledBaseWrap = styled.div` position: relative; width: 680px; margin: 0 auto; min-height: 320px; & > * { margin: 0 auto; } .bg { position: absolute; left: 0; right: 0; top: 0; bottom: 0; display: block; border-style: solid; border-width: 130px 340px 190px 340px; border-image-source: url(${require('./images/bg_wrap.png')}); border-image-slice: 130 340 190 340 fill; border-image-width: 1; border-image-repeat: repeat; } .title-bg-box { position: absolute; min-width: 380px; top: -20px; margin: 0 auto; font-size: 32px; left: 50%; transform: translateX(-50%); padding: 0 100px; box-sizing: border-box; div { position: relative; height: 52px; line-height: 52px; white-space: nowrap; } } .title-bg { position: absolute; left: 0; right: 0; top: 0; bottom: 0; display: block; border-style: solid; border-width: 24px 172px 24px 172px; border-image-source: url(${require('./images/title_bg.png')}); border-image-slice: 24 172 24 172 fill; border-image-width: 1; border-image-repeat: repeat; margin: 0 auto; }` 使用 ‘border-image-slice’ 属性来完成点九图,它接受 4 个参数,分别在图片的上右下左切一刀,把图片分为 9 个部分,一中心,四个角,四个边。伸缩只会让边进行伸缩,所以需要调整切的位置,尽量在规则的地方下刀。此时,若父容器的宽高未给定,则完全由内容撑开宽高,上面栗子中,宽度做了限制,高度未限制,传入的 children 会撑开点九图组件的高度,做到每个子组件高度根据内容自适应,但整体的样式不会发生变化。 Hybrid全面屏H5页面在iOS和安卓应用,通常是移动端给了屏幕空间,用来展示H5页面,不包含顶部电池栏tab,新的沉浸式体验则需要H5页面也要控制电池区域,铺满整个移动端屏幕。 每个手机设备的电池区域高度不尽相同,且设备的dpi也不一致,iOS是相对固定的22像素,安卓则是五花八门,这里需要桥接通信拿到移动端的“tab高度”和设备dpi,有这两个参数,H5页面则可以实现统一的全面屏幕沉浸式体验。 dpi :当前显示设备的物理像素分辨率与CSS像素分辨率之比,需要进行转化为H5的px单位,基本算法分为: 123456789101112131415// res 为桥接通信移动端返回的数据// statusBarHeight为状态栏移动端高度if (res.data.statusBarHeight) { if (client.instance.inIOsNative() ) { statusBarHeight.setValue(res.data.statusBarHeight) } else { statusBarHeight.setValue(res.data.statusBarHeight / window.devicePixelRatio) }}// react<HeaderBar style={{ paddingTop: `${statusBarHeight.value}px`, ...style }} className={`${className} `}> iOS的高度不需要额外转换,一般iOS机型返回都是22px,安卓则需要除以dpi得到CSS像素。 拿到最终的状态栏高度,进行app-header的布局,基本tab栏高度一般为 88 像素,再加上状态栏(电池栏)的高度,如果整个头部整体需要fixed布局,全局则增加padding-top 取巧实现。整个H5页面总体分为两个区域,tab栏和content内容页,一般tab栏使用纯色背景,内容页则有时候会使用渐变色,此时,content的高度无法确定,则整体页面使用 flex布局,tab栏使用 shrink: 0 ; 禁止缩放,content则使用 flex: 1; 自动填充满视口,这样,内容页的渐变色则和tab页无缝衔接。 hybrid touch bar判断js判断当前手机是否有touch bar,如果存在 touch bar ,则App全局头部添加样式类名,后续业务只需根据对应CSS标识处理不同的样式。 解决: iOS 手机屏幕底部存在白线(操作栏),会遮挡页面的一部分,常见的页面底部会存在用户点击按钮或其他UI,操作栏会降低用户的体验。判断iOS存在 touch bar ,则增加全局样式,在对应子业务中修改样式即可避免。 123456789101112131415<!-- App全局样式(app.js) -->if ( /iphone/gi.test(navigator.userAgent) && window.devicePixelRatio && window.devicePixelRatio >= 2 && window.screen.height >= 812) { document.querySelector('html').classList.add('fix-bottom');}<!-- 具体子业务 -->.fix-bottom & { padding-bottom: 60px;} iframe 跨域通信在对接一些第三方的游戏时,采用 App客户端 嵌入 Hybrid H5前端,前端对接第三方。比如,抖音直播间火起来的弹幕游戏和其他趣味游戏,在直播间接入这些三方游戏,在关闭游戏、支付等方面,涉及三端跨域通信。 这里有一个案例,游戏是第三方的,但游戏内充值兑换货币的页面是我们的。游戏最终需要在客户端、Web端、桌面端(window app, 主要是开播工具)三端展示。这里页面互相嵌套,但本质就是子父页面通信。 // 父页面监听单个事件 window.addEventListener('message', _handleMsg) const _handleMsg = (event) => { // doSomeThing } // 子页面发送事件 // window.parent.postMessage('你的参数, '*') 第二个参数即解决跨域问题,也可填写父窗口的域名 window.parent.postMessage('你的参数, 'https://0.0.0.9200') // window.parent 返回父窗口 // window.top 返回最顶层窗口,不一定是父窗口 const closeWebView = () => { window.parent.postMessage( { type: 'webViewEvent', source: 'xxx', event: 'close', }, '*', ); }; 自定义hooks 在手写较多的 useEffect 的时候,就应该抽一个 hooks 出来。 React的官网原话也有: 如果你发现自己经常需要手动编写 Effect,那么这通常表明你需要为组件所依赖的通用行为提取一些 自定义 Hook。","categories":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/categories/Programming/"}],"tags":[{"name":"么么","slug":"么么","permalink":"http://fanghl.top/tags/么么/"}]},{"title":"ES6","slug":"ES6","date":"2021-04-13T11:18:29.000Z","updated":"2023-11-03T03:29:00.555Z","comments":true,"path":"2021/04/13/ES6/","link":"","permalink":"http://fanghl.top/2021/04/13/ES6/","excerpt":"","text":"node版本管理不同项目交叉开发时,可能会出现node版本的冲突,常见有 nvm 插件来解决,但 nvm 并不是全自动,需要手动切换版本 nvm use … ,这里采用社区提供的 avn (avn)[https://github.com/wbyoung/avn],思路为: 项目根目录下创建 .node-version文件,以 server 格式约定好 Node 版本,如: 9.8.0,在CD到项目目录时,avn会自动切换到制定的Node版本。 n命令n命令来管理node的不同版本,基本为 123456789101112131415161718192021222324252627282930313233$ npm i -g n$ n ls-remote --all // 查看所有可安装的版本$ n <version> // 安装node,ex: n 10.15.0$ n ls // 本地已安装的版本sudo n run <version> // 使用某个版本n // 直接n 也可以选择版本sudo n rm <version> // 删除多余的版本``` 简单说下优缺点吧 优点: 方便,快速,相对avn来说, n命令安装方便,一条命令解决,avn则需要改IP地址或者科学上网等, 缺点: 这版本是全局且手动的,自己的项目可以用用,要是大型开发项目,建议用nvm编写 script 启动命令来控制版本,否则项目之间可能出现node版本冲突。 PS: n 命令大部分都需要使用 sudo 打开 ## 数组移除false类型 简单记录一个数组的移除 false 方法 ```javascriptconst arr = [1, "str", false, NaN, undefined, null]const res = arr.filter(Boolean)// Reactconst hideSomething ?: booleanconst options: any = [ hideSomething !== true && { otherOption: 'test' }, { normalOptions: 'normal' },].filter(Boolean) 解构附值log123const { log, warn } = consolelog('something')","categories":[{"name":"默认","slug":"默认","permalink":"http://fanghl.top/categories/默认/"}],"tags":[{"name":"默认","slug":"默认","permalink":"http://fanghl.top/tags/默认/"}]},{"title":"Amis","slug":"amis","date":"2021-02-24T12:11:31.000Z","updated":"2023-11-03T03:29:00.557Z","comments":true,"path":"2021/02/24/amis/","link":"","permalink":"http://fanghl.top/2021/02/24/amis/","excerpt":"","text":"Aims系列的高级用法Amis是百度开源的一套企业级管理系统,一个低代码前端框架,它使用 JSON 配置来生成页面,可以减少页面开发工作量,极大提升效率。 Ovine一个支持使用 Json 构建完整管理系统 UI 的框架,基于 Amis 二次开发。 相关文档依赖 ReactAmisstyled-componentimmerfont-awesome icon使用在Ovien项目中,icon的使用很简单也非常丰富,在[font-awesome](https://fontawesome.dashgame.com/)中选取适合你的icon名称即可 1icon: 'fa fa-${icon_name}'onFakeRequest应用amis对后端或者说中台转换依赖比较强,返回的API数据一般是后端组装好的JSON数据,直接渲染,但是对于其他后端不是很强大的项目来说,onFakeRequest应用就很有必要,它实现了假请求,在假请求里根据返回的数据拼接我们需要的schema,最后返回出去进行渲染,如下动态渲染 动态渲染动态渲染目前只能在 Service 容器中实现,核心思路是由后台返回需要的数据,再加以拼接返回一个Schema进行渲染而成。常见需求为: 某一个管理页面的行的每一项不是固定的,是根据其他配置页数据来渲染的,例如直播道具的使用情况,道具并不是事先就约定好的,是可配置的,想要渲染道具的使用数据表就得动态渲染表头。表头的数据接口只返回了简单的标识,未返回schema节点,我们在 onFakeRequest 里拼接schema返回渲染,具体看代码: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134schemaApi: {$preset: 'apis.tabConf',url: 'fakeRequest',onFakeRequest: async (source) => { // 假请求(Ovien实现) const confData = await app.request({ url: 'GET api/v1/apps/meme/hot-card', domain: 'flint', }) const cardConf = confData.data.data['hot-card']['hot-cards'] || {} const colConf = [ { name: 'nickname', label: '用户昵称(ID)', type: 'tpl', tpl: '${nickname}(ID:${_id})' } ] const selectConf = [] Object.keys(cardConf).map((key) => { const item = cardConf[key] const colOpt = { label: `${item['gift-name']}(未使用)` || '-', name: key, type: 'tpl', // tpl: `<%= (data.remain[${key}] || '-') + '/' + (data.total[${key}] || '-') %>`, tpl: `<%= (data.remain[${key}] || '/') %>`, } const selectOpt = { label: `${item['gift-name']} (${item['hot-value']}热度/张,生效${item.duration}分钟)`, value: key } selectConf.push(selectOpt) colConf.push(colOpt) return true }) // 二次弹窗内容 const retrieveCard = { api: { url: 'GET hotcard/del.json', data: { user_id: '$_id', gift_id: '$giftSelect', num: '$delNum', } }, type: 'form', horizontal: { left: 'col-sm-3', right: 'col-sm-8', }, controls: [ { type: 'select', label: '热度卡类型', name: 'giftSelect', required: true, options: selectConf, }, { type: 'number', name: 'delNum', label: '回收数量', description: '请输入每人回收热度卡的数量', required: true, // min: 1, precision: 0, }, ] } const retrieve: any = { type: 'action', label: '回收热度卡', level: 'danger', // visibleOn: '!data.status', actionType: 'dialog', dialog: { title: '回收热度卡', body: retrieveCard, }, } colConf.push(retrieve) const schemaNode = { type: 'lib-crud', syncLocation: false, // api: '$preset.apis.remainList', primaryField: '_id', perPageField: 'size', pageField: 'page', perPageAvailable: [50, 100, 200], defaultParams: { size: 50, }, api: { url: 'GET hotcard/remain.json' }, // source: '$rows', headerToolbar: [ { type: 'columns-toggler', align: 'left', }, // { // $preset: 'actions.add', // align: 'right', // }, ], footerToolbar: ['statistics', 'switch-per-page', 'pagination'], columns: colConf, filter: { type: 'form', title: '搜索', controls: [ { type: 'text', name: 'user_id', value: '', placeholder: '输入用户ID搜索', }, { type: 'submit', className: 'm-l', label: '搜索', }, ] }, } source.data = schemaNode return source}},","categories":[{"name":"默认","slug":"默认","permalink":"http://fanghl.top/categories/默认/"}],"tags":[{"name":"默认","slug":"默认","permalink":"http://fanghl.top/tags/默认/"}]},{"title":"React开发总结","slug":"React","date":"2020-11-11T08:33:03.000Z","updated":"2023-11-03T03:29:00.557Z","comments":true,"path":"2020/11/11/React/","link":"","permalink":"http://fanghl.top/2020/11/11/React/","excerpt":"","text":"传送门 React Mobx Mobx-state-tree React-Hooks Typescript VAP react-query SVGR polished immerjs 序言小程序和Vue要告一段落了,接下来要进行 React 、typescript、 Express 开发了。项目主要是一个2013年的直播项目,当时采用的是 Express \\ Ejs 实现的,现在V2版本一直陆续再往React、typescript方面重构。V2版本采用最新的React系列开发。知识点包括以上传送门等。 React","categories":[{"name":"React","slug":"React","permalink":"http://fanghl.top/categories/React/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"一些三方库","slug":"一些三方库","date":"2020-10-07T07:45:08.000Z","updated":"2023-11-03T03:35:02.534Z","comments":true,"path":"2020/10/07/一些三方库/","link":"","permalink":"http://fanghl.top/2020/10/07/一些三方库/","excerpt":"","text":"粘贴复制clipboard.js 现代化的拷贝文字,不依赖 flash, 不依赖其他框架,gzip 压缩后只有 3kb 大小 12yarn add clipboard npm install clipboard --save 123456789101112131415161718import ClipboardJS from 'lib/clipboard.min';useEffect(() => { const clipboard = new ClipboardJS('id'); clipboard.on('success', (e) => { UiToast.text('复制成功'); e.clearSelection(); }); clipboard.on('error', (e) => { UiToast.fail('复制失败'); # doSomeThing() }); return () => { clipboard.destroy(); };}, [codeStr.value]); React 拖拽","categories":[{"name":"三方库","slug":"三方库","permalink":"http://fanghl.top/categories/三方库/"}],"tags":[{"name":"React","slug":"React","permalink":"http://fanghl.top/tags/React/"}]},{"title":"Vue","slug":"Vue","date":"2020-09-27T09:07:25.000Z","updated":"2023-11-03T03:29:00.557Z","comments":true,"path":"2020/09/27/Vue/","link":"","permalink":"http://fanghl.top/2020/09/27/Vue/","excerpt":"","text":"Vue响应式原理Vue的数据响应式原理主要基于 Object.defineProperty() 函数来劫持数据变化,以及使用发布-订阅者模式达到更新数据的做法。首先要设置一个监听器Observer,用来监听所有的属性,当属性变化时,就需要通知订阅者Watcher,看是否需要更新.因为属性可能是多个,所以会有多个订阅者,故我们需要一个消息订阅器Dep来专门收集这些订阅者,并在监听器Observer和订阅者Watcher之间进行统一的管理.以为在节点元素上可能存在一些指令,所以我们还需要有一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令初始化成一个订阅者Watcher,并替换模板数据并绑定相应的函数,这时候当订阅者Watcher接受到相应属性的变化,就会执行相对应的更新函数,从而更新视图. 整理上面的思路,我们需要实现三个步骤,来完成双向绑定: 实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。 实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。 实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。 let {log} = console function defineReactive(data,key,val) { observe(val); //递归遍历所有的属性 Object.defineProperty(data,key,{ enumerable:true, //当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。 configurable:true, //当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中 get() { return val; }, set(newVal) { val = newVal; log(`属性${key}已被监听,现在值为\"${newVal.toString()}\"`) } }) } function observe(data) { if(!data || typeof data !== 'object') { return; } Object.keys(data).forEach(function(key){ //遍历每一个数据 defineReactive(data,key,data[key]); }); } let data = { user1: { name: '' }, user2: '' }; observe(data); data.user1.name = '约翰'; // 属性name已经被监听了,现在值为:“约翰” data.user2 = '鲍勃'; // 属性book2已经被监听了,现在值为:“鲍勃” 通过上面的代码,我们模拟了Vue实例的数据监听实现过程,这里也很好的解释了为什么页面需要的数据字段必须得在Vue实例挂载前就要注册在 data 对象中,不能动态的在data中设置数据字段,或者说动态的在 data 中设置字段,该字段是不能双向绑定的。原因就在于Vue实例挂载时,已经遍历了data 并为data中每个值都执行了Object.defineProperty(),而之后data中的数据自然就不会监听。","categories":[{"name":"Vue","slug":"Vue","permalink":"http://fanghl.top/categories/Vue/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"服务器部署-flask","slug":"flask-deploy","date":"2020-09-17T03:57:54.000Z","updated":"2023-11-03T03:29:00.558Z","comments":true,"path":"2020/09/17/flask-deploy/","link":"","permalink":"http://fanghl.top/2020/09/17/flask-deploy/","excerpt":"","text":"前言centos 系列的云服务器一般自带Python3.6,使用如下命令查看Python是否提前安装 whereis python 服务器的基本配置可参考之前的 服务器部署-Node 一文 工具: 云服务器 Xshell Navicat Postman 虚拟环境本地测试跑通的项目,生成 requirement.txt 或 Pipfile文件,通过Xshell导入到服务器目录下,使用 pipenv install 创建虚拟环境,并安装依赖。 安装依赖异常这里常见的报错为安装超时 timeout , pip 可以指定安装源。 阿里云 http://mirrors.aliyun.com/pypi/simple/ 中国科技大学 https://pypi.mirrors.ustc.edu.cn/simple/ 豆瓣(douban) http://pypi.douban.com/simple/ 清华大学 https://pypi.tuna.tsinghua.edu.cn/simple/ 中国科学技术大学 http://pypi.mirrors.ustc.edu.cn/simple/ 临时指定安装源 pip install -i https://pypi.tuna.tsinghua.edu.cn/simple 永久指定安装源 linux 修改 ~/.pip/pip.conf (没有就创建一个), 内容如下 [global]index-url = https://pypi.tuna.tsinghua.edu.cn/simple windows 直接在user目录中创建一个pip目录,如:C:\\Users\\xx\\pip,新建文件pip.ini [global]index-url = https://pypi.tuna.tsinghua.edu.cn/simple 服务器运行依赖都已经导入后,尝试运行项目的入口文件,如果报错,请先解决错误。 如果没有错误,此时的项目运行在 localhost:5000 端口,我们在外部是无法访问的,我们修改项目入口文件host: 12if __name__ == \"__main__\": app.run(host=\"0.0.0.0\", port=5000, debug=True) 来监听所有的端口请求。 端口安全组项目监听0.0.0.0,却在外面还是无法访问!使用 nmap your ipAddress 来查看开放的接口,这里5000端口是不存在的,eg: nmap 108.16.12.26 5000端口未被开放,可以使用防火墙命令开启5000端口,防火墙命令见 服务器部署-Node 开启了防火墙之后,如果可以访问5000端口了,那么恭喜你。如果防火墙开启5000端口或者关闭防火墙依然无法访问,那么这里需要去云服务器找到安全组 -> 创建安全组 -> 允许访问所有端口,之后在创建好的安全组,点击进去看到 关联云服务器 ,选择你的服务器实例即可。如此,我们便可以在外网访问到程序 gunicorn gevevnt可以访问项目后,当关闭 Xshell 后,项目仍然无法被访问,所以所以这里使用 gunicorn 来做持久访问。 安装gunicorn gevevnt, gevent 对Windows兼容不是很好。 pip install gunicorn gevevnt 安装好之后,在项目根目录建立 gunicorn.conf.py 内容如下: 123workers = 5 # 定义同时开启的处理请求的进程数量,根据网站流量适当调整worker_class = "gevent" # 采用gevent库,支持异步处理请求,提高吞吐量bind = "0.0.0.0:5000" 使用命令部署项目 gunicorn run:app -c gunicorn.conf.py # run:app 这里的 run 是你项目入口文件 启动后,关闭 Xshell 后依旧可以访问 重启和关闭 先查看进程 pstree -ap|grep gunicorn 得到的结果包含正在运行的进程和我们之前配置的线程数,这里操作的是进程pid。 kill -9 pid # 关闭进程 kill -HUP pid # 重启进程 docker容器部署 docker安装出现了报错 “Problem: package docker-ce-3:19.03.8-3.el7.x86_64 requires containerd.io >= 1.2.2-3, but none of the providers can be installed”解决办法 在这里 启动docker service docker start 创建 Dockerfile 文件 123456789FROM python:3.6WORKDIR /project/PythonProjectCOPY requirements.txt ./RUN pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simpleCOPY . .CMD ["gunicorn", "ginger:app", "-c", "./gunicorn.conf.py"] 构建 docker 镜像 (时间较长) docker build -t ‘testflask’ . 完成镜像后,使用如下命令查看 docker images 会发现存在一个 ‘testflask’镜像存在 配置阿里云镜像仓库 在阿里云dockerhub 点击这里 , 注册账号,他会生成一个阿里云镜像加速链接,(不适用国外的dockerhub,原因你懂的,网络问题), 将这个 加速链接 配置在我们的服务器上 /etc/docker/daemon.json 注意: 不存在 /etc/docker/daemon.json 则创建该文件!并复制链接进去 <!-- /etc/docker/daemon.json --> { "registry-mirrors": ["https:********.liyuncs.com"] } 重新加载服务配置文件 systemctl daemon-reload 重启Docker systemctl restart docker 查看本地镜像 docker inages 推送镜像到阿里云镜像仓库 docker tag 70517a163731 registry.cn-hangzhou.aliyuncs.com/命名空间/仓库名称:[镜像版本号]docker push registry.cn-hangzhou.aliyuncs.com/命名空间/仓库名称:[镜像版本号] 运行testginger 使我们的 docker images,端口映射前面是容器的端口、后面是项目暴露处的端口,相当于一层代理。 docker run -d -p 8080:5000 testginger此时,使用 nmap your id address 查看服务器端接口占用,8080是开启的。 日志 docker logs [options]docker logs –tail=”10” CONTAINER ID 重新build docker build -t ginger:test . 删除镜像初次部署时,我们可能会创造多个镜像,待成功部署后,我们可以删除多有的无用镜像容器。 docker rmi -f image_id 命令 查看正在运行的镜像容器 docker ps 查看所有存在的镜像容器 docker ps -a 停止镜像容器 docker stop image_id","categories":[{"name":"默认","slug":"默认","permalink":"http://fanghl.top/categories/默认/"}],"tags":[{"name":"默认","slug":"默认","permalink":"http://fanghl.top/tags/默认/"}]},{"title":"服务器部署-Node","slug":"server","date":"2020-06-09T03:28:31.000Z","updated":"2023-11-03T03:29:00.561Z","comments":true,"path":"2020/06/09/server/","link":"","permalink":"http://fanghl.top/2020/06/09/server/","excerpt":"","text":"配置服务器购买的服务器,属于一个空壳子,安装我们需要的Git、node等程序,使用Xshell进行控制。 Git连接仓库 生成秘钥,并将公钥注册在Git仓库即可 cd ~/.ssh 安装MySQL 目录切换至root下, cd ~ 下载与安装mysql, 步骤如下 安装MySQL官方的yum repository: wget -i -c http://dev.mysql.com/get/mysql57-community-release-el7-10.noarch.rpm 下载rpm包 yum -y install mysql57-community-release-el7-10.noarch.rpm 安装MySQL服务 yum -y install mysql-community-server,需要等待一段时间,最后出现 complete! 启动MySQL服务 systemctl start mysqld.service 查看MySQL状态 systemctl status mysqld.service 获取初始密码 grep "password" /var/log/mysqld.log,复制下来,待会修改密码要用到 登录MySQL mysql -u root -p,执行后输入刚刚复制的密码即可登录成功,成功后会展示MySQL的版本信息,这里是5.7.X 执行MySQL语句 set global validate_password_policy=0;set global validate_password_length=1;之后才可以修改密码(这里是5.7版本的修改密码方法) 修改MySQL密码 set password for root@localhost = password('123456'); root是用户名,可自定义。 退出使用新密码重新登录 配置mysql,在etc/目录下,编辑(不存在则新建)my.cnf文件,重启MySQL 配置用户,使得root用户在外网也可以访问到mysql, # mysql -h 127.0.0.1 -u root # mysql>use mysql; # mysql>update user set host = '%' where user ='root'; # mysql>select host, user from user; # mysql>flush privileges; 重启mysql服务 service mysqld restart; mysql异常集合 运行程序mysql报错 (node:6280) UnhandledPromiseRejectionWarning: SequelizeDatabaseError: Illegal mix of collations (latin1_swedish_ci,IMPLICIT) and (utf8mb4_unicode_ci,COERCIBLE) for,原因为sequelize创建的mysql默认字符集是 latin1 而不是 utf8,更改即可。 root用户丢失表现: 输入密码页无法访问mysql.user里面,root用户由于未知原因不在了,重新建立root用户 忽略密码登录- vim etc/my.cnf skip-grant-tables 创建root用户 create user ‘root’@’localhost’ identified by ‘123456’; 此步骤可能会报以下错误: 没报错的跳过(直接到权限那一步),用以下方法解决: ERROR 1290 (HY000): The MySQL server is running with the –skip-grant-tables option so it cannot execute this statement 解决: flush privileges; 再次创建root用户 create user ‘root’@’localhost’ identified by ‘123456’; 由于可能存在root用户,导致会报错,删除用户在创建 drop user ‘root’@’localhost’;create user ‘root’@’localhost’ identified by ‘123456’; 赋予root用户全部权限 GRANT ALL PRIVILEGES ON . TO ‘root’@’%’ IDENTIFIED BY ‘123456’flush privileges; 1396 报错 flush privileges;drop user ‘dl’@’%’;create user ‘dl’@’%’ identified by ‘123’; nmap使用该命令查看服务器开放的端口,查看3306端口是否防火墙中允许访问。 yum -y install nmapnmap 192.168.1.56 telnet检查我们的IP是否可以ping同 例如telnet 192.168.157.129 80 防火墙端口express程序默认运行在 3000 端口,云服务器默认是没有开放 3000 端口的,需要我们手动开启 3000 端口,命令如下: firewall-cmd --zone=public --add-port=3000/tcp --permanent重启防火墙firewall-cmd --reload 如报错FirewallD is not running,则开启防火墙systemctl start firewalld 使用nmap查看现在的端口情况,3000端口已经开启。nmap yourIpAddress pm2进程管理本地服务在我们关闭命令窗口后会停止服务,使用pm2 管理我们的服务,本地调试好之后传到git,Xshell中直接拉取最新代码,进入项目根目录,使用pm2管理进程,即使我们关闭xshell,服务依然还在跑。1、pm2需要全局安装npm install -g pm22、进入项目根目录2.1 启动进程/应用 pm2 start bin/www 或 pm2 start app.js 2.2 重命名进程/应用 pm2 start app.js –name wb123 2.3 添加进程/应用 watch pm2 start bin/www –watch 2.4 结束进程/应用 pm2 stop wwwb 2.5 结束所有进程/应用 pm2 stop all 2.6 删除进程/应用 pm2 delete www 2.7 删除所有进程/应用 pm2 delete all 2.8 列出所有进程/应用 pm2 list 2.9 查看某个进程/应用具体情况 pm2 describe www 2.10 查看进程/应用的资源消耗情况 pm2 monit 2.11 查看pm2的日志 pm2 logs 2.12 若要查看某个进程/应用的日志,使用 pm2 logs www 2.13 重新启动进程/应用 pm2 restart www 2.14 重新启动所有进程/应用 pm2 restart all 2.15 监听修改,自动重启 pm2 start xxx –watch nginx代理安装nginx,链接: https://www.cnblogs.com/shiyuelp/p/11945882.html路径: /usr/local/nginx/sbin配置文件修改:/usr/local/nginx/config注意点: nginx默认在80端口,而服务器默认不开放80端口。需要手动打开80端口。查看nginx是否启动成功: ps aux|grep nginx;检查IP是否可以ping通: telnet 106.13.4.74 80;启动nginx: /usr/local/nginx/sbin/nginx重启nginx: /usr/local/nginx/sbin/nginx -s reopen关闭nginx: /usr/local/nginx/sbin/nginx -s stop 配置文件可以自己修改 域名映射域名映射这块其实没必要单独再买一个域名,如果自己有域名的话,可以用一个二级域名来代替。这里以阿里云的域名为例。在阿里云的域名解析设置中,添加一个记录,主机记录那里就是填写我们二级域名的地方,记录值填写服务器的IP地址,添加完成后即可使用域名访问服务器了。可以直接在浏览器输入刚刚配置二级域名,看看是否有nginx欢迎页即可。 redis","categories":[{"name":"server","slug":"server","permalink":"http://fanghl.top/categories/server/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"Python-flask","slug":"flask","date":"2020-06-05T08:36:31.000Z","updated":"2023-11-03T03:29:00.559Z","comments":true,"path":"2020/06/05/flask/","link":"","permalink":"http://fanghl.top/2020/06/05/flask/","excerpt":"","text":"Base pointpycharm中打开debug模式:在终端中设置: set FLASK_ENV=development (windows)export FLASK_ENV=developm (mac)flask run开启debug会 激活调试器。 激活自动重载。 打开 Flask 应用的调试模式。 创建虚拟环境 python -m venv env 激活虚拟环境(win) $ env\\Scripts\\activate然后在虚拟环境中安装flask等 依赖 生成依赖文件 pip freeze > requirements.txt 安装依赖 pip install -r requirements.txt flask_scriptflaks_script通过命令的方式操作flask,跑起来开发版服务器、设置数据库,定时任务等。 1234567891011121314# manage.pyfrom flask_script import Managerapp = Flask(__name__)manager = Manager(app)def hello(): passmanager.add_command('hello', hello())# 装饰符命令@manager.commanddef hello(): pass 通过自定义方法操作flask,使用方法为: $ python manage.py hello Blueprint蓝图模块,帮助我们对于整体项目的分割,利于后续的管理和拓展。主要分为设置蓝图,注册蓝图,路由使用蓝图三部分。 1234567891011121314151617# @/api/user/controllers.pyfrom flask import Blueprint user = Blueprint('user', __name__)\"\"\"创建用户\"\"\"@user.route('/register', methods= ['get'])def user_register(): pass# @__init__.py def create_app(): app = Falsk(__name__) from om_core.api.user.controllers import user app.register_blueprint(user, url_prefix='/api/users') 至此,URL使用 localhost:5000/api/users/register 就可以访问注册路由。 flask-SQLAlchemy 查询结果一般使用db.session.query()来查询结果,结果返回一个list,多条数据处理需要用到遍历,单挑数据则可以使用一下方式获得值并返回 user = db.session.query(User).filter_by(name=’’liming).all() 数据接收 post json格式 data = json.loads(request.get_data(as_text = True))name = data.get(‘name’) get URL拼接 param = request.args[‘param’] 新增数据并提交数据库 user = User(name=”xxx”, dender=0)db.session.add(user)db.session.commit() 更新数据 user = db.session.query(User).filter(name=param.get(‘name’)).first()user.attr = param.get(‘attr’)db.session.commit() Faker生成大量的模拟数据 pip install Faker 12345678910111213from faker import Fakerfaker = Faker('zh-CN') # 默认美国for item in range(100): fname = faker.name() faddress = faker.address() fint = faker.pyint() user = User(name=fname, address=faddress, xxx=fint) db.session.add(user)try: db.session.commit()except: db.session.rollback() flask_restplus pip install flask-restplus from flask_restplus import Api Flask-RESTPlus提供的主要创建对象就是资源。资源创建于Flask可插入视图(pluggable view)之上,使得我们可以通过在资源上定义方法来很容易地访问多个HTTP方法。 flask_cors解决跨域 from flask_cors import CORS if Config.FLASK_ENV == ‘DEVELOPMENT’: CORS(app, supports_credentials=True) namedtuplenamedtuple是继承自tuple的子类。namedtuple创建一个和tuple类似的对象,而且对象拥有可访问的属性。 用户权限细粒度管理用户权限是一个常见的业务,这里使用scope模块在 token 进行验证时判断用户的接口访问权限,并且自定义的 scope 可以进行权限的合并与筛选,进行普通用户和管理员权限区分。scope 提供两套权限机制,api级别和蓝图级别,粒度粗细可以自己选择,十分灵活。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596# scope.pyclass Scope: allow_api = [] # api 级别的权限控制 粒度细 allow_module = [] # 蓝图级别权限控制 粒度粗 forbidden = [] # 权限筛选 def __add__(self, other): # 管理员合并普通用户权限 # 运算符重载 self.allow_api = self.allow_api + other.allow_api # set 去重 self.allow_api = list(set(self.allow_api)) # 红图级别权限相加 self.allow_module = self.allow_module + other.allow_module self.allow_module = list(set(self.allow_module)) # 逆向筛选权限 self.forbidden = self.forbidden + other.forbidden self.forbidden = list(set(self.forbidden)) return selfclass UserScope(Scope): allow_api = ['v1.user+get_user'] allow_module = [] forbidden = ['v1.user+super_get_user', 'v1.user+super_delete_user'] def __init__(self): self + AdminScope()class AdminScope(Scope): # allow_api = ['v1.user+super_get_user', # 'v1.user+super_delete_user'] allow_module = ['v1.user'] def __init__(self): # 排除 筛选视图函数 # self + UserScope() pass def is_in_scope(scope, endpoint): # scope() # globals # 反射 # token 内部携带权限参数,直接判断权限 # v1.red_name + view_func scope = globals()[scope]() splits = endpoint.split('+') red_name = splits[0] if endpoint in scope.forbidden: return False if endpoint in scope.allow_api: return True if red_name in scope.allow_module: return True else: return False# token_auth.py from app.libs.scope import is_in_scopeauth = HTTPBasicAuth()User = namedtuple('User', ['uid', 'ac_type', 'scope'])@auth.verify_passworddef verify_password(token, password): user_info = verify_auth_token(token) if not user_info: return False else: g.user = user_info return Truedef verify_auth_token(token): s = Serializer(current_app.config['SECRET_KEY']) try: data = s.loads(token) except BadSignature: raise AuthFailed(msg='token is invalid', error_code=1002) except SignatureExpired: raise AuthFailed(msg='token is expired', error_code=1003) uid = data['uid'] ac_type = data['type'] scope = data['scope'] # 视图函数 allow = is_in_scope(scope, request.endpoint) print('验证token参数--', uid, ac_type, scope, request.endpoint) if not allow: # 在这里拦截不同权限用户 raise Forbidden() return User(uid, ac_type, scope)","categories":[{"name":"Python","slug":"Python","permalink":"http://fanghl.top/categories/Python/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"Express","slug":"express","date":"2020-05-22T06:19:11.000Z","updated":"2023-11-03T03:29:00.558Z","comments":true,"path":"2020/05/22/express/","link":"","permalink":"http://fanghl.top/2020/05/22/express/","excerpt":"","text":"express文档 : https://www.expressjs.com.cn/guide/using-middleware.htmlsequelize文档: https://github.com/demopark/sequelize-docs-Zh-CN/blob/master/Readme.md ORM(sequelize)ORM(Object Relational Mapping)对象关系映射,减小操作层的代码量,直接方便的操作数据库。使用前,确保sequelize已经安装 12npm install --save sequelizenpm install --save mysql2 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127/** path: @src/models/message.js*/var Sequelize = require('sequelize')var sequelize = new Sequelize( 'nodesql', //database name 'root', //database user '123456', //database password { 'dialect': 'mysql', 'host': 'localhost', 'port': 3306 })//表模型var Message = sequelize.define('message', { id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true, }, userName: { type: Sequelize.STRING(32), }, content: { type: Sequelize.TEXT }})const User = sequelize.define('user', { id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true, }, userName: { type: Sequelize.STRING(32), }, age: { type: Sequelize.INTEGER }, gender: { type: Sequelize.INTEGER }, address: { type: Sequelize.STRING(32) }})Message.sync(); //创建表User.sunc()module.exports = {Message, User};/** path: @src/route/index.js* desc: 在路由中完成增删改查*/var express = require('express');var router = express.Router();var Message = require('../models/message.js')const setToken = require('../utils/middwares/jwt.js')const {log} = console//REST API//用户登录router.post('/login', async (req, res, next) => { const user = {} let {userName} = req.body let data = await User.findOne({ where: { userName: userName } }) // log('查询结果返回:', JSON.stringify(data, null, 2)) if(!data) { //不存在用户则创建用户 user.userName = userName data = await User.create(user) } setToken.setToken(data.id).then(token => { //返回用户信息及token return res.json({data: {data, token}}) })})//查找某内容router.get('/getOne', (req, res, next) => { if(!req.data) { return res.json({ msg:'token invalid' }) } Message.findAll().then(data => { log('查找数据res', JSON.stringify(data, null, 2)) res.json({ errcode: 0, data }) })})//删除一个用户router.get('/del_user', async (req, res, next) => { let {userName} = req.query let result = await User.findOne({ where: {userName,} }) if(!result) { return res.json({msg: '不存在该用户', errcode: 601}) } let data = await result.destroy() return res.json({msg: '删除成功', errcode: 0})})//更新用户信息router.post('/rich_user_info', async (req, res, next) => { let data = req.body let result = await User.findOne({ where: {userName: data.userName} }) //用数据表的result字段来匹配前端上传的字段!前端随便传参,我只过滤有用的 Object.keys(result.toJSON()).map(item => { data[item] ? result[item] = data[item] : '' }) await result.save() res.json({msg: 'succ', data: result})}) 至此,基本的sequlize就可以跑起来了。 原生SQL方法sequelize的确方便,但他的查询语句较为繁琐,这里我们还可以使用原生mysql语句。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263/*** path: utils/dbConfig.js* 数据库配置*/mysql = { host: 'localhost', user: 'root', password: '123456', database: 'nodesql'}module.exports = mysql;/*** path: @/db.js* 手动连接数据库*/let mysql = require('mysql')let dbConfig = require('./utils/dbConfig')const {log} = consolemodule.exports = { query: function(sql, params, callback) { let conn = mysql.createConnection(dbConfig) conn.connect(function(err) { if(err) { log('数据库连接失败') throw err } conn.query(sql, params, function(err, res, fields) { if(err) { log('数据库操作失败') throw err } callback && callback(res); conn.end(err => { if(err) { log('数据库关闭失败') throw err } }) }) }) }}/*** path: @/route/index.js* 路由使用*/router.get('/user', (req, res, next) => { let {id} = req.query const sql = `select * from user where id = ${id}` //复杂SQL语句 db.query(sql, [], function(result, fields) { let data = JSON.parse(JSON.stringify(result)) data1 = req.requestTime res.json({ status: 0, data, data1 }) })}) ORM和原生SQL语句之间并不冲突,合理选择使用即可。两个一起用也可以 JWT(token验证)jwt(jsonwebtoken)验证,前后端验证的一种方法。express实现jwt验证 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119// 安装 express-jwt$ npm i express-jwt --save /** path: @/util/jwt.js* 封装分发token和验证token函数*/var jwt = require('jsonwebtoken');var signkey = 'secret123456'; //随机串const setToken = function(userid){ return new Promise ((resolve, reject) => { const token = jwt.sign({ _id: userid }, signkey,{ expiresIn:'36h' }); resolve(token); })}const verToken = function (token) { return new Promise((resolve, reject) => { var info = jwt.verify(token.split(' ')[1], signkey); if(info) { resolve(info); } else { reject(info) } })}module.exports = { verToken, setToken,}/*** path: @/routes/index.js* 首先在login接口分发token*/var express = require('express');var router = express.Router();var {Message, User} = require('../models/test.js')const jwt = require('jsonwebtoken')const setToken = require('../utils/middwares/jwt.js')// 登录apirouter.post('/login', async (req, res, next) => { const user = {} let {account, password} = req.body //小程序这里应接受code,去换取openID存储 let data = await User.findOne({ where: { account: account, } }) // console.log('查询结果返回:', JSON.stringify(data, null, 2)) if(!data) { //不存在该用户,则创建用户 user.account = account data = await User.create(user) } setToken.setToken(data.id).then(token => { return res.json({data: {data, token}}) })})//请求内容router.get('/getOne', (req, res, next) => { if(!req.data) { //验证token的中间件成功后,把验证结果放置在req.data中 return res.json({ msg:'token invalid', errcode: 600, }) } Message.findAll().then(result => { res.json({ errcode: 0, result, }) })})/*** path: @/app.js* 配置token验证中间件*/var jwt = require('jsonwebtoken');var verToken = require('./utils/middwares/jwt.js')app.use(function(req, res, next) { var token = req.headers['authorization'] if(!token) { return next(); } else { verToken.verToken(token).then((data) => { req.data = data; return next(); }).catch((error)=>{ return next(); }) //上为封装方法,下为直接调用,都可以使用 // let info = jwt.verify(token.split(' ')[1], 'secret123456'); // req.data = info; // next() }})//过滤不需要token的路由app.use(expressJwt({ secret: 'secret123456' // 签名的密钥 或 PublicKey}).unless({ path: ['/login',] // 指定路径不经过 Token 解析}))//当token失效返回提示信息app.use(function(err, req, res, next) { if (err.status == 401) { return res.status(401).send({msg: 'token invalid'}); }}); 至此,token验证就可以跑起来了。在发送http时,headers中配置 Authorization: 'Bearer ${token}'即可,当然还可以继续再次封装。 middare(中间件)中间件用来处理后端服务,对前端的路由请求进行过滤处理。express本来就是服务加中间件的集合,不同的中间件构成了完整的api逻辑处理。应用级中间件绑定在APP内,路由中间件绑定在路由,除此之外,还有内置中间件,错误处理中间件等。不带有路由限制的中间件是会被所有路由执行的。 12app.use(middareFun) //所有请求都会触发app.use('/user/:id', middareFun) ///user路径请求触发 这里我们优化了上面的 token 验证中间件。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354/*** path: @/utils/middares/paramsVerify.js*/var verToken = require('./jwt.js')//请求时间const requestTime = function(req, res, next) { req.requestTime = Date.now() next()}//解析tokenconst tokenVerify = function(req, res, next) { var token = req.headers['authorization']; if(!token){ return next(); } else { verToken.verToken(token).then((data)=> { req.data = data; return next(); }).catch((error)=>{ return next(); }) }};//这里可以一个一个导出,也可以直接写在数组内,导出数组即可(二选一)。const middArr = [ requestTime = function() {}, tokenVerify = function() {},]module.exports = { requestTime, tokenVerify, middArr,}/*** path: @/app.js* 两种注册方式二选一即可*/let paramsVerify = require('./utils/middwares/paramsVerify.js')//1.注册paramsVerify文件中所有的中间件(单个导出式)let middwareArr = []for(let i=0; i<Object.keys(paramsVerify).length; i++) { let item = paramsVerify[Object.keys(paramsVerify)[i]] middwareArr.push(item) //干嘛不直接导出数组了?!en //app.use(item) }app.use(middwareArr)//2.导出数组式app.use(paramsVerify.middArr) //完事, 至此,中间的剥离优化完整。 封装log在调试中,可以封装一个log用来替代 console.log 123456789101112131415161718192021222324252627282930/*** path: @/utils/log.js**/Date.prototype.Format = function (fmt) {var o = { \"M+\": this.getMonth() + 1, \"d+\": this.getDate(), \"h+\": this.getHours(), \"m+\": this.getMinutes(), \"s+\": this.getSeconds(), \"q+\": Math.floor((this.getMonth() + 3) / 3), \"S\": this.getMilliseconds()};if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + \"\").substr(4 - RegExp.$1.length));for (var k in o) if (new RegExp(\"(\" + k + \")\").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : ((\"00\" + o[k]).substr((\"\" + o[k]).length)));return fmt;}function log() { const show = true //也可以和开发环境挂钩控制日志输出 if (show) { console.log(`[${new Date().Format(\"yyyy-MM-dd hh:mm:ss\")}] `, ...arguments) }}module.exports = log 跨域在搭建完后端api后,需要在前端调试。无论是小程序还是vue的webapp,访问本地连接会出现跨域问题(小程序得在开发工具上关闭域名检测,小程序默认https),如图:此时需要在app.js内加入允许跨域访问 123456789101112131415161718192021222324252627282930313233/** desc: 以下跨域代码未处理 OPTION 形式,在request不携带自定义参数如 token 等headers内的配置时,是可以跑起来的,一旦在request内设置headers内容,请求会被 OPTION 挂起,跨域失败!*///设置允许跨域访问该服务.(此代码为坑)app.all('*', function (req, res, next) { res.header('Access-Control-Allow-Origin', '*'); //Access-Control-Allow-Headers ,可根据浏览器的F12查看,把对应的粘贴在这里就行 res.header('Access-Control-Allow-Headers', 'Content-Type'); res.header('Access-Control-Allow-Methods', '*'); res.header('Content-Type', 'application/json;charset=utf-8'); next();})/** desc: 跨域* desc: 大胆copy* path: @/app.js*/app.use((req, res, next) => { // 设置是否运行客户端设置 withCredentials // 即在不同域名下发出的请求也可以携带 cookie res.header(\"Access-Control-Allow-Credentials\",true) // 第二个参数表示允许跨域的域名,* 代表所有域名 res.header('Access-Control-Allow-Origin', '*') res.header('Access-Control-Allow-Methods', 'GET, PUT, POST, OPTIONS') // 允许的 http 请求的方法 // 允许前台获得的除 Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma 这几张基本响应头之外的响应头 res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With') if (req.method == 'OPTIONS') { res.sendStatus(200) } else { next() }}) 此时,我们的vue前端使用axios携带token就可以请求到服务端接口了 morgan日志日志,记录服务器的操作行为,这里使用 morgan 把日志记录在本地文件保存。 /** * path: @/utils/morgan.js * desc: 封装一个morgan */ var path = require('path'); var fs = require('fs') var morgan = require('morgan'); var FileStreamRotator = require('file-stream-rotator') var logDirectory = path.join(__dirname, 'log') fs.existsSync(logDirectory) || fs.mkdirSync(logDirectory) morgan.token('timeStamp', function(req, res){ let date = new Date() let logHour = date.getHours() < 10 ? `0${date.getHours()}` : date.getHours() let logMinute = date.getMinutes() < 10 ? `0${date.getMinutes()}` : date.getMinutes() let logSecond = date.getSeconds() < 10 ? `0${date.getSeconds()}` : date.getSeconds() let logDate = `${logHour}:${logMinute}:${logSecond}` return logDate }); // 自定义format,其中包含自定义的token morgan.format('joke', '[joke :timeStamp] :remote-addr :remote-user :method :url HTTP/:http-version :status :res[content-length] - :response-time ms'); // create a rotating write stream var accessLogStream = FileStreamRotator.getStream({ date_format: 'YYYYMMDD', filename: path.join(logDirectory, 'access-%DATE%.log'), frequency: 'daily', verbose: false }) module.exports = {morgan, accessLogStream} /* * path: @/app * 引用这个日志系统 */ app.use(morgan('joke')); //服务器输入实时日志 app.use(morgan('short', {stream: accessLogStream})); //记录日志在文件中 至此,我们可以在 /utils 路径下看到创建的 log 文件夹,日志已经根据 file-stream-rotator 插件做了分割,每一天的日志集约在一个文件。以防日志过多导致混乱。 本项目git地址在这里, 欢迎star 本项目部署服务器文档在这里","categories":[{"name":"node","slug":"node","permalink":"http://fanghl.top/categories/node/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"vue-wx-h5","slug":"vue-wx-h5","date":"2019-12-30T02:47:44.000Z","updated":"2023-11-03T03:29:00.561Z","comments":true,"path":"2019/12/30/vue-wx-h5/","link":"","permalink":"http://fanghl.top/2019/12/30/vue-wx-h5/","excerpt":"","text":"微信公众号总结由于微信的约定,在ios端无法购买虚拟产品,安卓则没有限制。为了解决ios用户也可以享受购买虚拟服务的问题,可适用公众号H5支付来解决ios端支付问题 准备由于仅仅是为ios用户解决支付问题,故此H5页面内容很简单,登录、拉取支付列表、支付即可。H5采用vue+jq实现,未适用vue-cli。(就俩页面,没必要)。页面结构如下: 鉴权需求鉴权,是微信提供的H5授权方式,一般采用第三方授权,授权成功获取code,用code获取acces_token、unionID等,由于H5是小程序ios支付的延伸,故此需要unionID来判断用户唯一性!鉴权必不可少。index.html文件中首先导入jsapi 鉴权网页授权与小程序不同,网页是第三方网页授权,然后授权信息在重定向链接中(redirect_uri)返回,重定向链接我设置为index.html页面。在 created() 钩子中,去鉴权获取code 123456789101112131415161718192021222324252627282930313233created: function() { const token = window.sessionStorage.getItem('token') //解决刷新问题 if(token) { this.getCircleList() return } let c = this.getQueryCode('code') this.code = c if(this.code) { this.postData(c) } else { this.getUserCode() } },//鉴权getUserCode() { let redirect_uri = 'http://test.********.cn/projectName/' redirect_uri = encodeURI(redirect_uri) let url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${this.appId}&redirect_uri=${redirect_uri}&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirec` window.location.href = url},//获取鉴权成功后codegetQueryCode(variable) { let query = window.location.search.substring(1) let v = query.split(\"&\") for(let i = 0; i < v.length; i++) { let pair = v[i].split('=') if(pair[0] == variable) { return pair[1] } } return null}, 重点在于 getUserCode 鉴权函数,有人会在鉴权链接后面再加一个参数:&connect_redirect=1 我第一次使用的链接就是加了该参数的,我只替换了APPID和重定向地址,结果一直报错,坑货。第二,重定向链接不能是本地连接,得是外网可以访问的链接。 支付页面中点击单个商品,进行支付购买。具体的支付过程和小程序支付一样,只不过这里多了wx.config 的应用。 1234567891011121314151617181920212223242526272829303132333435wx.config({ debug: false, appId: that.appId, timestamp: timeStamp1, nonceStr, signature, jsApiList: ['chooseWXPay'] })wx.ready(function() { wx.chooseWXPay({ timestamp: timeStamp1, nonceStr, package: package1, signType, paySign, success: function (res) { that.showSuccTip = true that.clickStatus = true that.getCircleList() setTimeout(() => { that.showSuccTip = false }, 1000*3) }, cancel() { that.clickStatus = true }, fail(res) { alert('支付失败,稍候再试') that.clickStatus = true }, }); })wx.error(function(res) { console.log('config error : ', res)}) 页面通信 - ajax没有使用axios,使用ajax通信,封装ajax通信 123456789101112131415161718192021222324httpAjax (obj) { $.ajax({ url: obj.url, data: obj.data && obj.type === \"POST\" ? JSON.stringify(obj.data) : obj.data, type: obj.type ? obj.type : 'GET', contentType: 'application/json', beforeSend: function(xhr) { if (obj.token) { xhr.setRequestHeader('Authorization', '******** ' + obj.token); } }, dataType: 'json', success: function (res) { if (typeof obj.success === 'function') { obj.success(res) } }, error: function (res) { if (typeof obj.error === 'function') { obj.error(res) } } })}, 页面效果这个效果,想到了使用jq解决,vue可能有更简单的方法,但没试过! 123456789101112131415161718data: { clickId: 0, }//列表按钮携带自身idshowDetail(id) { if(id == this.clickId) return $('#'+this.clickId).addClass('contentBox') $('#'+id).removeClass('contentBox') $('#'+id).find(\"[name='bigCircle']\").removeClass('greyLine') $('#'+this.clickId).find(\"[name='bigCircle']\").addClass('greyLine') $('#'+id).find(\"[name='smallCircle']\").addClass('hited') $('#'+this.clickId).find(\"[name='smallCircle']\").removeClass('hited') this.clickId = id}, 采坑杂谈坑真的有点多,尤其是第一次搞得话。各种配置文件、微信公众平台里面的白名单,安全域名配置等等,token的传递坑了好久,跨域,没用vue-cli ,打开页面不能右击打开浏览器预览,使用 anywhere 插件来把路径转化为 http/HTTPS链接,后期测试直接把文件拉倒xshell服务器里面去测,要不Git分支被污染的不忍直视。 ios兼容ios端用户支付成功回调函数里面 alert 并不会被执行,Android则无影响。所以支付成功的提示自己写一个 alert 就可以。 刷新都快上线了,产品进入点了一个刷新,页面卡死报错!原因是鉴权返回的 code 一次性有效!,刷新时,链接其实没变,但是code已经过期了。解决: 页面刷新不影响逻辑,此时我们只需要token即可,故此把token存储在sessionStorage 里面即可避免页面刷新问题。","categories":[{"name":"Vue","slug":"Vue","permalink":"http://fanghl.top/categories/Vue/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"wepy-封装","slug":"wepy-wxminiPro","date":"2019-12-16T06:48:03.000Z","updated":"2023-11-03T03:29:00.561Z","comments":true,"path":"2019/12/16/wepy-wxminiPro/","link":"","permalink":"http://fanghl.top/2019/12/16/wepy-wxminiPro/","excerpt":"","text":"request封装目录结构: 目录: /utils/base.js 123456789101112131415161718192021222324252627282930313233343536373839404142import wepy from 'wepy'import qiniuyun = from '@/utils/qiniuUploader'const dev = falseconst baseUrl = dev ? 'https://xxxx' : 'https://test/xxx'//上传图片: const uploadImg = (imageURL, uptokenURL) { return new Promise((resolve, reject) => { qiniuyun.upload(imageURL, res => { resolve(res) }, error => { reject(error) }, { region: 'ECN', domain: 'https://xxxx', uptoken: uptokenURL }) }) }//请求封装const wxRequest = async (params = {}, url, method,) => { let token = params.token || '' if(params.getToken) { token = wepy,getStorageSync('token') } let res = await wepy.request({ url, method: methos || 'GET', data: params.data || {}, header: Object.assign({ 'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': `str**Token ${token}` }, params.header || {}) }) return res}module.exports = { wxRequest, baseUrl, uploadImg,}; 在 base.js 中封装请求,在api文件中则可以直接拿来使用目录: utils/api.js 12345678910import base from '@/utils/base'//登录、获取基本信息const login = (params) => base.wxRequest(params, `${base.baseUrl}/login/`, 'POST')const getUserInfo = (params) => base.wxRequest(params, `${base.baseUrl}/user/`,)module.exports = { login, getBaseInfo,} 统一管理维护所有的接口api, 在页面中,直接使用具体的api 1234567891011121314151617181920212223242526272829/** src/pages/index.wpy*/import api from '@/utils/api'//获取用户基本信息onShow() { this.getUserInfo(params)}//带参数,同步写法getUserInfo(params) { api.getUserInfo({ data: { data: data1 }, getToken: true }).then(res => { const data = res.data ... this.$apply() })}//不带参数,异步写法async getUserInfo() { let res = await api.getUserInfo({getToken: true}) if(res.statusCode === 200) { //doSth }} Mixins提出公共方法,方便全局调用 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121/**path: scr/mixins/common*name: common*/import wepy from 'wepy'export default class commonMixins extends wepy.mixins { //本地存储 saveData (k, v) { wepy.setStorage({ key: k, value: v }) } saveDataS(k, v){ wepy.setStorageSync(k, v || '') } getDataS(k){ let res = wepy.getStorageSync(k); return res } // 图片预览 preImg(c, u){ wepy.previewImage({ current: c, urls: u }) } // 提示框 toast(title,icon,dura){ wepy.showToast({ title: title, icon: icon, duration: dura || 1500 }) } //页面滚动 pageScro(num){ wepy.pageScrollTo({ scrollTop: num, duration: 0 }); } // 页面跳转 nav(url){ this.$navigate({ url: url }) } swi(url){ wepy.switchTab({ url: url }) } log() { const show = true if (show) { console.log(`[${new Date().Format(\"yyyy-MM-dd hh:mm:ss\")}] `, ...arguments) } } // 发送formid(已废弃) postFormId(id){ let arr = wepy.getStorageSync('form_ids') || []; arr.push(id); wepy.setStorageSync('form_ids', arr) } // showModal modal(data) { wx.showModal({ content: data.content, showCancel: data.cancel || false, confirmText: data.confirm || '知道了', }) } //粘贴板 setClipboardData(data, succFun) { wx.setClipboardData({ data: 'data', success(res) { wx.getClipboardData({ success (res) { succFun } }) } }) } // 页面顶部title setNavTitle(title) { wx.setNavigationBarTitle({ title: title }) } //跳转小程序 navToMini(appId) { wx.navigateToMiniProgram({ appId, }) } //加载框 loading(title) { wx.showLoading({ title: title || \"加载中...\" }) } //隐藏加载框 hideLoading() { wx.hideLoading({}) } //banner跳转 bannerJump(url) { if(url.slogan == 1) { this.swi(url.autoResponse1) return } this.nav(url.autoResponse1) } //返回上一个页面 navBack() { wx.navigateBack({}) }} 使用mixins ,在页面中引入,config中声明,即可在页面使用 this 调用 12345import commonMixin from '@/mixins/common'import req from '@/mixins/req' //页面配置中: mixins = [commonMixin, req] 常量配置数据前端配置数据抽离出来单独放,便于维护。 12345678910111213141516171819202122232425262728/***path: src/utils/configData.js*/const education = [ '高中及以下', '专科', '本科', '硕士', '博士',]//微信号码正则const wxreg=/^[a-zA-Z]{1}[-_a-zA-Z0-9]{5,19}$/;// banner轮播配置const swiperConfig = { autoplay: true, interval: 3500, duration: 500, circular: true, indicatorDdots: true, indicatorColor: 'rgba(255,255,255,1)', indicatorActiveColor: '#FF8356',}module.export = { education, wxreg, swiperConfig,} 分包小程序未超过2M大小,无需分包。超过2M,则采用分包,单包不超过2M,总计不超过16M。 分享普通分享略过。带shareTicket的分享,且需要记录分享群的信息时: 12345678910111213141516171819/**path: src/app.wpy**/async onShow(ops) { const that = this; // 判断是否是群点击进入 console.log('APP show : ', ops) that.scene = ops.scene; if (ops.scene === 1044 && ops.shareTicket !== undefined){ that.shareTicket = ops.shareTicket; }}//页面async onShow() { if(that.$parent.scene === 1044){ await that.touchGroup(); }} 由分享进入小程序某个页面,需先判断缓存中是否存在 token , 若不存在 token , 则先请求登录接口,到后端换取 token , 再请求其他api 。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455//某分享链接进入的页面async onShow() { const that = this if( !wepy.getStorageSync('token')) { await that.getLogin() } that.getUserInfo()}//mixins - reqasync getLogin(){ await wepy.login().then(async (res) => { let result = await this.timeOut(circleApi.login({ data: { app_id: this.$parent.globalData.appId, code: res.code } })); if(res.statusCode === 200){ wepy.setStorage({ key: \"token\", data: res.data.token }) wepy.setStorage({ key: 'user_id', data: res.data.user_id }) } })}//超时处理async timeOut(fn){ let that = this; let res = await Promise.race([this.test(), fn]).then((data) => { return data }); if(res === 'timeOut'){ that.toast('请检查网络或者重新试一下', 'none' ,1500); let status = { statusCode: 300 }; return status }else { return res }}// 请求超时处理test() { let promise = new Promise((resolve, reject) => { setTimeout(() => { resolve('timeOut') },20000) }); return promise;} formid (已废弃)小程序给用户发送模板消息需要消耗 fromID, 新版的则是授权。这里记录一下fromID的处理。发送一条模板消息需耗费一个 fromIDfromID的存储应该是用户本次使用完小程序,然后把收集到的fromID一次性发送到后端 123456789101112131415161718192021222324252627282930313233/**path: src/app.wpy**/onHide(){ let form_ids = wepy.getStorageSync('form_ids'); configApi.postFormId({ data: { form_ids: form_ids }, getToken: true }); wepy.setStorageSync('form_ids', [])}// mixins - common// 存储formid postFormId(id){ let arr = wepy.getStorageSync('form_ids') || []; arr.push(id); wepy.setStorageSync('form_ids', arr) }// 页面表单产生 fromID <form @submit=\"submit\" report-submit=\"true\"> <button class=\"publicBox\" hover-class=\"none\" form-type=\"submit\"> <image class=\"publicImg\" src=\"../images/common/bigButton.png\" ></image> <view class=\"publicText\">发布</view> </button></form>submit(e){ this.postFormId( e.detail.formId )}, 当用户的表单提交行为产生了 fromID 时, 统一进行本地存储,在用户沙雕该小程序时再统一提交全部fromid 小程序登录广播登录广播可以解决很多同步问题,在app内,执行登录获取token,在token还未拿到时,首页的接口不能去执行,需要等待后端返回token后才可执行,这里有两种方式实现:method1: async/awaitapp内会执行登录,首页也onload内判断token是否存在,不存在则重新登录(同步),在登录成功后在执行业务。 123456789101112// APPonLaunch() { this.getLogin() }//index.wpyasync onLoad() { if(!wepy.getStorageSync('token)) { await this.getLogin() } // 执行业务} 以上方法在有大量新用户分享进入小程序的场景下很实用,但是不存在token的用户(新用户)通常会请求两次登录。优化如下:利用广播,广播页面告知APP内登录是否成功,成功后各页面再去执行业务。code address: https://github.com/fanghongliang/Tools/blob/master/broadcast.js 小程序跳转路径携带对象参数Object小程序在跳转页面路径时会把query参数String化,也不能携带对象参数,当然我们可以通过localStorage、globalData来解决参数携带问题。但当不确定的对象参数需要传递到下一个页面时,可以使用对象参数传递,使用encodeURIComponent封装URL即可解决code address: https://github.com/fanghongliang/Tools/blob/master/urlHelper.js 小程序使用第三方UI库当需要使用第三方UI库时,优先考虑第三方库的适配性。这里wepy 版本为 1.7.3 ,使用kai-ui 采坑经历如下:kai-ui: https://www.npmjs.com/package/kai-ui/v/1.2.2npm引入之后要确保项目 /dist/npm 目录下,存在 kai-ui 文件夹,如没有,删除dist目录,重新编译。步骤一:在root目录app文件内,style中引入 @import '../node_modules/kai-ui/src/less/index';步骤二:在页面中直接引入你需要的组件, import loading from 'kai-ui/Loading' components = { loading, } 之后就可以使用 组件了,此时!可能会出现 [Error] TypeError: Cannot read property 'dir' of null 报错! 解决方法: 直接引用,不要在 components 内注册,亲测有效(坑)","categories":[{"name":"wepy框架封装","slug":"wepy框架封装","permalink":"http://fanghl.top/categories/wepy框架封装/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"Emmet","slug":"Emmet","date":"2019-10-11T05:53:19.000Z","updated":"2023-11-03T03:29:00.555Z","comments":true,"path":"2019/10/11/Emmet/","link":"","permalink":"http://fanghl.top/2019/10/11/Emmet/","excerpt":"","text":"Emmet功能快速编辑前端HTML标签,以及编辑器标签自动闭合功能编辑器安装插件:Auto Close TagVscode编辑器中,【设置】中打开 Emmet相关配置 Emmet初始化12! => tabhtml:5 标签id/class/属性12div.test#testidp.test-class{这里是p文本} 嵌套< : 子节点+ : 兄弟节点^ : 父节点 1div.aim-class>div.son-class^div.brother1-class+div.brother2-class 分组() : 分组分组内的标签在层级上视为整体 1div>(div>div>a)+div>p{test text} 隐式标签直接通过 类 或 ID 生成标签可以省略掉div,即输入.item即可生成<div class="item"></div>隐式标签集合: 1234li:用于ul和ol中tr:用于table、tbody、thead和tfoot中td:用于tr中option:用于select和optgroup中 乘法* : 重复指令$ : 自增符号 12div*5ul>li$*3 CSS缩写12w100 => width: 100pxh10p => height: 10% 单位别名列表:p 表示%e 表示 emx 表示 ex 更多参考: https://blog.csdn.net/comphoner/article/details/79670148","categories":[{"name":"html","slug":"html","permalink":"http://fanghl.top/categories/html/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"designMode","slug":"designMode","date":"2019-09-23T06:37:55.000Z","updated":"2023-11-03T03:29:00.557Z","comments":true,"path":"2019/09/23/designMode/","link":"","permalink":"http://fanghl.top/2019/09/23/designMode/","excerpt":"","text":"设计模式五大设计原则","categories":[{"name":"designMode","slug":"designMode","permalink":"http://fanghl.top/categories/designMode/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"Hexo建站","slug":"hexo","date":"2019-09-19T12:43:17.000Z","updated":"2023-11-03T03:29:00.559Z","comments":true,"path":"2019/09/19/hexo/","link":"","permalink":"http://fanghl.top/2019/09/19/hexo/","excerpt":"","text":"序言NEXT主题: http://theme-next.iissnan.com/getting-started.html参考文章: https://blog.csdn.net/sinat_37781304/article/details/82729029next参考: https://www.jianshu.com/p/5e56839ef917步骤: 12345678安装Git安装Node.js安装HexoGitHub创建个人仓库生成SSH添加到GitHub将hexo部署到GitHub设置个人域名发布文章 安装$ npm install hexo-cli -g安装后检查是否安装成功:$ hexo -v成功后进行初始化:$ hexo init myBlog安装组件: 1234$ cd myBlog$ npm install``` 至此,新建完成!目录会存在以下结构: node_modules: 依赖包public:存放生成的页面scaffolds:生成文章的一些模板source:用来存放你的文章themes:主题** _config.yml: 博客的配置文件** 1查看刚刚创建的hexo博客: $ hexo g$ hexo s 123456789打开localhost:4000就可以看到啦#### 创建GitHub仓库创建一个和你用户名相同的仓库,后面加.github.io,只有这样,将来要部署到GitHub page的时候,才会被识别,也就是xxxx.github.io,其中xxx就是你注册GitHub的用户名#### hexo部署到GitHub将hexo和GitHub关联起来,也就是将hexo生成的文章部署到GitHub上,打开站点配置文件 _config.yml,翻到最后,修改为 deploy: type: git repo: https://github.com/YourgithubName/YourgithubName.github.io.git branch: master 1此时需要安装deploy-git ,也就是部署的命令,这样你才能用命令部署到GitHub $ npm install hexo-deployer-git –save 1然后: $ hexo clean$ hexo generate$ hexo deploy $ hexo clean$ hexo d -g 1234567891011121314151617181920212223242526272829303132333435其中 deploy 时会要求输入 username 和 password (git账户密码) 之后,打开 http://yourname.github.io 这个网站就可以看到你的博客了!!#### 发布博客和线上GitHub关联后,新增一篇博客: `$ hexo new post <your blogName>` 编辑好文章发布部署: `$ hexo d -g `清除缓存: `$ hexo clean`#### 本地启Hexo动服务`$ hexo s || hexo serve` 默认端口:4000,修改端口号: `$ hexo serve -p 5000`#### 草稿#### 页面丰富##### 公益404:`$ hexo new page 404` 进入刚才生成的 \\source\\404\\index.md 添加: ```html<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8"> <title>404</title> </head> <body> <script type="text/javascript" src="//qzonestyle.gtimg.cn/qzone/hybrid/app/404/search_children.js" charset="utf-8"></script> </body></html> tags/标签页$ hexo new page tags配置tags页面,进入到刚才生成的 \\source\\tags\\index.md文件,添加type字段: 123title: 标签date: 2019-09-19 19:49:32type: "tags" 在文章中设置对应的标签即可: 1tags: [xxx,xxx,xxx] 分类页面/categories$ hexo new page categories配置categories页面,进入到刚才生成的 \\source\\categories\\index.md文件,添加type字段: 1234title: 分类date: 2019-08-19 15:11:42type: "categories"comments: false 设置每篇博客的 categories:categories: xxx 功能点字数统计、时长主题配置文件 _config.yml 中打开 wordcount 统计功能即可 123456post_wordcount: item_text: true wordcount: true #字数统计 min2read: true #阅读时长 totalcount: true separated_meta: true 配置之后还是没出现字数统计和阅读时长,可能是因为未安装 hexo-wordcount 插件,安装即可:$ npm insatll --save hexo-wordcount重启服务,OK 站内搜索安转插件npm install hexo-generator-searchdb --savehexo站点配置文件_config.yml,任意位置手动添加: 12345search: path: search.xml field: post format: html limit: 10000 修改主题(next)配置文件_config.yml,启用local_search 1234local_search: enable: true trigger: auto top_n_per_article: 1 ok GitHub page 404已经部署好的hexo,在我们修改了git仓库的属性之后(我是切换了仓库的公开和私有属性),再次访问域名就会 page 404,检查了仓库文件和本地hexo配置文件,均未改动。 解决: 在线上git仓库的setting中,选择theme-> Custom domain,在这里重新输入你的域名,保存即可,重新打开域名,页面已经回来了。hexo的域名绑定是双向的! hugo博客框架: go语言编写。多线程编译。速度快","categories":[{"name":"Hexo","slug":"Hexo","permalink":"http://fanghl.top/categories/Hexo/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"Charts","slug":"charts","date":"2019-09-17T02:33:17.000Z","updated":"2023-11-03T03:29:00.557Z","comments":true,"path":"2019/09/17/charts/","link":"","permalink":"http://fanghl.top/2019/09/17/charts/","excerpt":"","text":"序言antv蚂蚁官网: https://antv.alipay.com/zh-cn/index.html G2G2引入 CDN:<script src="https://gw.alipayobjects.com/os/lib/antv/g2/3.4.10/dist/g2.min.js"></script> NPM:$ npm install @antv/g2 --saveimport G2 from '@antv/g2' 本地脚本:<script src="./g2.js"></script> G2封装实战一个G2实例只能创建一个图表,若需要多个图表,可以封装G2 实例: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687/** * G2 * charths函数 * warn:数据格式相同可复用一个函数,否则请重新另创图表函数 */// utils/charts.jsexport function chartInstance( point, chartData, config={} ) { const [axisX, axisValue, axisY] = Object.keys(chartData[0]) const chart = new G2.Chart({ container: point, //point挂载点ID forceFit: config.width ? false : true, //表宽自适应配置 height: config.height || 600, width: config.width || null, //若配置forceFit,则width不生效 }); chart.source(chartData, { axisX: { range: [0, 1], min: 0, max: 100 } }); chart.tooltip({ crosshairs: { type: 'line' } }); chart.axis(axisY, { label: { formatter: function formatter(val) { return val + '¥'; } }, title: { textStyle: { fontSize: 12, // 文本大小 textAlign: 'center', // 文本对齐方式 fill: '#999', // 文本颜色 } }, line: { lineDash: [3, 3] } }); chart.line().position(`${axisX}*${axisY}`).color(`${axisValue}`).shape('smooth'); //平滑曲线图 chart.point().position(`${axisX}*${axisY}`).color(`${axisValue}`).size(4).shape('circle').style({ stroke: '#fff', lineWidth: 1 }); chart.render(); return chart}//showData.vueimport { chartInstance} from '@/utils/charts'data() { return{ chartIncome:'', chartData2: [{}], }},mounted() { this.chartIncome = chartInstance('c1', this.chartData2) //返回值很重要,关乎数据变动 this.chartRegister = chartInstance('c2', this.chartData2) this.chartActive = chartInstance('c3', this.chartData2) },methods: { //切换数据 onChangeIncome(e) { const dateChange = e.target.value switch(dateChange) { case 'a': this.chartIncome.changeData(this.chartData2) break case 'b': this.chartIncome.changeData(this.chartData3) break case 'c': this.chartIncome.changeData(this.chartData) break } },}","categories":[{"name":"Antv-G2","slug":"Antv-G2","permalink":"http://fanghl.top/categories/Antv-G2/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"javascript","slug":"js","date":"2019-09-12T08:25:21.000Z","updated":"2023-11-03T03:29:00.560Z","comments":true,"path":"2019/09/12/js/","link":"","permalink":"http://fanghl.top/2019/09/12/js/","excerpt":"","text":"Array属性:Array.length 方法:Array.from()从类数组对象或者可迭代对象中创建一个新的数组实例 123<!-- 数组去重 -->const arr = [1,3,5,56,3,2,1]const res = Array.from(new Set(arr)) Array.isArray()用来判断某个变量是否是一个数组对象。 12const obj = {'key': 'value'}Array.isArray(obj) // false Array.of()根据一组参数来创建新的数组实例,支持任意的参数数量和类型 Array instance属性:Array.prototype.constructor返回值 Array Array.prototype.length返回值长度 方法:修改器方法:下面的这些方法会改变调用它们的 对象自身 的值 123456789Array.prototype.copyWithin()Array.prototype.fill()Array.prototype.pop()Array.prototype.push()Array.prototype.reverse()Array.prototype.shift()Array.prototype.sort()Array.prototype.splice()Array.prototype.unshift() 访问方法:以下方法不会改变调用它们的对象的值,只会返回一个新的数组或者返回一个其它的期望值。 12345678Array.prototype.concat()Array.prototype.includes()Array.prototype.join()Array.prototype.slice()Array.prototype.toString()Array.prototype.toLocaleString()Array.prototype.indexOf()Array.prototype.lastIndexOf() 迭代方法: 1234567Array.prototype.forEach()Array.prototype.every()Array.prototype.some()Array.prototype.filter()Array.prototype.map()Array.prototype.reduce()Array.prototype.reduceRight() String属性:String.prototype 方法:String.fromCharCode()通过一串 Unicode 创建字符串 String instance属性:String.prototype.constructor返回值 String String.prototype.length字符串长度 方法: 123456789101112131415161718192021222324252627282930String.prototype.charAt()String.prototype.charCodeAt()String.prototype.codePointAt()String.prototype.concat()String.prototype.includes()String.prototype.endsWith()String.prototype.indexOf()String.prototype.lastIndexOf()String.prototype.localeCompare()String.prototype.match()String.prototype.normalize()String.prototype.padEnd()String.prototype.padStart()String.prototype.repeat()String.prototype.replace()String.prototype.search()String.prototype.slice()String.prototype.split()String.prototype.startsWith()String.prototype.substr()String.prototype.substring()String.prototype.toLocaleLowerCase()String.prototype.toLocaleUpperCase()String.prototype.toLowerCase()String.prototype.toUpperCase()String.prototype.toString()String.prototype.trim()String.prototype.trimLeft()String.prototype.trimRight()String.prototype.valueOf() ObjectObject构造函数方法1234567891011121314151617181920Object.assign()Object.create()Object.defineProperty()Object.defineProperties()Object.entries()Object.freeze()Object.getOwnPropertyDescriptor()Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Object.getPrototypeOf()Object.is()Object.isExtensible()Object.isFrozen()Object.isSealed()Object.keys()Object.values()Object.preventExtensions()Object.seal()Object.setPrototypeOf()delete obj.property","categories":[{"name":"Javascript","slug":"Javascript","permalink":"http://fanghl.top/categories/Javascript/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"Git","slug":"Git","date":"2019-09-11T03:43:41.000Z","updated":"2023-11-03T03:29:00.556Z","comments":true,"path":"2019/09/11/Git/","link":"","permalink":"http://fanghl.top/2019/09/11/Git/","excerpt":"","text":"Git实用命令总结总结了部分开发中常用的git操作命令,根据实际业务遇到的情况梳理。包括但不限于分支新建、分支合并、分支合并冲突、dist文件冲突、版本回退等。 命名规范 标记 解释 feat 新功能 fix 修补bug doc 文档 style 格式,不影响代码 refactor 重构,不添加新功能,也非修补bug test 增加测试 chore 构建过程或辅助工程变动 scope 用于说明commit影响的范围 问题解决修改文件大小写Windows对大小写不敏感,git对大小写不敏感,需要修改文件名的大小写,实际修改了git却不会生效的,解决: 1. 复制此文件到其他地方备份 2. 删除项目中的该文件 3. 提交代码 4. 重新添加此文件到项目 5. 提交代码,over git实用命令git stash想要切换分支,本地却已经做了改变。切换会报错,此时可以使用 git stash保存当前的修改,等处理完其他分支事务在回来‘取出保存’的修改即可。 命令 作用说明 git stash 保存当前工作区和暂存区的修改 git stash save ‘注释信息’ 作用同上,加上了注释信息方便区分 git stash list 查看保存列表 git stash pop 恢复最近一次保存并删掉保存列表的记录,只恢复工作区 git stash pop –index 与上面命令的效果一样但是还会恢复暂存区! git stash pop stash@{序号} 恢复保存列表里指定的保存记录,并把恢复的记录从保存列表中删除 git stash apply 恢复最近的保存记录但不会删除保存列表里面对应的记录 git stash drop 删除保存列表里面最近一条保存记录。后面加 stash@{序号}可以删除指定的保存记录 git stash clear 删除保存列表里面所以保存记录(清空保存列表) git stash 分支名 stash@{序号} 修改了文件,此次修该使用了 git stash 保存,然后继续修改了该文件,此时再用 git stash pop 或 git stash apply 恢复之前的保存,可能会出现冲突。此时使用该命令 git stash 分支名 stash@{序号} 会创建一个分支然后在创建的分支上把保存的记录恢复出来,避免冲突。 PS : git stash 保存的修改可以跨分支应用。例如:在 develop分支上做了修改, *git stash保存,切换到 *master 分支,使用 pop 或 apply 拉出来保存,这样就可以把 develop 分支上修改的内容迁移到 master 上,解决冲突可能会遇到。 git撤回文件 命令 作用说明 git reset HEAD 放弃暂存区的修改(已经add,未commit) git checkout – * 放弃本地修改(未commit) git reset –soft HEAD^ 撤销commit 撤销线上仓库的commit适用于错误的push后没有他人再次push 12$ git reset --soft <commitHash>$ git push --force git revert撤销几次之前的commit,又需要保留该commit之后的提交时,需要用到 revert 123$ git log $ git revert commit_hash$ git push git reset 本质即把指针 HEAD 指向某一个 commitgit revert 本质不算是回滚,是反做,反向操作commit。正常情况下,每一次操作文件后会让 Git 时间线往前走一步,revert反向操作某一个commit 记录,并生成一个新的 commit 来反做 配置别名长命令的别名配置 1$ git config --global alias.st status 一个比较实用的别名配置↓ 1git config --global alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit" 每个仓库的Git配置文件都放在.git/config文件中 1$ cat .git/config 12345[branch "master"] remote = originq merge = refs/heads/master[alias] last = log -1 //命令作用区域 配置别名也可以直接修改这个文件↑ 新建分支作业修改bug、添加新功能等,在主分支上开一个新分支进行作业,待作业完成后,再合并回主分支,并删掉新开的分支。 方法一:1234master分支$ git checkout master $ git checkout -b xqcircle origin/xqcircle //创建新分支并关联在远程同名分支上$ git push origin HEAD //把该分支推送到远程,即可以在git仓库看到 方法二:1234$ git branch dev $ git branch -a$ git branch -b branch_name //本地先创建该分支$ git push --set-upstream origin branch_name //本地分支推送到远程同名分支,且本地分支会自动track该分支 拉取分支远程存在分支,本地没有该分支,用以下命令拉下来 1$ git checkout --track origin/branch_name 合并回主分支12$ git checkout master$ git merge branch_name 删除远程分支12$ git branch -r -d origin/branch_name $ git push origin :branch_name 删除本地分支12$ git branch -d branch_name$ git branch -D branch_name 标签tag一个版本上线定义一个版本标签,方便快速回退该版本。 123git tag //列出所有taggit tag -r //查看远端所有分支git tag -l 'v2.0.1' //过滤tag 新建tag1git tag xqCircle-v2.0.0 查看tag,commit号1git show tagName 给某个commit打上tag1git tag -a v1.0.0 commitId -m 'my tag' 推送tag到服务器12git push origin tagName //推送某个具体taggit push origin --tags //推送本地所有tags 切换到某个tag跟分支一样,可以直接切换到某个tag去。这个时候不位于任何分支,处于游离状态 1git checkout xqCircle-v2.0.1 切换到某tag并新建分支1git checkout -b branchName tagName 删除tag12git tag -d xqCircle-v2.0.1 //本地删除git push origin :refs/tags/xqCircle2.0.1 //远端删除 port 22 fail“connect to host github.com port 22: Connection timed out fatal: Could not read from remote repository.”这个报错,优先解决本地是否正确的链接上线上仓库 git remote set-url origin git@yourGitUrlHeregit@yourGitUrlHere 为线上github的仓库访问地主","categories":[{"name":"Git","slug":"Git","permalink":"http://fanghl.top/categories/Git/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"Python与JavaScript对照学习总结点","slug":"learnPython","date":"2019-08-28T08:16:41.000Z","updated":"2023-11-03T03:29:00.560Z","comments":true,"path":"2019/08/28/learnPython/","link":"","permalink":"http://fanghl.top/2019/08/28/learnPython/","excerpt":"","text":"序言Python: 廖雪峰-Python传送门: https://www.liaoxuefeng.com/wiki/1016959663602400命令行打开 .py 文件;Python交互环境,执行Python代码 缩进方式进行代码格式!!4格空格缩进大小写敏感 数据类型: 整数 浮点数 字符串 布尔值 =》 and, or, not 空值 None 列表 字典 变量: 常量,即不能变的变量,指针指向不变 类const,常量一般为变量名全部大写 全局变量:全局变量globalData的定义,在读完了教程之后也没发现关于全局变量的定义,基于js的思想,在js中,全局变量取决于该变量定义的位置,传统web编程中,未添加定义符号(var, let, const)的变量都可被称为”全局变量”,即使是 var 也存在一个变量提升的问题。Python中全局变量一般有两种方式: 声明式关键字 global 定义变量法。可以直接进行全局变量声明。global OLD_URL 模块法模块法和js大同小异,js中没有关键字 global ,但是模块引用也是非常好用的,在中大型项目中,我们把项目中用到的常量单独提取出来放置在js文件中。在通过 import 来导入使用。Python中也是如此。 除法: / 浮点数除法,即便是两个整数相除,结果也是浮点数 // 地板除,两个整数的除法仍然是整数,结果只取整数 10 // 3 =》 3 % 求模取余 字符串编码: ord() 数获取字符的整数表示 chr() 函数把编码转换为对应的字符Python的字符串类型是str,在内存中以Unicode表示Python对bytes类型的数据用带b前缀的单引号或双引号表示 x = b’abc’ encode() 方法可以编码为指定的bytes decode() 方法 把bytes变为str如果bytes中只有一小部分无效的字节,可以传入errors=’ignore’忽略错误的字节: b’\\xe4\\xb8\\xad\\xff’.decode(‘utf-8’, errors=’ignore’) len() 方法计算str的字符数1个中文字符经过UTF-8编码占用3个字节,而1个英文字符只占用1个字节。 坚持 utf-8 编码 文件开头写上: 12#!/usr/bin/env python3# -*- coding: utf-8 -*- 格式化: Python与C一致,都采用 % 实现%运算符就是用来格式化字符串的。在字符串内部,%s表示用字符串替换,%d表示用整数替换'age: %s. nmae: %s' %(25, fhl) format() 格式化,比较麻烦 list 和 tuple : list : 列表。类数组 Arrayclassmates = ['xaioming', 'xiaohua', 'xiaoliu']获取最后一个元素 classmates[len(classmates) - 1] 或者 classmates[-1] list 方法:append(content) 末尾插入insert(index, content) 插入指定位置pop() 删除末尾元素pop(index) 删除指定位置元素 tuple 元组,有序列表一旦初始化,不能更改。没有append、insert方法,其他和list一致因为不能更改,故更为安全,能用tuple代替list就尽量用tuple!t = (1,)t = ('str', 23, ['a']) 条件判断: 123456if age >= 18: print('成年人')elif age >= 1: print('幼儿期')else: print('婴儿期') int() 转化为整数函数 循环 for…in循环 while 循环 range() 生成一个整数序列,再通过list()函数可以转换为list break 退出循环 配合if使用 continue 跳过循环 配合if使用 使用dict和set:dictionary字典,其他语言叫map,使用key-value存储,也就是js的对象。json的话,本质是字符串,也可以类比吧。d = {'name': 'fhl', 'age': 22, }PS: 区别点: js的对象可以使用 . 方法调用,Python目前只能 d[‘age’]或者 d.get(‘age’,-1)获取存储的值删除一个key,可以用 pop(key)dictionary是空间换时间的方法,list时间换空间 set同js中的key同根同源,存储key的集合,不存储value,且key不重复!!!创建set,需要一个list作为输入集合 add(key) 添加元素到set中,重复添加不会有效果 remove(key) 删除元素 set可看做成无序、无重复元素的集合,因此,两个set可以做数学意义上的交集、并集等1234s1 = set([1, 3, 54])s2 = set([2, 3, 4])s1 & s2 # 并 ,单个& s1 | s2 # | 或,单个| 函数自带函数(内置函数): abs() 、max()数据类型转换函数: int() float() str() bool() 参考js的数据转换函数 String() Number() Boolean() 定义函数:关键字: def, 依次写出函数名、括号、括号中的参数和冒号:,然后,在缩进块中编写函数体,函数的返回值用return语句返回类比js: 关键字function ,后面都一致,然后把 冒号 换成 js中的 大括号 即可 12345def my_abs(num): if x >= 10: return x else: return -x 执行函数: 相比于Python,js的函数执行比较简单,定义完函数后,直接用函数名字加一对小括号就可以调用当前函数,但Python貌似得先导入该函数,才可以调用 12from test import my_absmy_abs(-1) pass关键字pass语句啥都不做,就和0一样,用来占位的。让程序可以跑起来。可以理解为斗地主时,你的牌大不过上家的,你就可以大吼一声: pass/过,让单线程的斗地主可以走下去,而不至于卡在这里,让队友喷你 Python函数返回值js函数没有return语句时,返回的是 undefined。Python返回的是 None,在存在返回多个值的情况下。Python返回的是tuple,一个tuple可以被好多个变量接收,具体场景参考ES6的解构赋值,一毛一样。 默认参数没啥好讲的,和ES6函数默认参数一毛一样,默认参数在后,必填参数在前 可变参数参数前面添加 * ,在函数内部,参数numbers接收到的是一个tuple,因此,函数代码完全不变。但是,调用该函数时,可以传入任意个参数,包括0个参数,有点意思奥,即便我们的传参是list或者tuple,也可以在参数前加 * ,使得list或者 tuple 变为可变参数传进去 关键字参数**ky可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple。而关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。关键字参数有什么用?它可以扩展函数的功能。比如,在person函数里,我们保证能接收到name和age这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求 12345678def person(name, age, **kw): if 'city' in kw: # 有city参数 pass if 'job' in kw: # 有job参数 pass print('name:', name, 'age:', age, 'other:', kw) 参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。 总结: *args是可变参数,args接收的是一个tuple; **kw是关键字参数,kw接收的是一个dict。 递归函数:尾递归优化1234def fact(n): if n==1: return 1 return n * fact(n - 1) 以上代码是Python的递归代码,额,可能耦合性较高,函数内部使用了当前函数的名字,这高度耦合,在js里面我们可以通过 arguments.callee解决调用自身的问题,减小耦合。以下是js代码: 123456function fact(n) { if( n == 1 ) { return 1 } return n * arguments.callee(n-1)} 高级特性:切片 slice操作符slice切片操作符,简单来说,就是给js的slice()函数做了一个语法糖,其他都一致 12L[0:5] #[start:end]但不包括end,L为list或tupleL[-2:-1] #倒数第一个元素索引为-1 所有数,每5个取一个 123456L = list(range(100))L[:10] # 前十个数L[:-10] # 后十个数 L[:10:2] # 前10个数,每两个取一个L[::5] # 所有数,每5个取一个L[:] # 赋值list 切片也可以对字符串使用,不需要单独的类似substring()方法 迭代定义: 循环遍历list或者tuple,叫做迭代。迭代通过 for…in…来完成Python可以迭代一切可迭代的东西判断一个对象是否是可迭代对象呢? 12from collections import Iterableisinstance('abc', Iterable) # 判断str是否是可迭代的对象,返回Boolean 列表生成式就是简化了复杂列表生成的繁琐步骤 1234567L = [] # 实现一个 1*1, 2*2,....10*10的列表for x in range(1, 11): # 传统方法 L.append( x * x )[x*x for x in range(1, 10)] # 列表生成器[x*x for x in range(1, 10) if x % 2 == 0 ] # 还可以加上 if 判断[m+n for m in 'abc' for n in 'xyz'] # 双层循环 接下来,这行代码可能会让jser稍微羡慕一下,那就是操作文件 12import os # 拿到了当前目录的所有文件夹[d for d in os.listdir('.')] js不能操作文件的,当然表亲 node.js是可以的dict的items() 方法可以同时迭代key和value,那么: 12d = {'x': 'A', 'y': 'B', 'z': 'C'}[k + '=' + v for k, v in d.items() ] 12d = {'Hello', 'WorLD'} # 把list中所有的字符串小写[s.lower() for s in d] 生成器列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生generator 列表生成式的 [] 改成 () 即可创建生成器12L = [x * x for x in range(10) ] #列表生成式g = (x * x for x in range(10) ) # g是一个generator、next()方法打印值 generator保存的是算法,每次调用next(g),就计算出g的下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出StopIteration的错误一般,generator用for来循环,不用next() 12for n in g: print(n) 如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator:1234567def fib(max): n, a, b = 0, 0, 1 while n < max: yield b a, b = b, a + b n = n + 1 return 'done' PS::: enerator和函数的执行流程不一样。函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行用for循环调用generator时,发现拿不到generator的return语句的返回值。如果想要拿到返回值,必须捕获StopIteration错误,返回值包含在StopIteration的value中: 1234567g = fib()while True: try: x = next(g) except StopIteration as e: print('Generator return value:', e.value) break 迭代器可以直接作用于for循环的数据类型有以下几种: 一类是集合数据类型,如list、tuple、dict、set、str等; 一类是generator,包括生成器和带yield的generator function。 这些可以直接作用于for循环的对象统称为可迭代对象:Iterable。 可以使用isinstance()*判断一个对象是否是 *Iterable 对象 而生成器不但可以作用于for循环,还可以被next()函数不断调用并返回下一个值,直到最后抛出StopIteration错误表示无法继续返回下一个值了。 可以被next()函数调用并不断返回下一个值的对象称为迭代器:Iterator。 可以使用 isinstance() 判断一个对象是否是 Iterator 对象: 1isinstance([], Iterator) 生成器都是Iterator对象,但list、dict、str虽然是Iterable,却不是Iterator。 把list、dict、str等Iterable变成 Iterator 可以使用 iter() 函数: 1isinstance(iter([]), Iterator) # True PS: Python的for循环本质上就是通过不断调用next()函数实现的 函数式编程高阶函数函数名是指向函数的变量(同js ),即函数本身可以被变量指着,在变量引用也是可以的一个函数接受另一个函数作为参数,这种函数称为高阶函数(同js) map/reducemap 和 js 的功能一致,即都是为 Iterable 的全部元素应用一种规则。这个规则一般是一个函数。不过语法上稍有不同,js的map是Array的一个方法。 python: map()函数接收两个参数,一个是函数,一个是Iterable,map将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。 12345def f(x): return x*xr = map(f, [1, 2, 3, 4]) # 返回的是list每项的平方list(map(str, [1, 2, 32, 56]) # 把每一项变为字符串 123456789//js实现const arr = [1, 2, 3, 4]const r = arr.map(function(item) { return item*item})// ES6const r = arr.map(item => { return iten*item}) 为什么js的map就只是Array的一个方法呢?我个人觉得。js的数据结构并没有Python那么灵活,因为js的for循环只能循环Array和Object,而反观Python就比较多了,本质来说,就是 Iterable 数据结构js只有Array和Object。而Python有很多,除了list和tuple,还有string也算,等等 reducereduce把一个函数作用在一个序列[x1, x2, x3, …]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算,其效果就是: 1reduce(f,[x1, x2, x3]) = f(f(f(x1, x2,x3))) #三个f关系?其实这就说明了reduce()这个函数的作用了。 比如说序列求和: 1234from functools import reduce def add(x, y): return x + yreduce(add, [1, 3, 5, 7, 9]) #25 filter()和map()类似,filter()也接收一个函数和一个序列。和map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素。 例如,在一个list中,删掉偶数,只保留奇数,可以这么写: 12def is_odd(n): return n % 2 == 1 js中的filter同样是Array的一个方法,用于过滤数组并返回一个新的数组 1234const arr = [5, 16, 35, 15, 48]const r = arr.filter(item => { //返回r是一个大于18的数组 item >= 18}) 相比于js来说。Python的filter和map类似,都可以作用于Iterable数据类型 sorted()排序内置函数,用法和js的sort()类似。但是js的sort()方法在未传参数的情况下,默认按照字符编码的顺序进行排序。12L = [1, 5, 15, 25, 8]r = sorted(L) #1,5,8,15,25 js的sort(): 12345let arr = [1, 5, 15, 25, 8]arr.sort() // 1,15,25,5,8arr.sort(function(a, b){ //1,5,8,15,25 return a - b}) 看下sorted()的强大:可以传入三个参数,第一就是排序的list,第二个是key的规则,第三个是反转倒叙: 1sorted(['bov','lv','hln', 'Zomp'], key = str.lower, reverse = True) PS : sorted()也是一个高阶函数。用sorted()排序的关键在于实现一个映射函数 返回函数简单看了下返回函数的定义,里面提到了闭包,这个js里面也经常提到的操作。简单说,闭包就是函数嵌套函数,内部的函数保存了外层函数的变量等参数,在外部函数被销毁后,内部函数依旧可以拿到外部函数的传参。这个概念js和Python没大的区别。深层次理解的话,参考另外一篇博客: https://www.cnblogs.com/fanghl/p/11417906.html 匿名函数关键字: lambda只能有一个表达式,不用写return,返回值就是表达式的结果 1234list(map(lambda x: x * x, [1, 3, 5, 7] ))#lambda x: 相当于:def f(x): return x * x python的匿名函数和js的匿名函数不太一样,但作用大都相似,不用担心函数名冲突等等,简化写法等。js里面的匿名函数已经升级到了ES6箭头函数模式,简单方便: 123456item => { return item * item}(x, y) => { return x + y} js里面匿名函数用的较少,一般都是用了箭头函数替代了。匿名函数的使用场景我也想不出多少,但在定时器中使用较多: 1234567891011function test() { setTimeout(function() { console.log(1) }, 1000 * 2)}// 不过一般都箭头简化了function test() { setTimeout(() => { console.log(1) }, 1000 * 2)} 装饰器代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)函数也是一个对象,而且函数对象可以被赋值给变量,所以,通过变量也能调用该函数name 函数对象的该属性可以拿到函数的定义时名字没看懂,等懂了再回来写 偏函数个人理解又是一个语法糖!减少一些函数的繁杂写法关键模块:functools partial 123import functoolsint2 = functolls.partial(int, base = 2)# 创造一个偏函数int2,来进行2进制的转化,base是int内置函数固有的参数 模块模块是一组Python代码的集合,可以使用其他模块,也可以被其他模块使用创建自己的模块时,要注意:模块名要遵循Python变量命名规范,不要使用中文、特殊字符;模块名不要和系统模块名冲突,最好先查看系统是否已存在该模块,检查方法是在Python交互环境执行import abc,若成功则说明系统存在此模块 作用域__xxx__ 变量是特殊变量,可以被直接引用,但是有特殊用途_xxx或__xxx这样的函数或者变量是非公开的(private),不应该被直接引用 第三方模块安装 pip:安装第三方模块工具安装命令:pip install xxx 参照 npm 或者 yarn 包管理工具 安装常用模块 在使用Python时,我们经常需要用到很多第三方库,例如,上面提到的Pillow,以及MySQL驱动程序,Web框架Flask,科学计算Numpy等。用pip一个一个安装费时费力,还需要考虑兼容性。我们推荐直接使用Anaconda,这是一个基于Python的数据处理和科学计算平台,它已经内置了许多非常有用的第三方库,我们装上Anaconda,就相当于把数十个第三方模块自动安装好了,非常简单易用。Anaconda官网: https://www.anaconda.com/download/国内镜像: https://pan.baidu.com/s/1kU5OCOB#list/path=%2Fpub%2Fpython 面向对象编程 OOP面向过程处理学生的成绩表,为了表示一个学生的成绩,面向过程的程序可以用一个dict表示 12std1 = {'name': 'bob', 'score': 95}std1 = {'name': 'ming', 'score': 59} 处理学生成绩通过函数实现,打印学生成绩: 12def print_score(std): print('%s: %s' % (std['name'], std['score'])) 面向过程,顾名思义,关心的是程序下一步怎么走?这个过程如何保持正确的走法。而面向对象,即万物皆对象,我们要考虑学生这个对象,然后直接创建这个对象,需要什么功能直接调用这个对象上面的方法即可,不用管过程! 面向对象1234567891011121314class Student(object): def __init__(self, name, score): self.name = name self.score = score def print_score(self): print('%s: %s' % (self.name, self.score))xiaoming = Student('xiaoming', 95)xiaohong = Student('xiaohong', 59)xiaoming.print_score()xiaohong.print_score() 奥,语言都是相通的,JavaScript的class和Python思路都是一毛一样啊,不过js中class还有继承、super()、constructor()等等,Python应该也有的,往下继续学习。PS : js中定义的Class,创建实例需要 new 关键字 类和实例 创建类class + className + (继承自某个类) 12class Students(object): def __init__(self, xxx, xx1, xx2): 1234567891011121314class Student extends Person{ //js实现 super() //继承基类的属性 constructor(name, age) { //自己的属性 this.speed = 30 this.name = name this.age = age } otherMethods() { //挂载到Student的原型链上 doSth... }, otherMethods1() { //挂载到Student的原型链上 doOther... }} 创建实例12# pyxiaoming = Student(arg) 12// jsconst xioaming = new Student(arg) 看到这,终于深刻体会到了为啥class一定要首字母大写!js里面可能体会不深,因为有 new 关键字在class之前,而py里面,如果不区分,那么很容易搞混class 和 function 特殊方法__init__ 方法的第一个参数永远是self,表示创建的实例本身,因此,在init方法内部,就可以把各种属性绑定到self,因为self就指向创建的实例本身。diff:和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self,并且,调用时,不用传递该参数。 访问限制外部代码还是可以自由地修改一个实例的name、score属性: 12xiaoming = Student('xiaoming', 95)xioaming.score = 12 #可以修改实例的属性 大招来了!!!如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__这个大招js里面可没有啊在Python中,实例的变量名如果以__开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问 1234class Student(object): __init__(self, name, age): self.__name = name self__age = age 这样在外部就无法访问到内部的私有变量了,只能调用内部方法来访问。·····~真的是这样的吗?(手动滑稽.jpg),当然不是啦,之所有我们无法从外部访问到name,是因为Python解释器把该变量变成了 _Studentname ,所以呢,我们可以通过 短线 加 类名 加变量名继续来访问该私有变量吃饱了撑着了吗?哈哈 继承和多态有点C++的感觉了,毕竟js是没有显示的多态的~ 等等,我理解完了,凉凉打脸。Python的多态指的是基类和子类拥有同样的方法时,子类覆盖基类………………emmmm,按照js来说,这就是原型链的查找基本原理啊,先在自己内部找,找不到了就顺着原型链往上查找,一毛一样的……….把py中的class理解为一种数据结构,这个数据结构和py自带的list,tuple,dict 没有任何区别。那么 isinstance() 不就可以用了 12345678xiaoming = Student('xiaoming')isinstance(xiaoming, Student) # True isinstance可以理解为派生class Pupil(Student): passxiaoxioa = Pupil()isinstance(xiaoxiao, Student) # True 隔代的也算派生哦 鸭子模型 动态语言的鸭子类型特点决定了继承不像静态语言那样是必须的。 获取对象信息 type()判断对象类型,js中使用 typeof() , js判断字符串还可以更为准确的使用Object.prototype.toString().call()题外话了,js的type判断基本类型好用,其他就还是用 instanceof()这点上,JS和py还是高度相似的!!type() ===== typeof() ===== 基本类型isinstance() ==== instanceof() ===== 判断对象 dir()要获得一个对象的所有属性和方法,可以使用dir()函数,它返回一个包含字符串的list,比如,获得一个str对象的所有属性和方法仅仅把属性和方法列出来是不够的,配合getattr()、setattr()以及hasattr(),我们可以直接操作一个对象的状态, js中同理。setAttribute()、getAttribute()、hasAttribute() 实例属性和类属性总结一句话,就是class的类属性各个实例都会访问到,实例的实例属性各自相互独立。可以把这两个概念理解为 JS 基类中的方法,子类都会顺着原型链找到并访问到。 12class Student(object): school = 'hantaiMiddleSchool' #所有实例都可以访问到 面向对象高级编程__slots__slots的作用就是动态给class添加属性的一个约束,否则在class类建立完毕后,运行代码的时候动态随意绑定属性不就乱套了,需要一个约束,职责就是 slotsslots 英文: 插槽。在Vue中使用的广泛,可以理解为在这预先给你留了个位置,以后想用的时候可以用,没有留这个位置的话,以后相用都用不了,可以理解为图书馆同学帮你占座 1234567#pythonclass Student(object): __slots__ = ('name', 'age')>>> s = Student()>>> s.name = 'xiaoming' # 可以绑定成功>>> s.score = 98 # 会报错,“唉,这位置有人了,你坐不了(手动滑稽.jpg)” PS : 使用slots要注意,slots定义的属性仅对当前类实例起作用,对继承的子类是不起作用 @property解决问题前言:给Class绑定属性时,直接把属性暴露出去,写起来简单,调用起来简单,但是没办法检查参数,导致可以随便修改值。这不合理解决1: 123456789101112131415#Python#Python设置set、get方法来控制属性解决验证问题class Student(onject): def get_score(self): return self.score def set_scsoe(self, value): if not isinstance(value, int): raise ValueError('score must be int') if value < 0 or value > 100: raise ValueError('score must be 0-100') self.score = value 但是,上面的调用方法又略显复杂,没有直接用属性这么直接简单有没有既能检查参数,又可以用类似属性这样简单的方式来访问类的变量呢?对于追求完美的Python程序员来说,这是必须要做到的!还记得装饰器(decorator)可以给函数动态加上功能吗?对于类的方法,装饰器一样起作用。Python内置的@property装饰器就是负责把一个方法变成属性调用的:总结起来:语法糖,本质没变,但是简化了调用的繁琐程度 12345678910111213141516171819class Student(object): @property def score(self): return self.score @score.setter # 可读可写属性 def score(self, valule): if not isinstance(value, int): raise ValueError('score must be int') if value < 0 or value > 100: raise ValueError('score must be 0-100') self.score = value @property #只读属性 def age(self): return 23#这样,就依旧可以使用属性的.调用方法访问属性、设置属性值了 总结:@property广泛应用在类的定义中,可以让调用者写出简短的代码,同时保证对参数进行必要的检查,这样,程序运行时就减少了出错的可能性 多重继承关键字:一个子类可以拥有多个基类、mixinclass需要新的功能,只需要在继承一个基类就可以了,通常,主线都是单一继承下来的 * 为了更好地看出继承关系,我们把主线之外需要继承的基类命名为 Mixin,这样的的设计通常称为 *Mixin 12345678class Person(object): passclass ChineseMixin(Person): passclass ChinaPuple(Person, ChineseMixin): pass 定制类重点:前后双下划线的变量是特殊变量,py有特殊用途的!!__slots____len__()__str____repr____iter____getitem____setitem____getattr____call__上面罗列的方法未查看相关作用,以后需要用到在查看不迟,就最后一个 __call__ ,在js 中调用自身的有 arguments.callee() 枚举类…持续更新…….","categories":[{"name":"Python","slug":"Python","permalink":"http://fanghl.top/categories/Python/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"小程序项目自我总结","slug":"summary201907","date":"2019-08-22T08:16:41.000Z","updated":"2023-11-03T03:29:00.561Z","comments":true,"path":"2019/08/22/summary201907/","link":"","permalink":"http://fanghl.top/2019/08/22/summary201907/","excerpt":"","text":"序言小程序: 相亲小红圈+tool: wepy 传送门:[ wepy ] https://tencent.github.io/wepy/document.html#/[ Vue ] https://cn.vuejs.org/[ ES6 ] http://es6.ruanyifeng.com/[ git ] https://www.liaoxuefeng.com/ 七月份结束,项目上线,回过头来整理一下项目,项目为wepy1.7.0后版本开发的小程序,配套的后台前端使用ant-design-vue开发,上传测试服工具使用Xshell6。 内容 wepy构建工程具体可在wepy官网中查看,此处不多介绍。 1234$ wepy init standard my-project /**创建项目*/$ cd my-project /*进入项目目录*/$ npm install /**安装依赖*/$ wepy build --watch /*运行工程并监控项目修改自动刷新*/ wepy属于类Vue写法,要在wepy中使用异步操作(async/await)需要在工程的app.way入口文件中constructor函数中注册: 12345constructor () { super() this.use('requestfix') this.use('promisify') /*←手动添加这个*/} 生命周期:应用生命周期 属性 type 描述 触发时机 onLaunch Function 生命周期函数–监听小程序初始化 用户首次打开小程序,触发 onLaunch(全局只触发一次) onShow Function 生命周期函数–监听小程序显示 当小程序启动,或从后台进入前台显示,会触发 onShow onHide Function 生命周期函数–监听小程序隐藏 当小程序从前台进入后台,会触发 onHide 页面生命周期 属性 type 描述 触发时机 onLoad Function 监听页面加载,一个页面只会调用一次 小程序注册完成后,加载页面,触发onLoad方法,参数可以获取wx.navigateTo和wx.redirectTo及中的 query参数 onReady Function 监听页面初次渲染完成,代表页面已经准备妥当,可以和视图层进行交互 首次显示页面,会触发onReady方法,渲染页面元素和样式,一个页面只会调用一次 onShow Function 监听页面显示,当redirectTo或navigateBack的时候调用 当小程序有后台进入到前台运行或重新进入页面时,触发onShow方法。 onHide Function 监听页面隐藏,当navigateTo或底部tab切换时调用 当小程序后台运行或跳转到其他页面时,触发onHide方法 onUnload Function 监听页面卸载 当使用重定向方法wx.redirectTo(OBJECT)或关闭当前页返回上一页wx.navigateBack(),触发onUnload。 版本更新版本更新代码,一般较为固定,直接复制在 onLaunch 生命周期内 123456789101112131415161718192021222324if(wx.canIUse('getUpdateManager')){ const updateManager = wx.getUpdateManager(); updateManager.onCheckForUpdate((res) => { if(res.hasUpdate){ updateManager.onUpdateReady((res) => { wx.showModal({ title: '更新提示', content: '新版本已经准备好,是否重启应用?', success: function (res) { if(res.confirm){ updateManager.applyUpdate() } } }) }) updateManager.onUpdateFailed(function () { wx.showModal({ title: '已经有新版本了哟~', content: '新版本已经上线啦~,请您删除当前小程序,重新搜索打开哟~' }) }) } })} 组件组件复用说明: 组件复用,在 同一个页面,一个组件多次复用且每次传入不同的数据源,但是表现出来的数据源全部一致,其他的数据并没有渲染上原因: 组件名一致导致wepy认为是一模一样的组件,如此数据源也不会变动解决: 组件异名化 123456import CompA from 'path'components = { CompB: CompA, CompC: CompA,} 这样,既可以重复运用组件 文字换行小程序文字换行: 1<text>第一行\\n第二行\\n第三行\\n</text> 定时器 页面业务逻辑有需要用到倒计时功能,如下图。 在页面中有需要用到倒计时或者其他定时器任务时,新建的定时器在卸载页面时一定要清除掉,有时候页面可能不止一个定时器需求,在卸载页面(onUnload钩子函数)的时候一定要清除掉当前不用的定时器定时器用来做倒计时效果也不错,初始时间后台获取,前端处理,后台直接在数据库查询拿到的标准时间(数据库原始时间,T分割),前端需要正则处理一下这个时间: 123456let overTimeStr = data.over_time.split('T')let time1 = overTimeStr[0].replace(/-/g,\",\")let time2 = overTimeStr[1].replace(/:/g,',')let overTime = time1+ ',' + time2let overTimeArr = overTime.split(',')this.countDownCtrl( overTimeArr, 0 ); 最终把时间分割为[年,月, 日, 时, 分, 秒]的数组,(如果后端已经把时间处理过了那就更好了),然后把该数组传递给倒计时函数 123456789101112131415161718192021222324252627282930313233countDownCtrl( time, group ) { let deadline = new Date()//免费截止时间,月的下从0开始 deadline.setFullYear(time[0], time[1]-1, time[2]) deadline.setHours(time[3], time[4], time[5]) let curTimeJudge = new Date().getTime() let timeJudge = deadline.getTime()-curTimeJudge let remainTimeJudge = parseInt(timeJudge/1000) if( remainTimeJudge < 0) { log('倒计时已经过期') return; } this.interva1 = setInterval(() => { let curTime = new Date().getTime() let time = deadline.getTime()-curTime //剩余毫秒数 let remainTime = parseInt(time/1000) //总的剩余时间,以秒计 let day = parseInt( remainTime/(24*3600) )//剩余天 let hour = parseInt( (remainTime-day*24*3600)/3600 )//剩余小时 let minute = parseInt((remainTime-day*24*3600-hour*3600)/60)//剩余分钟 let sec = parseInt(remainTime%60)//剩余秒 hour = hour < 10 ? '0' + hour : hour; minute = minute < 10 ? '0' + minute : minute sec = sec < 10 ? '0' + sec : sec let countDownText = hour+ \":\" +minute+ \":\" +sec if( group === 0) { //个人业务逻辑,因为一个页面有两个倒计时需求,代码复用区分 this.countDown = countDownText; } else if( group === 1 ) { this.countDownGroup = countDownText } this.$apply() }, 1000 ); } 至此,倒计时效果处理完毕,PS:终止时间一定要大于currentDate,否则显示会出现异常(包括但不限于倒计时闪烁、乱码等) 最后,退出该页面去其他页面时,一定要在页码卸载钩子中清除倒计时!!! 123onUnload() { clearInterval(this.interva1);} 组件传值组件传值和Vue有点细微区别,Vue强调父组件的数组和对象不要直接传到子组件使用,应为子组件可能会修改这个data,如图: 但是,wepy中,有时候确实需要把一个对象传递到子组件使用,单个传递对象属性过于繁琐,而且!!!如果单个传递对象的属性到子组件,如果该属性是一个数组,则子组件永远会接收到 undefined 。此时最好用整个对象传值替代单个对象属性逐个传值的方法,且一定要在传值时加入 .sync 修饰符,双向传值绑定。确保从接口拿到的数据也能实时传递到子组件,而非 undefined :circleMembersList.sync="circleMembersList" 阻止组件的点击事件传播解决: 添加函数 catchtap=”funcName” 即可,funcName可为空函数,也可以直接不写 token判断 小程序调试时,有时候会出现首次打开无内容(拿不到数据)的状态,需要“杀死”小程序再打开才能看到数据内容,其中可能的原因之一便是 token 的失效。在与后台交互的时候,token必不可少。尤其是在小程序分享出去的链接,由其他用户点开分享链接进入小程序内部,此时更是要判断token,token的判断一般选在 onShow()钩子执行而不在 onLoad()钩子内执行。若不存在token,则应该执行登录去拿取token,再进行业务逻辑 1234567891011onShow() { const that = this; if( !wepy.getStorageSync('token') ) { wepy.login().then(async (res) => { if(res.code) { let code = res.code; await that.login(code) } }); } } formid 微信提供了服务通知,即在你支付、快递等行为时,微信会直接给你发一个服务通知(模板消息)来提醒,每次提醒都会消耗该用户存储的formID,formID为消耗品,用一个少一个,只有通过用户的表单提交行为才可以积攒formID 12345678<form @submit=\"submitForm\" report-submit=\"true\"> <button form-type=\"submit\" class=\"editCard\" @tap = \"goModifiPage('editFormTab')\">修改</button></form>//js方法submitForm(e) { this.postFormId( e.detail.formId ) // 向后端传输formid} 支付 准备: crypto-js.js && md5.js 微信支付流程为: 前端点击支付按钮拉起支付 ==》 准备加密数据 ==》 调用后端接口,传入需要的加密数据 ==》 后端验证加密数据,再返回加密数据 ==》 前端拿到后端加密数据(时间戳、内容、签名),对时间戳和内容进行本地签名,再判断本地签名和后端签名是否一致,若不一致,直接返回,退出支付,支付失败!若一致,对刚刚后台返回的content(内容)进行解析,拿到所需订单数据,前端拉起微信支付,参数传入刚刚解析数据 ===》 得到支付结果 success or fail !结束 12345678/** * 签名函数 Sign */function sign(timestamp, content) { var raw = timestamp + salt + content var hash = CryptoJS.SHA256(raw).toString() return CryptoJS.MD5(hash).toString()} 前端点击支付按钮: 123456789101112131415161718192021222324252627// 单独支付接口alonePay(arg) { const that = this; if( that.buttonClicked === false ) return; //防止重复多次拉起支付 that.buttonClicked = false; let mode = 1; //业务需求,我有五种不同模式支付 let appId = this.$parent.globalData.appId; let content; let sign; const timeStamp = new Date().Format(\"yyyy-MM-dd hh:mm:ss\").toString(); let code = wepy.getStorageSync('code'); wepy.login().then((res) => { //获取最新的code,可能这里没必要,具体和后端商量 if(res.code) { let code = res.code; log('code', code) wepy.setStorage({ key: \"code\", data: code }) } }).then( res => { content = `mode=${mode}&app_id=${appId}` sign = Sign.sign(timeStamp,content); }).then(res => { that.goCirclePay( that.circle_id, timeStamp, sign, content, mode ) })}, 支付函数: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879// 支付函数,我前端有五种支付情况(单独、自己发起拼团、拼别人团、ios、免费五种支付),所以单独抽出支付,每次调用支付函数goCirclePay( circle_id, timestamp, sign, content, mode) { const that = this; circleApi.goCirclePay({ data: { circle_id, timestamp, sign, content }, getToken: true }).then( res => { log('支付res:', res) let data = res.data const SignServer = data.sign const timeStampServer = data.timestamp let contentServer = data.content const SignLocal = Sign.sign(timeStampServer,contentServer); if( mode === 0 && data.status === \"success\") { that.nav('/pages/circleDetail?circle_id=' + that.circle_id) return; } if( SignLocal !== SignServer ) { log('签名不一致!') wx.showToast({ title: \"您已经支付过了\", duration: 1500, image: \"../images/common/icon_wxchat.png\", }) return } let contentArr = contentServer.split('&') const timeStamp = contentArr[0].split('=')[1]; const nonceStr = contentArr[1].split('=')[1]; let index = contentArr[2].indexOf(\"=\"); const package1 = contentArr[2].slice(index+1) const signType = contentArr[3].split('=')[1]; const paySign = contentArr[4].split('=')[1]; wepy.requestPayment({ timeStamp, nonceStr, package: package1, signType, paySign }).then(res => { return new Promise(resolve => { setTimeout(() => { resolve() }, 1000) }) }).then(res => { //支付后promise,这里有成功和fail两种,fail在catch捕获,这里直接开始写支付success后的业务代码 that.buttonClicked = true; let groupFormIdGet; circleApi.getGroupFormId({ ////获取getGroupFormId data: { circle_id: that.circle_id }, getToken: true }).then( res => { // let data = res.data that.group_form_id = data.group_form_id groupFormIdGet = data.group_form_id if( mode === 1) { that.nav(`/pages/paySuccess?circle_id=${that.circle_id}&shareLink=${that.shareLink}`) } else if( mode === 2) { that.nav(`/pages/paySuccess?circle_id=${that.circle_id}&group_form_id=${groupFormIdGet}`) } that.$apply() //脏值检查触发 }) }).catch(res => { log('支付失败', res) that.buttonClicked = true; }) })} 图片上传(七牛云) 更多图床网站请见我博客: https://www.cnblogs.com/fanghl/p/11419914.html 图片上传服务器采用七牛云服务,在app.wpy内小程序触发的时候,请求七牛云拿到token存为全局变量。 123456789101112//app.wpyonLaunch() { //other code *** // 七牛云,获取七牛云token wepy.request({ url: 'https://****************/qiniu_token/', header:{'content-type': 'application/json'}, }).then((res) => { this.globalData.qiniuToken = res.data.token });} 导入七牛云文件import qiniuyun from '@/utils/qiniuUploader' base.js代码: 1234567891011121314// 上传图片 base.jsconst uploadImg = (imageURL, uptokenURL) => { return new Promise((resolve, reject) => { qiniuyun.upload(imageURL, (res) => { resolve(res); }, (error) => { reject(error); }, { region: 'ECN', domain: '填入域名', uptoken: uptokenURL }); }); } 页面结构 12345678910111213141516<!-- 上传生活照 --> <view class=\"baseInfoTip\" style=\"border: 0\">上传生活照 <view class=\"imgUploadText\">(最多9张)</view> <view class=\"leftOriginLine\"></view> </view> <view class=\"uploadImgBox\"> <repeat for=\"{{images}}\" index=\"index\" item=\"item\" key=\"index\"> <view class=\"itemBox\"> <image class=\"imgItem\" src=\"{{item}}\" mode=\"aspectFill\"></image> <image class=\"imgItemCancel\" id=\"{{index}}\" src=\"../images/common/icon_cardImg_cancel.png\" @tap.stop=\"cancelUploadImg\"></image> </view> </repeat> <view class=\"itemBox\" @tap=\"addImg\" wx:if=\"{{!addImgCtrl}}\"> <image class=\"imgItem\" src=\"../images/common/icon_addImg.png\"></image> </view> </view> 上传图片业务: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455// 从相册选择照片上传addImg(){ const that = this; if( that.buttonClicked === false ) return; that.buttonClicked = false; wepy.chooseImage({ count:9 - that.images.length, sizeType: 'compressed', }).then(async(res1) => { that.buttonClicked = true; that.toast('上传图片中...','loading'); let filePath = res1.tempFilePaths; for(let i = 0;i < filePath.length;i++){ let imgSrc= res1.tempFilePaths[i]; let imgType = imgSrc.substring(imgSrc.length-3); let imgSize = res1.tempFiles[i].size; if(imgSize > 2000000 || imgType === 'gif'){ that.toast('该图片格式错误!请重新选择一张', 'none', 3000); continue } let res = await base.uploadImg(filePath[i], that.$parent.globalData.qiniuToken); that.images.push(res.imageURL); log('image长度:', that.images.length) log('image:', that.images) if( that.images.length >= 9) { that.addImgCtrl = true } if(that.images.length > 9){ that.images = that.images.slice(0,9) } if(that.images.length >0 && that.config.fImages){ that.config.progress = that.config.progress + parseFloat(that.config.getConfigs.lifepicweight*100); that.config.fImages = false } that.$apply(); // 上传用户头像列表 that.userInfo.photos = that.images if(i === filePath.length -1){ wepy.hideToast(); } } }).catch((res) => { if(res.errMsg === \"chooseImage:fail:system permission denied\"){ that.toast('请打开微信调用摄像头的权限', 'none', 3500) } })},// 取消图片上传cancelUploadImg(e) { if( this.images.length < 10 ) { this.addImgCtrl = false } let index = e.target.id this.images.splice(index, 1)}, 微信消息聊天布局微信聊天框整体布局特点有: 接收方和发送方消息分别位于屏幕的左右两侧、最新的消息一定是在屏幕最底部、进入消息dialog页面一定是显示的最新消息,即页面滑动在最底部。这三个基本特征构成了微信聊天页面的布局原则。先看效果图 (非最终效果):↓布局思路:flex反向布局 1234567891011121314151617<!-- 格式化代码 --><view class=\"msgBox\" id=\"msgBox\"> <repeat for=\"{{talkContent}}\" key=\"index\" item=\"item\"> <view class=\"msgItem {{item.send_user === configData.send_user ? 'msgItemReverse' : ''}}\"> <image class=\"adverseHeadimg\" src=\"{{item.send_user === configData.send_user ? configData.user_img : talkAimerInfo.headimg}}\" mode=\"aspectFill\"> </image> <text class=\"textBox {{item.send_user == configData.send_user ? 'textGreen' : ''}}\" selectable=\"true\"> {{item.message}} </text> </view> <view class=\"timeTip\" wx:if=\"{{item.send_user != configData.send_user}}\"> {{item.create_time}} </view> </repeat></view> 1234.msgBox{ display: flex; /*整体消息框flex布局,纵向取反布局*/ flex-direction: column-reverse;} 12345678.msgItem{ /*消息item样式*/ position: relative; display: flex; flex-direction: row;}.msgItemReverse{ /*对方的消息样式,flex行取反布局*/ flex-direction: row-reverse;} 保持页面始终滑动在最底部函数 12345678910pageScrollToBottom( msgLength ) { //在页面需要进行变化时调用 wx.createSelectorQuery().select('#contentBox').boundingClientRect(function(rect){ // 使页面滚动到底部 log('rect', rect) wx.pageScrollTo({ scrollTop: rect.bottom + msgLength*60, duration: 80 }) log('msgBox的下边界坐标: ', msgLength ) }).exec() } 自己发送的消息数据可以直接压入本地数组 talkContent 内,Unshift()进入,得到“负负得正”效果,即数据反,布局反即可得到从底部排列的布局。对方的消息从服务器拉下来的时候,放入 talkContent 内前 reverse() 一下即可 聊天页面input顶起页面相关聊天input点击后,默认为顶起页面,也可以关闭默认选择不顶起。但是不顶起页面其实是input脱离当前page,会出现键盘上方没有我们的输入框!因为键盘不顶起页面,故不会影响之前的布局,输入框一般都在页面最底部。解决: wx.onKeyboardHeightChange 监听键盘高度,严重不推荐input自身函数bindkeyboardheightchange,因为bindkeyboardheightchange 在手势上划隐藏键盘时Android是不会被触发的!!!思路: adjust-position = "false"设置不顶起页面,在手动把内容展示view 的高度减少键盘的高度!在键盘拉起时,内容高度减少键盘的高度,在键盘隐藏式,回复原高度。最后的效果和微信原生聊天一样!效果: 优化:在减少高度的同时,把内容页面滑到最底部,以展示最新消息! 12345678910<input class=\"inputContent\" type=\"text\" value=\"{{userInputContent}}\" bindinput = \"InputBlur\" adjust-position = \"{{false}}\" hold-keyboard = true confirm-hold = true confirm-type = 'done' @tap=\"onInpueChange\"> 123456789101112131415161718onInpueChange() { const that = this that.scrollBottom() wx.onKeyboardHeightChange(res => { that.log(res.height) that.scrollView.height = res.height *2 + 20 that.$apply() })}// 页面滚动到底部scrollBottom(){ const that = this; that.scrollTopValue++; setTimeout(function() { that.scrollTop = that.scrollTopValue; that.$apply() }, 300);} CSS注意点CSS持续补充中……word-break: break-all; //换行文字,英文溢出-webkit-overflow-scrolling: touch; //ios端启用硬件加速,解决ios端滑动粘手catchtouchmove='true' //模态框中添加,禁止页面滑动circleDynamic:last-of-type //特定类circleDynamic中最后一个元素:nth-of-type(1/odd/even) //选择特定元素下第几个元素 123 /* CSS 吸顶 */position: sticky; top: 0; async/await异步编程的终极解决方案,在小程序内拿取code或者login时会用到,await可理解为求值!async可理解为搭配await的语法,如果异步函数去掉await,返回的一般是 promise 对象,需要手动去reject 和 resolve 。 123456789101112if( !wepy.getStorageSync('token') ) { wepy.login().then(async (res) => { if(res.code) { let code = res.code; await that.login(code) wepy.setStorage({ key: \"code\", data: code }) } }); } else {} ios/android机型区别由于微信小程序的运行规范限制等,一些在 Android 上可以存在的业务需求并不能原封不动在 ios 端运行,否则小心 封号警告 (此处手动滑稽.jpg),所以一般采取两个系统的用户进入某一个页面,展现不同的内容。判断机型:在 app.wpy 入口文件中,onlaunch 生命周期内判断机型并保存到全局变量即可 12345678getSystemInfo() { const that = this; wx.getSystemInfo({ success(res) { that.globalData.userPlatform = res.platform; } })} 分包微信小程序官方限制小程序代码大小不得超过 2M ,在业务逻辑较多的情况下,查过2M后,我们可以采用分包加载。 1234567891011121314151617181920//app.wpyconfig = { pages: [ 'basePage1', 'basePage2', 'basePage3', ], subPackages: [ { root: 'dirName', //通常结构和 pages: [ 'subPage1', 'subPage2', ] } ]}//页面使用:this.nav(`/dirName/pages/subPage1`) canvas生成海报小程序分享至朋友圈的海报制作过程,海报内容为动态获取,内容根据每个用户生成不同的海报 如图,除了背景使用本地图片,其他的所有内容均为动态获取,且每次获取的不尽相同。 图片先从网络图片下载到本地才可以渲染,如果开发工具可以正常显示,而真机无法绘制,那么请先检查你的 downloadFile 域名!!! 绘制圆角-直角图片 图片保存模糊 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788//绘制文字 drawTxt(fontSize, color, content, x, y, bold=false) { this.ctx.save() this.ctx.setFontSize(fontSize) this.ctx.setFillStyle(color) this.ctx.fillText(content, x, y) if (bold) { this.ctx.fillText(content, x, y + 0.3) this.ctx.fillText(content, x + 0.3, y) } this.ctx.setTextBaseline('middle') this.ctx.restore() }//绘制圆角图片(默认)-矩形图片 getRectWithRadius(ctx, x, y, w, h, r, c, borderArgs = []) { // r > 0 则默认绘制圆角图片, r = 0 ,则绘制矩形 //绘制圆形 则 r = 1/2 w || 1/2h //绘制任意直角 则 borderArgs = [leftTop, rightTop, rightBottom, leftBottom]控制 let rate = this.rate let b = borderArgs ctx.beginPath() ctx.moveTo(x / rate, y / rate) b && b[0] ? '' : ctx.arc((x + r) / rate, (y + r) / rate, r / rate, Math.PI, 1.5 * Math.PI) b && b[1] ? ctx.lineTo((x + w) / rate, y / rate) : ctx.lineTo((x + w - r) / rate, y / rate) b && b[1] ? '' : ctx.arc((x + w - r) / rate, (y + r) / rate, r / rate, 1.5 * Math.PI, 0) b && b[2] ? ctx.lineTo((x + w) / rate, (y + h) / rate) : ctx.lineTo((x + w) / rate, (y + h - r) / rate) b && b[2] ? '' : ctx.arc((x + w - r) / rate, (y + h - r) / rate, r / rate, 0, 0.5 * Math.PI) b && b[3] ? ctx.lineTo((x) / rate, (y + h) / rate) : ctx.lineTo((x + r) / rate, (y + h) / rate) b && b[3] ? '' : ctx.arc((x + r) / rate, (y + h - r) / rate, r / rate, 0.5 * Math.PI, Math.PI) ctx.closePath() if (c) { ctx.fillStyle = c ctx.fill() } }// 保存海报 savePosterToLocal() { const that = this // 获取用户是否开启用户授权相册 wx.getSetting({ success(res) { // 如果没有则获取授权 if (!res.authSetting['scope.writePhotosAlbum']) { wx.authorize({ scope: 'scope.writePhotosAlbum', success() { that.saveCanvas() }, fail() { that.toast('右上角开启授权', 'none') wx.openSetting() } }) } else { that.saveCanvas() } } }) }//canvas保存至相册 saveCanvas() { // 1-把画布转化成临时文件 const that = this wx.canvasToTempFilePath({ x: 0, y: 0, width: 560, // 画布的宽 height: 996, // 画布的高 destWidth: 1080 * 750 / wx.getSystemInfoSync().windowWidth, destHeight: 1920 * 750 / wx.getSystemInfoSync().windowWidth, canvasId: 'poster', success(res) { wepy.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success(res2) { that.toast('保存至相册成功', 'none') }, fail() { that.toast('保存失败,稍后再试', 'none') } }) }, fail() { that.toast('保存失败,稍后再试', 'none') } }) } 多张图片下载网络图片下载成本地图片,且全部下载完全后才可以绘制canvas 1234567891011121314151617181920212223242526272829let promise1 = new Promise(function(resolve,reject){ wx.getImageInfo({ src: that.posterData.matchmaker.qrcord, success:function(res){ imgResource.qrcord = res.path resolve(res); }, fail:function(res){ reject(res); } }) })let promise2 = new Promise(function(resolve,reject){ wx.getImageInfo({ src: that.posterData.matchmaker.qrcord, success:function(res){ imgResource.qrcord = res.path resolve(res); }, fail:function(res){ reject(res); } })})Promise.all([promise1, promise2, promise3]).then(res => { //开始绘制 const ctx = wx.createCanvasContext('poster') that.ctx = ctx} 异常显示有时候Android手机会显示不完canvas,宽度异常,ios却没有该异常现象。其中一种可能就是设置canvas标签的宽高单位不一致,canvas内单位同一位 px ,把常用的 rpx 替换为 px <canvas canvas-id="poster" style="width: 280px; height: 498px;"></canvas> touchmove/onPageScroll动画效果如图,右下角的按钮,一般会做这样的效果,当用户滑动列表时,该按钮向下滑动并隐藏,当用户停止滑动且页面亦停止滑动(非用户手指脱离屏幕)时,该按钮再从页面底部滑出。 touchmove、 onPageScroll 思路1: 第一种想法是touchstart时, 触发下滑动画,touchend时触发上划动画 缺点: tap点击事件也会先触发start 和 end 事件,故点击屏幕也会触发动画,且屏幕抖动,pass 思路2: touchmove 时触发动画,touchend时上划动画, 缺点:虽然避免了点击就触发动画,但效果不佳,手指离开屏幕,页面还在滑动,动画已触发。 最终解: 只用 touchmove 来判断用户滑动列表,再用 onPageScroll 配合 超时器 来处理页面停止滑动。 代码: 12345678910111213141516171819202122//template<view calss=\"{{isSlide ? 'down-slide-hide' : 'up-slide-hide'}}\"></view>// datadata: { isSlide: false, timer: null,}//methodstouchmove(e) { this.isSlide = true this.$apply()},onPageScrool(e) { const that = this clearTimeout(that.timer) that.timer = setTimeout(() => { that.isSlide = false that.$apply() }, 400)} 页面一直处于滑动时,超时器不会生效,只有在页面停止滑动后,超时器才生效,程序执行 录音 录音没有难度,上传语音采用七牛云服务,拿到临时路径经过七牛云拿到网络路径,在传给自己服务器。这里实现了一个圆环进度条(canvas),在录音时配合录音时长展示 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748//绘制进度条圆环startRocord() { ... this.countInterval()}drawCircle(step) { !this.ctx && this.ctx = wx.createCanvasContext('progressBar') let ctx = this.ctx // ctx.clearRect(0, 0, 120, 120) //清除画布,(多次重复绘制需要。但绘制步伐过快会导致闪屏) // ctx.draw() ctx.setLineWidth(10) ctx.setStrokeStyle('#FF5757') ctx.setLineCap('round') ctx.beginPath() ctx.arc(60, 60, 55, -Math.PI/2, step*Math.PI - Math.PI/2, false) ctx.stroke() ctx.draw()}//进度绘制定时器countInterval() { this.countTimer = setInterval(() => { if (this.count <= 600) { //绘制步伐,这里0.1秒绘制,很丝滑 this.drawCircle(this.count / (600/2)) this.count++ this.$apply() } else { clearInterval(this.countTimer) this.count = 0 } }, 1000 * 0.1);}//录音实例 initRecord() { !this.RM && (this.RM = wx.getRecorderManager()) let RM = this.RM RM.onStop(async res => { //监听录音结束, res会返回录音信息(临时文件路径、时长、文件大小) //七牛云上传临时路径 let result = await base.uploadImg(res.tempFilePath, wepy.$instance.globalData.qiniuToken) ... }) RM.start({ duration: 60*1000, format: 'mp3', })} 播音 createInnerAudioContext 用来播放各个用户语音,可以随时切换不同用户的语音播放,安卓规规矩矩没问题 ios播放异常 ios用户切换语音时会播放第一个音频,但随后的语音却不会播放,实例已经销毁,但貌似对ios无效。解决:单例模式创建实例,在销毁实例后,在将变量手动清空,可以解决ios新建播音实例无效问题。 12345678910111213141516171819202122232425262728293031323334353637383940//dataIAC: null,isAudition: false,/*** src: 音频 cubicle:播放开关 index: 用户索引(不同音频) currentUser:当前需要播放的用户 lastUser: 上一个播放的用户 isAuditionStatus: 播放状态(未播放,正在播放)* handleSound 监听新老用户播音,若IAC正在播音,此时继续点击同一用户,则暂停当前音频,若IAC未在播音,则播音当前用户。若点击不同用户,则暂停当前正在播音的用户,播放新用户录音。*/async handleSound(src, cubicle, index) { if (this.currentUser !== index) { this.lastUser = this.currentUser this.currentUser = index this.changeUser = true } if (this.changeUser) { this.IAC && this.IAC.destroy() this.IAC = null //ios这点不仅需要destroy实例,还要手动清空变量 this.lastUser != -1 && (this.recommUserList[this.lastUser].isAuditionStatus = 0) !this.IAC && (this.IAC = wx.createInnerAudioContext()) let IAC = this.IAC IAC.src = src IAC.play() IAC.onPlay(() => { this.recommUserList[this.currentUser].isAuditionStatus = wepy.$instance.globalData.isPlaying = 1 this.$apply() }) IAC.onStop(() => { this.recommUserList[this.currentUser].isAuditionStatus = wepy.$instance.globalData.isPlaying = 0 this.$apply() }) IAC.onEnded(() => { this.recommUserList[this.currentUser].isAuditionStatus = wepy.$instance.globalData.isPlaying = 0 this.$apply() }) this.changeUser = false this.$apply() } else { cubicle ? this.IAC.play() : this.IAC.stop() }} 不管是录音还是播音,在页面卸载(onUnload)的时候清除掉实例或者初始化,有canvas也要清除画布 华为-textarea-层级异常华为部分机型,对小程序的textarea标签支出并不友好,其textarea的内容以及 placeholder 内容恨天高,无法通过程序控制,甚至小程序官方说 canvas 的层级是最高的,但也没高过textarea! 问题: 盖在textarea上面的弹框会被textarea的内容穿掉盖不住,并且点击事件直接穿透 解决:在拉起盖在textarea上面的弹框(或组件)时,用 view 标签重写模仿 textarea 样式,并把 textarea 关闭掉。 持续更新…….","categories":[{"name":"工作总结","slug":"工作总结","permalink":"http://fanghl.top/categories/工作总结/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"firstBlog","slug":"firstBlog","date":"2019-08-19T07:02:47.000Z","updated":"2023-11-03T03:29:00.558Z","comments":true,"path":"2019/08/19/firstBlog/","link":"","permalink":"http://fanghl.top/2019/08/19/firstBlog/","excerpt":"","text":"#既昨天搞崩GitHub博客,再次坚强的尝试 既昨天搞崩GitHub博客,再次坚强的尝试既昨天搞崩GitHub博客,再次坚强的尝试####既昨天搞崩GitHub博客,再次坚强的尝试 #####既昨天搞崩GitHub博客,再次坚强的尝试 1alert('hello world')","categories":[{"name":"test","slug":"test","permalink":"http://fanghl.top/categories/test/"}],"tags":[{"name":"Programming","slug":"Programming","permalink":"http://fanghl.top/tags/Programming/"}]},{"title":"Hello World","slug":"hello-world","date":"2019-08-15T12:43:17.000Z","updated":"2023-11-03T03:29:00.559Z","comments":true,"path":"2019/08/15/hello-world/","link":"","permalink":"http://fanghl.top/2019/08/15/hello-world/","excerpt":"","text":"Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub. Quick StartCreate a new post1$ hexo new \"My New Post\" More info: Writing Run server1$ hexo server More info: Server Generate static files1$ hexo generate More info: Generating Deploy to remote sites1$ hexo deploy More info: Deployment","categories":[],"tags":[]}]} \ No newline at end of file diff --git a/css/main.css b/css/main.css index caa7e73b..ff6ac419 100644 --- a/css/main.css +++ b/css/main.css @@ -1950,7 +1950,7 @@ pre .javascript .function { width: 4px; height: 4px; border-radius: 50%; - background: #283b9c; + background: #f5ad4b; } .links-of-blogroll { font-size: 13px;