webpack4React16项目构建二

11次阅读

共计 10332 个字符,预计需要花费 26 分钟才能阅读完成。

前言

之前一段时间工作原因把精力都放在小程序上, 趁现在有点空闲时间, 刚好官方文档也补充完整了, 我准备重温一下 webpack 之路了, 因为官方文档已经写得非常详细, 我会大量引用原文描述, 主要重点放在怎么从零构建 webpack4 代码上, 这不是一个系统的教程, 而是从零摸索一步步搭建起来的笔记, 所以前期可能 bug 会后续发现继续修复而不是修改文章.

系列文章

webpack4 从零开始构建(一)

基本已经可以使用的完整配置 webpack4_demo

PS:

2018/12/12 修改细节布局
2018/12/26 上传, 代码同步到第四篇文章
2019/03/14 上传, 补充代码到第二篇文章

引入 React

首先安装 React 环境库

yarn add react react-dom react-router-dom

react 和 react-dom 的区别

  • react 是核心代码, 只包含了定义组件的方法如 React.createElement,.createClass,.Component,.children 以及其他元素和组件类。
  • react-dom 是渲染代码, 包括 ReactDOM.render,.unmountComponentAtNode 和.findDOMNode 等实现将虚拟 DOM 渲染到界面

react-router 和 react-router-dom 的区别

  • react-router: 实现了路由的核心功能
  • react-router-dom: 基于 react-router,加入了在浏览器运行环境下的一些功能,例如:Link 组件,会渲染一个 a 标签,Link 组件源码 a 标签行; BrowserRouter 和 HashRouter 组件,前者使用 pushState 和 popState 事件构建路由,后者使用 window.location.hash 和 hashchange 事件构建路由。
  • react-router-native: 基于 react-router,类似 react-router-dom,加入了 react-native 运行环境下的一些功能。

开始使用

修改 index.html 如下

<!doctype html>
<html>

<head>
  <title>webpack + React</title>
</head>

<body>
  <div id="root"></div>
</body>

</html>

修改 index.js, 引用 React 语法写个简单例子

import React from "react";
import ReactDOM from "react-dom";

ReactDOM.render(<div>Hello world</div>, document.getElementById("root"));

到此还没结束, 我们还需要安装下面 babel 依赖配置 webpack 环境才能正常打包运行

yarn add babel-core babel-loader@7 babel-preset-env babel-preset-react

粗略讲解一下各个依赖干嘛的

  • babel-core 是作为 babel 的核心, 把 javascript 代码分析成 AST (抽象语法树, 是源代码的抽象语法结构的树状表现形式),方便各个插件分析语法进行相应的处理
  • babel-loader 也是核心插件, 允许使用 Babel 和 webpack 转换 JavaScript 文件
  • babel-preset-env 基于你的实际浏览器及运行环境,自动的确定 babel 插件及 polyfills,转译 ES2015 及此版本以上的语言
  • babel-preset-react 编译 react 代码

注意: 因为 babel-core@7+ 还不稳定, 所以默认安装 @6+, 需要 babel-loader@7+ 才能运行, 所以上面指定了版本

根目录新增 .babelrc 配置文件,babel 所有的操作基本都会来读取这个配置文件,如果没有这个配置文件,会从package.json 文件的 babel 属性中读取配置。

{
    "presets": [
        ["env", {modules: false}], "react"
    ]
}

注意: 因为 Tree Shaking 这个功能是基于 ES6 modules 的静态特性检测,来找出未使用的代码,所以如果你使用了 babel 插件的时候,如:babel-preset-env,它默认会将模块打包成 commonjs,这样就会让 Tree Shaking 失效了, 所以我们要设置一下关闭 Babel 的模块转换功能,保留原本的 ES6 模块化语法。

我们还要去 webpack.common.js 配置 loader, 完整配置如下

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const CleanWebpackPlugin = require("clean-webpack-plugin");

module.exports = {
  // 入口
  entry: "./src/index.js",
  // 输出
  output: {
    // 打包文件名
    filename: "[name].bundle.js",
    // 输出路径
    path: path.resolve(__dirname, "dist"),
    // 资源请求路径
    publicPath: ""
  },
  module: {
    rules: [
      {test: /\.(js|jsx)$/, // 匹配文件
        exclude: /node_modules/, // 过滤文件夹
        use: {loader: "babel-loader"}
      },
      {test: /\.(css|scss)$/, // 匹配文件
        use: [
          "style-loader", // 使用 <style> 将 css-loader 内部样式注入到我们的 HTML 页面
          "css-loader", // 加载.css 文件将其转换为 JS 模块
          "sass-loader" // 加载 SASS / SCSS 文件并将其编译为 CSS
        ]
      },
      {test: /\.(png|svg|jpg|jpeg|gif)$/, // 图片处理
        use: ["file-loader"]
      },
      {test: /\.(woff|woff2|eot|ttf|otf)$/, // 字体处理
        use: ["file-loader"]
      },
      {
        test: /\.xml$/, // 文件处理
        use: ["xml-loader"]
      },
      {
        test: /\.html$/, // 处理 html 资源如图片
        use: ["html-loader"]
      }
    ]
  },
  plugins: [
    // 清除文件
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      // title
      title: "test",
      // 模板
      template: "index.html"
    })
  ]
};

继续使用上一章配置过命令和当前依赖文件 package.json, 完整代码如下:

{
  "sideEffects": false,
  "scripts": {
    "dev": "webpack --config webpack.dev.js",
    "build": "webpack --config webpack.prod.js",
    "start": "webpack-dev-server --config webpack.server.js"
  },
  "dependencies": {
    "babel-core": "^6.26.3",
    "babel-loader": "7",
    "babel-preset-env": "^1.7.0",
    "babel-preset-react": "^6.24.1",
    "clean-webpack-plugin": "^2.0.0",
    "css-loader": "^2.1.1",
    "file-loader": "^3.0.1",
    "html-loader": "^0.5.5",
    "html-webpack-plugin": "^3.2.0",
    "node-sass": "^4.11.0",
    "react": "^16.8.4",
    "react-dom": "^16.8.4",
    "react-router-dom": "^4.3.1",
    "sass-loader": "^7.1.0",
    "style-loader": "^0.23.1",
    "url-loader": "^1.1.2",
    "webpack": "^4.29.6",
    "webpack-bundle-analyzer": "^3.1.0",
    "webpack-cli": "^3.2.3",
    "webpack-dev-server": "^3.2.1",
    "webpack-merge": "^4.2.1",
    "xml-loader": "^1.2.1"
  },
  "name": "webpack_demo",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT"
}

打开终端执行命令

npm run dev

运行 dist 目录下的 index.html 文件可以查看效果

项目拓展

接下来继续展开代码, 按照正常项目开发使用 ES6 方式开发和引用资源处理, 首先我们分门别类区分一下资源
index.js 修改如下:

import React from "react";
import ReactDOM from "react-dom";
import Main from "./page/main";

ReactDOM.render(<Main />, document.getElementById("root"));

新增 main.js 文件代码如下:

import React, {Component, Fragment} from "react";
import ReactDOM from "react-dom";
import "../style/style.scss";

export default class Main extends Component {constructor(props, context) {super(props, context);
    this.state = {title: "Hello World!"};
  }
  // 挂载前
  componentWillMount() {console.log("componentWillMount");
  }
  // 挂载后
  componentDidMount() {console.log("componentDidMount");
  }
  // 接受新 props
  componentWillReceiveProps(nextProps) {console.log("componentWillReceiveProps", nextProps);
  }
  // 是否重新渲染
  shouldComponentUpdate(nextProps, nextState) {console.log("shouldComponentUpdate", nextProps, nextState);
  }
  // 更新前
  componentWillUpdate(nextProps, nextState) {console.log("componentWillUpdate", nextProps, nextState);
  }
  // 更新后
  componentDidUpdate(prevProps, prevState) {console.log("componentDidUpdate", nextProps, nextState);
  }
  // 卸载前
  componentWillUnmount() {console.log("componentWillUnmount");
  }
  // 捕捉错误
  componentDidCatch() {console.log("componentDidCatch");
  }
  render() {
    return (
      <Fragment>
        <img className="img1" src={require("../img/1.jpg")} alt="" />
        <div className="img2" />
        <p>{this.state.title}</p>
      </Fragment>
    );
  }
}

在 jsx 里相对路径的图片不会被 file-loader 和 url-loader 处理, 所以我们使用这种写法引入比较方便

<img className="img1" src={require("../img/1.jpg")} alt="" />

style.scss 如下:

html {
  background-color: #666;

  p {color: red;}

  .img1,
  .img2 {
    width: 250px;
    height: 400px;
  }

  .img2 {background: url("../img/2.jpg") no-repeat center center;
    background-size: cover;
  }
}

修改一下 webpack.common.js 图片处理, 使用 url-loader 将 50kb 内的图片转成 base64 编码保存进代码减少请求, 不符合条件的打包图片放到一个单独文件夹 img, 因为 url-loader 内置有 file-loader, 所以我们不必要再引入

yarn add image-webpack-loader
{test: /\.(html)$/,
  use: {
    loader: "html-loader",
    options: {attrs: ["img:src", "img:data-src", "audio:src"],
      minimize: true
    }
  }
},
{test: /\.(png|svg|jpg|jpeg|gif)$/i, // 图片处理
  use: [
    {
      loader: "url-loader",
      options: {name: "[name].[hash:5].[ext]",
        limit: 50 * 1024, // size <= 50kb
        outputPath: "img"
      }
    }
  ]
},

重新执行命令

npm run dev

打开页面会看到 1.jpg 变成 base64 代码, 一切都在预期内.

使用路由

根目录新增 router 文件夹, 里面创建 index.js, 代码如下:

import React, {Component, Fragment} from "react";
import Main from "../page/main";

class App extends Component {render() {
    return (
      <Fragment>
        <Main />
      </Fragment>
    );
  }
}

然后收拾一下 main.js 页面, 把多余生命周期清除掉

import React, {Component, Fragment} from "react";
import {Switch, Route, Redirect, Link} from "react-router-dom";
import View1 from "../component/view1";
import View2 from "../component/view2";
import "../style/style.scss";

export default class Main extends Component {constructor(props, context) {super(props, context);
    this.state = {title: "Hello World!"};
  }

  render() {
    return (
      <Fragment>
        <p>{this.state.title}</p>
        <Link to="/view1/">View1</Link>
        <Link to="/view2/">View2</Link>
        <Switch>
          <Route exact path="/" component={View1} />
          <Route path="/view1/" component={View1} />
          <Route path="/view2/" component={View2} />
          <Redirect to="/" />
        </Switch>
      </Fragment>
    );
  }
}

分别新增 page1.jspage2.js,main.js的图片分别迁移进去新目录component

import React, {Fragment} from "react";

export default () => {
  return (
    <Fragment>
      <p>Page1</p>
      <img className="img1" src={require("../img/1.jpg")} alt="" />
    </Fragment>
  );
};
import React, {Fragment} from "react";

export default () => {
  return (
    <Fragment>
      <p>Page2</p>
      <div className="img2" />
    </Fragment>
  );
};

最后 src 目录下的 index.js 修改如下:

import React from "react";
import ReactDOM from "react-dom";
import {HashRouter} from "react-router-dom";
import Main from "./page/main";

ReactDOM.render(
  <HashRouter>
    <Main />
  </HashRouter>,
  document.getElementById("root")
);

现在整个目录结构如下

执行命令

npm run dev

一个简单的路由切换页面就完成了, 界面大概如下

图片压缩

上面我们只是将小于 50kb 的图片内嵌进代码里, 超过 50kb 的图片我们可以引入插件作处理

yarn add image-webpack-loader

然后我们在修改一下 loader 配置

{test: /\.(png|svg|jpe?g|gif)$/i, // 图片处理
  use: [
    {
      loader: "url-loader",
      options: {name: "[name].[hash:5].[ext]",
        limit: 20 * 1024, // size <= 50kb
        outputPath: "img"
      }
    },
    {
      loader: "image-webpack-loader",
      options: {
        // Compress JPEG images
        mozjpeg: {
          progressive: true,
          quality: 65
        },
        // Compress PNG images
        optipng: {enabled: false},
        //  Compress PNG images
        pngquant: {
          quality: "65-90",
          speed: 4
        },
        // Compress GIF images
        gifsicle: {interlaced: false},
        // Compress JPG & PNG images into WEBP
        webp: {quality: 75}
      }
    }
  ]
},

注意顺序, 这种写法会先经过压缩之后再有 url-loader 作处理, 能够让部分原本不符合大小的图片压缩之后就满足转码 base64 了, 为了突出效果限制到 20kb 内.
以我的测试图为例, 压缩率达到

80.4kb -> 45.9kb

至于其他图片配置可根据自己需求修改

解析(resolve)

随着文件越来越多, 引用路径越来越复杂, 会容易让人混乱, 我们可以使用 resolve 做些依赖处理, 这些选项能设置模块如何被解析
webpack.common.js新增下面配置代码, 设置简化路径

resolve: {
  // 创建 import 或 require 的别名,来确保模块引入变得更简单
  alias: {"@": path.resolve(__dirname, "src/"),
    IMG: path.resolve(__dirname, "src/img"),
    STYLE: path.resolve(__dirname, "src/style"),
    JS: path.resolve(__dirname, "src/js"),
    ROUTER: path.resolve(__dirname, "src/router"),
    PAGE: path.resolve(__dirname, "src/page"),
    CMT: path.resolve(__dirname, "src/component")
  }
}

然后我们就可以修改对应的文件引入模块写法, 例如

import "STYLE/style.scss";

其他可自行修改

打包文件性能可视化

目前基本搭建完了, 然后我们就可以利用一款检测打包性能的插件找到可优化空间

yarn add webpack-bundle-analyzer

在 webpack.server.js 新增依赖

const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
.BundleAnalyzerPlugin;

plugins 里初始化方法

new BundleAnalyzerPlugin()

执行命令会自动打开页面 http://127.0.0.1:8888/, 这里可以看到性能图, 不影响原本的 http://localhost:9000/#/ 查看项目

npm run start

CSS 优化

webpack4 使用插件和之前版本不一样, 我们安装以下依赖

yarn add mini-css-extract-plugin

修改一下 webpack.common.js 的配置

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
-------------------------- 省略 -----------------------------------
{
  test: /\.scss$/, // 匹配文件
  use: [
    {
      loader: MiniCssExtractPlugin.loader,
      options: {
        // you can specify a publicPath here
        // by default it use publicPath in webpackOptions.output
        publicPath: "../"
      }
    },
    // "style-loader", // 使用 <style> 将 css-loader 内部样式注入到我们的 HTML 页面
    "css-loader", // 加载.css 文件将其转换为 JS 模块
    "sass-loader" // 加载 SASS / SCSS 文件并将其编译为 CSS
  ]
},
--------------------------- 省略 ------------------------------------
plugins: [
  // 提取样式文件
  new MiniCssExtractPlugin({
    // Options similar to the same options in webpackOptions.output
    // both options are optional
    filename: "style/[name].[chunkhash:8].css",
    chunkFilename: "style/[id].css"
  })
],

执行命令之后就看到有新的样式文件独立出来了

npm run dev

提取公共库

从 webpack4 开始官方移除了 commonchunk 插件,改用了 optimization 属性进行更加灵活的配置, 再生产环境下回自动开启, 只需要设置

mode: "production"

从文档来看它的默认设置是这样的

New chunk can be shared OR modules are from the node_modules folder
New chunk would be bigger than 30kb (before min+gz)
Maximum number of parallel requests when loading chunks on demand would be lower or equal to 5
Maximum number of parallel requests at initial page load would be lower or equal to 3

新 chunk 是能够被共享或者来自 node_modules 文件
新 chunk 在 min+gz 压缩之前大于 30kb
按需加载的并行请求数小于等于 5
首屏渲染的最大并行请求数小于等于 3

因为现在 demo 比较小, 没什么好讲解的, 一般根据项目情况调整一下拆分机制就好了, 假如我想要把 node_modules 和组件代码拆分出来, 可以这么写

module.exports = merge(common, {
  optimization: {
    splitChunks: {// 表示显示块的范围,有三个可选值:initial(初始块)、async(按需加载块)、all(全部块)
      chunks: "all",
      cacheGroups: {
        libs: {
          // 优先级高于其他就不会被打包进其他 chunk, 如果想匹配自己定义的拆分规则,则 priority 需要设置为正数,优先匹配默认拆分规则就设置为负数。priority: 10,
          test: /[\\/]node_modules[\\/]/,
          name: "chunk-libs",
          chunks: "initial"
        },
        commons: {
          // 优先级高于其他就不会被打包进其他 chunk, 如果想匹配自己定义的拆分规则,则 priority 需要设置为正数,优先匹配默认拆分规则就设置为负数。priority: 15,
          test: path.resolve(__dirname, "src/component"),
          name: "chunk-commons",
          // 最小共用次数
          minChunks: 2, 
          // 如果当前 chunk 已经被打包进其他 chunk 的时候就不再打包, 而是复用其他 chunk
          reuseExistingChunk: true
        }
      }
    }
  }
});

正文完
 0