webpack5一步一步配置项目

258 阅读6分钟

代码仓库:

gitee.com/huxuwei_859…

安装包

  1. webpack
  • webpack: 构建工具,将多个模块打包成一个或多个静态文件

  • webpack-cli: webpack命令行工具

  • webpack-dev-server: 开发服务器,提供实时加载和热更新

  • webpack-merge: 合并webpack配置文件

  1. babel
  • babel-loader : webpack的loader,使webpack可以转换代码

  • @babel/core : Babel 的核心模块,提供了转换代码的 API

  • @babel/perset-env: 是babel的预设,可以将最新版的JavaScript转为兼容的版本

  1. CSS
  • style-loader: 将css注入到HTML页面的<style></style>

  • css-loader: 解析CSS文件,及文件中@importurl(),并生成依赖关系

  • postcss: PostCSS是一个处理CSS的工具框架,本身没有功能

  • postcss-loader: 在webpack构建过程中使用PostCSS构建CSS文件

  • post-preset-env: postCSS的插件,自动为CSS添加浏览前缀;把现代css转成浏览器认识的css

  • mini-css-extract-plugin: 将css提交到单独的文件中 (与style-loader互斥)

  • sass: CSS预处理器

  • sass-loader: 处理.scss文件

  • @types/node-sass: sass的ts支持

  1. vue
  • vue:

  • vue-loader: 把.vue文件的各个模块提取出来,然后经过对用的loader处理

  • @vue/compiler-sfc: 将template语法转换成render函数

  • eslint-plugin-vue: vue代码检查插件

  • @type/vue

  1. react
  • React

  • React-DOM

  • @babel/preset-react

  • eslint-plugin-react

  • @types/react:

  • @types/react-dom:

  • classnames:

  1. 代码规范
  • eslint: 代码检查工具

  • prettier: 代码格式化工具

  • eslint-config-prettier: 用于关闭ESLintPettier冲突的规则

  • eslint-plugin-prettier: 用于将Pettier的规则加入eslint中

  • husky: 基于Git Hooks的工具,可以在Git Hooks事件执行脚本

  • lint-staged: 在git中暂存的文件上运行任务

  • commitlint: commit提交信息规范

  1. TypeScript:
  • typescript

  • @typescript-eslint/parse : ESLint的解析器,把ts转成eslint可以理解的AST

  • @typescript-eslint/eslint-plugin : eslint的插件,检查的ts代码

  • @babel/preset-typescript : 使用babel解析ts, TypeScript的预设

  • esbuild-loader :

  1. 其他:

- ~~~~clean-webpack-plugin~~~~: 删除构建文件 webpack5已集成

  • dotenv-webpack : 可以在webpack构建时加载环境变量;将.env环境变量加载到process.env对象中

  • cross-env: 跨平台的环境变量设置工具

搭建项目

基础搭建

  1. 安装webpack
npm init -y
pnpm i webpack webpack-cli webpack-dev-server -D
  1. 处理html文件

pnpm i html-webpack-plugin -D

  1. 构建前删除上一次构建文件

pnpm i ~~~~clean-webpack-plugin~~~~ -D

output.clean 设置为true

  1. 配置
// webpack.config.js
const path = require('path');
const HTMLWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash:8].js',
    clean: true
  },
  plugins: [
    new HTMLWebpackPlugin({
      template: './public/index.html',
    })
  ],
  devServer: {
    open: true,
    hot:true
  }
}
// package.json
{
  "scripts": {
     "serve": "webpack serve --config webpack.config.js"
  }
}

处理 JavaScript 文件

  1. 安装依赖

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

  1. 配置项
   // webpack.config.js
    module: {
       rules: [
         {
           test: /.js$/,
           exclude: /node_modules/,
           use: {
             loader: 'babel-loader'
           }
         }
       ]
    }
    // .babelrc
    {
     "presets": [
       "@babel/preset-env"
     ]
    }

处理 CSS 文件

解析 CSS 文件

  1. 安装依赖

pnpm i style-loader css-loader sass sass-loader mini-css-extract-plugin -D

  1. 配置rules

mini-css-extract-pluginstyle-loader不能同时使用,需要根据不同的环境变量使用不同的loader

  // webpack.config.js
const isDevelopment = process.env.NODE_ENV !== 'production'
const style = isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader
  {
  module: {
    rules: [
        {
          test: /.css$/,
          use: [
            style,
            'css-loader'
          ]
        },
        {
          test: /.scss$/,
          use: [
              style,
              'css-loader', 
              'sass-loader'
            ]
        }
      ]
      }
    }

使用PostCSS处理CSS文件

  1. 安装依赖

pnpm i postcss postcss-loader postcss-preset-env -D

  1. 配置rules
    {
       test: /.css$/,
       use: [
         style,
         'css-loader',
         'postcss-loader'
       ]
    },
    {
       test: /.scss$/,
       use: [
           style,
           'css-loader', 
           'postcss-loader'  // sass-loader转换成css后交给postcss处理
           'sass-loader',
         ]
    }
  1. 在根目录下新建postcss.config.js
   module.exports = {
     plugins: [
       require('postcss-preset-env')({
         autoprefixer: {
           grid: true,
         },
       })
     ]
   }
  1. 配置browserslist, 指定支持的浏览器. browserslist(webpack5已经集成)
  // .browserslistrc
  last 2 versions
  >1%

解析图片等文件

webpack5之前file-loaderurl-loaderraw-loader 对文件处理,webpack5已经集成了这些,只需要配置即可

// webpack.config.js
moduel.exports = {
    module: {
        rules: [
            {
                test: /.(png|jpe?g)$/,
                type: 'asset',
                parse: {
                    dataUrlCondition: {
                      maxSize: 1024 * 1000 // 1M
                   }
                 }
            },
            {
                test: /.(svg$)/i,
                type: "asset/source"
            }
        ]
    }
}

区分不同的环境

  1. 安装依赖

pnpm i webpack-merge cross-env ``dotenv-webpack`` -D

  1. 拆分webapack.config.js

根据需要把webapack.config.js拆成多个文件:

  • webapack.common.js

  • webapack.dev.js : 开发环境时用到的配置

  • webpack.prod.js : 生成环境时用到的配置

  1. 配置

    1.   修改package.json 命令

cross-env适配不同平台设置环境变量;设置环境变量名,并读取不同的配置文件

  // package.json
  {
    "scripts":{
      "serve": "cross-env NODE_ENV=development webpack server --config ./build/webpack.dev.js",
      "build": "cross-env NODE_ENV=production webpack --config ./build/webpack.prod.js"
    }
  }
  1. 新建build/webpack.common.js 文件

主要改动:

  • 移出devServer到webpack.dev.js

  • 移除output到webpack.prod.js

  • 根据环境变量配置 style-loaderMiniCssExtractPlugin.loader

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

   const isDevelopment = process.env.NODE_ENV!== 'production';
   const style = isDevelopment? 'style-loader' : MiniCssExtractPlugin.loader;
   module.exports = {
     entry: './src/index.js',
     module: {
       rules: [
         {
           test: /.js$/,
           exclude: /node_modules/,
           use: {
             loader: 'babel-loader'
           }
         },
         {
           test: /.css$/,
           use: [
             style,
             'css-loader'
           ]
         },
         {
           test: /.scss$/,
           use: [
               style,
               'css-loader', 
               'sass-loader'
             ]
         }
       ]
     },
     plugins: [
       new HTMLWebpackPlugin({
         template: './public/index.html',
       }),
       new MiniCssExtractPlugin({
         filename: 'css/[name].[contenthash:8].css'
       })
     ],
  }
  1. 新建build/webpack.dev.js 文件

主要功能: 使用webpack-mergemerge方法合并webpack配置

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

  module.exports = merge(common, {
     mode: 'development',
     devServer: {
       open: true,
       hot:true
     }
  })
  1. 新建build/webpack.prod.js 文件
  const path = require('path');
  const {merge} = require('webpack-merge');
  const common = require('./webpack.common.js')

  module.exports = merge(common, {
     mode: 'production',
     output: {
       path: path.resolve(__dirname, '../dist'),
       filename: 'js/[name].[contenthash:8].js'
     },
  })
  1. 在业务中使用环境变量

dotenv-webpackdotenvDefinePlugin的包装

可以读取.env下的配置文件注入到环境变量中

  // webpack.common.js
  const DotenvWebpack = require('dotenv-webpack')
  {
    plugins: [
      new DotenvWebpack({
         path: '.env.' + process.env.NODE_ENV,
         systemvars: true,
       }),
    ]
  }

使用TypeScript

  1. 使用esbuild-loader解析.ts文件
  2. 安装依赖

pnpm i esbuild esbuild-loader @esbuild/darwin-arm64 -D

电脑是ARM芯片的需要安装@esbuild/darwin-arm64

  1. 配置
  • 配置rules
// webpack.common.js
{
  module: {
      rules: [
          {
            test: /.ts$/,
            exclude: /node_modules/,
            use: {
              loader: 'esbuild-loader',
              options: {
                loader: 'ts',
                target: 'es2015'
              }
            }
          },
      ]
  }
}
  • 根目录下新建ts.config.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "esnext",
    "lib": ["es6", "dom"],
    "jsx": "react",
    "strict": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "noFallthroughCasesInSwitch": true,
    "forceConsistentCasingInFileNames": true
  }
}

搭建Vue3项目

Vue项目需要处理.vue文件

  1. 安装依赖
pnpm i vue-loader @vue/compiler-sfc  -D
pnpm i vue

搭建React项目

  1. 安装依赖
pnpm i react react-dom 
pnpm i @type/react @type/react-dom -D
  1. 配置
  • 配置rules
{
    module: {
        rules: {
            test: /.[jt]sx?$/,
            loader: 'esbuild-loader',
            options: {
                loader: 'tsx',
                target: 'es2015'
            }
        }
    }
}

这样就可以开始React了

import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App'

createRoot(document.getElementById('app') as HTMLElement).render(
  <App message="Hello, world!"/>
)

模版文件复制

利用plop快速新建模版页面代码

  1. 安装依赖

pnpm i -D plop node-plop

  1. 在根目录下新建plopfile.js
module.exports = plop => {
  plop.setGenerator('new', {
    description: '创建一个新页面', // 描述
    // 命令式交互配置
    prompts: [
      {
        type: 'input',
        name: 'name',
        message: '页面名称',
        default: 'default-page',
      },
    ],
    actions: [
      {
        type: 'add',
        path: 'src/page/{{name}}/index.tsx',
        templateFile: './template/index.tsx',
      },
      {
        type: 'add',
        path: 'src/page/{{name}}/index.scss',
        templateFile: './template/index.scss',
      },
    ],
  })
}
  1. 在根目录下新建template目录,并在该目录下新建模版文件
  2. package.json添加命令
"scripts": {
  "new": "plop new"
}

运行pnpm new,提示输入页面名称,就会创建新的模版文件

团队协作及优化

代码规范

主要规范:

  • 代码风格规范
  • git提交规范

代码风格规范

  1. 安装依赖

pnpm i -D eslint prettier eslint-config-prettier eslint-plugin-prettier

  • TS文件代码检查

pnpm i @typescript-eslint/parser @typescript-eslint/eslint-plugin -D

  • Vue文件代码检查

pnpm i eslint-plugin-vue -D

  • React文件代码检查

pnpm i eslint-plugin-react eslint-plugin-react-hooks -D

  1. 根目录下新增配置文件.eslintrc.js
// .eslintrc.js
module.exports = {
  env: {
    browser: true,
    es6: true,
    node: true,
  },
  parser: "@typescript-eslint/parser", // Specifies the ESLint parser
  parserOptions: {
    ecmaVersion: 2020,
    sourceType: "module",
    ecmaFeatures: {
      jsx: true,
    },
  },
  plugins: ["react", "react-hooks", "@typescript-eslint", "prettier"],
  extends: [
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:react-hooks/recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:prettier/recommended",
  ],
  rules: {
    "@typescript-eslint/no-var-requires": "off",
  },
};
  1. 自动格式化
  • 安装依赖

pnpm i husky lint-staged -D

  • 执行配置

新建husky配置; 在pre-commit 钩子执行lint-staged

npx husky add .husky/pre-commit "pnpm lint-staged"

  • 配置package.json
//package.json
{
  "lint-staged": {
    "**/*.{js,vue,tsx,ts,less,md,json}": [
     "eslint --fix",
      "prettier --config .prettierrc --write",
      "git add"
    ]
  }
}

Git提交信息规范

  1. 安装依赖

pnpm i commitlint @commitlint/{cli,config-conventional} -D

  1. 配置
  • 配置.commitlintrc.js
module.exports = {
  extends: ['@commitlint/config-conventional'],
}
  • 新建husky配置; 在commit-msg 钩子执行命令

npx husky add .husky/commit-msg " npx --no-install commitlint --edit $1"

这样在commit 的时候,就会校验commit msg是否规范

Git 提交信息自动规则命令行工具

  1. 安装依赖

pnpm i commitizen cz-conventional-changelog-zh -D

  1. 配置

配置package.json

{
    "scripts": {
       commit: "git add .& git-cz"
    },
    "config": {
        "commitizen": {
          "path": "cz-conventional-changelog-zh"
        }
     }
}

添加.czrc文件

{
  "extends": [
    "cz-conventional-changelog-zh"
  ],
  "path": "cz-conventional-changelog-zh"
}

运行pnpm commit 就可以根据提示输入提交信息了

单元测试

使用jest测试框架进行单元测试

React

  1. 安装依赖
pnpm i -D jest @types/jest esbuild-jest ts-jest esbuild
pnpm i -D @testing-library/react @testing-library/jest-dom  @types/testing-library__jest-dom
  • jest: JavaScript 测试框架,进行写测试用例和断言
  • esbuild-jest: Jest转换器,解析Jest中的JavaScript和TypeScript代码
  • @testing-library/react: 是一个 React 测试工具库,提供了一些用于编写 UI 组件测试的实用工具函数。如:render渲染组件
  • @testing-library/jest-dom: 是一个 Jest 匹配器库,用于更方便地进行 DOM 元素测试,提供了一些用于测试 DOM 元素的实用工具函数。
  1. 配置
  • 配置jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
  transform: {
    '^.+\.(ts|tsx)$': 'esbuild-jest', // 指定了将 ts、tsx 文件使用 esbuild-jest 进行转换
  },
  testRegex: '(/__tests__/.*|(\.|/)(test|spec))\.tsx?$',
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
  setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'],. // 指定了 jest-dom 的扩展包。
}
  • 配置ts.config.js
{
  "compilerOptions": {
    "target": "es2017",
    "module": "esnext",
    "moduleResolution": "node",
    "lib": ["es6", "dom"],
    "jsx": "react",
    "strict": true,
    "baseUrl": ".",
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "noFallthroughCasesInSwitch": true,
    "forceConsistentCasingInFileNames": true,
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

指定 tsconfig.json 中的 module 为 esnext,同时开启 esModuleInterop。

  • 编写测试用例
// Button.test.tsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import Button from './Button';

test('renders a button', () => {
  render(<Button text="Click me" onClick={() => {}} />);
  const buttonElement = screen.getByText(/click me/i);
  expect(buttonElement).toBeInTheDocument();
});

运行pnpm test 就可以进行测试了。

构建优化

在webpack5中可以通过一些方法,对构建过程进行优化:

  1. 持久化缓存

      引入缓存后,首次构建时间将增加 15%,二次构建时间将减少 90%

    // webpack.common.js
    module.exports = {
        cache: {
          type: 'filesystem', // 使用文件缓存
        }
    }
    

  1. 多进程打包

Webpack5提供了内置的多进程并行打包功能,可以通过配置parallelism选项来开启多进程打包

const { ProgressPlugin } = require('webpack')
{
    plugins: [
        new ProgressPlugin()
    ],
    optimization: {
        parallelism: 4, // 开启4个worker
    }
}
  1. 在开发阶段关闭不必要的优化

module.exports = {
  // ...
  mode: "development",
  optimization: {
    removeAvailableModules: false,
    removeEmptyChunks: false,
    splitChunks: false,  // 关闭代码分包
    minimize: false,     // 关闭代码压缩
    concatenateModules: false, // 关闭模块合并
    usedExports: false, // 关闭 Tree-shaking 功能
  },
};
  1. 慎用source-map

开发环境使用eval, 获取最佳的编译速度

生产环境使用source-map, 获取最高的质量

  1. 优化resolve配置

resolve是配置webpack如何解析模块,可通过优化resolve配置,减少解析范围

  1. alias

alias 可以创建 importrequire 的别名,用来简化模块引入。

module.exports = {
    resolve: {
        alias: {
          '@': paths.appSrc, // @ 代表 src 路径
        },
    }
}
  1. extensions

extensions表示解析文件的类型列表,定义类型减少文件匹配次数

module.exports = {
    resolve: {
        extensions: ['.tsx', '.js'],  // 从右向左解析
    }
}
  1. 关闭引入模块时,未在项目中找到后,逐层查找的功能
module.exports = {
  resolve: {
    modules: [path.resolve(__dirname, 'node_modules')],
  },
};
  1. 约束loader的范围

module.exports = {
    module: {
        rules: [
          {
            test: /.css$/,
            exclude: /node_modules/,
            use: [style, 'css-loader', 'postcss-loader'],
          },
        ],
  }
}

产物优化

代码压缩

  1. webpack5内置了Terser压缩JavaScript

配置webpack.prod.js

optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            reduce_vars: true,
            pure_funcs: ['console.log'],
          },
        },
      }),
    ],
  },
  1. 压缩CSS

安装依赖

pnpm i -D css-minimizer-webpack-plugin

配置webpack.prod.js

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

module.exports = {
  optimization: {
    minimizer: [
      new CssMinimizerPlugin(),
    ],
  }
}

抽离重复代码

webpack提供SplitChunksPlugin 插件,专门根据产物包的体积、引用次数等做分包优化。


optimization: {
    splitChunks: {
      chunks: 'all', // 对 Initial Chunk 与 Async Chunk 都生效
      cacheGroups: {
          vendors:{ // node_modules里的代码
          test: /[\/]node_modules[\/]/,
          chunks: "all",
          // name: 'vendors', 一定不要定义固定的
          namepriority: 10, // 优先级
          enforce: true          
          }
      }
    },
  },
  • Initial Chunk:入口文件及引入的所有模块
  • Async Chunk: 按需加载的模块
  • node_modules 下的模块统一打包到 vendors 产物,从而实现第三方库与业务代码的分离
抽离runtimechunk(运行时)
{
    optimization: {
        runtimeChunk: true
    }
}
抽离babel运行时

@babel/plugin-transform-runtime : 转换代码时,会把ES6+的特殊代码抽离成一个运行时,以便代码复用

  1. 安装依赖

pnpm i -D @babel/plugin-transform-runtime

  1. 配置.babelrc
{
   "plugins": [
      "@babel/plugin-transform-runtime"
   ]
}

动态加载

HTTP缓存: 利用文件contenthash

使用外置依赖: script引入

module.exports = {
  // ...
  externals: {
    jquery: 'jQuery'
  }
};

使用Tree-shaking

Tree-Shaking只针对ESM有效

webpack中Tree-shaking的实现

  • 需要先【标记】出模块中哪些值没有被使用过

  • 再使用代码压缩(如:Terser)删除标记的代码

optimization: {
    usedExports: true, // 启用标记
},

代码仓库:

gitee.com/huxuwei_859…