LitElement组件适配SSR环境(NextJS等)遇window未定义及渲染无效问题
在NextJS/VueJS SSR环境中运行LitElement组件的解决方案
问题描述
我正在开发可在NextJS、VueJS等服务端渲染(SSR)环境中运行的LitElement Web Component,不想采用仅客户端导入的粗糙方案。根据Lit官方文档这应该可行,但在NextJS里持续遇到window is not defined错误。
当前实现代码:
import { render } from '@lit-labs/ssr' import { TestComponent } from './components/test-component' const ssrResult = render(TestComponent) export default ssrResult
除了构建体积暴涨数百KB,@lit-labs/ssr的render函数还未产生预期效果。我不一定要实现真正的SSR,只要有可行的变通方案即可。
组件结构:
import { html, LitElement, TemplateResult } from 'lit' import { customElement, property, } from 'lit/decorators.js' @customElement('test-component') export class TestComponent extends LitElement { @property({ type: String }) public title = '' render(): TemplateResult | void { return html` <div class="content"> <h3>${this.title}</h3> </div> ` } }
Rollup配置:
import { babel } from '@rollup/plugin-babel' import commonjs from '@rollup/plugin-commonjs' import filesize from 'rollup-plugin-filesize' import nodePolyfills from 'rollup-plugin-polyfill-node' import { nodeResolve } from '@rollup/plugin-node-resolve' import terser from '@rollup/plugin-terser' import typescript from '@rollup/plugin-typescript' import pkg from './package.json' assert { type: 'json' } //Node hack import { fileURLToPath } from 'url' const __filename = fileURLToPath(import.meta.url) global['__filename'] = __filename const extensions = ['.js', '.jsx', '.ts', '.tsx', '.mjs'] export default { input: './src/index.ts', output: [ { file: pkg.main, format: 'umd', name: pkg.name }, { file: pkg.module, format: 'es' } ], plugins: [ nodePolyfills(), nodeResolve({ extensions, jsnext: true, module: true, }), commonjs(), typescript({ tsconfig: './tsconfig.json' }), babel({ extensions, exclude: ['./node_modules/**'], babelHelpers: 'bundled' }), filesize(), terser(), ], }
解决方案
1. 解决window is not defined错误:条件导入与环境适配
LitElement依赖浏览器环境的window对象,SSR阶段直接导入会触发报错,可通过两种方式规避:
- NextJS动态导入(禁用SSR):不需要真SSR时,用NextJS的
dynamic方法实现客户端懒加载,避免服务端执行组件代码:
import dynamic from 'next/dynamic' const TestComponent = dynamic(() => import('./components/test-component'), { ssr: false, loading: () => <div>加载中...</div> })
- 模拟浏览器全局对象(尝试真SSR):在NextJS的
_document.js或服务端入口提前模拟基础浏览器API:
if (typeof window === 'undefined') { global.window = {} global.document = { createElement: () => ({}) } global.navigator = { userAgent: 'node' } }
注:该方式仅适配简单组件,复杂组件可能仍存在环境兼容问题。
2. 正确使用@lit-labs/ssr
当前render调用方式错误,@lit-labs/ssr的render函数需要传入模板实例而非组件类,同时需处理渲染结果:
import { render } from '@lit-labs/ssr' import { html } from 'lit' import { TestComponent } from './components/test-component' // 服务端需注册组件 customElements.define('test-component', TestComponent) // 渲染带属性的组件模板 const ssrHtml = Array.from(render(html`<test-component title="SSR测试"></test-component>`)).join('') export default ssrHtml
同时移除Rollup配置中的nodePolyfills(),服务端构建不需要浏览器polyfill,可大幅减少体积并避免冲突。
3. 优化Rollup配置适配SSR
- 拆分环境配置:为SSR和客户端分别配置输出格式,SSR用
cjs格式,客户端保留es/umd:
const baseConfig = { input: './src/index.ts', plugins: [ nodeResolve({ extensions, module: true }), commonjs(), typescript({ tsconfig: './tsconfig.json' }), babel({ extensions, exclude: ['./node_modules/**'], babelHelpers: 'bundled' }), filesize(), terser(), ], } export default [ // 客户端配置 { ...baseConfig, output: [ { file: pkg.main, format: 'umd', name: pkg.name }, { file: pkg.module, format: 'es' } ], }, // SSR配置 { ...baseConfig, output: { file: pkg.ssr, format: 'cjs' }, } ]
在package.json中添加"ssr": "./dist/ssr.cjs"字段。
4. 替代方案:静态HTML预渲染
不需要实时SSR时,可预渲染组件静态HTML,服务端直接输出,客户端再做hydrate:
- 预渲染HTML(Node环境执行脚本):
import { render } from '@lit-labs/ssr' import { html } from 'lit' import { TestComponent } from './components/test-component' customElements.define('test-component', TestComponent) const staticHtml = Array.from(render(html`<test-component title="静态测试"></test-component>`)).join('') // 将staticHtml保存到文件或直接在NextJS页面中使用
- 客户端hydrate:
import { hydrate } from 'lit' import { TestComponent } from './components/test-component' export default function Page() { useEffect(() => { customElements.define('test-component', TestComponent) hydrate(html`<test-component title="静态测试"></test-component>`, document.getElementById('component-root')) }, []) return <div id="component-root"><div dangerouslySetInnerHTML={{ __html: staticHtml }}></div></div> }
内容的提问来源于stack exchange,提问作者Johan Martin Aarstein
相关产品推荐
相关产品推荐

