借用了兩個久經考驗的輪子:fastClick和better-scroll,介意能夠就此打住。本文絕對原創,手打,思路清晰,知識不難,不適合大佬觀看,謝謝。javascript
首先說一下,我不是阿里的人,也沒去阿里面試過,這是某微信羣裏的一個小夥伴給的,我如今的能力達不到阿里的要求。不過人沒夢想還不如鹹魚,有能力的話仍是想去嘗試一下。本文若有不足,請勿嘲諷,指出不足便可,謝謝。碼字不易,且看且珍惜,轉載請註明出處。原創博客,若侵犯貴司的利益,請私信我刪除。若以爲不錯,求個贊和github的star。php
題目以下:css
大概就是這樣吧,分析一下就是作一個城市選擇組件,實現的功能或者要求呢就是能夠定位當前的城市、用localstorage存儲上次定位的城市和最近選擇過的城市、能夠按照輸入的字母或者文字篩選出想找的城市、將數據帶到頁面也就是一個父子傳參的問題吧、頁面使用flex佈局。html
我在下班閒暇時間簡單的作了一下,成功以下:前端
我僅僅作了這個組件,向頁面傳參的功能還沒作,能夠用父子組件傳參完成。vue
簡單的說一下我這個城市選擇組件和其中的一下知識點:java
我用node.js起了一個後臺服務,使用的express框架,完成知足了個人需求。個人數據來源是爬取的某網站的城市地址(若侵權請聯繫我刪除),數據是這樣的:node
{
"id": 151, "name": "鞍山", "pinyin": "anshan", "acronym": "as", "rank": "C", "firstChar": "A" }
我在node端調用了某浪的一個定位接口做爲個人定位服務,並將數據返回,當這個接口有問題或者沒獲取到的時候會返回定位在北京。具體代碼爲:webpack
// 獲取城市數據,city爲我爬取的信息
app.get('/', function (req, res) {
res.send(city);
res.end()
});
// 調用新浪的接口返回定位
app.get('/nowcity', function (req, res) { let getIpInfo = function (cb) { var url = 'http://int.dpool.sina.com.cn/iplookup/iplookup.php?format=json'; http.get(url, function (res) { var code = res.statusCode; if (code == 200) { res.on('data', function (data) { try { cb(JSON.parse(data)); } catch (err) { console.log(err) } }); } }).on('error',function(e){ cb({ city: "北京", country: "中國", province: "北京", }) }) }; getIpInfo(function (msg) { let nowcity = msg res.send(nowcity) res.end() }) });
本次組件基於vue框架,我使用vue-cli腳手架搭建的,這一塊知識很少作描述,參考個人博客《vue環境搭建與建立第一個vuejs文件》。ios
本次我使用了css預處理程序——stylus。
在vue-cli中使用stylus首先要安裝依賴npm install stylus --save-dev、
npm install stylus-loader --save-dev,而後再文件中使用
<style lang="stylus" scoped>便可。
引入單獨的stylus文件使用@import '~common/stylus/css.styl'。
本次項目中,除了安裝了有關stylus的依賴我還引入了better-scroll、fastclick、axios這三個依賴。
better-scroll是我見過的最好的處理移動端滾動的庫了,而且文檔清晰,思路明確。fastclick用於處理移動端click事件300毫秒延遲。至於axios,我想你們都知道,axios 是一個基於 promise 的 HTTP 庫,能夠用在瀏覽器和 node.js 中。
本次項目我構建了6個功能組件,分別是搜索框組件、搜索頁組件、定位組件、側邊欄組件、彈窗組件、城市顯示組件。還有倆個基礎組件,分別是滾動組件和城市組件。
引入城市組件的方法是:
// 先引入文件
import Search from 'components/Search'
import Scroll from 'base/Scroll.vue' import PositionBox from 'components/PositionBox' import CityList from 'components/CityList' import NavList from 'components/NavList' import MaskBox from 'components/MaskBox' import SearchList from 'components/SearchList' // 而後在父組件中註冊 components: { 'search': Search, 'scroll': Scroll, 'position-box': PositionBox, 'nav-list': NavList, 'city-list': CityList, 'mask-box': MaskBox, 'search-list': SearchList } // 使用 <search @txtdata="searchText" :clearText="clearSearch"></search>
父組件向子組件傳參很是簡單,就搜索框組件來講:
<search @txtdata="searchText" :clearText="clearSearch"></search>
父組件給子組件傳參只須要:clearText="clearSearch"便可,其中clearSearch爲要傳入的信息,clearText爲子組件接收的名稱。
在子組件中,使用props屬性操做傳參:
props: {
clearText: Boolean
}
// 帶默認參數的
props: { clearText: { type: Boolean, default:false } }
子組件向父組件傳參使用this.$emit傳參:
// 點擊列表觸發改變定位的事件
在上面的代碼中txtdata爲傳遞到父組件的內容的名字,this.searchText爲參數。在父組件端使用@來觸發接收事件@txtdata="searchText":
// 搜索框內容
searchText (text) {
// text即傳遞過來的參數
}
咱們在處理前端的ajax時通常但願減小交互來提升性能和效率。在搜索框組件中,咱們使用到了聯想搜索的功能,這裏我使用正則實現的。所以在打字的過程當中,咱們但願在打字完成菜進行交互(總不能讓瀏覽器一直都在遍歷數組或者Ajax)。在這裏我使用了一個定時函數完成延時效果:
if (this.timer) {
clearTimeout(this.timer) // 清除定時器
} this.timer = setTimeout(() => { this.$emit('txtdata', this.searchText) }, 300)
在這段代碼中,我綁定了keyup事件,也就是說,300毫秒中只要有按鈕彈起,就會觸發事件清除上一個定時器,而後從新生成新的定時器,300毫秒內無輸入則定時器觸發,向父組件傳遞參數。
話說曾經正則是我最頭疼的事情,直到我有一天耐心的看了許多文檔和博客。
export function getSearchList (text, list) {
let reg1 = /^\w+$/g //檢測是否爲字母
let reg2 = new RegExp(`^${text}`, 'g') //檢測模板text
let reg3 = new RegExp('^[\\u4E00-\\u9FFF]{1,}$', 'g') //檢測是否爲漢字
let resList = [] // 當text爲字母時 if (text.match(reg1)) { for (let i = 0, len1 = list.length; i < len1; i++) { for (let j = 0, len2 = list[i][1].length; j < len2; j++) { // 篩選知足這個正則的 if (list[i][1][j].pinyin.match(reg2)) { resList.push(list[i][1][j]) } } } } else { // 同上 if (reg3.test(text)) { for (let i = 0, len1 = list.length; i < len1; i++) { for (let j = 0, len2 = list[i][1].length; j < len2; j++) { if (list[i][1][j].name.match(reg2)) { resList.push(list[i][1][j]) } } } } } return resList }
JavaScript經過內置對象RegExp
支持正則表達式,有兩種方式建立正則表達式對象,分別是構造函數var reg=new RegExp('<%[^%>]+%>','g')和
字面量var reg=/<%[^%>]%>/g
,由於我此次用到了模板語句,就是用了構造函數,
最後的g表明全局。
//匹配一個字符,這個字符能夠是0-9中的任意一個 var reg1 = /[0123456789]/ //匹配一個字符,這個字符能夠是0-9中的任意一個 var reg2 = /[0-9]/ //匹配一個字符,這個字符能夠是a-z中的任意一個 var reg3 = /[a-z]/ //匹配一個字符,這個字符能夠是大寫字母、小寫字母、數字中的任意一個 var reg3 = /[a-zA-Z0-9]/ //匹配一個字符,這個字符能夠是漢字的任意一個 var reg4 = /[\\u4E00-\\u9FFF]/
咱們還能引入開頭結尾的限制:
^ | 以xxx開頭 |
$ | 以xxx結尾 |
\b | 單詞邊界 |
\B | 非單詞邊界 |
數量量詞:
字符 | 含義 |
---|---|
? | 出現零次或一次(最多出現一次) |
+ | 出現一次或屢次(至少出現一次) |
* | 出現零次或屢次(任意次) |
{n} | 出現n次 |
{n,m} | 出現n到m次 |
{n,} | 至少出現n次 |
通常來說,獲取DOM元素,需document.querySelector(".input1")獲取這個dom節點,而後在獲取input1的值。可是用ref綁定以後,咱們就不須要在獲取dom節點了,直接在上面的input上綁定input1,而後$refs裏面調用就行。而後在javascript裏面這樣調用:this.$refs.input1 這樣就能夠減小獲取dom節點的消耗了。
<div ref="wrapper" class="scroll"> </div> // 此時this.$refs('wrapper')就表明了這個div
經過字面意思理解,slot爲「插槽,水溝」,大概就是一個安放組件或者dom結構的地方。子組件模板必須包含至少一個 <slot>
插口,不然父組件的內容將會被丟棄。當子組件模板只有一個沒有屬性的插槽時,父組件傳入的整個內容片斷將插入到插槽所在的 DOM 位置,並替換掉插槽標籤自己。最初在 <slot>
標籤中的任何內容都被視爲備用內容。備用內容在子組件的做用域內編譯,而且只有在宿主元素爲空,且沒有要插入的內容時才顯示備用內容。
假定 my-component
組件有以下模板:
<div>
<h2>我是子組件的標題</h2>
<slot>
只有在沒有要分發的內容時纔會顯示。
</slot>
</div>
父組件模板:
<div> <h1>我是父組件的標題</h1> <my-component> <p>這是一些初始內容</p> <p>這是更多的初始內容</p> </my-component> </div>
渲染結果:
<div> <h1>我是父組件的標題</h1> <div> <h2>我是子組件的標題</h2> <p>這是一些初始內容</p> <p>這是更多的初始內容</p> </div> </div>
本次項目的插槽:
<!--父組件--> <scroll :data="citylist" ref="suggest" :probeType="3" :listenScroll="true" @distance="distance" @scrollStore="scrollStore"> <div> <position-box :chooseCity="chooseCity" :orientate="nowCity" :historyCityArr="historyCityArr" @changeCity="changeCity"></position-box> <city-list :citylist="citylist" :elementIndex="elementIndex" @positionCity="changeCity" @singleLetter="singleLetter"></city-list> </div> </scroll> <!--子組件--> <div ref="wrapper" class="scroll"> <slot></slot> </div>
this.scroll = new BScroll(this.$refs.wrapper, {
probeType: this.probeType, scrollY: true, // 滾動方向爲Y軸 click: true, // 是否派發click事件,一般判斷瀏覽器派發的click仍是betterscroll派發的click,能夠用event._constructed,如果bs派發的則爲true momentum: true, // 當快速滑動時是否開啓滑動慣性 bounce: false, // 是否啓用回彈動畫效果 bounceTime: 700, // 彈力動畫持續的毫秒數 deceleration: 0.001, // 滾動動量減速越大越快,建議不大於0.01 momentumLimitTime: 300, // 符合慣性拖動的最大時間 momentumLimitDistance: 15, // 符合慣性拖動的最小拖動距離 resizePolling: 60 // 從新調整窗口大小時,從新計算better-scroll的時間間隔 })
經過構建一個scroll對象來使用better-scroll,這裏必須綁定一個dom節點,即this.$refs.wrapper。裏面添加一些屬性來自定義。
在本次項目中,咱們使用了Bscroll的三個方法:
refresh()
參數:無
返回值:無
做用:從新計算 better-scroll,當 DOM 結構發生變化的時候務必要調用確保滾動的效果正常。
scrollTo(x, y, time, easing)
參數:返回值:無
{Number} x 橫軸座標(單位 px)
{Number} y 縱軸座標(單位 px)
{Number} time 滾動動畫執行的時長(單位 ms)
{Object} easing 緩動函數,通常不建議修改,若是想修改,參考源碼中的 ease.js 裏的寫法
做用:滾動到指定的位置
scrollToElement(el, time, offsetX, offsetY, easing)
參數:返回值:無
{DOM | String} el 滾動到的目標元素, 若是是字符串,則內部會嘗試調用 querySelector 轉換成 DOM 對象。(此處我使用了this.$refs)
{Number} time 滾動動畫執行的時長(單位 ms)
{Number | Boolean} offsetX 相對於目標元素的橫軸偏移量,若是設置爲 true,則滾到目標元素的中心位置
{Number | Boolean} offsetY 相對於目標元素的縱軸偏移量,若是設置爲 true,則滾到目標元素的中心位置
{Object} easing 緩動函數,通常不建議修改,若是想修改,參考源碼中的 ease.js 裏的寫法
做用:滾動到指定的目標元素。
我相信你們對localstorage和sessionstorage的區別已經都懂了,其最大的區別就是localstorage像ROM,而sessionstorage像RAM。
在本次項目中,經過setItem和getItem來操做localstorage:
localStorage.setItem('historyCityArr', arr)
localStorage.getItem('historyCityArr')
相似於在單位渲染和移除的時候添加一個動畫特效。
<transition name="flag"> <div class="nowFlag" v-if="flag">{{flagText}}</div> </transition>
.flag-leave-active transition all 1s .flag-leave-to opacity 0
對於至一段的解釋爲,添加一個離開(移除)的過渡,一秒鐘內不透明度由1變成0。
在事件處理程序中調用 event.preventDefault()
或 event.stopPropagation()
是很是常見的需求。儘管咱們能夠在方法中輕鬆實現這點,但更好的方式是:方法只有純粹的數據邏輯,而不是去處理 DOM 事件細節。爲了解決這個問題,Vue.js 爲 v-on
提供了事件修飾符。以前提過,修飾符是由點開頭的指令後綴來表示的。
.stop 阻止事件冒泡
.prevent 阻止默認事件
.capture 阻止事件捕獲
.once 只觸發一次
html代碼以下:父組件向子組件傳遞是否清空內容的信息(用於點擊搜索頁選項後更改搜索頁),子組件觸發keyup事件時向父組件傳遞須要搜索的內容。
<!--父組件--> <search @txtdata="searchText" :clearText="clearSearch"></search> <!--子組件--> <div class="search-box"> <div class="ipt-box"> <input type="text" class="ipt" placeholder="城市名稱/拼音" @keydown="entry()" v-model="searchText" /> <div class="icon-box"> <i class="iconfont icon-sousuo icon"></i> </div> </div> </div>
//子組件js
methods: {
// 延時搜索
entry () { if (this.timer) { clearTimeout(this.timer) } this.timer = setTimeout(() => { this.$emit('txtdata', this.searchText) }, 300) } }, watch: { // 清除搜索內容 clearText (val) { if (val) { this.searchText = '' this.entry() } } }
在向上傳遞時有一個減小交互和運算的效果,用定時器實現的,上文有講到。
<!--父組件模塊-->
<position-box :chooseCity="chooseCity" :orientate="nowCity" :historyCityArr="historyCityArr" @changeCity="changeCity"></position-box>
<!--子組件模塊-->
<div class="position-box">
<div class="choose">
<span>你已選擇:{{chooseCity}}</span>
</div>
<div class="hostory">
<p>定位/最近訪問</p>
<div class="citybox">
<button @click="changeCity(orientate)">
<i class="iconfont icon-dingwei icon"></i>{{orientate}}
</button>
<button @click="changeCity(item)" v-for="item in historyCityArr" :key="item">{{item}}</button>
</div>
</div>
<div class="hot">
<p>熱門城市</p>
<div class="citybox">
<button v-for="city in hotCitys" :key="city" @click="changeCity(city)">{{city}}</button>
</div>
</div>
</div>
在這一部分裏面,一開始加載頁面的時候會觸發兩個事件:定位和讀取localstorage裏面存儲的歷史查看的記錄。
axios.get('http://localhost:1234/nowcity').then((res) => {
this.nowCity = res.data.city if (!this.choiceCity && !this.choiceCityName) { this.choiceCity = this.nowCity this.choiceCityName = this.nowCity } }, () => { this.nowCity = '北京' if (!this.choiceCity && !this.choiceCityName) { this.choiceCity = this.nowCity this.choiceCityName = this.nowCity } })
定位部分邏輯簡單,無非就是獲取數據,若是獲取不到默認爲北京。
localstorage的數據處理就在這個組件中:
setHistory (arr) {
localStorage.setItem('historyCityArr', arr) }, // 從本地取 getHistory () { let history = localStorage.getItem('historyCityArr') if (!history) { this.historyCityArr = [] } else { this.historyCityArr = history.split(',') } }, // 存到本地,正在查看的城市 setCity (name) { localStorage.setItem('seeCity', name) }, // 從本地取,,正在查看的城市 getCity () { let name = localStorage.getItem('seeCity') if (!name) { this.choiceCity = '' this.choiceCityName = '' } else { this.choiceCity = name this.choiceCityName = name } }
當查看到城市發生變化時,出觸發兩個setItem事件(不管是存數組仍是字符串),以便於在此打開時getItem能夠獲取到數據。一開始加載頁面時,會發兩個get事件,獲取到數據以後傳入定位模塊中渲染數據。get獲得的信息是字符串,咱們獲取到以後要轉轉化爲數組。
<!--父組件模塊--> <city-list :citylist="citylist" :elementIndex="elementIndex" @positionCity="changeCity" @singleLetter="singleLetter"></city-list> <!--子組件模塊--> <div class="lists"> <div v-for="citys in citylist" :key="citys[0]" :dataNum="citys[1].length"> <p class="city-title" :ref="citys[0]">{{citys[0]}}</p> <p class="city-item" v-for="city in citys[1]" :key="city.id" @click="changeCity(city.name)">{{city.name}}</p> </div> </div>
單說這個組件呢,屬於很簡單的那種,僅僅有展現渲染信息和點擊城市選項向上傳遞城市信息值的功能。可是後面增長了右邊欄nav以後又增長了向上傳遞dom節點的功能:
// 父組件
singleLetter (dom) {
this.$refs.suggest.scrollToElement(dom, 200, false, false) } // 子組件 elementIndex (val) { if (val === '頂') { return false } this.$emit('singleLetter', this.$refs[val][0]) }
父組件獲取到城市組件上傳的城市dom節點信息以後觸發Bscroll的scrollToElement方法,0.2秒內滾動到相應位置。
這個組件爲點擊選擇城市以後(而且點擊的城市不是當前已經查看的城市)觸發。
<!--父組件模塊--> <mask-box v-if="maskShow" :message="maskMessage" @chooseing="chooseResult"></mask-box> <!--子組件模塊--> <div class="mask-box"> <div class="mask-body"></div> <div class="btn-box"> <div class="message"> <p>{{message}}</p> </div> <div class="btn-left" @click="chooseTrue()"> <p>肯定</p> </div> <div class="btn-right" @click="chooseFalse()"> <p>取消</p> </div> </div> </div>
js部分很是簡單
chooseTrue () {
this.$emit('chooseing', true) }, chooseFalse () { this.$emit('chooseing', false) }
根據點擊的按鈕的不一樣向上傳值。當傳值爲true時觸發父組件一個事件,讓頁面滾動到頂部。
// 是否確認切換定位
chooseResult (res) {
if (!res) { this.maskClose() // 不切換,僅關閉彈窗 } else { this.choiceCityName = this.choiceCity this.local() this.associationShow = false // 關閉搜索框(在搜索狀態下) this.clearSearch = true // 清除輸入框的字(在搜索狀態下) // 當確認後滾動到頂部 this.$refs.suggest.scrollTo(0, 0, 200) this.maskClose() } }
這個組件頁面代碼不過,邏輯代碼也比較簡單,用到了上文的正則,很少作解釋。
<!--父組件模塊--> <transition name="list"> <search-list v-if="associationShow" :searchListContent="searchListContent" @changeName="changeCity"></search-list> </transition> <!--子組件模塊--> <div class="listbody"> <scroll :data="searchListContent"> <div> <city-item :searchListContent="searchListContent" @changeName="changeCity"></city-item> </div> </scroll> </div>
組件僅做展現和點擊選擇城市,功能與3組件相同,可是沒有Bscroll的滾動事件。
<!--父組件模塊-->
<nav-list :navList="cityIndexList" @toElement="toElement" :flagText="flagText"></nav-list>
<!--子組件模塊-->
<div class="navbody">
<div class="navList" @touchstart.stop.prevent="start" @touchmove.stop.prevent="move">
<div :class="navClass(item)" :data-name="item" v-for="item in navList" :key="item">
{{item}}
</div>
< /div>
</div>
這部分html代碼量比較少,可是與其餘組件的聯動最多,好比點擊nav上的字母使頁面城市組件滾動到相應的位置了、在上面滑動實現頁面城市組件的持續滾動等。
在點擊nav上的字母使頁面城市組件滾動到相應的位置這個功能中,點擊觸發了touchstart這個事件:
start (e) {
let item = handleDomData(e.target, 'data-name') this.touch.start = e.touches[0].pageY this.touch.startIndex = getIndex(this.navList, item) this.scrollToElement(item) }
記錄第一次點擊的位置爲之後的滑動提供起點的高度,而且觸發scrollToElement事件,向上傳值,讓父組件的scroll滾動到相應的位置。
在滑動實現頁面城市組件的持續滾動這個功能在,觸發touchmove這個事件:
move (e) {
this.touch.end = e.touches[0].pageY let distance = this.touch.end - this.touch.start this.touch.endIndex = Math.min(Math.max(this.touch.startIndex + Math.floor((distance + 10) / 20), 0), 22) this.scrollToElement(this.navList[this.touch.endIndex]) }
經過滾動過程當中的距離量計算當前所處的字母,並上傳改字母,讓父組件的scroll滾動到相應的位置。
在這個組件中,咱們引入了兩個js函數,分別是start中的handleDomData和getIndex
// 獲取或者給dom屬性賦值
export function handleDomData (el, name, val) {
if (val) { return el.setAttribute(name, val) } else { return el.getAttribute(name) } } // 獲取每個字母在數組中對應的index export function getIndex (arr, query) { let key arr.map((val, index) => { if (val === query) { key = index return false } }) return key }
這個小東西不是一個組件,可是有必定的功能,所以放在了這裏。代碼超簡單,就是接受兩個參數,是否顯示和顯示啥:
<transition name="flag"> <div class="nowFlag" v-if="flag">{{flagText}}</div> </transition>
是否顯示這個參數來自與scroll基礎組件的三個事件:
// 監聽scroll事件
if (this.listenScroll) {
// 滾動開始時觸發
this.scroll.on('scrollStart', () => { this.$emit('scrollStore', true) }) // pos爲位置參數 this.scroll.on('scroll', (pos) => { this.$emit('distance', Math.abs(pos.y)) this.$emit('scrollStore', true) }) // 滾動結束 this.scroll.on('scrollEnd', () => { this.$emit('scrollStore', false) }) }
this.listenScroll這個參數咱們在搜索列表上不調用,所以默認爲false,只有在主頁面時傳true。觸發時監聽scroll組件的活動狀況,好比滾動開始時上傳true,正在滾動中傳true,結束時傳false來控制卡片的顯示與隱藏。
卡片上面的字時根據滾動到的距離計算得出的:
// 根據滑動距離顯示字母牌上的字
distance (val) {
for (let i = 0, len = this.arrHeight.length; i < len; i++) { if (val < this.arrHeight[i]) { this.flagText = this.cityIndexList[i] return false } } } // 高度數組來源 // 計算連接每一部分的高度 export function getDistance (arr) { let titleHeight = 30 let itemHeight = 35 let distanceArr = [] arr.map((item) => { distanceArr.push(titleHeight + itemHeight * item[1].length) }) return distanceArr }
獲得的字母除了在這個卡片使用還會傳入navList組件中,實現當前所處字母的樣式的區別。
感受寫的腦殼疼,這個城市選擇組件的形式被應用於各類app和網站,是繼省市二級聯動以後城市選擇功能的實現形式。邏輯頗多,大多在上面被提到。
項目也上傳github,地址爲:https://github.com/lunlunshiwo/ChooseCity(另還有無側欄直接滾動版版:https://github.com/lunlunshiwo/Choose-City-no-nav-list-,使用方法爲替換相應文件便可)。使用方式爲先用node起一個基於express的服務(指令爲——node .\playDate.js,下載位置:https://github.com/lunlunshiwo/ChooseCityServe),再運行vue-cli(指令爲npm run dev)。
至於如何起兩個服務,自行參考cmd和power shell。
碼字不易,且看且珍惜。
原創博客,若侵犯貴司的利益,請私信我刪除。
若以爲不錯,求個贊和github的star。
今天有一個博友向我說了一個錯誤,就是個人npm run dev 以後會報錯:94% asset optimization[copy-webpack-plugin] WARNING - unable to locate 'D:\blog\ChooseCity-master\static' at 'D:\blog\ChooseCity-master\static'。大致意思是少static的文件夾,這個文件夾通常會放一些不會改動的文件,我此次項目沒有使用,而且上傳的時候一直沒法上傳,所以就沒有上傳。沒想到給各位博友帶來不便了,很抱歉。
我如今在github上修正了,你能夠從新下載一邊,也能夠新建一個名字爲static的文件夾便可。