注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

前端开发的积木理论——像搭积木一样做前端开发

1 概述用户界面是由一系列组件组合而成,组件将数据和交互封装在内,仅保留必要的接口与其他组件进行通信。在前端开发中,组件就像一个一个的小积木块,我们用这些积木块拼出一个一个页面,这些页面组成了一个完整的为用户提供价值的业务。相信大部分前端工程师都使用过组件库,...
继续阅读 »



1 概述

用户界面是由一系列组件组合而成,组件将数据和交互封装在内,仅保留必要的接口与其他组件进行通信。

在前端开发中,组件就像一个一个的小积木块,我们用这些积木块拼出一个一个页面,这些页面组成了一个完整的为用户提供价值的业务。

相信大部分前端工程师都使用过组件库,比如Ant DesignElementUI,都是我们非常熟悉的组件库,以及我们团队做的DevUI组件库。

组件库就像一个工具箱,里面包含了各式各样奇形怪状、功能各异的组件,我们直接拿这些组件小积木来拼页面,非常方便。

2 界面的基本元素

从抽象的角度来看,任何用户界面都是由组件数据交互组成的。

2.1 组件

组件是一个具有一定功能的独立单元,不同的组件可以组合在一起,构成功能更强大的组件.

组件是网页的器官。

组件的概念要和HTML标签的概念区分开来,HTML标签是网页的基本单元,而组件是基于HTML标签的、包含特定功能的独立单元,可以简单理解为组件是HTML标签的一个超集。

组件内部封装的是数据和交互,对外暴露必要的接口,以与其他组件进行通信。

2.2 数据

用户界面中包含很多数据,有不变的静态数据,也有随时间和交互改变的动态数据,它们大多来自于后台数据库。

组件内部包含数据,组件之间传递的也是数据。

数据是网页的核心。

在前端开发中,数据主要以JSON格式进行存储和传递。

2.3 交互

交互是用户的操作,主要通过鼠标和键盘等计算机外设触发,点击一次按钮、在文本框中输入一些字符、按下回车键等都是交互。

在网页中,所有的交互都是通过事件的方式进行响应的。

交互是网页的灵魂,不能进行交互的网页就像干涸的河流,了无生气。

3 组件的特性

一个设计良好的组件应该包含以下特性:

  • 复用性(Reuseability)

  • 组合性(Composability)

  • 扩展性(Scalability)

3.1 复用性

组件作为一个独立的单元,除了自身的特定功能之外,不应该包含任何业务相关的数据。

组件应该能够复用到任何业务中,只要该业务需要用到组件所包含的功能。

组件应该是资源独立的,以增强组件的复用能力。

3.2 组合性

组件与组件之间可以像积木一样进行组合,组合之后的组件拥有子组件的所有功能,变得更强大。

组合的方式可以是包裹别的组件,也可以是作为参数传入别的组件中。

3.3 扩展性

可以基于现有的组件进行扩展,开发功能更加定制化的组件,以满足业务需求。

组件的可扩展能力依赖于接口的设计,接口要尽可能的灵活,以应对不断变化的需求。

4 组件间通信

4.1 从外向内通信

通过props将数据传递到组件内部,以供组件自身或其子组件使用。

props是不可变的:

  • 这意味着我们无法在组件内部修改props的原始值

  • 也意味着只有外部传入了props,才能在组件内部获取和使用它

4.2 从内向外通信

可以通过两种方式将组件内部的数据传递到组件外:

  • 通过ref属性(组件实例的引用),通过组件的ref属性,可以获取到组件实例的引用,进而获取组件内部的数据和方法

  • 通过事件回调(比如:click),通过事件回调的方式,可以通过props将一个回调函数传递到组件内部,并将组件内部的数据通过回调传递到外部

4.3 双向通信

可以通过全局context的方式进行双向通信。

父组件声明context对象,那么其下所有的子组件,不管嵌套多深,都可以使用该对象的数据,并且可以通过回调函数的方式将子组件的数据传递出来供父组件使用。

4.4 多端通信

通过事件订阅的方式可以实现多个组件之间互相通信。

通过自定义事件(Custom Event),任何组件都可以与其他组件进行通讯,采用的是发布/订阅模式,通过向事件对象上添加监听和触发事件来实现组件间通信。

5 案例

接下来通过广告详情页面的开发来演示如何用积木理论来构建网页。

设计图如下:

5.1 第一步:拆积木

将设计图拆分成有层次的积木结构。

5.1.1 顶层积木

最顶层拆分成四个大积木:

  • Header(头部组件)

  • ChartBlock(图表块组件)

  • DetailBlock(详情块组件)

  • TableBlock(表格块组件)

5.1.2 中间层积木

  • 每个大积木又可以拆分成若干小积木

  • 中间层有些是不可拆分的原子积木,比如:ButtonCheckbox

  • 有些是由原子积木组合而成的复合积木,比如:DateRangePickerTable

层次结构如下:

  • Header

    • Breadcrumb

    • Button

    • DateRangePicker

  • ChartBlock

    • Tabs

    • LineChart

    • BarChart

  • DetailBlock

    • Button

    • List

  • TableBlock

    • Checkbox

    • Select

    • Button

    • Table

5.1.3 底层积木

最底层的积木都是不可拆分的原子积木。

5.2 第二步:造积木

已经将积木的层次结构设计出来,接下来就要考虑每个层次的积木怎么制造的问题。

这一块后面会专门写一个系列文章给大家分享如何自己制造组件。

5.3 第三步:搭积木

5.3.1 顶层积木

对应的代码:

<div class="ad-detail-container">
   <Header />
   <div class="content">
       <div class="chart-detail-block">
           <ChartBlock />
           <DetailBlock />
       </div>
       <TableBlock />
   </div>
</div>

其中的<div>标签是为了布局方便加入的。

5.3.2 中间层积木

Header对应的代码:

<div class="header">
 <div class="breadcrumb-area">
   <div class="breadcrumb-current">gary audio</div>
     <div class="breadcrumb-from">
      From:
       <d-breadcrumb class="breadcrumb" separator="">
         <d-breadcrumb-item href="http://www.qq.com">Campaign List</d-breadcrumb-item>
         <span class="breadcrumb-seprator">> </span>
         <d-breadcrumb-item href="http://www.qq.com">gary audio test</d-breadcrumb-item>
       </d-breadcrumb>
     </div>
   </div>
 </div>
 <div class="operation-area">
   <d-button icon="mail" class="flat" (click)="sendReportEmail()">Send Report Email</d-button>
   <d-date-range-picker (change)="changeDate()" />
 </div>
</div>

需要注意的是:为了方便阐述积木理论的核心思想,这里的原子组件大多都是已经造好的(使用DevUI组件库),也可以选择自己制造,后面会专门写一个系列文章给大家分享如何自己制造组件。

ChartBlock对应的代码:

<div class="chart-block">
   <d-tabs [defaultActiveKey]="1">
       <d-tab-item tab="Ad performance" [key]="1">
           <d-line-chart></d-line-chart>
       </d-tab-item>
       <d-tab-item tab="Audience" [key]="2">
           <d-bar-chart></d-bar-chart>
       </d-tab-item>
   </d-tabs>
</div>

DetailBlock对应的代码:

<div class="detail-block">
   <div class="detail-header">
       <div class="detail-title">Ad detail</div>
       <div class="detail-operation">
           <d-button icon="edit" class="flat" (click)="edit()">Edit</d-button>
           <d-button icon="delete" class="flat" (click)="delete()">Delete</d-button>
           <d-button icon="eye" class="flat" (click)="preview">Preview</d-button>
       </div>
   </div>
   <d-list [data]="adDetail" [config]="detailConfig"></d-list>
</div>

TableBlock对应的代码:

<div class="table-block">
   <div class="table-operation-bar">
       <d-checkbox (change)="changeDelivery()">Has delivery</Checkbox>
       <d-select class="select-table-column" [defaultValue]="1"
           (change)="select()">
           <d-select-option value="1">Performance</d-select-option>
           <d-select-option value="2">Customize</d-select-option>
       </Select>
       <d-button icon="export" (click)="exportData()">Export Data</d-button>
   </div>
   <d-table [dataSource]="adsList" [columns]="columns"></d-table>
</div>

由于篇幅原因,这个案例并没有包含交互的部分,不过基本能够阐述清楚积木理论的核心思想。

作者:DevUI团队
来源:https://juejin.cn/post/7047503485054484516

收起阅读 »

深入理解 redux 数据流和异步过程管理

前端框架的数据流前端框架实现了数据驱动视图变化的功能,我们用 template 或者 jsx 描述好了数据和视图的绑定关系,然后就只需要关心数据的管理了。数据在组件和组件之间、组件和全局 store 之间传递,叫做前端框架的数据流。一般来说,除了某部分状态数据...
继续阅读 »


前端框架的数据流

前端框架实现了数据驱动视图变化的功能,我们用 template 或者 jsx 描述好了数据和视图的绑定关系,然后就只需要关心数据的管理了。

数据在组件和组件之间、组件和全局 store 之间传递,叫做前端框架的数据流。

一般来说,除了某部分状态数据是只有某个组件关心的,我们会把状态数据放在组件内以外,业务数据、多个组件关心的状态数据都会放在 store 里面。组件从 store 中取数据,当交互的时候去通知 store 改变对应的数据。

这个 store 不一定是 redux、mobox 这些第三方库,其实 react 内置的 context 也可以作为 store。但是 context 做为 store 有一个问题,任何组件都能从 context 中取出数据来修改,那么当排查问题的时候就特别困难,因为并不知道是哪个组件把数据改坏的,也就是数据流不清晰。

正是因为这个原因,我们几乎见不到用 context 作为 store,基本都是搭配一个 redux。

所以为什么 redux 好呢?第一个原因就是数据流清晰,改变数据有统一的入口。

组件里都是通过 dispatch 一个 action 来触发 store 的修改,而且修改的逻辑都是在 reducer 里面,组件再监听 store 的数据变化,从中取出最新的数据。

这样数据流动是单向的,清晰的,很容易管理。

这就像为什么我们在公司里想要什么权限都要走审批流,而不是直接找某人,一样的道理。集中管理流程比较清晰,而且还可以追溯。

异步过程的管理

很多情况下改变 store 数据都是一个异步的过程,比如等待网络请求返回数据、定时改变数据、等待某个事件来改变数据等,那这些异步过程的代码放在哪里呢?

组件?

放在组件里是可以,但是异步过程怎么跨组件复用?多个异步过程之间怎么做串行、并行等控制?

所以当异步过程比较多,而且异步过程与异步过程之间也不独立,有串行、并行、甚至更复杂的关系的时候,直接把异步逻辑放组件内不行。

不放组件内,那放哪呢?

redux 提供的中间件机制是不是可以用来放这些异步过程呢?

redux 中间件

先看下什么是 redux 中间件:

redux 的流程很简单,就是 dispatch 一个 action 到 store, reducer 来处理 action。那么如果想在到达 store 之前多做一些处理呢?在哪里加?

改造 dispatch!中间件的原理就是层层包装 dispatch。

下面是 applyMiddleware 的源码,可以看到 applyMiddleware 就是对 store.dispatch 做了层层包装,最后返回修改了 dispatch 之后的 store。

function applyMiddleware(middlewares) {
let dispatch = store.dispatch
middlewares.forEach(middleware =>
dispatch = middleware(store)(dispatch)
)
return { ...store, dispatch}
}

所以说中间件最终返回的函数就是处理 action 的 dispatch:

function middlewareXxx(store) {
return function (next) {
return function (action) {
// xx
};
};
};
}

中间件会包装 dispatch,而 dispatch 就是把 action 传给 store 的,所以中间件自然可以拿到 action、拿到 store,还有被包装的 dispatch,也就是 next。

比如 redux-thunk 中间件的实现:

function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}

return next(action);
};
}

const thunk = createThunkMiddleware();

它判断了如果 action 是一个函数,就执行该函数,并且把 store.dispath 和 store.getState 传进去,否则传给内层的 dispatch。

通过 redux-thunk 中间件,我们可以把异步过程通过函数的形式放在 dispatch 的参数里:

const login = (userName) => (dispatch) => {
dispatch({ type: 'loginStart' })
request.post('/api/login', { data: userName }, () => {
dispatch({ type: 'loginSuccess', payload: userName })
})
}
store.dispatch(login('guang'))

但是这样解决了组件里的异步过程不好复用、多个异步过程之间不好做并行、串行等控制的问题了么?

没有,这段逻辑依然是在组件里写,只不过移到了 dispatch 里,也没有提供多个异步过程的管理机制。

解决这个问题,需要用 redux-saga 或 redux-observable 中间件。

redux-saga

redux-saga 并没有改变 action,它会把 action 透传给 store,只是多加了一条异步过程的处理。

redux-saga 中间件是这样启用的:

import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import rootReducer from './reducer'
import rootSaga from './sagas'

const sagaMiddleware = createSagaMiddleware()
const store = createStore(rootReducer, {}, applyMiddleware(sagaMiddleware))
sagaMiddleware.run(rootSaga)

要调用 run 把 saga 的 watcher saga 跑起来:

watcher saga 里面监听了一些 action,然后调用 worker saga 来处理:

import { all, takeLatest } from 'redux-saga/effects'

function* rootSaga() {
yield all([
takeLatest('login', login),
takeLatest('logout', logout)
])
}
export default rootSaga

redux-saga 会先把 action 透传给 store,然后判断下该 action 是否是被 taker 监听的:

function sagaMiddleware({ getState, dispatch }) {
return function (next) {
return function (action) {
const result = next(action);// 把 action 透传给 store

channel.put(action); //触发 saga 的 action 监听流程

return result;
}
}
}

当发现该 action 是被监听的,那么就执行相应的 taker,调用 worker saga 来处理:

function* login(action) {
try {
const loginInfo = yield call(loginService, action.account)
yield put({ type: 'loginSuccess', loginInfo })
} catch (error) {
yield put({ type: 'loginError', error })
}
}

function* logout() {
yield put({ type: 'logoutSuccess'})
}

比如 login 和 logout 会有不同的 worker saga。

login 会请求 login 接口,然后触发 loginSuccess 或者 loginError 的 action。

logout 会触发 logoutSuccess 的 action。

redux saga 的异步过程管理就是这样的:先把 action 透传给 store,然后判断 action 是否是被 taker 监听的,如果是,则调用对应的 worker saga 进行处理。

redux saga 在 redux 的 action 流程之外,加了一条监听 action 的异步处理的流程。

其实整个流程还是比较容易理解的。理解成本高一点的就是 generator 的写法了:

比如下面这段代码:

function* xxxSaga() {
while(true) {
yield take('xxx_action');
//...
}
}

它就是对每一个监听到的 xxx_action 做同样的处理的意思,相当于 takeEvery:

function* xxxSaga() {
yield takeEvery('xxx_action');
//...
}

但是因为有一个 while(true),很多同学就不理解了,这不是死循环了么?

不是的。generator 执行后返回的是一个 iterator,需要另外一个程序调用 next 方法才会继续执行。所以怎么执行、是否继续执行都是由另一个程序控制的。

在 redux-saga 里面,控制 worker saga 执行的程序叫做 task。worker saga 只是告诉了 task 应该做什么处理,通过 call、fork、put 这些命令(这些命令叫做 effect)。

然后 task 会调用不同的实现函数来执行该 worker saga。

为什么要这样设计呢?直接执行不就行了,为啥要拆成 worker saga 和 task 两部分,这样理解成本不就高了么?

确实,设计成 generator 的形式会增加理解成本,但是换来的是可测试性。因为各种副作用,比如网络请求、dispatch action 到 store 等等,都变成了 call、put 等 effect,由 task 部分控制执行。那么具体怎么执行的就可以随意的切换了,这样测试的时候只需要模拟传入对应的数据,就可以测试 worker saga 了。

redux saga 设计成 generator 的形式是一种学习成本和可测试性的权衡。

还记得 redux-thunk 有啥问题么?多个异步过程之间的并行、串行的复杂关系没法处理。那 redux-saga 是怎么解决的呢?

redux-saga 提供了 all、race、takeEvery、takeLatest 等 effect 来指定多个异步过程的关系:

比如 takeEvery 会对多个 action 的每一个做同样的处理,takeLatest 会对多个 action 的最后一个做处理,race 会只返回最快的那个异步过程的结果,等等。

这些控制多个异步过程之间关系的 effect 正是 redux-thunk 所没有的,也是复杂异步过程的管理必不可少的部分。

所以 redux-saga 可以做复杂异步过程的管理,而且具有很好的可测试性。

其实异步过程的管理,最出名的是 rxjs,而 redux-observable 就是基于 rxjs 实现的,它也是一种复杂异步过程管理的方案。

redux-observable

redux-observable 用起来和 redux-saga 特别像,比如启用插件的部分:

const epicMiddleware = createEpicMiddleware();

const store = createStore(
rootReducer,
applyMiddleware(epicMiddleware)
);

epicMiddleware.run(rootEpic);

和 redux saga 的启动流程是一样的,只是不叫 saga 而叫 epic。

但是对异步过程的处理,redux saga 是自己提供了一些 effect,而 redux-observable 是利用了 rxjs 的 operator:

import { ajax } from 'rxjs/ajax';

const fetchUserEpic = (action$, state$) => action$.pipe(
ofType('FETCH_USER'),
mergeMap(({ payload }) => ajax.getJSON(`/api/users/${payload}`).pipe(
map(response => ({
type: 'FETCH_USER_FULFILLED',
payload: response
}))
)
);

通过 ofType 来指定监听的 action,处理结束返回 action 传递给 store。

相比 redux-saga 来说,redux-observable 支持的异步过程的处理更丰富,直接对接了 operator 的生态,是开放的,而 redux-saga 则只是提供了内置的几个 effect 来处理。

所以做特别复杂的异步流程处理的时候,redux-observable 能够利用 rxjs 的操作符的优势会更明显。

但是 redux-saga 的优点还有基于 generator 的良好的可测试性,而且大多数场景下,redux-saga 提供的异步过程的处理能力就足够了,所以相对来说,redux-saga 用的更多一些。

总结

前端框架实现了数据到视图的绑定,我们只需要关心数据流就可以了。

相比 context 的混乱的数据流,redux 的 view -> action -> store -> view 的单向数据流更清晰且容易管理。

前端代码中有很多异步过程,这些异步过程之间可能有串行、并行甚至更复杂的关系,放在组件里并不好管理,可以放在 redux 的中间件里。

redux 的中间件就是对 dispatch 的层层包装,比如 redux-thunk 就是判断了下 action 是 function 就执行下,否则就是继续 dispatch。

redux-thunk 并没有提供多个异步过程管理的机制,复杂异步过程的管理还是得用 redux-saga 或者 redux-observable。

redux-saga 透传了 action 到 store,并且监听 action 执行相应的异步过程。异步过程的描述使用 generator 的形式,好处是可测试性。比如通过 take、takeEvery、takeLatest 来监听 action,然后执行 worker saga。worker saga 可以用 put、call、fork 等 effect 来描述不同的副作用,由 task 负责执行。

redux-observable 同样监听了 action 执行相应的异步过程,但是是基于 rxjs 的 operator,相比 saga 来说,异步过程的管理功能更强大。

不管是 redux-saga 通过 generator 来组织异步过程,通过内置 effect 来处理多个异步过程之间的关系,还是 redux-observable 通过 rxjs 的 operator 来组织异步过程和多个异步过程之间的关系。它们都解决了复杂异步过程的处理的问题,可以根据场景的复杂度灵活选用。

原文:https://juejin.cn/post/7011835078594527263


收起阅读 »

重构B端 ? 表单篇

随着业务的庞大。B端的业务越来越重,导致后面的需求越来越难满足,人在工位坐,锅从天上来,一个小前端就地开启了重构之旅 1. 梳理待重构的B端 上面是待重构 B端 的结构图,由 PHP 编写,利用约定的字段上传 JSON 文件,让 Controller 读取文...
继续阅读 »

随着业务的庞大。B端的业务越来越重,导致后面的需求越来越难满足,人在工位坐,锅从天上来,一个小前端就地开启了重构之旅


1. 梳理待重构的B端



上面是待重构 B端 的结构图,由 PHP 编写,利用约定的字段上传 JSON 文件,让 Controller 读取文件配置 在 DB 生成一个表,再由 Controller 直出到 View 层;在应对那些比较简单的逻辑或者功能性单一的业务时可以起到非常大的作用。但是需求只会越来越多。越来越复杂,尤其在处理复杂业务的时,整个 Controller 作为数据中枢,如果夹杂了太多的冗余逻辑,渐渐的就跟蜘蛛网一样难以维护。


2. 设计重构方案


了解完整个大概的数据走向以及业务背景之后,接下来就是设计整个重构方案了。



  • 继承之前的业务逻辑、通过 JSON 文件 渲染整个页面

  • 整体 UI 升级 ,因为用 React 重构,UI 这里选择 Antd

  • 前后端分离,本地开发先 Mock ,后期用 node 去代理或者用其他更灵活的方式交互数据


3. 配置文件改造



脚手架搭建,这里用 Antd Pro 脚手架 ( umi.js + antd ) , Router 配置 以及 React-Redux 等工具 umi 都帮我们内置了 ,基础搭建就不展开描述了。



{
"字段名称": {
"name_cfg": {
"label": "label",
"type": "select",
"tips": "tips",
"required": "required",
"max_length": 0,
"val_arr": {
"-1": "请选择",
"1": "字段 A",
"2": "字段 B",
"default": "-1"
},
"tabhide": {
"1": "A 字段, B 字段",
"2": "B 字段, C 字段",
"3": "C 字段, D 字段"
}
"relation_select": {
"url": "其他业务的接口请求",
"value": "Option id",
"name": "Option name",
"relation_field": "relation_field1, relation_field2"
}
},
"sql_cfg": {
"type": "int(11)",
"length": 11,
"primary": 0,
"default": -1,
"auto_increment": 0
}
},
...
}

这个 JSON 文件共有几十个字段,这里拿了一个比较有代表性的字段。这是一个 Select 类型的表单,val_arr 是这个 Select 的值,relation_select 请求其他业务的接口填进去这个 val_arr 供给给 Select , tabhide 表示的是,当值为 Key 时 隐藏表单的 Value 字段,多字段时以逗号分割。


此时需要一个过度文件将这个 JSON 文件处理成咋们方便处理的格式


// transfrom.js 
let Arr = [];
let Data = json.field_cfg;
for (let i in Data) {
let obj = {};
let val_Array = [];
let tab_hide = Data[i]['name_cfg']['tabhide']
for (let index in Data[i]['name_cfg']['val_arr']) {
let obj = {};
if (index !== 'default') {
obj['value'] = index;
obj['label'] = Data[i]['name_cfg']['val_arr'][index];
if(tab_hide && tab_hide[index]){
obj['tab_hide'] = tab_hide[index].split(',');
}
val_Array.push(obj);
} else {
val_Array.map((item) => {
if (item.value === Data[i]['name_cfg']['val_arr'][index]) {
item['default'] = true;
}
});
}
}
obj['id'] = i;
obj['name'] = Data[i]['name_cfg']['label'];
obj['type'] = Data[i]['name_cfg']['type'];
obj['required'] = Data[i]['name_cfg']['required'];
obj['tips'] = Data[i]['name_cfg']['tips'];
obj['multiple'] = Data[i]['name_cfg']['multiple'];
obj['val_arr'] = val_Array;
obj['sql_cfg_type'] = Data[i]['sql_cfg']['type'];
obj['sql_default'] = Data[i]['sql_cfg']['default'];
Arr.push(obj);
}

// config.js
const config = [
{
id: '字段名称',
name: 'label -> Name',
type: 'select',
required: 'required',
tips: 'tips',
val_arr: [
{ value: '-1', label: '请选择', default: true }
{ value: '1', label: 'Option A', tab_hide: ['字段A', '字段B'] }
{ value: '2', label: 'Option B', tab_hide: ['字段B', '字段C'] }
],
sql_cfg_type: 'int(11)',
sql_default: -1,
},
...
]

4. 表单渲染


Antd 的表单组件的功能非常丰富。拿到 Config 按照文档一把唆就完事了。

React 的 函数组件 和 Hooks 让代码更加简洁了,可太棒了!


  const renderForm: () => (JSX.Element | undefined)[] = () => {
// renderSelect(); renderText(); renderNumber(); renderDatePicker();
}
const renderSelect: (configItem: ConfigListData) => JSX.Element = (configItem) => {...}

const renderText: (configItem: ConfigListData) => JSX.Element = (configItem) => {...}

const renderNumber: (configItem: ConfigListData) => JSX.Element = (configItem) => {...}

const renderDatePicker: (configItem: ConfigListData) => JSX.Element = (configItem) => {...}

5. 默认字段 & 隐藏字段


componentDidMount 的时候处理这些字段,用 Hooks 可以这么表达 React.useEffect(()=>{...},[])

默认字段 : 给 Form 表单 设置上配置表的 sql_default 即可。

隐藏字段 : 这里涉及到字段会重叠,之前用 JQuery 单纯操作 DOM 节点去 show() 和 hide()。在操作 Dom 这方面,JQ 确实是有他的优势。

现在的解决方案如下图所示:


  const [hideListMap, setHideListMap] = useState<any>(new Map());
configObj.forEach((item: { val_arr: { tab_hide: any; value: any; }[]; sql_default: any; id: any; }) => {
item.val_arr.forEach((val_arr_item: { tab_hide: any; value: any; }) => {
if (val_arr_item.tab_hide && val_arr_item.value === item.sql_default) {
hideListMap.set(item.id, val_arr_item.tab_hide);
}
});
});
setHideListMap(hideListMap)

new Map() 用字段名作为 key ,value 的值为 tab_hide 数组, 之前用 Array 创建一个动态的 key 、value ,发现操作起来没有 Map 好使。


  const [hideList, setHideList] = useState<string[]>();

React.useEffect(() => {
let arr: string[] = []
hideListMap.forEach((item: any, key: any) => {
arr.push(...item);
});
setHideList(arr);
}, [hideListMap])

const selctChange = React.useCallback((value: SelectValue, configItem: ConfigListData) => {
hideListMap.forEach((item: any, key: string) => {
if (key === configItem.id) {
configItem.val_arr.forEach((val_arr_item: { value: SelectValue; tab_hide: any; }) => {
if (val_arr_item.value === value) {
hideListMap.set(configItem.id, val_arr_item.tab_hide);
setHideListMap(new Map(hideListMap))
}
})
}
});
}, [hideListMap]);

React.useEffect 不仅可以当 componentDidMount ,还可以当 componentDidUpdate 使用,只需要在第二个参数加上监听的值 React.useEffect(()=>{...},[n]), 这里监听了 hideListMap 如果 Select 框触发了 改变了 hideListMap 会自动帮我个更新 hideList,只要拿这个 hideList 作为条件判断是否渲染。就满足了多字段隐藏的需求了。


  const renderForm: () => (JSX.Element | undefined)[] = () => {
return configObj.map((item: ConfigListData) => {
if (hideList && hideList.includes(item.id)) {
return undefined
}
if (item.type === 'text') {
if (item.sql_cfg_type.indexOf('int') !== -1) {
return renderNumber(item)
} else {
return renderText(item)
}
}
if (item.type === 'select') {
return renderSelect(item)
}
if (item.type === 'datetime') {
return renderDatePicker(item);
}
return undefined;
})
}

6. Select 动态渲染值


// transfrom.js
let service = [];
let Data = json.field_cfg;
for (let i in Data) {
let relation_select = Data[i]['name_cfg']['relation_select'];
if (relation_select && relation_select['relation_field']) {
relation_select['relation_field'] = relation_select['relation_field'].split(',');
service.push(relation_select);
}
}

// config.ts
const SERVICE = [
{
url: "其他业务的接口请求",
value: "Option id",
name: "Option name",
relation_field: ["relation_field1", "relation_field2" ],
method: 'get'
}
...
]
// utils.ts
import request from 'umi-request'; // 请求库
export const getSelectApi = async (url: string, method: string) => {
return request(url, {
method,
})
}

之前接口请求与 Config 是耦合在一起的,Config 的字段如果增加到一定的数量时就会变得难以维护。最后决定把 表单Config 和 Service 层解耦,目的是为了更为直观的区别 Config 和 Service,方便以后维护。



数据接口参数格式以及字段都要有所约束。这里需要起一个 node 层,主要是处理第三方接口跨域处理,以及参数统一等。



import { CONFIG_OBJ, SERVICE } from './config';
const Form: React.FC = () => {
const [configObj, setConfigObj] = React.useState(CONFIG_OBJ);
React.useEffect(() => {
(async () => {
for (let item of SERVICE) {
for (let fieldItem of item.relation_field) { // 多字段支持
insertObj[fieldItem] = await getSelectApi(item.url, item.method)
}
}
for (let key in insertObj) {
if (key === item.id) {
item.val_arr.push(...insertObj[key].list)
}
}
setConfigObj([...configObj]);
})()
})
...
}
export default Form

将解耦出来的接口配置融合在 form 表单。useState 检测到数据指向变动就会重新渲染。

到这里 Form 表单组件基本搭建完成了。


6. 最后


再进一步思考,这里还有些优化的点,我们可以把这些配置文件也融进去这个B端里,将 Config 和 Service 都写进 DB,脱离用文件上传这种方式。


B端产品的服务群体是企业内人员。B端的用户体验也一样重要,一个优秀的B端也是提高效率、节省成本的途径之一。



本文到此结束,希望对各位看官有帮助 (‾◡◝)


作者:Coffee_C
链接:https://juejin.cn/post/6922286595290693646

收起阅读 »

Taro的http封装

 当我们使用Taro的时候,经常会用到http请求,那么又怎么封装呢?serve.ts import { request, getStorageSync } from '@tarojs/taro' class Server { protecte...
继续阅读 »

当我们使用Taro的时候,经常会用到http请求,那么又怎么封装呢?

serve.ts


import { request, getStorageSync } from '@tarojs/taro'

class Server {
protected ajax({
url,
data,
method = 'GET',
...restParams
}: Taro.RequestParams
) {
// 用户token
const Authorization: string = getStorageSync('token') || ''
// 判断请求类型
let contentType: string
// GET请求
if (method === 'GET') {
contentType = 'application/json'
// POST 请求
} else if (method === 'POST') {
contentType = 'application/x-www-form-urlencoded'
}
return new Promise<Taro.request.SuccessCallbackResult>(
(resolve, reject) => {
request({
url,
data,
method,
header: {
'content-type': contentType,
Authorization,
},
...restParams,
// 成功回调
success(res: Taro.request.SuccessCallbackResult): void {
resolve(res)
},
// 失败回调
fail(err: Taro.General.CallbackResult): void {
reject(err)
},
})
}
)
}
}

export default Server

引用时


import Server from './serve'

let BASEURL: string
if (process.env.TARO_ENV === 'h5') {
BASEURL = '/api'
} else {
BASEURL = 'http://localhost:8000/api'
}

type Result = Taro.request.SuccessCallbackResult<any> | undefined

interface Err {
code: number
message: string
}

interface APILayout<T> {
status: number,
code: number
data: T
}
interface APIMessage<T> {
code: number
message: T
}

// 异常处理
function errMessage(error: Error | any, result: Result): Err | null {
console.log(error);

// H5
if (H5 && !error) {
return null
} else if (result?.statusCode === 200) {
return null
}
const code = error?.status || result?.statusCode
if (code === 401) {
// 清空token
}
const errInfo = {
401: '请先登录',
404: '服务器未响应',
500: '服务器繁忙',
}
return {
code,
message: errInfo[code] ? errInfo[code] : error.info,
}
}
export async function testAPI() {
let [error, result] = await to(
this.ajax({
url: BASEURL + '/test.php'
})
)
const err = errMessage(error, result)
const res: APILayout<Ip> = result?.data

return { err, res }
}

ok 直接用就行了













收起阅读 »

觉得前端不需要懂算法?那来看下这个真实的例子

算法是问题的解决步骤,同一个问题可以有多种解决思路,也就会有多种算法,但是算法之间是有好坏之分的,区分标志就是复杂度。通过复杂度可以估算出耗时/内存占用等性能的好坏,所以我们用复杂度来评价算法。(不了解复杂度可以看这篇:性能分析不一定得用 Profiler,复...
继续阅读 »

算法是问题的解决步骤,同一个问题可以有多种解决思路,也就会有多种算法,但是算法之间是有好坏之分的,区分标志就是复杂度。

通过复杂度可以估算出耗时/内存占用等性能的好坏,所以我们用复杂度来评价算法。

(不了解复杂度可以看这篇:性能分析不一定得用 Profiler,复杂度分析也行

开发的时候,大多数场景下我们用最朴素的思路,也就是复杂度比较高的算法也没啥问题,好像也用不到各种高大上的算法,算法这个东西似乎可学可不学。

其实不是的,那是因为你没有遇到一些数据量大的场景。

下面我给你举一个我之前公司的具体场景的例子:

体现算法威力的例子

这是我前公司高德真实的例子。

我们会做全源码的依赖分析,会有几万个模块,一个模块依赖另一个模块叫做正向依赖,一个模块被另一个模块依赖叫做反向依赖。我们会先分析一遍正向依赖,然后再分析一遍反向依赖。

分析反向依赖的时候,之前的思路是这样的,对于每一个依赖,都遍历一边所有的模块,找到依赖它的模块,这就是它的反向依赖。

这个思路是很朴素的,容易想到的思路,但是这个思路有没有问题呢?

这个算法的复杂度是 O(n^2),如果 n 达到了十几万,那性能会很差的,从复杂度我们就可以估算出来。

事实上也确实是这样,后来我们跑一遍全源码依赖需要用 10 几个小时,甚至一晚上都跑不出来。

如果让你去优化,你会怎么优化性能呢?

有的同学可能会说,能不能拆成多进程/多个工作线程,把依赖分析的任务拆成几部分来做,这样能得到几倍的性能提升。

是,几倍的提升很大了。

但是如果说我们后来做了一个改动,性能直接提升了几万倍你信么?

我们的改动方式是这样的:

之前是在分析反向依赖的时候每一个依赖都要遍历一遍所有的正向依赖。但其实正向依赖反过来不就是反向依赖么?

所以我们直接改成了分析正向依赖的时候同时记录反向依赖。

这样根本就不需要单独分析反向依赖了,算法复杂度从 O(n^2)降到了 O(n)。

O(n^2) 到 O(n) 的变化在有几万个模块的时候,就相当于几万倍的性能提升。

这体现在时间上就是我们之前要跑一个晚上的代码,现在十几分钟就跑完了。这优化力度,你觉得光靠多线程/进程来跑能做到么?

这就是算法的威力,当你想到了一个复杂度更低的算法,那就意味着性能有了大幅的提升。

为什么我们整天说 diff 算法,因为它把 O(n^2) 的朴素算法复杂度降低到了 O(n),这就意味着 dom 节点有几千个的时候,就会有几千倍的性能提升。

所以,感受到算法的威力了么?

总结

多线程、缓存等手段最多提升几倍的性能,而算法的优化是直接提升数量级的性能,当数据量大了以后,就是几千几万倍的性能提升。

那为什么我们平时觉得算法没用呢?那是因为你处理的数据量太小了,处理几百个数据,你用 O(n^2) O(n^3) 和 O(n) 的算法,都差不了多少。

你处理的场景数据量越大,那算法的重要性越高,因为好的算法和差的算法的差别不是几倍几十倍那么简单,可能是几万倍的差别。

所以,你会见到各大公司都在考算法,没用么?不是的,是太过重要了,直接决定着写出的代码的性能。

原文:https://juejin.cn/post/7023024399376711694

收起阅读 »

前端监控系统设计

前言: 创建一个可随意插拔的插件式前端监控系统 一、数据采集 1.异常数据 1.1 静态资源异常 使用window.addEventListener('error',cb) 由于这个方法会捕获到很多error,所以我们要从中筛选出静态资源文件加载错误情况,这里...
继续阅读 »

前言: 创建一个可随意插拔的插件式前端监控系统


一、数据采集


1.异常数据


1.1 静态资源异常


使用window.addEventListener('error',cb)


由于这个方法会捕获到很多error,所以我们要从中筛选出静态资源文件加载错误情况,这里只监控了js、css、img


// 捕获静态资源加载失败错误 js css img
window.addEventListener('error', e => {
const target = e.targetl
if (!target) return
const typeName = e.target.localName;
let sourceUrl = "";
if (typeName === "link") {
sourceUrl = e.target.href;
} else if (typeName === "script" || typeName === "img") {
sourceUrl = e.target.src;
}

if (sourceUrl) {
lazyReportCache({
url: sourceUrl,
type: 'error',
subType: 'resource',
startTime: e.timeStamp,
html: target.outerHTML,
resourceType: target.tagName,
paths: e.path.map(item => item.tagName).filter(Boolean),
pageURL: getPageURL(),
})
}
}, true)


1.2 js错误


通过 window.onerror 获取错误发生时的行、列号,以及错误堆栈


生产环境需要上传打包后生成的map文件,利用source-map 对压缩后的代码文件和行列号得出未压缩前的报错行列数和源码文件


// parseErrorMsg.js
const fs = require('fs');
const path = require('path');
const sourceMap = require('source-map');

export default async function parseErrorMsg(error) {
const mapObj = JSON.parse(getMapFileContent(error.url))
const consumer = await new sourceMap.SourceMapConsumer(mapObj)
// 将 webpack://source-map-demo/./src/index.js 文件中的 ./ 去掉
const sources = mapObj.sources.map(item => format(item))
// 根据压缩后的报错信息得出未压缩前的报错行列数和源码文件
const originalInfo = consumer.originalPositionFor({ line: error.line, column: error.column })
// sourcesContent 中包含了各个文件的未压缩前的源码,根据文件名找出对应的源码
const originalFileContent = mapObj.sourcesContent[sources.indexOf(originalInfo.source)]
return {
file: originalInfo.source,
content: originalFileContent,
line: originalInfo.line,
column: originalInfo.column,
msg: error.msg,
error: error.error
}
}

function format(item) {
return item.replace(/(\.\/)*/g, '')
}

function getMapFileContent(url) {
return fs.readFileSync(path.resolve(__dirname, `./dist/${url.split('/').pop()}.map`), 'utf-8')
}

1.3 自定义异常


通过console.error打印出来的,我们将其认为是自定义错误


使用 window.console.error 上报自定义异常信息


1.4 接口异常



  1. 当状态码异常时,上报异常

  2. 重写 onloadend 方法,当其 response 对象中 code 值不为 '000000' 时上报异常 

  3. 重写 onerror 方法,当网络中断时无法触发 onload(end) 事件,会触发 onerror, 此时上报异常


1.5 监听未处理的promise错误


当Promise 被reject 且没有reject 处理器的时候,就会触发 unhandledrejection 事件


使用 window.addEventListener('unhandledrejection',cb)


2.性能数据


2.1 FP/FCP/LCP/CLS


chrome 开发团队提出了一系列用于检测网页性能的指标:



  • FP(first-paint),从页面加载开始到第一个像素绘制到屏幕上的时间

  • FCP(first-contentful-paint),从页面加载开始到页面内容的任何部分在屏幕上完成渲染的时间

  • LCP(largest-contentful-paint),从页面加载开始到最大文本块或图像元素在屏幕上完成渲染的时间

  • CLS(layout-shift),从页面加载开始和其生命周期状态变为隐藏期间发生的所有意外布局偏移的累积分数




其中,前三个性能指标都可以直接通过 PerformanceObserver (PerformanceObserver 是一个性能监测对象,用于监测性能度量事件 )来获取。而CLS 则需要通过一些计算。


在了解一下计算方式之前,我们先了解一下会话窗口的概念:一个或多个布局偏移间,它们之间有少于1秒的时间间隔,并且第一个和最后一个布局偏移时间间隔上限为5秒,超过5秒的布局偏移将被划分到新的会话窗口。


Chrome 速度指标团队在完成大规模分析后,将所有会话窗口中的偏移累加最大值用来反映页面布局最差的情况(即CLS)。


如下图:会话窗口2只有一个微小的布局偏移,则会话窗口2会被忽略,CLS只计算会话窗口1中布局偏移的总和。


拉低平均值的小布局偏移示例


2.2 DOMContentLoaded事件 和  onload 事件



  • DOMContentLoaded: HTML文档被加载和解析完成。在文档中没有脚本的情况下,浏览器解析完文档便能触发DOMContentLoaded;当文档中有脚本时,脚本会阻塞文档的解析,而脚本需要等位于脚本前面的css加载完才能执行。但是在任何情况下,DOMContentLoaded 都不需要等图片等其他资源的解析。

  • onload: 需要等页面中图片、视频、音频等其他所有资源都加载后才会触发。


为什么我们在开发时强调把css放在头部,js放在尾部?


image.png


首先文件放置顺序决定下载的优先级,而浏览器为了避免样式变化导致页面重排or重绘,会阻塞内容的呈现,等所有css加载并解析完成后才一次性呈现页面内容,在此期间就会出现“白屏”。


而现代浏览器为了优化用户体验,无需等到所有HTML文档都解析完成才开始构建布局渲染树,也就是说浏览器能够渲染不完整的DOM tree和cssom,尽快减少白屏时间。


假设我们把js放在头部,js会阻塞解析dom,导致FP(First Paint)延后,所以我们将js放在尾部,以减少FP的时间,但不会减少 DOMContentLoaded 被触发的时间。


2.3 资源加载耗时及是否命中缓存情况


通过 PerformanceObserver 收集,当浏览器不支持 PerformanceObserver,还可以通过 performance.getEntriesByType(entryType) 来进行降级处理,其中:



  • Navigation Timing 收集了HTML文档的性能指标

  • Resource Timing 收集了文档依赖的资源的性能指标,如:css,js,图片等等


这里不统计以下资源类型:



  • beacon: 用于上报数据,不统计

  • xmlhttprequest:单独统计


我们能够获取到资源对象的如下信息:


image.png


使用performance.now()精确计算程序执行时间:



  • performance.now()  与 Date.now()  不同的是,返回了以微秒(百万分之一秒)为单位的时间,更加精准。并且与 Date.now()  会受系统程序执行阻塞的影响不同,performance.now()  的时间是以恒定速率递增的,不受系统时间的影响(系统时间可被人为或软件调整)。

  • Date.now()  输出的是 UNIX 时间,即距离 1970 的时间,而 performance.now()  输出的是相对于 performance.timing.navigationStart(页面初始化) 的时间。

  • 使用 Date.now()  的差值并非绝对精确,因为计算时间时受系统限制(可能阻塞)。但使用 performance.now()  的差值,并不影响我们计算程序执行的精确时间。


判断该资源是否命中缓存:

在这些资源对象中有一个 transferSize 字段,它表示获取资源的大小,包括响应头字段和响应数据的大小。如果这个值为 0,说明是从缓存中直接读取的(强制缓存)。如果这个值不为 0,但是 encodedBodySize 字段为 0,说明它走的是协商缓存(encodedBodySize 表示请求响应数据 body 的大小)。不符合以上条件的,说明未命中缓存。


2.4 接口请求耗时以及接口调用成败情况


对XMLHttpRequest 原型链上的send 以及open方法进行改写


import { originalOpen, originalSend, originalProto } from '../utils/xhr'
import { lazyReportCache } from '../utils/report'

function overwriteOpenAndSend() {
originalProto.open = function newOpen(...args) {
this.url = args[1]
this.method = args[0]
originalOpen.apply(this, args)
}

originalProto.send = function newSend(...args) {
this.startTime = Date.now()

const onLoadend = () => {
this.endTime = Date.now()
this.duration = this.endTime - this.startTime

const { duration, startTime, endTime, url, method } = this
const { readyState, status, statusText, response, responseUrl, responseText } = this
console.log(this)
const reportData = {
status,
duration,
startTime,
endTime,
url,
method: (method || 'GET').toUpperCase(),
success: status >= 200 && status < 300,
subType: 'xhr',
type: 'performance',
}

lazyReportCache(reportData)

this.removeEventListener('loadend', onLoadend, true)
}

this.addEventListener('loadend', onLoadend, true)
originalSend.apply(this, args)
}
}

export default function xhr() {
overwriteOpenAndSend()
}

二、数据上报


1. 上报方法


采用sendBeacon 和 XMLHttpRequest 相结合的方式


为什么要使用sendBeacon?



统计和诊断代码通常要在 unload 或者 beforeunload (en-US) 事件处理器中发起一个同步 XMLHttpRequest 来发送数据。同步的 XMLHttpRequest 迫使用户代理延迟卸载文档,并使得下一个导航出现的更晚。下一个页面对于这种较差的载入表现无能为力。

navigator.sendBeacon()  方法可用于通过HTTP将少量数据异步传输到Web服务器,同时不会延迟页面的卸载或影响下一导航的载入性能。这就解决了提交分析数据时的所有的问题:数据可靠,传输异步并且不会影响下一页面的加载。



2. 上报时机



  1. 先缓存上报数据,缓存到一定数量后,利用 requestIdleCallback/setTimeout 延时上报。

  2. 在即将离开当前页面(刷新或关闭)时上报 (onBeforeUnload )/ 在页面不可见时上报(onVisibilitychange,判断document.visibilityState/ document.hidden 状态)

作者:spider集控团队
链接:https://juejin.cn/post/7046697922255126558

收起阅读 »

拒绝!封装el-table,请别再用JSON数组来配置列了

阅读本文📖你将:明白通过JSON 来配置el-table的列可能并不是那么美。(作者主观意见)学会一点关于VNode操作的实例。(一点点)辩证地思考一下当我们在团队内对组件进行二次封装时,哪些东西是我们需要取舍的。前言大家好,我是春哥。我热爱&nbs...
继续阅读 »

阅读本文📖

你将:

  1. 明白通过JSON 来配置el-table的列可能并不是那么美。(作者主观意见)
  2. 学会一点关于VNode操作的实例。(一点点)
  3. 辩证地思考一下当我们在团队内对组件进行二次封装时,哪些东西是我们需要取舍的。

前言

大家好,我是春哥
我热爱 vue.js , ElementUI , Element Plus 相关技术栈,我的目标是给大家分享最实用、最有用的知识点,希望大家都可以早早下班,并可以飞速完成工作,淡定摸鱼🐟。

相信使用 vue 的同学大部分都用过 Element 系列框架,并且绝大部分都用过其中的 el-table 组件。并且几乎所有人都会把表格和分页进行一层封装。

不过,很多人在封装时,总是习惯性地把 el-table 官方推荐的 "插槽写法" 改成 "JSON 数组" 写法。

就像这样:

<template>
<my-el-table :columns="columns" :data="tableData">
</my-el-table>
</template>
<script setup>
const columns = [
{
prop: 'date',
label: 'Date',
width: '180'
},
{
prop: 'name',
label: 'Name'
}
]
// ...其他略
</script>

但经过我多年踩坑的惨痛经历,我必须要大声说出那句话:
快住手!有更好的封装技巧

尔康式拒绝

JSON 式封装哪些缺点?

缺点一:学习成本增高

以下两种场景,如果是你今天刚刚入职,你更愿意在业务代码里看到哪种组件呢?

你更愿意使用哪种组件?

我反正是更偏向于 1 和 4
第 1 种意味着它有丰富的社区支持,有准确而清晰的文档和 demo 可以借鉴。
第 4 种意味着你依然可以靠官方文档横行,并且可以使用一些同事根据业务进行的"增强能力"。

那么 2 和 3 呢?
也许我的同事真的可以做出很好的封装,但如果你在小厂、在初创公司、甚至在外包公司,更大的可能是你的同事并不靠谱。
他的某些封装只是为了满足单一的业务场景, 但你为了了解他的功能,却不得不去面对全新的 api,甚至是通过看他的源码才能了解具体有什么 api 和能力。

在这种场景下,我选择面对熟悉的,官方的 api。 

缺点二:自定义内容需要写 h 函数

不会真的有人喜欢在业务里写 h 函数吧?

当简单的 JSON 配置无法满足产品经理那天马行空的想象力时,你可能需要对 el-table-column 里的内容进行更多的自定义。
此时,也许你就会怀念"插槽式"的便捷了。
假设你的产品经理要求你写一个 带色彩的状态列 。 h和插槽

以上两种写法你会选择哪种呢?
而且,当业务变得更加复杂的时候,h 函数写法的可读性是指数式下跌的,你怕不怕?

当然,用JSX写法来简化 h 函数写法是个不错的思路。

JSON 式封装有哪些优点?

优点一:能简化写法?(并不)

有人说:“ JSON 式封装,能够简化代码写法。”
听到这样的话,我的内心其实是充满困惑的:它究竟能简化什么?
写法上的对比

看出来了吗?
这种常见的所谓 封装,只不过是做了做简单形式化的转换:你并没有少写哪怕一个属性,只不过把它们从这里挪到了那里。

甚至于极端场景,你还需要多写代码:

// 从:
<el-table-column show-overflow-tooltip />
// 变成:
{
'show-overflow-tooltip': true
}

优点二:只有 JSON 化才能实现动态列?(并不)

我在《我对 Element 的 Table 做了封装》 里讨论时, JackLiR8 同学提出了一个疑问: 
简化一下就是:

怎样封装才能在保持插槽写法的情况下,实现 动态列呢 ?

其实这个问题并不难,前提是需要你理解 vNode 是什么以及怎么操作它们。

我做了一个简单的例子,核心代码如下:

// vue3 函数式组件写法
const FunctionalTable = (props, context) => {
// 获得插槽里的 vNodes
const vNodes = context.slots.default()
// 过滤 vNodes
const filteredVNodes = props.visibleKeys == null ? vNodes : vNodes.filter(node => props.visibleKeys.includes(node?.props?.prop))
// 把属性透传给el-table
return <el-table {...context.attrs}>
{ filteredVNodes }
</el-table>
}
// vue3 函数式组件定义 props
FunctionalTable.props = {
visibleKeys: {
type: Array,
default: () => null
}
}

这就能实现 动态列 了?
是的。
下面正是使用时的代码:

<template>
<el-button @click="onClick">给我变!</el-button>
<FunctionalTable :data="tableData" :visibleKeys="visibleKeys">
<el-table-column prop="date" label="Date" width="180" />
<el-table-column prop="name" label="Name" />
<el-table-column prop="address" label="Address" />
</FunctionalTable>
</template>
<script setup>
// 其他略
const visibleKeys = ref(['date', 'name'])
const onClick = () => {
visibleKeys.value = ['date', 'name', 'address']
}
// 其他略
// ...

效果如下: 插槽写法的动态列

毫无疑问,当遇到复杂场景,以及列里需要渲染各种奇形怪状的东西如 tagpopover 或者你需要进行更加复杂的定义的时候,插槽写法是更为优秀的。
这是上述demo的源代码 => github源码

优点三:JSON 配置能存数据库?(我劝你慎重)

"如果我把列的 JSON 配置存到数据库里,那我就不用写代码了!"

好家伙,我直呼好家伙!

除非你已经封装了非常成熟的可视化配置方案,否则! 当业务上需要新增一列时,不还是你写?服务端和运维可不会帮你写代码。

只不过你存储代码的地点,从 git 变成了 数据库

碰上懒一点的服务端,你还需要安装数据库链接软件,增加一项写 sql update 语句的活儿。

更让人感到害怕的是,你丢失了对代码版本跟踪的能力。

"为啥生产库/测试库/开发库存的数据不一样,到底应该用谁,我也不知道这字段是哪个版本、因为什么被谁合入的呀...."

那一刻,你可能会无比怀念 git commit-msg 那。

我期望的封装是什么样的?

如果你想设计一款基于"element UI"或"element Plus",能解决一些迫在眉睫的问题,能优化一些写法,能规范一些格式,能让团队小伙伴们乐于使用到组件库。
我想,你可能得充分考虑以下内容:

  • 它的API是否简单易学(甚至大部分就和 element 一模一样 )?
  • 它是否确确实实简化了业务上的写法?
    比如把 表格 和 分页器 合并,比如提供 请求方法 作为 prop 等都是能极大降低业务复杂性的封装。
  • 它是否扩展性强,易维护?api 设计是否和项目保持风格上的一致?
    在同一个项目里,render/jsx/template 混用很可能会让一些新人感到吃力。
  • 它是否是增强和渐进的?
    我可不希望当我试图使用 elementUI 某个特性时,我猛然发现我同事封装的组件居然不支持!

震惊!

免喷声明

以上所有配图和文本都是我的个人观点。

如果你认为它们是错的,那它们就是错的吧。

关于我反对把 Element 表格列 JSON化 最初的初心,我确实厌倦了在不同团队不同公司总是要一遍又一遍去看前同事们蹩脚的封装,去理解他们做了哪些东西,拼写有没有改编,有没有丢失特性,去再学习一遍完全没有学习价值的API

希望大家写代码时,都能获得良好的体验。

封装组件时,都能封装出"能用","好用", "大家愿意用"的组件!


原文:https://juejin.cn/post/7043578962026430471

收起阅读 »

快速掌握 Performance 性能分析:一个真实的优化案例

这么强大的工具肯定是要好好掌握的,今天我们就来做一个性能优化的案例来快速上手 Performance 吧。首先,我们准备这样一段代码:<html lang="en"><head>    <meta charse...
继续阅读 »

Chrome Devtools 的 Performance 工具是性能分析和优化的利器,因为它可以记录每一段代码的耗时,进而分析出性能瓶颈,然后做针对性的优化。

这么强大的工具肯定是要好好掌握的,今天我们就来做一个性能优化的案例来快速上手 Performance 吧。

性能分析

首先,我们准备这样一段代码:


<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>worker performance optimizationtitle>
head>
<body>
   <script>
       function a() {
          b();
      }
       function b() {
           let total = 0;
           for(let i = 0; i< 10*10000*10000; i++) {
               total += i;
          }
           console.log('b:', total);
      }

       a();
   script>
   <script>
       function c() {
           d();
      }
       function d() {
           let total = 0;
           for(let i = 0; i< 1*10000*10000; i++) {
               total += i;
          }
           console.log('c:', total);
      }
       c();
   script>
body>
html>

很明显,两个 script 标签是两个宏任务,第一个宏任务的调用栈是 a、b,第二个宏任务的调用栈是 c、d。

我们用 Performance 来看一下是不是这样:

首先用无痕模式打开 chrome,无痕模式下没有插件,分析性能不会受插件影响。

打开 chrome devtools 的 Performance 面板,点击 reload 按钮,会重新加载页面并开始记录耗时:

过几秒点击结束。

这时候界面就会展示出记录的信息:

图中标出的 Main 就是主线程。

主线程是不断执行 Event Loop 的,可以看到有两个 Task(宏任务),调用栈分别是 a、b 和 c、d,和我们分析的对上了。(当然,还有一些浏览器内部的函数,比如 parseHtml、evaluateScript 等,这些可以忽略)

Performance 工具最重要的是分析主线程的 Event Loop,分析每个 Task 的耗时、调用栈等信息。

当你点击某个宏任务的时候,在下面的面板会显示调用栈的详情(选择 bottom-up 是列表展示, call tree 是树形展示)

每个函数的耗时也都显示在左侧,右侧有源码地址,点击就可以跳到 Sources 对应的代码。

直接展示了每行代码的耗时,太方便了!

工具介绍完了,我们来分析下代码哪里有性能问题。

很明显, b 和 d 两个函数的循环累加耗时太高了。

在 Performance 中也可以看到 Task 被标红了,下面的 summary 面板也显示了 long task 的警告。

有同学可能会问:为什么要优化 long task 呢?

因为渲染和 JS 执行都在主线程,在一个 Event Loop 中,会相互阻塞,如果 JS 有长时间执行的 Task,就会阻塞渲染,导致页面卡顿。所以,性能分析主要的目的是找到 long task,之后消除它。

可能很多同学都不知道,其实网页的渲染也是一个宏任务,所以才会和 JS 执行互相阻塞。关于这一点的证明可以看我前面一篇文章:

通过 Performance 证明,网页的渲染是一个宏任务

找到了要优化的代码,也知道了优化的目标(消除 long task),那么就开始优化吧。

性能优化

我们优化的目标是把两个 long task 中的耗时逻辑(循环累加)给去掉或者拆分成多个 task。

关于拆分 task 这点,可以参考 React 从递归渲染 vdom 转为链表的可打断的渲染 vdom 的优化,也就是 fiber 的架构,它的目的也是为了拆分 long task。

但明显我们这里的逻辑没啥好拆分的,它就是一个大循环。

那么能不能不放在主线程跑,放到其他线程跑呢?浏览器的 web worker 好像就是做耗时计算的性能优化的。

我们来试一下:

封装这样一个函数,传入 url 和数字,函数会创建一个 worker 线程,通过 postMessage 传递 num 过去,并且监听 message 事件来接收返回的数据。

function runWorker(url, num) {
   return new Promise((resolve, reject) => {
       const worker = new Worker(url);
       worker.postMessage(num);
       worker.addEventListener('message', function (evt) {
           resolve(evt.data);
      });
       worker.onerror = reject;
  });
};

然后 b 和 c 函数就可以改成这样了:

function b() {
   runWorker('./worker.js', 10*10000*10000).then(res => {
       console.log('b:', res);
  });
}

耗时逻辑移到了 worker 线程:

addEventListener('message', function(evt) {
   let total = 0;
   let num = evt.data;
   for(let i = 0; i< num; i++) {
       total += i;
  }
   postMessage(total);
});

完美。我们再跑一下试试:

哇,long task 一个都没有了!

然后你还会发现 Main 线程下面多了两个 Worker 线程:

虽然 Worker 还有 long task,但是不重要,毕竟计算量在那,只要主线程没有 long task 就行。

这样,我们通过把计算量拆分到 worker 线程,充分利用了多核 cpu 的能力,解决了主线程的 long task 问题,界面交互会很流畅。

我们再看下 Sources 面板:

对比下之前的:

这优化力度,肉眼可见!

就这样,我们一起完成了一次网页的性能优化,通过 Peformance 分析出 long task,定位到耗时代码,然后通过 worker 拆分计算量进行优化,成功消除了主线程的 long task。

代码传到了 github,感兴趣的可以拉下来用 Performance 工具分析下:

github.com/QuarkGluonP…

总结

Chrome Devtools 的 Performance 工具是网页性能分析的利器,它可以记录一段时间内的代码执行情况,比如 Main 线程的 Event Loop、每个 Event loop 的 Task,每个 Task 的调用栈,每个函数的耗时等,还可以定位到 Sources 中的源码位置。

性能优化的目标就是找到 Task 中的 long task,然后消除它。因为网页的渲染是一个宏任务,和 JS 的宏任务在同一个 Event Loop 中,是相互阻塞的。

我们做了一个真实的优化案例,通过 Performance 分析出了代码中的耗时部分,发现是计算量大导致的,所以我们把计算逻辑拆分到了 worker 线程以充分利用多核 cpu 的并行处理能力,消除了主线程的 long task。

做完这个性能优化的案例之后,是不是觉得 Peformance 工具用起来也不难呢?

其实会分析主线程的 Event Loop,会分析 Task 和 Task 的调用栈,找出 long task,并能定位到耗时的代码,Performance 工具就算是掌握了大部分了,常用的功能也就是这些。


作者:zxg_神说要有光
来源:https://juejin.cn/post/7046805217668497445

收起阅读 »

vue+elementui项目中,页面实现自适应,缩小放大页面排版基本保持不变

问题描述:vue+elementui项目中,页面实现自适应,缩小放大页面排版基本保持不变# 解决方案:第一步:最外层div样式 :fixed(固定定位):生成绝对定位的元素,相对于浏览器窗口进行定位。元素的位置通过 “left”, “top”, “right”...
继续阅读 »

问题描述:
vue+elementui项目中,页面实现自适应,缩小放大页面排版基本保持不变

# 解决方案:
第一步:最外层div样式 :
fixed(固定定位):生成绝对定位的元素,相对于浏览器窗口进行定位。元素的位置通过 “left”, “top”, “right” 以及 “bottom” 属性进行规定
display:flex 是一种布局方式。它即可以应用于容器中,也可以应用于行内元素。是W3C提出的一种新的方案,可以简便、完整、响应式地实现各种页面布局。目前,它已经得到了所有浏览器的支持。
width: 100%,height: 100%:实现页面宽高在不同窗口下都能占满整个屏幕。

.websit{undefined
position: fixed;
display: flex;
width: 100%;
height: 100%;
}


第二步:整体页面样式分三部分,分别是页面头部的:header-two,内容部分:main,页面底部的footer,
其中,头部和底部高度是不变的,中间内容部分的高度=页面高度-头部高度-底部高度,如下
给页面最外层div设置高度:自动获取当前浏览器高度,页面初始化的时候自动获取:

header-two {undefined
padding: 0;(内边距为0)
width: 100%;(宽度自动占满)
text-align: center;(内容居中显示)
height: 80px !important;(设置固定高度)
}
.footer {undefined
padding: 0;
width: 100%;
text-align: center;
height: 126px !important;
}
:style="{minHeight :minHeight +‘px’}"
mounted() {undefined
this.minHeight = document.documentElement.clientHeight - 0;
this.marginLeft = (document.documentElement.clientWidth - 1920) / 2;
const that = this;
window.onresize = function () {undefined
that.minHeight = document.documentElement.clientHeight - 0;
that.marginLeft = (document.documentElement.clientWidth - 1920) / 2
};
}


第三步:
这里header-two 下面还要加一个div,header-div,并为其设置项目要求的最小宽度,和最大宽度,这里设置为1920,保证缩放时的样式正常,同理,底部也要加上一个div,footer-div。
header-div,footer-div{undefined
margin: auto;
text-align: center;
min-width: 1920px !important;
max-width: 1920px !important;
}
第四步:为header-div和footer-div,设置向左偏移:style="{marginLeft:marginLeft + ‘px’ }"
第五步:中间内容过多时,会产生滚动弄条,我们想让滚动条产生在最外层,也就是,中间元素被撑开,因此设置属性
.main {undefined
overflow: visible;
}
A元素具有 overflow: visible 的属性,内层内容比较多时,分两种情况讨论
A元素高度auto:无作用,A元素撑开,正常滚动
A元素具有固定高度:虽然A限制的高度,但内层内容并不会隐藏,而是完全显示在屏幕上,参与布局,甚至撑开外层dom高度
第六步:涉及背景是图片,图片实现自适应,如下

header-first {undefined
padding: 0;
width: 100%;
text-align: center;
background-repeat: no-repeat;
height: 292px !important;
background-image: url(’…/aa.png’);
background-size: cover; /* 使图片平铺满整个浏览器(从宽和高的最大需求方面来满足,会使某些部分无法显示在区域中) */
代码如下:
export default {undefined
name: ‘ContainerMoudle’,
components: {Footer, WebsitHeaderTwo},
data() {undefined
return {undefined
minHeight: 0,
marginLeft: 0
}
},
mounted() {undefined
this.minHeight = document.documentElement.clientHeight - 0;
this.marginLeft = (document.documentElement.clientWidth - 1920) / 2;
const that = this;
window.onresize = function () {undefined
that.minHeight = document.documentElement.clientHeight - 0;
that.marginLeft = (document.documentElement.clientWidth - 1920) / 2
};
},
methods: {}
}


————————————————
原文链接:https://blog.csdn.net/weixin_44039043/article/details/109393574

收起阅读 »

node-sass的坑

国内做前端的,我感觉大多被这个坑过,所有的依赖都装的上,唯有这个依赖怎么都装不上。 首先第一个需要面对的问题,其实这个依赖装不上最大的原因是他在编译安装时需要下载一个安装包,这个安装包是在github上的,由于不可说的原因,国内连github的资源服务器raw...
继续阅读 »

国内做前端的,我感觉大多被这个坑过,所有的依赖都装的上,唯有这个依赖怎么都装不上。


首先第一个需要面对的问题,其实这个依赖装不上最大的原因是他在编译安装时需要下载一个安装包,这个安装包是在github上的,由于不可说的原因,国内连github的资源服务器rawgithubusercontent是很难连接的,这也直接导致了依赖无法安装。


解法1:
淘宝镜像,也是最直接的解法,淘宝镜像中的node-sass中的安装包地址指向已经被改成了淘宝镜像中的安装包地址,安装会很顺利。
这也是官方给出的对于网络问题的解法。


npm install -g mirror-config-china --registry=http://registry.npm.taobao.org
npm install node-sass

同样的--registry或者cnpm都适合用这个方法解。


解法2:
有些人可能因为团队使用package-lock.json来规范统一团队使用的依赖,可能就没法直接通过镜像的方式来下载淘宝镜像里的包了,但是这样也是有解决办法的,手动指定node-sass使用的安装包地址。
通过npmrc


npm config set sass_binary_site "https://npm.taobao.org/mirrors/node-sass/"

也可以设置环境变量:


set SASS_BINARY_SITE=https://npm.taobao.org/mirrors/node-sass/

这样直接通过npm install时候也可以单独使用淘宝的镜像来下载安装包。


解法3:
可以指定下载路径,那自然也可以先把安装包下载下来,再指定安装包进行安装。
先去https://github.com/sass/node-sass/releases/tag/{version}或者https://npm.taobao.org/mirrors/node-sass/下下载对应的安装包:{os}-{module-version}_bingding.node,具体的version根据你的nodejs版本查询下表可得。



接下来的步骤和解法2类似,这里给出npmrc的改法


npm config set sass_binary_path [path]

解法4:
既然主要原因是功夫网,那就只能翻过去了。
先设置系统代理,然后配置npm的代理。


npm config set proxy [system proxy]

完成下载以后问题只解决了一半,下载下来的安装包还需要编译,node-sass需要一些编译环境来确保编译完成,这里有个简单的方案:


npm install -g node-gyp
npm install --global --production windows-build-tools

这个会帮你安装对应的编译环境,问题大部分都能解决,这里面包含了vs的build工具以及python。


这里有个隐藏的小坑也是关于node-gyp的,如果你的node-gyp不是全局安装,而是一个在package-lock.json中的依赖,最好检查一下node-gyp的版本,就比如说我,node-gyp被限制在了一个老的版本上,那么最大的问题就是node-gyp本身去调用的build工具的版本是由他决定的,一些较老版本的node-gyp肯定并不支持更高版本的build工具,对应的支持可以在node-gyp的MSVSVersion.py中的version_map中找到,就拿我的3.8.0举例:


  version_map = {
'auto': ('14.0', '12.0', '10.0', '9.0', '8.0', '11.0'),
'2005': ('8.0',),
'2005e': ('8.0',),
'2008': ('9.0',),
'2008e': ('9.0',),
'2010': ('10.0',),
'2010e': ('10.0',),
'2012': ('11.0',),
'2012e': ('11.0',),
'2013': ('12.0',),
'2013e': ('12.0',),
'2015': ('14.0',),
}

可以看到这里最高仅仅支持到vs2015,所以我自己手动安装了vs2015的build工具最后才能成功编译。
这里也可以手动指定msvs_version来build工具的版本,不过大部分情况下auto就够用了。


npm config set msvs_version [version]

做到这里,执行npm install大概率就没有问题了,如果有问题的话,欢迎将输出的error log分享出来看看还有没有其他隐藏的坑在里面。


作者:Sczlog
链接:https://juejin.cn/post/6914193505061453838

收起阅读 »

微前端拆分实践

“这篇文章是我一次活动分享的讲稿”最近项目上机缘巧合用微前端解决了一些团队问题,借此机会跟大家分享一下。微前端作为近两年兴起的一种解决方案,也不是什么新东西了,既然是解决方案,那么微前端帮我们解决了什么问题呢?这里我以我们项目组为例子讲讲:我们为什么需要微前端...
继续阅读 »

这篇文章是我一次活动分享的讲稿

最近项目上机缘巧合用微前端解决了一些团队问题,借此机会跟大家分享一下。

微前端作为近两年兴起的一种解决方案,也不是什么新东西了,既然是解决方案,那么微前端帮我们解决了什么问题呢?这里我以我们项目组为例子讲讲:

我们为什么需要微前端?

我们的项目整体来看算得上一个比较大型的项目,整个项目规划完成后有 17 条业务线。但是在刚起项目的时候由于种种原因并没有考虑周全,将项目当成一个普通的前端项目来解决,在第一期项目结束,第一条业务上线后,我们紧接着开始了第二和第三条业务线的开发,紧接着我们就遇到了一些问题:

代码冲突

一期项目上线后交由维护团队维护,交付团队继续后面项目的开发。由于所有代码在同一个 repo 中作为一个大型单体被共同维护,两个团队的代码修改常常有冲突,需要小心 merge。同时还需要理解对方的业务,看自己的业务会不会破坏对方的业务。

部署冲突

由于所有的基础设施包括 CI/CD 等都是公用的,任何一个团队想要部署自己的代码,势必会对另外一个团队造成影响,不管是 feature toggle 还是 chunk base 的开发方式都将增大开发人员的心智负担。

技术栈冲突

由于项目比较大,未来团队的数量不确定,我们不能将技术栈限制死,否则就有可能有的团队要使用自己完全不熟练的技术栈,更别说未来还有第三方团队加入的可能性,我们不希望将整个项目绑定在某一个技术栈上。

基于这样的背景,我们发现微前端这套解决方案很好地解决了我们的问题。说白了在我们的项目背景下,我们最希望得到的东西是 -- 团队自治

我们希望各个业务线的团队能够自由修改自己的代码,不用担心与别的团队产生冲突。她们可以自由选择自己熟悉的技术栈,不必有过多限制。同时任何团队的部署都不会影响其他团队,这也就意味着某一个团队负责的部分如果挂掉了,网站上其他团队维护的部分也是可用的。

最重要的,这样的架构可以让各个团队聚焦在自己的技术和业务上,减少各个团队不必要的无效沟通,提升各个团队的开发效率。

拆分时机

对于微前端的拆分来说,这是一项工作量较大的技术改进,而且它不同于别的技术改进,它没有模版,没有办法按部就班的从网上找个东西过来照抄,必须要结合自己的项目来进行。

另一方面,我们需要达成共识的是,在我们的日常开发中,大多数情况下项目上不可能给开发人员足够的时间来做技术改进,这就意味着大多数技术改进需要同业务开发一同进行。那么找准一个改进的时机就很重要了。

那么这样的时机通常是什么时候呢?

业务有较大的改变或演进

这种情况我想大多数同学都经历过,在开发最初说的好好的需求,由于种种原因需要做一次大的改变。面对这种大的需求变更,通常我们的代码也需要做对应的改变,而这种改变也需要重写一些代码,这个重写的过程就是一个很好的进行拆分的好时机。

在这个期间我们有足够的理由说服项目干系人给我们时间去重新组织项目代码去更好地支持业务的发展。

业务稳定不再有大的改进

此时业务的发展趋于稳定,但目前的架构如果也的确给开发造成了阻碍。那么就可以在这个稳定架构上进行改进。当然此时的业务还在发展,我们可以采取两种策略:

  • 一种是以拆分任务为高优先级,新的业务开发基于新的架构
  • 一种是先在旧的架构上持续开发,在拆分的过程中由负责拆分的同学将业务和技术一起迁移过去

拆分原则

我们在拆分微前端的时候一定是带有某种目的的,有可能是想对技术栈进行渐进式升级,也有可能像我们一样想提升各个独立团队的自治力,在不同的目的下我们可能会秉持不同的原则,这也是另一个为什么微前端的拆分没办法简单抄作业的原因。

就我们项目来说,我们追求各个团队的最高自治力,那么我们就希望各个独立app尽量减少彼此的通信和依赖,每个app能够尽量独立处理自己的业务。

在这样的大前提下,我们可以按照业务为主模块为辅的方式指导拆分,基于此,我们定义了一些拆分时候的原则:

  • 保证业务独立,一条业务线应该由一个独立的app来支撑,使得该业务团队拥有这个app的完全控制权
  • 跨业务的页面不应该各个业务各自持有,也应该拆分为一个独立的app
  • 通用方法库和通用组件库由大家共同维护以支撑各自的业务

拆分前的准备

前置概念

single-spa

Single-spa 是一个微前端框架,它不限制每一个 app 具体使用怎样的技术栈,主要通过控制 route 的方式在页面上渲染不同的 app。

在开始微前端的拆分前我们进行了一些调研后选择了它作为我们微前端的框架,说是调研其实当时我们并没有过多的了解每一个框架,比如国内比较有名的 qiankun。

这里其实有一个小插曲,我们第一个了解的框架就是 single-spa,当时有一个小需求 single-spa 实现不了,于是我按照官网的文档去 slack 询问,第二天一大早我就收到了回复,算上时差他们一看到我的问题就给了我答复,这个反馈速度加上对国内开源社区的不乐观,我们直接就选择了 single-spa。

In-broswer module vs build time module

在开始实践前,我可能需要给大家介绍两个概念以帮助大家更好地理解接下来的架构设计,第一个概念是 in-broswer module,或者叫做es6 modules,与之对应的是现在用途最广的 build time module,这两个module有什么区别呢?我们先来看一个图:

module-build-result

module-build-result

这个图里两个 js 文件互相引用后最后打包的结果就是 build time module。在写代码的时候虽然你觉得这两个文件是分离的,但是其实在最终打包的时候这两个文件里的内容会被合并,最终变成一个 js 文件,然后这个 js 文件被 html 文件引用。

in-broswer module 则不同,这种模块是浏览器根据你提供的 url 从网络中请求回来的,你的每一个 import 都代表了一次网络请求,各个文件真的变成了独立的模块,通过网络请求相互依赖。

但是这样的模块有一个缺点,就是它没有办法像我们日常开发一样直接给一个名字就能直接引用到对应的模块:

import singleSpa from "single-spa";

由于需要在网络中定位到这个模块在哪里病发送对应的请求,它需要一个完整的url:

import singleSpa from "https://cdn.jsdelivr.net/npm/single-spa/esm/single-spa.min.js";

Import-map

这个特性使得大多数程序员都不喜欢它,毕竟大多数人都不想写一串长长的 url 来引用一个模块。为了解决这个问题,WICG 起草了一个新的浏览器规范,这个规范叫做 import map

<script type="importmap">
 {
  "imports": {
   "single-spa""https://cdn.jsdelivr.net/npm/single-spa/esm/single-spa.min.js"
  }
 }
</script>

import map 是一段特殊的 js,它的 type 为 importmap,在这个 script 标签里面的是一个 json object。这个 json object 的 key 就是某一个模块的名字,而它对应的 value 就是这个模块的 url 地址。

当然,既然 import-map 是一个 script 标签,那么理所应当它也可以加上 src 属性,成为一段外部 script:

<script type="importmap" src="https://some.url.to.your.importmap"></script>

在一些情况下,可能你的项目中引用了某一个包的不同版本,这时候可以用 import-map 的 scopes 功能来限制某一个文件的引用:

<script type="importmap">
 {
  "imports": {
   "lodash""https://unpkg.com/lodash@3"
  },
  "scopes": {
   "/module-a/": {
    "lodash""https://unpkg.com/lodash@4"
   }
  }
 }
</script>

这里的 scopes 代表了如果某一个 module 以 module-a 开头那么里面如果有引用 lodash 的 import,这个 import 将会引用 v4 版本,其他的 import 则都是引用的 v3 版本。

于是根据这个 import-map,我们就能够在代码里像使用正常模块那样使用 in-broswer module 了:

import singleSpa from "single-spa";

Systemjs

然后接下来就是前端传统节目,很显然,这么新的规范大部分浏览器目前都是不支持的,更别提永远也不可能支持的 IE 了,所以我们需要 polyfill - systemjs,它怎么工作的这里为了不扯远就不再赘述了,感兴趣的同学可以通过链接去 github 里面看文档,总的来说这是一个专门为了 es-module 而生的 polyfill。

我们从一个简单的 demo 来看它是怎么让 import-map 工作的:

es6-module-syntax

es6-module-syntax

这是一个很简单的 demo,HTML 页面中留有一段 template,然后导入一份 es-module,这份 module 也很简单,只做了一件事就是导入 vue 然后把 template 里面的 name 换成我们想要的东西。

但是这里有一个细节,我们在导入 vue 的时候必须用一段 url 来导入,如果我们把这段 url 换成我们平时开发时的字符串会发生什么呢?

import-without-url

import-without-url

这里会发生这样的错误是因为我们在 script 标签上标记了这个 script 是一个 es-module,于是里面的 import 关键字是浏览器在运行时执行的,但是因为后面的字符串没办法告诉浏览器 Vue 这个资源到底在哪,浏览器当然也就找不到对应的资源,于是就报错了。

如果我们想要将 url 替换为我们平时开发时候的字符串,就得依赖于 import-map,但是大部分浏览器现在都还不支持这一特性,于是我们需要引入 systemjs:

how-to-use-systemjs

how-to-use-systemjs

由于我们使用了 systemjs,为了按照它的规矩来行事,我们需要在原本的规范上修改一些代码:

  • 首先是我们需要在开始引入 systemjs
  • 然后将 import-map 的 type 从 importmap 改为 systemjs-importmap
  • 接着把 es-module 的 type 从 module 改为 systemjs-module
  • 最后是改动最大的地方,在 es-module 中我们不再使用 import 和 export 来导入导出模块,转而使用 systemjs 的语法,不过不用担心, webpack 和 rollup 等打包工具现在都支持将代码打包成 systemjs 风格,所以我们在写代码的时候还是可以按照正常规范来写

架构设计

到这里我们的前置概念就介绍完了,可以准备开始正式的拆分工作了,不过在拆分开始前,我们需要提前设计好我们的基础设施架构和代码组织方式。

基础设施架构

基于 single-spa 加上 import-map,我们最后计划好的基础设施架构大概长这个样子:

arch-of-micro-fe

arch-of-micro-fe

  1. 首先我们前端的所有静态资源都会分别部署在 AWS 的 S3 服务中,其中唯一的一份 HTML 文件存放在 root 容器的 S3 中。
  2. 当用户访问我们的网站时,流量会从 client 端到达 root 容器的 AWS S3,这个时候用户的浏览器会先加载根路径下的 HTML 页面,而 HTML 页面的 head 标签中有一份 import-map 的 script。
  3. 这时候 client 会再发送一次请求到我们的 import-map 所在的 S3 拿到 import-map。
  4. 然后我们在 body 标签中用 systemjs 引入 root 容器,整个 APP 开始运转,之后根据不同的路径去不同的 S3 拿对应的静态文件

部署策略

为了能够达到各个团队独立自治的目的,部署是必不可缺的一环,我们的最终目的是不同的团队部署不会影响其他团队的业务。一个团队的线上代码出了问题,其他团队的业务仍可正常运行,对于一个 to B 的项目来说,这样的规划是有意义的。

delpoy-plan

delpoy-plan

基于这个目的,每一个团队自己维护自己的 app 的 CI/CD pipeline。需要特别注意的是,在每一次部署后需要更新 import-map 自己团队对应的 app 地址,这样还可以达到版本管理的目的。只要 S3 中一直存放着某一个版本的静态资源,仅仅更新 import-map 的对应地址即可达到快速部署和回滚的目的。

pipeline-stage

pipeline-stage

本地开发策略

在本地开发时有两种策略,一种是直接在本地启动一个 root 容器,然后将本地的 APP 注册到 root 容器中。

但是这样的开发方式需要解决依赖问题,比如 APP 依赖的通用方法库、通用组件库。解决这些依赖问题也有两个办法,一个是直接将对应的依赖打包,在本地进行配置,本地开发时直接引用打包好的依赖;第二个方式是将这些依赖作为一个共享 APP 直接在本地作为一个类似于 server 一样运行,然后通过 import-map 来共享,在开发时直接引用导出的方法和组件,而 single-spa 也提供了这样的方式,感兴趣的读者可以通过这个链接详细了解。

第二种方式则要简单许多,并且开发体验也会好很多。通常我们都有开发环境。我们可以直接在线上开发环境的 import-map 开一个口,利用 import-map-overrides 这个工具把线上的 import-map 对应的那个 APP 地址覆盖成本地地址。这时线上通过 import-map 去寻找这个 APP 的时候就会直接请求你的本地某个地址,然后线上运行的代码其实就已经是你本地的代码了,可以无缝与各种依赖开发。

你可能会觉得有安全问题,但其实这个工具可以做一些配置,比如只在本地和某一个域名下才打开这个口子,在别的地方都不开放这个后门。

实际拆分

problem

problem

讲了这么多,终于开始上手了,但是这个世界上有一句名话叫做理想很丰满,现实很骨感。当你兴致勃勃准备好了一切计划,现实一般都不会让你如愿。我们这些看起来都还不错的计划有一部分被金主爸爸暂时搁置了,有一部分由于设计不妥开发体验不佳也被改造了。

太贵了

成本永远是和金主爸爸谈判绕不开的话题,我们新的架构设计在单体前端的基础上增加了许多东西:

  • 多 repo(当然这个不算钱,也就没啥阻碍,但是最终也没有用多 repo 的方案,这个后面再聊
  • 多 pipeline
  • 多部署资源(每一个 APP 使用单独的 S3
  • 多出来的 import map service

以前 10 块钱就能干完的活,你这么一搞我得出 100 块了吧,你这么玩我的钱包很难办啊

金主爸爸如是说。这种情况下我们就需要和金主爸爸谈判,为什么这些东西是必要的,为什么我们需要加这么多资源。但项目的问题在于,我们没时间谈判了,所以决定采取“架构降级”:

  • 先暂时用一条 pipeline 来 build 我们的 app,在下一期项目有足够证据的时候切分 pipeline

    • 这一决定在后来验证是完全错误的,设想一下一个内存只有 1G 的 agent,需要 build 一个有 5 个 APP 的前端项目
    • 同时由于金主爸爸的钱包问题,我们项目只有一个 agent,请想象一下我们的日常开发hhhhh
  • 先暂时将所有 app 部署到同一个地方,以文件夹分隔,如果一段时间后发现能满足需求,就先保持原状

  • 每次 build 生成一份 import map,不单独维护 import map 资源,当团队相互影响时再寻求拆分时机

repo 拆分问题

我们一开始的设想是一个单独的 APP 拆分为一个单独的 repo,真正上手的时候仔细一想,有必要吗?

这让我回想起了一期项目时后端的微服务 repo,由于是一期项目,不同微服务之间的调用需要 setup,所以大多数时候本地都打开了三个以上的 Intellij,加上乱七八糟的其他应用,不得不说对 16G 内存的 Macbook 是一个考验。

回到前端这边,极有可能我们在日常的开发过程中会频繁抽取/更改公用代码库,也就意味着我们需要频繁提交更改,更新版本,然后才能使用,想想都不想做了。

再者,目前两个团队的体量其实还不必如此细致的拆分

有必要吗 - 繁琐的开发流程 - 多个本地 idea

公用代码难以维护 - 不同repo 不同更改 - ts类型引用问题

跨业务页面拆分问题

最初的设想是一条业务线是一个单独的 APP,一些跨业务的页面(也就是每一个业务都会有的页面,比如 User Account Management)也会被单独抽取一个 APP。

我们也真的这么做了,然后小伙伴们就戴上了痛苦面具:

  • “BA 说这个页面是统一的,这个业务的改动,那个业务也要改。” “抽!”
  • “BA 说这个新的页面要独立,所有新功能要在所有业务中生效。” “抽!”
  • ......

“这个公共页面的逻辑跟那边的逻辑是一样的,我们是 copy 一份?” “......”

这样的策略导致我们的项目中存在大量 APP,而这些 APP 仔细一想好像没必要啊。增加 build 成本的同时也增加了我们自己的开发和维护成本,这拆的本末倒置了,于是我们做了一个改进 - 将所有公共页面塞进了一个 APP 中。

这个方案咋一听怪怪的,但是真的这么做了以后发现真香。所有的改动都会在所有的业务生效,不同的业务用不同的权限限制,大家维护同意份代码。等一下,你刚刚不是说不想大家维护同一份代码怕冲突吗?

这里的情况恰恰相反,所有的改动和需求都需要在所有地方生效,这样的方式我们就不用维护多份代码,而且也不会造成冲突 - 因为需求方的需求是单向的,如果有冲突,那就是需求冲突了,需要金主爸爸自己内部去掰头了。

可能有的小伙伴会说,怎么不试试后端拆分方式,使用 DDD 来指导拆分呢?巧了么不是,一开始我们就是按照后端 DDD 的方式来指导拆分的,然后就发生了这些问题,至少在我们的实践过程中,微服务的拆分方式不能照搬到前端来。

CSS 冲突问题

这是我们遇到的另一个比较严重的问题。我们在项目中使用了 Material UI,其中的 CSS 使用的是 CSS-in-JS 的方式,又因为有一套自己的 class name 生成规则,在没有控制好 scope 的情况下,多个 APP 的样式名冲突了,导致了严重的互相影响。

这虽然不是 single-spa 的问题,但是 single-spa 也提供了一些解决方案,包括 JS lib 和 CSS 的隔离问题,这些方案可以轻易地在官网或者 github issue 里面搜索到,这里就不过多解释了。解决的关键在于使用不同的 JS 或者 CSS 方案要做好相应的隔离。

写在最后

以上大概就是我们在拆分微前端过程中遇到的还记得住的事情了,从这次拆分中给我最大的益处其实不是技术上的提升,而是让我明白了做项目的两个关键点:

  • 所有事情不会原封不动按照你的计划执行,越大的事情越是这样,及时考虑突发事件,灵活应变,不要拘泥于设计,基于现实改变计划才是可行之策。
  • 架构的演进应该逐步推进,稳步前行,没有必要在一次架构演进中考虑好未来的所有情况,先不说你能不能考虑周全,谁又能说未来的情况不会发生改变呢,不要以现在的情况去揣度未来的情景,过好当下,灵活设计,提前预防未来可能发生的状况,准备好plan B即可。
原文:https://juejin.cn/post/7007774421502935054
收起阅读 »

配置一个好看的PowerShell

工作学习生活中不免要经常用到 PowerShell ,但是那深蓝色的背景实在让人想吐槽几句。今天我们就来美化一下它,几十种花里胡哨的主题任你选择~准备首先我们要下载 Windows Terminal,打开微软商店搜索或者在Gith...
继续阅读 »

工作学习生活中不免要经常用到 PowerShell ,但是那深蓝色的背景实在让人想吐槽几句。

今天我们就来美化一下它,几十种花里胡哨的主题任你选择~

image-20211017111042177

准备

  1. 首先我们要下载 Windows Terminal,打开微软商店搜索或者在Github搜索下载即可:

    image-20211017111722102

  2. Win11后,WSL又迎来了质的飞跃,你甚至可以直接在文件管理中看到它:

    image-20211017111555327

  3. 想修改 Windows Terminal 透明亚克力背景以及字体样式颜色也可以看我的上篇文章。

插一句,可以尝试一下新的 PowerShell 是跨平台的,挺好用

安装及修改

先贴出Oh My Posh官方文档

  1. 首先在命令行分别输入以下命令,中途询问输入Y确认即可:

    Install-Module oh-my-posh -Scope CurrentUser -SkipPublisherCheck
    Install-Module posh-git -Scope CurrentUser

  2. 直接来修改配置文件:

    notepad $PROFILE

  3. 会提示你新建一个文件,直接复制粘贴,然后保存退出重新启动即可。

    Import-Module posh-git
    Import-Module oh-my-posh
    Set-PoshPrompt -Theme agnosterplus

  4. 你可以通过 Get-PoshThemes 来查看所有可用主题,再次修改配置文件中第三行内容即可:

    image-20211017112633530

一些提示

  1. 你打开后可能会有些图标显示不出来,那是因为你的字体不支持,你可以下载Nerd Fonts ,或者下载官方推荐的Meslo LGM NF,使用字体需要修改Windows Terminal的设置文件,具体可以看我的上篇文章,我这里使用的是"MesloLGS NF"。

  2. 选择主题也可以直接输入 Set-PoshPrompt -Theme 主题名

  3. 这个主题在VScode中也可以适配显示,在设置中搜索 Integrated:font,修改字体即可:

    image.png

  4. 显示出来是这样的:

    image.png

  5. 暂时想不出来更多了,有问题可以评论然后我再补充~

原文:https://juejin.cn/post/7019878578703564807
收起阅读 »

web错误处理/错误捕获方案

前言花了一些时间整理完善项目的错误处理/错误捕获能力,借此进行一次总结。为了方便阅读,先概括下大概的思路:// 错误处理,避免报错导致程序无法继续执行1、自行对重要步骤进行容灾和try...catch...finally等处理;  2、通过打包工具(...
继续阅读 »



前言

花了一些时间整理完善项目的错误处理/错误捕获能力,借此进行一次总结。为了方便阅读,先概括下大概的思路:

// 错误处理,避免报错导致程序无法继续执行
1、自行对重要步骤进行容灾和try...catch...finally等处理;  
2、通过打包工具(我用的是vite,自己实现的是rollup的plugin)将绝大部分语句包裹try...catch语句,并补全错误信息(文件名,方法名);  

// 错误捕获
1、onerror 事件捕获全局错误;  
2、unhandledrejection 捕获异步错误;  
3、框架层面错误监听(e.g. Vue.config.errorHandler, react ErrorBoundary);  
3、axios等tcp/ip错误处理;  
4、静态资源加载异常捕获(window.addEventListener('error',(event)=>{});  

// 错误分析,补全错误信息
1、通过sourcemap解析错误信息,定位到具体错误代码;

错误处理

我实现的插件是rollup-plugin-trycatch,这个方案不算成熟:

涉及到业务代码的translate,单元测试覆盖面可能不足(目前仅处理几种类型函数,欢迎pr);

如果项目比较稳定,测试覆盖率较高,不建议使用此方案。

rollup-plugin-trycatch

跟webpack有些区别,rollup不分plugin和loader,只通过plugin来实现插件。主要流程如下:
1、创建rollup插件;
2、通过rollup的acorn插件将code转为ast语法树;
3、通过stack-utils插件将当前文件/文件名行数等信息添加到err里;
3、通过estree-walker遍历语法树,将相关的语句(函数)增加wrap节点;
4、通过escodegen插件将语法树转为code;
功能:
1、给所有函数(FunctionDeclaration, FunctionExpression, ArrowFunctionExpression, ObjectMethod)包一层try...catch捕获异常;
2、try...catch错误补全(增加报错信息,原文件名,方法名等错误分析,但仍不够精确,未能定位到具体报错的语句);
具体内容和使用方式可以查看源码,里面有vite的使用demo;

错误捕获

错误捕获的难点在于两个方面:
1、如何全面的捕获到错误;
2、如果通过错误分析到具体问题(一般来说线上代码都是打包压缩过的,如何通过打包压缩后的代码定位到问题);

全面(尽可能)的错误捕获

在我们尝试错误捕获之前,需要先了解有哪些错误,分三类(这里其实有很多分类的方式,我尝试用我自己的分类方式来解读):

1、远程资源加载错误;

window.addEventListener('error', (err) => {
let _url = ''

// 远程资源加载异常
if(err.target instanceof HTMLElement) {
  if(err.target instanceof HTMLAnchorElement) {
    _url = err.target.href
  } else {
    // maybe other htmlelement has src property
    _url = (err.target as HTMLImageElement).src
  }
  // 这里进行上报
  console.log(err, _url)
}
}, true) // 注意有三个参数,第三个参数表捕获阶段传播到该EventTarget时触发

2、同步代码错误 && 异步代码(setTimeout/setInterval,等);

window.onerror = (message, source, lineno, colno, error) => {
// 这里进行错误上报操作
console.log(message, source, lineno, colno, error)
}

3、异步代码错误;

// promise
window.addEventListener("unhandledrejection", (e) => {
// 这里进行上报
console.log(e.reason)
e.preventDefault() // 阻止错误冒泡
}, true)

4、框架提供的错误处理,e.g.

// vue3
app.config.errorHandler = (err, vm, info) => {
// 这里进行错误上报
console.log(err, vm, info)
}

5、xhr, fetch等异步请求方式;

// xhr
function xhr() {
const oReq = new XMLHttpRequest();
const url = "http://www.example.org/example.txt"
 
oReq.addEventListener("error", (err) => {
  console.log(err, url)
});
oReq.open("GET", url);
oReq.send();
}
// fetch
function fetchMethod() {
const url = 'http://www.example.org/example.txt'

fetch(url,
  {
    method: 'GET',
    mode: 'cors',
    cache: 'default'
  }
).then(data => {
  console.log(data)
}).catch(err => {
  // 错误处理
  console.log(url, err)
})
}

具体代码可参考 demo;

错误位置

我们的代码一般是打包压缩后的代码,错误提示的位置有时候很难定位到具体的内容,特别是很难重现的错误内容,我们常常需要更精准的错误信息进行定位。

对于以上方案以及对应的处理方式如下

1、rollup-plugin-trycatch插件wrap一层try...catch;  
在插件中已对错误记录路径,方法名等信息;  
2、远程静态资源;  
错误已记录路径信息;  
3、promise异步代码错误;  
建议自行全部加上catch处理错误;  
4、框架内部的错误处理;  
框架已提供错误定位信息(vue3, `info` is a Vue-specific error info, e.g. which lifecycle hook);  
5、xhr, fetch等;  
建议自行记录处理;  
5、同步代码错误 && 异步代码(setTimeout/setInterval,等);  
其实这个错误是主要错误之一,目前的方案是通过提前打包sourcemap来进行解析

sourcemap解析错误

这里主要介绍下如何实现解析功能(有些服务,e.g. sentry已提供sourcemap的服务,但我们是自己搭建的,所以需要自己来实现这个功能):
1、打包时候将最新的sourcemap覆盖上传到解析服务上(如果有不同版本的查询问题的需求,可以考虑多版本,我暂时没做);

现在大部分公司都是通过自动化工具(jenkins, gitlab等)在打包机进行打包编译,在打包成功后将sourcemap文件上传到解析的服务目录上即可(可以运维通过ssh上传,也可以自行搭建文件上传服务); 

2、通过source-map解析文件,返回具体错误位置;

// 需要传入参数,source, lineno, colno

解析sourcemap源码

问题记录

1、Error.prototype.stack是实验性功能,在不同浏览器,不同版本有不同的处理方式(包括try...catch, unhandledrejection等error都是Error的实例)。
可以参考stackoverflow兼容主流浏览器(未测试);
另外一种就是像我的plugin中自动化wrap try...catch方法时记录下行信息;对于promise,建议尽量全部通过catch处理(或变成同步代码async await);

2、rollup或者acorn并没有提供ast->code的方法,如何进行转换?
在修改ast后需要通过另外的插件来实现ast->code,较多人在issue里推荐的是escodegen。

3、estree-walker在jest调用时候出现module not found的问题;
用2.0.2版本是ok的, 有issue跟进

4、有一些浏览器支持但rollup尚未支持的实验属性需要慎用。

1、class 私有属性 相关issue: https://github.com/rollup/rollup/issues/4292,可以通过插件@rollup/plugin-typescript转化后使用;

参考文档

1、 React,优雅的捕获异常: juejin.cn/post/697438…
2、Allow plugin transforms to only return AST: github.com/rollup/roll…
3、source-map-demo: github.com/Joeoeoe/sou…

作者:vb
来源:https://juejin.cn/post/7046320743973388295

收起阅读 »

关于MobX,知无不言,言无不尽~

MobX 实践指南一、概览篇简介MobX 是一个专注于状态管理的库,在 React 世界的流行程度仅次于拥有官方背景的 Redux。但 MobX 有自己独特的优势,它通过运用透明的函数式响应编程使状态管理变得简单、高效、自由。MobX哲学任何源自应用状态的东西...
继续阅读 »

MobX 实践指南

一、概览篇

简介

MobX 是一个专注于状态管理的库,在 React 世界的流行程度仅次于拥有官方背景的 Redux。但 MobX 有自己独特的优势,它通过运用透明的函数式响应编程使状态管理变得简单、高效、自由。

MobX哲学

任何源自应用状态的东西都应该自动地获得。

核心原理

利用defineProperty(<=v5)或Proxy(v6)拦截对象属性的变化,实现数据的Observable,在 get 中依赖收集,set 中触发依赖绑定的监听函数。
假如你之前关注过 Vue.js、Knockout 等的一些 MVVM 框架的响应式原理,那么你应该会感到非常熟悉。是的,它们的原理如出一辙。

核心概念

不仅是原理,基础概念、顶层的 Api 设计也十分相似。Vue.js 中 data、computed、watch,几乎可以与 Mobx 中的observable-statecomputedreaction等概念一一对应。最大的不同是,MobX 通过 actions 约束对 state 的更新方式,实现了对状态的管理这一重要步骤。整体运行流程如下图所示。

alt 运行流程

安装

mobx 这个包,提供了 MobX 所有的与具体框架平台无关的基础 Api。比如(observable、makeObservable、action等)。

npm i mobx

如果在 react 中使用,需要添加针对 react 开发的包 mobx-react

npm i mobx mobx-react

如果你在 react 开发中,只使用函数式组件,没有使用类组件,那么可以将 mobx-react 替换为一个更轻量的包 mobx-react-lite

npm i mobx mobx-react-lite

相比mobx-react这个全量包,
1. 去掉了对class components的支持,
2. 并且移除了provider、inject
(原因:这两个HOC在React官方已经提供了React.createContext之后变得不是那么必要了)

二、实践篇

1. 声明Store

相比直接使用普通对象,MobX 更推荐使用的方式去创建 Store,主要原因是 class 对 TS 的类型系统更友好,更容易被索引实现自动补全等功能。

三种声明方式

方式一、直接使用普通对象的方式 (不推荐)

import { observable,action } from 'mobx'

const userStore = observable({
roleType:1
})

export const changeRoleType = action((val)=>{
userStore.roleType = val
})

export default userStore

方式二、使用类 + 装饰器 (V6 版本之前的推荐方式)

import { observable } from 'mobx'

class UserStore{
@observable roleType=1
@action changeRoleType(val){
this.data = val
}
}

export default UserStore

方式三、使用类 + makeObservable (V6 版本的推荐方式)(不再推荐装饰器的原因可以在Q&A章节找到)

import { makeObservable,observable,computed,action } from 'mobx'

class UserStore{
constructor(){
makeObservable(this,{
roleType:observable,
roleName:computed,
changeRoleType:action
})
}
roleType = 1
get roleName(){
return roleMap[roleType]
}
changeRoleType(val){
this.roleType = val
}
}
export default UserStore

//or

import { makeAutoObservable } from 'mobx'

class UserStore{
constructor(){
makeAutoObservable(this)
/* 无需显示的声明,会自动应用合适的MobX-Api去修饰。比如
(1)值字段会被推断为observable、
(2)get 修饰的方法,会推断为computed、
(3)普通方法,会自动应用action
(4)如果你有自定义调整某些字段的需求,请参考此方法的[其他入参](https://zh.mobx.js.org/observable-state.html#makeautoobservable)
*/
}
roleType = 1
get roleName(){
return roleMap[roleType]
}
changeRoleType(val){
this.roleType = val
}
}
export default UserStore

实现 store 间通信

例子:在一个角色管理的模块,因为自己拥有管理员权限,权力大到甚至能够更改自己的角色类型,那么果真这样操作时,就需要将RoleStore的修改同步到UserStore,这时就涉及到多个store间通信。
思路:创建一个公共的上级 rootStore,实现多个Store间的状态读取,方法调用。

// 用户信息Store
class UserStore{
constructor(rootStore){
this.rootStore = rootStore
makeAutoObservable(this)
}
uid = 'zyd123'
roleType = 1
changeRoleType(val){
this.roleType = val
}
}
// 角色管理Store
class RoleStore{
constructor(rootStore){
this.rootStore = rootStore
makeAutoObservable(this)
}
changeUserRoleType(uid,type){
const {userStore} = this.rootStore
//更改自己的角色类型
if(uid === userStore.uid){
//*** 同步UserStore ***
userStore.changeRoleType(type)
...
}else{
//更改别人的角色类型
...
}
}
}

// 新建一个上层rootStore,方便Stores间沟通
class RootStore {
constructor() {
this.userStore = new UserStore(this)
this.roleStore = new RoleStore(this)
}
}

const rootStore = new RootStore()
export default rootStore

2. 在React组件中使用

2.1 observer

作用:自动订阅在react组件渲染期间被使用到的可观察对象属性,当他们变化发生时,组件就会自动进行重新渲染。 前边在概览篇提到过MobX的核心能力就是能够将数据get中收集到的所有依赖,在set中一次性发布出去。在react场景中,就是要将状态与组件渲染建立联系,一旦状态变化,所有使用到此状态的组件都需要重新渲染,而这一切的关键就是observer。
用法如下:(demo:实现一个更改全局角色的功能,RoleManage组件负责更改,UserInfo组件负责展示)
src/demos/UserInfo.jsx

import { observer } from "mobx-react";
// 导入rootStore
import rootStore from './../store';
// 拿到对应的子Store
const { userStore } = rootStore;

class UserInfo extends Component {
render() {
//(1) 触发get,收集依赖(ps:当前组件已加入MobX的购物车)
const { roleName } = userStore;
return (
<Row justify="space-between">
<Col></Col>
<Col span={5} className='border'>
<Space align='center'>
<span>当前角色类型:</span>
<h2>{roleName}</h2>
</Space>
</Col>
</Row>
);
}
}
// (关键)observer HOC包裹住组件,将MobX强大的响应式更新能力赋予react组件。
export default observer(UserInfo)

src/demos/RoleManage.jsx

import rootStore from './../store'

const { userStore } = rootStore;

class RoleManage extends Component {
handleUpdateRoleType = ()=>{
//(2) 使用一个action去触发数据set,在set中发布依赖(触发组件更新,ps:Mobx要清空购物车啦)
userStore.changeRoleType(2)
}
render() {
return <Button onClick={this.handleUpdateRoleType}>更改角色</Button>
}
}
export default RoleManage;

2.2 Provider、inject

作用:刚才的例子中,大家可以看到全局Store的引入方式是文件的方式引入的。

import rootStore from './../store'

const { userStore } = rootStore;

这种方式繁琐且不利于维护,假如store文件重新组织,引入的地方需要处处更改与check。所以,有没有方式,在项目开发中Store只需一次注入,就可以在所有组件内非常便捷的引用呢?
答案就是使用 Provider、inject。
让我们重构上边的例子: src/index.jsx

import App from "./App";

import { Provider } from 'mobx-react'
import store from './store'
//利用Provider将Store注入全局
ReactDOM.render(
<Provider {...store}>
<App/>
</Provider>,
document.getElementById("root")
);

src/demos/UserInfo.jsx

class UserInfo extends Component {
render() {
//通过props的方式在render函数中引用
const { roleName } = this.props.userStore;

return (
<Row justify="space-between">
<Col></Col>
<Col span={5} className='border'>
<Space align='center'>
<span>当前角色类型:</span>
<h2>{roleName}</h2>
</Space>
</Col>
</Row>
);
}
}

// inject是高阶函数,所以inject('store')返回值还是个函数,最终入参是组件
export default inject('userStore')(observer(UserInfo))

Provider及inject看上去与react官方推出的context Api用法非常相似,要解决的问题也基本一致。
事实上,最新版的mobx-react,前者就是基于后者去做的封装,这也从侧面说明,这俩Api现在来看,并不是开发react应用的必需品。所以MobX官方在推出针对React平台的轻量包(mobx-react-lite)时,首先就把这俩api排除在外了。
但笔者认为,你如果使用的是class组件,Provider及inject依然建议使用,因为class组件内使用contextApi并不十分方便,但如果你用的hooks,则大可不必再使用Provider及inject了,得益于useContext的方便简洁,大大降低了使用他们的必要性(具体用法,后边会讲到)。

2.3 MobX + Hooks

函数组件+hooks是目前开发React应用的首选方式。MobX顺应趋势,推出了新的hook Api,这已经成为使用MobX的主流方式。

2.3.1 使用全局Store

自定义useStore替换Provider、inject 下边示例笔者会统一采用mobx-react-lite这个轻量包来编写。前边提到这个包并不提供Provider、inject,但是没有关系,有React官方提供的createContext及useContext就足够了。 下边我们自己动手封装一个好用的useStore-hook。
src/store/index.js

...

//创建rootStore的Context
export const rootStoreContext = React.createContext(rootStore)

/**
* @description 提供hook方式,方便组件内部获取Store
* @param {*} storeName 组件名字。作用类似inject(storeName),不传默认返回rootStore
*/

export const useStore = (storeName) => {
const rootStore = React.useContext(rootStoreContext)
if (storeName) {
const childStore = rootStore[storeName]
if (!childStore) {
throw new Error('根据传入storeName,找不到对应的子store')
}
return childStore
}
return rootStore
}

src/index.jsx

- import { Provider } from 'mobx-react'

+ import rootStore, {rootStoreContext} from './store'
+ const { Provider } = rootStoreContext

ReactDOM.render(
<Provider value={rootStore}>
<App/>
</Provider>,
document.getElementById("root")

src/demos/UserInfo.jsx

//换用更轻量的lite包
- import { observer } from "mobx-react";
+ import { observer } from "mobx-react-lite";
import { Row, Col, Space } from "antd";

+ import { useStore } from '../store';

// 函数式组件
const UserInfo = ()=> {
//使用自定义useStore获取全局store
const { roleName } = useStore('userStore')

return (
<Row justify="space-between">
<Col></Col>
<Col span={5} className='border'>
<Space align='center'>
<span>当前角色类型:</span>
<h2>{roleName}</h2>
</Space>
</Col>
</Row>
)
}
export default observer(UserInfo)

假如日常项目中,只希望MobX负责全局的状态管理,以上内容就完全够用了。下边我会介绍MobX+hook在局部状态管理方面的强大能力。
全局状态管理:store在组件外定义,经常放在全局一个单独的store文件夹。适合管理一些公共或者相对某模块是公共的状态。
局部状态管理:store常常定义在组件内部,适用于复杂的组件设计场景,用来解决组件多层嵌套下的状态层层传递、组件状态多且更新复杂等问题。

2.3.2 创建一个局部的Store

先介绍两个hook

useLocalObservable

作用:通过hook的方式声明一个组件内的Store,返回传入普通对象的响应式版本,并在函数组件之后的每一次渲染中保持对这个响应式对象的唯一引用(这点与useState是一致的)(useLocalStore是这个api的前身,但是将要废弃,这里不做介绍)。

useObserver

作用:前边讲的observer是HOC的方式,只能在外部通过包裹整个组件的方式去使用。想要在组件内部实现局部状态管理,在类组件中必须通过内置的Observer组件以renderProps的方式去解决,但在函数式中,hook一定是解决问题的首选,所以可以理解为useObserver是Observer的hook版实现。 示例:useLocalObservable + useObserver实现一个局部的状态管理 src/demos/UserInfoScopeStore.jsx

import { useLocalObservable, useObserver } from "mobx-react-lite";
import { Row, Col, Space,Button } from "antd";

const UserInfo = ()=> {
//定义组件内的响应式Store
const store = useLocalObservable(()=>({
name:'xxx',
changeName(text){
this.name = text
}
}))
// 对比以下两种组件内局部状态视图更新方式。
// useObserver
return useObserver(()=> <Row justify="space-between">
<Col></Col>
<Col span={5} className='border'>
<Space align='center'>
<span>当前用户:</span>
<h2>{store.name}</h2>
<Button onClick={()=>store.changeName('小米')}>修改</Button>
</Space>
</Col>
</Row>)
// or Observer
return <Observer>
{() => <Row justify="space-between">
<Col></Col>
<Col span={5} className='border'>
<Space align='center'>
<span>当前用户:</span>
<h2>{store.name}</h2>
<Button onClick={() => store.changeName('小米')}>修改</Button>
</Space>
</Col>
</Row>}
</Observer>
}
export default UserInfo

简单总结:(1)observer HOC的方式适合组件的整体更新场景(2)useObserver or Observer 都可用来处理局部的组件内更新场景,区别前者是hook的方式,只支持函数式组件,后者使用renderProps的方式,类与函数组件都兼容。

3. 开发者工具

chrome插件

三、Q&A

  1. IE项目能不能用?

V4版本默认可用,V5及以上如果需要兼容不支持Proxy的IE / React Native,请在应用初始化修改全局配置useProxies

import { configure } from "mobx"
// 如果需要兼容ie或rn,请通过全局配置,禁止使用代理
configure({ useProxies: "never" })

  1. 为什么MobX新的V6版本,不再推荐类的装饰器语法,而是建议用makeObservable的方式去修饰Store?

不再推荐装饰器的理由:因为装饰器语法尚未定案,纳入 ES 标准的时间遥遥无期,且未来制定的标准可能与当前的装饰器实现方案有所不同。所以出于兼容性,MobX 6中不推荐使用装饰器,并建议使用 makeObservable / makeAutoObservable 代替。但项目中如果使用的是 TS,笔者认为可以基本忽略影响,毕竟装饰器确实使用起来更简洁一些。

  1. 为什么我的组件并没有随着Store数据的更新而更新?

(1)忘记了observer,useObserver的包裹(大部分原因都是这个)。 (2)defineProperty的响应式方案会有一些针对数组和对象的限制,需要格外注意,必要时候需要使用mobx提供的set方法来解决。 (3)只要你始终传递响应式对象的引用,observer就可以很好的工作,如果只是传递属性值,就造成了响应式丢失,常发生在使用ES6解构的场景,或只传个响应式对象的属性进去。如果读者了解vue3,那么其中的toRefs就是为了解决类似的问题,但是Mobx中你可以通过下边的例子避免这种情况。

   //错误 ❌
const TimerView = observer(({ secondsPassed }) => <span>Seconds passed: {secondsPassed}</span>)

React.render(<TimerViewer secondPassed={myTimer.secondsPassed} />, document.body)

// 正确 🙆
const TimerView = observer(({ myTimer }) => <span>Seconds passed: {myTimer.secondsPassed}</span>)

React.render(<TimerViewer secondPassed={myTimer} />, document.body)
```

4. **必须要通过action去更新Store?**
原理上不必要,原则上必要。你直接**mutable**的方式直接更改Store也是能够触发响应式更新,但是mobx强烈不建议你这样做,因为你会丢失以下好处:
(1) 能够清晰表达出一个函数修改状态的意图,有**利于项目维护**
(2) action结合开发者工具,提供了非常**有用的调试**信息
当启用**严格模式**时,修改store状态需要强制使用action,参见全局配置enforceActions。MobX并不像redux那样,从原理上就限制了state的更新方式,只能靠这种约定的方式去限制。所以**强烈建议开启此选项**。

5. **频繁使用observer,会不会出现性能问题?**
当组件相关的 observable 发生变化时,组件将自动重新渲染,反之,它能够确保在没有相关更改时组件不会重新渲染。真正做到了组件的按需渲染,在实践中,这使得 MobX 应用程序开箱即用地进行了很好的优化,它们通常不需要任何额外的代码来防止过度渲染。

6. **MobX相比Redux最大的优势是什么?**
具体来说:MobX的开箱即用,简洁灵活,对现有项目侵入小,这都是相比Redux的优势方面。
抽象来讲:MobX相比Redux,它天然对实体模型是友好的,它在内部巧妙的借助拦截代理把数据做了observable转换,让你依然在使用层面感知到的是实体模型,但是它却拥有了响应式能力,这就是mobx最厉害的地方,它适合抽象**领域模型**!
## 结尾
以上所有例子都可在这个[github仓库](https://github.com/FEyudong/mobx-study.git)找到。
# END THANKS~

原文:


https://juejin.cn/post/6979095356302688286

收起阅读 »

js 实现双指缩放

前言随着智能手机、平板电脑等触控设备的普及,交互方式也发生了改变。相对于使用鼠标和键盘进行交互的电脑,触控设备可以直接使用手指进行交互,而且基本上都支持多点触控。多点触控最常见的操作莫过于双指缩放了。比如双指缩放网页大小、朋友圈双指缩放图片进行查看。那么如此常...
继续阅读 »

前言

随着智能手机、平板电脑等触控设备的普及,交互方式也发生了改变。相对于使用鼠标和键盘进行交互的电脑,触控设备可以直接使用手指进行交互,而且基本上都支持多点触控。多点触控最常见的操作莫过于双指缩放了。比如双指缩放网页大小、朋友圈双指缩放图片进行查看。那么如此常见的手势操作,你有没有想过它是如何实现的呢?下面跟着我一探究竟吧!

缩放原理

原理其实很简单,双指向外扩张表示放大,向内收缩表示缩小,缩放比例是通过计算双指当前的距离 / 双指上一次的距离获得的。详见下图:

p.jpg

计算出缩放比例后再通过下面两种方式实现缩放。

  1. 通过transform进行缩放
  2. 通过修改宽高来实现缩放

主流的方法都是采用transform来实现,因为性能更好。本篇文章两种方式都会介绍,任你选择。不过在讲之前,还是要先搞懂两个数学公式以及PointerEvent指针事件。因为接下来会用到。如果对PointerEvent指针事件不太熟悉的小伙伴,也可以看看这篇文章js PointerEvent指针事件简单介绍

两点间距离公式

设两个点A、B以及坐标分别为A(x1, y1)、B(x2, y2),则A和B两点之间的距离为:

e693d73856f43706273b0197b3cc42bf.svg

/**
* 获取两点间距离
* @param {object} a 第一个点坐标
* @param {object} b 第二个点坐标
* @returns
*/

function getDistance(a, b) {
const x = a.x - b.x;
const y = a.y - b.y;
return Math.hypot(x, y); // Math.sqrt(x * x + y * y);
}

中点坐标公式

设两个点A、B以及坐标分别为A(x1, y1)、B(x2, y2),则A和B两点的中点P的坐标为:

4a36acaf2edda3cce013415d11e93901203f92dc.png

/**
* 获取中点坐标
* @param {object} a 第一个点坐标
* @param {object} b 第二个点坐标
* @returns
*/

function getCenter(a, b) {
const x = (a.x + b.x) / 2;
const y = (a.y + b.y) / 2;
return { x: x, y: y };
}

获取图片缩放尺寸

<img id="image" alt="">
const image = document.getElementById('image');

let result, // 图片缩放宽高
x, // x轴偏移量
y, // y轴偏移量
scale = 1, // 缩放比例
maxScale,
minScale = 0.5;

// 由于图片是异步加载,需要在load方法里获取naturalWidth,naturalHeight
image.addEventListener('load', function () {
result = getImgSize(image.naturalWidth, image.naturalHeight, window.innerWidth, window.innerHeight);
maxScale = Math.max(Math.round(image.naturalWidth / result.width), 3);
// 图片宽高
image.style.width = result.width + 'px';
image.style.height = result.height + 'px';
// 垂直水平居中显示
x = (window.innerWidth - result.width) * 0.5;
y = (window.innerHeight - result.height) * 0.5;
image.style.transform = 'translate3d(' + x + 'px, ' + y + 'px, 0) scale(1)';
});

// 图片赋值需放在load回调之后,因为图片缓存后读取很快,有可能不执行load回调
image.src='../images/xxx.jpg';

/**
* 获取图片缩放尺寸
* @param {number} naturalWidth
* @param {number} naturalHeight
* @param {number} maxWidth
* @param {number} maxHeight
* @returns
*/

function getImgSize(naturalWidth, naturalHeight, maxWidth, maxHeight) {
const imgRatio = naturalWidth / naturalHeight;
const maxRatio = maxWidth / maxHeight;
let width, height;
// 如果图片实际宽高比例 >= 显示宽高比例
if (imgRatio >= maxRatio) {
if (naturalWidth > maxWidth) {
width = maxWidth;
height = maxWidth / naturalWidth * naturalHeight;
} else {
width = naturalWidth;
height = naturalHeight;
}
} else {
if (naturalHeight > maxHeight) {
width = maxHeight / naturalHeight * naturalWidth;
height = maxHeight;
} else {
width = naturalWidth;
height = naturalHeight;
}
}
return { width: width, height: height }
}

双指缩放逻辑

// 全局变量
let isPointerdown = false, // 按下标识
pointers = [], // 触摸点数组
point1 = { x: 0, y: 0 }, // 第一个点坐标
point2 = { x: 0, y: 0 }, // 第二个点坐标
diff = { x: 0, y: 0 }, // 相对于上一次pointermove移动差值
lastPointermove = { x: 0, y: 0 }, // 用于计算diff
lastPoint1 = { x: 0, y: 0 }, // 上一次第一个触摸点坐标
lastPoint2 = { x: 0, y: 0 }, // 上一次第二个触摸点坐标
lastCenter; // 上一次中心点坐标

// 绑定 pointerdown
image.addEventListener('pointerdown', function (e) {
pointers.push(e);
point1 = { x: pointers[0].clientX, y: pointers[0].clientY };
if (pointers.length === 1) {
isPointerdown = true;
image.setPointerCapture(e.pointerId);
lastPointermove = { x: pointers[0].clientX, y: pointers[0].clientY };
} else if (pointers.length === 2) {
point2 = { x: pointers[1].clientX, y: pointers[1].clientY };
lastPoint2 = { x: pointers[1].clientX, y: pointers[1].clientY };
lastCenter = getCenter(point1, point2);
}
lastPoint1 = { x: pointers[0].clientX, y: pointers[0].clientY };
});

// 绑定 pointermove
image.addEventListener('pointermove', function (e) {
if (isPointerdown) {
handlePointers(e, 'update');
const current1 = { x: pointers[0].clientX, y: pointers[0].clientY };
if (pointers.length === 1) {
// 单指拖动查看图片
diff.x = current1.x - lastPointermove.x;
diff.y = current1.y - lastPointermove.y;
lastPointermove = { x: current1.x, y: current1.y };
x += diff.x;
y += diff.y;
image.style.transform = 'translate3d(' + x + 'px, ' + y + 'px, 0) scale(' + scale + ')';
} else if (pointers.length === 2) {
const current2 = { x: pointers[1].clientX, y: pointers[1].clientY };
// 计算相对于上一次移动距离比例 ratio > 1放大,ratio < 1缩小
let ratio = getDistance(current1, current2) / getDistance(lastPoint1, lastPoint2);
// 缩放比例
const _scale = scale * ratio;
if (_scale > maxScale) {
scale = maxScale;
ratio = maxScale / scale;
} else if (_scale < minScale) {
scale = minScale;
ratio = minScale / scale;
} else {
scale = _scale;
}
// 计算当前双指中心点坐标
const center = getCenter(current1, current2);
// 计算图片中心偏移量,默认transform-origin: 50% 50%
// 如果transform-origin: 30% 40%,那origin.x = (ratio - 1) * result.width * 0.3
// origin.y = (ratio - 1) * result.height * 0.4
// 如果通过修改宽高或使用transform缩放,但将transform-origin设置为左上角时。
// 可以不用计算origin,因为(ratio - 1) * result.width * 0 = 0
const origin = {
x: (ratio - 1) * result.width * 0.5,
y: (ratio - 1) * result.height * 0.5
};
// 计算偏移量,认真思考一下为什么要这样计算(带入特定的值计算一下)
x -= (ratio - 1) * (center.x - x) - origin.x - (center.x - lastCenter.x);
y -= (ratio - 1) * (center.y - y) - origin.y - (center.y - lastCenter.y);
image.style.transform = 'translate3d(' + x + 'px, ' + y + 'px, 0) scale(' + scale + ')';
lastCenter = { x: center.x, y: center.y };
lastPoint1 = { x: current1.x, y: current1.y };
lastPoint2 = { x: current2.x, y: current2.y };
}
}
e.preventDefault();
});

// 绑定 pointerup
image.addEventListener('pointerup', function (e) {
if (isPointerdown) {
handlePointers(e, 'delete');
if (pointers.length === 0) {
isPointerdown = false;
} else if (pointers.length === 1) {
point1 = { x: pointers[0].clientX, y: pointers[0].clientY };
lastPointermove = { x: pointers[0].clientX, y: pointers[0].clientY };
}
}
});

// 绑定 pointercancel
image.addEventListener('pointercancel', function (e) {
if (isPointerdown) {
isPointerdown = false;
pointers.length = 0;
}
});

/**
* 更新或删除指针
* @param {PointerEvent} e
* @param {string} type
*/

function handlePointers(e, type) {
for (let i = 0; i < pointers.length; i++) {
if (pointers[i].pointerId === e.pointerId) {
if (type === 'update') {
pointers[i] = e;
} else if (type === 'delete') {
pointers.splice(i, 1);
}
}
}
}

注意事项

由于transform书写顺序并不满足交换律,换句话说transform: translateX(300px) scale(2);和transform: scale(2) translateX(300px);是不相等的。开发时请根据相应的书写顺序做处理。详见下图:

微信图片_20210802192116.png


原文:https://juejin.cn/post/7020243158529212423

收起阅读 »

为什么祖传代码会被称为屎山

有一天,有几条虫子,干扰了老板赚钱,老板希望你能抓住它们。 你带着年轻的锐气,青春的活力,学艺多年积累的程序设计艺术,打开了公司的代码仓库。 远看,似乎一个运转的机器,巨大的代码堆积在一起形成了大致的轮廓,蠕动着前进。 凑近了一看,在不净的框架中,乱码般的语句...
继续阅读 »

有一天,有几条虫子,干扰了老板赚钱,老板希望你能抓住它们。


你带着年轻的锐气,青春的活力,学艺多年积累的程序设计艺术,打开了公司的代码仓库。


远看,似乎一个运转的机器,巨大的代码堆积在一起形成了大致的轮廓,蠕动着前进。


凑近了一看,在不净的框架中,乱码般的语句在运转,像生了麻风病的蛞蝓一样在喷吐,粘稠的水在流动,而穿着格子衫的人群则在焰柱旁围成了一个半圆,这就是码农的仪式。他们环绕着那不可名状植物,不断的伸手进去拨弄,又不断的掏出一些东西填上去,使他堆积的更高,为了防止到他,又掏出黏糊糊的糊糊,用力的涂抹,试图把它们黏在一起。


这是一个前人留下的屎堆起来的一个克苏鲁缝合怪,看起来摇摇欲坠,有无数的虫子爬来爬去。但勉强堆起了山一样的形体,蠕动着为老板赚钱。



你满心热血,要对这座山进行清理,使它成为一个鲁棒的钢铁巨兽,可以随时更换最新的部件,奔腾如飞,坚固异常,带着兄弟们走向人生巅峰。


你经过缜密的分析,顺着虫子留下的痕迹,终于找到了问题的源头,发现一坨很多年前某码农因为时代局限或者水平有限拉的陈年旧屎,你觉得只要对它改良一下,梳理清楚结构,加强判断与容错,就可以变化成一个钢铁部件,让这坨怪物离巨兽更近一步。


你用力的挖掘其中的信息,却发现,事情没有那么简单,这一坨实际上不是孤立的一坨,而是和整个山体融合在一起。或者说,这座山实际上是一坨坨粘稠滑腻的克苏鲁,通过无数的触角和粘液连接在了一起,这些克苏鲁伸出无数的触角,伸进这座山体中未知的角落。


有看起来结构相同,但是出现了几十上百次的重复逻辑。有无数道不知道伸向何处的判断分支。有七零八落到处都是又无法解释的神秘数字。有从表面直接伸向最底层的神秘调用。还有猜不出,看不懂,无法预计什么时候会触发,什么时候会爆发的无数定时器。还有无数神秘的线程在独立的挂在那里,猜不出哪个什么时候会忽然启动,什么时候会忽然挂起,什么时候会忽然互相抢资源而死锁,哪些资源会莫名其妙的被改动。神秘的链接,神秘的任务队列,神秘的池,神秘的环形缓存,神秘的堆栈。


他们耦合在一起,互相支撑,构成了一坨更大的克苏鲁屎怪,缓慢的蠕动。


你极其困难的清理和修改了其中的一点点内容,让这一点点的内容脱离出耦合态,看起来清晰一点。结果,忽然屎山对面十万八千行外,你永远意想不到的一块功能,忽然挂了。一个你完全在工作上没接触过的同事,通过他的盘查,发现是他维护的一个函数/方法、类、线程、内存块,池,和你改动的部分是深度耦合的,你的解耦导致了难以理解的错误使他们的部分产生了错误。于是你被骂了,你只能再退一步,在一个更小的范围内进行调整,但是发现,虫子不止是由这一块构成的,于是你追踪者虫子的足迹,去改良一个一个的模块。


在经历了一轮又一轮的批评,几乎结识了全公司所有模块的负责人之后,你终于抓住了一条虫子。但是在这个漫长的过程中,你早已忘却初心。在无数次的赶工加班熬夜的迷糊中,被同事老板挨骂后的愤懑中,表白失败/和女朋友吵架/发现自己头顶有点绿的低落中;无数次当做临时代码写下,计划单元测试完成后就重写却忘记的过程中,因为偷懒或者不舍得打断思路而而懒得抽出轮子而产生的超大代码块中。


留下了无数看起来结构相同,但是出现了几十上百次的重复逻辑。无数道不知道伸向何处的判断分支。大量的无法解释的神秘数字。从表面直接伸向最底层的神秘调用。猜不出,看不懂,无法预计什么时候会触发,什么时候会爆发的无数定时器。无数猜不出哪个什么时候会忽然启动,什么时候会忽然挂起,什么时候会忽然互相抢资源而死锁,莫名其妙改动资源的神秘线程。神秘的链接,神秘的任务队列,神秘的池,神秘的环形缓存,神秘的堆栈。


你要抓的哪条虫子确实抓出来了。然而,在你没看到的地方,随着运转,更多的新的虫子正在茁壮的成长。


这时,你突然发现你的脚抽不出来了,几条触手顺着你的腿向上攀延,你的手被深深地吸入泥沼一样的屎山,你使尽全力想要抽出胳膊,但越是挣扎,陷得越深,仿佛屎山中心有一个冰冷的黑洞,要将所有接近的物体吞噬殆尽。你的精气在一点点流失,一种极度的疲惫,但是又释然的感觉涌了上来。此刻,你觉得舒适又满足,渐渐地闭上了双眼,你甘愿奉献头发与生命,将自己化作一块补丁,维系着系统的苟延残喘。它再也没法离开你了,你和你的头发,成了它的一部分。


不知道过了多久。终于又有一条虫子在运行中暴露,干扰了老板赚钱。


老板又安排了一个年轻人来抓住这条虫子。这个年轻人带着锐气,青春和活力来到这座山前。


看到这摇摇欲坠的克苏鲁大山,不仅倒吸一口冷气。


“oh shit ! shit mountain !”



作者:码农出击666
链接:https://juejin.cn/post/7045924498461163533

收起阅读 »

我做了一款vuepress的音乐可视化播放插件

体验地址:博客,github,npm前言博客上的音乐播放器,大多都长一个样,小小的,塞在页面的一个角落里,在别人阅读文章的同时可以听音乐,增加某些体验的满意指数。而我,做了一件不太一样的事情:博客不就是让人看文章的么?再播放音乐甚至有可能会降低阅读的质量,那听...
继续阅读 »



体验地址:博客githubnpm

前言

博客上的音乐播放器,大多都长一个样,小小的,塞在页面的一个角落里,在别人阅读文章的同时可以听音乐,增加某些体验的满意指数。而我,做了一件不太一样的事情:

博客不就是让人看文章的么?再播放音乐甚至有可能会降低阅读的质量,那听歌就好好听歌不好么?既然要体验,那就沉浸体验到爽不好么?

某天,偶然打开了豆瓣FM网页版,很符合豆瓣的感觉,干净简洁,当然网上类似的音乐播放有很多,这里为我后面做的事情埋下了伏笔。

我博客是用 vuepress 搭建的,主题是 vuepress-reco,最开始想找一个播放音乐的插件,于是去找了 awesome-vuepress,搜到唯一和音乐相关的插件,只有一个叫:vuepress-plugin-music-bar 的插件.....还是个bar....有点失落。于是,没人做?那...我做个试试?最终的效果图就是上面看到的四张图了:亮/暗系歌词,亮暗系可视化解码。在看完 vuepress 官网的插件api,就开始搞了!

开搞

不管怎么画页面,初衷是沉浸式体验,找了很多播放器的大体结构,还是觉得网易云的播放界面算比较舒服的,自己也有尝试画过脑海里的播放界面,但是最终还是选择用网易云的效果(拿来吧你):左侧黑胶唱片滚动,右侧歌词滚动, 目前不需要上一曲下一曲,就有播放和分享按钮,也就是长这个样子:

一天半时间,匆匆忙忙做完之后,npm link 调试成功就发了一版npm包。好用?不好说。能不能用?能!

优化

做到这里之后,沉浸式有那么点感觉了,体验?照搬过来就是好的体验么?不,还是要加点东西,比如可视化

这里特别感谢网易云大前端团队的一篇文章:Web Audio在音频可视化中的应用,基本上照着看下来,里面的文献也看一下,就可以做出来上面的效果。说实话,文献是真头大....波长,正余弦,频域时域,奈奎斯特定理,还有什么快速傅里叶变换,头发在偷偷的掉...顺便附上一张某个文献的截图:

不过不看这些也可以做出来!

基本上的思路就是:

  1. 创建 AudioContext,关联音频输入,进行解码、控制音频播放和暂停

  2. 创建 analyser ,获取音频的频率数据(FrequencyData)和时域数据(TimeDomainData)

  3. 设置快速傅里叶变换值,信号样本的窗口大小,区间为32-32768,默认2048

  4. 创建音频源,音频源关联到分析器,分析器关联到输出设备(耳机、扬声器等)

  5. 获取频率数组,转格式,然后用 requestAnimationFrame 通过 canvas 画出来

这些东西上面的文章里讲的很详细,我这种门外汉就不多说啥了。

遇到的问题

npm link

之前使用 npm link 的时候,依赖包没有三方/四方的依赖,所以没注意到,如果开发的npm包带有别的依赖,那么调试的时候要在主项目里的 package.json 先加上这些包,就不会报错说 resolve 失败什么的了,调试结束记得 npm unlink 断开。

接口

本来想用的是 网易云音乐 NodeJS 版 API,但是有些东西不好找,比如我需要歌曲id,封面和歌词,但是文档里没有歌曲id反查专辑id的(封面在专辑id里),只有一个歌曲详情的,但是这个接口,还需要认证跳转....对于使用者来说,我没必要让使用者多这么一步操作,而且很容易出错。于是就换了一个api:保罗API,这个API可以解析的网易云歌曲不是那么的多,不过一般的够了,唯一的缺点就是,多频次刷新会一直 pending,应该是后端设置了ip频次。

既然都有问题,不使用接口行么?尝试找一种 mp3 文件解析出来歌词和封面呢?找到一个 jsmediatags 的仓库,可以解析ID3v2,MP4,FLAC等字段,但是.....这不就是给用户添加麻烦么?需要找专辑,歌词,歌曲,艺人信息全部合一的音源文件....如果我是用户,我不会用它。

翻来覆去,最终还是决定,歌曲用户传进来,然后再传一个歌曲id,封面和歌词走接口,歌曲就是传进来的音源链接,使用方法如下:

<MusicPlayer musicId="xxx" musicSrc="xxx.mp3" style="margin:0 auto">

音源我个人建议要么放vuepress的静态资源,要么就搞成类似图床一样的音源仓库,这样也好维护。

后期想办法优化吧。

主题色

亮系和暗系是适配 vuepress-reco 的主题切换做的适配。

结尾

灵感来自 豆瓣FM,结构参考了昊神的 音乐播放器,可视化播放参考了 Web Audio在音频可视化中的应用,接口感谢 保罗API,这么一说我好像也没做什么事.....

该插件已发npm包,awesome-vuepress 仓库也已收录,可能多少还会有点体验上的小问题,会慢慢修复的。大家也可以提建议,能听进去算我输!

项目写的匆匆忙忙,希望可以做一点更有深度的东西吧——致自己。


作者:道道里
来源:https://juejin.cn/post/7045944008190722079

收起阅读 »

微信小程序反编译获取源码

文章目录 前言一、前置条件二、操作步骤1.进入adb shell2.提取源码编译文件3.反编译前言 对微信小程序进行源码反编译,一般目的为:获取js签名算法,过数据包的防篡改策略获取接口的判断逻辑,一般用于修改返回包来达到未授权的效果,在尝试无法找到争取的返回...
继续阅读 »



文章目录

前言

对微信小程序进行源码反编译,一般目的为:

  • 获取js签名算法,过数据包的防篡改策略

  • 获取接口的判断逻辑,一般用于修改返回包来达到未授权的效果,在尝试无法找到争取的返回值的时候,需要从源码来进行构造

本文旨在记录如何对一个微信小程序进行编译获取其源码,后续分析不做分享,因目的不同,分析的方式也会不同

一、前置条件

需要一台root了的安卓测试手机,root的方式请自行查找。如果是红米手机,可以参考我的root方式,博客链接

本文演示的步骤,基于macbook m1进行,其他设备操作基本也差不多

二、操作步骤

1.进入adb shell

命令如下(示例):

(base)   ~ adb shell
davinci:/ $ whoami
shell
# 提权
davinci:/ $ su root
davinci:/ # whoami
root

2.提取源码编译文件

代码如下(示例):

davinci:/ # cd /data/data/com.tencent.mm                                                                                                               
davinci:/data/data/com.tencent.mm # ls
982178cdd5589cb042c4efb99be0333c  WebNetFile                        ipcallCountryCodeConfig.cfg  recovery        version_history.cfg
CheckResUpdate                    appbrand                          last_avatar_dir              regioncode      webcompt
ClickFlow                         autoauth.cfg                      luckymoney                   snsreport.cfg   webservice
CompatibleInfo.cfg                channel_history.cfg               media_export.proto           staytime.cfg    webview_tmpl
CronetCache                       configlist                        mmslot                       systemInfo.cfg
NowRev.ini                        deviceconfig.cfg                  mobileinfo.ini               textstatus
ProcessDetector                   ee1da3ae2100e09165c2e52382cfe79f  newmsgringtone               tmp
WebCanvasPkg                      heavy_user_id_mapping.dat         patch_ver_history.bin        trace

重点关注一个很长的用户随机码,比如ee1da3ae2100e09165c2e52382cfe79f和982178cdd5589cb042c4efb99be0333c,分别访问判断即可

davinci:/data/data/com.tencent.mm/MicroMsg # cd ./982178cdd5589cb042c4efb99be0333c/                                                        
davinci:/data/data/com.tencent.mm/MicroMsg/982178cdd5589cb042c4efb99be0333c # cd appbrand/                                                          
davinci:/data/data/com.tencent.mm/MicroMsg/982178cdd5589cb042c4efb99be0333c/appbrand # ls
pagesidx  pkg  web_renderingcache
davinci:/data/data/com.tencent.mm/MicroMsg/982178cdd5589cb042c4efb99be0333c/appbrand # cd pkg/  
davinci:/data/data/com.tencent.mm/MicroMsg/982178cdd5589cb042c4efb99be0333c/appbrand/pkg # ls
_-1223314631_166.wxapkg  _-1991183043_171.wxapkg  _-86252332_166.wxapkg   _1233860900_205.wxapkg  _1233860900_230.wxapkg  _2106768478_166.wxapkg
_-1223314631_167.wxapkg  _-289032338_166.wxapkg   _-86252332_167.wxapkg   _1233860900_206.wxapkg  _1233860900_231.wxapkg  _2106768478_171.wxapkg
_-1223314631_168.wxapkg  _-289032338_167.wxapkg   _-86252332_168.wxapkg   _1233860900_207.wxapkg  _1233860900_232.wxapkg  _288413523_8.wxapkg

这些wxapkg即编译后的小程序源码,为了准确找到目标小程序对应的wxapkg文件,可以重新访问目标小程序,之后对这些包进行排序,找到最新的

davinci:/data/data/com.tencent.mm/MicroMsg/982178cdd5589cb042c4efb99be0333c/appbrand/pkg # ls -lt                                                    
total 238092
-rw------- 1 u0_a239 u0_a239   907997 2021-12-26 15:25 _255193015_171.wxapkg
-rw------- 1 u0_a239 u0_a239   427489 2021-12-25 09:37 _1245338104_171.wxapkg
-rw------- 1 u0_a239 u0_a239   258272 2021-12-25 09:35 _2106768478_171.wxapkg
-rw------- 1 u0_a239 u0_a239   745490 2021-12-25 09:35 _927440678_171.wxapkg

找到了目标文件之后,需要将其挪到电脑的目录下。mac环境下,可以下载一个Android文件传输工具,之后通过mv命令,将该文件移动到可访问的目录,即可拖到电脑目录下

mv /data/data/com.tencent.mm/MicroMsg/982178cdd5589cb042c4efb99be0333c/appbrand/pkg/_255193015_171.wxapkg  /mnt/sdcard/Download

注意:有些情况下是分包,需要删除pkg目录下所有文件,重新访问该小程序,之后将所有的wxapkg都移动出来

davinci:/data/data/com.tencent.mm/MicroMsg/982178cdd5589cb042c4efb99be0333c/appbrand # mv /data/data/com.tencent.mm/MicroMsg/982178cdd5589cb042c4efb99be0333c/appbrand/pkg/*  /mnt/sdcard/Download/wxpkg           

davinci:/mnt/sdcard/Download/wxpkg # ls
_-1223314631_171.wxapkg _-1820590985_171.wxapkg _-372062782_171.wxapkg _1123949441_612.wxapkg _255193015_171.wxapkg
_-1325581962_171.wxapkg _-1991183043_171.wxapkg _-86252332_171.wxapkg   _2041131240_171.wxapkg _453111957_171.wxapkg
_-1536422934_171.wxapkg _-289032338_171.wxapkg   _-942297262_171.wxapkg _2106768478_171.wxapkg _927440678_171.wxapkg

3.反编译

使用wxappUnpacker

# 安装依赖(具体参考下官方github)
npm install

node wuWxapkg.js /Users/spark/tools/安卓武器库/_255193015_171.wxapkg

忽略分包爆错,直接进去格式化一下js,Ctrl+F搜索接口

作者:Sp4rkW
来源:https://blog.csdn.net/wy_97/article/details/122155518

收起阅读 »

解决小程序里面的图片之间有空隙的问题

1、将图片转换为块级对象  即,设置img为:  display:block;  在本例中添加一组CSS代码:  #sub img {display:block;}2、设置图片的垂直对齐方式  即设置图片的vertical-align属性为“top,text-...
继续阅读 »

1、将图片转换为块级对象

  即,设置img为:

  display:block;

  在本例中添加一组CSS代码:

  #sub img {display:block;}

2、设置图片的垂直对齐方式

  即设置图片的vertical-align属性为“top,text-top,bottom,text-bottom”也可以解决。如本例中增加一组CSS代码:

  #sub img {vertical-align:top;}

3、设置父对象的文字大小为0px

  即,在#sub中添加一行:

  font-size:0;

  可以解决问题。但这也引发了新的问题,在父对象中的文字都无法显示。就算文字部分被子对象括起来,设置子对象文字大小依然可以显示,但在CSS效验的时候会提示文字过小的错误。

4、改变父对象的属性

  如果父对象的宽、高固定,图片大小随父对象而定,那么可以设置:

  overflow:hidden;

  来解决。如本例中可以向#sub中添加以下代码:

  width:88px;height:31px;overflow:hidden;

5、设置图片的浮动属性

  即在本例中增加一行CSS代码:

  #sub img {float:left;}

  如果要实现图文混排,这种方法是很好的选择。

6、取消图片标签和其父对象的最后一个结束标签之间的空格。

原文:https://blog.csdn.net/Function_JX_/article/details/79588578

收起阅读 »

傻傻分不清之 Cookie、Session、Token、JWT

什么是认证(Authentication)通俗地讲就是验证当前用户的身份,证明“你是你自己”(比如:你每天上下班打卡,都需要通过指纹打卡,当你的指纹和系统里录入的指纹相匹配时,就打卡成功)互联网中的认证:用户名密码登录邮箱发送登录链接手机号接收验证码只要你能收...
继续阅读 »



什么是认证(Authentication)

  • 通俗地讲就是验证当前用户的身份,证明“你是你自己”(比如:你每天上下班打卡,都需要通过指纹打卡,当你的指纹和系统里录入的指纹相匹配时,就打卡成功)

  • 互联网中的认证:

    • 用户名密码登录

    • 邮箱发送登录链接

    • 手机号接收验证码

    • 只要你能收到邮箱/验证码,就默认你是账号的主人

什么是授权(Authorization)

  • 用户授予第三方应用访问该用户某些资源的权限

    • 你在安装手机应用的时候,APP 会询问是否允许授予权限(访问相册、地理位置等权限)

    • 你在访问微信小程序时,当登录时,小程序会询问是否允许授予权限(获取昵称、头像、地区、性别等个人信息)

  • 实现授权的方式有:cookie、session、token、OAuth

什么是凭证(Credentials)

  • 实现认证和授权的前提

    是需要一种

    媒介(证书)

    来标记访问者的身份

    • 在战国时期,商鞅变法,发明了照身帖。照身帖由官府发放,是一块打磨光滑细密的竹板,上面刻有持有人的头像和籍贯信息。国人必须持有,如若没有就被认为是黑户,或者间谍之类的。

    • 在现实生活中,每个人都会有一张专属的居民身份证,是用于证明持有人身份的一种法定证件。通过身份证,我们可以办理手机卡/银行卡/个人贷款/交通出行等等,这就是认证的凭证。

    • 在互联网应用中,一般网站(如掘金)会有两种模式,游客模式和登录模式。游客模式下,可以正常浏览网站上面的文章,一旦想要点赞/收藏/分享文章,就需要登录或者注册账号。当用户登录成功后,服务器会给该用户使用的浏览器颁发一个令牌(token),这个令牌用来表明你的身份,每次浏览器发送请求时会带上这个令牌,就可以使用游客模式下无法使用的功能。

什么是 Cookie

  • HTTP 是无状态的协议(对于事务处理没有记忆能力,每次客户端和服务端会话完成时,服务端不会保存任何会话信息):每个请求都是完全独立的,服务端无法确认当前访问者的身份信息,无法分辨上一次的请求发送者和这一次的发送者是不是同一个人。所以服务器与浏览器为了进行会话跟踪(知道是谁在访问我),就必须主动的去维护一个状态,这个状态用于告知服务端前后两个请求是否来自同一浏览器。而这个状态需要通过 cookie 或者 session 去实现。

  • cookie 存储在客户端: cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。

  • cookie 是不可跨域的: 每个 cookie 都会绑定单一的域名,无法在别的域名下获取使用,一级域名和二级域名之间是允许共享使用的靠的是 domain)

cookie 重要的属性

属性说明
name=value键值对,设置 Cookie 的名称及相对应的值,都必须是字符串类型 - 如果值为 Unicode 字符,需要为字符编码。 - 如果值为二进制数据,则需要使用 BASE64 编码。
domain指定 cookie 所属域名,默认是当前域名
path指定 cookie 在哪个路径(路由)下生效,默认是 '/'。 如果设置为 /abc,则只有 /abc 下的路由可以访问到该 cookie,如:/abc/read
maxAgecookie 失效的时间,单位秒。如果为整数,则该 cookie 在 maxAge 秒后失效。如果为负数,该 cookie 为临时 cookie ,关闭浏览器即失效,浏览器也不会以任何形式保存该 cookie 。如果为 0,表示删除该 cookie 。默认为 -1。 - 比 expires 好用
expires过期时间,在设置的某个时间点后该 cookie 就会失效。 一般浏览器的 cookie 都是默认储存的,当关闭浏览器结束这个会话的时候,这个 cookie 也就会被删除
secure该 cookie 是否仅被使用安全协议传输。安全协议有 HTTPS,SSL等,在网络上传输数据之前先将数据加密。默认为false。 当 secure 值为 true 时,cookie 在 HTTP 中是无效,在 HTTPS 中才有效。
httpOnly如果给某个 cookie 设置了 httpOnly 属性,则无法通过 JS 脚本 读取到该 cookie 的信息,但还是能通过 Application 中手动修改 cookie,所以只是在一定程度上可以防止 XSS 攻击,不是绝对的安全


什么是 Session

  • session 是另一种记录服务器和客户端会话状态的机制

  • session 是基于 cookie 实现的,session 存储在服务器端,sessionId 会被存储到客户端的cookie 中

session.png

  • session 认证流程:

    • 用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 Session

    • 请求返回时将此 Session 的唯一标识信息 SessionID 返回给浏览器

    • 浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名

    • 当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在 Cookie 信息,如果存在自动将 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。

根据以上流程可知,SessionID 是连接 Cookie 和 Session 的一道桥梁,大部分系统也是根据此原理来验证用户登录状态。

Cookie 和 Session 的区别

  • 安全性: Session 比 Cookie 安全,Session 是存储在服务器端的,Cookie 是存储在客户端的。

  • 存取值的类型不同:Cookie 只支持存字符串数据,想要设置其他类型的数据,需要将其转换成字符串,Session 可以存任意数据类型。

  • 有效期不同: Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,Session 一般失效时间较短,客户端关闭(默认情况下)或者 Session 超时都会失效。

  • 存储大小不同: 单个 Cookie 保存的数据不能超过 4K,Session 可存储数据远高于 Cookie,但是当访问量过多,会占用过多的服务器资源。

什么是 Token(令牌)

Acesss Token

  • 访问资源接口(API)时所需要的资源凭证

  • 简单 token 的组成: uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)

  • 特点:

    • 服务端无状态化、可扩展性好

    • 支持移动端设备

    • 安全

    • 支持跨程序调用

  • token 的身份验证流程:

img

  1. 客户端使用用户名跟密码请求登录

  2. 服务端收到请求,去验证用户名与密码

  3. 验证成功后,服务端会签发一个 token 并把这个 token 发送给客户端

  4. 客户端收到 token 以后,会把它存储起来,比如放在 cookie 里或者 localStorage 里

  5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 token

  6. 服务端收到请求,然后去验证客户端请求里面带着的 token ,如果验证成功,就向客户端返回请求的数据

  • 每一次请求都需要携带 token,需要把 token 放到 HTTP 的 Header 里

  • 基于 token 的用户认证是一种服务端无状态的认证方式,服务端不用存放 token 数据。用解析 token 的计算时间换取 session 的存储空间,从而减轻服务器的压力,减少频繁的查询数据库

  • token 完全由应用管理,所以它可以避开同源策略

Refresh Token

  • 另外一种 token——refresh token

  • refresh token 是专用于刷新 access token 的 token。如果没有 refresh token,也可以刷新 access token,但每次刷新都要用户输入登录用户名与密码,会很麻烦。有了 refresh token,可以减少这个麻烦,客户端直接用 refresh token 去更新 access token,无需用户进行额外的操作。

img

  • Access Token 的有效期比较短,当 Acesss Token 由于过期而失效时,使用 Refresh Token 就可以获取到新的 Token,如果 Refresh Token 也失效了,用户就只能重新登录了。

  • Refresh Token 及过期时间是存储在服务器的数据库中,只有在申请新的 Acesss Token 时才会验证,不会对业务接口响应时间造成影响,也不需要向 Session 一样一直保持在内存中以应对大量的请求。

Token 和 Session 的区别

  • Session 是一种记录服务器和客户端会话状态的机制,使服务端有状态化,可以记录会话信息。而 Token 是令牌访问资源接口(API)时所需要的资源凭证。Token 使服务端无状态化,不会存储会话信息。

  • Session 和 Token 并不矛盾,作为身份认证 Token 安全性比 Session 好,因为每一个请求都有签名还能防止监听以及重放攻击,而 Session 就必须依赖链路层来保障通讯安全了。如果你需要实现有状态的会话,仍然可以增加 Session 来在服务器端保存一些状态。

  • 所谓 Session 认证只是简单的把 User 信息存储到 Session 里,因为 SessionID 的不可预测性,暂且认为是安全的。而 Token ,如果指的是 OAuth Token 或类似的机制的话,提供的是 认证 和 授权 ,认证是针对用户,授权是针对 App 。其目的是让某 App 有权利访问某用户的信息。这里的 Token 是唯一的。不可以转移到其它 App上,也不可以转到其它用户上。Session 只提供一种简单的认证,即只要有此 SessionID ,即认为有此 User 的全部权利。是需要严格保密的,这个数据应该只保存在站方,不应该共享给其它网站或者第三方 App。所以简单来说:如果你的用户数据可能需要和第三方共享,或者允许第三方调用 API 接口,用 Token 。如果永远只是自己的网站,自己的 App,用什么就无所谓了。

什么是 JWT

  • JSON Web Token(简称 JWT)是目前最流行的跨域认证解决方案。

  • 是一种认证授权机制

  • JWT 是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准(RFC 7519)。JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。

  • 可以使用 HMAC 算法或者是 RSA 的公/私秘钥对 JWT 进行签名。因为数字签名的存在,这些传递的信息是可信的。

  • 阮一峰老师的 JSON Web Token 入门教程 讲的非常通俗易懂,这里就不再班门弄斧了

生成 JWT

jwt.io/
http://www.jsonwebtoken.io/

JWT 的原理

img

  • JWT 认证流程:

    • 用户输入用户名/密码登录,服务端认证成功后,会返回给客户端一个 JWT

    • 客户端将 token 保存到本地(通常使用 localstorage,也可以使用 cookie)

    • 当用户希望访问一个受保护的路由或者资源的时候,需要请求头的 Authorization 字段中使用Bearer 模式添加 JWT,其内容看起来是下面这样

Authorization: Bearer <token>
复制代码
  • 服务端的保护路由将会检查请求头 Authorization 中的 JWT 信息,如果合法,则允许用户的行为

  • 因为 JWT 是自包含的(内部包含了一些会话信息),因此减少了需要查询数据库的需要

  • 因为 JWT 并不使用 Cookie 的,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS)

  • 因为用户的状态不再存储在服务端的内存中,所以这是一种无状态的认证机制

JWT 的使用方式

  • 客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。

方式一

  • 当用户希望访问一个受保护的路由或者资源的时候,可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求头信息的 Authorization 字段里,使用 Bearer 模式添加 JWT。

    GET /calendar/v1/events
    Host: api.example.com
    Authorization: Bearer <token>
    复制代码
    • 用户的状态不会存储在服务端的内存中,这是一种 无状态的认证机制

    • 服务端的保护路由将会检查请求头 Authorization 中的 JWT 信息,如果合法,则允许用户的行为。

    • 由于 JWT 是自包含的,因此减少了需要查询数据库的需要

    • JWT 的这些特性使得我们可以完全依赖其无状态的特性提供数据 API 服务,甚至是创建一个下载流服务。

    • 因为 JWT 并不使用 Cookie ,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS)

方式二

  • 跨域的时候,可以把 JWT 放在 POST 请求的数据体里。

方式三

  • 通过 URL 传输

http://www.example.com/user?token=xxx
复制代码

项目中使用 JWT

项目地址

Token 和 JWT 的区别

相同:

  • 都是访问资源的令牌

  • 都可以记录用户的信息

  • 都是使服务端无状态化

  • 都是只有验证成功后,客户端才能访问服务端上受保护的资源

区别:

  • Token:服务端验证客户端发送过来的 Token 时,还需要查询数据库获取用户信息,然后验证 Token 是否有效。

  • JWT: 将 Token 和 Payload 加密后存储于客户端,服务端只需要使用密钥解密进行校验(校验也是 JWT 自己实现的)即可,不需要查询或者减少查询数据库,因为 JWT 自包含了用户信息和加密的数据。

常见的前后端鉴权方式

  1. Session-Cookie

  2. Token 验证(包括 JWT,SSO)

  3. OAuth2.0(开放授权)

常见的加密算法

image.png

  • 哈希算法(Hash Algorithm)又称散列算法、散列函数、哈希函数,是一种从任何一种数据中创建小的数字“指纹”的方法。哈希算法将数据重新打乱混合,重新创建一个哈希值。

  • 哈希算法主要用来保障数据真实性(即完整性),即发信人将原始消息和哈希值一起发送,收信人通过相同的哈希函数来校验原始数据是否真实。

  • 哈希算法通常有以下几个特点:

    • 正像快速:原始数据可以快速计算出哈希值

    • 逆向困难:通过哈希值基本不可能推导出原始数据

    • 输入敏感:原始数据只要有一点变动,得到的哈希值差别很大

    • 冲突避免:很难找到不同的原始数据得到相同的哈希值,宇宙中原子数大约在 10 的 60 次方到 80 次方之间,所以 2 的 256 次方有足够的空间容纳所有的可能,算法好的情况下冲突碰撞的概率很低:

      • 2 的 128 次方为 340282366920938463463374607431768211456,也就是 10 的 39 次方级别

      • 2 的 160 次方为 1.4615016373309029182036848327163e+48,也就是 10 的 48 次方级别

      • 2 的 256 次方为 1.1579208923731619542357098500869 × 10 的 77 次方,也就是 10 的 77 次方

注意:

  1. 以上不能保证数据被恶意篡改,原始数据和哈希值都可能被恶意篡改,要保证不被篡改,可以使用RSA 公钥私钥方案,再配合哈希值。

  2. 哈希算法主要用来防止计算机传输过程中的错误,早期计算机通过前 7 位数据第 8 位奇偶校验码来保障(12.5% 的浪费效率低),对于一段数据或文件,通过哈希算法生成 128bit 或者 256bit 的哈希值,如果校验有问题就要求重传。

常见问题

使用 cookie 时需要考虑的问题

  • 因为存储在客户端,容易被客户端篡改,使用前需要验证合法性

  • 不要存储敏感数据,比如用户密码,账户余额

  • 使用 httpOnly 在一定程度上提高安全性

  • 尽量减少 cookie 的体积,能存储的数据量不能超过 4kb

  • 设置正确的 domain 和 path,减少数据传输

  • cookie 无法跨域

  • 一个浏览器针对一个网站最多存 20 个Cookie,浏览器一般只允许存放 300 个Cookie

  • 移动端对 cookie 的支持不是很好,而 session 需要基于 cookie 实现,所以移动端常用的是 token

使用 session 时需要考虑的问题

  • 将 session 存储在服务器里面,当用户同时在线量比较多时,这些 session 会占据较多的内存,需要在服务端定期的去清理过期的 session

  • 当网站采用集群部署的时候,会遇到多台 web 服务器之间如何做 session 共享的问题。因为 session 是由单个服务器创建的,但是处理用户请求的服务器不一定是那个创建 session 的服务器,那么该服务器就无法拿到之前已经放入到 session 中的登录凭证之类的信息了。

  • 当多个应用要共享 session 时,除了以上问题,还会遇到跨域问题,因为不同的应用可能部署的主机不一样,需要在各个应用做好 cookie 跨域的处理。

  • sessionId 是存储在 cookie 中的,假如浏览器禁止 cookie 或不支持 cookie 怎么办? 一般会把 sessionId 跟在 url 参数后面即重写 url,所以 session 不一定非得需要靠 cookie 实现

  • 移动端对 cookie 的支持不是很好,而 session 需要基于 cookie 实现,所以移动端常用的是 token

使用 token 时需要考虑的问题

  • 如果你认为用数据库来存储 token 会导致查询时间太长,可以选择放在内存当中。比如 redis 很适合你对 token 查询的需求。

  • token 完全由应用管理,所以它可以避开同源策略

  • token 可以避免 CSRF 攻击(因为不需要 cookie 了)

  • 移动端对 cookie 的支持不是很好,而 session 需要基于 cookie 实现,所以移动端常用的是 token

使用 JWT 时需要考虑的问题

  • 因为 JWT 并不依赖 Cookie 的,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS)

  • JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。

  • JWT 不加密的情况下,不能将秘密数据写入 JWT。

  • JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。

  • JWT 最大的优势是服务器不再需要存储 Session,使得服务器认证鉴权业务可以方便扩展。但这也是 JWT 最大的缺点:由于服务器不需要存储 Session 状态,因此使用过程中无法废弃某个 Token 或者更改 Token 的权限。也就是说一旦 JWT 签发了,到期之前就会始终有效,除非服务器部署额外的逻辑。

  • JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。

  • JWT 适合一次性的命令认证,颁发一个有效期极短的 JWT,即使暴露了危险也很小,由于每次操作都会生成新的 JWT,因此也没必要保存 JWT,真正实现无状态。

  • 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

使用加密算法时需要考虑的问题

  • 绝不要以明文存储密码

  • 永远使用 哈希算法 来处理密码,绝不要使用 Base64 或其他编码方式来存储密码,这和以明文存储密码是一样的,使用哈希,而不要使用编码。编码以及加密,都是双向的过程,而密码是保密的,应该只被它的所有者知道, 这个过程必须是单向的。哈希正是用于做这个的,从来没有解哈希这种说法, 但是编码就存在解码,加密就存在解密。

  • 绝不要使用弱哈希或已被破解的哈希算法,像 MD5 或 SHA1 ,只使用强密码哈希算法。

  • 绝不要以明文形式显示或发送密码,即使是对密码的所有者也应该这样。如果你需要 “忘记密码” 的功能,可以随机生成一个新的 一次性的(这点很重要)密码,然后把这个密码发送给用户。

分布式架构下 session 共享方案

1. session 复制

  • 任何一个服务器上的 session 发生改变(增删改),该节点会把这个 session 的所有内容序列化,然后广播给所有其它节点,不管其他服务器需不需要 session ,以此来保证 session 同步

优点: 可容错,各个服务器间 session 能够实时响应。
缺点: 会对网络负荷造成一定压力,如果 session 量大的话可能会造成网络堵塞,拖慢服务器性能。

2. 粘性 session /IP 绑定策略

  • 采用 Ngnix 中的 ip_hash 机制,将某个 ip的所有请求都定向到同一台服务器上,即将用户与服务器绑定。 用户第一次请求时,负载均衡器将用户的请求转发到了 A 服务器上,如果负载均衡器设置了粘性 session 的话,那么用户以后的每次请求都会转发到 A 服务器上,相当于把用户和 A 服务器粘到了一块,这就是粘性 session 机制。

优点: 简单,不需要对 session 做任何处理。
缺点: 缺乏容错性,如果当前访问的服务器发生故障,用户被转移到第二个服务器上时,他的 session 信息都将失效。
适用场景: 发生故障对客户产生的影响较小;服务器发生故障是低概率事件 。
实现方式: 以 Nginx 为例,在 upstream 模块配置 ip_hash 属性即可实现粘性 session。

3. session 共享(常用)

  • 使用分布式缓存方案比如 Memcached 、Redis 来缓存 session,但是要求 Memcached 或 Redis 必须是集群

  • 把 session 放到 Redis 中存储,虽然架构上变得复杂,并且需要多访问一次 Redis ,但是这种方案带来的好处也是很大的:

    • 实现了 session 共享;

    • 可以水平扩展(增加 Redis 服务器);

    • 服务器重启 session 不丢失(不过也要注意 session 在 Redis 中的刷新/失效机制);

    • 不仅可以跨服务器 session 共享,甚至可以跨平台(例如网页端和 APP 端)

img

4. session 持久化

  • 将 session 存储到数据库中,保证 session 的持久化

优点: 服务器出现问题,session 不会丢失
缺点: 如果网站的访问量很大,把 session 存储到数据库中,会对数据库造成很大压力,还需要增加额外的开销维护数据库。

只要关闭浏览器 ,session 真的就消失了?

不对。对 session 来说,除非程序通知服务器删除一个 session,否则服务器会一直保留,程序一般都是在用户做 log off 的时候发个指令去删除 session。
然而浏览器从来不会主动在关闭之前通知服务器它将要关闭,因此服务器根本不会有机会知道浏览器已经关闭,之所以会有这种错觉,是大部分 session 机制都使用会话 cookie 来保存 session id,而关闭浏览器后这个 session id 就消失了,再次连接服务器时也就无法找到原来的 session。如果服务器设置的 cookie 被保存在硬盘上,或者使用某种手段改写浏览器发出的 HTTP 请求头,把原来的 session id 发送给服务器,则再次打开浏览器仍然能够打开原来的 session。
恰恰是由于关闭浏览器不会导致 session 被删除,迫使服务器为 session 设置了一个失效时间,当距离客户端上一次使用 session 的时间超过这个失效时间时,服务器就认为客户端已经停止了活动,才会把 session 删除以节省存储空间。

项目地址

在项目中使用 JWT

后语

  • 本文只是基于自己的理解讲了理论知识,因为对后端/算法知识不是很熟,如有谬误,还请告知,万分感谢

  • 如果本文对你有所帮助,还请点个赞~~

参考

百度百科-cookie

百度百科-session

详解 Cookie,Session,Token

一文彻底搞懂Cookie、Session、Token到底是什么

3种web会话管理的方式!!!

Token ,Cookie和Session的区别!!!

彻底理解 cookie、session、token!!!

前端鉴权

SHA-1

SHA-2

SHA-3

不要再使用MD5和SHA1加密密码了!

廖雪峰 Node 教程之 crypto


作者:秋天不落叶
来源:https://juejin.cn/post/6844904034181070861

收起阅读 »

不常见但是有用的chrome调试技巧

dom添加选中dom节点为全局变量方便需要调试多个dom的场景适用对dom有多次操作的场景force node state (触发)状态调试dom的某个状态copy element拷贝选中dom的信息style/class给选中元素添加一个 class 名快速...
继续阅读 »



dom

添加选中dom节点为全局变量方便需要调试多个dom的场景

适用对dom有多次操作的场景

force node state (触发)状态

调试dom的某个状态

copy element

拷贝选中dom的信息

style/class

给选中元素添加一个 class 名

快速给元素添加class

修改元素的盒模型大小

快速修改元素的盒模型大小(margin/padding/width/height等)

network

block specific request

block特定的请求

快捷键:command + shift + p -> show request blocking

改变请求的 user agent

修改请求的user agent

快捷键:command + shift + p -> network conditions 切换 user agent

javascript

断点,断浏览器的行为(比如 click、mouse 等等)

拦截浏览器的行为


快速改变拦截的变量的值

双击改变拦截变量的值

添加 watch 表达式

添加watch表达式

条件断点

设置断点的条件

快速调试代码片段

Snippet(片段)代码调试,不需要创建特定的页面

参考文档


作者:seventhMa
来源:https://juejin.cn/post/6963600839587921927

收起阅读 »

前端工程师生产环境 debugger 技巧

导言:那我们今天讲一讲如何使用 chrome 在生产环境进行 debug 。生产环境 debug 需要几步?这问题和“把大象装进冰箱拢共分几步”一样简单。第二步,把大象装进冰箱。找到需要 debug 的前端文件,格式化,打断点,调试上下文,定位问题;如何快速定...
继续阅读 »

导言:

开发环境 debug 是每个程序员上岗的必备技能。生产环境呢?虽然生产环境 debug 是一件非常不优雅的行为,但是由于种种原因,我们又不得不这么干。

那我们今天讲一讲如何使用 chrome 在生产环境进行 debug 。

生产环境 debug 步骤

生产环境 debug 需要几步?这问题和“把大象装进冰箱拢共分几步”一样简单。

第一步,把冰箱门打开。F12 打开 devTools;

第二步,把大象装进冰箱。找到需要 debug 的前端文件,格式化,打断点,调试上下文,定位问题;

第三部,关闭冰箱门。解决问题。

如何快速定位错误是前端还是后端接口返回的?

在把大象装进冰箱之前,先初步判断下,是否真的需要由你将大象装进冰箱。

首先我们需要判断,错误是前端还是后端报的,那么如何快速判断?

方案一:根据对代码的实现的了解,判断报错属于前端还是后端。

这个方案前提是需要你对代码实现很熟悉,也是最简单的方式。

方案二:前端代码全局搜索关键字,工程代码里搜索/控制台打开搜索。

对应工程 gitlab 或者 vscode 或者 devTools global search 里去进行全局搜索。

方案三:翻阅 network 面板中的请求。

翻阅 network 面板中的请求,看下返回的 response 是否携带错误提示,有则表示后端返回的;如果报错的接口刚好是以非200 的状态返回,或者是由新的操作触发调用接口,我们很快就能查找到对应的接口,如下:

方案四:使用 network search 进行搜索。

但是很多情况,接口业务错误会以 http status 200 的状态码返回,如果此时请求了大量的接口(举个例子:进入页面调用了大量的接口,其中有一个接口返回了错误信息),那么除了逐个翻阅 network 这种低效的方式,chrome devTools 还提供了 network search 面板这种更便捷的方式,可以搜索接口详细信息(包括详细的返回信息),返回匹配结果。

如何打开 network search 面板?

在 network 面板中,按快捷键 ⌘ + F(Mac)、 CTRL + F(Windows)可呼出 network search 面板。

如果确定需要你把大象装进冰箱,那把大象装进冰箱的技巧有哪些?

如何快速定位到问题相关的代码

global search ,全局搜素关键字,再定位到关键的代码

chrome devTools 的 global search 是一个非常实用的一个功能,当你不知道需要调试的代码在哪个文件时,当你是一个非常大的系统,引用了很多的资源文件,你可以使用 global search 进行搜索关键字,这个操作会搜索所有加载进来的资源,点击搜索结果,就可以使用 source 面板打开对应的资源文件,然后格式化代码,再然后在当前的文件内 再次搜索关键字,打断点。

打开 global search 快捷键:

⌘ + ⌥ + F (Mac),CTRL + SHIFT + F (Windows)

看下图例子,我们随便找个页面根据提示搜索代码:

可以尝试使用哪些关键字进行搜索:

(1) 页面存在明确的报错信息,且已经明确该错误文案是写在前端代码中错误信息文案。提示信息在 coding 过程中一般是使用 字符串,压缩混淆过程中一般是不会进行处理的,会保留原文,当然代码打包构建过程中,对代码压缩混淆也可以选择对中文进行 unicode 转码,此时如果关键字是中文,就需要先转码再搜索了。

(2) 已知相关代码中存在的编译混淆后依然还保留的的关键代码,会向外暴露的方法名;

如何 debug 混淆后的 js ?

生产环境的 js 基本上都是混淆过的(点击了解前端代码的压缩混淆),压缩混淆的优点就不赘述了,压缩混淆后随之来的是生产环境调试的难度,虽然通过打断点,勉强还能看的懂,但是已经很反人类了。

我们用一个最简单的 demo ,对比一下代码生产环境构建编译前后的差距。

这里选择用 vue-cli 创建了一个最简单的 demo ,看下源代码和编译后的代码。

源代码:

构建编译后的代码(此处关闭了 sourceMap ):

这里我们看到构建编译后的代码做了压缩混淆,出现了出现了大量大的 abcd 替换了原有的函数方法名、变量名,编译后的代码已经不是能通过单纯的读代码码能读懂的了。但是我们通过 debug ,大概还是能看得懂。

那么有没有方式使用本地的 sourceMap 调试生产环境的代码?答案当然是有的。

如何在生产环境使用本地 sourceMap 调试?

第一步:打开混淆代码

第二步:右键 -> 选择【Add source map】

第三步:输入本地 sourceMap 的地址(此处需要启用一个静态资源服务,可以使用 http-server),完成。本地代码执行构建命令,注意需要打开 sourceMap 配置,编译产生出构建后的代码,此时构建后的结果会包含 sourceMap 文件。

关联上 sourceMap 后,我们就可以看到 sources -> page 面板上的变化了

如何在 chrome 中修改代码并调试?

开发环境中,我们可以直接在 IDE 中修改代码,代码的变更就直接更新到了浏览器中了。那么生产环境,我们可以直接在 chrome 中修改代码,然后立马看代码修改后的效果吗?

当然,你想要的 chrome devTools 都有。chrome devTools 提供了 local overrides 能力。

local overrides 如何工作的?

指定修改后的文件的本地保存目录,当修改完代码保存的时候,就会将修改后的文件保存到你指定的目录目录下,当再次加载页面的时候,对应的文件不再读取网络上的文件,而是读取存储在本地修改过的文件。

local overrides 如何使用?

首先,打开 sources 下的 overrides 面板;

然后,点击【select folder overrides】选择修改后的文件存储地址;

再然后,点击顶部的授权,确认同意;

最后,我们就可以打开文件修改,修改完成后保存,重新刷新页面后,修改后的代码就被执行到了。

⚠️注意,原js文件直接 format 是无法修改的;在代码 format 之前先添加无效代码进行代码变更进行保存,然后再 format 就可以修改;

总结

chrome 调试技巧远远当然不只这些,以上只是生产环境 debug 的小技巧,祝愿大家用不到,最好的 bug 处理方式当然是事前,在上线前得到就解决;如果真的发生问题,如果做好监控和日志,在问题发生的第一时间发现并解决。

参考文献

作者:七喜
来源:https://zoo.team/article/prod-debugger

收起阅读 »

JS 的 6 种打断点的方式,你用过几种?

Debugger 是前端开发很重要的一个工具,它可以在我们关心的代码处断住,通过单步运行来理清逻辑。而 Debugger 用的好坏与断点打得好坏有直接的关系。Chrome Devtools 和 VSCode 都提供了 Debugger,它们支持的打断点的方式有...
继续阅读 »

Debugger 是前端开发很重要的一个工具,它可以在我们关心的代码处断住,通过单步运行来理清逻辑。而 Debugger 用的好坏与断点打得好坏有直接的关系。

Chrome Devtools 和 VSCode 都提供了 Debugger,它们支持的打断点的方式有 6 种。

普通断点

在想断住的那一行左侧单击一下就可以添加一个断点,运行到该处就会断住。

这是最基础的断点方式,VSCode 和 Chrome Devtools 都支持这种断点。

条件断点

右键单击代码所在的行左侧,会出现一个下拉框,可以添加一个条件断点。

输入条件表达式,当运行到这一行代码并且表达式的值为真时就会断住,这比普通断点灵活些。

这种根据条件来断住的断点 VSCode 和 Chrome Devtools 也都支持。

DOM 断点

在 Chrome Devtools 的 Elements 面板的对应元素上右键,选择 break on,可以添加一个 dom 断点,也就是当子树有变动、属性有变动、节点移除这三种情况的时候会断住。可以用来调试导致 dom 变化的代码。

因为是涉及到 DOM 的调试,只有 Chrome Devtools 支持这种断点。

URL 断点

在 Chrome Devtools 的 Sources 面板可以添加 XHR 的 url 断点,当 ajax 请求对应 url 时就会断住,可以用来调试请求相关的代码。

这个功能只有 Chrome Devtools 有。

Event Listener 断点

在 Chrome Devtools 的 Sources 面板还可以添加 Event Listener 的断点,指定当发生什么事件时断住,可以用来调试事件相关代码。

这个功能也是只有 Chrome Devtools 有。

异常断点

在 VSCode 的 Debugger 面板勾选 Uncaught Exceptions 和 Caught Exceptions 可以添加异常断点,在抛出异常未被捕获或者被捕获时断柱。用来调试一些发生异常的代码时很有用。

总结

Debugger 打断点的方式除了直接在对应代码行单击的普通断点以外,还有很多根据不同的情况来添加断点的方式。

一共有六种:

  • 普通断点:运行到该处就断住
  • 条件断点:运行到该处且表达式为真就断住,比普通断点更灵活
  • DOM 断点:DOM 的子树变动、属性变动、节点删除时断住,可以用来调试引起 DOM 变化的代码
  • URL 断点:URL 匹配某个模式的时候断住,可以用来调试请求相关代码
  • Event Listener 断点:触发某个事件监听器的时候断住,可以用来调试事件相关代码
  • 异常断点:抛出异常被捕获或者未被捕获的时候断住,可以用来调试发生异常的代码

这些打断点方式大部分都是 Chrome Devtools 支持的(普通、条件、DOM、URL、Event Listener、异常),也有的是 VSCode Debugger 支持的(普通、条件、异常)。

不同情况下的代码可以用不同的打断点方式,这样调试代码会高效很多。

JS 的六种打断点方式,你用过几种呢?

原文:https://juejin.cn/post/7041946855592165389

收起阅读 »

这些都能成为 Web 语法规范,强迫症看不下去了

JavaScript 一直是饱受诟病,源于网景公司在 1995 年用了 10 天的时间创造。没有什么能用 10 天创造就是完美的,可是某些特性一旦发布,错误或不完善的地方迅速成为必不可少的特色,并且是几乎不可能改变。 Javascript 的发展非常快,根本没...
继续阅读 »

JavaScript 一直是饱受诟病,源于网景公司在 1995 年用了 10 天的时间创造。没有什么能用 10 天创造就是完美的,可是某些特性一旦发布,错误或不完善的地方迅速成为必不可少的特色,并且是几乎不可能改变。


Javascript 的发展非常快,根本没有时间调整设计。在推出一年半之后,国际标准就问世了。设计缺陷还没有充分暴露就成了标准。


历史遗留


比如常见的历史设计缺陷:



  • nullundefined 两者非常容易混淆

  • == 类型转换的问题

  • var 声明创建全局变量

  • 自动插入行尾分号

  • 加号可以表示数字之和,也可以表示字符的连接

  • NaN 奇怪的特性

  • 更多...


Javascript 很多不严谨的特性我们可以添加 eslint 来规避。比如禁用 var== 成了大多数人写代码的必备条件。


现在/未来


如今 CSS、DOM、HTML 规范由 W3C 来制定,JavaScript 规范由 TC39 制定。那些历史缺陷也成为了过去,但是现在也出现了一些不尽人意的规范。


CSS 变量


声明变量的时候,变量名前面要加两根连词线 --


body {
--foo: #7f583f;
--bar: #f7efd2;
}

var() 函数用于读取变量。


a {
color: var(--foo);
text-decoration-color: var(--bar, #7f583f);
}

为什么选择两根连词线(--)表示变量?因为 $Sass 用掉,@Less 用掉。_-,用作为 IEchrome 兼容写法。CSS 中已经找不出来字符可以代替变量声明了。为了不产生冲突,官方的 CSS 变量就改用两根连词线。


作为一个官方的标准规范,时刻影响后面的行业发展。竟然能被第三方的插件所左右,令人大跌眼镜。有开发者吐槽:微软的架构师也是够窝囊。


现在很多应用都放弃了 Sassless,转向了 PostCSS 的怀抱。面向组件编程,根本用不到 Sassless 里面的一些复杂功能。那么 -- 两个字符的繁琐将成为开发者永远的痛。


类私有属性(proposal-class-fields)


JavaScript 中的 class 大家已经不陌生了,简直跟 Javaclass 一模一样。


基本用法:


class BaseClass {
msg = 'hello world';

basePublicMethod() {
return this.msg;
}
}

继承:


class SubClass extends BaseClass {
subPublicMethod() {
return super.basePublicMethod();
}
}

静态属性:


class ClassWithStaticField {
static baseStaticMethod() {
return 'base static method output';
}
}

异步方法


class ClassWithFancyMethods {
*generatorMethod() {}
async asyncMethod() {}
async *asyncGeneratorMethod() {}
}

而类私有属性的提案目前已经进入标准,它用了 # 关键字前缀来修饰一个类的属性。


class ClassWithPrivateField {
#privateField;

constructor() {
this.#privateField = 42;
}
}

你没看错,不是 typescript 中的 private 关键字。


class BaseClass {
readonly msg = 'hello world';

private basePrivateMethod() {
return this.msg;
}
}

然而 # 的语法丑陋本身引起了社区的争议:



「class fields 提案提供了一个极具争议的私有字段访问语法——并成功地做对了唯一一件事情,让社区把全部的争议焦点放在了这个语法上」。




TS 投降主义已经被迫实现了。




No dynamic access, no destructuring is a deal breaker for me




我们制作一个 eslint 插件 no-private-class-fields 并使用下载计数来说明社区反对




'#' 作为名称的一部分会导致混淆,因为 this.#x !== this['#x'] 太奇怪了



前端架构师、TC39 成员贺师俊也在知乎连发好几篇文章吐槽 class fields


不妨大家看看关于 private 的 side: johnhax.net/2017/js-pri…


提案地址:github.com/tc39/propos…


globalThis


在不同的 JavaScript 环境中拿到全局对象是需要不同的语句的。在 Web 中,可以通过 windowself 取到全局对象,但是在 Web Workers 中只有 self 可以。在 Node.js 中,必须使用 global。非严格模式下,可以在函数中返回 this 来获取全局对象,否则会返回 undefined


因此一个叫 global 的提案出现。主要用 global 变量统一上面的行为,但后面绕来绕去改成了 globalThis,引起了激烈讨论。


globalThis 这个名字会让 this 变得更加复杂。



  1. this 一直是困扰程序员的话题,尤其是 JavaScript 新手,关于它的博客文章源源不断

  2. ES6 让事情变得更简单,因为可以告诉人们更喜欢箭头函数并且只使用 this 内部方法定义

  3. 在现代 JS(modules) 中,并没有真正的全局 this,所以 globalThis 甚至不引用现有的概念


现在说这一切都是徒劳的,因为它已经进入 stage 4


提案地址:github.com/tc39/propos…


总结


JavaScript 中遗留的糟粕太多。现在受到这些糟粕的影响,很多新的提案又不得不妥协。在未来,它会变得极其复杂。


也许某一天,会出现一个没有历史包袱的 JavaScript 子集来替换它。



作者:MinJie
链接:https://juejin.cn/post/7043340139049222152

收起阅读 »

13 行 JavaScript 代码让你看起来像是高手

Javascript 可以做许多神奇的事情,也有很多东西需要学习,今天我们介绍几个短小精悍的代码段。 获取随机布尔值(True/False) 使用 Math.random() 会返回 0 到 1 的随机数,之后判断它是否大于 0.5,将会得到一个 50% 概率...
继续阅读 »

Javascript 可以做许多神奇的事情,也有很多东西需要学习,今天我们介绍几个短小精悍的代码段。


获取随机布尔值(True/False)


使用 Math.random() 会返回 0 到 1 的随机数,之后判断它是否大于 0.5,将会得到一个 50% 概率为 TrueFalse 的值


const randomBoolean = () => Math.random() >= 0.5;
console.log(randomBoolean());

判断一个日期是否是工作日


判断给定的日期是否是工作日


const isWeekday = (date) => date.getDay() % 6 !== 0;
console.log(isWeekday(new Date(2021, 0, 11)));
// Result: true (周一)
console.log(isWeekday(new Date(2021, 0, 10)));
// Result: false (周日)

反转字符串


有许多反转字符串的方法,这里使用一种最简单的,使用了 split()reverse()join()


const reverse = str => str.split('').reverse().join('');
reverse('hello world');
// Result: 'dlrow olleh'

判断当前标签页是否为可视状态


浏览器可以打开很多标签页,下面 👇🏻 的代码段就是判断当前标签页是否是激活的标签页


const isBrowserTabInView = () => document.hidden;
isBrowserTabInView();

判断数字为奇数或者偶数


取模运算符 % 可以很好地完成这个任务


const isEven = num => num % 2 === 0;
console.log(isEven(2));
// Result: true
console.log(isEven(3));
// Result: false

从 Date 对象中获取时间


使用 Date 对象的 .toTimeString() 方法转换为时间字符串,之后截取字符串即可


const timeFromDate = date => date.toTimeString().slice(0, 8);
console.log(timeFromDate(new Date(2021, 0, 10, 17, 30, 0)));
// Result: "17:30:00"
console.log(timeFromDate(new Date()));
// Result: 返回当前时间

保留指定的小数位


const toFixed = (n, fixed) => ~~(Math.pow(10, fixed) * n) / Math.pow(10, fixed);
// Examples
toFixed(25.198726354, 1); // 25.1
toFixed(25.198726354, 2); // 25.19
toFixed(25.198726354, 3); // 25.198
toFixed(25.198726354, 4); // 25.1987
toFixed(25.198726354, 5); // 25.19872
toFixed(25.198726354, 6); // 25.198726

检查指定元素是否处于聚焦状态


可以使用 document.activeElement 来判断元素是否处于聚焦状态


const elementIsInFocus = (el) => (el === document.activeElement);
elementIsInFocus(anyElement)
// Result: 如果处于焦点状态会返回 True 否则返回 False

检查当前用户是否支持触摸事件


const touchSupported = () => {
('ontouchstart' in window || window.DocumentTouch && document instanceof window.DocumentTouch);
}
console.log(touchSupported());
// Result: 如果支持触摸事件会返回 True 否则返回 False

检查当前用户是否是苹果设备


可以使用 navigator.platform 判断当前用户是否是苹果设备


const isAppleDevice = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
console.log(isAppleDevice);
// Result: 是苹果设备会返回 True

滚动至页面顶部


window.scrollTo() 会滚动至指定的坐标,如果设置坐标为(0,0),就会回到页面顶部


const goToTop = () => window.scrollTo(0, 0);
goToTop();
// Result: 将会滚动至顶部

获取所有参数的平均值


可以使用 reduce() 函数来计算所有参数的平均值


const average = (...args) => args.reduce((a, b) => a + b) / args.length;
average(1, 2, 3, 4);
// Result: 2.5

转换华氏/摄氏


再也不怕处理温度单位了,下面两个函数是两个温度单位的相互转换。


const celsiusToFahrenheit = (celsius) => celsius * 9/5 + 32;
const fahrenheitToCelsius = (fahrenheit) => (fahrenheit - 32) * 5/9;
// Examples
celsiusToFahrenheit(15); // 59
celsiusToFahrenheit(0); // 32
celsiusToFahrenheit(-20); // -4
fahrenheitToCelsius(59); // 15
fahrenheitToCelsius(32); // 0

感谢阅读,希望你会有所收获😄


作者:夜色镇歌
链接:https://juejin.cn/post/7043062481954013197

收起阅读 »

美团跨端一体化富文本管理技术实践

为了减少产品和前端开发人员之间的矛盾,不断降本提效,美团医药技术部构建了跨端一体化富文本管理平台Page-佩奇。本文系统介绍了该平台的定位、设计思路、实现原理以及取得的成效。希望这些实战经验与总结,能给大家带来一些启发或帮助。一、引言在互联网圈,开发和产品经理...
继续阅读 »



为了减少产品和前端开发人员之间的矛盾,不断降本提效,美团医药技术部构建了跨端一体化富文本管理平台Page-佩奇。本文系统介绍了该平台的定位、设计思路、实现原理以及取得的成效。希望这些实战经验与总结,能给大家带来一些启发或帮助。

一、引言

在互联网圈,开发和产品经理之间相爱相杀的故事,相信大家都有所耳闻。归根结底,往往都是从简单的改需求开始,然后你来我往、互不相让,接着吵架斗嘴,最后导致矛盾不断升级,甚至带来比较严重的后果。

pic_4bf1ae9b.png

图1

在这种背景下,如果把一些功能相对简单的、需求变动比较频繁的页面,直接交给产品或者运营自己去通过平台实现,是不是就可以从一定程度上减少产品和开发人员之间的矛盾呢?

二、背景

当然上述的情况,美团也不例外。近些年,美团到家事业群(包括美团外卖、美团配送、闪购、医药、团好货等)的各个业务稳步发展,业务前端对接的运营团队有近几十个,每个运营团队又有不同的运营规则,这些规则还存在一些细微的样式差别,同时规则内容还会随着运营季节、节日、地理位置等进行变化和更新。这些需求具体来说有以下几个特点:

  1. 需求量大:业务稳步发展,业务需求不断叠加,甚至部分业务呈指数级增长,且业务方向涉及到一些业务规则、消息通知、协议文档、规则介绍等需求。

  2. 变更频繁:面对市场监管和法务的要求,以及新业务调整等因素的影响,会涉及到需求的频繁变更,像一些业务FAQ、产品介绍、协议文档、业务规则、系统更新日志等页面,需要做到快速响应和及时上线。

  3. 复杂度低:这些页面没有复杂的交互逻辑,如果能把这些简单的页面交给运营/产品去实现,开发人员就能有更多的时间去进行复杂功能的研发。

  4. 时效性高:临时性业务需求较多,且生命周期较短,具有定期下线和周期性上线等特点。

基于以上特点,为了提高研发效率,美团医药技术部开始构建了一个跨端一体化富文本管理平台,希望提供解决这一大类问题的产研方案。不过,部门最初的目标是开发一套提效工具,解决大量诸如帮助文档、协议页、消息通知、规则说明等静态页面的生产与发布问题,让产品和运营同学能够以所见即所得的方式自主完成静态页面制作与发布,进而缩短沟通成本和研发成本。

但是,随着越来越多业务部门开始咨询并使用这个平台,我们后续不断完善并扩充了很多的功能。经过多次版本的设计和迭代开发后,将该平台命名为Page-佩奇,并且注册成为美团内部的公共服务,开始为美团内部更多同学提供更好的使用体验。

本文将系统地介绍Page-佩奇平台的定位、设计思路、实现原理及取得成效。我们也希望这些实战经验与总结,能给更多同学带来一些启发和思考。

三、跨端一体化富文本管理解决方案

3.1 平台定位

我们希望将Page-佩奇打造成一款为产品、运营、开发等用户提供快速一站式发布网页的产研工作台,这是对该平台的一个定位。

  • 对产品运营而言,他们能够可视化地去创建或修改一些活动说明、协议类、消息类的文章,无需开发排期,省去向开发二次传递消息等繁琐的流程,也无需等待漫长的发布时间,从而达到灵活快速地进行可视化页面的发布与管理。

  • 对开发同学而言,他们能够在线编写代码,并实现秒级的发布上线,并且支持ES 6、JavaScript 、Less、CSS语法,我们还提供了基础的工具、图表库等,能够生成丰富多样的页面。帮助开发同学快速实现数据图表展示,设计特定样式,完成各种交互逻辑等需求。

  • 对项目管理方而言,他们能够清晰地看到整个需求流转状态和开发日志信息,为运营管理提供强大的“抓手”。

一般来讲,传统开发流程是这样的:首先产品提出需求,然后召集研发评审,最后研发同学开发并且部署上线;当需求上线之后,如果有问题需要反馈,产品再找研发同学进行沟通并修复,这种开发流程也是目前互联网公司比较常见的开发流程。

pic_b81cd143.png

图2 传统开发流程图

而美团Page-佩奇平台的开发流程是:首先产品同学提出需求,然后自己在Page平台进行编辑和发布上线,当需求上线之后有问题需要反馈,直接就能触达到产品同学,他们通常可自行进行修复。如果需求需要定制化,或者需要做一些复杂的逻辑处理,那么再让研发人员配合在平台上进行开发并发布上线。

pic_b3e5d331.png

图3 Page-佩奇平台开发流程图

简单来说,对那些功能相对简单、需求变动比较频繁的页面,如果用传统的开发流程将会增加产研沟通和研发排期成本,因此传统方案主要适用于功能复杂型的需求。而Page-佩奇平台开发流程,并不适合功能复杂型的需求,特别适用于功能相对简单、需求变动比较频繁的页面需求。

综上所述,可以看出这两种开发流程其实起到了一个互补的作用,如果一起使用,既可以减少工作量,又可以达到降本提效的目的。

3.2 设计思路

我们最初设计Page-佩奇平台的初心其实很简单,为了给产品和运营提供一个通过富文本编辑器快速制作并发布网页的工具。但是,在使用的过程中,很多缺陷也就慢慢地开始暴露,大致有下面这些问题:

  1. 简单的富文本编辑器满足不了想要的页面效果,怎么办?

  2. 如果能导入想要的模板,是否会更友好?

  3. 怎么查看这个页面的访问数据?如何能监控这个页面的性能问题?

  4. 发布的页面是否有存在安全风险?

于是,我们针对这些问题进行了一些思考和调研:

  • 当富文本编辑器满足不了想要实现的效果的时候,可以引入了WebIDE编辑器,可以让研发同学再二次编辑进行实现。

  • 一个系统想要让用户用得高效便捷,那么就要完善它的周边生态。就需要配备完善的模板素材和物料供用户灵活选择。

  • 如果用户想要了解页面的运行情况,那么页面运行的性能数据、访问的数据也是必不可少的。

  • 如果发布的内容存在不当言论,就会造成不可控的法律风险,所以内容风险审核也是必不可少的。

实现一个功能很容易,但是想要实现一个相对完善的功能,就必须好好下功夫,多思考和多调研。于是,围绕着这些问题,我们不断挖掘和延伸出了一系列功能:

  1. 富文本编辑:强大而简单的可视化编辑器,让一切操作变得简单、直观。产品同学可以通过编辑器自主创建、编辑网页,即使无程序开发经验也可以通过富文本编辑器随意操作,实现自己想要的效果,最终可以实现一键快速发布上线。

  2. WebIDE:定制化需求,比如,与客户端和后端进行一些通信和请求需求,以及针对产品创建的HTML进行二次加工需求,均可以基于WebIDE通过JavaScript代码实现。具备专业开发经验的同学也可以选择通过前端框架jQuery、Vue,Echarts或者工具库Lodash、Axios实现在线编辑代码。

  3. 页面管理:灵活方便地管理页面。大家可以对有权限的文档进行查看、编辑、授权、下线、版本对比、操作日志、回滚等操作,且提供便捷的文档搜索功能。

  4. 模板市场:丰富多样的网页模板,简易而又具备个性。模板市场提供丰富的页面模板,大家可选择使用自己的模板快速创建网页,且发布的每个页面又可以作为自己的模板,再基于这个模板,可随时添加个性化的操作。

  5. 物料平台:提供基础Utils、Echart、Vue、jQuery等物料,方便开发基于产品的页面进行代码的二次开发。

  6. 多平台跨端接入:高效快捷地接入业务系统。通过通信SDK,其他系统可以快速接入Page-佩奇平台。同时支持以HTTP、Thrift方式的开放API供大家选择,支持客户端、后端调用开放API。

  7. 内容风险审核:严谨高效的审核机制。接入美团内部的风险审核公共服务,针对发布的风险内容将快速审核,防止误操作造成不可控的法律风险。

  8. 数据大盘:提供页面的数据监测,帮助大家时刻掌握流量动向。接入美团内部一站式数据分析平台,帮助大家安全、快速、高效地掌握页面的各种监测数据。

  9. 权限管理:创建的每个页面都有相对独立的权限,只有经过授权的人才能查看和操作该页面。

  10. 业务监控:提供页面级别JavaScript错误和资源加载成功率等数据,方便开发排查和解决线上问题。

功能流程图如下所示:

pic_9d007bab.png

图4 Page-佩奇平台功能流程图

3.3 实现原理

3.3.1 基础服务

Page-佩奇平台的基础服务有四个部分,包括物料服务、编译服务、产品赋能、扩展服务。

pic_f0bf9912.png

图5 整体架构图

3.3.2 核心架构

pic_bbb3bb9a.png

图6 核心架构图

Page-佩奇平台核心架构主要包含页面基础配置层、页面组装层以及页面生成层。我们通过Vuex全局状态对数据进行维护。

  • 页面基础配置层主要提供生成页面的各种能力,包括富文本的各种操作能力、编辑源码(HTML、CSS、JavaScript)的能力、自定义域名配置、适配的容器(PC/H5)、发布环境等。

  • 页面组装层则会基于基础配置层所提供的的能力,实现页面的自由编辑,承载大量的交互逻辑,用户的所有操作都在这一层进行。

    • 业务PV和UV埋点,错误统计,访问成功率上报。

    • 自动适配PC和移动端样式。

    • 内网页面显示外网不可访问标签。

  • 页面生成层则需要根据组装后的配置进行解析和预处理、编译等操作,最终生成HTML、CSS、JavaScript渲染到网页当中。

3.3.3 关键流程

pic_b5adef78.png

图7 关键流程图

如上图7所示,平台的核心流程主要包含页面创建之后的页面预览、编译服务、生成页面。

  • 页面预览:创建、编辑之后的页面,将会根据内容进行页面重组,对样式和JavaScript进行预编译之后,对文本+JavaScript+CSS进行组装,生成HTML代码块,然后将代码块转换成Blob URL,最终以iframe的方式预览页面。

  • 编译服务:文件树状结构和代码发送请求到后端接口,基于Webpack将Less编译成CSS,ES 6语法编译成ES 5。通用物料使用CDN进行引入,不再进行二次编译。

  • 生成页面:当创建、编辑之后的页面进行发布时,服务端将会进行代码质量检测、内容安全审查、代码质量检测、单元测试、上传对象存储平台、同步CDN检测,最终生成页面链接进行访问。

3.3.4 多平台接入

Page-佩奇平台也可以作为一个完善的富文本编辑器供业务系统使用,支持内嵌到其他系统内。作为消息发布等功能承载,减少重复的开发工作,同时我们配备完善的SDK供大家选择使用。通过Page-SDK可以直接触发Page平台发布、管理等操作,具体的流程如下图所示:

pic_e3c8e777.png

图8 Page-SDK流程图

3.3.5 Open API

在使用Page-佩奇平台的时候,美团内部一些业务方提出想要通过Page-佩奇平台进行页面的发布,同时想要拿到发布的内容做一些自定义的处理。于是,我们提供了Open API开放能力,支持以HTTP和Thrift两种方式进行调用。下面主要讲一下Thrift API实现的思路,首先我们先了解下Thrift整体流程:

pic_a6286e54.png

图9 Thrift整体流程图

Thrift的主要使用过程如下:

  1. 服务端预先编写接口定义语言 IDL(Interface Definition Language)文件定义接口。

  2. 使用Thrift提供的编译器,基于IDL编译出服务语言对应的接口文件。

  3. 被调用服务完成服务注册,调用发起服务完成服务发现。

  4. 采用统一传输协议进行服务调用与数据传输。

下面具体讲讲,Node语言是如何实现和其他服务语言实现调用的。由于我们的服务使用的Node语言,因此我们的Node服务就充当了服务端的角色,而其他语言(Java等)调用就充当了客户端的角色。

pic_ca36c11d.png

图10 Thrift使用详细流程图

  • 生成文件:由服务端定义IDL接口描述文件,然后基于IDL文件转换为对应语言的代码文件,由于我们用的是Node语言,所以转换成JavaScript文件。

  • 服务端启动服务:引入生成的JavaScript文件,解析接口、处理接口、启动并监听服务。

  • 服务注册:通过服务器内置的“服务治理代理”,将服务注册到美团内部的服务注册路由中心(也就是命名服务),让服务可被调用方发现。

  • 数据传输:被调用时,根据“服务治理服务”协议序列化和反序列化,与其他服务进行数据传输。

目前,美团内部已经有相对成熟的NPM包服务,已经帮我们实现了服务注册、数据传输、服务发现和获取流程。客户端如果想调用我们所提供的的Open API开放能力,首先申请AppKey,然后选择使用Thrift方式或者HTTP的方式,按照所要求的参数进行请求调用即可。

3.4 方案实践

3.4.1 H5协议

能力:富文本编辑。

描述:提供富文本可视化编辑,产品和运营无需前端就可以发布和二次编辑页面。

场景:文本协议,消息通知,产品FAQ。

具体案例:

pic_f883a4d1.png

图11 H5静态文本协议案例

3.4.2 业务自定义渲染

能力:开放API(Thirft + HTTP)。

描述:提供开放API,支持业务自定义和样式渲染到业务系统,同时解决了iframe体验问题。

场景:客户端、后端、小程序的同学,可根据API渲染文案,实现动态化管理富文本信息。

具体案例:

小程序使用组件、Vue使用v-html指令实现动态化渲染商品选择说明。

{
   "code": 0,
   "data": {
     "tag": "苹果,标准",
     "title": "如何挑选苹果",
     "html": "<h1>如何挑选苹果</h1>><p>以下标准可供消费者参考</p><ul><li>酸甜</li><li>硬度</li></ul>",
     "css": "",
     "js": "",
     "file": {}
  },
   "msg": "success"
}

3.4.3 投放需求

能力:WebIDE代码编辑。

描述:开发基于WebIDE代码开发工作,基于渠道和环境修改下载链接,能够做到分钟级支撑。

场景:根据产品创建静态页面进行逻辑和样式开发。

具体案例:

var ua = window.navigator.userAgent
   var URL_MAP = {
       ios: 'https://apps.apple.com/cn/app/xxx',
       android: 'xxx.apk',
       ios_dpmerchant: 'itms-apps://itunes.apple.com/cn/app/xxx'
  }
   
   if (ua.match(/android/i)) location.href = URL_MAP.android
   if (ua.match(/(ipad|iphone|ipod).*os\s([\d_]+)/i)) {
       if (/xx\/com\.xxx\.xx\.mobile/.test(ua)) {
           location.href = URL_MAP.ios_dpmerchant
      } else {
           location.href = URL_MAP.ios
      }
  }

3.4.4 客户端通信中间页

能力:WebIDE代码编辑 + 物料平台。

描述:通过物料平台,引入公司客户端桥SDK,可以快速完成客户端通信需求。方便前端调试客户端基础桥功能。

场景:客户端跳转,通信中间页。

具体案例:

// 业务伪代码
   XXX.ready(() => {
       XXX.sendMessage({
          sign: true,
           params: {
               id: window.URL
          }
      }, () => {
           console.error('通信成功')
      }, () => {
           console.error('通信失败')
      })
  })

3.4.5 业务系统内嵌Page

能力:提供胶水层Page-SDK,连接业务系统和Page。

描述:业务系统与Page-佩奇平台可进行通信,业务系统可调用Page发布、预览、编辑等功能,Page可返回业务系统页面链接、内容、权限等信息。减少重复前后端工作,提升研发效率。

场景:前端富文本信息渲染,后端富文本信息管理后台。

具体案例:

pic_ec5b5f49.png

图12 业务系统内嵌Page案例

3.5 业务成绩

截止目前数据统计,Page-佩奇平台生成网页5000多个,编辑页面次数16000多次,累计页面访问PV超过8260万。现在,美团已经有十多个部门和三十多条业务线接入并使用了Page-佩奇平台。

pic_becbff8a.png

图13 Page-佩奇平台每日生成页面统计

四、总结与展望

富文本编辑器和WebIDE不仅是复杂的系统,而且还是比较热门的研究方向。特别是在和美团的基建结合之后,能够解决团队内部很多效率和质量问题。这套系统还提供了语法智能提示、Diff对比、前置检测、命令行调试等功能,不仅要关注业务发布出去页面的稳定性和质量,更要有内置的一系列研发插件,主动帮助研发提高代码质量,降低不必要的错误。

经过长期的技术和业务演进,Page-佩奇平台已经能够有效地帮助研发人员大幅提升开发效率,具备初级的Design To Code能力,但是仍有许多业务场景值得去我们探索。我们也期待优秀的你参与进来,一起共同建设。

  • WebIDE融合:完善基础设施建设和功能需求,更好地支持Vue、React、ES 6、TS、Less语法,预览模式采用浏览器编译,能有效地提高预览的速度,发布使用后端编译的模式。

  • 研发流程链路:针对代码进行有效评估,包括ESlint、代码重复率、智能提示是否可以三方库替代。出具开发代码质量、业务上线的质量报告。

  • 综合研发平台:减少团队同学了解整体基建的时间成本,内置了监控、性能、任务管理等功能,提升业务开发效率。建设自动化日报、周报系统,降低非开发工作量占比。

  • 物料开放能力:接入公共组件平台,沉淀更多的物料,快速满足产品更多样化的需求。

五、作者简介

高瞻、宇立、肖莹、浩畅,来自美团医药终端团队。王咏、陈文,来自美团闪购终端团队。
来源:https://blog.csdn.net/MeituanTech/article/details/121551030

收起阅读 »

换一个方式组织你的Axios代码?

自从Jquery被mvvm平替了之后,$.ajax 也被 axios 平替了,在使用这个方式之前,我想大部分的人也想到了去封装一个请求,然后每一次调用去做Get 、Post的请求服务,也有一些人习惯在vue里直接编写 this.$axios.get() ,萝卜...
继续阅读 »

自从Jquery被mvvm平替了之后,$.ajax 也被 axios 平替了,在使用这个方式之前,我想大部分的人也想到了去封装一个请求,然后每一次调用去做Get 、Post的请求服务,也有一些人习惯在vue里直接编写 this.$axios.get() ,萝卜青菜各有所爱,没有优劣之分。



灵感来源


d4axios的灵感来源于open-figen,现在功能还没有那么丰富,但是足以应付多大多数的场景,比如上传、下载、get、post等等请求,配合上ts,可以解决数据类型前后台一致性,数据类型的转换,保证了请求的便利性。让代码专注于数据处理,而非复制粘贴模版代码



别忘了 在 [ts | js]config.json 文件里开启对装饰器的支持。"experimentalDecorators":true



在项目中使用 d4axios



d4axios (Decorators for Axios) 是一款基于axios请求方式的装饰器方法组,可以快速地对服务进行管理和控制,增强前端下服务请求方式,有效降低服务的认知难度,并且在基于ts的情况下可以保证服务的前后台的数据接口一致性



npm i d4axios

yarn add d4axios

一、 引入配置信息


在这里提供了几种配置方式,可以采用原始的axios的配置方法,也可以采用 d4axios 提供的方法


// 在 vue3下我们建议使用 `createService` 
// 其他情况下使用 `serviceConfig`
import { createApp } from 'vue'
import {createService,serviceConfig} from 'd4axios'


createApp(App).use(createService({ ... /* 一些配置信息 */}))


1.1 提供的axios配置项


createServiceserviceConfig 使用的配置项是一样的,并且完全兼容axios的配置。在现有的项目中改造的话,可以使用:


// 可以直接使用由d4axios提供的服务
createService()

// 可直接传入axios的相关配置,由d4axios自动基于配置相关构建
createService({axios:{ baseURL:"domain.origin" }})

// 可直接传入已经配置好的 `axios` 实例对象
const axios = Axios.create({ /* 你的配置*/ });


createService({axios})

1.2 提供基于请求和相应数据的配置


createService({
beforeRequest(requestData){
// form对象会被转为JSON对象的浅拷贝,但是会在该方法执行完后重新转为form对象
// 你可在请求前追加一些补充的请求参数
// 比如对请求体进行签名等等
return requestData
},
beforeResponse(responseData){
// 默认情况下会返回 axios的response.data值,而不会返回response的完整对象
// 可以修改返回的响应结果
return responseData
}
})

1.3 提供快速的axios interceptors 配置


createService({
interceptors:{
request:{
use(AxiosRequestConfig){},
reject(){}
},
response{
use(AxiosResponse){},
reject(){}
}
}
})

配置完成后,会返回一个axios实例对象,可以继续对axios对象做更多的操作,可以绑定到vue.prototype.$axios下使用


Vue.prototype.$axios = serviceConfig({... /*一些配置*/})

二、创建请求服务


为了更好的组织业务逻辑,d4axios提供了一系列的组织方法,供挑选使用


import {Service,Prefix,Get,Post,Download,Upload,Param,After,Header} from 'd4axios'

@Service("UserService") // 需要提供一个服务名称,该名称将在使用服务的时候被用到
@Prefix("/user") // 可以给当前的服务添加一个请求前缀
export class UserService {

@Get("/:id") // 等同于 /user/:id
@After((respData)=>{
//在输出给最终结果前,可以对结果做一些简单处理
return respData
})
async getUserById(id:string){
// 异步请求需要增加 `async` 属性以便语法识别
// 支持restful的方式
}


@Post("/regist")
@Header({'plantform':'android'}) // 请求前追加一些header参数
async registUser(form:UserForm){
// 可以在请求的时候做一些参数修改
form.nickName = createNickName(form.nickName);

// return的值是最终请求的参数
return {
...form,
plant:"IOS"
};
}

@Download("/user/card") // 支持文件下载
async downloadCard(@Param("id") id:stirng){
// 当我们的参数较少并且不是一个key-value形式的值时
// 可以使用@Param辅助,确定传参名称
}

@Upload("/user/card") // 支持文件上传
async uploadCard(file:File){
return {file}
}

// 可以定义同步函数,直接做服务计算
someSyncFunc(){
return 1+1
}

// 我们还可以直接定义非请求函数
async someFunc(){
// 所有的当前函数都是可以直接调用的
return await this.getUserById(this.someSyncFunc());
}

}


三、使用服务



使用服务分为几种方式,第一种是在一个服务中调用另一个服务。第二种是在react或者vue中调用服务,对于这两种有不同的方法,也可以用相同的方法。



3.1 在 vue或者react中使用useService 导入服务


// 在 vue 或者 react中,可以直接使用 useService 导入一个服务对象
import {useService} from 'd4axios'
import SomeService from './some.service'

const someService = useService(SomeService)

复制代码

3.2 在一个服务中Use调用另一个服务


import {Use} from 'd4axios'
import SomeService from './some.service'
// 也可以直接像上面一样的导入进来是用
const someService = useService(SomeService)

@Service("MyService")
export class MyService {
@Use(SomeService) // use 导入服务
// 默认的属性名为小写驼峰
// 用 S<T> 包裹服务名称,这样可以得到相应的async方法的响应类型
someService !: S<SomeService>

async someMethod(){
// 就可以使用了,
await this.someService.something();
}
}

四、响应重写


默认情况下,d4axios支持async响应类型值,该值为


 export interface ResponseDataType<T> { }

在项目根路径下定义 d4axios.d.ts文件
然后文件内定义,通过重写该类型,可以得到响应的 response type类型,比如



export interface ResponseDataType<T> {
data : T;
msg:string ;
code:string ;
}

后即可以得到相关内容的提示信息


dataType.png


五、其他一些基于 Decorators 的操作


5.1 在使用装饰器的class上都可以使用 Use 导入服务 比如:


import {Component,Vue,} from 'vue-class-decorator'
import SomeService from './some.service'

@VueServiceBind(MyService,OtherService) // 只能在vue的这种形式下使用,可以绑定多个值
@Component
export default class MyVueComp extends Vue {

@Use(SomeService) // use 导入服务
// 默认的属性名为小写驼峰
// 用 S<T> 包裹服务名称,这样可以得到相应的async方法的响应类型
someService !: S<SomeService>

myService !: S<MyService>

otherService !: S<OtherService>
}

5.2 在一般的vue的服务下可以使用这种 mapService 形式


// 传统的模式下

import { mapService } from 'd4axios';
import MyService from './MyService.service'

export default {
computed:{
...mapService(MyService,OtherService)
},
created(){
this.myService.getName(10086);
}

作者:非思不可
链接:https://juejin.cn/post/7041930275458285582

收起阅读 »

如何在浏览器 console 控制台中播放视频?

如何在浏览器 console 控制台中播放视频? 要实现这个目标,主要涉及到这几个点: 如何获取和解析视频流? 如何在 console 里播放动态内容? 如何在 console 里播放彩色内容? 如何连接视频流和 console? 事实上最后的代码极其简单...
继续阅读 »

如何在浏览器 console 控制台中播放视频?


要实现这个目标,主要涉及到这几个点:



  1. 如何获取和解析视频流?

  2. 如何在 console 里播放动态内容?

  3. 如何在 console 里播放彩色内容?

  4. 如何连接视频流和 console?


事实上最后的代码极其简单,我们就一步一步简单讲一下


效果



测试地址:yu-tou.github.io/colors-web/…


如何获取和解析视频流?


这里我们用电脑摄像头捕获视频流,然后获取视频流每一帧的图像数据,作为下一步的输入。


// 捕捉电脑摄像头的视频流
const mediaStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
// 创建一个 video 标签
const video = document.createElement("video");
document.body.appendChild(video);

video.onloadedmetadata = function (e) {
video.play(); // 等摄像头数据加载完成后,开始播放
};
// video 标签播放视频流
video.srcObject = mediaStream;

如何获取每一帧图像的数据?创建一个 canvas 画布,可以将 video 当前的内容绘制到画布上,然后通过 canvas 的方法即可拿到图像的像素数据。


const ctx = canvas.getContext("2d");
canvas.width = width;
canvas.height = height;

ctx.drawImage(video, 0, 0, width, height);
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
// imageData 的结构是平铺的,需要自己去学习下

如何在 console 里播放动态内容?


视频每帧的图像内容我们已经可以拿到了,继续下一步,如果需要在 console 中完成播放视频,首先需要能够一帧一帧绘制内容,但是这个好像是不太现实的,console.log 只能输出文本。


回想远古时代,在终端里大家怎么播放视频的?没错,用字符画一帧一帧绘制,连起来不就是动态的视频了。


当然 chrome dev tool 里如果每一帧绘制后都调用 console.clear() 清空重绘,体验不是很好,闪烁会很严重,所以我们采用持续输出的方式绘制,当你停留在 console 的最后的时候,看起来也算是动态内容了。


如何在 console 里播放彩色内容?


console.log 支持部分 css 特性,可以为输出的字符串指定简单的样式,最基本的支持背景色、字体颜色、下划线等,甚至支持 background-image、padding 等特性,利用这些特性,甚至可以插入图片,但是这些特性在不同浏览器的 console 中或多或少有些兼容问题,不过要实现字体着色,或者输出色块(用背景色),问题不大。


我们在此使用 colors-web 来更方便地输出彩色内容到控制台。


这是一个非常方便的库,可以使用链式调用在控制台快速输出彩色内容,并且支持诸多特性,无需自己去了解,直接使用对应的方法即可。


如:


import { logger, colors } from "colors-web";
logger(
colors().red().fontsize(48).fontfamily("SignPainter").log("hello"),
colors().white.redBg("hello").linethrough(),
"world",
colors().white.padding(2, 5).underline().lightgrey("芋头")
);

相信我不解释,大家也基本理解这些用法,非常简单和自由,而且支持 typescript。


我们这里,用 colors-web 输出色块:


for (let i = 0; i < height; i++) {
for (let j = 0; j < width; j++) {
if (i * width + j < data.length) {
const color = `rgb(${data[(i * width + j) * 4 + 0]},${data[(i * width + j) * 4 + 1]},${
data[(i * width + j) * 4 + 2]
})`;
colors()
.bg(color)
.color(color)
.fontfamily(/Chrome/.test(navigator.userAgent) ? "Courier" : "")
.log("╳");
}
}
}

最终逻辑


最终我将每一帧所有的像素值都转换成一个 colors 的实例,记录到数组之后,最终统一调用 logger 即可完成一帧的渲染。


const frameColors = [];
for (let i = 0; i < height; i++) {
for (let j = 0; j < width; j++) {
if (i * width + j < data.length) {
const color = `rgb(${data[(i * width + j) * 4 + 0]},${data[(i * width + j) * 4 + 1]},${
data[(i * width + j) * 4 + 2]
})`;
frameColors.push(
colors()
.bg(color)
.color(color)
.fontfamily(/Chrome/.test(navigator.userAgent) ? "Courier" : "")
.log("╳")
);
}
}
}
// 绘制,colors() 只是在准备数据结构,logger 才是真正的输出
logger(...frameColors);

大公告成啦!


作者:芋头君
链接:https://juejin.cn/post/7013620775143866376

收起阅读 »

某科技公司前端三面面经

okay, it's me again. 哈哈哈我怎么也没想到,我又会经历多一次三面,这次可以说是被狠狠的按在地上摩擦了,没办法,只能奉行一贯的“技术不够,吹牛来凑”原则 btw 应该看多点别人的面经,而不是自己写面经,当然自己写也可以当作一个很好的复盘 这次...
继续阅读 »

okay, it's me again.


哈哈哈我怎么也没想到,我又会经历多一次三面,这次可以说是被狠狠的按在地上摩擦了,没办法,只能奉行一贯的“技术不够,吹牛来凑”原则


btw 应该看多点别人的面经,而不是自己写面经,当然自己写也可以当作一个很好的复盘


这次是某家准备上市的公司,公司的技术部门也是挺强大的,所以也才会有三面吧可能


其实复盘过程中记的也不太清楚,只能说想起来一点写一点,这里建议将整个面试过程录音,以便做一次彻底的复盘,当然最好还是取得面试官同意才这么做


面试流程:boss直聘聊 -> 发邮件邀约面试 -> 一面技术面 -> 二面技术面 -> 三面HR面 -> 电话沟通薪资和入职事宜 -> offer


一面技术面




  1. 自我介绍




  2. 一个业务场景,PC端用vue做后台管理系统的时候,一般路由是动态生成的,前端的文件与路由是一一对应的,假如不小心删了一个文件,这个时候就会跳404页面,会有不好的用户体验,怎么做才能比较好的防止跳去404页面?




  3. 有一个页面,一个绝对够长的背景图,我们知道不给盒子设定高度的情况下默认是100%的高度,盒子高度会被内容所撑开。那么怎么做到第一屏完全显示背景图,第二屏也能继续显示呢?


    好,来看我的第一个错误回答🤣


    <style>
    * {
    margin: 0;
    padding: 0;
    }
    .container {
    width: 100%;
    height: 100vh;
    background-image: url('./assets/images/long.jpeg');
    }
    </style>

    <body>
    <div class="container">
    <p>1</p>
    这里复制出足够多的<p>1</p>就好,我就不贴出来重复代码占据太大篇幅了
    </div>
    </body>


    这是第一屏的效果,嗯很好完全没有问题! 但是当我们鼠标来到第二屏就哦豁了🙈




WechatIMG83.jpeg


然后我的第二个回答是:将图片绝对定位,这样图片就能适应不管多少屏了,但是图片绝对定位的话,没有内容撑开,那么第一屏根本都不会出现背景,所以这样也是不行的😅


答案:将 height: 100vh; 换成 min-height: 100vh;就可以了😂




  1. 我们都知道在谷歌浏览器里面字体的最低像素是 12px ,就算设置font-size: 8px;也会变成 12px ,我现在有一个需求需要 8px 的字体,怎么才能突破 12px 的限制?


    基本原理是使用css3的 transform: scale(); 属性


    需求是 8px 的字体,那我们就 font-size: 16px; transform: scale(0.5); 即可




  2. 讲一下 ES6 的新特性




  3. 说一些你经常用到的数组的方法




  4. 前端性能优化


    传送门:聊一聊前端性能优化




  5. 原型链


    传送门:继承与原型链


    传送门:JavaScript原型系列(三)Function、Object、null等等的关系和鸡蛋问题




  6. 假设在一个盒子里,里面所有小盒子的宽高都是相等的(PS技术不好,画的不相等),大盒子刚好放得下7个小盒子,使用css实现下面的布局




WechatIMG84.png




  1. 讲一下微信登录流程




  2. 怎么给每个请求加上 Authorization token ? (考察封装请求,axios 拦截器)




  3. 讲一下 vue 的双向数据绑定原理




  4. 移动端防止重复点击,防抖节流




  5. 怎么触发BFC,有什么应用场景?




  6. Promise有哪几种状态?




  7. 如果现在有一个任务,让你来做主力开发,架构已经搭好了,UI设计图也已经出完了,那你第一步会做什么?




  8. 后台管理系统怎么做权限分配?




  9. 怎么判断一个对象是否为空对象?




  10. 数字1-50的累加,不用 for 循环,用递归写


    因为我很抗拒当场写代码,然后满脑子都是1-50的累加为什么不用 for 循环,用 for 循环不是更快吗?为什么要用递归?但是面试官都把纸笔递过来了,没办法也是只能硬着头皮上了,但是这也是很简单的一道题,下面贴出当时手写的代码(是错的)


        // 这是错的这是错的这是错的
    function add(n) {
    let sum = 0;

    if (n > 0) {
    sum += add(n - 1);
    } else {
    return sum;
    }
    }

    // 这是根据上面改进之后的写法
    function add(n, sum) {
    if (n > 0) {
    return add(n - 1, (sum += n));
    } else {
    return sum;
    }
    }

    // 当然还有一种更为优雅与简便的写法
    function add(n) {
    return n === 0 ? 0 : n + add(n - 1);
    }

    // 想一行代码搞定的话就是
    const add = (n) => (n === 0 ? 0 : n + add(n - 1));



  11. 怎么解决 vuex 里的数据在页面刷新后丢失的问题?




  12. 说一下 vue 组件通信有几种方式(老生常谈的问题)




  13. 说一下 vue 和微信小程序各自的生命周期




  14. 看一下这个 ts 问题


        let num: string = '1';
    转一下数据类型转成 number



  15. 说一下 ts 总共有多少种数据类型




二面技术面




  1. 封装一个级联组件,讲一下思路




  2. 封装 v-model




  3. POST请求的 Content-Type 有多少种?




  4. css flex: 1; 是哪几个属性的组合写法




  5. vue provide/inject 的数据不会及时回流到父组件的问题(我记得没错的话好像是这么问的)




  6. 不用Promise的情况下,怎么实现一个Promise.all()方法




  7. [1, 2, 3].map((item, index) => parseInt(item, index))的结果


    这里考察了两点,1是parseInt()方法的第二个参数有什么作用,2是进制转换的相关知识




  8. cookie,sessionStorage,localStorage 3者之间有什么区别?




  9. http://www.xxx.com (a网站) 和 http://www.api.xxx.com (b网站) 两个网站,在b网站里登录授权拿到了 cookie ,怎么在a网站里拿到这个 cookie ?




  10. 说一下 forEach, map, for...in, for...of 的区别




  11. git fetch和git pull的区别(最后一道题)


    git pull:相当于是从远程获取最新版本并 merge 到本地


    git fetch:相当于是从远程获取最新版本到本地,不会自动 merge


    区别就是会不会自动 merge




三面HR面


这里就不展开了,HR面差不多都是那些东西


以上


其实一面二面还有很多问题都没有写出来,但是碍于当时也没有录音,只记得这么多


严格来讲,这并不太算是一篇面经,在上面很多都只是抛出了问题,因为技术的原因并没有做出相应的解答,还是有些遗憾的



作者:Lieo
链接:https://juejin.cn/post/7021394272519716872

收起阅读 »

亲身经历,大龄程序员找工作,为什么这么难!

背景 临近年底,公司还在招人,可筛选的人才真是越来越少,这可能是因为大家都在等年终奖吧。于是在简历筛选时,将学历和年龄都适当的放松了。正因为如此,面试了不少大龄的程序员。 网络上一直有讨论大龄程序员找工作困境的话题,对于我个人来说,是将信将疑的,但作为程序员对...
继续阅读 »

背景


临近年底,公司还在招人,可筛选的人才真是越来越少,这可能是因为大家都在等年终奖吧。于是在简历筛选时,将学历和年龄都适当的放松了。正因为如此,面试了不少大龄的程序员。


网络上一直有讨论大龄程序员找工作困境的话题,对于我个人来说,是将信将疑的,但作为程序员对自己职业生涯和未来的危机感还是有的。同时,作为技术部门领导,我是不介意年龄比我大,能力比我强的人加入的,只要能把事做好,这都不是事。


随着互联网的发展,大量程序员必然增多,都找不到工作是不可能的。而且中国的未来必然也会像发达国家一样,几十岁甚至一辈子都在写代码,也不是有可能的。


那么,我们担忧的是什么呢?又是什么影响了找工作呢?本文就通过自己亲身面试的几个典型案例来说说,可能样本有些小,仅供参考,不喜勿喷。


大厂与小厂招人的区别


前两天在朋友圈发了一条招人的感慨,关于大厂招人和小公司招人的区别。


大厂:有影响力,有钱,能够吸引了大量的应聘者。因此,也就有了筛选的资格,比如必须985名校毕业,必须35岁以下,不能5年3跳,必须这个……不能那个……当员工不合适时,绩效分给的低点或直接赔钱让其出局。


小公司:没有品牌,资金有限,每一分钱都要精打细算。招聘的人选有限,在这有限的选择范围内,还要考虑成本、能不能用、能不能留住等问题。能力太强,给不起钱,留不住;能力太弱,只会让项目越来越糟糕;所以,最好的选择只能是稍微高于现有团队能力,又不至于轻易跳槽的人。


有了上面的基本前提,再来看看大厂与小厂对待大龄程序员的差别。


对于35岁以后的程序员,有的大厂已经直接卡死,也就别死磕了。另外一些大厂还是开放的,但肯定是有一定的要求的,比如职位必须达到什么等级,能力必须达到什么要求。换句话说,如果你是牛人,其实35岁并不是什么问题,如果不是,那么这个选项几乎不存在。


所以,大厂的选择基本上等于没什么选择。再来看看小公司,小公司追求的核心是性价比,或者直白点说就是能干事且节省成本。另外就是能不能一职多能,能不能带新人,能不能加班……


个人看来,相对于大厂的要求,小公司的要求稍微努力一下还是可以满足的。对于加班这一项,不是所有的公司都有加班文化,也不是所有的公司常年需要加班。


招聘案例


挑选面试中几个比较典型的案例来聊聊,看看对你有什么启发。


案例一


84年的应聘者,自己在简历上填写的是应聘“中高级Java开发”。面试中,各项技能都平平,属于有功能开发经验,但没有深钻技术,没有考虑更好解决方案的状态。明确加班不能超过9点。也有写博客和公众号。9月份离职,目前暂未未找到工作。


就这位应聘者而言:第一,能接受员工比自己年龄大的领导不多,因为担心管不住;第二,技能没有亮点,就像他自己定位的那样,十多年工作经验,只是中高级开发;第三,加班这一项卡的太死,哪家公司上个线不到10点以后了,有突发需求不加个班?


案例二


86年的应聘者,别家公司裁员,推荐过来的简历。十来年工作经验,一直负责一个简单彩会不会是敏感词票业务的开发,中间有几年还没有项目履历。简历上写的功能还是:用户管理、权限管理、XX接口对接。推荐他的老板,给他的定位是中级开发。


这位应聘者,真的是将一年的代码写了十年。上家老板裁员选择了他,定位也只是中级,然后帮忙推荐了他到其他公司。这背后的故事,你仔细品,再仔细品。


案例三


87年的应聘者,学历一般,这两年的工作履历有点糟糕,跳槽的时机选择的也不好。长期从事支付行业,十来年的工作履历中,有七八年在做支付及相关的行业,其中在一家支付公司工作了四年。面试中,特意问了行业中的一些解决方案、技术实现,都没问题。基础知识稍微有点弱,但影响不大。面试过后,发了Offer,其中我还在老板面前帮忙争取了一把。


这位应聘者,虽然在学历,近两年的经历,基础知识上都略有不足。但他的行业经验丰富,给人一种踏实做事的感觉。整体能力恰好符合上面提到的小公司选择标准:比现有团队人能力强,有行业经验,薪资适中,稳定性较好。他的长板完全弥补了短板。


案例四


91年的应聘者,一家小有名气二线互联网公司出来。最近半年未工作,给出的原因是:家中有事,处理了半年,现在决定找工作了。聊半年前做过的项目,基本上记不起逻辑了;聊技术知识,也只能说个大概,但能感觉还是做过一些功能的,但没有深入思考过或没有做过复杂逻辑。


这位应聘者,不确定已经面试多久了,但应该不那么容易找工作。第一,半年未工作,即使有原因,也让人多少有些顾虑;第二,面试前完全没做功课,这不是能力问题,而是态度的问题了。


上面的案例,有成功的也有失败的。总结一下失败的原因,基本上有几点:能力与年龄不匹配、不能加班、家庭影响、没有特长……当然,你如果能看到其他的失败原因,那就更好了。


小结


上面只是最近一段时间面试的几个典型案例,至于你能从中获得什么,能不能提前行动,做好准备。那就是大家自己的事了。当然,还是那句话,样本有些小,但也能说明一些问题。仅个人观点,不抬杠,不喜勿喷。


我也曾为职场的未来担忧,也曾为年龄担忧,但始终未放弃的就是持续学习和思考。多位朋友都曾说过:无论你是否当上领导,是否还在写代码,技术能力都不能丢,你必须是团队中技术最牛的那一个。我一直在努力做到,你呢


作者:程序新视界
链接:https://juejin.cn/post/7043589223345029133

收起阅读 »

JavaScript函数封装随机颜色验证码

数字或者字母或者数字字母混合的n位验证码带随机的颜色。下面是完整的代码,需要的自取哈!function verify(a = 6,b = "num"){ //定义三个随机验证码验证码库 var num ="0123456789" var str ="ab...
继续阅读 »

数字或者字母或者数字字母混合的n位验证码带随机的颜色。下面是完整的代码,需要的自取哈!

function verify(a = 6,b = "num"){
//定义三个随机验证码验证码库
var num ="0123456789"
var str ="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNIPQRSTUVWXYZ"
var mixin = num +str;
 
//定义一个空字符串用来存放验证码
var verify=""
if(a == undefined || b == undefined){
  //验证输入是否合法 不通过就抛出一个异常
  throw new Error("参数异常");
}else{
    if(a ==""||b==""){
      //判断用户是否没有输入
      throw new Error("参数非法.");
    }else{
      //检测输入的类型来判断是否进入
      var typea = typeof(a);
      var typeb = typeof(b);
      if(typea =="number" && typeb =="string"){
          if(b == "num"){
                 
              //定义一个循环来接收验证码   纯数字验证码
              for(var i=0;i<a;i++){
                    //定义一个变量来存储颜色的随机值
                    var r1 = Math.random()*255;
                    var g1 = Math.random()*255;
                    var b1 = Math.random()*255;

                    //确定随机索引
                    var index = Math.floor(Math.random()*(num.length-1))
                    //确定随机的验证码
                    var char = num[index];
                    //给随机的验证码加颜色
                    verify += `<span style ='color:rgb(${r1},${g1},${b1})'>${char}</span>`
                }
                //返回到数组本身
              return verify;
          }else if(b =="str"){
                for(var i=0;i<a;i++){
                  //纯字母的验证码
                  var r1 = Math.random()*255;
                  var g1 = Math.random()*255;
                  var b1 = Math.random()*255;

                  var index = Math.floor(Math.random()*(str.length-1));
                  var char = str[index];

                  verify += `<span style ='color:rgb(${r1},${g1},${b1})'>${char}</span>`
                }
                return verify;  
          }else if(b == "mixin"){
                // 混合型的验证码
              for(var i=0;i<a;i++){
                  var r1 = Math.random()*255;
                  var g1 = Math.random()*255;
                  var b1 = Math.random()*255;

                  var index = Math.floor(Math.random()*(mixin.length-1));
                  var char = mixin[index];

                  verify += `<span style ='color:rgb(${r1},${g1},${b1})'>${char}</span>`
              }
              return verify;
          }else{
              //验证没通过抛出一个异常
              throw new Error("输入类型非法.")
          }
       
      }else{
          //验证没通过抛出一个异常
          throw new Error("输入类型非法.")
      }
    }
}
}

下面我们来调用函数试试看

  //第一个值为用户输入的长度,第二个为类型! 
var arr = verify(8,"mixin");
    document.write(arr)

上面就是结果啦!

这个记录下来为了方便以后使用的方便,也希望大佬们多多交流,多多留言,指出我的不足的之处啦!

有需要的小伙伴可以研究研究啦!!
————————————————
作者:土豆切成丝
来源:https://blog.csdn.net/A20201130/article/details/122030872

收起阅读 »

localhost、127.0.0.1和0.0.0.0和本机IP的区别

localhostlocalhost其实是域名,一般windows系统默认将localhost指向127.0.0.1,但是localhost并不等于127.0.0.1,localhost指向的IP地址是可以配置的 127.0.0.1首先我们要先知道一...
继续阅读 »
localhost
localhost其实是域名,一般windows系统默认将localhost指向127.0.0.1,但是localhost并不等于127.0.0.1,localhost指向的IP地址是可以配置的
 
127.0.0.1
首先我们要先知道一个概念,凡是以127开头的IP地址,都是回环地址(Loop back address),其所在的回环接口一般被理解为虚拟网卡,并不是真正的路由器接口。
所谓的回环地址,通俗的讲,就是我们在主机上发送给127开头的IP地址的数据包会被发送的主机自己接收,根本传不出去,外部设备也无法通过回环地址访问到本机。
 
小说明:正常的数据包会从IP层进入链路层,然后发送到网络上;而给回环地址发送数据包,数据包会直接被发送主机的IP层获取,后面就没有链路层他们啥事了。
而127.0.0.1作为{127}集合中的一员,当然也是个回环地址。只不过127.0.0.1经常被默认配置为localhost的IP地址。
一般会通过ping 127.0.0.1来测试某台机器上的网络设备是否工作正常。
 
0.0.0.0
首先,0.0.0.0是不能被ping通的。在服务器中,0.0.0.0并不是一个真实的的IP地址,它表示本机中所有的IPV4地址。监听0.0.0.0的端口,就是监听本机中所有IP的端口。
 
本机IP
本机IP通常仅指在同一个局域网内,能同时被外部设备访问和本机访问的那些IP地址(可能不止一个)。像127.0.0.1这种一般是不被当作本机IP的。本机IP是与具体的网络接口绑定的,比如以太网卡、无线网卡或者PPP/PPPoE拨号网络的虚拟网卡,想要正常工作都要绑定一个地址,否则其他设备就不知道如何访问它。
 
 

localhost
首先 localhost 是一个域名,在过去它指向 127.0.0.1 这个IP地址。在操作系统支持 ipv6 后,它同时还指向ipv6 的地址 [::1] 
在 Windows 中,这个域名是预定义的,从 hosts 文件中可以看出:
# localhost name resolution is handled within DNS itself.
# 127.0.0.1 localhost
# ::1 localhost
 
而在 Linux 中,其定义位于 /etc/hosts 中:
127.0.0.1 localhost
 
注意这个值是可修改的,比如把它改成
192.068.206.1 localhost
 
然后再去 ping localhost,提示就变成了
PING localhost (192.168.206.1) 56(84) bytes of data.
 
127.0.0.1
127.0.0.1 这个地址通常分配给 loopback 接口。loopback 是一个特殊的网络接口(可理解成虚拟网卡),用于本机中各个应用之间的网络交互。只要操作系统的网络组件是正常的,loopback 就能工作。Windows 中看不到这个接口,Linux中这个接口叫 lo:
#ifconfig
eth0 Link encap:Ethernet hwaddr 00:00:00:00:00:00
  inet addr :192.168.0.1 Bcase:192.168.0.255 Mask:255.255.255.0
  ......
lo     Link encap:Local Loopback
  inetaddr: 127.0.0.1 Mask: 255.0.0.0
       ......
 
可以看出 lo 接口的地址是 127.0.0.1。事实上整个 127.* 网段都算能够使用,比如你 ping 127.0.0.2 也是通的。 
但是使用127.0.0.1作为loopback接口的默认地址只是一个惯例,比如下面这样:
#ifconfig lo 192.168.128.1
#ping localhost  #糟糕,ping不通了
#ping 192.128.128.1 # 可以通
#ifconfig lo
lo  Link encap:Local Loopback
  inetaddr: 192.168.128.1 Mask: 255.255.255.0
     ......
 
如果随便改这些配置,可能导致很多只认 127.0.0.1 的软件挂掉。
 
本机IP
确切地说,“本机地址”并不是一个规范的名词。通常情况下,指的是“本机物理网卡所绑定的网络协议地址”。由于目前常用网络协议只剩下了IPV4,IPX/Apple Tak消失了,IPV6还没普及,所以通常仅指IP地址甚至ipv4地址。一般情况下,并不会把 127.0.0.1当作本机地址——因为没必要特别说明,大家都知道。 
本机地址是与具体的网络接口绑定的。比如以太网卡、无线网卡或者PPP/PPPoE拨号网络的虚拟网卡,想要正常工作都要绑定一个地址,否则其他设备就不知道如何访问它。
● localhost 是个域名,不是地址,它可以被配置为任意的 IP 地址,不过通常情况下都指向 127.0.0.1(ipv4)和 ::1 
● 整个127.* 网段通常被用作 loopback 网络接口的默认地址,按惯例通常设置为 127.0.0.1。这个地址在其他计算机上不能访问,就算你想访问,访问的也是自己,因为每台带有TCP/IP协议栈的设备基本上都有 localhost/127.0.0.1。 
● 本机地址通常指的是绑定在物理或虚拟网络接口上的IP地址,可供其他设备访问到。 
● 最后,从开发度来看 
○ localhost是个域名,性质跟 “www.baidu.com” 差不多。不能直接绑定套接字,必须先gethostbyname转成IP才能绑定。 
○ 127.0.0.1 是绑定在 loopback 接口上的地址,如果服务端套接字绑定在它上面,你的客户端程序就只能在本机访问

原文链接:https://www.cnblogs.com/absoluteli/p/13958072.html
收起阅读 »

如何用JavaScript实现双向映射?

本文翻译自 《How to create a Bidirectional Map in JavaScript》双向映射是指在键值对中建立双向一一对应关系的一种模式。它既可以通过键名(key)去获取值(value),也可以通过值去获取键名。让我们看下如何在Jav...
继续阅读 »



本文翻译自 《How to create a Bidirectional Map in JavaScript》

双向映射是指在键值对中建立双向一一对应关系的一种模式。它既可以通过键名(key)去获取值(value),也可以通过值去获取键名。让我们看下如何在JavaScript中实现一个双向映射,以及 TypeScript 中的应用。

双向映射背后的计算机科学与数学

首先看一下双向映射的基本定义:

在计算机科学中,双向映射是由一一对应的键值对组成的数据结构,因此在每个方向都可以建立二元关系:每个值也可以对应唯一的键。

百科指路双向映射

计算机科学中的双向映射,源于数学上的双射函数。双射函数是指两个集合中的每个元素,都可以在另一个集合中找到与之匹配的另一个元素,反之也可以通过后者找到匹配的前者,因此也被叫做可逆函数。

百科指路: 双射函数

扩展:

  • 单射(injection):每一个x都有唯一的y与之对应;

  • 满射(surjection):每一个y都必有至少一个x与之对应;

  • 双射(又叫一一对应,bijection):每一个x都有y与之对应,每一个y都有x与之对应。

根据上面的说明,一个简单的双射函数就像这样:

f(1) = 'D';
f(C) = 3;

另外,双射函数需要两个集合的长度相等,否则会失败。

初始化双向映射

我们可以在JavaScript 中创建一个类来初始化键值对:

const bimap = new BidirectionalMap({
 a: 'A',
 b: 'B',
 c: 'C',
})

在类里面,我们将会创建两个列表,一个用来处理正向映射,存放初始化对象的副本;另一个用来处理逆向映射,存放的内容是「键」「值」翻转后的初始化对象。

class BidirectionalMap {
 fwdMap = {}
 revMap = {}

 constructor(map) {
     this.fwdMap = { ...map }
     this.revMap = Object.keys(map).reduce(
        (acc, cur) => ({
             ...acc,
            [map[cur]]: cur,
        }),
        {}
    )
}
}

注意,由于初始对象本身的性质,你不能用数字当 key,但可以作为值来使用。

const bimap = new BidirectionalMap({
 a: 42,
 b: 'B',
 c: 'C',
})

如果不满足于此,也有更强大健壮的实现方式,按照 JavaScript 映射数据类型 中允许使用数字、函数甚至NaN来作为 key 的规范来实现,当然这会更加复杂。

通过双向映射获取元素

现在,我们有了一个包含两个对象的数据结构,它们互为键值对的镜像。我们现在需要一个方法来取出元素,让我们来实现一个 get() 函数:

 get( key ) {
   return this.fwdMap[key] || this.revMap[key]
}

这个方法非常简单: 如果正向映射里存在就返回,否则返回逆向映射,都没有就返回 undefined

试一下获取元素:

console.log(bimap.get('a')) // displays A
console.log(bimap.get('A')  // displays a

给双向映射添加元素

目前映射还无法添加元素,我们创建一个添加方法:

add(pair) {
   this.fwdMap[pair[0]] = pair[1]
   this.revMap[pair[1]] = pair[0]
}

add 函数接收一个双元素数组(在TypeScript 中叫做元组),按不同键值顺序加入到相应对象中。

现在我们可以添加和读取映射中的元素了:

bimap.add(['d', 'D'])
console.log( bimap.get('D') ) // displays d

在TypeScript中安全使用双向映射

为了确保数据类型安全,我们可以在 TypeScript 中进行改写,对输入类型进行检查,例如初始化的映射必须为一个通用对象,添加的元素必须为一个 元组

class BidirectionalMap {
 fwdMap = {}
 revMap = {}

 constructor(map: { [key: string]: string }) {
     this.fwdMap = { ...map }
     this.revMap = Object.keys(map).reduce(
        (acc, cur) => ({
             ...acc,
            [map[cur]]: cur,
        }),
        {}
    )
}

 get(key: string): string | undefined {
     return this.fwdMap[key] || this.revMap[key]
}

 add(pair: [string, string]) {
   this.fwdMap[pair[0]] = pair[1]
   this.revMap[pair[1]] = pair[0]
}
}

这样我们的映射就更加安全和完美了。在这里,我们的 key 和 value 都必须使用字符串。


翻译:sherryhe
来源:https://juejin.cn/post/6976797991277428750

收起阅读 »

Vue图片懒加载

1、问题在vue项目中,如果图片是从服务器端加载到页面上,图片较大的时候,就会存在一部分一部分加载的情况,会显示非常卡顿,影响体验。2、实现(1)、图片懒加载首先将图片的src链接设为一张我们已经准备好的图片(比如类似加载中的图片),并将其真正的图片地址存储在...
继续阅读 »

1、问题

在vue项目中,如果图片是从服务器端加载到页面上,图片较大的时候,就会存在一部分一部分加载的情况,会显示非常卡顿,影响体验。

2、实现

(1)、图片懒加载

首先将图片的src链接设为一张我们已经准备好的图片(比如类似加载中的图片),并将其真正的图片地址存储在img标签的自定义属性中。当js监听到该图片元素进入可视窗口时,即将自定义属性中的地址存储到src属性中,达到懒加载的效果。这样就可以缓解服务器压力,并且提高用户体验。

(2)、安装vue-lazyload
npm i vue-lazyload -S
(3)、在main.js中引入
import VueLazyload from "vue-lazyload";
Vue.use(VueLazyload,{
  preLoad: 1.3,
  loading: require('../src/assets/loading.gif'),
  attempt: 1
})

其中../src/assets/loading.gif是我本地的正在加载图片gif路径。

3、查看效果

在LazyLoad.vue中引入一张网络图片,在浏览器中限制网速,模拟图片加载缓慢的情况。

LazyLoad.vue

<template>
<div>
<img v-lazy=url1 alt="">
</div>
</template>
<script>
export default {
data (){
return{
url1: 'https://w.wallhaven.cc/full/pk/wallhaven-pkgkkp.png'
}
}
}
</script>

效果:

图片加载中:

图片加载完成:


常用参数:


作者:小绵杨Yancy
来源:https://blog.csdn.net/ZHANGYANG_1109/article/details/121868420

收起阅读 »

大白话讲解JavaScript 执行机制,一看就懂

JavaScript的运行机制所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。 为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是...
继续阅读 »



JavaScript的运行机制

1.JavaScript为什么是单线程?
JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

2、执行机制相关知识点

  • 同步任务

  • 异步任务

举个栗子
有一天,张三要去做饭 这时候他要做两件事 分别是蒸米饭 和 炒菜 ,现在有两种方式去完成这个任务

A. 先去蒸米饭 然后等蒸米饭好了 再去抄菜 ---同步任务
B. 先去蒸米饭 然后等蒸米饭的过程中 再去抄菜 ---异步任务

同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。

具体来说,异步执行的运行机制如下:(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。

总结: 同步任务在主线程执行,形成一个执行栈,执行栈中的所有同步任务执行完毕,就会去读取任务队列,就是对应的异步任务。

JavaScript的宏任务与微任务
除了广义上的定义,我们可以将任务进行更精细的定义,分为宏任务微任务

宏任务(macro-task):包括整体代码script脚本的执行,setTimeout,setInterval,ajax,dom操作,还有如 I/O 操作、UI 渲染等。

微任务(micro-task):Promise回调 node 中的 process.nextTick 、对 Dom 变化监听的 MutationObserver。

主线程都从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为 Event Loop(事件循环)

我们解释一下这张图:

1、同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
2、当指定的事情完成时,Event Table会将这个函数移入Event Queue。
3、主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
上述过程会不断重复,也就是常说的Event Loop(事件循环)。(Event Loop是javascript的执行机制)

优先级
setTimeout()、setInterval()
setTimeout() 和 setInterval() 产生的任务是 异步任务,也属于 宏任务。
setTimeout() 接受两个参数,第一个是回调函数,第二个是推迟执行的毫秒数。
如果将第二个参数设置为0或者不设置,意思 并不是立即执行,而是指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在"任务队列"的尾部添加一个事件,因此要等到同步任务和"任务队列"现有的事件都处理完,才会得到执行。(画重点)
所以说,setTimeout() 和 setInterval() 第二个参数设置的时间并不是绝对的,它需要根据当前代码最终执行的时间来确定的

Promise
Promise 相对来说就比较特殊了,在 new Promise() 中传入的回调函数是会 立即执行 的,但是它的 then() 方法是在 执行栈之后,任务队列之前 执行的,它属于 微任务。

process.nextTick
process.nextTick 是 Node.js 提供的一个与"任务队列"有关的方法,它产生的任务是放在 执行栈的尾部,并不属于 宏任务 和 微任务,因此它的任务 总是发生在所有异步任务之前。

setImmediate
setImmediate 是 Node.js 提供的另一个与"任务队列"有关的方法,它产生的任务追加到"任务队列"的尾部,它和 setTimeout(fn, 0) 很像,但优先级都是 setTimeout 优先于 setImmediate。
有时候,setTimeout 的执行顺序会在 setImmediate 的前面,有时候会在 setImmediate 的后面,这并不是 node.js 的 bug,这是因为虽然 setTimeout 第二个参数设置为0或者不设置,但是 setTimeout 源码中,会指定一个具体的毫秒数(node为1ms,浏览器为4ms),而由于当前代码执行时间受到执行环境的影响,执行时间有所起伏,如果当前执行的代码小于这个指定的值时,setTimeout 还没到推迟执行的时间,自然就先执行 setImmediate 了,如果当前执行的代码超过这个指定的值时,setTimeout 就会先于 setImmediate 执行。

通过上面的介绍,我们就可以得出一个代码执行的优先级:
同步代码(宏任务) > process.nextTick > Promise(微任务)> setTimeout(fn)、setInterval(fn)(宏任务)> setImmediate(宏任务)> setTimeout(fn, time)、setInterval(fn, time),其中time>0

面试回答
面试中该如何回答呢? 下面是我个人推荐的回答:
首先js 是单线程运行的,在代码执行的时候,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行。
在执行同步代码的时候,如果遇到了异步事件,js 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务
当同步事件执行完毕后,再将异步事件对应的回调加入到与当前执行栈中不同的另一个任务队列中等待执行。
任务队列可以分为宏任务对列和微任务对列,当当前执行栈中的事件执行完毕后,js 引擎首先会判断微任务对列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行。
当微任务对列中的任务都执行完成后再去判断宏任务对列中的任务。

面试遇到的问题总结
1、同步和异步的区别是什么?分别举一个同步和异步的例子
同步会阻塞代码执行,而异步不会。alert是同步,setTimeout是异步

2、为何需要异步呢?
如果第一个示例中间步骤是一个 ajax 请求,现在网络比较慢,请求需要5秒钟。如果是同步,这5秒钟页面就卡死在这里啥也干不了了。

最后,前端 JS 脚本用到异步的场景主要有两个:

  • 定时 setTimeout setInverval

  • 网络请求,如 ajax 加载

  • 事件绑定

3、写出下图执行顺序

执行顺序是2431
在 new Promise() 中传入的回调函数是 立即执行 的,但是它的 then() 方法是在 执行栈之后,任务队列之前 执行的,
.then是回调函数,链式回调,会被放在挂起,等待执行栈的内容执行完后(输出4)再回调(输出3),最后执行异步的1

作者:我写的代码绝对没有问题
来源:https://www.jianshu.com/p/22641c97e351

收起阅读 »

12个有用的JavaScript数组技巧

数组是Javascript最常见的概念之一,它为我们提供了处理数据的许多可能性,熟悉数组的一些常用操作是很有必要的。1、数组去重1、from()叠加new Set()方法字符串或数值型数组的去重可以直接使用from方法。var plants = ['Satur...
继续阅读 »

数组是Javascript最常见的概念之一,它为我们提供了处理数据的许多可能性,熟悉数组的一些常用操作是很有必要的。

1、数组去重

1、from()叠加new Set()方法

字符串或数值型数组的去重可以直接使用from方法。

var plants = ['Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
var uniquePlants = Array.from(new Set(plants));
console.log(uniquePlants); // [ 'Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Mars', 'Jupiter' ]

2、spread操作符(…)

扩展运算符是ES6的一大创新,还有很多强大的功能。

var plants = ['Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
var uniquePlants = [...new Set(plants)];
console.log(uniquePlants); // [ 'Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Mars', 'Jupiter' ]

2、替换数组中的特定值

splice() 方法向/从数组中添加/删除项目,然后返回被删除的项目。该方法会改变原始数组。特别需要注意插入值的位置!

// arrayObject.splice(index,howmany,item1,.....,itemX)

var plants = ['Saturn', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
var result = plants.splice(2, 1, 'www.shanzhonglei.com')
console.log(plants); // ['Saturn','Uranus','www.shanzhonglei.com','Mercury','Venus','Earth','Mars','Jupiter']
console.log(result); // ['Mercury']

3、没有map()的映射数组

我们先介绍一下map方法。map()方法返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值,它会按照原始数组元素顺序依次处理元素。注意: map()不会改变原始数组,也不会对空数组进行检测。
下面我们来实现一个没有map的数组映射:

// array.map(function(currentValue,index,arr), thisValue)

var plants = [
  { name: "Saturn" },
  { name: "Uranus" },
  { name: "Mercury" },
  { name: "Venus" },
]
var plantsName = Array.from(plants, ({ name }) => name);
console.log(plantsName); // [ 'Saturn', 'Uranus', 'Mercury', 'Venus' ]

4、空数组

如果要清空一个数组,将数组的长度设置为0即可,额,这个有点简单。

var plants = ['Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
plants.length = 0;
console.log(plants); // []

5、将数组转换为对象

如果要将数组转换为对象,最快的方法莫过于spread运算符(…)。

var plants = ['Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
var plantsObj = {...plants }
console.log(plantsObj); // {'0': 'Saturn','1': 'Earth', '2': 'Uranus','3': 'Mercury','4': 'Venus','5': 'Earth','6': 'Mars','7': 'Jupiter'}

6、用数据填充数组

如果我们需要用一些数据来填充数组,或者需要一个具有相同值的数据,我们可以用fill()方法。

var plants = new Array(8).fill('8');
console.log(plants); // ['8', '8', '8','8', '8', '8','8', '8']

7、合并数组

当然你会想到concat()方法,但是哦,spread操作符(…)也很香的,这也是扩展运算符的另一个应用。

var plants1 = ['Saturn', 'Earth', 'Uranus', 'Mercury'];
var plants2 = ['Venus', 'Earth', 'Mars', 'Jupiter'];
console.log([...plants1, ...plants2]); // ['Saturn', 'Earth','Uranus', 'Mercury','Venus', 'Earth','Mars', 'Jupiter']

8、两个数组的交集

要求两个数组的交集,首先确保数组不重复,然后使用filter()方法和includes()方法。

var plants1 = ['Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
var plants2 = ['Saturn', 'Earth', 'Uranus'];
var alonePlants = [...new Set(plants1)].filter(item => plants2.includes(item));
console.log(alonePlants); // [ 'Saturn', 'Earth', 'Uranus' ]

9、删除数组中的假值

我们时常需要在处理数据的时候要去掉假值。在Javascript中,假值是false, 0, " ", null, NaN, undefined。

var plants = ['Saturn', 'Earth', null, undefined, false, "", NaN, 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
var trueArr = plants.filter(Boolean);
console.log(trueArr); // ['Saturn', 'Earth','Uranus', 'Mercury','Venus', 'Earth','Mars', 'Jupiter']

10、获取数组中的随机值

我们可以根据数组长度获得一个随机索引号。

var plants = ['Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
console.log(plants[Math.floor(Math.random() * (plants.length + 1))])

11、lastIndexOf()方法

lastIndexOf()可以帮助我们查找元素最后一次出现的索引。

var plants = ['Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
console.log(plants.lastIndexOf('Earth')) // 5

12、将数组中的所有值相加

reduce()方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。

// array.reduce(function(total, currentValue, currentIndex, arr), initialValue)

var nums = [1, 2, 3, 4, 5];
var sum = nums.reduce((x, y) => x + y);
console.log(sum); // 15

作者:前端技术驿站
来源:https://www.jianshu.com/p/651338c88bb4

收起阅读 »

字节面试被虐后,是时候搞懂 DNS 了

前几天面了字节 👦🏻:“浏览器从输入URL到显示页面发生了什么?” 👧🏻:%^&@#^&(这我怎么可能没有准备?从网络到渲染说了一通后) 👦🏻:“你刚刚提到了 DNS,那说说 DNS 的查询过程吧” 👧🏻:“DNS 查询是一个递归 + 迭代的...
继续阅读 »

前几天面了字节



👦🏻:“浏览器从输入URL到显示页面发生了什么?”


👧🏻:%^&@#^&(这我怎么可能没有准备?从网络到渲染说了一通后)


👦🏻:“你刚刚提到了 DNS,那说说 DNS 的查询过程吧”


👧🏻:“DNS 查询是一个递归 + 迭代的过程...”


👦🏻:“那具体的递归和迭代过程是怎样的呢?”


👧🏻:“...”



当时我脑子里有个大概的过程,但是细节就记不起来了,所以今天就来梳理一下 DNS 相关的内容,如有不妥之处,还望大家指出。


什么是 DNS


DNS 即域名系统,全称是 Domain Name System。当我们在浏览器输入一个 URL 地址时,浏览器要向这个 URL 的主机名对应的服务器发送请求,就得知道服务器的 IP,对于浏览器来说,DNS 的作用就是将主机名转换成 IP 地址。下面是摘自《计算机网络:自顶向下方法》的概念:



DNS 是:



  1. 一个由分层的 DNS 服务器实现的分布式数据库

  2. 一个使得主机能够查询分布式数据库的应用层协议



也就是,DNS 是一个应用层协议,我们发送一个请求,其中包含我们要查询的主机名,它就会给我们返回这个主机名对应的 IP;


其次,DNS 是一个分布式数据库,整个 DNS 系统由分散在世界各地的很多台 DNS 服务器组成,每台 DNS 服务器上都保存了一些数据,这些数据可以让我们最终查到主机名对应的 IP。


所以 DNS 的查询过程,说白了,就是去向这些 DNS 服务器询问,你知道这个主机名的 IP 是多少吗,不知道?那你知道去哪台 DNS 服务器上可以查到吗?直到查到我想要的 IP 为止。


分布式、层次数据库


什么是分布式?

这个世界上没有一台 DNS 服务器拥有因特网上所有主机的映射,每台 DNS 只负责部分映射。


什么是层次?

DNS 服务器有 3 种类型:根 DNS 服务器、顶级域(Top-Level Domain, TLD)DNS 服务器和权威 DNS 服务器。它们的层次结构如下图所示:



DNS 的层次结构.jpeg



图片来源:《计算机网络:自顶向下方法》



  • 根 DNS 服务器


首先我们要明确根域名是什么,比如 http://www.baidu.com,有些同学可能会误以为 com 就是根域名,其实 com 是顶级域名,http://www.baidu.com 的完整写法是 http://www.baidu.com.,最后的这个 . 就是根域名。


根 DNS 服务器的作用是什么呢?就是管理它的下一级,也就是顶级域 DNS 服务器。通过询问根 DNS 服务器,我们可以知道一个主机名对应的顶级域 DNS 服务器的 IP 是多少,从而继续向顶级域 DNS 服务器发起查询请求。



  • 顶级域 DNS 服务器


除了前面提到的 com 是顶级域名,常见的顶级域名还有 cnorgedu 等。顶级域 DNS 服务器,也就是 TLD,提供了它的下一级,也就是权威 DNS 服务器的 IP 地址。



  • 权威 DNS 服务器


权威 DNS 服务器可以返回主机 - IP 的最终映射。


关于这几个层次的服务器之间是怎么交互的,接下来我们会讲到 DNS 具体的查询过程,结合查询过程,大家就不难理解它们之间的关系了。


本地 DNS 服务器


之前对 DNS 有过了解的同学可能会发现,上一节的 DNS 层次结构,为什么没有提到本地 DNS 服务器?因为严格来说,本地 DNS 服务器并不属于 DNS 的层次结构,但它对 DNS 层次结构是至关重要的。那什么是本地 DNS 服务器呢?


每个 ISP 都有一台本地 DNS 服务器,比如一个居民区的 ISP、一个大学的 ISP、一个机构的 ISP,都有一台或多台本地 DNS 服务器。当主机发出 DNS 请求时,该请求被发往本地 DNS 服务器,本地 DNS 服务器起着代理的作用,并负责将该请求转发到 DNS 服务器层次结构中。


接下来就让我们通过一个简单的例子,看看 DNS 的查询过程是怎样的,看看客户端、本地 DNS 服务器、DNS 服务器层次结构之间是如何交互的。


递归查询、迭代查询


如下图,假设主机 m.n.com 想要获取主机 a.b.com 的 IP 地址,会经过以下几个步骤:



DNS.png





  1. 首先,主机 m.n.com 向它的本地 DNS 服务器发送一个 DNS 查询报文,其中包含期待被转换的主机名 a.b.com




  2. 本地 DNS 服务器将该报文转发到根 DNS 服务器;




  3. 该根 DNS 服务器注意到 com 前缀,便向本地 DNS 服务器返回 com 对应的顶级域 DNS 服务器(TLD)的 IP 地址列表。


    意思就是,我不知道 a.b.com 的 IP,不过这些 TLD 服务器可能知道,你去问他们吧;




  4. 本地 DNS 服务器则向其中一台 TLD 服务器发送查询报文;




  5. 该 TLD 服务器注意到 b.com 前缀,便向本地 DNS 服务器返回权威 DNS 服务器的 IP 地址。


    意思就是,我不知道 a.b.com 的 IP,不过这些权威服务器可能知道,你去问他们吧;




  6. 本地 DNS 服务器又向其中一台权威服务器发送查询报文;




  7. 终于,该权威服务器返回了 a.b.com 的 IP 地址;




  8. 本地 DNS 服务器将 a.b.com 跟 IP 地址的映射返回给主机 m.n.comm.n.com 就可以用该 IP 向 a.b.com 发送请求啦。





bqb4.jpeg



“你说了这么多,递归呢?迭代呢?”


这位同学不要捉急,其实递归和迭代已经包含在上述过程里了。


主机 m.n.com 向本地 DNS 服务器 dns.n.com 发出的查询就是递归查询,这个查询是主机 m.n.com 以自己的名义向本地 DNS 服务器请求想要的 IP 映射,并且本地 DNS 服务器直接返回映射结果给到主机。


而后继的三个查询是迭代查询,包括本地 DNS 服务器向根 DNS 服务器发送查询请求、本地 DNS 服务器向 TLD 服务器发送查询请求、本地 DNS 服务器向权威 DNS 服务器发送查询请求,所有的请求都是由本地 DNS 服务器发出,所有的响应都是直接返回给本地 DNS 服务器


那么问题来了,所有的 DNS 查询都必须遵循这种递归 + 迭代的模式吗?


当然不是。


从理论上讲,任何 DNS 查询既可以是递归的,也可以是迭代的。下图的所有查询就都是递归的,不包含迭代。



DNS2.png



看到这里,大家可能会有个疑问,TLD 一定知道权威 DNS 服务器的 IP 地址吗?


emmm...



bqb7.png



还真不一定,有时 TLD 只是知道中间的某个 DNS 服务器,再由这个中间 DNS 服务器去找到权威 DNS 服务器。这种时候,整个查询过程就需要更多的 DNS 报文。


DNS 缓存


为了让我们更快的拿到想要的 IP,DNS 广泛使用了缓存技术。DNS 缓存的原理非常简单,在一个 DNS 查询的过程中,当某一台 DNS 服务器接收到一个 DNS 应答(例如,包含某主机名到 IP 地址的映射)时,它就能够将映射缓存到本地,下次查询就可以直接用缓存里的内容。当然,缓存并不是永久的,每一条映射记录都有一个对应的生存时间,一旦过了生存时间,这条记录就应该从缓存移出。


事实上,有了缓存,大多数 DNS 查询都绕过了根 DNS 服务器,需要向根 DNS 服务器发起查询的请求很少。


面试感想


这次面试收获还蛮大的,有些东西以为自己懂了,以为自己能说清楚,但到了真的要说的时候,又没有办法完整地梳理出来,描述起来磕磕绊绊,在面试中会很减分。


所以不要偷懒,不要抱有侥幸心理,踏实学。共勉。



作者:我是陆小北
链接:https://juejin.cn/post/6990344840181940261

收起阅读 »

H5页面中调用微信和支付宝支付

最近在工作中,有个H5页面需要实现微信支付和支付宝支付的功能,现在已经完成,抽个时间写出来,分享给有需要的人。 第一步:先判断当前环境 判断用户所属环境,根据环境不同,执行不同的支付程序。 if (/MicroMessenger/.test(window.na...
继续阅读 »

最近在工作中,有个H5页面需要实现微信支付和支付宝支付的功能,现在已经完成,抽个时间写出来,分享给有需要的人。


第一步:先判断当前环境


判断用户所属环境,根据环境不同,执行不同的支付程序。


if (/MicroMessenger/.test(window.navigator.userAgent)) {
// alert('微信');
} else if (/AlipayClient/.test(window.navigator.userAgent)) {
//alert('支付宝');
} else {
//alert('其他浏览器');
}

第二步:如果是微信环境,需要先进行网页授权


网页授权的详细介绍可以查看微信相关文档。这里不做介绍。


第三步:


1、微信支付


微信支付有两种方法:
1:调用微信浏览器提供的内置接口WeixinJSBridge
2:引入微信jssdk,使用wx.chooseWXPay方法,需要先通过config接口注入权限验证配置。
我这里使用的是第一种,在从后台拿到签名、时间戳这些数据后,直接调用微信浏览器提供的内置接口WeixinJSBridge即可完成支付功能。


getRequestPayment(data) {
function onBridgeReady() {
WeixinJSBridge.invoke(
"getBrandWCPayRequest", {
"appId": data.appId, //公众号ID,由商户传入
"timeStamp": data.timeStamp, //时间戳,自1970年以来的秒数
"nonceStr": data.nonceStr, //随机串
"package": data.package,
"signType": data.signType, //微信签名方式:
"paySign": data.paySign //微信签名
},
function(res) {
alert(JSON.stringify(res));
// get_brand_wcpay_request
if (res.err_msg == "get_brand_wcpay_request:ok") {
// 使用以上方式判断前端返回,微信团队郑重提示:
//res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。
}
}
);
}
if (typeof WeixinJSBridge == "undefined") {
if (document.addEventListener) {
document.addEventListener(
"WeixinJSBridgeReady",
onBridgeReady,
false
);
} else if (document.attachEvent) {
document.attachEvent("WeixinJSBridgeReady", onBridgeReady);
document.attachEvent("onWeixinJSBridgeReady", onBridgeReady);
}
} else {
onBridgeReady();
}
},

2、支付宝支付


支付宝支付相对于微信来说,前端这块工作更简单 ,后台会返回给前端一个form表单,我们要做的就是把这个表单进行提交即可。相关代码如下:


this.$api.alipayPay(data).then((res) => {
// console.log('支付宝参数', res.data)
if (res.code == 200) {
var resData =res.data
const div = document.createElement('div')
div.id = 'alipay'
div.innerHTML = resData
document.body.appendChild(div)
document.querySelector('#alipay').children[0].submit() // 执行后会唤起支付宝
}

}).catch((err) => {
})

作者:故友
链接:https://juejin.cn/post/7034033584684204068

收起阅读 »

现代配置指南——YAML 比 JSON 高级在哪?

一直以来,前端工程中的配置大多都是 .js 文件或者 .json 文件,最常见的比如:package.jsonbabel.config.jswebpack.config.js这些配置对前端非常友好,因为都是我们熟悉的 JS 对象结构。一般静态化的配置会选择 j...
继续阅读 »

一直以来,前端工程中的配置大多都是 .js 文件或者 .json 文件,最常见的比如:

  • package.json

  • babel.config.js

  • webpack.config.js

这些配置对前端非常友好,因为都是我们熟悉的 JS 对象结构。一般静态化的配置会选择 json 文件,而动态化的配置,涉及到引入其他模块,因此会选择 js 文件。

还有现在许多新工具同时支持多种配置,比如 Eslint,两种格式的配置任你选择:

  • .eslintrc.json

  • .eslintrc.js

后来不知道什么时候,突然出现了一种以 .yaml.yml 为后缀的配置文件。一开始以为是某个程序的专有配置,后来发现这个后缀的文件出现的频率越来越高,甚至 Eslint 也支持了第三种格式的配置 .eslintrc.yml

既然遇到了,那就探索它!

下面我们从 YAML 的出现背景使用场景具体用法高级操作四个方面,看一下这个流行的现代化配置的神秘之处。

出现背景

一个新工具的出现避免不了有两个原因:

  1. 旧工具在某些场景表现吃力,需要更优的替代方案

  2. 旧工具也没什么不好,只是新工具出现,比较而言显得它不太好

YAML 这种新工具就属于后者。其实在 yaml 出现之前 js+json 用的也不错,也没什么特别难以处理的问题;但是 yaml 出现以后,开始觉得它好乱呀什么东西,后来了解它后,越用越喜欢,一个字就是优雅。

很多文章说选择 yaml 是因为 json 的各种问题,json 不适合做配置文件,这我觉得有些言过其实了。我更愿意将 yaml 看做是 json 的升级,因为 yaml 在格式简化和体验上表现确实不错,这个得承认。

下面我们对比 YAML 和 JSON,从两方面分析:

精简了什么?

JSON 比较繁琐的地方是它严格的格式要求。比如这个对象:

{
 name: 'ruims'
}

在 JSON 中以下写法通通都是错的:

// key 没引号不行
{
 name: 'ruims'
}
// key 不是 "" 号不行
{
 'name': 'ruims'
}
// value 不是 "" 号不行
{
 "name": 'ruims'
}

字符串的值必须 k->v 都是 "" 才行:

// 只能这样
{
 "name": "ruims"
}

虽然是统一格式,但是使用上确实有不便利的地方。比如我在浏览器上测出了接口错误。然后把参数拷贝到 Postman 里调试,这时就我要手动给每个属性和值加 "" 号,非常繁琐。

YAML 则是另辟蹊径,直接把字符串符号干掉了。上面对象的同等 yaml 配置如下:

name: ruims

没错,就这么简单!

除了 "" 号,yaml 觉得 {}[] 这种符号也是多余的,不如一起干掉。

于是呢,以这个对象数组为例:

{
 "names": [{ "name": "ruims" }, { "name": "ruidoc" }]
}

转换成 yaml 是这样的:

names:
- name: ruims
- name: ruidoc

对比一下这个精简程度,有什么理由不爱它?

增加了什么?

说起增加的部分,最值得一提的,是 YAML 支持了 注释

用 JSON 写配置是不能有注释的,这就意味着我们的配置不会有备注,配置多了会非常凌乱,这是最不人性化的地方。

现在 yaml 支持了备注,以后配置可以是这样的:

# 应用名称
name: my_app
# 应用端口
port: 8080

把这种配置丢给新同事,还怕他看不懂配了啥吗?

除注释外,还支持配置复用的相关功能,这个后面说。

使用场景

我接触的第一个 yaml 配置是 Flutter 项目的包管理文件 pubspec.yaml,这个文件的作用和前端项目中的 package.json 一样,用于存放一些全局配置和应用依赖的包和版本。

看一下它的基本结构:

name: flutter_demo
description: A new Flutter project.

publish_to: 'none'
version: 1.0.0

dependencies:
cupertino_icons: ^1.0.2

dev_dependencies:
flutter_lints: ^1.0.0

你看这个结构和 package.json 是不是基本一致?dependencies 下列出应用依赖和版本,dev_dependencies 下的则是开发依赖。

后来在做 CI/CD 自动化部署的时候,我们用到了 GitHub Action。它需要多个 yaml 文件来定义不同的工作流,这个配置可比 flutter 复杂的多。

其实不光 GitHub Action,其他流行的类似的构建工具如 GitLab CI/CDcircleci,全部都是齐刷刷的 yaml 配置,因此如果你的项目要做 CI/CD 持续集成,不懂 yaml 语法肯定是不行的。

还有,接触过 Docker 的同学肯定知道 Docker Compose,它是 Docker 官方的单机编排工具,其配置文件 docker-compose.yml 也是妥妥的 yaml 格式。现在 Docker 正是如日中天的时候,使用 Docker 必然免不了编排,因此 yaml 语法早晚也要攻克。

上面说的这 3 个案例,几乎都是现代最新最流行的框架/工具。从它们身上可以看出来,yaml 必然是下一代配置文件的标准,并且是前端-后端-运维的通用标准。

说了这么多,你跃跃欲试了吗?下面我们详细介绍 yaml 语法。

YAML 语法

介绍 yaml 语法会对比 json 解释,以便我们快速理解。

先看一下 yaml 的几个特点:

  • 大小写敏感

  • 使用缩进表示层级关系

  • 缩进空格数不强制,但相同层级要对齐

  • # 表示注释

相比于 JSON 来说,最大的区别是用 缩进 来表示层级,这个和 Python 非常接近。还有强化的一点是支持了注释,JSON 默认是不支持的(虽然 TS 支持),这也对配置文件非常重要。

YAML 支持以下几种数据结构:

  • 对象:json 中的对象

  • 数组:json 中的数组

  • 纯量:json 中的简单类型(字符串,数值,布尔等)

对象

先看对象,上一个 json 例子:

{
 "id": 1,
 "name": "杨成功",
 "isman": true
}

转换成 yaml:

id: 1
name: 杨成功
isman: true

对象是最核心的结构,key 值的表示方法是 [key]:,注意这里冒号后面有个空格,一定不能少。value 的值就是一个纯量,且默认不需要引号。

数组

数组和对象的结构差不多,区别是在 key 前用一个 - 符号标识这个是数组项。注意这里也有一个空格,同样也不能少。

- hello
- world

转换成 JSON 格式如下:

["hello", "world"]

了解了基本的对象和数组,我们再来看一个复杂的结构。

众所周知,在实际项目配置中很少有简单的对象或数组,大多都是对象和数组相互嵌套而成。在 js 中我们称之为对象数组,而在 yaml 中我们叫 复合结构

比如这样一个稍复杂的 JSON:

{
 "name": "杨成功",
 "isman": true,
 "age": 25,
 "tag": ["阳光", "帅气"],
 "address": [
  { "c": "北京", "a": "海淀区" },
  { "c": "天津", "a": "滨海新区" }
]
}

转换成复合结构的 YAML:

name: 杨成功
isman: true
age: 25
tag:
- 阳光
- 帅气
address:
- c: 北京
  a: 海淀区
- c: 天津
  a: 滨海新区

若你想尝试更复杂结构的转换,可以在 这个 网页中在线实践。

纯量

纯量比较简单,对应的就是 js 的基本数据类型,支持如下:

  • 字符串

  • 布尔

  • 数值

  • null

  • 时间

比较特殊的两个,null 用 ~ 符号表示,时间大多用 2021-12-21 这种格式表示,如:

who: ~
date: 2019-09-10

转换成 JS 后:

{
 who: null,
 date: new Date('2019-09-10')
}

高级操作

在 yaml 实战过程中,遇到过一些特殊场景,可能需要一些特殊的处理。

字符串过长

在 shell 中我们常见到一些参数很多,然后特别长的命令,如果命令都写在一行的话可读性会非常差。

假设下面的是一条长命令:

$ docker run --name my-nginx -d nginx

在 linux 中可以这样处理:

$ docker run \
--name my-nginx \
-d nginx

就是在每行后加 \ 符号标识换行。然而在 YAML 中更简单,不需要加任何符号,直接换行即可:

cmd: docker run
--name my-nginx
-d nginx

YAML 默认会把换行符转换成空格,因此转换后 JSON 如下,正是我们需要的:

{ "cmd": "docker run --name my-nginx -d nginx" }

然而有时候,我们的需求是保留换行符,并不是把它转换成空格,又该怎么办呢?

这个也简单,只需要在首行加一个 | 符号:

cmd: |
docker run
--name my-nginx
-d nginx

转换成 JSON 变成了这样:

{ "cmd": "docker run\n--name my-nginx\n-d nginx" }

获取配置

获取配置是指,在 YAML 文件中定义的某个配置,如何在代码(JS)里获取?

比如前端在 package.json 里有一个 version 的配置项表示应用版本,我们要在代码中获取版本,可以这么写:

import pack from './package.json'
console.log(pack.version)

JSON 是可以直接导入的,YAML 可就不行了,那怎么办呢?我们分环境解析:

在浏览器中

浏览器中代码用 webapck 打包,因此加一个 loader 即可:

$ yarn add -D yaml-loader

然后配置 loader:

// webpack.config.js
module.exports = {
 module: {
   rules: [
    {
       test: /\.ya?ml$/,
       type: 'json', // Required by Webpack v4
       use: 'yaml-loader'
    }
  ]
}
}

在组件中使用:

import pack from './package.yaml'
console.log(pack.version)

在 Node.js 中

Node.js 环境下没有 Webpack,因此读取 yaml 配置的方法也不一样。

首先安装一个 js-yaml 模块:

$ yarn add js-yaml

然后通过模块提供的方法获取:

const yaml = require('js-yaml')
const fs = require('fs')

const doc = yaml.load(fs.readFileSync('./package.yaml', 'utf8'))
console.log(doc.version)

配置项复用

配置项复用的意思是,对于定义过的配置,在后面的配置直接引用,而不是再写一遍,从而达到复用的目的。

YAML 中将定义的复用项称为锚点,用& 标识;引用锚点则用 * 标识。

name: &name my_config
env: &env
version: 1.0

compose:
key1: *name
key2: *env

对应的 JSON 如下:

{
 "name": "my_config",
 "env": { "version": 1 },
 "compose": { "key1": "my_config", "key2": { "version": 1 } }
}

但是锚点有个弊端,就是不能作为 变量 在字符串中使用。比如:

name: &name my_config
compose:
key1: *name
key2: my name is *name

此时 key2 的值就是普通字符串 my name is *name,引用变得无效了。

其实在实际开发中,字符串中使用变量还是很常见的。比如在复杂的命令中多次使用某个路径,这个时候这个路径就应该是一个变量,在多个命令中复用。

GitHub Action 中有这样的支持,定义一个环境变量,然后在其他的地方复用:

env:
NAME: test
describe: This app is called ${NAME}

这种实现方式与 webpack 中使用环境变量类似,在构建的时候将变量替换成对应的字符串。

作者:杨成功
来源:https://segmentfault.com/a/1190000041108051

收起阅读 »

LOOK 直播活动地图生成器方案

在最近的活动开发中,笔者就刚好碰到了这个问题。这次活动开发需要完成一款大富翁游戏,而作为一款大富翁游戏,地图自然是必不可少的。在整个地图中,有很多的不同种类的方格,如果一个个手动去调整位置,工作量是很大的。那么有没有一种方案能够帮助我们快速确定方格的位置和种类...
继续阅读 »

对于前端而言,与视觉稿打交道是必不可少的,因为我们需要对照着视觉稿来确定元素的位置、大小等信息。如果是比较简单的页面,手动调整每个元素所带来的工作量尚且可以接受;然而当视觉稿中素材数量较大时,手动调整每个元素便不再是个可以接受的策略了。

在最近的活动开发中,笔者就刚好碰到了这个问题。这次活动开发需要完成一款大富翁游戏,而作为一款大富翁游戏,地图自然是必不可少的。在整个地图中,有很多的不同种类的方格,如果一个个手动去调整位置,工作量是很大的。那么有没有一种方案能够帮助我们快速确定方格的位置和种类呢?下面便是笔者所采用的方法。

方案简述

位点图

首先,我们需要视觉同学提供一张特殊的图片,称之为位点图。

这张图片要满足以下几个要求:

  1. 在每个方格左上角的位置,放置一个 1px 的像素点,不同类型的方格用不同颜色表示。

  2. 底色为纯色:便于区分背景和方格。

  3. 大小和地图背景图大小一致:便于从图中读出的坐标可以直接使用。

bitmap

上图为一个示例,在每个路径方格左上角的位置都有一个 1px 的像素点。为了看起来明显一点,这里用红色的圆点来表示。在实际情况中,不同的点由于方格种类不同,颜色也是不同的。

bitmap2

上图中用黑色边框标出了素材图的轮廓。可以看到,红色圆点和每个路径方格是一一对应的关系。

读取位点图

在上面的位点图中,所有方格的位置和种类信息都被标注了出来。我们接下来要做的,便是将这些信息读取出来,并生成一份 json 文件来供我们后续使用。

const JImp = require('jimp');
const nodepath = require('path');

function parseImg(filename) {
   JImp.read(filename, (err, image) => {
       const { width, height } = image.bitmap;

       const result = [];

       // 图片左上角像素点的颜色, 也就是背景图的颜色
       const mask = image.getPixelColor(0, 0);

       // 筛选出非 mask 位置点
       for (let y = 0; y < height; ++y) {
           for (let x = 0; x < width; ++x) {
               const color = image.getPixelColor(x, y);
               if (mask !== color) {
                   result.push({
                       // x y 坐标
                       x,
                       y,
                       // 方格种类
                       type: color.toString(16).slice(0, -2),
                  });
              }
          }
      }

       // 输出
       console.log(JSON.stringify({
           // 路径
           path: result,
      }));
  });
}

parseImg('bitmap.png');

在这里我们使用了 jimp 用于图像处理,通过它我们能够去扫描这张图片中每个像素点的颜色和位置。

至此我们得到了包含所有方格位置和种类信息的 json 文件:

{
   "path": [
      {
           "type": "",
           "x": 0,
           "y": 0,
      },
       // ...
  ],
}

其中,x y 为方格左上角的坐标;type 为方格种类,值为颜色值,代表不同种类的地图方格。

通路连通算法

对于我们的项目而言,只确定路径点是不够的,还需要将这些点连接成一个完整的通路。为此,我们需要找到一条由这些点构成的最短连接路径。

代码如下:

function takePath(point, points) {
   const candidate = (() => {
       // 按照距离从小到大排序
       const pp = [...points].filter((i) => i !== point);
       const [one, two] = pp.sort((a, b) => measureLen(point, a) - measureLen(point, b));

       if (!one) {
           return [];
      }

       // 如果两个距离 比较小,则穷举两个路线,选择最短连通图路径。
       if (two && measureLen(one, two) < 20000) {
           return [one, two];
      }
       return [one];
  })();

   let min = Infinity;
   let minPath = [];
   for (let i = 0; i < candidate.length; ++i) {
       // 递归找出最小路径
       const subpath = takePath(candidate[i], removeItem(points, candidate[i]));

       const path = [].concat(point, subpath);
       // 测量路径总长度
       const distance = measurePathDistance(path);

       if (distance < min) {
           min = distance;
           minPath = subpath;
      }
  }

   return [].concat(point, minPath);
}

到这里,我们已经完成了所有的准备工作,可以开始绘制地图了。在绘制地图时,我们只需要先读取 json 文件,再根据 json 文件内的坐标信息和种类信息来放置对应素材即可。

方案优化

上述方案能够解决我们的问题,但仍有一些不太方便的地方:

  1. 只有 1px 的像素点太小了,肉眼无法辨别。不管是视觉同学还是开发同学,如果点错了位置就很难排查。

  2. 位点图中包含的信息还是太少了,颜色仅仅对应种类,我们希望能够包含更多的信息,比如点之间的排列顺序、方格的大小等。

像素点合并

对于第一个问题,我们可以让视觉同学在画图的时候,将 1px 的像素点扩大成一个肉眼足够辨识的区域。需要注意两个区域之间不要有重叠。

bitmap3

这时候就要求我们对代码做一些调整。在之前的代码中,当我们扫描到某个颜色与背景色不同的点时,会直接记录其坐标和颜色信息;现在当我们扫描到某个颜色与背景色不同的点时,还需要进行一次区域合并,将所有相邻且相同颜色的点都纳入进来。

区域合并的思路借鉴了下图像处理的区域生长算法。区域生长算法的思路是以一个像素点为起点,将该点周围符合条件的点纳入进来,之后再以新纳入的点为起点,向新起点相邻的点扩张,直到所有符合条件条件的点都被纳入进来。这样就完成了一次区域合并。不断重复该过程,直到整个图像中所有的点都被扫描完毕。

我们的思路和区域生长算法非常类似:

  1. 依次扫描图像中的像素点,当扫描到颜色与背景色不同的点时,记录下该点的坐标和颜色。

    步骤1.png

  2. 之后扫描与该点相邻的 8 个点,将这些点打上”已扫描“的标记。筛选出其中颜色与背景色不同且尚未被扫描过的点,放入待扫描的队列中。

    步骤2.png

  3. 从待扫描队列中取出下一个需要扫描的点,重复步骤 1 和步骤 2。

  4. 直到待扫描的队列为空时,我们就扫描完了一整个有颜色的区域。区域合并完毕。

    步骤3.png

const JImp = require('jimp');

let image = null;
let maskColor = null;

// 判断两个颜色是否为相同颜色 -> 为了处理图像颜色有误差的情况, 不采用相等来判断
const isDifferentColor = (color1, color2) => Math.abs(color1 - color2) > 0xf000ff;

// 判断是(x,y)是否超出边界
const isWithinImage = ({ x, y }) => x >= 0 && x < image.width && y >= 0 && y < image.height;

// 选择数量最多的颜色
const selectMostColor = (dotColors) => { /* ... */ };

// 选取左上角的坐标
const selectTopLeftDot = (reginDots) => { /* ... */ };

// 区域合并
const reginMerge = ({ x, y }) => {
   const color = image.getPixelColor(x, y);
   // 扫描过的点
   const reginDots = [{ x, y, color }];
   // 所有扫描过的点的颜色 -> 扫描完成后, 选择最多的色值作为这一区域的颜色
   const dotColors = {};
   dotColors[color] = 1;

   for (let i = 0; i < reginDots.length; i++) {
       const { x, y, color } = reginDots[i];

       // 朝临近的八个个方向生长
       const seeds = (() => {
           const candinates = [/* 左、右、上、下、左上、左下、右上、右下 */];

           return candinates
               // 去除超出边界的点
              .filter(isWithinImage)
               // 获取每个点的颜色
              .map(({ x, y }) => ({ x, y, color: image.getPixelColor(x, y) }))
               // 去除和背景色颜色相近的点
              .filter((item) => isDifferentColor(item.color, maskColor));
      })();

       for (const seed of seeds) {
           const { x: seedX, y: seedY, color: seedColor } = seed;

           // 将这些点添加到 reginDots, 作为下次扫描的边界
           reginDots.push(seed);

           // 将该点设置为背景色, 避免重复扫描
           image.setPixelColor(maskColor, seedX, seedY);

           // 该点颜色为没有扫描到的新颜色, 将颜色增加到 dotColors 中
           if (dotColors[seedColor]) {
               dotColors[seedColor] += 1;
          } else {
               // 颜色为旧颜色, 增加颜色的 count 值
               dotColors[seedColor] = 1;
          }
      }
  }

   // 扫描完成后, 选择数量最多的色值作为区域的颜色
   const targetColor = selectMostColor(dotColors);

   // 选择最左上角的坐标作为当前区域的坐标
   const topLeftDot = selectTopLeftDot(reginDots);

   return {
       ...topLeftDot,
       color: targetColor,
  };
};

const parseBitmap = (filename) => {
   JImp.read(filename, (err, img) => {
       const result = [];
       const { width, height } = image.bitmap;
       // 背景颜色
       maskColor = image.getPixelColor(0, 0);
       image = img;

       for (let y = 0; y < height; ++y) {
           for (let x = 0; x < width; ++x) {
               const color = image.getPixelColor(x, y);

               // 颜色不相近
               if (isDifferentColor(color, maskColor)) {
                   // 开启种子生长程序, 依次扫描所有临近的色块
                   result.push(reginMerge({ x, y }));
              }
          }
      }
  });
};

颜色包含额外信息

在之前的方案中,我们都是使用颜色值来表示种类,但实际上颜色值所能包含的信息还有很多。

一个颜色值可以用 rgba 来表示,因此我们可以让 r、g、b、a 分别代表不同的信息,如 r 代表种类、g 代表宽度、b 代表高度、a 代表顺序。虽然 rgba 每个的数量都有限(r、g、b 的范围为 0-255,a 的范围为 0-99),但基本足够我们使用了。

rgba.png

当然,你甚至可以再进一步,让每个数字都表示一种信息,不过这样每种信息的范围就比较小,只有 0-9。

总结

对于素材量较少的场景,前端可以直接从视觉稿中确认素材信息;当素材量很多时,直接从视觉稿中确认素材信息的工作量就变得非常大,因此我们使用了位点图来辅助我们获取素材信息。

无标题-2021-09-28-1450.png

地图就是这样一种典型的场景,在上面的例子中,我们已经通过从位点图中读出的信息成功绘制了地图。我们的步骤如下:

  1. 视觉同学提供位点图,作为承载信息的载体,它需要满足以下三个要求:

    1. 大小和地图背景图大小一致:便于我们从图中读出的坐标可以直接使用。

    2. 底色为纯色:便于区分背景和方格。

    3. 在每个方格左上角的位置,放置一个方格,不同颜色的方格表示不同类型。

  2. 通过 jimp 扫描图片上每个像素点的颜色,从而生成一份包含各个方格位置和种类的 json。

  3. 绘制地图时,先读取 json 文件,再根据 json 文件内的坐标信息和种类信息来放置素材。

上述方案并非完美无缺的,在这里我们主要对于位点图进行了改进,改进方案分为两方面:

  1. 由于 1px 的像素点对肉眼来说过小,视觉同学画图以及我们调试的时候,都十分不方便。因此我们将像素点扩大为一个区域,在扫描时,对相邻的相同颜色的像素点进行合并。

  2. 让颜色的 rgba 分别对应一种信息,扩充位点图中的颜色值能够给我们提供的信息。

我们在这里只着重讲解了获取地图信息的部分,至于如何绘制地图则不在本篇的叙述范围之内。在我的项目中使用了 pixi.js 作为引擎来渲染,完整项目可以参考这里,在此不做赘述。

FAQ

  • 在位点图上,直接使用颜色块的大小作为路径方格的宽高可以不?

    当然可以。但这种情况是有局限性的,当我们的素材很多且彼此重叠的时候,如果依然用方块大小作为宽高,那么在位点图上的方块就会彼此重叠,影响我们读取位置信息。

  • 如何处理有损图的情况?

    有损图中,图形边缘处的颜色和中心的颜色会略微有所差异。因此需要增加一个判断函数,只有扫描到的点的颜色与背景色的差值大于某个数字后,才认为是不同颜色的点,并开始区域合并。同时要注意在位点图中方块的颜色尽量选取与背景色色值相差较大的颜色。

    这个判断函数,就是我们上面代码中的 isDifferentColor 函数。

    const isDifferentColor = (color1, color2) => Math.abs(color1 - color2) > 0xf000ff;
  • 判断两个颜色不相等的 0xf000ff 是怎么来的?

    随便定的。这个和图片里包含颜色有关系,如果你的背景色和图片上点的颜色非常相近的话,这个值就需要小一点;如果背景色和图上点的颜色相差比较大,这个值就可以大一点。

参考资料

作者:李一笑
来源:https://segmentfault.com/a/1190000041115022

收起阅读 »

一个Vue3可使用的JSON转excel组件

JSON to Excel for VUE3在浏览器中将JSON格式数据以excel文件的形式下载。该组件是基于this thread 提出的解决方案。支持Vue3.2.25及以上版本使用重要提示! Microsoft Excel中的额外提示此组件中实现的方法...
继续阅读 »



JSON to Excel for VUE3

在浏览器中将JSON格式数据以excel文件的形式下载。该组件是基于this thread 提出的解决方案。支持Vue3.2.25及以上版本使用

重要提示! Microsoft Excel中的额外提示

此组件中实现的方法使用HTML表绘制。在xls文件中,Microsoft Excel不再将HTML识别为本机内容,因此在打开文件之前会显示警告消息。excel的内容已经完美呈现,但是提示信息无法避免,请不要在意!

Getting started

安装依赖:

npm install vue3-json-excel

在vue3的应用入口处有两种注册组件的方式:

import Vue from "vue"
import {vue3JsonExcel} from "vue3-json-excel"

Vue.component("vue3JsonExcel", vue3JsonExcel)

或者

import Vue from "vue"
import vue3JsonExcel from "vue3-json-excel"

Vue.use(vue3JsonExcel)

在template文件中直接使用即可

<vue3-json-excel :json-data="json_data">
Download Data
</vue3-json-excel>

Props List

NameTypeDescriptionDefaultremark
json-dataArray即将导出的数据

fieldsObject要导出的JSON对象内的字段。如果未提供任何属性,将导出JSON中的所有属性。

export-fields (exportFields)Object用于修复使用变量字段的其他组件的问题,如vee-validate。exportFields的工作原理与fields完全相同

typestringMime 类型 [xls, csv]xls1.0.x版本暂时只支持xls,csv会在下个版本迭代
namestringFile 导出的文件名jsonData.xls
headerstring/Array数据的标题。可以是字符串(一个标题)或字符串数组(多个标题)。

title(deprecated)string/Array与header相同,title是出于追溯兼容性目的而维护的,但由于与HTML5 title属性冲突,不建议使用它。

License

MIT

Status

该项目处于早期开发阶段。欢迎参与共建。
有好的产品建议可以联系我!!!!

npm地址

vue3-json-excel

作者:小章鱼
来源:https://segmentfault.com/a/1190000041117522

收起阅读 »

小程序框架对比(Taro VS uni-app)

前前段时间,要开发一个小程序,需要选一个跨平台的框架,为此做了一些调研,在这里记录一下。 目前的跨平台方案大致是以下三种类型,各有优劣。 结合项目自身情况,我选择了第三种类型的框架,再结合支持多平台的要求,重点锁定在了Taro和uni-app之间。 ...
继续阅读 »

前前段时间,要开发一个小程序,需要选一个跨平台的框架,为此做了一些调研,在这里记录一下。


目前的跨平台方案大致是以下三种类型,各有优劣。


image.png


结合项目自身情况,我选择了第三种类型的框架,再结合支持多平台的要求,重点锁定在了Tarouni-app之间。















































框架技术栈微信小程序H5App支付宝/百度小程序
TaroReact/Vue
uni-appVue
WePYVue
mpvueVue

Taro


开发者:


京东


优缺点:


Taro在App端使用的是React Native的渲染引擎,原生的UI体验较好,但据说在实时交互和高响应要求的操作方面不是很理想。


微信小程序方面,结合度感觉没有那么顺滑,有一些常见功能还是需要自己去封装。


另外就是开发环境难度稍高,需要自己去搭建iOS和Android的环境,对于想要一处开发到处应用的傻瓜式操作来讲,稍显繁琐。


但Taro 3的出现,支持了React 和 Vue两种DSL,适合的人群会更多一点,并且对快应用的支持也更好。


案例:


image.png


学习成本:


React、RN、小程序、XCode、Android Studio


uni-app


开发者:


DCloud


优缺点:


uni-app在App渲染方面,提供了原生渲染引擎和小程序引擎的双选方案,加上自身的一些技术优化(renderjs),对于高性能和响应要求的场景展现得更为流畅。


另外它整体的开发配套流程也做得很容易上手。比如有丰富的插件市场,使用简单,支持大量常用场景。


还比如它的定制IDE——HBuilder,提供了强大的整合能力。在用HBuilder之前,我心想:“还要多装一个编辑器麻烦,再好用能有VS Code好用?”用过之后:“真香!”


虽然用惯了VS Code对比起来还是有一些痛点没有解决,但是对于跨平台开发太友好了,其他缺点都可以忍受。HBuilder里支持直接跳转到微信开发者工具调试,支持真机实时预览,支持直接打包小程序和App,零门槛上手。


image.png


不过,uni-app也还是不够成熟,开发中也存在一些坑,需要不时到论坛社区去寻找答案。


代表产品:


image.png


学习成本:


Vue、小程序


总结


跨平台方案目前来看都不完善,适合以小程序、H5为主,原生APP(RN)为辅,不涉及太过复杂的交互的项目。


uni-app 开发简单,小项目效率高,入门容易debug难,不适合中大型项目。
Taro 3 开发流程稍微复杂一点,但复杂项目的支持度会稍好,未来可以打通React和Vue,已经开始支持RN了。



  1. 不考虑原生RN的话二者差不多,考虑RN目前Taro3不支持,只能选uni-app;

  2. 开发效率uni-app高,有自家的IDE(HBuilderX),编译调试打包一体化,对原生App开发体验友好;

  3. 个人技术栈方面倾向于Taro/React,但项目角度uni-app/Vue比较短平快,社区活跃度也比较高。

作者:sherryhe
链接:https://juejin.cn/post/6974584590841167879

收起阅读 »

前端重新部署后,领导跟我说页面崩溃了..

背景: 每次前端更新,重新部署后,用户还停留在更新之前的页面,当请求页面数据时,会导致页面白屏,报错信息如下: Uncaught ChunkLoadError: Loading chunk {n} failed. 原因 每次更新后,用户端的html文件中的 j...
继续阅读 »

背景:


每次前端更新,重新部署后,用户还停留在更新之前的页面,当请求页面数据时,会导致页面白屏,报错信息如下:


Uncaught ChunkLoadError: Loading chunk {n} failed.


原因


每次更新后,用户端的html文件中的 js 和 css 名称就和服务器上的不一致导致,导致加载失败。


解决方案


1.对error事件进行监听,检测到错误之后重新刷新


      window.addEventListener(
'error',
function (event) {
if (
event.message &&
String(event.message).includes('Loading chunk') &&
String(event.message).includes('failed')
) {
window.location.replace(window.location.href);
}
},
true,
);

2.对window.console.error事件监听,效果同上


      window.console.error = function () {
console.log(JSON.stringify(arguments), 'window.console.error'); // 自定义处理
};

3.其他方案


如:HTTP2.0推送机制 / fis3 构建 /webSocket通知等,未尝试


注:有好的方案可以在下面评论讨论哈


本篇收录在个人工作记录专栏中,专门记录一些比较有意思的场景和问题。


后记


在之后的某一天,该问题再次暴露出来。源于一位同事在使用过程中,会不定页面的出现报错情况,报错如下:


image.png


很明显,还是资源加载问题,按道理讲应该可以走入我们逻辑进行刷新。但是当时用户反馈:刷新也不能解决问题,强制刷新才可以解决。


分析:刷新为什么不能解决问题?其实还是因为当用户第一次因为网络原因未成功加载到资源,后续刷新均走的缓存,因此让用户手动去刷新解决不了问题。


解决方案:


不知道大家发现没有,上面对error事件进行监听代码中,并没有包括css失败的情况,因此匹配上css加载失败的情况即可。


更新代码如下:


      window.addEventListener(
'error',
function (event) {
if (
event.message &&
String(event.message).includes('chunk') &&
String(event.message).includes('failed')
) {
window.location.replace(window.location.href);
}
},
true,
);

可能有人会问,既然刷新解决不了问题,那window.location.replace(window.location.href)可以解决吗?


其实刷新的方法有很多,根据MDN的说法,Location.replace() 方法会以给定的URL替换当前的资源,因此可解决此问题。



作者:纵有疾风起
链接:https://juejin.cn/post/6981718762483679239

收起阅读 »

上一个程序员提桶跑路了!我接手后用这些方法优化了项目

平常我们在开发和维护项目的过程中,如果我们跑的项目有点大啊,或者数据太多,导致项目跑起来弊蜗牛还要慢,然后用户体验还不友好,对于新手程序员来说!老板天天都要你加班改!你还不敢辞职!这种时候,就很让人头痛了,怎么办! 但是!也不是没有办法的!骚年!你当时学vue...
继续阅读 »

平常我们在开发和维护项目的过程中,如果我们跑的项目有点大啊,或者数据太多,导致项目跑起来弊蜗牛还要慢,然后用户体验还不友好,对于新手程序员来说!老板天天都要你加班改!你还不敢辞职!这种时候,就很让人头痛了,怎么办!


但是!也不是没有办法的!骚年!你当时学vue的时候可不是这样说的!


接下来我来给你浓重介绍几个优化性能的小技巧,让你的项目蹭蹭蹭的飞起来!老板看了都直呼内行!


1.v-if和v-show的使用场景要区分


v-if 是条件渲染,当条件满足了,那肯定是渲染哇!如果你需要设置一个元素随时隐藏或者消失,然后用v-if是非常的浪费性能的,因为它不停的创建然后销毁。


但是它也是惰性的,如果你开始一给它条件为 false,它就害羞不出来了,跟你家女朋友一样天天晚上都不跟你回家,甚至你家里都没有你女朋友的衣物!然后结构页面里也不会渲染出来查不到这个元素,不知情的朋友以为你谈了个虚拟女友,直到你让它条件为 true ,它才开始渲染,也就是拿了结婚证才跟你回家。


v-show 就很简单,他的原理就是利用 css display 的属性,让他隐藏或者出现,所以一开始渲染页面哪怕我们看不到这个元素,但是它在文档的话,是确确实实存在的,只是因为 display:none; 隐藏了。




就像是你的打气女朋友,平常有人你肯定不敢打气哇!肯定是等夜深人静的时候,才偷偷打气,然后早上又继续放气藏起来,这样是不是很方便咧!所以这个元素你也就是你打气女朋友每天打气放气,是不是也没有那么费力咧!白天就可以藏起来快乐的上班啦!


好啦划重点啦!不要瞎鸡巴想什么女朋友了,女朋友只会影响我码项目的速度!


所以这样看来,如果是很少改变条件来渲染或者销毁的,建议是用 v-if ,如果是需要不断地切换,不断地隐藏又出现又隐藏这些场景的话, v-show 更适合使用!所以要在这些场景里合适的运用 v-if v-show 会节省很多性能以达到性能优化。


2.v-if和v-for不能同时使用


v-if v-for 一起使用时, v-for **** v-if 更高的优先级。这样就意味着 v-if 将分别重复运行于每一个 v-for 循中,那就是先运行 v-for 的循环,然后在每一个 v-for 的循环中,再进行 v-if 的条件对比,会造成性能浪费,影响项目的速度




如果按照下面的写法(我是用vue3写的),好家伙!直接不工作了,没有报错也没有页面,诡异的很


<template>
<div id="app">
<div v-for="item in list" v-if="list.flag" :key="item">{{item.color}}</div>
</div>
</template>
<script>
export default {
data(){
return{
list:[
{
color:"red",
flag:true
},
{
color:"green",
flag:false
},
{
color:"blue",
flag:true
},
],
}
}}
</script>

当你真的需要根据条件渲染页面的时候,建议是采用计算属性,这样及高效且美观,又不会出问题,如下面的代码展示


<template>
<div id="app">
<div v-for="item in newList" :key="item">{{item.color}}</div>
</div>
</template>
<script>
export default {
data(){
return{
list:[
{
color:"red",
flag:true
},
{
color:"green",
flag:false
},
{
color:"blue",
flag:true
},
],
}
},
computed:{
newList(){
return this.list.filter(list => {
return list.flag
})
}
}
}
</script>

3.computed和watch使用要区分场景


先看一下计算属性computed,它支持缓存,当依赖的数据发生变化的时候,才会重新计算,但是它并不支持异步操作,它无法监听数据变化。而且计算属性是基于响应式依赖进行缓存的。


再看一下侦听属性watch,它不支持缓存,它支持异步操作,当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。这是和computed最大的区别。


所以说,如果你的需求是写像购物车那种的,一个属性受其他属性影响的,用计算属性 computed 就像是你家的二哈,你不带它出去玩,你一回家就发现你家能拆的都被二哈拆掉了,因为你不带它出去跟你女朋友逛街!


如果是像写那种像模糊查询的,可以用侦听属性 watch ,因为可以一条数据影响多条数据。 比如你双12给你女朋友买了很多东西,那双十二之后,是不是很多机会回不去宿舍咧!


用好这两个,可以让你的代码更加高效,看起来也更加简洁优雅,让项目蹭蹭跑起来!这样都是一种优化性能的方式!


4.路由懒加载


当Vue项目是单页面应用的时候,可能会有很多路由引入 ,这样的话,使用 webpcak 打包后的文件会非常的大,当第一次进入首页的时候,加载的资源很多多,页面有时候会出现白屏的情况,对用户很不友好,体验也差。


但是,当我们把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就很高效了。会大大提升首屏加载显示的速度,但是可能其他的页面的速度就会降下来。有利有弊吧,根据自己业务需求来使用,实现效果也非常的简单,在 router index.js 文件下,如下所示


import Home from '../views/Home'

const routes = [
{
path:'/home',
name:"Home", //这里没有用懒加载
component:Home
},
{
path:'/about',
name:'About',
component:()=>import(/*webpackChunkName:"about"*/ '../views/About.vue') //这里用了懒加载
}
]

打开浏览器运行,当我没有点击进入 about 组件的时候包的大小就如蓝色框住的那些,当我点击了 about 组件进入后,就增加了后面红色圈住的包,总的大小是增加了



所以,使用路由懒加载可以降低首次加载的时候的性能消耗,但是后面打开这些组件可能会有所减慢,建议是如果体积不大的又不用马上展示的页面可以使用路由懒加载降低性能消耗,从而做到性能优化!


5.第三方插件按需引入


比如我们做一个项目,如果是全局引入第三方插件,打包构建的时候,会将别人整个插件包也一起打包进去,这样的话文件是非常庞大的,然后我们就需要将第三方插件按需引入,这个就需要自己去根据每个插件的官方文档在项目配置,始终就是一句话,用什么引什么!


6.优化列表的数据


当我们遇到哪些,一开始取的数据非常的庞大,然后还要渲染在页面上的时候,比如一下子给你传回来10w条数据还要渲染在页面上,项目一下子渲染出来是非常的有难度的。


这个时候,我们就 需要采用窗口化的技术来优化性能,只需要渲染少部分的内容(比如一下子拿多少条数据),这样就可以减少重新渲染组件和创建dom节点的时间。可以看看下面代码


<template>
<div>
<h3>列表的懒加载</h3>
<div>
<div v-for="item in list">
<div>{{ item }}</div>
</div>
</div>
<div>
<div v-if="moreShowBoolen">滚动加载更多</div>
<div v-else>已无更多</div>
</div>
</div>
</template>
<script>
export default {
name: 'List',
data() {
return {
list: [],
moreShowBoolen: false,
nowPage: 1,
scrollHeight: 0,
};
},
mounted() {
this.init();
// document.documentElement.scrollTop获取当前页面滚动条的位置,documentElement对应的是html标签,body对应的是body标签
// document.compatMode用来判断当前浏览器采用的渲染方式,CSS1Compat表示标准兼容模式开启
window.addEventListener("scroll", () => {
let scrollY=document.documentElement.scrollTop || document.body.scrollTop; // 滚动条在Y轴上的滚动距离
let vh=document.compatMode === "CSS1Compat"?document.documentElement.clientHeight:document.body.clientHeight; // 页面的可视高度
let allHeight = Math.max(
document.body.scrollHeight,
document.documentElement.scrollHeight
); // 整个页面的高度
if (scrollY + vh >= allHeight) {
// 当滚动条滑到页面底部的时候触发这个函数继续添加数据
this.init2();
}
});
},
methods: {
init() {
//一开始就往list添加数据
for (let i = 0; i < 100; i++) {
this.list.push(i);
}
},
init2() {
for (let i = 0; i < 200; i++) {
// 当滑动到底部的时候,继续触发这个函数
this.list.push(i);
}
},
},
};
</script>

这样的话,就可以做到数据懒加载啦,根据需要,逐步添加数据进去,减少一次性拉取所有数据,因为数据是非常庞大的,这样就可以优化很多性能了!


作者:零零后程序员小三
链接:https://juejin.cn/post/7041471019327946759

收起阅读 »

axios 封装,API接口统一管理,支持动态API!

vue
分享一个自己封装的 axios 网络请求 主要的功能及其优点: 将所有的接口放在一个文件夹中管理(api.js)。并且可以支持动态接口,就是 api.js 文件中定义的接口可以使用 :xx 占位,根据需要动态的改变。动态接口用法模仿的是vue的动态路由,如果你...
继续阅读 »

分享一个自己封装的 axios 网络请求


主要的功能及其优点:


将所有的接口放在一个文件夹中管理(api.js)。并且可以支持动态接口,就是 api.js 文件中定义的接口可以使用 :xx 占位,根据需要动态的改变。动态接口用法模仿的是vue的动态路由,如果你不熟悉动态路由可以看看我的这篇文章:Vue路由传参详解(params 与 query)


1.封装请求:



  1. 首先在 src 目录下创建 http 目录。继续在 http 目录中创建 api.js 文件与 index.js 文件。

  2. 然后再 main.js 文件中导入 http 目录下的 index.js 文件。将请求注册为全局组件。

  3. 将下面封装所需代码代码粘到对应的文件夹


2.基本使用:


//示例:获取用户列表
getUsers() {
 const { data } = await this.$http({
   url: 'users' //这里的 users 就是 api.js 中定义的“属性名”
})
},

3.动态接口的使用:


//示例:删除用户
deleteUser() {
 const { data } = await this.$http({
   method: 'delete',
   //动态接口写法模仿的是vue的动态路由
   //这里 params 携带的是动态参数,其中 “属性名” 需要与 api 接口中的 :id 对应
   //也就是需要保证携带参数的 key 与 api 接口中的 :xx 一致
   url: {
     // 这里的 name 值就是 api.js 接口中的 “属性名”
     name: 'usersEdit',
     params: {
       id: userinfo.id,
    },
  },
})
},

4.不足:


封装的请求只能这样使用 this.$http() 。不能 this.$http.get()this.$http.delete()


由于我感觉使用 this.$http() 这种就够了,所以没做其他的封装处理


如果你有更好的想法可以随时联系我


如下是封装所需代码:



  • api.js 管理所有的接口


// 如下接口地址根据自身项目定义
const API = {
 // base接口
 baseURL: 'http://127.0.0.1:8888/api/private/v1/',
 // 用户
 users: '/users',
 // “修改”与“删除”用户接口(动态接口)
 usersEdit: '/users/:id',
}

export default API


  • index.js 逻辑代码


// 这里请求封装的主要逻辑,你可以分析并将他优化,如果有更好的封装方法欢迎联系我Q:2356924146
import axios from 'axios'
import API from './api.js'

const instance = axios.create({
 baseURL: API.baseURL,
 timeout: '8000',
 method: 'GET'
})

// 请求拦截器
instance.interceptors.request.use(
 config => {
   // 此处编写请求拦截代码,一般用于加载弹窗,或者每个请求都需要携带的token
   console.log('正在请求...')
   // 请求携带的token
   config.headers.Authorization = sessionStorage.getItem('token')
   return config
},
 err => {
   console.log('请求失败', err)
}
)

// 响应拦截器
instance.interceptors.response.use(
 res => {
   console.log('响应成功')
   //该返回对象会绑定到响应对象中
   return res
},
 err => {
   console.log('响应失败', err)
}
)

//options 接收 {method, url, params/data}
export default function(options = {}) {
 return instance({
   method: options.method,
   url: (function() {
     const URL = options.url

     if (typeof URL === 'object') {
       //拿到动态 url
       let DynamicURL = API[URL.name]

       //将 DynamicURL 中对应的 key 进行替换
       for (const key of Object.keys(URL.params)) {
         DynamicURL = DynamicURL.replace(':' + key, URL.params[key])
      }

       return DynamicURL
    } else {
       return API[URL]
    }
  })(),
   //获取查询字符串参数
   params: options.params,
   //获取请求体字符串参数
   data: options.data
})
}



  • main.js 将请求注册为全局组件


import Vue from 'vue'

// 会自动导入 http 目录中的 index.js 文件
import http from './http'

Vue.prototype.$http = http

作者:寸头男生
链接:https://juejin.cn/post/7006508579595223070

收起阅读 »

关于组件文档从编写到生成的那些事

前言说到前端领域的组件,Vue 技术体系下有 Element UI,React 技术体系下有 Ant Design,这些都是当前的前端攻城狮们都免不了要实际使用到的基础组件库。而在实际工作中,我们也总免不了要根据自己的工作内容,整理一些适合自己业务风格的一套组...
继续阅读 »



前言

说到前端领域的组件,Vue 技术体系下有 Element UI,React 技术体系下有 Ant Design,这些都是当前的前端攻城狮们都免不了要实际使用到的基础组件库。而在实际工作中,我们也总免不了要根据自己的工作内容,整理一些适合自己业务风格的一套组件库,基础组件部分可以基于上面开源的组件库以及 less 框架等多主题样式方案做自己的定制,但更多的是一些基于这些基础组件整理出适合自己业务产品的一套业务组件库。

而说到开发组件库,我们或选择 Monorepo 单仓库多包的形式(参考网文 https://segmentfault.com/a/11... 等)或其他 Git 多仓库单包的形式来维护组件代码,最终都免不了要将组件真正落到一个文档中,提供给其他同事去参考使用。

本篇文章就产出组件文档这件事,聊聊我在产出文档过程中的一系列思考过程,解决组件开发这「最后一公里」中的体验问题。

组件文档的编写

规范与搭建的调研

组件文档是要有一定的规范的,规范是任何软件工程阶段的第一步。对于组件来说,文档透出的内容都应包含哪些内容,决定着组件开发者和使用者双方的所有的体验。确定这些规范并不难,参考开源的组件库的文档,我们会有一些初步的印象。

因为我们团队使用的是 React 技术栈,这里我们参考 Ant Design 组件库。

比如这个最基本的 Button 组件,官方文档从上至下的内容结构是这样:

  1. 显示组件标题,下跟组件的简单描述。

  2. 列出组件的使用场景。

  3. 不同使用场景下的效果演示、代码案例。可外跳 CodeSandbox、CodePen 等在线源码编辑器网站编辑实时查看效果。

  4. 列出组件可配置的属性、接口方法列表。列表中包含属性/方法名、字段类型、默认值、使用描述等。

  5. 常见问题的 FAQ。

  6. 面向设计师的一些 Case 说明链接。

这些文档内容很丰富,作为一个开放的组件库,几乎考虑到的从设计到开发视角的方方面面,使用体验特别好。而在好奇心驱使下,我去查看了官网源码方库,比如 Button 组件:https://github.com/ant-design...。在源码库下,放置了和组件入口文件同名的分别以 .zh-CN.md.en-US.md 后缀命名的 Markdown 文件,而在这些 Markdown 文件中,便是我们看到的官网文档内容...咦?不对,好像缺少了什么,案例演示和示例代码呢?

难道 AntD 官网文档是另外自己手动开发维护的?这么大的一个项目肯定不至于,根据其官网类似 docs/react/introduce-cn 这种访问路径在源码库中有对应的 Markdown 文件来看,官网的文档肯定是官方仓库根据一种规范来生成的。那么是怎么生成的呢?第一次做组件文档规范的我被挑起了兴趣。

而作为一个前端工程老手,我很熟练地打开了其 package.json 文件,通过查看其中的 scripts 命令,轻易便发现了其下的 site 命令(源码仓库 package.json):

npm run site:theme && cross-env NODE_ICU_DATA=node_modules/full-icu ESBUILD=1 bisheng build --ssr -c ./site/bisheng.config.js

原来如此,网站的构建使用了 bisheng。通过查阅了解 bisheng 这个工具库,发现它确实是一个文档系统的自动生成工具,其下有一个插件 bisheng-plugin-react,可以将 Markdown 文档中的 JSX 源码块转换成可以运行演示的示例。而每个组件自身的示例代码文档,则在每个组件路径下的
demo 目录下维护。

Emmm,bisheng 确实是很好很强大,还能支持多语言,结合一定的文档规范约束下,能够快速搭建一个文档的主站。但在深入了解 bisheng 的过程中,发现其文档相对来说比较缺乏,包装的东西又那么多,使用过程中黑盒感严重,而我们团队的组件库其实要求很简单,一是能做到方便流通,而是只在内部流通使用,不会开源。那么,有没有更简单的搭建文档的方式呢?

更多的文档工具库的调研

在谷歌搜索中敲入如 React Components Documentation 等关键字,我们很快便能搜索出很多与 React 组件文档相关的工具库,这里我看到了如下这些:DoczStoryBookReact Styleguidist 、UMI 构建体系下的 dumi 等等。

这些工具库都支持解析 Markdown 文档,其中 DoczStoryBook 还支持使用 mdx 格式(Markdown 和 JSX 的混合写法),且在文档内容格式都能支持到组件属性列表、示例代码演示等功能。

接下来,我们分别简单看下这些工具库对于组件文档的支持情况。

Docz

在了解过程中,发现 Docz 其实是一个比较老牌的文档系统搭建工具了。它本身便主推 MDX 格式的文档,基本不需要什么配置便能跑起来。支持本地调试和构建生成可发布产物,支持多包仓库、TypeScript 环境、CSS 处理器、插件机制等,完全满足功能需要。

只是 Docz 貌似只支持 React 组件(当然对于我们来说够用),且看其 NPM 包最近更新已经是两年之前。另外 MDX 格式的文档虽然理解成本很少但对于使用不多的同事来说还是有一定的接受和熟练上手的成本。暂时备选。

StoryBook

在初次了解到 StoryBook 时便被其 66.7K 的 Star 量惊到了(Docz 是 22K),相对 Docz 来说,StoryBook 相关的社区内容非常丰富,它不依赖组件的技术栈体系,现在已经支持 React、Vue、Angular、Web Components 等数十个技术栈。

StoryBook 搭建文档系统的方式不是去自动解析 Markdown 文件,而是暴露一系列搭建文档的接口,让开发者自己为组件手动编写一个个的 stories 文件,StoryBook 会自动解析这些 stories 文件来生成文档内容。这种方式会带来一定的学习和理解接口的成本,但同时也基于这种方式实现了支持跨组件技术栈的效果,并让社区显得更为丰富。

官方示例:https://github.com/storybookj...

StoryBook 的强大毋庸置疑,但对于我们团队的情况来说还是有些杀鸡用牛刀了。另外,其需要额外理解接口功能并编写组件的 stories 文件在团队内很难推动起来:大家都很忙,组件开发分布在团队几十号人,情况比较复杂,将文档整理约束到一个人身上又不现实。继续调研。

React Styleguidist

React Styleguidist 的 Star 量没有 StoryBook 那么耀眼(10K+),但包体的下载量也比较大,且近期的提交也是相当活跃。由名字可知,它支持的是 React 组件的环境。它是通过自动解析 Mardown 文件的形式来生成文档的,实现方式是自动解析文档中 JSX 声明代码块,按照名称一一对应的规则查找到组件源码,然后将声明的代码块通过 Webpack 打包产生出对应的演示示例。

而在继续试用了 React Styleguidist 的一些基础案例后,它的一个功能让我眼前一亮:它会自动解析组件的属性,并解析出其类型、默认值、注释描述等内容,然后将解析到的内容自动生成属性表格放置在演示示例的上方。这就有点 JSDOC 的意思了,对于一个组件开发者来说,TA 确实需要关心组件属性的透出、注释以及文档案例的编写,但编写完也就够了,不用去考虑怎么适应搭建出一个文档系统。

另外, React Styleguidist 解析组件属性是基于解析 AST 以及配合工具 react-docgen 来实现的,并且还支持配合 react-docgen-typescript 来实现解析 TypeScript 环境下的组件,另外还能很多配置项支持更改文档站点相关的各个部分的展示样式、内容格式等,配置自定义支持相当灵活。

当然,它也有一些缺点,比如内嵌 Webpack,对于已经将编译组件库的构建工具换为 Rollup.js 的情况是一个额外的配置负担。

总的来说,React Styleguidist 在我看来是一个小而美的工具库,很适合我们团队协作参与人多、且大都日常开发工作繁重的情况。暂时备选。

dumi

了解到 dumi 是因为我们团队内已经有部分组件文档站点是基于它来搭建的了。dumi 一样是通过自动解析 Markdown 文档的方式来实现搭建文档系统,同样基本零配置,也有很多灵活的配置支持更改文档站点一些部分的显示内容、(主题)样式等,整体秉承了 UMI 体系的风格:开箱即用,封装极好。它能单独使用,也能结合 UMI 框架一起配置使用。

只是相比于上面已经了解到的 React Styleguidist 来说,并未看到有其他明显的优势,且貌似没有看到有自动解析组件属性部分的功能,对于我来说没有 React Styleguidist 下得一些亮点。可以参考,不再考虑。

组件文档的生成

在多方对比了多个文档搭建的工具库后,我最终还是选用了 React Styleguidist。在我看来,自然是其基于 react-docgen 来实现解析组件属性、类型、注释描述等的功能吸引到了我,这个功能一方面能在较少的额外时间付出下规范团队同事开发组件过程中一系列规范,另一方面其 API 接口的接入形式能够通过统一构建配置而统一产出文档内容格式和样式,方便各业务接入使用。

决定了技术方案后,便是如何具体实现基于其封装一个工具,便于各业务仓库接入了。

我们团队有自己统一的 CLI 构建工具,再多一个 React Styleguidist 的 CLI 配置会在理解上有一定的熟悉成本,但我可以基于 React Styleguidist 的 Node API 接入形式,将 React Styleguidist 的功能分别融入我们自身 CLI 的 devbuild 命令。

首先,基于 React Styleguidist API 的形式,统一一套配置,将生成 React Styleguidist 示例的代码抽象出来:

// 定义一套统一的配置,生成 react-styleguidist 实例
import styleguidist from 'react-styleguidist/lib/scripts/index.esm';
import * as docgen from 'react-docgen';
import * as docgenTS from 'react-docgen-typescript';

import type * as RDocgen from 'react-docgen';

export type DocStyleguideOptions = {
 cwd?: string;
 rootDir: string;
 workDir: string;
 customConfig?: object;
};

const DOC_STYLEGUIDE_DEFAULTS = {
 cwd: process.cwd(),
 rootDir: process.cwd(),
 workDir: process.cwd(),
 customConfig: {},
};

export const createDocStyleguide = (
 env: 'development' | 'production',
 options: DocStyleguideOptions = DOC_STYLEGUIDE_DEFAULTS,
) => {
 // 0. 处理配置项
 const opts = { ...DOC_STYLEGUIDE_DEFAULTS, ...options };
 const {
   cwd: cwdPath = DOC_STYLEGUIDE_DEFAULTS.cwd,
   rootDir,
   workDir,
   customConfig,
} = opts;

 // 标记:是否正在调试所有包
 let isDevAllPackages = true;

 // 解析工程根目录包信息
 const pkgRootJson = Utils.parsePackageSync(rootDir);

 // 1. 解析指定要调试的包下的组件
 let componentsPattern: (() => string[]) | string | string[] = [];
 if (path.relative(rootDir, workDir).length <= 0) {
   // 选择调试所有包时,则读取根路径下 packages 字段定义的所有包下的组件
   const { packages = [] } = pkgRootJson;
   componentsPattern = packages.map(packagePattern => (
     path.relative(cwdPath, path.join(rootDir, packagePattern, 'src/**/[A-Z]*.{js,jsx,ts,tsx}'))
  ));
} else {
   // 选择调试某个包时,则定位至选择的具体包下的组件
   componentsPattern = path.join(workDir, 'src/**/[A-Z]*.{js,jsx,ts,tsx}');
   isDevAllPackages = false;
}

 // 2. 获取默认的 webpack 配置
 const webpackConfig = getWebpackConfig(env);

 // 3. 生成 styleguidist 配置实例
 const styleguide = styleguidist({
   title: `${pkgRootJson.name}`,
   // 要解析的所有组件
   components: componentsPattern,
   // 属性解析设置
   propsParser: (filePath, code, resolver, handlers) => {
     if (/\.tsx?/.test(filePath)) {
       // ts 文件,使用 typescript docgen 解析器
       const pkgRootDir = findPackageRootDir(path.dirname(filePath));
       const tsConfigParser = docgenTS.withCustomConfig(
         path.resolve(pkgRootDir, 'tsconfig.json'),
        {},
      );
       const parseResults = tsConfigParser.parse(filePath);
       const parseResult = parseResults[0];
       return (parseResult as any) as RDocgen.DocumentationObject;
    }
     // 其他使用默认的 react-docgen 解析器
     const parseResults = docgen.parse(code, resolver, handlers);
     if (Array.isArray(parseResults)) {
       return parseResults[0];
    }
     return parseResults;
  },
   // webpack 配置
   webpackConfig: { ...webpackConfig },
   // 初始是否展开代码样例
   // expand: 展开 | collapse: 折叠 | hide: 不显示;
   exampleMode: 'expand',
   // 组件 path 展示内容
   getComponentPathLine: (componentPath) => {
     const pkgRootDir = findPackageRootDir(path.dirname(componentPath));
     try {
       const pkgJson = Utils.parsePackageSync(pkgRootDir);
       const name = path.basename(componentPath, path.extname(componentPath));
       return `import ${name} from '${pkgJson.name}';`;
    } catch (error) {
       return componentPath;
    }
  },
   // 非调试所有包时,不显示 sidebar
   showSidebar: isDevAllPackages,
   // 日志配置
   logger: {
     // One of: info, debug, warn
     info: message => Utils.log('info', message),
     warn: message => Utils.log('warning', message),
     debug: message => console.debug(message),
  },
   // 覆盖自定义配置
   ...customConfig,
});

 return styleguide;
};

这样,在 devbuild 命令下可以分别调用实例的 server 接口方法和 build 接口方法来实现调试和构建产出文档静态资源。

// dev 命令下启动调试
// 0. 初始化配置
const HOST = process.env.HOST || customConfig.serverHost || '0.0.0.0';
const PORT = process.env.PORT || customConfig.serverPort || '6060';

// 1. 生成 styleguide 实例
const styleguide = createDocStyleguide(
 'development',
{
   cwd: cwdPath,
   rootDir: pkgRootPath,
   workDir: workPath,
   customConfig: {
     ...customConfig,
     // dev server host
     serverHost: HOST,
     // dev server port
     serverPort: PORT,
  },
},
);

// 2. 调用 server 接口方法启动调试
const { compiler } = styleguide.server((err, config) => {
 if (err) {
   console.error(err);
} else {
   const url = `http://${config.serverHost}:${config.serverPort}`;
   Utils.log('info', `Listening at ${url}`);
}
});
compiler.hooks.done.tap('done', (stats: any) => {
 const timeStr = stats.toString({
   all: false,
   timings: true,
});

 const statStr = stats.toString({
   all: false,
   warnings: true,
   errors: true,
});

 console.log(timeStr);

 if (stats.hasErrors()) {
   console.log(statStr);
   return;
}
});
// build 命令下执行构建

// 生成 styleguide 实例
const styleguide = MonorepoDev.createDocStyleguide('production', {
 cwd,
 rootDir,
 workDir,
 customConfig: {
   styleguideDir: path.join(pkgDocsDir, 'dist'),
},
});
// 构建文档内容
await new Promise<void>((resolve, reject) => {
 styleguide.build(
  (err, config, stats) => {
     if (err) {
       reject(err);
    } else {
       if (stats != null) {
         const statStr = stats.toString({
           all: false,
           warnings: true,
           errors: true,
        });
         console.log(statStr);
         if (stats.hasErrors()) {
           reject(new Error('Docs build failed!'));
           return;
        }
         console.log('\n');
         Utils.log('success', `Docs published to ${path.relative(workDir, config.styleguideDir)}`);
      }
       resolve();
    }
  },
);

最后,在组件多包仓库的每个包下的 package.json 中,分别配置 devbuild 命令即可。实现了支持无感启动调试和构建产出文档资源。

小结

本文主要介绍了我在调研实现组件文档规范和搭建过程中的一个思考过程,诚如文中介绍其他文档系统搭建工具时所说,有很多优秀的开源工具能够支持实现我们想要的效果,这是前端攻城狮们的幸运,也是不幸:我们可以站在前人的肩膀上,但要在这么多优秀库中选择一个适合自己的,更需要多做一些了解和收益点的权衡。一句老话经久不衰:适合自己的才是最好的。

希望这篇文章对看到这里的你能有所帮助。

作者:ES2049 / 靳志凯
链接:https://segmentfault.com/a/1190000041097170

收起阅读 »

前端 4 种渲染技术的计算机理论基础

前端可用的渲染技术有 html + css、canvas、svg、webgl,我们会综合运用这些技术来绘制页面。有没有想过这些技术有什么区别和联系,它们和图形学有什么关系呢?本文我们就来谈一下网页渲染技术的计算机理论基础。渲染的理论基础人眼的视网膜有视觉暂留机...
继续阅读 »

前端可用的渲染技术有 html + css、canvas、svg、webgl,我们会综合运用这些技术来绘制页面。有没有想过这些技术有什么区别和联系,它们和图形学有什么关系呢?

本文我们就来谈一下网页渲染技术的计算机理论基础。

渲染的理论基础

人眼的视网膜有视觉暂留机制,也就是看到的图像会继续保留 0.1s 左右,图形界面就是根据这个原理来设计的一帧一帧刷新的机制,要保证 1s 至少要渲染 10 帧,这样人眼看到画面才是连续的。

每帧显示的都是图像,它是由像素组成的,是显示的基本单位。不同显示器实现像素的原理不同。

我们要绘制的目标是矩形、圆形、椭圆、曲线等各种图形,绘制完之后要把它们转成图像。图形的绘制有一系列的理论,比如贝塞尔曲线是画曲线的理论。图形转图像的过程叫做光栅化。这些图形的绘制和光栅化的过程,都是图形学研究的内容。

图形可能做缩放、平移、旋转等变化,这些是通过矩阵计算来实现的,也是图形学的内容。

除了 2D 的图形外,还要绘制 3D 的图形。3D 的原理是把一个个三维坐标的顶点连起来,构成一个一个三角形,这是造型的过程。之后再把每一个三角形的面贴上图,叫做纹理。这样组成的就是一个 3D 图形,也叫 3D 模型。

3D 图形也同样需要经历光栅化变成二维的图像,然后显示出来。这种三维图形的光栅化需要找一个角度去观察,就像拍照一样,所以一般把这个概念叫做相机。

同时,为了 3D 图形更真实,还引入了光线的概念,也就是一束光照过来,3D 图形的每个面都会有什么变化,怎么反射等。不同材质的物体反射的方式不同,比如漫反射、镜面反射等,也就有不同的计算公式。一束光会照射到一些物体,到物体的反射,这个过程需要一系列跟踪的计算,叫做光线追踪技术。

我们也能感受出来,3D 图形的计算量比 2D 图形大太多了,用 CPU 计算很可能达不到 1s 大于 10 帧,所以后面出现了专门用于 3D 渲染加速的硬件,叫做 GPU。它是专门用于这种并行计算的,可以批量计算一堆顶点、一堆三角形、一堆像素的光栅化,这个渲染流程叫做渲染管线。

现在的渲染管线都是可编程的,也就是可以控制顶点的位置,每个三角形的着色,这两种分别叫做顶点着色器(shader)、片元着色器。

总之,2D 或 3D 的图形经过绘制和光栅化就变成了一帧帧的图像显示出来。

变成图像之后其实还可以做一些图像处理,比如灰度、反色、高斯模糊等各种滤镜的实现。

所以,前端的渲染技术的理论基础是计算机图形学 + 图像处理。

不同的渲染技术的区别和联系

具体到前前端的渲染技术来说,html+css、svg、canvas、webgl 都是用于图形和图像渲染的技术,但是它们各有侧重:

html + css

html + css 是用于图文布局的,也就是计算文字、图片、视频等的显示位置。它提供了很多计算规则,比如流式布局很适合做图文排版,弹性布局易于做自适应的布局等。但是它不适合做更灵活的图形绘制,这时就要用其他几种技术了。

canvas

canvas 是给定一块画布区域,在不同的位置画图形和图像,它没有布局规则,所以很灵活,常用来做可视化或者游戏的开发。但是 canvas 并不会保留绘制的图形的信息,生成的图像只能显示在固定的区域,当显示区域变大的时候,它不能跟随一起放缩,就会失真,如果有放缩不失真的需求就要用其他渲染技术了。

svg

svg 会在内存中保留绘制的图形的信息,显示区域变化后会重新计算,是一个矢量图,常用于 icon、字体等的绘制。

webgl

上面的 3 种技术都是用于 2D 的图形图像的绘制,如果想绘制 3D 的内容,就要用 webgl 了。它提供了绘制 3D 图形的 api,比如通过顶点构成 3D 的模型,给每一个面贴图,设置光源,然后光栅化成图像等的 api。它常用于通过 3D 内容增强网站的交互效果,3D 的可视化,3D 游戏等,再就是虚拟现实中的 3D 交互。

所以,虽然前端渲染技术的底层原理都是图形学 + 图像处理,但上层提供的 4 种渲染技术各有侧重点。

不过,它们还是有很多相同的地方的:

  • 位置、大小等的变化都是通过矩阵的计算

  • 都要经过图形转图像,也就是光栅化的过程

  • 都支持对图像做进一步处理,比如各种滤镜

  • html + css 渲染会分不同图层分别做计算,canvas 也会根据计算量分成不同的 canvas 来做计算

因为他们底层的图形学原理还是一致的。

除此以外,3D 内容,也就是 webgl 的内容会通过 GPU 来计算,但 css 其实也可以通过 GPU 计算,这叫做 css 的硬件加速,有四个属性可以触发硬件加速:transform、opacity、filter、will-change。(更多的 GPU 和 css 硬件加速的内容可以看这篇文章:这一次,彻底搞懂 GPU 和 css 硬件加速

编译原理的应用

除了图形学和图像技术外,html+css 还用到了编译技术。因为 html、css 是一种 DSL( domin specific language,领域特定语言),也就是专门为界面描述所设计的语言。用 html 来表达 dom 结构,用 css 来给 dom 添加样式都只需要很少的代码,然后运行时解析 html 和 css 来创建 dom、添加样式。

DSL 可以让特定领域的逻辑更容易表达,前端领域还有一些其他技术也用到了 DSL,比如 graphql。

总结

因为人眼的视觉暂留机制,只要每帧绘制不超过 0.1s,人看到的画面就是连续的,这是显示的原理。每帧的绘制要经过图形绘制和图形转图像的光栅化过程,2D 和 3D 图形分别有不同的绘制和光栅化的算法。此外,转成图像之后还可以做进一步的图像处理。

前端领域的四种渲染技术:html+css、canvas、svg、webgl 各有侧重点,分别用于不同内容的渲染:

  • html+ css 用于布局

  • canvas 用于灵活的图形图像渲染

  • svg 用于矢量图渲染

  • webgl 用于 3D 图形的渲染

但他们的理论基础都是计算机图形学 + 图像处理。(而且,html+css 为了方便逻辑的表达,还设计了 DSL,这用到了编译技术)

这四种渲染技术看似差别很大,但在理论基础层面,很多东西都是一样的。这也是为什么我们要去学计算机基础,因为它可以让我们对技术有一个更深入的更本质的理解。


作者:zxg_神说要有光
来源:https://juejin.cn/post/7041157165024804895

收起阅读 »

如何用 docker 打造前端开发环境

用 docker 做开发环境的好处 保持本机清爽 做开发的都知道,电脑一买回来就要安装各种各样的环境,比如前端开发需要安装 node、yarn、git 等,为了使用某些工具或者包,可能还需要安装 python 或者 java 等(比如 jenkins 就依赖了...
继续阅读 »

用 docker 做开发环境的好处


保持本机清爽


做开发的都知道,电脑一买回来就要安装各种各样的环境,比如前端开发需要安装 node、yarn、git 等,为了使用某些工具或者包,可能还需要安装 python 或者 java 等(比如 jenkins 就依赖了 java),久而久之,本机环境会非常乱,对于一些强迫证患者或者有软件洁癖的人来说多少有点不爽。


使用 docker 后,开发环境都配置在容器中,开发时只需要打开 docker,开发完后关闭 docker,本机不用再安装乱七八糟的环境,非常清爽。


隔离环境


不知道大家在开发时有没有遇到这种情况:公司某些项目需要在较新的 node 版本上运行(比如 vite,需要在 node12 或以上),某些老的项目需要在较老的 node 版本上运行,切换起来比较麻烦,虽然可以用 nvm 来解决,但使用 docker 可以更方便的解决此问题。


快速配置环境


买了新电脑,或者重装了系统,又或者换了新的工作环境,第一件事就是配置开发环境。下载 node、git,然后安装一些 npm 的全局包,然后下载 vscode,配置 vscode,下载插件等等……


使用 docker 后,只需从 docker hub 中拉取事先打包好的开发环境镜像,就可以愉快的进行开发了。


安装 docker


到 docker 官网(http://www.docker.com)下载 docker desktop 并安装,此步比较简单,省略。


安装完成,打开 docker,待其完全启动后,打开 shell 输入:


docker images

截屏2021-09-29 22.16.13.png


显示上述信息即成功!


配置开发环境


假设有一个项目,它必须要运行在 8.14.0 版本的 node 中,我们先去 docker hub 中将这个版本的 node 镜像拉取下来:


docker pull node:8.14.0

拉取完成后,列出镜像列表:


docker images

截屏2021-09-29 23.30.16.png


有了镜像后,就可以使用镜像启动一个容器:


docker run -it --name my_container 3b7ecd51 /bin/bash

上面的命令表示以命令行交互的模式启动一个容器,并将容器的名称指定为 my_container。


截屏2021-10-08 23.16.11.png


此时已经新建并进入到容器,容器就是一个 linux 系统,可以使用 linux 命令,我们尝试输入一些命令:


截屏2021-10-08 23.18.11.png


可以看到这个 node 镜像除了预装了 node 8.14.0,还预装了 git 2.11.0。



镜像和容器的的关系:镜像只预装了最基本的环境,比如上面的 node:8.14.0 镜像可以看成是预装了 node 8.14.0 的 linux 系统,而容器是基于镜像克隆出来的另一个 linux 系统,可以在这个系统中安装其它环境比如 java、python 等,一个镜像可以建立多个容器,每个容器环境都是相互隔离的,互不影响(比如在容器 A 中安装了 java,容器 B 是没有的)。



使用命令行操作项目并不方便,所以我们先退出命令行模式,使用 exit 退出:


截屏2021-10-08 23.20.49.png


借助 IDE 可以更方便的玩 docker,这里我们选择 vscode,打开 vscode,安装 Remote - Containers 扩展,这个扩展可以让我们更方便的管理容器:


截屏2021-10-08 23.03.53.png


安装成功后,左下角会多了一个图标,点击:


截屏2021-10-08 23.23.00.png


在展开菜单中选择“Attach to Running Container”:


截屏2021-10-08 23.25.02.png


此时会报一个错“There are no running containers to attach to.”,因为我们刚刚退出了命令行交互模式,所以现在容器是处理停止状态的,我们可以使用以下命令来查看正在运行的容器:


docker ps

# 或者
docker container ls

截屏2021-10-08 23.30.51.png


发现列表中并没有正在运行的容器,我们需要找到刚刚创建的容器并将其运行起来,先显示所有容器列表:


# -a 可以显示所有容器,包括未运行的
docker ps -a

# 或者
docker container ls -a

截屏2021-10-08 23.29.25.png


运行指定容器:


# 使用容器名称
docker start my_container

# 或者使用容器 id,id 只需输入前几位,docker 会自动识别
docker start 8ceb4

再次运行 docker ps 命令后,就可以看到已运行的容器了。然后回到 vscode,再次选择"Attach to Running Container",就会出现正在运行的容器列表:


截屏2021-10-08 23.36.14.png


选择容器进入,添加一个 bash 终端,就可以进入我们刚刚的命令行模式:


截屏2021-10-08 23.40.14.png


我们安装 vue-cli,并在 /home 目录下创建一个项目:


# 安装 vue-cli
npm install -g @vue/cli

# 进入到 home 目录
cd /home

# 创建 vue 项目
vue create demo

在 vscode 中打开目录,发现打开的不再是本机的目录,而是容器中的目录,找到我们刚刚创建的 /home/demo 打开:


截屏2021-10-09 00.01.13.png


输入 npm run serve,就可以愉快的进行开发啦:


截屏2021-10-09 00.03.32.png


上面我们以 node 8.14.0 镜像为例创建了一个开发环境,如果想使用新版的 node 也是一样的,只需要将指定版本的 node 镜像 pull 下来,然后使用这个镜像创建一个容器,并在容器中创建项目或者从 git 仓库中拉取项目进行开发,这样就有了两个不同版本的 node 开发环境,并且可以同时进行开发。


使用 ubuntu 配置开发环境


上面这种方式使用起来其实并不方便,因为 node 镜像只安装了 node 和 git,有时我们希望镜像可以内置更多功能(比如预装 nrm、vue-cli 等 npm 全局包,或者预装好 vscode 的扩展等),这样用镜像新建的容器也包含这些功能,不需要每个容器都要安装一次。


我们可以使用 ubuntu 作为基础自由配置开发环境,首先获取 ubuntu 镜像:


# 不输入版本号,默认获取 latest 即最新版
docker pull ubuntu

新建一个容器:


docker run -itd --name fed 597ce /bin/bash

这里的 -itd 其实是 -i -t -d 的合写,-d 是在后台中运行容器,相当于新建时一并启动容器,这样就不用使用 docker start 命令了。后面我们直接用 vscode 操作容器,所以也不需要使用命令行模式了。


我们将容器命名为 fed(表示 front end development),建议容器的名称简短一些,方便输入。


截屏2021-10-10 00.11.40.png


ubuntu 镜像非常纯净(只有 72m),只具备最基本的能力,为了后续方便使用,我们需要更新一下系统,更新前为了速度快一点,先换到阿里的源,用 vscode 打开 fed 容器,然后打开 /etc/apt/sources.list 文件,将内容改为:


deb http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse

截屏2021-10-10 00.18.57.png


在下面的终端中依次输入以下命令更新系统:


apt-get update
apt-get upgrade

安装 sudo:


apt-get install sudo

安装 git:


apt-get install git

安装 wget(wget 是一个下载工具,我们需要用它来下载软件包,当然也可以选择 axel,看个人喜好):


apt-get install wget

为了方便管理项目与软件包,我们在 /home 目录中创建两个文件夹(projects 与 packages),projects 用于存放项目,packages 用于存放软件包:


cd /home
mkdir projects
mkdir packages

由于 ubuntu 源中的 node 版本比较旧,所以从官网中下载最新版,使用 wget 下载 node 软件包:


# 将 node 放到 /home/packages 中
cd /home/packages

# 需要下载其它版本修改版本号即可
wget https://nodejs.org/dist/v14.18.0/node-v14.18.0-linux-x64.tar

解压文件:


tar -xvf node-v14.18.0-linux-x64.tar

# 删除安装包
rm node-v14.18.0-linux-x64.tar

# 改个名字,方便以后切换 node 版本
mv node-v14.18.0-linux-x64 node

配置 node 环境变量:


# 修改 profile 文件
echo "export PATH=/home/packages/node/bin:$PATH" >> /etc/profile

# 编译 profile 文件,使其生效
source /etc/profile

# 修改 ~.bashrc,系统启动时编译 profile
echo "source /etc/profile" >> ~/.bashrc

# 之后就可以使用 node 和 npm 命令了
node -v
npm -v

安装 nrm,并切换到 taobao 源:


npm install nrm -g
nrm use taobao

安装一些 vscode 扩展,比如 eslint、vetur 等,扩展是安装在容器中的,在容器中会保留一份配置文件,到时打包镜像会一并打包进去。当我们关闭容器后再打开 vscode,可以发现本机的 vscode 中并没有安装这些扩展。


至此一个简单的前端开发环境已经配置完毕,可以根据自己的喜好自行添加一些包,比如 yarn、nginx、vim 等。


打包镜像


上面我们通过 ubuntu 配置了一个简单的开发环境,为了复用这个环境,我们需要将其打包成镜像并推送到 docker hub 中。


第一步:先到 docker 中注册账号。


第二步:打开 shell,登录 docker。


截屏2021-10-10 01.44.26.png


第三步:将容器打包成镜像。


# commit [容器名称] [镜像名称]
docker container commit fed fed

第四步:为镜像打 tag,因为镜像推送到 docker hub 中,要用 tag 来区分版本,这里我们先设置为 latest。tag 名称加上了用户名做命名空间,防止与 docker hub 上的镜像冲突。


docker tag fed huangzhaoping/fed:latest

第五步:将 tag 推送至 docker hub。


docker push huangzhaoping/fed:latest

第六步:将本地所有关于 fed 的镜像和容器删除,然后从 docker hub 中拉取刚刚推送的镜像:


# 拉取
docker pull huangzhaoping/fed

# 创建容器
docker run -itd --name fed huangzhaoping/fed /bin/bash

用 vscode 打开容器,打开命令行,输入:


node -v
npm -v
nrm -V
git --version

然后再看看 vscode 扩展,可以发现扩展都已经安装好了。


如果要切换 node 版本,只需要下载指定版本的 node,解压替换掉 /home/packages/node 即可。


至此一个 docker 开发环境的镜像就配置完毕,可以在不同电脑,不同系统中共享这个镜像,以达到快速配置开发环境的目的。


注意事项



  • 如果要将镜像推送到 docker hub,不要将重要的信息保存到镜像中,因为镜像是共享的,避免重要信息泄露。

  • 千万不要在容器中存任何重要的文件或信息,因为容器一旦误删这些文件也就没了。

  • 如果在容器中开发项目,记得每天提交代码到远程仓库,避免容器误删后代码丢失。

作者:Rise_
链接:https://juejin.cn/post/7017129520649994253

收起阅读 »

【手写代码】面试官:请你手写防抖和节流

一、前言当用户高频触发某一事件时,如窗口的resize、scroll,输入框内容校验等,此时这些事件调用函数的频率如果没有限制,可能会导致响应跟不上触发,出现页面卡顿,假死现象。此时,我们可以采用 防抖(debounce) 和 节...
继续阅读 »

一、前言

当用户高频触发某一事件时,如窗口的resize、scroll,输入框内容校验等,此时这些事件调用函数的频率如果没有限制,可能会导致响应跟不上触发,出现页面卡顿,假死现象。此时,我们可以采用 防抖(debounce) 和 节流(throttle) 的方式来减少调用频率,同时又不影响实际效果。

二、防抖

假设你用手压住一个弹簧,那么弹簧不会弹起来,除非你松手。

函数防抖,就是指触发事件后,函数在 n 秒后只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数的执行时间。

简单的说,当一个函数连续触发,只执行最后一次。

函数防抖一般用在什么情况之下呢?一般用在,连续的事件只需触发一次回调的场合。具体有:

  1. 搜索框搜索输入。只需用户最后一次输入完,再发送请求;
  2. 用户名、手机号、邮箱输入验证;
  3. 浏览器窗口大小改变后,只需窗口调整完后,再执行resize事件中的代码,防止重复渲染。

代码实现

在下面这段代码中,我们实现了最简单的一个防抖函数,我们设置一个定时器,你重复调用一次函数,我们就清除定时器,重新定时,直到在设定的时间段内没有重复调用函数。

// fn是你要调用的函数,delay是防抖的时间
function debounce(fn, delay) {
// timer是一个定时器
let timer = null;
// 返回一个闭包函数,用闭包保存timer确保其不会销毁,重复点击会清理上一次的定时器
return function () {
// 调用一次就清除上一次的定时器
clearTimeout(timer);
// 开启这一次的定时器
timer = setTimeout(() => {
fn();
}, delay)
}
}

代码优化

仔细一想,上面的代码是不是有什么问题?

问题一: 我们返回的fn函数,如果需要事件参数e怎么办?事件参数被debounce函数保存着,如果不把事件参数给闭包函数,若fn函数需要e我们没给,代码毫无疑问会报错。

问题二: 我们怎么确保调用fn函数的对象是我们想要的对象?你发现了吗,在上面这段代码中fn()函数的调用者是fn所定义的环境,这里涉及this指向问题,想要了解为什么可以去了解下js中的this。

为了解决上述两个问题,我们对代码优化如下

// fn是你要调用的函数,delay是防抖的时间
function debounce(fn, delay) {
// timer是一个定时器
let timer = null;
// 返回一个闭包函数,用闭包保存timer确保其不会销毁,重复点击会清理上一次的定时器
return function () {
// 保存事件参数,防止fn函数需要事件参数里的数据
let arg = arguments;
// 调用一次就清除上一次的定时器
clearTimeout(timer);
// 开启这一次的定时器
timer = setTimeout(() => {
// 若不改变this指向,则会指向fn定义环境
fn.apply(this, arg);
}, delay)
}
}

三、节流

当水龙头的水一直往下流,这十分的浪费水,所以我们可以把龙头关小一点,让水一滴一滴往下流,每隔一段时间掉下来一滴水。

节流就是限制一个函数在一段时间内只能执行一次,过了这段时间,在下一段时间又可以执行一次。应用场景如:

  1. 输入框的联想,可以限定用户在输入时,只在每两秒钟响应一次联想。
  2. 搜索框输入查询,如果用户一直在输入中,没有必要不停地调用去请求服务端接口,等用户停止输入的时候,再调用,设置一个合适的时间间隔,有效减轻服务端压力。
  3. 表单验证
  4. 按钮提交事件。

代码实现1(时间戳版)

// 方法一:时间戳
function throttle(fn, delay = 1000) {
// 记录第一次的调用时间
var prev = null;
console.log(prev);
// 返回闭包函数
return function () {
// 保存事件参数
var args = arguments;
// 记录现在调用的时间
var now = Date.now();
// console.log(now);
// 如果间隔时间大于等于设置的节流时间
if (now - prev >= delay) {
// 执行函数
fn.apply(this, args);
// 将现在的时间设置为上一次执行时间
prev = now;
}
}
}

触发事件时立即执行,以后每过delay秒之后才执行一次,并且最后一次触发事件若不满足要求不会被执行

代码实现2(定时器版)

// 方法二:定时器
function throttle(fn, delay) {
// 重置定时器
let timer = null;
// 返回闭包函数
return function () {
// 记录事件参数
let args = arguments;
// 如果定时器为空
if (!timer) {
// 开启定时器
timer = setTimeout(() => {
// 执行函数
fn.apply(this, args);
// 函数执行完毕后重置定时器
timer = null;
}, delay);
}
}
}

第一次触发时不会执行,而是在delay秒之后才执行,当最后一次停止触发后,还会再执行一次函数。

代码实现3(时间戳 & 定时器)

// 方法三:时间戳 & 定时器
function throttle(fn, delay) {
// 初始化定时器
let timer = null;
// 上一次调用时间
let prev = null;
// 返回闭包函数
return function () {
// 现在触发事件时间
let now = Date.now();
// 触发间隔是否大于delay
let remaining = delay - (now - prev);
// 保存事件参数
const args = arguments;
// 清除定时器
clearTimeout(timer);
// 如果间隔时间满足delay
if (remaining <= 0) {
// 调用fn,并且将现在的时间设置为上一次执行时间
fn.apply(this, args);
prev = Date.now();
} else {
// 否则,过了剩余时间执行最后一次fn
timer = setTimeout(() => {
fn.apply(this, args)
}, delay);
}
}
}


原文链接:https://juejin.cn/post/7040633388625035272


收起阅读 »

vue工程师必须学会封装的埋点指令思路

vue
前言 最近项目中需要做埋点功能,梳理下产品的埋点文档,发现点击埋点的场景比较多。因为使用的是阿里云sls日志服务去埋点,所以通过使用手动侵入代码式的埋点。定好埋点的形式后,技术实现方法也有很多,哪种比较好呢? 稍加思考... 决定封装个埋点指令,这样使用起来...
继续阅读 »

前言


最近项目中需要做埋点功能,梳理下产品的埋点文档,发现点击埋点的场景比较多。因为使用的是阿里云sls日志服务去埋点,所以通过使用手动侵入代码式的埋点。定好埋点的形式后,技术实现方法也有很多,哪种比较好呢?


稍加思考...



决定封装个埋点指令,这样使用起来会比较方便,因为指令的颗粒度比较细能够直击要害,挺适合上面所说的业务场景。


指令基础知识


在此之前,先来复习下vue自定义指令吧,这里只介绍常用的基础知识。更全的介绍可以查看官方文档


钩子函数




  • bind:只调用一次,指令第一次绑定到元素时调用。




  • inserted:被绑定元素插入父节点时调用。




  • update:所在组件的 VNode 更新时调用。




钩子函数参数



  • el:指令所绑定的DOM元素。

  • binding:一个对象,包含以下 property:

    • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2。

    • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"。



  • vnode:指令所绑定的当前组件vnode。


在这里分享个小技巧,钩子函数参数中没有可以直接获取当前实例的参数,但可以通过 vnode.context 获取到,这个在我之前的vue技巧文章中也有分享到,有兴趣可以去看看。


正文


进入正题,下面会介绍埋点指令的使用,内部是怎么实现的。


用法与思路


一般我在封装一个东西时,会先确定好它该怎么去用,然后再从用法入手去封装。这样会令整个思路更加清晰,在定义用法时也可以思考下易用性,不至于封装完之后因为用法不理想而返工。


埋点上报的数据会分为公共数据(每个埋点都要上报的数据)和自定义数据(可选的额外数据,和公共数据一起上报)。那么公共数据在内部就进行统一处理,对于自定义数据则需要从外部传入。于是有了以下两种用法:



  • 一般用法


<div v-track:clickBtn></div>


  • 自定义数据


<div v-track:clickBtn="{other:'xxx'}"></div>

可以看到埋点事件是通过 arg 的形式传入,在此之前也看到有些小伙伴封装的埋点事件是在 value 传入。但我个人比较喜欢 arg 的形式,这种更能让人一目了然对应的埋点事件是什么。


另外上报数据结构大致为:


{   
eventName: 'clickBtn'
userId: 1,
userName: 'xxx',
data: {
other: 'xxx'
}
}

eventName 是埋点对应的事件名,与之同级的是公共数据,而自定义数据放在 data 内。


实现


定义一个 track.js 的文件


import SlsWebLogger from 'js-sls-logger'

function getSlsWebLoggerInstance (options = {}) {
return new SlsWebLogger({
host: '***',
project: '***',
logstore: `***`,
time: 10,
count: 10,
...options
})
}

export default {
install (Vue, {baseData = {}, slsOptions = {}) {
const slsWebLogger = getSlsWebLoggerInstance(slsOptions)
// 获取公共数据的方法
let getBaseTrackData = typeof baseData === 'function' ? baseData : () => baseData
let baseTrackData = null
const Track = {
name: 'track',
inserted (el, binding) {
el.addEventListener('click', () => {
if (!binding.arg) {
console.error('Track slsWebLogger 事件名无效')
return
}
if (!baseTrackData) {
baseTrackData = getBaseTrackData()
}
baseTrackData.eventName = binding.arg
// 自定义数据
let trackData = binding.value || {}
const submitData = Object.assign({}, baseTrackData, {data: trackData})
// 上报
slsWebLogger.send(submitData)
if (process.env.NODE_ENV === 'development') {
console.log('Track slsWebLogger', submitData)
}
})
}
}
Vue.directive(Track.name, Track)
}
}

封装比较简单,主要做了两件事,首先是为绑定指令的 DOM 添加 click 事件,其次处理上报数据。在封装埋点指令时,公共数据通过baseData传入,这样可以增加通用性,第二个参数是上报平台的一些配置参数。


在初始化时注册指令:


import store from 'src/store'
import track from 'Lib/directive/track'

function getBaseTrackData () {
let userInfo = store.state.User.user_info
// 公共数据
const baseTrackData = {
userId: userInfo.user_id, // 用户id
userName: userInfo.user_name // 用户名
}
return baseTrackData
}

Vue.use(track, {baseData: getBaseTrackData})

Vue.use 时会自动寻找 install 函数进行调用,最终在全局注册指令。


加点通用性


除了点击埋点之外,如果有停留埋点等场景,上面的指令就不适用了。为此,可以增加手动调用的形式。


export default {
install (Vue, {baseData = {}, slsOptions = {}) {
// ...
Vue.directive(Track.name, Track)
// 手动调用
Vue.prototype.slsWebLogger = {
send (trackData) {
if (!trackData.eventName) {
console.error('Track slsWebLogger 事件名无效')
return
}
const submitData = Object.assign({}, getBaseTrackData(), trackData)
slsWebLogger.send(submitData)
if (process.env.NODE_ENV === 'development') {
console.log('Track slsWebLogger', submitData)
}
}
}
}

这种挂载到原型的方式可以在每个组件实例上通过 this 方便进行调用。


export default {
// ...
created () {
this.slsWebLogger.send({
//...
})
}
}

总结


本文分享了封装埋点指令的过程,封装并不难实现。主要有两种形式,点击埋点通过绑定 DOM click 事件监听点击上报,而其他场景下提供手动调用的方式。主要是想记录下封装的思路,以及使用方式。埋点实现也是根据业务做了一些调整,比如注册埋点指令可以接受上报平台的配置参数。毕竟人是活的,代码是死的。只要能满足业务需求并且能维护,怎么使用舒服怎么来嘛。


作者:出来吧皮卡丘
链接:https://juejin.cn/post/7040649951923142687

收起阅读 »