通用封装简化 webpack 配置

Posted SegmentFault

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了通用封装简化 webpack 配置相关的知识,希望对你有一定的参考价值。

现在,基本上前端的项目打包都会用上 webpack,因为 webpack 提供了无与伦比强大的功能和生态。但在创建一个项目的时候,总是免不了要配置 webpack,很是麻烦。

简化 webpack 配置的一种方式是使用社区封装好的库,比如 roadhog。 roadhog 封装了 webpack 的一些基础配置,然后暴露一些额外配置的接口,并附加本地数据模拟功能( mock),详情可以参考 roadhog 主页。

另一种方式是自己封装 webpack,这样做自己能够更好的掌控项目。

1. 要封装哪些功能

一般搭建一个项目至少需要两种功能:本地开发调试、构建产品代码。

其他的诸如测试、部署到服务器、代码检查、格式优化等功能则不在这篇文章讲解范围,如果有意了解,可以查看我的其他文章。

2. 基础配置

2.1 目录结构(示例,配合后面的代码讲解)
 
   
   
 
  1. package.json

  2. dev.js                       # 本地开发脚本

  3. build.js                     # 产品构建脚本

  4. analyze.js                   # 模块大小分析(可选)


  5. # 单页面结构

  6. src/                         # 源代码目录

  7.  - index.js                 # js 入口文件

  8.  - index.html               # html 入口文件

  9.  - ...                      # 其他文件


  10. # 多页面结构

  11. src/                         # 源代码目录

  12.  - home/                    # home 页面工作空间

  13.    - index.js               # home 页面 js 入口文件

  14.    - index.html             # home 页面 html 入口文件

  15.    - ...                    # home 页面其他文件


  16.  - explore/                 # explore 页面工作空间

  17.    - index.js               # explore 页面 js 入口文件

  18.    - index.html             # explore 页面 html 入口文件

  19.    - ...                    # explore 页面其他文件


  20.  - about/                   # about 目录

  21.    - company                # about/company 页面工作空间

  22.      - index.js             # about/company 页面 js 入口文件

  23.      - index.html           # about/company 页面 html 入口文件

  24.      - ...                  # about/company 页面其他文件


  25.    - platform               # about/platform 页面工作空间

  26.      - index.js             # about/platform 页面 js 入口文件

  27.      - index.html           # about/platform 页面 html 入口文件

  28.      - ...                  # about/platform 页面其他文件    


  29.  - ...                      # 更多页面        

2.2 基础 npm 包
 
   
   
 
  1. # package.json


  2. "devDependencies": {

  3.  "@babel/core": "^7.1.2",                  # babel core      

  4.  "@babel/plugin-syntax-dynamic-import": "^7.0.0",         # import() 函数支持

  5.  "@babel/plugin-transform-react-jsx": "^7.0.0",           # react jsx 支持

  6.  "@babel/preset-env": "^7.1.0",            # es6+ 转 es5

  7.  "@babel/preset-flow": "^7.0.0",           # flow 支持

  8.  "@babel/preset-react": "^7.0.0",          # react 支持

  9.  "autoprefixer": "^9.1.5",                 # css 自动添加厂家前缀 -webkit-, -moz-

  10.  "babel-loader": "^8.0.4",                 # webpack 加载 js 的 loader

  11.  "babel-plugin-component": "^1.1.1",       # 如果使用 element ui,需要用到这个

  12.  "babel-plugin-flow-runtime": "^0.17.0",   # flow-runtime 支持

  13.  "babel-plugin-import": "^1.9.1",          # 如果使用 ant-design,需要用到这个

  14.  "browser-sync": "^2.24.7",                # 浏览器实例组件,用于本地开发调试

  15.  "css-loader": "^1.0.0",                   # webpack 加载 css 的 loader

  16.  "chalk": "^2.4.1",                        # 让命令行的信息有颜色

  17.  "file-loader": "^2.0.0",                  # webpack 加载静态文件的 loader

  18.  "flow-runtime": "^0.17.0",                # flow-runtime 包

  19.  "html-loader": "^0.5.5",                  # webpack 加载 html 的 loader

  20.  "html-webpack-include-assets-plugin": "^1.0.5",          # 给 html 文件添加额外静态文件链接的插件

  21.  "html-webpack-plugin": "^3.2.0",          # 更方便操作 html 文件的插件

  22.  "less": "^3.8.1",                         # less 转 css

  23.  "less-loader": "^4.1.0",                  # webpack 加载 less 的 loader

  24.  "mini-css-extract-plugin": "^0.4.3",      # 提取 css 单独打包

  25.  "minimist": "^1.2.0",                     # process.argv 更便捷处理

  26.  "node-sass": "^4.9.3",                    # scss 转 css

  27.  "optimize-css-assets-webpack-plugin": "^5.0.1",          # 优化 css 打包,包括压缩

  28.  "postcss-loader": "^3.0.0",               # 对 css 进行更多操作,比如添加厂家前缀

  29.  "sass-loader": "^7.1.0",                  # webpack 加载 scss 的 loader

  30.  "style-loader": "^0.23.0",                # webpack 加载 style 的 loader

  31.  "uglifyjs-webpack-plugin": "^2.0.1",      # 压缩 js 的插件

  32.  "url-loader": "^1.1.1",                   # file-loader 的升级版

  33.  "vue-loader": "^15.4.2",                  # webpack 加载 vue 的 loader

  34.  "vue-template-compiler": "^2.5.17",       # 配合 vue-loader 使用的

  35.  "webpack": "^4.20.2",                     # webpack 模块

  36.  "webpack-bundle-analyzer": "^3.0.2",      # 分析当前打包各个模块的大小,决定哪些需要单独打包

  37.  "webpack-dev-middleware": "^3.4.0",       # webpack-dev-server 中间件

  38.  "webpack-hot-middleware": "^2.24.2"       # 热更新中间件

  39. }

2.3 基本命令
 
   
   
 
  1. # package.json


  2. "scripts": {

  3.  "dev": "node dev.js",

  4.  "build": "node build.js",

  5.  "analyze": "node analyze.js",

  6. }

 
   
   
 
  1. npm run dev                                 # 开发

  2. npm run build                               # 构建

  3. npm run analyze                             # 模块分析

如果需要支持多入口构建,在命令后面添加参数:

 
   
   
 
  1. npm run dev -- home                         # 开发 home 页面

  2. npm run analyze -- explore                  # 模块分析 explore 页面


  3. # 构建多个页面

  4. npm run build -- home explore about/* about/all --env test/prod  

说明:

  • home、 explore 确定构建的页面; about/*、 about/all 指 about 目录下所有的页面; all、 * 整个项目所有的页面。

  • 有时候可能还会针对不同的服务器环境(比如测试机、正式机)做出不同的构建,可以在后面加参数。

  • -- 用来分割 npm 本身的参数与脚本参数,参考 npm - run-script 了解详情。

2.4 dev.js 配置

开发一般用需要用到下面的组件:

  • webpack

  • webpack-dev-server 或 webpack-dev-middleware

  • webpack-hot-middleware

  • HotModuleReplacementPlugin

  • browser-sync

 
   
   
 
  1. const minimist = require('minimist');

  2. const webpack = require('webpack');

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

  4. const devMiddleWare = require('webpack-dev-middleware');

  5. const hotMiddleWare = require('webpack-hot-middleware');

  6. const browserSync = require('browser-sync');

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


  8. const { HotModuleReplacementPlugin } = webpack;


  9. const argv = minimist(process.argv.slice(2));


  10. const page = argv._[0];


  11. // 单页面

  12. const entryFile = `${__dirname}/src/index.js`;

  13. // 多页面

  14. const entryFile = `${__dirname}/src/${page}/index.js`;


  15. // 编译器对象

  16. const compiler = webpack({

  17.  entry: [

  18.    'webpack-hot-middleware/client?reload=true',      // 热重载需要

  19.    entryFile,

  20.  ],

  21.  output: {

  22.    path: `${__dirname}/dev/`,                        // 打包到 dev 目录

  23.    filename: 'index.js',

  24.    publicPath: '/dev/',

  25.  },

  26.  plugins: [

  27.    new HotModuleReplacementPlugin(),                 // 热重载插件

  28.    new HtmlWebpackPlugin({                           // 处理 html

  29.      // 单页面

  30.      template: `${__dirname}/src/index.html`,

  31.      // 多页面

  32.      template: `${__dirname}/src/${page}/index.html`,

  33.    }),

  34.    new VueLoaderPlugin(),                            // vue-loader 所需

  35.  ],

  36.  module: {

  37.    rules: [

  38.      {                                               // js 文件加载器

  39.        loader: 'babel-loader',

  40.        exclude: /node_modules/,

  41.        options: {

  42.          presets: ['@babel/preset-env', '@babel/preset-react'],

  43.          plugins: [

  44.            '@babel/plugin-transform-react-jsx',

  45.            '@babel/plugin-syntax-dynamic-import',

  46.          ],

  47.        },

  48.        test: /\.(js|jsx)$/,

  49.      },

  50.      {                                               // css 文件加载器

  51.        loader: 'style-loader!css-loader',

  52.        test: /\.css$/,

  53.      },

  54.      {                                               // less 文件加载器

  55.        loader: 'style-loader!css-loader!less-loader',

  56.        test: /\.less$/,

  57.      },

  58.      {                                               // scss 文件加载器

  59.        loader: 'style-loader!css-loader!sass-loader',

  60.        test: /\.(scss|sass)$/,

  61.      },

  62.      {                                               // 静态文件加载器

  63.        loader: 'url-loader',

  64.        test: /\.(gif|jpg|png|woff|woff2|svg|eot|ttf|ico)$/,

  65.        options: {

  66.          limit: 1,

  67.        },

  68.      },

  69.      {                                               // html 文件加载器

  70.        loader: 'html-loader',

  71.        test: /\.html$/,

  72.        options: {

  73.          attrs: ['img:src', 'link:href'],

  74.          interpolate: 'require',

  75.        },

  76.      },

  77.      {                                               // vue 文件加载器

  78.        loader: 'vue-loader',

  79.        test: /\.vue$/,

  80.      },

  81.    ],

  82.  },

  83.  resolve: {

  84.    alias: {},                                        // js 配置别名  

  85.    modules: [`${__dirname}/src`, 'node_modules'],    // 模块寻址基路径

  86.    extensions: ['.js', '.jsx', '.vue', '.json'],     // 模块寻址扩展名

  87.  },

  88.  devtool: 'eval-source-map',                         // sourcemap

  89.  mode: 'development',                                // 指定 webpack 为开发模式

  90. });


  91. // browser-sync 配置

  92. const browserSyncConfig = {

  93.  server: {

  94.    baseDir: `${__dirname}/`,                         // 静态服务器基路径,可以访问项目所有文件

  95.  },

  96. };


  97. // 添加中间件

  98. browserSyncConfig.middleware = [

  99.  devMiddleWare(compiler, {

  100.    stats: 'errors-only',

  101.    publicPath: '/dev/',

  102.  }),

  103.  hotMiddleWare(compiler),

  104. ];


  105. browserSync.init(browserSyncConfig);                  // 初始化浏览器实例,开始调试开发

2.5 build.js 配置

构建过程中,一般会有这些过程:

  1. 提取样式文件,单独打包、压缩、添加浏览器厂家前缀

  2. 对 js 在产品模式下进行打包,并生成 sourcemap 文件

  3. html-webpack-plugin 自动把打包好的样式文件与脚本文件引用到 html 文件中,并压缩

  4. 对所有资源进行 hash 化处理(可选)

 
   
   
 
  1. const minimist = require('minimist');

  2. const webpack = require('webpack');

  3. const chalk = require('chalk');

  4. const autoprefixer = require('autoprefixer');

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

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

  7. const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');

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


  9. const {yellow, red} = chalk;


  10. const argv = minimist(process.argv.slice(2));


  11. const pages = argv._; // ['home', 'explore', 'about/*', 'about/all']

  12. const allPages = getAllPages(pages); // 根据 page 中的 `*, all` 等关键字,获取所有真正的 pages


  13. // 单页面,只有一个入口,所以只有一个配置文件

  14. const config = { ... };


  15. // 多页面,多个入口,所有有多个配置文件

  16. const configs = allPages.map(page => ({

  17.  // 单页面

  18.  entry: `${__dirname}/src/index.js`,                 // js 入口文件

  19.  // 多页面

  20.  entry: `${__dirname}/src/${page}/index.js`,         // js 入口文件

  21.  output: {

  22.    path: `${__dirname}/dist/`,                       // 输出路径

  23.    filename: '[chunkhash].js',                       // 输出文件名,这里完全取 hash 值来命名

  24.    hashDigestLength: 32,                             // hash 值长度

  25.    publicPath: '/dist/',

  26.  },

  27.  plugins: [

  28.    new MiniCssExtractPlugin({                        // 提取所有的样式文件,单独打包

  29.      filename: '[chunkhash].css',                    // 输出文件名,这里完全取 hash 值来命名

  30.    }),

  31.    new HtmlWebpackPlugin({

  32.      // 单页面

  33.      template: `${__dirname}/src/index.html`,        // html 入口文件

  34.      // 多页面

  35.      template: `${__dirname}/src/${page}/index.html`,// html 入口文件

  36.      minify: {                                       // 指定如果压缩 html 文件

  37.        removeComments: !0,

  38.        collapseWhitespace: !0,

  39.        collapseBooleanAttributes: !0,

  40.        removeEmptyAttributes: !0,

  41.        removeScriptTypeAttributes: !0,

  42.        removeStyleLinkTypeAttributes: !0,

  43.        minifyJS: !0,

  44.        minifyCSS: !0,

  45.      },

  46.    }),

  47.    new VueLoaderPlugin(),                            // vue-loader 所需

  48.    new OptimizeCssAssetsPlugin({                     // 压缩 css

  49.      cssProcessorPluginOptions: {

  50.        preset: ['default', { discardComments: { removeAll: true } }],

  51.      },

  52.    }),


  53.    // webpack 打包的 js 文件是默认压缩的,所以这里不需要再额外添加 uglifyjs-webpack-plugin

  54.  ],

  55.  module: {

  56.    rules: [

  57.      {                                               // js 文件加载器,与 dev 一致

  58.        loader: 'babel-loader',

  59.        exclude: /node_modules/,

  60.        options: {

  61.          presets: ['@babel/preset-env', '@babel/preset-react'],

  62.          plugins: [

  63.            '@babel/plugin-transform-react-jsx',

  64.            '@babel/plugin-syntax-dynamic-import',

  65.          ],

  66.        },

  67.        test: /\.(js|jsx)$/,

  68.      },

  69.      {                                               // css 文件加载器,添加了浏览器厂家前缀

  70.        use: [

  71.          MiniCssExtractPlugin.loader,

  72.          'css-loader',

  73.          {

  74.            loader: 'postcss-loader',

  75.            options: {

  76.              plugins: [

  77.                autoprefixer({

  78.                  browsers: [

  79.                    '> 1%',

  80.                    'last 2 versions',

  81.                    'android >= 3.2',

  82.                    'Firefox >= 20',

  83.                    'ios 7',

  84.                  ],

  85.                }),

  86.              ],

  87.            },

  88.          },

  89.        ],

  90.        test: /\.css$/,

  91.      },

  92.      {                                               // less 文件加载器,添加了浏览器厂家前缀

  93.        use: [

  94.          MiniCssExtractPlugin.loader,

  95.          'css-loader',

  96.          {

  97.            loader: 'postcss-loader',

  98.            options: {

  99.              plugins: [

  100.                autoprefixer({

  101.                  browsers: [

  102.                    '> 1%',

  103.                    'last 2 versions',

  104.                    'Android >= 3.2',

  105.                    'Firefox >= 20',

  106.                    'iOS 7',

  107.                  ],

  108.                }),

  109.              ],

  110.            },

  111.          },

  112.          'less-loader',

  113.        ],

  114.        test: /\.less$/,

  115.      },

  116.      {                                               // scss 文件加载器,添加了浏览器厂家前缀

  117.        use: [

  118.          MiniCssExtractPlugin.loader,

  119.          'css-loader',

  120.          {

  121.            loader: 'postcss-loader',

  122.            options: {

  123.              plugins: [

  124.                autoprefixer({

  125.                  browsers: [

  126.                    '> 1%',

  127.                    'last 2 versions',

  128.                    'Android >= 3.2',

  129.                    'Firefox >= 20',

  130.                    'iOS 7',

  131.                  ],

  132.                }),

  133.              ],

  134.            },

  135.          },

  136.          'sass-loader',

  137.        ],

  138.        test: /\.(scss|sass)$/,

  139.      },

  140.      {                                               // 静态文件加载器,与 dev 一致

  141.        loader: 'url-loader',

  142.        test: /\.(gif|jpg|png|woff|woff2|svg|eot|ttf|ico)$/,

  143.        options: {

  144.          limit: 1,

  145.        },

  146.      },

  147.      {                                               // html 文件加载器,与 dev 一致

  148.        loader: 'html-loader',

  149.        test: /\.html$/,

  150.        options: {

  151.          attrs: ['img:src', 'link:href'],

  152.          interpolate: 'require',

  153.        },

  154.      },

  155.      {                                               // vue 文件加载器,与 dev 一致

  156.        loader: 'vue-loader',

  157.        test: /\.vue$/,

  158.      },

  159.    ],

  160.  },

  161.  resolve: {

  162.    alias: {},                                        // js 配置别名  

  163.    modules: [`${__dirname}/src`, 'node_modules'],    // 模块寻址基路径

  164.    extensions: ['.js', '.jsx', '.vue', '.json'],     // 模块寻址扩展名

  165.  },

  166.  devtool: 'source-map',                              // sourcemap

  167.  mode: 'production',                                 // 指定 webpack 为产品模式

  168. }));


  169. // 执行一次 webpack 构建

  170. const run = (config, cb) => {

  171.  webpack(config, (err, stats) => {

  172.    if (err) {

  173.      console.error(red(err.stack || err));

  174.      if (err.details) {

  175.        console.error(red(err.details));

  176.      }

  177.      process.exit(1);

  178.    }


  179.    const info = stats.toJson();


  180.    if (stats.hasErrors()) {

  181.      info.errors.forEach(error => {

  182.        console.error(red(error));

  183.      });

  184.      process.exit(1);

  185.    }


  186.    if (stats.hasWarnings()) {

  187.      info.warnings.forEach(warning => {

  188.        console.warn(yellow(warning));

  189.      });

  190.    }


  191.    // 如果是多页面,需要把 index.html => `${page}.html`

  192.    // 因为每个页面导出的 html 文件都是 index.html 如果不重新命名,会被覆盖掉


  193.    if(cb) cb();

  194.  });

  195. };


  196. // 单页面

  197. run(config);


  198. // 多页面

  199. let index = 0;

  200. // go on

  201. const goon = () => {

  202.  run(configs[index], () => {

  203.    index += 1;


  204.    if (index < configs.length) goon();

  205.  });

  206. };


  207. goon();

2.6 analyze.js 配置

 
   
   
 
  1. const minimist = require('minimist');

  2. const chalk = require('chalk');

  3. const webpack = require('webpack');

  4. const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

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


  6. const {yellow, red} = chalk;


  7. const argv = minimist(process.argv.slice(2));


  8. const page = argv._[0];


  9. // 单页面

  10. const entryFile = `${__dirname}/src/index.js`;

  11. // 多页面

  12. const entryFile = `${__dirname}/src/${page}/index.js`;


  13. const config = {

  14.  entry: entryFile,

  15.  output: {

  16.    path: `${__dirname}/analyze/`,                    // 打包到 analyze 目录

  17.    filename: 'index.js',

  18.  },

  19.  plugins: [

  20.    new VueLoaderPlugin(),                            // vue-loader 所需

  21.    new BundleAnalyzerPlugin(),                       // 添加插件

  22.  ],

  23.  module: {

  24.    rules: [

  25.      {                                               // js 文件加载器

  26.        loader: 'babel-loader',

  27.        exclude: /node_modules/,

  28.        options: {

  29.          presets: ['@babel/preset-env', '@babel/preset-react'],

  30.          plugins: [

  31.            '@babel/plugin-transform-react-jsx',

  32.            '@babel/plugin-syntax-dynamic-import',

  33.          ],

  34.        },

  35.        test: /\.(js|jsx)$/,

  36.      },

  37.      {                                               // css 文件加载器

  38.        loader: 'style-loader!css-loader',

  39.        test: /\.css$/,

  40.      },

  41.      {                                               // less 文件加载器

  42.        loader: 'style-loader!css-loader!less-loader',

  43.        test: /\.less$/,

  44.      },

  45.      {                                               // scss 文件加载器

  46.        loader: 'style-loader!css-loader!sass-loader',

  47.        test: /\.(scss|sass)$/,

  48.      },

  49.      {                                               // 静态文件加载器

  50.        loader: 'url-loader',

  51.        test: /\.(gif|jpg|png|woff|woff2|svg|eot|ttf|ico)$/,

  52.        options: {

  53.          limit: 1,

  54.        },

  55.      },

  56.      {                                               // html 文件加载器

  57.        loader: 'html-loader',

  58.        test: /\.html$/,

  59.        options: {

  60.          attrs: ['img:src', 'link:href'],

  61.          interpolate: 'require',

  62.        },

  63.      },

  64.      {                                               // vue 文件加载器

  65.        loader: 'vue-loader',

  66.        test: /\.vue$/,

  67.      },

  68.    ],

  69.  },

  70.  resolve: {

  71.    alias: {},                                        // js 配置别名  

  72.    modules: [`${__dirname}/src`, 'node_modules'],    // 模块寻址基路径

  73.    extensions: ['.js', '.jsx', '.vue', '.json'],     // 模块寻址扩展名

  74.  },

  75.  mode: 'production',                                 // 指定 webpack 为产品模式

  76. };


  77. webpack(config, (err, stats) => {

  78.  if (err) {

  79.    console.error(red(err.stack || err));

  80.    if (err.details) {

  81.      console.error(red(err.details));

  82.    }

  83.    process.exit(1);

  84.  }


  85.  const info = stats.toJson();


  86.  if (stats.hasErrors()) {

  87.    info.errors.forEach(error => {

  88.      console.error(red(error));

  89.    });

  90.    process.exit(1);

  91.  }


  92.  if (stats.hasWarnings()) {

  93.    info.warnings.forEach(warning => {

  94.      console.warn(yellow(warning));

  95.    });

  96.  }

  97. });

2.7 扩展配置

你可以根据需要扩展配置,比如添加插件、加载器等,比如:

  • provide-plugin 可以提供一些全局模块的导出,比如 jquery

  • define-plugin 可以动态定义一些全局变量

  • css-loader 可以配置成 css-modules

  • 如果某个页面导出 js bundle 很大,想分割成多个文件,可以使用 dll-plugin、split-chunks-plugin

  • 如果想在命令行显示构建的进度,可以使用 progress-plugin

3. 封装

上面的代码可以封装成一个全局命令,比如 lila,运行上面的命令就可以更简洁:

 
   
   
 
  1. lila dev home                               # 开发 home 页面

  2. lila analyze explore                        # 模块分析 explore 页面


  3. # 构建多个页面

  4. lila build home explore about/* about/all --env test/prod

后续

更多博客,查看 https://github.com/senntyou/blogs

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)


以上是关于通用封装简化 webpack 配置的主要内容,如果未能解决你的问题,请参考以下文章

使用通用动作和减速器简化 redux

json 配合Vue.js配置Webpack -27。通过npm脚本简化的webpack和webpack-dev-server命令

使用 Typescript 的 Webpack:两个配置中的通用设置,那么哪个优先?

vue3+webpack项目搭建实验

webpack基础配置

Webpack:Webworker 和 Web 代码之间共享代码的通用块?