移动端 Web
总体认识
客户端的所有形式:Native App(IOS、Android、Mac、Windows),小程序(微信、百度、支付宝、字节跳动),桌面端网页、移动端网页(浏览器H5、webview H5、微信H5),公众号机器人(自动回复 和 主动推送)
移动端 web 的存在形式:
Native App:React Native,Weex,cordova(phoneGap)、wap2app
Web App:浏览器H5,webview H5,微信H5
小程序:微信、百度、支付宝、字节跳动
跨平台框架:Taro、Chameleon、uni-app等
资源:
Apache Cordova 是Adobe PhoneGap 贡献给Apache的开源项目,Cordova 是 PhoneGap的核心程序。
基于Cordova的方案:ionic
不复杂的应用不用框架,只要用一些库:Zepto,underscore,axios.js
一些框架:JqueryMobile/jqmobi/SenchaTouch/ionicframework
移动web开发资源收集:https://github.com/jtyjty99999/mobileTech
优秀实践:
RequireJS(按需加载)+Backbone(组织代码与路由管理)+Zepto(轻量DOM操作) + fastclick.js(点击穿透与延迟处理)+Hammer.js(各种触屏事件)+iScroll5.js(滚动条处理)+Animate.css(CSS3动画)+Enquire.js(处理响应式布局)。
屏幕与视口
见文章《移动端屏幕与视口》
Flex 布局(弹性布局)
见文章《布局》
媒体查询
见文章《响应式布局》
在 <html>标签 设置属性 data-dpr="2",则可简单得通过 [data-dpr=2] .class 来适配不同分辨率,省去媒体查询。
click事件延迟
单击后,浏览器等300ms ,来确定会不会是双击。就导致单击事件回调,延迟了300ms。
zepto.js方案
解决:用zepto.js的tap事件代替click事件。$("#id").on('tap',function(){});
tap事件原理:对比touchstart、touchend的位置和时间间隔,来判断是不是单击
点透bug:场景(两个重叠的层,上层绑定tap事件,让上层消失),
bug(会触发下层的click事件),
原因(点击后,浏览器等待300ms,此时上层已经消失,就认为是点击了下层)
解决(上层过渡300ms消失,下层也用tap事件)
fastclick.js方案
使用:FastClick.attach(document.body)
原理:touchend 冒泡到 body时,event.preventDefault()阻止后续的click事件,创建一个点击事件并触发
触摸事件
touchstart、touchend、touchmove、touchcancel
H5
H5 调用 native
//native代码:
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true); // 打开JS通道
webView.addJavascriptInterface(new JsInterface(), "control"); // 设置JS接口
public class JsInterface {
@JavascriptInterface // android 4.2 以后,有这个注解的方法 才能被 JS 访问
public void do(String s) {
log(s);
}
}
//H5代码:
javascript:control.do('some');
native 调用 H5
webView.loadUrl("javascript:do();"); // do 是一个全局函数
H5 会话:
1、native将会话信息 设置进 webview 的 cookie
2、native将会话信息 加到 url 的参数里
H5 登陆状态调试:
1、在web端登陆,复制其cookie中的会话信息,再在H5中用JS 将会话信息 写入cookie
2、H5坐一个登陆页面,未登陆就跳到此页面进行认证,登陆后再调试
真机调试
见文字《移动端H5调试》
优化
移动端优化
1、优化等待体验:显式加载效果,滚到可视范围再加载响应资源
2、减少图片:用样式和iconfont代替图片
3、重视渲染优化,见上方
4、动画,过渡,转换使用CSS,而不是JS。可以触发GPU硬件加速
移动端H5调试
Chrome Remote Debug
参考:https://developers.google.com/chrome-developer-tools/docs/remote-debugging
PC准备:
1、安装chrome
2、chrome 打开 Remote devices,勾选 Discover USB devices
(1)地址方式打开:地址栏输入 chrome://inspect/#deviceswebview
(2)DevTool入口打开:打开DevTool -》右上角点点点 -》 More tools -》Remote devices
3、连外网
Android准备:
1、设置 -》开发者选项 -》打开USB调试
2、安装驱动
(1)手动安装:https://developer.android.com/studio/run/oem-usb.html
(2)自动安装: PC上安装360手机助手,USB连接手机,即会启动自动安装
3、待调APP设置webview
WebView.setWebContentsDebuggingEnabled(true);
调试:
1、手机USB连接PC
2、手机上访问浏览器 或 webview
3、在 Remote devices 里 找到手机浏览器或webview访问的地址,点击inspect ,即可打开调试工具进行调试
Weinre
Weinre(WebInspector Remote) web远程检查器
1、安装命令行工具:npm -g install weinre
2、启动Debug Server(Agent):weinre --boundHost 本地IP
3、用浏览器访问服务说明:http://本地IP:8080/
4、植入Debug Target脚本:复制服务说明上”Target Script“ 下的<script>到项目代码里
5、在其他浏览器(例如手机浏览器)访问项目
6、访问Debug Client: 访问服务说明上“Access Points” 下的 “debug client user interface” 链接
7、选择Target:在 Debug Client 上 Remote -> Targets 下,点击一个链接,即选择了一个Target。(有多处同时访问项目就会有多个Target)
8、调试
vconsole
npm install vconsole
require('vconsole');
let vconsole = console;
vconsole.info(3);
electron 整合 vue
2、在同一个目录下搭建 electron 项目
(1)安装electron,npm install --save-dev electron
(2)配置electron主进程入口 和 启动命令
package.json
{
"main": "main.js",
"scripts": {
"start": "electron ."
}
}
(3)main.js
const { app, BrowserWindow, Menu, globalShortcut } = require('electron')
let win;
function createWindow () {
win = new BrowserWindow({
width: 800,
height: 600,
icon: './favicon.ico', // 窗口标题栏图标、系统任务栏图标;Ubuntu下需是完整路径,只能是png/jpg
webPreferences: {
nodeIntegration: true
}
})
win.maximize(); // 窗口最大化
win.loadURL('http://127.0.0.1'); // 打开网络地址
win.loadFile('./dist/index.html') // 打开本地文件
// 自定义菜单
const template = [
{
label:'视图',
submenu:[
{
label:'重新加载',
role:'reload',
accelerator:'Ctrl+R',
},
{
label:'开放者工具',
role: 'toggleDevTools',
accelerator:'Ctrl+D'
}
]
}
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
// 注册快捷方式
globalShortcut.register('CommandOrControl+Shift+t', function () {
win.webContents.openDevTools(); // 打开开放者工具
});
}
# 避免打开多窗口
const singleInstanceLock = app.requestSingleInstanceLock();
if (!singleInstanceLock) {
app.quit()
}
else{
app.on('second-instance', (event) => {
if (win) {
if (win.isMinimized()) win.restore();
win.focus();
}
});
app.whenReady().then(createWindow);
}
(4)使用api
const { app, BrowserWindow } = require('electron').remote; // 获取electron API
console.log(app.getVersion())
const fs = require('fs') // 获取Nodejs API
const root = fs.readdirSync('/')
console.log(root)
console.log(window.location.href); // 获取window API
3、构建运行,npm run build;npm run start
Electron 整合 Vue 注意事项
1、在vue组件中 require('electron') 会导致webpack编译不通过,因为 electron模块 依赖了NodeJS环境的内置模块
解决方案一:
<!-- main.js 中 创建BrowserWindow时指定以下两参数 -->
const win = new BrowserWindow({
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
<!-- 在html中把API放到全局,再在组件中使用 -->
<script>
window.child_process = require('child_process');
window.iconv = require('iconv-lite');
window.shell = require('electron').shell;
</script>
<!-- 在vue组件种使用 API -->
window.child_process.exec("ipconfig", {encoding: 'binary'}, // 调用本地命令
(err, stdout, stderr) => {
if (stderr) {
stderr = window.iconv.decode(new Buffer(stderr, 'binary'), 'cp936'); // 解决中文乱码
console.log(stderr);
} else {
stdout = window.iconv.decode(new Buffer(stdout, 'binary'), 'cp936');
console.log(stdout);
}
}
);
// openExternal 不能打开 createObjectURL 得到的 URL
window.shell.openExternal('http://baidu.com');
解决方案二:发送消息给主进程
/* preload.js */
const { contextBridge, ipcRenderer } = require('electron');
// 给渲染进程暴露的 API
contextBridge.exposeInMainWorld('electron', {
download: (url) => ipcRenderer.send('download', url) // 给主进程发送消息
});
/* main.js */
const { BrowserWindow, ipcMain } = require('electron');
const path = require('path');
const win = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js'), // 预加载
nodeIntegration: true,
contextIsolation: true
}
});
ipcMain.on('download', (event, url) => { // 接收渲染进程消息
const webContents = event.sender;
const win = BrowserWindow.fromWebContents(webContents);
win.webContents.downloadURL(url);
});
/* 渲染进程中 */
window.electron.download(url); // 调用preload.js中暴露的API
Electron 打包发布 之 手动打包
1、在github下载Electron 的 prebuilt binaries,即Electron Release 里的 electron-**.zip,要对应合适的运行平台,例如 electron-v6.1.11-win32-x64.zip、electron-v21.3.3-linux-x64.zip
备注:网速不好要科学上网,选对地域
2、解压 prebuilt binaries
3、把web项目(package.json、main.js、dist)放到 resources\app 目录下,加上 node_modules 中被项目用到的 nodejs 依赖
4、【Windows】用 ResourceHacker 更改 electron.exe 的图标;双击 electron.exe 运行
5、【Linux】开通运行权限 chmod +x electron;./electron 运行
6、【Linux 图形化界面】设置桌面入口
在桌面新建 name.desktop,填入
[Desktop Entry]
Version=1.0
Name=应用名
Comment=
Exec=/**/electron
Icon=图标路径
StartupNotify=true
Terminal=false
Type=Application
Categories=Applications;
添加可执行权限:chmod a+x name.desktop
允许启动:右键 Allow Launching
Electron 打包发布 之 electron-packager
// 安装electron-packager
npm install electron-packager -g
npm install electron --save-dev
npm install electron-packager --save-dev
// 设置electron资源镜像
set ELECTRON_MIRROR=https://npm.taobao.org/mirrors/electron/
// 首次打包
electron-packager . appname -–platform=win32|linux -–arch=x64
// 后续打包
手动将改动的web文件放到 jpf-win32-x64\resources\app 下,新增的依赖放到 jpf-win32-x64\resources\app\node_modules 下
缓存目录:~/.config/项目名
其他技术
微信小程序
微信小程序开发
对比
与微信网页:微信网页 给 微信提供的是 URL,小程序给微信提供的是 源代码;微信给小程序提供了框架、组件、更多Native能力的API;小程序需要审核才能上线;小程序被收藏后有更多入口;
与MVVM:page 接近于 vue 的单文件组件,模板、样式、脚本完全分离,模板采用xml;检查模型变化的方式 this.setData() 接近于 react 的 this.setState()
代码示例
1、获取用户信息(昵称、头像、性别、省市县)
<template>
<button open-type="getUserInfo" @getuserinfo="getUserInfoHandle"
v-show="!userInfoAuth">获取用户信息</button>
</template>
<script>
export default {
data() {
return {
userInfoAuth: false,
userInfo: {}
}
},
onLoad() {
this.getSetting();
},
methods: {
// 查看已有权限
getSetting() {
wx.getSetting({
success: (res) => {
let authSetting = res.authSetting;
if (authSetting['scope.userInfo']) {
this.getUserInfo();
}
}
})
},
// 已授权,直接接调用wx.getUserInfo()得到userInfo
getUserInfo() {
wx.getUserInfo({
success: (res) => {
this.analysisUserInfo(res.userInfo);
}
})
},
// 未授权,使用<button>获得授权,并得到userInfo
getUserInfoHandle(event) {
let userInfo = event.detail.userInfo;
if (userInfo) {
this.analysisUserInfo(userInfo);
}
},
analysisUserInfo(userInfo) {
this.userInfoAuth = true;
this.userInfo = userInfo;
}
}
}
</script>
2、登录(获取openId,session_key、unionid)
export default {
onLoad() {
this.checkSession();
},
methods: {
// 检查会话是否有效,即上次login得到的session_key(由后端从微信服务器获取)是否还有效
checkSession() {
wx.checkSession({
success: ()=>{
this.fetchMe();
},
fail: () => {
this.login();
}
})
},
// 检查会话是否还有效
fetchMe(){
wx.request({
url: 'https://test.com/me',
success:(res)=>{
console.log(res.data)
},
fail:(res)=> {
this.login();
}
})
},
login() {
wx.login({
success:(res)=> {
if (res.code) {
this.loginServer(res.code);
}
}
})
},
loginServer(code){
wx.request({
url: 'https://test.com/login',
data: {
code
},
success(res) {
console.log(res.data) // 用户已授权登录 微信开放平台同账号下的 公众号或移动应用 才会有 unionid
}
})
}
}
}
3、获取手机号
前提:已登录
<template>
<button open-type="getPhoneNumber" @getphonenumber="getPhoneNumber">获取手机号</button>
</template>
<script>
export default {
methods: {
getPhoneNumber(e) {
// 得到加密过的数据
if (e.detail.iv && e.detail.encryptedData) {
this.getPhoneNumberFromServer(e.detail);
}
},
// 发给后端解密,后端拿session_key去微信服务器解密
getPhoneNumberFromServer(detail) {
wx.request({
url: 'https://test.com/phone',
data: {
iv: detail.iv,
encryptedData: detail.encryptedData
},
success(res) {
console.log(res.data)
}
})
}
}
}
</script>
4、获取unionId
前提:微信开放平台上绑定了小程序,已经调用过wx.login(),后端已存有session_key
getEncryptedData() {
uni.getUserInfo({
withCredentials: true,
success: (res) => {
this.decryptUnionId(res); // 将加密数据传给后端解密
}
})
},
decryptUnionId(detail) {
this.request({
url: '/user/encrypted-data',
method: 'PUT',
data: {
signature: detail.signature,
rawData: detail.rawData,
iv: detail.iv,
encryptedData: detail.encryptedData
},
success: (res) => {
res = res.data;
if (res.code === 0) {
this.globalData.$store.state.me.unionId = res.data;
this.globalData.$store.commit("change");
} else {
this.showToast(res.msg || '获取失败');
}
}
})
}
5、登录与获取用户信息的结合
0、用户进入小程序时,有16种情况:有没有getUserInfo权限 2 * 我方服务器有没有记录这个用户 2 * 有没有登录我方服务器 2 * 有没有登录微信服务器
1、虽然登录与获取用户信息可以分离,但是可以设计成 获取用户信息 是登录的前提,从而能采集一些用户信息;具体来说是,用户触发 getUserInfo 按钮 后再 wx.login(),并把用户信息保存到后端
2、用户触发过 getUserInfo 按钮,即可奖获取的信息保存到后端,但是有必要获取用户最新的信息,因此可以设计成每次登录都触发 getUserInfo
3、第一次获取用户信息 只能是 getUserInfo 按钮,之后可以是 wx.getUserInfo,可以设计成 任何情况都是 getUserInfo 按钮,从而不用区分两种情况
4、有getUserInfo权限的情况下,使用getUserInfo 按钮,不会弹出授权框,但能正常回调;因此,在显式登录的场景中,(type为getUserInfo的登录按钮 + wx.login) 不会比 (登录按钮 + wx.getUserInfo + wx.login)给用户带来更多负担;因此,在显式登录的场景中,无需判断是否有getUserInfo权限,统统使用getUserInfo 按钮
5、不在code2Session时获取unionId,因为此时往往没有unionId;可用在登录我方服务器后判断是否已存有unionId,没有的话再调用wx.getUserInfo({withCredentials: true})获取加密数据,发给后端解密出unionId(前提是 微信开放平台上绑定了小程序)
6、登录功能可以设计成一个抽屉,能被各处调用
总结:
方案一:不要自动wx.login(),用户要进行登录后才能有的操作时,弹出登录抽屉,抽屉里放置getUserInfo 按钮;用户触发getUserInfo 按钮后,再调用wx.login(),并将最新的用户信息保存到后端;缺点,用户每次都要显式的登录
方案二:先用wx.getSetting判断有没有getUserInfo权限,有的话,自动wx.getUserInfo并wx.login,并将最新的用户信息保存到后端;还没有getUserInfo权限的话,再按方案一
场景一:在有退出功能的小程序中,不能自动wx.login(),需要显式的登录,此场景采用方案一
6、登录与获取手机号的结合
需求:获取用户手机号即登录
0、获取手机号之前必须先wx.login,且后端存有session_key
1、只有一个按钮获取手机号的话,那么得不到getUserInfo权限,但是可以通过<open-data>显示用户基本信息
方案
1、刚进入小程序就自动wx.login(),并登录我方服务器,如果已有phoneNumber,则返回给前端,如果没有phoneNumber,当作未登录
2、未登录的情况下,用户要进行登录后才能有的操作时,弹出登录抽屉,抽屉里放置getPhoneNumber 按钮,用户触发getPhoneNumber 按钮后,调用敏感数据解密接口,得到phoneNumber
3、用<open-data>显示用户基本信息
7、Cookie
小程序不支持cookie机制,但可以读取响应头的Set-Cookie,有Storage机制。
可以用现成的组件使得小程序支持cookie:https://github.com/charleslo1/weapp-cookie
8、canvas
获取CanvasContext实例的三中方法:
<canvas canvas-id="poster-canvas" />
let ctx = wx.createCanvasContext('poster-canvas')
<canvas type="2d" class="poster-canvas" />
wx.createSelectorQuery().in(this).select('.poster-canvas').context((res)=>{
let ctx = res.context;
}).exec();
let canvas = wx.createOffscreenCanvas();
let ctx = canvas.getContext('webgl'); // 离屏canvas 目前只支持 webgl
9、<web-view>打开第三方网页
问题:需要配置域名白名单,iframe 引用的域名也需要
方案1:在 nginx 中配置代理,将请求转发到第三方;缺点:会受防盗链限制
location / {
proxy_pass https://www.目标.com;
proxy_set_header Referer ''; # 清除Referer,冲破防盗链
proxy_set_header Accept-Encoding ""; # 避免 gzip压缩,使得sub_filter能正常进行
sub_filter_once off;
sub_filter '第三方域名.com' 'www.我方域名.cn'; # 替换,让对第三方资源的请求都改成对我方的请求,我方转发时 改造 Referer 等字段,已规避 第三方的 防盗链机制
}
方案2:小程序关联的公众号网页中嵌入ifram(待试)
10、自定义TabBar
1、参考官方文档的示例代码
2、修改 path 和 icon 路径,注意有没有前面的 /
3、在TabBar组件中切换页面
switchTab(e) {
const data = e.currentTarget.dataset
wx.switchTab({url: data.path})
}
4、由于每个Tab页都有各自的TabBar组件实例,因此在TabBar组件中无法知道当前实例属于哪个Tab页,因此在Tab页中告知TabBar组件实例其所在的index
created(){
this.getTabBar().setData({ // 在uni-app中要写成 this.$mp.page.getTabBar()
selected: 1 // selected初始值可以设置为 -1,避免一开始就有菜单被选中
})
}
11、iPhoneX 底部横条避让
const model = wx.getSystemInfoSync().model;
this.setData({
isIphoneX: /iphone\sx/i.test(model) || /iphone\s1/i.test(model) || (/iphone/i.test(model) && /unknown/i.test(model) && !/8/.test(model))
});
12、注意事项
IOS 不支持 webp 图片
IOS 抖音小程序 不支持引用网络字体文件,需要把字体文件放到项目中
小程序、快应用 框架
运行平台
小程序平台:百度智能小程序、支付宝小程序、微信小程序、QQ小程序、字节跳动小程序
快应用平台:努比亚手机、联想手机、一加手机、小米手机、vivo手机、华为手机、OPPO手机、金立手机、魅族手机、中兴手机
跨平台框架
omix:是腾讯omi项目的子项目,是腾讯webstore项目的进化版,是原生微信小程序项目的状态管理组件、响应式组件
腾讯 WePY(类vue):支持输出 微信小程序
腾讯 kbone(纯vue,模拟浏览器环境):支持输出 微信小程序、H5
美团点评 mpvue(纯vue):支持输出 微信小程序、H5
滴滴 MPX(微信小程序原生语法增强为类vue):支持输出 微信/QQ/支付宝/百度/头条小程序
滴滴 Chameleon(CML,类vue):支持输出 微信/QQ/支付宝/百度/头条小程序、快应用、H5、Weex
DCLOUD uni-app(纯vue):支持输出 微信/QQ/支付宝/百度/头条小程序、快应用、H5、Weex
京东 Taro(纯react):支持输出 微信/QQ/支付宝/百度/头条小程序、快应用、H5、React Native
阿里巴巴 Rax(类React):支持输出 支付宝/微信小程序、H5(PWA)、Weex、Flutter
小程序框架指标
跨端支持度:小程序、H5、Native App、快应用
学习成本:是否独立DSL(Domain Specific Language,领域特定语言)、目前掌握的是vue还是react
组件丰富度:官方(内置)组件库、第三方组件库、是否支持原生(H5、小程序)组件库
坑数:跨端数越多,bug就会越多,性能就会越差,使用各端的原生能力就越难;增强型框架跨平台
热度:社区活跃度、更新频率
微信小程序组件库
有赞-Vant Weapp、微信-weui-miniprogram、TalkingData - iView Weapp、蘑菇街-MinUI、Wux Weapp(个人项目,组件最多)
京东-TaroUI(基于Taro框架)、ColorUI(WXSS框架)、腾讯-WeUI-wxss(WXSS框架)
转译型 与 增强型
转译型框架(MPX以外):将其他的语法规范转译为小程序语法规范
转译型框架的缺点:不支持源框架的一些语法特性,不支持原生组件库(H5、小程序)
增强型框架(MPX):基于某一小程序语法规范,使用Vue中优秀的语法特性进行增强;但在跨平台编译时,仍然是转译
增强型框架的优点:可以从原生小程序项目渐进迁移、一定程度上支持原生小程序组件库
增强型框架的缺点:由于是基于某一小程序的语法规范,跨平台编译时,更难抹平平台差异;一旦抹平了,也就有了转译型框架的缺点
总结:不跨平台编译时,用MPX作为语法增强,是可行的。需要跨平台编译时,会出现很多不支持转译的语法特性
MPX跨平台编译
思路:新平台不支持的内容,要么抹平 要么 再编译前进行差异化
抹平案例:微信小程序代码 编译成 头条小程序
问题:vant-weapp组件库用到了<wxs>,不能跨平台编译成 头条小程序
解决思路:去掉vant-weapp里的<wxs>
步骤:
1、把 /wxs 目录下的 wxs文件 改为 .js,在wxss中引入
2、把这些文件里的wxs语法改为js语法,例如 模块导入导出语法、getRegExp()改为new RegExp()、判断是否是数组array.constructor === 'Array' 改为 Array.isArray(array)
3、template里调用方法 {{f(x)}} 改用 computed 方法实现
4、template里wx:for 内 调用方法 {{f(x)}} 得想办法展开成表达式 或者 数据预处理
kbone
不能用现成的第三方vue组件库;使用了小程序原生组件,则web端用不了;使用kbone-ui,才能两端通用
启动时打开指定路径:每个页面要设置成单独的page,而不能是一个单页多个路由;打开新页要用window.open(route); 否则会出现页面空白、没有返回按钮、没有Home按钮等问题
从外部(公众号菜单、分享到对话框的卡片、分享到朋友圈的卡片、服务消息、小程序码等)打开指定路径要用 pages/xx/index?type=share&targeturl=${encodeURIComponent(location.href)},这里坑比较多,比如生成小程序码时不能带参
uniapp
1、微信小程序不支持指定tabBar高度,开发者工具模拟器某些机型下,tabBar文字会贴边,不代表真机会贴边
2、全局变量:Vue.prototype里的常量只能在JS中用,不能在template里用;写在App.globalData里的常量,可以通过computed用在template里;写在Vuex.Store里的变量,可以通过computed用在template里