The bundle
file is interpreted by the server and is used to create routes, manage assets, styles, a few other things. This article will cover all these aspects.
When a project is initialized with lvweb init
, the bundle file will be generated in the bundle directory of the project (bundle/bundle.lv
). The generated bundle file will have a home ViewRoute
and a main Stylesheet
:
import live-elements-web-server.bundle
import live-elements-web-server.router
import live-elements-web-server.style
import .app
instance bundle Bundle{
ViewRoute{ url: '/' c: Home }
Stylesheet{ src: './styles/style.css' output: 'style.css' }
}
When running lvweb serve
, the bundle file is sent to the server, where the route and stylesheet are prepared, optimized and then served to the client. ViewRoute
uses webpack behind the scenes to bundle all the files for that specific route. ViewRoute
is one type of route for views, but there are other route types available like the GetRoute
or PostRoute
, which will be covered in the following section.
GetRoute is used to define an endpoint for GET requests. The component is built on top of express api, so the implementation for the request handler is similar to the one used in express. This is how to use it in a bundle file:
instance bundle Bundle{
GetRoute{
url: '/get-route'
f: (req, res) => {
res.send('hello from get route')
}
}
}
The req
and res
objects are forwarded from express, so they have the same API and functionality.
GetRoute
also has a middleware
property, which is forwarded to the middleware parameter in express:
GetRoute{
url: '/get-route'
middleware: (req, res, next) {
console.log('passed by middleware')
next()
}
f: (req, res) => {
res.send('hello from get route')
}
}
Route paths and parameters are supported as well:
// match acd and abcd
GetRoute{ url: '/ab?cd' f: (req, res) => { res.send('ab?cd') }}
// match abcd, abbcd, abbbcd, and so on
GetRoute{ url: '/ab+cd' f: (req, res) => { res.send('ab+cd') }}
// match route parameters
GetRoute{ url: '/users/:userId/books/:bookId' f: (req, res) => { res.send(req.params) }}
To summarize, the following are the properties for GetRoute
:
url
:string
: The url of the routemiddleware
:Array|function
: Middleware handler(s).f
:function
: Route handler.The PostRoute
is used for post requests:
PostRoute{
url: '/post-route'
f: (req, res) => {
res.json(req.body)
}
}
The properties are the same as for the GetRoute
:
url
:string
: The url of the routemiddleware
:Array|function
: Middleware handler(s).f
:function
: Route handler.The ViewRoute
is stricter than get or post routes. Instead of the handler function, it specifies a component to load either on the client side or on the server side, depending on the render
property. The component must inherit either from PageView
or Application
:
ViewRoute{ url: '/' c: Home } // client side render
ViewRoute{ url: '/' c: Home render: ViewRoute.CSR } // client side render
ViewRoute{ url: '/' c: Home render: ViewRoute.SSR } // server side render
When rendering on the client side, view components should be declared in separate files, outside of any server imports, as the files will be bundled together for the client.
Note: When running lvweb serve
all routes will be rendered on the client, including routes with SSR rendering. This is due to the serve command running in development mode, as rendering on the client is easier to debug, since it supports live reloading. Compiling (lvweb compile
) and running in production (lvweb run
) will render these routes on the server side. There's a way to test out server rendered routes in development, by using --renderMode production
args in lvweb serve
:
lvweb serve --renderMode production
The following properties are available for ViewRoute
:
url
:string
: The url of the routemiddleware
:Array|function
: Middleware handler(s).c
:component
: Component to load on the client side.render
:ViewRoute.SSR
/ViewRoute.CSR
: Server or client side rendering.placement
:Page|ViewPlacement
: Layout placement. See pages or layouts section in this article for more info.Routes can be nested inside a Route
component. The GetRoute
below will be available at /nested/get-route
:
instance bundle Bundle{
Route{
url: '/nested'
GetRoute{
url: '/get-route'
f: (req, res) => {
res.send('in /nested/get-route')
}
}
}
}
Route
components support middleware that will be executed when any nested route gets requested:
instance bundle Bundle{
Route{
url: '/nested'
middleware: (req, res, next) {
console.log('passed by main route middleware')
next()
}
GetRoute{
url: '/get-route'
f: (req, res) => {
res.send('in /nested/get-route')
}
}
}
}
Assets are files that are served to the client. Bundles provide 3 components to manage assets: BundleFile
, BundleDirectory
and Stylesheet
.
BundleFile
is used to serve a single file at a specific route. When compiling the bundle, the file will be copied in the dist
directory.
instance bundle Bundle{
BundleFile{ src: './assets/images/background.jpg' output: '/images/background.jpg' }
BundleFile{ src: 'images-package/image.jpg' output: '/images/image.jpg' }
}
BundleFile
has 2 properties:
src
:string
: Specifies the source location for the asset, or in other words, the location where to serve the assset from.The location can be either relative to the root of the bundle package (./assets/images/background.jpg
), or inside a package found in node_modules
. For example, in case of a package-based url (images-package/image.jpg
), the server will look for the images-package
similar to how a javascript import would be searched for. In this example, the package might be found in node_modules/images-package/image.jpg
.output
::string
: The url the asset will be available at.BundleFile
can also be used to serve Javascript files.
instance bundle Bundle{
BundleFile{ src: 'bootstrap/dist/js/bootstrap.bundle.min.js' output: '/js/bootstrap.bundle.min.js' }
BundleFile{ src: 'bootstrap/dist/js/bootstrap.bundle.min.js.map' output: '/js/bootstrap.bundle.min.js.map' }
}
Directories can be served with BundleDirectory
:
instance bundle Bundle{
BundleDirectory{ src: './assets/images' output: '/images' }
BundleDirectory{ src: 'images-package/images' output: '/images-package' }
}
BundleDirectory
has the same 2 properties as BundleFile
:
src
:string
: Specifies the source location for the directory. This will be scanned recursively. The location can be either relative to the root of the package (./assets/images
), or inside a package to be searched for (images-package/images
).output
::string
: The url the folder files will be available at.The Stylesheet
component is used to declare stylesheets the same as way as bundle files:
Stylesheet{ src: './styles/style.css' output: 'style.css' }
Stylesheet
adds 2 more features.
Stylesheets can be merged:
Stylesheet{ src: './styles/header.css' output: 'style.css' } // create style.css
Stylesheet{ src: './styles/content.css' output: 'style.css' } // merge in style.css
And stylesheets can be processed:
Stylesheet{
src: './styles/style.css'
output: 'style.css'
process: PostCSS.create(TailwindCSS.createForPostCSS())
}
The example creates a PostCSS processing function with TailwindCSS plugin.
Custom processing functions can be implemented. See this code on how PostCSS.process
is implemented:
import postcss from 'postcss'
import url from 'postcss-url'
component PostCSS{
Array plugins: []
static fn create(plugins:Array){
const postcss = PostCSS{}
postcss.plugins = plugins ? plugins instanceof Array ? plugins : [plugins] : []
return postcss
}
static fn createUrlPlugin(opts:Object){
return url(opts)
}
fn process(file:string, content:string, destination:string){
return postcss(this.plugins)
.process(content, { from: file, to: destination })
.then(result => {
return { content: result.css, map: result.map ? result.map.toString() : null }
})
}
}
To summarize the 3 properties from Stylesheet
:
src
:string
: style source location. This can be either relative to the root of the package, or inside an installed package.output
::string
: The url the file will be available at.process
::element
: The style processing element.All view routes are rendered on pages. For client side rendering routes, their page is loaded on the server side, while their content is then loaded on the client.
Pages are useful for 2 things:
ViewRoute
'sA ViewRoute
can specify the page it will render to by its placement
property.
A bundle provides a default IndexPage
, so ViewRoute
's' without a placement property set will simply render on that page:
import live-web.dom
import .page
component IndexPage < Page{
output: 'index.html'
title: 'Index'
clientRender: body
Body{
id: body
}
}
The page configures the output property, and since it's index.html
, the server knows it's the default page. The title
is the default title of the page in case it doesn't get overriden by the current PageView
. clientRender
is the actual location where the view will be rendered, in this case the page body
. We can also render deeper inside a page hierarchy, for example:
import live-web.dom
import .page
component IndexPage < Page{
output: 'index.html'
title: 'Index'
clientRender: content
Body{
Menu{ /* ... */ }
Section{
id: content
}
}
}
This will render inside the Section
element.
To create a custom page, simply create a page component inside your project:
import live-web.dom
import live-elements-web-server.page
component CustomPage < Page{
output: 'custom.html'
title: 'My Custom Page'
clientRender: content
Body{
H1`Custom Page`
Div{
id: content
}
}
}
And then add it to the bundle, and place a ViewRoute
to be rendered on that page:
import live-elements-web-server.bundle
import live-elements-web-server.router
import live-elements-web-server.style
import .app
instance bundle Bundle{
// create the page in the bundle
CustomPage{ id: customPage }
// assign the page to the placement property
ViewRoute{ url: '/' c: Home placement: customPage }
Stylesheet{ src: './styles/style.css' output: 'style.css' }
}
You will now see both the title from the page, and the content of the ViewRoute
.
On top of pages, layouts provide additional customization for ViewRoute
placement. Layouts are defined using the ViewPlacement
component. ViewPlacement
's sit between pages and ViewRoute
's, in order to share commong layouts.
For example, a page can share a layout with multiple views, a ViewPlacement
can define a menu for some, and another ViewPlacement
can add a sidebar for others:
import live-elements-web-server.bundle
import live-elements-web-server.router
import live-elements-web-server.style
import live-elements-web-server.view
import .app
instance bundle Bundle{
CustomPage{ id: customPage }
// create a MenuLayout inside CustomPage
ViewPlacement{ id: menuLayout c: MenuLayout placement: customPage }
// place the Home view inside the menuLayout
ViewRoute{ url: '/' c: Home placement: menuLayout }
// create a SidebarLayout inside CustomPage
ViewPlacement{ id: sidebarLayout c: SidebarLayout placement: customPage }
// place the same Home view inside the sidebar layout
ViewRoute{ url: '/with-sidebar' c: Home placement: sidebarLayout }
Stylesheet{ src: './styles/style.css' output: 'style.css' }
}
A ViewPlacement
component inherits ViewLayout
, and has a similar structure to a ViewPage
:
import live-elements-web-server.view
import live-web.dom
import live-web.behavior
component MenuLayout < ViewLayout{
id: layout
render: content
Div{
Div{ classes: ['menu']
H2`Menu`
}
Div{ id: content
}
}
}