webpack从0搭建一个vue项目(搭建vue开发环境)

524 阅读4分钟

1. 开篇

1.1 Vue CLI搭建项目

vue create <project-name>  # Vue CLI >= 3
# vue init <template-name> <project-name> (vue CLI 2.x)

Vue CLI可以生成vue.js+webpack的项目模板,用于快速搭建vue.js项目。

Vue CLI 2.x 对应命令是从vuejs-templates/webpack仓库中拉取模板,生成项目文件。

1.2 Vue CLI内置了哪些webpack的功能

  • ES6+代码转换为ES5代码(处理JS代码兼容性问题)
  • 解析scss/sass/less/stylus资源为css资源
  • 处理图片、字体等资源文件
  • 自动给css代码添加浏览器厂商前缀(处理CSS兼容性问题)
  • 代码热更新
  • 资源预加载
  • 每次构建代码之前清除之前生成的代码
  • 定义环境变量
  • 区分开发环境和生产环境打包
  • ... 下面的命令查看Vue CLI创建的项目对应的webpack配置文件:
vue-cli-service inspect --mode development >> webpack.dev.js # 开发环境
vue-cli-service inspect --mode production >> webpack.prod.js # 生产环境

2. 不同模式下的webpack配置

webpack提供了mode配置选项,用来告知webpack使用相应模式的内置优化,我们也可以根据模式的不同编写不同的配置文件,提供特定模式下的优化。

2.1 开发模式和生产模式的区别

开发模式和生产模式的区别:

  • 内置优化的不同:参考链接
  • 构建目标不同:
    • 开发环境模式下,我们需要强大的、具有实时重新加载或热模块替换能力和本地服务器。
    • 在生产模式下,我们更关注更小的包和资源的优化。

2.2 基础配置

2.2.1 .vue文件相关依赖

安装:

npm i vue -S     
npm i vue-loader -D
npm i @vue/compiler-sfc -D

vue-loader:解析.vue文件,解析和转换.vue文件。提取出其中的逻辑代码 script,样式代码style,以及HTML模板template,再分别把他们交给对应的loader去处理。参考链接

@vue/compiler-sfc:将解析完的vue单页面组件(sfc)编译为js。参考链接

配置:

const { VueLoaderPlugin } = require('vue-loader');
module: {
    rules: [
        {
            test: /\.vue$/,
            loader: 'vue-loader',
        },
    ];
}
plugins: [
        new VueLoaderPlugin(),
]

2.2.2 处理样式资源

处理CSS、Less、Sass、Scss和Styl样式资源。css代码提取成单独文件。css兼容性处理。css压缩。

多个loader配合使用时,处理的顺序是从下到上,从右到左。

安装:

npm i css-loader style-loader less-loader sass-loader sass stylus-loader mini-css-extract-plugin postcss-loader postcss postcss-preset-env css-minimizer-webpack-plugin -D
  • css-loader:分析各个css文件的关系,把各个css文件合并成一段css代码
  • style-loader:动态创建一个style标签,里面放经过css-loader处理得到的css代码
  • less-loader:把less代码编译成css代码
  • sass-loader:加载sass/scss文件并把它们编译成css代码
  • stylus-loader: 把stylus文件编译成css文件
  • mini-css-extract-plugin:把css代码提取成单独文件
  • css-minimizer-webpack-plugin:优化和压缩css

配置:

const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const getStyleLoaders = (preProcessor) => {
  return [
    MiniCssExtraPlugin.loader,
    'css-loader',
    {
      loader: 'postcss-loader',
      options: {
        postcssOptions: {
          plugins: [
            'postcss-preset-env'  // 能解决大多数样式兼容性问题
          ]
        }
      }
    },
    preProcessor
  ].filter(Boolean);
};
module.exports = {
    ...
    module: {
        rules: [
            {
              test: /\.css$/,
              use: getStyleLoaders()
             },
            {
              test: /\.less$/,
              use: getStyleLoaders('less-loader')
            }
        ]
    },
    plugins: [
        new MiniCssExtraPlugin({
          filename: 'css/index.css'
        }),
        new CssMinimizerPlugin()
    ]
}

控制兼容性:

package.json文件中,添加browserslist控制兼容性做到什么程度。browserslist 文档

{
  // 其他省略
  "browserslist": ["last 2 version", "> 1%", "not dead"]
}

2.2.3 处理图片资源

在webpack4中,处理图片资源是通过file-loader和url-loader来处理的,webpack5已经把这两个loader功能内置到webpack中了。

优化:将小于某个大小的图片转化成data URI形式(base64格式)

  • 优点:减少请求数量
  • 缺点:体积变大

配置:

module.exports = {
    module: {
        rules: [
            {
                test: /\.(png|jpe?g|gif|webp)$/,
                type: 'asset',
                parser: {
                    dataUrlCondition: {
                        maxSize: 10 * 1024 // 小于10kb的图片会被base64处理
                    },
                    generator: {
                      filename: 'static/images/[hash:8][ext][query]'    // 输出到指定文件夹下
                    }
                }
            }
        ]
    }
}

2.2.5 处理字体图标资源

配置:

module.exports = {
    ...
    module: {
        rulse: [
          {
            test: /.(ttf|woff2?)$/,
            type: "asset/resource",
            generator: {
              filename: "static/media/[hash:8][ext][query]",
            },
          }
        ]
    }
}

type: "asset/resource"type: "asset"的区别:

  1. type: "asset/resource" 相当于file-loader, 将文件转化成 Webpack 能识别的资源,其他不做处理
  2. type: "asset" 相当于url-loader, 将文件转化成 Webpack 能识别的资源,同时小于某个大小的资源会处理成 data URI 形式

2.2.6 处理js资源

  • 针对代码格式,使用eslint完成
  • js兼容性处理,使用babel完成
Eslint

可组装的JavaScript和JSX检查工具。

安装:

npm i eslint-webpack-plugin eslint -D

生成eslint配置文件:

npx eslint --init

根据提示选择配置 生成的.eslintrc.json文件:

{
  "env": {
    "browser": true,  // 可以使用window对象
    "es2021": true   // 可以使用es2021的语法
  },
    // 继承其他规则
  "extends": [
    "eslint:recommended",  // 使用这个集合的代码规范
    "plugin:vue/essential"
  ],
  // 解析选项
  "parserOptions": {
    "ecmaVersion": "latest", //ES 语法版本
    "sourceType": "module"  // ES 模块化
  },
  // 引用额外的rules
  "plugins": [
    "vue"
  ],
  // 具体规则,可能会覆盖extends继承的规则(位置靠后的覆盖前面的)
  "rules": {
  }
}

Eslint规则参考

Babel

主要作用:ES6+语法编写的代码转换为向后兼容的JS语法,以便能够运行在当前和旧版本的浏览器或其他环境中

安装:

npm i babel-loader @babel/core @babel/preset-env -D

配置:

module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /mode_modules/,
                loader: 'babel-loader'
            }
        ]
    }
}

2.2.7 处理html资源

安装:

npm i html-webpack-plugin -D

配置:

const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    ...
    plugins: [
        new HtmlWebpackPlugin({
            template: path.join(__dirname, 'index.html'),
            filename: 'index.html'
        })
    ]
}

2.2.8 自动清除上次的打包文件

配置:

module.exports = {
    output: {
        ...
        clean: true  // 自动将上次打包目录资源清空
    }
}

2.2.9 开启服务器&自动化

安装:

npm i webpack-dev-server -D

配置:

module.exports = {
    ...
    devServer: {
        host: 'localhost',  //启动服务器域名
        port: 3001,  // 启动服务器端口号
        open: true  // 自动打开浏览器
    }
}

运行脚本:

npx webpack serve

2.3 优化配置

2.3.1 提升开发体验

SourceMap

SourceMap(源代码映射)是一个用来生成源代码和构建后代码一一映射的方案。它会生成一个xxx.map文件,里面包含源代码和构建后代码每一行、每一列的映射关系。让浏览器提示源代码文件出错位置,帮助我们更快的找到错误根源。

开发模式配置:

module.exports = {
    devtool: 'cheap-module-source-map' 
}
  • 优点:打包编译速度快,只包含行映射
  • 缺点:没有列映射

生产模式配置:

module.exports = {
    devtool: 'source-map' 
}
  • 优点:包含行/列映射
  • 缺点:打包编译速度更慢

2.3.2 提升打包构建速度

OneOf

打包的时候,每个文件都会经过所有loader处理,因为test正则匹配原因,实际上可能只有一个loader处理。OneOf设置每个文件只匹配一个loader,剩下的就不匹配了。 配置:

module.exports = {
    module: {
        rules: [
            {
                OneOf: [
                    {
                        test: /\.css$/,
                        use: ['style-loader', 'css-loader']
                    },
                    ...
                ]
            }
        ]
    }
}
HotModuleReplaceMent

HotModuleReplaceMent(HMR/模块热替换):在程序运行时,替换、添加或删除模块,而无需重新加载整个页面。

配置:

module.exports = {
  // 其他省略
  devServer: {
    host: "localhost", // 启动服务器域名
    port: "3000", // 启动服务器端口号
    open: true, // 是否自动打开浏览器
    hot: true, // 开启HMR功能(只能用于开发环境,生产环境不需要了)
  },
};

html,css代码这时已经支持HMR功能了,但是js代码不支持。

js配置:

// 判断是否支持HMR功能
if (module.hot) {
  module.hot.accept("./js/tab.js", function () {
    console.log('我现在已经支持HMR功能啦!')
  });
}

vue代码的HMR支持由vue-loader处理。

Cache

每次打包js文件时,都要经过eslint和babel编译,主要缓存eslint检查结果和babel编译结果。 配置:

module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.js$/,
                include: path.resolve(__dirname, '../src'),
                loader: 'babel-loader',
                options: {
                    cacheDirectory: true, // 开启babel编译缓存
                    cacheCompression: false // 缓存文件不要压缩
                }
            }
        ]
    },
    plugins: [
        new ESlintWebpackPlugin({
            context: path.resolve(__dirname, '../src'),
            exclude: 'node_modules',
            cache: true,  // 开启缓存
            cacheLocation: path.resolve(
                __dirname,
                '../node_modules/.cache/.eslintcache'
            )
        })
    ]
}

2.3.3 减少代码体积

Tree Shaking

Tree Shaking(摇树优化)指的是一种通过移除多于代码,来优化打包体积的,生产环境默认开启。我们也可以自己配置。

// webpack.config.js
module.exports = {
    usedExports: true,
    minimize: true
}
// package.json
"sideEffects": [
    "*.css",
    "*.less",
    "*.vue"
]
Babel

Babel为编译的每个文件都插入了辅助代码,会使得代码体积变大。
@babel/plugin-transform-runtime是一个可以重用babel辅助代码的插件,可以减少代码体积大小。

安装:

npm install --save-dev @babel/plugin-transform-runtime

配置:

module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: [
                    {
                        loader: 'babel-loader',
                        options: {
                            cacheDirectory: true,
                            cacheCompression: false,
                            plugins: ['@babel/plugin-transform-runtime'] // 减少代码体积
                        }
                    }
                ]
            }
        ]
    }
}
Scope Hoisting

Scope Hoisting 又称为“作用域提升”,可以让webpack打包出来的代码文件更小、运行的更快。

原理:分析出模块之间的依赖关系,尽可能的把打散的模块合并到一个函数中去,但前提是不能造成代码冗余。因此,只有那些被引用了一次的模块才能被合并。

2.3.4 优化代码运行性能

Code Split

对于单入口文件来说,webpack打包代码时会把所有js文件打包到一个文件中,体积太大。如果我们只渲染首页,就应该只加载首页的js文件,其他文件不应该加载。

配置:

module.exports = {
    optimization: {
        chunks: "all", // 对所有模块都进行分割
      // 以下是默认值
      // minSize: 20000, // 分割代码最小的大小
      // minRemainingSize: 0, // 类似于minSize,最后确保提取的文件大小不能为0
      // minChunks: 1, // 至少被引用的次数,满足条件才会代码分割
      // maxAsyncRequests: 30, // 按需加载时并行加载的文件的最大数量
      // maxInitialRequests: 30, // 入口js文件最大并行请求数量
      // enforceSizeThreshold: 50000, // 超过50kb一定会单独打包(此时会忽略minRemainingSize、maxAsyncRequests、maxInitialRequests)
      // cacheGroups: { // 组,哪些模块要打包到一个组
      //   defaultVendors: { // 组名
      //     test: /[\/]node_modules[\/]/, // 需要打包到一起的模块
      //     priority: -10, // 权重(越大越高)
      //     reuseExistingChunk: true, // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块
      //   },
      //   default: { // 其他没有写的配置会使用上面的默认值
      //     minChunks: 2, // 这里的minChunks权重更大
      //     priority: -20,
      //     reuseExistingChunk: true,
      //   },
      // },
    }
}

2.4 webpack.common.js

const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { VueLoaderPlugin } = require('vue-loader');

const getStyleLoaders = (preProcessor) => {
  return [
    MiniCssExtractPlugin.loader,
    'css-loader',
    {
      loader: 'postcss-loader',
      options: {
        postcssOptions: {
          plugins: [
            'postcss-preset-env'  // 能解决大多数样式兼容性问题
          ]
        }
      }
    },
    preProcessor
  ].filter(Boolean);
};

module.exports = {
  entry: './src/main.js',
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: ['vue-loader']
      },
      {
        oneOf: [
          {
            test: /\.css$/,
            use: getStyleLoaders()
          },
          {
            test: /\.less$/,
            use: getStyleLoaders('less-loader')
          },
          {
            test: /\.(png|jpe?g|gif|webp)$/,
            type: 'asset',
            parser: {
              dataUrlCondition: {
                maxSize: 10 * 1024
              }
            },
            generator: {
              filename: 'static/images/[hash:8][ext][query]'
            }
          },
          {
            test: /\.js$/,
            exclude: /node_modules/,
            loader: 'babel-loader',
            options: {
              cacheDirectory: true, // 开启babel编译缓存
              cacheCompression: false, // 缓存文件不要压缩
              plugins: ['@babel/plugin-transform-runtime']
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin(),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, '../public/index.html')
    }),
    new MiniCssExtractPlugin({
      filename: 'css/[name].css'
    })
  ],
  resolve: {
    alias: {
      '@': '../src',
      vue$: 'vue/dist/vue.runtime.esm.js'
    },
    extensions: [
      '.mjs',
      '.js',
      '.jsx',
      '.vue',
      '.json',
      '.wasm'
    ],
    modules: [
      'node_modules'
    ]
  }
}

2.5 webpack.dev.js

const path = require('path');
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, {
  mode: 'development',
  // devtool: 'cheap-source-map',
  devtool: 'cheap-module-source-map',
  entry: {
    index: './src/main.js'
  },
  output: {
    path: undefined,
    filename: 'js/[name].js',
    publicPath: '/',
    chunkFilename: 'js/[name].js'
  },
  devServer: {
    hot: true,
    port: 3000
  },
  optimization: {
    usedExports: false
  }
});

运行脚本:

"dev": "webpack serve --config ./build/webpack.dev.js"

2.6 webpack.prod.js

const path = require('path');
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = merge(common, {
  mode: 'production',
  // devtool: 'source-map',
  output: {
    filename: 'js/[name].js',
    path: path.resolve(__dirname, '../dist'),
    clean: true
  },
  plugins: [
    new CssMinimizerPlugin()
  ],
  optimization: {
    minimize: true,
    usedExports: true,
    splitChunks: {
      chunks: 'all'
    }
  }
});

运行脚本:

"build": "webpack --config ./build/webpack.prod.js"