|
|
马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
引言
Vue.js是一款用于构建用户界面的渐进式JavaScript框架,由尤雨溪创建和维护。Vue2作为其重要版本,凭借其简洁的API、灵活的组件系统和强大的生态系统,在前端开发领域占据了重要地位。本文将从基础到进阶,全面介绍Vue2纯前端项目开发的核心知识和技术栈,帮助读者系统掌握Vue2开发技能。
Vue2基础知识
Vue2的安装与配置
Vue2可以通过多种方式安装和使用,最常见的是通过CDN引入或使用npm安装。
CDN引入方式:
- <!-- 开发环境版本,包含了有帮助的命令行警告 -->
- <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
- <!-- 生产环境版本,优化了尺寸和速度 -->
- <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js"></script>
复制代码
npm安装方式:
- # 最新稳定版
- npm install vue@2.6.14
复制代码
Vue CLI脚手架安装:
- # 全局安装Vue CLI
- npm install -g @vue/cli
- # 创建一个新项目
- vue create my-project
- # 进入项目目录
- cd my-project
- # 运行项目
- npm run serve
复制代码
Vue实例与生命周期
每个Vue应用都是通过创建一个Vue实例开始的:
- var vm = new Vue({
- // 选项
- })
复制代码
Vue实例拥有一系列的生命周期钩子函数,这些函数允许我们在实例的不同阶段添加自己的代码:
- new Vue({
- el: '#app',
- data: {
- message: 'Hello Vue!'
- },
- beforeCreate: function() {
- console.log('beforeCreate: 实例初始化之后,数据观测和事件配置之前调用')
- },
- created: function() {
- console.log('created: 实例创建完成之后调用')
- },
- beforeMount: function() {
- console.log('beforeMount: 实例挂载之前调用')
- },
- mounted: function() {
- console.log('mounted: 实例挂载完成后调用')
- console.log(this.$el) // 访问DOM元素
- },
- beforeUpdate: function() {
- console.log('beforeUpdate: 数据更新时调用,发生在虚拟DOM重新渲染和打补丁之前')
- },
- updated: function() {
- console.log('updated: 数据更改导致虚拟DOM重新渲染和打补丁之后调用')
- },
- beforeDestroy: function() {
- console.log('beforeDestroy: 实例销毁之前调用')
- },
- destroyed: function() {
- console.log('destroyed: 实例销毁后调用')
- }
- })
复制代码
模板语法
Vue.js使用了基于HTML的模板语法,允许开发者声明式地将DOM绑定至底层Vue实例的数据。
文本插值:
- <span>Message: {{ msg }}</span>
复制代码
一次性插值:
- <span v-once>这个将不会改变: {{ msg }}</span>
复制代码
原始HTML:
- <p>Using mustaches: {{ rawHtml }}</p>
- <p>Using v-html directive: <span v-html="rawHtml"></span></p>
复制代码
特性:
- <div v-bind:id="dynamicId"></div>
复制代码
使用JavaScript表达式:
- {{ number + 1 }}
- {{ ok ? 'YES' : 'NO' }}
- {{ message.split('').reverse().join('') }}
- <div v-bind:id="'list-' + id"></div>
复制代码
计算属性与侦听器
计算属性:
- var vm = new Vue({
- el: '#example',
- data: {
- message: 'Hello'
- },
- computed: {
- // 计算属性的 getter
- reversedMessage: function () {
- // `this` 指向 vm 实例
- return this.message.split('').reverse().join('')
- },
- // 计算属性的 setter
- fullName: {
- // getter
- get: function () {
- return this.firstName + ' ' + this.lastName
- },
- // setter
- set: function (newValue) {
- var names = newValue.split(' ')
- this.firstName = names[0]
- this.lastName = names[names.length - 1]
- }
- }
- }
- })
复制代码
侦听器:
- var vm = new Vue({
- data: {
- question: '',
- answer: 'I cannot give you an answer until you ask a question!'
- },
- watch: {
- // 如果 `question` 发生改变,这个函数就会运行
- question: function (newQuestion, oldQuestion) {
- this.answer = 'Waiting for you to stop typing...'
- this.debouncedGetAnswer()
- }
- },
- created: function () {
- // `_.debounce` 是一个通过 Lodash 限制操作频率的函数。
- // 在这个例子中,我们希望限制访问 yesno.wtf/api 的频率
- // AJAX 请求直到用户输入完毕才会发出
- this.debouncedGetAnswer = _.debounce(this.getAnswer, 500)
- },
- methods: {
- getAnswer: function () {
- if (this.question.indexOf('?') === -1) {
- this.answer = 'Questions usually contain a question mark. ;-)'
- return
- }
- this.answer = 'Thinking...'
- var vm = this
- axios.get('https://yesno.wtf/api')
- .then(function (response) {
- vm.answer = _.capitalize(response.data.answer)
- })
- .catch(function (error) {
- vm.answer = 'Error! Could not reach the API. ' + error
- })
- }
- }
- })
复制代码
条件渲染与列表渲染
条件渲染:
- <div v-if="type === 'A'">
- A
- </div>
- <div v-else-if="type === 'B'">
- B
- </div>
- <div v-else-if="type === 'C'">
- C
- </div>
- <div v-else>
- Not A/B/C
- </div>
- <!-- v-show -->
- <h1 v-show="ok">Hello!</h1>
复制代码
列表渲染:
- <ul id="example-1">
- <li v-for="item in items" :key="item.message">
- {{ item.message }}
- </li>
- </ul>
- <!-- 带索引的 v-for -->
- <ul id="example-2">
- <li v-for="(item, index) in items">
- {{ parentMessage }} - {{ index }} - {{ item.message }}
- </li>
- </ul>
- <!-- 遍历对象 -->
- <div v-for="(value, name, index) in object">
- {{ index }}. {{ name }}: {{ value }}
- </div>
复制代码
事件处理
- <!-- 内联语句中的方法 -->
- <button v-on:click="say('hi')">Say hi</button>
- <!-- 缩写语法 -->
- <button @click="say('hi')">Say hi</button>
- <!-- 事件修饰符 -->
- <!-- 阻止单击事件继续传播 -->
- <a v-on:click.stop="doThis"></a>
- <!-- 提交事件不再重载页面 -->
- <form v-on:submit.prevent="onSubmit"></form>
- <!-- 修饰符可以串联 -->
- <a v-on:click.stop.prevent="doThat"></a>
- <!-- 按键修饰符 -->
- <!-- 只有在 `keyCode` 是 13 时调用 vm.submit() -->
- <input v-on:keyup.13="submit">
- <!-- 缩写语法 -->
- <input @keyup.enter="submit">
复制代码
表单输入绑定
- <!-- 文本 -->
- <input v-model="message" placeholder="edit me">
- <!-- 多行文本 -->
- <textarea v-model="message" placeholder="add multiple lines"></textarea>
- <!-- 复选框 -->
- <input type="checkbox" id="checkbox" v-model="checked">
- <label for="checkbox">{{ checked }}</label>
- <!-- 多个复选框,绑定到同一个数组 -->
- <div id='example-3'>
- <input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
- <label for="jack">Jack</label>
- <input type="checkbox" id="john" value="John" v-model="checkedNames">
- <label for="john">John</label>
- <input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
- <label for="mike">Mike</label>
- <br>
- <span>Checked names: {{ checkedNames }}</span>
- </div>
- <!-- 单选按钮 -->
- <div id="example-4">
- <input type="radio" id="one" value="One" v-model="picked">
- <label for="one">One</label>
- <br>
- <input type="radio" id="two" value="Two" v-model="picked">
- <label for="two">Two</label>
- <br>
- <span>Picked: {{ picked }}</span>
- </div>
- <!-- 选择框 -->
- <div id="example-5">
- <select v-model="selected">
- <option disabled value="">请选择</option>
- <option>A</option>
- <option>B</option>
- <option>C</option>
- </select>
- <span>Selected: {{ selected }}</span>
- </div>
- <!-- 多选时绑定到一个数组 -->
- <div id="example-6">
- <select v-model="selected" multiple style="width: 50px;">
- <option>A</option>
- <option>B</option>
- <option>C</option>
- </select>
- <br>
- <span>Selected: {{ selected }}</span>
- </div>
复制代码
Vue2组件化开发
组件基础
组件是Vue.js最强大的功能之一。组件可以扩展HTML元素,封装可重用的代码。
全局注册:
- Vue.component('my-component', {
- template: '<div>A custom component!</div>'
- })
复制代码
局部注册:
- var ComponentA = {
- /* ... */
- }
- var ComponentB = {
- /* ... */
- }
- new Vue({
- el: '#app',
- components: {
- 'component-a': ComponentA,
- 'component-b': ComponentB
- }
- })
复制代码
组件的data选项:
- // 这样是错误的
- Vue.component('my-component', {
- template: '<span>{{ message }}</span>',
- data: {
- message: 'hello'
- }
- })
- // 正确做法
- Vue.component('my-component', {
- template: '<span>{{ message }}</span>',
- data: function () {
- return {
- message: 'hello'
- }
- }
- })
复制代码
组件通信
Props向下传递数据:
- Vue.component('child', {
- // 声明 props
- props: ['message'],
- // 就像 data 一样,prop 也可以在模板中使用
- // 同样也可以在 vm 实例中通过 this.message 来使用
- template: '<span>{{ message }}</span>'
- })
- // 使用
- <child message="hello!"></child>
复制代码
自定义事件向上传递:
- Vue.component('button-counter', {
- template: '<button v-on:click="incrementCounter">{{ counter }}</button>',
- data: function () {
- return {
- counter: 0
- }
- },
- methods: {
- incrementCounter: function () {
- this.counter += 1
- this.$emit('increment')
- }
- },
- })
- // 使用
- <div id="counter-event-example">
- <p>{{ total }}</p>
- <button-counter v-on:increment="incrementTotal"></button-counter>
- <button-counter v-on:increment="incrementTotal"></button-counter>
- </div>
复制代码
使用v-model进行双向绑定:
- Vue.component('my-input', {
- props: ['value'],
- template: `
- <input
- :value="value"
- @input="$emit('input', $event.target.value)"
- >
- `
- })
- // 使用
- <my-input v-model="message"></my-input>
复制代码
非父子组件通信:
- // 简单的状态管理模式
- var store = {
- debug: true,
- state: {
- message: 'Hello!'
- },
- setMessageAction (newValue) {
- if (this.debug) console.log('setMessageAction triggered with', newValue)
- this.state.message = newValue
- },
- clearMessageAction () {
- if (this.debug) console.log('clearMessageAction triggered')
- this.state.message = ''
- }
- }
- // 组件A
- var vmA = new Vue({
- data: {
- privateState: {},
- sharedState: store.state
- }
- })
- // 组件B
- var vmB = new Vue({
- data: {
- privateState: {},
- sharedState: store.state
- }
- })
复制代码
插槽
插槽内容:
- <!-- 父组件模板 -->
- <child-component>
- <p>这是一些初始内容</p>
- <p>这是更多的初始内容</p>
- </child-component>
- <!-- 子组件模板 -->
- <div class="child">
- <slot>
- 只有在没有要分发的内容时才会显示。
- </slot>
- </div>
复制代码
具名插槽:
- <!-- 父组件模板 -->
- <base-layout>
- <template v-slot:header>
- <h1>Here might be a page title</h1>
- </template>
- <template v-slot:default>
- <p>A paragraph for the main content.</p>
- <p>And another one.</p>
- </template>
- <template v-slot:footer>
- <p>Here's some contact info</p>
- </template>
- </base-layout>
- <!-- 子组件模板 -->
- <div class="container">
- <header>
- <slot name="header"></slot>
- </header>
- <main>
- <slot></slot>
- </main>
- <footer>
- <slot name="footer"></slot>
- </footer>
- </div>
复制代码
作用域插槽:
- <!-- 父组件模板 -->
- <child>
- <template v-slot:default="slotProps">
- {{ slotProps.user.firstName }}
- </template>
- </child>
- <!-- 子组件模板 -->
- <span>
- <slot v-bind:user="user">
- {{ user.lastName }}
- </slot>
- </span>
复制代码
动态组件与异步组件
动态组件:
- <!-- 组件会在 `currentTabComponent` 改变时改变 -->
- <component v-bind:is="currentTabComponent"></component>
复制代码
异步组件:
- Vue.component('async-webpack-example', function (resolve, reject) {
- // 这个特殊的 require 语法将会告诉 webpack
- // 自动将你的构建代码切割成多个包,这些包
- // 会通过 Ajax 请求加载
- require(['./my-async-component'], resolve)
- })
- // 或者
- Vue.component('async-webpack-example', () => import('./my-async-component'))
- // 高级异步组件
- const AsyncComp = () => ({
- // 需要加载的组件 (应当是一个 Promise 对象)
- component: import('./MyComp.vue'),
- // 加载中应当渲染的组件
- loading: LoadingComp,
- // 出错时渲染的组件
- error: ErrorComp,
- // 渲染加载中组件前的等待时间。默认:200ms。
- delay: 200,
- // 最长等待时间。超出此时间则渲染错误组件。默认:Infinity
- timeout: 3000
- })
复制代码
Vue Router路由管理
路由基础
安装:
基本使用:
- // 1. 定义 (路由) 组件。
- // 可以从其他文件 import 进来
- const Foo = { template: '<div>foo</div>' }
- const Bar = { template: '<div>bar</div>' }
- // 2. 定义路由
- // 每个路由应该映射一个组件。 其中"component" 可以是
- // 通过 Vue.extend() 创建的组件构造器,
- // 或者,只是一个组件配置对象。
- const routes = [
- { path: '/foo', component: Foo },
- { path: '/bar', component: Bar }
- ]
- // 3. 创建 router 实例,然后传 `routes` 配置
- const router = new VueRouter({
- routes // (缩写) 相当于 routes: routes
- })
- // 4. 创建和挂载根实例。
- // 记得要通过 router 配置参数注入路由,
- // 从而让整个应用都有路由功能
- const app = new Vue({
- router
- }).$mount('#app')
复制代码
HTML:
- <div id="app">
- <h1>Hello App!</h1>
- <p>
- <!-- 使用 router-link 组件来导航. -->
- <!-- 通过传入 `to` 属性指定链接. -->
- <!-- <router-link> 默认会被渲染成一个 `<a>` 标签 -->
- <router-link to="/foo">Go to Foo</router-link>
- <router-link to="/bar">Go to Bar</router-link>
- </p>
- <!-- 路由出口 -->
- <!-- 路由匹配到的组件将渲染在这里 -->
- <router-view></router-view>
- </div>
复制代码
动态路由匹配
- const router = new VueRouter({
- routes: [
- // 动态路径参数 以冒号开头
- { path: '/user/:id', component: User }
- ]
- })
- // 在组件中访问
- const User = {
- template: '<div>User {{ $route.params.id }}</div>'
- }
- // 响应路由参数的变化
- const User = {
- template: '...',
- watch: {
- '$route' (to, from) {
- // 对路由变化作出响应...
- }
- }
- }
- // 或者
- const User = {
- template: '...',
- beforeRouteUpdate (to, from, next) {
- // react to route changes...
- // don't forget to call next()
- }
- }
复制代码
嵌套路由
- const router = new VueRouter({
- routes: [
- { path: '/user/:id', component: User,
- children: [
- {
- // 当 /user/:id/profile 匹配成功,
- // UserProfile 会被渲染在 User 的 <router-view> 中
- path: 'profile',
- component: UserProfile
- },
- {
- // 当 /user/:id/posts 匹配成功
- // UserPosts 会被渲染在 User 的 <router-view> 中
- path: 'posts',
- component: UserPosts
- }
- ]
- }
- ]
- })
复制代码
编程式导航
- // 字符串
- router.push('home')
- // 对象
- router.push({ path: 'home' })
- // 命名的路由
- router.push({ name: 'user', params: { userId: '123' }})
- // 带查询参数,变成 /register?plan=private
- router.push({ path: 'register', query: { plan: 'private' }})
- // 替换当前位置,不会向 history 添加新记录
- router.replace(...)
- // 在浏览器记录中前进一步,等同于 history.forward()
- router.go(1)
- // 后退一步记录,等同于 history.back()
- router.go(-1)
- // 前进 3 步记录
- router.go(3)
- // 如果 history 记录不够用,那就默默地失败呗
- router.go(-100)
- router.go(100)
复制代码
路由守卫
全局守卫:
- const router = new VueRouter({ ... })
- router.beforeEach((to, from, next) => {
- // ...
- })
- router.beforeResolve((to, from, next) => {
- // ...
- })
- router.afterEach((to, from) => {
- // ...
- })
复制代码
路由独享的守卫:
- const router = new VueRouter({
- routes: [
- {
- path: '/foo',
- component: Foo,
- beforeEnter: (to, from, next) => {
- // ...
- }
- }
- ]
- })
复制代码
组件内的守卫:
- const Foo = {
- template: `...`,
- beforeRouteEnter (to, from, next) {
- // 在渲染该组件的对应路由被 confirm 前调用
- // 不!能!获取组件实例 `this`
- // 因为当守卫执行前,组件实例还没被创建
- },
- beforeRouteUpdate (to, from, next) {
- // 在当前路由改变,但是该组件被复用时调用
- // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
- // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
- // 可以访问组件实例 `this`
- },
- beforeRouteLeave (to, from, next) {
- // 导航离开该组件的对应路由时调用
- // 可以访问组件实例 `this`
- }
- }
复制代码
Vuex状态管理
Vuex核心概念
安装:
基本使用:
- import Vue from 'vue'
- import Vuex from 'vuex'
- Vue.use(Vuex)
- const store = new Vuex.Store({
- state: {
- count: 0
- },
- mutations: {
- increment (state) {
- state.count++
- }
- }
- })
- // 在组件中使用
- this.$store.commit('increment')
- console.log(this.$store.state.count) // -> 1
复制代码
State、Getter、Mutation、Action
State:
- // 创建一个 Counter 组件
- const Counter = {
- template: `<div>{{ count }}</div>`,
- computed: {
- count () {
- return this.$store.state.count
- }
- }
- }
- // 或者使用 mapState 辅助函数
- import { mapState } from 'vuex'
- export default {
- // ...
- computed: mapState({
- // 箭头函数可使代码更简练
- count: state => state.count,
- // 传字符串参数 'count' 等同于 `state => state.count`
- countAlias: 'count',
- // 为了能够使用 `this` 获取局部状态,必须使用常规函数
- countPlusLocalState (state) {
- return state.count + this.localCount
- }
- })
- }
复制代码
Getter:
- const store = new Vuex.Store({
- state: {
- todos: [
- { id: 1, text: '...', done: true },
- { id: 2, text: '...', done: false }
- ]
- },
- getters: {
- doneTodos: state => {
- return state.todos.filter(todo => todo.done)
- }
- }
- })
- // 访问
- store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
- // 使用 mapGetters 辅助函数
- import { mapGetters } from 'vuex'
- export default {
- // ...
- computed: {
- // 使用对象展开运算符将 getter 混入 computed 对象中
- ...mapGetters([
- 'doneTodosCount',
- 'anotherGetter',
- // ...
- ])
- }
- }
复制代码
Mutation:
- const store = new Vuex.Store({
- state: {
- count: 1
- },
- mutations: {
- increment (state) {
- // 变更状态
- state.count++
- }
- }
- })
- // 提交
- store.commit('increment')
- // 提交载荷(Payload)
- mutations: {
- increment (state, n) {
- state.count += n
- }
- }
- store.commit('increment', 10)
- // 对象风格的提交方式
- store.commit({
- type: 'increment',
- amount: 10
- })
- // 使用 mapMutations 辅助函数
- import { mapMutations } from 'vuex'
- export default {
- // ...
- methods: {
- ...mapMutations([
- 'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`
- // `mapMutations` 也支持载荷:
- 'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
- ]),
- ...mapMutations({
- add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
- })
- }
- }
复制代码
Action:
- const store = new Vuex.Store({
- state: {
- count: 0
- },
- mutations: {
- increment (state) {
- state.count++
- }
- },
- actions: {
- increment (context) {
- context.commit('increment')
- },
- incrementAsync ({ commit }) {
- setTimeout(() => {
- commit('increment')
- }, 1000)
- }
- }
- })
- // 分发 Action
- store.dispatch('increment')
- // 以载荷形式分发
- store.dispatch('incrementAsync', {
- amount: 10
- })
- // 以对象形式分发
- store.dispatch({
- type: 'incrementAsync',
- amount: 10
- })
- // 使用 mapActions 辅助函数
- import { mapActions } from 'vuex'
- export default {
- // ...
- methods: {
- ...mapActions([
- 'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`
- // `mapActions` 也支持载荷:
- 'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
- ]),
- ...mapActions({
- add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
- })
- }
- }
复制代码
模块化
- const moduleA = {
- state: () => ({ ... }),
- mutations: { ... },
- actions: { ... },
- getters: { ... }
- }
- const moduleB = {
- state: () => ({ ... }),
- mutations: { ... },
- actions: { ... }
- }
- const store = new Vuex.Store({
- modules: {
- a: moduleA,
- b: moduleB
- }
- })
- store.state.a // -> moduleA 的状态
- store.state.b // -> moduleB 的状态
- // 在带命名空间的模块内访问全局内容
- modules: {
- foo: {
- namespaced: true,
- getters: {
- // 在这个模块的 getter 中,`getters` 被局部化了
- // 你可以使用 getter 的第四个参数来调用 `rootGetters`
- someGetter (state, getters, rootState, rootGetters) {
- getters.someOtherGetter // -> 'foo/someOtherGetter'
- rootGetters.someOtherGetter // -> 'someOtherGetter'
- },
- someOtherGetter: state => { ... }
- },
- actions: {
- // 在这个模块中, dispatch 和 commit 也被局部化了
- // 他们可以接受 `root` 属性以访问根 dispatch 或 commit
- someAction ({ dispatch, commit, getters, rootGetters }) {
- getters.someGetter // -> 'foo/someGetter'
- rootGetters.someGetter // -> 'someGetter'
- dispatch('someOtherAction') // -> 'foo/someOtherAction'
- dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'
- commit('someMutation') // -> 'foo/someMutation'
- commit('someMutation', null, { root: true }) // -> 'someMutation'
- },
- someOtherAction (ctx, payload) { ... }
- }
- }
- }
复制代码
Vue2进阶技巧
自定义指令
注册全局指令:
- // 注册一个全局自定义指令 `v-focus`
- Vue.directive('focus', {
- // 当被绑定的元素插入到 DOM 中时……
- inserted: function (el) {
- // 聚焦元素
- el.focus()
- }
- })
复制代码
注册局部指令:
- directives: {
- focus: {
- // 指令的定义
- inserted: function (el) {
- el.focus()
- }
- }
- }
复制代码
钩子函数:
- Vue.directive('demo', {
- bind: function (el, binding, vnode) {
- var s = JSON.stringify
- el.innerHTML =
- 'name: ' + s(binding.name) + '<br>' +
- 'value: ' + s(binding.value) + '<br>' +
- 'expression: ' + s(binding.expression) + '<br>' +
- 'argument: ' + s(binding.arg) + '<br>' +
- 'modifiers: ' + s(binding.modifiers) + '<br>' +
- 'vnode keys: ' + Object.keys(vnode).join(', ')
- }
- })
复制代码
过滤器
创建过滤器:
- // 在组件的选项中定义本地过滤器
- filters: {
- capitalize: function (value) {
- if (!value) return ''
- value = value.toString()
- return value.charAt(0).toUpperCase() + value.slice(1)
- }
- }
- // 创建全局过滤器
- Vue.filter('capitalize', function (value) {
- if (!value) return ''
- value = value.toString()
- return value.charAt(0).toUpperCase() + value.slice(1)
- })
复制代码
使用过滤器:
- <!-- 在双花括号中 -->
- {{ message | capitalize }}
- <!-- 在 `v-bind` 中 -->
- <div v-bind:id="rawId | formatId"></div>
- <!-- 过滤器可以串联 -->
- {{ message | filterA | filterB }}
- <!-- 过滤器是 JavaScript 函数,因此可以接收参数 -->
- {{ message | filterA('arg1', arg2) }}
复制代码
混入
定义混入:
- // 定义一个混入对象
- var myMixin = {
- created: function () {
- this.hello()
- },
- methods: {
- hello: function () {
- console.log('hello from mixin!')
- }
- }
- }
- // 定义一个使用混入对象的组件
- var Component = Vue.extend({
- mixins: [myMixin]
- })
- var component = new Component() // -> "hello from mixin!"
复制代码
选项合并:
- var mixin = {
- data: function () {
- return {
- message: 'hello',
- foo: 'abc'
- }
- }
- }
- new Vue({
- mixins: [mixin],
- data: function () {
- return {
- message: 'goodbye',
- bar: 'def'
- }
- },
- created: function () {
- console.log(this.$data)
- // => { message: "goodbye", foo: "abc", bar: "def" }
- }
- })
复制代码
插件开发
开发插件:
- MyPlugin.install = function (Vue, options) {
- // 1. 添加全局方法或 property
- Vue.myGlobalMethod = function () {
- // 逻辑...
- }
- // 2. 添加全局资源
- Vue.directive('my-directive', {
- bind (el, binding, vnode, oldVnode) {
- // 逻辑...
- }
- ...
- })
- // 3. 注入组件选项
- Vue.mixin({
- created: function () {
- // 逻辑...
- }
- ...
- })
- // 4. 添加实例方法
- Vue.prototype.$myMethod = function (methodOptions) {
- // 逻辑...
- }
- }
复制代码
使用插件:
- // 调用 `MyPlugin.install(Vue)`
- Vue.use(MyPlugin)
- // 传入可选的选项对象
- Vue.use(MyPlugin, { someOption: true })
复制代码
项目实战
项目结构设计
一个典型的Vue2项目结构如下:
- my-project
- ├── public # 静态资源
- │ ├── favicon.ico # 网站图标
- │ └── index.html # HTML 模板
- ├── src # 源代码
- │ ├── assets # 项目资源
- │ │ ├── logo.png # Logo
- │ │ └── css # 样式文件
- │ │ ├── global.css # 全局样式
- │ │ └── variables.scss # SCSS 变量
- │ ├── components # 公共组件
- │ │ ├── HelloWorld.vue # 示例组件
- │ │ └── CommonHeader.vue # 公共头部
- │ ├── layouts # 布局组件
- │ │ └── MainLayout.vue # 主布局
- │ ├── views # 页面组件
- │ │ ├── Home.vue # 首页
- │ │ └── About.vue # 关于页面
- │ ├── router # 路由配置
- │ │ └── index.js # 路由主文件
- │ ├── store # Vuex 状态管理
- │ │ ├── index.js # Vuex 主文件
- │ │ └── modules # 模块
- │ │ ├── user.js # 用户模块
- │ │ └── app.js # 应用模块
- │ ├── utils # 工具函数
- │ │ ├── request.js # 请求封装
- │ │ └── auth.js # 权限工具
- │ ├── App.vue # 根组件
- │ └── main.js # 入口文件
- ├── .env # 环境变量
- ├── .env.development # 开发环境变量
- ├── .env.production # 生产环境变量
- ├── .gitignore # Git 忽略文件
- ├── babel.config.js # Babel 配置
- ├── package.json # 项目依赖
- ├── vue.config.js # Vue CLI 配置
- └── README.md # 项目说明
复制代码
API封装与请求拦截
request.js:
- import axios from 'axios'
- import { Message, Loading } from 'element-ui'
- import store from '@/store'
- import { getToken } from '@/utils/auth'
- // 创建axios实例
- const service = axios.create({
- baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
- timeout: 5000 // 请求超时时间
- })
- // 请求拦截器
- let loadingInstance = null
- service.interceptors.request.use(
- config => {
- // 如果是特定接口,显示loading
- if (config.showLoading) {
- loadingInstance = Loading.service({
- lock: true,
- text: 'Loading...',
- spinner: 'el-icon-loading',
- background: 'rgba(0, 0, 0, 0.7)'
- })
- }
-
- // 如果有token,添加到请求头
- if (store.getters.token) {
- config.headers['X-Token'] = getToken()
- }
- return config
- },
- error => {
- // 关闭loading
- if (loadingInstance) {
- loadingInstance.close()
- }
- console.log(error)
- return Promise.reject(error)
- }
- )
- // 响应拦截器
- service.interceptors.response.use(
- response => {
- // 关闭loading
- if (loadingInstance) {
- loadingInstance.close()
- }
-
- const res = response.data
-
- // 如果自定义code不是200,则判断为错误
- if (res.code !== 200) {
- Message({
- message: res.message || 'Error',
- type: 'error',
- duration: 5 * 1000
- })
-
- // 50008: 非法的token; 50012: 其他客户端登录; 50014: Token过期;
- if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
- // 重新登录
- MessageBox.confirm(
- '您已被登出,可以取消继续留在该页面,或者重新登录',
- '确定登出',
- {
- confirmButtonText: '重新登录',
- cancelButtonText: '取消',
- type: 'warning'
- }
- ).then(() => {
- store.dispatch('user/resetToken').then(() => {
- location.reload()
- })
- })
- }
- return Promise.reject(new Error(res.message || 'Error'))
- } else {
- return res
- }
- },
- error => {
- // 关闭loading
- if (loadingInstance) {
- loadingInstance.close()
- }
-
- console.log('err' + error)
- Message({
- message: error.message,
- type: 'error',
- duration: 5 * 1000
- })
- return Promise.reject(error)
- }
- )
- export default service
复制代码
API模块化:
- // api/user.js
- import request from '@/utils/request'
- export function login(data) {
- return request({
- url: '/user/login',
- method: 'post',
- data
- })
- }
- export function getInfo(token) {
- return request({
- url: '/user/info',
- method: 'get',
- params: { token }
- })
- }
- export function logout() {
- return request({
- url: '/user/logout',
- method: 'post'
- })
- }
- // 在组件中使用
- import { login, getInfo, logout } from '@/api/user'
- export default {
- methods: {
- handleLogin() {
- this.loading = true
- login(this.loginForm)
- .then(() => {
- this.$router.push({ path: this.redirect || '/' })
- this.loading = false
- })
- .catch(() => {
- this.loading = false
- })
- }
- }
- }
复制代码
权限控制
路由权限:
- // router/index.js
- import Vue from 'vue'
- import VueRouter from 'vue-router'
- import Layout from '@/layouts'
- Vue.use(VueRouter)
- // 基础路由
- export const constantRoutes = [
- {
- path: '/login',
- component: () => import('@/views/login/index'),
- hidden: true
- },
- {
- path: '/404',
- component: () => import('@/views/404'),
- hidden: true
- },
- {
- path: '/',
- component: Layout,
- redirect: '/dashboard',
- children: [
- {
- path: 'dashboard',
- component: () => import('@/views/dashboard/index'),
- name: 'Dashboard',
- meta: { title: 'Dashboard', icon: 'dashboard' }
- }
- ]
- }
- ]
- // 动态路由,需要根据用户角色动态加载
- export const asyncRoutes = [
- {
- path: '/permission',
- component: Layout,
- redirect: '/permission/page',
- alwaysShow: true, // will always show the root menu
- name: 'Permission',
- meta: {
- title: 'Permission',
- icon: 'lock',
- roles: ['admin', 'editor'] // you can set roles in root nav
- },
- children: [
- {
- path: 'page',
- component: () => import('@/views/permission/page'),
- name: 'PagePermission',
- meta: {
- title: 'Page Permission',
- roles: ['admin'] // or you can only set roles in sub nav
- }
- },
- {
- path: 'directive',
- component: () => import('@/views/permission/directive'),
- name: 'DirectivePermission',
- meta: {
- title: 'Directive Permission'
- // if do not set roles, means: this page does not require permission
- }
- },
- {
- path: 'role',
- component: () => import('@/views/permission/role'),
- name: 'RolePermission',
- meta: {
- title: 'Role Permission',
- roles: ['admin']
- }
- }
- ]
- },
- // 404 page must be placed at the end !!!
- { path: '*', redirect: '/404', hidden: true }
- ]
- const createRouter = () => new VueRouter({
- // mode: 'history', // require service support
- scrollBehavior: () => ({ y: 0 }),
- routes: constantRoutes
- })
- const router = createRouter()
- // 重置路由
- export function resetRouter() {
- const newRouter = createRouter()
- router.matcher = newRouter.matcher // reset router
- }
- export default router
复制代码
权限控制逻辑:
- // permission.js
- import router from './router'
- import store from './store'
- import { Message } from 'element-ui'
- import NProgress from 'nprogress' // progress bar
- import 'nprogress/nprogress.css' // progress bar style
- import { getToken } from '@/utils/auth' // get token from cookie
- import getPageTitle from '@/utils/get-page-title'
- NProgress.configure({ showSpinner: false }) // NProgress Configuration
- const whiteList = ['/login', '/auth-redirect'] // no redirect whitelist
- router.beforeEach(async(to, from, next) => {
- // start progress bar
- NProgress.start()
- // set page title
- document.title = getPageTitle(to.meta.title)
- // determine whether the user has logged in
- const hasToken = getToken()
- if (hasToken) {
- if (to.path === '/login') {
- // if is logged in, redirect to the home page
- next({ path: '/' })
- NProgress.done() // hack: https://github.com/PanJiaChen/vue-element-admin/pull/2939
- } else {
- // determine whether the user has obtained his permission roles through getInfo
- const hasRoles = store.getters.roles && store.getters.roles.length > 0
- if (hasRoles) {
- next()
- } else {
- try {
- // get user info
- // note: roles must be a object array! such as: ['admin'] or ,['developer','editor']
- const { roles } = await store.dispatch('user/getInfo')
- // generate accessible routes map based on roles
- const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
- // dynamically add accessible routes
- router.addRoutes(accessRoutes)
- // hack method to ensure that addRoutes is complete
- // set the replace: true so the navigation will not leave a history record
- next({ ...to, replace: true })
- } catch (error) {
- // remove token and go to login page to re-login
- await store.dispatch('user/resetToken')
- Message.error(error || 'Has Error')
- next(`/login?redirect=${to.path}`)
- NProgress.done()
- }
- }
- }
- } else {
- /* has no token*/
- if (whiteList.indexOf(to.path) !== -1) {
- // in the free login whitelist, go directly
- next()
- } else {
- // other pages that do not have permission to access are redirected to the login page.
- next(`/login?redirect=${to.path}`)
- NProgress.done()
- }
- }
- })
- router.afterEach(() => {
- // finish progress bar
- NProgress.done()
- })
复制代码
按钮级权限控制:
- // 注册全局指令
- import Vue from 'vue'
- import store from '@/store'
- Vue.directive('permission', {
- inserted(el, binding) {
- const { value } = binding
- const roles = store.getters && store.getters.roles
- if (value && value instanceof Array && value.length > 0) {
- const permissionRoles = value
- const hasPermission = roles.some(role => {
- return permissionRoles.includes(role)
- })
- if (!hasPermission) {
- el.parentNode && el.parentNode.removeChild(el)
- }
- } else {
- throw new Error(`need roles! Like v-permission="['admin','editor']"`)
- }
- }
- })
- // 在组件中使用
- <template>
- <div>
- <!-- admin 可以看到这个按钮 -->
- <el-button v-permission="['admin']">Admin Button</el-button>
-
- <!-- admin 和 editor 都可以看到这个按钮 -->
- <el-button v-permission="['admin','editor']">Editor Button</el-button>
- </div>
- </template>
复制代码
性能优化
路由懒加载:
- const Foo = () => import('./Foo.vue')
- // 或者
- const Foo = resolve => require(['./Foo.vue'], resolve)
- // 分组代码块,将同一个路由下的所有组件都打包在同个异步块 (chunk) 中
- const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
- const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
- const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')
复制代码
组件按需加载:
- components: {
- 'my-component': () => import('./myComponent.vue')
- }
复制代码
使用keep-alive缓存组件:
- <keep-alive>
- <component :is="currentComponent"></component>
- </keep-alive>
- <!-- 或者结合路由 -->
- <keep-alive>
- <router-view v-if="$route.meta.keepAlive"></router-view>
- </keep-alive>
- <router-view v-if="!$route.meta.keepAlive"></router-view>
复制代码
减少watch的使用:
- // 不推荐
- watch: {
- someObject: {
- handler: function (newVal, oldVal) {
- // 当someObject发生变化时,做一些事情
- },
- deep: true // 深度监听,会监听对象内部属性的变化,性能开销大
- }
- }
- // 推荐,只监听需要的属性
- watch: {
- 'someObject.prop': function (newVal, oldVal) {
- // 当someObject.prop发生变化时,做一些事情
- }
- }
复制代码
使用计算属性代替methods:
- // 不推荐
- <template>
- <div>{{ getName() }}</div>
- </template>
- methods: {
- getName() {
- return this.firstName + ' ' + this.lastName
- }
- }
- // 推荐
- <template>
- <div>{{ fullName }}</div>
- </template>
- computed: {
- fullName() {
- return this.firstName + ' ' + this.lastName
- }
- }
复制代码
虚拟滚动:
- <!-- 使用虚拟滚动处理长列表 -->
- <template>
- <div class="virtual-scroll-container" @scroll="handleScroll">
- <div class="scroll-content" :style="{ height: totalHeight + 'px' }">
- <div
- v-for="item in visibleData"
- :key="item.id"
- class="scroll-item"
- :style="{ transform: `translateY(${item.offset}px)` }"
- >
- {{ item.content }}
- </div>
- </div>
- </div>
- </template>
- <script>
- export default {
- data() {
- return {
- allData: [], // 所有数据
- itemHeight: 50, // 每项高度
- visibleCount: 20, // 可见项数量
- startIndex: 0, // 起始索引
- offsetTop: 0 // 滚动条偏移量
- }
- },
- computed: {
- totalHeight() {
- return this.allData.length * this.itemHeight
- },
- visibleData() {
- return this.allData.slice(
- this.startIndex,
- this.startIndex + this.visibleCount
- ).map((item, index) => {
- return {
- ...item,
- offset: this.startIndex * this.itemHeight + index * this.itemHeight
- }
- })
- }
- },
- methods: {
- handleScroll(e) {
- const scrollTop = e.target.scrollTop
- this.startIndex = Math.floor(scrollTop / this.itemHeight)
- this.offsetTop = scrollTop - (scrollTop % this.itemHeight)
- }
- },
- created() {
- // 模拟获取大量数据
- this.allData = Array.from({ length: 10000 }, (_, index) => ({
- id: index,
- content: `Item ${index}`
- }))
- }
- }
- </script>
- <style>
- .virtual-scroll-container {
- height: 1000px;
- overflow-y: auto;
- border: 1px solid #ddd;
- }
- .scroll-content {
- position: relative;
- }
- .scroll-item {
- position: absolute;
- width: 100%;
- height: 50px;
- line-height: 50px;
- box-sizing: border-box;
- border-bottom: 1px solid #eee;
- padding-left: 10px;
- }
- </style>
复制代码
部署与上线
项目打包
- # 生产环境打包
- npm run build
- # 打包结果会生成在 dist 目录下
复制代码
vue.config.js配置:
- const path = require('path')
- const CompressionPlugin = require('compression-webpack-plugin')
- function resolve(dir) {
- return path.join(__dirname, dir)
- }
- module.exports = {
- // 部署应用包时的基本 URL
- publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
-
- // 输出文件目录
- outputDir: 'dist',
-
- // 放置生成的静态资源 (js、css、img、fonts) 的 (相对于 outputDir 的) 目录。
- assetsDir: 'static',
-
- // 是否在保存的时候使用 `eslint-loader` 进行检查。
- lintOnSave: process.env.NODE_ENV === 'development',
-
- // 生产环境是否生成 sourceMap 文件
- productionSourceMap: false,
-
- // 配置webpack
- configureWebpack: {
- resolve: {
- alias: {
- '@': resolve('src')
- }
- },
- // 生产环境配置
- plugins: [
- new CompressionPlugin({
- algorithm: 'gzip',
- test: /\.(js|css|html)$/,
- threshold: 10240,
- minRatio: 0.8
- })
- ]
- },
-
- // 链式配置
- chainWebpack(config) {
- // 设置 svg-sprite-loader
- config.module
- .rule('svg')
- .exclude.add(resolve('src/icons'))
- .end()
- config.module
- .rule('icons')
- .test(/\.svg$/)
- .include.add(resolve('src/icons'))
- .end()
- .use('svg-sprite-loader')
- .loader('svg-sprite-loader')
- .options({
- symbolId: 'icon-[name]'
- })
- .end()
- // 设置保留空白
- config.module
- .rule('vue')
- .use('vue-loader')
- .loader('vue-loader')
- .tap(options => {
- options.compilerOptions.preserveWhitespace = true
- return options
- })
- .end()
- // 设置 runtimeCompiler
- config.resolve.alias.set('vue$', 'vue/dist/vue.esm.js')
- // 生产环境配置
- if (process.env.NODE_ENV === 'production') {
- // 移除 prefetch 插件
- config.plugins.delete('prefetch')
-
- // 压缩代码
- config.optimization.minimize(true)
-
- // 分割代码
- config.optimization.splitChunks({
- chunks: 'all',
- cacheGroups: {
- libs: {
- name: 'chunk-libs',
- test: /[\\/]node_modules[\\/]/,
- priority: 10,
- chunks: 'initial' // 只打包初始时依赖的第三方
- },
- elementUI: {
- name: 'chunk-elementUI', // 单独将 elementUI 拆包
- priority: 20, // 权重要大于 libs 和 app
- test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
- },
- commons: {
- name: 'chunk-commons',
- test: resolve('src/components'), // 可自定义拓展你的规则
- minChunks: 3, // 最小公用次数
- priority: 5,
- reuseExistingChunk: true
- }
- }
- })
-
- // https://webpack.js.org/configuration/optimization/#optimizationruntimechunk
- config.optimization.runtimeChunk('single')
- }
- },
-
- // 开发服务器配置
- devServer: {
- port: 8080,
- open: true,
- overlay: {
- warnings: false,
- errors: true
- },
- proxy: {
- // 代理配置
- '/api': {
- target: 'http://localhost:3000',
- changeOrigin: true,
- pathRewrite: {
- '^/api': ''
- }
- }
- }
- }
- }
复制代码
部署到静态服务器
Nginx配置:
- server {
- listen 80;
- server_name yourdomain.com;
- root /usr/share/nginx/html; # 网站根目录
- index index.html; # 默认首页
- # 处理单页应用路由问题
- location / {
- try_files $uri $uri/ /index.html;
- }
- # 静态资源缓存
- location ~* \.(?:css|js)$ {
- try_files $uri =404;
- expires 1y;
- add_header Cache-Control "public";
- }
- # 图片等媒体资源缓存
- location ~* \.(jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ {
- try_files $uri =404;
- expires 1M;
- access_log off;
- add_header Cache-Control "public";
- }
- # 开启gzip压缩
- gzip on;
- gzip_min_length 1k;
- gzip_comp_level 6;
- gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml;
- gzip_vary on;
- gzip_disable "MSIE [1-6]\.";
- # 错误页面
- error_page 404 /index.html;
- }
复制代码
Docker部署:
- # 构建阶段
- FROM node:14 as build-stage
- WORKDIR /app
- COPY package*.json ./
- RUN npm install
- COPY . .
- RUN npm run build
- # 生产阶段
- FROM nginx:stable-alpine as production-stage
- COPY --from=build-stage /app/dist /usr/share/nginx/html
- EXPOSE 80
- CMD ["nginx", "-g", "daemon off;"]
复制代码
Docker Compose:
- version: '3'
- services:
- app:
- build: .
- ports:
- - "80:80"
- volumes:
- - ./nginx.conf:/etc/nginx/conf.d/default.conf
- restart: always
复制代码
SEO优化
预渲染:
- // 安装prerender-spa-plugin
- npm install prerender-spa-plugin --save-dev
- // vue.config.js配置
- const PrerenderSPAPlugin = require('prerender-spa-plugin')
- const Renderer = PrerenderSPAPlugin.PuppeteerRenderer
- const path = require('path')
- module.exports = {
- configureWebpack: {
- plugins: [
- new PrerenderSPAPlugin({
- // 生成文件的路径,也可以与webpakc打包的一致。
- // 下面这句话非常重要!!!
- // 这个目录只能有一级,如果目录层次大于一级,在生成的时候不会有任何错误提示,在预渲染的时候只会卡着不动。
- staticDir: path.join(__dirname, 'dist'),
- // 对应自己的路由文件,比如a有参数,就需要写成 /a/param1。
- routes: ['/', '/about', '/contact'],
- // 这个很重要,如果没有配置这段,也不会进行预编译
- renderer: new Renderer({
- injectProperty: '__PRERENDER_INJECTED',
- inject: {
- foo: 'bar'
- },
- // 在 app.vue 文件中,document.dispatchEvent(new Event('custom-render-trigger')),
- // 这里的事件名要和下面的一致
- renderAfterDocumentEvent: 'custom-render-trigger',
- // 或者等待指定时间后渲染
- renderAfterTime: 5000
- })
- })
- ]
- }
- }
复制代码
Meta标签管理:
- // 安装vue-meta
- npm install vue-meta --save
- // main.js
- import VueMeta from 'vue-meta'
- Vue.use(VueMeta, {
- // optional pluginOptions
- refreshOnceOnNavigation: true
- })
- // 在组件中使用
- export default {
- metaInfo() {
- return {
- title: this.pageTitle,
- meta: [
- { name: 'description', content: this.pageDescription },
- { property: 'og:title', content: this.pageTitle },
- { property: 'og:description', content: this.pageDescription },
- { property: 'og:type', content: 'website' },
- { property: 'og:url', content: this.pageUrl },
- { name: 'twitter:card', content: 'summary_large_image' },
- { name: 'twitter:title', content: this.pageTitle },
- { name: 'twitter:description', content: this.pageDescription },
- { name: 'twitter:image', content: this.pageImage }
- ]
- }
- }
- }
复制代码
结构化数据:
- <!-- 在组件中添加结构化数据 -->
- <template>
- <div>
- <!-- 页面内容 -->
-
- <!-- 结构化数据 -->
- <script type="application/ld+json" v-html="structuredData"></script>
- </div>
- </template>
- <script>
- export default {
- computed: {
- structuredData() {
- return JSON.stringify({
- "@context": "https://schema.org",
- "@type": "Article",
- "headline": this.article.title,
- "image": [
- this.article.imageUrl
- ],
- "datePublished": this.article.publishDate,
- "dateModified": this.article.updateDate,
- "author": {
- "@type": "Person",
- "name": this.article.author
- },
- "publisher": {
- "@type": "Organization",
- "name": "Your Organization",
- "logo": {
- "@type": "ImageObject",
- "url": "https://www.your-organization.com/logo.jpg"
- }
- },
- "description": this.article.description
- })
- }
- }
- }
- </script>
复制代码
总结与展望
Vue2作为一个成熟的前端框架,提供了完整的解决方案来构建现代化的单页应用。通过本文的学习,我们从Vue2的基础知识开始,逐步深入到组件化开发、路由管理、状态管理等核心概念,再到进阶技巧和项目实战,最后介绍了部署与SEO优化。
Vue2的核心优势在于其简洁的API设计、灵活的组件系统和强大的生态系统,使得开发者能够快速构建高性能、可维护的前端应用。同时,Vue2的渐进式特性也使得它可以轻松地与其他项目或库集成。
随着Vue3的发布,Vue生态系统正在不断演进。Vue3带来了更好的性能、更小的包体积以及更好的TypeScript支持。对于Vue2开发者来说,学习Vue3是一个自然的过程,因为许多核心概念在两个版本中是相通的。
未来,前端开发将继续朝着组件化、模块化和工程化的方向发展。Vue作为一个活跃的开源项目,也将继续演进,为开发者提供更好的工具和体验。无论技术如何变化,掌握核心概念和最佳实践,将帮助开发者更好地适应未来的变化。
希望本文能够帮助读者全面掌握Vue2前端开发技术栈,并在实际项目中应用这些知识,构建出优秀的Web应用。
版权声明
1、转载或引用本网站内容(Vue2纯前端项目开发实战指南从基础到进阶全面掌握前端技术栈)须注明原网址及作者(威震华夏关云长),并标明本网站网址(https://pixtech.cc/)。
2、对于不当转载或引用本网站内容而引起的民事纷争、行政处理或其他损失,本网站不承担责任。
3、对不遵守本声明或其他违法、恶意使用本网站内容者,本网站保留追究其法律责任的权利。
本文地址: https://pixtech.cc/thread-37598-1-1.html
|
|