콘텐츠 관리 시스템 만들기: nodePress

PHPz
풀어 주다: 2023-09-03 13:53:05
원래의
1431명이 탐색했습니다.
<p>Go를 사용하여 플랫 파일 시스템 콘텐츠 관리 시스템(CMS)을 성공적으로 만들었습니다. 다음 단계는 동일한 아이디어를 가지고 Node.js를 사용하여 웹 서버를 만드는 것입니다. 라이브러리를 로드하고, 서버를 생성하고, 서버를 실행하는 방법을 보여드리겠습니다. </p> <p>이 CMS는 첫 번째 튜토리얼 "CMS 구축: 구조 및 스타일"에서 소개된 사이트 데이터 구조를 사용합니다. 따라서 이 기본 구조를 다운로드하여 새 디렉토리에 설치하십시오. </p> <h2>노드 및 노드 라이브러리 가져오기</h2> <p>Mac에 Node.js를 설치하는 가장 쉬운 방법은 Homebrew를 사용하는 것입니다. Homebrew가 아직 설치되어 있지 않은 경우 Homebrew Revealed: The Ultimate Package Manager for OS X 튜토리얼에서 설치 방법을 보여줍니다. </p> <p>Homebrew를 사용하여 Node.js를 설치하려면 터미널에 다음 명령을 입력하세요. </p> 으아악 <p>완료되면 Node 및 npm 명령이 Mac에 완전히 설치됩니다. 다른 모든 플랫폼의 경우 Node.js 웹사이트의 지침을 따르세요. </p> <p>참고: 현재 많은 패키지 관리자가 Node.js 버전 0.10을 설치하고 있습니다. 이 튜토리얼에서는 버전 5.3 이상이 있다고 가정합니다. 다음을 입력하여 버전을 확인할 수 있습니다. </p> 으아악 <p><code class="inline">node</code> 命令运行 JavaScript 解释器。 <code class="inline">npm</code> 명령은 Node.js의 패키지 관리자이며 새 라이브러리를 설치하고, 새 프로젝트를 만들고, 프로젝트 스크립트를 실행하는 데 사용됩니다. Envato Tuts+에는 Node.js 및 NPM에 대한 훌륭한 튜토리얼과 강좌가 많이 있습니다. </p> <p>웹 서버의 라이브러리를 설치하려면 Terminal.app 또는 iTerm.app 프로그램에서 다음 명령을 실행해야 합니다. </p> 으아악 <p>Express는 웹 애플리케이션 개발 플랫폼입니다. Go의 goWeb 라이브러리와 유사합니다. 핸들바는 페이지 생성을 위한 템플릿 엔진입니다. Moment는 날짜 작업을 위한 라이브러리입니다. Marked는 JavaScript의 HTML 변환기로 훌륭한 Markdown을 제공합니다. Jade는 HTML을 쉽게 생성할 수 있게 해주는 HTML 속기 언어입니다. Morgan은 Apache 표준 로그 파일을 생성하는 Express용 미들웨어 라이브러리입니다. </p> <p>라이브러리를 설치하는 또 다른 방법은 이 튜토리얼의 소스 파일을 다운로드하는 것입니다. 다운로드하고 압축을 푼 후 기본 디렉터리에 다음을 입력하세요. </p> 으아악 <p>이렇게 하면 이 프로젝트를 만드는 데 필요한 모든 것이 설치됩니다. </p> <h2>nodePress.js</h2> <p>이제 서버 생성을 시작할 수 있습니다. 프로젝트의 최상위 디렉터리에서 nodePress.js라는 파일을 만들고 원하는 편집기에서 열고 다음 코드를 추가하기 시작합니다. 파일에 넣은 코드를 설명하겠습니다. </p> 으아악 <p>서버 코드는 서버를 생성하는 데 사용된 모든 라이브러리를 초기화하는 것부터 시작됩니다. URL이 포함된 주석이 없는 라이브러리는 내부 Node.js 라이브러리입니다. </p> 으아악 <p>다음으로 모든 전역 변수와 라이브러리 구성을 설정했습니다. 전역 변수를 사용하는 것은 최고의 소프트웨어 설계 방식은 아니지만 빠른 개발에 효과가 있고 도움이 됩니다. </p> <p><code class="inline">parts</code> 변수는 웹페이지의 모든 부분을 포함하는 해시 배열입니다. 각 페이지는 이 변수의 내용을 참조합니다. 이는 서버 디렉터리 상단에 있는 server.json 파일의 내용으로 시작됩니다. </p> <p>그런 다음 server.json 파일의 정보를 사용하여 이 사이트의 <code class="inline">styles</code> 和 <code class="inline">layouts</code> 디렉터리에 대한 전체 경로를 만들었습니다. </p> <p>그런 다음 세 개의 변수를 빈 값으로 설정합니다. <code class="inline">siteCSS</code>、<code class="inline">siteScripts</code> 和 <code class="inline">mainPage</code>。这些全局变量将包含所有 CSS、JavaScript 和主索引页内容。这三个项目是任何 Web 服务器上请求最多的项目。因此,将它们保留在内存中可以节省时间。如果 server.json 文件中的 <code class="inline">Cache</code> 변수가 false이면 요청이 있을 때마다 항목을 다시 읽습니다. </p> 으아악 <p>이 코드 블록은 Markdown에서 HTML을 생성하도록 Marked 라이브러리를 구성하는 데 사용됩니다. 대부분의 경우 테이블과 smartList 지원을 켭니다. </p> 으아악 <p><code class="inline">parts</code> 变量进一步加载 <code class="inline">styles</code> 和 <code class="inline">layout</code> 目录中的部分。 <code class="inline">site</code> 目录内的 <code class="inline">parts</code> 目录中的每个文件也被加载到 <code class="inline">parts</code> 변수는 <code class="inline">styles</code> 및 <code class="inline">layout</code> 디렉터리의 섹션을 추가로 로드합니다. <code class="inline">site</code> 디렉터리 내 </p> 디렉터리의 각 파일도 <p> 전역 변수에 로드됩니다. 확장자가 없는 파일 이름은 파일 내용을 저장하는 데 사용되는 이름입니다. 이러한 이름은 Handlebars 매크로에서 확장됩니다. <code class="inline">save</code>、<code class="inline">date</code> 和 <code class="inline">cdate</code> 으아악 </p>다음 코드 조각은 웹 서버에서 사용하기 위해 정의한 Handlebars 도우미를 정의합니다. <p>. 저장 도우미를 사용하면 페이지 내에서 변수를 생성할 수 있습니다. 이 버전은 매개변수의 이름과 값이 "|"로 구분된 goPress 버전을 지원합니다. 두 개의 매개변수를 사용하여 저장을 지정할 수도 있습니다. 예: </p> 으아악 🎜이렇게 하면 동일한 결과가 나타납니다. 나는 두 번째 접근 방식을 선호하지만 Go의 Handlebars 라이브러리는 여러 매개변수를 허용하지 않습니다. 🎜 <p><code class="inline">date</code> 和 <code class="inline">cdate</code> 帮助程序格式化当前日期 (<code class="inline">date</code>) 或给定日期 (<code class="inline">cdate</code>)根据 <strong>moment.js</strong> 库格式化规则。 <code class="inline">cdate</code> 帮助程序期望渲染的日期是第一个参数并且具有 ISO 8601 格式。</p> <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:javascript;toolbal:false;">// // Create and configure the server. // var nodePress = express(); // // Configure middleware. // nodePress.use(morgan('combined')) </pre><div class="contentsignin">로그인 후 복사</div></div> <p>现在,代码创建一个 Express 实例来配置实际的服务器引擎。 <code>nodePress.use()</code> 函数设置中间件软件。中间件是在每次调用服务器时提供服务的任何代码。在这里,我设置了 Morgan.js 库来创建正确的服务器日志输出。</p> <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:javascript;toolbal:false;">// // Define the routes. // nodePress.get('/', function(request, response) { setBasicHeader(response); if((parts["Cache"] == true) && (mainPage != null)) { response.send(mainPage); } else { mainPage = page("main"); response.send(mainPage); } }); nodePress.get('/favicon.ico', function(request, response) { var options = { root: parts['Sitebase'] + 'images/', dotfiles: 'deny', headers: { 'x-timestamp': Date.now(), 'x-sent': true } }; response.set("Content-Type", "image/ico"); setBasicHeader(response); response.sendFile('favicon.ico', options, function(err) { if (err) { console.log(err); response.status(err.status).end(); } else { console.log('Favicon was sent:', 'favicon.ico'); } }); }); nodePress.get('/stylesheets.css', function(request, response) { response.set("Content-Type", "text/css"); setBasicHeader(response); response.type("css"); if((parts["Cache"] == true) && (siteCSS != null)) { response.send(siteCSS); } else { siteCSS = fs.readFileSync(parts['Sitebase'] + 'css/final/final.css'); response.send(siteCSS); } }); nodePress.get('/scripts.js', function(request, response) { response.set("Content-Type", "text/javascript"); setBasicHeader(response); if((parts["Cache"] == true) && (siteScripts != null)) { response.send(siteScripts); } else { siteScripts = fs.readFileSync(parts['Sitebase'] + 'js/final/final.js', 'utf8'); response.send(siteScripts); } }); nodePress.get('/images/:image', function(request, response) { var options = { root: parts['Sitebase'] + 'images/', dotfiles: 'deny', headers: { 'x-timestamp': Date.now(), 'x-sent': true } }; response.set("Content-Type", "image/" + path.extname(request.params.image).substr(1)); setBasicHeader(response); response.sendFile(request.params.image, options, function(err) { if (err) { console.log(err); response.status(err.status).end(); } else { console.log('Image was sent:', request.params.image); } }); }); nodePress.get('/posts/blogs/:blog', function(request, response) { setBasicHeader(response); response.send(post("blogs", request.params.blog, "index")); }); nodePress.get('/posts/blogs/:blog/:post', function(request, response) { setBasicHeader(response); response.send(post("blogs", request.params.blog, request.params.post)); }); nodePress.get('/posts/news/:news', function(request, response) { setBasicHeader(response); response.send(post("news", request.params.news, "index")); }); nodePress.get('/posts/news/:news/:post', function(request, response) { setBasicHeader(response); response.send(post("news", request.params.news, request.params.post)); }); nodePress.get('/:page', function(request, response) { setBasicHeader(response); response.send(page(request.params.page)); }); </pre><div class="contentsignin">로그인 후 복사</div></div> <p>这部分代码定义了实现 Web 服务器所需的所有路由。所有路由都运行 <code>setBasicHeader()</code> 函数来设置正确的标头值。所有针对页面类型的请求都会调用 <code>page()</code> 函数,而所有针对 post 类型页面的请求都会调用 <code>posts()</code> 函数。</p> <p><code class="inline">Content-Type</code> 的默认值为 HTML。因此,对于 CSS、JavaScript 和图像,<code class="inline">Content-Type</code> 显式设置为其适当的值。</p> <p>您还可以使用 <code>put</code>、<code>delete</code> 和 <code>post</code> REST 动词定义路由。这个简单的服务器仅使用 <code>get</code> 动词。</p> <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:javascript;toolbal:false;">// // Start the server. // var addressItems = parts['ServerAddress'].split(':'); var server = nodePress.listen(addressItems[2], function() { var host = server.address().address; var port = server.address().port; console.log('nodePress is listening at http://%s:%s', host, port); }); </pre><div class="contentsignin">로그인 후 복사</div></div> <p>在定义所使用的不同函数之前要做的最后一件事是启动服务器。 server.json 文件包含 DNS 名称(此处为 <code>localhost</code>)和服务器的端口。解析后,服务器的 <code>listen()</code> 函数使用端口号来启动服务器。服务器端口打开后,脚本会记录服务器的地址和端口。</p> <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:javascript;toolbal:false;">// // Function: setBasicHeader // // Description: This function will set the basic header information // needed. // // Inputs: // response The response object // function setBasicHeader(response) { response.append("Cache-Control", "max-age=2592000, cache"); response.append("Server", "nodePress - a CMS written in node from Custom Computer Tools: http://customct.com."); } </pre><div class="contentsignin">로그인 후 복사</div></div> <p>定义的第一个函数是 <code>setBasicHeader()</code> 函数。该函数设置响应头,告诉浏览器将页面缓存一个月。它还告诉浏览器该服务器是nodePress服务器。如果您需要任何其他标准标头值,您可以使用 <code>response.append()</code> 函数在此处添加它们。</p> <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:javascript;toolbal:false;">// // Function: page // // Description: This function processes a page request // // Inputs: // page The requested page // function page(page) { // // Process the given page using the standard layout. // return (processPage(parts["layout"], parts['Sitebase'] + "pages/" + page)); } </pre><div class="contentsignin">로그인 후 복사</div></div> <p><code>page()</code> 函数将页面的布局模板以及页面在服务器上的位置发送到 <code>processPage()</code> 函数。</p> <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:javascript;toolbal:false;">// // Function: post // // Description: This function processes a post request // // Inputs: // type The type of post. // cat The category of the post. // post The requested post // function post(type, cat, post) { // // Process the post given the type and the post name. // return (processPage(parts["layout"], parts['Sitebase'] + "posts/" + type + "/" + cat + "/" + post)); } </pre><div class="contentsignin">로그인 후 복사</div></div> <p><code>post()</code> 函数就像 <code>page()</code> 函数,不同之处在于帖子有更多项目来定义每个帖子。在这个系列的服务器中,一个post包含一个<code>type</code>、category,以及实际的<code>post</code>。类型为 <code>blogs</code> 或 <code>news</code>。类别是 <code>flatcms</code>。由于这些代表目录名称,因此您可以将它们设为您想要的任何名称。只需将命名与文件系统中的名称相匹配即可。</p> <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:javascript;toolbal:false;">// // Function: processPage // // Description: This function processes a page for the CMS. // // Inputs: // layout The layout to use for the page. // page Path to the page to render. // function processPage(layout, page) { // // Get the pages contents and add to the layout. // var context = {}; context = MergeRecursive(context, parts); context['content'] = figurePage(page); context['PageName'] = path.basename(page, path.extname(page)); // // Load page data. // if(fileExists(page + ".json")) { // // Load the page's data file and add it to the data structure. // context = MergeRecursive(context, JSON.parse(fs.readFileSync(page + '.json', 'utf8'))); } // // Process Handlebars codes. // var template = Handlebars.compile(layout); var html = template(context); // // Process all shortcodes. // html = processShortCodes(html); // // Run through Handlebars again. // template = Handlebars.compile(html); html = template(context); // // Return results. // return (html); } </pre><div class="contentsignin">로그인 후 복사</div></div> <p><code>processPage()</code> 函数获取要呈现的页面内容的布局和路径。该函数首先创建 <code>parts</code> 全局变量的本地副本,并添加“contents”主题标签以及调用 <code>figurePage()</code> 函数的结果。然后,它将 <code>PageName</code> 哈希值设置为页面名称。</p> <p>然后,该函数使用 Handlebars 将页面内容编译到布局模板。之后, <code>processShortCodes()</code> 函数将展开页面上定义的所有短代码。然后,Handlebars 模板引擎再次检查代码。然后浏览器接收结果。</p> <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:javascript;toolbal:false;">// // Function: processShortCodes // // Description: This function takes a string and // processes all of the shortcodes in // the string. // // Inputs: // content String to process // function processShortCodes(content) { // // Create the results variable. // var results = ""; // // Find the first match. // var scregFind = /\-\[([^\]]*)\]\-/i; var match = scregFind.exec(content); if (match != null) { results += content.substr(0,match.index); var scregNameArg = /(\w+)(.*)*/i; var parts = scregNameArg.exec(match[1]); if (parts != null) { // // Find the closing tag. // var scregClose = new RegExp("\\-\\[\\/" + parts[1] + "\\]\\-"); var left = content.substr(match.index + 4 + parts[1].length); var match2 = scregClose.exec(left); if (match2 != null) { // // Process the enclosed shortcode text. // var enclosed = processShortCodes(content.substr(match.index + 4 + parts[1].length, match2.index)); // // Figure out if there were any arguments. // var args = ""; if (parts.length == 2) { args = parts[2]; } // // Execute the shortcode. // results += shortcodes[parts[1]](args, enclosed); // // Process the rest of the code for shortcodes. // results += processShortCodes(left.substr(match2.index + 5 + parts[1].length)); } else { // // Invalid shortcode. Return full string. // results = content; } } else { // // Invalid shortcode. Return full string. // results = content; } } else { // // No shortcodes found. Return the string. // results = content; } return (results); } </pre><div class="contentsignin">로그인 후 복사</div></div> <p><code>processShortCodes()</code> 函数将网页内容作为字符串并搜索所有短代码。短代码是类似于 HTML 标签的代码块。一个例子是:</p> <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:html;toolbal:false;">-[box]- <p>This is inside a box</p> -[/box]- </pre><div class="contentsignin">로그인 후 복사</div></div> <p>此代码在 HTML 段落周围有一个 <code>box</code> 的简码。其中 HTML 使用 <code><</code> 和 </code>>></code>,短代码使用 <code>-[</code> 和 </code>>]-</code>。在名称后面,可以包含或不可以包含包含短代码参数的字符串。</p> <p><code>processShortCodes()</code> 函数查找短代码,获取其名称和参数,找到末尾以获取内容,处理短代码的内容,使用参数和内容执行短代码,将结果添加到完成中页面,并在页面的其余部分搜索下一个短代码。循环是通过递归调用函数来执行的。</p> <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:javascript;toolbal:false;">// // Define the shortcodes function array. // var shortcodes = { 'box': function(args, inside) { return ("<div class='box'>" + inside + "</div>"); }, 'Column1': function(args, inside) { return ("<div class='col1'>" + inside + "</div>"); }, 'Column2': function(args, inside) { return ("<div class='col2'>" + inside + "</div>"); }, 'Column1of3': function(args, inside) { return ("<div class='col1of3'>" + inside + "</div>"); }, 'Column2of3': function(args, inside) { return ("<div class='col2of3'>" + inside + "</div>"); }, 'Column3of3': function(args, inside) { return ("<div class='col3of3'>" + inside + "</div>"); }, 'php': function(args, inside) { return ("<div class='showcode'><pre type='syntaxhighlighter' class='brush: php'>" + inside + "</pre><div class="contentsignin">로그인 후 복사</div></div></div>"); }, 'js': function(args, inside) { return ("<div class='showcode'><div class="code" style="position:relative; padding:0px; margin:0px;"><pre type='syntaxhighlighter' class='brush: javascript'>" + inside + "</pre><div class="contentsignin">로그인 후 복사</div></div></div>"); }, 'html': function(args, inside) { return ("<div class='showcode'><div class="code" style="position:relative; padding:0px; margin:0px;"><pre type='syntaxhighlighter' class='brush: html'>" + inside + "</pre><div class="contentsignin">로그인 후 복사</div></div></div>"); }, 'css': function(args, inside) { return ("<div class='showcode'><div class="code" style="position:relative; padding:0px; margin:0px;"><pre type='syntaxhighlighter' class='brush: css'>" + inside + "</pre><div class="contentsignin">로그인 후 복사</div></div></div>"); } }; </pre> <p>下一节定义 <code>shortcodes</code> json 结构,该结构定义与其函数关联的短代码的名称。所有短代码函数都接受两个参数:<code>args</code> 和 <code>inside</code>。 <code>args</code> 是名称和空格之后、标签结束之前的所有内容。 <code>inside</code> 是开始和结束短代码标记包含的所有内容。这些功能是基本功能,但您可以创建一个短代码来执行您能在 JavaScript 中想到的任何功能。</p> <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:javascript;toolbal:false;">// // Function: figurePage // // Description: This function figures the page type // and loads the contents appropriately // returning the HTML contents for the page. // // Inputs: // page The page to load contents. // function figurePage(page) { var result = ""; if (fileExists(page + ".html")) { // // It's an HTML file. Read it in and send it on. // result = fs.readFileSync(page + ".html"); } else if (fileExists(page + ".amber")) { // // It's a jade file. Convert to HTML and send it on. I // am still using the amber extension for compatibility // to goPress. // var jadeFun = jade.compileFile(page + ".amber", {}); // Render the function var result = jadeFun({}); } else if (fileExists(page + ".md")) { // // It's a markdown file. Convert to HTML and send // it on. // result = marked(fs.readFileSync(page + ".md").toString()); // // This undo marked's URI encoding of quote marks. // result = result.replace(/\&quot\;/g,"\""); } return (result); } </pre><div class="contentsignin">로그인 후 복사</div></div> <p><code>figurePage()</code> 函数接收服务器上页面的完整路径。然后,此函数根据扩展名测试它是否为 HTML、Markdown 或 Jade 页面。我仍然在 Jade 中使用 .amber,因为那是我在 goPress 服务器上使用的库。所有 Markdown 和 Jade 内容都会先转换为 HTML,然后再传递给调用例程。由于 Markdown 处理器将所有引号翻译为 <code>"</code>,因此我在传回之前将它们翻译回来。</p> <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:javascript;toolbal:false;">// // Function: fileExists // // Description: This function returns a boolean true if // the file exists. Otherwise, false. // // Inputs: // filePath Path to a file in a string. // function fileExists(filePath) { try { return fs.statSync(filePath).isFile(); } catch (err) { return false; } } </pre><div class="contentsignin">로그인 후 복사</div></div> <p><code>fileExists()</code> 函数是 <code>fs.exists()</code> 函数的替代品,该函数曾经是 Node.js 的 <code>fs</code> 库的一部分。它使用 <code>fs.statSync()</code> 函数来尝试获取文件的状态。如果发生错误,则会返回 <code>false</code>。否则,返回 <code>true</code>。</p> <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:javascript;toolbal:false;">// // Function: MergeRecursive // // Description: Recursively merge properties of two objects // // Inputs: // obj1 The first object to merge // obj2 The second object to merge // function MergeRecursive(obj1, obj2) { for (var p in obj2) { try { // Property in destination object set; update its value. if (obj2[p].constructor == Object) { obj1[p] = MergeRecursive(obj1[p], obj2[p]); } else { obj1[p] = obj2[p]; } } catch (e) { // Property in destination object not set; create it and set its value. obj1[p] = obj2[p]; } } return obj1; } </pre><div class="contentsignin">로그인 후 복사</div></div> <p>最后一个函数是 <code>MergeRecursive()</code> 函数。它将第二个传递对象复制到第一个传递对象中。在添加特定于页面的部分之前,我利用它将主 <code>parts</code> 全局变量复制到本地副本中。</p> <h3>本地运行</h3> <p>保存文件后,您可以使用以下命令运行服务器:</p> <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:shell;toolbal:false;">node nodePress.js </pre><div class="contentsignin">로그인 후 복사</div></div> <p>或者,您可以使用 package.json 文件中的 <code class="inline">npm</code> 脚本。您可以像这样运行 npm 脚本:</p> <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:shell;toolbal:false;">npm start </pre><div class="contentsignin">로그인 후 복사</div></div> <p>这将运行 package.json 文件内的 <code>start</code> 脚本。</p> <p><img src="https://img.php.cn/upload/article/000/000/164/169372039225694.jpg" alt="콘텐츠 관리 시스템 만들기: nodePress"></p> <p>将您的网络浏览器指向 <code>http://localhost:8080</code>,您将看到上面的页面。您可能已经注意到我在主页上添加了更多测试代码。对页面的所有更改都包含在本教程的下载中。它们大多只是一些小的调整,以更全面地测试功能并适应使用不同库的任何差异。最显着的区别是 Jade 库不使用 <code class="inline">$</code> 来命名变量,而 Amber 则使用。</p> <h2>结论</h2> <p>现在,您在 Go 和 Node.js 中拥有完全相同的平面文件系统 CMS。这只是您可以使用此平台构建的内容的表面。尝试并尝试新事物。这是创建您自己的网络服务器的最佳部分。</p>

위 내용은 콘텐츠 관리 시스템 만들기: nodePress의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

원천:php.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿