Failed to resolve module specifier 'vue' Relative references xxx 报错处理过程

阅读说明

软件版本

  • vite v5.3.1
  • vue v3.4.29
  • rollup-plugin-external-globals v0.11.0
  • vite-plugin-external v4.3.1

背景

最近在将基于vite、vue的前端项目中的依赖包改成CDN外部形式。

打包完以后访问页面,遇到了如题的报错,完整报错如下:

1
Uncaught TypeError: Failed to resolve module specifier "vue". Relative references must start with either "/", "./", or "../".

该报错会在不同的使用场景下会出现,我的场景是打包成CDN外部依赖的时候遇到的。

问题过程

处理成外部CDN我用到的是fengxinming/vite-plugins vite-plugin-external插件包的形式,根据文档介绍,采用了常规使用的写法。

vite.config.ts关键配置如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import vue from '@vitejs/plugin-vue'
import createExternal from 'vite-plugin-external'
/** @type {import('vite').UserConfig} */  
export default defineConfig({
  root: 'app',  
  plugins: [  
    vue(),
    createExternal({
	  externals: {  
	    vue: 'Vue',  
	    'vue-router': 'VueRouter'
	    //...
	  }  
	})
  ]
})

打包前的index.html如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <link rel="icon" href="/favicon.ico">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="https://unpkg.com/vue@3.4.35/dist/vue.global.prod.js"></script>
    <script src="https://unpkg.com/vue-router@4.4.2/dist/vue-router.global.prod.js"></script>
    <script type="module" src="./src/main.ts"></script>
  </body>
</html>

打包以后的index.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <link rel="icon" href="./favicon.ico">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vite App</title>
    <script type="module" crossorigin src="./assets/index-Bc_167s1.js"></script>
    <link rel="stylesheet" crossorigin href="./assets/index-B9wcSffo.css">
  </head>
  <body>
    <div id="app"></div>
    <script src="https://unpkg.com/vue@3.4.35/dist/vue.global.prod.js"></script>
    <script src="https://unpkg.com/vue-router@4.4.2/dist/vue-router.global.prod.js"></script>
  </body>
</html>

访问该页面以后,遇到了如标题的报错内容。

然后查看打包以后的index.jsindex-Bc_167s1.js),只放关键内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
const __vite__mapDeps = (
  i,
  m = __vite__mapDeps,
  d = m.f ||
    (m.f = [
      './page1-Da4j-Y0r.js',
      './page2-XQJYOUa2.js',
      './page3-DXfHDBAk.js'
    ])
) => i.map((i) => d[i])
import {
  defineComponent as P,
  openBlock as w,
  createElementBlock as O,
  Fragment as R,
  createElementVNode as p,
  createVNode as m,
  unref as d,
  withCtx as h,
  createTextVNode as _,
  createApp as A
} from 'vue'
import {
  RouterLink as g,
  RouterView as V,
  createRouter as S,
  createWebHistory as k
} from 'vue-router'

// ...

const B = { class: 'wrapper' },  
  C = P({  
    __name: 'App',  
    setup(c) {  
      return (o, n) => (  
        w(),  
        O(  
          R,  
          null,  
          [  
            p('header', null, [  
              p('div', B, [  
                p('nav', null, [  
                  m(d(g), { to: '/path/page1' }, { default: h(() => [_('page1')]), _: 1 }),  
                  m(d(g), { to: '/path/page2' }, { default: h(() => [_('page2')]), _: 1 }),  
                  m(d(g), { to: '/path/page3' }, { default: h(() => [_('page3')]), _: 1 })  
                ])  
              ])  
            ]),  
            m(d(V))  
          ],  
          64  
        )  
      )  
    }  
  }),

// ...

可以看到这里有引入vuevue-router,当注释imoprt {xxx} from vue以后,可以看到报错内容变成了:

1
Uncaught TypeError: Failed to resolve module specifier "vue-router". Relative references must start with either "/", "./", or "../".

说明就是这里import的问题,直接注释只是屏蔽了该报错,并不能解决问题,因为在index.js下面有使用到import进来的相关模块。

那怎么解决呢?

一番捣鼓以后,解决方式有两种。

解决方法一

关键属性是interop:'auto',先放上解决方法:

修改你的vite.config.ts文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import vue from '@vitejs/plugin-vue'
import createExternal from 'vite-plugin-external'
/** @type {import('vite').UserConfig} */  
export default defineConfig({
  root: 'app',  
  plugins: [  
    vue(),
    createExternal({
      interop:'auto', // 加入这行
	  externals: {  
	    vue: 'Vue',  
	    'vue-router': 'VueRouter'
	    //...
	  }  
	})
  ]
})

重新打包即可。

fengxinming/vite-plugins vite-plugin-external的文档其实是有介绍的,称之为使用兼容的方式读取外部依赖,并说明了参数加与不加的区别,但对于我个人来说并不是很醒目和清晰,并且文档对于interop的作用也没有过多的介绍,虽然在折腾的情况下解决了问题,但只知道它能解决问题,并不知其作用。

知其然而不知其所以然对我这种强迫症来说是不能忍受的。

但毕竟前端水平有限,仅浅浅捣鼓一下,底层原理先不深究,日后再说,这里当抛砖引玉,留给读者琢磨了。

首先查看vite-plugin-external配置项的interop参数定义:

1
2
3
4
export interface Options extends BasicOptions {
    interop?: 'auto';
    enforce?: 'pre' | 'post';
}

可以看出是一个可选参数,但默认值并不是auto,只是声明了其参数可能的值。

通过修改./node_modules/.pnpm/vite-plugin-external@4.3.1/node_modules/vite-plugin-external/dist/index.mjs文件关键位置如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
function createPlugin(opts) {  
  let libNames;  
  return {  
    name: "vite-plugin-external",  
    enforce: opts.enforce,  
    async config(config, { mode, command }) {  
      const { cacheDir, externals, interop } = buildOptions(opts, mode);  
      // 加入以下行
      console.log([65435345, cacheDir, externals, interop])  
      libNames = !externals ? [] : Object.keys(externals);  
      let externalLibs = libNames;  
      let globals = externals;  
      if (command === "serve" || interop === "auto") {  
        await addAliases(config, cacheDir, globals, libNames);  
        externalLibs = [];  
        globals = void 0;  
      }
      // ...
    }
  }
}

的方式进行确认,interop的默认值是undefined

1
2
3
4
5
6
7
8
9
[
  65435345,
  '/xxx/xxx/project-name/node_modules/.vite_external',
  {
    vue: 'Vue',
    'vue-router': 'VueRouter'
  },
  undefined
]

当设置interop的值为auto,重新打包,观察一下index.js文件前后的变化,关键内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
const __vite__mapDeps = (
  i,
  m = __vite__mapDeps,
  d = m.f ||
    (m.f = [
      './page1-Da4j-Y0r.js',
      './page2-XQJYOUa2.js',
      './page3-DXfHDBAk.js'
    ])
) => i.map((i) => d[i])

// ...

var e = Vue
var l = VueRouter
const V = { class: 'wrapper' },  
  L = e.defineComponent({  
    __name: 'App',  
    setup(n) {  
      return (i, s) => (  
        e.openBlock(),  
        e.createElementBlock(  
          e.Fragment,  
          null,  
          [  
            e.createElementVNode('header', null, [  
              e.createElementVNode('div', V, [  
                e.createElementVNode('nav', null, [  
                  e.createVNode(  
                    e.unref(l.RouterLink),  
                    { to: '/path/page1' },  
                    { default: e.withCtx(() => [e.createTextVNode('page1')]), _: 1 }  
                  ),  
                  e.createVNode(  
                    e.unref(l.RouterLink),  
                    { to: '/path/page2' },  
                    { default: e.withCtx(() => [e.createTextVNode('page2')]), _: 1 }  
                  ),  
                  e.createVNode(  
                    e.unref(l.RouterLink),  
                    { to: '/path/page3' },  
                    { default: e.withCtx(() => [e.createTextVNode('page3')]), _: 1 }  
                  )  
                ])  
              ])  
            ]),  
            e.createVNode(e.unref(l.RouterView))  
          ],  
          64  
        )  
      )  
    }  
  })

// ...

可以清晰观察到,未设置interop的情况下,vue依赖需要通过import {xxx} vue的方式进行引入,而当设置interop:'auto'的情况下,vue依赖变成了var e=Vue全局的方式进行引入,问题得到解决。

解决方法二

需要用到rollup-plugin-external-globals包,修改rollup配置的方式。

vite.config.ts关键内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import externalGlobals from 'rollup-plugin-external-globals'

/** @type {import('vite').UserConfig} */  
export default defineConfig({
  build: {
    rollupOptions: {
      external: [
        'vue',
        'vue-router'
      ],
      plugins: [
        externalGlobals({
          vue: 'Vue',
          'vue-router': 'VueRouter'
        })
      ]
    }
  }
})

重新打包即可。

该方式暂时不深入,以记录为主。

参考

Built with Hugo
主题 StackJimmy 设计