blueju

blueju 查看完整档案

深圳编辑  |  填写毕业院校  |  填写所在公司/组织 www.yuque.com/blueju 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

blueju 发布了文章 · 1月11日

记录一下我离线依赖迁移安装的具体尝试过程

首发于 语雀文档

第 1 次

直接安装,不可行,因为是私有依赖,根本安装不上。

第 2 次

流程:

  1. 拷贝依赖到项目目录 library 中
  2. 通过 webpack alias 别名的方式关联依赖

不可行,npm run start 运行报错,缺少依赖。

想想也是,
依赖的依赖其实在我们 npm install 的时候也是一起安装上的,
而我通过上述流程 1→2 的方式关联依赖,缺少了依赖的依赖被安装的过程,自然会报缺少依赖的错误。

第 3 次

依赖 react-color-ui
依赖 react-color-ui 的依赖 react-input-tags
均为私有依赖,无法通过 npm install 安装

希望通过离线安装依赖的方式。

  1. 在外网机中使用 npm-pack-all 命令将 react-color-ui 依赖打包成了 tgz 文件
  2. 将 tgz 文件拷贝入内网机,并放置到项目根目录中
  3. 使用 npm install react-color-ui.tgz --save 命令安装 react-color-ui
  4. 安装失败,提示报错:无法获取 react-input-tags 依赖
其实是因为我的 react-color-ui 项目目录下的 node_modules 不全,导致依赖没打包好。
这也就是为什么更推荐大家使用 npm install xxx -g 全局安装的方式安装依赖。
因为全局安装的特点就是:这个包的所有依赖包都安装在了自己的目录下的 node_modules 子目录里。

第 4 次

  1. 安装 npm-pack-all,用于将依赖打成 tgz 格式的离线安装包
  2. 将 react-color-ui(源码包)中的除 node_modules 和 dist 外的文件删除
  3. 将 react-color-ui(源码包)dist 目录下除 node_modules 外的文件剪切到 react-color-ui(源码包)的根目录下
  4. 删除 react-color-ui(源码包)的 dist 目录
  5. 在 react-color-ui(源码包)目录下打开 git bash,执行 npm-pack-all,将依赖打包成离线安装包
其实就是模拟 npm install 全局安装的效果。
全局安装的特点就是:这个包的所有依赖包都安装在了自己的目录下的 node_modules 子目录里。

产出

最后总结一下我产出了什么

  1. react-color-ui 依赖安装指南
  2. react-color-ui 离线安装包制作指南
  3. react-color-ui-版本号.tgz 离线安装包
  4. 将另一个私有依赖也使用该方式安装到项目中

通用做法

当然,以上是为适配我项目的一点变通做法(尤其是234步),通用的做法是:(和参考文章其实差不多)

  1. 使用 npm install -g xxx 命令全局安装依赖
  2. 找到所安装依赖文件位置
  3. 右键打开 git bash,执行 npm-pack-all,将依赖打包成 tgz 格式的离线安装包
  4. 将离线安装包拷贝入内网机的项目根目录中
  5. 右键打开 git bash,npm install xxx --save 将依赖安装
  6. Success!!!

参考文章

  1. 离线环境安装npm包
查看原文

赞 0 收藏 0 评论 0

blueju 发布了文章 · 2020-12-29

umi-request 获取数据流

首发于 语雀文档

前言

微前端产品开发中使用到了 umi-request 这个,类似于 axios(但貌似又强于 axios),因为碰到文件下载的功能,也即是:获取数据流,记录一下。


其实最重要的两点无非是:

  1. responseType 要设置成 blob
https://github.com/blueju/umi-request-practice/blob/4f11c63e7b47f730ed1bcdde867c6f4575241485/example/get-data-stream.jsx#L14
  1. 如何将响应结果中的文件流导出来
https://github.com/blueju/umi-request-practice/blob/4f11c63e7b47f730ed1bcdde867c6f4575241485/example/get-data-stream.jsx#L16

Github

https://github.com/blueju/umi-request-practice.git

位置

image.png

查看原文

赞 0 收藏 0 评论 0

blueju 发布了文章 · 2020-12-29

umi-request 统一异常处理实践

首发于语雀文档

前言

本人在工作中用到了 umi-request,百度谷歌搜了一遍,感觉都没找到超过 3 篇合适且含代码的文章,因此只能自行实践总结了。

umi-request 有点不同

umi-request 与 axios & xhr 不太一样,它是基于 fetch 实现的,因此它只有在网络错误的情况下,才会抛出错误(即:catch),即使接口返回的是 404/500 等错误。


umi-request 自带提供一个错误处理函数 errorHandler,所有在 umi-request 中执行的代码如果抛出错误,都会被捕获。

参考:
https://github.com/umijs/umi-request/blob/master/README_zh-CN.md#错误处理

统一异常处理

官方文档中提供的 errorHandler 示例比较简单,只简单判断了所抛错误 error 是否有 response 属性(比如说网络错误这个场景,error 中就是没有 response 属性)。
这就容易出现一个问题:代码执行出错(比如获取不到外部传入的参数),它所抛的 error 也是没有 response 属性。。。


我觉得目前其实还挺简单的,主要搞清楚以下两点:

  1. 错误捕获顺序
  2. 自定义 Error,扩展 Error

错误捕获顺序

  1. 发生错误
  2. errorHandler(是个 Promise)

    1. 如果抛出错误或 reject,则进入下一个 Promise 的 catch,即:request("",{}).then().catch()
    2. 否则进入下一个 Promise 的 then,即:request("",{}).then()

自定义 Error,扩展 Error

我希望能判断区分以下几类错误:

  1. 网络错误
  2. HTTP 错误
  3. 接口错误
  4. 其他错误(如代码执行错误)


那就使用 ES6 Class 继承一下 Error 进行封装。


参考:
https://zh.javascript.info/custom-errors

后续

后续还可以再复杂一点,自定义扩展一下请求错误和响应错误以示区分,不过应该都差不多。

Github

github 上的代码是本人的一整套 umi-request 实践,并非只含 umi-request 错误处理

https://github.com/blueju/umi-request-practice

查看原文

赞 0 收藏 0 评论 0

blueju 发布了文章 · 2020-12-18

NPM如何只发布打包后的dist

首发于语雀文档

前言

因为项目要求,我写了一个基于 umi-request 的统一请求插件,但公司的私有库尚未搭建起来,虽然可以暂时通过 webpack alias 别名引用的方式解决,但为了其他同学能快速拉到我的插件,我只能暂时先只将 dist 上传到 npm 仓库上。


一共有两种方式。

  • .npmignore(黑名单方式)
  • pkg.files(白名单方式)

黑名单方式

.npmignore

即:在项目根目录下新建一个名称为 .npmignore 的文件,使用与 .gitignore 一样的写法,将不希望上传到 npm 仓库的文件记录进去。

白名单方式

pkg.files

即:在 package.json 新建一个名称为 files 的属性,以数组的方式,记录下希望上传到 npm 仓库的文件。

备注

如果你的项目比较复杂比较深层,如果你手动添加白名单的话,可能会漏掉某些文件。
这是推荐使用 ypkgfiles 自动生成 pkg.files。

ypkgfiles 的原理非常简单,通过 main 入口找到所有的相对依赖,然后获取第一层的文件或目录就能生成 pkg.files 了。

ypkgfiles 还提供 --entry 参数来支持一些特定的场景,比如以下场景无法被 ypkgfiles 识别。

  • 比如某个目录是模板文件,不是通过 require 加载的
  • mz 模块使用 require('mz/fs') 的方式,并且这个文件不被 main require
  • egg 的目录是由 egg loader 加载的

使用 --entry 来添加这些文件或目录,比如 egg 项目

$ ypkgfiles --entry app --entry config --entry *.js

区别

根据自己的业务场景选择即可,还是很容易区分。
比如你是开源项目,所有的文件会希望上传,如果你用第二种方式,就会使 package.json 变的庞大。
比如你是保密项目,只希望打包后的 dist 文件上传,如果你用第一种方式,书写 .npmignore 的时候你就会很头大。

参考

  1. 如何过滤 npm publish 的文件
查看原文

赞 1 收藏 0 评论 0

blueju 发布了文章 · 2020-11-03

umi +qiankun 主应用动态装载子应用(路由)解决方案

umi +qiankun 主应用动态装载子应用(路由)解决方案

前言

接上一篇(https://www.yuque.com/blueju/blog/gtgndg),上一篇中使用的都是运行时动态注册子应用,子应用路由仍然是写死的、非动态获取。


然后真实项目中除了需要动态注册子应用,还很有可能需要动态装载子应用(路由),比如说:不同权限的用户需要给予他们不同的路由。


此篇 blog 的代码是基于上一篇进行改动的,上一篇 blog 中的代码对应的 GitHub 地址是:https://github.com/blueju/umi-mirror-docs/tree/runtime-dynamic-register-sub-app

子应用

为了体现出现演示效果,我们先为子应用添加两个新页面和新路由,如下图:

路由代码

image.png

页面代码
copy 自 pages/index.tsx

image.png

主应用

根据官网文档:

我们知道,动态装载子应用(路由)的方式有两种,


在这里,我可以明确地告诉大家,截止目前(2020年9月6日17点51分),动态装载子应用(路由)无法使用第一种方式(在 config/config.ts 中配置路由)。
image.png

删除之前写死的子应用(路由)装载

在 config/config.ts 中删除之前写的以下子应用(路由)装载配置:

{
  name: 'sub-app-1',
  icon: 'smile',
  path: '/sub-app-1',
  microApp: 'sub-app-1',
},

添加子应用路由配置(Mock)

https://github.com/blueju/umi-docs/commit/5f330d5233ebe566c8e4f8c1cc1749e2b4cdcfdf

添加子应用容器

https://github.com/blueju/umi-docs/commit/849d9f3dbe930a092938334e66d51568ed873fe3
在 src/layouts 下新建一个叫 MicroAppLayout.tsx 的文件

import { MicroApp } from 'umi';
import React from 'react';

function MicroAppLayout() {
  return <MicroApp name="sub-app-1" />;
}

export default MicroAppLayout;

动态装载子应用路由

在 src/app.tsx 下新建 app.tsx 文件,代码为:

import { dynamic } from 'umi';
import LoadingComponent from '@/components/PageLoading';

let extraRoutes: object[] = [];

export const qiankun = fetch('/api/config')
  .then((res) => {
    return res.json();
  })
  .then(({ apps }) => {
    return Promise.resolve({
      // 注册子应用信息
      apps,
      // 完整生命周期钩子请看 https://qiankun.umijs.org/zh/api/#registermicroapps-apps-lifecycles
      lifeCycles: {
        afterMount: (props) => {
          console.log(props);
        },
      },
      // 支持更多的其他配置,详细看这里 https://qiankun.umijs.org/zh/api/#start-opts
    });
  });

export function patchRoutes({ routes }) {
  extraRoutes.forEach((element) => {
    routes[1].routes[0].routes.unshift({
      name: element.name,
      icon: 'smile',
      path: element.path,
      component: dynamic({
        loader: () =>
          import(/* webpackChunkName: 'layouts__MicroAppLayout' */ '@/layouts/MicroAppLayout'),
        loading: LoadingComponent,
      }),
    });
  });
}

export async function render(oldRender) {
  fetch('/api/config')
    .then((res) => {
      return res.json();
    })
    .then((resJson) => {
      extraRoutes = resJson.routes;
      oldRender();
    });
}

Github

https://github.com/blueju/umi-docs/tree/runtime-dynamic-load-sub-app-router


参考:
https://github.com/ant-design/ant-design-pro/issues/5909
如何从服务端获取菜单数据及权限校验
umi 运行时配置
Antd Design Pro 中如何动态获取路由替换掉config/route.config.js
antd pro 动态菜单与动态路由
是否真的不支持动态路由
[[Feature Request] 微前端框架下动态增加微前端路由](https://github.com/umijs/umi/...
运行时配置:patchRoutes动态添加多级路由,子路由没效果。
https://github.com/umijs/umi/issues/5003

查看原文

赞 0 收藏 0 评论 0

blueju 发布了文章 · 2020-09-30

umi + qiankun 动态注册子应用解决方案

前言

首发于 语雀文档@blueju


本篇文章可以被视为上一篇文章(https://www.yuque.com/blueju/blog/huuie8)的延续,上一篇文章大致讲述了搭建一个简易微前端平台的过程,其中关于子应用注册的配置是通过硬编码的方式,在构建打包前写入的。


但是真实项目中,更需要的是动态注册子应用,比如说我正参与的这个项目。


本篇文章的示例代码是基于上一篇文章示例代码改动的,上一篇文章示例代码对应的 GitHub 地址是:https://github.com/blueju/umi-qiankun/tree/umi-micro-fe-platform


代码修改集中在主应用,子应用不需要修改代码。

删除之前直接写死的子应用注册配置

官网文档中缺少该部分配置。
以下代码中,- 代表删除该行。
config/config.ts
export default defineConfig({
  ...
  qiankun: {
    master: {
      - apps: [
      -   {
      -     name: 'sub-app-1',
      -     entry: '//localhost:8001',
      -   }
      - ]
    }
  }
})

配置Mock

在 mock 文件夹下新建 config.ts,写入以下代码:

export default {
  '/api/config': {
    apps: [
      {
        name: 'sub-app-1',
        entry: '//localhost:8001',
      },
    ],
  },
};

配置运行时动态注册子应用

参考:
https://umijs.org/zh-CN/plugins/plugin-qiankun#b-运行时动态配置子应用(srcappts-里开启)
个人认为 Umi 官网中的写法并不是那么好懂。

在 src 文件夹下新建 app.ts,写入以下代码:

// 从接口中获取子应用配置,export 出的 qiankun 变量是一个 promise
export const qiankun = fetch('/api/config')
  .then((res) => {
    return res.json();
  })
  .then(({ apps }) => {
    return Promise.resolve({
      // 注册子应用信息
      apps,
      // 完整生命周期钩子请看 https://qiankun.umijs.org/zh/api/#registermicroapps-apps-lifecycles
      lifeCycles: {
        afterMount: (props) => {
          console.log(props);
        },
      },
      // 支持更多的其他配置,详细看这里 https://qiankun.umijs.org/zh/api/#start-opts
    });
  });

效果

image.png

GitHub

https://github.com/blueju/umi-qiankun/tree/runtime-dynamic-register-sub-app

查看原文

赞 0 收藏 0 评论 0

blueju 赞了文章 · 2020-09-27

build-react-cli一个React项目脚手架工具

build-react-cli

build-react-cli是帮助你快速创建生成react项目的脚手架工具,配置了多种可选择的不同类型项目模版。

Demonstration

Installation

npm install -g build-react

Usage

build-react init my-react

tip: 初始化项目是第一项选择项目模版提供三种类型。

  1. init为通用项目模版,配置了常用的react工具。
  2. complete-project为完整的react项目,包括请求工具,服务转发,登录注册,复杂嵌套路由。
  3. simple-project为简单项目模版,提供了react以及react-router基本配置。
初始化项目完成以后
cd my-react //进入项目目录
npm install //安装项目依赖
npm start //运行项目

command

启动项目

npm start 或者 npm run dev

启动项目后自动打开浏览器(传入 --o 参数)

npm start --o 或者npm start --open

打包项目

npm run build

打包项目并查看分析项目大小

npm run build --a

config

config目录下的index.js文件为项目webpack配置文件,包括有运行端口、地址、本地服务代理配置等一系列配置。

名称类型描述
hostString主机
portNumber端口
proxyTableObject代理配置
useEslintBoolean是否使用eslint
autoOpenBrowserBoolean是否自动打开浏览器
errorOverlayBoolean是否使用全屏报错提示
notifyOnErrorsBoolean是否使用消息通知
showEslintErrorsInOverlayBoolean是否使用eslint全屏报错提示
bundleAnalyzerReportBoolean是否使用打包编译完成后显示依赖分析

Github or Npm

Github or Npm

查看原文

赞 2 收藏 4 评论 3

blueju 收藏了文章 · 2020-09-21

万字长文+图文并茂+全面解析微前端框架 qiankun 源码 - qiankun 篇

写在开头

微前端系列文章:

本系列其他文章计划一到两个月内完成,点个 关注 不迷路。

计划如下:

  • 生命周期篇;
  • IE 兼容篇;
  • 生产环境部署篇;
  • 性能优化、缓存方案篇;

引言

本文将针对微前端框架 qiankun 的源码进行深入解析,在源码讲解之前,我们先来了解一下什么是 微前端

微前端 是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将单页面前端应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用还可以独立开发、独立部署。同时,它们也可以在共享组件的同时进行并行开发——这些组件可以通过 NPM 或者 Git Tag、Git Submodule 来管理。

qiankun(乾坤) 就是一款由蚂蚁金服推出的比较成熟的微前端框架,基于 single-spa 进行二次开发,用于将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。(见下图)

qiankun

那么,话不多说,我们的源码解析正式开始。

初始化全局配置 - start(opts)

我们从两个基础 API - registerMicroApps(apps, lifeCycles?) - 注册子应用start(opts?) - 启动主应用 开始,由于 registerMicroApps 函数中设置的回调函数较多,并且读取了 start 函数中设置的初始配置项,所以我们从 start 函数开始解析。

我们从 start 函数开始解析(见下图):

qiankun

我们对 start 函数进行逐行解析:

  • 第 196 行:设置 window__POWERED_BY_QIANKUN__ 属性为 true,在子应用中使用 window.__POWERED_BY_QIANKUN__ 值判断是否运行在主应用容器中。
  • 第 198~199 行:设置配置参数(有默认值),将配置参数存储在 importLoaderConfiguration 对象中;
  • 第 201~203 行:检查 prefetch 属性,如果需要预加载,则添加全局事件 single-spa:first-mount 监听,在第一个子应用挂载后预加载其他子应用资源,优化后续其他子应用的加载速度。
  • 第 205 行:根据 singularMode 参数设置是否为单实例模式。
  • 第 209~217 行:根据 jsSandbox 参数设置是否启用沙箱运行环境,旧版本需要关闭该选项以兼容 IE。(新版本在单实例模式下默认支持 IE,多实例模式依然不支持 IE)。
  • 第 222 行:调用了 single-spastartSingleSpa 方法启动应用,这个在 single-spa 篇我们会单独剖析,这里可以简单理解为启动主应用。

从上面可以看出,start 函数负责初始化一些全局设置,然后启动应用。这些初始化的配置参数有一部分将在 registerMicroApps 注册子应用的回调函数中使用,我们继续往下看。

注册子应用 - registerMicroApps(apps, lifeCycles?)

registerMicroApps 函数的作用是注册子应用,并且在子应用激活时,创建运行沙箱,在不同阶段调用不同的生命周期钩子函数。(见下图)

qiankun

从上面可以看出,在 第 70~71 行registerMicroApps 函数做了个处理,防止重复注册相同的子应用。

第 74 行 调用了 single-sparegisterApplication 方法注册了子应用。

我们直接来看 registerApplication 方法,registerApplication 方法是 single-spa 中注册子应用的核心函数。该函数有四个参数,分别是

  • name(子应用的名称)
  • 回调函数(activeRule 激活时调用)
  • activeRule(子应用的激活规则)
  • props(主应用需要传递给子应用的数据)

这些参数都是由 single-spa 直接实现,这里可以先简单理解为注册子应用(这个我们会在 single-spa 篇展开说)。在符合 activeRule 激活规则时将会激活子应用,执行回调函数,返回一些生命周期钩子函数(见下图)。

注意,这些生命周期钩子函数属于 single-spa,由 single-spa 决定在何时调用,这里我们从函数名来简单理解。(bootstrap - 初始化子应用,mount - 挂载子应用,unmount - 卸载子应用)

qiankun

如果你还是觉得有点懵,没关系,我们通过一张图来帮助理解。(见下图)

qiankun

获取子应用资源 - import-html-entry

我们从上面分析可以看出,qiankunregisterMicroApps 方法中第一个入参 apps - Array<RegistrableApp<T>> 有三个参数 name、activeRule、props 都是交给 single-spa 使用,还有 entryrender 参数还没有用到。

我们这里需要关注 entry(子应用的 entry 地址)render(子应用被激活时触发的渲染规则) 这两个还没有用到的参数,这两个参数延迟到 single-spa 子应用激活后的回调函数中执行。

那我们假设此时我们的子应用已激活,我们来看看这里做了什么。(见下图)

qiankun

从上图可以看出,在子应用激活后,首先在 第 81~84 行 处使用了 import-html-entry 库从 entry 进入加载子应用,加载完成后将返回一个对象(见下图)

qiankun

我们来解释一下这几个字段

字段解释
template将脚本文件内容注释后的 html 模板文件
assetPublicPath资源地址根路径,可用于加载子应用资源
getExternalScripts方法:获取外部引入的脚本文件
getExternalStyleSheets方法:获取外部引入的样式表文件
execScripts方法:执行该模板文件中所有的 JS 脚本文件,并且可以指定脚本的作用域 - proxy 对象

我们先将 template 模板getExternalScriptsgetExternalStyleSheets 函数的执行结果打印出来,效果如下(见下图):

qiankun

从上图我们可以看到我们外部引入的三个 js 脚本文件,这个模板文件没有外部 css 样式表,对应的样式表数组也为空。

然后我们再来分析 execScripts 方法,该方法的作用就是指定一个 proxy(默认是 window)对象,然后执行该模板文件中所有的 JS,并返回 JS 执行后 proxy 对象的最后一个属性(见下图 1)。在微前端架构中,这个对象一般会包含一些子应用的生命周期钩子函数(见下图 2),主应用可以通过在特定阶段调用这些生命周期钩子函数,进行挂载和销毁子应用的操作。

qiankun

qiankun

qiankunimportEntry 函数中还传入了配置项 getTemplate,这个其实是对 html 目标文件的二次处理,这里就不作展开了,有兴趣的可以自行去了解一下。

主应用挂载子应用 HTML 模板

我们回到 qiankun 源码部分继续看(见下图)

qiankun

从上图看出,在 第 85~87 行 处,先对单实例进行检测。在单实例模式下,新的子应用挂载行为会在旧的子应用卸载之后才开始。

第 88 行 中,执行注册子应用时传入的 render 函数,将 HTML Templateloading 作为入参,render 函数的内容一般是将 HTML 挂载在指定容器中(见下图)。

qiankun

在这个阶段,主应用已经将子应用基础的 HTML 结构挂载在了主应用的某个容器内,接下来还需要执行子应用对应的 mount 方法(如 Vue.$mount)对子应用状态进行挂载。

此时页面还可以根据 loading 参数开启一个类似加载的效果,直至子应用全部内容加载完成。

沙箱运行环境 - genSandbox

我们回到 qiankun 源码部分继续看,此时还是子应用激活时的回调函数部分(见下图)

qiankun

第 90~98 行qiankun 比较核心的部分,也是几个子应用之间状态独立的关键,那就是 js 的沙箱运行环境。如果关闭了 useJsSandbox 选项,那么所有子应用的沙箱环境都是 window,就很容易对全局状态产生污染。

我们进入到 genSandbox 内部,看看 qiankun 是如何创建的 (JS)沙箱运行环境。(见下图)

qiankun

从上图可以看出 genSandbox 内部的沙箱主要是通过是否支持 window.Proxy 分为 LegacySandboxSnapshotSandbox 两种。

扩展阅读:多实例还有一种 ProxySandbox 沙箱,这种沙箱模式目前看来是最优方案。由于其表现与旧版本略有不同,所以暂时只用于多实例模式。

ProxySandbox 沙箱稳定之后可能会作为单实例沙箱使用。

LegacySandbox

我们先来看看 LegacySandbox 沙箱是怎么进行状态隔离的(见下图)

qiankun

我们来分析一下 LegacySandbox 类的几个属性:

字段解释
addedPropsMapInSandbox记录沙箱运行期间新增的全局变量
modifiedPropsOriginalValueMapInSandbox记录沙箱运行期间更新的全局变量
currentUpdatedPropsValueMap记录沙箱运行期间操作过的全局变量。上面两个 Map 用于 关闭沙箱 时还原全局状态,而 currentUpdatedPropsValueMap 是在 激活沙箱 时还原沙箱的独立状态
name沙箱名称
proxy代理对象,可以理解为子应用的 global/window 对象
sandboxRunning当前沙箱是否在运行中
active激活沙箱,在子应用挂载时启动
inactive关闭沙箱,在子应用卸载时启动
constructor构造函数,创建沙箱环境

我们现在从 window.Proxysetget 属性来详细讲解 LegacySandbox 是如何实现沙箱运行环境的。(见下图)

qiankun

注意:子应用沙箱中的 proxy 对象(第 62 行)可以简单理解为子应用的 window 全局对象(代码如下),子应用对全局属性的操作就是对该 proxy 对象属性的操作,带着这份理解继续往下看吧。
// 子应用脚本文件的执行过程:
eval(
  // 这里将 proxy 作为 window 参数传入
  // 子应用的全局对象就是该子应用沙箱的 proxy 对象
  (function(window) {
    /* 子应用脚本文件内容 */
  })(proxy)
);

第 65~72 行中,当调用 set 向子应用 proxy/window 对象设置属性时,所有的属性设置和更新都会先记录在 addedPropsMapInSandboxmodifiedPropsOriginalValueMapInSandbox 中,然后统一记录到
currentUpdatedPropsValueMap 中。

第 73 行 中修改全局 window 的属性,完成值的设置。

当调用 get 从子应用 proxy/window 对象取值时,会直接从 window 对象中取值。对于非构造函数的取值将会对 this 指针绑定到 window 对象后,再返回函数。

LegacySandbox 的沙箱隔离是通过激活沙箱时还原子应用状态,卸载时还原主应用状态(子应用挂载前的全局状态)实现的,具体实现如下(见下图)。

qiankun

从上图可以看出:

  • 第 37 行:在激活沙箱时,沙箱会通过 currentUpdatedPropsValueMap 查询到子应用的独立状态池(沙箱可能会激活多次,这里是沙箱曾经激活期间被修改的全局变量),然后还原子应用状态。
  • 第 44~45 行:在关闭沙箱时,通过 addedPropsMapInSandbox 删除在沙箱运行期间新增的全局变量,通过 modifiedPropsOriginalValueMapInSandbox 还原沙箱运行期间被修改的全局变量,从而还原到子应用挂载前的状态。

从上面的分析可以得知,LegacySandbox 的沙箱隔离机制利用快照模式实现,我们画一张图来帮助理解(见下图)

qiankun

多实例沙箱 - ProxySandbox

ProxySandbox 是一种新的沙箱模式,目前用于多实例模式的状态隔离。在稳定后以后可能会成为 单实例沙箱,我们来看看 ProxySandbox 沙箱是怎么进行状态隔离的(见下图)

qiankun

我们来分析一下 ProxySandbox 类的几个属性:

字段解释
updateValueMap记录沙箱中更新的值,也就是每个子应用中独立的状态池
name沙箱名称
proxy代理对象,可以理解为子应用的 global/window 对象
sandboxRunning当前沙箱是否在运行中
active激活沙箱,在子应用挂载时启动
inactive关闭沙箱,在子应用卸载时启动
constructor构造函数,创建沙箱环境

我们现在从 window.Proxysetget 属性来详细讲解 ProxySandbox 是如何实现沙箱运行环境的。(见下图)

qiankun

注意:子应用沙箱中的 proxy 对象可以简单理解为子应用的 window 全局对象(代码如下),子应用对全局属性的操作就是对该 proxy 对象属性的操作,带着这份理解继续往下看吧。
// 子应用脚本文件的执行过程:
eval(
  // 这里将 proxy 作为 window 参数传入
  // 子应用的全局对象就是该子应用沙箱的 proxy 对象
  (function(window) {
    /* 子应用脚本文件内容 */
  })(proxy)
);

当调用 set 向子应用 proxy/window 对象设置属性时,所有的属性设置和更新都会命中 updateValueMap,存储在 updateValueMap 集合中(第 38 行),从而避免对 window 对象产生影响(旧版本则是通过 diff 算法还原 window 对象状态快照,子应用之间的状态是隔离的,而父子应用之间 window 对象会有污染)。

当调用 get 从子应用 proxy/window 对象取值时,会优先从子应用的沙箱状态池 updateValueMap 中取值,如果没有命中才从主应用的 window 对象中取值(第 49 行)。对于非构造函数的取值将会对 this 指针绑定到 window 对象后,再返回函数。

如此一来,ProxySandbox 沙箱应用之间的隔离就完成了,所有子应用对 proxy/window 对象值的存取都受到了控制。设置值只会作用在沙箱内部的 updateValueMap 集合上,取值也是优先取子应用独立状态池(updateValueMap)中的值,没有找到的话,再从 proxy/window 对象中取值。

相比较而言,ProxySandbox 是最完备的沙箱模式,完全隔离了对 window 对象的操作,也解决了快照模式中子应用运行期间仍然会对 window 造成污染的问题。

我们对 ProxySandbox 沙箱画一张图来加深理解(见下图)

qiankun

SnapshotSandbox

在不支持 window.Proxy 属性时,将会使用 SnapshotSandbox 沙箱,我们来看看其内部实现(见下图)

qiankun

我们来分析一下 SnapshotSandbox 类的几个属性:

字段解释
name沙箱名称
proxy代理对象,此处为 window 对象
sandboxRunning当前沙箱是否激活
windowSnapshotwindow 状态快照
modifyPropsMap沙箱运行期间被修改过的 window 属性
constructor构造函数,激活沙箱
active激活沙箱,在子应用挂载时启动
inactive关闭沙箱,在子应用卸载时启动

SnapshotSandbox 的沙箱环境主要是通过激活时记录 window 状态快照,在关闭时通过快照还原 window 对象来实现的。(见下图)

qiankun

我们先看 active 函数,在沙箱激活时,会先给当前 window 对象打一个快照,记录沙箱激活前的状态(第 38~40 行)。打完快照后,函数内部将 window 状态通过 modifyPropsMap 记录还原到上次的沙箱运行环境,也就是还原沙箱激活期间(历史记录)修改过的 window 属性。

在沙箱关闭时,调用 inactive 函数,在沙箱关闭前通过遍历比较每一个属性,将被改变的 window 对象属性值(第 54 行)记录在 modifyPropsMap 集合中。在记录了 modifyPropsMap 后,将 window 对象通过快照 windowSnapshot 还原到被沙箱激活前的状态(第 55 行),相当于是将子应用运行期间对 window 造成的污染全部清除。

SnapshotSandbox 沙箱就是利用快照实现了对 window 对象状态隔离的管理。相比较 ProxySandbox 而言,在子应用激活期间,SnapshotSandbox 将会对 window 对象造成污染,属于一个对不支持 Proxy 属性的浏览器的向下兼容方案。

我们对 SnapshotSandbox 沙箱画一张图来加深理解(见下图)

qiankun

挂载沙箱 - mountSandbox

qiankun

我们继续回到这张图,genSandbox 函数不仅返回了一个 sandbox 沙箱,还返回了一个 mountunmount 方法,分别在子应用挂载时和卸载时的时候调用。

我们先看看 mount 函数内部(见下图)

qiankun

首先,在 mount 内部先激活了子应用沙箱(第 26 行),在沙箱启动后开始劫持各类全局监听(第 27 行),我们这里重点看看 patchAtMounting 内部是怎么实现的。(见下图)

qiankun

patchAtMounting 内部调用了下面四个函数:

  • patchTimer(计时器劫持)
  • patchWindowListener(window 事件监听劫持)
  • patchHistoryListener(window.history 事件监听劫持)
  • patchDynamicAppend(动态添加 Head 元素事件劫持)

上面四个函数实现了对 window 指定对象的统一劫持,我们可以挑一些解析看看其内部实现。

计时器劫持 - patchTimer

我们先来看看 patchTimer 对计时器的劫持(见下图)

qiankun

从上图可以看出,patchTimer 内部将 setInterval 进行重载,将每个启用的定时器的 intervalId 都收集起来(第 23~24 行),以便在子应用卸载时调用 free 函数将计时器全部清除(见下图)。

qiankun

我们来看看在子应用加载时的 setInterval 函数验证即可(见下图)

qiankun

从上图可以看出,在进入子应用时,setInterval 已经被替换成了劫持后的函数,防止全局计时器泄露污染。

动态添加样式表和脚本文件劫持 - patchDynamicAppend

patchWindowListenerpatchHistoryListener 的实现都与 patchTimer 实现类似,这里就不作复述了。

我们需要重点对 patchDynamicAppend 函数进行解析,这个函数的作用是劫持对 head 元素的操作(见下图)

qiankun

从上图可以看出,patchDynamicAppend 主要是对动态添加的 style 样式表和 script 标签做了处理。

我们先看看对 style 样式表的处理(见下图)

qiankun

从上图可以看出,主要的处理逻辑在 第 68~74 行,如果当前子应用处于激活状态(判断子应用的激活状态主要是因为:当主应用切换路由时可能会自动添加动态样式表,此时需要避免主应用的样式表被添加到子应用 head 节点中导致出错),那么动态 style 样式表就会被添加到子应用容器内(见下图),在子应用卸载时样式表也可以和子应用一起被卸载,从而避免样式污染。同时,动态样式表也会存储在 dynamicStyleSheetElements 数组中,在后面还会提到其用处。

qiankun

我们再来看看对 script 脚本文件的处理(见下图)

qiankun

对动态 script 脚本文件的处理较为复杂一些,我们也来解析一波:

第 83~101 行 处对外部引入的 script 脚本文件使用 fetch 获取,然后使用 execScripts 指定 proxy 对象(作为 window 对象)后执行脚本文件内容,同时也触发了 loaderror 两个事件。

第 103~106 行 处将注释后的脚本文件内容以注释的形式添加到子应用容器内。

第 109~113 行 是对内嵌脚本文件的执行过程,就不作复述了。

我们可以看出,对动态添加的脚本进行劫持的主要目的就是为了将动态脚本运行时的 window 对象替换成 proxy 代理对象,使子应用动态添加的脚本文件的运行上下文也替换成子应用自身。

HTMLHeadElement.prototype.removeChild 的逻辑就是多加了个子应用容器判断,其他无异,就不展开说了。

最后我们来看看 free 函数(见下图)

qiankun

这个 free 函数与其他的 patches(劫持函数) 实现不太一样,这里缓存了一份 cssRules,在重新挂载的时候会执行 rebuild 函数将其还原。这是因为样式元素 DOM 从文档中删除后,浏览器会自动清除样式元素表。如果不这么做的话,在重新挂载时会出现存在 style 标签,但是没有渲染样式的问题。

卸载沙箱 - unmountSandbox

我们再回到 mount 函数本身(见下图)

qiankun

从上图可以看出,在 patchAtMounting 函数中劫持了各类全局监听,并返回了解除劫持的 free 函数。在卸载应用时调用 free 函数解除这些全局监听的劫持行为(见下图)

qiankun

从上图可以看到 sideEffectsRebuildersfree 后被返回,在 mount 的时候又将被调用 rebuild 重建动态样式表。这块环环相扣,是稍微有点绕,没太看明白的同学可以翻上去再看一遍。

到这里,qiankun 的最核心部分-沙箱机制,我们就已经解析完毕了,接下来我们继续剖析别的部分。

在这里我们画一张图,对沙箱的创建过程进行一个总梳理(见下图)

qiankun

注册内部生命周期函数

在创建好了沙箱环境后,在 第 100~106 行 注册了一些内部生命周期函数(见下图)

qiankun

在上图中,第 106 行mergeWith 方法的作用是将内置的生命周期函数与传入的 lifeCycles 生命周期函数。

这里的 lifeCycles 生命周期函数指的是全子应用共享的生命周期函数,可用于执行多个子应用间相同的逻辑操作,例如 加载效果 之类的。(见下图)

qiankun

除了外部传入的生命周期函数外,我们还需要关注 qiankun 内置的生命周期函数做了些什么(见下图)

qiankun

我们对上图的代码进行逐一解析:

  • 第 13~15 行:在加载子应用前 beforeLoad(只会执行一次)时注入一个环境变量,指示了子应用的 public 路径。
  • 第 17~19 行:在挂载子应用前 beforeMount(可能会多次执行)时可能也会注入该环境变量。
  • 第 23~30 行:在卸载子应用前 beforeUnmount 时将环境变量还原到原始状态。

通过上面的分析我们可以得出一个结论,我们可以在子应用中获取该环境变量,将其设置为 __webpack_public_path__ 的值,从而使子应用在主应用中运行时,可以匹配正确的资源路径。(见下图)

qiankun

触发 beforeLoad 生命周期钩子函数

在注册完了生命周期函数后,立即触发了 beforeLoad 生命周期钩子函数(见下图)

qiankun

从上图可以看出,在 第 108 行 中,触发了 beforeLoad 生命周期钩子函数。

随后,在 第 110 行 执行了 import-html-entryexecScripts 方法。指定了脚本文件的运行沙箱(jsSandbox),执行完子应用的脚本文件后,返回了一个对象,对象包含了子应用的生命周期钩子函数(见下图)。

qiankun

第 112~121 行 对子应用的生命周期钩子函数做了个检测,如果在子应用的导出对象中没有发现生命周期钩子函数,会在沙箱对象中继续查找生命周期钩子函数。如果最后没有找到生命周期钩子函数则会抛出一个错误,所以我们的子应用一定要有 bootstrap, mount, unmount 这三个生命周期钩子函数才能被 qiankun 正确嵌入到主应用中。

这里我们画一张图,对子应用挂载前的初始化过程做一个总梳理(见下图)

qiankun

进入到 mount 挂载流程

在一些初始化配置(如 子应用资源、运行沙箱环境、生命周期钩子函数等等)准备就绪后,qiankun 内部将其组装在一起,返回了三个函数作为 single-spa 内部的生命周期函数(见下图)

qiankun

single-spa 内部的逻辑我们后面再展开说,这里我们可以简单理解为 single-spa 内部的三个生命周期钩子函数:

  • bootstrap:子应用初始化时调用,只会调用一次;
  • mount:子应用挂载时调用,可能会调用多次;
  • unmount:子应用卸载时调用,可能会调用多次;

我们可以看出,在 bootstrap 阶段调用了子应用暴露的 bootstrap 生命周期函数。

我们这里对 mount 阶段进行展开,看看在子应用 mount 阶段执行了哪些函数(见下图)

qiankun

我们进行逐行解析:

  • 第 127~133 行:对单实例模式进行检测。在单实例模式下,新的子应用挂载行为会在旧的子应用卸载之后才开始。(由于这里是串行顺序执行,所以如果某一处发生阻塞的话,会阻塞所有后续的函数执行)
  • 第 134 行:执行注册子应用时传入的 render 函数,将 HTML Templateloading 作为入参。这里一般是在发生了一次 unmount 后,再次进行 mount 挂载行为时将 HTML 挂载在指定容器中(见下图)

    由于初始化的时候已经调用过一次 render,所以在首次调用 mount 时可能已经执行过一次 render 方法。

    在下面的代码中也有对重复挂载的情况进行判断的语句 - if (frame.querySelector("div") === null,防止重复挂载子应用。

qiankun

  • 第 135 行:触发了 beforeMount 全局生命周期钩子函数;
  • 第 136 行:挂载沙箱,这一步中激活了对应的子应用沙箱,劫持了部分全局监听(如 setInterval)。此时开始子应用的代码将在沙箱中运行。(反推可知,在 beforeMount 前的部分全局操作将会对主应用造成污染,如 setInterval
  • 第 137 行:触发子应用的 mount 生命周期钩子函数,在这一步通常是执行对应的子应用的挂载操作(如 ReactDOM.render、Vue.$mount。(见下图)

qiankun

  • 第 138 行:再次调用 render 函数,此时 loading 参数为 false,代表子应用已经加载完成。
  • 第 139 行:触发了 afterMount 全局生命周期钩子函数;
  • 第 140~144 行:在单实例模式下设置 prevAppUnmountedDeferred 的值,这个值是一个 promise,在当前子应用卸载时才会被 resolve,在该子应用运行期间会阻塞其他子应用的挂载动作(第 134 行);

我们在上面很详细的剖析了整个子应用的 mount 挂载流程,如果你还没有搞懂的话,没关系,我们再画一个流程图来帮助理解。(见下图)

qiankun

进入到 unmount 卸载流程

我们刚才梳理了子应用的 mount 挂载流程,我们现在就进入到子应用的 unmount 卸载流程。在子应用激活阶段, activeRule 未命中时将会触发 unmount 卸载行为,具体的行为如下(见下图)

qiankun

从上图我们可以看出,unmount 卸载流程要比 mount 简单很多,我们直接来梳理一下:

  • 第 148 行:触发了 beforeUnmount 全局生命周期钩子函数;
  • 第 149 行:这里与 mount 流程的顺序稍微有点不同,这里先执行了子应用的 unmount 生命周期钩子函数,保证子应用仍然是运行在沙箱内,避免造成状态污染。在这里一般是对子应用的一些状态进行清理和卸载操作。(如下图,销毁了刚才创建的 vue 实例)

qiankun

  • 第 150 行:卸载沙箱,关闭了沙箱的激活状态。
  • 第 151 行:触发了 afterUnmount 全局生命周期钩子函数;
  • 第 152 行:触发 render 方法,并且传入的 appContent 为空字符串,此处可以清空主应用容器内的内容。
  • 第 153~156 行:当前子应用卸载完成后,在单实例模式下触发 prevAppUnmountedDeferred.resolve(),使其他子应用的挂载行为得以继续进行,不再阻塞。

我们对 unmount 卸载流程也画一张图,帮助大家理解(见下图)。

qiankun

总结

到这里,我们对 qiankun 框架的总流程梳理就差不多了。这里应该做个总结,大家看了这么多文字,估计大家也看累了,最后用一张图对 qiankun 的总流程进行总结吧。

qiankun

彩蛋

qiankun

展望

传统的云控制台应用,几乎都会面临业务快速发展之后,单体应用进化成巨石应用的问题。我们要如何维护一个巨无霸中台应用?

上面这个问题引出了微前端架构理念,所以微前端的概念也越来越火,我们团队最近也在尝试转型微前端架构。

工欲善其事必先利其器,所以本文针对 qiankun 的源码进行解读,在分享知识的同时也是帮助自己理解。

这是我们团队对微前端架构的最佳实践(见下图),如果有需求的话,可以在评论区留言,我们会考虑出一篇《微前端框架 qiankun 最佳实践》来帮助大家搭建一套微前端架构。

架构图

最后一件事

如果您已经看到这里了,希望您还是点个赞再走吧~

您的点赞是对作者的最大鼓励,也可以让更多人看到本篇文章!

如果觉得本文对您有帮助,请帮忙在 github 上点亮 star 鼓励一下吧!

personal

查看原文

blueju 赞了文章 · 2020-09-19

react状态组件state定义在constructor里外的区别

react状态组件中state定义方式分为两种,一种是在construcor中定义;另一种是在constructor外面定义。网上有些人说state属性定义在constructor里面时,state挂在组件实例上,定义在constructor外面会挂在原型对象上。为了方便分析两种定义方式之间的区别,下面通过babel转译到ES5

1、配置好babel

这里我们采用babel7版本

cnpm i @babel/core @babel/cli @babel/preset-env @babel/preset-react -D

在项目根目录下新建.babelrc配置文件,babel命令默认寻找该文件

// .babelrc
{
    presets: ["@babel/preset-env", "@babel/preset-react"]
}

package.json添加指令脚本

"scripts": {
    "babel": "babel ./demo3.jsx -o build.js"
  },

2、编写class状态组件

state在constructor里面

import React from 'react'

export default class App extends React.Component {
  constructor() {
    super()
    this.state = {
      count: 0
    }
  }
  render() {
    return <div>{this.state.count}</div>
  }
}

state在constructor外面

安装@babel/plugin-proposal-class-properties,修改.babelrc

{
  "presets": ["@babel/preset-env", "@babel/preset-react"],
  "plugins": ["@babel/plugin-proposal-class-properties"]
}
import React from 'react'

export default class App extends React.Component {
  // 下面state写法属于ES7语法,目前浏览器还不支持,需要安装@babel/plugin-proposal-class-properties,并在.babelrc中plugins属性中配置
  state = {
    count: 0
  }

  render() {
    return <div>{this.state.count}</div>
  }
}

3、比较两者转义结果

state在constructor里面

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports["default"] = void 0;

var _react = _interopRequireDefault(require("react"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }

function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }

function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }

function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }

function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function () { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }

function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }

function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }

function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }

function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }

var App = /*#__PURE__*/function (_React$Component) {
  _inherits(App, _React$Component);

  var _super = _createSuper(App);

  // Support for the experimental syntax 'classProperties' isn't currently enabled
  // Add @babel/plugin-proposal-class-properties (https://git.io/vb4SL) to the 'plugins' section of your Babel config to enable transformation.
  function App() {
    var _this;

    _classCallCheck(this, App);

    _this = _super.call(this);
    _this.state = {
      count: 0
    };
    return _this;
  }

  _createClass(App, [{
    key: "render",
    value: function render() {
      return /*#__PURE__*/_react["default"].createElement("div", null, this.state.count);
    }
  }]);

  return App;
}(_react["default"].Component);

exports["default"] = App;

state在constructor外面

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports["default"] = void 0;

var _react = _interopRequireDefault(require("react"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }

function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }

function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }

function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }

function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function () { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }

function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }

function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }

function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }

function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

var App = /*#__PURE__*/function (_React$Component) {
  _inherits(App, _React$Component);

  var _super = _createSuper(App);

  function App() {
    var _this;

    _classCallCheck(this, App);

    for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
      args[_key] = arguments[_key];
    }

    _this = _super.call.apply(_super, [this].concat(args));

    _defineProperty(_assertThisInitialized(_this), "state", {
      count: 0
    });

    return _this;
  }

  _createClass(App, [{
    key: "render",
    value: function render() {
      return /*#__PURE__*/_react["default"].createElement("div", null, this.state.count);
    }
  }]);

  return App;
}(_react["default"].Component);

exports["default"] = App;

转义后导出规则采用的commonjs,我们写一个index.js导入App来看看state属性在哪?

// index.js
const App = require('./build.js').default // 得到的是App构造函数
console.log(new App())
console.log(App.prototype)

state定义在constructor里面打印结果

App {
  props: undefined,
  context: undefined,
  refs: {},
  updater: {
    isMounted: [Function: isMounted],
    enqueueForceUpdate: [Function: enqueueForceUpdate],
    enqueueReplaceState: [Function: enqueueReplaceState],
    enqueueSetState: [Function: enqueueSetState]
  },
  state: { count: 0 }
}
App {}

state定义在constructor外面打印结果

App {
  props: undefined,
  context: undefined,
  refs: {},
  updater: {
    isMounted: [Function: isMounted],
    enqueueForceUpdate: [Function: enqueueForceUpdate],
    enqueueReplaceState: [Function: enqueueReplaceState],
    enqueueSetState: [Function: enqueueSetState]
  },
  state: { count: 0 }
}
App {}

我们看到这两种定义方式转义后打印结果是一样的,state属性都是挂在组件实例上。

参考:
https://github.com/babel/babe...

查看原文

赞 2 收藏 0 评论 0

blueju 发布了文章 · 2020-09-13

搭建一个开发 UI 组件库的工程

前言

公司项目需要开发一套 UI 组件库,提供给其他项目组使用,技术栈是 React,因此选用了 alibaba 的 father,它已经帮我做了目录规范、文件规范、打包配置、配套文档、UI组件测试等一系列工作,并提供了一个简单 UI 组件库示例,供使用者参考。


father,前身的是 umi-plugin-library。


father:https://github.com/umijs/father
umi-plugin-library:https://github.com/umijs/umi-plugin-library


umi 作者 sorrycc 录制的视频使用教程:
https://www.bilibili.com/video/av47853431

搭建工程

在 father 的 github 仓库中,它并没有提供搭建教程或脚手架,而是直接在 README.md 中写使用文档。

搭建方式

经过一番折腾和研究,发现搭建起工程有两种方式,

  1. 参照视频教程,从零开始搭建;
  2. 使用 umi v2 版本中提供的脚手架;

image.png

搭建过程

至于第一种方式我不过多阐述,照着视频即可,在此更多的是说第二种方式:使用 umi v2 提供的脚手架。


具体创建过程如下图:

其中有几个选项,可自行根据情况选择或填写。

image.png

注意事项

使用 umi 脚手架搭建的 father 依赖版本是 2.16.0,估计是阿里的人懒得维护升级了,不过我自行尝试过升级,可正常启动、打包。

结语

剩下的就是自行根据 github 仓库的 issue 、作者的视频教程和官方文档进行探索了。

文档一

https://github.com/umijs/father/blob/2.x/README.md

文档二

https://umijs.github.io/umi-plugin-library/#/

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 5 次点赞
  • 获得 5 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 5 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-02-16
个人主页被 610 人浏览