Laravel 应该是在5.几之后吧,其laravel/ui项目的前端框架默认使用Bootstrap与Vue。
1. 无法打印vue实例
对于vue官方文档中的例子,我们都可以在浏览器console窗口通过app变量打印出vue实例。
但是在laravel/ui的页面中,我们打印app变量时候,输出的却是id=”app”的那个dom元素,并不是在/resources/js/app.js中定义的vue实例。
const app = new Vue({ el: '#app', });
原因是laravel mix在编译打包js文件的时候,会将每个js源文件作为一个独立的作用域,这样可以使不同js文件中的变量互不干扰。而这个app变量(打包时候还会将const变成var。。。=。=# js真是迷)的作用域就只在该/resources/js/app.js文件中。在其他地方是无法读取该变量的。
我们可以看一下laravel mix打包后生成的/pulbic/js/app.js文件,整个/resources/js/app.js源文件的代码(包括app变量)都被function(module, exports, __webpack_require__) 这个匿名函数包裹起来,所以app变量只作为该匿名函数的一个局部变量。
/*!*****************************!*\ !*** ./resources/js/app.js ***! \*****************************/ /***/ (function(module, exports, __webpack_require__) { /** * ... */ var app = new Vue({ el: '#app' }); /***/ }),
一个题外话,关于webpack打包的基本原理。(laravel-mix就是基于webpack的封装)
webpack在打包项目中的js源文件,最终生成一个app.js的时候。我们来看一下最终的app.js的简化结构:
(function(modules) { // 这是一个立即执行函数(IIFE)的形式 对于modules实参数组中的每个module: 执行module源代码 // 一个module就代表一个被打包的js源文件 })({"module_filename_1": (function() { module_1 源码 }), "module_filename_2": (function() { module_2 源码 }) // 这就是modules的实参数组,每个js源文件的代码都被包裹在闭包中 });这样子,每个js源文件就是一个module,用闭包将它们的作用域隔离开。然后通过IIFE的形式立即执行所有module闭包,即执行每个js源文件。
如果非要在全局范围使用该app变量,可以在/resources/js/app.js源文件中将该变量定义为window全局对象的一个属性:
window.app = new Vue({ el: '#app', });
这样在浏览器的console窗口打印app时候,就能打印出该vue实例了。
2. 无法立即使用jquery
我们来看一下laravel/ui是如何引入jquery的,在/resources/js/app.js中,通过require(‘./bootstrap’); 引入同目录下的bootstrap.js文件:
try { window.Popper = require('popper.js').default; // bootstrap的tooltips与popovers组件需要popper.js window.$ = window.jQuery = require('jquery'); // 导入并注册jquery全局变量 require('bootstrap'); // 导入bootstrap } catch (e) {}
所以我们可以看到,它已经将jQuery以及$符号注册到window全局对象中。理论上我们就可以直接在前端页面的代码中直接使用$符号了:
<script type="application/javascript"> console.log($('.container')); </script>
但是实际上它却报错了:Uncaught ReferenceError: $ is not defined。$未定义?它不是已经在/resources/js/bootstrap.js中注册了$符号了嘛。而如果我们将前端页面的这个代码修改一下:
<script type="application/javascript"> window.onload = function() { console.log($('.container')); } // console.log($('.container')); </script>
在window.onload事件函数中使用这个$符号却是可行的。
这其实是由于laravel/ui前端模版页面app.blade.php在引入app.js文件的<script>标签中,使用了defer属性:<script src=”{{ asset(‘js/app.js’) }}” defer></script> 。defer的意思是告诉浏览器将该脚本文件异步下载并延迟执行。
关于浏览器对JavaScript文件的下载执行顺序,参考该讨论:https://segmentfault.com/q/1010000000640869
关于原生的 window.onload 、DOMContentLoaded 与 jquery的 $(document).ready 之间的区别,参考:https://api.jquery.com/ready/。(实际上去看jquery的源码会发现,jQuery.ready就是由DOMContentLoaded触发的)
总结一下js代码在浏览器中的执行顺序:
由于defer属性,使得当浏览器解析并执行console.log($(‘.container’)); 这一句的时候,app.js文件还没被执行,jquery还没被引入。所以这时候“$ is not defined”。如果把这一句放在 window.onload 事件函数中,window.onload事件是在所有资源(包括图片、css、script)都加载完后才触发的,此时app.js已经下载并执行了。那么这时候再调用$符号就是OK的。
2.1 如何补救
a. 取消defer加载,并将<script>标签移到html文件尾
既然如上面所诉,那么我们把laravel/ui前端模版页面中<script src=’app.js’>标签的defer属性去掉会怎么样呢?<script src=”{{ asset(‘js/app.js’) }}” ></script>
[Vue warn]: Cannot find element: #app
可以在浏览器的控制台窗口看到它报错了!jquery可以用了,却引发了vue的错误。
原因是在laravel/ui的前端模版app.blade.php中,这个<script>标签是放在文件头的,浏览器解析到这里后就会“阻塞”地去下载这个app.js文件并执行。而在app.js文件中,它创建了一个vue实例,并绑定到id为 ‘#app’ 的这个<div>元素。但此时浏览器还没解析到前端页面的<div id=”app”> 这一句,所以此时是找不到 ‘#app’ 这个元素的。
所以,在去掉defer属性的同时,我们还需要将这个<script>标签移动到html文件的尾部。比如将app.blade.php修改成如下样式:
... ... <script src="{{ asset('js/app.js') }}"></script> @yield('js') // 后续继承这个app.blade的模版页都可以在这个section('js')中引入JavaScript // 在这个section('js')中就能直接使用jquery了。 </body> </html>
ps:顺便吐槽一下,我猜测Laravel之所以把app.js文件放在html头部使用defer属性加载。是因为所谓的“前端工程化”(我觉得应该叫“前端复杂化”)后,会导致打包出来的app.js文件特别大,所以它必须让浏览器在解析文件头时就开始异步下载app.js文件。
b. 使用DOMContentLoaded事件
如果不改动默认的<script src=”app.js”>标签的话,上面我们已经试过可以在window.onload事件函数中直接使用jquery。但windows.onload事件的触发时间太滞后了,我们可以采用DOMContentLoaded事件。在DOMContentLoaded事件触发之前,<script src=”app.js” defer>标签就已经下载并执行完了,app.js中的jquery注册已经完成。所以在DOMContentLoaded事件函数中就可以直接使用jquery了。
<script type="application/javascript"> document.addEventListener("DOMContentLoaded", function(event) { console.log($('.container')); }); </script>
c. 从app.js中剥离jquery
更彻底的解决方案就是不使用 laravel/ui 默认的将vue与jquery、bootstrap统一打包进app.js这种方法,可以将二者分开打包。
或者再简单一点,对于我这种前端苦手,由于比较少用,可以把app = new Vue({}); 这一块从app.js文件中删掉,哪个页面要用vue了,再单独在那个页面创建这个vue实例。