这是一个仿小米商城的
vue
全家桶项目,点击预览
项目环境介绍:
- 系统:
macos
- 包管理工具:
yarn
Node
:v12.4.0
项目会完成的页面和功能:
- 登录页面 -> 封装表单校验方法
- 首页 -> 实现前进后退路由动画
- 分类页 -> 使用第三方懒加载组件
- 详情 -> 封装
popup
组件 - 购物车 ->
vue
列表动画
项目中有适当加入一些动画来使交互更加丰富
项目涉及到的大概知识:
vue 3.x
最新脚手架使用webstorm
使用小技巧webpack
配置优化vue
通用组件封装vw
移动端适配及踩坑实践jsDOC
来为工具函数编写注释mockjs
进行数据模拟- 打包部署到
github page
......等等相关知识
在编写代码的过程中我会注意自己的代码规范以及命名的可读性,我也会在这个过程中边学习边记录。接下来让我们一起开启这一段令人期待的旅程吧!
通过如下命令我们可以快速将项目运行,打包和发布:
git clone git@github.com:wangkaiwd/xiaomi-shop.git
cd xiaomi-shop
# 启动项目
yarn start
# 打包项目
yarn build
# 分析项目打包文件
yarn build:analysis
# 部署到github page
yarn deploy
项目的目录结构如下:
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
来进行项目初始化:
yarn global add @vue/cli
vue create xiaomi-shop
如果发现我们之前已经安装过了vue cli
,为了确保使用的cli
工具是最新版本,我们可以为版本进行升级:
yarn global upgrade @vue/cli
之后可以根据cli
工具的提示来选择自己需要的模块和工具来进行开发,笔者用到的是如下选项:
Babel
+Router(mode:hash)
+Vuex
+Sass/SCSS(with dart-sass)
这里使用
dart-sass
是因为node-sass
在下载安装过程中总是会有各种问题
接下来我们在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
提供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
图标是通过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
和其它组件的使用方式不一样,它是通过使用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
又会擦出不一样的火花。
如果你想自己配置webpack
可以参考我的这篇文章:webpack入门手记