目录
一. 项目概述
二、项目初始化
移动端 REM 适配:
关于 PostCSS 配置文件:
Autoprefixer 插件的配置 :
postcss-pxtorem 插件的配置:
关于字体图标:
配置路由:
封装请求模块:
三:登录注册:
存储用户 Token:
关于 Token 过期问题:
四:个人中心
五、首页—文章列表:
关于第三方图片资源403问题:
处理时间:
六、首页—频道编辑:
频道数据持久化:
正确的获取首页频道列表数据图:
七、文章搜索
八、文章详情
关于后端返回数据中的大数字问题:
关于文章正文的样式:
九、文章评论
十、 编辑资料
图片裁切:
方式一:结合服务器方式
方式二:纯客户端方式
十一. 小智同学
WebSocket:
使用原生 WebSocket(了解):
Socket.IO(了解体验):
十二: 功能优化
组件缓存:
响应拦截器:
1. 有哪些业务:
本项目涵盖了新闻资讯类 App 的重点核心功能,其中包括:用户登录 / 注册、文章列表、文章详情、文章评论、个人资料、用户中心、频道管理、文章搜索、用户收藏 / 历史 / 作品,以及机器人聊天等功能。
2. 用的什么技术栈?
本项目采用完全前后端分离的开发模式,使用 Vue.js 技术栈(全家桶)构建的移动端 SPA 单页面应用程序。
3. 业务功能:
本项目的目标是完成一个新闻资讯类 App 的核心功能,但项目中所覆盖的核心业务却非常具有普适性,在众多 App 中都有体现,诸如:
基于 Token 的验证方案处理用户登录
移动端表单验证方案及交互提示
短信验证码的发送与验证
用户中心、个人资料展示
对文章内容的收藏、点赞和分享
编辑用户资料,其中包括基本信息修改及头像裁切上传等功能
列表类页面的加载、缓存及优化
列表类页面的下拉刷新和上拉加载更多
资讯类文章详情展示和文章评论
完整的搜索业务:输入联想建议、搜索关键词高亮、搜索历史记录、搜索结果列表等
即时通信核心业务及基于 WebSocket 的数据通信
4. 本项目采用了以下项技术解决方案:
基于 Vue.js 的前端框架
基于 webpack 工程化开发解决方案
基于 Vant 的前端 UI 组件库,开发效率更高
基于 axios 的请求库,功能强大性能高效
基于 RESTful 风格的数据 API 解决方案(90% 的后端均采用这种风格提供数据 API,全 栈级能力,升职加薪不在话下)
基于 JWT 的 Token 状态维持解决方案(能够强化更多服务器知识)
基于 Vue Router 的路由管理方案
基于 Vuex 的状态共享方案
基于 Vue CLI 的脚手架工具,快速创建项目快速开发
基于 Socket.IO 的实时通信解决方案
基于 PostCSS 的移动端 REM 适配解决方案
基于 DCloud 平台的移动 App 开发解决方案
删除初始化的默认文件
新增调整我们需要的目录结构,下载vuex,router路由,less,vant组件库,axios
├── src
│ ├── api 存储 API 请求模块
│ ├── router Vue Router 路由模块
│ ├── store Vuex 容器模块
│ ├── styles 存储全局样式资源目录,在main.js中引入
│ ├── utils 存储工具模块
│ ├── views 存储视图组件
│ ├── App.vue 根组件
│ └── main.js 入口模块
Vant 中的样式默认使用 px
作为单位,如果需要使用 rem
单位,推荐使用以下两个工具:
下面我们分别将这两个工具配置到项目中完成 REM 适配。
1. 使用 lib-flexible 动态设置 REM 基准值(html 标签的字体大小)
安装依赖:
# yarn add amfe-flexible npm i amfe-flexible
然后在
main.js
中加载执行该模块:import 'amfe-flexible'
最后测试:在浏览器中切换不同的手机设备尺寸,观察 html 标签
font-size
的变化。
2. 使用 postcss-pxtorem 将
px
转为rem
安装依赖:
# yarn add -D postcss-pxtorem
# -D 是 --save-dev 的简写
npm install postcss-pxtorem -D然后在项目根目录中创建
postcss.config.js
文件:module.exports = { plugins: { 'autoprefixer': { browsers: ['Android >= 4.0', 'iOS >= 8'] }, 'postcss-pxtorem': { rootValue: 37.5, propList: ['*'] } } }
配置完毕,重新启动服务。
最后测试:刷新页面,审查元素样式查看是否已将
px
转换为rem
。需要注意的是:
- 该插件不能转换行内样式中的
px
,例如
postcss.config.js
是 PostCSS 的配置文件。
PostCSS 是一个处理 CSS 的处理工具,本身功能比较单一,它主要负责解析 CSS 代码,再交由插件来进行处理,它的插件体系非常强大,所能进行的操作是多种多样的,例如:
- Autoprefixer 插件可以实现自动添加浏览器相关的声明前缀
- PostCSS Preset Env 插件可以让你使用更新的 CSS 语法特性并实现向下兼容
- postcss-pxtorem 可以实现将 px 转换为 rem
- ...
目前 PostCSS 已经有 200 多个功能各异的插件。开发人员也可以根据项目的需要,开发出自己的 PostCSS 插件。
PostCSS 一般不单独使用,而是与已有的构建工具进行集成。
Vue CLI 默认集成了 PostCSS,并且默认开启了 autoprefixer 插件。
Vue CLI 内部使用了 PostCSS。
可以通过
.postcssrc
或任何 postcss-load-config 支持的配置源来配置 PostCSS。也可以通过vue.config.js
中的css.loaderOptions.postcss
配置 postcss-loader。我们默认开启了 autoprefixer。如果要配置目标浏览器,可使用
package.json
的 browserslist 字段。
autoprefixer 是一个自动添加浏览器前缀的 PostCss 插件,browsers
用来配置兼容的浏览器版本信息,但是写在这里的话会引起以下编译器警告:
Replace Autoprefixer browsers option to Browserslist config.
Use browserslist key in package.json or .browserslistrc file.
Using browsers option can cause errors. Browserslist config
can be used for Babel, Autoprefixer, postcss-normalize and other tools.
If you really need to use option, rename it to overrideBrowserslist.
Learn more at:
https://github.com/browserslist/browserslist#readme
https://twitter.com/browserslist
警告意思就是说应该将 browsers
选项写到 package.json
或 .browserlistrc
文件中。
package.json
文件里的browserslist
字段 (或一个单独的.browserslistrc
文件),指定了项目的目标浏览器的范围。这个值会被 @babel/preset-env 和 Autoprefixer 用来确定需要转译的 JavaScript 特性和需要添加的 CSS 浏览器前缀。参考官方文档中的语法,我们将
package.json
文件里的browserslist
字段 (或一个单独的.browserslistrc
文件) 修改如下:"browserslist": [ "Android >= 4.0", "iOS >= 8" ] 或 Android >= 4.0 iOS >= 8
并将postcss.config.js中的此代码删掉
'autoprefixer': { browsers: ['Android >= 4.0', 'iOS >= 8'] },
保存之后就不报错了
rootValue
:表示根元素字体大小,它会根据根元素大小进行单位转换
propList
用来设定可以从 px 转为 rem 的属性例如
*
就是所有属性都要转换,width
就是仅转换width
属性
rootValue
应该如何设置呢?
如果使用的是基于lib-flexable的REM适配方案,则应该设置为你的设计稿的十分之一。
例如设计稿是 750 宽,则应该设置为 75。大多数设计稿的原型都是以 iPhone 6 为原型,iPhone 6 设备的宽是 750,我们的设计稿也是这样。
但是 Vant 建议设置为 37.5,为什么呢?
因为 Vant 是基于逻辑像素 375 写的,所以如果你设置为 75 的话,Vant 的样式就小了一半。
所以如果设置为
37.5
的话,Vant 的样式是没有问题的,但是我们在测量设计稿的时候都必须除2才能使用,否则就会变得很大。
有没有更好的办法不用除以2呢?
1. 不用写代码的方式(1)
在 Photoshop 中打开单位与标尺设置面板:菜单栏 -> 编辑 -> 首选项 -> 单位与标尺。
将单位中的标尺和文字的单位修改为点
打开设置图像大小面板:
菜单栏 -> 图像 -> 图像大小 快捷键:Alt + Ctrl + I
关闭重新采样
将宽度单位设置为
点
将高度单位设置为
点
将宽度修改为
375
,高度不用动(它会适应宽度自动调整)点击确定完成修改。
调整之后,我们可以看到图像的大小变成了 375 点 x 667 点(144 ppi)。
在 iPhone 6/7/8 设备下,1个点 = 2个物理像素,所以导出的图片还是原来的二倍图。
不用写代码的方式(2)
使用像素大厨的二倍图功能
2. 写代码的方式:
通过查阅文档可以看到 rootValue
支持两种参数类型:
数字:固定值
函数:动态计算返回
有一个默认参数:一个对象,其中包含一个 file 属性(编译的文件路径)
所以我们可以这样来处理它:
module.exports = {
plugins: {
'postcss-pxtorem': {
rootValue ({ file }) {
// 如果是 Vant 的样式就按照 37.5 处理转换
// 如果是我们自己的样式就按照 75 处理转换
return file.indexOf('vant') !== -1 ? 37.5 : 75
},
propList: ['*']
}
}
}
这种方式不方便调试。因为在调试面板中看到的都是逻辑像素大小,它和 750 物理像素设计稿不一致,无法很好的利用调试工具。
使用 iconfont 制作字体图标:
登录 iconfont
创建项目
上传图标到项目中 图标下载
去除颜色并提交
点击生成代码
将css代码引入public/index.html中就可直接使用
或在项目中创建
src/styles/icon.less
并写入上面css中复制到的代码,并引入main.js
注意:
@import用于在css中引入css文件
创建 src/utils/request.js
:
/**
* 封装 axios 请求模块
*/
import axios from "axios"
const request = axios.create({
baseURL: "http://ttapi.research.itcast.cn/" // 基础路径
})
export default request
哪里使用,哪里加载:
import request from '@/utils/request'
request({
method: 'xxx',
url: 'xxx',
...
})
登陆成功后后台为了区分用户是谁,服务器会下发token(令牌:唯一标识)
我们需要把它存储到一个公共的位置,方便随时取用
往哪儿存?
本地存储
获取麻烦
数据不是响应式
Vuex 容器(推荐)
获取方便
响应式的
使用容器存储 Token 的思路:
登录成功,将 Token 存储到 Vuex 容器中
获取方便
响应式
为了持久化,还需要把 Token 放到本地存储
持久化
见最后
项目中的接口除了登录之外大多数都需要提供 token 才有访问权限。
后端接口要求我们将 token 放到请求头中发送
方式一:在每次请求的时候手动添加(麻烦)
axios({
method: "",
url: "",
headers: {
Authorization(后端与前端商量好的字段): "Bearer token"
}
})
方式二:使用请求拦截器统一添加(推荐,更方便)
import axios from "axios"; 引入axios
import store from "@/store/index"; 引入store
const request = axios.create({ 创建axios实例对象
baseURL: "http://toutiao.itheima.net",
});
request.interceptors.request.use((config) => { 请求拦截器
let token = store.state.mytoken;
if (token) { 如果用户已登录,统一给接口设置 token 信息
config.headers.Authorization = `Bearer ${token.token}`;
}
return config; 处理完之后一定要把 config 返回,否则请求就会停在这里
});
export default request; 导出
问题:
假如把a列表的滚动条滚动到10的位置,然后切换到b列表并把滚动条滚动到20的位置,再次切换到a列表发现滚动条也在20的位置
为什么列表滚动会相互影响?
因为它们并不是在自己内部滚动,而是整个body页面在滚动。无论你是在a频道还是在b频道,其实滚动的都是body 元素。
怎么解决:
让每一个标签内容文章列表产生自己的滚动容器,这样就不会相互影响了。
如何让标签内容文章列表产生自己的滚动容器?
单独给每个列表设置
固定高度:height:xxx;溢出滚动:overflow-y:auto;
如果设置高100%的话没有作用,为什么?
因为百分比是相对于父元素,如果审查元素发现它所有的父元素都没有高,那肯定没有作用了。也可以用vw或vh
为什么文章列表数据中的好多图片资源请求失败返回 403?
这是因为我们项目的接口数据是后端通过爬虫抓取的第三方平台内容,而第三方平台对图片资源做了防盗链保护处理。
第三方平台怎么处理图片资源保护的?
服务端一般使用 Referer 请求头识别访问来源,然后处理资源访问。
Referer 是什么东西?
扩展参考:HTTP Referer 教程 - 阮一峰的网络日志
Referer 是 HTTP 请求头的一部分,当浏览器向 Web 服务器发送请求的时候,一般会带上 Referer,它包含了当前请求资源的来源页面的地址。服务端一般使用 Referer 请求头识别访问来源,可能会以此进行统计分析、日志记录以及缓存优化等。需要注意的是 referer 实际上是 "referrer" 误拼写。参见 HTTP referer on Wikipedia (HTTP referer 在维基百科上的条目)来获取更详细的信息。
怎么解决?
不要发送 referrer ,对方服务端就不知道你从哪来的了,姑且认为是你是自己人吧。
如何设置不发送 referrer?
1. 用 、、、
介绍
原生的 WebSocket 使用比较麻烦,所以推荐使用一个封装好的解决方案:socket.io
socket.io 提供了服务端 + 客户端的实现
- 客户端:浏览器
- 服务端:Java、Python、PHP、。。。。Node.js
对于前端开发者来说,只需要关心他的客户端如何使用即可
怎么建立连接,怎么发送消息,怎么接收消息
注意:Socket.io 必须前后端配套使用
- 实际工作中,socket.io 已经成为了各大后端开发的 WebSocket 通信主流解决方案
GitHub 仓库
官网
Socket.io 和 WebSocket 就好比它就好比 axios 和 XMLHTTPRequest 的关系。
官网:https://socket.io/get-started/chat/
Socket.IO使用:
1. 下载 npm install socket.io-client
2. 在需要使用的模块中引入
import io from "socket.io-client";
3. 在created()或mounted()中
data() {
return {
socket: null,
};
},
created() {
this.socket = io("http://toutiao.itheima.net", {
query: {
token: this.$store.state.user.token
},
transports: ['websocket'] //建立连接,第一个参数是路径,第二个参数是要求携带的参数
})
this.socket.on("disconnec", () => {
console.log("断开连接了") //断开连接的回调
})
this.socket.on("connect", () => {
console.log("建立连接成功!"); //连接成功的回调
});
this.socket.on('message', data => {
console.log('收到服务器消息:', data)//服务器收到消息的回调,这里的message是接口要求的
})
}
在methods中写:
点击发送后触发发送事件:this.socket.emit("message",消息内容) message为消息类型
消息类型和消息内容要按文档要求写
从首页切换到我的,再从我的回到首页,我们发现首页重新渲染原来的状态没有了。
首先,这是正常的状态,并非问题,路由在切换的时候会销毁切出去的页面组件,然后渲染匹配到的页面组件。
但是我想要某些页面保持状态,而不会随着路由切换导致重新渲染。
解决:
keep-alive 缓存组件
- 组件缓存不是持久化,它只是在应用运行期间不会重新渲染,如果页面刷新还是会回到初始状态。
是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。
要求被切换到的组件都有自己的名字,不论是通过组件的
name
选项还是局部/全局注册。注意:keep-alive 仅对该路由出口渲染的组件有效,二级路由无效
组件缓存造成的问题:
1. 退出登陆后再换另一个账户重新登陆,因为缓存的问题用户信息的数据没有变,还是上一 个用户的信息
解决:
在登陆成功之后跳转路由之前把缓存删除,在跳转路由之后把缓存加上
步骤:
把要缓存的组件数组存在vuex中,在mutations中设置删除缓存和添加缓存的函数,设置缓存时读取vuex中设置的值,在登陆成功之后和路由跳转之前分别调用删除和添加缓存的函数
2. 点击详情页面在返回时,滚动条回到的最顶部
解决:
给要滚动的dom添加滚动事件,添加一个变量保存在每次滚动完之后滚动条滚动的垂直距离(利用scrollTop),点击详情页返回后利用生命周期钩子activated(路由组件被激活时触发)设置滚动条的垂直滚动距离
非以 2xx 开头的状态码都会触发,都会触发响应拦截器失败的回调
console.dir() 可以打印出该对象的所有属性和属性值.
解决token过期问题:
在响应拦截器失败的回调中先判断返回的值是多少,如果是非401那么就给出相应的提示,如果返回的是401,那么继续判断是否已登录,如果未登录直接跳转到登陆页面,如果已登陆那么用已有的refresh_token作为参数发送请求去获取新的token(这里发送请求要新建一个axios的实例去发),如果发送请求成功则把返回的新的token保存在vuex中并发送请求(参数是error.config响应拦截器失败的对象),如果发送请求失败那么就跳转到登陆页面
登陆成功跳转回之前页面:
在处理token过期时(响应拦截器失败时),在跳转到登陆页面时同时加上一个query参数,
query:{xxx: router.currentRoute.fullPath}
router.currentRoute和我们在组件中获取的this.$route 是一样的,router.currentRoute.fullPath就是当前的路径
在登陆成功后判断query参数上是否有这个属性,如果有就跳转到这个页面,如果没有就跳转到首页面("/")
统一处理页面访问权限:
给每个路由都配一个路由元信息
通过路由元信息判断是否需要登陆才能访问
使用路由导航配合路由元信息控制权限访问