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又會擦出不同的火花。

相關文章
相關標籤/搜索