webpack4+React16項目構建(二)

前言

以前一段時間工做緣由把精力都放在小程序上,趁如今有點空閒時間,恰好官方文檔也補充完整了,我準備重溫一下 webpack 之路了,由於官方文檔已經寫得很是詳細,我會大量引用原文描述,主要重點放在怎麼從零構建 webpack4 代碼上,這不是一個系統的教程,而是從零摸索一步步搭建起來的筆記,因此前期可能bug會後續發現繼續修復而不是修改文章.javascript

系列文章

webpack4從零開始構建(一)
webpack4+React16項目構建(二)
webpack4功能配置劃分細化(三)
webpack4引入Ant Design和Typescript(四)
webpack4代碼去重,簡化信息和構建優化(五)
webpack4配置Vue版腳手架(六)css

基本已經可使用的完整配置webpack4_demohtml

PS: java

2018/12/12 修改細節佈局
2018/12/26上傳,代碼同步到第四篇文章
2019/03/14上傳,補充代碼到第二篇文章node

引入React

首先安裝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以下webpack

<!doctype html>
<html>

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

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

</html>

修改index.js,引用React語法寫個簡單例子git

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

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

到此還沒結束,咱們還須要安裝下面babel依賴配置webpack環境才能正常打包運行github

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

粗略講解一下各個依賴幹嗎的web

  • 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
        }
      }
    }
  }
});
相關文章
相關標籤/搜索