vue仿小米商城-我知道的都在这里了

vue仿小米商城 -- 小做坊实战记录

这是一个仿小米商城的vue全家桶项目,点击预览css

项目环境介绍:html

  • 系统:macos
  • 包管理工具: yarn
  • Node: v12.4.0

项目会完成的页面和功能:vue

  • 登陆页面 -> 封装表单校验方法
  • 首页 -> 实现前进后退路由动画
  • 分类页 -> 使用第三方懒加载组件
  • 详情 -> 封装popup组件
  • 购物车 -> vue列表动画

项目中有适当加入一些动画来使交互更加丰富node

项目涉及到的大概知识:react

  • vue 3.x最新脚手架使用
  • webstorm使用小技巧
  • webpack配置优化
  • vue通用组件封装
  • vw移动端适配及踩坑实践
  • jsDOC来为工具函数编写注释
  • mockjs进行数据模拟
  • 打包部署到github page
    ......等等相关知识

在编写代码的过程当中我会注意本身的代码规范以及命名的可读性,我也会在这个过程当中边学习边记录。接下来让咱们一块儿开启这一段使人期待的旅程吧!webpack

快速启动

经过以下命令咱们能够快速将项目运行,打包和发布:ios

git clone git@github.com:wangkaiwd/xiaomi-shop.git
cd xiaomi-shop
# 启动项目
yarn start
# 打包项目
yarn build
# 分析项目打包文件
yarn build:analysis
# 部署到github page
yarn deploy
复制代码

项目的目录结构以下:git

xiaomi-shop
├─ .browserslistrc
├─ .env.analysis                              // vue cli环境变量文件
├─ .gitignore
├─ README.md
├─ babel.config.js
├─ deploy.sh                                  // 项目部署脚本
├─ package.json
├─ postcss.config.js
├─ public
│    ├─ favicon.ico
│    ├─ img
│    │    └─ icons
│    ├─ index.html
│    ├─ manifest.json
│    └─ robots.txt
├─ screenshots                                // 项目截图
│    ├─ calc-scss.png
│    ├─ icon-font-link.png
│    └─ icon-font-prefix.png
├─ src
│    ├─ MiApp.vue
│    ├─ api                                   // 接口api
│    │    └─ index.js
│    ├─ assets                                // 静态资源
│    │    ├─ img
│    │    └─ styles
│    ├─ components                            // 通用组件
│    │    ├─ dialog
│    │    ├─ footerNav
│    │    ├─ guessLove
│    │    ├─ icon
│    │    ├─ layout
│    │    ├─ number
│    │    ├─ popup
│    │    ├─ skeleton
│    │    ├─ toast
│    │    └─ topHeader
│    ├─ config                                // 项目配置项
│    │    └─ navConfig.js
│    ├─ helpers                               // 帮助函数
│    │    ├─ autoRegister.js
│    │    ├─ dom
│    │    ├─ globalPlugin.js
│    │    ├─ pxToVw.js
│    │    ├─ regConfig.js
│    │    ├─ routeNavigation.js
│    │    └─ validator.js
│    ├─ http                                  // axios相关封装
│    │    ├─ axiosConfig.js
│    │    └─ request.js
│    ├─ main.js                               // 入口文件
│    ├─ registerServiceWorker.js
│    ├─ router                                // 路由配置
│    │    ├─ lazyLoading.js
│    │    └─ router.js
│    ├─ store                                 // vuex
│    │    └─ store.js
│    └─ views                                 // 项目页面
│           ├─ category
│           ├─ detail
│           ├─ example
│           ├─ home
│           ├─ homeCategory
│           ├─ login
│           ├─ mine
│           ├─ search
│           └─ shopCart
├─ vue.config.js                              // webpack配置
└─ yarn.lock
复制代码

项目建立

这里咱们使用vue官方提供的vue cli来进行项目初始化:es6

yarn global add @vue/cli
vue create xiaomi-shop
复制代码

若是发现咱们以前已经安装过了vue cli,为了确保使用的cli工具是最新版本,咱们能够为版本进行升级:github

yarn global upgrade @vue/cli
复制代码

以后能够根据cli工具的提示来选择本身须要的模块和工具来进行开发,笔者用到的是以下选项:
Babel+Router(mode:hash)+Vuex+Sass/SCSS(with dart-sass)

这里使用dart-sass是由于node-sass在下载安装过程当中老是会有各类问题

配置webpack

接下来咱们在vue.config.jswebpack进行配置,个人配置代码在这里:传送门

配置文件大概作了下面几件事:

  1. 关闭eslint
  2. 设置全局变量,方便实现不一样环境的打包
  3. 配置路径别名
  4. 配置文件扩展项
  5. 自动引入全局css
  6. 设置favicon图标路径
  7. 移除打包后的console.log
  8. 经过HardSourceWebpackPlugin缓存打包中间步骤,提高性能
  9. 开启gzip
  10. 使用autodll-webpack-plugin将第三方模块和一些不常常更改的文件进行提早打包,提高打包速速

这里也有一份社区总结的一份vue.config.js的详细配置文件: 传送门

这里着重说一下HardSourceWebpackPluginautodll-webpack-plugin插件。在项目中使用这俩个插件以后,首次打包速度并不会提高太多,可是第二次打包会节省将近80%的打包时间。若是有小伙伴遇到打包特别慢的状况能够尝试使用(React项目中配置也很简单)。

完成以后再package.json中添加相应的快捷方式:

"scripts": {
  "start": "vue-cli-service serve",
  "build": "vue-cli-service build",
  "build:analysis": "vue-cli-service build --mode analysis",
  "deploy": "sh ./deploy.sh"
},
复制代码

webstorm实用技巧

咱们能够为webstorm提供webpack配置文件,来让webstorm实现对路径别名以及后缀等配置的识别,极大的方便了webstorm对咱们的路径补全和代码自动引入。

vuewebpack.config.js在这里,它会动态识别vue.config.js中的配置:

若是咱们使用的是react-create-app进行项目构建,而且不想使用eject命令的话,能够经过写一个假的webpack.config.js文件来专门供webstorm识别:

// 这并非真的webpack配置文件,只是用来让webpack识别相应的配置
const path = require('path');
module.exports = {
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src')
    }
  }
};
复制代码

项目中咱们禁用了eslint插件,而是经过webstorm来控制咱们的代码风格,配置好以后只须要格式化一下就行了:

这里咱们JavaScript的代码分格采用预设的标准代码风格,而且设置为每行结束都要加分号

code style中也能够对css,html,sass等文件设置代码风格,你们能够本身研究一下。

这里再介绍几个我的以为特别好用的快捷键:

笔者使用的是mac

  • shift+F6: 能够对变量进行重命名,用到变量的地方也会进行更改,极大的方便了代码重构
  • ctrl+B: 当不使用鼠标的时候,能够经过键盘跳转到函数或变量定义处
  • option+enter: 弹出代码提示弹窗,在自动导入依赖模块的时候尤为好用
  • ctrl+[ / ctrl+]: 能够跳转到咱们以前或以后操做代码的位置,使经过ctrl+B跳转到定义处而后再回到使用位置的操做异常快捷

安装第三方项目依赖

项目中咱们也用到了一些社区内优秀的第三方插件:

这里只在开发环境使用vConsole:

if (process.env.NODE_ENV === 'development') {
  const VConsole = require('vconsole');
  const vConsole = new VConsole();
}
复制代码

程序界一直有一句话:不要重复造轮子。尤为是在工做中,开发比较注重效率,使用一些优秀的第三方插件以及第三方组件库能够更好的辅助咱们的工做,咱们更应该在原有的组件上进行二次封装提高开发效率。

可是若是是学习的话,手撸各类轮子仍是能提高咱们的我的实力的。虽然咱们不反对不要重复造轮子,可是并不表明咱们没有造轮子的能力。

适配方案

项目使用vw单位进行移动端适配,来兼容不一样的机型。

首先咱们要安装以下依赖:

yarn add cssnano cssnano-preset-advanced postcss-aspect-ratio-mini postcss-cssnext postcss-import postcss-px-to-viewport postcss-url postcss-viewport-units postcss-write-svg -D
复制代码

而后在postcss.config.js中添加以下配置:

module.exports = {
  plugins: {
    'postcss-import': {},
    'postcss-url': {},
    'postcss-aspect-ratio-mini': {},
    'postcss-write-svg': {
      'utf8': false
    },
    'postcss-cssnext': {},
    // document address: https://github.com/evrone/postcss-px-to-viewport/blob/master/README_CN.md
    'postcss-px-to-viewport': {
      'viewportWidth': 375,
      'unitPrecision': 5,
      'selectorBlackList': [
        '.ignore',
        '.hairlines'
      ],
      'mediaQuery': false
    },
    'postcss-viewport-units': {
      // 过滤在使用伪元素时覆盖插件生成的content而在command line 中产生的warning:https://github.com/didi/cube-ui/issues/296
      filterRule: rule => rule.nodes.findIndex(i => i.prop === 'content') === -1
    },
    'cssnano': {
      'preset': 'advanced',
      'autoprefixer': false,
      'postcss-zindex': false
    }
  }
};
复制代码

这里须要注意的是viewportWidth这个配置项,咱们这里设置为了375,而在实际工做中ui设计师会给咱们2倍图,也就是750。想要对应配置项的小伙伴能够去查阅文档:传送门

踩坑指南

在使用vw适配方案的过程当中,大概遇到了下面俩个问题:

  • 使用伪元素添加content属性时命令行会提示error
  • 设置的style没法转换为vw

这里对于命令行中的伪元素content报错我经过在babel.config.js中配置了以下代码来进行过滤:

'postcss-viewport-units': {
  // 过滤在使用伪元素时覆盖插件生成的content而在command line 中产生的warning:https://github.com/didi/cube-ui/issues/296
  filterRule: rule => rule.nodes.findIndex(i => i.prop === 'content') === -1
}
复制代码

style转换vw的问题是简单写了一个js方法来帮咱们进行转换:

export const vw = (number) => {
  const htmlWidth = document.documentElement.offsetWidth;
  return number * (100 / htmlWidth);
};
复制代码

这样咱们简单的解决了目前开发遇到的一些小问题。

通用组件设计

对于通用组件,因为在全局不少地方会进行引入,因此为了使用方便,咱们经过webpack中的require.context方法来自动全局注册,这要以后再添加全局组件也不用在进行注册了。笔者将它放到了一个单独的js文件中来执行:

// autoRegister.js
import Vue from 'vue';
// 不须要自动注册的组件
const blackList = ['MuiToast'];
const requireComponent = require.context('components', true, /Mui[A-Z]\w+\.vue$/);
requireComponent.keys().forEach(filename => {
  const componentConfig = requireComponent(filename);
  const start = filename.lastIndexOf('/') + 1;
  const end = filename.lastIndexOf('.');
  const componentName = filename.slice(start, end);
  if (blackList.includes(filename)) {return;}
  // 全局注册组件
  Vue.component(
    componentName,
    // 若是这个组件选项是经过 `export default` 导出的,
    // 那么就会优先使用 `.default`,
    // 不然回退到使用模块的根。
    componentConfig.default || componentConfig
  );
});
复制代码

固然这里有须要咱们定义好命名规范:组件名必需要以Mui开头,而且遵循驼峰命名的规则

根据项目须要,我实现了如下通用组件:

  • layout布局组件(MuiLayout,MuiHeder,MuiFooter,MuiAside,MuiContent)
  • icon字体图标组件(MuiIcon)
  • popup弹出框组件(MuiPopup)
  • dialog对话框组件(MuiDialog)
  • toast全局提示(MuiToast)
  • number商品添加按钮(MuiNumber)

这里主要讲一下iconToast组件的实现过程,其它组件的实现过程小伙伴能够看源代码。

icon组件

icon图标在项目中使用的特别频繁,我颇有必要进行一个统一封装,方便使用。

项目中用到的icon图标是经过iconfont网站进行获取: 传送门。这里咱们使用的是symbol的方式来进行实现,能够支持多色图标,也能够经过font-sizecolor来进行样式的调整。

首先咱们须要在图标库选好本身的图标,以后咱们能够为咱们图标所在的项目进行简单设置:

而后咱们选择symbol类型的图标,并将地址复制到pubic/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, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <link rel="icon" href="<%= BASE_URL %>favicon.ico">
  <title>小米商城</title>
  <script src="//at.alicdn.com/t/font_1253950_whicd7mh5w.js"></script>
</head>
<body>
<noscript>
  <strong>We're sorry but vue-cli-demo 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>
复制代码

准备工做完成后,咱们创建MuiIcon文件,添加以下代码:

<template>
  <svg
    class="mui-icon"
    aria-hidden="true"
  >
    <use xlink:href="#icon-xxx"></use>
  </svg>
</template>

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

<style lang="scss" scoped>
  .mui-icon {
    display: inline-block;
    width: 1em; height: 1em;
    vertical-align: top;
    fill: currentColor;
    overflow: hidden;
  }
</style>
复制代码

接下来的内容再也不介绍css

代码中的xxx在使用过程当中须要替换为对应icon的名字,咱们经过为Icon组件传入一个name属性来动态设置图标名称。因为上边为项目图标设置了统一前缀mi,因此这里要进行以下修改:

<template>
  <svg
    class="mui-icon"
    aria-hidden="true"
  >
    <use :xlink:href="`#mi-${name}`"></use>
  </svg>
</template>

<script>
  export default {
    name: 'MiIcon',
    props: {
      name: { type: String, required: true }
    }
  };
</script>
复制代码

这样咱们就实现了一个最基础的icon组件,能够在项目中这样使用:

<mui-icon name="logo"></mui-icon>
复制代码

在平常的项目中,咱们还会遇到以下需求:

  • 鼠标移入icon图标,图标旋转
  • 点击icon进行页面跳转

诸如此类的需求咱们不可能一个一个为icon组件添加对应的属性和方法,这里咱们运用到vue中几个不太经常使用的api:

  • v-onv-bind绑定对象: 会将对象的属性分发到当前节点
  • $attrs: 能够获取没有在props中定义的属性
  • $listens:获取父做用域中不含.native修饰器的v-on事件监听器
  • inheritAttrs: 可让非props中添加的属性再也不显示到icon组件的根节点上
<template>
  <svg
    class="mui-icon"
    aria-hidden="true"
    v-bind="$attrs"
    v-on="$listeners"
  >
    <use :xlink:href="`#mi-${name}`"></use>
  </svg>
</template>

<script>
  export default {
    name: 'MiIcon',
    inheritAttrs: false, // 默认值为true,是否在根节点上显示传入的没有经过props接收的属性
    props: {
      name: { type: String, required: true }
    }
  };
</script>
复制代码

这样书写以后,icon组件就能够接受任意的svg原生支持的事件和属性。

react中,咱们也会碰到相似的需求,而且在react中不会帮咱们对class进行合并。因此在react中的思路大概以下:

  • 单独对class进行处理,手动拼接为多类名格式(Vue这里已经帮咱们作好)
  • 经过...restProps将其他的属性扩展到对应的节点上

toast组件

这里的toast和其它组件的使用方式不同,它是经过使用Vue.use来进行全局注册。当咱们使用Vue.use方式时,咱们传入的内容要暴露一个install方法,这个方法会传入vue实例以及配置项options做为参数。

export default {
  install (Vue,options) {
    
  }
};
复制代码

咱们简单瞄一眼源码会发现:在执行Vue.use的时候,也会执行上边的install方法

vue社区中,咱们常常会看到经过vue实例上的函数来直接调用组件的例子:

this.$toast('这是一个toast');
this.$toast({ message: '加载中...', type: 'loading', mask: true })
复制代码

这种调用方式是由于咱们在vue的原型上绑定了对应的方法,以后即可以在vue的实例对象上直接访问,结合咱们上面说到的内容,代码大概是这样的:

export default {
  install (Vue) {
    Vue.prototype.$toast = (options) => {
      // doSomeThing
    };
  }
};
复制代码

这样咱们就能够经过Vue.use来为vue原型上添加$toast方法,方便直接在组件中调用。

到这里,咱们大概肯定了咱们组件的调用方式,调用时的传参咱们进行以下设计:

  • message:提示信息
  • mask: 是否有遮罩层
  • type: 提示类型,当传入loading时,能够显示加载状态
  • icon: 提示字体图标展现
  • duration: 提示信息展现事件,单位毫秒,传入0不会自动关闭

贴上个人实现代码(不包括css):

<template>
  <transition name="fade">
    <div class="mui-toast" v-if="visible">
      <div class="mui-toast-content" :class="{hasIcon}">
        <div class="mui-toast-icon" v-if="hasIcon">
          <mui-icon class="mui-toast-icon-loading" v-if="isLoading" name="loading"></mui-icon>
          <mui-icon v-else :name="icon"></mui-icon>
        </div>
        {{message}}
      </div>
      <div class="mui-toast-mask" v-if="mask"></div>
    </div>
  </transition>
</template>

<script>
  export default {
    name: 'MuiToast',
    props: {
      message: {
        type: String,
      },
      mask: {
        type: Boolean,
        default: false
      },
      type: {
        type: String,
        validator (value) {
          return ['default', 'loading'].includes(value);
        },
        default: 'default'
      },
      icon: { type: String },
      duration: {
        type: Number,
        default: 3000
      }
    },
    data () {
      return {
        visible: false
      };
    },
    computed: {
      isLoading () {
        return this.type === 'loading';
      },
      hasIcon () {
        return this.isLoading || this.icon;
      }
    },
    mounted () {
      this.visible = true;
      this.autoClose();
    },
    methods: {
      closeToast () {
        this.visible = false;
        this.$nextTick(() => {
          this.$el.remove();
          this.$destroy();
        });
      },
      autoClose () {
        if (this.duration === 0 || this.type === 'loading') {return;}
        setTimeout(() => {
          this.closeToast();
        }, this.duration);
      }
    }
  };
</script>
复制代码

动画实现的思路是先在data中定义visible:false,以后再组件挂载完成后设置visible:true,这样结合transition组件就能够实现组件出现和销毁时的动画了。

须要注意的是,若是咱们分别为transition中的根元素中的子元素指定过渡动画的时候,须要显式的指定过渡时间,不然动画效果不会生效

文档地址

在组件建立完成后,咱们并不能直接调用,而是要经过vue的一些api来动态生成组件,并将内容渲染到body中:

export default {
  install (Vue) {
    Vue.prototype.$toast = (options) => {
      // 为`Vue.extend`传入`Toast`组件配置项来生成构造函数
      const componentClass = Vue.extend(Toast);
      // 经过构造函数动态建立`toastInstance`
      const toastInstance = new componentClass({
        // 经过propsData来进行参数传递
        propsData: options,
      });
      // 若是没有为$mount指定渲染节点,能够经过原生DOM API来将组件插入到文档中
      toastInstance.$mount();
      document.body.appendChild(toastInstance.$el);
    };
  }
};

复制代码

关于动态建立vue组件并渲染到页面中,能够参考这篇文章:

到这里,一个基本的Toast组件大概就完成了

通过测试,我大概发现了以下问题:

  • 屡次点击重复建立组件
  • 没法在组件外部关闭组件,致使loading没法关闭
  • 提供简化调用方式: this.$toast(message),并不用传入复杂的配置项,方便使用

这里咱们经过一个外部变量来接收生成的组件实例,并在每次建立时将旧的实例和DOM结构从页面中删除。在经过函数建立组件后会返回一个关闭组件函数,咱们能够直接调:

import Toast from './MuiToast';
let toastInstance = null;
export default {
  install (Vue) {
    Vue.prototype.$toast = (options) => {
      // 组件已经存在的话销毁从新建立
      if (toastInstance) { // 这里能够经过实例来直接调用组件中的方法
        toastInstance.closeToast();
      }
      const componentClass = Vue.extend(Toast);
      if (typeof options === 'string') {
        options = { message: options };
      }
      toastInstance = new componentClass({
        propsData: options,
      });
      toastInstance.$mount();
      document.body.appendChild(toastInstance.$el);
      // 在组件调用后返回关闭函数
      return toastInstance.closeToast;
    };
  }
};
复制代码

在项目中使用效果以下:

知识趣谈

在项目的书写过程当中,关于es6importexport使用又多了一份心得。

这里想出一道题来考考小伙伴,有兴趣的请在下方留言。

项目src目录下新建3个文件: a.js,b.js,c.js,其中a.js是入口文件(即最早执行),每一个文件中的代码以下:

// a.js
console.log('a.js');
import './b.js'

// b.js
console.log('b.js');
import './c.js'

// c.js
console.log('c.js');
import './a.js'
复制代码

最后的输出结果是怎样的呢?反正这里是颠覆了笔者的认知

参考资料: Module的加载实现

结语

此次的项目书写和总结大概耗费了2个月的时间,笔者将本身看到的和学到的东西都分享了出来,但愿对你们有帮助。

开源不易,但愿你们能给个start给与鼓励,让社区中乐于分享的开发者创造出更好的做品。

源码地址:xiaomi-shop

个人另外一个vue实战项目:vue+element后台管理系统,当vue结合element ui又会擦出不同的火花。

相关文章
相关标签/搜索