Svelte 是如何用 RollUp 构建应用的?—— Svelte 模板解读

我心飞翔 分类:javascript

对 Svelte 框架有所耳闻的朋友可能都听说过:和 React 或 Vue 不同, Svelte 并不采用 Virtual DOM,而是在部署前将代码编译为原生的 DOM 操作和 vanilla js,从而在运行 Web 应用的时候不需要依赖框架本身的运行时,也不需要做 diff/patch 这样的操作,从而提升运行速度,并降低包的大小。(对 svelte 的简单解读可以看看尤大的知乎回答)

根据上面的这个简介,一个很自然的问题就是,我们该怎么进行这个所谓的编译呢?官方并没有直接给出回答,而是推荐我们使用官网上的 REPL,一个根据输入代码实时渲染页面的在线编辑器(类似一个绑定了 Svelte 的 CodePen)。在 REPL 页面上制作出预想的效果后,下载为 svelte-app.zip,解压后运行:

cd /path/to/svelte-app
npm install
npm run dev
 

就能在 localhost:5000 访问到 REPL 的结果了。

image-20210318111703532.png

Svelte REPL 的使用效果

REPL 隐藏了 Svelte 的底层机制,让用户只用专注于写 .svelte 文件就好,的确很方便。不过为了知其然也要知其所以然,今天就让我们看看从 REPL 下载下来的 Svelte 模板项目是什么样的。

Svelte 模板的文件夹结构

直接下载 REPL 的初始应用,也就是上面图片中的 Hello World,得到的文件夹有这样的结构:

svelte-app
├── .gitignore
├── README.md
├── package.json
├── rollup.config.js
├── public
│   ├── favicon.png
│   ├── global.css
│   └── index.html
├── scripts
|   └── setupTypeScript.js
└── src
    ├── App.svelte
    └── main.js
 

因为本文的重点不是 Svelte 的 TypeScript 用法,所以我们可以不考虑 scripts 文件夹,再删去几个和代码没什么关系的文件,模板就简化为了:

svelte-app
├── package.json
├── rollup.config.js
├── public
│   ├── favicon.png
│   ├── global.css
│   └── index.html
└── src
    ├── App.svelte
    └── main.js
 

熟悉 React 的朋友应该感觉这个结构很眼熟,和 create-react-app 生成的模板一样,有 srcpublic 两个子目录。

src

src 文件夹里面有 Svelte 的代码。其中 App.svelte 里面的内容和上面截图中的一致:

<script>
	let name = 'world';
</script>

<h1>Hello {name}!</h1>
 

main.js 则是把 App 连接到最终页面上的桥梁:

import App from './App.svelte';

var app = new App({
	target: document.body
});

export default app;
 

这里的 target: document.body 就是把 App 渲染为 body 的子节点。

public

public 文件夹则是包含了最终的 HTML 页面。其中的 favicon.png 是一个 svelte 图标,global.css 是默认的全局 CSS,我们主要来看一下 index.html

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset='utf-8'>
	<meta name='viewport' content='width=device-width,initial-scale=1'>
	<title>Svelte app</title>
	<link rel='icon' type='image/png' href='/favicon.png'>
	<link rel='stylesheet' href='/global.css'>

	<link rel='stylesheet' href='/build/bundle.css'>
	<script defer src='/build/bundle.js'></script>
</head>

<body>
</body>
</html>
 

有两个值得关注的点。首先 index.html 中的 body 是空的,上文提到的 src/main.js 会用框架填充内容。其次则是下面的两行:

	<link rel='stylesheet' href='/build/bundle.css'>
	<script defer src='/build/bundle.js'></script>
 

它们引用了目前还不存在的 bundle.cssbundle.js。这两个文件就是就是编译的结果了,可以猜测 Svelte 模板会把 src 中的代码编译进 public/build 文件夹中。

RollUp 是如何构建 Svelte 应用的

知道了编译的起始点(src) 和终点(public/build),那么编译器是如何完成这个转化的呢?根据启动项目的指令 npm run dev,让我们来看看对应的 package.json

{
  "name": "svelte-app",
  "version": "1.0.0",
  "scripts": {
    "build": "rollup -c",
    "dev": "rollup -c -w",
    "start": "sirv public"
  },
  ...
}
 

npm run dev 相当于 rollup -c -w,也就是说 Svelte 利用的是 RollUp 进行编译和打包的。给不了解的朋友们介绍一下,RollUp 是一个 JavaScript 打包器,类似 Webpack,只是 RollUp 专注于打包 JavaScript。rollup -c -w 会让 RollUp 执行打包操作,其中 -c 指使用项目中的 rollup.config.js 作为配置文件,-w 则表示监听待打包的文件们,一旦有改动,就会自动重新打包。

所以执行 Svelte 的编译,以及最后在本地托管静态网页的秘密都在 rollup.config.js 中了。我们先把整个文件列在这里(对默认的英文注释进行了翻译):

import svelte from 'rollup-plugin-svelte';
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import livereload from 'rollup-plugin-livereload';
import { terser } from 'rollup-plugin-terser';
import css from 'rollup-plugin-css-only';
const production = !process.env.ROLLUP_WATCH;
function serve() {
let server;
function toExit() {
if (server) server.kill(0);
}
return {
writeBundle() {
if (server) return;
server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
stdio: ['ignore', 'inherit', 'inherit'],
shell: true
});
process.on('SIGTERM', toExit);
process.on('exit', toExit);
}
};
}
export default {
input: 'src/main.js',
output: {
sourcemap: true,
format: 'iife',
name: 'app',
file: 'public/build/bundle.js'
},
plugins: [
svelte({
compilerOptions: {
// 在非生产环境中开启运行时检查
dev: !production
}
}),
// 将所有组件中的 CSS 提取进一个文件——提高性能
css({ output: 'bundle.css' }),
// 如果你有从 npm 安装的外部依赖,那么你一般都需要这些插件。
// 一些情况下你需要进行额外的配置——详情见文档:
// https://github.com/rollup/plugins/tree/master/packages/commonjs
resolve({
browser: true,
dedupe: ['svelte']
}),
commonjs(),
// 在非生产环境中,在打包生成后运行 `npm run start`
!production && serve(),
// 在非生产环境中,在 `public` 目录中有改动时刷新浏览器
!production && livereload('public'),
// 如果为生产环境进行构建(npm run build 而不是 npm run dev),
// minify
production && terser()
],
watch: {
clearScreen: false
}
};

rollup.config.js 通用结构

让我们先挑出 RollUp 配置的通用结构,也就是:

export default {
input: 'src/main.js',  // 输入文件为 src/main.js
output: {
sourcemap: true,
// 把生成的代码为 (function () { code })() 形式
// 详见:https://en.wikipedia.org/wiki/Immediately_invoked_function_expression
format: 'iife',
name: 'app',
// 输出至 public/build/bundle.js
file: 'public/build/bundle.js'
},
plugins: [
...
],
watch: {
// 在重新打包的时候不要清空终端
clearScreen: false
}
};

这部分代码印证了上面我们的猜测,也就是 RollUp 会把 src 中的文件打包至 public/build/bundle.js。不过这里只提到了 main.js.svelte 文件该怎么办呢?这就要取决于 Svelte 模板使用的 RollUp 插件了。

RollUp 插件

export default {
...
plugins: [
svelte({
compilerOptions: {
// 在非生产环境中开启运行时检查
dev: !production
}
}),
// 将所有组件中的 CSS 提取进一个文件——提高性能
css({ output: 'bundle.css' }),
// 如果你有从 npm 安装的外部依赖,那么你一般都需要这些插件。
// 一些情况下你需要进行额外的配置——详情见文档:
// https://github.com/rollup/plugins/tree/master/packages/commonjs
resolve({
browser: true,
dedupe: ['svelte']
}),
commonjs(),
// 在非生产环境中,在打包生成后运行 `npm run start`
!production && serve(),
// 在非生产环境中,在 `public` 目录中有改动时刷新浏览器
!production && livereload('public'),
// 如果为生产环境进行构建(npm run build 而不是 npm run dev),
// minify
production && terser()
],
...
};

插件会根据定义的顺序依次执行。这里我们可以把插件分为 3 组。

1. Svelte 编译插件

import svelte from 'rollup-plugin-svelte';
import css from 'rollup-plugin-css-only';
export default {
...
plugins: [
svelte({
compilerOptions: {
// 在非生产环境中开启运行时检查
dev: !production
}
}),
// 将所有组件中的 CSS 提取进一个文件——提高性能
css({ output: 'bundle.css' }),
...
],
...
};

rollup-plugin-svelte 会调用 Svelte 编译器编译,由于篇幅所限,我会在后续的文章中给大家详细拆解一下这个插件。目前大家只需要知道:

  • 在默认配置下,rollup-plugin-svelte 会编译所有的 .svelte 文件;
  • rollup-plugin-svelte 先后使用了 svelte 包中的 preprocesscompiler 函数。

rollup-plugin-css-only 则是一个和 Svelte 不直接相关的插件,但是因为 Svelte 编译器会单独处理每个组件的 CSS 片段,所以需要使用这个插件把 CSS 合并起来。

2. 通用插件

import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
export default {
...
plugins: [
...
// 如果你有从 npm 安装的外部依赖,那么你一般都需要这些插件。
// 一些情况下你需要进行额外的配置——详情见文档:
// https://github.com/rollup/plugins/tree/master/packages/commonjs
resolve({
browser: true,
dedupe: ['svelte']
}),
commonjs(),
...
],
...
};

第二组是 RollUp 的 2 个通用插件,resolve 是为了把依赖项打进包里,commonjs 则是为了把 CommonJS 模块转化为 ES2015 供 RollUp 处理。对于这两个插件的用例介绍,请见 RollUp 文档。

3. 调试环境与生产环境用插件

import livereload from 'rollup-plugin-livereload';
import { terser } from 'rollup-plugin-terser';
const production = !process.env.ROLLUP_WATCH;
export default {
...
plugins: [
...
// 在非生产环境中,在打包生成后运行 `npm run start`
!production && serve(),
// 在非生产环境中,在 `public` 目录中有改动时刷新浏览器
!production && livereload('public'),
// 如果为生产环境进行构建(npm run build 而不是 npm run dev),
// minify
production && terser()
],
...
};

在模板项目的配置文件中,通过查看是否启用了 RollUp 的监听功能(ROLLUP_WATCH)来判断当前是否处于生产环境。最后一组的 3 个插件则和是否处于生产环境息息相关。注释已经把 livereloadterser 的作用讲得很清楚了,就不再赘述。我们来看看 serve 这个插件。它是唯一一个在配置文件中编写的插件,也是负责托管静态网页的重要角色。它的源码如下:

function serve() {
let server;
function toExit() {
if (server) server.kill(0);
}
return {
writeBundle() {
if (server) return;
server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
stdio: ['ignore', 'inherit', 'inherit'],
shell: true
});
process.on('SIGTERM', toExit);
process.on('exit', toExit);
}
};
}

serve 使用了 RollUp 的一个钩子函数 writeBundle,这个函数会在打包完成的时候被调用。对于 serve 来说,就是在打包完成的时候启动(spawn)一个新进程,运行 npm run start。对应 package.json 的话就是运行:

sirv public

sirv 是一个用来托管静态文件的 cli 应用,Svelte 模板就是利用它来完成最后的预览工作的。

到这里我们就介绍完了 Svelte 模板了~ 到这里你是不是对 Svelte REPL 生成的模板应用多了一份了解呢?在后续的文章中,我会再进一步,聊一聊 rollup-plugin-svelte 里面都做了什么,敬请期待~ 如果喜欢本文的话,也别忘了点个赞哦!

作者:掘金-Svelte 是如何用 RollUp 构建应用的?—— Svelte 模板解读

回复

我来回复
  • 暂无回复内容