Rails 5/6:如何使用webpacker包含JS函数?

18

我正在尝试将一个Rails 3应用程序升级到Rails 6,但由于现在默认使用webpacker,我的JavaScript函数无法访问,我遇到了问题。

我收到以下错误:ReferenceError: Can't find variable: functionName,这是针对所有js函数触发器的。

我所做的是:

  • create an app_directory in /app/javascript
  • copied my development javascript file into the app_directory and renamed it to index.js
  • added console.log('Hello World from Webpacker'); to index.js
  • added import "app_directory"; to /app/javascript/packs/application.js
  • added to /config/initializers/content_security_policy.rb:

    Rails.application.config.content_security_policy do |policy|
      policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development?
    end
    
我在控制台中打印出了“Hello World from Webpacker”,但是当我尝试通过浏览器中的<div id="x" onclick="functionX()"></div>访问一个简单的JS函数时,我遇到了引用错误。
我知道资产管道已被Webpacker替换,这对于包括模块应该很好,但是我应该如何包含简单的JS函数?我错过了什么?
提前感谢你!

1
当你说你正在添加一个名为index.js的文件,你是在添加你的应用程序的哪个部分?在哪个目录下? - Mark
@Mark,我正在使用目前貌似在app/assets中没有默认JS目录和在application.html.erb中没有JavaScript include标签的Rails 6 pre版进行开发。我已经找到了重新创建资产管道位置的方法,但是当R6准备就绪时它是否能正常工作还存在疑惑... - SEJU
本文 https://blog.capsens.eu/how-to-write-javascript-in-rails-6-webpacker-yarn-and-sprockets-cdf990387463 解释了为什么这个问题基于错误的假设,如何使用webpacker以及为什么像sprockets一样使用它是行不通的。 - ToTenMilan
4个回答

23

有关如何从旧的 asset pipeline 迁移到新的 webpacker 的指南,请参见此处:

https://www.calleerlandsson.com/replacing-sprockets-with-webpacker-for-javascript-in-rails-5-2/

这是一个如何从 Rails 5.2 中的 asset pipeline 迁移到 webpacker 的指南,并且它会让你了解到在 Rails 6 中,由于 webpacker 是 javascript 的默认值,事情发生了什么变化。 特别地:

现在是时候将所有应用程序 JavaScript 代码从 app/assets/javascripts/ 移动到 app/javascript/。

要在 JavaScript pack 中包含它们,请确保在 app/javascript/pack/application.js 中引用它们:

require('your_js_file')
因此,请在app/javascript/hello.js中创建一个文件,内容如下:
console.log("Hello from hello.js");
然后,在app/javascript/packs/application.js中添加这行代码:
require("hello")

(请注意,扩展名不需要)

现在,您可以在浏览器控制台打开的情况下加载页面,并在控制台中看到“ Hello!”消息。只需添加您需要的内容到app / javascript目录,或者最好创建子目录以保持代码组织。


更多信息:

这个问题是有诅咒的。曾经被接受的答案不仅错误而且非常错误,而最受欢迎的答案仍然没有达到标准。

anode84仍在尝试以旧的方式处理事情,如果您尝试,则webpacker会妨碍您。当您将JavaScript移动到webpacker时,必须完全改变JavaScript的使用方式和思考方式。默认情况下,没有任何内容是全局的。

我理解这很令人沮丧。您可能和我一样习惯于在JavaScript文件中声明函数,然后在HTML文件中调用它。或者只需在HTML文件末尾添加一些JavaScript。我从1994年开始做Web编程(不是错别字),因此我已经看过多次演变。JavaScript已经发展了。您必须学习新的做事方式。

如果要向表单或其他内容添加操作,则可以创建一个在app/javascript中执行所需操作的文件。要将数据传递给它,可以使用数据属性、隐藏字段等。如果字段不存在,则代码不运行。

这是一个您可能发现有用的示例。如果表单具有Google reCAPTCHA并且用户在提交表单时没有勾选框,则我使用此功能显示弹出窗口:

// For any form, on submit find out if there's a recaptcha
// field on the form, and if so, make sure the recaptcha
// was completed before submission.
document.addEventListener("turbolinks:load", function() {
  document.querySelectorAll('form').forEach(function(form) {
    form.addEventListener('submit', function(event) {
      const response_field = document.getElementById('g-recaptcha-response');
      // This ensures that the response field is part of the form
      if (response_field && form.compareDocumentPosition(response_field) & 16) {
        if (response_field.value == '') {
          alert("Please verify that you are not a robot.");
          event.preventDefault();
          event.stopPropagation();
          return false;
        }
      }
    });
  });
});

请注意,这是自包含的。它不依赖于任何其他模块,也没有其他东西依赖于它。您只需在您的包中引用它即可监控所有表单提交。

以下是在页面加载时加载带有 GeoJSON 叠加层的 Google 地图的另一个示例:

document.addEventListener("turbolinks:load", function() {
  document.querySelectorAll('.shuttle-route-version-map').forEach(function(map_div) {
    let shuttle_route_version_id = map_div.dataset.shuttleRouteVersionId;
    let geojson_field = document.querySelector(`input[type=hidden][name="geojson[${shuttle_route_version_id}]"]`);

    var map = null;

    let center = {lat: 36.1638726, lng: -86.7742864};
    map = new google.maps.Map(map_div, {
      zoom: 15.18,
      center: center
    });

    map.data.addGeoJson(JSON.parse(geojson_field.value));

    var bounds = new google.maps.LatLngBounds();
    map.data.forEach(function(data_feature) {
      let geom = data_feature.getGeometry();
      geom.forEachLatLng(function(latlng) {
        bounds.extend(latlng);
      });
    });
    map.setCenter(bounds.getCenter());
    map.fitBounds(bounds); 
  });
});
当页面加载时,我查找类名为“shuttle-route-version-map”的div。对于每个我找到的div,数据属性“shuttleRouteVersionId”(data-shuttle-route-version-id)包含路线的ID。我已经将geojson存储在一个隐藏的字段中,可以根据该ID轻松查询,然后初始化地图,添加geojson,然后根据该数据设置地图中心和边界。再次说明,它是自包含的,除了谷歌地图功能之外。
你还可以学习如何使用导入/导出来共享代码,这真的很强大。
所以,再来展示如何使用导入/导出。以下是一段简单的代码,用于设置“观察者”以监视您的位置:
var driver_position_watch_id = null;

export const watch_position = function(logging_callback) {
  var last_timestamp = null;

  function success(pos) {
    if (pos.timestamp != last_timestamp) {
      logging_callback(pos);
    }
    last_timestamp = pos.timestamp;
  }

  function error(err) {
    console.log('Error: ' + err.code + ': ' + err.message);
    if (err.code == 3) {
      // timeout, let's try again in a second
      setTimeout(start_watching, 1000);
    }
  }

  let options = {
    enableHighAccuracy: true,
    timeout: 15000, 
    maximumAge: 14500
  };

  function start_watching() {
    if (driver_position_watch_id) stop_watching_position();
    driver_position_watch_id = navigator.geolocation.watchPosition(success, error, options);
    console.log("Start watching location updates: " + driver_position_watch_id);  
  }

  start_watching();
}

export const stop_watching_position = function() {
  if (driver_position_watch_id) {
    console.log("Stopped watching location updates: " + driver_position_watch_id);
    navigator.geolocation.clearWatch(driver_position_watch_id);
    driver_position_watch_id = null;
  }
}

这导出了两个函数:"watch_position"和"stop_watching_position"。要使用它,您需要在另一个文件中导入这些函数。

import { watch_position, stop_watching_position } from 'watch_location';

document.addEventListener("turbolinks:load", function() {
  let lat_input = document.getElementById('driver_location_check_latitude');
  let long_input = document.getElementById('driver_location_check_longitude');

  if (lat_input && long_input) {
    watch_position(function(pos) {
      lat_input.value = pos.coords.latitude;
      long_input.value = pos.coords.longitude;
    });
  }
});

当页面加载时,我们查找名为“driver_location_check_latitude”和“driver_location_check_longitude”的字段。如果它们存在,我们设置一个带有回调函数的观察器,并且当它们改变时,回调填充这些字段的纬度和经度值。这就是如何在模块之间共享代码。

因此,这是一种非常不同的做事方式。当模块化并正确组织代码时,您的代码更加清晰和可预测。

这是未来的趋势,所以抵制它(并设置“window.function_name”就是抵制它)是无济于事的。


嘿@MichaelChaney,-所以我们在函数声明前面加上export,在application.js中要求我们的文件...但是我们应该把import语句放在哪里? - Ben
1
谢谢 @MichaelChaney!我很感激。 - Matias Carpintini
1
将这个答案的核心提炼出来,为那些发现很难理解的人(包括我自己):不要在你的HTML中引用一些JS函数,而是在你的JS中添加一个turbolinks加载回调(基本上是说,嘿,页面已经加载完毕并准备好了),在该回调中查询你的元素(querySelector,getElementById等),并在那里添加一个点击监听器。 - nepps
1
@MichaelChaney 这个答案真是救了我一命!随着Javascript的发展,我一直在艰难地摸索,而且我承认我有点懒,因为我不喜欢这门语言。当我转向Webpacker时,我无法理解为什么会有那么多问题出现,以及为什么那么多安装教程都将模块分配给窗口。这绝对是一个改变游戏规则的东西。我会花更多时间学习,确保我已经掌握了它。 - Michael Glenn
1
非常好的答案,我不得不花费数小时在随机垃圾中寻找这颗宝石。感谢您的帮助。 - ricks
显示剩余15条评论

13

观察webpacker如何“打包”js文件和函数:

/***/ "./app/javascript/dashboard/project.js":
/*! no static exports found */
/***/ (function(module, exports) {

  function myFunction() {...}

因此,webpacker会将这些函数存储在另一个函数中,使其无法访问。不确定为什么会这样,也不知道如何正确解决。

不过,确实有一种解决方法,你可以:

1)将函数签名从以下形式进行更改:

function myFunction() { ... }

至:

window.myFunction = function() { ... }

2) 保留函数签名不变,但仍需像这里所示添加对它们的引用:window.myFunction = myFunction

这将使得你的函数可以从"window"对象中全局访问。


3
由于webpacker鼓励使用新的模块功能,以避免名称冲突等问题,因此这是一个有意为之的设计。Chaney的回复解释了如何正确实现这一点。 - Peter Gerdes
2
请不要使用这个解决方案,因为您不想阅读Chaney的答案。如果您想正确地完成它,请花几分钟理解Chaney的答案。 - ricks

-1

请将您自定义的Java脚本文件中的代码 替换为

function function_name() {// body //}

window.function_name = function() {// body //}

为什么人们应该这样做? - Sebastián Palma
@SebastianPalma 他们不应该这样做。请看我的正确答案。我不知道为什么有人会重复已经存在的错误答案。 - Michael Chaney
我认为官方文档没有根据Rails 6中使用的webpacker进行更新。 - Hassam Saeed
@HassamSaeed 1. 这不仅是“我的方法”,而是“正确的方法”。2. 我在这里有一个答案,解释了这个问题,当你两天前来到这里复制/粘贴错误答案时,它已经被标记为接受。你是正确的,官方文档已经过时了。 - Michael Chaney
1
@HassamSaeed,你的答案是错误的。请阅读我上面的完整答案。你可能需要反复阅读才能理解它(我肯定需要)。我将回答你在上面链接的问题。 - Michael Chaney
显示剩余5条评论

-4

来自官方Rails应用程序指南:

在哪里放置JavaScript

无论您使用Rails资产管道还是直接向视图添加标签,您都必须选择将任何本地JavaScript文件放在哪里。

我们有三个位置可以放置本地JavaScript文件:

app/assets/javascripts文件夹,lib/assets/javascripts文件夹和vendor/assets/javascripts文件夹

以下是选择脚本位置的准则:

对于为应用程序创建的JavaScript,请使用app/assets/javascripts。

对于许多应用程序共享的脚本,请使用lib/assets/javascripts(但如果可以请使用gem)。

对于从其他开发人员处复制的jQuery插件等,请使用vendor/assets/javascripts。在最简单的情况下,当所有JavaScript文件都在app/assets/javascripts文件夹中时,您不需要做任何其他事情。

将JavaScript文件添加到任何其他位置,您将需要了解如何修改清单文件。

更多阅读材料: http://railsapps.github.io/rails-javascript-include-external.html


非常感谢您的回答。我正在使用Rails 6.0.0 pre版本,由于默认情况下在资产中没有JavaScript目录,因此我认为他们将默认位置移动到了应用程序目录中的JavaScript目录,但我想那个目录实际上是为JavaScript框架驱动的站点或节点、npm模块而设立的。我重新创建了资产+application.js中的JS目录,现在它可以运行了。我仍然很好奇,在Rails 6中是否可以将标准JS文件打包到新的JS目录中...我能够记录到控制台,但无法调用函数... - SEJU
1
是的,在6.0中,似乎JavaScript不再保存在资产文件夹中,而是采用了新的格式。 - Corey Gibson
3
请在发布此类内容时始终仔细查看。从链接页面顶部可以看到:“最后更新于2012年12月31日”。那是在Rails 3时期的更新。不确定原询问者是否可以收回绿色勾选,但这对于Rails 6来说并不是正确答案。 - Michael Chaney
我提供的文本来自官方的Rails指南,适用于最新版本的Rails。链接提供了进一步的解释。 - Mark
2
Rails 6 不会 使用资产管道来处理JavaScript。问题明确要求关于Rails 6的情况,因此这是不正确的。不幸的是,即使是边缘指南,Rails 6的一些指南也有点过时了。 - Michael Chaney
要回复@CoreyGibson,您需要将您的JavaScript放在app/javascript/packs中,并让webpacker处理它。请注意,这是一种与资产管道非常不同的处理JavaScript的方式。这里还有其他正确的答案。 - Michael Chaney

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