Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00fd6e541a | ||
|
|
6cc1af4ca9 | ||
|
|
ed27df0fe2 | ||
|
|
14ebdf19ca | ||
|
|
e89a1b0c7e | ||
|
|
a1bd0ec7a5 | ||
|
|
82d3665606 | ||
|
|
67a289e750 | ||
|
|
1b0a7ed8ea |
@@ -54,7 +54,7 @@ function fakeParseJSON(str) {
|
||||
}
|
||||
```
|
||||
|
||||
其中 `skipWhitespace` 表示匹配并跳过空格,所谓匹配意味着匹配成功,此时 `i` 下标可以继续后移,否则匹配失败。下一步则判断如果 `i` 不是结束标志 `}`,则按照 `parseString` 匹配字符串 → `skipWhitespace` 跳过空格 → `eatColon` 吃掉逗号 → `parseValue` 匹配值,这个链路循环。其中吃掉逗号表示 “匹配逗号但不会产生任何结果,所以就像吃掉了一样”,吃这个动作还可以用在其他场景,比如吃掉尾分号。
|
||||
其中 `skipWhitespace` 表示匹配并跳过空格,所谓匹配意味着匹配成功,此时 `i` 下标可以继续后移,否则匹配失败。下一步则判断如果 `i` 不是结束标志 `}`,则按照 `parseString` 匹配字符串 → `skipWhitespace` 跳过空格 → `eatColon` 吃掉冒号 → `parseValue` 匹配值,这个链路循环。其中吃掉冒号表示 “匹配冒号但不会产生任何结果,所以就像吃掉了一样”,吃这个动作还可以用在其他场景,比如吃掉尾分号。
|
||||
|
||||
> 对于看到这儿的小伙伴,笔者要友情提示一下,原文的思路是一种定制语法解析思路,无论是 `eatColon` 还是 `parseValue` 都仅具备解析 JSON 的通用性,但不具备解析任意语法的通用性。如果你想做一个具备解析任何通用语法的解析器,读入的内容应该是语法描述,处理方式必须更加通用,如果感兴趣可以阅读 [精读《手写 SQL 编译器 - 语法分析》](https://github.com/dt-fe/weekly/blob/v2/066.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E8%AF%AD%E6%B3%95%E5%88%86%E6%9E%90%E3%80%8B.md) 系列文章了解更多。
|
||||
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
## 1 引言
|
||||
|
||||
拖拽是前端非常常见的交互操作,但显然拖拽是强 DOM 交互的,而 React 绕过了 DOM 这一层,那么基于 React 的拖拽方案就必定值得聊一聊。
|
||||
|
||||
结合 [How To Use The HTML Drag-And-Drop API In React](https://www.smashingmagazine.com/2020/02/html-drag-drop-api-react/) 这篇文章,让我们谈谈 React 拖拽这些事。
|
||||
|
||||
## 2 概述
|
||||
|
||||
原文说的比较简单,笔者先快速介绍其中重点部分。
|
||||
|
||||
首先拖拽主要的 API 有 4 个:`dragEnter` `dragLeave` `dragOver` `drop`,分别对应拖入、拖出、正在当前元素范围内拖拽、完成拖入动作。
|
||||
|
||||
基于这些 API,我们可以利用 React 实现一个拖入区域:
|
||||
|
||||
```jsx
|
||||
import React from "react";
|
||||
|
||||
const DragAndDrop = props => {
|
||||
const handleDragEnter = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
const handleDragLeave = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
const handleDragOver = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
const handleDrop = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className={"drag-drop-zone"}
|
||||
onDrop={e => handleDrop(e)}
|
||||
onDragOver={e => handleDragOver(e)}
|
||||
onDragEnter={e => handleDragEnter(e)}
|
||||
onDragLeave={e => handleDragLeave(e)}
|
||||
>
|
||||
<p>Drag files here to upload</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default DragAndDrop;
|
||||
```
|
||||
|
||||
`preventDefault` 指的是阻止默认响应,这个响应可能是跳转页面之类的,`stopPropagation` 是阻止冒泡,这样同样监听了事件的父元素就不会收到响应,我们可以精准作用于嵌套的子元素。
|
||||
|
||||
接下来是拖拽状态管理,提到了 `useReducer`,顺便复习一下用法:
|
||||
|
||||
```jsx
|
||||
...
|
||||
const reducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case 'SET_DROP_DEPTH':
|
||||
return { ...state, dropDepth: action.dropDepth }
|
||||
case 'SET_IN_DROP_ZONE':
|
||||
return { ...state, inDropZone: action.inDropZone };
|
||||
case 'ADD_FILE_TO_LIST':
|
||||
return { ...state, fileList: state.fileList.concat(action.files) };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
const [data, dispatch] = React.useReducer(
|
||||
reducer, { dropDepth: 0, inDropZone: false, fileList: [] }
|
||||
)
|
||||
...
|
||||
```
|
||||
|
||||
最后一个关键点在于拖入后的处理,利用 `dispatch` 增加拖入文件、设置拖入状态即可:
|
||||
|
||||
```js
|
||||
const handleDrop = e => {
|
||||
...
|
||||
let files = [...e.dataTransfer.files];
|
||||
|
||||
if (files && files.length > 0) {
|
||||
const existingFiles = data.fileList.map(f => f.name)
|
||||
files = files.filter(f => !existingFiles.includes(f.name))
|
||||
|
||||
dispatch({ type: 'ADD_FILE_TO_LIST', files });
|
||||
e.dataTransfer.clearData();
|
||||
dispatch({ type: 'SET_DROP_DEPTH', dropDepth: 0 });
|
||||
dispatch({ type: 'SET_IN_DROP_ZONE', inDropZone: false });
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
`e.dataTransfer.clearData` 函数用于清除拖拽过程中产生的临时变量,这些临时变量可以通过 `e.dataTransfer.xxx =` 的方式赋值,一般用于拖拽过程中值的传递。
|
||||
|
||||
总结一下,利用 HTML5 的 API 将拖拽转化为状态,最终通过状态映射到 UI。
|
||||
|
||||
原文内容还是比较简单的,笔者在精读部分再拓展一些更体系化的内容。
|
||||
|
||||
## 3 精读
|
||||
|
||||
现阶段拖拽主要分为两种,一种是 HTML5 原生规范的拖拽,这种方式在拖拽过程中不会影响 DOM 结构。另一种是完全所见即所得的拖拽方式,拖拽过程中 DOM 位置会随之变动,好处是可以立即反馈拖拽结果,当然缺点是华而不实,一旦用在生产环境,这种拖拽过程可能导致页面结构频繁跳动,反而看不清拖拽效果。
|
||||
|
||||
由于本文也采用了第一种拖拽方案,因为笔者再重新整理一遍自己的封装思路。
|
||||
|
||||
从使用角度反推,假设我们拥有一个拖拽库,那必定要拥有两个 API:
|
||||
|
||||
```jsx
|
||||
import { DragContainer, DropContainer } from 'dnd'
|
||||
|
||||
const DragItem = (
|
||||
<DragContainer>
|
||||
{({ dragProps }) => (
|
||||
<div {...dragProps} />
|
||||
)}
|
||||
</DragContainer>
|
||||
)
|
||||
|
||||
const DropItem = (
|
||||
<DropContainer>
|
||||
{({ dropProps }) => (
|
||||
<div {...dropProps} />
|
||||
)}
|
||||
</DropContainer>
|
||||
)
|
||||
```
|
||||
|
||||
`DragContainer` 包裹可以被拖拽的元素,`DropContainer` 包裹可以被拖入的元素,而至于 `dragProps` 与 `dropProps` 需要透传到子元素的 dom 节点,是为了利用 DOM API 控制拖拽效果,这也是拖拽唯一对 DOM 的要求,双方元素都需要有实体 DOM 承载。
|
||||
|
||||
而上面例子中给出 `dragProps` 与 `dropProps` 的方式属于 RenderProps,我们可以将 `children` 当作函数执行以达到效果:
|
||||
|
||||
```jsx
|
||||
const DragContainer = ({ children, componentId }) => {
|
||||
const { dragProps } = useDnd(componentId)
|
||||
|
||||
return children({
|
||||
dragProps
|
||||
})
|
||||
}
|
||||
|
||||
const DropContainer = ({ children, componentId }) => {
|
||||
const { dropProps } = useDnd(componentId)
|
||||
|
||||
return children({
|
||||
dropProps
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
那么这里创建了一个自定义 Hook `useDnd` 接收 `dragProps` 与 `dropProps`,这个自定义 Hook 可以这么写:
|
||||
|
||||
```jsx
|
||||
const useDnd = ({ componentId }) => {
|
||||
const dragProps = {}
|
||||
const dropProps = {}
|
||||
|
||||
return { dragProps, dropProps }
|
||||
}
|
||||
```
|
||||
|
||||
接下来,我们就要分别实现 `drag` 与 `drop` 了。
|
||||
|
||||
对 `drag` 来说,只要实现 `onDragStart` 与 `onDragEnd` 即可:
|
||||
|
||||
```jsx
|
||||
const dragProps = {
|
||||
onDragStart: ev => {
|
||||
ev.stopPropagation()
|
||||
ev.dataTransfer.setData('componentId', componentId)
|
||||
},
|
||||
onDragEnd: ev => {
|
||||
// 做一些拖拽结束的清理工作
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`stopPropagation` 的作用在原文简介中已经介绍过了,`setData` 则是通知拖拽方,当前拖拽的组件 id 是什么,**这是由于拖拽由 `drag` 发起而由 `drop` 响应,因此必须有个数据传输过程,而 `dataTransfer` 就最适合做这件事。**
|
||||
|
||||
对于 `drop` 来说,只要实现 `onDragOver` 与 `onDrop` 即可:
|
||||
|
||||
```jsx
|
||||
const dropProps = {
|
||||
onDropOver: ev => {
|
||||
// 做一些样式处理,提示用户此时松手会将元素防止在何处
|
||||
},
|
||||
onDrop: ev => {
|
||||
ev.stopPropagation()
|
||||
const componentId = ev.dataTransfer.getData('componentId')
|
||||
// 通过 componentId 修改数据,通过 React Rerender 刷新 UI
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
重点在 `onDrop`,它是实现拖拽效果的 “真正执行处”,最终通过修改 UI 的方式更新数据。
|
||||
|
||||
存在一种场景,一个容器既可以被拖动,也可以被拖入,这种情况一般这个组件是个容器,但这个容器可以被拖入到其他容器中,可以自由嵌套。
|
||||
|
||||
实现这种场景的方式就是将 `DragContainer` 与 `DropContainer` 作用到一个组件上:
|
||||
|
||||
```jsx
|
||||
const Box = (
|
||||
<DragContainer>
|
||||
{({ dragProps }) => (
|
||||
<DropContainer>
|
||||
{({ dropProps }) => {
|
||||
<div {...dragProps} {...dropProps} />
|
||||
}}
|
||||
</DropContainer>
|
||||
)}
|
||||
</DragContainer>
|
||||
)
|
||||
```
|
||||
|
||||
之所以能嵌套,在于 HTML5 的 API 允许一个元素同时拥有 `onDragStart`、`onDrop` 这两种属性,而上面的语法不过是同时将这两种属性传给组件 DOM。
|
||||
|
||||
所以,动手实现一个拖拽库就是这么简单,只要活用 HTML5 的拖拽 API,结合 React 一些特殊语法便够了。
|
||||
|
||||
## 4 总结
|
||||
|
||||
最后留下一个思考题,许多具有拖拽功能的系统都具备 “拖拽 placeholder” 的功能,即拖拽元素的过程中,在其 “落点” 位置展示一条横线或竖线,引导出松手后元素位置落点,如图所示:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB11H04wbY1gK0jSZTEXXXDQVXa-1434-384.png">
|
||||
|
||||
那么这条辅助线是通过什么方式实现的呢?欢迎在评论区留言!如果你有辅助线实现方案解析的文章,欢迎分享,也可以期待笔者未来专门写一篇 “拖拽 placeholder” 实现剖析的精读。
|
||||
|
||||
> 讨论地址是:[精读《手写 JSON Parser》 · Issue #233 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/233)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,108 @@
|
||||
## 1 引言
|
||||
|
||||
`useRef` 是常用的 API,但还有一个 `createRef` 的 API,你知道他们的区别吗?通过 [React.useRef and React.createRef: The Difference](https://blog.bitsrc.io/react-useref-and-react-createref-the-difference-afedb9877d0f) 这篇文章,你可以了解到何时该使用它们。
|
||||
|
||||
## 2 概述
|
||||
|
||||
其实原文就阐述了这样一个事实:`useRef` 仅能用在 FunctionComponent,`createRef` 仅能用在 ClassComponent。
|
||||
|
||||
第一句话是显然的,因为 Hooks 不能用在 ClassComponent。
|
||||
|
||||
第二句话的原因是,`createRef` 并没有 Hooks 的效果,其值会随着 FunctionComponent 重复执行而不断被初始化:
|
||||
|
||||
```tsx
|
||||
function App() {
|
||||
// 错误用法,永远也拿不到 ref
|
||||
const valueRef = React.createRef();
|
||||
return <div ref={valueRef} />;
|
||||
}
|
||||
```
|
||||
|
||||
上述 `valueRef` 会随着 App 函数的 Render 而重复初始化,**这也是 Hooks 的独特之处,虽然用在普通函数中,但在 React 引擎中会得到超出普通函数的表现,比如初始化仅执行一次,或者引用不变**。
|
||||
|
||||
为什么 `createRef` 可以在 ClassComponent 正常运行呢?这是因为 ClassComponent 分离了生命周期,使例如 `componentDidMount` 等初始化时机仅执行一次。
|
||||
|
||||
原文完。
|
||||
|
||||
## 3 精读
|
||||
|
||||
那么知道如何正确创建 Ref 后,还知道如何正确更新 Ref 吗?
|
||||
|
||||
由于 Ref 是贯穿 FunctionComponent 所有渲染周期的实例,理论上在任何地方都可以做修改,比如:
|
||||
|
||||
```tsx
|
||||
function App() {
|
||||
const valueRef = React.useRef();
|
||||
|
||||
valueRef.current += 1;
|
||||
|
||||
return <div />;
|
||||
}
|
||||
```
|
||||
|
||||
但其实上面的修改方式是不规范的,React 官方文档里要求我们避免在 Render 函数中直接修改 Ref,请先看下面的 FunctionComponent 生命周期图:
|
||||
|
||||
<img width=600 src="https://img.alicdn.com/tfs/TB12aHDwQL0gK0jSZFtXXXQCXXa-3300-2550.png">
|
||||
|
||||
从图中可以发现,在 `Render phase` 阶段是不允许做 “side effects” 的,也就是写副作用代码,这是因为这个阶段可能会被 React 引擎随时取消或重做。
|
||||
|
||||
修改 Ref 属于副作用操作,因此不适合在这个阶段进行。我们可以看到,在 `Commit phase` 阶段可以做这件事,或者在回调函数中做(脱离了 React 生命周期)。
|
||||
|
||||
当然有一种情况是可以的,即 [懒初始化](https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily):
|
||||
|
||||
```ts
|
||||
function Image(props) {
|
||||
const ref = useRef(null);
|
||||
|
||||
// ✅ IntersectionObserver is created lazily once
|
||||
function getObserver() {
|
||||
if (ref.current === null) {
|
||||
ref.current = new IntersectionObserver(onIntersect);
|
||||
}
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
// When you need it, call getObserver()
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
懒初始化的情况下,副作用最多执行一次,而且仅用于初始化赋值,所以这种行为是被允许的。
|
||||
|
||||
为什么对副作用限制的如此严格?因为 FunctionComponent 增加了内置调度系统,为了优先响应用户操作,可能会暂定某个 React 组件的渲染,具体可以看第 99 篇精读:[精读《Scheduling in React》](https://github.com/dt-fe/weekly/blob/v2/099.%E7%B2%BE%E8%AF%BB%E3%80%8AScheduling%20in%20React%E3%80%8B.md)
|
||||
|
||||
Ref 不仅可以拿到组件引用、创建一个 Mutable 副作用对象,还可以配合 `useEffect` 存储一个较老的值,最常用来拿到 `previousProps`,React 官方利用 Ref 封装了一个简单的 Hooks 拿到上一次的值:
|
||||
|
||||
```tsx
|
||||
function usePrevious(value) {
|
||||
const ref = useRef();
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
});
|
||||
return ref.current;
|
||||
}
|
||||
```
|
||||
|
||||
由于 `useEffect` 在 Render 完毕后才执行,因此 `ref` 的值在当前 Render 中永远是上一次 Render 时候的,我们可以利用它拿到上一次 Props:
|
||||
|
||||
```tsx
|
||||
function App(props) {
|
||||
const preProps = usePrevious(props);
|
||||
}
|
||||
```
|
||||
|
||||
要实现这个功能,还是要归功于 `ref` 可以将值 “在各个不同的 Render 闭包中传递的特性”。最后,不要滥用 Ref,Mutable 引用越多,对 React 来说可维护性一般会越差。
|
||||
|
||||
## 4 总结
|
||||
|
||||
你还挖掘了 `useRef` 哪些有意思的使用方式?欢迎在评论区留言。
|
||||
|
||||
> 讨论地址是:[精读《useRef 与 createRef 的区别》 · Issue #236 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/236)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,97 @@
|
||||
## 1 引言
|
||||
|
||||
任何软件都是协同开发的,所以 CodeReview 非常重要,它可以帮助你减少代码质量问题,提高开发效率,提升稳定性,同时还能保证软件架构的稳定性,防止代码结构被恶意破坏导致难以维护。
|
||||
|
||||
所以 CodeReview 机制是否健全是一个工程团队能否长期健康发展的决定因素之一,这次我们读一篇关于 CodeReview 如何做得更好的文章: [how-to-make-good-code-reviews-better](https://stackoverflow.blog/2019/09/30/how-to-make-good-code-reviews-better/)。
|
||||
|
||||
## 2 概述 & 精读
|
||||
|
||||
作者结合自己在 Uber、微软的工作经历介绍了自己对如何做好 CodeReview 的看法。
|
||||
|
||||
### CodeReview 的覆盖范围
|
||||
|
||||
**Good CodeReview** 会检查代码的正确性、测试覆盖率、功能变化、是否遵循代码规范与最佳实践、可以指出一些较为明显的改进点,比如难以阅读的写法、未使用到变量、一些边界问题、commit 数量过大需要拆分等等。
|
||||
|
||||
**Better CodeReview** 会检查引入代码的必要性,与已有系统是否适配,是否具有可维护性,从抽象角度思考代码是否与已有系统逻辑能够自洽。
|
||||
|
||||
> Better CodeReview 会关注在可维护性层面,并具有全局性,往往几个局部正确的代码组合在一起会产生错误的结果,或者是没必要的代码,或者是相互冲突的逻辑。Better CodeReview 更多用在底层架构场景,因为架构底层模块关联比较紧密,需要有整体视角,而业务上层模块间最好采用解耦模式,这样不仅不需要更耗费精力的 Better CodeReview,也是一种更正确的架构设计。
|
||||
|
||||
### CodeReview 的语气
|
||||
|
||||
**Good CodeReview** 会给出建设性意见,而不是发表强硬措辞要求对方改正,或认为自己的意见是唯一正确的答案,因为这样的评论其实具有一定攻击性,激发对方的防御心理,产生敌对心态,这样会从内部瓦解一个团队。最好能给出建议,或者多个选择,给对方留有余地。
|
||||
|
||||
**Better CodeReview** 永远是考虑全面且正向积极的,会对写的好的地方进行鼓励,对写的不好的地方也体现出善解人意的关怀,考虑到对方可能花费了很多心血,以一种换位思考的鼓励心态进行评论。
|
||||
|
||||
> 其实读到语气这一章节,逐渐发现 CodeReview 不仅是一个技术专业行为,还是一个人与人相处的社交行为,有的人平时与人打交道非常谦逊,但在 CodeReview 中就变得尖酸刻薄,显然是只关注到了 CodeReview 的专业性这一面,忽略了社交性这一点。而要做到 Better CodeReview 还要学会换位思考,体现出包容、正向积极的态度,因为你技术经验更丰富,能指出别人的问题很正常,但能保持谦逊,让别人容易接受并受到鼓励,可以让你成为一个有气度的技术专家。
|
||||
|
||||
### 如何完成 CodeReview 的审阅
|
||||
|
||||
**Good CodeReview** 不会轻易通过那些开放式 PR,至少在其被得到充分讨论前,但每个 Review 者对自己关注的部分完成 Review 后需要进行反馈,无论是 “看起来不错” 或者用缩写单词 “LGTM”,之后需要有明确的跟进,比如通过协作软件通知作者进行进一步反馈。
|
||||
|
||||
**Better CodeReview** 实际执行中会更加灵活一些,对于一些比较紧急的改动会留下改进建议,但快速通过,让作者通过后续代码提交解决遗留的问题。
|
||||
|
||||
> 实际工作场景会遇到一些开放式或紧急的提交,良好的 CodeReview 习惯自然是要严谨一些,讨论清楚再通过,并且要及时反馈。但某些比较紧急的提交就要区别对待了,更好的态度是在实践中灵活对待,但及时紧急通过了,也要保证问题在后续得以修复,比如在代码中留一些 "TODO" 或 "FIXME" 的标记,写上对应的负责人与预期解决时间。
|
||||
|
||||
### 从 CodeReview 到直接交流
|
||||
|
||||
**Good CodeReview** 会给出完整的评论和修改建议,如果后续提交的代码不符合预期,Review 者可以直接与代码提交者面对面交流,这样可以避免后续花费更多沟通时间。
|
||||
|
||||
**Better CodeReview** 会在第一次给出完整的评论和修改建议,如果后续提交代码不符合预期,会立即与代码提交者当面沟通,避免异步沟通带来更多的理解偏差。
|
||||
|
||||
> 补充一下,在 PR 内容过多时也可以选择直接与提交者当面沟通,这样可以更多理解作者的想法,使 Review 准确性更高。另外并不要每次都直接交流,异步的 CodeReview 本身就是一种提效方案,这会使你工作节奏把握在自己手中,仅在这种方案出现沟通问题时再选择当面交流。
|
||||
|
||||
### 区分重点
|
||||
|
||||
**Good CodeReview** 可以区分提示的重要程度,并在不太重要的改动前面加上 “nit:” 标记,这样可以使提交者的注意力集中在重要的问题上。
|
||||
|
||||
**Better CodeReview** 会采取工具手段解决这些问题,比如一些代码 lint 工具,因为这些问题往往是可以被工具自动化解决的。
|
||||
|
||||
> 代码自动化工具的目的,很大一部分也是为了保证代码一致性,从而降低 CodeReview 成本,也减少不重要的评论信息出现,让 CodeReview 尽可能反馈逻辑问题而不是格式问题。
|
||||
|
||||
### 针对新人的 CodeReview
|
||||
|
||||
**Good CodeReview** 对任何人都是用相同评判标准,可以遵循上面几点注意事项。
|
||||
|
||||
**Better CodeReview** 会对新人区分对待,对新人给予对多的耐心、解释和评论,甚至给出解决方法,并更积极的给出鼓励。
|
||||
|
||||
> 任何人到一家新公司都有适应过程,一视同仁是 base 要求,但如果能给予新人更多关怀就更好啦。
|
||||
|
||||
### 跨办公区、时区的 CodeReview
|
||||
|
||||
**Good CodeReview** 仅在工作时间有重叠的时间范围内进行 CodeReview,这样能保证对方可以积极响应,在必要时进行语音、视频沟通。
|
||||
|
||||
**Better CodeReview** 会注意到更本质的问题,留意跨团队协作的必要性,如果某个模块经常被另一个时区同时修改,也许可以将这个模块交给对方维护,或者将 CodeReview 交给对方团队内部进行会更加高效。
|
||||
|
||||
> 笔者所在公司也有跨时区协作情况,但绝大部分场景会避免跨时区的 CodeReview,因为 CodeReview 一般会在同一时区团队内部进行,这样效率更高,应对跨时区协作时,往往也是电话、视频会议优先。
|
||||
|
||||
### 公司支持
|
||||
|
||||
**Good CodeReview** 会得到公司组织支持,公司能意识到这么做虽然看起来占用开发时间,但长远来看提升了开发效率,因此能任何 CodeReview 价值。
|
||||
|
||||
**Better CodeReview** 会得到公司进一步支持,公司甚至不断研发并完善 CodeReview 系统与流程,通过系统化方案保证上面几项 CodeReview 注意事项是否有在团队内落实,可以全员参与。
|
||||
|
||||
> CodeReview 也是一种团队文化和公司文化,公司文化带来的是规章制度与系统工具,团队文化带来的是良好 CodeReview 氛围与更高 CodeReview 的效率。
|
||||
|
||||
## 3 总结
|
||||
|
||||
总结一下,良好的 CodeReview 需要做到以下几点:
|
||||
|
||||
1. 更全面,从正确性到系统影响评估。
|
||||
2. 注意语气,从给出建设性一觉到换位思考。
|
||||
3. 及时完成审阅,从充分讨论到随机应变。
|
||||
4. 加强交流,从面对面交流到灵活选择最高效的沟通方式。
|
||||
5. 区分重点,从添加标记到利用工程化工具自动解决。
|
||||
6. 对新人要更友好。
|
||||
7. 尽量避免跨时区协作,必要时选择视频会议。
|
||||
|
||||
最后,希望 CodeReview 能够得到公司的支持,如果你们公司还没有认可 CodeReview 的价值,可以将这篇文章分享给你的领导。
|
||||
|
||||
> 讨论地址是:[精读《如何做好 CodeReview》 · Issue #237 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/237)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,302 @@
|
||||
## 1 引言
|
||||
|
||||
很多人都用过 React Suspense,但如果你认为它只是配合 React.lazy 实现异步加载的蒙层,就理解的太浅了。实际上,React Suspense 改变了开发规则,要理解这一点,需要作出思想上的改变。
|
||||
|
||||
我们结合 [Why React Suspense Will Be a Game Changer](https://medium.com/react-in-depth/why-react-suspense-will-be-a-game-changer-37b40fea71ec) 这篇文章,带你重新认识 React Suspense。
|
||||
|
||||
## 2 概述
|
||||
|
||||
异步加载是前端开发的重要环节,也是一直以来样板代码最严重的场景之一,原文通过三种取数方案的对比,逐渐找到一种最佳的异步取数方式。
|
||||
|
||||
在讲解这三种取数方案之前,首先通过下面这张图说明了 Suspense 的功能:
|
||||
|
||||

|
||||
|
||||
从上图可以看出,子元素在异步取数时会阻塞父组件渲染,并一直冒泡到最外层第一个 Suspense,此时 Suspense 不会渲染子组件,而是渲染 `fallback`,当所有子组件异步阻塞取消后才会正常渲染。
|
||||
|
||||
下面介绍文中给出的三种取数方式,首先是最原始的本地状态管理方案。
|
||||
|
||||
### 本地异步状态管理,直白但不利于维护
|
||||
|
||||
在 Suspense 方案出来之前,我们一般都在代码中利用本地状态管理异步数据。
|
||||
|
||||
即便代码做了一定抽象,那也只是把逻辑从一个文件移到了另一个问题,可维护性与可拓展性都没有本质的改变,因此基本可以用下面的结构说明:
|
||||
|
||||
```javascript
|
||||
class DynamicData extends Component {
|
||||
state = {
|
||||
loading: true,
|
||||
error: null,
|
||||
data: null
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
fetchData(this.props.id)
|
||||
.then(data => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
data
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: error.message
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.id !== prevProps.id) {
|
||||
this.setState({ loading: true }, () => {
|
||||
fetchData(this.props.id)
|
||||
.then(data => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
data
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: error.message
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loading, error, data } = this.state;
|
||||
return loading ? (
|
||||
<p>Loading...</p>
|
||||
) : error ? (
|
||||
<p>Error: {error}</p>
|
||||
) : (
|
||||
<p>Data loaded ?</p>
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如上所述,首先申明本地状态管理至少三种数据:异步状态、异步结果与异步错误,其次在不同的生命周期中处理初始化发请求与重新发请求的问题,最后在渲染函数中根据不同的状态渲染不同的结果,所以实际上我们写了三个渲染组件。
|
||||
|
||||
从下面几个角度对上述代码进行评价:
|
||||
|
||||
- **冗余的三种状态 - 糟糕的开发体验**
|
||||
- 很明显,存储了三套数据,渲染三种结果,不利于开发维护。
|
||||
- **冗余的样板代码 - 糟糕的开发体验**
|
||||
- 为了管理异步状态,上述代码非常冗长,显然这个问题是存在的。
|
||||
- **数据与状态封闭性 - 糟糕的用户体验 + 开发体验**
|
||||
- 所有数据与状态管理都存储在每一个这种组件中,将取数状态与组件绑定的结果就是,我们只能忍受组件独立运行的 Loading 逻辑,而无法对他们进行统一管理。
|
||||
- **重新取数 - 糟糕的开发体验**
|
||||
- 需要在另一个生命周期中申明重新取数,很明显是个麻烦的行为。
|
||||
- **一闪而过的短暂 Loading - 糟糕的用户体验**
|
||||
- 如果用户网速足够快,则 Loading 时间会非常短,此时一闪而过的 Loading 反而比没有 Loading 更烦人,我们应该在用户感知到卡的时候再出现 Loading 状态。
|
||||
|
||||
### Context 管理状态,有进步但问题依然很多
|
||||
|
||||
如果利用 Context 做状态共享,我们将取数的数据管理与逻辑代码写在父组件,子组件专心用于展示,效果会好一些,代码如下:
|
||||
|
||||
```javascript
|
||||
const DataContext = React.createContext();
|
||||
|
||||
class DataContextProvider extends Component {
|
||||
// We want to be able to store multiple sources in the provider,
|
||||
// so we store an object with unique keys for each data set +
|
||||
// loading state
|
||||
state = {
|
||||
data: {},
|
||||
fetch: this.fetch.bind(this)
|
||||
};
|
||||
|
||||
fetch(key) {
|
||||
if (this.state[key] && (this.state[key].data || this.state[key].loading)) {
|
||||
// Data is either already loaded or loading, so no need to fetch!
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(
|
||||
{
|
||||
[key]: {
|
||||
loading: true,
|
||||
error: null,
|
||||
data: null
|
||||
}
|
||||
},
|
||||
() => {
|
||||
fetchData(key)
|
||||
.then(data => {
|
||||
this.setState({
|
||||
[key]: {
|
||||
loading: false,
|
||||
data
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
this.setState({
|
||||
[key]: {
|
||||
loading: false,
|
||||
error: e.message
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <DataContext.Provider value={this.state} {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
class DynamicData extends Component {
|
||||
static contextType = DataContext;
|
||||
|
||||
componentDidMount() {
|
||||
this.context.fetch(this.props.id);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.id !== prevProps.id) {
|
||||
this.context.fetch(this.props.id);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { id } = this.props;
|
||||
const { data } = this.context;
|
||||
|
||||
const idData = data[id];
|
||||
|
||||
return idData.loading ? (
|
||||
<p>Loading...</p>
|
||||
) : idData.error ? (
|
||||
<p>Error: {idData.error}</p>
|
||||
) : (
|
||||
<p>Data loaded ?</p>
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`DataContextProvider` 组件承担了状态管理与异步逻辑工作,而 `DynamicData` 组件只需要从 Context 获取异步状态渲染即可,这样来看至少解决了一部分问题,我们还是从之前的角度进行评价:
|
||||
|
||||
- **冗余的三种状态 - 糟糕的开发体验**
|
||||
- 问题依然存在,只不过代码的位置转移了一部分到父组件。
|
||||
- **冗余的样板代码 - 糟糕的开发体验**
|
||||
- 将展示与逻辑分离,成功降低了样板代码数量,至少当一个异步数据复用于多个组件时,不需要写多份样板代码了。
|
||||
- **数据与状态封闭性 - 糟糕的用户体验 + 开发体验**
|
||||
- 这个问题得到一定程度解决,但是引入了新问题,即这个子组件仅在特定环境下可以正常运行。但在一个良好的设计下,组件运行不应该依赖于它所处的位置。
|
||||
- **重新取数 - 糟糕的开发体验**
|
||||
- 问题依然存在。
|
||||
- **一闪而过的短暂 Loading - 糟糕的用户体验**
|
||||
- 问题依然存在。
|
||||
|
||||
### Suspense 管理状态,最棒的方案
|
||||
|
||||
利用 Suspense 进行异步处理,代码处理大概是这样的:
|
||||
|
||||
```javascript
|
||||
import createResource from "./magical-cache-provider";
|
||||
const dataResource = createResource(id => fetchData(id));
|
||||
|
||||
class DynamicData extends Component {
|
||||
render() {
|
||||
const data = dataResource.read(this.props.id);
|
||||
return <p>Data loaded ?</p>;
|
||||
}
|
||||
}
|
||||
|
||||
class App extends Component {
|
||||
render() {
|
||||
return (
|
||||
<Suspense fallback={<p>Loading...</p>}>
|
||||
<DeepNesting>
|
||||
<DynamicData />
|
||||
</DeepNesting>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在原文写作的时候,Suspense 仅能对 React.lazy 生效,但现在已经可以对任何异步状态生效了,只要符合 Pending 中 throw promise 的规则。
|
||||
|
||||
我们再审视一下上面的代码,可以发现代码量减少了很多,其中和转换成 Function Component 的写法也有关系。
|
||||
|
||||
最后还是从如下几个角度进行评价:
|
||||
|
||||
- **冗余的三种状态 - 糟糕的开发体验** - ⭐️
|
||||
- 可以看到,组件只要处理成功得到数据的状态即可,三种状态合并成了一种状态。
|
||||
- **冗余的样板代码 - 糟糕的开发体验** - ⭐️
|
||||
- 展示与逻辑完全分离,展示只要拿到数据展示 UI 即可。
|
||||
- **数据与状态封闭性 - 糟糕的用户体验 + 开发体验** - ⭐️
|
||||
- 这个问题得到了完美的解决,具体看下面详细介绍。
|
||||
- **重新取数 - 糟糕的开发体验** - ⭐️
|
||||
- 不需要关心何时需要重新取数,当数据变化时会自动执行。
|
||||
- **一闪而过的短暂 Loading - 糟糕的用户体验**
|
||||
- 问题依然存在。
|
||||
|
||||
为了进一步说明 Suspense 的魔力,笔者特意把这段代码单独拿出来说明:
|
||||
|
||||
```javascript
|
||||
class App extends Component {
|
||||
render() {
|
||||
return (
|
||||
<Suspense fallback={<p>Loading...</p>}>
|
||||
<DeepNesting>
|
||||
<MaybeSomeAsycComponent />
|
||||
<Suspense fallback={<p>Loading content...</p>}>
|
||||
<ThereMightBeSeveralAsyncComponentsHere />
|
||||
</Suspense>
|
||||
<Suspense fallback={<p>Loading footer...</p>}>
|
||||
<DeeplyNestedFooterTree />
|
||||
</Suspense>
|
||||
</DeepNesting>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
上面代码表明了逻辑与展示的完美分离。
|
||||
|
||||
从代码结构上来看,我们可以在任何需要异步取数的组件父级添加 Suspense 达到 Loading 的效果,也就是说,如果只在最外层加一个 Suspense,那么整个应用所有 Loading 都结束后才会渲染,然而我们也能随心所欲的在任何层级继续添加 Suspense,那么对应作用域内的 Loading 就会首先执行完毕,并由当前的 Suspense 控制。
|
||||
|
||||
**这意味着我们可以自由决定 Loading 状态的范围组合。** 试想当 Loading 状态交由组件控制的方案一与方案二,是不可能做到合并 Loading 时机的,而 Suspense 方案做到了将 Loading 状态与 UI 分离,我们可以通过添加 Suspense 自由控制 Loading 的粒度。
|
||||
|
||||
## 3 精读
|
||||
|
||||
Suspense 对所有子组件异步都可以作用,因此无论是 React.lazy 还是异步取数,都可以通过 Suspense 进行 Pending。
|
||||
|
||||
异步时机被 Suspense pending 需要遵循一定规则,这个规则在之前的 [精读《Hooks 取数 - swr 源码》](https://github.com/dt-fe/weekly/blob/v2/128.%E7%B2%BE%E8%AF%BB%E3%80%8AHooks%20%E5%8F%96%E6%95%B0%20-%20swr%20%E6%BA%90%E7%A0%81%E3%80%8B.md) 有介绍过,即 Suspense 要求代码 suspended,即抛出一个可以被捕获的 Promise 异常,在这个 Promise 结束后再渲染组件,因此取数函数需要在 Pending 状态时抛出一个 Promise,使其可以被 Suspense 捕获到。
|
||||
|
||||
另外,关于文中提到的 fallback 最小出现时间的保护间隔,目前还是一个 [Open Issue](https://github.com/facebook/react/issues/17351),也许有一天 React 官方会提供支持。
|
||||
|
||||
不过即便官方不支持,我们也有方式实现,即让这个逻辑由 fallback 组件实现:
|
||||
|
||||
```jsx
|
||||
<Suspense fallback={MyFallback} />;
|
||||
|
||||
const MyFallback = () => {
|
||||
// 计时器,200 ms 以内 return null,200 ms 后 return <Spin />
|
||||
};
|
||||
```
|
||||
|
||||
## 4 总结
|
||||
|
||||
之所以说 Suspense 开发方式改变了开发规则,是因为它做到了将异步的状态管理与 UI 组件分离,所有 UI 组件都无需关心 Pending 状态,而是当作同步去执行,这本身就是一个巨大的改变。
|
||||
|
||||
另外由于状态的分离,我们可以利用纯 UI 组件拼装任意粒度的 Pending 行为,以整个 App 作为一个大的 Suspense 作为兜底,这样 UI 彻底与异步解耦,哪里 Loading,什么范围内 Loading,完全由 Suspense 组合方式决定,这样的代码显然具备了更强的可拓展性。
|
||||
|
||||
> 讨论地址是:[精读《Suspense 改变开发方式》 · Issue #238 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/238)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,132 @@
|
||||
## 1 引言
|
||||
|
||||
先说结论:Webpack5 模块联邦让 Webpack 达到了线上 Runtime 的效果,让代码直接在项目间利用 CDN 直接共享,不再需要本地安装 Npm 包、构建再发布了!
|
||||
|
||||
我们知道 Webpack 可以通过 DLL 或者 Externals 做代码共享时 Common Chunk,但不同应用和项目间这个任务就变得困难了,我们几乎无法在项目之间做到按需热插拔。
|
||||
|
||||
模块联邦是 Webpack5 新内置的一个重要功能,可以让跨应用间真正做到模块共享,所以这周让我们通过 [webpack-5-module-federation-a-game-changer-in-javascript-architecture](https://indepth.dev/webpack-5-module-federation-a-game-changer-in-javascript-architecture/#its-important-to-note-these-are-special-entry-points-they-are-only-a-few-kb-in-size-containing-a-special-webpack-runtime-that-can-interface-with-the-host-it-is-not-a-standard-entry-point--7/) 这篇文章了解什么是 “模块联邦” 功能。
|
||||
|
||||
## 2 概述 & 精读
|
||||
|
||||
### NPM 方式共享模块
|
||||
|
||||
想象一下正常的共享模块方式,对,就是 NPM。
|
||||
|
||||
如下图所示,正常的代码共享需要将依赖作为 Lib 安装到项目,进行 Webpack 打包构建再上线,如下图:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1MoLPy.z1gK0jSZLeXXb9kVXa-2494-1478.png">
|
||||
|
||||
对于项目 Home 与 Search,需要共享一个模块时,最常见的办法就是将其抽成通用依赖并分别安装在各自项目中。
|
||||
|
||||
虽然 Monorepo 可以一定程度解决重复安装和修改困难的问题,但依然需要走本地编译。
|
||||
|
||||
### UMD 方式共享模块
|
||||
|
||||
真正 Runtime 的方式可能是 UMD 方式共享代码模块,即将模块用 Webpack UMD 模式打包,并输出到其他项目中。这是非常普遍的模块共享方式:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1rQnSy4n1gK0jSZKPXXXvUXXa-2404-1484.png">
|
||||
|
||||
对于项目 Home 与 Search,直接利用 UMD 包复用一个模块。但这种技术方案问题也很明显,就是包体积无法达到本地编译时的优化效果,且库之间容易冲突。
|
||||
|
||||
### 微前端方式共享模块
|
||||
|
||||
微前端:micro-frontends (MFE) 也是最近比较火的模块共享管理方式,微前端就是要解决多项目并存问题,多项目并存的最大问题就是模块共享,不能有冲突。
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1vqvTy1T2gK0jSZFvXXXnFXXa-2410-1520.png">
|
||||
|
||||
由于微前端还要考虑样式冲突、生命周期管理,所以本文只聚焦在资源加载方式上。微前端一般有两种打包方式:
|
||||
|
||||
1. 子应用独立打包,模块更解耦,但无法抽取公共依赖等。
|
||||
2. 整体应用一起打包,很好解决上面的问题,但打包速度实在是太慢了,不具备水平扩展能力。
|
||||
|
||||
### 模块联邦方式
|
||||
|
||||
终于提到本文的主角了,作为 Webpack5 内置核心特性之一的 Federated Module:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1qLz1yYj1gK0jSZFuXXcrHpXa-2414-1474.png">
|
||||
|
||||
从图中可以看到,这个方案是直接将一个应用的包应用于另一个应用,同时具备整体应用一起打包的公共依赖抽取能力。
|
||||
|
||||
让应用具备模块化输出能力,其实开辟了一种新的应用形态,即 “中心应用”,这个中心应用用于在线动态分发 Runtime 子模块,并不直接提供给用户使用:
|
||||
|
||||
<img width=300 src="https://img.alicdn.com/tfs/TB1ymbWy7Y2gK0jSZFgXXc5OFXa-1346-1442.png">
|
||||
|
||||
对微前端而言,这张图就是一个完美的主应用,因为所有子应用都可以利用 Runtime 方式复用主应用的 Npm 包和模块,更好的集成到主应用中。
|
||||
|
||||
模块联邦的使用方式如下:
|
||||
|
||||
```js
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
|
||||
|
||||
module.exports = {
|
||||
// other webpack configs...
|
||||
plugins: [
|
||||
new ModuleFederationPlugin({
|
||||
name: "app_one_remote",
|
||||
remotes: {
|
||||
app_two: "app_two_remote",
|
||||
app_three: "app_three_remote"
|
||||
},
|
||||
exposes: {
|
||||
AppContainer: "./src/App"
|
||||
},
|
||||
shared: ["react", "react-dom", "react-router-dom"]
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./public/index.html",
|
||||
chunks: ["main"]
|
||||
})
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
模块联邦本身是一个普通的 Webpack 插件 `ModuleFederationPlugin`,插件有几个重要参数:
|
||||
|
||||
1. `name` 当前应用名称,需要全局唯一。
|
||||
2. `remotes` 可以将其他项目的 `name` 映射到当前项目中。
|
||||
3. `exposes` 表示导出的模块,只有在此申明的模块才可以作为远程依赖被使用。
|
||||
4. `shared` 是非常重要的参数,制定了这个参数,可以让远程加载的模块对应依赖改为使用本地项目的 React 或 ReactDOM。
|
||||
|
||||
比如设置了 `remotes: { app_twp: "app_two_remote" }`,在代码中就可以直接利用以下方式直接从对方应用调用模块:
|
||||
|
||||
```js
|
||||
import { Search } from "app_two/Search";
|
||||
```
|
||||
|
||||
这个 `app_two/Search` 来自于 `app_two` 的配置:
|
||||
|
||||
```js
|
||||
// app_two 的 webpack 配置
|
||||
export default {
|
||||
plugins: [
|
||||
new ModuleFederationPlugin({
|
||||
name: "app_two",
|
||||
library: { type: "var", name: "app_two" },
|
||||
filename: "remoteEntry.js",
|
||||
exposes: {
|
||||
Search: "./src/Search"
|
||||
},
|
||||
shared: ["react", "react-dom"]
|
||||
})
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
正是因为 `Search` 在 `exposes` 被导出,我们因此可以使用 `[name]/[exposes_name]` 这个模块,这个模块对于被引用应用来说是一个本地模块。
|
||||
|
||||
## 3 总结
|
||||
|
||||
模块联邦为更大型的前端应用提供了开箱解决方案,并已经作为 Webpack5 官方模块内置,可以说是继 Externals 后最终的运行时代码复用解决方案。
|
||||
|
||||
另外 Webpack5 还内置了大量编译时缓存功能,可以看到,无论是性能还是多项目组织,Webpack5 都在尝试给出自己的最佳思路,期待 Webpack5 正式发布,前端工程化会迈向一个新的阶段。
|
||||
|
||||
> 讨论地址是:[精读《Webpack5 新特性 - 模块联邦》 · Issue #239 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/239)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
Reference in New Issue
Block a user