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