Helping you serve vue with servue
Server-side rendering engine that renders vue files into html strings
- Servue
- Installation
- What is servue?
- Setup
- Layouts & Views
- Setting custom path variables
- Custom html body template
- Custom Preprocessor/Language Support
- Mode
- Head Management
- Passing data to vue from server-side
- Global State / Cross Component Data
- Precompiling Vue Pages
- Future Of This Package
- Package Changelog / Version
npm install servue --save
Then install peer dependencies (so you can control your vue version)
npm install vue --save
npm install vue-server-renderer --save
npm install vue-template-compiler --save
Servue server-side-renders Vue Single File Components (.vue
files) into html. It is a fully capable templating engine, and allows users to create nested layouts using slots and components.
The renderer provides the correct scripts, and styles on the client-side. It creates no extra build/compiled files. It's perfect for multi-page applications and great for users wanting to use Vue with Express or Koa
It's this easy to render .vue
files:
await servue.render('home') // renders "home.vue" into html string
Requires Node 8+
- Supports precompilation
- Supports layouts
- Supports templating
- Supports server-side Rendering
- Supports head management
- Supports imports CSS files and other assets
- Supports custom language preprocessors (less, pug, etc)
- No build or compile files, all in-memory
- Supports custom templating
const Servue = require("servue")
const express = require("express")
var app = express()
var servue = new Servue()
servue.resources = path.resolve(__dirname, "resources")
app.get('/', async (req, res) => {
res.send(await servue.render('home')) // renders "./resources/home.vue"
})
app.listen(2000)
const Servue = require("servue")
const Koa = require("koa")
var app = new Koa()
var servue = new Servue()
servue.resources = path.resolve(__dirname, "resources")
app.use(async (ctx) => {
ctx.body = await servue.render('home') // renders "./resources/home.vue"
})
app.listen(2000)
Servue fully supports templating features and allows for multiple and nested layouts using Vue slots.
Here's a simple example:
Ensure you wrap your app/root-level layout with
<servue>
. It is required for Servue to work.
<template>
<servue>
<template slot="head">
<title><slot name="title"/></title>
</template>
<template slot="content">
<h1>The page title is: <slot name="title"></slot></h1>
<slot name="content"/>
</template>
</servue>
</template>
This layout has a slot for content named content
and a slot for the title named title
Now, the home file can use this layout:
<template>
<parent>
<template slot="title">Home</template>
<template slot="content">
Hello
</template>
</parent>
</template>
<script>
import parent from "layouts/parent.vue"
export default {
components: {
parent //layouts can be nested, just provide the correct slots
}
}
</script>
<html>
<head>
<title>Home</title>
<!-- CSS, state and head are all automatically injected here -->
</head>
<body>
<div id="app">
<h1>The page title is: Home</h1>
Hello
</div>
<script>/** Vue client-side webpack bundle script is rendered here **/</script>
</body>
</html>
You may use your own custom paths for folders
//Sets views folder path which contains .vue .css .less .js and etc
servue.resources = path.resolve(__dirname, "resources")
//path of node_modules
servue.nodemodules = path.resolve(__dirname, 'node_modules')
servue.template = (content, context, bundle) => (`
<!DOCTYPE html>
<html${ context.htmlattr ? ' ' + context.htmlattr : '' }>
<head>
${context.head ? context.head : ''}
${context.renderResourceHints()}
${context.renderStyles()}
${context.renderState({windowKey: '__INITIAL_STATE__', contextKey: "data"})}
${context.renderState({windowKey: '__INITIAL_ROOTSTATE__', contextKey: "state"})}
</head>
<body>
${content}
<script>${bundle.client}</script>
</body>
</html>
`)
Here we add support for the LESS/SCSS/Stylus/pug preprocessor language so it can be used in our .vue
files.
In this example, we use the stylus-loader
packagetop to add stylus support to our package.
servue.webpackCommon.module.rules.push({
test: /\.styl(us)?$/,
use: [
'vue-style-loader',
'css-loader',
'stylus-loader'
]
})
<template>
<div class="red">
This will be red
</div>
</template>
<style lang="red">
.red{
color: red;
}
</style>
For faster compilation, you can use the production version of vue.
servue.mode = "production" //default = "development"
However, this will remove vue development warnings, so it is best to use development
for debugging & dev as it enables vue dev-tools.
<template>
<servue>
<template slot="head">
<title><slot name="title"/></title>
<meta name="og:title" content="This is my Facebook OpenGraph title">
<slot name="head"/> <!-- Pass slot for head in-case other child vues want to add additional elements to the head -->
</template>
<template slot="content">
<h1>Page: <slot name="title"/></h1>
<slot name="content"/>
</template>
</servue>
</template>
the head
template slot is rendered into the <head>
tags on the server-side. The head
tag can be nested, as shown in parent.vue
. This means that home.vue
may optionally pass it's own head to the user.
<template>
<parent>
<template slot="title">Home</template>
<template slot="content">
Hello
</template>
<template slot="head">
<meta name="meta2">
</template>
</parent>
</template>
<script>
import parent from "layouts/parent.vue"
export default {
components: {
parent //layouts can be nested, just provide the correct slots
}
}
</script>
head output
<head>
<title>Home</title>
<meta name="meta1">
<meta name="meta2">
<!-- Other Servue injections here -->
</head>
asyncData()
is a custom feature provided by servue, it allows you to make asynchronous requests like API calls. It is called on the server-side.
Data returned from asyncData is merged into the data()
function, and then also passed onto the client.
It only works on the rendered file, and not inside subcomponents or layouts.
router.get((ctx) => {
ctx.body = await servue.render('pages/home', {ctx})
})
pages/home.vue
export default {
async asyncData(ssrContext){
let ctx = ssrContext.ctx
return {
url: ctx.url,
hello: "World"
}
}
}
You can simply do this through the context argument
let data = { "hello": "world" }
await servue.render('home', {data})
This data is merged with your vue component's data function (if there is one), and can then be accessed by your vue file:
<template>
<div>
{{ hello }}
</div>
</template>
Output:
<div>
world
</div>
Data merges are shallow (via Object.assign
) and have this priority:
asyncData()
- If Providedservue.render('home', {data})
- If Provideddata()
- If Provided
To manage state globally (across components) and maintain reactivity, Servue has implemented a feature that easily allows you to do so.
All of your components will have access to a reactive object (including 3rd-party components)
this.$state
In your root-level parent layout which you share across multiple pages, you must implement the following code:
import statizer from "statizer" //this package is provided with Servue, so you don't need to add it as a dependency in package.json
/**
* You must declare all properties that components will use up-front because
* of the limitations of Vue's reactivity (which will be fixed in Vue 3.0)
*
* To add new properties, you should be able to use:
* ```js
* Vue.set(this.$state, 'name', val)
* ```
*/
statizer({
me: {
firstName: null,
lastName: null,
email: null,
username: null,
displayPicture: null
}
})
export default {
...
}
export default {
async serverPrefetch(){
// make async api request for user
let me = {
firstName: "Steve"
}
this.$state.firstName = me.firstName
}
}
export default {
async serverPrefetch(){
// make async api request for user
let me = {
firstName: "Steve"
}
/**
*
* never reassign this.$state or it will lose reference to the global
* object, and lose reactivity, if you want to assign a large number of
* varibles to the global state, use Object.assign(this.$state, obj)
*/
Object.assign(this.$state, {me})
}
}
Don't do this
this.$state = {foo: "bar" } // this will lose reference to the original global state
It's a memory intensive process to compile the .vue
SFCs. After they are compiled once, they are cached and rerendered at a far greater speed.
This means that the first time a .vue
SFC is rendered, it will take a few seconds to do so. You don't want this in a production environment, so we have an option for you to precompile all .vue
files so your users get the fastest speeds when visiting your website.
You can do this with the precompile function
/**
* ----------
* IMPORTANT:
* ----------
* Make sure to only precompile the pages you need, eg: you don't need to
* precompile header.vue` or footer.vue, only home.vue or about.vue,
* so seperate the actual full pages into a seperate folder.
*/
/**
* @param {string} folder - Folder that contains the .vue files to render
* @returns {Promise<void>}
*/
await servue.precompile('pages')
I may be further developing this package into a full MPA (multi-page apps) and SPA (single-page apps) capable application with internal routing using Vue Router.
It does somewhat unalign with the original goals of the project which was to easily integrate with koa
and express
, but it still will, just in a different way.
- Implement state management
- Reduce RAM usage by deleting virtual memory files after caching the renderers
- Make package more applicable for single page applications (SPAs)
- Potentially create an option for creating and serving build/dist files (as it takes a lot of ram and CPU to regenerate each SFC)
- Add hot-reloading
- Improve docs, create website
- Alternatively, create a nuxtjs "competitor" with servues head management system and layouts
You're no longer required to import the servue component in your master/parent layouts. It's automatically injected into every .vue
SFC
<template>
<servue>
Your content
</servue>
</template>
<script>
import servue from "servue.vue"
export default {
components: {
servue
}
}
</script>
Now becomes
<template>
<servue>
Your content
</servue>
</template>
We've changed the way data is passed from the server to the vue files
servue.render('home', data)
becomes
servue.render('home', {data})
Instead of passing data through a typical route controller, you can now pass it inside from your .vue
files (see asyncData()
above)
This was done because req/res
or ctx
data is often required in .vue
files so we have made it easier to pass data like ctx to the views, and allow routing to be simpler.
router.get('home', (ctx)=>{
let data = {...}
servue.render('home', {data, ctx})
})
Accessible in asyncData by
export default {
async asyncData(ssrContext){
let ctx = ssrContext.ctx
let url = ctx.url
return { url }
}
}
Future Australia Party Website - Lumina Government Website - Proma Times