使用 Node.js 构建您自己的 Web 分析仪表板

2025-05-25

使用 Node.js 构建您自己的 Web 分析仪表板

作者:Jon Corbin✏️

如果你用过 Google Analytics,你就知道它的界面不太美观。它确实能完成工作,但我不太喜欢它的外观,也不喜欢它的配色。比如,看看这个:

Google Analytics 主页信息中心

它实在太无聊乏味了——我的生活需要更多色彩。我还想从 Google Analytics 中获得更多它目前无法提供的自定义功能。还好我们是软件开发者,所以我们可以按照自己的标准构建自己的 Google Analytics 版本!

Google API

幸运的是,Google 提供了大量不同的 API 供我们在项目中使用。我们只需在Google 开发者帐户中进行设置即可

创建新项目

首先,我们需要通过单击左上角的项目选择来创建一个新项目:

Google API 我的项目下拉菜单

然后创建一个新项目并随意命名。

Google API 新项目页面

Google API 新项目名称

添加 Google Analytics API

创建项目后,我们需要添加一些服务以便使用 Google Analytics API。为此,我们需要点击页面顶部的“启用 API 和服务” 。

启用 API 和服务选项

进入API 和服务页面后,我们将搜索“google analytics api”并将其添加到我们的项目中。不要添加 Google Analytics Reporting API。这不是我们想要的 API。

搜索结果中的 Google Analytics API

启用 Google Analytics API

创建服务帐户

添加 Analytics API 后,我们需要创建一个服务帐户,以便我们的应用可以访问该 API。为此,请从控制台主屏幕转到凭据部分。

Google API 凭证部分

到达那里后,单击“创建凭据”下拉菜单并选择“服务帐户密钥”

Google API 创建凭证下拉菜单

现在将您看到的选项设置为以下内容(除了服务帐户名称- 您可以随意命名)。

Google API 服务帐户信息

点击“创建”后,将生成一个 JSON 文件。请将其保存在已知位置,因为我们需要其中的部分内容。

生成的 JSON 密钥

在该 JSON 文件中,找到客户邮箱并复制。然后前往 Google Analytics(分析)页面,向视图添加新用户。操作步骤如下:首先点击左下角的齿轮图标,然后前往视图部分中的“用户管理” 。

Google Analytics 用户管理

在这里,通过单击右上角的蓝色大加号并选择“添加用户”来添加新用户。

Google Analytics 添加用户选项

从 JSON 文件中粘贴客户邮箱地址,并确保在权限设置中勾选了“读取和分析” 。这是我们想赋予此账户的唯一权限。

Google Analytics 用户权限选项

最后,我们想获取视图 ID,以供稍后使用。请从管理员设置中,前往“视图设置”,复制视图 ID以供稍后使用(最好将其保存在单独的打开的标签页中)。

Google Analytics 查看设置

您的 Google API 现在应该已准备就绪!

LogRocket 免费试用横幅

后端

对于后端,我们将使用 Node.js。让我们开始设置我们的项目吧!我将使用它yarn作为包管理器,但npm应该也能正常工作。

设置

首先,让我们运行一下yarn init,启动我们的结构。输入名称、描述等你喜欢的内容。Yarn 会将我们的入口点设置为,server.js而不是index.js,所以从现在开始,它将引用 。现在,让我们添加依赖项:

$ yarn add cors dotenv express googleapis
Enter fullscreen mode Exit fullscreen mode

我们还需要将concurrently和添加jest到我们的开发依赖项中,因为我们将在脚本中使用它。

$ yarn add -D concurrently
Enter fullscreen mode Exit fullscreen mode

说到这,我们现在就开始设置吧。在我们的 中package.json,我们需要将脚本设置为:

"scripts": {
    "test_server": "jest ./ --passWithNoTests",
    "test_client": "cd client && yarn test",
    "test": "concurrently \"yarn test_server\" \"yarn test_client\"",
    "start": "concurrently \"npm run server\" \"npm run client\"",
    "server": "node server.js",
    "client": "cd client && npm start",
    "build": "cd client && yarn build"
  },
Enter fullscreen mode Exit fullscreen mode

最后,我们需要创建一个.env文件来存储我们的机密和一些配置。以下是我们要添加的内容:

CLIENT_EMAIL="This is the email in your json file from google"
PRIVATE_KEY="This is also in the json file"
VIEW_ID="The view id from google analytics you copied down earlier"
SERVER_PORT=3001 // or whatever port you'd like
NODE_ENV="dev"
Enter fullscreen mode Exit fullscreen mode

太棒了——现在我们基本上可以开始开发服务器了。如果你愿意,可以eslint在开始之前添加依赖项(我建议这样做)。

服务器

现在就开始处理这个服务器文件吧?首先,用 创建它touch server.js。现在用你喜欢的编辑器打开它。首先,我们需要定义一些内容:

require('dotenv').config();

// Server
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());
const server = require('http').createServer(app);

// Config
const port = process.env.SERVER_PORT;
if (process.env.NODE_ENV === 'production') {
  app.use(express.static('client/build'));
}
Enter fullscreen mode Exit fullscreen mode

.env这里我们将使用 来加载我们的变量require('dotenv').config(),它会帮我们处理好繁琐的工作。这会将所有变量加载到 中,process.env以供后续使用。

接下来,我们定义服务器,并使用express。我们将它添加cors到 Express 应用,以便稍后可以从前端访问它。然后,我们将应用包装进去,require('http').createServer以便稍后可以使用 Socket.IO 添加一些有趣的功能。

最后,我们通过设置全局常量来进行一些配置,port以便稍后进行简写,并static根据我们的NODE_ENV变量改变我们的路径。

现在让我们通过将其添加到文件底部来让我们的服务器监听我们的端口server.js

server.listen(port, () => {
  console.log(`Server running at localhost:${port}`);
});
Enter fullscreen mode Exit fullscreen mode

太棒了!在我们开发 Google API 库之前,我们能为服务器做的事情就这些了。

分析库

回到终端,让我们创建一个名为libraries/using的新目录mkdir libraries,并创建我们的分析处理程序。我将其命名为gAnalytics.js,我们可以使用它创建它touch libraries/gAnalytics.js,然后切换回编辑器。

在中gAnalytics.js,让我们定义一些配置:

// Config
const clientEmail = process.env.CLIENT_EMAIL;
const privateKey = process.env.PRIVATE_KEY.replace(new RegExp('\\\\n'), '\n');
const scopes = ['https://www.googleapis.com/auth/analytics.readonly'];
Enter fullscreen mode Exit fullscreen mode

我们需要从 中提取客户端电子邮件和私钥(它们从 Google API 控制台提供的 JSON 凭证文件中提取)process.env,并且需要将\\n私钥中的所有 s 替换为 (这是dotenv读取私钥的方式)\n。最后,我们为 Google API 定义一些作用域。这里有不少不同的选项,例如:

https://www.googleapis.com/auth/analytics to view and manage the data
https://www.googleapis.com/auth/analytics.edit to edit the management entities
https://www.googleapis.com/auth/analytics.manage.users to manage the account users and permissions
Enter fullscreen mode Exit fullscreen mode

还有很多,但我们只想要只读的,这样我们的应用程序就不会暴露太多。

现在让我们使用这些变量来设置 Google Analytics:

// API's
const { google } = require('googleapis');
const analytics = google.analytics('v3');
const viewId = process.env.VIEW_ID;
const jwt = new google.auth.JWT({
  email: clientEmail,
  key: privateKey,
  scopes,
});
Enter fullscreen mode Exit fullscreen mode

这里我们只需要google创建analytics和。我们还需要从 中jwt提取。我们在这里创建了一个JWT,以便稍后需要数据时进行授权。现在我们需要创建一些函数来实际检索数据。首先,我们将创建获取函数:viewIdprocess.env

async function getMetric(metric, startDate, endDate) {
  await setTimeout[Object.getOwnPropertySymbols(setTimeout)[0]](
    Math.trunc(1000 * Math.random()),
  ); // 3 sec
  const result = await analytics.data.ga.get({
    auth: jwt,
    ids: `ga:${viewId}`,
    'start-date': startDate,
    'end-date': endDate,
    metrics: metric,
  });
  const res = {};
  res[metric] = {
    value: parseInt(result.data.totalsForAllResults[metric], 10),
    start: startDate,
    end: endDate,
  };
  return res;
}
Enter fullscreen mode Exit fullscreen mode

这个有点复杂,我们来分解一下。首先,我们将其设置为异步,以便可以一次性获取多个指标。但是,Google 强制要求,所以我们需要添加一个随机等待,使用

await setTimeout[Object.getOwnPropertySymbols(setTimeout)[0]](
    Math.trunc(1000 * Math.random()),
  );
Enter fullscreen mode Exit fullscreen mode

如果有许多用户尝试加载数据,这很可能会引入可扩展性问题,但我只是一个人,所以它可以满足我的需求。

接下来,我们使用 获取数据analytics.data.ga.get,这将返回一个包含大量数据的相当大的对象。我们不需要全部数据,所以只提取重要的部分:result.data.totalsForAlResults[metric]。这是一个字符串,所以我们将其转换为 int 类型,并将其与开始日期和结束日期一起返回。

接下来我们添加批量获取指标的方法:

function parseMetric(metric) {
  let cleanMetric = metric;
  if (!cleanMetric.startsWith('ga:')) {
    cleanMetric = `ga:${cleanMetric}`;
  }
  return cleanMetric;
}
function getData(metrics = ['ga:users'], startDate = '30daysAgo', endDate = 'today') {
  // ensure all metrics have ga:
  const results = [];
  for (let i = 0; i < metrics.length; i += 1) {
    const metric = parseMetric(metrics[i]);
    results.push(getMetric(metric, startDate, endDate));
  }
  return results;
}
Enter fullscreen mode Exit fullscreen mode

这样我们就能轻松地一次性请求大量指标。它只会返回一个getMetricPromise 列表。我们还添加了一种方法,用于清理传递给函数的指标名称parseMetricga:如果指标名称尚未存在,则只需将其添加到指标的前面即可。

最后,getData从底部导出,我们的图书馆就可以使用了。

module.exports = { getData };
Enter fullscreen mode Exit fullscreen mode

把一切都联系起来

现在让我们通过添加一些路由来合并我们的库和服务器。在 中server.js,我们将添加以下路径:

app.get('/api', (req, res) => {
  const { metrics, startDate, endDate } = req.query;
  console.log(`Requested metrics: ${metrics}`);
  console.log(`Requested start-date: ${startDate}`);
  console.log(`Requested end-date: ${endDate}`);
  Promise.all(getData(metrics ? metrics.split(',') : metrics, startDate, endDate))
    .then((data) => {
      // flatten list of objects into one object
      const body = {};
      Object.values(data).forEach((value) => {
        Object.keys(value).forEach((key) => {
          body[key] = value[key];
        });
      });
      res.send({ data: body });
      console.log('Done');
    })
    .catch((err) => {
      console.log('Error:');
      console.log(err);
      res.send({ status: 'Error getting a metric', message: `${err}` });
      console.log('Done');
    });
});
Enter fullscreen mode Exit fullscreen mode

此路径允许我们的客户端请求指标列表(或仅一个指标),然后在检索到所有数据后返回,正如我们所见Promise.all。这将等到给定列表中的所有 Promise 都完成或其中一个失败。

然后,我们可以添加一个.then接受data参数的 。这个data参数是我们在 中创建的数据对象列表gAnalytics.getData,因此我们遍历所有对象并将它们组合成一个 body 对象。这个对象将以 的形式发送回我们的客户端res.send({data: body});

我们还将.catch向我们的添加一个Promise.all,它将发回一条错误消息并记录错误。

现在我们来添加api/graph/路径,它将用于……嗯,绘图。它和我们的/api路径非常相似,但又有自己的细微差别。

app.get('/api/graph', (req, res) => {
  const { metric } = req.query;
  console.log(`Requested graph of metric: ${metric}`);
  // 1 week time frame
  let promises = [];
  for (let i = 7; i >= 0; i -= 1) {
    promises.push(getData([metric], `${i}daysAgo`, `${i}daysAgo`));
  }
  promises = [].concat(...promises);
  Promise.all(promises)
    .then((data) => {
      // flatten list of objects into one object
      const body = {};
      body[metric] = [];
      Object.values(data).forEach((value) => {
        body[metric].push(value[metric.startsWith('ga:') ? metric : `ga:${metric}`]);
      });
      console.log(body);
      res.send({ data: body });
      console.log('Done');
    })
    .catch((err) => {
      console.log('Error:');
      console.log(err);
      res.send({ status: 'Error', message: `${err}` });
      console.log('Done');
    });
});
Enter fullscreen mode Exit fullscreen mode

如您所见,我们仍然依赖gAnalytics.getDataPromise.all,但是,我们获取过去七天的数据并将其全部压缩到一个列表中以发送回正文。

我们的服务器现在就完成了。很简单,不是吗?现在来看看真正的重头戏——前端。

前端

前端开发虽然充满乐趣,但开发和设计起来却颇具挑战性。不妨一试!我们的前端将充分运用React框架。我建议大家在开始之前,先起身散散步,或者喝杯水。

你什么都没做吧?好吧,我们开始吧。

设置和结构

首先,我们需要创建样板。我们将使用create-react-app样板,因为它始终是一个很好的起点。所以,运行create-react-app client它并让它运行。完成后,我们将安装一些所需的依赖项。确保你cd进入了client/文件夹,然后运行$ yarn add @material-ui/core prop-types recharts​​。

再次,如果您愿意,请在此处设置 eslint。接下来,src/App.js在进入结构之前,我们将进行清理。打开src/App.js并删除所有内容,这样只剩下以下内容:

import React from 'react';
import './App.css';
function App() {
  return (
    <div className="App">
    </div>
  );
}
export default App;
Enter fullscreen mode Exit fullscreen mode

我们还想将serviceWorker.js其从中删除src/index.js

至于结构,我们先把所有东西都设置好,然后再进行开发。我们的src文件夹看起来是这样的(稍后会讲解):

├── App.css
├── App.js
├── App.test.js
├── components
   ├── Dashboard
      ├── DashboardItem
         ├── DashboardItem.js
         └── DataItems
             ├── index.js
             ├── ChartItem
                └── ChartItem.js
             └── TextItem
                 └── TextItem.js
      └── Dashboard.js
   └── Header
       └── Header.js
├── index.css
├── index.js
├── theme
   ├── index.js
   └── palette.js
└── utils.js
Enter fullscreen mode Exit fullscreen mode

创建所有这些文件和文件夹,因为我们将编辑它们来构建我们的应用。从这里开始,每个文件引用都与src/文件夹相关。

成分

Apptheme

让我们从头开始App。我们需要将其编辑为如下所示:

import React from 'react';
import './App.css';
import Dashboard from './components/Dashboard/Dashboard';
import { ThemeProvider } from '@material-ui/styles';
import theme from './theme';
import Header from './components/Header/Header';
function App() {
  return (
    <ThemeProvider theme={theme}>
      <div className="App">
        <Header text={"Analytics Dashboard"}/>
        <br/>
        <Dashboard />
      </div>
    </ThemeProvider>
  );
}
export default App;
Enter fullscreen mode Exit fullscreen mode

这将引入必要的组件并创建我们的主题提供程序。接下来,让我们编辑该主题。打开theme/index.js并添加以下内容:

import { createMuiTheme } from '@material-ui/core';
import palette from './palette';
const theme = createMuiTheme({
  palette,
});
export default theme;
Enter fullscreen mode Exit fullscreen mode

接下来打开theme/palette.js并添加以下内容:

import { colors } from '@material-ui/core';
const white = '#FFFFFF';
const black = '#000000';
export default {
  black,
  white,
  primary: {
    contrastText: white,
    dark: colors.indigo[900],
    main: colors.indigo[500],
    light: colors.indigo[100]
  },
  secondary: {
    contrastText: white,
    dark: colors.blue[900],
    main: colors.blue['A400'],
    light: colors.blue['A400']
  },
  text: {
    primary: colors.blueGrey[900],
    secondary: colors.blueGrey[600],
    link: colors.blue[600]
  },
  background: {
    primary: '#f2e1b7',
    secondary: '#ffb3b1',
    tertiary: '#9ac48d',
    quaternary: '#fdae03',
    quinary: '#e7140d',
  },
};
Enter fullscreen mode Exit fullscreen mode

以上这些都让我们可以在组件中使用主题来设置不同的样式。我们还定义了主题颜色,您可以根据自己的喜好进行更改。我喜欢这些主题颜色柔和的色调。

Header

接下来,我们来创建页眉。打开components/Header/header.js并添加以下内容:

import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import Paper from '@material-ui/core/Paper';
import AppBar from '@material-ui/core/AppBar';
const styles = (theme) => ({
  header: {
    padding: theme.spacing(3),
    textAlign: 'center',
    color: theme.palette.text.primary,
    background: theme.palette.background.primary,
  },
});
export const Header = (props) => {
  const { classes, text } = props;
  return (
    <AppBar position="static">
      <Paper className={classes.header}>{text}</Paper>
    </AppBar>
  );
};
Header.propTypes = {
  classes: PropTypes.object.isRequired,
  text: PropTypes.string.isRequired,
};
export default withStyles(styles)(Header);
Enter fullscreen mode Exit fullscreen mode

这将在页面顶部创建一个水平条,其文本是我们设置 prop 的值。它还会引入我们的样式,并使用它来使页面看起来非常棒。

Dashboard

接下来,我们来处理components/Dashboard/Dashboard.js。这是一个更简单的组件,如下所示:

import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import Grid from '@material-ui/core/Grid';
import DashboardItem from './DashboardItem/DashboardItem';
import { isMobile } from '../../utils';
const styles = () => ({
  root: {
    flexGrow: 1,
    overflow: 'hidden',
  },
});
const Dashboard = (props) => {
  const { classes } = props;
  return (
    <div className={classes.root}>
      <Grid container direction={isMobile ? 'column' : 'row'} spacing={3} justify="center" alignItems="center">
        <DashboardItem size={9} priority="primary" metric="Users" visual="chart" type="line" />
        <DashboardItem size={3} priority="secondary" metric="Sessions"/>
        <DashboardItem size={3} priority="primary" metric="Page Views"/>
        <DashboardItem size={9} metric="Total Events" visual="chart" type="line"/>
      </Grid>
    </div>
  );
};
Dashboard.propTypes = {
  classes: PropTypes.object.isRequired,
};
export default withStyles(styles)(Dashboard);
Enter fullscreen mode Exit fullscreen mode

这里我们添加了一些Dashboard Item具有不同指标的示例。这些指标来自Google API 的 Metrics & Dimensions Explore。我们还需要创建一个utils.js包含以下内容的文件:

export function numberWithCommas(x) {
  return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
export const isMobile = window.innerWidth <= 500;
Enter fullscreen mode Exit fullscreen mode

这将告诉我们用户是否在使用移动设备。我们需要一个响应式应用,所以我们需要知道用户是否在使用移动设备。好的,我们继续。

DashboardItem

接下来,我们需要DashboardItem编辑Dashboard/DashboardItem/DashboardItem.js并创建它。将以下内容添加到该文件中:

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import Paper from '@material-ui/core/Paper';
import Grid from '@material-ui/core/Grid';
import { TextItem, ChartItem, RealTimeItem } from './DataItems';
import { numberWithCommas, isMobile } from '../../../utils';
const styles = (theme) => ({
  paper: {
    marginLeft: theme.spacing(1),
    marginRight: theme.spacing(1),
    paddingTop: theme.spacing(10),
    textAlign: 'center',
    color: theme.palette.text.primary,
    height: 200,
    minWidth: 300,
  },
  chartItem: {
    paddingTop: theme.spacing(1),
    height: 272,
  },
  mainMetric: {
    background: theme.palette.background.quaternary,
  },
  secondaryMetric: {
    background: theme.palette.background.secondary,
  },
  defaultMetric: {
    background: theme.palette.background.tertiary,
  },
});
class DashboardItem extends Component {
  constructor(props) {
    super(props);
    const {
      classes,
      size,
      metric,
      priority,
      visual,
      type,
    } = this.props;
    this.state = {
      classNames: classes,
      size,
      metric,
      priority,
      visual,
      type,
      data: 'No data',
    };
  }
  componentDidMount() {
    this.getMetricData();
    this.getClassNames();
  }
  getMetricData() {
    const { visual, metric } = this.state;
    const strippedMetric = metric.replace(' ', '');

    let url;
    if (visual === 'chart') {
      url = `http://localhost:3001/api/graph?metric=${strippedMetric}`;
    } else {
      url = `http://localhost:3001/api?metrics=${strippedMetric}`;
    }
    fetch(url, {
      method: 'GET',
      mode: 'cors',
    })
      .then((res) => (res.json()))
      .then((data) => {
        let value;
        let formattedValue;
        if (visual === 'chart') {
          value = data.data[strippedMetric];
          formattedValue = value;
        } else {
          try {
            value = strippedMetric.startsWith('ga:') ? data.data[strippedMetric] : data.data[`ga:${strippedMetric}`];
            formattedValue = numberWithCommas(parseInt(value.value, 10));
          } catch (exp) {
            console.log(exp);
            formattedValue = "Error Retrieving Value"
          }
        }
        this.setState({ data: formattedValue });
      });
  }
  getClassNames() {
    const { priority, visual } = this.state;
    const { classes } = this.props;
    let classNames = classes.paper;
    switch (priority) {
      case 'primary':
        classNames = `${classNames} ${classes.mainMetric}`;
        break;
      case 'secondary':
        classNames = `${classNames} ${classes.secondaryMetric}`;
        break;
      default:
        classNames = `${classNames} ${classes.defaultMetric}`;
        break;
    }
    if (visual === 'chart') {
      classNames = `${classNames} ${classes.chartItem}`;
    }
    this.setState({ classNames });
  }
  getVisualComponent() {
    const { data, visual, type } = this.state;
    let component;
    if (data === 'No data') {
      component = <TextItem data={data} />;
    } else {
      switch (visual) {
        case 'chart':
          component = <ChartItem data={data} xKey='start' valKey='value' type={type} />;
          break;
        default:
          component = <TextItem data={data} />;
          break;
      }
    }
    return component;
  }
  render() {
    const {
      classNames,
      metric,
      size,
    } = this.state;
    const visualComponent = this.getVisualComponent();
    return (
      <Grid item xs={(isMobile || !size) ? 'auto' : size} zeroMinWidth>
        <Paper className={`${classNames}`}>
          <h2>{ metric }</h2>
          {visualComponent}
        </Paper>
      </Grid>
    );
  }
}
DashboardItem.propTypes = {
  size: PropTypes.number,
  priority: PropTypes.string,
  visual: PropTypes.string,
  type: PropTypes.string,
  classes: PropTypes.object.isRequired,
  metric: PropTypes.string.isRequired,
};
DashboardItem.defaultProps = {
  size: null,
  priority: null,
  visual: 'text',
  type: null,
};
export default withStyles(styles)(DashboardItem);
Enter fullscreen mode Exit fullscreen mode

这个组件相当庞大,但它是我们应用程序的核心。简而言之,这个组件是我们实现高度可定制界面的关键。通过这个组件,我们可以根据传递的 props 更改视觉效果的大小、颜色和类型。该DashboardItem组件还会自行获取数据,然后将其传递给其可视化组件。

不过,我们确实必须创建这些可视化组件,所以让我们这样做吧。

可视化组件(DataItems

我们需要创建 和ChartItem才能TextItem正确DashboardItem渲染。打开components/Dashboard/DashboardItem/DataItems/TextItem/TextItem.js并添加以下内容:

import React from 'react';
import PropTypes from 'prop-types';

export const TextItem = (props) => {
  const { data } = props;
  let view;
  if (data === 'No data') {
    view = data;
  } else {
    view = `${data} over the past 30 days`
  }
  return (
    <p>
      {view}
    </p>
  );
};
TextItem.propTypes = {
  data: PropTypes.string.isRequired,
};
export default TextItem;
Enter fullscreen mode Exit fullscreen mode

这个非常简单——它基本上会显示传递给它的 props 的文本data。现在让我们ChartItem打开它components/Dashboard/DashboardItem/DataItems/ChartItem/ChartItem.js,并添加以下内容:

import React from 'react';
import PropTypes from 'prop-types';
import {
  ResponsiveContainer, LineChart, XAxis, YAxis, CartesianGrid, Line, Tooltip,
} from 'recharts';
export const ChartItem = (props) => {
  const { data, xKey, valKey } = props;
  return (
    <ResponsiveContainer height="75%" width="90%">
      <LineChart data={data}>
        <XAxis dataKey={xKey} />
        <YAxis type="number" domain={[0, 'dataMax + 100']} />
        <Tooltip />
        <CartesianGrid stroke="#eee" strokeDasharray="5 5" />
        <Line type="monotone" dataKey={valKey} stroke="#8884d8" />
      </LineChart>
    </ResponsiveContainer>
  );
};
ChartItem.propTypes = {
  data: PropTypes.array.isRequired,
  xKey: PropTypes.string,
  valKey: PropTypes.string,
};
ChartItem.defaultProps = {
  xKey: 'end',
  valKey: 'value',
};
export default ChartItem;
Enter fullscreen mode Exit fullscreen mode

顾名思义,它的作用就是渲染一个图表。它使用了api/graph/我们添加到服务器的路由。

完成的!

至此,你应该已经可以使用我们已有的内容了!你只需yarn start要从最顶层目录运行,一切就应该正常启动了。

即时的

Google Analytics 的一大优势在于它能够实时查看哪些用户正在使用您的网站。我们也能做到!可惜的是,Google API 的 Realtime API 还处于封闭测试阶段,不过,我们毕竟是软件开发者!那就自己动手吧!

后端

添加Socket.IO

我们将使用 Socket.IO 来实现这一点,因为它允许机器之间进行实时通信。首先,使用 将 Socket.IO 添加到你的依赖项中yarn add socket.io。现在,打开你的server.js文件,并在其顶部添加以下内容:

const io = require('socket.io').listen(server);
Enter fullscreen mode Exit fullscreen mode

你可以在定义下方添加这段代码server。然后在底部、上方server.listen添加以下内容:

io.sockets.on('connection', (socket) => {
  socket.on('message', (message) => {
    console.log('Received message:');
    console.log(message);
    console.log(Object.keys(io.sockets.connected).length);
    io.sockets.emit('pageview', { connections: Object.keys(io.sockets.connected).length - 1 });
  });
});
Enter fullscreen mode Exit fullscreen mode

这将允许我们的服务器监听连接到它的套接字并向其发送消息。当它收到消息时,它会'pageview'向所有套接字发出一个事件(这可能不是最安全的做法,但我们只发送连接数,所以这并不重要)。

创建公共脚本

为了让客户端向服务器发送消息,他们需要一个脚本!让我们在client/publiccalled中创建一个脚本realTimeScripts.js,其中包含:

const socket = io.connect();
socket.on('connect', function() {
  socket.send(window.location);
});
Enter fullscreen mode Exit fullscreen mode

现在我们只需要在我们的任何网页中引用这两个脚本,连接就会被跟踪。

<script src="/socket.io/socket.io.js"></script>
<script src="realTimeScripts.js"></script>
Enter fullscreen mode Exit fullscreen mode

/socket.io/socket.io.js由 的安装处理socket.io因此无需创建它。

前端

创建新组件

要查看这些连接,我们需要一个新的组件。首先,我们来编辑一下DashboardItem.js,添加以下内容getMetricData

    //...
    const strippedMetric = metric.replace(' ', '');
    // Do not need to retrieve metric data if metric is real time, handled in component
    if (metric.toUpperCase() === "REAL TIME") {
      this.setState({ data: "Real Time" })
      return;
    }
    //...
Enter fullscreen mode Exit fullscreen mode

这将设置我们的状态并返回函数,getMetricData因为我们不需要获取任何内容。接下来,让我们将以下内容添加到getVisualComponent

    //...
      component = <TextItem data={data} />;
    } else if (data === 'Real Time') {
      component = <RealTimeItem />
    } else {
      switch (visual) {
    //...
Enter fullscreen mode Exit fullscreen mode

RealTimeItem现在,当metricprop 为时,我们的视觉组件将被设置为我们的"Real Time"

现在我们需要创建RealTimeItem组件。创建以下路径和文件:Dashboard/DashboardItem/DataItems/RealTimeItem/RealTimeItem.js。现在向其中添加以下内容:

import React, { useState } from 'react';
import openSocket from 'socket.io-client';
const socket = openSocket('http://localhost:3001');
const getConnections = (cb) => {
  socket.on('pageview', (connections) => cb(connections.connections))
}
export const RealTimeItem = () => {
  const [connections, setConnections] = useState(0);
  getConnections((conns) => {
    console.log(conns);
    setConnections(conns);
  });
  return (
    <p>
      {connections}
    </p>
  );
};

export default RealTimeItem;
Enter fullscreen mode Exit fullscreen mode

这将在我们的仪表板上添加一张实时卡片。

我们完成了!

您现在应该拥有一个功能齐全的仪表板,如下所示:

已完成的自定义分析仪表板

这是一个高度可扩展的仪表板,您可以像添加实时数据项一样添加新的数据项。我会继续进一步开发它,因为我已经想到了其他一些功能,包括“添加卡片”按钮、更改大小、不同的图表类型、添加维度等等!如果您希望我继续撰写有关这个仪表板的文章,请告诉我!最后,如果您想查看源代码,可以在这里找到代码库


编者注:觉得这篇文章有什么问题?您可以在这里找到正确版本

插件:LogRocket,一个用于 Web 应用的 DVR

 
LogRocket 仪表板免费试用横幅
 
LogRocket是一款前端日志工具,可让您重放问题,就像它们发生在您自己的浏览器中一样。您无需猜测错误发生的原因,也无需要求用户提供屏幕截图和日志转储,LogRocket 允许您重放会话以快速了解问题所在。它可与任何应用程序完美兼容,无论使用哪种框架,并且提供插件来记录来自 Redux、Vuex 和 @ngrx/store 的更多上下文。
 
除了记录 Redux 操作和状态之外,LogRocket 还记录控制台日志、JavaScript 错误、堆栈跟踪、带有标头 + 正文的网络请求/响应、浏览器元数据以及自定义日志。它还会对 DOM 进行插桩,以记录页面上的 HTML 和 CSS,即使是最复杂的单页应用程序,也能重现像素完美的视频。
 
免费试用


使用 Node.js 构建您自己的 Web 分析仪表板一文首先出现在LogRocket 博客上。

文章来源:https://dev.to/bnevilleoneill/build-your-own-web-analytics-dashboard-with-node-js-327b
PREV
在 Docker 上使用 Node.js 和 ElasticSearch 进行全文搜索
NEXT
Axios 或 fetch():您应该使用哪一个?