Leon

Leon 查看完整档案

成都编辑电子科技大学  |  电子信息 编辑  |  填写所在公司/组织 segmentfault.com/u/leon07 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

Leon 关注了用户 · 9月4日

SevenOutman @sevenoutman

有耐心,但气不过不讲道理的事。
GitHub 间歇活跃用户。
赛文奥特曼??

关注 6557

Leon 赞了文章 · 7月24日

Prettier+VScode 治好你的代码洁癖

试想一个多人开发的项目,每次同步代码,看到各个风格迥异,换行空格混乱,4格,2格缩进交替上演的代码文件,分分钟逼死强迫症啊。忍无可忍只能拔枪相见了~~。统一的代码风格规范,对于多人开发的大项目一定是必不可少的,但是口头约定又是一定没有用的(nice脸)。这时候就需要Prettier上场了,一个配置文件,配合VScode,保存即格式化,你的同事都不需要知道代码风格是什么,,整个团队只要clone项目,自动保持统一风格。搞定!

Prettier是什么

Prettier is an opinionated code formatter. It enforces a consistent style by parsing your code and re-printing it with its own rules that take the maximum line length into account, wrapping code when necessary.

以上是github上的官方解释,其实就是简单的代码格式工具,和esLint不同在于,ESLint只是一个代码质量工具(确保没有未使用的变量、没有全局变量,等等)。而Prettier只关心格式化文件(最大长度、混合标签和空格、引用样式等)。可见,代码格式统一的问题,交给Prettirer再合适不过了。和Eslint配合使用,风味更佳。

Prettier怎么用

  • 编辑器插件
  • CLI命令行

编辑器

这里介绍VSCode中如何配置Prettier

  • 安装 Prettier插件 Prettier - Code formatter
    安装插件后,调出系统设置就能看到prettier相关选项了,如图
    图片描述
  • 添加自定义配置文件.prettierrc
    作为项目的整体代码规范,如果依赖本地配置,显然是不科学的,所以需要.prettierrc 文件覆盖本地配置。我的配置文件如下

       {
          "singleQuote": true,
          "trailingComma": "es5",
          "printWidth": 140,
          "overrides": [
            {
              "files": ".prettierrc",
              "options": { "parser": "json" }
            }
          ]
        }
    

到此,prettier安装完毕,使用shift+alt+f就可格式化代码。当然每次手动格式化还是很费力啊,怎么办?自动保存。系统设置中增加"editor.formatOnSave": true即可自动保存,还要注意的一点是,如果同时设置了"files.autoSave": "autoSaveDelay",保存及格式化会失效。files.autoSave配置成别的选项即可。

另外,如果项目配置了.editorConfig文件,在配置了"editor.formatOnSave": true后,如果项目成员没有安装 Prettier 插件,保存时就会读取.editorConfig文件,同样可以格式化代码。启用 Prettier 插件后,.editorConfig的配置就会失效,读取.prettierrc 文件的配置

命令行

命令行用法需要安装prettier,npm install prettier --save-dev,使用prettier write **.js 就可以格式化文件。将添加到你的NPM脚本中,prettier --write './src/**/*.js,这样就可以批量格式化项目中的所有文件了。

结语

prettier只关心代码格式,显然是不够的。项目中还是要引入ESlint。两者配合才能使项目代码优雅健壮

查看原文

赞 17 收藏 11 评论 1

Leon 收藏了文章 · 7月24日

Prettier+VScode 治好你的代码洁癖

试想一个多人开发的项目,每次同步代码,看到各个风格迥异,换行空格混乱,4格,2格缩进交替上演的代码文件,分分钟逼死强迫症啊。忍无可忍只能拔枪相见了~~。统一的代码风格规范,对于多人开发的大项目一定是必不可少的,但是口头约定又是一定没有用的(nice脸)。这时候就需要Prettier上场了,一个配置文件,配合VScode,保存即格式化,你的同事都不需要知道代码风格是什么,,整个团队只要clone项目,自动保持统一风格。搞定!

Prettier是什么

Prettier is an opinionated code formatter. It enforces a consistent style by parsing your code and re-printing it with its own rules that take the maximum line length into account, wrapping code when necessary.

以上是github上的官方解释,其实就是简单的代码格式工具,和esLint不同在于,ESLint只是一个代码质量工具(确保没有未使用的变量、没有全局变量,等等)。而Prettier只关心格式化文件(最大长度、混合标签和空格、引用样式等)。可见,代码格式统一的问题,交给Prettirer再合适不过了。和Eslint配合使用,风味更佳。

Prettier怎么用

  • 编辑器插件
  • CLI命令行

编辑器

这里介绍VSCode中如何配置Prettier

  • 安装 Prettier插件 Prettier - Code formatter
    安装插件后,调出系统设置就能看到prettier相关选项了,如图
    图片描述
  • 添加自定义配置文件.prettierrc
    作为项目的整体代码规范,如果依赖本地配置,显然是不科学的,所以需要.prettierrc 文件覆盖本地配置。我的配置文件如下

       {
          "singleQuote": true,
          "trailingComma": "es5",
          "printWidth": 140,
          "overrides": [
            {
              "files": ".prettierrc",
              "options": { "parser": "json" }
            }
          ]
        }
    

到此,prettier安装完毕,使用shift+alt+f就可格式化代码。当然每次手动格式化还是很费力啊,怎么办?自动保存。系统设置中增加"editor.formatOnSave": true即可自动保存,还要注意的一点是,如果同时设置了"files.autoSave": "autoSaveDelay",保存及格式化会失效。files.autoSave配置成别的选项即可。

另外,如果项目配置了.editorConfig文件,在配置了"editor.formatOnSave": true后,如果项目成员没有安装 Prettier 插件,保存时就会读取.editorConfig文件,同样可以格式化代码。启用 Prettier 插件后,.editorConfig的配置就会失效,读取.prettierrc 文件的配置

命令行

命令行用法需要安装prettier,npm install prettier --save-dev,使用prettier write **.js 就可以格式化文件。将添加到你的NPM脚本中,prettier --write './src/**/*.js,这样就可以批量格式化项目中的所有文件了。

结语

prettier只关心代码格式,显然是不够的。项目中还是要引入ESlint。两者配合才能使项目代码优雅健壮

查看原文

Leon 发布了文章 · 4月24日

基于vue2,eggjs,mysql的个人博客(正在更新)

简单做个博客。
功能点:注册、登录、cookie、权限控制、文章列表、文章详情、文章目录、点赞、评论、分页加载
整体架构:
image.png

前端

使用vue-cli3创建项目

npm install -g @vue/cli
vue create hello-world

为了避开烦人的 eslint,选择了手动选择特性:
image.png
同时没有选择 Linter/Formatter,使用 vscode 中的插件 prettier 和 vetur 配合格式化代码。
image.png

使用axios封装http请求方法

参考了vue中Axios的封装和API接口的管理,对网络请求进行了发封装。
image.png
网络请求统一放在/src/request中,config.js中是基本配置信息:
image.png
http.js中封装请求拦截、响应拦截、错误统一处理。
api.js中是网站所有接口,将其导出后,挂载到 vue.prototype.$api 上,这样全局可以使用 this.$api.xxx 使用接口:
image.png
然后在main.js引入并挂载:
image.png

cookie/session

博客采用cookie/session的方式来记录会话状态,在app.vuecreated生命周期函数中通过checkLogin()接口查询用户登录状态,具体的逻辑为:
当当网流程图3.png

首页分页加载

  let documentEle = document.documentElement
  let needLoadMore =
    documentEle.scrollTop + documentEle.clientHeight + 50 > documentEle.scrollHeight;
  if (needLoadMore && !this.nomore) {
    this.loadingMore = true;
    //暂时不能再滚动加载数据
    this.nomore = true;
    this.loadMoreArticle();
  }

document.documentElement.scrollTop:文档滚动的距离
document.documentElement.clientHeight:文档在可视范围内的高度
document.documentElement.scrollHeight:文档总高度

判断思路是:视口的高度 + 文档的滚动距离 >= 文档总高度,可以预留50的距离做预加载。前端每一次判断触底后,会向后端请求下一页数据,并返回 nomore 字段,表示是否还有未加载文章。这里有个细节是:每一次触底后手动将 nomore 设为 true,后端返回 nomore 后再修改其值,这样做的目的是防止在请求返回前再次发送请求。
参考:搞清clientHeight、offsetHeight、scrollHeight、offsetTop、scrollTop

如何展示博文

没有实际做博客之前,以为每一篇博文都是通过单独写html标签的方式排版的...然后通过别人的项目,发现了正确的做法是:
使用markdown/富文本编辑器编辑文章,生成html片段,在vue中使用v-html语法插入该html片段,文章便以html标签的形式渲染出来可。博客中使用的markdown编辑器是mavonEditor,另外使用highlightjs高亮代码。

<div v-html="articleInfo.contentHtml" class="article-container" ref="content"></div>

提取目录

    extractCatalog() {
      let contentElementRef = Array.from(
        this.$refs.content.querySelectorAll("h1,h2,h3,h4,h5,h6")
      );
      contentElementRef.forEach((item, index) => {
        item.id = item.localName + "-" + index;
        this.catalogList.push({
          tagName: item.localName,
          href: `#${item.localName}-${index}`,
          text: item.innerText
        });
      });
    }

上一小节中,将文章的html片段渲染到了div容器元素中,接下来可以使用querySelectorAll("h1,h2,h3,h4,h5,h6")函数来找出文中所有的标题Dom节点,querySelectorAll()的好处在于它会按照传入参数的顺序进行查找,所以不用担心乱序。获取到目录后对各级目录编号,以便生成锚点进行跳转。

判断对应节是否出现

    watchPageScrollFunc() {
      let contentElementRef = Array.from(
        this.$refs.content.querySelectorAll("h1,h2,h3,h4,h5,h6")
      );
      for (let index = 0; index < contentElementRef.length; index++) {
        const viewPortHeight =
          window.innerHeight ||
          document.documentElement.clientHeight ||
          document.body.clientHeight;
        const elementHeight = contentElementRef[index].clientHeight;
        const el = contentElementRef[index];
        const top =
          el.getBoundingClientRect() &&
          el.getBoundingClientRect().top + elementHeight;
        if (top <= viewPortHeight && top > 0) {
          this.firstVisibleElemetHref = this.catalogList[index].href;
          break;
        }
      }
    }
    
    window.addEventListener("scroll", this.watchPageScrollFunc);

判断方法:元素上边到视口上边的距离 + 元素自身高度 <= 视口高度 && 元素上边到视口上边的距离 + 元素自身高度 > 0
其中,获取元素到视口上边的距离用到:getBoundingClientRect(),某个元素相对于视窗的位置集合。集合中有top, right, bottom, left等属性:

rectObject = object.getBoundingClientRect();

rectObject.top // 元素上边到视窗上边的距离;

rectObject.right // 元素右边到视窗左边的距离;

rectObject.bottom // 元素下边到视窗上边的距离;

rectObject.left // 元素左边到视窗左边的距离;

在整个文章的Dom结构中,使用querySelectorAll("h1,h2,h3,h4,h5,h6")去搜索所有标题类的Dom节点,每一次滚动都去依次动态检查这些标题元素距离视口上方的距离,找到第一个出现在视口中的Dom元素就返回。另外加上节流函数可优化性能。

参考:如何判断元素是否在可视区域ViewPort

评论的实现

评论数据采用的数据结构如下,这里只做到了二级评论,数据结构定下来,具体的实现还是比较简单的。

let comments = [
  {
    author: "admin",
    content: "留言1",
    articleId: "9",
    time: "2020-04-12 10:59",
    id: "0",
    replyList: [
      {
        author: "ghm",
        content: "回复留言1",
        articleId: "9",
        replyTo: "admin",
        time: "2020-04-12 10:59",
        id: "0-0"
      }
    ]
  },
  {
    author: "admin",
    content: "留言2",
    articleId: "9",
    time: "2020-04-12 10:59",
    id: "1",
    replyList: [
      {
        author: "ghm",
        content: "回复留言2",
        articleId: "9",
        replyTo: "admin",
        time: "2020-04-12 11:00",
        id: "1-0"
      }
    ]
  }
];

可添加表情的输入框

第一版的实现方式:在<input>中直接插入emojiunicode,展示效果如下所示:
image.png
这样做的问题在于:展示效果没有图片好,并且在不同的浏览器中会展现出不同的效果。果断换用图片形式展示表情,但是<input>是不能插入<img>标签的,于是参考了掘金的实现方式,使用contenteditable这个css属性将普通Dom元素变为可编辑的Dom元素,这样就可以使用appendChild()的方式将<img>插入,最终的显示效果也是明显优于直接使用emoji的。

  <div
    class="textarea"
    ref="inputContent"
    contenteditable="true"
    autocomplete="off"
    :placeholder="placeholder"
    draggable="false"
    spellcheck="false"
  ></div>

image.png

后端

数据库表的设计

表1:文章列表:
image.png

表2: 文章详情:
image.png

表3:用户详情
image.png

webServer

使用了 eggjs 作为 web服务器,eggjs 的文档很完善,照着文档撸代码就够了。eggjs奉行约定优于配置,有以下优点:

  1. 统一目录结构
  2. 统一分层设计:router-controller-service-model
  3. 全套安全、日志、测试方案
  4. 高扩展性的插件机制

初始化

npm init egg --type=simple
npm i

目录结构

image.png

路由

框架约定在app/router.js文件中统一配置所有路由

// app/router.js
module.exports = app => {
  const { router, controller } = app;
  router.get('/user/:id', controller.user.info);
};

完整的理由定义如下:

router.verb('router-name', 'path-match', middleware1, ..., middlewareN, app.controller.action);

注意事项:

  • 在 Router 定义中, 可以支持多个 Middleware 串联执行
  • Controller 必须定义在 app/controller 目录中。
  • 一个文件里面也可以包含多个 Controller 定义,在定义路由的时候,可以通过 ${fileName}.${functionName} 的方式指定对应的 Controller。
  • Controller 支持子目录,在定义路由的时候,可以通过 ${directoryName}.${fileName}.${functionName} 的方式制定对应的 Controller。

框架中一些常用的路由用法:

  • 获取查询参数

    // curl http://127.0.0.1:7001/search?name=egg
    ctx.query.name
  • 获取路径参数

    // app/router.js
    module.exports = app => {
      app.router.get('/user/:id/:name', app.controller.user.info);
    };
    
    // app/controller/user.js
    exports.info = async ctx => {
      ctx.body = `user: ${ctx.params.id}, ${ctx.params.name}`;
    };
    
    // curl http://127.0.0.1:7001/user/123/xiaoming
    
  • post请求boby的获取

    ctx.request.body
  • 重定向

     // 内部路由重定向
     app.router.redirect('/', '/home/index', 302);
     // 外部路由重定向
     ctx.redirect(\`http://cn.bing.com\`);

控制器(Controller)

简单的说 Controller 负责解析用户的输入,处理后返回相应的结果。框架推荐 Controller 层主要对用户的请求参数进行处理(校验、转换),然后调用对应的service方法处理业务,得到业务结果后封装并返回:

  1. 获取用户通过 HTTP 传递过来的请求参数。
  2. 校验、组装参数。
  3. 调用 Service 进行业务处理,必要时处理转换 Service 的返回结果,让它适应用户的需求。
  4. 通过 HTTP 将结果响应给用户。

定义:
所有的 Controller 文件都必须放在 app/controller 目录下,可以支持多级目录,访问的时候可以通过目录名级联访问。

// app/controller/post.js
const Controller = require('egg').Controller;
class PostController extends Controller {
  async create() {
    const { ctx, service } = this;
    const createRule = {
      title: { type: 'string' },
      content: { type: 'string' },
    };
    // 校验参数
    ctx.validate(createRule);
    // 组装参数
    const author = ctx.session.userId;
    const req = Object.assign(ctx.request.body, { author });
    // 调用 Service 进行业务处理
    const res = await service.post.create(req);
    // 设置响应内容和响应状态码
    ctx.body = { id: res.id };
    ctx.status = 201;
  }
}
module.exports = PostController;

上面定义的PostController的方法可以通过文件名和方法名的方式使用。

// app/router.js
module.exports = app => {
  const { router, controller } = app;
  router.post('createPost', '/api/posts', controller.post.create);
}

定义的 Controller 类,会在每一个请求访问到 server 时实例化一个全新的对象,而项目中的 Controller 类继承于 egg.Controller,会有下面几个属性挂在 this 上:this.ctxthis.appthis.servicethis.configthis.logger

服务(Service)

Service 就是在复杂业务场景下用于做业务逻辑封装的一个抽象层,提供这个抽象有以下几个好处:

  • 保持 Controller 中的逻辑更加简洁。
  • 保持业务逻辑的独立性,抽象出来的 Service 可以被多个 Controller 重复调用。
  • 将逻辑和展现分离,更容易编写测试用例

使用场景:

  • 复杂数据的处理,比如要展现的信息需要从数据库获取,还要经过一定的规则计算,才能返回用户显示。或者计算完成后,更新到数据库。
  • 第三方服务的调用,比如 GitHub 信息获取等。

Service 文件必须放在 app/service 目录,可以支持多级目录,访问的时候可以通过目录名级联访

app/service/biz/user.js => ctx.service.biz.user

由于它继承于 egg.Service,故拥有下列属性方便我们进行开发:this.ctxthis.appthis.servicethis.configthis.logger

MySQL

egg提供了 egg-mysql插件来访问 MySQL 数据库

安装插件:

$ npm i --save egg-mysql

开启插件:

// config/plugin.js
exports.mysql = {
  enable: true,
  package: 'egg-mysql',
};

最后在 config/config.${env}.js 配置各个环境的数据库连接信息

// config/config.${env}.js
exports.mysql = {
  // 单数据库信息配置
  client: {
    // host
    host: 'mysql.com',
    // 端口号
    port: '3306',
    // 用户名
    user: 'test_user',
    // 密码
    password: 'test_password',
    // 数据库名
    database: 'test',
  },
  // 是否加载到 app 上,默认开启
  app: true,
  // 是否加载到 agent 上,默认关闭
  agent: false,
};

Sequelize

Sequelize 是 nodejs 社区广泛使用的 ORM 框架,将关系数据库的表结构映射为对象,让开发者使用 js 语法完成数据库操作。另外Sequelize 提供了 Migrations,帮助开发者管理数据库的每一次变更,因此每一次库表的变动都应通过 Migrations 来实现。

egg 提供了集成 Sequelize 的脚手架:

npm init egg --type=sequelize

上线

配置服务器环境

1.一台linux服务器
作者买的阿里云最便宜的ECS云服务器,配置为1核2G,1M带宽,作为学习使用完全够了,操作系统选择的CentOS。然后参照阿里云给出的教程在服务器上部署NodeJs环境部署mysql
2.购买域名
在阿里云上购买域名后,按照新手引导设置域名解析,同时设置 www 和 @,网站便可通过 www.xxx.com 和 xxx.com 访问。同时,想要使用域名访问网站,还需要对域名进行备案。
3.本地数据库迁移到云服务器
作者使用的数据库可视化工具是navicat,在navicat中对选中的数据库做转储SQL文件处理,再在云服务器的mysql中新建数据库,运行此SQL文件,便完成了数据库的迁移。
image.png

上传文件

1.下载文件传输工具 Xftp
2.部署前端代码

npm run build

打包好后,使用 Xftp 将 dist 文件夹上传到服务器中
3.部署后端代码
将后端代码 git clone 到服务器中

npm i
npm start

配置nginx

1.安装nginx

yum install nginx

安装好的 nginx 会在 /etc/nginx
2.使用 Xftp 修改 nginx 配置
找到/etc/nginx目录下的 nginx.conf文件,使用记事本打开编辑:

user  root;
worker_processes  1;

events {
    worker_connections  1024;
}

http {
  include       mime.types;
  default_type  application/octet-stream;

  sendfile        on;
  keepalive_timeout  65;

  server {
    listen       80;
    server_name  localhost;
    root  /root/project/dt_blog/frontend/dist/;
    index  index.html;
    add_header Access-Control-Allow-Origin *;
      
    location /api {
      proxy_pass  http://127.0.0.1:7001;
      proxy_set_header  HOST $host;
    }
        
    location / {
      try_files $uri $uri/ @router;
      index index.html;
    }

    location @router {
      rewrite ^.*$ /index.html last;
    }
  }
}

其中这两个配置很重要:

location / {
  try_files $uri $uri/ @router;
  index index.html;
}

location @router {
  rewrite ^.*$ /index.html last;
}

网站部署好后,可以正常访问,但是打开二级页面后再刷新,就会404,原因是:v-router设置的路径并不是真实存在的路由,工程中的路由跳转都是通过 js 实现的,而将前端打包好的 dist 目录部署在服务器后,在浏览器中访问根路径会默认打到 index.html 中,但是直接访问其他路径,就没有一个真实的路径与其对应。(参考:https://www.cnblogs.com/kevingrace/p/6126762.html

做好以上配置后,就可以通过 服务器ip:80 来访问部署好的网站了,但是一直通过 ip 地址访问网站不太合理,那么就需要给网站配置域名。

配置域名

查看原文

赞 16 收藏 11 评论 0

Leon 收藏了文章 · 4月6日

如何判断元素是否在可视区域ViewPort

个性签名: 生如夏花,逝如冬雪;人生如此,何悔何怨。

前言: 经常需要计算元素的大小或者所在页面的位置,offsetWidth,clientWidth,scrollWidth,scrollTop这几个关键字的出现更是家常便饭,每次碰到都需要事先实验一番。为了下次开发提高效率。在这里一次性做个总结,以用来判断元素是否在可视区域以及用原生js简单实现懒加载。文末有个简单的懒加载实现的demo,有需要的可以看一下。

目录

工欲善其事,必先利其器。在判断元素是否在可视区域实现简单的原生懒加载前,我们先简单回顾下以下几个关键的概念。

ps: 如果你对这些概念已经比较熟悉了,可以直接跳到第五点查看关键代码示例。

1. 偏移量

偏移量(offset dimension),元素的可见大小由其高度、宽度决定,包括所有内边距、滚动条和边框大小(注意,不包括外边距)。通过下列4个属性可以取得元素的偏移量。

偏移量概念公式
offsetHeight元素在垂直方向上占用的空间大小,以像素计。包括元素的高度、(可见的) 水平滚动条的高度、上边框高度和下边框高度。offsetHeght = content + padding + border + scrollX
offsetWidth元素在水平方向上占用的空间大小,以像素计。包括元素的宽度、(可见的)垂 直滚动条的宽度、左边框宽度和右边框宽度。offsetWidth = content + padding + border + scrollY
offsetLeft元素的左外边框至**包含元素的左内边框之间的像素距离。
offsetTop元素的上外边框至包含元素的上内边框之间的像素距离。

其中,offsetLeft 和 offsetTop 属性与包含元素有关,包含元素的引用保存在 offsetParent 属性中。**offsetParent 属性不一定与 parentNode 的值相等。

如下图显示
偏移量图示

注意: 所有这些偏移量属性都是只读的,而且每次访问它们都需要重新计算。因此,应该尽量避免重复访问这些属性;如果需要重复使用其中某些属性的值,可以将它们保 存在局部变量中,以提高性能。

这也是上篇文章文字跑马灯项目中(戳此跳转),为什么增加padding后,textWidth需要重新获取的原因
文字跑马灯

小结

偏移量: 只读属性;包括滚动条和边框,不包括外边距。

2. 客户区大小

客户区大小是只读的,每次访问都要重新计算的。

客户区大小概念公式
clientWidthclientWidth 属性是元素内容区宽度加 上左右内边距宽度;clientWidth = content + padding
clientHeight元素内容区高度加上上下内边距高度clientHeight = content + padding

最常用到这些属性的情况,就是确定浏览器视口大小的时候(在 IE7 之前的版本中)。如下面的例子所示:

function getViewport(){
    // 检查 document.compatMode 属性,以确定浏览器是否运行在混杂模式。
    // Safari3.1 之前的版本不支持这个属性,因此就会自动执行 else 语句
    if (document.compatMode == "BackCompat"){
        return {
            width: document.body.clientWidth,
            height: document.body.clientHeight
        };
    } else {
        return {
            width: document.documentElement.clientWidth,
            height: document.documentElement.clientHeight
        }; 
    }
}

小结

客户区大小: 只读属性;不包括滚动条和边框,不包括外边距。

3. 滚动大小

概念
scrollHeight在没有滚动条的情况下,元素内容的总高度。
scrollWidth在没有滚动条的情况下,元素内容的总宽度。
scrollLeft被隐藏在内容区域左侧的像素数。通过设置这个属性可以改变元素的滚动位置。
scrollTop被隐藏在内容区域上方的像素数。通过设置这个属性可以改变元素的滚动位置。

scrollWidth 和 scrollHeight 主要用于确定元素内容的实际大小。

scrollLeft 和 scrollTop属性既可以确定元素当前滚动的状态,也可以设置元素的滚动位 置。在元素尚未被滚动时,这两个属性的值都等于 0。如果元素被垂直滚动了,那么 scrollTop 的值 会大于 0,且表示元素上方不可见内容的像素高度。如果元素被水平滚动了,那么 scrollLeft 的值会 大于 0,且表示元素左侧不可见内容的像素宽度。这两个属性都是可以设置的,因此将元素的 scrollLeft 和 scrollTop 设置为 0,就可以重置元素的滚动位置。比如:上篇文章文字跑马灯项目中scrollLeft的使用(戳此跳转)

小结

只读属性,不包括滚动条、border。

4. 确定元素大小

getBoundingClientRect

getBoundingClientRect的兼容性写法:

对于不支持 getBoundingClientRect()的浏览器,可以通过其他手段取得相同的信息。一般来 说,right 和 left 的差值与 offsetWidth 的值相等,而 bottom 和 top 的差值与 offsetHeight 相等。综合上述,就可以创建出下面这个跨浏览器的函数:


function getElementLeft(element){
    var actualLeft = element.offsetLeft;
    var current = element.offsetParent;
    while (current !== null){
        actualLeft += current.offsetLeft;
        current = current.offsetParent;
    }
    return actualLeft;
}

function getElementTop(element){
    var actualTop = element.offsetTop;
    var current = element.offsetParent;
    while (current !== null){
        actualTop += current. offsetTop;
        current = current.offsetParent;
    }
    return actualTop;
}

function getBoundingClientRect(element) {
    var scrollTop = document.documentElement.scrollTop;
    var scrollLeft = document.documentElement.scrollLeft;
    if (element.getBoundingClientRect) {
        if (typeof arguments.callee.offset != "number") {
            var temp = document.createElement("div");
            temp.style.cssText = "position:absolute;left:0;top:0;"; document.body.appendChild(temp);
            arguments.callee.offset = -temp.getBoundingClientRect().top - scrollTop; document.body.removeChild(temp);
            temp = null;
        }
        var rect = element.getBoundingClientRect();
        var offset = arguments.callee.offset;
        return {
            left: rect.left + offset,
            right: rect.right + offset,
            top: rect.top + offset,
            bottom: rect.bottom + offset
        };
    } else {
        var actualLeft = getElementLeft(element);
        var actualTop = getElementTop(element);
        return {
            left: actualLeft - scrollLeft,
            right: actualLeft + element.offsetWidth - scrollLeft,
            top: actualTop - scrollTop,
            bottom: actualTop + element.offsetHeight - scrollTop
        }
    }
}

5.判断元素是否在可视区域

知道了元素的大小以及所位于的区域外,我们可以做些什么呢?我们可以通过上面学到的知识点来检测元素是否在可视区域,再说大一点,这也是懒加载图片的实现原理。

5.1 第一种方法

公式: el.offsetTop - document.documentElement.scrollTop <= viewPortHeight

function isInViewPortOfOne (el) {
    // viewPortHeight 兼容所有浏览器写法
    const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight 
    const offsetTop = el.offsetTop
    const scrollTop = document.documentElement.scrollTop
    const top = offsetTop - scrollTop
    console.log('top', top)
     // 这里有个+100是为了提前加载+ 100
    return top <= viewPortHeight + 100
}

5.2 第二种方法

公式: el.getBoundingClientReact().top <= viewPortHeight

其实, el.offsetTop - document.documentElement.scrollTop = el.getBoundingClientRect().top, 利用这点,我们可以用下面代码代替方法一

function isInViewPortOfTwo (el) {
    const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight 
    const top = el.getBoundingClientRect() && el.getBoundingClientRect().top
    console.log('top', top)
    return top  <= viewPortHeight + 100
}

5.3 第三种方法

公式: intersectionRatio > 0 && intersectionRatio <= 1

// 定义一个交叉观察器
const io = new IntersectionObserver(ioes => {
    ioes.forEach(ioe => {
        const el = ioe.target
        const intersectionRatio = ioe.intersectionRatio
        if (intersectionRatio > 0 && intersectionRatio <= 1) {
            loadImg(el)
            io.unobserve(el)
        }
         el.onload = el.onerror = () => io.unobserve(el)
    })
})
// 执行交叉观察器
function isInViewPortOfThree (el) {
    io.observe(el)
}

5.4 兼容性比较

在兼容性方面,我们知道越原始的方法兼容性是最好的,那么第二种方法和第三种方法能否代替第三种方法呢?我们来看看。

getBoundingClientReact

IntersectionObserver

从caniuse的数据来看,getBoundingClientReact的适配情况很乐观了。

所以,如果在移动端和桌面端都要做兼容适配的话,方法二完全可以代替方法一进行适配了。如果仅仅是桌面端适配(比如运营后台),我们或许可以尝试下新的IntersectionObserver方法,毕竟IntersectionObserver里面还有更多丰富的功能等着我们去体验呢。

5.5 实例

有时,我们希望某些静态资源(比如图片),只有用户向下滚动,它们进入视口时才加载,这样可以节省带宽,提高网页性能。这就叫做"惰性加载",也称为懒加载。

惰性加载预览DEMO(放入你的本地图片即可通过更换不同方法实现懒加载)

------------------------------ 华丽的分割线 ---------------------------

ps: 有公众号大佬想减轻日常写推文的时间消耗,收购技术原创文章的稿子放在自己公众号展示推送的,可以联系找我合作哈。(在公众号里可以查到我的联系方式)

关于我

公众号二维码

参考

查看原文

Leon 赞了文章 · 4月6日

如何判断元素是否在可视区域ViewPort

个性签名: 生如夏花,逝如冬雪;人生如此,何悔何怨。

前言: 经常需要计算元素的大小或者所在页面的位置,offsetWidth,clientWidth,scrollWidth,scrollTop这几个关键字的出现更是家常便饭,每次碰到都需要事先实验一番。为了下次开发提高效率。在这里一次性做个总结,以用来判断元素是否在可视区域以及用原生js简单实现懒加载。文末有个简单的懒加载实现的demo,有需要的可以看一下。

目录

工欲善其事,必先利其器。在判断元素是否在可视区域实现简单的原生懒加载前,我们先简单回顾下以下几个关键的概念。

ps: 如果你对这些概念已经比较熟悉了,可以直接跳到第五点查看关键代码示例。

1. 偏移量

偏移量(offset dimension),元素的可见大小由其高度、宽度决定,包括所有内边距、滚动条和边框大小(注意,不包括外边距)。通过下列4个属性可以取得元素的偏移量。

偏移量概念公式
offsetHeight元素在垂直方向上占用的空间大小,以像素计。包括元素的高度、(可见的) 水平滚动条的高度、上边框高度和下边框高度。offsetHeght = content + padding + border + scrollX
offsetWidth元素在水平方向上占用的空间大小,以像素计。包括元素的宽度、(可见的)垂 直滚动条的宽度、左边框宽度和右边框宽度。offsetWidth = content + padding + border + scrollY
offsetLeft元素的左外边框至**包含元素的左内边框之间的像素距离。
offsetTop元素的上外边框至包含元素的上内边框之间的像素距离。

其中,offsetLeft 和 offsetTop 属性与包含元素有关,包含元素的引用保存在 offsetParent 属性中。**offsetParent 属性不一定与 parentNode 的值相等。

如下图显示
偏移量图示

注意: 所有这些偏移量属性都是只读的,而且每次访问它们都需要重新计算。因此,应该尽量避免重复访问这些属性;如果需要重复使用其中某些属性的值,可以将它们保 存在局部变量中,以提高性能。

这也是上篇文章文字跑马灯项目中(戳此跳转),为什么增加padding后,textWidth需要重新获取的原因
文字跑马灯

小结

偏移量: 只读属性;包括滚动条和边框,不包括外边距。

2. 客户区大小

客户区大小是只读的,每次访问都要重新计算的。

客户区大小概念公式
clientWidthclientWidth 属性是元素内容区宽度加 上左右内边距宽度;clientWidth = content + padding
clientHeight元素内容区高度加上上下内边距高度clientHeight = content + padding

最常用到这些属性的情况,就是确定浏览器视口大小的时候(在 IE7 之前的版本中)。如下面的例子所示:

function getViewport(){
    // 检查 document.compatMode 属性,以确定浏览器是否运行在混杂模式。
    // Safari3.1 之前的版本不支持这个属性,因此就会自动执行 else 语句
    if (document.compatMode == "BackCompat"){
        return {
            width: document.body.clientWidth,
            height: document.body.clientHeight
        };
    } else {
        return {
            width: document.documentElement.clientWidth,
            height: document.documentElement.clientHeight
        }; 
    }
}

小结

客户区大小: 只读属性;不包括滚动条和边框,不包括外边距。

3. 滚动大小

概念
scrollHeight在没有滚动条的情况下,元素内容的总高度。
scrollWidth在没有滚动条的情况下,元素内容的总宽度。
scrollLeft被隐藏在内容区域左侧的像素数。通过设置这个属性可以改变元素的滚动位置。
scrollTop被隐藏在内容区域上方的像素数。通过设置这个属性可以改变元素的滚动位置。

scrollWidth 和 scrollHeight 主要用于确定元素内容的实际大小。

scrollLeft 和 scrollTop属性既可以确定元素当前滚动的状态,也可以设置元素的滚动位 置。在元素尚未被滚动时,这两个属性的值都等于 0。如果元素被垂直滚动了,那么 scrollTop 的值 会大于 0,且表示元素上方不可见内容的像素高度。如果元素被水平滚动了,那么 scrollLeft 的值会 大于 0,且表示元素左侧不可见内容的像素宽度。这两个属性都是可以设置的,因此将元素的 scrollLeft 和 scrollTop 设置为 0,就可以重置元素的滚动位置。比如:上篇文章文字跑马灯项目中scrollLeft的使用(戳此跳转)

小结

只读属性,不包括滚动条、border。

4. 确定元素大小

getBoundingClientRect

getBoundingClientRect的兼容性写法:

对于不支持 getBoundingClientRect()的浏览器,可以通过其他手段取得相同的信息。一般来 说,right 和 left 的差值与 offsetWidth 的值相等,而 bottom 和 top 的差值与 offsetHeight 相等。综合上述,就可以创建出下面这个跨浏览器的函数:


function getElementLeft(element){
    var actualLeft = element.offsetLeft;
    var current = element.offsetParent;
    while (current !== null){
        actualLeft += current.offsetLeft;
        current = current.offsetParent;
    }
    return actualLeft;
}

function getElementTop(element){
    var actualTop = element.offsetTop;
    var current = element.offsetParent;
    while (current !== null){
        actualTop += current. offsetTop;
        current = current.offsetParent;
    }
    return actualTop;
}

function getBoundingClientRect(element) {
    var scrollTop = document.documentElement.scrollTop;
    var scrollLeft = document.documentElement.scrollLeft;
    if (element.getBoundingClientRect) {
        if (typeof arguments.callee.offset != "number") {
            var temp = document.createElement("div");
            temp.style.cssText = "position:absolute;left:0;top:0;"; document.body.appendChild(temp);
            arguments.callee.offset = -temp.getBoundingClientRect().top - scrollTop; document.body.removeChild(temp);
            temp = null;
        }
        var rect = element.getBoundingClientRect();
        var offset = arguments.callee.offset;
        return {
            left: rect.left + offset,
            right: rect.right + offset,
            top: rect.top + offset,
            bottom: rect.bottom + offset
        };
    } else {
        var actualLeft = getElementLeft(element);
        var actualTop = getElementTop(element);
        return {
            left: actualLeft - scrollLeft,
            right: actualLeft + element.offsetWidth - scrollLeft,
            top: actualTop - scrollTop,
            bottom: actualTop + element.offsetHeight - scrollTop
        }
    }
}

5.判断元素是否在可视区域

知道了元素的大小以及所位于的区域外,我们可以做些什么呢?我们可以通过上面学到的知识点来检测元素是否在可视区域,再说大一点,这也是懒加载图片的实现原理。

5.1 第一种方法

公式: el.offsetTop - document.documentElement.scrollTop <= viewPortHeight

function isInViewPortOfOne (el) {
    // viewPortHeight 兼容所有浏览器写法
    const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight 
    const offsetTop = el.offsetTop
    const scrollTop = document.documentElement.scrollTop
    const top = offsetTop - scrollTop
    console.log('top', top)
     // 这里有个+100是为了提前加载+ 100
    return top <= viewPortHeight + 100
}

5.2 第二种方法

公式: el.getBoundingClientReact().top <= viewPortHeight

其实, el.offsetTop - document.documentElement.scrollTop = el.getBoundingClientRect().top, 利用这点,我们可以用下面代码代替方法一

function isInViewPortOfTwo (el) {
    const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight 
    const top = el.getBoundingClientRect() && el.getBoundingClientRect().top
    console.log('top', top)
    return top  <= viewPortHeight + 100
}

5.3 第三种方法

公式: intersectionRatio > 0 && intersectionRatio <= 1

// 定义一个交叉观察器
const io = new IntersectionObserver(ioes => {
    ioes.forEach(ioe => {
        const el = ioe.target
        const intersectionRatio = ioe.intersectionRatio
        if (intersectionRatio > 0 && intersectionRatio <= 1) {
            loadImg(el)
            io.unobserve(el)
        }
         el.onload = el.onerror = () => io.unobserve(el)
    })
})
// 执行交叉观察器
function isInViewPortOfThree (el) {
    io.observe(el)
}

5.4 兼容性比较

在兼容性方面,我们知道越原始的方法兼容性是最好的,那么第二种方法和第三种方法能否代替第三种方法呢?我们来看看。

getBoundingClientReact

IntersectionObserver

从caniuse的数据来看,getBoundingClientReact的适配情况很乐观了。

所以,如果在移动端和桌面端都要做兼容适配的话,方法二完全可以代替方法一进行适配了。如果仅仅是桌面端适配(比如运营后台),我们或许可以尝试下新的IntersectionObserver方法,毕竟IntersectionObserver里面还有更多丰富的功能等着我们去体验呢。

5.5 实例

有时,我们希望某些静态资源(比如图片),只有用户向下滚动,它们进入视口时才加载,这样可以节省带宽,提高网页性能。这就叫做"惰性加载",也称为懒加载。

惰性加载预览DEMO(放入你的本地图片即可通过更换不同方法实现懒加载)

------------------------------ 华丽的分割线 ---------------------------

ps: 有公众号大佬想减轻日常写推文的时间消耗,收购技术原创文章的稿子放在自己公众号展示推送的,可以联系找我合作哈。(在公众号里可以查到我的联系方式)

关于我

公众号二维码

参考

查看原文

赞 43 收藏 36 评论 1

Leon 收藏了文章 · 4月6日

scrollHeight, clientHeight, offsetHeight的区别

浏览器窗口和网页文档

先明确浏览器窗口和网页文档的区别,拿下面这张图来说

clipboard.png

右边那张图中,大红色方框框起来的是浏览器窗口,而网页文档就是左边这张图。先不用去管scrollHeight这些东西,后面再解释。
先明确浏览器窗口网页文档是不同的!!不用去纠结它们什么时候高度相等,明白这两个代表的含义才是最重要的。

浏览器窗口

宽:window.innerWidth
高:window.innerHeight

一些注意点:

  1. 无论是否出现滚动条,这两个值都是不变的。
  2. 当调整浏览器大小时,这两个值会变。

简而言之:就是你可以看到的浏览器视窗的大小(不包括顶部的菜单栏)

有小伙伴会问了,那window.outerWidth是和outerHeight是什么呢,这两个就是包含菜单栏的,比如你可以在chrome里按下F12打开调试窗口,放在右侧,就可以发现innerWidth和outerWidth是不同的。

网页文档

宽:document.body.scrollWidth
高:document.body.scrollHeight

好了既然这里讲到scrollHeight了,那刚好把clientHeight和offsetHeight讲了。
要比较这三个属性的不同,有个前提条件,就是元素要出现滚动条。举个栗子就是:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        .container {
            width: 600px;
            height: 600px;
            padding: 10px;
            border: 10px solid lightgray;
            overflow: auto; // 注意这个属性
        }
        .large_block {
            width: 1000px;
            height: 2000px;
            background-color: lightblue;
            padding: 20px;
            margin: 20px;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="large_block"></div>
    </div>
</body>
</html>

如图所示,大家也可以拷贝代码自己看效果.

clipboard.png

对于以上代码,分别获取:

clipboard.png

对于这三个属性,还是拿这张图来说:

clipboard.png

scrollHeight: 就是container内部的总高度
这里内部元素就是large_block,large_block所撑开的高度(2000 + 40(上下padding) + 40(上下margin)) = 2080px,然后加上自身container上下各10px的padding,因此一共是2100px

clientHeight: 就是container内部可见高度 + 自身padding。
内部可见高度为600 - 17(滚动条高度)
padding为上下各10,因此一共是600 - 17 + 20 = 603

offsetHeight: 也是container自己本身的可见高度 + 自身padding + 自身border + 滚动条
与clientHeight不同的就是要加上自身border以及滚动条的高度,因此是603 + 20 + 17 = 640

写在最后

有问题的话欢迎讨论~一起进步

查看原文

Leon 赞了文章 · 4月6日

scrollHeight, clientHeight, offsetHeight的区别

浏览器窗口和网页文档

先明确浏览器窗口和网页文档的区别,拿下面这张图来说

clipboard.png

右边那张图中,大红色方框框起来的是浏览器窗口,而网页文档就是左边这张图。先不用去管scrollHeight这些东西,后面再解释。
先明确浏览器窗口网页文档是不同的!!不用去纠结它们什么时候高度相等,明白这两个代表的含义才是最重要的。

浏览器窗口

宽:window.innerWidth
高:window.innerHeight

一些注意点:

  1. 无论是否出现滚动条,这两个值都是不变的。
  2. 当调整浏览器大小时,这两个值会变。

简而言之:就是你可以看到的浏览器视窗的大小(不包括顶部的菜单栏)

有小伙伴会问了,那window.outerWidth是和outerHeight是什么呢,这两个就是包含菜单栏的,比如你可以在chrome里按下F12打开调试窗口,放在右侧,就可以发现innerWidth和outerWidth是不同的。

网页文档

宽:document.body.scrollWidth
高:document.body.scrollHeight

好了既然这里讲到scrollHeight了,那刚好把clientHeight和offsetHeight讲了。
要比较这三个属性的不同,有个前提条件,就是元素要出现滚动条。举个栗子就是:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        .container {
            width: 600px;
            height: 600px;
            padding: 10px;
            border: 10px solid lightgray;
            overflow: auto; // 注意这个属性
        }
        .large_block {
            width: 1000px;
            height: 2000px;
            background-color: lightblue;
            padding: 20px;
            margin: 20px;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="large_block"></div>
    </div>
</body>
</html>

如图所示,大家也可以拷贝代码自己看效果.

clipboard.png

对于以上代码,分别获取:

clipboard.png

对于这三个属性,还是拿这张图来说:

clipboard.png

scrollHeight: 就是container内部的总高度
这里内部元素就是large_block,large_block所撑开的高度(2000 + 40(上下padding) + 40(上下margin)) = 2080px,然后加上自身container上下各10px的padding,因此一共是2100px

clientHeight: 就是container内部可见高度 + 自身padding。
内部可见高度为600 - 17(滚动条高度)
padding为上下各10,因此一共是600 - 17 + 20 = 603

offsetHeight: 也是container自己本身的可见高度 + 自身padding + 自身border + 滚动条
与clientHeight不同的就是要加上自身border以及滚动条的高度,因此是603 + 20 + 17 = 640

写在最后

有问题的话欢迎讨论~一起进步

查看原文

赞 40 收藏 30 评论 2

Leon 赞了问题 · 3月18日

解决vue如何监听浏览器刷新事件呢?不仅仅是页面销毁就触发的事件。

目前需求是需要在页面刷新时某个组件记录一个sessionStorage状态,而在进行路由跳转的条件下不记录这个状态。
因此destroyed以及beforeDestroy都不太适用。
网上搜索到的onbeforeunload方法不太清楚在改在哪里使用,尝试在beforeDestroy的时候调用,但是并没有触发方法,
Vue里面该用什么钩子函数去执行onbeforeunload方法呢?或者Vue有什么更适合的方法监听浏览器刷新事件呢?

关注 3 回答 1

Leon 赞了问题 · 3月18日

解决Vue 刷新页面时会触发事件吗

刚学习vue不久,按照网上的demo做了一个简单的note,使用localstorage做本地存储,然后我想在刷新页面或者离开页面的调用localstorage方法,请问这样做能实现吗?如果可以,要怎么做?谢谢回答,感激不尽!

关注 7 回答 3

认证与成就

  • 获得 685 次点赞
  • 获得 5 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 5 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-07-05
个人主页被 1.7k 人浏览