callmeDAY

callmeDAY 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

callmeDAY 赞了文章 · 2020-08-05

报告老板,我们的H5页面在iOS11系统上白屏了!

0802.png

时间回到一周前,当时刚开发完公司A项目的一个新的版本,等待着测试完成就进行发布。此时的我也准备从连续多日的紧张开发状态中走出来,以为可以稍稍放松一下。而那时的我还不知道,我即将面临一个强大的Bug选手,更不知道我要跟这个Bug来来回回进行多次的搏斗。当然,我们能看到这篇文章也就说明了我最终解决了这个Bug,而且这个过程也是相当的精彩的。什么?你不相信,那就让我来带你进入这个“跌宕起伏”的经历中吧。

友情提示:接下来的文章也许有一点长,但是希望你能够坚持读下去。我相信我在解决这个Bug的过程中的一些思路会给你带来一些思考。当然也希望你在这个过程中能够像我一样学习到一些新的知识,为以后排查类似的Bug积累一些经验。好啦,话不多说,让我们开始吧。

项目介绍

先来简单介绍一下A项目,这是一个基于Vue框架的项目,项目使用的也是Vue CLI这个开发工具。这个项目是需要集成在别的APP中的,也就是页面需要在APP中进浏览和操作。这个项目在我接手之前已经开发过一段时间了。所以项目中的一些依赖库和工具库版本相对比较低,这也给我后续的调试以及解决Bug的过程增加了一些困难。

BUG初现

当时开发完成之后,就交给我们这边的测试和另一个城市的相关同学去验收这次开发的功能。在我们这边一切都很正常,测试这边也没有反馈有什么问题。但是在另一个城市的同学小C的iPhone手机上却发现了白屏,打开页面之后什么内容也没有。

发现了这个问题之后,我再次跟我们这边的测试同学确认了一下,看看我们这边测试的iOS系统的iPhone手机有没有这个问题。经过测试的测试,发现我们这边的几台iPhone手机都没有问题。然后就问了小C他使用的测试手机的系统版本是多少,当时感觉应该跟iOS的系统版本有关系。

小C反馈说他的iPhone是6S Plus,然后系统的版本是11.1.2。我问了我们这边测试使用的iPhone版本都是多少,测试反馈说系统的版本都是12以上的。所以到这里,我确定了这个白屏Bug的出现肯定跟iPhone手机的系统有关系

重现BUG之路

虽然确定了问题出现的环境,但是因为我身边没有系统是11的iPhone手机,所以想让这个问题重现就变成了一个难题。询问了身边的同事,大家的系统版本也都普遍高于12,所以借用别人的手机进行调试这个方法暂时也不可行。

在平时的开发中,如果网页在iOS系统的APP中有一些问题的话,我们一般都会通过Safari浏览器进行调试。但是因为这次出现问题的iPhone手机不在我这里,并且我这边也没有相同系统的手机。所以想通过真机进行调试就不太可能了。那怎么办呢?这个问题肯定是要解决的,我也相信办法总比困难多

想要进行调试,最简单的办法就是让我有一个系统是11的iPhone手机。所以我就搜索看看有没有什么办法可以给iPhone手机安装11的系统。一搜索还真的有,过程也不算是很复杂。但是其中有一个步骤是需要到一些论坛或者第三方的助手网站下载跟自己手机型号相匹配的iOS系统,这个步骤让我有点感觉不安全。毕竟不是官方的,不能够保证安全性。而且也未必有版本是11的系统。所以这个方案就暂时作罢

在我搜索的过程中,我发现有网友说可以使用Xcode安装相应系统版本的iPhone模拟器来进行调试。哎,你说我怎么没有想到这个办法呢?这确实是一个不错的办法。因为之前跟公司的同事学习过Swift,也了解过Xcode的一些操作。突然感慨,真是技多不压身,你不知道你什么时候就会用上你学过的知识。所以有条件的话,还是多学习一些知识。额,有点跑题了。

安装Xcode

我打开公司的电脑,开始安装Xcode,但是发现公司的电脑系统版本太低,安装Xcode需要升级系统,所以没办法,先升级系统吧。因为升级的时间比较长,我想到自己家中的Mac电脑上是有安装过Xcode,所以决定先回家。留下公司的电脑慢慢升级。

回到家,二话不说就开始准备调试,但是发现我的Xcode上面的iPhone模拟器的系统版本也都是12以上的,查了一下资料,Xcode是可以安装不同系统版本的模拟器的,于是我就安装了系统版本是11的模拟器。这个过程需要我们打开Xcode的偏好设置,然后在Components选项中,选择下载你要安装的对应系统版本的模拟器。

安装iOS11的模拟器

安装成功之后,运行iPhone 6S Plus模拟器,使用模拟器的Safari打开h5的页面地址,果然是白屏。

iPhone 6S Plus模拟器出现白屏

小样,终于把这个问题给复现了,这样就距离解决这个Bug不远了。我打开MacSafari浏览器,进入开发者模式,发现了如下所示的报错

Safari浏览器控制台的报错

我搜索了一下这个错误,发现是因为项目中使用了...ES6扩展运算符,然后iOS 11系统不支持这个这个运算符。这么容易就找到问题了,开心。想到这个问题还是比较好解决的,可以通过使用Babel的一些插件,很容易就可以将这个问题解决掉。然后我就开心的睡觉去了,心想这个问题也不是什么大问题,明天处理一下就好了。

安装Safari Technology Preview

第二天到公司,我就在项目中的babel的配置文件中添加了相应的插件

{
  ...  // 省略原来的配置内容
  "plugins": ["@babel/plugin-proposal-object-rest-spread"]
}

然后发布到测试环境中。告诉了小C同学再次测试一下,我也在等着解决这个Bug的好消息。但是,出现的却不是好消息,小C给我回复说还是不可以。什么,不可能呀,我就马上用公司的电脑再次进行测试。当我用公司电脑的Safari调试系统是iOS 11iPhone 6S PLus模拟器的时候,却发现出现了下面这个情况:审核警告:“data-custom”太新,无法在此检查的页面上运行

审核警告:“data-custom”太新,无法在此检查的页面上运行

我就又搜索了一下为什么会出现这个问题,终于让我找到了答案Safari浏览器的Web Inspector工程师也说这是一个Bug,不过他们已经修复了,在下个发布的版本中就可以正常使用新的Safari浏览器去调试比较老的iOS系统的模拟器了。知道现在这个版本的Safari调试不了模拟的iOS 11系统的页面。我有点沮丧,总不能我现在回家把我的电脑拿过来吧😂?当我想着该如何解决的时候,我发现了上面那个回答中提到了Safari Technology PreviewSafari技术预览

stackoverflow上面Safari浏览器的Web检查器的开发者的回复

我看这个名字感觉有点希望,然后就搜索了一下Safari Technology Preview是什么。然后就发现它相对于Safari就跟Chromium相对于Chrome是一样,都相当于是开发版本的浏览器。

Safari Technology Preview

这时,我觉得可以使用Safari Technology Preview进行调试。所以就下载了Safari Technology Preview,当我打开Safari Technology Preview然后进入开发者模式后,发现确实可以调试iOS 11系统的页面。然后我就看了一下为什么还是白屏的问题。发现出现的错误还是上次的问题:

SyntaxError: Unexpected token '...'. Expected a property name.

也就是说这个问题还没有解决掉,因为打包后的代码是没有SourceMap的,所以要想看更详细的报错信息,需要在本地进行调试。本地的环境中是有SourceMap的,可以定位到更详细的错误信息,我在本地运行了项目,然后我打开了控制台的错误详情,发现是使用的一个第三方的库出现了问题。

找到了出现问题的使用的第三方库

那么到这里为止,可以说明上面我们使用的Babel插件没有处理这个第三方的库,所以现在我们的问题就变成了:如何解决第三方库中出现的...扩展运算符没有被编译为ES5语法的问题

将第三方库中的ES6语法进行编译

查看Vue CLI中相关的配置方法

这时我又仔细的看了一下Vue CLI的相关文档,发现确实在浏览器的兼容性这个章节中,提到了一些处理的方法。原来我们在项目中写的代码默认会帮我们转换为ES5的语法的,但是如果项目中依赖的第三方库需要polyfill的话,那需要我们手动进行配置。一看到这里,我感觉黎明就要来了

Vue CLI浏览器兼容性

我就开始尝试这三种方法。我发现第一种方法是比较简单的,也很好配置。于是我就尝试了第一种方法。在项目的vue.config.js中添加如下的配置:

...  // 省略的配置
transpileDependencies: [
  'module-name/library-name' // 出现问题的那个库
],
...  // 省略的配置

重新运行项目,当我将要为即将到来的成功欢呼鼓掌时,控制台突然报告了如下的错误:
Uncaught TypeError: Cannot assign to read only property 'exports' of object '#<Object>'

Uncaught TypeError: Cannot assign to read only property ...

这个报错是在Chrome浏览器的控制台出现的,因为项目在本地重新运行之后会首先打开Chrome浏览器。真是的,一个问题还没有解决,又出来了一个新的问题。然后再次查询资料后发现,原来是因为这个第三方的库是一个CommonJS类型的库,而Babel默认处理的是ES6module类型的库,所以这里就又出现了新的问题。

https://github.com/webpack/webpack/issues/4039 sokra的回复

第一种方法遇到了阻碍,先暂停一下。我准备继续尝试下面两种方法。但是因为后面两种方法对原来的项目改动有点大,所以我直接通过Vue CLI创建了一个新的项目,在package.json中加入项目中使用的那个第三方包的依赖,使用公司的包管理工具安装了依赖。然后运行项目,打开控制台确实发现了相同的错误。但是打开详情以后,发现出错的路径跟我原来项目不一致。然后我这次抱着试一试的心态,继续使用了第一种方法尝试看看可不可以。然后复制了出错路径的包名称,在vue.config.js文件中的对应位置添加了如下的配置代码:

...  // 省略的配置
transpileDependencies: [
  'module-name-new/library-name-new' // 出现问题的那个库
],
...  // 省略的配置

然后重新运行项目,发现居然可以了。啊,居然可以了。为什么我在原来的项目中这样却不可以呢?我看了一下原来项目的依赖以及现在新的测试项目的依赖,发现它们的vue, babel版本差了好多。我猜测可能是因为这个原因。但是现在肯定不可以贸然升级这些依赖的版本,因为为了解决这个问题再次带来新的问题就得不偿失了。

还有一个问题就是为什么同样的第三方库,在原来的项目中和现在的项目中报错的路径不一样。而且看着像是使用了两个不一样的第三方库。这里先留个悬念,我会在后面的文章中进行解释。

接下来,我开始在测试项目中继续尝试剩下的两种方法,对于第二种方法,因为老项目中使用的presets是没有polyfills这个配置选项的,到现在为止出问题的这个第三方库我不知道除了这个...对象扩展操作符之外还有没有别的依赖。所以这个方法我暂时也放弃了。

对于第三个方法,我觉得可以尝试,首先我将测试项目中的一些关键依赖进行了手动降级,然后按照上面的第三个方法的步骤在测试项目中使用。但是发现测试项目运行之后,提示需要安装core-js,安装core-js之后还报错,再次提示需要安装es.module.regex.match等等很多依赖,继续查资料,发现需要把配置中的 useBuiltIns修改,但是因为我接手的这个项目是老项目,依赖比较多,不确定修改useBuiltIns这个配置选项后会不会出现新的问题。所以也不敢贸然修改这个配置选项,所以也暂时放弃了这个方法。

我后来想了一下,对于...扩展运算符来说,这是一个新的语法。是不能够通过一些polyfills去解决的。需要Babel对这个语法进行编译,然后才可以在低版本的系统中使用,所以解决的办法还是要让Babel对这个库再次进行编译。

寻找新的突破口

当进行到了这里的时候,似乎没有了出路。一时间我感觉我要被这个Bug打败了,我似乎听到了它无情的嘲笑,“小伙子,是不是被我折磨的没有脾气啦;放弃吧,你是没办法打倒我的。哈哈哈。。。

Photo by sebastiaan stam on Unsplash

但是,它看错我了,Bug越是难解决,我对它就越有兴趣。所以我决定好好理一下思路,准备再次扬帆起航。

我发现第一种办法其实是起作用的,只不过是因为一个是CommonJS类型的,一个需要是ES6 module类型的。所以我决定从这个地方入手,于是我决定查查相关的资料,看看Babel有没有办法可以即能够处理CommonJS模块,又能够处理ES6 module模块呢?终于,功夫不负有心人,我发现了Babel里面有这么一个配置sourceType,如果把sourceType设置为unambiguous就可以解决这个问题

https://babeljs.io/docs/en/options#sourcetype

这样Babel就会根据模块文件中有没有import/export来决定使用哪种解析模块的方式。于是我再次使用了第一种方法,在vue.config.js中添加了transpileDependencies选项的配置,然后在项目中的Babel配置文件中添加了如下的配置:

module.exports = {
  ...  // 省略的配置
  sourceType: 'unambiguous',
  ...  // 省略的配置
};

发现的确可以,这一刻成功的喜悦再次降临。然后我再次打包,再次把代码部署到测试环境,赶忙让小C同学再次测试一下,发现的确可以。欧耶,终于解决这个问题了。我终于可以松一口气了,哈哈哈。。。小样,这怎么会难得到我呢?

但是,当我仔细阅读将这个选项设置为unambiguous时,我发现了一些问题。因为这样的话会有一些风险,因为就算不使用import/export语句的这些模块也可能是完全有效的ES6 module,所以这样的话就有可能会出现一些意外的情况。怎么办,我似乎在一不留神的时候又被Bug卡住了脖子

https://babeljs.io/docs/en/options#sourcetype

我觉得老天总是给我开玩笑,当我从一个坑里跳出来,以为没有危险的时候。前面突然又多出来一个坑,我一不留心就又掉了进去。我感觉既然都走到了这里,肯定要继续走下去,一定有办法可以优化我现在遇到的问题。我就很仔细的再次看了一下Babel的配置说明文档,这个时候就心想如果我对Babel再熟悉一些就好了。没关系,继续努力。终于,我似乎看到了什么了不得的配置选项。

https://babeljs.io/docs/en/options#overrides

我在Config Merging options里发现了overrides选项,这个配置选项不正是我需要的吗?我可以利用这个配置选项将我需要的第三方包使用unambiguous的处理方式,然后其他的第三方库都按照之前的方式处理不就可以了。哈哈哈,我真是个天才,我心里这样对自己说😂。

Photo by bruce mars on Unsplash

所以只需要在项目的babel.config.js中写下如下的配置就可以了:

module.exports = {
  ...  // 省略的配置
  overrides: [
    {
      include: './node_modules/module-name/library-name/name.common.js',  // 使用的第三方库
      sourceType: 'unambiguous'
    }
  ],
  ...  // 省略的配置
};

对了,还有一件事情还没有说,那就是上文提到的关于为什么使用公司自己的包管理工具下载下来的node_modules包的名称跟使用官方的npm包管理工具下载的包的名称不一致的问题。原因是公司使用的包管理工具是cnpm的一个修改版本。又因为cnpm为了提高下载的速度,使用了cnpm/npminstall,所以才会出现下载的包名比较混乱的情况,详情可以看这里

到此完结撒花,总结一下:出现白屏的原因是因为使用的第三方库的包中使用了...扩展运算符,然后因为第三方的包默认是没有被Babel处理过的,所以在不支持...iOS 11系统上就出现了白屏。解决的方式就是通过给vue.config.js的配置文件中transpileDependencies配置选项中添加上出问题的包的名称就可以了。当然如果项目比较老,可能还需要像文章上面写的那样的处理方式。

解决这个Bug过程就像是升级打怪一样,不断失败,不断尝试,只要不放弃,终有成功的那一天。如果你坚持看到了这里,那说明你也很棒呀。在当今这个信息爆炸的时代里,能够坚持看完一篇很长的文章已经很不错了。

一点反思与思考:这个过程中我也发现了自己对BabelVue CLI其实没有那么熟练,如果我对它们比较熟练的话,那我解决这个Bug应该会花费更少的时间。当然,现在把它们学习好也不算晚。要抱着学习的态度,这次解决这个Bug的过程,就是我以后解决其它类似Bug的经验。还有在解决Bug的这个过程中要有耐心,当然在尝试之后也要学会放弃错误的方向

写这篇文章也花费了我不少的时间,如果你有所收获或者感悟,不妨点赞,转发,收藏走一波,这个要求应该不算过分吧😂?

如果你对本篇文章有什么意见和建议,都可以直接在文章下面留言,也可以在这里提出来。也欢迎大家关注我的公众号关山不难越,学习更多实用的前端知识,让我们一起努力进步吧。

公众号:关山不难越

查看原文

赞 21 收藏 10 评论 11

callmeDAY 赞了文章 · 2019-11-01

前端性能优化指南

作者:JowayYoung
仓库:GithubCodePen
博客:官网掘金思否知乎
公众号:IQ前端
特别声明:原创不易,未经授权不得转载或抄袭,如需转载可联系笔者授权

前言

发现总结性的小干货可以为大家提升更好的开发技巧和编码思维,对代码量产化提供更扎实的质量和支持。这次我们来聊聊大家可能都比较关心的话题:性能优化

一说到页面的性能优化,大家可能都会想起雅虎军规2-5-8原则3秒钟首屏指标等规则,这些规则在开发过程中不是强制要求的,但是有时候为了追求页面性能的完美和体验,就不得不对原有的代码进行修改和优化。

下面就结合自己三年多的开发经验和大量的项目实践,整理出一些常用的性能优化要点,同时再罗列一下雅虎军规2-5-8原则3秒钟首屏指标这三个常用规则的要点。

为了方便记忆和阅读,文章使用部分简写名词,解释如下
  • D端:桌面端页面Desktop End Page
  • M端:移动端页面Mobile End Page

概述指南

  1. D端优化手段在M端同样适用
  2. 在M端提出3秒钟渲染完成首屏指标
  3. 基于第二点,首屏加载3秒内完成或使用Loading进行占位
  4. 基于联通3G网络平均338kb/s(2.71mb/s),首屏资源不应超过1014kb
  5. M端因配置原因,除加载外渲染速度也是优化重点
  6. 基于第五点,要合理处理代码减少渲染损耗
  7. 基于第二点和第五点,所有影响首屏加载和渲染的代码应在处理逻辑中后置
  8. 加载完成后,用户交互使用时也需注意性能
加载优化
  • 减少HTTP请求:尽量减少页面的请求数(首次加载同时请求数不能超过4个),移动设备浏览器同时响应请求为4个请求(Android支持4个,iOS5+支持6个)

    • 合并CSS和JS
    • 使用CSS精灵图
  • 缓存资源:使用缓存可减少向服务器的请求数,节省加载时间,所有静态资源都要在服务器端设置缓存,并且尽量使用长缓存(使用时间戳更新缓存)

    • 缓存一切可缓存的资源
    • 使用长缓存
    • 使用外联的样式和脚本
  • 压缩代码:减少资源大小可加快网页显示速度,对代码进行压缩,并在服务器端设置GZip

    • 压缩代码(多余的缩进、空格和换行符)
    • 启用Gzip
  • 无阻塞:头部内联的样式和脚本会阻塞页面的渲染,样式放在头部并使用link方式引入,脚本放在尾部并使用异步方式加载
  • 首屏加载:首屏快速显示可大大提升用户对页面速度的感知,应尽量针对首屏的快速显示做优化
  • 按需加载:将不影响首屏的资源和当前屏幕不用的资源放到用户需要时才加载,可大大提升显示速度和降低总体流量(按需加载会导致大量重绘,影响渲染性能)

    • 懒加载
    • 滚屏加载
    • Media Query加载
  • 预加载:大型资源页面可使用Loading,资源加载完成后再显示页面,但加载时间过长,会造成用户流失

    • 可感知Loading:进入页面时Loading
    • 不可感知Loading:提前加载下一页
  • 压缩图像:使用图像时选择最合适的格式和大小,然后使用工具压缩,同时在代码中用srcset来按需显示(过度压缩图像大小影响图像显示效果)

    • 使用TinyJpgTinyPng压缩图像
    • 使用CSS3、SVG、IconFont代替图像
    • 使用img的srcset按需加载图像
    • 选择合适的图像:webp优于jpgpng8优于gif
    • 选择合适的大小:首次加载不大于1014kb、不宽于640px
    • PS切图时D端图像保存质量为80,M端图像保存质量为60
  • 减少CookieCookie会影响加载速度,静态资源域名不使用Cookie
  • 避免重定向:重定向会影响加载速度,在服务器正确设置避免重定向
  • 异步加载第三方资源:第三方资源不可控会影响页面的加载和显示,要异步加载第三方资源
加载过程是最为耗时的过程,可能会占到总耗时的`80%时间(**优化重点**)
执行优化
  • CSS写在头部,JS写在尾部并异步
  • 避免img、iframe等的src为空:空src会重新加载当前页面,影响速度和效率
  • 尽量避免重置图像大小:多次重置图像大小会引发图像的多次重绘,影响性能
  • 图像尽量避免使用DataURLDataURL图像没有使用图像的压缩算法,文件会变大,并且要解码后再渲染,加载慢耗时长
执行处理不当会阻塞页面加载和渲染
渲染优化
  • 设置viewport:HTML的viewport可加速页面的渲染

    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, minimum-scale=1, maximum-scale=1">
  • 减少DOM节点:DOM节点太多影响页面的渲染,尽量减少DOM节点
  • 优化动画

    • 尽量使用CSS3动画
    • 合理使用requestAnimationFrame动画代替setTimeout
    • 适当使用Canvas动画:5个元素以内使用CSS动画,5个元素以上使用Canvas动画iOS8+可使用WebGL动画
  • 优化高频事件scrolltouchmove等事件可导致多次渲染

    • 函数节流
    • 函数防抖
    • 使用requestAnimationFrame监听帧变化:使得在正确的时间进行渲染
    • 增加响应变化的时间间隔:减少重绘次数
  • GPU加速:使用某些HTML5标签和CSS3属性会触发GPU渲染,请合理使用(过渡使用会引发手机耗电量增加)

    • HTML标签:videocanvaswebgl
    • CSS属性:opacitytransformtransition
样式优化
  • 避免在HTML中书写style
  • 避免CSS表达式:CSS表达式的执行需跳出CSS树的渲染
  • 移除CSS空规则:CSS空规则增加了css文件的大小,影响CSS树的执行
  • 正确使用displaydisplay会影响页面的渲染

    • display:inline后不应该再使用floatmarginpaddingwidthheight
    • display:inline-block后不应该再使用float
    • display:block后不应该再使用vertical-align
    • display:table-*后不应该再使用floatmargin
  • 不滥用floatfloat在渲染时计算量比较大,尽量减少使用
  • 不滥用Web字体:Web字体需要下载、解析、重绘当前页面,尽量减少使用
  • 不声明过多的font-size:过多的font-size影响CSS树的效率
  • 值为0时不需要任何单位:为了浏览器的兼容性和性能,值为0时不要带单位
  • 标准化各种浏览器前缀

    • 无前缀属性应放在最后
    • CSS动画属性只用-webkit-、无前缀两种
    • 其它前缀为-webkit-、-moz-、-ms-、无前缀四种:Opera改用blink内核,-o-已淘汰
  • 避免让选择符看起来像正则表达式:高级选择符执行耗时长且不易读懂,避免使用
脚本优化
  • 减少重绘和回流

    • 避免不必要的DOM操作
    • 避免使用document.write
    • 减少drawImage
    • 尽量改变class而不是style,使用classList代替className
  • 缓存DOM选择与计算:每次DOM选择都要计算和缓存
  • 缓存.length的值:每次.length计算用一个变量保存值
  • 尽量使用事件代理:避免批量绑定事件
  • 尽量使用id选择器id选择器选择元素是最快的
  • touch事件优化:使用tap(touchstarttouchend)代替click(注意touch响应过快,易引发误操作)

常用规则

雅虎军规

雅虎团队通过大量实践总结出以下7类35条前端优化规则,规则详情请参考这位兄弟的《雅虎前端优化35条规则翻译》

  • 内容

    • Make Fewer HTTP Requests:减少HTTP请求数
    • Reduce DNS Lookups:减少DNS查询
    • Avoid Redirects:避免重定向
    • Make Ajax Cacheable:缓存AJAX请求
    • Postload Components:延迟加载资源
    • Preload Components:预加载资源
    • Reduce The Number Of DOM Elements:减少DOM元素数量
    • Split Components Across Domains:跨域拆分资源
    • Minimize The Number Of Iframes:减少iframe数量
    • No 404s:消除404错误
  • 样式

    • Put Stylesheets At The Top:置顶样式
    • Avoid CSS Expressions:避免CSS表达式
    • Choose \<link\> Over @import:选择<link>代替@import
    • Avoid Filters:避免滤镜
  • 脚本

    • Put Scripts At The Bottom:置底脚本
    • Make JavaScript And CSS External:使用外部JSCSS
    • Minify JavaScript And CSS:压缩JSCSS
    • Remove Duplicate Scripts:删除重复脚本
    • Minimize DOM Access:减少DOM操作
    • Develop Smart Event Handlers:开发高效的事件处理
  • 图像

    • Optimize Images:优化图片
    • Optimize CSS Sprites:优化CSS精灵图
    • Don't Scale Images In HTML:不在HTML中缩放图片
    • Make Favicon.ico Small And Cacheable:使用小体积可缓存的favicon
  • 缓存

    • Reduce Cookie Size:减少Cookie大小
    • Use Cookie-Free Domains For Components:使用无Cookie域名的资源
  • 移动端

    • Keep Components Under 25kb:保持资源小于25kb
    • Pack Components Into A Multipart Document:打包资源到多部分文档中
  • 服务器

    • Use A Content Delivery Network:使用CDN
    • Add An Expires Or A Cache-Control Header:响应头添加ExpiresCache-Control
    • Gzip ComponentsGzip资源
    • Configure ETags:配置ETags
    • Flush The Buffer Early:尽早输出缓冲
    • Use Get For AJAX RequestsAJAX请求时使用get
    • Avoid Empty Image Src:避免图片空链接
2-5-8原则

在前端开发中,此规则作为一种开发指导思路,针对浏览器页面的性能优化。

  • 用户在2秒内得到响应,会感觉页面的响应速度很快 Fast
  • 用户在2~5秒间得到响应,会感觉页面的响应速度还行 Medium
  • 用户在5~8秒间得到响应,会感觉页面的响应速度很慢,但还可以接受 Slow
  • 用户在8秒后仍然无法得到响应,会感觉页面的响应速度垃圾死了(此时会有以下四种可能)

    • 难道是网速不好,发起第二次请求 => 刷新页面
    • 什么垃圾页面呀,怎么还不打开 => 离开页面,有可能转投竞争对手的网站
    • 垃圾程序猿,做的是什么页面啊 => 咒骂开发此页面的程序猿
    • 断网了 => 网线断了?Wi-Fi断了?信号不好?话费用完了?
知道这个规则的数字顺序怎样来的吗,看下键盘右方的数字键盘由下往上排序:2-5-8
3秒钟首屏指标

此规则适用于M端,顾名思义就是打开页面后3秒钟内完成渲染并展示内容。

结语

❤️关注+点赞+收藏+评论+转发❤️,原创不易,鼓励笔者创作更多高质量文章

关注公众号IQ前端,一个专注于CSS/JS开发技巧的前端公众号,更多前端小干货等着你喔

  • 关注后回复资料免费领取学习资料
  • 关注后回复进群拉你进技术交流群
  • 欢迎关注IQ前端,更多CSS/JS开发技巧只在公众号推送

查看原文

赞 148 收藏 109 评论 1

callmeDAY 赞了文章 · 2018-04-19

GraphQL 搭配 Koa 最佳入门实践

代码有更新,点击直接查看

GraphQL一种用为你 API 而生的查询语言,2018已经到来,PWA还没有大量投入生产应用之中就已经火起来了,GraphQL的应用或许也不会太远了。前端的发展的最大一个特点就是变化快,有时候应对各种需求场景的变化,不得不去对接口开发很多版本或者修改。各种业务依赖强大的基础数据平台快速生长,如何高效地为各种业务提供数据支持,是所有人关心的问题。而且现在前端的解决方案是将视图组件化,各个业务线既可以是组件的使用者,也可以是组件的生产者,如果能够将其中通用的内容抽取出来提供给各个业务方反复使用,必然能够节省宝贵的开发时间和开发人力。那么问题来了,前端通过组件实现了跨业务的复用,后端接口如何相应地提高开发效率呢?GraphQL,就是应对复杂场景的一种新思路。

官方解释:

GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。 GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且没有任何冗余,也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具。

下面介绍一下GraphQL的有哪些好处:

  • 请求你所要的数据不多不少
  • 获取多个资源只用一个请求
  • 自定义接口数据的字段
  • 强大的开发者工具
  • API 演进无需划分版本

本篇文章中将搭配koa实现一个GraphQL查询的例子,逐步从简单kao服务到mongodb的数据插入查询再到GraphQL的使用,
让大家快速看到:

  • 搭建koa搭建一个后台项目
  • 后台路由简单处理方式
  • 利用mongoose简单操作mongodb
  • 掌握GraphQL的入门姿势

项目如下图所示

1、搭建GraphQL工具查询界面。

2、前端用jq发送ajax的使用方式

入门项目我们都已经是预览过了,下面我们动手开发吧!!!

lets do it

首先建立一个项目文件夹,然后在这个项目文件夹新建一个server.js(node服务)、config文件夹mongodb文件夹router文件夹controllers文件夹以及public文件夹(这个主要放前端静态数据展示页面),好啦,项目的结构我们都已经建立好,下面在server.js文件夹里写上

server.js
// 引入模块
import Koa from 'koa'
import KoaStatic from 'koa-static'
import Router from 'koa-router'
import bodyParser from 'koa-bodyparser'


const app = new Koa()
const router = new Router();

// 使用 bodyParser 和 KoaStatic 中间件
app.use(bodyParser());
app.use(KoaStatic(__dirname + '/public'));

// 路由设置test
router.get('/test', (ctx, next) => {
  ctx.body="test page"
});

app
  .use(router.routes())
  .use(router.allowedMethods());

app.listen(4000);

console.log('graphQL server listen port: ' + 4000)

在命令行npm install koa koa-static koa-router koa-bodyparser --save

安装好上面几个模块,

然后运行node server.js,不出什么意外的话,你会发现报如下图的一个error

原因是现在的node版本并没有支持es6的模块引入方式。

放心 我们用神器babel-polyfill转译一下就阔以了。详细的请看阮一峰老师的这篇文章

下面在项目文件夹新建一个start.js,然后在里面写上以下代码:

start.js
require('babel-core/register')({
  'presets': [
    'stage-3',
    ["latest-node", { "target": "current" }]
  ]
})

require('babel-polyfill')
require('./server')

然后 在命令行,运行npm install babel-core babel-polyfill babel-preset-latest-node babel-preset-stage-3 --save-dev安装几个开发模块。

安装完毕之后,在命令行运行 node start.js,之后你的node服务安静的运行起来了。用koa-router中间件做我们项目路由模块的管理,后面会写到router文件夹中统一管理。

打开浏览器,输入localhost:4000/test,你就会发现访问这个路由node服务会返回test page文字。如下图

yeah~~kao服务器基本搭建好之后,下面就是,链接mongodb然后把数据存储到mongodb数据库里面啦。

实现mongodb的基本数据模型

tip:这里我们需要mongodb存储数据以及利用mongoose模块操作mongodb数据库

  • mongodb文件夹新建一个index.jsschema文件夹, 在 schema文件夹文件夹下面新建info.jsstudent.js
  • config文件夹下面建立一个index.js,这个文件主要是放一下配置代码。

又一波文件建立好之后,先在config/index.js下写上链接数据库配置的代码。

config/index.js
export default {
  dbPath: 'mongodb://localhost/graphql'
}

然后在mongodb/index.js下写上链接数据库的代码。

mongodb/index.js
// 引入mongoose模块
import mongoose from 'mongoose'
import config from '../config'

// 同步引入 info model和 studen model
require('./schema/info')
require('./schema/student')

// 链接mongodb
export const database = () => {
  mongoose.set('debug', true)

  mongoose.connect(config.dbPath)

  mongoose.connection.on('disconnected', () => {
    mongoose.connect(config.dbPath)
  })
  mongoose.connection.on('error', err => {
    console.error(err)
  })

  mongoose.connection.on('open', async () => {
    console.log('Connected to MongoDB ', config.dbPath)
  })
}

上面我们我们代码还加载了info.jsstuden.js这两个分别是学生的附加信息和基本信息的数据模型,为什么会分成两个信息表?原因是顺便给大家介绍一下联表查询的基本方法(嘿嘿~~~)

下面我们分别完成这两个数据模型

mongodb/schema/info.js
// 引入mongoose
import mongoose from 'mongoose'

// 
const Schema = mongoose.Schema

// 实例InfoSchema
const InfoSchema = new Schema({
  hobby: [String],
  height: String,
  weight: Number,
  meta: {
    createdAt: {
      type: Date,
      default: Date.now()
    },
    updatedAt: {
      type: Date,
      default: Date.now()
    }
  }
})
// 在保存数据之前跟新日期
InfoSchema.pre('save', function (next) {
  if (this.isNew) {
    this.meta.createdAt = this.meta.updatedAt = Date.now()
  } else {
    this.meta.updatedAt = Date.now()
  }

  next()
})
// 建立Info数据模型
mongoose.model('Info', InfoSchema)

上面的代码就是利用mongoose实现了学生的附加信息的数据模型,用同样的方法我们实现了student数据模型

mongodb/schema/student.js
import mongoose from 'mongoose'

const Schema = mongoose.Schema
const ObjectId = Schema.Types.ObjectId


const StudentSchema = new Schema({
  name: String,
  sex: String,
  age: Number,
  info: {
    type: ObjectId,
    ref: 'Info'
  },
  meta: {
    createdAt: {
      type: Date,
      default: Date.now()
    },
    updatedAt: {
      type: Date,
      default: Date.now()
    }
  }
})

StudentSchema.pre('save', function (next) {
  if (this.isNew) {
    this.meta.createdAt = this.meta.updatedAt = Date.now()
  } else {
    this.meta.updatedAt = Date.now()
  }

  next()
})

mongoose.model('Student', StudentSchema)

实现保存数据的控制器

数据模型都链接好之后,我们就添加一些存储数据的方法,这些方法都写在控制器里面。然后在controler里面新建info.jsstudent.js,这两个文件分别对象,操作info和student数据的控制器,分开写为了方便模块化管理。

  • 实现info数据信息的保存,顺便把查询也先写上去,代码很简单
controlers/info.js
import mongoose from 'mongoose'
const Info = mongoose.model('Info')

// 保存info信息
export const saveInfo = async (ctx, next) => {
  // 获取请求的数据
  const opts = ctx.request.body
  
  const info = new Info(opts)
  const saveInfo = await info.save() // 保存数据
  console.log(saveInfo)
  // 简单判断一下 是否保存成功,然后返回给前端
  if (saveInfo) {
    ctx.body = {
      success: true,
      info: saveInfo
    }
  } else {
    ctx.body = {
      success: false
    }
  }
}

// 获取所有的info数据
export const fetchInfo = async (ctx, next) => {
  const infos = await Info.find({}) // 数据查询

  if (infos.length) {
    ctx.body = {
      success: true,
      info: infos
    }
  } else {
    ctx.body = {
      success: false
    }
  }
}

上面的代码,就是前端用post(路由下面一会在写)请求过来的数据,然后保存到mongodb数据库,在返回给前端保存成功与否的状态。也简单实现了一下,获取全部附加信息的的一个方法。下面我们用同样的道理实现studen数据的保存以及获取。

  • 实现studen数据的保存以及获取
controllers/sdudent.js
import mongoose from 'mongoose'
const Student = mongoose.model('Student')

// 保存学生数据的方法
export const saveStudent = async (ctx, next) => {
  // 获取前端请求的数据
  const opts = ctx.request.body
  
  const student = new Student(opts)
  const saveStudent = await student.save() // 保存数据

  if (saveStudent) {
    ctx.body = {
      success: true,
      student: saveStudent
    }
  } else {
    ctx.body = {
      success: false
    }
  }
}

// 查询所有学生的数据
export const fetchStudent = async (ctx, next) => {
  const students = await Student.find({})

  if (students.length) {
    ctx.body = {
      success: true,
      student: students
    }
  } else {
    ctx.body = {
      success: false
    }
  }
}

// 查询学生的数据以及附加数据
export const fetchStudentDetail = async (ctx, next) => {

  // 利用populate来查询关联info的数据
  const students = await Student.find({}).populate({
    path: 'info',
    select: 'hobby height weight'
  }).exec()

  if (students.length) {
    ctx.body = {
      success: true,
      student: students
    }
  } else {
    ctx.body = {
      success: false
    }
  }
}

实现路由,给前端提供API接口

数据模型和控制器在上面我们都已经是完成了,下面就利用koa-router路由中间件,来实现请求的接口。我们回到server.js,在上面添加一些代码。如下

server.js
import Koa from 'koa'
import KoaStatic from 'koa-static'
import Router from 'koa-router'
import bodyParser from 'koa-bodyparser'

import {database} from './mongodb' // 引入mongodb
import {saveInfo, fetchInfo} from './controllers/info' // 引入info controller
import {saveStudent, fetchStudent, fetchStudentDetail} from './controllers/student' // 引入 student controller

database() // 链接数据库并且初始化数据模型

const app = new Koa()
const router = new Router();

app.use(bodyParser());
app.use(KoaStatic(__dirname + '/public'));

router.get('/test', (ctx, next) => {
  ctx.body="test page"
});

// 设置每一个路由对应的相对的控制器
router.post('/saveinfo', saveInfo)
router.get('/info', fetchInfo)

router.post('/savestudent', saveStudent)
router.get('/student', fetchStudent)
router.get('/studentDetail', fetchStudentDetail)

app
  .use(router.routes())
  .use(router.allowedMethods());

app.listen(4000);

console.log('graphQL server listen port: ' + 4000)

上面的代码,就是做了,引入mongodb设置,info以及student控制器,然后链接数据库,并且设置每一个设置每一个路由对应的我们定义的的控制器。

安装一下mongoose模块 npm install mongoose --save

然后在命令行运行node start,我们服务器运行之后,然后在给info和student添加一些数据。这里是通过postman的谷歌浏览器插件来请求的,如下图所示

yeah~~~保存成功,继续按照步骤多保存几条,然后按照接口查询一下。如下图

嗯,如图都已经查询到我们保存的全部数据,并且全部返回前端了。不错不错。下面继续保存学生数据。

tip: 学生数据保存的时候关联了信息里面的数据哦。所以把id写上去了。

同样的一波操作,我们多保存学生几条信息,然后查询学生信息,如下图所示。

好了 ,数据我们都已经保存好了,铺垫也做了一大把了,下面让我们真正的进入,GrapgQL查询的骚操作吧~~~~

重构路由,配置GraphQL查询界面

别忘了,下面我们建立了一个router文件夹,这个文件夹就是统一管理我们路由的模块,分离了路由个应用服务的模块。在router文件夹新建一个index.js。并且改造一下server.js里面的路由全部复制到router/index.js

顺便在这个路由文件中加入,graphql-server-koa模块,这是koa集成的graphql服务器模块。graphql server是一个社区维护的开源graphql服务器,可以与所有的node.js http服务器框架一起工作:express,connect,hapi,koa和restify。可以点击链接查看详细知识点。

加入graphql-server-koa的路由文件代码如下:

router/index.js

import { graphqlKoa, graphiqlKoa } from 'graphql-server-koa'
import {saveInfo, fetchInfo} from '../controllers/info'
import {saveStudent, fetchStudent, fetchStudentDetail} from '../controllers/student'


const router = require('koa-router')()

router.post('/saveinfo', saveInfo)
      .get('/info', fetchInfo)
      .post('/savestudent', saveStudent)
      .get('/student', fetchStudent)
      .get('/studentDetail', fetchStudentDetail)
      .get('/graphiql', async (ctx, next) => {
        await graphiqlKoa({endpointURL: '/graphql'})(ctx, next)
      })
module.exports = router

之后把server.js的路由代码去掉之后的的代码如下:

server.js

import Koa from 'koa'
import KoaStatic from 'koa-static'
import Router from 'koa-router'
import bodyParser from 'koa-bodyparser'

import {database} from './mongodb'

database()

const GraphqlRouter = require('./router')

const app = new Koa()
const router = new Router();

const port = 4000

app.use(bodyParser());
app.use(KoaStatic(__dirname + '/public'));

router.use('', GraphqlRouter.routes())

app.use(router.routes())
   .use(router.allowedMethods());

app.listen(port);

console.log('GraphQL-demo server listen port: ' + port)

恩,分离之后简洁,明了了很多。然后我们在重新启动node服务。在浏览器地址栏输入http://localhost:4000/graphiql,就会得到下面这个界面。如图:

没错,什么都没有 就是GraphQL查询服务的界面。下面我们把这个GraphQL查询服务完善起来。

编写GraphQL Schema

看一下我们第一张图,我们需要什么数据,在GraphQL查询界面就编写什么字段,就可以查询到了,而后端需要定义好这些数据格式。这就需要我们定义好GraphQL Schema。

首先我们在根目录新建一个graphql文件夹,这个文件夹用于存放管理graphql相关的js文件。然后在graphql文件夹新建一个schema.js

这里我们用到graphql模块,这个模块就是用javascript参考实现graphql查询。向需要详细学习,请使劲戳链接。

我们先写好info的查询方法。然后其他都差不多滴。

graphql/schema.js

// 引入GraphQL各种方法类型

import {
  graphql,
  GraphQLSchema,
  GraphQLObjectType,
  GraphQLString,
  GraphQLID,
  GraphQLList,
  GraphQLNonNull,
  isOutputType
} from 'graphql';

import mongoose from 'mongoose'
const Info = mongoose.model('Info') // 引入Info模块

// 定义日期时间 类型
const objType = new GraphQLObjectType({
  name: 'mete',
  fields: {
    createdAt: {
      type: GraphQLString
    },
    updatedAt: {
      type: GraphQLString
    }
  }
})

// 定义Info的数据类型
let InfoType = new GraphQLObjectType({
  name: 'Info',
  fields: {
    _id: {
      type: GraphQLID
    },
    height: {
      type: GraphQLString
    },
    weight: {
      type: GraphQLString
    },
    hobby: {
      type: new GraphQLList(GraphQLString)
    },
    meta: {
      type: objType
    }
  }
})

// 批量查询
const infos = {
  type: new GraphQLList(InfoType),
  args: {},
  resolve (root, params, options) {
    return Info.find({}).exec() // 数据库查询
  }
}

// 根据id查询单条info数据

const info = {
  type: InfoType,
  // 传进来的参数
  args: {
    id: {
      name: 'id',
      type: new GraphQLNonNull(GraphQLID) // 参数不为空
    }
  },
  resolve (root, params, options) {
    return Info.findOne({_id: params.id}).exec() // 查询单条数据
  }
}

// 导出GraphQLSchema模块

export default new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Queries',
    fields: {
      infos,
      info
    }
  })
})

看代码的时候建议从下往上看~~~~,上面代码所说的就是,建立info和infos的GraphQLSchema,然后定义好数据格式,查询到数据,或者根据参数查询到单条数据,然后返回出去。

写好了info schema之后 我们在配置一下路由,进入router/index.js里面,加入下面几行代码。

router/index.js

import { graphqlKoa, graphiqlKoa } from 'graphql-server-koa'
import {saveInfo, fetchInfo} from '../controllers/info'
import {saveStudent, fetchStudent, fetchStudentDetail} from '../controllers/student'

// 引入schema
import schema from '../graphql/schema'

const router = require('koa-router')()

router.post('/saveinfo', saveInfo)
      .get('/info', fetchInfo)
      .post('/savestudent', saveStudent)
      .get('/student', fetchStudent)
      .get('/studentDetail', fetchStudentDetail)




router.post('/graphql', async (ctx, next) => {
        await graphqlKoa({schema: schema})(ctx, next) // 使用schema
      })
      .get('/graphql', async (ctx, next) => {
        await graphqlKoa({schema: schema})(ctx, next) // 使用schema
      })
      .get('/graphiql', async (ctx, next) => {
        await graphiqlKoa({endpointURL: '/graphql'})(ctx, next) // 重定向到graphiql路由
      })
module.exports = router

详细请看注释,然后被忘记安装好npm install graphql-server-koa graphql --save这两个模块。安装完毕之后,重新运行服务器的node start(你可以使用nodemon来启动本地node服务,免得来回启动。)

然后刷新http://localhost:4000/graphiql,你会发现右边会有查询文档,在左边写上查询方式,如下图

重整Graphql代码结构,完成所有数据查询

现在是我们把schema和type都写到一个文件上面了去了,如果数据多了,字段多了变得特别不好维护以及review,所以我们就把定义type的和schema分离开来,说做就做。

graphql文件夹新建info.jsstuden.js,文件,先把info type 写到info.js代码如下

graphql/info.js
import {
  graphql,
  GraphQLSchema,
  GraphQLObjectType,
  GraphQLString,
  GraphQLID,
  GraphQLList,
  GraphQLNonNull,
  isOutputType
} from 'graphql';

import mongoose from 'mongoose'
const Info = mongoose.model('Info')


const objType = new GraphQLObjectType({
  name: 'mete',
  fields: {
    createdAt: {
      type: GraphQLString
    },
    updatedAt: {
      type: GraphQLString
    }
  }
})

export let InfoType = new GraphQLObjectType({
  name: 'Info',
  fields: {
    _id: {
      type: GraphQLID
    },
    height: {
      type: GraphQLString
    },
    weight: {
      type: GraphQLString
    },
    hobby: {
      type: new GraphQLList(GraphQLString)
    },
    meta: {
      type: objType
    }
  }
})


export const infos = {
  type: new GraphQLList(InfoType),
  args: {},
  resolve (root, params, options) {
    return Info.find({}).exec()
  }
}


export const info = {
  type: InfoType,
  args: {
    id: {
      name: 'id',
      type: new GraphQLNonNull(GraphQLID)
    }
  },
  resolve (root, params, options) {
    return Info.findOne({
      _id: params.id
    }).exec()
  }
}

分离好info type 之后,一鼓作气,我们顺便把studen type 也完成一下,代码如下,原理跟info type 都是相通的,

graphql/student.js

import {
  graphql,
  GraphQLSchema,
  GraphQLObjectType,
  GraphQLString,
  GraphQLID,
  GraphQLList,
  GraphQLNonNull,
  isOutputType,
  GraphQLInt
} from 'graphql';

import mongoose from 'mongoose'

import {InfoType} from './info'
const Student = mongoose.model('Student')


let StudentType = new GraphQLObjectType({
  name: 'Student',
  fields: {
    _id: {
      type: GraphQLID
    },
    name: {
      type: GraphQLString
    },
    sex: {
      type: GraphQLString
    },
    age: {
      type: GraphQLInt
    },
    info: {
      type: InfoType
    }
  }
})


export const student = {
  type: new GraphQLList(StudentType),
  args: {},
  resolve (root, params, options) {
    return Student.find({}).populate({
      path: 'info',
      select: 'hobby height weight'
    }).exec()
  }
}

tips: 上面因为有了联表查询,所以引用了info.js

然后调整一下schema.js的代码,如下:


import {
  GraphQLSchema,
  GraphQLObjectType
} from 'graphql';
// 引入 type 
import {info, infos} from './info'
import {student} from './student'

// 建立 schema
export default new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Queries',
    fields: {
      infos,
      info,
      student
    }
  })
})

看到代码是如此的清新脱俗,是不是深感欣慰。好了,graophql数据查询都已经是大概比较完善了。
课程的数据大家可以自己写一下,或者直接到我的github项目里面copy过来我就不一一重复的说了。

下面写一下前端接口是怎么查询的,然后让数据返回浏览器展示到页面的。

前端接口调用

public文件夹下面新建一个index.htmljs文件夹css文件夹,然后在js文件夹建立一个index.js, 在css文件夹建立一个index.css,代码如下

public/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>GraphQL-demo</title>
  <link rel="stylesheet" href="./css/index.css">
</head>
<body>
  <h1 class="app-title">GraphQL-前端demo</h1>
  <div id="app">
    <div class="course list">
      <h3>课程列表</h3>
      <ul id="courseList">
        <li>暂无数据....</li>
      </ul>
    </div>
    <div class="student list">
      <h3>班级学生列表</h3>
      <ul id="studentList">
        <li>暂无数据....</li>
      </ul>
    </div>
  </div>
  <div class="btnbox">
    <div class="btn" id="btn1">点击常规获取课程列表</div>
    <div class="btn" id="btn2">点击常规获取班级学生列表</div>
    <div class="btn" id="btn3">点击graphQL一次获取所有数据,问你怕不怕?</div>
  </div>
  <div class="toast"></div>
  <script data-original="https://cdn.bootcss.com/jquery/1.10.2/jquery.js"></script>
  <script data-original="./js/index.js"></script>
</body>
</html>

我们主要看js请求方式 代码如下


window.onload = function () {

  $('#btn2').click(function() {
    $.ajax({
      url: '/student',
      data: {},
      success:function (res){
        if (res.success) {
          renderStudent (res.data)
        }
      }
    })
  })

  $('#btn1').click(function() {
    $.ajax({
      url: '/course',
      data: {},
      success:function (res){
        if (res.success) {
          renderCourse(res.data)
        }
      }
    })
  })

  function renderStudent (data) {
    var str = ''
    data.forEach(function(item) {
      str += '<li>姓名:'+item.name+',性别:'+item.sex+',年龄:'+item.age+'</li>'
    })
    $('#studentList').html(str)
  }

  function renderCourse (data) {
    var str = ''
    data.forEach(function(item) {
      str += '<li>课程:'+item.title+',简介:'+item.desc+'</li>'
    })
    $('#courseList').html(str)
  }
  
  // 请求看query参数就可以了,跟查询界面的参数差不多

  $('#btn3').click(function() {
    $.ajax({
      url: '/graphql',
      data: {
        query: `query{
          student{
            _id
            name
            sex
            age
          }
          course{
            title
            desc
          }
        }`
      },
      success:function (res){
        renderStudent (res.data.student)
        renderCourse (res.data.course)
      }
    })
  })
}

css的代码 我就不贴出来啦。大家可以去项目直接拿嘛。

所有东西都已经完成之后,重新启动node服务,然后访问,http://localhost:4000/就会看到如下界面。界面丑,没什么设计美化细胞,求轻喷~~~~

操作点击之后就会想第二张图一样了。

所有效果都出来了,本篇文章也就到此结束了。

附上项目地址: https://github.com/naihe138/GraphQL-demo

ps:喜欢的话丢一个小星星(star)给我嘛

查看原文

赞 52 收藏 79 评论 19

callmeDAY 回答了问题 · 2018-01-29

解决谁有自制node前端脚手架完整流程和说明?

去这里看看吧,应该和你的需求对口
http://yeoman.io

关注 8 回答 4

callmeDAY 回答了问题 · 2018-01-23

解决["1","2","3"].map(parseInt),结果为什么是1,NaN,NaN??

相当于这样

[1, 2, 3].map((num, index) => parseInt(num, index))

而parseInt接收的第二个参数会作为将要转换的进制,只接受2到36之间的整数
0估计是布尔为false了,使用默认进制,也就是10进制
1是不被允许的进制,应该是相当于错误去处理了,无论怎么样都会返回NaN
二进制中是没有3的,为NaN

大概是这样,没有详细验证,见谅

关注 6 回答 5

callmeDAY 提出了问题 · 2017-11-10

element-ui v-loading指令使用在组件顶层元素的时候会fullscreen触发

如标题所诉,该如何解决?

<template>
    <div v-loading="loading"> </div>
</template>

就像这样写,如果这个组件挂载在其他组件下,会导致全屏的loading触发。

官网并没有对这个现象有所解释,如果写在template下的顶层元素上的话,就不会触发全屏loading

<template>
    <div> 
        <section v-loading="loading"></section>
    </div>
</template>

这样写就不会触发。

关注 2 回答 1

callmeDAY 回答了问题 · 2017-10-10

解决JS如何每隔3个元素给下一个元素添加一个Class?

    div:nth-child(4n+2){
        background: red;
    }

clipboard.png

选中需要添加样式的所有元素,直接用这个不就好啦

关注 5 回答 4

callmeDAY 回答了问题 · 2017-09-21

用js配合css做出以下的效果!

在password底下放一个形状一模一样的div,然后比password框大一个border的宽度,颜色调成那个动态效果的颜色,然后每次切换的时候把只露出来一点的那个div动态变大变小边长变宽什么的,就写出来了。

这个效果在京东一些活动页经常出现,有心的话就打开控制台看一下就知道了。

关注 12 回答 4

callmeDAY 回答了问题 · 2017-09-19

解决js遍历解析多层级对象数组,如何给每个元素加入层级标识?

    let layer = 0
    function setLayer(obj) {
        obj.layer = layer ++
        if('children' in obj) setLayer(obj['children'])
    }

关注 4 回答 4

callmeDAY 回答了问题 · 2017-09-19

解决npm run dev报错?

有几个找不到的依赖文件,应该是路径配置的问题,是vue的项目吧?你把router里边的的路径配置正确就行了,直接用常规的相对路径配置,不用@索引试试

关注 7 回答 6

认证与成就

  • 获得 12 次点赞
  • 获得 11 枚徽章 获得 0 枚金徽章, 获得 2 枚银徽章, 获得 9 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-05-19
个人主页被 469 人浏览