day-059-fifty-nine-20230427-移动端商城
移动端商城
vant2
-
看vant官网,发现vue2要用vant2,进入vant2官网。
-
Vant 4.x
版本 的文档,适用于Vue 3
开发。- 使用
Vue 2
,请浏览Vant 2
文档。
- 使用
安装vant2
- 安装vant2插件
npm i vant@latest-v2
使用vant2组件
选一种使用
-
方式一-自动按需引入组件 (推荐)
-
安装插件
npm i babel-plugin-import -D
-
在
/babel.config.js
中添加module.exports = { presets: [ '@vue/cli-plugin-babel/preset' ], plugins: [ ['import', { libraryName: 'vant', libraryDirectory: 'es', style: true }, 'vant'] ] }
-
在
/src/main.js
中导入import 'vant/lib/index.css'; import { Button } from 'vant'; Vue.use(Button)
-
不过一般在
/src/vant/index.js
中统一配置,以统一处理并解耦关于vant的组件引入// 1. 引入vue; import Vue from "vue"; // 2. 引入vant的css样式 import "vant/lib/index.css"; // 3. 引入button组件,在组件中使用时是用`van-button`使用 import { Button } from "vant"; Vue.use(Button);
-
在
/src/main.js
中引入/src/vant/index.js
并让它运行一次import './vant/index.js'
-
-
-
方式二-手动按需引入组件
-
在不使用插件的情况下,可以手动引入需要的组件。
-
在具体的某个组件内
<template> <div class="home"> <van-button type="danger">使用van-button组件</van-button> <h1>h1</h1> <h3>h3</h3> </div> </template> <script> import 'vant/lib/button/style';//引入button的样式,使用这个的前提是安装了vant这个ui框架。 import Button from 'vant/lib/button';//引入button的js代码,使用这个的前提是安装了vant这个ui框架。 export default { name: "HomeView", components: { "van-button": Button,//要重命名引入的组件,最好是两个字母,中间用连接符分隔。 }, }; </script>
-
-
方式三-导入所有组件
-
Vant支持一次性导入所有组件,引入所有组件会增加代码包体积,因此不推荐这种做法。
-
在
/src/main.js
中导入//1. 全局导入 //import Vue from 'vue';//这个一般不用写,因为之前Vue脚手架已经导入了 import Vant from 'vant'; import 'vant/lib/index.css'; Vue.use(Vant);
-
vant2组件的注册方式
全局注册
-
全局组件-要全局注册
-
在
/src/main.js
或/src/vant/index.js
中导入对应的组件import Vue from 'vue'; import { Button } from 'vant';
-
全局注册组件
-
通过 Vue.use 注册
// 方式一. 通过 Vue.use 注册 // 注册完成后,在模板中通过 <van-button> 或 <VanButton> 标签来使用按钮组件 Vue.use(Button);
-
通过 Vue.component 注册
// 方式二. 通过 Vue.component 注册 // 注册完成后,在模板中通过 <van-button> 标签来使用按钮组件 Vue.component(Button.name, Button);
-
- 全局注册后,在项目所有的vue文件中都可以直接在模板中使用
-
局部注册
-
局部组件-要局部注册
-
在具体的一个vue组件中调用
<template> <div class="home"> <van-button type="primary">主要按钮</van-button> <van-button type="info">信息按钮</van-button> <van-button type="default">默认按钮</van-button> <van-button type="warning">警告按钮</van-button> <van-button type="danger">危险按钮</van-button> <h1>h1</h1> <h3>h3</h3> </div> </template> <script> import { Button } from 'vant'; export default { name: "HomeView", components: { "van-button": Button,//要重命名引入的组件,最好是两个字母,中间用连接符分隔。 }, }; </script>
-
局部注册后,你只可以在当前组件中使用注册的 Vant 组件。
-
-
仅演示,一般项目不用这种方式,有点麻烦
- 但如果要放博客或vue2与vue3兼容,这种方式比较清晰
作用域插槽
- Vant 提供了丰富的组件插槽,通过插槽可以对组件的某一部分进行个性化定制。
<template>
<div>
<van-checkbox v-model="checked">
<!-- 使用组件提供的 icon 插槽 -->
<!-- 将默认图标替换为个性化图片 -->
<template #icon="props">
<img :src="props.checked ? 'https://img01.yzcdn.cn/vant/user-active.png' : 'https://img01.yzcdn.cn/vant/user-inactive.png'" />
</template>
</van-checkbox>
</div>
</template>
<script>
export default {
data() {
return {
checked: true,
};
},
};
</script>
浏览器适配
移动端适配
-
Viewport 布局
- Vant 默认使用 px 作为样式单位,如果需要使用 viewport 单位 (vw, vh, vmin, vmax),推荐使用 postcss-px-to-viewport 进行转换。
- 步骤:
-
安装postcss插件和postcss-px-to-viewport插件
//npm i postcss -D //vant默认已经安装了 npm i postcss-px-to-viewport -D
-
/postcss.config.js
module.exports = { plugins: { 'postcss-px-to-viewport': { viewportWidth: 375, }, }, };
-
-
Rem 布局适配
- 根标签设置rem基准值,使用rem单位
- px em rem 区别?
- px是物理像素,em是相对父元素字体大小,rem是相对根元素字体大小。
- 步骤
-
引入插件,用于在根标签设置rem基准值
npm i -S amfe-flexible yarn add amfe-flexible
-
要求在根元素中引入一个js文件–即amfe-flexible插件中生成的js文件
-
生效版:在
/src/main.js
中导入import "../node_modules/amfe-flexible/index.js"
-
官网推荐:在模块文件
/public/index.html
中使用<script src="./node_modules/amfe-flexible/index.js"></script>
- 不过不生效,是因为路径出错了。没能引到amfe-flexible插件中生成的js文件
- 应该要在vue-cli或webpack中配置好,或者script的标签写对路径
- 不过不生效,是因为路径出错了。没能引到amfe-flexible插件中生成的js文件
-
-
使用rem单位
- 自己计算转化
- 自动计算转化,通过postcss-pxtorem这个postcss插件
- 在项目中可以直接写px,自动转化为rem,不需要进行计算了
- 步骤:
-
安装postcss插件和postcss-pxtorem插件
//npm i postcss -D //vant默认已经安装了 npm install postcss postcss-pxtorem@5.1.1 --save-dev//vue2的postcss-pxtorem要用@5.1.1,最新版不太支持。vue3可以使用最新版比较多。
-
/postcss.config.js
//设计稿的尺寸是 375 // postcss.config.js module.exports = { plugins: { 'postcss-pxtorem': { rootValue: 37.5, propList: ['*'], }, }, };
-
-
桌面端适配
- Vant 是一个面向移动端的组件库,因此默认只适配了移动端设备,这意味着组件只监听了移动端的 触摸touch 事件,没有监听桌面端的 鼠标mouse 事件。
- 如果需要在桌面端使用 Vant,可以引入Vant提供的 @vant/touch-emulator。
- 这个库会在桌面端自动将 mouse 事件转换成对应的 touch 事件,使得组件能够在桌面端使用。
- 如果需要在桌面端使用 Vant,可以引入Vant提供的 @vant/touch-emulator。
- 步骤:
-
安装@vant/touch-emulator模块
npm i @vant/touch-emulator -S
-
在
/src/main.js
中导入// 引入模块后自动生效 import '@vant/touch-emulator';
-
底部安全区适配
- iPhone X 等机型底部存在底部指示条,指示条的操作区域与页面底部存在重合,容易导致用户误操作,因此我们需要针对这些机型进行安全区适配。
- Vant 中部分组件提供了
safe-area-inset-top
或safe-area-inset-bottom
属性,设置该属性后,即可在对应的机型上开启适配。
- Vant 中部分组件提供了
- 步骤:
-
在 head 标签中添加 meta 标签,并设置 viewport-fit=cover 值。
-
/public/index.html
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover" />
-
-
在vue组件中使用
safe-area-inset-top
或safe-area-inset-bottom
属性。-
顶部安全区适配
<template> <div class="home-root"> <!-- 开启顶部安全区适配 --> <van-nav-bar safe-area-inset-top /> </div> </template>
-
底部安全区适配
<template> <div class="home-root"> <!-- 开启底部安全区适配 --> <van-number-keyboard safe-area-inset-bottom /> </div> </template>
-
-
基础页面
<template>
<div style="height: 2000px">
<h1>ClassifyView</h1>
</div>
</template>
<script>
export default {};
</script>
<style>
</style>
路由配置
- 写五个vue页面组件,用于让van-tabbar标签来跳转对应的页面。
/src/views/IndexView.vue
<template> <div style="height: 2000px"> <h1>IndexView</h1> </div> </template> <script> export default {}; </script>
/src/views/ClassifyView.vue
<template> <div style="height: 2000px"> <h1>ClassifyView</h1> </div> </template> <script> export default {}; </script>
/src/views/CartView.vue
<template> <div style="height: 2000px"> <h1>CartView</h1> </div> </template> <script> export default {}; </script>
/src/views/MyView.vue
<template> <div style="height: 2000px"> <h1>MyView</h1> </div> </template> <script> export default {}; </script>
/src/views/LoginView.vue
<template> <div style="height: 2000px"> <h1>LoginView</h1> </div> </template> <script> export default {}; </script>
- 配置路由及重定向
/src/router/index.js
import Vue from "vue"; import VueRouter from "vue-router"; import IndexView from "../views/IndexView.vue"; Vue.use(VueRouter); const ClassifyView = () => import(/* webpackChunkName: "ClassifyView" */ "../views/ClassifyView.vue"); const CartView = () => import(/* webpackChunkName: "CartView" */ "../views/CartView.vue"); const MyView = () => import(/* webpackChunkName: "MyView" */ "../views/MyView.vue"); const LoginView = () => import(/* webpackChunkName: "LoginView" */ "../views/LoginView.vue"); const routes = [ { path: "/", redirect: "/IndexView",//让`/#/`可以跳转到`/#/IndexView` //component: IndexView, }, { path: "*",//让`/#/未定义路径`可以跳转到`/#/IndexView` redirect: "IndexView", }, { path: "/IndexView", name: "IndexView", component: IndexView, }, { path: "/ClassifyView", name: "ClassifyView", component: ClassifyView, }, { path: "/CartView", name: "CartView", component: CartView, }, { path: "/MyView", name: "MyView", component: MyView, }, { path: "/LoginView", name: "LoginView", component: LoginView, }, ]; const router = new VueRouter({ routes, }); export default router;
- 使用vant组件
- 用[].forEach()
// 1. 引入vue; import Vue from "vue"; // 2. 引入vant的css样式 import "vant/lib/index.css"; // 3. 引入button组件,在组件中使用时是用`van-button`使用 import { Button } from "vant"; Vue.use(Button); // 更集中 import { Tabbar, TabbarItem } from 'vant'; let list01 = [Tabbar, TabbarItem] list01.forEach(item=>[ Vue.use(item) ]) // 等价于:分散的写法 // import { Tabbar, TabbarItem } from 'vant'; // Vue.use(Tabbar); // Vue.use(TabbarItem);
/src/components/FooterPage.vue
<template> <div> <van-tabbar v-model="active" active-color="#1baeae" :route="true"> <van-tabbar-item icon="home-o" to="/IndexView">首页</van-tabbar-item> <van-tabbar-item icon="search" dot to="/ClassifyView"> <span>分类</span> <template #icon="props"> <i class="iconfont icon-fenlei"></i> </template> </van-tabbar-item> <van-tabbar-item icon="shopping-cart-o" badge="5" to="/CartView"> 购物车 </van-tabbar-item> <van-tabbar-item icon="user-o" badge="20" to="/MyView"> 我的 </van-tabbar-item> </van-tabbar> </div> </template> <script> export default { data() { return { active: 0, }; }, }; </script>
- 用[].forEach()
- 改Tabbar图标选中颜色样式
/src/components/FooterPage.vue
<template> <div> <van-tabbar v-model="active" active-color="#1baeae"></van-tabbar> </div> </template>
- 用自定义图标
-
/src/components/FooterPage.vue
<template> <div> <i class="iconfont icon-fenlei"></i> </div> </template> <style lang="less" scoped> @import url("../assets/icon.less"); </style>
-
- App组件中使用,但一些页面不用Tabbar组件显示
/src/App.vue
<template> <div id="app"> <router-view /> <FooterPage></FooterPage> </div> </template> <script> import FooterPage from "./components/FooterPage.vue"; export default { components: { FooterPage, }, }; </script>
- 用路由的mate属性控制App中的Tabbar的显隐
/src/router/index.js
const routes = [ { path: "/", redirect: "/IndexView",//让`/#/`可以跳转到`/#/IndexView` //component: IndexView, }, { path: "*",//让`/#/未定义路径`可以跳转到`/#/IndexView` redirect: "IndexView", }, { path: "/IndexView", name: "IndexView", component: IndexView, meta: { isShowFooter: true,//在这个meta.isShowFooter为true的,显示App.vue中的导航栏。 }, }, { path: "/ClassifyView", name: "ClassifyView", component: ClassifyView, meta: { isShowFooter: true, }, }, { path: "/CartView", name: "CartView", component: CartView, meta: { isShowFooter: true, }, }, { path: "/MyView", name: "MyView", component: MyView, meta: { isShowFooter: true, }, }, { path: "/LoginView", name: "LoginView", component: LoginView, // meta: { // isShowFooter: false,//或不写; // }, }, ]; const router = new VueRouter({ routes, });
/src/App.vue
<template> <div id="app"> <router-view /> <FooterPage v-show="flag"></FooterPage> </div> </template> <script> import FooterPage from "./components/FooterPage.vue"; export default { data() { return { flag: true, }; }, watch: { $route: { handler(newValue) { console.log(`newValue-->`, newValue); this.flag = newValue?.meta?.isShowFooter || false; }, immediate: true, }, }, components: { FooterPage, }, }; </script>
- 开启van-tabbar标签的路由模式,让van-tabbar自动随着路由路径的变化而切换当前选中的van-tabbar-item标签。
/src/components/FooterPage.vue
<template> <div> <van-tabbar v-model="active" active-color="#1baeae" :route="true"></van-tabbar> </div> </template>
- 最终代码
/src/App.vue
最终代码<template> <div id="app"> <router-view /> <FooterPage v-show="flag"></FooterPage> </div> </template> <script> import FooterPage from "./components/FooterPage.vue"; export default { data() { return { flag: true, }; }, watch: { $route: { handler(newValue) { console.log(`newValue-->`, newValue); this.flag = newValue?.meta?.isShowFooter || false;//在这个this.$route.meta.isShowFooter为true的,显示App.vue中的导航栏。 }, immediate: true, }, }, components: { FooterPage, }, }; </script> <style lang="less" scoped> // #app { // font-family: Avenir, Helvetica, Arial, sans-serif; // -webkit-font-smoothing: antialiased; // -moz-osx-font-smoothing: grayscale; // text-align: center; // color: #2c3e50; // nav { // padding: 30px; // a { // font-weight: bold; // color: #2c3e50; // &.router-link-exact-active { // color: #42b983; // } // } // } // } </style>
/src/router/index.js
最终代码import Vue from "vue"; import VueRouter from "vue-router"; import IndexView from "../views/IndexView.vue"; Vue.use(VueRouter); const ClassifyView = () => import(/* webpackChunkName: "ClassifyView" */ "../views/ClassifyView.vue"); const CartView = () => import(/* webpackChunkName: "CartView" */ "../views/CartView.vue"); const MyView = () => import(/* webpackChunkName: "MyView" */ "../views/MyView.vue"); const LoginView = () => import(/* webpackChunkName: "LoginView" */ "../views/LoginView.vue"); const routes = [ { path: "/", redirect: "/IndexView",//让`/#/`可以跳转到`/#/IndexView` //component: IndexView, }, { path: "*",//让`/#/未定义路径`可以跳转到`/#/IndexView` redirect: "IndexView", }, { path: "/IndexView", name: "IndexView", component: IndexView, meta: { isShowFooter: true,//在这个meta.isShowFooter为true的,显示App.vue中的导航栏。 }, }, { path: "/ClassifyView", name: "ClassifyView", component: ClassifyView, meta: { isShowFooter: true, }, }, { path: "/CartView", name: "CartView", component: CartView, meta: { isShowFooter: true, }, }, { path: "/MyView", name: "MyView", component: MyView, meta: { isShowFooter: true, }, }, { path: "/LoginView", name: "LoginView", component: LoginView, // meta: { // isShowFooter: false,//或不写; // }, }, ]; const router = new VueRouter({ routes, }); export default router;
/src/components/FooterPage.vue
最终代码<template> <div> <van-tabbar v-model="active" active-color="#1baeae" :route="true"> <van-tabbar-item icon="home-o" to="/IndexView">首页</van-tabbar-item> <van-tabbar-item icon="search" dot to="/ClassifyView"> <span>分类</span> <template #icon> <i class="iconfont icon-fenlei"></i> </template> </van-tabbar-item> <van-tabbar-item icon="shopping-cart-o" badge="5" to="/CartView"> 购物车 </van-tabbar-item> <van-tabbar-item icon="user-o" badge="20" to="/MyView"> 我的 </van-tabbar-item> </van-tabbar> </div> </template> <script> export default { data() { return { active: 0, }; }, }; </script> <style lang="less" scoped> @import url("../assets/icon.less"); </style>
自定义图标
-
/src/components/FooterPage.vue
<template> <div> <i class="iconfont icon-fenlei"></i> </div> </template> <style lang="less" scoped> @import url("../assets/icon.less"); </style>
-
/src/assets/icon.less
@font-face { font-family: "iconfont"; /* Project id 3141501 */ src: url('//at.alicdn.com/t/font_3141501_ftrvn4vtda5.woff2?t=1642133344053') format('woff2'), url('//at.alicdn.com/t/font_3141501_ftrvn4vtda5.woff?t=1642133344053') format('woff'), url('//at.alicdn.com/t/font_3141501_ftrvn4vtda5.ttf?t=1642133344053') format('truetype'); } .iconfont { font-family: "iconfont" !important; font-size: 16px; font-style: normal; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; font-size:22px; } .icon-gouwucheman:before { content: "\e600"; } .icon-user:before { content: "\e63c"; } .icon-fenlei:before { content: "\e612"; }
滚动修改样式
-
样式一般用class代替,用一个变量来控制是否添加一个类名。
<template> <div class="home-root"> <div :class="['the-header', flag ? 'is-active' : '']">新峰商城</div> </div> </template> <script> export default { data() { return { flag: false, }; }, }; </script> <style lang="less" scoped> .home-root { .the-header { opacity: 0.2; &.is-active { opacity: 1; } } } </style>
-
一般使用DOMElement.addEventListener()与DOMElement.removeEventListener()操作事件的绑定与解绑。
- 主要原因是DOM2级事件可以绑定多个。
- 用DOM0级,只能绑定一个,可能会对其它组件中绑定的事件造成影响。
- 一般函数方法在methods中进行定义,允许在vue组件的一些生命周期中可以调用到。
- methods中的方法,可以被DOMElement.addEventListener()与DOMElement.removeEventListener()操作事件的绑定与解绑。
- 在生命周期中定义的方法,一般很难可以被重复调用。
- methods中的方法,可以被DOMElement.addEventListener()与DOMElement.removeEventListener()操作事件的绑定与解绑。
- 一般在created()为window之类的全局变量绑定一个方法。
- 如果涉及到DOM,可以在mounted()中绑定。
- 在destroyed()为绑定的事件进行解绑,防止内存一直占用,DOM删除不了,造成内存泄露。
- 事件绑定与解绑说明
-
DOM0级事件绑定与解绑
-
DOM0级事件绑定方式所绑定的事件,可以直接在生命周期中定义
<script> export default { data() { return { flag: false, }; }, created() { console.log(`window-->`, window); window.onscroll = () { let html = document.documentElement || document.body; console.log(`111-->`, 111); if (html.scrollTop > 100) { this.flag = true; } else { this.flag = false; } }; }, destroyed() { window.onscroll = null }, }; </script>
-
-
DOM2级事件的绑定与解绑
-
DOM0级事件绑定方式所绑定的事件,一般不能在生命周期中定义。而要在mothods中进行定义。
- 以便在绑定与解绑时,都能访问到同一个事件。
<script> export default { data() { return { flag: false, }; }, created() { console.log(`window-->`, window); //创建组件的时候向事件池中添加方法--DOM已经存在 window.addEventListener("scroll", this.showHeader); }, methods: { showHeader() { let html = document.documentElement || document.body; console.log(`111-->`, 111); if (html.scrollTop > 100) { this.flag = true; } else { this.flag = false; } }, }, destroyed() { window.removeEventListener("scroll", this.showHeader); }, }; </script>
-
-
- 主要原因是DOM2级事件可以绑定多个。
-
例子完整版
<template> <div class="home-root"> <div :class="['the-header', flag ? 'is-active' : '']">新峰商城</div> <div class="the-body"> <h1>11111</h1> <h1>11111</h1> <h1>11111</h1> </div> </div> </template> <script> export default { data() { return { flag: false, }; }, created() { console.log(`window-->`, window); // window.onscroll = () => {}; //创建组件的时候向事件池中添加方法--DOM已经存在 window.addEventListener("scroll", this.showHeader); }, methods: { showHeader() { let html = document.documentElement || document.body; console.log(`111-->`, 111); if (html.scrollTop > 100) { this.flag = true; } else { this.flag = false; } }, }, destroyed() { window.removeEventListener("scroll", this.showHeader); }, }; </script> <style lang="less" scoped> .home-root { .the-header { width: 100%; height: 50px; background-color: #1baeae; text-align: center; color: #fff; font-size: 20px; line-height: 50px; // position: fixed; position: sticky; top: 0; left: 0; opacity: 0.2; &.is-active { opacity: 1; } } .the-body { height: 2000px; } } </style>