Vue教程--使用官方脚手架构建实例(上篇)

本文基于工作项目开发,做的整理笔记
因工作需要,项目框架由最初的Java/jsp模式,逐渐转移成node/express模式。现在新项目又因为业务流程中的交互操作,会使页面dom根据数据data不断显示变化,便觉得此时是否是最佳实际,应该尝试一下vue框架。因为之前也偷偷的进行了一些vue框架的踩坑工作,已经填了一些坑或者有一些解决方案了,这也才敢用上。依然由后台Java提供API。本文就是做个笔记,说明如何使用Vue官方脚手架开始构建项目。

前提条件:
你已经听过、了解过Vue,至少一点点。

编码环境:
系统:OS X EI Capitan
版本:10.12.2

共2篇:
上篇:http://www.jianshu.com/p/ee2464501c65(当前)
下篇:http://www.jianshu.com/p/9baa03eb31fc

vue.jpg

目录
| - 0.传送门
| - 1.安装
| - 2.项目初始化
| - 3.项目构建
  | - Step1.阅读结构
  | - Step2. 阅读代码(泛读)
| - 4.项目实操
  | - Step1. 修改eslint配置
  | - Step2. 调整结构
  | - Step3. 添加登陆页面
  | - Step4. 模版嵌套
  | - (上下篇分割线)
  | - Step5. 引入mockjs独立开发
  | - Step6. 调用API接口
  | - Step7. 状态管理(store仓库)
  | - Step8. 抽离公共方法
  | - Step9. 剥离组件
  | - Finish. 生成打包
| - 5.项目部署
  | - a)本地部署
  | - b)服务器部署
| - 6.结束

0.传送门

如果你是直接<script>引用vue.js,去写一些实例或进行页面开发的话,那么你可能去看一官网的一些知识点,以下内容可能不适合你。
官网‘起步’传送门:http://cn.vuejs.org/v2/guide/#起步

1.安装

你已经安装了npm,它是随node.js安装的,装了node.js也就有了它。
node.js安装下载地址:http://nodejs.cn/download/

# 检查node.js是否安装,若有则显示版本号
$ node -v
# 检查npm的版本号
$ npm -v
# 若要更新npm,使用
$ npm install npm@latest -g

一般,我们还会安装淘宝镜像cnpm,因为在墙内,有时候使用npm安装会很慢,所以需要。

# 安装cnpm,并指定镜像地址
$ npm install -g cnpm --registry=https://registry.npm.taobao.org

个人一点点喜好(不介意的话无所谓):
如果能用npm,我就不太喜欢用cnpm。原因你自己通过npm install / cnpm install安装后,去看看node_modules文件夹内的文件就可以了。

写文章时,我的目前版本如下:

Yuxinde-MacBook-Pro:~ yuxin$ node -v
v6.2.0
Yuxinde-MacBook-Pro:~ yuxin$ npm -v
3.8.9
Yuxinde-MacBook-Pro:~ yuxin$ cnpm -v
cnpm@4.5.0 (/usr/local/lib/node_modules/cnpm/parse_argv.js)
npm@3.10.10 (/usr/local/lib/node_modules/cnpm/node_modules/npm/lib/npm.js)
node@6.2.0 (/usr/local/bin/node)
npminstall@2.29.1 (/usr/local/lib/node_modules/cnpm/node_modules/npminstall/lib/index.js)
prefix=/usr/local 
darwin x64 16.3.0 
registry=https://registry.npm.taobao.org

vue的安装:

# 最新稳定版
$ npm install vue
# 同时安装它的命令行工具
$ npm install --global vue-cli

安装好之后我们接下来就使用它的命令行工具vue-cli构建项目。类似的还有在react.js方面,快速上手会使用到‘蚂蚁金服’的Ant-design,使用antd-init或者dva-cli,都是命令行构建项目。

2.项目初始化

使用命令行工具,开始一点点构建项目:

# 去到放项目的文件夹地址
Yuxinde-MacBook-Pro:~ yuxin$ cd /Users/yuxin/Documents/Season/Project/Vue 
# 创建基于webpack模版的项目,名称为vue-demo
Yuxinde-MacBook-Pro:Vue yuxin$ vue init webpack vue-demo

  # 提示我有新版本
  A newer version of vue-cli is available.

  latest:    2.8.2
  installed: 2.8.1

  This will install Vue 2.x version of the template.

  # 提示我如果需要使用webpack#1.0的话,创建时用这个命令
  For Vue 1.x use: vue init webpack#1.0 vue-demo

# 填写了一些基础信息,根据你的需要选择
# 这里我用了ESLint,标准选择了默认的Standard,不熟悉ESLint的话建议不启用或标准选择none,我是给自己挖坑的,玩玩,我不熟。
# 这里我不启动unit测试和e2e测试

? Project name vue-demo
? Project description This is a demo by season
? Author Season <yuxin0721@gmail.com>
? Vue build standalone
? Install vue-router? Yes
? Use ESLint to lint your code? Yes
? Pick an ESLint preset Standard
? Setup unit tests with Karma + Mocha? No
? Setup e2e tests with Nightwatch? No

   vue-cli · Generated "vue-demo".

   To get started:
   
     cd vue-demo
     npm install
     npm run dev
   
   Documentation can be found at https://vuejs-templates.github.io/webpack

先尝试跑起来,看看项目初始化最初的效果。

Yuxinde-MacBook-Pro:Vue yuxin$ cd vue-demo
Yuxinde-MacBook-Pro:vue-demo yuxin$ npm install
fetchMetadata → network   ▀ ╢███████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░╟
^C
# 安装很慢,所以我control+r停止了安装,尝试使用cnpm install去安装
Yuxinde-MacBook-Pro:vue-demo yuxin$ cnpm install
# 此处省略安装信息,安装瞬间完成
Yuxinde-MacBook-Pro:vue-demo yuxin$ npm run dev

> vue-demo@1.0.0 dev /Users/yuxin/Documents/Season/Project/Vue/vue-demo
> node build/dev-server.js

> Starting dev server...


 DONE  Compiled successfully in 3800ms                                  09:06:56

> Listening at http://localhost:8080

浏览器查看,效果如下:

项目初始化效果.png

3.项目构建

接下来,我们就一步步来完善这个项目。但是我想先讨论一个问题,题外话。

插入一小段题外话:你是如何学习、快速上手,并逐步使用一个框架的?

我个人的一个小小想法是这个样子的:
无论是听别人的经验或是自己实践后,都会得到这样一个命题“如果一个东西自己都不会使用,就开始学的话,学会也蛮难的”。其实,也就是说如果不去先用、先去体验,拥有自己的感受、自己的想法、以至于有自己的思考,这样才会得到一个认识,而不是盲目的跟着教程一步一步,一定要思考。

比如,我想学vue.js,我一定是有一个目的,我想用它做什么,其次才是我怎样去学、去用。我的快速上手,是这样的。

  • 搞清楚我要做什么,我的目的,我的需求;
  • 找到官网,了解知识,这里就是了解vue.js;
  • 找demo,比如说,去玩它的功能,和我的需求去匹配,不满足就找下一个demo,直到对比多个之后“嗯,这个差不多,就先玩下这个”;
  • 几个demo下来,其实你会觉得可能其中某个demo也有你欣赏的功能、欣赏的地方,那留着日后你可能参考、整合、或重写;
  • 确定了demo框架,就开始看它的结构,其实也会大概的看其他demo的结构,有好的地方会搬过来优化这个框架结构,经常是相互借鉴的;
  • 阅读demo的代码,根据我的需求,去编写实现我的功能的代码。遇到问题就爬坑学习,解决问题的经验不断积累,或者又看了其他人的demo,从而不断优化整个架构。
  • 最后就是,玩的差不多的时候。可以从项目初始化创建开始,自己动手搭建一个,加深学习啦。

Step1. 阅读结构

初始化的项目结构如下所示:(其中加粗为文件夹,斜体为文件)

vue-demo
| - build (运行文件夹,dev/prod运行时需要用到的相关文件,含webpack配置)
| - config (配置文件夹,比如说dev/prod两种对应的不同接口等设置)
| - dist (打包生成的部署文件夹,一开始没有的,跑命令npm run build之后可见!!!)
| - node_modules
| - src (项目代码)
  | - assets (资源文件夹,为了区分static,这里存自己的资源)
  | - components (页面文件夹,题外:其实我觉得这个像组件,页面应该是views,先保留这样)
  | - router (路由文件夹)
  | - App.vue (vue主文件)
  | - main.js (入口文件,webpack配置的entrance)
| - static (资源文件夹,为了区分assets,这里存第三方的资源)
| - .babelrc (转码规则配置)
| - .editorconfig (代码规范配置)
| - .eslintignore (eslint检查忽略配置)
| - .eslintrc.js (eslint配置文件)
| - .gitignore (git忽略配置)
| - .postcssrc.js (postcss的load的config,文件里有对应的github地址)
| - index.html (html主页面)
| - package.json
| - README.md

Step2. 阅读代码(泛读)

各部分结构已经基本掌握了,部分内容去谷歌了解一下,直接看src文件夹的代码。

1)index.html

先看一下index.html里的代码是什么吧。这里唯一需要留意的是下面这两行,告诉我们有一个#app的DOM元素,还有就是会自动插入生成后的JS脚本。

    <div id="app"></div>
    <!-- built files will be auto injected -->
2)main.js

main.js这个文件里,就做了一件事,声明了一个Vue根实例,主要是说明用APP组件去渲染替换#app的这个DOM元素。

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'    // 引入App.vue文件
import router from './router'    // 引入router文件夹里的index.js(文件夹就是默认找index.js文件,没有报错)

Vue.config.productionTip = false    // 设置为 false 以阻止 vue 在启动时生成生产提示,可以查API文档

/* eslint-disable no-new */
new Vue({
  el: '#app',    // id为app的DOM元素,将被挂载(替换)的元素
  router,    // 引入路由,可以查看router文档
  template: '<App/>',    // 将组件包装成这样一个模版,并且将它挂载(替换)到#app这个DOM上
  components: { App }    // 引入组件,这个组件相当于一个大容器,用于承载我们其他的页面
})

让我们回头看一下DEMO页面,上面明确的提示我们,可以参考的一些有用的网站:
vue的API文档:http://cn.vuejs.org/v2/api/#全局配置
vue-router路由:http://router.vuejs.org/zh-cn/
vue-loader加载器:http://vue-loader.vuejs.org/zh-cn/
vuex状态管理:http://vuex.vuejs.org/

3)App.vue

查看App.vue这个文件,里面主要由3部分组成【template】、【script】、【style】,分别对应的就是页面里的html、js、css。这里唯一留意的就是<router-view></router-view>了,因为在demo页面中,我们看到了一张图片之外,还有下面的文字链接内容,这就要去了解路由了。

<template>
  <div id="app">
    ![](./assets/logo.png)
    <router-view></router-view>     <!-- 路由的页面将会被嵌入到这里 -->
  </div>
</template>

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

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
4)router/index.js

看一下路由index.js文件,它引入了vue-router路由管理,还引入了一个Hello组件。
我们需要做的就是将组件(components)映射到路由(routes),然后告诉 vue-router 在哪里渲染它们。routes数组里就是放着N对映射关系。

import Vue from 'vue'
import Router from 'vue-router'
import Hello from '@/components/Hello'   // 引入Hello组件

Vue.use(Router)

export default new Router({
  routes: [
    /******  一对映射 start  *******/
    {
      path: '/',
      name: 'Hello',
      component: Hello
    }
    /******  一对映射 end  *******/
    // 下一对,如果有的话,别忘了逗号
    // ...
  ]
})
5)components/Hello.vue

最后,看一下Hello.vue文件,【template】代码和平时我们写的html基本是一样的,除了用到一些变量、语法,比如这样:

  • {{ msg }} // msg是变量
  • :class="myclass" // myclass是变量
  • :ref="'mass.'+index+'.detail' // index是变量
  • v-if="flag" // flag是变量
  • v-show="page=='Hello'" // page是变量
  • v-for="(item, index) in list" // list是变量
  • v-html="myHtml" // myHtml是变量
  • ...
<!-- Hello.vue的template部分 -->
<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <h2>Essential Links</h2>
    <ul>
      <li><a href="https://vuejs.org" target="_blank">Core Docs</a></li>
      <li><a href="https://forum.vuejs.org" target="_blank">Forum</a></li>
      <li><a href="https://gitter.im/vuejs/vue" target="_blank">Gitter Chat</a></li>
      <li><a href="https://twitter.com/vuejs" target="_blank">Twitter</a></li>
      <br>
      <li><a href="http://vuejs-templates.github.io/webpack/" target="_blank">Docs for This Template</a></li>
    </ul>
    <h2>Ecosystem</h2>
    <ul>
      <li><a href="http://router.vuejs.org/" target="_blank">vue-router</a></li>
      <li><a href="http://vuex.vuejs.org/" target="_blank">vuex</a></li>
      <li><a href="http://vue-loader.vuejs.org/" target="_blank">vue-loader</a></li>
      <li><a href="https://github.com/vuejs/awesome-vue" target="_blank">awesome-vue</a></li>
    </ul>
  </div>
</template>

那接着看一下【script】,这个项目初始化的例子代码比较简单,只有一个data(name忽视,没什么特别的)。这里其实需要了解很多方法,他们都是用来干什么的,怎么用,什么时候用。我们稍后来看一下。

<!-- Hello.vue的script部分 -->
<script>
export default {
  name: 'hello',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App'
    }
  }
}
</script>

那接着看一下【style】,和平时写的没什么区别。这里重点留意下这个关键字scoped,代码这里有一个注释,说“添加这一关键字表示这些样式被限制在这个组件内,不会污染其他组件”。

<!-- Hello.vue的style部分 -->
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1, h2 {
  font-weight: normal;
}

ul {
  list-style-type: none;
  padding: 0;
}

li {
  display: inline-block;
  margin: 0 10px;
}

a {
  color: #42b983;
}
</style>

大多时候,基本都会添加scoped关键字,不过比如我们在使用elementUI(或者其他UI)的时候,我们需要覆盖它的样式,那可能需要添加多一个<style></style>,注意这个不带scoped关键字。一般把覆盖的样式独立成一个单独的css文件,通过每个组件的一个个性化class去区分设置多个同样ui的不同样式。

<!-- element-UI.css UI样式覆盖文件 -->
<style>
...
</style>
6)常用方法

先看一下官网的这张“生命周期图示”,正如官网所说,一时看不懂,但是用一用就看懂了,记得回来再看看。

lifecycle.png

如果你看得懂这张图,你就会发现App.vue刚好是黄色棱形判断的一种,然后其他components的xxx.vue(比如这里只有一个Hello.vue)就是判断的另外一种。先不多说这张图,在玩的过程中实现你的demo就清楚了。

我们常用的方法有如下一些:

  • props,页面不会有,但是组件基本都会有,主要用于传数据
// 举例
props:{
      id: {
        type: String,
        default: ''
      },
      status: {
        type: Number,
        default: 0
      }
},

  • watch,监视页面某一个值的变化,由它变化可以导致其他变化
// 举例
watch:{
      status(val) {
        if(val == 1) {
              this.value = 'aaaa';
        } else {
              this.value = 'bbbb';
        }
      }
},
  • components,引入组件的时候,我们要在这里声明
// 举例
components: {
      AddDialog,    // 组件1
      ContainerFooter    // 组件2
},
  • beforeRouteEnter,在页面进入前要执行的一些操作放在这里,比如请求数据
// 举例
beforeRouteEnter (to, from, next) {
      getActData(params).then(response => {
           // 拿回结果后才next()
           ...
           next(vm => {
                // 赋值
                ....
           });
      }).catch(err => {
           ....
      });
},
  • created,在页面创建后执行的操作,有些请求也可以放这里
// 举例
created() {
      // 加载列表数据
      this.status = this.$route.query.status == undefined ? 1 : this.$route.query.status;
      this.fetchData();
},
  • computed,比如我根据某个变量做的不同显示、判断,都可以变成一个方法放这里,以便调用
// 举例
computed: {
      confirmText() {
          return this.edit?'立即修改':'立即创建';
      },
 },
  • filters,过滤器,用来将一些值格式化输出显示,比如枚举
// 举例
filters: {
        toDateShort(val) {
          return val.substring(0,11);
        }
},
  • methods,整个页面调用的方法,click事件、change事件都在这里
// 举例
methods: {
        showLoading() {
          this.loading = true
        },
        hideLoading() {
          this.loading = false
        },
},

浏览vue.js的api文档https://cn.vuejs.org/v2/api/,还是有好多东西在不断的开发过程中会被用到。比如slot也是一个很值得研究的东西。

4.项目实操

接下来,我们就一起实现一个小demo,但它的结构应该是比较完善的,至少可以较好的满足大部分场景和功能需求。

Step1. 修改eslint配置

试了一下,standard标准检查出现的错误提示实在太多了,换一下自定义标准,这个没那么严格,不会那么尴尬。

# 新的配置,用来替换原来的标准
module.exports = {
  root: true,
  parser: 'babel-eslint',
  "evn": {
    "node": true,
    "commonjs": true,
    "shared-node-browser": true,
    "browser": true
  },
  parserOptions: {
    sourceType: 'module'
  },
  env: {
    browser: true,
    "es6": true,
    "node": true
  },
  // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
  extends: 'eslint:recommended',
  // required to lint *.vue files
  plugins: [
    'html'
  ],
  // add your custom rules here
  'rules': {
    // allow paren-less arrow functions
    'arrow-parens': 0,
    // allow async-await
    'generator-star-spacing': 0,
    // allow debugger during development
    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
    // allow console
    'no-console': ["error", { allow: ["warn", "error"] }]
  }
}

Step2. 调整结构

先调整一下结构吧,因为后面会陆续用到。考虑到后面一个个添加,会没有一个整体直观的印象,感觉有点乱吧,我个人不太喜欢,就先整体了解一下。

注意:有注释的说明是刚刚新添加的。

vue-demo
| - build
| - config
| - dist
| - node_modules
| - src
  | - api (api接口文件夹)
  | - assets
  | - components (组件文件夹,原来保留)
     | - Hello.vue (转移到views目录下------>)

| - mock (mock.js假数据接口文件夹)
  | - router
  | - store (仓库文件夹)
  | - styles (样式文件夹)
  | - utils (公共方法文件夹)
  | - vendor (第三方库文件夹)
  | - views (页面文件夹)
     | - dashboard (看板页面)
     | - tablelist (列表页面)
     | - error (错误页面)
     | - layout (模版)
     | - login (登陆页面)
     | - hello (原demo的Hello页面)
        | - Hello.vue (------>转移后的位置)
  | - App.vue
  | - main.js
| - static
| - .babelrc
| - .editorconfig
| - .eslintignore
| - .eslintrc.js
| - .gitignore
| - .postcssrc.js
| - index.html
| - package.json
| - README.md

让我们使用npm run dev运行一下demo,会发现报错“在./src/router/index.js里找不到引用的Hello.vue文件”,如下:

Yuxinde-MacBook-Pro:vue-demo yuxin$ npm run dev

> vue-demo@1.0.0 dev /Users/yuxin/Documents/Season/Project/Vue/vue-demo
> node build/dev-server.js

> Starting dev server...


 ERROR  Failed to compile with 1 errors                                             09:49:17

This dependency was not found:

* @/components/Hello in ./src/router/index.js

To install it, you can run: npm install --save @/components/Hello
> Listening at http://localhost:8080

这里你需要改一下引用路径,因为刚刚我们调整过结构。修改之后,发现页面正常了,OK。后面我们不会用到Hello.vue这一块,但暂时保留吧。

# ./src/router/index.js
# 将这句
import Hello from '@/components/Hello'
# 改为
import Hello from '@/views/hello/Hello'

Step3. 添加登陆页面

从这里开始,后面的每一步可能会涉及到使用npm install xxx去安装一些模块,来服务项目的创建、运行。

安装element-ui,官方网站:http://element.eleme.io/#/zh-CN/component/installation

Yuxinde-MacBook-Pro:vue-demo yuxin$ npm install element-ui --save
npm WARN skippingAction Module is inside a symlinked module: not running update node-pre-gyp@0.6.36 node_modules/fsevents/node_modules/node-pre-gyp
npm WARN skippingAction Module is inside a symlinked module: not running move co@4.6.0 node_modules/ajv-keywords/node_modules/ajv/node_modules/co
npm WARN skippingAction Module is inside a symlinked module: not running move json-stable-stringify@1.0.1 node_modules/ajv-keywords/node_modules/ajv/node_modules/json-stable-stringify
npm WARN skippingAction Module is inside a symlinked module: not running add ajv@4.11.8 node_modules/ajv-keywords/node_modules/ajv
vue-demo@1.0.0 /Users/yuxin/Documents/Season/Project/Vue/vue-demo
└─┬ element-ui@1.3.7 
  ├── async-validator@1.6.9 
  ├── babel-helper-vue-jsx-merge-props@2.0.2 
  ├── deepmerge@1.4.4 
  └── throttle-debounce@1.0.1 

# 然后把项目跑起来
Yuxinde-MacBook-Pro:vue-demo yuxin$ npm run dev

./src/views/login下添加login.vue(也可以index.vue,但是为了清晰,先不这么写)文件,等下编写login.vue的代码。先修改一下路由配置./src/router/index.js,因为等下要访问:

vue-demo
| -

-

| - src
  | -
  | - views
     | -
     | - login
        | - login.vue (新增)

-
# ./src/router/index.js
import Login from '@/views/login/login'   // 新增
...
  routes: [
    {
      path: '/',
      name: 'Hello',
      component: Hello
    },  // 注意逗号
    // 新增
    {
      path: '/login',
      name: 'Login',
      component: Login
    }
  ]

因为我们已经跑起来了项目,在浏览器里访问login看看,http://localhost:8080/#/login 。我们看到一个图片,还发现有个错误,大概说template找不到,先不理它,因为我们的login.vue还是空白的。

我们注意到这样一个问题,为何路由是/#/login这个样子的,这里你的项目可以继续保持这样,但不喜欢也可以改变它。这里我们改变路由router的模式,由默认的hash变为history
相关阅读:https://router.vuejs.org/zh-cn/api/options.html#mode

# ./src/router/index.js
export default new Router({
  mode: 'history',  // 新增
  routes: [
     ...

之后的访问路径就变为http://localhost:8080/login。刚刚我们发现登陆页面出现了一个大大的logo图片,现在把它去掉,修改./src/App.vue文件,如下:

# ./src/App.vue
<template>
  <div id="app">
    ![](./assets/logo.png)    // 去掉这一句,删除
    ...

我们将会用到element-ui,去./src/main.js里把它引入进来,如下:

# ./src/main.js
...
import ElementUI from 'element-ui';    // 新增
import 'element-ui/lib/theme-default/index.css';    // 新增
...
Vue.use(ElementUI);    // 新增

/* eslint-disable no-new */
new Vue({
    ...

编辑./src/views/login/login.vue,添加代码如下:

# ./src/views/login/login.vue
<template>
  <div class="login-container">
    <el-form autoComplete="on" :model="loginForm" :rules="loginRules" ref="loginForm" label-position="left" label-width="0px" class="card-box login-form">
      <h3 class="title">系统登录</h3>
      <el-form-item prop="email">
        <span class="svg-container"><i class="el-icon-menu"></i></span>
        <el-input name="email" type="text" v-model="loginForm.email" autoComplete="on" placeholder="邮箱"></el-input>
      </el-form-item>
      <el-form-item prop="password">
        <span class="svg-container"><i class="el-icon-setting"></i></span>
        <el-input name="password" type="password" @keyup.enter.native="handleLogin" v-model="loginForm.password" autoComplete="on" placeholder="密码"></el-input>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" style="width:100%;" :loading="loading" @click.native.prevent="handleLogin">
            登录
        </el-button>
      </el-form-item>
      <div class='tips'>直接点登陆就可以登录成功...</div>
      <div class='tips'>如果想试试验证,建议删除输入框字符试试...</div>
    </el-form>
  </div>
</template>

<script>
  export default {
    data() {
      // 验证邮箱的规则
      const validateEmail = (rule, value, callback) => {
        if (!this.isEmail(value)) {
          callback(new Error('请输入正确的合法邮箱'));
        } else {
          callback();
        }
      };
      // 验证密码的规则
      const validatePass = (rule, value, callback) => {
        if (value.length < 6) {
          callback(new Error('密码不能小于6位'));
        } else {
          callback();
        }
      };
      return {
        loginForm: {
          email: 'admin@fusio.com.cn',
          password: '123456'
        },
        loginRules: {
          email: [
              { required: true, trigger: 'blur', validator: validateEmail }
          ],
          password: [
              { required: true, trigger: 'blur', validator: validatePass }
          ]
        },
        loading: false,
        showDialog: false
      }
    },
    methods: {
      isEmail(str) {
        const reg = /^[a-z0-9](?:[-_.+]?[a-z0-9]+)*@fusio\.com\.cn$/i;
        return reg.test(str.trim());
      },
      handleLogin() {
        // 验证表单
        this.$refs.loginForm.validate(valid => {
          if (valid) {
            // 验证通过
            this.loading = true;
            if(this.loginForm.email == 'admin@fusio.com.cn' && this.loginForm.password == '123456') {
               // 注意这里有2种方法
               // 注意下不同写法时this的指向问题,用=>可以保持this的固定指向

               // 方法1
               // let that = this;
               // setTimeout(function(){
               //   that.loading = false;
               //   that.$router.push({ path: '/' });
               // }, 1000);

               // 方法2
               setTimeout(() => {
                  this.loading = false;
                  this.$router.push({ path: '/' });
               }, 1000)
                    
            }
          } else {
            // 验证失败
            console.error('error submit!!');
            return false;
          }
        });
      }
    }
  }
</script>

<style rel="stylesheet/scss" lang="scss" scoprd>
  .tips{
    text-align: left;
    font-size: 14px;
    color: #999;
    margin-bottom: 5px;
  }
  .login-container {
    position: relative;
    width: 100%;
    height: 100%;
    height: 100vh;
    background-color: #2d3a4b;
  
    input:-webkit-autofill {
      -webkit-box-shadow: 0 0 0px 1000px #293444 inset !important;
      -webkit-text-fill-color: #fff !important;
    }
    input {
      background: transparent;
      border: 0px;
      -webkit-appearance: none;
      border-radius: 0px;
      padding: 12px 5px 12px 15px;
      color: #eeeeee;
      height: 47px;
    }
    .el-input {
      display: inline-block;
      height: 47px;
      width: 85%;
    }
    .svg-container {
      padding: 6px 5px 6px 15px;
      color: #889aa4;
    }
    .title {
      font-size: 26px;
      font-weight: 400;
      color: #eeeeee;
      margin: 0px auto 40px auto;
      text-align: center;
      font-weight: bold;
    }
    .login-form {
      position: absolute;
      left: 0;
      right: 0;
      width: 400px;
      padding: 35px 35px 15px 35px;
      margin: 120px auto;
    }
    .el-form-item {
      border: 1px solid rgba(255, 255, 255, 0.1);
      background: rgba(0, 0, 0, 0.1);
      border-radius: 5px;
      color: #454545;
    }
    .forget-pwd {
      color: #fff;
    }
  }
</style>

发现需要调整全局样式,比如body。我们把样式抽离出./src/App.vue,集中到单独的./src/styles/index.scss,注意这里开始用scss,反正都抽出来了。最后在./src/main.js文件中引用。

vue-demo
| -
| - src
  | -
  | - styles
     | - index.scss (新增)
  | -
| -

题外话:如果您只是抽成index.css,那就不用执行npm install去安装scss需要的模块。

# 安装sass需要的模块
Yuxinde-MacBook-Pro:vue-demo yuxin$ npm install sass-loader --save
npm WARN skippingAction Module is inside a symlinked module: not running update node-pre-gyp@0.6.36 node_modules/fsevents/node_modules/node-pre-gyp
npm WARN skippingAction Module is inside a symlinked module: not running move co@4.6.0 node_modules/ajv-keywords/node_modules/ajv/node_modules/co
npm WARN skippingAction Module is inside a symlinked module: not running move json-stable-stringify@1.0.1 node_modules/ajv-keywords/node_modules/ajv/node_modules/json-stable-stringify
npm WARN skippingAction Module is inside a symlinked module: not running add ajv@4.11.8 node_modules/ajv-keywords/node_modules/ajv
vue-demo@1.0.0 /Users/yuxin/Documents/Season/Project/Vue/vue-demo
├── UNMET PEER DEPENDENCY node-sass@^4.0.0
└─┬ sass-loader@6.0.6 
  ├─┬ clone-deep@0.3.0 
  │ ├── for-own@1.0.0 
  │ ├─┬ is-plain-object@2.0.3 
  │ │ └── isobject@3.0.0 
  │ ├── kind-of@3.2.2 
  │ └─┬ shallow-clone@0.1.2 
  │   ├── kind-of@2.0.1 
  │   ├── lazy-cache@0.2.7 
  │   └─┬ mixin-object@2.0.1 
  │     └── for-in@0.1.8 
  ├── lodash.tail@4.1.1 
  └── pify@3.0.0 

npm WARN sass-loader@6.0.6 requires a peer of node-sass@^4.0.0 but none was installed.

# 这里它提示我说node-sass没有安装
Yuxinde-MacBook-Pro:vue-demo yuxin$ npm install node-sass --save
(安装过程...略)

# 重新运行项目
Yuxinde-MacBook-Pro:vue-demo yuxin$ npm run dev

上面我们只是安装了scss用到的模块,然后去查看一下./build/webpack.base.conf.js里的配置代码,你发现它把各种css-loader整合在一个方法放在了./build/utils.js文件里,自己不需要再添加任何配置。

其他几个文件的代码修改如下:

# ./src/App.vue
...
// 删除style整块内容
<style>  
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}
</style>
...
# ./src/styles/index.css
// 全部为添加
body {
  margin: 0;
}
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

# ./src/main.js
...
import './styles/index.scss';  // 新增
...

现在我们访问http://localhost:8080/login,修改输入可以看到表单检查,直接点登录,则登录成功在1s后跳转首页。这里的1s只是为了看一下loading效果。具体代码里的一些vue基础知识,可以查阅官网。

Step4. 模版嵌套

前面我们已经能够实现单个无模版页面的编写,现在去看一下模板嵌套吧,这个在项目搭建中还是必须要使用的呢。

那我们先在./src/views/layout文件夹下创建这几个文件,分别是AppMain.vueindex.jsLayout.vueNavbar.vueSidebar.vue。然后在./src/views/dashboard文件夹下创建dashboard.vue,它将使用Layout.vue模版,这样我们才能看到效果。对应的新增文件结构如下:

vue-demo
| -
| - src
  | -
  | - views
     | -
     | - dashboard
        | - * dashboard.vue* (看板页面,新增)
     | - layout
        | - AppMain.vue (页面容器,新增)
        | - index.js (新增)
        | - Layout.vue (模版框架,新增)
        | - Navbar.vue (顶部导航栏,新增)
        | - Sidebar.vue (侧边菜单栏,新增)
        | - 更多还有面包屑层级路径(略)
  | -
| -

我们先不去给上面新建的各个文件添加代码,我们先去修改一下./src/router/index.js文件,创建dashboard页面的路径映射关系,调整一下hello页面的路径映射关系。修改代码如下:

# ./src/router/index.js 当前的完整代码
import Vue from 'vue'
import Router from 'vue-router'
import Hello from '@/views/hello/Hello'
import Login from '@/views/login/login'
import Dashboard from '@/views/dashboard/dashboard'

// Layout引入(这里采用这种写法2,不再提示)
// import Layout from '../views/layout/Layout';  // 写法1,通过../层级查找
import Layout from '@/views/layout/Layout';  // 写法2,@默认指向src文件夹,在./build/webpack.base.conf.js的alias可以配置

Vue.use(Router)

export default new Router({
  mode: 'history',
  // 注意
  // routes这里可以好多种结构写法,自己回头研究下吧
  routes: [
    {
      path: '/login',
      name: 'Login',
      component: Login
    },
    {
      path: '/',
      component: Layout,
      redirect: '/dashboard',
      children: [
        { path: '/dashboard', component: Dashboard, name: 'Dashboard' },
        { path: '/hello', component: Hello, name: 'Hello' }
      ]
    },
  ]
})

这时候,我们去访问http://localhost:8080/dashboard,它提示了我一个之前遇到过的template or render function not defined.错误,由于空白文件引起。现在我们去编辑一下./src/views/dashboard/dashboard.vue和```./src/views/layout/Layout.vue``文件,因为它们现在都是空白的。

# ./src/views/dashboard/dashboard.vue
<template>
  <p>This is dashboard page.</p>
</template>

<script>
  export default {
  }
</script>
# ./src/views/layout/Layout.vue 临时代码
# 模版框架
<template>
  <div class="app-wrapper">
    <p>This is a demo for learning vue.</p>
    <hr/>
    <router-view></router-view>
  </div>
</template>

<script>
  export default {
  }
</script>

再次访问http://localhost:8080/dashboard,发现页面正常无报错,并且显示了两个页面的两句话,说明模板框架已经被成功引用。下面我们快速的应用框架代码,借用比较常用的后台框架,学习模版嵌套使用。它们各文件的代码如下所示:

# ./src/views/layout/Layout.vue 新代码!!!
# 模版框架
<template>
  <div class="app-wrapper">
    <div class="navigator-wrapper">
      <div class="navigator-container">
        <div class="logo"></div>
        <Sidebar />
      </div>
    </div>
    <div class="main-container">
      <Navbar />
      <App-main />
    </div>
  </div>
</template>

<script>
  // 引入组件文件
  import { Navbar, Sidebar, AppMain } from '@/views/layout'; //也就是指向./layout/index.js
  export default {
    // 名称(可不写)
    name: 'layout',
    // 使用组件
    components: {
      Navbar,
      Sidebar,
      AppMain
    }
  }
</script>

<style rel="stylesheet/scss" lang="scss" scoped>
  .app-wrapper {
    padding-left: 190px;
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    &:after {
      content: "";
      display: table;
      clear: both;
    }
    .navigator-wrapper {
      display: block;
      position: absolute;
      z-index: 1000;
      width: 190px;
      left: 0;
      top: 0;
      bottom: 0;
      overflow: hidden;
      background: #2F323A;
      transition: all .28s ease-out;
      &::-webkit-scrollbar-track-piece {
        background: #d3dce6;
      }
      &::-webkit-scrollbar {
        width: 6px;
      }
      &::-webkit-scrollbar-thumb {
        background: #99a9bf;
        border-radius: 20px;
      }
    }
    .navigator-wrapper a:focus, 
    .navigator-wrapper a:active {
      outline: none;
    }
    .navigator-container {
      transition: all .28s ease-out;
    }
    .logo {
      box-shadow: -1px 0 0 1px #000;
      width: 190px;
      height: 70px;
      display: block;
      background-color: #111111;
      background-image: url(../../assets/logo.png);
      background-repeat: no-repeat;
      background-size: contain;
      background-position: 50%;
      overflow: hidden;
    }
    .main-container {
      width: 100%;
      height: 100%;
      overflow-y: hidden;
      transition: all .28s ease-out;
    }
  }
</style>
# ./src/views/layout/AppMain.vue
# 页面容器
<template>
  <section class="app-main" ref="wrap">
    <transition name="fade" mode="out-in">
      <router-view :key="key">
      </router-view>
    </transition>
  </section>
</template>

<script>
  export default {
    name: 'AppMain',
    computed: {
      key() {
        return this.$route.name !== undefined
          ? this.$route.name + +new Date()
          : this.$route + +new Date()
      }
    },
    methods:{
      scrollBottom() {
        var foot = this.$refs['wrap'].getElementsByClassName('container-footer')[0];
        foot?foot.style.bottom = -this.$refs.wrap.scrollTop + 'px':null;
      }
    }
  }
</script>

<style rel="stylesheet/scss" lang="scss" scoped>
  .app-main {
    position: relative;
    height: 100%;
    overflow-y: auto;
    background-color: #f5f5f5;
  }
</style>
# ./src/views/layout/Navbar.vue
# 顶部导航栏
<template>
  <div class="navbar">
    <div class="info-container">
      <a href="javascript:;" @click="logout"><i class="el-icon-upload2"></i> 退出</a>
    </div>
  </div>
</template>

<script>
  export default {
    methods: {
      logout() {
        this.$router.push({ path: '/login' });
      }
    }
  }
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
  .navbar {
    transition: height 0.28s;
    position: relative;
    height: 70px;
    line-height: 70px;
    color: #717171;
    background-color: #fff;
    padding: 0 20px 0 30px;
    border-radius: 0px !important;
    box-shadow: 0 0 0 1px #f6f6f6;
    .info-container {
      height: 70px;
      display: inline-block;
      position: absolute;
      right: 32px;
      >a {
        color: #999;
        text-decoration: none;
        &:hover {
          color: #11a0e0;
        }
      }
    }
  }
</style>

# ./src/views/layout/Sidebar.vue
# 侧边菜单栏
<template>
  <div class="menu">
    <!-- 为了简单,这里的菜单不是从router读回来的 -->
    <!-- 若要读回来,带上管理权限,请参考文后推荐的demo -->
    <el-menu :default-active="onRoutes" class="el-menu-vertical-demo" unique-opened router theme="dark">
      <el-menu-item index="/dashboard">工作台</el-menu-item>
      <el-menu-item index="/hello">Hello页面</el-menu-item>
    </el-menu>
  </div>
</template>

<script>
  export default {
    name: 'Sidebar',
    computed: {
      onRoutes () {
        return this.$route.path;
      }
    }
  }
</script>

<style rel="stylesheet/scss" lang="scss">
  .menu {
    overflow-y: auto;
    height: 100%;
    .el-menu {
      background-color: transparent;
    }
    .el-menu-item {
      background-color: transparent;
      &.is-active {
        background-color: #4e535f;
        color: #fff;
      }
    }
  }
</style>
# ./src/views/layout/index.js
# 快速引用
export { default as Navbar } from './Navbar';
export { default as Sidebar } from './Sidebar';
export { default as AppMain } from './AppMain';

再次访问http://localhost:8080/dashboard,发现整个后台的框架基本成型了,点击切换一下dashboardhello页面。上面的文件代码也基本可以看懂的,这一部分就这样了。

(请继续阅读下篇http://www.jianshu.com/p/9baa03eb31fc)


学习是一条漫漫长路,每天不求一大步,进步一点点就是好的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容