async await写一个训练过程1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39async function practise() {
console.log('训练开始...')
let result;
result = await warmup(2)
if(result) console.log('热身完成。。')
result = await squat(5)
if(result) console.log('深蹲完成。。')
result = await boating(3)
if(result) console.log('划船完成。。')
}
function warmup(time){
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, time*1000);
});
}
function squat(time){
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, time*1000);
});
}
function boating(time){
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, time*1000);
});
}
// 执行这个‘训练’
practise().then(res=>{
console.log('完了')
})
从输出结果上看,特点是按照顺序逐步完成某动作,似乎本身就是一个generator
于是把每个动作装到一个generator里1
2
3
4
5function* practisePlan() {
yield warmup(2)
yield squat(5)
yield boating(3)
}
yield返回的是一个promise,promise ‘resolve’的情况下才会 ‘next’1
2
3
4
5
6
7
8
9
10
11
12
13
14
15let practise = function () {
return new Promise((resolve, reject)=>{
const plan = practisePlan()
Promise.resolve(plan.next().value).then(result=>{
if(result) console.log('热身完成。。')
return plan.next().value;
}).then(result=>{
if(result) console.log('深蹲完成。。')
return plan.next().value;
}).then(result=>{
if(result) console.log('划船完成。。')
resolve() // <--完了
})
})
}
再看循环中的async await的栗子 —-> S-为什么说 async/await是generator的语法糖?
node.js stream
Node.js Streams: Everything you need to know(译文)
在 Node.js 中有四种基本的流类型:Readable(可读流),Writable(可写流),Duplex(双向流),Transform(转换流)。
- 可读流是数据可以被消费的源的抽象。一个例子就是 fs.createReadStream 方法。
- 可写流是数据可以被写入目标的抽象。一个例子就是 fs.createWriteStream 方法。
- 双向流即是可读的也是可写的。一个例子是 TCP socket。
- 转换流是基于双向流的,可以在读或者写的时候被用来更改或者转换数据。一个例子是 zlib.createGzip 使用 gzip 算法压缩数据。你可以将转换流想象成一个函数,它的输入是可写流,输出是可读流。你或许也听过将转换流成为“通过流(through streams)”。
所有的流都是 EventEmitter 的实例。触发它们的事件可以读或者写入数据,然而,我们可以使用 pipe 方法消费流的数据。
从流到流
Azure-BlobStorage
Blob Storage存储资源的三个层次
- storage account 存储实例的顶级层次
- containers 相当于目录
- blob 文件实体
Azure 门户点击Storage Account,查看当前tenant的Storage Account,进入其中某个account, 关于存储,提供了一个Explorer工具
左侧工具 Blob Services - Containers 创建容器csd-commom,(意外地发现之前做App Services备份地Deploy Packages在这里)
进入容器,可以直接使用页面提供的上传入口,上传可以填写一个folder,文件上传时自动使用该folder作为子目录
官方教程:使用 Azure 存储在云中上传图像数据
$blobStorageAccount(存储账户)
$blobStorageAccount(容器)
myAppServicePlan(应用服务计划)
myResourceGroup(资源组)
$webapp(App Service)dotNet Blob Uploader
YourStorageAccount — Settings — Access keys 查看账户密钥以及连接字符串
dotNet Blob Client Package: Azure.Storage1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45using Azure.Storage;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
...
public Task<bool> UploadFileToStorage(Stream fileStream){
// Create a URI to the blob
Uri blobUri = new Uri("https://" +
_storageConfig.AccountName +
".blob.core.windows.net/" +
_storageConfig.ImageContainer +
"/" + fileName);
// connection credential
StorageSharedKeyCredential storageCredentials =
new StorageSharedKeyCredential(_storageConfig.AccountName, _storageConfig.AccountKey);
// Create the blob client.
BlobClient blobClient = new BlobClient(blobUri, storageCredentials);
// Upload the file
await blobClient.UploadAsync(fileStream);
return await Task.FromResult(true);
}
public static async Task<List<string>> GetThumbNailUrls(AzureStorageConfig _storageConfig)
{
List<string> thumbnailUrls = new List<string>();
// Create BlobServiceClient from the account URI
BlobContainerClient container = new BlobContainerClient(connectionString, _storageConfig.ThumbnailContainer);
// Get reference to the container
BlobContainerClient container = blobServiceClient.GetBlobContainerClient(_storageConfig.ThumbnailContainer);
if (container.Exists())
{
foreach (BlobItem blobItem in container.GetBlobs())
{
thumbnailUrls.Add(container.Uri + "/" + blobItem.Name);
}
}
return await Task.FromResult(thumbnailUrls);
}
Blob SDK for Node.js: @azure/storage-blob1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44const express = require('express')
const bodyParser = require('body-parser')
const multer = require('multer')
const _fs = require('fs')
const { StorageSharedKeyCredential,
BlobServiceClient } = require('@azure/storage-blob')
const {AbortController} = require('@azure/abort-controller')
const app = express();
// init blob client
const STORAGE_ACCOUNT_NAME = 'YourResourceGroupName'
const CONTAINER_NAME = 'BlobStorageContainerName'
const ACCOUNT_ACCESS_KEY ='****************************'
const ONE_MEGABYTE = 1024 * 1024;
const FOUR_MEGABYTES = 4 * ONE_MEGABYTE;
const ONE_MINUTE = 60 * 1000;
const aborter = AbortController.timeout(30 * ONE_MINUTE);
const credentials = new StorageSharedKeyCredential(STORAGE_ACCOUNT_NAME, ACCOUNT_ACCESS_KEY);
const blobServiceClient = new BlobServiceClient(`https://${STORAGE_ACCOUNT_NAME}.blob.core.windows.net`,credentials);
const containerClient = blobServiceClient.getContainerClient(CONTAINER_NAME);
//app.use(bodyParser.urlencoded({ extended: false }));
const upload = multer({dest:'/uploads'})
app.post('/upload',upload.single('file'), async (req,res)=>{
var des_file = __dirname + '/tmp/' +req.file.originalname;
const stream = _fs.createReadStream(req.file.path)
const blobClient = containerClient.getBlobClient(req.file.originalname);
const blockBlobClient = blobClient.getBlockBlobClient();
const uploadOptions = {
bufferSize: FOUR_MEGABYTES,
maxBuffers: 5,
};
const result = await blockBlobClient.uploadStream(
stream,
uploadOptions.bufferSize,
uploadOptions.maxBuffers,
aborter);
res.json(result)
})
const port =process.env.PORT||3000;
app.listen(port,()=>{
console.log('server on port:', port)
})
Azure Blob js SDK
—>自己的栗子
关于Upload功能的概述
file input
前端文件上传入口一律使用input type=”file”, 关于UI的优化可参考Angular-Tips
图片以base64存放关系数据库
File对象转Uri1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19<input type="file" (change)="handleUpload($event)">
————————————————————————————
handleUpload(event) {
const fileInput = event.target;
const imgFile: File = fileInput.files[0];
if (imgFile.type !== 'image/jpeg' && imgFile.type !== 'image/png') {
this.msgService.error('You can only upload JPG file or PNG file.');
return;
}
if (imgFile.size! / 1024 > this.logoSizeLimit) {
this.msgService.error(`Image must smaller than ${this.logoSizeLimit}k.`);
return;
}
const reader = new FileReader();
reader.onload = (e) => {
this.logoUri = e.target.result;
}
reader.readAsDataURL(imgFile);
}
logoUri即图片经Bese64编码的字符串,可以直接存入数据库字段,可放入img:src作为上传预览
文件和流
Blob 对象表示一个不可变、原始数据的类文件对象。它的数据可以按文本或二进制的格式进行读取,也可以转换成 ReadableStream 来用于数据操作。
File继承Blob, File作为特殊的Blob,可以用在任意的 Blob 类型的 context 中。比如FileReader, URL.createObjectURL(), createImageBitmap(), 及 XMLHttpRequest.send()
上一节使用的FileReader.readAsDataURL方法,读取指定的 Blob 或 File 对象。读取操作完成的时候,readyState 会变成已完成DONE,并触发 loadend 事件,同时 result 属性将包含一个data:URL格式的字符串(base64编码)以表示所读取文件的内容。
URL.createObjectURL(object)返回一个DOMString,其包含object的URL,console中输出的话所谓的DOMString形如1
blob:https://localhost:44362/fd57b5f3-a3b9-47ae-bd9d-56fc9012fb83
其生命周期与当前document相同,调用URL.revokeObjectURL释放
canvas.toBlob1
2
3
4
5
6
7
8
9
10function canvas2file(){
var image = document.querySelector('img');
var canvas = document.createElement("canvas");
canvas.width = image.width;
canvas.height= image.height;
var ctx = canvas.getContext("2d");
ctx.drawImage( image, 0, 0 );
console.log(canvas.toBlob());//转换成bold类型
console.log(canvas.toDataURL());//转换成dataURL类型
}
格物致知:从响应式编程理解‘流’
nodejs_stream
React组件交互
受控组件和不受控组件
以封装Html表单控件的组件为例,假设ControlledComponent渲染一个input控件,为使input的value可以通过ControlledComponent进行控制,
会在ControlledComponent的state为input的value创建一个属性,比如this.state.text,这个属性会绑定到input上,同时input修改时会触发onChange事件,于是在onChange的响应方法中setState更新1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class ControlledComponent extends React.Component{
constructor (props) {
super(props);
this.state = {
text: "add your comments"
}
}
onChange (e) {
console.log(e.target.value);
this.setState({
text: e.target.value
})
}
render () {
return <input name="text" value={this.state.text} onChange={(e) => this.onChange(e)} />
}
}
在HTML的表单元素中,它们通常自己维护一套state,并随着用户的输入自己进行UI上的更新,这种行为是不被我们程序所管控的。而如果将React里的state属性和表单元素的值建立依赖关系,再通过onChange事件与setState()结合更新state属性,就能达到控制用户输入过程中表单发生的操作。被React以这种方式控制取值的表单输入元素就叫做受控组件。 掘金:受控和非受控组件真的那么难理解吗?
props
略 见React 子组件传参
context
Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。
App.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 import Tools from "./Tools"
const AppContext = React.createContext(null);
const [ var1, var2, var3] =[ "111", "121", "311" ]
const App = () => {
return (
<AppContext.Provider value={
{
var1,
var2,
var3
}
}>
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
<Tools />
</AppContext.Provider>
);
};
export { App, AppContext };
Tools.tsx略
ToolButton.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14 import { AppContext } from "./App";
export default () => {
return (
<AppContext.Consumer>
{({ var1, var2, var3 }) => (
<>
<button >{var1}</button>
<button >{var2}</button>
<button >{var3}</button>
</>
)}
</AppContext.Consumer>
);
};hook
这里不只是组件交互的范畴,React hook是一套新的状态管理API
useState
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 import React, { useState } from 'react';
function Example() {
// 声明一个叫 “count” 的 state 变量。
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useState使用闭包定义setXX函数(参考函数式编程看React Hooks),大致如下
1
2
3
4
5
6
7
8
9 let _state;
function useState(initialState) {
_state = _state || initialState; // 如果存在旧值则返回, 使得多次渲染后的依然能保持状态。
function setState(newState) {
_state = newState;
render(); // 重新渲染,将会重新执行 Counter
}
return [_state, setState];
}
useEffect
Effect是指获取数据,订阅,Dom操作等。useEffect跟 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API,即组件在初始化完成、重新渲染、即将销毁时执行useEffect指定的逻辑。
1
2
3
4
5
6
7
8
9
10 /**
* Accepts a function that contains imperative, possibly effectful code.
*
* @param effect Imperative function that can return a cleanup function
* @param deps If present, effect will only activate if the values in the list change.
*
* @version 16.8.0
* @see https://reactjs.org/docs/hooks-reference.html#useeffect
*/
function useEffect(effect: EffectCallback, deps?: DependencyList): void;
useEffect接受一个‘事件’列表作为可选参数
关于订阅/取消订阅和清除函数
常见于需要在组件初始化后(componentDidMount)设置订阅,在组件销毁前(componentWillUnmount)取消订阅以免内存泄漏,对应于使用Effect钩子的函数式React组件:
1
2
3
4
5
6
7
8
9
10 useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
Effectf返回一个函数,该函数会作为Effect钩子的清除函数,React组件自动在销毁前执行清除函数
useContext
上面的ToolBtn经改写变为
1
2
3
4
5
6
7
8
9
10 export default () => {
const {var1, var2, var3} = useContext(AppContext);
return (
<>
<button >{var1}</button>
<button >{var2}</button>
<button >{var3}</button>
</>
)
};
useCallback
useCallback是一个允许在多次渲染中缓存函数的hook
useState不能返回一个方法(读取类型为function的state,会自动执行返回结果),当需要根据若干state的变化得到动态的函数时,使用useCallback, useCallback的定义形如useEffect1
2
3
4
5
6
7
8useEffect(()=>{
// do sth with refresh
renderView()
},[refresh])
renderView = useCallback(()=>{
// do render data
}, [renderer, data])
上述renderView方法随renderer, data的变化更新函数体实现,而renderView的执行时机只受refresh状态触发(而不需要在同时监听三个状态的副作用中判断是否需要执行renderView)
但是调用useCallback方法时,函数体可能已因依赖项改变而改变
useMemo
在组件重复渲染中缓存结果
其他hooks
关于函数式组件和Hooks
使用Hooks代替class中的生命周期函数,是函数式组件进行逻辑复用、状态管理的方式
旧的思维:“我在这个生命周期要检查props.A和state.B(props和state),如果改变的话就触发xxx副作用”。这种思维在后续修改逻辑的时候很容易漏掉检查项,造成bug。新的思维:“我的组件有xxx这个副作用,这个副作用依赖的数据是props.A和state.B”。从过去的命令式转变成了声明式编程。
———— csr632 《为什么 React 现在要推行函数式组件,用 class 不好吗?》下的回答
Web无障碍
Azure 标识管理
用户删除后挂起30日,期间可以还原
yarn
install packages:1
yarn
execuate project1
yarn [YourScriptInPackageJSON]
yarn 和 npm
- yarn 速度更快(并行和离线缓存)
- lock 版本 (与package-lock.json)
yarn.lock
- 如package-lock.json 开发过程中不应删除重建 应当及时提交
- yarn 过程根据yarn.lock的依赖树安装及拷贝package 且对与package.json不一致的依赖进行更新
- 使用 yarn upgrade 根据package.json对依赖进行升级 并更新yarn.lock
yarn 和 yarn install
yarn install is used to install all dependencies for a project. This is most commonly used when you have just checked out code for a project, or when another developer on the project has added a new dependency that you need to pick up.
If you are used to using npm you might be expecting to use —save or —save-dev. These have been replaced by yarn add and yarn add —dev. For more information, see the yarn add documentation.Running yarn with no command will run yarn install, passing through any provided flags.
查看包版本 yarn info xxpkg
yarn list xxpkg —depth=0
从私有Repository安装
配置.yarnrc.yml 即yarn resouce configure1
2
3
4
5
6
7
8npmRegistries:
//qqstone.jfrog.io/artifactory/api/npm/Viewer/:
npmAlwaysAuth: true
npmAuthIdent: c2hpLnFpdUAlbnZpc3RhY28uY236QUtDcDhuSER6YWo3NDNIekNDOVRxOW1Kb0tGVHVaKU5yZ2N4aU5jaWVRQ0hEb2tNR0ROTE43TGkybV5aRkVzSkxkUzdMYkDudA==
npmScopes:
qqsjfrog:
npmRegistryServer: "https://qqstone.jfrog.io/artifactory/api/npm/Viewer/"
TroubleShooting
Error: This tool requires a Node version compatible with >=18.12.0
Yarn v4要求nodejs >=18.12.0
切换Yarn version的命令是 yarn set version 3.0.2
但是在v4且node imcompatible的情况下任何yarn命令均失效只有npm i -g yarn 并没有npm i -g yarn@3 因此使用nvm切到node18再复原是较好的办法
YN0028: The lockfile would have been modified by this install, which is explicitly forbidden.
1
yarn install --frozen-lockfile false
see Github issue
Error: Your application tried to access XXX but it isn’t declared in your dependencies; this makes the require call ambiguous and unsound
疑因使用nvm导致存在冲突的yarn cache路径,导致程序并没有按yarn.lock确定所需的依赖包,该问题在卸载nvm,删除%USERPROFILE%\AppData\Local\Yarn\Berry文件并重装nodejs后解决
大前端
跨端开发,混合应用,小程序等
工程化
- 编码规范
- repository规范
- ci/cd
- 测试 单元测试 e2e
- 性能
- 数据上报 requestIdleCallback, restful api
- 用户行为收集 metadata, page/input/click… —> kafka Queue —> ElasticSearch
Tips
刷新/关闭页面前发请求
Navigator.sendBeacon(url, data) 在页面unload时也可以调用
microservice和serverless
Interview T-stack
网络延迟
响应式设计
移动端‘像素点’密度往往高于桌面显示器,而横纵比例较小,使用移动设备打开页面,可能因为宽度不足产生挤压变形,也可能因为自动像素缩放导致layout变形
视口(viewport)
在document形成的整个视图中,从一个虚拟的窗口观察,视口较小时,需要借助横纵滚动条才能浏览页面1
<meta name=”viewport” content=”width=device-width, initial-scale=1, maximum-scale=1″>
视口宽度为设备宽度,初始缩放比例为1,最大缩放比例为1
水平滚动条是极差的用户体验,将视口宽度设置为设备宽度,将页面样式调整到方便垂直滚动浏览
- 请勿使用较大的固定宽度元素
- 不要让内容依赖于特定值的视口宽度
- 使用媒体查询为小屏幕和大屏幕应用不同样式
Grid
如Bootstrap Grid水平分割若干份,计算并罗列每份宽度,使用百分比,block之间用float排列,行末清除浮动1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22.col-1 {width: 8.33%;}
.col-2 {width: 16.66%;}
.col-3 {width: 25%;}
.col-4 {width: 33.33%;}
.col-5 {width: 41.66%;}
.col-6 {width: 50%;}
.col-7 {width: 58.33%;}
.col-8 {width: 66.66%;}
.col-9 {width: 75%;}
.col-10 {width: 83.33%;}
.col-11 {width: 91.66%;}
.col-12 {width: 100%;}
[class*="col-"] {
float: left;
padding: 15px;
border: 1px solid red;
}
.row::after {
content: "";
clear: both;
display: table;
}Breakpoint
设置一个边界,使用媒体查询,实现当宽度越过边界值时,触发从桌面屏幕layout到移动设备layout的跃变Images
tip: 图片的拉伸是有限的,专业的做法是为不同设备提供不同的资源HTML5 新元素1
2
3
4
5
6
7
8
9
10
11/* For width smaller than 400px: */
body {
background-image: url('img_smallflower.jpg');
}
/* For width 400px and larger: */
@media only screen and (min-width: 400px) {
body {
background-image: url('img_flowers.jpg');
}
}支持指定多个资源以及媒体条件 1
2
3
4
5<picture>
<source srcset="img_smallflower.jpg" media="(max-width: 400px)">
<source srcset="img_flowers.jpg">
<img src="img_flowers.jpg" alt="Flowers">
</picture>