Skip to content

Commit

Permalink
feat: 开发服务注入中间件实现占位图片请求响应
Browse files Browse the repository at this point in the history
  • Loading branch information
pengzhanbo committed Jan 10, 2023
1 parent cf8ae15 commit 8a48a35
Show file tree
Hide file tree
Showing 10 changed files with 208 additions and 152 deletions.
4 changes: 1 addition & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
{
"cSpell.words": [
"middlewares"
]
"cSpell.words": ["middlewares"]
}
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,23 @@ export default defineConfig(() => ({

```

### 匹配规则
```ts
const rules = [
'',
'/:width?/:height?{.:type}?',
'/bg/:background/:width?/:height?{.:type}?',
'/text/:text/:width?/:height?{.:type}?',
'/text/:text/bg/:background/:width?/:height?{.:type}?',
'/bg/:background/text/:text/:width?/:height?{.:type}?',
].map((rule) => `${prefix}${rule}`)
```
- width
- height
- background
- text
- type


# 示例

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"release:changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"release": "bumpp package.json --execute=\"pnpm release:changelog\" --commit --all --push --tag && pnpm publish --access public"
},
"prettier": "@pengzhanbo/prettier-config",
"dependencies": {
"debug": "^4.3.4",
"lru-cache": "^7.14.1",
Expand All @@ -59,7 +60,6 @@
"peerDependencies": {
"vite": ">=3.0.0"
},
"prettier": "@pengzhanbo/prettier-config",
"packageManager": "pnpm@7.18.2",
"engines": {
"node": "^14.18.0 || >=16"
Expand Down
42 changes: 0 additions & 42 deletions src/generateImage.ts

This file was deleted.

94 changes: 0 additions & 94 deletions src/middlewares.ts

This file was deleted.

117 changes: 117 additions & 0 deletions src/pathToImage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { parse as urlParse } from 'node:url'
import { match, pathToRegexp } from 'path-to-regexp'
import type { Create, CreateText } from 'sharp'
import sharp from 'sharp'
import { cache } from './cache'
import { DEFAULT_PARAMS } from './constants'
import type {
ImageCacheItem,
ImagePlaceholderOptions,
ImagePlaceholderParams,
ImagePlaceholderQuery,
ImageType,
} from './types'
import { formatColor, formatText, getBackground } from './utils'

export type CreateOptions = Create

export type TextOptions = CreateText

export async function pathToImage(
url: string,
rules: string[],
options: Required<ImagePlaceholderOptions>,
): Promise<ImageCacheItem | undefined> {
if (cache.has(url)) {
return cache.get(url)
}
const { query: urlQuery, pathname } = urlParse(url, true)

const rule = rules.find((rule) => {
return pathToRegexp(rule).test(pathname!)
})

if (!rule) return

const urlMatch = match(rule, { decode: decodeURIComponent })(pathname!) || {
params: {
width: options.width,
background: getBackground(options.background),
type: options.type,
} as ImagePlaceholderParams,
}

const params = urlMatch.params as ImagePlaceholderParams
const query = urlQuery as ImagePlaceholderQuery
const imgType = params.type || DEFAULT_PARAMS.type!
const width = Number(params.width) || 300
const height = Number(params.height) || Math.ceil((width / 16) * 9)

const createOptions: CreateOptions = {
width,
height,
channels: 4,
background: formatColor(
params.background || getBackground(options.background),
),
}
if (Number(query.noise) === 1) {
createOptions.noise = {
type: 'gaussian',
mean: query.noiseMean || Math.ceil((Math.min(width, height) / 100) * 10),
sigma: query.noiseSigma || 10,
}
}

const textOptions: TextOptions = {
dpi: Math.floor((Math.min(width, height) / 4) * 3) || 1,
text: formatText(
params.text || options.text || `${width}x${height}`,
formatColor(query.textColor, true) || options.textColor,
),
rgba: true,
}

const imgBuf = await createImage(imgType, createOptions, textOptions)
const result: ImageCacheItem = {
type: imgType,
buffer: imgBuf,
}
cache.set(url, result)
return result
}

export async function createImage(
type: ImageType = 'png',
createOptions: CreateOptions,
textOptions?: TextOptions,
) {
let image = sharp({ create: createOptions })

textOptions && image.composite([{ input: { text: textOptions } }])

switch (type) {
case 'jpg':
case 'jpeg':
image = image.jpeg({ quality: 100 })
break
case 'webp':
image = image.webp({ quality: 100 })
break
case 'heif':
image = image.heif({ quality: 100 })
break
case 'avif':
image = image.avif({ quality: 100 })
break
case 'gif':
image = image.gif()
break
default:
image = image.png({ compressionLevel: 1 })
}

const buf = await image.toBuffer()

return buf
}
66 changes: 60 additions & 6 deletions src/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,89 @@
import type { Plugin } from 'vite'
import { DEFAULT_PREFIX } from './constants'
import { imagePlaceholderMiddlewares } from './middlewares'
import { generatePathRules } from './pathRules'
import { pathToImage } from './pathToImage'
import type { ImagePlaceholderOptions } from './types'
import { getMimeType, logger } from './utils'

const parseOptions = (
options: ImagePlaceholderOptions,
server = false,
): Required<ImagePlaceholderOptions> => {
options = Object.assign(
{
prefix: DEFAULT_PREFIX,
background: '#ccc',
textColor: '#999',
textColor: '#333',
width: 300,
type: 'png',
} as ImagePlaceholderOptions,
options,
)
options.prefix = `/${options.prefix}`.replace(/\/\//g, '/').replace(/\/$/, '')

if (!server) {
options.prefix = options.prefix.slice(1)
}

return options as Required<ImagePlaceholderOptions>
}

export function imagePlaceholderPlugin(
function placeholderServerPlugin(
options: ImagePlaceholderOptions = {},
): Plugin {
const opts = parseOptions(options)
const opts = parseOptions(options, true)
const pathRules = generatePathRules(opts.prefix)

return {
name: 'vite-plugin-image-placeholder',
name: 'vite-plugin-image-placeholder-server',
apply: 'serve',
async configureServer({ middlewares }) {
middlewares.use(imagePlaceholderMiddlewares(opts))
middlewares.use(async function (req, res, next) {
const url = req.url!
if (!url.startsWith(opts.prefix)) return next()
logger(url)

try {
const image = await pathToImage(url, pathRules, opts)

if (!image) return next()

res.setHeader('Accept-Ranges', 'bytes')
res.setHeader('Content-Type', getMimeType(image.type))
res.end(image.buffer)
return
} catch (e) {
console.error(e)
}
next()
})
},
}
}

function placeholderInjectPlugin(
options: ImagePlaceholderOptions = {},
): Plugin {
const opts = parseOptions(options)
const moduleId = `virtual:${opts.prefix}`
const resolveVirtualModuleId = `\0${moduleId}`
return {
name: 'vite-plugin-image-placeholder-inject',
resolveId(id) {
if (id.startsWith(moduleId)) {
return `\0${id}`
}
},
async load(id) {
if (id.startsWith(resolveVirtualModuleId)) {
// const url = id.replace(resolveVirtualModuleId, '')
}
},
}
}

export function imagePlaceholderPlugin(
options: ImagePlaceholderOptions = {},
): Plugin[] {
return [placeholderServerPlugin(options), placeholderInjectPlugin(options)]
}
Loading

0 comments on commit 8a48a35

Please sign in to comment.