mirror of
https://github.com/kevin-DL/sapper.git
synced 2026-01-22 15:15:19 +00:00
only replace components for changed segments
This commit is contained in:
@@ -255,14 +255,18 @@ function get_page_handler(routes: RouteObject, store_getter: (req: Req) => Store
|
|||||||
? {}
|
? {}
|
||||||
: get_params(page.pattern.exec(req.path));
|
: get_params(page.pattern.exec(req.path));
|
||||||
|
|
||||||
const chunks: Record<string, string> = get_chunks();
|
const chunks: Record<string, string | string[]> = get_chunks();
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'text/html');
|
res.setHeader('Content-Type', 'text/html');
|
||||||
|
|
||||||
// preload main.js and current route
|
// preload main.js and current route
|
||||||
// TODO detect other stuff we can preload? images, CSS, fonts?
|
// TODO detect other stuff we can preload? images, CSS, fonts?
|
||||||
const link = []
|
let preloaded_chunks = Array.isArray(chunks.main) ? chunks.main : [chunks.main];
|
||||||
.concat(chunks.main, error ? [] : page.parts.map(part => chunks[part.name]))
|
page.parts.forEach(part => {
|
||||||
|
preloaded_chunks = preloaded_chunks.concat(chunks[part.name]); // using concat because it could be a string or an array. thanks webpack!
|
||||||
|
});
|
||||||
|
|
||||||
|
const link = preloaded_chunks
|
||||||
.filter(file => !file.match(/\.map$/))
|
.filter(file => !file.match(/\.map$/))
|
||||||
.map(file => `<${req.baseUrl}/client/${file}>;rel="preload";as="script"`)
|
.map(file => `<${req.baseUrl}/client/${file}>;rel="preload";as="script"`)
|
||||||
.join(', ');
|
.join(', ');
|
||||||
|
|||||||
@@ -7,6 +7,31 @@ export let root: Component;
|
|||||||
let target: Node;
|
let target: Node;
|
||||||
let store: Store;
|
let store: Store;
|
||||||
let routes: Routes;
|
let routes: Routes;
|
||||||
|
let segments: string[] = [];
|
||||||
|
|
||||||
|
type RootProps = {
|
||||||
|
path: string;
|
||||||
|
params: Record<string, string>;
|
||||||
|
query: Record<string, string>;
|
||||||
|
child: Child;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Child = {
|
||||||
|
segment?: string;
|
||||||
|
props?: any;
|
||||||
|
component?: Component;
|
||||||
|
};
|
||||||
|
|
||||||
|
const root_props: RootProps = {
|
||||||
|
path: null,
|
||||||
|
params: null,
|
||||||
|
query: null,
|
||||||
|
child: {
|
||||||
|
segment: null,
|
||||||
|
component: null,
|
||||||
|
props: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export { root as component }; // legacy reasons — drop in a future version
|
export { root as component }; // legacy reasons — drop in a future version
|
||||||
|
|
||||||
@@ -52,10 +77,23 @@ function select_route(url: URL): Target {
|
|||||||
|
|
||||||
let current_token: {};
|
let current_token: {};
|
||||||
|
|
||||||
function render(data: any, scroll: ScrollPosition, token: {}) {
|
function render(data: any, changed_from: number, scroll: ScrollPosition, token: {}) {
|
||||||
if (current_token !== token) return;
|
if (current_token !== token) return;
|
||||||
|
|
||||||
if (root) {
|
if (root) {
|
||||||
|
// first, clear out highest-level root component
|
||||||
|
let level = data.child;
|
||||||
|
for (let i = 0; i < changed_from; i += 1) {
|
||||||
|
if (i === changed_from) break;
|
||||||
|
level = level.props.child;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { component } = level;
|
||||||
|
level.component = null;
|
||||||
|
root.set({ child: data.child });
|
||||||
|
|
||||||
|
// then render new stuff
|
||||||
|
level.component = component;
|
||||||
root.set(data);
|
root.set(data);
|
||||||
} else {
|
} else {
|
||||||
// first load — remove SSR'd <head> contents
|
// first load — remove SSR'd <head> contents
|
||||||
@@ -80,18 +118,32 @@ function render(data: any, scroll: ScrollPosition, token: {}) {
|
|||||||
window.scrollTo(scroll.x, scroll.y);
|
window.scrollTo(scroll.x, scroll.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Object.assign(root_props, data);
|
||||||
ready = true;
|
ready = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changed(a: Record<string, string | true>, b: Record<string, string | true>) {
|
||||||
|
return JSON.stringify(a) !== JSON.stringify(b);
|
||||||
|
}
|
||||||
|
|
||||||
function prepare_page(target: Target): Promise<{
|
function prepare_page(target: Target): Promise<{
|
||||||
redirect?: Redirect;
|
redirect?: Redirect;
|
||||||
data?: any
|
data?: any;
|
||||||
|
changed_from?: number;
|
||||||
}> {
|
}> {
|
||||||
if (root) {
|
if (root) {
|
||||||
root.set({ preloading: true });
|
root.set({ preloading: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { page, path, query } = target;
|
const { page, path, query } = target;
|
||||||
|
const new_segments = path.split('/').filter(Boolean);
|
||||||
|
let changed_from = 0;
|
||||||
|
|
||||||
|
while (
|
||||||
|
segments[changed_from] &&
|
||||||
|
new_segments[changed_from] &&
|
||||||
|
segments[changed_from] === new_segments[changed_from]
|
||||||
|
) changed_from += 1;
|
||||||
|
|
||||||
let redirect: Redirect = null;
|
let redirect: Redirect = null;
|
||||||
let error: { statusCode: number, message: Error | string } = null;
|
let error: { statusCode: number, message: Error | string } = null;
|
||||||
@@ -111,6 +163,8 @@ function prepare_page(target: Target): Promise<{
|
|||||||
};
|
};
|
||||||
|
|
||||||
return Promise.all(page.parts.map(async (part, i) => {
|
return Promise.all(page.parts.map(async (part, i) => {
|
||||||
|
if (i < changed_from) return null;
|
||||||
|
|
||||||
const { default: Component } = await part.component();
|
const { default: Component } = await part.component();
|
||||||
const req = {
|
const req = {
|
||||||
path,
|
path,
|
||||||
@@ -131,6 +185,8 @@ function prepare_page(target: Target): Promise<{
|
|||||||
return { redirect };
|
return { redirect };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
segments = new_segments;
|
||||||
|
|
||||||
const get_params = page.parts[page.parts.length - 1].params || (() => ({}));
|
const get_params = page.parts[page.parts.length - 1].params || (() => ({}));
|
||||||
const params = get_params(target.match);
|
const params = get_params(target.match);
|
||||||
|
|
||||||
@@ -154,28 +210,37 @@ function prepare_page(target: Target): Promise<{
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO skip unchanged segments
|
|
||||||
const props = { path, query };
|
const props = { path, query };
|
||||||
const data = { path, query, params, preloading: false, child: {} };
|
const data = {
|
||||||
|
path,
|
||||||
|
preloading: false,
|
||||||
|
child: Object.assign({}, root_props.child)
|
||||||
|
};
|
||||||
|
if (changed(query, root_props.query)) data.query = query;
|
||||||
|
if (changed(params, root_props.params)) data.params = params;
|
||||||
|
|
||||||
let level = data.child;
|
let level = data.child;
|
||||||
for (let i = 0; i < page.parts.length; i += 1) {
|
for (let i = 0; i < page.parts.length; i += 1) {
|
||||||
const part = page.parts[i];
|
const part = page.parts[i];
|
||||||
const get_params = page.parts[page.parts.length - 1].params || (() => ({}));
|
const get_params = part.params || (() => ({}));
|
||||||
|
|
||||||
Object.assign(level, {
|
if (i < changed_from) {
|
||||||
// TODO segment
|
level.props.path = path;
|
||||||
props: Object.assign({}, props, {
|
level.props.query = query;
|
||||||
|
level.props.child = Object.assign({}, level.props.child);
|
||||||
|
} else {
|
||||||
|
level.segment = new_segments[i];
|
||||||
|
level.component = results[i].Component;
|
||||||
|
level.props = Object.assign({}, level.props, props, {
|
||||||
params: get_params(target.match),
|
params: get_params(target.match),
|
||||||
}, results[i].preloaded),
|
}, results[i].preloaded);
|
||||||
component: results[i].Component
|
|
||||||
});
|
|
||||||
if (i < results.length - 1) {
|
|
||||||
level.props.child = {};
|
level.props.child = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
level = level.props.child;
|
level = level.props.child;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { data };
|
return { data, changed_from };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,12 +265,12 @@ async function navigate(target: Target, id: number): Promise<any> {
|
|||||||
prefetching = null;
|
prefetching = null;
|
||||||
|
|
||||||
const token = current_token = {};
|
const token = current_token = {};
|
||||||
const { redirect, data } = await loaded;
|
const { redirect, data, changed_from } = await loaded;
|
||||||
|
|
||||||
if (redirect) {
|
if (redirect) {
|
||||||
await goto(redirect.location, { replaceState: true });
|
await goto(redirect.location, { replaceState: true });
|
||||||
} else {
|
} else {
|
||||||
render(data, scroll_history[id], token);
|
render(data, changed_from, scroll_history[id], token);
|
||||||
document.activeElement.blur();
|
document.activeElement.blur();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -271,7 +336,7 @@ function handle_popstate(event: PopStateEvent) {
|
|||||||
|
|
||||||
let prefetching: {
|
let prefetching: {
|
||||||
href: string;
|
href: string;
|
||||||
promise: Promise<{ redirect?: Redirect, data?: any }>;
|
promise: Promise<{ redirect?: Redirect, data?: any, changed_from?: number }>;
|
||||||
} = null;
|
} = null;
|
||||||
|
|
||||||
export function prefetch(href: string) {
|
export function prefetch(href: string) {
|
||||||
|
|||||||
20
test/app/routes/[x]/[y]/[z].html
Normal file
20
test/app/routes/[x]/[y]/[z].html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<span>z: {segment} {count}</span>
|
||||||
|
<a href="foo/bar/qux"></a>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import counts from '../_counts.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
preload() {
|
||||||
|
return {
|
||||||
|
count: counts.z += 1
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
oncreate() {
|
||||||
|
this.set({
|
||||||
|
segment: this.get().params.z
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
20
test/app/routes/[x]/[y]/index.html
Normal file
20
test/app/routes/[x]/[y]/index.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<span>y: {segment} {count}</span>
|
||||||
|
<svelte:component this={child.component} {...child.props}/>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import counts from '../_counts.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
preload() {
|
||||||
|
return {
|
||||||
|
count: counts.y += 1
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
oncreate() {
|
||||||
|
this.set({
|
||||||
|
segment: this.get().params.y
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
5
test/app/routes/[x]/_counts.js
Normal file
5
test/app/routes/[x]/_counts.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
x: process.browser ? 1 : 0,
|
||||||
|
y: process.browser ? 1 : 0,
|
||||||
|
z: process.browser ? 1 : 0
|
||||||
|
};
|
||||||
20
test/app/routes/[x]/index.html
Normal file
20
test/app/routes/[x]/index.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<span>x: {segment} {count}</span>
|
||||||
|
<svelte:component this={child.component} {...child.props}/>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import counts from './_counts.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
preload() {
|
||||||
|
return {
|
||||||
|
count: counts.x += 1
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
oncreate() {
|
||||||
|
this.set({
|
||||||
|
segment: this.get().params.x
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -632,6 +632,33 @@ function run({ mode, basepath = '' }) {
|
|||||||
assert.equal(html.indexOf('%sapper'), -1);
|
assert.equal(html.indexOf('%sapper'), -1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('only recreates components when necessary', () => {
|
||||||
|
return nightmare
|
||||||
|
.goto(`${base}/foo/bar/baz`)
|
||||||
|
.init()
|
||||||
|
.evaluate(() => document.querySelector('#sapper').textContent)
|
||||||
|
.then(text => {
|
||||||
|
assert.deepEqual(text.split('\n').filter(Boolean), [
|
||||||
|
'x: foo 1',
|
||||||
|
'y: bar 1',
|
||||||
|
'z: baz 1'
|
||||||
|
]);
|
||||||
|
|
||||||
|
return nightmare.click(`a`)
|
||||||
|
.then(() => wait(100))
|
||||||
|
.then(() => {
|
||||||
|
return nightmare.evaluate(() => document.querySelector('#sapper').textContent);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(text => {
|
||||||
|
assert.deepEqual(text.split('\n').filter(Boolean), [
|
||||||
|
'x: foo 1',
|
||||||
|
'y: bar 1',
|
||||||
|
'z: qux 2'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('headers', () => {
|
describe('headers', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user