webpack5 从零开始搭建 vue 项目

作者: tww844475003 分类: 前端开发 发布时间: 2021-08-17 21:22
  1. 基础用法
  2. 进阶用法
  3. 构建速度和体积优化

一、基础用法

  • 初始化项目

mkdir build-webpack && cd build-webpack

npm init -y

npm i -D webpack webpack-cli@3.3.12

注意:由于 webpack-cli4版本也 webpack-dev-server 最新版不兼容,固这里安装的是webpack-cli 底版本,不然热刷新报错。

新建 src/index.js

'use strict'

function hello() {
  return 'hello webpack!'
}

document.write(hello());

根目录新建 webpack.config.js

'use strict'

const path = require('path');;

module.exports = {
  mode: 'production', // 打包模式 development、production
  // 入口
  entry: {
    index: './src/index.js'
  },
  // 输出
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].js'
  }
}

package.json scripts 新增打包命令

"build": "webpack",

这样一个 webpack 基础的打包工程就完成了,当然这个工程也仅公只能支持 js、json 文件打包。下面我们来通过 loader、plugin 扩展这个基础工程,使他成为名副其实的前端通用工程。

  • 支持ES6(Babel)

src/index.js

'use strict'



const users = ['zhangsan', 'lishi'];
users.forEach(item => {
   document.write(item)
})

比如上面这段带有ES6新特性代码,在某些浏览器下是不能去接运行的,这就需要进行 babel 转换

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

根目录新建 .babelrc

{
  "presets": [
    "@babel/preset-env"
  ]
}

webpack.config.js

  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'babel-loader'
      }
    ]
  },
  • 支持 vue

由于 vue 模板里里边有 style css,还需要安装对应loader。vue-style-loader css-loader

npm i -S vue vue-template-compiler

npm i -D vue-loader vue-style-loader css-loader

webpack.config.js 新增对应 loader plugin

const VueLoaderPlugin = require('vue-loader/lib/plugin');

  module: {
    rules: [
      {
        test: /\.vue$/,
        use: 'vue-loader'
      },
      {
        test: /\.css$/,
        use: [
          'vue-style-loader',
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin()
  ]

webpack 的配置算是完成,下面我们来写个基础组件

index.js

'use strict'

import Vue from 'vue';
import Hello from './components/hello.vue';

new Vue({
  render: (h) => h(Hello)
}).$mount('#app')

hello.vue

<template>
  <div class="hello">
    <p>{{text}}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      text: 'hello webpack!'
    }
  }
}
</script>

<style lang="css" scoped>
.hello {
  font-size: 20px;
  color: red;
}
</style>
  • html-webpack-plugin

每次 build 根据模块文件生成对就的 html 文件

npm i -D html-webpack-plugin

webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin');

  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
      filename: 'index.html'
    })
  ]
  • clean-webpack-plugin

每次打包前清空上次 build 生成的 dist 目录

npm i -D clean-webpack-plugin

webpack.config.js

const { CleanWebpackPlugin } = require('clean-webpack-plugin');

  plugins: [
    new CleanWebpackPlugin()
  ]
  • less

支持 css 预处理

npm i -D less less-loader

  module: {
    rules: [
      {
        test: /\.less$/,
        use: [
          'vue-style-loader',
          'css-loader',
          'less-loader'
        ]
      }
    ]
  }
  • 图片处理

npm i -D file-loader url-loader

  module: {
    rules: [
      {
        test: /\.(jpg|jpeg|png|gif)$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 10240
            }
          }
        ]
      }
    ]
  }
  • 字体处理


module: {
    rules: [
      {
        test: /\.(ttf|eot|svg|woff|woff2)$/,
        use: 'file-loader'
      }
    ]
  }

  • text 处理

npm i -D raw-loader


module: {
    rules: [
      {
        test: /\.txt$/,
        use: 'raw-loader'
      }
    ]
  }

  • 资源文件 webpack5 asset 处理

webpack5使用四种新增的资源模块(Asset Modules)替代了这些loader的功能。

asset/resource 将资源分割为单独的文件,并导出url,就是之前的 file-loader的功能.
asset/inline 将资源导出为dataURL(url(data:))的形式,之前的 url-loader的功能.
asset/source 将资源导出为源码(source code). 之前的 raw-loader 功能.
asset 自动选择导出为单独文件或者 dataURL形式(默认为8KB). 之前有url-loader设置asset size limit 限制实现。

module: {
  rules: [
     {
        test: /\.(jpg|jpeg|png|gif|svg)$/i,
        type: 'asset',
        generator: {
          filename: 'images/[hash][ext][query]'
        }
      },
      {
        test: /\.(ttf|eot|woff|woff2|otf)$/i,
        type: 'asset/resource',
        generator: {
          filename: 'fonts/[hash][ext][query]'
        }
      },
      {
        test: /\.txt$/,
        type: 'asset',
        generator: {
          filename: 'files/[hash][ext][query]'
        },
        parser: {
          dataUrlCondition: {
            maxSize: 4 * 1024 // 4kb  指定大小
          }
        }
      }
  ]
}
  • 文件指纹策略:chunkhash、contenthash和hash

文件指纹用作版本管理及浏览器缓存

  1. Hash:整个项目的构建相关,即只要项目有文件修改, hash 值就会更改
  2. Chunkhash:不同的 entry 会生成不同的 chunkhash 值
  3. Contenthash:根据文件内容定义 hash

一般 entry 里边采用 chunkhash,css,img 资源采用 contenthash

  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name][chunkhash:8].js'
  },
module: {
    rules: [
      {
        test: /\.(jpg|jpeg|png|gif)$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 10240,
              name: '[name]_[hash:8].[ext]'
            }
          }
        ]
      },
      {
        test: /\.(ttf|eot|svg|woff|woff2)$/,
        use: {
          loader: 'file-loader',
          options: {
            name: '[name]_[hash:8].[ext]'
          }
        }
      }
    ]
  },

css 文件指纹由于前面是设置 vue-style-loader 内联在 html head 里并没有打包成单独的 css 文件,需要借助一个插件,打包在独立的 css 文件,才能看到 文件指纹效果

  • mini-css-extract-plugin

npm i -D mini-css-extract-plugin

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name]_[contenthash:8].css'
    })
  ]
  module: {
    rules: [

      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      },
      {
        test: /\.less$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'less-loader'
        ]
      }
    ]
  }
  • PostCSS插件autoprefixer自动补齐CSS3前缀

npm i -D postcss-loader autoprefixer@8.0.0

注意这里安装的postcss-loader postcss8版本,最新版会报错

Error: PostCSS plugin autoprefixer requires PostCSS 8. Update PostCSS or downgrade this plugin.

package.json

  "browserslist": [
    "defaults",
    "not ie < 8",
    "last 2 versions",
    "> 1%",
    "iOS 7",
    "last 3 iOS versions"
  ]

根目录新建 postcss 配置文件 postcss.config.js

module.exports = {
  plugins:[
    require('autoprefixer')
  ]
}

module.rules 改造,注意 less-loader 与 postcss-loader 不能调换,不然 vue template 里的 style 样式不能被补全,只能补全通过 import ‘***.less|.css’ 的样式文件

  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'postcss-loader'
        ]
      },
      {
        test: /\.less$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'postcss-loader',
          'less-loader'
        ]
      }
    ]
  },
  • CssNext

npm i -D postcss-cssnext

'use strict'


const postcssCssnext = require('postcss-cssnext');

module.exports = {
  plugins: [
    postcssCssnext({
      browsers: [
        '> 1%',
        'last 2 versions'
      ]
    })
  ]
}
:root {
  --heighlightColor: hwb(190, 35%, 20%);
}

body {

  color: var(--heighlightColor);
}

打包输出

body {
  color: rgb(89, 185, 204);
}

该插件可以转换一些 css 新语法特性

具体新语法参考:http://caibaojian.com/scb/cssnext.html

  • 设置别名
  resolve: {
    // 设置别名
    alias: {
      '@': path.join(__dirname, 'src') // 这样配置后 @ 可以指向 src 目录
    }
  },
  • eslint 配置

npm i -D eslint eslint-loader eslint-friendly-formatter

npm i -D babel-eslint eslint-config-standard eslint-config-vue eslint-plugin-html eslint-plugin-import eslint-plugin-node eslint-plugin-promise eslint-plugin-standard eslint-plugin-vue

// .eslintrc.js
module.exports = {
  root: true,
  env: {
    browser: true,
    es6: true
  },
  extends: [
    'plugin:vue/essential',
    'standard'
  ],
  parser: 'vue-eslint-parser',
  parserOptions: {
    sourceType: 'module',
    ecmaFeatures: {
      jsx: true
    }
  },
  plugins: [
    'vue',
    'html'
  ]
}
// loader
{
  test: /^((?!bmap).)*\.(js|vue)$/,
  loader: 'eslint-loader',
  enforce: 'pre',
  include: [path.resolve(process.cwd(), 'src')],
  options: {
    formatter: require('eslint-friendly-formatter')
  }
},
// package.json
"scripts": {

  "lint": "eslint --ext .js,.vue src",
  "lint-fix": "eslint --fix --ext .js,.vue src"
},
  • stylelint 配置
npm i stylelint stylelint-webpack-plugin stylelint-config-standard -D
// .stylelintrc.js
'use strict'

module.exports = {
  "extends": "stylelint-config-standard",
  "rules": {
    "rule-empty-line-before": "never",
    "selector-list-comma-newline-after": "never-multi-line",
    "string-quotes": "single",
    "indentation": 2,
    "selector-pseudo-element-colon-notation": "single",
    "no-descending-specificity": null
  }
}
// webpack.config.js
const StyleLintPlugin = require('stylelint-webpack-plugin')

module.exports = {
  plugins: [
    new StyleLintPlugin({
      files: ['src/**/*.vue', 'src/**/*.(le|c)ss']
    })
  ]
}
  • webpack-dev-server 热刷新

npm i -D webpack-dev-server

  
  target: 'web',
  devServer: {
    contentBase: './dist',
    host: 'localhost', // hostname
    port: '8888', // 端口
    open: true, // 打开应用
    hot: true, // 热刷新
    inline: true // 刷新模式
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]

package.json scripts 新增命令

"serve": "webpack-dev-server"

二、进阶用法

  • 打包配置文件归总

scripts 命令修改

npm i -D cross-env webpack-merge

"build": "cross-env NODE_ENV=production webpack",
"serve": "cross-env NODE_ENV=development webpack serve"

webapck.config.js

'use strict'

const baseConfig = require('./lib/webpack.base.js');
const devConfig = require('./lib/webpack.dev.js');
const prodConfig = require('./lib/webpack.prod.js');
const { merge } = require('webpack-merge');

const NODE_ENV = process.env.NODE_ENV;
module.exports = () => {
  switch (NODE_ENV) {
    case 'development':
      return merge(baseConfig, devConfig);
    case 'production':
      return merge(baseConfig, prodConfig);
    default:
      return new Error('no mode');
  }
}

lib/webpack.base.js

'use strict'

const path = require('path');
const { VueLoaderPlugin } = require('vue-loader');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
  target: 'web',
  entry: {
    main: './src/main.js'
  },
  output: {
    path: path.join(process.cwd(), 'dist'),
    filename: '[name]_[chunkhash:8].js'
  },
  resolve: {
    // 设置别名
    alias: {
      '@': path.join(process.cwd(), 'src') // 这样配置后 @ 可以指向 src 目录
    }
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'babel-loader'
      },
      {
        test: /\.vue$/,
        use: 'vue-loader'
      },
      {
        test: /\.(jpg|jpeg|png|gif)$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 10240,
              name: '[name]_[hash:8].[ext]'
            }
          }
        ]
      },
      {
        test: /\.(ttf|eot|svg|woff|woff2)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '[name]_[hash:8].[ext]'
            }
          }
        ]
      },
      {
        test: /\.txt$/,
        use: 'raw-loader'
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin(),
    new HtmlWebpackPlugin({
      template: './public/index.html',
      filename: 'index.html'
    }),
    new CleanWebpackPlugin()
  ]
};

lib/webpack.dev.js

'use strict'

const webpack = require('webpack');

module.exports = {
  mode: 'development',
  devServer: {
    contentBase: './dist',
    host: 'localhost', // hostname
    port: '8888', // 端口
    open: true, // 打开应用
    hot: true, // 热刷新
    inline: true // 刷新模式
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'vue-style-loader',
          'css-loader',
          'postcss-loader'
        ]
      },
      {
        test: /\.less$/,
        use: [
          'vue-style-loader',
          'css-loader',
          'postcss-loader',
          'less-loader'
        ]
      }
    ]
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
};

lib/webpack.prod.js

'use strict'

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'postcss-loader'
        ]
      },
      {
        test: /\.less$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'postcss-loader',
          'less-loader'
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name]_[contenthash:8].css'
    })
  ]
};
  • 环境变量

通常项目需要分为生产环境和本地环境添加不同的环境变量,webpack 中可以使用 DefinePlugin 进行设置


'use strict'

const webpack = require('webpack');

module.exports = {
  mode: 'production',
  plugins: [
    new webpack.DefinePlugin({
      ENV: JSON.stringify('production'),
      HOST: JSON.stringify('prod.ifrontend.net'),
      IS_PRODUCTION: true,
      ENV_ID: 20210829,
      CONSTANTS: JSON.stringify({
        TYPES: ['mobile', 'qq', 'email']
      })
    })
  ]
}


// app.js
console.log(ENV) // production
console.log(HOST) // prod.ifrontend.net
console.log(IS_PRODUCTION) // true
console.log(ENV_ID) // 20210829
console.log(CONSTANTS) // {TYPES: ['mobile', 'qq', 'email']}

注意这里值必须加上 JSON.stringify,是因为 DefinePlugin 在替换环境变量时对字符串类型的值进行的是完全替换。如果不加 JSON.stringify 的话,在替换后就会成为变量名,而非字符串值。

  • source map 配置

webpack 配置非常简单,只需要在 webpack.config.js 中添加 devtool 即可。

具体参考:https://webpack.docschina.org/configuration/devtool/#root

source map 主要是帮助开发者调试源码,跟 Chrome 的开发者工具配合,在 “Sources” 选项卡下面的 “webpack://” 目录中可以找到解析后的工程源码

开发环境:由于生成 source map 是会延长整体的构建时间、打包速度。所以一般不会配置 devtool: ‘source-map’,会选择一个简化版的 source map 。cheap-source-map、eval-source-map、eval-cheap-module-source-map

生产环境:为了防止暴露源码,提高安全性,一般会选择 hidden-source-map、nosources-source-map 两种策略。

  • 代码分割及动态 import

npm i -D @babel/plugin-syntax-dynamic-import

.babelrc 增加 plugins

{

  "plugins": [
    "@babel/plugin-syntax-dynamic-import"
  ]
}
<button @click="dynamicImportFn">动态 import 组件</button>
<component :is="dynamicComponent"></component>

export default {
  data() {
    return {
      dynamicComponent: null
    }
  },
  methods: {
    dynamicImportFn() {
      console.log('动态组件')
      import(/* webpackChunkName: "dynamic" */ './Dynamic.vue').then(component => {
        this.dynamicComponent = component.default
      })
    }
  }
}

最好使用 @babel/plugin-transform-runtime 插件,它包括 es6 的新特性,比如动态 import 、async/await 等等

  • css 质量检测

npm i -D stylelint

'use strict'


const stylelint = require('stylelint');

module.exports = {
  plugins: [
    stylelint({
      config: {
        rules: {
          'declaration-no-important': true
        }
      }
    })
  ]
}

三、构建速度和体积优化

  • 速度分析插件

npm i – D speed-measure-webpack-plugin

const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')
const smp = new SpeedMeasurePlugin();

module.exports = smp.wrap({...config配置})
  • 体积分析
npm install --save-dev webpack-bundle-analyzer
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
}
  • 多进程/多实例

多进程构建的方案比较知名的有以下三个:

  1. thread-loader (推荐使用这个)
  2. parallel-webpack
  3. HappyPack

npm i -D thread-loader

'use strict'



module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: 'thread-loader',
            options: {
              workers: 2
            }
          },
          {
            loader: 'babel-loader'
          }
        ]
      }

    ]
  }
}
  • 代码压缩

webpack4+ 设置 mode: ‘production’ ,就会默认包括 tree shaking、scope hosting、js 代码压缩等等

css 并没有做处理,下面我们来使用 css-minimizer-webpack-plugin 进行压缩,

注意:压缩之前首先做 css 代码分离, mini-css-extract-plugin 具体配置,上面已经有了,这里不引入了

npm i -D css-minimizer-webpack-plugin

'use strict'



const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
  plugins: [
    new CssMinimizerPlugin()
  ]
}
  • image 压缩

npm i -D image-webpack-loader

rules: [{
  test: /\.(gif|png|jpe?g|svg)$/i,
  use: [
    'file-loader',
    {
      loader: 'image-webpack-loader',
      options: {
        mozjpeg: {
          progressive: true,
        },
        // optipng.enabled: false will disable optipng
        optipng: {
          enabled: false,
        },
        pngquant: {
          quality: [0.65, 0.90],
          speed: 4
        },
        gifsicle: {
          interlaced: false,
        },
        // the webp option will enable WEBP
        webp: {
          quality: 75
        }
      }
    },
  ],
}]
  • 清除冗余 css 代码

npm i -D @fullhuman/postcss-purgecss

这里笔者选择使用 purgecss 的 postcss 插件方式,是因为前面已经用过 postcss。

这里当然也可以选择用 purgecss-webpack-plugin ,webpack 插件方式使用

注意:

min-css-extract-plugin purgcess-webpack-plugin 配合

extract-text-webpack-plugin purifycss-webpack 配合

min-css-extract-plugin 可以理解成 extract-text-webpack-plugin 升级版本,它拥有更丰富的特性和更好的性能。

postcss.config.js

'use strict'

const purgecss = require('@fullhuman/postcss-purgecss')

module.exports = {
  plugins: [
    require('autoprefixer'),
    purgecss({
      content: ['./public/**/*.html', './src/**/*.vue'],
      defaultExtractor(content) {
        const contentWithoutStyleBlocks = content.replace(/<style[^]+?<\/style>/gi, '')
        return contentWithoutStyleBlocks.match(/[A-Za-z0-9-_/:]*[A-Za-z0-9-_/]+/g) || []
      },
      safelist: [/-(leave|enter|appear)(|-(to|from|active))$/, /^(?!(|.*?:)cursor-move).+-move$/,
        /^router-link(|-exact)-active$/, /data-v-.*/
      ],
    })
  ]
}
  • 多进程并行压缩代码

js、css 多线程压缩代码

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: true
      }),
      new CssMinimizerPlugin({
        parallel: true
      }),
    ]
  },
}
  • 预编译资源模块

webpack.dll.js

'use strict'

const path = require('path');
const webpack = require('webpack');
const dllAssetPath = path.resolve(process.cwd(), 'dll');
const dllLibraryName = 'dllExample';

module.exports = {
  mode: 'production',
  entry: ['vue'],
  output: {
    path: dllAssetPath,
    filename: 'vendor.js',
    library: dllLibraryName
  },
  plugins: [
    new webpack.DllPlugin({
      name: dllLibraryName,
      path: path.resolve(dllAssetPath, 'manifest.json')
    })
  ]
}
// package.json
{
   ...
   "scripts": {
     "dll": "webpack --config webpack.dll.js"
   }
}

npm run dll 会生成一个 dll目录,里面会有两个文件 vendor.js 和 manifest.json,前者是包含库的代码,后者则是资源清单。

链接到业务代码

// webpack.config.js

module.exports = {
  plugins; [
    new webpack.DllReferencePlugin({
      manifest: require(path.resolve(process.cwd(), 'dll/manifest.json')),
    })
  ]
}
  • 利用缓存提升二次构建速度
'use strict'


module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              cacheDirectory: true // 设置 babel-loader 缓存
            }
          }
        ]
      }

    ]
  }
}

参考网址:https://webpack.docschina.org/configuration/cache/

缓存生成的 webpack 模块和 chunk,来改善构建速度。cache 会在开发 模式被设置成 type: 'memory' 而且在 生产 模式 中被禁用。 cache: true 与 cache: { type: 'memory' } 配置作用一致。 传入 false 会禁用缓存:

module.exports = {
  cache: {
    type: 'filesystem', // 将缓存类型设置为文件系统
    buildDependencies: {
      config: [__filename],
    },
    version: '1.0'
  }
}

前端开发那点事
微信公众号搜索“前端开发那点事”

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注