[React 从零实践01-后台] 代码分割

导航

[react] Hooks

[React 从零实践01-后台] 代码分割
[React 从零实践02-后台] 权限控制
[React 从零实践03-后台] 自定义hooks
[React 从零实践04-后台] docker-compose 部署react+egg+nginx+mysql
[React 从零实践05-后台] Gitlab-CI使用Docker自动化部署

[源码-webpack01-前置知识] AST抽象语法树
[源码-webpack02-前置知识] Tapable
[源码-webpack03] 手写webpack - compiler简单编译流程
[源码] Redux React-Redux01
[源码] axios
[源码] vuex
[源码-vue01] data响应式 和 初始化渲染
[源码-vue02] computed 响应式 - 初始化,访问,更新过程
[源码-vue03] watch 侦听属性 - 初始化和更新
[源码-vue04] Vue.set 和 vm.$set
[源码-vue05] Vue.extend

[源码-vue06] Vue.nextTick 和 vm.$nextTick

[部署01] Nginx
[部署02] Docker 部署vue项目
[部署03] gitlab-CI

[深入01] 执行上下文
[深入02] 原型链
[深入03] 继承
[深入04] 事件循环
[深入05] 柯里化 偏函数 函数记忆
[深入06] 隐式转换 和 运算符
[深入07] 浏览器缓存机制(http缓存机制)
[深入08] 前端安全
[深入09] 深浅拷贝
[深入10] Debounce Throttle
[深入11] 前端路由
[深入12] 前端模块化
[深入13] 观察者模式 发布订阅模式 双向数据绑定
[深入14] canvas
[深入15] webSocket
[深入16] webpack
[深入17] http 和 https
[深入18] CSS-interview
[深入19] 手写Promise
[深入20] 手写函数
[深入21] 算法 - 查找和排序

前置知识

一些单词

automatic:自动的
delimiter:分隔符
( automaticNameDelimiter )

lighthouse:灯塔
priority:优先级

vendor: 第三方
Suspense:悬念,悬停
fallback:退路

(1) 为什么要做代码分割

  • ( A文件 ) 分割成 ( B文件,C文件 )
  • 加载一个2MB的文件A,和加载两个1MB的文件B和C,由于存在异步加载并行加载,所以分割后可能加载速度更快
  • 当修改代码时,不做代码分割,只修改一小部分就会重新打包整个文件A,生成新的文件A',用户端就得从新加载整个文件A';做代码分割后,如果修改的代码在B文件,从新打包只需要打包B文件,同时用户端也只需要重新加载B文件,C文件会被缓存
  • 还可以做按需加载,懒加载,路由懒加载,解决白屏等,最终提升性能

(2) 代码分割的三种角度

  • 拆分成 ( 业务代码-经常变动 ) 和 ( 第三方依赖代码-几乎不变 )
    • 业务代码会随着需求迭代等不断变化,而第三方依赖包基本不变,所以可以把第三方依赖包单独拆分打包,比如叫vender.js这个包基本不变,代码发布后,在用户端不用重新加载,而是浏览器会自动缓存
  • 根据路由进行切割,即路由懒加载
    • 比如进入首页的路由时,只需要加载首页的那部分代码
    • 首页有依赖其他模块,同步引入其实也可以在拆分成粒度更细的包,动态引入的可以通过import()函数做动态加载拆分
  • 根据组件进行切割
    • 按路由方式进行代码切割,当A组件包含C组件,而B组件也包含C组件时,两个打包后的包,都会分别包含C组件的代码,造成冗余。
    • 按组件方式进行代码切割,则能避免上面的问题,但是由此带来的问题就是包的数量会急剧增加,需要开发者自己衡量利弊。

(3) import(specifier) 函数

  • import加载模块时,不能做到像require那样的运行时加载模块,所以有了 import()函数 提案,动态加载模块

  • 参数:模块的路径

  • 返回值:返回一个 promise 对象

  • 适用场合:

    • 按需加载:在需要时在加载模块
    • 条件加载:在if语句中做条件加载
    • 动态模块路径:允许模块路径动态生成
  • 注意点:

    • import()返回的是一个promise实例对象,加载成功后,模块对象作为.then() 方法的参数,可以通过 解构赋值 获取输出接口
    • 如果模块有 default 输出接口,可以通过参数直接获取default接口,即 .then(moudle => module.default)
    • 通过加载多个模块
    • 当 Webpack 解析到import()语法时,会自动进行代码分割。如果你使用 Create React App,该功能已开箱即用,你可以立刻使用该特性。当然也可以自己配置webpack
    • 当使用 Babel 时,你要确保 Babel 能够解析动态 import 语法而不是将其进行转换。对于这一要求你需要 @babel/plugin-syntax-dynamic-import
import(/* webpackChunkName: "AsyncTest" */'../../components/async-test') 
  .then(({ default: AsyncTest }) => {
    ...
  })
  .catch(err => console.log(err))
  

异步加载,动态加载( import() )
- 代码拆分如何命名包名 
    1. /* webpackChunkName: "AsyncTest" */
    2. 使用插件 @babel/plugin-syntax-dynamic-import 就可以上面的 魔法注释 写法
    3. 通过 create-react-app新建的项目中 
            => babel-preset-react-app 依赖=> @babel/preset-env 依赖=> @babel/plugin-syntax-dynamic-import 
            
同步加载( import )
- 代码拆分如何命名包名
    1. 通过设置 optimization => splitchunks => cashGroups 来配置包名

(4) webpack => optimization

  • 对于用webpack构建的项目

    • 同步方式引入的模块( import ),做代码分割需要配置 optimization.splitchunks
    • 异步方式引入的模块( import() ),不需要做任何配置
  • optimization.splitchunks

    • automaticNameDelimiter
      • 指定拆分出来的包的连接符,来源组名称 连接符 入口名称(例如vendors~main.js)
      • 默认是 ~
    • maxAsyncRequests
      • 按需加载时最大的并行请求数,默认30
    • maxInitialRequests
      • 入口最大并行请求数,默认30
    • chunks
      • string 或者 function
      • string时,有效值为 allasyncinitial,all表示同步和异步模块都进行拆分
      • function时,可以有效的指定具体的哪些模块需要进行拆分
      • chunks 需要配合 cashGroups
    • cacheGroups
      • priority:定义每个组的优先级
        • 当一个模块满足多个组规则时,该模块将被打包到 priority 高的文件中
        • number越大优先级越高,默认组的默认值是负数,自定义组的默认值是0
      • filename:打包后模块的名字
      • reuseExistingChunk:boolean
        • 如果在之前的模块中引入过该模块A,并打包了,现在又引入了模块A,就复用之前已经打包好的A
    • minChunks(maxChunks)
      • 模块是否进行拆分的最小引用次数,即至少该模块被引用多少次才进行拆分
    • minSize(maxSize)
      • 模块是否进行拆分的最小大小(以字节为单位)
  • 官网说明

  • SplitChunksPlugin

optimization.splitchunks默认配置项如下:


module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'all', // 对同步引入模块 和 异步引入模块都做代码分割,all async initial
      minSize: 20000, // 当引入的模块大小大于 20KB 时,对该模块进行代码分割
      minRemainingSize: 0,
      maxSize: 0, // 超过值后,对模块进行二次拆分
      minChunks: 1, // 引入的模块被引用一次时就进行代码分割
      maxAsyncRequests: 30, // 最大的按需(异步)加载次数,整个项目最多进行30个代码分割
      maxInitialRequests: 30, // 最大的初始化加载次数,首页最多进行30个代码分割
      automaticNameDelimiter: '~', // 打包后的名字中的连接符,组名+连接符+入口文件名
      enforceSizeThreshold: 50000,
      cacheGroups: {
        defaultVendors: { // 组名称
          test: /[\\/]node_modules[\\/]/, // 匹配的范围是 node_modules
          priority: -10 // 优先级,当一个模块满足多个组规则时,该模块将被打包到 priority 高的文件中
          // filename: 'vender.js' // 指定打包后模块的名字
        },
        default: { // 引入的模块,如果不满足上面的defaultVendors组规则的模块,就会进行default组的规则匹配
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true, // 之前已经打包过该模块,就直接复用
        }
      }
    }
  }
};

(5) 错误边界

部分 UI 中的 JavaScript 错误不应该破坏整个应用程序。 为了解决这个问题,React引入了 “错误边界(Error Boundaries)”



react中的代码分割实现

(1) React.lazy 和 Suspense 实现代码分割

    1. React.lazy(() => import()) 参数是一个函数,函数返回值必须是一个promsie对象,React.lazy 目前仅支持默认导出
    1. Suspense组件,fallback 属性接受任何在组件加载过程中你想展示的 React 元素。你可以将 Suspense 组件置于懒加载组件之上的任何位置。你甚至可以用一个 Suspense 组件包裹多个懒加载组件。
import React, { useState, Suspense } from 'react'
import { Button } from 'antd';


const Home = (props: any) => {
  console.log(props);

  const [AsyncTest, setAsyncTest] = useState<any>()
  const [AsyncTest2, setAsyncTest2] = useState<any>()

  // import()方式代码分割
  const asyncLoad1 = () => {
    import(/* webpackChunkName: "AsyncTest" */'../../components/async-test')
      .then(({ default: AsyncTest }) => {
        setAsyncTest((element: any) => element = AsyncTest)
      })
      .catch(err => console.log(err))
  }

  // React.lazy() + Suspense 方式代码分割
  const asyncLoad2 = () => {
    const Test2 = React.lazy(() => import(/* webpackChunkName: "AsyncTest2" */'../../components/async-test2'))
    setAsyncTest2((component: any) => component = Test2)
  }

  return (
    <div>
      <header>home page bigscreen</header>

      <Button onClick={() => {
        asyncLoad1();
        asyncLoad2()
      }}>异步加载</Button>

      {AsyncTest ? AsyncTest() : null}
      {/* {AsyncTest ? <AsyncTest />: null} */}

      <Suspense fallback={<div>Loading...</div>}>
        {AsyncTest2 ? <AsyncTest2 /> : null}
      </Suspense>
    </div>
  )
}

export default Home

(2) 基于路由的代码分割(React.laze)(Suspense)(react-router-config)

  • 和vue类似
React.lazy   Suspense   react-router-config


routes.js------------------
const Login = lazy(() => import(/* webpackChunkName: 'Login' */'../pages/login'))
const HomeBigScreen = lazy(() => import(/* webpackChunkName: 'HomeBigScreen' */'../pages/home/bigscreen.home'))
const HomeAdmin = lazy(() => import(/* webpackChunkName: 'HomeAdmin' */'../pages/home/admin.home'))
const Layout = lazy(() => import(/* webpackChunkName: 'Layout' */'../pages/layout'))
const routes: RouteModule[] = [
  {
    path: '/login',
    component: Login,
  },
  {
    path: '/',
    component: Layout,
    routes: [ // -------------------------------------------------------- 嵌套路由
      {
        path: '/home-bigscreen',
        exact: true,
        component: HomeBigScreen,
      },
      {
        path: '/home-admin',
        exact: true,
        component: HomeAdmin,
      },
    ]
  }
]


router.js------------------
import { renderRoutes } from 'react-router-config' //--------------------- react-router-config集中式路由解决方案
const Router = () => {
  return (
    <Suspense fallback={<div>loading...</div>}> //------------------------ Suspense包裹lazy,Suspense.fallback
      <Switch>
        {renderRoutes(routes)}
      </Switch>
    </Suspense>

  )
}


layout.js----------------
const render = () => {
    if (systemType === SYSTEMTYPE.ADMIN) {
      return (
        <div className={styles.layoutAdmin}>
          <header className={styles.header}>layout page admin</header>
          {renderRoutes(props.route.routes)} //--------------------------- renderRoutes(props.router.routes)
        </div>
      )
    } else {
      return (
        <div className={styles.layoutBigScreen}>
          {renderRoutes(props.route.routes)}
        </div>
      )
    }
  }

(3) 基于路由的代码分割(第三方库 react-loadable)

项目源码

资料

官网教程: https://www.html.cn/create-react-app/docs/code-splitting/
react中做代码分片:https://juejin.im/post/6844903953721737224#heading-1

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

推荐阅读更多精彩内容