webpack基础

在 Webpack 眼中都是一个个模块,这样的好处是能清晰的描述出各个模块之间的依赖关系,以方便 Webpack 对模块进行组合和打包。 经过 Webpack 的处理,最终会输出浏览器能使用的静态资源

Posted by page on February 15, 2021

webpack

前言

webapck干什么?

依赖打包(主要),即根据资源依赖的分析后进行处理,输出结果,文件处理(次之)

为什么要对资源打包?

功能逐渐复杂推动引入大量其它类库,团队成员按照功能划分开发项目这两大现象;项目愈加需要合理组织代码,否则无法持续开发或维护;推动模块化开发,这时各种模块化规范用于规定模块独立的标准;

但多模块导致项目文件增加,导致http请求数增加;且随着模块数增加,依旧存在依赖关系逐渐复杂影响开发的问题;推动打包工具,代码在开发环境是按功能划分模块,开发体验良好;经过webpack分析模块依赖,打包输出为优化后的生产环境资源,优秀的项目;

My V-Cli

webpack4配置实践,使用webpack配置搭建一个多页面,符合实际项目开发并同时支持Vue模块,React模块的项目环境

准备

  1. 初始化npm
  2. 安装webpack与webpack-cli
    • 建议webpack安装到局部,防止不同项目依赖不同版本的 Webpack 而导致冲突
    • 使用webpack4,需要安装webpack-cli来执行命令
  3. 创建项目结构
    • build/ webpack配置
    • config/ 项目环境配置
    • dist/ 打包输出资源
    • src/ 模块资源
      • pages/ 项目页面
      • assets/ 项目依赖的静态资源
    • static/ 已被编译过的第3方静态资源
  4. 配置npm脚本命令
"dev": "webpack --config ./build/webpack.dev.config.js",

基础配置

在/src/pages/index下添加用于测试的模块,然后在webpack.base.conf.js和webpack.dev.conf.js添加基本配置,成功打包输出index页面

webpack.base.config.js

  1. 配置入口文件与输出
entry: './src/pages/index/index.js',
output: {
  path: path.resolve(__dirname,'../dist'),  //要求绝对路径
  filename: 'index.js'
}

webpack配置文件中,所有的相对路径配置值基于项目根目录

  1. 配置css模块加载
rules: [
  {
    test: /\.css$/,
    use: ['style-loader','css-loader']
  }
]
  1. 配置html-webpack-plugin插件
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    ...
    plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: './src/pages/index/index.html',
      templateParameters:{
        base: "htttp://192.168.4.43" // ejs模板语法的变量解析
      },
    }),
    new CleanWebpackPlugin()
  ]
}

将指定html文件作为模板,打包时输出引入了入口文件的html页面;

  1. 配置clean-webpack-plugin

    • 每次打包输出前清空上次打包输出目录,避免重复输出包
  2. 配置resolve

resolve: {
  alias: {
    '@assets': path.resolve(__dirname,'./src/assets'),
  }
}

resolve配置项用于定义webpack如何解析部分模块;

src项目模块之间引用路径过深,定义alias别名用于webpack解析路径;

配合url-loader支持webpack解析.html/.css文件中资源url

关于配置分离

实际开发中将webpack配置分为生产环境,开发环境;配置分离两种实现:

  • webpack-merge工具实现
  • 判断打包环境
--- package.json srcipts ---
  "scripts": {
    "dev": "webpack --config ./build/webpack.dev.config.js",
    "prod": "webpack --config ./build/webpack.prod.config.js --env NODE_ENV=production",
    "serve": "webpack serve --open --config ./build/webpack.dev.conf.js",
  }
--- webpack.dev.config.js ---
module.exports = function(env){
  const isProd = Object.is(env.NODE_ENV, 'production');
  return {
    ...
    isProd ? MiniCssExtractPlugin.loader : 'style-loader'
  }
}

webpack.dev.config.js

  1. 引入并使用webpack-merge模块
const {merge} = require('webpack-merge');
const baseconfig = require('./webpack.base.conf.js');
module.exports = merge(baseconfig,{
  mode: 'development'
});
  1. devtool(帮助控制台调试)
const isProd = Object.is(process.env.NODE_ENV, 'production');
module.exports = {
    ...
    devtool: isProd ? 'hidden-source-map' : 'cheap-module-eval-source-map'
}
  1. 开启devServer

    • 安装webpack-dev-server

    • 配置devServer

      --- webpack.base.conf.js ---
      devServer: {
        contentBase: './dist',
        inline: true,   // 开启热加载(刷新页面)
        hot: true,      // 开启热替换HMR,(替换页面部分)
        // ...其它配置,如本地代理
        proxy: {
            '/': {
                 target: 'http://localhost:3000',
                changeOrigin: true,
            },
        },
      }
      
    • 添加npm命令

      "serve": "webpack serve --open --config ./build/webpack.dev.conf.js",
      
    • 在开发环境下使用devServer,可以提供 HTTP 服务而不是使用本地文件预览项目;

    • 监听文件的变化并自动刷新网页,做到实时预览;

    • 支持 Source Map,以方便调试;(mode值自动为development)

    • hot热模块替换时,只能使用chunkhash,不能使用hash;不支持分离的模块;

其它loader

图片/字体模块加载

{
  test: /\.html/,
  use: 'html-loader'
},
{
  test: /\.(png|gif|jpe?g|eot|ttf|svg|woff2?)$/,
  use: {
    loader: 'url-loader',
    options: {
      limit: 1024 * 3,
      falllback: 'file-loader'
    }
  }
}
  • 图片模块
    • 加载css模块中会自动加载背景属性url引入的图片/字体文件
    • 设置html模块中img标签src属性来引入图片,需配置html-loader(html代码的alias别名路径前需加上’~’才能正确解析)
    • js模块中部分依赖图片模块的操作,需先引入图片模块

sass模块加载

  • 安装sass(编译sass),sass-loader
{
  test: /\.s[ac]ss$/,
  use: ['style-loader','css-loader','sass-loader'],
}

PostCSS

作为强大的CSS 处理工具,它通过插件机制可以灵活的扩展其支持的特性,如自动加前缀,支持下一代css语法;

  • 安装并配置postcss-loader
{
  test: /\.css$/,
  use: ['style-loader','css-loader','postcss-loader']
},
{
  test: /\.s[ac]ss$/,
  use: ['style-loader','css-loader','postcss-loader','sass-loader'],
}
  • 全局下新建postcss.config.js
module.exports = {
  plugins: [
    // require('postcss-cssnext'),
    require('autoprefixer')({       // 使用postcss插件前需安装插件
      overrideBrowserslist:[
          "defaults",
          "Android 4.1",
          "iOS 7.1",
          "Chrome>31",
          "ff>31",
          "ie>=8",
          "last 2 versions",
          ">0%"
        // 'last 2 version','>1%' 根据业务选择浏览器兼容
      ]
    })
  ]
}

babel转换

  • 安装babel三件套:babel-loader,@babel/core,@babel/preset-env
  • 配置babel-loader对js转换
{
  test: /\.js$/,
  exclude: /node_modules/,    // 必须限制转换目录
  use: 'babel-loader'
},
  • 预设与ployfill配置
--- babel.config.js ---
modules.export = {
  presets: [
    ["@babel/preset-env",
      { 
        modules: false,     // 转为es6
        targets: {          // 根据业务设置兼容列表
          "chrome": "58",
          "ie": "11"
        },
        useBuiltIns: "usage"    // 按需引入polyfill,需安装@babel/polyfill
        "corejs": 3,            // 安装corejs@3,并声明
      }
    ]
  ]
}

React与Vue支持

在’src/pages/’下新建分别使用了react与vue框架的两个页面目录,使用webpack配置使项目环境支持react和vue框架开发

React

  • 安装React框架基础:react react-dom
  • 安装并配置jsx语法转换预设:@babel/preset-react
--- babel.config.js ---
module.exports = {
  presets: [
    ["@babel/preset-env",
      { 
        targets: {
          "chrome": "58",
          "ie": "11"
        },
        useBuiltIns: "usage"
      }
    ],
    "@babel/preset-react"
  ]
}
  • 编写’src/pages/reactPage’的资源模块,如html模板,入口文件
  • webpack配置为多页面应用,即多个入口chunk分别对应输出的html页面
entry: {
  index: './src/pages/index/index.js',          // 首页入口文件
  reactPage: './src/pages/reactPage/index.js'   // react页面入口文件
  ...
  plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: './src/pages/index/index.html',
      chunks: ['index']                         // 输出HTML页面及其chunks模块
    }),
    new HtmlWebpackPlugin({
      filename: 'reactPage.html',
      template: './src/pages/reactPage/index.html',
      chunks: ['reactPage']
    }),
  ]
},
output: {
    path: path.resolve(__dirname,'../dist'),
    filename: '[name]-[chunkhash:8].js'
},

Vue

  • 安装vue框架基础:vue
  • 安装并配置.vue模块loader与模板编译:vue-loader,vue-template-compiler
const VueLoaderPlugin = require('vue-loader/lib/plugin');   //  插件支持
module.exports = {
    module: {
        rules: [
          {                     // vue-loader必须在rules规则第1位
            test: /\.vue$/,
            loader: 'vue-loader'
          }...
        ]
    }
}
  • 编写vue页面及入口文件main.js模块
  • webpack配置新页面VuePage的入口文件与html页面

多页面管理

入口配置与html模板配置

实际项目的多页面应用,页面数量是未知的,将所有页面都枚举在配置里显然是不合理的。根据项目的pages目录结构,定义getEntry()方法来遍历指定文件夹获取入口文件,getTemplate获取对应的html模板并配置到plugins;

const glob = require('glob');   // glob包用于匹配文件路径
function getEntry(){
    let entry = {};
    glob.sync('./src/pages/*/index.js')
    .forEach(filepath => {
        let chunkName = filepath.match(/pages\/(.+)\/index\.js/)[1];
        entry[chunkname] = filepath;
    });
    return entry;
}

let templates = [];
glob.sync('./src/pages/*/*.html')
.forEach(filepath => {
    filepath.match(/pages\/(.+)\/index\.html/);
    let name = RegExp.$1;
    templates.push(new HtmlWebpackPlugin({
        filename: name,
        template: filepath,
        chunks: [name]
    }))
});

module.exports = {
  entry: getEntry(),
  ...
  plugins: [...templates]
}

控制dist输出目录结构

  • 期待目录结构

    • dist
      • css
      • img
      • js
      • pages
      • index.html
      • favicon.ico
  • 配置

    • 配置output.filename将chunk打包至’dist/js’

      output: {
        path: path.resolve(__dirname,'../dist/'),
        filename: './js/[name]-[chunkhash:8].js'
      },
      
    • 配置url-loader的name参数将图片/字体模块打包至’dist/css’

      {
        test: /\.(png|gif|jpe?g|eot|ttf|svg|woff2?)$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 1024 * 3,
            falllback: 'file-loader',
            name: './css/[name]-[contenthash:8].[ext]'
          }
        }
      }
      
    • 配置html-webpack-plugin实例的filename,将非主页的html页面打包至’dist/pages/’

      new HtmlWebpackPlugin({
          filename: name == 'index.html' ? name : './pages/' + name,
          template: filepath,
          chunks: [name]
      })
      

webpack.prod.conf.js

分离css代码

开发环境使用style-loader对css文件进行处理后,css文件被作为模块也打包在了js文件中。实际生产环境,会对js文件和css文件分离;

  • 安装并配置mini-css-extract-plugin
--- webpack.base.config.js ---
    output: {
        publicPath: './',       // 该插件要求配置公共路径选项
        ...
    },
--- webpack.prod.config.js ---
module.exports = merge(baseconfig,{
  mode: 'production',
  devtool: false,
  module: {
    rules: [
      {
        test: /\.css$/,
        exclude: /node_modules/,
        use: [MiniCssExtractPlugin.loader,
        'css-loader','postcss-loader']
      },
      {
        test: /\.s[ac]ss$/,
        exclude: /node_modules/,
        use: [MiniCssExtractPlugin.loader,
        'css-loader','postcss-loader','sass-loader',
        ],
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
        filename: './css/[name].[contenthash:8].css'
    })
  ]
})

优化

缩小文件搜索范围

webpack从配置的entry触发,递归解析出导入的语句

  • 优化loader配置
    {
      test: /\.js$/,
      include: /src/,
      use: 'babel-loader'
    },

loader配置中使用test正则,include/exclude命中文件

  • 优化resolve.alias配置(以react为例)
--- webpack.dev.config.js ---
module.exports = {
  resolve: {
    alias: {
      // 开发环境使用包含检查警告的代码
      'react': path.resolve(__dirname, '../node_modules/react/dist/react.js'),
      // 'react': path.resolve(__dirname, '../node_modules/react/umd/react.development.js') // react16
    }
  },
};
--- webpack.prod.config.js ---
module.exports = {
  resolve: {
    alias: {
      'react': path.resolve(__dirname, '../node_modules/react/dist/react.min.js'), // react15
      // 'react': path.resolve(__dirname, '../node_modules/react/umd/react.production.min.js'), // react16
    }
  },
};

默认从入口文件 .’/node_modules/react/react.js’ 开始递归的解析和处理依赖的几十个文件。 通过配置 resolve.alias 可以让 Webpack 在处理 React 库时,直接使用单独完整的 react.min.js 文件,从而跳过耗时的递归解析操作。

  • 优化 module.noParse 配置
const path = require('path');

module.exports = {
  module: {
    // 忽略对 `react.xxx.min.js` 文件的递归解析处理
    noParse: [/react\.\w+\.min\.js$/],
  },
};

对某些独立完整文件(如上面的react.production.min.js)和为使用模块化规范的库不进行模块化编译(如jQuery和ChartJs)

Dllplugin动态链接库

HappyPack开启子线程

ParallelUglifyPlugin多线程压缩

--- webpack.prod.config.js ---
  const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
  plugins: [
    ...
    new ParallelUglifyPlugin({
      // 传递给 UglifyJS 的参数
      uglifyJS: {
        output: {
          // 最紧凑的输出
          beautify: false,
          // 删除所有的注释
          comments: false,
        },
        compress: {
          // 删除所有的 `console` 语句,可以兼容ie浏览器
          drop_console: true,
          // 内嵌定义了但是只用到一次的变量
          collapse_vars: true,
          // 提取出出现多次但是没有定义成变量去引用的静态值
          reduce_vars: true,
        }
      },
    }),
  ]

CSS压缩

---- webpack.base.config.js ---
  const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
  optimization: {
    minimize: true,
    minimizer: [
      new CssMinimizerPlugin(),
    ],
  }

区分环境

在项目中需要根据当前环境不同设置不同处理,例如请求地址;借助 DefinePlugin插件定义编译时全局变量,在代码中判断变量值进行不同处理

--- webpack.base.config.js ---
    const webpack = require('webpack');
    plugins: [
      ...
      new webpack.DefinePlugin({
        // 声明全局变量process.env.NODE_ENV
        'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
      })
    ]
--- index.js ---
    console.log(process.env.NODE_ENV);

Tree Shaking摇树

Tree Shaking自动去掉没有使用的代码,仅对ES6引入的模块检测

optimization: {
    usedExports: true,
    "sideEffects":false,    
}

提取公共代码

  • 将多个页面公共的代码抽离成单独的文件,项目公共代码分为两种:

    • 基础库,如react,vue,一般不会更新(base.js)
    • 业务公共业务代码,常更新(common.js)
  • 提取公共代码

optimization: {
  splitChunks:{
      cacheGroups: {
        common: {
          name: 'common',   // 输出名
          chunks: 'initial', // 检索的chunk
          priority: 2,        // 规则权重
          minChunks: 2,     // 被引用2次以上提取
        },
        // 进一步提取base基础库
        reactBase: {    
          name: 'reactBase',
          test: (module) => {   // chunk名称符合时再次提取
              return /react|redux|prop-types/.test(module.context);
          },
          chunks: 'initial',
          priority: 10,
        }
      }
  }
}