Svelte

Sveltekit1.0后的使用感受

月盾

之前提到过关于svelte框架——sapper和sveltekit的发展,已经是1年前的事了,其中提到过svelte的框架sapper和sveltekit,sapper已经明确不再更新了,官方推荐的是sveltekit。然后经过两年的迭代更新,于2022年12月终于推出了1.0版本。可以看看官方博客对1.0的介绍Announcing SvelteKit 1.0

然后谈谈个人感受。

优点: sveltekit集成的还不错,开发体验可以,使用官方提供的脚手架创建的项目就可以直接使用,不需要做任何配置。热更新,响应速度快,支持typescript等等,该有的都有了。

缺点: 又是一个全新轮子,这也是整个前端的通病,除了js是通用的,其他的都能给你整出花来,sveltekit在1.0版本之内已经有破坏性的更新,一年前创建的新项目,一年后基本不能用了。

至于要不要使用,那就看个人情况了,如果你厌烦了其他框架,倒是可以尝尝鲜,如果你想以此来做长期项目的话,个人就不推荐了,毕竟太新,而且向下兼容又做的不好,隔三差五一个破坏性更新,这种折腾劲恐怕没几个人受得了。而且对自己的技术也不能积累,长此以往并没有好处。

sveltekit官方文档

使用sveltekit开发一个服务端渲染(SSR)项目

月盾

上篇简单介绍了sapper和sveltekit的发展,目前sveltekit还只是Beta版本,有很多不确定因素存在,有可能会有大的变更,所以还不推荐在生产环境中使用,不过在个人项目和小项目中可以大胆尝试。

今天我们就正式使用sveltekit开发一个web项目。

第一步:创建项目

mkdir my-app

cd my-app

npm init svelte@next

npm install

npm run dev

这样就可以创建一个简单的项目了,不过和我们真实需求还有些差距,既然是使用sveltekit,那么最重要的原因是其支持服务端渲染了。这就需要从服务端获取数据,接下来就实现这样的需求。

第二步:路由

和sapper一样,sveltekit也是基于文件系统的的路由器,这就需要我们来合理的组织目录结构。路由的核心目录是src/routes,当然,这个也是可配置的,按照自己的需求修改svelte.config.cjs,参考文档:https://kit.svelte.dev/docs#configuration

我们以一个博客系统为例,在scr/routes下创建blog目录,光有目录还不行,如果想要访问 /blog 路由,还需要创建index.svelte文件,内容如下:

<script context="module">
    /**
	 * @type {import('@sveltejs/kit').Load}
	 */
	export async function load({ page, fetch, session, context }) {
		return fetch(`blog.json`)// index.json.js = blog.json或blog/blog.json
			.then((r) => r.json())
			.then((posts) => {
				console.log(posts);
				return {
					props: {
						posts
					}
				};
			});
	}
</script>

<script>
	export let posts;
</script>

<svelte:head>
	<title>Blog</title>
</svelte:head>

<h1>Recent posts</h1>

<ul>
	{#each posts as post}
		<!-- we're using the non-standard `rel=prefetch` attribute to
				tell Sapper to load the data for the page as soon as
				the user hovers over the link or taps it, instead of
				waiting for the 'click' event -->
		<li><a rel="prefetch" href="blog/{post.slug}">{post.title}</a></li>
	{/each}
</ul>

<style lang="less">
	ul {
		margin: 0 0 1em 0;
		line-height: 1.5;
	}
</style>

index.svelte中load函数是一个关键函数,它接收四个参数:

关于svelte框架——sapper和sveltekit的发展

月盾

虽然您可能现在还没有听说过svelte,但是其实svelte的发展速度超过了你的想象。

本文主要讲的是关于sapper和sveltekit这两款框架的发展。

svelte作者里奇·哈里斯(Rich Harris)在2020年10月的svelte峰会上表示:sapper永远不会发布1.0版本。

也就是说sapper不会发布正式版,一直处于非稳定版本。也可能放弃更新。

主要原因是sapper多年来代码库变得凌乱,但更主要的原因是最近网络发生了很大变化。

而作者放弃sapper后的另一种选择是开发SvelteKit

SapperSvelteKit都是svelte的开发框架,类似于vue的nuxt框架。

sveltekit包含的功能有:

  • 服务端渲染(SSR)

  • 路由

  • typescript支持

  • less, scss支持

  • serverless

  • vite打包

可以看到,sveltekit几乎包含了所有我们想要的功能,既能高效开发,又有高性能。

创建sveltekit的方法:


mkdir my-app

cd my-app

npm init svelte@next

npm install

npm run dev

需要注意,您的nodejs版本需要更新到v12以上,否则可能出现以下错误:


$ npm run dev -- --open


> sveltekit-app@0.0.1 dev D:\workspace\sveltekit-app

> svelte-kit dev "--open"

D:\workspace\sveltekit-app\node_modules\@sveltejs\kit\svelte-kit.js:2

import './dist/cli.js';

	SyntaxError: Unexpected string
	at Module._compile (internal/modules/cjs/loader.js:723:23)
	at Object.Module._extensions..js (internal/modules/cjs/loader.js:789:10)
	at Module.load (internal/modules/cjs/loader.js:653:32)
	at tryModuleLoad (internal/modules/cjs/loader.js:593:12)
	at Function.Module._load (internal/modules/cjs/loader.js:585:3)
	at Function.Module.runMain (internal/modules/cjs/loader.js:831:12)
	at startup (internal/bootstrap/node.js:283:19)
	at bootstrapNodeJSCore (internal/bootstrap/node.js:622:3)
									   
	npm ERR! code ELIFECYCLE
	npm ERR! errno 1
	npm ERR! sveltekit-app@0.0.1 dev: `svelte-kit dev "--open"`
	npm ERR! Exit status 1
	npm ERR!
	npm ERR! Failed at the sveltekit-app@0.0.1 dev script.
	npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
   

使用sveltekit开发一个服务端渲染(SSR)项目

svelte history路由刷新后404

月盾

npms.io上搜索到svelte的route包其实也不算少,使用比较广泛的svelte-spa-router路由包却不支持history模式。有些支持history模式的使用上也不是很方便,试用过五六个支持history的路由后最终@spaceavocado/svelte-router算是满足了要求。

  • 使用简单
  • 功能丰富
  • 支持history和hash 我也是够难伺候的。

在试用了多个支持history的路由过程中,都遇到了一个问题:切换路由后刷新404。这也算是单页应用的通病了。不过像vue这种是在部署到服务器上刷新404,而svelte却在开发过程中也出现了,又想放弃了… 好在我也是试用过五六七八个路由的人了,中间不知道尝试过多少种方法来实现history路由。最后使用@spaceavocado/svelte-router包实现了history路由的时候我还是满心欢喜。

使用方法:

//App.svelte
<script>
	import RouterView from '@spaceavocado/svelte-router/component/view';
	import { routes } from './router.js'
</script>

<RouterView />
//router.js
import createRouter from '@spaceavocado/svelte-router';
import index from './index/index.svelte';
import a from './a/a.svelte';
import b from './b/b.svelte';

export const routes = createRouter({
    mode: "HISTORY",
    routes: [
        {
            path: '/',
            name: 'HOME',
            component: index,
        },
        {
            path: '/a',
            name: 'a',
            component: a,
        },
        {
            path: '/b',
            name: 'b',
            component: b,
        },
        {
            path: '*',
            component: index,
        },
    ],
});
// index.svelte
<script>
	import RouterLink from '@spaceavocado/svelte-router/component/link';
</script>

<main>
	<h1>Hello {name}!</h1>
	<RouterLink to="/a">a页面</RouterLink>
	<RouterLink to="/b">b页面</RouterLink>
</main>

可是一刷新就嗝屁了,依旧出现404.无意中就试了一下之前使用其他路由包使用的一种rollup配置方法。 package.json中的start命令: sirv public -s加了-s参数就好了。

基于sapper开发svelte项目配置本地代理

基于sapper开发svelte项目配置本地代理

月盾

sapper可能被放弃更新,如果您要继续使用svelte,可以考虑使用sveltekit

最近使用svelte开发一个项目,说实在的,开发过程中遇到不少问题。 每次遇到问题的时候都有种想放弃的冲动,这生态也太差了,查个啥啥问题都查不到,找个啥啥插件也没有。 不过,到最后,遇到的问题又都解决了。 这不,今天又遇到了本地代理的设置问题。 在说遇到的问题之前先介绍一些项目架构。该项目是基于sapper框架开发,这是一个使用svelte开发的框架,具备以下特点:

  • 服务端渲染
  • 路由
  • 代码分割
  • 默认支持渐进式web应用(PWA)
  • 预取路由
  • 单独的头标签(meta,link等)
  • 作为静态站点弹出
  • Cypress测试(免费,简单,端到端的测试)

可以看到,sapper基本是集合了目前前端开发所有需求,双向数据绑定,渐进式开发,SSR,静态化,高性能。

遇到的新问题:对于前端项目,调用接口时容易遇到跨域问题,一般是使用本地代理解决。自然也想这样来做,可是sapper没有vue项目那样的生态,用的打包工具也不是webpack,而是rollup。 那么就使用sapper自带的服务端来做代理好了。sapper的服务端用的是polka,而不是express,不过没关系,其实可以相互替换。 最关键的是增加了http-proxy-middleware中间件,却对中间件位置很敏感,不是想随便在哪添加一下就行。需要添加在第一个中间件位置,否则就会优先使用静态服务中间件,导致接口找不到。 完整代码:

import sirv from 'sirv';
import polka from 'polka';
import compression from 'compression';
import * as sapper from '@sapper/server';
import { createProxyMiddleware } from 'http-proxy-middleware';

const { PORT, NODE_ENV } = process.env;
const dev = NODE_ENV === 'development';

polka()
	// 需要放在最前面,否则接口404,secure参数解决调用https问题
	.use('/api', createProxyMiddleware({ target: 'https://example.com', pathRewrite: { '^/api': '' }, secure: false, changeOrigin: true, }))
	.use(
		compression({ threshold: 0 }),
		sirv('static', { dev }),
		sapper.middleware()
	)
	.listen(PORT, err => {
		if (err) console.log('error', err);
	});

总结: 总体来说,使用svelte开发的确没有vue顺畅,但遇到的问题都有解决方案,对于一些非核心项目还是可以使用的,粗略的对比老项目,在性能上有50%的提升。如果你不想使用回最原始的开发方式来提升性能,那么svelte是个不错的备选方案。

svelte项目rollup配置px2rem

月盾

使用svelte开发项目时遇到需要将px转换成rem的需求,有试过postcss-px2rempostcss-pxtorem,等postcss插件,都没成成功,最后找到了postcss-units插件成功实现。 完整rollup配置文件如下: converts px to rem 该配置是sapper项目配置

import path from 'path';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import commonjs from '@rollup/plugin-commonjs';
import url from '@rollup/plugin-url';
import svelte from 'rollup-plugin-svelte';
import babel from '@rollup/plugin-babel';
import { terser } from 'rollup-plugin-terser';
import config from 'sapper/config/rollup.js';
import pkg from './package.json';
import sveltePreprocess from 'svelte-preprocess';
// import { less } from 'svelte-preprocess';
const postcssUnits = require('postcss-units');

const mode = process.env.NODE_ENV;
const dev = mode === 'development';
const legacy = !!process.env.SAPPER_LEGACY_BUILD;

const onwarn = (warning, onwarn) =>
	(warning.code === 'MISSING_EXPORT' && /'preload'/.test(warning.message)) ||
	(warning.code === 'CIRCULAR_DEPENDENCY' && /[/\]@sapper[/\]/.test(warning.message)) ||
	onwarn(warning);

export default {
	client: {
		input: config.client.input(),
		output: config.client.output(),
		plugins: [
			replace({
				'process.browser': true,
				'process.env.NODE_ENV': JSON.stringify(mode)
			}),
			svelte({
				dev,
				hydratable: true,
				emitCss: true,
				// preprocess: [
				// 	less(),
				// ]
				preprocess: sveltePreprocess({
					postcss: {
						plugins: [
							postcssUnits({
								size: 75
							}),
						]
					},
				}),
			}),
			url({
				sourceDir: path.resolve(__dirname, 'src/node_modules/images'),
				publicPath: '/client/'
			}),
			resolve({
				browser: true,
				dedupe: ['svelte']
			}),
			commonjs(),

			legacy && babel({
				extensions: ['.js', '.mjs', '.html', '.svelte'],
				babelHelpers: 'runtime',
				exclude: ['node_modules/@babel/**'],
				presets: [
					['@babel/preset-env', {
						targets: '> 0.25%, not dead'
					}]
				],
				plugins: [
					'@babel/plugin-syntax-dynamic-import',
					['@babel/plugin-transform-runtime', {
						useESModules: true
					}]
				]
			}),

			!dev && terser({
				module: true
			}),
		],

		preserveEntrySignatures: false,
		onwarn,
	},

	server: {
		input: config.server.input(),
		output: config.server.output(),
		plugins: [
			replace({
				'process.browser': false,
				'process.env.NODE_ENV': JSON.stringify(mode)
			}),
			svelte({
				generate: 'ssr',
				hydratable: true,
				dev,
				// preprocess: [
				// 	less(),
				// ]
				preprocess: sveltePreprocess({
					postcss: {
						plugins: [
							postcssUnits({
								size: 75
							}),
						]
					},
				}),
			}),
			url({
				sourceDir: path.resolve(__dirname, 'src/node_modules/images'),
				publicPath: '/client/',
				emitFiles: false // already emitted by client build
			}),
			resolve({
				dedupe: ['svelte']
			}),
			commonjs(),
		],
		external: Object.keys(pkg.dependencies).concat(require('module').builtinModules),

		preserveEntrySignatures: 'strict',
		onwarn,
	},

	serviceworker: {
		input: config.serviceworker.input(),
		output: config.serviceworker.output(),
		plugins: [
			resolve(),
			replace({
				'process.browser': true,
				'process.env.NODE_ENV': JSON.stringify(mode)
			}),
			commonjs(),
			!dev && terser()
		],

		preserveEntrySignatures: false,
		onwarn,
	}
};

配置完成后同时需要将css进行修改:

svelte函数传参

月盾

svelte给dom对象绑定事件和vue框架类似。 定义函数:

function handler(index){
	alert("hello", index);
}

绑定事件:

<button on:click={handler}>点击</button>

但是带参函数的使用就略有不同了,函数handler的参数index需要传入的时候,不能直接这样使用<button on:click={handler(123)}>点击</button>,这样的写法会在页面打开时直接执行,而不是在点击按钮的时候执行。

这是初学svelte的时候比较郁闷的事,官方文档中也没有明显的文档说明如何传参。 正确的传参方式是这样的: <button on:click={() => handler(123)}>点击</button>on:click的内容改写为匿名函数,在函数中调用。

Svelte3路由

月盾

svelte目前没有提供官网路由组件,不过可以在社区中找到。本文介绍的是svelte-spa-router的使用方法。

npm i svelte-spa-router 参考以下目录结构创建文件(不是必须)

router.js:

import index from './index/index.svelte';
import a from './a/a.svelte';
import b from './b/b.svelte';

export const routes = {
    '/': index,
    '/a': a,
    '/b': b
}

动态导入组件和代码分割

import { wrap } from 'svelte-spa-router/wrap'
import index from './index/index.svelte';

export const routes = {
    '/': index,
    '/a': wrap({
        asyncComponent: () => import('./a/a.svelte')
    }),
    // '/b': b
	// 动态加载
    '/b': wrap({
        asyncComponent: () => import('./b/b.svelte')
    }),
}

动态导入组件的优点是组件不会一起打包,而是单独的组件文件,在打开对应的页面时才会请求,可以有效减少包文件大小。 App.svelte:

<script>
	import Router from 'svelte-spa-router'
	import { routes } from './router.js'
</script>

<Router {routes} />

a.svelte:

<script>
import { link } from "svelte-spa-router";
</script>

<a href="/a" use:link>a</a>
<a href="/b" use:link>b</a>

注意:svelte-spa-router是基于hash实现,作者认为这是静态单页应用的理想实现方式,history方式的路由需要增加服务端,这增加了复杂性。