小程序,大世界
1.简介
1.1 小程序解决了什么问题
- 技术方向
- H5(白屏时间长,调用系统能力弱)
- 提高启动速度
- 本质是数据和展示更彻底的分离
- 符合数据驱动的前端优势
- 实现最大限度地宿主能力外放
- APP(体验好,开发成本高)
- 开发成本降低
- 技术统一
- 多平台展现逻辑一致
- 跨平台解决方案
- 非技术方向
- 审核保证应用合法性(审核后才放心下发更多权限)
- 平台优势
- 推广入口
- 拉活手段
- 配套工具
- 微信开发者后台的分析报告
1.2 相关的技术栈
HTML/CSS/JavaScript
- NodeJS
- 移动端适配
- HTTP协议/HTTPS
- OAuth2
- GIT
1.3 主流的小程序平台
微信小程序、支付宝小程序、百度小程序、头条小程序、京东小程序、快应用、钉钉小程序、360小程序(基于PC的小程序)
1.4类似小程序的技术
- Cordova: 通过webview渲染,通过插件调用系统服务
- PWA:Service Worker和PushAPI
- React Native/Weex:JavaScript通过JavaScriptCore等执行,并通过Bridges和Native组件交互
- Flutter:Dart直接与独立系统的UI库进行交互
2.小程序技术架构
2.1文件结构及其含义
.json后缀的JSON配置文件
app.json
- 职责:小程序文件的配置
- pages
- window
project.config.json
-职责:微信开发者工具配置
每个页面下的json - 职责:个性化page,可覆盖app.json中window的配置
sitemao.json - 职责:指导搜索引擎进行检索
.wxml后缀的WXML模板文件
- 本质是HTML模板
- 有特定的标签
- 接管一些简单的逻辑判断
- JS不直接操作DOM,只复制set数据
.wxss后缀的WXSS样式文件
- 提供rpx单位(屏幕宽度与750的比值)
- 精简的CSS
- 提供全局和局部的CSS
.js后缀的JS脚本逻辑文件
- 负责逻辑交互
- APP、Page、Component三个构造函数
- 可调用系统API
2.2 双线程模型
渲染层和逻辑层是两个线程
渲染层不能操作 DOM,降低风险,但同时也造成不便
双线程导致线程通信实现数据传递,会有一定延迟
2.3生命周期
小游戏实时性要求较高,没有采用双线程架构
2.4组件
- 官方组件
- 容器
- 基础内容
- 表单组件
- 导航
- 地图
- 画布
- 原生组件
- 自定义组件
使用Component构造器,通过usingComponents引入
2.5其他
插件机制
如果想要开发插件,就将开发模式选为插件即可
云端函数
小游戏
3.开发发布流程
- 开发者在小程序平台注册小程序,以获得APPID(需要用一个没有注册过小程序的邮箱进行注册)
- 初始化代码并完成代码仓库配置
- 开发代码并调试
- 上传并发布
4.小程序的发展
多端同构框架
- 意义:一次编写适配多端,一次迭代各端同步
-
利用Web的优点,以及对各个平台进行动态适配
常用框架:uni-app、Taro、KBone
自动化
- 控制小程序跳转到指定页面
- 获取小程序页面数据
- 获取小程序页面元素状态
- 触发小程序元素绑定事件
- 往AppService注入代码片段
- 调用wx对象上任意接口
硬件框架
云IDE
W3C小程序工作组
- W3C小程序和快应用草案
- URL Schema
- Widget
- Manifest
- Lifestyle
- Packaging
Web前端点播直播入门
什么是视频
了解媒体数据存储和应用的基础原理
格式与内容
1.文件扩展名≈媒体封装格式(媒体容器类型)
2.媒体封装格式≠音视频编码格式(使用了谁家的编码器)
3.文件内容:
- 头信息(格式、时长、帧率、码率、分辨率...)
- 索引信息
- 视频数据
- 音频数据
- 附加增强数据(存储一些音视频之外的信息,比如自定义的一些数据)
视频数据
1.显示器颜色呈现基于RGB(红绿蓝)颜色空间模型
2.视频领域大多基于YUV颜色空间做抽样存储
3.帧内预测&帧间预测复用进一步有效的压缩数据
4.P帧(前向预测帧)、B帧(双向预测帧)、I帧(参考帧)
5.基于通用标准集N多技术于一身 --- 视频编码器
H.264(AVC)、H.265(HEVC)、VP8、VP9...
音频数据
1.声音:不同振幅&频率而产生的机械波;数字形式是一维波形
2.对自然中连续的声波采样,做数字化PCM存储
3.扬声器还原PCM(脉冲编码调制)数字信号为模拟音频信号
4.音频压缩基本算法:预测、变换
5.基于通用标准集N多技术于一身 --- 音频编码器
AAC、MP3...
传输协议
- 传统场景
1.流媒体(直播)
- HLS:苹果为利
用现有CDN设施而发明的"流媒体"协议(实际上是基于文件存储的)- HTTP(S)-FLV:基于HTTP的流媒体协议
- RTMP、RTP/RTSP、TS、MMS...
2.点播传输 - HTP(S):通过Range方式或参数方式完成Seek
2.Web端 - HTTP(S)、WS(S)、P2P...
播放器原理
1.解协议(加载数据)
2.解封装(解复用)
3.解码
4.渲染
好玩的Web端API
了解通过Web端接口可以实现哪些方向的具体应用
媒体兼容判断
let videoEl = document.createElement("video");
let types = {
'mp4': 'audio/mp4',
'MP4': 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"',
'webm': 'video/webm; codecs="vp8, vorbis"',
'ogg': 'video/ogg; codecs="theora, vorbis"',
'm3u8': 'application/vnd.apple.mpegURL',
'ts': 'video/mp2t; codecs="avc1.42E01E,mp4a.40.2"'
};
Object.keys(types).forEach(key => {
const type = types[key];
const ret = videoEl.canPlayType(type) || '不支持';
console.log(key + ': ' + ret);
});
交互式视频
基于Video时间轴实现交互式视频
let video = $('video');
video.ontimeupdate = ()=>{
let {currentTime} = video;
show(currentTime > 64 ? '.s2' : '.s1');
hide(currentTime > 64 ? '.s1' : '.s2');
if(
(currentTime > 64 && currentTime < 65) ||
(currentTime > 113 && currentTime < 114)
){
video.pause();
}
};
let ppBtn = $('paly_pause');
video.onplay = ()=>{
ppBtn.innerText = '暂停';
};
video.onpause = ()=>{
ppBtn.innerText = '播放';
};
ppBtn.onclick = ()=>{
video[video.paused ? 'play' : 'pause' ]();
};
$('start').onclick = ()=>{
video.currentTime = 1;
video.play();
};
$('step').onclick = ()=>{
video.currentTime = 60;
video.play();
};
$('dream').onclick = ()=>{
video.currentTime = 83;
video.play();
};
$('drink').onclick = ()=>{
video.currentTime = 116;
video.play();
};
hide('.s2');
function show(sel){
document.querySelectorAll(sel).forEach(el=>{
el.style.display='inline'
});
}
function hide(sel){
document.querySelectorAll(sel).forEach(el=>{
el.style.display='none'
});
}
function $(id){
return document.getElementById(id);
}
播放本地视频文件
基于 FileReader API 播放本地文件
let iptFileEl = document.querySelector('input[type="file"]');
let videoEl = document.querySelector('video');
iptFileEl.onchange = e =>{
let file = iptFileEl.files && iptFileEl.files[0];
playFile(file);
};
function playFile(file){
if(file){
let fileReader = new FileReader();
fileReader.onload = evt => {
if(FileReader.DONE == fileReader.readyState){
videoEl.src = fileReader.result;
}else{
console.log('FileReader Error:', evt);
}
}
fileReader.readAsDataURL(file);
}else{
videoEl.src = '';
}
}
播放硬件资源(调用摄像头或麦克风)
基于 getUserMedia 调用摄像头或麦克风
iframe 引入页要调用媒体设备的,需要父页面中给iframe设置 allow="microphone;camera;midi;encrypted-media;" 。
const getUserMediaPromise = options => new Promise((resolve, reject) => {
const nvgt = window.navigator;
if(nvgt) {
if(nvgt.mediaDevices && nvgt.mediaDevices.getUserMedia) {
return nvgt.mediaDevices.getUserMedia(options).then(resolve, reject);
}
const getUserMedia = nvgt.getUserMedia || nvgt.webkitGetUserMedia || nvgt.mozGetUserMedia;
if(getUserMedia) {
return getUserMedia(options, resolve, reject)
}
}
reject('当前环境不支持获取媒体设备。');
});
let streamTrack;
const video = document.querySelector('video');
document.querySelector('#play').onclick = () => {
getUserMediaPromise({
audio: false,
video: true
}).then(stream => {
video.srcObject = stream;
streamTrack = stream.getTracks()[0];
},
err => {
console.log('getUserMedia error: [' + err.name + '] ' + err.message)
});
};
document.querySelector('#stop').onclick = () => {
streamTrack && streamTrack.stop();
};
const box = document.querySelector('div');
document.querySelector('#sketch').onclick = () => {
box.className = box.className ==='' ? 'sketch' : '';
};
人脸识别
实现视频录制
基于 getUserMedia、MediaRecorder 实现录像
const getUserMediaPromise = options => new Promise((resolve, reject) => {
const nvgt = window.navigator;
if(nvgt) {
if(nvgt.mediaDevices && nvgt.mediaDevices.getUserMedia) {
return nvgt.mediaDevices.getUserMedia(options).then(resolve, reject);
}
const getUserMedia = nvgt.getUserMedia || nvgt.webkitGetUserMedia || nvgt.mozGetUserMedia;
if(getUserMedia) {
return getUserMedia(options, resolve, reject)
}
}
reject('当前环境不支持获取媒体设备。');
});
const video = document.querySelector('#preview');
let cameraStream;
const opencameraBtn = document.querySelector('#opencamera');
const closecameraBtn = document.querySelector('#closecamera');
const recordBtn = document.querySelector('#record');
const stopRecordBtn = document.querySelector('#stoprecord');
const playBtn = document.querySelector('#play');
const downloadBtn = document.querySelector('#download');
opencameraBtn.onclick = () => getUserMediaPromise({
audio: false,
video: true
}).then(
stream => {
cameraStream = video.srcObject = stream;
opencameraBtn.disabled = true;
closecameraBtn.disabled = false;
recordBtn.disabled = false;
},
err => {
console.log('getUserMedia error: [' + err.name + '] ' + err.message)
}
);
closecameraBtn.onclick = () => {
cameraStream && cameraStream.getTracks()[0].stop();
cameraStream = null;
opencameraBtn.disabled = false;
closecameraBtn.disabled = true;
stopRecordBtn.onclick();
};
let mediaRecorder;
let recordedBlobs;
const mimeType = ['video/webm;codecs=vp9', 'video/webm;codecs=vp8', 'video/webm', ''].find(type => {
return MediaRecorder.isTypeSupported(type);
});
// console.log('mimeType', mimeType);
recordBtn.onclick = () => {
recordedBlobs = [];
try {
mediaRecorder = new MediaRecorder(cameraStream, { mimeType });
} catch(e) {
alert('Exception while creating MediaRecorder: ' + e + '. mimeType: ' + mimeType);
return;
}
recordBtn.disabled = true;
stopRecordBtn.disabled = false;
playBtn.disabled = true;
downloadBtn.disabled = true;
mediaRecorder.onstop = evt => {
console.log('Recorder stopped');
};
mediaRecorder.ondataavailable = function(event) {
if (event.data && event.data.size > 0) {
recordedBlobs.push(event.data);
}
};
mediaRecorder.start(20); // 单次收集数据毫秒时长,ondataavailable 触发频率时长间隔
};
const recordedVideo = document.querySelector('#recorded');
stopRecordBtn.onclick = () => {
mediaRecorder && mediaRecorder.stop();
mediaRecorder = null;
// console.log('Recorded Blobs: ', recordedBlobs);
recordedVideo.controls = true;
playBtn.disabled = false;
downloadBtn.disabled = false;
stopRecordBtn.disabled = true;
if(!cameraStream) {
recordBtn.disabled = true;
}
};
const getRecordedBlobUrl = () => {
const superBuffer = new Blob(recordedBlobs, {type: mimeType.split(';')[0]});
return window.URL.createObjectURL(superBuffer);
};
playBtn.onclick = () => {
recordedVideo.src = getRecordedBlobUrl();
}
downloadBtn.onclick = () => {
var a = document.createElement('a');
a.style.display = 'none';
a.href = getRecordedBlobUrl();
a.download = 'test.webm';
document.body.appendChild(a);
a.click();
setTimeout(function() {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 100);
}
播放JS拉取的媒体数据
基于MediaSource播放JS拉取的媒体数据
const video = document.querySelector('video');
const fetchMp4 = (url, cb) => {
const xhr = new XMLHttpRequest();
xhr.open('get', url);
xhr.responseType = 'arraybuffer';
xhr.onload = function () {
cb(xhr.response);
};
xhr.send();
};
const assetURL = 'https://nickdesaulniers.github.io/netfix/demo/frag_bunny.mp4';
const mimeCodec = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"';
// 创建动态媒体源,并关联到video元素上
const mediaSource = new MediaSource();
video.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', () => {
const sourceBuffer = mediaSource.addSourceBuffer(mimeCodec);
// 拉取数据
fetchMp4(assetURL, buf => {
sourceBuffer.addEventListener('updateend', () => {
// 媒体流传输完毕
mediaSource.endOfStream();
// video.play();
});
// 将数据喂给 Video -- 注意这里只是一次性输入整个MP4数据
sourceBuffer.appendBuffer(buf);
});
});
Web端点播直播&播放方案
点播直播的区别
1.应用流程
- 点播:创作者 => 上传 => 转码 => 存储 <=> CDN分发 <=> 观众
- 直播:创作者 => 推流 <=> 存储 <=> 转码 <=> CDN分发 <=> 观众
2.媒体类型的选择
- HTTP(S)-MP4..
点播服务 - HTTP(S)-FLV
点播、直播 - HTTP(S)-HLS
点播、直播(高延迟)
播放器解决方案
- 原生浏览器支持的
- 直接走原生Video播放
- 原生浏览器不支持的
- 协议或容器类型不支持
- JS解协议下载数据、解容器、重新封装,然后通过MSE喂给Video解码、渲染播放
例如Web端播放FLV、HLS:http://chimee.org
- JS解协议下载数据、解容器、重新封装,然后通过MSE喂给Video解码、渲染播放
- 解码器不支持
- JS下载数据,WASM 解容器、解码,通过 WebGL&WebAudio 渲染播放
例如Web端播放HEVC编码视频:https://zyun.360.cn/developer/doc?did=QHWWPlayer
- JS下载数据,WASM 解容器、解码,通过 WebGL&WebAudio 渲染播放
- 有解密需求的
- 参考前两条,在解容器之后对每帧数据启用解密逻辑。
- 协议或容器类型不支持
可扩展学习的参考资料
基础API:
https://developer.mozilla.org/zh-CN/docs/Web/Guide/HTML/Using_HTML5_audio_and_video
数据获取:
https://developer.mozilla.org/zh-CN/docs/Web/API/MediaDevices/getUserMedia
https://developer.mozilla.org/zh-CN/docs/Web/API/FileReader
https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest
https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API
虚拟文件:
https://developer.mozilla.org/zh-CN/docs/Web/API/Blob
https://developer.mozilla.org/zh-CN/docs/Web/API/URL/createObjectURL
动态媒体源:
https://developer.mozilla.org/zh-CN/docs/Web/API/Media_Source_Extensions_API
数据操作:
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/TypedArray
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly
https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API
画音渲染:
https://developer.mozilla.org/zh-CN/docs/Web/API/WebGL_API
https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Audio_API
场景应用:
https://developer.mozilla.org/zh-CN/docs/Web/API/MediaRecorder
https://developer.mozilla.org/zh-CN/docs/Web/API/WebRTC_API
开源项目:
http://chimee.org
https://github.com/bilibili/flv.js
https://github.com/video-dev/hls.js
https://github.com/huzunjie/WasmVideoPlayer
非开源 WasmVideoPlayer 示例:
http://lab.pyzy.net/qhww
前端代码的自我修养
如何衡量代码质量的好坏
衡量代码质量的唯一有效标准:WTF/min —— Robert C. Martin
代码的自我修养
代码规范
- no-fallthrough(质量问题)
- max-depth(风格问题)
ESlint
yarn global add eslint//安装eslint
{
"extends": "eslint:recommended",
"rules": {
// enable additional rules
"indent": ["error", 4],
"linebreak-style": ["error", "unix"],
"quotes": ["error", "double"],
"semi": ["error", "always"],
// override default options for rules from base configurations
"comma-dangle": ["error", "always"],
"no-cond-assign": ["error", "always"],
// disable rules from base configurations
"no-console": "off",
}
ESlint还可以配合前端框架工作
.eslintrc.js
module.exports = {
"env": {
"browser": true,
"es6": true
},
"extends": [
"standard"
],
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"rules": {
"no-console" : ["error"]
}
};
husky + lint-staged
husky 原理:修改 .git/hooks/pre-commit
"scripts": {
"precommit": "lint-staged"
},
"lint-staged":{
"src/**/*.js": [
"eslint --fix --ext .js",
"prettier --write",
"git add"
]
}
完成配置后,再提交代码就会自动检查代码问题
格式
eslint会自动检查代码格式是否有错误
工具:prettier
流程化
如何优雅地提交代码
git commit message规范
合并提交
有的时候,我们会遇到多次Commit才能完成一个feature
这时git log就会出现多次记录,如下图所示,污染提交历史
使用以下命令进行合并提交
git rebase -i $GIT_LOG_VERSION$
具体步骤如下
git commit --fixup HEAD
git rebase -i $GIT_LOG_VERSION$ --autosquash
git push -f origin master
rebase 有一定危险,多人协作慎用
如果改变了代码的时间线,push 时需要带上 -f
技术翻译,进阶地直梯
翻译的类型
- 文学翻译
- 非文学翻译
文学翻译| 非文学翻译
-|-
艺术成分多一些 |科学成分多一些
需要更多的灵感 |需要更多的勤奋
责任小一些 |责任大一些
技术翻译的意义
- 翻译技术文章,学习新技术思想
- 翻译技术文档,掌握标准和工具
- 翻译技术图书,获得名声和报酬
技术翻译的标准
准确、地道、简洁
技术翻译的方法
- 消化吸收原文
- 母语地道表达
- 就是翻译意思