资讯专栏INFORMATION COLUMN

通用、封装、简化 webpack 配置

PingCAP / 2703人阅读

摘要:通用封装简化配置现在,基本上前端的项目打包都会用上,因为提供了无与伦比强大的功能和生态。简化配置的一种方式是使用社区封装好的库,比如。封装了的一些基础配置,然后暴露一些额外配置的接口,并附加本地数据模拟功能,详情可以参考主页。

通用、封装、简化 webpack 配置

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

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

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

1. 要封装哪些功能

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

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

2. 基础配置 2.1 目录结构(示例,配合后面的代码讲解)
package.json
dev.js                       # 本地开发脚本
build.js                     # 产品构建脚本
analyze.js                   # 模块大小分析(可选)

# 单页面结构
src/                         # 源代码目录
  - index.js                 # js 入口文件
  - index.html               # html 入口文件
  - ...                      # 其他文件

# 多页面结构
src/                         # 源代码目录
  - home/                    # home 页面工作空间
    - index.js               # home 页面 js 入口文件
    - index.html             # home 页面 html 入口文件
    - ...                    # home 页面其他文件
    
  - explore/                 # explore 页面工作空间
    - index.js               # explore 页面 js 入口文件
    - index.html             # explore 页面 html 入口文件
    - ...                    # explore 页面其他文件 
     
  - about/                   # about 目录
    - company                # about/company 页面工作空间
      - index.js             # about/company 页面 js 入口文件
      - index.html           # about/company 页面 html 入口文件
      - ...                  # about/company 页面其他文件
      
    - platform               # about/platform 页面工作空间
      - index.js             # about/platform 页面 js 入口文件
      - index.html           # about/platform 页面 html 入口文件
      - ...                  # about/platform 页面其他文件    
  
  - ...                      # 更多页面        
2.2 基础 npm 包
# package.json

"devDependencies": {
  "@babel/core": "^7.1.2",                  # babel core       
  "@babel/plugin-syntax-dynamic-import": "^7.0.0",         # import() 函数支持
  "@babel/plugin-transform-react-jsx": "^7.0.0",           # react jsx 支持
  "@babel/preset-env": "^7.1.0",            # es6+ 转 es5
  "@babel/preset-flow": "^7.0.0",           # flow 支持
  "@babel/preset-react": "^7.0.0",          # react 支持
  "autoprefixer": "^9.1.5",                 # css 自动添加厂家前缀 -webkit-, -moz-
  "babel-loader": "^8.0.4",                 # webpack 加载 js 的 loader
  "babel-plugin-component": "^1.1.1",       # 如果使用 element ui,需要用到这个
  "babel-plugin-flow-runtime": "^0.17.0",   # flow-runtime 支持 
  "babel-plugin-import": "^1.9.1",          # 如果使用 ant-design,需要用到这个
  "browser-sync": "^2.24.7",                # 浏览器实例组件,用于本地开发调试
  "css-loader": "^1.0.0",                   # webpack 加载 css 的 loader
  "chalk": "^2.4.1",                        # 让命令行的信息有颜色
  "file-loader": "^2.0.0",                  # webpack 加载静态文件的 loader
  "flow-runtime": "^0.17.0",                # flow-runtime 包
  "html-loader": "^0.5.5",                  # webpack 加载 html 的 loader
  "html-webpack-include-assets-plugin": "^1.0.5",          # 给 html 文件添加额外静态文件链接的插件
  "html-webpack-plugin": "^3.2.0",          # 更方便操作 html 文件的插件
  "less": "^3.8.1",                         # less 转 css
  "less-loader": "^4.1.0",                  # webpack 加载 less 的 loader
  "mini-css-extract-plugin": "^0.4.3",      # 提取 css 多带带打包
  "minimist": "^1.2.0",                     # process.argv 更便捷处理
  "node-sass": "^4.9.3",                    # scss 转 css
  "optimize-css-assets-webpack-plugin": "^5.0.1",          # 优化 css 打包,包括压缩
  "postcss-loader": "^3.0.0",               # 对 css 进行更多操作,比如添加厂家前缀
  "sass-loader": "^7.1.0",                  # webpack 加载 scss 的 loader
  "style-loader": "^0.23.0",                # webpack 加载 style 的 loader
  "uglifyjs-webpack-plugin": "^2.0.1",      # 压缩 js 的插件
  "url-loader": "^1.1.1",                   # file-loader 的升级版
  "vue-loader": "^15.4.2",                  # webpack 加载 vue 的 loader
  "vue-template-compiler": "^2.5.17",       # 配合 vue-loader 使用的 
  "webpack": "^4.20.2",                     # webpack 模块
  "webpack-bundle-analyzer": "^3.0.2",      # 分析当前打包各个模块的大小,决定哪些需要多带带打包
  "webpack-dev-middleware": "^3.4.0",       # webpack-dev-server 中间件
  "webpack-hot-middleware": "^2.24.2"       # 热更新中间件
}
2.3 基本命令
# package.json

"scripts": {
  "dev": "node dev.js",
  "build": "node build.js",
  "analyze": "node analyze.js",
}
npm run dev                                 # 开发
npm run build                               # 构建
npm run analyze                             # 模块分析

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

npm run dev -- home                         # 开发 home 页面 
npm run analyze -- explore                  # 模块分析 explore 页面 

# 构建多个页面 
npm run build -- home explore about/* about/all --env test/prod  

home, explore 确定构建的页面;about/*, about/allabout 目录下所有的页面;all, * 整个项目所有的页面

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

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

2.4 dev.js 配置

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

webpack

webpack-dev-server 或 webpack-dev-middleware

webpack-hot-middleware

HotModuleReplacementPlugin

browser-sync

const minimist = require("minimist");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const devMiddleWare = require("webpack-dev-middleware");
const hotMiddleWare = require("webpack-hot-middleware");
const browserSync = require("browser-sync");
const VueLoaderPlugin = require("vue-loader/lib/plugin");

const { HotModuleReplacementPlugin } = webpack;

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

const page = argv._[0];

 // 单页面
const entryFile = `${__dirname}/src/index.js`;
// 多页面 
const entryFile = `${__dirname}/src/${page}/index.js`; 

// 编译器对象
const compiler = webpack({
  entry: [
    "webpack-hot-middleware/client?reload=true",      // 热重载需要
    entryFile,
  ],
  output: {
    path: `${__dirname}/dev/`,                        // 打包到 dev 目录
    filename: "index.js",
    publicPath: "/dev/",
  },
  plugins: [
    new HotModuleReplacementPlugin(),                 // 热重载插件
    new HtmlWebpackPlugin({                           // 处理 html
      // 单页面
      template: `${__dirname}/src/index.html`,
      // 多页面
      template: `${__dirname}/src/${page}/index.html`,
    }),
    new VueLoaderPlugin(),                            // vue-loader 所需
  ],
  module: {
    rules: [
      {                                               // js 文件加载器
        loader: "babel-loader",
        exclude: /node_modules/,
        options: {
          presets: ["@babel/preset-env", "@babel/preset-react"],
          plugins: [
            "@babel/plugin-transform-react-jsx",
            "@babel/plugin-syntax-dynamic-import",
          ],
        },
        test: /.(js|jsx)$/,
      },
      {                                               // css 文件加载器
        loader: "style-loader!css-loader",
        test: /.css$/,
      },
      {                                               // less 文件加载器
        loader: "style-loader!css-loader!less-loader",
        test: /.less$/,
      },
      {                                               // scss 文件加载器
        loader: "style-loader!css-loader!sass-loader",
        test: /.(scss|sass)$/,
      },
      {                                               // 静态文件加载器
        loader: "url-loader",
        test: /.(gif|jpg|png|woff|woff2|svg|eot|ttf|ico)$/,
        options: {
          limit: 1,
        },
      },
      {                                               // html 文件加载器
        loader: "html-loader",
        test: /.html$/,
        options: {
          attrs: ["img:src", "link:href"],
          interpolate: "require",
        },
      },
      {                                               // vue 文件加载器
        loader: "vue-loader",
        test: /.vue$/,
      },
    ],
  },
  resolve: {
    alias: {},                                        // js 配置别名   
    modules: [`${__dirname}/src`, "node_modules"],    // 模块寻址基路径
    extensions: [".js", ".jsx", ".vue", ".json"],     // 模块寻址扩展名
  },
  devtool: "eval-source-map",                         // sourcemap 
  mode: "development",                                // 指定 webpack 为开发模式
});

// browser-sync 配置
const browserSyncConfig = {
  server: {
    baseDir: `${__dirname}/`,                         // 静态服务器基路径,可以访问项目所有文件 
  },
  startPath: "/dev/index.html",                       // 开启服务器窗口时的默认地址
};

// 添加中间件
browserSyncConfig.middleware = [
  devMiddleWare(compiler, {
    stats: "errors-only",
    publicPath: "/dev/",
  }),
  hotMiddleWare(compiler),
];

browserSync.init(browserSyncConfig);                  // 初始化浏览器实例,开始调试开发
2.5 build.js 配置

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

提取样式文件,多带带打包、压缩、添加浏览器厂家前缀

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

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

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

const minimist = require("minimist");
const webpack = require("webpack");
const chalk = require("chalk");
const autoprefixer = require("autoprefixer");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCssAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const VueLoaderPlugin = require("vue-loader/lib/plugin");

const {yellow, red} = chalk;

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

const pages = argv._; // ["home", "explore", "about/*", "about/all"]
const allPages = getAllPages(pages); // 根据 page 中的 `*, all` 等关键字,获取所有真正的 pages

// 单页面,只有一个入口,所以只有一个配置文件
const config = { ... }; 

// 多页面,多个入口,所有有多个配置文件
const configs = allPages.map(page => ({
  // 单页面
  entry: `${__dirname}/src/index.js`,                 // js 入口文件
  // 多页面
  entry: `${__dirname}/src/${page}/index.js`,         // js 入口文件
  output: {
    path: `${__dirname}/dist/`,                       // 输出路径
    filename: "[chunkhash].js",                       // 输出文件名,这里完全取 hash 值来命名
    hashDigestLength: 32,                             // hash 值长度
    publicPath: "/dist/",
  },
  plugins: [
    new MiniCssExtractPlugin({                        // 提取所有的样式文件,多带带打包
      filename: "[chunkhash].css",                    // 输出文件名,这里完全取 hash 值来命名
    }),
    new HtmlWebpackPlugin({
      // 单页面
      template: `${__dirname}/src/index.html`,        // html 入口文件
      // 多页面
      template: `${__dirname}/src/${page}/index.html`,// html 入口文件
      minify: {                                       // 指定如果压缩 html 文件
        removeComments: !0,
        collapseWhitespace: !0,
        collapseBooleanAttributes: !0,
        removeEmptyAttributes: !0,
        removeScriptTypeAttributes: !0,
        removeStyleLinkTypeAttributes: !0,
        minifyJS: !0,
        minifyCSS: !0,
      },
    }),
    new VueLoaderPlugin(),                            // vue-loader 所需
    new OptimizeCssAssetsPlugin({                     // 压缩 css
      cssProcessorPluginOptions: {
        preset: ["default", { discardComments: { removeAll: true } }],
      },
    }),
    
    // webpack 打包的 js 文件是默认压缩的,所以这里不需要再额外添加 uglifyjs-webpack-plugin
  ],
  module: {
    rules: [
      {                                               // js 文件加载器,与 dev 一致
        loader: "babel-loader",
        exclude: /node_modules/,
        options: {
          presets: ["@babel/preset-env", "@babel/preset-react"],
          plugins: [
            "@babel/plugin-transform-react-jsx",
            "@babel/plugin-syntax-dynamic-import",
          ],
        },
        test: /.(js|jsx)$/,
      },
      {                                               // css 文件加载器,添加了浏览器厂家前缀
        use: [
          MiniCssExtractPlugin.loader,
          "css-loader",
          {
            loader: "postcss-loader",
            options: {
              plugins: [
                autoprefixer({
                  browsers: [
                    "> 1%",
                    "last 2 versions",
                    "Android >= 3.2",
                    "Firefox >= 20",
                    "iOS 7",
                  ],
                }),
              ],
            },
          },
        ],
        test: /.css$/,
      },
      {                                               // less 文件加载器,添加了浏览器厂家前缀
        use: [
          MiniCssExtractPlugin.loader,
          "css-loader",
          {
            loader: "postcss-loader",
            options: {
              plugins: [
                autoprefixer({
                  browsers: [
                    "> 1%",
                    "last 2 versions",
                    "Android >= 3.2",
                    "Firefox >= 20",
                    "iOS 7",
                  ],
                }),
              ],
            },
          },
          "less-loader",
        ],
        test: /.less$/,
      },
      {                                               // scss 文件加载器,添加了浏览器厂家前缀
        use: [
          MiniCssExtractPlugin.loader,
          "css-loader",
          {
            loader: "postcss-loader",
            options: {
              plugins: [
                autoprefixer({
                  browsers: [
                    "> 1%",
                    "last 2 versions",
                    "Android >= 3.2",
                    "Firefox >= 20",
                    "iOS 7",
                  ],
                }),
              ],
            },
          },
          "sass-loader",
        ],
        test: /.(scss|sass)$/,
      },
      {                                               // 静态文件加载器,与 dev 一致
        loader: "url-loader",
        test: /.(gif|jpg|png|woff|woff2|svg|eot|ttf|ico)$/,
        options: {
          limit: 1,
        },
      },
      {                                               // html 文件加载器,与 dev 一致
        loader: "html-loader",
        test: /.html$/,
        options: {
          attrs: ["img:src", "link:href"],
          interpolate: "require",
        },
      },
      {                                               // vue 文件加载器,与 dev 一致
        loader: "vue-loader",
        test: /.vue$/,
      },
    ],
  },
  resolve: {
    alias: {},                                        // js 配置别名   
    modules: [`${__dirname}/src`, "node_modules"],    // 模块寻址基路径
    extensions: [".js", ".jsx", ".vue", ".json"],     // 模块寻址扩展名
  },
  devtool: "source-map",                              // sourcemap 
  mode: "production",                                 // 指定 webpack 为产品模式
}));

// 执行一次 webpack 构建
const run = (config, cb) => {
  webpack(config, (err, stats) => {
    if (err) {
      console.error(red(err.stack || err));
      if (err.details) {
        console.error(red(err.details));
      }
      process.exit(1);
    }
  
    const info = stats.toJson();
  
    if (stats.hasErrors()) {
      info.errors.forEach(error => {
        console.error(red(error));
      });
      process.exit(1);
    }
  
    if (stats.hasWarnings()) {
      info.warnings.forEach(warning => {
        console.warn(yellow(warning));
      });
    }
    
    // 如果是多页面,需要把 index.html => `${page}.html`
    // 因为每个页面导出的 html 文件都是 index.html 如果不重新命名,会被覆盖掉
    
    if(cb) cb();
  });
};

// 单页面
run(config);

// 多页面
let index = 0;
// go on
const goon = () => {
  run(configs[index], () => {
    index += 1;

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

goon();
2.6 analyze.js 配置

const minimist = require("minimist");
const chalk = require("chalk");
const webpack = require("webpack");
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
const VueLoaderPlugin = require("vue-loader/lib/plugin");

const {yellow, red} = chalk;

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

const page = argv._[0];

 // 单页面
const entryFile = `${__dirname}/src/index.js`;
// 多页面 
const entryFile = `${__dirname}/src/${page}/index.js`; 

const config = {
  entry: entryFile,
  output: {
    path: `${__dirname}/analyze/`,                    // 打包到 analyze 目录
    filename: "index.js",
  },
  plugins: [
    new VueLoaderPlugin(),                            // vue-loader 所需
    new BundleAnalyzerPlugin(),                       // 添加插件 
  ],
  module: {
    rules: [
      {                                               // js 文件加载器
        loader: "babel-loader",
        exclude: /node_modules/,
        options: {
          presets: ["@babel/preset-env", "@babel/preset-react"],
          plugins: [
            "@babel/plugin-transform-react-jsx",
            "@babel/plugin-syntax-dynamic-import",
          ],
        },
        test: /.(js|jsx)$/,
      },
      {                                               // css 文件加载器
        loader: "style-loader!css-loader",
        test: /.css$/,
      },
      {                                               // less 文件加载器
        loader: "style-loader!css-loader!less-loader",
        test: /.less$/,
      },
      {                                               // scss 文件加载器
        loader: "style-loader!css-loader!sass-loader",
        test: /.(scss|sass)$/,
      },
      {                                               // 静态文件加载器
        loader: "url-loader",
        test: /.(gif|jpg|png|woff|woff2|svg|eot|ttf|ico)$/,
        options: {
          limit: 1,
        },
      },
      {                                               // html 文件加载器
        loader: "html-loader",
        test: /.html$/,
        options: {
          attrs: ["img:src", "link:href"],
          interpolate: "require",
        },
      },
      {                                               // vue 文件加载器
        loader: "vue-loader",
        test: /.vue$/,
      },
    ],
  },
  resolve: {
    alias: {},                                        // js 配置别名   
    modules: [`${__dirname}/src`, "node_modules"],    // 模块寻址基路径
    extensions: [".js", ".jsx", ".vue", ".json"],     // 模块寻址扩展名
  },
  mode: "production",                                 // 指定 webpack 为产品模式
};

webpack(config, (err, stats) => {
  if (err) {
    console.error(red(err.stack || err));
    if (err.details) {
      console.error(red(err.details));
    }
    process.exit(1);
  }

  const info = stats.toJson();

  if (stats.hasErrors()) {
    info.errors.forEach(error => {
      console.error(red(error));
    });
    process.exit(1);
  }

  if (stats.hasWarnings()) {
    info.warnings.forEach(warning => {
      console.warn(yellow(warning));
    });
  }
});
2.7 扩展配置

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

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

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

css-loader 可以配置成 css-modules

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

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

3. 封装

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

lila dev home                               # 开发 home 页面 
lila analyze explore                        # 模块分析 explore 页面 

# 构建多个页面 
lila build home explore about/* about/all --env test/prod
后续

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

作者:深予之 (@senntyou)

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/99701.html

相关文章

  • 构建Vue-cli通用项目目录

    摘要:使用基于依赖追踪的观察并且使用异步队列更新。为项目配置文件。为项目静态资源目录。其实个人感觉通用项目目录可以很随意的搭配,比如说之后清空目录封装通用组件,像是啊,滑动常用组件。 写在前面的个人体会,大神们可以跳过 这段时间接手一个后台管理项目,从最开始写一点我自己的体会吧。首先Vue,Angular和React是当今主流前端三大框架。Vue是一个用来构建网页的JS库,相比较Angula...

    Winer 评论0 收藏0
  • 全栈开发自学路线

    摘要:前言这里筑梦师是一名正在努力学习的开发工程师目前致力于全栈方向的学习希望可以和大家一起交流技术共同进步用简书记录下自己的学习历程个人学习方法分享本文目录更新说明目录学习方法学习态度全栈开发学习路线很长知识拓展很长在这里收取很多人的建议以后决 前言 这里筑梦师,是一名正在努力学习的iOS开发工程师,目前致力于全栈方向的学习,希望可以和大家一起交流技术,共同进步,用简书记录下自己的学习历程...

    galaxy_robot 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<