vuecli4+vant移动端响应式项目踩坑记录

关键词

  • @vue/cli4, vant, rem, svg, axios

参考链接

一. 使用vue/cli4

  1. 全局安装@vue/cli最新版本
    yarn add -g @vue/cli 或者 npm install @vue/cli -g

  2. 查看安装的vue/cli版本 vue --version

  3. 创建vue项目 vue create hello-world

  4. 创建项目时候让选择,默认or手动,一般选择手动,按照提示选择自己需要的即可。我选择了以下2个关键的。

    • CSS Pre-processors
      • scss(node scss)
    • eslint(prettier)
  5. vue/cli有个小坑。如果删除了依赖,自己安装一遍,发现有警告⚠️:warning " > sass-loader@8.0.2" has unmet peer dependency "webpack@^4.36.0 || ^5.0.0".。应该是脚手架的坑,暂时不知怎么去改。

二. 使用vant

  1. 安装插件yarn add vant
  2. 按需引入插件yarn add babel-plugin-import --dev(注:这个插件装到开发依赖)
  3. 自动按需引入使用示例:
// template
<van-button type="default">默认按钮</van-button>
// script
import { Button } from "vant";

components: {
    [Button.name]: Button
}

三. 加入响应式布局

1. rem适配插件
  • postcss-pxtorem 是一款 postcss 插件,用于将单位转化为 rem 。(!安装到开发依赖 --dev)
  • lib-flexible 用于动态改变根节点的font-size,设置 rem 基准值。(!安装到生产依赖 --save)
  • 【小坑】lib-flexible按照官网提供的在html引入js会报错,改为在main.js中引入依赖import "amfe-flexible/index.js";就ok了。
2. PostCSS配置
  1. vue.config.js中配置
    css: {
    loaderOptions: {
      postcss: {
        plugins: [
          require("autoprefixer")({
            // 配置使用 autoprefixer
            overrideBrowserslist: ["last 15 versions"]
          }),
          require("postcss-pxtorem")({
            rootValue: 37.5, // 换算的基数
            // 忽略转换正则匹配项。插件会转化所有的样式的px。比如引入了三方UI,也会被转化。目前我使用 selectorBlackList字段,来过滤
            //如果个别地方不想转化px。可以简单的使用大写的 PX 或 Px 。
            selectorBlackList: ["ig"],
            propList: ["*"]
          })
        ]
      }
    }
    }
    
  2. postcss.config.js中配置
    module.exports = {
      plugins: {
        autoprefixer: {
          overrideBrowserslist: ['Android >= 4.0', 'iOS >= 8'],
        },
        'postcss-pxtorem': {
          rootValue: 37.5, // ⚠️这里是设计稿的1/10
          propList: ['*'],
          mediaQuery: true
        },
      },
    };
    

在配置 postcss-loader 时,应避免 ignore node_modules 目录,否则将导致 Vant 样式无法被编译

四. 图标库:封装svg图标组件

  • 原因:svg放大后不失真,可以像css一样设置颜色,非常方便。
  • 使用步骤:
1. 建立如下目录结构:
icon
    index.js
    svg
        test1.svg // (去阿里的iconfont随便下载一个来试验)
        test2.svg

components
    SvgIcon.vue
2. components/SvgIcon.vue
<template>
  <svg :class="svgClass" aria-hidden="true" v-on="$listeners">
    <use :xlink:href="iconName" />
  </svg>
</template>

<script>
export default {
  name: "SvgIcon",
  props: {
    iconClass: {
      type: String,
      required: true
    },
    className: {
      type: String,
      default: ""
    }
  },
  computed: {
    iconName() {
      return `#icon-${this.iconClass}`;
    },
    svgClass() {
      if (this.className) {
        return "svg-icon " + this.className;
      } else {
        return "svg-icon";
      }
    }
  }
};
</script>

<style scoped>
.svg-icon {
  width: 16px;
  height: 16px;
  vertical-align: -3px;
  fill: currentColor;
  overflow: hidden;
}
</style>

3. icon/index.js
import Vue from "vue";
import SvgIcon from "@/components/SvgIcon"; // svg组件

// register globally
Vue.component("svg-icon", SvgIcon);

const req = require.context("./svg", false, /\.svg$/);

const requireAll = requireContext => requireContext.keys().map(requireContext);
requireAll(req);

4. 配置vue.config.js
// 添加svg-sprite-loader,同时不要忽略了其他不作为图片的svg文件, 
// file-loader 用来处理除了icon/svg文件夹下其他地方的.svg文件

chainWebpack: config => {
    const svgRule = config.module.rule("svg");
    // 清除已有的所有 loader。
    // 如果你不这样做,接下来的 loader 会附加在该规则现有的 loader 之后。
    svgRule.uses.clear();
    svgRule
      .test(/\.svg$/)
      .include.add(path.resolve(__dirname, "./src/icons/svg"))
      .end()
      .use("svg-sprite-loader")
      .loader("svg-sprite-loader")
      .options({
        symbolId: "icon-[name]"
      });
    const fileRule = config.module.rule("file");
    fileRule.uses.clear();
    fileRule
      .test(/\.svg$/)
      .exclude.add(path.resolve(__dirname, "./src/icons/svg"))
      .end()
      .use("file-loader")
      .loader("file-loader");
  }

5. svg图标使用
// class="color-red" 可以添加自定义的样式,可以覆盖默认的fill

<svg-icon
    class="color-red"
    icon-class="arrow_bottom_solid"
></svg-icon>

五.axios+api封装

目录结构(示例)
request
    http.js
    api
        index.js
        user.js
http.js封装
import axios from "axios";
import router from "../router";
import store from "../store";

/**
 * 提示函数
 * 禁止点击蒙层、显示一秒后关闭
 */
const tip = msg => {
  Toast({
    message: msg,
    duration: 1000,
    forbidClick: true
  });
};


/**
 * 跳转登录页
 * 携带当前页面路由,以期在登录页面完成登录后返回当前页面
 */
const toLogin = async () => {
    router.replace({
      path: "/login",
      query: {
        redirect: router.currentRoute.fullPath
      }
    });
};


/**
 * 请求失败后的错误统一处理
 * @param {Number} status 请求失败的状态码
 */
const errorHandle = status => {
  // 状态码判断
  switch (status) {
    // 401: 未登录状态,跳转登录页
    case 401:
      toLogin();
      break;
    // 403 token过期
    // 清除token并跳转登录页
    case 403:
      tip("登录过期,请重新登录");
      localStorage.removeItem("token");
      setTimeout(() => {
        toLogin();
      }, 1000);
      break;
    // 404请求不存在
    case 404:
      tip("请求的资源不存在");
      break;
    default:
      tip(`其他未知错误,状态码:${status}`);
  }
};


// 状态200时候, code码判断
const errorCodeHandle = ({ code, message }) => {
  switch (code) {
    case "000000": //系统交易成功
      break;
    case "999999": //系统异常
      tip(message);
      break;
    case "AUTH_x1": //用户未登陆
      store.commit("storeUser/clearUserInfo");
      toLogin();
      break;
    case "AUTH_x2": //用户无权限
      store.commit("storeUser/clearUserInfo");
      tip(message);
      break;
    case "LOGIN_x3": //用户已禁用
      store.commit("storeUser/clearUserInfo");
      tip(message);
      break;
    case "LOGIN_x4": //用户session失效
      store.commit("storeUser/clearUserInfo");
      toLogin();
      break;
    default:
      tip(message);
      break;
  }
};

// 创建axios实例
var instance = axios.create({ timeout: 5000 });
// 设置post请求头
instance.defaults.headers.post["Content-Type"] =
  "application/json;charset=UTF-8;";
instance.defaults.baseURL = "api";

// 请求拦截器
instance.interceptors.request.use(
  config => {
    // 对config做一些处理
    // ...
    // 加载弹窗
    Toast.loading({
      message: "加载中...",
      forbidClick: true
    });
    return config;
  },
  error => Promise.error(error)
);

// 响应拦截器
instance.interceptors.response.use(
  // 请求成功
  res => {
    Toast.clear();
    if (!store.state.storeGlobal.network) {
      store.commit("storeGlobal/changeNetwork", true);
    }
    if (res.status === 200 && res.data.code === "000000") {
      return Promise.resolve(res.data);
    } else {
      errorCodeHandle(res.data);
      return Promise.reject(res);
    }
  },
  // 请求失败
  error => {
    const { response } = error;
    if (response) {
      // 请求已发出,但是不在2xx的范围
      errorHandle(response.status, response.data.message);
      return Promise.reject(response);
    } else {
      // 处理断网的情况
      // eg:请求超时或断网时,更新state的network状态
      // network状态在app.vue中控制着一个全局的断网提示组件的显示隐藏
      // 关于断网组件中的刷新重新获取数据,会在断网组件(refresh.vue)中说明
      if (!window.navigator.onLine) {
        store.commit("storeGlobal/changeNetwork", false);
      } else {
        return Promise.reject(error);
      }
    }
  }
);

export default instance;
api/index.js
/**
 * api接口的统一出口
 */
import user from "@/request/api/user";

// 导出接口
export default {
  user
};

api/user.js
/**
 * user模块接口列表
 */
import axios from "@/request/http"; // 导入http中创建的axios实例

const user = {
  login(params) {
    return axios.post("/login", params);
  }
};

export default user;

api注册到全局(main.js文件)
import api from '@/request/api';

Vue.prototype.$api = api;
api接口调用示例
// Login.vue

methods: {
    async onSubmit() {
      let params = {
        loginName: '小美',
        password: '123'
      };
      const res = await this.$api.login(params);
      console.log("登录信息:", res)
    }
}
App.vue(断网代码示例)

使用一个全局的store状态存储网络状态

<template>
  <div id="app">
    <div v-if="!network" class="offline">
      哎呀,网络开小差啦。<van-icon name="replay" @click.native="onRefresh" />
    </div>
    <router-view />
  </div>
</template>

<script>
import { mapState } from "vuex";
import { Icon } from "vant";
export default {
  components: {
    [Icon.name]: Icon
  },
  computed: {
    ...mapState("storeGlobal", ["network"])
  },
  methods: {
    onRefresh() {
      this.$router.replace("/refresh");
    }
  }
};
</script>

<style lang="scss">
#app {
  font-family: STHeitiSC-Medium, STHeitiSC, Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
  background-color: #f5f5f5;
  height: 100vh;
  .offline {
    text-align: center;
    padding: 10px;
    background-color: #ffeeaa;
    font-size: 14px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
}
</style>

refresh.vue
<template>
  <div></div>
</template>

<script>
/* 从app.vue来,这里简单介绍一下断网。在http.js中介绍了,我们会在断网的时候,来更新vue中network的状态,
 * 那么这里我们根据network的状态来判断是否需要加载这个断网组件。断网情况下,加载断网组件,不加载对应页面的组件。
 * 当点击刷新的时候,我们通过跳转refesh页面然后立即返回的方式来实现重新获取数据的操作。
 * 因此我们需要新建一个refresh.vue页面,并在其beforeRouteEnter钩子中再返回当前页面。
 */
export default {
  beforeRouteEnter(to, from, next) {
    next(vm => {
      vm.$router.replace(from.fullPath);
    });
  }
};
</script>

六. router全局守卫处理

// router/index.js

const routes = [
  {
    path: "/login",
    name: "Login",
    component: Login,
    meta: {
      title: "登录"
    }
  },
]

router.beforeEach((to, from, next) => {
  // 添加title, 无需每个页面设置
  if (to.meta && to.meta.title) {
    document.title = to.meta.title;
  }

  // 添加路由来源,无需每个页面添加路由守卫判断来自哪个页面
  to.params.last = from;

  next();
});

七. 跨域代理proxy -> 配置vue.config.js文件

从vue/cli3开始项目就看不到webpcak.config.js之类的配置文件了。需要添加前端代理需要自己在根目录下添加vue.config.js进行配置。
下面⬇️展示一个配置比较齐全的文件。

const path = require("path");

module.exports = {
  /* 部署生产环境和开发环境下的URL:可对当前环境进行区分,baseUrl 从 Vue CLI 3.3 起已弃用,要使用publicPath */
  /* baseUrl: process.env.NODE_ENV === 'production' ? './' : '/' */
  publicPath: process.env.NODE_ENV === "production" ? "/public/" : "./",
  /* 输出文件目录:在npm run build时,生成文件的目录名称 */
  outputDir: "dist",
  /* 放置生成的静态资源 (js、css、img、fonts) 的 (相对于 outputDir 的) 目录 */
  assetsDir: "assets",
  /* 是否在构建生产包时生成 sourceMap 文件,false将提高构建速度 */
  productionSourceMap: false,
  /* 默认情况下,生成的静态资源在它们的文件名中包含了 hash 以便更好的控制缓存,你可以通过将这个选项设为 false 来关闭文件名哈希。(false的时候就是让原来的文件名不改变) */
  filenameHashing: false,
  /* 代码保存时进行eslint检测 */
  lintOnSave: true,
  /* webpack-dev-server 相关配置 */
  devServer: {
    /* 自动打开浏览器 */
    open: true,
    /* 设置为0.0.0.0则所有的地址均能访问 */
    host: "0.0.0.0",
    port: 8088,
    https: false,
    hotOnly: false,
    /* 使用代理 */
    proxy: {
      "/sunrise-gateway": {
        /* 目标代理服务器地址 */
        target: "http://xxx.com/",
        /* 允许跨域 */
        changeOrigin: true
      }
    }
  },
  css: {
    loaderOptions: {
      postcss: {
        plugins: [
          require("autoprefixer")({
            // 配置使用 autoprefixer
            overrideBrowserslist: ["last 15 versions"]
          }),
          require("postcss-pxtorem")({
            rootValue: 37.5, // 换算的基数
            // 忽略转换正则匹配项。插件会转化所有的样式的px。比如引入了三方UI,也会被转化。目前我使用 selectorBlackList字段,来过滤
            //如果个别地方不想转化px。可以简单的使用大写的 PX 或 Px 。
            selectorBlackList: ["ig"],
            propList: ["*"]
          })
        ]
      }
    }
  },
  chainWebpack: config => {
    const svgRule = config.module.rule("svg");
    // 清除已有的所有 loader。
    // 如果你不这样做,接下来的 loader 会附加在该规则现有的 loader 之后。
    svgRule.uses.clear();
    svgRule
      .test(/\.svg$/)
      .include.add(path.resolve(__dirname, "./src/icons/svg"))
      .end()
      .use("svg-sprite-loader")
      .loader("svg-sprite-loader")
      .options({
        symbolId: "icon-[name]"
      });
    const fileRule = config.module.rule("file");
    fileRule.uses.clear();
    fileRule
      .test(/\.svg$/)
      .exclude.add(path.resolve(__dirname, "./src/icons/svg"))
      .end()
      .use("file-loader")
      .loader("file-loader");
  }
};

八. vscode中自定义配置prettier

  • vscode安装插件:Prettier - Code formatter
  • 问题:插件格式化的文件和vuecli要求的prettimer需要的不一致。所以需要自定义配置成vuecli要求的效果。
  • 解决:代码(code) -> 首选项(preference) -> 设置(settings) -> extensions -> premitter
  • 具体配置可以参考premitter配置文件官方网站
  • 中文的找到一篇基本配置+解释的参考文章Prettier格式化配置
{
    // 使能每一种语言默认格式化规则
    "[html]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "[css]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "[less]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "[javascript]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    },

    /*  prettier的配置 */
    "prettier.printWidth": 100, // 超过最大值换行
    "prettier.tabWidth": 4, // 缩进字节数
    "prettier.useTabs": false, // 缩进不使用tab,使用空格
    "prettier.semi": true, // 句尾添加分号
    "prettier.singleQuote": true, // 使用单引号代替双引号
    "prettier.proseWrap": "preserve", // 默认值。因为使用了一些折行敏感型的渲染器(如GitHub comment)而按照markdown文本样式进行折行
    "prettier.arrowParens": "avoid", //  (x) => {} 箭头函数参数只有一个时是否要有小括号。avoid:省略括号
    "prettier.bracketSpacing": true, // 在对象,数组括号与文字之间加空格 "{ foo: bar }"
    "prettier.disableLanguages": ["vue"], // 不格式化vue文件,vue文件的格式化单独设置
    "prettier.endOfLine": "auto", // 结尾是 \n \r \n\r auto
    "prettier.eslintIntegration": false, //不让prettier使用eslint的代码格式进行校验
    "prettier.htmlWhitespaceSensitivity": "ignore",
    "prettier.ignorePath": ".prettierignore", // 不使用prettier格式化的文件填写在项目的.prettierignore文件中
    "prettier.jsxBracketSameLine": false, // 在jsx中把'>' 是否单独放一行
    "prettier.jsxSingleQuote": false, // 在jsx中使用单引号代替双引号
    "prettier.parser": "babylon", // 格式化的解析器,默认是babylon
    "prettier.requireConfig": false, // Require a 'prettierconfig' to format prettier
    "prettier.stylelintIntegration": false, //不让prettier使用stylelint的代码格式进行校验
    "prettier.trailingComma": "es5", // 在对象或数组最后一个元素后面是否加逗号(在ES5中加尾逗号)
    "prettier.tslintIntegration": false // 不让prettier使用tslint的代码格式进行校验
}

九. 查看隐藏的webpack配置:

  • vue inspect 执行后,控制台会显示你的webpack所有的配置
  • vue inspect --rules 显示所有的rule配置规则
  • vue inspect --rule svg (我们在上面配置了svg)

十. vantlist组件在ios手机中滑动无效

发现设置了 高度为100vh的#app标签,没有设置overflow,其他浏览器都默认可以滑动。ios上的wkwebview中不能滑动,设置上overflow: auto就好啦

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
对于Vue 3和Vant移动TabBar的使用,你可以按照以下步骤进行操作: 1. 首先,确保你已经安装了Vue 3和Vant。你可以使用以下命令来安装它们: ```bash npm install vue@next vant ``` 2. 在你的Vue项目中,创建一个新的组件文件,例如`TabBar.vue`。 3. 在`TabBar.vue`文件中,引入Vue和Vant的相关组件和样: ```javascript <template> <div> <van-tabbar v-model="active"> <van-tabbar-item icon="home-o" to="/"> 首页 </van-tabbar-item> <van-tabbar-item icon="search" to="/search"> 搜索 </van-tabbar-item> <van-tabbar-item icon="star-o" to="/favorites"> 收藏 </van-tabbar-item> <van-tabbar-item icon="contact" to="/profile"> 个人中心 </van-tabbar-item> </van-tabbar> </div> </template> <script> import { Tabbar, TabbarItem } from 'vant'; export default { components: { [Tabbar.name]: Tabbar, [TabbarItem.name]: TabbarItem, }, data() { return { active: '/', }; }, }; </script> <style> /* 这里可以添加自定义样 */ </style> ``` 4. 在你的主应用组件中,例如`App.vue`,使用`TabBar`组件: ```html <template> <div id="app"> <!-- 其他内容 --> <TabBar /> </div> </template> <script> import TabBar from './components/TabBar.vue'; export default { components: { TabBar, }, }; </script> <style> /* 这里可以添加全局样 */ </style> ``` 5. 最后,你可以根据自己的需要在`TabBar.vue`中设置每个Tab项的图标、文字和链接。你还可以通过修改`active`的值来控制当前选中的Tab。 这样,你就可以在Vue 3项目中使用Vant移动的TabBar了。记得根据自己的需求进行样和功能的调整。希望对你有帮助!如果有任何问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值