0%

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 方法消费流的数据。

从流到流

面试补充(2026-07-22)

本节为后续补充,用于资深软件工程师基础面试复习;上文保留原始笔记。

流解决的是分段处理与速度匹配

Stream 不只是“读取大文件的 API”。它提供一套协议,让数据可以分段产生、转换和消费,并在上下游速度不一致时传递背压。

readFile() 把完整文件读入内存相比,流式管道可以更早地产生首批输出,稳态内存通常与各阶段缓冲和并发量相关,而不是与整个文件大小线性增长。但这不是“流永远只占一个 chunk 的内存”:每个阶段都有缓冲,业务代码也可能缓存 chunk,错误的并发 Transform 仍会扩大内存。

四种流和两种模式

  • Readable 是数据源,例如文件读取、HTTP request 和查询结果游标。
  • Writable 是数据目标,例如文件写入、HTTP response 和压缩包输出。
  • Duplex 同时可读写,读写两侧相互独立,例如 TCP socket。
  • Transform 是一种 Duplex,输出由输入转换而来,例如 gzip、解码和逐行解析。

默认模式处理 BufferUint8Array 或字符串,highWaterMark 按字节相关单位工作。objectMode 可以传任意 JavaScript 值,此时 highWaterMark 计的是对象数量;一个对象可能非常大,所以“缓存 16 个对象”不代表内存很小。

Readable 还可能处于 paused 或 flowing 状态。注册 'data' 监听、调用 resume() 或使用 pipe() 会影响消费方式。不要同时混用 'data''readable'pipe() 和 async iterator 去竞争消费同一条流,除非清楚状态切换和数据所有权。

highWaterMarkwrite()drain

highWaterMark 是内部缓冲达到何种程度后开始施加背压的阈值,不是硬内存上限,也不表示单个 chunk 一定小于该值。

writable.write(chunk) 返回 false 的含义是:当前缓冲已达到或超过阈值,生产者应停止继续写,等待 'drain' 后再恢复。返回值并不表示这次 chunk 写入失败;数据通常已进入内部缓冲。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { once } from 'node:events'
import type { Writable } from 'node:stream'

async function writeAll(
writable: Writable,
chunks: AsyncIterable<Buffer>
): Promise<void> {
for await (const chunk of chunks) {
if (!writable.write(chunk)) {
await once(writable, 'drain')
}
}

const finish = once(writable, 'finish')
writable.end()
await finish
}

生产代码还要定义 error 和 abort 策略。若忽略 false 并持续写入,慢消费者前方的缓冲会不断积累,导致 RSS 增长、GC 压力、延迟扩大,最终可能进程退出。这个问题不是通过把 highWaterMark 调得更大就能根治;首先要让生产速度受消费能力约束。

pipe()pipeline()

readable.pipe(writable) 会处理常见的暂停与恢复,但多段管道发生错误时,业务仍需正确销毁其他阶段并等待收尾。pipeline() 把完成、错误传播和销毁组合为一个操作,Promise 版本也便于与 async/await 集成:

1
2
3
4
5
6
7
8
9
import { createReadStream, createWriteStream } from 'node:fs'
import { pipeline } from 'node:stream/promises'
import { createGzip } from 'node:zlib'

await pipeline(
createReadStream('telemetry.ndjson'),
createGzip(),
createWriteStream('telemetry.ndjson.gz')
)

如果读取失败、压缩失败或写入失败,返回的 Promise 会 reject,管道中的相关流会被销毁。调用方仍要决定如何记录、重试、清理不完整输出,以及任务是否幂等。对长任务还可以给 pipeline()AbortSignal,但取消后的业务状态仍需显式定义。

不要只写:

1
source.pipe(transform).pipe(destination)

然后只在最后一个流监听 error。错误可能来自任一阶段;手工拼装时必须明确每个阶段的错误传播和资源清理。

Async Iterator 与结束语义

Readable 支持 async iterator,适合用顺序控制流消费数据:

1
2
3
4
5
6
7
import { createReadStream } from 'node:fs'

const input = createReadStream('telemetry.ndjson', { encoding: 'utf8' })

for await (const chunk of input) {
console.log(chunk.length)
}

for await...of 会配合 Readable 的背压逐步拉取数据,异常会从循环抛出。提前 break 时通常会销毁流;若业务要求保留流继续消费,需要使用支持的 iterator 选项并明确后续所有者,而不是依赖偶然行为。

结束语义也必须区分:Readable 的 'end' 表示没有更多数据;Writable 的 'finish' 表示调用 end() 后数据已交给底层系统处理;'close' 表示资源关闭,不等于业务数据已经持久化。网络断开时还可能出现不完整消息,协议层需要帧边界、校验和重连策略。

大文件和实时消息的工程边界

大文件上传或压缩

优先让请求体、校验、压缩和对象存储上传形成受背压约束的管道。限制总大小、单段大小、处理时间和并发数;即使采用流,也不能接受无限输入。失败后清理临时文件或未完成的 multipart upload。

逐行遥测处理

TCP 和文件 chunk 不等于业务消息,一条 NDJSON 或协议帧可能跨多个 chunk,也可能一个 chunk 包含多条消息。解析器要保留不完整尾部、限制单帧长度,并对坏数据定义丢弃、告警或断连策略。

机器人与工业消息

背压策略取决于语义:高频位姿展示可能只保留每台设备最新值;报警、审计和控制命令通常不能静默覆盖。系统需要为不同消息类别定义有界队列、采样、合并、落盘或拒绝策略,而不是只有一个全局缓冲区。

“流为什么更省内存”的量化回答

比较两种实现时,应测量相同文件、并发和处理链路下的峰值 RSS、heapUsed、external memory、吞吐量和尾延迟。流式方案的预期不是内存为常数零,而是内存不随单个完整输入线性膨胀,并在消费者变慢时能限制生产者。

常见追问与回答边界

write() 返回 false 是否表示写入失败?

不是。它表示数据已进入缓冲,但生产者应等待 'drain' 再继续。真正的写入失败通过 error 通道报告。

highWaterMark 调大是否一定提高吞吐量?

不一定。更大的缓冲可能减少部分调度,但会提高内存和排队延迟,也可能掩盖消费端瓶颈。应基于 chunk 大小、下游速度和测量调整。

pipe() 已经有背压,为什么还需要 pipeline()

pipeline() 的主要增量是把整条链路的完成、错误传播和资源销毁收敛成一个可等待结果,而不是发明另一套背压。

使用 Stream 是否就不会内存泄漏?

不会。业务仍可能缓存 chunk、无限并发处理、忘记移除监听器或在错误后不销毁资源。Stream 提供协议,正确性取决于使用者是否遵守背压和生命周期。

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.Storage

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
39
40
41
42
43
44
45
using 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-blob
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
39
40
41
42
43
44
const 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

—>自己的栗子

file input

前端文件上传入口一律使用input type=”file”, 关于UI的优化可参考Angular-Tips

图片以base64存放关系数据库

File对象转Uri

1
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.toBlob

1
2
3
4
5
6
7
8
9
10
function 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

大文件分片上传

受控组件和不受控组件

以封装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
17
class 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的定义形如useEffect

1
2
3
4
5
6
7
8
useEffect(()=>{
// 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 不好吗?》下的回答

install packages:

1
yarn

execuate project
1
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 configure

1
2
3
4
5
6
7
8
npmRegistries:
//qqstone.jfrog.io/artifactory/api/npm/Viewer/:
npmAlwaysAuth: true
npmAuthIdent: c2hpLnFpdUAlbnZpc3RhY28uY236QUtDcDhuSER6YWo3NDNIekNDOVRxOW1Kb0tGVHVaKU5yZ2N4aU5jaWVRQ0hEb2tNR0ROTE43TGkybV5aRkVzSkxkUzdMYkDudA==

npmScopes:
qqsjfrog:
npmRegistryServer: "https://qqstone.jfrog.io/artifactory/api/npm/Viewer/"

TroubleShooting

  1. cannot be loaded because running scripts is disabled on this system.

  1. 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再复原是较好的办法

  2. YN0028: The lockfile would have been modified by this install, which is explicitly forbidden.

    1
    yarn install --frozen-lockfile false

    see Github issue

  3. 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后解决

2020前端展望

跨端开发,混合应用,小程序等

工程化

FCC article:前端工程化

  • 编码规范
  • 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

后端一次性给你10w条数据

网络延迟

抓包工具 Fiddler4 配置移动设备抓取 Https 请求


移动端‘像素点’密度往往高于桌面显示器,而横纵比例较小,使用移动设备打开页面,可能因为宽度不足产生挤压变形,也可能因为自动像素缩放导致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: 图片的拉伸是有限的,专业的做法是为不同设备提供不同的资源
    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');
    }
    }
    HTML5 新元素支持指定多个资源以及媒体条件
    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>

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import { Component, ContentChild, Input, TemplateRef } from "@angular/core";

@Component({
selector:'app-partner-carousel',
template:`
<div class="carousel">
<div class="btn-pre" (click)="carouselMove('LEFT')" [hidden]="carouselIndex===0"></div>
<div class="slick-window">
<div class="slick-container" *ngIf="data.length>0" [ngStyle]="carouselAnimotion">
<span *ngFor="let dataItem of data">
<ng-container
*ngIf="dataItemTemplateRef"
[ngTemplateOutlet]="dataItemTemplateRef"
[ngTemplateOutletContext]="{$implicit:dataItem}"></ng-container>
</span>
</div>
</div>
<div class="btn-next" (click)="carouselMove('RIGHT')" [hidden]="carouselIndex>=data.length-3"></div>
</div>
`,
styles:[`.carousel{display:flex}`,
`.slick-window {
position: relative;
width: 690px;
overflow: hidden;
transform: translateZ(0);
}`,
`.slick-container{
transform: translate3d(0px, 0px, 0px);
transition: 0.6s ease-in-out;
// width: 1500px;
position: relative;
line-height: inherit;
margin-right: 45px;
overflow-y: hidden;
}`,
`.slick-container>span{display:inline-block}`,
`.btn-pre, .btn-next {
// width: 15px;
cursor: pointer;
}`,
`.btn-pre::before, .btn-next::before {
content: '';
width: 0;
height: 0;
position: absolute;
top: 16px;
border-top: 22px solid transparent;
border-bottom: 22px solid transparent;
}`,
`.btn-pre::before {
border-right: 15px solid #ccc;
border-left: 0px solid transparent;
left: -15px;
}`,
`.btn-next::before {
border-right: 0px solid transparent;
border-left: 15px solid #ccc;
}`]
})
export class PartnerCarouselComponent{
readonly slickWidth = 230;
carouselAnimotion:any = {};
private _data:Array<any> = [];
@Input()
set data(value:Array<any>){
this.carouselAnimotion.width = `${this.slickWidth * value.length}px`;
this._data = value;
}
get data(){
return this._data;
}
@ContentChild('dataItem', {static:false}) dataItemTemplateRef:TemplateRef<any>;
carouselIndex:number = 0;
constructor(){}

carouselMove(orient: string) {
let distance = 0;
if (orient === 'LEFT') {
this.carouselIndex = this.carouselIndex > 3 ? this.carouselIndex -= 3 : 0
}
if (orient === 'RIGHT') {
this.carouselIndex = this.carouselIndex < this._data.length - 5 ? this.carouselIndex += 3 : this._data.length - 3;
}
distance = this.slickWidth * this.carouselIndex;
this.carouselAnimotion.transform = `translate3d(-${distance}px, 0px, 0px)`;
}
}

滑动原理

position: relative和transform: translateZ(0)将‘窗口’定位,其子元素是宽度超过屏幕宽度的所有滑块的集合,这个超长的‘胶片’用translate3d控制移动,使其某一格可以暴露在窗口中
移动的动画效果为 transition: 0.6s ease-in-out

循环体套用父元素模板

子组件(Carousel)中,为每个item套用其宿主定义的template,这里用到了NgContainer和TemplateRef,NgContainer使组件可以内嵌视图模板,内嵌的内容(ng-template)用ViewChild读取,此处声明为dataItemTemplateRef,使用NgTemplateOutlet指令将dataItemTemplateRef插入到ng-content位置,同时可以用ngTemplateOutletContext指令为内嵌视图附加一个上下文对象,显然就是为传递循环体的item打造的。
使用#id指定要插入到ng-container的template
官方示例:

1
2
3
4
5
6
7
8
9
10
<ng-container *ngTemplateOutlet="greet"></ng-container>
<hr>
<ng-container *ngTemplateOutlet="eng; context: myContext"></ng-container>
<hr>
<ng-container *ngTemplateOutlet="svk; context: myContext"></ng-container>
<hr>

<ng-template #greet><span>Hello</span></ng-template>
<ng-template #eng let-name><span>Hello {{name}}!</span></ng-template>
<ng-template #svk let-person="localSk"><span>Ahoj {{person}}!</span></ng-template>

其中 myContext = {$implicit: ‘World’, localSk: ‘Svet’};模板中用let-绑定上下文, $implicit(implicit 隐式的)指默认值,也就是例如let-keyXX=”parameterXX”的语法在缺省=”parameterXX”的表达下的值

响应式解决方案

npm install @angular/sdk
注意该package大版本跟随Angular版本
Angular CDK Layout

1
2
3
4
5
6
7
8
9
10
11
12
constructor(private breakpointObserver: BreakpointObserver) {
this.breakpointObserver.observe([Breakpoints.XSmall, Breakpoints.Small]).subscribe(result => {
if (result.matches) {
// max-width: 959px
this.windowWidth = { width: `${this.slickWidth}px` };
this.interval = 1;
} else {
this.windowWidth = { width: `${this.slickWidth * 3}px` };
this.interval = 3;
}
});
}