乐趣区

用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.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 事件监听用户滚动行为;

因此,需要在构造函数中就执行 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-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。
发布步骤:

先到 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 鼓励一下啦。

退出移动版