04-深入React开发

作者: 七玄之主 | 来源:发表于2019-06-24 15:00 被阅读12次

    什么是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没有变化,这样我们就可以使浏览器的缓存发挥作用了。

    相关文章

      网友评论

        本文标题:04-深入React开发

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