You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 20:05:39