【填坑】vue+webpack 升級後在原有專案上的適配問題

lain_lee發表於2017-11-23

古人云:不作死就不會死

本人的專案是 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 的自定義樣式展示出來了,每一行都加了一個通用的描述

vueversion >= 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 版本的模組,如果有新員工加入,很有可能裝了最新的包,導致他的環境與你的或者線上的有很大出入

謝謝各位觀看,如果能解決你目前遇到的問題,那對我來說就是最大的欣慰了

相關文章