• webpack 之 构建包设计


    将构建配置抽离成 npm 包的意义

    • 通用性
      • 业务开发这无需关注构建配置
      • 统一团队构建脚本
    • 可维护性
      • 构建配置合理的拆分(开发、生产、ssr环境)
      • README文档 changelog 文档等
    • 质量
      • 冒烟测试、单元测试、测试覆盖等
      • 持续集成

    可选方案

    • 通过多个配置文件管理不同环境的配置
      • 通过webpack --config参数来控制
    • 将构建配置设计成一个库
    • 抽成一个工具进行管理
    • 将所有配置都放在一个文件,通过 --env 参数控制分支选择

    构建配置包设计

    通过多个配置文件管理不同环境的 webpack 配置

    • 基础环境 webpack.base.js
    • 开发环境 webpack.dev.js
    • 生产环境 webpack.prod.js
    • ssr 环境 webpack.ssr.js

    抽离成一个 npm 包统一管理

    • 规范
      • git commit 日志
      • README
      • ESlint规范
      • Semver 规范
    • 质量
      • 冒烟测试
      • 单元测试
      • 测试覆盖率
      • CI

    操作方法:通过 webpack-merge 组合配置

    merge = require('webpack-merge')
    ...
    merge(
    	{ a[1], b: 5, c: 20 },
        { a[2], b: 10, d: 421 }
    )
    { a: [1, 2], b: 10, c: 20, d: 421 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    合并配置

    module.exports = merge( baseConfig, devConfig )
    
    • 1

    功能模块设计与目录结构

    目录结构

    • lib 放置源代码
    • test 放置测试代码

    请添加图片描述

    新建项目

    1. 新建 builder-webpack 目录
    2. cd builder-webpack 进入目录
    3. yarn init 初始化项目
    4. 新建 lib 目录,其下新建四个文件
      • webpack.base.js
      • webpack.dev.js
      • webpack.prod.js
      • webpack.ssr.js

    配置 webpack.base.js 文件

    const { CleanWebpackPlugin } = require('clean-webpack-plugin');
    const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');
    
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');
    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    
    const setMPA = () => {
      let entry = {};
      let htmlWebpackPlugins = [];
      let HtmlWebpackExternalsPlugins = [];
      let url = path.join(__dirname, '/src/*/index.jsx').replaceAll('\\', '/');
      const entryFiles = glob.sync(url);
      Object.keys(entryFiles).map(index => {
        let entryFile = entryFiles[index];
        let match = entryFile.match(/src\/(.*)\/index.jsx/);
        const pageName = match && match[1];
        if (pageName) {
          entry[pageName] = entryFile;
          // 每个入口文件设置 基础库的cdn
          HtmlWebpackExternalsPlugins.push(new HtmlWebpackExternalsPlugin({
            externals: [
              {
                module: 'react',
                entry: 'https://unpkg.com/react@18/umd/react.development.js',
                global: 'React'
              },
              {
                module: 'react-dom',
                entry: 'https://unpkg.com/react-dom@18/umd/react-dom.development.js',
                global: 'ReactDOM'
              }
            ],
            files: [`${pageName}.html`]
          }));
          // 入口文件生成模板
          htmlWebpackPlugins.push(
            new HtmlWebpackPlugin({
              template: path.join(__dirname, `/src/${pageName}/index.html`),
              filename: `${pageName}.html`,
              chunks: [pageName],
              inject: true,
              minify: {
                html5: true,
                collapseWhitespace: true,
                preserveLineBreaks: false,
                minifyCSS: true,
                minifyJS: true,
                removeComments: false
              }
            })
          );
        }
      });
    
      return { entry, htmlWebpackPlugins, HtmlWebpackExternalsPlugins };
    };
    
    const { entry, htmlWebpackPlugins, HtmlWebpackExternalsPlugins } = setMPA();
    
    module.exports = {
      entry: entry,
      output:{
        filename: '[name].js',
        path: path.join(__dirname, 'dist')
      }
      module: {
        rules: [
          {
            test: /\.(js|jsx)$/,
            exclude: /node_modules/,
            use: ['babel-loader']
          },
          {
            test: /\.less$/,
            use: [
              MiniCssExtractPlugin.loader,
              'css-loader',
              {
                loader: 'postcss-loader',
                options: {
                  postcssOptions: {
                    plugins: ['autoprefixer']
                  }
                }
              },
              'less-loader'
            ]
          },
          {
            test: /\.css$/,
            use: [
              MiniCssExtractPlugin.loader,
              'css-loader',
              {
                loader: 'postcss-loader',
                options: {
                  postcssOptions: {
                    plugins: ['autoprefixer']
                  }
                }
              }
            ]
          },
          {
            test: /\.(png|svg|jpeg|jpg|gif|ico)$/i,
            type: 'asset',
            generator: {
              filename: "static/img/[name].[hash:7][ext]"
            }
          }
        ]
      },
      plugins: [
        new CleanWebpackPlugin(),
        /* 命令行信息显示优化 */
        new FriendlyErrorsWebpackPlugin(),
        /* 打包捕获 error */
        function () {
          this.hooks.done.tap('done', stats => {
            if (stats.compilation.errors && stats.compilation.errors.length && process.argv.indexOf('--watch') == -1) {
              console.log('build error');
              process.exit(1);
            }
          });
        },
        /* CSS 提取成一个单独的文件 */
        new MiniCssExtractPlugin({
          filename: '[name]_[hash:8].css'
        }),
      ]
        .concat(htmlWebpackPlugins)
        .concat(HtmlWebpackExternalsPlugins),
      stats: 'errors-only'
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135

    安装 webpack-merge,配置 webpack.dev.js

    yarn add webpack-merge -D
    
    • 1

    在 webpack.dev.js 中引入

    const {merge} = requrie('webpack-merge')
    const baseConfig = require('./webpack.base')
    const devConfig = {}
    module.exports = merge(baseConfig, devConfig)
    
    • 1
    • 2
    • 3
    • 4
    const {merge} = requrie('webpack-merge');
    const webpack = require('webpack');
    const baseConfig = require('./webpack.base');
    
    const devConfig = {
      plugins: [
        new webpack.HotModuleReplacementPlugin(),
      ],
      devServer: {
        port: 3000,
        compress: false,
        static: {
          directory: path.join(process.cwd(), 'dist'),
          publicPath: '/'
        },
        client: {
          overlay: {
            errors: true,
            warnings: false
          }
        },
        stats: 'errors-only'
      },
      devtools: 'cheap-source-map'
    };
    module.exports = merge(baseConfig, devConfig);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    配置 webpack.prod.js

    同样是使用 webpack-merge

    const merge = require('webpack-merge');
    const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin');
    const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');
    const baseConfig = require('./webpack.base');
    
    const prodConfig = {
      mode: 'production',
      /* 设置提取的公共文件包的大小 */
      optimization: {
      	 /* 压缩 css */
        minimize: true,
        minimizer: [
          new CssMinimizerWebpackPlugin()
        ],
        splitChunks: {
          minSize: 0,
          cacheGroups: {
            commons: {
              name: 'commons',
              chunks: 'all',
              minChunks: 2
            }
          }
        }
      },
      plugins: [
        /* 速度优化: 引入基础包的 cdn */
        new HtmlWebpackExternalsPlugin({
          externals: [
            {
              module: 'react',
              entry: 'https://unpkg.com/react@18/umd/react.development.js',
              global: 'React'
            },
            {
              module: 'react-dom',
              entry: 'https://unpkg.com/react-dom@18/umd/react-dom.development.js',
              global: 'ReactDOM'
            }
          ]
        })
      ]
    };
    
    module.exports = merge(baseConfig, prodConfig);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    optimize-css-assets-webpack-plugin 插件配合 cssnano css 处理器来压缩处理配置中 ExtractTextPlugin 实例导出的文件的文件名运行,而不是 源css文件的文件名,默认为 /\.css$/g

    • assetNameRegExp 一个正则表达式,指示应优化的最小化的资源的名称
      • 提供的正则表达式针对配置中 ExtractTextPlugin 实例导出的文件的文件名运行,而不是源css 文件的文件名,默认是 /\.css$/g
    • cssProcessor 用于优化最小化 css 的 css 处理器,默认是 cssnano
      • 这应该是一个跟随 cssnano.processor 接口的函数(接收 css 和 选项参数并返回一个 Promise)
    • cssProcessorOptions 传递给 cssProcessor 的选项,默认为 {}
    • cssProcessorPluginOptions 传递给 CSSProcessor 的插件选项,默认为 {}
    • canPrint 一个布尔值,指示插件是否可以将信息打印到控制台,默认为 true

    配置 webpack.ssr.js

    拷贝一份 webpack.prod.js,需要设置忽略解析 css

    {
        module:{
            rules:[
                {
                    test:/\.css$/,
                    use:[
                        'ignore-loader'
                    ]
                },
                {
                    test:/\.less$/,
                    use:[
                        'ignore-loader'
                    ]
                }
            ]
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    const merge = require('webpack-merge');
    const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin');
    const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');
    const baseConfig = require('./webpack.base');
    
    const prodConfig = {
      module: {
        rules: [
          {
            test: /\.css$/,
            use: ['ignore-loader']
          },
          {
            test: /\.less$/,
            use: ['ignore-loader']
          }
        ]
      },
      mode: 'production',
      /* 设置提取的公共文件包的大小 */
      optimization: {
      	 /* 压缩 css */
        minimize: true,
        minimizer: [
          new CssMinimizerWebpackPlugin()
        ],
        splitChunks: {
          minSize: 0,
          cacheGroups: {
            commons: {
              name: 'commons',
              chunks: 'all',
              minChunks: 2
            }
          }
        }
      },
      plugins: [
        /* 速度优化: 引入基础包的 cdn */
        new HtmlWebpackExternalsPlugin({
          externals: [
            {
              module: 'react',
              entry: 'https://unpkg.com/react@18/umd/react.development.js',
              global: 'React'
            },
            {
              module: 'react-dom',
              entry: 'https://unpkg.com/react-dom@18/umd/react-dom.development.js',
              global: 'ReactDOM'
            }
          ]
        })
      ]
    };
    
    module.exports = merge(baseConfig, prodConfig);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    其他文件

    • 根目录新建 README.md 文件
    • .gitignore 设置 git 忽略文件
    /node_modules
    /logs
    
    • 1
    • 2

    通过 ESLint 规范构建脚本

    使用 eslint-config-airbnb-base

    eslint --fix 可自动处理空格

    module.exports = {
        "parser": "babal-eslint",
        "extends": "airbnb-base",
        "env": {
            "browser": true,
            "node": true
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 安装 eslint@babel/eslint-parser@babel/coreeslint-plugin-importeslint-config-airbnb-base
    yarn add eslint @babel/eslint-parser @babel/core eslint-plugin-import eslint-config-airbnb-base -D 
    
    • 1
    • 配置 eslint 配置文件 .eslintrc.js
    module.exports = {
      "parser": "@babel/eslint-parser",
      "parserOptions": {
        "requireConfigFile": false
      },
      "extends": "airbnb-base",
      "env": {
        "browser": true,
        "node": true
      }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • package.json 设置 scripts 脚本
    {
        "scripts": {
            "fix": "eslint ./lib --fix",
            "eslint": "eslint ./lib"
        },
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 使用(根据 eslint 提示对代码进行调整)
    yarn run eslint 
    
    • 1

    yarn run fix 可自动更正一些格式问题

    冒烟测试(预测试)

    对提交测试的软件在进行详细深入的测试之前进行的预测试

    主要目的暴露导致软件需要重新发布的基本功能失效等严重问题

    关注问题

    • 构建是否成功

    • 每次构件完成 build 目录是否有内容输出

      • 是否有 JS、CSS 等静态资源文件
      • 是否有 HTML 文件

    每次都手动执行,比较繁琐,可通过一些工具(mocha)来完成这一步骤

    检测构建(清空dist、判断是否构建成功)

    在示例项目中原型构建,看是否有报错

    • 根目录下创建 test 目录

    • test/smoke/index.js 再次书写冒烟测试相关的代码

    • 需要判断否写构建是否正常运作,需要有一个模板项目

    • 新建 test/smoke/template 目录下创建模板项目

      • 拷贝一个项目到 template 目录下,删除 该项目相关的webpack 配置(webpack.prod.js 等文件)
    • 需要 rimraf 这个库来处理删除 dist 目录这个操作

      • 每次构建之前否需要将 dist 目录清空
      • 执行删除操作之后,会执行一个回调函数
      yarn add rimraf -D
      
      • 1
    • test/smoke/index.js

      • 首先删除 dist 目录,清除上次构建内容
      • 引入配置文件 webpack.prod.js 文件
      • webpack 方法接收一个配置文件,后执行回调函数,在回调函数中捕获错误信息,若构建有问题,打印错误信息
    const path = require('path');
    const webpack = require('webpack');
    const rimraf = requir('rimraf');
    
    /* 需要现将目录切换到 template  */
    process.chdir(path.join(__dirname, 'template'));
    
    rimraf('./dist', () => {
      const prodConfig = require('../../lib/webpack.prod');
      webpack(prodConfig, (err, stats) => {
        if (err) {
          console.log(err);
          process.exit(2);
        }
        console.log(stats.toString({
          colors: true,
          modules: false,
          children: false
        }));
      });
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 执行 node index.js 文件
    node ./test/smoke/index.js
    
    • 1
    • 模板项目中路径均使用 __dirname(以template为根) 来配置路径,需要使用 process.cwd() 来获取整个项目的根路径
      • webpack.base.js 中定义一个全局变量 projectRoot 来替换 __dirname

    webpack.prod.js

    const { merge } = require('webpack-merge');
    const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin');
    const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');
    const baseConfig = require('./webpack.base');
    
    
    const prodConfig = {
      mode: 'production',
      /* 设置提取的公共文件包的大小 */
      optimization: {
        /* 压缩 css */
        minimize: true,
        minimizer: [
          new CssMinimizerWebpackPlugin()
        ],
        splitChunks: {
          minSize: 0,
          cacheGroups: {
            commons: {
              name: 'commons',
              chunks: 'all',
              minChunks: 2,
            },
          },
        },
      },
      plugins: [
    
        /* 速度优化: 引入基础包的 cdn */
        new HtmlWebpackExternalsPlugin({
          externals: [
            {
              module: 'react',
              entry: 'https://unpkg.com/react@18/umd/react.development.js',
              global: 'React',
            },
            {
              module: 'react-dom',
              entry: 'https://unpkg.com/react-dom@18/umd/react-dom.development.js',
              global: 'ReactDOM',
            },
          ],
        }),
      ],
    };
    
    module.exports = merge(baseConfig, prodConfig);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    webpack.base.js

    const { CleanWebpackPlugin } = require('clean-webpack-plugin');
    const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    const path = require('path');
    const glob = require('glob');
    
    const projectRoot = process.cwd();
    /* /builder-webpack/test/smoke/template/ */
    
    const setMPA = () => {
      const entry = {};
      const htmlWebpackPlugins = [];
      const url = path.join(projectRoot, '/src/*/index.jsx').replaceAll('\\', '/');
      const entryFiles = glob.sync(url);
      Object.keys(entryFiles).map((index) => {
        const entryFile = entryFiles[index];
        const match = entryFile.match(/src\/(.*)\/index.jsx/);
        const pageName = match && match[1];
        if (pageName) {
          entry[pageName] = entryFile;
          // 入口文件生成模板
          htmlWebpackPlugins.push(
            new HtmlWebpackPlugin({
              template: path.join(projectRoot, `/src/${pageName}/index.html`),
              filename: `${pageName}.html`,
              chunks: [pageName],
              inject: true,
              minify: {
                html5: true,
                collapseWhitespace: true,
                preserveLineBreaks: false,
                minifyCSS: true,
                minifyJS: true,
                removeComments: false,
              },
            }),
          );
        }
        return htmlWebpackPlugins;
      });
    
      return { entry, htmlWebpackPlugins };
    };
    
    const { entry, htmlWebpackPlugins } = setMPA();
    console.log(entry);
    module.exports = {
      entry: entry,
      output: {
        filename: '[name].js',
        path: path.join(projectRoot, 'dist')
      },
      module: {
        rules: [
          {
            test: /\.(js|jsx)$/,
            exclude: /node_modules/,
            use: ['babel-loader'],
          },
          {
            test: /\.less$/,
            use: [
              MiniCssExtractPlugin.loader,
              'css-loader',
              {
                loader: 'postcss-loader',
                options: {
                  postcssOptions: {
                    plugins: ['autoprefixer'],
                  },
                },
              },
              'less-loader',
            ],
          },
          {
            test: /\.css$/,
            use: [
              MiniCssExtractPlugin.loader,
              'css-loader',
              {
                loader: 'postcss-loader',
                options: {
                  postcssOptions: {
                    plugins: ['autoprefixer'],
                  },
                },
              },
            ],
          },
          {
            test: /\.(png|svg|jpeg|jpg|gif|ico)$/i,
            type: 'asset',
            generator: {
              filename: 'static/img/[name].[hash:7][ext]',
            },
          },
        ],
      },
      plugins: [
        new CleanWebpackPlugin(),
        /* 命令行信息显示优化 */
        new FriendlyErrorsWebpackPlugin(),
        /* 打包捕获 error */
        function doneErrorPlugin() {
          this.hooks.done.tap('done', (stats) => {
            if (stats.compilation.errors && stats.compilation.errors.length && process.argv.indexOf('--watch') === -1) {
              process.exit(1);
            }
          });
        },
        /* CSS 提取成一个单独的文件 */
        new MiniCssExtractPlugin({
          filename: '[name]_[hash:8].css',
        }),
      ]
        .concat(htmlWebpackPlugins),
      resolve: {
        alias: {
          '@': path.join(projectRoot, '/'),
          '@server': path.join(projectRoot, '/server'),
          '@src': path.join(projectRoot, '/src'),
          '@comp': path.join(projectRoot, '/src/components'),
          '@images': path.join(projectRoot, '/src/assets/images'),
        }
      },
      stats: 'errors-only',
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129

    创建测试文件

    • /test/smoke/目录下新建两个测试文件

      • html-test.js 检测是否生成 HTML 文件
      • css-js-test.js 检测是否生成 css、js 文件
    • 安装 mocha

    yarn add mocha -D
    
    • 1
    • 安装 glob-all
    yarn add glob-all -D 
    
    • 1
    • html-test.js
    const glob = require('glob-all');
    
    describe('Checking generated html files', () => {
      it('should generate html files', done => {
        const files = glob.sync([
          './dist/index.html',
          './dist/search.html',
        ]);
    
        if (files.length > 0) {
          done();
        } else {
          throw new Error('no html files generate');
        }
      });
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • css-js-test.js
    const glob = require('glob-all');
    
    describe('Checking generated css js files', () => {
      it('should generate css js files', done => {
        const files = glob.sync([
          './dist/index_*.js',
          './dist/index_*.css',
          './dist/search_*.js',
          './dist/News_*.js',
        ]);
    
        if (files.length > 0) {
          done();
        } else {
          throw new Error('no css js files generate');
        }
      });
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 在 index.js 中
      • 新建 mocha 实例
      • 使用 实例方法 addFile() 引入以上两个测试文件
      • 使用 实例方法 run() 运行 mocha
    const path = require('path');
    const webpack = require('webpack');
    const rimraf = require('rimraf');
    const Mocha = require('mocha');
    
    /* 设置过期时间 */
    const mocha = new Mocha({
      timeout: '10000ms'
    });
    
    /* 需要现将目录切换到 template  */
    process.chdir(path.join(__dirname, 'template'));
    
    rimraf('./dist', () => {
      const prodConfig = require('../../lib/webpack.prod');
      webpack(prodConfig, (err, stats) => {
        if (err) {
          console.log(err);
          process.exit(2);
        }
        console.log(stats.toString({
          colors: true,
          modules: false,
          children: false
        }));
    
    +    console.log('Webpack build success, begin run test');
    
    +    mocha.addFile(path.join(__dirname, 'html-test.js'));
    +    mocha.addFile(path.join(__dirname, 'css-js-test.js'));
    
    +    mocha.run();
      });
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    在这里插入图片描述

    单元测试与测试覆盖率

    冒烟测试保证了 构建包的基本功能可用

    细节部分的把控需要单元测试来完成

    可选方案

    • 单纯测试框架,需要断言库(chai / should.js / expect / better-assert)

      • mocha 框架
      • ava 框架
    • 集成框架,开箱即用

      • Jasmine 框架
      • Jest 框架
    • 极简 API

    编写单元测试用例

    • 技术选型: Mocha + Chai
    • 测试代码: describe, it, expect
    • 测试命令: mocha add.test.js

    1. test 目录下新建 unit 目录用来编写测试用例

      • 创建对应的测试文件 webpack-base-test.js
      
      describe('webpack.base.js test case', () => {
        const baseConfig = require('../../lib/webpack.base');
        console.log(baseConfig);
        it('entry', () => {
      
        });
      });
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
    2. test 目录下新建 index.js 作为单元测试的入口文件(主要用来引入对应的单元测试用例

      • 每次执行测试用例之前需要进入到模板项目 template 中去
      • 因此使用 process.chdir() 切换路径到 template 目录
      const path = require('path');
      
      process.chdir(path.join(__dirname, 'smoke/template'));
      
      describe('builder-webpack test case', () => {
        require('./unit/webpack-base-test');
      });
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
    3. package.json 新建 scripts

      {
          "scripts": {
          	"test": "./node_modules/.bin/_mocha"
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5

      执行 yarn run test 时就会自动访问 test 目录下 index.js 入口文件

      yarn run test
      
      • 1

    在这里插入图片描述

    在这里插入图片描述
    在这里插入图片描述

    1. 为判断 每次构建是否影响 entry 使用断言库 assert

    2. 安装 assert

      yarn add assert -D
      
      • 1
      const assert = require('assert');
      
      describe('webpack.base.js test case', () => {
        const baseConfig = require('../../lib/webpack.base');
        // console.log(baseConfig);
        it('entry', () => {
          assert.equal(baseConfig.entry.index, 'D:/Z-workSpace/React/ssr-react/builder-webpack/test/smoke/template/src/index/index.jsx');
          assert.equal(baseConfig.entry.search, 'D:/Z-workSpace/React/ssr-react/builder-webpack/test/smoke/template/src/search/index.jsx');
        });
      });
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10

    在这里插入图片描述

    此时测试用例已经跑通了

    测试覆盖率

    推荐使用 nyc

    • 安装
    yarn add nyc -D
    
    • 1
    • 修改 scripts 脚本
    {
        "scripts": {
            "test": "mocha",
            "coverage": "npx nyc mocha"
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述

    单元测试与冒烟测试

    单元测试和测试覆盖率一般是在构建前还是构建后完成?

    • 针对基础组件或者构建包,通过单元测试和测试覆盖率 保证组件质量,这需要在发版之前严格遵守
    • 对业务而言,每次 commit 就会进行业务代码的构建。同时异步触发单元测试和测试覆盖率检查,无先后顺序

    持续集成

    在这里插入图片描述

    优点

    • 快速发现错误
    • 防止分支大幅偏离主干

    核心措施

    代码集成到主干之前,必须通过自动化测试。只要有一个测试用例失败,就不能集成

    接入 github action

    • 首先创建一个 github 项目,命名为 builder-webpack
      在这里插入图片描述

    • 项目根目录下创建 .github/workflow/test.yml 编写 yml 脚本设置自动化测试即可

    • git 链接远程仓库

      # 克隆远程仓库到一个干净的目录
      git clone <远程仓库地址>
      
      # 进入项目
      cd builder-webpack[项目名称]
      
      # 查看当前目录绝对路径
      pwd
      
      # 将源码复制到该目录下(-r 表示递归,对多个文件操作)
      cp -r [被复制目录 ./***/ ] [当前目录 ./]
      
      cd ../../
      cp -r ../ssr-react/builder-webpack/ ./
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
    • 编写 github action 自动化测试脚本 test.yml

      # This is a basic workflow to help you get started with Actions
      
      name: CI
      
      # Controls when the workflow will run
      on:
        # Triggers the workflow on push or pull request events but only for the "main" branch
        push:
          branches: [ "main" ]
        pull_request:
          branches: [ "main" ]
      
        # Allows you to run this workflow manually from the Actions tab
        workflow_dispatch:
      
      # A workflow run is made up of one or more jobs that can run sequentially or in parallel
      jobs:
        # This workflow contains a single job called "build"
        build:
          # The type of runner that the job will run on
          runs-on: ubuntu-latest
      
          # Steps represent a sequence of tasks that will be executed as part of the job
          steps:
            # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
            - uses: actions/checkout@v3
                  
            - name: setup node.js environment
              uses: actions/setup-node@v3.3.0
              with:
                node-version: "18.X"
      
            - name: install dep
              run: yarn install -D
              
            - name: switch to template project
              run: cd ./test/smoke/template
              
            - name: install template project dep
              run: yarn install -D
              
            - name: run test scripts
              run: yarn run test
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
    • 提交代码到远程仓库

      git add .
      git commit -m 'feat: builder github action ci'
      git push origin [branch_name]
      
      • 1
      • 2
      • 3
    • 由于之前是在本地测试,entry 入口是本地目录,推送到 github 之后需要修改 webpack-base-test.js 单元测试脚本

      assert.equal(baseConfig.entry.index, '/home/runner/work/builder-webpack/builder-webpack/test/smoke/template/src/index/index.jsx');
          assert.equal(baseConfig.entry.search, '/home/runner/work/builder-webpack/builder-webpack/test/smoke/template/src/search/index.jsx');
      
      • 1
      • 2

      具体构建信息可在 github 项目 action 中查看

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    • test.yml 设置依赖缓存,减少每次构建安装依赖的时间

      
      name: CI
      
      on:
      
        push:
          branches: [ "main" ]
        pull_request:
          branches: [ "main" ]
      
      
        workflow_dispatch:
      
      
      jobs:
       
        test:
       
          runs-on: ubuntu-latest
      
         
          steps:
            # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
            - uses: actions/checkout@v3
            
      
                
                
            - name: setup node.js environment
              uses: actions/setup-node@v3.3.0
              with:
                node-version: "18.X"
                
            - name: Cache node_modules
              id: cache-node-modules
              uses: actions/cache@v1
              with:
                path: node_modules
                key: ${{ runner.os }}-${{ matrix.node-version }}-nodeModules-${{ hashFiles('package.json') }}
                restore-keys: |
                  ${{ runner.os }}-${{ matrix.node-version }}-nodeModules-
      
            - name: install dep
              if: steps.cache-node-modules.outputs.cache-hit != 'true'
              run: yarn install -D
              
            - name: switch to template project
              run: cd ./test/smoke/template
              
              
            - name: Cache node_modules
              id: cache-node-modules-template
              uses: actions/cache@v2
              with:
                path: node_modules
                key: ${{ runner.os }}-${{ matrix.node-version }}-nodeModules-${{ hashFiles('package.json') }}
                restore-keys: |
                  ${{ runner.os }}-${{ matrix.node-version }}-nodeModules-
              
            - name: install template project dep
              if: steps.cache-node-modules-template.outputs.cache-hit != 'true'
              run: yarn install -D
              
            - name: run test scripts
              run: yarn run test
              
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66

    在这里插入图片描述

    发布构建包到 npm 社区

    • 修改 package.json 中 name 字节(确保 npm 社区 中该包名没有被使用过)
    • 升级版本
      • 补丁版本号 npm version patch
      • 小版本号 npm version minor
      • 大版本号 npm version major
    # 登录 npm
    npm login
    
    # 升级版本
    npm version minor
    
    # 发布构建包
    npm publish
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

  • 相关阅读:
    Restful风格真的有必要吗?
    WinUI 3 踩坑记:第一个窗口
    【数字电路基础】进制转换:二进制、十进制、八进制、十六进制、反码、补码、原码
    面向对象分析与设计_用例图
    leaflet实现自定义线、矩形和扇形的绘制
    敏捷在线开发管理工具
    Matlab中saveobj函数的使用
    C++——多态调用和普通调用的本质区别
    Spring Cloud 升级之路 - 2020.0.x - 1. 背景知识、需求描述与公共依赖
    英语语法 — 词性
  • 原文地址:https://blog.csdn.net/VOID_Pointer_G/article/details/126397381