⚡️向 Vue3 进发,老项目升级 Vue2.7 之 Naruto 踩坑总结

前言

Vue3 发布置为默认版本也有很长一段时间了,但是公司大多 B 端项目跟周边相关库仍在使用 Vue2,而由于项目过于庞大和迭代更新得非常频繁,公司考虑到没有足够的时间做 Vue3 的升级和给予升级版本足够的测试资源与时间,导致不得不继续使用 Vue2。但是在日常学习和新项目中尝试到了 Vue3 的各种“真香”特性,难道旧项目只能眼巴巴看着,继续像以前那样带上痛苦面具维护吗?当然不是,官方也考虑到这点,在 2022.7.1 正式发布了 Vue2.7,该版本从 Vue3 中向后移植了一些最重要的功能,让 Vue2 的用户和项目也能从中受益。

本文着重关注从 2.6 升级到 2.7 过程中遇到的问题与解决思路,在此我就不搬运 2.7 相关内容了,详情以官方发布的 Vue2.7 为准,可前往 Vue 2.7 "Naruto" Released 查阅

升级

Vue CLI

如果项目是通过官方脚手架生成的,那么需要把本地的 @vue/cli-xxx 依赖都需要升级到主版本下的最新版本

  • cli 4:~4.5.18
  • cli 5:~5.0.6

Webpack

由于老项目以前是基于 webpack 从零搭建的,所以可以略过上述 cli 的步骤,这里以我们项目为例,列出所需升级的部分(看项目引用的依赖做出调整,有的依赖会有其他问题,需要小伙伴们自行根据公司项目情况来解决)

vue 升级到 2.7

"dependencies": {
    // "vue": "2.6.12"
    "vue": "^2.7.0"
}
复制代码

Vue2.7 中能支持使用的 Vue3 特性可以在其声明文件:node_modules\vue\types\v3-generated.d.ts 中查看

移除不需要的 vue-template-compiler

删除后出现报错,原因是旧版 vue-loader 解析的时候使用了 @vue/component-compiler-utils,其内部调用 compiler.parse 的时候找不到之前 vue-template-compiler 提供的 parseComponent 方法

TypeErrorCannot read property 'parseComponent' of undefined
复制代码

vue-loader 升级到 15.10.0

"devDependencies": {
    //"vue-loader": "^15.7.0"
    "vue-loader": "^15.10.0"
}
复制代码

旧版的是通过 vue-template-compiler 对 sfc 进行编译

// node_modules\vue-loader\lib\index.js
function loadTemplateCompiler (loaderContext) {
  try {
    return require('vue-template-compiler')
  } catch (e) {
    if (/version mismatch/.test(e.toString())) {
      loaderContext.emitError(e)
    } else {
      loaderContext.emitError(new Error(
        `[vue-loader] vue-template-compiler must be installed as a peer dependency, ` +
        `or a compatible compiler implementation must be passed via options.`
      ))
    }
  }
}
复制代码

Vue2.7 指定的 [email protected] 则是对 vue 版本进行判断,然后通过 vue/compiler-sfc 对 2.7 的 sfc 进行编译

// node_modules\vue-loader\lib\compiler.js
exports.resolveCompiler = function (ctx, loaderContext) {
  if (cached) {
    return cached
  }

  // check 2.7
  try {
    const pkg = loadFromContext('vue/package.json', ctx)
    const [major, minor] = pkg.version.split('.')
    if (major === '2' && Number(minor) >= 7) {
      return (cached = {
        is27: true,
        compiler: loadFromContext('vue/compiler-sfc', ctx),
        templateCompiler: undefined
      })
    }
  } catch (e) {}

  return (cached = {
    compiler: require('@vue/component-compiler-utils'),
    templateCompiler: loadTemplateCompiler(ctx, loaderContext)
  })
}
复制代码

eslint-plugin-vue 升级到 v9 以上

在使用 setup 语法糖的时候由于内部变量都是直接声明暴露给模板使用的,所以旧版 eslint 检测到会有未使用的变量的时候会报错 'unused...'

"devDependencies": {
    "eslint-plugin-vue": "^9.3.0"
}
复制代码

修改深度作用选择器(非必须)

更新后,以前使用的写法,经过 compiler-sfc 处理后可能会报错,也可能只是警告,本地目前只是输出了警告信息,但是没有报错,也不影响使用,所以可能是跟其他依赖包有关,如果小伙伴们发现有报错的话,就按照提示将其统一改成 :deep() 的方式

@vue/compiler-sfc] ::v-deep usage as a combinator has been deprecated. Use :deep(<inner-selector>) instead.
复制代码

特性

下面介绍两个迁移两个比较实用的新特性,如果小伙伴们对其余的感兴趣,也可以动手实践起来呀~

Composition API

按照上述升级成功后,即可在当前项目中新建 sfc 体验 Vue3 新引入的特性,这里取官方提供的 demo 来展示一下用法

<script setup>
import { ref, onMounted } from 'vue'

// reactive state
const count = ref(0)

// functions that mutate state and trigger updates
function increment() {
  count.value++
}

// lifecycle hooks
onMounted(() => {
  console.log(`The initial count is ${count.value}.`)
})
</script>

<template>
  <button @click="increment">Count is: {{ count }}</button>
</template>
复制代码

ESNext in template

Vue2.7 在 template 中还支持了 ESNext 的用法,当我们本地进行构建的时候,template 编译后生成的 render 函数也会交由我们本地为 .js 文件配置的 loader 或者 plugin 进行处理,这意味着我们本地如果为 .js 文件配置了 babel 支持 ESNext 相关的预设或插件,那么我们也可以在 template 上使用 ESNext 语法,而不仅局限于以前的在 script 中使用了,这是很多人在以前版本的 issue 中疯狂给尤大提的一个点,在 issue 中看了下,以前的 template 是通过 vue-template-compiler 处理的,但这个插件并不支持 babel,也不支持 ES6 以上的语法,并且官方也没打算支持,所以之前社区大多使用第三方的解决方案,引入了 vue-template-babel-compiler 但是毕竟是第三方的,仍然存在不少问题(不过内部做了额外的处理,已经支持了可选链的操作,感兴趣的也可以去了解使用一下),vue-template-compiler 内部使用 Bublé 进行编译(仅做了解:简单的说,Bublé 是一个为解决 Babel 在编译速度上慢的问题而引进的一个 ES2015+ 的编译器,但是其不支持插件和预设,在可扩展性上没有 Babel 好,而且看外界评价其不支持大部分的特性,也没遵循 ES2015+ 的语义),而 Vue2.7 在之前的基础上,通过 vue-loader 提供的 node_modules\vue-loader\lib\loaders\pitcher.js 对 vue-loader 解析的 templateImport 做了转换的处理:

1、通过 vue-loader 对 sfc 每个 block 进行处理,生成对应的模块请求如:import { render, staticRenderFns } from "./test.vue?vue&type=template&id=13429420&scoped=true&"

// node_modules\vue-loader\lib\index.js
// template
let templateImport = `var render, staticRenderFns`
let templateRequest
if (descriptor.template) {
    const src = descriptor.template.src || resourcePath
    const idQuery = `&id=${id}`
    const scopedQuery = hasScoped ? `&scoped=true` : ``
    const attrsQuery = attrsToQuery(descriptor.template.attrs)
    // const tsQuery =
    // options.enableTsInTemplate !== false && isTS ? `&ts=true` : ``
    const query = `?vue&type=template${idQuery}${scopedQuery}${attrsQuery}${inheritQuery}`
    const request = (templateRequest = stringifyRequest(src + query))
    templateImport = `import { render, staticRenderFns } from ${request}`
}
复制代码

2、然后基于上述生成的 request,交由 pitcher 做进一步处理

// node_modules\vue-loader\lib\loaders\pitcher.js
const preLoaders = loaders.filter(isPreLoader)
const postLoaders = loaders.filter(isPostLoader)
const { is27 } = resolveCompiler(this.rootContext, this)

const request = genRequest([
  ...cacheLoader,
  ...postLoaders,
  ...(is27 ? [] : [templateLoaderPath + `??vue-loader-options`]),
  ...preLoaders
])
复制代码

处理后的 template 模块请求变成:

15.10.0 以前:-!./lib/vue-loader/loaders/templateLoader.js??vue-loader-options!./lib/vue-loader/index.js??vue-loader-options!./test.vue?vue&type=template&id=13429420&scoped=true&

15.10.0 以后,在上述基础上继续交由 babel 做解析(此处没打印具体值,手动拼一下):-!./lib/babel-loader/index.js??./lib/vue-loader/loaders/templateLoader.js??vue-loader-options!./lib/vue-loader/index.js??vue-loader-options!./test.vue?vue&type=template&id=13429420&scoped=true&

3、从上面可以看出,最终 template 的模块请求处理成 webpack loader 的内联写法,通过 ! 分割,加入了三个 loader 进行处理,也就是将我们在 sfc 中编写的 template 经过 vue-loader 入口分别对三个 block 处理得到 descriptior 后,template block 继续交由 templateLoader 处理,完了后最后交由我们在项目中配置的 babel-loader 处理,所以如果我们本地项目配置的 babel 是支持了 ESNext 语法的话,那么我们就可以在日常开发中往 template 上面去使用了,如:

<template>
  <div>
    {{ userInfo?.name || '?.' }}
    {{ userName ?? '??' }}
  </div>
</template>
复制代码

这样我们总算可以在 template 中使用 可选链 而不用在模板判断中写上一大堆的 &&,还可以使用 空值合并运算符而不用使用可能会导致预期不符的 ||

注意事项

如果小伙伴们发现经过上述步骤后,模板编译 ESNext 的时候报错的话,可以去检查一下本地构建配置,如 webpack 中的 loader,检查一下其对 /\.js$/ 的 babel-loader 配置

{
    test: /\.js$/,
    loader: 'babel-loader',
    // include: path.resolve(process.cwd(), 'src')
}
复制代码

此处配置了 include 限制在 src 目录下后就会无法解析到 sfc 模板中的语法了,因为编译后的 render 函数是在 vue-loader 的 pitcher 处理的模块请求如 -!./lib/babel-loader/index.js??./lib/vue-loader/loaders/templateLoader.js??vue-loader-options!./lib/vue-loader/index.js??vue-loader-options!./test.vue?vue&type=template&id=13429420&scoped=true&,而 include 的配置如果是字符串如上面的绝对路径,那么匹配的规则是要求以它为开头的,详情可前往 Webpack Module Condition 查阅

总结

以上是我在本次对旧项目升级 Vue2.7 的总结,期间遇到了不少升级所导致项目依赖的内部库报错问题,沿着 Vue2.7 发布的公告里面的指南一步一步的排查解决,期间还深入比对升级前后,对 sfc 的不同处理方式,从中学到不少知识,如 vue-loader 是如何对我们编写的 .vue 文件做处理等等,希望本文能帮助到有需要升级 Vue2.7 的小伙伴们,如文中有描述不对的地方,还请大家指出,也欢迎大家来一起讨论。

参考文献

Vue 2.7 "Naruto" Released

vue-loader 深入学习

深入 vue-loader 原理

猜你喜欢

转载自juejin.im/post/7129050578440683533