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 " > [email protected]" 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)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章