渐进式JavaScript框架:Vue 工具 & 模块化 & 迁移

渐进式JavaScript框架:Vue 工具 & 模块化 & 迁移

前言

Vue2 其生态系统围绕开发效率、模块化架构和版本迁移提供了完整支持,同时与其他框架相比有独特的优势。

工具

在项目开发中,合理使用工具可以显著提升开发效率、优化项目质量并简化部署流程。这些工具覆盖了开发辅助、代码规范、构建打包、调试测试、状态管理、UI 组件库等多个核心场景

测试

当构建可靠的应用时,测试在个人或团队构建新特性、重构代码、修复 bug 等工作中扮演了关键的角色。尽管测试的流派有很多,它们在 web 应用这个领域里主要有三大类:

  • 单元测试:单元测试允许你将独立单元的代码进行隔离测试,其目的是为开发者提供对代码的信心。通过编写细致且有意义的测试,你能够有信心在构建新特性或重构已有代码的同时,保持应用的功能和稳定。

当测试失败时,提供有用的错误信息对于单元测试框架来说至关重要。这是断言库应尽的职责。一个具有高质量错误信息的断言能够最小化调试问题所需的时间。除了简单地告诉你什么测试失败了,断言库还应额外提供上下文以及测试失败的原因,例如预期结果 vs. 实际得到的结果。

Jest 是一个专注于简易性的 JavaScript 测试框架。一个其独特的功能是可以为测试生成快照 (snapshot),以提供另一种验证应用单元的方法。

javascript 复制代码
// 引入必要的测试工具
const { shallowMount } = require('@vue/test-utils')
// 假设我们已经通过全局方式引入了Vue

// 定义要测试的组件(与HTML中定义的一致)
const CounterComponent = {
  template: `
    <div>
      <button @click="increment">+</button>
      <span>{{ count }}</span>
      <button @click="decrement">-</button>
    </div>
  `,
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    },
    decrement() {
      this.count--
    }
  }
}

describe('CounterComponent (纯HTML环境)', () => {
  it('初始计数应为0', () => {
    const wrapper = shallowMount(CounterComponent)
    expect(wrapper.vm.count).toBe(0)
    expect(wrapper.find('span').text()).toBe('0')
  })

  it('点击+按钮应该增加计数', async () => {
    const wrapper = shallowMount(CounterComponent)
    const incrementBtn = wrapper.find('button:first-child')
    
    await incrementBtn.trigger('click')
    expect(wrapper.vm.count).toBe(1)
    expect(wrapper.find('span').text()).toBe('1')
  })

  it('点击-按钮应该减少计数', async () => {
    const wrapper = shallowMount(CounterComponent)
    const decrementBtn = wrapper.find('button:last-child')
    
    await decrementBtn.trigger('click')
    expect(wrapper.vm.count).toBe(-1)
    expect(wrapper.find('span').text()).toBe('-1')
  })
})

Vue CLI 官方提供了插件Jest

javascript 复制代码
npm install --save-dev jest vue-jest@3 babel-jest @vue/test-utils@1 babel-core@^7.0.0-0

package.json 中添加脚本:

javascript 复制代码
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch", // 实时监控文件变化并重新测试
    "test:coverage": "jest --coverage" // 生成测试覆盖率报告
  }
}

运行测试:

javascript 复制代码
npm test

Mocha 是一个专注于灵活性的 JavaScript 测试框架。因为其灵活性,它允许你选择不同的库来满足诸如侦听 (如 Sinon ) 和断言 (如 Chai ) 等其它常见的功能。另一个 Mocha 独特的功能是它不止可以在 Node.js 里运行测试,还可以在浏览器里运行测试。

javascript 复制代码
// 引入依赖
const { expect } = require('chai');
const { capitalize, reverseString, isPalindrome } = require('./stringUtils');

// 测试套件
describe('stringUtils', () => {
  // 测试capitalize函数
  describe('capitalize', () => {
    it('should capitalize the first letter of a string', () => {
      expect(capitalize('hello')).to.equal('Hello');
      expect(capitalize('vue')).to.equal('Vue');
    });

    it('should handle empty string', () => {
      expect(capitalize('')).to.equal('');
    });
  });

  // 测试reverseString函数
  describe('reverseString', () => {
    it('should reverse a string', () => {
      expect(reverseString('hello')).to.equal('olleh');
      expect(reverseString('test')).to.equal('tset');
    });
  });

  // 测试isPalindrome函数(回文判断)
  describe('isPalindrome', () => {
    it('should return true for palindromes', () => {
      expect(isPalindrome('racecar')).to.be.true;
      expect(isPalindrome('madam')).to.be.true;
    });

    it('should return false for non-palindromes', () => {
      expect(isPalindrome('hello')).to.be.false;
      expect(isPalindrome('world')).to.be.false;
    });
  });
});
    

Vue CLI 官方提供了插件Mocha

javascript 复制代码
npm install --save-dev mocha chai @vue/test-utils@1 vue@2.6.14 jsdom jsdom-global

package.json 中添加测试脚本:

javascript 复制代码
{
  "scripts": {
    "test": "mocha 'test/**/*.test.js'",
    "test:watch": "mocha 'test/**/*.test.js' --watch"
  }
}

执行测试:

javascript 复制代码
npm test          # 运行所有测试
npm run test:watch # 监控文件变化并自动重新测试
  • 组件测试 :测试大多数 Vue 组件时都必须将它们挂载到 DOM (虚拟或真实) 上,才能完全断言它们正在工作。这是另一个与框架无关的概念。因此组件测试框架的诞生,是为了让用户能够以可靠的方式完成这项工作,同时还提供了 Vue 特有的诸如对 VuexVue Router 和其他 Vue 插件的集成的便利性。

Vue Testing Library 是一组专注于测试组件而不依赖实现细节的工具。由于在设计时就充分考虑了可访问性,它采用的方案也使重构变得轻而易举。强调从用户视角测试组件,不关注内部实现。

javascript 复制代码
import { shallowMount } from '@vue/test-utils';
import ButtonComponent from '@/components/ButtonComponent.vue';

describe('ButtonComponent', () => {
  // 测试渲染内容
  it('should render correct text', () => {
    const wrapper = shallowMount(ButtonComponent, {
      propsData: {
        label: '点击我'
      }
    });
    // 验证按钮文本是否正确
    expect(wrapper.find('button').text()).toBe('点击我');
  });

  // 测试点击事件
  it('should emit "click" event when clicked', async () => {
    const wrapper = shallowMount(ButtonComponent);
    // 模拟点击事件
    await wrapper.find('button').trigger('click');
    // 验证是否触发了emit
    expect(wrapper.emitted('click')).toBeTruthy();
    // 验证触发次数
    expect(wrapper.emitted('click').length).toBe(1);
  });
});

Vue Test Utils 是官方的偏底层的组件测试库,它是为用户提供对 Vue 特定 API 的访问而编写的。如果你对测试 Vue 应用不熟悉,我们建议你使用 Vue Testing Library ,它是 Vue Test Utils 的抽象。

javascript 复制代码
import { shallowMount } from '@vue/test-utils'
import Counter from './Counter.vue'

describe('Counter.vue (Vue Test Utils)', () => {
  it('初始值为0', () => {
    const wrapper = shallowMount(Counter)
    // 直接访问组件内部状态
    expect(wrapper.vm.count).toBe(0)
  })

  it('调用increment方法后值增加1', () => {
    const wrapper = shallowMount(Counter)
    // 直接调用组件内部方法
    wrapper.vm.increment()
    expect(wrapper.vm.count).toBe(1)
  })

  it('点击按钮后DOM更新', async () => {
    const wrapper = shallowMount(Counter)
    // 按选择器查找元素
    const button = wrapper.find('button')
    await button.trigger('click')
    // 检查DOM文本
    expect(wrapper.find('p').text()).toBe('当前值: 1')
  })
})
  • 端到端 (E2E,end-to-end) 测试 :虽然单元测试为开发者提供了一定程度的信心,但是单元测试和组件测试在部署到生产环境时提供应用整体覆盖的能力是有限的。因此,端到端测试可以说从应用最重要的方面进行测试覆盖:当用户实际使用应用时会发生什么。从用户视角出发,模拟真实用户操作,测试整个应用的流程是否正常(如登录→浏览→下单的完整链路)

端到端测试的一个主要优点是它能够跨多个浏览器测试应用。尽管 100% 的跨浏览器覆盖看上去很诱人,但需要注意的是,因为持续运行这些跨浏览器测试需要额外的时间和机器消耗,它会降低团队的资源回报。因此,在选择应用需要的跨浏览器测试数量时,必须注意这种权衡。

Cypress.io 是一个测试框架(现代化 E2E 测试工具,自带浏览器环境和可视化界面),旨在通过使开发者能够可靠地测试他们的应用,同时提供一流的开发者体验,来提高开发者的生产率。

javascript 复制代码
// 自定义登录命令:cy.login(username, password)
Cypress.Commands.add('login', (username, password) => {
  cy.visit('/login');
  cy.get('input[name="username"]').type(username);
  cy.get('input[name="password"]').type(password);
  cy.contains('button', '登录').click();
  // 验证登录成功(确保跳转完成)
  cy.url().should('include', '/dashboard');
});

// 自定义退出命令:cy.logout()
Cypress.Commands.add('logout', () => {
  cy.get('.user-info').click(); // 点击用户头像打开菜单
  cy.contains('li', '退出登录').click(); // 点击退出
  cy.url().should('include', '/login'); // 验证跳转至登录页
});

可视化界面运行(开发调试):

javascript 复制代码
npm run cypress:open

命令行运行(CI/CD 集成):

javascript 复制代码
npm run cypress:run

Nightwatch.js 是一个端到端测试框架(基于 Selenium 的测试框架),可用于测试 web 应用和网站,以及 Node.js 单元测试和集成测试。

javascript 复制代码
module.exports = {
  '登录测试': function (browser) {
    // 访问登录页面
    browser.url('http://localhost:8080/login')
      .waitForElementVisible('body', 1000);

    // 测试1:空表单提交
    browser.click('button[type="submit"]')
      .assert.containsText('.error-message', '用户名不能为空')
      .assert.containsText('.error-message', '密码不能为空');

    // 测试2:错误密码登录
    browser.setValue('input[name="username"]', 'testuser')
      .setValue('input[name="password"]', 'wrongpassword')
      .click('button[type="submit"]')
      .pause(500) // 等待异步响应
      .assert.containsText('.error-message', '用户名或密码错误');

    // 测试3:正确登录并跳转
    browser.clearValue('input[name="password"]')
      .setValue('input[name="password"]', 'correctpassword')
      .click('button[type="submit"]')
      .waitForElementVisible('.dashboard', 1000)
      .assert.urlContains('/dashboard')
      .assert.containsText('.user-info', 'testuser')
      .end(); // 结束测试
  }
};

运行测试:

javascript 复制代码
# 先启动Vue开发服务器
npm run dev

# 在另一个终端运行测试
npm run test:e2e

Puppeteer 是一个 Node.js 库,它提供高阶 API 来控制浏览器,并可以与其他测试运行程序 (例如 Jest) 配对来测试应用。

javascript 复制代码
import puppeteer from 'puppeteer';
// Or import puppeteer from 'puppeteer-core';

// Launch the browser and open a new blank page
const browser = await puppeteer.launch();
const page = await browser.newPage();

// Navigate the page to a URL.
await page.goto('https://developer.chrome.com/');

// Set screen size.
await page.setViewport({width: 1080, height: 1024});

// Type into search box using accessible input name.
await page.locator('aria/Search').fill('automate beyond recorder');

// Wait and click on first result.
await page.locator('.devsite-result-item-link').click();

// Locate the full title with a unique string.
const textSelector = await page
  .locator('text/Customize and automate')
  .waitHandle();
const fullTitle = await textSelector?.evaluate(el => el.textContent);

// Print the full title.
console.log('The title of this blog post is "%s".', fullTitle);

await browser.close();

TestCafe 是一个基于端到端的 Node.js 框架,旨在提供简单的设置,以便开发者能够专注于创建易于编写和可靠的测试。

javascript 复制代码
fixture('Pizza Palace')
    .page('https://testcafe-demo-page.glitch.me/');

test('Submit a form', async t => {
    await t
        // automatically dismiss dialog boxes
        .setNativeDialogHandler(() => true)

        // drag the pizza size slider
        .drag('.noUi-handle', 100, 0)

        // select the toppings
        .click('.next-step')
        .click('label[for="pepperoni"]')
        .click('#step2 .next-step')

        // fill the address form
        .click('.confirm-address')
        .typeText('#phone-input', '+1-541-754-3001')
        .click('#step3 .next-step')

        // zoom into the iframe map
        .switchToIframe('.restaurant-location iframe')
        .click('button[title="Zoom in"]')

        // submit the order
        .switchToMainWindow()
        .click('.complete-order');
});

TypeScript 支持

静态类型系统能帮助你有效防止许多潜在的运行时错误,而且随着你的应用日渐丰满会更加显著。这就是为什么 Vue 不仅仅为 Vue core 提供了针对 TypeScript 的官方类型声明,还为 Vue RouterVuex 也提供了相应的声明文件。

而且,我们已经把它们发布到了 NPM ,最新版本的 TypeScript 也知道该如何自己从 NPM 包里解析类型声明。这意味着只要你成功地通过 NPM 安装了,就不再需要任何额外的工具辅助,即可在 Vue 中使用 TypeScript 了。

javascript 复制代码
# 1. 安装 Vue CLI(若未安装)
npm install -g @vue/cli

# 2. 创建项目(选择 Manually select features)
vue create vue2-ts-demo

核心配置文件说明:

  • tsconfig.jsonTypeScript 编译配置(指定目标版本、模块系统等)
javascript 复制代码
// tsconfig.json
{
  "compilerOptions": {
    // 与 Vue 的浏览器支持保持一致
    "target": "es5",
    // 这可以对 `this` 上的数据 property 进行更严格的推断
    "strict": true,
    // 如果使用 webpack 2+ 或 rollup,可以利用 tree-shake:
    "module": "es2015",
    "moduleResolution": "node"
  }
}

注意你需要引入 strict: true (或者至少 noImplicitThis: true,这是 strict 模式的一部分) 以利用组件方法中 this 的类型检查,否则它会始终被看作 any 类型。

  • 编辑器支持

要使用 TypeScript 开发 Vue 应用程序,我们强烈建议您使用 Visual Studio Code ,它为 TypeScript 提供了极好的"开箱即用"支持。如果你正在使用单文件组件 (SFC ),可以安装提供 SFC 支持以及其他更多实用功能的 Vetur 插件。

WebStorm 同样为 TypeScriptVue 提供了"开箱即用"的支持。

  • 基本用法

要让 TypeScript 正确推断 Vue 组件选项中的类型,您需要使用 Vue.componentVue.extend 定义组件:

javascript 复制代码
import Vue from 'vue'
const Component = Vue.extend({
  // 类型推断已启用
})

const Component = {
  // 这里不会有类型推断,
  // 因为 TypeScript 不能确认这是 Vue 组件的选项
}

如果您在声明组件时更喜欢基于类的 API ,则可以使用官方维护的 vue-class-component 装饰器:

html 复制代码
<!-- src/components/HelloWorld.vue -->
<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <button @click="increment">计数: {{ count }}</button>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import { Component, Prop, Vue as VueDecorator } from 'vue-property-decorator'

// 用 @Component 装饰器定义组件
@Component
export default class HelloWorld extends Vue {
  // 定义响应式数据(替代 data())
  count: number = 0

  // 定义 props(通过 @Prop 装饰器,自动类型检查)
  @Prop({ type: String, required: true })
  msg!: string  // "!" 表示非空断言

  // 定义方法(直接作为类的方法)
  increment(): void {
    this.count++
  }

  // 生命周期钩子(直接作为类的方法)
  mounted(): void {
    console.log('组件挂载完成,msg:', this.msg)
  }
}
</script>

生产环境部署

开发环境下,Vue 会提供很多警告来帮你对付常见的错误与陷阱。而在生产环境下,这些警告语句却没有用,反而会增加应用的体积。此外,有些警告检查还有一些小的运行时开销,这在生产环境模式下是可以避免的。

  • 不使用构建工具

如果用 Vue 完整独立版本,即直接用 <script> 元素引入 Vue 而不提前进行构建,请记得在生产环境下使用压缩后的版本 (vue.min.js)。

  • 使用构建工具

当使用 webpackBrowserify 类似的构建工具时,Vue 源码会根据 process.env.NODE_ENV 决定是否启用生产环境模式,默认情况为开发环境模式。在 webpackBrowserify 中都有方法来覆盖此变量,以启用 Vue 的生产环境模式,同时在构建过程中警告语句也会被压缩工具去除。所有这些在 vue-cli 模板中都预先配置好了,但了解一下怎样配置会更好。

webpack 4+ 中,你可以使用 mode 选项:

javascript 复制代码
module.exports = {
  mode: 'production'
}

但是在 webpack 3 及其更低版本中,你需要使用 DefinePlugin

javascript 复制代码
var webpack = require('webpack')

module.exports = {
  // ...
  plugins: [
    // ...
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('production')
    })
  ]
}

如果使用Browserify ,在运行打包命令时将 NODE_ENV 设置为 "production"。这等于告诉 vueify 避免引入热重载和开发相关的代码。对打包后的文件进行一次全局的 envify 转换。这使得压缩工具能清除掉 Vue 源码中所有用环境变量条件包裹起来的警告语句。例如:

javascript 复制代码
NODE_ENV=production browserify -g envify -e main.js | uglifyjs -c -m > build.js

或者在 Gulp 中使用 envify

javascript 复制代码
// 使用 envify 自定义模块指定环境变量
var envify = require('envify/custom')

browserify(browserifyOptions)
  .transform(vueify)
  .transform(
    // 必填项,以处理 node_modules 里的文件
    { global: true },
    envify({ NODE_ENV: 'production' })
  )
  .bundle()

或者配合 Gruntgrunt-browserify 使用 envify

javascript 复制代码
// 使用 envify 自定义模块指定环境变量
var envify = require('envify/custom')

browserify: {
  dist: {
    options: {
        // 该函数用来调整 grunt-browserify 的默认指令
        configure: b => b
        .transform('vueify')
        .transform(
            // 必填项,以处理 node_modules 里的文件
          { global: true },
          envify({ NODE_ENV: 'production' })
        )
        .bundle()
    }
  }
}

执行构建命令:

javascript 复制代码
# 本地开发(实时监听文件变化)
npm run dev
# 执行生产环境打包(会自动读取 .env.production 配置)
npm run build

构建完成后,项目根目录会生成 dist 文件夹,包含所有可部署的静态文件:

复制代码
dist/
├─ index.html           # 入口 HTML
├─ static/              # 静态资源(js/css/img/fonts)
│  ├─ js/
│  ├─ css/
│  └─ img/
└─ favicon.ico          # 网站图标(如果有)
  • 模板预编译

模板预编译(Template Precompilation)是 Vue 等前端框架中一项重要的性能优化技术,指的是在项目构建阶段(而非运行时)将组件的模板(<template> 中的内容)提前编译为 JavaScript 渲染函数,从而避免浏览器在运行时动态编译模板的开销。

预编译模板最简单的方式就是使用单文件组件------相关的构建设置会自动把预编译处理好,所以构建好的代码已经包含了编译出来的渲染函数而不是原始的模板字符串。

如果你使用 webpack ,并且喜欢分离 JavaScript 和模板文件,你可以使用 vue-template-loader,它也可以在构建过程中把模板文件转换成为 JavaScript 渲染函数。

  • 开发阶段 :开发者编写组件模板(如 .vue 文件中的 <template> 标签):
html 复制代码
<template>
  <div class="app">
    <h1>{{ title }}</h1>
    <ul>
      <li v-for="item in list" :key="item.id">{{ item.name }}</li>
    </ul>
  </div>
</template>
  • 构建阶段vue-loader 在打包时解析 <template> 内容,通过 Vue 内部的编译器将模板转换为渲染函数(render 函数):
javascript 复制代码
// 预编译后生成的渲染函数(简化版)
function render(h) {
  return h('div', { class: 'app' }, [
    h('h1', this.title),
    h('ul', this.list.map(item => 
      h('li', { key: item.id }, item.name)
    ))
  ])
}
  • 运行阶段 :浏览器加载打包后的代码时,直接执行渲染函数生成 DOM,无需再编译模板。

模板预编译是 Vue 性能优化的关键手段,通过 "构建时编译" 替代 "运行时编译",显著提升页面加载速度和运行效率。避免浏览器在用户访问时进行模板编译,减少主线程阻塞,尤其对复杂组件或低性能设备更明显。

  • 提取组件的 CSS

提取组件的 CSS (将组件内的样式从 JS 中分离出来,生成独立的 CSS 文件)是优化生产环境构建的重要手段,可避免样式在 JS 中打包导致的加载阻塞问题,同时便于浏览器缓存 CSS

Vue CLI 已内置相关配置,只需在 vue.config.js 中开启 CSS 提取即可:

javascript 复制代码
// vue.config.js
module.exports = {
  // 生产环境提取 CSS 为独立文件
  css: {
    extract: process.env.NODE_ENV === 'production' ? {
      filename: 'css/[name].[contenthash:8].css', // 提取的 CSS 文件名(带哈希,利于缓存)
      chunkFilename: 'css/[name].[contenthash:8].css' // 异步组件的 CSS 文件名
    } : false // 开发环境不提取(便于热更新)
  }
};

配置后,执行 npm run build 会在 dist/css 目录下生成独立的 CSS 文件,而非内嵌在 JS 中。

  • 跟踪运行时错误

跟踪运行时错误(如组件渲染异常、方法调用错误等)需要结合框架自身的错误处理机制和前端监控工具,确保能及时捕获并定位问题。

如果在组件渲染时出现运行错误,错误将会被传递至全局 Vue.config.errorHandler 配置函数 (如果已设置)。利用这个钩子函数来配合错误跟踪服务是个不错的主意。

javascript 复制代码
import Vue from 'vue';
import App from './App.vue';

// 捕获 Vue 组件渲染和观察器错误
Vue.config.errorHandler = function(err, vm, info) {
  // err: 错误对象
  // vm: 发生错误的 Vue 实例
  // info: 错误信息(如 "render function" 表示渲染函数出错)
  
  console.error('Vue 运行时错误:', err);
  console.error('错误实例:', vm);
  console.error('错误详情:', info);
  
  // 可在此处将错误发送到后端监控服务
  reportErrorToServer({
    message: err.message,
    stack: err.stack,
    info: info,
    component: vm.$options.name || vm.$options._componentTag, // 组件名
    url: window.location.href,
    time: new Date().toISOString()
  });
};

// 捕获未被 Vue 捕获的 promise 错误
window.addEventListener('unhandledrejection', (event) => {
  console.error('未处理的 Promise 错误:', event.reason);
  reportErrorToServer({
    type: 'unhandledrejection',
    message: event.reason.message,
    stack: event.reason.stack,
    url: window.location.href
  });
  event.preventDefault(); // 阻止默认处理(避免控制台警告)
});

// 捕获其他未捕获的错误
window.addEventListener('error', (event) => {
  console.error('全局未捕获错误:', event.error);
  reportErrorToServer({
    type: 'global error',
    message: event.error.message,
    stack: event.error.stack,
    url: window.location.href
  });
});

new Vue({
  el: '#app',
  render: h => h(App)
});

对于复杂项目,推荐使用成熟的错误监控工具,自动收集和分析错误:比如 Sentry ,它为 Vue 提供了官方集成。

javascript 复制代码
npm install @sentry/browser @sentry/integrations --save
javascript 复制代码
// main.js
import * as Sentry from '@sentry/browser';
import { Vue as VueIntegration } from '@sentry/integrations';
import Vue from 'vue';

Sentry.init({
  dsn: 'https://your-sentry-dsn', // 从 Sentry 项目获取
  integrations: [
    new VueIntegration({
      Vue,
      attachProps: true, // 记录组件 props 信息
      logErrors: true // 在控制台打印错误
    })
  ],
  environment: process.env.NODE_ENV, // 区分开发/生产环境
  release: 'v1.0.0' // 版本号
});

规模化

"规模化" 指的是在大型项目(如多人协作、复杂业务逻辑、多模块)中,通过规范架构、优化性能、统一协作等方式,确保项目可维护、可扩展、性能稳定。

路由

Vue2 中路由管理主要依赖 vue-router@3.x(注意:Vue2 必须使用 vue-router 3.x 版本,4.x 版本仅支持 Vue 3 )。路由的核心作用是实现单页应用(SPA)中不同页面的切换,无需刷新整个页面。

安装 vue-router

javascript 复制代码
# Vue 2 需指定 3.x 版本
npm install vue-router@3 --save

Unpkg.com 提供了基于 npmCDN 链接。

javascript 复制代码
https://unpkg.com/vue-router@4.0.15/dist/vue-router.global.js

如果你只需要非常简单的路由而不想引入一个功能完整的路由库,可以像这样动态渲染一个页面级的组件:

html 复制代码
<body>
  <div id="app">
    <!-- 路由导航链接 -->
    <p>
      <router-link to="/">首页</router-link> |
      <router-link to="/about">关于</router-link>
    </p>
    <!-- 路由匹配的组件会渲染在这里 -->
    <router-view></router-view>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script src="https://unpkg.com/vue-router@3.6.5/dist/vue-router.js"></script>
  <script>
    // 1. 定义路由组件
    const Home = { template: '<p>home page</p>' }
    const About = { template: '<p>about page</p>' }
    const NotFound = { template: '<p>Page not found</p>' }

    // 2. 定义路由规则(hash 模式,路径以 / 开头)
    const routes = [
      { path: '/', component: Home },       // 匹配 #/
      { path: '/about', component: About }, // 匹配 #/about
      { path: '*', component: NotFound }    // 匹配所有未定义路径
    ]

    // 3. 创建路由实例(强制 hash 模式,适配 file 协议)
    const router = new VueRouter({
      mode: 'hash', // file 协议下必须用 hash 模式,否则路径无法匹配
      routes: routes
    })

    // 4. 初始化 Vue 实例
    new Vue({
      el: '#app',
      router,
      // 可选:初始化时强制跳转到首页(避免初始路径不匹配)
      mounted() {
        // 如果初始 hash 为空(如直接打开文件),跳转到 #/
        if (window.location.hash === '' || window.location.hash === '#') {
          this.$router.push('/');
        }
      }
    })
  </script>
  </script>
</body>

状态管理

由于状态零散地分布在许多组件和组件之间的交互中,大型应用复杂度也经常逐渐增长。状态管理的核心是解决组件间数据共享和复杂状态逻辑的问题,最常用的方案是官方推荐的 Vuex

javascript 复制代码
npm install vuex@3 --save

当项目存在以下场景时,需使用状态管理:

  • 多个组件共享同一数据(如用户信息、购物车数据);
  • 组件间嵌套层级较深,通过 props 传递数据繁琐;
  • 多个组件需要修改同一数据,难以追踪数据变化来源。

Vuex 提供了一个集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

如果是HTML ,可以通过CDN 引入 Vue 2Vuex 3.x

javascript 复制代码
<!-- 引入 Vuex 3.x(示例用 3.6.2 版本) -->
<script src="https://cdn.jsdelivr.net/npm/vuex@3.6.2/dist/vuex.js"></script>

<script> 标签中创建 store 实例,定义 statemutations 等核心模块,创建 Vue 实例时,通过 store 选项注入,使所有组件(包括根组件)可通过 this.$store 访问:

html 复制代码
<body>
  <div id="app">
    <!-- 直接访问 state 和 getters -->
    <p>计数器:{{ $store.state.count }}</p>
    <p>是否偶数:{{ $store.getters.isEven }}</p>
    <p>用户名:{{ $store.state.user?.name || "未登录" }}</p>

    <!-- 触发 mutations 和 actions -->
    <button @click="$store.commit('INCREMENT')">count+1</button>
    <button @click="loadUser">加载用户信息</button>
  </div>

  <!-- 引入依赖(Vue 2 + vue-router 3.x,确保版本兼容) -->
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <!-- 引入 Vuex 3.x(示例用 3.6.2 版本) -->
  <script src="https://cdn.jsdelivr.net/npm/vuex@3.6.2/dist/vuex.js"></script>

  <script>
    // 1. 注册 Vuex 插件(全局 Vue 实例可用)
    Vue.use(Vuex);

    // 2. 创建 store 实例
    const store = new Vuex.Store({
      state: {
        count: 0, // 共享状态:计数器
        user: null // 共享状态:用户信息
      },
      mutations: {
        // 同步修改 count
        INCREMENT(state) {
          state.count++;
        },
        // 同步设置用户信息
        SET_USER(state, user) {
          state.user = user;
        }
      },
      actions: {
        // 异步获取用户信息(模拟接口请求)
        fetchUser({ commit }) {
          return new Promise(resolve => {
            setTimeout(() => {
              const mockUser = { name: "张三", id: 123 };
              commit("SET_USER", mockUser); // 提交 mutation 修改状态
              resolve(mockUser);
            }, 1000);
          });
        }
      },
      getters: {
        // 计算属性:判断 count 是否为偶数
        isEven(state) {
          return state.count % 2 === 0;
        }
      }
    });

    // 3. 注入 store 到 Vue 实例
    new Vue({
      el: "#app",
      store, // 关键:注入全局 store
      methods: {
        loadUser() {
          // 触发 action(异步操作)
          this.$store.dispatch("fetchUser")
            .then(user => alert(`加载成功:${user.name}`));
        }
      }
    });
  </script>
</body>

Vuexstorestatemutationsactionsgettersmodules 五部分组成,各自职责明确:

概念 作用 特点
state 存储共享状态(类似组件的 data 唯一数据源,只读(不能直接修改)
mutations 修改 state 的唯一方法(同步操作) 必须是同步函数,通过 commit 触发
actions 处理异步操作(如接口请求) 可通过 dispatch 触发,可提交 mutation
getters state 进行计算(类似组件的 computed 缓存结果,依赖变化时自动更新
modules 拆分复杂状态为模块 每个模块可拥有独立的 state/mutations

服务端渲染

服务器渲染(SSR,Server-Side Rendering )是指 在服务器端将 Vue 组件渲染为完整的 HTML 字符串,再发送到客户端,客户端只需 "激活"(hydrate )这些 HTML 为可交互的 Vue 实例。相比客户端渲染(CSR ),SSR 主要解决 首屏加载慢 和 SEO 不友好 的问题。

  • CSR 的本质区别
    • 客户端渲染CSR):

服务器仅返回空 HTML (如 index.html ),客户端加载 JS 后由 Vue 动态渲染页面。

缺点:首屏白屏时间长(需等待 JS 下载执行),搜索引擎难以抓取动态内容。

html 复制代码
<html>
<head>
  <title>不使用 SSR(客户端渲染)</title>
  <!-- 故意延迟加载 Vue,模拟网络慢的情况 -->
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js" defer></script>
</head>
<body>
  <!-- 客户端渲染前,这里是空的 -->
  <div id="app"></div>

  <script>
    // 等待 Vue 加载完成后执行
    window.onload = function() {
      new Vue({
        el: '#app',
        data() {
          return {
            message: '客户端渲染的内容',
            count: 0
          }
        },
        template: `
          <div>
            <h1>{{ message }}</h1>
            <p>计数器: {{ count }}</p>
            <button @click="count++">+1</button>
          </div>
        `
      })
    }
  </script>
</body>
</html>

使用浏览器直接访问html页面,刷新显示效果.

  • 服务器渲染SSR):

服务器先执行 Vue 代码,生成包含完整内容的 HTML 并发送给客户端,客户端只需激活交互(绑定事件等)。

优势:首屏加载快(HTML 直接渲染),SEO 友好(内容在 HTML 中)。

html 复制代码
<!DOCTYPE html>
<html>
<head>
  <title>模拟使用 SSR(服务器渲染)</title>
  <!-- 同样延迟加载 Vue,模拟网络慢的情况 -->
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js" ></script>
</head>
<body>
  <!-- 服务器已预先渲染好的内容(模拟 SSR 输出) -->
  <div id="app">
    <h1>客户端渲染的内容</h1>
    <p>计数器: 0</p>
    <button>+1</button>
  </div>

  <script>
    // 等待 Vue 加载完成后激活(hydrate)
    window.onload = function() {
      new Vue({
        el: '#app',
        data() {
          return {
            message: '客户端渲染的内容', // 与服务器渲染内容一致,确保激活不冲突
            count: 0
          }
        },
        template: `
          <div>
            <h1>{{ message }}</h1>
            <p>计数器: {{ count }}</p>
            <button @click="count++">+1</button>
          </div>
        `
      })
    }
  </script>
</body>
</html>

SSR 的核心是 "服务端渲染 HTML + 客户端激活",通过 vue-server-renderer 实现渲染流程,解决首屏加载和 SEO 问题。

  • Nuxt.js

从头搭建一个服务端渲染的应用是相当复杂的。Nuxt.jsVue 官方推荐的服务端渲染框架,核心目标是简化 Vue 项目的 SSR 开发,同时支持静态站点生成(SSG),主打 "开箱即用" 的服务器渲染体验。

javascript 复制代码
// nuxt.config.js
export default {
  ssr: true, // 启用 SSR(默认 true,false 则为客户端渲染)
  modules: [
    '@nuxtjs/pwa' // 集成 PWA 模块
  ],
  pwa: {
    manifest: {
      name: 'Nuxt PWA App', // 应用名称
      lang: 'zh-CN'
    },
    workbox: {
      // 配置 Service Worker 缓存策略
      runtimeCaching: [{
        urlPattern: 'https://api.example.com/.*',
        handler: 'networkFirst' // 网络优先,离线缓存
      }]
    }
  }
}
  • Quasar Framework SSR + PWA

Quasar Framework (全平台 Vue 框架(含 SSR + PWA )) 是一个全栈 Vue 框架,核心特点是 "一次开发,多平台部署",支持 WebSPA/SSR/PWA)、移动端(通过 Cordova )、桌面端(通过 Electron )等场景,同时内置丰富的 UI 组件库。

javascript 复制代码
// quasar.config.js
module.exports = function (ctx) {
  return {
    // 启用 SSR
    ssr: {
      pwa: true // 在 SSR 中集成 PWA
    },
    // PWA 配置
    pwa: {
      workboxPluginMode: 'InjectManifest',
      workboxOptions: {},
      manifest: {
        name: 'Quasar App',
        short_name: 'Quasar',
        display: 'standalone', // 类似原生 App 的显示模式
        background_color: '#ffffff'
      }
    }
  }
}

安全

  • 第一原则:永远不要使用不可信任的模板

在使用 Vue 的时候最基本的安全规则是永远不要将不可信任的内容作为模板内容使用。这样做等价于允许在应用程序中执行任意的 JavaScript------甚至更糟的是如果在服务端渲染的话可能导致服务器被攻破。举个例子:

html 复制代码
<body>
  <div id="app">
  </div>

  <!-- 引入依赖(Vue 2 + vue-router 3.x,确保版本兼容) -->
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 模拟用户输入的不可信内容(可能包含恶意代码)
    const userInput = `
      <p>我是正常文本</p>
      <!-- 以下是恶意内容 -->
      <button @click="stealData()">点击领奖</button>
    `;

    // 危险操作:将用户输入直接作为模板
    new Vue({
      el: '#app',
      // 这里直接拼接用户输入到模板中,等价于执行恶意代码
      template: `<div>${userInput}</div>`,
      methods: {
        stealData() {
          // 模拟数据窃取逻辑(实际中可能发送用户信息到恶意服务器)
          alert('恶意代码执行:用户 cookie 已被获取!');
        }
      }
    });
  </script>
</body>

始终使用 v-textv-html(谨慎使用,仅用于完全可信的内容)或文本插值 {``{ }} 展示动态内容,避免将不可信内容作为模板的一部分编译。

  • 注入 URL

在类似这样的 URL 中:

html 复制代码
<body>
  <div id="app">
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 恶意用户输入的 URL
    const userProvidedUrl = 'javascript:alert("你的数据已被窃取");'

    // 危险操作:将用户输入直接作为模板
    new Vue({
      el: '#app',
      data:{
        // 将变量定义在 data 中,供模板使用
          userProvidedUrl: userProvidedUrl
      },
      // 这里直接拼接用户输入到模板中,等价于执行恶意代码
      template: `
      <a v-bind:href="userProvidedUrl">
        click me
      </a>`
    });
  </script>
</body>

如果没有对该 URL 进行"过滤"以防止通过 javascript: 来执行 JavaScript,则会有潜在的安全问题。

必须对用户提供的 URL 进行严格过滤,只允许安全的协议(如 http://https://mailto: 等),禁止 javascript:vbscript: 等危险协议。

html 复制代码
<body>
  <div id="app">
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 恶意用户输入的 URL
    const userProvidedUrl = 'javascript:alert("你的数据已被窃取");'

    // 危险操作:将用户输入直接作为模板
    new Vue({
      el: '#app',
      data: {
        // 将变量定义在 data 中,供模板使用
        userProvidedUrl: userProvidedUrl
      },
      // 这里直接拼接用户输入到模板中,等价于执行恶意代码
      template: `
      <a v-bind:href="filteredUrl">
        click me
      </a>`,

      computed: {
        filteredUrl() {
          const url = this.userProvidedUrl;
          if (!url) return '';

          // 1. 检测并禁止危险协议(核心过滤逻辑)
          const dangerousProtocols = ['javascript:', 'vbscript:', 'data:']; // 扩展需要禁止的协议
          const lowerUrl = url.toLowerCase();
          if (dangerousProtocols.some(protocol => lowerUrl.startsWith(protocol))) {
            return '#'; // 或返回一个安全的默认链接
          }

          // 2. 可选:强制添加 http/https 协议(如果用户输入的是域名,如 "example.com")
          if (!/^https?:\/\//i.test(url)) {
            return `https://${url}`;
          }

          return url;
        }
      }
    });
  </script>
</body>

只要你是在前端进行 URL 过滤,那么就已经有安全问题了。用户提供的 URL 永远需要通过后端在入库之前进行过滤。然后这个问题就会在每个客户端连接该 API 时被阻止,包括原生移动应用。还要注意,甚至对于被过滤过的 URLVue 仍无法帮助你保证它们会跳转到安全的目的地。

  • 注入样式

允许用户直接提供样式(如通过 v-bind:style 绑定用户输入的 userProvidedStyles ,或直接插入 <style> 元素内容)会带来严重的安全风险,甚至可能导致整个页面被恶意篡改。

html 复制代码
<body>
  <div id="app">
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 危险操作:将用户输入直接作为模板
    new Vue({
      el: '#app',
      data:{
        sanitizedUrl: '#',
      // 恶意用户提供的样式(未过滤)
      userProvidedStyles: {
        // 隐藏页面关键元素(如登录按钮、警告信息)
        position: 'fixed',
        top: '0',
        left: '0',
        width: '100%',
        height: '100%',
        backgroundColor: 'white',
        zIndex: '9999',
        // 伪装成官方登录框
        content: 'attr(data-phishing)', // 配合伪元素
        'data-phishing': '请输入密码:<input type="password">'
      }
      },
      // 这里直接拼接用户输入到模板中,等价于执行恶意代码
      template: `
      <a
        v-bind:href="sanitizedUrl"
        v-bind:style="userProvidedStyles"
      >
      click me
    </a>`
    });
  </script>
</body>

如果直接让用户输入 <style> 标签内的内容(如通过 v-html 渲染),风险会进一步放大,因为完整的 CSS 语法支持更危险的操作:

html 复制代码
<body>
  <div id="app">
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    const userProvidedStyleContent = `
      /* 隐藏原页面所有内容 */
      body > * { display: none !important; }
      /* 插入钓鱼表单 */
      body::after {
        content: '系统升级,请重新登录:<br><input type="text" placeholder="用户名"><br><input type="password" placeholder="密码"><br><button>登录</button>';
        display: block;
        padding: 20px;
        font-size: 16px;
      }
    `;

    new Vue({
      el: '#app',
      data: {
        sanitizedUrl: '#'
      },
      template: `<a :href="sanitizedUrl">click me</a>`,
      mounted() {
        // 攻击者可能通过直接操作 DOM 注入样式(绕过 Vue 模板限制)
        const styleTag = document.createElement('style');
        styleTag.innerHTML = userProvidedStyleContent; // 注入恶意样式
        document.head.appendChild(styleTag);
      }
    });
  </script>
</body>

允许用户控制样式(无论是 v-bind:style 还是 <style> 内容)本质上是将页面渲染控制权部分交给了不可信用户,风险极高。安全的做法是完全禁止用户提供原始样式,或通过极其严格的白名单和值校验限制样式范围,同时配合 CSP 等机制加固防护。

  • 注入 JavaScript

我们强烈不鼓励使用 Vue 渲染 <script> 元素,因为模板和渲染函数永远不应该产生副作用。

html 复制代码
<body>
  <div id="app">
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    new Vue({
      el: '#app',
      template: `
        <div>
          <!-- 尝试在模板中渲染<script>,会被 Vue 过滤 -->
          <script>alert('这段代码不会执行')</script>
          <p>页面内容</p>
        </div>
      `
    });
  </script>
</body>

每个 HTML 元素都有接受 JavaScript 字符串作为其值的 attribute,如 onclickonfocusonmouseenter。将用户提供的 JavaScript 绑定到它们任意当中都是一个潜在的安全风险,因此应该避免。

请注意,永远不要认为用户提供的 JavaScript 是 100% 安全的,除非它是在一个 iframe 沙盒里或者应用中只有编写该 JavaScript 的用户可以接触到它。

  • 最佳实践

通用的规则是只要允许执行未过滤的用户提供的内容 (不论作为 HTMLJavaScript 甚至 CSS ),你就可能令自己处于被攻击的境地。这些建议实际上不论使用 Vue 还是别的框架甚至不使用框架,都是成立的。

  • 后端协作

HTTP 安全漏洞,诸如伪造跨站请求 (CSRF/XSRF ) 和跨站脚本注入 (XSSI ),都是后端重点关注的方向,因此并不是 Vue 所担心的。尽管如此,和后端团队交流学习如何和他们的 API 最好地进行交互,例如在表单提交时提交 CSRF token,永远是件好事。

  • 服务端渲染 (SSR)

使用 SSR 时存在额外的安全考量,因此请确认遵循我们的 SSR 文档中概括出的最佳实践以避免安全漏洞。

深入响应式原理

Vue2 的响应式原理是其核心特性之一,它实现了数据与视图的自动同步:当数据发生变化时,视图会自动更新,反之亦然。这一机制的底层依赖 Object.defineProperty 对数据进行劫持,并通过 依赖收集派发更新 完成整个响应式流程。

javascript 复制代码
  <script>
    let student = {
      'name': '张三',
      'sex': '男'
    }
    Object.defineProperty(student, 'age', {
      value: 18,
      // 默认false,表示可枚举(遍历)
      enumerable: true,
      // 默认false,表示属性是否可以修改
      writable: true,
      // 默认值false,控制属性是否可以删除
      configurable: true,
    })
  </script>
  • 数据劫持

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setterObject.definePropertyES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。

javascript 复制代码
  <script>
    let student = {
      'name': '张三',
      'sex': '男'
    }
    Object.defineProperty(student, 'age', {
      // value和writable与get、set互斥
      // 默认false,表示可枚举(遍历)
      enumerable: true,
      // 默认值false,控制属性是否可以删除
      configurable: true,
      // 访问值
      get: function(){
        console.log("age被获取")
        return 18;
      },
      // 设置值
      set: function(value){
        console.log("age重新赋值");
        age = value
      }
    })
  </script>

执行结果如图:

数据代理就是一个对象代理另外一个对象的数据,示例代码如下:

javascript 复制代码
  <script>
    let student = {
      'name': '张三'
    }
    let student2 = {
      'age': 18
    }
    Object.defineProperty(student2, 'name', {
      get: function(){
        console.log("name被获取")
        return student.name;
      },
      // 设置值
      set: function(value){
        console.log("name重新赋值");
        student.name = value
      }
    })
  </script>

执行结果如图:

这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在属性被访问和修改时通知变更。这里需要注意的是不同浏览器在控制台打印数据对象时对 getter/setter 的格式化并不同,所以建议安装 vue-devtools 来获取对检查数据更加友好的用户界面。

  • 依赖收集

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把"接触"过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

检测变化的注意事项

由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。尽管如此我们还是有一些办法来回避这些限制并保证它们的响应性。

  • 对于对象

Vue 无法检测对象的添加或移除。由于 Vue 会在初始化实例时对 属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。

javascript 复制代码
var vm = new Vue({
  data:{
    a:1
  }
})

// `vm.a` 是响应式的
vm.b = 2
// `vm.b` 是非响应式的

对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式属性。

html 复制代码
<body>
  <div id="app">
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    var vm = new Vue({
      data: {
        someObject: {
          a: 1
        }
      }
    })
    Vue.set(vm.someObject, 'b', 2)
    console.log(vm.$data)
  </script>
</body>

您还可以使用 vm.$set 实例方法,这也是全局 Vue.set 方法的别名:

javascript 复制代码
this.$set(this.someObject,'b',2)
  • 对于数组

Vue 不能检测以下数组的变动,不会触发 setter

  • 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
  • 当你修改数组的长度时,例如:vm.items.length = newLength
html 复制代码
<body>
  <div id="app">
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    var vm = new Vue({
      data: {
        items: ['a', 'b', 'c']
      }
    })
    vm.items[1] = 'x' // 不是响应性的
    vm.items.length = 2 // 不是响应性的
    console.log(vm.items)
  </script>
</body>

为了解决第一类问题,以下两种方式都可以实现和 vm.items[indexOfItem] = newValue 相同的效果,同时也将在响应式系统内触发状态更新:

html 复制代码
<body>
  <div id="app">
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    var vm = new Vue({
      data: {
        items: ['a', 'b', 'c']
      }
    })
    // Vue.set
    Vue.set(vm.items, 1, 'x')
    console.log(vm.items)
  </script>
</body>

Vue 对数组的 7 个方法(pushpopshiftunshiftsplicesortreverse)进行了重写,调用这些方法会触发更新。

  • 声明响应式属性

由于 Vue 不允许动态添加根级响应式属性,所以你必须在初始化实例前声明所有根级响应式属性,哪怕只是一个空值:

html 复制代码
<body>
  <div id="app">
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    var vm = new Vue({
      data: {
        // 声明 message 为一个空值字符串
        message: ''
      },
      template: '<div>{{ message }}</div>'
    })
    // 之后设置 `message`
    vm.message = 'Hello!'
    console.log(vm.message)
  </script>
</body>

如果你未在 data 选项中声明 messageVue 将警告你渲染函数正在试图访问不存在的属性。

这样的限制在背后是有其技术原因的,它消除了在依赖项跟踪系统中的一类边界情况,也使 Vue 实例能更好地配合类型检查系统工作。但与此同时在代码可维护性方面也有一点重要的考虑:data 对象就像组件状态的结构 (schema)。提前声明所有的响应式属性,可以让组件代码在未来修改或给其他开发人员阅读时更易于理解。

  • 异步更新队列

可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环"tick"中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.thenMutationObserversetImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

例如,当你设置 vm.someData = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环"tick"中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用"数据驱动"的方式思考,避免直接接触 DOM ,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。例如:

html 复制代码
<body>
  <div id="app">{{message}}</div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    var vm = new Vue({
      el: '#app',
      data: {
        message: '123'
      }
    })
    vm.message = 'new message' // 更改数据
    vm.$el.textContent === 'new message' // false
    Vue.nextTick(function () {
      vm.$el.textContent === 'new message' // true
    })
  </script>
</body>

在组件内使用 vm.$nextTick() 实例方法特别方便,因为它不需要全局 Vue ,并且回调函数中的 this 将自动绑定到当前的 Vue 实例上:

html 复制代码
<body>
  <div id="app">
    <example></example>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    Vue.component('example', {
      template: '<span @click="updateMessage">{{ message }}</span>',
      data: function () {
        return {
          message: '未更新'
        }
      },
      methods: {
        updateMessage: function () {
          this.message = '已更新'
          console.log(this.$el.textContent) // => '未更新'
          this.$nextTick(function () {
            console.log(this.$el.textContent) // => '已更新'
          })
        }
      }
    })
    var vm = new Vue({
      el: '#app',
      data: {
      }
    })
  </script>
</body>

因为 $nextTick() 返回一个 Promise 对象,所以你可以使用新的 ES2017 async/await 语法完成相同的事情:

html 复制代码
<body>
  <div id="app">
    <example></example>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    Vue.component('example', {
      template: '<span @click="updateMessage">{{ message }}</span>',
      data: function () {
        return {
          message: '未更新'
        }
      },
      methods: {
        updateMessage: async function () {
          this.message = '已更新'
          console.log(this.$el.textContent) // => '未更新'
          await this.$nextTick()
          console.log(this.$el.textContent) // => '已更新'
        }
      }
    })
    var vm = new Vue({
      el: '#app',
      data: {
      }
    })
  </script>
</body>
相关推荐
ly_Enhs2 小时前
Vulkan 一句话心智词典(去恐惧版)
开发语言·vulkan图形渲染c/c++
xiaoxue..2 小时前
二叉搜索树 BST 三板斧:查、插、删的底层逻辑
javascript·数据结构·算法·面试
程序员小白条2 小时前
提前实习的好处有哪些?有坏处吗?
java·开发语言·数据结构·数据库·链表
ss2732 小时前
Executors预定义线程池-正确使用姿势
linux·开发语言·python
七夜zippoe2 小时前
Python高级数据结构深度解析:从collections模块到内存优化实战
开发语言·数据结构·python·collections·内存视图
lly2024062 小时前
Vue.js 过渡 & 动画
开发语言
石工记2 小时前
Java 作为主开发语言 + 调用 AI 能力(大模型 API / 本地化轻量模型)
java·开发语言·人工智能
wei yun liang2 小时前
4.数据类型
前端·javascript·css3
Ccuno2 小时前
Java虚拟机的内存结构
java·开发语言·深度学习