mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-16 04:40:39 -07:00
perf: make the most of streamed rendering
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
[](https://www.npmjs.org/package/liquidjs)
|
[](https://www.npmjs.org/package/liquidjs)
|
||||||
[](https://www.npmjs.org/package/liquidjs)
|
[](https://www.npmjs.org/package/liquidjs)
|
||||||
[](https://coveralls.io/github/harttle/liquidjs?branch=master)
|
[](https://coveralls.io/github/harttle/liquidjs?branch=master)
|
||||||
[](https://travis-ci.org/harttle/liquidjs)
|
[](https://github.com/harttle/liquidjs/actions/workflows/check.yml?query=branch%3Amaster)
|
||||||
[](https://david-dm.org/harttle/liquidjs)
|
[](https://david-dm.org/harttle/liquidjs)
|
||||||
[](https://github.com/harttle/liquidjs/blob/master/LICENSE)
|
[](https://github.com/harttle/liquidjs/blob/master/LICENSE)
|
||||||
[](https://github.com/harttle/liquidjs)
|
[](https://github.com/harttle/liquidjs)
|
||||||
|
|||||||
+14
-4
@@ -3,7 +3,7 @@ const { Liquid } = require('liquidjs')
|
|||||||
const engine = new Liquid({
|
const engine = new Liquid({
|
||||||
root: __dirname,
|
root: __dirname,
|
||||||
extname: '.liquid',
|
extname: '.liquid',
|
||||||
globals: {title: 'LiquidJS Demo'}
|
globals: { title: 'LiquidJS Demo' }
|
||||||
})
|
})
|
||||||
|
|
||||||
engine.registerTag('header', {
|
engine.registerTag('header', {
|
||||||
@@ -21,6 +21,16 @@ const ctx = {
|
|||||||
todos: ['fork and clone', 'make it better', 'make a pull request']
|
todos: ['fork and clone', 'make it better', 'make a pull request']
|
||||||
}
|
}
|
||||||
|
|
||||||
engine.renderFile('todolist', ctx)
|
async function main () {
|
||||||
.then(console.log)
|
console.log('==========renderFile===========')
|
||||||
.catch(err => console.error(err.stack))
|
const html = await engine.renderFile('todolist', ctx)
|
||||||
|
console.log(html)
|
||||||
|
|
||||||
|
console.log('===========Streamed===========')
|
||||||
|
const tpls = await engine.parseFile('todolist')
|
||||||
|
engine.renderToNodeStream(tpls, ctx)
|
||||||
|
.on('data', data => process.stdout.write(data))
|
||||||
|
.on('end', () => console.log(''))
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
|||||||
@@ -7,6 +7,6 @@
|
|||||||
"start": "node index.js"
|
"start": "node index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"liquidjs": "*"
|
"liquidjs": "latest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-23
@@ -1,45 +1,39 @@
|
|||||||
import BlockMode from '../../context/block-mode'
|
import BlockMode from '../../context/block-mode'
|
||||||
import { BlockDrop } from '../../drop/block-drop'
|
import { BlockDrop } from '../../drop/block-drop'
|
||||||
import { ParseStream, TagToken, TopLevelToken, Template, Context, TagImpl, Emitter } from '../../types'
|
import { TagToken, TopLevelToken, Template, Context, TagImpl, Emitter } from '../../types'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
parse (this: TagImpl, token: TagToken, remainTokens: TopLevelToken[]) {
|
parse (this: TagImpl, token: TagToken, remainTokens: TopLevelToken[]) {
|
||||||
const match = /\w+/.exec(token.args)
|
const match = /\w+/.exec(token.args)
|
||||||
this.block = match ? match[0] : ''
|
this.block = match ? match[0] : ''
|
||||||
this.tpls = [] as Template[]
|
this.tpls = [] as Template[]
|
||||||
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
|
this.liquid.parser.parseStream(remainTokens)
|
||||||
.on('tag:endblock', () => stream.stop())
|
.on('tag:endblock', function () { this.stop() })
|
||||||
.on('template', (tpl: Template) => this.tpls.push(tpl))
|
.on('template', (tpl: Template) => this.tpls.push(tpl))
|
||||||
.on('end', () => {
|
.on('end', () => { throw new Error(`tag ${token.getText()} not closed`) })
|
||||||
throw new Error(`tag ${token.getText()} not closed`)
|
.start()
|
||||||
})
|
|
||||||
stream.start()
|
|
||||||
},
|
},
|
||||||
|
|
||||||
* render (this: TagImpl, ctx: Context, emitter: Emitter) {
|
* render (this: TagImpl, ctx: Context, emitter: Emitter) {
|
||||||
const blockRender = this.getBlockRender(ctx)
|
const blockRender = this.getBlockRender(ctx)
|
||||||
yield this.emitHTML(ctx, emitter, blockRender)
|
if (ctx.getRegister('blockMode') === BlockMode.STORE) {
|
||||||
|
ctx.getRegister('blocks')[this.block] = blockRender
|
||||||
|
} else {
|
||||||
|
yield blockRender(new BlockDrop(), emitter)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getBlockRender (this: TagImpl, ctx: Context) {
|
getBlockRender (this: TagImpl, ctx: Context) {
|
||||||
const { liquid, tpls } = this
|
const { liquid, tpls } = this
|
||||||
const extendedBlockRender = ctx.getRegister('blocks')[this.block]
|
const renderChild = ctx.getRegister('blocks')[this.block]
|
||||||
const defaultBlockRender = function * (superBlock: BlockDrop) {
|
const renderCurrent = function * (superBlock: BlockDrop, emitter: Emitter) {
|
||||||
|
// add {{ block.super }} support when rendering
|
||||||
ctx.push({ block: superBlock })
|
ctx.push({ block: superBlock })
|
||||||
const result = yield liquid.renderer.renderTemplates(tpls, ctx)
|
yield liquid.renderer.renderTemplates(tpls, ctx, emitter)
|
||||||
ctx.pop()
|
ctx.pop()
|
||||||
return result
|
|
||||||
}
|
|
||||||
return extendedBlockRender
|
|
||||||
? (superBlock: BlockDrop) => extendedBlockRender(new BlockDrop(() => defaultBlockRender(superBlock)))
|
|
||||||
: defaultBlockRender
|
|
||||||
},
|
|
||||||
|
|
||||||
* emitHTML (this: TagImpl, ctx: Context, emitter: Emitter, blockRender: (block: BlockDrop) => string) {
|
|
||||||
if (ctx.getRegister('blockMode', BlockMode.OUTPUT) === BlockMode.STORE) {
|
|
||||||
ctx.getRegister('blocks')[this.block] = blockRender
|
|
||||||
} else {
|
|
||||||
emitter.write(yield blockRender(new BlockDrop()))
|
|
||||||
}
|
}
|
||||||
|
return renderChild
|
||||||
|
? (superBlock: BlockDrop, emitter: Emitter) => renderChild(new BlockDrop(() => renderCurrent(superBlock, emitter)), emitter)
|
||||||
|
: renderCurrent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { assert, Tokenizer, Emitter, Hash, TagToken, TopLevelToken, Context, TagImplOptions } from '../../types'
|
import { assert, Tokenizer, Emitter, Hash, TagToken, TopLevelToken, Context, TagImplOptions } from '../../types'
|
||||||
import BlockMode from '../../context/block-mode'
|
import BlockMode from '../../context/block-mode'
|
||||||
import { parseFilePath, renderFilePath } from './render'
|
import { parseFilePath, renderFilePath } from './render'
|
||||||
|
import { BlankDrop } from '../../drop/blank-drop'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
parseFilePath,
|
parseFilePath,
|
||||||
@@ -16,8 +17,7 @@ export default {
|
|||||||
const { renderer } = liquid
|
const { renderer } = liquid
|
||||||
if (file === null) {
|
if (file === null) {
|
||||||
ctx.setRegister('blockMode', BlockMode.OUTPUT)
|
ctx.setRegister('blockMode', BlockMode.OUTPUT)
|
||||||
const html = yield renderer.renderTemplates(this.tpls, ctx)
|
yield renderer.renderTemplates(this.tpls, ctx, emitter)
|
||||||
emitter.write(html)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const filepath = yield this.renderFilePath(this['file'], ctx, liquid)
|
const filepath = yield this.renderFilePath(this['file'], ctx, liquid)
|
||||||
@@ -28,13 +28,14 @@ export default {
|
|||||||
ctx.setRegister('blockMode', BlockMode.STORE)
|
ctx.setRegister('blockMode', BlockMode.STORE)
|
||||||
const html = yield renderer.renderTemplates(this.tpls, ctx)
|
const html = yield renderer.renderTemplates(this.tpls, ctx)
|
||||||
const blocks = ctx.getRegister('blocks')
|
const blocks = ctx.getRegister('blocks')
|
||||||
if (blocks[''] === undefined) blocks[''] = () => html
|
|
||||||
|
// set whole content to anonymous block if anonymous doesn't specified
|
||||||
|
if (blocks[''] === undefined) blocks[''] = (parent: BlankDrop, emitter: Emitter) => emitter.write(html)
|
||||||
ctx.setRegister('blockMode', BlockMode.OUTPUT)
|
ctx.setRegister('blockMode', BlockMode.OUTPUT)
|
||||||
|
|
||||||
// render the layout file use stored blocks
|
// render the layout file use stored blocks
|
||||||
ctx.push(yield hash.render(ctx))
|
ctx.push(yield hash.render(ctx))
|
||||||
const partial = yield renderer.renderTemplates(templates, ctx)
|
yield renderer.renderTemplates(templates, ctx, emitter)
|
||||||
ctx.pop()
|
ctx.pop()
|
||||||
emitter.write(partial)
|
|
||||||
}
|
}
|
||||||
} as TagImplOptions
|
} as TagImplOptions
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ export class Context {
|
|||||||
this.globals = opts.globals
|
this.globals = opts.globals
|
||||||
this.environments = env
|
this.environments = env
|
||||||
}
|
}
|
||||||
public getRegister (key: string, defaultValue = {}) {
|
public getRegister (key: string) {
|
||||||
return (this.registers[key] = this.registers[key] || defaultValue)
|
return (this.registers[key] = this.registers[key] || {})
|
||||||
}
|
}
|
||||||
public setRegister (key: string, value: any) {
|
public setRegister (key: string, value: any) {
|
||||||
return (this.registers[key] = value)
|
return (this.registers[key] = value)
|
||||||
|
|||||||
@@ -3,10 +3,14 @@ import { Drop } from './drop'
|
|||||||
export class BlockDrop extends Drop {
|
export class BlockDrop extends Drop {
|
||||||
constructor (
|
constructor (
|
||||||
// the block render from layout template
|
// the block render from layout template
|
||||||
private superBlockRender: () => Iterable<string> = () => ''
|
private superBlockRender: () => Iterable<any> = () => ''
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Provide parent access in child block by
|
||||||
|
* {{ block.super }}
|
||||||
|
*/
|
||||||
public super () {
|
public super () {
|
||||||
return this.superBlockRender()
|
return this.superBlockRender()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
export interface Emitter {
|
export interface Emitter {
|
||||||
|
/**
|
||||||
|
* Write a html value into emitter
|
||||||
|
* @param html string, Drop or other primitive value
|
||||||
|
*/
|
||||||
write (html: any): void;
|
write (html: any): void;
|
||||||
end (): void;
|
/**
|
||||||
|
* Buffered string
|
||||||
|
*/
|
||||||
|
buffer: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { stringify, toValue } from '../util/underscore'
|
import { stringify, toValue } from '../util/underscore'
|
||||||
|
import { Emitter } from '../types'
|
||||||
|
|
||||||
export class KeepingTypeEmitter {
|
export class KeepingTypeEmitter implements Emitter {
|
||||||
public html: any = '';
|
public buffer: any = '';
|
||||||
|
|
||||||
public write (html: any) {
|
public write (html: any) {
|
||||||
html = toValue(html)
|
html = toValue(html)
|
||||||
@@ -9,14 +10,10 @@ export class KeepingTypeEmitter {
|
|||||||
// I.E:
|
// I.E:
|
||||||
// {{ my-port }} -> 42
|
// {{ my-port }} -> 42
|
||||||
// {{ my-host }}:{{ my-port }} -> 'host:42'
|
// {{ my-host }}:{{ my-port }} -> 'host:42'
|
||||||
if (typeof html !== 'string' && this.html === '') {
|
if (typeof html !== 'string' && this.buffer === '') {
|
||||||
this.html = html
|
this.buffer = html
|
||||||
} else {
|
} else {
|
||||||
this.html = stringify(this.html) + stringify(html)
|
this.buffer = stringify(this.buffer) + stringify(html)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public end () {
|
|
||||||
return this.html
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,9 @@ import { stringify } from '../util/underscore'
|
|||||||
import { Emitter } from './emitter'
|
import { Emitter } from './emitter'
|
||||||
|
|
||||||
export class SimpleEmitter implements Emitter {
|
export class SimpleEmitter implements Emitter {
|
||||||
public html: any = '';
|
public buffer = '';
|
||||||
|
|
||||||
public write (html: any) {
|
public write (html: any) {
|
||||||
this.html += stringify(html)
|
this.buffer += stringify(html)
|
||||||
}
|
|
||||||
|
|
||||||
public end () {
|
|
||||||
return this.html
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { stringify } from '../util/underscore'
|
import { stringify } from '../util/underscore'
|
||||||
|
|
||||||
export class StreamedEmitter {
|
export class StreamedEmitter {
|
||||||
public html: any = '';
|
public buffer = '';
|
||||||
public stream = new (require('stream').PassThrough)()
|
public stream = new (require('stream').PassThrough)()
|
||||||
public write (html: any) {
|
public write (html: any) {
|
||||||
this.stream.write(stringify(html))
|
this.stream.write(stringify(html))
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
export interface Emitter {
|
|
||||||
/**
|
|
||||||
* Write a html value into emitter
|
|
||||||
* @param html string, Drop or other primitive value
|
|
||||||
*/
|
|
||||||
write (html: any): void;
|
|
||||||
/**
|
|
||||||
* Notify the emitter render has ended
|
|
||||||
*/
|
|
||||||
end (): void;
|
|
||||||
/**
|
|
||||||
* Collect rendered string value immediately
|
|
||||||
*/
|
|
||||||
collect (): string;
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,7 @@ import { KeepingTypeEmitter } from '../emitters/keeping-type-emitter'
|
|||||||
export class Render {
|
export class Render {
|
||||||
public renderTemplatesToNodeStream (templates: Template[], ctx: Context): NodeJS.ReadableStream {
|
public renderTemplatesToNodeStream (templates: Template[], ctx: Context): NodeJS.ReadableStream {
|
||||||
const emitter = new StreamedEmitter()
|
const emitter = new StreamedEmitter()
|
||||||
toThenable(this.renderTemplates(templates, ctx, emitter))
|
toThenable(this.renderTemplates(templates, ctx, emitter)).then(() => emitter.end())
|
||||||
return emitter.stream
|
return emitter.stream
|
||||||
}
|
}
|
||||||
public * renderTemplates (templates: Template[], ctx: Context, emitter?: Emitter): IterableIterator<any> {
|
public * renderTemplates (templates: Template[], ctx: Context, emitter?: Emitter): IterableIterator<any> {
|
||||||
@@ -29,6 +29,6 @@ export class Render {
|
|||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return emitter.end()
|
return emitter.buffer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user