在Vue2项目里,组件化不只是拆模板,核心是数据派生、副作用、生命周期和通信边界。原文以Node.js运行环境为背景,使用axios@1.7.3请求Flask的/films接口,并演示computed、watch、生命周期、props/$emit、ref、动态组件和slot。下面按可落地实现重组。
Node.js是JavaScript运行环境和包管理环境,node可运行JavaScript文件或进入交互式命令行,npm用于安装依赖,TypeScript增加类型系统后通常编译成浏览器可执行的JavaScript。Vue、React等前端框架通常依赖Node.js完成依赖安装、代码打包和开发服务器启动;Node.js不等于Vue,也不一定要作为业务后端,它首先是JavaScript运行环境。
一、axios请求电影数据与CORS
Vue页面用axios.get('http://127.0.0.1:5000/films')获取JSON,response.data才是后端返回的响应体;loading用于防止重复点击,try/catch/finally让成功、失败和结束状态都得到处理,列表渲染使用后端稳定id作为key。- <div id="app">
- <button @click="loadFilms" :disabled="loading">{{ loading ? '加载中...' : '加载电影' }}</button>
- <p v-if="error">{{ error }}</p>
- <ul>
- <li v-for="film in films" :key="film.id">{{ film.name }}</li>
- </ul>
- </div>
- <script src="https://unpkg.com/axios@1.7.3/dist/axios.min.js"></script>
- <script src="./vue2/vue.js"></script>
- <script>
- new Vue({
- el: '#app',
- data: { films: [], loading: false, error: '' },
- methods: {
- async loadFilms() {
- this.loading = true
- this.error = ''
- try {
- const response = await axios.get('http://127.0.0.1:5000/films')
- this.films = response.data.results || []
- } catch (err) {
- console.error(err)
- this.error = '电影数据加载失败,请检查接口服务。'
- } finally {
- this.loading = false
- }
- }
- }
- })
- </script>
复制代码 Flask接口读取films.json并jsonify返回,演示跨域时设置Access-Control-Allow-Origin为http://localhost:8080;生产环境应配置明确域名,*不适合需要携带Cookie的请求,也不应在生产环境无限制开放。页面和接口的协议、域名或端口不一致时,浏览器会执行同源策略检查,服务端需要通过CORS响应头声明允许的来源。- import json
- from flask import Flask, jsonify
- app = Flask(__name__)
- @app.get("/films")
- def films():
- with open("films.json", encoding="utf-8") as file:
- data = json.load(file)
- response = jsonify(data)
- response.headers["Access-Control-Allow-Origin"] = "http://localhost:8080"
- return response
- if __name__ == "__main__":
- app.run(port=5000)
复制代码
二、computed:带缓存的派生值
computed本质上是一个带缓存的派生值,依赖的数据变化时才重新计算,依赖不变时会复用上次结果,模板中像普通属性一样使用,不需要写括号。它适合格式化文本、过滤列表、统计总价等由已有数据推导出的值。与方法相比,计算属性会缓存结果;不要为了展示派生值把过滤结果重复存进data。- new Vue({
- el: '#app',
- data: {
- search: '',
- words: ['a', 'at', 'atom', 'atommon', 'be', 'beyond', 'cs', 'csrf']
- },
- computed: {
- filteredWords() {
- const keyword = this.search.toLowerCase()
- return this.words.filter(word => word.includes(keyword))
- }
- }
- })
复制代码 模板中用v-model.trim="search"和v-for="word in filteredWords",key用word。首字母大写示例通过charAt(0).toUpperCase()+slice(1)实现,name为空则返回空串。
三、watch:执行副作用
watch用来监听数据变化并执行副作用,例如发送请求、保存草稿、记录日志或根据一个字段修改另一个字段;computed返回结果,watch执行动作。监听对象或数组内部变化时,Vue2需要使用deep:true;初始化时就希望执行一次,可以使用immediate:true。深度监听成本更高,应只监听确实需要的对象。- watch: {
- category(newValue, oldValue) {
- console.log(`${oldValue} -> ${newValue}`)
- // 这里可以根据新分类请求后端数据
- }
- }
复制代码 点击按钮修改category为“国产”“日韩”“欧美”,watch即可拿到新旧值并执行相应动作。
四、生命周期与资源清理
created可发初始化请求,mounted在DOM挂载后操作DOM;beforeDestroy、destroyed用于清理定时器、事件监听和资源。created中还不能可靠访问已经挂载的DOM;需要操作DOM尺寸、焦点等内容时使用mounted。组件销毁前不清理setInterval,定时器会继续执行。- Vue.component('child-panel', {
- template: `<section><h2>{{ name }}</h2><button @click="name = '新名字'">修改名字</button></section>`,
- data() { return { name: '子组件', timerId: null } },
- created() {
- this.timerId = setInterval(() => { console.log('定时任务') }, 3000)
- },
- mounted() { console.log('mounted:DOM 已插入页面') },
- beforeDestroy() {
- clearInterval(this.timerId)
- this.timerId = null
- }
- })
复制代码 配合v-if控制子组件显示和销毁,可观察组件创建与销毁时的生命周期日志。
五、组件注册与父子通信
组件data必须是返回对象的函数,保证每个组件实例拥有独立数据。Vue.component注册全局组件,注册后所有Vue实例和子组件都可以使用;new Vue的components注册局部组件,只能在当前实例模板中使用。实际.vue单文件组件项目更常使用import/export管理组件,全局注册虽然方便,但会增加命名冲突和维护成本。
父传子用props,HTML属性通常使用短横线user-name,在组件JavaScript中对应驼峰userName;:title表示绑定变量,title="固定文本"则表示传入字符串常量。Props是单向数据流,子组件不应直接修改父组件传入的值。- Vue.component('child-card', {
- props: {
- title: { type: String, required: true },
- userName: { type: String, default: '' }
- },
- template: `<section><h2>{{ title }}</h2><p>用户:{{ userName }}</p></section>`
- })
- new Vue({
- el: '#app',
- data: { pageTitle: '父组件传来的标题' }
- })
复制代码 父模板中使用<child-card :title="pageTitle" user-name="lqz"></child-card>。子传父用$emit触发事件并传递数据,父组件使用@事件名监听;$emit的事件名要与父组件监听的名称一致。复杂项目中可以进一步使用状态管理工具,避免组件层级过深时逐层传递。- Vue.component('child-editor', {
- data() { return { text: '' } },
- template: `<div><input v-model="text"><button @click="$emit('submit-message', text)">发送</button></div>`
- })
- new Vue({
- el: '#app',
- data: { message: '' },
- methods: {
- receiveMessage(value) { this.message = value }
- }
- })
复制代码 父模板用<child-editor @submit-message="receiveMessage"></child-editor>接收数据。
六、ref、动态组件与slot
ref放在普通标签上得到DOM对象,放在组件上得到组件实例,适合聚焦输入框、调用子组件暴露的方法等少量命令式操作,不应替代正常的props和事件通信;$refs只有在组件挂载后才可用,不能在created中依赖,直接修改子组件内部数据会提高耦合度。动态组件通过<component :is="组件名或组件对象">切换要渲染的组件,适合选项卡、步骤表单和不同状态页面;旧组件可能被销毁,如果希望保留组件内部输入内容和滚动状态,可以使用<keep-alive>包裹动态组件,并结合activated、deactivated钩子处理缓存状态。插槽让父组件向子组件提供模板内容,子组件负责定义布局,适合封装卡片、弹窗、布局容器等结构固定但内容可变的组件;默认插槽使用<slot>,具名插槽通过name区分。- Vue.component('base-card', {
- template: `<section class="card"><header><slot name="header">默认标题</slot></header><main><slot>默认内容</slot></main></section>`
- })
- new Vue({ el: '#app' })
复制代码 父组件在<base-card>内传入h2和p内容。动态组件切换示例中,current在home-panel与profile-panel之间变化,<component :is="current"></component>渲染对应组件。
总结:computed用于缓存派生值,watch用于执行副作用;created中可请求数据,mounted中操作DOM,销毁钩子中清理定时器和监听器;父子通信优先使用props与$emit,ref仅做少量命令式操作,动态组件负责切换,插槽负责布局复用。可用电影列表、选项卡、父子消息传递和可复用卡片四个练习验证这些边界。 |