01-小兔鲜儿

1,648 阅读7分钟

项目起步

01-项目介绍

了解:项目背景以及一些开发物料

小兔鲜儿

  • 电商发展十余年,是个成熟的模式,小兔鲜儿是B2C电商平台,综合品类平台
  • 平台理念:(品质)新鲜、(价格)亲民、(物流)快捷
  • 参考竞品:网易严选、京东

开发物料

目标功能

  • 首页,分类,搜索,商品详情,登录(第三方),购物车,收货地址,结算,支付(支付宝)
  • 形成一个购买功能功能闭环

02-技术选型

了解:项目会使用到的一些库和技术解决方案

Vue全家桶:

  • Vue3 前端框架
  • Vue-router 前端路由
  • Vuex 状态管理
  • Vue-cli 脚手架

第三方库:

  • axios 接口请求
  • vuex-persistedstate 状态持久化
  • @vueuse/core 组合API工具库
  • dayjs 日期处理
  • vee-validate

自研组件库

  • erabbit-ui 简版 www.npmjs.com/package/era…
  • 如果是UI风格高度定制,可以尝试独立一套组件UI
  • 如果是后台,优先考虑使用现有组件库。

总结:

  • 项目所有功能采用Composition API 编写,解决电商常见业务逻辑。

03-创建项目

掌握,使用vuecli创建vue3的项目

通过脚手架创建项目,保证脚手架是5.0版本,npm i -g @vue/cli 更新。

vue create erabbit-vue3
  • 1)选择自定义创建方式

image.png

  • 2)选择需要的插件和工具

image.png

  • 3)选择 vue3 版本

image.png

  • 4)选择 hash 路由模式

image.png

  • 5)选择 less 预处理器

image.png

  • 6)选择 Prettier 代码风格

image.png

  • 7)选择 保存代码,提交代码,进行代码风格校验

image.png

  • 8)选择 把工具的配置文件存放到自己对应的配置文件中

image.png

  • 9)是否记录刚才的操作,不记录

image.png

  • 10)安装成功:进入目录启动项目即可

老师提交至远程仓库,每开发一个功能提交一次

04-目录调整

了解生成的默认代码,按照功能调整目录

核心要点:

  • 解读调整默认生成的代码
  • 按照项目功能调整目录

具体内容:

  1. 解读默认生成的代码

src/router/index.js 路由代码

import { createRouter, createWebHashHistory } from "vue-router";

// 写路由规则
const routes = [];

// createRouter 创建路由实例 
const router = createRouter({
  // 路由模式:createWebHashHistory() 创建hash模式路由
  history: createWebHashHistory(),
  routes,
});

export default router;

store/index.js vuex的代码

import { createStore } from "vuex";

// createStore 创建一个vuex仓库
export default createStore({
  // 状态
  state: {},
  // vuex的计算属性
  getters: {},
  // 修改数据
  mutations: {},
  // 异步操作
  actions: {},
  // 模块
  modules: {},
});

main.js 入口文件

import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
// 创建app实例,使用store插件,使用router插件,挂载到app容器
createApp(App).use(store).use(router).mount("#app");
  1. 调整项目目录

assets 下的logo删除,components下组件删除,views下组件删除,App组件代码删除。

src
|-- assets
|-- components
|-- router
|-- store
|-- views
|-- App.vue
|-- main.js

总结:

  • 怎么创建 router 实例?
    • createRouter()
  • 怎么创建 store 仓库?
    • createStore()

05-拆分vuex模块

能够,定义 cart 模块,category模块 和 user 模块在vuex中

核心内容:

  • 介绍我们有三个模块需要vuex来存储数据
  • 分别定义这三个模块,合并到store的 modules 选项中

代码落地:

  • 三个模块
    • cart 存储购物车,头部组件要使用,购物车页面要使用。
    • category 存储分类,头部组件要使用,首页需要使用分类。
    • user 模块,用户信息,token信息,几乎所有页面需要使用。
  • 代码

store/modules/cart.js

// 购物车状态
export default {
  namespaced: true,
  state() {
    return {
      // 购物车列表
      list: [],
    };
  },
};

store/modules/category.js

// 分类模块
export default {
  namespaced: true,
  state() {
    return {
      // 分类列表
      list: [],
    };
  },
};

store/modules/user.js

// 用户模块
export default {
  namespaced: true,
  state() {
    return {
      // 用户信息
      profile: {
        id: null,
        avatar: null,
        nickname: null,
        account: null,
        mobile: null,
        token: null,
      },
    };
  },
};

store/index.js

import { createStore } from "vuex";

import cart from "./modules/cart";
import category from "./modules/category";
import user from "./modules/user";

export default createStore({
  modules: {
    cart,
    category,
    user,
  },
});

总结:

  • 使用 modules 选项,关联三个vuex模块。

06-实现vuex持久化

掌握,为项目配置vuex数据持久化插件

核心内容:

  • 理解 user 模块 cart 模块需要本地存储
  • 在项目中使用 vuex-persistedstate 持久化 cart 模块 和 user 模块
  • 测试 通过修改 user 模块的数据验证是否生效

具体落地:

  • 为什么要本地存储 user 和 cart ?
    • user 信息保存登录token,刷新页面还需要存在
    • cart 信息在没有登录的时候要存储,存储在哪里本地
  • 为什么要用户插件?
    • 如果不使用插件,操作了vuex还需要手动的去更新下本地存储,非常麻烦
  • vuex-persistedstate 插件作用?
  • 在更新vuex数据的时候,插件会自动去更新本地存储,方便
  • 使用步骤:

安装:

yarn add vuex-persistedstate

使用:store/index.js

// 创建插件函数
import createPersistedstate from "vuex-persistedstate";

export default createStore({
  modules: {
    cart,
    category,
    user,
  },
  plugins: [
    // 使用插件
    createPersistedstate({
      // 本地存储key
      key: "erabbit-store",
      // 存储哪些modules
      paths: ["cart", "user"],
    }),
  ],
});

测试:

store/modules/user.js

   mutations: {
    // 1.测试vuex持久化是否好使
    // 2.编写一个修改用户信息的mutations
    // 3.在组件中使用要修改的数据
    // 4.准备一个按钮,点击更新要修改的数据
    // 5.检验:vuex数据变化(页面变化),本地储存变化 localStorage
    // 设置个人资料
    setProfile (state, payload) {
      // 保留原来数据,合并新数据
      state.profile = { ...state.profile, ...payload };
    },
  },

App.vue

<template>
  <div>App 组件</div>
  <div>nikename值:{{ $store.state.user.profile.nickname }}</div>
  <button @click="$store.commit('user/setProfile', { nickname: '测试2' })">
    改值
  </button>
</template>

07-request封装-axios配置

掌握,request的axios配置

核心要点:

  • 创建一个axios实例,添加基础配置
  • 请求头有token的时候携带token
  • 剥离一层数据,响应401的时候拦截到登录

安装axios:yarn add axios

代码落地:

utils/request.js

import axios from "axios";
import store from "@/store";
import router from "@/router";

// 1. 创建axios和基本配置
const instance = axios.create({
  baseURL: "http://pcapi-xiaotuxian-front-devtest.itheima.net/",
  timeout: 5000,
});

// 2. 请求拦截器,携带token
instance.interceptors.request.use(
  (config) => {
    const { profile } = store.state.user;
    if (profile.token) {
      config.headers.Authorization = `Bearer ${profile.token}`;
    }
    return config;
  },
  (err) => Promise.reject(err)
);

// 3. 响应拦截器,取出内ret中的data,处理401错误
instance.interceptors.response.use(
  (ret) => ret.data,
  (err) => {
    if (err.response && err.response.status === 401) {
      // 清除用户信息
      store.commit("user/setProfile", {});
      // 跳转登录页
      router.push("/login");
    }
    return Promise.reject(err);
  }
);

总结:

  • 创建axios实例,使用 axios.create({...})
  • 拦截器和之前写一样,剥离一层数据是为了组件中使用方便。

08-request封装-工具函数

掌握,使用配置好的axios封装请求工具函数给api接口函数使用

核心要点:

  • 回顾之前调用接口的套路
  • 封装一个调用接口相对简洁的函数(便捷)导出给 api 层使用

主要内容:

  • 回顾之前调用接口的套路

image.png

  • 封装一个调用接口相对简洁的函数(便捷)导出给 api 层使用
/**
 * 请求工具函数
 * @param {string} url 请求地址
 * @param {string} method 请求方式
 * @param {object} submitData 请求传参
 * @returns Promise
 */
const request = (url, method, submitData) => {
  return instance({
    url,
    method,
    // 1. get 请求 params 传参
    // 2. 其他请求使用 data 传参
    // 3. [js表达式] 是动态使用key
    // 4. toLowerCase() 处理大小写,程序健壮
    [method.toLowerCase() === "get" ? "params" : "data"]: submitData,
  });
};

export default request;

总结:

  • 封装request工具函数的目的是提供复用,调用接口代码更简洁。

09-路由规则

理解,路由设计的依据,提前约定好路由规则

路由设计依据:

  • (全部切换)首页 与 登录
  • (部分切换)首页框架容器不变,内容变化
    • 首页内容
    • 分类内容
    • 商品内容
    • ...
  • 会变化的地方就是路由出口,写 router-view 的 地方,根据嵌套关系分为 1 2 级路由。

约定路由规则:

路径组件(功能)嵌套级别
/首页布局容器Layout1级
/home首页2级
/category/:id​分类2级
/search搜索2级
/product/:id商品详情2级
/login登录1级
/cart购物车2级
/checkout填写订单2级
/pay支付2级
/pay/result支付结果2级

总结:

  • 路由规则约定好之后,可以按照路由去开发对应的页面。

首页开始

10-首页-路由与组件

编写:首页路由规则代码,和页面框架结构 大致步骤:

  • 定义一级路由出口
  • Layout布局容器
  • Home页面组件
  • 配置路由规则

落地代码:

  • 根组件下定义一级路由组件出口 src/App.vue

<template>
  <!-- 一级路由 -->
  <router-view></router-view>
</template>

  • 一级路由布局容器 src/views/Layout.vue
<template>
  <nav>顶部通栏</nav>
  <header>头部</header>
  <main>
    <!-- 二级路由 -->
    <router-view></router-view>
  </main>
  <footer>底部</footer>
</template>

<script>
export default {
  name: 'xtx-layout'
}
</script>

<style scoped lang='less'></style>

11-首页-小兔鲜组件库

知道,如何导入小兔鲜组件库

注意:这个库服务于小兔鲜项目,很有组件业务特性很明显,不是很通用。

安装:

yarn add erabbit-ui

图标:public/index.html

<link rel="stylesheet" href="https://at.alicdn.com/t/font_2143783_iq6z4ey5vu.css">

全局使用:

import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";

import ErabbitUI from 'erabbit-ui'
import 'erabbit-ui/packages/theme/index.less'

// 创建一个vue应用,使用仓库vuex,使用路由,使用小兔鲜儿组件库,挂载到app容器
createApp(App).use(store).use(router).use(ErabbitUI).mount("#app");

测试:

<xtx-button type="plain">按钮</xtx-button>

文档:www.npmjs.com/package/era…

12-首页-css变量

理解:css变量的作用和使用

核心要点:

  • css变量定义
  • css变量使用
  • 介绍小兔鲜css变量

具体内容:

  • css变量定义
:root{
  --main-color: #4d4e53;
}  

为什么选择两根连词线(--)表示变量?因为$foo被 Sass 用掉了,@foo被 Less 用掉了。为了不产生冲突,官方的 CSS 变量就改用两根连词线了。

  • css变量使用
a {
  color: var(--main-color)
}

var()函数用于读取变量

  • 介绍小兔鲜css变量
:root {
  --xtx-color: #27ba9b;
  --help-color: #e26237;
  --suc-color: #1dc779;
  --warn-color: #ffb302;
  --price-color: #cf4444;
}

主题色,帮助色,成功色,警告色,价格色

13-首页-顶部通栏渲染

完成:顶部通栏

核心要点:

  • 顶部通栏组件
  • 动态展示通栏信息(登录|未登录)

落地代码:

  • 顶部通栏组件

src/components/ 下新建 app-topnav.vue 组件

<template>
  <nav class="app-topnav">
    <div class="container">
      <ul>
        <li><a href="javascript:;"><i class="iconfont icon-user"></i>周杰伦</a></li>
        <li><a href="javascript:;">退出登录</a></li>
        <li><a href="javascript:;">请先登录</a></li>
        <li><a href="javascript:;">免费注册</a></li>
        <li><a href="javascript:;">我的订单</a></li>
        <li><a href="javascript:;">会员中心</a></li>
        <li><a href="javascript:;">帮助中心</a></li>
        <li><a href="javascript:;">关于我们</a></li>
        <li><a href="javascript:;"><i class="iconfont icon-phone"></i>手机版</a></li>
      </ul>
    </div>
  </nav>
</template>
<script>
export default {
  name: 'AppTopnav'
}
</script>
<style scoped lang="less">
.app-topnav {
  background: #333;
  ul {
    display: flex;
    height: 53px;
    justify-content: flex-end;
    align-items: center;
    li {
      a {
        padding: 0 15px;
        color: #cdcdcd;
        line-height: 1;
        display: inline-block;
        i {
          font-size: 14px;
          margin-right: 2px;
        }
        &:hover {
          color: var(--xtx-color);
        }
      }
      ~ li {
        a {
          border-left: 2px solid #666;
        }
      }
    }
  }
}
</style>

src/views/Layout.vue 中导入使用。

<template>
+  <AppTopnav/>
  <header>头部</header>
  <main>
    <!-- 二级路由 -->
    <router-view></router-view>
  </main>
  <footer>底部</footer>
</template>

<script>
+import AppTopnav from '@/components/app-topnav'
export default {
  name: 'XtxLayout',
+  components: { AppTopnav }
}
</script>

<style scoped lang='less'></style>
  • 动态展示通栏信息(登录|未登录)
<script>
import { useStore } from 'vuex'
import { computed } from 'vue'    
export default {
  name: 'AppTopnav',
  setup () {
      const store = useStore()
      const profile = computed(()=>{
          return store.state.user.profile
      })
      return { profile }
  }
}
</script>
        <template v-if="profile.token">
          <li><a href="javascript:;"><i class="iconfont icon-user"></i>{{profile.account}}</a></li>
          <li><a href="javascript:;">退出登录</a></li>
        </template>
        <template v-else>
          <li><a href="javascript:;">请先登录</a></li>
          <li><a href="javascript:;">免费注册</a></li>
        </template>

总结:

  • 完成基础布局,根据用户信息动态展示导航菜单。

14-首页-头部和底部布局

完成:头部和底部布局,静态

核心要点:

  • 头部组件
  • 底部组件

落地代码:

  • 头部组件

src/components/ 下新建 app-header.vue 组件,基础布局如下:

<template>
  <header class='app-header'>
    <div class="container">
      <h1 class="logo"><RouterLink to="/">小兔鲜</RouterLink></h1>
      <ul class="navs">
        <li class="home"><RouterLink to="/">首页</RouterLink></li>
        <li><a href="#">美食</a></li>
        <li><a href="#">餐厨</a></li>
        <li><a href="#">艺术</a></li>
        <li><a href="#">电器</a></li>
        <li><a href="#">居家</a></li>
        <li><a href="#">洗护</a></li>
        <li><a href="#">孕婴</a></li>
        <li><a href="#">服装</a></li>
        <li><a href="#">杂货</a></li>
      </ul>
      <div class="search">
        <i class="iconfont icon-search"></i>
        <input type="text" placeholder="搜一搜">
      </div>
      <div class="cart">
        <a class="curr" href="#">
          <i class="iconfont icon-cart"></i><em>2</em>
        </a>
      </div>
    </div>
  </header>
</template>

<script>
export default {
  name: 'AppHeader'
}
</script>

<style scoped lang='less'>
.app-header {
  background: #fff;
  .container {
    display: flex;
    align-items: center;
  }
  .logo {
    width: 200px;
    a {
      display: block;
      height: 132px;
      width: 100%;
      text-indent: -9999px;
      background: url(../assets/logo.png) no-repeat center 18px / contain;
    }
  }
  .navs {
    width: 820px;
    display: flex;
    justify-content: space-around;
    padding-left: 40px;
    li {
      margin-right: 40px;
      width: 38px;
      text-align: center;
      a {
        font-size: 16px;
        line-height: 32px;
        height: 32px;
        display: inline-block;
      }
      &:hover {
        a {
          color: var(--xtx-color);
          border-bottom: 1px solid var(--xtx-color);
        }
      }
    }
  }
  .search {
    width: 170px;
    height: 32px;
    position: relative;
    border-bottom: 1px solid #e7e7e7;
    line-height: 32px;
    .icon-search {
      font-size: 18px;
      margin-left: 5px;
    }
    input {
      width: 140px;
      padding-left: 5px;
      color: #666;
    }
  }
  .cart {
    width: 50px;
    .curr {
      height: 32px;
      line-height: 32px;
      text-align: center;
      position: relative;
      display: block;
      .icon-cart{
        font-size: 22px;
      }
      em {
        font-style: normal;
        position: absolute;
        right: 0;
        top: 0;
        padding: 1px 6px;
        line-height: 1;
        background: var(--help-color);
        color: #fff;
        font-size: 12px;
        border-radius: 10px;
        font-family: Arial;
      }
    }
  }
}
</style>

src/views/Layout.vue 中导入使用

<template>
  <AppTopnav/>
+  <AppHeader/>
  <main>
    <!-- 二级路由 -->
    <router-view></router-view>
  </main>
  <footer>底部</footer>
</template>

<script>
import AppTopnav from '@/components/app-topnav'
+import AppHeader from '@/components/app-header'
export default {
  name: 'XtxLayout',
+  components: { AppTopnav, AppHeader }
}
</script>

<style scoped lang='less'></style>
  • 底部组件

src/components/ 下新建 app-footer.vue 组件,基础布局如下:

<template>
  <footer class="app-footer">
    <!-- 联系我们 -->
    <div class="contact">
      <div class="container">
        <dl>
          <dt>客户服务</dt>
          <dd><i class="iconfont icon-kefu"></i> 在线客服</dd>
          <dd><i class="iconfont icon-question"></i> 问题反馈</dd>
        </dl>
        <dl>
          <dt>关注我们</dt>
          <dd><i class="iconfont icon-weixin"></i> 公众号</dd>
          <dd><i class="iconfont icon-weibo"></i> 微博</dd>
        </dl>
        <dl>
          <dt>下载APP</dt>
          <dd class="qrcode"><img src="../assets/qrcode.jpg" /></dd>
          <dd class="download">
            <span>扫描二维码</span>
            <span>立马下载APP</span>
            <a href="javascript:;">下载页面</a>
          </dd>
        </dl>
        <dl>
          <dt>服务热线</dt>
          <dd class="hotline">400-0000-000 <small>周一至周日 8:00-18:00</small></dd>
        </dl>
      </div>
    </div>
    <!-- 其它 -->
    <div class="extra">
      <div class="container">
        <div class="slogan">
          <a href="javascript:;">
            <i class="iconfont icon-footer01"></i>
            <span>价格亲民</span>
          </a>
          <a href="javascript:;">
            <i class="iconfont icon-footer02"></i>
            <span>物流快捷</span>
          </a>
          <a href="javascript:;">
            <i class="iconfont icon-footer03"></i>
            <span>品质新鲜</span>
          </a>
        </div>
        <!-- 版权信息 -->
        <div class="copyright">
          <p>
            <a href="javascript:;">关于我们</a>
            <a href="javascript:;">帮助中心</a>
            <a href="javascript:;">售后服务</a>
            <a href="javascript:;">配送与验收</a>
            <a href="javascript:;">商务合作</a>
            <a href="javascript:;">搜索推荐</a>
            <a href="javascript:;">友情链接</a>
          </p>
          <p>CopyRight © 小兔鲜儿</p>
        </div>
      </div>
    </div>
  </footer>
</template>

<script>
export default {
  name: 'AppFooter'
}
</script>

<style scoped lang='less'>
.app-footer {
  overflow: hidden;
  background-color: #f5f5f5;
  padding-top: 20px;
  .contact {
    background: #fff;
    .container {
      padding: 60px 0 40px 25px;
      display: flex;
    }
    dl {
      height: 190px;
      text-align: center;
      padding: 0 72px;
      border-right: 1px solid #f2f2f2;
      color: #999;
      &:first-child {
        padding-left: 0;
      }
      &:last-child {
        border-right: none;
        padding-right: 0;
      }
    }
    dt {
      line-height: 1;
      font-size: 18px;
    }
    dd {
      margin: 36px 12px 0 0;
      float: left;
      width: 92px;
      height: 92px;
      padding-top: 10px;
      border: 1px solid #ededed;
      .iconfont {
        font-size: 36px;
        display: block;
        color: #666;
      }
      &:hover {
        .iconfont {
          color:  var(--xtx-color);
        }
      }
      &:last-child {
        margin-right: 0;
      }
    }
    .qrcode {
      width: 92px;
      height: 92px;
      padding: 7px;
      border: 1px solid #ededed;
    }
    .download {
      padding-top: 5px;
      font-size: 14px;
      width: auto;
      height: auto;
      border: none;
      span {
        display: block;
      }
      a {
        display: block;
        line-height: 1;
        padding: 10px 25px;
        margin-top: 5px;
        color: #fff;
        border-radius: 2px;
        background-color: var(--xtx-color);
      }
    }
    .hotline {
      padding-top: 20px;
      font-size: 22px;
      color: #666;
      width: auto;
      height: auto;
      border: none;
      small {
        display: block;
        font-size: 15px;
        color: #999;
      }
    }
  }
  .extra {
    background-color: #333;
  }
  .slogan {
    height: 178px;
    line-height: 58px;
    padding: 60px 100px;
    border-bottom: 1px solid #434343;
    display: flex;
    justify-content: space-between;
    a {
      height: 58px;
      line-height: 58px;
      color: #fff;
      font-size: 28px;
      i {
        font-size: 50px;
        vertical-align: middle;
        margin-right: 10px;
        font-weight: 100;
      }
      span {
        vertical-align: middle;
        text-shadow: 0 0 1px #333;
      }
    }
  }
  .copyright {
    height: 170px;
    padding-top: 40px;
    text-align: center;
    color: #999;
    font-size: 15px;
    p {
      line-height: 1;
      margin-bottom: 20px;
    }
    a {
      color: #999;
      line-height: 1;
      padding: 0 10px;
      border-right: 1px solid #999;
      &:last-child {
        border-right: none;
      }
    }
  }
}
</style>

src/views/Layout.vue 中导入使用。

<template>
  <AppTopnav/>
  <AppHeader/>
  <main class="app-body">
    <!-- 二级路由 -->
    <router-view></router-view>
  </main>
+  <AppFooter/>
</template>

<script>
import AppTopnav from '@/components/app-topnav'
import AppHeader from '@/components/app-header'
+import AppFooter from '@/components/app-footer'
export default {
  name: 'XtxLayout',
+  components: { AppTopnav, AppHeader, AppFooter }
}
</script>

<style scoped lang='less'>
+.app-body {
+  min-height: 600px;
+}
</style>

总结:

  • 完成 头部组件 和 底部组件 基础布局

15-首页-头部导航抽离

完成:提取头部分类导航组件,提供给头部,和将来的吸顶头部使用

  • 新建src/components/app-header-nav.vue 组件。
<template>
  <ul class="app-header-nav">
    <li class="home"><RouterLink to="/">首页</RouterLink></li>
    <li><a href="#">美食</a></li>
    <li><a href="#">餐厨</a></li>
    <li><a href="#">艺术</a></li>
    <li><a href="#">电器</a></li>
    <li><a href="#">居家</a></li>
    <li><a href="#">洗护</a></li>
    <li><a href="#">孕婴</a></li>
    <li><a href="#">服装</a></li>
    <li><a href="#">杂货</a></li>
  </ul>
</template>

<script>
export default {
  name: 'AppHeaderNav'
}
</script>

<style scoped lang='less'>
.app-header-nav {
  width: 820px;
  display: flex;
  padding-left: 40px;
  position: relative;
  z-index: 998;
  li {
    margin-right: 40px;
    width: 38px;
    text-align: center;
    a {
      font-size: 16px;
      line-height: 32px;
      height: 32px;
      display: inline-block;
    }
    &:hover {
      a {
          color: var(--xtx-color);
          border-bottom: 1px solid var(--xtx-color);
      }
    }
  }
}
</style>
  • app-header.vue 中使用组件。注意,删除结构和样式。
<template>
  <header class='app-header'>
    <div class="container">
      <h1 class="logo"><RouterLink to="/">小兔鲜</RouterLink></h1>
+      <AppHeaderNav />
      <div class="search">
        <i class="iconfont icon-search"></i>
        <input type="text" placeholder="搜一搜">
      </div>
      <div class="cart">
        <a class="curr" href="#">
          <i class="iconfont icon-cart"></i><em>2</em>
        </a>
      </div>
    </div>
  </header>
</template>

<script>
+import AppHeaderNav from './app-header-nav'
export default {
  name: 'AppHeader',
+  components: { AppHeaderNav }
}
</script>