在webpack5中利用es-loader代替babel-loader编译vue2项目, 性能提升10倍以上,同时支持vue2 jsx

168 阅读2分钟

github完整示例项目

版本依赖

 "vue-loader": "^15.9.8",
 "webpack": "^5.72.0",
 "webpack-cli": "^4.9.2",
 "webpack-dev-server": "^4.9.0",
 "css-loader": "^6.7.1",
 "esbuild-loader": "^3.0.1",
 "mini-css-extract-plugin": "^2.6.0",
 "dotenv-webpack": "^7.1.0",
 "clean-webpack-plugin": "^4.0.0",
 "copy-webpack-plugin": "^11.0.0",
 "vue": "^2.7.14",
 "vue-template-compiler": "^2.7.14",
 "element-ui": "^2.12.0",

完整的package.json

{
  "name": "project-vue",
  "version": "1.0.0",
  "description": "webpack搭建vue项目",
  "private": true,
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": " cross-env NODE_ENV=development node_modules/.bin/webpack serve  --config webpack.config.js",
    "build": "cross-env NODE_ENV=production  webpack --config=webpack.config.js",
    "lint": "npx lint-staged"
  },
  "author": "gmm",
  "license": "ISC",
  "devDependencies": {
    "autoprefixer": "^10.4.7",
    "buffer": "^6.0.3",
    "clean-webpack-plugin": "^4.0.0",
    "copy-webpack-plugin": "^11.0.0",
    "cross-env": "^7.0.3",
    "crypto-browserify": "^3.12.0",
    "css-loader": "^6.7.1",
    "css-minimizer-webpack-plugin": "^3.4.1",
    "dotenv-webpack": "^7.1.0",
    "esbuild-loader": "^3.0.1",
    "eslint": "^8.15.0",
    "eslint-plugin-vue": "^8.7.1",
    "html-webpack-plugin": "^5.5.0",
    "husky": "^7.0.4",
    "less-loader": "^10.2.0",
    "lint-staged": "^12.4.1",
    "mini-css-extract-plugin": "^2.6.0",
    "path-browserify": "^1.0.1",
    "postcss-loader": "^6.2.1",
    "prettier": "^2.6.2",
    "progress-bar-webpack-plugin": "^2.1.0",
    "stream-browserify": "^3.0.0",
    "vue-loader": "^15.9.8",
    "vue-router": "^3.6.5",
    "vue-style-loader": "^4.1.3",
    "webpack": "^5.72.0",
    "webpack-cli": "^4.9.2",
    "webpack-dev-server": "^4.9.0"
  },
  "dependencies": {
    "element-ui": "^2.12.0",
    "expression-eval": "^3.1.1",
    "query-string": "^4.3.4",
    "vue": "^2.7.14",
    "vue-template-compiler": "^2.7.14",
    "vue-cookies": "^1.8.3",
    "vuex": "^3.6.2"
  },
  "lint-staged": {
    "src/**/*.{js,vue}": [
      "npx eslint --fix",
      "git add"
    ]
  },
  "browserslist": [
    "defaults",
    "not ie < 11",
    "last 2 versions",
    "> 1%",
    "iOS 7",
    "last 3 iOS versions"
  ]
}

添加别名jsxFactory

 resolve: {
    modules: [path.resolve(__dirname, "src"), "node_modules"],
    extensions: [".js", ".vue"], // 引入路径是不用写对应的后缀名
    alias: {
      jsxFactory: path.resolve(__dirname, "./src/jsxFactory/index.js"),
      "@": path.resolve(__dirname, "./src"), // 用@直接指引到src目录下
    },
  },

rule js部分配置

  {
        test: /\.(js|jsx|ts|tsx)$/,
        loader: "esbuild-loader",
        options: {
          loader: "jsx",
          jsx: "transform",
          jsxFactory: "jsxFactory",
          target: "es2015",
        },
        include: [path.resolve(__dirname, "src")],
      },

引入esbuild插件

const { EsbuildPlugin } = require("esbuild-loader");

在plugins中加上部分配置(暴露jsxFactory全局变量)

 plugins:[new webpack.ProvidePlugin({ jsxFactory: ["jsxFactory", "default"], })]

css采用esbuild压缩,编译结果为es2015(esbuild最低版本只支持es2015)

optimization: {
    nodeEnv: false,
      minimizer: [
      new EsbuildPlugin({
        target: "es2015", 
        css: true,
      }),
    ],
  },

完整的webpack.config.js

const path = require("path"); // 引入node内置模块path
const HtmlWebpackPlugin = require("html-webpack-plugin"); // 构建html文件
const VueLoaderPlugin = require("vue-loader/lib/plugin-webpack5");
const { CleanWebpackPlugin } = require("clean-webpack-plugin"); // 清理构建目录下的文件
const ProgressBarWebpackPlugin = require("progress-bar-webpack-plugin"); // 设置打包进度条
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); // webpack4以后 改为此插件 css样式分离
const Dotenv = require("dotenv-webpack"); // 支持程序获取.env配置的环境变量
const { EsbuildPlugin } = require("esbuild-loader");
const webpack = require("webpack");

module.exports = {
  cache: process.env.NODE_ENV === "production" ? false : true,
  mode: process.env.NODE_ENV === "production" ? "production" : "development", // 开发模式
  stats: "errors-only", // 日志打印只打印错误
  devServer: {
    open: false, // 自动打开浏览器
    hot: process.env.NODE_ENV === "production" ? false : true, // 热更新打开
    host: "localhost",
    port: "8899", // 端口:8888
    client: {
      overlay: false,
    },
    proxy: {},
  },
  entry: {
    app: {
      import: "./src/main.js",
    },
  },
  output: {
    // 出口文件
    path: path.resolve(__dirname, "dist"), // 出口路径和目录
    filename: "js/[name].js", // 编译后的名称
    clean: true,
    chunkFilename: "js/[name][id].js",
    asyncChunks: true,
  },
  resolve: {
    modules: [path.resolve(__dirname, "src"), "node_modules"],
    extensions: [".js", ".vue"], // 引入路径是不用写对应的后缀名
    alias: {
      jsxFactory: path.resolve(__dirname, "./src/jsxFactory/index.js"),
      vue$: "vue/dist/vue.esm.js", // 正在使用的是vue的运行时版本,而此版本中的编译器时不可用的,我们需要把它切换成运行时 + 编译的版本
      "@": path.resolve(__dirname, "./src"), // 用@直接指引到src目录下
    },
    fallback: {
      crypto: require.resolve("crypto-browserify"),
      buffer: require.resolve("buffer/"),
      path: require.resolve("path-browserify"),
      stream: require.resolve("stream-browserify"),
    },
  },
  optimization: {
    nodeEnv: false,
    splitChunks: {
      chunks: "all",
      minChunks: 1, //拆分前必须共享模块的最小 chunks 数。
      minSize: 0,
      cacheGroups: {
        vueLib: {
          minChunks: 1,
          test: /[\\/]node_modules[\\/](vue|vue-router|vuex|axios|vuex-persistedstate)[\\/]/,
          name: "vueLib",
          chunks: "all",
        },
      },
    },
    minimizer: [
      new EsbuildPlugin({
        target: "es2015", // Syntax to compile to (see options below for possible values)
        css: true,
      }),
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      // 自动插入到dist目录中
      title: process.env.VUE_APP_TITLE,
      template: "public/index.html",
      // favicon: path.resolve(__dirname, `dist/eGreatWall.ico`), //配置网站图标
      inject: "body",
    }),
    new VueLoaderPlugin(),
    new webpack.HotModuleReplacementPlugin(),
    new webpack.ProvidePlugin({
      Vue: ["vue/dist/vue.esm.js", "default"],
    }),
    process.env.NODE_ENV === "production" && new CleanWebpackPlugin(),
    new ProgressBarWebpackPlugin({
      complete: "█",
      clear: true,
    }),
    new MiniCssExtractPlugin({
      filename:
        process.env.NODE_ENV === "production"
          ? "css/[name].[contenthash].css"
          : "css/[name].css",
      chunkFilename:
        process.env.NODE_ENV === "production"
          ? "css/[name].[contenthash].css"
          : "css/[name].css",
    }),
    new Dotenv({
      path: path.resolve(__dirname, `./.env.${process.env.NODE_ENV}`),
    }),
    new webpack.ProvidePlugin({
      jsxFactory: ["jsxFactory", "default"],
    }),
  ],
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: "vue-loader",
        include: [path.resolve(__dirname, "src")],
      },
      {
        test: /\.css$/,
        use: [
          process.env.NODE_ENV === "production"
            ? MiniCssExtractPlugin.loader
            : "vue-style-loader",
          "css-loader",
        ],
      },
      {
        test: /.less$/,
        use: [
          process.env.NODE_ENV === "production"
            ? MiniCssExtractPlugin.loader
            : "vue-style-loader",
          "css-loader",
          "postcss-loader",
          "less-loader",
        ],
      },
      {
        test: /\.(js|jsx|ts|tsx)$/,
        loader: "esbuild-loader",
        options: {
          loader: "jsx",
          jsx: "transform",
          jsxFactory: "jsxFactory",
          target: "es2015",
        },
        include: [path.resolve(__dirname, "src")],
      },
    ],
  },
};

由于esbuild转换的jsx和vue jsx有差异,因此需要写转换函数对其转换一下 jsx转换 group-props.js文件

/**
 *
 *  参考文档 深入数据对象 https://v2.cn.vuejs.org/v2/guide/render-function.html#%E6%B7%B1%E5%85%A5%E6%95%B0%E6%8D%AE%E5%AF%B9%E8%B1%A1
 *  参考文档 插件babel-plugin-transform-vue-jsx https://github.com/vuejs/babel-plugin-transform-vue-jsx/blob/master/lib/group-props.js
 *
 *
 */
 
import { h } from "vue";
 
function isTopLevelWrap() {
  var map = Object.create(null);
  var list = [
    "class",
    "staticClass",
    "style",
    "key",
    "ref",
    "refInFor",
    "slot",
    "scopedSlots",
  ];
  let len = list.length;
  for (var i = 0; i < len; i++) {
    map[list[i]] = true;
  }
  return (val) => map[val];
}
let isTopLevel = isTopLevelWrap();
// https://v2.cn.vuejs.org/v2/guide/render-function.html#%E6%B7%B1%E5%85%A5%E6%95%B0%E6%8D%AE%E5%AF%B9%E8%B1%A1
let nestableReg = /^(props|domProps|on|nativeOn|hook)([-_A-Z])/;
let dirReg = /^v-/;
let xlinkReg = /^xlink([A-Z])/;

/**
 * groupProps
 * @param {*} obj
 * @returns
 */
function groupProps(obj) {
  let currentNewPropObjects = Object.create(null);
  let props = [];
  console.warn("obj", JSON.stringify(obj));
  Object.keys(obj).forEach(function (key) {
    props.push({
      key,
      value: obj[key],
    });
  });
  props.forEach(function (prop) {
    let name = prop.key;
    if (isTopLevel(name)) {
      currentNewPropObjects[name] = prop.value;
    } else {
      let nestMatch = name.match(nestableReg);

      if (nestMatch) {
        let prefix = nestMatch[1];
        let suffix = name.replace(nestableReg, function (_, $1, $2) {
          return $2 === "-" ? "" : $2.toLowerCase();
        });

        let nestedProp = { [suffix]: prop.value };

        if (!currentNewPropObjects[prefix]) {
          currentNewPropObjects[prefix] = {};
        }
        Object.assign(currentNewPropObjects[prefix], nestedProp);
      } else if (dirReg.test(name)) {
        name = name.replace(dirReg, "");
        let dirs = currentNewPropObjects.directives;
        if (!dirs) {
          dirs = currentNewPropObjects.directives = [];
        }
        dirs.push({
          name: name,
          value: prop.value,
        });
      } else {
        if (xlinkReg.test(prop.key)) {
          prop.key = JSON.stringify(
            prop.key.replace(xlinkReg, function (m, p1) {
              return "xlink:" + p1.toLowerCase();
            })
          );
        }
        if (!currentNewPropObjects.attrs) {
          currentNewPropObjects.attrs = {};
        }
        currentNewPropObjects.attrs[prop.key] = prop.value;
      }
    }
  });
  console.log("currentNewPropObjects", JSON.stringify(currentNewPropObjects));
  return currentNewPropObjects;
}

/**
 *
 * @param {*} tag
 * @param {*} props
 * @param {*} children
 * @returns
 */
function vueJsx(tag, props = null, ...children) {
  const newPros = props ? groupProps(props) : {};
  return h(tag, newPros, children);
}
export default vueJsx;

以上,就能在vue2项目中使用es-build了,速度提升10倍以上,编译产物为es2015