Flask 项目中使用 Svelte

如果想在新项目中同时使用 Flask 后端和 Svelte 前端,可以通过 flask-svelte 进行创建。而对于已有项目,在不重构 Flask 项目目录结构的前提下,可以按照本文的方法集成 Svelte 前端。

假设现有的 Flask 项目目录结构如下:

/home/user/my-project/
├── app/
│   ├── __init__.py
│   ├── models.py
│   ├── views.py
│   ├── templates/
│   │   └── index.html
│   └── static/
│       └── style.css
├── tests/
│   └── test_app.py
├── venv/
├── setup.py
└── MANIFEST.in

my-project 下创建一个名为 frontend 的 Svelte 前端项目:

shellnpx degit sveltejs/template frontend

其目录结构如下:

/home/user/my-project/frontend/
├── public/
│   ├── favicon.png
│   ├── global.css
│   └── index.html
├── scripts/
│   └── setupTypeScript.js
├── src/
│   ├── App.svelte
│   └── main.js
├── package.json
├── README.md
└── rollup.config.js

如果执行命令:

shellnode scripts/setupTypeScript.js

将项目转换为使用 TypeScript 语言,则目录结构如下:

/home/user/my-project/frontend/
├── public/
│   ├── favicon.png
│   ├── global.css
│   └── index.html
├── src/
│   ├── App.svelte
│   ├── global.d.ts
│   └── main.ts
├── package.json
├── README.md
├── rollup.config.js
├── svelte.config.js
└── tsconfig.json

my-project/frontend/public/ 下的文件复制到 my-project/static 下,得到的项目目录结构如下:

/home/user/my-project/
├── app/
│   ├── __init__.py
│   ├── models.py
│   ├── views.py
│   ├── templates/
│   │   └── index.html
│   └── static/
│       ├── favicon.png
│       ├── global.css
│       └── index.html
├── frontend/
│   ├── src/
│   │   ├── App.svelte
│   │   ├── global.d.ts
│   │   └── main.ts
│   ├── package.json
│   ├── README.md
│   ├── rollup.config.js
│   ├── svelte.config.js
│   └── tsconfig.json
├── tests/
│   └── test_app.py
├── venv/
├── setup.py
└── MANIFEST.in

Svelte 项目需要修改的文件有 tsconfig.jsonrollup.config.jspackage.json。修改内容见下面代码的高亮部分:

json{
  "extends": "@tsconfig/svelte/tsconfig.json",

  "include": ["src/**/*"],
  "exclude": ["node_modules/*", "__sapper__/*", "../static/*"]
}
tsconfig.json
javascriptimport { spawn } from 'child_process';
import svelte from 'rollup-plugin-svelte';
import commonjs from '@rollup/plugin-commonjs';
import terser from '@rollup/plugin-terser';
import resolve from '@rollup/plugin-node-resolve';
import livereload from 'rollup-plugin-livereload';
import css from 'rollup-plugin-css-only';
import sveltePreprocess from 'svelte-preprocess';
import typescript from '@rollup/plugin-typescript';

const production = !process.env.ROLLUP_WATCH;

function serve() {
    let server;

    function toExit() {
        if (server) server.kill(0);
    }

    return {
        writeBundle() {
            if (server) return;
            server = spawn('npm', ['run', 'start', '--', '--dev'], {
                stdio: ['ignore', 'inherit', 'inherit'],
                shell: true
            });

            process.on('SIGTERM', toExit);
            process.on('exit', toExit);
        }
    };
}

export default {
    input: 'src/main.ts',
    output: {
        sourcemap: true,
        format: 'iife',
        name: 'app',
        file: '../static/build/bundle.js'
    },
    plugins: [
        svelte({
            preprocess: sveltePreprocess({ sourceMap: !production }),
            compilerOptions: {
                // enable run-time checks when not in production
                dev: !production
            }
        }),
        // we'll extract any component CSS out into
        // a separate file - better for performance
        css({ output: 'bundle.css' }),

        // If you have external dependencies installed from
        // npm, you'll most likely need these plugins. In
        // some cases you'll need additional configuration -
        // consult the documentation for details:
        // https://github.com/rollup/plugins/tree/master/packages/commonjs
        resolve({
            browser: true,
            dedupe: ['svelte'],
            exportConditions: ['svelte']
        }),
        commonjs(),
        typescript({
            sourceMap: !production,
            inlineSources: !production
        }),

        // In dev mode, call `npm run start` once
        // the bundle has been generated
        !production && serve(),

        // Watch the `public` directory and refresh the
        // browser on changes when not in production
        !production && livereload('../static'),

        // If we're building for production (npm run build
        // instead of npm run dev), minify
        production && terser()
    ],
    watch: {
        clearScreen: false
    }
};
rollup.config.js
json{
  "name": "svelte-app",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "rollup -c",
    "dev": "rollup -c -w",
    "start": "sirv ../static --no-clear",
    "check": "svelte-check"
  },
  "devDependencies": {
    "@rollup/plugin-commonjs": "^24.0.0",
    "@rollup/plugin-node-resolve": "^15.0.0",
    "@rollup/plugin-terser": "^0.4.0",
    "rollup": "^3.15.0",
    "rollup-plugin-css-only": "^4.3.0",
    "rollup-plugin-livereload": "^2.0.0",
    "rollup-plugin-svelte": "^7.1.2",
    "svelte": "^3.55.0",
    "svelte-check": "^3.0.0",
    "svelte-preprocess": "^5.0.0",
    "@rollup/plugin-typescript": "^11.0.0",
    "typescript": "^4.9.0",
    "tslib": "^2.5.0",
    "@tsconfig/svelte": "^3.0.0"
  },
  "dependencies": {
    "sirv-cli": "^2.0.0"
  }
}
package.json

此外,由于 Flask 项目默认的静态资源路径 static_url_path/static,为了兼容 Svelte,需要添加如下代码:

pythonimport app

from flask import send_from_directory


@app.route('/<path:path>')
@app.route('/', defaults={'path': 'index.html'})
def static_file(path):
    return send_from_directory(app.static_folder, path)

也可以使用模板来渲染主页:

pythonimport app

from flask import send_from_directory, render_template


@app.route('/<path:path>')
def static_file(path):
    return send_from_directory(app.static_folder, path)


@app.route('/')
def index():
    return render_template('index.html')