构建工具

create-project 创建的项目,在构建工具方面,支持以 webpack 为代表的 bundle 工具。同时也支持最近这两年比较流行的,基于 ES module 的 bundless 工具,以 vite 为代表。

下面来介绍下,这两种构建工具在项目模板具体的一些使用特性。

bundle

是基于官方提供的命令行工具 vue-cliopen in new window 进行搭建的。参考了里面的一些配置,并在 vue.config.js 文件里扩展了一些常用的功能。

vue.config.js 基础配置

const { defineConfig } = require('@vue/cli-service');

module.exports = defineConfig({
  /**
   * You can set by yourself according to actual condition
   * You will need to set this if you plan to deploy your site under a sub path,
   * for example GitHub pages. If you plan to deploy your site to https://foo.github.io/bar/,
   * then assetsPublicPath should be set to "/bar/".
   * In most cases please use '/' !!!
   * Detail https://cli.vuejs.org/config/#publicPath
   */
  publicPath: './',
  assetsDir: 'static',
  lintOnSave: process.env.NODE_ENV !== 'production',
  productionSourceMap: false,
  pluginOptions: {
    lintStyleOnBuild: true,
    stylelint: {}
  },
  // disable thread-loader, which is not compactible with this plugin
  parallel: false,
})

proxy 解决接口跨域问题

const { defineConfig } = require('@vue/cli-service');

module.exports = defineConfig({
  // webpack-dev-server 相关配置
  devServer: {
    port: 3000,
    https: false,
    client: {
      overlay: {
        warnings: false,
        errors: true
      }
    },
    // 代理示例 https://webpack.docschina.org/configuration/dev-server/#devserver-proxy
    proxy: {
      '/api': {
        target: 'http://localhost:8000', // 后端接口地址
        ws: true,
        changeOrigin: true, // 是否允许跨域
        pathRewrite: {
          '^/api': ''   // 直接用'api/接口名'进行请求.
        }
      }
    }
  },
})

使用示例:

<script>
    import axios from "axios";
    axios.get("/api/getPetId").then(res => {
      console.log('getPetId:', res);
    });
</script>

添加别名 alias

const { defineConfig } = require('@vue/cli-service');
const path = require('path');
const pkg = require('./package.json');
const resolve = (dir) => {
  return path.join(__dirname, './', dir);
};

module.exports = defineConfig({
  configureWebpack: () => ({
    name: `${pkg.name}`,
    resolve: {
      alias: {
        '@': resolve('src'),
        '@assets': resolve('src/assets'),
        '@style': resolve('src/assets/style'),
        '@js': resolve('src/assets/js'),
        '@components': resolve('src/components'),
        '@mixins': resolve('src/mixins'),
        '@filters': resolve('src/filters'),
        '@store': resolve('src/store'),
        '@views': resolve('src/views'),

        // 文件别名
        services: resolve('src/services'),
        variable: resolve('src/assets/style/variable.less'),
        utils: resolve('node_modules/@winner-fed/cloud-utils/dist/cloud-utils.esm'),
        mixins: resolve('node_modules/@winner-fed/magicless/magicless.less'),
      }
    }
  }),
});

支持 SVG 组件

const { defineConfig } = require('@vue/cli-service');

module.exports = defineConfig({
  chainWebpack: (config) => {
    // module

    // svg
    // exclude icons
    config.module
      .rule('svg')
      .exclude.add(resolve('src/icons'))
      .end();
    config.module
      .rule('icons')
      .test(/\.svg$/)
      .include.add(resolve('src/icons'))
      .end()
      .use('svg-sprite-loader')
      .loader('svg-sprite-loader')
      .options({
        symbolId: 'icon-[name]'
      })
      .end();
  },
})

使用 splitChunks 单独打包三方依赖模块

const { defineConfig } = require('@vue/cli-service');

module.exports = defineConfig({
  chainWebpack: (config) => {
  // optimization
  config
  .when(process.env.NODE_ENV === 'production',
    config => {
      config
        .optimization
        .splitChunks({
          chunks: 'all',
          cacheGroups: {
            defaultVendors: {
              name: 'chunk-vendors',
              test: /[\\/]node_modules[\\/]/,
              priority: 10,
              chunks: 'initial' // 只打包初始时依赖的第三方
            },
            commons: {
              name: 'chunk-commons',
              test: resolve('src/components'), // 可自定义拓展你的规则
              minChunks: 3, // 最小公用次数
              priority: 5,
              reuseExistingChunk: true
            }
          }
        });
      config.optimization.runtimeChunk('single');
    });
  },
})

CSS 相关配置及 Less 全局注入变量及函数

const { defineConfig } = require('@vue/cli-service');

module.exports = defineConfig({
  // css相关配置
  css: {
    // 是否使用css分离插件 ExtractTextPlugin
    extract: isProd() ? true : false,
    // 开启 CSS source maps?
    sourceMap: isProd() ? true : false,
    // css预设器配置项
    loaderOptions: {
      less: {
        // 全局注入变量及mixins
        additionalData: `@import "@/assets/style/variable.less";@import "@winner-fed/magicless/magicless.less";`,
      }
    }
  },
})

生产环境移除 console.log

const { defineConfig } = require('@vue/cli-service');
const TerserPlugin = require('terser-webpack-plugin');

const isProd = () => {
  return process.env.NODE_ENV === 'production';
};
// 生产环境去掉 console.log
const getOptimization = () => {
  let optimization = {};
  if (isProd()) {
    optimization = {
      // https://webpack.docschina.org/configuration/optimization/#optimization-minimizer
      minimizer: [
        new TerserPlugin({
          terserOptions: {
            // https://github.com/webpack-contrib/terser-webpack-plugin#terseroptions
            compress: {
              warnings: false,
              drop_console: true,
              drop_debugger: true,
              pure_funcs: ['console.log']
            }
          }
        })
      ]
    };
  }
  return optimization;
};

module.exports = defineConfig({
  configureWebpack: () => ({
    // https://github.com/cklwblove/vue-cli3-template/issues/12
    optimization: getOptimization()
  })
})

支持 gzip 压缩

const { defineConfig } = require('@vue/cli-service');
const webpack = require('webpack');
const pkg = require('./package.json');
const { formatDate } = require('@winner-fed/cloud-utils');
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');
const WebpackBar = require('webpackbar');
const CompressionWebpackPlugin = require('compression-webpack-plugin');

const isProd = () => {
  return process.env.NODE_ENV === 'production';
};

const genPlugins = () => {
  const plugins = [
    new WebpackBar(),
    // 为静态资源文件添加 hash,防止缓存
    new AddAssetHtmlPlugin([
      {
        filepath: path.resolve(__dirname, './public/console.js'),
        hash: true,
      }
    ])
  ];

  if (isProd()) {
    plugins.push(
      // bannerPlugin
      new webpack.BannerPlugin({
        banner:
          `@author: Whale FE${
            N}@version: ${pkg.version}${
            N}@description: Build time ${formatDate(new Date(), 'yyyy-MM-dd HH:mm:ss')}          `
      }),
      new CompressionWebpackPlugin({
        filename: '[path][base].gz[query]',
        algorithm: 'gzip',
        test: new RegExp(
          '\\.(' +
          ['js', 'css'].join('|') +
          ')$'
        ),
        threshold: 10240,
        minRatio: 0.8
      })
    );
  }

  return plugins;
};

module.exports = defineConfig({
  configureWebpack: () => ({
    plugins: genPlugins(),
  }) 
})

生成 manifest.json

const { defineConfig } = require('@vue/cli-service');
const path = require('path');
const pkg = require('./package.json');
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');

const isProd = () => {
  return process.env.NODE_ENV === 'production';
};

const genPlugins = () => {
  const plugins = [];

  if (isProd()) {
    plugins.push(
      new WebpackManifestPlugin({
        fileName: path.resolve(
          __dirname,
          'dist',
          `manifest.${Date.now()}.json`
        ),
        filter: ({name, path}) => !name.includes('runtime'),
        generate (seed, files, entries) {
          return files.reduce((manifest, {name, path: manifestFilePath}) => {
            const {root, dir, base} = path.parse(manifestFilePath);
            return {
              ...manifest,
              [name + '-' + base]: {path: manifestFilePath, root, dir}
            };
          }, seed);
        }
      }),
    );
  }

  return plugins;
};

module.exports = defineConfig({
  configureWebpack: () => ({
    plugins: genPlugins(),
  }) 
})

Vue2 支持 composition-api/setup 写法

const { defineConfig } = require('@vue/cli-service');
const ScriptSetup = require('unplugin-vue2-script-setup/webpack').default;

const isProd = () => {
  return process.env.NODE_ENV === 'production';
};

const genPlugins = () => {
  const plugins = [
    ScriptSetup({})
  ];

  return plugins;
};

module.exports = defineConfig({
  configureWebpack: () => ({
    plugins: genPlugins(),
  }) 
})

package.json 添加相关依赖

{
  "dependencies": {
    "@vue/composition-api": "^1.4.6"
  },
  "devDependencies": {
    "@vue/runtime-dom": "^3.2.31",
    "unplugin-vue2-script-setup": "^0.9.3"
  }
}

注意

@vue/composition-apiopen in new window 是用于提供组合式 API 的 Vue 2 插件。支持所有现代浏览器以及IE11+。对于更低版本的 IE 浏览器你需要安装 WeakMap polyfill (例如使用 core-js库)

bundless

配置路径别名 alias

vite.config.js 相关配置:

import { defineConfig, loadEnv } from 'vite';
import { wrapperEnv } from './build/vite/utils';
import { resolve } from 'path';

function pathResolve(dir) {
  return resolve(__dirname, '.', dir);
}

export default defineConfig(({ command, mode }) => {
  const root = process.cwd();

  const env = loadEnv(mode, root);

  // The boolean type read by loadEnv is a string. This function can be converted to boolean type
  const viteEnv = wrapperEnv(env);

  const { VITE_PORT, VITE_PUBLIC_PATH, VITE_PROXY, VITE_DROP_CONSOLE, VITE_LEGACY } = viteEnv;
  
  return {
    resolve: {
      alias: [
        {
          // @/xxxx  =>  src/xxx
          find: /^@\//,
          replacement: pathResolve('./src') + '/'
        },
        // #/xxxx => types/xxxx
        {
          find: /#\//,
          replacement: pathResolve('./types') + '/'
        },
        {
          find: /^utils/,
          replacement: pathResolve('node_modules/@winner-fed/cloud-utils/dist/cloud-utils.esm')
        }
      ]
    },
  }
});

配置代理 Proxy,本地解决跨域问题

vite.config.js 相关配置:

import { defineConfig, loadEnv } from 'vite';
import { wrapperEnv } from './build/vite/utils';
import { createProxy } from './build/vite/proxy';
import { resolve } from 'path';

function pathResolve(dir) {
  return resolve(__dirname, '.', dir);
}

export default defineConfig(({ command, mode }) => {
  const root = process.cwd();

  const env = loadEnv(mode, root);

  // The boolean type read by loadEnv is a string. This function can be converted to boolean type
  const viteEnv = wrapperEnv(env);

  const { VITE_PORT, VITE_PUBLIC_PATH, VITE_PROXY, VITE_DROP_CONSOLE, VITE_LEGACY } = viteEnv;
  
  return {
    server: {
      host: '0.0.0.0',
      port: VITE_PORT,
      // Load proxy configuration from .env
      proxy: createProxy(VITE_PROXY),
      hmr: {
        overlay: true
      }
    },
  }
});

其中,/build/vite/ 目录的 proxy.js 代码如下:

// proxy.ts

const httpsRE = /^https:\/\//;

/**
 * Generate proxy
 * @param list
 */
export function createProxy(list) {
  const ret = {};
  if (Array.isArray(list) && list.length) {
    for (const [prefix, target] of list) {
      const isHttps = httpsRE.test(target);

      // https://github.com/http-party/node-http-proxy#options
      ret[prefix] = {
        target: target,
        changeOrigin: true,
        ws: true,
        rewrite: (path) => path.replace(new RegExp(`^${prefix}`), ''),
        // https is require secure=false
        ...(isHttps ? { secure: false } : {})
      };
    }
  }

  return ret;
}

VITE_PROXY 是在根目录下的 .env.development 里定义的,如下:

# Cross-domain proxy, you can configure multiple
VITE_PROXY=[["/api","http://localhost:3000"],["/upload","http://localhost:3001/upload"]]

使用 JSX或TSX

@vitejs/plugin-vue-jsxopen in new window 通过 HMR 提供 Vue 3 JSX 和 TSX 支持。

vite.config.js 配置

import { defineConfig, loadEnv } from 'vite';
import { resolve } from 'path';
import { createVitePlugins } from './build/vite/plugin';
import { createProxy } from './build/vite/proxy';
import { wrapperEnv } from './build/vite/utils';

function pathResolve(dir) {
  return resolve(__dirname, '.', dir);
}

// https://vitejs.dev/config/
export default defineConfig(({ command, mode }) => {
  const root = process.cwd();

  const env = loadEnv(mode, root);

  // The boolean type read by loadEnv is a string. This function can be converted to boolean type
  const viteEnv = wrapperEnv(env);

  const { VITE_PORT, VITE_PUBLIC_PATH, VITE_PROXY, VITE_DROP_CONSOLE, VITE_LEGACY } = viteEnv;

  const isBuild = command === 'build';
  
  return {
    // The vite plugin used by the project. The quantity is large, so it is separately extracted and managed
    plugins: createVitePlugins(viteEnv, isBuild),
  }
})

其中,在使用 Vue3 时,build/vite/plugin/ 目录的 index.js

// build/vite/index.js

import legacy from '@vitejs/plugin-legacy';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import svgLoader from 'vite-svg-loader';

export function createVitePlugins(viteEnv, isBuild) {
  const {
    VITE_USE_IMAGEMIN,
    VITE_LEGACY,
    VITE_BUILD_COMPRESS,
    VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE
  } = viteEnv;

  const vitePlugins = [
    vue(),
    vueJsx(),
    svgLoader()
  ];

  // @vitejs/plugin-legacy
  VITE_LEGACY &&
  isBuild &&
  vitePlugins.push(
    legacy({
      targets: ['ie >= 11'],
      additionalLegacyPolyfills: ['regenerator-runtime/runtime']
    })
  );
  
  return vitePlugins;
}

在使用 Vue2 时,build/vite/plugin/ 目录的 index.js

import legacy from '@vitejs/plugin-legacy';
import { createVuePlugin as vue2 } from 'vite-plugin-vue2';
// @ts-ignore
import vueTemplateBabelCompiler from 'vue-template-babel-compiler';
import scriptSetup from 'unplugin-vue2-script-setup/vite';
import svgLoader from 'vite-svg-loader';

export function createVitePlugins(viteEnv, isBuild) {
  const {
    VITE_USE_IMAGEMIN,
    VITE_LEGACY,
    VITE_BUILD_COMPRESS,
    VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE
  } = viteEnv;

  const vitePlugins = [
    vue2({
      jsx: true,
      vueTemplateOptions: {
        compiler: vueTemplateBabelCompiler
      }
    }),
    scriptSetup(),
    svgLoader()
  ];

  // @vitejs/plugin-legacy
  VITE_LEGACY &&
    isBuild &&
    vitePlugins.push(
      legacy({
        targets: ['ie >= 11'],
        additionalLegacyPolyfills: ['regenerator-runtime/runtime']
      })
    );

  return vitePlugins;
}

Less 全局注入变量及函数

vite.config.js 配置如下:

import { defineConfig, loadEnv } from 'vite';

// https://vitejs.dev/config/
export default defineConfig(({ command, mode }) => {
  return {
    css: {
      preprocessorOptions: {
        less: {
          additionalData: `@import "@/assets/style/variable.less";@import "@winner-fed/magicless/magicless.less";`,
          modifyVars: {
            // Used for global import to avoid the need to import each style file separately
            // reference:  Avoid repeated references
          },
          javascriptEnabled: true
        }
      }
    }
  };
});

支持 gzip 压缩

使用 vite-plugin-compressionopen in new window 插件实现。

build/vite/plugin/index.js 配置如下:

import { configCompressPlugin } from './compress';

export function createVitePlugins(viteEnv, isBuild) {
  const {
    VITE_USE_IMAGEMIN,
    VITE_LEGACY,
    VITE_BUILD_COMPRESS,
    VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE
  } = viteEnv;

  const vitePlugins = [];


  // The following plugins only work in the production environment
  if (isBuild) {
    // rollup-plugin-gzip
    vitePlugins.push(
      configCompressPlugin(VITE_BUILD_COMPRESS, VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE)
    );
  }

  return vitePlugins;
}

build/vite/plugin/compress.js 如下:

/**
 * Used to package and output gzip. Note that this does not work properly in Vite, the specific reason is still being investigated
 */
import compressPlugin from 'vite-plugin-compression';

export function configCompressPlugin(
  // 'gzip' | 'brotli' | 'none'
  compress,
  deleteOriginFile = false
) {
  const compressList = compress.split(',');

  const plugins = [];

  if (compressList.includes('gzip')) {
    plugins.push(
      compressPlugin({
        ext: '.gz',
        deleteOriginFile
      })
    );
  }
  if (compressList.includes('brotli')) {
    plugins.push(
      compressPlugin({
        ext: '.br',
        algorithm: 'brotliCompress',
        deleteOriginFile
      })
    );
  }
  return plugins;
}

构建包依赖分析

使用 rollup-plugin-visualizeropen in new window 插件实现。

import { configVisualizerConfig } from './visualizer';

export function createVitePlugins(viteEnv, isBuild) {
  const {
    VITE_USE_IMAGEMIN,
    VITE_LEGACY,
    VITE_BUILD_COMPRESS,
    VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE
  } = viteEnv;

  const vitePlugins = [];
  
  // vite-plugin-svg-icons
  vitePlugins.push(configVisualizerConfig(isBuild));
  
  return vitePlugins;
}

build/vite/plugin/visualizer.js 如下:

/**
 * Package file volume analysis
 */
import { visualizer } from 'rollup-plugin-visualizer';
import { isReportMode } from '../utils';

export function configVisualizerConfig() {
  if (isReportMode()) {
    return visualizer({
      filename: './node_modules/.cache/visualizer/stats.html',
      open: true
    });
  }
  return [];
}

SVG 雪碧图

使用 vite-plugin-svg-iconsopen in new window 插件实现。

import { configSvgIconsPlugin } from './svgSprite';

export function createVitePlugins(viteEnv, isBuild) {
  const {
    VITE_USE_IMAGEMIN,
    VITE_LEGACY,
    VITE_BUILD_COMPRESS,
    VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE
  } = viteEnv;

  const vitePlugins = [];
  
  // vite-plugin-svg-icons
  vitePlugins.push(configSvgIconsPlugin(isBuild));
  
  return vitePlugins;
}

build/vite/plugin/svgSprite.js 如下:

/**
 * Vite Plugin for fast creating SVG sprites.
 * https://github.com/vbenjs/vite-plugin-svg-icons
 */

import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
import path from 'path';

export function configSvgIconsPlugin(isBuild) {
  const svgIconsPlugin = createSvgIconsPlugin({
    iconDirs: [path.resolve(process.cwd(), 'src/icons/svg')],
    svgoOptions: isBuild,
    // default
    symbolId: 'icon-[dir]-[name]'
  });
  return svgIconsPlugin;
}

上次更新:
贡献者: cklwblove