I have been working on my spare time on a Starter Project to make prototypes quickly running. But this does not fall short on functionalities: isomorphic routing, SSR, State management.
This focuses on Developer happiness and productivity (using what @manuel who made Viewmodel explained to me).
The file structure is very simple to understand. Even for designers
I kept it simple so you can expand in any direction easily.
That being said, this is opinionated about the technologies you should use, as the stack is the simplest I could find out there to quickly start a prototype project.
Create a file which will be called from both client (components) and server (router). It calls the server’s initial state with require because we don’t want to expose the internals of our app to the client. If it’s on the client it will return a global object (set in the page’s head by the router, see 3 below).
// /imports/initialState.js
let state;
if (Meteor.isClient) {
state = window.initialState;
} else {
state = require("./server/initialState").default;
}
export default state;
Modify /server/router.js so it puts the initial state on the global object:
// /server/router.js
// rest of imports
import initialState from "../imports/initialState";
const router = new UniversalRouter(routes);
function renderPage(thatSink) {
router.resolve({ path: thatSink.request.url.path }).then(route => {
// rest of block
const state = `<script>window.initialState = ${JSON.stringify(initialState)}</script>`;
thatSink.appendToHead(state);
});
}
// rest of file
Is this initial state common for all users? as far as I can tell, there is no way at the moment to get any session info in Meteor at the server when using the sink object.
Meteor server-render package is really great and enabled me to achieve a first paint of less than 1.4 seconds (used to to be almost 4 seconds) so I’m very grateful that @benjamn took this initiative.
But it seems we’re running into a limitation with the current implementation. We’re unable to infer any user information at the sink object, ideally we would’ve access to the userId and the request cookies in the server sink object so we dynamically generate html based on their state and data. There is currently an open issue discussing the need of extending the sink object.
Meteor uses client localstorage and then sockets to store and communicate the user session however this context in not available when doing SSR. The fast-render package had a workaround for this limitation by grabbing the user localstorage token from the client and passing it as a cookie and then it uses this cookie to grab the user at the server.
Using the user token to fetch the user data when doing SSR has some security concerns that were discussed here, and here.
Emily Stark from MDG already pointed out 3 years ago that at some point cookies and local storage need to co-exist for Meteor SSR to work. Here is an excerpt from her blog post:
Meteor and cookies will almost certainly have a long and happy relationship in the future. We will likely use cookies to implement server-side rendering; when you visit a Meteor app at foo.com that renders templates on the server, it needs to know who you are so that it can render the templates with your data in it. The only way for the very first HTTP request to foo.com to know who you are? A cookie set on a previous visit to foo.com! Moreover, as discussed above, we can combine cookies with localStorage tokens to achieve some of the best security properties of both mechanisms.
Perhaps a good first step is to extend the sink object request to include the cookies information which would allow us to create a package that inject the user data into the sink object, this should not be hard to do, I think it’s single block of code that needs to be modified in the webapp_server.js file. In addition to the cookies, ideally a userId field would also be available in the sink object if the user is logged in which can be used to retrieve user specific data.
I love the story but the best thing to do is just storing user data on the server instead of the database. Why a user cookie? Only to tell if the user can have automatic identification on the server at connection time
just storing user data on the server instead of the database
I’m not really sure how is that possible or what would that solve? if I hit https://yourapp.com then how does the server SSR function know that I’m new user or returning user with an already logged in session? and if I’m already logged in, then how can the server know it’s me and not someone else?
Just updated viewmodel.org with this project. I’ve been wanting to add SSR to the documentation for a long time. The only problem is that I had to move the Blaze docs to another domain (with a link in the main page). I wanted to keep both docs under the same roof but I don’t think I should wait until SSR for a hybrid app is available (if that ever happens).
I’m glad it was helpful
Just a remark but do you plan to transfer the old Help section to somewhere else? There was some good info in that, it would be a shame to lose it forever
In Universal Router I want to asynchronously pass my collection of Ads as a ‘props’ in order to access the collection stored in component’s state from server side (I hope this will fix the promise error).
I have one question: can I reuse ads from <App data={ads} /> inside my App’s ViewModel component and children ? if so in which vm property ?
Or Do I have to write this in plain React to get the props data?
I am working on next steps for Protostarter.
I made Inferno-ProtoStarter, a version that is more advanced and showcase more features like nested routes and added client-side navigation between routes if javascript is enabled.
I think Inferno version will be the main one since there are so many problems with React licensing, and Inferno is actually faster than React.
I would like to showcase more features. The next steps are:
Manage to pre-load DB on server so it serves a list of Ads at SSR time (+control performance of doing this) @alawi if you have a working code that demoes this I would be happy to get it
Add a form for users to add their own Ads.
Remove insecure and autopublish packages, and manage methods accordingly
Add simple User account management with Meteor account and a custom UI for it.
Add Local storage / Cookies to identify the current user on the server at startup and SSR DB data accordingly
Session storage Persistence : demonstrate how to persist user inputs (text area, checkboxes…) upon navigation (when changing routes and coming back - maybe a local minimongo will do it)
Make routes filtered by user roles
Infinite scrolling demo and other UI goodness demonstrations (Loading component, Modal dialog, animations, Tabs, Search with highlights…)
Have a component written in Native Inferno, that would be integrated with VM components (to showcase that the hybrid approach is possible). @manuel if you already have some working code of this I would be happy to get it .
Have actual tests (unit and integration) -> I have no knowledge here
Make it a PWA (Progressive Web App): Full offline capability, responsive…
I will also make an architecture pattern like Mantra that will go along with the ProtoStarter.
My goal is to have a generic, morphable project that demonstrates many use cases of current webapp needs, easy to get-going and add, remove or edit features following the architecture pattern.
VM is perfect for that. Thanks for making it @manuel
Of course, some help from the community is appreciated so if you show interest in this project and would like to help make this happen, please contact me and we can work together.
The proper way to test a VM component is to test the component’s properties and methods, and that they’re properly bound to the template. I haven’t tried it but you should be able to instantiate an Inferno component the same way you do with a React one: var myComponent = new MyComponent();.
Testing the bindings requires a mechanism to inspect the rendered component. With React I use Enzyme:
describe('bindings', ()=>{
const rendered = shallow(<Person />);
it('binds first name', ()=>{
const elements = rendered.find('input[data-bind="value: firstName"]');
expect(elements.length).toBe(1);
});
})
If you can do that with Inferno then testing is easy.
New version of proto-starter is out, with React 16, Universal-router 4.2.1, etc… the first meaningful paint now happens at ~1500 ms from 4990 ms in previous version, huge improvement
I think I will stop iterating on Inferno and will go back on React as well, with this improvement and the new license it makes sense again, even if I don’t trust facebook that much on their intentions.
The Google Lighthouse audit has better numbers as well: on my crappy machine, for performance, I went from 32 to 65 ! (x2)
Well that’s a very good improvement with the new React 16 SSR.
I also added client-side navigation so when you click on About us it should feel way faster as well.
I initially thought React 16 was now faster than Inferno, but this is actually not true. Inferno is still better.
I created the branch Inferno on the repo if you want to test: you will get around 1300 ms for Inferno vs 1500ms for React.