GithubHelp home page GithubHelp logo

blog's People

Contributors

bowenz avatar

Stargazers

 avatar

Watchers

 avatar  avatar

blog's Issues

GithubDesktop问题总结

Mac系统

提交时遇到npx command not found

解决:

  • 创建文件 ~/.huskyrc
  • 写入以下代码
# ~/.huskyrc
# This loads nvm.sh and sets the correct PATH before running hook
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

husky执行commitlint报错——【The '.husky/commit-msg' hook was ignored because it's not set as executable.】

通过husky提交代码时运行commitlint,设置的commit-msg文件执行报错,提示

hint: The '.husky/commit-msg' hook was ignored because it's not set as executable.
hint: You can disable this warning with `git config advice.ignoredHook false`.

原因是 hooks 脚本默认为不可执行,所以需要将它设为可执行,可以在项目根目录运行 chmod +x .husky/pre-commit 或者 chmod +x .husky/commit-msg命令,然后再提交一次就可以了。

在Windows环境没有出现这种问题,Mac环境下会出现

工具代码搜集

1. 保存Chrome中network中的所有图片

来自https://gist.github.com/tobek/a17fa9101d7e28ddad26

/* open up chrome dev tools (Menu > More tools > Developer tools)
 * go to network tab, refresh the page, wait for images to load (on some sites you may have to scroll down to the images for them to start loading)
 * right click/ctrl click on any entry in the network log, select Copy > Copy All as HAR
 * open up JS console and enter: var har = [paste]
 * (pasting could take a while if there's a lot of requests)
 * paste the following JS code into the console
 * copy the output, paste into a text file
 * open up a terminal in same directory as text file, then: wget -i [that file]
 */

var imageUrls = [];
har.log.entries.forEach(function (entry) {
  // This step will filter out all URLs except images. If you just want e.g. just jpg's then check mimeType against "image/jpeg", etc.
  if (entry.response.content.mimeType.indexOf("image/") !== 0) return;
  imageUrls.push(entry.request.url);
});
console.log(imageUrls.join('\n'));

2. 获取网页某元素中的所有汉字

function getChinese(element) {
  return Array.from(
    new Set(
      Array.from(element.innerText).filter((item) =>
        /\p{Unified_Ideograph}/u.test(item)
      )
    )
  );
}

React Hooks搭配TypeScript使用

Hooks 的 TS 用法

翻译自:https://medium.com/@jrwebdev/react-hooks-in-typescript-88fce7001d0d
并加了些改造

useState

多数情况下,useState可以根据初始值做类型推断,不需要额外限定类型,但如果初始值是nullundefined的话,或者需要更复杂的类型限制,比如数组或者对象,就需要声明类型。

// 可以推断为number类型
const [value, setValue] = useState(0);

// 明确声明类型
const [value, setValue] = useState<number | undefined>(undefined);
const [value, setValue] = useState<Array<number>>([]);

// 复杂对象类型
interface MyObject {
  foo: string;
  bar?: number;
}
const [value, setValue] = useState<MyObject>({ foo: 'hello' });

useContext

useContent可以通过传入的context对象来推断类型,所以也不需要明确限定类型。

type Theme = 'light' | 'dark';
const ThemeContext = createContext<Theme>('dark');

const App = () => (
  <ThemeContext.Provider value="dark">
    <MyComponent />
  </ThemeContext.Provider>
);

const MyComponent = () => {
  const theme = useContext(ThemeContext);
  return <div>The theme is {theme}</div>;
};

useEffect / useLayoutEffect

这两个 hooks 用于处理函数副作用,返回一个清理函数,不需要处理返回值,所以不需要类型定义。

useEffect(() => {
  const subscriber = subscribe(options);
  return () => {
    unsubscribe(subscriber);
  };
}, [options]);

useMemo / useCallback

这两个都可以通过返回值来推断返回类型,但是 useCallback 需要指定参数的类型。

const value = 10;
// 推断为 number
const result = useMemo(() => value * 2, [value]);

const multiplier = 2;
// 推断为 (value: number) => number
const multiply = useCallback((value: number) => value * multiplier, [
  multiplier,
]);

useRef

在过去,refs 用于引用 DOM 节点,其属性current是只读的,这个属性在被挂载到 DOM 上之前的初始值为null

const MyInput = () => {
  const inputRef = useRef<HTMLInputElement>(null);
  return <input ref={inputRef} />;
};

Hooks 出现之后,useRef还可以用作类似 class 中的实例属性的功能,而且current也变成可以修改了,并且可以使用类型推断:

const myNumberRef = useRef(0);
myNumberRef.current += 1;

useReducer

这个 hook 的模式有点像 Redux,其中 actions 和 state 的类型可以根据useReducer的参数来推断:

interface State {
  value: number;
}

type Action =
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'incrementAmount'; amount: number };

const counterReducer = (state: State, action: Action) => {
  switch (action.type) {
    case 'increment':
      return { value: state.value + 1 };
    case 'decrement':
      return { value: state.value - 1 };
    case 'incrementAmount':
      return { value: state.value + action.amount };
    default:
      throw new Error();
  }
};

const [state, dispatch] = useReducer(counterReducer, { value: 0 });

dispatch({ type: 'increment' });
dispatch({ type: 'decrement' });
dispatch({ type: 'incrementAmount', amount: 10 });

// 错误
dispatch({ type: 'invalidActionType' });

useImperativeHandle

useImperativeHandle可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用:

export interface MyInputHandles {
  focus(): void;
}

const MyInput: React.RefForwardingComponent<MyInputHandles, MyInputProps> = (
  props,
  ref
) => {
  const inputRef = useRef<HTMLInputElement>(null);

  useImperativeHandle(ref, () => ({
    focus: () => {
      if (inputRef.current) {
        inputRef.current.focus();
      }
    },
  }));

  return <input {...props} ref={inputRef} />;
};

export default React.forwardRef(MyInput);

上例的功能是,组件暴露出一个 focus 方法,可以在外部调用,使组件内的 input 获取焦点。

首先,MyInputHandles声明了通过 ref 暴露出的对象类型,MyInputHandles声明了组件参数类型。
注意MyInput类型需要定义为React.RefForwardingComponent

如果要使用这个组件,需要:

import MyInput, { MyInputHandles } from './MyInput';

const Autofocus = () => {
  const myInputRef = useRef<MyInputHandles>(null);

  useEffect(() => {
    if (myInputRef.current) {
      myInputRef.current.focus();
    }
  });

  return <MyInput ref={myInputRef} />;
};

useDebugValue

useDebugValue可用于在 React 开发者工具中显示自定义 hook 的标签。类型可以根据传入的值直接推断,不需要明确指定。

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  // ...

  // 在开发者工具中的这个 Hook 旁边显示标签
  // e.g. "FriendStatus: Online"
  useDebugValue(isOnline ? 'Online' : 'Offline');

  return isOnline;
}

手机浏览器视频内置播放

<video 
  x-webkit-airplay="true" 
  playsinline webkit-playsinline 
  preload="meta" 
  x5-playsinline 
  x5-video-player-type="h5" 
  x5-video-player-fullscreen="true" 
  src="./xxx.mp4"></video>

CSS Flex 居中定位移除滚动问题

问题

在平常开发中我们经常会遇到上下左右居中的布局,通常都会用flex这么写:

Loading  - CodeSandbox

这种在父容器宽高大于子元素的时候是没问题的,但是如果父容器高度小于子元素,就会这样:

Loading  - CodeSandbox (1)

这种情况可能就不是我们希望要的,尤其是当父容器设置overflow: auto滚动后,会发现即便滚动到顶部,子元素依然无法完全展示:

image

解决

方案1

最简单的办法,就是用margin代替flex的居中,将父容器的justify-content: center;align-items: center;去掉,给子元素加上margin: auto;

Loading  - CodeSandbox (2)

方案2

可以用flex的safe,但是现在兼容性比较差,只有火狐浏览器支持,下图是在火狐浏览器的效果:

image

总结

最终的代码可以查看CodeSandbox

资料

webpack打包时出现的问题记录

Babel相关

1.@babel/helper-compilation-targets' do not define a '.' subpath

上线打包时出现Error: Package exports for '/path/xxx/node_modules/@babel/helper-compilation-targets' do not define a '.' subpath的问题,经查是新版babel(v7.8)在nodejs版本小于13.2时会出现该问题,可以升级nodejs版本,或将@babel/core@babel/preset-env版本锁定到7.7.0版本

npm包安装

1. 安装imagemin-pngquant 报错: pngquant failed to build, make sure that libpng is installed

国内安装经常导致依赖的pngquant下载超时,所以可以先本地安装依赖的文件:

brew install libpng-dev
npm install -g pngquant-bin

然后再运行npm i就可以安装成功了

html2canvas使用踩坑总结

1. 版本问题

html2canvas要用1.0.0-rc.0比较稳定,其他版本会有各种问题,如rc.7 出现iOS一直pending的状态,安卓图片会被切一半

2.text-shadow问题

如果要加文字阴影text-shadow,需要用rc.1版本,因为rc.0给一个字加阴影,其他字也会自动加上,升级到rc1问题解决

3.iOS白边问题

ios 底部出现白边,加上宽高限定就好了

const $realPoster = document.querySelector('.real-poster') as HTMLElement;
html2canvas($realPoster, {
  backgroundColor: null,
  useCORS: true,
  width: $realPoster.offsetWidth,
  height: $realPoster.offsetHeight,
})
  .then((canvas): void => {
    const posterImg = new Image();
    posterImg.src = canvas.toDataURL();
    document.querySelector('.canvas-poster')?.appendChild(posterImg);
    Toast.hide();
  })
  .catch((err) => {
    console.log('====html2canvas err====', err);
    Toast.hide();
  });

移动端H5问题总结

微信

通用

  1. 开发整个页面旋转90deg的活动时发现,页面整体旋转后,很多元素的展示会出现问题,如modal弹窗会有闪烁,或者打开后看不到,建议不要做这种尝试,开发正常的竖屏项目或者横屏旋转打开的横屏项目。
  2. body旋转90deg后,有些设备,如安卓和部分系统版本低于14的iOS,会出现容器内滚动手势相反的问题——解决方案:使用better-scrollQuadrant工具,代码如下:
import BScroll from '@better-scroll/core';
import { Quadrant } from '@better-scroll/shared-utils';

new BScroll('.content', {
  quadrant: 2 as Quadrant,
});
  1. 微信授权会在URL上挂载code,若在进页面后,设定微信分享之前直接点反向,会导致分享出去的页面URL中带有code,造成A用户分享的页面,B用户打开后使用A用户的code,获取A用户的信息,解决方案是在进入页面时先将URL中的code去掉,再执行设置微信分享:
  // 如果链接有code, 将其从链接里去除(否则用户在initShare之前分享,有可能带上自己的wxCode)
  if (urlParams.code) {
    const params = Object.keys(urlParams).reduce((res, key) => {
      if (key !== 'code') {
        res.push(`${key}=${urlParams[key as keyof typeof urlParams] || ''}`);
      }
      return res;
    }, [] as string[]);

    const { origin, pathname } = window.location;
    const url = `${origin}${pathname}?${params.join('&')}`;
    window.history.replaceState(null, '', url);
  }

iOS

  1. iOS15长按二维码图片无法识别:iOS15更新了跨应用拖动功能,长按二维码图片会触发拖动,暂无法解决。
  2. body元素旋转90deg后,body内的二维码图片识别有问题,可能长按识别区域不在图片区域内,会偏移很多,导致二维码大部分区域无法触发长按识别——解决方案:不要旋转body,旋转body内的元素没问题,或者在二维码识别的时候将body旋转回去即可解决。

安卓iOS微信分享给机制不同

微信H5分享,安卓和iOS签名检查机制不同,iOS判断页面进入时的URL,安卓判断当前页面的URL,例如进入首页 https://h.domain.com/aa/ ,使用该URL请求微信签名,然后内部路由跳转到 https://h.domain.com/aa/bb ,重新配置新的分享内容,iOS能正常配置,安卓不会生效,若每次路由跳转后重新请求签名,则安卓能正常配置,iOS会报invalid signature错误。

最后解决方法是,iOS每次跳转不请求新签名,直接配置分享,安卓每次跳转都重新请求签名,然后配置分享

Webpack5新功能预览

回顾Webpack

概念

从Webpack官网可知,本质上,webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容。

为什么使用Webpack

前端工程化的一个很大的跨越就是模块化。

Javascript是一个10天就设计出来的语言,设计之初并没有料想到日后会有如此蓬勃的发展,因此语言在设计初期有很多的缺陷,其中之一就是缺少模块化,而代码的模块化恰好就是开发大型项目的基础功能。但在早期,Web项目更多是展示性功能,前端开发体量小,逻辑简单,没有模块化也能做。有过早期前端开发经历的伙伴可能会记得,当时还是通过手动的文件分割,然后通过立即调用函数表达式(IIFE) 来达到不同文件间的简单隔离。

随着Web技术的发展(ES5),前端项目体谅逐渐增加,此时缺少模块化支持的弊端就开始显现,这时(2010年开始)就慢慢出现了requirejs(AMD)、seajs(CMD)这种优秀的非官方模块化解决方案,还有CommonJS这种Node.js的模块方案。

之后由于HTML5和CSS3的发布,还有iPhone的出现(取消支持Flash给了JS的发展又加了把火),Web开发在2012年开始迎来了新一轮爆发式的发展,各种复杂Web项目层出不穷,项目越来越复杂,不仅有图片、视频、音频、字体等媒体文件,还有Sass、Less等样式文件,再加上后边发布的ES6、Nodejs的诞生,Web项目复杂度达到了空前的高度。

虽然JavaScript一直保持着更新,CommonJs、ES Module都是为了解决模块化而发明的,但是由于用户浏览器版本的限制,开发者不能只使用JS最新的特性,还需要兼容低版本的浏览器(IE出来挨打),因此还需要对ES6代码进行转码。

此时,Webpack出现了。

Webpack解决了哪些问题

总结来说就是:在前端项目中更高效地管理和维护项目中的每一个资源。

  1. 处理资源依赖关系
  2. 模块化开发
  3. 代码预处理(ES6、TS、Sass、Less等)
  4. 代码分割,异步chunk,按需加载
  5. tree shaking
  6. 各种插件,实现代码检查、压缩、分析等功能

Webpack存在哪些问题

随着项目的增大,代码量增多,每次执行打包都会走一遍:全部代码的ESLint、依赖分析、转码(babel、less等各种loader)、代码压缩等流程,打包速度可能从一开始的二十几秒增加到一两分钟,每次打包都是个漫长的等待。
因此我们都会做一些打包速度上的优化,比如启动babel-loader或者terser-webpack-plugin的缓存,将React之类的库改成外部依赖,或者启用DllPlugin,减少处理第三方库花费的时间等。

Webpack5

更新重点

  1. 某些语法和功能的废弃(详情查看文档
  2. 尝试用持久性缓存来提高构建性能。cache
  3. 尝试用更好的算法和默认值来改进长期缓存。
  4. 尝试用更好的 Tree Shaking 和代码生成来改善包大小。
  5. 尝试改善与网络平台的兼容性。
  6. 尝试在不引入任何破坏性变化的情况下,清理那些在实现 v4 功能时处于奇怪状态的内部结构。
  7. 试图通过现在引入突破性的变化来为未来的功能做准备,使其能够尽可能长时间地保持在 v5 版本上

其他几个可以说一下的更新点

  1. 默认支持pnp-webpack-plugin
  2. 不再为 Node.js 模块 自动引用 Polyfills(这可以使最初为 Node.js 环境编写的代码,在其他环境(如浏览器)中运行)

新特性概览

1. 资源模块(asset module)

资源模块是一种模块类型,它允许使用资源文件(字体,图标等)而无需配置额外 loader。

在 webpack 5 之前,通常使用:

资源模块类型(asset module type),通过添加 4 种新的模块类型,来替换所有这些 loader:

  • asset/resource 发送一个单独的文件并导出 URL。之前通过使用 file-loader 实现。
  • asset/inline 导出一个资源的 data URI。之前通过使用 url-loader 实现。
  • asset/source 导出资源的源代码。之前通过使用 raw-loader 实现。

asset 在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用 url-loader,并且配置资源体积限制实现。

2. 持久化缓存

我们随便找一个中大型项目,其中的文件都是成百上千的,每次编译打包,都是执行一分多钟,但是我们可能只会修改其中一部分代码,大部分代码是没有任何修改的,但是每次打包都要全量编译,无疑是一个很大的浪费。

在webpack4时代,我们需要使用cache-loader或者TerserPlugin中的cache配置来达到简单的缓存目的。使用 cache-loader 可以将编译结果写入硬盘缓存,Webpack 再次构建时如果文件没有发生变化则会直接拉取缓存。babel-loader自带缓存配置,可以配置参数 cacheDirectory 使用缓存,将每次的编译结果写进磁盘(默认在 node_modules/.cache/babel-loader 目录),使用起来不是很方便,效果也是差强人意。

到了Webpack5,直接内置缓存功能,只需要在配置中增加:

cache: {
    // v5 中缓存默认是 memory,你可以修改设置写入硬盘:
    type: 'filesystem',
    //  告诉 webpack 什么时候将数据存放在文件系统中, 当编译器闲置时候,将缓存数据都存放在一个文件中
    store: 'pack',
    // cache.buildDependencies 是一个针对构建的额外代码依赖的数组对象。webpack 将使用这些项和所有依赖项的哈希值来使文件系统缓存失效
    buildDependencies: {
      defaultWebpack: ['webpack/lib/'],
      config: [__filename],
    },
  },

我创建了一个空项目,将lodash全量引入到项目中,经对比,v5版本中,首次打包时间约为7s,但是二次打包仅需0.39s,速度飞跃提升。反观同样配置的v4版本,每次打包时间都3-7s之间,提升巨大。下图分别是v5和v4的打包情况:

image

image

cache配置说明如下:

  • cache.type:缓存类型。值为 'memory'或‘filesystem’,分别代表基于内存的临时缓存,以及基于文件系统的持久化缓存。在选择 filesystem 的情况下,下面介绍的其他属性生效。
  • cache.cacheDirectory:缓存目录。默认目录为 node_modules/.cache/webpack。
  • cache.name:缓存名称。同时也是 cacheDirectory 中的子目录命名,默认值为 Webpack 的 ${config.name}-${config.mode}。
  • cache.cacheLocation:缓存真正的存放地址。默认使用的是上述两个属性的组合:path.resolve(cache.cacheDirectory, cache.name)。该属性在赋值情况下将忽略上面的 cacheDirectory 和 name 属性。
单个模块的缓存失效

Webpack 5 会跟踪每个模块的依赖项:fileDependencies、contextDependencies、missingDependencies。当模块本身或其依赖项发生变更时,Webpack 能找到所有受影响的模块,并重新进行构建处理。

这里需要注意的是,对于 node_modules 中的第三方依赖包中的模块,出于性能考虑,Webpack 不会跟踪具体模块文件的内容和修改时间,而是依据依赖包里package.json 的 name 和 version 字段来判断模块是否发生变更。因此,单纯修改 node_modules 中的模块内容,在构建时不会触发缓存的失效。

全局的缓存失效

当模块代码没有发生变化,但是构建处理过程本身发生变化时(例如升级了 Webpack 版本、修改了配置文件、改变了环境变量等),也可能对构建后的产物代码产生影响。因此在这种情况下不能复用之前缓存的数据,而需要让全局缓存失效,重新构建并生成新的缓存。在 Webpack 5 **提供了 3 种不同维度的全局缓存失效配置。

buildDependencies

第一种配置是cache.buildDependencies,用于指定可能对构建过程产生影响的依赖项。

它的默认选项是{defaultWebpack: ["webpack/lib"]}。这一选项的含义是,当 node_modules 中的 Webpack 或 Webpack 的依赖项(例如 watchpack 等)发生变化时,当前的构建缓存即失效。

上述选项是默认内置的,无须写在项目配置文件中。配置文件中的 buildDenpendencies 还支持增加另一种选项 {config: [__filename]},它的作用是当配置文件内容或配置文件依赖的模块文件发生变化时,当前的构建缓存即失效。

version

第二种配置是 cache.version。当配置文件和代码都没有发生变化,但是构建的外部依赖(如环境变量)发生变化时,预期的构建产物代码也可能不同。这时就可以使用 version 配置来防止在外部依赖不同的情况下混用了相同的缓存。例如,可以传入 cache: {version: process.env.NODE_ENV},达到当不同环境切换时彼此不共用缓存的效果。

name

缓存的名称除了作为默认的缓存目录下的子目录名称外,也起到区分缓存数据的作用。例如,可以传入 cache: {name: process.env.NODE_ENV}。这里有两点需要补充说明:

name 的特殊性:与 version 或 buildDependencies 等配置不同,name 在默认情况下是作为缓存的子目录名称存在的,因此可以利用 name保留多套缓存。在 name 切换时,若已存在同名称的缓存,则可以复用之前的缓存。与之相比,当其他全局配置发生变化时,会直接将之前的缓存失效,即使切换回之前已缓存过的设置,也会当作无缓存处理。
当 cacheLocation 配置存在时,将忽略 name 的缓存目录功能,上述多套缓存复用的功能也将失效。

其他

除了上述介绍的配置项外,cache 还支持其他属性:managedPath、hashAlgorithm、store、idleTimeout 等,具体功能可以通过官方文档进行查询。

此外,在 Webpack 4 中,部分插件是默认启用缓存功能的(例如压缩代码的 Terser 插件等),项目在生产环境下构建时,可能无意识地享受缓存带来的效率提升,但是在 Webpack 5 中则不行。无论是否设置 cache 配置,Webpack 5 都将忽略各插件的缓存设置(例如 TerserWebpackPlugin),而由引擎自身提供构建各环节的缓存读写逻辑。因此,项目在迁移到 Webpack 5 时都需要通过上面介绍的 cache 属性来单独配置缓存。

3. 优化tree shaking

tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块语法的 静态结构 特性,例如 importexport。这个术语和概念实际上是由 ES2015 模块打包工具 rollup 普及起来的。

Webpack 4 中的 Tree Shaking 功能在使用上存在限制:只支持 ES6 类型的模块代码分析,且需要相应的依赖包或需要函数声明为无副作用等。这使得在实际项目构建过程中 Tree Shaking 的优化效果往往不尽如人意。而这一问题在 Webpack 5 中得到了不少改善。

Webpack 5 增加了对嵌套模块的导出跟踪功能,能够找到那些嵌套在最内层而未被使用的模块属性。例如下面的示例代码,在构建后的结果代码中只包含了引用的内部模块的一个属性,而忽略了不被引用的内部模块和中间模块的其他属性:

// file1.js
export const func1 = () => {
  console.log('====11111111111111====');
};
export const func2 = () => {
  console.log('====222222222222222====');
};

// file2.js
import * as file1 from './file1';
export const func3 = () => {
  file1.func1();
  console.log('====333333333333333====');
};
export const func4 = () => {
  console.log('====444444444444444====');
};
export { file1 };

// test.js
import * as file2 from './file2';
export default function test() {
  file2.file1.func2();
}

上边这段代码,在webpack4,func1和func2都会被打进去(因为func1被func3引用),但是webpack5中,只会引入func2。

4. 模块联邦(module federation)

假如现在有两个项目,A和B,A中有个User组件,B项目开发时候也需要用到,我们通常会怎么做呢?首先想到的是将User组件抽离出一个npm包,两个项目都可以引入,这样做的弊端是,如果User组件要更新,就要发新版本,然后两个项目都需要升级。或者想到使用monorepo,但是项目的配置都已经让人很头疼了。如果用现在流行的微前端,无法很好的解决问题,因为微前端更聚焦于解耦,对于抽取公共资源也没有很好地解决方案。
现在webpack5包含了一个新特性——模块联邦。简单的说,模块联邦提供了可以在当前应用中远程加载其他服务器上应用的能力。
可以使用官方提供的demo项目查看:https://github.com/mizx/mfe-webpack-demo
模块联邦本身是一个普通的 Webpack 插件 ModuleFederationPlugin,插件有几个重要参数:

  1. name 当前应用名称,需要全局唯一。
  2. remotes 可以将其他项目的 name 映射到当前项目中。
  3. exposes 表示导出的模块,只有在此申明的模块才可以作为远程依赖被使用。
  4. shared 是非常重要的参数,制定了这个参数,可以让远程加载的模块对应依赖改为使用本地项目的 React 或 ReactDOM。

引用
Webpack 究竟解决了什么问题?
webpack5新特性一览
从 v4 升级到 v5
Webpack 5 发布 (2020-10-10)
changelog-v5
Module Federation
webpack 5 模块联邦实现微前端疑难问题解决

自定义ESLint规则

2020年3月24日 10:03:18

背景

由于项目中有些问题平常不好检查,或者公司内部编码时有些规定好的注意事项,时间久了容易忘记,但是这种内部约定的规则,ESLint没有对应的规则可以检查的(比如我公司生产链接必须以斜杠结尾,否则上线会404,运维遗留问题,前端暂时也没办法改变),所以就需要自定义ESLint来检查这些问题。

开始

先放上官方教程

官方推荐使用Yeoman generator,但是我是根据一个流行的ESLint plugin库eslint-plugin-jsx-a11y作为模板开发的。

就以URL后必须加斜杠这个规则作为例子吧。

先上项目结构:

.
├── .DS_Store
├── .babelrc
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .npmignore
├── .npmrc
├── README.md
├── __tests__
│   ├── __utils__
│   │   └── parserOptionsMapper.js
│   ├── index-test.js
│   └── src
│       └── rules
│           └── url-ended-with-slash-test.js
├── package-lock.json
├── package.json
└── src
    ├── index.js
    └── rules
        └── url-ended-with-slash.js

其中src内为规则的定义,__tests__为测试代码

项目搭建

package.json文件内容:

{
  "name": "@prefix-xxx/eslint-plugin",
  "version": "1.0.0",
  "description": "自定义ESLint规则",
  "main": "lib/index.js",
  "scripts": {
    "build": "rimraf lib && babel src --out-dir lib --copy-files",
    "pub": "npm run build && xnpm publish",
    "lint:fix": "npm run lint -- --fix",
    "lint": "eslint  --config .eslintrc.js src",
    "test": "npm run jest",
    "jest": "jest __tests__/**/*"
  },
  "repository": {
    "type": "git",
    "url": "http://mtgitlab.domain.com/packages/eslint-plugin.git"
  },
  "keywords": [],
  "author": "zhaobowen",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.8.4",
    "@babel/core": "^7.8.7",
    "babel-eslint": "^10.1.0",
    "babel-jest": "^25.1.0",
    "babel-preset-airbnb": "^4.4.0",
    "eslint": "^6.8.0",
    "eslint-config-airbnb-base": "^14.1.0",
    "eslint-config-prettier": "^6.10.1",
    "eslint-plugin-import": "^2.20.1",
    "jest": "^25.1.0",
    "rimraf": "^3.0.2"
  },
  "peerDependencies": {
    "eslint": ">=0.8.0"
  },
  "jest": {
    "roots": [
      "__tests__"
    ],
    "testPathIgnorePatterns": [
      "__tests__/__utils__/"
    ],
    "testEnvironment": "node"
  }
}

es6转码使用babel,因为最终打包后的文件结构和src内的文件结构是一样的,不需要文件合并

使用ESLint的Airbnb代码风格检查

使用jest进行测试

开始开发

每个自定义规则文件,都放在src/rules/目录下,文件名就是规则名,我们新建url-ended-with-slash.js

代码如下:

module.exports = {
  meta: {
    docs: {
      description: '检测生产环境链接是否以/结尾',
      category: 'Possible Errors',
      recommended: true
    },
    messages: {
      invalidName: '生产链接需要以/结尾'
    }
  },
  create(context) {
    return {
      // 普通字符串
      Literal(node) {
        const str = String(node.value);
        if (isNotASafeLink(str)) {
          context.report({
            node,
            message:
              '检测到改字符串可能为敏感 URL,请确定是否需要以「/」结尾(生产域名后不加斜杠会404)',
            data: {
              str: node.value
            }
          });
        }
      },
      // 模板字符串
      TemplateLiteral(node) {
        const strs = node.quasis.filter(item => item.value.raw);
        if (strs.filter(item => item.value.raw.match('/')).length > 1) {
          return;
        }
        if (
          strs.some(item => {
            const str = item.value.raw;
            return isNotASafeLink(str);
          })
        ) {
          context.report({
            node,
            message:
              '检测到改字符串可能为敏感 URL,请确定是否需要以「/」结尾(生产域名后不加斜杠会404)',
            data: {
              str: node.value
            }
          });
        }
      }
    };
  }
};

meta中是这条规则的描述

create中是具体的规则校验,其中return中定义AST的检查

这里讲一下ESLint的检查机制,是先将JS代码转化成AST(abstract syntax tree, 具体概念不会可以搜索),然后根据AST的属性校验是否匹配规则,如果不匹配就report出错误或者警告。

这里重点推荐AST explorer,把要检查的代码输入进入,可以输出AST解析结果,仔细观察一下AST的结构和内容,然后根据这个去写规则。

我们上边的例子中,就是判断类型为Literal(普通字符串)和TemplateLiteral(模板字符串)节点,然后获取节点内容,判断是否可能是URL,是的话,是否以斜杠结尾,如果命中我们的规则的话,就调用context.report报出问题(问题类型是错误还是警告,可以在ESLint config中配置)。

写好的规则需要在src/index.js中引入:

/* eslint-disable global-require */
module.exports = {
  rules: {
    'url-ended-with-slash': require('./rules/url-ended-with-slash')
  },
  configs: {
    recommended: {
      plugins: ['@prefix-xxx'],
      rules: {
        'url-ended-with-slash': 'warn'
      }
    }
  }
};

其中rules就是引入的规则文件,configs中可以写一下推荐的检查规则(应该是error或者是warn),configs中的recommended名字可以随便起,这个是用在开发项目中的.eslintrc中,添加到extends中的,比如添加'plugin:our-custom-eslint-plugin-name/recommended'

测试

ESLint官方有提供专门的测试工具,RuleTester

url-ended-with-slash-test.js代码如下:

/* eslint-disable no-template-curly-in-string */
import { RuleTester } from 'eslint';
import rule from '../../../src/rules/url-ended-with-slash';

const ruleTester = new RuleTester({
  parserOptions: {
    ecmaVersion: 6
  }
});

const expectedError = {
  message:
    '检测到改字符串可能为敏感 URL,请确定是否需要以「/」结尾(生产域名后不加斜杠会404)'
};

ruleTester.run('url-ended-with-slash-test', rule, {
  valid: [
    {
      code:
        "'https://h.domain.com/promotion/?prom_type=put&channel=zyxf_gw_mobile&pi=APP&template_num=yyq'"
    },
    { code: "'/customservice/'" },
    {
      code:
        'const linkurl = `${window.location.origin}/delay-apply/?sysFlag=04&fromMsg=N`;'
    },
    {
      code: `window.location.href = 'https://xx.aa.com/download/'`
    }
  ],
  invalid: [
    {
      code: "'https://xx.aa.com/app-article?test=123&test2=234'",
      errors: [expectedError]
    },
    { code: "'/app-article'", errors: [expectedError] },
  ]
});

其中ruleTester.run及时执行测试,第一个参数是测试规则名,第二个是引入的规则逻辑,第三个参数中的validinvalid是正确和报错的情况测试代码,注意字符串内是要测试的代码,比如要测试字符串,就要写成"'xxx'"这种。

命令行执行npm run test执行测试代码,没问题就可以npm run build打包发布npm了,具体发布可以发布在公司内网npm或者公共的npm服务器,取决于个人选择了。

项目中引入

npm安装后,在.eslintrc文件中,plugins中添加插件,然后在rules中配置具体规则,或者直接在extends中假如包内定义好的规则(就是之前说的configs/recommended

根据npm包名,有两种情况:

包名有@Scoped限定,如@foo/bar

这种有分两种情况,一种是@foo/eslint-plugin,scoped后名字为eslint-plugin,可以直接在plugins中添加@foo,就会自动在node_modules下寻找@foo下名为eslint-plugin的文件,然后在extends中添加'plugin:@prefix-xxx/recommended',引入配置好的规则。要修改某条具体规则的配置,可以在rules中写'@foo/url-ended-with-slash': 1

类似的有@typescript-eslint

另一种是包名为@foo/eslint-plugin-xxx,就需要配置的时候写全名了,比如plugins中增加@foo/eslint-plugin-xxxextends中增加plugin:@foo/eslint-plugin-xxx/recommended

包名为eslint-plugin-xxx

这种是最常用的命名,可以在plugins中直接添加xxx,不需要前缀

自此就应该可以生效了。

其他问题

在开发中遇到的一些问题,也记录下来吧

  • 运行npm run test时报错,最终发现是本地安装的nodejs版本太低,是v10.x版本的,升级到v13.xj就好了

  • 其他问题忘记了,想起来再补充吧

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.