仅此而已

仅此而已 查看完整档案

杭州编辑  |  填写毕业院校  |  填写所在公司/组织 15858209984@163.com 编辑
编辑

在自己的专业领域:勇于思考,勇于探索,勇于创新,勇于实践。

个人动态

仅此而已 回答了问题 · 2020-10-20

React Hook useEffect has a missing dependency

有2种方法 :
1、setList(newList);前对比手动对比list,如果一样不执行setList
2、checked这个状态单独维护一个变量

关注 3 回答 2

仅此而已 赞了文章 · 2020-07-02

细说 js 压缩、sourcemap、通过 sourcemap 查找原始报错信息

细说 js 压缩、sourcemap、通过 sourcemap 查找原始报错信息

1. js 压缩

js 压缩对前端开发者来说是一门必修课。

一般来说,压缩 js 主要出于以下两个目的:

  1. 减小代码体积,加快前端资源加载速度
  2. 保护源代码不被别人获取

压缩 js 使用的工具库:

压缩 js 的主要过程:

  1. 移除无用代码
  2. 混淆代码中变量名称、函数名称等
  3. 预编译代码
  4. 对结构进行扁平化处理

1. 移除无用代码

去掉所有对解析引擎来说无用的字符,包括空格、注释、换行、没有用的变量声明、函数声明等。

2. 混淆代码中变量名称、函数名称等

把一些局部变量名称、函数名称等用 a, b, ...$1, $2, ..._1, _2, ... 之类的简略字符进行替换,达到混淆的目的。

源代码

(function () {
  var hello = 'hi';
  var print = function (str) {
    console.log(str);
  };

  print(hello);
})();

压缩后的代码(仅演示混淆功能)

(function () {
  var a = 'hi';
  var b = function (c) {
    console.log(c);
  };

  b(a);
})();

3. 预编译代码

把不依赖外部环境的逻辑提前进行运算,并把运算结果替换到相应的源码处,然后从源码中移除这段逻辑。

源代码

(function () {
  var hello = 'hi' + ' everyone, ';
  var count = 3 * 5;

  console.log(hello + count + ' girls');
})();

压缩后的代码(仅演示预编译功能)

(function () {
  var hello = 'hi everyone, ';
  var count = 15;

  console.log(hello + count + ' girls');
})();

4. 对结构进行扁平化处理

对于 js 来说,嵌套越深,执行越慢,对代码进行扁平化处理也是优化代码的一种方式。

源代码

(function () {
  var say = {
    hello: function (str) {
      console.log('hello ' + str);
    }
  };

  say.hello('everyone');
})();

压缩后的代码(仅演示扁平化结构功能)

!function(str){console.log("hello "+str)}("everyone");

完整示例

源代码

(function () {
  var say = {
    hello: function (str) {
      console.log('hello ' + str);
    }
  };

  say.hello('everyone');
})();

压缩后的代码

!function(l){console.log("hello "+l)}("50 girls");

2. sourcemap

通常 js 压缩后只有一行代码,并且里面的变量名与函数名等都是混淆了的,这在实际运行中会有一个问题,就是 js 的报错信息将会失真,无法追踪到是在源代码哪一行哪一列报的错。

sourcemap 便是为了解决这个问题而生的。

sourcemap 文件就是记录了从源代码文件到压缩文件的一个代码对应关系记录表,通过压缩文件和 sourcemap 文件可以原原本本找出源代码文件。

查看阮一峰老师的 JavaScript Source Map 详解 了解 sourcemap 的原理与格式。

一般在压缩 js 的过程中,会生成相应的 sourcemap 文件,并且在压缩的 js 文件末尾追加 sourcemap 文件的链接 //# sourceMappingURL=bundle-file-name.js.map。这样,浏览器在加载这个压缩 js 的时候,就知道还有一个相应的 sourcemap 文件,也一并加载下来,运行的过程中如果 js 报错,也会给出相应源代码的行号与列号,而非压缩文件的。

比如,对下面的源码进行压缩:

(function () {
  var say = {
    hi: function () {
      console.log('hi');
    }
  };

  say.hello();

  return say;
})();

未加 sourcemap 文件时,报错信息是:

图片描述

图片描述

加上 sourcemap 文件时,报错信息是:

图片描述

图片描述

sourcemap 扩展

webpacksourcemap 做了扩展,定义在 devtool 配置项中:

  • eval: 每个模块都使用 eval() 执行,并且都有 //@ sourceURL,构建很快,但无法正确显示行号
  • eval-source-map: 每个模块使用 eval() 执行,并且 source map 转换为 DataUrl 后添加到 eval() 中,一般开发模式中使用这种方式
  • cheap-eval-source-map: 类似 eval-source-map,但只映射行,不映射列,并忽略源自 loadersource map,仅显示转译后的代码
  • cheap-module-eval-source-map: 类似 cheap-eval-source-map,但会保留源自 loadersource map
  • inline-source-map: source map 转换为 DataUrl 后添加到 bundle
  • cheap-source-map: 只映射行,不映射列,并忽略源自 loadersource map,仅显示转译后的代码
  • inline-cheap-source-map: inline-source-mapcheap-source-map 的结合
  • cheap-module-source-map: 类似 cheap-module-eval-source-map,但不使用 eval() 执行
  • inline-cheap-module-source-map: inline-source-mapcheap-module-source-map 的结合
  • source-map: 整个 source map 作为一个单独的文件生成,产品环境一般使用这种模式
  • hidden-source-map: 类似 source-map,但不会把 //# sourceMappingURL=bundle-file-name.js.map 追加到压缩文件后面
  • nosources-source-map: 类似 source-map,但只有堆栈信息,没有源码信息

更详细信息可以参考:

使用建议

对于使用 webpack 来构建项目,建议在开发时使用 eval-source-map,产品环境使用 source-map

因为用压缩文件与 sourcemap 文件是可以原原本本的找到源代码的,所以,为了保护源代码,可以这样隐藏 sourcemap 文件:

  1. web 服务器设置外部不能访问 sourcemap 文件,只能内部访问
  2. 直接把 sourcemap 文件存放到其他地方

3. 通过 sourcemap 查找原始报错信息

一般而言,在产品阶段,我们会用 window.onerror 来捕获 js 报错,然后上报到服务器,以此来收集用户使用时发生的 bug

window.onerror = function(message, source, lineno, colno, error) {
  // message: 错误信息
  // source: 报错脚本的 url 地址
  // lineno: 行号
  // colno: 列号
  // error: 错误对象
  // 上报必要的信息到服务器
}

但产品环境的代码都是压缩的,行号和列号都是失真的,所以就需要用 sourcemap 文件来找到错误对应源代码的行号与列号,以及其他的信息。

使用工具: mozilla/source-map

源代码

(function () {
  var say = {
    hi: function () {
      console.log('hi');
    }
  };

  say.hello();

  return say;
})();

压缩后报错信息

window.onerror = function(message, source, lineno, colno, error) {
  console.log(`message: ${message}`);
  console.log(`source: ${source}`);
  console.log(`lineno: ${lineno}`);
  console.log(`colno: ${colno}`);
  console.log(`error: ${error}`);
}

// message: Uncaught TypeError: e.hello is not a function
// source: url/to/bundle.min.js
// lineno: 1
// colno: 982
// error: TypeError: e.hello is not a function

通过 source-map 查找原始报错信息

const fs = require('fs');
const SourceMap = require('source-map');

const { readFileSync } = fs;
const { SourceMapConsumer } = SourceMap;

const rawSourceMap = JSON.parse(readFileSync('path/to/js/map/file', 'utf8'));

SourceMapConsumer.with(rawSourceMap, null, consumer => {
  const pos = consumer.originalPositionFor({
    line: 1,
    column: 982
  });

  console.log(pos);
});

查找到的原始信息

{ 
  source: 'path/to/index.js',
  line: 8,
  column: 7,
  name: 'hello' 
}

这样,便找到了原始报错信息:

  • 原始报错文件:path/to/index.js
  • 原始报错行号:8
  • 原始报错列号:7
  • 原始对象名称:hello

如此,便能一下子就找到错误在哪里了。

更多用法,参考 mozilla/source-map

后续

更多博客,查看 https://github.com/senntyou/blogs

作者:深予之 (@senntyou)

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证

查看原文

赞 49 收藏 33 评论 5

仅此而已 赞了文章 · 2019-12-05

使用 VSCode + ESLint 实践前端编码规范

在团队的项目开发过程中,代码维护所占的时间比重往往大于新功能的开发。因此编写符合团队编码规范的代码是至关重要的,这样做不仅可以很大程度地避免基本语法错误,也保证了代码的可读性,毕竟:

程序是写给人读的,只是偶尔让计算机执行一下。—— Donald Knuth

本文将讲解如何在 VSCode 中配合 ESLint 扩展来实践团队内部的前端编码规范。

什么是 ESLint

ESLint中文站点)是一个开源的 JavaScript 代码检查工具,使用 Node.js 编写,由 Nicholas C. Zakas 于 2013 年 6 月创建。ESLint 的初衷是为了让程序员可以创建自己的检测规则,使其可以在编码的过程中发现问题而不是在执行的过程中。ESLint 的所有规则都被设计成可插入的,为了方便使用,ESLint 内置了一些规则,在这基础上也可以增加自定义规则。

安装 ESLint 扩展

安装环境

安装 ESLint 扩展

首先,打开 VSCode 扩展面板并搜索 ESLint 扩展,然后点击安装

图片描述

安装完毕之后点击 重新加载 以激活扩展,但想要让扩展进行工作,我们还需要先进行 ESLint 的安装配置。

安装 ESLint

如果你仅仅想让 ESLint 成为你项目构建系统的一部分,我们可以在项目根目录进行本地安装:

$ npm install eslint --save-dev

如果想使 ESLint 适用于你所有的项目,我们建议使用全局安装,使用全局安装 ESLint 后,你使用的任何 ESLint 插件或可分享的配置也都必须在全局安装。

这里我们使用全局安装:

$ npm install -g eslint

安装完毕后,我们使用 eslint --init 命令在用户目录中生成一个配置文件(也可以在任何你喜欢的位置进行生成)

图片描述

我们在第一个选项中选择自定义代码风格,之后根据需要自行选择。

设置完成后我们会得到一份文件名为 .eslintrc.js 的配置文件:

module.exports = {
    "env": {
        "browser": true,
        "commonjs": true,
        "es6": true
    },
    "extends": "eslint:recommended",
    "parserOptions": {
        "sourceType": "module"
    },
    "rules": {
        "indent": [
            "error",
            4
        ],
        "linebreak-style": [
            "error",
            "windows"
        ],
        "quotes": [
            "error",
            "single"
        ],
        "semi": [
            "error",
            "never"
        ]
    }
};

配置 ESLint

配置文件生成之后,我们接着可以进行自定义修改,这里我们只粗略讲解常用的配置项,完整的可配置项可访问官方文档

配置环境

在上文生成的配置文件中可以使用 env 属性来指定要启用的环境,将其设置为 true,以保证在进行代码检测时不会把这些环境预定义的全局变量识别成未定义的变量而报错:

"env": {
    "browser": true,
    "commonjs": true,
    "es6": true,
    "jquery": true
}

设置语言选项

默认情况下,ESLint 支持 ECMAScript 5 语法,如果你想启用对 ECMAScript 其它版本和 JSX 等的支持,ESLint 允许你使用 parserOptions 属性进行指定想要支持的 JavaScript 语言选项,不过你可能需要自行安装 eslint-plugin-react 等插件。

"parserOptions": {
    "ecmaVersion": 6,
    "sourceType": "module",
    "ecmaFeatures": {
        "jsx": true
    }
}

配置规则

在上文的配置文件中, "extends": "eslint:recommended" 选项表示启用推荐规则,在推荐规则的基础上我们还可以根据需要使用 rules 新增自定义规则,每个规则的第一个值都是代表该规则检测后显示的错误级别:

  • "off"0 - 关闭规则

  • "warn"1 - 将规则视为一个警告

  • "error"2 - 将规则视为一个错误

"rules": {
    "indent": [
        "error",
        4
    ],
    "linebreak-style": [
        "error",
        "windows"
    ],
    "quotes": [
        "error",
        "single"
    ],
    "semi": [
        "error",
        "never"
    ]
}

完整的可配置规则列表可访问:http://eslint.cn/docs/rules/

其中带 标记的表示该规则为推荐规则。

设置 ESLint 扩展

安装并配置完成 ESLint 后,我们继续回到 VSCode 进行扩展设置,依次点击 文件 > 首选项 > 设置 打开 VSCode 配置文件

图片描述

图片描述

从左侧系统设置中可以看到,ESLint 扩展默认已经启用,我们现在只需在右侧用户设置中添加配置来指定我们创建的 .eslintrc.js 配置文件路径即可启用自定义规则检测,ESLint 会查找并自动读取它们:

"eslint.options": {
    "configFile": "E:/git/github/styleguide/eslint/.eslintrc.js"
},

至此,我们已经可以使用 ESLint 扩展来检测我们的 js 文件了。

让 ESLint 支持 Vue 单文件组件

由于 ESLint 默认只支持 js 文件的脚本检测,如果我们需要支持类 html 文件(如 vue)的内联脚本检测,还需要安装 eslint-plugin-html 插件。

因为我们使用全局安装了 ESLint,所以 eslint-plugin-html 插件也必须进行全局安装:

$ npm install -g eslint-plugin-html

安装完成后,我们再次打开 文件 > 首选项 > 设置,在右侧用户设置中修改 ESLint 的相关配置并保存:

"eslint.options": {
    "configFile": "E:/git/github/styleguide/eslint/.eslintrc.js",
    "plugins": ["html"]
},
"eslint.validate": [
    "javascript",
    "javascriptreact",
    "html",
    "vue"
]

图片描述

最后,我们打开一个 vue 文件,可以发现 ESLint 扩展已经正常工作了,嗯,enjoy yourself (●ˇ∀ˇ●)

查看原文

赞 118 收藏 153 评论 15

仅此而已 赞了文章 · 2019-11-19

实施微前端的六种方式

微前端架构是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用

由此带来的变化是,这些前端应用可以独立运行独立开发独立部署。以及,它们应该可以在共享组件的同时进行并行开发——这些组件可以通过 NPM 或者 Git Tag、Git Submodule 来管理。

注意:这里的前端应用指的是前后端分离的单应用页面,在这基础才谈论微前端才有意义。

结合我最近半年在微前端方面的实践和研究来看,微前端架构一般可以由以下几种方式进行:

  1. 使用 HTTP 服务器的路由来重定向多个应用
  2. 在不同的框架之上设计通讯、加载机制,诸如 MooaSingle-SPA
  3. 通过组合多个独立应用、组件来构建一个单体应用
  4. iFrame。使用 iFrame 及自定义消息传递机制
  5. 使用纯 Web Components 构建应用
  6. 结合 Web Components 构建

不同的方式适用于不同的使用场景,当然也可以组合一起使用。那么,就让我们来一一了解一下,为以后的架构演进做一些技术铺垫。

基础铺垫:应用分发路由 -> 路由分发应用

在一个单体前端、单体后端应用中,有一个典型的特征,即路由是由框架来分发的,框架将路由指定到对应的组件或者内部服务中。微服务在这个过程中做的事情是,将调用由函数调用变成了远程调用,诸如远程 HTTP 调用。而微前端呢,也是类似的,它是将应用内的组件调用变成了更细粒度的应用间组件调用,即原先我们只是将路由分发到应用的组件执行,现在则需要根据路由来找到对应的应用,再由应用分发到对应的组件上。

后端:函数调用 -> 远程调用

在大多数的 CRUD 类型的 Web 应用中,也都存在一些极为相似的模式,即:首页 -> 列表 -> 详情:

  • 首页,用于面向用户展示特定的数据或页面。这些数据通常是有限个数的,并且是多种模型的。
  • 列表,即数据模型的聚合,其典型特点是某一类数据的集合,可以看到尽可能多的数据概要(如 Google 只返回 100 页),典型见 Google、淘宝、京东的搜索结果页。
  • 详情,展示一个数据的尽可能多的内容。

如下是一个 Spring 框架,用于返回首页的示例:

@RequestMapping(value="/")
public ModelAndView homePage(){
   return new ModelAndView("/WEB-INF/jsp/index.jsp");
}

对于某个详情页面来说,它可能是这样的:

@RequestMapping(value="/detail/{detailId}")
public ModelAndView detail(HttpServletRequest request, ModelMap model){
   ....
   return new ModelAndView("/WEB-INF/jsp/detail.jsp", "detail", detail);
}

那么,在微服务的情况下,它则会变成这样子:

@RequestMapping("/name")
public String name(){
    String name = restTemplate.getForObject("http://account/name", String.class);
    return Name" + name;
}

而后端在这个过程中,多了一个服务发现的服务,来管理不同微服务的关系。

前端:组件调用 -> 应用调用

在形式上来说,单体前端框架的路由和单体后端应用,并没有太大的区别:依据不同的路由,来返回不同页面的模板。

const appRoutes: Routes = [
  { path: 'index', component: IndexComponent },
  { path: 'detail/:id', component: DetailComponent },
];

而当我们将之微服务化后,则可能变成应用 A 的路由:

const appRoutes: Routes = [
  { path: 'index', component: IndexComponent },
];

外加之应用 B 的路由:

const appRoutes: Routes = [
  { path: 'detail/:id', component: DetailComponent },
];

而问题的关键就在于:怎么将路由分发到这些不同的应用中去。与此同时,还要负责管理不同的前端应用。

路由分发式微前端

路由分发式微前端,即通过路由将不同的业务分发到不同的、独立前端应用上。其通常可以通过 HTTP 服务器的反向代理来实现,又或者是应用框架自带的路由来解决。

就当前而言,通过路由分发式的微前端架构应该是采用最多、最易采用的 “微前端” 方案。但是这种方式看上去更像是多个前端应用的聚合,即我们只是将这些不同的前端应用拼凑到一起,使他们看起来像是一个完整的整体。但是它们并不是,每次用户从 A 应用到 B 应用的时候,往往需要刷新一下页面。

在几年前的一个项目里,我们当时正在进行遗留系统重写。我们制定了一个迁移计划:

  1. 首先,使用静态网站生成动态生成首页
  2. 其次,使用 React 计划栈重构详情页
  3. 最后,替换搜索结果页

整个系统并不是一次性迁移过去,而是一步步往下进行。因此在完成不同的步骤时,我们就需要上线这个功能,于是就需要使用 Nginx 来进行路由分发。

如下是一个基于路由分发的 Nginx 配置示例:

http {
  server {
    listen       80;
    server_name  www.phodal.com;
    location /api/ {
      proxy_pass http://http://172.31.25.15:8000/api;
    }
    location /web/admin {
      proxy_pass http://172.31.25.29/web/admin;
    }
    location /web/notifications {
      proxy_pass http://172.31.25.27/web/notifications;
    }
    location / {
      proxy_pass /;
    }
  }
}

在这个示例里,不同的页面的请求被分发到不同的服务器上。

随后,我们在别的项目上也使用了类似的方式,其主要原因是:跨团队的协作。当团队达到一定规模的时候,我们不得不面对这个问题。除此,还有 Angluar 跳崖式升级的问题。于是,在这种情况下,用户前台使用 Angular 重写,后台继续使用 Angular.js 等保持再有的技术栈。在不同的场景下,都有一些相似的技术决策。

因此在这种情况下,它适用于以下场景:

  • 不同技术栈之间差异比较大,难以兼容、迁移、改造
  • 项目不想花费大量的时间在这个系统的改造上
  • 现有的系统在未来将会被取代
  • 系统功能已经很完善,基本不会有新需求

而在满足上面场景的情况下,如果为了更好的用户体验,还可以采用 iframe 的方式来解决。

使用 iFrame 创建容器

iFrame 作为一个非常古老的,人人都觉得普通的技术,却一直很管用。

HTML 内联框架元素<iframe> 表示嵌套的正在浏览的上下文,能有效地将另一个 HTML 页面嵌入到当前页面中。

iframe 可以创建一个全新的独立的宿主环境,这意味着我们的前端应用之间可以相互独立运行。采用 iframe 有几个重要的前提:

  • 网站不需要 SEO 支持
  • 拥有相应的应用管理机制

如果我们做的是一个应用平台,会在我们的系统中集成第三方系统,或者多个不同部门团队下的系统,显然这是一个不错的方案。一些典型的场景,如传统的 Desktop 应用迁移到 Web 应用:

Angular Tabs 示例

如果这一类应用过于复杂,那么它必然是要进行微服务化的拆分。因此,在采用 iframe 的时候,我们需要做这么两件事:

  • 设计管理应用机制
  • 设计应用通讯机制

加载机制。在什么情况下,我们会去加载、卸载这些应用;在这个过程中,采用怎样的动画过渡,让用户看起来更加自然。

通讯机制。直接在每个应用中创建 postMessage 事件并监听,并不是一个友好的事情。其本身对于应用的侵入性太强,因此通过 iframeEl.contentWindow 去获取 iFrame 元素的 Window 对象是一个更简化的做法。随后,就需要定义一套通讯规范:事件名采用什么格式、什么时候开始监听事件等等。

有兴趣的读者,可以看看笔者之前写的微前端框架:Mooa

不管怎样,iframe 对于我们今年的 KPI 怕是带不来一丝的好处,那么我们就去造个轮子吧。

自制框架兼容应用

不论是基于 Web Components 的 Angular,或者是 VirtualDOM 的 React 等,现有的前端框架都离不开基本的 HTML 元素 DOM。

那么,我们只需要:

  1. 在页面合适的地方引入或者创建 DOM
  2. 用户操作时,加载对应的应用(触发应用的启动),并能卸载应用。

第一个问题,创建 DOM 是一个容易解决的问题。而第二个问题,则一点儿不容易,特别是移除 DOM 和相应应用的监听。当我们拥有一个不同的技术栈时,我们就需要有针对性设计出一套这样的逻辑。

尽管 Single-SPA 已经拥有了大部分框架(如 React、Angular、Vue 等框架)的启动和卸载处理,但是它仍然不是适合于生产用途。当我基于 Single-SPA 为 Angular 框架设计一个微前端架构的应用时,我最后选择重写一个自己的框架,即 Mooa

虽然,这种方式的上手难度相对比较高,但是后期订制及可维护性比较方便。在不考虑每次加载应用带来的用户体验问题,其唯一存在的风险可能是:第三方库不兼容

但是,不论怎样,与 iFrame 相比,其在技术上更具有可吹牛逼性,更有看点。同样的,与 iframe 类似,我们仍然面对着一系列的不大不小的问题:

  • 需要设计一套管理应用的机制。
  • 对于流量大的 toC 应用来说,会在首次加载的时候,会多出大量的请求

而我们即又要拆分应用,又想 blabla……,我们还能怎么做?

组合式集成:将应用微件化

组合式集成,即通过软件工程的方式在构建前、构建时、构建后等步骤中,对应用进行一步的拆分,并重新组合。

从这种定义上来看,它可能算不上并不是一种微前端——它可以满足了微前端的三个要素,即:独立运行独立开发独立部署。但是,配合上前端框架的组件 Lazyload 功能——即在需要的时候,才加载对应的业务组件或应用,它看上去就是一个微前端应用。

与此同时,由于所有的依赖、Pollyfill 已经尽可能地在首次加载了,CSS 样式也不需要重复加载。

常见的方式有:

  • 独立构建组件和应用,生成 chunk 文件,构建后再归类生成的 chunk 文件。(这种方式更类似于微服务,但是成本更高)
  • 开发时独立开发组件或应用,集成时合并组件和应用,最后生成单体的应用。
  • 在运行时,加载应用的 Runtime,随后加载对应的应用代码和模板。

应用间的关系如下图所示(其忽略图中的 “前端微服务化”):

组合式集成对比

这种方式看上去相当的理想,即能满足多个团队并行开发,又能构建出适合的交付物。

但是,首先它有一个严重的限制:必须使用同一个框架。对于多数团队来说,这并不是问题。采用微服务的团队里,也不会因为微服务这一个前端,来使用不同的语言和技术来开发。当然了,如果要使用别的框架,也不是问题,我们只需要结合上一步中的自制框架兼容应用就可以满足我们的需求。

其次,采用这种方式还有一个限制,那就是:规范!规范!规范!。在采用这种方案时,我们需要:

  • 统一依赖。统一这些依赖的版本,引入新的依赖时都需要一一加入。
  • 规范应用的组件及路由。避免不同的应用之间,因为这些组件名称发生冲突。
  • 构建复杂。在有些方案里,我们需要修改构建系统,有些方案里则需要复杂的架构脚本。
  • 共享通用代码。这显然是一个要经常面对的问题。
  • 制定代码规范。

因此,这种方式看起来更像是一个软件工程问题。

现在,我们已经有了四种方案,每个方案都有自己的利弊。显然,结合起来会是一种更理想的做法。

考虑到现有及常用的技术的局限性问题,让我们再次将目光放得长远一些。

纯 Web Components 技术构建

在学习 Web Components 开发微前端架构的过程中,我尝试去写了我自己的 Web Components 框架:oan。在添加了一些基本的 Web 前端框架的功能之后,我发现这项技术特别适合于作为微前端的基石

Web Components 是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的 Web 应用中使用它们。

它主要由四项技术组件:

  • Custom elements,允许开发者创建自定义的元素,诸如 <today-news></today-news>。
  • Shadow DOM,即影子 DOM,通常是将 Shadow DOM 附加到主文档 DOM 中,并可以控制其关联的功能。而这个 Shadow DOM 则是不能直接用其它主文档 DOM 来控制的。
  • HTML templates,即 <template><slot> 元素,用于编写不在页面中显示的标记模板。
  • HTML Imports,用于引入自定义组件。

每个组件由 link 标签引入:

<link rel="import" href="components/di-li.html">
<link rel="import" href="components/d-header.html">

随后,在各自的 HTML 文件里,创建相应的组件元素,编写相应的组件逻辑。一个典型的 Web Components 应用架构如下图所示:

Web Components 架构

可以看到这边方式与我们上面使用 iframe 的方式很相似,组件拥有自己独立的 ScriptsStyles,以及对应的用于单独部署组件的域名。然而它并没有想象中的那么美好,要直接使用 Web Components 来构建前端应用的难度有:

  • 重写现有的前端应用。是的,现在我们需要完成使用 Web Components 来完成整个系统的功能。
  • 上下游生态系统不完善。缺乏相应的一些第三方控件支持,这也是为什么 jQuery 相当流行的原因。
  • 系统架构复杂。当应用被拆分为一个又一个的组件时,组件间的通讯就成了一个特别大的麻烦。

Web Components 中的 ShadowDOM 更像是新一代的前端 DOM 容器。而遗憾的是并不是所有的浏览器,都可以完全支持 Web Components。

结合 Web Components 构建

Web Components 离现在的我们太远,可是结合 Web Components 来构建前端应用,则更是一种面向未来演进的架构。或者说在未来的时候,我们可以开始采用这种方式来构建我们的应用。好在,已经有框架在打造这种可能性。

就当前而言,有两种方式可以结合 Web Components 来构建微前端应用:

  • 使用 Web Components 构建独立于框架的组件,随后在对应的框架中引入这些组件
  • 在 Web Components 中引入现有的框架,类似于 iframe 的形式

前者是一种组件式的方式,或者则像是在迁移未来的 “遗留系统” 到未来的架构上。

在 Web Components 中集成现有框架

现有的 Web 框架已经有一些可以支持 Web Components 的形式,诸如 Angular 支持的 createCustomElement,就可以实现一个 Web Components 形式的组件:

platformBrowser()
    .bootstrapModuleFactory(MyPopupModuleNgFactory)
        .then(({injector}) => {
            const MyPopupElement = createCustomElement(MyPopup, {injector});
            customElements.define(‘my-popup’, MyPopupElement);
});

在未来,将有更多的框架可以使用类似这样的形式,集成到 Web Components 应用中。

集成在现有框架中的 Web Components

另外一种方式,则是类似于 Stencil 的形式,将组件直接构建成 Web Components 形式的组件,随后在对应的诸如,如 React 或者 Angular 中直接引用。

如下是一个在 React 中引用 Stencil 生成的 Web Components 的例子:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';

import 'test-components/testcomponents';

ReactDOM.render(<App />, document.getElementById('root'));
registerServiceWorker();

在这种情况之下,我们就可以构建出独立于框架的组件。

同样的 Stencil 仍然也只是支持最近的一些浏览器,比如:Chrome、Safari、Firefox、Edge 和 IE11

复合型

复合型,对就是上面的几个类别中,随便挑几种组合到一起。

我就不废话了~~。

结论

那么,我们应该用哪种微前端方案呢?答案见下一篇《微前端快速选型指南》

相关资料:

查看原文

赞 158 收藏 191 评论 5

仅此而已 回答了问题 · 2019-10-10

react数据刷新会消失为什么?

redux 只是存在浏览器当前窗口的内存中,当你刷新窗口的时候,浏览器内存重制了,所以是空白没有内容,一般的操作是history传递详情页id,在详情页初始化请求详情接口

关注 4 回答 5

仅此而已 回答了问题 · 2019-10-10

echarts环形饼图的圆环背景色怎么设置?

series:[{axisLine:{lineStyle:{color:['#000']}}}]

希望能帮到你

关注 2 回答 1

仅此而已 回答了问题 · 2019-09-06

typescript中在对对象解构赋值的时候,默认值的类型可以不符合定义的类型吗?

typescript只是类型检查,不会影响js实际运行

关注 4 回答 3

仅此而已 发布了文章 · 2019-07-17

React Hooks

Hooks简介

HooksReact v16.7.0-alpha 中加入的新特性。它能够让函数组件拥有自己的statereact 16.8.0稳定版本支持Hooks,本文就是演示 Hooks 在项目中的使用示例,对于内部的原理这里就不做详细说明。

useState

import React, { useState  } from 'react';
function Example() {
    // 声明一个名为“count”的新状态变量
    const [count, setCount] = useState(0);

    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={() => setCount(count + 1)}>
                Click me
            </button>
        </div>
    );
}

export default Example;

useEffect


import React, { useEffect  } from 'react';
function Example() {
    //生命周期中的componentDidMount
    useEffect(() => {
        console.log('componentDidMount')  
    },[]);
    //生命周期中的componentDidMount
    useEffect(() => {
        console.log('componentDidMount')
        return ()=>{ //componentWillUnmount
            console.log('componentWillUnmount')
        }
    },[]);
    //生命周期中的componentDidMount 和 componentDidUpdate
    useEffect(() => {
        console.log('类似于 componentDidMount 和 componentDidUpdate:')
    });
    return (
        <div></div>
    );
}

export default Example;

componentDidMount、componentDidUpdate、componentWillUnmount的使用方法

useMemo

import React, { useMemo  } from 'react';
export default ({a}) => {
    const exampleA = useMemo(() => <div>{a}</div>, [a]);  //当a的值 发生变化时候才会渲染
    return exampleA
}

useRef

import React, { useRef  } from 'react';

export default ({a}) => {
    const inputEl = useRef(null);
    return <input ref={ inputEl } type="text" />
}

react-router 获取路由参数

import React from 'react';
import { withRouter } from 'react-router-dom';

export default withRouter((props) => {
    return <div>{props.match.params.id}</div>
})

react-redux和redux-saga 的使用

import React, { useEffect } from 'react';
import { connect } from 'react-redux';

const mapStateToProps = (state) => {
  return {
      list:state.list
  };
};
const mapDispatchToProps = (dispatch) => {
  return {
      getList:()=>{},//只是实例使用方式
  };
};
const useAddField = (props:Props) => {
  useEffect(()=>{
      console.log('----------第一次渲染')
      this.props.list();
      return ()=>{
        console.log('-------退出')
      }
  },[])   //componentDidMount
  console.log(props.list)  //redux里面的值
  return <div></div>
};
export default connect(mapStateToProps, mapDispatchToProps)(useAddField);

参考文档

React 官网

React Hooks FAQ

参考博客
查看原文

赞 2 收藏 1 评论 1

仅此而已 赞了文章 · 2019-07-10

99%的程序都没有考虑的网络异常?使用Fundebug.notify()主动上报

近日看到一篇文章99%的程序都没有考虑的网络异常,开篇提到:

绝大多数程序只考虑了接口正常工作的场景,而用户在使用我们的产品时遇到的各类异常,全都丢在看似 ok 的 try catch 中。如果没有做好异常的兼容和兜底处理,会极大的影响用户体验,严重的还会带来安全和资损风险。

于是,笔者分析了 GitHub 上的一些开源微信小程序,发现大多数的代码异常处理确实是不够的。

  • 登录接口只考虑成功的情况,没考虑失败的情况
//调用登录接口
wx.login({
    success: function() {
        wx.getUserInfo({
            success: function(res) {
                that.globalData.userInfo = res.userInfo;
                typeof cb == "function" && cb(that.globalData.userInfo);
            }
        });
    }
});
  • 网络请求只考虑then不考虑catch

    util.getData(index_api).then(function(data) {
        //this.setData({
        //
        //});
        console.log(data);
    });
  • 考虑了异常情况但是没有做妥善的处理

    db.collection("config")
        .where({})
        .get()
        .then(res => {
            console.log(res);
            if (res.data.length > 0) {
                Taro.setStorage({
                    key: "config_gitter",
                    data: res.data[0]
                });
            }
        })
        .catch(err => {
            console.error(err);
        });

也许 99%的情况下接口都是正常返回的,只有 1%的情况会失败。看起来好像不是一件严重的事情,但是考虑到用户的量级,这个事情就不那么简单了。假设有 100 万用户,那么就有 1 万用户遇到异常情况,而且如果用户的使用频次很高,影响的何止 1 万用户。并且,如今产品都是体验至上,如果遇到这样的问题,用户极大可能就弃你而去,流失了客户就等于流失了收入。

如何妥善地处理接口异常的情况是一件严肃的事情,应当被重视起来。

妥善处理请求异常

那么,应当如何做呢?首先要定义请求异常的处理代码,比如微信开放接口的参数中有fail(“接口调用失败的回调函数”)、Promise 的catch部分;其次,根据异常可能导致的后果,在函数中做相应的处理。如果会导致后续操作失败、或则界面无反馈,那么应当在 fail 回调中正确处理;如果你真的认为基本不可能出问题,那么至少写个异常上报。即使出错了,也知道具体的情况。

下图是微信支付接口的参数列表,其中包含了接口调用失败的回调函数(fail)。

image-20190703114115173

而且官方也给出了示例:

wx.requestPayment({
    timeStamp: "",
    nonceStr: "",
    package: "",
    signType: "MD5",
    paySign: "",
    success(res) {},
    fail(res) {}
});

在回调函数fail中上报异常

为了确保完全掌握小程序的运行状况,我们将异常上报。Fundebug 的微信小程序插件除了可以自动捕获异常外,还支持通过API 接口主动上报异常。

根据其官方文档:

使用 fundebug.notify(),可以将自定义的错误信息发送到 Fundebug

name: 错误名称,参数类型为字符串

message: 错误信息,参数类型为字符串

option: 可选对象,参数类型为对象,用于发送一些额外信息

示例:

fundebug.notify("Test", "Hello, Fundebug!", {
 metaData: {
     company: "云麒",
     location: "厦门"
 }
});

首先在 Fundebug 创建一个小程序监控项目,并按照指示接入插件,然后在app.jsonLaunch函数下面调用wx.requestPayment来进行测试。

Fundebug 的微信小程序插件捕获并上报了异常:

metaData标签还可以看到我们配置的 metaData,也就是fail回调函数的res参数。

因此,我们可以知道失败的原因是订单过期。

另外,如果在二维码页面停留时间过久,也会触发报错:

通过简单的加入几行代码,就可以将小程序的异常情况了如指掌。而且 Fundebug 的微信小程序插件还可以监控线上 JavaScript 执行异常、自动捕获wx.request请求错误、监控慢 HTTP 请求,推荐大家接入试用!

关于Fundebug

Fundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了10亿+错误事件,付费客户有阳光保险、核桃编程、荔枝FM、掌门1对1、微脉、青团社等众多品牌企业。欢迎大家免费试用

img

版权声明

转载时请注明作者 Fundebug以及本文地址:
https://blog.fundebug.com/2019/07/08/report-http-error-by-fundebug-notify/

查看原文

赞 12 收藏 8 评论 0

仅此而已 发布了文章 · 2019-06-03

react-router@4.0 使用方法和源码分析

react-router-dom@4.3.0 || react-router@4.4.1

react-router 使用方法

配置 router.js

import React, { Component } from 'react';
import {  Switch, Route } from 'react-router-dom';

const router = [{
    path: '/',
    exact: true,
    component:importPath({
      loader: () => import(/* webpackChunkName:"home" */ "pages/home/index.js"),
    }),
  },]
const Routers = () => (
  <main>
    <Switch>
      {
        router.map(({component,path,exact},index)=>{
          return <Route exact={exact}  path={path} component={component} key={path} />
        })
      }
    </Switch>
  </main>
);

export default Routers;

入口 index.js

import {HashRouter} from 'react-router-dom';
import React from 'react';
import ReactDOM from 'react-dom';
import Routers from './router';

ReactDOM.render (
      <HashRouter>
        <Routers />
      </HashRouter>,
  document.getElementById ('App')
);

home.js

import { withRouter } from "react-router-dom";

@withRouter
class Home extends React.Component<PropsType, stateType> {
  constructor(props: PropsType) {
    super(props);
    this.state = {};
  }
  goPath=()=>{
      this.props.history.push('/home')
  }
  render() {
    return (
      <div onClick={this.goPath}>home</div>
    );
  }
export default Home;

react-router 源码解析

下面代码中会移除部分的类型检查和提醒代码,突出重点代码

第一步 Switch react-router

function _possibleConstructorReturn(self, call) {
  if (!self) {
    throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
  }
  if(call&&(typeof call === "object" || typeof call === "function") ){
    return call
  }else {
    return self
  }
}
var Switch = function (_React$Component) {
  function Switch() {
    //使用传递进来的组件覆盖本身
    return _possibleConstructorReturn(this, _React$Component.apply(this, arguments));  
  }
  Switch.prototype.render = function render() {
    var route = this.context.router.route;
    var children = this.props.children;
    var location = this.props.location || route.location;
    var match = void 0,child = void 0;
    
    //检查element是否是react组件,初始match为null,
    React.Children.forEach(children, function (element) {
     //如果match符合,forEach不会进入该if
      if (match == null && React.isValidElement(element)) { 
        var _element$props = element.props,
            pathProp = _element$props.path,
            exact = _element$props.exact,
            strict = _element$props.strict,
            sensitive = _element$props.sensitive,
            from = _element$props.from;
        var path = pathProp || from;
        child = element; 
        //检查当前配置是否符合,
        match = matchPath(location.pathname, { path: path, exact: exact, strict: strict, sensitive: sensitive }, route.match); 
      }
    });
    //如果有匹配元素,则返回克隆child
    return match ? React.cloneElement(child, { location: location, computedMatch: match }) : null;
  };

  return Switch;
}(React.Component);

总结:switch根据location.pathname,path,exact,strict,sensitive获取元素并返回element

第二步 Route react-router

var Route = function (_React$Component) {
  function Route() {
    var _temp, _this, _ret;
    //获取参数
    for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
      args[_key] = arguments[_key];
    }
    //修改this
    return _ret = (
      _temp = (_this = _possibleConstructorReturn(this, _React$Component.call.apply(_React$Component, [this].concat(args))), _this), 
      //检查当前元素是否符合match
      _this.state = {match: _this.computeMatch(_this.props,_this.context.router)},_temp),
       //这里是真正return
       _possibleConstructorReturn(_this, _ret); 
  }
  // 设置content
  Route.prototype.getChildContext = function getChildContext() {
    return {
      router: _extends({}, this.context.router, {
        route: {
          location: this.props.location || this.context.router.route.location,
          match: this.state.match
        }
      })
    };
  };
  // 根据参数检查当前元素是否符合匹配规则
  Route.prototype.computeMatch = function computeMatch(_ref, router) {
    var computedMatch = _ref.computedMatch,
        location = _ref.location,
        path = _ref.path,
        strict = _ref.strict,
        exact = _ref.exact,
        sensitive = _ref.sensitive;

    if (computedMatch) return computedMatch;

    var route = router.route;

    var pathname = (location || route.location).pathname;

    return matchPath(pathname, { path: path, strict: strict, exact: exact, sensitive: sensitive }, route.match);
  };
  // 设置match
  Route.prototype.componentWillReceiveProps = function componentWillReceiveProps(nextProps, nextContext) {
    this.setState({
      match: this.computeMatch(nextProps, nextContext.router)
    });
  };

  Route.prototype.render = function render() {
    var match = this.state.match;
    var _props = this.props,
        children = _props.children,
        component = _props.component,
        render = _props.render;
    var _context$router = this.context.router,
        history = _context$router.history,
        route = _context$router.route,
        staticContext = _context$router.staticContext;

    var location = this.props.location || route.location;
    var props = { match: match, location: location, history: history, staticContext: staticContext };
    //检查route 是否有component组
    if (component) return match ? React.createElement(component, props) : null;  
    // 检查是否包含render 组件
    if (render) return match ? render(props) : null;
    // withRouter 使用的方式
    if (typeof children === "function") return children(props);

    if (children && !isEmptyChildren(children)) return React.Children.only(children);

    return null;
  };

  return Route;
}(React.Component);

总结:route 渲染的方式: componentrenderchildren,代码示例用的是component,route 是检查当前组件是否符合路由匹配规则并执行创建过程

第三步 HashRouter react-router-dom

import Router from './Router'
import {createHistory} from 'history'
var HashRouter = function (_React$Component) {
  function HashRouter() {
    var _temp, _this, _ret;
    //参数转换为数组
    for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {  
      args[_key] = arguments[_key];
    }
    return _ret = (
      _temp = (_this = _possibleConstructorReturn(this, _React$Component.call.apply(_React$Component, [this].concat(args))), _this),
       _this.history = createHistory(_this.props), _temp), //创建history
       _possibleConstructorReturn(_this, _ret);  //真正返回的东西 返回this
  }
  HashRouter.prototype.render = function render() {
    // 返回一个Router,并且把history,children传递给Router
    return React.createElement(Router, { history: this.history, children: this.props.children });
  };
  return HashRouter;
}(React.Component);

总结 通过 history库里面 createHistory 创建路由系统

第四部 Router react-router

var Router = function (_React$Component) {
  function Router() {
    var _temp, _this, _ret;
    //获取参数,和其他组件一样
    for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
      args[_key] = arguments[_key];
    }
    return _ret = (_temp = (_this = _possibleConstructorReturn(this, _React$Component.call.apply(_React$Component, [this].concat(args))), _this), _this.state = {
      match: _this.computeMatch(_this.props.history.location.pathname)  //返回路由对象
    }, _temp), _possibleConstructorReturn(_this, _ret);  //返回this
  }
  // 返回context
  Router.prototype.getChildContext = function getChildContext() {
    return {
      router: _extends({}, this.context.router, {
        history: this.props.history,
        route: {
          location: this.props.history.location,
          match: this.state.match
        }
      })
    };
  };
    
  Router.prototype.computeMatch = function computeMatch(pathname) {
    return {
      path: "/",
      url: "/",
      params: {},
      isExact: pathname === "/"
    };
  };

  Router.prototype.componentWillMount = function componentWillMount() {
    var _this2 = this;

    var _props = this.props,
        children = _props.children,
        history = _props.history;

    // 启动监听 当hash 改变是做一次检查,并返回unlisten 取消事件
    this.unlisten = history.listen(function () {
      _this2.setState({
        match: _this2.computeMatch(history.location.pathname)
      });
    });
  };
  //销毁前取消监听
  Router.prototype.componentWillUnmount = function componentWillUnmount() {
    this.unlisten();
  };
  // children是HashRouter 传递进来的
  Router.prototype.render = function render() {
    var children = this.props.children;
    return children ? React.Children.only(children) : null;
  };

  return Router;
}(React.Component);

总结 history是一个JavaScript库,可让您在JavaScript运行的任何地方轻松管理会话历史记录。history抽象出各种环境中的差异,并提供最小的API,使您可以管理历史堆栈,导航,确认导航以及在会话之间保持状态。

第五部 withRouter <react-router>

var withRouter = function withRouter(Component) {
  var C = function C(props) {
   //获取props
    var wrappedComponentRef = props.wrappedComponentRef,
        remainingProps = _objectWithoutProperties(props, ["wrappedComponentRef"]);
    // Route 组件 children方式
    return React.createElement(Route, {
      children: function children(routeComponentProps) {
        // 这里使用的是route 组件 children(props)
        //routeComponentProps 实际等于 { match: match, location: location, history: history, staticContext: staticContext };
        return React.createElement(Component, _extends({}, remainingProps, routeComponentProps, {
          ref: wrappedComponentRef
        }));
      }
    });
  };

  C.displayName = "withRouter(" + (Component.displayName || Component.name) + ")";
  C.WrappedComponent = Component;
  // 该类似于object.assign(C,Component),得到的结果是C
  return hoistStatics(C, Component);
};

到这里真个流程基本结束了,这只是react-router的一种使用方式的解析,本文的目的是理解react-router的运行机制,如果有什么错误还望指出,谢谢🙏

查看原文

赞 1 收藏 1 评论 0

认证与成就

  • 获得 55 次点赞
  • 获得 7 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 6 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-04-28
个人主页被 1.4k 人浏览