【项目-喵内记账-meoney-01】导航栏

大纲链接 §

[toc]


项目思路

  • 根据设计稿,使用Vue Router 分别导航三个页面 Money.Vue Labels.vue Statistics.vue
  • 提取Layout.vue组件,作为整体布局模板
    • 其中包含Nav.vue组件作为导航栏
  • 后期根据修改的设计稿重构页面

使用Vue Router

  • 设计稿:Figma
  • 根据设计稿页面,确定路由页面 url 使用 Vue Router 添加路由,本地版默认使用 哈希模式
    • #/money 记账(默认页面)重定向页面redirect: Money
    • #/labels 标签编辑页面
    • #/statistics 统计页面
    • / 默认页面 记账页面,重定向路径redirect: '/money'(注意是和路径path: '/money'相同,而不是组件名Money
    • #/404 保底页面
  • 添加代码 router/index.ts添加router,配置4个路径相对应组件到src/views/路径下(路径名小写,组件名大写)
  • 初始化组,将router传给new Vue()
    • 在路径中src/router/index.ts自动直接识别index.ts文件
    • import router from './router'相当于import router from './router/index.ts'
    • new Vue({router, store, render: h => h(App)}).$mount('#app')
  • App组件里用<router-view>/给出router渲染区域
  • 每个组件里都必须写<script></script>标签,即使不包含逻辑代码,否则不会被引用到

@/router/index.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import Vue from 'vue';
import VueRouter, {RouteConfig} from 'vue-router';
import Home from '@/views/Home.vue';
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;

Nav组件做成全局组件

并不是所有页面都需要展示导航,比如404页面,抽成全局组件,按需引入;

不要在App.vue中写任何有关页面展示的详细代码,只引入组件和负责渲染根节点

  • 思路分析:
    • App.vue还是每个组件中写<Nav/>—有的页面不需要展示<Nav/>,会添加冗余的代码去处理<Nav/>逻辑
    • 全局引入<Nav/>组件还是局部引入—三个页面都需要展示<Nav/>,统一在全局引入

Vue Router 404 页面

  • Vue Router
  • 捕获所有路由或 404 Not found 路由
  • 路由的匹配顺序按数组依次匹配,最后匹配*表示匹配除了之前所有的
  • 添加NotFound.vue组件
    • 添加返回首页
      • 使用<router-link to="/">返回首页</router-link>
      • 使用<a href="#/">返回首页</a>

样式注意点

Fixed 还是用 Flex 布局

  • 不要在手机上使用fixed定位;不要在手机上使用fixed定位;不要在手机上使用fixed定位;
  • 移动端的flex定位的坑相对少,解决方案成熟

vue处理隔离样式scoped,类加上属性选择器,随机字符串作为属性名

  • data-v-[随机字符]

组件名-作用 命名 样式

  • nav-wrapper

App.vue的样式不能加scoped

  • 其他全局样式

内容容器样式

  • 固定高度100vh占满一屏

Layout 组件 & slot 插槽 复用UI结构

重复就是Bug - 与重复不共戴天

  • 重复的代码难以应对需求变更

Money.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<template>
  <div class="nav-wrapper">
    <div class="content">
      <p>Money.vue</p>
    </div>
    <Nav/>
  </div>
</template>

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

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

  • 抽取Money.vue会复用相同结构的组件为Layout.vue

Layout.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<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 red;
  display: flex;
  flex-direction: column;
  height: 100vh;
  .content {
    border: 1px solid cornflowerblue;
    flex-grow: 1;
    overflow: auto;
    }
  }
</style>
  • 使用slot 插槽
  • Layout组件中获取各引用它的组件的数据,即 内容分发,将 <slot/> 元素作为承载分发内容的出口
  • 模板组件中的 <slot/> 部分将会被替换为引用这个模板的组件双标签中的部分
    • <yourComponent>replace Part for Slot</yourComponent>
  • 插槽内可以包含任何模板代码,包括 HTML

Money.vue简化为

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<template>
  <div class="nav-wrapper">
    <Layout>
      <p>Money.vue</p>
    </Layout>
  </div>
</template>

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

<style lang="scss" scoped>
</style>
  • 将相同的部分封装为组件
  • 将不同的部分通过插槽插入

父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的


使用svg-sprite-loader引入icon

  • 使用svg-sprite-loader
    • 安装svg-sprite-loader:
      • yarn add svg-sprite-loader -D
    • 配置vue.config.js
  • shims-vue.d.ts中添加svg声明
  • 将在iconfont下载的svg图标引入到Nav组件中

vue.config.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const path = require('path')

module.exports = {
  lintOnSave: false,
  chainWebpack: config => {
    const dir = path.resolve(__dirname, 'src/assets/icons') // 确定目录
    config.module.uses.clear() // 清除已有的loader, 如果不这样做会添加在此loader之后
      .rule('svg-sprite')
      .test(/\.svg$/)// .test(/\.(svg)(\?.*)?$/)
      .include.add(dir).end() // 指定 仅包含 icons 的目录
      .use('svg-sprite-loader').loader('svg-sprite-loader')
      .options({extract: false}).end() // 不解析出文件

    config.plugin('svg-sprite').use(require('svg-sprite-loader/plugin'), [{plainSprite: true}])
    config.module.rule('svg').exclude.add(dir)
  }
}

shims-vue.d.ts

1
2
3
4
5
6
7
8
9
declare module '*.vue' {
  import Vue from 'vue';
  export default Vue;
}
declare module '*.svg' {
  const content: string;
  export default content;
}

Nav.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
  <div>
    <nav>
      <router-link to="/money">记账</router-link>
      |
      <router-link to="/labels">标签</router-link>
      |
      <router-link to="/statistics">统计</router-link>
    </nav>
  </div>
</template>

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

console.log(x);
export default {
  name: 'Nav'
};
</script>

  • svg-sprite-loader 会导致 cssimport ~@ WebStorm 里报错

Eslint 报错如何解决

鸵鸟

  • 在IDE和命令行中关闭eslint提示
  • 或者在vue.config.js里代码顶端添加/* eslint-disable */

配置.eslintrc.js,关闭相应的检查

1
2
3
4
5
6
7
8
module.exports = {
...
  rules: {
  ...
    '@typescript-eslint/no-var-requires': 0,
  ...
  },
}
  • 或者配置.eslintIgnore文件

代码

  • 按提示修改代码

svg-sprite-loader相关文章


如何 import 一个目录,引入所有svg图标文件

Nav.vuescript中(后续将Icon的逻辑提取到单独组件中)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const importAll = (requireContext: __WebpackModuleApi.RequireContext) => {
  requireContext.keys().forEach(requireContext);
};
// 指定目录 只能用相对路径 不支持@别名路径
// 使用importAll加载所有的svg
importAll(require.context('../assets/icons/', true, /\.svg$/));

try {
  importAll(require.context('../assets/icons/', true, /\.svg$/));
} catch (error) {
  console.log(error);
}


封装 Icon.vue 组件

样式 symbol引用

1
2
3
4
5
6
7
8
<style lang="scss">
    .icon {
       width: 1em; height: 1em;
       vertical-align: -0.15em;
       fill: currentColor;
       overflow: hidden;
    }
</style>
  • 尺寸为一个字符的大小

使用<svg></svg>标签

1
2
3
<svg class="icon">
    <use xlink:href="<NAME>"/>
</svg>

向父组件传递点击事件

1
2
3
<svg class="icon" @click="$emit('click', $event)">
    <use :xlink:href="<NAME>"/>
</svg>

Icon.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<template>
  <svg class="icon" @click="$emit('click', $event)">
    <use :xlink:href="'#' + name"/>
  </svg>
</template>

<script lang="ts">
const importAll = (requireContext: __WebpackModuleApi.RequireContext) => {
  requireContext.keys().forEach(requireContext);
};

// 指定目录 只能用相对路径 不支持@别名路径
// 使用importAll加载所有的svg
importAll(require.context('../assets/icons/', true, /\.svg$/));

try {
  importAll(require.context('../assets/icons/', true, /\.svg$/));
} catch (error) {
  console.log(error);
}

import Vue from 'vue';
import {Component, Prop} from 'vue-property-decorator';

@Component
export default class Numpad extends Vue {
  // 动态加载
  @Prop({default: ''}) ['name']: string;
}
</script>

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

单一逻辑原则,抽出importAllSvg方法

src/lib/importAllSvg.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
export default function importAllSvg() {
  // 批量导入 svg
  const importAll = (requireContext: __WebpackModuleApi.RequireContext) => {
    requireContext.keys().forEach(requireContext);
  };

// 指定目录 只能用相对路径 不支持 @ 别名路径
// 使用 importAll 方法加载所有的 *.svg 文件
  importAll(require.context('../assets/icons/', true, /\.svg$/));

// 捕获报错
  try {
    importAll(require.context('../assets/icons/', true, /\.svg$/));
  } catch (error) {
    console.log(error);
  }
}

重构src/components/Icon.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<script lang="ts">
import importAllSvg from '@/lib/importAllSvg';
import {Component, Prop, Vue} from 'vue-property-decorator';

importAllSvg();

@Component
export default class Icon extends Vue {
  // 动态引入 svg name
  @Prop({default: ''}) ['name']: string;
}
</script>

<template>
  <svg class="icon" @click="$emit('click', $event)">
    <use :xlink:href="'#' + name"/>
  </svg>
</template>

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


导航栏样式与路由激活样式active-class

  • 不写具体高度值
  • 链接默认颜色设为color: inherit;

active-class的使用(路由激活)

激活一个路由时,将对应的图标变为高亮

  • 标签中添加active-class属性,
    • 当前元素处于被选中状态时,可以看到动态路由匹配router-link中自动添加active-class属性
  • router-link标签上加class="item"active-class="selected"属性
  • 添加.item.selected的样式

使用 svgo-loader 插件 删除 svg 标签的 fill 填充色属性

可以记录为项目遇到的困难 bug 踩坑

  • 自动批量去除字体图标的填充色
  • 安装yarn add --dev svgo-loader@2.2.1
  • 配置vue.config.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const path = require('path')
module.exports = {
  lintOnSave: false,
  chainWebpack: config => {
    const dir = path.resolve(__dirname, 'src/assets/icons') // 确定目录
    config.module
      .rule('svg-sprite')
      .test(/\.svg$/) // .test(/\.(svg)(\?.*)?$/)
      .include.add(dir).end() // 指定 仅包含 icons 的目录
      .use('svg-sprite-loader').loader('svg-sprite-loader')
      .options({extract: false}).end() // 不解析出文件
      .use('svgo-loader').loader('svgo-loader') // SVG优化插件
      .tap(options =>({...options, plugins: [{removeAttrs: {attrs: 'fill'}}]})).end() // 去除 SVG填充色属性

    config.plugin('svg-sprite').use(require('svg-sprite-loader/plugin'), [{plainSprite: true}])
    config.module.rule('svg').exclude.add(dir)
  },
}
  • 无视SVG自带填充色,都由css控制
  • 注意svgo-loader的版本"svgo-loader": "2.2.1"
    • 如果使用目前最新版"svgo-loader": "3.0.0"的编译错误的话,就回退一个版本

后续修复vue.config.js:本地预览 dist 目录发现 JS 路径错误

使用 yarn build 得到 dist 目录后,再用 serve -s dist 然后在本地浏览器打开 http://localhost:5000 会看到:

如图报错

  • 这是因为在本地环境,这个 JS 的路径不对
  • 虽然在 GitHub Pages 里,这个JS 的路径其实是对的
  • vue.config.js,其实已经考虑了这个问题
    • vue.config.js会在 production 环境(也就是 GitHub Pages 上)使用 /morney-3-website/ 作为路径前缀
    • 在本地使用 / 作为路径前缀:

如图

  • 但实际情况是,本地的 dist 使用了 /morney-3-website/,正确的前缀应该是 /
  • 为什么 vue.config.js 考虑了这个问题,还是会出现这个问题呢?

怎么解决这个问题

步骤如下:

  • yarn add cross-env(这个 cross-envWindows 用户必须的,其他系统的用户装了它也没事,不会有任何副作用)
  • package.jsonscript字段里添加
    • "build:dev":"cross-env NODE_ENV=development yarn build"

注意行尾的逗号

如图所示

  • 使用 yarn build:dev 得到的 dist 即可在本地用 serve 预览
  • 使用 yarn build 得到的 dist 即可在 GitHub Pages 上正常预览

vue.confi.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// ESLint ignore first line
/* eslint-disable */
const path = require('path')

module.exports = {
  publicPath: process.env.NODE_ENV === 'production'
    ? '/meowney-0-website/'
    : '/',
  lintOnSave: false,
  chainWebpack: config => {
    const dir = path.resolve(__dirname, 'src/assets/icons') // 确定目录
    config.module
      .rule('svg-sprite')
      .test(/\.svg$/) // .test(/\.(svg)(\?.*)?$/)
      .include.add(dir).end() // 指定 仅包含 icons 的目录
      .use('svg-sprite-loader').loader('svg-sprite-loader')
      .options({extract: false}).end() // 不解析出文件
      .use('svgo-loader').loader('svgo-loader')
      .tap(options => ({...options, plugins: [{removeAttrs: {attrs: 'fill'}}]})).end()

    config.plugin('svg-sprite').use(require('svg-sprite-loader/plugin'), [{plainSprite: true}])
    config.module.rule('svg').exclude.add(dir) // 其他 svg loader 排除 icons 目录
  */
  },
  // pluginOptions: {}
}


提取公共样式

  • 创建目录src/assets/style/global.scss
  • 无需将scss文件变为css文件,webpack自动编译

更新 meta viewport

  • 淘宝手机页面的meta viewport
1
2
3
4
5
<html lang="zh-CN">
<meta name="viewport" 
      content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no,viewport-fit=cover">
...
</html>