前言
常見的js插件都不多使用ES6的class,通常都是經過構造函數,並且經常是手寫CMD、AMD規範來封裝一個庫,好比這樣:html
// 引用自:https://www.jianshu.com/p/e65...前端
(function(undefined) {vue
"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); }
}());node
但如今都9102年了,是時候祭出咱們的ES6大法了,能夠用更優雅的的寫法來實現一個庫,好比這樣:webpack
class RememberScroll {git
constructor(options) { ... }
}
export default RememberScroll
複製代碼
在這篇文章,博主主要經過分享最近本身寫的一個記住頁面滾動位置小插件,講一下如何用class語法配合webpack 4.x和babel 7.x封裝一個可用的庫。es6
項目地址:Github, 在線Demo:Demogithub
喜歡的朋友但願能點個Star收藏一下,很是感謝。web
需求來源
相信不少同窗都會遇到這樣一個需求:用戶瀏覽一個頁面並離開後,再次打開時須要從新定位到上一次離開的位置。chrome
這個需求很常見,咱們平時在手機上閱讀微信公衆號的文章頁面就有這個功能。想要作到這個需求,也比較好實現,但博主有點懶,心想有沒有現成的庫能夠直接用呢?因而去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()初始化一下便可。
下面就帶着這個目標,一步一步去實現啦。
設計方案
用戶瀏覽頁面的位置,主要須要存兩個字段:哪一個頁面和離開時的位置,經過這兩個字段,咱們才能夠在用戶第二次打開網站的頁面時,命中該頁面,並自動跳轉到上一次離開的位置。
2.存在哪?
記住瀏覽位置,須要將用戶離開前的瀏覽位置記錄在客戶端的瀏覽器中。這些信息能夠主要存放在:cookie、sessionStorage、localStorage中。
存放在cookie,大小4K,空間雖有限但也勉強能夠。但cookie是每次請求服務器時都會攜帶上的,無形中增長了帶寬和服務器壓力,因此整體來講是不太合適的。
存放在sessionStorage中,因爲僅在當前會話下有效,用戶離開頁面sessionStorage就會被清除,因此不能知足咱們的需求。
存放在localStorage,瀏覽器可永久保存,大小通常限制5M,知足咱們需求。
綜上,最後咱們應該選擇localStorage。
一個站點可能有不少頁面,如何標識是哪一個頁面呢?
通常來講能夠用頁面的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,若是有小夥伴的需求是記錄更多的頁面,能夠經過這個參數來設置。
咱們須要時刻監聽用戶瀏覽頁面時的滾動條的位置,能夠經過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
複製代碼
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/e65...
;(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'
}
};
複製代碼
注意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。
發佈步驟:
先到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鼓勵一下啦。