简体中文 繁體中文 English 日本語 Deutsch 한국 사람 بالعربية TÜRKÇE português คนไทย Français

站内搜索

搜索

活动公告

11-02 12:46
10-23 09:32
通知:本站资源由网友上传分享,如有违规等问题请到版务模块进行投诉,将及时处理!
10-23 09:31
10-23 09:28
通知:签到时间调整为每日4:00(东八区)
10-23 09:26

Vue2纯前端项目开发实战指南从基础到进阶全面掌握前端技术栈

3万

主题

423

科技点

3万

积分

大区版主

木柜子打湿

积分
31916

三倍冰淇淋无人之境【一阶】财Doro小樱(小丑装)立华奏以外的星空【二阶】⑨的冰沙

发表于 2025-9-21 09:50:00 | 显示全部楼层 |阅读模式 [标记阅至此楼]

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
引言

Vue.js是一款用于构建用户界面的渐进式JavaScript框架,由尤雨溪创建和维护。Vue2作为其重要版本,凭借其简洁的API、灵活的组件系统和强大的生态系统,在前端开发领域占据了重要地位。本文将从基础到进阶,全面介绍Vue2纯前端项目开发的核心知识和技术栈,帮助读者系统掌握Vue2开发技能。

Vue2基础知识

Vue2的安装与配置

Vue2可以通过多种方式安装和使用,最常见的是通过CDN引入或使用npm安装。

CDN引入方式:
  1. <!-- 开发环境版本,包含了有帮助的命令行警告 -->
  2. <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
  3. <!-- 生产环境版本,优化了尺寸和速度 -->
  4. <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js"></script>
复制代码

npm安装方式:
  1. # 最新稳定版
  2. npm install vue@2.6.14
复制代码

Vue CLI脚手架安装:
  1. # 全局安装Vue CLI
  2. npm install -g @vue/cli
  3. # 创建一个新项目
  4. vue create my-project
  5. # 进入项目目录
  6. cd my-project
  7. # 运行项目
  8. npm run serve
复制代码

Vue实例与生命周期

每个Vue应用都是通过创建一个Vue实例开始的:
  1. var vm = new Vue({
  2.   // 选项
  3. })
复制代码

Vue实例拥有一系列的生命周期钩子函数,这些函数允许我们在实例的不同阶段添加自己的代码:
  1. new Vue({
  2.   el: '#app',
  3.   data: {
  4.     message: 'Hello Vue!'
  5.   },
  6.   beforeCreate: function() {
  7.     console.log('beforeCreate: 实例初始化之后,数据观测和事件配置之前调用')
  8.   },
  9.   created: function() {
  10.     console.log('created: 实例创建完成之后调用')
  11.   },
  12.   beforeMount: function() {
  13.     console.log('beforeMount: 实例挂载之前调用')
  14.   },
  15.   mounted: function() {
  16.     console.log('mounted: 实例挂载完成后调用')
  17.     console.log(this.$el) // 访问DOM元素
  18.   },
  19.   beforeUpdate: function() {
  20.     console.log('beforeUpdate: 数据更新时调用,发生在虚拟DOM重新渲染和打补丁之前')
  21.   },
  22.   updated: function() {
  23.     console.log('updated: 数据更改导致虚拟DOM重新渲染和打补丁之后调用')
  24.   },
  25.   beforeDestroy: function() {
  26.     console.log('beforeDestroy: 实例销毁之前调用')
  27.   },
  28.   destroyed: function() {
  29.     console.log('destroyed: 实例销毁后调用')
  30.   }
  31. })
复制代码

模板语法

Vue.js使用了基于HTML的模板语法,允许开发者声明式地将DOM绑定至底层Vue实例的数据。

文本插值:
  1. <span>Message: {{ msg }}</span>
复制代码

一次性插值:
  1. <span v-once>这个将不会改变: {{ msg }}</span>
复制代码

原始HTML:
  1. <p>Using mustaches: {{ rawHtml }}</p>
  2. <p>Using v-html directive: <span v-html="rawHtml"></span></p>
复制代码

特性:
  1. <div v-bind:id="dynamicId"></div>
复制代码

使用JavaScript表达式:
  1. {{ number + 1 }}
  2. {{ ok ? 'YES' : 'NO' }}
  3. {{ message.split('').reverse().join('') }}
  4. <div v-bind:id="'list-' + id"></div>
复制代码

计算属性与侦听器

计算属性:
  1. var vm = new Vue({
  2.   el: '#example',
  3.   data: {
  4.     message: 'Hello'
  5.   },
  6.   computed: {
  7.     // 计算属性的 getter
  8.     reversedMessage: function () {
  9.       // `this` 指向 vm 实例
  10.       return this.message.split('').reverse().join('')
  11.     },
  12.     // 计算属性的 setter
  13.     fullName: {
  14.       // getter
  15.       get: function () {
  16.         return this.firstName + ' ' + this.lastName
  17.       },
  18.       // setter
  19.       set: function (newValue) {
  20.         var names = newValue.split(' ')
  21.         this.firstName = names[0]
  22.         this.lastName = names[names.length - 1]
  23.       }
  24.     }
  25.   }
  26. })
复制代码

侦听器:
  1. var vm = new Vue({
  2.   data: {
  3.     question: '',
  4.     answer: 'I cannot give you an answer until you ask a question!'
  5.   },
  6.   watch: {
  7.     // 如果 `question` 发生改变,这个函数就会运行
  8.     question: function (newQuestion, oldQuestion) {
  9.       this.answer = 'Waiting for you to stop typing...'
  10.       this.debouncedGetAnswer()
  11.     }
  12.   },
  13.   created: function () {
  14.     // `_.debounce` 是一个通过 Lodash 限制操作频率的函数。
  15.     // 在这个例子中,我们希望限制访问 yesno.wtf/api 的频率
  16.     // AJAX 请求直到用户输入完毕才会发出
  17.     this.debouncedGetAnswer = _.debounce(this.getAnswer, 500)
  18.   },
  19.   methods: {
  20.     getAnswer: function () {
  21.       if (this.question.indexOf('?') === -1) {
  22.         this.answer = 'Questions usually contain a question mark. ;-)'
  23.         return
  24.       }
  25.       this.answer = 'Thinking...'
  26.       var vm = this
  27.       axios.get('https://yesno.wtf/api')
  28.         .then(function (response) {
  29.           vm.answer = _.capitalize(response.data.answer)
  30.         })
  31.         .catch(function (error) {
  32.           vm.answer = 'Error! Could not reach the API. ' + error
  33.         })
  34.     }
  35.   }
  36. })
复制代码

条件渲染与列表渲染

条件渲染:
  1. <div v-if="type === 'A'">
  2.   A
  3. </div>
  4. <div v-else-if="type === 'B'">
  5.   B
  6. </div>
  7. <div v-else-if="type === 'C'">
  8.   C
  9. </div>
  10. <div v-else>
  11.   Not A/B/C
  12. </div>
  13. <!-- v-show -->
  14. <h1 v-show="ok">Hello!</h1>
复制代码

列表渲染:
  1. <ul id="example-1">
  2.   <li v-for="item in items" :key="item.message">
  3.     {{ item.message }}
  4.   </li>
  5. </ul>
  6. <!-- 带索引的 v-for -->
  7. <ul id="example-2">
  8.   <li v-for="(item, index) in items">
  9.     {{ parentMessage }} - {{ index }} - {{ item.message }}
  10.   </li>
  11. </ul>
  12. <!-- 遍历对象 -->
  13. <div v-for="(value, name, index) in object">
  14.   {{ index }}. {{ name }}: {{ value }}
  15. </div>
复制代码

事件处理
  1. <!-- 内联语句中的方法 -->
  2. <button v-on:click="say('hi')">Say hi</button>
  3. <!-- 缩写语法 -->
  4. <button @click="say('hi')">Say hi</button>
  5. <!-- 事件修饰符 -->
  6. <!-- 阻止单击事件继续传播 -->
  7. <a v-on:click.stop="doThis"></a>
  8. <!-- 提交事件不再重载页面 -->
  9. <form v-on:submit.prevent="onSubmit"></form>
  10. <!-- 修饰符可以串联 -->
  11. <a v-on:click.stop.prevent="doThat"></a>
  12. <!-- 按键修饰符 -->
  13. <!-- 只有在 `keyCode` 是 13 时调用 vm.submit() -->
  14. <input v-on:keyup.13="submit">
  15. <!-- 缩写语法 -->
  16. <input @keyup.enter="submit">
复制代码

表单输入绑定
  1. <!-- 文本 -->
  2. <input v-model="message" placeholder="edit me">
  3. <!-- 多行文本 -->
  4. <textarea v-model="message" placeholder="add multiple lines"></textarea>
  5. <!-- 复选框 -->
  6. <input type="checkbox" id="checkbox" v-model="checked">
  7. <label for="checkbox">{{ checked }}</label>
  8. <!-- 多个复选框,绑定到同一个数组 -->
  9. <div id='example-3'>
  10.   <input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
  11.   <label for="jack">Jack</label>
  12.   <input type="checkbox" id="john" value="John" v-model="checkedNames">
  13.   <label for="john">John</label>
  14.   <input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
  15.   <label for="mike">Mike</label>
  16.   <br>
  17.   <span>Checked names: {{ checkedNames }}</span>
  18. </div>
  19. <!-- 单选按钮 -->
  20. <div id="example-4">
  21.   <input type="radio" id="one" value="One" v-model="picked">
  22.   <label for="one">One</label>
  23.   <br>
  24.   <input type="radio" id="two" value="Two" v-model="picked">
  25.   <label for="two">Two</label>
  26.   <br>
  27.   <span>Picked: {{ picked }}</span>
  28. </div>
  29. <!-- 选择框 -->
  30. <div id="example-5">
  31.   <select v-model="selected">
  32.     <option disabled value="">请选择</option>
  33.     <option>A</option>
  34.     <option>B</option>
  35.     <option>C</option>
  36.   </select>
  37.   <span>Selected: {{ selected }}</span>
  38. </div>
  39. <!-- 多选时绑定到一个数组 -->
  40. <div id="example-6">
  41.   <select v-model="selected" multiple style="width: 50px;">
  42.     <option>A</option>
  43.     <option>B</option>
  44.     <option>C</option>
  45.   </select>
  46.   <br>
  47.   <span>Selected: {{ selected }}</span>
  48. </div>
复制代码

Vue2组件化开发

组件基础

组件是Vue.js最强大的功能之一。组件可以扩展HTML元素,封装可重用的代码。

全局注册:
  1. Vue.component('my-component', {
  2.   template: '<div>A custom component!</div>'
  3. })
复制代码

局部注册:
  1. var ComponentA = {
  2.   /* ... */
  3. }
  4. var ComponentB = {
  5.   /* ... */
  6. }
  7. new Vue({
  8.   el: '#app',
  9.   components: {
  10.     'component-a': ComponentA,
  11.     'component-b': ComponentB
  12.   }
  13. })
复制代码

组件的data选项:
  1. // 这样是错误的
  2. Vue.component('my-component', {
  3.   template: '<span>{{ message }}</span>',
  4.   data: {
  5.     message: 'hello'
  6.   }
  7. })
  8. // 正确做法
  9. Vue.component('my-component', {
  10.   template: '<span>{{ message }}</span>',
  11.   data: function () {
  12.     return {
  13.       message: 'hello'
  14.     }
  15.   }
  16. })
复制代码

组件通信

Props向下传递数据:
  1. Vue.component('child', {
  2.   // 声明 props
  3.   props: ['message'],
  4.   // 就像 data 一样,prop 也可以在模板中使用
  5.   // 同样也可以在 vm 实例中通过 this.message 来使用
  6.   template: '<span>{{ message }}</span>'
  7. })
  8. // 使用
  9. <child message="hello!"></child>
复制代码

自定义事件向上传递:
  1. Vue.component('button-counter', {
  2.   template: '<button v-on:click="incrementCounter">{{ counter }}</button>',
  3.   data: function () {
  4.     return {
  5.       counter: 0
  6.     }
  7.   },
  8.   methods: {
  9.     incrementCounter: function () {
  10.       this.counter += 1
  11.       this.$emit('increment')
  12.     }
  13.   },
  14. })
  15. // 使用
  16. <div id="counter-event-example">
  17.   <p>{{ total }}</p>
  18.   <button-counter v-on:increment="incrementTotal"></button-counter>
  19.   <button-counter v-on:increment="incrementTotal"></button-counter>
  20. </div>
复制代码

使用v-model进行双向绑定:
  1. Vue.component('my-input', {
  2.   props: ['value'],
  3.   template: `
  4.     <input
  5.       :value="value"
  6.       @input="$emit('input', $event.target.value)"
  7.     >
  8.   `
  9. })
  10. // 使用
  11. <my-input v-model="message"></my-input>
复制代码

非父子组件通信:
  1. // 简单的状态管理模式
  2. var store = {
  3.   debug: true,
  4.   state: {
  5.     message: 'Hello!'
  6.   },
  7.   setMessageAction (newValue) {
  8.     if (this.debug) console.log('setMessageAction triggered with', newValue)
  9.     this.state.message = newValue
  10.   },
  11.   clearMessageAction () {
  12.     if (this.debug) console.log('clearMessageAction triggered')
  13.     this.state.message = ''
  14.   }
  15. }
  16. // 组件A
  17. var vmA = new Vue({
  18.   data: {
  19.     privateState: {},
  20.     sharedState: store.state
  21.   }
  22. })
  23. // 组件B
  24. var vmB = new Vue({
  25.   data: {
  26.     privateState: {},
  27.     sharedState: store.state
  28.   }
  29. })
复制代码

插槽

插槽内容:
  1. <!-- 父组件模板 -->
  2. <child-component>
  3.   <p>这是一些初始内容</p>
  4.   <p>这是更多的初始内容</p>
  5. </child-component>
  6. <!-- 子组件模板 -->
  7. <div class="child">
  8.   <slot>
  9.     只有在没有要分发的内容时才会显示。
  10.   </slot>
  11. </div>
复制代码

具名插槽:
  1. <!-- 父组件模板 -->
  2. <base-layout>
  3.   <template v-slot:header>
  4.     <h1>Here might be a page title</h1>
  5.   </template>
  6.   <template v-slot:default>
  7.     <p>A paragraph for the main content.</p>
  8.     <p>And another one.</p>
  9.   </template>
  10.   <template v-slot:footer>
  11.     <p>Here's some contact info</p>
  12.   </template>
  13. </base-layout>
  14. <!-- 子组件模板 -->
  15. <div class="container">
  16.   <header>
  17.     <slot name="header"></slot>
  18.   </header>
  19.   <main>
  20.     <slot></slot>
  21.   </main>
  22.   <footer>
  23.     <slot name="footer"></slot>
  24.   </footer>
  25. </div>
复制代码

作用域插槽:
  1. <!-- 父组件模板 -->
  2. <child>
  3.   <template v-slot:default="slotProps">
  4.     {{ slotProps.user.firstName }}
  5.   </template>
  6. </child>
  7. <!-- 子组件模板 -->
  8. <span>
  9.   <slot v-bind:user="user">
  10.     {{ user.lastName }}
  11.   </slot>
  12. </span>
复制代码

动态组件与异步组件

动态组件:
  1. <!-- 组件会在 `currentTabComponent` 改变时改变 -->
  2. <component v-bind:is="currentTabComponent"></component>
复制代码

异步组件:
  1. Vue.component('async-webpack-example', function (resolve, reject) {
  2.   // 这个特殊的 require 语法将会告诉 webpack
  3.   // 自动将你的构建代码切割成多个包,这些包
  4.   // 会通过 Ajax 请求加载
  5.   require(['./my-async-component'], resolve)
  6. })
  7. // 或者
  8. Vue.component('async-webpack-example', () => import('./my-async-component'))
  9. // 高级异步组件
  10. const AsyncComp = () => ({
  11.   // 需要加载的组件 (应当是一个 Promise 对象)
  12.   component: import('./MyComp.vue'),
  13.   // 加载中应当渲染的组件
  14.   loading: LoadingComp,
  15.   // 出错时渲染的组件
  16.   error: ErrorComp,
  17.   // 渲染加载中组件前的等待时间。默认:200ms。
  18.   delay: 200,
  19.   // 最长等待时间。超出此时间则渲染错误组件。默认:Infinity
  20.   timeout: 3000
  21. })
复制代码

Vue Router路由管理

路由基础

安装:
  1. npm install vue-router
复制代码

基本使用:
  1. // 1. 定义 (路由) 组件。
  2. // 可以从其他文件 import 进来
  3. const Foo = { template: '<div>foo</div>' }
  4. const Bar = { template: '<div>bar</div>' }
  5. // 2. 定义路由
  6. // 每个路由应该映射一个组件。 其中"component" 可以是
  7. // 通过 Vue.extend() 创建的组件构造器,
  8. // 或者,只是一个组件配置对象。
  9. const routes = [
  10.   { path: '/foo', component: Foo },
  11.   { path: '/bar', component: Bar }
  12. ]
  13. // 3. 创建 router 实例,然后传 `routes` 配置
  14. const router = new VueRouter({
  15.   routes // (缩写) 相当于 routes: routes
  16. })
  17. // 4. 创建和挂载根实例。
  18. // 记得要通过 router 配置参数注入路由,
  19. // 从而让整个应用都有路由功能
  20. const app = new Vue({
  21.   router
  22. }).$mount('#app')
复制代码

HTML:
  1. <div id="app">
  2.   <h1>Hello App!</h1>
  3.   <p>
  4.     <!-- 使用 router-link 组件来导航. -->
  5.     <!-- 通过传入 `to` 属性指定链接. -->
  6.     <!-- <router-link> 默认会被渲染成一个 `<a>` 标签 -->
  7.     <router-link to="/foo">Go to Foo</router-link>
  8.     <router-link to="/bar">Go to Bar</router-link>
  9.   </p>
  10.   <!-- 路由出口 -->
  11.   <!-- 路由匹配到的组件将渲染在这里 -->
  12.   <router-view></router-view>
  13. </div>
复制代码

动态路由匹配
  1. const router = new VueRouter({
  2.   routes: [
  3.     // 动态路径参数 以冒号开头
  4.     { path: '/user/:id', component: User }
  5.   ]
  6. })
  7. // 在组件中访问
  8. const User = {
  9.   template: '<div>User {{ $route.params.id }}</div>'
  10. }
  11. // 响应路由参数的变化
  12. const User = {
  13.   template: '...',
  14.   watch: {
  15.     '$route' (to, from) {
  16.       // 对路由变化作出响应...
  17.     }
  18.   }
  19. }
  20. // 或者
  21. const User = {
  22.   template: '...',
  23.   beforeRouteUpdate (to, from, next) {
  24.     // react to route changes...
  25.     // don't forget to call next()
  26.   }
  27. }
复制代码

嵌套路由
  1. const router = new VueRouter({
  2.   routes: [
  3.     { path: '/user/:id', component: User,
  4.       children: [
  5.         {
  6.           // 当 /user/:id/profile 匹配成功,
  7.           // UserProfile 会被渲染在 User 的 <router-view> 中
  8.           path: 'profile',
  9.           component: UserProfile
  10.         },
  11.         {
  12.           // 当 /user/:id/posts 匹配成功
  13.           // UserPosts 会被渲染在 User 的 <router-view> 中
  14.           path: 'posts',
  15.           component: UserPosts
  16.         }
  17.       ]
  18.     }
  19.   ]
  20. })
复制代码

编程式导航
  1. // 字符串
  2. router.push('home')
  3. // 对象
  4. router.push({ path: 'home' })
  5. // 命名的路由
  6. router.push({ name: 'user', params: { userId: '123' }})
  7. // 带查询参数,变成 /register?plan=private
  8. router.push({ path: 'register', query: { plan: 'private' }})
  9. // 替换当前位置,不会向 history 添加新记录
  10. router.replace(...)
  11. // 在浏览器记录中前进一步,等同于 history.forward()
  12. router.go(1)
  13. // 后退一步记录,等同于 history.back()
  14. router.go(-1)
  15. // 前进 3 步记录
  16. router.go(3)
  17. // 如果 history 记录不够用,那就默默地失败呗
  18. router.go(-100)
  19. router.go(100)
复制代码

路由守卫

全局守卫:
  1. const router = new VueRouter({ ... })
  2. router.beforeEach((to, from, next) => {
  3.   // ...
  4. })
  5. router.beforeResolve((to, from, next) => {
  6.   // ...
  7. })
  8. router.afterEach((to, from) => {
  9.   // ...
  10. })
复制代码

路由独享的守卫:
  1. const router = new VueRouter({
  2.   routes: [
  3.     {
  4.       path: '/foo',
  5.       component: Foo,
  6.       beforeEnter: (to, from, next) => {
  7.         // ...
  8.       }
  9.     }
  10.   ]
  11. })
复制代码

组件内的守卫:
  1. const Foo = {
  2.   template: `...`,
  3.   beforeRouteEnter (to, from, next) {
  4.     // 在渲染该组件的对应路由被 confirm 前调用
  5.     // 不!能!获取组件实例 `this`
  6.     // 因为当守卫执行前,组件实例还没被创建
  7.   },
  8.   beforeRouteUpdate (to, from, next) {
  9.     // 在当前路由改变,但是该组件被复用时调用
  10.     // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
  11.     // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
  12.     // 可以访问组件实例 `this`
  13.   },
  14.   beforeRouteLeave (to, from, next) {
  15.     // 导航离开该组件的对应路由时调用
  16.     // 可以访问组件实例 `this`
  17.   }
  18. }
复制代码

Vuex状态管理

Vuex核心概念

安装:
  1. npm install vuex
复制代码

基本使用:
  1. import Vue from 'vue'
  2. import Vuex from 'vuex'
  3. Vue.use(Vuex)
  4. const store = new Vuex.Store({
  5.   state: {
  6.     count: 0
  7.   },
  8.   mutations: {
  9.     increment (state) {
  10.       state.count++
  11.     }
  12.   }
  13. })
  14. // 在组件中使用
  15. this.$store.commit('increment')
  16. console.log(this.$store.state.count) // -> 1
复制代码

State、Getter、Mutation、Action

State:
  1. // 创建一个 Counter 组件
  2. const Counter = {
  3.   template: `<div>{{ count }}</div>`,
  4.   computed: {
  5.     count () {
  6.       return this.$store.state.count
  7.     }
  8.   }
  9. }
  10. // 或者使用 mapState 辅助函数
  11. import { mapState } from 'vuex'
  12. export default {
  13.   // ...
  14.   computed: mapState({
  15.     // 箭头函数可使代码更简练
  16.     count: state => state.count,
  17.     // 传字符串参数 'count' 等同于 `state => state.count`
  18.     countAlias: 'count',
  19.     // 为了能够使用 `this` 获取局部状态,必须使用常规函数
  20.     countPlusLocalState (state) {
  21.       return state.count + this.localCount
  22.     }
  23.   })
  24. }
复制代码

Getter:
  1. const store = new Vuex.Store({
  2.   state: {
  3.     todos: [
  4.       { id: 1, text: '...', done: true },
  5.       { id: 2, text: '...', done: false }
  6.     ]
  7.   },
  8.   getters: {
  9.     doneTodos: state => {
  10.       return state.todos.filter(todo => todo.done)
  11.     }
  12.   }
  13. })
  14. // 访问
  15. store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
  16. // 使用 mapGetters 辅助函数
  17. import { mapGetters } from 'vuex'
  18. export default {
  19.   // ...
  20.   computed: {
  21.     // 使用对象展开运算符将 getter 混入 computed 对象中
  22.     ...mapGetters([
  23.       'doneTodosCount',
  24.       'anotherGetter',
  25.       // ...
  26.     ])
  27.   }
  28. }
复制代码

Mutation:
  1. const store = new Vuex.Store({
  2.   state: {
  3.     count: 1
  4.   },
  5.   mutations: {
  6.     increment (state) {
  7.       // 变更状态
  8.       state.count++
  9.     }
  10.   }
  11. })
  12. // 提交
  13. store.commit('increment')
  14. // 提交载荷(Payload)
  15. mutations: {
  16.   increment (state, n) {
  17.     state.count += n
  18.   }
  19. }
  20. store.commit('increment', 10)
  21. // 对象风格的提交方式
  22. store.commit({
  23.   type: 'increment',
  24.   amount: 10
  25. })
  26. // 使用 mapMutations 辅助函数
  27. import { mapMutations } from 'vuex'
  28. export default {
  29.   // ...
  30.   methods: {
  31.     ...mapMutations([
  32.       'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`
  33.       // `mapMutations` 也支持载荷:
  34.       'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
  35.     ]),
  36.     ...mapMutations({
  37.       add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
  38.     })
  39.   }
  40. }
复制代码

Action:
  1. const store = new Vuex.Store({
  2.   state: {
  3.     count: 0
  4.   },
  5.   mutations: {
  6.     increment (state) {
  7.       state.count++
  8.     }
  9.   },
  10.   actions: {
  11.     increment (context) {
  12.       context.commit('increment')
  13.     },
  14.     incrementAsync ({ commit }) {
  15.       setTimeout(() => {
  16.         commit('increment')
  17.       }, 1000)
  18.     }
  19.   }
  20. })
  21. // 分发 Action
  22. store.dispatch('increment')
  23. // 以载荷形式分发
  24. store.dispatch('incrementAsync', {
  25.   amount: 10
  26. })
  27. // 以对象形式分发
  28. store.dispatch({
  29.   type: 'incrementAsync',
  30.   amount: 10
  31. })
  32. // 使用 mapActions 辅助函数
  33. import { mapActions } from 'vuex'
  34. export default {
  35.   // ...
  36.   methods: {
  37.     ...mapActions([
  38.       'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`
  39.       // `mapActions` 也支持载荷:
  40.       'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
  41.     ]),
  42.     ...mapActions({
  43.       add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
  44.     })
  45.   }
  46. }
复制代码

模块化
  1. const moduleA = {
  2.   state: () => ({ ... }),
  3.   mutations: { ... },
  4.   actions: { ... },
  5.   getters: { ... }
  6. }
  7. const moduleB = {
  8.   state: () => ({ ... }),
  9.   mutations: { ... },
  10.   actions: { ... }
  11. }
  12. const store = new Vuex.Store({
  13.   modules: {
  14.     a: moduleA,
  15.     b: moduleB
  16.   }
  17. })
  18. store.state.a // -> moduleA 的状态
  19. store.state.b // -> moduleB 的状态
  20. // 在带命名空间的模块内访问全局内容
  21. modules: {
  22.   foo: {
  23.     namespaced: true,
  24.     getters: {
  25.       // 在这个模块的 getter 中,`getters` 被局部化了
  26.       // 你可以使用 getter 的第四个参数来调用 `rootGetters`
  27.       someGetter (state, getters, rootState, rootGetters) {
  28.         getters.someOtherGetter // -> 'foo/someOtherGetter'
  29.         rootGetters.someOtherGetter // -> 'someOtherGetter'
  30.       },
  31.       someOtherGetter: state => { ... }
  32.     },
  33.     actions: {
  34.       // 在这个模块中, dispatch 和 commit 也被局部化了
  35.       // 他们可以接受 `root` 属性以访问根 dispatch 或 commit
  36.       someAction ({ dispatch, commit, getters, rootGetters }) {
  37.         getters.someGetter // -> 'foo/someGetter'
  38.         rootGetters.someGetter // -> 'someGetter'
  39.         dispatch('someOtherAction') // -> 'foo/someOtherAction'
  40.         dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'
  41.         commit('someMutation') // -> 'foo/someMutation'
  42.         commit('someMutation', null, { root: true }) // -> 'someMutation'
  43.       },
  44.       someOtherAction (ctx, payload) { ... }
  45.     }
  46.   }
  47. }
复制代码

Vue2进阶技巧

自定义指令

注册全局指令:
  1. // 注册一个全局自定义指令 `v-focus`
  2. Vue.directive('focus', {
  3.   // 当被绑定的元素插入到 DOM 中时……
  4.   inserted: function (el) {
  5.     // 聚焦元素
  6.     el.focus()
  7.   }
  8. })
复制代码

注册局部指令:
  1. directives: {
  2.   focus: {
  3.     // 指令的定义
  4.     inserted: function (el) {
  5.       el.focus()
  6.     }
  7.   }
  8. }
复制代码

钩子函数:
  1. Vue.directive('demo', {
  2.   bind: function (el, binding, vnode) {
  3.     var s = JSON.stringify
  4.     el.innerHTML =
  5.       'name: '       + s(binding.name) + '<br>' +
  6.       'value: '      + s(binding.value) + '<br>' +
  7.       'expression: ' + s(binding.expression) + '<br>' +
  8.       'argument: '   + s(binding.arg) + '<br>' +
  9.       'modifiers: '  + s(binding.modifiers) + '<br>' +
  10.       'vnode keys: ' + Object.keys(vnode).join(', ')
  11.   }
  12. })
复制代码

过滤器

创建过滤器:
  1. // 在组件的选项中定义本地过滤器
  2. filters: {
  3.   capitalize: function (value) {
  4.     if (!value) return ''
  5.     value = value.toString()
  6.     return value.charAt(0).toUpperCase() + value.slice(1)
  7.   }
  8. }
  9. // 创建全局过滤器
  10. Vue.filter('capitalize', function (value) {
  11.   if (!value) return ''
  12.   value = value.toString()
  13.   return value.charAt(0).toUpperCase() + value.slice(1)
  14. })
复制代码

使用过滤器:
  1. <!-- 在双花括号中 -->
  2. {{ message | capitalize }}
  3. <!-- 在 `v-bind` 中 -->
  4. <div v-bind:id="rawId | formatId"></div>
  5. <!-- 过滤器可以串联 -->
  6. {{ message | filterA | filterB }}
  7. <!-- 过滤器是 JavaScript 函数,因此可以接收参数 -->
  8. {{ message | filterA('arg1', arg2) }}
复制代码

混入

定义混入:
  1. // 定义一个混入对象
  2. var myMixin = {
  3.   created: function () {
  4.     this.hello()
  5.   },
  6.   methods: {
  7.     hello: function () {
  8.       console.log('hello from mixin!')
  9.     }
  10.   }
  11. }
  12. // 定义一个使用混入对象的组件
  13. var Component = Vue.extend({
  14.   mixins: [myMixin]
  15. })
  16. var component = new Component() // -> "hello from mixin!"
复制代码

选项合并:
  1. var mixin = {
  2.   data: function () {
  3.     return {
  4.       message: 'hello',
  5.       foo: 'abc'
  6.     }
  7.   }
  8. }
  9. new Vue({
  10.   mixins: [mixin],
  11.   data: function () {
  12.     return {
  13.       message: 'goodbye',
  14.       bar: 'def'
  15.     }
  16.   },
  17.   created: function () {
  18.     console.log(this.$data)
  19.     // => { message: "goodbye", foo: "abc", bar: "def" }
  20.   }
  21. })
复制代码

插件开发

开发插件:
  1. MyPlugin.install = function (Vue, options) {
  2.   // 1. 添加全局方法或 property
  3.   Vue.myGlobalMethod = function () {
  4.     // 逻辑...
  5.   }
  6.   // 2. 添加全局资源
  7.   Vue.directive('my-directive', {
  8.     bind (el, binding, vnode, oldVnode) {
  9.       // 逻辑...
  10.     }
  11.     ...
  12.   })
  13.   // 3. 注入组件选项
  14.   Vue.mixin({
  15.     created: function () {
  16.       // 逻辑...
  17.     }
  18.     ...
  19.   })
  20.   // 4. 添加实例方法
  21.   Vue.prototype.$myMethod = function (methodOptions) {
  22.     // 逻辑...
  23.   }
  24. }
复制代码

使用插件:
  1. // 调用 `MyPlugin.install(Vue)`
  2. Vue.use(MyPlugin)
  3. // 传入可选的选项对象
  4. Vue.use(MyPlugin, { someOption: true })
复制代码

项目实战

项目结构设计

一个典型的Vue2项目结构如下:
  1. my-project
  2. ├── public                     # 静态资源
  3. │   ├── favicon.ico           # 网站图标
  4. │   └── index.html            # HTML 模板
  5. ├── src                        # 源代码
  6. │   ├── assets                # 项目资源
  7. │   │   ├── logo.png          # Logo
  8. │   │   └── css               # 样式文件
  9. │   │       ├── global.css    # 全局样式
  10. │   │       └── variables.scss # SCSS 变量
  11. │   ├── components            # 公共组件
  12. │   │   ├── HelloWorld.vue    # 示例组件
  13. │   │   └── CommonHeader.vue  # 公共头部
  14. │   ├── layouts               # 布局组件
  15. │   │   └── MainLayout.vue    # 主布局
  16. │   ├── views                 # 页面组件
  17. │   │   ├── Home.vue          # 首页
  18. │   │   └── About.vue         # 关于页面
  19. │   ├── router                # 路由配置
  20. │   │   └── index.js          # 路由主文件
  21. │   ├── store                 # Vuex 状态管理
  22. │   │   ├── index.js          # Vuex 主文件
  23. │   │   └── modules           # 模块
  24. │   │       ├── user.js       # 用户模块
  25. │   │       └── app.js        # 应用模块
  26. │   ├── utils                 # 工具函数
  27. │   │   ├── request.js        # 请求封装
  28. │   │   └── auth.js           # 权限工具
  29. │   ├── App.vue               # 根组件
  30. │   └── main.js               # 入口文件
  31. ├── .env                      # 环境变量
  32. ├── .env.development          # 开发环境变量
  33. ├── .env.production           # 生产环境变量
  34. ├── .gitignore                # Git 忽略文件
  35. ├── babel.config.js           # Babel 配置
  36. ├── package.json              # 项目依赖
  37. ├── vue.config.js             # Vue CLI 配置
  38. └── README.md                 # 项目说明
复制代码

API封装与请求拦截

request.js:
  1. import axios from 'axios'
  2. import { Message, Loading } from 'element-ui'
  3. import store from '@/store'
  4. import { getToken } from '@/utils/auth'
  5. // 创建axios实例
  6. const service = axios.create({
  7.   baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
  8.   timeout: 5000 // 请求超时时间
  9. })
  10. // 请求拦截器
  11. let loadingInstance = null
  12. service.interceptors.request.use(
  13.   config => {
  14.     // 如果是特定接口,显示loading
  15.     if (config.showLoading) {
  16.       loadingInstance = Loading.service({
  17.         lock: true,
  18.         text: 'Loading...',
  19.         spinner: 'el-icon-loading',
  20.         background: 'rgba(0, 0, 0, 0.7)'
  21.       })
  22.     }
  23.    
  24.     // 如果有token,添加到请求头
  25.     if (store.getters.token) {
  26.       config.headers['X-Token'] = getToken()
  27.     }
  28.     return config
  29.   },
  30.   error => {
  31.     // 关闭loading
  32.     if (loadingInstance) {
  33.       loadingInstance.close()
  34.     }
  35.     console.log(error)
  36.     return Promise.reject(error)
  37.   }
  38. )
  39. // 响应拦截器
  40. service.interceptors.response.use(
  41.   response => {
  42.     // 关闭loading
  43.     if (loadingInstance) {
  44.       loadingInstance.close()
  45.     }
  46.    
  47.     const res = response.data
  48.    
  49.     // 如果自定义code不是200,则判断为错误
  50.     if (res.code !== 200) {
  51.       Message({
  52.         message: res.message || 'Error',
  53.         type: 'error',
  54.         duration: 5 * 1000
  55.       })
  56.       
  57.       // 50008: 非法的token; 50012: 其他客户端登录; 50014: Token过期;
  58.       if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
  59.         // 重新登录
  60.         MessageBox.confirm(
  61.           '您已被登出,可以取消继续留在该页面,或者重新登录',
  62.           '确定登出',
  63.           {
  64.             confirmButtonText: '重新登录',
  65.             cancelButtonText: '取消',
  66.             type: 'warning'
  67.           }
  68.         ).then(() => {
  69.           store.dispatch('user/resetToken').then(() => {
  70.             location.reload()
  71.           })
  72.         })
  73.       }
  74.       return Promise.reject(new Error(res.message || 'Error'))
  75.     } else {
  76.       return res
  77.     }
  78.   },
  79.   error => {
  80.     // 关闭loading
  81.     if (loadingInstance) {
  82.       loadingInstance.close()
  83.     }
  84.    
  85.     console.log('err' + error)
  86.     Message({
  87.       message: error.message,
  88.       type: 'error',
  89.       duration: 5 * 1000
  90.     })
  91.     return Promise.reject(error)
  92.   }
  93. )
  94. export default service
复制代码

API模块化:
  1. // api/user.js
  2. import request from '@/utils/request'
  3. export function login(data) {
  4.   return request({
  5.     url: '/user/login',
  6.     method: 'post',
  7.     data
  8.   })
  9. }
  10. export function getInfo(token) {
  11.   return request({
  12.     url: '/user/info',
  13.     method: 'get',
  14.     params: { token }
  15.   })
  16. }
  17. export function logout() {
  18.   return request({
  19.     url: '/user/logout',
  20.     method: 'post'
  21.   })
  22. }
  23. // 在组件中使用
  24. import { login, getInfo, logout } from '@/api/user'
  25. export default {
  26.   methods: {
  27.     handleLogin() {
  28.       this.loading = true
  29.       login(this.loginForm)
  30.         .then(() => {
  31.           this.$router.push({ path: this.redirect || '/' })
  32.           this.loading = false
  33.         })
  34.         .catch(() => {
  35.           this.loading = false
  36.         })
  37.     }
  38.   }
  39. }
复制代码

权限控制

路由权限:
  1. // router/index.js
  2. import Vue from 'vue'
  3. import VueRouter from 'vue-router'
  4. import Layout from '@/layouts'
  5. Vue.use(VueRouter)
  6. // 基础路由
  7. export const constantRoutes = [
  8.   {
  9.     path: '/login',
  10.     component: () => import('@/views/login/index'),
  11.     hidden: true
  12.   },
  13.   {
  14.     path: '/404',
  15.     component: () => import('@/views/404'),
  16.     hidden: true
  17.   },
  18.   {
  19.     path: '/',
  20.     component: Layout,
  21.     redirect: '/dashboard',
  22.     children: [
  23.       {
  24.         path: 'dashboard',
  25.         component: () => import('@/views/dashboard/index'),
  26.         name: 'Dashboard',
  27.         meta: { title: 'Dashboard', icon: 'dashboard' }
  28.       }
  29.     ]
  30.   }
  31. ]
  32. // 动态路由,需要根据用户角色动态加载
  33. export const asyncRoutes = [
  34.   {
  35.     path: '/permission',
  36.     component: Layout,
  37.     redirect: '/permission/page',
  38.     alwaysShow: true, // will always show the root menu
  39.     name: 'Permission',
  40.     meta: {
  41.       title: 'Permission',
  42.       icon: 'lock',
  43.       roles: ['admin', 'editor'] // you can set roles in root nav
  44.     },
  45.     children: [
  46.       {
  47.         path: 'page',
  48.         component: () => import('@/views/permission/page'),
  49.         name: 'PagePermission',
  50.         meta: {
  51.           title: 'Page Permission',
  52.           roles: ['admin'] // or you can only set roles in sub nav
  53.         }
  54.       },
  55.       {
  56.         path: 'directive',
  57.         component: () => import('@/views/permission/directive'),
  58.         name: 'DirectivePermission',
  59.         meta: {
  60.           title: 'Directive Permission'
  61.           // if do not set roles, means: this page does not require permission
  62.         }
  63.       },
  64.       {
  65.         path: 'role',
  66.         component: () => import('@/views/permission/role'),
  67.         name: 'RolePermission',
  68.         meta: {
  69.           title: 'Role Permission',
  70.           roles: ['admin']
  71.         }
  72.       }
  73.     ]
  74.   },
  75.   // 404 page must be placed at the end !!!
  76.   { path: '*', redirect: '/404', hidden: true }
  77. ]
  78. const createRouter = () => new VueRouter({
  79.   // mode: 'history', // require service support
  80.   scrollBehavior: () => ({ y: 0 }),
  81.   routes: constantRoutes
  82. })
  83. const router = createRouter()
  84. // 重置路由
  85. export function resetRouter() {
  86.   const newRouter = createRouter()
  87.   router.matcher = newRouter.matcher // reset router
  88. }
  89. export default router
复制代码

权限控制逻辑:
  1. // permission.js
  2. import router from './router'
  3. import store from './store'
  4. import { Message } from 'element-ui'
  5. import NProgress from 'nprogress' // progress bar
  6. import 'nprogress/nprogress.css' // progress bar style
  7. import { getToken } from '@/utils/auth' // get token from cookie
  8. import getPageTitle from '@/utils/get-page-title'
  9. NProgress.configure({ showSpinner: false }) // NProgress Configuration
  10. const whiteList = ['/login', '/auth-redirect'] // no redirect whitelist
  11. router.beforeEach(async(to, from, next) => {
  12.   // start progress bar
  13.   NProgress.start()
  14.   // set page title
  15.   document.title = getPageTitle(to.meta.title)
  16.   // determine whether the user has logged in
  17.   const hasToken = getToken()
  18.   if (hasToken) {
  19.     if (to.path === '/login') {
  20.       // if is logged in, redirect to the home page
  21.       next({ path: '/' })
  22.       NProgress.done() // hack: https://github.com/PanJiaChen/vue-element-admin/pull/2939
  23.     } else {
  24.       // determine whether the user has obtained his permission roles through getInfo
  25.       const hasRoles = store.getters.roles && store.getters.roles.length > 0
  26.       if (hasRoles) {
  27.         next()
  28.       } else {
  29.         try {
  30.           // get user info
  31.           // note: roles must be a object array! such as: ['admin'] or ,['developer','editor']
  32.           const { roles } = await store.dispatch('user/getInfo')
  33.           // generate accessible routes map based on roles
  34.           const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
  35.           // dynamically add accessible routes
  36.           router.addRoutes(accessRoutes)
  37.           // hack method to ensure that addRoutes is complete
  38.           // set the replace: true so the navigation will not leave a history record
  39.           next({ ...to, replace: true })
  40.         } catch (error) {
  41.           // remove token and go to login page to re-login
  42.           await store.dispatch('user/resetToken')
  43.           Message.error(error || 'Has Error')
  44.           next(`/login?redirect=${to.path}`)
  45.           NProgress.done()
  46.         }
  47.       }
  48.     }
  49.   } else {
  50.     /* has no token*/
  51.     if (whiteList.indexOf(to.path) !== -1) {
  52.       // in the free login whitelist, go directly
  53.       next()
  54.     } else {
  55.       // other pages that do not have permission to access are redirected to the login page.
  56.       next(`/login?redirect=${to.path}`)
  57.       NProgress.done()
  58.     }
  59.   }
  60. })
  61. router.afterEach(() => {
  62.   // finish progress bar
  63.   NProgress.done()
  64. })
复制代码

按钮级权限控制:
  1. // 注册全局指令
  2. import Vue from 'vue'
  3. import store from '@/store'
  4. Vue.directive('permission', {
  5.   inserted(el, binding) {
  6.     const { value } = binding
  7.     const roles = store.getters && store.getters.roles
  8.     if (value && value instanceof Array && value.length > 0) {
  9.       const permissionRoles = value
  10.       const hasPermission = roles.some(role => {
  11.         return permissionRoles.includes(role)
  12.       })
  13.       if (!hasPermission) {
  14.         el.parentNode && el.parentNode.removeChild(el)
  15.       }
  16.     } else {
  17.       throw new Error(`need roles! Like v-permission="['admin','editor']"`)
  18.     }
  19.   }
  20. })
  21. // 在组件中使用
  22. <template>
  23.   <div>
  24.     <!-- admin 可以看到这个按钮 -->
  25.     <el-button v-permission="['admin']">Admin Button</el-button>
  26.    
  27.     <!-- admin 和 editor 都可以看到这个按钮 -->
  28.     <el-button v-permission="['admin','editor']">Editor Button</el-button>
  29.   </div>
  30. </template>
复制代码

性能优化

路由懒加载:
  1. const Foo = () => import('./Foo.vue')
  2. // 或者
  3. const Foo = resolve => require(['./Foo.vue'], resolve)
  4. // 分组代码块,将同一个路由下的所有组件都打包在同个异步块 (chunk) 中
  5. const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
  6. const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
  7. const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')
复制代码

组件按需加载:
  1. components: {
  2.   'my-component': () => import('./myComponent.vue')
  3. }
复制代码

使用keep-alive缓存组件:
  1. <keep-alive>
  2.   <component :is="currentComponent"></component>
  3. </keep-alive>
  4. <!-- 或者结合路由 -->
  5. <keep-alive>
  6.   <router-view v-if="$route.meta.keepAlive"></router-view>
  7. </keep-alive>
  8. <router-view v-if="!$route.meta.keepAlive"></router-view>
复制代码

减少watch的使用:
  1. // 不推荐
  2. watch: {
  3.   someObject: {
  4.     handler: function (newVal, oldVal) {
  5.       // 当someObject发生变化时,做一些事情
  6.     },
  7.     deep: true // 深度监听,会监听对象内部属性的变化,性能开销大
  8.   }
  9. }
  10. // 推荐,只监听需要的属性
  11. watch: {
  12.   'someObject.prop': function (newVal, oldVal) {
  13.     // 当someObject.prop发生变化时,做一些事情
  14.   }
  15. }
复制代码

使用计算属性代替methods:
  1. // 不推荐
  2. <template>
  3.   <div>{{ getName() }}</div>
  4. </template>
  5. methods: {
  6.   getName() {
  7.     return this.firstName + ' ' + this.lastName
  8.   }
  9. }
  10. // 推荐
  11. <template>
  12.   <div>{{ fullName }}</div>
  13. </template>
  14. computed: {
  15.   fullName() {
  16.     return this.firstName + ' ' + this.lastName
  17.   }
  18. }
复制代码

虚拟滚动:
  1. <!-- 使用虚拟滚动处理长列表 -->
  2. <template>
  3.   <div class="virtual-scroll-container" @scroll="handleScroll">
  4.     <div class="scroll-content" :style="{ height: totalHeight + 'px' }">
  5.       <div
  6.         v-for="item in visibleData"
  7.         :key="item.id"
  8.         class="scroll-item"
  9.         :style="{ transform: `translateY(${item.offset}px)` }"
  10.       >
  11.         {{ item.content }}
  12.       </div>
  13.     </div>
  14.   </div>
  15. </template>
  16. <script>
  17. export default {
  18.   data() {
  19.     return {
  20.       allData: [], // 所有数据
  21.       itemHeight: 50, // 每项高度
  22.       visibleCount: 20, // 可见项数量
  23.       startIndex: 0, // 起始索引
  24.       offsetTop: 0 // 滚动条偏移量
  25.     }
  26.   },
  27.   computed: {
  28.     totalHeight() {
  29.       return this.allData.length * this.itemHeight
  30.     },
  31.     visibleData() {
  32.       return this.allData.slice(
  33.         this.startIndex,
  34.         this.startIndex + this.visibleCount
  35.       ).map((item, index) => {
  36.         return {
  37.           ...item,
  38.           offset: this.startIndex * this.itemHeight + index * this.itemHeight
  39.         }
  40.       })
  41.     }
  42.   },
  43.   methods: {
  44.     handleScroll(e) {
  45.       const scrollTop = e.target.scrollTop
  46.       this.startIndex = Math.floor(scrollTop / this.itemHeight)
  47.       this.offsetTop = scrollTop - (scrollTop % this.itemHeight)
  48.     }
  49.   },
  50.   created() {
  51.     // 模拟获取大量数据
  52.     this.allData = Array.from({ length: 10000 }, (_, index) => ({
  53.       id: index,
  54.       content: `Item ${index}`
  55.     }))
  56.   }
  57. }
  58. </script>
  59. <style>
  60. .virtual-scroll-container {
  61.   height: 1000px;
  62.   overflow-y: auto;
  63.   border: 1px solid #ddd;
  64. }
  65. .scroll-content {
  66.   position: relative;
  67. }
  68. .scroll-item {
  69.   position: absolute;
  70.   width: 100%;
  71.   height: 50px;
  72.   line-height: 50px;
  73.   box-sizing: border-box;
  74.   border-bottom: 1px solid #eee;
  75.   padding-left: 10px;
  76. }
  77. </style>
复制代码

部署与上线

项目打包
  1. # 生产环境打包
  2. npm run build
  3. # 打包结果会生成在 dist 目录下
复制代码

vue.config.js配置:
  1. const path = require('path')
  2. const CompressionPlugin = require('compression-webpack-plugin')
  3. function resolve(dir) {
  4.   return path.join(__dirname, dir)
  5. }
  6. module.exports = {
  7.   // 部署应用包时的基本 URL
  8.   publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
  9.   
  10.   // 输出文件目录
  11.   outputDir: 'dist',
  12.   
  13.   // 放置生成的静态资源 (js、css、img、fonts) 的 (相对于 outputDir 的) 目录。
  14.   assetsDir: 'static',
  15.   
  16.   // 是否在保存的时候使用 `eslint-loader` 进行检查。
  17.   lintOnSave: process.env.NODE_ENV === 'development',
  18.   
  19.   // 生产环境是否生成 sourceMap 文件
  20.   productionSourceMap: false,
  21.   
  22.   // 配置webpack
  23.   configureWebpack: {
  24.     resolve: {
  25.       alias: {
  26.         '@': resolve('src')
  27.       }
  28.     },
  29.     // 生产环境配置
  30.     plugins: [
  31.       new CompressionPlugin({
  32.         algorithm: 'gzip',
  33.         test: /\.(js|css|html)$/,
  34.         threshold: 10240,
  35.         minRatio: 0.8
  36.       })
  37.     ]
  38.   },
  39.   
  40.   // 链式配置
  41.   chainWebpack(config) {
  42.     // 设置 svg-sprite-loader
  43.     config.module
  44.       .rule('svg')
  45.       .exclude.add(resolve('src/icons'))
  46.       .end()
  47.     config.module
  48.       .rule('icons')
  49.       .test(/\.svg$/)
  50.       .include.add(resolve('src/icons'))
  51.       .end()
  52.       .use('svg-sprite-loader')
  53.       .loader('svg-sprite-loader')
  54.       .options({
  55.         symbolId: 'icon-[name]'
  56.       })
  57.       .end()
  58.     // 设置保留空白
  59.     config.module
  60.       .rule('vue')
  61.       .use('vue-loader')
  62.       .loader('vue-loader')
  63.       .tap(options => {
  64.         options.compilerOptions.preserveWhitespace = true
  65.         return options
  66.       })
  67.       .end()
  68.     // 设置 runtimeCompiler
  69.     config.resolve.alias.set('vue$', 'vue/dist/vue.esm.js')
  70.     // 生产环境配置
  71.     if (process.env.NODE_ENV === 'production') {
  72.       // 移除 prefetch 插件
  73.       config.plugins.delete('prefetch')
  74.       
  75.       // 压缩代码
  76.       config.optimization.minimize(true)
  77.       
  78.       // 分割代码
  79.       config.optimization.splitChunks({
  80.         chunks: 'all',
  81.         cacheGroups: {
  82.           libs: {
  83.             name: 'chunk-libs',
  84.             test: /[\\/]node_modules[\\/]/,
  85.             priority: 10,
  86.             chunks: 'initial' // 只打包初始时依赖的第三方
  87.           },
  88.           elementUI: {
  89.             name: 'chunk-elementUI', // 单独将 elementUI 拆包
  90.             priority: 20, // 权重要大于 libs 和 app
  91.             test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
  92.           },
  93.           commons: {
  94.             name: 'chunk-commons',
  95.             test: resolve('src/components'), // 可自定义拓展你的规则
  96.             minChunks: 3, // 最小公用次数
  97.             priority: 5,
  98.             reuseExistingChunk: true
  99.           }
  100.         }
  101.       })
  102.       
  103.       // https://webpack.js.org/configuration/optimization/#optimizationruntimechunk
  104.       config.optimization.runtimeChunk('single')
  105.     }
  106.   },
  107.   
  108.   // 开发服务器配置
  109.   devServer: {
  110.     port: 8080,
  111.     open: true,
  112.     overlay: {
  113.       warnings: false,
  114.       errors: true
  115.     },
  116.     proxy: {
  117.       // 代理配置
  118.       '/api': {
  119.         target: 'http://localhost:3000',
  120.         changeOrigin: true,
  121.         pathRewrite: {
  122.           '^/api': ''
  123.         }
  124.       }
  125.     }
  126.   }
  127. }
复制代码

部署到静态服务器

Nginx配置:
  1. server {
  2.     listen 80;
  3.     server_name yourdomain.com;
  4.     root /usr/share/nginx/html; # 网站根目录
  5.     index index.html; # 默认首页
  6.     # 处理单页应用路由问题
  7.     location / {
  8.         try_files $uri $uri/ /index.html;
  9.     }
  10.     # 静态资源缓存
  11.     location ~* \.(?:css|js)$ {
  12.         try_files $uri =404;
  13.         expires 1y;
  14.         add_header Cache-Control "public";
  15.     }
  16.     # 图片等媒体资源缓存
  17.     location ~* \.(jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ {
  18.         try_files $uri =404;
  19.         expires 1M;
  20.         access_log off;
  21.         add_header Cache-Control "public";
  22.     }
  23.     # 开启gzip压缩
  24.     gzip on;
  25.     gzip_min_length 1k;
  26.     gzip_comp_level 6;
  27.     gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml;
  28.     gzip_vary on;
  29.     gzip_disable "MSIE [1-6]\.";
  30.     # 错误页面
  31.     error_page 404 /index.html;
  32. }
复制代码

Docker部署:
  1. # 构建阶段
  2. FROM node:14 as build-stage
  3. WORKDIR /app
  4. COPY package*.json ./
  5. RUN npm install
  6. COPY . .
  7. RUN npm run build
  8. # 生产阶段
  9. FROM nginx:stable-alpine as production-stage
  10. COPY --from=build-stage /app/dist /usr/share/nginx/html
  11. EXPOSE 80
  12. CMD ["nginx", "-g", "daemon off;"]
复制代码

Docker Compose:
  1. version: '3'
  2. services:
  3.   app:
  4.     build: .
  5.     ports:
  6.       - "80:80"
  7.     volumes:
  8.       - ./nginx.conf:/etc/nginx/conf.d/default.conf
  9.     restart: always
复制代码

SEO优化

预渲染:
  1. // 安装prerender-spa-plugin
  2. npm install prerender-spa-plugin --save-dev
  3. // vue.config.js配置
  4. const PrerenderSPAPlugin = require('prerender-spa-plugin')
  5. const Renderer = PrerenderSPAPlugin.PuppeteerRenderer
  6. const path = require('path')
  7. module.exports = {
  8.   configureWebpack: {
  9.     plugins: [
  10.       new PrerenderSPAPlugin({
  11.         // 生成文件的路径,也可以与webpakc打包的一致。
  12.         // 下面这句话非常重要!!!
  13.         // 这个目录只能有一级,如果目录层次大于一级,在生成的时候不会有任何错误提示,在预渲染的时候只会卡着不动。
  14.         staticDir: path.join(__dirname, 'dist'),
  15.         // 对应自己的路由文件,比如a有参数,就需要写成 /a/param1。
  16.         routes: ['/', '/about', '/contact'],
  17.         // 这个很重要,如果没有配置这段,也不会进行预编译
  18.         renderer: new Renderer({
  19.           injectProperty: '__PRERENDER_INJECTED',
  20.           inject: {
  21.             foo: 'bar'
  22.           },
  23.           // 在 app.vue 文件中,document.dispatchEvent(new Event('custom-render-trigger')),
  24.           // 这里的事件名要和下面的一致
  25.           renderAfterDocumentEvent: 'custom-render-trigger',
  26.           // 或者等待指定时间后渲染
  27.           renderAfterTime: 5000
  28.         })
  29.       })
  30.     ]
  31.   }
  32. }
复制代码

Meta标签管理:
  1. // 安装vue-meta
  2. npm install vue-meta --save
  3. // main.js
  4. import VueMeta from 'vue-meta'
  5. Vue.use(VueMeta, {
  6.   // optional pluginOptions
  7.   refreshOnceOnNavigation: true
  8. })
  9. // 在组件中使用
  10. export default {
  11.   metaInfo() {
  12.     return {
  13.       title: this.pageTitle,
  14.       meta: [
  15.         { name: 'description', content: this.pageDescription },
  16.         { property: 'og:title', content: this.pageTitle },
  17.         { property: 'og:description', content: this.pageDescription },
  18.         { property: 'og:type', content: 'website' },
  19.         { property: 'og:url', content: this.pageUrl },
  20.         { name: 'twitter:card', content: 'summary_large_image' },
  21.         { name: 'twitter:title', content: this.pageTitle },
  22.         { name: 'twitter:description', content: this.pageDescription },
  23.         { name: 'twitter:image', content: this.pageImage }
  24.       ]
  25.     }
  26.   }
  27. }
复制代码

结构化数据:
  1. <!-- 在组件中添加结构化数据 -->
  2. <template>
  3.   <div>
  4.     <!-- 页面内容 -->
  5.    
  6.     <!-- 结构化数据 -->
  7.     <script type="application/ld+json" v-html="structuredData"></script>
  8.   </div>
  9. </template>
  10. <script>
  11. export default {
  12.   computed: {
  13.     structuredData() {
  14.       return JSON.stringify({
  15.         "@context": "https://schema.org",
  16.         "@type": "Article",
  17.         "headline": this.article.title,
  18.         "image": [
  19.           this.article.imageUrl
  20.         ],
  21.         "datePublished": this.article.publishDate,
  22.         "dateModified": this.article.updateDate,
  23.         "author": {
  24.           "@type": "Person",
  25.           "name": this.article.author
  26.         },
  27.         "publisher": {
  28.           "@type": "Organization",
  29.           "name": "Your Organization",
  30.           "logo": {
  31.             "@type": "ImageObject",
  32.             "url": "https://www.your-organization.com/logo.jpg"
  33.           }
  34.         },
  35.         "description": this.article.description
  36.       })
  37.     }
  38.   }
  39. }
  40. </script>
复制代码

总结与展望

Vue2作为一个成熟的前端框架,提供了完整的解决方案来构建现代化的单页应用。通过本文的学习,我们从Vue2的基础知识开始,逐步深入到组件化开发、路由管理、状态管理等核心概念,再到进阶技巧和项目实战,最后介绍了部署与SEO优化。

Vue2的核心优势在于其简洁的API设计、灵活的组件系统和强大的生态系统,使得开发者能够快速构建高性能、可维护的前端应用。同时,Vue2的渐进式特性也使得它可以轻松地与其他项目或库集成。

随着Vue3的发布,Vue生态系统正在不断演进。Vue3带来了更好的性能、更小的包体积以及更好的TypeScript支持。对于Vue2开发者来说,学习Vue3是一个自然的过程,因为许多核心概念在两个版本中是相通的。

未来,前端开发将继续朝着组件化、模块化和工程化的方向发展。Vue作为一个活跃的开源项目,也将继续演进,为开发者提供更好的工具和体验。无论技术如何变化,掌握核心概念和最佳实践,将帮助开发者更好地适应未来的变化。

希望本文能够帮助读者全面掌握Vue2前端开发技术栈,并在实际项目中应用这些知识,构建出优秀的Web应用。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

频道订阅

频道订阅

加入社群

加入社群

联系我们|TG频道|RSS

Powered by Pixtech

© 2025 Pixtech Team.