标签:bug rank 改变 function 初始 webpack 轻松 += http
此应用的全部数据来自 QQ音乐,利用 axios 结合 node.js 代理后端请求抓取
全局通用的应用级状态使用 vuex 集中管理
全局引入 fastclick 库,消除 click 移动浏览器 300ms 延迟
页面是响应式的,适配常见的移动端屏幕,采用 flex 布局
因为这个变量不需要观测它的变化,因此不用定义在 data 里,这样也会对性能有所优化
需要监测这个数据变化的时候,放在 data() 里,会给数据添加 getter 和 setter
生命周期钩子函数,比如 mounted 是先触发子组件的 mounted,再会触发父组件的 mounted,但是对于 created 钩子,又会先触发父组件,再触发子组件。
如果组件有计数器,在组件销毁时期要记得清理,好习惯
一般来说 JS 线程执行完毕后一个 Tick 的时间约17ms内 DOM 就可以渲染完毕所以课程中 setTimeout(fn, 20) 是非常稳妥的写法
@import "~common/scss/mixin";
@import "~common/scss/variable";
从触摸按钮到触发点击事件,移动端浏览器会等待接近300ms,原因是浏览器会等待以确定你是否执行双击事件
<meta name="viewport" content="width=device-width, initial-scale=1">
jsonp:
XHR:
利用 BScroll
BScroll 设置 loop 会自动 clone 两个轮播插在前后位置
如果轮播循环播放,是前后各加一个轮播图保证无缝切换,所以需要再加两个宽度
if (this.loop) { width += 2 * sliderWidth }
初始化 dots 要在 BScroll 克隆插入两个轮播图之前
dots active状态 是通过判断 currentIndex 与 index 是否相等
currentIndex 更新是通过获取 scroll 当前 page,BScroll 提供了 api 方便调用
this.currentPageIndex = this.scroll.getCurrentPage().pageX
为了保证改变窗口大小依然正常轮播,监听窗口 resize 事件,重新渲染轮播图
window.addEventListener(‘resize‘, () => { if (!this.scroll || !this.scroll.enabled) return clearTimeout(this.resizeTimer) this.resizeTimer = setTimeout(() => { if (this.scroll.isInTransition) { this._onScrollEnd() } else { if (this.autoPlay) { this._play() } } this.refresh() }, 60) })
在切换 tab 相当于 切换了 keep-alive 的组件
轮播会出问题,需要手动帮助执行,利用了 activated , deactivated 钩子函数
activated() { this.scroll.enable() let pageIndex = this.scroll.getCurrentPage().pageX this.scroll.goToPage(pageIndex, 0, 0) this.currentPageIndex = pageIndex if (this.autoPlay) { this._play() } }, deactivated() { this.scroll.disable() clearTimeout(this.timer) }
实测,首次打开网页并不会执行 activated,只有在之后切换 tab ,切回来才会执行
在组件销毁之前 beforeDestroy 销毁定时器是好习惯,keep-alive 因为是将组件缓存了,所以不会触发
beforeDestroy() { this.scroll.disable() clearTimeout(this.timer) }
简单设置一下 Referer, Host,让别人直接通过浏览器抓到你的接口
但是这种方式防不了后端代理的方式
前端 XHR 会有跨域限制,后端发送 http 请求则没有限制,因此可以伪造请求
axios 可以在浏览器端发送 XMLHttpRequest
请求,在服务器端发送 http
请求
(在项目编写阶段,可以将后端代理请求写在 webpack 的 dev 文件的 before 函数内)
before(app) { app.get(‘/api/getDiscList‘, function (req, res) { const url = ‘https://c.y.qq.com/splcloud/fcgi-bin/fcg_get_diss_by_tag.fcg‘ axios.get(url, { headers: { referer: ‘https://c.y.qq.com/‘, host: ‘c.y.qq.com‘ }, params: req.query }).then((response) => { res.json(response.data) // axios 返回的数据在 response.data,要把数据透传到我们自定义的接口里面 res.json(response.data) }).catch((e) => { console.log(e) }) }); }
定义一个路由,get 到一个 /api/getDiscList
接口,通过 axios 伪造 headers,发送给QQ音乐服务器一个 http 请求,还有 param 参数。
得到服务端正确的响应,通过 res.json(response.data)
返回到浏览器端
另外 因为是 http 请求数据,是ajax,所以 format 参数要将原本接口的 jsonp 改为 json
大公司怎么防止被恶意代理呢?当你的访问量大的时候,出口ip会被查到获取封禁,还有一种就是参数验签,也就是请求人家的数据必须带一个签名参数,然后这个签名参数是很难拿到的这个正确的签名,从而达到保护数据的目的
当然,获取的数据并不能直接拿来用,需要做进一步的规格化,达到我们使用的要求,所以在这方面单独封装了一个 class 来处理这方面的数据,具体请看src/common/js/song.js
左侧 icon 固定大小,flex: 0 0 60px
flex 属性是 flex-grow
, flex-shrink
和 flex-basis
的简写,默认值为 0 1 auto。后两个属性可选。
flex-grow
属性定义项目的放大比例,默认为 0,即如果存在剩余空间,也不放大。flex-shrink
属性定义了项目的缩小比例,默认为 1,即如果空间不足,该项目将缩小。flex-basis
属性定义了在分配多余空间之前,项目占据的主轴空间(main size)。浏览器根据这个属性,计算主轴是否有多余空间。它的默认值为auto,即项目的本来大小。右侧 text 区块 自适应占据剩下的空间,并且内部也采用 flex,使用 flex-direction: column; justify-content: center;
来达到纵向居中排列
Scroll 初始化但却没有滚动,是因为初始化时机不对,必须保证数据到来,DOM 成功渲染之后 再去进行初始化
可以使用父组件 给 Scrol组件传 :data
数据,Scroll 组件自己 watch
这个 data,有变化就立刻 refesh 滚动
新版本 BScroll 已经自己实现检测 DOM 变化,自动刷新,大部分场景下无需传 data 了
所以也就 无需监听 img 的 onload 事件 然后执行 滚动刷新 了
<img @load="loadImage" class="needsclick" :src="item.picUrl">
loadImage() { if (!this.checkloaded) { this.checkloaded = true this.$refs.scroll.refresh() } }
歌手页面的结构是 热门、 A-Z
的顺序排列,但抓取的接口数据只是 100条常见的歌手,并且是乱序的,但我们可以利用接口的 Findex 进行数据的重构
首先可以定义一个 map 结构
let map = {
hot: {
title: HOT_NAME,
item: []
}
}
接着遍历得到的数据,将前10条添加到热门 hot 里
然后查看每条的 Findex ,如果 map[Findex] 没有,创建 map[Findex] push 进新条目,如果 map[Findex] 有,则向其 push 进新条目
list.forEach((item, index) => { if (index < HOT_SINGER_LEN) { map.hot.item.push(new SingerFormat({ id: item.Fsinger_mid, name: item.Fsinger_name, })) } const key = item.Findex if (!map[key]) { map[key] = { title: key, items: [] } } map[key].items.push(new SingerFormat({ id: item.Fsinger_mid, name: item.Fsinger_name })) })
这样就得到了一个 符合我们基本预期的 map 结构,但是因为 map 是一个对象,数据是乱序的,Chrome 控制台在展示的时候会对 key 做排序,但实际上我们代码并没有做。
所以还要将其进行排序,这里会用到 数组的 sort 方法,所以我们要先把 map对象 转为 数组
let hot = [] let ret = [] let un = [] for (let key in map) { let val = map[key] if (val.title.match(/[a-zA-z]/)) { ret.push(val) } else if (val.title === HOT_NAME) { hot.push(val) } else { un.push(val) } } ret.sort((a, b) => { return a.title.charCodeAt(0) - b.title.charCodeAt(0) }) return hot.concat(ret, un)
根据 title 字母的 Unicode 编码大小排序的(比如:‘A‘.charCodeAt(0)=65;‘B‘.charCodeAt(0)=66)然后就a,b,c,d...的顺序了
因为 shortcut 整体的高度是不确定的,所以采用的是 top:50%
之后,transform: translateY(-50%);
这样就能动态的根据内容高度而垂直居中
利用了 BScroll 的 api ,scrollToElement
第一次点击触碰 shortcut ,获取点击具体锚点的 index 值,记录触碰位置的 index ,利用 scrollToElement ,滚动至相应 index 的区块
而之后,滑动锚点实现滚动是利用 touchmove 事件,将两次触碰的的位置计算值变成 delta 差值:变成改变后的锚点区块 index 值,再将首次触碰的 index 值 + 改变的 delta 值,再利用 scrollToElement ,滚动至相应的区块
onShortcutTouchStart(e) { let anchorIndex = getData(e.target, ‘index‘) // 获取 点击具体锚点的 index 值 let firstTouch = e.touches[0] // 第一次触碰的位置 this.touch.y1 = firstTouch.pageY // 保存 第一次触碰的位置的Y值 this.touch.anchorIndex = anchorIndex // 保存 第一次触碰时的锚点 index 值 this._scrollTo(anchorIndex) }, onShortcutTouchMove(e) { let firstTouch = e.touches[0] this.touch.y2 = firstTouch.pageY let delta = (this.touch.y2 - this.touch.y1) / ANCHOR_HEIGHT | 0 // 两次触碰 Y 轴的偏移锚点值 let anchorIndex = +this.touch.anchorIndex + delta // 获取 偏移了多少 index 值 ,因为 anchorIndex 是字符串,所以要转成数字再相加 this._scrollTo(anchorIndex) }, _scrollTo(index) { this.$refs.listview.scrollToElement(this.$refs.listGroup[index], 200) }
<Scroll class="listview" ref="listview"> <!--歌手列表--> <ul> <li v-for="group in data" class="list-group" ref="listGroup"> <h2 class="list-group-title">{{group.title}}</h2> <!--首字母条目--> <ul> <li v-for="item in group.items" class="list-group-item"> <img :src="item.avatar" class="avatar"> <span class="name">{{item.name}}</span> </li> </ul> </li> </ul> <div class="list-shortcut" @touchstart="onShortcutTouchStart" @touchmove.stop.prevent="onShortcutTouchMove"> <ul> <li v-for="(item, index) in shortcutlist" :data-index="index" class="item"> {{item}} </li> </ul> </div> </Scroll>
if (this.listenScroll) { let self = this this.scroll.on(‘scroll‘, (pos) => { // 实时监测滚动事件,派发事件:Y轴距离 self.$emit(‘scroll‘, pos) }) }
@scroll="scroll"
将 pos.y 存在 this.scrollY
scroll(pos) { this.scrollY = pos.y // 实时获取 BScroll 滚动的 Y轴距离 }
watch: { data() { // 延时,确保DOM渲染之后执行,通常是nextTick,这里用setTimeout是为了兼容更低 setTimeout(() => { this._calculateHeight() }, 20) }, // 这里的 scrollY 是当前组件上的,和 BScroll 的并不是一个 scrollY(newY) { const listHeight = this.listHeight // 1. 当滚动至顶部以上 if (newY > 0) { this.currentIndex = 0 return } // 2. 当在中间部分滚动,length之所以 -1 是因为 当初高度列表定义必须多一个 for (let i = 0; i < listHeight.length - 1; i++) { let height1 = listHeight[i] let height2 = listHeight[i + 1] if (-newY >= height1 && -newY < height2) { this.currentIndex = i this.diff = height2 + newY // height 上限 - newY 的值 return } } // 3. 当滚动至底部,且 newY 大于最后一个元素的上限 this.currentIndex = listHeight.length - 2 } }
_calculateHeight
函数实现_calculateHeight() { this.listHeight = [] const list = this.$refs.listGroup let height = 0 this.listHeight.push(height) for (let i = 0; i < list.length; i++) { let item = list[i] height += item.clientHeight this.listHeight.push(height) } }
:class="{‘current‘: currentIndex === index}"
这里的 Vue 用法提示: watch 的 scrollY(newY){} 当我们在 Vue 里修改了在 data 里定义的变量,就会出发这个变量的 setter,经过一系列的处理,会触发 watch 的回调函数,也就是 scrollY(newY) {} 这里的函数会执行,同时,newY 就是我们修改后的值。 scrollY 是定义在 data 里的,列表滚动的时候,scroll 事件的回调函数里有修改 this.scrollY,所以能 watch 到它的变化。 watch 的回调函数的第一个参数表示变化的新值 滚动固定标题 效果实现 在中间部分滚动时,会不断设置 diff 值,每个区块的高度上限(也就是底部)减去 Y轴偏移的值
this.diff = height2 + newY // 就是 height 上限 - newY 的值
watch 检测 diff 变化,判断如果 diff>0 且 小于 title 块的高度,设为差值,否则为0
再将 fixed 的 title 块 translate 偏移
diff(newVal) { let fixedTop = (newVal > 0 && newVal < TITLE_HEIGHT) ? newVal - TITLE_HEIGHT : 0 if (this.fixedTop === fixedTop) return // 判断如果两个title区块没有碰到,是不会触发 DOM 操作的 this.fixedTop = fixedTop this.$refs.fixed.style.transform = `translate3d(0,${fixedTop}px,0)` }
singer page 页面 引入 singer-detail 二级路由
index.js 路由里配置
{ path: ‘/singer‘, component: Singer, children: [ { path: ‘:id‘, // 表示 id 为变量 component: SingerDetail } ] }
singer.vue 里设定跳转路由 this.$router.push({})
html:
<router-view></router-view>
js:
selectSinger(singer){ this.$router.push({ path: `/singer/${singer.id}` }) }
Vuex 教程见:Vuex
通常的流程为:
在定义 mutations 之前 要先定义 mutation-types (通常为动词+名词)
actions.js
通常是两种操作
selectItem(item) { this.$emit(‘select‘, item) },
singer.id
SET_SINGER
的 commitselectSinger(singer) { this.$router.push({ path: `/singer/${singer.id}` }) this.setSinger(singer) }, ...mapMutations({ // 语法糖,‘...‘将多个对象注入当前对象 setSinger: ‘SET_SINGER‘ // 将 this.setSinger() 映射为 this.$store.commit(‘SET_SINGER‘) })
mapMutations (语法糖) 映射 mutations ,this.setSinger(singer)
相当于执行 this.$store.commit(‘SET_SINGER‘)
(singer 为 mutation 的第二个参数)
而 mutations 内 SET_SINGER
的逻辑为
[types.SET_SINGER](state, singer) { state.singer = singer }
computed: { ...mapGetters([ ‘singer‘ ]) }
getters 内 singer
的逻辑为
singer = state => state.singer
主要是 监听滚动距离,根据不同的距离条件发生不同的效果
mounted() { this.imageHeight = this.$refs.bgImage.clientHeight this.$refs.list.$el.style.top = `${this.imageHeight}px` // 对于 Vue 组件,this.$refs.xxx 拿到的是 Vue 实例,所以需要再通过 $el 拿到真实的 dom this.minTransalteY = -this.imageHeight + RESERVED_HEIGHT }, watch: { scrollY(newY) { let translateY = Math.max(this.minTransalteY, newY) // 最远滚动改变的距离就是 minTransalteY let zIndex = 0 let scale = 1 const percent = Math.abs(newY / this.imageHeight) this.$refs.layer.style.transform = `translate3d(0,${translateY}px,0)` this.$refs.layer.style.webkitTransform = `translate3d(0,${translateY}px,0)` if (newY < this.minTransalteY) { zIndex = 10 this.$refs.bgImage.style.paddingTop = 0 this.$refs.bgImage.style.height = `${RESERVED_HEIGHT}px` } else { this.$refs.bgImage.style.paddingTop = ‘70%‘ this.$refs.bgImage.style.height = 0 } if (newY > 0) { scale = 1 + percent zIndex = 10 } this.$refs.bgImage.style.zIndex = zIndex this.$refs.bgImage.style.transform = `scale(${scale})` this.$refs.bgImage.style.webkitTransform = `scale(${scale})` } }
let elementStyle = document.createElement(‘div‘).style let vendor = (() => { let transformNames = { webkit: ‘webkitTransform‘, Moz: ‘MozTransform‘, O: ‘OTransform‘, ms: ‘msTransform‘, standard: ‘transform‘ } for (let key in transformNames) { if (elementStyle[transformNames[key]] !== undefined) return key } return false })() export function prefixStyle(style) { if (vendor === false) return false if (vendor === ‘standard‘) return style return vendor + style.charAt(0).toUpperCase() + style.substr(1) }
方法就是判断创建的 div 样式是否有相应的前缀样式,如果有,则返回前缀样式的key,也就是需要的 前缀
-webkit-transform-origin
,JS操作时,不能写 -
,可以采用驼峰写法,也就是样式首字母大写)把播放器组件放在 App.vue 下,因为它是一个跟任何路由都不相关的东西。在任何路由下,它都可以去播放。切换路由并不会影响播放器的播放。
点击 歌手/歌单 都会进入详情页,详情页 created() 会根据点击的歌手请求相应的数据,然后利用 _normalizeSongs 将数据整理,其中很重要的函数是 createSong ,生成自定义 song 类,方便以后读取
animation-play-state
animation-play-state CSS 属性定义一个动画是否运行或者暂停。可以通过查询它来确定动画是否正在运行。另外,它的值可以被设置为暂停和恢复的动画的重放。
恢复一个已暂停的动画,将从它开始暂停的时候,而不是从动画序列的起点开始在动画。
修复BUG:ios下safari与chrome浏览器,animation-play-state样式失效 #60
点击暂停播放的时候,歌曲的图片会继续转动,导致的原因是因为animation-play-state:paused这个样式失效了
修复具体代码
核心代码:
/** * 计算内层Image的transform,并同步到外层容器 * @param wrapper * @param inner */ syncWrapperTransform(wrapper, inner) { if (!this.$refs[wrapper]) return let imageCdWrapper = this.$refs[wrapper] let image = this.$refs[inner] let wTransform = getComputedStyle(imageCdWrapper)[transform] let iTransform = getComputedStyle(image)[transform] imageCdWrapper.style[transform] = wTransform === ‘none‘ ? iTransform : iTransform.concat(‘ ‘, wTransform) }
解决快速切换歌曲引发的错误 这个错误是由于切换的太快,歌曲并未获取到播放地址,而提前播放 利用了H5新api: canplay 当终端可以播放媒体文件时触发该canplay事件,估计加载足够的数据来播放媒体直到其结束,而不必停止以进一步缓冲内容。 利用这个api,在audio上监听 canplay 派发的事件,做成标志位 后来 api 改至 playing 播放器 进度条 功能 normal 的长形进度条 在 progress 上监听 touchstart, touchmove, touchend 三个事件 touchstart: 获取第一次点击的横坐标和已播放的进度条长度 touchmove: 获取移动后的横坐标,并定义 delta 为 移动后坐标 - 第一次点击的横坐标 设置 偏移量 offsetWidth 为 已播放的进度条长度 + delta 在去设置 progress 和 progressBtn 的宽度和transform 量都为 offsetWidth touchend: 一些组件特有的逻辑,和进度条不太相关暂不赘述 而点击任意位置,移动进度按钮,则是通过为 progress 进度条添加点击事件
progressClick(e) { this._offset(e.offsetX - progressBtnWidth / 2) this._triggerPercent() }
利用了 SVG 实现,其中有两个圆,一个是背景圆形,另一个为已播放的圆形进度
<div class="progress-circle">
<svg :width="radius" :height="radius" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg">
<circle class="progress-background" r="50" cx="50" cy="50" fill="transparent"/>
<circle class="progress-bar" r="50" cx="50" cy="50" fill="transparent" :stroke-dasharray="dashArray"
:stroke-dashoffset="dashOffset"/>
</svg>
<slot></slot>
</div>
迷你播放器暂停状态,进入全屏,按钮在进度条最左边
progress-bar 组件要 watch 下 fullScreen,在进入全屏的时候调用一下 移动按钮函数
获取歌词,虽然我们约定返回数据是 json,但QQ音乐 返回的是依然是 jsonp,所以我们需要做一层数据的处理
const reg = /^\w+\(({.+})\)$/
就是将返回的jsonp格式摘取出我们需要的json字段
ret = JSON.parse(matches[1])
将正则分组(就是正则括号内的内容)捕获的json字符串数据 转成 json 格式
然后我们在 player 组件中监听 currentSong 的变化,获取 this.currentSong.getLyric()
axios.get(url, { headers: { referer: ‘https://c.y.qq.com/‘, host: ‘c.y.qq.com‘ }, params: req.query }).then((response) => { let ret = response.data if (typeof ret === ‘string‘) { const reg = /^\w+\(({.+})\)$/ const matches = ret.match(reg) if (matches) { ret = JSON.parse(matches[1]) } } res.json(ret) })
然后我们得到的返回数据的是 base64 的字符串,需要解码,这里用到了第三方库: js-base64
(我们这次用的是QQ音乐pc版的歌词,需要解码base64,而移动版的QQ音乐是不需要的)
this.lyric = Base64.decode(res.lyric)
之后利用第三方库: js-lyric
,解析我们的歌词,生成方便操作的对象
getLyric() { this.currentSong.getLyric() .then(lyric => { this.currentLyric = new Lyric(lyric) }) }
当前歌曲的歌词高亮是利用 js-lyric
会派发的 handle 事件
this.currentLyric = new Lyric(lyric, this.handleLyric)
js-lyric
会在每次改变当前歌词时触发这个函数,函数的参数为 当前的 lineNum 和 txt
let middleLine = isIphoneX() ? 7 : 5 // 鉴于iphonex太长了,做个小优化 if (lineNum > middleLine) { let lineEl = this.$refs.lyricLine[lineNum - middleLine] this.$refs.lyricList.scrollToElement(lineEl, 1000) } else { this.$refs.lyricList.scrollTo(0, 0, 1000) }
通过监听 middle 的 三个 touch 事件
offsetWidth
是为了计算歌词列表的一个偏移量的,首先它的偏移量不能大于0,也不能小于 -window.innerWidth
。
left 是根据当前显示的是 cd 还是歌词列表初始化的位置,如果是 cd,那么 left 为 0 ,歌词是从右往左拖的,deltaX 是小于 0 的,所以最终它的偏移量就是 0+deltaX
;如果已经显示歌词了,那么 left 为 -window.innerWidth
,歌词是从左往右拖,deltaX 是大于 0 的,所以最终它的偏移量就是 -window.innerWidth + deltaX
。
middleTouchStart(e) { this.touch.initiated = true this.touch.startX = e.touches[0].pageX this.touch.startY = e.touches[0].pageY }, middleTouchMove(e) { if (!this.touch.initiated) return const deltaX = e.touches[0].pageX - this.touch.startX const deltaY = e.touches[0].pageY - this.touch.startY if (Math.abs(deltaY) > Math.abs(deltaX)) { return } const left = this.currentShow === ‘cd‘ ? 0 : -window.innerWidth const offsetWidth = Math.min(0, Math.max(-window.innerWidth, left + deltaX)) this.touch.percent = Math.abs(offsetWidth / window.innerWidth) console.log(this.touch.percent) this.$refs.lyricList.$el.style[transform] = `translate3d(${offsetWidth}px,0,0)` this.$refs.lyricList.$el.style[transitionDuration] = 0 this.$refs.middleL.style.opacity = 1 - this.touch.percent this.$refs.middleL.style[transitionDuration] = 0 }, middleTouchEnd() { let offsetWidth, opacity // 从右向左滑 的情况 if (this.currentShow === ‘cd‘) { if (this.touch.percent > 0.1) { offsetWidth = -window.innerWidth opacity = 0 this.currentShow = ‘lyric‘ } else { offsetWidth = 0 opacity = 1 } } else { // 从左向右滑 的情况 if (this.touch.percent < 0.9) { offsetWidth = 0 opacity = 1 this.currentShow = ‘cd‘ } else { offsetWidth = -window.innerWidth opacity = 0 } } const durationTime = 300 this.$refs.lyricList.$el.style[transform] = `translate3d(${offsetWidth}px,0,0)` this.$refs.lyricList.$el.style[transitionDuration] = `${durationTime}ms` this.$refs.middleL.style.opacity = opacity this.$refs.middleL.style[transitionDuration] = `${durationTime}ms` }
Vue 按需加载路由:
当打包构建应用时,Javascript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
结合 Vue 的异步组件和 Webpack 的代码分割功能,轻松实现路由组件的懒加载。
const Foo = () => Promise.resolve({ /* 组件定义对象 */ })
import(‘./Foo.vue‘) // 返回 Promise
在我们的项目中的 router/index.js 是这样定义的:
// Vue 异步加载路由 // 引入5个 一级路由组件 const Recommend = () => import(‘components/recommend/recommend‘) const Singer = () => import(‘components/singer/singer‘) const Rank = () => import(‘components/rank/rank‘) const Search = () => import(‘components/search/search‘) const UserCenter = () => import(‘components/user-center/user-center‘) // 二级路由组件 const SingerDetail = () => import(‘components/singer-detail/singer-detail‘) const Disc = () => import(‘components/disc/disc‘) const TopList = () => import(‘components/top-list/top-list‘)
标签:bug rank 改变 function 初始 webpack 轻松 += http
原文地址:https://www.cnblogs.com/catbrother/p/9180984.html