脚本不会从 nodejs 应用程序中的 ejs 文件运行

2022-01-22 00:00:00 node.js javascript html chart.js ejs

我正在尝试使用 nodejs、express、mysql 和 ejs 使网页显示图表,但我显然不了解 ejs/javascript 等的工作原理.我需要运行一个脚本来设置图表(来自 chart.js 模块),但它不输出任何类型的图表.

I'm trying to make a webpage display a chart using nodejs, express, mysql, and ejs, but I clearly don't understand something about how ejs / javascript etc. works. I need a script to run that sets up a chart (from chart.js module) yet it is not outputting any sort of chart whatsoever.

我尝试了什么:

  • 在脚本中放置 console.log() 消息以查看脚本中的代码是否存在问题.控制台没有输出,所以我确信脚本本身没有运行
  • 将段落标签放在脚本上方的同一文件中,显示正常情况下,显示文件仍在访问中,只是脚本无法正常工作

下面的脚本不会运行:

<canvas id="myChart" width="50%" height="100px"></canvas>

<script scr="map-access-data.js" type="text/javascript"></script>
<script id ="map-popularity" type="text/javascript">
    var Chart = require('chart');
    var ctx = document.getElementById("myChart").getContext("2d");
    var myChart = new Chart(ctx, {
        type: 'bar',
        data: {
            labels: getMapAccessNames(),
            datasets:[{
                label: "Map Selection Popularity",
                data: getMapAccessData(),
                backgroundColor: getMapColors(),
                borderColor: getMapColors(),
                borderWidth: 1
            }]
        },
        options: {
            scales: {
                yAxes: [{
                    ticks: {
                        beginAtZero:true
                    }
                }]
            }
        }
    });
</script>

在该文件的第一个脚本中引用的 map-access-data.js 文件:

map-access-data.js file referenced in first script of that file:

var mysql = require('mysql');

var connection = mysql.createConnection({
    host     : 'localhost',
    user     : 'admin',
    password : 'pass',
    database : 'db'
});

connection.connect();

function getNumMaps(){
    connection.query("SELECT NAME, COUNT(*) FROM map_access GROUP BY NAME ORDER BY NAME", function(err, rows){
        return rows.length();
    });
}

function getMapAccessData(){
    var arr = [];
    connection.query("SELECT NAME, COUNT(*) FROM map_access GROUP BY NAME ORDER BY NAME", function(err, rows){
        for(i in rows){
            arr.push(i.count);
        }
    });
}

function getMapAccessNames(){
    var arr = [];
    connection.query("SELECT NAME, COUNT(*) FROM map_access GROUP BY NAME ORDER BY NAME", function(err, rows){
        for(i in rows){
            arr.push(i.name);
        }
    });
    return arr;
}

function getMapColors(){
    var arr = [];
    for(var i = 0; i < getNumMaps(); i++){
        arr.push('rgba(255,99,132,1)');
    }
    return arr;

此代码呈现的实际文件:

actual file that this code is rendered in:

<!DOCTYPE html>
<html>
  <head>
    <title><%= title %></title>
    <link rel='stylesheet' href='/stylesheets/style.css' />
  </head>
  <body>
    <% include header.ejs %>
    <h1><%= title %></h1>
    <p>See how people are using our app <br/></p>
    <% include map-access-chart %>
  </body>
</html>

推荐答案

这里有一堆误解: HTML 文件将提供给你的客户端,它会看到 <script> 标签,从 Web 服务器请求它们,然后执行代码.您不能期望 Web 浏览器在您的服务器上运行 SQL 查询),因此显然这不是您想要执行此 JS 代码的方式.

There are a bunch of misconceptions here: The HTML file will be served to your client, which will see <script> tags, request them from web server, and execute the code. You cannot expect a web browser to run a SQL query on your server), so obviously this is not the way you want to execute this JS code.

错的太多了,这将是一个很长的答案.这是您的两个主要误解:

So much is wrong, it will be a long answer. Here is a your two main misconceptions:

  • 你试图让客户端执行服务器代码
  • 你运行异步代码并认为它​​会同步返回

然后你的许多小错误:

  • 长度是一个属性,而不是一个方法
  • 您必须导出一个方法才能在节点外部使用它
  • 你使用 for (i in rows) 所以 i 是项目索引(我猜你得到它是因为你将它命名为 i),然后你使用 i.count
  • 在您的 SQL 中,您输入 SELECT COUNT(*) FROM 然后只需使用 .count 属性,我不确定没有 AS count
  • length is a property, not a method
  • you have to export a method to make it usable from outside in node
  • you use for (i in rows) so i is the item index (and I guess you got that because you named it i), then you use i.count
  • in your SQL you put SELECT COUNT(*) FROM then just use .count property, I'm not sure it will work without AS count

在这一点上,我只能猜测您的 SQL 和 Chart 用法并没有更好,抱歉 :( 不过我会尽力为您指出正确的方向.

At this point I can only guess your SQL and Chart usage are no better, sorry :( I will try to point you in the right direction though.

因此,首先,您需要从您的 Node.js 服务器执行此 JS 代码.通常的步骤是:

So, first of all, you need to execute this JS code from your Node.js server. The usual steps would be:

  • 启动 Express 应用
  • 配置 EJS 渲染
  • 在您的路线中:
    • 运行你的 sql 查询,获取一堆数据(你仍然是服务器端)
    • 渲染您的 HTML,传递一些数据
    • 现在在您的模板中,只需将您需要的内容从服务器注入到客户端

    后续步骤的示例数据结构:

    Sample data structure for the next steps:

    /
    +- app.js
    +- lib/
       +- map-access-data.js
    +- views/
       +- index.ejs
       +- map-access-chart.ejs
    +- stylesheets/
       +- styles.css
    

    服务器

    所需的服务器依赖:npm install express ejs mysql,其余用于客户端(如chart)

    The server

    Required server dependencies: npm install express ejs mysql, the rest is for client (like chart)

    // app.js
    const express = require('express')
    const app = express()
    
    // Serve public static assets, like stylesheets and client-side JS
    app.use(app.static('public'))
    
    // Configure EJS template engine
    app.set('view engine', 'ejs')
    
    // Your route
    app.get('/', (req, res) => {
      // Your route handler
      // req is client's request
      // res is server's response
    })
    
    // Start web server on http://localhost:8000
    app.listen(8000)
    

    好的,这里你是服务器端的,你可以使用 MySQL 和类似的系统.但首先我们需要解决另一个问题.

    OK, here you're server-side, you can use MySQL and similar systems. But first we need to address another issue.

    异步是 Node 的一个非常重要的部分,真的,但我们不能在这里解决所有问题.你会有关键词,我让你做你的研究来驯服那部分.我将使用 async/await 这样您在阅读代码时不会太受干扰,并使用 util.promisify 来转换方法.您必须了解的事情:

    Asynchronous is a very important part of Node, really, but we can't address everything here. You will have the keywords, I let you do your research to tame that part. I'll use async/await so you're not too disturbed when reading the code, and util.promisify to transform the methods. The things you have to understand:

    • connection.query 将查询远程服务器,在 Node 中它将异步完成,这意味着您不会立即得到任何结果,但您的代码也不会停止(或者它会被阻塞,这很糟糕)
    • 要表示异步操作,基本上有两种方式:
      • 将回调函数传递给您的异步函数,该回调将在结果可用时立即调用;使用回调时,您无法返回任何有趣的内容
      • 返回一个对象(称为 promise),它只是一个空包装器;然后稍后这个对象会突然包含结果,并且能够调用你将传递给它的 then 方法的函数;使用 Promise 时,您必须返回这些对象,这些对象代表您未来的数据,也是访问它的唯一方式
      • connection.query will query a remote server, in Node it will be done asynchronously, which means you won't get any result immediately, but your code won't be stopped either (or it would be blocking, which sucks)
      • to represent asynchronous operations, you have basically two ways:
        • pass a callback function to your async function, this callback will be called with the result as soon as it's available; when using a callback you cannot return anything interesting
        • return an object (called a promise) which is just an empty wrapper; then later this object will suddenly contain the result, and be able to call functions you will have passed to its then method; when using promises you must return those objects, which are a representation of your future data and the only way to access it

        这是你的错误:

        • getNumMaps 中,您的 return 在回调中.这个回调是在函数返回自己的结果后调用的,所以它只会返回 undefined
        • getMapAccessData 中你甚至都懒得返回任何东西,仍然未定义
        • getMapAccessNames 中,你终于返回了一些东西!但是由于 connection.query 是异步的,所以在函数已经返回 arr 之后,您将数据推送到您的数组中,所以它总是返回 []
        • In getNumMaps, your return is in the callback. This callback is called way after the function has returned its own result, so it will just return undefined
        • In getMapAccessData you didn't even bother to return anything, still undefined
        • In getMapAccessNames, finally you return something! but as connection.query is async, you will push data to your array way after funtion has already returned arr, so it always returns []

        我会添加你执行相同请求的三倍,听起来很浪费;)所以,你知道你想最终将所有这些都包含在你的 Chart 实例中,我们不要将其拆分为 4 个函数,它们都执行相同的查询,相反,我们将使用经过调整的格式构建单个结果.

        And I'll add you execute three times the same request, sounds wasteful ;) So, you know you want to finally include all this in your Chart instance, let's not split that in 4 functions which all execute the same query, we'll instead build a single result with adapted format.

        // lib/map-access-data.js
        const mysql = require('mysql')
        const connection = mysql.createConnection(/* your config here */)
        
        // get promises instead of using callbacks
        const util = require('util')
        const queryCallback = connection.query.bind(connection) // or we'll have issues with "this"
        const queryPromise = util.promisify(queryCallback) // now this returns a promise we can "await"
        
        // our asynchronous method, use "async" keyword so Node knows we can await for promises in there
        async function getChartData () {
          const rows = await queryPromise("SELECT name, COUNT(*) AS count FROM map_access GROUP BY name ORDER BY name")
        
          // Array#map allows to create a new array by transforming each value
          const counts = rows.map(row => row.count)
          const names = rows.map(row => row.name)
          const colors = rows.map(row => 'rgba(255,99,132,1)')
        
          // Return an object with chart's useful data
          return {
            labels: names,
            data: counts,
            colors: colors,
          }
        }
        

        模块

        好的,现在你有了一个功能,只有服务器端可用,它可以满足你的需要.

        Modules

        OK, now you have a function, available server side only, that gives you what you need.

        现在您需要能够从 app.js 中调用它,这意味着您需要:

        Now you need to be able to call it from app.js, which means you need to:

        • 导出:
        // lib/map-access-data.js
        …
        
        // Export your function as default
        module.exports = getChartData
        

        • 然后在您的路由处理程序中导入并使用它:
        • // app.js
          const getChartData = require('./lib/map-access-data)
          

          这称为 CommonJS 模块

          现在在您的路由处理程序中,您可以简单地调用您的 async 函数,然后 await 获取其结果:

          Now in your route handler you can simply call your async function, and await for its result:

          // app.js
          …
          
          app.get('/', async (req, res) => {
            // Your route handler
            const data = await getChartData()
          })
          

          生成 HTML

          现在你的数据已经可用,你仍然是服务器端的,你现在必须为你的客户端生成有效的 HTML,目前看起来像:

          Generating the HTML

          Now you have your data made available, you're still server-side, you now have to generate valid HTML for your client, which currently looks like:

          <!DOCTYPE html>
          <html>
            … a bunch of HTML …
          
              <p>See how people are using our app <br/></p>
              <canvas id="myChart" width="50%" height="100px"></canvas>
          
              <!-- NO! it's not a client JS, it's server JS, client CANNOT download it -->
              <script scr="map-access-data.js" type="text/javascript"></script>
          
              <script id ="map-popularity" type="text/javascript">
                  var Chart = require('chart'); // NO! you can't *require* from client
                  var ctx = document.getElementById("myChart").getContext("2d");
                  var myChart = new Chart(ctx, {
                      type: 'bar',
                      data: {
                          labels: getMapAccessNames(), // NO! You can't call server methods from client
                          datasets:[{
          …
          

          显然我们需要解决一些问题:

          Obviously we need to fix a few things:

          • 删除对 map-access-data.js 的引用,这是没有意义的
          • 添加 chart.js 浏览器的方式,就像来自 CDN 一样
          • 在你的客户端 JS 中注入数据
          • Remove the reference to map-access-data.js which makes no sense
          • Add chart.js the browser's way, like from a CDN
          • Inject data inside your client JS

          在这里,我认为您可以使用 Ajax 请求,而不是将真实数据直接注入 HTML,但我不知道 Chart,所以我会让您完成这部分.提供 JSON 数据的 Express 应用程序绝对是微不足道的,只需 res.send(data),然后在客户端执行一些 Ajax.让我们看一下将数据直接注入 HTML 以打破所有围墙的版本:

          Here I think instead of injecting the real data directly into HTML you could use an Ajax request, but I don't know Chart so I will let you do this part. An Express app serving JSON data is absolutely trivial, just res.send(data), then do some Ajax on client side. Let's see the version where you inject data directly into HTML to break all the walls:

          • 在服务器端运行 SQL,为您提供一些数据
          • 您将此数据传递给您的 EJS 模板,该模板(仍然是服务器端)生成 HTML
          • 在此 HTML 中,您将注入服务器数据的字符串表示形式(使用 JSON.stringify)
          • 服务器最终将生成的 HTML 发送给客户端
          • 客户端收到这个格式正确的HTML,<script>里面有一些JS,运行一下,大家都很开心
          • Server-side you run your SQL, that gives you some data
          • You pass this data to your EJS template, which (still server-side) generates HTML
          • In this HTML you will inject String representation of your server data (using JSON.stringify)
          • Server finally sends generated HTML to client
          • Client receives this wellformed HTML, with some JS in <script>, runs it, everyone is happy
          <!-- views/map-access-chart.ejs -->
          <canvas id="myChart" width="50%" height="100px"></canvas>
          
          <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/1.0.2/Chart.min.js"></script>
          <script id ="map-popularity" type="text/javascript">
              var ctx = document.getElementById("myChart").getContext("2d");
              var myChart = new Chart(ctx, {
                  type: 'bar',
                  data: {
                      labels: <%- JSON.stringify(data.labels) %>,
                      datasets:[{
                          label: "Map Selection Popularity",
                          data: <%- JSON.stringify(data.data) %>,
                          backgroundColor: <%- JSON.stringify(data.colors) %>,
                          borderColor: <%- JSON.stringify(data.colors) %>,
                          borderWidth: 1
          …
          

          // app.js
          …
          
          // Your route handler
          const data = await getChartData()
          // Render 'index.ejs' with variables 'title' and 'data' available
          res.render('index', {
            title: 'Page title',
            data: data,
          })
          

          现在,当您从终端运行 node app.js 并转到 http://localhost:8000 你应该看到结果.我想会有很多剩余的错误,但这将是一个更好的开始:)

          Now when you run node app.js from your terminal, and go to http://localhost:8000 you should see the result. I guess there will be many remaining errors, but that will be a better start :)

相关文章