From aea34418de24cb85ea1acddf68c3683ce7fc9fa8 Mon Sep 17 00:00:00 2001 From: Harttle Date: Fri, 1 Oct 2021 17:30:35 +0800 Subject: [PATCH] perf: make the most of streamed rendering --- README.md | 2 +- demo/nodejs/index.js | 18 ++++++++++--- demo/nodejs/package.json | 2 +- src/builtin/tags/block.ts | 40 ++++++++++++---------------- src/builtin/tags/layout.ts | 11 ++++---- src/context/context.ts | 4 +-- src/drop/block-drop.ts | 6 ++++- src/emitters/emitter.ts | 9 ++++++- src/emitters/keeping-type-emitter.ts | 15 +++++------ src/emitters/simple-emitter.ts | 8 ++---- src/emitters/streamed-emitter.ts | 2 +- src/render/emitter.ts | 15 ----------- src/render/render.ts | 4 +-- 13 files changed, 65 insertions(+), 71 deletions(-) delete mode 100644 src/render/emitter.ts diff --git a/README.md b/README.md index 1193ca8d8..3fdcef560 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![npm version](https://img.shields.io/npm/v/liquidjs.svg?logo=npm&style=flat-square)](https://www.npmjs.org/package/liquidjs) [![npm downloads](https://img.shields.io/npm/dm/liquidjs.svg?style=flat-square)](https://www.npmjs.org/package/liquidjs) [![Coverage](https://img.shields.io/coveralls/harttle/liquidjs.svg?style=flat-square)](https://coveralls.io/github/harttle/liquidjs?branch=master) -[![Build Status](https://img.shields.io/github/checks-status/harttle/liquidjs/master?style=flat-square)](https://travis-ci.org/harttle/liquidjs) +[![Build Status](https://img.shields.io/github/workflow/status/harttle/liquidjs/Check/master.svg?style=flat-square)](https://github.com/harttle/liquidjs/actions/workflows/check.yml?query=branch%3Amaster) [![David dependencies](https://img.shields.io/david/harttle/liquidjs.svg?style=flat-square)](https://david-dm.org/harttle/liquidjs) [![DUB license](https://img.shields.io/dub/l/vibe-d.svg?style=flat-square)](https://github.com/harttle/liquidjs/blob/master/LICENSE) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg?style=flat-square)](https://github.com/harttle/liquidjs) diff --git a/demo/nodejs/index.js b/demo/nodejs/index.js index 957b3d5f0..88f1c1f6c 100644 --- a/demo/nodejs/index.js +++ b/demo/nodejs/index.js @@ -3,7 +3,7 @@ const { Liquid } = require('liquidjs') const engine = new Liquid({ root: __dirname, extname: '.liquid', - globals: {title: 'LiquidJS Demo'} + globals: { title: 'LiquidJS Demo' } }) engine.registerTag('header', { @@ -21,6 +21,16 @@ const ctx = { todos: ['fork and clone', 'make it better', 'make a pull request'] } -engine.renderFile('todolist', ctx) - .then(console.log) - .catch(err => console.error(err.stack)) +async function main () { + console.log('==========renderFile===========') + 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() diff --git a/demo/nodejs/package.json b/demo/nodejs/package.json index 78b8ef936..68a69386a 100644 --- a/demo/nodejs/package.json +++ b/demo/nodejs/package.json @@ -7,6 +7,6 @@ "start": "node index.js" }, "dependencies": { - "liquidjs": "*" + "liquidjs": "latest" } } diff --git a/src/builtin/tags/block.ts b/src/builtin/tags/block.ts index fb9ab7d29..d966017ba 100644 --- a/src/builtin/tags/block.ts +++ b/src/builtin/tags/block.ts @@ -1,45 +1,39 @@ import BlockMode from '../../context/block-mode' 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 { parse (this: TagImpl, token: TagToken, remainTokens: TopLevelToken[]) { const match = /\w+/.exec(token.args) this.block = match ? match[0] : '' this.tpls = [] as Template[] - const stream: ParseStream = this.liquid.parser.parseStream(remainTokens) - .on('tag:endblock', () => stream.stop()) + this.liquid.parser.parseStream(remainTokens) + .on('tag:endblock', function () { this.stop() }) .on('template', (tpl: Template) => this.tpls.push(tpl)) - .on('end', () => { - throw new Error(`tag ${token.getText()} not closed`) - }) - stream.start() + .on('end', () => { throw new Error(`tag ${token.getText()} not closed`) }) + .start() }, * render (this: TagImpl, ctx: Context, emitter: Emitter) { 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) { const { liquid, tpls } = this - const extendedBlockRender = ctx.getRegister('blocks')[this.block] - const defaultBlockRender = function * (superBlock: BlockDrop) { + const renderChild = ctx.getRegister('blocks')[this.block] + const renderCurrent = function * (superBlock: BlockDrop, emitter: Emitter) { + // add {{ block.super }} support when rendering ctx.push({ block: superBlock }) - const result = yield liquid.renderer.renderTemplates(tpls, ctx) + yield liquid.renderer.renderTemplates(tpls, ctx, emitter) 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 } } diff --git a/src/builtin/tags/layout.ts b/src/builtin/tags/layout.ts index 3a35eda74..8e32a94ba 100644 --- a/src/builtin/tags/layout.ts +++ b/src/builtin/tags/layout.ts @@ -1,6 +1,7 @@ import { assert, Tokenizer, Emitter, Hash, TagToken, TopLevelToken, Context, TagImplOptions } from '../../types' import BlockMode from '../../context/block-mode' import { parseFilePath, renderFilePath } from './render' +import { BlankDrop } from '../../drop/blank-drop' export default { parseFilePath, @@ -16,8 +17,7 @@ export default { const { renderer } = liquid if (file === null) { ctx.setRegister('blockMode', BlockMode.OUTPUT) - const html = yield renderer.renderTemplates(this.tpls, ctx) - emitter.write(html) + yield renderer.renderTemplates(this.tpls, ctx, emitter) return } const filepath = yield this.renderFilePath(this['file'], ctx, liquid) @@ -28,13 +28,14 @@ export default { ctx.setRegister('blockMode', BlockMode.STORE) const html = yield renderer.renderTemplates(this.tpls, ctx) 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) // render the layout file use stored blocks ctx.push(yield hash.render(ctx)) - const partial = yield renderer.renderTemplates(templates, ctx) + yield renderer.renderTemplates(templates, ctx, emitter) ctx.pop() - emitter.write(partial) } } as TagImplOptions diff --git a/src/context/context.ts b/src/context/context.ts index caba5c7e0..044345011 100644 --- a/src/context/context.ts +++ b/src/context/context.ts @@ -18,8 +18,8 @@ export class Context { this.globals = opts.globals this.environments = env } - public getRegister (key: string, defaultValue = {}) { - return (this.registers[key] = this.registers[key] || defaultValue) + public getRegister (key: string) { + return (this.registers[key] = this.registers[key] || {}) } public setRegister (key: string, value: any) { return (this.registers[key] = value) diff --git a/src/drop/block-drop.ts b/src/drop/block-drop.ts index 23c32d33c..0e994aa92 100644 --- a/src/drop/block-drop.ts +++ b/src/drop/block-drop.ts @@ -3,10 +3,14 @@ import { Drop } from './drop' export class BlockDrop extends Drop { constructor ( // the block render from layout template - private superBlockRender: () => Iterable = () => '' + private superBlockRender: () => Iterable = () => '' ) { super() } + /** + * Provide parent access in child block by + * {{ block.super }} + */ public super () { return this.superBlockRender() } diff --git a/src/emitters/emitter.ts b/src/emitters/emitter.ts index 381141d55..be08b6c2a 100644 --- a/src/emitters/emitter.ts +++ b/src/emitters/emitter.ts @@ -1,4 +1,11 @@ export interface Emitter { + /** + * Write a html value into emitter + * @param html string, Drop or other primitive value + */ write (html: any): void; - end (): void; + /** + * Buffered string + */ + buffer: string; } diff --git a/src/emitters/keeping-type-emitter.ts b/src/emitters/keeping-type-emitter.ts index ac488ffd1..7565f264f 100644 --- a/src/emitters/keeping-type-emitter.ts +++ b/src/emitters/keeping-type-emitter.ts @@ -1,7 +1,8 @@ import { stringify, toValue } from '../util/underscore' +import { Emitter } from '../types' -export class KeepingTypeEmitter { - public html: any = ''; +export class KeepingTypeEmitter implements Emitter { + public buffer: any = ''; public write (html: any) { html = toValue(html) @@ -9,14 +10,10 @@ export class KeepingTypeEmitter { // I.E: // {{ my-port }} -> 42 // {{ my-host }}:{{ my-port }} -> 'host:42' - if (typeof html !== 'string' && this.html === '') { - this.html = html + if (typeof html !== 'string' && this.buffer === '') { + this.buffer = html } else { - this.html = stringify(this.html) + stringify(html) + this.buffer = stringify(this.buffer) + stringify(html) } } - - public end () { - return this.html - } } diff --git a/src/emitters/simple-emitter.ts b/src/emitters/simple-emitter.ts index f96265b78..64a7f5b20 100644 --- a/src/emitters/simple-emitter.ts +++ b/src/emitters/simple-emitter.ts @@ -2,13 +2,9 @@ import { stringify } from '../util/underscore' import { Emitter } from './emitter' export class SimpleEmitter implements Emitter { - public html: any = ''; + public buffer = ''; public write (html: any) { - this.html += stringify(html) - } - - public end () { - return this.html + this.buffer += stringify(html) } } diff --git a/src/emitters/streamed-emitter.ts b/src/emitters/streamed-emitter.ts index 1fb9fb72e..17b7690f5 100644 --- a/src/emitters/streamed-emitter.ts +++ b/src/emitters/streamed-emitter.ts @@ -1,7 +1,7 @@ import { stringify } from '../util/underscore' export class StreamedEmitter { - public html: any = ''; + public buffer = ''; public stream = new (require('stream').PassThrough)() public write (html: any) { this.stream.write(stringify(html)) diff --git a/src/render/emitter.ts b/src/render/emitter.ts deleted file mode 100644 index 40ddece04..000000000 --- a/src/render/emitter.ts +++ /dev/null @@ -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; -} diff --git a/src/render/render.ts b/src/render/render.ts index 5a452cfd6..4d7eca6aa 100644 --- a/src/render/render.ts +++ b/src/render/render.ts @@ -10,7 +10,7 @@ import { KeepingTypeEmitter } from '../emitters/keeping-type-emitter' export class Render { public renderTemplatesToNodeStream (templates: Template[], ctx: Context): NodeJS.ReadableStream { const emitter = new StreamedEmitter() - toThenable(this.renderTemplates(templates, ctx, emitter)) + toThenable(this.renderTemplates(templates, ctx, emitter)).then(() => emitter.end()) return emitter.stream } public * renderTemplates (templates: Template[], ctx: Context, emitter?: Emitter): IterableIterator { @@ -29,6 +29,6 @@ export class Render { throw err } } - return emitter.end() + return emitter.buffer } }