月盾的博客

让web项目不再502

月盾

前后端分离

近几年比较流行的web项目开发架构是前后端分离,前后端分离架构在系统稳定性方面非常有优势,其中一点优势主要体现在用户感知上,即使服务端发生错误也不会展现在视图层,一般情况下用户是可以继续浏览网页,不会很突兀的显示这样的信息: 502 bad gateway

502 bad gateway

在接口发生错误时虽然可能会获取不到一些数据,但是在用户体验上比直接显示502错误要好。

部署也相对安全和方便。

前后端分离架构虽好,但不是”银弹”,不是所有网站都能使用前后端分离架构来做。

服务端渲染

至于分不分离都有诸多的优缺点,可根据实际场景选择,本文要说的是不分离情况下文章开始所提到的问题——难看的502,500错误。

本文我将使用“服务端渲染”来代替不分离的架构。

由于服务端渲染的所有数据处理都在服务端进行,发生任何错误都可能引起500错误,这种错误会直接体现在视图层,用户看到这样的信息很不友好。 500 Internal Server Error

500 Internal Server Error

遇到这种错误,可以使用适当的措施来弥补,带来的效果却非常良好。开发web最多遇到的错误码有:404,500,502。

404错误很多网站都有默认页面,像这样的:

404 not found

但是500,502错误却鲜有默认页面,我们何不也搞一个呢?

500错误

首先说一下500错误的应对措施,500发生在服务端,比如空指针,数组溢出,超时等都会引起web服务500错误,一般的500错误在应用层面是可以捕捉到的,在response返回之前捕获异常跳转到一个”合适的页面”,这样就可以避免出现500 Internal Server Error信息。虽然这个“合适的页面”不是本应该跳转的页面,但起码还是个正常页面。

502错误

再说一下502错误,发生502错误一般是应用直接宕机了,任何页面都是访问不到的,这样是无法在应用层面监听和处理的,但也不是没办法,我们可以在NGINX上处理,当NGINX发生502时跳转到”合适的页面”。这个页面需要是一个静态页,可以是友好的提示信息,可以是网站首页,根据需求自己定制即可。

server{
    error_page  502  /502.html;
        location = /502.html {
                root   /usr/share/nginx/html;
        }
}

以上两种错误处理虽然不能应对所有网站,比如一些个性化网站,即使掩饰的再好,用户也知道出问题了,但是在有些网站却能起到良好的作用,比如有些web站点发布时会中断服务出现502,比起光秃秃的500和502错误,上面的处理是不是就好多了。

go mongo-driver动态条件

月盾

在go mongo中查询是使用的是bson.M类型的条件,但是直接使用时无法动态添加条件,只能初始化赋值,bson.M其实就是map类型,只能使用someMap[“someKey”]=“someValue” 的形式添加,这样的话只能是用if判断字段的值来决定是否添加map key/value,写起来比较繁琐。还有一种是利用结构体转换为bson.M来实现。

//构造一个查询结构体
search := User{
		ID: id,
		Name: name,
		Age: age,
	}
//构造一个条件变量
	condition := bson.M{}
	//将结构体转为字节数组,userInfo中的字段根据需要设置值,需要保证没有值时不会有默认值出现
	userbyte, err := bson.Marshal(search)
	if err != nil {
		return user, err
	}
	//将字节码转为bson.M类型
	bson.Unmarshal(userbyte, &condition)
	log.Println(condition)
	if err = this.mongo.Collection("user").FindOne(context.TODO(),
		condition).Decode(&user); err != nil {
		return user, err
	}

以上基本就实现了动态条件查询的效果,其中:

search := User{
		ID: id,
		Name: name,
		Age: age,
	}

search结构中的字段可能值为空,假设在前端并未传递age字段,那么最终condition=map[id:xxx,name:xxx],并不会出现age:0这个的字段,有效避免了零值情况。

puppeteer TypeError: text is not iterable

月盾
(node:9828) UnhandledPromiseRejectionWarning: TypeError: text is not iterable
    at Keyboard.type (D:\workspace\auto-ui\node_modules\puppeteer-core\lib\Input.js:160:24)
    at Keyboard.<anonymous> (D:\workspace\auto-ui\node_modules\puppeteer-core\lib\helper.js:112:23)
    at openBrowser (D:\workspace\auto-ui\test-email\index.js:106:25)
    at process._tickCallback (internal/process/next_tick.js:68:7)
(node:9828) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 3)
(node:9828) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

错误原因:input输入框的type=text,但是输入的可能是其他类型,比如输入数字就会报错。

你是拿锤子的前端开发吗?

月盾

前言

接上篇《说道说道前后端分离》今天再次对前端现状作一次分析(吐槽)。

再次引用一句《穷查理宝典》中的理论:

在手里拿着锤子的人看来,所有的东西都会是钉子。

因为有锤子的关系,遇到任何问题,都会先想如何用锤子解决。久而久之,陷入了一种思维定式。任何工具带来便利的同时,也带来了局限性。而这往往是用锤子的人很难看到的。

事出有因

这种现状在开发圈内决不少见,不仅限于前端。本文只说说前端的现状,原因是笔者最近在工作中遇到一个棘手的问题:性能优化。

最近接手了多个现有的前端项目,是公司比较核心的移动端官网,作为门户网站访问量和用户量都比较大,但是随着项目的迭代出现了性能问题,页面加载速度在WiFi网络下达到3s,3G网络15s以上,更差的网络40s+。加载的资源小则3M,大则6M。如果一切往好处想,假设所有用户使用最好的网络,用户和公司都不在乎流量费,两三秒的加载速度也还挺快的,每次打开页面费个3M流量也不是个事。但如果考虑这些问题的话就会发现这不是小问题。

对以上问题分析得出结论之一:资源过大,有兴趣的可以打开淘宝网看下首屏资源做下对比,可以看到资源不超过3M,时间不超过2s。

而我们一个移动端网站的资源居然能超过3M,究其原因:

  • 图片大
  • js大
  • css大

图片大是因为图片基本没任何大小控制,都是使用了最高标准原图。js和css大基本是属于架构问题,一个项目中包含的上百的页面每个页面600多k的js是绕不过去的(vendor.js,app.js等打包资源,不包含其他引入资源)。

看到vendor.js,app.js这两个名称很多人应该想到了,这是vue(react)框架开发的网站。 是的,就是用vue开发的移动端网站,使用vue开发网站本身也不是什么大问题,毕竟有实力的公司不需要SEO,直接竞价排名就行。而我要说的问题是,不是什么网站都可以用vue来开发,不信请继续往下看。

问题分析

我司的移动端网站作用并不仅仅是用来展示公司形象的,更重要的是用户转化的,就是让用户注册的。而且是要和很多第三方机构合作投放引流,经常需要分析页面UI的不同对转化率的影响,所以需要的页面不是几个,而是几十上百个,还在不停增加,每周都有三五个页面增加。 由于vue主要是以开发单页SPA应用为主的,在开发人员不考虑真实需求的情况下自然会使用流行的技术,最终把网站开发成一个单页应用。单页应用的特点就是单页,就是把不同的页面做成一个页面一次加载,加载完成后页面之间的切换就会很快,一般无需再加载资源,用户体验也会好很多,可以套用一句话:“一次等待,处处快速”。

这个特点在管理后台项目中很合适,但是在只需要展示一次的项目中也合适吗?不合适。

我们的网站项目是用来做很多落地页的,各个落地页之间没有关联性,不会A页面跳到B页面,从B页面跳到C页面,A页面中不需要B页面的资源,B页面也不需要C页面的资源。然而vue项目打包的时候会把每个页面独有的一些资源都融合在一起,形成公共资源。结果就显而易见了,一个页面总要加载一堆无关资源,不仅资源大,还有很长的白屏时间,用户体验下降。

还有一点不该使用单页应用的原因是我们的页面是纯展示的页面,不需要很多数据交互,vue能起到什么作用?操作数据?驱动UI?模块化?通通不需要。现代html可以不借助第三方库和框架的情况下完全能实现。

结论

JavaScript 的最大优势之一是它不需要编译,所以可以在浏览器中直接运行。这样你就可以立刻获得编码的反馈。入门门槛很低;你只需一个文本编辑器和一个浏览器就能编写软件了。 不幸的是,这种简单性和可访问性已被称为过度工具链的风气破坏了。这种风气已经将 JavaScript 的开发工作变成了一场噩梦。我甚至看过一整套关于配置 Webpack 的课程。这种乱象需要有个尽头——生命苦短啊。

VUE,React这类框架用于构建应用方面很合适,但不太适合构建网站。应用是需要有较多的UI和数据方面的交互,而网站则更多的是信息展示,你可能根本不需要JavaScript(框架)。

追求新技术可以让我们获得新奇感,成就感,解决老问题,而不是带来新问题。复杂性才是造成软件问题的根本原因。——试问:离开框架的你还会开发网站吗?

人人都值得学习的UI自动化

月盾

为什么需要UI自动化?

说起自动化,听着很厉害,可是也没见识过到底多厉害,基本是属于传说,没见过实战。但不能否认其价值,作者本人作为一个开发者也是偶然的机会接触到UI自动化,感受到了自动化的魅力,才不惜花时间来学习并使用在实际工作中。下面就来说一下为什么要做自动化。 自动化有很多种,单元测试,接口测试,UI测试。所有测试过程可以形成这样一个金字塔:

测试金字塔 (图片来自网络)

测试金字塔 (图片来自网络)

从图中看出底层测试简单快速,每个单元相对独立,测试成本也较低。而最顶层的UI层聚合了底层的很多接口服务,一个测试流程相对更长更复杂,也就导致了速度慢,成本高的问题。如果由人工来完成,一个完整的测试流程往往需要几分钟,而且是不停的重复这样一个流程。所以很多开发都不愿意完整的测试自己所开发的一套业务,只能由测试人员不厌其烦的循环往复。此时如果有自动化的话就非常nice了。口说无凭,一图胜千言:

自动注册

是不是还没看清?没错,平时人工测试一个报名表单需要15秒左右,而自动化后几乎一瞬间即可完成,速度提升不言而喻。如果您对UI自动化已产生兴趣,请继续往下看。

哪些场景可以UI自动化?

UI自动化的目的不是为了自动化而自动化,也并不能覆盖所有测试。还是以测试金字塔来说明:

测试金字塔

UI自动化虽然只能覆盖到约10%,但其价值却不可忽略,因为越往顶层消耗的人力成本越高,如果底层测试不够充分就只能靠顶层测试来保证。自动化虽好,但需要满足特定的场景才行,那么什么样的场景可以做UI自动化?

  • 流程变化少。侧重UI的修改,而不是流程的修改。
  • 频繁的回归测试。比如一个报名表单样式修改比较频繁,需要测试其报名是否可用。
  • 界面稳定。样式可以频繁修改,但表单顺序和个数变化较少不能频繁变更。
  • 维护周期长。如果只是使用一次那也不必自动化了,毕竟自动化是解决重复劳动。
  • 开发与测试可相互配合。自动化需要程序有一定规范便于测试人员写测试程序。
  • 测试人员具备编程能力。 如果满足了上述的场景,那么UI自动化在人力消耗和效率上就有很大的提升。正确性上更有保证,手工录入会有输错重输的情况,而自动化则不会出现。 再看一个流程比较长的页面:

自动下单

上面动图并没与快进,一气呵成完成了一次下单流程,基本不需要停留即可进入下一步,相比起手动操作省时省力。

为什么UI自动化普及率低?

既然UI自动化能提高效率,但为什么却很少有人去使用?

  • 开发自动化程序对测试人员的编程水平有一定要求,很少有人愿意花时间去写这个程序。
  • 对于互联网公司,大多数业务需要快速迭代,一个页面的生存周期很短,也可能只是一次性的,自动化测试没有存在的必要性。
  • 测试人员没有切实的感受到效率提升。

其实说白了,就是大家觉得投入和产出不值得。如果是迫于领导压力要写自动化程序,就会不停的对程序修修补补,自己用的话有问题大不了不用自动化,手动测试一下通过就行,如果是给别人用就会不停收到反馈和吐槽,自然也就没有写下去的动力了。 所以个人觉得,自动化程序不应该成为一种流程和形式,而是应该由开发和测试人员自发的去将自身经常重复的工作做成自动化。因为自动化本身就不能覆盖所有场景,只有实际参与的人才能知道哪些是可以自动化的。

有哪些工具可以选择?

目前市场上不仅提供了多种工具可以选择,还支持不同语言。 web端:selenium、webdriver、robotframework、puppeteer等。 APP端:Appium、Instrumentation、Robotium 、UIAutomator、Espresso、Calabash、Selendroid、Robolectric、RoboSpock、Cafe、Athrun等。

移动APP自动化测试框架

本文不对所有工具一一详解,可自行根据平台选择合适的工具和语言进行学习使用。只针对个别WEB端工具做简单说明。 拿selenium来说,selenium是一款很多人比较熟悉的工具,支持的语言有Python,Java,JavaScript等,推荐使用Python。而且其支持的浏览器也很全面: Google Chrome Internet Explorer 7, 8, 9, 10, 11 Firefox Safari Opera HtmlUnit phantomjs Android iOS

一些常见问题

在自动化过程中最多的就是对元素进行定位,自动化工具常见的定位符有:

  • id
  • name
  • class name
  • tag
  • link text
  • partial link text
  • xpath
  • css selector

以上这些元素定位对于以前的网页来说还足以应对,因为以前开发的网页大多数元素会有id,class这些属性,定位起来也比较方便。但是对于react,vue,angular这类数据驱动的框架就不那么友好了。 比如有这么一个元素:<div>{{element}}</div>结果为:

免费的mongodb集群

月盾

mongodb提供了免费的mongodb集群可用于学习使用 https://www.mongodb.com/cloud 并且有3个节点

mongodb Atlas免费集群存储空间为512M,这对于个人项目来说足够使用(本博客运行6年使用空间8M)。

本地客户端连接mongo Atlas

连接mongodb Atlas 连接mongodb Atlas 连接mongodb Atlas

复制了连接字符串后直接粘贴到客户端中,替换,点击From SRV,会自动拉取集群配置,点击Test测试连接是否成功。

连接mongodb Atlas 连接mongodb Atlas

golang操作mongodb

月盾

在之前mgo是一个使用广泛的mongodb驱动器,不过从2018年开始已不再维护,虽然觉得怪可惜的,但也不推荐使用了,毕竟mongodb本身一直在迭代,如果驱动器不更新后续也没法使用。 详细说明见仓库:https://github.com/go-mgo/mgo

而mongodb提供了官方驱动,目前能找到的中文文档大多比较旧了,推荐直接看官方文档,有完整的操作手册:https://www.mongodb.com/blog/search/golang 本文也不想做一次搬运工,毕竟也不能随时保持更新,还是直接看官方文档比较好。下面列出一些主要的文章链接:

Stack Overflow Research of 100,000 Developers Finds MongoDB is the Most Wanted Database (2019-2-2)

Official MongoDB Go Driver Now Available for Beta Testing (2019-2-2) mongodb将为go提供官方驱动支持

MongoDB Go Driver Tutorial (2019-5-30) MongoDB Go驱动程序教程

Go Migration Guide (2019-2-2) 从社区驱动(mgo)迁移到官方驱动

MongoDB Stitch Functions – The AWS re:Invent Stitch Rover Demo(2019-10-15)

Calling the MongoDB Atlas API - How to do it from Go(2019-3-18)

MongoDB Go Driver Tutorial Part 1: Connecting, Using BSON, and CRUD Operations(2019-4-23)

mongodb创建新数据库和创建用户

月盾

使用mongodb数据库时有这样的场景,使用可视化工具登陆了某个数据库blogs,然后又创建了数据库website,给website数据库添加用户hp_website

db.createUser( {user: "hp_website",pwd: "xxxxxx",roles: [ { role: "userAdmin", db: "website" },{ role: "dbAdmin", db: "website" },{ role: "dbOwner", db: "website" } ]})

退出后使用bbb-user登陆数据库bbb却发现登陆不上去。登陆aaa数据查看用户db.getUsers()显示

虽然创建了很多用户,但db值都是blogs,显然是不对的。即时使用use website后创建用户也是不对。 正确的做法是关闭数据库认证,使用不带--auth启动数据库,使用命令行来创建

use website

db.createUser( {user: "username",pwd: "xxxxxx",roles: [ { role: "userAdmin", db: "website" },{ role: "dbAdmin", db: "website" },{ role: "dbOwner", db: "website" } ]})

这样就可以登录了。由于数据库和用户名是绑定的,只有确保用户是创建在对应数据库上才行。

使用pm2一键部署多个服务

月盾
  1. pm2支持远程部署服务,创建文件ecosystem.json,内容形式如:
{
  // Applications part
  "apps" : [{
    "name"      : "API",
    "script"    : "app.js",
    "env": {
      "COMMON_VARIABLE": "true"
    },
    // Environment variables injected when starting with --env production
    // http://pm2.keymetrics.io/docs/usage/application-declaration/#switching-to-different-environments
    "env_production" : {
      "NODE_ENV": "production"
    }
  },{
    "name"      : "WEB",
    "script"    : "web.js"
  }],
  // 部署部分
  // Here you describe each environment
  "deploy" : {
    "production" : {
      "user" : "node",
      // 多主机配置
      "host" : ["212.83.163.1", "212.83.163.2", "212.83.163.3"],
      // 服务使用的分支
      "ref"  : "origin/master",
      // Git 仓库地址
      "repo" : "git@github.com:repo.git",
      // 项目目录
      "path" : "/var/www/production",
      // Can be used to give options in the format used in the configura-
      // tion file.  This is useful for specifying options for which there
      // is no separate command-line flag, see 'man ssh'
      // can be either a single string or an array of strings
      "ssh_options": "StrictHostKeyChecking=no",
      // To prepare the host by installing required software (eg: git)
      // even before the setup process starts
      // 可以使用";"分割多个命令
      // or path to a script on your local machine
      "pre-setup" : "npm install",
      // Commands / path to a script on the host machine
      // This will be executed on the host after cloning the repository
      // eg: placing configurations in the shared dir etc
      "post-setup": "ls -la",
      // Commands to execute locally (on the same machine you deploy things)
      // Can be multiple commands separated by the character ";"
      "pre-deploy-local" : "echo 'This is a local executed command'"
      // Commands to be executed on the server after the repo has been cloned
      "post-deploy" : "npm install && pm2 startOrRestart ecosystem.json --env production"
      // Environment variables that must be injected in all applications on this env
      "env"  : {
        "NODE_ENV": "production"
      }
    },
    "staging" : {
      "user" : "node",
      "host" : "212.83.163.1",
      "ref"  : "origin/master",
      "repo" : "git@github.com:repo.git",
      "path" : "/var/www/development",
      "ssh_options": ["StrictHostKeyChecking=no", "PasswordAuthentication=no"],
      "post-deploy" : "pm2 startOrRestart ecosystem.json --env dev",
      "env"  : {
        "NODE_ENV": "staging"
      }
    }
  }
}

Edit the file according to your needs.

gorm模糊查询和分页查询同时查总条数

月盾

gorm概述

  • 全功能ORM(几乎)
  • 关联(包含一个,包含多个,属于,多对多,多种包含)
  • Callbacks(创建/保存/更新/删除/查找之前/之后)
  • 预加载(急加载)
  • 事务
  • 复合主键
  • SQL Builder
  • 自动迁移
  • 日志
  • 可扩展,编写基于GORM回调的插件
  • 每个功能都有测试
  • 开发人员友好

like查询

gorm提供了丰富的查询功能,在开发中我们经常需要组合查询,比如列表查询,列表查询一般需要支持条件查询,模糊查询,分页查询,数据条数查询。 已上支持基本满足了日常开发需要,一些基本的查询需求可以查看文档得到解决,不过文档并没有覆盖所有日常开发案例,尤其是一些组合需求,本文挑了一段常见的场景。

func (u *userService) GetuserList(offset, limit int, search User) (users []User, count int, err error) {
	query := u.mysql.Model(&User{})
	if search.Name != "" {
		query.Where("name LIKE ?", search.Name+"%")
	}
	if search.Age != "" {
		query.Where("age = ?", search.Age)
	}

	err = query.Offset(offset).Limit(limit).Find(&users).Offset(-1).Limit(-1).Count(&count).Error
	return users, count, err
}

这简单的一小段已经包含了gorm的模糊查询动态条件分页查询数据条数。 这就是一个最常见的列表查询,列表需要支持条件查询,模糊查询,分页,从代码可以直接看到。

  1. if代码是动态组装条件。

  2. err = query.Offset(offset).Limit(limit).Find(&users).Offset(-1).Limit(-1).Count(&count).Error 这行代码包含了数据列表查询和数据条数。

  3. 有些需要注意的地方是query.Offset(offset).Limit(limit).Find(&users) 用于查询数据列表,

  4. .Offset(-1).Limit(-1).Count(&count)用户查询条数,**Offset(-1)和Limit(-1)**很重要,也是一个小技巧,不加的话会在统计条数后也加上offset和limit,导致查不到条数。 查询结果:

SELECT * FROM `user` LIMIT 10 OFFSET 0;
SELECT count(*) FROM `user`;