Vue.js 填坑笔记

记录一些使用 vue-cli 脚手架创建项目时,遇到的一些问题以及解决方案

vue 1.x 升级到 vue 2.x

首先需要安装 vue-migration-helper CLI 工具:

控制台运行命令: npm install --global vue-migration-helper CLI 工具来帮助项目从 Vue 1.x 迁移到 2.x。 它扫描文件以查找特定于 Vue 的代码,并对需要升级的代码提供详细的警告。 vue-migration-helper 的介绍说明告诉我们它大概能捕获 80% 的升级帮助信息,而不是全部。所以终端输出的帮助信息并不是完全正确的,在升级时不要盲目 copy & paste ,还是要根据实际情况去改写。

进入当前的项目:运行: vue-migration-helper

具体升级指南,请参照官方文档-从 Vue 1.x 迁移

给与 Windows 用户的一条强烈建议

Windows 在执行 npm install 期间遇到关于 node-gyp 的错误,那么你很有可能没有在你的系统上安装正确的构建工具。构建工具包括 PythonVisual Studio 等等。

使用 windows-build-tools 来为我们完成大部分烦人的工作。全局安装此工具将依次设置 Visual C++ 软件包、Python 等等

npm install --global --production windows-build-tools

执行上条命令时注意以管理员权限开启一个 PowerShell 来执行该条命令,因为自动化工具会自动的添加一些 PATH 变量

如果是 Windows 7 系统,还需要安装 .NET Framework 4.5.1

还有一些更加恶心的报错信息通常是因为墙的原因,这里建议安装 nrm来随时切换软件源

npm -g nrm
nrm use taobao

以下这几种软件源可供选择,默认使用 npm 官方软件源

* npm ---- https://registry.npmjs.org/
  cnpm --- http://r.cnpmjs.org/
  taobao - https://registry.npm.taobao.org/
  nj ----- https://registry.nodejitsu.com/
  rednpm - http://registry.mirror.cqupt.edu.cn/
  npmMirror  https://skimdb.npmjs.com/registry/
  edunpm - http://registry.enpmjs.org/

设置代理与跨域

开发时设置

如果你的后端 API 服务是 Express 提供或者是 Thinkjs 再或者是 koa2 等等,当你请求数据时就会面临着跨域请求问题

执行 npm run dev,你会发现会报一个错误:vue-resource.common.js?e289:1071 POST http://localhost:8080/api/use... 404 (Not Found)。这是由于直接访问 8080 端口,是访问不到的,所以这里需要设置一下代理转发映射.

项目根目录下的 config 文件夹中有一个 proxyTable 参数,用来设置地址映射表,可以添加到开发时配置(dev)中

|-- config
  |-- dev.env.js
  |-- index.js
  |-- prod.env.js

config/index.js

dev: {
  // ...
  proxyTable: {
    '/api': {
      target: 'http://127.0.0.1:3000/api/',
      changeOrigin: true,
      pathRewrite: {
          '^/api': ''
      }
    }
  },
  // ...
}

添加以上代码之后,请求 /api 时就代表 http://127.0.0.1:3000/api/(这里要写 ip,不要写 localhost), changeOrigin 参数接收一个布尔值,如果为 true,这样就不会有跨域问题了。

更多接口参数配置,请参考 https://github.com/chimurai/http-proxy-middleware#options

webpack 接口配置文档 https://webpack.js.org/configuration/dev-server/#devserver-proxy

正式上线时设置

|-- src
  |-- axios
    |-- index.js
  |-- config.js

正式上线时,不推荐使用上一个方案,这里推荐使用 axios 进行转发

src/config.js

export default {
  serverUrl: "http://127.0.0.1:3000/"
};

src/axios/index.js

import axios from "axios";
import config from "@/config";

// 设置全局 axio s默认值
axios.defaults.baseURL = config.serverUrl;
axios.defaults.timeout = 5000;// 5000的超时验证
axios.defaults.headers.post["Content-Type"] = "application/jsoncharset=UTF-8";

// 创建一个 axios 实例
const instance = axios.create();
instance.defaults.headers.post["Content-Type"] =
  "application/jsoncharset=UTF-8";

axios.interceptors.request.use = instance.interceptors.request.use;

export async function postDate(username, password) {
  try {
    const response = await fetch.post("/postDate", {
      username,
      password
    });
    return response.data;
  } catch (err) {
    console.log("message", err);
    if (err.response) {
      throw Error(err.response.data.message);
    }
    throw err;
  }
}

父子组件

|-- src
  |-- components
    |-- HerderBar.vue
    |-- FooterBar.vue
  |-- pages
    |-- Home.vue

假如你的 components 目录下有 HerderBar.vue 和 FooterBar.vue 这两个子组件,而 Home.vue 要引用这两组件,那么下面这种写法可以完成该需求

src/pages/Home.vue

<template>
<div>
  <header-bar></header-bar>
  <!-- ... ... -->
  <footer-bar></footer-bar>
</div>
</template>

<script>
import HeaderBar from '@/components/HeaderBar'
import FooterBar from '@/components/FooterBar'

export default {
  name: 'Home',
  components: {
    HeaderBar,
    FooterBar
  }
}
</script>

图标库

市面上用的比较广泛的图标库有两个,一是阿里巴巴矢量图标库,其有上百万图标共程序员选择,自定义比较强;二是Font Awesome,该图标库虽没有上百万图标,但也受到大部门程序员喜爱。

很多人在写 Vue 项目时,前端 UI 框架都喜欢使用 Element UI,但是该 UI 框架默认提供的图标库实现是少之又少,但是该 UI 框架允许我们引入第三方图标库

iconfont

这个引入就非常简单了,在 iconfont 网站上有提供离线版和在线版,看自己的意愿,然后在 index.html 里使用 style 标签引入即可。

fontawesome

参考代码element-font-awesome

使用 less 时,别忘了安装 npm 依赖

npm install -S less less-loader

目录结构

|-- src
  |-- font.less
  |-- main.js

src/main.js

import "./font.less";

src/font.less

[class^="el-icon-fa"], [class*=" el-icon-fa"] {
  display: inline-block;
  font: normal normal normal 14px/1 FontAwesome!important;
  font-size: inherit;
  text-rendering: auto;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
};

@import url("../node_modules/font-awesome/less/font-awesome");
@fa-css-prefix: el-icon-fa;

sass

对于 css 的预编译器,个人比较喜欢 sass 的,在使用 sass 时仍然需要添加 npm 依赖

npm install --save node-sass sass-lodaer

目录结构

|-- src
  |-- assets
    |-- scss
      |-- _public.scss
      |-- index.scss
  |-- App.vue

src/App.vue

<style lang="scss">
@import './assets/scss/index';
</style>

<template>
<div id="app">
</div>
</template>

<script>
export default {
  name: 'app'
}
</script>

src/assets/scss/index.scss

@import "public";

src/assets/scss/public.scss

#app {
  font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
    "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

vuex

目录结构

|-- src
  |-- store
    |-- modules
      |-- ... ...
    |-- actions.js      # 根级别的 action =>
    |-- getters.js      # 根级别的 mutation =>
    |-- index.js        # 我们组装模块并导出 store 的地方
    |-- types.js        # 根级别的 type => 状态
  |-- main.js

src/main.js

import Vue from "vue";
import App from "./App";

import store from "./store";

Vue.config.productionTip = false;

/* eslint-disable no-new */
new Vue({
  el: "#app",
  store,
  template: "<App/>",
  components: {
    App
  }
});

vue-router

目录结构

|-- router
  |-- axios
    |-- index.js
  |-- main.js

src/main.js

import Vue from "vue";
import App from "./App";
import router from "./router";

Vue.config.productionTip = false;

/* eslint-disable no-new */
new Vue({
  el: "#app",
  router,
  template: "<App/>",
  components: {
    App
  }
});

HTML5 History 模式

下面这一代码片段是使用 vue-cli 下载的模板写法,但是这种写法会使你的 URL 变成 http://localhost:8080/#/

import Vue from "vue";
import Router from "vue-router";
// ... ...

Vue.use(Router);

export default new Router({
  routes: [
    {
      path: "/"
      // ... ...
    }
  ]
});

对于强迫症的人来说,这样的 URL 非常丑,这就需要开启 HTML5 History 模式,更具体的说明请看官方文档 vue-router HTML5 History 模式

router/index.js

import Vue from "vue";
import Router from "vue-router";
// ... ...

Vue.use(Router);

const router = new VueRouter({
  mode: "history",
  routes: [
    {
      path: "/"
      // ... ...
    }
  ]
});

export default router;

路由拦截

对于进入某些页面需要进行登录验证,那么就需要设置路由拦截,vue-router 官方文档称之为导航钩子,具体请看官方文档 vue-router 导航钩子

实际上在进行路由拦截时需要进行数据验证,当验证通过时方能允许其通过该路由,该验证数据通常会存储在 vuexstate 中,或者会存储在 Local Storage,再或者 Session Storage,无论存储在哪里,vue-router 配置文件能够正确访问到即可,当然验证程序就需要后端服务 API 提供了

router/index.js

import Vue from "vue";
import Router from "vue-router";
import store from "@/store";
// ... ...

Vue.use(Router);

const router = new VueRouter({
  mode: "history",
  routes: [
    {
      path: "/",
      // ... ...
      meta: {
        requireAuth: true // 添加该字段,表示进入这个路由是需要进行验证的
      }
    }
  ]
});

router.beforeEach((to, from, next) => {
  if (to.matched.some(r => r.meta.requireAuth)) {
    // 判断该路由是否需要登录权限
    if (store.state.token) {
      // 通过 vuex state 获取当前的 token 是否存在
      next();
    } else {
      next({
        path: "/login", // 验证失败,将会跳转到该路由
        query: {
          redirect: to.fullPath
        } // 将跳转的路由 path 作为参数,登录成功后跳转到该路由
      });
    }
  } else {
    next();
  }
});

export default router;

axios

自从 Vue.js 更新至 2.x 版本之后,官方就不再使用 vue-resource,替而代之的是 axios

目录结构

|-- src
  |-- axios
    |-- index.js
  |-- pages
    |-- Home.vue
  |-- main.js

src/main.js

import Vue from "vue";
import App from "./App";

import api from "./axios";
Vue.prototype.$api = api;

Vue.config.productionTip = false;

/* eslint-disable no-new */
new Vue({
  el: "#app",
  template: "<App/>",
  components: {
    App
  }
});

axios 拦截

使用 vue-router 进行路由拦截是不够的,当然也是需要数据验证的,更加详细的说明以及例子请移步 【vue+axios】一个项目学会前端实现登录拦截

axios/index.js

import axios from "axios";
import store from "../store";
import router from "../router";

// 设置全局axios默认值
axios.defaults.timeout = 5000;// 5000的超时验证
axios.defaults.headers.post["Content-Type"] = "application/jsoncharset=UTF-8";

// 创建一个axios实例
const instance = axios.create();
instance.defaults.headers.post["Content-Type"] =
  "application/jsoncharset=UTF-8";

axios.interceptors.request.use = instance.interceptors.request.use;

// http request 拦截器
instance.interceptors.request.use(
  config => {
    if (store.state.token) {
      config.headers.Authorization = `token ${store.state.token}`.replace(
        /(^")|("$)/g,
        ""
      );
    }
    return config;
  },
  error => {
    return Promise.reject(error);
  }
);

// http response 拦截器
instance.interceptors.response.use(
  response => {
    return response;
  },
  error => {
    if (error.response) {
      switch (error.response.status) {
        case 401:
          store.dispatch("UserLogout");
          router.replace({
            path: "login",
            query: {
              redirect: router.currentRoute.fullPath
            }
          });
      }
    }
    return Promise.reject(error.response);
  }
);

export default {
  // POST
  PostData(data) {
    return instance.post("/api/postData", data);
  },
  // GET
  GetData() {
    return instance.get("/api/user/getData");
  }
};

在 Vue 组件内调用

src/pages/Home.vue

<template>
<div>
  <!-- ... ... -->
</div>
</template>

<script>

export default {
  name: 'Home',
  data: {
    return {
      fromData: [
        // ... ...
      ]
    }
  },
  methods: {
    post () {
      const opt = this.fromData
      this.$api.PostData(opt)
        .then(({data}) => {
          // ... ...
        })
        .catch((err) => {
          console.log(err)
        })
    }
  }
  created () {
    this.$api.GetData()
      .then(({data}) => {
        // ... ...
      })
      .catch((err) => {
        console.log(err)
      })
  }
}
</script>

规范性

这里只举例 .vue 文件 script 模块中,各个函数的书写顺序,更多的规范性请查询文档 风格指南

export default {
  name: "", // 组件名字
  props: [
    // 继承父组件数据
    ""
  ],

  data() {
    return {
      // 该组件数据
    };
  },
  watch: {
    // 于观察 Vue 实例上的数据变动。对应一个对象,键是观察表达式,值是对应回调。值也可以是方法名,或者是对象,包含选项
  },
  computed: {
    // 相当于属性的一个实时计算,如果实时计算里关联了对象,那么当对象的某个值改变的时候,同事会出发实时计算
  },
  methods: {
    // 该组件的方法
  },
  filter: {
    // 过滤器
  },
  beforeRouteUpdate(to, from, next) {
    // 在当前路由改变,但是该组件被复用时调用
  },
  created() {
    // 在 vue 1.0 中起到初始化数据作用,在 vue 2.0 之后推荐改用 computed
  },
  mounted() {
    // 在这发起后端请求,拿回数据,配合路由钩子做一些事情
  },
  components: {
    // 组件
  }
};

在 vue 中引入 markdown

安装相关依赖

npm i -D text-loader
npm i -S marked github-markdown-css

配置 webpack

build/webpack.base.conf.js

module.exports = {
  module: {
    rules: [
      {
        test: /.md$/,
        loader: 'text-loader'
      }
    }
  }
}

vue.config.js

module.exports = {
  chainWebpack: config => {
    config.module
      .rule('test')
      .test(/\.md$/)
      .use('test-loader')
      .loader('test-loader')
  }
}

编写 vue 文件

<template lang="html">
  <div>

    <div class="article markdown-body" v-html="compiledMarkdown">
    </div>

  </div>
</template>

<script>
import marked from 'marked'
import versionLog from './versionLog.md'
import 'github-markdown-css/github-markdown.css'

export default {
  name: 'VersionLog',
  data () {
    return {
      context: versionLog
    }
  },
  computed: {
    compiledMarkdown () {
      return marked(this.context, { sanitize: true })
    }
  }
}
</script>

基于 Markdown 的幻灯片

导入 text-loader 上一节已经说明过了,这里就不重复了

github vue-mark-display

npm i vue-mark-display

usage

demo.md

# Hello World
----
This is Vue Mark Display

demo.vue

<template>
  <mark-display
    :markdown="markdown"
    @title="setTitle"
    keyboard-ctrl
    url-hash-ctrl
    auto-font-size
    auto-blank-target
  ></mark-display>
</template>

<script>
import MarkDisplay from "vue-mark-display";
import markdown from "./demo.md";

export default {
  components: { MarkDisplay },
  data() {
    return { markdown };
  },
  methods: {
    setTitle({ title }) {
      document.title = title;
    }
  }
};
</script>
亲!!! 听说给作者打赏一杯咖啡钱,会给自己带来好运哦!