Skip to content
This repository has been archived by the owner on Jan 11, 2023. It is now read-only.

support SPA mode #932

Closed
wants to merge 34 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d100f65
add --spa option
dishuostec Oct 7, 2019
e453401
use basepath when build
dishuostec Oct 7, 2019
9b3ef0c
skip craw pages from server when use --spa option.
dishuostec Oct 7, 2019
ec818e5
build command support --basepath
dishuostec Oct 7, 2019
95632dd
fix: css is not defined
dishuostec Oct 11, 2019
3810a19
split option to '--ssr' and '--hashbang'
dishuostec Oct 15, 2019
e0bd733
fix can not correctly create route factory for regex slot and sapread…
dishuostec Oct 18, 2019
e3aecbe
add session object to initial data
dishuostec Oct 18, 2019
b02fa9b
auto scroll to hash while start when enable hashbang
dishuostec Oct 21, 2019
35d195b
wait for root preloaded
dishuostec Oct 28, 2019
b623a1c
add template variable
dishuostec Dec 3, 2019
3e375ad
fix legacy_assets data
dishuostec Dec 18, 2019
bac87f5
fix create index html before create service-worker.js
dishuostec Jan 1, 2020
36bca3b
support --template option
dishuostec Jan 1, 2020
c90c592
fix pathname is ""
dishuostec Feb 10, 2020
dee8470
fix protocol when behind reverse proxy
dishuostec Mar 16, 2020
a92c093
support set dev host
dishuostec Mar 16, 2020
b2852b4
Merge upstream/master into feature/spa-option
dishuostec Apr 10, 2020
08228ce
fix --template option is not used in dev mode
dishuostec Jun 6, 2020
e26c71e
Merge upstream/master into feature/spa-option
dishuostec Jul 4, 2020
529907c
fix route factory generator
dishuostec Aug 5, 2020
119e182
Merge upstream/master into feature/spa-option
dishuostec Aug 5, 2020
1b68922
fix client redirect 404
dishuostec Aug 5, 2020
c3bd070
add qualifier hash to name
dishuostec Aug 5, 2020
de88bbe
fix empty path
dishuostec Aug 5, 2020
90fa9cc
Merge upstream/master into feature/spa-option
dishuostec Aug 15, 2020
c8d7385
create route use template literal
dishuostec Aug 15, 2020
b7b4e30
fix lint
dishuostec Aug 15, 2020
88c0ecf
add param control whether enable service worker
dishuostec Aug 15, 2020
e0ad7f6
fix throw "TypeError: Class extends value [object Object] is not a co…
dishuostec Aug 19, 2020
d168d90
Merge upstream/master into feature/spa-option
dishuostec Aug 27, 2020
e4000f1
define build_info
dishuostec Aug 27, 2020
18be869
remove styles process
dishuostec Aug 27, 2020
dca7a72
support render CSP tag using process.env.SAPPER_CONTENT_SECURITY_POLICY
dishuostec Aug 27, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ export default [
dir: 'dist',
format: 'cjs',
sourcemap: true,
chunkFileNames: '[name].js'
chunkFileNames: '[name].js',
interop(id) {
if (id === 'events') {return 'esModule';}
return 'auto';
}
},
external,
plugins: [
Expand Down
86 changes: 67 additions & 19 deletions runtime/src/app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
Page
} from './types';
import goto from './goto';
import { extract_path } from "./utils/route_path";
import { page_store } from './stores';

declare const __SAPPER__;
Expand Down Expand Up @@ -96,18 +97,25 @@ export function extract_query(search: string) {
search.slice(1).split('&').forEach(searchParam => {
const [, key, value = ''] = /([^=]*)(?:=(.*))?/.exec(decodeURIComponent(searchParam.replace(/\+/g, ' ')));
if (typeof query[key] === 'string') query[key] = [<string>query[key]];
if (typeof query[key] === 'object') (query[key] as string[]).push(value);
else query[key] = value;
if (typeof query[key] === 'object') {
(query[key] as string[]).push(value);
} else {
query[key] = value;
}
});
}
return query;
}

export function select_target(url: URL): Target {
if (url.origin !== location.origin) return null;
if (!url.pathname.startsWith(initial_data.baseUrl)) return null;
if (initial_data.hashbang) {
if (url.pathname && url.pathname !== location.pathname) return null;
} else {
if (!url.pathname.startsWith(initial_data.baseUrl)) return null;
}

let path = url.pathname.slice(initial_data.baseUrl.length);
let path = extract_path(url);

if (path === '') {
path = '/';
Expand All @@ -131,6 +139,13 @@ export function select_target(url: URL): Target {
return { href: url.href, route, match, page };
}
}

if (initial_data.hashbang) {
const query: Query = extract_query(url.search);
const page = { host: location.host, path, query, params: {} };

return { href: url.href, route: null, match: null, page };
}
}

export function handle_error(url: URL) {
Expand Down Expand Up @@ -207,23 +222,40 @@ export async function navigate(dest: Target, id: number, noscroll?: boolean, has
if (document.activeElement && (document.activeElement instanceof HTMLElement)) document.activeElement.blur();

if (!noscroll) {
let scroll = scroll_history[id];
scroll_to_target(hash);
}
}

if (hash) {
// scroll is an element id (from a hash), we need to compute y.
const deep_linked = document.getElementById(hash.slice(1));
function getOffsetY() {
if (self.pageYOffset) {
return self.pageYOffset;
} // Firefox, Chrome, Opera, Safari.
if (document.documentElement && document.documentElement.scrollTop) {
return document.documentElement.scrollTop;
} // Internet Explorer 6 (standards mode).
if (document.body.scrollTop) {
return document.body.scrollTop;
} // Internet Explorer 6, 7 and 8.
return 0; // None of the above.
}

if (deep_linked) {
scroll = {
x: 0,
y: deep_linked.getBoundingClientRect().top + scrollY
};
}
}
export function scroll_to_target(hash: string) {
let scroll = scroll_history[cid];

scroll_history[cid] = scroll;
if (scroll) scrollTo(scroll.x, scroll.y);
if (hash) {
// scroll is an element id (from a hash), we need to compute y.
const deep_linked = document.getElementById(hash.slice(1));

if (deep_linked) {
scroll = {
x: 0,
y: deep_linked.getBoundingClientRect().top + getOffsetY()
};
}
}

scroll_history[cid] = scroll;
if (scroll) scrollTo(scroll.x, scroll.y);
}

async function render(branch: any[], props: any, page: Page) {
Expand Down Expand Up @@ -281,6 +313,16 @@ export async function hydrate_target(dest: Target): Promise<HydratedTarget> {

const props = { error: null, status: 200, segments: [segments[0]] };

if (route === null && initial_data.hashbang) {
if (!page.path) {
redirect = { statusCode: 302, location: '#!/' };
return { redirect, props, branch: [] };
}
props.error = { message: 'Page not found.' };
props.status = 404;
return { redirect, props, branch: [] };
}

const preload_context = {
fetch: (url: string, opts?: any) => fetch(url, opts),
redirect: (statusCode: number, location: string) => {
Expand All @@ -297,7 +339,7 @@ export async function hydrate_target(dest: Target): Promise<HydratedTarget> {

if (!root_preloaded) {
const root_preload = root_comp.preload || (() => {});
root_preloaded = initial_data.preloaded[0] || root_preload.call(preload_context, {
root_preloaded = initial_data.preloaded[0] || await root_preload.call(preload_context, {
host: page.host,
path: page.path,
query: page.query,
Expand Down Expand Up @@ -346,7 +388,13 @@ export async function hydrate_target(dest: Target): Promise<HydratedTarget> {
preloaded = initial_data.preloaded[i + 1];
}

return (props[`level${j}`] = { component, props: preloaded, segment, match, part: part.i });
return (props[`level${j}`] = {
component,
props: preloaded,
segment,
match,
part: part.i
});
}));
} catch (error) {
props.error = error;
Expand Down
38 changes: 33 additions & 5 deletions runtime/src/app/start/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
navigate,
scroll_history,
scroll_state,
scroll_to_target,
select_target,
handle_error,
set_target,
Expand All @@ -13,6 +14,9 @@ import {
set_cid
} from '../app';
import prefetch from '../prefetch/index';
import { extract_hash, extract_path, location_not_include_hash } from "../utils/route_path";
import { debug, init } from "svelte/internal";
import goto from "../goto";

export default function start(opts: {
target: Node
Expand All @@ -38,13 +42,16 @@ export default function start(opts: {

addEventListener('click', handle_click);
addEventListener('popstate', handle_popstate);
if (initial_data.hashbang) {
addEventListener('hashchange', handle_hashchange);
}

// prefetch
addEventListener('touchstart', trigger_prefetch);
addEventListener('mousemove', handle_mousemove);

return Promise.resolve().then(() => {
const { hash, href } = location;
const { href } = location;

history.replaceState({ id: uid }, '', href);

Expand All @@ -53,7 +60,8 @@ export default function start(opts: {
if (initial_data.error) return handle_error(url);

const target = select_target(url);
if (target) return navigate(target, uid, true, hash);
const hash = extract_hash(url.hash);
if (target) return navigate(target, uid, !initial_data.hashbang, hash);
});
}

Expand Down Expand Up @@ -91,7 +99,7 @@ function handle_click(event: MouseEvent) {
const href = String(svg ? (<SVGAElement>a).href.baseVal : a.href);

if (href === location.href) {
if (!location.hash) event.preventDefault();
if (location_not_include_hash()) event.preventDefault();
return;
}

Expand All @@ -106,17 +114,37 @@ function handle_click(event: MouseEvent) {
const url = new URL(href);

// Don't handle hash changes
if (url.pathname === location.pathname && url.search === location.search) return;
if (extract_path(url) === extract_path(location) && url.search === location.search) {
return;
}

const target = select_target(url);
if (target) {
const noscroll = a.hasAttribute('sapper:noscroll');
navigate(target, null, noscroll, url.hash);
navigate(target, null, noscroll, extract_hash(url.hash));
event.preventDefault();
history.pushState({ id: cid }, '', url.href);
}
}

function handle_hashchange(event: HashChangeEvent) {
const from = new URL(event.oldURL);
const to = new URL(event.newURL);
const hash = extract_hash(to.hash);

if (extract_path(from) === extract_path(to)) {
// same page
scroll_to_target(hash);
return;
}

const target = select_target(to);

if (target) {
navigate(target, null, true, hash);
}
}

function which(event: MouseEvent) {
return event.which === null ? event.button : event.which;
}
Expand Down
31 changes: 31 additions & 0 deletions runtime/src/app/utils/route_path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { initial_data } from "../app";

export function hash_is_route(hash: string) {
return hash.startsWith('#!/');
}

export function extract_hash(hash: string) {
if (initial_data.hashbang) {
if (hash_is_route(hash) && hash.includes('#', 3)) {
return hash.slice(hash.indexOf('#', 3));
} else {
return '';
}
}

return hash;
}

export function extract_path(url: URL | Location) {
if (initial_data.hashbang) {
return hash_is_route(url.hash)
? url.hash.slice(2).replace(/#.*/, '')
: '';
}

return url.pathname.slice(initial_data.baseUrl.length);
}

export function location_not_include_hash() {
return !extract_hash(location.hash);
}
1 change: 1 addition & 0 deletions runtime/src/internal/manifest-server.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const src_dir: string;
export const build_dir: string;
export const dev: boolean;
export const manifest: Manifest;
export const template: string;

export interface SSRComponentModule {
default: SSRComponent;
Expand Down
23 changes: 14 additions & 9 deletions runtime/src/server/middleware/get_page_handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import devalue from 'devalue';
import fetch from 'node-fetch';
import URL from 'url';
import { sourcemap_stacktrace } from './sourcemap_stacktrace';
import { Manifest, ManifestPage, Req, Res, build_dir, dev, src_dir } from '@sapper/internal/manifest-server';
import { Manifest, ManifestPage, Req, Res, build_dir, dev, src_dir, template as template_file } from '@sapper/internal/manifest-server';
import App from '@sapper/internal/App.svelte';

export function get_page_handler(
Expand All @@ -18,8 +18,8 @@ export function get_page_handler(
: (assets => () => assets)(JSON.parse(fs.readFileSync(path.join(build_dir, 'build.json'), 'utf-8')));

const template = dev
? () => read_template(src_dir)
: (str => () => str)(read_template(build_dir));
? () => read_template(src_dir, template_file)
: (str => () => str)(read_template(build_dir, template_file));

const has_service_worker = fs.existsSync(path.join(build_dir, 'service-worker.js'));

Expand All @@ -34,7 +34,12 @@ export function get_page_handler(
res.end(`<pre>${message}</pre>`);
}

function handle_error(req: Req, res: Res, statusCode: number, error: Error | string) {
function handle_error(
req: Req,
res: Res,
statusCode: number,
error: Error | string
) {
handle_page({
pattern: null,
parts: [
Expand Down Expand Up @@ -369,8 +374,8 @@ export function get_page_handler(
};
}

function read_template(dir = build_dir) {
return fs.readFileSync(`${dir}/template.html`, 'utf-8');
function read_template(dir = build_dir, file: string = 'template.html') {
return fs.readFileSync(`${dir}/${file}`, 'utf-8');
}

function try_serialize(data: any, fail?: (err) => void) {
Expand Down Expand Up @@ -398,11 +403,11 @@ function serialize_error(error: Error | { message: string }) {

function escape_html(html: string) {
const chars: Record<string, string> = {
'"' : 'quot',
'"': 'quot',
"'": '#39',
'&': 'amp',
'<' : 'lt',
'>' : 'gt'
'<': 'lt',
'>': 'gt'
};

return html.replace(/["'&<>]/g, c => `&${chars[c]};`);
Expand Down
Loading