04-深入React开发

什么是React

React 是一个用于构建用户界面的,由Facebook开源的JavaScript库,以声明式编写 UI,创建具有各自状态的组件,再通过基础组件组合为各种复杂的业务组件,组件逻辑使用JavaScript编写而非模版,因此你可以轻松地在应用中传递数据,并使得状态与DOM分离。

React的组件分为有状态的普通组件和无状态的函数组件。有状态的普通组件除了使用外部数据(通过 this.props 访问)以外,组件还可以维护其内部的状态数据(通过 this.state 访问)。当组件的状态数据改变时,组件会再次调用 render() 方法重新渲染对应的标记。组件名称必须以大写字母开头,因为React会将以小写字母开头的组件视为原生 DOM 标签。

在最新的React16.8中引入了Hook,它可以让你在不编写有状态组件的情况下使用state以及其他的React特性。

React中配合使用 TSX,TSX 可以很好地描述 UI 应该呈现出它应有交互的本质形式。TSX 可能会使人联想到模版语言,但它具有 JavaScript 的全部功能。

React分别支持浏览器端渲染,服务器端渲染两种方式。

React中具体内容我们将在具体开发中陆续介绍。

增加路由控制

添加React程序需要的路由控制包

yarn add react-router-dom

以及类型定义文件

yarn add -D @types/react-router-dom

react-router-dom包比react-router多了一些预制的DOM标签,其他功能都一样,所以我们只需要导入前者即可。路由这块有个坑,请参考 0X-各种疑难杂症

按照如下路径创建对应的组件

my-app/
├─ dist/
└─ src/
   └─ components/
      └─ About.tsx
      └─ Header.tsx
      └─ Top.tsx
   └─ containers/
      └─ App.tsx

Header.tsx定义了所有页面公用的导航链接,是一个无状态的函数控件。Linkreact-router-dom的预制标签,最终会解析为a标签。

import * as React from 'react'
import { Link } from "react-router-dom";
import { PATHS } from '../constants';

const Header: React.SFC = () => {
    return (
        <ul>
            <li><Link to={PATHS.TOP}>Top</Link></li>
            <li><Link to={PATHS.ABOUT}>About</Link></li>
        </ul>
    );
}

export default Header;

Top.tsx是一个有状态的类控件

import * as React from "react";

export interface TopProps {

}

class Top extends React.Component<TopProps> {
    constructor(props: TopProps) {
        super(props);
    }
    
    render() {
        return (<div>Welcome to my app!!</div>);
    }
}

export default Top;

About.tsx

import * as React from "react";

const About: React.SFC = () => {
    return (<div>About</div>);
}

export default About;

容器组件App.tsx

import * as React from 'react'
import { Route, Router } from "react-router-dom";
import { createBrowserHistory } from "history";
import Top from '../components/Top';
import About from '../components/About';
import Header from '../components/Header';
import { PATHS } from '../constants';

let history = createBrowserHistory()

class App extends React.Component {
    render() {
        return (
            <Router history={history}>
                <Header />
                <Route exact path={PATHS.TOP} component={Top}></Route>
                <Route exact path={PATHS.ABOUT} component={About} ></Route>
            </Router>
        );
    }
}

export default App;

容器组件App.tsx是有不同组件组合而成的,比如通用的Header及根据路由显示不同内容的组件。通过Router标签的history属性将使用Html5 History API的属性注入到所包含的所有组件内,通过组件的props可以访问到history,location,match等与路由相关的属性。

Route标签关联了路由及显示的组件,exact属性是exact=true的简写,保证了路由严格只能匹配到一个组件。

最后修改index.tsx

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

import App from "./containers/App";

ReactDOM.render(
    <App/>,
    document.getElementById("app")
);

根目录添加constants.ts管理程序所有常量

// 路由配置
export const PATHS = {
    TOP: '/',
    ABOUT: '/about',
}

到此为止,我们在应用程序上实现了简单的路由功能。

按需加载

我们通过React自带的lazy, Suspense来实现按需加载。注意此方式仅支持客户端渲染。

在App.tsx,首先导入对应函数及标签

import { lazy, Suspense } from "react";

将原有的直接引用方式

import About from '../components/About';

替换为

const About = lazy(() => import('../components/About'));

使用Suspense标签来包含路由标签

<Suspense fallback={() => <div>Loading...</div>}>
   <Route exact path={PATHS.ABOUT} component={About} ></Route>
</Suspense>

以此完成动态加载。重新编译后报错:

ERROR in ./src/containers/App.tsx
Module build failed (from ./node_modules/babel-loader/lib/index.js):
SyntaxError: C:\work\temp\my-app\src\containers\App.tsx: Support for the experimental syntax 'dynamicImport' isn't currently enabled (9:26):

   7 | import Top from '../components/Top';
   8 | 
>  9 | const About = lazy(() => import('../components/About'));
     |                          ^
  10 | let history = createBrowserHistory()
  11 | 
  12 | class App extends React.Component {

Add @babel/plugin-syntax-dynamic-import (https://git.io/vb4Sv) to the 'plugins' section of your Babel config to enable parsing.

看来预制的presets没有包含对应的插件,这里需要手动添加插件支持。运行安装命令yarn add -D @babel/plugin-syntax-dynamic-import后,修改babel.config.js

const presets = [
    [
        "@babel/preset-env",
        {
            targets: {
                "browsers": ["last 2 versions", "> 0.2%", "maintained node versions", "not dead"],
            },
            useBuiltIns: "usage",
            corejs: 2
        },
    ],
    ["@babel/preset-react"],
    ["@babel/preset-typescript"]
];

const plugins = [
    ["@babel/plugin-syntax-dynamic-import"]
]

module.exports = {
    presets,
    plugins
};

再执行编译

Version: webpack 4.35.0
Time: 2308ms
Built at: 2019-06-24 4:52:11 PM
         Asset       Size  Chunks             Chunk Names
   0.bundle.js   1.47 KiB       0  [emitted]
    index.html  320 bytes          [emitted]
main.bundle.js   3.57 MiB    main  [emitted]  main
Entrypoint main = main.bundle.js

首页初始加载时没有包括0.bundle.js的bundle文件

切换到About后,出现了0.bundle.js的bundle文件的引用

分割代码

实际开发中对于node_modules中的库代码及程序中提供的通用工具库的代码,因为相对于业务代码来说,修改的频率要小的多,所以为更好的使用浏览器的缓存功能,需要将其分离出来。分离出来后,总体代码大小也会下降。

Webpack4里原有的CommonsChunkPlugin被移除,取而代之的是 optimization.splitChunks 配置项来完成分割工作。

下面是 splitChunks 配置项的默认值

splitChunks: {
    chunks: "async",
    // 分割独立chunk的最小大小(单位字节)
    minSize: 30000,
    // 分割前必须共享chunk的最小块数
    minChunks: 1,
    // 按需加载的最大并行请求数
    maxAsyncRequests: 5,
    // 一个入口最大并行请求数
    maxInitialRequests: 3,
    automaticNameDelimiter: '~',
    name: true,
    cacheGroups: {
        vendors: {
            test: /[\\/]node_modules[\\/]/,
            priority: -10
        },
      default: {
            minChunks: 2,
            priority: -20,
            reuseExistingChunk: true
        }
    }
}

由此可知,Webpack 默认的分割标准为

  • 新 bundle 被两个及以上模块引用,或者来自 node_modules
  • 新 bundle 大于 30kb (压缩之前)
  • 异步加载并发加载的 bundle 数不能大于 5 个
  • 初始加载的 bundle 数不能大于 3 个

简单的说,Webpack 会把代码中的公共模块自动抽出来,变成一个包,前提是这个包大于 30kb,不然 Webpack 是不会抽出公共代码的,因为增加一次请求的成本是不能忽视的。

注意 maxSize 比 maxInitialRequest 和 maxAsyncRequests 具有更高的优先级。 实际优先级为maxInitialRequest和maxAsyncRequests < maxSize < minSize。

我们这里希望将 node_modules 里的代码块独立出来,因为这里的改动最小。通过splitChunks.cacheGroups我们可以自定义分割标准。

修改 webpack.prod.js 配置,添加 optimization 配置项

optimization: {
        splitChunks: {
            cacheGroups: {
                vendor: {
                    name: 'vendor',
                    chunks: 'all',
                    test: /node_modules/,
                    priority: 20,
                    reuseExistingChunk: true,
                }
            }
        },
        runtimeChunk: {
            name: entrypoint => `manifest.${entrypoint.name}`
        }
    },

runtimeChunk 的作用是将包含 chunks 映射关系的 list单独从入口文件里提取出来,因为每一个 chunk 的 id 基本都是基于内容 hash 出来的,所以你每次改动都会影响它,如果不将它提取出来的话,等于入口文件每次都会改变。缓存就失效了。

生产环境下编译后

yarn run v1.16.0
$ webpack --config webpack.prod.js
Hash: aa35580d8ecf16c47c23
Version: webpack 4.35.0
Time: 15054ms
Built at: 2019-06-24 6:26:59 PM
                                    Asset       Size  Chunks             Chunk Names
                3.7e0873588f0bd55f2579.js  227 bytes       3  [emitted]
            3.7e0873588f0bd55f2579.js.map  486 bytes       3  [emitted]
                               index.html  495 bytes          [emitted]
             main.d49317ef696d1a92aee1.js   3.47 KiB       0  [emitted]  main
         main.d49317ef696d1a92aee1.js.map   5.12 KiB       0  [emitted]  main
    manifest.main.6e467d970b586c9e8edf.js   2.25 KiB       1  [emitted]  manifest.main
manifest.main.6e467d970b586c9e8edf.js.map   11.8 KiB       1  [emitted]  manifest.main
           vendor.10bfb86250c9761c590b.js    150 KiB       2  [emitted]  vendor
       vendor.10bfb86250c9761c590b.js.map    445 KiB       2  [emitted]  vendor
Entrypoint main = manifest.main.6e467d970b586c9e8edf.js manifest.main.6e467d970b586c9e8edf.js.map vendor.10bfb86250c9761c590b.js vendor.10bfb86250c9761c590b.js.map main.d49317ef696d1a92aee1.js main.d49317ef696d1a92aee1.js.map

这样类似react等类库被打包到 vendor.10bfb86250c9761c590b.js 中去了。

我们再进一步测试一下,修改下 About.tsx后再重新编译

yarn run v1.16.0
$ webpack --config webpack.prod.js
Hash: d564931b25d7ebed1c50
Version: webpack 4.35.0
Time: 4123ms
Built at: 2019-06-25 11:14:14 AM
                                    Asset       Size  Chunks             Chunk Names
                3.4d7128957ac27d873d2d.js  230 bytes       3  [emitted]
            3.4d7128957ac27d873d2d.js.map  493 bytes       3  [emitted]
                               index.html  495 bytes          [emitted]
             main.d49317ef696d1a92aee1.js   3.47 KiB       0  [emitted]  main
         main.d49317ef696d1a92aee1.js.map   5.12 KiB       0  [emitted]  main
    manifest.main.ae0e4a7ff5acc96f667e.js   2.25 KiB       1  [emitted]  manifest.main
manifest.main.ae0e4a7ff5acc96f667e.js.map   11.8 KiB       1  [emitted]  manifest.main
           verdor.10bfb86250c9761c590b.js    150 KiB       2  [emitted]  verdor
       verdor.10bfb86250c9761c590b.js.map    445 KiB       2  [emitted]  verdor
Entrypoint main = manifest.main.ae0e4a7ff5acc96f667e.js manifest.main.ae0e4a7ff5acc96f667e.js.map verdor.10bfb86250c9761c590b.js verdor.10bfb86250c9761c590b.js.map main.d49317ef696d1a92aee1.js main.d49317ef696d1a92aee1.js.map

可以看到关于About.tsx的bundle和manifest发生了变化,而入口及由 node_modules 打包出来的bundle没有变化,这样我们就可以使浏览器的缓存发挥作用了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容

  • 函数是面向过程的,函数的调用不需要主体,而方法是属于对象的,调用方法需要一个主体-即对象。 npm install...
    Gukson666阅读 465评论 0 3
  • 在这个教程里,我们会从一个例子React应用开始学习react-router-dom。其中你会学习如何使用Link...
    uncle_charlie阅读 34,711评论 1 40
  • React-Router v4 1. 设计理念1.1. 动态路由1.2. 嵌套路由1.3. 响应式路由 2. 快速...
    wlszouc阅读 8,547评论 0 14
  • 我的愿望是做一个有弹性的人,这样的人就算能清清楚楚看得到他人的不明事理,不思进取,乖戾叛逆,依旧能够真诚坦荡待之。...
    海婉姑姑阅读 244评论 0 0
  • 随便说说看,不清楚 板凳子上
    紫檀涧阅读 86评论 0 1