admin管理员组文章数量:1568418
说明:Vuejs进阶版继承Vuejs基础版
上一篇:Vuejs基础版
下一篇:Vuejs补充版
目录大纲二
- 十一、组件化开发
- 1、组件化的实现和使用步骤
- 1.1、什么是组件化?
- 1.2、Vue组件化思想
- 1.3、注册组件的基本步骤
- 1.4、组件创建原理
- 2、全局组件和局部组件
- 3、父组件和子组件
- 4、注册组件语法糖
- 4.1、全局组件注册的语法糖
- 4.2、局部组件注册的语法糖
- 5、组件模板抽离写法
- 6、父子组件的通信
- 7、父传子——props数据验证
- 8、子传父——自定义事件
- 9、父子组件的访问
- 9.1、父组件访问子组件:使用 $children 或 $refs
- 9.2、子组件访问父组件:使用 $parent 或 $root
- 10、插槽 slot :抽取共性,保留不同
- 10.1、插槽的基本使用
- 10.2、具名插槽的使用
- 10.3、编译作用域与作用域插槽的使用
- 十二、模块化开发
- 1、CommonJS 导入与导出
- 2、ES模块化的导入与导出
- 2.1、导出
- 导出变量
- 2.2、导入
- 2.3、export default
- 十三、webpack
- 1、初识 webpack
- 2、安装使用 webpack
- 2.1、webpack 的基本使用
- 2.2、webpack.config.js配置和package.json配置
- 3、webpack中使用css文件的配置
- 4、less 文件处理
- 5、webpack-图片文件处理
- 6、ES6语法转换成ES5 - babel
- 7、webpack 配置Vue
- 7.1、安装配置使用Vue
- 7.2、组件中使用组件
- 7.3、小Tips:在导入文件时省略后缀名,需在`webpack.config.js`中做修改:
- 8、webpack 的 plugin插件
- 8.1、添加版权的 Plugin
- 8.2、打包 html 的 plugin
- 8.3、js压缩的 Plugin
- 8.4、小TIps:使用该插件压缩js时,会将注释也删除,所以添加版权的插件与该插件两者不互存
- 9、webpack-dev-server搭建本地服务器
- 10、webpack 配置文件的分离
- 十四、Vue CLI 脚手架
- 1、vuecli脚手架介绍和安装
- 1.1、介绍
- 1.2、安装
- 1.3、目录结构详解
- 2、Vue程序运行过程
- 3、VueCLI3
- 3.1、VueCLI 创建项目和目录
- 3.2、目录结构详解
- 3.3、vueCLI3配置文件的查看和修改
- 4、箭头函数的使用和 this 的指向
- 十五、Vue-Router
- 1、认识路由
- 1.1、URL 的 hash
- 1.2、HTML5 的 history 模式:pushState
- 1.3、HTML5 的 history 模式:replaceState
- 1.4、HTML5 的 history 模式:go
- 2、vue-router 基本使用
- 2.1、安装和使用 vue-router
- 2.1、路由的默认路径
- 2.3、router-link 属性
- 3、vue-router 动态路由
- 3.1、路由的懒加载
- 3.2、懒加载的方式
- 4、vue-router 嵌套路由
- 4.1、实现嵌套路由的两个步骤
- 5、vue-router 参数传递
- 5.1、准备工作
- 5.2、传递参数的方式
- 5.3、传递参数 —— JavaScript代码
- 6、vue-router 导航守卫
- 7、keep-alive
- 十六、TabBar
- 1、TarBar案例的实现
- 2、TarBar案例的实现思路
- 3、给 TabBarItem 传入 active 图片
- 十七、Promise 的使用
- 1、Promise的三种状态
- 2、Promise链式调用的三种简写
- 3、Promise 的 all 方法的使用
- 十八、Vuex
- 1、单界面状态管理的实现
- 2、多界面状态管理
- 3、插件 vuex-devtools
- 4、mutations
- 4.1、mutations 是定义方法
- 4.2、Mutation中的参数
- 5、vuex - getters
- 6、vuex-数据的响应式原理
- 响应式管理
- 7、vuex-actions
- 8、vuex-modules
- 8.1、是什么
- 8.2、怎么做
- 8.3、vuex-modules中的相关属性
- 9、对象的解构
- 10、Vuex的安装与使用
十一、组件化开发
1、组件化的实现和使用步骤
1.1、什么是组件化?
如果将一个复杂的问题,拆分成很多个可以处理的小问题,再将其放在整体当中,就会将问题迎刃而解
组件的定义:实现应用中局部功能代码和资源的集合
组件化思想:将一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么整个页面的管理和维护就变得非常容易了
1.2、Vue组件化思想
组件树:
1.3、注册组件的基本步骤
组件的使用分成三个步骤:
- 创建组件构造器
- 注册组件
- 使用组件
- Vue.extend():
- 调用 Vue.extend() 创建的是一个组件构造器
- 通常在创建组件构造器时,传入 template 代表我们自定义组件的模板
- 该模板就是在使用到组件的地方,要显示的HTMl代码
- Vueponent():
- 调用Vueponent() 是将刚才的组件构造器注册为一个全局组件,并且给它起一个组件的标签名称。
- 所以需要传递两个参数:1、注册组件的标签名 2、组件构造器
- 组件必须挂载在某个Vue实例下,否则它不会生效
1.4、组件创建原理
Vue组件创建原理:https://blog.csdn/qq_53810245/article/details/122894319
2、全局组件和局部组件
3、父组件和子组件
组件嵌套:https://blog.csdn/qq_53810245/article/details/122891352
app 管理所有父组件,父组件中再嵌套子组件
理解:在root组件中定义父级组件,在父级组件中定义子组件,从而形成调用关系
4、注册组件语法糖
4.1、全局组件注册的语法糖
4.2、局部组件注册的语法糖
5、组件模板抽离写法
一般使用第二张模板
6、父子组件的通信
在开发中,旺旺一些数据需要从上层传递到下层:比如在一个页面中,我们从服务器请求到了很多的数据。其中一部分数据,并非是我们整个页面的大组件来展示,而是需要下面的子组件进行展示,这个时候,并不会让子组件再次发送一个网络请求,而是直接让大组件(父组件)将数据传递给小组件(子组件)
通过 props 【properties属性】向子组件传递数据,通过事件向父组件发送消息
7、父传子——props数据验证
<div id="app">
<cpn :c-Info="info"></cpn>
</div>
<template id="cpn">
<h2>{{cInfo.name}}</h2>
</template>
<script src="../js/vue.js"></script>
<script>
const cpn = {
template:'#cpn',
props:{
cInfo:{
type:Object,
default() {
return {};
}
}
}
}
const app = new Vue({
el: '#app',
data: {
info:{
name:'why',
age:18,
height:1.88
}
},
components:{
cpn
}
})
</script>
8、子传父——自定义事件
当子组件需要向父组件传递数据时,就要用到自定义事件】
自定义事件的流程
- 在子组件中,通过
$emit()
来触发事件 - 在父组件中,通过
v-on
来监听子组件事件
<!--父组件的模板-->
<div id="app">
<cpn @itemclick="cpnClick"></cpn>
</div>
<!--子组件模板-->
<template id="cpn">
<div>
<button v-for="item in categories"
@click="btnClick(item)">
{{item.name}}
</button>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
// 1. 子组件
const cpn = {
template:'#cpn',
data(){
return{
categories:[
{id:101,name:'热门推荐'},
{id:102,name:'手机数码'},
{id:103,name:'电脑办公'},
{id:104,name:'家用家电'},
]
}
},
methods:{
btnClick(item){
// console.log(item.id);
// 发射事件:自定义事件,自定义发出的函数,自定义的参数
this.$emit('itemclick',item);
}
}
}
// 2. 父组件
const app = new Vue({
el:'#app',
components:{
cpn
},
methods: {
cpnClick(item){
console.log('cpnClick',item)
}
}
})
</script>
9、父子组件的访问
9.1、父组件访问子组件:使用 $children 或 $refs
- $ children 的访问
- this.$children 是一个数组类型,它包含所有子组件对象![在这里插入图片描述](https://img-blog.csdnimg/e7bade92e09946d1a75aaf8dd64fd56d.png)
9.2、子组件访问父组件:使用 $parent 或 $root
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>子访问父-parent-root</title>
</head>
<body>
<!--父组件的模板-->
<div id="app">
<cpn></cpn>
</div>
<!--子组件的模板-->
<template id="cpn">
<div>
<h2>我是子组件</h2>
<button @click="btnClick">按钮</button>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊',
},
// 注册子组件
components:{
cpn:{
template:'#cpn',
methods:{
btnClick(){
// 1.访问父组件 $parent,获取到的层级关系是VueComponents,以上为Vue实例
// console.log(this.$parent);
// 2. 访问根组件$root,获取到的是Vue实例
console.log(this.$root.message);
}
}
}
},
})
</script>
</body>
</html>
10、插槽 slot :抽取共性,保留不同
- slot 译为插槽,例如电脑的USB插槽、插板中的电源插槽
- 插槽的目的是让我们原有的设备更多的扩展性
- 组件的插槽
- 组件的插槽是为了让我们封装的组件更加具有扩展性
- 让使用者可以决定组件内部的一些内容到底展示什么
- 插槽的应用:
- 移动网站中的导航栏
- 导航栏会封装成一个插件,比如 nav-bar 组件
- 一旦有了这个组件,我们就可以在多个页面中复用了
10.1、插槽的基本使用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>slot-插槽的基本使用</title>
</head>
<body>
<div id="app">
<cpn ref="ref"></cpn>
<cpn><button>跳转</button></cpn>
<cpn><button>点击</button></cpn>
<cpn><input type="text"></cpn>
</div>
<template id="cpn">
<div>
<h2>我是子组件</h2>
<p>我是组件哈哈哈</p>
<!-- 预留插条,并在cpn标签下进行自行定义使用-->
<!-- <slot></slot>-->
<!-- 插条中也可以使用 默认的值,如重新定义插条中的内容即可覆盖原默认内容-->
<slot><button>按钮</button></slot>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊',
},
methods:{},
// 注册组件
components:{
cpn:{
template:'#cpn',
data(){
return{}
}}}
})
</script>
</body>
</html>
10.2、具名插槽的使用
- 具名插槽:具有名字的插条
- 作用:针对 slot 插槽编排名字,可以实现指定替换
<div id="app">
<AAA>
<span slot="center">更改</span>
</AAA>
</div>
<template id="cpn">
<!-- <div>
<h2>这行代码我是不会更改的</h2>
<slot><button>默认按钮</button></slot>
</div>-->
<div>
<slot name="left"><span>左</span></slot>
<slot name="center"><span>中</span></slot>
<slot name="right"><span>右</span></slot>
<slot></slot>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊',
},
components:{
cpn:{
template:'#cpn'
}
}
})
- 如果想要在插槽中存放多个内容但又不想用一个div将他们包裹起来,可以用如下方法,但必须添加绑定
- 父组件
- 子组件
10.3、编译作用域与作用域插槽的使用
-
编译作用域:在哪里定义的变量只会在它的作用域下进行查找调用
-
官方解释:父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子集作用域内编译
-
作用域插槽的使用:父组件替换插槽的标签,但是内容由子组件来提供
-
例如:
十二、模块化开发
前言:随着 ajax 异步请求的出现,慢慢形成了前后端的分离
模块化的核心:导入与导出
1、CommonJS 导入与导出
2、ES模块化的导入与导出
导入导出使用前提:
2.1、导出
导出变量
- 方式一:边定义边导出
export var num = 1000 export let name = 'why' export const height = 1.88 // 导出函数/类: export function sum(num1,num2){ return num1 + num2 } export class Person{ constructor(name,age) { this.name = name; this.age = age; } run(){ console.log(this.name + '在奔跑'); } }
- 方式二:先定义后导出
var num = 1000 let name = 'why' const height = 1.88 // 导出函数/类: function sum(num1,num2){ return num1 + num2 } class Person{ constructor(name,age) { this.name = name; this.age = age; } run(){ console.log(this.name + '在奔跑'); } } export {num,name,height,sum,Person}
2.2、导入
import{num1,height} from "./aaa.js";
或 统一全部导入
import * as all from "./aaa.js";
console.log(all.flag)
2.3、export default
- 某些情况下,给函数或功能不命名,在导入的时候导入者自己命名
需注意: export defalut 在同一个模块中,不允许同时存在多个
export default function (args){
console.log(args);
}
导入
import fun from './aaa.js'
fun('快快打印')
十三、webpack
webpack 是JavaScript应用的静态模块打包工具
webpack 为了可以正常运行,必须依赖 node 环境,node环境为了可以正常的执行很多代码,必须其中包含各种依赖的包, npm 工具就是 node packages manager
1、初识 webpack
前端模块化的方案:AMD、CMD、CommonJs、ES6
webpack 与 grunt / gulp 的对比:
- grunt / gulp 的核心是 Task
- 配置一系列的 task,并且定义 task 要处理的事务(例如ES6、ts转化、图片压缩、scss转成css)
- 之后让 grunt / gulp 来依次执行这些task,而且让整个流程自动化,grunt / gulp 也被称为前端自动化任务管理工具
- 举例:gulp 的task
- 下面的 task 就是将src下面的所有js文件转成ES5的语法,并且最终输出到dist文件夹中
const gulp = require('gulp');
const babel = require('gulp-babel');
gulp.task('js',()=>
gulp.src('src/*.js')
.pipe(babel({
presets:['es2015']
}))
.pipe(gulp.dest('dist'))
);
- 什么时候用 grunt / gulp ?
- 如果你的工程模块依赖非常简单,甚至没有用到模块化的概念
- 只需要进行简单的合并、压缩,就使用 grunt / gulp 即可
- 但是如果整个项目使用了模块化管理,而且相互依赖非常强,我们就可以使用更加强大的 webpack 了
- grunt / gulp 和 webpack 有什么不同呢?
- grunt / gulp 更加强调的是前端流程的自动化,模块化不是它的核心
- webpack 更加强调模块化开发管理,而文件压缩合并、预处理等功能,是他附带的功能
2、安装使用 webpack
2.1、webpack 的基本使用
切记:命令行敲命令一定要在对应的目录文件下才能生效
- 查看node版本:
node -v
- 全局安装 webpack,指定自己依赖的版本号:
npm install webpack@3.6.0 -g
- 查看webpack版本:
webpack --version
- 该文件夹下建立
dist文件夹
以及src/main.js
文件和src/mathUtils.js
文件和index.html
文件
mathUtils.js
function add(num1,num2){
return num1 + num2
}
function mul(num1,num2){
return num1 * num2
}
// 1、commonjs 导出
module.exports = {
add,mul
}
info.js
// ES6的导出语法
export const name = 'why';
export const age = 18;
export const height = 1.88;
main.js
// 1、使用commonjs的模块化导入
const {add,mul} = require('./mathUtils')
// 打印
console.log(add(1, 2));
console.log(mul(2, 3));
// 2、使用ES6的模块化导入
import {name, age, height} from './info';
console.log(name, age, height);
- 一般的启动方式:
webpack .\src\main.js .\dist\bundle.js
直接从 index.html 中引入即可:<script src="./dist/bundle.js"> </script>
2.2、webpack.config.js配置和package.json配置
-
在该文件夹下新建
webpack.config.js
文件// 导入模块,从node包里进行导入 const path = require('path') module.exports = { //入口:可以是字符串/数组/对象/,这里的入口只写一个,所以写一个字符串即可 entry:'./src/main.js', //出口:通常是一个对象,里面至少包含两个重要属性,path和filename output:{ // 动态的获取路径,此处获取的是绝对路径。resolve()对路径进行拼接,__dirname获取当前路径 path: path.resolve(__dirname,'dist'), filename:'bundle.js' } }
-
npm初始化,生成package.json:
npm init
-
根据package.json 文件中的依赖进行相对应的安装:
npm install
-
如果想要执行 npm run build,,需要先在package.json文件中添加至脚本script中
"scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack" },
-
局部安装webpack,开发时依赖
第一步:局部安装 cd 对应目录 npm install webpack@3.6.0 --save-dev 第二步:启动webpack ,使用局部的 webpack node_moules/.bin/webpack
3、webpack中使用css文件的配置
踩坑:1、要在该文件夹目录下进行局部配置,2、使用管理员身份运行webstorm
loader 使用过程:点击跳转官网学习
-
在src目录中创建一个 css 文件,在其中创建一个normal.css文件
-
步骤一:通过 npm 安装需要使用的 loader
npm install --save-dev css-loader@2.0.2
-
步骤二:在 webpack.config.js 下进行配置
module.exports = { module: { rules: [ { test: /\.css$/, use: ["style-loader", "css-loader"], }, ], }, }
-
在 main.js 中导入
// 1、使用 commonjs 的模块化规范 const {add,mul} = require('./js/mathUtils.js') console.log(add(20, 30)); console.log(mul(20, 30)); // 2、 使用ES6的模块化的规范 import {name,height,age} from "./js/info.js"; console.log(name); console.log(age); // 3.依赖css文件 require('./css/normal.css')
-
安装 style-loader
npm install --save-dev style-loader@0.23.1
-
npm run build 运行
4、less 文件处理
项目中会使用 less、css、stylus来写样式
-
创建一个 css / special.less 文件,放在 css 文件中
@fontSize:50px; @fontColor:orange; body{ font-size: @fontSize; color: @fontColor; }
-
main.js 中引入
// 1、使用commonjs的模块化导入 const {add,mul} = require('./js/mathUtils') // 使用 console.log(add(1, 2)); console.log(mul(2, 3)); // 2、使用ES6的模块化导入 import {name, age, height} from './js/info'; console.log(name, age, height); //3.依赖 css 文件 require('./css/normal.css') // 4.依赖less文件 require('./css/special.less') document.writeln('<h2>余婷的背景是红色</h2>')
-
安装 loader:
npm install --save-dev less-loader@4.1.0 less@3.9.0
-
在 webpack.config.js 文件中的 rules 中选项配置相关内容
// webpack.config.js module.exports = { ... module: { rules: [{ test: /\.less$/, use: [{ loader: "style-loader" // creates style nodes from JS strings }, { loader: "css-loader" // translates CSS into CommonJS }, { loader: "less-loader" // compiles Less to CSS }] }] } };
5、webpack-图片文件处理
-
在n ormal.css 文件中引入图片路径如下:
body{ background:url("../img/test.jpg"); }
-
安装 loader:
npm install --save-dev url-loader
-
在 webpack.config.js 中进行配置 rules 选项
{ test: /\.(png|jpg|gif)$/, use: [ { loader: 'url-loader', options: { limit: 8192 } } ] }
-
当图片小于 8kb 时
-
当图片大于 8kb 时,就要安装 file-loader:
npm install --szve-dev file-loader
-
再次打包
npm run build
时,就会发现dist文件下多了一个图片文件
6、ES6语法转换成ES5 - babel
-
babel 的使用:
npm install --save-dev babel-loader@7 babel-core@6.26.3 babel-preset-es2015@6.24.1
-
配置 webpack.config.js 文件,rules 选项中添加如下配置
rules: [ { test: /\.js$/, // exculde:排除,include:包含 exclude: /(node_modules|bower_components)/, use: { loader: 'babel-loader', options: { presets: ['es2015'] } } } ]
-
使用
npm run build
打包执行,发现所有ES6语法皆变成了ES5的语法
7、webpack 配置Vue
7.1、安装配置使用Vue
-
npm 安装 Vue :
npm install vue@2.5.21 --save
-
【该步骤已被替换,可直接进行下一步操作】新建 src / vue / app.js 文件,并写入组件内容并导出
export default { template:` <div id="app"> <h2>我正在学习Vue开发{{message}}</h2> <button @click="btnClick">按钮</button> <h2>{{name}}正在学习Vue开发</h2> </div>`, data(){ return{ message:'Hello Webpack', name:'Mark' } }, methods:{ btnClick(){ console.log('被点击事件'); } } }
-
【该步骤已被替换,可直接进行下一步操作】新建 src / vue / App.vue 的vue component文件,并在相应内容块中写入相应组件内容
<template> <div> <h2 class="title">我正在学习Vue开发{{message}}</h2> <button @click="btnClick">按钮</button> <h2>{{name}}正在学习Vue开发</h2> </div> </template> <script> export default { name: "App", data(){ return{ message:'Hello Webpack', name:'Mark' } }, methods:{ btnClick(){ console.log('被点击事件'); } } } </script> <style scoped> .title{ color: green; } </style>
-
在 main.js 文件中导入 Vue 及组件 APP 使用
// 5.使用vue开发 import Vue from 'vue' // import App from './vue/app' 该方法已被注释,同上 import App from "./vue/App.vue"; new Vue({ el:'#app', template:`<App/>`, // 注册组件 components:{ App } })
-
在页面 index.html 中写入id为app的挂载
<div id="app"></div>
-
在 webpack.config.js 中配置
model.exports= { resolve:{ // alias:别名 alias: { 'vue$': 'vue/dist/vue.esm.js' } } }
-
安装 vue-loader 和 vue-template-compiler 作为解析:
npm install vue-loader@13.0.0 vue-template-compiler@2.5.21 --save-dev
-
执行 npm run build 进行运行
7.2、组件中使用组件
-
新建 src / vue / Cpn.vue 的vue component文件
<template> <div> <h2>我是组件中的小组件</h2> <p>我是Cpn组件的内容</p> <h2>{{name}}</h2> </div> </template> <script> export default { name: "Cpn", data(){ return{ name:'Cpn组件的name' } } } </script> <style scoped> </style>
-
新建 src / vue / App.vue 的vue component文件
<template> <div> // 原组件内容 <h2 class="title">我正在学习Vue开发{{message}}</h2> // 使用字组件 <Cpn></Cpn> </div> </template> <script> // 导入子组件 Cpn import Cpn from './Cpn.vue' export default { name: "App", components:{ Cpn }, data(){ return{ message:'Hello Webpack', } }, } </script> <style scoped> </style>
7.3、小Tips:在导入文件时省略后缀名,需在webpack.config.js
中做修改:
8、webpack 的 plugin插件
plguin 是插件的意思,通常是用于对某个现有的架构进行扩展
webpack 中的插件,就是对webpack 现有功能的各种扩展,比如打包优化,文件压缩等等
loader 和 plugin 区别
- loader 主要用于转换某些类型的模块,它是一个转换器
- plugin 是插件,它是对 webpack 本身的扩展,是一个扩展器
plugin 的使用过程:
步骤一:通过 npm 安装需要使用的 plugins (某些webpack已经内置的插件不需要安装)
步骤二:在webpack.config.js中的plugins中配置插件
8.1、添加版权的 Plugin
- 导入 webpack 模块并使用Plugins设置版权
- bundle.js 文件效果为:
/*! 最终版权归aaa所有 */
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
。。。。。。
8.2、打包 html 的 plugin
-
HtmlWebpackPlugin插件可以自动生成一个 index.html(可以指定模板来生成);并将打包的 js 文件,自动通过 script 标签插入到 body中
-
安装:
npm install html-webpack-plugin@3.2.0 --save-dev
-
注释掉默认文件路径
-
使用:在 webpackconfig文件下修改如下,并
npm run build
运行
8.3、js压缩的 Plugin
-
使用第三方插件
npm install uglifyjs-webpack-plugin@1.1.1 --save-dev
-
修改 webpack.config.js文件,使用插件:然后执行 npm run build
-
查看打包后的 bunlde.js 文件,是已经被压缩过的
8.4、小TIps:使用该插件压缩js时,会将注释也删除,所以添加版权的插件与该插件两者不互存
9、webpack-dev-server搭建本地服务器
-
安装:
npm install webpack-dev-server@2.9.3 --save-dev
-
配置 webpack.config.js 文件
// 导入模块,从node包里进行导入 const path = require('path') const webpack = require('webpack') const HtmlWebpackPlugin = require('html-webpack-plugin') const UglifyjsWebpackPlugin = require('uglifyjs-webpack-plugin') module.exports = { entry:'./src/main.js', resolve: {...}, output:{...}, module: {...}, plugins:[...], devServer:{ contentBase:'./dist',// 为哪一个文件夹提供本地服务,默认是根文件夹./dist inline:true, // 页面实时刷新 /*以下内容可添可不添 port:端口号,默认跑在8080端口 historyApiFallback:在SPA页面中,依赖HTML5的history模式 */ } }
-
在package.json文件中添加如下脚本,
--open
可直接将生成的链接打开:
-
npm run server 执行文件,在生成的端口如下
-
优点是不再重新打包运行,而是当调试完毕后在运行 npm run build进行打包
10、webpack 配置文件的分离
-
进行抽离:开发时依赖一个文件,运行时依赖另一个文件,即用三个配置文件完成一个配置文件的使用
-
建立文件夹build用来存放配置文件,基础配置
base.config.js
文件,开发时的配置dev.config.js
文件,生产时的配置prod.config.js
文件 -
即开发时要用
dev.config.js + base.config.js
,生产时要用prod.config.js + base.config.js
-
抽离完之后,使用他们就需要合并文件,分支合并到主干需要用到插件:
npm install webpack-merge@4.1.5 --save-dev
-
合并与导出如下,
prod.config.js
和dev.config.js
文件都需要执行合并与导出prod.config.js 文件 const baseConfig = require('./base.config') // 将合并后的内容导出 module.exports = webpackMerge(baseConfig,{ plugins:[ new UglifyjsWebpackPlugin() ] })
dev.config.js 文件 // 导入所需要的模块 const webpackMerge = require('webpack-merge') const baseConfig = require('./base.config') module.exports = webpackMerge(baseConfig,{ devServer:{ contentBase:'./dist', inline:true } })
-
在 package.json文件中配置如下属性:
-
改动生成的dist文件的路径如下:修改base.config.js中的path
十四、Vue CLI 脚手架
对文章进行了重新整理,详情点击跳转:https://blog.csdn/qq_53810245/article/details/122901321
1、vuecli脚手架介绍和安装
1.1、介绍
Vue CLI 官网:https://cli.vuejs/zh/
- 使用 Vue.js 开发大型应用,需要考虑代码目录结构、项目结构和部署、热加载、代码单元测试等事情。我们通常会使用一些脚手架工具来帮助完成这事情
- CLI 是 Command-Line Interface,译为命令行界面,俗称脚手架
- 使用 vue-cli 可以快速搭建Vue开发环境以及对应的webpack配置
1.2、安装
环境要求:Node 环境要求 8.9 以上
- 安装 Vue 脚手架:
npm install -g @vue/cli@3.2.1
- 拉取 2.x 模板(旧版本):
npm install -g @vue/cli-init@3.2.0 # `vue init`的运行效果将会跟`vue-cli@2.x`相同 vue init webpack my-project
- Vue CLI2 初始化项目:
vue init webpack my-project
Vue CLI3 初始化项目:vue create my-project
Vue 全家桶:VueCore + vue-route + vuex - 创建Vue项目:
vue init webpack vuecli2test
相关配置如下:
1.3、目录结构详解
2、Vue程序运行过程
3、VueCLI3
3.1、VueCLI 创建项目和目录
3.2、目录结构详解
3.3、vueCLI3配置文件的查看和修改
启动配置服务器——vue图形化GUI界面:vue ui
4、箭头函数的使用和 this 的指向
箭头函数的使用:当我们准备用一个函数作为参数传到另外一个函数中使用时
点击链接查看详解
十五、Vue-Router
1、认识路由
路由:通过互联网的网络把信息从源地址传输到目的地址的活动
官网:https://router.vuejs/zh/
1.1、URL 的 hash
1.2、HTML5 的 history 模式:pushState
1.3、HTML5 的 history 模式:replaceState
1.4、HTML5 的 history 模式:go
2、vue-router 基本使用
- vue-router 是Vue.js 官方的路由插件,它和vue.js是深度集成的,适合用于构建单页面应用
- 官方网站为:https://router.vuejs/zh/
- vue-router 是基于路由和组件的,路由用于设定访问路径,将路径和组件映射起来
- 在 vue-router 的单页面应用中,页面的路径的改变就是组件的切换
2.1、安装和使用 vue-router
- 安装 vue-router:
npm install vue-router --save
- 在模块化工程中使用它(因为是一个插件,所以可以通过Vue.use()来安装路由功能)
- 第一步:导入路由对象,并且调用 Vue.use(VueRouter)
- 第二步:创建路由实例,并且传入路由映射配置
- 第三步:在Vue实例汇总挂载创建的路由实例
- 使用 vue-router 的步骤:
-
第一步:创建路由组件
learnvuerouter/src/components/Home.vue
<template> <div> <h2>我是关于</h2> <p>我是首页内容,哈哈哈</p> </div> </template> <script> export default { name: "home" } </script> <style scoped> </style>
-
第二步:配置路由映射,
learnvuerouter/src/router/index.js
import Vue from 'vue' import Router from 'vue-router' // import HelloWorld from '@/components/HelloWorld' import Home from '../components/Home' import About from "../components/About"; // 1.通过 Vue.use(插件),安装插件 Vue.use(Router) // 2.创建VueRouter对象 const routes = [ { path: '/home', component: Home }, { path:'./about', component:About } ] export default router
-
第三步:使用路由:通过
<router-link>
和<router-view>
,learnvuerouter/src/router/index.js
<template> <div id="app"> <router-link to="/home">首页</router-link> <router-link to="/about">关于</router-link> <router-view></router-view> </div> </template> <script> export default { name: 'App' } </script> <style> </style>
-
2.1、路由的默认路径
2.3、router-link 属性
<router-link to="/home">首页</router-link>
:用于指定跳转的路径<router-link to="/about" tag="button">关于</router-link>
:指定<router-link>
之后渲染成什么组件,默认是 a标签,但此处更改为 button 标签<router-link to="/home" replace>首页</router-link>
:在指定 replace 的情况下,后退键返回不能返回到上一个页面中
3、vue-router 动态路由
3.1、路由的懒加载
- 当打包构建应用时,Javascript 包会变得非常大,影响页面加载
- 如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效
- 路由懒加载的主要作用就是将路由对应的组件打包成一个个的js代码块
- 只有在这个路由被访问到的时候,才加载对应的组件
- 未进行路由懒加载的效果
- 进行了路由懒加载之后的效果
3.2、懒加载的方式
const Home = ()=> import('../components/Home')
E:\html\WebStorm\project\project_Vuejs\LearnVuejs03\learnvuerouter\src\router\index.js 下修改使用懒加载方式
import Vue from 'vue'
import Router from 'vue-router'
// import HelloWorld from '@/components/HelloWorld'
/*
import Home from '../components/Home';
import About from "../components/About";
import User from "../components/User";
*/
// 路由懒加载方式导入路由
const Home = ()=> import('../components/Home')
const About = ()=> import('../components/About')
const User = ()=> import('../components/User')
// 1.通过 Vue.use(插件),安装插件
Vue.use(Router)
// 2.创建VueRouter对象
const routes = [
{
// 默认路径
path:'',
// redirect 重定向
redirect:'/home'
},
{
path: '/home',
component: Home
},
{
path:'/about',
component:About
},
{
path:'/user/:userId',
component: User
}
]
const router = new Router({
// 配置路由和组件之间的应用关系
routes,
mode:'history'
})
/*export default new Router({
routes: [
]
})*/
// 3.将 router 对象传入到Vue实例
export default router
4、vue-router 嵌套路由
4.1、实现嵌套路由的两个步骤
-
创建对应的子组件E:\html\WebStorm\project\project_Vuejs\LearnVuejs03\learnvuerouter\src\components\HomeNews.vue
<template> <div> <ul> <li>新闻1</li> <li>新闻2</li> <li>新闻3</li> <li>新闻4</li> </ul> </div> </template> <script> export default { name: "HomeNews" } </script> <style scoped> </style>
-
在路由映射中配置对应的子路由,可以添加默认路径作为默认显示:E:\html\WebStorm\project\project_Vuejs\LearnVuejs03\learnvuerouter\src\router\index.js
// 路由懒加载方式导入路由 const Home = ()=> import('../components/Home') const HomeNews = ()=>import('../components/HomeNews') const HomeMessage = ()=>import('../components/HomeMessage') ... const routes = [ /*添加默认路径*/ { path: '', redirect: 'news' }, { // 默认路径 path:'', // redirect 重定向 redirect:'/home' }, { path: '/home', component: Home, // 配置子路由关系 children:[ { path:'news', component: HomeNews }, { path: 'message', component: HomeMessage } ] }, { path:'/about', component:About }, { path:'/user/:userId', component: User } ]
-
在组件内部使用
<router-view>
标签:E:\html\WebStorm\project\project_Vuejs\LearnVuejs03\learnvuerouter\src\components\Home.vue<template> <div> <h2>我是首页</h2> <p>我是首页的内容,哈哈哈</p> <router-link to="/home/news">新闻</router-link> <router-link to="/home/message">消息</router-link> <router-view></router-view> </div> </template>
5、vue-router 参数传递
5.1、准备工作
-
创建新的组件 Profile.vue
-
配置路由映射
-
添加跳转的
<router-link>
5.2、传递参数的方式
-
方式一:params
- 配置路由格式:/router/:id
- 传递的方式:在 path 后面跟上对应的值
- 传递后形成的路径:/router/123,/router/abc
-
方式二:query
- 新建
E:\html\WebStorm\project\project_Vuejs\LearnVuejs03\learnvuerouter\src\components\Profile.vue
<template> <div> <h2>我是Profile组件内容</h2> <h2>{{$route.query}}</h2> <h2>{{$route.query.name}}</h2> <h2>{{$route.query.age}}</h2> <h2>{{$route.query.height}}</h2> </div> </template> <script> export default { name: "Profile" } </script>
-
设置index.js
-
在
E:\html\WebStorm\project\project_Vuejs\LearnVuejs03\learnvuerouter\src\App.vue
中添加组件并显示,传递后形成的路径:/router?id=123,/router?id=abc
- 新建
<router-link :to="'/user/'+userId">用户</router-link>
<router-link :to="{path:'/profile',query:{name:'why',age:18,height:1.88}}">档案</router-link>
5.3、传递参数 —— JavaScript代码
<template>
<div id="app">
<router-link to="/home">首页</router-link>
<router-link to="/about">关于</router-link>
<!--
<router-link :to="'/user/'+userId">用户</router-link>
<!– <router-link to="/profile">档案</router-link>–>
<router-link :to="{path:'/profile',query:{name:'why',age:18,height:1.88}}">档案</router-link>
-->
<!-- 另外一种写法-->
<button @click="userClick">用户</button>
<button @click="profileClick">档案</button>
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'App',
data(){
return{
userId:'lisi'
}
},
methods:{
userClick(){
this.$router.push('/user/' + this.userId)
},
profileClick(){
this.$router.push(
{
path:'/profile',
query:{
name:'kobe',
age:18,
height:1.88
}
}
)
}
}
}
</script>
6、vue-router 导航守卫
E:\html\WebStorm\project\project_Vuejs\LearnVuejs03\learnvuerouter\src\router\index.js
在该文件下
-
调用 beforeEach 函数
-
在 VueRouter对象中设置元数据作为导航的实现
// 2.创建VueRouter对象 const routes = [ { // 默认路径 path:'', // redirect 重定向 redirect:'/home' }, { path: '/home', component: Home, // 元数据 meta:{ title:'首页' }, // 配置子路由关系 children:[ /*添加默认路径*/ { path: '', redirect: 'news' }, { path:'news', component: HomeNews }, { path: 'message', component: HomeMessage } ] }, { path:'/about', component:About, // 元数据 meta:{ title:'关于' }, }, { path:'/user/:userId', component: User, // 元数据 meta:{ title:'用户' }, }, { path:'/profile', component:Profile, // 元数据 meta:{ title:'档案' }, } ]
7、keep-alive
作用:所有路径匹配到的视图组件都会被缓存,即保存上次浏览记录,可以使被包含的组件保留状态,或 避免重新渲染
十六、TabBar
1、TarBar案例的实现
-
npm init webpack tabbar
,创建项目,目录结构如下
-
在base.css中设置基础样式,即清除浏览器自带样式
-
TabBarItem.vue 设置插槽并设置样式并导出
<template> <div class="tab-bar-item"> <slot name="item-icon"></slot> <slot name="item-text"></slot> <!-- <img src="../../assets/img/tabbar/shouye.svg" alt=""> <div>首页</div>--> </div> </template> <script> export default { name: "TabBarItem" } </script> <style scoped> .tab-bar-item { flex: 1; text-align: center; height: 49px; font-size: 14px; } .tab-bar-item img{ width: 24px; height: 24px; margin-top: 3px; /*将图片的默认3px去掉*/ vertical-align: middle; } </style>
-
TabBar.vue 中设置插槽并导出
<template> <div id="tab-bar"> <!-- 设置插槽--> <slot></slot> </div> </template> <script> export default { name: "TabBar" } </script> <style scoped> #tab-bar { display: flex; background-color: #f6f6f6; position: fixed; left: 0; right: 0; bottom: 0; /*设置阴影:x方向,y方向,阴影宽度,阴影颜色*/ box-shadow: 0 -1px 1px rgba(100,100,100,.1); } </style>
-
App.vue 中导入、注册、使用组件
<template> <div id="app"> <!-- 3、使用组件--> <tab-bar> <tab-bar-item> <img slot="item-icon" src="./assets/img/tabbar/shouye.svg" alt=""> <div slot="item-text">首页</div> </tab-bar-item> <tab-bar-item> <img slot="item-icon" src="./assets/img/tabbar/profile.svg" alt=""> <div slot="item-text">分类</div> </tab-bar-item> <tab-bar-item> <img slot="item-icon" src="./assets/img/tabbar/gouwuche.svg" alt=""> <div slot="item-text">购物车</div> </tab-bar-item> <tab-bar-item> <img slot="item-icon" src="./assets/img/tabbar/wode.svg" alt=""> <div slot="item-text">我的</div> </tab-bar-item> </tab-bar> </div> </template> <script> /*1、导入组件*/ import TabBar from "./components/tabbar/TabBar"; import TabBarItem from "./components/tabbar/TabBarItem"; export default { name: 'App', components:{ /*2、注册组件*/ TabBar, TabBarItem } } </script> <style> /*导入基本样式*/ @import "./assets/css/base.css"; </style>
2、TarBar案例的实现思路
-
1、封装TarBar组件
- 自定义TarBar组件,在App中使用
- 让TarBar处于底部,并且设置相关的样式
-
2、TarBar中显示的内容由外界决定
- 定义插槽
- flex布局平分TarBar
-
3、自定义TabBarItem.vue,可以传入图片和文字
- 定义TarBarItem,并且定义两个插槽:图片、文字
- 给两个插槽外层包装 div,用于设置样式
- 填充插槽,实现底部TarBar的效果
<template> <!-- 所有的Item都展示同一个图片,同一个文字--> <div class="tab-bar-item" @click="itemClick"> <div v-if="!isActive"><slot name="item-icon"></slot> </div> <div v-else><slot name="item-icon-active"></slot> </div> <div :style="activeStyle"> <slot name="item-text"></slot> </div> </div> </template> <script> export default { name: "TabBarItem", props:{ path:String, activeColor:{ type:String, default:'red' } }, data(){ return { // isActive:true } }, /*搞一个计算属性*/ computed:{ isActive(){ /*拿到活跃路由的path与当前路径的path作为对比*/ /* * /home -> item1(/home) = true * /home -> item2(/category) = false * indexOf()方法:寻找,找到的话不为-1*/ return this.$route.path.indexOf(this.path) !== -1 }, activeStyle(){ return this.isActive ? {color:this.activeColor} :{} } }, methods:{ itemClick(){ this.$router.replace(this.path);// 该方法允许返回 } } } </script> <style scoped> .tab-bar-item { /*使盒子都有相同的长度*/ flex: 1; text-align: center; height: 49px; font-size: 14px; } .tab-bar-item img{ width: 24px; height: 24px; margin-top: 3px; /*将图片的默认3px去掉*/ vertical-align: middle; } /* !*设置 active 下的状态*! .active{ color: red; }*/ </style>
-
4、App.vue 使用组件
<template> <div id="app"> <!-- 3、使用组件--> <tab-bar> <tab-bar-item path="/home" activeColor="deeppink"> <img slot="item-icon" src="./assets/img/tabbar/home.svg" alt=""> <img slot="item-icon-active" src="./assets/img/tabbar/home_active.svg" alt=""> <div slot="item-text">首页</div> </tab-bar-item> <tab-bar-item path="/category" activeColor="deeppink"> <img slot="item-icon" src="./assets/img/tabbar/category.svg" alt=""> <img slot="item-icon-active" src="./assets/img/tabbar/category_active.svg" alt=""> <div slot="item-text">分类</div> </tab-bar-item> <tab-bar-item path="/cart" activeColor="deeppink"> <img slot="item-icon" src="./assets/img/tabbar/cart.svg" alt=""> <img slot="item-icon-active" src="./assets/img/tabbar/cart_active.svg" alt=""> <div slot="item-text">购物车</div> </tab-bar-item> <tab-bar-item path="/profile" activeColor="deeppink"> <img slot="item-icon" src="./assets/img/tabbar/profile.svg" alt=""> <img slot="item-icon-active" src="./assets/img/tabbar/profile_active.svg" alt=""> <div slot="item-text">我的</div> </tab-bar-item> </tab-bar> <router-view></router-view> </div> </template> <script> /*1、导入组件*/ import TabBar from "./components/tabbar/TabBar"; import TabBarItem from "./components/tabbar/TabBarItem"; export default { name: 'App', components:{ /*2、注册组件*/ TabBar, TabBarItem } } </script> <style> /*导入基本样式*/ @import "./assets/css/base.css"; </style> ```
-
4、index.js 中设置相关属性
3、给 TabBarItem 传入 active 图片
- 在 TabBarItem 组件中设置插槽用于存放图片以及文字,并设置active下的style属性为
color:red
- 在App.vue中设置两种状态下的的图片显示
文件路径的引用问题——取别名
情况二:HTML中的图片路径需要加~
十七、Promise 的使用
导读:Promise 是异步编程的一种解决方案【同步:sync,异步:async】
1、Promise的三种状态
- pending :等待状态,比如正在进行网络请求,或者定时器没有到时间
- fulfill :满足状态,当我们主动回调了 resolve 时,就处于该状态,并且会回调 .then()
- reject :拒绝状态,当我们主动回调了 reject 时,就处于该状态,并且会回调 .catch()
简写为如下:
2、Promise链式调用的三种简写
以 resolve 为例,reject同 resolve
最终可简写为:
3、Promise 的 all 方法的使用
十八、Vuex
官方解释:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式
- 状态管理到底是什么?
- 简单来说是将多个组件共享的变量全部存储在一个对象里面
vue init webpack learnvuex
:创建Vuex的项目npm install vuex@3.0.1 --save
:安装Vuex
1、单界面状态管理的实现
2、多界面状态管理
-
创建store/index.js文件
-
导入、挂载并拿到store
-
创建 Hellovuex.vue 组件
-
App.vue 中使用组件
<template> <div id="app"> <h2>{{message}}</h2> <!-- <h2>{{counter}}</h2>--> <h2>-------------App内容----------------</h2> <h2>{{$store.state.counter}}</h2> <button @click="$store.state.counter++">+</button> <button @click="$store.state.counter--">-</button> <h2>-------------App内容----------------</h2> <Hello-vuex/> <!-- <router-view/>--> </div> </template> <script> import HelloVuex from "./components/HelloVuex"; export default { name: 'App', components:{ HelloVuex }, data(){ return{ message:'你好,从今天开始学习Vuex', // counter:0 } } } </script>
3、插件 vuex-devtools
安装vuex-devtools :https://blog.csdn/qq_53810245/article/details/121295089
4、mutations
mutations 唯一的目的就是修改 state 中状态
mutations 中的每个方法尽可能完成的事件比较单一一点儿
4.1、mutations 是定义方法
修改state中的数据都是通过mutations去修改的
作用是:在浏览器中可以跟踪到数据的变化状况
在App.vue 中设置点击的方法,在方法中调用this.$storemit('increment)
- 从而使用index中设置的方法
- 通过 mutation更新
4.2、Mutation中的参数
- 参数被称为mutation的载荷(Payload)
- 参数可以是数,也可以是对象
- 第一步:写入按钮并设置点击事件
- 第二步:添加相关方法
- 在index.js中设置相关方法
import Vue from "vue"; import Vuex from 'vuex' import {stat} from "copy-webpack-plugin/dist/utils/promisify"; // 1.安装插件 Vue.use(Vuex) // 2.创建对象 const store = new Vuex.Store({ // 用于保存状态 state:{ counter:10, students:[ {id:110,name:'why',age:18}, {id:111,name:'koba',age:24}, {id:112,name:'james',age:30}, {id:113,name:'curry',age:10} ] }, mutations:{ // 定义方法,修改state中的数据都是通过mutations去修改的 /* Mutation 状态主要包括两部分: - 字符串的事件类型(type) - 一个回调函数(handler),该回调函数的第一个参数就是state */ increment(state){ state.counter++ }, decrement(state){ state.counter-- }, incrementCount(state,count){ state.counter += count; }, addStudent(state,stu){ state.students.push(stu) } }, // 做一些异步操作 actions:{}, // 类似于组组件中的计算属性 getters:{ powerCounter(state){ return state.counter * state.counter }, // 获取年龄大于20岁的集合 more20stu(state){ return state.students.filter(s=>s.age>20) }, // 获取年龄大于20岁的集合的长度 more20stuLength(state,getters){ return getters.more20stu.length }, // 获取年龄大于指定的岁数的集合 moreAgestu(state,getters){ return function (age){ return state.students.filter(s => s.age >age ) } // 将上述函数改为箭头函数为: /* return age => { return state.students.filter(s => s.age > age) }*/ } }, // 划分模块 modules:{} }) // 3.导出 store对象 export default store
- Mustation 提交风格,当为特殊提交封装时需要传入payload
5、vuex - getters
作用:当我们每次想要使用state经过变化之后的值时
-
在App.vue中使用getters
-
index.js中写入相关函数
6、vuex-数据的响应式原理
Vuex的store中的state是响应式的,当state中的数据发生改变时,Vue组件会自动更新
但是需遵守一些Vuex对应的规则
- 提前在store中初始化好所需的规则
响应式管理
响应式 - 添加: Vue.set(state.info,'address','洛杉矶')// 响应式的添加属性address
响应式 - 删除:Vue.delete(state.info,'age') // 响应式的删除age属性
- 当给state中的对象添加新属性时,使用下面的方式
- 方式一:使用Vue.set(obj,‘newProp’,123)
- 方式二:用新对象给旧对象重新赋值
7、vuex-actions
异步操作不能在 Mutation中进行
Action类似于 Mutation ,但是是来代替 Mutation 进行异步操作的
8、vuex-modules
8.1、是什么
将 store 分割成模块(Modules),而某个模块拥有自己的 state、mutations、action、getters等
8.2、怎么做
那我们按照什么样的方式来组织模块呢?
8.3、vuex-modules中的相关属性
9、对象的解构
10、Vuex的安装与使用
- 安装:
npm install vuex@3.1.0 --save
- 创建目录:
src/store/index.js
- 在
index.js
中导入使用:
import Vue from 'vue'
import App from './App.vue'
//1. 安装插件
Vue.use(Vuex)
//2. 创建 Store 对象
const store = new Vuex.Store({
state:{
// 添加的商品
cartList:[]
},
mutations:{
addCounter(state,payload){
payload.count++;
}
addToCart(state,payload){
state.cartList.push(payload)
}
},
actions:{
addCart(context,payload){
// 1.payload :新添加的商品,判断与已加商品是否重复
let oldProduct = null;
for(let item of context.state.cartList){
if(item.iid === payload.iid){
oldProduct = item;
}
}
// 步骤1 可以一步到位为以下内容:
// let oldProduct = context.state.carList.find(item => item.iid === payload.iid)
// 2.判断 oldProduct
if(oldProduct){
// oldProduct.count += 1;该方法无法跟踪
context.commit('addCounter),oldProduct
}else{
payload.count = 1;
// state.carList.push(payload);该方法无法跟踪
context.commit('addToCart',payload)
}
}
}
})
// 3.挂载到 Vue 实例上
export default store
版权声明:本文标题:篇章九(2)Vuejs进阶版 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://www.elefans.com/dianzi/1726163232a1057939.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论