Merge pull request #202 from sveltejs/gh-178-store

add server- and client-side store management (#178)
This commit is contained in:
Rich Harris
2018-03-17 16:10:48 -04:00
committed by GitHub
8 changed files with 67 additions and 15 deletions

View File

@@ -13,3 +13,7 @@ export type Template = {
render: (data: Record<string, string>) => string; render: (data: Record<string, string>) => string;
stream: (req, res, data: Record<string, string | Promise<string>>) => void; stream: (req, res, data: Record<string, string | Promise<string>>) => void;
}; };
export type Store = {
get: () => any;
};

View File

@@ -19,7 +19,7 @@ type RouteObject = {
pattern: RegExp; pattern: RegExp;
params: (match: RegExpMatchArray) => Record<string, string>; params: (match: RegExpMatchArray) => Record<string, string>;
module: { module: {
render: (data: any) => { render: (data: any, opts: { store: Store }) => {
head: string; head: string;
css: { code: string, map: any }; css: { code: string, map: any };
html: string html: string
@@ -31,15 +31,22 @@ type RouteObject = {
type Handler = (req: Req, res: ServerResponse, next: () => void) => void; type Handler = (req: Req, res: ServerResponse, next: () => void) => void;
type Store = {
get: () => any
};
interface Req extends ClientRequest { interface Req extends ClientRequest {
url: string; url: string;
baseUrl: string;
originalUrl: string;
method: string; method: string;
pathname: string; path: string;
params: Record<string, string>; params: Record<string, string>;
} }
export default function middleware({ routes }: { export default function middleware({ routes, store }: {
routes: RouteObject[] routes: RouteObject[],
store: (req: Req) => Store
}) { }) {
const output = locations.dest(); const output = locations.dest();
@@ -75,7 +82,7 @@ export default function middleware({ routes }: {
cache_control: 'max-age=31536000' cache_control: 'max-age=31536000'
}), }),
get_route_handler(client_info.assetsByChunkName, routes) get_route_handler(client_info.assetsByChunkName, routes, store)
].filter(Boolean)); ].filter(Boolean));
return middleware; return middleware;
@@ -120,7 +127,7 @@ function serve({ prefix, pathname, cache_control }: {
const resolved = Promise.resolve(); const resolved = Promise.resolve();
function get_route_handler(chunks: Record<string, string>, routes: RouteObject[]) { function get_route_handler(chunks: Record<string, string>, routes: RouteObject[], store_getter: (req: Req) => Store) {
const template = dev() const template = dev()
? () => fs.readFileSync(`${locations.app()}/template.html`, 'utf-8') ? () => fs.readFileSync(`${locations.app()}/template.html`, 'utf-8')
: (str => () => str)(fs.readFileSync(`${locations.dest()}/template.html`, 'utf-8')); : (str => () => str)(fs.readFileSync(`${locations.dest()}/template.html`, 'utf-8'));
@@ -142,6 +149,7 @@ function get_route_handler(chunks: Record<string, string>, routes: RouteObject[]
res.setHeader('Link', link); res.setHeader('Link', link);
const store = store_getter ? store_getter(req) : null;
const data = { params: req.params, query: req.query }; const data = { params: req.params, query: req.query };
let redirect: { statusCode: number, location: string }; let redirect: { statusCode: number, location: string };
@@ -154,7 +162,8 @@ function get_route_handler(chunks: Record<string, string>, routes: RouteObject[]
}, },
error: (statusCode: number, message: Error | string) => { error: (statusCode: number, message: Error | string) => {
error = { statusCode, message }; error = { statusCode, message };
} },
store
}, req) : {} }, req) : {}
).catch(err => { ).catch(err => {
error = { statusCode: 500, message: err }; error = { statusCode: 500, message: err };
@@ -172,10 +181,15 @@ function get_route_handler(chunks: Record<string, string>, routes: RouteObject[]
return; return;
} }
const serialized = try_serialize(preloaded); // TODO bail on non-POJOs const serialized = {
preloaded: mod.preload && try_serialize(preloaded),
store: store && try_serialize(store.get())
};
Object.assign(data, preloaded); Object.assign(data, preloaded);
const { html, head, css } = mod.render(data); const { html, head, css } = mod.render(data, {
store
});
let scripts = [] let scripts = []
.concat(chunks.main) // chunks main might be an array. it might not! thanks, webpack .concat(chunks.main) // chunks main might be an array. it might not! thanks, webpack
@@ -184,7 +198,8 @@ function get_route_handler(chunks: Record<string, string>, routes: RouteObject[]
let inline_script = `__SAPPER__={${[ let inline_script = `__SAPPER__={${[
`baseUrl: "${req.baseUrl}"`, `baseUrl: "${req.baseUrl}"`,
mod.preload && serialized && `preloaded: ${serialized}`, serialized.preloaded && `preloaded: ${serialized.preloaded}`,
serialized.store && `store: ${serialized.store}`
].filter(Boolean).join(',')}}` ].filter(Boolean).join(',')}}`
const has_service_worker = fs.existsSync(path.join(locations.dest(), 'service-worker.js')); const has_service_worker = fs.existsSync(path.join(locations.dest(), 'service-worker.js'));

View File

@@ -1,10 +1,11 @@
import { detach, findAnchor, scroll_state, which } from './utils'; import { detach, findAnchor, scroll_state, which } from './utils';
import { Component, ComponentConstructor, Params, Query, Route, RouteData, ScrollPosition, Target } from './interfaces'; import { Component, ComponentConstructor, Params, Query, Route, RouteData, ScrollPosition, Store, Target } from './interfaces';
const manifest = typeof window !== 'undefined' && window.__SAPPER__; const manifest = typeof window !== 'undefined' && window.__SAPPER__;
export let component: Component; export let component: Component;
let target: Node; let target: Node;
let store: Store;
let routes: Route[]; let routes: Route[];
let errors: { '4xx': Route, '5xx': Route }; let errors: { '4xx': Route, '5xx': Route };
@@ -69,6 +70,7 @@ function render(Component: ComponentConstructor, data: any, scroll: ScrollPositi
component = new Component({ component = new Component({
target, target,
data, data,
store,
hydrate: !component hydrate: !component
}); });
@@ -227,7 +229,7 @@ function handle_touchstart_mouseover(event: MouseEvent | TouchEvent) {
let inited: boolean; let inited: boolean;
export function init(_target: Node, _routes: Route[]) { export function init(_target: Node, _routes: Route[], opts?: { store?: (data: any) => Store }) {
target = _target; target = _target;
routes = _routes.filter(r => !r.error); routes = _routes.filter(r => !r.error);
errors = { errors = {
@@ -235,6 +237,10 @@ export function init(_target: Node, _routes: Route[]) {
'5xx': _routes.find(r => r.error === '5xx') '5xx': _routes.find(r => r.error === '5xx')
}; };
if (opts && opts.store) {
store = opts.store(manifest.store);
}
if (!inited) { // this check makes HMR possible if (!inited) { // this check makes HMR possible
window.addEventListener('click', handle_click); window.addEventListener('click', handle_click);
window.addEventListener('popstate', handle_popstate); window.addEventListener('popstate', handle_popstate);

View File

@@ -1,9 +1,12 @@
import { Store } from '../interfaces';
export { Store };
export type Params = Record<string, string>; export type Params = Record<string, string>;
export type Query = Record<string, string | true>; export type Query = Record<string, string | true>;
export type RouteData = { params: Params, query: Query }; export type RouteData = { params: Params, query: Query };
export interface ComponentConstructor { export interface ComponentConstructor {
new (options: { target: Node, data: any, hydrate: boolean }): Component; new (options: { target: Node, data: any, store: Store, hydrate: boolean }): Component;
preload: (data: { params: Params, query: Query }) => Promise<any>; preload: (data: { params: Params, query: Query }) => Promise<any>;
}; };

View File

@@ -1,8 +1,11 @@
import { init, prefetchRoutes } from '../../../runtime.js'; import { init, prefetchRoutes } from '../../../runtime.js';
import { Store } from 'svelte/store.js';
import { routes } from './manifest/client.js'; import { routes } from './manifest/client.js';
window.init = () => { window.init = () => {
return init(document.querySelector('#sapper'), routes); return init(document.querySelector('#sapper'), routes, {
store: data => new Store(data)
});
}; };
window.prefetchRoutes = prefetchRoutes; window.prefetchRoutes = prefetchRoutes;

View File

@@ -3,6 +3,7 @@ import { resolve } from 'url';
import express from 'express'; import express from 'express';
import serve from 'serve-static'; import serve from 'serve-static';
import sapper from '../../../dist/middleware.ts.js'; import sapper from '../../../dist/middleware.ts.js';
import { Store } from 'svelte/store.js';
import { routes } from './manifest/server.js'; import { routes } from './manifest/server.js';
let pending; let pending;
@@ -77,7 +78,14 @@ const middlewares = [
next(); next();
}, },
sapper({ routes }) sapper({
routes,
store: () => {
return new Store({
title: 'Stored title'
});
}
})
]; ];
if (BASEPATH) { if (BASEPATH) {

View File

@@ -0,0 +1 @@
<h1>{{$title}}</h1>

View File

@@ -521,6 +521,18 @@ function run({ mode, basepath = '' }) {
assert.equal(title, '42'); assert.equal(title, '42');
}); });
}); });
it('renders store props', () => {
return nightmare.goto(`${base}/store`)
.page.title()
.then(title => {
assert.equal(title, 'Stored title');
return nightmare.init().page.title();
})
.then(title => {
assert.equal(title, 'Stored title');
});
});
}); });
describe('headers', () => { describe('headers', () => {