美文网首页
react 项目 webpack 配置(开发篇)

react 项目 webpack 配置(开发篇)

作者: Jellal_ | 来源:发表于2020-05-21 15:48 被阅读0次

目录结构

dir.png

paths 配置

// path 与 fs 是 node 环境自带不需要通过 npm 安装
const path = require('path')
const fs = require('fs')

// 获取项目根目录路径
const appDirectory = fs.realpathSync(path.resolve(__dirname, '../'))
// 获取目标文件的函数
const resolveApp = relativePath => path.resolve(appDirectory, relativePath)

module.exports = {
  appPath: resolveApp('.'),
  appScript: resolveApp('script'),
  appSrc: resolveApp('src'),
  appDist: resolveApp('dist'),
  appPublic: resolveApp('public'),
  appIndexJs: resolveApp('src/index.js'),
  appIndexHtml: resolveApp('public/index.html')
}

实现最基本的打包功能

// webpack.dev.js

const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const paths = require('../paths')

const config = {
  // mode 可选参数 development | production | none 
  // 开启 webpack 开发环境下内置插件优化
  mode: 'development',
  // 主入口文件 
  // webpack 会根据主入口文件中的 import 或者 require 的语句 先解析推断入口文件所依赖资源模块 然后再去解析每个资源模块的依赖 最终形成一个依赖关系树
  entry: paths.appIndexJs,
  // 输出目录
  // 开发环境下并不会产生 dist 到磁盘目录 而是存储在内存中
  output: {
    // 编译后的的资源引用地址的前缀都会加上 publicPath
    // webpack-dev-server 会在 publicPath 下启用服务来访问 webpack 输出的文件 
    publicPath: '/',
    // 输出目录路径
    path: paths.appDist,
    // 最终输出的文件名 
    filename: 'js/[name].bundle.js',
  },
resolve: {
    // 模块引入如果不加后缀名 根据以下后缀名顺序查找
    extensions: ['.js', '.jsx']
  },
  // 模块配置
  // webpack 会循环遍历上面提到的依赖树 根据下面 rule 的配置 找到对应的模块资源文件 交给对应的 loader 加载 最终把加载的结果打包到 bundle.js 中
  module: {
    rules: [
      {
        // 匹配后缀为 .js|.jsx 的模块
        test: /\.jsx?$/,
        // 需要排除目录 接受正则匹配与绝对路径
        exclude: /node_modules/,
        use: [
          {
            // 通过 babel 对源代码中的 后缀为 .js|.jsx的语法 进行转换(转换配置可写在根目录下的 .bablrc文件中)
            loader: 'babel-loader',
            options: {
              // 开启 babel-loader 加载结果的缓存
              // 指定的目录将用来缓存 loader 的加载结果 设置空值或者 true 的话 使用默认缓存目录 node_modules/.cache/babel-loader
              cacheDirectory: true
            }
          }
        ],
      },
      // 匹配后缀名为 .less 的模块
      {
        test: /\.less/,
        // loader 加载资源模块 类似工作管道 可以使用多个 loader 加载同一个资源模块 最终返回一段标准的 js 代码字符串(webpack 默认只支持 js 的语法)
        // 多个 loader 执行顺序是从后往前执行
        use: [
          // 把 css-loader 的处理结果 通过 style 标签 添加到 html 页面
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              // 此处开启 在他前面的所有 loader 都必须开启
              sourceMap: true,
              // 在当前 loader 加载之前的 loader 数量
              importLoaders: 1
            }
          },
          {
            // 把 .less 模块的内容加载成 css
            loader: 'less-loader',
            options: {
              // 开启源码地图(可理解成源码中的错误定位)
              sourceMap: true
            }
          }
        ]
      },
      // 匹配图片资源模块
      {
        test: /\.(png|jpg|jpeg|gif)/,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '[name].[ext]',
              // 打包到 ouput.path + images 目录下 
              outputPath: 'images',
            }
          }
        ]
      }
    ]
  },
  // 开发服务 这里使用的是 webpack-dev-server
  devServer: {
    // 页面刷新出现 404 重新定向到 output.path 下的index.html
    historyApiFallback: true,
    // 告诉服务器从哪个目录中提供内容
    contentBase: paths.appDist,
    // 服务端口
    port: 8080,
    // webpack 打包完成 自动打开浏览器
    open: true,
  },
  plugins: [
    // 首次打包的时候清除 output.path 目录中所有的文件 保留 ouput.path 文件夹 并且再每次 rebuild 的时候会清除所有不再被使用的文件
    new CleanWebpackPlugin(),
    // 自动生成 html 页面 并把打包后的资源内容 自动添加到页面上
    new HtmlWebpackPlugin({
      // 指定 html 模板路径
      template: paths.appIndexHtml,
      // 打包后的文件名
      filename: 'index.html'
    })
  ],
}

module.exports = config

.babelrc 基本配置

@babel/preset-env

在 babel 中 preset 表示 plugin 的集合, @babel/preset-env 可以让 babel 根据我们配置的 Browserslist 按需转换语法和添加 polyfill
配置 Browserslist)

Browserslist

配置有三种方式 并按下面的优先级使用:

    1. @babel/preset-env options.targets
    2. package.json 中的 browserslist
    3. .browserslistrc 配置文件

--useBuiltIns(false | entry | usage)

默认值为false babel 只对新的语法如 class|箭头函数进行语法转换 对于 Object.assign|Promise|Set| Proxy 等新的 API 不进行转换 保留原样输出 如需使用 则需要添加 polyfill

false
// .babelrc
{
  presets: [
    [
      "@babel/preset-env",
      {
         "useBuiltIns": false
      }
    ]
  ]
}

此时不对 polyfill 做操作 如果引入 @babel/polyfill 则无视 Browserslist 配置的 引入所有的 polyfill

entry
// .babelrc
{
  presets: [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "entry",
        "corejs": 3
      }
    ]
  ]
}

根据 Browserslist 配置 引入浏览器不兼容的 polyfill 需要在入口文件手动添加
import 'core-js/stable' & import 'regenerator-runtime/runtime'

usage
// .babelrc
{
  presets: [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ]
}

根据 Browserslist 的配置,以及你代码中用到的 API 来自动添加 polyfill,实现了按需添加

@babel/plugin-transform-plugin

通过 babel 转换后的代码 会有有一些辅助代码内嵌在模块当中 如果是多个模块的话 就会有大量的重复代码 这个时候 就需要通过 @babel/plugin-transform-runtime 插件复用 babel 转换后的辅助函数

@babel/preset-react

支持 react 语法转换的插件集合

@babel/plugin-proposal-decorators

支持 装饰器语法 主要用于 react 的高阶组件

@babel/plugin-proposal-class-properties

支持类内部属性直接赋值

.babelrc

// .babelrc
{
  presets: [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ],
    "@babel/preset-react"
  ],
  plugins: [
    "@babel/plugin-transform-plugin",
    [
      "@babel/plugin-proposal-decorators",
      {
        "legacy": true
      }
    ],
    [
      "@babel/plugin-proposal-class-properties",
      {
       "loose": true
      }
    ]
  ]
}

后续还会加上 react-hot-loader(热模块更新的配置) babel-plugin-import 集成 antd 按需加载的配置

package.json

{
  ...
  "script": {
    "start": "webpack-dev-server --config script/config/webpack.dev.js"
  }
}

至此一个简配版的 webpack 配置完成 运行 npm start 就可以启动项目了

打包动态链接库(DLLPlugin&DLLReference)

根据简配版的配置 我们会把第三方依赖也打包进我们的 bundle.js 这样会导致我们最终的 bundle.js 体积过大 这时候我们就该考虑把这些第三方依赖单独拆分出来

DllPlugin 可以帮助我们把不需要频繁更新的第三方依赖进行编译 单独打包成一个 bundle 并输出一份依赖映射文件 manifest.json
DllReferencePlugin 解析 manifest.json 映射文件 就可以引用我们打包后的依赖了

需要在 script/config 目录下重新新建一个 webpack.config.dll.js 的文件 专门用来打包动态链接库

// webpack.config.dll.js
const webpack = require('webpack')
const paths = require('../paths')

const config = {
  mode: 'production',
  entry: {
    react: ['react', 'react-dom']
  },
  output: {
    path: paths.appDist,
    filename: 'dll/[name].dll.[contenthash:8].bundle.js',
    // 暴露给外部使用的库的名称
    library: '[name]_dll'
  },
  plugins: [
    new webpack.DllPlugin({
      // 就是我们需要引用库的名称 (这里必须与 library 一致)
      name: '[name]_dll',
      path: `${paths.appDist}/dll/manifest.json`
    })
  ]
}

module.exports = config

在 package.json scripts 中增加脚本命令

{
  ....,
  "scripts": {
    ...,
    "build:dll": "webpack --config script/config/webpack.config.dll.js"
  }
}

执行 npm run build:dll,可以看到 dist 目录如增加了 dll 目录,之所以将动态链接库单独放在 dll 目录下,主要是为了使用 CleanWebpackPlugin 更为方便的过滤掉动态链接库。

修改 webpack.dev.js 中的配置

{
  ...,
  plugins: [
    cleanOnceBeforeBuildPatterns: ['**/*', '!dll', '!dll/**'], //不删除dll目录
    new webpack.DllReferencePlugin({
      manifest: `${paths.appDist}/dll/manifest.json`
    })
  ]
}

public/index.html 添加 script 标签引用我们打包的动态链接库

<script src="./dll/react.dll.cb13c2ef.bundle.js"></script>

抽离公共代码

抽离公共代码是对于多页应用来说的(即多入口配置),如果多个页面引入了一些公共模块,那么可以把这些公共的模块抽离出来,单独打包。公共代码只需要下载一次就缓存起来了,避免了重复下载。

// webpack.dev.js
{
  ...,
  optimization: {
    splitChunks: {
      // 可选参数 initial|async|all
      // initial 入口引入 对于异步引入的模块不抽离
      // async 异步引入 只对异步引入的模块做抽离
      // all 包含以上两种
      chunks: "all",
      cacheGroups: {
        // 第三方依赖提取
        vendor: {
          name: 'vendor',  // chunk 名称
          priority: 1,  // 优先级 根据优先级高的优先抽离
          test: /node_modules/, // 匹配 node_modules 下引用的模块
          minSize: 0, // 模块的最小体积规则 高于这个做抽离
          minChunks: 1  // 模块最少引用次数
        },
        // 公用模块提取
        common: {
          name: 'common',
          priority: 0,
          minSize: 0,
          minChunks: 2
        }
      }
    }
  }
}

HMR(热模块跟新)

我们不是使用了 webpack-dev-server 可以自动刷新了吗?为什么还需要热模块更新呢?举个🌰 比如我们页面当中有个表单我们把数据都填好了 但是我们发现 提交按钮没加 我们去代码里加一下提交按钮组件 保存代码 回到浏览器 此时页面状态被刷新了 所有填写的数据都没了 你有需要重新填写一遍
如果使用的是 HMR 就可以实现只将修改的模块实时替换至应用中 不必完全刷新整个应用了

// webpack.dev.js
{
  devServer: {
    ...,
    hot: true
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
}

还需要安装 react-hot-loader

npm i react-hot-loader -D
// .babelrc
{
 "plugins": [
    ...,
    "react-hot-loader/babel"
  ]
}
// webpack.dev.js
{
  ...,
  entry: ['react-hot-loader/patch', paths.appIndexJs]
}
// App.js
import { hot } from 'react-hot-loader/root'

...

export default module.hot ? hot(App) : App

module.hot 是根据你 webpack 是否开启了 devServer.hot

好了 Bye Bye

相关文章

网友评论

      本文标题:react 项目 webpack 配置(开发篇)

      本文链接:https://www.haomeiwen.com/subject/qlsgohtx.html