11 小程序
小程序:小程序是借助 Web 的开发方式和技术栈,通过宿主 APP 加快渲染,并将 APP 能力有效对外释放的一系列方案的总称
- 简介
- 技术架构
- 开发发布流程
- 小程序发展
简介
小程序解决的问题
- 技术方向
- H5(白屏时间长,调用系统能力弱)
- 提高启动速度
- 本质是数据和展示更彻底的分离
- 符合数据驱动的前端优势
- 实现最大限度地宿主能力外放
- 提高启动速度
- APP(体验好,开发成本高)
- 开发成本降低
- 技术统一
- 多平台展现逻辑一致
- 跨平台解决方案
- H5(白屏时间长,调用系统能力弱)
- 非技术方向
- 审核保证应用合法性(审核后才放心下发更多权限)
- 平台优势
- 推广入口
- 拉活手段
- 配套工具
- 微信开发者后台的分析报告
小程序技术栈
- HTML/CSS/JavaScript
- NodeJS
- 移动端适配
- HTTP/HTTPS
- OAuth2
- Git
类似小程序的技术
- Cordova: 通过 webview 渲染,通过插件调用系统服务
- PWA: Service Worker 和 Push API
- React Native/Weex: JavaScript 通过 JavaScriptCore 等执行,并通过 Bridges 和 Native 组件交互
- Flutter: Dart 直接与独立系统的 UI 库交互
小程序技术架构
双线程模式
渲染层:Webview
逻辑层:JsCore
渲染层不能操作 DOM,降低风险,但同时也造成不便
双线程导致线程通信实现数据传递,会有一定延迟
小游戏实时性要求较高,没有采用双线程架构
自动化测试
- Selenium
- Puppeteer
12 点播直播
- 什么是视频
- Web 端 API
- Web 端点播直播&播放方案
什么是视频
- 格式与内容
- 视频数据
- 音频数据
- 传输协议
- 播放器原理
内容与格式
- 文件扩展名≈媒体封装格式(媒体容器类型)
- 媒体封装格式≠音视频编码格式(使用了谁家的编码器)
- 文件内容:
- 头信息(格式、时长、帧率、码率、分辨率…)
- 索引信息
- 视频数据
- 音频数据
- 附加增强数据…
视频数据
-
显示器颜色呈现基于RGB(红绿蓝)颜色空间模型
-
视频领域大多基于YUV颜色空间做抽样存储
-
帧内预测&帧间预测复用进一步有效的压缩数据
-
P帧(前向预测帧)、B帧(双向预测帧)、I帧(参考帧)
-
基于通用标准集N多技术于一身 — 视频编码器
H.264(AVC)、H.265(HEVC)、VP8、VP9…
音频数据
- 声音:不同振幅&频率而产生的机械波;数字形式是一维波形
- 对自然中连续的声波采样,做数字化PCM存储
- 扬声器还原PCM(脉冲编码调制)数字信号为模拟音频信号
- 音频压缩基本算法:预测、变换
- 基于通用标准集N多技术于一身 — 音频编码器
AAC、MP3…
传输协议
- 传统场景
- 流媒体(直播)
- HLS:苹果为利用现有CDN设施而发明的"流媒体"协议
- HTTP(S)-FLV:基于HTTP的流媒体协议
- RTMP、RTP/RTSP、TS、MMS…
- 点播传输
- HTTP(S):通过Range方式或参数方式完成Seek
- 流媒体(直播)
- Web端
- HTTP(S)、WS(S)、P2P…
播放器通用原理
- 解协议(加载数据)
- 解封装(解复用)
- 解码
- 渲染
Web 端 API
- 媒体兼容判断
- 交互式视频
- 播放本地视频文件
- 播放硬件资源
- 实现视频录制
- 播放 JS 拉取的媒体数据
兼容判断
video
标签的 canPlayType
方法
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);
});
// "mp4: maybe"
// "MP4: probably"
// "webm: probably"
// "ogg: probably"
// "m3u8: 不支持"
// "ts: 不支持"
进度控制实现交互视频
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);
}
本地加载文件播放
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 调用摄像头
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' : '';
};
人脸识别
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 localVideo = document.querySelector('video');
const localCanvas = document.querySelector('canvas');
function poll() {
const w = localVideo.videoWidth;
const h = localVideo.videoHeight;
const canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
ctx.drawImage(localVideo, 0, 0, w, h);
// ccv.js 来自 https://github.com/liuliu/ccv/tree/unstable/js
const comp = ccv.detect_objects({
canvas: ccv.grayscale(canvas),
cascade: cascade,
interval: 5,
min_neighbors: 1
});
/* draw detected area */
localCanvas.width = localVideo.clientWidth;
localCanvas.height = localVideo.clientHeight;
const ctx2 = localCanvas.getContext('2d');
ctx2.lineWidth = 2;
ctx2.lineJoin = 'round';
ctx2.clearRect (0, 0, localCanvas.width,localCanvas.height);
let x_offset = 0, y_offset = 0, x_scale = 1, y_scale = 1;
if (localVideo.clientWidth * localVideo.videoHeight > localVideo.videoWidth * localVideo.clientHeight) {
x_offset = (localVideo.clientWidth - localVideo.clientHeight * localVideo.videoWidth / localVideo.videoHeight) / 2;
} else {
y_offset = (localVideo.clientHeight - localVideo.clientWidth * localVideo.videoHeight / localVideo.videoWidth) / 2;
}
x_scale = (localVideo.clientWidth - x_offset * 2) / localVideo.videoWidth;
y_scale = (localVideo.clientHeight - y_offset * 2) / localVideo.videoHeight;
for (let i = 0; i < comp.length; i++) {
comp[i].x = comp[i].x * x_scale + x_offset;
comp[i].y = comp[i].y * y_scale + y_offset;
comp[i].width = comp[i].width * x_scale;
comp[i].height = comp[i].height * y_scale;
let opacity = 0.1;
if (comp[i].confidence > 0) {
opacity += comp[i].confidence / 10;
if (opacity > 1.0) opacity = 1.0;
}
//ctx2.strokeStyle = 'rgba(255,0,0,' + opacity * 255 + ')';
ctx2.lineWidth = opacity * 10;
ctx2.strokeStyle = 'rgb(255,0,0)';
ctx2.strokeRect(comp[i].x, comp[i].y, comp[i].width, comp[i].height);
}
setTimeout(poll, 500);
}
document.querySelector('button').onclick = () => getUserMediaPromise({ video:true }).then(
stream => {
localVideo.srcObject = stream;
localVideo.onplay = poll;
},
error => alert('Failed to get access to local media. Error code was ' + error.code + '.')
);
实时录像
基于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 端点播直播&播放方案
点播与直播区别
- 应用流程
- 点播:创作者 => 上传 => 转码 => 存储 <=> CDN分发 <=> 观众
- 直播:创作者 => 推流 <=> 存储 <=> 转码 <=> CDN分发 <=> 观众
- 媒体类型的选择
- 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 渲染播放
- 有解密需求的
- 参考前两条,在解容器之后对每帧数据启用解密逻辑。
- 协议或容器类型不支持
学习资料
-
[数据获取](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/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)
13 代码自我修养
WTF/min——Robert C.Martin
好代码每分钟的抱怨数少
好代码的要求
-
代码规范
-
代码格式
-
流程化
代码规范
ESlint
- no-fallthrough(质量问题)
- max-depth(风格问题)
yarn global add 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 和 lint-staged 结合使用,配合 ESlint
husky 原理:修改 .git/hooks/pre-commit
"scripts": {
"precommit": "lint-staged"
},
"lint-staged":{
"src/**/*.js": [
"eslint --fix --ext .js",
"prettier --write",
"git add"
]
}
配置好后,在提交后就会自动检查代码问题
代码格式
工具:prettier
流程化
Gitlab Flow
如何优雅提交代码
-
git commit message 规范
-
合并提交
comments 规范
yarn global add commitizen cz-conventional-changelog standard-version
commitizen init cz-conventional-changelog --yarn --dev --exact
安装工具后,在 commit 时会出现交互提示,根据规范和引导设置 comment
合并提交
希望一个 Feature 只有一个 Commit(但实际上可能一个 Feature 要很多天完成),多个 Commit 会污染 Git logs
git rebase -i $GIT_LOG_VERSION$
rebase 有一定危险,多人协作慎用
如果改变了代码的时间线,push 时需要带上 -f
配置总结
.eslintrc.js
module.exports = {
env: {
browser: true,
es6: true,
},
extends: ['standard', 'plugin:prettier/recommended'],
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
},
rules: {
'no-console': ['error'],
},
}
.prettierrc
{
"printWidth": 150,
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"insertPragma": false,
"eslintIntegration": true,
"jsxBracketSameLine": true
}
package.json
{
"name": "75start-project",
"version": "1.2.0",
"description": "code maintain",
"main": "index.js",
"scripts": {
"commit": "npx git-cz",
"release": "standard-version",
"precommit": "lint-staged"
},
"lint-staged": {
"src/**/*.js": [
"eslint --fix --ext .js",
"prettier --write",
"git add"
]
},
"author": "sunlei",
"license": "MIT",
"devDependencies": {
"cz-conventional-changelog": "3.1.0",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.0",
"eslint-config-standard": "^14.1.1",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-node": "^11.0.0",
"eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"husky": "^4.2.3",
"lint-staged": "^10.0.8",
"prettier": "^2.0.1",
"pretty-quick": "^2.0.1"
},
"husky": {
"hooks": {
"pre-commit": "pretty-quick --staged"
}
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}
}
14 技术翻译:进阶的直梯
翻译的意义:阅读外文文档有助于提升
翻译类型
- 文学翻译
- 非文学翻译
文学翻译 | 非文学翻译 |
---|---|
艺术成分多一些 | 科学成分多一些 |
需要更多灵感 | 需要更多勤奋 |
责任小一些 | 责任大一些 |
技术翻译的意义
- 翻译技术文章,学习新技术思想
- 翻译技术文档,掌握保准和工具
- 翻译技术图书,获取名声和报酬
技术翻译的标准
准确、地道、简洁
文学翻译:信、达、雅
技术翻译的方法
- 消化吸收原文
- 母语地道表达
- 翻译原文意思
为什么不能直译
You can never tell: 你永远不知道
Don’t give that: 少来这套
Talk to your hand: 懒得理你
Ask for the moon: 异想天开
My hands are tired: 我无能为力
I’ll take a rain check: 能不能改天
You have my word: 我保证
技术翻译要坚持技术驱动