Vue移动端

基于koa2和vue实现qq音乐wap站点

2019-02-21  本文已影响34人  liujunyan

前言

前段时间一直在捣鼓小程序,感觉都快不晓得wap怎么写了,赶紧找个项目练练手。之前基于vue写过一个商城,很多东西木有处理好,比如尺寸单位的适配,vuex在组件间的通信,页面切换动画等等,于是乎,这次还是基于vue,做个基于qq音乐的小项目,尽量贴近app的体验。这里打个广告,慕课上有个音乐app的课程,看课程导学感觉不错,只是没舍得花那百来块钱😰😰😰,不过作者很贴心的给出了成品链接,emmmmmm~设计图就是它了!开干😎😎😎
 
 

demo

开始之前先贴下预览~哈哈

技术栈

项目目录结构

image.png

目录主要分为前端页面和后端转发逻辑

前端页面基于vue-cli3.0初始化,宝宝我选的是pwa的模式,虽然好像做完后感觉没怎么用上哈哈哈哈大家按自己口味初始化就好。

下面贴一下我初始化的时候使用的选项配置:

image.png
image.png image.png

前期准备工作

这里先不展开src目录,我们先倒腾一下开始编码前代码规范的工作,好处嘛,emmmmm,yy不出来不说了 ~哈哈哈哈

[*.{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

 

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,主要是为了可以内置一些页面管理参数和动画处理的默认逻辑

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以便可以内置一些逻辑,达到的效果是将这个类useVue的时候,页面自动就有了切换时参数的管理以及动画处理的逻辑。

同事,为了可以像使用vue-router一样使用这个子类,需要添加静态的install方法,以便可以使用Vue.use(PageRouter)的方式被useVue中。(顺带提一嘴,执行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_enterslide_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
上一篇下一篇

猜你喜欢

热点阅读