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.js
對webpack
進行配置,個人配置代碼在這裏:傳送門
配置文件大概作了下面幾件事:
eslint
css
favicon
圖標路徑console.log
HardSourceWebpackPlugin
緩存打包中間步驟,提高性能gzip
autodll-webpack-plugin
將第三方模塊和一些不常常更改的文件進行提早打包,提高打包速速這裏也有一份社區總結的一份vue.config.js
的詳細配置文件: 傳送門
這裏着重說一下HardSourceWebpackPlugin
和autodll-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
對咱們的路徑補全和代碼自動引入。
vue
的webpack.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
跳轉到定義處而後再回到使用位置的操做異常快捷項目中咱們也用到了一些社區內優秀的第三方插件:
vue-awesome-swiper
: vue
版的swiper
插件,支持全部swiper
中的api
vue-lazyload
: vue
圖片懶加載插件axios
: 支持以Promise
的形式來發送http
請求nprogress
:實現頭部加載進度條vConsole
: 移動端頁面開發工具這裏只在開發環境使用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
)這裏主要講一下icon
和Toast
組件的實現過程,其它組件的實現過程小夥伴能夠看源代碼。
icon
組件icon
圖標在項目中使用的特別頻繁,我頗有必要進行一個統一封裝,方便使用。
項目中用到的icon
圖標是經過iconfont
網站進行獲取: 傳送門。這裏咱們使用的是symbol
的方式來進行實現,能夠支持多色圖標,也能夠經過font-size
,color
來進行樣式的調整。
首先咱們須要在圖標庫選好本身的圖標,以後咱們能夠爲咱們圖標所在的項目進行簡單設置:
而後咱們選擇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-on
和v-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);
};
}
};
複製代碼
到這裏,一個基本的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;
};
}
};
複製代碼
在項目中使用效果以下:
在項目的書寫過程當中,關於es6
中import
和export
使用又多了一份心得。
這裏想出一道題來考考小夥伴,有興趣的請在下方留言。
項目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
又會擦出不同的火花。