【前端工程化】EasyBuilders-前端打包编译通用解决方案(01. EasyWebpacker)

665 阅读16分钟

前言

先解释一下来由,大概率也是大家工作中经常会面临的问题:

  • 所有的项目都会有一套自己的webpack配置,版本差异差的离谱,不同项目完全不兼容;
  • 所有的项目都是从0到1的,搭建者能力不同、或者技术设计的针对点不同、考虑的全面程度不一样的等,很难给出一套比较实用又接近完美的配置,缺少沉淀;

我理想的状态是在cover住一定的兼容版本的前提下,尽可能去沉淀去优化一套打包编译工具,集思广益。 即使出现下一个版本,跨版本的问题,也可以基于当前比较完全的配置直接去针对性升级。这也是我理解的工程化的样子。

工程化组件,一定要考虑的是可复用性以及可扩展性。理解是,我是小霸王的一张游戏卡,插到指定版本的小霸王游戏机上,都可以运行~当然,前端技术迭代飞快,我们面对的是工程,要的是稳定而不是实验,所以,所有的工程化方案都是基于相对稳定版本依赖实现。

EasyBuilders作为一项前端打包编译通用解决方案提出,支持Webpacker、Rolluper、Viter,全部适配实际项目多环境打包的案例。

接下来结合实际项目配置,讲解一下常用配置。

  • Webpacker
  • Viter

Webpacker

主要功能:

  • 热更新:完善的TypeScript + React + scss技术栈热更新支持。
  • 环境区分:process.env.D_ENV判断,值为dev, test, pre, production
  • 构建性能优化。
  • 预设拆包最佳实践。
  • 完全可扩展,暴露方法可以传入对应自定义配置进行融合。

规划:
1.less -F
2.常用到的,能提升性能的打包配置/插件全部加上
3.完善为 TypeScript/javascript/ es6 + React/Vue + scss/less 技术栈热更新支持。
4.Vue
5.js/es6

接下来,详细讲下webpacker以及结合webpacker,看一下webpack的常用配置。

传参解析

export interface ConfigOptions {
  static: string; // 镜像节点 举例 https://aaaa.com/ccc/ddd
  devPort?: number; // 开发环境端口号
  report?: boolean; //是否调用webpack打印日志报告
}

主要分为以下几部分:

getBaseConfig 获取通用基础配置

一些基础配置,无论是开发环境、测试环境、还是生产环境,都通用的配置。

import { Root } from './utils';
import merge from 'webpack-merge';
import * as webpack from 'webpack';
import { createHappyPlugin } from './happypack';
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
  • ► mode

由指定环境config的mode进行配置,默认production,不使用任何优化。 选项 | 描述 ---|--- development | 会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 development。打包的文件默认不被压缩,开发时可以设置为development。 production | 会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 production。 打包的文件默认被压缩 none | 不使用任何默认优化选项

mode: 'none', //由指定环境config的mode进行配置,默认production,不使用任何优化
  • ► entry

打包入口

entry: {
      app: Root('src/index'), // name ==> app
},

===========================知识补充===========================
1.MPA多页面配置:由于此时是多页面应用,

  • 故entry不再是数组形式按照原来数组的格式配置每个入口项,对应到entry对象中的某个key值。
  • webpack.config.js中找到plugins:复制三个new HtmlWepackPlugin实例,并修改其中的第二个参数的 template 项,并在其中增加filename项和chunks:['query'] 项。配置完毕尝试 build 编译。若不指定chunks,则所有的chunks会载入html中
# MPA 
# entry 配置样例
entry: {
    app: './src/app.js',
    search: './src/search.js'
},

对象中变量可直接应用于输出配置文件名等,eg.output/filename: '[name].js'

  • ► output

打包输出配置

    output: {
    
      path: Root("dist"),
      /**
       * path是webpack所有文件的输出的路径,必须是绝对路径,
       * 比如:output输出的js,url-loader解析的图片,HtmlWebpackPlugin生成的html文件,
       * 都会存放在以path为基础的目录下, 
       * “path”仅仅告诉Webpack结果存储在哪里,
       */
       
       
      publicPath: '/',
      //“publicPath”项则被许多Webpack的插件用于在生产模式下更新内嵌到css、html文件里的url值,
      /**
       * 例如,在localhost(即本地开发模式)里的css文件中边你可能用“./test.png”这样的url来加载图片,
       * 但是在生产模式下“test.png”文件可能会定位到CDN上并且你的Node.js服务器可能是运行在HeroKu上边的。
       * 这就意味着在生产环境你必须手动更新所有文件里的url为CDN的路径。
       * 
       * 在生产模式下,对你的页面里面引入的资源的路径做对应的补全
       * 比如在 prod配置 publicPath: `https://static.ccc.com/a/`, 
       * 生产环境url loader会把css中的url直接更新为 https://static.ccc.com/a/xxxxx
       *
       * 在dev/test 配置 publicPath: '/',
       * 在开发阶段,我们要用devServer启动一个开发服务器,这里也有一个publicPath需要配置。
       * webpack-dev-server打包的文件是放在内存中的而不是本地上,这些打包后的资源对外的根目录就是publicPath。
       * http://localhost:9000/dist/  +  资源名, 就可以访问到该资源
       * 
       * 生产环境: 当打包的时候,webpack会在静态文件路径前面添加publicPath的值,当我们把资源放到CDN上的时候,把publicPath的值设为CDN的值就可以了
       * 开发环境: 但是当我们使用webpack-dev-server 进行开发时,它却不是在静态文件的路径上加publicPath的值,
       * 相反,它指的是webpack-dev-server 在进行打包时生成的静态文件所在的位置, 相当于/+url
       * 样例,开发环境,将注入到html中的静态资源文件路径前面加上制定地址
       *   webpack配置,publicPath: '/cd/';
       *   实际打包,index.html中引入静态资源:
       *   <script src="/cd/assets/js/runtime_ae78c761.js"></script>
       *   <script src="/cd/assets/js/chunks/ui-libs_0f0fdd38.js"></script>
       *   <script src="/cd/assets/js/chunks/vendors_218bc1f2.js"></script>
       *   <script src="/cd/assets/js/chunks/app_bb776138.js"></script></body>
       */
       
       
       
      filename: "assets/js/[name]_[hash:8].js", 
      // app_HHHJKKLS.js
      // 考虑缓存的问题,一定要有hash
      // filename: '[name]-[hash].bundle.js' 
      // 是项目级别的,就是有一个文件发生改动,打包后的所有文件 hash值都会发升变化
      // filename: '[name]-[chunkhash].bundle.js'  
      // 在打包过程当中,只要是同一路的打包,那么chunkhash 都是相同的
      // filename: '[name]-[contenthash:8].bundle.js' 
      // 文件级别的 hash,根据输出文件的内容生成的 hash 值,:8 设置的是 hash值的长度

      
      
      chunkFilename: 'assets/js/chunks/[name]_[contenthash:8].js',
      /**
       * chunkname我的理解是未被列在entry中,却又需要被打包出来的文件命名配置。
       * 没指定也会打包,此处为显式指定该部分文件的名称
       * 
       * 什么场景需要呢?
       * 在按需加载(异步)模块的时候,这样的文件是没有被列在entry中的,
       * 如使用CommonJS的方式异步加载模块
       * require.ensure(["modules/tips.jsx"], 
            function(require) { var a = require("modules/tips.jsx"); // ... }, 'tips');
       * output.chunkFilename 默认使用 [id].js 或从 output.filename 中推断出的值([name] 会被预先替换为 [id] 或 [id].),
       * 没有配置该项的时候也会有打包输出,只是命名为默认值.
eg.
       */
       
        /* 
        * 执行默认配置
        * {
        *     entry: {
        *         index: "../src/index.js"
        *     },
        *     output: {
        *         filename: "[name].min.js", // index.min.js
        *     }
        * }

        * 打包结果:
        * output.filename 的输出文件名是 [name].min.js,
        * [name] 根据 entry 的配置推断为 index,所以输出为 index.min.js;

        * 由于 output.chunkFilename 没有显示指定,
        * 就会把 [name] 替换为 chunk 文件的 id 号,这里文件的 id 号是 1,所以文件名就是 1.min.js

        * 执行显式设置
        * {
        *     entry: {
        *          index: "../src/index.js"
        *     },
        *     output: {
        *         filename: "[name].min.js",  // index.min.js
        *         chunkFilename: 'bundle.js', // bundle.js
        *     }
        * }

        * 打包结果:
        * output.filename 的输出文件名是 [name].min.js,
        * [name] 根据 entry 的配置推断为 index,所以输出为 index.min.js;

        * output.chunkFilename 的输出文件名是 bundle.js
      */
   },
    
  • **► resolve **

解析配置

    resolve: {
    
      modules: [Root('node_modules'), 'node_modules', Root('src')],
      /**
       * 配置 Webpack 去哪些目录下寻找第三方模块,默认是只会去  node_modules  目录下寻找
       * 越靠前优先级越高。
       * 相对路径将类似于 Node 查找 'node_modules' 的方式进行查找。
       * 使用绝对路径,将只在给定目录中搜索。
       * 
       * 为什么有src ???
       *
       * 有时你的项目里会有一些模块会大量被其它模块依赖和导入,
       * 由于其它模块的位置分布不定,针对不同的文件都要去计算被导入模块文件的相对路径, 这个路径有时候会很长,
       * 就像这样  import '../../../components/button'  
       * 这时你可以利用  modules  配置项优化,
       * 假如那些被大量导入的模块都在  ./src/components  目录下,
       * 把  modules  配置成modules:['./src/components','node_modules']后,
       * 你可以简单通过  import 'button'  导入。
       */


      alias: {
      //      components: './src/components/'
      }
      /**
       * 当你通过  import Button from 'components/button 导入时,
       * 实际上被 alias 等价替换成了  import Button from './src/components/button' 。
       * 以上 alias 配置的含义是把导入语句里的  components  关键字替换成  ./src/components/ 。
       */
       
       
      extensions: ['.tsx', '.ts', '.jsx', '.js'], 
      // 在导入语句没带文件后缀时,Webpack 会自动带上后缀后去尝试访问文件是否存在
    },
  • ► module

Module 中可以配置对指定的文件类型进行指定的 Loader 解析规则

   module: {
      rules: [
      //   引入各种处理loader
      //   以及happyPack等
      //   loader处理规则为由后至前,是栈的形式执行
      
      
     
        {
          test: /\.tsx?$/,
          //把对.tsx? 的文件处理交给id为happy-ts 的HappyPack 的实例执行,happyPack实例在plugins中配置完成
          use: 'happypack/loader?id=happy-ts',
          /**
           * 需要结合 happyPlugin 来处理
           * 
           * 能同一时间处理多个任务,发挥多核 CPU 电脑的威力,HappyPack 就能让 Webpack 做到这点,
           * 它把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程
           * 
           * js 的情况
           * 把对.js 的文件处理交给id为happyBabel 的HappyPack 的实例执行
           * loader: 'happypack/loader?id=happyBabel',
           */
          exclude: /node_modules/,
        },
        
        /**
         * 使用 @babel/preset-typescript 取代 awesome-typescript-loader和ts-loader
         *
         * 1. awesome-typescript-loader方案是如何对TypeScript进行处理的
         *
         * 2.@babel/preset-typescript
         * 要使用@babel/preset-typescript,务必确保你是Babel7+
         *
         * @babel/preset-typescript和@babel/preset-react类似,是将特殊的语法转换为JS
         * 但是有点区别的是,@babel/preset-typescript是直接移除TypeScript,转为JS,这使得它的编译速度飞快。
         * 并且只需要管理Babel一个编译器就行了,因为我将脚手架中的typescript库卸载后,依然可以完美运行。
         * 而且重要的是你写的TypeScript不会再进行类型检测,使得你改动代码后中断运行的页面。
         */

        /**
         * ts ===> es ===> js
         * 首先我们需要知道TypeScript是一个将TypeScript转换为指定版本JS代码的编译器,
         * 而Babel同样是一个将新版本JS新语法转换为低版本JS代码的编译器。
         * 所以我们之前的方案每次修改了一点代码,都会将TS代码传递给TypeScript转换为JS,
         * 然后再将这份JS代码传递给Babel转换为低版本JS代码。
         * 因此我们需要配置两个编译器,并且每次做了一点更改,都会经过两次编译。
         */
 
        
        {
          test: /\.jpe?g$|\.ico$|\.png$|\.svg$/,
          use: {
            loader: 'file-loader',
            options: {
              name: '[name].[hash:8].[ext]',
              outputPath: 'assets/images/',
            },
          },
        },
        
        
        {
          test: /\.(mp4|webm|ogg|mp3|wav|flac|aac|gif)(\?.*)?$/,
          loader: 'url-loader',
          options: {
            limit: 1024,
            name: 'assets/media/[name].[hash:7].[ext]',
          },
        },
        
        
        {
          test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
          loader: 'url-loader',
          options: {
            limit: 1024,
            name: 'assets/fonts/[name].[hash:7].[ext]',
          },
        },
        


        // 将.js文件中的es6语法转成es5语法
        { test: /\.js$/, use: 'babel-loader', exclude: /node_modules/ },
        
        
        // 配置 vue-loader 来处理 .vue 文件
        { test: /\.vue$/, use: 'vue-loader' },
        
        
        // {
        //   test: /\.md$/,
        //   use: {
        //     loader: 'raw-loader',
        //     options: {
        //       name: '[name].[hash:8].[ext]',
        //       outputPath: 'assets/docs/',
        //     },
        //   },
        // },
      ],
    },

===========================知识补充===========================

sass-loader与node-sass版本协调的问题, 切换node版本需要 npm rebuild node-sass

  • ► optimization

打包优化项

optimization: {
      
      // 增加模块标识: development默认都为true, production默认为false,
      // 选择是否给module和chunk更有意义的名称
      namedModules: true, 
      // 形式有所不同: 为true打包编译之后会是{'./src/b.js': (funciton()) }, 
      // 为false会是[(function())]
      namedChunks: true, 
      // false采用索引[1,2,1], true [1,"runtime~app","b"]
      
      
      splitChunks: {
      // 主要就是根据不同的策略来分割打包出来的`bundle`
      
        chunks: 'async', // 默认
        // 同时分割同步和异步代码,
        // 拆分模块的范围,它有三个值async、initial和all
        //      async表示只从异步加载得模块(动态加载import())里面进行拆分,分割异步打包的代码
        //      initial表示只从入口模块进行拆分,也会同时打包同步和异步,
        //             但是异步内部的引入不再考虑,直接打包在一起 
        //      all表示以上两者都包括,同时分割同步和异步代码,推荐。
        
        minSize: 30000, // 最小拆分组件大小
        
        minChunks: 1, // 最小公用模块次数,默认为1
        
        maxAsyncRequests: 5,
        // 限制异步模块内部的并行最大请求数的,对应chunks => async
        // 当整个项目打包完之后,一个按需加载的包最终被拆分成n个包,maxAsyncRequests就是用来限制n的最大值
        
        maxInitialRequests: 3,
        // 允许入口并行加载的最大请求数 ,同上,对应chunks => initial
        
        name: true, 
        //split 的 chunks name, 
        // 默认为true,返回${cacheGroup的key} ${automaticNameDelimiter} ${moduleName},可以自定义
        
        cacheGroups: { 
        // 设置缓存的 chunks 策略
        
          'ui-libs': {
            test: chunk => chunk.resource 
            && /\.js$/.test(chunk.resource) 
            && /node_modules/.test(chunk.resource) 
            && /react|mobx|redux|antd|@ant-*|ora-ui/.test(chunk.resource),
            
            chunks: 'initial',
            name: 'ui-libs',  
            priority: 4,
          },
          'chart-libs': {
            test: chunk => chunk.resource 
            && /\.js$/.test(chunk.resource) 
            && /node_modules/.test(chunk.resource) 
            && /echarts/.test(chunk.resource),
            
            chunks: 'initial',
            name: 'chart-libs',
            priority: 3,
          },
          vendors: {
            test: chunk => chunk.resource 
            && /\.js$/.test(chunk.resource) 
            && /node_modules/.test(chunk.resource),
            
            chunks: 'initial',
            name: 'vendors',
            priority: 1,
          },
          'async-vendors': {
            test: /[\\/]node_modules[\\/]/,
            minChunks: 2,
            chunks: 'async',
            name: 'async-vendors',
          },
        },
      },
      
      
      runtimeChunk: {
        name: 'runtime',
        /**
         * 设置runtimeChunk是将包含chunks 映射关系的 list单独从 app.js里提取出来,
         * 因为每一个 chunk 的 id 基本都是基于内容 hash 出来的,所以每次改动都会影响它,
         * 如果不将它提取出来的话,等于app.js每次都会改变。缓存就失效了。
         * 设置runtimeChunk之后,webpack就会生成一个个runtime~xxx.js的文件。
         * 然后每次更改所谓的运行时代码文件时,打包构建时app.js的hash值是不会改变的。
         * 如果每次项目更新都会更改app.js的hash值,那么用户端浏览器每次都需要重新加载变化的app.js,
         * 如果项目大切优化分包没做好的话会导致第一次加载很耗时,导致用户体验变差。
         * 现在设置了runtimeChunk,就解决了这样的问题。
         * 所以这样做的目的是避免文件的频繁变更导致浏览器缓存失效,所以其是更好的利用缓存。提升用户体验。
         */
        /**
         * 虽然每次构建后app的hash没有改变,但是runtime~xxx.js会变啊。
         * 每次重新构建上线后,浏览器每次都需要重新请求它,它的 http 耗时远大于它的执行时间了,
         * 所以建议不要将它单独拆包,而是将它内联到我们的 index.html 之中。
         * 这边我们使用script-ext-html-webpack-plugin来实现。
         * (也可使用html-webpack-inline-source-plugin,其不会删除runtime文件。)
         */
         // 它的作用是将包含chunks 映射关系的 list单独从 app.js里提取出来,
         // 因为每一个 chunk 的 id 基本都是基于内容 hash 出来的,
         // 所以你每次改动都会影响它,如果不将它提取出来的话,等于app.js每次都会改变。
         // 缓存就失效了。
      },
    },

===========================知识补充===========================
一些打包实例: image.png image.png image.png image.png image.png image.png image.png

  • ► plugins

webpack构建插件

    plugins: [
     
      // new CopyPlugin({
      //   patterns: [
      //     { from: Root('src/favicon.ico'), to: '.' }
      //   ]
      // }),
      // 并非旨在复制从构建过程中生成的文件,而是在构建过程中复制源树中已经存在的文件
      /**
       * from  定义要拷贝的源文件         from:__dirname+'/src/components'
       * to      定义要拷贝到的目标文件夹  to: __dirname+'/dist'
       * toType  file 或者 dir          可选,默认是文件
       * force   强制覆盖前面的插件        可选,默认是文件
       * context                        可选,默认base   context可用specific  context
       * flatten  只拷贝指定的文件         可以用模糊匹配
       * ignore  忽略拷贝指定的文件         可以模糊匹配
       */


      // happypack
      createHappyPlugin('happy-ts', [
        {
          loader: 'ts-loader',
          options: {
            happyPackMode: true, 
            // 在添加happyPackMode: true后要和fork-ts-checker-webpack-plugin进行配合使用,
            // 完善检查机制
            transpileOnly: true,
          },
        },
      ]),
      // 创建id为happy-ts的happyPack实例。
      

      CleanWebpackPlugin(['dist'], {
        root: Root(), //一个根的绝对路径
        verbose: true,// 将log写到 console.
        dry: false,// 不要删除任何东西,主要用于测试.
        exclude: []//排除不删除的目录,主要用于避免删除公用的文件
      }),

      new ForkTsCheckerWebpackPlugin(),
      // The minimal webpack config (with ts-loader)
    ],

===========================知识补充===========================

cleanWebpackPlugin: image.png

htmlWebpackPlugin:
添加minify配置,进行html代码压缩 image.png htmlWebpackPlugin.chunks: 如果不指定chunks,默认会把所有entry相关的chunks都载入到html中;如果指定了一个entry的chunks,也就是入口,只会把这个入口相关的文件插入html。

getProdBaseConfig

在getBaseConfig的基础之上,再做处理,使用webpack.merge()。

import * as MiniCssExtractPlugin from 'mini-css-extract-plugin';
import * as ParallelUglifyPlugin from 'webpack-parallel-uglify-plugin';
const optimizeCss = require('optimize-css-assets-webpack-plugin');
  • mode
mode: "production",
  • module 增加样式压缩
module: {
      rules: [
        {
          test: /.s?css$/,
          use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
          /**
           * mini-css-extract-plugin:
           * 从css文件中提取css代码到单独的文件中,对css代码进行代码压缩等
           * 
           * 版本兼容坑。
           * 在使用mini-css-extract-plugin的0.9.0版本的时候估计是和其他某个插件冲突了,会有这么一个错误
           * No module factory available for dependency type: CssDependency
           * 可以尝试降级到0.8.2或者0.8.0版本即可解决
           * 
           * 第二个,使用了mini-css-extract-plugin的loader必须配合plugin部分一起使用。
           */
        },
        {
          test: /.less$/,
          use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader'],
        }
      ],
    },
  • plugins
plugins: [
      new MiniCssExtractPlugin({
        filename: '/assets/styles/style_[hash:8].css',
      }),
      

      new optimizeCss({
        cssProcessor: require('cssnano'), //引入cssnano配置压缩选项
        cssProcessorOptions: {
          discardComments: { removeAll: true }
        },
        canPrint: true //是否将插件信息打印到控制台
      })
      /**
         * optimize-css-assets-webpack-plugin & cssnano
         * 普通压缩:
         *   plugins: [
         *       new optimizeCss()
         *   ]
         *
         *
         * 使用cssnano规则压缩:
         * plugins: [
         *     new optimizeCss({
         *             cssProcessor: require('cssnano'), //引入cssnano配置压缩选项
         *             cssProcessorOptions: {
         *             discardComments: { removeAll: true }
         *            },
         *             canPrint: true //是否将插件信息打印到控制台
         *         })
         * ]
         */
],
  • optimazation
optimization: {
      minimizer: [
        new ParallelUglifyPlugin({
          cacheDir: '.cache/',
          // 缓存压缩后的结果,下次遇到一样的输入时直接从缓存中获取压缩后的结果并返回,
          // cacheDir 用于配置缓存存放的目录路径。默认不会缓存,想开启缓存请设置一个目录路径。
          uglifyES: {
            // 用于压缩 ES6 代码时的配置,Object 类型,直接透传给 UglifyES 的参数。
            // uglifyJS:用于压缩 ES5 代码时的配置,Object 类型,直接透传给 UglifyJS 的参数。
            output: {
              comments: false,
              beautify: false,
            },
            compress: {
              drop_console: false,
              collapse_vars: true,
              reduce_vars: true,
            },
          },
          /**
           * test: 使用正则去匹配哪些文件需要被 ParallelUglifyPlugin 压缩,默认是 /.js$/.
           * include: 使用正则去包含被 ParallelUglifyPlugin 压缩的文件,默认为 [].
           * exclude: 使用正则去不包含被 ParallelUglifyPlugin 压缩的文件,默认为 [].
           * workerCount:开启几个子进程去并发的执行压缩。默认是当前运行电脑的 CPU 核数减去1。
           * sourceMap:是否为压缩后的代码生成对应的Source Map, 默认不生成,开启后耗时会大大增加,
           * 一般不会将压缩后的代码的
           */
            /**
             * webpack默认提供了UglifyJS插件来压缩JS代码,
             * 但是它使用的是单线程压缩代码,
             * 也就是说多个js文件需要被压缩,它需要一个个文件进行压缩。
             * 所以说在正式环境打包压缩代码速度非常慢(因为压缩JS代码需要先把代码解析成用Object抽象表示的AST语法树,
             * 再去应用各种规则分析和处理AST,导致这个过程耗时非常大)。
             * 
             * 当webpack有多个JS文件需要输出和压缩时候,原来会使用UglifyJS去一个个压缩并且输出,
             * 但是ParallelUglifyPlugin插件则会开启多个子进程,
             * 把对多个文件压缩的工作分别给多个子进程去完成,但是每个子进程还是通过UglifyJS去压缩代码。
             * 无非就是变成了并行处理该压缩了,并行处理多个子任务,效率会更加的提高。
             */
        })
      ]
    },
  • devtool

配置sourceMap

devtool: "cheap-module-source-map",

getDevConfig

开发环境webpack配置

基于getBaseConfig。

import * as HtmlPlugin from 'html-webpack-plugin';
import * as betterProgress from 'better-webpack-progress';
import * as FriendlyErrorsPlugin from 'friendly-errors-webpack-plugin';

基础准备

const interfaces = require('os').networkInterfaces();
    for (const devName in interfaces) {
      for (const alias of interfaces[devName]) {
        if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
          return alias.address;
        }
      }
    }
    return '';
  };
  
  const PORT = options?.devPort || 9000;
  const IP = getIPAddress();
  • mode
mode: "development",
  • resolve
 resolve: {
        alias: {
          "react-dom": "@hot-loader/react-dom"
          // 在构建react项目时,默认使用的webpack-dev-serve有热刷新功能,
          // 但是局限是修改一处会使整个页面刷新,当引入了react-hot-loader时,可以实现局部刷新,
          // 即同个页面上,某一处的数据修改不会让整个页面一起刷新
          // 在react16.6+以后,推荐使用兼容性更好的 @hot-loader/react-dom 来代替react-dom

          // 因此需要本alias, 但我理解这个更应该在业务项目中做处理
        }
      },
  • module
module: {
        rules: [
          {
            test: /.s?css$/,
            use: ['css-hot-loader', 'style-loader', 'css-loader', 'sass-loader'],
            /**
             * style-loader——将处理结束的CSS代码存储在js中,运行时嵌入<style>后挂载至html页面上
             * css-loader——加载器,使webpack可以识别css模块
             * sass-loader——加载器,使webpack可以识别scss/sass文件,默认使用node-sass进行编译
             * 
             * css-hot-loader 在大多数情况下,我们可以通过style-loader实现CSS热重载。
             * 但是样式加载器需要将样式标签注入文档中,在js就绪之前,网页将没有任何样式
             * 使用webpack4 时存在问题。请使用mini-css-extract-plugin替换extract-text-webpack-plugin。
             */
          },
          {
            test: /.less$/,
            use: ['css-hot-loader', 'style-loader', 'css-loader', 'less-loader'],
          },
        ]
},
  • devServer
      devServer: {
        port: PORT,
        open: true,
        quiet: true,
        
        historyApiFallback: true,
        /**
         * devServer.historyApiFallback的意思是当路径匹配的文件不存在时不出现404,
         * 而是取配置的选项historyApiFallback.index对应的文件
         * 
         * 单页应用(SPA)一般只有一个index.html, 导航的跳转都是基于HTML5 History API,
         * 当用户在越过index.html 页面直接访问这个地址或是通过浏览器的刷新按钮重新获取时,
         * 就会出现404问题;
         * 比如 直接访问/login, /login/online,这时候越过了index.html,去查找这个地址下的文件。
         * 由于这是个一个单页应用,最终结果肯定是查找失败,返回一个404错误。
         * 这个中间件就是用来解决这个问题的;
         * 只要满足下面四个条件之一,这个中间件就会改变请求的地址,指向到默认的index.html:
         * 1 GET请求
         * 2 接受内容格式为text/html
         * 3 不是一个直接的文件请求,比如路径中不带有 .
         * 4 没有 options.rewrites 里的正则匹配
         */
         
         
        inline: true,
        hot: true,
        /**
         * inline选项会为入口页面添加“热加载”功能,即代码改变后重新加载页面。
         * 
         * 当使用--hot参数时,只能使用hash,如果使用chunkhash会报错
         * 在使用--inline时,hash和chunkhash都可以使用
         * 
         * webpack的hash字段是根据每次编译compilation的内容计算所得,也可以理解为项目总体文件的hash值,而不是针对每个具体文件的。
         * chunkhash是根据模块内容计算出的hash值。
         */
        
        contentBase: Root('dist'),
        host: '0.0.0.0',
      },
  • ► plugins
    plugins: [
        new webpack.HotModuleReplacementPlugin(),
        /**
         * webpack官方文档(devserverhot)中介绍,使用hmr的时候,需要满足两个条件:
         * 配置devServer.hot为true
         * 配置webpack.HotModuleReplacementPlugin插件
         */
         
         
        new webpack.DefinePlugin({
          'process.env.D_ENV': isLocal ? '"local"' : '"dev"',
        }),
        
        
        new HtmlPlugin({
          template: Root('src/index.html'),
          filename: 'index.html',
          env: isLocal ? 'local' : 'dev',
        }),
        
        
        new FriendlyErrorsPlugin({
          compilationSuccessInfo: {
            messages: [
              `This APP is runing at:
              - Network: http://${IP}:${PORT}/
              - Local:   http://localhost:${PORT}/
              `,
            ],
          },
        }),
        
        
        new webpack.ProgressPlugin(betterProgress({
          mode: 'compact', // or 'detailed' or 'bar'
        })),
      ],
  • ► devtool
devtool: 'cheap-module-eval-source-map'

===========================知识补充===========================

DefinePlugin:
Webpack的DefinePlugin要求我们将所有东西都包装在JSON.stringify中

# 样例
plugins: [
  new webpack.DefinePlugin({
    DEV: JSON.stringify('development'), // production
    flag: 'true',
    calc: '1 + 1'
  })
]
# 取值
// DefinePlugin定义值
console.log(DEV);    // development
console.log(flag);      // true
console.log(calc);      // 2
// console.log(process); 

getTestConfig

基于getProdBaseConfig

export function getTestConfig(options: ConfigOptions, extra: Configuration = {}) {
  return merge(
    getProdBaseConfig(options),

    {
      output: {
        publicPath: `${options.static}`,
      },

      plugins: [
        new webpack.DefinePlugin({
          'process.env.D_ENV': '"test"',
        }),
        new HtmlPlugin({
          template: Root('src/index.html'),
          filename: 'index.html',
          env: 'test',
        })
      ],
      devtool: "cheap-module-source-map"
    } as Configuration,

    extra
  );
}

getProdConfig

const config: Configuration = merge(
    getProdBaseConfig(options),

    {
      output: {
        publicPath: `${options.static}`,
      },

      plugins: [
        new webpack.DefinePlugin({
          'process.env.D_ENV': '"production"',
        }),

        new HtmlPlugin({
          template: Root('src/index.html'),
          filename: 'index.html',
          env: 'production',
        }),
      ] as any,
    },
    extra
  );

需要额外处理的部分,是bundleAnalysis

import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';

if (options.report) {
    config.plugins = config.plugins || [];

    config.plugins.push(
      new BundleAnalyzerPlugin({
        analyzerMode: 'server',
        /**
         *   可以是`server`,`static`或`disabled`。
         *   在`server`模式下,分析器将启动HTTP服务器来显示软件包报告。
         *   在“静态”模式下,会生成带有报告的单个HTML文件。
         *   在`disabled`模式下,你可以使用这个插件来将`generateStatsFile`设置为`true`来生成Webpack Stats JSON文件。
         * 
         */

        analyzerHost: '127.0.0.1', //  将在“服务器”模式下使用的主机启动HTTP服务器。
        analyzerPort: 9999,//  将在“服务器”模式下使用的端口启动HTTP服务器。
        reportFilename: 'report.html',//  路径捆绑,将在`static`模式下生成的报告文件。 相对于捆绑输出目录。
        defaultSizes: 'parsed',//  模块大小默认显示在报告中。应该是`stat``parsed`或者`gzip`中的一个。
        openAnalyzer: true,//  在默认浏览器中自动打开报告
        generateStatsFile: false,//  如果为true,则Webpack Stats JSON文件将在bundle输出目录中生成
        statsFilename: 'stats.json',
        //  如果`generateStatsFile``true`,将会生成Webpack Stats JSON文件的名字。
        // 相对于捆绑输出目录。
        statsOptions: null,
        /**
         *   stats.toJson()方法的选项。
         *   例如,您可以使用`source:false`选项排除统计文件中模块的来源。
         *   在这里查看更多选项:https:  //github.com/webpack/webpack/blob/webpack-1/lib/Stats.js#L21
         */
        logLevel: 'info',// 日志级别。可以是'信息''警告''错误''沉默'。
      })
    );
  }

utils

  • ► Root()
import * as path from 'path';

export function Root(...paths: string[]) {
  return path.join(process.cwd(), ...paths);
  // process.cwd() 是当前执行node命令时候的文件夹地址
  // 工作目录, 保证了文件在不同的目录下执行时,路径始终不变
  // __dirname 是被执行的js 文件的地址 
  // 文件所在目录, 实际上不是一个全局变量,而是每个模块内部的
};
  • ► createHappy()
import * as HappyPack from 'happypack';
import { Rule } from 'webpack';
import * as os from 'os';

const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });

export function createHappyPlugin(id: string, loaders: Rule[]) {
  return new HappyPack({
    id,
    loaders,
    threadPool: happyThreadPool,
  });
}

  1. 在 Loader 配置中,所有文件的处理都交给了 happypack/loader 去处理,使用紧跟其后的 querystring ?id=babel 去告诉 happypack/loader 去选择哪个 HappyPack 实例去处理文件。
  2. 在 Plugin 配置中,新增了两个 HappyPack 实例分别用于告诉 happypack/loader 去如何处理 .js 和 .css 文件。选项中的 id 属性的值和上面 querystring 中的 ?id=babel 相对应,选项中的 loaders 属性和 Loader 配置中一样。
  • id: String 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件.
  • loaders: Array 用法和 webpack Loader 配置中一样.
  • threads: Number 代表开启几个子进程去处理这一类型的文件,默认是3个,类型必须是整数。
  • verbose: Boolean 是否允许 HappyPack 输出日志,默认是 true。
  • threadPool: HappyThreadPool 代表共享进程池,即多个 HappyPack 实例都使用同一个共享进程池中的子进程去处理任务,以防止资源占用过多。
  • verboseWhenProfiling: Boolean 开启webpack --profile ,仍然希望HappyPack产生输出。
  • debug: Boolean 启用debug 用于故障排查。默认 false。

所引用的插件及版本:

  "dependencies": {
    "@babel/preset-typescript": "^7.13.0",
    "@hot-loader/react-dom": "^16.13.0",
    "@types/html-webpack-plugin": "^3.2.3",
    "@types/node": "^14.6.0",
    "@types/react-hot-loader": "^4.1.1",
    "@types/webpack-bundle-analyzer": "^3.9.1",
    "@types/webpack-env": "^1.15.2",
    "@types/webpack-merge": "^4.1.5",
    "@types/webpack-node-externals": "^2.5.0",
    "awesome-typescript-loader": "^5.2.1",
    "babel-core": "^6.26.3",
    "babel-loader": "^7.1.5",
    "babel-plugin-transform-runtime": "^6.23.0",
    "babel-preset-env": "^1.7.0",
    "babel-preset-stage-0": "^6.24.1",
    "better-webpack-progress": "^1.1.0",
    "clean-webpack-plugin": "^3.0.0",
    "copy-webpack-plugin": "^6.0.3",
    "css-hot-loader": "^1.4.4",
    "css-loader": "^4.2.1",
    "cssnano": "^4.1.10",
    "file-loader": "^6.0.0",
    "fork-ts-checker-webpack-plugin": "^5.1.0",
    "friendly-errors-webpack-plugin": "^1.7.0",
    "happypack": "^5.0.1",
    "html-webpack-plugin": "^4.3.0",
    "less": "^3.13.1",
    "less-loader": "^8.0.0",
    "mini-css-extract-plugin": "^0.10.0",
    "node-sass": "^4.14.1",
    "optimize-css-assets-webpack-plugin": "^5.0.4",
    "progress-bar-webpack-plugin": "^2.1.0",
    "react-hot-loader": "^4.12.21",
    "sass-loader": "^9.0.3",
    "style-loader": "^1.2.1",
    "ts-import-plugin": "^1.6.6",
    "ts-loader": "^8.0.2",
    "ts-node": "^9.0.0",
    "typescript": "^4.0.2",
    "url-loader": "^4.1.1",
    "vue-loader": "^15.9.8",
    "webpack": "^4.44.1",
    "webpack-bundle-analyzer": "^3.8.0",
    "webpack-cli": "^3.3.12",
    "webpack-dev-server": "^3.11.0",
    "webpack-merge": "^5.1.2",
    "webpack-node-externals": "^2.5.1",
    "webpack-parallel-uglify-plugin": "^1.1.2"
  },
  "devDependencies": {
    "@types/fork-ts-checker-webpack-plugin": "^0.4.5",
    "npm-check-updates": "^7.1.1"
  }

github.com/KangdiNi/Bu…

Viter