如何检测用户首次登录和特定页面加载的时间?

11

我想在用户首次登录时触发一些JS,仅在特定页面第一次加载时触发。

我相信我可以通过简单地检查user.sign_in_count < 2来处理他们首次登录,但我不知道如何仅指定在第一次页面加载时触发。

即我不希望JS在用户第一次登录并在未注销的情况下刷新页面后被触发。

我正在使用Turbolinks和$(document).on('turbolinks:load', function() {来触发它。

编辑1

所以我现在想要做的是在多个页面上执行Bootstrap Tour。但我只希望该游览在第一次页面加载时自动执行。该游览本身将引导用户访问应用程序中的其他特定页面,但每个页面都将具有特定于该页面的游览JS。

现在,在我的HTML代码中,我有类似以下的内容:

<script type="text/javascript">
  $(document).on('turbolinks:load', function() {
      var tour = new Tour({
        storage: false,
        backdrop: true,
        onStart: function(){
        $('body').addClass('is-touring');
        },
        onEnd: function(){
        $('body').removeClass('is-touring');
        },
        steps: [
        {
          element: "#navbar-logo",
          title: "Go Home",
          content: "All throughout the app, you can click our logo to get back to the main page."
        },
        {
          element: "input#top-search",
          title: "Search",
          content: "Here you can search for players by their name, school, positions & bib color (that they wore in our tournament)"
        }
      ]});

      // Initialize the tour
      tour.init();

      // Start the tour
      tour.start();
  });
</script>

所以我真正想做的就是:

  • 不要在用户第一次登录并重新加载页面时强制执行新的导览。
  • 允许他们能够稍后手动执行导览,只需简单地按下一个链接即可。
  • 如果可以的话,我不想在我的数据库中存储任何内容——因此最好采用基于 cookie 或本地存储的方法。
  • 假设我将使用 Rails 来跟踪他们已经完成的签到次数。因此,一旦他们签到超过一次,我就不能触发这个 JS。

真正的问题只出现在第一次登录时,如果他们刷新主页面 10 次,这个导览就会执行 10 次。这就是我想要解决的问题。

希望这提供了更多的清晰度。


您想让事件在特定页面上仅在该用户的“生命周期”内触发一次,还是每个会话只触发一次(这意味着如果用户一周后返回该页面,事件应该会触发)? - jackel414
@jackel414 用户一生只能看到一次。它不应该在一周后自动触发,但我相信我可以通过 user.sign_in_count > 1 检查来管理它。因此,一旦他们登录超过一次,他们就不会再看到它了。我现在正在努力解决的问题只是防止在第一次登录时每次加载页面时都触发导览。 - marcamillion
这不是你想要的答案,但从用户体验的角度来看,你应该将其存储在数据库中以供用户输入。这将允许它存在于跨浏览器会话中,即如果用户打开Chrome,关闭了tour,然后稍后在不同的计算机上打开Firefox,你仍然知道不要向他们呈现tour。在我看来,你过于考虑限制自己不去访问数据库。 - engineerDave
1
@engineerDave,如果他正在使用来自数据库的“登录计数”与localStorage解决方案相结合,是否存在这样的情况:在不同计算机上使用Firefox实际上会向他们展示旅游?(用户仍然需要重新登录,这将增加登录计数> 1) - haxxxton
@haxxxton,由于localStorage是特定于浏览器的https://dev59.com/Ol8d5IYBdhLWcg3weiB-#26795496,从用户体验的角度来看,简短的答案是肯定的。(除非您想假设登录计数> 0表示没有旅游。)最终用户将在每个新浏览器中看到该旅游,无论他们在同一台计算机上还是在不同的机器上使用。 - engineerDave
显示剩余2条评论
8个回答

15

前言

据我了解,您有以下内容:

  1. 多个包含单个导览的页面(每个页面的导览不同)
  2. 一种检测首次登录帐户的方法(ruby 登录计数)
  3. 根据首次登录能够添加脚本值

解决方案概述

以下解决方案使用 localStorage 存储每个导览的标识符及其是否已被查看的键值对。由于 localStorage 是唯一的,它在页面刷新和会话之间都能保持存储状态。正如名称所示,localStorage 对每个域、设备和浏览器都是唯一的(例如,即使是相同的域,Chrome 的 localStorage 也无法访问 Firefox 的 localStorage,您笔记本电脑上的 Chrome localStorage 也无法访问您手机上 Chrome 的 localStorage)。我提到这一点是为了说明对 Preface 3 的依赖性,以切换 JS 标志表示用户之前是否已登录。

为了启动导览,代码将检查 localStorage 是否将其对应的键值对设置为 true(表示已经“查看”了)。如果存在且设置为 true,则不会启动导览,否则将运行。当每个导览开始时,使用其 onStart 方法,更新/添加导览的标识符到 localStorage 并将其值设置为 true

可以通过手动调用导览的 start 方法来执行导览,如果您只想执行当前页面的导览;否则,可以清除所有与导览相关的 localStorage 并将用户发送回第一个页面/如果您在第一个页面上,再次调用 start 方法。

JSFiddle(基于其他问题的 HTML 问答)

HTML(这可以是具有 id="tourAgain" 属性的任何元素,以使以下代码工作。

<button class="btn btn-sm btn-default" id="tourAgain">Take Tour Again</button>

JS

var isFirstLogin = true; // this value is populated by ruby based upon first login
var userID = 12345; // this value is populated by ruby based upon current_user.id, change this value to reset localStorage if isFirstLogin is true
// jquery on ready function
$(function() {
    var $els = {};  // storage for our jQuery elements
    var tour; // variable that will become our tour
    var tourLocalStorage = JSON.parse(localStorage.getItem('myTour')) || {};

    function activate(){
        populateEls();
        setupTour();
        $els.tourAgain.on('click', tourAgain);
        // only check check if we should start the tour if this is the first time we've logged in
        if(isFirstLogin){
            // if we have a stored userID and its different from the one passed to us from ruby
            if(typeof tourLocalStorage.userID !== "undefined" && tourLocalStorage.userID !== userID){
                // reset the localStorage
                localStorage.removeItem('myTour');
                tourLocalStorage = {};
            }else if(typeof tourLocalStorage.userID === "undefined"){ // if we dont have a userID set, set it and save it to localStorage
                tourLocalStorage.userID = userID;
                localStorage.setItem('myTour', JSON.stringify(tourLocalStorage));
            }
            checkShouldStartTour();
        }
    }

    // helper function that creates a cache of our jQuery elements for faster lookup and less DOM traversal
    function populateEls(){
        $els.body = $('body');
        $els.document = $(document);
        $els.tourAgain = $('#tourAgain');
    }

    // creates and initialises a new tour
    function setupTour(){
        tour = new Tour({
            name: 'homepage', // unique identifier for each tour (used as key in localStorage)
            storage: false,
            backdrop: true,
            onStart: function() {
                tourHasBeenSeen(this.name);
                $els.body.addClass('is-touring');
            },
            onEnd: function() {
                console.log('ending tour');
                $els.body.removeClass('is-touring');
            },
            steps: [{
                element: "div.navbar-header img.navbar-brand",
                title: "Go Home",
                content: "Go home to the main page."
            }, {
                element: "div.navbar-header input#top-search",
                title: "Search",
                content: "Here you can search for players by their name, school, positions & bib color (that they wore in our tournament)"
            }, {
                element: "span.num-players",
                title: "Number of Players",
                content: "This is the number of players that are in our database for this Tournament"
            }, {
                element: '#page-wrapper div.contact-box.profile-24',
                title: "Player Info",
                content: "Here we have a quick snapshot of the player stats"
            }]
        });
        // Initialize the tour
        tour.init();
    }

    // function that checks if the current tour has already been taken, and starts it if not
    function checkShouldStartTour(){
        var tourName = tour._options.name;
        if(typeof tourLocalStorage[tourName] !== "undefined" && tourLocalStorage[tourName] === true){
            // if we have detected that the tour has already been taken, short circuit
            console.log('tour detected as having started previously');
            return;
        }else{
            console.log('tour starting');
            tour.start();
        }
    }

    // updates localStorage with the current tour's name to have a true value
    function tourHasBeenSeen(key){
        tourLocalStorage[key] = true;
        localStorage.setItem('myTour', JSON.stringify(tourLocalStorage));
    }

    function tourAgain(){
        // if you want to tour multiple pages again, clear our localStorage 
        localStorage.removeItem('myTour');
        // and if this is the first part of the tour, just continue below otherwise, send the user to the first page instead of using the function below
        // if you just want to tour this page again just do the following line
        tour.start();
    }

    activate();
});

PS. 我们不使用onEnd来触发tourHasBeenSeen函数的原因是,当前版本的bootstrap tour存在一个bug,即如果最后一步的元素不存在,则导游会在不触发onEnd回调的情况下结束,BUG


哇哦...感谢您的详细回答@haxxxton。"Preface 3"是什么?我刚刚意识到需要涵盖一个用例,即如果两个用户在同一台计算机上首次登录。想象一下办公室里有一台他们共享的计算机,或者他们使用别人的计算机(已经参加了旅游)首次登录。无论是否设置/创建了“localStorage”,我们如何确保在current_user.sign_in_count < 2的情况下执行导览? - marcamillion
@marcamillion,前言3涉及前言中提到的第三点“基于首次登录添加script值的能力”。您是否可以访问用户的“唯一”标识符?例如像userId这样的东西?您只需将该值作为localStorage的一部分包含在内,如果isFirstLogin为true,但userIdlocalStorage不匹配,则使用localStorage.removeItem('myTour');行清除localStorage :) - haxxxton
@marcamillion,如果您能告诉我是否有一个唯一标识符的访问权限,我可以更新我的代码以包含它 :) - haxxxton
你可以假设它是 current_user.id。那是我的 Rails 变量。不过我可以在 JS 中设置它。 - marcamillion
我还没有测试过这个,最近几天太疯狂了。但是基于答案的质量,我已经授予您奖励了。请问您能否定期回来查看一下,以防我在测试时有任何问题?我打算在这个周末尝试一下。谢谢! - marcamillion
显示剩余3条评论

6

您可以尝试使用JavaScript的sessionStorage,它会在用户关闭标签页时被删除,但在刷新后仍然存在。只需使用sessionStorage.setItem(key, value)sessionStorage.getItem(key)即可。请记住,sessionStorage只能存储字符串!


使用您的代码:

<script type="text/javascript">
  $(document).on('turbolinks:load', function() {
      var tour = new Tour({
        storage: false,
        backdrop: true,
        onStart: function(){
        $('body').addClass('is-touring');
        },
        onEnd: function(){
        $('body').removeClass('is-touring');
        },
        steps: [
        {
          element: "#navbar-logo",
          title: "Go Home",
          content: "All throughout the app, you can click our logo to get back to the main page."
        },
        {
          element: "input#top-search",
          title: "Search",
          content: "Here you can search for players by their name, school, positions & bib color (that they wore in our tournament)"
        }
      ]});
      if(sessionStorage.getItem("loggedIn") !== "yes"){//Remember that sessionStorage can only store strings!
        //Initialize the tour
        tour.init();
        // Start the tour
        tour.start();
      }
      else{
        //Set item "loggedIn" in sessionStorage to "yes"
        sessionStorage.putItem("loggedIn", "yes");
      }
      var goBackToTour = function(e){
        //You can also make a "fake" link, so that it looks like a link, but is not, and you don't have to put the following line:
        e.preventDefault();
        tour.init();
        tour.start();
      };
      document.getElementById("goBackToTourLink").addEventListener("click", goBackToTour);
  });
  //On the logout
  var logout = function(){
    sessionStorage.setItem("loggedIn", "no");
  };
</script>

你能否给出一个更完整的例子,以便于澄清问题中所提供的代码? - marcamillion
现在已经有了。 - MasterBob

4
您可以在cookie中存储用户是否看过导览的信息。您可以维护一个"TrackingCookie",其中包含所有用户跟踪信息(例如:已显示的导览、已显示的促销等),这些信息通过您的javascript代码访问。以下是维护所有此类跟踪信息的TrackingCookie代码。我称其为 tracking_cookie
可以使用cookies [:tracking_cookie]在服务器端访问cookie。

tracking_cookie.js

var TrackingCookie = (function() {
  function TrackingCookie() {
    this.name = 'tracking_cookie';
    this.expires = new Date(new Date().setYear(new Date().getFullYear() + 1));
  }

  TrackingCookie.prototype.set = function(name, value) {
    var data={};
    if(!this.readFromStore()) {
      data = this.readFromStore();
    }
    data[name] = value;
    return this.writeToStore(data);
  };

  TrackingCookie.prototype.set_if_unset = function(name, value) {
    if (!this.get(name)) {
      return this.set(name, value);
    }
  };

  TrackingCookie.prototype.get = function(name) {
    return this.readFromStore()[name];
  };

  TrackingCookie.prototype.writeToStore = function(data) {
    return $.cookie(this.name, JSON.stringify(data), {
      path: '/',
      expires: this.expires
    });
  };

  TrackingCookie.prototype.readFromStore = function() {
    return $.parseJSON($.cookie(this.name));
  };

  return TrackingCookie;

})();

在你的HTML中

<script type="text/javascript">
  $(document).on('turbolinks:load', function() {
    //Instantiate the cookie
    var tracking_cookie = new TrackingCookie();
    //Cookie value not set means, it is a new user.
    if(!tracking_cookie.get("tour_shown")){
      //Set the value to be true.
      tracking_cookie.set("tour_shown",true)
      var tour = new Tour({
        storage: false,
        backdrop: true,
        onStart: function(){
        $('body').addClass('is-touring');
        },
        onEnd: function(){
        $('body').removeClass('is-touring');
        },
        steps: [
        {
          element: "#navbar-logo",
          title: "Go Home",
          content: "All throughout the app, you can click our logo to get back to the main page."
        },
        {
          element: "input#top-search",
          title: "Search",
          content: "Here you can search for players by their name, school, positions & bib color (that they wore in our tournament)"
        }
      ]});

      // Initialize the tour
      tour.init();

      // Start the tour
      tour.start();
    };

  });
</script>

Cookie类太冗长了。你可以只使用$.cookie来实现简单的一次切换行为。上面的代码适用于所有首次使用的用户,无论是否登录或注销。如果只想让登录的用户使用它,请在服务器端的用户登录时设置标志。


这是一个有趣的方法,但对我来说感觉很笨重。 - marcamillion
我更喜欢其他人提出的本地存储解决方案。它更轻量级,实现起来更简单。不过还是谢谢你的努力。我已经为你的努力点赞了。 - marcamillion

4

使用本地存储:

  if (typeof(Storage) !== "undefined") {
    var takenTour = localStorage.getItem("takenTour");
    if (!takenTour) {
      localStorage.setItem("takenTour", true);
      // Take the tour
    }
  }

我们使用此解决方案是因为我们的用户不需要登录,而且它比使用cookies更轻便。正如上面提到的,当用户切换设备或清除缓存时,它无法工作,但您可以通过登录计数来处理这个问题。

我喜欢这种方法。我测试过了,似乎可以工作。所以唯一缺少的部分是,如何通过链接手动执行旅游? - marcamillion
@marcamillion - 我认为你已经准备好了所有的元素,可以从链接开始进行导览。你可以将初始化和启动导览的代码移动到一个单独的函数中,并在单击链接时调用它(以及首次登录)。$(“.tour-link”)。click(function(e){ 如果(tour){ tour.restart(); } else { initTour(); } }) - brenzy
在所有的答案中,我仍然认为使用本回答中描述的 localStorage 是最简洁、直接和前瞻性的。 - Phillip Chan

2
根据您的评论,我认为您希望在数据中跟踪此项内容(这实际上就是使用user.sign_in_count > 1检查所做的事情)。我的建议是使用像Redis这样的轻量级键值数据存储。

在这个模型中,每当用户访问具有此功能的页面时,您都需要在Redis中检查与该用户相关联的“visited”值。如果不存在,则触发JS事件并为该用户添加"visited": true到Redis中,这将防止JS在未来触发。


这差不多是我想要的,但感觉对我的需求来说有点过重。我只需要一些轻量级的JS代码,可以检测用户是否曾经加载过此页面,因此将检测推向客户端,而不是服务器端。 - marcamillion
我明白你的意思。问题是,仅使用前端解决方案无法保证其始终准确。毕竟,用户可能在第二次访问您的应用时使用完全不同的计算机。Redis(或类似类型的数据存储)实际上非常轻量级(通常用于服务器端缓存)。它的键值结构也使其相当灵活-如果事情发生变化,很容易进行操作。如果您真的想/需要仅使用前端,则利用localStorage / cookies将是最佳选择。 - jackel414
是的,这就是重点。如果用户再次登录,我不希望他们看到新手指引。他们只在第一次登录时才能看到它。如果他们想要在第二次登录后手动触发新手指南,那么他们可以这样做。但是,如果他们已经登录了多次,我不希望系统自动启动新手指引。使用浏览器中的localStorage是最轻量级的版本。 - marcamillion
是的,我们意见一致。我只想强调localStorage解决方案并不是一个保证可靠的方案。在许多情况下,即使是像用户第一次在Firefox中访问您的应用程序,第二次在Chrome中访问,仅依赖localStorage也无法正常工作。 - jackel414

2

本地存储不是跨浏览器的解决方案。尝试使用跨浏览器 SQL 实现,该实现使用不同的方法(包括 localstorage)来无限期地存储“数据库”在用户硬盘上。

var visited;
jSQL.load(function(){
  // create a table
  jSQL.query("create table if not exists visits (time date)").execute();

  // check if the user visited
  visited = jSQL.query("select * from visits").execute().fetchAll("ASSOC").length;

  // update the table so we know they visited already next time
  jSQL.query("insert into visits values (?)").execute([new Date()]);

  jSQL.persist();
});

请问您能否添加有关本地存储的文档,并说明它与哪些浏览器兼容/不兼容? - haxxxton

1
如果您想对页面进行门控,那么这应该是有效的。如果您需要更长时间防止重新执行,请考虑使用localStorage。
var triggered;
$(document).on('turbolinks:load', function() {
if (triggered === undefined) {
            triggered = "yes";
...code...
}}

我喜欢这个想法,但出于某种原因,它不起作用。当我重新加载页面时,它仍会触发 if (triggered == undefined)... 条件内的代码。 - marcamillion
我刚刚在变量初始化后,更新值后和每次重新加载页面时快速使用了console.logs,我看到triggered的两个值:undefined和“yes”。这表明triggered在页面重新加载时不会保留“yes”值。唯一不触发的时间是如果我返回,这让我相信turbolinks:load没有再次调用。但是一旦我硬刷新,它就会执行JS。 - marcamillion
@marcamillion 我已经成功地在 jQuery Mobile 中使用了那个门,但是在 Turbolinks 中则没有。两者都使用 AJAX 来更新页面主体,因此从目标而言它们相似,尽管实现不同。我的特定实现实际上是一个单页应用程序 (SPA),这可能会有所不同。顺便说一下,应该有三个等号,但这可能不会有影响。我只是不明白为什么它不起作用。如果它不起作用,你可以使用 localStorage,但如果没有在注销或类似的实现中清理它,就可能会出现意外的副作用,这可能会让事情变得混乱。 - Richard_G
是的,我认为这可能与TurboLinks有关。但我还不确定它是什么。我正在使用三个等号=== - marcamillion

1

你需要以某种方式与后端通信,以获取登录计数。可以通过注入变量或使用ajax命中json路由来实现逻辑:

if !session[:seen_tour] && current_user.sign_in_count == 1
  @show_tour = true
  session[:seen_tour] = true
else
  @show_tour = false
end

respond_to do |format|
  format.html {}
  format.json { render json: {show_tour: @show_tour } }
end

会话中的值将保持不变,无论您如何配置会话存储, 默认情况下,它存储在 cookie 中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接