Vite True Fragrance Road

1. Empezar

Recientemente, se reemplazó el andamiaje de varios proyectos de Vue-CLI a Vite, que es realmente fragante. Solía ​​tomar más de 2 minutos para iniciar un arranque en frío, pero ahora solo toma unos segundos. Para aquellos que necesitan cortar proyectos con frecuencia, es realmente una herramienta de desarrollo.

Las ventajas actuales de Vite son más que eso. Este artículo no analiza las ventajas de Vite, sino que solo registra algunos inconvenientes de cambiar de Vue-CLI a Vite.

2. Registro de problemas

Explique de antemano que puede haber múltiples soluciones a los siguientes problemas. Aquí, se selecciona la que presenta los cambios más pequeños en la biblioteca comercial por las siguientes razones:

  1. Un proyecto a menudo tiene varios desarrolladores, y no se espera que los cambios afecten el inicio o el empaquetado de Vue-CLI anterior.
  2. El archivo de configuración se extraerá a la biblioteca básica y se utilizarán muchos proyectos. Si los cambios son grandes, el costo será alto y la probabilidad de errores será mayor.

1. Variables de entorno

Vite expone variables de entorno en un objeto import.meta.env especial, y Vue-CLI se basa en webpack, que está montado en process.env.

Además, hay otra diferencia. El vue.config.js original puede obtener las variables de entorno directamente a través de process.env, pero vite.config.js no puede obtenerlo directamente. El desarrollador debe llamar a loadEnv para cargarlo.

Además, Vite solo expone las variables de entorno que comienzan con VITE_ al cliente, y en Vue-CLI comienza con VUE_APP_.

El procesamiento correspondiente es el siguiente: Reemplace las variables globales con define, que actualmente es seguro.

import { loadEnv } from 'vite';

const ENV_PREFIX = ['VITE_', 'VUE_APP'];

export default ({ mode, serverProxy }) => {
  const envMap = loadEnv(mode, process.cwd(), ENV_PREFIX) || {};
  const appDir = envMap.VUE_APP_DIR;

  return defineConfig({
    root: `${path.resolve(curDirname, `./src/${appDir}`)}/`,
    define: {
      'process.env': {
        ...envMap,
        NODE_ENV: mode,
      },
    },
  })
}

2. Procesamiento de index.html

El index.html predeterminado en Vite está en el directorio raíz del proyecto, que está al mismo nivel que vite.config.js, pero la mayoría de nuestros proyectos están en modo monorepo, e index.html está en src/project/some-project.

La solución es establecer la raíz:

{
  root: `${path.resolve(curDirname, `./src/${appDir}`)}/`,
}

Sin embargo, solo esto no es suficiente, el index.html predeterminado no tiene <script type="module" src="./src/project/some-project/main.js"></script>esta oración, por lo que debemos escribir un complemento para insertar dinámicamente esta oración cuando se carga index.html.

打包的时候发现另一个问题,只打包出来 index.html,其他js等文件没有被打包,猜测是打包的时候找不到 main.js,于是给插件增加配置enforce: pre

下面是插件核心代码:

return {
  name: 'vite-plugin-transform-html',
  enforce: 'pre',
  transformIndexHtml(html) {
    return html.replace(
      /<\/head>/,
      `<script type="module" src="./main.js"></script>
  </head>`,
    );
  },
};

后面发现,生产环境不会触发transformIndexHtml方法,上面代码并没有效果,于是优化成:

const res = {
  name: 'vite-plugin-transform-html',
  enforce: 'pre',
};

if (mode === 'development') {
  return {
    ...res,
    transformIndexHtml(code) {
      return transformIndexHtml(code, mode);
    },
  };
}

return {
  ...res,
  transform(code, id) {
    if (id?.endsWith('.html')) {
      return transformIndexHtml(code, mode);
    }
  },
};

3. 预构建缓存问题

Vite有个预构建阶段,用于将commonjs/UMD模块转为ESM,和合并多个模块。就是把一些模块处理后放在node_modules/.vite/deps目录下,项目启动时直接引用这个目录下的内容。

值得注意的是,这一阶段是有缓存的,且存在两处缓存,一处是.vite/deps下的缓存,一处是浏览器的缓存。如果发现修改了插件,但是观察不到效果,可以尝试npx vite --fore,以及禁用浏览器缓存。

4. vue2/vue3并存

有个公共库是同时支持vue2/vue3的,比如有个extend-comp功能,用来扩展组件,代码如下:

import Vue, { createApp } from 'vue';

export function extendComp(arg: ExtendCompParam) {
  if (Vue?.version?.startsWith?.('2')) {
    return extendV2(arg);
  }
  if (typeof createApp === 'function') {
    return extendV3(arg);
  }
  return extendV2(arg);
}

它的顶部会尝试引用 createApp,如果是vue2的项目,它会报错,之前的兼容方案是扩展下vue的类型声明:

import 'vue/types/index';

declare module 'vue/types/index' {
  function createApp(c: any, d?: any): any;
}

现在vite的预构建,会直接报错,因为vue依然没导出createApp,想到一个方式是写个插件在最底部加上createApp的导出,核心代码如下:

return {
  transform(source, id) {
    if (id.indexOf('vue.js') > -1 || id.indexOf('vue.runtime.esm.js') > -1) {
      return `${source}

  export const createApp = () => {}
`;
    }
    return source;
  },
};

5. vue组件的动态导入

vue动态导入有多种方式,Vite可以支持 xxComp: ()=>import('xx.vue'),不支持 xxComp(resolve){ require(['xx.vue'], resolve) },可以手动改业务库,但我们的目标是尽可能少的改项目,所以也可以写个插件,用于替换源代码,核心代码如下:

return {
  transform(source, id) {
    if (id.indexOf('.vue') === -1) {
      return source;
    }
    const reg = new RegExp(/([a-zA-Z]+?)\(resolve\)(?:\s*?)\{(?:\n\s*)require\(\['(.*?)'\],(?:\s*?)resolve\);(?:\n\s*)\}/, 'g');
    const match = source.match(reg);
    if (match?.[1] && match[2]) {
      const res = source.replace(reg, (match, originA, originB) => `${originA}: () => import('${originB}')`);
      return res;
    }
    return source;
  },
};

6. Vant样式按需加载

这个问题只要使用一下vite-plugin-style-import就可以。

import {
  createStyleImportPlugin,
  VantResolve,
} from 'vite-plugin-style-import';
// ...
plugins: [
  createStyleImportPlugin({
    resolves: [
      VantResolve(),
    ],
  }),
]

7. externals配置

关于external的Vite插件众多,这里用的是vite-plugin-externals

import { viteExternalsPlugin } from 'vite-plugin-externals';
// ...
plugins: [
  viteExternalsPlugin({
    vue: 'Vue',
    'vue-router': 'VueRouter',
    vuex: 'Vuex',
    axios: 'axios',
    'vue-lazyload': 'VueLazyload',
  }),
]

8. proxy配置

部分项目需要配置proxy,配置如下。

const serverProxy = {
  '/xxx-cgi': {
    target: 'http://localhost:3000',
    changeOrigin: true,
    ws: true,
    rewrite: path => path.replace(/^\/xxx-cgi/, ''),
  },
};
// ...
server: {
  proxy: {
    ...serverProxy,
  },
},

9. sass相关

Vite 中要想支持scss文件,需要安装sass,注意不是node-sass,这会引起另一个问题,/deep/会报错,需要将 /deep/ 换成 ::v-deep,这两个作用一样,都可以在scoped下修改子组件样式,一些文章说::v-deep性能更佳。

此外,某些项目有这种写法:

$--font-path: "~element-ui/lib/theme-chalk/fonts";

这种引用方式Vite默认情况下是无法识别的,最简单的方式是改成:

$--font-path: "node_modules/element-ui/lib/theme-chalk/fonts";

10. BASE_URL

之前index.html中的这种写法会报错:

<link rel="icon" href="<%= BASE_URL %>favicon.ico" />

报错信息为:

[vite] Internal server error: URI malformed

解决方法是写个插件替换下:

res = code.replace(/<%=\s+BASE_URL\s+%>/g, baseDir);

11. 编译时动态加载对应的样式

值得注意的是下面这行代码不会报错,所以当要找的样式文件不存在时,可以直接用空字符串替换。

<style lang="scss" scoped src=""></style>

如何判断要找的文件存不存在呢,如何判断当前操作的文件目录呢?用path.dirname(id)就可以,相关插件代码如下:

transform(source, id) {
  let res = source;

  if (res.indexOf(STYLE_KEYWORD) !== -1) {
    const styleName = getStyleName(appDir);
    const curDir = path.dirname(id);

    let pureCSSLink = `./css/${styleName}.scss`;
    const cssLink = path.resolve(curDir, pureCSSLink);

    const isExist = fs.existsSync(cssLink);
    if (!isExist) {
      pureCSSLink = '';
    }

    res = res.replace(new RegExp(STYLE_KEYWORD, 'g'), pureCSSLink);
  }
  return res;
}

12. 分包策略

关于分包策略没有标准答案,每个项目都有自己的特点,目前我们项目采用的是这种:

const SPLIT_CHUNK_CONFIG = [
  {
    match: /[\\/]src[\\/]_?common(.*)/,
    output: 'chunk-common',
  },
  {
    match: /[\\/]src[\\/]_?component(.*)/,
    output: 'chunk-component',
  },
  {
    match: /[\\/]src[\\/]_?logic(.*)/,
    output: 'chunk-logic',
  },
];

const rollupOptions = {
  output: {
    chunkFileNames: 'assets/js/[name]-[hash].js',
    entryFileNames: 'assets/js/[name]-[hash].js',
    assetFileNames: 'assets/static/[name]-[hash].[ext]',
    manualChunks(id) {
      for (const item of SPLIT_CHUNK_CONFIG) {
        const { match, output } = item;

        if (match.test(id)) {
          return output;
        }
      }

      if (id.includes('node_modules')) {
        return id.toString().split('node_modules/')[1].split('/')[0].toString();
      }
    },
  },
},

13. Vue2中支持JSX

在Vue-CLI中是默认支持Vue2+JSX的,也就是不需额外配置,但是vite+vue2项目中,如果直接写jsx会报错,报错信息如下:

[vite] Internal server error: Failed to parse source for import analysis because the content contains invalid JS syntax. Install @vitejs/plugin-vue to handle .vue files.

尽管上面提示让你安装@vitejs/plugin-vue这个库,但是这个是for Vue3 版本的,如果加上它,会报额外的的错误,说这个库仅服务于Vue3。

那怎么办呢,很简单,在使用JSX的script的地方加上:

<script lang="jsx">

然后在vite.config.js中,为vite-plugin-vue2这个插件增加jsx: true的选项。

import { createVuePlugin } from 'vite-plugin-vue2';

// ...
plugins: [
  createVuePlugin({
    jsx: true,
  }),
  // ...
]

14. qrcodejs2报错

报错信息如下:

TypeError: Cannot read properties of undefined (reading '_android')

这个问题其实是判断的不严谨,已经有很多issue了,比如159,也有人提了PR,甚至合了,但是没发布版本。。

if (this._android && this._android <= 2.1) {
  var factor = 1 / window.devicePixelRatio;
  // ...
}

知道了问题,解决办法就很多了,可以fork下,自己发个包,也可以写个Vite插件转换下代码。

此外,有个问题是,在Vue-CLI中为什么不会报错呢?

因为Vite中使用的是ESM模块,默认会使用严格模式,“禁止this指向全局对象”。而Vue-CLI中使用的是UMD方式加载,在浏览器中会顶层的this等于window,所以不会报错。

15. 使用 path-browserify

不要在前端项目中使用:

import path from 'path'

会报错:

Error in render: "Error: Module "path" has been externalized for browser compatibility and cannot be accessed in client code."

而应该使用path-browserify

import path from 'path-browserify';

如果是用 path.resolve 方法,这样还是不行的,因为 resolve 方法里面使用了 process.cwd 方法,而 Vite 是没有注入 process 这个变量的。

有多个解决方法:

  1. 安装process包,然后在项目中执行 window.process = process,注意不要与vite.config.js中define变量冲突。
  2. 写个Vite插件用来转换源码,开发环境替换为真实的process.cwd()对应的字符串,生产环境替换成/
  3. 自己写path.resolve方法,不用第三方库
// 模拟path.resolve()
function resolve(...paths) {
  let resolvePath = '';
  let isAbsolutePath = false;
  let cwd;

  for (let i = paths.length - 1; i >= -1; i--) {
    let path;
    if (isAbsolutePath) {
      break;
    }
    if (i >= 0) {
      path = paths[i];
    } else {
      if (cwd === undefined) {
        cwd = process.cwd();
      }
      path = cwd;
    }
    if (!path) {
      continue;
    }
    resolvePath = `${path}/${resolvePath}`;
    isAbsolutePath = path.charCodeAt(0) === 47;
  }
  if (/^\/+$/.test(resolvePath)) {
    resolvePath = resolvePath.replace(/(\/+)/, '/');
  } else {
    resolvePath = resolvePath.replace(/(?!^)\w+\/+\.{2}\//g, '')
      .replace(/(?!^)\.\//g, '')
      .replace(/\/+$/, '');
  }
  return resolvePath;
}

console.log(resolve('/aa', '../bb', 'cc', 'dd')); // => /bb/cc/dd
console.log(resolve('/aa', '../bb', './cc', 'dd')); // =>  bb/cc/dd
console.log(resolve('/', '/system', 'user', 'userIndex')); // => /system/user/userIndex
console.log(resolve('', 'system', 'user', 'userIndex')); // => ${cwd}/system/user/userIndex

16. base设置

base是开发或生产环境服务的公共基础路径,也就是文件引用路径,默认是/。合法的值包括以下几种:

  • 绝对 URL 路径名,例如 /foo/
  • 完整的 URL,例如 https://foo.com/
  • 空字符串或 ./(用于开发环境)

我们项目会把静态文件上传到CDN,所以生产环境会应该是第二种——完整的URL,所以可以这么设置:

base: envMap.VUE_APP_PUBLIC_PATH || './',

三、参考

Supongo que te gusta

Origin juejin.im/post/7119455615840092173
Recomendado
Clasificación