用Class寫一個記住使用者離開位置的js外掛

fengxianqi發表於2019-03-19

前言

常見的js外掛都很少使用ES6的class,一般都是通過建構函式,而且常常是手寫CMDAMD規範來封裝一個庫,比如這樣:

// 引用自: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.xbabel 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.存在哪?

記住瀏覽位置,需要將使用者離開前的瀏覽位置記錄在客戶端的瀏覽器中。這些資訊可以主要存放在: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鼓勵一下啦。

相關文章