web 應用程序的工程構建清單

說明

  1. 項目主要探討並記錄 web 工程體系搭建的主要過程。( 脫離框架提供的 cli 從基礎開始構建一個工程上還算完整的 web 應用 )
  2. 現代 Web 工程主要是三大框架 React、Vue、Angular。項目選擇 React。
  3. 爲何選擇 React ?緣由是工做中應用的是 Vue ;選擇什麼,對本項目來講不是很重要,重要的是論述好工程搭建過程
  4. 論述角度:
    • 版本管理(git)
    • npm包管理
    • webpack(構建開發環境、及打包線上資源)
    • 代碼規範質量(eslint、stylint、prettier)
    • 單元測試(UI組件、私有工具函數「jest」)
    • 工程目錄組織
    • 先後端分離(MOCK、json-server)
    • 客服端與服務端交互(axios)
    • 組件樣式書寫規範
    • 組件分離

開始吧

1. 版本管理(git

$ 如何安裝 ?

# https://git-scm.com/downloads

$ 如何使用 ?

# https://git-scm.com/doc || git --help
複製代碼

⬆ back to topjavascript

2. Create a local .gitignore

有時,有些文件不但願 Git 簽入 GitHub.gitignore 配置文件能夠告訴 Git 忽略哪些文件。css

$ touch .gitignore # starter/.gitignore

# dependencies
/node_modules

# testing
/coverage

# production
/build
dist

# misc
.DS_Store

# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Editor directories and files
.vscode
複製代碼

⬆ back to tophtml

3. nodenpmyarn

$ node 是什麼 ?

# http://nodejs.cn/

$ 如何安裝 ?

# http://nodejs.cn/download/

$ npm 是什麼 ?

# https://docs.npmjs.com/about-npm/

$ npm 如何使用 ?

# 安裝 Node.js 時附帶安裝了 npm || npm -v

$ 建立包管理配置文件 package.json

# https://docs.npmjs.com/creating-a-package-json-file

$ package.json 文件中的要求 ?

# https://docs.npmjs.com/files/package.json.html

$ package-lock.json 是什麼 ?

# https://docs.npmjs.com/files/package-lock.json.html

$ yarn 是什麼?

# https://yarn.bootcss.com/

$ yarn 如何安裝 ?

# https://yarn.bootcss.com/docs/install/#mac-stable

$ yarn 如何使用 ?

# https://yarn.bootcss.com/docs/

$ 初始化項目

# mkdir starter && npm init
複製代碼

⬆ back to top前端

初始工程目錄package.json 的信息 ✅

工程目錄vue

└── starter
    ├── README.md
    └── package.json
複製代碼

package.jsonjava

{
  "name": "starter",
  "version": "1.0.0",
  "description": "List of engineering builds for web applications",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/cllemon/starter.git"
  },
  "keywords": [
    "javascript",
    "typescript",
    "react"
  ],
  "author": "cllemon",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/cllemon/starter/issues"
  },
  "homepage": "https://github.com/cllemon/starter#readme"
}
複製代碼

注意:如下描述中全部的包安裝都採用 yarn 命令node

⬆ back to topreact

4. editorconfig

EditorConfig 能夠幫助開發者在不一樣的編輯器和 IDE 之間定義和維護一致的代碼風格。webpack

EditorConfig is awesome: editorconfig.orgios

$ touch .editoorconfig

# starter/.editoorconfig

root = true                       # 代表是最頂層的配置文件,發現設爲 true 時,纔會中止查找.editorconfig 文件。

[*]
charset = utf-8
indent_style = space              # tab 爲 hard-tabs,space 爲 soft-tabs。
indent_size = 2                   # 規定每級縮進的列數和 soft-tabs 的寬度(空格數)。若是設定爲 tab,則會使用 tab_width 的值。
end_of_line = lf                  # 定義換行符,支持 lf(UNIX/Linux採用換行符 LF 表示下一行)、cr(MAC OS系統)則採用回車符 CR 表示下一行) 和 crlf。
insert_final_newline = true       # 設爲 true 代表使文件以一個空白行結尾,false 反之
trim_trailing_whitespace = true   # 設爲 true 表示會除去換行行首的任意空白字符,false 反之。

[*.md]                            # 校驗 markdown 文檔
insert_final_newline = false
trim_trailing_whitespace = false
複製代碼

⬆ back to top

5. browserslist

  • browserslist是什麼?

    用於在不一樣前端工具之間共享目標瀏覽器和Node.js版本的配置。例如 AutoprefixerStylelintbabel-preset-env

  • browserslist 配置方式

    當您將如下內容添加到 package.json 或 .browserslistrc配置文件中時,全部工具都會自動找到目標瀏覽器:

    # package.json
    
    {
      "browserslist": {
        "production": [             // 生產環境配置
          ">0.2%",                  // 支持市場份額大於 1% 的瀏覽器。
          "not dead",               // not(邏輯非)對 dead 取反,而瀏覽器被認爲是 dead 條件是:最新的兩個版本中發現其市場份額已經低於 0.5% 而且 24 個月內沒有官方支持和更新。
          "not op_mini all"         // OperaMini or op_mini for Opera Mini.
        ],
        "development": [            // 開發環境配置
          "last 1 chrome version",  // 瀏覽器版本查詢範圍, chrome 最近的一個版本
          "last 1 firefox version",
          "last 1 safari version"
        ]
      }
    }
    
    或🔥
    
    # .browserslistrc
    
    $ touch .browserslist
    
      [production]
    
      > .2%
      not dead
      not op_mini all
    
      [development]
    
      last 1 chrome version
      last 1 firefox version
      last 1 safari version
    複製代碼

⬆ back to top

6. 引入 webpack

本質上,webpack 是一個現代 JavaScript 應用程序的靜態模塊打包工具。當 webpack 處理應用程序時,它會在內部構建一個 依賴圖(dependency graph),此依賴圖會映射項目所需的每一個模塊,並生成一個或多個 bundle

  • 安裝與建立基本文件

    $ mkdir src                                # 建立存放核心代碼文件夾
    $ cd src && touch index.js                  # 建立入口文件
    
    $ yarn add -D webpack                      # 安裝最新版本 webpack^4.41.2
    $ yarn add -D webpack-cli                  # 安裝 webpack v4+ 版本,所需的 webpack-cli^3.3.9
    
    $ cd .. && touch webpack.config.js          # 根目錄,建立 webpack 基本配置文件
    複製代碼
  • 工程目錄

    └── starter
    + ├── node_modules
    + ├── src
    + │ └── index.js
    + ├── webpack.config.js
      ├── package.json
      └── README.md
    複製代碼

⬆ back to top

7. 引入 React

A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • 安裝與建立基本文件

    $ yarn add react                 # 安裝 react^16.10.2
    $ yarn add react-dom             # 安裝 react-dom^16.10.2
    
    $ mkdir public                   # 新建公共資源文件夾
    $ cd public && touch index.html  # 新建 html 文件
    $ copy favicon.ico               # 添加 網頁圖標 文件
    $ cd ..                          # 回到根目錄
    複製代碼
  • 編寫 index.html 文件

    <!DOCTYPE html>
      <html lang="en">
    
      <head>
        <meta charset="utf-8" />
        <link rel="icon" href="./favicon.ico" />
        <meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
        <meta name="theme-color" content="#000000" />
        <meta name="description" content="This is a react application built from scratch with JavaScript, away from the cli tool." />
        <title>Starter</title>
      </head>
    
      <body>
        <noscript>You need to enable JavaScript to run this app.</noscript>
        <div id="root"></div>
      </body>
    
      </html>
    複製代碼
  • 編寫 index.js 文件

    import React from 'react';
    import ReactDom from 'react-dom';
    
    const App = () => <h1>Hello, world!</h1>
    
    ReactDom.render(<App />, document.getElementById('root')); 複製代碼

注意:因爲瀏覽器不支持最新的 JavaScript 語法和 react jsx 的語法解析,因此咱們須要一個編譯器幫助咱們。

⬆ back to top

8. 引入 Babel

Babel 是一個工具鏈,主要用於在舊的瀏覽器或環境中將 ECMAScript 2015+ 代碼轉換爲向後兼容版本的 JavaScript 代碼。

  • babel 安裝

    $ yarn add -D @babel/core                      # Babel 編譯器核心模塊
    $ yarn add -D @babel/preset-env                # 是一個智能預設,它使您可使用最新的JavaScript,而無需微觀管理目標環境所需的語法轉換
    $ yarn add -D @babel/preset-react              # react 智能預設, 包含了解析 jsx 等插件
    $ yarn add -D babel-loader                     # Babel loader for webpack 該軟件包容許使用 Babel 和 webpack 來轉譯 JavaScript 文件。
    
    $ touch .babelrc                               # 新建 babel 配置文件
    複製代碼
  • babel 配置

    // .babelrc
    {
      "presets": ["@babel/preset-env", "@babel/preset-react"],
    }
    複製代碼
  • 編寫 webpack 配置

    // starter/webpack.config.js
    const path = require('path');
    
    module.exports = function() {
      const baseConfig = {
        entry: './src/index.js',
    
        output: {
          path: path.resolve(__dirname, 'dist'),
          filename: 'bundle.js'
        },
    
        module: {
          rules: [
            {
              test: /\.(js|jsx)$/,
              exclude: /node_modules/,
              loader: 'babel-loader'
            },
          ]
        }
      };
    
      return baseConfig;
    };
    複製代碼
  • 修改 package.json 添加 webpack 命令, 快捷運行

    {
        "scripts": {
          "test": "echo \"Error: no test specified\" && exit 1",
    + "build": "webpack --color --progress"
        }
      }
    複製代碼
  • 修改 index.html 引入打包以後的 bundle.js 文件

    ...
      <div id="root"></div>
    + <script src="../dist/bundle.js"></script>
      ...
    複製代碼
  • 運行項目

    $ yarn build # 打包文件
    
      Hash: 6e4adf36d533e9d646c0
      Version: webpack 4.41.2
      Time: 693ms
    
      Built at: 2019-10-19 11:41:22
          Asset      Size      Chunks             Chunk Names
         bundle.js  1.09 MiB    main   [emitted]     main
    
      Entrypoint main = bundle.js
    
      [./src/index.js] 233 bytes {main} [built]
    
    # 瀏覽器 打開 index.html 查看效果
    複製代碼

    x

  • 工程目錄

    └── starter
    + ├── dist
    + │ └── bundle.js
      ├── node_modules
    + ├── public
    + │ ├── favicon.ico
    + │ └── index.html
      ├── src
      │   └── index.js
      ├── webpack.config.js
      ├── package.json
      ├── README.md
    + └── yarn.lock
    複製代碼

⬆ back to top

9. 搭建開發環境 - 藉助 webpack-dev-server

webpack-dev-server 爲你提供了一個簡單的 web server,而且具備 live reloading(實時從新加載) 功能。

  • 安裝

    $ yarn add -D webpack-dev-server # 用於快速開發應用程序
    複製代碼
  • 添加相應配置

    // starter/webpack.config.js
    
    const path = require('path');
    
    module.exports = function() {
      const baseConfig = {
    
        devtool: 'inline-source-map', // 控制是否生成,以及如何生成 source map
    
        entry: './src/index.js',
    
        output: {
          path: path.resolve(__dirname, 'dist'),
          filename: 'bundle.js'
        },
    
        module: {
          rules: [
            {
              test: /\.(js|jsx)$/,
              exclude: /node_modules/,
              loader: 'babel-loader'
            },
          ]
        },
    
    + devServer: {
    + contentBase: path.resolve(__dirname, 'public'), // 告訴服務器從哪一個目錄中提供內容
    + historyApiFallback: true, // 啓用當使用 HTML5 History API 時,任意的 404 響應均可能須要被替代爲 index.html。
    + compress: true, // 一切服務都啓用 gzip 壓縮
    + open: true, // 告訴 dev-server 在 server 啓動後打開瀏覽器
    + port: 3000, // 指定要監聽請求的端口號
    + stats: 'errors-only', // 精確控制要顯示的 bundle 信息 (在 bundle 中只顯示錯誤)
    + }
      };
    
      return baseConfig;
    };
    複製代碼
  • 修改 package.json 添加 webpack 命令, 快捷運行

    {
        "scripts": {
          "test": "echo \"Error: no test specified\" && exit 1",
          "build": "webpack --color --progress",
    + "server": "webpack-dev-server --color --progress"
        }
      }
    複製代碼

    --color: 啓用/禁用控制檯的彩色輸出; --progress: 將運行進度輸出到控制檯。

  • 修改 index.html 主文件 bundle.js 路徑

    ...
      <div id="root"></div>
    - <script src="../dist/bundle.js"></script>
    + <script src="bundle.js"></script>
      ...
    複製代碼
  • 運行項目

    $ yarn server
    
    # 結果:
    
    $ webpack-dev-server --color --progress
    
    10% building 1/1 modules 0 activeℹ 「wds」: Project is running at http://localhost:3000/
    ℹ 「wds」: webpack output is served from /
    ℹ 「wds」: Content not from webpack is served from /Users/mr.lemon/cl/CODE_CL/REACT/starter/public
    ℹ 「wds」: 404s will fallback to /index.html
    ℹ 「wdm」: Compiled successfully.
    複製代碼

    打開 http://localhost:3000/ 將顯示 Hello, world!; 修改 src/index.js 將會刷新瀏覽器實時更新修改。Try it!

  • 存在問題或待改進提高點

    1. 每次更改都需刷新整個瀏覽器,這顯然是不符合現代工程開發體驗!
    2. 未區分環境( webpack.config.js 有些配置咱們只但願在開發環境有,而在生產環境應有其特定配置)

    帶着這些問題,繼續吧!👍

⬆ back to top

10. 搭建開發環境 - 環境變量

  • 安裝

    $ yarn add -D cross-env # Cross platform setting of environment scripts
    複製代碼
  • 修改 package.json webpack 命令

    {
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
    - "build": "webpack --color --progress",
    + "build": "cross-env NODE_ENV=production webpack --color --progress",
    - "server": "webpack-dev-server --color --progress"
    + "server": "cross-env NODE_ENV=development webpack-dev-server --color --progress"
      }
    }
    複製代碼
  • 爲 webpack.config.js 添加相應配置

    // starter/webpack.config.js
    
      const path = require('path');
    + const IS_PROD = process.env.NODE_ENV === 'production';
    
      module.exports = function() {
        const baseConfig = {
    + mode: IS_PROD ? 'production' : 'development',
    - devtool: 'inline-source-map', // 控制是否生成,以及如何生成 source map
    + devtool: IS_PROD ? false : 'inline-source-map',
    
          entry: './src/index.js',
    
          output: {
            path: path.resolve(__dirname, 'dist'),
            filename: 'bundle.js'
          },
    
          module: {
            rules: [
              {
                test: /\.(js|jsx)$/,
                exclude: /node_modules/,
                loader: 'babel-loader'
              },
            ]
          },
    
    - devServer: {
    - contentBase: path.resolve(__dirname, 'public'), // 告訴服務器從哪一個目錄中提供內容
    - historyApiFallback: true, // 啓用當使用 HTML5 History API 時,任意的 404 響應均可能須要被替代爲 index.html。
    - compress: true, // 一切服務都啓用 gzip 壓縮
    - open: true, // 告訴 dev-server 在 server 啓動後打開瀏覽器
    - port: 3000, // 指定要監聽請求的端口號
    - stats: 'errors-only', // 精確控制要顯示的 bundle 信息 (在 bundle 中只顯示錯誤)
    - }
        };
    
    + if (!IS_PROD) {
    + baseConfig.devServer = {
    + contentBase: path.resolve(__dirname, 'public'), // 告訴服務器從哪一個目錄中提供內容
    + historyApiFallback: true, // 啓用當使用 HTML5 History API 時,任意的 404 響應均可能須要被替代爲 index.html。
    + compress: true, // 一切服務都啓用 gzip 壓縮
    + open: true, // 告訴 dev-server 在 server 啓動後打開瀏覽器
    + port: 3000, // 指定要監聽請求的端口號
    + stats: 'errors-only', // 精確控制要顯示的 bundle 信息 (在 bundle 中只顯示錯誤)
    + };
    + }
    
        return baseConfig;
      };
    複製代碼

⬆ back to top

11. 搭建開發環境 - 熱替換模塊(Hot Module Replacement)

模塊熱替換(hot module replacement 或 HMR)是 webpack 提供的最有用的功能之一。它容許在運行時更新全部類型的模塊,而無需徹底刷新。

  • 爲 webpack.config.js 添加相應配置

    // starter/webpack.config.js
    
      const path = require('path');
    + const webpack = require('webpack');
      const IS_PROD = process.env.NODE_ENV === 'production';
    
      module.exports = function() {
        const baseConfig = {
          mode: IS_PROD ? 'production' : 'development',
    
          entry: './src/index.js',
    
          output: {
            path: path.resolve(__dirname, 'dist'),
            filename: 'bundle.js'
          },
    
          module: {
            rules: [
              {
                test: /\.(js|jsx)$/,
                exclude: /node_modules/,
                loader: 'babel-loader'
              },
            ]
          },
    
    + plugins: []
        };
    
        if (!IS_PROD) {
          baseConfig.devServer = {
            contentBase: path.resolve(__dirname, 'public'), // 告訴服務器從哪一個目錄中提供內容
            historyApiFallback: true,                       // 啓用當使用 HTML5 History API 時,任意的 404 響應均可能須要被替代爲 index.html。
            compress: true,                                 // 一切服務都啓用 gzip 壓縮
            open: true,                                     // 告訴 dev-server 在 server 啓動後打開瀏覽器
            port: 3000,                                     // 指定要監聽請求的端口號
            stats: 'errors-only',                           // 精確控制要顯示的 bundle 信息 (在 bundle 中只顯示錯誤)
    + hot: true // 啓用 webpack 的 模塊熱替換 功能
          };
    + baseConfig.plugins.concat([
    + new webpack.HotModuleReplacementPlugin() // 熱替換模塊插件
    + ]);
        }
    
        return baseConfig;
      };
    複製代碼
  • 修改 src/index.js 文件

    - import React from 'react';
    + import React, { useState } from 'react';
      import ReactDom from 'react-dom';
    
    
    - const App = () => <h1>Hello, world!</h1>;
    
    + const App = () => {
    + const [title, setTitle] = useState('hello, world!');
    + const reversedTitle = () =>
    + setTitle(
    + title
    + .split('')
    + .reverse()
    + .join('')
    + );
    + return (
    + <div>
    + <h1>{ title }</h1>
    + <button type='button' onClick={reversedTitle}>
    + reversed title
    + </button>
    + </div>
    + );
    + };
    
    + if (module.hot) {
    + module.hot.accept();
    + }
    
      ReactDom.render(<App />, document.getElementById('root'));
    複製代碼
  • 運行項目

    $ yarn server
    
    # 結果:
    
    $ cross-env NODE_ENV=development webpack-dev-server --color --progress
    10% building 1/1 modules 0 activeℹ 「wds」: Project is running at http://localhost:3000/
    ℹ 「wds」: webpack output is served from /
    ℹ 「wds」: Content not from webpack is served from /Users/mr.lemon/cl/CODE_CL/REACT/starter/public
    ℹ 「wds」: 404s will fallback to /index.html
    ℹ 「wdm」: Compiled successfully.
    
    # 瀏覽器 console
    [HMR] Waiting for update signal from WDS...      log.js:24
    [WDS] Hot Module Replacement enabled.            client:48
    [WDS] Live Reloading enabled.                    client:52
    複製代碼

    打開 http://localhost:3000/ 修改 src/index.js 實現了未刷新瀏覽器更新修改。Try it!

    x

  • 存在問題或待改進提高點

    1. 每次修改內容時,作到了無刷新更新,但同時也清空了組件內部狀態值;這顯然也是不能接受的。

    帶着這個問題,繼續吧!✈️

⬆ back to top

12. 搭建開發環境 - 熱替換模塊 - 引入 react-hot-loader

實時調整React組件。

  • 說明

    1. 因爲該項目選擇的是 react 框架,故引入 react-hot-loader。
    2. 若是換成別的框架也有對應的插件集成,好比:vue 在 vue-loader 集成的 vue-hot-reload-api。
    3. 固然,這些你也能夠本身去實現。
  • 安裝

    $ yarn add react-hot-loader
    $ yarn add @hot-loader/react-dom # 替換了相同版本的 react-dom 軟件包,但附加了一些補丁以支持熱重裝。
    複製代碼
  • "react-hot-loader/babel" 添加到您的 .babelrc

    {
        "presets": ["@babel/preset-env", "@babel/preset-react"],
    + "plugins": ["react-hot-loader/babel"]
      }
    複製代碼
  • 重置 react-dom 兼容 hooks

    ...
    
      moduele.exports = function () {
    
        ...
    
    + resolve: {
    + alias: {
    + 'react-dom': '@hot-loader/react-dom' // react-hot-loader 兼容 hook 寫法
    + }
    + },
    
        ...
    
      }
    
      ...
    複製代碼
  • 修改 src/index.js 主文件,將根組件標記爲 hot-exported

    + import { hot } from 'react-hot-loader';
      import React, { useState } from 'react';
      import ReactDom from 'react-dom';
    
    - const App = () => {
    + const App = hot(module)(() => {
        const [title, setTitle] = useState('hello, world!');
    
        const reversedTitle = () =>
          setTitle(
            title
              .split('')
              .reverse()
              .join('')
          );
        return (
          <div>
            <h1>{title}</h1>
            <button type='button' onClick={reversedTitle}>
              reversed title!
            </button>
          </div>
        );
    - };
    + });
    
    - if (module.hot) {
    - module.hot.accept();
    - }
    
      ReactDom.render(<App />, document.getElementById('root'));
    複製代碼
  • 運行項目

    $ yarn server
    
    # 結果:
    
    $ cross-env NODE_ENV=development webpack-dev-server --color --progress
    10% building 1/1 modules 0 activeℹ 「wds」: Project is running at http://localhost:3000/
    ℹ 「wds」: webpack output is served from /
    ℹ 「wds」: Content not from webpack is served from /Users/mr.lemon/cl/CODE_CL/REACT/starter/public
    ℹ 「wds」: 404s will fallback to /index.html
    ℹ 「wdm」: Compiled successfully.
    ℹ 「wdm」: Compiling...
    ℹ 「wdm」: Compiled successfully.
    
    # 瀏覽器 console
    [HMR] Waiting for update signal from WDS...      log.js:24
    [WDS] App hot update...                          reloadApp.js:19
    [HMR] Checking for updates on the server...      log.js:24
    [HMR] Updated modules:                           log.js:24
    [HMR]  - ./src/index.js                          log.js:24
    [HMR] App is up to date.                         log.js:24
    複製代碼

    打開 http://localhost:3000/, 點擊 reversed title 而後修改 src/index.js 實現了未刷新瀏覽器保留組件狀態的更新修改。Try it!

    x

  • 階段結語

    1. 至此一個簡易的開發環境,就這樣被搭建出來了。🐃
    2. 後面的工做還不少,繼續吧!🍺

⬆ back to top

13. 引入 CSS 與 Sass 樣式文件處理

樣式是前端組件重要組成部分,而 Sass 讓 CSS 語言更強大、優雅;有助於保持大型樣式表結構良好。

注意:本項目引入 sass ,固然你也能夠不引入或者引入其它,如:less、stylus。

  • 安裝

    $ yarn add -D node-sass      # Node-sass是一個庫,提供了 Node.js 與 LibSass(流行的樣式表預處理器Sass的C版本)的綁定。 它使您可以以驚人的速度經過鏈接中間件自動將 .scss 文件本地編譯爲 css
    $ yarn add -D sass-loader    # Compiles Sass to CSS
    $ yarn add -D css-loader     # The css-loader interprets @import and url() like import/require() and will resolve them.
    $ yarn add -D style-loader   # Inject CSS into the DOM.
    複製代碼

    注:sass基於Ruby語言開發而成,所以安裝sass前須要安裝Ruby。(注:mac下自帶Ruby無需在安裝Ruby!)

    爲何須要 node-sass : 由於 sass-loader 的 peerDependencies 聲明瞭其依賴 node-sass,因此須要預裝,不然警告。

  • 配置:修改 webpack.config.js 增長css/sass解析能力

    ...
    
      moduele.exports = function () {
    
        ...
    
        module: {
          rules: [
            ...
    
    + {
    + test: /\.(sa|sc|c)ss$/,
    + exclude: /node_modules/,
    + use: [
    + {
    + loader: 'style-loader'
    + },
    + {
    + loader: 'css-loader',
    + options: {
    + sourceMap: !IS_PROD
    + }
    + },
    + {
    + loader: 'sass-loader',
    + options: {
    + sourceMap: !IS_PROD
    + }
    + }
    + ]
    + }
          ]
        }
    
        ...
    
      }
    
      ...
    複製代碼
  • 新增 src/index.scss 和 style/global.css 樣式文件

    $ cd src && touch index.scss
    $ mkdir style && cd style
    $ touch global.css && touch reset.css
    複製代碼
    // src/style/reset.css
    # reset 重置瀏覽器初始樣式,具體樣式參見項目 src/style/reset.css
    
    // src/style/global.css
    @import url('./reset.css');
    
    // src/index.scss
    .app {
      background-color: red;
    }
    複製代碼
  • 修改 src/index.js 導入樣式表

    import { hot } from 'react-hot-loader';
      import React, { useState } from 'react';
      import ReactDom from 'react-dom';
    + import './style/global.css';
    + import './index.scss';
    
      const App = hot(module)(() => {
        const [title, setTitle] = useState('hello, world!');
    
        const reversedTitle = () =>
          setTitle(
            title
              .split('')
              .reverse()
              .join('')
          );
        return (
    - <div>
    + <div className='app'>
            <h1>{title}</h1>
            <button type='button' onClick={reversedTitle}>
              reversed title!
            </button>
          </div>
        );
      });
    
      ReactDom.render(<App />, document.getElementById('root'));
    複製代碼
  • 運行項目

    $ yarn server
    
    # 結果:
    
    $ cross-env NODE_ENV=development webpack-dev-server --color --progress
    10% building 1/1 modules 0 activeℹ 「wds」: Project is running at http://localhost:3000/
    ℹ 「wds」: webpack output is served from /
    ℹ 「wds」: Content not from webpack is served from /Users/mr.lemon/cl/CODE_CL/REACT/starter/public
    ℹ 「wds」: 404s will fallback to /index.html
    ℹ 「wdm」: Compiled successfully.
    複製代碼

    打開 http://localhost:3000/, 如你所寫,出現一個紅色背景。Try it!

    x

  • 問題與改進點🤔

    1. 缺乏自動管理瀏覽器前綴的插件,解析 CSS 文件而且添加瀏覽器前綴到 CSS 內容裏;postcss/autoprefixer
    2. 當組件樣式文件不少時,爲了不樣式衝突,能夠採用 css-modules 去解決這個問題。固然你也能夠採用嚴格命名規範繞開這個問題,如:BEM。

    那繼續吧!💪

⬆ back to top

14. CSS-Modulesautoprefixer

  • 安裝

    $ yarn add - D postcss-loader # 用於webpack的Loader以使用PostCSS處理CSS
    
    $ yarn add -D autoprefixer # Parse CSS and add vendor prefixes to rules by Can I Use
    複製代碼
  • 新建 postcss 配置文件

    $ touch postcss.config.js # 新建 postcss 配置文件
    
    # starter/postcss.config.js 添加 autoprefixer 插件
    module.exports = {
      plugins: {
        autoprefixer: {},
      }
    };
    複製代碼
  • 添加 webpack postcss 配置

    ...
    
      moduele.exports = function () {
    
        ...
    
        module: {
          rules: [
            ...
    
            {
    - test: /\.(sa|sc|c)ss$/,
    + test: /\.(sa|sc)ss$/,
              exclude: /node_modules/,
              use: [
                {
                  loader: 'style-loader'
                },
                {
                  loader: 'css-loader',
    + options: {
    + sourceMap: !IS_PROD,
    + importLoaders: 2, // 啓用/禁用或設置在CSS加載程序以前應用的加載程序的數量
    + modules: {
    + context: path.resolve(__dirname, 'src'), // 容許爲本地標識符名稱從新定義基本的加載程序上下文。
    + localIdentName: '[name]__[local]-[hash:base64:5]' // 使用 localIdentName 查詢參數配置生成類名
    + }
    + }
                },
    + {
    + loader: 'postcss-loader'
    + }
                {
                  loader: 'sass-loader',
                  options: {
                    sourceMap: !IS_PROD
                  }
                }
              ]
            },
    + {
    + test: /\.css$/,
    + exclude: /node_modules/,
    + use: [
    + 'style-loader',
    + {
    + loader: 'css-loader',
    + options: {
    + sourceMap: !IS_PROD
    + }
    + },
    + 'post-loader'
    + ]
    + }
          ]
        }
    
        ...
    
      }
    
      ...
    複製代碼

    postcss : 一個用 JavaScript 轉換 CSS 的工具
    css-loader 提供 CSS 模塊及其配置

  • 修改 src/index.js 類名寫法

    import { hot } from 'react-hot-loader';
      import React, { useState } from 'react';
      import ReactDom from 'react-dom';
      import './style/global.css';
    - import './index.scss';
    + import styles from './index.scss';
    
      const App = hot(module)(() => {
        const [title, setTitle] = useState('hello, world!');
    
        const reversedTitle = () =>
          setTitle(
            title
              .split('')
              .reverse()
              .join('')
          );
        return (
    - <div className='app'>
    + <div className={styles.app}>
            <h1>{title}</h1>
            <button type='button' onClick={reversedTitle}>
              reversed title!
            </button>
          </div>
        );
      });
    
      ReactDom.render(<App />, document.getElementById('root'));
    複製代碼
  • 運行項目

    $ yarn server
    
    # 結果
    
    $ cross-env NODE_ENV=development webpack-dev-server --color --progress
    10% building 1/1 modules 0 activeℹ 「wds」: Project is running at http://localhost:3000/
    ℹ 「wds」: webpack output is served from /
    ℹ 「wds」: Content not from webpack is served from /Users/gt/LEMON/starter/public
    ℹ 「wds」: 404s will fallback to /index.html
    ℹ 「wdm」: Compiled successfully.
    複製代碼

    打開 http://localhost:3000/ 查看,是否如你所寫!

    x

⬆ back to top

15. 更進一步,構建咱們的應用 yarn build

打包

$ yarn build

# 結果
$ cross-env NODE_ENV=production webpack --color --progress
Hash: 4f40eeb2a231c73dacd9
Version: webpack 4.41.2
Time: 4142ms
Built at: 2019-10-21 10:54:37

    Asset     Size        Chunks             Chunk Names
  bundle.js  136 KiB       0  [emitted]         main

Entrypoint main = bundle.js
[5] ./src/index.scss 498 bytes {0} [built]
[7] ./src/index.js 1.57 KiB {0} [built]
[8] (webpack)/buildin/harmony-module.js 573 bytes {0} [built]
[13] ./src/style/global.css 457 bytes {0} [built]
[14] ./node_modules/css-loader/dist/cjs.js!./node_modules/postcss-loader/src!./src/style/global.css 237 bytes {0} [built]
[15] ./node_modules/css-loader/dist/cjs.js!./src/style/reset.css 1.28 KiB {0} [built]
[16] ./node_modules/css-loader/dist/cjs.js??ref--5-1!./node_modules/postcss-loader/src!./node_modules/sass-loader/dist/cjs.js!./src/index.scss 238 bytes {0} [built]
    + 11 hidden modules
✨  Done in 5.90s.
複製代碼

咱們看到這隻打出一個 bundle.js 這顯然作的還不夠。接下來,咱們作幾點改變!

⬆ back to top

管理輸出

到目前爲止,咱們都是在 index.html 文件中手動引入全部資源,然而隨着應用程序增加,而且一旦開始 在文件名中使用 hash] 並輸出 多個 bundle,若是繼續手動管理 index.html 文件,就會變得困難起來。

  • 修改 webpack - output

    const path = require('path');
      const webpack = require('webpack');
      const IS_PROD = process.env.NODE_ENV === 'production';
    
      ...
    
        output: {
          path: path.resolve(__dirname, 'dist'),
    - publicPath: '/',
    + publicPath: IS_PROD ? '/starter/' : '/', // 公共路徑
    - filename: 'bundle.js'
    + filename: IS_PROD ? '[name].[contenthash:8].js' : '[name].js', // 輸出文件的文件名
    + chunkFilename: IS_PROD ? 'chunks/[name].[contenthash:8].js' : '[name].js', // 非入口(non-entry) chunk 文件的名稱
        },
    
      ...
    複製代碼
  • HtmlWebpackPlugin

    $ yarn add -D html-webpack-plugin # 安裝插件
    複製代碼
    <!-- starter/webpack.config.js -->
    
      const path = require('path');
      const webpack = require('webpack');
      const IS_PROD = process.env.NODE_ENV === 'production';
    + const HtmlWebpackPlugin = require('html-webpack-plugin')
    
      ...
    
    - plugins: []
    + plugins: [
    + new HtmlWebpackPlugin({
    + title: 'Starter',
    + filename: 'index.html',
    + template: path.resolve(__dirname, 'public/index.html'),
    + minify: IS_PROD
    + ? {
    + removeComments: true,
    + collapseWhitespace: true,
    + removeAttributeQuotes: true,
    + collapseBooleanAttributes: true,
    + removeScriptTypeAttributes: true
    + }
    + : {}
    + }),
    + ]
    
      ...
    複製代碼
  • 修改 public/index.html

    <!DOCTYPE html>
      <html lang="en">
        <head>
          <meta charset="utf-8" />
          <link rel="icon" href="./favicon.ico" />
          <meta
            name="viewport"
            content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
          />
          <meta name="theme-color" content="#000000" />
          <meta
            name="description"
            content="This is a react application built from scratch with JavaScript, away from the cli tool."
          />
    - <title>Starter</title>
    + <title><%= htmlWebpackPlugin.options.title %></title>
        </head>
    
        <body>
          <noscript>You need to enable JavaScript to run this app.</noscript>
          <div id="root"></div>
    - <script src="bundle.js"></script>
        </body>
      </html>
    複製代碼
  • 經過上述配置,讓咱們來看看效果吧

    $ yarn build
    
    # 結果
    $ cross-env NODE_ENV=production webpack --color --progress
    Hash: 6bb93a13b6a8a7926f58
    Version: webpack 4.41.2
    Time: 4418ms
    Built at: 2019-10-21 11:47:05
    
              Asset         Size             Chunks                   Chunk Names
          index.html      553 bytes          [emitted]
        main.2f781ad1.js   136 KiB         0  [emitted] [immutable]      main
    
    Entrypoint main = main.2f781ad1.js
    [5] ./src/index.scss 498 bytes {0} [built]
    [7] ./src/index.js 1.57 KiB {0} [built]
    [8] (webpack)/buildin/harmony-module.js 573 bytes {0} [built]
    [13] ./src/style/global.css 457 bytes {0} [built]
    [14] ./node_modules/css-loader/dist/cjs.js!./node_modules/postcss-loader/src!./src/style/global.css 237 bytes {0} [built]
    [15] ./node_modules/css-loader/dist/cjs.js!./src/style/reset.css 1.28 KiB {0} [built]
    [16] ./node_modules/css-loader/dist/cjs.js??ref--5-1!./node_modules/postcss-loader/src!./node_modules/sass-loader/dist/cjs.js!./src/index.scss 238 bytes {0} [built]
        + 11 hidden modules
    Child html-webpack-plugin for "index.html":
        1 asset
        Entrypoint undefined = index.html
        [0] ./node_modules/html-webpack-plugin/lib/loader.js!./public/index.html 858 bytes {0} [built]
        [2] (webpack)/buildin/global.js 472 bytes {0} [built]
        [3] (webpack)/buildin/module.js 497 bytes {0} [built]
            + 1 hidden module
    ✨  Done in 6.04s.
    複製代碼
    <!-- starter/dist/index.html -->
    <!DOCTYPE html><html lang=en><head><meta charset=utf-8><link rel=icon href=./favicon.ico><meta name=viewport content="width=device-width,minimum-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover"><meta name=theme-color content=#000000><meta name=description content="This is a react application built from scratch with JavaScript, away from the cli tool."><title>React App TS</title></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id=root></div><script src=/starter/main.2f781ad1.js></script></body></html>
    複製代碼

    注:若是你仔細看了咱們的輸出,你會發現 main.2f781ad1.js size=136KiB, 而咱們的代碼卻量卻不多,若是你打開該文件你會發現它包含了 react.production.min.js babel 所需的幫助函數等。

    ⬆ back to top

代碼分離

  • mini-css-extract-plugin - 分離 css 代碼

    webpack 默認把 css 和 js 打到一個文件,該插件將CSS提取到單獨的文件中。它爲每一個包含CSS的JS文件建立一個CSS文件。

    爲何分離?webpack-contrib/mini-css-extract-plugin

    $ yarn add -D mini-css-extract-plugin # 安裝
    複製代碼
    <!-- starter/webpack.config.js -->
      ...
    + const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    
      module: {
        rules: [
          {
            test: /\.(js|jsx)$/,
            exclude: /node_modules/,
            loader: 'babel-loader'
          },
          {
            test: /\.(sa|sc)ss$/,
            exclude: /node_modules/,
            use: [
              {
    - loader: 'style-loader'
    + loader: IS_PROD ? MiniCssExtractPlugin.loader : 'style-loader',
    + options: IS_PROD ? { publicPath: '../' } : {}
              },
              {
                loader: 'css-loader',
                options: {
                  sourceMap: false,
                  importLoaders: 2,
                  modules: {
                    context: path.resolve(__dirname, 'src'),
                    localIdentName: '[name]__[local]-[hash:base64:5]'
                  }
                }
              },
              {
                loader: 'postcss-loader'
              },
              {
                loader: 'sass-loader'
              }
            ]
          },
          {
            test: /\.css$/,
            exclude: /node_modules/,
    - use: ['style-loader', 'css-loader', 'postcss-loader']
    + use: [
    + {
    + loader: IS_PROD ? MiniCssExtractPlugin.loader : 'style-loader',
    + options: IS_PROD ? { publicPath: '../' } : {}
    + },
    + 'css-loader',
    + 'postcss-loader'
    + ]
          }
        ]
      },
    + plugins: [
        ...,
    + new MiniCssExtractPlugin({
    + filename: IS_PROD ? 'css/[name].[contenthash:8].css' : 'css/[name].css',
    + chunkFilename: IS_PROD ? 'css/[name].[contenthash:8].css' : 'css/[name].css'
    + })
      ]
    複製代碼
    $ yarn build
    
      $ cross-env NODE_ENV=production webpack --color --progress
      Hash: 95fccf0e0844c2df588f
      Version: webpack 4.41.2
      Time: 4416ms
      Built at: 2019-10-21 13:56:23
                      Asset       Size           Chunks                 Chunk Names
    ! css/main.f9cee851.css 1.08 KiB 0 [emitted] [immutable] main
                index.html      605 bytes         [emitted]
    ! main.ced0f821.js 131 KiB 0 [emitted] [immutable] main
      Entrypoint main = css/main.f9cee851.css main.ced0f821.js
    
    複製代碼

    屢次打包以後咱們發現多處不少上次結果文件,這顯然不能忍受 w(゚Д゚)w; 咱們但願在每次構建以前刪除以前構建生成的文件夾。

  • clean-webpack-plugin 保持目錄清潔

    用於在構建以前刪除您的構建文件夾

    $ yarn add -D clean-webpack-plugin # 安裝
    複製代碼
    <!-- starter/webpack.config.js -->
    
      ...
    
    + const { CleanWebpackPlugin } = require('clean-webpack-plugin');
    
      ...
    
      plugins: [
        ...,
    
    + new CleanWebpackPlugin()
      ]
    
      ...
    複製代碼

    試試看👀,清理乾淨了 (。・∀・)ノ゙ Try it!

    ⬆ back to top

防止重複

  • optimization.splitChunks 將公共的依賴模塊提取到已有的 entry chunk 中

    <!-- starter/webpack.config.js -->
    
      ...
    
      module.exports = function () {
        const baseConfig = {
          ...
        }
    
    + if (IS_PROD) {
    + baseConfig.optimization = {
    + minimizer: [
    + // Automatically split vendor and commons
    + splitChunks: {
    + chunks: 'all'
    + }
    + ]
    + }
    + }
    
        return baseConfig;
      }
    複製代碼
    $ yarn build # 打包查看效果
    
      # 結果
    
      $ cross-env NODE_ENV=production webpack --color --progress
      Hash: ebe27d1c4dc54ff22c4b
      Version: webpack 4.41.2
      Time: 4470ms
      Built at: 2019-10-21 14:28:59
    
                                Asset     Size                  Chunks             Chunk Names
    ! chunks/vendors~main.f501917c.js 129 KiB 1 [emitted] [immutable] vendors~main
                css/main.f9cee851.css     1.08 KiB      0  [emitted] [immutable]      main
    ! index.html 667 bytes [emitted]
    ! main.76c9ecec.js 2.54 KiB 0 [emitted] [immutable] main
    
      Entrypoint main = chunks/vendors~main.f501917c.js css/main.f9cee851.css main.76c9ecec.js
    
      # 注意:若是你仔細看 chunks/vendors~main.f501917c.js 你會發現 與 react 相關的庫
      #(react.production.min.js、react-dom.production.min.js、scheduler.production.min.js)和你代
      # 碼所引用的公共庫都將被提取出來,防止重複引用。
    複製代碼

    webpack 4: Code Splitting, chunk graph and the splitChunks optimization

    ⬆ back to top

  • @babel/plugin-transform-runtime 一個插件,可從新使用Babel注入的幫助程序代碼以節省代碼大小。

    $ yarn add -D @babel/plugin-transform-runtime
    複製代碼
    <!-- starter/postcss.config.js -->
    
      {
        "presets": [
          "@babel/preset-env",
          "@babel/preset-react"
        ],
        "plugins": [
    + "@babel/plugin-transform-runtime",
          "react-hot-loader/babel"
        ]
      }
    複製代碼
    $ yarn build
    
      # 結果
    
      $ cross-env NODE_ENV=production webpack --color --progress
      Hash: 6425898f896ed7244e2b
      Version: webpack 4.41.2
      Time: 4510ms
      Built at: 2019-10-21 15:18:47
    
                                Asset       Size                Chunks                Chunk Names
    ! chunks/vendors~main.e9e35553.js 130 KiB 1 [emitted] [immutable] vendors~main
                css/main.f9cee851.css      1.08 KiB      0  [emitted] [immutable]        main
                            index.html      667 bytes        [emitted]
    ! main.5fb316df.js 2.07 KiB 0 [emitted] [immutable] main
      Entrypoint main = chunks/vendors~main.e9e35553.js css/main.f9cee851.css main.5fb316df.js
    
      # 能夠比對上次構建結果,主文件減小了一些。
    複製代碼

    ⬆ back to top

  • webpack.DefinePlugin 容許建立一個在編譯時能夠配置的全局常量

    插件可配置一些全局變量,在構建時將會對代碼內引用的這些變量進行替換。好比:NODE_ENV(經常使用於處理生產環境與開發環境)。若是在開發構建中,而不在發佈構建中執行日誌記錄,則可使用全局常量來決定是否記錄日誌。這就是 DefinePlugin 的用處,設置它,就能夠忘記開發環境和生產環境構建的規則。

    <!-- starter/webpack.config.js -->
    
      ...
    
      plugins: [
    
        ...,
    
    + new webpack.DefinePlugin({
    + 'process.env': {
    + NODE_ENV: JSON.stringify(process.env.NODE_ENV),
    + }
    + })
      ]
    
      ...
    複製代碼

    這裏若是你的代碼沒有對區分環境,作特定處理(去除開發環境下的代碼)則,包尺寸不變。

    ⬆ back to top

minify JavaScript / css

  • uglifyjs-webpack-plugin

    $ yarn add -D uglifyjs-webpack-plugin
    複製代碼
    <!-- starter/webpack.config.js -->
    
      ...
    
    + const UglifyjsWebpackPlugin = require('uglifyjs-webpack-plugin');
    
      ...
    
        if (IS_PROD) {
        baseConfig.optimization = {
    + minimizer: [
    + new UglifyjsWebpackPlugin({
    + exclude: /node_modules/,
    + sourceMap: false, // 使用源映射將錯誤消息位置映射到模塊(這會減慢編譯速度)。若是您使用本身的縮小功能,請閱讀縮小部分以正確處理源地圖。
    + cache: true, // 啓用文件緩存
    + parallel: true // 使用多進程並行運行可提升構建速度。併發運行的默認數量:os.cpus().length - 1.
    + })
    + ],
          splitChunks: {
            chunks: 'all',
          }
        };
      }
    複製代碼
    $ yarn build # 打包驗證 ✅
    
      # 結果
    
      $ cross-env NODE_ENV=production webpack --color --progress
      Hash: 3f450244bccc719560c5
      Version: webpack 4.41.2
      Time: 2209ms
      Built at: 2019-10-21 16:25:18
    
                                Asset       Size                  Chunks             Chunk Names
    ! chunks/vendors~main.1a122e64.js 129 KiB 1 [emitted] [immutable] vendors~main
                css/main.f9cee851.css      1.08 KiB      0  [emitted] [immutable]       main
                            index.html      667 bytes        [emitted]
                      main.e82008bc.js      2.07 KiB      0  [emitted] [immutable]       main
    
      Entrypoint main = chunks/vendors~main.1a122e64.js css/main.f9cee851.css main.e82008bc.js
    複製代碼

    注意: uglifyjs-webpack-plugin v2.x 版本基於 uglify-js,沒法支持 ES6 的壓縮

    參考:爲何 webpack4 默認支持 ES6 語法的壓縮?

    ⬆ back to top

  • terser-webpack-plugin

    咱們用 terser-webpack-plugin 替換 uglifyjs-webpack-plugin

    $ yarn add -D terser-webpack-plugin
    複製代碼
    <!-- starter/webpack.config.js -->
    
    - const UglifyjsWebpackPlugin = require('uglifyjs-webpack-plugin');
    + const TerserPlugin = require('terser-webpack-plugin');
    
      if (IS_PROD) {
        baseConfig.optimization = {
          minimizer: [
    - new UglifyjsWebpackPlugin({
    - exclude: /node_modules/,
    - sourceMap: false,
    - cache: true,
    - parallel: true
    - }),
    + new TerserPlugin({
    + // Terser minify options.
    + terserOptions: {
    + parse: {
    + // We want terser to parse ecma 8 code. However, we don't want it
    + // to apply any minification steps that turns valid ecma 5 code
    + // into invalid ecma 5 code. This is why the 'compress' and 'output'
    + // sections only apply transformations that are ecma 5 safe
    + ecma: 8,
    + },
    + compress: {
    + ecma: 5,
    + // display warnings when dropping unreachable code or unused declarations etc.
    + warnings: false,
    + // apply certain optimizations to binary nodes
    + // Disabled because of an issue with Uglify breaking seemingly valid code:
    + // Pending further investigation: https://github.com/mishoo/UglifyJS2/issues/2011
    + comparisons: false,
    + // inline calls to function with simple/return statement:
    + // Disabled because of an issue with Terser breaking valid code:
    + // Pending further investigation: https://github.com/terser-js/terser/issues/120
    + inline: 2, // inline functions with arguments
    + },
    + mangle: {
    + // Pass true to work around the Safari 10 loop iterator bug "Cannot declare a let variable twice".
    + // See also: the safari10 output option.
    + safari10: true,
    + },
    + // Added for profiling in devtools
    + keep_classnames: true,
    + keep_fnames: true,
    + output: {
    + ecma: 5,
    + // pass true or "all" to preserve all comments, "some" to preserve some comments,
    + // a regular expression string (e.g. /^!/) or a function.
    + comments: false,
    + // escape Unicode characters in strings and regexps (affects directives with non-ascii characters becoming invalid)
    + // Turned on because emoji and regex is not minified properly using default
    + ascii_only: true,
    + },
    + },
    + // Use multi-process parallel running to improve the build speed.
    + //Default number of concurrent runs: os.cpus().length - 1.
    + parallel: true,
    + cache: true, // Enable file caching
    + }),
          ],
          splitChunks: {
            chunks: 'all',
          }
        };
      }
    複製代碼
    $ yarn build
    
      $ cross-env NODE_ENV=production webpack --color --progress
      Hash: dbf5243d5591e4ac0268
      Version: webpack 4.41.2
      Time: 2461ms
      Built at: 2019-10-21 17:52:26
    
                                        Asset     Size             Chunks                  Chunk Names
    ! chunks/vendors~main.ae62441b.js 130 KiB 1 [emitted] [immutable] vendors~main
    ! chunks/vendors~main.ae62441b.js.LICENSE 790 bytes [emitted]
                        css/main.f9cee851.css    1.08 KiB      0  [emitted] [immutable]       main
                                    index.html    667 bytes        [emitted]
    ! main.2130b172.js 2.52 KiB 0 [emitted] [immutable] main
    
      Entrypoint main = chunks/vendors~main.ae62441b.js css/main.f9cee851.css main.2130b172.js
    複製代碼

    ⬆ back to top

  • optimize-css-assets-webpack-plugin - 優化/減小CSS資產

    $ yarn add -D optimize-css-assets-webpack-plugin # 壓縮 CSS
    $ yarn add -D postcss-safe-parser                # 查找並修復 CSS 語法錯誤
    複製代碼
    <!-- starter/webpack.config.js -->
      ...
    
    + const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
    + const SafePostCssParser = require('postcss-safe-parser');
    
        if (IS_PROD) {
          baseConfig.optimization = {
            minimizer: [
              ...
    
    + new OptimizeCSSAssetsPlugin({
    + // The options passed to the cssProcessor, defaults to {}
    + // cssProcessor: The CSS processor used to optimize \ minimize the CSS, defaults to cssnano.
    + // This should be a function that follows cssnano.process interface
    + // (receives a CSS and options parameters and returns a Promise).
    + cssProcessorOptions: {
    + parser: SafePostCssParser,
    + map: false,
    + },
    + })
            ],
    
            ...
          };
        }
    
      ...
    複製代碼
    $ yarn build # 打包實驗 ✅
    
    
      # 結果
    
      $ cross-env NODE_ENV=production webpack --color --progress
      Hash: dbf5243d5591e4ac0268
      Version: webpack 4.41.2
      Time: 3543ms
      Built at: 2019-10-21 20:17:23
    
                                        Asset     Size                  Chunks            Chunk Names
              chunks/vendors~main.ae62441b.js    130 KiB       1  [emitted] [immutable]   vendors~main
      chunks/vendors~main.ae62441b.js.LICENSE    790 bytes        [emitted]
    ! css/main.f9cee851.css 869 bytes 0 [emitted] [immutable] main
                                    index.html    667 bytes        [emitted]
                              main.2130b172.js    2.52 KiB      0  [emitted] [immutable]       main
    
      Entrypoint main = chunks/vendors~main.ae62441b.js css/main.f9cee851.css main.2130b172.js
    複製代碼

    ⬆ back to top

外部擴展(externals)

從輸出的 bundle 中排除依賴; 防止將某些 import 的包打包到 bundle 中,而是在運行時(runtime)再去從外部獲取這些擴展依賴(external dependencies)。

  • CDN - 此步可忽略

    <!-- starter/webpack.config.js -->
    
      ...
    
      module.exports = function() {
        const baseConfig = {
    
          ...
    
          resolve: {
            alias: {
              'react-dom': '@hot-loader/react-dom' // react-hot-loader 兼容 hook 寫法
            }
          },
    
    + externals: {
    + react: 'React',
    + 'react-dom': 'ReactDOM'
    + },
    
          ...
    
        }
    
        ...
    
    
    <!--  starter/public/index.html -->
      ...
    
        <div id="root"></div>
    + <script crossorigin src="https://unpkg.com/react@16.10.2/umd/react.production.min.js"></script>
    + <script crossorigin src="https://unpkg.com/@hot-loader/react-dom@16.10.2/umd/react-dom.production.min.js"></script>
    
      ...
    複製代碼
    $ yarn build
    
      $ cross-env NODE_ENV=production webpack --color --progress
        Hash: 6bb2de2632bdaf2dc081
        Version: webpack 4.41.2
        Time: 2557ms
        Built at: 2019-10-21 21:35:38
                        Asset       Size                Chunks             Chunk Names
        css/main.34dd0d40.css     869 bytes     0  [emitted] [immutable]      main
                    index.html     811 bytes        [emitted]
              main.da7fbe78.js     3.85 KiB      0  [emitted] [immutable]      main
        Entrypoint main = css/main.34dd0d40.css main.da7fbe78.js
    複製代碼

    1. CDN是什麼?使用CDN有什麼優點?
    2. 幾個 CDN 公共庫:cdnjsjsdelivrunpkg
    3. 爲提升訪問速度,最好把前端不常更新的類庫,如,react、react-dom、axios、moment等從輸出的 bundle 中排除依賴
    4. 提示,最好本身弄個,用本身的老是來得保險一些 🤡

    ⬆ back to top

工程目錄

└── starter
+ ├── dist
+ │ └── chunks
+ │ │ ├── vendors~main.ae62441b.js
+ │ │ └── vendors~main.ae62441b.js.LICENSE
+ ├── css
+ │ │ └── main.f9cee851.css
+ │ ├── index.html
+ │ └── main.2130b172.js
  ├── node_modules
  ├── public
  │   ├── favicon.ico
  │   └── index.html
  ├── src
+ │ └── style
+ │ | ├── global.css
+ │ | └── reset.css
  |   ├── index.js
+ │ ├── index.scss
+ ├── postcss.config.js
  ├── webpack.config.js
  ├── package.json
  ├── README.md
  ├── LICENSE
  └── yarn.lock
複製代碼

階段結語

  1. 至此整個構建過程和構建過程當中所作的優化點都已經大體論述完畢,固然不足之處還有一些。📚
  2. 離完整的工程還有不少工做要作,繼續吧!🔥👇🔥

⬆ back to top

16. 引入路由

前端單頁應用,路由必不可少,目前主流框架都有配套路由插件,這裏配合所選框架引入 react-router-dom

  • 安裝

    $ yarn add react-router-dom
    複製代碼
  • 新建路由配置文件夾

    $ cd src && mkdir router # 新建 router 文件夾
    $ cd router
    $ touch index.js         # 新建路由配置文件
    $ touch list.js          # 新建路由表文件
    複製代碼
  • 編寫路由配置及路由表


    • 路由配置 - src/router/index.js

      import React from 'react';
      import {
        BrowserRouter,
        Route,
        Switch,
        Redirect
      } from 'react-router-dom';
      import routes from './list';
      
      function RouterView(route) {
        return (
          <Route
            path={route.path}
            render={(props) => {
              if (route.redirect) {
                return <Redirect to={route.redirect} />;
              }
              return (
                <route.component
                  {...props}
                  render={() => (
                    <Switch>
                      {route.routes.map((children) => (
                        <RouterView key={children.path} {...children} />
                      ))}
                    </Switch>
                  )}
                />
              );
            }}
          />
        );
      }
      
      export default function Router() {
        return (
          <BrowserRouter>
            <Switch>
              {routes.map((route) => (
                <RouterView key={route.path} {...route} />
              ))}
            </Switch>
          </BrowserRouter>
        );
      }
      複製代碼

      這裏根據路由配置文檔編寫的,僅作 DEMO 使用;詳情參閱 react-router: Route Config

    • 路由表 - src/router/list.js

      import Github from '../views/Github/Github';
      import Setting from '../views/Setting/Setting';
      
      const routes = [
        {
          path: '/',
          exact: true,
          redirect: '/github'
        },
        {
          path: '/github',
          component: Github,
        },
        {
          path: '/setting',
          component: Setting,
        }
      ];
      
      export default routes;
      複製代碼

      這裏的命名你能夠隨意建立🙄

    ⬆ back to top

  • 新建 Setting、GitHub 頁面,並編寫

    # 新建 Setting、GitHub 頁面
    $ cd src/views
    $ mkdir Github && cd Github
    $ touch Github.js && touch Github.scss
    $ cd ..
    
    $ mkdir Setting && cd Setting
    $ touch Setting.js && touch Setting.scss
    $ cd ..
    複製代碼
    // starter/Github/Github.js
    import React from 'react';
    import { useHistory } from 'react-router-dom';
    import styles from './Github.scss';
    
    function Github() {
      const history = useHistory();
    
      function handleClick() {
        history.push('/setting');
      }
    
      return (
        <div className={`${styles.root}`}> <h1>Github</h1> <div className={`${styles.bg} ${styles.wh}`}> {`當前環境: ${process.env.NODE_ENV}`} </div> <button type='button' onClick={handleClick}> Go setting </button> </div>
      );
    }
    
    export default Github;
    
    // starter/Setting/Setting.js
    import React from 'react';
    import { useHistory } from 'react-router-dom';
    import styles from './Setting.scss';
    
    function Setting() {
      const history = useHistory();
    
      function handleClick() {
        history.push('/github');
      }
    
      return (
        <div className={`${styles.root}`}> <h1>Setting</h1> <div className={`${styles.bg} ${styles.wh}`}> {`當前環境: ${process.env.NODE_ENV}`} </div> <button type='button' onClick={handleClick}> Go github </button> </div>
      );
    }
    
    export default Setting;
    複製代碼
    // starter/Setting/Setting.scss
    .root {
      .wh {
        width: 200px;
        height: 180px;
      }
      .bg {
        text-align: center;
        line-height: 180px;
        background: no-repeat url('~assets/images/logo.png');
      }
    }
    // starter/Github/Github.scss
    .root {
      .wh {
        width: 200px;
        height: 200px;
      }
      .bg {
        text-align: center;
        line-height: 200px;
        background: no-repeat url('~assets/images/logo.png');
      }
    }
    複製代碼

    因爲樣式引入圖片,因此咱們新建資源存放文件夾,用來存放這些資源

    $ cd src && mkdir assets
    $ cd assets && mkdir images
    $ cd images
    $ copy logo.png # 這裏的圖標是官網摟過來的,🤣
    複製代碼
  • 修改咱們的主文件 src/index.js

    import { hot } from 'react-hot-loader';
    - import React, { useState } from 'react';
    + import React from 'react';
      import ReactDom from 'react-dom';
      import './style/global.css';
    - import styles from './index.scss';
    + import Router from './router/index';
    
    - const App = hot(module)(() => {
    - const reversedTitle = () =>
    - setTitle(
    - title
    - .split('')
    - .reverse()
    - .join('')
    - );
    - return (
    - <div className={styles.app}>
    - <h1>{title}</h1>
    - <button type='button' onClick={reversedTitle}>
    - reversed title!
    - </button>
    - </div>
    - );
    - });
    
    + const App = hot(module)(() => (
    + <div className='app'>
    + <Router />
    + </div>
    + ));
    
      ReactDom.render(<App />, document.getElementById('root'));
    複製代碼
  • 如今一切準備就緒,但在啓動項目以前,首先說明幾點

    1. ( 咱們在頁面內引入了圖片,隨着項目的增加後續可能會引入字體圖標、音頻等文件 ) 這裏咱們利用 webpack 幫咱們統一管理這些資源
    2. 隨着項目深刻,目錄結構也必將愈來愈複雜,咱們利用 webpack - resolve.alias, 建立 import 或 require 的別名,來確保模塊引入變得更簡單。

    作點改進吧️ ⚓️

    ⬆ back to top

17. 管理資源、優化模塊解析

  • 模塊解析

    <!-- starter/webpack.config.js -->
      ...
    
        resolve: {
          alias: {
            'react-dom': '@hot-loader/react-dom', // react-hot-loader 兼容 hook 寫法
    + '@': path.resolve(__dirname, 'src'),
    + assets: path.resolve(__dirname, 'src/assets'),
    + style: path.resolve(__dirname, 'src/style')
          }
        },
    
      ...
    複製代碼
  • 管理資源

    # 安裝
    
    $ yarn add -D url-loader  # 將文件轉換爲 base64 URI。
    $ yarn add -D file-loader # 將文件上的 import/require() 解析爲 url,並將該文件發射到輸出目錄中。
    複製代碼
    <!-- starter/webpack.config.js -->
    
      module: {
        rules: [
          ...
    
    + {
    + test: /\.(png|jpe?g|gif|webp)(\?.*)?$/, // 匹配這些格式的圖片
    + use: [
    + {
    + loader: 'url-loader',
    + options: {
    + limit: 4096, // 文件大小等於或大於限制,則將使用 file-loader。
    + fallback: {
    + loader: 'file-loader',
    + options: {
    + name: 'images/[name].[hash:8].[ext]'
    + }
    + }
    + }
    + }
    + ]
    + },
    + {
    + test: /\.(svg)(\?.*)?$/,
    + use: [
    + {
    + loader: 'file-loader',
    + options: {
    + name: 'svg/[name].[hash:8].[ext]'
    + }
    + }
    + ]
    + },
    + {
    + test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i,
    + use: [
    + {
    + loader: 'url-loader',
    + options: {
    + limit: 4096,
    + fallback: {
    + loader: 'file-loader',
    + options: {
    + name: 'fonts/[name].[hash:8].[ext]'
    + }
    + }
    + }
    + }
    + ]
    + },
    + {
    + test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
    + use: [
    + {
    + loader: 'url-loader',
    + options: {
    + limit: 4096,
    + fallback: {
    + loader: 'file-loader',
    + options: {
    + name: 'media/[name].[hash:8].[ext]'
    + }
    + }
    + }
    + }
    + ]
    + }
    + ]
      }
    複製代碼

    這裏咱們雖然沒有引入 svg、字體圖標文件、音頻文件,可是這裏爲了方便後續深刻,咱們索性把其配置添加。

  • 好了,啓動咱們的項目。Try it!

    $ yarn server
    複製代碼

    x

  • 打包

    $ yarn build
    
      # 結果
    
      $ cross-env NODE_ENV=production webpack --color --progress
      Hash: fb9c0cc487e7845fd915
      Version: webpack 4.41.2
      Time: 3533ms
      Built at: 2019-10-23 11:42:44
    
                                        Asset       Size              Chunks               Chunk Names
              chunks/vendors~main.64d1203b.js    160 KiB       1  [emitted] [immutable]    vendors~main
      chunks/vendors~main.64d1203b.js.LICENSE    1.01 KiB         [emitted]
    ! css/main.b7d00a9e.css 1.19 KiB 0 [emitted] [immutable] main
                    images/logo.581fa1d8.png     8.38 KiB         [emitted]
                                  index.html     667 bytes        [emitted]
    ! main.cfa18e59.js 3.95 KiB 0 [emitted] [immutable] main
    
      Entrypoint main = chunks/vendors~main.64d1203b.js css/main.b7d00a9e.css main.cfa18e59.js
    複製代碼
  • 問題與待優化點

    1. 隨着項目複雜度遞增,當打包構建應用時,JavaScript 包會變得很是大,影響頁面加載。若是咱們能把不一樣路由對應的組件分割成不一樣的代碼塊,而後當路由被訪問的時候才加載對應組件,這樣就更加高效了。

⬆ back to top

18. 路由懶加載 @loadable/component

注:使用該插件對應用進行代碼分割可以幫助你「懶加載」當前用戶所須要的內容,可以顯著地提升你的應用性能。儘管並無減小應用總體的代碼體積,但你能夠避免加載用戶永遠不須要的代碼,並在初始加載的時候減小所需加載的代碼量。

  • 安裝

    # 固然你也能夠選擇,React.lazy 和 Suspense,但他們還不支持服務端渲染。這裏直接選擇功能更增強大的 @loadable/component
    
    $ yarn add @loadable/component
    複製代碼
  • 修改路由表

    ! <!-- src/router/list -->
    
    - import Github from '@/views/Github/Github';
    - import Setting from '@/views/Setting/Setting';
    + import React from 'react';
    + import loadable from '@loadable/component';
    
    + const Github = import(/* webpackChunkName: "github" */ '@/views/Github/Github.js');
    + const Setting = import/* webpackChunkName: "setting" */ ('@/views/Setting/Setting.js');
    
    + const AsyncComponent = (loader) => loadable(loader, { fallback: <h3>Loading...</h3> });
    
      const routes = [
        {
          path: '/',
          exact: true,
          redirect: '/github'
        },
        {
          path: '/github',
    - component: Github
    + component: AsyncComponent(() => Github)
        },
        {
          path: '/setting',
    - component: Setting
    + component: AsyncComponent(() => Setting)
        }
      ];
    
      export default routes;
    複製代碼
  • 打包咱們的應用,看一看代碼分割結果

    $ yarn build
    
      # 結果
    
      $ cross-env NODE_ENV=production webpack --color --progress
      Hash: b93be70da668f4dff43b
      Version: webpack 4.41.2
      Time: 6077ms
      Built at: 2019-10-23 16:21:08
    
                                        Asset        Size                  Chunks            Chunk Names
    ! chunks/github.45dc6c0d.js 634 bytes 0 [emitted] [immutable] github
    ! chunks/setting.316d765f.js 637 bytes 2 [emitted] [immutable] setting
              chunks/vendors~main.a51021eb.js    164 KiB         3  [emitted] [immutable]     vendors~main
      chunks/vendors~main.a51021eb.js.LICENSE    1.01 KiB           [emitted]
    ! css/github.8de607a6.css 191 bytes 0 [emitted] [immutable] github
    ! css/setting.1a0bfbdd.css 195 bytes 2 [emitted] [immutable] setting
                        css/main.c1fb052e.css    830 bytes       1  [emitted] [immutable]        main
                    images/logo.581fa1d8.png     8.38 KiB           [emitted]
                                  index.html     667 bytes          [emitted]
                            main.02bdd0e7.js     4.96 KiB        1  [emitted] [immutable]        main
    
      Entrypoint main = chunks/vendors~main.a51021eb.js css/main.c1fb052e.css main.02bdd0e7.js
    複製代碼
  • 工程目錄

    └── starter
    + ├── dist
    + │ └── chunks
    + │ │ ├── github.45dc6c0d.js
    + │ │ ├── setting.316d765f.js
    + │ │ ├── vendors~main.a51021eb.js
    + │ │ └── vendors~main.a51021eb.js.LICENSE
    + ├── css
    + │ │ ├── 0.8de607a6.css
    + │ │ ├── 2.1a0bfbdd.css
    + │ │ └── main.c1fb052e.css
    + │ ├── images
    + │ │ └── logo.581fa1d8.png
      │   ├── index.html
    + │ └── main.02bdd0e7.js
      ├── node_modules
      ├── public
      │   ├── favicon.ico
      │   └── index.html
      ├── src
    + │ ├── assets
    + │ │ └── images
    + │ │ └── logo.png
    + │ ├── router
    + │ │ ├── index.js
    + │ │ └── list.js
      │   ├── style
      │   |   ├── global.css
      │   |   └── reset.css
    + | ├── views
    + │ | ├── Github
    + │ | │ ├── Github.js
    + │ | │ └── Github.scss
    + │ | └── Setting
    + │ | ├── Setting.js
    + │ | └── Setting.scss
    - │ ├── index.scss
      |   └──  index.js
      ├── postcss.config.js
      ├── webpack.config.js
      ├── package.json
      ├── README.md
      ├── LICENSE
      └── yarn.lock
    
    複製代碼

    到此,咱們已經把路由功能添加,繼續後續工做吧!🚘

⬆ back to top

19. 編碼規範

到目前爲止,咱們項目的代碼量愈來愈多了,寫的代碼可能還會存在一些潛在問題(這很難避免);再一個,一個大型項目每每是一個團隊在維護,團隊成員代碼風格卻不盡相同。基於此,咱們須要一個工具去解決這些痛點。

  • 工具

    • eslint: 經常使用於檢查常見的 JavaScript 代碼錯誤,也能夠進行代碼風格檢查。

    • stylelint: 強大的現代化 linter,可幫助您避免錯誤並在樣式中強制執行約定。

    • prettier: 代碼格式化工具,它經過解析代碼並使用本身的規則從新打印代碼,從而實現一致的樣式,並在必要時包裝代碼。


    論述完編碼規範的重要性,及工具鏈以後,咱們看看如何在項目中應用。

配置 eslint

  • 安裝

    $ yarn add -D eslint                          # eslint
    $ yarn add -D babel-eslint                    # 一個對 Babel 解析器的包裝,使其可以與 ESLint 兼容
    $ yarn add -D eslint-plugin-react             # 檢測 react 代碼
    $ yarn add -D eslint-plugin-react-hooks       # 用於檢測 hook 規則
    $ yarn add -D eslint-plugin-jsx-a11y          # 用於檢測 jsx 規範
    $ yarn add -D eslint-plugin-import            # ESLint 插件,帶有有助於驗證正確導入的規則。
    $ yarn add -D eslint-import-resolver-webpack  # 用於 eslint-plugin-import的 Webpack-literate 模塊解析插件。
    複製代碼
  • 新建 eslint 配置文件

    $ touch .eslintrc     # eslint 配置文件
    $ touch .eslintignore # eslint 忽略檢測配置文件
    複製代碼
    <!-- starter/.eslintrc -->
    
      {
        "root": true,
        "env": {
          "es6": true,
          "browser": true,
        },
        "extends": ["eslint:recommended", "plugin:react/recommended", "plugin:jsx-a11y/recommended"],
        "parser": "babel-eslint",
        "plugins": ["react", "jsx-a11y", "react-hooks", "import"],
        "rules": {
          "semi": ["error", "always"],
          "quotes": ["error", "single"],
          "camelcase": [0, { "properties": "never" }],
          "no-console": [2, { "allow": ["warn", "error"] }],
          "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
          "react/jsx-props-no-spreading": "off",
          "jsx-a11y/click-events-have-key-events": "off",
          "react-hooks/rules-of-hooks": "error",
          "react-hooks/exhaustive-deps": "warn",
          "react/no-unused-prop-types": "off"
        },
        "settings": {
          "react": {
            "version": "16.10.2"
          },
          "import/resolver": "webpack"
        },
        "globals": {
          "process": true,
          "module": true
        }
      }
    
    <!-- starter/.eslintignore -->
    
      node_modules
      dist
    複製代碼
  • 配置說明

    1. "eslint:recommended" 啓用推薦的規則
    2. "plugin:react/recommended" 該插件會導出建議的配置,以強制實施 React 的良好作法。
    3. "babel-eslint" 一個對 Babel 解析器的包裝,使其可以與 ESLint 兼容
    4. rules: 自定義規則,可覆蓋擴展配置。
    5. eslint-plugin-import 該插件旨在支持ES2015 +(ES6 +)導入/導出語法的檢查,並防止文件路徑和導入名稱拼寫錯誤的問題。
    6. "import/resolver": "webpack" :解決 webpack 別名配置致使的 eslint-plugin-import 報錯。
    7. 此配置是一份簡單的配置 詳細配置說明請參考 Configuring ESLint

    注:eslint 配置須要根據團隊內部去協定出一套行之有效的規範。

  • 修改 package.json 新建快捷命令

    <!-- starter/package.json -->
    
         "scripts": {
           "test": "echo \"Error: no test specified\" && exit 1",
           "server": "cross-env NODE_ENV=development webpack-dev-server --color --progress",
           "build": "cross-env NODE_ENV=production webpack --color --progress",
    + "lint:script": "eslint --ext '.js,.jsx' src",
    + "lint-fix:script": "npm run lint:script -- --fix"
         },
    複製代碼
  • 執行命令,查看是否存在不符合規則之處

    $ yarn lint:script     # 執行 lint
    $ yarn lint-fix:script # 執行 lint 並自動修復
    
    # 結果, 若是存在錯誤,則根據文檔自行修復。
    
    $ npm run lint:script -- --fix
    
    > starter@1.0.0 lint:script /Users/gt/LEMON/starter
    > eslint --ext '.js,.jsx' src "--fix"
    
    ✨  Done in 2.59s.
    複製代碼
  • 另外,咱們但願在每次轉譯js、jsx文件以前,執行 lint 格式化代碼

    # 安裝
    
    $ yarn add -D eslint-loader  # eslint loader (for webpack)
    複製代碼
    // 修改 webpack.config.js 配置
    
      ...
    
        module: {
          rules: [
    + {
    + test: /\.(js|jsx)$/,
    + exclude: /node_modules/,
    + include: path.resolve(__dirname, 'src'),
    + enforce: 'pre',
    + use: [
    + {
    + loader: 'eslint-loader',
    + options: {
    + cache: false,
    + fix: true
    + }
    + }
    + ]
    + },
            {
              test: /\.(js|jsx)$/,
              exclude: /node_modules/,
              loader: 'babel-loader'
            },
            ...
          ]
        }
    
      ...
    複製代碼

    測試一下吧. Try it 🚨


    ⬆ back to top


配置 stylelint

  • 安裝

    $ yarn add -D stylelint                    # 強大的現代化 linter,可幫助您避免錯誤並在樣式中強制執行約定。
    $ yarn add -D stylelint-config-recommended # Stylelint 的推薦可共享配置
    $ yarn add -D postcss-reporter             # 在控制檯中記錄 PostCSS 消息
    
    # $ yarn add -D stylelint-config-standard # Stylelint 的標準可共享配置
    # stylelint 插件經過 PostCSS 註冊警告 。所以,您須要用於打印警告的 PostCSS 運行器或插件,其目的是格式化和打印警告(例如 postcss-reporter)
    複製代碼
  • 新建 stylelint 配置文件

    $ touch .stylelintrc     # stylelint 配置文件
    複製代碼
    <!-- starter/.eslintrc -->
    # 你也可使用 stylint 推薦開啓的規則, 只需引入擴展推薦包便可。
    # 你也能夠 使用 rules 擴充規則或者覆蓋推薦規則,這取決於你!
    
    {
      "extends": "stylelint-config-recommended",
      "rules": {
        "indentation": 2,                              // 縮進
        "declaration-colon-space-after": "always",     // 在冒號聲明後須要一個空格或禁止使用空格。 a { color:pink } => a { color: pink }
        "declaration-colon-space-before": "never",     // 在冒號以前須要一個空格或禁止空格。 a { color : pink } => a { color: pink }
        "function-comma-space-after": "always",        // 在功能的逗號後面須要一個空格或不容許空格。 a { transform: translate(1,1) } => a { transform: translate(1, 1) }
        "function-url-quotes": "always",               // 要求或禁止使用網址引號 a { background: url(x.jpg) } => a { background: url("x.jpg") }
        "media-feature-colon-space-before": "never",   // 媒體功能中的冒號以前須要單個空格或不容許使用空格。@media (max-width :600px) {} => @media (max-width:600px) {}
        "media-feature-name-no-vendor-prefix": true,   // 禁止使用媒體功能名稱的供應商前綴。@media (-webkit-min-device-pixel-ratio: 1) {} => @media (min-resolution: 96dpi) {}
        "max-empty-lines": 5,                          // 限制相鄰的空行數。
        "number-leading-zero": "never",                // 小數部分小於或等於1的前導零。a { line-height: 0.5; } => a { line-height: .5; }
        "number-no-trailing-zeros": true,              // 禁止數字尾隨零。a { top: 1.0px } => a { top: 1px }
        "at-rule-semicolon-newline-after": "always",   // 規則後的分號換行符 @import url("x.css"); a {} => @import url("x.css");\n a {}
        "selector-list-comma-space-before": "never",   // 選擇器列表的逗號前須要一個空格或不容許空格 a ,b { color: pink; } => a, b { color: pink; }
        "selector-list-comma-newline-after": "always", // 選擇器列表的逗號後須要換行符或不容許使用空格。a, b { color: pink; } => a,\n b { color: pink; }
        "string-quotes": "single",                     // 在字符串周圍指定單引號或雙引號。 a { content: 「x」; } => a { content: 'x'; }
      }
    }
    複製代碼
  • 擴展共享配置及規則表

  • 添加快捷命令

    <!-- starter/package.json -->
    
         "scripts": {
           "test": "echo \"Error: no test specified\" && exit 1",
           "server": "cross-env NODE_ENV=development webpack-dev-server --color --progress",
           "build": "cross-env NODE_ENV=production webpack --color --progress",
           "lint:script": "eslint --ext '.js,.jsx' src",
           "lint-fix:script": "npm run lint:script -- --fix",
    + "lint:style": "stylelint 'src/**/*.css' 'src/**/*.scss' --syntax scss",
    + "lint-fix:style": " npm run lint:style -- --fix",
         },
    複製代碼
  • 配置 postcss-reporter

    在控制檯中記錄 PostCSS 消息

    <!-- starter/postcss.config.js -->
    
      module.exports = {
        plugins: {
          autoprefixer: {},
    + 'postcss-reporter': {
    + clearReportedMessages: true, # 插件將在記錄結果消息後清除它們。這樣能夠防止其餘插件或您使用的任何運行程序再次記錄相同的信息並引發混亂。
    + throwError: true # 在插件記錄您的消息後,若是發現任何警告,它將引起錯誤。
    + },
        }
      };
    複製代碼
  • 執行命令,查看是否存在不符合規則之處

    $ yarn lint:style     # 格式化 style
    $ yarn lint-fix:style # 格式化 style 並自動修復
    
    # 結果
    
    $  npm run lint:style -- --fix
    
    > starter@1.0.0 lint:style /Users/gt/LEMON/starter
    > stylelint 'src/**/*.css' 'src/**/*.scss' --syntax scss "--fix"
    
    src/style/reset.css
    54:1  ✖  Expected selector "h1" to come before selector "h1:first-child"   no-descending-specificity
    54:1  ✖  Expected selector "h1" to come before selector "h1:last-child"    no-descending-specificity
    58:1  ✖  Expected selector "h2" to come before selector "h2:first-child"   no-descending-specificity
    58:1  ✖  Expected selector "h2" to come before selector "h2:last-child"    no-descending-specificity
    62:1  ✖  Expected selector "h3" to come before selector "h3:first-child"   no-descending-specificity
    62:1  ✖  Expected selector "h3" to come before selector "h3:last-child"    no-descending-specificity
    66:1  ✖  Expected selector "h4" to come before selector "h4:first-child"   no-descending-specificity
    66:1  ✖  Expected selector "h4" to come before selector "h4:last-child"    no-descending-specificity
    67:1  ✖  Expected selector "h5" to come before selector "h5:first-child"   no-descending-specificity
    67:1  ✖  Expected selector "h5" to come before selector "h5:last-child"    no-descending-specificity
    68:1  ✖  Expected selector "h6" to come before selector "h6:first-child"   no-descending-specificity
    68:1  ✖  Expected selector "h6" to come before selector "h6:last-child"    no-descending-specificity
    
    # no-descending-specificity 禁止較低特異性的選擇器在覆蓋較高特異性的選擇器以後出現。
    # 根據規則表修復 reset.css 文件
    
    # 再次運行,結果:
    
    $  npm run lint:style -- --fix
    
    > starter@1.0.0 lint:style /Users/gt/LEMON/starter
    > stylelint 'src/**/*.css' 'src/**/*.scss' --syntax scss "--fix"
    
    ✨  Done in 1.96s.
    複製代碼

    測試一下吧. Try it 💄


    ⬆ back to top


配置 prettier

  • 安裝

    $ yarn add -D prettier
    $ yarn add -D eslint-plugin-prettier # 將 Prettier 做爲 ESLint 規則運行,並將差別報告爲單個ESLint問題
    
    $ yarn add -D eslint-config-prettier # 關閉全部沒必要要的或可能與 Prettier 衝突的規則。
    $ yarn add -D stylelint-config-prettier # 禁用與 Prettier 衝突的規則的配置
    複製代碼

    關於這些禁用規則,請參考 eslint-config-prettier#special-rules, stylelint-config-prettier special-rules

  • 在 eslint 配置中擴展 prettier

    <!-- starter/.eslintrc -->
    
      {
        ...
    
    - "extends": ["eslint:recommended", "plugin:react/recommended", "plugin:jsx-a11y/recommended"],
    + "extends": [
    + "eslint:recommended",
    + "plugin:react/recommended",
    + "plugin:jsx-a11y/recommended",
    + "plugin:prettier/recommended",
    + "prettier/react"
    + ],
    
        ...
    
      }
    
    
    <!-- 說明 -->
    
    "plugin:prettier/recommended" does three things:
    
      1. Enables eslint-plugin-prettier.
      2. Sets the prettier/prettier rule to "error".
      3. Extends the eslint-config-prettier configuration.
    
    "prettier/react"
    
      爲了支持特殊的 ESLint 插件(eslint-plugin-react)所添加額外的排除項
    複製代碼

    固然,你能夠在 .prettierrc 文件中設置 Prettier 本身的選項。

  • 新建 prettier 配置文件

    $ touch .prettierrc     # prettier 配置文件
    複製代碼
    <!-- starter/.prettierrc -->
    
      {
        "semi": true,
        "singleQuote": true,
        "trailingComma": 'all',
      }
    
    複製代碼
  • 在 stylelint 配置中擴展 prettier

    <!-- starter/.stylelintrc -->
    
      {
        ...
    
    - "extends": "stylelint-config-recommended",
    + "extends": [
    + "stylelint-config-recommended",
    + "stylelint-config-prettier"
    + ],
    
        ...
    
      }
    複製代碼
  • 說明

    • 上述咱們在擴展 eslint、stylelint 配置都是爲了整合工具並把它們集成在一塊兒。因此你看到的處理是,禁用了其它 linter 中可能與 Prettier 但願格式化代碼的方式衝突的全部現有格式化規則

  • 添加快捷命令行

    <!-- starter/package.json -->
    
      ...
    
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "server": "cross-env NODE_ENV=development webpack-dev-server --color --progress",
        "build": "cross-env NODE_ENV=production webpack --color --progress",
        "lint:script": "eslint --ext '.js,.jsx' src",
        "lint-fix:script": "npm run lint:script -- --fix",
        "lint:style": "stylelint 'src/**/*.css' 'src/**/*.scss' --syntax scss",
        "lint-fix:style": " npm run lint:style -- --fix",
    + "prettier": "prettier --check --write 'src/**/*.{js,jsx,scss,css}' --config ./.prettierrc"
      },
    
      ...
    複製代碼

    更多參數請參考 Prettier CLI

  • 運行命令,格式化代碼

    $ yarn prettier
    
    # 結果, 它幫你格式化的代碼以下
    
    $ prettier --check --write './src/**/*.js' './src/**/*.jsx'
    
    Checking formatting...
    
    src/index.js
    src/router/index.js
    src/router/list.js
    src/views/Github/Github.js
    src/views/Setting/Setting.js
    
    Code style issues fixed in the above file(s).
    ✨  Done in 0.79s.
    複製代碼

    ⬆ back to top


配置 Husky

Git鉤子腳本對於在提交代碼審查以前識別簡單問題頗有用。咱們在每次提交時都運行鉤子,以自動指出代碼中的問題,例如缺乏分號,尾隨空白和調試語句。經過在代碼審閱以前指出這些問題,一來,能夠確保沒有錯誤進入存儲庫;二來,代碼審閱者能夠專一於更改的體系結構,而不會由於瑣碎的風格問題而浪費時間。

  • 安裝

    $ yarn add -D husky # 🐶 Git hooks made easy
    $ yarn add -D lint-staged # 對暫存的 git 文件運行 linters,不要讓💩進入您的代碼庫!
    複製代碼
  • 配置

    <!-- starter/package.json -->
    
      {
        "scripts": {
          "test": "echo \"Error: no test specified\" && exit 1",
          "server": "cross-env NODE_ENV=development webpack-dev-server --color --progress",
          "build": "cross-env NODE_ENV=production webpack --color --progress",
    + "lint": "npm run lint:style && npm run lint:script",
    + "lint-fix": "npm run lint-fix:style && npm run lint-fix:script",
          "lint:script": "eslint --ext '.js,.jsx' src",
          "lint-fix:script": "npm run lint:script -- --fix",
          "lint:style": "stylelint 'src/**/*.css' 'src/**/*.scss' --syntax scss",
          "lint-fix:style": " npm run lint:style -- --fix",
          "prettier": "prettier --check --write 'src/**/*.{js,jsx,scss,css}' --config ./.prettierrc"
        },
    + "husky": {
    + "hooks": {
    + "pre-commit": "lint-staged"
    + }
    + },
    + "lint-staged": {
    + "src/**/*.{js, jsx, css, scss}": [
    + "npm run prettier",
    + "npm run lint-fix",
    + "git add"
    + ]
    + }
      }
    複製代碼

    推個代碼測試一下吧! Try it! 🎊🎊


    ⬆ back to top


  • 題外話:commit changelog 規範

    # feat: 添加新功能(feature)
    # fix : 修復 bug
    # docs: 文檔(documentation)
    # style: 樣式及代碼格式化等不涉及邏輯的改動點
    # refactor: 重構
    # test: 添加測試用例
    # chore: 構建過程或輔助工具的變更
    
    # 這裏推薦一個 lint 插件 commitlint。可根據須要添加
    # 詳細參考:https://github.com/conventional-changelog/commitlint
    
    # 關於 commit 信息編寫的更多規範指南
    # 請參考:http://www.ruanyifeng.com/blog/2016/01/commit_message_change_log.html
    複製代碼

    ⬆ back to top


    到此編碼規範的內容基本陳述完畢,說的東西有限、具體如何配置取決於你或你的團隊要求! Go 🚠

20. 完善應用

爲了接下來更好的論述,咱們來完成一個小需求。

x

  • 由草圖需求改造咱們的項目

    • 改造路由表

      import React from 'react';
      import loadable from '@loadable/component';
      import Loading from '@/components/Loading/Loading';
      
      const BottomTabNavigator = import(
        /* webpackChunkName: "bottom-tab-navigator" */ '@/components/BottomTabNavigator/BottomTabNavigator'
      );
      const Empty = import(
        /* webpackChunkName: 'not-found' */ '@/components/Empty/Empty'
      );
      const Github = import(/* webpackChunkName: "github" */ '@/views/Github/Github');
      const Setting = import(
        /* webpackChunkName: "setting" */ '@/views/Setting/Setting'
      );
      
      const AsyncComponent = loader => loadable(loader, { fallback: <Loading /> }); const routes = [ { path: '/', exact: true, redirect: '/dashboard/github', }, { path: '/dashboard', component: AsyncComponent(() => BottomTabNavigator), routes: [ { path: '/dashboard/github', component: AsyncComponent(() => Github), }, { path: '/dashboard/setting', component: AsyncComponent(() => Setting), }, ], }, { path: '*', component: AsyncComponent(() => Empty), }, ]; export default routes; 複製代碼
    • 改造 Github 頁面

      /* * 路徑: starter/src/views/Github * 說明: * RepositoriesCard 根據草圖編寫的倉庫信息卡片 * Loading 加載態組件 * Empty 空數據態組件 * useRequest 自定義 hook,用於包裝請求 * searchRepositories 統一 API 請求封裝 * * 提示: 說明涉及到的組件,能夠參考項目;你也能夠本身實現,這不重要。 */
      
      import React from 'react';
      import styles from './Github.scss';
      import RepositoriesCard from '@/components/RepositoriesCard/RepositoriesCard';
      import Loading from '@components/Loading/Loading';
      import Empty from '@components/Empty/Empty';
      import useRequest from '@/containers/useRequest';
      import { searchRepositories } from '@/services/api/github';
      
      function Github() {
        const [loading, data] = useRequest(searchRepositories, { q: 'javascript' });
      
        if (loading === true) {
          return <Loading />
        }
      
        return (
          <div className={styles.root}>
            {(data && data.items.map(
              ({
                description,
                id,
                name,
                forks_count,
                stargazers_count,
                language,
                owner
              }) => (
                <RepositoriesCard
                  key={id}
                  name={name}
                  avatarUrl={owner.avatar_url}
                  description={description}
                  stargazersCount={stargazers_count}
                  forksCount={forks_count}
                  language={language}
                />
              )
            ))
              || <Empty />}
          </div>
        );
      }
      
      export default Github;
      複製代碼

    • 改造 Setting 頁面(不改造 😜)


    咱們在改造 Github 頁面, 在組件內部調用了請求方法,並對請求作了統一封裝,在繼續改造工做以前,咱們先來看看 先後端交互

⬆ back to top

21. 先後端交互 Axios

x

  • 安裝

    $ yarn add axios # Promise based HTTP client for the browser and node.js
    複製代碼
  • 新建相關文件

    # 新建 services 文件夾
    
    $ cd src && mkdir services
    $ cd services && touch index.js   # 基於 axios 簡單封裝
    $ mkdir interface && cd interface # 用於存在項目全部接口
    $ touch github.js                 # 用於存放 GitHub 相關請求
    複製代碼
  • 基於 axios 簡單封裝 src/services/index.js

    /** * 說明: AXIOS_DEFAULT_OPTIONS 默認配置,詳細參考 utils * * 注: 如下封裝僅僅簡單包裝一層,你也能夠本身實現。 */
    import axios from 'axios';
    import constants from '@/utils/constants';
    
    // 使用自定義配置新建一個 axios 實例
    const instance = axios.create(constants.AXIOS_DEFAULT_OPTIONS);
    
    // 請求攔截器
    instance.interceptors.request.use(
      (AxiosRequsetConfig) => AxiosRequsetConfig, // 在發送請求以前作些什麼
      (error) => Promise.reject(error) // 對請求錯誤作些什麼
    );
    
    // 響應攔截器
    instance.interceptors.response.use(
      (AxiosResponse) => AxiosResponse, // 對響應數據作點什麼
      (error) => Promise.reject(error) // 對響應錯誤作點什麼, 如,處理一些鑑權類問題
    );
    
    export default function (options = {}, customConfig = {}) {
      return new Promise((resolve, reject) => {
        const finalConfig = Object.assign(options, customConfig);
        instance(finalConfig)
          .then(({ data }) => {
            if (data) {
              return resolve(data);
            }
            return reject(new Error('Request return result exception!'));
          })
          .catch((reason) => reject(reason));
      });
    }
    複製代碼
  • 業務接口層 src/services/interface/github.js

    import network from '../index';
    
    /** * @desc 搜索倉庫 * * @param {Object} data 請求參數 * @returns {Promise} */
    export const searchRepositories = (data = {}) => network({
      url: '/search/repositories',
      params: data
    });
    複製代碼

    上述簡單封裝核心請求方法,分離接口等,主要目的是輔助論述,固然,這還很簡單,你能夠本身根據實際須要作更全面的封裝!

⬆ back to top

22. 項目改造 - 組件

UI Component

  • 準則

    1. 最基礎的組件形式,如:按鈕、標籤。
    2. 無狀態
    3. 純靜態展現做用
    4. 組成的基本結構(props + render)
    5. 不須要依賴生命週期
    6. 單一職責,多處複用。
  • 樣例

    import React from 'react';
    import PropTypes from 'prop-types';
    
    const UI = ({ title }) => {
      return (
        <div className="UI"> { title } </div>
      );
    };
    
    UI.propTypes = {
      title: PropTypes.string,
    };
    
    UI.defaultProps = {
      title: 'UI Component !',
    };
    
    export default UI;
    複製代碼

Container Component

  • 準則

    1. 單一職責原則,下降組件的耦合度
    2. 提供數據( 如:包含 Ajax 返回數據處理邏輯 )
    3. 與狀態管理工具交互( 如:包含 Redux 注入邏輯 )
    4. 有狀態
    5. 樣式及 DOM 較少
  • 樣例

    import { connect } from 'react-redux';
    import Demo from 'components/Demo/Demo';
    import {
      incrementEnthusiasm,
      decrementEnthusiasm
    } from 'actions/index';
    
    export function mapStateToProps({ enthusiasm }) {
      return {
        enthusiasm,
      };
    }
    
    export function mapDispatchToProps(dispatch) {
      return {
        onIncrement: () => dispatch(actions.incrementEnthusiasm()),
        onDecrement: () => dispatch(actions.decrementEnthusiasm()),
      };
    }
    
    export default connect(mapStateToProps, mapDispatchToProps)(Demo);
    複製代碼

Tip: 因爲對 react 不是很熟,故談的比較簡單,這裏推薦參考:Presentational and Container Components編寫有彈性的組件

⬆ back to top

23. 項目改造 - 移動端適配

這裏咱們直接引入 postcss-px-to-viewport 插件。

  • 安裝

    $ yarn add -D postcss-px-to-viewport`
    複製代碼
  • 配置

    <!-- starter/postcss-config.js -->
    
      module.exports = {
        plugins: {
          autoprefixer: {}
        },
        'postcss-reporter': {
          clearReportedMessages: true,
          throwError: true
        },
    + 'postcss-px-to-viewport': {
    + viewportWidth: 375, // 設計稿的視口寬度
    + viewportHeight: 812, // 設計稿的視口高度
    + unitPrecision: 5, // 單位轉換後保留的精度
    + viewportUnit: 'vw', // 但願使用的視口單位
    + fontViewportUnit: 'vw', // 字體使用的視口單位
    + selectorBlackList: ['.ignore', '.hairlines'], // 須要忽略的CSS選擇器,不會轉爲視口單位,使用原有的px等單位。
    + minPixelValue: 1, // 設置最小的轉換數值,若是爲1的話,只有大於1的值會被轉換
    + mediaQuery: false, // 媒體查詢裏的單位是否須要轉換單位
    + exclude: [/node_modules/] // 須要排除的
    + }
      };
    複製代碼
  • 運行項目,看看效果!

    $ yarn server # 運行項目
    
      # 結果
    
      $ cross-env NODE_ENV=development webpack-dev-server --color --progress
      10% building 1/1 modules 0 activeℹ 「wds」: Project is running at http://localhost:3000/
      ℹ 「wds」: webpack output is served from /
      ℹ 「wds」: Content not from webpack is served from /Users/mr.lemon/cl/CODE_CL/REACT/starter/public
      ℹ 「wds」: 404s will fallback to /index.html
      ℹ 「wdm」: Compiled successfully.
    複製代碼

    x


    🔥 Good job!🎉 🔥


    查看現階段完整工程目錄
    └── starter
      ├── dist
      │   ├── chunks
      │   │   ├── bottom-tab-navigator.77d17027.js
      │   │   ├── github.4e7f6c35.js
      │   │   ├── not-found.638dbdfc.js
      │   │   ├── setting.bc3fbe14.js
      │   │   ├── vendors~github.7acdaa67.js
      │   │   ├── vendors~github.7acdaa67.js.LICENSE
      │   │   ├── vendors~main.b1a4bdbf.js
      │   │   └── vendors~main.b1a4bdbf.js.LICENSE
      │   ├── css
      │   │   ├── bottom-tab-navigator.25c0dead.css
      │   │   ├── github.866c72ba.css
      │   │   ├── main.45091b7c.css
      │   │   ├── not-found.d566b1be.css
      │   │   └── setting.2b60ef7c.css
      │   ├── fonts
      │   │   ├── iconfont.63765329.woff
      │   │   ├── iconfont.c2eabadd.ttf
      │   │   └── iconfont.cad7bb52.eot
      │   ├── images
      │   │   ├── empty-data.788c1924.png
      │   │   ├── logo.581fa1d8.png
      │   │   └── webpage-lost.a02f7942.png
      │   ├── svg
      │   │   └── iconfont.1247822e.svg
      │   ├── main.a010b425.js
      │   └── index.html
      ├── node_modules
      |   ├──  ...
      |   └──  ...
      ├── public
      │   ├── favicon.ico
      │   └── index.html
      ├── src
      │   ├── assets
      │   │   ├── font
      │   │   │   ├── iconfont.css
      │   │   │   ├── iconfont.eot
      │   │   │   ├── iconfont.svg
      │   │   │   ├── iconfont.ttf
      │   │   │   └── iconfont.woff
      │   │   └── images
      │   │       ├── empty-data.png
      │   │       ├── logo.png
      │   │       └── webpage-lost.png
      │   ├── components
      │   │   ├── BottomTabNavigator
      │   │   │   ├── BottomTabNavigator.js
      │   │   │   ├── BottomTabNavigator.scss
      │   │   │   └── index.zh-CN.md
      │   │   ├── Circle
      │   │   │   ├── Circle.js
      │   │   │   ├── Circle.scss
      │   │   │   └── index.zh-CN.md
      │   │   ├── Empty
      │   │   │   ├── Empty.js
      │   │   │   ├── Empty.scss
      │   │   │   └── index.zh-CN.md
      │   │   ├── Loading
      │   │   │   ├── Loading.js
      │   │   │   ├── Loading.scss
      │   │   │   └── index.zh-CN.md
      │   │   ├── README.md
      │   │   └── RepositoriesCard
      │   │       ├── RepositoriesCard.js
      │   │       ├── RepositoriesCard.scss
      │   │       └── index.zh-CN.md
      │   ├── containers
      │   │   ├── README.md
      │   │   └── useRequest.js
      │   ├── index.js
      │   ├── router
      │   │   ├── index.js
      │   │   └── list.js
      │   ├── services
      │   │   ├── index.js
      │   │   └── interface
      │   │       └── github.js
      │   ├── style
      │   │   ├── global.css
      │   │   ├── reset.css
      │   │   └── variable.scss
      │   ├── utils
      │   │   ├── constants.js
      │   │   ├── enume.js
      │   │   └── tools.js
      │   └── views
      │       ├── Github
      │       │   ├── Github.js
      │       │   └── Github.scss
      │       └── Setting
      │           ├── Setting.js
      │           └── Setting.scss
      ├── webpack.config.js
      ├── postcss.config.js
      ├── package.json
      ├── LICENSE
      ├── README.md
      └── yarn.lock
    複製代碼

項目改造到此已基本完成,但後續仍然還有工做要作 💊😯。繼續吧!

⬆ back to top

24. 先後端分離 mock

先後端分離,讓前端脫離後臺獨立開發,mock 起了很大的做用。在實際業務開發中,咱們須要一種能不侵入現有代碼,便可攔截請求,返回模擬數據。 咱們利用 json-server 幫助咱們完成這個需求。

  • 安裝

    $ yarn add -D json-server
    複製代碼
  • 新建 mock 文件夾

    $ mkdir mock
    $ cd mock && touch index.js
    $ mkdir interface && cd interface
    $ touch index.js && touch github.js
    複製代碼
    // starter/mock/index.js
    const data = require('./interface/index');
    module.exports = function Mock() {
      return data;
    };
    
    // starter/mock/interface/index.js
    const github = require('./github');
    module.exports = {
      ...github,
    };
    
    // starter/mock/interface/github.js
    const repositories = {
      "items": [
        {
          "id": 6498492,
          "name": "javascript",
          "full_name": "airbnb/javascript",
          "owner": {
            "login": "airbnb",
            "id": 698437,
            "avatar_url": "https://avatars3.githubusercontent.com/u/698437?v=4",
          },
          "description": "JavaScript Style Guide",
          "size": 3002,
          "stargazers_count": 89966,
          "watchers_count": 89966,
          "language": "JavaScript",
          "forks_count": 17404,
          "open_issues_count": 110,
          "license": {
            "key": "mit",
            "name": "MIT License",
          },
          "forks": 17404,
          "open_issues": 110,
          "watchers": 89966,
          "default_branch": "master",
          "score": 151.055
        },
        {
          "id": 18286232,
          "name": "javascript",
          "full_name": "GitbookIO/javascript",
          "private": false,
          "owner": {
            "login": "GitbookIO",
            "id": 7111340,
            "avatar_url": "https://avatars0.githubusercontent.com/u/7111340?v=4",
          },
          "description": "GitBook teaching programming basics with Javascript",
          "size": 1267,
          "stargazers_count": 1923,
          "watchers_count": 1923,
          "language": "javascript",
          "forks_count": 730,
          "open_issues_count": 43,
          "license": {
            "key": "apache-2.0",
            "name": "Apache License 2.0",
          },
          "forks": 730,
          "open_issues": 43,
          "watchers": 1923,
          "default_branch": "master",
          "score": 104.4313
        },
      ]
    }
    
    module.exports = {
      repositories
    };
    複製代碼

    數據來源於 GitHub ,這裏只作演示,故直接貼出數據。若是你須要動態生成數據,能夠引入 mockjs 幫助你生成數據。這裏就不作贅述了!

  • 配置快捷命令 starter/package.json

    ...
      {
        "scripts": {
          "test": "echo \"Error: no test specified\" && exit 1",
          "server": "cross-env NODE_ENV=development webpack-dev-server --color --progress",
    + "server:mock": "npm run mock & cross-env NODE_ENV=development MOCK=true webpack-dev-server --color --progress",
    + "mock": "json-server mock/index.js --watch --port 3001",
          "build": "cross-env NODE_ENV=production webpack --color --progress",
          "lint": "npm run lint:style && npm run lint:script",
          "lint-fix": "npm run lint-fix:style && npm run lint-fix:script",
          "lint:script": "eslint --ext '.js,.jsx' src",
          "lint-fix:script": "npm run lint:script -- --fix",
          "lint:style": "stylelint 'src/**/*.css' 'src/**/*.scss' --syntax scss",
          "lint-fix:style": " npm run lint:style -- --fix",
          "prettier": "prettier --check --write 'src/**/*.{js,jsx,scss,css}' --config ./.prettierrc"
        },
      }
      ...
    複製代碼
  • 配置 dev-server 代理

    # 新建代理文件
    
    $ cd ../.. && mkdir config
    $ cd config && touch proxy.js
    複製代碼
    // starter/config/proxy.js
    
    /** * @desc mock 服務代理配置 */
    const MOCK_SERVER_PROXY = {
      '/search/*': {
        target: 'http://localhost:3001/$1',
      }
    }
    
    /** * @desc 默認服務代理 */
    const DEFAULT_PROXY = {};
    
    /** * @desc dev-server 代理配置 * @param {Boolean} IS_MOCK mock 標識 * @param {Object} Proxy */
    module.exports = function({ IS_MOCK }) {
      if (IS_MOCK) return MOCK_SERVER_PROXY;
      return DEFAULT_PROXY;
    }
    複製代碼

    具體如何配置代理,根據接口自定!更多請參考 devServer - proxy

    # starter/webpack.config.js
    
      ...
    + const IS_MOCK = process.env.MOCK === 'true';
    + const filterProxy = require('./config/proxy');
    
        ...
    
          baseConfig.devServer = {
            ...
    + proxy: filterProxy({ IS_MOCK })
          }
    
      ...
    複製代碼
  • 運行項目

    $ yarn server:mock
    
    # 結果
    
    $ npm run mock & cross-env NODE_ENV=development MOCK=true webpack-dev-server --color --progress
    
    > starter@1.0.0 mock /Users/mr.lemon/cl/CODE_CL/REACT/starter
    > json-server mock/index.js --watch --port 3001
    
      \{^_^}/ hi!
    
      Loading mock/index.js
      Done
    
      Resources
      http://localhost:3001/repositories
    
      Home
      http://localhost:3001
    
      Type s + enter at any time to create a snapshot of the database
      Watching...
    
    10% building 1/1 modules 0 activeℹ 「wds」: Project is running at http://192.168.0.102:3000/
    ℹ 「wds」: webpack output is served from /
    ℹ 「wds」: Content not from webpack is served from /Users/mr.lemon/cl/CODE_CL/REACT/starter/public
    ℹ 「wds」: 404s will fallback to /index.html
    ℹ 「wdm」: Compiled successfully.
    複製代碼

    x
    x

以上僅僅闡述了 mock 這一環, 關於先後端分離這裏推薦一個知乎問答 Web 先後端分離的意義大嗎?

⬆ back to top

25. 單元測試 jest

單元測試是用來對一個模塊、一個函數或者一個類來進行正確性檢驗的測試工做。業內優秀的測試框架不少,這裏直接選擇 jest

  • 安裝

    $ yarn add -D jest                    # Jest is a delightful JavaScript Testing Framework with a focus on simplicity.
    $ yarn add -D babel-jest              # Jest plugin to use babel for transformation
    $ yarn add -D enzyme                  # 一種用於 React 的 JavaScript 測試實用程序,能夠更輕鬆地測試 React 組件的輸出。您還能夠操縱,遍歷並以某種方式模擬給定輸出的運行時。
    $ yarn add -D enzyme-adapter-react-16 # react 16 適配器
    $ yarn add -D identity-obj-proxy      # 模擬一個代理以啓用 className 查找
    複製代碼
  • 新建用於存放測試用例的文件夾及 jest 配置文件

    $ touch jest.config.js
    $ cd src && mkdir __tests__
    $ cd __tests__
    $ mkdir __mocks__ && mkdir ui && touch setup.js
    $ cd __mocks__ && touch fileMock.js
    $ cd ../ui && touch Loading.spec.js
    複製代碼
  • 配置 jest

    <!-- starter/jest.config.js -->
    
    module.exports = {
      testRegex: '(\\.)(test|spec)(\\.)jsx?$',
      // 處理靜態文件
      // 樣式表和圖像等,這些文件在測試中無足輕重,由於咱們能夠安全地 mock 他們。
      // 模擬 CSS 模塊,用類名查找模擬一個代理
      moduleNameMapper: {
        '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
          '<rootDir>/src/__tests__/__mocks__/fileMock.js',
        '\\.(css|scss|sass)$': 'identity-obj-proxy',
        '^@/(.*)$': '<rootDir>/src/$1'
      },
      // 爲轉換源文件提供同步功能的模塊
      transform: {
        '^.+\\.(js|jsx)$': 'babel-jest'
      },
      // 在每次測試以前配置或設置測試環境
      setupFiles: ['<rootDir>/src/__tests__/setupTests.js']
    };
    
    <!-- starter/src/__tests__/__mocks__/fileMock.js --> module.exports = 'test-file-stub'; 複製代碼
  • 註冊 enzyme 適配器配置

    // starter/src/__tests__/setup.js
    
    import enzyme from 'enzyme';
    import Adapter from 'enzyme-adapter-react-16';
    
    enzyme.configure({ adapter: new Adapter() });
    複製代碼
  • 配置快捷運行命令

    <!-- starter/package.json -->
    
      {
        ...
    
        "scripts": {
    - "test": "echo \"Error: no test specified\" && exit 1",
    + "test": "jest --config jest.config.js --no-cache",
          "server": "cross-env NODE_ENV=development webpack-dev-server --color --progress",
          "server:mock": "npm run mock & cross-env NODE_ENV=development MOCK=true webpack-dev-server --color --progress",
          "mock": "json-server mock/index.js --watch --port 3001",
          "build": "cross-env NODE_ENV=production webpack --color --progress",
          "lint": "npm run lint:style && npm run lint:script",
          "lint-fix": "npm run lint-fix:style && npm run lint-fix:script",
          "lint:script": "eslint --ext '.js,.jsx' src",
          "lint-fix:script": "npm run lint:script -- --fix",
          "lint:style": "stylelint 'src/**/*.css' 'src/**/*.scss' --syntax scss",
          "lint-fix:style": " npm run lint:style -- --fix",
          "prettier": "prettier --check --write 'src/**/*.{js,jsx,scss,css}' --config ./.prettierrc"
        },
    
        ...
    
      }
    複製代碼
  • 編寫測試用例

    import React from 'react';
    import { shallow } from 'enzyme';
    import Loading from '../../components/Loading/Loading';
    
    describe('Loading 組件基礎測試組合!', () => {
      it('<Loading /> 組件默認標題應該是 "loading..."', () => {
        const loading = shallow(<Loading />);
        expect(loading.find('span').text()).toBe('loading...');
      });
      it('<Loading /> 組件標題應該是 "加載中..."', () => {
        const loading = shallow(<Loading title='加載中...' />);
        expect(loading.find('span').text()).toBe('加載中...');
      });
    });
    複製代碼

    這裏的用例只作演示,在實際開發中要嚴格根據 UI 組件的功能編寫用例。

  • 運行測試

    $ yarn test
    
    # 結果
    
    $ jest --config jest.config.js --no-cache
     PASS  src/__tests__/ui/Loading.spec.js
      Loading 組件基礎測試組合!
        ✓ <Loading /> 組件默認標題應該是 "loading..." (7ms)
        ✓ <Loading /> 組件標題應該是 "加載中..." (1ms)
    
    Test Suites: 1 passed, 1 total
    Tests:       2 passed, 2 total
    Snapshots:   0 total
    Time:        1.66s
    Ran all test suites.
    ✨  Done in 2.44s.
    複製代碼
  • 說明

    1. 編寫測試用例很重要!以上僅僅論述瞭如何接入 jest 具體根據實際需求去寫。
    2. 建議集中在私有工具函數及 UI 組件;至於業務,變更性太大就不建議寫了!
    3. 關於測試用例,可參考行業內一些UI組件庫,如:element-UIantd
    4. 推薦一篇文章 前端單元測試實踐

    try it! 🍁

⬆ back to top

26. 部署上線

分析資源包

在實際項目中,考慮到前端性能(首屏加載,全屏加載,白屏時間)都會對打出的資源包進行分析,而後採起相應的方案進行優化。咱們在前面論述打包構建時,已經在多個方面進行說明了,這裏不在贅述。

  • Webpack Bundle Analyzer

    # 安裝
     $ yarn add -D webpack-bundle-analyzer # 交互式可縮放樹圖可視化webpack輸出文件的大小。
    複製代碼
    # 添加相應配置 starter/webpack.config.js
    
      ...
      const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
    
        if (IS_PROD) {
          ...
    + baseConfig.plugins.push(
    + new BundleAnalyzerPlugin()
    + );
        }
    
      ...
    複製代碼
    $ yarn build
    
    # 結果
    
    $ cross-env NODE_ENV=production webpack --color --progress
    98% after emitting SizeLimitsPluginWebpack Bundle Analyzer is started at http://127.0.0.1:8888
    Use Ctrl+C to close it
    Hash: 3bf5742858f4d53ed50d
    Version: webpack 4.41.2
    Time: 3079ms
    Built at: 2019-10-27 21:43:54
    
                                        Asset       Size  Chunks                         Chunk Names
      chunks/bottom-tab-navigator.04852ffb.js   1.54 KiB       0  [emitted] [immutable]  bottom-tab-navigator
                    chunks/github.a1f67f6e.js   4.65 KiB    1, 3  [emitted] [immutable]  github
                 chunks/not-found.b1d86988.js  576 bytes       3  [emitted] [immutable]  not-found
                   chunks/setting.34d43c5a.js  673 bytes       4  [emitted] [immutable]  setting
            chunks/vendors~github.d521d263.js   24.5 KiB       5  [emitted] [immutable]  vendors~github
    chunks/vendors~github.d521d263.js.LICENSE  120 bytes          [emitted]
              chunks/vendors~main.89c9b5d2.js    166 KiB       6  [emitted] [immutable]  vendors~main
      chunks/vendors~main.89c9b5d2.js.LICENSE   1.01 KiB          [emitted]
        css/bottom-tab-navigator.b9b2f600.css   1.54 KiB       0  [emitted] [immutable]  bottom-tab-navigator
                      css/github.e0893952.css   1.99 KiB    1, 3  [emitted] [immutable]  github
                        css/main.09cf98ab.css     11 KiB       2  [emitted] [immutable]  main
                   css/not-found.091bbea2.css   64 bytes       3  [emitted] [immutable]  not-found
                     css/setting.42e9f58f.css  252 bytes       4  [emitted] [immutable]  setting
                 fonts/iconfont.63765329.woff   4.58 KiB          [emitted]
                  fonts/iconfont.c2eabadd.ttf   7.52 KiB          [emitted]
                  fonts/iconfont.cad7bb52.eot   7.69 KiB          [emitted]
               images/empty-data.788c1924.png   11.7 KiB          [emitted]
                     images/logo.581fa1d8.png   8.38 KiB          [emitted]
             images/webpage-lost.a02f7942.png   13.5 KiB          [emitted]
                                   index.html  667 bytes          [emitted]
                             main.02466eca.js   6.64 KiB       2  [emitted] [immutable]  main
                    svg/iconfont.1247822e.svg     22 KiB          [emitted]
    
    Entrypoint main = chunks/vendors~main.89c9b5d2.js css/main.09cf98ab.css main.02466eca.js
    複製代碼

    x

Travis CI

  1. 在實際開發環境中,公司內部都會有一套持續集成的東西幫助咱們去部署。這裏爲了演示方便,咱們採用 Travis CI 幫助咱們去作這個事情。

  2. Travis CI: 代碼有變動,自動運行構建和測試,並反饋運行結果。

  3. 如何使用配置?請參考 持續集成服務 Travis CI 教程

  4. 本項目的 yml 腳本,請參考項目

  5. 預覽 「preview」

    x

結語

  • 到此整個web應用程序構建流程論述完畢!
  • 整篇文章,更像是列出一份清單,偏總結。若能一步一步實現我相信必定會有所收穫!
  • 洋洋灑灑寫了那麼多...
  • 最後,文中如有錯誤,感謝指出!
  • 【項目地址】

⬆ back to top

參閱

相關文章
相關標籤/搜索