Issue
I'm building document templater service where there are numerous document templates(contracts, protocols etc.) written in Vue. The idea behind this is that client sends props in body that are passed to the Vue component(document template) and rendered document is sent back to the client as a response. Vue is perfect fit for this task as it's easy to use and many of the templates are fairly complex so they need flexible enviroment that Vue has.
The tricky part is that I have quite complex script part that vary from doc to doc so I can't just take the template part of the component and render it with context. I need to transpile all parts of my Vue template(script setup + typescript, template and css) and then specify context(props) for my component.
For the transpilation part I'm trying to use webpack(vue-loader, ts-loader) with commonjs config as I can't use ESM because my express app would require fairly complex refactor to work with ESM as it's quite big.
The problem with webpack is that the bundle that produces can't be imported as when I try to import it trough this code:
export async function loadSSRTemplate(templateName: string) {
// I said that I'm using commonjs but this is typescript layer, so it's not compiled yet
// @ts-ignore
const bundle = await import('@/ssr/dist/main.js');
console.log(bundle.default); //this is always undefined
return bundle[templateName]; //this also
}
I can't find a way how to import my transpiled Vue components from the bundle so they can be used with createSSRApp and h() method in my templater service.
I know that there isn't any clear problem(I don't want to bother you with all my config and structure of Vue components) but all I need is someone who did something like this in the past and can tell me if it's possible and if i'm not just wasting time here.
PS: You can also suggest any other technology for this if you know smth that works but if it's possible I would prefer to do the transpilation beforehand(using build) for performance reasons.
My stack: Templates: Vue 3 + Typescript + Composition API(setup script) + Webpack 5.9
Service: Node 18(Express) + Typescript. Dockerized using Node:18. Exec using ts-node-dev with commonjs config
EDIT: For those interested enough to dig deeper, here is my webpack config:
const path = require('path');
const nodeExternals = require('webpack-node-externals');
const { VueLoaderPlugin } = require('vue-loader/dist/index');
module.exports = {
target: 'node',
mode: 'development',
entry: './webpack-entry-point.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
libraryTarget: 'commonjs2',
},
externals: nodeExternals({
allowlist: /\.css$/,
}),
module: {
rules: [
{
test: /\.ts$/,
loader: 'ts-loader',
options: { appendTsSuffixTo: [/\.vue$/] },
},
{
test: /\.vue$/,
loader: 'vue-loader',
},
],
},
resolve: {
alias: {
'@templates': path.resolve(__dirname, './templates'),
},
extensions: ['.ts', '.js', '.vue', '.json'],
},
plugins: [new VueLoaderPlugin()],
optimization: {
splitChunks: {
chunks: 'all',
},
},
};
entry point is nothing more than simple js file which imports all top-level components and exports them e.g.
import NajomnaZmluva from '@templates/reality_module/NajomnaZmluva.vue';
export { NajomnaZmluva };
My templater service's render method implementation:
async render(id: string, data: any): Promise<string> {
if (!process.env.SSR_DOCUMENT_TEMPLATE_PATH) {
console.error('SSR_DOCUMENT_TEMPLATE_PATH is not defined');
throw new UnknownServerError();
}
const documentTemplate = await this.retrieve(id);
if (!documentTemplate.schema) {
throw new BadRequestError(
responsesConstants.errors.badRequestError.schemaNotDefined,
);
}
this.validateSchema(
documentTemplate.schema as Record<string, schemaType>,
data,
);
//Template name will be specified dynamically. This is hardcoded for test purposes only
const Template = await loadSSRTemplate('NajomnaZmluva');
//I cant confirm if code below works as I'm stuck at loading the bundle
const ssrDocument = createSSRApp({
template: h(Template, { props: data }),
});
return await renderToString(ssrDocument);
}
EDIT: I'm finally making progress. For those interested I will post solution in a few hours.
Solution
For those interested in solution and my implementation of this functionality I will be updating this answer until I'll have fully working endpoint with the functionality I described.
- I resolved the issue of webpack not building working bundles with ability to export transpiled components. This is only for commonjs enviroment but I belive that ESM works just fine. I changed output block like this(see comments):
//webpack.config.js
...
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
//Change from previous config:
//I removed libraryTarget: 'commonjs2' and added library: { type: 'commonjs-module' }
library: {
type: 'commonjs-module',
},
},
...
EDIT: Completed ssr.util.ts file which handles ssr vue rendering from webpack bundle
// Import required functions and types from Vue and Vue server-renderer
import { renderToString } from 'vue/server-renderer';
import { createSSRApp, h, SetupContext } from 'vue';
import { RenderFunction } from '@vue/runtime-dom';
// Define an interface for the structure of a transpiled Vue component
interface TranspiledComponent {
ssrRender(props: Record<string, unknown>): RenderFunction;
setup(props: Record<string, unknown>, context: SetupContext): RenderFunction;
}
// Function to load a transpiled component from a webpack bundle
export async function loadSSRTemplate(templateName: string): TranspiledComponent {
// @ts-ignore is used to bypass TypeScript checks,
// as the specific module structure is known at runtime
//You can problably generate types in webpack to handle this but i didn't find it that important
const bundle = await import('@/ssr/dist/main.js');
return bundle[templateName];
}
// Function to render a Vue component with provided props into an HTML string
export async function renderVueComponent(
Component: TranspiledComponent,
propsData: Record<string, unknown>,
) {
// Create a Vue app for SSR with the component and its props
const app = createSSRApp({
render() {
// The 'h' function is used to create a VNode for the component
return h(Component, propsData);
},
});
// Render the app to an HTML string and return it
return await renderToString(app);
}
This util works with my webpack config that is fully shown in the question but you need to edit output block according to code snippet in this answer.
If you use my config there will be 2 files generated - main.js and chunk-vendors.js. Your vue components are in main.js so that's the file you should load in loadSSRTemplate
function. You can also find my relevant tech stack in the question.
Answered By - Daniel Klimek
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.