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

前言

常见的js插件都不多使用ES6的class,通常都是经过构造函数,并且经常是手写CMDAMD规范来封装一个库,好比这样:javascript

// 引用自: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大法了,能够用更优雅的的写法来实现一个库,好比这样:html

class RememberScroll {
    constructor(options) {
        ...
    }
}
export default RememberScroll
复制代码

在这篇文章,博主主要经过分享最近本身写的一个记住页面滚动位置小插件,讲一下如何用class语法配合webpack 4.xbabel 7.x封装一个可用的库。前端

项目地址:Github, 在线Demo:Demovue

喜欢的朋友但愿能点个Star收藏一下,很是感谢。java

需求来源

相信不少同窗都会遇到这样一个需求:用户浏览一个页面并离开后,再次打开时须要从新定位到上一次离开的位置node

这个需求很常见,咱们平时在手机上阅读微信公众号的文章页面就有这个功能。想要作到这个需求,也比较好实现,但博主有点懒,心想有没有现成的库能够直接用呢?因而去GitHub上搜了一波,发现并无很好的且符合我需求的,因而得本身实现一下。webpack

为了灵活使用(只是部分页面须要这个功能),博主在项目中单独封装了这个库,原本是在公司项目中用的,后来想一想何不开源出来呢?因而有了这个分享,这也是对本身工做的一个总结。git

预期效果

博主喜欢在作一件事情前先yy一下预期的效果。博主但愿这个库用起来尽可能简单,最好是插入一句代码就能够了,好比这样:es6

<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()初始化一下便可。github

下面就带着这个目标,一步一步去实现啦。

设计方案

1. 须要存哪些信息?

用户浏览页面的位置,主要须要存两个字段:哪一个页面离开时的位置,经过这两个字段,咱们才能够在用户第二次打开网站的页面时,命中该页面,并自动跳转到上一次离开的位置。

2.存在哪?

记住浏览位置,须要将用户离开前的浏览位置记录在客户端的浏览器中。这些信息能够主要存放在:cookiesessionStoragelocalStorage中。

  1. 存放在cookie,大小4K,空间虽有限但也勉强能够。但cookie是每次请求服务器时都会携带上的,无形中增长了带宽和服务器压力,因此整体来讲是不太合适的。
  2. 存放在sessionStorage中,因为仅在当前会话下有效,用户离开页面sessionStorage就会被清除,因此不能知足咱们的需求。
  3. 存放在localStorage,浏览器可永久保存,大小通常限制5M,知足咱们需求。

综上,最后咱们应该选择localStorage

3. 需注意的问题

  1. 一个站点可能有不少页面,如何标识是哪一个页面呢?

通常来讲能够用页面的url做为页面的惟一标识,好比:www.xx.com/article/${id},不一样的id对应不一样的页面。

但博主考虑到如今不少站点都是用spa了,并且常见在url后面会带有#xxx的哈希值,如www.xx.com/article/${id}#tag1www.xx.com/article/${id}#tag2这种状况,这可能表示的是同一个页面的不一样锚点,因此用url做为页面的惟一标识不太可靠。

所以,博主决定将这个页面惟一标识做为一个参数来让使用者来决定,姑且命名为pageKey,让使用者保证是全站惟一的便可。

  1. 若是用户访问咱们的站点中不少不少的页面,因为localStorage是永久保存的,如何避免localStorage不断累积占用过大?

咱们的需求可能仅仅是想近期记住便可,即只须要记住用户的浏览位置几天,可能会更但愿咱们存的数据可以自动过时。

localStorage自身是没有自动过时机制的,通常只能在存数据的时候同时存一下时间戳,而后在使用时判断是否过时。若是只能是在使用时才判断是否清除,而新访问页面时又会生成新的记录,localStorage中始终都会存在至少一条记录的,也就是说没法真正实现自动过时。这里不由就以为有点多余了,既然都是会一直保留记录在localStorage中,那干脆就不判断了,咱换一个思路:只记录有限的最新页面数量

举个例子:

我们网站有个文章页:www.xx.com/articles/${id},每一个的id表示不一样的文章,我们只记录用户最新访问的5篇文章,即维护一个长度为5的队列。

好比当前网站有id从1100篇文章,用户分别访问第1,2,3,4,5篇文章时,这5篇文章都会记录离开的位置,而当用户打开第六篇文章时,第六条记录入队的同时第一条记录出队,此时localStorage中记录的是2,3,4,5,6这几篇文章的位置,这就保证了localStorage永远不会累积存储数据且旧记录会随着不断访问新页面自动“过时”。

为了更灵活一点,博主决定给这个插件添加一个maxLength的参数,表示当前站点下记录的最新的页面最大数量,默认值设为5,若是有小伙伴的需求是记录更多的页面,能够经过这个参数来设置。

4. 实现思路

  1. 咱们须要时刻监听用户浏览页面时的滚动条的位置,能够经过window.onscroll事件,得到当前的滚动条位置:scrollTop
  2. scrollTop和页面惟一标识pageKey存进localStorage中。
  3. 用户再次打开以前访问过的页面,在页面初始化时,读取localStorage中的数据,判断页面的pageKey是否一致,若一致则将页面的滚动条位置自动滚动到相应的scrollTop值。

是否是很简单?不过实现的过程当中须要注意一下细节,好比作一下防抖处理。

实现步骤

逼逼了这么久,是时候开始撸代码了。

1.封装localStorage工具方法

工欲善其事,必先利其器。为更好服务接下来的工做,我们先简单封装一下调用localStorage的几个方法,主要是get,set,remove

// storage.js
const 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 Storage
复制代码

2. 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事件监听用户滚动行为; 所以,须要在构造函数中就执行initScrolladdScrollEvent这两个方法:
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

打包

接下来应该是本文的重点了,首先要清楚为何要打包?

  1. 将项目中所用到的js文件合并,只对外输出一个js文件。
  2. 使项目同时支持AMD,CMD、浏览器<script>标签引入,即umd规范。
  3. 配合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.jsindex.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 。在咱们这里只需在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'
  }
};
复制代码

注意libraryTargetumd,就是咱们要打包的目标规范为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-js
复制代码

core-js是JavaScript模块化标准库,在@babel/preset-env按需打包时会使用core-js中的函数,所以这里也是要安装的,否则打包的时候会报错。

2.修改webpack.config.js配置,添加rules

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'
  },
  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

发布步骤:

  1. 先到www.npmjs.com/注册一个帐号,而后验证邮箱。
  2. 而后在命令行中输入:npm adduser,输入帐号密码邮箱登陆。
  3. 运行npm publish上传包,几分钟后就能够在npm搜到你的包了。

至此,基本就完成一个插件的开发发布过程啦。

不过一个优秀的开源项目,还应该要有详细的说明文档,使用示例等等,你们能够参考下博主这个项目的README.md中文README.md

最后

文章写了好几天了,可谓呕心沥血,虽然比较啰嗦,但应该比较清楚地交代了如何运用ES6语法从零写一个记住用户离开位置的js插件,也很详细地讲解了如何用最新的webpack打包咱们的库,但愿能让你们都有所收获,也但愿你们能到GitHub上点个Star鼓励一下啦。

remember-scroll这个插件其实几个月前就已经发布到npm了,一直比较忙(懒)没写章分享。虽然功能简单但颇有诚意,能兼容到IE9。

使用起来也很是方便简单,可直接经过script标签cdn引入,也能够在vue中import RememberScroll from 'remember-scroll'使用。文档中有详细的使用示例:

项目地址Github,在线Demo

欢迎你们评论交流,也欢迎PR,同时但愿你们能点个Star鼓励一下啦。

相关文章
相关标签/搜索