前言
前段时间一直在捣鼓小程序,感觉都快不晓得wap怎么写了,赶紧找个项目练练手。之前基于vue写过一个商城,很多东西木有处理好,比如尺寸单位的适配,vuex在组件间的通信,页面切换动画等等,于是乎,这次还是基于vue,做个基于qq音乐的小项目,尽量贴近app的体验。这里打个广告,慕课上有个音乐app的课程,看课程导学感觉不错,只是没舍得花那百来块钱😰😰😰,不过作者很贴心的给出了成品链接,emmmmmm~设计图就是它了!开干😎😎😎
demo
开始之前先贴下预览~哈哈
技术栈
- vue
- vuex
- vue-router
- node (项目中所有的接口都基于node做转发,以便项目可以在跨域的情况下获得qq音乐的数据)
- koa2
项目目录结构
image.png目录主要分为前端页面和后端转发逻辑
前端页面基于vue-cli3.0初始化,宝宝我选的是pwa的模式,虽然好像做完后感觉没怎么用上哈哈哈哈大家按自己口味初始化就好。
下面贴一下我初始化的时候使用的选项配置:
- 使用手动选择的选项
image.png
- 这里代码检测的选项我勾选了两个
-
最终配置
image.png
前期准备工作
这里先不展开src目录,我们先倒腾一下开始编码前代码规范的工作,好处嘛,emmmmm,yy不出来不说了 ~哈哈哈哈
-
在根目录添加
.editorconfig
文件以便规范缩进格式等,下面是我用的一些格式:
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 4
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 100
-
添加
eslint
做必要的代码格式规范项目中我使用的是airbnb
的eslint
,具体可以参考它的npm
:
https://www.npmjs.com/package/eslint-config-airbnb-base
这里要注意的是它还有个很相近的叫eslint-config-airbnb
,区别是这个版本包括了react
,这里因为我没有用到react
, 所以就用了eslint-config-airbnb-base
这个版本。
安装到项目里也很简单,如果使用的npm版本在5以上,可以直接运行:
npx install-peerdeps --dev eslint-config-airbnb-base
然后需要在项目根目录配置.eslintrc.js
文件 (主要在extends选项中加上'@vue/airbnb'
)
module.exports = {
root: true,
env: {
node: true,
},
extends: [
'plugin:vue/essential',
'@vue/airbnb',
],
rules: {
'indent': [
'error',
4
],
'max-len': ['error', 120],
'prefer-destructuring': ['error', {
'VariableDeclarator': {
object: false
}}
],
'no-plusplus': [
'error', {
'allowForLoopAfterthoughts': true
}
],
'no-unused-expressions': ['error', {
'allowTernary': true,
'allowShortCircuit': true
}],
"no-underscore-dangle": ["off", "always"],
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'linebreak-style': 'off',
},
parserOptions: {
parser: 'babel-eslint',
},
};
配置中可以根据需要覆盖一些原来的规则,比如原来的规则要求一行代码最大长度不能超过80个字符,想想宝宝我屏幕好像有点点宽,80个字符就被逼着换行好像有点不划算,就把它变成120个了😑
然后为了让自己阔以更好的遵守配置的规则,可以在提交代码对代码做一个检测,如果有不符合规则的代码,先尝试自动进行修复,不行的就报个错,改好了才能提交(心情复杂.gif),这里宝宝我在上面初始化项目的时候就选择了提交代码的时候做检查的选项,具体看大佬们需求
src目录结构
image.png
样式文件编写
assets/base.styl
页面的基础样式包括字体等
@import 'variable.styl'
html, body
line-height 1
font-family 'PingFang SC', 'STHeitiSC-Light', 'Helvetica-Light', arial, sans-serif, 'Droid Sans Fallbask'
user-select none
-webkit-tap-highlight-color transparent
background $color-background
color: $color-text
font-size: $font-size-medium
button
display flex
justify-content center
align-items center
border none
outline none
background none
assets/stylusanimate.styl
编写页面切换时的动画样式,下面解释路由模块编写的时候会托词提到
.normal_enter
transform translateX(20px)
opacity 0
.normal_leave-to
transform translateX(-20px)
opacity 0
.normal_enter-active
transition all .24s .24s
.normal_leave-active
transition all .24s
.pre_normal_enter
transform translateX(-20px)
opacity 0
.pre_normal_leave-to
transform translateX(20px)
opacity 0
.pre_normal_enter-active
transition all .24s .24s
.pre_normal_leave-active
transition all .24s
.slide_enter,
.slide_leave-to
transform translate3d(100%, 0, 0)
.slide_enter-to,
.slide_leave
transform: translate3d(0, 0, 0)
.slide_enter-active,
.slide_leave-active
transition all .24s
assets/reset.styl
重置页面元素的样式
/**
* Eric Meyer's Reset CSS v2.0 (http://meyerweb.com/eric/tools/css/reset/)
* http://cssreset.com
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header,
menu, nav, output, ruby, section, summary,
time, mark, audio, video, input
margin: 0
padding: 0
border: 0
font-size: 100%
font-weight: normal
vertical-align: baseline
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, menu, nav, section
display: block
body
line-height: 1
blockquote, q
quotes: none
blockquote:before, blockquote:after,
q:before, q:after
content: none
table
border-collapse: collapse
border-spacing: 0
/* custom */
a
color: #7e8c8d
-webkit-backface-visibility: hidden
text-decoration: none
li
list-style: none
body
-webkit-text-size-adjust: none
-webkit-tap-highlight-color: rgba(0, 0, 0, 0)
assets/variable.styl
定义一些统一的样式变量
// 颜色定义
$color-background = #222;
$color-background-d = rgba(0, 0, 0, 0.3);
$color-background-dd = rgba(0, 0, 0, 0.6);
$color-background-ddd = rgba(0, 0, 0, 0.8);
$color-highlight-ld = #333;
$color-background-lowlight = rgba(255, 255, 255, .5);
$color-background-light = #fff;
$color-theme = #ffcd32;
$color-black = #000;
$color-text = #fff;
$color-text-l = rgba(255, 255, 255, 0.3);
$color-text-ll = rgba(255, 255, 255, 0.5);
$color-text-lll = rgba(255, 255, 255, 0.8);
//字体定义
$font-size-small = 10PX;
$font-size-small-x = 12PX;
$font-size-medium = 14PX;
$font-size-medium-x = 16PX;
$font-size-large = 18PX;
$font-size-large-x = 22PX;
assets/mixin.styl
定义一些通用的样式,在有需要的地方import进去使用
// 背景图片
bg-image($url)
background-image: url($url + "@2x.png")
@media (-webkit-min-device-pixel-ratio: 3),(min-device-pixel-ratio: 3)
background-image: url($url + "@3x.png")
// 不换行
no-wrap()
text-overflow: ellipsis
overflow: hidden
white-space: nowrap
// 扩展点击区域
extend-click()
position: relative
&:after
content: ''
position: absolute
top: -10px
left: -10px
right: -10px
bottom: -10px
z-index 2
extend-click-absolute()
position: absolute
&:after
content: ''
position: absolute
top: -10px
left: -10px
right: -10px
bottom: -10px
z-index 2
assets/index.styl
汇总stylus中定义的文件
@import './reset.styl';
@import './base.styl';
路由代码编写
这里没有直接使用vue-router,而是编写一个路由类继承vue-router,主要是为了可以内置一些页面管理参数和动画处理的默认逻辑
- 在
router
目录下添加page-router.js
,编写vue-router的子类
import Router from 'vue-router';
import PageManager from './page-manager';
class PageRouter extends Router {
constructor(obj) {
super(obj);
this._pm = PageManager;
super.beforeEach(PageManager._beforeEachProxy.bind(PageManager));
}
get pm() {
return this._pm;
}
static install(Vue, options) {
Router.install(Vue, options);
Vue.mixin({
data() {
return {
pm: PageManager,
};
},
});
}
beforeEach(fn) {
this._pm.setBeforeEach(fn);
}
}
export default PageRouter;
关键代码段是在coustructor
中要显示调用super.beforeEach
以便可以内置一些逻辑,达到的效果是将这个类use
到Vue
的时候,页面自动就有了切换时参数的管理以及动画处理的逻辑。
同事,为了可以像使用vue-router
一样使用这个子类,需要添加静态的install
方法,以便可以使用Vue.use(PageRouter)
的方式被use
到Vue
中。(顺带提一嘴,执行Vue.use(XXX)
的时候,Vue
会去执行定义在XXX
上的install
方法,这就是这里显示定义install
的原因)
install
方法中,我还添加了一个全局的混入,将PageManager
挂载到data
上,方便后续所有的组件都可以直接通过this.pm
的方式使用到。
然后我们看下page-manager.js
/**
* 管理页面,包括内容
* pageToken路由参数,根据当前token值判断页面是前进还是后退或者刷新,开新页时会在当前token值得基础上+1
* 管理页面跳转的过渡动画
*/
import Vue from 'vue';
const JUMP_WAY = {
RE_FRESH: 'Refresh',
NEXT: 'Next',
PREV: 'Prev',
};
const DEFAULT_ANIMATE = {
ENTER: 'normal_enter',
LEAVE: 'normal_leave',
};
const PageManager = new Vue({
data() {
return {
pageToken: 0,
enterClass: DEFAULT_ANIMATE.ENTER,
leaveClass: DEFAULT_ANIMATE.LEAVE,
jumpWay: JUMP_WAY.RE_FRESH,
};
},
methods: {
_beforeEachProxy(to, from, next) {
const _to = { ...to };
if (!_to.query || !_to.query.pageToken) { // 第一次打开当前路由页
_to.query = _to.query || {};
_to.replace = this.pageToken === 0;
_to.query.pageToken = +this.pageToken + 1;
next(_to);
return true;
}
// 通过点击浏览器前进后退或者刷新按钮触发的路由变化,根据pageToken判断是哪种跳转方式~并记录当前pageToken
this._updateJumpWay(to);
this._updateAnimate(to);
this.pageToken = to.query.pageToken;
if (this._beforeEach) {
this._beforeEach(to, from, next);
} else {
next();
}
return true;
},
_updateJumpWay(to) {
if (to.query.pageToken > this.pageToken) {
this.jumpWay = JUMP_WAY.NEXT;
} else if (to.query.pageToken === this.pageToken) {
this.jumpWay = JUMP_WAY.RE_FRESH;
} else {
this.jumpWay = JUMP_WAY.PREV;
}
},
_updateAnimate(to) {
const animate = (to.meta && to.meta.animate) || {};
const animateClass = {
enter: animate.enter || DEFAULT_ANIMATE.ENTER,
leave: animate.leave || DEFAULT_ANIMATE.LEAVE,
};
if (this.jumpWay === JUMP_WAY.PREV) {
// 从其他跳转方式切换到返回跳转方式
this.enterClass = `pre_${animateClass.enter}`;
this.leaveClass = `pre_${animateClass.leave}`;
} else if (this.jumpWay === JUMP_WAY.NEXT) {
this.enterClass = animateClass.enter;
this.leaveClass = animateClass.leave;
}
},
setBeforeEach(fn) {
if (fn && Object.prototype.toString.call(fn) === '[object Function]') {
this._beforeEach = fn;
}
},
setOverrideAnim(overrideAnim) {
this.enterClass = (overrideAnim && overrideAnim.enter) || DEFAULT_ANIMATE.ENTER;
this.leaveClass = (overrideAnim && overrideAnim.leave) || DEFAULT_ANIMATE.LEAVE;
},
},
});
export default PageManager;
这里主要是实现上面说的页面前进后退刷新和切换的时候动画过渡的内置逻辑的,同时为了使得状态的修改可以动态反映到页面,这里采用Vue
实例的方法来实现对应的逻辑(这里不得不赞一下这数据驱动的框架,只需要关心业务逻辑,剩下的,交给大Vue解决~哈哈哈哈哈)
这里我采用一个pageToken
的变量来处理页面前进后退和刷新,如果即将跳转的页面没有pageToken
参数,则拼接上重新跳转过去,页面切换有默认的动画效果,如果有哪个页面有特殊需求,可以在路由配置的meta字段配置,然后在assets/stylus/animate.styl
中编写相应的切换动画样式,这里也可以结合animate.css
,看具体需求,项目中我两种方式都用了,animate.css
我主要用在弹出层的动画上。
最后再看下index.js
:
import Vue from 'vue';
import PageRouter from './page-router';
const Rank = () => import(/* webpackChunkName: "rank" */ '@/views/rank/rank.vue');
const RankDetail = () => import(/* webpackChunkName: "rankDetail" */ '@/views/rank/rank-detail/rank-detail.vue');
Vue.use(PageRouter);
const router = new PageRouter({
mode: 'history',
base: process.env.BASE_URL,
routes: [{
path: '/rank',
component: Rank,
children: [
{
path: ':id',
component: RankDetail,
meta: {
animate: {
enter: 'slide_enter',
leave: 'slide_leave',
},
},
},
],
}],
});
router.beforeEach((to, from, next) => {
next();
});
router.onError((res) => {
console.log(res);
});
export default router;
export const PM = router.pm;
这里我省略了一些路由,主要举了一个排名页面的配置例子,对应上面提到的单独设置特殊页面的切换动画,这里排名页面的子路由我有一个滑动切换的需求,所以就配置了slide_enter
和slide_leave
的动画名称,对应assets/stylus/animate.styl
编写的样式为:
.slide_enter,
.slide_leave-to
transform translate3d(100%, 0, 0)
.slide_enter-to,
.slide_leave
transform: translate3d(0, 0, 0)
.slide_enter-active,
.slide_leave-active
transition all .24s
移动端适配处理
在开始编写业务代码之前,有必要先处理一下适配的问题,这方面的资料网上已经有很多了,不做过多的介绍,这里我使用的是基于“vm, vh”的适配方案,配置好后,开发体验就像在小程序上编写样式时写rpx
一样爽歪歪,750的设计稿上写着多少像素,直接写就好啦~啦啦啦啦,具体可以参考大漠老师关于适配的文章
https://www.w3cplus.com/mobile/vw-layout-in-vue.html
这里我有选择性的使用了其中一些,具体配置如下:
npm install postcss-px-to-viewport
然后在项目根目录新建一个postcss.config.js
文件,配置如下:
module.exports = {
plugins: {
autoprefixer: {},
'postcss-px-to-viewport': {
viewportWidth: 750, // (Number) The width of the viewport.
viewportHeight: 1334, // (Number) The height of the viewport.
unitPrecision: 3, // (Number) The decimal numbers to allow the REM units to grow to.
viewportUnit: 'vw', // (String) Expected units.
selectorBlackList: ['.ignore', '.hairlines'], // (Array) The selectors to ignore and leave as px.
minPixelValue: 1, // (Number) Set the minimum pixel value to replace.
mediaQuery: false, // (Boolean) Allow px to be converted in media queries.
},
},
};
另外还有点这样配置后程序中写的px都会被转为相应的vm,有的时候我们希望有些尺寸就是保持原来的px不变,处理方式有很多,上面配置文件中使用的selectorBlackList
是一种方式,但是如果一个元素里面有些尺寸是要响应式的,有些又是不要的,这种方式就不好处理了,比如一个div
的盒子,宽、高我需要它是响应的,但是里面的文字我希望他在所有的手机上都显示一样的大小,这个时候可以将文字的样式单位写成大写的PX
。嗯宝宝我在stylus文件夹中定义字体大小的变量的时候用的就是这个方法。现在就阔以愉快的编码啦
开始编写页面代码
因为项目代码比较多,一个一个文件贴出来可能很有点长,这里我就不贴了,对应的代码已经po到github,对一些编写时遇到的坑我会尽量说明
自定义页面切换样式的使用可以参看
App.vue
<template>
<div id="app">
<m-header />
<tab />
<transition
:enter-class="transitionClass.enter"
:enter-active-class="transitionClass.enterActive"
:enter-to-class="transitionClass.enterTo"
:leave-class="transitionClass.leave"
:leave-active-class="transitionClass.leaveActive"
:leave-to-class="transitionClass.leaveTo">
<keep-alive>
<router-view class="transition_view"></router-view>
</keep-alive>
</transition>
<x-player ref="xPlay" />
<toast ref="toast" />
</div>
</template>
<script>
import Vue from 'vue';
import { PM } from '@/router/index';
import Tab from '@/components/tab/tab.vue';
import MHeader from '@/components/m-header/m-header.vue';
import XPlayer from '@/components/x-player/x-player.vue';
import Toast from '@/base/toast/toast.vue';
export default {
computed: {
transitionClass() {
return {
enter: PM.enterClass,
enterActive: `${PM.enterClass}-active`,
enterTo: `${PM.enterClass}-to`,
leave: PM.leaveClass,
leaveActive: `${PM.leaveClass}-active`,
leaveTo: `${PM.leaveClass}-to`,
};
},
},
mounted() {
Vue.prototype.toast = this.$refs.toast; // 将吐司组件挂载到全局
Vue.prototype.xPlay = this.$refs.xPlay; // 将吐司组件挂载到全局
},
components: {
Tab,
MHeader,
XPlayer,
Toast,
},
};
</script>
<style lang="stylus">
@import "~assets/stylus/variable.styl"
@import "~assets/stylus/animate.styl"
.main
position fixed
width 100%
top 172px
bottom 0
background $color-background
overflow hidden
</style>
文章有点长,未完,待续。。。。。。。
后记
第一次敲大篇幅的文章,如果文章对小伙伴有帮助,欢迎随缘打赏👻
image.png
网友评论