从0到1搭建webpack5+vue2.7+ts脚手架

背景

目前公司用的是基于 vue-cli4 开发的脚手架,在vue-cli的基础增加了业务开发的 多语言、多皮肤,以及接口请求,权限等解决方案,能快速搭建项目的基本结构。

但随着技术的迭代,vue2.7/vue3的推出(composition-api),vite的发展,以及vue-cli的不维护,webpack5(缓存+模块联邦)的推出,目前这套脚手架确实有些老旧,需要升级。

使用过 vue3+ts+vite 的开发体验还是很丝滑,但目前的公司的情况上这套解决方案,还面临一些问题。下面讨论一些方案的选择:

  • vite

vite带来的开发体验的提升,目前的生态发展也是不错的;

使用 vite 搭建工程,但集成公司所有的组件库会有一些问题,而且很多插件还需重新开发,目前不适合迁移到vite。

  • webpack5

webpack5 带来的 cache 特性,是一个折中的方案,webpack 的兼容性不用多说,同时缓存也可以提高开发启动的速度,提升开发体验。同时可搭配 esbuild-loader 代替 babel-loader ,提升构建速度。

  • vue3

vue3的升级带来的生态割裂,导致组件库的如果直接想要迁移很麻烦。公司的组件库短期应该不会有 vue3 版本。

  • vue2.7

vue官方推出 vue2.7,虽然今年就不维护了,但可使用 composition-api,目前来说体验还是不错的,但有一些小问题不维护了,整体上还是值得升级的。

  • ts

vue2.6 如果要使用 ts,要使用装饰器写法,这种风格也不再推荐;使用 composition-api 搭配 ts 体验还是不错的。ts 虽然一开始写起来有些痛苦,解决各种报错,但是长期来讲可大大提高项目的可维护性。

基于以上情况的分析和讨论,我基于webpack5+vue2.7+typescript 搭建了项目脚手架,同时使用了 esbuild-loader, 提升了构建速度,下面开始详细介绍如何从0到1 搭建一个脚手架。

安装依赖

只列出一些重要的依赖,一些loader和plugin后面会介绍:

  • vue 2.7.14
  • pinia:代替vuex
  • vue-loader 15.10.1 :注意vue2只能v15
  • webpack 、webpack-cli、webapck-dev-server最新版本

环境和配置文件

从 package.json 的命令开始

json 复制代码
{
  "scripts": {
    "dev": "webpack serve --env development",
    "build": "webpack --env production",
    "build:check": "run-p typecheck build:only",
    "typecheck": "vue-tsc --noEmit",
    "lint": "eslint . --ext .js,.mjs,.ts,.vue --fix --ignore-path .gitignore",
  }
 }

区分两个环境:productiondevelopment

根目录的webpack.config.js

javascript 复制代码
const prodConfig = require('./config/webpack.prod.js');
const devConfig = require('./config/webpack.dev.js');
const { loadClientEnv } = require('./config/env.js');

module.exports = env => {
  // 根据 env 加载不同的配置文件
  return env.production ? prodConfig() : devConfig();
};

接下来就编写webpack config文件:配置文件分为 webpack.prod.jswebpack.dev.js,以及公用配置 webpack.base.js,通过webpack-merge进行合并。

webpack.base.js

javascript 复制代码
module.exports = webpackEnv => {
  const isProd = webpackEnv === 'production';
  return {
    entry: paths.appEntry, // 入口文件
    devtool: isProd
      ? 'nosources-source-map'
      : 'eval-cheap-module-source-map',
    // 输出配置
    output: {
      path: paths.appOutput,
      publicPath: paths.getPublicPath(),
      filename: genAssetPath('js', isProd),
      chunkFilename: genAssetPath('js', isProd),
      hashFunction: 'xxhash64', 
      clean: true /
    },
    resolve: {
      extensions: paths.moduleFileExtensions,
      alias: {
        '@': paths.appSrc,
        vue$: 'vue/dist/vue.runtime.esm.js', // fix:$attrs is readonly / $listeners is readonly
      },
      symlinks: false // 提升性能
    },
    plugins: [
     // 下文讲解
    ],
    module: {
      noParse: /^(vue|vue-router|pinia|vue-i18n|axios)$/,
      rules: getRules(isProd)
    },
    performance: false // 关闭性能提示
  };
};
  • 借鉴cra脚手架,path相关的配置都统一到了paths对象中:
javascript 复制代码
const moduleFileExtensions = ['.js', '.mjs', '.ts', '.vue', '.json'];

module.exports = {
  appTitle: 'vuetage',
  appPath: resolveApp('.'),
  appEntry: resolveApp('src/main.ts'),
  appOutput: resolveApp('dist'), // 输出路径
  appPublic: resolveApp('public'),
  appSrc: resolveApp('src'),
  appHtml: resolveApp('public/index.html'),
  dotenv: resolveApp('.env'),
  getPublicPath: () => ensureEndSlash(process.env.BASE_URL || '/'), // 从.env文件中读取后动态获取
  getAssetsPath: () => removeSlash(process.env.VUE_APP_ASSETS || ''),
  moduleFileExtensions
};
  • output.hashFunction配置hash算法,默认值是md4,这里指定xxhash64 作为优化。从 webpack v5.54.0+ 起,hashFunction支持将xxhash64作为更快的算法,当启用 experiments.futureDefaults 时,此算法将被默认使用。
  • output.clean为true,清除 dist 目录, webpack5之前需要使用 clean-webpack-plugin插件

plugins

javascript 复制代码
    plugins: [
      new VueLoaderPlugin(),
      VueDefineOptions(),
      new ESLintPlugin({
        extensions: ['vue', 'js', 'mjs', 'ts'],
        context: paths.appSrc,
        cache: !isProd,
        emitWarning: true,
        failOnError: false,
        lintDirtyModulesOnly: !isProd
      }),
      new DefinePlugin(getClientEnv()),
      new HtmlWebpackPlugin({
        title: paths.appTitle,
        template: paths.appHtml,
        filename: 'index.html',
        templateParameters: getClientEnv(true),
      }),
      new FriendlyErrorsWebpackPlugin({
        clearConsole: false
      }),
      new ProvidePlugin({
        process: 'process/browser' // fix: Uncaught ReferenceError: process is not defined
      }),
      new IgnorePlugin({
        resourceRegExp: /^\.\/locale$/,
        contextRegExp: /moment$/
      }),
      new CaseSensitivePathsPlugin()
    ],
  • VueLoaderPlugin:vue-loader的插件
  • VueDefineOptions:使用unplugin-vue-define-options vue中可以使用宏 defineOptions
  • ESLintPlugin:eslint-loader已经deprecated,要使用eslint-webpack-plugin
  • ProvidePlugin:webpack内置插件,webpack5取消了注入环境的polyfill

rules

介绍了就是比较重要的模块的处理规则配置,配置各种loader:

javascript 复制代码
function getRules(isProd) {
  return [
    ...getVueRules(),
    ...getAssetRules(),
    ...getCssRules(isProd),
    ...getScssRules(isProd),
    ...getJsRules(isProd),
    ...getTsRules(isProd)
  ];
}

function getStyleRules(isProd, preOptions) {
  return [
    ...getAssetRules(),
    ...getCssRules(isProd),
    ...getScssRules(isProd, preOptions)
  ];
}

js和ts这里都是用了esbuild-loader 代替babel-loader

javascript 复制代码
/* js */
const getJsRules = isProd => [
  {
    test: /\.m?js$/,
    loader: require.resolve('esbuild-loader'),
    include: [resolveApp('src')],
    exclude: jsExclude,
    options: {
      loader: 'js',
      target: isProd ? browserTarget : 'esnext'
    }
  }
];

/* ts */
const getTsRules = isProd => [
  {
    test: /\.ts$/,
    loader: require.resolve('esbuild-loader'),
    include: [resolveApp('src')],
    exclude: /node_modules/,
    options: {
      loader: 'ts',
      target: isProd ? browserTarget : 'esnext'
    }
  }
];

webpack内置了静态资源的处理能力,不需要再额外安装loader。具体看文档,这里仅示例一下:

javascript 复制代码
      {
        type: 'asset', //  url-loader - {maxSize} -  file-loader
        test: /\.(png|jpe?g|gif|webp|avif)(\?.*)?$/,
        generator: {
          filename: genAssetPath('img')
        },
        parser: {
          dataUrlCondition: {
            maxSize: 4 * 1024 // 4kb
          }
        }
      }
   
  • 样式文件的loader通过一个函数获取
javascript 复制代码
function getStyleLoaders(type, extract, preOptions = {}, sourceMap = false) {
  const cssFinalLoader = extract
    ? {
        loader: MiniCssExtractPlugin.loader,
        options: {}
      }
    : {
        loader: require.resolve('vue-style-loader'),
        options: { sourceMap }
      };

  const rules = [
    cssFinalLoader,
    {
      loader: require.resolve('css-loader'),
      options: {
        sourceMap,
        importLoaders: 2
      }
    },
    {
      loader: require.resolve('postcss-loader'), // 配置在 postcss.config.js
      options: { sourceMap }
    }
  ];

  if (type === 'scss') {
    rules.push({
      loader: require.resolve('sass-loader'),
      options: { sourceMap, ...preOptions }
    });
  }

  return rules;
}
  • 默认关闭了 source-map,打开后使用 F12 调试样式可能会引发问题,vue-cli也是默认关闭的。

webpack.dev.js

javascript 复制代码
// webpack.dev.js
module.exports = () => {
  return merge(baseConfig('development'), {
    mode: 'development',
    devServer: {
      static: {
        directory: paths.appPublic,
        publicPath: paths.getPublicPath()
      },
      devMiddleware: {
        // remove last slash so user can land on `/test` instead of `/test/`
        publicPath: paths.getPublicPath().slice(0, -1)
      },
      // history路径在刷新出错时重定向开启
      historyApiFallback: {
        index: paths.getPublicPath()
      },
      open: false,
      proxy,
    },
    cache: {
      type: 'filesystem'
    }
  });
};
  • 代理进行了单独文件的配置,这里就不贴代码了

webpack.prod.js

javascript 复制代码
// webpack.prod.js
module.exports = () => {
  return merge(baseConfig('production'), {
    mode: 'production',
    optimization: {
      minimizer: [new EsbuildPlugin()],
      splitChunks: {
        cacheGroups: {
          defaultVendors: {
            name: 'chunk-vendors',
            test: /[\\/]node_modules[\\/]/,
            priority: -10,
            chunks: 'initial'
          },
          common: {
            name: 'chunk-common',
            minChunks: 2,
            priority: -20,
            chunks: 'initial',
            reuseExistingChunk: true
          }
        }
      }
    },
    plugins: [
      new MiniCssExtractPlugin({
         filename: genAssetPath('css'),
         chunkFilename: genAssetPath('css')
       }),
      new CopyPlugin({
        patterns: [
          {
            from: paths.appPublic,
            to: paths.appOutput,
            globOptions: {
              ignore: ['**/index.html']
            }
          }
        ]
      }),
      new CompressionPlugin({
        algorithm: 'gzip',
        test: /\.(js|css|html)$/,
        threshold: 10240
      })
    ],
    stats: {
      all: false,
      // 生成资源信息
      assets: true,
      cachedAssets: true,
      assetsSpace: 100,
      assetsSort: '!size',
      version: true
    }
  });
};
  • 这里压缩用了esbuild-loader 内置的插件

eslint配置

参考了 create-vue 脚手架的.eslintrc.js配置:

javascript 复制代码
require('@rushstack/eslint-patch/modern-module-resolution');

module.exports = {
  root: true,
  extends: [
    'plugin:vue/essential',
    'eslint:recommended',
    '@vue/eslint-config-typescript/recommended',
    '@vue/eslint-config-prettier'
  ],
  env: {
    node: true
  },
  parserOptions: {
    ecmaVersion: 2022
  },
  rules: {
   // 自定义
  }
};

相关依赖:

javascript 复制代码
"@vue/eslint-config-prettier": "^7.1.0",
"@vue/eslint-config-typescript": "^11.0.3",
"eslint": "^8.46.0",
"eslint-plugin-vue": "^9.16.1",
"eslint-webpack-plugin": "^4.0.1",

.prettierrc.js配置:

javascript 复制代码
module.exports = {
  printWidth: 80, // 换行长度
  tabWidth: 2,
  singleQuote: true, // 使用单引号
  semi: true, // 分号
  trailingComma: 'none', // 无尾随逗号
  arrowParens: 'avoid', // 箭头函数单个参数不加分号
  endOfLine: 'auto' // 换行符
}

ts配置

项目安装的 ts 4.9.5 版本,配置如下:

javascript 复制代码
{
  "compilerOptions": {
    "module": "ESNext",
    "target": "ESNext",
    "lib": ["ESNext", "DOM", "DOM.Iterable"],
    "baseUrl": ".",

    "moduleResolution": "Node",
    "resolveJsonModule": true,

    // class-field
    "useDefineForClassFields": true,

    "ignoreDeprecations": "5.0",

    // Required in Vue projects
    "jsx": "preserve",

    // `"noImplicitThis": true` is part of `strict`
    // Added again here in case some users decide to disable `strict`.
    // This enables stricter inference for data properties on `this`.
    "noImplicitThis": true,
    "strict": true,
    "allowJs": true,

    // esbuild
    "isolatedModules": true,
    // For `<script setup>`
    // See <https://devblogs.microsoft.com/typescript/announcing-typescript-4-5-beta/#preserve-value-imports>
    "preserveValueImports": true,
    // Enforce using `import type` instead of `import` for types
    "importsNotUsedAsValues": "error",

    // Recommended
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,

    // See <https://github.com/vuejs/vue-cli/pull/5688>
    "skipLibCheck": true,
    "paths": {
      "@/*": ["./src/*"]
    },
    "types": [
      "node",
      "webpack-env"
    ]
  },

  "vueCompilerOptions": {
    "target": 2.7
  },
  "include": ["src/**/*", "src/**/*.vue", "tests/**/*"],
  "exclude": ["node_modules"]
}

总结

本文通过使用 webpack5 从0到1搭建项目脚手架,参考业内的很多最近实践。使用了 vue2.7+ts 提高了项目的整体可维护性,使用 esbuild-loader 以及 webpack5 的缓存提高了项目的整体构建速度。

目前不足之处是,没有开发cli 交互,配置文件全部暴露,需要了解 webpack 的配置才能进行个性化配置,后续可进行抽象和封装,加入cli 交互。

参考

  • vue-cli
  • create-vue
  • create-react-app
相关推荐
梦境之冢30 分钟前
axios 常见的content-type、responseType有哪些?
前端·javascript·http
racerun33 分钟前
vue VueResource & axios
前端·javascript·vue.js
m0_548514771 小时前
前端Pako.js 压缩解压库 与 Java 的 zlib 压缩与解压 的互通实现
java·前端·javascript
AndrewPerfect1 小时前
xss csrf怎么预防?
前端·xss·csrf
Calm5501 小时前
Vue3:uv-upload图片上传
前端·vue.js
浮游本尊1 小时前
Nginx配置:如何在一个域名下运行两个网站
前端·javascript
m0_748239831 小时前
前端bug调试
前端·bug
m0_748232921 小时前
[项目][boost搜索引擎#4] cpp-httplib使用 log.hpp 前端 测试及总结
前端·搜索引擎
新中地GIS开发老师1 小时前
《Vue进阶教程》(12)ref的实现详细教程
前端·javascript·vue.js·arcgis·前端框架·地理信息科学·地信
漫天转悠1 小时前
Vue3中404页面捕获(图文详情)
vue.js