JavaScript系列vue项目中实现滚动条具体视窗口的滚动条操作1置底2置于上次停留的位置

一、前言之前写了一个happyChat的项目,主要是想学习一下socketIO的使用。然后最近在给happyChat做前端优化和升级。发现第一版做的很low。 需要优化的问题: 1、问题1:滚动条会出现在头部和底部的视窗中,之前固定头部和固定底部是使用`position:fixed`。解决方案:body和html禁止`overflow:hidden`,头部和底部是`position:relative`,聊天视窗使用`overflow: auto`,这样聊天视窗才会出现滚动条。滚动条不会出现在头部和底部视窗中。2、问题2:聊天视窗一次性加载了所有的聊天内容,如果数据太多会出现超时的问题。解决方案:聊天内容查询使用分页查询,一次性查询20条或者30条。3、问题3:群聊的时候,找不到群成员列表。解决方案:做一个群信息汇总的组件,包括群成员列表。新增功能:(1)滚动条置底;(2)分页加载的时候,保持滚动条置于上次停留的位置。项目【前端】源代码地址:https://github.com/saucxs/hap... 项目【后端】端源代码地址:https://github.com/saucxs/hap... 欢迎fork和start。 线上地址:http://chat.chengxinsong.cn/ 二、具体【视窗口】的滚动条的问题环境:vue 我们先来看一下,给滚动条添加监听事件,必须在created或者mouted周期中,添加scroll的监听事件。 mounted: function () { window.addEventListener('scroll', this.handleScroll, true); // 监听(绑定)滚轮滚动事件 },其中handleScroll事件是methods里的事件: handleScroll() { this.viewBox = this.$refs.viewBox; console.log(this.viewBox.scrollTop, '到头部的距离-------------------') console.log(this.viewBox.scrollHeight, '滚动条的总高度-------------------') }注意:div 到头部的距离 + 屏幕高度 = 可滚动的总高度this.$refs.viewBox取到视窗口的dom对象,必须确定dom加载完毕了。 <ul ref="viewBox"> <li v-for="item in message"> <ChatItem v-if="userInfo.user_id === item.from_user" :href="item.from_user" :img="item.avator" me="true" :msg="item.message" :name="item.name" :time="item.time"></ChatItem> <ChatItem v-else :img="item.avator" :msg="item.message" :href="item.from_user" :name="item.name" :time="item.time"></ChatItem> </li> </ul>如果我们想置底,我们必须将【滚动条总高度】赋值给【滚动条到头部的距离】。这样就置底了。 业务是这样的:(1)当进入到聊天页面时候,这时候置底。(2)如果翻看之前的聊天记录,这时候就不需要置底,保持在当前的位置。 1、先解决【进入到聊天页面时候,这时候置底】的问题解决方案:(1)全局设置一个标志位,如果是第一次进入,【标志位为置底】,执行置底方法。(2)如果是翻看之前聊天记录,【标志位改为不置底】,不执行置底方法。(3)监听内容变化方法,判断标志位是啥,然后看是否执行置底方法。 watch: { message() { this.viewBox = this.$refs.viewBox if(this.type == 'bottom'){ this.refresh(); } }},refresh就是一个置底方法。 ...

July 5, 2019 · 1 min · jiezi

Elementui-elscrollbar-源码解析

前几天美化博客时发现滚动条在window下实在太难看,所以在基于vue的技术上寻找美化滚动条的方法。记得Element-ui源码中有名为 el-scrollbar 的滚动组件,虽然文档上没有提到,但使用的人还是不少。今天记录下源码的阅读心得。 在这之前在看苦涩的代码前,先大概描述一下滚动条组件的用处和行为,方便理解代码逻辑。 因为操作系统和浏览器的不同,滚动条外观是不一样的。需要做风格统一时,就需要做自定义滚动条。当然也可以直接修改CSS3中的 ::-webkit-scrollbar 相关属性来达到修改原生滚动条外观,但这个属性部分浏览器上没有能够完美兼容。 在一个固定高度的元素中,如内部内容超出了父级元素的固定高。为了让用户浏览其余的内容,通常都会设置父级元素overflow-y: scroll 出现滚动条。允许用户以滚动的形式来浏览剩下的内容。 而自定义滚动条,是先通过偏移视图元素,达到隐藏原生滚动条的效果。同时在视图元素的右侧和下方,增加用标签写出的模拟滚动条。监听模拟滚动条的事件(按下滑块或点击轨道),来动态更新视图窗口的scrollTop或scrollLeft值。同样的,也会监听视图窗口的事件(滚动事件或视图窗口的尺寸缩放事件),来同步更新自定义滚动条的状态(滑块所处的位置或滑块长度)。 滚动条其实是当前已浏览内容的一个直观展示,在固定元素中,如果scrollTop发生改变往下滚动。滚动条中的滑块也会向下移动。此时能够通过滚动条来得知内容的已滚动程度和剩余程度。 我们将页面想象成一个很长的画布,而我们能看到的是一个移动的窗口。当页面往下滚动时,窗口在画布中也就往下移动,来查看被遮挡的内容。同样的,滚动块里的滑块也往下移动同样比例的距离。所以滚动条就是一个等比例的缩小模型。 也就是说,固定元素的高度clientHeight 除以 固定元素包括溢出的总高度scrollHeight。同等于 滑块的高度 除以 滚动条的高度。他们的比例是一样的。 在大概了解滚动条的工作内容和计算公式后,看看源码中是如何处理他们之间的计算关系的。 文件scrollbar组件在 package/scrollbar/index.js 中被导出,其中 package/scrollbar/src 是代码的核心部分,入口文件是 main.js。 结构<el-scrollbar> <div style="height: 300px;"> <div style="height: 600px;"></div> </div></el-scrollbar>使用自定义标签 el-scrollbar 裹住使用的区域,scrollbar 组件会生成view 和 wrap 两个父级元素包裹插槽中的内容,还有两种类型的自定义滚动条 horizontal 和 vertical。 main.jsmain.js默认导出一个对象,接收一系列配置。 name: 'ElScrollbar',components: { // 滚动条组件,拥有水平与垂直两种形态 Bar },props: { native: Boolean, // 是否使用原生滚动条,即不附加自定义滚动条 wrapStyle: {}, // wrap的内联样式 wrapClass: {}, // wrap的样式名 viewClass: {}, // view的样式名 viewStyle: {}, // view的内联样式 noresize: Boolean, // 当container尺寸发生变化时,自动更新滚动条组件的状态 tag: { // 组件最外层的标签属性,默认为 div type: String, default: 'div' }},data() { return { sizeWidth: '0', // 水平滚动条的宽度 sizeHeight: '0', // 垂直滚动条的高度 moveX: 0, // 垂直滚动条的移动比例 moveY: 0 // 水平滚动条的移动比例 };},组件在render函数中生成结构。 ...

May 1, 2019 · 7 min · jiezi

用Class写一个记住用户离开位置的js插件

前言常见的js插件都很少使用ES6的class,一般都是通过构造函数,而且常常是手写CMD、AMD规范来封装一个库,比如这样:// 引用自:https://www.jianshu.com/p/e65c246beac1;(function(undefined) { “use strict” var _global; var plugin = { // … } _global = (function(){ return this || (0, eval)(’this’); }()); if (typeof module !== “undefined” && module.exports) { module.exports = plugin; } else if (typeof define === “function” && define.amd) { define(function(){return plugin;}); } else { !(‘plugin’ in _global) && (_global.plugin = plugin); }}());但现在都9102年了,是时候祭出我们的ES6大法了,可以用更优雅的的写法来实现一个库,比如这样:class RememberScroll { constructor(options) { … }}export default RememberScroll在这篇文章,博主主要通过分享最近自己写的一个记住页面滚动位置小插件,讲一下如何用class语法配合webpack 4.x和babel 7.x封装一个可用的库。项目地址:Github, 在线Demo:Demo喜欢的朋友希望能点个Star收藏一下,非常感谢。需求来源相信很多同学都会遇到这样一个需求:用户浏览一个页面并离开后,再次打开时需要重新定位到上一次离开的位置。这个需求很常见,我们平时在手机上阅读微信公众号的文章页面就有这个功能。想要做到这个需求,也比较好实现,但博主有点懒,心想有没有现成的库可以直接用呢?于是去GitHub上搜了一波,发现并没有很好的且符合我需求的,于是得自己实现一下。为了灵活使用(只是部分页面需要这个功能),博主在项目中单独封装了这个库,本来是在公司项目中用的,后来想想何不开源出来呢?于是有了这个分享,这也是对自己工作的一个总结。预期效果博主喜欢在做一件事情前先yy一下预期的效果。博主希望这个库用起来尽量简单,最好是插入一句代码就可以了,比如这样:<html><head> <meta charset=“utf-8”> <title>remember-scroll examples</title></head><body> <div id=“content”></div> <script src="../dist/remember-scroll.js"></script> <script> new RememberScroll() </script></body></html>在想要加上记住用户浏览位置的页面上引入一下库,然后new RememberScroll()初始化一下即可。下面就带着这个目标,一步一步去实现啦。设计方案1. 需要存哪些信息?用户浏览页面的位置,主要需要存两个字段:哪个页面和离开时的位置,通过这两个字段,我们才可以在用户第二次打开网站的页面时,命中该页面,并自动跳转到上一次离开的位置。2.存在哪?记住浏览位置,需要将用户离开前的浏览位置记录在客户端的浏览器中。这些信息可以主要存放在:cookie、sessionStorage、localStorage中。存放在cookie,大小4K,空间虽有限但也勉强可以。但cookie是每次请求服务器时都会携带上的,无形中增加了带宽和服务器压力,所以总体来说是不太合适的。存放在sessionStorage中,由于仅在当前会话下有效,用户离开页面sessionStorage就会被清除,所以不能满足我们的需求。存放在localStorage,浏览器可永久保存,大小一般限制5M,满足我们需求。综上,最后我们应该选择localStorage。3. 需注意的问题一个站点可能有很多页面,如何标识是哪个页面呢?一般来说可以用页面的url作为页面的唯一标识,比如:www.xx.com/article/${id},不同的id对应不同的页面。但博主考虑到现在很多站点都是用spa了,而且常见在url后面会带有#xxx的哈希值,如www.xx.com/article/${id}#tag1和www.xx.com/article/${id}#tag2这种情况,这可能表示的是同一个页面的不同锚点,所以用url作为页面的唯一标识不太可靠。因此,博主决定将这个页面唯一标识作为一个参数来让使用者来决定,姑且命名为pageKey,让使用者保证是全站唯一的即可。如果用户访问我们的站点中很多很多的页面,由于localStorage是永久保存的,如何避免localStorage不断累积占用过大?我们的需求可能仅仅是想近期记住即可,即只需要记住用户的浏览位置几天,可能会更希望我们存的数据能够自动过期。但localStorage自身是没有自动过期机制的,一般只能在存数据的时候同时存一下时间戳,然后在使用时判断是否过期。如果只能是在使用时才判断是否清除,而新访问页面时又会生成新的记录,localStorage中始终都会存在至少一条记录的,也就是说无法真正实现自动过期。这里不禁就觉得有点多余了,既然都是会一直保留记录在localStorage中,那干脆就不判断了,咱换一个思路:只记录有限的最新页面数量。举个例子:咱们网站有个文章页:www.xx.com/articles/${id},每个的id表示不同的文章,咱们只记录用户最新访问的5篇文章,即维护一个长度为5的队列。比如当前网站有id从1到100篇文章,用户分别访问第1,2,3,4,5篇文章时,这5篇文章都会记录离开的位置,而当用户打开第六篇文章时,第六条记录入队的同时第一条记录出队,此时localStorage中记录的是2,3,4,5,6这几篇文章的位置,这就保证了localStorage永远不会累积存储数据且旧记录会随着不断访问新页面自动“过期”。为了更灵活一点,博主决定给这个插件添加一个maxLength的参数,表示当前站点下记录的最新的页面最大数量,默认值设为5,如果有小伙伴的需求是记录更多的页面,可以通过这个参数来设置。4. 实现思路我们需要时刻监听用户浏览页面时的滚动条的位置,可以通过window.onscroll事件,获得当前的滚动条位置:scrollTop 。将scrollTop和页面唯一标识pageKey存进localStorage中。用户再次打开之前访问过的页面,在页面初始化时,读取localStorage中的数据,判断页面的pageKey是否一致,若一致则将页面的滚动条位置自动滚动到相应的scrollTop值。是不是很简单?不过实现的过程中需要注意一下细节,比如做一下防抖处理。实现步骤逼逼了这么久,是时候开始撸代码了。1.封装localStorage工具方法工欲善其事,必先利其器。为更好服务接下来的工作,咱们先简单封装一下调用localStorage的几个方法,主要是get,set,remove:// storage.jsconst Storage = { isSupport () { if (window.localStorage) { return true } else { console.error(‘Your browser cannot support localStorage!’) return false } }, get (key) { if (!this.isSupport) { return } const data = window.localStorage.getItem(key) return data ? JSON.parse(data) : undefined }, remove (key) { if (!this.isSupport) { return } window.localStorage.removeItem(key) }, set (key, data) { if (!this.isSupport) { return } const newData = JSON.stringify(data) window.localStorage.setItem(key, newData) }}export default Storage2. class大法class即类,本质上虽然是一个function,但使用class定义一个类会更直观。咱们为即将写的库起个名字为RememberScroll,开始就是如下的样子啦:import Storage from ‘./storage’class RememberScroll { constructor() { }}1.处理传进来的参数我们需要在类的构造函数constructor中接收参数,并覆盖默认参数。还记得上面咱们预期的用法吗?即new RememberScroll({pageKey: ‘myPage’, maxLength: 10})。 constructor (options) { let defaultOptions = { pageKey: ‘_page1’, // 当前页面的唯一标识 maxLength: 5 } this.options = Object.assign({}, defaultOptions, options)}如果没有传参数,就会使用默认的参数,如果传了参数,就使用传进来的参数。this.options就是最终处理后的参数啦。2.页面初始化当页面初始化时,咱们需要做三件事情:从loaclStorage取出缓存列表将滚动条滚动到记录的位置(若有记录的话);注册window.onscroll事件监听用户滚动行为;因此,需要在构造函数中就执行initScroll和addScrollEvent这两个方法:import Storage from ‘./utils/storage’class RememberScroll { constructor (options) { // … this.storageKey = ‘_rememberScroll’ this.list = Storage.get(this.storageKey) || [] this.initScroll() this.addScrollEvent() } initScroll () { // … } addScrollEvent () { // … }}这里咱们将localStorage中的键名命名为_rememberScroll,应该能够尽量避免和平常站点使用localStorage的键名冲突。3.监听滚动事件:addScrollEvent()的实现 addScrollEvent () { window.onscroll = () => { // 获取最新的位置,只记录垂直方向的位置 const scrollTop = document.documentElement.scrollTop || document.body.scrollTop // 构造当前页面的数据对象 const data = { pageKey: this.options.pageKey, y: scrollTop } let index = this.list.findIndex(item => item.pageKey === data.pageKey) if (index >= 0) { // 之前缓存过该页面,则替换掉之前的记录 this.list.splice(index, 1, data) } else { // 如果已经超出长度了,则清除一条最早的记录 if (this.list.length >= this.options.maxLength) { this.list.shift() } this.list.push(data) } // 更新localStorage里面的记录 Storage.set(this.storageKey, this.list) } }ps:这里最好需要做一下防抖处理4.初始化滚动条位置: initScroll()的实现initScroll () { // 先判断是否有记录 if (this.list.length) { // 当前页面pageKey是否一致 let currentPage = this.list.find(item => item.pageKey === this.options.pageKey) if (currentPage) { setTimeout(() => { // 一致,则滚动到对应的y值 window.scrollTo(0, currentPage.y) }, 0) }}细心的同学可能会发现,这里用了setTimeout,而不是直接调用window.scrollTo。这是因为博主在这里遇到坑了,这里涉及到页面加载执行顺序的问题。在执行window.scrollTo前,页面必须是已经加载完成了的,滚动条要已存在才可以滚动对吧。如果页面加载时直接执行,当时的scroll高度可能为0,window.scrollTo执行就会无效。如果页面的数据是异步获取的,也会导致window.scrollTo无效。因此用setTimeout会是比较稳的一个办法。5.将模块export出去最后我们需要将模块export出去,整体代码大概是这个样子:import Storage from ‘./utils/storage’class RememberScroll { constructor (options) { let defaultOptions = { pageKey: ‘_page1’, // 当前页面的唯一标识 maxLength: 5 } this.storageKey = ‘_rememberScroll’ // 参数 this.options = Object.assign({}, defaultOptions, options) // 缓存列表 this.list = Storage.get(this.storageKey) || [] this.initScroll() this.addScrollEvent() } initScroll () { // … } addScrollEvent () { // … }}export default RememberScroll这样就基本完成整个插件的功能啦,是不是很简单哈哈。篇幅原因就不贴具体代码了,可以直接到GitHub上看:remember-scroll打包接下来应该是本文的重点了,首先要清楚为什么要打包?将项目中所用到的js文件合并,只对外输出一个js文件。使项目同时支持AMD,CMD、浏览器<script>标签引入,即umd规范。配合babel,将es6语法转为es5语法,兼容低版本浏览器。PS: 由于webpack和babel更新速度很快,网上很多教程可能早已过时,现在(2019-03)的版本已经是babel 7.3.0,webpack 4.29.6, 本篇文章只分享现在的最新的配置方法,因此本篇文章也是会过时的,读者们请注意版本号。npm init项目咱们先新建一个目录,这里名为:remember-scroll,然后将上面写好的remember-scroll.js放进remember-scroll/src/目录下。PS:一般项目的资源文件都放在src目录下,为了显得专业点,最好将remember-scroll.js改名为index.js。)此时项目还没有package.json文件,因此在根目录执行命令初始化package.json:npm init需要根据提示填写一些项目相关信息。安装webpack和webpack-cli运行webpack命令时需要同时装上webpack-cli:npm i webpack webpack-cli -D配置webpack.config.js在根目录中添加一个webpack.config.js,按照webpack官网的示例代码配置:const path = require(‘path’);module.exports = { entry: ‘./src/index.js’, output: { path: path.resolve(__dirname, ‘dist’), filename: ‘remember-scroll.js’ // 修改下输出的名称 }};然后在package.json的script中配置运行webpack的命令: “scripts”: { “test”: “echo "Error: no test specified" && exit 1”, “dev”: “webpack –mode=development –colors” },这样配置完成,在根目录运行npm run dev,会自动生成dist/remember-scroll.js。此时已经实现了我们的第一个小目标:赚它一个亿,哦不,是将storage.js和index.js合并输出为一个remember-scroll.js。这种简单的打包可以称为:非模块化打包。由于我们在js文件中没有通过AMD的return或者CommonJS的exports或者this导出模块本身,导致模块被引入的时候只能执行代码而无法将模块引入后赋值给其它模块使用。支持umd规范相信很多同学都听过AMD,CommonJS规范了,不清楚的同学可以看看阮一峰老师的介绍:Javascript模块化编程(二):AMD规范。为了让我们的插件同时支持AMD,CommonJS,所以需要将我们的插件打包为umd通用模块。之前看过一篇文章:如何定义一个高逼格的原生JS插件,在没有使用webpack打包时,需要在插件中手写支持这些模块化的代码:// 引用自:https://www.jianshu.com/p/e65c246beac1;(function(undefined) { “use strict” var _global; var plugin = { // … } // 最后将插件对象暴露给全局对象 _global = (function(){ return this || (0, eval)(’this’); }()); if (typeof module !== “undefined” && module.exports) { module.exports = plugin; } else if (typeof define === “function” && define.amd) { define(function(){return plugin;}); } else { !(‘plugin’ in _global) && (_global.plugin = plugin); }}());博主看到这坨东西,也是有点晕,不得不佩服大佬就是大佬。还好现在有了webpack,我们现在只需要写好主体关键代码,webpack会帮我们处理好这些打包的问题。在webpack4中,我们可以将js打包为一个库的形式,详情可看:[Webpack Expose the Library](https://webpack.js.org/guides…。在我们这里只需在output中加上library属性:const path = require(‘path’);module.exports = { entry: ‘./src/index.js’, output: { path: path.resolve(__dirname, ‘dist’), filename: ‘remember-scroll.js’, library: ‘RememberScroll’, libraryTarget: ‘umd’, libraryExport: ‘default’ }};注意libraryTarget为umd,就是我们要打包的目标规范为umd。当我们在html中通过script标签引入这个js时,会在window下注册RememberScroll这个变量(类似引入jQuery时会在全局注册$这个变量)。此时就直接使用RememberScroll这个变量了。<script src="../dist/remember-scroll.js"></script><script> console.log(RememberScroll)</script>这里有个坑需要注意一下,如果没有加上libraryExport: ‘default’,由于我们代码中是export default RememberScroll,打包出来的代码会类似:{ ‘default’: { initScroll () {} }}而我们期望的是这样:{ initScroll () {}}即我们希望的是直接输出default中的内容,而不是隔着一层default。所以这里还要加上libraryExport: ‘default’,打包时只输出default的内容。PS: webpack英文文档看得有点懵逼,这个坑让博主折腾了很久才爬起来,所以特别讲下。刚兴趣的同学可以看下文档:output.libraryExport。到这里,已经实现了我们的第二个小目标:支持umd规范。使用babel-loader上面我们打包出来的js,其实已经可以正常运行在支持es6语法的浏览器中了,比如chrome。但想要运行在IE10,IE11中,还得让神器Babel帮我们一把。PS: 虽然很多人说不考虑兼容IE了,但作为一个通用性的库,古董级的IE7,8,9可以不兼容,但较新版本的IE10,11还是需要兼容一下的。Babel是一个JavaScript转译器,相信大家都听过。由于JavaScript在不断的发展,但是浏览器的发展速度跟不上,新的语法和特性不能马上被浏览器支持,因此需要一个能将新语法新特性转为现代浏览器能理解的语法的转译器,而Babel就是充当了转译器的角色。PS:以前博主一直以为(相信很多刚接触Babel的同学也是这样),只要使用了Babel,就可以放心无痛使用ES6的语法了,然而事情并不是这样。Babel编译并不会做polyfill,Babel为了保证正确的语义,只能转换语法而不会增加或修改原有的属性和方法。要想无痛使用ES6,还需要配合polyfill。不太理解的同学,在这里推荐大家看下这篇文章:21 分钟精通前端 Polyfill 方案,写得非常通俗易懂。总的来说,就是Babel需要配合polyfill来使用。Babel更新比较频繁,网上搜出来的很多配置教程是旧版本的,可能并不适用最新的Babel 7.x,所以我们这里折腾一下最新的webpack4配置Babel方案:babel-loader。1.安装babel-loader,@babel/core、@babel/preset-env。npm install -D babel-loader @babel/core @babel/preset-env core-jscore-js是JavaScript模块化标准库,在@babel/preset-env按需打包时会使用core-js中的函数,因此这里也是要安装的,不然打包的时候会报错。2.修改webpack.config.js配置,添加rulesconst path = require(‘path’);module.exports = { entry: ‘./src/index.js’, output: { path: path.resolve(__dirname, ‘dist’), filename: ‘remember-scroll.js’, library: ‘RememberScroll’, libraryTarget: ‘umd’, libraryExport: ‘default’ }, module: { rules: [ { test: /.m?js$/, exclude: /(node_modules|bower_components)/, use: { loader: ‘babel-loader’ } } ] }};表示.js的代码使用babel-loader打包。3.在根目录新建babel.config.js,参考Babel官网const presets = [ [ “@babel/env”, { targets: { browsers: [ “last 1 version”, “> 1%”, “maintained node versions”, “not dead” ] }, useBuiltIns: “usage”, }, ],];browsers配置的是目标浏览器,即我们想要兼容到哪些浏览器,比如我们想兼容到IE10,就可以写上IE10,然后webpack会在打包时自动为我们的库添加polyfill兼容到IE10。博主这里用的是推荐的参数,来自:npm browserslist,这样就能兼容到大多数浏览器啦。配置好后,npm run dev打包即可。此时,我们已经实现了第三个小目标:兼容低版本浏览器。生产环境打包npm run dev打包出来的js会比较大,一般还需要压缩一下,而我们可以使用webpack的production模式,就会自动为我们压缩js,输出一个生产环境可用的包。在package.json再添加一条build命令: “scripts”: { “test”: “echo "Error: no test specified" && exit 1”, “build”: “webpack –mode=production -o dist/remember-scroll.min.js –colors”, “dev”: “webpack –mode=development –colors” },这里同时指定了输出的文件名为:remember-scroll.min.js,一般生产环境就是使用这个文件啦。发布到npm经过上面的步骤,我们已经写完这个库,有需求的同学可以将库发布到npm,让更多的人可以方便用到你这个库。在发布到npm前,需要修改一下package.json,完善下描述作者之类的信息,最重要的是要添加main入口文件:{ “main”: “dist/remember-scroll.min.js”,}这样别人使用你的库时,可以直接通过import RememberScroll from ‘remember-scroll’来使用remember-scroll.min.js。发布步骤:先到https://www.npmjs.com/注册一个账号,然后验证邮箱。然后在命令行中输入:npm adduser,输入账号密码邮箱登录。运行npm publish上传包,几分钟后就可以在npm搜到你的包了。至此,基本就完成一个插件的开发发布过程啦。不过一个优秀的开源项目,还应该要有详细的说明文档,使用示例等等,大家可以参考下博主这个项目的README.md, 中文README.md。最后文章写了好几天了,可谓呕心沥血,虽然比较啰嗦,但应该比较清楚地交代了如何运用ES6语法从零写一个记住用户离开位置的js插件,也很详细地讲解了如何用最新的webpack打包我们的库,希望能让大家都有所收获,也希望大家能到GitHub上点个Star鼓励一下啦。remember-scroll这个插件其实几个月前就已经发布到npm了,一直比较忙(懒)没写章分享。虽然功能简单但很有诚意,能兼容到IE9。使用起来也非常方便简单,可直接通过script标签cdn引入,也可以在vue中import RememberScroll from ‘remember-scroll’使用。文档中有详细的使用示例:script标签使用方式vue中使用方式vue异步获取数据时使用方式项目地址Github,在线Demo。欢迎大家评论交流,也欢迎PR,同时希望大家能点个Star鼓励一下啦。 ...

March 19, 2019 · 3 min · jiezi

easyui messager中的内容不能自适应大小问题处理

项目中发现一个问题,easyui下面的 messager下面的组件不能根据消息内容自适应宽高,正常情况下应该是内容超过弹框时可以自动出现滚动条的,但经过测试 1.4.1 1.4.2是直接把内容截断了,试了几个其他版本 1.3.1、1.4.5、1.7.1都没有这个问题,不能升级组件,只能修改css样式。修改easyui.css,大概在2248行.messager-body { padding: 10px; overflow: hidden;}为.messager-body { padding: 10px; /overflow: hidden;/}修改前效果:修改后效果:相关代码:<!doctype html><html lang=“en”> <head> <meta charset=“UTF-8”> <meta name=“Generator” content=“EditPlus®"> <meta name=“Author” content=”"> <meta name=“Keywords” content=""> <meta name=“Description” content=""> <link id=“easyuiTheme” rel=“stylesheet” type=“text/css” href=“easyui1.4.1/themes/default/easyui.css?v=4.0”> <link rel=“stylesheet” type=“text/css” href=“easyui1.4.5/themes/icon.css?v=4.0”> <script type=“text/javascript” src=“easyui1.4.1/jquery.min.js?v=4.0”></script> <script type=“text/javascript” src=“easyui1.4.1/jquery.easyui.min.js?v=4.0”></script> <title>Document</title> </head> <body> <p> <a href=“javascript:void(0)” class=“easyui-linkbutton” onclick=“center()">Center</a> <a href=“javascript:void(0)” class=“easyui-linkbutton” onclick=“dialog()">dialog</a> </p> <div id=“dlg” class=“easyui-dialog” style=“width:200px;height:100px;padding:10px;” closed=“true”> org.springframework.jdbc.BadSqlGrammarException: PreparedStatementCallback; bad SQL grammar [/*mycat:schema=G_101_bi/ select a.,b.gna dsgna,b.daad dsad from dtip a left join dsrc b on a.dsgco = b.gco where 1=1 order by gco desc limit 0,20]; nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Unknown column ‘b.gna’ in ‘field list’ </div> <script> var msg=“org.springframework.jdbc.BadSqlGrammarException: PreparedStatementCallback; bad SQL grammar [/*mycat:schema=G_101_bi/ select a.,b.gna dsgna,b.daad dsad from dtip a left join dsrc b on a.dsgco = b.gco where 1=1 order by gco desc limit 0,20]; nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Unknown column ‘b.gna’ in ‘field list’”; var temp = escape(msg); function center(){ $.messager.show({ title:‘My Title’, msg:’<div style=“width:200px;word-wrap:break-word;">’+unescape(temp)+"</div>”, showType:‘fade’, width:250, height:200, timeout:0, draggable:true, style:{ right:’’, bottom:’’ } }); } function dialog(){ $(’#dlg’).dialog(‘open’); } </script> </body></html> ...

January 11, 2019 · 1 min · jiezi

移动端滚动研究

移动web滚动问题在移动端如果使用局部滚动,意思就是我们的滚动在一个固定宽高的div内触发,将该div设置成overflow:scroll/auto;来形成div内部的滚动,这时我们监听div的onscroll发现触发的时机区分android和ios两种情况,具体可以看下面表格:| 机型(内核) | body滚动 | 局部滚动 || :-: | :-: | :-: | | ios | 不能实时触发 | 不能实时触发 || android | 实时触发| 实时触发 || ios wkwebview内核 | 实时触发| 实时触发 |不能实时触发表现:只在手指触摸的屏幕上一直滑动时和滚动停止的那一刻才触发。关于模拟滚动概念正常的滚动:我们平时使用的scroll,包括上面讲的滚动都属于正常滚动,利用浏览器自身提供的滚动条来实现滚动,底层是由浏览器内核控制。模拟滚动:最典型的例子就是iscroll了,原理一般有两种:监听滚动元素的touchmove事件,当事件触发时修改元素的transform属性来实现元素的位移,让手指离开时触发touchend事件,然后采用requestanimationframe来在一个线型函数下不断的修改元素的transform来实现手指离开时的一段惯性滚动距离。监听滚动元素的touchmove事件,当事件触发时修改元素的transform属性来实现元素的位移,让手指离开时触发touchend事件,然后给元素一个css的animation,并设置好duration和function来实现手指离开时的一段惯性距离。方案比较第一种方案由于惯性滚动的时机时由js自己控制所以可以拿到滚动触发阶段的scrolltop值,并且滚动的回调函数onscroll在滚动的阶段都会触发。第二种方案相比第一种要劣势一些,区别在于手指离开时,采用的时css的animation来实现惯性滚动,所以无法直接触发惯性滚动过程中的onscroll事件,只有在animation结束时才可以借助animationend来获取到事件,当然也有一种方法可以实时获取滚动事件,也是借助于requestanimationframe来不断的去读取滚动元素的transform来拿到scrolltop同时触发onscroll回调。正常滚动和模拟滚动的性能比较模拟滚动的fps值波动较大,这样滚动起来会有明显的卡顿感觉,各位体验的时候如果滚动超过10屏之后就可以明显感觉到两着的区别。在使用模拟滚动时,浏览器在js层面会消耗更多的性能去改变dom元素的位置,在dom复杂层级深的页面更为高,所以在长列表滚动时还要使用正常滚动更好。滚动和下拉刷新方案1:借助iscroll的原理,整个页面使用模拟滚动,将下拉刷新元素放在顶部,当页面滚动到顶部下拉时,下拉刷新元素随着页面的滚动出现,当手指离开时收回,此方案实现起来较为简单直接借助iscoll即可,但是使用了模拟滚动之后在正常的列表滚动时性能上不如正常滚动。方案2:页面使用正常滚动,将下拉刷新元素放置在顶部top值为负值(正常情况下不可见),当页面处于顶部时下拉,这时监听touchmove事件,修改scrollcontent的tranlateY值,同时修改下拉刷新元素的tranlateY值,将两者同时位移来将下拉刷新元素显示出来,手指离开时(touchend)收回,这种方案满足了在正常列表滚动时使用原生的滚动节省性能,只在下拉刷新时使用模拟滚动来实现效果。方案3:方案2的改良版,唯一不同是将下拉刷新元素和scrollcontent放在一个div里,将下拉刷新元素的margintop设为负值,在下拉刷新时,只需要修改scrollcontent一个元素的tranlateY值即可实现下拉,在性能上要比方案2好。还会有一个性能上的问题就是:当页面的列表过长,dom元素过多时,在模拟滚动,下拉刷新这段时间内,页面也会有卡顿现象,这里采取了一个优化策略即:列表较长时dom数量较多时,在触发下拉刷新的时机时将页面视窗之外的dom元素隐藏或者存放在fragment里面。在刷新完成之后手指离开(touchend)时将隐藏的元素显示出来。需要注意的是,隐藏和显示视窗外的元素这个操作在下拉刷新时只会执行一次,并且只有在下拉刷新时才会执行。下面介绍如何去优化scroll事件的触发,避免scroll事件过度消耗资源:防抖(Debouncing)和节流(Throttling)scroll 事件本身会触发页面的重新渲染,同时 scroll 事件的 handler 又会被高频度的触发, 因此事件的 handler 内部不应该有复杂操作,例如 DOM 操作就不应该放在事件处理中。特别是针对此类高频度触发事件问题(例如页面 scroll ,屏幕 resize,监听用户输入等)。防抖(Debouncing)防抖技术即是可以把多个顺序地调用合并成一次,也就是在一定时间内,规定事件被触发的次数。节流(Throttling)防抖函数确实不错,但是也存在问题,譬如图片的懒加载,我希望在下滑过程中图片不断的被加载出来,而不是只有当我停止下滑时候,图片才被加载出来。又或者下滑时候的数据的 ajax 请求加载也是同理。这个时候,我们希望即使页面在不断被滚动,但是滚动 handler 也可以以一定的频率被触发(譬如 250ms 触发一次),这类场景,就要用到另一种技巧,称为节流函数(throttling)。节流函数,只允许一个函数在 X 毫秒内执行一次。与防抖相比,节流函数最主要的不同在于它保证在 X 毫秒内至少执行一次我们希望触发的事件 handler。关于防抖动与节流,我的博客文章也有提及。使用rAF(requestAnimationFrame)触发滚动事件如果页面只需要兼容高版本浏览器或应用在移动端,又或者页面需要追求高精度的效果,那么可以使用浏览器的原生方法 rAF(requestAnimationFrame)。window.requestAnimationFrame() 这个方法是用来在页面重绘之前,通知浏览器调用一个指定的函数。这个方法接受一个函数为参,该函数会在重绘前调用。rAF 常用于 web 动画的制作,用于准确控制页面的帧刷新渲染,让动画效果更加流畅,当然它的作用不仅仅局限于动画制作,我们可以利用它的特性将它视为一个定时器。(当然它不是定时器)通常来说,rAF 被调用的频率是每秒 60 次,也就是 1000/60 ,触发频率大概是 16.7ms 。(当执行复杂操作时,当它发现无法维持 60fps 的频率时,它会把频率降低到 30fps 来保持帧数的稳定。)var ticking = false; // rAF 触发锁 function onScroll(){ if(!ticking) { requestAnimationFrame(realFunc); ticking = true; }} function realFunc(){ // do something… console.log(“Success”); ticking = false;}// 滚动事件监听window.addEventListener(‘scroll’, onScroll, false);实现以16.7ms 触发一次 handler,降低了可控性,但是提升了性能和精确度。从本质上而言,我们应该尽量去精简 scroll 事件的 handler ,将一些变量的初始化、不依赖于滚动位置变化的计算等都应当在 scroll 事件外提前就绪。避免在scroll 事件中修改样式属性 / 将样式操作从 scroll 事件中剥离输入事件处理函数,比如 scroll / touch 事件的处理,都会在 requestAnimationFrame 之前被调用执行。因此,如果你在 scroll 事件的处理函数中做了修改样式属性的操作,那么这些操作会被浏览器暂存起来。然后在调用 requestAnimationFrame 的时候,如果你在一开始做了读取样式属性的操作,那么这将会导致触发浏览器的强制同步布局。滑动过程中尝试使用 pointer-events: none 禁止鼠标事件pointer-events 是一个 CSS 属性,可以有多个不同的值,大概的意思就是禁止鼠标行为,应用了该属性后,譬如鼠标点击,hover 等功能都将失效,即是元素不会成为鼠标事件的 target。pointer-events: none 可用来提高滚动时的帧频。的确,当滚动时,鼠标悬停在某些元素上,则触发其上的 hover 效果,然而这些影响通常不被用户注意,并多半导致滚动出现问题。对 body 元素应用 pointer-events: none ,禁用了包括 hover 在内的鼠标事件,从而提高滚动性能。大概的做法就是在页面滚动的时候, 给 添加上 .disable-hover 样式,那么在滚动停止之前, 所有鼠标事件都将被禁止。当滚动结束之后,再移除该属性。// css 代码.disable-hover,.disable-hover * { pointer-events: none !important;}// js 代码var body = document.body, timer;window.addEventListener(‘scroll’, function() { clearTimeout(timer); if(!body.classList.contains(‘disable-hover’)) { body.classList.add(‘disable-hover’) } timer = setTimeout(function(){ body.classList.remove(‘disable-hover’) },500);}, false);参考 移动 Web 的滚动,高性能滚动及页面渲染优化 ...

January 5, 2019 · 1 min · jiezi