如何在Next.js中实现类似Leetcode的在线代码编译器并解决localStorage报错
Next.js 接入在线编译器localStorage未定义问题解决方案
问题根因
Next.js默认会在服务端执行组件初始化逻辑,localStorage是浏览器端独有的全局API,服务端Node.js环境不存在该对象,你在组件constructor中直接读取localStorage的操作会在服务端渲染阶段触发localStorage is not defined错误。
可行解决方案
方案1:调整生命周期读取localStorage(通用方案,兼容所有Next.js版本)
- 移除constructor中直接读取
localStorage的逻辑,给state设置初始默认值 - 在
componentDidMount生命周期(仅会在客户端执行)中读取localStorage更新state - 原有的写
localStorage逻辑无需修改,所有事件回调都只会在客户端触发,不会出现异常
修改后的完整组件代码如下:
// Next.js 13+ App Router请在第一行加 'use client' 标记为客户端组件 'use client' import React, { Component } from "react"; import "./Compiler.css"; export default class Compiler extends Component { constructor(props) { super(props); this.state = { input: '', output: '', language_id: 2, user_input: '', isClient: false }; } componentDidMount() { // 仅客户端执行,读取localStorage更新状态 this.setState({ isClient: true, input: localStorage.getItem('input') || '', language_id: localStorage.getItem('language_Id') || 2 }) } input = (event) => { event.preventDefault(); this.setState({ input: event.target.value }); localStorage.setItem('input', event.target.value) }; userInput = (event) => { event.preventDefault(); this.setState({ user_input: event.target.value }); }; language = (event) => { event.preventDefault(); this.setState({ language_id: event.target.value }); localStorage.setItem('language_Id',event.target.value) }; submit = async (e) => { e.preventDefault(); let outputText = document.getElementById("output"); outputText.innerHTML = ""; outputText.innerHTML += "Creating Submission ...\n"; const response = await fetch( "https://judge0-ce.p.rapidapi.com/submissions", { method: "POST", headers: { "x-rapidapi-host": "judge0-ce.p.rapidapi.com", "x-rapidapi-key": "*****************", "content-type": "application/json", accept: "application/json", }, body: JSON.stringify({ source_code: this.state.input, stdin: this.state.user_input, language_id: this.state.language_id, }), } ); outputText.innerHTML += "Submission Created ...\n"; const jsonResponse = await response.json(); let jsonGetSolution = { status: { description: "Queue" }, stderr: null, compile_output: null, }; while ( jsonGetSolution.status.description !== "Accepted" && jsonGetSolution.stderr == null && jsonGetSolution.compile_output == null ) { outputText.innerHTML = `Creating Submission ... \nSubmission Created ...\nChecking Submission Status\nstatus : ${jsonGetSolution.status.description}`; if (jsonResponse.token) { let url = `https://judge0-ce.p.rapidapi.com/submissions/${jsonResponse.token}?base64_encoded=true`; const getSolution = await fetch(url, { method: "GET", headers: { "x-rapidapi-host": "judge0-ce.p.rapidapi.com", "x-rapidapi-key": "******************", "content-type": "application/json", }, }); jsonGetSolution = await getSolution.json(); } } if (jsonGetSolution.stdout) { const output = atob(jsonGetSolution.stdout); outputText.innerHTML = ""; outputText.innerHTML += `Results :\n${output}\nExecution Time : ${jsonGetSolution.time} Secs\nMemory used : ${jsonGetSolution.memory} bytes`; } else if (jsonGetSolution.stderr) { const error = atob(jsonGetSolution.stderr); outputText.innerHTML = ""; outputText.innerHTML += `\n Error :${error}`; } else { const compilation_error = atob(jsonGetSolution.compile_output); outputText.innerHTML = ""; outputText.innerHTML += `\n Error :${compilation_error}`; } }; render() { // 避免服务端和客户端渲染内容不一致,客户端初始化完成后再渲染组件内容 if (!this.state.isClient) return null; return ( <> <div className="row container-fluid"> <div className="col-6 ml-4 "> <label htmlFor="solution "> <span className="badge badge-info heading mt-2 "> <i className="fas fa-code fa-fw fa-lg"></i> Code Here </span> </label> <textarea required name="solution" id="source" onChange={this.input} className=" source" value={this.state.input} ></textarea> <button type="submit" className="btn btn-danger ml-2 mr-2 " onClick={this.submit} > <i className="fas fa-cog fa-fw"></i> Run </button> <label htmlFor="tags" className="mr-1"> <b className="heading">Language:</b> </label> <select value={this.state.language_id} onChange={this.language} id="tags" className="form-control form-inline mb-2 language" > <option value="54">C++</option> <option value="50">C</option> <option value="62">Java</option> <option value="71">Python</option> </select> </div> <div className="col-5"> <div> <span className="badge badge-info heading my-2 "> <i className="fas fa-exclamation fa-fw fa-md"></i> Output </span> <textarea id="output"></textarea> </div> </div> </div> <div className="mt-2 ml-5"> <span className="badge badge-primary heading my-2 "> <i className="fas fa-user fa-fw fa-md"></i> User Input </span> <br /> <textarea id="input" onChange={this.userInput}></textarea> </div> </> ); } }
方案2:禁用组件SSR(适合Pages Router)
如果不想修改原组件代码,可以在导入Compiler组件时使用Next.js动态导入能力关闭SSR,组件只会在客户端加载:
import dynamic from 'next/dynamic' const Compiler = dynamic(() => import('../components/Compiler'), { ssr: false })
额外优化建议
- 代码中直接操作DOM的逻辑可以改成用React state管理output内容,更符合React设计规范,避免DOM操作异常
- Judge0的API密钥不要硬编码在前端代码中,容易泄露被盗刷,建议封装到自己的后端接口代理请求
内容的提问来源于stack exchange,提问作者Farhan Ahmed
相关产品推荐
相关产品推荐

