创建单文件 Vue 组件
为快速实现效果,我们一直在使用单个 HTML
页面编写 Vue
应用。但在实际的 Vue
或 Nuxt
开发项目中,我们不应采用这种写法:
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }
上述代码将两个 Vue
组件以 JavaScript
对象形式集中编写(例如在单个 HTML
页面中),更合理的做法是将它们分离到单独的 .js
文件中:
// components/foo.js
Vue.component('page-foo', {
data: function () {
return { message: 'foo' }
},
template: '<div>{{ count }}</div>'
})
这种方式适用于 HTML
结构简单的组件。但当涉及复杂 HTML
布局时,我们应避免在 JavaScript
文件中编写 HTML
标记。此时可以使用 .vue
扩展名的单文件组件:
<!-- index.vue -->
<template>
<p>{{ message }}</p>
</template>
<script>
export default {
data () {
return { message: 'Hello World!' }
}
}
</script>
<style scoped>
p {
font-size: 2em;
text-align: center;
}
</style>
注意:这类文件需要通过 webpack
或 rollup
等构建工具编译后才能运行。本书将使用 webpack
进行构建。这意味着从现在开始不再使用 CDN
或单个 HTML
页面开发复杂 Vue
应用。改用 .vue
和 .js
文件配合单个 .html
文件构建应用下文将指导如何使用 webpack
实现这一目标。
使用 webpack 编译单文件组件
要编译 .vue
组件,我们需要在 webpack
构建流程中安装 vue-loader
和 vue-template-compiler
。但在之前,必须在项目目录下创建 package.json
文件来列出项目依赖的 Node.js
包。您可以在 https://docs.npmjs.com/creating-a-package-json-file 查看 package.json
字段的详细信息。最基础且必需的字段是 name
和 version
。让我们开始操作:
-
在项目目录创建包含以下必需字段的
package.json
文件:package.json{ "name": "vue-single-file-component", "version": "1.0.0" }
-
打开终端,进入项目目录并安装依赖:
$ npm i vue-loader --save-dev $ npm i vue-template-compiler --save-dev
安装时会看到警告提示,因为这些
Node.js
包还依赖其他包(最值得注意的是webpack
包)。本书中我们已在GitHub
仓库的/chapter-5/vue/component-webpack/basic/
目录配置了基础webpack
构建流程,后续Vue
应用将主要采用这个配置。我们将webpack
配置文件拆分为三个部分:-
webpack.common.js:包含开发和生产环境共享的通用插件和配置
-
webpack.dev.js:仅包含开发环境专用配置
-
webpack.prod.js:仅包含生产环境专用配置
在
script
命令中的使用方式如下:package.json"scripts": { "start": "webpack-dev-server --open --config webpack.dev.js", "watch": "webpack --watch", "build": "webpack --config webpack.prod.js" }
本书假设您已掌握
webpack
编译JavaScript
模块的基础知识,初学者请参考 https://webpack.js.org/ 。 -
-
安装完依赖后,需要在
webpack.common.js
(若使用单一配置文件则是webpack.config.js
)中配置module.rules
:// webpack.common.js const VueLoaderPlugin = require('vue-loader/lib/plugin') module.exports = { mode: 'development', module: { rules: [ { test: /\.vue$/, loader: 'vue-loader' }, { test: /\.js$/, loader: 'babel-loader' }, { test: /\.css$/, use: [ 'vue-style-loader', 'css-loader' ] } ] }, plugins: [ new VueLoaderPlugin() ] }
-
最后可以通过
package.json
中预设的命令运行应用:$ npm run start # 在localhost:8080启用热重载开发服务 $ npm run watch # 在项目dist目录下开发构建 $ npm run build # 在项目dist目录下生产构建
至此,您已经建立了使用 webpack
开发 Vue
应用的基础构建流程。在后续更复杂的应用中,我们将编写单文件组件并采用这种方式进行编译。下一节我们将创建一个简单的 Vue
应用。
在单文件组件中传递数据和监听事件
目前为止,我们一直在使用单个 HTML
页面进行 "待办事项" 功能演示。这次我们将使用单文件组件构建一个简单的 "待办事项" 购物清单。让我们开始:
-
创建一个包含
Vue
实例挂载点(id
为 "todos" 的div
元素)的index.html
文件:<!doctype html> <html> <head> <title>待办购物清单应用(单文件组件)</title> </head> <body> <div id="todos"></div> </body> </html>
-
在项目根目录创建
/src/
文件夹,并在其中创建entry.js
文件作为webpack
的入口文件。webpack
将使用该文件确定应用的内部依赖关系图:// src/entry.js 'use strict' import Vue from 'vue/dist/vue.js' import App from './app.vue' new Vue({ el: 'todos', template: '<App/>', components: { App } })
-
创建父组件,在
<script>
块中提供包含商品列表的模拟数据:<!-- src/app.vue --> <template> <div> <ol> <TodoItem v-for="thing in groceryList" v-bind:item="thing" v-bind:key="item.id" v-on:add-item="addItem" v-on:delete-item="deleteItem" ></TodoItem> </ol> <p><span v-html="£"></span>{{ total }}</p> </div> </template> <script> import TodoItem from './todo-item.vue' export default { data () { return { cart: [], total: 0, groceryList: [ { id: 0, text: '小扁豆', price: 2 }, //... ] } }, components: { TodoItem } } </script>
在前面的代码中,我们只是简单地将子组件
TodoItem
导入,然后使用v-for
指令根据groceryList
中的数据生成一个子组件列表。 -
在
methods
对象中添加以下方法来实现商品增减功能,并在computed
对象中添加计算购物车总金额的方法:methods: { addItem (item) { this.cart.push(item) this.total = this.shoppingCartTotal }, deleteItem (item) { this.cart.splice(this.cart.findIndex(e => e === item), 1) this.total = this.shoppingCartTotal } }, computed: { shoppingCartTotal () { let prices = this.cart.map(item => item.price) let sum = prices.reduce((accumulator, currentValue) => accumulator + currentValue, 0) return sum } }
-
创建子组件来显示通过
props
从父组件传递的商品信息:
<!-- src/todo-item.vue -->
<template>
<li>
<input type="checkbox" :name="item.id" v-model="checked">
{{ item.text }}
<span v-html="£"></span>{{ item.price }}
</li>
</template>
<script>
export default {
props: ['item'],
data () {
return { checked: false }
},
methods: {
addToCart (item) {
this.$emit('add-item', item)
}
},
watch: {
checked (boolean) {
if (boolean === false) {
return this.$emit('delete-item', this.item)
}
this.$emit('add-item', this.item)
}
}
}
</script>
在这个组件中,我们通过复选框按钮来触发 delete-item
或 add-item
事件,将商品数据传递给父组件。现在如果运行 $npm run start
命令,应用将在 localhost:8080
加载显示。
干得漂亮!你已经成功使用 webpack
构建了基于单文件组件的 Vue
应用,而 Nuxt
在底层正是使用 webpack
来编译和构建应用的。了解底层运行机制总是很有帮助的。掌握 webpack
后,你可以将刚学到的 webpack
构建配置应用于各种 JavaScript
和 CSS
相关项目。
你可以在本书 |
下一节中,我们将把前面学到的知识应用到示例网站(位于本书 GitHub
仓库的 /chapter-5/nuxt-universal/local-components/sample-website/
目录)。
在 Nuxt 中添加 Vue 组件
在这个示例网站中,只有两个 .vue
文件可以通过 Vue
组件进行优化: /layouts/default.vue
和 /pages/work/index.vue
。首先我们应该改进 /layouts/default.vue
文件。这个文件中只需要优化三个部分:导航栏、社交媒体链接和版权信息。
重构导航和社交链接
我们将从重构导航栏和社交媒体链接开始:
-
在
/components/
目录下创建导航组件:<!-- components/nav.vue --> <template> <li> <nuxt-link :to="item.link" v-html="item.name"></nuxt-link> </li> </template> <script> export default { props: ['item'] } </script>
-
同样在
/components/
目录下创建社交媒体链接组件:<!-- components/social.vue --> <template> <li> <a :href="item.link" target="_blank"> <i :class="item.classes"></i> </a> </li> </template> <script> export default { props: ['item'] } </script>
-
在布局文件的
<script>
块中导入这些组件:// layouts/default.vue import Nav from '~/components/nav.vue' import Social from '~/components/social.vue' components: { Nav, Social }
注意:如果在
Nuxt
配置文件中已将components
选项设为true
,可以跳过此步骤。 -
从
<template>
块中移除原有的导航和社交媒体链接代码:<!-- 删除以下代码 --> <template v-for="item in nav"> <li><nuxt-link :to="item.link" v-html="item.name"></nuxt-link></li> </template> <template v-for="item in social"> <li> <a :href="item.link" target="_blank"> <i :class="item.classes"></i> </a> </li> </template>
-
用导入的
Nav
和Social
组件替换原有代码:<!-- layouts/default.vue --> <Nav v-for="item in nav" v-bind:item="item" v-bind:key="item.slug" ></Nav> <Social v-for="item in social" v-bind:item="item" v-bind:key="item.name" ></Social>
至此,重构工作就完成了!
重构版权组件
现在我们将重构已存在于 /components/
目录下的版权组件。让我们开始:
-
从
/components/base-copyright.vue
文件的<script>
块中移除data
函数:// components/copyright.vue export default { data () { return { copyright: '© Lau Tiam Kok' } } }
-
用
props
属性替换上述data
函数:// components/copyright.vue export default { props: ['copyright'] }
-
将版权数据改为在
/layouts/default.vue
的<script>
块中定义:// layouts/default.vue data () { return { copyright: '© Lau Tiam Kok', } }
-
移除
<template>
块中原有的<Copyright />
组件:// layouts/default.vue <!-- 删除此行 --> <Copyright />
-
添加新的
<Copyright />
组件并绑定版权数据:// layouts/default.vue <Copyright v-bind:copyright="copyright" />
通过以上步骤,您已成功实现了从默认页面(父组件)向组件(子组件)的数据传递。做得很好!至此已完成 /layouts/default.vue
的优化工作。我们同样优化了作品(work)页面,相关代码已为您准备好,可在本书 GitHub
仓库的 /chapter-5/nuxt-universal/local-components/sample-website/
目录找到。如果您在本地运行这个示例网站,将会看到我们最终完美应用的组件效果。
这个过程展示了:一旦理解 Vue
组件系统的工作原理,就能轻松将布局中的元素抽象为组件。但如何将数据传递回父组件呢?我们在 /chapter-5/nuxt-universal/local-components/emit-events/
目录创建了示例应用,演示子组件通过事件向父组件传递数据,您可以在本书 GitHub
仓库找到。我们还添加了自定义输入框和复选框组件,以下是示例代码片段:
<!-- components/input-checkbox.vue -->
<input
type="checkbox"
v-bind:checked="checked"
v-on:change="changed"
name="subscribe"
value="newsletter"
>
<script>
export default {
model: {
prop: 'checked',
event: 'change'
},
props: { checked: Boolean },
methods: {
changed ($event) {
this.$emit('change', $event.target.checked)
}
}
}
</script>
可以看到,我们在 Nuxt
应用中使用的组件代码与 Vue
应用中的写法完全一致。这类组件属于嵌套组件,通过 props
属性和 $emit
方法实现父子组件间的双向数据传递。从另一个角度看,Vue
组件可分为局部组件和全局组件。自 "什么是组件?" 章节以来,您一直在学习全局组件,但仅限于 Vue
应用中的用法。下一节我们将探讨如何在 Nuxt
应用中注册全局组件。不过在深入之前,让我们从全局视角重新审视 Vue
组件:全局组件与局部组件的区别与应用。