DDFE / DDFE-blog

:clap: welcome to DDFE's blog

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

【译】使用VueJS 2.0开发一款app

ustbhuangyi opened this issue · comments

VueJS 有新的版本推出,在这里可以看到所有的变化。这里有一个使用新版本开发的工作示例的应用程序,代码可以在 Github 上找到,去那里做一些很棒的事情吧。

VueJS 推出了一个新的版本。对于不知道 VueJS 为何物的同学,可以去他们的官网看看。VueJS 是一个融合了 AngularJS 和 React 一些特性的前端框架。第一版的 VueJS 可以被称作 “AgularJS lite”,它有类似 Angular 的模板系统,也用到了“脏检查”技术(原文这里有误,在Vue中,实际上用的是 es5 的 Object.defineProperty)去监测那些需要在 DOM 中变化的数据。然而,VueJS 的API 很小巧,因为它不会包含一些额外的工具库如 AJAX,这点和 React 很像。 然而,在下一个版本中,一些事情改变了。它像 React 一样使用了 “Virtual DOM” 模型,同时允许开发者选择任意类型的模板。因此,VueJS 的作者也实现了流式服务端渲染技术,这个技术在今天的 Web 开发中总是受欢迎的。幸运的是,API 接口本身没有多少改变。VueJS 的周边的开发工具需要更新去兼容新的版本,不过我们仍然可以使用 .vue 单文件去开发组件。想要好好的看一下新版本的实现和一些 API 的改变的话,仔细阅读 VueJS 这个Github issue

VueJS 2.0,像 React 一样,使用了 “Virtual DOM” 并且允许你使用任意的模板系统。

让我们用 Express,PassportJS,VueJS 2.0 开发一个简单的应用,来演示一下如何在应用程序中设置身份验证以及服务端和客户端是如何通信的。这个应用可以让用户去查看、添加和删除 exclamation。你可以查看任意用户的 exclamation,可以添加 exclamation,也可以随时删除自己的 exclamation 。你甚至可以删除其它用户的 exclamation,如果你有删除权限的话。

第一件事,让我们创建一个目录来保存我们的代码,然后引入初始依赖关系,我们使用 npm 进行安装。

mkdir vuejs2-authentication
cd vuejs2-authentication
npm init -y
npm install --save-dev nodemon
npm install --save express body-parser express-session connect-mongo flash node-uuid passport passport-local pug

这些将被用来创建我们的服务器。接下来,让我们创建一些虚拟的数据并存放在 data.json 文件中。

{
  "users": [
    {
      "username": "rachel@friends.com",
      "password": "green",
      "scopes": ["read", "add", "delete"]
    },
    {
      "username": "ross@friends.com",
      "password": "geller",
      "scopes": ["read"]
    }
  ],
  "exclamations": [
    {
      "id": "10ed2d7b-4a6c-4dad-ac25-d0a56c697753",
      "text": "I'm the holiday armadillo!",
      "user": "ross@friends.com"
    },
    {
      "id": "c03b65c8-477b-4814-aed0-b090d51e4ca0",
      "text": "It's like...all my life, everyone has always told me: \"You're a shoe!\"",
      "user": "rachel@friends.com"
    },
    {
      "id": "911327fa-c6fc-467f-8138-debedaa6d3ce",
      "text": "I...am over...YOU.",
      "user": "rachel@friends.com"
    },
    {
      "id": "ede699aa-9459-4feb-b95e-db1271ab41b7",
      "text": "Imagine the worst things you think about yourself. Now, how would you feel if the one person that you trusted the most in the world not only thinks them too, but actually uses them as reasons not to be with you.",
      "user": "rachel@friends.com"
    },
    {
      "id": "c58741cf-22fd-4036-88de-fe51fd006cfc",
      "text": "You threw my sandwich away?",
      "user": "ross@friends.com"
    },
    {
      "id": "dc8016e0-5d91-45c4-b4fa-48cecee11842",
      "text": "I grew up with Monica. If you didn't eat fast, you didn't eat!",
      "user": "ross@friends.com"
    },
    {
      "id": "87ba7f3a-2ce7-4aa0-9827-28261735f518",
      "text": "I'm gonna go get one of those job things.",
      "user": "rachel@friends.com"
    },
    {
      "id": "9aad4cbc-7fff-45b3-8373-a64d3fdb239b",
      "text": "Ross, I am a human doodle!",
      "user": "rachel@friends.com"
    }
  ]
}

另外,确保添加了如下脚本到package.json文件中。我们稍后会在写VueJS部分时添加更多的脚本。

"start": "node server.js",
"serve": "nodemon server.js"

接着创建server.js文件,添加如下代码:

// Import needed modules
const express = require('express');
const bodyParser = require('body-parser');
const session = require('express-session');
const MongoStore = require('connect-mongo')(session);
const flash = require('flash');
const passport = require('passport');
const LocalStrategy = require('passport-local');
const uuid = require('node-uuid');
const appData = require('./data.json');

// Create app data (mimics a DB)
const userData = appData.users;
const exclamationData = appData.exclamations;

function getUser(username) {
  const user = userData.find(u => u.username === username);
  return Object.assign({}, user);
}

// Create default port
const PORT = process.env.PORT || 3000;

// Create a new server
const server = express();

// Configure server
server.use(bodyParser.json());
server.use(bodyParser.urlencoded({ extended: false }));
server.use(session({
  secret: process.env.SESSION_SECRET || 'awesomecookiesecret',
  resave: false,
  saveUninitialized: false,
  store: new MongoStore({
    url: process.env.MONGO_URL || 'mongodb://localhost/vue2-auth',
  }),
}));
server.use(flash());
server.use(express.static('public'));
server.use(passport.initialize());
server.use(passport.session());
server.set('views', './views');
server.set('view engine', 'pug');

让我们过一下这部分代码。首先,我们引入了依赖库。接下来,我们引入了应用所需的数据文件,通常你会使用一些数据库,不过对于我们这个应用这样就足够了。最后,我们创建了一个 Express 的服务器并且用 session 和 body parser 模块对它配置。我们还开启了flash消息模块以及静态资源模块,这样我们可以通过 Node 服务器提供对Javascript文件的访问的服务。然后我们把Pug作为我们的模板引擎,我们将会在 home 页和 dashboard 页使用。

接下来,我们配置 Passport 来提供一些本地的身份验证。稍后我们会创建与它交互的页面。

// Configure Passport
passport.use(new LocalStrategy(
  (username, password, done) => {
    const user = getUser(username);

    if (!user || user.password !== password) {
      return done(null, false, { message: 'Username and password combination is wrong' });
    }

    delete user.password;

    return done(null, user);
  }
));

// Serialize user in session
passport.serializeUser((user, done) => {
  done(null, user.username);
});

passport.deserializeUser((username, done) => {
  const user = getUser(username);

  delete user.password;

  done(null, user);
});

这是一段相当标准的 Passport 代码。我们会告诉 Passport 我们本地的策略,当它尝试验证,我们会从用户数据中找到该用户,如果该用户存在且密码正确,那么我们继续前进,否则我们会返回一条消息给用户。同时我们也会把用户的名称放到session中,当我们需要获取用户消息的时候,我们可以直接通过 session 中的用户名查找到用户。

接下来部分的代码,我们将编写一些自定义的中间件功能应用到我们的路由上,去确保用户可以做某些事情。

// Create custom middleware functions
function hasScope(scope) {
  return (req, res, next) => {
    const { scopes } = req.user;

    if (!scopes.includes(scope)) {
      req.flash('error', 'The username and password are not valid.');
      return res.redirect('/');
    }

    return next();
  };
}

function canDelete(req, res, next) {
  const { scopes, username } = req.user;
  const { id } = req.params;
  const exclamation = exclamationData.find(exc => exc.id === id);

  if (!exclamation) {
    return res.sendStatus(404);
  }

  if (exclamation.user !== username && !scopes.includes('delete')) {
    return res.status(403).json({ message: "You can't delete that exclamation." });
  }

  return next();
}

function isAuthenticated(req, res, next) {
  if (!req.user) {
    req.flash('error', 'You must be logged in.');
    return res.redirect('/');
  }

  return next();
}

让我们过一下这段代码。hasScope 方法检查请求中的用户是否有所需的特定权限,我们通过传入一个字符串去调用该方法,它会返回一个服务端使用的中间件。canDelete 方法是类似的,不过它同时检查用户是否拥有这个 exclamation 以及是否拥有删除权限,如果都没有的话用户就不能删除这条 exclamation。这些方法稍后会被用到一个简单的路由上。最后,我们实现了 isAuthenticated,它仅仅是检查这个请求中是否包含用户字段来判断用户是否登录。

接下来,让我们创建2个主要的路由:home 路由和 dashboard 路由。

// Create home route
server.get('/', (req, res) => {
  if (req.user) {
    return res.redirect('/dashboard');
  }

  return res.render('index');
});

server.get('/dashboard',
  isAuthenticated,
  (req, res) => {
    res.render('dashboard');
  }
);

这里,我们创建了 home 路由。我们检查用户是否登录,如果登录,则把请求重定向到 dashborad 页面。同时我们创建了 dashborad 路由,我们先使用 isAuthenticated 中间件去确保用户已经登录,然后渲染 dashborad 页面模板。

现在我们需要去创建身份验证的路由。

// Create auth routes
const authRoutes = express.Router();

authRoutes.post('/login',
  passport.authenticate('local', {
    failureRedirect: '/',
    successRedirect: '/dashboard',
    failureFlash: true,
  })
);

server.use('/auth', authRoutes);

我们创建了路由安装在 /auth 路径上,它提供了一个简单路由 /login,这些我们稍后会在页面的表单提交时用到。

接下来,我们将会创建一些 API 的路由。这些 API 会允许我们获取所有的 exclamation,添加一条 exclamation,删除一条 exclamation。还有一个路由 /api/me 去获取当前登录用户的信息。为了保持结构统一,我们创建一个新的路由,把我们的路由添加上去,通过 /api 安装到服务中。

// Create API routes
const apiRoutes = express.Router();

apiRoutes.use(isAuthenticated);

apiRoutes.get('/me', (req, res) => {
  res.json({ user: req.user });
});

// Get all of a user's exclamations
apiRoutes.get('/exclamations',
  hasScope('read'),
  (req, res) => {
    const exclamations = exclamationData;

    res.json({ exclamations });
  }
);

// Add an exclamation
apiRoutes.post('/exclamations',
  hasScope('add'),
  (req, res) => {
    const { username } = req.user;
    const { text } = req.body;
    const exclamation = {
      id: uuid.v4(),
      text,
      user: username,
    };

    exclamationData.unshift(exclamation);

    res.status(201).json({ exclamation });
  }
);

// Delete an exclamation
apiRoutes.delete('/exclamations/:id',
  canDelete,
  (req, res) => {
    const { id } = req.params;
    const exclamationIndex = exclamationData.findIndex(exc => exc.id === id);

    exclamationData.splice(exclamationIndex, 1);

    res.sendStatus(204);
  }
);

server.use('/api', apiRoutes);

现在我们只需要启动服务。

// Start the server
server.listen(PORT, () => {
  console.log(`The API is listening on port ${PORT}`);
});

以上就是服务端我们需要的代码!我们仍然要创建模板。创建 views/index.pug 文件,添加以下代码。

doctype html
html(lang='en')
  head
    title Exclamations!
    link(rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css' integrity='sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7' crossorigin='anonymous')

    style.
      h1 {
        margin-bottom: 20px;
      }
  body
    .container-fluid
      .row
        .col-md-4.col-md-offset-4
          while message = flash.shift()
            .alert.alert-danger
              p= message.message
          h1.text-center Exclamations!
          form(action='/auth/login' method='POST')
            .form-group
              label(for='username') Email Address
              input.form-control(name='username')
            .form-group
              label(for='password') Password
              input.form-control(name='password' type='password')
            button.btn.btn-primary(type='submit') Login

这是一个基本的 HTML 页面,我们使用 bootstrap 去添加一些基本的样式。我们创建一个简单的表单,它用来提交数据到我们的服务器。我们还把从服务端输出的错误消息打印在页面中。
现在,通过执行 npm run serve 命令启动服务然后在浏览器中输入 localhost:3000,可以看到这个 login 页面。

login页面

然后从 data.json 文件里找到一条邮箱地址和密码的数据登录。一旦你登录,你会得到一条消息说我们没有 dashborad 模板。所以我们来现在来创建它。

doctype html
html(lang='en')
  head
    title Dashboard
    link(rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css' integrity='sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7' crossorigin='anonymous')
    link(rel='stylesheet' href='/styles.bundle.css')
  body
    #app-container
    script(src='app.bundle.js')

这代码也太少了,这一切都是在哪里?其实我们要做的就是给 VueJS 一个位置去安装它的初始化组件。这就是为什么我们只需要一个 app-container 和一个包含我们代码 script 文件就足够了。

这里并不会做任何事情,除非我们创建了这些文件并且建立一个开发管道让我们的代码工作起来。让我们创建这些文件并且下载所需的依赖。

mkdir public
touch public/app.bundle.js public/styles.bundle.css
npm install --save vue@next axios
npm install --save-dev babel-core babel-runtime babel-plugin-transform-runtime babel-preset-es2015 browserify babelify vueify@next browserify-hmr vue-hot-reload-api watchify concurrently

虽然需要安装很多依赖,但是这些都很简单。它能允许我们使用 Babel 和它的所有能力。我们通过 browersify 去打包代码,还可以使用 vueify 把组件放在一个文件中。注意到我们把 next 版本放在了 vue 和 vueify 后,这样可以安装到最新的 alpha 版本的 VueJS 和配合这个 VueJS 版本的 vueify。让我们给 package.json 文件添加一些脚本,来让我们的应用编译变得更加容易。

"prestart": "npm run build:js",
"build:js": "browserify src/app.js -t vueify -p [ vueify/plugins/extract-css -o public/styles.bundle.css ] -t babelify -o public/app.bundle.js",
"watch:js": "watchify src/app.js -t vueify -t babelify -p browserify-hmr -p [ vueify/plugins/extract-css -o public/styles.bundle.css ] -o public/app.bundle.js",
"dev": "concurrently \"npm run serve\" \"npm run watch:js\""

我们还需要对 Babel 做配置,创建一个 .babelrc 文件,添加如下代码:

{
  "presets": [
    "es2015"
  ],
  "plugins": [
    "transform-runtime"
  ]
}

我们已经做好了管道配置,在命令行运行 npm run dev 。它将启动我们的服务器,编译静态资源,同时监听我们 JavaScript 文件的变化。一切就绪,让我们去编写 Vue 应用吧。创建 src/app.js 文件,添加如下代码:

import Vue from 'vue';
import ExclamationsViewer from './exclamations_viewer.vue';

new Vue({
  el: '#app-container',
  render(createElement) {
    return createElement(ExclamationsViewer);
  },
});

这里我们依赖了 Vue 和 ExclamationsViewer 组件,稍后我们会创建它。然后我们创建了一个 Vue 实例。我们在实例化的时候会传入一个配置对象,它包含一个 el 属性,它是我们应用容器的一个选择器,在这个例子中它是 id 为 app-container 选择器。我们还会传入一个 render 方法,这是 Vue 创建模板的新方式。在以前,我们会传入 template 属性,值为模板字符串。现在,我们可以通过编程方式创建模板就像在 React 里用 React.createElment 方法一样。render 方法传入父组件的 createElement 方法,我们使用它去在父组件里创建子组件。在这个例子中我们要实例化一个 ExclamationsViewer 。

现在让我们来创建 ExclamationsViewer 组件。创建 src/exclamations_viewer.vue 文件,添加如下代码:

<style>
    .exclamations-viewer,
    .add-form-container {
      margin-top: 20px;
    }
  </style>

  <template>
    <div class="container">
      <div class="row exclamations-viewer">
        <div class="col-md-4">
          <Exclamation-List :user='user' title='All Exclamations' :exclamations='exclamations'></Exclamation-List>
        </div>
      </div>
    </div>
  </template>

  <script>
    import axios from 'axios';
    import ExclamationList from './exclamation_list.vue';

    export default {
      name: 'ExclamationsViewer',
      data: () => ({
        user: {
          scopes: [],
        },
        exclamations: [],
      }),
      beforeMount() {
        axios.all([
          axios.get('/api/me'),
          axios.get('/api/exclamations'),
        ]).then(([{ data: meData }, { data: exclamationData }]) => {
          this.user = meData.user;
          this.exclamations = exclamationData.exclamations;
        });
      },
      components: {
        ExclamationList,
      },
    };
  </script>

这里我们创建一个简单的 Vue 组件。因为我们用了 vueify ,我们可以在一个 vue 文件中把组件分离成 CSS,template,scrip t代码 3 个部分。CSS 被 <style></style> 标签包围。我们没有写太多 CSS 代码,只设置了一些间距。在模板中,我们设置了一个经典的 bootstrap 栅格布局并且添加了一个自定义组件 Exclamation-List,稍后我们会创建它。为了给组件传入 props 我们在属性前面加上了冒号,然后我们传入一个字符串,它表示我们要传递给子组件的一段数据。例如,:user='user' 表示我们把当前组件 data 中的 user 作为属性传入 Exclamation-List。接下来,在我们的 script 标签中,我们引入了 axios 库和 ExclamationList 库。我们通过设置 data 属性为一个 function 并调用它去实例化和组件的数据。这里,我们仅仅返回一个对象包一个拥有空 scopes 数组的 user 对象和一个空 exclamations 数组。任何将要被使用的数据需要在 data 对象里做初始化,这点很重要。否则,Vue 可能不能有效的监测到这部分数据的变化。

接下来,在 Vue 生命周期的 beforeMount 方法中去调用 API 请求当前登录用户和所有 exclamation 信息。然后我们把数据保存在组件中,它会替换掉我们在 data 方法里创建的数据,Vue 会完美实现这一点。最后,我们通过把 ExclamationList 组件添加到当前组件的 components 属性来局部注册。如果不添加的话,VueJS是不知道任何关于 ExclamationList 组件的。注意:我们添加组件的时候可以用 PascalCase(ExclamationList) 命名法,也可以用 camelCase(exclamationList) 命名法,但是在模板中引用组件的时候,必须用 list-case(Exclamation-List) 命名法。

接下来我们要做的事情就是创建 ExclamationList 组件。创建 src/exclamation_list.vue,然后添加如下代码:

  <style scoped>
    .exclamation-list {
      background-color: #FAFAFA;
      border: 2px solid #222;
      border-radius: 7px;
    }

    .exclamation-list h1 {
      font-size: 1.5em;
      text-align: center;
    }

    .exclamation:nth-child(2) {
      border-top: 1px solid #222;
    }

    .exclamation {
      padding: 5px;
      border-bottom: 1px solid #222;
    }

    .user {
      font-weight: bold;
      margin-top: 10px;
      margin-bottom: 5px;
    }
  </style>

  <template>
    <div class="exclamation-list">
      <h1></h1>
      <div class="exclamation" v-for='exclamation in exclamations' :key='exclamation.id'>
        <p class="user"></p>
        <p class="text"></p>
        <button v-if='canDelete(exclamation.user)' class="btn btn-danger">Remove</button>
      </div>
    </div>
  </template>

  <script>
    export default {
      props: {
        title: {
          type: String,
          default: '',
        },
        exclamations: {
          type: Array,
          default: () => ([]),
        },
        user: {
          default: {},
        },
      },
      methods: {
        canDelete(user) {
          return this.user.scopes.includes('delete') || this.user.username === user;
        },
      },
    };
  </script>

在这个组件中,我们写了更多的 CSS 代码,但没有什么疯狂的地方。在我们的模板中,我们遍历了从属性传入的 exclamations 。v-for 指令会遍历获得到每一个 exclamation 并且可以提供 exclamation 的属性给该 div 的内部元素使用。这里我们打印出用户名称和exclamation的文本。注意我们给 div 传入了一个 key 属性,它替代了早起版本 Vue 的 track-by 属性。它可以帮助 Vue 去优化 DOM 的修改,尽可能去复用已有示例。组件有一个 delete 按钮,只有当 user 拥有delete scope的时候才会显示出来。我们通过 v-if 指令去依据条件现实或者隐藏 delete 按钮。这个条件就是 canDelete 方法的的返回值,它接收 exclamation 的 user 属性作为参数。我们接下来把这个方法添加到组件中。

在组件的脚本部分,我们导出了一个对象并指出了 props 属性接收从父组件传来 props。我们期望 title 是字符串类型,exclamtions 是数组类型,user 是对象类型。接下来,我们创建一个 methods 对象,上面有 canDelete 方法,该方法检查当前用户是否含有 delete scope 或者拥有这个 exclamation。

如果我们打开浏览器,会看到所有 exclamation 。如果我们用 Rachel 这个用户登录,会显示所有 exclamation 删除按钮,但是用 Ross 这个用户登录,只能在他自己的 exclamation 显示删除按钮。

单例

既然我们有了删除按钮,接下来实现它的功能。因为是父组件拥有所有的数据,我们会传入子组件一个方法去从 API 层面和本地数据层面去删除父组件的 exclamation。

在 ExclamationsViewer 组件中,添加如下代码:

methods: {
  onExclamationRemoved(id) {
    axios.delete(`/api/exclamations/${id}`)
      .then(() => {
        this.exclamations = this.exclamations.filter(e => e.id !== id);
      });
  },
},

我们给 methods 对象添加了 onExclamationRemoved 方法,接收 exclamation 的 ID。该方法会发送一个 delete 请求到后端 API,然后过滤掉本地数据中被删除的 exclamation。所有使用这块数据的组件都会被更新。现在,我们需要把这个方法传入到子组件中,更新模板如下所示:

   <Exclamation-List :user='user' :onRemove='onExclamationRemoved' title='All Exclamations' :exclamations='exclamations'></Exclamation-List>

我们通过 onRmove 属性把方法传入子组件。现在我们在 ExclamationList 组件添加这个属性。

props: {
  ...
  onRemove: {
    default: () => {},
  },
  ...
},

我们还添加了一个方法到 methods 对象中

methods: {
  onRemoveClicked(id) {
    this.onRemove(id);
  },
  ...
}

这个方法对 onRemove 属性做了简单的封装,传入 exclamation 的 ID。现在我们就可以在 ExclamationList 模板中使用这个方法了。

<button v-on:click='onRemoveClicked(exclamation.id)' v-if='canDelete(exclamation.user)' class="btn btn-danger">Remove</button>

这里我们添加了带有 click 的 v-on 指令。这是一个很好的方式当元素被点击时告诉 Vue 去执行该表达式。我们通过传入 exclamation 的 ID 去运行 onRemovedClicked 方法,这会反过来让父组件通过 API 层面和本地数据层面删除该 exclamation。

现在如果你尝试删除一条 exclamation,它会从列表中删除。如果你刷新页面,它也不会再回来!

删除

为了看更多酷炫的功能,让我们创建另一个列表只显示用户自己的 exclamation。在 ExclamationsViewer 组件添加如下代码:

<div class="col-md-4">
   <Exclamation-List :user='user' :onRemove='onExclamationRemoved' title='Your Exclamations' :exclamations='userExclamations'></Exclamation-List>
</div>

这里,我们添加了另一个列表,不过注意到我们只把 userExclamations 作为属性。userExclamations 是一个计算属性。这是 Vue 的一个概念,它会执行一个方法,在模板中可以当做一个普通变使用。它只会计算一次,除非当它依赖的数据发生变化。我们会通过过滤当前的 exclamation 列表计算出 userExclamations。这样让我们可以单独处理两个列表,我们不必改变原来的列表。给 ExclamationsViewer 组件添加如下代码:

computed: {
  userExclamations() {
    return this.exclamations.filter(exc => exc.user === this.user.username);
  },
},

现在当你打开浏览器,你会看到两个列表。新的列表只会显示当前登录用户的 exclamations。

双列

这就是在我们组件代码背后的力量,我们可以在很多地方轻松的复用各个组件。

一旦我们有非常多的 exclamations,我们就很难找出特定的一条。我们可以给列表添加一个搜索框,我们将用一个新组件实现。这个新组件会复用当前的 ExclamationList 组件。创建 src/exclamation_search_list.vue,添加如下代码:

<template>
    <div>
      <div class="input-container">
        <div class="form-group">
          <label for='searchTerm'>Search:</label>
          <input v-model='searchTerm' type="text" class='form-control' placeholder="Search term">
        </div>
      </div>
      <Exclamation-List :user='user' :onRemove='onRemove' title='Filtered Exclamations' :exclamations='exclamationsToShow'></Exclamation-List>
    </div>
  </template>

  <script>
    import ExclamationList from './exclamation_list.vue';

    export default {
      data() {
        return {
          searchTerm: '',
        };
      },
      props: {
        exclamations: {
          type: Array,
          default: () => ([]),
        },
        onRemove: {
          default: () => {},
        },
        user: {
          default: {},
        },
      },
      computed: {
        exclamationsToShow() {
          let filteredExclamations = this.exclamations;

          this.searchTerm.split(' ')
            .map(t => t.split(':'))
            .forEach(([type, query]) => {
              if (!query) return;

              if (type === 'user') {
                filteredExclamations = filteredExclamations.filter(e => e.user.match(query));
              } else if (type === 'contains') {
                filteredExclamations = filteredExclamations.filter(e => e.text.match(query));
              }
            });

          return filteredExclamations;
        },
      },
      components: {
        ExclamationList,
      },
    };
  </script>

这里我们有一个由表单和 ExclamationList 组成的模板。表单没有什么特别的地方,它由一个 label 和一个 text input 构成。然而,注意到 input 上有一个 v-model 属性。传入该属性的字符串(本例是 ’searchForm’ )和我们想要同步给 input 的 value 上的 data 是相关联的。如果我们在input框输入改变它的 value, Vue 会更新组件中的 data 数据,这样我们就可以用这部分数据去过滤显示的 exclamations。

为了过滤 exclamations,我们将使用另一个计算属性。这个过滤规则允许我们按照 user 过滤或者按照文本内容过滤。如果我们想根据 user 过滤,我们输入 user:searchTerm,这个 searchTerm 就是我们要过滤的的 user。如果我们想根据文本内容过滤,我们输入 contains:searchTerm。如果我们同时想要 2 种过滤规则,我们可以设置空格分隔2种规则,然后分别获得 2 种规则的 type 和 query。最终,我们遍历这 2 种规则去过滤,拿到最终的过滤结果。

看一下我们在模板中 ExclamationList 部分添加的代码。注意到当前组件传递到子组件的属性和从父组件传来的属性名称相同,不同的是传入子组件的 exclamations 的值是 exclamationsToShow。一旦我们会当前组件中过滤它的值,那么它将会在ExclamationList组件中改变。

这是我们打开浏览器,它并没有任何显示,当然不会显示了!我们创建了组件,但我们并没有把它添加到 ExclamationsViewer 组件中。打开那个文件,把 ExclamationSearchList 添加到模板中,导入到 script 的依赖中,然后添加到 components 对象里。

<template>
  ...
  <div class="col-md-4">
    <Exclamation-Search-List :user='user' :onRemove='onExclamationRemoved' :exclamations='exclamations'></Exclamation-Search-List>
  </div>
  ...
  </template>

  <script>
  ...
  import ExclamationSearchList from './exclamation_search_list.vue';
  ...
  components: {
    ExclamationList,
    ExclamationSearchList,
  },
  ...
  </script>

现在我们打开浏览器,我们有了第三列列表,它可以根据 input 输入的文本过滤相关的 exclamations 显示。尝试输入 user:rac,它应该只会显示 Rachel 的 exclamations。

三列

我们还剩最后一件事,添加表单。创建 src/exclamation_add_form.vue 文件,添加如下代码:

<template>
    <form class="form-inline" v-on:submit.prevent='onFormSubmit'>
      <div class="form-group">
        <label for='exclamationText'>Exclamation</label>
        <textarea cols="30" rows="2" class="form-control" placeholder="Enter exclamation here." v-model='exclamationText'></textarea>
      </div>
      <input type="submit" value="Submit" class="btn btn-success">
    </form>
  </template>

  <script>
    export default {
      data() {
        return {
          exclamationText: '',
        };
      },
      props: ['onAdd'],
      methods: {
        onFormSubmit() {
          this.onAdd(this.exclamationText);
          this.exclamationText = '';
        },
      },
    };
  </script>

这是一个相当简单的组件。模板部分只有一个 textarea 和一个提交 button 构成的表单。表单上有 v-on 指令,v-on 指令是 Vue 中我们给DOM 元素添加事件的一种途径。我们使用 :submit 去表达我们要监听的是 submit 事件。.prevent 修饰符表示我们会在事件处理函数的最后自动调用 preventDefault 方法。在我们的 script 代码中,我们只需要通过 data 中的 exclamationText 跟踪 texteara 文本的变化。我们接收父组件传递的 onAdd 方法,当表单提交的时候会调用 onAdd 方法,传入 exclamationText,然后把 exclamationText 清空。

我们需要把这些添加到 ExclamationsViewer 组件中。

<template>
  <div class="container">
    <div class="row add-form-container" v-if='canAdd()'>
      <div class="col-md-12">
        <Exclamation-Add-Form :onAdd='onExclamationAdded'></Exclamation-Add-Form>
      </div>
    </div>
    <div class="row exclamations-viewer">
    ...
  </template>

  <script>
  import ExclamationAddForm from './exclamation_add_form.vue';
  ...
  methods: {
    onExclamationAdded(text) {
      axios.post('/api/exclamations', { text }).then(({ data }) => {
        this.exclamations = [data.exclamation].concat(this.exclamations);
      });
    },
    canAdd() {
      return this.user.scopes.includes('add');
    },
    onExclamationRemoved(id) {
    ...
  components: {
    ...
    ExclamationAddForm,
    ...
  },
  </script>

我们把 add 表单添加到模板中并传递一个 onAdd 属性。我们用v-if去条件显示这个表单,只有当用户拥有 add scope 的时候显示。我们添加了 onExclamationAdded 和 canAdd 方法到当前组件对象中,也把表单组件添加到 components 属性中。onExclamationAdded 把文本作为 post 请求发送到 API 接口,接着在把接口的返回值添加到我们的 exclamations 数组中。幸运的是,所有的列表都更新并显示我们新的 exclamation。

添加

如果我们打开浏览器,我们可以添加一条 exclamation。如果我们添加一条后刷新浏览器,它仍然在那儿。

原文链接

学习了。

commented

界面好丑!

没有提供源代码下载

希望能有2.0的文章开启~

@ilovevue 代码在这里:https://github.com/searsaw/vue2-auth
文章的开头也提到了喔

@fegg 我们团队今后会陆续输出vue的相关文章,请关注~

代码缩进的好蛋疼啊...

@BuptStEve 楼主是webstorm党,

@Jerret321 哈?咩意思?

@BuptStEve webstorm默认的缩进就是这个样子的。

@Jerret321 不对齐难道是一种feature?

<style>
    .exclamations-viewer,
    .add-form-container {
      margin-top: 20px;
    }
  </style>

  <template>
    <div class="container">
      <div class="row exclamations-viewer">
        <div class="col-md-4">
          <Exclamation-List :user='user' title='All Exclamations' :exclamations='exclamations'></Exclamation-List>
        </div>
      </div>
    </div>
  </template>

@BuptStEve 使用.editorconfig处理下规范就好了。 不是future额

@BuptStEve ,哈哈,可能是我粘代码的问题~

commented

学习

学习

不错,如果用上vuex的话就更好了,@ustbhuangyi 建议与一个结合 vuex的文章哈,谢谢