Vue进阶
- 能够实现项目的打包
- 能够说出开发移动App的几种方式
- 能够使用HBuliderX把移动端网站打包成移动App
- 能够说出defineProperty的作用
- 能够说出服务端渲染和客户端渲染的区别
- 理解Nuxt的作用
- 能够理解asyncData的作用
Day01
01-阶段说明和内容介绍
内容介绍
- 打包app
- MVVM实现原理
- Object.defineProperty()方法的使用
- 发布订阅模式
- 实现MVVM框架
- SSR服务端渲染
阶段说明
-
本阶段MVVM和SSR部分的内容难度大、抽象、难理解
-
本阶段不要求掌握编码,但需要用自己的语言将内容进行描述
-
本阶段要求对Vue基本使用非常熟练
-
data->声明组件自己的简单的响应式的数据
-
computed->声明组件自己的复杂(数据b依赖了数据a,此时数据b是计算属性)的响应式的数据
-
props->声明值来源于外部(通常是父组件)的响应式的数据
-
watch->监听数据变化-在变化时要做的事儿是异步||开销大
-
异步:定时器/ajax/事件
-
开销大:循环||递归(自己玩自己)
-
语法 watch:
// 凡是可以使用Vue的实例this.出来的东西都可以watch监测变化 watch : { msg(){}, this.msg computedMsg(){}, this.computed '$route'(){}, this.$route '$store'(){} this.$store }
-
-
-
****本阶段的内容MVVM部分在面试环节Vue部分属于重点项、必问点,常见的面试问题:
- Vue 数据绑定的原理?
- MVVM 数据绑定的原理?
- Vue 双向数据绑定的原理?
- Vue 数据响应式原理?
- 数据响应式原理?
- 数据驱动视图的原理?
02-移动App开发的几种方式
- 原生App
- WebApp
- HybridApp
- 分支:跨平台开发
- 其他类型(小程序/快应用等等)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3hYDAAwc-1589879881943)(assets/Mobile-App-Development.png)]
03-移动App开发-NativeApp介绍和特点
定义:传统的原生App开发模式,有iOS和Android两大系统,需要各自语言开发各自App。
优点:性能和体验都是最好的
缺点:开发和发布成本高
应用技术:Swift,OC,Java
常识: 安卓/iOS/Web 基本都使用Mac进行开发
04-移动App开发-WebApp介绍和特点
M站演示
定义: 移动端的网站,常被称为H5应用,说白了就是特定运行在移动端浏览器上的网站应用。一般泛指 **SPA(Single Page Application)模式开发出的网站,与MPA(**Multi-page Application)对应。
优点:开发和发布成本最低
-
开发成本低,可以跨平台,调试方便,开发速度最快
web app一般只需要一个前端人员开发出一套代码,然后即可应用于各大主流浏览器(特殊情况可以代码进行下兼容),没有新的学习成本,而且可以直接在浏览器中调试
-
维护成本低
同上,如果代码合理,只需要一名前端就可以维护多个web app
-
更新最为快速
由于web app资源是直接部署在服务器端的,所以只需要替换服务器端的文件,用户访问是就已经更新了(当然需要解决一些缓存问题)
-
无需安装App,不会占用手机内存
通过浏览器即可访问,无需安装,用户就会比较愿意去用
缺点:性能和体验不能讲是最差的,但也受到浏览器处理能力的限制
-
性能低,用户体验差
由于是直接通过的浏览器访问,所以无法使用原生的API,操作体验不好
-
依赖于网络,页面访问速度慢,耗费流量
Web App每次访问都需要去服务端加载资源访问,所以必须依赖于网络,而且网速慢时访问速度很不理想,特别是在移动端,如果网站优化不好会无故消耗大量流量
-
功能受限,大量功能无法实现
只能使用Html5的一些02-特殊api,无法调用原生API,所以很多功能存在无法实现情况
-
临时性入口,用户留存率低
这既是它的优点,也是缺点,优点是无需安装,缺点是用完后有时候很难再找到,或者说很难专门为某个web app留存一个入口,导致用户很难再次使用
应用技术:ReactJS,AugularJS,VueJS等等
05-移动App开发-HybridApp介绍和特点
定义:混合模式移动应用,介于Web App、Native App这两者之间的App开发技术,兼具“Native App良好交互体验的优势”和“Web App跨平台开发的优势” ,原生客户端的壳WebView,其实里面是HTML5的网页
- 把网页打包成移动 App
- 使你的 Web 程序可以访问手机原生能力
优点:开发和发布都比较方便,效率介于Native App、Web App之间
-
开发成本较低,可以跨平台,调试方便
Hybrid模式下,由原生提供统一的API给JS调用,实际的主要逻辑有Html和JS来完成,而由于最终是放在webview中显示的,所以只需要写一套代码即可,达到跨平台效果,另外也可以直接在浏览器中调试,很为方便
最重要的是只需要一个前端人员稍微学习下JS api的调用即可,无需两个独立的原生人员
一般Hybrid中的跨平台最少可以跨三个平台:Android App,iOS App,普通webkit浏览器
-
维护成本低,功能可复用
同上,如果代码合理,只需要一名前端就可以维护多个app,而且很多功能还可以互相复用
-
更新较为自由
虽然没有web app更新那么快速,但是Hybrid中也可以通过原生提供api,进行资源主动下载,达到只更新资源文件,不更新apk(ipa)的效果
-
针对新手友好,学习成本较低
这种开发模式下,只需要前端人员关注一些原生提供的API,具体的实现无需关心,没有新的学习内容,只需要前端人员即可开发
-
功能更加完善,性能和体验要比起web app好太多
因为可以调用原生api,所以很多功能只要原生提供出就可以实现,另外性能也比较接近原生了
-
部分性能要求的页面可用原生实现
这应该是Hybrid模式的最多一个好处了,因为这种模式是原生混合web,所以我们完全可以将交互强,性能要求高的页面用原生写,然后一些其它页面用JS写,嵌入webview中,达到最佳体验
缺点:学习范围较广,需要原生配合
-
相比原生,性能仍然有较大损耗
这种模式受限于webview的性能桎梏,相比原生而言有不少损耗,体验无法和原生相比
-
不适用于交互性较强的app
这种模式的主要应用是:一些新闻阅读类,信息展示类的app;但是不适用于一些交互较强或者性能要求较高的app(比如动画较多就不适合)
应用技术:Cordova、APPCan、 DCloud 、API Cloud
四种方式对比
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mEns2X7B-1589879881945)(assets/zl9vgosvxs.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dtF3bDHb-1589879881947)(assets/4t0yf9dnpw.png)]
js模块化单独回顾~~~~~
06-移动App开发-跨平台开发介绍和特点
特点:使用类似于 Web 技术的方式来开发 Native App。
定义: Facebook发现Hybrid App存在很多缺陷和不足,于是发起开源的一套新的App开发方案RN。使用JSX语言写原生界面,js通过JSBridge调用原生API渲染UI交互通信。
优点:效率体验接近Native App,发布和开发成本低于Native App
-
虽然说开发成本大于Hybrid模式,但是小于原生模式,大部分代码可复用
相比于原生模式,这种模式是统一用JS写代码,所以往往只需要一名成员投入学习,即可完成跨平台app的开发,而且后续代码封装的好,很多功能可复用
-
性能体验高于Hybrid,不逊色与原生
这种模式和Hybrid不一样,Hybrid中的view层实际上还是dom,但是这种模式的view层是虚拟dom,所以性能要高于Hybrid,距离原生差距不大
这种模式可以认为是用JS写原生,即页面用JS写,然后原生通过Bridge技术分析JS,将JS内容单独渲染成原生Android和iOS,所以也就是为什么性能不逊色原生
-
开发人员单一技术栈,一次学习,跨平台开发
这种模式是统一由JS编写,有着独特的语法,所以只需要学习一次,即可同时开发Android和iOS
-
社区繁荣,遇到问题容易解决
这应该是React Native的很大一个优势,不像Hybrid模式和原生模式一样各自为营,这种模式是Facebook统一发起的,所以有一个统一的社区,里面有大量资源和活跃的人员,对开发者很友好
缺点: 学习有一定成本,且文档较少,免不了踩坑
-
虽然可以部分跨平台,但并不是Hybrid中的一次编写,两次运行那种,而是不同平台代码有所区别
这种模式实际上还是JS来写原生,所以Android和iOS中的原生代码会有所区别,如果需要跨平台,对开发人员有一定要求
当然了,如果发展了有一定时间,组件库够丰富了,那么其实影响也就不大了,甚至会比Hybrid更快
-
开发人员学习有一定成本
虽然社区已经比较成熟了,但是一个新的普通前端学习起来还是有一定学习成本的,无法像Hybrid模式一样平滑
-
学习成本大,对开发人员技术要求比较高
-
不懂原生开发很难驾驭好
-
说是使用 Web 技术进行开发,还是多少得学点儿原生 App 开发,才能处理好跨平台。
-
前期投入比较大,后劲很足。
应用技术 :
React Native(主流)
- 公司:Facebook
- 技术栈:React
- 基于React开发App的框架RN
其他技术:
Weex(使用不多)
- 公司:Apache 开源基金会
- Vue.js 技术栈
- 基于Vue开发App的框架WEEX
Flutter(未来趋势)
- 公司:Google
- 它提供了官方的原生 UI 组件
- 比 RN、Weex 之类的体验更好
- 开发语言:Dart(和 JavaScript 很像)
- 商业应用:闲鱼
行业常识:
- 前端工程师 Web
- FE客户端开发工程师
- 苹果开发工程师
- 安卓开发工程师
- Web开发工程师
07-移动App开发-其他类型App
小程序
- 微信小程序
- 百度小程序
- 头条小程序
- 支付宝小程序
- 。。。
统一开发平台
- taro(京东)->坑多 趋势很好
- uni-app
微网页
- 微信公众号
- 百度直达号
- 。。。
快应用(不温不火,iPhone 不参与很难搞起来)
- 各大手机厂商联合制定推出的一种方式,类似于小程序
- 使用 Web 技术进行开发, 而且提供了在 Web 中访问手机硬件等底层交互的 API
- 属于混合 App 的一种方式
PWA(网站离线访问技术,没有 iPhone 不参与)
- 它可以让网站拥有一个类似于 App 的入口
- 提供了网站的离线应用访问
- Google 在推动
- 手机端目前只能在 安卓手机的 Chrome 浏览器运行
08-移动App开发-开发模式对比和选择
各大开发模式对比
Native App | Web App | Hybrid App | React Native App | |
---|---|---|---|---|
原生功能体验 | 优秀 | 差 | 良好 | 接近优秀 |
渲染性能 | 非常快 | 慢 | 接近快 | 快 |
是否支持设备底层访问 | 支持 | 不支持 | 支持 | 支持 |
网络要求 | 支持离线 | 依赖网络 | 支持离线(资源存本地情况) | 支持离线 |
更新复杂度 | 高(几乎总是通过应用商店更新) | 低(服务器端直接更新) | 较低(可以进行资源包更新) | 较低(可以进行资源包更新) |
编程语言 | Android(Java),iOS(OC/Swift) | js+html+css3 | js+html+css3 | 主要使用JS编写,语法规则JSX |
社区资源 | 丰富(Android,iOS单独学习) | 丰富(大量前端资源) | 有局限(不同的Hybrid相互独立) | 丰富(统一的活跃社区) |
上手难度 | 难(不同平台需要单独学习) | 简单(写一次,支持不同平台访问) | 简单(写一次,运行任何平台) | 挺等(学习一次,写任何平台) |
开发周期 | 长 | 短 | 较短 | 中等 |
开发成本 | 昂贵 | 便宜 | 较为便宜 | 中等 |
跨平台**** | 不跨平台**** | 所有H5浏览器 | Android,iOS,h5浏览器**** | Android,iOS |
APP发布 | App Store | Web服务器 | App Store | App Store |
如何选择开发模式
目前有多种开发模式,那么我们平时开发时如何选择用哪种模式呢?如下
选择纯Native App模式的情况
-
性能要求极高,体验要求极好,不追求开发效率
一般属于吹毛求疵的那种级别了,因为正常来说如果要求不是特别高,会有Hybrid
选择Web App模式的情况
-
不追求用户体验和性能,对离线访问没要求
正常来说,如果追求性能和体验,都不会选用web app
-
没有额外功能,只有一些信息展示
因为web有限制,很多功能都无法实现,所以有额外功能就只能弃用这种方案了
选择Hybrid App模式的情况
-
大部分情况下的App都推荐采用这种模式
这种模式可以用原生来实现要求高的界面,对于一些比较通用型,展示型的页面完全可以用web来实现,达到跨平台效果,提升效率
当然了,一般好一点的Hybrid方案,都会把资源放在本地的,可以减少网络流量消耗
选择React Native App模式的情况
-
追求性能,体验,同时追求开发效率,而且有一定的技术资本,舍得前期投入
React Native这种模式学习成本较高,所以需要前期投入不少时间才能达到较好水平,但是有了一定水准后,开发起来它的优势就体现出来了,性能不逊色原生,而且开发速度也很快
选择其它方案
- 小程序(目前移动 App 中开发难度最低的,体验也是仅次于原生+跨平台NativeApp)
- 接活: 整包(8K+) || 按页面算(500静态||2K) => 另外的薪酬计算方式: 按时薪
08-补-如何分辨一个 App 是原生做的还是 Web 做的
1、看断网情况
通过断开网络,刷新页面,观察内容缓存情况来有个大致的判断,可以正常显示的就是原生写的,显示404或者错误页面的就是html页面。
3、看复制文章的提示,需要通过对比才能得出结果。
比如文章资讯页面可以长按页面试试,如果出现文字选择,粘贴功能的是H5页面,否则是native原生的页面。
有些原生APP开放了复制粘贴功能或者关闭了,而H5的CSS屏蔽了复制选择功能等情况,需要通过对目标测试APP进行对比才可知。
在支付宝APP、蚂蚁聚宝是可以判断的。
4、看加载的方式
如果在打开新页面导航栏下面有一条加载线的话,这个页面就是H5页面,如果没有就是原生的。
5、看app顶部,导航栏是否会有关闭的操作
如果APP顶部导航栏当中出现了关闭的按钮或者关闭的图标,那么当前的页面是H5页面,原生的不会出现(除非设计开发者特意设计),美团、大众点评的APP、微信APP当加载H5过多的时候,左上角会出现关闭两个字。
6、判断页面下拉刷新的时候(前提是要有下拉刷新的功能)
如果页面没有明显刷新现象的是原生的,如果有明显刷新现象(比如闪一下)的是H5页面(Ios和Android)。比如淘宝的众筹页面。
7、下拉页面的时候显示网址提供方的一定是H5页面。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f3zCdnlx-1589879881948)(/assets/wKiom1kKyzWDcmeRAARsWiUM4mA579.png-wh_500x0-wm_3-wmp_4-s_2902533966.png)]
8、利用系统开发人员工具
找到手机的设置,开发者选项,显示布局边界,选择开启后再去查看APP整体布局边界,这样所有应用控件布局就会一目了然。
如果是native APP那么每个按钮、文字、图片都是红色的线显示这个控件的布局情况。如下图的微信:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JgspHUeP-1589879881949)(/assets/wKioL1kKzGzTEtNCAAXhWJHYlgI333.png-wh_500x0-wm_3-wmp_4-s_3312311617.png)]
如果是web APP那么应该就是一个webview去加载网页,webview作为一个控件,只有一个边界框,即只有屏幕边才有红色线,如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CS2nEto9-1589879881950)(/assets/wKioL1kKzXDhS9IMAAb9O0qy84c885.png-wh_500x0-wm_3-wmp_4-s_1494819383.png)]
混合APP 则是native 与 webview 混排的界面,如下图红色线框是各控件的绘制边界,中间那一大块布局丰富的界面没有显示出很多边界红线,就是网页实现的。如下图的京东:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ybr9EPcN-1589879881951)(/assets/wKioL1kKzjWjSa77AA10F9eHjLU763.png-wh_500x0-wm_3-wmp_4-s_2780626697.png)]
09-DCloud-HTML5+介绍-HBuliderX安装和创建项目
使用HBuliderX创建项目并且编码运行测试(网页测试/模拟器测试/真机测试)
下载安装HBuilderX
IDE:敲代码的软件.比如VSCode/WebStrom/HBuliderX
下载地址:https://www.dcloud.io/hbuilderx.html
创建项目
1. 新建项目
2. 5+runtime类型
3. 生成项目目录
4. 编码
5. 测试
1. 运行->选择chrome
2. 运行->选择安卓模拟器(夜神)
开发神器
- 软件名 everything+wox
- 作用:全局搜索软件或者文件
- 关键字: windows everything wox配置
- 原因: 提高开发效率 (没鼠标)
10-DCloud-HBuliderX模拟器测试和API测试
1. 在浏览器上测试效果
2. 在模拟器上测试
3. 在真机上测试
- 用手机助手连接手机(**安装驱动**)
- 开启手机的**开发者选项**
- **启用 USB 调试**
- 使用数据线连接手机
- 在 HBuilder 中找到:运行 -> 运行到手机或模拟器 -> 你的设备
- [HBuilder/HBuilderX真机运行、手机运行、真机联调常见问题](http://ask.dcloud.net.cn/article/97)
访问 HTML5 + API
- HTML5+ API Reference
11-DCloud-HBuliderX打包demo
打包发布
- 配置 manifest.json 文件
- 在 HBuilder 中找到:发行 -> 原生 App(云打包)
- 等待一段时间,得到打包结果安装包,然后安装到手机上测试
- 最后根据需要发布到对应的手机应用商店
配置 manifest
- Manifest.json文档说明 manifest配置
打包
- 离线打包
- 云打包
12-DCloud-HBuliderX打包黑马头条
通过HBuliderX将黑马头条项目打包成app,生成apk文件下载安装测试
- 测试之前的网站效果
- npm run build
- http-server- o
- 打包app
- 复制manifest.json到public中
- 修改配置
- 发布(发行)
- 测试
13-MVVM-介绍和演示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6R8iwFcY-1589879881951)(./assets/mvvm-3510948.png)]
- Model(M):普通的 JavaScript 对象,例如 Vue 实例中的 data
- 普通数据
- View(V):视图
- HTML DOM 模板 #app
- ViewModel(VM):Vue实例
- 负责数据和视图的更新
- 它是 Model数据 和 View 视图通信的一个桥梁
- 简单一句话:数据驱动视图
<!-- 视图 -->
<template>
<div>{{ message }}</div>
</template>
<!-- ViewModel -->
把普通的 JavaScript 对象和视图 DOM 之间建立了一种映射关系:
- 数据的改变影响视图
- 视图(表单元素)的改变影响数据
<script>
// Model 普通数据对象
export default {
data () {
return {
message: 'Hello World'
}
}
}
</script>
<style>
</style>
研究原理的套路
- 玩明白 vue.js
- 分析特点 重点vm实例
- 假设没有vue.js
- 效果一样,证明原理搞懂
14-MVVM-响应式变化原理-介绍
Vue文档说明
Vue的MVVM的实现原理包含以下几部分
- Object.defineProperty()方法->MDN文档- > 为属性设置set和get方法->在数据变化时更新视图
- js发布订阅模式: 在数据变化时->通知其他位置更新视图
Day02
01-Object.defineProperty()-基本使用
- 作用:
**Object.defineProperty()**
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。 - 说明 Object.defineProperty(obj, prop, descriptor)
- 形参1: 被操作的对象
- 形参2:新属性或者现有属性
- 形参3:属性描述符
- 返回值:返回被操作的对象
- 注意:value和get/set不能同时进行设置
const per = {
name: 'zs'
}
var _age = 1000
const per1 = Object.defineProperty(per, 'age', {
// value: 200,
set(age) {
_age = age
},
get() {
return _age
}
})
per1.age = 2000
console.log(per1.age);
当访问对象的属性时,会调用属性的get方法,当为对象的属性设置值时,会调用属性的set方法
02-Object.defineProperty()-模拟vm对象
目的: 利用Object.defineProperty()模拟Vue中的Vue实例化对象的效果
核心:把data的数据挂到vm对象上,直接访问vm.数据名字可以调用对应的set/get方法
const data = {
msg: 'abc'
}
const vm = {}
Object.defineProperty(vm, 'msg', {
get() {
console.log('get');
return data.msg
},
set(value) {
console.log('set');
data.msg = value
}
})
console.log(vm);
03-Object.defineProperty()-可枚举
目的: 利用属性描述符中的enumerable选项设置属性是否可以被遍历访问
代码
const data = {
msg: 'abc'
}
const vm = {}
Object.defineProperty(vm, 'msg', {
enumerable: true,
get() {
console.log('get');
return data.msg
},
set(value) {
console.log('set');
data.msg = value
}
})
// console.log(vm);
for (const key in vm) {
console.log(key);
}
04-Object.defineProperty()-可配置和严格模式
目的:利用属性描述符中的enumerable选项设置属性是否可以被delete删除
代码
'use strict';
const data = {
msg: 'abc'
}
const vm = {}
Object.defineProperty(vm, 'msg', {
configurable: true,
enumerable: true,
get() {
console.log('get');
return data.msg
},
set(value) {
console.log('set');
data.msg = value
}
})
delete vm.msg
// console.log(vm);
for (const key in vm) {
console.log(key);
}
js可以开启严格模式’use strict’ [MDN文档](https://developer.mozilla.org/zh-CN/docs/Web/JavaS cript/Reference/Strict_mode)
05-Object.defineProperty()-可写性
目的:利用属性描述符中的writable 选项设置属性是否可写,可以被修改
代码
'use strict';
const data = {
msg: 'abc'
}
const vm = {}
Object.defineProperty(vm, 'msg', {
configurable: true,
enumerable: true,
value: data.msg,
writable: false
})
vm.msg = '新值'
console.log(vm);
writable和访问器(set/get)无法同时设置
06-Object.defineProperty()-总结
- 作用1: 为对象增加属性
- 作用2:一旦设置了访问器->
- 取值->get
- 该值->set
结果: 1. 模拟vm的结构啦 好开心 2. 修改数据时,执行set->更新视图
07-发布订阅模式的介绍
设计模式: 前人总结的经验,用于开发
介绍:Vue的MVVM原理实现中,当数据变化时,要通知多个位置更新视图,所以我们可以使用设计模式->发布订阅模式来实现->当事情A发生时通知多个人进行事件B->在busevent.js中就是利用了这一模式
组成:
- 监听/注册一个自定义事件 bus.$on(‘事件类型’,处理函数)
- 发布事件 bus.$emit(‘事件类型’,参数)
代码
// 注册事件(订阅消息)
// bus.on('click',fn1)
// bus.on('mouseover',fn2)
// bus.on('xxoo',fn3)
// 把多个事件类型和事件处理函数进行保存
// 触发事件(发布消息)
// bus.$emit('click',1)
// bus.$emit('mouseover',2)
// bus.$emit('xxoo',3)
// 把多个事件类型进行取出,并且执行事件处理函数
// 使用构造函数进行封装
function EventEmitter() {
}
EventEmitter.prototype.$on = function() {
}
EventEmitter.prototype.$emit = function() {
}
08-发布订阅模式的实现-$on
// 使用构造函数进行封装
function EventEmitter() {
// {'click':[fn1,fn2,...],'mouseover':[fn1,fn2,...]}
this.subs = {}
}
// 注册事件
// click , fn
EventEmitter.prototype.$on = function(eventType, handler) {
// // subs ---> {click:[fn1]}
// if (this.subs[eventType]) {
// this.subs[eventType].push(handler)
// } else {
// // sub2 ---> {}
// this.subs[eventType] = []
// this.subs[eventType].push(handler)
// }
this.subs[eventType] = this.subs[eventType] || []
this.subs[eventType].push(handler)
}
09-发布订阅模式的实现-$emit
EventEmitter.prototype.$emit = function(eventType) {
if (this.subs[eventType]) {
this.subs[eventType].forEach((handler) => {
handler()
})
}
}
// 测试
const em = new EventEmitter()
em.$on('a', function() {
console.log('---a');
})
em.$on('b', function() {
console.log('---b');
})
em.$on('c', function() {
console.log('---c');
})
em.$emit('a')
em.$emit('b')
em.$emit('c')
10-发布订阅模式的实现-$emit-参数
EventEmitter.prototype.$emit = function(eventType, ...rest) {
if (this.subs[eventType]) {
this.subs[eventType].forEach((handler) => {
handler(...rest)
})
}
}
这里用到了ES6语法剩余参数…rest
11-发布订阅模式的实现-$emit-this问题
目的:我们希望$on事件触发时的this是em实例化对象
EventEmitter.prototype.$emit = function(eventType, ...rest) {
if (this.subs[eventType]) {
this.subs[eventType].forEach((handler) => {
// handler(...rest)
handler.call(this, ...rest)
})
}
}
这里用到了call方法修改函数内部的this指向
this指向玩法->单讲
// this指向->
// 规则
// 1. 如果函数调用时,前面没东西->独立调用: this->window
// 2. obj.fn() this->obj
// 3. bind/call/apply eg:fn.call(obj) this->obj
// 4. new Fn() this->实例对象
12-发布订阅模式的实现-总结
// 注册事件(订阅消息)
// bus.on('click',fn1)
// bus.on('mouseover',fn2)
// bus.on('xxoo',fn3)
// 把多个事件类型和事件处理函数进行保存
// 触发事件(发布消息)
// bus.$emit('click',1)
// bus.$emit('mouseover',2)
// bus.$emit('xxoo',3)
// 把多个事件类型进行取出,并且执行事件处理函数
// 使用构造函数进行封装
function EventEmitter() {
// {'click':[fn1,fn2,...],'mouseover':[fn1,fn2,...]}
this.subs = {}
}
// 注册事件
// click , fn
EventEmitter.prototype.$on = function(eventType, handler) {
this.subs[eventType] = this.subs[eventType] || []
this.subs[eventType].push(handler)
}
EventEmitter.prototype.$emit = function(eventType, ...rest) {
if (this.subs[eventType]) {
this.subs[eventType].forEach((handler) => {
handler.call(this, ...rest)
})
}
}
const em = new EventEmitter()
em.$on('a', function(...rest) {
console.log('---a', rest);
})
em.$on('b', function() {
console.log('---b');
})
em.$on('c', function() {
console.log('---c', this);
})
// em.$emit('a', 100, 1, 2, 3, 4)
// em.$emit('b', 200)
em.$emit('c', 300)
利用发布订阅模式可以实现当事件触发时会通知到很多人去做事情,Vue中做的事情是更新DOM
13-MVVM实现-DOM复习
- DOM是什么?->文档对象模型 document
- DOM的作用?->可以通过对象去操作页面元素
- document对象里面的每个内容都是节点
- 节点类型
- 注释节点
- 文本节点
- 元素节点
子节点 : childNodes 伪数组
节点类型: nodeType === 1 3 8
属性集合 attributes -> 指令 v-xxoo
节点内容: innerText || textContent -> {{}}
找到对应的DOM,更新DOM
14-MVVM实现-分析Vue构造函数-准备结构
- 新建mvvm/vue.js->构造函数
- 新建测试文件mvvm/mvvm.html->实例化Vue
el的值:string || Element
15-MVVM实现-代理数据proxyData
mvvm/vue.js
// Vue构造函数
// 初始化数据
// 1. 注入:data中的属性,设置为Vue实例的属性,并且设置成getter/setter
// 2. 数据劫持:$data中的属性设置为getter/setter(响应式数据),当数据变化时,要发送通知,更新视图
// 3. 编译模板:解析模板中的{{}}和v-指令
function Vue(options) {
this.$options = options
this.$data = options.data || {}
// 判断options.el的类型
// 如果字符串(选择器'#app'),要获取对应的DOM元素
const el = options.el
this.$el = typeof el === 'string' ? document.querySelector(el) : el
this.$proxyData()
}
// 1. 注入:data中的属性,设置为Vue的属性,并且设置成getter/setter
// 1.1 遍历data中的属性
// 1.2 把遍历到的属性挂载到Vue实例上,并且设置getter/setter
Vue.prototype.$proxyData = function() {
Object.keys(this.$data).forEach(key => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: false,
getter() {
return this.$data[key]
},
setter(value) {
// 相同的值,不需要赋值
if (this.$data[key] === value) return
this.$data[key] = value
}
})
})
}
16-MVVM实现-数据劫持Observer
mvvm/observer.js
// 数据劫持:$data中的属性设置为getter/setter(响应式数据),当数据变化时,要发送通知,更新视图
// 1. 遍历data中的属性->walk方法
// 2. 为属性设置getter和setter->defineReactive方法
function Observer(data) {
this.walk(data)
}
Observer.prototype.walk = function(data) {
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
Observer.prototype.defineReactive = function(data, key, value) {
Object.defineProperty(data, key, {
configurable: false,
enumerable: true,
get() {
return value
},
set(newValue) {
if (value === newValue) return
value = newValue
// 当数据变化时,更新视图
}
})
}
测试
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
<!-- 引入vue.js -->
<!-- <script src='./lib/vue.js'></script> -->
<!-- <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> -->
<script src="./mvvm/observer.js"></script>
<script src="./mvvm/vue.js"></script>
</head>
<body>
<!-- 设置Vue所管理的视图-->
<div id="app"></div>
<script> // 实例化Vue对象 const vm = new Vue({ // 指定Vue管理的视图 el: '#app', data: { msg: 'abc', age: 10 } }) console.log(vm) </script>
</body>
</html>
17-MVVM实现-编译模板Compiler-设计结构
mvvm/compiler.js
// 3. 编译模板:解析模板中的{{}}和v-指令
// 找到$el->就找到了模板中的{{}}和指令
// 找到数据$data
// 所以, 直接找vm
function Compiler(vm) {
this.$vm = vm
// 编译模板
}
// 编译模板
Compiler.prototype.compile = function(el) {}
// 处理文本节点
Compiler.prototype.compileTextNode = function(node) {}
// 处理元素节点
Compiler.prototype.compileElementNode = function(node) {}
// 判断当前节点是否是文本节点
Compiler.prototype.isTextNode = function(node) {}
// 判断当前节点是否是元素节点
Compiler.prototype.isElementNode = function(node) {}
// 判断属性名称是否是指令
Compiler.prototype.isDirective = function(attrName) {}
18-MVVM实现-编译模板Compiler-辅助方法
mvvm/compiler.js
// 3. 编译模板:解析模板中的{{}}和v-指令
// 找到$el->就找到了模板中的{{}}和指令
// 找到数据$data
// 所以, 直接找vm
function Compiler(vm) {
this.$vm = vm
// 编译模板
this.compile(vm.$el)
}
// 编译模板
// 1.1 遍历节点
// 1.2 判断节点类型
Compiler.prototype.compile = function(el) {
// 找到el下所有子节点childNodes
Array.from(el.childNodes).forEach(node => {
if (this.isTextNode(node)) {
this.compileTextNode(node)
}
if (this.isElementNode(node)) {
this.compileElementNode(node)
}
})
}
// 处理文本节点
Compiler.prototype.compileTextNode = function(node) {}
// 处理元素节点
Compiler.prototype.compileElementNode = function(node) {}
// 判断当前节点是否是文本节点
Compiler.prototype.isTextNode = function(node) {
return node.nodeType === 3
}
// 判断当前节点是否是元素节点
Compiler.prototype.isElementNode = function(node) {
return node.nodeType === 1
}
// 判断属性名称是否是指令
Compiler.prototype.isDirective = function(attrName) {
return attrName.startsWith('v-')
}
Day03
01-MVVM实现-编译模板Compiler-处理文本节点
mvvm/compiler.js
// 编译模板
// 1.1 遍历节点
// 1.2 判断节点类型
Compiler.prototype.compile = function(el) {
// 找到el下所有子节点childNodes
Array.from(el.childNodes).forEach(node => {
if (this.isTextNode(node)) {
this.compileTextNode(node)
}
if (this.isElementNode(node)) {
this.compileElementNode(node)
this.compile(node)
}
})
}
// 处理文本节点
// 1. 获取文本节点的内容
// 2. 正则判断
// 3. 获取属性名
// 4. 给文本节点重新赋值为data中的key属性
Compiler.prototype.compileTextNode = function(node) {
const text = node.textContent
const reg = /\{\{(.+)\}\}/
if (reg.test(text)) {
const key = RegExp.$1.trim()
node.textContent = text.replace(reg, this.vm.$data[key])
}
}
测试
<script src="./mvvm/compiler.js"></script>
<script src="./mvvm/observer.js"></script>
<script src="./mvvm/vue.js"></script>
<div id="app">
<p>{{msg}}</p>
<p>{{age}}</p>
</div>
提示: 实际开发时正则不需要记 但是要能看懂
02-MVVM实现-编译模板Compiler-处理元素节点
mvvm/compiler.js
// 处理元素节点
Compiler.prototype.compileElementNode = function(node) {
Array.from(node.attributes).forEach(attr => {
const name = attr.name
if (this.isDirective(attr.name)) {
const value = attr.value
if (name === 'v-text') {
node.textContent = this.$vm[value]
}
if (name === 'v-model') {
node.value = this.$vm[value]
}
}
})
}
03-MVVM实现-数据驱动视图
数据变化时,视图的多个位置都需要变化->发布订阅模式EventEmitter.js
- observer.js->数据变化时->触发事件$emit
- compiler.js->更新视图时->注册事件$on
observer.js
->触发事件
Observer.prototype.defineReactive = function(data, key, value) {
Object.defineProperty(data, key, {
configurable: false,
enumerable: true,
get() {
return value
},
set(newValue) {
if (value === newValue) return
value = newValue
// 当某个数据变化时,发送通知更新视图 触发事件$emit()
em.$emit(key)
}
})
}
compiler.js
-> 注册事件
Compiler.prototype.compileTextNode = function(node) {
const text = node.textContent
const reg = /\{\{(.+)\}\}/
if (reg.test(text)) {
const key = RegExp.$1.trim()
node.textContent = text.replace(reg, this.vm.$data[key])
// 注册事件
em.$on(key, () => {
node.textContent = this.vm[key]
})
}
}
Compiler.prototype.compileElementNode = function(node) {
Array.from(node.attributes).forEach(attr => {
const name = attr.name
if (this.isDirective(name)) {
const value = attr.value
if (name === 'v-model') {
node.value = this.vm.$data[value]
// 注册事件
em.$on(value, () => {
node.value = this.vm[value]
})
}
if (name === 'v-text') {
node.textContent = this.vm.$data[value]
// 注册事件
em.$on(value, () => {
node.textContent = this.vm[value]
})
}
}
})
}
04-MVVM实现-视图变化更新数据
处理v-model的视图变化更新数据
Compiler.prototype.compileElementNode = function(node) {
Array.from(node.attributes).forEach(attr => {
const name = attr.name
if (this.isDirective(name)) {
const value = attr.value
if (name === 'v-model') {
node.value = this.vm.$data[value]
// 注册事件
em.$on(value, () => {
node.value = this.vm[value]
})
// 视图更新,更改数据
node.oninput = () => {
this.vm[value] = node.value
}
}
if (name === 'v-text') {
node.textContent = this.vm.$data[value]
// 注册事件
em.$on(value, () => {
node.textContent = this.vm[value]
})
}
}
})
}
05-CSR演示和特点
黑马头条项目演示
Client Side Render (客户端渲染 CSR):页面初始加载的 HTML 文档中无核心内容,需要下载执行 js 文件,由浏览器动态生成页面,并通过 JS 进行页面交互事件与状态管理
- 优点:适合前后端分离开发,方便维护,单页应用中几乎都是客户端渲染
- 缺点:首次加载慢,不利于 SEO
拼接数据的操作在客户端完成 审查元素看不到数据
提示: 其实我们学过服务端渲染.比如node的项目/案例-> 大量代码在服务器,最后竟然有页面
06-SSR渲染的演示和特点
Server Side Render (服务端渲染 SSR):服务器直接生成 HTML 文档返回给浏览器,但页面交互能力有限。适合于任何后端语言:PHP、Java、Python、Go 等。
- 优点:响应速度快(首屏渲染速度快),有利于 SEO
- 缺点:前后端代码混合在一起,难以开发和维护,不适合进行前后端分离开发
拼接数据的操作在服务端完成 审查元素可以看到数据
07-SPA演示和特点
SPA(单页面应用程序)
- 好处:页面导航不用刷新整个页面,**体验好,**有利于前后端分离开发
- 缺点:不利于 SEO(因为单页面应用中都是使用客户端渲染的方式),还有首次响应慢(第1次要加载大量的公共资源)
SPA是效果,接近于原生应用
SPA面向用户->体验好->解决用户的痛点:用户是最没耐心/吝啬/想体验好的服务的人-
08-Vue的SSR介绍
Vue的SSR文档
- 利于ToC(面向客户)项目的SEO
- 提高首页渲染速度
Vue的SSR代码既包含客户端部分,又包含服务端部分
isomorphic web apps(同构应用):基于react、vue框架,客户端渲染和服务器端渲染的结合,在服务器端执行一次,用于实现服务器端渲染(首屏直出),在客户端再执行一次,用于接管页面交互,核心解决SEO和首屏渲染慢的问题。
- 单页面 + 服务端渲染
09-Vue的SSR演示
Vue的SSR文档
- 安装vue vue-server-renderer
- 渲染一个Vue实例
- 服务端渲染
以上所有操作和编码都是在服务端代码
问题: 目前服务端的代码既有客户端的又有服务端的,此时可以用第三方框架实现Nuxt
10-Nuxt介绍
说明
- Nuxt不是Vue官方提供的
- Nuxt是基于Vue的服务端渲染的框架
- Nuxt.js 预设了利用 Vue.js 开发服务端渲染的应用所需要的各种配置。
作用
基于 Vue、Webpack 和 Babel Nuxt.js 集成了以下组件/框架,用于开发完整而强大的 Web 应用:
Babel作用: 编译各种各样的js->转换浏览器可以认识的js(其中各种各样的js包含ES6/ES7/ES8/ES9/typescript/jsx等)
- Vue 2
- Vue-Router
- Vuex (当配置了 Vuex 状态树配置项 时才会引入)
- Vue 服务器端渲染 (排除使用
mode: 'spa'
) - Vue-Meta
11-Nuxt创建项目
-
mkdir <项目名>
-
$ cd <项目名>
-
npm init
{ "name": "my-app", "scripts": { // npm的功能:自定义指令(封装指令用的) // "start" -> npm start "dev": "nuxt" } }
-
npm install --save nuxt
-
mkdir pages
-
创建第一个页面->
pages/index.vue
:<template> <h1>Hello world!</h1> </template>
-
npm run dev
- 自动生成路由配置
- 生成.nuxt文件夹(编译客户端和服务端代码的结果)
- 自动修改代码重新编译
-
新建组件pages/list.vue->自动生成路由/自动重启
12-Nuxt路由
- 按照路由的语法规定处理动态路由
- 新建pages/user/_id.vue
- 测试 /user/100
- 处理路由嵌套
- 新建组件pages/user.vue->父组件 设置
父组件名字user 子组件所在的文件夹同名user
13-Nuxt验证服务端渲染和单页应用
- 验证是否是服务端渲染的
- 审查元素->看是否可以看到页面内容
- 编写log->看服务端控制台和客户端控制台->二者都执行
14-Nuxt的asyncData
- 作用: 为组件data提供数据
- 特点
- this->没意义
- 顺序:在组件创建之前beforeCreate执行
- 服务端(自定执行)和客户端(路由更新 )都会执行
- 场景: 获取首屏数据 axios请求
文档
测试接口
index.vue
<template>
<div id="index">
<h1>Index</h1>
<p>{{ msg }}</p>
<nuxt-link to="/user/one">链接1</nuxt-link>
<nuxt-link to="/user/200">链接2</nuxt-link>
<ul>
<li v-for="(item, index) in userList" :key="index">
{{ item.title }}
</li>
</ul>
</div>
</template>
<script>
import axios from 'axios'
// console.log('test----')
export default {
// 1. 组件创建之前被调用(beforeCreate之前)->服务端执行
// 2. 路由跳转的时候会执行->客户端执行
// 3. asyncData返回的数据 会被融合到组件的data中
// 4. asyncData的this不是组件的实例 而是undefined
async asyncData() {
console.log(this)
console.log('asyncData-----')
const { data } = await axios.get(
`https://jsonplaceholder.typicode.com/todos`
)
return {
userList: data
}
},
data() {
return {
msg: 'abc'
}
}
}
</script>
<style></style>
15-Nuxt的服务端和客户端发送请求的区别
axio支持客户端和服务器(node)环境
- 客户端: 请求 <-> 接口服务器
- 服务器: 客户端<->中间件(asyncData中axios代码)<->接口服务器
难点: 中间件二次处理的数据操作(接口数据不便于直接使用)
16-Nuxt的生命周期钩子函数
需要注意的是,在任何 Vue 组件的生命周期内, 只有 beforeCreate
和 created
这两个方法会在 客户端和服务端被调用。其他生命周期函数仅在客户端被调用。
前后端都调用:
- beforeCreate
- created
在服务端渲染期间不被调用:
- beforeMount
- mounted
- beforeUpdate
- updated
- activated
- deactivated
- beforeDestroy
- destroyed
beforeCreate() {
console.log('beforeCreate------')
console.log(this.$isServer)
},
17-Nuxt脚手架创建项目
- 使用Nuxt脚手架创建项目->文档
- 执行项目
- 熟悉目录结构
pages是需要关注的
18-案例-准备素材-介绍
案例: TODOlist->表格增删改查
xxxyyy123a@xxxyy.cn 12345678
- 在线示例:
- https://demo.realworld.io/
- 接口文档:
- https://github.com/gothinkster/realworld/tree/master/api
- 页面模板:
- https://github.com/gothinkster/realworld-starter-kit/blob/master/FRONTEND_INSTRUCTIONS.md
- 所需资源
- https://github.com/gothinkster/realworld-starter-kit
19-案例-页面布局
-
在页面引入样式资源->资源链接
- 视图-模板
- 新建app.html
- 引入样式文件
<!DOCTYPE html> <html {{ HTML_ATTRS }}> <head {{ HEAD_ATTRS }}> <link href="//code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css" rel="stylesheet" type="text/css"> <link rel="stylesheet" href="//demo.productionready.io/main.css"> {{ HEAD }} </head> <body {{ BODY_ATTRS }}> {{ APP }} </body> </html>
-
有公共头部和尾部->使用Nuxt提供的视图布局
- Nuxt-视图-布局
- 默认layouts/default.vue
<template> 头部->来源于RealWorld素材 <nuxt/> 尾部->来源于RealWorld素材 </template>
-
修改了根目录的文件,需要重启,测试
20-案例-静态页面
-
准备首页index.vue
<template> <div class="home-page"> <div class="banner"> <div class="container"> <h1 class="logo-font">conduit</h1> <p>A place to share your knowledge.</p> </div> </div> <div class="container page"> <div class="row"> <div class="col-md-9"> <div class="feed-toggle"> <ul class="nav nav-pills outline-active"> <li class="nav-item"> <a class="nav-link disabled" href="">Your Feed</a> </li> <li class="nav-item"> <a class="nav-link active" href="">Global Feed</a> </li> </ul> </div> <div class="article-preview"> <!-- 将来要遍历--> </div> </div> <div class="col-md-3"> <div class="sidebar"> <p>Popular Tags</p> <div class="tag-list"> <a href="" class="tag-pill tag-default">programming</a> <a href="" class="tag-pill tag-default">javascript</a> <a href="" class="tag-pill tag-default">emberjs</a> <a href="" class="tag-pill tag-default">angularjs</a> <a href="" class="tag-pill tag-default">react</a> <a href="" class="tag-pill tag-default">mean</a> <a href="" class="tag-pill tag-default">node</a> <a href="" class="tag-pill tag-default">rails</a> </div> </div> </div> </div> </div> </div> </template> <script> export default { }; </script> <style></style>
-
准备登陆页login.vue
<template> <div class="auth-page"> <div class="container page"> <div class="row"> <div class="col-md-6 offset-md-3 col-xs-12"> <h1 class="text-xs-center">Sign up</h1> <p class="text-xs-center"> <a href="">Have an account?</a> </p> <ul class="error-messages"> <li>That email is already taken</li> </ul> <form> <fieldset class="form-group"> <input class="form-control form-control-lg" type="text" placeholder="Your Name" /> </fieldset> <fieldset class="form-group"> <input class="form-control form-control-lg" type="text" placeholder="Email" /> </fieldset> <fieldset class="form-group"> <input class="form-control form-control-lg" type="password" placeholder="Password" /> </fieldset> <button class="btn btn-lg btn-primary pull-xs-right"> Sign up </button> </form> </div> </div> </div> </div> </template> <script> export default {}; </script> <style></style>
-
准备文章详情页articles/_slug.vue
<template> <div class="article-page"> <div class="banner"> <div class="container"> <h1>How to build webapps that scale</h1> <div class="article-meta"> <a href=""><img src="http://i.imgur.com/Qr71crq.jpg" /></a> <div class="info"> <a href="" class="author">Eric Simons</a> <span class="date">January 20th</span> </div> <button class="btn btn-sm btn-outline-secondary"> <i class="ion-plus-round"></i> Follow Eric Simons <span class="counter">(10)</span> </button> <button class="btn btn-sm btn-outline-primary"> <i class="ion-heart"></i> Favorite Post <span class="counter">(29)</span> </button> </div> </div> </div> <div class="container page"> <div class="row article-content"> <div class="col-md-12"> <p> Web development technologies have evolved at an incredible clip over the past few years. </p> <h2 id="introducing-ionic">Introducing RealWorld.</h2> <p>It's a great solution for learning how other frameworks work.</p> </div> </div> <hr /> <div class="article-actions"> <div class="article-meta"> <a href="profile.html"><img src="http://i.imgur.com/Qr71crq.jpg" /></a> <div class="info"> <a href="" class="author">Eric Simons</a> <span class="date">January 20th</span> </div> <button class="btn btn-sm btn-outline-secondary"> <i class="ion-plus-round"></i> Follow Eric Simons <span class="counter">(10)</span> </button> <button class="btn btn-sm btn-outline-primary"> <i class="ion-heart"></i> Favorite Post <span class="counter">(29)</span> </button> </div> </div> <div class="row"> <div class="col-xs-12 col-md-8 offset-md-2"> <form class="card comment-form"> <div class="card-block"> <textarea class="form-control" placeholder="Write a comment..." rows="3"></textarea> </div> <div class="card-footer"> <img src="http://i.imgur.com/Qr71crq.jpg" class="comment-author-img" /> <button class="btn btn-sm btn-primary"> Post Comment </button> </div> </form> <div class="card"> <div class="card-block"> <p class="card-text">With supporting text below as a natural lead-in to additional content.</p> </div> <div class="card-footer"> <a href="" class="comment-author"> <img src="http://i.imgur.com/Qr71crq.jpg" class="comment-author-img" /> </a> <a href="" class="comment-author">Jacob Schmidt</a> <span class="date-posted">Dec 29th</span> </div> </div> <div class="card"> <div class="card-block"> <p class="card-text">With supporting text below as a natural lead-in to additional content.</p> </div> <div class="card-footer"> <a href="" class="comment-author"> <img src="http://i.imgur.com/Qr71crq.jpg" class="comment-author-img" /> </a> <a href="" class="comment-author">Jacob Schmidt</a> <span class="date-posted">Dec 29th</span> <span class="mod-options"> <i class="ion-edit"></i> <i class="ion-trash-a"></i> </span> </div> </div> </div> </div> </div> </div> </template> <script> export default { } </script> <style> </style>
自动生成路由,可以通过手动修改标识进行测试
21-案例-axios模块
使用Nuxt提供的axios模块进行请求->文档
- 安装
- 配置
22-案例-文章列表
nuxt.config.js
export default {
mode: "universal",
head: {
title: process.env.npm_package_name || "",
meta: [
{ charset: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
{
hid: "description",
name: "description",
content: process.env.npm_package_description || ""
}
],
link: [{ rel: "icon", type: "image/x-icon", href: "/favicon.ico" }]
},
loading: { color: "#fff" },
css: [],
plugins: [],
buildModules: [],
modules: ["@nuxtjs/axios"],
axios: {
// proxyHeaders: false
baseURL: `https://conduit.productionready.io/api`
},
build: {
extend(config, ctx) {}
}
};
index.vue
async asyncData({ $axios }) {
const { articles } = await $axios.$get("/articles");
console.log(articles);
return { articles };
}
// 遍历数据->测试