-曾老湿, 江湖人称曾老大。
-笔者QQ:133411023、253097001
-笔者交流群:198571640
-笔者微信:z133411023


-多年互联网运维工作经验,曾负责过大规模集群架构自动化运维管理工作。
-擅长Web集群架构与自动化运维,曾负责国内某大型金融公司运维工作。
-devops项目经理兼DBA。
-开发过一套自动化运维平台(功能如下):

1)整合了各个公有云API,自主创建云主机。
2)ELK自动化收集日志功能。
3)Saltstack自动化运维统一配置管理工具。
4)Git、Jenkins自动化代码上线及自动化测试平台。
5)堡垒机,连接Linux、Windows平台及日志审计。
6)SQL执行及审批流程。
7)慢查询日志分析web界面。


底部导航(VueRouter)


确定每个页面的url
/money      记账
/labels     标签
/statistics 统计

// 默认进入 #/money
// 添加一个404页面

添加router

先设置一下typescript的格式化

router/index.ts

import Vue from 'vue';
import VueRouter, {RouteConfig} from 'vue-router';
import Money from '@/views/Money.vue';
import Labels from '@/views/Labels.vue';
import Statistics from '@/views/Statistics.vue';


Vue.use(VueRouter);

const routes: Array<RouteConfig> = [
    {
        path: '/',
        redirect: '/money'
    },
    {
        path: '/money',
        component: Money
    },
    {
        path: '/labels',
        component: Labels
    },
    {
        path: '/statistics',
        component: Statistics
    }
];

const router = new VueRouter({
    routes
});

export default router;

创建vue文件

在写router的过程中,一边写,一边在views目录下创建对应的vue文件,方便导入。

views/Money.vue
views/Labels.vue
views/Statistics.vue

修改App.vue

我们来测试一下,如何将刚才创建的三个vue组件,显示在页面上

<template>
    <div>
        App
        <hr>
        <router-view/>
    </div>
</template>

<style lang="scss">
    #app {
        font-family: Avenir, Helvetica, Arial, sans-serif;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
        text-align: center;
        color: #2c3e50;
    }

    #nav {
        padding: 30px;

        a {
            font-weight: bold;
            color: #2c3e50;

            &.router-link-exact-active {
                color: #42b983;
            }
        }
    }
</style>

Labels.vue

<template>
    <div>Labels.vue</div>
</template>

<script lang="ts">
    export default {
        name: 'Labels'
    };
</script>

<style lang="scss" scoped>

</style>

Money.vue

<template>
    <div>Money.vue</div>
</template>

<script lang="ts">
    export default {
        name: 'Money'
    };
</script>

<style lang="scss" scoped>

</style>

Statistics.vue

<template>
    <div>Statistics.vue</div>
</template>

<script lang="ts">
    export default {
        name: 'statistics'
    };
</script>

<style lang="scss" scoped>

</style>

这样一来,我们就成功的使用路由来渲染页面。


初级导航栏

App.vue

<template>
    <div>
        <router-view/>
        <hr>
        <div>
            <router-link to="/money">记账</router-link>
            |
            <router-link to="/labels">标签</router-link>
            |
            <router-link to="/statistics">统计</router-link>
        </div>
    </div>
</template>

将Nav做成全局组件


导航栏组件

components/Nav.vue

<template>
    <div>
        <router-link to="/money">记账</router-link>
        |
        <router-link to="/labels">标签</router-link>
        |
        <router-link to="/statistics">统计</router-link>
    </div>
</template>

App.vue

<template>
    <div>
        <router-view/>
    </div>
</template>

Money.vue引入导航栏

<template>
    <div>
        Money.vue
        <Nav/>
    </div>
</template>

<script lang="ts">
    import Nav from '@/components/Nav.vue';
    export default {
        name: 'Money',
        components: {Nav},
    };
</script>

<style lang="scss" scoped>

</style>

这样一来,谁想要使用导航栏,谁就引入Nav,但是还是有点点麻烦,可不可以全局引入?


全局引入

main.ts

import Vue from 'vue';
import App from './App.vue';
import './registerServiceWorker';
import router from './router';
import store from './store';
// 此处导入Nav
import Nav from '@/components/Nav.vue';

Vue.config.productionTip = false;

// 此处全局引入Nav
Vue.component('Nav', Nav);

new Vue({
    router,
    store,
    render: h => h(App)
}).$mount('#app');

然后我们就可以把,Money、Labels、Statistics中导入Nav删掉了。

<template>
    <div>
        Labels.vue
        <Nav/>
    </div>
</template>

<script lang="ts">
    export default {
        name: 'Labels',
    };
</script>

<style lang="scss" scoped>

</style>
<template>
    <div>
        Money.vue
        <Nav/>
    </div>
</template>

<script lang="ts">
    export default {
        name: 'Money',
    };
</script>

<style lang="scss" scoped>

</style>
<template>
    <div>
        Statistics.vue
        <Nav/>
    </div>
</template>

<script lang="ts">
    export default {
        name: 'statistics',
    };
</script>

<style lang="scss" scoped>

</style>

404页面

router/index.ts

import Vue from 'vue';
import VueRouter, {RouteConfig} from 'vue-router';
import Money from '@/views/Money.vue';
import Labels from '@/views/Labels.vue';
import Statistics from '@/views/Statistics.vue';
import NotFound from '@/views/NotFound.vue';


Vue.use(VueRouter);

const routes: Array<RouteConfig> = [
    {
        path: '/',
        redirect: '/money'
    },
    {
        path: '/money',
        component: Money
    },
    {
        path: '/labels',
        component: Labels
    },
    {
        path: '/statistics',
        component: Statistics
    },
    {
        path: '*',
        component: NotFound
    }
];

const router = new VueRouter({
    routes
});

export default router;

创建NotFound组件

views/NotFound.vue

<template>
    <div>
        404 Not Found
        <hr>
        当前页面不存在,请检查url是否正确
        <div>
            <router-link to="/">返回首页</router-link>
            |
            <a href="#/">a标签返回首页</a>
        </div>
    </div>
</template>

<script lang="ts">
    export default {
        name: 'NotFound'
    };
</script>

<style lang="scss" scoped>

</style>

这里有两种返回首页的方式,一种是router-link另一种是a标签,建议使用高端的,router-link

布局

千万不要在手机上,使用fixed定位,使用flex布局


Money页面布局

Money.vue

<template>
    <div class="nav-wrapper">
        <div class="content">
            Money.vue
        </div>
        <Nav/>
    </div>
</template>

<script lang="ts">
    export default {
        name: 'Money',
    };
</script>

<style lang="scss" scoped>
    .nav-wrapper {
        border: 1px solid green;
        display: flex;
        flex-direction: column;
        height: 100vh;
    }
    .content {
        flex-grow: 1;
        overflow: auto;
    }
</style>

Labels页面布局

Labels.vue

<template>
    <div class="nav-wrapper">
        <div class="content">
            Labels.vue
        </div>
        <Nav/>
    </div>
</template>

<script lang="ts">
    export default {
        name: 'Labels',
    };
</script>

<style lang="scss" scoped>
    .nav-wrapper {
        border: 1px solid green;
        display: flex;
        flex-direction: column;
        height: 100vh;
    }

    .content {
        flex-grow: 1;
        overflow: auto;
    }
</style>

会发现一个问题,我们写了重复的代码,CSS重复,这样的话如果有一天老板说,我们需要换布局,目前咱们才两个地方 重复,如果还有更多页面都是这样样式100个,1000个,10000个...改去吧。

Layout组件 & slot插槽

我与重复不共戴天...


创建Layout组件

然后把上面代码,相同部分都拷贝过去。

在main.ts中全局引入layout

import Vue from 'vue';
import App from './App.vue';
import './registerServiceWorker';
import router from './router';
import store from './store';
import Nav from '@/components/Nav.vue';
import Layout from '@/components/Layout.vue';

Vue.config.productionTip = false;

Vue.component('Nav', Nav);
Vue.component('Layout', Layout);

new Vue({
    router,
    store,
    render: h => h(App)
}).$mount('#app');

components/Layout.vue

<template>
    <div class="nav-wrapper">
        <div class="content">
            <slot/> <!--此处就是插槽,可以传递引用的内容-->
        </div>
        <Nav/>
    </div>
</template>

<script lang="ts">
    export default {
        name: 'Layout'
    };
</script>

<style lang="scss" scoped>
    .nav-wrapper {
        border: 1px solid green;
        display: flex;
        flex-direction: column;
        height: 100vh;
    }

    .content {
        flex-grow: 1;
        overflow: auto;
    }
</style>

引用Layout组件

Money.vue 引用Layout

<template>
    <div>
        <Layout>
            <p>Money.vue</p>
        </Layout>
    </div>
</template>

<script lang="ts">
    export default {
        name: 'Money',
    };
</script>

<style lang="scss" scoped>
</style>

Labels.vue 引用Layout

<template>
    <div>
        <Layout>
            <p>Labels.vue</p>
        </Layout>
    </div>
</template>

<script lang="ts">
    export default {
        name: 'Labels',
    };
</script>

<style lang="scss" scoped>
</style>

Statistics.vue 引用Layout

<template>
    <div>
        <Layout>
            <p>Statistics.vue</p>
        </Layout>
    </div>
</template>

<script lang="ts">
    export default {
        name: 'statistics',
    };
</script>

<style lang="scss" scoped>

</style>

使用svg-sprite-loader引入icon


下载svg

使用iconfont.cn

搜索并下载svg

然后将svg放入项目中,在src/assets/下创建icons目录


引入svg

注意,刚才我们只是给他放到了项目的目录下,并没有引入,引入的话相对来说比较复杂。

很显然,直接引入svg是有点困难的,所以我们需要给TS加入一段配置

shims-vue.d.ts

// 在原本的内容下,加入下面内容
declare module '*.svg' {
    const content: string;
    export default content;
}

Nav.vue测试是否可以导入svg

<template>
    <div>
        <router-link to="/money">记账</router-link>
        |
        <router-link to="/labels">标签</router-link>
        |
        <router-link to="/statistics">统计</router-link>
    </div>
</template>

<script lang="ts">
    import x from '@/assets/icons/label.svg'
    console.log(`这里是x:${x}`);

    export default {
        name: 'Nav'
    };
</script>

<style lang="scss" scoped>

</style>

打印出了,svg的路径,不过这不是我们想要的,我们需要的是一个svg use的使用方法,此时我们需要svg sprite loader


安装svg sprite loader
MacBook-pro:morney driverzeng$ yarn add svg-sprite-loader -D

修改webpack配置文件

我们会发现,mmp压根没有webpack配置文件,肿么办?

我们只能修改vue.config.js这个文件,把webpack语法翻译一下。

vue.config.js

const path = require('path')

module.exports = {
    lintOnSave: false,
    chainWebpack: config => {
        const dir = path.resolve(__dirname, 'src/assets/icons')

        // config是Vue把webpack配置封装成了对象暴露给我们的接口
        config.module
            // 添加一个规则的名字,叫做svg-sprite
            .rule('svg-sprite')
            // 如果文件能匹配上下面的正则,则使用这个规则,就是以.svg结尾的
            .test(/\.svg$/)
            // 因为我们不想让整个项目的svg都走这个规则所以,我们需要include包含指定的目录,只包含icons目录
            .include.add(dir).end()
            // 使用哪些loader,使用svg-sprite-loader,extract:false是不要把它解析出文件来
            .use('svg-sprite-loader').loader('svg-sprite-loader').options({extract: false}).end()
        // 使用svg-sprite-loader目录下的plugin
        config.plugin('svg-sprite').use(require('svg-sprite-loader/plugin'), [{plainSprite: true}])
        // 排除其他的svg,只要不在icons目录下的svg都不走这个规则
        config.module.rule('svg').exclude.add(dir)
    }
}

重启服务,之后就会发现,这回打印出来的x就不一样了,并且在element中,多了svg的标签

然后我们就可以使用use标签,使用svg

Nav.vue

<template>
    <div>
        <router-link to="/money">
            <svg>
                <use xlink:href="#label"/>
            </svg>
            记账
        </router-link>
        |
        <router-link to="/labels">
            标签
        </router-link>
        |
        <router-link to="/statistics">统计</router-link>
    </div>
</template>

<script lang="ts">
    import x from '@/assets/icons/label.svg';

    console.log(`这里是x:${x}`);

    export default {
        name: 'Nav'
    };
</script>

<style lang="scss" scoped>

</style>

存在两个问题:
1.那如果我有100个icon文件就需要import 100次?
2.而且每次都需要写:xlink:href="#label"

git commit会报错

在此我们需要先解决一个问题,我们加完刚才那个webpack配置后,会出现eslint的报错,于是git commit就会报错。我们需要在开头加上 /* eslint-disable */,然后重新 git add .git commit

/* eslint-disable */
const path = require('path')

module.exports = {
    lintOnSave: false,
    chainWebpack: config => {
        const dir = path.resolve(__dirname, 'src/assets/icons')

        // config是Vue把webpack配置封装成了对象暴露给我们的接口
        config.module
            // 添加一个规则的名字,叫做svg-sprite
            .rule('svg-sprite')
            // 如果文件能匹配上下面的正则,则使用这个规则,就是以.svg结尾的
            .test(/\.svg$/)
            // 因为我们不想让整个项目的svg都走这个规则所以,我们需要include包含指定的目录,只包含icons目录
            .include.add(dir).end()
            // 使用哪些loader,使用svg-sprite-loader,extract:false是不要把它解析出文件来
            .use('svg-sprite-loader-mod').loader('svg-sprite-loader-mod').options({extract: false}).end()
            .use('svgo-loader').loader('svgo-loader')
            .tap(options => ({...options, plugins: [{removeAttrs: {attrs: 'fill'}}]})).end()
        // 使用svg-sprite-loader目录下的plugin
        config.plugin('svg-sprite').use(require('svg-sprite-loader-mod/plugin'), [{plainSprite: true}])
        // 排除其他的svg,只要不在icons目录下的svg都不走这个规则
        config.module.rule('svg').exclude.add(dir)
    }
}

import一个目录问题解决

只需要两句话。

Nav.vue

<template>
    <div>
        <router-link to="/money">
            <svg>
                <use xlink:href="#label"/>
            </svg>
            记账
        </router-link>
        |
        <router-link to="/labels">
            标签
        </router-link>
        |
        <router-link to="/statistics">统计</router-link>
    </div>
</template>

<script lang="ts">
    // import x from '@/assets/icons/label.svg';
    // console.log(`这里是x:${x}`);
    const importAll = (requireContext: __WebpackModuleApi.RequireContext) => requireContext.keys().forEach(requireContext);
    try {importAll(require.context('../assets/icons/',true,/\.svg$/));} catch (error) {console.log(error);}
    export default {
        name: 'Nav'
    };
</script>

<style lang="scss" scoped>

</style>

只需要两句话,三个全部导入进去了。

封装icon组件


首封Icon

新建一个components/Icon.vue,提取重复内容

<template>
    <svg>
        <use xlink:href="#label"/>
    </svg>
</template>

<script lang="ts">
    const importAll = (requireContext: __WebpackModuleApi.RequireContext) => requireContext.keys().forEach(requireContext);
    try {importAll(require.context('../assets/icons/',true,/\.svg$/));} catch (error) {console.log(error);}
    export default {
        name: 'Icon'
    };
</script>

<style lang="scss" scoped>

</style>

引用Icon

main.ts 设置全局引用

import Vue from 'vue';
import App from './App.vue';
import './registerServiceWorker';
import router from './router';
import store from './store';
import Nav from '@/components/Nav.vue';
import Layout from '@/components/Layout.vue';
import Icon from '@/components/Icon.vue';

Vue.config.productionTip = false;

Vue.component('Nav', Nav);
Vue.component('Layout', Layout);
Vue.component('Icon', Icon);

new Vue({
    router,
    store,
    render: h => h(App)
}).$mount('#app');

Nav.vue

<template>
    <div>
        <router-link to="/money">
            <Icon/>
            记账
        </router-link>
        |
        <router-link to="/labels">
            <Icon/>
            标签
        </router-link>
        |
        <router-link to="/statistics">
            <Icon/>
            统计
        </router-link>
    </div>
</template>

<script lang="ts">
    // import x from '@/assets/icons/label.svg';
    // console.log(`这里是x:${x}`);
    import Icon from '@/components/Icon.vue';
    export default {
        name: 'Nav',
        components: {Icon}
    };
</script>

<style lang="scss" scoped>

</style>

怎么样能让三个是不一样的呢,给Icon添加一个外部属性。


外部属性props

Icon.vue

<template>
    <svg>
        <!-- 我们可以从后端传递name过来,因为name要加上#,直接加会报错,所以当做字符串 -->
        <use :xlink:href="'#'+name"/>
        <!-- v-bind可以省略 -->
        <!-- use v-bind:xlink:href="'#'+name"/-->
    </svg>
</template>

<script lang="ts">
    const importAll = (requireContext: __WebpackModuleApi.RequireContext) => requireContext.keys().forEach(requireContext);
    try {importAll(require.context('../assets/icons/',true,/\.svg$/));} catch (error) {console.log(error);}
    export default {
        props:['name'],
        name: 'Icon'
    };
</script>

<style lang="scss" scoped>

</style>

引用Icon传递props

Nav.vue

<template>
    <div>
        <router-link to="/money">
            <Icon name="money"/>
            记账
        </router-link>
        |
        <router-link to="/labels">
            <Icon name="label"/>
            标签
        </router-link>
        |
        <router-link to="/statistics">
            <Icon name="statistics"/>
            统计
        </router-link>
    </div>
</template>

<script lang="ts">
    // import x from '@/assets/icons/label.svg';
    // console.log(`这里是x:${x}`);
    import Icon from '@/components/Icon.vue';

    export default {
        name: 'Nav',
        components: {Icon}
    };
</script>

<style lang="scss" scoped>

</style>


修改Icon样式

让icon跟字体一样大小,1em

Icon.vue

<template>
    <svg class="icon">
        <use :xlink:href="'#'+name"/>
    </svg>
</template>

<script lang="ts">
    const importAll = (requireContext: __WebpackModuleApi.RequireContext) => requireContext.keys().forEach(requireContext);
    try {
        importAll(require.context('../assets/icons/', true, /\.svg$/));
    } catch (error) {
        console.log(error);
    }
    export default {
        props: ['name'],
        name: 'Icon'
    };
</script>

<style lang="scss" scoped>
    .icon {
        width: 1em;
        height: 1em;
        vertical-align: -0.15em;
        fill: currentColor;
        overflow: hidden;
    }
</style>

优化样式


Nav.vue
<template>
    <nav>
        <router-link to="/money" class="item">
            <Icon name="money"/>
            记账
        </router-link>
        <router-link to="/labels" class="item">
            <Icon name="label"/>
            标签
        </router-link>
        <router-link to="/statistics" class="item">
            <Icon name="statistics"/>
            统计
        </router-link>
    </nav>
</template>

<script lang="ts">
    // import x from '@/assets/icons/label.svg';
    // console.log(`这里是x:${x}`);
    import Icon from '@/components/Icon.vue';

    export default {
        name: 'Nav',
        components: {Icon}
    };
</script>

<style lang="scss" scoped>
    nav {
        display: flex;
        flex-direction: row;
        box-shadow: 0 0 3px rgba(0, 0, 0, .25);

        > .item {
            padding: 2px 0;
            width: 33.33333%;
            display: flex;
            justify-content: center;
            align-items: center;
            flex-direction: column;

            .icon {
                width: 32px;
                height: 32px;
            }
        }
    }
</style>

App.vue
<template>
    <div>
        <router-view/>
    </div>
</template>

<style lang="scss">
    * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
    }

    body {
        line-height: 1.5;
    }

    a {
        text-decoration: none;
        color: inherit;
    }

    #app {
        font-family: Avenir, Helvetica, Arial, sans-serif;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
        text-align: center;
        color: #2c3e50;
    }

    #nav {
        padding: 30px;

        a {
            font-weight: bold;
            color: #2c3e50;

            &.router-link-exact-active {
                color: #42b983;
            }
        }
    }
</style>

路由激活


点击链接变色

active-class的使用

Nav.vue

<template>
    <nav>
        <router-link to="/money" class="item" active-class="selected">
            <Icon name="money"/>
            记账
        </router-link>
        <router-link to="/labels" class="item" active-class="selected">
            <Icon name="label"/>
            标签
        </router-link>
        <router-link to="/statistics" class="item" active-class="selected">
            <Icon name="statistics"/>
            统计
        </router-link>
    </nav>
</template>

<script lang="ts">
    // import x from '@/assets/icons/label.svg';
    // console.log(`这里是x:${x}`);
    import Icon from '@/components/Icon.vue';

    export default {
        name: 'Nav',
        components: {Icon}
    };
</script>

<style lang="scss" scoped>
    nav {
        display: flex;
        flex-direction: row;
        box-shadow: 0 0 3px rgba(0, 0, 0, .25);

        > .item {
            padding: 2px 0;
            width: 33.33333%;
            display: flex;
            justify-content: center;
            align-items: center;
            flex-direction: column;

            .icon {
                width: 32px;
                height: 32px;
            }
        }

        > .item.selected {
            background: #ffb139;
            box-shadow: 0 0 10px rgba(0, 0, 0, 1);
            border-radius: 6%;
        }
    }
</style>

使用淘宝meta vp


查找淘宝meta vp

淘宝手机网站:TP

点开F12开发者工具,在Elment中

标签下找到meta vp

<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">

引用meta vp

public/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
    <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
        Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

svg的bug


svg的fill

如果svg文件中,有fill,那么svg的颜色是无法更改的,他本身就有颜色。

所以我们需要删除svg的fill属性,那么就需要使用一个svgo-loader的工具


修改vue.config.js
/* eslint-disable */
const path = require('path')

module.exports = {
    lintOnSave: false,
    chainWebpack: config => {
        const dir = path.resolve(__dirname, 'src/assets/icons')

        // config是Vue把webpack配置封装成了对象暴露给我们的接口
        config.module
            // 添加一个规则的名字,叫做svg-sprite
            .rule('svg-sprite')
            // 如果文件能匹配上下面的正则,则使用这个规则,就是以.svg结尾的
            .test(/\.svg$/)
            // 因为我们不想让整个项目的svg都走这个规则所以,我们需要include包含指定的目录,只包含icons目录
            .include.add(dir).end()
            // 使用哪些loader,使用svg-sprite-loader,extract:false是不要把它解析出文件来
            .use('svg-sprite-loader-mod').loader('svg-sprite-loader-mod').options({extract: false}).end()
            // 使用 svgo-loader
            .use('svgo-loader').loader('svgo-loader')
            // 删除fill属性
            .tap(options => ({...options, plugins: [{removeAttrs: {attrs: 'fill'}}]})).end()
        // 使用svg-sprite-loader目录下的plugin
        config.plugin('svg-sprite').use(require('svg-sprite-loader-mod/plugin'), [{plainSprite: true}])
        // 排除其他的svg,只要不在icons目录下的svg都不走这个规则
        config.module.rule('svg').exclude.add(dir)
    }
}

安装svgo-loader
MacBook-pro:morney driverzeng$ yarn add --dev svgo-loader