- 云开发能力基础讲解
前言 本文主要是在云开发能力基础讲解官方的学习过程中,发现官方讲解版本过老而且有很多不清楚的地方作一些解释和细化,以便新手更快入门而不是卡在一些无谓的细节上面,在阅读上述链接文章和本文之前,前置课程云开发快速入门官方 官方教程视频演示中的模板与新版开发者工具中的模板又不太一样,新手很难上手实操,故有了本文 旧模板 [图片] 新模板 [图片] 本文作为补充讲解,建议是在阅读云开发能力基础讲解官方的过程中遇到问题到本文寻找解决方案 1.本地调试和云端测试 第一步 新建云函数 按照教程流程 第二步 本地调试云函数是否正确 官方本地调试文档可能对新手来说比较晦涩 按照教程流程打开本地调试后,弹出窗口 [图片] 让人一筹莫展,不知应该在何处修改代码 此时我们将请求方式改为“手动触发”,再将请求参数 ’’’ { “width”: 3, “height”:7 } ’’' 填入请求参数一栏,点击调用,即能在控制台看到本地调用结果 [图片] 第三步 云端测试云函数是否正确 按照文档,把 [图片] 改为 [图片]运行测试后即可看到结果 侧边栏滚轮往下即可看到 [图片] 2.获取用户信息和登录 2.1 通过button获取用户信息 教程中要新建login页面,可很多人压根不知道在哪新建,这里提供一种简单的方式 点击app.json,按照图示输入[代码]"pages/login/login"[代码](index或者login只是文件名的关系,图中index是为了跟其他文件保持格式一致),注意逗号的有无,最后一行无需逗号,然后Ctrl+S保存 [图片] 3.小程序端上传图片到云存储 3.1 文件名与后缀的处理 临时路径是选择图片上传图片之后在控制台可以看到 [图片] 4.调用数据库 在测试操作数据库时, 记得在导入数据后更改云数据库权限, 默认是"仅创建者可读写", 可以改成"所有用户可读, 仅创建者可读写", 这样客户端加载页面时就能顺利请求到数据了, 否则会显示请求成功, 但请求到的数据列表为空. [图片]
2022-02-04 - 技术文章写作规范
技术文章写作规范 来源: GitHub - ruanyf/document-style-guide: 中文技术文档的写作规范,by阮一峰 前言 最近在写文章会想一篇好的文章是不是和写语文作文一样有规范呢?所以就在网上看到阮一峰老师的写的这篇中文技术文档写作规范,觉得真的写得很好,这里就和大家分享一下。 但是阮老师写的是技术文档规范,个人觉得文章的话如果整得太严肃的话或许不太好,不过一些结构,表达还是要注意得,像代码用 md 代码块, uml 图等等会好一点,当然这是个人爱好,这里给大家转载一些我觉得在平时写文章要注意一下的几个点,有兴趣的可以直接去看阮老师的正文! 标题 层级 标题分为四级。 一级标题:文章的标题 二级标题:文章主要部分的大标题 三级标题:二级标题下面一级的小标题 四级标题:三级标题下面某一方面的小标题 下面是示例。 [代码]# 一级标题 ## 二级标题 ### 三级标题 #### 四级标题 [代码] 原则 (1)一级标题下,不能直接出现三级标题。 示例:下面的文章结构,缺少二级标题。 [代码]# 一级标题 ### 三级标题 [代码] (2)标题要避免孤立编号(即同级标题只有一个)。 示例:下面的文章结构,[代码]二级标题 A[代码]只包含一个三级标题,完全可以省略[代码]三级标题 A[代码]。 [代码]## 二级标题 A ### 三级标题 A ## 二级标题 B [代码] (3)下级标题不重复上一级标题的名字。 示例:下面的文章结构,二级标题与下属的三级标题同名,建议避免。 [代码]## 概述 ### 概述 [代码] (4)谨慎使用四级标题,尽量避免出现,保持层级的简单,防止出现过于复杂的章节。 如果三级标题下有并列性的内容,建议只使用项目列表(Item list)。 示例:下面的结构二要好于结构一。结构一适用的场景,主要是较长篇幅的内容。 [代码]结构一 ### 三级标题 #### 四级标题 A #### 四级标题 B #### 四级标题 C 结构二 ### 三级标题 **(1)A** **(2)B** **(3)C** [代码] 文本 字间距 (1)全角中文字符与半角英文字符之间,应有一个半角空格。 [代码]错误:本文介绍如何快速启动Windows系统。 正确:本文介绍如何快速启动 Windows 系统。 [代码] (2)全角中文字符与半角阿拉伯数字之间,有没有半角空格都可,但必须保证风格统一,不能两种风格混杂。 [代码]正确:2011年5月15日,我订购了5台笔记本电脑与10台平板电脑。 正确:2011 年 5 月 15 日,我订购了 5 台笔记本电脑与 10 台平板电脑。 [代码] 半角的百分号,视同阿拉伯数字。 [代码]正确:今年我国经济增长率是6.5%。 正确:今年我国经济增长率是 6.5%。 [代码] (3)英文单位若不翻译,单位前的阿拉伯数字与单位符号之间,应留出适当的空隙。 [代码]例1:一部容量为 16 GB 的智能手机 例2:1 h = 60 min = 3,600 s [代码] (4)半角英文字符和半角阿拉伯数字,与全角标点符号之间不留空格。 [代码]错误:他的电脑是 MacBook Air 。 正确:他的电脑是 MacBook Air。 [代码] 句子 (1)避免使用长句。 不包含任何标点符号的单个句子,或者以逗号分隔的句子构件,长度尽量保持在 20 个字以内;20~29 个字的句子,可以接受;30~39 个字的句子,语义必须明确,才能接受;多于 40 个字的句子,任何情况下都不能接受。 [代码]错误:本产品适用于从由一台服务器进行动作控制的单一节点结构到由多台服务器进行动作控制的并行处理程序结构等多种体系结构。 正确:本产品适用于多种体系结构。无论是由一台服务器(单一节点结构),还是由多台服务器(并行处理结构)进行动作控制,均可以使用本产品。 [代码] 逗号分割的长句,总长度不应该超过 100 字或者正文的 3 行。 (2)尽量使用简单句和并列句,避免使用复合句。 [代码]并列句:他昨天生病了,没有参加会议。 复合句:那个昨天生病的人没有参加会议。 [代码] (3)同样一个意思,尽量使用肯定句表达,不使用否定句表达。 [代码]错误:请确认没有接通装置的电源。 正确:请确认装置的电源已关闭。 [代码] (4)避免使用双重否定句。 [代码]错误:没有删除权限的用户,不能删除此文件。 正确:用户必须拥有删除权限,才能删除此文件。 [代码] 变化程度的表示法 数字的增加要使用“增加了”、“增加到”。“了”表示增量,“到”表示定量。 [代码]增加到过去的两倍 (过去为一,现在为二) 增加了两倍 (过去为一,现在为三) [代码] 数字的减少要使用“降低了”、“降低到”。“了”表示增量,“到”表示定量。 [代码]降低到百分之八十 (定额是一百,现在是八十) 降低了百分之八十 (原来是一百,现在是二十) [代码] 不能用“降低 N 倍”或“减少 N 倍”的表示法,要用“降低百分之几”或“减少百分之几”。因为减少(或降低)一倍表示数值原来为一百,现在等于零。 参考链接 产品手册中文写作规范, by 华为 写作规范和格式规范, by DaoCloud 技术写作技巧在日汉翻译中的应用, by 刘方 简体中文规范指南, by lengoo 文档风格指南, by LeanCloud 豌豆荚文案风格指南, by 豌豆荚 中文文案排版指北, by sparanoid 中文排版需求, by W3C 为什么文件名要小写?, by 阮一峰 Google Developer Documentation Style Guide, by Google
2021-11-18 - mongodb知识点总结
mongodb知识总结 微信云开发使用的云数据库集成了简化版的mongodb,因此学习两者之一就可以很快掌握另一个 mongoDB 开篇 [代码]<!-- 1.什么是MongoDB? MongoDB和MySQL一样都是数据库, 都是存储数据的仓库, 不同的是MySQL是关系型数据库, 而MongoDB是非关系型数据库 2.什么是非关系型数据库? - 在'关系型数据库'中, 数据都是存储在表中的, 对存储的内容有严格的要求 因为在创建表的时候我们就已经规定了表中有多少个字段, 已经规定了每个字段将来要存储什么类型数据, 已经规定了每个字段将来是否可以为空,是否必须唯一等等 - 在'非关系型数据库'中, 没有表概念, 所以存储数据更加灵活 因为不需要创建表,所以也没有规定有哪些字段, 也没有规定每个字段数据类型, 也没有规定每个字段将来是否可以为空,是否必须唯一等等 - '关系型数据库'由于操作的都是结构化的数据, 所以我们需要使用结构化语言SQL来操作 - '非关系型数据库'由于数据没有严格的结构要求, 所以无需使用SQL来操作 3.什么是MongoDB? 存储文档(BSON)的非关系型数据库 --> <!-- 例如在MySQL中: |--------------------------------------------------------| | name(varchar(255) not null) | age(int unique) | |--------------------------------------------------------| 我们可以把 'zs', 33 保存到表中 但是我们不能将 33, 'zs' 保存到表中 但我们不能能将 null, 33 保存到表中 但是我们不能将 'zs', 33, '男' 保存到表中 但是我们不能再次将 'zs', 33 保存到表中 --> <!-- 例如在MongoDB中: 我们可以把 {name: 'zs', age: 33}; 保存到集合中 我们也可以把 {name: 33, age: 'zs'}; 保存到集合中 我们也可以把 {name: null, age: 33}; 保存到集合中 我们也可以把 {name: 'zs', age: 33, gender:'男'}; 保存到集合中 但是我们可以再次将 {name: 'zs', age: 33}; 保存到集合中 - '非关系型数据库'可以看做是'关系型数据库'的功能阉割版本, 通过减少用不到或很少用的功能,从而提升数据库的性能 --> <!-- 4.MongoDB是如何存储文档的? MySQL中所有的数据都是存储在表中的, 而MongoDB中所有的数据都是存储在集合中的 4.1MySQL |--行1 |--表1--|--行2 数据库--| |--行3 |--表2 |--... ... 4.2MongoDB |--文档1 |--集合1--|--文档2 数据库--| |--文档3 |--集合2 |--... ... --> <!-- 5.企业开发如何选择? - 关系型数据库和非关系型数据库之间并不是替代关系, 而是互补关系 所以在企业开发中大部分情况是结合在一起使用. - 对于数据模型比较简单、数据性能要求较高、数据灵活性较强的数据, 我们存储到非关系型数据库中 相反则存储到关系型数据库中 - 具体使用: 会在项目中实现 --> [代码] 快速上手 [代码]<!-- https://docs.mongodb.com/manual/ https://www.mongodb.org.cn/tutorial/ 1.连接MongoDB服务器 通过mongo连接MongoDB服务器 2.查看数据库 show dbs #和MySQL中的 show databases; 指令一样 3.创建数据库 use 数据库名称 #和MySQL中的 use 指令一样, 只不过MongoDB中的use数据库不存在会自动创建 4.查看数据库中有哪些集合 show collections #和MySQL中的 show tables; 指令一样 5.创建集合 db.createCollection('集合名称'); #和MySQL中的 create table xxx(); 指令一样 6.插入数据 db.集合名称.insert(文档对象); #和MySQL中的 insert into xxx values () 指令一样 7.查询数据 db.集合名称.find(); #和MySQL中的 select * from xxx; 指令一样 8.删除集合 db.集合名称.drop() #和MySQL中的 drop table xxx; 指令一样 9.删除数据库 db.dropDatabase() #在哪个数据库中就会删除哪个数据库 #和MySQL中的 drop database xxx; 指令一样 10.和MySQL的不同 - 没有MySQL中表的概念, 取而代之的是集合 - 创建集合时不用指定集合中有哪些字段 - 只要是一个合法的文档对象都可以往里面存储 - ... ... --> [代码] 主键 [代码]<!-- 1.主键 - MongoDB的主键和MySQL一样, 也是用于保证每一条数据唯一性的 - 和MySQL不同的是, MongoDB中的主键无需明确指定 + 每一个文档被添加到集合之后, MongoDB都会自动添加主键 + MongoDB中文档主键的名称叫做 _id - 默认情况下文档主键是一个ObjectId类型的数据 + ObjectId类型是一个12个字节字符串(5e8c5ae9-c9d35e-759b-d6847d) + 4字节是存储这条数据的时间戳 + 3字节的存储这条数据的那台电脑的标识符 + 2字节的存储这条数据的MongoDB进程id + 3字节是计数器 2.为什么要使用ObjectId类型数据作为主键? 因为MongoDB是支持'横向扩展'的数据库 - 横向扩展是指'增加数据库服务器的台数' - 纵向扩展是指'增加数据库库服务器的配置' - 过去一个数据库只能安装在一台电脑上, 但是每台电脑的性能是有峰值的 一旦达到峰值就会导致服务器卡顿、宕机、重启等问题. 所以过去为了防止如上问题的出现,我们只能不断的'纵向扩展' 也就是不断的提升服务器的配置, 让服务器能处理更多的请求 但是纵向扩展也是有峰值的, 一台电脑的配置不可能无限提升 所以为了解决这个问题就有了分布式数据库 - 分布式数据库是指可以在多台电脑上安装数据库, 然后把多台电脑组合成一个完整的数据库, 在分布式数据库中,我们可以通过不断同步的方式, 让多台电脑都保存相同的内容 当用户请求数据时, 我们可以把请求派发给不同的数据库服务器处理 当某一台服务器宕机后, 我们还可以继续使用其它服务器处理请求 从而有效的解决了单台电脑性能峰值和单台电脑宕机后服务器不能使用的问题 2.2为什么要使用ObjectId类型数据作为主键? 正是因为MongoDB是一个分布式数据库, 正是因为分布式数据库可以把请求派发给不同的服务器 所以第一次插入数据时, 我们可能派发给了A服务器, 插入到了A服务器的数据库中 但是第二次插入数据时, 我们又可能派发给了B服务器, 插入到了B服务器的数据库中 但是B服务器此时并不知道A服务器当前的主键值是多少, 如果通过MySQL中简单的递增来保证数据的唯一性 那么将来在多台服务器同步数据的时候就会出现重复的情况, 所以MongoDB的主键并没有使用简单的递增 而是使用了ObjectId类型数据作为主键 3.是否支持其它类型数据作为主键? 3.1在MongoDB中支持除了'数组类型'以外的其它类型数据作为主键 3.2在MongoDB中甚至还支持将一个文档作为另一个文档的主键(复合主键) db.person.insert({name: 'lnj', age: 33}); db.person.insert({_id: 1, name: 'lnj', age: 33}); #db.person.insert({_id: 1, name: 'lnj', age: 33}); #报错 db.person.insert({_id: '1', name: 'lnj', age: 33}); db.person.insert({_id: {name:'it66', gender: '男'}, name: 'lnj', age: 33}); #db.person.insert({_id: {name:'it66', gender: '男'}, name: 'lnj', age: 33}); #报错 db.person.insert({_id: {gender: '男', name:'it66'}, name: 'lnj', age: 33}); --> [代码] 创建文档 [代码]<!-- 1.主键 - MongoDB的主键和MySQL一样, 也是用于保证每一条数据唯一性的 - 和MySQL不同的是, MongoDB中的主键无需明确指定 + 每一个文档被添加到集合之后, MongoDB都会自动添加主键 + MongoDB中文档主键的名称叫做 _id - 默认情况下文档主键是一个ObjectId类型的数据 + ObjectId类型是一个12个字节字符串(5e8c5ae9-c9d35e-759b-d6847d) + 4字节是存储这条数据的时间戳 + 3字节的存储这条数据的那台电脑的标识符 + 2字节的存储这条数据的MongoDB进程id + 3字节是计数器 2.为什么要使用ObjectId类型数据作为主键? 因为MongoDB是支持'横向扩展'的数据库 - 横向扩展是指'增加数据库服务器的台数' - 纵向扩展是指'增加数据库库服务器的配置' - 过去一个数据库只能安装在一台电脑上, 但是每台电脑的性能是有峰值的 一旦达到峰值就会导致服务器卡顿、宕机、重启等问题. 所以过去为了防止如上问题的出现,我们只能不断的'纵向扩展' 也就是不断的提升服务器的配置, 让服务器能处理更多的请求 但是纵向扩展也是有峰值的, 一台电脑的配置不可能无限提升 所以为了解决这个问题就有了分布式数据库 - 分布式数据库是指可以在多台电脑上安装数据库, 然后把多台电脑组合成一个完整的数据库, 在分布式数据库中,我们可以通过不断同步的方式, 让多台电脑都保存相同的内容 当用户请求数据时, 我们可以把请求派发给不同的数据库服务器处理 当某一台服务器宕机后, 我们还可以继续使用其它服务器处理请求 从而有效的解决了单台电脑性能峰值和单台电脑宕机后服务器不能使用的问题 2.2为什么要使用ObjectId类型数据作为主键? 正是因为MongoDB是一个分布式数据库, 正是因为分布式数据库可以把请求派发给不同的服务器 所以第一次插入数据时, 我们可能派发给了A服务器, 插入到了A服务器的数据库中 但是第二次插入数据时, 我们又可能派发给了B服务器, 插入到了B服务器的数据库中 但是B服务器此时并不知道A服务器当前的主键值是多少, 如果通过MySQL中简单的递增来保证数据的唯一性 那么将来在多台服务器同步数据的时候就会出现重复的情况, 所以MongoDB的主键并没有使用简单的递增 而是使用了ObjectId类型数据作为主键 3.是否支持其它类型数据作为主键? 3.1在MongoDB中支持除了'数组类型'以外的其它类型数据作为主键 3.2在MongoDB中甚至还支持将一个文档作为另一个文档的主键(复合主键) db.person.insert({name: 'lnj', age: 33}); db.person.insert({_id: 1, name: 'lnj', age: 33}); #db.person.insert({_id: 1, name: 'lnj', age: 33}); #报错 db.person.insert({_id: '1', name: 'lnj', age: 33}); db.person.insert({_id: {name:'it66', gender: '男'}, name: 'lnj', age: 33}); #db.person.insert({_id: {name:'it66', gender: '男'}, name: 'lnj', age: 33}); #报错 db.person.insert({_id: {gender: '男', name:'it66'}, name: 'lnj', age: 33}); --> <!-- 1.写入一个文档 db.<collection>.insertOne( <document>, { writeConcern: <document> } ); document: 需要写入的文档 writeConcern: 写入安全级别 2.安全级别 用于判断数据是否写入成功, 安全级别越高, 丢失数据风险越小, 但是性能消耗(操作延迟)也就越大 默认情况下MongoDB会开启默认的安全些级别,先不用关心 3.注意点 在使用insertXXX写入文档时, 如果调用insertOne的集合不存在会自动创建 db.person.insertOne({name:'zs', age:18}) db.person.find() db.student.insertOne({name:'zs', age:18}) #集合不存在会自动创建 db.student.find() 4.其它方式 db.<collection>.save( <document>, { writeConcern: <document> } ); db.person.save({name:'ls', age:19}) db.person.find() db.teacher.save({name:'ls', age:19}) #集合不存在会自动创建 db.teacher.find() 5.insertOne和save不同 主键冲突时insertOne会报错,而save会直接用新值覆盖久值 db.person.insertOne({_id:1, name:'ww', age:22}) db.person.find() db.person.insertOne({_id:1, name:'ww', age:22}) #报错 db.person.save({_id:1, name:'it666', age:66}) #用新数据替换久数据 db.person.find() --> <!-- 1.写入多个文档 db.<collection>.insertMany( [<document>, ...], { writeConcern: <document>, ordered: <boolean> } ); ordered: 是否按顺序写入 ordered默认取值是true, 也就是会严格按照顺序写入 如果ordered是false, 则不会按照顺序写入, 但写入效率更高(系统会自动优化) db.person.insertMany( [{name:'zs', age:18},{name:'ls', age:19},{name:'ww', age:20}], {} ) db.person.find() 2.注意点: 如果ordered是true, 前面的文档出错, 后面的所有文档都不会被写入 如果ordered是false, 前面的文档出错, 后面的所有文档也会被写入 db.person.insertMany( [{_id:1, name:'zs', age:18},{_id:1, name:'ls', age:19},{_id:2, name:'ww', age:20}], { ordered: true } ) db.person.find() db.person.remove({}) db.person.insertMany( [{_id:1, name:'zs', age:18},{_id:1, name:'ls', age:19},{_id:2, name:'ww', age:20}], { ordered: false } ) db.person.find() --> <!-- 1.写入一个或多个文档 db.<collection>.insert( <document> or ,[<document>, ...] { writeConcern: <document>, ordered: <boolean> } ); insertOne和insertMany结合体 2.注意点: 和insertOne/insertMany一样, 集合不存在会自动创建 和insertOne/insertMany一样, 主键冲突会报错 和insertMany一样, 默认都是按顺序插入, 前面的文档出错, 后续所有文档不会被插入 --> [代码] 读取文档 查询文档 [代码]<!-- 1.查询文档 db.<collection>.find( <query>, <projection> ) query: 查询条件, 相当于MySQL中的where projection: 投影条件, 规定了结果集中显示那些字段, 相当于MySQL中的 select 字段1, 字段2, .. from 表名; 2.查询所有文档 db.<collection>.find(); 不传入条件, 默认就是查询所有 3.查询满足条件文档 db.person.insert([{name:'zs', age:17},{name:'ls', age:18},{name:'ww', age:19}]) 3.1单个字段条件 db.person.find() // 默认会返回指定集合中所有的数据和所以的字段 db.person.find({name:'zs'}) // 我们可以通过第一个参数指定查询条件, find方法会把所有满足条件的数据返回给我们 3.2多个字段条件 db.person.find({name:'zs', age:17}) // 默认是And关系, 也就是默认要求同时满足所有指定的条件, 才会返回对应的数据 db.person.find({age:17, name:'zs'}) // 注意点: 没有顺序要求, 只要是同时满足多个条件即可 3.3文档中又是文档情况 db.person.insert( [{name:'zs', age:17, book:{name:'HTML', price:66}}, {name:'ls', age:18, book:{name:'JavaScript', price:88}}, {name:'ww', age:19, book:{name:'Vue', price:199}}] ) db.person.find({'book.name':'JavaScript'}) // 如果某一个文档的某一个字段的取值又是一个文档, 那么在判断的时候我们可以通过'字段.文档属性名称'的方式来判断 4.查询指定字段 0表示不显示, 1表示显示 除主键以外, 其它字段不能同时出现0和1(要么不写,写了就必须全是1或者全是0) db.person.find({},{_id:0}) // 如果不想查询某一个字段, 那么就可以指定这个字段的投影取值为0 db.person.find({},{_id:0, name:1, age:1, book:1}) // 如果想查询某一个字段, 那么就可以指定这个字段的投影取值为1 // 默认情况下如果不指定, 那么所有字段的投影取值都是1 db.person.find({},{_id:0, name:1, age:1, book:0}) // 除了_id字段以外, 其它的字段不能同时出现0和1 db.person.find({},{_id:0, book:0}) --> [代码] 比较运算符 [代码]<!-- 1.比较操作符 和MySQL一样, MongodDB中也支持很多比较操作符 $eq: 等于 / $ne: 不等于 $gt: 大于 / $gte: 大于等于 $lt: 小于 / $lte: 小于等于 2.使用格式 db.<collection>.find( {<field>: {$<operator>: <value>}}, <projection> ) 3.示例 db.person.insert([{name:'zs', age:17, gender:'男'},{name:'ls', age:18},{name:'ww', age:19}]) 查询名称叫做zs的人 查询所有成年人 查询所有未成年人 查询所有不是18岁的人 db.person.find({name:'zs'}) //默认情况下就是按照相等来判断 db.person.find({name:{$eq:'zs'}}) //这里就是明确的告诉MongoDB需要按照相等来查询 db.person.find({age:{$gte: 18}}) db.person.find({age:{$lt: 18}}) db.person.find({age:{$ne: 18}}) 注意点: 没有指定字段也算作不等于 db.person.find({gender:{$ne: '女'}}) // 注意点: 在做不等于判断的时候, 没有需要判断的字段, 也算作是不等于 --> <!-- 1.其它比较操作符 $in: 匹配和任意指定值相等的文档 $nin:匹配和任意指定值都不相等的文档 2.使用格式 db.<collection>.find( {<field>: {$<operator>: [<value1>, <value2>, ...]}}, <projection> ) 3.实例 查询名称叫做zs或者ls的人 查询名称不叫zs或者ls的人 查询性别不是男或女的人 db.person.find({name:{$in:['zs', 'ls']}}) // 匹配和任意指定值相等的文档 db.person.find({name:{$nin:['zs', 'ls']}}) // 匹配和任意指定值都不相等的文档 db.person.find({gender:{$nin:['男', '女']}}) // 注意点: 和$ne一样, 如果没有需要判断的字段, 也算作满足条件 注意点: 没有指定字段也算作不包含 --> [代码] 逻辑操作符 [代码]<!-- 1.逻辑操作符 $not: 匹配条件不成立的文档 {<field>: {$not: {<expression>}}} $and: 匹配条件全部成立的文档 {<field>: {$and: [{<expression1>}, {<expression2>}, ...}]} $or : 匹配至少一个条件成立的文档 {<field>: {$or: [{<expression1>}, {<expression2>}, ...}]} $nor: 匹配多个条件全部不成立的文档 {<field>: {$nor: [{<expression1>}, {<expression2>}, ...}]} 2.示例: //2.1$not //查询所有年龄不等于18岁的人 db.person.find({age:{$ne:18}}) db.person.find({age:{$not:{$eq:18}}}) //查询不是男人的人 db.person.find({gender:{$eq:'男'}}) // 注意点: $not运算符和$ne/$nin一样, 如果需要查询的字段不存在, 也会算作条件成立 db.person.find({gender:{$not:{$eq:'男'}}}) //2.2$and //查询所有名称叫做zs的未成年人 db.person.find({$and:[{name:{$eq:'zs'}},{age:{$lt:18}}]}) db.person.find({$and:[{name:'zs'},{age:{$lt:18}}]}) db.person.find({name:'zs', age:{$lt:18}}) //2.3$or //查询所有名称叫做zs或者ls的人 db.person.find({name:{$in:['zs','ls']}}) db.person.find({$or:[{name:{$eq:'zs'}},{name:{$eq:'ls'}}]}) db.person.find({$or:[{name:'zs'},{name:'ls'}]}) //2.4$nor //查询所有名称不叫zs或者ls的人 db.person.find({name:{$nin:['zs','ls']}}) db.person.find({$nor:[{name:'zs'},{name:'ls'}]}) //查询所有名称不叫zs或者性别不是男的人 // 注意点: $nor运算符和$ne/$nin/$not一样, 如果需要查询的字段不存在, 也会算作条件成立 db.person.find({$nor:[{gender:'男'}]}) --> [代码] 字段操作符 [代码]<!-- 1.字段操作符 $exists: 查询包含某个字段的文档 {<field>: {$exists: <boolean>}} $type: 查询指定字段包含指定类型的文档 {<field>: {$type: <BSON> or [<BSON1>, <BSON2>]}} 2.查询包含字段gender的人 db.person.insert([ {name:'zs', age:17, gender:'男'}, {name:'ls', age:18}, {name:'ww', age:19}, {name:'it666', age:20, gender:'女'} ]) // 需求: 要求查询出所有拥有gender属性的文档 db.person.find({gender:{$exists: true}}) 3.应用场景: // 应用场景: 配合$ne/$nin/$nor/$not来清理数据 db.person.find({gender:{$ne:'男'}}) db.person.find({gender:{$ne:'男', $exists:true}}) 4.查询所有年龄是字符串类型的文档 db.person.insert([ {name:'itzb', age:'666'}, {name:'lnj', age:'888'}, ]) // 需求: 要求查询出所有age属性的取值是字符串类型的文档 db.person.find({age:{$type:'string'}}) --> [代码] 数组操作符 [代码]<!-- 1.数组操作符 $all : 匹配数组中包含所有指定查询值的文档 {<field>: {$all: [<value1>, <value2>, ...]}} $elemMatch: 匹配数组中至少有一个能完全匹配所有的查询条件的文档 {<field>: {$elemMatch: {<query1>, <query2>, ...}}} 2.示例 查询tags中同时拥有html和js的文档 db.person.insert([ {name: 'zs', tags:['html', 'js', 'vue']}, {name: 'ls', tags:['html', 'react', 'vue']}, {name: 'ww', tags:['html', 'node', 'js']}, ]) db.person.find({tags:{$all:['html', 'js']}}) 查询所有名称叫做zs,年龄是18岁的文档 db.school.insert([ {class: 'one', students: [ {name:'zs', age: 18}, {name:'ls', age: 19}, {name:'ww', age: 20}, ]}, {class: 'two', students: [ {name:'zs', age: 20}, {name:'ls', age: 19}, {name:'ww', age: 18}, ]}, ]) db.school.find({'studnets.name':'ww', 'studnets.age':18}) db.school.find({studnets:{$elemMatch:{name:'ww',age:18}}}) --> [代码] 运算操作符:正则表达式 [代码]<!-- 1.运算操作符 { <field>: { $regex: /pattern/, $options: '<options>' } } { <field>: { $regex: /pattern/<options> } } 查询满足正则的文档 2.示例 db.person.insert([ {name:'zs', age:18}, {name:'ls', age:19}, {name:'ww', age:17}, {name:'Zsf', age:18}, {name:'Lnj', age:19}, {name:'Wz', age:17} ]) // 需求: 要求查询出所有姓z的人(文档) db.person.find({name:{$regex:/^z/, $options: 'i'}}) // 需求: 要求查询出所有姓是z或者l的人(文档) db.person.find({name:{$in:[/^z/i, /^l/i]}}) --> [代码] 文档游标 [代码]<!-- 1.文档游标 1.1为什么学习前端都要学习MongoDB? 因为MongoDB原生就支持JavaScript, 也就是我们可以直接在MongoDB中混入JS代码 1.2什么是文档游标 我们执行find方法后, find方法其实是有返回值的, find方法会返回一个文档游标(相当于C语言指针) 1.3文档游标常用方法 hasNext(): 是否还有下一个文档 next(): 取出下一个文档 forEach(): 依次取出所有文档 1.4文档游标注意点 默认情况下通过文档游标遍历完所有文档后, 系统会在10分钟后自动关闭当前游标 如果不想自动关闭, 我们可以通过noCursorTimeout函数来保持游标一直有效 let cursor = db.person.find().noCursorTimeout() 如果想手动关闭游标, 我们也可以通过close函数来手动关闭游标 cursor.close() 2.示例 // 需求: 往person集合中插入100个文档 var arr =[]; for(var i = 0; i < 100; i++){ arr.push({name:'it'+i, age:18+i}); } db.person.insertMany(arr) var cursor = db.person.find().noCursorTimeout() //cursor[0] //cursor[1] while(cursor.hasNext()){ printjson(cursor.next()) } cursor.forEach(printjson) cursor.close() --> [代码] 分页方法 [代码]<!-- 1.分页方法 cursor.limit(<number>): 取多少个文档 cursor.skip(<offset>) : 跳过多少个文档 2.示例 //var cursor = db.person.find() // 需求: 要求取出前5个文档 //cursor.limit(5) // 需求: 要求跳过前面的5个文档, 取出剩余的所有 //cursor.skip(5) // 注意点: 我们可以直接在find方法后面调用limit方法或者skip方法 //db.person.find().limit(5) //db.person.find().skip(5) 3.分页函数注意点 // 注意点: MongoDB是支持链式调用的 // 需求: 跳过前面5个文档, 取出后面的5个文档 //db.person.find().skip(5).limit(5) // 注意点:在链式调用的时候, 无论skip写在前面还是后面, 都会在limit之前执行 db.person.find().limit(5).skip(10) --> [代码] 排序函数 [代码]<!-- 1.排序函数 cursor.sort({field: ordering, ...}): 按照指定规则排序 ordering为1表示升序排序 ordering为-1表示降序排序 2.示例 // 注意点: 默认情况下find方法只会返回100个文档 db.person.find() db.person.insert({name:'itzb', age:15}) db.person.find().limit(101) db.person.find().sort({age:1}) db.person.find().sort({age:-1}) 3.注意点 3.1find方法默认只会取出100个文档 3.2sort函数永远在分页函数之前执行 db.person.find().skip(5).limit(5) db.person.find().skip(5).limit(5).sort({age:-1}) --> [代码] 统计函数 [代码]<!-- 1.统计函数 cursor.count(<applySkipLimit>): 统计集合中文档的个数 applySkipLimit默认为false, 表示忽略skip和limit 2.示例 db.person.find().count() // 注意点: count函数可以接收一个applySkipLimit参数, 通过这个参数可以告诉MongoDB在统计的时候是否需要忽略Skip和Limit // 默认情况下applySkipLimit的取值是false, 表示忽略Skip和Limit db.person.find().skip(6).count() db.person.find().limit(5).count() db.person.find().skip(6).count({applySkipLimit:true}) db.person.find().limit(5).count({applySkipLimit:true}) 2.统计函数注意点 在find函数不提供筛选条件时, count函数会从集合的元数据中取得结果 在单台电脑上是这个结果是准确的, 但是如果数据库为分布式结构(多台电脑)时, 如果不给find函数提供筛选条件, 那么count函数返回的结果并不一定准确 --> [代码] 更新文档 save方法 [代码]<!-- 1.更新文档 MongoDB中有三个常用的更新方法: save()/update()/findAndmodify() 2.save方法 save用于往集合里添加一个新文档或者覆盖文档 当没有指定文档_id的时候就是新增 当指定了集合中已经存在的_id的时候就是覆盖 3.示例 db.person.insert([ {name:'zs', age:18}, {name:'ls', age:19}, {name:'ww', age:20}, {name:'zs', age:21}, ]) --> [代码] update方法 [代码]<!-- 1.update方法 db.collection.update(<filter>, <update>, <options>) <filter>: 筛选条件 <update>: 新的内容 <options>: 额外配置 2.通过update覆盖满足条件数据 默认情况下如果<update>没有使用更新操作符, 那么就会使用指定的内容覆盖符合条件的内容 3.示例: db.person.update({name:'lnj'}, {name:'zs'}) 4.注意点: // 注意点: update方法默认情况下就是覆盖 // 如果不想覆盖, 而是想单纯的更新, 那么就必须在第二个参数中使用'更新操作符' db.person.update({name:'ww'},{score: 99.9},{}) // 注意点: update方法默认只会更新满足条件的第一个文档 // 如果想更新所有满足条件的文档, 那么就必须指定第三个参数 db.person.update({name:'zs'}, {name:'zs', age:55}, {}) // 注意点: 如果在使用update方法的时候, 在第二个参数中指定了_id, 那么就必须保证指定的_id和被更新的文档的_id的取值一致 // 否则就无法更新, 否则就会报错 // 开发技巧: 在企业开发中如果需要使用update方法, 那么就不要指定_id db.person.update({name:'zs'}, {_id:1, name:'zs', age:55}, {}) db.person.update({name:'zs'}, {_id:ObjectId("5e9007350718cb6e37ab4515"), name:'zs', age:88}, {}) // 注意点: 如果想更新所有满足条件的文档, 我们可以指定第三个参数的取值multi:true // 注意点: 如果指定了multi:true, 那么就必须在第二个参数中使用'更新操作符' db.person.update({name:'zs'}, {name:'zs', age:55}, {multi:true}) --> [代码] $set更新操作符 [代码]<!-- 1.更新操作符 默认情况下update会使用新文档覆盖旧文档 如果不想覆盖而是仅仅想更新其中的某些字段 那么我们就需要使用update的更新操作符 2.$set更新操作符 $set: 更新或者新增字段, 字段存在就是更新, 字段不存在就是新增 格式: {$set:<value1>, ...} 3.示例: db.person.update({name:'zs'}, {$set:{name:'itzb'}}) db.person.update({name:'itzb'}, {$set:{age:'888'}}) 4.更新内嵌文档和数组 db.person.insert([ {name:'zs', age:18}, {name:'ls', age:19}, {name:'ww', age:20}, {name:'zs', age:21}, ]) db.person.update({name:'ww'}, {age:55}) // 更新普通字段 db.person.update({name:'ls'}, {$set:{age:55}}) db.person.update({name:'zs'}, {$set:{age:88}}, {multi:true}) db.person.insert( { name:'ww', age:18, book:{name:'跟着江哥学编程', price:2888}, tags:['html', 'JavaScript']} ) // 更新文档字段 db.person.update({name:'ww'}, {$set:{'book.name': 'it666.com'}}) // 更新数组字段 db.person.update({name:'ww'}, {$set: {'tags.0': 'vue'}}) 5.注意点: // 注意点: 如果操作的字段存在, 那么就是更新, 如果操作的字段不存在, 那么就是新增 db.person.update({name:'ls'}, {$set:{score: 59.5}}) // 注意点: 如果操作的是数组字段, 如果操作索引不存在, 那么也会自动新增 // 如果被操作的索引前面没有数据, 那么会自动用null来填充 db.person.update({name:'ww'}, {$set: {'tags.2': 'react'}}) db.person.update({name:'ww'}, {$set: {'tags.5': 'node'}}) --> [代码] $unset更新操作符 [代码]<!-- 1.$unset更新操作符 $unset: 删除字段 格式 :{$unset:{<field>:'', ...}} 2.示例: // 删除普通字段 db.person.update({name:'ls'}, {$unset:{score:''}}) // 注意点: 如果使用$unset删除某一个字段, 那么后面赋值为任何的内容都不重要 db.person.update({name:'ls'}, {$unset:{age:'www.it666.com'}}) // 删除文档字段中的字段 db.person.update({name:'ww'}, {$unset:{'book.price': ''}}) // 删除数组字段中的元素 // 注意点: 如果删除的是数组字段中的元素, 那么并不会修改数组的长度, 而是用null来填充删除的内容 db.person.update({name:'ww'}, {$unset:{'tags.1': ''}}) 3.注意点: 3.1删除数组元素并不会影响数组的长度, 而是设置为Null 3.2如果删除的字段不存在, 不会做任何操作 --> [代码] $rename重命名操作 [代码]<!-- 1.$rename更新操作符 $rename: 重命名字段 格式 :{$rename:{<field>:<newName>, ...}} 2.示例 db.person.update({name:'zs'}, {$rename:{name:'MyName'}}) // 注意点: 如果修改的是文档字段中的字段, 那么取值必须写上层级关系 db.person.update({name:'ww'}, {$rename:{'book.name':'book.BookName'}}) // 注意点: 如果要操作的字段不存在, 那么不会做任何的操作 db.person.update({name:'ls'}, {$rename:{age:'MyAge'}}) // 注意点: 如果重命名之后的名称已经存在了, 那么已经存在的字段就会被删除 // 底层的本质: 先调用了$unset删除了原有的book字段, 然后再调用$set修改字段的名称 db.person.update({name:'ww'}, {$rename:{name:'book'}}) // 注意点: 不能通过$rename更新操作符来操作数组 db.person.insert( { name:'it666', age:666, book:{name:'知播渔', price:999}, tags:[{name:'html', price:'123'}, {name:'js', price:456}] } ) db.person.update({name:'it666'}, {$rename:{'tags.0.name':'tags.0.TagName'}}) 4.乾坤大挪移 // 可以将外层的字段转移到内层的文档中 db.person.update({name:'it666'}, {$rename:{age:'book.age'}}) db.person.find() // 可以将内存文档中的字段, 转移到外层文档中 db.person.update({name:'it666'}, {$rename:{'book.age':'age'}}) --> [代码] $inc$mul更新操作符 [代码]<!-- 1.$inc和$mul更新操作符 $inc:更新字段值(增加或者减少字段保存的值) 格式: {$inc:{<field>: <number>}} $mul:更新字段值(乘以或者除以字段保存的值) 格式: {$mul:{<field>: <number>}} 2.示例 db.person.update({name:'lnj'}, {$inc:{age:2}}) db.person.update({name:'lnj'}, {$inc:{age:-5}}) db.person.update({name:'lnj'}, {$mul:{age:0.5}}) db.person.update({name:'lnj'}, {$mul:{age:2}}) 3.注意点: 3.1只能操作数字类型字段 3.2如果操作的字段不存在, 会自动新增这个字段 不同的是$inc会把操作的值赋值给新增的字段, 而$mul会自动赋值为0 db.person.update({name:'lnj'}, {$inc:{weight:2}}) db.person.update({name:'lnj'}, {$mul:{height:2}}) --> [代码] $max$min更新操作符 [代码]<!-- 1.$min和$max更新操作符 $min:比较保留更小字段值 格式: {$min:{<field>: <value>}} $max:比较保留更大字段值 格式: {$max:{<field>: <value>}} 2.示例 db.person.insert({name:'lnj', age:33}) db.person.update({name:'lnj'}, {$min:{age:50}}) db.person.update({name:'lnj'}, {$min:{age:18}}) db.person.update({name:'lnj'}, {$max:{age:5}}) db.person.update({name:'lnj'}, {$max:{age:55}}) 3.注意点: // 注意点: 如果操作的字段不存在, 那么会自动增加, 并且会将操作的值赋值给新增的字段 db.person.update({name:'lnj'}, {$min:{weight:120}}) db.person.update({name:'lnj'}, {$max:{height:175}}) // 注意点: 和$inc/$mul不同, $min/$max不仅仅能操作数值类型的字段, 只要是可以比较的字段都可以操作 db.person.insert({name:'def', age:666}) db.person.update({name:'def'}, {$min:{name:'efg'}}) db.person.update({name:'def'}, {$min:{name:'cde'}}) // 注意点: 不是相同的数据类型也可以进行比较 db.person.update({name:'lnj'}, {$min:{age:null}}) MongoDB对BSON的数据类型有一个潜在的排序规则 Null Numbers(ints, longs, doubles, decimals) Symbol, String Object Array BinData ObjectId Boolean Date Timestamp Regular Expression --> [代码] $addToSet数组更新操作符 [代码]<!-- 1.$addToSet数组更新操作符 $addToSet: 向数组字段中添加元素 格式 : {$addToSet: {<field>:<values>, ...}} 2.示例 db.person.insert([ {name:'zs', books:[{name:'html', price:66}, {name:'js', price:88}], tags:['html', 'js']}, {name:'ls', books:[{name:'vue', price:99}, {name:'node', price:199}], tags:['vue', 'node']} ]) db.person.update({name:'zs'}, {$addToSet:{tags:'react'}}) 3.注意点 // 注意点:如果操作的元素不存在, 那么会自动新增, 并且将操作的值赋值给新增的数组字段 db.person.update({name:'zs'}, {$addToSet:{other:'123'}}) // 注意点: $addToSet会自动去重, 如果添加的元素已经存在了, 那么就不会添加了 db.person.update({name:'zs'}, {$addToSet:{other:'123'}}) // 注意点: 如果往数组字段中添加的是文档类型, 那么必须一模一样才会去重 db.person.update({name:'zs'}, {$addToSet:{books:{name:'html', price:66}}}) db.person.update({name:'zs'}, {$addToSet:{books:{price:66, name:'html'}}}) // 注意点: 如果往数组字段中添加的是数组, 那么也必须一模一样才会去重 db.person.update({name:'ls'}, {$addToSet:{tags:['1', '2']}}) db.person.update({name:'ls'}, {$addToSet:{tags:['1', '2']}}) db.person.update({name:'ls'}, {$addToSet:{tags:['2', '1']}}) // 注意点: 如果往往数组字段中添加的是数组, 那么默认情况下会将整个数组作为一个元素添加进去 // 如果不想诶一个整体添加进去,那么必须使用$each来添加 db.person.update({name:'ls'}, {$addToSet:{tags:{$each: ['1', '2', '3']}}}) --> [代码] $push更新操作符 [代码]<!-- 1.$push数组更新操作符 $push: 向数组字段中添加元素(不去重) 格式 : {$push: {<field>:<value1>, ...}} --> <!-- 1.$pop数组更新操作符 $pop: 从数组字段中删除元素 格式: {$pop: {<field>:<1|-1>, ...}} 2.示例 db.person.update({name:'zs'}, {$pop:{tags:1}}) #删除最后一个 db.person.update({name:'zs'}, {$pop:{tags:-1}})#删除第一个 3.注意点 数组中的元素都被删除以后, 仍然会保留空的数组 --> [代码] $pull更新操作符 [代码]<!-- 1.$pull数组更新操作符 $pull: 从数组字段中删除特定元素 格式: {$pull: {<field>:<value|condition>, ...}} 2.示例 db.person.insert([ {name:'zs', books:[{name:'html', price:66}, {name:'js', price:88}], tags:['html', 'js', ['1', '2']]}, {name:'ls', books:[{name:'vue', price:99}, {name:'node', price:199}], tags:['a', 'b', 'ab', 'c', 'ac']} ]) 删除特定元素 根据条件删除元素 db.person.update({name:'zs'}, {$pull:{tags:'js'}}) db.person.update({name:'ls'}, {$pull:{tags:/^a/}}) 3.注意点 // 注意点: 如果要删除的元素是一个数组, 那么必须一模一样才能删除 db.person.update({name:'zs'}, {$pull:{tags:['2', '1']}}) db.person.update({name:'zs'}, {$pull:{tags:['1', '2']}}) // 注意点: 如果要删除的元素是一个文档, 那么不用一模一样也可以删除 db.person.update({name:'zs'}, {$pull:{books:{price:66, name:'html'}}}) db.person.update({name:'zs'}, {$pull:{books:{name:'js'}}}) --> [代码] $pullAll更新操作符 [代码]<!-- 1.$pullAll数组更新操作符 $pullAll: 从数组字段中批量删除特定元素 格式: {$pullAll: {<field>: [<value1>, <value2>, ...], ...}} 2.示例 db.person.insert([ {name:'zs', books:[{name:'html', price:66}, {name:'js', price:88}], tags:['html', 'js', ['1', '2']]}, {name:'ls', books:[{name:'vue', price:99}, {name:'node', price:199}], tags:['a', 'b', 'ab', 'c', 'ac']} ]) db.person.update({name:'zs'}, {$pullAll:{tags:['html', 'js']}}) 3.注意点 // 注意点: 和$pull一样, 如果删除的是数字字段中的数组元素, 那么必须一模一样才能删除 db.person.update({name:'zs'}, {$pullAll:{tags:[['2','1']]}}) db.person.update({name:'zs'}, {$pullAll:{tags:[['1','2']]}}) // 注意点: 和$pull不一样, 如果删除的是数组字段中的文档元素, 那么也必须一模一样才能删除 db.person.update({name:'zs'}, {$pullAll:{books:[{price:66,name:'html'}]}}) db.person.update({name:'zs'}, {$pullAll:{books:[{name:'html',price:66}]}}) --> [代码] $和$[]更新操作符 [代码]<!-- 1.$和$[]数组更新操作符 $ : 更新数组中满足条件的特定元素 格式: db.<collection>.update( { <array field>:<query selector> } { <update operator>: {'<array field>.$':value}} ) $[]: 更新数组中所有元素 db.<collection>.update( { <update operator>: {'<array field>.$[]':value}} ) 2.示例 db.person.insert([ {name:'zs', books:[{name:'html', price:66}, {name:'js', price:88}], tags:['html', 'js', ['1', '2']]}, {name:'ls', books:[{name:'vue', price:99}, {name:'node', price:199}], tags:['a', 'b', 'ab', 'c', 'ac']} ]) db.person.find() db.person.update( {name:'zs', tags:'js'}, {$set:{'tags.$':'JavaScript'}} ) db.person.update( {name:'zs'}, {$set:{'tags.$[]': 'it666'}} ) --> [代码] 删除文档 [代码]<!-- 1.删除文档 db.<collection>.remove(<query>, <options>) <query>: 删除筛选条件 <options>: 删除额外配置 2.示例 db.person.insert([ {name:'zs', age:18}, {name:'zs', age:19}, {name:'ls', age:20}, {name:'ls', age:21}, {name:'ww', age:22}, {name:'zl', age:23}, ]) 2.示例 //2.1删除所有满足条件 // 注意点: 和update方法不同, remove方法默认就会删除所有满足条件的数据 db.person.remove({name:'zs'}) //2.2删除第一个满足条件 db.person.remove({name:'ls'},{justOne:true}) //2.3删除所有文档 db.person.remove({}) --> [代码] 聚合操作 $project [代码]<!-- 1.什么是聚合操作? - 聚合操作就是通过一个方法完成一系列的操作 - 在聚合操作中, 每一个操作我们称之为一个阶段, 聚合操作会将上一个阶段处理结果传给下一个阶段继续处理, 所有阶段都处理完毕会返回一个新的结果集给我们 2.聚合操作格式 db.<collection>.aggregate(<pipeline>, <options>) <pipeline>: 定义每个阶段操作 <options> : 聚合操作额外配置 3.聚合管道阶段 $project: 对输入文档进行再次投影 作用 : 按照我们需要的格式生成结果集 格式 : {$project:{<field>:<value>}} 4.示例 db.person.insert([ {name:{firstName:'Jonathan', lastName:'Lee'}, age:18, book:{name:'玩转HTML', price: 88}}, {name:{firstName:'Amelie', lastName:'Jiang'}, age:17, book:{name:'玩转JavaScript', price: 99}} ]) db.person.aggregate([ { $project:{ _id:0, clientName: '$name.firstName', clientAge: '$age' } } ]) 5.聚合表达式 5.1字段路径表达式 $<filed>: 使用$来指示字段路径 $<filed>.<sub-field>: 使用$和.来指示内嵌文档字段路径 5.2字段路径表达式示例 $name $book.name 6.注意点: // 注意点: $project修改的是结果集而不是原有的集合 db.person.find() // 注意点: 如果字段表达式使用了不存在的字段, 那么会自动用Null填充 db.person.aggregate([ { $project:{ _id:0, fullName: ['$name.firstName', '$name.middleName','$name.lastName'], clientAge: '$age' } } ]) --> [代码] $match [代码]<!-- 1.聚合管道阶段 $match: 和find方法中的第一个参数一样, 用于筛选符合条件的文档 格式 : {$match:{<query>}} 2.示例 db.person.aggregate([ { $match:{ 'name.firstName':'Jonathan' } } ]) db.person.aggregate([ { $match:{ 'name.firstName':'Jonathan' } }, { $project:{ _id:0, clientName: '$name.firstName', clientAge: '$age' } } ]) 3.使用技巧: 应该在聚合操作的最前面使用$match, 这样可以有效减少处理文档的数量, 大大提升处理的效率 --> [代码] $limit和$skip [代码]<!-- 1.聚合管道阶段 $limit: 和游标的limit方法一样, 用于指定获取几个文档 格式 : {$limit:<number>} $skip : 和游标的skip方法一样, 用于指定跳过几个文档 格式 : {$skip:<number>} 2.示例 db.person.aggregate([ { $skip:1 }, { $limit:1 }, { $project:{ _id:0, clientName: '$name.firstName', clientAge: '$age' } } ]) --> [代码] $unwind [代码]<!-- 1.聚合管道阶段 $unwind: 展开数组字段 格式 : {$unwind:{path:<field>}} 2.示例: db.person.update({'name.firstName':'Jonathan'}, {$set:{tags:['html', 'js']}}) db.person.update({'name.firstName':'Amelie'}, {$set:{tags:'vue'}}) db.person.aggregate([ { $unwind:{ path:'$tags' } } ]) 3.注意点: 3.1$unwind会为数组中的每个元素创建一个新的文档 3.2可以通过includeArrayIndex属性添加展开之后的元素在原数组中的位置 db.person.aggregate([ { $unwind:{ path:'$tags', includeArrayIndex: 'index' } } ]) 3.3如果需要展开的字段不存在, 或者数组中没有元素, 或者为null, 会被unwind剔除 db.person.insert([ {name:{firstName:'san', lastName:'zhang'}, age:20}, {name:{firstName:'si', lastName:'li'}, age:21, tags:[]}, {name:{firstName:'wu', lastName:'wang'}, age:22, tags:null} ]) 3.4如果想让unwind不剔除不存在/没有元素/为Null的文档, 那么可以添加preserveNullAndEmptyArrays属性 db.person.aggregate([ { $unwind:{ path:'$tags', includeArrayIndex: 'index', preserveNullAndEmptyArrays: true } } ]) --> [代码] $sort [代码]<!-- 1.聚合管道阶段 $sort: 和文档游标sort方法一样, 对文档进行排序 格式 : {$sort:{<field>>:1|-1}} 2.示例 db.person.aggregate([ { $sort:{ age: 1 } } ]) --> [代码] $lookup [代码]<!-- 1.聚合管道阶段 $lookup: 用来做关联查询 格式 : {$lookup:{ from: 关联集合名称, localField: 当前集合中的字段名称, foreignField:关联集合中的字段名称, as: 输出字段的名称 }} 2.示例: db.person.insert([ {name:{firstName:'Jonathan', lastName:'Lee'}, age:18, books:['html', 'js']}, {name:{firstName:'Amelie', lastName:'Jiang'}, age:19, books:['vue']}, {name:{firstName:'si', lastName:'Li'}, age:20, books:[]} ]) db.books.insert([ {name:'html', price:88}, {name:'js', price:99}, {name:'vue', price:110}, ]) db.person.aggregate([ { $lookup:{ from: 'books', localField: 'books', foreignField: 'name', as: 'booksData' } } ]) 3.和unwind阶段结合使用 可以有效的过滤掉无效数据 可以给每个匹配的结果生成一个新的文档 db.person.aggregate([ { $unwind:{ path:'$books' } }, { $lookup:{ from: 'books', localField: 'books', foreignField: 'name', as: 'booksData' } } ]) --> [代码] $lookup相关查询 [代码]<!-- 1.聚合管道阶段 $lookup: 用来做关联查询 格式 : {$lookup:{ from: 关联集合名称, let: {定义给关联集合的聚合操作使用的当前集合的常量}, pipeline: [关联集合的聚合操作] as: 输出字段的名称 }} 2.示例: 不相关查询 db.person.aggregate([ { $lookup:{ from: 'books', pipeline: [ { $match:{ price:{$gte:100} } } ], as: 'booksData' } } ]) 相关查询 db.person.aggregate([ { $lookup:{ from: 'books', let: { bks: '$books'}, pipeline: [ { $match:{ $expr:{ $and:[ {$gte: ['$price', 100]}, {$in: ['$name', '$$bks']} ] } //price:{$gte:100} } } ], as: 'booksData' } } ]) 3系统变量表达式 $$<variable>: 使用$$来指示系统变量 --> [代码] $group [代码]<!-- 1.聚合管道阶段 $group: 对文档进行分组 格式 : {$group:{ _id:<expression>, <field1>: {<accumulator1>: <expression1>} ... ... }} _id: 定义分组规则 <field>: 定义新字段 2.示例 db.person.insert([ {name:'zs', age:10, city:'北京'}, {name:'ls', age:20, city:'上海'}, {name:'ww', age:30, city:'北京'}, {name:'zl', age:40, city:'上海'}, {name:'lnj', age:50, city:'北京'}, {name:'jjj', age:60, city:'广州'}, ]) db.person.aggregate([ {$group:{ _id:'$city', totalAge:{$sum:'$age'}, avgAge:{$avg:'$age'}, minAge:{$min:'$age'}, maxAge:{$max:'$age'}, totalName:{$push:'$name'} }} ]) --> [代码] $out [代码]<!-- 1.聚合管道阶段 $out: 前面阶段处理完的文档写入一个新的集合 格式: {$out: <new collection name>} 2.示例: db.person.aggregate([ { $group:{ _id: '$city', totalAge: {$sum:'$age'}, avgAge: {$avg: '$age'}, minAge: {$min: '$age'}, maxAge: {$max: '$age'}, totalAges: {$push: '$age'} } }, { $out:'newPerson' } ]) db.newPerson.find() 3.注意点: 如果利用$out写入一个已经存在的集合, 那么集合中的原有数据会被覆盖 --> [代码] 配置 [代码]<!-- 1.聚合操作额外配置 db.<collection>.aggregate(<pipeline>, <options>) 格式: {allowDiskUse: <boolean>} allowDiskUse默认取值是false, 默认情况下管道阶段占用的内存不能超过100M,如果超出100M就会报错 如果需要处理的数据比较多, 聚合操作使用的内存可能超过100M, 那么我们可以将allowDiskUse设置为true 如果allowDiskUse设置为true, 那么一旦超出100M就会将操作的数据写入到临时文件中, 然后再继续操作 --> <!-- 1.字段路径表达式 $<filed>: 使用$来指示字段路径 $<filed>.<sub-field>: 使用$和.来指示内嵌文档字段路径 2.示例 $name $book.name 3.系统变量表达式 $$CURRENT: 表示当前操作的文档 4.示例 $$CURRENT.name 等价于 $name 5.常量表达式 $literal:<value> : 表示常量<value> 6.示例 $literal:'$name' : 表示常量字符串$name db.person.insert([ {name:{firstName:'Jonathan', lastName:'Lee'}, age:18}, {name:{firstName:'Amelie', lastName:'Jiang'}, age:19} ]) db.person.find() db.person.aggregate([ {$project:{ _id:0, //myName:'$name.firstName', // 字段路径表达式 //myAge:'$age' // 字段路径表达式 //myName:'$$CURRENT.name.firstName', //系统变量表达式 //myAge:'$$CURRENT.age' // 系统变量表达式 myName:'$name.firstName', myAge:{$literal:'$age'} // 常量表达式 }} ]) --> [代码] 数据类型转换 [代码]<!-- 1.数据类型转换操作符 - MongoDB对于文档的格式并没有强制性的要求, 同一个集合中存储的文档, 字段的个数和数据类型都可以不同 对与文档的格式没有强制性的要求是MongoDB的一大优势, 但是同时也增加了数据消费端的使用难度 因为我们在使用数据的时候, 有可能同一个字段取出来的数据类型是不同的, 这样非常不利于我们后续操作 所以也正是因为如此, MongoDB在4.0中推出了$convert数据类型转换操作符 - 通过$convert数据类型转换操作符, 我们可以将不同的数据类型转换成相同的数据类型, 以便于后续我们在使用数据的过程中能够统一对数据进行处理 2.$convert格式 {$convert:{ input: '需要转换的字段', to: '转换之后的数据类型', onError: '不支持的转换类型', onNull: '没有需要转换的数据' }} 3.示例 db.person.insert([ {name:'zs', timestamp:ISODate('2020-08-09T11:23:34.733Z')}, {name:'ls', timestamp:'2021-02-14 12:00:06 +0800 '}, {name:'ww', timestamp:' 2023-04-01T12:00:00Z'}, {name:'zl', timestamp:'1587009270000'}, {name:'it666', timestamp:'Sunday'}, {name:'itzb'}, ]) db.person.aggregate([ {$project:{ _id:0, timestamp:{ $convert:{ input:'$timestamp', to:'date', onError: '不支持的转换类型', onNull: '没有需要转换的数据' } } }} ]) --> [代码]
2021-11-17 - MYSQL笔记
创建数据库 指令规范 注意:从现在开始将会频繁使用mysql指令,需要遵守一些指令规范 除use database指令外,其他指令都需要在结尾加上分号’;’ 使用英文的标点符号 数据库命名规则一般以小写英语字母和_组成 数据表命名是小写字母和下划线 [代码]_[代码] 组成,用来分割不同单词之间的含义 各功能指令 查看数据库 [代码]>show databases; [代码] 系统会显示当前已经存在四个MySQL系统数据库,这些数据库用来存储和管理 MySQL 服务相关的一些配置。 新建数据库 [代码]>create database item_name; [代码] 也可以利用客户端工具新建数据库 此处略. 删除数据库 在登录状态下,输入指令: [代码]>drop database item_name; [代码] 选中数据库 输入选择数据库指令: [代码]>use item_name; [代码] 选中数据后才能对数据库进行操作,当想选中另一个数据库时,直接输入use database_name即可 设计数据表 一个数据表主要包含信息有 : 表名、主键、字段、数据类型、索引 数据表是在数据中进行操作的,所以进行数据表操作时,需要先进入相对应的数据库 字段 以数据表student为例: 字段名称 数据类型 含义 id 无符号整型 (unsigned int) 自增子健 name varchar(50) 学生姓名 age unsigned int 学生年龄 id_number varchar(18) 身份证号 主键 每一张表都必须有一个主键,一般建议选定为无符号整型 id 作为主键,并且 id 一般作为主键一般设置为自增的(特殊情况可使用其他非自增 id 作为主键) 新建数据表 学生数据表 [代码]create table `student` ( `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL DEFAULT '无名', `age` int(10) UNSIGNED NOT NULL DEFAULT 0, `id_number` varchar(18) NOT NULL DEFAULT '', PRIMARY KEY (`id`) ); [代码] 注:其中 “student” 为表名称,“id”、“name”、“age”、“id_number” 为字段名称,跟在字段名称后面的是字段的数据类型,“UNSIGNED” 表示无符号,“AUTO_INCREMENT” 表示自增,"PRIMARY KEY ([代码]id[代码])"表示设置 “id” 为业务主键,,"NOT NULL DEFAULT ‘无名’ " 表示默认不为空,且默认值为 “无名” 。 同理: 教师数据表 [代码]create table `teacher` ( `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL DEFAULT '教师名', `age` int(10) UNSIGNED NOT NULL DEFAULT 0, `id_number` varchar(18) NOT NULL DEFAULT '', PRIMARY KEY (`id`) ); [代码] 课程表 [代码]create table `course` ( `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, `course_name` varchar(50) NOT NULL DEFAULT '', `teacher_id` int(10) UNSIGNED NOT NULL DEFAULT 0, PRIMARY KEY (`id`) ); [代码] 学生选课关联表 [代码]create table `student_course` ( `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, `student_id` int(10) UNSIGNED NOT NULL DEFAULT 0, `course_id` int(10) UNSIGNED NOT NULL DEFAULT 0, PRIMARY KEY (`id`) ); [代码] 查看数据表 输入查看指令: [代码]show tables; [代码] 可查看到新建了四个数据表. 数据表设计规范 第一设计范式 第一设计范式要求表中字段都是不可再分的,如果实体中的某个属性有多个值时,必须拆分为不同的属性 。通俗理解即一个字段只存储一项信息. 第二设计范式 第二设计范式要求表中必须存在业务主键,并且全部非主键依赖于业务主键。 第三设计范式 一个数据库表中不包含已在其它表中已包含的非主键字段。就是说表的信息如果能够被推导出来,就不应该单独的设计一个字段来存放(能尽量外键 join 就用外键 join)。 反范式设计 没有冗余的数据库未必是最好的数据库,有时为了提高运行效率,提高读性能,就必须降低范式标准,适当保留冗余数据。具体做法是: 在概念数据模型设计时遵守第三范式,降低范式标准的工作放到物理数据模型设计时考虑。降低范式就是增加字段,减少了查询时的关联,提高查询效率,因为在数据库的操作中查询的比例要远远大于 DML 的比例。 alter 命令修改表 修改数据表名称 [代码]alter table 旧的表名 rename to 新的表名; [代码] 查看建表sql语句 [代码]show create table 需要查看的表名; [代码] 修改建表sql语句 [代码]alter table `表名` modify column `字段1` 数据类型 新字段类型的字符集和编排方式 not NULL default '无名' after `字段2`; [代码] 新增表字段 [代码]alter table `表名` add column `新字段` 数据类型 not NULL default 1 comment '字段名 : 1:数据一 2:数据二' alter `字段2`; [代码] 其中comment '字段名 : 1:数据一 2:数据二’表示该字段的注释说明 删除表字段 [代码]alter table `表名` drop column `字段`; [代码] 修改表字段名称 [代码]alter table `表名` change column `旧字段名` `新字段名` 数据类型 编码方式 [代码] 删除数据表 先选中表所在的数据库,然后输入删除指令: [代码]use 数据库; drop table 表名; [代码] insert 插入数据 [代码]insert into 表名 (字段_1,字段_2,字段_3...) values (数据,数据,数据...), (数据,数据,数据...), ... (数据,数据,数据...); [代码] 通过使用真实数据替换上面指令,可进行一条至多条数据的插入. delete删除数据 删除部分数据 [代码]delete from 表名 where 字段=1(假设为1); [代码] 选中数据库,然后输入上面指令表明:删除所选表中,所选字段对应数据为1的数据. 删除表全部数据 delete [代码]delete from 表名; [代码] truncate [代码]truncate table 表名; [代码] 注: TRUNCATE 清空表数据的实际过程是先删除数据表,然后新建一张和原来表结构一模一样的表来替代清空。 DELETE 删除表数据不会改变自增主键的增长值 select查询数据 查询表中所有的数据 [代码]select * from 表名; [代码] 查询指定条数的结果集 [代码]select * from 表名 limit 10(假设为10); [代码] 输入上述指令将呈现10条结果的结果集. 查询指定其实条数的结果集 [代码]select * from 表名 limit 10,10;(假设为10,10); [代码] 将输出11-20的十条结果的结果集. 查询指定字段列的结果集 输入以下指令: [代码]select 字段1,字段2 from 表名 limit 6,5; [代码] 给指定字段重命名 [代码]select 字段1 as 字段3,字段2 from 表名 limlt 6,5; [代码] 注:该重命名仅仅使用呈现结果,而不会保存到原数据表 update更新数据 更新某一列字段的值 输入下列指令: [代码]update 表名 set 字段1=数据1 limit 3; [代码] 即可将前三条数据中字段1的数据修改为 数据1. 更新多段字段的值 [代码]update 表名 set 字段1=数据1,字段2=数据2 where 某字段=某一数据; [代码] 即可进行修改. like模糊查询 模糊查询表达式 [代码]%[代码] 表示指代任意内容,例如 [代码]'%小%'[代码] 表示包含 [代码]小[代码] 的表达式,且 [代码]小[代码] 前后都有内容, [代码]'%小'[代码] 表示以 [代码]小[代码] 结尾的表达式,[代码]王[代码] 前面有内容,后面没有内容,[代码]'小%'[代码] 表示以 [代码]小[代码] 开头的表达式,[代码]小[代码] 前面没有内容,后面有内容。 使用like模糊查询 输入下面指令: [代码]select * from 表名 where 某字段 like '小%'; [代码] 即可查看该字段对应的字符串最右边的字符为’小’的结果. where条件查询 where条件 符号 说明 < 小于 = 等于 > 大于 <>或!= 不等于 <= 小于等于 >= 大于等于 like 模糊条件 not like 不符合模糊条件 between and 在两个值之间(包含两端值) not between and 不在在两个值之间(不包含两端值) is null 空 is not null 不为空 单条件查询 [代码]select * from 表名 where 条件1; [代码] and多条件查询 [代码]select * from 表名 where 条件1 and 条件2; [代码] and表示同时满足两个条件才为真. or多条件查询 [代码]select * from 表名 where 条件1 or 条件2; [代码] 只需要满足至少一个条件即为真 union联合查询 union all查询 输入以下指令即可把满足两种查询条件的结果集并到一起: [代码]select * from 表名 where 条件1 union all select * from 表名 where 条件2; [代码] 注: [代码]UNION ALL[代码] 联合查询的结果集没有去掉重复的数据. union查询 [代码]select * from 表名 where 条件1 union select * from 表名 where 条件2; [代码] 注:[代码]UNION[代码] 将两种查询结果并到一起,可以看到结果集中已经去掉重复的数据 order by排序 ASC 从小到大排序 输入以下指令: [代码]select * from 表名 order by 某字段 ASC; [代码] 即可根据该字段数据实现从小到大排序 DESC 从大到小排序 [代码]select * from 表名 order by 某字段 DESC; [代码] 多字段混合排序 [代码]select * from 表名 order by 字段1 DESC,字段2 ASC; [代码] 根据优先级是:先进行根据字段1的降序,在此基础上再进行根据字段2的升序. 对字符串排序 对字符串排序和对数值进行排序的区别在于:英文字符排序在前,中文在后,其中排序规则是按照字符 ASCII码 对应值的大小排序. join表连接 left左连接 输入以下指令: [代码]select a.原字段名 as 新字段名,a.*,b.* from 表一 a left join 表二 b on a.字段1=b.字段2; [代码] [代码]a.*[代码] 表示 [代码]表一[代码]所有字段数据; [代码]t.*[代码] 表示 [代码]表二[代码] 表字段所有数据; [代码]ON[代码] 后面跟着的条件是连接表的条件; [代码]表一 a[代码] 表示将 [代码]表一[代码] 简写为 [代码]a[代码], [代码]表二 b[代码] 表示将 [代码]表二[代码] 简写为 [代码]b[代码]; [代码]left join[代码] 为左连接,是以左边的表为’基准’,若右表没有对应的值,用 [代码]NULL[代码] 来填补。 同理: [代码]right join[代码] 为右连接,是以右边的表为’基准’,若左表没有对应的值,用 [代码]NULL[代码] 来填补。 当然,内连接有些许不同: [代码]INNER JOIN[代码] 为内连接,展示的是左右两表都有对应的数据。 多表混合连接 即一或多种连接方式的多次使用: [代码]select a.原字段名 as 新字段名,a.*,b.* from 表一 a left join 表二 b on a.字段1=b.字段2 right join 表三 c on b.字段2=c.字段3 inner join 表四 d on b.字段3=d.字段4; [代码] 该多表连接以表二的字段2为基准. distinct去重 输入下列指令: [代码]select distinct 字段1,字段2,...字段n from 表一 a inner join 表二 b on a.字段1=b.字段2; [代码] group by分组 单字段分组 输入以下指令: [代码]select a.原字段名 as 新字段名,a.*,b.* from 表一 a left join 表二 b on a.字段1=b.字段2 right join 表三 c on b.字段2=c.字段3 inner join 表四 d on b.字段3=d.字段4 group by 某字段; [代码] 即可完成单字段分组. 多字段分组 输入以下指令: [代码]select a.原字段名 as 新字段名,a.*,b.* from 表一 a left join 表二 b on a.字段1=b.字段2 right join 表三 c on b.字段2=c.字段3 inner join 表四 d on b.字段3=d.字段4 group by a.字段1,b.字段2; [代码] 即可完成所需分组. 单字段分组和多字段分组的区别在于,单字段是以一个字段来判断数据是否重复分组出来的结果,多字段分组是以多个字段同时来判断是否重复分组出来的结果。 聚合函数 函数 作用 avg() 计算平均值 sum() 计算总和 count() 计算总条数 min() 取最小值 max() 取最大值 数据类型 整数类型 类型 大小 说明 tinyint 1字节 小整型 smallint 2字节 小整型 mediumint 3字节 中整型 int 4字节 整型 bigint 8字节 大整型 浮点类型 类型 大小 说明 float 4字节 单精度浮点型 double 8字节 双精度浮点型 decimal 每4个字节存储9个数字,小数点占1字节 精确类型,常用来表示金额 日期与时间类型 类型 大小 格式 范围 date 3字节 YYYY-MM-DD 1000-01-01~9999-12-31 time 3-6字节 HH::MM::SS[.微秒] -838:59:59~838:59:59 year 1字节 YYYY 1901~2155 datetime 5-8字节 YYYY-MM-DD HH::MM::SS[.微秒] 1000-01-01 00:00:00~2038-01-19 03:14:07 UTC timestamp 4-7字节 YYYY-MM-DD HH::MM::SS[.微秒] 1974-01-01 00:00:00~2038-01-19 03:14:07 UTC 字符串类型 类型 范围 说明 char 1~255个字节 固定长度 varchar 字段存储所占字节数不能超过65535字节 可变长度 tinytest 最大长度255字节 可变长度 text 最大长度65535字节 可变字节 mediumtext 最大字节16777215字节 可变字节 longtext 最大长度4294967295字节 可变字节 char 类型是一种固定长度的类型,varchar 则是一种可变长度的类型,它们的区别是:char(M) 类型的数据列里,每个值都占用 M 个字节,如果某个长度小于 M,MySQL 就会在它的右边用空格字符补足.。 枚举值类型 类型 范围 说明 enum 几何数最大65535 枚举值类型,只能插入列表中指定的值 二进制类型 类型 范围 说明 tinyblob 最大长度255字节 可变长度 blob 最大长度65535字节 可变长度 mediumblob 最大长度16777215字节 可变长度 longblob 最大长度4294967295字节 可变字节 如果觉得对你们有帮助,可以给文章点个赞哦~ [图片]
2021-11-17 - vue的基础知识点总结
vue的基础知识点总结 小程序的前端框架可以选用uniapp,是一款集成了vue的小程序前端开发框架,同时一套代码可以适配h5,安卓和小程序。对vue感兴趣的同学可以上手玩玩 vue的基础指令 基本模板(不使用脚手架) [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>02-Vue基本模板</title> <!--1.下载导入Vue.js--> <script src="js/vue.js"></script> </head> <body> <div id="app"> <p>{{ name }}</p> </div> <script> // 2.创建一个Vue的实例对象 let vue = new Vue({ // 3.告诉Vue的实例对象, 将来需要控制界面上的哪个区域 el: '#app', // 4.告诉Vue的实例对象, 被控制区域的数据是什么 data: { name: "李南江" } }); </script> </body> </html> <!-- 1.Vue框架使用方式 1.1传统下载导入使用 1.2vue-cli安装导入使用 2.Vue框架使用步骤 2.1下载Vue框架 2.2导入Vue框架 2.3创建Vue实例对象 2.4指定Vue实例对象控制的区域 2.5指定Vue实例对象控制区域的数据 --> [代码] vue数据的单项绑定 [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>03-Vue数据单向传递</title> <!--1.下载导入Vue.js--> <script src="js/vue.js"></script> </head> <body> <!-- 1.MVVM设计模式 在MVVM设计模式中由3个部分组成 M : Model 数据模型(保存数据, 处理数据业务逻辑) V : View 视图(展示数据, 与用户交互) VM: View Model 数据模型和视图的桥梁(M是中国人, V是美国人, VM就是翻译) MVVM设计模式最大的特点就是支持数据的双向传递 数据可以从 M -> VM -> V 也可以从 V -> VM -> M 2.Vue中MVVM的划分 Vue其实是基于MVVM设计模式的 被控制的区域: View Vue实例对象 : View Model 实例对象中的data: Model 3.Vue中数据的单向传递 我们把"数据"交给"Vue实例对象", "Vue实例对象"将数据交给"界面" Model -> View Model -> View --> <!--这里就是MVVM中的View--> <div id="app"> <p>{{ name }}</p> </div> <script> // 这里就是MVVM中的View Model let vue = new Vue({ el: '#app', // 这里就是MVVM中的Model data: { name: "李南江" } }); </script> </body> </html> [代码] v-model数据的双向传递 [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>04-Vue数据双向传递</title> <script src="js/vue.js"></script> </head> <body> <!-- 1.Vue调试工具安装 如果你能打开谷歌插件商店, 直接在线安装即可 https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd?hl=zh-CN 由于国内无法打开谷歌国外插件商店, 所以可以离线安装 https://www.chromefor.com/vue-js-devtools_v5-3-0/ 2.安装步骤: 2.1下载离线安装包 2.2打开谷歌插件界面 2.3直接将插件拖入 2.4报错 程序包无效: "CRX_HEADER_INVALID" 可以将安装包修改为rar后缀, 解压之后再安装 2.5重启浏览器 --> <!-- 2.数据双向绑定 默认情况下Vue只支持数据单向传递 M -> VM -> V 但是由于Vue是基于MVVM设计模式的, 所以也提供了双向传递的能力 在<input>、<textarea> 及 <select> 元素上可以用 v-model 指令创建双向数据绑定 注意点: v-model 会忽略所有表单元素的 value、checked、selected 特性的初始值 而总是将 Vue 实例的数据作为数据来源 --> <!--这里就是MVVM中的View--> <div id="app"> <p>{{ name }}</p> <input type="text" v-model="msg"> </div> <script> // 这里就是MVVM中的View Model let vue = new Vue({ el: '#app', // 这里就是MVVM中的Model data: { name: "李南江", msg: "知播渔" } }); </script> </body> </html> [代码] v-once只渲染一次 [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>05-常用指令-v-once</title> <script src="js/vue.js"></script> </head> <body> <!-- 1.什么是指令? 指令就是Vue内部提供的一些自定义属性, 这些属性中封装好了Vue内部实现的一些功能 只要使用这些指令就可以使用Vue中实现的这些功能 2.Vue数据绑定的特点 只要数据发生变化, 界面就会跟着变化 3.v-once指令: 让界面不要跟着数据变化, 只渲染一次 --> <!--这里就是MVVM中的View--> <div id="app"> <p v-once>原始数据: {{ name }}</p> <p>当前数据: {{ name }}</p> </div> <script> // 这里就是MVVM中的View Model let vue = new Vue({ el: '#app', // 这里就是MVVM中的Model data: { name: "李南江", } }); </script> </body> </html> [代码] v-cloak数据渲染完之后才显示 [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>06-常用指令-v-cloak</title> <style> [v-cloak] { display: none } </style> </head> <body> <!-- 1.Vue数据绑定过程 1.1会先将未绑定数据的界面展示给用户 1.2然后再根据模型中的数据和控制的区域生成绑定数据之后的HTML代码 1.3最后再将绑定数据之后的HTML渲染到界面上 正是在最终的HTML被生成渲染之前会先显示模板内容 所以如果用户网络比较慢或者网页性能比较差, 那么用户会看到模板内容 2.如何解决这个问题 利用v-cloak配合 [v-cloak]:{display: none}默认先隐藏未渲染的界面 等到生成HTML渲染之后再重新显示 3.v-cloak指令作用: 数据渲染之后自动显示元素 --> <!--这里就是MVVM中的View--> <div id="app"> <p v-cloak>{{ name }}</p> </div> <!-- <div id="app"> <p> 李南江 </p> </div> --> <script src="js/vue.js"></script> <script> // 这里就是MVVM中的View Model let vue = new Vue({ el: '#app', // 这里就是MVVM中的Model data: { name: "李南江", } }); </script> </body> </html> [代码] v-text和v-html [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>07-常用指令v-text和v-html</title> <script src="js/vue.js"></script> </head> <body> <!-- 1.什么是v-text指令 v-text就相当于过去学习的innerText 2.什么是v-html指令 v-html就相当于过去学习的innerHTML --> <!--这里就是MVVM中的View--> <div id="app"> <!--插值的方式: 可以将指定的数据插入到指定的位置--> <!-- <p>++++{{ name }}++++</p>--> <!--插值的方式: 不会解析HTML--> <!-- <p>++++{{ msg }}++++</p>--> <!--v-text的方式: 会覆盖原有的内容--> <!-- <p v-text="name">++++++++</p>--> <!--v-text的方式: 也不会解析HTML--> <!-- <p v-text="msg">++++++++</p>--> <!--v-html的方式: 会覆盖原有的内容--> <p v-html="name">++++++++</p> <!--v-html的方式:会解析HTML--> <p v-html="msg">++++++++</p> </div> <script> // 这里就是MVVM中的View Model let vue = new Vue({ el: '#app', // 这里就是MVVM中的Model data: { name: "李南江", msg: "<span>我是span</span>" } }); </script> </body> </html> [代码] v-if [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>08-常用指令v-if</title> <script src="js/vue.js"></script> </head> <body> <!-- 1.什么是v-if指令 条件渲染: 如果v-if取值是true就渲染元素, 如果不是就不渲染元素 2.v-if特点: 如果条件不满足根本就不会创建这个元素(重点) 3.v-if注意点 v-if可以从模型中获取数据 v-if也可以直接赋值一个表达式 --> <!-- 4.v-else指令 v-else指令可以和v-if指令配合使用, 当v-if不满足条件时就执行v-else就显示v-else中的内容 5.v-else注意点 v-else不能单独出现 v-if和v-else中间不能出现其它内容 --> <!-- 6.v-else-if指令 v-else-if可以和v-if指令配合使用, 当v-if不满足条件时就依次执行后续v-else-if, 哪个满足就显示哪个 7.v-else-if注意点 和v-else一样 --> <!--这里就是MVVM中的View--> <div id="app"> <!-- <p v-if="show">我是true</p>--> <!-- <p v-if="hidden">我是false</p>--> <!-- <p v-if="true">我是true</p>--> <!-- <p v-if="false">我是false</p>--> <!-- <p v-if="age >= 18">我是true</p>--> <!-- <p v-if="age < 18">我是false</p>--> <!-- <p v-if="age >= 18">成年人</p>--> <!-- <p>中间的内容</p>--> <!-- <p v-else>未成年人</p>--> <p v-if="score >= 80">优秀</p> <p v-else-if="score >= 60">良好</p> <p v-else>差</p> </div> <script> // 这里就是MVVM中的View Model let vue = new Vue({ el: '#app', // 这里就是MVVM中的Model data: { show: true, hidden: false, age: 17, score: 50 } }); </script> </body> </html> [代码] v-show [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>09-常用指令v-show</title> <script src="js/vue.js"></script> </head> <body> <!-- 1.什么是v-show指令 v-show和v-if的能够一样都是条件渲染, 取值为true就显示, 取值为false就不显示 2.v-if和v-show区别 v-if: 只要取值为false就不会创建元素 v-show: 哪怕取值为false也会创建元素, 只是如果取值是false会设置元素的display为none 3.v-if和v-show应用场景 由于取值为false时v-if不会创建元素, 所以如果需要切换元素的显示和隐藏, 每次v-if都会创建和删除元素 由于取值为false时v-show会创建元素并设置display为none, 所有如果需要切换元素的显示和隐藏, 不会反复创建和删除, 只是修改display的值 所以: 如果企业开发中需要频繁切换元素显示隐藏, 那么推荐使用v-show, 否则使用v-if --> <!--这里就是MVVM中的View--> <div id="app"> <!-- <p v-show="show">我是true</p>--> <!-- <p v-show="hidden">我是false</p>--> <!-- <p v-show="true">我是true</p>--> <!-- <p v-show="false">我是false</p>--> <!-- <p v-show="age >= 18">我是true</p>--> <!-- <p v-show="age < 18">我是false</p>--> <p v-show="show">我是段落1</p> <p v-show="hidden">我是段落2</p> </div> <script> // 这里就是MVVM中的View Model let vue = new Vue({ el: '#app', // 这里就是MVVM中的Model data: { show: true, hidden: false, age: 18 } }); </script> </body> </html> [代码] v-for [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>10-常用指令v-for</title> <script src="js/vue.js"></script> </head> <body> <!-- 1.什么是v-for指令 相当于JS中的for in循环, 可以根据数据多次渲染元素 2.v-for特点 可以遍历 数组, 字符, 数字, 对象 --> <!--这里就是MVVM中的View--> <div id="app"> <ul> <!-- <li v-for="(value, index) in list">{{index}}---{{value}}</li> --> <!-- <li v-for="(value, index) in 'abcdefg'"> {{index}}---{{value}} </li> --> <!-- <li v-for="(value, index) in 6">{{index}}---{{value}}</li> --> <!-- <li v-for="(value, key) in obj">{{key}}---{{value}}</li> --> </ul> </div> <script> // 这里就是MVVM中的View Model let vue = new Vue({ el: "#app", // 这里就是MVVM中的Model data: { list: ["张三", "李四", "王五", "赵六"], obj: { name: "lnj", age: 33, gender: "man", class: "知播渔", }, }, }); </script> </body> </html> [代码] v-bind(语法糖: 😃 [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>10-常用指令v-bind</title> <script src="js/vue.js"></script> </head> <body> <!-- 1.什么是v-bind指令 在企业开发中想要给"元素"绑定数据, 我们可以使用{{}}, v-text, v-html 但是如果想给"元素的属性"绑定数据, 就必须使用v-bind 所以v-bind的作用是专门用于给"元素的属性"绑定数据的 2.v-bind格式 v-bind:属性名称="绑定的数据" :属性名称="绑定的数据" 3.v-bind特点 赋值的数据可以是任意一个合法的JS表达式 例如: :属性名称="age + 1" --> <!--这里就是MVVM中的View--> <div id="app"> <!-- <p>{{name}}</p>--> <!-- <p v-text="name"></p>--> <!-- <p v-html="name"></p>--> <!--注意点: 如果要给元素的属性绑定数据, 那么是不能够使用插值语法的--> <!-- <input type="text" value="{{name}}" /> --> <!--注意点: 虽然通过v-model可以将数据绑定到input标签的value属性上 但是v-model是有局限性的, v-model只能用于input/textarea/select 但是在企业开发中我们还可能需要给其它标签的属性绑定数据--> <!-- <input type="text" v-model="name">--> <!-- <input type="text" v-bind:value="name">--> <!-- <input type="text" :value="name">--> <input type="text" :value="age + 1" /> </div> <script> // 这里就是MVVM中的View Model let vue = new Vue({ el: "#app", // 这里就是MVVM中的Model data: { name: "知播渔666", age: 18, }, }); </script> </body> </html> [代码] 绑定类名:class [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>12-常用指令-绑定类名</title> <script src="js/vue.js"></script> <style> *{ margin: 0; padding: 0; } .size{ font-size: 100px; } .color{ color: red; } .active{ background: skyblue; } </style> </head> <body> <!-- 1.v-bind指令的作用 v-bind指令给"任意标签"的"任意属性"绑定数据 对于大部分的属性而言我们只需要直接赋值即可, 例如:value="name" 但是对于class和style属性而言, 它的格式比较特殊 2.通过v-bind绑定类名格式 :class="['需要绑定类名', ...]" 3.注意点: 3.1直接赋值一个类名(没有放到数组中)默认回去Model中查找 :class="需要绑定类名" 2.2数组中的类名没有用引号括起来也会去Model中查找 :class="[需要绑定类名]" 2.3数组的每一个元素都可以是一个三目运算符按需导入 :class="[flag?'active':'']" 2.4可以使用对象来替代数组中的三目运算符按需导入 :class="[{'active': true}]" 2.5绑定的类名太多可以将类名封装到Model中 obj: { 'color': true, 'size': true, 'active': false, } 4.绑定类名企业应用场景 从服务器动态获取样式后通过v-bind动态绑定类名 这样就可以让服务端来控制前端样式 常见场景: 618 双11等 --> <!--这里就是MVVM中的View--> <div id="app"> <!-- <p class="size color active">我是段落</p>--> <!-- 注意点: 如果需要通过v-bind给class绑定类名, 那么不能直接赋值 默认情况下v-bind会去Model中查找数据, 但是Model中没有对应的类名, 所以无效, 所以不能直接赋值 --> <!-- <p :class="size">我是段落</p>--> <!-- 注意点: 如果想让v-bind去style中查找类名, 那么就必须把类名放到数组中 但是放到数组中之后默认还是回去Model中查找 --> <!-- <p :class="[size]">我是段落</p>--> <!-- 注意点: 将类名放到数组中之后, 还需要利用引号将类名括起来才会去style中查找 --> <!-- <p :class="['size', 'color', 'active']">我是段落</p>--> <!-- 注意点: 如果是通过v-bind类绑定类名, 那么在绑定的时候可以编写一个三目运算符来实现按需绑定 格式: 条件表达式 ? '需要绑定的类名' : '' --> <!-- <p :class="['size', 'color', flag ? 'active' : '']">我是段落</p>--> <!-- 注意点: 如果是通过v-bind类绑定类名, 那么在绑定的时候可以通过对象来决定是否需要绑定 格式: {'需要绑定的类名' : 是否绑定} --> <!-- <p :class="['size', 'color',{'active' : false}]">我是段落</p>--> <!-- 注意点: 如果是通过v-bind类绑定类名, 那么还可以使用Model中的对象来替换数组 --> <p :class="obj">我是段落</p> </div> <script> // 这里就是MVVM中的View Model let vue = new Vue({ el: '#app', // 这里就是MVVM中的Model data: { flag: false, obj:{ 'size': false, 'color': false, 'active': true, } } }); </script> </body> </html> [代码] 绑定样式:style [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>13-常用指令-绑定样式</title> <script src="js/vue.js"></script> </head> <body> <!-- 1.如何通过v-bind给style属性绑定数据 1.1将数据放到对象中 :style="{color:'red','font-size':'50px'}" 1.2将数据放到Model对象中 obj: { color: 'red', 'font-size': '80px', } 2.注意点 2.1如果属性名称包含-, 那么必须用引号括起来 2.2如果需要绑定Model中的多个对象, 可以放到一个数组中赋值 --> <!--这里就是MVVM中的View--> <div id="app"> <!-- <p style="color: red">我是段落</p>--> <!-- 注意点: 和绑定类名一样, 默认情况下v-bind回去Model中查找, 找不到所以没有效果 --> <!-- <p :style="color: red">我是段落</p>--> <!-- 注意点: 我们只需要将样式代码放到对象中赋值给style即可 但是取值必须用引号括起来 --> <!-- <p :style="{color: 'red'}">我是段落</p>--> <!-- 注意点: 如果样式的名称带-, 那么也必须用引号括起来才可以 --> <!-- <p :style="{color: 'red', 'font-size': '100px'}">我是段落</p>--> <!-- <p :style="obj">我是段落</p>--> <!-- 注意点: 如果Model中保存了多个样式的对象 ,想将多个对象都绑定给style, 那么可以将多个对象放到数组中赋值给style即可 --> <p :style="[obj1, obj2]">我是段落</p> </div> <script> // 这里就是MVVM中的View Model let vue = new Vue({ el: '#app', // 这里就是MVVM中的Model data: { obj1:{ "color": "blue", "font-size": "100px" }, obj2: { "background-color": "red" } } }); </script> </body> </html> [代码] v-on(语法糖: @) [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>14-常用指令-v-on</title> <script src="js/vue.js"></script> </head> <body> <!-- 1.什么是v-on指令? v-on指令专门用于给元素绑定监听事件 2.v-on指令格式 v-on:事件名称="回调函数名称" @事件名称="回调函数名称" 3.v-on注意点: v-on绑定的事件被触发之后, 会去Vue实例对象的methods中查找对应的回调函数 --> <!--这里就是MVVM中的View--> <div id="app"> <!-- <button onclick="alert('lnj')">我是按钮</button>--> <!-- 注意点: 1.如果是通过v-on来绑定监听事件, 那么在指定事件名称的时候不需要写on 2.如果是通过v-on来绑定监听事件, 那么在赋值的时候必须赋值一个回调函数的名称 --> <!-- <button v-on:click="alert('lnj')">我是按钮</button>--> <!-- 注意点: 当绑定的事件被触发后, 会调用Vue实例的methods对象中对应的回调函数 --> <!-- <button v-on:click="myFn">我是按钮</button>--> <button @click="myFn">我是按钮</button> </div> <script> // 这里就是MVVM中的View Model let vue = new Vue({ el: '#app', // 这里就是MVVM中的Model data: { }, methods: { myFn(){ alert('lnj') } } }); </script> </body> </html> [代码] v-on的修饰符 [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>15-常用指令-v-on修饰符</title> <script src="js/vue.js"></script> <style> * { margin: 0; padding: 0; } .a { width: 300px; height: 300px; background: red; } .b { width: 200px; height: 200px; background: blue; } .c { width: 100px; height: 100px; background: green; } </style> </head> <body> <!-- 1.v-on修饰符 在事件中有很多东西需要我们处理, 例如事件冒泡,事件捕获, 阻止默认行为等 那么在Vue中如何处理以上内容呢, 我们可以通过v-on修饰符来处理 2.常见修饰符 .once - 只触发一次回调。 .prevent - 调用 event.preventDefault()。 .stop - 调用 event.stopPropagation()。 .self - 只当事件是从侦听器绑定的元素本身触发时才触发回调。 .capture - 添加事件侦听器时使用 capture 模式。 --> <!--这里就是MVVM中的View--> <div id="app"> <!--注意点: 默认情况下事件的回调函数可以反复的执行, 只要事件被触发就会执行--> <!-- <button v-on:click="myFn">我是按钮</button>--> <!--如果想让事件监听的回调函数只执行一次, 那么就可以使用.once修饰符--> <!-- <button v-on:click.once ="myFn">我是按钮</button>--> <!--如果想阻止元素的默认行为, 那么可以使用.prevent修饰符--> <!-- <a href="http://www.it666.com" v-on:click.prevent="myFn">我是A标签</a>--> <!-- 默认情况下载嵌套的元素中, 如果都监听了相同的事件, 那么会触发事件冒泡 如果想阻止事件冒泡, 那么可以使用.stop修饰符 --> <!--<div class="a" @click="myFn1"> <div class="b" @click.stop="myFn2"> <div class="c" @click="myFn3"></div> </div> </div>--> <!-- 如果想让回调只有当前元素触发事件的时候才执行, 那么就可以使用.self修饰符 --> <!-- <div class="a" @click="myFn1"> <div class="b" @click.self="myFn2"> <div class="c" @click="myFn3"></div> </div> </div> --> <!-- 默认情况下是事件冒泡, 如果想变成事件捕获, 那么就需要使用.capture修饰符 --> <!-- <div class="a" @click.capture="myFn1"> <div class="b" @click.capture="myFn2"> <div class="c" @click.capture="myFn3"></div> </div> </div> --> </div> <script> // 这里就是MVVM中的View Model let vue = new Vue({ el: "#app", // 这里就是MVVM中的Model data: {}, // 专门用于存储监听事件回调函数 methods: { myFn() { alert("lnj"); }, myFn1() { console.log("爷爷"); }, myFn2() { console.log("爸爸"); }, myFn3() { console.log("儿子"); }, }, }); </script> </body> </html> [代码] v-on的注意点 [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>16-常用指令-v-on注意点</title> <script src="js/vue.js"></script> </head> <body> <!-- 1.v-on注意点 1.1绑定回调函数名称的时候, 后面可以写()也可以不写 v-on:click="myFn" v-on:click="myFn()" 1.2可以给绑定的回调函数传递参数 v-on:click="myFn('lnj', 33)" 1.3如果在绑定的函数中需要用到data中的数据必须加上this --> <!--这里就是MVVM中的View--> <div id="app"> <button @click="myFn('lnj', 33, $event)">我是按钮</button> </div> <script> // 这里就是MVVM中的View Model let vue = new Vue({ el: '#app', // 这里就是MVVM中的Model data: { gender: "man" }, // 专门用于存储监听事件回调函数 methods: { myFn(name, age, e){ // alert('lnj'); // console.log(name, age, e); console.log(this.gender); } } }); </script> </body> </html> [代码] v-on按键修饰符 [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>17-常用指令-v-on按键修饰符</title> <script src="js/vue.js"></script> </head> <body> <!-- 1.什么是按键修饰符 我们可以通过按键修饰符监听特定按键触发的事件 例如: 可以监听当前事件是否是回车触发的, 可以监听当前事件是否是ESC触发的等 2.按键修饰符分类 2.1系统预定义修饰符 2.2自定义修饰符 --> <!--这里就是MVVM中的View--> <div id="app"> <!-- <input type="text" @keyup.enter="myFn">--> <input type="text" @keyup.f2="myFn"> </div> <script> Vue.config.keyCodes.f2 = 113; // 这里就是MVVM中的View Model let vue = new Vue({ el: '#app', // 这里就是MVVM中的Model data: { }, // 专门用于存储监听事件回调函数 methods: { myFn(){ alert("lnj"); } } }); </script> </body> </html> [代码] vue自定义全局指令 [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>18-常用指令-自定义指令</title> <script src="js/vue.js"></script> </head> <body> <!-- 1.自定义全局指令 在Vue中除了可以使用Vue内置的一些指令以外, 我们还可以自定义指令 2.自定义全局指令语法 vue.directive('自定义指令名称', { 生命周期名称: function (el) { 指令业务逻辑代码 } }); 3.指令生命周期方法 自定义指令时一定要明确指令的业务逻辑代码更适合在哪个阶段执行 例如: 指令业务逻辑代码中没有用到元素事件, 那么可以在bind阶段执行 例如: 指令业务逻辑代码中用到了元素事件, 那么就需要在inserted阶段执行 4.自定义指令注意点 使用时需要加上v-, 而在自定义时不需要加上v- --> <!--这里就是MVVM中的View--> <div id="app"> <!-- <p v-color>我是段落</p>--> <input type="text" v-focus /> </div> <script> /* directive方法接收两个参数 第一个参数: 指令的名称 第二个参数: 对象 注意点: 在自定义指令的时候, 在指定指令名称的时候, 不需要写v- 注意点: 指令可以在不同的生命周期阶段执行 bind: 指令被绑定到元素上的时候执行 inserted: 绑定指令的元素被添加到父元素上的时候执行 * */ Vue.directive("color", { // 这里的el就是被绑定指令的那个元素 bind: function (el) { el.style.color = "red"; }, }); Vue.directive("focus", { // 这里的el就是被绑定指令的那个元素 inserted: function (el) { el.focus(); }, }); // 这里就是MVVM中的View Model let vue = new Vue({ el: "#app", // 这里就是MVVM中的Model data: {}, // 专门用于存储监听事件回调函数 methods: {}, }); </script> </body> </html> [代码] 自定义指令传参 [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>19-常用指令-自定义指令参数</title> <script src="js/vue.js"></script> </head> <body> <!-- 1.自定义指令参数 在使用官方指令的时候我们可以给指令传参 例如: v-model="name" 在我们自定义的指令中我们也可以传递传递 2.获取自定义指令传递的参数 在执行自定义指令对应的方法的时候, 除了会传递el给我们, 还会传递一个对象给我们 这个对象中就保存了指令传递过来的参数 --> <!--这里就是MVVM中的View--> <div id="app"> <!-- <p v-color="'blue'">我是段落</p>--> <p v-color="curColor">我是段落</p> </div> <script> Vue.directive("color", { // 这里的el就是被绑定指令的那个元素 bind: function (el, obj) { // el.style.color = "red"; el.style.color = obj.value; } }); // 这里就是MVVM中的View Model let vue = new Vue({ el: '#app', // 这里就是MVVM中的Model data: { curColor: 'green' }, // 专门用于存储监听事件回调函数 methods: { } }); </script> </body> </html> [代码] directives自定义局部指令 [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>21-常用指令-自定义局部指令</title> <script src="js/vue.js"></script> </head> <body> <!-- 1.自定义全局指令的特点 在任何一个Vue实例控制的区域中都可以使用 2.自定义局部指令的特点 只能在自定义的那个Vue实例中使用 3.如何自定义一个局部指令 给创建Vue实例时传递的对象添加 directives: { // key: 指令名称 // value: 对象 'color': { bind: function (el, obj) { el.style.color = obj.value; } } } --> <!--这里就是MVVM中的View--> <div id="app1"> <p v-color="'blue'">我是段落</p> </div> <div id="app2"> <p v-color="'red'">我是段落</p> </div> <script> /* Vue.directive("color", { // 这里的el就是被绑定指令的那个元素 bind: function (el, obj) { el.style.color = obj.value; } }); */ // 这里就是MVVM中的View Model let vue1 = new Vue({ el: '#app1', // 这里就是MVVM中的Model data: {}, // 专门用于存储监听事件回调函数 methods: {} }); // 这里就是MVVM中的View Model let vue2 = new Vue({ el: '#app2', // 这里就是MVVM中的Model data: {}, // 专门用于存储监听事件回调函数 methods: {}, // 专门用于定义局部指令的 directives: { "color": { // 这里的el就是被绑定指令的那个元素 bind: function (el, obj) { el.style.color = obj.value; } } } }); </script> </body> </html> [代码] computed计算属性 [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>22-Vue-计算属性</title> <script src="js/vue.js"></script> </head> <body> <!-- 1.插值语法特点 可以在{{}}中编写合法的JavaScript表达式 2.在插值语法中编写JavaScript表达式缺点 2.1没有代码提示 2.2语句过于复杂不利于我们维护 3.如何解决? 对于任何复杂逻辑,你都应当使用计算属性 --> <!--这里就是MVVM中的View--> <div id="app"> <p>{{name}}</p> <p>{{age + 1}}</p> <p>{{msg.split("").reverse().join("")}}</p> <!-- 注意点: 虽然在定义计算属性的时候是通过一个函数返回的数据 但是在使用计算属性的时候不能在计算属性名称后面加上() 因为它是一个属性不是一个函数(方法) --> <p>{{msg2}}</p> </div> <script> // 这里就是MVVM中的View Model let vue = new Vue({ el: '#app', // 这里就是MVVM中的Model data: { name: "lnj", age: 18, msg: "abcdef" }, // 专门用于存储监听事件回调函数 methods: {}, // 专门用于定义计算属性的 computed: { msg2: function () { let res = "abcdef".split("").reverse().join(""); return res; } } }); </script> </body> </html> [代码] 计算属性和函数的区别 [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>23-Vue-计算属性和函数</title> <script src="js/vue.js"></script> </head> <body> <!-- 1.计算属性和函数 通过计算属性我们能拿到处理后的数据, 但是通过函数我们也能拿到处理后的数据 那么计算属性和函数有什么区别呢? 2.1函数"不会"将计算的结果缓存起来, 每一次访问都会重新求值 2.2计算属性"会"将计算的结果缓存起来, 只要数据没有发生变化, 就不会重新求值 2.计算属性应用场景 计算属性:比较适合用于计算不会频繁发生变化的的数据 --> <!--这里就是MVVM中的View--> <div id="app"> <!--<p>{{msg1()}}</p> <p>{{msg1()}}</p> <p>{{msg1()}}</p>--> <p>{{msg2}}</p> <p>{{msg2}}</p> <p>{{msg2}}</p> </div> <script> // 这里就是MVVM中的View Model let vue = new Vue({ el: '#app', // 这里就是MVVM中的Model data: { }, // 专门用于存储监听事件回调函数 methods: { /* 函数的特点: 每次调用都会执行 * */ msg1(){ console.log("msg1函数被执行了"); let res = "abcdef".split("").reverse().join(""); return res; } }, // 专门用于定义计算属性的 computed: { /* 计算属性的特点: 只要返回的结果没有发生变化, 那么计算属性就只会被执行一次 计算属性的应用场景: 由于计算属性会将返回的结果缓存起来 所以如果返回的数据不经常发生变化, 那么使用计算属性的性能会比使用函数的性能高 * */ msg2: function () { console.log("msg2计算属性被执行了"); let res = "abcdef".split("").reverse().join(""); return res; } } }); </script> </body> </html> [代码] 自定义全局的过滤器 [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>24-Vue-自定义全局过滤器</title> <script src="js/vue.js"></script> </head> <body> <!-- 1.什么是过滤器? 过滤器和函数和计算属性一样都是用来处理数据的 但是过滤器一般用于格式化插入的文本数据 2.如何自定义全局过滤器 Vue.filter("过滤器名称", 过滤器处理函数): 3.如何使用全局过滤器 {{msg | 过滤器名称}} :value="msg | 过滤器名称" 4.过滤器注意点 4.1只能在插值语法和v-bind中使用 4.2过滤器可以连续使用 --> <!--这里就是MVVM中的View--> <div id="app"> <!--Vue会把name交给指定的过滤器处理之后, 再把处理之后的结果插入到指定的元素中--> <p>{{name | formartStr1 | formartStr2}}</p> </div> <script> /* 如何自定义一个全局过滤器 通过Vue.filter(); filter方法接收两个参数 第一个参数: 过滤器名称 第二个参数: 处理数据的函数 注意点: 默认情况下处理数据的函数接收一个参数, 就是当前要被处理的数据 * */ Vue.filter("formartStr1", function (value) { // console.log(value); value = value.replace(/学院/g, "大学"); console.log(value); return value; }); Vue.filter("formartStr2", function (value) { // console.log(value); value = value.replace(/大学/g, "幼儿园"); console.log(value); return value; }); // 这里就是MVVM中的View Model let vue = new Vue({ el: '#app', // 这里就是MVVM中的Model data: { name: "知播渔学院, 指趣学院, 前端学院, 区块链学院" }, // 专门用于存储监听事件回调函数 methods: { }, // 专门用于定义计算属性的 computed: { } }); </script> </body> </html> [代码] filters自定义局部过滤器 [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>24-Vue-自定义全局过滤器</title> <script src="js/vue.js"></script> </head> <body> <!-- 1.自定义全局过滤器的特点 在任何一个Vue实例控制的区域中都可以使用 2.自定义局部过滤器的特点 只能在自定义的那个Vue实例中使用 3.如何自定义一个局部指令 给创建Vue实例时传递的对象添加 filters: { // key: 过滤器名称 // value: 过滤器处理函数 'formartStr': function (value) {} } --> <!--这里就是MVVM中的View--> <div id="app1"> <p>{{name | formartStr}}</p> </div> <div id="app2"> <p>{{name | formartStr}}</p> </div> <script> /* Vue.filter("formartStr", function (value) { // console.log(value); value = value.replace(/学院/g, "大学"); // console.log(value); return value; }); */ // 这里就是MVVM中的View Model let vue1 = new Vue({ el: '#app1', // 这里就是MVVM中的Model data: { name: "知播渔学院, 指趣学院, 前端学院, 区块链学院" }, // 专门用于存储监听事件回调函数 methods: { }, // 专门用于定义计算属性的 computed: { } }); // 这里就是MVVM中的View Model let vue2 = new Vue({ el: '#app2', // 这里就是MVVM中的Model data: { name: "知播渔学院, 指趣学院, 前端学院, 区块链学院" }, // 专门用于存储监听事件回调函数 methods: { }, // 专门用于定义计算属性的 computed: { }, // 专门用于定义局部过滤器的 filters: { "formartStr": function (value) { // console.log(value); value = value.replace(/学院/g, "大学"); // console.log(value); return value; } } }); </script> </body> </html> [代码] 过滤器传参和String的原型方法padStart [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>24-Vue-自定义全局过滤器</title> <script src="js/vue.js"></script> </head> <body> <!-- 需求: 利用过滤器对时间进行格式化 --> <!--这里就是MVVM中的View--> <div id="app"> <p>{{time | dateFormart("yyyy-MM-dd")}}</p> </div> <script> /* 注意点: 在使用过滤器的时候, 可以在过滤器名称后面加上() 如果给过滤器的名称后面加上了(), 那么就可以给过滤器的函数传递参数 * */ Vue.filter("dateFormart", function (value, fmStr) { // console.log(fmStr); let date = new Date(value); let year = date.getFullYear(); let month = date.getMonth() + 1 + ""; let day = date.getDate() + ""; let hour = date.getHours() + ""; let minute = date.getMinutes() + ""; let second = date.getSeconds() + ""; if(fmStr && fmStr === "yyyy-MM-dd"){ return `${year}-${month.padStart(2, "0")}-${day.padStart(2, "0")}`; } return `${year}-${month.padStart(2, "0")}-${day.padStart(2, "0")} ${hour.padStart(2, "0")}:${minute.padStart(2, "0")}:${second.padStart(2, "0")}`; }); // 这里就是MVVM中的View Model let vue = new Vue({ el: '#app', // 这里就是MVVM中的Model data: { time: Date.now() }, // 专门用于存储监听事件回调函数 methods: { }, // 专门用于定义计算属性的 computed: { } }); </script> </body> </html> [代码] vue过渡动画 trainsition [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>28-Vue-过渡动画</title> <script src="js/vue.js"></script> <style> *{ margin: 0; padding: 0; } .box{ width: 200px; height: 200px; background: red; } .v-enter{ opacity: 0; } .v-enter-to{ opacity: 1; } .v-enter-active{ transition: all 3s; } .v-leave{ opacity: 1; } .v-leave-to{ opacity: 0; } .v-leave-active{ transition: all 3s; } </style> </head> <body> <!-- 1.如何给Vue控制的元素添加过渡动画 1.1将需要执行动画的元素放到transition组件中 1.2当transition组件中的元素显示时会自动查找.v-enter/.v-enter-active/.v-enter-to类名 当transition组件中的元素隐藏时会自动查找.v-leave/ .v-leave-active/.v-leave-to类名 1.3我们只需要在.v-enter和.v-leave-to中指定动画动画开始的状态 在.v-enter-active和.v-leave-active中指定动画执行的状态 即可完成过渡动画 --> <!--这里就是MVVM中的View--> <div id="app"> <button @click="toggle">我是按钮</button> <transition> <div class="box" v-show="isShow"></div> </transition> </div> <script> // 这里就是MVVM中的View Model let vue = new Vue({ el: '#app', // 这里就是MVVM中的Model data: { isShow: false }, // 专门用于存储监听事件回调函数 methods: { toggle(){ this.isShow = !this.isShow; } }, // 专门用于定义计算属性的 computed: { } }); </script> </body> </html> [代码] 给多个组件添加不同的动画 [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>29-Vue-过渡动画</title> <script src="js/vue.js"></script> <style> *{ margin: 0; padding: 0; } .box{ width: 200px; height: 200px; background: red; } .one-enter{ opacity: 0; } .one-enter-to{ opacity: 1; margin-left: 500px; } .one-enter-active{ transition: all 3s; } .two-enter{ opacity: 0; } .two-enter-to{ opacity: 1; margin-top: 500px; } .two-enter-active{ transition: all 3s; } </style> </head> <body> <!-- 1.transition注意点: transition中只能放一个元素, 多个元素无效 如果想给多个元素添加过渡动画, 那么就必须创建多个transition组件 2.初始动画设置 默认情况下第一次进入的时候没没有动画的 如果想一进来就有动画, 我们可以通过给transition添加appear属性的方式 告诉Vue第一次进入就需要显示动画 3.如何给多个不同的元素指定不同的动画 如果有多个不同的元素需要执行不同的过渡动画,那么我们可以通过给transition指定name的方式 来指定"进入之前/进入之后/进入过程中, 离开之前/离开之后/离开过程中"对应的类名 来实现不同的元素执行不同的过渡动画 --> <!--这里就是MVVM中的View--> <div id="app"> <button @click="toggle">我是按钮</button> <transition appear name="one"> <div class="box" v-show="isShow"></div> <!-- <div class="box" v-show="isShow"></div>--> </transition> <transition appear name="two"> <div class="box" v-show="isShow"></div> </transition> </div> <script> // 这里就是MVVM中的View Model let vue = new Vue({ el: '#app', // 这里就是MVVM中的Model data: { isShow: true }, // 专门用于存储监听事件回调函数 methods: { toggle(){ this.isShow = !this.isShow; } }, // 专门用于定义计算属性的 computed: { } }); </script> </body> </html> [代码] 通过js钩子执行动画 [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>30-Vue-过渡动画</title> <script src="js/vue.js"></script> <style> *{ margin: 0; padding: 0; } .box{ width: 200px; height: 200px; background: red; } .v-enter{ opacity: 0; } .v-enter-to{ opacity: 1; margin-left: 500px; } .v-enter-active{ transition: all 3s; } </style> </head> <body> <!-- 1.当前过渡存在的问题 通过transition+类名的方式确实能够实现过渡效果 但是实现的过渡效果并不能保存动画之后的状态 因为Vue内部的实现是在过程中动态绑定类名, 过程完成之后删除类名 正式因为删除了类名, 所以不能保存最终的效果 2.在Vue中如何保存过渡最终的效果 通过Vue提供的JS钩子来实现过渡动画 v-on:before-enter="beforeEnter" 进入动画之前 v-on:enter="enter" 进入动画执行过程中 v-on:after-enter="afterEnter" 进入动画完成之后 v-on:enter-cancelled="enterCancelled" 进入动画被取消 v-on:before-leave="beforeLeave" 离开动画之前 v-on:leave="leave" 离开动画执行过程中 v-on:after-leave="afterLeave" 离开动画完成之后 v-on:leave-cancelled="leaveCancelled" 离开动画被取消 3.JS钩子实现过渡注意点 3.1在动画过程中必须写上el.offsetWidth或者el.offsetHeight 3.2在enter和leave方法中必须调用done方法, 否则after-enter和after-leave不会执行 3.3需要需要添加初始动画, 那么需要把done方法包裹到setTimeout方法中调用 --> <!--这里就是MVVM中的View--> <div id="app"> <button @click="toggle">我是按钮</button> <!-- 注意点: 虽然我们是通过JS钩子函数来实现过渡动画 但是默认Vue还是回去查找类名, 所以为了不让Vue去查找类名 可以给transition添加v-bind:css="false" --> <transition appear v-bind:css="false" v-on:before-enter="beforeEnter" v-on:enter="enter" v-on:after-enter="afterEnter"> <div class="box" v-show="isShow"></div> </transition> </div> <script> // 这里就是MVVM中的View Model let vue = new Vue({ el: '#app', // 这里就是MVVM中的Model data: { isShow: true }, // 专门用于存储监听事件回调函数 methods: { toggle(){ this.isShow = !this.isShow; }, beforeEnter(el){ // 进入动画开始之前 console.log("beforeEnter"); el.style.opacity = "0"; }, enter(el, done){ // 进入动画执行过程中 console.log("enter"); /* 注意点: 如果是通过JS钩子来实现过渡动画 那么必须在动画执行过程中的回调函数中写上 el.offsetWidth / el.offsetHeight * */ // el.offsetWidth; el.offsetHeight; el.style.transition = "all 3s"; /* 注意点: 动画执行完毕之后一定要调用done回调函数 否则后续的afterEnter钩子函数不会被执行 * */ // done(); /* 注意点: 如果想让元素一进来就有动画, 那么最好延迟以下再调用done方法 * */ setTimeout(function () { done(); }, 0); }, afterEnter(el){ // 进入动画执行完毕之后 console.log("afterEnter"); el.style.opacity = "1"; el.style.marginLeft = "500px"; } }, // 专门用于定义计算属性的 computed: { } }); </script> </body> </html> [代码] 自定义类名动画 [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>32-Vue-过渡动画</title> <script src="js/vue.js"></script> <style> *{ margin: 0; padding: 0; } .box{ width: 200px; height: 200px; background: red; } .a{ opacity: 0; } .b{ opacity: 1; margin-left: 500px; } .c{ transition: all 3s; } </style> </head> <body> <!-- 1.自定义类名动画 在Vue中除了可以使用 默认类名(v-xxx)来指定过渡动画 除了可以使用 自定义类名前缀(yyy-xx)来指定过渡动画(transition name="yyy") 除了可以使用 JS钩子函数来指定过渡动画以外 还可以使用自定义类名的方式来指定过渡动画 enter-class // 进入动画开始之前 enter-active-class // 进入动画执行过程中 enter-to-class // 进入动画执行完毕之后 leave-class // 离开动画开始之前 leave-active-class // 离开动画执行过程中 leave-to-class // 离开动画执行完毕之后 --> <!--这里就是MVVM中的View--> <div id="app"> <button @click="toggle">我是按钮</button> <transition appear enter-class="a" enter-active-class="c" enter-to-class="b"> <div class="box" v-show="isShow"></div> </transition> </div> <script> // 这里就是MVVM中的View Model let vue = new Vue({ el: '#app', // 这里就是MVVM中的Model data: { isShow: true }, // 专门用于存储监听事件回调函数 methods: { toggle(){ this.isShow = !this.isShow; } }, // 专门用于定义计算属性的 computed: { } }); </script> </body> </html> [代码] Animate动画库的使用 [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>33-Vue-过渡动画</title> <script src="js/vue.js"></script> <style> *{ margin: 0; padding: 0; } .box{ width: 200px; height: 200px; background: red; } </style> <link href="https://cdn.jsdelivr.net/npm/animate.css@3.5.1" rel="stylesheet" type="text/css"> </head> <body> <!-- 1.配合Animate.css实现过渡动画 1.1导入Animate.css库 1.2在执行过程中的属性上绑定需要的类名 --> <!--这里就是MVVM中的View--> <div id="app"> <button @click="toggle">我是按钮</button> <transition appear enter-class="" enter-active-class="animated bounceInRight" enter-to-class=""> <div class="box" v-show="isShow"></div> </transition> </div> <script> // 这里就是MVVM中的View Model let vue = new Vue({ el: '#app', // 这里就是MVVM中的Model data: { isShow: true }, // 专门用于存储监听事件回调函数 methods: { toggle(){ this.isShow = !this.isShow; } }, // 专门用于定义计算属性的 computed: { } }); </script> </body> </html> [代码] v-for中的key作用 [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>35-Vue-过渡动画</title> <script src="js/vue.js"></script> <style> *{ margin: 0; padding: 0; } .v-enter{ opacity: 0; } .v-enter-to{ opacity: 1; } .v-enter-active{ transition: all 3s; } .v-leave{ opacity: 1; } .v-leave-to{ opacity: 0; } .v-leave-active{ transition: all 3s; } </style> </head> <body> <!-- 1.如何同时给多个元素添加过渡动画 通过transition可以给单个元素添加过渡动画 如果想给多个元素添加过渡动画, 那么就必须通过transition-group来添加 transition-group和transition的用法一致, 只是一个是给单个元素添加动画, 一个是给多个元素添加动画而已 --> <!--这里就是MVVM中的View--> <div id="app"> <form> <input type="text" v-model="name"> <input type="submit" value="添加" @click.prevent="add"> </form> <ul> <transition-group appear> <li v-for="(person,index) in persons" :key="person.id" @click="del(index)"> <input type="checkbox"> <span>{{index}} --- {{person.name}}</span> </li> </transition-group> </ul> </div> <script> // 这里就是MVVM中的View Model let vue = new Vue({ el: '#app', // 这里就是MVVM中的Model data: { persons: [ {name: "zs", id: 1}, {name: "ls", id: 2}, {name: "ww", id: 3} ], name: "" }, // 专门用于存储监听事件回调函数 methods: { add(){ let lastPerson = this.persons[this.persons.length - 1]; let newPerson = {name: this.name, id: lastPerson.id + 1}; // this.persons.push(newPerson); this.persons.unshift(newPerson); this.name = ""; }, del(index){ this.persons.splice(index, 1); } }, // 专门用于定义计算属性的 computed: { } }); </script> </body> </html> [代码] transition-group的使用 [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>35-Vue-过渡动画</title> <script src="js/vue.js"></script> <style> *{ margin: 0; padding: 0; } .v-enter{ opacity: 0; } .v-enter-to{ opacity: 1; } .v-enter-active{ transition: all 3s; } .v-leave{ opacity: 1; } .v-leave-to{ opacity: 0; } .v-leave-active{ transition: all 3s; } </style> </head> <body> <!-- 1.如何同时给多个元素添加过渡动画 通过transition可以给单个元素添加过渡动画 如果想给多个元素添加过渡动画, 那么就必须通过transition-group来添加 transition-group和transition的用法一致, 只是一个是给单个元素添加动画, 一个是给多个元素添加动画而已 --> <!--这里就是MVVM中的View--> <div id="app"> <form> <input type="text" v-model="name"> <input type="submit" value="添加" @click.prevent="add"> </form> <ul> <transition-group appear> <li v-for="(person,index) in persons" :key="person.id" @click="del(index)"> <input type="checkbox"> <span>{{index}} --- {{person.name}}</span> </li> </transition-group> </ul> </div> <script> // 这里就是MVVM中的View Model let vue = new Vue({ el: '#app', // 这里就是MVVM中的Model data: { persons: [ {name: "zs", id: 1}, {name: "ls", id: 2}, {name: "ww", id: 3} ], name: "" }, // 专门用于存储监听事件回调函数 methods: { add(){ let lastPerson = this.persons[this.persons.length - 1]; let newPerson = {name: this.name, id: lastPerson.id + 1}; // this.persons.push(newPerson); this.persons.unshift(newPerson); this.name = ""; }, del(index){ this.persons.splice(index, 1); } }, // 专门用于定义计算属性的 computed: { } }); </script> </body> </html> [代码] transition-group的注意点 [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>36-Vue-过渡动画</title> <script src="js/vue.js"></script> <style> *{ margin: 0; padding: 0; } .v-enter{ opacity: 0; } .v-enter-to{ opacity: 1; } .v-enter-active{ transition: all 3s; } .v-leave{ opacity: 1; } .v-leave-to{ opacity: 0; } .v-leave-active{ transition: all 3s; } </style> </head> <body> <!-- 1.transition-group注意点: 默认情况下transition-group会将动画的元素放到span标签中 我们可以通过tag属性来指定将动画元素放到什么标签中 2.transition-group动画混乱问题 一般情况下组动画出现动画混乱都是因为v-for就地复用导致的 我们只需要保证所有数据key永远是唯一的即可 --> <!--这里就是MVVM中的View--> <div id="app"> <form> <input type="text" v-model="name"> <input type="submit" value="添加" @click.prevent="add"> </form> <!-- <ul>--> <transition-group appear tag="ul"> <li v-for="(person,index) in persons" :key="person.id" @click="del(index)"> <input type="checkbox"> <span>{{index}} --- {{person.name}}</span> </li> </transition-group> <!-- </ul>--> </div> <script> // 这里就是MVVM中的View Model let vue = new Vue({ el: '#app', // 这里就是MVVM中的Model data: { persons: [ {name: "zs", id: 1}, {name: "ls", id: 2}, {name: "ww", id: 3} ], name: "", id: 3 }, // 专门用于存储监听事件回调函数 methods: { add(){ this.id++; // let lastPerson = this.persons[this.persons.length - 1]; let newPerson = {name: this.name, id: this.id}; // this.persons.push(newPerson); this.persons.unshift(newPerson); this.name = ""; }, del(index){ this.persons.splice(index, 1); } }, // 专门用于定义计算属性的 computed: { } }); </script> </body> </html> [代码] 组件化开发 自定义全局组件 [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>37-Vue组件-自定义全局组件</title> <script src="js/vue.js"></script> </head> <body> <!-- Vue两大核心: 1.数据驱动界面改变 2.组件化 1.什么是组件? 什么是组件化? 1.1在前端开发中组件就是把一个很大的界面拆分为多个小的界面, 每一个小的界面就是一个组件 1.2将大界面拆分成小界面就是组件化 2.组件化的好处 2.1可以简化Vue实例的代码 2.2可以提高复用性 3.Vue中如何创建组件? 3.1创建组件构造器 3.2注册已经创建好的组件 3.3使用注册好的组件 --> <!--这里就是MVVM中的View--> <div id="app"> <!--// 3.3使用注册好的组件--> <abc></abc> </div> <script> // 3.1创建组件构造器 let Profile = Vue.extend({ // 注意点: 在创建组件指定组件的模板的时候, 模板只能有一个根元素 template: ` <div> <img src="images/fm.jpg"/> <p>我是描述信息</p> </div> ` }); // 3.2注册已经创建好的组件 // 第一个参数: 指定注册的组件的名称 // 第二个参数: 传入已经创建好的组件构造器 Vue.component("abc", Profile ); // 这里就是MVVM中的View Model let vue = new Vue({ el: '#app', // 这里就是MVVM中的Model data: { }, // 专门用于存储监听事件回调函数 methods: { }, // 专门用于定义计算属性的 computed: { } }); </script> </body> </html> [代码] 自定义全局组件的注意点 首先vue.component(组件名,组件对象)可以代替vue.extend script标签中可以定义组件,加上id属性 template标签加上id属性才是vue官方的方式 [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>38-Vue组件-自定义全局组件</title> <script src="js/vue.js"></script> </head> <body> <!-- 1.创建组件的其它方式 1.1在注册组件的时候, 除了传入一个组件构造器以外, 还可以直接传入一个对象 1.2在编写组件模板的时候, 除了可以在字符串模板中编写以外, 还可以像过去的art-template一样在script中编写 1.3在编写组件模板的时候, 除了可以在script中编写以外, vue还专门提供了一个编写模板的标签template --> <!--这里就是MVVM中的View--> <div id="app"> <!--// 3.3使用注册好的组件--> <abc></abc> </div> <!-- <script id="info" type="text/html"> <div> <img src="images/fm.jpg"/> <p>我是描述信息</p> </div> </script> --> <template id="info"> <div> <img src="images/fm.jpg"/> <p>我是描述信息</p> </div> </template> <script> // 3.1创建组件构造器 /* let Profile = Vue.extend({ // 注意点: 在创建组件指定组件的模板的时候, 模板只能有一个根元素 template: ` <div> <img src="images/fm.jpg"/> <p>我是描述信息</p> </div> ` }); */ /* let obj = { // 注意点: 在创建组件指定组件的模板的时候, 模板只能有一个根元素 template: ` <div> <img src="images/fm.jpg"/> <p>我是描述信息</p> </div> ` }; */ // 3.2注册已经创建好的组件 // 第一个参数: 指定注册的组件的名称 // 第二个参数: 传入已经创建好的组件构造器 // Vue.component("abc", Profile ); // Vue.component("abc", obj ); /* Vue.component("abc", { // 注意点: 在创建组件指定组件的模板的时候, 模板只能有一个根元素 template: ` <div> <img src="images/fm.jpg"/> <p>我是描述信息</p> </div> ` }); */ Vue.component("abc", { // 注意点: 在创建组件指定组件的模板的时候, 模板只能有一个根元素 template: "#info" }); // 这里就是MVVM中的View Model let vue = new Vue({ el: '#app', // 这里就是MVVM中的Model data: { }, // 专门用于存储监听事件回调函数 methods: { }, // 专门用于定义计算属性的 computed: { } }); </script> </body> </html> [代码] 自定义局部组件 [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>39-Vue组件-自定义局部组件</title> <script src="js/vue.js"></script> </head> <body> <!-- 1.自定义全局组件特点 在任何一个Vue实例控制的区域中都可以使用 2.自定义局部组件特点 只能在自定义的那个Vue实例控制的区域中可以使用 3.如何自定义一个局部组件 在vue实例中新增components: {} 在{}中通过key/vue形式注册组件 components:{ abc: {} } --> <!--这里就是MVVM中的View--> <div id="app1"> <abc></abc> </div> <div id="app2"> <abc></abc> </div> <template id="info"> <div> <img src="images/fm.jpg"/> <p>我是描述信息</p> </div> </template> <script> /* // 自定义全局组件 Vue.component("abc", { // 注意点: 在创建组件指定组件的模板的时候, 模板只能有一个根元素 template: "#info" }); */ // 这里就是MVVM中的View Model let vue1 = new Vue({ el: '#app1', // 这里就是MVVM中的Model data: { }, // 专门用于存储监听事件回调函数 methods: { }, // 专门用于定义计算属性的 computed: { } }); // 这里就是MVVM中的View Model let vue2 = new Vue({ el: '#app2', // 这里就是MVVM中的Model data: { }, // 专门用于存储监听事件回调函数 methods: { }, // 专门用于定义计算属性的 computed: { }, // 专门用于定义局部组件的 components: { "abc": { // 注意点: 在创建组件指定组件的模板的时候, 模板只能有一个根元素 template: "#info" } } }); </script> </body> </html> [代码]
2021-11-16 - vue发送ajax请求
常用发送ajax请求的方法 xhr jQury $.get $.post axios fetch window内置,promise风格,会把返回数据包两层promise,而且兼容不好 v-resouce [代码]//server1.js const express = require('express') const app = express() app.use((request,response,next)=>{ console.log('有人请求服务器1了'); // console.log('请求来自于',request.get('Host')); // console.log('请求的地址',request.url); next() }) app.get('/students',(request,response)=>{ const students = [ {id:'001',name:'tom',age:18}, {id:'002',name:'jerry',age:19}, {id:'003',name:'tony',age:120}, ] response.send(students) }) app.listen(5000,(err)=>{ if(!err) console.log('服务器1启动成功了,请求学生信息地址为:http://localhost:5000/students'); }) [代码] [代码]//server2.js const express = require('express') const app = express() app.use((request,response,next)=>{ console.log('有人请求服务器2了'); next() }) app.get('/cars',(request,response)=>{ const cars = [ {id:'001',name:'奔驰',price:199}, {id:'002',name:'马自达',price:109}, {id:'003',name:'捷达',price:120}, ] response.send(cars) }) app.listen(5001,(err)=>{ if(!err) console.log('服务器2启动成功了,请求汽车信息地址为:http://localhost:5001/cars'); }) [代码] 服务器1为:http://localhost:5000/students 服务器2地址为:http://localhost:5001/cars [代码]//App.vue <template> <div> <button @click="getStudents">获取学生信息</button> </div> </template> <script> import axios from 'axios' export default { name:'App', methods:{ getStudents:function(){ axios.get('http://localhost:5000/students').then( response=>{ console.log('请求成功',response.data) }, error=>{ console.log('请求失败',error.message) } ) } } } </script> [代码] 这样子,会有跨域问题,当前处于http://localhost:8080,请求服务器1为:http://localhost:5000/students 什么是跨域? 浏览器从一个域名的网页去请求另一个域名的资源时,域名、端口、协议任一不同,都是跨域 主域名不同 http://www.baidu.com/index.html http://www.sina.com/test.js 子域名不同 http://www.666.baidu.com/index.html http://www.555.baidu.com/test.js 域名和域名ip http://www.baidu.com/index.html http://180.149.132.47/test.js 端口: http://www.baidu.com:8080/index.html http://www.baidu.com:8081/test.js 协议: http://www.baidu.com:8080/index.html https://www.baidu.com:8080/test.js 备注: 1、端口和协议的不同,只能通过后台来解决 2、localhost和127.0.0.1虽然都指向本机,但也属于跨域 解决跨域 cors 写服务器的人在写服务器时给响应信息加上特殊的响应头 jsonp 代理服务器 代理服务器在中间出现,代理服务器所处位置和请求数据端位置一样,请求方向代理服务器要数据,代理服务器向服务器要数据,然后原路返回。代理服务器和服务器之间没有跨域问题,是通过http请求(ngnix、vue-cli) vue配置代理 方式一 [代码]//vue.config.js module.exports = { pages: { index: { //入口 entry: 'src/main.js', }, }, lintOnSave:false,//关闭语法检查, //配置代理服务器 devServer:{ proxy:'http://localhost:5000'//只写到端口号就行 } } [代码] [代码]//App.vue <template> <div> <button @click="getStudents">获取学生信息</button> </div> </template> <script> import axios from 'axios' export default { name:'App', methods:{ getStudents:function(){ axios.get('http://localhost:8080/students').then( response=>{ console.log('请求成功',response.data) }, error=>{ console.log('请求失败',error.message) } ) } } } </script> [代码] [图片] 说明: 优点:配置简单,请求资源时直接发给前端(8080)即可。 缺点:不能配置多个代理,不能灵活的控制请求是否走代理。 工作方式:若按照上述配置代理,当请求了前端不存在的资源时,那么该请求会转发给服务器 (优先匹配前端资源),如果前端有的话,代理服务器不会转发请求给服务器 方式二 协议名、主机名、端口号后面加前缀api,灵活控制走不走本地,有就让代理服务器转发,但是给服务器的地址不能带前缀 [代码]//vue.config.js module.exports = { pages: { index: { //入口 entry: 'src/main.js', }, }, lintOnSave:false,//关闭语法检查, devServer:{ proxy:{ '/api':{ //target:'<url>', target:'http://localhost:5000', pathRewrite:{'^/api':''},//正则,重写路径,匹配所有有/api的字符串替换成空 ws:true,//用于支持websocket changeOrigin:true //如果为true,告诉服务器来自和服务器同一位置(http://localhost:5000),其实就是请求头中的host信息。 }, '/api2':{ //target:'<url>', target:'http://localhost:5000', pathRewrite:{'^/api2':''}, ws:true,//用于支持websocket changeOrigin:true //用于控制请求中的host值 } } } } [代码] [代码]//App.vue <template> <div> <button @click="getStudents">获取学生信息</button> </div> </template> <script> import axios from 'axios' export default { name:'App', methods:{ getStudents:function(){ axios.get('http://localhost:8080/api/students').then( response=>{ console.log('请求成功',response.data) }, error=>{ console.log('请求失败',error.message) } ) } } } </script> [代码] 说明: 优点:可以配置多个代理,且可以灵活的控制请求是否走代理。 缺点:配置略微繁琐,请求资源时必须加前缀。 vue-resource 和axios的使用相同,在vue1.0时较多使用 [代码]//main.js //引入Vue import Vue from 'vue' //引入App import App from './App.vue' //引入插件 import vueResource from 'vue-resource' //关闭Vue的生产提示 Vue.config.productionTip = false //使用插件 Vue.use(vueResource) //创建vm new Vue({ el:'#app', render: h => h(App), beforeCreate() { Vue.prototype.$bus = this }, }) [代码] [代码] this.$http.get(`https://api.github.com/search/users?q=${this.keyWord}`).then( response => { console.log('请求成功了') //请求成功后更新List的数据 this.$bus.$emit('updateListData',{isLoading:false,errMsg:'',users:response.data.items}) }, error => { //请求后更新List的数据 this.$bus.$emit('updateListData',{isLoading:false,errMsg:error.message,users:[]}) } ) [代码]
2021-11-16 - 微信小程序开发-将数据写入全局数据
微信小程序的全局数据写在 [代码]app.js[代码]中,需要现在里面声明存储数据的变量如下: [代码]//app.js this.globalData = {} this.userInfo = {}//这个是我们。等下要用到的变量// this.userMessage = [] [代码] 然后在需要使用以上声明的全局变量的时候声明一下: [代码]const app = getApp()//这个声明是为了后面调用的方便 [代码] 然后使用[代码]Object.assign()[代码]将数据拷贝到全局变量中。 注:[代码]Object.assign()[代码]用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。 [代码]Object.assign( app.userInfo , res.data ); [代码] 此时res.data中的数据已经传输到全局变量中。在其他页面使用使用[代码]getApp()[代码]即可调用。
2021-11-16 - 小程序开发-图片上传到云存储
1、获取用户手机中的照片路径 使用[代码]wx.chooseImage[代码] 微信开发者文档中的介绍 https://developers.weixin.qq.com/miniprogram/dev/api/media/image/wx.chooseImage.html 下面是一个demo,其中success会返回本地图片的路径,此时已经可以用来做临时预览了,但是我们还要上传到云存储中,这样才算是更新图片成功。 [代码] wx.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['album', 'camera'], success : (res)=>{ //console.log(res); const tempFilePaths = res.tempFilePaths[0]; this.setData({ userPhoto: tempFilePaths }); } }) [代码] 2、上传 使用[代码]wx.cloud.uploadFile[代码] 微信开发者文档介绍 上一步获得的地址存储在 userPhoto 中,将用户的openid设为改文件的路径。 ```Date.now()``为当前的时间戳,为文件添加唯一的标识 [代码]let cloudPath = "userPhoto/" + app.userInfo._openid + Date.now() + ".jpg"; wx.cloud.uploadFile({ cloudPath, filePath: this.data.userPhoto }) [代码] 3、将存储好的数据更新到用户的数据中,这样用户就可以使用云存储中的图片。 在成功调用完上一个函数之后,会返回一个fileID,存储着上传成功的文件的路径。 [代码]let cloudPath = "userPhoto/" + app.userInfo._openid + Date.now() + ".jpg"; wx.cloud.uploadFile({ cloudPath, filePath: this.data.userPhoto }).then((res)=>{ //console.log(res); let fileID = res.fileID; if(fileID){ db.collection('users').doc(app.userInfo._id).update({ data : { userPhoto : fileID } }) [代码] 使用[代码]db.collection('users').doc(app.userInfo._id).update[代码]将fileID更新到用户的数据列表中即可。 还要记得更新此时的全局变量,然后再渲染一遍用户的头像,这样就更新成功了。
2021-11-16 - 微信小程序的git管理
*原来这个东西还有这么多没搞清楚的地方,太难了* - 以下内容分为空的gitee仓库,将微信开发这工具导入gitee和从gitee上扒下代码导入到空的本地项目中。 --------------------------------------------------------------------------------------- 1、创建gitee仓库 - 直接在gitee上创建一个新的仓库就好了,新建,就一个空的。等着把代码从微信开发者工具导进去就好了 2、开发者工具初始化本地仓库 - 打开微信开发者工具想要共同开发的那个项目,点击版本管理,点击弹出的框中的确定,就可以初始化完毕了。 3、连接gitee仓库和微信开发者工具 - 点击微信开发这工具中的```版本管理```——>```设置```——>```远程```——>```添加```。 然后在弹出的框中填入gitee仓库的名称和网址就好了。 因为要对这个仓库进行操作,所以你得验证你的操作权限吧,这个时候就找到```设置```——>```网路和认证```——>```认证方式```——>```使用用户名和密码```。然后填出你的gitee注册邮箱和密码,即可获得你在gitee上拥有的对改仓库的权限。 **获取权限** 如果你是改仓库的创建者,那么你就拥有这个仓库的拥有者的权限,可以邀请队友进入仓库进行开发;如果你要加入别人的仓库,就点击拥有者的分享的链接就可以获得相应的权限。 4、上传和导入的操作 - 1、将本地文件的修改上传到gitee仓库中。 本地文件的修改情况会先在”工作区“中展现,需要将工作区中的变更提交到本地的git仓库上。 [图片] [图片] 2、将本地仓库的修改推送到gitee仓库中,找到推送按钮,选择要推送的本地分支和要推送到的远程仓库分支,点击确定即可完成推送: [图片] 3、导入gitee仓库代码。 先直接从gitee中克隆一份仓库中的完整的代码,然后将这份代码导入到新建的空白项目中。就可以完成整体项目的导入。若以后仓库中有代码发生更改,则使用左上角的“抓取”按钮,获得gitee上的最新修改。接下来: 1、点击“远程”里面的“master,将HEAD重置到最新的记录中,这时候工作区就会有最相信的修改信息 2、填写标题,将工作区中的修改提交到本地的仓库中。 [图片] 3、在这边可以看到细节修改,接下来右键点击修改的文件,选择: [图片] 然后在本地的文件中就会添加出来仓库中最新的修改。
2021-11-16 - 微信小程序 订阅消息使用 详细教程
微信小程序订阅消息使用 官方文档连接 本说明的使用场景例子为:用户在组队大厅发送招募信息,后台管理系统收到后对用户的信息进行审核,并将审核结果,通过消息推送发送给用户。 逻辑步骤 逻辑步骤0:等待用户点击触发需要发送消息的事件 逻辑步骤1:向用户申请获得发送消息的权限 逻辑步骤2:等待发送的触发行为 逻辑步骤3:向特定用户发送消息 操作步骤 操作步骤1:在微信公众号平台获得合适的模板 [图片] 在公共模板库搜索适合的模板,找不到的话可以申请。 操作步骤2:编写查询发送权限函数 写一个获得访问权限的函数(也可以直接复制我的),封装好API里面,后面还需要用的话方便调用。 [代码]const checkSub = async(params)=>{//传入tmplId,检测用户是否开放权限,允许推送消息 var tmplIds = params.tmplIds //这里的tmplId是一个数组得注意一下 console.log(tmplIds) return new Promise((resolve,reject)=>{ wx.getSetting({ withSubscriptions: true, success(res){ console.log(res) if(res.subscriptionsSetting.itemSettings!=undefined){ var flag = res.subscriptionsSetting.itemSettings[tmplIds[0]] }else{ var flag = undefined } console.log(flag) if(flag==undefined){ console.log("debug") wx.requestSubscribeMessage({ tmplIds: tmplIds, success(res){ console.log(res) //点击完成后就返回成功就行 }, }) }else if(flag!='accept'){ wx.requestSubscribeMessage({ tmplIds: tmplIds, success(res){ console.log(res) resolve(true)//点击完成后就返回成功就行 } }) }else{//直接返回true,原本以为用户选择一直同意之后,就可以一直推送,这里是一个bug wx.requestSubscribeMessage({ tmplIds: tmplIds, success(res){ console.log(res) resolve(true)//点击完成后就返回成功就行 } }) } } }) }) } [代码] 操作步骤3:用户触发点击事件,调起询问权限 在用户触发的事件开始前,调用询问权限函数,获得发送订阅消息的权限 在本例子中,用户提交组队的表单,在成功提交前,询问用户是否接受“审核通过通知”,无论用户选择是或否,表单信息都可以顺利提交。但是选择否,用户则无法顺利收到订阅信息。 代码如下 [代码]submit: async function() { if(!this.checkTap()){ //防止用户多次点击,重复提交的函数 return 0 } const { contestName,imgs,mates,status,currNum } = this.data var imgUrls = []; for (var i=0; i<imgs.length; i++) { imgUrls.push(imgs[i].url); }//提交数据的图片连接 var tmplIds = ['复制你的templId进来']//审核通过通知权限 var param = {tmplIds} api.subscription.checkSub(param).then(res=>{//在这里调用检查函数 var params = {imgs: imgUrls,contestName,mates,status,currNum} api.team.publish(params).then(res=>{ //提交表单的函数 //提交成功 }) }) } [代码] PS:好了,现在用户点击了"是"之后,我们就可以向用户推送消息了,但是机会只有一次 ! 要好好把握 ! 操作步骤4: 编写发出推送的函数 这里我们使用云函数,可以很方便的使用, 将不同的模板封装在同一个函数里面,使用哪个就调用哪个. 下面是我的代码 [代码]const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database(); const _ = db.command exports.main = async (event, context) => { switch(event.action) { //通过传入的参数 action 来选择模板,其他模板为了方便阅读我删掉了,直接复制一下就搞自己的模板啦 case 'send1' :return send3(event)//审核模板通过模板 } } async function send1(event){ const {touser,status} = event.data //touser 是发送给用户的openid now_db = formatDate2(event.data.now_db) createTime = formatDate2(event.data.createTime) //待审核信息的创建时间 try{//这部分data的内容,和微信公众平台上的模板相对应 var data = { thing2: { value: "您发布的信息已审核" }, phrase1: { value:status//通过或拒绝 }, date3:{ value:now_db//现在的时间 }, date4:{ value:createTime //消息创建的时间 } } const result =await cloud.openapi.subscribeMessage.send({ // 此处发送 touser:touser, page: 'pages/user/index/index', //用户点击消息后进入程序的页面 data: data, templateId: '这里复制你的templateId', miniprogramState: 'developer' // 跳转小程序类型:developer为开发版;trial为体验版;formal为正式版;默认为正式版 }); return result } catch(err){ return err } } function formatDate2 (time) {//时间处理函数 const date = new Date(time); const year = date.getFullYear(); const month = date.getMonth() + 1; const day = date.getDate(); const hour = date.getHours(); const minute = date.getMinutes(); return year + '年' + [month, day].map(formatNumber).join('月') + '日 ' + [hour, minute].map(formatNumber).join(':'); } function formatNumber(n) { n = n.toString(); return n[1] ? n : '0' + n; } [代码] 操作步骤5:向用户推送消息的时间到,发出推送 就是,我们设定的推送时间,或者其他用户触发了向用户发送给推送消息的事,那么就是现在,发出推送. 这个例子中,就是管理员进行了操作,将用户的组队信息状态,从"wait"等待审核,变成了"accept",通过审核.状态发生改变的同时,我们向用户发出推送. [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() const _ = db.command const $ = db.command.aggregate // 云函数入口函数 exports.main = async (event, context) => { switch (event.action) {//通过传入action参数选择函数 case 'changeStatus': return changeStatus(event) default: return Promise.reject("unknown action") } } async function changeStatus(event){//管理员改变信息的状态 var {teamID,touser,status} = event var mp = {'reject':'拒绝','need':'通过'}//通过mp将英文转换为中文 var status2 = mp[status] var time = new Date() var createTime = event.createTime console.log(createTime) return new Promise((reslove,reject)=>{ db.collection('team').doc(teamID) .update({ data:{ status:status //更新信息状态 } }) .then(res=>{//成功之后发送订阅消息 cloud.callFunction({ name:'subscribe', data:{ action:"send3", data:{status:status2,touser:touser,now_db:time,createTime:createTime} }//这里的数据都需要和模板中的数据对应,touser就是发送给目标用户的openid }).then(res=>{ console.log(res) }).catch(err=>{ console.log(err) }) reslove({code:200,message:"转换成功",data:res}) }) .catch(err=>{ reject({code:300,message:"转换失败",data:err}) }) }) } [代码] 测试 这里因为开发者工具和真机调试弹出来的页面有所不同,所以推荐最好是使用真机进行调试. [图片] 用户点击允许之后,再触发发送函数,就可以收到消息了,一开始可以用自己的openid作为touser,来进行测试. !!!这里注意!!! 如果用户选择了"总是保持以上选择,不在询问",那么以后都不会调起这个界面,但是用户点击一次允许,我们才获得一次发送订阅消息的能力.所以这里会有一个BUG,目前还没有找到解决的方案.只能希望用户不选择这个了. 那么就到这里结束了,有什么问题欢迎留言交流,欢迎点赞关注收藏…好耶!
2021-11-16 - 小程序前端开发实战
4.1学习对象+课程目的+推荐工具+学习方法 [视频] 4.2微信小程序平台介绍 [视频] 4.3微信开发者工具介绍 [视频] 4.4微信小程序代码框架介绍 [视频] 4.5微信小程序代码wxml学习 [视频] 4.6微信小程序代码wxss学习 [视频] 4.7微信小程序代码js学习 [视频] 4.8微信小程序js中的数据处理 [视频] 4.9微信小程序代码API介绍h [视频] 4.10微信小程序前端实现思路(1) [视频] 4.10微信小程序前端实现思路(2) [视频] 4.11手把手1:注册界面(1) [视频] 4.11手把手1:注册界面(2) [视频] 4.12自主实操1:登录界面 [视频] 4.13手把手2:发布新树洞界面 [视频] 4.14手把手3:树洞广场界面(1) [视频] 4.14手把手3:树洞广场界面(2) [视频] 4.14手把手3:树洞广场界面(3) [视频] 4.15自主实操2:我的树洞界面 [视频] 4.16课后小练 [视频] 4.17本章小结 [视频] 第四章结束后全部代码,点此领取。
2021-09-15 - 小程序路由的跳转
在微信小程序实战视频中(https://developers.weixin.qq.com/community/business/doc/00008c2f9d83a040de5ad3a945b80dw ),由于编写注册页面代码时,视频仅仅是使wx.navigateBack调回上n级页面:(delta表示第几级,例:1表示上一级) [图片] 但是在登录页面由于是自己完成的小作业,而且作为登录页面,逻辑上先于注册页面出现,注册页面自然也就不是登录页面的上一级页面,所以我把目光放在了wx.redirectTo,如下: [图片] 但是出现了如下bug,页面not found: [图片] 根据报错信息,我们可以看到url:pages/login/pages/enroll/enroll,这是为什么呢?我明明写的是pages/enroll/enroll为什么前面还多了个前缀? 原来这个是作为相对路径寻找页面的,而我写的确实绝对路径,自然出现bug。 那好,我现在修改好了,总不会出bug了吧。 [图片] 确实,我可以正确跳转到了注册页面,那我们再点击返回登录页面试试?然后,没错,又出bug了,如下: [图片] 找不到前一个页面?明明我是从登录页面过来的,怎么会找不到前一个页面呢?然后我去看了一些微信开发者文档https://developers.weixin.qq.com/miniprogram/dev/api/route/wx.navigateBack.html 查询了wx.navigateBack的相关信息,如下: [图片] 原来,使用wx.redirectTo并不会把前一个页面保存到堆栈,而wx.navigateBack是从堆栈中寻找先前页面,自然也就找不到先前页面 所以,我改用了wx.navigateTo函数,不出意料,这次成功了 [图片] [图片]、 反复横跳,流畅无比,哈哈。 感谢各位观看,期待下次再见。 如果觉得写得还不错,可以给文章点个赞哦~ [图片]
2021-11-15 - 微信小程序前端开发踩坑——引入weui组件库
前言 今天在写微信小程序前端页面,想引入weui组件库来完成开发。结果按着官方文档来遇到了一堆问题,最后靠着不断百度查资料才最终解决。所以将过程记录一下,避免后面再遇到这类坑。 注意:本文默认读者已知道怎么使用npm 1. 初始化 以管理员身份运行命令行窗口(cmd),在cmd中进入项目的根目录。然后输入以下命令: [代码]npm init [代码] 后面一路按回车健即可,最终会在项目的根目录中创建出一个名为package.json的文件。 2. 安装weui组件库 在cmd中紧接着输入以下命令: [代码]npm install weui-miniprogram [代码] 命令执行完毕后会多出来一个node_modules文件夹,里面包含了weui组件库。 3. 构建npm 在微信开发者工具中,选择“工具”->“构建npm”。如无意外会出现类似“没有找到可以构建的NPM包……”这样的报错。 这时就需要在项目根目录找到package.config.json文件,修改相关的配置如下: [代码]{ ... "setting": { ... "packNpmManually": true, "packNpmRelationList": [ { "packageJsonPath": "./package.json", "miniprogramNpmDistDir": "./" } ] } } [代码] 继续在开发者工具中的“详情”->“本地设置”里检查是否勾选上“使用npm模块”选项,若没勾选则勾选上。 完成上述配置后,重新构建npm,即可构建完成。 4. 重启项目 在开发者工具中“项目”->“重新打开此项目”,完成对项目的重启。 注意:这一步非常重要!!!否则引入组件会提示找不到文件!!! 5. 引入wxss 在app.wxss中,引入weui库的wxss文件 [代码]@import 'miniprogram_npm/weui-miniprogram/weui-wxss/dist/style/weui.wxss'; [代码] 引入时要根据实际情况调整路径,但最长后缀均为 [代码]/weui-miniprogram/weui-wxss/dist/style/weui.wxss [代码] 6. 引入组件 在想要使用组件的页面对应的js文件中,对组件进行的引入。一定要注意自己项目的目录结构!!! [图片] 而官方文档的写法是 [图片] 如果直接照搬官方文档的写法,则忽略了目录结构,会报错!! 接着在要使用组件的页面对应的wxml文件中使用该组件即可 [代码]<mp-dialog title="test" show="{{true}}" bindbuttontap="tapDialogButton" buttons="{{[{text: '取消'}, {text: '确认'}]}}"> <view>test content</view> </mp-dialog> [代码] 效果如下: [图片] 后记 不得不说,前端开发的坑实在是太多了,上面记录的过程我摸索了一个多小时。看来平时一定要多注意总结才行,不然真的非常消耗时间!!! 创作不易,觉得有用麻烦点个赞,谢谢~~~
2022-01-11 - 小程序云开发学习笔记4
前面记录了云开发数据库的一些增删改查的操作,发现有一些权限问题,这其实涉及到云函数。其实前面操作云开发环境下的数据库有点类似Java后端操作本地3306端口的sql数据库,但是本地数据库自己玩玩就行,其他ip地址的主机获取不到,这时候就需要服务器。 云函数其实就是一种服务器,传统服务器部署需要很多步骤: 1. 花钱购买腾讯云等服务器 2. 在获取Linux服务器上配置Java或者其他语言环境,配置数据库环境 3. 在云服务器官网开放对应端口允许访问 4. 本地编写后端代码,上传部署至服务器 5. 前端编写代码获取服务器运行程序的接口 6. 如果要上线,还需要购买域名,进行域名备案和安全设置 对于一名全栈程序员来说,就要求掌握前后端知识,数据库知识,linux系统指令,如果有多人协作分布式开发,也需要彼此互相沟通编写文档才能使用,这样搭建的服务器虽然性能稳定但是费时费力。这是我自己写的后端获取用户openid返回前端的代码: [图片] 这里需要很多行代码,且需要前后端配合完成。 小程序云函数帮我们进行傻瓜式部署,也不要求我们自己进行域名备案和安全设置,比较适合小白,想获取openid,就只需要5行左右代码即可实现。 接下来,就来看看如何使用云函数: 配置云函数只需要两步: [图片] 然后需要在cloud文件右击选择创建Node.js云函数,之后我们就可以在js中调用云函数,先获取一下openid: [图片] 之后,我们获取一下数据库中的数据,这里是从云函数获取,相当于数据在服务器上,而不是仅仅在我们的本地小程序,当然,首先也要配置一下云环境: [图片] 获取数据跟之前数据库一样 [图片] 这样,在js中调用wx.cloud.callfunction时,res中的数据就是云函数入口函数返回的数据。 还要注意的是,熟悉后端的都清楚,每次代码修改,都需要重新部署到服务器上,可以手动上传再运行,也可以用docker容器进行部署(有点阴间,我没有成功),而云函数也是服务器,每次代码修改都需要重新部署,不过,这里的部署就简单许多,只需要右击cloud文件夹,选择“上传并部署”即可。 前面讲到过,云开发wx.cloud.database()直接修改和删除数据存在一定的权限问题,而使用云函数就不会有这种权限问题,因为使用云函数是获得了超级权限。云函数操作数据库其实跟之前的差不多,主要的区别在于js要先向云函数传递参数,云函数根据参数进行数据库操作,最后js再调用改云函数,所以一次操作就需要创建一个云函数。 这里以修改数据为例,首先创建两个对应的云函数,注意先要用cloud.init()初始化环境: [图片] 然后云函数需要从js中获取参数: [图片] 接着,云函数调用数据库操作方法,返回结果 [图片] 修改成功: [图片]
2021-11-13 - 微信小程序前端开发黑科技——前后端交互
1. 问题引入 现在正在学习小程序开发。在开发过程中,前后端交互是非常重要的一环。我选择的是自己搭建后台、前后端分离的开发方式。在这种情况下,按照官方文档的说法,我只能自己配置域名或使用局域网通信。但我想节约学习成本,该怎么办呢? 2. 问题解决——使用工具ngrok 2.1. 下载ngrok 通过这个链接下载即可:https://ngrok.com/download [图片] 解压后,就一个exe文件 [图片] 2.2. 注册获得Authtoken 回到官网进行注册。如果已经有GitHub账号,建议直接使用GitHub账号进行登录并完成注册,因为这外网的注册一直有些问题。 完成注册并登录后,获取Authtoken(已打码) [图片] 2.3. 配置代理域名 打开下载解压后的exe文件,输入命令 [代码]ngrok authtoken yourauthtoken [代码] 将yourauthtoken换成自己的Authtoken即可 然后根据开发需要,配置相应的端口(这里以8080端口为例) [代码]ngrok http 8080 [代码] 可以得到如下结果 [图片] 红框中生成的即为代理域名,任选一个即可。 2.4. 修改微信开发者工具中的设置 [图片] 这样就完成配置了。这时候我们就可以在外网访问内网,那样子我们手机不用与电脑在同一网段(即手机用4g,电脑连wifi)也可以进行测试。 2.5. 测试验证 后端的测试代码(go语言,gin框架) [代码]package main import "github.com/gin-gonic/gin" func main() { r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{"message": "pong"}) }) r.Run() } [代码] 前端发送请求的测试代码 [图片] 前端输出的测试结果 [图片] 后端输出的测试结果 [图片] 测试结果表明前后端成功完成交互,测试成功! 3. 问题拓展——内网穿透 看懂该部分需要有计算机网络的知识。 首先来描述一下问题的本质。在本机调试中,自己的电脑或服务器在没有配置域名的情况下,相当与内网;而微信开发者工具却是在外网上运行的(需要使用自己的微信号登录)。当微信小程序前端向自己搭建的后端发出请求时,相当于外网主动访问内网。所以问题的本质就是外网对内网的访问。 由计算机网络的知识可以知道这显然是不行的。这时有两种解决方式。 第一种是给服务器配置域名,相当于让内网主动接入外网,这样外网就能访问到了。 第二种是使用内网穿透。即让在内网的节点主动访问一个拥有公网IP地址的服务器,并由中间服务器搭桥,打通经过该服务器从其他主机到NAT之后节点的隧道。结合该问题通俗地讲,就是找了一个服务器作为中转站,让微信小程序先将消息发到中转服务器上,再让中转服务器将消息发送到本机的后台,这样就完成了前后端交互通信。 4. 心得 平时总觉得专业课学的东西没啥用,跟空中楼阁一样排不上用场。这次解决问题的过程却用到了许多计算机网络的知识。我不禁感概,果然技术还是需要实践的,只有真正动手了才能更好地理解所学的专业知识。未来我一定要好好复习计算机基础知识,进一步把专业知识的功底打扎实! 参考资料: [1]. 博客:https://blog.csdn.net/nanshenjiang/article/details/87889983 [2]. 知乎:https://zhuanlan.zhihu.com/p/303175108 [3]. 《计算机网络:自顶向下方法》(原书第七版)
2021-11-15 - 提交小程序审核时,提示“系统繁忙,请稍后再试”的一些解决方法
更改浏览器重新尝试一下 可能是由于浏览器的原因,我一般是使用谷歌浏览器,兼容性比较好 使用小程序助手,试着在小程序提交审核一下试试 之前有次就是电脑上提交不上去,然后用手机就可以提交 尝试一下修改版本号 如果上面两种方法都不可以的话,可以试着修改一下版本号
2021-11-15 - 小程序实战中登录页面代码分享
由于视频教程(https://developers.weixin.qq.com/community/business/doc/00008c2f9d83a040de5ad3a945b80d)中仅仅是布置了编写登录页面的作业,在下一节并没有代码呈现,所以我把我编写好的代码发出来,供大家交流使用: login.json文件{ "usingComponents": {}, "navigationBarBackgroundColor": "#fff", "navigationBarTitleText": "洞主登录", "navigationBarTextStyle":"black" } login.wxml文件<!--pages/login/login.wxml--> <view class="background"> <view class="first"> <input id='account' placeholder="用户名/邮箱/手机号" placeholder-class="plas" type="text" class="inputs" bindinput="accountInput" /> </view> <view class="second"> <input id='pwd' placeholder="登录密码" placeholder-class="plas" type="password" class="inputs" bindinput=passwordInput" /> </view> <view id='btn' class="click" bindtap='signin'>注册</view> <view class="cha"> <text class="no" bindtap="regist">还没有账号?点我注册</text> </view> </view> login.wxss/* pages/login/login.wxss */ pages { left: 0rpx; right: 0rpx; background-color: white; } .first { width: 90%; height: 100rpx; margin-top: 80rpx; margin-left: 5%; margin-right: 5%; display: flex; /*Flex是Flexible Box的缩写,意为"弹性布局",用来为盒状模型提供最大的灵活性*/ flex-direction: row; align-items: center; justify-content: center; background-color: #f2f2f2; } .second { width: 90%; height: 100rpx; margin-top: 30rpx; margin-left: 5%; margin-right: 5%; display: flex; /*Flex是Flexible Box的缩写,意为"弹性布局",用来为盒状模型提供最大的灵活性*/ flex-direction: row; align-items: center; justify-content: center; background-color: #f2f2f2; } .flas { font-size: 30rpx; color: #CCCCCC; } .inputs { line-height: 100rpx; font-size: 30rpx; color: #000000; margin: auto; margin-left: 20rpx; width: 100%; } .click { width: 90%; height: 100rpx; line-height: 100rpx; margin: auto; margin-top: 80rpx; margin-left: 5%; margin-right: 5%; background-color: #F76968; text-align: center; color: white; font-size: 33rpx; } .cha { width: 90%; height: 50rpx; margin: auto; margin-top: 30rpx; margin-left: 5%; margin-right: 5%; } .no { color: black; font-size: 28rpx; margin-left: 15rpx; font-family: PingFangSC-regular; } login.js// pages/login/login.js Page({ /** * 页面的初始数据 */ data: { account:'', password:'' }, accountInput:function (e) { this.data.account = e.detail.value }, passwordInput:function (e) { this.data.password = e.detail.value }, regist:function (e) { wx.navigateTo({ url: '../enroll/enroll', }) }, signin:function (e) { var that = this if(that.data.account==''){ wx.showModal({ title:"提示", content:"请输入用户名/邮箱/手机号", showCancel:false, success(res){} }) }else if(that.data.password==''){ wx.showModal({ title:"提示", content:"请输入密码", showCancel:false, success(res){} }) }else{ console.log('success') } }, /** * 生命周期函数--监听页面加载 */ onLoad: function (options) { }, /** * 生命周期函数--监听页面初次渲染完成 */ onReady: function () { }, /** * 生命周期函数--监听页面显示 */ onShow: function () { }, /** * 生命周期函数--监听页面隐藏 */ onHide: function () { }, /** * 生命周期函数--监听页面卸载 */ onUnload: function () { }, /** * 页面相关事件处理函数--监听用户下拉动作 */ onPullDownRefresh: function () { }, /** * 页面上拉触底事件的处理函数 */ onReachBottom: function () { }, /** * 用户点击右上角分享 */ onShareAppMessage: function () { } }) 代码如上。希望我的代码能给你们带来帮助。 如果觉得对你们有帮助,可以给文章点个赞哦~ [图片]
2021-11-15 - 小程序开发笔记#2 封装一个页面栈工具类:框架与模块的导入导出(二)
创建文件 1)在miniprogram目录内直接新建一个文件夹,命名为utils(工具类) (文件夹的图标会自动带绿色的加号~) [图片] 2)在utils文件夹中新建JavaScript文件,命名为navigateUtils.js [图片] 初始化类 [代码]//navigateUtils.js //使用NavigateUtils对象封装我们的工具类函数 var NavigateUtils = {} /** * 进入页面(页面栈的Push操作) * @param {String} page 页面地址 * @param {Object} data 要传递的数据 */ NavigateUtils.push = function(page, data, force) {} /** * 返回页面(页面栈的Pop操作) * @param {Number} delta 返回层数 */ NavigateUtils.pop = function(delta) {} /** * 改变当前页面(先Pop再Push) * @param {String} page 页面地址 * @param {Object} data 要传递的数据 */ NavigateUtils.redirect = function(page, data, force) {} /** * 强制跳转页面(先Clear再Push) * @param {String} page 页面地址 * @param {Object} data 要传递的数据 */ NavigateUtils.reLaunch = function(page, data, force) {} /** * 切换页面(同switchTab) * @param {String} page 页面地址 * @param {Object} data 要传递的数据 */ NavigateUtils.switchTab = function(page, data, force) {} export default NavigateUtils [代码] 类的调用方法:export & import的使用 export导出模块 export语法声明用于导出函数、对象、指定文件(或模块)的原始值。 1)命名式导出 命名式导出可以同时导出多个模块 [代码]export {name1,name2,L,nameN}; [代码] 可以导出函数或者常量 [代码]export {fun}; export const foo = Math.sqrt(2); [代码] 可以指定导出成员,可以认为导出成员为类中公有成员,未导出的成员为类中私有成员 [代码]//以本文为例 export {NavigateUtils} //等价: export {NavigateUtils:NavigateUtils} [代码] 2)默认导出 默认导出只能导出单一值,可以是一个函数、类或其它类型的值。由于我们写的工具类只是一个封装的函数集,均为公有成员,故采用更加便捷的默认导出方式 [代码]export default NavigateUtils; //等价: export {NavigateUtils as default}; [代码] import导入模块 1)命名式导入 [代码]import {foo,fun} from "my-module" //{}中为导入模块中对应导出值的名字 //from后跟导入模块的文件名(路径) [代码] 具有重命名的写法 [代码]import {NavigateUtils as Nav} from '../../utils/navigateUtils'; [代码] 2)默认导入 同样的,在导入时可以使用import指令导出这些默认值。 [代码]import NavigateUtils from '../../utils/navigateUtils'; [代码] [代码]//可以直接进行重命名 import NavU from '../../utils/navigateUtils' [代码] 更多有关import和export的具体用法可以看一下这篇!非常清晰 参考:https://www.cnblogs.com/libin-1/p/7127481.html 关于数据的传输 实现更方便的数据传输是我们写这个工具类的主要目的。初步目标为,直接通过传入page(目标页面路径)data(对象)作为方法参数实现页面间切换同时数据传输。 makeParams 说明:将传入的data对象中所有属性拆分为一个字符串res,并契合url要求的数据传输格式 [代码]NavigateUtils.makeParams = (data) => { var res = ""; for (var key in data) res += key + "=" + data[key] + "&"; return res; } [代码] makePageUrl 说明:将页面路径page与makeParams获得的字符串拼接 [代码]NavigateUtils.makePageUrl = (page, data) => { //若data为空,则表明本次页面切换不需要传递数据 return data ? page + "?" + NavigateUtils.makeParams(data) : page; } [代码] 由此,我们得到了我们需要的url,下一节挑一个函数写实现吧~ 还是小白,如果有错误或更好的想法,欢迎各位大佬一同交流、指出哇~
2021-11-13 - 小程序开发笔记#1 封装一个页面栈工具类:思路分析(一)
为啥想要写一个页面栈工具类? 关于官方API 微信官方文档中提供了有关路由的5个API,对应实现不同的页面切换需求,分别为: wx.switchTab(Object object) wx.navigateTo(Object object) wx.navigateBack(Object object) wx.reLaunch(Object object) wx.redirectTo(Object object) 改进思路(主要还是打代码的习惯) 1)5个API可以看作页面栈的进出栈操作 tabBar之间的跳转 保留当前页面,跳转到应用内的某个页面(但是不能跳到 tabbar 页面)——push(进栈) 关闭当前页面,返回上一页面或多级页面——pop(出栈) 关闭所有页面,打开到应用内的某个页面——清空栈后push一个页面 关闭当前页面,跳转到应用内的某个页面(但是不能跳 tabbar 页面)——先pop后push一个页面 不妨尝试封装一个页面栈工具类,通过更简洁的进出栈操作实现页面切换 2)要实现页面间跳转时的数据传输,一般采用把data放入url中,如: [代码]wx:navigateTo({ url: '../pageTest/pageTest?id=123&name='bao'' }) [代码] 在目标页面pageTest的onload方法中可以这样获取数据: [代码]onLoad: function (res) { this.setData({ id:res.id, name:res.name }) console.log(this.data.id); console.log(this.data.name) }, [代码] 可以看到,对于大量数据的传输时,url会非常长,处理起来比较不方便,我们可以尝试封装一些方法实现从要传输数据对象到url的转换 NavigateUtilAPI push(page页面地址,data需要传递的数据) pop(delta返回层数) switch(page页面地址,data需要传递的数据):进行tabBar间的切换 change(page页面地址,data需要传递的数据):改变当前页面 goto(page页面地址,data需要传递的数据):强制跳转 (……更加细化的接口,如将data嵌入url,当前页面获取等 后记 下一节开始写代码实现!希望能对刚开始学小程序的朋友有所帮助! 还是小白,如果有错误或更好的想法,欢迎各位大佬一同交流、指出哇~
2021-11-13 - 表单组件使用小练习——制作一套微信风格的动态表单
表单组件使用小练习笔记——制作一套微信风格的动态表单 写在前面 使用git仓库的quick start进行快速体验一番: WeuiDynamicForm (gitee.com) 一、 效果 一次网络维修的工单提交流程是这样的: 用户选择服务->小程序获取该服务所指定的表单设置数据->小程序展示表单-> 用户按照格式进行填写->小程序根据表单设置进行数据校验->小程序提交表单 [图片] 二、需求 通过json或js数据对表单进行渲染 表单组件可以输入和输出数据 三、思路 [图片] 传入一个组件数据对象list,通过[代码]wx:for[代码]和[代码]wx:if[代码]去判断“组件类型、组件设置”; 表单组件能够正常将值传递到表单待校验的数据中; 伪代码: [图片] 四、开干 1、通过表单设置对表单进行渲染 (一) 新建项目,通过NPM的方式引入同微信原生视觉体验一致的UI组件库weui (1)通过useExtendedLib方式引入weui组件库 通过npm或useExtendedLib方式引入组件库 | 微信开放社区 (qq.com) 顺便看看官方的组件展示,可以归纳为以下这些 选择列表:选项会以列表的方式展示 [图片] 输入框包含标题:一种样式; [图片] 输入框包含标题及按钮; [图片] 日期输入框; [图片] 图片选择框 [图片] (2)先放下一个大表单 [图片] (3) 再搭建一个单一表单组件的样子 ,通过[代码]wx:for[代码]进行遍历, [图片] (二)将单个组件的设置传入对应的表单组件中 [图片] 其实就是把每个元素的设置都传进去就好了,输入框同理 (三)将部分特殊的组件独立出一个组件 可以不分开,但考虑到代码的整洁,决定将一些复杂的组件独立出来 这里以图片选择组件为例 (1)图片选择器 创建一个新的组件,组件中引入weui的uploader,然后将官方文档的[代码]wxml[代码] [代码]js[代码]代码粘贴进来Uploader | 微信开放文档 (qq.com) [图片] [代码]{ "component": true, "usingComponents": { "mp-uploader": "weui-miniprogram/uploader/uploader" } } [代码] 在大表单中引入自定义组件,包括两步,在json中配置[代码]usingComponents[代码],然后放入组件的标签,传入单个表单组件的数据 [图片] 再在自定义组件中,将部分官方放的静态数据替换成动态的 [图片] 从官方文档中,我们可以看出这个图片上传组件的工作流程是这样的: [图片] 那我们的工作就简单了,我们只需要实现[代码]uploadFile[代码]里面的上传函数即可,最后在[代码]uploadSuccess[代码]方法里面将事件传递出去 组件间通讯,仅传递事件 如图,我们需要在[代码]my-uploader.js[代码]里的[代码]uploadSuccess[代码]里面使用[代码]this.triggerEvent[代码]将事件传递上去,注意,这只是说明操作逻辑的图,不代表实际传递过程。 [图片] 操作过程 在最外层的[代码]<my-uploader/>[代码]里绑定事件 在[代码]my-uploader.js[代码]的图片上传成功回调里面使用[代码]this.triggerEvent()[代码]触发事件 [图片] [图片] (2)图形验证码组件 [图片] 我们需要实现验证码的生成,将组件输入框及验证码加密后的数据传递到表单页中 图像生成:使用云函数配合[代码]captchapng[代码]库生成base64编码的验证码图片字符串并返回; [图片] 验证码校验:将用户输入结果使用配置数据库的盐值进行解密 [代码]npm install --save captchapng[代码] [代码]var captchapng = require('captchapng') [代码] [代码] var vcodeInt = parseInt(Math.random() * 9000 + 1000) var vcodeRaw = new captchapng(80, 30, vcodeInt) // width,height,numeric captcha vcodeRaw.color(0, 0, 0, 0) // First color: background (red, green, blue, alpha) vcodeRaw.color(80, 80, 80, 255) // Second color: paint (red, green, blue, alpha) var vcodeRaw64 = vcodeRaw.getBase64() var vcodeBase64 = new Buffer(vcodeRaw64, 'base64') //选择性进行MD5加盐 //设置加密字符串 var salt = '' //在原来的字符串的基础上加上一些特殊文本 var vcodeEncrypt = md5(salt + vcodeInt) return { openid: OPENID, vcodeImg: vcodeBase64.toString('base64', 0), vcodeEncrypt: vcodeEncrypt } [代码] 拿到图片的base64字符串后,在<image/>里进行回显 [图片] 当然,图片的点击会再次召唤新的验证码 2、表单数据校验 Form | 微信开放文档 (qq.com) 从文档得知,表单数据校验需要设置“规则”和“需校验数据”,重点:了解“规则”和“数据”是如何联系的 (一)传入规则及数据 规则设置及传入 将包含一整个表单规则的数组丢进去即可 [图片] 需校验的数据 将整个表单的数据传入即可 [图片] (二)将“规则”和“数据”联系起来 (1)阅读官方的规则,发现是这样连起来的 规则名称对应models对象的一个结点名称 [代码] <mp-form id="form" rules="{{formConfig.rules}}" models="{{formData}}"> [代码] [代码]{ name: 'ticketClass', rules: {required: true, message: '分类多少选一个吧'}, } [代码] 如上述规则,他的名字是[代码]ticketClass[代码],那如果需要对他进行校验,那他在大的动态表单[代码]form[代码]里面需要是[代码]formData.ticketClass[代码] [图片] (2)这样的目标就很明确了,我们需要对[代码]formData[代码]进行赋值,名称是这个字段的名称,值是输入值 普通表单组件 官方demo里面,每个表单组件都有一个属性[代码]data-feild[代码] [图片] 在每次对组件进行事件监听时,在返回的事件对象中,我们可以在 [代码]event.currentTarget.dataset[代码]中找到数据名称 [代码]event.detail.value[代码]中找到数据值 [图片] 然后,我们将“名称”和“值”放到[代码]formData[代码]上,使用[代码]this.setData({})[代码]方法进行赋值,其中,值的名称是动态的,采用一个小点,不是单引号!(因为在markdown的缘故,无法在这单独打这样一个点出来,自行体会一下,常规的87 104键键盘会在左上角数字1的左边,[代码]点${能返回值的函数或者值}点[代码]是ES6新增的字符串方法) [代码] const { field } = event.currentTarget.dataset this.setData({ [`formData.${field}`]: event.detail.value }) [代码] [图片] 五、小提问 可否通过设置表单id的方式,一次性生成多张表单呢? 如何使用<slot/>的方式优化渲染性能呢?
2021-11-15 - 能不能实现公众号、群聊的批量管理?
上了大学之后,通讯录里的群聊和关注的公众号都到了两百多个,一个一个删除或者点消息免打扰特别麻烦,请问能否上线批量管理功能(如批量删除、增加群聊标签、批量设置消息免打扰等) 既然可以批量管理通讯录了,那这些也该有了吧
2021-11-15 - 小程序示例
小程序官方示例Demo,包含云开发示例。
2020-06-17 - 十年老站长心声:我为什么选择把 Hexo 网站迁移到 Webify
前 言我的这个博客站点大约始建于2010年以前,使用过 dlog、pjblog、zblog 等博客程序,也手动建立使用过纯静态 HTML 页面,大约2010年开始使用 WordPress。最近几年有感于 WordPress 过于臃肿复杂,而我的目的只是想写点什么,给自己留下些记录,于是将网站迁移到了更加简洁的hexo。 在本地电脑搭建好环境,配置好 hexo 站点。每次在网易云笔记里写好 markdown 格式的文章,然后复制到本地电脑 hexo 的对应目录下,手动生成新的网站静态页面。最后再手动上传到服务器空间里,这样就完成了网站的更新。 Webify官网:https://webify.cloudbase.net/ 我的需求Hexo虽然简洁、快速,但有几个问题一直困扰着我,甚至影响了写作的兴趣和欲望。 第一,一定要使用我安装配置了 hexo 的那台电脑,才能发布生成新的静态页面,然后才能更新到服务器。虽然也了解过一些办法,比如将 hexo 配置文件复制安装到不同电脑,或者上传到 github,通过 github 更新,但终究不能很好解决网站更新,文章发布的问题。 第二个问题,其实是第一个问题的延伸。每次发布新文章,文章写好以后,还需要复制到 hexo 目录下,手动生成静态页面,然后上传到网站服务器,这个过程太过繁琐。当然也去了解过解决办法,比如使用 hexo-admin 插件、给 hexo 添加后台管理,把 hexo 部署到服务器上,通过 admin-hexo 在线更新文章。然而这些都背离了我当初使用 hexo 的初心---简洁,快速。如果要这样折腾,为何不使用 WordPress 等功能更强大的博客程序呢? 在折腾的过程中,在 hexo 官网上看见了一篇技术文章,介绍如何将 hexo 部署到GitHub Pages,非常符合我的需求,唯一的问题是,部署过程中需要借助 Travis CI 这个第三方平台,简单的事情又变得复杂了。最后我甚至动了想把 hexo 转换成 jekyll 念头,因为 hexo 和 jekyll 结构基本一样,完全支持.md的文章格式,更重要的是 GitHub Pages 原生的支持 jekyll,几乎不需要任何设置。Github 虽然在国内很多地方访问不稳定,但是套上 cdn 以后可以解决这个问题。 我开始着手将 hexo 迁移到 GitHub Pages 上,使用 jekyll 部署。但是不久发现了一个大问题,GitHub Pages 不支持 jekyll 插件。没错,简单的借助 jekyll 插件实现的比如翻页,搜索等功能,在 GitHub Pages 几乎实现不了,这样就很绝望了。于是一切再次停下来。 喜提Webify随后便看到了云开发 Webify 这款产品,支持将任意形式的博客,个人站点部署到 Webify。于是去了解了下,发现正是我所需要的,正好解决了我上述的需求。 Webify 部署 hexo 非常简单,在 Webify 控制面板里绑定 GitHub 里的自己的 hexo 网站仓库就可以了,因为 Webify 已经预设了 hexo 框架,所有配置都是默认和自动的。 [图片] 部署好以后,网站就可以访问了,还可以绑定自己的域名,默认会开启 CDN 加速。 在自己的 GitHub 仓库里修改网站,或者拟写 markdown 格式的日志,webify 会自动构建和部署,生成新的静态页面并发布。这意味着以后只需要在 GitHub 里写文章(这篇博文即是如此),网站会完全自动更新,完美,这正是我一直以来所需求的。 下图是我的 hexo 网站在 GitHub 里的仓库文件列表。 [图片] 按照官方文档介绍,webify在构建时,平台会首先为您的应用安装 package.json 中指定的项目依赖(包括开发环境依赖 devDependencies)。这意味着我在本地创建 hexo 时所安装和依赖的各种环境和插件,webify完全会自动帮我安装。最后webify生成的hexo站点和我本地运行的完全一样。 Webify的其他特点1、Webify默认支持cdn,所以网站构建部署成功以后,可能需要2-5分钟时间才能看到效果(官方团队会继续优化)。 2、Webify目前支持的框架有: React/Vue/Angular/Next.js/Nuxt.js/Hexo/Gatsby.js/Vite/Docusaurus 2等。 [图片] 3、Webify 基本支持了国内主流的代码托管平台 Github/Gitlab/Gitee 和腾讯云 Coding。 现在,用 webify 部署应用还能领取300元无门槛代金券,足够个人网站用两年了,感兴趣的可以去官网看看。 Webify官网:https://webify.cloudbase.net/ [图片] 作者:YU JIANG 原文链接:https://zhuanlan.zhihu.com/p/410642192 [图片]
2021-11-11 - 关于ES6新增的方法总结,包含了一些个人笔记的重点强调
ES6 创建数组: Array.prototype.from(伪数组,函数)//伪数组转换成真数组 Array(length) 设置初始值: Array.of(value) Array.fill(value,start,end)//数组的填充start-end-1都用value填充,可以用于数组值的更改 数组值限制 Array.filter(function)//满足条件的所有值并返回,数组形式。return true时将该数加入新数组,false就过滤掉 Array.map(function)//对数组中的值进行普遍操作,比如全部乘2,就return n*2 查找值 Array.find(function)//满足条件的第一个值就返回,数值形式 Array.findIndex(function)//下标 数组删减增加 Array.splice(index,length,[value])//1.开始下标 2.要删除多少长度 3.替换成后面的值,删除为空 可以改变原数组 数组求和 Array.reduce(function,initValue)//对数组进行合并缩减,最终缩减为一个值 [图片] 数组合并 Array.concat(secondArray) 字符串翻转 string.split("").reverse().join("") class类 原型链 ES5中 类的静态方法(只加到原型上,即Animal.eat=function) 实例对象的方法(要Animal.prototype.eat=function)要加到object的原型对象上,所有的实例才继承 ES6中 加static就是类的静态方法 不加static就是实例对象的方法 类的继承: ES5不管了过于复杂,直接上ES6 Dog继承于Animal class Dog extends Animal {} [图片] 子类中如果没有新构造,例如上图中的this.age=2,那么子类中不需要写constructor(已默认继承) 函数 arguments为传入的参数伪数组 函数形参可以设置默认值:f(x,y=1,z) 如果要用函数默认值,则调用console.log(f(1,undefined,2))//undefined即可,而不能0或空字符串 function sum (…nums)//调用时可以是 sum(1,2,3) data=[1,2,3] function sum(x,y,z)//调用时 sum(…data) 即数组遍历,一边参数散开,一边是数组形式的收 箭头函数 ()=>{} 箭头函数无this,箭头函数内部的this指针为外层 Set 声明时内为可遍历对象, let s=new Set([1,2,3]) s.add()//添加元素 s.has()//是否有某数据 s.size() s.forEach(item => {console.log(item)}) Map let map=new Map([[1,2],[3,4],[“zjc”,“shuaibi”]])//创建map对象同时初始化值 map.set(1,2)//新增 map.delete(1)//删除的是键为1的键值对 map.has()//找的是键值 map.get()//找的是某个键对应的值 map.forEach((value,key)=>{console.log(value,key)})中,是先值再键 对象拷贝 Object.assign(new,old)//直接覆盖,即深拷贝,直接将新的引用到旧的数据上 object和Map存储的都是键值对组合。但是: object的键的类型是 字符串; map的键的类型是 可以是任意类型; 另外注意,object获取键使用Object.keys(返回数组); Map获取键值使用 map变量.keys() (返回迭代器)。 正则表达式 const s=‘aaa_aa_a’ const r1=/a+/g const r2=/a+/y r1.exec(s)//对于对象r1调用该方法,目的是在s中找r1出现的首个下标 /g 全局匹配,从第一位开始 /y 从上一次匹配的位置开始 即第一次执行r2.exec(s)返回0,第二次执行r2.exec(s)返回4 字符串拼接 ``的妙用 es5中用+拼接 es6中用 ``表示字符串,里面的${}内部表示字面量 [图片] ${‘retail’}为传入参数 函数中的s1=string[0]代表传入参数位置的前面的字符串,type为传入的参数, 检查完type==='retail’后,返回传入参数处前面的字符串和函数内部计算后的字符串,即如下 [图片] 字符串换行: [图片] 解构赋值 let arr=[1,2,3] let [zjc,aaa]=arr//以一对应赋值 对于对象(键值对),可以用Object.entries(object)来遍历所有键值对 因此可以用for-of来迭代 for(let [k,v] of Object.entries(user)) {console.log(k,v)} [图片] 如何达到异步机制 Promise对象,then,resolve,reject(这俩为了快速创建promise对象),catch,all,race js是单线程,同步的 先执行完某个作用域中的所有语句,再执行这些语句中引发的另外的事情,下例中先1再2后3 Callback 回调函数 [图片] Promise 返回一个状态(改变同步) 处于pending挂起状态 当第一个文件正确执行后运行onload事件,即执行resolve函数 当第一个文件执行失败运行onerror事件,即执行reject函数 来改变挂起状态 [图片] then 传入两个参数,且返回的是Promise实例 (.then是promise原型的对象和方法) promise.then(onFulfilled,onRejected)//onFullfilled对应上图resolve 必选参数;第二个为可选 如果onFulfilled为非函数,则.then返回空的Promise对象 下例中,第一个return的意思是箭头函数要有一个返回值,将return后面的loadScript得到的promise对象返回给第一个.then,让他能继续作为一个promise继续执行下一个.then [图片] 用Promise对象提供的两个静态方法resolve && reject 实现异步操作 resolve和reject可以快速生成promise实例,而不用new Promise [图片] [图片] Catch 捕获错误 Promise对象的方法,代替reject [图片] 上面的分一个一个接着执行的,为串行 All 对于并行的异步操作 [图片] Race 竞争,异步中的先到先得 Promise.race(p1(),p2()) [图片] Rflect反射机制,未来可能代替object Reflect不用new,直接用 apply 在非反射中(es5中无反射),应用apply就得先指定方法再通过apply改变作用域 反射中可以先apply,再根据执行中的条件去指定要调用哪个方法 用法:Reflect.apply(function,作用域,以数组形式传入的参数) Reflect.apply(Math.floor,null,[4.72])//apply中的null指的是作用域,没指定作用域就默认全局 construct 用于实例化一个类,跟new关键字用法一样 let d=Reflect.construct(Date,[])跟 let d=new Date()一样 construct方法中,第一个参数为什么类,第二个参数为初始化填入的东西,都必填,可以是空数组 修改某对象的原型对象 Reflect.setPrototypeof(old,new)//例如将一个数组改为字符串 例如:Reflect.setPrototypeof(arr,String.prototype) 新增一个寻找某个对象其原型对象的方法 Reflect.getPrototypeof() 验证某个对象上是否有某个属性或方法 Reflect.has(object,属性或方法) 返回某对象的所有键或某数组所有数据的索引值 Reflect.ownKeys(object) Proxy 代理 let d=new Proxy(object,function)//object要代理谁,哪个对象;第二个参数为代理后干什么事,读写 有点像中介[图片] 代理对象禁止赋值(通过set返回false) [图片] 用处:某对象想只能自己修改而用户不能修改,那么将代理对象设置成只读后给用户即可 对代理的写进行限制且 对象结构不被破坏(has限制) [图片] 监控错误 最顶端加,且要捕获而不是冒泡 [图片] 对于错误的处理,可以return false,也可以throw new TypeError(’’) 对新类添加代理,让某个属性只读 [图片] 撤销代理 [图片] 对代理对象进行Proxy.revocable声明,将代理数据和撤销代理方法存到对象d中 接着可以通过d.proxy.price读取数据 d.revoke() 撤销代理 Generator 控制遍历的停止 要点: 控制的函数声明时加星号 function * name(){} 当运行函数时遇到 yield 就会停止,调用.next()让函数继续运行,传递yield后面的结果回去 通过l.next()控制函数执行 [图片] 加星号函数返回给l两个结果,一个是value,遍历的值,另一个是done,是否结束 yield后面加星号,代表后面为可遍历对象 .next(xxx)//next方法可以传参,将参数传给函数体内部yield的返回值,上图中是给val,value空时才能传 .return()//return方法可以控制函数的提前结束 函数外部抛出异常到函数,如何在函数内部捕获该异常? 内部try{}catch{},外部.throw(new Error(’’)) [图片] 可迭代对象 如何将拥有复杂的数据结构的不可迭代对象变为可迭代对象呢?即该对象能用for-of进行遍历 给对象挂载一个Symbol.iterator方法,该方法输入为this,输出为一个对象 该方法的返回值写法为return{next(){return{done:true,value:1}}} 返回值要有next方法,该next方法有两个字段,返回值为done和value, done代表遍历结束没,默认为false没结束,value为遍历开始的值 [图片] [图片] 以上方法为通过控制done是否结束来输出value 以下为通过yield来按节奏来输出 [图片] 可迭代协议:(for of循环) 允许对象定义他的可迭代行为,比如在for of结构中,哪些值可以遍历到。在js中的某些类型是内置好的可迭代对象,比如:字符串、数组、类型数组、Map对象、Get对象等。而Object类型不可迭代。这些内置可迭代对象可以进行迭代的原因是内部实现了@@iterator 方法,即在该对象或该对象的原型链上有Symbol.iterator属性实现了@@iterator 方法。 简单来说就是将不可迭代对象变为可迭代对象,上例手写该协议,用Generator实现,yield控制输出 迭代器协议:(Iterator对象) 定义了产生一系列值的标准方式。一个对象必须实现next()方法,才能成为迭代器。 next() 方法必须返回一个对象,该对象应当有两个属性: done 和 value done(boolean)如果迭代器可以产生序列中的下一个值,则为 false。 value 迭代器返回的任何 JavaScript 值 模块 导出 export {const one=‘name’} 导入 import {name} from ‘xxx文件,可以省略.js拓展名’ 对于默认导出和对导入的模块进行改名 export default 变量名//只能有一个默认导出 在导入时,对于默认导入变量不写在{}里,例如import name2,{name} from ‘src’ 如果想对模块改名,默认变量直接改,其他得加as 例如,原文件默认导出为name,导入时 import name2,{addr as addr2} from ‘src’ 如果导出的是对象,只能导出一个对象, 可以import对象后,进行结构化赋值取出该对象里涵盖的多个对象 默认导出类时,可以不声明类名 如果想一次性导入多个模块,可以用*关键字,例如 import * as Mod from ‘src’ 注意:默认导出的模块需要用Mod.default()//默认去查找 觉得该篇文章有用的请不要忘记忘记点击右下角的大拇指~ 欢迎大家关注我的公众号:Smooth前端成长记录 公众号同步更新CSDN博客内容,想方便阅读博客的C友可以来关注我的公众号以便获得更优良的阅读体验~ [图片]
2021-11-15 - 思考:该开发什么小程序好呢?
我打开微信上“我的小程序”和“最近使用的小程序”发现,常用的很多小程序很大一部分是各种奶茶店、餐厅预约或者点餐的小程序。如今,几乎每一家零售商都会有自己的小程序。 对于我这样的“社恐”来说,当着收银员或服务员的面看菜谱属实有点尴尬。对商家来说,这样效率低下,例如站在奶茶店收银台前看好久才选择自己想点的饮品,而后面还有人在排队点单。并且菜品有无库存往往要在点单之后才能被告知,又要重新点造成更多麻烦。于我而言,其他因素相同时,我更愿意去有扫码点餐功能的店家吃饭。 如果商家通过上线一些平台提供点餐或者外卖功能,免不了每单会被抽成。依附于平台的餐厅,享受着平台带来的额外利润,但也要忍受平台的抽成,并且没有属于自己的忠实客户,如果通过小程序的会员系统将属于客户转化成忠实客户,进而保持一个长久的发展关系,对商家而言会更加有利可图,也很符合微信的一句口号:“就是再小的个体也有自己的品牌。”,还有微信之父张小龙所说的:“我们希望能够消除中介”“我们希望我们的系统是真正的去中心化的”。 开发者很多时候就是跟着需求走的,因此,我选择着手开发一个有支付功能,可供商家使用的小平台~ 开发途中会遇到很多困难,但幸好这里有活跃的社区和易懂的文档,也请大家多多指教~ It starts right now. Bury your doubts under the ground. Right now you’re taking your turn with the projects.
2021-11-14 - 搜索不到小程序、公众号
是否是七天内注册或者改名的? 如果是的话,可以再等几天 排名比较低,没有滑到底? 由于小程序、公众号的简介、昵称、关键词等没有设置好导致搜索权重比较低。 是否受到官方的私信?违规被限制搜索? 打开小程序、公众号的后台查看私信,看看近期是否因为违规被限制搜索,如果存在的话,按照官方的指示进行修改。
2021-11-14 - JavaScript 内存详解 & 分析指南
前言 JavaScript 诞生于 1995 年,最初被设计用于网页内的表单验证。 这些年来 JavaScript 成长飞速,生态圈日益壮大,成为了最受程序员欢迎的开发语言之一。并且现在的 JavaScript 不再局限于网页端,已经扩展到了桌面端、移动端以及服务端。 随着大前端时代的到来,使用 JavaScript 的开发者越来越多,但是许多开发者都只停留在“会用”这个层面,而对于这门语言并没有更多的了解。 如果想要成为一名更好的 JavaScript 开发者,理解内存是一个不可忽略的关键点。 📖 本文主要包含两大部分: JavaScript 内存详解 JavaScript 内存分析指南 看完这篇文章后,相信你会对 JavaScript 的内存有比较全面的了解,并且能够拥有独自进行内存分析的能力。 🧐 话不多说,我们开始吧! 文章篇幅较长,除去代码也有 12000 字左右,需要一定的时间来阅读,但是我保证你所花费的时间都是值得的。 正文 内存(memory) 什么是内存(What is memory) 相信大家都对内存有一定的了解,我就不从盘古开天辟地开始讲了,稍微提一下。 首先,任何应用程序想要运行都离不开内存。 另外,我们提到的内存在不同的层面上有着不同的含义。 💻 硬件层面(Hardware) 在硬件层面上,内存指的是随机存取存储器。 内存是计算机重要组成部分,用来储存应用运行所需要的各种数据,CPU 能够直接与内存交换数据,保证应用能够流畅运行。 一般来说,在计算机的组成中主要有两种随机存取存储器:高速缓存(Cache)和主存储器(Main memory)。 高速缓存通常直接集成在 CPU 内部,离我们比较远,所以更多时候我们提到的(硬件)内存都是主存储器。 💡 随机存取存储器(Random Access Memory,RAM) 随机存取存储器分为静态随机存取存储器(Static Random Access Memory,SRAM)和动态随机存取存储器(Dynamic Random Access Memory,DRAM)两大类。 在速度上 SRAM 要远快于 DRAM,而 SRAM 的速度仅次于 CPU 内部的寄存器。 在现代计算机中,高速缓存使用的是 SRAM,而主存储器使用的是 DRAM。 💡 主存储器(Main memory,主存) 虽然高速缓存的速度很快,但是其存储容量很小,小到几 KB 最大也才几十 MB,根本不足以储存应用运行的数据。 我们需要一种存储容量与速度适中的存储部件,让我们在保证性能的情况下,能够同时运行几十甚至上百个应用,这也就是主存的作用。 计算机中的主存其实就是我们平时说的内存条(硬件)。 硬件内存不是我们今天的主题,所以就说这么多,想要深入了解的话可以根据上面提到关键词进行搜索。 🧩 软件层面(Software) 在软件层面上,内存通常指的是操作系统从主存中划分(抽象)出来的内存空间。 此时内存又可以分为两类:栈内存和堆内存。 接下来我将围绕 JavaScript 这门语言来对内存进行讲解。 在后面的文章中所提到的内存均指软件层面上的内存。 栈与堆(Stack & Heap) 栈内存(Stack memory) 💡 栈(Stack) 栈是一种常见的数据结构,栈只允许在结构的一端操作数据,所有数据都遵循后进先出(Last-In First-Out,LIFO)的原则。 现实生活中最贴切的的例子就是羽毛球桶,通常我们只通过球桶的一侧来进行存取,最先放进去的羽毛球只能最后被取出,而最后放进去的则会最先被取出。 栈内存之所以叫做栈内存,是因为栈内存使用了栈的结构。 栈内存是一段连续的内存空间,得益于栈结构的简单直接,栈内存的访问和操作速度都非常快。 栈内存的容量较小,主要用于存放函数调用信息和变量等数据,大量的内存分配操作会导致栈溢出(Stack overflow)。 栈内存的数据储存基本都是临时性的,数据会在使用完之后立即被回收(如函数内创建的局部变量在函数返回后就会被回收)。 简单来说:栈内存适合存放生命周期短、占用空间小且固定的数据。 [图片] 💡 栈内存的大小 栈内存由操作系统直接管理,所以栈内存的大小也由操作系统决定。 通常来说,每一条线程(Thread)都会有独立的栈内存空间,Windows 给每条线程分配的栈内存默认大小为 1MB。 堆内存(Heap memory) 💡 堆(Heap) 堆也是一种常见的数据结构,但是不在本文讨论范围内,就不多说了。 堆内存虽然名字里有个“堆”字,但是它和数据结构中的堆没半毛钱关系,就只是撞了名罢了。 堆内存是一大片内存空间,堆内存的分配是动态且不连续的,程序可以按需申请堆内存空间,但是访问速度要比栈内存慢不少。 堆内存里的数据可以长时间存在,无用的数据需要程序主动去回收,如果大量无用数据占用内存就会造成内存泄露(Memory leak)。 简单来说:堆内存适合存放生命周期长,占用空间较大或占用空间不固定的数据。 [图片] 💡 堆内存的上限 在 Node.js 中,堆内存默认上限在 64 位系统中约为 1.4 GB,在 32 位系统中约为 0.7 GB。 而在 Chrome 浏览器中,每个标签页的内存上限约为 4 GB(64 位系统)和 1 GB(32 位系统)。 💡 进程、线程与堆内存 通常来说,一个进程(Process)只会有一个堆内存,同一进程下的多个线程会共享同一个堆内存。 在 Chrome 浏览器中,一般情况下每个标签页都有单独的进程,不过在某些情况下也会出现多个标签页共享一个进程的情况。 函数调用(Function calling) 明白了栈内存与堆内存是什么后,现在让我们看看当一个函数被调用时,栈内存和堆内存会发生什么变化。 当函数被调用时,会将函数推入栈内存中,生成一个栈帧(Stack frame),栈帧可以理解为由函数的返回地址、参数和局部变量组成的一个块;当函数调用另一个函数时,又会将另一个函数也推入栈内存中,周而复始;直到最后一个函数返回,便从栈顶开始将栈内存中的元素逐个弹出,直到栈内存中不再有元素时则此次调用结束。 [图片] 上图中的内容经过了简化,剥离了栈帧和各种指针的概念,主要展示函数调用以及内存分配的大概过程。 在同一线程下(JavaScript 是单线程的),所有被执行的函数以及函数的参数和局部变量都会被推入到同一个栈内存中,这也就是大量递归会导致栈溢出(Stack overflow)的原因。 关于图中涉及到的函数内部变量内存分配的详情请接着往下看。 储存变量(Store variables) 当 JavaScript 程序运行时,在非全局作用域中产生的局部变量均储存在栈内存中。 但是,只有原始类型的变量是真正地把值储存在栈内存中。 而引用类型的变量只在栈内存中储存一个引用(reference),这个引用指向堆内存里的真正的值。 💡 原始类型(Primitive type) 原始类型又称基本类型,包括 [代码]string[代码]、[代码]number[代码]、[代码]bigint[代码]、[代码]boolean[代码]、[代码]undefined[代码]、[代码]null[代码] 和 [代码]symbol[代码](ES6 新增)。 原始类型的值被称为原始值(Primitive value)。 补充:虽然 [代码]typeof null[代码] 返回的是 [代码]'object'[代码],但是 [代码]null[代码] 真的不是对象,会出现这样的结果其实是 JavaScript 的一个 Bug~ 💡 引用类型(Reference type) 除了原始类型外,其余类型都属于引用类型,包括 [代码]Object[代码]、[代码]Array[代码]、[代码]Function[代码]、[代码]Date[代码]、[代码]RegExp[代码]、[代码]String[代码]、[代码]Number[代码]、[代码]Boolean[代码] 等等… 实际上 [代码]Object[代码] 是最基本的引用类型,其他引用类型均继承自 [代码]Object[代码]。也就是说,所有引用类型的值实际上都是对象。 引用类型的值被称为引用值(Reference value)。 🎃 简单来说 在多数情况下,原始类型的数据储存在栈内存,而引用类型的数据(对象)则储存在堆内存。 [图片] 特别注意(Attention) 全局变量以及被闭包引用的变量(即使是原始类型)均储存在堆内存中。 🌐 全局变量(Global variables) 在全局作用域下创建的所有变量都会成为全局对象(如 [代码]window[代码] 对象)的属性,也就是全局变量。 而全局对象储存在堆内存中,所以全局变量必然也会储存在堆内存中。 不要问我为什么全局对象储存在堆内存中,一会我翻脸了啊! 📦 闭包(Closures) 在函数(局部作用域)内创建的变量均为局部变量。 当一个局部变量被当前函数之外的其他函数所引用(也就是发生了逃逸),此时这个局部变量就不能随着当前函数的返回而被回收,那么这个变量就必须储存在堆内存中。 而这里的“其他函数”就是我们说的闭包,就如下面这个例子: [代码]function getCounter() { let count = 0; function counter() { return ++count; } return counter; } // closure 是一个闭包函数 // 变量 count 发生了逃逸 let closure = getCounter(); closure(); // 1 closure(); // 2 closure(); // 3 [代码] 闭包是一个非常重要且常用的概念,许多编程语言里都有闭包这个概念。这里就不详细介绍了,贴一篇阮一峰大佬的文章。 学习 JavaScript 闭包:http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html 💡 逃逸分析(Escape Analysis) 实际上,JavaScript 引擎会通过逃逸分析来决定变量是要储存在栈内存还是堆内存中。 简单来说,逃逸分析是一种用来分析变量的作用域的机制。 不可变与可变(Immutable and Mutable) 栈内存中会储存两种变量数据:原始值和对象引用。 不仅类型不同,它们在栈内存中的具体表现也不太一样。 原始值(Primitive values) 🚫 Primitive values are immutable! 前面有说到:原始类型的数据(原始值)直接储存在栈内存中。 ⑴ 当我们定义一个原始类型变量的时候,JavaScript 会在栈内存中激活一块内存来储存变量的值(原始值)。 ⑵ 当我们更改原始类型变量的值时,实际上会再激活一块新的内存来储存新的值,并将变量指向新的内存空间,而不是改变原来那块内存里的值。 ⑶ 当我们将一个原始类型变量赋值给另一个新的变量(也就是复制变量)时,也是会再激活一块新的内存,并将源变量内存里的值复制一份到新的内存里。 [图片] 🤠 总之就是:栈内存中的原始值一旦确定就不能被更改(不可变的)。 原始值的比较(Comparison) 当我们比较原始类型的变量时,会直接比较栈内存中的值,只要值相等那么它们就相等。 [代码]let a = '123'; let b = '123'; let c = '110'; let d = 123; console.log(a === b); // true console.log(a === c); // false console.log(a === d); // false [代码] 对象引用(Object references) 🧩 Object references are mutable! 前面也有说到:引用类型的变量在栈内存中储存的只是一个指向堆内存的引用。 ⑴ 当我们定义一个引用类型的变量时,JavaScript 会先在堆内存中找到一块合适的地方来储存对象,并激活一块栈内存来储存对象的引用(堆内存地址),最后将变量指向这块栈内存。 💡 所以当我们通过变量访问对象时,实际的访问过程应该是: 变量 -> 栈内存中的引用 -> 堆内存中的值 ⑵ 当我们把引用类型变量赋值给另一个变量时,会将源变量指向的栈内存中的对象引用复制到新变量的栈内存中,所以实际上只是复制了个对象引用,并没有在堆内存中生成一份新的对象。 ⑶ 而当我们给引用类型变量分配为一个新的对象时,则会直接修改变量指向的栈内存中的引用,新的引用指向堆内存中新的对象。 [图片] 🤠 总之就是:栈内存中的对象引用是可以被更改的(可变的)。 对象的比较(Comparison) 所有引用类型的值实际上都是对象。 当我们比较引用类型的变量时,实际上是在比较栈内存中的引用,只有引用相同时变量才相等。 即使是看起来完全一样的两个引用类型变量,只要他们的引用的不是同一个值,那么他们就是不一样。 [代码]// 两个变量指向的是两个不同的引用 // 虽然这两个对象看起来完全一样 // 但它们确确实实是不同的对象实例 let a = { name: 'pp' } let b = { name: 'pp' } console.log(a === b); // false // 直接赋值的方式复制的是对象的引用 let c = a; console.log(a === c); // true [代码] 对象的深拷贝(Deep copy) 当我们搞明白引用类型变量在内存中的表现时,就能清楚地理解为什么浅拷贝对象是不可靠的。 在浅拷贝中,简单的赋值只会复制对象的引用,实际上新变量和源变量引用的都是同一个对象,修改时也是修改的同一个对象,这显然不是我们想要的。 想要真正的复制一个对象,就必须新建一个对象,将源对象的属性复制过去;如果遇到引用类型的属性,那就再新建一个对象,继续复制… 此时我们就需要借助递归来实现多层次对象的复制,这也就是我们说的深拷贝。 对于任何引用类型的变量,都应该使用深拷贝来复制,除非你很确定你的目的就是复制一个引用。 内存生命周期(Memory life cycle) 通常来说,所有应用程序的内存生命周期都是基本一致的: 分配 -> 使用 -> 释放 当我们使用高级语言编写程序时,往往不会涉及到内存的分配与释放操作,因为分配与释放均已经在底层语言中实现了。 对于 JavaScript 程序来说,内存的分配与释放是由 JavaScript 引擎自动完成的(目前的 JavaScript 引擎基本都是使用 C++ 或 C 编写的)。 但是这不意味着我们就不需要在乎内存管理,了解内存的更多细节可以帮助我们写出性能更好,稳定性更高的代码。 垃圾回收(Garbage collection) 垃圾回收即我们常说的 GC(Garbage collection),也就是清除内存中不再需要的数据,释放内存空间。 由于栈内存由操作系统直接管理,所以当我们提到 GC 时指的都是堆内存的垃圾回收。 基本上现在的浏览器的 JavaScript 引擎(如 V8 和 SpiderMonkey)都实现了垃圾回收机制,引擎中的垃圾回收器(Garbage collector)会定期进行垃圾回收。 📢 紧急补课 在我们继续之前,必须先了解“可达性”和“内存泄露”这两个概念: 💡 可达性(Reachability) 在 JavaScript 中,可达性指的是一个变量是否能够直接或间接通过全局对象访问到,如果可以那么该变量就是可达的(Reachable),否则就是不可达的(Unreachable)。 [图片] 上图中的节点 9 和节点 10 均无法通过节点 1(根节点)直接或间接访问,所以它们都是不可达的,可以被安全地回收。 💡 内存泄漏(Memory leak) 内存泄露指的是程序运行时由于某种原因未能释放那些不再使用的内存,造成内存空间的浪费。 轻微的内存泄漏或许不太会对程序造成什么影响,但是一旦泄露变严重,就会开始影响程序的性能,甚至导致程序的崩溃。 垃圾回收算法(Algorithms) 垃圾回收的基本思路很简单:确定哪个变量不会再使用,然后释放它占用的内存。 实际上,在回收过程中想要确定一个变量是否还有用并不简单。 直到现在也还没有一个真正完美的垃圾回收算法,接下来介绍 3 种最广为人知的垃圾回收算法。 标记-清除(Mark-and-Sweep) 标记清除算法是目前最常用的垃圾收集算法之一。 从该算法的名字上就可以看出,算法的关键就是标记与清除。 标记指的是标记变量的状态的过程,标记变量的具体方法有很多种,但是基本理念是相似的。 对于标记算法我们不需要知道所有细节,只需明白标记的基本原理即可。 需要注意的是,这个算法的效率不算高,同时会引起内存碎片化的问题。 🌰 举个栗子 当一个变量进入执行上下文时,它就会被标记为“处于上下文中”;而当变量离开执行上下文时,则会被标记为“已离开上下文”。 💡 执行上下文(Execution context) 执行上下文是 JavaScript 中非常重要的概念,简单来说的是代码执行的环境。 如果你现在对于执行上下文还不是很了解,我强烈建议你抽空专门去学习下!!! 垃圾回收器将定期扫描内存中的所有变量,将处于上下文中以及被处于上下文中的变量引用的变量的标记去除,将其余变量标记为“待删除”。 随后,垃圾回收器会清除所有带有“待删除”标记的变量,并释放它们所占用的内存。 标记-整理(Mark-Compact) 准确来说,Compact 应译为紧凑、压缩,但是在这里我觉得用“整理”更为贴切。 标记整理算法也是常用的垃圾收集算法之一。 使用标记整理算法可以解决内存碎片化的问题(通过整理),提高内存空间的可用性。 但是,该算法的标记阶段比较耗时,可能会堵塞主线程,导致程序长时间处于无响应状态。 虽然算法的名字上只有标记和整理,但这个算法通常有 3 个阶段,即标记、整理与清除。 🌰 以 V8 的标记整理算法为例 ① 首先,在标记阶段,垃圾回收器会从全局对象(根)开始,一层一层往下查询,直到标记完所有活跃的对象,那么剩下的未被标记的对象就是不可达的了。 [图片] ② 然后是整理阶段(碎片整理),垃圾回收器会将活跃的(被标记了的)对象往内存空间的一端移动,这个过程可能会改变内存中的对象的内存地址。 ③ 最后来到清除阶段,垃圾回收器会将边界后面(也就是最后一个活跃的对象后面)的对象清除,并释放它们占用的内存空间。 [图片] 引用计数(Reference counting) 引用计数算法是基于“引用计数”实现的垃圾回收算法,这是最初级但已经被弃用的垃圾回收算法。 引用计数算法需要 JavaScript 引擎在程序运行时记录每个变量被引用的次数,随后根据引用的次数来判断变量是否能够被回收。 虽然垃圾回收已不再使用引用计数算法,但是引用计数技术仍非常有用! 🌰 举个栗子 注意:垃圾回收不是即使生效的!但是在下面的例子中我们将假设回收是立即生效的,这样会更好理解~ [代码]// 下面我将 name 属性为 ππ 的对象简称为 ππ // 而 name 属性为 pp 的对象则简称为 pp // ππ 的引用:1,pp 的引用:1 let a = { name: 'ππ', z: { name: 'pp' } } // b 和 a 都指向 ππ // ππ 的引用:2,pp 的引用:1 let b = a; // x 和 a.z 都指向 pp // ππ 的引用:2,pp 的引用:2 let x = a.z; // 现在只有 b 还指向 ππ // ππ 的引用:1,pp 的引用:2 a = null; // 现在 ππ 没有任何引用了,可以被回收了 // 在 ππ 被回收后,pp 的引用也会相应减少 // ππ 的引用:0,pp 的引用:1 b = null; // 现在 pp 也可以被回收了 // ππ 的引用:0,pp 的引用:0 x = null; // 哦豁,这下全完了! [代码] 🔄 循环引用(Circular references) 引用计数算法看似很美好,但是它有一个致命的缺点,就是无法处理循环引用的情况。 在下方的例子中,当 [代码]foo()[代码] 函数执行完毕之后,对象 [代码]a[代码] 与 [代码]b[代码] 都已经离开了作用域,理论上它们都应该能够被回收才对。 但是由于它们互相引用了对方,所以垃圾回收器就认为他们都还在被引用着,导致它们哥俩永远都不会被回收,这就造成了内存泄露。 [代码]function foo() { let a = { o: null }; let b = { o: null }; a.o = b; b.o = a; } foo(); // 即使 foo 函数已经执行完毕 // 对象 a 和 b 均已离开函数作用域 // 但是 a 和 b 还在互相引用 // 那么它们这辈子都不会被回收了 // Oops!内存泄露了! [代码] V8 中的垃圾回收(GC in V8) 8️⃣ V8 V8 是一个由 Google 开源的用 C++ 编写的高性能 JavaScript 引擎。 V8 是目前最流行的 JavaScript 引擎之一,我们熟知的 Chrome 浏览器和 Node.js 等软件都在使用 V8。 在 V8 的内存管理机制中,把堆内存(Heap memory)划分成了多个区域。 [图片] 这里我们只关注这两个区域: New Space(新空间):又称 Young generation(新世代),用于储存新生成的对象,由 Minor GC 进行管理。 Old Space(旧空间):又称 Old generation(旧世代),用于储存那些在两次 GC 后仍然存活的对象,由 Major GC 进行管理。 也就是说,只要 New Space 里的对象熬过了两次 GC,就会被转移到 Old Space,变成老油条。 🧹 双管齐下 V8 内部实现了两个垃圾回收器: Minor GC(副 GC):它还有个名字叫做 Scavenger(清道夫),具体使用的是 Cheney’s Algorithm(Cheney 算法)。 Major GC(主 GC):使用的是文章前面提到的 Mark-Compact Algorithm(标记-整理算法)。 储存在 New Space 里的新生对象大多都只是临时使用的,而且 New Space 的容量比较小,为了保持内存的可用率,Minor GC 会频繁地运行。 而 Old Space 里的对象存活时间都比较长,所以 Major GC 没那么勤快,这一定程度地降低了频繁 GC 带来的性能损耗。 💥 加点魔法 我们在上方的“标记整理算法”中有提到这个算法的标记过程非常耗时,所以很容易导致应用长时间无响应。 为了提升用户体验,V8 还实现了一个名为增量标记(Incremental marking)的特性。 增量标记的要点就是把标记工作分成多个小段,夹杂在主线程(Main thread)的 JavaScript 逻辑中,这样就不会长时间阻塞主线程了。 [图片] 当然增量标记也有代价的,在增量标记过程中所有对象的变化都需要通知垃圾回收器,好让垃圾回收器能够正确地标记那些对象,这里的“通知”也是需要成本的。 另外 V8 中还有使用工作线程(Worker thread)实现的平行标记(Parallel marking)和并行标记(Concurrent marking),这里我就不再细说了~ 🤓 总结一下 为了提升性能和用户体验,V8 内部做了非常非常多的“骚操作”,本文提到的都只是冰山一角,但足以让我五体投地佩服连连! 总之就是非常 Amazing 啊~ 内存管理(Memory management) 或者说是:内存优化(Memory optimization)? 虽然我们写代码的时候一般不会直接接触内存管理,但是有一些注意事项可以让我们避免引起内存问题,甚至提升代码的性能。 全局变量(Global variable) 全局变量的访问速度远不及局部变量,应尽量避免定义非必要的全局变量。 在我们实际的项目开发中,难免会需要去定义一些全局变量,但是我们必须谨慎使用全局变量。 因为全局变量永远都是可达的,所以全局变量永远不会被回收。 🌐 还记得“可达性”这个概念吗? 因为全局变量直接挂载在全局对象上,也就是说全局变量永远都可以通过全局对象直接访问。 所以全局变量永远都是可达的,而可达的变量永远都不会被回收。 🤨 应该怎么做? 当一个全局变量不再需要用到时,记得解除其引用(置空),好让垃圾回收器可以释放这部分内存。 [代码]// 全局变量不会被回收 window.me = { name: '吴彦祖', speak: function() { console.log(`我是${this.name}`); } }; window.me.speak(); // 解除引用后才可以被回收 window.me = null; [代码] 隐藏类(HiddenClass) 实际上的隐藏类远比本文所提到的复杂,但是今天的主角不是它,所以我们点到为止。 在 V8 内部有一个叫做“隐藏类”的机制,主要用于提升对象(Object)的性能。 V8 里的每一个 JS 对象(JS Objects)都会关联一个隐藏类,隐藏类里面储存了对象的形状(特征)和属性名称到属性的映射等信息。 隐藏类内记录了每个属性的内存偏移(Memory offset),后续访问属性的时候就可以快速定位到对应属性的内存位置,从而提升对象属性的访问速度。 在我们创建对象时,拥有完全相同的特征(相同属性且相同顺序)的对象可以共享同一个隐藏类。 🤯 再想象一下 我们可以把隐藏类想象成工业生产中使用的模具,有了模具之后,产品的生产效率得到了很大的提升。 但是如果我们更改了产品的形状,那么原来的模具就不能用了,又需要制作新的模具才行。 🌰 举个栗子 在 Chrome 浏览器 Devtools 的 Console 面板中执行以下代码: [代码]// 对象 A let objectA = { id: 'A', name: '吴彦祖' }; // 对象 B let objectB = { id: 'B', name: '彭于晏' }; // 对象 C let objectC = { id: 'C', name: '刘德华', gender: '男' }; // 对象 A 和 B 拥有完全相同的特征 // 所以它们可以使用同一个隐藏类 // good! [代码] 随后在 Memory 面板打一个堆快照,通过堆快照中的 Comparison 视图可以快速找到上面创建的 3 个对象: 注:关于如何查看内存中的对象将会在文章的第二大部分中进行讲解,现在让我们专注于隐藏类。 [图片] 在上图中可以很清楚地看到对象 A 和 B 确实使用了同一个隐藏类。 而对象 C 因为多了一个 [代码]gender[代码] 属性,所以不能和前面两个对象共享隐藏类。 🧀 动态增删对象属性 一般情况下,当我们动态修改对象的特征(增删属性)时,V8 会为该对象分配一个能用的隐藏类或者创建一个新的隐藏类(新的分支)。 例如动态地给对象增加一个新的属性: 注:这种操作被称为“先创建再补充(ready-fire-aim)”。 [代码]// 增加 gender 属性 objectB.gender = '男'; // 对象 B 的特征发生了变化 // 多了一个原本没有的 gender 属性 // 导致对象 B 不能再与 A 共享隐藏类 // bad! [代码] 动态删除([代码]delete[代码])对象的属性也会导致同样的结果: [代码]// 删除 name 属性 delete objectB.name; // A:我们不一样! // bad! [代码] 不过,添加数组索引属性(Array-indexed properties)并不会有影响: 其实就是用整数作为属性名,此时 V8 会另外处理。 [代码]// 增加 1 属性 objectB[1] = '数字组引属性'; // 不影响共享隐藏类 // so far so good! [代码] 🙄 那问题来了 说了这么多,隐藏类看起来确实可以提升性能,那它和内存又有什么关系呢? 实际上,隐藏类也需要占用内存空间,这其实就是一种用空间换时间的机制。 如果由于动态增删对象属性而创建了大量隐藏类和分支,结果就是会浪费不少内存空间。 🌰 举个栗子 创建 1000 个拥有相同属性的对象,内存中只会多出 1 个隐藏类。 而创建 1000 个属性信息完全不同的对象,内存中就会多出 1000 个隐藏类。 🤔 应该怎么做? 所以,我们要尽量避免动态增删对象属性操作,应该在构造函数内就一次性声明所有需要用到的属性。 如果确实不再需要某个属性,我们可以将属性的值设为 [代码]null[代码],如下: [代码]// 将 age 属性置空 objectB.age = null; // still good! [代码] 另外,相同名称的属性尽量按照相同的顺序来声明,可以尽可能地让更多对象共享相同的隐藏类。 即使遇到不能共享隐藏类的情况,也至少可以减少隐藏类分支的产生。 其实动态增删对象属性所引起的性能问题更为关键,但因本文篇幅有限,就不再展开了。 闭包(Closure) 前面有提到:被闭包引用的变量储存在堆内存中。 这里我们再重点关注一下闭包中的内存问题,还是前面的例子: [代码]function getCounter() { let count = 0; function counter() { return ++count; } return counter; } // closure 是一个闭包函数 let closure = getCounter(); closure(); // 1 closure(); // 2 closure(); // 3 [代码] 现在只要我们一直持有变量(函数) [代码]closure[代码],那么变量 [代码]count[代码] 就不会被释放。 或许你还没有发现风险所在,不如让我们试想变量 [代码]count[代码] 不是一个数字,而是一个巨大的数组,一但这样的闭包多了,那对于内存来说就是灾难。 [代码]// 我将这个作品称为:闭包炸弹 function closureBomb() { const handsomeBoys = []; setInterval(() => { for (let i = 0; i < 100; i++) { handsomeBoys.push( { name: '陈皮皮', rank: 0 }, { name: ' 你 ', rank: 1 }, { name: '吴彦祖', rank: 2 }, { name: '彭于晏', rank: 3 }, { name: '刘德华', rank: 4 }, { name: '郭富城', rank: 5 } ); } }, 100); } closureBomb(); // 即将毁灭世界 // 💣 🌍 💥 💨 [代码] 🤔 应该怎么做? 所以,我们必须避免滥用闭包,并且谨慎使用闭包! 当不再需要时记得解除闭包函数的引用,让闭包函数以及引用的变量能够被回收。 [代码]closure = null; // 变量 count 终于得救了 [代码] 如何分析内存(Analyze) 说了这么多,那我们应该如何查看并分析程序运行时的内存情况呢? “工欲善其事,必先利其器。” 对于 Web 前端项目来说,分析内存的最佳工具非 Memory 莫属! 这里的 Memory 指的是 DevTools 中的一个工具,为了避免混淆,下面我会用“Memory 面板”或”内存面板“代称。 🔧 DevTools(开发者工具) DevTools 是浏览器里内置的一套用于 Web 开发和调试的工具。 使用 Chromuim 内核的浏览器都带有 DevTools,个人推荐使用 Chrome 或者 Edge(新)。 Memory in Devtools(内存面板) 在我们切换到 Memory 面板后,会看到以下界面(注意标注): [图片] 在这个面板中,我们可以通过 3 种方式来记录内存情况: Heap snapshot:堆快照 Allocation instrumentation on timeline:内存分配时间轴 Allocation sampling:内存分配采样 小贴士:点击面板左上角的 Collect garbage 按钮(垃圾桶图标)可以主动触发垃圾回收。 🤓 在正式开始分析内存之前,让我们先学习几个重要的概念: 💡 Shallow Size(浅层大小) 浅层大小指的是当前对象自身占用的内存大小。 浅层大小不包含自身引用的对象。 💡 Retained Size(保留大小) 保留大小指的是当前对象被 GC 回收后总共能够释放的内存大小。 换句话说,也就是当前对象自身大小加上对象直接或间接引用的其他对象的大小总和。 需要注意的是,保留大小不包含那些除了被当前对象引用之外还被全局对象直接或间接引用的对象。 Heap snapshot(堆快照) [图片] 堆快照可以记录页面当前时刻的 JS 对象以及 DOM 节点的内存分配情况。 🚀 如何开始 点击页面底部的 Take snapshot 按钮或者左上角的 ⚫ 按钮即可打一个堆快照,片刻之后就会自动展示结果。 [图片] 在堆快照结果页面中,我们可以使用 4 种不同的视图来观察内存情况: Summary:摘要视图 Comparison:比较视图 Containment:包含视图 Statistics:统计视图 默认显示 Summary 视图。 Summary(摘要视图) 摘要视图根据 Constructor(构造函数)来将对象进行分组,我们可以在 Class filter(类过滤器)中输入构造函数名称来快速筛选对象。 [图片] 页面中的几个关键词: Constructor:构造函数。 Distance:(根)距离,对象与 GC 根之间的最短距离。 Shallow Size:浅层大小,单位:Bytes(字节)。 Retained Size:保留大小,单位:Bytes(字节)。 Retainers:持有者,也就是直接引用目标对象的变量。 📌 Retainers(持有者) Retainers 栏在旧版的 Devtools 里叫做 Object’s retaining tree(对象保留树)。 Retainers 下的对象也展开为树形结构,方便我们进行引用溯源。 在视图中的构造函数列表中,有一些用“()”包裹的条目: (compiled code):已编译的代码。 (closure):闭包函数。 (array, string, number, symbol, regexp):对应类型([代码]Array[代码]、[代码]String[代码]、[代码]Number[代码]、[代码]Symbol[代码]、[代码]RegExp[代码])的数据。 (concatenated string):使用 [代码]concat()[代码] 函数拼接而成的字符串。 (sliced string):使用 [代码]slice()[代码]、[代码]substring()[代码] 等函数进行边缘切割的字符串。 (system):系统(引擎)产生的对象,如 V8 创建的 HiddenClasses(隐藏类)和 DescriptorArrays(描述符数组)等数据。 💡 DescriptorArrays(描述符数组) 描述符数组主要包含对象的属性名信息,是隐藏类的重要组成部分。 不过描述符数组内不会包含整数索引属性。 而其余没有用“()”包裹的则为全局属性和 GC 根。 另外,每个对象后面都会有一串“@”开头的数字,这是对象在内存中的唯一 ID。 小贴士:按下快捷键 Ctrl/Command + F 展示搜索栏,输入名称或 ID 即可快速查找目标对象。 💪 实践一下:实例化一个对象 ① 切换到 Console 面板,执行以下代码来实例化一个对象: [代码]function TestClass() { this.number = 123; this.string = 'abc'; this.boolean = true; this.symbol = Symbol('test'); this.undefined = undefined; this.null = null; this.object = { name: 'pp' }; this.array = [1, 2, 3]; this.getSet = { _value: 0, get value() { return this._value; }, set value(v) { this._value = v; } }; } let testObject = new TestClass(); [代码] [图片] ② 回到 Memory 面板,打一个堆快照,在 Class filter 中输入“TestClass”: 可以看到内存中有一个 [代码]TestClass[代码] 的实例,该实例的浅层大小为 80 字节,保留大小为 876 字节。 [图片] 🤔 注意到了吗? 堆快照中的 [代码]TestClass[代码] 实例的属性中少了一个名为 [代码]number[代码] 属性,这是因为堆快照不会捕捉数字属性。 💪 实践一下:创建一个字符串 ① 切换到 Console 面板,执行以下代码来创建一个字符串: [代码]// 这是一个全局变量 let testString = '我是吴彦祖'; [代码] ② 回到 Memory 面板,打一个堆快照,打开搜索栏(Ctrl/Command + F)并输入“我是吴彦祖”: [图片] Comparison(比较视图) 只有同时存在 2 个或以上的堆快照时才会出现 Comparison 选项。 比较视图用于展示两个堆快照之间的差异。 使用比较视图可以让我们快速得知在执行某个操作后的内存变化情况(如新增或减少对象)。 通过多个快照的对比还可以让我们快速判断并定位内存泄漏。 文章前面提到隐藏类的时候,就是使用了比较视图来快速查找新创建的对象。 💪 实践一下 ① 新建一个无痕(匿名)标签页并切换到 Memory 面板,打一个堆快照 Snapshot 1。 💡 为什么是无痕标签页? 普通标签页会受到浏览器扩展或者其他脚本影响,内存占用不稳定。 使用无痕窗口的标签页可以保证页面的内存相对纯净且稳定,有利于我们进行对比。 另外,建议打开窗口一段之间之后再开始测试,这样内存会比较稳定(控制变量)。 ② 切换到 Console 面板,执行以下代码来实例化一个 [代码]Foo[代码] 对象: [代码]function Foo() { this.name = 'pp'; this.age = 18; } let foo = new Foo(); [代码] ③ 回到 Memory 面板,再打一个堆快照 Snapshot 2,切换到 Comparison 视图,选择 Snapshot 1 作为 Base snapshot(基本快照),在 Class filter 中输入“Foo”: 可以看到内存中新增了一个 [代码]Foo[代码] 对象实例,分配了 52 字节内存空间,该实例的引用持有者为变量 [代码]foo[代码]。 [图片] ④ 再次切换到 Console 面板,执行以下代码来解除变量 [代码]foo[代码] 的引用: [代码]// 解除对象的引用 foo = null; [代码] ⑤ 再回到 Memory 面板,打一个堆快照 Snapshot 3,选择 Snapshot 2 作为 Base snapshot,在 Class filter 中输入“Foo”: 内存中的 [代码]Foo[代码] 对象实例已经被删除,释放了 52 字节的内存空间。 [图片] Containment(包含视图) 包含视图就是程序对象结构的“鸟瞰图(Bird’s eye view)”,允许我们通过全局对象出发,一层一层往下探索,从而了解内存的详细情况。 [图片] 包含视图中有以下几种全局对象: GC roots(GC 根) GC roots 就是 JavaScript 虚拟机的垃圾回收中实际使用的根节点。 GC 根可以由 Built-in object maps(内置对象映射)、Symbol tables(符号表)、VM thread stacks(VM 线程堆栈)、Compilation caches(编译缓存)、Handle scopes(句柄作用域)和 Global handles(全局句柄)等组成。 DOMWindow objects(DOMWindow 对象) DOMWindow objects 指的是由宿主环境(浏览器)提供的顶级对象,也就是 JavaScript 代码中的全局对象 [代码]window[代码],每个标签页都有自己的 [代码]window[代码] 对象(即使是同一窗口)。 Native objects(原生对象) Native objects 指的是那些基于 ECMAScript 标准实现的内置对象,包括 [代码]Object[代码]、[代码]Function[代码]、[代码]Array[代码]、[代码]String[代码]、[代码]Boolean[代码]、[代码]Number[代码]、[代码]Date[代码]、[代码]RegExp[代码]、[代码]Math[代码] 等对象。 💪 实践一下 ① 切换到 Console 面板,执行以下代码来创建一个构造函数 [代码]$ABC[代码]: 构造函数命名前面加个 $ 是因为这样排序的时候可以排在前面,方便找。 [代码]function $ABC() { this.name = 'pp'; } [代码] ② 切换到 Memory 面板,打一个堆快照,切换为 Containment 视图: 在当前标签页的全局对象下就可以找到我们刚刚创建的构造函数 [代码]$ABC[代码]。 [图片] Statistics(统计视图) 统计视图可以很直观地展示内存整体分配情况。 [图片] 在该视图里的空心饼图中共有 6 种颜色,各含义分别为: 红色:Code(代码) 绿色:Strings(字符串) 蓝色:JS arrays(数组) 橙色:Typed arrays(类型化数组) 紫色:System objects(系统对象) 白色:空闲内存 Allocation instrumentation on timeline(分配时间轴) [图片] 在一段时间内持续地记录内存分配(约每 50 毫秒打一张堆快照),记录完成后可以选择查看任意时间段的内存分配详情。 另外还可以勾选同时记录分配堆栈(Allocation stacks),也就是记录调用堆栈,不过这会产生额外的性能消耗。 🚀 如何开始 点击页面底部的 Start 按钮或者左上角的 ⚫ 按钮即可开始记录,记录过程中点击左上角的 🔴 按钮来结束记录,片刻之后就会自动展示结果。 💪 操作一下 ① 打开 Memory 面板,开始记录分配时间轴。 ② 切换到 Console 面板,执行以下代码: 代码效果:每隔 1 秒钟创建 100 个对象,共创建 1000 个对象。 [代码]console.log('测试开始'); let objects = []; let handler = setInterval(() => { // 每秒创建 100 个对象 for (let i = 0; i < 100; i++) { const name = `n${objects.length}`; const value = `v${objects.length}`; objects.push({ [name]: value}); } console.log(`对象数量:${objects.length}`); // 达到 1000 个后停止 if (objects.length >= 1000) { clearInterval(handler); console.log('测试结束'); } }, 1000); [代码] 😈 又是一个细节 不知道你有没有发现,在上面的代码中,我干了一件坏事。 在 for 循环创建对象时,会根据对象数组当前长度生成一个唯一的属性名和属性值。 这样一来 V8 就无法对这些对象进行优化,方便我们进行测试。 另外,如果直接使用对象数组的长度作为属性名会有惊喜~ ③ 静静等待 10 秒钟,控制台会打印出“测试结束”。 ④ 切换回 Memory 面板,停止记录,片刻之后会自动进入结果页面。 [图片] 分配时间轴结果页有 4 种视图: Summary:摘要视图 Containment:包含视图 Allocation:分配视图 Statistics:统计视图 默认显示 Summary 视图。 Summary(摘要视图) 看起来和堆快照的摘要视图很相似,主要是页面上方多了一条横向的时间轴(Timeline)。 [图片] 🧭 时间轴 时间轴中主要的 3 种线: 细横线:内存分配大小刻度线 蓝色竖线:表示内存在对应时刻被分配,最后仍然活跃 灰色竖线:表示内存在对应时刻被分配,但最后被回收 时间轴的几个操作: 鼠标移动到时间轴内任意位置,点击左键或长按左键并拖动即可选择一段时间 鼠标拖动时间段框上方的方块可以对已选择的时间段进行调整 鼠标移到已选择的时间段框内部,滑动滚轮可以调整时间范围 鼠标移到已选择的时间段框两旁,滑动滚轮即可调整时间段 双击鼠标左键即可取消选择 [图片] 在时间轴中选择要查看的时间段,即可得到该段时间的内存分配详情。 [图片] Containment(包含视图) 分配时间轴的包含视图与堆快照的包含视图是一样的,这里就不再重复介绍了。 [图片] Allocation(分配视图) 对不起各位,这玩意儿我也不知道有啥用… 打开就直接报错,我:喵喵喵? [图片] 是不是因为没人用这玩意儿,所以没人发现有问题… Statistics(统计视图) 分配时间轴的统计视图与堆快照的统计视图也是一样的,不再赘述。 [图片] Allocation sampling(分配采样) [图片] Memory 面板上的简介:使用采样方法记录内存分配。这种分析方式的性能开销最小,可以用于长时间的记录。 好家伙,这个简介有够模糊,说了跟没说似的,很有精神! 我在官方文档里没有找到任何关于分配采样的介绍,Google 上也几乎没有与之有关的信息。所以以下内容仅为个人实践得出的结果,如有不对的地方欢迎各位指出! 简单来说,通过分配采样我们可以很直观地看到代码中的每个函数(API)所分配的内存大小。 由于是采样的方式,所以结果并非百分百准确,即使每次执行相同的操作也可能会有不同的结果,但是足以让我们了解内存分配的大体情况。 ✍ 如何开始 点击页面底部的 Start 按钮或者左上角的 ⚫ 按钮即可开始记录,记录过程中点击左上角的 🔴 按钮来结束记录,片刻之后就会自动展示结果。 💪 操作一下 ① 打开 Memory 面板,开始记录分配采样。 ② 切换到 Console 面板,执行以下代码: 代码看起来有点长,其实就是 4 个函数分别以不同的方式往数组里面添加对象。 [代码]// 普通单层调用 let array_a = []; function aoo1() { for (let i = 0; i < 10000; i++) { array_a.push({ a: 'pp' }); } } aoo1(); // 两层嵌套调用 let array_b = []; function boo1() { function boo2() { for (let i = 0; i < 20000; i++) { array_b.push({ b: 'pp' }); } } boo2(); } boo1(); // 三层嵌套调用 let array_c = []; function coo1() { function coo2() { function coo3() { for (let i = 0; i < 30000; i++) { array_c.push({ c: 'pp' }); } } coo3(); } coo2(); } coo1(); // 两层嵌套多个调用 let array_d = []; function doo1() { function doo2_1() { for (let i = 0; i < 20000; i++) { array_d.push({ d: 'pp' }); } } doo2_1(); function doo2_2() { for (let i = 0; i < 20000; i++) { array_d.push({ d: 'pp' }); } } doo2_2(); } doo1(); [代码] ③ 切换回 Memory 面板,停止记录,片刻之后会自动进入结果页面。 [图片] 分配采样结果页有 3 种视图可选: Chart:图表视图 Heavy (Bottom Up):扁平视图(调用层级自下而上) Tree (Top Down):树状视图(调用层级自上而下) 这个 Heavy 我真的不知道该怎么翻译,所以我就按照具体表现来命名了。 默认会显示 Chart 视图。 Chart(图表视图) Chart 视图以图形化的表格形式展现各个函数的内存分配详情,可以选择精确到内存分配的不同阶段(以内存分配的大小为轴)。 [图片] 鼠标左键点击、拖动和双击以操作内存分配阶段轴(和时间轴一样),选择要查看的阶段范围。 [图片] 将鼠标移动到函数方块上会显示函数的内存分配详情。 [图片] 鼠标左键点击函数方块可以跳转到相应代码。 [图片] Heavy(扁平视图) Heavy 视图将函数调用层级压平,函数将以独立的个体形式展现。另外也可以展开调用层级,不过是自下而上的结构,也就是一个反向的函数调用过程。 [图片] 视图中的两种 Size(大小): Self Size:自身大小,指的是在函数内部直接分配的内存空间大小。 Total Size:总大小,指的是函数总共分配的内存空间大小,也就是包括函数内部嵌套调用的其他函数所分配的大小。 Tree(树状视图) Tree 视图以树形结构展现函数调用层级。我们可以从代码执行的源头开始自上而下逐层展开,呈现一个完整的正向的函数调用过程。 [图片] 参考资料 《JavaScript 高级程序设计(第4版)》 Memory Management:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management Visualizing memory management in V8 Engine:https://deepu.tech/memory-management-in-v8/ Trash talk: the Orinoco garbage collector:https://v8.dev/blog/trash-talk Fast properties in V8:https://v8.dev/blog/fast-properties Concurrent marking in V8:https://v8.dev/blog/concurrent-marking Chrome DevTools:https://developers.google.com/web/tools/chrome-devtools 传送门 微信推文版本 个人博客:菜鸟小栈 开源主页:陈皮皮 Eazax-CCC 游戏开发脚手架 更多分享 《为什么选择使用 TypeScript ?》 《高斯模糊 Shader》 《一文看懂 YAML》 《Cocos Creator 性能优化:DrawCall》 《互联网运营术语扫盲》 《在 Cocos Creator 里画个炫酷的雷达图》 《用 Shader 写个完美的波浪》 《在 Cocos Creator 中优雅且高效地管理弹窗》 《Cocos Creator 源码解读:引擎启动与主循环》 公众号 菜鸟小栈 😺我是陈皮皮,一个还在不断学习的游戏开发者,一个热爱分享的 Cocos Star Writer。 🎨这是我的个人公众号,专注但不仅限于游戏开发和前端技术分享。 💖每一篇原创都非常用心,你的关注就是我原创的动力! Input and output. [图片]
2021-01-13 - 快进来看看简单易懂的分分钟提升小程序性能50%上下的性能优化
场景引入为什么需要用到高性能虚拟列表+节流+分页请求的优化?当有场景需求为需要将大量数据(10000条)呈现在一页上,我们不断下拉访问,页面中有大量的数据列表的时候,用户会不会有不好的体验?会不会出现滚动不流畅而卡顿的情况?会不会因卡顿而出现短暂的白屏现象(数据渲染不成功)? 通过微信开发者工具自带的调试器->Network页面,我们可以观察到当有长列表时如果不使用高性能虚拟列表+节流+分页请求的优化,会出现以下问题: FPS:每秒帧数,图表上的红色块表示长时间帧,很可能会出现卡顿。CPU:CPU消耗占用,实体图越多消耗越高。NET:网络请求效率低,一次性请求10000条的渲染效率远远低于分1000次,每次请求10条数据内存:滑动该列表时明显能看到内存消耗大。总结:如果需要将大量数据(10000条)呈现在一页上,可以通过高性能虚拟列表+节流+按需请求分页数据并追加显示。优化的具体实现可拆分为以下需求(将一个大问题拆分为一个个小问题并逐个去解决): 不把长列表数据一次性全部直接显示在页面上。截取长列表一部分数据用来填充屏幕容器区域。长列表数据不可视部分使用使用空白占位填充。监听滚动事件根据滚动位置动态改变可视列表。监听滚动事件根据滚动位置动态改变空白填充。分页从服务器请求数据,将一次性请求所有数据变为滚动到底部才再次向服务器发送获取数据的请求 更详细的实现方法请阅读我的下一篇文章:https://developers.weixin.qq.com/community/develop/article/doc/000a2618d34908e3610d5978856c13
2021-11-12 - 微信小程序性能优化入门指南(转载)
原文地址:https://segmentfault.com/a/1190000016901634 小程序从发布到现在也已经有将近两年的时间,越来越来多的公司开始重视小程序生态带来的流量,今年也由于小程序平台对外能力的越来越多的开放以及小程序平台的自身优化,越来越多的开发者也自主的投入到小程序的开发当中,现在,作为前端如果会写小程序,绝对是一个不折不扣的面试加分项。 相信不少人刚接触小程序时的感觉大都是小程序很简单,开发只要是会写html、css、js就可以了,但是当自己的第一个小程序开发完成上线时,却发现小程序体验非常糟糕,接下来就让我们一窥小程序优化之道。 加载流程要想给小程序做优化,对小程序的加载流程一定要有一定的了解,小程序是怎么加载的,让我们先来看一个图片: [图片] 这三个图片大家一定都不陌生,当你打开一个小程序的时候就会经历这三个过程: 资源准备,这个过程就是小程序在下载你的代码包的过程业务代码注入和渲染,这个过程就是小程序将的业务代码分别注入视图层和逻辑层,并在视图层做视图的渲染异步数据的请求,显示加载中的时候,其实就是在到达首页时,如果首页有异步数据请求,这个时候小程序就会执行异步数据请求上述就是对小程序的启动过程的一个简单概述,让我们再来看一个更加具体一点的图片,可能会更好理解小程序启动过程: [图片] 从这个图片可以看到,小程序在启动加载的时候,其实分为两部分,一部分是逻辑层的启动启动,另一部分是视图层的启动,逻辑层的启动就是加载小程序的js代码,视图层的启动webview对页面进行加载和渲染,那预加载又是什么时候执行的呢?其实在微信动的时候,小程序平台就开始静默执行与加载的过程,包括JS引擎初始化和WebView的初始化,然后会注入小程序自带的公共库,例如自带api、组件等,后面的小程序启动,就是上面说过的打开一个小程序具体的启动加载过程了,下载代码包,分别注入逻辑层和视图层,然后共同完成首屏渲染。 启动性能优化讲完小程序的启动过程,就可以开始介绍具体的性能优化方案了,让我们一起看看影响小程序性能的因素以及具体的解决办法 代码包大小代码包大小会直接影响小程序的启动速度,代码包越大,小程序的启动时间就越长,在小程序启动时,下载代码包和代码注入的时间和小程序代码包大小是成正比的,一般小程序的平均启动时间是2s左右,可以看看你的小程序有没有拖后腿,那么如何控制包大小呢? 资源控制开启开发工具”上传代码时自动压缩”,小程序开发工具有一个上传代码时自动压缩的功能,当开启时,会在你上传代码时为你做代码压缩,除了这个,我们也可以通过使用第三方打包工具做代码压缩,如webpack、grunt、grulp。及时清理无用代码和资源文件,无用的代码和资源也会占用一定的包大小。减少代码包中的资源文件,将资源存放在cdn上,小程序开发工具对资源文件的压缩比率非常低,资源有条件的可以尽量放在CDN上,因为小程序开发工具对资源文件的压缩比率非常低,只有10%左右,或者也可以用第三编译工具对资源文件自己进行压缩处理分包加载[图片] 分包加载是小程序提高加载启动性能的一个重要方法,如果有人还不了解,可以点开链接看官方介绍,那么如何做好分包加载呢? 将小程序中不常用的代码放在分包中,主包内只保留用户最常访问的页面,但是由于官方规定tab页面只能放在主包中,因为小程序启动时只会加载主包,使用时按需下载分包,不会在加载时一次将整个代码包下载,这样就能有效减少启动加载的时间。 但是分包加载也有它的局限性,用户首次打开分包页面时,需要先进行分包代码的加载和注入,会造成页面切换时产生一定的延时,因此在此基础上,官方又推出了分包预加载和独立分包。 分包预加载先来看一下之前分包加载时的流程是怎样的: [图片] 那么分包预加载是怎么干的呢?分包预下载:提前配置可能会跳到哪些分包,框架在进入页面后根据配置进行预下载,分包预加载会在你进入主包页面后,为你静默开启分包代码的下载和注入,这个过程是无感的,来看一下分包预加载的流程是怎样的: [图片] 分包预加载需要注意的是:同一个分包中的页面享有共同的预下载大小限额2M,限额会在工具中打包时校验,因此不能把所有的分包页面都配置到分包预加载的配置中,只配置主包页面会跳转的页面即可。 独立分包独立分包又是什么呢?由于从分包页面启动是,必须要依赖于主包的下载和注入,启动速度会受到主包大小的制约,因此这就有了独立分包,独立分包在启动分包页面时,可以独立启动而不需要依赖主包,这样就可以减少主包下载和注入的时间,通常情况下我们会将活动、广告一类的具有独立逻辑的功能代码标记为一个独立分包,在分包页面启动时,可以不依赖于主包启动,只下载分包代码进行注入。让我们来看一下独立分包的加载流程是怎样的: [图片] 首屏加载性能优化首屏加载的体验对小程序来说十分重要,那么如何提升首屏加载性能呢? 提前请求:异步数据数据请求不需要等待页面渲染完成利用缓存:利用storage API对异步请求数据进行缓存,二次启动时先利用缓存数据渲染页面,再进行后台更新避免白屏:先展示页面骨架和基础内容及时反馈:及时地对需要用户等待的交互操作给出反馈,避免用户以为小程序没有响应渲染性能优化要想提高渲染性能,就需要知道小程序如何做页面渲染的,让我们先来看一个页面渲染的流程图: [图片] js引擎和native都可以过js的计算或者data修改来对Webview发起绘制操作,但是对开发者来说最重要的就是js引擎和Webview之间的通信,这通信过程是一个跨进程通信,是非常耗时的一个过程,我们要提高渲染的性能,也就是减少这个跨进程通信的时间,那么怎么去减少跨进程通信的时间呢? 避免不当使用setData使用data在方法间共享数据,会增加setData传输的数据量,同时会增加页面重绘的概率data仅包括与页面相关的数据使用setData传输大量数据,通讯耗时与数据量正相关,页面更新延迟可能造成更新开销增加仅传输页面中发生变化的数据,使用setData的特殊key实现局部更新后台页面进行setData抢占前台页面的资源页面切入后台后的setData调用,延迟到页面重新展示的时候执行总结来说就是在data中只定义与页面渲染相关的数据,其他与页面渲染无关的数据都定义成普通变量,在做setData操作时,尽量只传输页面渲染需要的数据,当页面切换时,将后台执行的setData操作销毁,等到页面重新展示的时候再执行。 避免不当使用onPageScroll只在必要的时候监听pageScroll事件避免在onPageScroll中执行复杂逻辑避免在onPageSroll中频繁调用setData避免频繁查询节点信息(SelectQuery),部分场景使用节点布局相交状态监听(IntersectionObserver)替代由于onPageSroll事件监听在处理js引擎和webview之间的通信时也是一个跨进程通信,因此在使用onPageScroll事件时,要注意以上的几点内容,来进行相关的优化 使用自定义事件在需要频繁更新的场景下,自定组件的更新只在组件内部更新,不受页面其他部分内容复杂性影响,这样也可以在一定程度优化渲染性能 总结这篇文章简单的介绍了微信小程序性能优化的一些方法,还有很多我没有介绍到方法就需要大家自己去探索总结了。希望大家看完这篇文章能对小程序性能优化有一定的认识,如果有错误或不严谨的地方,欢迎批评指正,如果喜欢,欢迎点赞收藏。
2021-09-23