lennonover


  • 首页

  • 归档

  • 标签

代码质量-圈复杂度

发表于 2021-01-03

背景

圈复杂度是 oasis(衡量前端工程质量平台)的检测指标之一,目的是为了检测出逻辑复杂,可能质量低,且难于测试和维护的模块,提高 CR 效率和推动优化提升前端工程质量。

概念

圈复杂度(Cyclomatic complexity,CC)也称为条件复杂度,其符号为 V(G),是衡量计算机程序复杂程度的一种措施。它根据程序从开始到结束的线性独立路径的数量计算得来的,其数量上为独立路径的条数,也可理解为覆盖所有的可能情况最少使用的测试用例个数。

衡量标准

圈复杂度大说明程序代码的判断逻辑复杂,可能质量低,且难于测试和维护。 代码复杂度低,代码不一定好,但代码复杂度高,代码一定不好。

圈复杂度 代码状况 可测性 维护成本
1 - 10 清晰 高 低
10 - 20 复杂 中 中
20 - 30 非常复杂 低 高
> 30 不可读 不可测 非常高

同时测试驱动的开发和圈复杂度存在紧密的关系。一个好的测试用例是创建数量与被测代码圈复杂度值相等的测试用例,以此提升测试用例对代码的分支覆盖率。

计算方法

圈复杂度有两种计算方法:点边计算法和节点判定法。

节点判定法

因为圈复杂度所反映的是“判定条件”的数量,所以圈复杂度实际上就是等于判定节点的数量再加上 1。从 1 开始,一直往下通过程序,一但遇到 P 关键字,或者其它同类的词,就加 1。

V (G) = P + 1

其中 P 为判定节点数,常见的判定节点有:

if 语句、while 语句、for 语句、case 语句、catch 语句、and 和 or 布尔操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function test(a) {
let b = 1;
if (a < 0 || a > 100) {
a = 1;
}
for (let i = 0; i < 10; i++) {
a++;
}
while (a < 30 ) {
a++;
}
switch (a) {
case 31:
b++ ;
break;
default:
break;
}
return b === 1 ? 0 : b;
}

1 个 if ,1 个 or ,1 个 for ,1 个 while,1 个 case,1 个三元 所以代码复杂度 7

点边计算法

圈复杂度由程序的控制流图来计算:节点对应程序中个别的代码,而若一个程序运行后会立刻运行另一代码,则会有边连接另一代码对应的节点。

V(G) = E - N + 2

E 表示控制流图中边的数量,N 表示控制流图中节点的数量。

如上图正常顺序的圈复杂度为 1;if else 的圈复杂度为 2;while 的圈复杂度也为 2。

如何检测

es6-plato

1
npm install --save-dev es6-plato
1
2
3
"scripts" : {
"complexity-report": "./node_modules/.bin/es6-plato -r -d ./report src",
}
1
npm run complexity-report

eslint

oasis 用的是 eslint 提供了检测代码圈复杂度的 rules ,根据 eslint 的 api 会自动检测出所有函数的代码复杂度输出检测信息,通过解析检测信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
new CLIEngine({
baseConfig: {
root: true,
parser: "vue-eslint-parser",
},
parserOptions: {
ecmaVersion: 9,
sourceType: "module",
ecmaFeatures: {
experimentalObjectRestSpread: true,
jsx: true,
globalReturn: true,
impliedStrict: true,
},
parser: "babel-eslint",
},
useEslintrc: false,
rules: {
complexity: ["error", { max: 0 }],
},
});

降低圈复杂度

降低复杂度有的是把函数的一部分提取成另一个子函数,不会降低整个项目的复杂度,只是把决策点移到其他地方,但是这样做可以降低你在同一时间必须关注的复杂度。

单一职责

函数应该做一件事,做好这件事,只做这一件事。 — 代码整洁之道

1
2
3
4
5
6
7
8
9
10
11
12
13
function add(a, b) {
if (a === 10) {
// dosomething
} else if (a === 12) {
// dosomething
}
if (b === 10) {
// dosomething
} else if (b === 12) {
// dosomething
}
return a + b;
}

转换后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function add(a, b) {
return calcA(a) + calcB(b);
}
function calcA(a) {
if (a === 10) {
return; // dosomething
} else if (a === 12) {
return; // dosomething
}
}
function calcB(b) {
if (b === 10) {
return; // dosomething
} else if (b === 12) {
return; // dosomething
}
}

查找表

1
2
3
4
5
6
7
8
9
10
11
12
13
function demo (a, b, c) {
if (f(a, b, c)) {
if (g(a, b, c)) {
// ...
}else if (h(a, b, c)) {
// ...
}
} else if (j(a, b, c)) {
// ...
} else if (k(a, b, c)) {
// ...
}
}

转换后

1
2
3
4
5
6
7
8
9
10
11
const rules = {
x: function (a, b, c) {},
y: function (a, b, c) {},
z: function (a, b, c) {}
...
}

function demo (a, b, c) {
const action = determineAction(a, b, c)
return rules[action](a, b, c)
}

职责链

1
2
3
4
5
6
7
8
9
10
11
12
13
function demo (a, b, c) {
if (f(a, b, c)) {
if (g(a, b, c)) {
// ...
}else if (h(a, b, c)) {
// ...
}
} else if (j(a, b, c)) {
// ...
} else if (k(a, b, c)) {
// ...
}
}

转换后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const rules = [
{
match: function (a, b, c) {},
action: function (a, b, c) {},
},
{
match: function (a, b, c) {},
action: function (a, b, c) {},
},
{
match: function (a, b, c) {},
action: function (a, b, c) {},
},
];
function demo(a, b, c) {
for (let i = 0; i < rules.length; i++) {
if (rules[i].match(a, b, c)) {
return rules[i].action(a, b, c);
}
}
}

总结

圈复杂度低,代码不一定好,但圈复杂度很高,代码一定不好,所以圈复杂度才作为一个 oasis 的指标之一。圈复杂度还需要具体情况具体分析,其只作为重构的一个度量指标,作为决策的一个参考依据。

附

oasisV0.1 beta 版本目前已经完成大文件、重复代码块,圈复杂度、最佳实践、基础库落地、 npm 依赖库的安全分析功能,接入 0 成本目前已经上线欢迎大家体验、建议、吐槽。

SPA VUE应用实现页面缓存

发表于 2020-11-23

使用场景

在 SPA 应用中产品希望在用户在多个页面来回切换的时候,不要丢失查询的结果。

keep-alive

包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。和 相似, 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中。

实现方案

  • 整个页面缓存

采用在 router 的 meta 属性里增加一个 keepAlive 字段,然后在父组件或者根组件中,根据 keepAlive 字段的状态使用 标签,实现对 的缓存

1
2
3
4
<keep-alive>
<router-view v-if="$route.meta.keepAlive" />
</keep-alive>
<router-view v-if="!$route.meta.keepAlive" />
  • 动态缓存

使用 vuex 配合 exclude 和 include,通过 include 和 exclude 决定那些组件进行缓存。注意这里说的是组件,并且 cachedView 数组存放的是组件的名字,如下:

1
2
3
<keep-alive :include="$store.state.keepAlive.cachedView">
<router-view />
</keep-alive>

本项目的实现

  • 版本一:基于路由缓存

当前框架是嵌套的动态路由是无法动态缓存组件,并且如果使用方案一 meta 属性里增加一个 keepAlive 字段也是不支持的,只能通过维护一个缓存的 url 来实现:

1
2
3
4
5
6
7
8

// ISCACHE_ARRAY = ['url']
<keep-alive>
<router-view v-if="ISCACHE_ARRAY.indexOf($route.fullPath) !== -1" :key="$route.fullPath">
</router-view>
</keep-alive>
<router-view v-if="ISCACHE_ARRAY.indexOf($route.fullPath) === -1" :key="$route.fullPath">
</router-view>

这样是持久缓存了整个页面,问题也就出现当用户通过 tabviews 关闭页面然后从左侧打开菜单时是缓存的页面,这个不符合日常使用习惯,所以为了解决数据新鲜度的问题,看了文档发现在 keep-alive 的模式下多了 activated 这个生命周期函数,keep-alive 的声明周期执行:

  • 页面第一次进入,钩子的触发顺序
    created-> mounted-> activated,
    退出时触发 deactivated 当再次进入(前进或者后退)时,只触发 activated
  • 事件挂载的方法等,只执行一次的放在 mounted 中;组件每次进去执行的方法放在 activated 中

所以在 activated 触发查询请求就能保证数据的新鲜度。

但是使用后发现由于你切换 tab 是会请求数据,但是本项目的数据量有很大导致频繁 loading 。

  • 版本二:动态缓存

由于版本一需要频繁拉去数据导致此方案已不合适只能动态缓存。
由于框架的原因,框架里把路由和菜单用同一个数据模型采用的嵌套路由是无法缓存组件的,改造前路由如下:

所以需要改造确定:左侧菜单嵌套,路由展开不嵌套的方式:
app.vue

1
2
3
<div id="app">
<Layout></Layout>
</div>

layout.vue

1
2
3
4
<keep-alive :include="cachedViews">
<router-view :key="key">
</router-view>
</keep-alive>

通过上面的修改完成动态缓存实现。

其中 cachedViews 是通过监听路由动态增加删除维护要缓存的组件数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const state = {
cachedViews: [],
}
const mutations = {
ADD_VIEWS: (state, view) => {
if (state.cachedViews.includes(view.name)) return
state.cachedViews.push(view.name)
},
DEL_CACHED_VIEW: (state, view) => {
const index = state.cachedViews.indexOf(view.name)
index > -1 && state.cachedViews.splice(index, 1)
},
}
const actions = {
addCachedView({ commit }, view) {
commit('ADD_VIEWS', view)
},
deleteCachedView({ commit }, view) {
commit('DEL_CACHED_VIEW', view)
},
}
export default {
namespaced: true,
state,
mutations,
actions,
}

通过监听路由变化:

1
2
3
4
5
6
7
8
9
10
watch: {
'$route'(newRoute) {
const { name } = newRoute
console.log(name)
const cacheRout = this.ISCACHE_MAP[name] || []
cacheRout.map((item) => {
store.dispatch('cached/addCachedView', { name: item })
})
},
},

ISCACHE_MAP 是配置每个页面要缓存的那些组件

1
2
3
const ISCACHE_MAP = {
'/receipts-order': ['ReceiptsOrder'],
}

当通过 tabview 关闭页面时清除组件名称:

1
2
3
4
5
6
7
closeTag(newRoute) {
const { name } = newRoute
const cacheRout = this.ISCACHE_MAP[name] || []
cacheRout.map((item) => {
store.dispatch('cached/deleteCachedView', { name: item })
})
},

关于展开路由是在动态路由添加时展开:

1
2
3
4
5
6
7
8
9
10
export const flatten = (data) => {
return data.reduce((arr, v) => {
if (Array.isArray(v.children) && v.children.length !== 0) {
return arr.concat(flatten(v.children))
} else {
return arr.concat([v])
}
}, [])
}
routes.addRoutes(flatten(routeList))

改造后的路由:

到此完美解决了当前框架的设计问题

总结

上述的常规方案网上一大把,如何将它们合理的使用在项目中,这才是我们需要多去思考的。

vue-cli 配置打包分析与实践

发表于 2020-10-02

Vue CLI 是一个基于 Vue.js 进行快速开发的完整系统,它确保了各种构建工具能够基于智能的默认配置即可平稳衔接,这样你可以专注在撰写应用上,而不必花好几天去纠结配置的问题。与此同时,它也为每个工具提供了调整配置的灵活性,无需 eject。

如何看 webpack 最终配置

1
2
3
4
"scripts": {
"webpack_output_dynamic": "vue inspect --mode=dynamic > output.js",
"webpack_output_production": "vue inspect --mode=production > output.js"
}

如何统计打包的时间

speed-measure-webpack-plugin 是一个专门测试 webpack 构建速度的工具,可以在终端列出所有 Loader 和 Plugin 的耗时。

1
2
3
4
5
6
7
8
9
10
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')
const smp = new SpeedMeasurePlugin()

module.exports = {
configureWebpack: smp.wrap({
output: {
...
}
})
}

webpack 打包结束后,输出的时间统计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 SMP  ⏱
General output time took 51.66 secs

SMP ⏱ Plugins
DllReferencePlugin took 0.55 secs

SMP ⏱ Loaders
mini-css-extract-plugin, and
css-loader, and
sass-loader took 14.16 secs
module count = 1
css-loader, and
sass-loader took 14.15 secs
module count = 1

配置 vue.config.js

添加别名 resolve.alias

目前的配置

1
2
3
4
5
6
7
8
configureWebpack: {
resolve: {
alias: {
'@': '/Users/admin/Documents/code/gondor/src',
vue$: 'vue/dist/vue.runtime.esm.js'
},
},
}

扩展名配置 resolve.extensions

extensions 的配置原则是频率出现高的排前面,数组长度尽量小,以下是 vue-cli 中的默认配置 [‘.mjs’, ‘.js’, ‘.jsx’, ‘.vue’, ‘.json’, ‘.wasm’]

我们的文件没有 .mjs、.jsx,所以在 vue.config.js 中我这样配置会更好:

1
2
3
chainWebpack: (config) => {
config.resolve.extensions.store = new Set([".js", ".vue", ".json", ".wasm"]);
};

确定搜索目录 resolve.modules

当搜索模块的时候告诉 webpack 确定的目录文件,这样可以避免无用的检索。webpack 默认的配置是相对路径,检索当前目录的下的 node_modules,如果没有则再会去根目录下的 node_modules,直到没有最后抛错。

1
2
3
4
5
6
chainWebpack: (config) => {
config.resolve.modules.store = new Set([
path.resolve(__dirname, "node_modules"),
"node_modules",
]);
};

上面也是默认的

忽略非模块化 Library module.noParse

忽略那些文件中不含有 import, require, define 的调用,或任何其他导入机制的 library。忽略大型的 library 可以提高构建性能。下面是默认配置,请根据项目需要修改

1
2
3
4
5
configureWebpack: {
module: {
noParse: /^(vue|vue-router|vuex|vuex-router-sync)$/,
},
}

动态链接库 DllPlugin

DllPlugin 和 DllReferencePlugin 实现了拆分 bundles,可以将一些不常被更新的第三方依赖预编译到一个或多个 manifest.json 中,做到一次打包就可以重复引用的效果。比如说我们 vue 全家桶、axios 等依赖,只要版本不升级就没必要多次打包,我们可以让这些依赖与业务代码分开打包。
使用方式大致分三步:

  • 利用 DllPlugin 配置预先打包出 _.manifest.json 文件。
  • 在 webpack 主配置中设置 DllReferencePlugin,告诉 webpack 使用了哪些动态链接库。
  • 在页面文件中加载所有的动态链接库。
    webpack.dll.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const path = require("path");
const DllPlugin = require("webpack/lib/DllPlugin");

module.exports = {
entry: {
// 将 Vue 相关模块打包到一个单独的动态链接库中
vue: ["vue", "vue-router", "vuex"],
// 将其他的一些不变动的第三方库打包到一个动态链接库中
polyfill: ["element-ui"],
},
output: {
// 输出动态链接库的文件名称,[name] 代表当前动态链接库的名称
// name 来自 entry 对象中的 key
filename: "[name].dll.js",
// 输出的文件都放到 dist 目录下
path: path.resolve(__dirname, "dist"),
// 动态链接库全局名称,例如 vue 就是 _dll_vue
library: "dll[name]",
},
plugins: [
new DllPlugin({
// 动态链接库的全局变量名称需要和 output.library 中的名称一致
// 代表 manifest.json 文件中的 name 字段的值
// 例如 vue.manifest.json 中的 'name': '_dll_vue'
name: "dll[name]",
// 动态链接库文件输出的文件名,例如 vue.manifest.json
path: path.join(__dirname, "dll", "[name].manifest.json"),
}),
],
};

vue.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
configureWebpack: {
plugins: [
// 告诉 webpack 使用的动态链接库
new DllReferencePlugin({
manifest: require('./dll/vue.manifest.json'),
}),
new DllReferencePlugin({
manifest: require('./dll/polyfill.manifest.json'),
}),
],
}
···
在 package.json 中增加
```js
"scripts": {
"dll": "webpack --config webpack.dll.config.js"
}

hard-source-webpack-plugin

webpack4 抛弃了使用 dll,hard-source-webpack-plugin 就是一个很好的替代者,它可为模块提供中间缓存步骤。第二次构建将明显更快。

外部扩展(external)

externals 配置选项提供了「从输出的 bundle 中排除依赖」的方法,也是个人最提倡的采用 script 引入 CDN 资源

分环境压缩代码

在 development 环境时,我们可以关闭压缩来提升打包速度

1
2
3
4
5
6
7
8
9
chainWebpack: (config) => {
if (process.env.VUE_APP_ENV === "dynamic") {
// development
// js is not compressed
config.optimization.minimize(false);
// css is not compressed
config.plugins.delete("optimize-css");
}
};

打包后的代码质量

使用 webpack-bundle-analyzer 分析工具,在启动打包完成后,会在 8888 端口显示一个交互式可视化 bundle treemap。

其他

升级打包机配置

视频编码

发表于 2020-09-03

视频编解码对于大多数前端工程师来说是一个比较少涉足的领域,业内主流的处理是通过 FFmpeg 做简单的转码和简单的优化,其中涉及到流媒体技术中的文本、图形、图像、音频和视频多种理论知识。

视频编码

我们常说的视频编码是指通过特定的压缩技术,将某个视频格式的文件转换成另一种视频格式文件的方式。常见的视频编码 H.265、H.264 等。

H.264

H.264,同时也是 MPEG-4 第十部分,是由视频编解码技术的组织国际电联(ITU-T)视频编码专家组(VCEG)和 ISO/IEC 动态图像专家组(MPEG)联合组成的联合视频组(JVT)提出的高度压缩数字视频编解码器标准。这个标准通常被称之为 H.264/AVC–用来明确的说明它两方面的开发者。因此,不论是 MPEG-4 AVC、MPEG-4 Part 10,还是 ISO/IEC 14496-10,都是指 H.264。

H.265

H.265(HEVC,High Efficiency Video Coding,高效率视频编码),是新一代视频编码技术。它围绕现有视频编码标准 H.264,保留原来的某些技术,使用新技术对某些方面进行改进优化,如码流、编码质量、延时等,提高压缩效率、增强鲁棒性和错误恢复能力、减少实时的时延、降低复杂度等。

H.265 与 H.264

H.265 与 H.264 的区别,就在于 H.265 压缩效率更高,传输码率更低,视频画质更优,实现监控视频传输带宽减半、存储减半、成本减半,带给人们更优质的体验。
但是因为 H.265 专利及硬解支持情况不完善的原因,主流现代浏览器均不兼容 H.265 编码的视频播放。想要在浏览器端播放 H.265 原生的视频标签是没有办法支持的。浏览器兼容性:
h265

解码支持

硬解码

Apple 公司的最新的操作系统版本(Mac Hight Sierra 和 iOS 11)迎来了 HEVC (高效视频编码,也称 H.265) 这一新的行业标准,也是上面的 safari 13 以上版本支持的原因。

Web 软解码

主要用到了 WebAssembly 及 WebWorker 的支持,通过解码器编译为 wasm 库,wasm 文件是以二进制形式存在的,这也是很多移动平台采用的方案。该技术方案已经能在大部分机器的主流浏览器上流畅的播放 720P 的高清直播流,但是在性能稍差的机器上还是存在高清视频解码性能不能满足流畅播放的风险,针对 WebAssembly 达到 native 速度的目标还有一定距离,尤其是汇编并行计算的支持。

遇到的问题

有了上面的理论基础我们来看看问题:

测试在上传视频时有的视频无法获得宽高默认是 0

上传组件是用的 @du/upload ,通过代码研究发现 @du/upload 获取宽高是基于 loadedmetadata

1
2
3
4
5
6
7
8
9
10
video.addEventListener(
"loadedmetadata",
function () {
return resolve({
width: this.videoWidth,
height: this.videoHeight,
});
},
false
);

通过分析异常视频:

出现错误的视频编码是 HEVC 也就是 H.265,因为当前浏览器不支持解析视频所以拿不到视频资源的数据。至于为什么通过飞书、微信之类的聊天工具传输过就可以是因为在传输过程中通讯工具已经默认转码成 AVC(H.264) 格式。最快解决方案目前做的是提醒用户转换视频。

  • 判断视频编码格式
    可以引用 mediainfo 或者 libde265 解析视频但是成本太高 mediainfo 这个库 7M 多。
  • 判断平台是否支持播放
1
2
3
4
5
6
7
8
9
var supportHEVC = function(video) {
if (typeof video.canPlayType == ‘ function’) {
var playable = elem.canPlayType('video/mp4; codecs="hevc"');
if ((playable.toLowerCase() == 'maybe') || (playable.toLowerCase() == 'probably')) {
return true;
}
}
return false;
};
  • web 播放 H.265 视频
    libde265.js 是一个通过 JS 来解码 H.265 视频的库,它通过将 视频的 frame data 转化为 rgba 像素,然后绘制到 Canvas 上。

未来展望

在有限带宽下 H.265 能传输更高质量的网络视频,更低的带宽可以更好的降低存储及传输成本,H.265 将为未来基于短视频及直播领域更多更复杂好玩的互动玩法做铺垫。以后借助 WebAssembly 的跨平台优势,web 端可以将传统的其他语言的开源框架如图形相关开源库 OpenGL、SDL 等的能力移植到浏览器上来。借助性能上的优势也可以将传统的图像、3D 等运算能力要求较高的应用扩展到浏览器端,前端将会后更多的玩法,尤其是直播的兴起,所以在 WebAssembly 技术上需要提前做好积累。

参考链接

1.花椒前端基于 WebAssembly 的 H.265 播放器研发
2.ffmpeg
3.webassembly

祖传项目(vue)接入单元测试

发表于 2020-07-10

最近在做财务项目,用着祖传的项目脚手架,秉承着能升级就升级,该用的就用了原则增加了单元测试。

Vue-Test-Utils 和 Jest

Vue-Test-Utils 是 Vue.js 官方的单元测试实用工具库,它提供了一系列的 API 来使得我们可以很便捷的去写 Vue 应用中的单元测试。

Jest 是一个由 Facebook 开发的测试框架,他是是功能最全的测试运行器。它所需的配置是最少的,默认安装了 JSDOM,内置断言且命令行的用户体验非常好。

本文主要利用 Vue-Test-Utils + Jest 结合

安装依赖

插件模式

直接在项目中添加一个 unit-jest 插件

1
vue add @vue/unit-jest

安装完成后配置自动完成

新建项目

对于新建项目通过脚手架 vue-cli 来新建项目,选择了 Unit Testing 单元测试,选择的是 Jest 作为测试运行器,项目创建好后,会自动配置好单元测试需要的环境。

老项目接入

安装依赖包
1
yarn add --dev @vue/test-utils jest vue-jest @vue/babel-preset-app babel-core@^7.0.0-bridge.0
  • @vue/test-utils
  • jest
  • vue-jest
  • @vue/babel-preset-app
配置 Jest

在根目录新建一个文件 jest.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
moduleFileExtensions: ["js", "vue"],
transform: {
"^.+\\.vue$": "<rootDir>/node_modules/vue-jest",
"^.+\\.js$": "<rootDir>/node_modules/babel-jest",
},
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1",
},
snapshotSerializers: ["jest-serializer-vue"],
testMatch: ["**/__tests__/**/*.spec.js"],
transformIgnorePatterns: ["<rootDir>/node_modules/"],
};

置项说明:

  • moduleFileExtensions 告诉 Jest 需要匹配的文件后缀
  • transform 匹配到 .vue 文件的时候用 vue-jest 处理, 匹配到 .js 文件的时候用 babel-jest 处理
  • moduleNameMapper 处理 webpack 的别名,比如:将 @ 表示 /src 目录
  • snapshotSerializers 将保存的快照测试结果进行序列化,使得其更美观
  • testMatch 匹配哪些文件进行测试
  • transformIgnorePatterns 不进行匹配的目录

本地文件 babel.config.js 新增

1
2
3
4
5
6
7
module.exports = {
env: {
test: {
presets: [["@babel/env", { targets: { node: "current" } }]],
},
},
};

顺利的话已经完成了

过程遇到的一些问题

  • babel-preset-app

vue-cli 这边用的 babel 预设集合是自己封装的 @vue/app,他是封装了 @babel/preset-env 且配置 useBuiltIns: ‘usage’

而且最新版本是依赖 core-js@3 的所以当安装最新本版本会引起下面问题

  • core-js@3 的更新引起的连锁反应

@babel/polyfill 无法提供 core-js@2 向 core-js@3 过渡 Babel > 7.4.0 不在使用
@babel/preset-env 也因 core-js@3 的原因,需要配置 corejs 参数去指定使用的 corejs 版本

如果使用 corejs@3 根据 useBuiltIns 配置参数不同,需要做转译 ES 的新语法 + 新 API 升级

关于 Babel

因为处理上面 babel 的问题所以这两天研究了一些,简单说下

  • @babel/preset-env

preset-env 是 ES 语法插件的合集,官方已经不再推荐使用 preset-201x 之类的包,该包可以通过配置自动兼容代码,包括自动引入 polyfill 垫片处理新的 API

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"presets": [
[ "@babel/preset-env", {
"targets": {},
"useBuiltIns": false,
"corejs": false
}]
]
}
// useBuiltIns的不同可以分为三种,即 entry(引入了所有的es扩展包), usage(按需) 和 false(只做了语法转换)。
// corejs: false 其实等同于使用 @babel/polyfill 时的 useBuiltIns: false,只对ES语法进行了转换。
// corejs:2 等同于 Babel 6时的 polyfill: true
// corejs: 3 是在 corejs: 2的基础上进而解决了之前无法实例方法的窘况,同时也保持了不污染全局空间

preset-env 在不引入 polyfill 时无法处理,实例的扩展 Array.prototype.includes 等,以及很多内置函数如:Promise、Symbol。而为了解决这样的问题,我们通常有两种方法:使用 Polyfill 或 Babel-runtime 进行功能填充。

  • @babel/polyfill
1
import @babel/polyfill

等同于

1
2
import 'core-js/stable';
import 'regenerator-runtime/runtime';

Babel > 7.4.0 之前,通常我们会安装 @babel-polyfill,而 7.4.0 之后我们需要安装 core-js 替代 babel-polyfill

  • @babel/runtime

能实现按需加载,沙箱环境,公用函数的统一抽象

总结

不可否认 vue-cli 作为非常优秀的脚手架,它可以实现在终端内输入一行指令就能生成模板,非常便利。我们在日常工作中享受工具带给我们的便利同时是不是也应该多花点时间了解脚手架前端工程化方面的内容,这样在日后项目优化,升级会有很大的便利。

CSS 也可以这么写

发表于 2020-06-02

前言

这篇文章想介绍一下在项目中积累实践并结合 Atomic CSS 的思想的另一个 css 解决思路但并不适用所有。大家可能听说过各种 CSS 解决方案,例如 BEM,OOCSS 等等,最近几年体验下来可以说到目前来说还没让人完全满意的 CSS 框架的样式设计,目前市面上的主流 css 框架是提供完整的设计输出,是组件级别,你只要把类名复制进去,或者通过框架提供的变量进行自定义,在总的来说意义不大。

AtomicCSS

Atomic CSS 的思想,采用类似乐高搭积木的方法提供丰富原子,接入到项目的话它就是设计规范,你只需要从里面把这些元素挑出来进行组合,写好设计规范,接下来就是搭积木。
我很不喜欢起应用在 CSS 做用上的命名, 为了给一个按钮美化样式,需要给 HTML 元素命名,然后到样式文件写一堆无聊又重复的 CSS。
在以前我们在编写 css 方式总结了有以下几种:

语义化(结构化类名)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<style>
.nav{
height: 34px;
display: flex;
justify-content: space-between;
}
.nav-left{
font-size:14px;
}
.nav-center{
font-size:16px;
}
.nav-right-login{
font-size:18px;
}
</style>
<nav class="nav">
<a class="nav-left" href="#">Home</a>
<a class="nav-center" href="#">About</a>
<span class="nav-right">
<a class="nav-right-login" href="#">Login</a>
</span>
</nav>

这种模式主要受 BEM 系统方法命名的影响,在 jQuery 独霸天下的年代很适用因为我们要操作 DOM 。

复用组合

1
2
3
4
5
6
7
<nav class="uilist">
<a class="uilist-item" href="#">Home</a>
<a class="uilist-item" href="#">About</a>
<span class="uilist-item">
<a class="btn" href="#">Login</a>
</span>
</nav>

当我们开始专注于创建可重用的类时,就会发现组件执行的次数越多,或者组件越具体,则重用就越困难。

组件修饰符

1
2
<button class="btn">Default</button>
<button class="btn btn-primary">Login</button>

原子化

最后就是原子化也就是 AtomiCSS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<style>
.df{display:flex;}
.h34{height:34px;}
.jcsb{justify-content: space-between;}
.fs14{ font-size:14px; }
.fs14{ font-size:16px; }
.fs14{ font-size:18px; }
</style>
<nav class="df h34 jcsb">
<a class="fs14" href="#">Home</a>
<a class="fs16" href="#">About</a>
<span>
<a class="fs18" href="#">Login</a>
</span>
</nav>

看这种方法很容易认为这就像在 HTML 元素上添加样式标签并添加所需的任何属性 inline css 一中简写方式,但是根据我的经验,这是非常不同的。使用内联样式时,您选择的值没有限制,更不用说复用了,随着 react vue 的流程在组件化开发浪潮中才真正变得可行。

  • Text sizes, colors, and weights
  • Border colors, widths, and positions
  • Background colors
  • Flexbox
  • Padding and margin

Tailwind CSS

1
2
3
4
5
<button
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Button
</button>

Tailwind CSS 可以看作是 utility-first CSS 的一种极致抽象: 所有的 CSS 类都有且只有一个单一的,独特的 CSS 规则。Tailwind CSS 的目的不是直接把设计过的东西给你,而是帮助你更快地实现你的设计。原子化的通用的 class 我们可以称为 utility, Tailwind CSS 提供的就是一些 utility。那么和 Tailwind 相比,是否需要手写 Atomic CSS ?
Tailwind CSS 和 手写 Atomic CSS 相比 :
优点:

  • 可以方便地做到响应式设计
  • 丰富的预设,如字体大小,预设颜色
    缺点:
  • Tailwind CSS 有一定的学习曲线
  • 丰富的预设,也就带来繁重的内容,实际项目中配合规范之后并不需要这么多预设
  • 可能很多 (React, Vue) 组件需要自己动手实现毕竟只是个 UI

总结

我认为原子化的 CSS 在基于组件化思维的 react 或者 vue 项目中使用是未来的大趋势,虽然感觉是在写行内样式。经过实践下来对与 Tailwind CSS 来说,它有一定的学习曲线且上手成本相对较高,且对与一个有追求的项目来说 Tailwind CSS 提供的内容过于丰富对与 UI 层影响大。从开发体验来说自定义的 Atomic CSS 更适合。

最后

前端时间看到一篇文章,Facebook 的改版也是用了原子化的思想,唯一的区别的他们是在工程化层面的实现,而且带来的收益正如 Atomic CSS 所期望的那样,Twitter 和 Facebook 的 CSS 都已经大幅减少了,他们都遵循图中的曲线。

微码

发表于 2020-05-04
  • XSS 对应
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function escapeHtml(value) {
if (typeof value !== "string") {
return value;
}
return value.replace(/[&<>`"'\/]/g, function (result) {
return {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
"`": "&#x60;",
'"': "&quot;",
"'": "&#x27;",
"/": "&#x2f;",
}[result];
});
}
  • 数字千分位
1
2
3
const reg = /(?=(\B\d{3})+$)/g;
(123456789).replace(reg, ",");
(123456789).toLocaleString("en-US");
  • url 参数
1
2
3
4
5
6
7
8
9
10
11
12
13
function params(url, key) {
const obj = {};
const reg = /(\w_)=(\w+)/g;
while (reg.exec(url)) {
obj[RegExp.$1]
? (obj[RegExp.$1] = [...obj[RegExp.$1], RegExp.$2])
: (obj[RegExp.$1] = RegExp.$2);
}
if (key) {
return obj[key];
}
return obj;
}
  • 时间转换
1
2
3
const date = new Date();
date.toLocaleString("zh", { hour12: true }); //2020/4/4 下午6:57:36
date.toLocaleString("zh", { hour12: false }); //2020/4/4 18:57:36

webpack 动态导入

发表于 2020-03-28

加载 chunks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
__webpack_require__.e = function requireEnsure(chunkId) {
// 1、缓存查找 从缓存 installedChunks 中查找是否有缓存模块,如果缓存标识为0,则表示模块已加载过,直接返回 promise;
var installedChunkData = installedChunks[chunkId];
if (installedChunkData === 0) {
return new Promise(function(resolve) {
resolve();
});
}
// 2、 如果缓存为数组,表示缓存正在加载中,则返回缓存的 promise 对象
if (installedChunkData) {
return installedChunkData[2];
}
// 3、没有缓存,则创建一个 promise,并将 promise和resolve、reject 缓存在 installedChunks 中
var promise = new Promise(function(resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
installedChunkData[2] = promise;
// 4、加载模块 发送 JSONP 请求
var head = document.getElementsByTagName("head")[0];
var script = document.createElement("script");
script.type = "text/javascript";
script.charset = "utf-8";
script.async = true;
script.timeout = 120000;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
script.src =
__webpack_require__.p +
"" +
({ "0": "foo" }[chunkId] || chunkId) +
".bundle.js";
// 5、异常处理 添加 script 标签 onload、onerror 事件,如果超时或者模块加载失败,则会调用 reject 返回模块加载失败异常
var timeout = setTimeout(onScriptComplete, 120000);
script.onerror = script.onload = onScriptComplete;
function onScriptComplete() {
// avoid mem leaks in IE.
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
if (chunk !== 0) {
if (chunk) {
chunk[1](new Error("Loading chunk " + chunkId + " failed."));
}
installedChunks[chunkId] = undefined;
}
}
head.appendChild(script);
// 6、返回 promise
return promise;
};

执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
window["webpackJsonp"] = function webpackJsonpCallback(
chunkIds,
moreModules,
executeModules
) {
var moduleId,
chunkId,
i = 0,
resolves = [],
result;
// 7、根据 chunkId 收集模块 resolve
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if (installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]);
}
installedChunks[chunkId] = 0;
}
// 8、将 chunks 模块放到 modules
for (moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
if (parentJsonpFunction)
parentJsonpFunction(chunkIds, moreModules, executeModules);
// 9、resolve import 执行回调
while (resolves.length) {
resolves.shift()();
}
};

总的流程

  • __webpack_require__.e 开始加载异步 chunks 将异步回调放入 installedChunks 发送 JSONP 请求
  • window["webpackJsonp"] 根据 chunkIds 收集 resolve ,将 chunks 模块 放到 modules 中,执行 resolve ,加载 model

useEffect 引起的无限循环

发表于 2019-11-28

自定义 hook

我们在使用 React Hock 时 为了逻辑复用经常来封装⼀个请求分⻚列表的⾃定义 hooks,写⼀个 hook 的核⼼就是处理:输⼊(使⽤ hook 时的变量)、输出(使⽤ hook 获取的值),以及哪些值可以被维护在 hook 内部,使⽤者可以⽆感知,⽽从输⼊到输出的过程,即可复⽤的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
export function useFetchList(url, params) {
const [loading, setLoading] = useState(false);
const [pageNo, setPageNo] = useState(1);
const [data, setData] = useState({
totalCount: 0,
end: false,
records: []
});
useEffect(() => {
setLoading(true);
ajax
.get(url, {
...params,
pageNo
})
.then(({ result }) => {
setData(result);
})
.catch(handleError)
.then(() => {
setLoading(false);
});
}, [pageNo, params, url]);
return [{ loading, data }, setPageNo];
}

但是!!上述代码会引发⼀个问题——⽆限循环

引发原因:useEffect 的依赖对⽐,⽤的是 Object.is()
也就是说,上⾯的代码中,使⽤ { bookId } 作为参数时,每次渲染都是⼀个新的参数,会导致
useFetchList 不断去取数据,⽽取到数据后,⼜会引发函数组件重新渲染,导致 { bookId } 更新。

  • 解决⽅法 1:调⽤ hook 前,使⽤ useState 缓存 { bookId }
  • 解决⽅法 2:调⽤ hook 前,使⽤ useMemo 缓存 { bookId }
  • 解决⽅法 3:hook 内部判断 params 是否真的更新了
  • 解决⽅法 4:使⽤ useDeepCompareEffect(第三⽅库)替换 useEffect,但是谨慎使⽤,滥⽤可能会导致性能问题。

这⾥针对⽅法 3 做了实现,因为这是使⽤⽅可以最轻松的使⽤⽅法。

1
2
3
4
5
6
7
8
9
10
// 通过 useRef(⻅官⽅ Hook API 说明) 缓存值的引⽤,每次传⼊新值时,通过深对⽐进⾏⽐较,如果没
function useMemoizedValue(value) {
const cache = useRef(null);
const previousValue = cache.current;
if (_.isEqual(previousValue, value)) {
return previousValue;
}
cache.current = value;
return value;
}

这个 hook 终于可以被正常调⽤了

视频无损压缩工具

发表于 2019-10-13

Handbrake

Handbrake 是开源的视频转换工具,从几乎任何格式转换视频和支持多平台(Windows,Mac 和 Linux,而且内置丰富的设备预设通过选择针对您设备优化的配置文件。下载地址

安装完成后,如上图就可以简单使用了。
由于内置丰富的设备预设,在 web 开发中一般选择内置 web 预设。

强烈推荐使用!!

更多功能:

  • 标题/章节和范围选择
  • 批量扫描和编码排队
  • 章标记
  • 字幕(VobSub,隐藏式字幕 CEA-608,SSA,SRT)
  • 恒定质量或平均比特率视频编码
  • 支持 VFR 和 CFR
  • 视频滤镜:逐行扫描,去梳,去噪,检测,去块,灰度,裁剪和缩放
  • 实时静态和视频预览

其他在线压缩网站

  • Online UniConverter
    https://www.media.io/video-compressor.html

  • youcompress
    https://www.youcompress.com/videos/

上面这两个是挑选出来的免费的在线压缩工具网站

123…7
lennonover

lennonover

一丿口石砳磊

70 日志
28 标签
© 2022 lennonover
由 Hexo 强力驱动
主题 - NexT.Muse