Laravel 6 - laravel/ui 中的 vue 与 jquery

Laravel 6 - laravel/ui 中的 vue 与 jquery

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实例。

Leave a Reply

Your email address will not be published. Required fields are marked *

TOC