前言
相信大家都遇到过这么个场景,就是某些script代码需要在业务代码执行之前执行
通俗来说,就是在框架 如 ReactDOM.render,或者 Vue.mount 之前执行一些script代码
解决方案
看到这里的你会觉得:这个问题很好解决,不就是把script标签放在header上吗
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script>这里写一些代码,比如设置html字体大小</script>
</head>
<body>
<script src="/src/main.ts"></script>
</body>
</html>
可是在typescript大行其道的时代,硬写js并不是一个稳健的选择。
在vite中,我们可以写typescript脚本,直接放在html中即可。因为vite会从index.html作为入口打包,所以html中的ts也会被构建打包。
很简单:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
+ <script src="/some-scripts/hello-world.ts"></script>
</head>
<body>
<script src="/src/main.ts"></script>
</body>
</html>
当vite解析到 /some-scripts/hello-world.ts
这个脚本时,会自然经过vite的处理,即使代码中包含了三方库,也会被vite通通处理
hello-world.ts
ts
console.log('hello world!')
console.log(import.meta.env, 'import.meta.env')
vite 运行起来看看打印结果:
而这些代码,是在我们的App被挂载之前执行的,这样做的意义是什么呢?
假设你的网页需要做rem适配,如果在 useEffect 中设置字体,在进入页面的一瞬间,网页字体是原始的 16px
,而在DOM挂载后,才设置html字体大小,导致网页短暂的变形(如果网页原始字体跟当前页面分辨率应该展示的字体不同的情况下)
打包
可以看到,执行顺序也是ok的
但是打包后,在没有手动拆包的情况下,代码都打到一起去了
一般来说,需要提前执行的代码,几乎不会有变动,所以我们最好是手动拆包,这样有利于缓存
ts
// https://vitejs.dev/config/
export default defineConfig(() => ({
define: {
custom_define: JSON.stringify('custom define!'),
hello_world: JSON.stringify({ hello: 'world' }),
},
build: {
rollupOptions: {
output: {
manualChunks: (id) => {
if (id.includes('some-scripts/hello-world.ts')) {
return 'hello-world'
}
},
},
},
},
}))
然后重新打包,看看结果:
然后 vite preview 看看效果(这是打包后的效果):
看到这里你可能会疑惑,index.html中明明是 index.js 在 hello-world.js 之前,打印顺序怎么还是hello-word先执行呢?
因为 index 中引入了 hello-world,而 link modulepreload 是告诉浏览器,预加载一个 JavaScript 模块,但并不执行它。
兼容性处理
正式构建时,我们通常需要做代码兼容,在vite中,我们使用 @vitejs/pliugin-leagcy 做传统浏览器兼容,关于这个vite插件,不了解的同学可以看我的另一篇文章
在这里,我们直接引入legacy插件,看看效果即可
ts
import legacy from '@vitejs/plugin-legacy'
import { defineConfig } from 'vite'
// https://vitejs.dev/config/
export default defineConfig(() => ({
define: {
custom_define: JSON.stringify('custom define!'),
hello_world: JSON.stringify({ hello: 'world' }),
},
build: {
rollupOptions: {
output: {
manualChunks: (id) => {
if (id.includes('some-scripts/hello-world.ts')) {
return 'hello-world'
}
},
},
},
},
// 使用legacy插件做传统浏览器兼容
plugins: [legacy()],
}))
打包后可以看到 hello-world 也打了一个legacy包,完全OK!
小总结
至此,我们对目前普遍的 spa vite项目做了一遍处理,那对于ssr项目,又该如何处理呢?
SSR服务端渲染处理
SSR项目目前主要有两种形式:
- 动态返回html字符串或stream流
- 根据html文件解析后返回
普遍来说,SSR框架都是第一种形式,比如Astro,Nuxt,
上文说的解决方案处理第二种情况,我们先抛开SSR框架,用原生的vite来做SSR,看看结果如何
我们先用模板命令生成一个vite SSR项目
bash
pnpm create vite-extra
这里我选择纯原生的,能最大程度简化教程的复杂度
生成好了之后,我们直接往index.html添加typescript文件,试试能否生效:
老套路了
html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ssr</title>
<script type="module" src="/some-scripts/hello-world.ts"></script>
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/src/entry-client.tsx"></script>
</body>
</html>
开发环境
启动!查看浏览器打印台,没问题!
生产环境
打包后预览再看看效果:
也没问题!
分包
同样的,我们也分包,做好缓存效果
ts
import { defineConfig } from 'vite'
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: (id) => {
if (id.includes('some-scripts/hello-world.ts')) {
return 'hello-world'
}
},
},
},
},
})
构建之后,可以看到分包成功了
兼容性处理
引入 @vitejs/plugin-legacy
插件,打包。看看结果:
ok,构建后运行也没问题
SSR框架
SSR框架通常是没有html文件的,都是动态返回html字符串或者string,这种情况下,我们无法从html入口下手
推荐使用 vite-plugin-public-typescript
使用方式:
ts
import { defineConfig } from 'vite'
import { injectScripts, publicTypescript } from 'vite-plugin-public-typescript'
// https://vitejs.dev/config/
export default defineConfig(() => ({
define: {
custom_define: JSON.stringify('custom define!'),
hello_world: JSON.stringify({ hello: 'world' }),
},
plugins: [
publicTypescript({
destination: 'file',
}),
],
}))
这个插件会默认读取根目录下的 public-typescript
文件目录,然后解析所有的typescript文件,并构建成js。
有两种方式引用ts文件
其一是在vite配置中
ts
import { defineConfig } from 'vite'
import { injectScripts, publicTypescript } from 'vite-plugin-public-typescript'
// https://vitejs.dev/config/
export default defineConfig(() => ({
define: {
custom_define: JSON.stringify('custom define!'),
hello_world: JSON.stringify({ hello: 'world' }),
},
plugins: [
publicTypescript(),
// 在这里通过 injectScrpts 把代码插入到最后的html中
injectScripts((manifest) => [
{
attrs: { src: manifest['hello-world'] },
injectTo: 'head',
},
]),
],
}))
其二直接引入manifest
ts
import { manifest } from 'vite-plugin-public-typescript/client'
console.log(manifest['hello-world'])
这个manifest里面包含了js对应的资源地址
总结
希望把某些script在页面挂载前执行,在大部分情况下,在 index.html 中直接写 script ts 就可以了,少部分ssr的情况下,可以使用 vite-plugin-public-typescript 插件辅助