【填坑】vue+webpack 升级后在原有项目上的适配问题

4,667 阅读10分钟

链接:https://juejin.cn/post/6844903513470795789

古人云:不作死就不会死

本人的项目是 vue + webpack,vue单文件中使用了 Jade 模板与 less 预编译器

起因是因为谈论到 Jade 模板问题,Jade 早已改名为 Pug,并且发布了2.0版本,想着不如升级了吧,顺便把 webpack 与 vue 也一并升级了,事实证明,升级需谨慎 = =

首先之前的版本如下:

"vue": "2.4.2"
"webpack": "2.7.0"

升级后的版本为:

"vue": "2.5.7"
"webpack": "3.8.1"

运行 npm update ,一切正常,然而运行 vue 项目就开始报错了

// webpack 报错
(node:45948) DeprecationWarning: loaderUtils.parseQuery() received a non-string value which can be problematic, see https://github.com/webpack/loader-utils/issues/56
parseQuery() will be replaced with getOptions() in the next major version of loader-utils.

// vue报错
// 略过一些重复的报错
warning  in ./src/pages/List.vue

(Emitted value instead of an instance of Error) the "scope" attribute for scoped slots have been deprecated and replaced by "slot-scope" since 2.5. The new "slot-scope" attribute can also be used on plain elements in addition to <template> to denote scoped slots.

 @ ./src/pages/List.vue 9:2-305
 @ ./src/router/index.js
 @ ./src/main.js

我们一个个的分析报错的原因并改正

1. loaderUtils.parseQuery()

node 中的报错很不友好,光从报错中很难发现出问题的地方在哪,我们甚至不知道这是 webpack 的报错还是哪个模块的错误,抑或是自己代码的错误,只能通过错误信息中找线索,第一行的报错信息中可以提取出如下信息

  • loaderUtils.parseQuery() 方法引起的错误
  • 有一个 issue 链接,指向了 webpack
  • 关键字:loader-utils

可以说是一个 webpack 的方法更新引起的错误,且和 loader 相关

解决方案:

这是由于webpack的一个loader相关的公用方法更新导致的,因此需要更新所有的相应的loader,如果你的项目下有

  • css-loader
  • file-loader
  • vue-loader
  • less-loader
  • eslint-loader
  • url-loader
    ......

全部更新到最新版就可以了

不过先别急,即使如此仍会有遗漏,因此我们还需要在node_module文件中查找使用到loaderUtils.parseQuery方法的模块,也同样更新到最新版,例如下图中的html-webpack-plugin模块

html-webpack-plugin 这个模块也用到了相应的方法,因此需要更新到最新版本
html-webpack-plugin 这个模块也用到了相应的方法,因此需要更新到最新版本

如果你还想看一下具体的原因,可以继续看下去

我们可以先看一下报错中提到的 issue

The deprecation warning is for webpack loader authors, because calling parseQuery with a non-string value can have bad side-effects (see below). If you're a webpack user, you can set process.traceDeprecation = true in your webpack.config.js to find out which loader is causing this deprecation warning. Please file an issue in the loader's repository.
Starting with webpack 2, it is also possible to pass an object reference from the webpack.config.js to the loader. This is a lot more straight-forward and it allows the use of non-stringifyable values like functions.
For compatibility reasons, this object is still read from the loader context via this.query. Thus, this.query can either be the options object from the webpack.config.js or an actual loader query string. The current implementation of parseQuery just returns this.query if it's not a string.
Unfortunately, this leads to a situation where the loader re-uses the options object across multiple loader invocations. This can be a problem if the loader modifies the object (for instance, to add sane defaults). See jtangelder/sass-loader#368 (comment).
Modifying the object was totally fine with webpack 1 since this.query was always a string. Thus, parseQuery would always return a fresh object.
Starting with the next major version of loader-utils, we will unify the way of reading the loader options:
  • We will remove parseQuery and getLoaderConfig
  • We will add getOptions as the only way to get the loader options. It will look like this:
const options = loaderUtils.getOptions(this);
The returned options object is read-only, you should not modify it. This is because getOptions can not make assumptions on how to clone the object correctly.

这个报错是给那些写loader模块的人看的(一个微笑的表情.jpg),在webpack2中,可以在配置里传递一个对象(或者function)给loader,由于一些原因,现在是通过query这个变量传给loader的上下文的

现在,query这个变量有了多重意思(即是一个options对象,又是一个查询参数),当一个场景用了多个loader的时候(例如),这个对象可能会被其中一个loader修改

webpack1的时候因为querystring类型的所以没问题

为了避免不可预知的情况,所以做了如下修改

  • 删了parseQuerygetLoaderConfig方法
  • 加了getOptions方法去读取loaderoptions对象,且这个方法返回值是只读的,不能修改

因此,当你升级了webpack后,就需要配套的升级相应的所有loader,以及所有用到该方法的模块,还真是强制性的呢...

不过如果你的webpack.config.js中没有用到多个loader,且query只是用于查询参数的话,基本上只会提示错误,但还是能正常编译的

原文还好心的提醒了我们一下如何调试 webpack 以查明是哪个 loader 导致的报错,可以在 webpack.config.js 中设置 process.traceDeprecation = true

2. vue - scope

可能很少人会遇到这个错误,这是由于2.5版本以后 vue 对作用域插槽的使用方式更新造成的,错误信息里说的也比较直观

官方文档说明:作用域插槽

解决方案

将所有用到作用域插槽的地方,把属性名 scope 换成 slot-scope 即可

那么为何要用作用域插槽呢,感兴趣的朋友可以继续读下去

在之前(version >= 2.1),如果我们想写一个通用的列表组件,同时想让不同列表中每个子元素有自己独特的交互,该怎么做呢

我们可以写一个列表组件,写两个子元素组件,通过作用域插槽将这些组件组合在一起,即实现了列表的一致性,又解耦了列表与列表内元素的耦合性,举一个很简单的例子

我们先写一个 List 组件,它的样式是一个 ul 无序列表,且有一个外层 border

[ html ]
<div class="list">
  <ul>
    <li v-for="item in items"> {{item}} </li>
  </ul>
</div>

[ js ]
const List = {
  props: {
    items: {
      type: Array,
      default: []
    }
  }
}

// 调用
<list :items="['a', 'b', 'c', 'd', 'e']"></list>    


如果我们有个新需求,这时候需要展示一个列表A,这个列表的每一行都需要添加一个 title ,该怎么做

  • 新写一个 List 组件,显然不太合适,因为只有一点变化但要复制整个组件,失去了组件的意义
  • 加一个判断条件 isA 来控制 item 的展示方式,简单来说是可以的,但是如果以后 item 的种类越来越多,交互也越来越骚怎么办(例如加一个动画效果),如果都放在 List 组件里,那这个组件可以预计的代码将突破上千行,每调整一处甚至需要考虑其他列表的兼容,pass
  • 让每个 item 抽离出来,独立的控制自己的交互与样式,List 组件控制自己的样式,不负责 item 的交互,这个看起来很美好,幸运的是,vue 也提供了同样美好的使用方式

接下来我们修改一下上面的代码,首先修改 List 组件,将原本的 li 标签改成一个 slot

[ html ] 原来的 li 标签可以保留,当作一个默认样式
<div class="list">
  <ul>
    <slot name="item" v-for="item in items" :item="item">
      <li> {{item}} </li>
    </slot>
  </ul>
</div>

添加一个 Item 组件

[ html ] 这个 li 标签与默认的不同,在每个 item 的前面加了一句描述
<li> 这是A列表:{{ item }} </li>

[ js ]
const ItemA = {
  props: {
    item: {
      type: Number,
      default: 0
    }
  }
}

最后要怎么使用呢,如果是 version >= 2.1 的情况下

<list :items="[0, 1, 2, 3, 4]">
  <template slot="item" scope="props">
    <item-a :item="props.item"></item-a>
  </template>
</list>


可以看到,列表的样式按照 Item 的自定义样式展示出来了,每一行都加了一个通用的描述

在 vue 的 version >= 2.5 后,使用的方式略微有些变化,但是更加直观,去掉了中间的 template 组件,并且把属性名 scope 改为了 slot-scope

<list :items="[0, 1, 2, 3, 4]">
  <item-a slot="item" slot-scope="props" :item="props.item"></item-a>
</list>

这也是这个报错所提示的地方

以上代码的 demo 可以参见 这里

3. 番外篇:vue-loader 的报错

当你以为大功告成的时候,代码总会给你点惊喜

经过了以上的修改,编译成功了,也不报错了,以为可以安心的继续之前的工作的时候,打开浏览器看到的却是一个新的报错

[Vue warn]: Failed to mount component: template or render function not defined.
found in
---> <Anonymous>
       <App> at src\App.vue
         <Root>
         ......

WTF?这个提示直译过来就是加载不了组件: 找不到模板或者没有 render

然而代码没有变过,组件也确实引入了,于是百度大法、StackOverflow大法、GitHub issue大法,不论怎么搜最终都是一个解决方案,甚至 vue 官方也写了这个方案,那就是

在 webpack.config.js 配置中添加一个别名,见 vue官方文档:运行时-编译器-vs-只包含运行时

module.exports = {
  // ...
  resolve: {
    alias: {
      'vue$': 'vue/dist/vue.esm.js' // 'vue/dist/vue.common.js' for webpack 1
    }
  }
}

加了以后会发现没起作用,为什么呢,仔细一想,这个是因为在运行的时候有动态编译的模板,需要加入编译器才做的配置,而我的项目中并没有需要在运行的时候编译的模板,很明显是别的原因

最终在 vue-loader 的更新文档中找到了原因

先说解决方案

  • 如果有通过 require 引入组件的话,全部改为 require(xxx).default
  • 如果有异步引入组件的话,全部更新为动态 import 方式,() => import(xxx)

具体说明

在引入组件的时候,除了ES6的 import 之外,还可以用 webpackrequire,比如,在我的路由配置里,就写了大量的如下代码

routes: [
    {
      path: '/',
      name: 'home',
      component: require('src/pages/Home.vue')
    }
    ...
]

在旧版本里,这是ok的,可以引入组件,但是最新版的 vue-loader 中,默认开启了 ES modules 模块,于是,要配合新的语法进行一些代码上的更改

我们来看下更新文档的说明 releases#v13.0.0

New

  • Now uses ES modules internally to take advantage of webpack 3 scope hoisting. This should result in smaller bundle sizes.
  • Now uses PostCSS@6.

Breaking Changes

  • The esModule option is now true by default, because this is necessary for ES-module-based scope hoisting to work. This means the export from a *.vue file is now an ES module by default, so async components via dynamic import like this will break:
    const Foo = () => import('./Foo.vue')
    Note: the above can continue to work with Vue 2.4 + vue-router 2.7, which will automatically resolve ES modules' default exports when dealing with async components. In earlier versions of Vue and vue-router you will have to do this:
    const Foo = () => import('./Foo.vue').then(m => m.default)
    Alternatively, you can turn off the new behavior by explicitly using esModule: false in vue-loader options.
    Similarly, old CommonJS-style requires will also need to be updated:
    // before
    const Foo = require('./Foo.vue')
    // after
    const Foo = require('./Foo.vue').default
  • PostCSS 6 might break old PostCSS plugins that haven't been updated to work with it yet.

文档里说的很清楚了

  • 可以在 vue-loaderoptions 里通过 esModule: false 配置来关闭 ES 模块

或者

  • 同步引入组件,正常用 import,而原来使用 require 引入 ES6 语法的文件(例如:export default {...}),现在需要多加一个 default 属性来引用
  • 异步引入组件,需要用动态 import 语法

    注:如果使用的是 Babel,需要添加 syntax-dynamic-import 插件,才能使 Babel 可以正确地解析语法。

结语

至此,此次升级的坑全部填完,还是要吐槽一下,没事千万不要随便升级,虽然可以使用新的功能,前提是你有时间踩坑且短时间内能将代码修复,否则还是乖乖保持原样吧

最后,我个人建议,在 package.json 里最好将版本写死,或者写成 ~2.5.7 的形式
而默认的写法 ^2.5.7 会自动下载 2.x.x 版本的模块,如果有新员工加入,很有可能装了最新的包,导致他的环境与你的或者线上的有很大出入

谢谢各位观看,如果能解决你目前遇到的问题,那对我来说就是最大的欣慰了


作者:lain_lee
链接:https://juejin.cn/post/6844903513470795789
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。