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 14 px / 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>