一、需求:
微信小程序部分页面需要一键长图的功能。
通过html2canvas插件可以实现,具体可参考微信小程序实现一键长图并保存图片到相册。由于该插件只能在H5项目中使用,则需要截图的小程序页面点击后跳转到H5页面,把所需的token及接口参数带过去,在H5页面进行长截图并保存或转发,点击左上角返回即回到小程序。
二、思路:
首先,考虑需要长截图的页面根据需求可能会迭代增加,新建了一个vue项目,基于vue-cli
脚手架创建了一个项目;
其次,H5页面最重要的就是适配,决定使用rem,根据相关资料发现使用postcss-pxtorem和lib-flexible可以实现vue项目自动将px转成rem
,进行相关下载,配置,在写具体页面时可直接使用px。
最后,打包部署到服务上,微信小程序通过web-view加载该H5页面,在H5页面中引入html2canvas
插件实现页面长截图。
1. 搭建项目,通过rem适配机型
2. 页面请求、路由跳转、插件引入、rem配置、跨域问题
3. 截图样式,滚动、提示
4. 小程序跳转H5传参,H5接收参数
5. 动态表头匹配问题、截图时新结构的添加
6. 取消截图,通过点击触发实现
7. 项目打包部署,替换小程序中跳转的服务地址
8. git创建仓库,上传代码
三、代码:
H5项目
首先安装这两个包
npm install amfe-flexible --save
npm install postcss-pxtorem --save-dev
app.vue
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<script>
export default {
name: "app",
data () {
return {
// token: ''
}
},
created() {
let params = window.location.href.split("?")[1]
let token = params.substring(params.lastIndexOf("=")+1)
sessionStorage.setItem('token', token)
}
};
</script>
<style>
*{
margin: 0;
padding: 0;
}
html,body{
width: 100%;
height: 100%;
}
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
width: 100%;
height: 100%;
overflow-x:hidden;
}
</style>
main,js
import Vue from "vue";
import App from "./App.vue";
import {
Toast,
} from "vant";
import "amfe-flexible/index.js";
import router from "./router";
import axios from 'axios'
import html2canvas from 'html2canvas'
Vue.prototype.$html2canvas = html2canvas
Vue.prototype.$http = axios
// import Echarts from 'echarts'
// Vue.prototype.echarts = Echarts
// Vue.use(Echarts)
Vue.use(Toast);
Vue.config.productionTip = false;
new Vue({
router,
render: h => h(App)
}).$mount("#app");
untils/request.js
import axios from 'axios'
const service = axios.create({
// 请求基础路径
// baseURL: process.env.VUE_APP_BASE_API,
baseURL: '/jupiter/i',
timeout: 5000 // 请求超时时间5秒
})
// 请求拦截器 给请求头设置token
service.interceptors.request.use(
request => {
// 携带token
let token = sessionStorage.getItem('token')
if (token) {
request.headers['authorization'] = token
}
if (request.method === 'post') {
request.params = {}
}
// eslint-disable-next-line no-unused-expressions
error => {
return Promise.reject(error)
}
return request
}
)
// response interceptor
service.interceptors.response.use(
response => {
const res = response.data
console.log('axios的res', res)
const {status} = res
switch (status) {
// token失效!
case 710:
console.log('token失效,请返回小程序')
break
case 0:
console.log('请返回小程序')
break
// 统一处理200和错误message
case 200:
return res.data
default:
console.log
}
},
// 作用 catch的处理
error => {
const { response } = error
if (response && response.status === 404) {
return Promise.reject(error)
}
if (response && response.status === 500) {
return Promise.reject(error)
}
if (response && response.status === 405) {
return Promise.reject(error)
}
return Promise.reject(error)
}
)
export default service
api/test.js
import service from '../untils/request.js';
// 列表
export function boxDataList (data) {
return service({
url: '/box/day/boxData/list',
method: 'get',
params: data
})
}
// 大盘数据
export function boxDataMax (data) {
return service({
url: '/box/day/boxData/max',
method: 'get',
params: data
})
}
vue.config.js
module.exports = {
publicPath: '/',
css: {
loaderOptions: {
postcss: {
plugins: [
require("postcss-pxtorem")({
// 把px单位换算成rem单位
rootValue: 75, // vant官方使用的是75
selectorBlackList: ["vant", "mu"], // 忽略转换正则匹配项
propList: ["*"]
})
]
}
}
},
devServer: {
open: true, //是否自动弹出浏览器页面
host: "0.0.0.0",
port: "8081",
https: false,
hotOnly: false,
proxy: {
"/jupiter/i": {
target: "https://testdata.aaa.com",
ws: true, //代理websockets
changeOrigin: true, // 虚拟的站点需要更管origin
pathRewrite: {
//重写路径 比如'/api/aaa/ccc'重写为'/aaa/ccc'
"^/": ""
}
}
}
}
};
router.index.js
import Vue from "vue";
import VueRouter from "vue-router";
Vue.use(VueRouter);
const router = new VueRouter({
mode: 'history',
base: '/',
routes: [
{
path: '/dayList',
name: '日榜',
component: () => import('./../components/dayList.vue')
},
// {
// path: "/home",
// name: "年榜",
// component: () => import("./../components/Home.vue")
// },
]
});
export default router;
components/dayList.vue
<template>
<div class="dayListBox" @click="cancel">
<div ref="area">
<div class="logoBox" style="display: none">
<img src="../assets/1.png" alt="">
</div>
<div class="headBox">
<div class="headTitle">日票房榜<span>({{dateTime}}·{{platformIdName}})</span></div>
<div class="longPic" @click="generateImage" style="display: block">
<img src="../assets/long.png" alt="">
生成长图
</div>
<div class="datetime">
<div class="today">
<div class="today-num">
{{boxMaxData.sum}}<span class="wan">万</span>
</div>
<div class="explain">
次日更新,数据来自爱奇艺、优酷、腾讯
</div>
<div class="dayTop" v-if="platformIdIndex > 0"><span>日最高票房:{{boxMaxData.dayMax ? boxMaxData.dayMax : '--'}}万元</span><span class="platProp">平台占比:{{boxMaxData.pre}}%</span></div>
</div>
<div class="daypalt" v-if="platformIdIndex == 0"><span>爱奇艺:{{boxMaxData.aqy ? boxMaxData.aqy : '--'}}万元</span><span>优酷:{{boxMaxData.yk ? boxMaxData.yk : '--'}}万元</span><span>腾讯视频:{{boxMaxData.tx ? boxMaxData.tx : '--'}}万元</span></div>
</div>
</div>
<div class="interval"></div>
<div class="listBox">
<div class="boxOfficeList">
<div class="dailyList">票房排名</div>
</div>
<div class="tableList">
<div class="listHead">
<div>排名</div>
<div>影片名</div>
<div class="dayBoxOff">当日票房(万)</div>
<div v-for="(item, index) in selectListed" :key="index">{{item.nameC}}</div>
</div>
<div class="li" v-for="(item, index) in list" :key="index">
<div>
<div>{{ item.top }}</div>
</div>
<div>
<div class="name">
<span class="newTip" v-if="item.issueDay<=3">new </span>{{ item.name }}
</div>
<div class="file">上映{{item.issueDay ? item.issueDay : '--'}}天
<img v-if="item.platformId1 == '1'" class="imgStyle1 aqy" src="../assets/aqy.png">
<img v-if="item.platformId2 == '2'" class="imgStyle1 yk" src="../assets/yk.png">
<img v-if="item.platformId3 == '3'" class="imgStyle1 tx" src="../assets/tx.png">
</div>
</div>
<div>
<div class="dayBoxOff">{{ item.income }}</div>
</div>
<div>
{{ showList[index]['async' + 0] ? showList[index]['async' + 0] : '--' }}
</div>
<div>
{{ showList[index]['async' + 1] ? showList[index]['async' + 1] : '--' }}
</div>
</div>
</div>
</div>
<div class="codeBox" style="display: none">
<div class="codeBoxMain">
<div class="codeBoxLeft">
<img src="../assets/2.png" alt="">
</div>
<div class="codeBoxRight">
<p>AA云AA服务</p>
<p>互联网视频数据查询平台</p>
</div>
</div>
</div>
</div>
<div class="imgBox" style="display: none">
<div class="imgMain">
<p style="font-size: 14px; font-weight: bold; color: rgb(88, 88, 88);">长按下图保存或分享</p>
<div class="canvasImg">
<img src="" alt="" id="canvasPic">
</div>
</div>
</div>
</div>
</template>
<script>
import { boxDataList, boxDataMax } from '../api/test.js'
export default {
name:"home",
data() {
return {
params: {},
href: '',
isShow: true,
queryParam: {
dateStart: '2021-07-27',
dateStop: '',
platformId: 0
},
platformIdIndex: 0,
platformIdName: '全网',
dateTime: '2021-07-27',
list: [],
selectList: [],
selectListed: [],
showList: [],
items1: [{
name: '排名比',
value: '01',
checked: false,
nameC: '排名比',
backendName: 'topR'
},
{
name: '日环比',
value: '04',
checked: true,
nameC: '日环比',
backendName: 'incomeR'
},
{
name: '累计票房',
value: '05',
checked: true,
nameC: '累计票房(万)',
backendName: 'tIncome'
},
{
name: '当日观影人次',
value: '07',
checked: false,
nameC: '当日观影人次(万)',
backendName: 'ev'
},
{
name: '累计观影人次',
value: '09',
checked: false,
nameC: '累计观影人次(万)',
backendName: 'tEv'
},
{
name: '制作成本',
value: '12',
checked: false,
nameC: '制作成本(万)',
backendName: 'cost'
}
],
maxParam: {
dateStart: '2021-07-27',
platformId: 0
},
boxMaxData: {
sum: '',
},
}
},
created() {
// dateStart=2021-08-01&platformId=0&selectList=incomeR,tIncome
this.href = window.location.href.split("?")[1]
this.parseParams()
this.getBoxDataList(this.queryParam)
this.getBoxDataMax(this.maxParam)
},
mounted() {
},
methods: {
// 小程序中的表头是动态的,将表头对应的name带过来,先将其字符串分割成数组,selectList则是后两列表头
parseParams () {
let urls = this.href
let theRequest = new Object()
if (urls.indexOf("?") == -1) {
let strs = urls.split("&") // ['dateStart=2021-08-01', 'platformId=0', 'selectList=incomeR,tIncome']
for(var i = 0; i < strs.length; i ++) {
theRequest[strs[i].split("=")[0]]= strs[i].split("=")[1]
}
this.params = theRequest
this.dateTime = this.params.dateStart
this.platformIdIndex = this.params.platformId
if (this.platformIdIndex == 0) {
this.platformIdName = '全网'
} else if (this.platformIdIndex == 1) {
this.platformIdName = '爱奇艺'
} else if (this.platformIdIndex == 2) {
this.platformIdName = '优酷'
} else if (this.platformIdIndex == 3) {
this.platformIdName = '腾讯视频'
}
this.queryParam.dateStart = this.params.dateStart
this.queryParam.platformId = this.params.platformId
this.maxParam.dateStart = this.params.dateStart
this.maxParam.platformId = this.params.platformId
// 将字符串转成数组,通过数组中的每一项和后两列所有可能是表头的数组items1对比,如果相等,则将该项push到新的数组,以便于html结构中进行循环渲染其中文name
this.selectList = this.params.selectList.split(',') // ["incomeR","tIncome"]
this.selectList.forEach((selectListItem) => {
let obj = {}
this.items1.forEach(listItem => {
if (listItem.backendName == selectListItem) {
obj = listItem
}
})
this.selectListed.push(obj)
})
// console.log(theRequest) // {dateStart: "2021-08-01", platformId: "0", selectList: "incomeR,tIncome"}
}
},
generateImage () {
// 点击截图时,页面要头部添加logo,底部添加二维码及名称
// 通过在截图前将该结构进行显示,以达到截图效果
document.getElementsByClassName('logoBox')[0].style.display = "block"
document.getElementsByClassName('longPic')[0].style.display = "none"
document.getElementsByClassName('codeBox')[0].style.display = "block"
const rect = this.$refs.area.getBoundingClientRect() // 关键代码
// console.log(rect)
document.body.scrollTop = 0
document.documentElement.scrollTop = 0
this.$html2canvas(this.$refs.area, {
scrollY: rect.top, // 关键代码
height: rect.height + 50 // 加高度,避免截取不全
}).then(canvas => {
canvas.toBlob(() => {
var imgBoxEle = document.getElementsByClassName('imgBox')[0]
imgBoxEle.style.display = "block"
// console.log('blob', blob)
this.imgUrl = canvas.toDataURL('image/jpeg')
const aImg = document.getElementById('canvasPic')
aImg.style = 'width: 100%;height: auto;-webkit-touch-callout: none;margin-left: 20'
aImg.src = this.imgUrl
}, 'image/png')
})
},
ellipsis (val) {
if (val.length > 8) {
return val.substr(0, 7) + '...'
}
return val
},
getBoxDataList (queryParam) {
boxDataList(queryParam).then(data => {
data.list.forEach(item => {
if (item.platformId) {
if (item.platformId.indexOf('1') !== -1) {
item.platformId1 = '1'
}
if (item.platformId.indexOf('2') !== -1) {
item.platformId2 = '2'
}
if (item.platformId.indexOf('3') !== -1) {
item.platformId3 = '3'
}
}
if (item.incomeR) {
item.incomeR = item.incomeR + '%'
}
item.name = this.ellipsis(item.name)
})
this.list = data.list
this.list.forEach(listItem => {
const obj = {}
this.selectList.forEach((selectListItem, i) => {
obj['async' + i] = listItem[selectListItem]
})
this.showList.push(obj)
})
})
},
getBoxDataMax (maxParam) {
boxDataMax(maxParam).then(data => {
this.boxMaxData = data
})
},
cancel () {
document.getElementsByClassName('logoBox')[0].style.display = "none"
document.getElementsByClassName('longPic')[0].style.display = "block"
document.getElementsByClassName('codeBox')[0].style.display = "none"
document.getElementsByClassName('imgBox')[0].style.display = "none"
}
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="less">
.imgBox {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #eee;
color: #607d8b;
text-align: center;
z-index: 10000;
.imgMain {
position: relative;
margin: 0;
padding: 0 0 15px 0;
width: 100%;
max-width: 750px;
min-width: 300px;
height: auto;
top: 0;
left: 50%;
transform: translate(-50%, 0);
p {
padding: 10px 0px;
}
.canvasImg {
max-height: 1300px;
position: relative;
margin-left: 30px;
margin-right: 30px;
border-radius: 10px;
overflow-x: hidden;
overflow-y: auto;
img {
width: 100%;
height: auto;
background: #525252;
}
}
}
}
.interval {
height: 21px;
background-color: #F7F7F7;
}
.logoBox {
img {
width: 100%;
height: 300px;
}
}
/* 头部 */
.headBox {
width: 100%;
position: relative;
}
.headTitle {
color: #000;
font-size: 36px;
display: inline-block;
padding: 10px 0 0 20px;
}
.headTitle span {
color: #888;
font-size: 22px;
}
.longPic img {
display: block;
padding-left: 40px;
width: 40px;
height: 40px;
}
.longPic {
display: inline-block;
font-size: 26px;
color: #E8380D;
position: absolute;
right: 20px;
top: 10px;
}
.wan {
font-size: 20px;
color: #E8380D;
}
/* 日期 */
.datetime {
display: flex;
flex-direction: row;
height: 240px;
position: relative;
}
.today {
flex: 1.25;
height: 54px;
background-color: #fff;
font-size: 26px;
border-radius: 8px;
margin-top: 42px;
color: #000;
display: flex;
align-items: center;
justify-content: center;
}
.today-num {
color: #E8380D;
font-size: 60px;
font-weight: 600;
margin-top: 6px;
}
.explain {
font-size: 22px;
color: #888888;
position: absolute;
top: 120px;
}
.dayTop {
position: absolute;
top: 180px;
font-size: 22px;
color: #888888;
}
.dayTop span {
flex: 1;
text-align: center;
}
.platProp {
padding-left: 50px;
}
.daypalt {
position: absolute;
top: 180px;
font-size: 22px;
color: #888888;
width: 100%;
display: flex;
justify-content: center;
}
.daypalt span {
flex: 1;
text-align: center;
}
/* 列表头部 */
.boxOfficeList {
height: 76px;
line-height: 76px;
overflow: hidden;
}
.dailyList {
display: inline-block;
font-size: 26px;
color: #000000;
padding-left: 35px;
}
/* 列表样式 */
.tableList {
font-size: 21px;
}
.listHead {
display: flex;
text-align: center;
align-items: center;
height: 70px;
background-color: #F7F7F7;
color: #333;
font-size: 24px;
}
.listHead div:nth-child(1){
flex: 1;
}
.listHead div:nth-child(2){
text-align: left;
flex: 3;
}
.listHead div:nth-child(3){
flex: 1.5;
}
.listHead div:nth-child(4){
flex: 1.5;
}
.listHead div:nth-child(5){
flex: 1.5;
position: relative;
}
/* 表体样式 */
.dayBoxOff {
color: #E8380D;
}
.newTip {
color: #E1BC74;
font-size: 24px;
}
.li {
display: flex;
flex-direction: row;
text-align: center;
background-color: #fff;
color: #333;
justify-content: center;
height: 100px;
line-height: 100px;
}
.li:nth-child(2n-1) {
background-color: #F7F7F7;
}
.li div:nth-child(1) {
flex: 1;
font-size: 26px;
}
.imgStyle {
width: 39px;
height: 39px;
padding-top: 21px;
}
.imgStyle1 {
padding-left: 10px;
}
.li div:nth-child(2) {
position: relative;
flex: 3;
text-align: left;
line-height: 70px;
font-size: 23px;
}
.name {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
font-size: 24px;
color: #000000;
line-height: 62px;
height: 58px;
}
.fileNum {
padding-left: 4px;
}
/* 上映样式 */
.li div:nth-child(2) .file {
font-size: 20px;
color: #999;
line-height: 8px;
}
.li div:nth-child(2) .file div {
display: inline-block;
}
.li div:nth-child(3) {
flex: 1.5;
font-size: 24px;
color: #333;
}
.mg {
width: 24px;
height: 26px;
}
.yk {
width: 25px;
height: 20px;
}
.aqy {
width: 24px;
height: 19px;
}
.tx {
width: 20px;
height: 20px;
}
.li div:nth-child(4) {
flex: 1.5;
font-size: 24px;
color: #333;
}
.li div:nth-child(5) {
flex: 1.5;
font-size: 24px;
color: #333;
}
.codeBox {
.codeBoxMain {
padding: 40px 0 50px 0;
display: flex;
align-items: center;
.codeBoxLeft {
flex: 1;
text-align: center;
img {
width: 200px;
height: 200px;
}
}
.codeBoxRight {
flex: 1;
font-size: 30px;
p:last-child {
font-size: 26px;
padding-top: 8px;
}
}
}
}
</style>
运行npm run build 将dist文件夹发送给后端部署即可
微信小程序:
微信小程序通过按钮点击跳转新的页面,新页面则只有web-view标签
boxList.wxml
<text class="longGraph" bindtap="longGraphClick">一键长图text>
boxList.js
longGraphClick: function (e) {
let dateStart = this.data.queryParam.dateStart
let platformId = this.data.queryParam.platformId
let selectList = [this.data.selectList[0].backendName, this.data.selectList[1].backendName]
// hash模式
// let url = 'https://testdata.aaa.com/#/dayList'
// history模式
let url = 'https://testdata.aaa.com/yearList'
wx.navigateTo({
url: '/pages/boxOfficeModule/boxOfficePic/boxOfficePic?url=' + url + '&dateStart=' + dateStart + '&platformId=' + platformId + '&selectList=' + selectList
})
},
boxOfficePic.wxml
<web-view src="{{url}}">web-view>
boxOfficePic.js
// pages/boxOfficeModule/boxOfficePic/boxOfficePic.js
Page({
/**
* 页面的初始数据
*/
data: {
url: '',
dateStart: ''
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
let k = wx.getStorageSync('token')
this.setData({
"url": options.url + '?' + 'dateStart=' + options.dateStart + '&platformId=' + options.platformId + '&selectList=' + options.selectList + '&k=' + k,
"dateStart": options.dateStart
})
console.log('跳转H5的url', this.data.url)
},
})
三、问题:
1. 在H5项目中引入插件后,浏览器上并没有将px转为rem
最初基于vue2.0及vue-cli2脚手架,通过命令vue init webpack my-proj创建了一个项目配置文件如下,运行之后,选中页面元素后发现px并没有转成rem如上图。
解决:
发现通过vue-cli3创建的项目进行上面的lib-flexible和postcss-pxtorem
相关配置成功实现了px转rem。
而针对vue-cli2则需要通过lib-flexible
和px2rem-loader
实现rem的响应。
参考:
使用postcss-pxtorem和lib-flexible来实现vue项目自动将px转成rem
vue cli3中使用postcss-pxtorem进行适配(亲测有效!)
通过插件postcss-pxtorem轻松实现px到rem转换,完成移动端适配
vue-cli3.0使用amfe-flexible + postcss-pxtorem,适配移动端并px自动转rem
vue开发之vue-cli2与vue-cli3的对比
vue-cli2用px2rem实现响应式布局
2. 运行成功的H5项目,通过手机无法打开页面
本地运行vue项目npm run serve,在浏览器打开后,将该地址复制发送到手机上,通过手机打开,页面打不开报错。
解决:
需要手机和电脑在同一网络下,将dev里的host: "localhost"改成host: “0.0.0.0”
3. npm run serve运行项目时报错Error:Loading PostCSS Plugin failed: Cannot find module ‘postcss-pxtorem‘
解决:
把文件名改成了postcss.config.js
参考:
Error:Loading PostCSS Plugin failed: Cannot find module ‘postcss-pxtorem‘报错解决
4. vue项目中的token保存
由于不涉及其他数据存储,没有使用vuex,通过sessionStorage实现。
保存token的方法
sessionStorage.setItem(‘token’,response.data.token)
获取token的方法
sessionStorage.getItem(“token”)
参考:
vue项目中token的保存和使用
5. 微信小程序通过web-view向H5页面传值
微信小程序通过
加载H5页面,在url后面拼接上所需参数即可
this.setData({
"url": options.url + '?' + 'dateStart=' + options.dateStart + '&platformId=' + options.platformId + '&selectList=' + options.selectList,
})
在H5页面通过window.location.href.split("?")[1]
获取url后面拼接的所有参数
参考:
【微信小程序】webview向h5页面传值
6. H5项目打包发布后,无法打开页面,报错
npm run build打包发给后端,部署服务后,无法打开页面,放到浏览器运行报错Uncaught SyntaxError: Unexpected token ‘<‘
,
可以看到,当出错的时候会强制跳转到 index.html 页面,而js文件是不会识别html,因此报Uncaught SyntaxError: Unexpected token < 错误
可将vue路由模式改为 history模式,并且打包路径使用绝对路径(module.exports={publicPath:"/"};)
解决:
找到vue.config.js文件,添加publicPath: '/',
找到router/index.js文件,添加new VueRouter({ mode: 'history', base: '/',})
参考:
【Vue中的坑】Vue打包上传线上报Uncaught SyntaxError: Unexpected token <
vue js报Uncaught SyntaxError: Unexpected token < 错误
vue项目部署后Uncaught SyntaxError: Unexpected token <
vue打包后页面空白,提示Uncaught SyntaxError:Unexpected token
7. H5页面截图后新增头部及底部结构
最初考虑,在截图的时候如果给图片画进去新的内容,后面发现有点困难。
采取提前将截图写好,给其设置style="display: none"样式,当触发截图事件时,在截图之前将其样式设置为display: block。
8. H5页面截图后新增头部及底部结构
最初考虑,在截图的时候如果给图片画进去新的内容,后面发现有点困难。
采取提前将截图写好,给其设置style="display: none"样式,当触发截图事件时,在截图之前将其样式设置为display: block。
9. 当数据列表多的时候,在浏览器截图不全、真机图片不显示
当列表数据很多的时候,在浏览器上截图时,会比较慢卡顿并截取不全底部的二维码。在小程序中打开时页面空白,没有图片。
考虑高度设置、元素显示隐藏、设置定时器发现都无法解决。
猜测可能是图片太长,生成的base64的url图片地址不对,具体还不确定,随后将页面数据都设定为最多显示50条可以正常使用截图功能。
通过引入vConsole,在真机上可以看到打印内容,发现数据过多时出现了报错。
vConsole使用
四、相关文章:
手机端页面自适应解决方案—rem布局
移动端(手机端)页面自适应解决方案—rem布局篇
vue开发H5步骤详解(环境搭建+rem布局+域名配置+请求)
vue.cli项目封装全局axios,封装请求,封装公共的api和调用请求的全过程
关于vue-cli3中配置请求跨域的问题
vue-cli3 配置请求代理,去除开发环境api跨域问题
在vue项目中如何关闭Eslint校验
解决html2canvas截图不全问题
Vue-router 中hash模式和history模式的区别
vue路由的两种模式,hash与history的区别