注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Vue + qiankun 快速实现前端微服务

什么是微前端Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. -...
继续阅读 »

什么是微前端

Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. -- Micro Frontends

微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。

qiankun

qiankun 是蚂蚁金服开源的一套完整的微前端解决方案。具体描述可查看 文档 和 Github

下面将通过一个微服务Demo 介绍 Vue 项目如何接入 qiankun,代码地址:micro-front-vue)

二、配置主应用

  1. 使用 vue cli 快速创建主应用;
  2. 安装 qiankun
$ yarn add qiankun # 或者 npm i qiankun -S
  1. 调整主应用 main.js 文件:具体如下:
import Vue from "vue"
import App from "./App.vue"
import router from "./router"

import { registerMicroApps, setDefaultMountApp, start } from "qiankun"
Vue.config.productionTip = false
let app = null;
/**
* 渲染函数
* appContent 子应用html内容
* loading 子应用加载效果,可选
*/
function render({ appContent, loading } = {}) {
if (!app) {
app = new Vue({
el: "#container",
router,
data() {
return {
content: appContent,
loading
};
},
render(h) {
return h(App, {
props: {
content: this.content,
loading: this.loading
}
});
}
});
} else {
app.content = appContent;
app.loading = loading;
}
}

/**
* 路由监听
* @param {*} routerPrefix 前缀
*/
function genActiveRule(routerPrefix) {
return location => location.pathname.startsWith(routerPrefix);
}

function initApp() {
render({ appContent: '', loading: true });
}

initApp();

// 传入子应用的数据
let msg = {
data: {
auth: false
},
fns: [
{
name: "_LOGIN",
_LOGIN(data) {
console.log(`父应用返回信息${data}`);
}
}
]
};
// 注册子应用
registerMicroApps(
[
{
name: "sub-app-1",
entry: "//localhost:8091",
render,
activeRule: genActiveRule("/app1"),
props: msg
},
{
name: "sub-app-2",
entry: "//localhost:8092",
render,
activeRule: genActiveRule("/app2"),
}
],
{
beforeLoad: [
app => {
console.log("before load", app);
}
], // 挂载前回调
beforeMount: [
app => {
console.log("before mount", app);
}
], // 挂载后回调
afterUnmount: [
app => {
console.log("after unload", app);
}
] // 卸载后回调
}
);

// 设置默认子应用,与 genActiveRule中的参数保持一致
setDefaultMountApp("/app1");

// 启动
start();
  1. 修改主应用 index.html 中绑定的 id ,需与 el  绑定 dom 为一致;
  2. 调整 App.vue 文件,增加渲染子应用的盒子:
<template>
<div id="main-root">
<!-- loading -->
<div v-if="loading">loading</div>
<!-- 子应用盒子 -->
<div id="root-view" class="app-view-box" v-html="content"></div>
</div>
</template>

<script>
export default {
name: "App",
props: {
loading: Boolean,
content: String
}
};
</script>
  1. 创建 vue.config.js 文件,设置 port :
module.exports = {
devServer: {
port: 8090
}
}

三、配置子应用

  1. 在主应用同一级目录下快速创建子应用,子应用无需安装 qiankun
  2. 配置子应用 main.js:
import Vue from 'vue';
import VueRouter from 'vue-router';
import App from './App.vue';
import routes from './router';
import './public-path';

Vue.config.productionTip = false;

let router = null;
let instance = null;

function render() {
router = new VueRouter({
base: window.__POWERED_BY_QIANKUN__ ? '/app1' : '/',
mode: 'history',
routes,
});

instance = new Vue({
router,
render: h => h(App),
}).$mount('#app');
}

if (!window.__POWERED_BY_QIANKUN__) {
render();
}

export async function bootstrap() {
console.log('vue app bootstraped');
}

export async function mount(props) {
console.log('props from main app', props);
render();
}

export async function unmount() {
instance.$destroy();
instance = null;
router = null;
}
  1. 配置 vue.config.js
const path = require('path');
const { name } = require('./package');

function resolve(dir) {
return path.join(__dirname, dir);
}

const port = 8091; // dev port

module.exports = {
/**
* You will need to set publicPath if you plan to deploy your site under a sub path,
* for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/,
* then publicPath should be set to "/bar/".
* In most cases please use '/' !!!
* Detail: https://cli.vuejs.org/config/#publicpath
*/
outputDir: 'dist',
assetsDir: 'static',
filenameHashing: true,
// tweak internal webpack configuration.
// see https://github.com/vuejs/vue-cli/blob/dev/docs/webpack.md
devServer: {
// host: '0.0.0.0',
hot: true,
disableHostCheck: true,
port,
overlay: {
warnings: false,
errors: true,
},
headers: {
'Access-Control-Allow-Origin': '*',
},
},
// 自定义webpack配置
configureWebpack: {
resolve: {
alias: {
'@': resolve('src'),
},
},
output: {
// 把子应用打包成 umd 库格式
library: `${name}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${name}`,
},
},
};

其中有个需要注意的点:

  1. 子应用必须支持跨域:由于 qiankun 是通过 fetch 去获取子应用的引入的静态资源的,所以必须要求这些静态资源支持跨域;
  2. 使用 webpack 静态 publicPath 配置:可以通过两种方式设置,一种是直接在 mian.js 中引入 public-path.js 文件,一种是在开发环境直接修改 vue.config.js:
{
output: {
publicPath: `//localhost:${port}`;
}
}

public-path.js 内容如下:

if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
至此,Vue 项目的前端微服务已经简单完成了。

但是在实际的开发过程中,并非如此简单,同时还存在应用间跳转、应用间通信等问题。


原文:https://segmentfault.com/a/1190000021872481


收起阅读 »

使用自定义url发图片的坑

发送URL图片消息 App端需要开发者自己实现下载,Web端需要在 WebIMConfig.js中 设置 useOwnUploadFun: true。实际上还得在WEBIM里面再配置一下WebIM.conn = new WebIM.connect...
继续阅读 »


发送URL图片消息





App端需要开发者自己实现下载,Web端需要在 WebIMConfig.js中 设置 useOwnUploadFun: true

实际上还得在WEBIM里面再配置一下

WebIM.conn = new WebIM.connection({
appKey: WebIM.config.appkey,
isMultiLoginSessions: WebIM.config.isMultiLoginSessions,
https: typeof WebIM.config.https === "boolean" ? WebIM.config.https : location.protocol === "https:",
url: WebIM.config.xmppURL,
apiUrl: WebIM.config.apiURL,
isAutoLogin: false,
heartBeatWait: WebIM.config.heartBeatWait,
autoReconnectNumMax: WebIM.config.autoReconnectNumMax,
autoReconnectInterval: WebIM.config.autoReconnectInterval,
useOwnUploadFun: WebIM.config.useOwnUploadFun,
isDebug: false,
isHttpDNS:false
});







单聊通过URL发送图片消息的代码示例如下:


// 单聊通过URL发送图片消息
var sendPrivateUrlImg = function () {
var id = conn.getUniqueId(); // 生成本地消息id
var msg = new WebIM.message('img', id); // 创建图片消息
var option = {
body: {
type: 'file',
url: url,
size: {
width: msg.width,
height: msg.height,
},
length: msg.length,
filename: msg.file.filename,
filetype: msg.filetype
},
to: 'username', // 接收消息对象
};
msg.set(option);
conn.send(msg.body);
}
收起阅读 »

微前端框架 qiankun 技术分析

如何加载子应用single-spa 通过 js entry 的形式来加载子应用。而 qiankun 采用了 html entry 的形式。这两种方式的优缺点我们在理解微前端技术原理中已经做过分析,这里不再赘述,我们看看 qiankun 是如何实现 html e...
继续阅读 »

如何加载子应用

single-spa 通过 js entry 的形式来加载子应用。而 qiankun 采用了 html entry 的形式。这两种方式的优缺点我们在理解微前端技术原理中已经做过分析,这里不再赘述,我们看看 qiankun 是如何实现 html entry 的。

qiankun 提供了一个 API registerMicroApps 来注册子应用,其内部调用 single-spa 提供的 registerApplication 方法。在调用 registerApplication 之前,会调用内部的 loadApp 方法来加载子应用的资源,初始化子应用的配置。

通过阅读 loadApp 的代码,我们发现,qiankun 通过 import-html-entry 这个包来加载子应用。import-html-entry 的作用就是通过解析子应用的入口 html 文件,来获取子应用的 html 模板、css 样式和入口 JS 导出的生命周期函数。

import-html-entry

import-html-entry 是这样工作的,假设我们有如下 html entry 文件:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>test</title>
</head>
<body>

<!-- mark the entry script with entry attribute -->
<script src="https://unpkg.com/mobx@5.0.3/lib/mobx.umd.js" entry></script>
<script src="https://unpkg.com/react@16.4.2/umd/react.production.min.js"></script>
</body>
</html>

我们使用 import-html-entry 来解析这个 html 文件:

import importHTML from 'import-html-entry';

importHTML('./subApp/index.html')
.then(res => {
console.log(res.template);

res.execScripts().then(exports => {
const mobx = exports;
const { observable } = mobx;
observable({
name: 'kuitos'
})
})
});

importHTML 的返回值有如下几个属性:

  • template 处理后的 HTML 模板
  • assetPublicPath 静态资源的公共路径
  • getExternalScripts 获取所有外部脚本的函数,返回脚本路径
  • getExternalStyleSheets 获取所有外部样式的函数,返回样式文件的路径
  • execScripts 执行脚本的函数

在 importHTML 的返回值中,除了几个工具类的方法,最重要的就是 template 和 execScripts 了。

importHTML('./subApp/index.html') 的整个执行过程代码比较长,我们只讲一下大概的执行原理,感兴趣的同学可以自行查看importHTML 的源码

importHTML 首先会通过 fetch 函数请求具体的 html 内容,然后在 processTpl 函数 中通过一系列复杂的正则匹配,解析出 html 中的样式文件和 js 文件。

importHTML 函数返回值为 { template, scripts, entry, styles },分别是 html 模板,html 中的 js 文件(包含内嵌的代码和通过链接加载的代码),子应用的入口文件,html 中的样式文件(同样是包含内嵌的代码和通过链接加载的代码)。

之后通过 getEmbedHTML 函数 将所有使用外部链接加载的样式全部转化成内嵌到 html 中的样式。getEmbedHTML 返回的 html 就是 importHTML 函数最终返回的 template 内容。

现在,我们看看 execScripts 是怎么实现的。

execScripts 内部会调用 getExternalScripts 加载所有 js 代码的文本内容,然后通过 eval("code") 的形式执行加载的代码。

注意,execScripts 的函数签名是这样的 (sandbox?: object, strictGlobal?: boolean, execScriptsHooks?: ExecScriptsHooks): Promise<unknown>。允许我们传入一个沙箱对象,如果子应用按照微前端的规范打包,那么会在全局对象上设置 mountunmount 这几个生命周期函数属性。execScripts 在执行 eval("code") 的时候,会巧妙的把我们指定的沙箱最为全局对象包装到 "code" 中,子应用能够运行在沙盒环境中。

在执行完 eval("code") 以后,就可以从沙盒对象上获取子应用导出的生命周期函数了。

loadApp

现在我们把视线拉回 loadApp 中,loadApp 在获取到 templateexecScripts 这些信息以后,会基于 template 生成 render 函数用于渲染子应用的页面。之后会根据需要生成沙盒,并将沙盒对象传给 execScripts 来获取子应用导出的声明周期函数。

之后,在子应用生命周期函数的基础上,构建新的生命周期函数,再调用 single-spa 的 API 启动子应用。

在这些新的生命周期函数中,会在不同时机负责启动沙盒、渲染子应用、清理沙盒等事务。

隔离

在完成子应用的加载以后,作为一个微前端框架,要解决好子应用的隔离问题,主要要解决 JS 隔离和样式隔离这两方面的问题。

JS 隔离

qiankun 为根据浏览器的能力创建两种沙箱,在老旧浏览器中会创建快照模式 的浏览器中创建 VM 模式的沙箱 ProxySandbox

篇幅限制,我们只看 ProxySandbox 的实现,在其构造函数中,我们可以看到具体的逻辑:首先会根据用户指定的全局对象(默认是 window)创建一个 fakeWindow,之后在这个 fakeWindow 上创建一个 proxy 对象,在子应用中,这个 proxy 对象就是全局变量 window

constructor(name: string, globalContext = window) {
const { fakeWindow, propertiesWithGetter } = createFakeWindow(globalContext);
const proxy = new Proxy(fakeWindow, {
set: (target: FakeWindow, p: PropertyKey, value: any): boolean => {},
get: (target: FakeWindow, p: PropertyKey): any => {},
has(target: FakeWindow, p: string | number | symbol): boolean {},

getOwnPropertyDescriptor(target: FakeWindow, p: string | number | symbol): PropertyDescriptor | undefined {},

ownKeys(target: FakeWindow): ArrayLike<string | symbol> {},

defineProperty(target: Window, p: PropertyKey, attributes: PropertyDescriptor): boolean {},

deleteProperty: (target: FakeWindow, p: string | number | symbol): boolean => {},

getPrototypeOf() {
return Reflect.getPrototypeOf(globalContext);
},
});
this.proxy = proxy;
}

其实 qiankun 中的沙箱分两个类型:

  • app 环境沙箱
    app 环境沙箱是指应用初始化过之后,应用会在什么样的上下文环境运行。每个应用的环境沙箱只会初始化一次,因为子应用只会触发一次 bootstrap 。子应用在切换时,实际上切换的是 app 环境沙箱。
  • render 沙箱
    子应用在 app mount 开始前生成好的的沙箱。每次子应用切换过后,render 沙箱都会重现初始化。

上面说的 ProxySandbox 其实是 render 沙箱。至于 app 环境沙箱,qiankun 目前只针对在应用 bootstrap 时动态创建样式链接、脚本链接等副作用打了补丁,保证子应用切换时这些副作用互不干扰。

之所以设计两层沙箱,是为了保证每个子应用切换回来之后,还能运行在应用 bootstrap 之后的环境下。

样式隔离

qiankun 提供了多种样式隔离方式,隔离效果最好的是 shadow dom,但是由于其存在诸多限制,qiankun 官方在将来的版本中将会弃用,转而推行 experimentalStyleIsolation 方案。

我们可以通过下面这段代码看到 experimentalStyleIsolation 方案的基本原理。

const styleNodes = appElement.querySelectorAll('style') || [];
forEach(styleNodes, (stylesheetElement: HTMLStyleElement) => {
css.process(appElement!, stylesheetElement, appInstanceId);
});

css.process 的核心逻辑,就是给读取到的子应用的样式添加带有子应用信息的前缀。效果如下:

/* 假设应用名是 react16 */
.app-main {
font-size: 14px;
}

div[data-qiankun-react16] .app-main {
font-size: 14px;
}

通过上面的隔离方法,基本可以保证子应用间的样式互不影响。

小结

qiankun 在 single-spa 的基础上根据实际的生产实践开发了很多有用的功能,大大降低了微前端的使用成本。

本文仅仅针对如何加载子应用和如何做好子应用间的隔离这两个问题,介绍了 qiankun 的实现。其实,在隔离这个问题上,qiankun 也仅仅是根据实际中会遇到的情况做了必要的隔离措施,并没有像 iframe 那样实现完全的隔离。我们可以说 qiankun 实现的隔离有缺陷,也可以说是 qiankun 在实际的业务需求和完全隔离的实现成本之间做的取舍。

原文:https://segmentfault.com/a/1190000041151414

收起阅读 »

pc端微信授权登录两种实现方式的总结

在开发pc端项目中,使用微信授权登录是种很常用的功能,目前在功能实现上有两种不同的方式,现根据两种方式做如下总结。一、跳转微信授权登录页面进行扫码授权这种方法实现非常简单只用跳转链接就可以实现微信授权登录window.location = https://op...
继续阅读 »

在开发pc端项目中,使用微信授权登录是种很常用的功能,目前在功能实现上有两种不同的方式,现根据两种方式做如下总结。

一、跳转微信授权登录页面进行扫码授权

这种方法实现非常简单只用跳转链接就可以实现微信授权登录

window.location = https://open.weixin.qq.com/connect/qrconnect?appid=${appid}&redirect_uri=${回调域名}/login&response_type=code&scope=snsapi_login&state=${自定义配置}#wechat_redirect

跳转之后进行微信扫码,之后微信会带着code,回调回你设置的回调域名,这之后拿到code再和后台进行交互,即可实现微信登陆。
这种方法相对来说实现起来非常简单,但是因为需要先跳转微信授权登录页面,在体验上来说可能不是太好。

二、在当前页面生成微信授权登录二维码

这种方法是需要引入wxLogin.js,动态生成微信登陆二维码,具体实现方法如下:

const s = document.createElement('script')
s.type = 'text/javascript'
s.src = 'https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js'
const wxElement = document.body.appendChild(s)
wxElement.onload = function () {
var obj = new WxLogin({
id: 'wx_login_id', // 需要显示的容器id
appid: '', // 公众号appid
scope: 'snsapi_login', // 网页默认即可
redirect_uri:'', // 授权成功后回调的url
state: '', // 可设置为简单的随机数加session用来校验
style: 'black', // 提供"black"、"white"可选。二维码的样式
href: '' // 外部css(查看二维码的dom结构,根据类名进行样式覆盖)文件url,需要https
})
}

其中href参数项还可以通过node将css文件转换为data-url,实现方式如下:

var fs = require('fs');
function base64_encode(file) {
var bitmap = fs.readFileSync(file);
return 'data:text/css;base64,'+new Buffer(bitmap).toString('base64');
}
console.log(base64_encode('./qrcode.css'))

在终端对该js文件执行命令:

node qr.js

把打印出来的url粘贴到href即可。
这种实现方法避免了需要跳转新页面进行扫码,二维码的样式也可以进行更多的自定义设置,可能在体验上是更好的选择。

原文:https://segmentfault.com/a/1190000024492932


收起阅读 »

Three.js控制物体显示与隐藏的方法

本文会讲解一下Three.js控制物体显示与隐藏的方法,主要包括以下几种方式:visible属性;layers属性。下面会分别通过简单的例子介绍下上述几个方式的简单使用方法和一些它们之间的区别。如果没有特殊说明,下面的源码以 r105 版本...
继续阅读 »

本文会讲解一下Three.js控制物体显示与隐藏的方法,主要包括以下几种方式:

  1. visible属性;
  2. layers属性。

下面会分别通过简单的例子介绍下上述几个方式的简单使用方法和一些它们之间的区别。如果没有特殊说明,下面的源码以 r105 版本为例:

visible属性

visible 是Object3D的属性。只有当 visible 是 true 的时候,该物体才会被渲染。任何继承 Object3D 的对象都可以通过该属性去控制它的显示与否,比如:MeshGroupSpriteLight等。

举个简单的例子:

// 控制单个物体的显示和隐藏
const geometry = new THREE.PlaneGeometry(1, 1) // 1*1的一个平面
const planeMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 }) // 红色平面
const plane = new THREE.Mesh(geometry, planeMaterial)
plane.visible = false // 不显示单个物体
scene.add(plane)
// 控制一组物体的显示和隐藏
const geometry = new THREE.PlaneGeometry(1, 1)
const planeMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
const plane = new THREE.Mesh(geometry, planeMaterial)
const group = new THREE.Group()
group.add(plane)
group.visible = false // 不显示一组物体
scene.add(group)

通过后面的例子可以看出,当我们想要控制一组物体的显示与隐藏,可以把这些物体放入一个 Group 中,只通过控制 Group 的显示与隐藏即可。

这块的代码逻辑是在WebGLRenderer.js的 projectObject 方法中实现的。

首先,在 render 方法中调用了 projectObject 方法:

this.render = function ( scene, camera ) {
// ...
projectObject( scene, camera, 0, _this.sortObjects );
// ...
}

projectObject 方法的定义如下:

function projectObject( object, camera, groupOrder, sortObjects ) {
if ( object.visible === false ) return; // 注释1:visible属性是false直接返回
// ...
var children = object.children; // 注释2:递归应用在children上

for ( var i = 0, l = children.length; i < l; i ++ ) {

projectObject( children[ i ], camera, groupOrder, sortObjects ); // 注释2:递归应用在children上

}
}

从注释1可以看出,如果 Group 的 visible 是 false,那么就不会在 children 上递归调用,所以就能达到通过 Group 控制一组对象的显示与隐藏的效果。

当 visible 是 false 的时候,Raycaster 的 intersectObject 或者 intersectObjects 也不会把该物体考虑在内。这块的代码逻辑是在 Raycaster.js

intersectObject: function ( object, recursive, optionalTarget ) {
// ...
intersectObject( object, this, intersects, recursive ); // 注释1:调用了公共方法intersectObject
// ...
},

intersectObjects: function ( objects, recursive, optionalTarget ) {
// ...

for ( var i = 0, l = objects.length; i < l; i ++ ) {

intersectObject( objects[ i ], this, intersects, recursive ); // 注释1:循环调用了公共方法intersectObject

}
// ...
}

// 注释1:公共方法intersectObject
function intersectObject( object, raycaster, intersects, recursive ) {

if ( object.visible === false ) return; // 注释1:如果visible是false,直接return

// ...
}

从注释1可以看出,如果 Group 或者单个物体的 visible 是 false ,就不做检测了。

layers属性

Object3D的layers属性 是一个 Layers 对象。任何继承 Object3D 的对象都有这个属性,比如 Camera 。Raycaster 虽然不是继承自 Object3D ,但它同样有 layers 属性(r113版本以上)。

和上面的 visible 属性一样,layers 属性同样可以控制物体的显示与隐藏、Raycaster 的行为。当物体和相机至少有一个同样的层的时候,物体就可见,否则不可见。同样,当物体和 Raycaster 至少有一个同样的层的时候,才会进行是否相交的测试。这里,强调了是至少有一个,是因为 Layers 可以设置多个层。

Layers 一共可以表示 32 个层,0 到 31 层。内部表示为:




Layers 可以设置同时拥有多个层:

  1. 可以通过 Layers 的 enable 和 disable 方法开启和关闭当前层,参数是上面表格中的 0 到 31 。
  2. 可以通过 Layers 的 set 方法 只开启 当前层,参数是上述表格中的 0 到 31
  3. 可以通过 Layers 的 test 的方法判断两个 Layers 对象是否存在 至少一个公共层 。

当开启多个层的时候,其实就是上述表格中的二进制进行 按位或 操作。比如 同时 开启 0231 层,那么内部存储的值就是 10000000000000000000000000000101

layers 属性默认只开启 0 层。

还是上面那个例子,我们看下怎么控制物体的显示和隐藏:

// 控制单个物体的显示和隐藏
const geometry = new THREE.PlaneGeometry(1, 1)
const planeMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
const plane = new THREE.Mesh(geometry, planeMaterial)
plane.layers.set(1) // 设置平面只有第1层,相机默认是在第0层,所以该物体不会显示出来
scene.add(plane)
// 控制一组物体的显示和隐藏
const geometry = new THREE.PlaneGeometry(1, 1)
const planeMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
const plane = new THREE.Mesh(geometry, planeMaterial)
const group = new THREE.Group()
group.layers.set(1) // 注释1: 设置group只有第一层,相机默认是在第0层,但是此时平面物体还是显示出来了?
group.add(plane)
scene.add(group)

设置单个物体的 layer 可以看到物体成功的没有显示出来。但是,当我们给 group 设置 layer 之后,发现 group 的 children(平面物体)还是显示了出来。那么,这是什么原因呢?让我们看下源码,同样还是上面的 projectObject 方法:

function projectObject( object, camera, groupOrder, sortObjects ) {

if ( object.visible === false ) return;

var visible = object.layers.test( camera.layers ); // 注释1:判断物体和相机是否存在一个公共层

if ( visible ) { // 注释1:如果存在,对物体进行下面的处理
// ...
}

var children = object.children; // 注释1:不管该物体是否和相机存在一个公共层,都会对children进行递归

for ( var i = 0, l = children.length; i < l; i ++ ) {

projectObject( children[ i ], camera, groupOrder, sortObjects );

}
}

从上述注释1可以看出,即使该物体和相机不存在公共层,也不影响该物体的 children 显示。这也就解释了上述为什么给 group 设置 layers ,但是平面物体还是能显示出来。从这一点上来看,layers 和 visible 属性在控制物体显示和隐藏的方面是不一样的。

和 visible 属性一样,接下来我们看下 Layers 对 Raycaster 的影响。同样我还是看了 Raycaster.js 文件,但是发现根本就没有 layers 字段。后来,我看了下最新版本 r140 的 Raycaster.js

function intersectObject( object, raycaster, intersects, recursive ) {

if ( object.layers.test( raycaster.layers ) ) { // 注释1:判断物体和Raycaster是否有公共层

object.raycast( raycaster, intersects );

}

if ( recursive === true ) { // 注释1:不管该物体和Raycaster是否有公共层,都不影响children

const children = object.children;

for ( let i = 0, l = children.length; i < l; i ++ ) {

intersectObject( children[ i ], raycaster, intersects, true );

}
}
}

不同于前面,visible 和 layers 都可以用来控制物体的显示与隐藏,visible 和 layers 只有一个可以用来控制 Raycaster 的行为,具体是哪一个生效,可以看下 Three.js的迁移指南

可以看到,从 r114 版本,废除了 visible ,开始使用 layers 控制 Raycaster 的行为:

r113 → r114
Raycaster honors now invisible 3D objects in intersection tests. Use the new property Raycaster.layers for selectively ignoring 3D objects during raycasting.

总结

从上面可以看出,visible 和 layers 在控制物体显示与隐藏、Raycaster 是否进行等方面是存在差异的。

当该物体的 visible 属性为 false 并且 layers 属性测试失败的时候,行为总结如下:


原文链接:https://segmentfault.com/a/1190000041881241

收起阅读 »

qiankun微前端

本文参考: 官网 你可能并不需要微前端什么是微前端?Techniques, strategies and recipes for building a modern web app with multiple teams that can ship fea...
继续阅读 »

本文参考
官网
你可能并不需要微前端

什么是微前端?

Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. -- Micro Frontends 微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。

qiankun是怎么来的?

所有的技术都是为了解决当前的现实问题,然后通过思考和实践创造出来的。微前端本质上是为了解决组织和团队间协作带来的沟通和管理的问题

引用微前端作者的思想:

微前端是康威定律在前端架构上的映射。 康威定律指导思想:既然沟通是大问题,那么就不要沟通就好了

作者认为大型系统都逃不过熵增定律,宇宙的本质,所有的东西都会从有序走向无序。一个东西如果你不去管理,他就会变成一坨垃圾,所以你想要维持一个东西的有序性,就要付出努力去维护他。所以从中找到平衡,qiankun就诞生了。通过分治的手段,让上帝的归上帝,凯撒的归凯撒

什么情况下使用qiankun?

我们在开发中可能会碰到下面的问题

  • 旧的系统不能下,新的需求还在来

  • 公司内部有很多的系统,不同系统间可能需要展示同一个页面

  • 一个系统过于庞大,每个人分别管理一个模块,git分支比较混乱。想要把系统拆分开来

微前端首先解决的,是如何解构巨石应用

核心价值:技术栈无关,应用之间不应该有任何直接或间接的技术栈、依赖、以及实现上的耦合。

作者认为正确的微前端方案的目标应该是

方案上跟使用 iframe 做微前端一样简单,同时又解决了 iframe 带来的各种体验上的问题

qiankun的原理

qiankun 是一个基于 single-spa微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。

qiankun框架内部fetch请求资源,解析出js、css文件和HTML document,插入到主应用指定的容器中(使用HTML Entry接入方式)

  1. 调用import-html-entry模块的importEntry函数,获取到对应子应用的html文件、可执行脚本文件以及publicpath

  2. 调用getDefaultTplWrapper将子应用的html内容用div标签包裹起来

  3. 调用createElement函数生成剔除html、body、head标签后的子应用html内容(通过innerHTML达到过滤效果)

  4. 调用getRender函数得到render函数(所以子应用一定要有render函数)

  5. 调用第4步得到的render,将container内部清空,并将子应用的dom元素渲染到指定的contanter元素上

  6. 调用getAppWrapperGetter函数,生成一个可以获取处理过的子应用dom元素的函数initialAppWrapperGetter,以备后续使用子应用dom元素

  7. 如果sandbox为true,则调用createSandboxContainer函数

  8. 执行execScripts函数,执行子应用脚本

  9. 执行getMicroAppStateActions函数,获取onGlobalStateChange、setGlobalState、offGlobalStateChange,用于主子应用传递信息

  10. 执行parcelConfigGetter函数,包装mount和unmount

上述步骤的源码

qiankun如何实现隔离?

沙箱隔离

qiankun的沙箱有2种 JS沙箱 和 CSS沙箱

JS沙箱

JS沙箱又分为2种,快照沙箱(为了兼容IE)和 代理沙箱

快照沙箱 snapshotSandbox

基于diff实现,用来兼容不支持Proxy的浏览器,只适用单个子应用。会污染全局window

  1. 激活沙箱:将主应用window的信息存到windowSnapshot

  2. 根据

    modifyPropMap

    ,恢复为子应用的window信息

    读取和修改的是window中的数据,windowSnapshot是缓存的数据

  3. 退出沙箱:根据windowSnapshot把window恢复为主应用数据,将windowSnapshot和window进行diff,将变更的值存到modifyPropMap中,然后把window恢复为主应用数据

总结

  • windowSnapshot主应用的window信息

  • modifyPropMap子应用修改的window信息

相对应的源码

代理沙箱

代理沙箱也分为2种,单例和多例,都是由Proxy实现

单例沙箱 legacySandbox

为了兼容性 singular 模式下依旧使用该沙箱,等新沙箱稳定之后再切换。 创建 addedPropsMapInSandbox(沙箱期间新增的全局变量)、modifiedPropsOriginalValueMapInSandbox(沙箱期间更新的全局变量)、currentUpdatedPropsValueMap(持续记录更新的(新增和修改的)全局变量的 map 用于在任意时刻做 snapshot) 三个变量,前两个用来恢复主应用window,最后一个用来恢复子应用window。同样会污染window,但性能比快照沙箱稍好,不用遍历window

  1. 激活沙箱:根据currentUpdatedPropsValueMap还原子应用的window数据

  2. window只要变动,在

    currentUpdatedPropsValueMap

    中进行记录

    1. 判断addedPropsMapInSandbox中是否有对应 key 的记录,没有新增一条,有的话往下执行

    2. 判断modifiedPropsOriginalValueMapInSandbox中是否有对应 key 的记录,没有的话,记录从window中对应key/value,有的话继续往下执行

    3. 修改window对应的key/value

  3. 退出沙箱:根据addedPropsMapInSandboxmodifiedPropsOriginalValueMapInSandbox还原主应用的window信息

相对性的源码

多例沙箱 proxySandbox

主应用和子应用的window独立,不再共同维护一份window,终于JS沙箱也和qiankun微前端的思想统一了…实行了分治。不会污染全局window,支持多个子应用。

  1. 激活沙箱

  2. 取值,先从自己命名空间下的fakeWindow找key,没找到,找window

  3. 赋值,直接给自己命名空间下的fakeWindow赋值

  4. 退出沙箱

相对应的源码

CSS沙箱

严格沙箱 和 实验性沙箱

严格沙箱

在加载子应用时,添加strictStyleIsolation: true属性,会将整个子应用放到Shadow DOM内进行嵌入,完全隔离了主子应用


缺点:子应用中应用的一些弹框组件会因为找不到body而丢失

实验性沙箱

在加载子应用时,添加experimentalStyleIsolation: true属性,实现形式类似于vue中style标签中的scoped属性,qiankun会自动为子应用所有的样式增加后缀标签,如:div[data-qiankun=“xxx”],这里的XXX为注册子应用时name的值


缺点:子应用中应用的一些弹框组件会因为插入到了主应用到body而丢失样式

相对应的源码


作者:丙乙
来源:https://juejin.cn/post/7100825726424711204

收起阅读 »

v-for中diff算法

当没有key时获取新旧数组长度,取最短的数组(Math.min())进行比较,如果用长的数组进行比较,会发生越界错误以短数组进行for循环,从新旧数组各组一个值进行patch,如果内容一样就不进行更新,如果内容不一样,Vue源码会进行更深层次的比较,如果类型都...
继续阅读 »

当没有key时


获取新旧数组长度,取最短的数组(Math.min())进行比较,如果用长的数组进行比较,会发生越界错误


以短数组进行for循环,从新旧数组各组一个值进行patch,如果内容一样就不进行更新,如果内容不一样,Vue源码会进行更深层次的比较,如果类型都不一样的话,直接创建一个新类型,如果类型一样,值不同,就只更新值,效率会更高,当for循环完毕,新旧数组长度会进行比较,如果旧的长度大有新的长度,就会执行unmountChildren,删除多余的节点,如果新的长度大于旧的长度,就会执行mountChildren,创建新的节点

当有key时


第一步,从头部开始遍历


通过isSameVNodeType进行比较


如果type 和 key 都一样,继续遍历,如果不同,跳出循环,进入第二步

第二步,从尾部开始遍历


和第一步操作一致

如果不同,跳出循环进入第三步

第三步,果旧节点遍历完,依然有新的节点,就是添加节点操作,用一个null和新节点进行patch,n1为空值时,是添加



如果新节点遍历完了,旧节点还有就进入第四步

第四步,新节点遍历完毕,旧节点还有,就进行删除操作


第五步,如果是一个无序的节点,vue会从旧的节点里找到新的节点里相同的值并创建一个新的数组,根据key建立一个索引,找到了就放入新数组里,比较完之后,有多余的旧节点就删除,有没有比较过的新节点就添加


作者:啊哈呀呀呀呀
来源:juejin.cn/post/7100858461520560135

收起阅读 »

IP属地获取,前端获取用户位置信息

尝试获取用户的位置信息写在前面想要像一些平台那样显示用户的位置信息,例如某省市那样。那么这是如何做到的, 据说这个位置信息的准确性在通信网络运营商那里?先不管,先实践尝试下能不能获取。尝试一:navigator.geolocation尝试了使用 navigat...
继续阅读 »


尝试获取用户的位置信息

写在前面

想要像一些平台那样显示用户的位置信息,例如某省市那样。那么这是如何做到的, 据说这个位置信息的准确性在通信网络运营商那里?先不管,先实践尝试下能不能获取。

尝试一:navigator.geolocation

尝试了使用 navigator.geolocation,但未能成功拿到信息。

getGeolocation(){
 if ('geolocation' in navigator) {
   /* 地理位置服务可用 */
   console.log('地理位置服务可用')
   navigator.geolocation.getCurrentPosition(function (position) {
     console.dir('回调成功')
     console.dir(position) // 没有输出
     console.dir(position.coords.latitude, position.coords.longitude)
  }, function (error) {
     console.error(error)
  })
} else {
   /* 地理位置服务不可用 */
   console.error('地理位置服务可用')
}
}

尝试二:sohu 的接口

尝试使用 pv.sohu.com/cityjson?ie… 获取用户位置信息, 成功获取到信息,信息样本如下:

{"cip": "14.11.11.11", "cid": "440000", "cname": "广东省"}
// 需要做跨域处理
getIpAndAddressSohu(){
 // config 是配置对象,可按需设置,例如 responseType,headers 中设置 token 等
 const config = {
   headers: {
     Accept: 'application/json',
     'Content-Type': 'application/json;charset=UTF-8',
  },
}
 axios.get('/apiSohu/cityjson?ie=utf-8', config).then(res => {
   console.log(res.data) // var returnCitySN = {"cip": "14.23.44.50", "cid": "440000", "cname": "广东省"};
   const info = res.data.substring(19, res.data.length - 1)
   console.log(info) // {"cip": "14.23.44.50", "cid": "440000", "cname": "广东省"}
   this.ip = JSON.parse(info).cip
   this.address = JSON.parse(info).cname
})
}

调试的时候,做了跨域处理。

proxy: {
 '/apiSohu': {
   target: 'http://pv.sohu.com/', // localhost=>target
   changeOrigin: true,
   pathRewrite: {
   '/apiSohu': '/'
  }
},
}

下面是一张获取到位置信息的效果图:


尝试三:百度地图的接口

需要先引入百度地图依赖,有一个参数 ak 需要注意,这需要像管理方申请。例如下方这样

<script src="https://api.map.baidu.com/api?v=2.0&ak=3ufnnh6aD5CST"></script>
getLocation() { /*获取当前位置(浏览器定位)*/
const $this = this;
var geolocation = new BMap.Geolocation();//返回用户当前的位置
geolocation.getCurrentPosition(function (r) {
  if (this.getStatus() == BMAP_STATUS_SUCCESS) {
    $this.city = r.address.city;
    console.log(r.address) // {city: '广州市', city_code: 0, district: '', province: '广东省', street: '', …}
  }
});
}
function getLocationBaiduIp(){/*获取用户当前位置(ip定位)*/
function myFun(result){
  const cityName = result.name;
  console.log(result) // {center: O, level: 12, name: '广州市', code: 257}
}
var myCity = new BMap.LocalCity();
myCity.get(myFun);
}

成功用户的省市位置,以及经纬度坐标,但会先弹窗征求用户意见。



写在后面

尝试结果不太理想,sohu 的接口内部是咋实现的,这似乎没有弹起像下面那样的征询用户意见的提示。


而在 navigator.geolocation 和 BMap.Geolocation() 中是弹起了的。

用别人的接口总归是没多大意思,也不知道不用征求用户意见是咋实现的。

经实测 sohu 的接口和 new BMap.Geolocation() 都可以拿到用户的位置信息(省市、经纬度等)。

作者:灵扁扁

来源:https://juejin.cn/post/7100916925504421918

收起阅读 »

一种兼容、更小、易用的WEB字体API

如何使用 Google Fonts CSS API 有效地使用WEB字体?多年来,WEB字体技术发生了很多变化,过去在WEB中使用特殊字体的常用做法是图片或者Flash,这种借助图片或者Flash的实现方式不够灵活。随着 WEB 字体的出现,特别是 Googl...
继续阅读 »

如何使用 Google Fonts CSS API 有效地使用WEB字体?

多年来,WEB字体技术发生了很多变化,过去在WEB中使用特殊字体的常用做法是图片或者Flash,这种借助图片或者Flash的实现方式不够灵活。随着 WEB 字体的出现,特别是 Google Fonts CSS API 的普及,让在WEB中使用特殊字体变得简单、快速、灵活,当然更多的还是面向英文字体,对于做外贸或者英文网站的开发者来说是福音。

Google Fonts CSS API 在不断发展,以跟上WEB字体技术的变化。它从最初的价值主张——允许浏览器在所有使用API的网站上缓存常用字体,从而使网页加载更快,到现在已经有了很大的进步。现在不再是这样了,但API仍然提供了额外的优化方案,使网站加载迅速,字体工作性能更佳。

使用Google Fonts CSS API ,网站可以请求它需要的字体数据来保持它的CSS加载时间到最少,确保网站访问者可以尽可能快地加载内容。该API将以最佳的字体响应每个请求的web浏览器。

所有这一切都是通过在代码中包含一行 HTML 来实现的。

如何使用 Google Fonts CSS API

Google Fonts CSS API 文档很好地总结了它:

你不需要做任何编程;所要做的就是在 HTML 文档中添加一个特殊的样式表链接,然后在 CSS 样式中引用该字体。

需要做的最低限度是在 HTML 中包含一行,如下所示:

<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap" rel="stylesheet" />

复制代码

当从 API 请求字体时,可以指定想要的一个或多个系列,以及(可选)它们的权重、样式、子集和其他选项。然后 API 将通过以下两种方式之一处理请求:

  1. 如果请求使用 API 已有文件的通用参数,它会立即将 CSS 返回给用户,将定向到这些文件。

  2. 如果请求的字体带有 API 当前未缓存的参数,它将即时对字体进行子集化,使用 HarfBuzz 快速完成,并返回指向它们的 CSS。

字体文件可以很大,但不一定要很大

WEB 字体可以很大,在 WOFF2 中,仅一个 Noto Sans Japanese 的大小就几乎是 3.4MB ,将其下载给每一位用户将拖累页面加载时间。当每一毫秒都很重要并且每个字节都很宝贵时,需要确保只加载用户需要的数据。

Google Fonts CSS API 可以创建非常小的字体文件(称为子集),实时生成,只为用户提供网站所需的文本和样式。可以使用 text 参数请求特定字符,而不是提供整个字体。

<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap&text=RobtMn" rel="stylesheet" />

复制代码


CSS API 还自动为用户提供额外的WEB字体优化,无需设置任何 API 参数。该 API 将为用户提供已启用 unicode-range 的 CSS 文件(如果 Web 浏览器支持),因此只为网站需要的特定字符加载字体。

unicode-range CSS 描述符是一种现在可用于应对大字体下载的工具,这个 CSS 属性设置 @font-face 声明包含的 Unicode 字符范围。如果在页面上呈现这些字符之一,则下载该字体。这适用于所有类型的语言,因此可以采用包含拉丁文、希腊文或西里尔文字符的字体并制作更小的子集。在前面的图表中,可以看到如果必须加载所有这三个字符集,则将超过 600 个字形。


这也为 Web 启用了中文、日文和韩文 (CJK) 字体提供支持。在上图中,可以看到 CJK 字体覆盖的字符数是拉丁字符字体的 15-20 倍。 CJK 字体通常非常大,并且这些语言中的许多字符不像其他字体那样频繁使用。

使用 CSS API 和 unicode-range 可以减少大约 90% 的文件传输。使用 unicode-range 描述符,可以单独定义每个部分,并且只有在内容包含这些字符范围中的一个字符时才会下载每个切片。

例如只想在 Noto Sans JP 中设置单词 こんにちは ,则可以按照如下方式使用:

  • 自托管自己的 WOFF2 文件

  • 使用 CSS API 检索 WOFF2

  • 使用 CSS API 并将 text= 参数设置为 こんにちは


在此示例中,可以看到通过使用 CSS API,已经比自托管 WOFF2 字体节省了 97.5%,这要归功于 API 内置支持将大字体分隔到 unicode-range 中功能。通过更进一步并准确指定要显示的文本,可以进一步将字体大小减小到仅 CSS API 字体的 95.3% ,相当于比自托管字体小 99.9%

Google Fonts CSS API 将自动以用户浏览器支持的最小和最兼容格式提供字体。如果用户使用的是支持 WOFF2 的浏览器,API 将提供 WOFF2 中的字体,但如果他们使用的是旧版浏览器,API 将以该浏览器支持的格式提供字体。为了减少每个用户的文件大小,API 还会在不需要时从字体中删除数据。例如,将为浏览器不需要的用户删除提示数据。

使用 Google Fonts CSS API 让WEB字体面向未来

Google 字体团队还为新的 W3C 标准做出了贡献,这些标准继续创新网络字体技术,例如 WOFF2。当前的一个项目是增量字体传输,它允许用户在屏幕上使用字体文件时加载非常小的部分,并按需流式传输其余部分,超过了 unicode-range 的性能。当使用 WEB 字体API时,当用户在浏览器中可用时,就可以获得这些底层字体传输技术的优化改进。

这就是字体 API 的美妙之处:用户可以从每项新技术改进中受益,而无需对网站进行任何更改。新的WEB字体格式?没问题,新的浏览器或操作系统支持?它已经处理好了。因此,可以自由地专注于用户和内容,而不是陷入WEB字体维护的困境。

可变字体支持内置

可变字体是可以在多个轴之间存储一系列设计变化的字体文件,新版本的 Google Fonts CSS API 包括对它们的支持。添加一个额外的变化轴可以使字体具有新的灵活性,但它几乎可以使字体文件的大小增加一倍。

当 CSS API 请求更具体时,Google Fonts CSS API 可以仅提供网站所需的可变字体部分,以减少用户的下载大小。这使得可以为 WEB 使用可变字体,而不会导致页面加载时间过长。可以通过在轴上指定单个值或指定范围来执行此操作,甚至可以在一个请求中指定多个轴和多个字体系列, API 可以灵活地满足需求。

总结

Google Fonts CSS API 可帮助WEB提供以下字体:

  • 更兼容

  • 体积更小

  • 加载快速

  • 易于使用

有关 Google 字体的更多信息,请访问 fonts.google.com


作者:天行无忌
来源:juejin.cn/post/7100927964224700424

收起阅读 »

什么是请求参数、表单参数、url参数、header参数、Cookie参数?一文讲懂

最近在工作中对 http 的请求参数解析有了进一步的认识,写个小短文记录一下。回顾下自己的情况,大概就是:有点点网络及编程基础,只需要加深一点点对 HTTP 协议的理解就能弄明白了。先分享一个小故事:我至今仍清晰地记得大三实习时的第一个工作任务,我需要调用其他...
继续阅读 »

最近在工作中对 http 的请求参数解析有了进一步的认识,写个小短文记录一下。

回顾下自己的情况,大概就是:有点点网络及编程基础,只需要加深一点点对 HTTP 协议的理解就能弄明白了。

先分享一个小故事:我至今仍清晰地记得大三实习时的第一个工作任务,我需要调用其他部门提供的 api 去完成某项业务。

那个 api 文档只告诉了我请求参数需要传什么,没有提及用什么方式传,比如这样:


其实如果有经验的话,直接在请求体或 url 里填参数试一下就知道了;另一个是新人有时候不太敢问问题,其实只要向同事确认一下就好的。

然而由于当时我掌握的编程知识有限,只会用表单提交数据。所以当我下载完同事安利的 api 调用调试工具 postman 后,我就在网上查怎么用 postman 发送表单数据,结果折腾了好久 api 还是没能调通。

当天晚上我向老同学求助,他问我上课是不是又睡过去了?

我说你怎么知道?

他说当然咯,你上课睡觉不学习又不是一天两天的事情......

后来他告诉我得好好学一下 http 协议,看看可以在协议的哪些位置放请求参数。

一个简单的 http 服务器还原

那么,在正式讲解之前,我们先简单搭建一个 http 服务器,阿菌沿用经典的 python 版云你好服务器进行讲解。

云你好服务器的代码很简单,服务器首先会获取 name 用户名这个参数,如果用户传了这个参数,就返回 Hello xxx,xxx 指的是 name 用户名;如果用户没有传这个参数则返回 Hello World

# 云你好服务源码
from flask import Flask
from flask import request

app = Flask(__name__)

# 云你好服务 API 接口
@app.get("/api/hello")
def hello():
   # 看用户是否传递了参数 name
   name = request.args.get("name", "")
   # 如果传了参数就向目标对象打招呼,输出 Hello XXX,否则输出 Hello World
   return f"Hello {name}" if name else "Hello World"

# 启动云你好服务
if __name__ == '__main__':
   app.run()

为了快速开发(大伙可以下载一个 python 把这个代码跑一下,用自己的语言实现一个类似的服务器也是可以的),阿菌这里使用了 flask 框架构建后端服务。

在具体获取参数的时候,我选择了在 request.args 中获取参数。这里提前剧透一下:在 flask 框架中,request.args 指的是从 url 中获取参数(不过这是我们后面讲解的内容,大家有个印象就好)

抓包查看 http 报文

有了 http 服务器后,我们开始深入讲解 http 协议,em...个人觉得只在学校上课看教材学计算机网络好像还欠缺了点啥,比较推荐大家下载一个像 Wireshark 这样的网络抓包软件,动手拆解网络包,深入学习各种网络协议。抓取网络包的示例视频

为了搞清楚什么是请求参数、表单参数、url 参数、Header 参数、Cookie 参数,我们先发一个 http 请求,然后抓取这个请求的网络包,看看一份 http 报文会携带哪些信息。

呼应开头,用户阿菌是个只会发表单数据的萌新,他使用 postman 向云你好 api 发送了一个 post 请求:


剧情发展正常,我们没能得到 Hello 阿菌(服务器会到 url 中获取参数,咱们用表单形式提交,所以获取不到)

由于咱们对请求体这个概念比较模糊,接下来我们重新发一个一模一样的请求,并且通过 Wireshark 抓包看一下:


可以看到强大的 Wireshark 帮助我们把请求抓取了下来,并把整个网络包的链路层协议,IP层协议,传输层协议,应用层协议全都解析好了。

由于咱们小码农一般都忙于解决应用层问题,所以我们把目光聚焦于高亮的 Hypertext Transfer Protocol 超文本传输协议,也就是大名鼎鼎的 HTTP 协议。

首先我们查看一下 HTTP 报文的完整内容:


可以看到,http 协议大概是这么组成的:

  • 第一行是请求的方式,比如 GET / POST / DELETE / PUT

  • 请求方式后面跟的是请求的路径,一般把这个叫 URI(统一资源标识符)

补充:URL 是统一资源定位符,见名知义,因为要定位,所以要指定协议甚至是位置,比如这样:http://localhost:5000/api/hello

  • 请求路径后面跟的是 HTTP 的版本,比如这里是 HTTP/1.1

完整的第一行如下:

POST /api/hello HTTP/1.1

第二行的 User-Agent 则用于告诉对方发起请求的客户端是啥,比如咱们用 Postman 发起的请求,Postman 就会自动把这个参数设置为它自己:

User-Agent: PostmanRuntime/7.28.4

第三行的 Accept 用于告诉对方我们希望收到什么类型的数据,这里默认是能接受所有类型的数据:

Accept: */*

第四行就非常值得留意,Postman-Token 是 Postman 自己传的参数,这个我们放到下面讲!

Postman-Token: ddd72e1a-0d63-4bad-a18e-22e38a5de3fc

第五行是请求的主机,网络上的一个服务一般用 ip 加端口作为唯一标识:

Host: 127.0.0.1:5000

第六行指定的是咱们请求发起方可以理解的压缩方式:

Accept-Encoding: gzip, deflate, br

第七行告诉对方处理完当前请求后不要关闭连接:

Connection: keep-alive

第八行告诉对方咱们请求体的内容格式,这个是本文的侧重点啦!比如我们这里指定的是一般浏览器的原生表单格式:

Content-Type: application/x-www-form-urlencoded

好了,下面大家要留意了,第九行的 Content-Length 给出的是请求体的大小。

而请求体,会放在紧跟着的一个空行之后。比如本请求的请求体内容是以 key=value 形式填充的,也就是我们表单参数的内容了:

Content-Length: 23

name=%E9%98%BF%E8%8F%8C

看到这里我们先简单小结一下,想要告诉服务器我们发送的是表单数据,一共需要两步:

  1. Content-Type 设置为 application/x-www-form-urlencoded

  2. 在请求体中按照 key=value 的形式填写请求参数

什么是协议?进一步了解 http

好了,接下来我们进一步讲解,大家试想一下,网络应用,其实就是端到端的交互,最常见的就是服务端和客户端交互模型:客户端发一些参数数据给服务端,通过这些参数数据告诉服务端它想得到什么或想干什么,服务端根据客户端传递的参数数据作出处理。

传输层协议通过 ip 和端口号帮我们定位到了具体的服务应用,具体怎么交互是由我们程序员自己定义的。

大概在 30 年前,英国计算机科学家蒂姆·伯纳斯-李定义了原始超级文本传输协议(HTTP),后续我们的 web 应用大都延续采用了他定义的这套标准,当然这套标准也在不断地进行迭代。

许多文献资料会把 http 协议描述得比较晦涩,加上协议这个词听起来有点高大上,初学者入门学习的时候往往感觉不太友好。

其实协议说白了就是一种格式,就好比我们写书信,约定要先顶格写个敬爱的 xxx,然后写个你好,然后换一个段落再写正文,可能最后还得加上日期署名等等。

我们只要按照格式写信,老师就能一眼看出来我们在写信;只要我们按协议格式发请求数据,服务器就能一眼看出来我们想要得到什么或想干什么。

当然,老师是因为老早就学过书信格式,所以他才能看懂书信格式;服务端程序也一样,我们要预先编写好 http 协议的解析逻辑,然后我们的服务器才能根据解析逻辑去获取一个 http 请求中的各种东西。

当然这个解析 http 协议的逻辑不是谁都能写出来的,就算能写出来,也未必写得好,所以我们会使用厉害的人封装好的脚手架,比如 java 里的 spring 全套、Go 语言里的 Gin 等等。

回到我们开头给出的示例:

from flask import Flask
from flask import request

app = Flask(__name__)

# 云你好服务 API 接口
@app.get("/api/hello")
def hello():
   # 看用户是否传递了参数 name
   name = request.args.get("name", "")
   # 如果传了参数就向目标对象打招呼,输出 Hello XXX,否则输出 Hello World
   return f"Hello {name}" if name else "Hello World"

# 启动云你好服务
if __name__ == '__main__':
   app.run()

阿菌的示例使用了 python 里的 flask 框架,在处理逻辑中使用了 request.args 获取请求参数,而 args 封装的就是框架从 url 中获取参数的逻辑。比如我们发送请求的 url 为:

http://127.0.0.1:5000/api/hello?name=ajun

框架会帮助我们从 url 中的 ? 后面开始截取,然后把 name=ajun 这些参数存放到 args 里。

切换一下,假设我们是云你好服务提供者,我们希望用户通过表单参数的形式使用云你好服务,我们只要把获取 name 参数的方式改成从表单参数里获取就可以了,flask 在 request.form 里封装了表单参数(关于框架是怎么在数行 http 请求中封装参数的,大家可以看自己使用的框架的具体逻辑,估计区别不大,只是存在一些语言特性上的差异):

@app.post("/api/hello")
def hello():
   # 看用户是否传递了参数 name
   name = request.form.get("name", "")
   # 如果传了参数就向目标对象打招呼,输出 Hello XXX,否则输出 Hello World
   return f"Hello {name}" if name else "Hello World"

思考:我们可以在 http 协议中传递什么参数?

最后,我们解释本文的标题,其实想要明白各种参数之间的区别,我们可以换一个角度思考:

咱们可以在一份 http 报文的哪些位置传递参数?

接下来回顾一下一个 http 请求的内容:

POST /api/hello HTTP/1.1
User-Agent: PostmanRuntime/7.28.4
Accept: */*
Postman-Token: fbf75035-a647-46dc-adc0-333751a9399e
Host: 127.0.0.1:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 23

name=%E9%98%BF%E8%8F%8C

大家看,咱们的 http 报文,也就是基于传输层之上的应用层报文,大概就长上面这样。

我们考虑两种情况,第一种情况,我们基于别人已经开发好的脚手架开发 http 服务器。

由于框架会基于 http 协议进行解析,所以框架会帮助我们解析好请求 url,各种 Header 头(比如:Cookie 等),以及具体的响应内容都帮我们封装解析好了(比如按照 key=value 的方式去读取请求体)。

那当我们开发服务端的时候,就可以指定从 url、header、响应体中获取参数了,比如:

  • url 参数:指的就是 url 中 ? 后面携带的 key value 形式参数

  • header 参数:指的就是各个 header 头,我们甚至可以自定义 header,比如 Postman-Token 就是 postman 这个软件自己携带的,我们服务端如果需要的话是可以指定获取这个参数的

  • Cookie 参数:其实就是名字为 Cookie 的请求头

  • 表单参数:指的就是 Content-Type 为 application/x-www-form-urlencoded 下请求体的内容,如果我们的表单需要传文件,还会有其他的 Content-Type

  • json 参数:指的就是 Content-Type 为 application/json 下请求体的内容(当然服务端可以不根据 Content-Type 直接解析请求体,但按照协议的规范工程项目或许会更好维护)

综上所述,请求参数就是对上面各种类型的参数的一个总称了。

大家会发现,不管什么 url 参数、header 参数、Cookie 参数、表单参数,其实就是换着法儿,按照一定的格式把数据放到应用层报文中。关键在于我们的服务端程序和客户端程序按照一种什么样的约定去传递和获取这些参数。这就是协议吧~

还有另一种情况,当然这只是开玩笑了,比如以后哪位大佬或者哪家企业定义了一种新的数据传输标准,推广至全球,比如叫 hppt 协议,这样是完全可以自己给各种形式参数下定义取名字的。这可能就是为啥我们说一流的企业、大佬制定标准,接下来的围绕标准研发技术,进而是基于技术卖产品,最后是围绕产品提供服务了。

一旦标准制定了,整个行业都围绕这个标准转了,而且感觉影响会越来越深远......

讲解参考链接

作者:胡涂阿菌
来源:juejin.cn/post/7100400494081736711

收起阅读 »

JavaScript中的事件委托

事件委托基本概念事件委托,就是一个元素的响应事件的函数委托给另一个元素一般我们都是把函数绑定给当前元素的父元素或更外层元素,当事件响应到需要绑定的元素的时候,会通过事件冒泡机制(或事件捕获)去触发外层元素的绑定事件,在外层元素上去执行函数在了解事件委托之前,我...
继续阅读 »

事件委托基本概念

事件委托,就是一个元素的响应事件的函数委托给另一个元素

一般我们都是把函数绑定给当前元素的父元素或更外层元素,当事件响应到需要绑定的元素的时候,会通过事件冒泡机制(或事件捕获)去触发外层元素的绑定事件,在外层元素上去执行函数

在了解事件委托之前,我们可以先了解事件流,事件冒泡以及事件捕获

事件流:捕获阶段,目标阶段,冒泡阶段

DOM事件流有3个阶段:捕获阶段,目标阶段,冒泡阶段;

三个阶段的顺序为:捕获阶段——目标阶段——冒泡阶段

事件冒泡

事件的触发响应会从最底层目标一层层地向外到最外层(根节点)

比如说我现在有一个盒子f,里面有个子元素s

  <div class="f">
       <div class="s"></div>
 </div>

添加事件

var f = document.querySelector('.f')
var s = document.querySelector('.s')
f.addEventListener('click',()=>{
   console.log('fffff');
})
s.addEventListener('click',()=>{
   console.log('sssss');
})

当我点击子元素的时候

冒泡顺序 s -> f

事件捕获

事件响应从最外层的Window开始,逐级向内层前进,直到具体事件目标元素。在捕获阶段,不会处理响应元素注册的冒泡事件

继续使用上一个例子,只需要将addEventListener第三个参数改为true即可

添加事件

var f = document.querySelector('.f')
var s = document.querySelector('.s')
f.addEventListener('click',()=>{
   console.log('fffff');
},true)
s.addEventListener('click',()=>{
   console.log('sssss');
},true)

点击子元素

捕获顺序 f -> s

这里我们可以思考一下,如果同时绑定了冒泡和捕获事件的话,会有怎样的执行顺序呢?

例子不变,稍微改一下js代码

var f = document.querySelector('.f')
var s = document.querySelector('.s')
f.addEventListener('click',()=>{
   console.log('f捕获');
},true)
s.addEventListener('click',()=>{
   console.log('s捕获');
},true)
f.addEventListener('click',()=>{
   console.log('f冒泡');
})
s.addEventListener('click',()=>{
   console.log('s冒泡');
})

此时点击子元素

执行顺序: f捕获->s捕获->s冒泡—>f冒泡

得出结论:当我们同时绑定捕获和冒泡事件的时候,会先从外层开始捕获到目标元素,然后由目标元素冒泡到外层

回到事件委托

了解了事件捕获和事件冒泡,再来看事件委托就很好理解了

强调一遍,事件委托把函数绑定给当前元素的父元素或更外层元素,当事件响应到需要绑定的元素的时候,会通过事件冒泡机制(或事件捕获)去触发外层元素的绑定事件,在外层元素上去执行函数

新开一个例子

<ul class="list">
       <li class="item"></li>
       <li class="item"></li>
       <li class="item"></li>
       <li class="item"></li>
       <li class="item"></li>
</ul>

现在我们有一个列表,当我们点击列表中的某一项时可以触发对应事件,如果我们给列表的每一项都添加事件,对于内存消耗是非常大的,效率上需要消耗很多性能

这个时候我们就可以把这个点击事件绑定到他的父层,也就是 ul 上,然后在执行事件的时候再去匹配判断目标元素;

var list = document.querySelector('.list')
// 利用冒泡机制实现
list.addEventListener('click',(e)=>{
  e.target.style.backgroundColor='blue'
})
// 利用捕获机制实现
list.addEventListener('click',(e)=>{
  e.target.style.backgroundColor='red'
},true)

当我点击其中一个子元素的时候


总结

  • 事件委托就是根据事件冒泡或事件捕获的机制来实现的

  • 事件冒泡就是事件的触发响应会从最底层目标一层层地向外到最外层(根节点)

  • 事件捕获就是事件响应从最外层的Window开始,逐级向内层前进,直到具体事件目标元素。在捕获阶段,不会处理响应元素注册的冒泡事件

补充:

对于目标元素,捕获和冒泡的执行顺序是由绑定事件的执行顺序决定的

作者:张宏都
来源:https://juejin.cn/post/7100468737647575048

收起阅读 »

axios 请求拦截器&响应拦截器

一、 拦截器介绍一般在使用axios时,会用到拦截器的功能,一般分为两种:请求拦截器、响应拦截器。请求拦截器 在请求发送前进行必要操作处理,例如添加统一cookie、请求体加验证、设置请求头等,相当于是对每个接口里相同操作的一个封装;响应拦截器 同理,响应拦截...
继续阅读 »

一、 拦截器介绍

一般在使用axios时,会用到拦截器的功能,一般分为两种:请求拦截器、响应拦截器。

  1. 请求拦截器
    在请求发送前进行必要操作处理,例如添加统一cookie、请求体加验证、设置请求头等,相当于是对每个接口里相同操作的一个封装;

  2. 响应拦截器
    同理,响应拦截器也是如此功能,只是在请求得到响应之后,对响应体的一些处理,通常是数据统一处理等,也常来判断登录失效等。

二、 Axios实例

  1. 创建axios实例

// 引入axios
import axios from 'axios'

// 创建实例
let instance = axios.create({
   baseURL: 'xxxxxxxxxx',
   timeout: 15000  // 毫秒
})
  1. baseURL设置:

let baseURL;
if(process.env.NODE_ENV === 'development') {
   baseURL = 'xxx本地环境xxx';
} else if(process.env.NODE_ENV === 'production') {
   baseURL = 'xxx生产环境xxx';
}

// 实例
let instance = axios.create({
   baseURL: baseURL,
   ...
})
  1. 修改实例配置的三种方式

// 第一种:局限性比较大
axios.defaults.timeout = 1000;
axios.defaults.baseURL = 'xxxxx';

// 第二种:实例配置
let instance = axios.create({
   baseURL: 'xxxxx',
   timeout: 1000,  // 超时,401
})
// 创建完后修改
instance.defaults.timeout = 3000

// 第三种:发起请求时修改配置、
instance.get('/xxx',{
   timeout: 5000
})

这三种修改配置方法的优先级如下:请求配置 > 实例配置 > 全局配置

三、 配置拦截器

// 请求拦截器
instance.interceptors.request.use(req=>{}, err=>{});
// 响应拦截器
instance.interceptors.reponse.use(req=>{}, err=>{});
  1. 请求拦截器

// use(两个参数)
axios.interceptors.request.use(req => {
   // 在发送请求前要做的事儿
   ...
   return req
}, err => {
   // 在请求错误时要做的事儿
   ...
   // 该返回的数据则是axios.catch(err)中接收的数据
   return Promise.reject(err)
})
  1. 响应拦截器

// use(两个参数)
axios.interceptors.reponse.use(res => {
   // 请求成功对响应数据做处理
   ...
   // 该返回的数据则是axios.then(res)中接收的数据
   return res
}, err => {
   // 在请求错误时要做的事儿
   ...
   // 该返回的数据则是axios.catch(err)中接收的数据
   return Promise.reject(err)
})
  1. 常见错误码处理(error)
    axios请求错误时,可在catch里进行错误处理。

axios.get().then().catch(err => {
   // 错误处理
})

四、 axios请求拦截器的案例

// 设置请求拦截器
axios.interceptors.request.use(
 config => {
   // console.log(config) // 该处可以将config打印出来看一下,该部分将发送给后端(server端)
   config.headers.Authorization = store.state.token
   return config // 对config处理完后返回,下一步将向后端发送请求
},
 error => { // 当发生错误时,执行该部分代码
   // console.log(error) // 调试用
   return Promise.reject(error)
}
)

// 定义响应拦截器 -->token值无效时,清空token,并强制跳转登录页
axios.interceptors.response.use(function (response) {
 // 响应状态码为 2xx 时触发成功的回调,形参中的 response 是“成功的结果”
 return response
}, function (error) {
 // console.log(error)
 // 响应状态码不是 2xx 时触发失败的回调,形参中的 error 是“失败的结果”
 if (error.response.status === 401) {
   // 无效的 token
   // 把 Vuex 中的 token 重置为空,并跳转到登录页面
   // 1.清空token
   store.commit('updateToken', '')
   // 2.跳转登录页
   router.push('/login')
}
 return Promise.reject(error)
})

作者:我彦祖不会秃
来源:https://juejin.cn/post/7100470316857557006

收起阅读 »

说说你对事件循环的理解

一、事件循环是什么首先,JavaScript是一门单线程的语言,意味着同一时间内只能做一件事,但是这并不意味着单线程就是阻塞,而实现单线程非阻塞的方法就是事件循环在JavaScript中,所有的任务都可以分为同步任务:立即执行的任务,同步任务一般会直接进入到主...
继续阅读 »

一、事件循环是什么

首先,JavaScript是一门单线程的语言,意味着同一时间内只能做一件事,但是这并不意味着单线程就是阻塞,而实现单线程非阻塞的方法就是事件循环

在JavaScript中,所有的任务都可以分为

  • 同步任务:立即执行的任务,同步任务一般会直接进入到主线程中执行

  • 异步任务:异步执行的任务,比如ajax网络请求,setTimeout定时函数等

同步任务与异步任务的运行流程图如下:


从上面我们可以看到,同步任务进入主线程,即主执行栈,异步任务进入任务队列,主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。上述过程的不断重复就事件循环

二、宏任务与微任务

如果将任务划分为同步任务和异步任务并不是那么的准确,举个例子:

console.log(1)

setTimeout(()=>{
  console.log(2)
}, 0)

new Promise((resolve, reject)=>{
  console.log('new Promise')
  resolve()
}).then(()=>{
  console.log('then')
})

console.log(3)

最终结果: 1=>'new Promise'=> 3 => 'then' => 2

微任务

一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前

常见的微任务有:

  • Promise.then

  • MutaionObserver

  • Object.observe(已废弃;Proxy 对象替代)

  • process.nextTick(Node.js)

宏任务

宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合

常见的宏任务有:

  • script (可以理解为外层同步代码)

  • setTimeout/setInterval

  • UI rendering/UI事件

  • postMessage、MessageChannel

  • setImmediate、I/O(Node.js)

这时候,事件循环,宏任务,微任务的关系如图所示


  • 执行一个宏任务,如果遇到微任务就将它放到微任务的事件队列中

  • 当前宏任务执行完成后,会查看微任务的事件队列,然后将里面的所有微任务依次执行完

回到上面的题目

console.log(1)
setTimeout(()=>{
  console.log(2)
}, 0)
new Promise((resolve, reject)=>{
  console.log('new Promise')
  resolve()
}).then(()=>{
  console.log('then')
})
console.log(3)

最终结果: 1=>'new Promise'=> 3 => 'then' => 2

// 遇到 console.log(1) ,直接打印 1
// 遇到定时器,属于新的宏任务,留着后面执行
// 遇到 new Promise,这个是直接执行的,打印 'new Promise'
// .then 属于微任务,放入微任务队列,后面再执行
// 遇到 console.log(3) 直接打印 3
// 好了本轮宏任务执行完毕,现在去微任务列表查看是否有微任务,发现 .then 的回调,执行它,打印 'then'
// 当一次宏任务执行完,再去执行新的宏任务,这里就剩一个定时器的宏任务了,执行它,打印 2

三、async与await

async 是异步的意思,await则可以理解为 async wait。所以可以理解async就是用来声明一个异步方法,而 await是用来等待异步方法执行

async

async函数返回一个promise对象,下面两种方法是等效的

function f() {
  return Promise.resolve('TEST');
}

// asyncF is equivalent to f!
async function asyncF() {
  return 'TEST';
}

await

正常情况下,await命令后面是一个 Promise对象,返回该对象的结果。如果不是 Promise对象,就直接返回对应的值

async function f(){
  // 等同于
  // return 123
  return await 123
}
f().then(v => console.log(v)) // 123

不管await后面跟着的是什么,await都会阻塞后面的代码

async function fn1 (){
  console.log(1)
  await fn2()
  console.log(2) // 阻塞
}

async function fn2 (){
  console.log('fn2')
}

fn1()
console.log(3)

上面的例子中,await 会阻塞下面的代码(即加入微任务队列),先执行 async外面的同步代码,同步代码执行完,再回到 async 函数中,再执行之前阻塞的代码

所以上述输出结果为:1,fn2,3,2

四、流程分析

通过对上面的了解,我们对JavaScript对各种场景的执行顺序有了大致的了解

这里直接上代码:

async function async1() {
  console.log('async1 start')
  await async2()
  console.log('async1 end')
}
async function async2() {
  console.log('async2')
}
console.log('script start')
setTimeout(function () {
  console.log('settimeout')
})
async1()
new Promise(function (resolve) {
  console.log('promise1')
  resolve()
}).then(function () {
  console.log('promise2')
})
console.log('script end')

分析过程:

执行整段代码,遇到 console.log('script start') 直接打印结果,输出 script start
遇到定时器了,它是宏任务,先放着不执行
遇到 async1(),执行 async1 函数,先打印 async1 start,下面遇到await怎么办?先执行 async2,打印 async2,然后阻塞下面代码(即加入微任务列表),跳出去执行同步代码
跳到 new Promise 这里,直接执行,打印 promise1,下面遇到 .then(),它是微任务,放到微任务列表等待执行
最后一行直接打印 script end,现在同步代码执行完了,开始执行微任务,即 await下面的代码,打印 async1 end
继续执行下一个微任务,即执行 then 的回调,打印 promise2
上一个宏任务所有事都做完了,开始下一个宏任务,就是定时器,打印 settimeout
所以最后的结果是:script start、async1 start、async2、promise1、script end、async1 end、promise2、settimeout

作者:用户8249803991033
来源:https://juejin.cn/post/7100468871752056868

收起阅读 »

如何美化你的图表,关于SVG渐变你需要了解的一切!

渐变在网页设计中几乎随处可见,渐变的背景、文字、按钮、图表等等,相比于纯色,渐变的颜色显得更加灵动自然。今天我们要探讨的,就是SVG中的渐变绘制。更多SVG系列文章:SVG基础知识、SVG动画、SVG中的Transform变换。概述或许你有使用css绘制渐变图...
继续阅读 »

渐变在网页设计中几乎随处可见,渐变的背景、文字、按钮、图表等等,相比于纯色,渐变的颜色显得更加灵动自然。

今天我们要探讨的,就是SVG中的渐变绘制。

更多SVG系列文章:SVG基础知识、SVG动画、SVG中的Transform变换。

概述

或许你有使用css绘制渐变图形的经验,如果要绘制一个渐变的矩形,我们可以这样写:

<div></div>

.bg{
  height: 100px;
  width: 200px;
  //给元素设置渐变背景
  background: linear-gradient(#fb3,#58a);
}

使用SVG绘图,颜色是通过设置元素的fill(填充颜色)和stroke(边框颜色)属性来实现。

<rect height="100" width="150" stroke="#45B649" stroke-width="2" fill="#DCE35B"></rect>


对于渐变颜色的设置,我们不能像在css中那样,直接写fill="linear-gradient(color1, color2)",而要使用专门的渐变标签:<linearGradient>(线性渐变) 和 <radialGradient>(径向渐变)。

线性渐变

基础使用

先来看一个最简单的例子,如何绘制一个线性渐变的矩形:

<svg>
  <defs>
      <linearGradient id="gradient-test">
          <stop offset="0%" stop-color="#DCE35B" />
          <stop offset="100%" stop-color="#45B649" />
      </linearGradient>
  </defs>
  <rect height="100" width="150" fill="url(#gradient-test)"></rect>
</svg>


通常,我们将渐变标签<linearGradient>定义在<defs>元素中,<linearGradient>id属性作为其唯一标识,方便后面需要使用的地方对其进行引用。

<linearGradient>中的<stop>标签定义渐变色的色标,它的offsetstop-color属性分别定义色标的位置和颜色值,它还有一个属性stop-opacity,设定stop-color颜色的透明度。

如果将色标的位置拉近:

<linearGradient id="gradient-1">
  <stop offset="30%" stop-color="#DCE35B" />
  <stop offset="70%" stop-color="#45B649" />
</linearGradient>


矩形左边的 30% 区域被填充为 #DCE35B 实色,而右边 30% 区域被填充为 #45B649 实色。真正的渐变只出现在矩形中间 40% 的区域。

如果两个颜色都设为50%,就得到了两块均分矩形的实色。在这基础上,我们可以生成各种颜色的条纹图案。


渐变的方向和范围

在没有设置渐变方向的时候,渐变的默认方向是从左向右。

如果要设定渐变方向,要用到<linearGradient>x1,y1,x2,y2这几个属性。

<linearGradient id="gradient-1" x1="0" y1="0" x2="0" y2="1">
  <stop offset="0%" stop-color="#DCE35B" />
  <stop offset="100%" stop-color="#45B649" />
</linearGradient>

我们知道,在平面上,方向一般由向量来表示。而渐变的方向由(x1,y1)(起点)和(x2,y2)(点)两个点定义的向量来表示。

在一般的应用场景中,x1,y1,x2,y2的取值范围是[0,1](或者用百分数[0%, 100%])。

对于矩形而言,不管矩形的长宽比例是多少,它的左上角对应的都是(0,0),右下角则对应(1,1)


x1="0" y1="0" x2="0" y2="1"表示从(0,0)(0,1),即渐变方向从矩形上边框垂直向下到下边框。

x1="0" y1="0.3" x2="0" y2="0.7"的情形如下:


可以看出,x1,y1,x2,y2不仅决定渐变的方向,还决定了渐变的范围,超出渐变范围的部分由起始或结束色标的颜色进行纯色填充。

案例1:渐变文字


<svg width="600" height="270">
  <defs>
      <linearGradient id="background">                         <!--背景渐变色-->
          <stop offset="0%" stop-color="#232526" />
          <stop offset="100%" stop-color="#414345" />
      </linearGradient>
      <linearGradient id="text-color" x1="0" y1="0" x2="0" y2="100%"> <!--文字渐变色-->
          <stop offset="0%" stop-color="#DCE35B" />
          <stop offset="100%" stop-color="#45B649" />
      </linearGradient>
  </defs>
  <rect x="0" y="0" height="100%" width="100%" fill="url(#background)"></rect>
  <text y="28%" x="28%">试问闲情都几许?</text>
  <text y="44%" x="28%">一川烟草</text>
  <text y="60%" x="28%">满城风絮</text>
  <text y="76%" x="28%">梅子黄时雨</text>
</svg>
<style>
  text{
      font-size: 32px;
      letter-spacing:5px;
      fill:url(#text-color);     //文字的填充使用渐变色
  }
</style>

文字的填充,我们用了垂直方向的渐变色,对于每一行文字,都是从黄色渐变到绿色。

如果要将这几行文字作为一个整体来设置渐变色,像下面这样,应该怎样设置呢?


这就要用到gradientUnits属性了。

gradientUnits属性定义渐变元素(<linearGradient><radialGradient>)要参考的坐标系。 它有两个取值:objectBoundingBoxuserSpaceOnUse

默认值是objectBoundingBox,它定义渐变元素的参考坐标系为引用该渐变的SVG元素,渐变的起止、范围、方向都是基于引用该渐变的SVG元素(之前的<rect>,这里的<text>)自身,比如这里的每一个<text>元素的左上角都是渐变色的(0,0)位置,右下角都是(100%,100%)

userSpaceOnUse则以当前的SVG元素视窗区域(viewport) 为渐变元素的参考坐标系。也就是SVG元素的左上角为渐变色的(0,0)位置,右下角为(100%,100%)

<svg height="200" width="300"> 
  <defs>
      <!-- 定义两个渐变,除了gradientUnits,其他配置完全相同 -->
      <linearGradient id="gradient-1" x1="0" y1="0" x2="100%" y2="100%" gradientUnits="objectBoundingBox">
          <stop offset="0%" stop-color="#C6FFDD" />
          <stop offset="100%" stop-color="#f7797d" />
      </linearGradient>
      <linearGradient id="gradient-2" x1="0" y1="0" x2="100%" y2="100%" gradientUnits="userSpaceOnUse">
          <stop offset="0%" stop-color="#C6FFDD" />
          <stop offset="100%" stop-color="#f7797d" />
      </linearGradient>
  </defs>
  <rect x="0" y="0" ></rect>
  <rect x="150" y="0" ></rect>
  <rect x="0" y="100" ></rect>
  <rect x="150" y="100" ></rect>
</svg>
rect{
  height: 100px;
  width: 150px;
  fill: url(#gradient-1); //四个矩形都填充渐变色,下面左图为gradient-1,右图为gradient-2。
}


gradientUnits:userSpaceOnUse 适用于画布中有多个图形,但每个图形都是整体渐变中的一部分这样的场景。值得注意的是,当gradientUnits="userSpaceOnUse"时,x1,y1,x2,y2的取值只有用%百分数这样的相对单位才表示比例,如果取值为x2="1",那就真的是1px,这一点与gradientUnits="objectBoundingBox"是不同的。

案例2:渐变的环形进度条

上一篇文章中,我们实现了可交互的环形进度条:


这里我们将其改造成渐变的环形进度条。


使用渐变色作为描边stroke的颜色,中间使用一个白色透明度渐变的圆,增加立体感。

<!--改动部分的代码-->
<svg height="240" width="240" viewBox="0 0 100 100">
  <defs>
      <linearGradient id="circle">
          <stop offset="0%" stop-color="#A5FECB" />
          <stop offset="50%" stop-color="#20BDFF" />
          <stop offset="100%" stop-color="#5433FF" />
      </linearGradient>
      <linearGradient id="center">
          <stop offset="0%" stop-color="rgba(255,255,255,0.25)" />
          <stop offset="100%" stop-color="rgba(255,255,255,0.08)" />
        </linearGradient>
    </defs>
    <!--灰色的背景圆环-->
    <circle cx="50" cy="50" r="40" stroke-width="12" stroke="#eee" fill="none"></circle>
    <!--渐变的动态圆环-->
    <circle      
       
        cx="50" cy="50" r="40"
        transform="rotate(-90 50 50)"
        stroke-width="12"
        stroke="url(#circle)"
        fill="none"
        stroke-linecap="round"
        stroke-dasharray="251"></circle>
    <!--白色透明度渐变的圆,增加立体感-->
    <circle cx="50" cy="50" r="40" fill="url(#center)"></circle>
</svg>

径向渐变

基础使用

径向渐变是色彩从中心点向四周辐射的渐变。


<svg height="300" width="200">
<defs>
<radialGradient id="test">
<stop offset="0%" stop-color="#e1eec3" />
<stop offset="100%" stop-color="#f05053" />
</radialGradient>
</defs>
<rect fill="url(#test)" x="10" y="10" width="150" height="150"></rect>
</svg>

和线性渐变的结构类似,我们将径向渐变标签<radialGradient>定义在<defs>元素中,其id属性作为其唯一标识,以便后面需要使用的地方对其进行引用。

<radialGradient>中的<stop>标签定义渐变色的色标,它的offsetstop-color属性分别定义色标的位置和颜色值。

渐变的范围

径向渐变的范围由<radialGradient>cx,cy,r三个属性共同决定,它们的默认值均是50%,是相对值,相对的是引用该渐变的SVG元素

cxcy定义径向渐变范围的圆心,(50%, 50%)意味着是引用该渐变的SVG元素的中心。r设定渐变范围的半径,当r=50%时,说明渐变范围的半径在xy方向的分别是引用该渐变的SVG元素widthheight的50%。

//当rect高度减小时,渐变在y方向的半径也减小。
<rect fill="url(#test)" x="10" y="10" width="150" height="100"></rect>


cx,cy,r都取默认值的情况下,径向渐变的范围刚好覆盖引用该渐变的SVG元素。实际开发中,我们常常需要调整渐变范围。


渐变起点的移动

在默认情况下,渐变起点都是在渐变范围的中心,如果想要不那么对称的渐变,就需要改变渐变起点的位置。

<radialGradient>fxfy就是用来设置渐变色起始位置的。fxfy的值也是相对值,相对的也是引用该渐变的SVG元素


我们可以设定渐变的范围(cx,cy,r),也可以设定渐变的起点位置(fx,fy)。但是如果渐变的起点位置在渐变的范围之外,会出现一些我们不想要的效果。


测试代码如下,可直接运行:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
      body{
          display: flex;
          justify-content: center;
      }
      .control{
          margin-top:20px;
      }
  </style>
</head>
<body>
  <svg height="300" width="200">
      <defs>
          <radialGradient id="test">
              <stop offset="0%" stop-color="#e1eec3" />
              <stop offset="100%" stop-color="#f05053" />
          </radialGradient>
      </defs>
      <rect fill="url(#test)" x="10" y="10" width="150" height="150"></rect>
  </svg>
  <div>
      <div>cx:<input value="50" type="range" min="0" max="100" id="cx" /></div>
      <div>cy:<input value="50" type="range" min="0" max="100" id="cy" /></div>
      <div>r:<input value="50" type="range" min="0" max="100" id="r" /></div>
      <div>fx:<input value="50" type="range" min="0" max="100" id="fx" /></div>
      <div>fy:<input value="50" type="range" min="0" max="100" id="fy" /></div>
  </div>
  <script>
      const rg = document.getElementById('test')
      document.querySelectorAll('input').forEach((elem) => {
          elem.addEventListener('change', (ev) => {
              rg.setAttribute(ev.target.id, ev.target.value+'%')
          })
      })
  </script>
</body>
</html>

综合案例:透明的泡泡

最后我们用线性渐变和径向渐变画一个泡泡。


分析:

  • 背景是一个用线性渐变填充的矩形。

  • 泡泡分为三个部分:由径向渐变填充的一个圆形和两个椭圆。

这里的径向渐变主要是颜色透明度的渐变。设定颜色透明度,我们可以直接指定stop-color的值为rgba,也可以通过stop-opacity来设定stop-color颜色的透明度。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
      .bubble{
          animation: move 5s linear infinite;
          animation-direction:alternate;
      }
      //泡泡的运动
      @keyframes move {
          0%{
              transform: translate(0,0);
          }
          50%{
              transform: translate(250px,220px);
          }
          100%{
              transform: translate(520px,50px);
          }
      }
  </style>
</head>
<body>
  <svg height="400" width="700">
      <defs>
          <!--背景的线性渐变-->
          <linearGradient id="background">
              <stop offset="0%" stop-color="#DCE35B" />
              <stop offset="100%" stop-color="#45B649" />
          </linearGradient>
          <!--光斑的径向渐变,通过fx、fy设置不对称的渐变-->
          <radialGradient id="spot" fx="50%" fy="30%">
              <stop offset="10%" stop-color="white" stop-opacity=".7"></stop>  
              <stop offset="70%" stop-color="white" stop-opacity="0"></stop>
          </radialGradient>
          <!--泡泡本体的径向渐变-->
          <radialGradient id="bubble">
              <stop offset="0%" stop-color="rgba(255,255,255,0)" ></stop>  
              <stop offset="80%" stop-color="rgba(255,255,255,0.1)" ></stop>
              <stop offset="100%" stop-color="rgba(255,255,255,0.42)"></stop>
          </radialGradient>
      </defs>
      <rect fill="url(#background)" width="100%" height="100%"></rect>
      <g>
          <circle cx="100" cy="100" r="70" fill="url(#bubble)"></circle>
          <ellipse rx="50" ry="20" cx="80" cy="60" fill="url(#spot)" transform="rotate(-25, 80, 60)" ></ellipse>
          <ellipse rx="20" ry="10" cx="140" cy="130" fill="url(#spot)" transform="rotate(125, 140, 130)" ></ellipse>
      </g>  
  </svg>
</body>
</html>

以上渐变配色均来自网站: uigradients.com/

作者:Alaso
来源:juejin.cn/post/7098637240825282591

收起阅读 »

H5如何实现唤起APP

前言写过hybrid的同学,想必都会遇到这样的需求,如果用户安装了自己的APP,就打开APP或跳转到APP内某个页面,如果没安装则引导用户到对应页面或应用商店下载。这里就涉及到了H5与Native之间的交互,为什么H5能够唤起APP并且跳转到对应的页面?就算你...
继续阅读 »

前言

写过hybrid的同学,想必都会遇到这样的需求,如果用户安装了自己的APP,就打开APP或跳转到APP内某个页面,如果没安装则引导用户到对应页面或应用商店下载。这里就涉及到了H5与Native之间的交互,为什么H5能够唤起APP并且跳转到对应的页面?

就算你没写过想必也体验过,最常见的就是抖音里面的一些广告,如果你点击了广告,他判断你手机装了对应APP,那他就会去打开那个APP,如果没安装,他会帮你跳转到应用商店去下载,这个还算人性化一点的,有些直接后台给你去下载,你完全无感知。

哈哈,是不是觉得这种技术很神奇,今天我们就一起来看看它是如何实现的~

如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,文章公众号首发,关注 前端南玖 第一时间获取最新文章~

唤端体验

实现之前我们先简单体验一下什么是唤端


从上图中,我们可以看到在浏览器中我们点击打开知乎,系统会提示我们是否在知乎中打开,当我们点击打开时,知乎就被打开了,这就是一个简单的唤端体验。

有了这项技术我们就可以实现H5唤起APP应用了,现阶段的引流方式大都得益于这种技术,比如广告投放、用户拉新、引流等。

唤端技术

体验过后,我们就来聊一聊它的实现技术是怎样的,唤端技术我们也称之为deep link技术。当然,不同平台的实现方式有些不同,一般常见的有这几种,分别是:

  • URL Scheme(通用)

  • Universal Link (iOS)

  • App Link、Chrome Intents(android)

URL Scheme(通用)

这种方式是一种比较通用的技术,各平台的兼容性也很好,它一般由协议名、路径、参数组成。这个一般是由Native开发的同学提供,我们前端同学再拿到这个scheme之后,就可以用来打开APP或APP内的某个页面了。

URL Scheme 组成

[scheme:][//authority][path][?query][#fragment]

常用APP的 URL Scheme

APP微信支付宝淘宝QQ知乎
URL Schemeweixin://alipay://taobao://mqq://zhihu://

打开方式

常用的有以下这几种方式

  • 直接通过window.location.href跳转

window.location.href = 'zhihu://'
  • 通过iframe跳转

const iframe = document.createElement('iframe')
iframe.style.display = 'none'
iframe.src = 'zhihu://'
document.body.appendChild(iframe)
  • 直接使用a标签进行跳转

  • 通过js bridge来打开

window.miduBridge.call('openAppByRouter', {url: 'zhihu://'})

判断是否成功唤起

当用户唤起APP失败时,我们希望可以引导用户去进行下载。那么我们怎么才能知道当前APP是否成功唤起呢?

我们可以监听当前页面的visibilitychange事件,如果页面隐藏,则表示唤端成功,否则唤端失败,跳转到应用商店。

OK,我们尝试来实现一下:

首先我手机上并没有安装腾讯微博,所以也就无法唤起,我们让他跳到应用商店对应的应用下载页,这里就用淘宝的下载页来代替一下~

<template>
 <div class="open_app">
     <div class="open_app_title">前端南玖唤端测试Demo</div>
     <div class="open_btn" @click="open">打开腾讯微博</div>
 </div>
</template>

<script>
let timer
export default {
   name: 'openApp',
   methods: {
       watchVisibility() {
           window.addEventListener('visibilitychange', () => {
               // 监听页面visibility
               if(document.hidden) {
                   // 如果页面隐藏了,则表示唤起成功,这时候需要清除下载定时器
                   clearTimeout(timer)
              }
          })
      },
       open() {
           timer = setTimeout(() => {
             // 没找到腾讯微博的下载页,这里暂时以淘宝下载页代替
               window.location.href = 'http://apps.apple.com/cn/app/id387682726'
          }, 3000)
           window.location.href = 'TencentWeibo://'
      }
  }
}
</script>

<style lang="less">
.open_app_title {
   font-size: (20/@rem);
}
.open_btn{
   margin-top:(20/@rem);
   padding:(10/@rem) 0;
   border-radius: (8/@rem);
   background: salmon;
   color: #fff;
   font-size: (16/@rem);
}
</style>


适用性

URL Scheme 这种方式兼容性好,无论安卓或者 iOS 都能支持,是目前最常用的方式。从上图我们能够看出它也有一些比较明显的缺点:

  • 无法准确判断是否唤起成功,因为本质上这种方式就是打开一个链接,并且还不是普通的 http 链接,所以如果用户没有安装对应的 APP,那么尝试跳转后在浏览器中会没有任何反应,通过定时器来引导用户跳到应用商店,但这个定时器的时间又没有准确值,不同手机的唤端时间也不同,我们只能大概的估计一下它的时间来实现,一般设为3000ms左右比较合适;

  • 从上图中我们可以看到会有一个弹窗提示你是否在对应 APP中打开,这就可能会导致用户流失;

  • 有 URL Scheme 劫持风险,比如有一个 app 也向系统注册了 zhihu:// 这个 scheme ,唤起流量可能就会被劫持到这个 app 里;

  • 容易被屏蔽,app 很轻松就可以拦截掉通过 URL Scheme 发起的跳转,比如微信内经常能看到一些被屏蔽的现象。

Universal Link (iOS)

Universal Link 是在iOS 9中新增的功能,使用它可以直接通过https协议的链接来打开 APP。 它相比前一种URL Scheme的优点在于它是使用https协议,所以如果没有唤端成功,那么就会直接打开这个网页,不再需要判断是否唤起成功了。并且使用 Universal Link,不会再弹出是否打开的弹出,对用户来说,唤端的效率更高了。

原理

  • 在 APP 中注册自己要支持的域名;

  • 在自己域名的根目录下配置一个 apple-app-site-association 文件即可。(具体的配置前端同学不用关注,只需与iOS同学确认好支持的域名即可)

打开方式

openByUniversal () {
 // 打开知乎问题页
 window.location.href = 'https://oia.zhihu.com/questions/64966868'
 // oia.zhihu.com
},


适用性

  • 相对 URL Scheme,universal links 有一个较大优点是它唤端时没有弹窗提示是否打开,提升用户体验,可以减少一部分用户流失;

  • 无需关心用户是否安装对应的APP,对于没有安装的用户,点击链接就会直接打开对应的页面,因为它也是http协议的路径,这样也能一定程度解决 URL Scheme 无法准确判断唤端失败的问题;

  • 只能够在iOS上使用

  • 只能由用户主动触发

App Link、Chrome Intents(Android)

App Link

在2015年的Google I/O大会上,Android M宣布了一个新特性:App Links让用户在点击一个普通web链接的时候可以打开指定APP的指定页面,前提是这个APP已经安装并且经过了验证,否则会显示一个打开确认选项的弹出框,只支持Android M以上系统。

App Links的最大的作用,就是可以避免从页面唤醒App时出现的选择浏览器选项框;

前提是必须注册相应的Scheme,就可以实现直接打开关联的App。

  • App links在国内的支持还不够,部分安卓浏览器并不支持跳转至App,而是直接在浏览器上打开对应页面。

  • 系统询问是否打开对应App时,假如用户选择“取消”并且选中了“记住此操作”,那么用户以后就无法再跳转App。

Chrome Intents

  • Chrome Intent 是 Android 设备上 Chrome 浏览器中 URI 方案的深层链接替代品。

  • 如果 APP 已安装,则通过配置的 URI SCHEME 打开 APP。

  • 如果 APP 未安装,配置了 fallback url 的跳转 fallback url,没有配置的则跳转应用市场。

这两种方案在国内的应用都比较少。

方案对比

URL SchemeUniversal LinkApp Link
<ios9支持不支持不支持
>=ios9支持支持不支持
<android6支持不支持不支持
>=android6支持不支持支持
是否需要HTTPS不需要需要需要
是否需要客户端需要需要需要
无对应APP时的现象报错/无反应跳到对应的页面跳到对应的页面

URI Scheme

  • URI Scheme的兼容性是最高,但使用体验相对较差:

  • 当要被唤起的APP没有安装时,这个链接就会出错,页面无反应。

  • 当注册有多个scheme相同的时候,没有办法区分。

  • 不支持从其他app中的UIWebView中跳转到目标APP, 所以ios和android都出现了自己的独有解决方案。

Universal Link

  • 已经安装APP,直接唤起APP;APP没有安装,就会跳去对应的web link。

  • universal Link 是从服务器上查询是哪个APP需要被打开,所以不会存在冲突问题

  • universal Link 支持从其他app中的UIWebView中跳转到目标app

  • 缺点在于会记住用户的选择:在用户点击了Universal link之后,iOS会去检测用户最近一次是选择了直接打开app还是打开网站。一旦用户点击了这个选项,他就会通过safiri打开你的网站。并且在之后的操作中,默认一直延续这个选择,除非用户从你的webpage上通过点击Smart App Banner上的OPEN按钮来打开。

App link

  • 优点与 universal Link 类似

  • 缺点在于国内的支持相对较差,在有的浏览器或者手机ROM中并不能链接至APP,而是在浏览器中打开了对应的链接。

  • 在询问是否用APP打开对应的链接时,如果选择了“取消”并且“记住选择”被勾上,那么下次你再次想链接至APP时就不会有任何反应


作者:南玖
来源:https://juejin.cn/post/7097784616961966094

收起阅读 »

基于react/vue开发一个专属于程序员的朋友圈应用

前言今天本来想开源自己写的CMS应用的,但是由于五一期间笔者的mac电脑突然崩溃了,所有数据无法恢复,导致部分代码丢失,但庆幸的是cms的打包文件已上传服务器,感兴趣的朋友可以在文末链接中访问查看。今天要写的H5朋友圈也是基于笔者开发的cms搭建的,我将仿照微...
继续阅读 »

前言

今天本来想开源自己写的CMS应用的,但是由于五一期间笔者的mac电脑突然崩溃了,所有数据无法恢复,导致部分代码丢失,但庆幸的是cms的打包文件已上传服务器,感兴趣的朋友可以在文末链接中访问查看。

今天要写的H5朋友圈也是基于笔者开发的cms搭建的,我将仿照微信朋友圈,带大家一起开发一个能发布动态(包括图片上传)的朋友圈应用。有关服务端部分笔者在本文中不会细讲,后续会在cms2.0中详细介绍。

你将收获

  • 使用umi快速创建一个H5移动端应用

  • 基于react-lazy-load实现图片/内容懒加载

  • 使用css3基于图片数量动态改变布局

  • 利用FP创建一个朋友圈form

  • 使用rc-viewer查看/旋转/缩放朋友圈图片

  • 基于axios + formdata实现文件上传功能

  • ZXCMS介绍

应用效果预览

朋友圈列表


查看朋友圈图片


发布动态


正文

在开始文章之前,笔者想先粗略总结一下开发H5移动端应用需要考虑的点。对于任何移动端应用来说,我们都要考虑如下问题:

  • 首屏加载时间

  • 适配问题

  • 页面流畅度

  • 动画性能

  • 交互友好

  • 提供用户反馈 这些不仅仅是前端工程师需要考虑的问题,也是产品经理和交互设计师考虑的范畴。当然还有很多实际的考虑点需要根据自身需求去优化,以上几点大致解决方案如下:

  1. 提高首屏加载时间 可以采用资源懒加载+gzip+静态资源CDN来优化,并且提供加载动画来降低用户焦虑。

  2. 适配问题 移动端适配问题可以通过js动态设置视口宽度/比率或者采用css媒介查询来处理,这块市面上已经有非常成熟的方案

  3. 页面流畅度 我们可以在body上设置-webkit-overflow-scrolling:touch;来提高滚动流畅度,并且可以在a/img标签上使用 -webkit-touch-callout: none来禁止长按产生菜单栏。

  4. 动画性能 为了提高动画性能, 我们可以将需要变化的属性采用transform或者使用absolute定位代替,transform不会导致页面重绘。

  5. 提供用户反馈 提供友好的用户反馈我们可以通过合理设置toastmodal等来控制

以上介绍的只是移动端优化的凤毛麟角,有关前端页面性能优化的方案还有很多,笔者在之前的文章中也详细介绍过,下面我们进入正文。

1. 使用umi快速创建一个应用

笔者将采用umi作为项目的前端集成解决方案,其提供了非常多了功能,使用起来也非常方便,并且对于antd和antd-mobile自动做了按需导入,所以熟悉react的朋友可以尝试一下,本文的方案对于vue选手来说也是适用的,因为任何场景下,方法和思维模式都是跨语言跨框架的。

目前umi已经升级到3.0,本文所使用的是2.0,不过差异不是很大,大家可以放心使用3.0. 具体使用步骤如下

// umi2.0
// 新建项目目录
mkdir friendcircle
// 创建umi项目
cd friendcircle
yarn create umi
// 安装依赖
yarn
yarn add antd-moblie

这样一个umi项目就创建好了。

2. 基于react-lazy-load实现图片/内容懒加载

在项目创建好之后,我们先分析我们需要用到那些技术点:


笔者在设计时研究了很多懒加载实现方式,目前采用react-lazy-load来实现,好处是支持加载事件通知,比如我们需要做埋点或者广告上报等功能时非常方便。当然大家也可以自己通过observer API去实现,具体实现方案笔者在几个非常有意思的javascript知识点总结文章中有所介绍。 具体使用方式:

<LazyLoad key={item.uid} overflow height={280} onContentVisible={onContentVisible}>
  // 需要懒加载的组件
  <ComponentA />
</LazyLoad>

react-lazy-load使用方式非常简单,大家不懂的可以在官网学习了解。

3. 使用css3基于图片数量动态改变布局

目前在朋友圈列表页有个核心的需求就是我们需要在用户传入不同数量的图片时,要有不同的布局,就像微信朋友圈一样,主要作用就是为了让用户尽可能多的看到图片,提高用户体验,如下图所示例子:


我们用js实现起来很方便,但是对性能及其不友好,而且对于用户发布的每一条动态的图片都需要用js重新计算一遍,作为一个有追求的程序员是不可能让这种情况发生的,所以我们用css3来实现,其实有关这种实现方式笔者在之前的css3高级技巧的文章中有详细介绍,我们这里用到了子节点选择器,具体实现如下:

.imgItem {
  margin-right: 6px;
  margin-bottom: 10px;
  &:nth-last-child(1):first-child {
    margin-right: 0;
    width: 100%;
  }
  &:nth-last-child(2):first-child,
  &:nth-last-child(3):first-child,
  &:nth-last-child(4):first-child,
  &:first-child:nth-last-child(n+2) ~ div {
    width:calc(50% - 6px);
    height: 200px;
    overflow: hidden;
  }
  &:first-child:nth-last-child(n+5),
  &:first-child:nth-last-child(n+5) ~ div {
    width: calc(33.33333% - 6px);
    height: 150px;
    overflow: hidden;
  }
}

以上代码中我们对于一张图片,2-4张图片,5张以上的图片分别设置了不同的尺寸,这样就可以实现我们的需求了,还有一个要注意的是,当用户上传不同尺寸的图片时,有可能出现高低不一致的情况,这个时候为了显示一致,我们可以使用img样式中的object-fit属性,有点类似于background-size,我们可以把img便签看作一个容器,里面的内容如何填充这个容器,完全用object-fit来设置,具体属性如下:

  • fill 被替换的内容正好填充元素的内容框。整个对象将完全填充此框。如果对象的宽高比与内容框不相匹配,那么该对象将被拉伸以适应内容框

  • contain 被替换的内容将被缩放,以在填充元素的内容框时保持其宽高比。 整个对象在填充盒子的同时保留其长宽比,因此如果宽高比与框的宽高比不匹配,该对象将被添加“黑边”

  • cover 被替换的内容在保持其宽高比的同时填充元素的整个内容框。如果对象的宽高比与内容框不相匹配,该对象将被剪裁以适应内容框

  • scale-down 内容的尺寸与 none 或 contain 中的一个相同,取决于它们两个之间谁得到的对象尺寸会更小一些

  • none 被替换的内容将保持其原有的尺寸

所以为了让图片保持一致,我们这么设置img标签的样式:

img {
width: 100%;
height: 100%;
object-fit: cover;
}

4. 利用FP创建一个朋友圈form

FP是笔者开源的一个表单配置平台,主要用来定制和分析各种表单模型,界面如下:



通过该平台可以定制各种表单模版并分析表单数据。这里朋友圈功能我们只需要配置一个简单的朋友圈发布功能即可,如下:


由于笔者电脑数据丢失导致代码部分损失,感兴趣可以了解一下。

5. 使用rc-viewer查看/旋转/缩放朋友圈图片

对于朋友圈另一个重要的功能就是能查看每一条动态的图片,类似于微信朋友圈的图片查看器,这里笔者采用第三方开源库rc-viewer来实现,具体代码如下:

<RcViewer options={{title: 0, navbar: 0, toolbar: 0}} ref={imgViewRef}>
<div className={styles.imgBox}>
  {
    item.imgUrls.map((item, i) => {
      return <div className={styles.imgItem} key={i}>
        <img src={item} alt=""/>
      </div>
    })
  }
</div>  
</RcViewer>

由上代码可知我们只需要在RcViewer组件里写我们需要的查看的图片结构就行了,其提供了很多配置选项可是使用,这里笔者在option中配置了title,navbar,toolbar均为0,意思是不显示这些功能,因为移动端只需要有基本的查看,缩放,切换图片功能即可,尽可能轻量化。效果如下:


当我们点击动态中的某一张图片时,我们可以看到它的大图,并通过手势进行切换。

6. 基于axios + formdata实现文件上传功能

实现文件上传,除了采用antd的upload组件,我们也可以结合http请求库和formdata来实现,为了支持多图上传并保证时机,我们采用async await函数,具体代码如下:

const onSubmit = async () => {
  // ... something code
  const formData = new FormData()
  for(let i=0; i< files.length; i++) {
    formData.delete('file')
    formData.append('file', files[i].file)
    try{
      const res = await req({
        method: 'post',
        url: '/files/upload/tx',
        data: formData,
        headers: {
            'Content-Type': 'multipart/form-data'
        }
      });
      // ... something co
    }catch(err) {
      Toast.fail('上传失败', 2);
    }
  }

其中req是笔者基于axios封装的http请求库,支持简单的请求/响应拦截,感兴趣的朋友可以参考笔者源码。

7. ZXCMS介绍

ZXCMS是笔者开发的一个商业版CMS,可以快速搭建自己的社区,博客等,并且集成了表单定制平台,配置中心,数据分发中心等功能,后期会扩展H5可视化搭建平台和PC端建站平台,成为一个更加只能强大的开源系统。设计架构如下:


具体界面如下:

一个笔者配置的社区平台:


文章详情页:



社区支持评论,搜索文章等功能。以下介绍后台管理系统:





简单介绍一下,后期笔者会专门出文章介绍具体实现方式和源码设计。

8. 源码地址

由于笔者电脑数据丢失,只能找到部分源码,所以大家可以参考以下地址:

开源不易,欢迎支持~


作者:徐小夕
来源:https://juejin.cn/post/6844904150417801224

收起阅读 »

如何利用performance进行性能优化

可以记录站点在运行过程中的性能数据,有了这些性能数据,就可以回放整个页面的执行过程,这样就方便我们来定位和诊断每个时间段内页面的运行情况,从而有效的找出页面的性能瓶颈。各种配置及说明如图所示: 观察下图的报告页,我们可以将它分为三个主要的部分,分别为概览面板、...
继续阅读 »

Performance 可以记录站点在运行过程中的性能数据,有了这些性能数据,就可以回放整个页面的执行过程,这样就方便我们来定位和诊断每个时间段内页面的运行情况,从而有效的找出页面的性能瓶颈。

配置 Performance

各种配置及说明如图所示:20210430104643828.png


Performance 不仅可以录制加载阶段的性能数据,还可以录制交互阶段,不过交互阶段的录制需要手动停止录制过程。

观察下图的报告页,我们可以将它分为三个主要的部分,分别为概览面板、性能指标面板和详情面板。image.png


在概览面板中,Performance 就会将几个关键指标,诸如页面帧速 (FPS)、CPU 资源消耗、网络请求流量、V8 内存使用量 (堆内存) 等,按照时间顺序做成图表的形式展现出来,可以参看上图。

  • 如果 FPS 图表上出现了红色块,那么就表示红色块附近渲染出一帧所需时间过久,帧的渲染时间过久,就有可能导致页面卡顿。

  • 如果 CPU 图形占用面积太大,表示 CPU 使用率就越高,那么就有可能因为某个 JavaScript 占用太多的主线程时间,从而影响其他任务的执行。

除了以上指标以外,概览面板还展示加载过程中的几个关键时间节点,如 FP、LCP、DOMContentLoaded、Onload 等事件产生的时间点。

Main 指标

在性能面板中,记录了非常多的性能指标项,比如 Main 指标记录渲染主线程的任务执行过程,Compositor 指标记录了合成线程的任务执行过程,GPU 指标记录了 GPU 进程主线程的任务执行过程。有了这些详细的性能数据,就可以帮助我们轻松地定位到页面的性能问题。

简而言之,我们通过概览面板来定位问题的时间节点,然后再使用性能面板分析该时间节点内的性能数据。具体地讲,比如概览面板中的 FPS 图表中出现了红色块,那么我们点击该红色块,性能面板就定位到该红色块的时间节点内了。

因为浏览器的渲染机制过于复杂,所以渲染模块在执行渲染的过程中会被划分为很多子阶段,输入的 HTML 数据经过这些子阶段,最后输出屏幕上的像素,我们把这样的一个处理流程叫做渲染流水线。一条完整的渲染流水线包括了解析 HTML 文件生成 DOM、解析 CSS 生成 CSSOM、执行 JavaScript、样式计算、构造布局树、准备绘制列表、光栅化、合成、显示等一系列操作。

渲染流水线主要是在渲染进程中执行的,在执行渲染流水线的过程中,渲染进程又需要网络进程、浏览器进程、GPU 等进程配合,才能完成如此复杂的任务。另外在渲染进程内部,又有很多线程来相互配合。具体的工作方式你可以参考下图:image.pngimage.png


观察上图,一段段横条代表执行一个个任务,长度越长,花费的时间越多;竖向代表该任务的执行记录。我们知道主线程上跑了特别多的任务,诸如渲染流水线的大部分流程,JavaScript 执行、V8 的垃圾回收、定时器设置的回调任务等等,因此 Main 指标的内容非常多,而且非常重要,所以我们在使用 Perofrmance 的时候,大部分时间都是在分析 Main 指标。

任务 vs 过程

渲染进程中维护了消息队列,如果通过 SetTimeout 设置的回调函数,通过鼠标点击的消息事件,都会以任务的形式添加消息队列中,然后任务调度器会按照一定规则从消息队列中取出合适的任务,并让其在渲染主线程上执行。

Main 指标就记录渲染主线上所执行的全部任务,以及每个任务的详细执行过程image.png


观察上图,图上方有很多一段一段灰色横条,每个灰色横条就对应了一个任务,灰色长条的长度对应了任务的执行时长。通常,渲染主线程上的任务都是比较复杂的,如果只单纯记录任务执行的时长,那么依然很难定位问题,因此,还需要将任务执行过程中的一些关键的细节记录下来,这些细节就是任务的过程,灰线下面的横条就是一个个过程,同样这些横条的长度就代表这些过程执行的时长。

直观地理解,你可以把任务看成是一个 Task 函数,在执行 Task 函数的过程中,它会调用一系列的子函数,这些子函数就是我们所提到的过程。为了让你更好地理解,我们来分析下面这个任务的图形:image.png


观察上面这个任务记录的图形,你可以把该图形看成是下面 Task 函数的执行过程:  

function A(){
A1()
A2()
}
function Task(){
A()
B()
}
Task()  

分析页面加载过程

结合 Main 指标来分析页面的加载过程。先来分析一个简单的页面,代码如下所示:

<html>
<head>
<title>Main</title>
<style>
area {
border: 2px ridge;
}
box {
background-color: rgba(106, 24, 238, 0.26);
height: 5em;
margin: 1em;
width: 5em;
}
</style>
</head>

<body>
<div class="area">
<div class="box rAF"></div>
</div>
<br>
<script>
function setNewArea() {
let el = document.createElement('div')
el.setAttribute('class', 'area')
el.innerHTML = '<div class="box rAF"></div>'
document.body.append(el)
}
setNewArea()
</script>
</body>
</html>

可以看出,它只是包含了一段 CSS 样式和一段 JavaScript 内嵌代码,其中在 JavaScript 中还执行了 DOM 操作了,我们就结合这段代码来分析页面的加载流程。

首先生成报告页,再观察报告页中的 Main 指标,由于阅读实际指标比较费劲,所以先手动绘制了一些关键的任务和其执行过程,如下图所示:image.png


通过上面的图形我们可以看出,加载过程主要分为三个阶段,它们分别是:

  • 导航阶段,该阶段主要是从网络进程接收 HTML 响应头和 HTML 响应体。

  • 解析 HTML 数据阶段,该阶段主要是将接收到的 HTML 数据转换为 DOM 和 CSSOM。

  • 生成可显示的位图阶段,该阶段主要是利用 DOM 和 CSSOM,经过计算布局、生成层树 (LayerTree)、生成绘制列表 (Paint)、完成合成等操作,生成最终的图片。

那么接下来,我就按照这三个步骤来介绍如何解读 Main 指标上的数据。

导航阶段

当你点击了 Performance 上的重新录制按钮之后,浏览器进程会通知网络进程去请求对应的 URL 资源;一旦网络进程从服务器接收到 URL 的响应头,便立即判断该响应头中的 content-type 字段是否属于 text/html 类型;如果是,那么浏览器进程会让当前的页面执行退出前的清理操作,比如执行 JavaScript 中的 beforunload 事件,清理操作执行结束之后就准备显示新页面了,这包括了解析、布局、合成、显示等一系列操作。image.png


当你点击重新加载按钮后,当前的页面会执行上图中的这个任务:

  • 该任务的第一个子过程就是 Send request,该过程表示网络请求已被发送。然后该任务进入了等待状态。

  • 接着由网络进程负责下载资源,当接收到响应头的时候,该任务便执行 Receive Respone 过程,该过程表示接收到 HTTP 的响应头了。

  • 接着执行 DOM 事件:pagehide、visibilitychange 和 unload 等事件,如果你注册了这些事件的回调函数,那么这些回调函数会依次在该任务中被调用。

  • 这些事件被处理完成之后,那么接下来就接收 HTML 数据了,这体现在了 Recive Data 过程,Recive Data 过程表示请求的数据已被接收,如果 HTML 数据过多,会存在多个 Receive Data 过程。

  • 等到所有的数据都接收完成之后,渲染进程会触发另外一个任务,该任务主要执行 Finish load 过程,该过程表示网络请求已经完成。


解析 HTML 数据阶段

这个阶段的主要任务就是通过解析 HTML 数据、解析 CSS 数据、执行 JavaScript 来生成 DOM 和 CSSOM。那么继续来分析这个阶段的图形,看看它到底是怎么执行的?可以观看下图:image.png


观察上图这个图形,可以看出,其中一个主要的过程是 HTMLParser,顾名思义,这个过程是用来解析 HTML 文件,解析的就是上个阶段接收到的 HTML 数据。

  1. 在 ParserHTML 的过程中,如果解析到了 script 标签,那么便进入了脚本执行过程,也就是图中的 Evalute Script。

  2. 要执行一段脚本我们需要首先编译该脚本,于是在 Evalute Script 过程中,先进入了脚本编译过程,也就是图中的 Complie Script。脚本编译好之后,就进入程序执行过程,执行全局代码时,V8 会先构造一个 anonymous 过程,在执行 anonymous 过程中,会调用 setNewArea 过程,setNewArea 过程中又调用了 createElement,由于之后调用了 document.append 方法,该方法会触发 DOM 内容的修改,所以又强制执行了 ParserHTML 过程生成的新的 DOM。

  3. DOM 生成完成之后,会触发相关的 DOM 事件,比如典型的 DOMContentLoaded,还有 readyStateChanged。

生成可显示位图阶段

生成了 DOM 和 CSSOM 之后,就进入了第三个阶段:生成页面上的位图。通常这需要经历布局 (Layout)、分层、绘制、合成等一系列操作,同样,将第三个阶段的流程也放大了,如下图所示:


image.png


结合上图,我们可以发现,在生成完了 DOM 和 CSSOM 之后,渲染主线程首先执行了一些 DOM 事件,诸如 readyStateChange、load、pageshow。具体地讲,如果你使用 JavaScript 监听了这些事件,那么这些监听的函数会被渲染主线程依次调用。

接下来就正式进入显示流程了,大致过程如下所示。

  1. 首先执行布局,这个过程对应图中的 Layout。

  2. 然后更新层树 (LayerTree),这个过程对应图中的 Update LayerTree。

  3. 有了层树之后,就需要为层树中的每一层准备绘制列表了,这个过程就称为 Paint。

  4. 准备每层的绘制列表之后,就需要利用绘制列表来生成相应图层的位图了,这个过程对应图中的 Composite Layers。

走到了 Composite Layers 这步,主线程的任务就完成了,接下来主线程会将合成的任务完全教给合成线程来执行,下面是具体的过程,你也可以对照着 Composite、Raster 和 GPU 这三个指标来分析,参考下图:


image.png

  1. 首先主线程执行到 Composite Layers 过程之后,便会将绘制列表等信息提交给合成线程,合成线程的执行记录你可以通过 Compositor 指标来查看。

  2. 合成线程维护了一个 Raster 线程池,线程池中的每个线程称为 Rasterize,用来执行光栅化操作,对应的任务就是 Rasterize Paint。

  3. 当然光栅化操作并不是在 Rasterize 线程中直接执行的,而是在 GPU 进程中执行的,因此 Rasterize 线程需要和 GPU 线程保持通信。

  4. 然后 GPU 生成图像,最终这些图层会被提交给浏览器进程,浏览器进程将其合成并最终显示在页面上。

本文解答了个人一个长期困扰的问题:在某些情况下,比如网速比较慢或者页面内容很多的时候,页面是一点一点的显示出来的,原本以为是网络数据是加载一点就渲染一点,其实不是的,数据在导航阶段就已经全部获取回来了。之所以会慢慢渲染出来,是因为浏览器的显示频率是60hz,也就是16.67ms就刷新下浏览器,但是在16.67ms内,渲染流水线可能只进行到一半,但是这个时候也要把渲染一半的画面显示出来,所以就会看到页面是一点一点的绘制出来的。


作者:小p
来源:juejin.cn/post/7095647383488299044
收起阅读 »

Today,我们不聊技术,聊聊前端发展

今天是2022年04月26日,一年已经过去三分之一。 掘金里面有很多的技术文章,每一位前端工程师都在这里展现自己的技术水平。有很多时候,我看见很多的技术文章,里面大致上的内容其实都是差不多的,总的来说,其实普通的前端工程师是用不到去学习这么多的技术点的。就比如...
继续阅读 »

今天是2022年04月26日,一年已经过去三分之一。


掘金里面有很多的技术文章,每一位前端工程师都在这里展现自己的技术水平。有很多时候,我看见很多的技术文章,里面大致上的内容其实都是差不多的,总的来说,其实普通的前端工程师是用不到去学习这么多的技术点的。就比如Node.js 。 一般的公司也不会用JavaScript语言来写后端,所以大部分的前端甚至都不需要去了解它,反而更应该了解多一点Ajax与网络请求协议。数据的问题交给后端去处理就好了,前端有自己要做的活。


我个人认为,技术框架的源码这种东西,如果能不学习,就不要去深入的学习了。很多人其实是没有达到进大厂的门槛的,大部分的前端其实都达不到,而一些中小型的公司,一般也不会去问一个技术架构的源码及核心问题(绝大部分),因为中小型公司需要的是能干活的人,而大部分的项目业务,其实还没有说你不懂源码就做不了的程度。总的来说就是只要你能干活,你懂什么是你自己的事儿,我就给这么多钱,这些项目你能干就来,你做不了我就辞退你。


其实大部分的前端,只要有请求到后端的接口,然后能把后端接口的数据处理好,并渲染到页面上就可以了。然后一些不懂的问题,一些复杂的功能模块,其实你一百度,基本上都能解决问题,如果你百度都不能解决的问题,那不是百度解决不了,而是你的项目本身就是有问题的。这里面说的是绝大部分的情况,当然也有一些奇怪的例子,这种只是占少部分。


其实我们前端的活总体来说都不难,就好比开车,其实绝大部分人都会开车,但是要想要把车技提升上去,那就需要去学习了,如果说你只是为了通勤,那么很多时候,你都不需要去提升你的车技。你只需要懂得怎么启动,怎么刹车等一些基本的操作就行了(实在不行就百度)。


前端往后的生态


其实前端往后也不会有什么太大的变化,基本上就定型了。像网上说的什么新技术啊,新方向什么的,其实很多都会不了了之,因为在没有发生技术变革的年代,我们想要去改变一些东西是很难的。我们很多人其实都是需要去等待,等待那个奇点的到来。没有很大的改变,其实都只能这样子。就好比我知道的,在网络请求中,其实有很大部分资源都浪费在了一些协议上,而这些协议的束缚,导致了我们的网络传输会消耗掉三分之一的性能,这种问题是历史遗留问题,虽然现在已经有很多方法能够解决掉这个性能消耗问题,但是解决这个问题需要互联网的企业把旧机器换成新机器,而新机器的成本又高于网络传输消耗的成本,所以我们普通人只能这样去无端的消耗掉这些资源,又或者等待那个奇点的到来。


说到设备又不得不提现如今的大部分互联网用户,在现在的互联网,其实绝大部分用户的设备性能已经是非常高了,而我们缺还有的人说在项目做一些性能优化问题,其实有时候,这种优化是无意义的,还不如不去做这种优化。当然这种场景也是区分项目的体验人的年龄段,如果项目主要服务于年轻人,其实年轻人的设备性能说不定比我们自己的设备都好,你的优化起不到太大的作用。如果项目主要服务于老年人,其实这个时候需要思考的不是设备性能优化的问题,反而更需要注重项目体验上的问题,就是怎么简单怎么来,别让老人觉得用你的东西太麻烦。


我所期待的前端世界


随着电子产品的更新换代,设备的性能越来越好,用户的CUP跑得越来越快,我们可以在我们的前端项目中放更多的新颖东西,比如把项目变革为3D场景,让用户在体验产品时,如同进入一个真实的虚拟世界(希望这一天不会超过50年)web3D值得期待。还有就是网页端游戏,现在绝大部分游戏都是部署在用户的设备中,而每个人的设备存放1个G,那一百个人, 就会有100个G的文件是存在重复,如果一款游戏,能把他部署在服务器上面,而用户只需要进入到网页中就可以体验,那真的是非常令人期待。


END


其实这些都是我瞎写的,没有什么值得看的地方,各位看官就当做是一个笑话,如果觉得有意思,麻烦点个赞。谢谢


作者:无我上青云
链接:https://juejin.cn/post/7090725867441258503
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

前端单点登录实现

通过token校验登录信息前端单点存储方式共享本地存储数据token值,token存储方式用的是localStorage 或 sessionStorage,由于这两种都会受到同源策略限制。跨域存储想要实现跨域存储,先找到一种可跨域通信的机制,就是 iframe...
继续阅读 »

通过token校验登录信息

前端单点存储方式

共享本地存储数据token值,token存储方式用的是localStoragesessionStorage,由于这两种都会受到同源策略限制。

跨域存储

想要实现跨域存储,先找到一种可跨域通信的机制,就是 iframe postMessage,它可以安全的实现跨域通信,不受同源策略限制(后端要修改配置允许iframe打开其他域的地址)。

cross-storage.js(开源库)

原理是用 postMessage 可跨域特性,来实现跨域存储。因为多个不同域下的页面无法共享本地存储数据,我们需要找个“中转页面”来统一处理其它页面的存储数据


前端后端通讯

多平台入口页=》某平台中转页=》平台首页

平台中转页

主要将其他平台的token 转成当前平台的信任token值

/** 单点登录获取票据 */
export async function getTicket(token) {
return request('/getTicket', {
  method: 'GET',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'x-access-token': token },
});
}
/**免登录 */
export async function singleLogin(data) {
return request('/singleLogin', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'x-access-token': token   },
  data
});
}

export default (props: any) => {
const _singleLogin = async () => {
  try {
    //根据本地token 获取票据 getTicket 通过localStorage.getItem("token")
    const { code, data } = await getTicket(token);
    //免登录成功后跳转页面
    const link = '/home';
    if (code !== 200 || !data) {
      window.location.href = link;
      return;
    }
    //免登接口 获取登录token值
    const res: any = await singleLogin({
      ticket: data,
      source: '',//平台来源
    });
    if (res?.code === 200) {
      localStorage.setItem('tokneKey', res?.data.tokneKey);
      localStorage.setItem('tokenValue', res?.data.tokenValue);
    } else {
      console.log(res?.msg);
      localStorage.removeItem('tokneKey');
      localStorage.removeItem('tokenValue');
    }
    window.location.href = link;
  } catch (e) {
    window.location.href = link;
  }
};
useEffect(() => {
  _singleLogin();
});

return (
  <div style={{ width: '100%', height: '100%', justifyContent: 'center', alignItems: 'center', display: 'flex' }}>
    <Spin spinning={loading}></Spin>
  </div>
);
};


作者:NeverSettle_
来源:https://juejin.cn/post/7021407926837313544

收起阅读 »

关于防抖函数的思考

防抖概念本质:是优化高频率执行代码的一种手段。防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时。好处:能够保证用户在频繁触发某些事件的时候,不会频繁的执行回调,只会被执行一次。一个经典的比喻:想象每天上班大厦底下的电梯。把电梯完成一次运送,类...
继续阅读 »

防抖概念

本质:是优化高频率执行代码的一种手段。

防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时。

好处:能够保证用户在频繁触发某些事件的时候,不会频繁的执行回调,只会被执行一次。

一个经典的比喻:

想象每天上班大厦底下的电梯。把电梯完成一次运送,类比为一次函数的执行和响应。

电梯第一个人进来后,等待15秒。如果过程中又有人进来,15秒等待重新计时,直到15秒后开始运送,这就是防抖策略(debounce)。

用于测试的HTML结构

实现效果:鼠标在盒子上移动时,盒子中央打印出数字。



      //未实现防抖时的测试代码
     const container = document.querySelector('#container')
     let count = 0
     function move(e) {
       container.innerHTML = count++
       console.log(this)
       console.log(e)
    }
     container.addEventListener('mousemove', move)

未实现防抖时对应的页面效果如下:


     //实现防抖后的测试代码
     const container = document.querySelector('#container')
     let count = 0
     function move(e) {
       container.innerHTML = count++
       console.log(this)
       console.log(e)
    }
     const test = debounce(move, 500, true)
     container.addEventListener('mousemove', test)
     const btn = document.querySelector('button')
     btn.onclick = function () {
       test.cancel()
    }

实现防抖后对应的页面效果如下:


接下来记录我一步步思考完善的过程。

v1.0 简单实现一个防抖(非立即执行版本)

function debounce(func, delay) {
 let timeout
 return function () {
   if (timeout) clearTimeout(timeout)
   timeout = setTimeout(func, delay)
}
}

问题探讨:发现打印出来的this是window,打印出来的e是undefined。实际想要得到的是div#container和mouseEvent。出现这种情况的原因:在container的鼠标移动事件调用debounce函数时,在传递给形参func的实参move里打印了this与e。注意move是在定时器setTimeout里,定时器里的this在非严格模式下指向的是window对象,而window对象里的e自然是undefined。解决办法是在return的function里保存this与arguments,通过apply改变func的this指向同时把保存的参数传递给func。

v2.0 解决了this指向和event对象的问题。

function debounce(func, delay) {
 let timeout
 return function () {
   const context = this,
     args = arguments
   if (timeout) clearTimeout(timeout)
   timeout = setTimeout(function () {
     func.apply(context, args)
  }, delay)
}
}

问题探讨:发现第一次不能立即执行,需要等到delay秒以后才会执行第一次。

v3.0 立即执行版本

function debounce(func, delay) {
 let timeout
 return function () {
   const context = this,
     args = arguments,
     callNow = !timeout
   if (timeout) clearTimeout(timeout)
   timeout = setTimeout(function () {
     timeout = null
  }, delay)
   if (callNow) func.apply(context, args)
}
}

Q:为什么利用callNow = !timeout来判断?而不是用callNow = true,然后在定时器内将callNow设置为false?

首先解答为什么不能用布尔值来判断。因为定时器是异步任务,在delay时间段内,callNow始终为true,这就会导致func在delay时间段内会一直触发,直到时间到达delay,callNow变成false才会停止执行func。

再回到为什么可以利用callNow = !timeout来判断的问题上。在首次触发mousemove事件时,'let timeout'执行,此时timeout为undefined;callNow对timeout取反为true;因为此时timeout为undefined,跳过清除定时器操作;把定时器赋值给timeout,注意此时timeout保存的值是1(第一个定时器的id),但是定时器是异步任务,里面的'timeout = null'尚未执行;接下来判断callNow为true,执行func函数,达到了立即执行的效果。在delay秒内第二次移动鼠标,此时timeout保存的值为1,callNow取反为false;清除上一个id为1的定时器;timeout保存值2(id为2的定时器),判断callNow为false,不执行func;反之如果等到delay秒后第二次移动鼠标,此时异步任务已执行,timeout变为null,callNow取反为true,就会执行func。注意点:这里利用了闭包,timeout是可以被访问的。

问题探讨:可以通过传入一个参数来判断实际业务需求是要立即执行还是非立即执行。

v4.0 立即执行与非立即执行结合版本(immediate为true时立即执行,反之非立即执行)

function debounce(func, delay, immediate) {
 let timeout
 return function () {
   const context = this,
     args = arguments
   if (timeout) clearTimeout(timeout)
   if (immediate) {
     const callNow = !timeout
     timeout = setTimeout(function () {
       timeout = null
    }, delay)
     if (callNow) func.apply(context, args)
  } else {
     timeout = setTimeout(function () {
       func.apply(context, args)
    }, delay)
  }
}
}

问题探讨:继续完善,如果需要获得func函数的返回值该怎么办呢?那就需要把func的执行结果保存为一个result变量return出来。由此又引出了一个问题,setTimeout是一个异步任务,return时获得的是undefined,只有在立即执行的情况下会获得返回值(immediate为true时)。

v5.0 包含返回值的版本

function debounce(func, delay, immediate) {
 let result, timeout
 return function () {
   const context = this,
     args = arguments
   if (timeout) clearTimeout(timeout)
   if (immediate) {
     const callNow = !timeout
     timeout = setTimeout(function () {
       timeout = null
    }, delay)
     if (callNow) result = func.apply(context, args)
  } else {
     timeout = setTimeout(function () {
       func.apply(context, args)
    }, delay)
  }
   return result
}
}

问题探讨:当delay设置时间过长时(比如30秒甚至更长),我只有等到delay时间过后才能再次触发,如果可以把取消防抖绑定在一个按钮上,点击之后可以立即执行代码。需要考虑的问题是:可以把这个功能做成是debounce的一个cancel方法,因为函数也是一个对象。具体实现思路应该是把原先return出来的函数用一个变量debounced保存,然后再定义debounced.cancel,赋值为一个函数。

v6.0 包含取消功能的版本

function debounce(func, delay, immediate) {
 let timeout, result
 const debounced = function () {
   const context = this,
     args = arguments
   if (timeout) clearTimeout(timeout)
   if (immediate) {
     const callNow = !timeout
     timeout = setTimeout(function () {
       timeout = null
    }, delay)
     if (callNow) result = func.apply(context, args)
  } else {
     timeout = setTimeout(function () {
       func.apply(context, args)
    }, delay)
  }
   return result
}
 debounced.cancel = function () {
   if (timeout) clearTimeout(timeout)
   //需要注意,这里的目的并不是为了避免内存泄漏!而是为了让取消后鼠标再次移入盒子能立即执行代码。如果不置空,取消过后再移入,是不会立即执行打印数字的操作的。
   timeout = null
}
 return debounced
}


v7.0 ES6箭头函数版本(省略了this指向与参数对象的版本)

function debounce(func, delay, immediate) {
 let timeout, result
 //注意下面的函数声明不能改成箭头函数,否则this会指向window
 const debounced = function () {
   if (timeout) clearTimeout(timeout)
   if (immediate) {
     const callNow = !timeout
     timeout = setTimeout(() => {
       timeout = null
    }, delay)
     if (callNow) result = func.apply(this, arguments)
  } else {
     timeout = setTimeout(() => {
       func.apply(this, arguments)
    }, delay)
  }
   return result
}
 debounced.cancel = () => {
   if (timeout) clearTimeout(timeout)
   timeout = null
}
 return debounced
}

作者:GreyJiangy
来源:https://juejin.cn/post/7093466427805401118

收起阅读 »

仅用了81行代码,实现一个简易打包器

最近打算跳槽到大厂,webpack打包流程必须了解,于是尝试一下手写一个打包器1. 3个js文件index.js -> 依赖 subtraction.js => 依赖 sum.js2. 5个npm依赖包代码const path = require(...
继续阅读 »

前言

最近打算跳槽到大厂,webpack打包流程必须了解,于是尝试一下手写一个打包器

准备工作

1. 3个js文件

index.js -> 依赖 subtraction.js => 依赖 sum.js


2. 5个npm依赖包


代码

const path = require("path")
const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default
const fs = require("fs")
const { transformFromAst } = require("babel-core")
const config = {
   entry: "./src/index.js",
   output: {
       path: "./src/",
       filename: "build.js",
  },
}
const { output } = config
let id = 0
const createAsset = (entryFile) => {
   // 读取文件
   const source = fs.readFileSync(entryFile, "utf-8")
   // 代码转为ast,为了转换成ES5
   const ast = parser.parse(source, {
       sourceType: "module",
  })
   const dependents = {}
   // 借用traverse提取文件import的依赖
   traverse(ast, {
       ImportDeclaration({ node }) {
           dependents[node.source.value] = node.source.value
      },
  })
   // es6语法转es5
   const { code } = transformFromAst(ast, null, {
       presets: ["env"],
  })
   return {
       entryFile,
       dependents,
       code,
       id: id++,
       mapping: {},
  }
}
const createGraph = (rootPath) => {
   // 从根路径出发,获取所有与根路径相关依赖存放到modules中
   const mainAsset = createAsset(rootPath)
   const modules = [mainAsset]
   const dirname = path.dirname(rootPath)
   for (let asset of modules) {
       const { dependents } = asset
       for (let dep in dependents) {
           const childPath = path.join(dirname, dependents[dep])
           const childAsset = createAsset(childPath)
           asset.mapping[dependents[dep]] = childAsset.id
           modules.push(childAsset)
      }
  }
   return modules
}
// 转换一下数据结构
const createModules = (graph) => {
   const obj = {}
   graph.forEach((item) => {
       obj[item.id] = [item.code, item.mapping]
  })
   return obj
}
// 生成文件
const writeFiles = (modules) => {
   // 编译模板,modules是不固定的,其他都一样
   const bundle = `
   ;(function (modules) {
       const require = (id) => {
           const [code, mapping] = modules[id]
           const exports = {}
           ;(function (_require, exports, code, mapping) {
               const require = (path) => {
                   return _require(mapping[path])
               }
               eval(code)
           })(require, exports, code, mapping)
           return exports
       }
       require(0)
   })(${JSON.stringify(modules)})
   `
   // 生成文件
   const filePath = path.join(output.path, output.filename)
   fs.writeFileSync(filePath, bundle, "utf-8")
}
const graph = createGraph(config.entry)
const modules = createModules(graph)
writeFiles(modules)


作者:SYX
来源:juejin.cn/post/7091225169120722952

收起阅读 »

一种emoji表情判断方法

Emoji表情输入 常用的utf8编码,最多只会达到3字节,如MySQL的utf8编码。但像emoji表情等Unicode是4字节的(UCS-4),在编码为utf8时,也会占用4字节。在MySQL中,就要使用utf8mb4(most bytes 4)编码,否则...
继续阅读 »

image.png


Emoji表情输入


常用的utf8编码,最多只会达到3字节,如MySQL的utf8编码。但像emoji表情等Unicode是4字节的(UCS-4),在编码为utf8时,也会占用4字节。在MySQL中,就要使用utf8mb4(most bytes 4)编码,否则插入时会报错。


在某些场景下,我们并不希望文本中出现emoji表情等非常用字符,那么如何过滤呢?

对于字符过滤,一般我们第一个想到的大多是正则表达式。然而,实际使用中,由于emoji表情的不断增加或正则表达式本身的缺陷,往往达不到过滤的效果。


image.png


发现问题



欢迎来到王者荣耀😊😊



字符数量10,字符串长度12


一次开发中,使用了el-input的字符数统计属性show-word-limit,发现输入emoji表情统计到的字符数量和实际看到的字符数量不一致。

然后,尝试通过字符串分割成数组,再比较长度,发现str.split('')得到的数组长度和统计到的字符数是一样的,但是和肉眼看到的字符数量还是不一致。


var str = '欢迎来到王者荣耀😊😊'
var arr = str.split('')
console.log(str.length) // 12
console.log(arr.length) // 12

解决问题


那么,是否可以通过字符串的字符数量和字符串长度来判断是否输入了emoji表情呢?

要验证这个问题,关键的是获取到字符串中字符的数量。


那么如何获取字符串中字符的数量呢,通过研究(百度)发现,分割utf8字符串的正确方法是使用 Array.from(str) 而不是str.split('')。


Array.from() 方法对一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。


var str = '欢迎来到王者荣耀😊😊'
var arr2 = Array.from(str)
console.log(str.length) // 12
console.log(arr2.length) // 10

一个大胆的猜想


emoji表情判断,可以通过字符串长度和字符数量的比较判断是否存在emoji表情,当长度和数量不一致的时候,有emoji表情。


isEmojiStr(str) { 
if (typeof (str) === 'string') {
const arr = Array.from(str);
if (str.length !== arr.length) {
return true;
}
}
return false;
}

image.png


参考


# Emoji Unicode Tables

# 深入理解Emoji(一) —— 字符集,字符集编码

# 深入理解Emoji(二) —— 字节序和BOM

# 深入理解Emoji(三) —— Emoji详解


作者:前端老兵
来源:https://juejin.cn/post/7090182766158938120
收起阅读 »

复盘前端工程师必知的javascript设计模式

前言 设计模式是一个程序员进阶高级的必备技巧,也是评判一个工程师工作经验和能力的试金石.设计模式是程序员多年工作经验的凝练和总结,能更大限度的优化代码以及对已有代码的合理重构.作为一名合格的前端工程师,学习设计模式是对自己工作经验的另一种方式的总结和反思,也是...
继续阅读 »

前言

设计模式是一个程序员进阶高级的必备技巧,也是评判一个工程师工作经验和能力的试金石.设计模式是程序员多年工作经验的凝练和总结,能更大限度的优化代码以及对已有代码的合理重构.作为一名合格的前端工程师,学习设计模式是对自己工作经验的另一种方式的总结和反思,也是开发高质量,高可维护性,可扩展性代码的重要手段.

我们所熟知的金典的几大框架,比如jquery, react, vue内部也大量应用了设计模式, 比如观察者模式, 代理模式, 单例模式等.所以作为一个架构师,设计模式是必须掌握的.

在中高级前端工程师的面试的过程中,面试官也会适当考察求职者对设计模式的了解,所以笔者结合多年的工作经验和学习探索, 总结并画出了针对javascript设计模式的思维导图和实际案例,接下来就来让我们一起来探索习吧.

你将收获

  • 单例模式

  • 构造器模式

  • 建造者模式

  • 代理模式

  • 外观模式

  • 观察者模式

  • 策略模式

  • 迭代器模式

正文

我们先来看看总览.设计模式到底可以给我们带来什么呢?


以上笔者主要总结了几点使用设计模式能给工程带来的好处, 如代码可解耦, 可扩展性,可靠性, 条理性, 可复用性. 接下来来看看我们javascript的第一个设计模式.

1. 单例模式


1.1 概念解读

单例模式: 保证一个类只有一个实例, 一般先判断实例是否存在,如果存在直接返回, 不存在则先创建再返回,这样就可以保证一个类只有一个实例对象.

1.2 作用

  • 模块间通信

  • 保证某个类的对象的唯一性

  • 防止变量污染

1.3 注意事项

  • 正确使用this

  • 闭包容易造成内存泄漏,所以要及时清除不需要的变量

  • 创建一个新对象的成本较高

1.4 实际案例

单例模式广泛应用于不同程序语言中, 在实际软件应用中应用比较多的比如电脑的任务管理器,回收站, 网站的计数器, 多线程的线程池的设计等.

1.5 代码实现

(function(){
// 养鱼游戏
let fish = null
function catchFish() {
  // 如果鱼存在,则直接返回
  if(fish) {
    return fish
  }else {
    // 如果鱼不存在,则获取鱼再返回
    fish = document.querySelector('#cat')
    return {
      fish,
      water: function() {
        let water = this.fish.getAttribute('weight')
        this.fish.setAttribute('weight', ++water)
      }
    }
  }
}

// 每隔3小时喂一次水
setInterval(() => {
  catchFish().water()
}, 3*60*60*1000)
})()

2. 构造器模式


2.1 概念解读

构造器模式: 用于创建特定类型的对象,以便实现业务逻辑和功能的可复用.

2.2 作用

  • 创建特定类型的对象

  • 逻辑和业务的封装

2.3 注意事项

  • 注意划分好业务逻辑的边界

  • 配合单例实现初始化等工作

  • 构造函数命名规范,第一个字母大写

  • new对象的成本,把公用方法放到原型链上

2.4 实际案例

构造器模式我觉得是代码的格局,也是用来考验程序员对业务代码的理解程度.它往往用于实现javascript的工具库,比如lodash等以及javascript框架.

2.5 代码展示

function Tools(){
if(!(this instanceof Tools)){
  return new Tools()
}
this.name = 'js工具库'
// 获取dom的方法
this.getEl = function(elem) {
  return document.querySelector(elem)
}
// 判断是否是数组
this.isArray = function(arr) {
  return Array.isArray(arr)
}
// 其他通用方法...
}

3. 建造者模式


3.1 概念解读

建造者模式: 将一个复杂的逻辑或者功能通过有条理的分工来一步步实现.

3.2 作用

  • 分布创建一个复杂的对象或者实现一个复杂的功能

  • 解耦封装过程, 无需关注具体创建的细节

3.3 注意事项

  • 需要有可靠算法和逻辑的支持

  • 按需暴露一定的接口

3.4 实际案例

建造者模式其实在很多领域也有应用,笔者之前也写过很多js插件,大部分都采用了建造者模式, 可以在笔者github地址徐小夕的github学习参考. 其他案例如下:

  • jquery的ajax的封装

  • jquery插件封装

  • react/vue某一具体组件的设计

3.5 代码展示

笔者就拿之前使用建造者模式实现的一个案例:Canvas入门实战之用javascript面向对象实现一个图形验证码, 那让我们使用建造者模式实现一个非常常见的验证码插件吧!

// canvas绘制图形验证码
(function(){
  function Gcode(el, option) {
      this.el = typeof el === 'string' ? document.querySelector(el) : el;
      this.option = option;
      this.init();
  }
  Gcode.prototype = {
      constructor: Gcode,
      init: function() {
          if(this.el.getContext) {
              isSupportCanvas = true;
              var ctx = this.el.getContext('2d'),
              // 设置画布宽高
              cw = this.el.width = this.option.width || 200,
              ch = this.el.height = this.option.height || 40,
              textLen = this.option.textLen || 4,
              lineNum = this.option.lineNum || 4;
              var text = this.randomText(textLen);
   
              this.onClick(ctx, textLen, lineNum, cw, ch);
              this.drawLine(ctx, lineNum, cw, ch);
              this.drawText(ctx, text, ch);
          }
      },
      onClick: function(ctx, textLen, lineNum, cw, ch) {
          var _ = this;
          this.el.addEventListener('click', function(){
              text = _.randomText(textLen);
              _.drawLine(ctx, lineNum, cw, ch);
              _.drawText(ctx, text, ch);
          }, false)
      },
      // 画干扰线
      drawLine: function(ctx, lineNum, maxW, maxH) {
          ctx.clearRect(0, 0, maxW, maxH);
          for(var i=0; i < lineNum; i++) {
              var dx1 = Math.random()* maxW,
                  dy1 = Math.random()* maxH,
                  dx2 = Math.random()* maxW,
                  dy2 = Math.random()* maxH;
              ctx.strokeStyle = 'rgb(' + 255*Math.random() + ',' + 255*Math.random() + ',' + 255*Math.random() + ')';
              ctx.beginPath();
              ctx.moveTo(dx1, dy1);
              ctx.lineTo(dx2, dy2);
              ctx.stroke();
          }
      },
      // 画文字
      drawText: function(ctx, text, maxH) {
          var len = text.length;
          for(var i=0; i < len; i++) {
              var dx = 30 * Math.random() + 30* i,
                  dy = Math.random()* 5 + maxH/2;
              ctx.fillStyle = 'rgb(' + 255*Math.random() + ',' + 255*Math.random() + ',' + 255*Math.random() + ')';
              ctx.font = '30px Helvetica';
              ctx.textBaseline = 'middle';
              ctx.fillText(text[i], dx, dy);
          }
      },
      // 生成指定个数的随机文字
      randomText: function(len) {
          var source = ['a', 'b', 'c', 'd', 'e',
          'f', 'g', 'h', 'i', 'j',
          'k', 'l', 'm', 'o', 'p',
          'q', 'r', 's', 't', 'u',
          'v', 'w', 'x', 'y', 'z'];
          var result = [];
          var sourceLen = source.length;
          for(var i=0; i< len; i++) {
              var text = this.generateUniqueText(source, result, sourceLen);
              result.push(text)
          }
          return result.join('')
      },
      // 生成唯一文字
      generateUniqueText: function(source, hasList, limit) {
          var text = source[Math.floor(Math.random()*limit)];
          if(hasList.indexOf(text) > -1) {
              return this.generateUniqueText(source, hasList, limit)
          }else {
              return text
          }  
      }
  }
  new Gcode('#canvas_code', {
      lineNum: 6
  })
})();
// 调用
new Gcode('#canvas_code', {
lineNum: 6
})

4. 代理模式


4.1 概念解读

代理模式: 一个对象通过某种代理方式来控制对另一个对象的访问.

4.2 作用

  • 远程代理(一个对象对另一个对象的局部代理)

  • 虚拟代理(对于需要创建开销很大的对象如渲染网页大图时可以先用缩略图代替真图)

  • 安全代理(保护真实对象的访问权限)

  • 缓存代理(一些开销比较大的运算提供暂时的存储,下次运算时,如果传递进来的参数跟之前相同,则可以直接返回前面存储的运算结果)

4.3 注意事项

使用代理会增加代码的复杂度,所以应该有选择的使用代理.

实际案例

我们可以使用代理模式实现如下功能:

  • 通过缓存代理来优化计算性能

  • 图片占位符/骨架屏/预加载等

  • 合并请求/资源

4.4 代码展示

接下来我们通过实现一个计算缓存器来说说代理模式的应用.

// 缓存代理
function sum(a, b){
return a + b
}
let proxySum = (function(){
let cache = {}
return function(){
    let args = Array.prototype.join.call(arguments, ',');
    if(args in cache){
        return cache[args];
    }

    cache[args] = sum.apply(this, arguments)
    return cache[args]
}
})()

5. 外观模式


5.1 概念解读

外观模式(facade): 为子系统中的一组接口提供一个一致的表现,使得子系统更容易使用而不需要关注内部复杂而繁琐的细节.

5.2 作用

  • 对接口和调用者进行了一定的解耦

  • 创造经典的三层结构MVC

  • 在开发阶段减少不同子系统之间的依赖和耦合,方便各个子系统的迭代和扩展

  • 为大型复杂系统提供一个清晰的接口

5.3 注意事项

当外观模式被开发者连续调用时会造成一定的性能损耗,这是由于每次调用都会进行可用性检测

5.4 实际案例

我们可以使用外观模式来设计兼容不同浏览器的事件绑定的方法以及其他需要统一实现接口的方法或者抽象类.

5.5 代码展示

接下来我们通过实现一个兼容不同浏览器的事件监听函数来让大家理解外观模式如何使用.

function on(type, fn){
// 对于支持dom2级事件处理程序
if(document.addEventListener){
    dom.addEventListener(type,fn,false);
}else if(dom.attachEvent){
// 对于IE9一下的ie浏览器
    dom.attachEvent('on'+type,fn);
}else {
    dom['on'+ type] = fn;
}
}

6. 观察者模式


6.1 概念解读

观察者模式: 定义了一种一对多的关系, 所有观察对象同时监听某一主题对象,当主题对象状态发生变化时就会通知所有观察者对象,使得他们能够自动更新自己.

6.2 作用

  • 目标对象与观察者存在一种动态关联,增加了灵活性

  • 支持简单的广播通信, 自动通知所有已经订阅过的对象

  • 目标对象和观察者之间的抽象耦合关系能够单独扩展和重用

6.3 注意事项

观察者模式一般都要注意要先监听, 再触发(特殊情况也可以先发布,后订阅,比如QQ的离线模式)

6.4 实际案例

观察者模式是非常经典的设计模式,主要应用如下:

  • 系统消息通知

  • 网站日志记录

  • 内容订阅功能

  • javascript事件机制

  • react/vue等的观察者

6.5 代码展示

接下来我们我们使用原生javascript实现一个观察者模式:

class Subject {
constructor() {
  this.subs = {}
}

addSub(key, fn) {
  const subArr = this.subs[key]
  if (!subArr) {
    this.subs[key] = []
  }
  this.subs[key].push(fn)
}

trigger(key, message) {
  const subArr = this.subs[key]
  if (!subArr || subArr.length === 0) {
    return false
  }
  for(let i = 0, len = subArr.length; i < len; i++) {
    const fn = subArr[i]
    fn(message)
  }
}

unSub(key, fn) {
  const subArr = this.subs[key]
  if (!subArr) {
    return false
  }
  if (!fn) {
    this.subs[key] = []
  } else {
    for (let i = 0, len = subArr.length; i < len; i++) {
      const _fn = subArr[i]
      if (_fn === fn) {
        subArr.splice(i, 1)
      }
    }
  }
}
}

// 测试
// 订阅
let subA = new Subject()
let A = (message) => {
console.log('订阅者收到信息: ' + message)
}
subA.addSub('A', A)

// 发布
subA.trigger('A', '我是徐小夕')   // A收到信息: --> 我是徐小夕

7. 策略模式


7.1 概念解读

策略模式: 策略模式将不同算法进行合理的分类和单独封装,让不同算法之间可以互相替换而不会影响到算法的使用者.

7.2 作用

  • 实现不同, 作用一致

  • 调用方式相同,降低了使用成本以及不同算法之间的耦合

  • 单独定义算法模型, 方便单元测试

  • 避免大量冗余的代码判断,比如if else等

7.3 实际案例

  • 实现更优雅的表单验证

  • 游戏里的角色计分器

  • 棋牌类游戏的输赢算法

7.4 代码展示

接下来我们实现一个根据不同类型实现求和算法的模式来带大家理解策略模式.

const obj = {
A: (num) => num * 4,
B: (num) => num * 6,
C: (num) => num * 8
}

const getSum =function(type, num) {
return obj[type](num)
}

8. 迭代器模式


8.1 概念解读

迭代器模式: 提供一种方法顺序访问一个聚合对象中的各个元素,使用者并不需要关心该方法的内部表示.

8.2 作用

  • 为遍历不同集合提供统一接口

  • 保护原集合但又提供外部访问内部元素的方式

8.3 实际案例

迭代器模式模式最常见的案例就是数组的遍历方法如forEach, map, reduce.

8.4 代码展示

接下来笔者使用自己封装的一个遍历函数来让大家更加理解迭代器模式的使用,该方法不仅可以遍历数组和字符串,还能遍历对象.lodash里的.forEach(collection, [iteratee=.identity])方法也是采用策略模式的典型应用.

function _each(el, fn = (v, k, el) => {}) {
// 判断数据类型
function checkType(target){
  return Object.prototype.toString.call(target).slice(8,-1)
}

// 数组或者字符串
if(['Array', 'String'].indexOf(checkType(el)) > -1) {
  for(let i=0, len = el.length; i< len; i++) {
    fn(el[i], i, el)
  }
}else if(checkType(el) === 'Object') {
  for(let key in el) {
    fn(el[key], key, el)
  }
}
}

最后

如果想了解本文完整的思维导图, 更多H5游戏, webpacknodegulpcss3javascriptnodeJScanvas数据可视化等前端知识和实战,欢迎在公号《趣谈前端》加入我们一起学习讨论,共同探索前端的边界。

来源:https://mp.weixin.qq.com/s/xTp3jY0IvXiOWBZhZ5H9fQ

收起阅读 »

前端-SSO单点登录方案

一个完整形态的项目和产品,必然绕不开登录,作为一名前端开发工程师,了解单点登录还是非常有必要的。本文就简单分享一下前端所写到的单点登录。什么是单点登录概念一大堆,长话短说。单点登录就是指通过用户的一次性鉴别登陆,其他子项目在需要验证用户信息的时候,无需再做登录...
继续阅读 »

一个完整形态的项目和产品,必然绕不开登录,作为一名前端开发工程师,了解单点登录还是非常有必要的。本文就简单分享一下前端所写到的单点登录。

什么是单点登录

概念一大堆,长话短说。单点登录就是指通过用户的一次性鉴别登陆,其他子项目在需要验证用户信息的时候,无需再做登录操作,自动识别登录。

为什么要选择单点登录

  • [🌰] 举个栗子 目前有一个产品,产品下有三个子项目,如果每个子项目都写一遍登录,那么后面维护的时候,开发人员需要打开三处的登录去修改同样的逻辑,这样会发生一种情况就是在改逻辑的时候,如果有另外一个bug着急修改,再回来的时候发现自己不知道改到哪了。(别问,问就是发生在我身上了!)这仅仅是站在前端开发的角度上,维护起来非常累。

  • [ ✔] 使用案例 单点登录在大型网站里使用得非常频繁,例如,阿里旗下有淘宝、天猫等网站,还有背后的成百上千的子系统,用户一次操作或交易可能涉及到几十个子系统的协作,如果每个子系统都需要用户认证,不仅用户会疯掉,各子系统也会为这种重复认证授权的逻辑搞疯掉。

话不多说直接上图

图片有点抽象,不过让我们清晰认知到单点登录要解决的就是,用户只需要登录一次就可以访问所有相互信任的应用系统。

单点登录的实现方式

单点登录的本质就是在多个应用系统中共享登录状态,所以实现单点登录的关键在于,如何让Token在多个域中共享。

1、同域下的单点登录

一个企业一般情况下只有一个域名,通过二级域名区分不同的系统。

比如我有个域名:clnct.cn,同时有三个业务系统分别为:

cpc.clnct.cn
cmk.clnct.cn
ckn.clnct.cn

我们要做单点登录(SSO),需要一个登录系统,叫做:cuc.clnct.cn。

我们只要在cuc.clnct.cn登录,cpc.clnct.cn、cmk.clnct.cn、ckn.clnct.cn也登录了。

实现方式:其实这里就是利用了 二级域名 写 一级域名的 Cookie 。cuc.clnct.cn登录以后,可以将Cookie的域设置为顶域,即.clnct,这样所有子域的系统都可以访问到顶域的Cookie。

此种实现方式比较简单,但不支持跨主域名,局限性限于一级域名是一样的。

2、不同域下的单点登录

同域下的单点登录是巧用了Cookie顶域的特性,如果是不同域呢,比如:下面三个是不同域的

cpc.dun.cn
cmk.qun.cn
ckn.nun.cn

实现方式:我们可以部署一个SSO认证中心,认证中心就是一个专门负责处理登录请求。


所有的请求(登录、退出、获取用户信息、当前用户状态)都请求sso系统,sso系统维护用户信息。

此种实现方式相对复杂,支持跨域,扩展性好,是单点登录的标准做法。

逻辑分析

  • 输入用户名密码,登陆成功,接口返回token

有token,调取换code的接口。 1、接口如果获取的code值为空,清除本地的cookies,再登录;2、如果code有值,将url中的redirectUrl后拼接接口拿到的code 重定向到想要去的页面。

  • 判断域名

这里需用用到
document.domain 获取浏览器的域名

获取到浏览器域名后,匹配当前获取的域名是同域还是来自第三方系统。

如果是同域名,直接将redirectUrl返回,无需携带code接口返回的值。
如果是第三方系统,需要处理redirectUrl。因为可能用户会做登录成功再退出,那么带到登录系统的redirectUrl就会携带code值,我们需要通过js的方法去替换原来的code值。
  • 成功返回redirectUrl

一切流程通了之后,通过 location.replace(redirectUrl) 浏览器跳转返回到重定向页面

目标达成: 子系统在未登录的情况下,点击【登录】按钮,跳转到统一用户中心。统一用户中心判断当前cookies是否有token存在,如果不存在--登录;如果存在就去校验token的合法性(调取code接口),调取code接口成功,重定向到原页面。那么同域下所有的子系统,都无需登录。第三方系统进来的时候,因为做了domian的校验,因此登录成功之后,将code码放在redirectUrl,重定向到第三方系统。

总结一下

这虽然并不是最规范的SSO单点登录。但事实上比起一搜一堆概念性的文章,我认为这仅此是我个人的一种做法,至于逻辑对与错,希望大家给出合理的意见和建议,互相学习。


作者:上班摸鱼看日记
来源:https://juejin.cn/post/7088978055737114638

收起阅读 »

你确定(a == 1 && a == 2 && a == 3)不能为true?

前言最近遇到一个非常有意思的面试题: JavaScript中有没有可能让(a== 1 && a ==2 && a==3)返回true?讲真刚看到这题的时候,我是用这种眼神看面试官的:你TM逗我呢? 尊重一下我可行?没10年脑血栓...
继续阅读 »

前言

最近遇到一个非常有意思的面试题: JavaScript中有没有可能让(a== 1 && a ==2 && a==3)返回true?

讲真刚看到这题的时候,我是用这种眼神看面试官的:你TM逗我呢? 尊重一下我可行?没10年脑血栓问不出这玩意,


但看他一脸"贱笑",一副你一定答不出来的感觉,我觉得此事定不简单...


障眼法我TM给跪了

咱们先不管面试官的意图是什么,具体考察的是什么知识,先来看看几种奇特的解法。

解法1:隐藏字符 + if

const if = () => !0
const a = 9

if(a == 1 && a == 2 && a == 3)
{
 console.log('前端胖头鱼') // 前端胖头鱼
}

眼见为虚


我觉得此时你和我一样,在严重怀疑自己怕是个假前端if也能被改写?a明明是9却可以等于1、2、3


别急,这其实是一个障眼法,只是取巧蒙蔽了我们的双眼,请看下图


真相大白if的后面有个隐藏字符,本质上是声明了一个无论输入啥都返回true函数,而下面的代码块,更是和这个函数没半毛钱关系,怎么样都会执行!!!

{
 console.log('前端胖头鱼') // 前端胖头鱼
}

所以通过构造一个看似重写了if的代码块,仿佛真的实现了题目,实在是太骚了!!!

解法2:隐藏字符 + a变量

有了上面的经验,接下来的解法,你也不会感到奇怪了。

const aᅠ = 1
const a = 2
const ᅠa = 3

if (aᅠ == 1 && a == 2 && ᅠa == 3) {
 console.log('前端胖头鱼') // 前端胖头鱼
}


解法3:隐藏字符 + 数字变量

既然可以伪造三个a变量,那也可以伪造三个123变量嘛

const a = 1
const ᅠ1 = a
const ᅠ2 = a
const ᅠ3 = a

if (a == ᅠ1 && a == ᅠ2 && a == ᅠ3) {
 console.log('前端胖头鱼') // 前端胖头鱼
}

大千世界,果然眼见为虚啊!!!


再来一种奇特的解法

上面几种解法本质上都没有使 a == 1 && a == 2 && a == 3true,不过是障眼法,大家笑笑就好啦!接下来我要认真起来了...

解法4:“with”

MDN上映入眼帘的是一个警告,仿佛他的存在就是个错误,我也从来没有在实际工作中用过他,但他却可以用来解决这个题目。


let i = 1

with ({
 get a() {
   return i++
}
}) {
 if (a == 1 && a == 2 && a == 3) {
   console.log('前端胖头鱼')
}
}

聪明的你甚至都不用我解释代码啥意思了。

隐式转换成解题的关键

上面给出的4种解法多少有点歪门邪道的意思,为了让面试官死心,接下来的才是正解之道,而JS中的隐式转换规则大概也是出这道题的初衷。

隐式转换部分规则

JS中使用==对两个值进行比较时,会进行如下操作:

  1. 将两个被比较的值转换为相同的类型。

  2. 转换后(等式的一边或两边都可能被转换)再进行值的比较。

比较的规则如下表(mdn


从表中可以得到几点信息为了让(a == 1),a只有这几种:

  1. a类型为String,并且可转换为数字1('1' == 1 => true

  2. a类型为Boolean,并且可转换为数字1 (true == 1 => true)

  3. a类型为Object,通过转换机制后,可转换为数字1 (请看下文

对象转原始类型的"转换机制"

规则1和2没有什么特殊的地方,我们来看看3:

对象转原始类型,会调用内置的[ToPrimitive]函数,逻辑大致如下:

  1. 如果有Symbol.toPrimitive方法,优先调用再返回,否则进行2。

  2. 调用valueOf,如果可以转换为原始类型,则返回,否则进行3。

  3. 调用toString,如果可以转换为原始类型,则返回,否则进行4。

  4. 如果都没有返回原始类型,会报错。

const obj = {
 value: 1,
 valueOf() {
   return 2
},
 toString() {
   return '3'
},
[Symbol.toPrimitive]() {
   return 4
}
}

obj == 4 // true
// 您可以将Symbol.toPrimitive、toString、valueOf分别注释掉验证转换规则

解法5: Symbol.toPrimitive

我们可以利用隐式转换规则3完成题目(看完答案你就知道为什么啦!

const a = {
 i: 1,
[Symbol.toPrimitive]() {
   return this.i++
}
}
// 每次进行a == xxx时都会先经过Symbol.toPrimitive函数,自然也就可以实现a依次递增的效果
if (a == 1 && a == 2 && a == 3) {
 console.log('前端胖头鱼') // 前端胖头鱼
}

解法6: valueOf vs toString

当然也可以利用valueOftoString

let a = {
 i: 1,
 // valueOf替换成toString效果是一样的
 // toString
 valueOf() {
   return this.i++
}
}

if (a == 1 && a == 2 && a == 3) {
 console.log('前端胖头鱼') // 前端胖头鱼
}

解法7:Array && join

数组对象在进行隐式转换时,同样符合规则3,只是在toString时还会调用join方法。所以也可以从这里下手

let a = [1, 2, 3]

a.join = a.shift

if (a == 1 && a == 2 && a == 3) {
 console.log('前端胖头鱼') // 前端胖头鱼
}

数据劫持亦是一条出路

通过隐式转换我们做出了3种让a == 1 && a == 2 && a == 3返回true的方案,聪明的你一定想到另一种思路,数据劫持,伟大的Vue就曾使用数据劫持赢得了千万开发者的芳心,我们也试试用它来解决这道面试题

解法8:Object.defineProperty

通过劫持window对象,每次读取a属性时,都给_a 增加1

let _a = 1
Object.defineProperty(window, 'a', {
 get() {
   return _a++
}
})

if (a == 1 && a == 2 && a == 3) {
 console.log('前端胖头鱼') // 前端胖头鱼
}

解法9:Proxy

当然还有另一种劫持数据的方式,Vue3也是将响应式原理中的数据劫持Object.defineProperty换成了Proxy

let a = new Proxy({ i: 1 }, {
get(target) {
return () => target.i++
}
})

if (a == 1 && a == 2 && a == 3) {
console.log('前端胖头鱼') // 前端胖头鱼
}

最后

希望能一直给大家分享实用、基础、进阶的知识点,一起早早下班,快乐摸鱼。

作者:前端胖头鱼
来源:https://juejin.cn/post/7079936779914051615

收起阅读 »

我用 nodejs 爬了一万多张小姐姐壁纸

前言哈喽,大家好,我是小马,为什么要下载这么多图片呢? 前几天使用 uni-app + uniCloud 免费部署了一个壁纸小程序,那么接下来就需要一些资源,给小程序填充内容。爬取图片首先初始化项目,并且安装 axios 和 ch...
继续阅读 »

前言

哈喽,大家好,我是小马,为什么要下载这么多图片呢? 前几天使用 uni-app + uniCloud 免费部署了一个壁纸小程序,那么接下来就需要一些资源,给小程序填充内容。

爬取图片

首先初始化项目,并且安装 axios 和 cheerio

npm init -y && npm i axios cheerio

axios 用于爬取网页内容,cheerio 是服务端的 jquery api, 我们用它来获取 dom 中的图片地址;const axios = require('axios')

const cheerio = require('cheerio')

function getImageUrl(target_url, containerEelment) {
let result_list = []
const res = await axios.get(target_url)
const html = res.data
const $ = cheerio.load(html)
const result_list = []
$(containerEelment).each((element) => {
result_list.push($(element).find('img').attr('src'))
})
return result_list
}

这样就可以获取到页面中的图片 url 了。接下来需要根据 url 下载图片。

如何使用 nodejs 下载文件

方式一:使用内置模块 ‘https’ 和 ‘fs’

使用 node js 下载文件可以使用内置包或第三方库完成。

GET 方法用于 HTTPS 来获取要下载的文件。 createWriteStream() 是一个用于创建可写流的方法,它只接收一个参数,即文件保存的位置。Pipe()是从可读流中读取数据并将其写入可写流的方法。const fs = require('fs')

const https = require('https')

// URL of the image
const url = 'GFG.jpeg'

https.get(url, (res) => {
// Image will be stored at this path
const path = `${__dirname}/files/img.jpeg`
const filePath = fs.createWriteStream(path)
res.pipe(filePath)
filePath.on('finish', () => {
filePath.close()
console.log('Download Completed')
})
})

方式二:DownloadHelper
npm install node-downloader-helper

下面是从网站下载图片的代码。一个对象 dl 是由类 DownloadHelper 创建的,它接收两个参数:

  1. 将要下载的图像。
  2. 下载后必须保存图像的路径。

File 变量包含将要下载的图像的 URL,filePath 变量包含将要保存文件的路径。const { DownloaderHelper } = require('node-downloader-helper')


// URL of the image
const file = 'GFG.jpeg'
// Path at which image will be downloaded
const filePath = `${__dirname}/files`

const dl = new DownloaderHelper(file, filePath)

dl.on('end', () => console.log('Download Completed'))
dl.start()

方法三: 使用 download

是 npm 大神 sindresorhus 写的,非常好用

npm install download

下面是从网站下载图片的代码。下载函数接收文件和文件路径。const download = require('download')


// Url of the image
const file = 'GFG.jpeg'
// Path at which image will get downloaded
const filePath = `${__dirname}/files`

download(file, filePath).then(() => {
console.log('Download Completed')
})

最终代码

本来想去爬百度壁纸,但是清晰度不太够,而且还有水印等,后来, 群里有个小伙伴找到了一个 api,估计是某个手机 APP 上的高清壁纸,可以直接获得下载的 url,我就直接用了。

下面是完整代码

const download = require('download')
const axios = require('axios')

let headers = {
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36',
}

function sleep(time) {
return new Promise((reslove) => setTimeout(reslove, time))
}

async function load(skip = 0) {
const data = await axios
.get(
'http://service.picasso.adesk.com/v1/vertical/category/4e4d610cdf714d2966000000/vertical',
{
headers,
params: {
limit: 30, // 每页固定返回30条
skip: skip,
first: 0,
order: 'hot',
},
}
)
.then((res) => {
return res.data.res.vertical
})
.catch((err) => {
console.log(err)
})
await downloadFile(data)
await sleep(3000)
if (skip < 1000) {
load(skip + 30)
} else {
console.log('下载完成')
}
}

async function downloadFile(data) {
for (let index = 0; index < data.length; index++) {
const item = data[index]

// Path at which image will get downloaded
const filePath = `${__dirname}/美女`

await download(item.wp, filePath, {
filename: item.id + '.jpeg',
headers,
}).then(() => {
console.log(`Download ${item.id} Completed`)
return
})
}
}

load()

上面代码中先要设置 User-Agent 并且设置 3s 延迟, 这样可以防止服务端阻止爬虫,直接返回 403。

直接 node index.js 就会自动下载图片了。

爬取运行中

来源:https://juejin.cn/post/7078206989402112037

收起阅读 »

你最少用几行代码实现深拷贝?

前言深度克隆(深拷贝)一直都是初、中级前端面试中经常被问到的题目,网上介绍的实现方式也都各有千秋,大体可以概括为三种方式:JSON.stringify+JSON.pars e, 这个很好理解;全量判断类型,根据类型做不同的处理2的变型,简化类型判断过程前两种比...
继续阅读 »

前言

深度克隆(深拷贝)一直都是初、中级前端面试中经常被问到的题目,网上介绍的实现方式也都各有千秋,大体可以概括为三种方式:

  1. JSON.stringify+JSON.pars e, 这个很好理解;

  2. 全量判断类型,根据类型做不同的处理

  3. 2的变型,简化类型判断过程

前两种比较常见也比较基础,所以我们今天主要讨论的是第三种。

阅读全文你将学习到:

  1. 更简洁的深度克隆方式

  2. Object.getOwnPropertyDescriptors()api

  3. 类型判断的通用方法

问题分析

深拷贝 自然是 相对 浅拷贝 而言的。 我们都知道 引用数据类型 变量存储的是数据的引用,就是一个指向内存空间的指针, 所以如果我们像赋值简单数据类型那样的方式赋值的话,其实只能复制一个指针引用,并没有实现真正的数据克隆。

通过这个例子很容易就能理解:

const obj1 = {
   name: 'superman'
}
const obj2 = obj1;
obj1.name = '前端切图仔';
console.log(obj2.name); // 前端切图仔

所以深度克隆就是为了解决引用数据类型不能被通过赋值的方式 复制 的问题。

引用数据类型

我们不妨来罗列一下引用数据类型都有哪些:

  • ES6之前: Object, Array, Date, RegExp, Error,

  • ES6之后: Map, Set, WeakMap, WeakSet,

所以,我们要深度克隆,就需要对数据进行遍历并根据类型采取相应的克隆方式。 当然因为数据会存在多层嵌套的情况,采用递归是不错的选择。

简单粗暴版本

function deepClone(obj) {
   let res = {};
   // 类型判断的通用方法
   function getType(obj) {
       return Object.prototype.toString.call(obj).replaceAll(new RegE xp(/\[|\]|object /g), "");
  }
   const type = getType(obj);
   const reference = ["Set", "WeakSet", "Map", "WeakMap", "RegExp", "Date", "Error"];
   if (type === "Object") {
       for (const key in obj) {
           if (Object.hasOwnProperty.call(obj, key)) {
               res[key] = deepClone(obj[key]);
          }
      }
  } else if (type === "Array") {
       console.log('array obj', obj);
       obj.forEach((e, i) => {
           res[i] = deepClone(e);
      });
  }
   else if (type === "Date") {
       res = new Date(obj);
  } else if (type === "RegExp") {
       res = new RegExp(obj);
  } else if (type === "Map") {
       res = new Map(obj);
  } else if (type === "Set") {
       res = new Set(obj);
  } else if (type === "WeakMap") {
       res = new WeakMap(obj);
  } else if (type === "WeakSet") {
       res = new WeakSet(obj);
  }else if (type === "Error") {
       res = new Error(obj);
  }
    else {
       res = obj;
  }
   return res;
}

其实这就是我们最前面提到的第二种方式,很傻对不对,明眼人一眼就能看出来有很多冗余代码可以合并。

我们先进行最基本的优化:

合并冗余代码

将一眼就能看出来冗余的代码合并下。

function deepClone(obj) {
   let res = null;
   // 类型判断的通用方法
   function getType(obj) {
       return Object.prototype.toString.call(obj).replaceAll(new RegExp(/\[|\]|object /g), "");
  }
   const type = getType(obj);
   const reference = ["Set", "WeakSet", "Map", "WeakMap", "RegExp", "Date", "Error"];
   if (type === "Object") {
       res = {};
       for (const key in obj) {
           if (Object.hasOwnProperty.call(obj, key)) {
               res[key] = deepClone(obj[key]);
          }
      }
  } else if (type === "Array") {
       console.log('array obj', obj);
       res = [];
       obj.forEach((e, i) => {
           res[i] = deepClone(e);
      });
  }
   // 优化此部分冗余判断
   // else if (type === "Date") {
   //     res = new Date(obj);
   // } else if (type === "RegExp") {
   //     res = new RegExp(obj);
   // } else if (type === "Map") {
   //     res = new Map(obj);
   // } else if (type === "Set") {
   //     res = new Set(obj);
   // } else if (type === "WeakMap") {
   //     res = new WeakMap(obj);
   // } else if (type === "WeakSet") {
   //     res = new WeakSet(obj);
   // }else if (type === "Error") {
   //   res = new Error(obj);
   //}
   else if (reference.includes(type)) {
       res = new obj.constructor(obj);
  } else {
       res = obj;
  }
   return res;
}

为了验证代码的正确性,我们用下面这个数据验证下:

const map = new Map();
map.set("key", "value");
map.set("ConardLi", "coder");

const set = new Set();
set.add("ConardLi");
set.add("coder");

const target = {
   field1: 1,
   field2: undefined,
   field3: {
       child: "child",
  },
   field4: [2, 4, 8],
   empty: null,
   map,
   set,
   bool: new Boolean(true),
   num: new Number(2),
   str: new String(2),
   symbol: Object(Symbol(1)),
   date: new Date(),
   reg: /\d+/,
   error: new Error(),
   func1: () => {
       let t = 0;
       console.log("coder", t++);
  },
   func2: function (a, b) {
       return a + b;
  },
};
//测试代码
const test1 = deepClone(target);
target.field4.push(9);
console.log('test1: ', test1);

执行结果:


还有进一步优化的空间吗?

答案当然是肯定的。

// 判断类型的方法移到外部,避免递归过程中多次执行
const judgeType = origin => {
   return Object.prototype.toString.call(origin).replaceAll(new RegExp(/\[|\]|object /g), "");
};
const reference = ["Set", "WeakSet", "Map", "WeakMap", "RegExp", "Date", "Error"];
function deepClone(obj) {
   // 定义新的对象,最后返回
    //通过 obj 的原型创建对象
   const cloneObj = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));

   // 遍历对象,克隆属性
   for (let key of Reflect.ownKeys(obj)) {
       const val = obj[key];
       const type = judgeType(val);
       if (reference.includes(type)) {
           newObj[key] = new val.constructor(val);
      } else if (typeof val === "object" && val !== null) {
           // 递归克隆
           newObj[key] = deepClone(val);
      } else {
           // 基本数据类型和function
           newObj[key] = val;
      }
  }
   return newObj;
}

执行结果如下:


  • Object.getOwnPropertyDescriptors() 方法用来获取一个对象的所有自身属性的描述符。

  • 返回所指定对象的所有自身属性的描述符,如果没有任何自身属性,则返回空对象。

具体解释和内容见MDN

这样做的好处就是能够提前定义好最后返回的数据类型。

这个实现参考了网上一位大佬的实现方式,个人觉得理解成本有点高,而且对数组类型的处理也不是特别优雅, 返回类数组。

我在我上面代码的基础上进行了改造,改造后的代码如下:

function deepClone(obj) {
   let res = null;
   const reference = [Date, RegExp, Set, WeakSet, Map, WeakMap, Error];
   if (reference.includes(obj?.constructor)) {
       res = new obj.constructor(obj);
  } else if (Array.isArray(obj)) {
       res = [];
       obj.forEach((e, i) => {
           res[i] = deepClone(e);
      });
  } else if (typeof obj === "Object" && obj !== null) {
       res = {};
       for (const key in obj) {
           if (Object.hasOwnProperty.call(obj, key)) {
               res[key] = deepClone(obj[key]);
          }
      }
  } else {
       res = obj;
  }
   return res;
}

虽然代码量上没有什么优势,但是整体的理解成本和你清晰度上我觉得会更好一点。那么你觉得呢?

最后,还有循环引用问题,避免出现无线循环的问题。

我们用hash来存储已经加载过的对象,如果已经存在的对象,就直接返回。

function deepClone(obj, hash = new WeakMap()) {
   if (hash.has(obj)) {
       return obj;
  }
   let res = null;
   const reference = [Date, RegExp, Set, WeakSet, Map, WeakMap, Error];

   if (reference.includes(obj?.constructor)) {
       res = new obj.constructor(obj);
  } else if (Array.isArray(obj)) {
       res = [];
       obj.forEach((e, i) => {
           res[i] = deepClone(e);
      });
  } else if (typeof obj === "Object" && obj !== null) {
       res = {};
       for (const key in obj) {
           if (Object.hasOwnProperty.call(obj, key)) {
               res[key] = deepClone(obj[key]);
          }
      }
  } else {
       res = obj;
  }
   hash.set(obj, res);
   return res;
}

总结

对于深拷贝的实现,可能存在很多不同的实现方式,关键在于理解其原理,并能够记住一种最容易理解和实现的方式,面对类似的问题才能做到 临危不乱,泰然自若。 上面的实现你觉得哪个更好呢?欢迎大佬们在评论区交流~


作者:前端superman
来源:https://juejin.cn/post/7075351322014253064

收起阅读 »

为了快乐的摸鱼,专门写了个网站!

这是鄙人做的网站,目的呢原本是为了摸鱼,把产品那边整的页面快速构建出来,咱们公司用的是比较老的vue2版本,组件库是ant-design-vue,做的系统是一些中规中矩的企业用的办公系统,所以页面都是千篇一律。作为卑微的996社畜,不想被肆无忌惮的压榨,于是有...
继续阅读 »

直接进入主题: demo

这是鄙人做的网站,目的呢原本是为了摸鱼,把产品那边整的页面快速构建出来,咱们公司用的是比较老的vue2版本,组件库是ant-design-vue,做的系统是一些中规中矩的企业用的办公系统,所以页面都是千篇一律。作为卑微的996社畜,不想被肆无忌惮的压榨,于是有一天,我就琢磨着通过拖拉拽的方式把组件模块组合起来,能快速的响应产品那边朝令夕改的无理要求。

经过将近一个月的鼓捣,小破站也在命运多舛中慢慢走向成熟。

先简单介绍吧,显而易见的操作界面:传统的页眉,低调却不失风采;左侧的手风琴列表,简约而不简单;中控是一个设计器,有了它你可以写出一个出色的网页,而不需要写一行代码(少量代码还是必要的)!





本小破站还做了国际化、自适应,能基本满足常规的企业系统界面需求,比如传统的ERP/HR/SDM等后台管理系统,页面的顶部有一个下拉框,里面有默认的几个示例,都是通过这种拖拽方式做出来的。

有一个地方需要特别说明,就是组件提供的事件回调函数提供w,w,w,vm这两个全局参数。w表示当前window全局对象,w表示当前window全局对象,w表示当前window全局对象,vm则代表全局vm对象,也就是this。通过这两个参数,是可以简单的写出组件间调用的方法的 (可以看看test#table这个例子)。当然,涉及更复杂一点的业务逻辑,则需要做更多的代码复用,以及watch监听等等,这部分功能的话暂时还没有想好怎么实现。

组件有基本的antd组件、echarts组件,还有vue-3d-model组件,为了更方便的编辑属性和代码,用了bin-ace-editor,有了这些大佬们的轮子,转起来确实快乐。

功能还在逐步完善中,最近也没很多时间去写,总之有时间就去补充,日积月累的完善吧。

PS:

小破站带宽是乞丐版的1Mb,鄙人已经尽力做了cdn加速,希望不卡

第一次打开会自动生成3个示例,放在localStorage里面

感谢阅读 ^_^


作者:AllenThomas
来源:https://juejin.cn/post/7077743139934437406

收起阅读 »

前端无痛刷新Token

前端无痛刷新Token这个需求场景很常见,几乎很多项目都会用上,之前项目也实现过,最近刚好有个项目要实现,重新梳理一番。需求对于需要前端实现无痛刷新Token,无非就两种:请求前判断Token是否过期,过期则刷新请求后根据返回状态判断是否过期,过期则刷新处理逻...
继续阅读 »

前端无痛刷新Token

这个需求场景很常见,几乎很多项目都会用上,之前项目也实现过,最近刚好有个项目要实现,重新梳理一番。

需求

对于需要前端实现无痛刷新Token,无非就两种:

  1. 请求前判断Token是否过期,过期则刷新

  2. 请求后根据返回状态判断是否过期,过期则刷新

处理逻辑

实现起来也没多大差别,只是判断的位置不一样,核心原理都一样:

  1. 判断Token是否过期

    1. 没过期则正常处理

    2. 过期则发起刷新Token的请求

      1. 拿到新的Token保存

      2. 重新发送Token过期这段时间内发起的请求

重点:

  • 保持Token过期这段时间发起请求状态(不能进入失败回调)

  • 把刷新Token后重新发送请求的响应数据返回到对应的调用者

实现

  1. 创建一个flag isRefreshing 来判断是否刷新中

  2. 创建一个数组队列retryRequests来保存需要重新发起的请求

  3. 判断到Token过期

    1. isRefreshing = false的情况下 发起刷新Token的请求

      1. 刷新Token后遍历执行队列retryRequests

    2. isRefreshing = true 表示正在刷新Token,返回一个Pending状态的Promise,并把请求信息保存到队列retryRequests

import axios from "axios";
import Store from "@/store";
import Router from "@/router";
import { Message } from "element-ui";
import UserUtil from "@/utils/user";

// 创建实例
const Instance = axios.create();
Instance.defaults.baseURL = "/api";
Instance.defaults.headers.post["Content-Type"] = "application/json";
Instance.defaults.headers.post["Accept"] = "application/json";

// 定义一个flag 判断是否刷新Token中
let isRefreshing = false;
// 保存需要重新发起请求的队列
let retryRequests = [];

// 请求拦截
Instance.interceptors.request.use(async function(config) {
 Store.commit("startLoading");
 const userInfo = UserUtil.getLocalInfo();
 if (userInfo) {
   //业务需要把Token信息放在 params 里面,一般来说都是放在 headers里面
   config.params = Object.assign(config.params ? config.params : {}, {
     appkey: userInfo.AppKey,
     token: userInfo.Token
  });
}
 return config;
});

// 响应拦截
Instance.interceptors.response.use(
 async function(response) {
   Store.commit("finishLoading");
   const res = response.data;
   if (res.errcode == 0) {
     return Promise.resolve(res);
  } else if (
     res.errcode == 30001 ||
     res.errcode == 40001 ||
     res.errcode == 42001 ||
     res.errcode == 40014
  ) {
   // 需要刷新Token 的状态 30001 40001 42001 40014
   // 拿到本次请求的配置
     let config = response.config;
   //   进入登录页面的不做刷新Token 处理
     if (Router.currentRoute.path !== "/login") {
       if (!isRefreshing) {
           // 改变flag状态,表示正在刷新Token中
         isRefreshing = true;
       //   刷新Token
         return Store.dispatch("user/relogin")
          .then(res => {
           // 设置刷新后的Token
             config.params.token = res.Token;
             config.params.appkey = res.AppKey;
           //   遍历执行需要重新发起请求的队列
             retryRequests.forEach(cb => cb(res));
           //   清空队列
             retryRequests = [];
             return Instance.request(config);
          })
          .catch(() => {
             retryRequests = [];
             Message.error("自动登录失败,请重新登录");
               const code = Store.state.user.info.CustomerCode || "";
               // 刷新Token 失败 清空缓存的用户信息 并调整到登录页面
               Store.dispatch("user/logout");
               Router.replace({
                 path: "/login",
                 query: { redirect: Router.currentRoute.fullPath, code: code }
              });
          })
          .finally(() => {
               // 请求完成后重置flag
             isRefreshing = false;
          });
      } else {
         // 正在刷新token,返回一个未执行resolve的promise
         // 把promise 的resolve 保存到队列的回调里面,等待刷新Token后调用
         // 原调用者会处于等待状态直到 队列重新发起请求,再把响应返回,以达到用户无感知的目的(无痛刷新)
         return new Promise(resolve => {
           // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
           retryRequests.push(info => {
               // 将新的Token重新赋值
             config.params.token = info.Token;
             config.params.appkey = info.AppKey;
             resolve(Instance.request(config));
          });
        });
      }
    }
     return new Promise(() => {});
  } else {
     return Promise.reject(res);
  }
},
 function(error) {
   let err = {};
   if (error.response) {
     err.errcode = error.response.status;
     err.errmsg = error.response.statusText;
  } else {
     err.errcode = -1;
     err.errmsg = error.message;
  }
   Store.commit("finishLoading");
   return Promise.reject(err);
}
);

export default Instance;


作者:沐夕花开
来源:https://juejin.cn/post/7075348765162340383

收起阅读 »

你已经是个成熟的前端了,应该学会破解防盗链了

今天一早打开微信,就看到国产github——gitee崩了。 Issue列表里面全是反馈图片显示异常,仔细一看,原来是图床的防盗链。 场景复现 之前没用过gitee,火速去建了一个账号试验一下。 我在我的gitee中上传一张图片,在gitee本站里面显示是正...
继续阅读 »

今天一早打开微信,就看到国产github——gitee崩了。



Issue列表里面全是反馈图片显示异常,仔细一看,原来是图床的防盗链。


场景复现


之前没用过gitee,火速去建了一个账号试验一下。


我在我的gitee中上传一张图片,在gitee本站里面显示是正常的。


1-1.png


右键复制这张图片的地址,放到一个第三方的在线编辑器中,发现图片变成gitee的logo了



什么是防盗链


防盗链不是一根链条,正确的停顿应该是防·盗链——防止其他网站盗用我的链接。


我把图片上传到gitee的服务器,得到了图片的链接,然后拿着这个链接在第三方编辑器中使用,这就是在“盗用”——因为这张图片占用了gitee的服务器资源,却为第三方编辑器工作,gitee得不到好处,还得多花钱。


如何实现防盗链


要实现防盗链,就需要知道图片的请求是从哪里发出的。可以实现这一功能的有请求头中的originrefererorigin只有在XHR请求中才会带上,所以图片资源只能借助referer。其实gitee也确实是这么做的。


通过判断请求的referer,如果请求来源不是本站就返回302,重定向到gitee的logo上,最后在第三方网站引用存在gitee的资源就全变成它的logo了。


可以在开发者工具中看到第三方网站请求gitee图片的流程:



  1. 首先请求正常的图片,但是没有返回200,而是302重定向,其中响应头中的location就是要重定向去向的地址;

  2. 接着浏览器会自动请求这个location,并用这个返回结果代替第一次请求的返回内容;


最后,我们的图片在第三方网站就变成gitee的logo了。


如何破解防盗链


想让gitee不知道我在盗用,就不能让他发现请求的来源是第三方,只要把referer藏起来就好,可以在终端尝试这段代码:


curl 'https://images.gitee.com/uploads/images/2022/0326/155444_dc9923a4_10659337.jpeg' \
-o noReferer.jpg

这段👆代码的意思是请求这张jpg图片资源,把返回结果以noReferer.jpg这个名称保存在当前目录下,并且没有带上referer,测试结果是图片正常保存下来了。


就像加上了gitee本站的referer一样可以正常请求👇:


curl 'https://images.gitee.com/uploads/images/2022/0326/155444_dc9923a4_10659337.jpeg' \
-H 'referer: https://gitee.com' \
-o fromGitee.jpg

而在第三方网站请求的效果就像这段👇代码


curl 'https://images.gitee.com/uploads/images/2022/0326/155444_dc9923a4_10659337.jpeg' \
-H 'referer: https://editor.mdnice.com/' \
-o otherReferer.png

带上了第三方网站的标识https://editor.mdnice.com最终无法正常下载。


gitee做的不够完善吗


测试完上面的三段代码,不知道你会不会疑惑,gitee为什么不把“请求来源不能是第三方网站”的策略改成“请求来源必须是本站点”呢?换句话说,控制referer不能为空,只要是空就重定向。


因为在浏览器的地址栏中直接输入这个图片的url,然后回车,发起的请求是没有referer字段的,在这种场景下如果还是返回gitee的logo,就显得不太合理了。



图片的url:https://images.gitee.com/uploads/images/2022/0326/155444_dc9923a4_10659337.jpeg



图片看不到了,现在怎么办


如果你的个人搭建的博客里面用了很多存在gitee的图片,你可以在html的head部分加上这样一行


<meta name="referrer" content="no-referrer" />


或者


<img referrer="no-referrer|origin|unsafe-url" src="{item.src}"/>


来阻止请求因带上站点来源而被重定向成gitee的logo。


如果你是博客的访问者,可以借助一个chrome小插件ModHeader,把referer给“擦掉”



这样第三方站点就可以正常访问啦~


1-2.png


结语


上面提到的解决方式只是开个玩笑,临时恢复使用可以。但还是要慢慢把图片迁移到自己的服务器才最可靠。


作者:前端私教年年
来源:https://juejin.cn/post/7079705713781506079 收起阅读 »

七大跨域解决方法原理

前言 大家好,我是林三心。用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初衷。 咱们做前端的,平时跟后端对接接口那是必须的事情,但是可能很多同学忽略了一个对接过程中可能会发生的问题——跨域,那跨域到底是啥呢?为什么会跨域呢?又怎么才能解决呢...
继续阅读 »

前言


大家好,我是林三心。用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初衷。


咱们做前端的,平时跟后端对接接口那是必须的事情,但是可能很多同学忽略了一个对接过程中可能会发生的问题——跨域,那跨域到底是啥呢?为什么会跨域呢?又怎么才能解决呢?


截屏2021-10-01 上午7.16.06.png


为什么跨域?


image.png


为什么会出现跨域问题呢?那就不得不讲浏览器的同源策略了,它规定了协议号-域名-端口号这三者必须都相同才符合同源策略


截屏2021-10-01 上午8.50.11.png


如有有一个不相同,就会出现跨域问题,不符合同源策略导致的后果有



  • 1、LocalStorge、SessionStorge、Cookie等浏览器内存无法跨域访问

  • 2、DOM节点无法跨域操作

  • 3、Ajax请求无法跨域请求


注意点:一个IP是可以注册多个不同域名的,也就是多个域名可能指向同一个IP,即使是这样,他们也不符合同源策略


截屏2021-10-01 上午9.02.55.png


跨域的时机?


跨域发生在什么时候呢?我考过很多位同学,得到了两种答案



  • 1、请求一发出就被浏览器的跨域报错拦下来了(大多数人回答)

  • 2、请求发出去到后端,后端返回数据,在浏览器接收后端数据时被浏览器的跨域报错拦下来


那到底是哪种呢?我们可以验证下,咱们先npm i nodemon -g,然后创建一个index.js,然后nodemon index起一个node服务


// index.js  http://127.0.0.1:8000

const http = require('http');

const port = 8000;

http.createServer(function (req, res) {
const { query } = urllib.parse(req.url, true);
console.log(query.name)
console.log('到后端喽')
res.end(JSON.stringify('林三心'));
}).listen(port, function () {
console.log('server is listening on port ' + port);
})
复制代码

再创建一个index.html,用来写前端的请求代码,咱们就写一个简单的AJAX请求


// index.html  http://127.0.0.1:5500/index.html
<script>
//步骤一:创建异步对象
var ajax = new XMLHttpRequest();
//步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数
ajax.open('get', 'http://127.0.0.1:8000?name=前端过来的林三心');
//步骤三:发送请求
ajax.send();
//步骤四:注册事件 onreadystatechange 状态改变就会调用
ajax.onreadystatechange = function () {
if (ajax.readyState == 4 && ajax.status == 200) {
//步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的
console.log(ajax.responseText);//输入相应的内容
}
}

</script>
复制代码

截屏2021-10-01 下午1.37.01.png


最终,前端确实是跨域报错了。但这不是结果,我们要想知道是哪一个答案,关键在于看后端的node服务那里有没有输出,就一目了然了。所以,答案2才是对的。


截屏2021-10-01 下午1.38.52.png


截屏2021-10-01 下午1.41.51.png


同域情况 && 跨域情况?


前面提到了同源策略,满足协议号-域名-端口号这三者都相同就是同域,反之就是跨域,会导致跨域报错,下面通过几个例子让大家巩固一下对同域和跨域的认识把!


截屏2021-10-01 上午9.24.38.png


解决跨域的方案


跨域其实是一个很久的问题了,对应的解决方案也有很多,一起接着往下读吧!!!


JSONP


前面咱们说了,因为浏览器同源策略的存在,导致存在跨域问题,那有没有不受跨域问题所束缚的东西呢?其实是有的,以下这三个标签加载资源路径是不受束缚的



  • 1、script标签:<script src="加载资源路径"></script>

  • 2、link标签:<link herf="加载资源路径"></link>

  • 3、img标签:<img src="加载资源路径"></img>


而JSONP就是利用了scriptsrc加载不受束缚,从而可以拥有从不同的域拿到数据的能力。但是JSONP需要前端后端配合,才能实现最终的跨域获取数据


JSONP通俗点说就是:利用script的src去发送请求,将一个方法名callback传给后端,后端拿到这个方法名,将所需数据,通过字符串拼接成新的字符串callback(所需数据),并发送到前端,前端接收到这个字符串之后,就会自动执行方法callback(所需数据)。老规矩,先上图,再上代码。


截屏2021-10-01 下午1.22.08.png


后端代码


// index.js  http://127.0.0.1:8000

const http = require('http');
const urllib = require('url');

const port = 8000;

http.createServer(function (req, res) {
const { query } = urllib.parse(req.url, true);
if (query && query.callback) {
const { name, age, callback } = query
const person = `${name}今年${age}岁啦!!!`
const str = `${callback}(${JSON.stringify(person)})` // 拼成callback(data)
res.end(str);
} else {
res.end(JSON.stringify('没东西啊你'));
}
}).listen(port, function () {
console.log('server is listening on port ' + port);
})
复制代码

前端代码


// index.html  http://127.0.0.1:5500/index.html

const jsonp = (url, params, cbName) => {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
window[cbName] = (data) => {
resolve(data)
document.body.removeChild(script)
}
params = { ...params, callback: cbName }
const arr = Object.keys(params).map(key => `${key}=${params[key]}`)
script.src = `${url}?${arr.join('&')}`
document.body.appendChild(script)
})
}

jsonp('http://127.0.0.1:8000', { name: '林三心', age: 23 }, 'callback').then(data => {
console.log(data) // 林三心今年23岁啦!!!
})
复制代码

截屏2021-10-01 下午1.47.29.png



JSONP的缺点就是,需要前后端配合,并且只支持get请求方法



WebSocket


WebSocket是什么东西?其实我也不怎么懂,但是我也不会像别人一样把MDN的资料直接复制过来,因为复制过来相信大家也是看不懂的。


我理解的WebSocket是一种协议(跟http同级,都是协议),并且他可以进行跨域通信,为什么他支持跨域通信呢?我这里找到一篇文章WebSocket凭啥可以跨域?,讲的挺好


截屏2021-10-01 下午10.02.39.png


后端代码


先安装npm i ws


// index.js  http://127.0.0.1:8000
const Websocket = require('ws');

const port = 8000;
const ws = new Websocket.Server({ port })
ws.on('connection', (obj) => {
obj.on('message', (data) => {
data = JSON.parse(data.toString())
const { name, age } = data
obj.send(`${name}今年${age}岁啦!!!`)
})
})
复制代码

前端代码


// index.html  http://127.0.0.1:5500/index.html


function myWebsocket(url, params) {
return new Promise((resolve, reject) => {
const socket = new WebSocket(url)
socket.onopen = () => {
socket.send(JSON.stringify(params))
}
socket.onmessage = (e) => {
resolve(e.data)
}
})
}
myWebsocket('ws://127.0.0.1:8000', { name: '林三心', age: 23 }).then(data => {
console.log(data) // 林三心今年23岁啦!!!
})
复制代码

结果如下


截屏2021-10-01 下午1.47.29.png


Cors


Cors,全称是Cross-Origin Resource Sharing,意思是跨域资源共享,Cors一般是由后端来开启的,一旦开启,前端就可以跨域访问后端。


为什么后端开启Cors,前端就能跨域请求后端呢?我的理解是:前端跨域访问到后端,后端开启Cors,发送Access-Control-Allow-Origin: 域名 字段到前端(其实不止一个),前端浏览器判断Access-Control-Allow-Origin的域名如果跟前端域名一样,浏览器就不会实行跨域拦截,从而解决跨域问题。


截屏2021-10-01 下午6.41.11.png


后端代码


// index.js  http://127.0.0.1:8000

const http = require('http');
const urllib = require('url');

const port = 8000;

http.createServer(function (req, res) {
// 开启Cors
res.writeHead(200, {
//设置允许跨域的域名,也可设置*允许所有域名
'Access-Control-Allow-Origin': 'http://127.0.0.1:5500',
//跨域允许的请求方法,也可设置*允许所有方法
"Access-Control-Allow-Methods": "DELETE,PUT,POST,GET,OPTIONS",
//允许的header类型
'Access-Control-Allow-Headers': 'Content-Type'
})
const { query: { name, age } } = urllib.parse(req.url, true);
res.end(`${name}今年${age}岁啦!!!`);
}).listen(port, function () {
console.log('server is listening on port ' + port);
})
复制代码

前端代码


// index.html  http://127.0.0.1:5500/index.html
//步骤一:创建异步对象
var ajax = new XMLHttpRequest();
//步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数
ajax.open('get', 'http://127.0.0.1:8000?name=林三心&age=23');
//步骤三:发送请求
ajax.send();
//步骤四:注册事件 onreadystatechange 状态改变就会调用
ajax.onreadystatechange = function () {
if (ajax.readyState == 4 && ajax.status == 200) {
//步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的
console.log(ajax.responseText);//输入相应的内容
}
}
复制代码

结果如下


截屏2021-10-01 下午1.47.29.png


截屏2021-10-01 下午7.10.57.png


Node接口代理


还是回到同源策略,同源策略它只是浏览器的一个策略而已,它是限制不到后端的,也就是前端-后端会被同源策略限制,但是后端-后端则不会被限制,所以可以通过Node接口代理,先访问已设置Cors的后端1,再让后端1去访问后端2获取数据到后端1,后端1再把数据传到前端


截屏2021-10-01 下午8.46.28.png


后端2代码


// index.js  http://127.0.0.1:8000

const http = require('http');
const urllib = require('url');

const port = 8000;

http.createServer(function (req, res) {
console.log(888)
const { query: { name, age } } = urllib.parse(req.url, true);
res.end(`${name}今年${age}岁啦!!!`)
}).listen(port, function () {
console.log('server is listening on port ' + port);
})
复制代码

创建一个index2.js,并nodmeon index2.js


后端1代码


// index2.js  http://127.0.0.1:8888

const http = require('http');
const urllib = require('url');
const querystring = require('querystring');
const port = 8888;

http.createServer(function (req, res) {
// 开启Cors
res.writeHead(200, {
//设置允许跨域的域名,也可设置*允许所有域名
'Access-Control-Allow-Origin': 'http://127.0.0.1:5500',
//跨域允许的请求方法,也可设置*允许所有方法
"Access-Control-Allow-Methods": "DELETE,PUT,POST,GET,OPTIONS",
//允许的header类型
'Access-Control-Allow-Headers': 'Content-Type'
})
const { query } = urllib.parse(req.url, true);
const { methods = 'GET', headers } = req
const proxyReq = http.request({
host: '127.0.0.1',
port: '8000',
path: `/?${querystring.stringify(query)}`,
methods,
headers
}, proxyRes => {
proxyRes.on('data', chunk => {
console.log(chunk.toString())
res.end(chunk.toString())
})
}).end()
}).listen(port, function () {
console.log('server is listening on port ' + port);
})
复制代码

前端代码


// index.html  http://127.0.0.1:5500

//步骤一:创建异步对象
var ajax = new XMLHttpRequest();
//步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数,动态的传递参数starName到服务端
ajax.open('get', 'http://127.0.0.1:8888?name=林三心&age=23');
//步骤三:发送请求
ajax.send();
//步骤四:注册事件 onreadystatechange 状态改变就会调用
ajax.onreadystatechange = function () {
if (ajax.readyState == 4 && ajax.status == 200) {
//步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的
console.log(ajax.responseText);//输入相应的内容
}
}
复制代码

结果如下


截屏2021-10-01 下午1.47.29.png


Nginx


其实NginxNode接口代理是一个道理,只不过Nginx就不需要我们自己去搭建一个中间服务


截屏2021-10-01 下午8.47.40.png


先下载nginx,然后将nginx目录下的nginx.conf修改如下:


    server{
listen 8888;
server_name 127.0.0.1;

location /{
proxy_pass 127.0.0.1:8000;
}
}
复制代码

最后通过命令行nginx -s reload启动nginx


后端代码


// index.js  http://127.0.0.1:8000

const http = require('http');
const urllib = require('url');

const port = 8000;

http.createServer(function (req, res) {
const { query: { name, age } } = urllib.parse(req.url, true);
res.end(`${name}今年${age}岁啦!!!`);
}).listen(port, function () {
console.log('server is listening on port ' + port);
})
复制代码

前端代码


// index.html  http://127.0.0.1:5500

//步骤一:创建异步对象
var ajax = new XMLHttpRequest();
//步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数,动态的传递参数starName到服务端
ajax.open('get', 'http://127.0.0.1:8888?name=林三心&age=23');
//步骤三:发送请求
ajax.send();
//步骤四:注册事件 onreadystatechange 状态改变就会调用
ajax.onreadystatechange = function () {
if (ajax.readyState == 4 && ajax.status == 200) {
//步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的
console.log(ajax.responseText);//输入相应的内容
}
}
复制代码

结果如下


截屏2021-10-01 下午1.47.29.png


postMessage


场景:http://127.0.0.1:5500/index.html页面中使用了iframe标签内嵌了一个http://127.0.0.1:5555/index.html的页面


虽然这两个页面存在于一个页面中,但是需要iframe标签来嵌套才行,这两个页面之间是无法进行通信的,因为他们端口号不同,根据同源策略,他们之间存在跨域问题


那应该怎么办呢?使用postMessage可以使这两个页面进行通信


截屏2021-10-01 下午9.28.53.png


// http:127.0.0.1:5500/index.html

<body>
<iframe src="http://127.0.0.1:5555/index.html" id="frame"></iframe>
</body>
<script>
document.getElementById('frame').onload = function () {
this.contentWindow.postMessage({ name: '林三心', age: 23 }, 'http://127.0.0.1:5555')
window.onmessage = function (e) {
console.log(e.data) // 林三心今年23岁啦!!!
}
}
</script>
复制代码

// http://127.0.0.1:5555/index.html

<script>
window.onmessage = function (e) {
const { data: { name, age }, origin } = e
e.source.postMessage(`${name}今年${age}岁啦!!!`, origin)
}
</script>
复制代码

document.domain && iframe


场景:a.sanxin.com/index.htmlb.sanxin.com/index.html之间的通信


其实上面这两个正常情况下是无法通信的,因为他们的域名不相同,属于跨域通信


那怎么办呢?其实他们有一个共同点,那就是他们的二级域名都是sanxin.com,这使得他们可以通过document.domain && iframe的方式来通信


截屏2021-10-01 下午9.58.55.png


由于本菜鸟暂时没有服务器,所以暂时使用本地来模拟


// http://127.0.0.1:5500/index.html

<body>
<iframe src="http://127.0.0.1:5555/index.html" id="frame"></iframe>
</body>
<script>
document.domain = '127.0.0.1'
document.getElementById('frame').onload = function () {
console.log(this.contentWindow.data) // 林三心今年23岁啦!!!
}
</script>
复制代码

// http://127.0.0.1:5555/index.html

<script>
// window.name="林三心今年23岁啦!!!"
document.domain = '127.0.0.1'
var data = '林三心今年23岁啦!!!';
</script>

复制代码

结果如下


截屏2021-10-01 下午1.47.29.png


作者:Sunshine_Lin
链接:https://juejin.cn/post/7017614708832206878
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

CSS性能优化的8个技巧

我们都知道对于网站来说,性能至关重要,CSS作为页面渲染和内容展现的重要环节,影响着用户对整个网站的第一体验。因此,与其相关的性能优化是不容忽视的。对于性能优化我们常常在项目完成时才去考虑,经常被推迟到项目的末期,甚至到暴露出严重的性能问题时才进行性能优化,相...
继续阅读 »

我们都知道对于网站来说,性能至关重要,CSS作为页面渲染和内容展现的重要环节,影响着用户对整个网站的第一体验。因此,与其相关的性能优化是不容忽视的。

对于性能优化我们常常在项目完成时才去考虑,经常被推迟到项目的末期,甚至到暴露出严重的性能问题时才进行性能优化,相信大多数人对此深有体会。

笔者认为,为了更多地避免这一情况,首先要重视起性能优化相关的工作,将其贯穿到整个产品设计与开发中。其次,就是了解性能相关的内容,在项目开发过程中,自然而然地进行性能优化。最后,也是最最重要的,那就是从现在开始实施优化。

推荐大家阅读下奇舞周刊之前推的《嗨,送你一张Web性能优化地图》1这篇文章,能够帮助大家对性能优化需要做的事以及需要考虑的问题形成一个整体的概念。

本文将会详细介绍CSS性能优化相关的技巧,笔者将它们分为实践型建议型两类,共8个小技巧。实践型技巧能够快速地应用在项目中,能够很好地提升性能,也是笔者经常使用的,建议大家尽快在项目中实践。建议型技巧中,有的可能对性能影响并不显著,有的平时大家也并不会那么用,所以笔者不会着重讲述,读者们可以根据自身情况了解一下即可。

在正式开始之前,需要大家对于浏览器的工作原理2有些一定的了解,需要的小伙伴可以先简单了解下。

下面我们开始介绍实践型的4个优化技巧,先从首屏关键CSS开始。

1. 内联首屏关键CSS(Critical CSS)

性能优化中有一个重要的指标——首次有效绘制(First Meaningful Paint,简称FMP)即指页面的首要内容(primary content)出现在屏幕上的时间。这一指标影响用户看到页面前所需等待的时间,而内联首屏关键CSS(即Critical CSS,可以称之为首屏关键CSS)能减少这一时间。

大家应该都习惯于通过link标签引用外部CSS文件。但需要知道的是,将CSS直接内联到HTML文档中能使CSS更快速地下载。而使用外部CSS文件时,需要在HTML文档下载完成后才知道所要引用的CSS文件,然后才下载它们。所以说,内联CSS能够使浏览器开始页面渲染的时间提前,因为在HTML下载完成之后就能渲染了。

既然内联CSS能够使页面渲染的开始时间提前,那么是否可以内联所有的CSS呢?答案显然是否定的,这种方式并不适用于内联较大的CSS文件。因为初始拥塞窗口3存在限制(TCP相关概念,通常是 14.6kB,压缩后大小),如果内联CSS后的文件超出了这一限制,系统就需要在服务器和浏览器之间进行更多次的往返,这样并不能提前页面渲染时间。因此,我们应当只将渲染首屏内容所需的关键CSS内联到HTML中

既然已经知道内联首屏关键CSS能够优化性能了,那下一步就是如何确定首屏关键CSS了。显然,我们不需要手动确定哪些内容是首屏关键CSS。Github上有一个项目Critical CSS4,可以将属于首屏的关键样式提取出来,大家可以看一下该项目,结合自己的构建工具进行使用。当然为了保证正确,大家最好再亲自确认下提取出的内容是否有缺失。

不过内联CSS有一个缺点,内联之后的CSS不会进行缓存,每次都会重新下载。不过如上所说,如果我们将内联后的文件大小控制在了14.6kb以内,这似乎并不是什么大问题。

如上,我们已经介绍了为什么要内联关键CSS以及如何内联,那么剩下的CSS我们怎么处理好呢?建议使用外部CSS引入剩余CSS,这样能够启用缓存,除此之外还可以异步加载它们。

2. 异步加载CSS

CSS会阻塞渲染,在CSS文件请求、下载、解析完成之前,浏览器将不会渲染任何已处理的内容。有时,这种阻塞是必须的,因为我们并不希望在所需的CSS加载之前,浏览器就开始渲染页面。那么将首屏关键CSS内联后,剩余的CSS内容的阻塞渲染就不是必需的了,可以使用外部CSS,并且异步加载。

那么如何实现CSS的异步加载呢?有以下四种方式可以实现浏览器异步加载CSS。

第一种方式是使用JavaScript动态创建样式表link元素,并插入到DOM中。

// 创建link标签
const myCSS = document.createElement( "link" );
myCSS.rel = "stylesheet";
myCSS.href = "mystyles.css";
// 插入到header的最后位置
document.head.insertBefore( myCSS, document.head.childNodes[ document.head.childNodes.length - 1 ].nextSibling );

第二种方式是将link元素的media属性设置为用户浏览器不匹配的媒体类型(或媒体查询),如media="print",甚至可以是完全不存在的类型media="noexist"。对浏览器来说,如果样式表不适用于当前媒体类型,其优先级会被放低,会在不阻塞页面渲染的情况下再进行下载。

当然,这么做只是为了实现CSS的异步加载,别忘了在文件加载完成之后,将media的值设为screenall,从而让浏览器开始解析CSS。

<link rel="stylesheet" href="mystyles.css" media="noexist" onl0ad="this.media='all'">

与第二种方式相似,我们还可以通过rel属性将link元素标记为alternate可选样式表,也能实现浏览器异步加载。同样别忘了加载完成之后,将rel改回去。

<link rel="alternate stylesheet" href="mystyles.css" onl0ad="this.rel='stylesheet'">

上述的三种方法都较为古老。现在,rel="preload"5这一Web标准指出了如何异步加载资源,包括CSS类资源。

<link rel="preload" href="mystyles.css" as="style" onl0ad="this.rel='stylesheet'">

注意,as是必须的。忽略as属性,或者错误的as属性会使preload等同于XHR请求,浏览器不知道加载的是什么内容,因此此类资源加载优先级会非常低。as的可选值可以参考上述标准文档。

看起来,rel="preload"的用法和上面两种没什么区别,都是通过更改某些属性,使得浏览器异步加载CSS文件但不解析,直到加载完成并将修改还原,然后开始解析。

但是它们之间其实有一个很重要的不同点,那就是使用preload,比使用不匹配的media方法能够更早地开始加载CSS。所以尽管这一标准的支持度还不完善,仍建议优先使用该方法。

该标准现在已经是候选标准,相信浏览器会逐渐支持该标准。在各浏览器的支持度如下图所示。


从上图可以看出这一方法在现在的浏览器中支持度不算乐观,不过我们可以通过loadCSS6进行polyfill,所以支持不支持,这都不是事儿。

3. 文件压缩

性能优化时有一个最容易想到,也最常使用的方法,那就是文件压缩,这一方案往往效果显著。

文件的大小会直接影响浏览器的加载速度,这一点在网络较差时表现地尤为明显。相信大家都早已习惯对CSS进行压缩,现在的构建工具,如webpack、gulp/grunt、rollup等也都支持CSS压缩功能。压缩后的文件能够明显减小,可以大大降低了浏览器的加载时间。

4. 去除无用CSS

虽然文件压缩能够降低文件大小。但CSS文件压缩通常只会去除无用的空格,这样就限制了CSS文件的压缩比例。那是否还有其他手段来精简CSS呢?答案显然是肯定的,如果压缩后的文件仍然超出了预期的大小,我们可以试着找到并删除代码中无用的CSS

一般情况下,会存在这两种无用的CSS代码:一种是不同元素或者其他情况下的重复代码,一种是整个页面内没有生效的CSS代码。对于前者,在编写的代码时候,我们应该尽可能地提取公共类,减少重复。对于后者,在不同开发者进行代码维护的过程中,总会产生不再使用的CSS的代码,当然一个人编写时也有可能出现这一问题。而这些无用的CSS代码不仅会增加浏览器的下载量,还会增加浏览器的解析时间,这对性能来说是很大的消耗。所以我们需要找到并去除这些无用代码。

当然,如果手动删除这些无用CSS是很低效的。我们可以借助Uncss7库来进行。Uncss可以用来移除样式表中的无用CSS,并且支持多文件和JavaScript注入的CSS。

前面已经说完了实践型的4个优化技巧,下面我们介绍下建议型的4个技巧

1. 有选择地使用选择器

大多数朋友应该都知道CSS选择器的匹配是从右向左进行的,这一策略导致了不同种类的选择器之间的性能也存在差异。相比于#markdown-content-h3,显然使用#markdown .content h3时,浏览器生成渲染树(render-tree)所要花费的时间更多。因为后者需要先找到DOM中的所有h3元素,再过滤掉祖先元素不是.content的,最后过滤掉.content的祖先不是#markdown的。试想,如果嵌套的层级更多,页面中的元素更多,那么匹配所要花费的时间代价自然更高。

不过现代浏览器在这一方面做了很多优化,不同选择器的性能差别并不明显,甚至可以说差别甚微。此外不同选择器在不同浏览器中的性能表现8也不完全统一,在编写CSS的时候无法兼顾每种浏览器。鉴于这两点原因,我们在使用选择器时,只需要记住以下几点,其他的可以全凭喜好。

  1. 保持简单,不要使用嵌套过多过于复杂的选择器。

  2. 通配符和属性选择器效率最低,需要匹配的元素最多,尽量避免使用。

  3. 不要使用类选择器和ID选择器修饰元素标签,如h3#markdown-content,这样多此一举,还会降低效率。

  4. 不要为了追求速度而放弃可读性与可维护性。

如果大家对于上面这几点还存在疑问,笔者建议大家选择以下几种CSS方法论之一(BEM9,OOCSS10,SUIT11,SMACSS12,ITCSS13,Enduring CSS14等)作为CSS编写规范。使用统一的方法论能够帮助大家形成统一的风格,减少命名冲突,也能避免上述的问题,总之好处多多,如果你还没有使用,就赶快用起来吧。

Tips:为什么CSS选择器是从右向左匹配的?

CSS中更多的选择器是不会匹配的,所以在考虑性能问题时,需要考虑的是如何在选择器不匹配时提升效率。从右向左匹配就是为了达成这一目的的,通过这一策略能够使得CSS选择器在不匹配的时候效率更高。这样想来,在匹配时多耗费一些性能也能够想的通了。

2. 减少使用昂贵的属性

在浏览器绘制屏幕时,所有需要浏览器进行操作或计算的属性相对而言都需要花费更大的代价。当页面发生重绘时,它们会降低浏览器的渲染性能。所以在编写CSS时,我们应该尽量减少使用昂贵属性,如box-shadow/border-radius/filter/透明度/:nth-child等。

当然,并不是让大家不要使用这些属性,因为这些应该都是我们经常使用的属性。之所以提这一点,是让大家对此有一个了解。当有两种方案可以选择的时候,可以优先选择没有昂贵属性或昂贵属性更少的方案,如果每次都这样的选择,网站的性能会在不知不觉中得到一定的提升。

3. 优化重排与重绘

在网站的使用过程中,某些操作会导致样式的改变,这时浏览器需要检测这些改变并重新渲染,其中有些操作所耗费的性能更多。我们都知道,当FPS为60时,用户使用网站时才会感到流畅。这也就是说,我们需要在16.67ms内完成每次渲染相关的所有操作,所以我们要尽量减少耗费更多的操作。

3.1 减少重排

重排会导致浏览器重新计算整个文档,重新构建渲染树,这一过程会降低浏览器的渲染速度。如下所示,有很多操作会触发重排,我们应该避免频繁触发这些操作。

  1. 改变font-sizefont-family

  2. 改变元素的内外边距

  3. 通过JS改变CSS类

  4. 通过JS获取DOM元素的位置相关属性(如width/height/left等)

  5. CSS伪类激活

  6. 滚动滚动条或者改变窗口大小

此外,我们还可以通过CSS Trigger15查询哪些属性会触发重排与重绘。

值得一提的是,某些CSS属性具有更好的重排性能。如使用Flex时,比使用inline-blockfloat时重排更快,所以在布局时可以优先考虑Flex

3.2 避免不必要的重绘

当元素的外观(如color,background,visibility等属性)发生改变时,会触发重绘。在网站的使用过程中,重绘是无法避免的。不过,浏览器对此做了优化,它会将多次的重排、重绘操作合并为一次执行。不过我们仍需要避免不必要的重绘,如页面滚动时触发的hover事件,可以在滚动的时候禁用hover事件,这样页面在滚动时会更加流畅。

此外,我们编写的CSS中动画相关的代码越来越多,我们已经习惯于使用动画来提升用户体验。我们在编写动画时,也应当参考上述内容,减少重绘重排的触发。除此之外我们还可以通过硬件加速16和will-change17来提升动画性能,本文不对此展开详细介绍,感兴趣的小伙伴可以点击链接进行查看。

最后需要注意的是,用户的设备可能并没有想象中的那么好,至少不会有我们的开发机器那么好。我们可以借助Chrome的开发者工具进行CPU降速,然后再进行相关的测试,降速方法如下图所示。


如果需要在移动端访问的,最好将速度限制更低,因为移动端的性能往往更差。

4. 不要使用@import

最后提一下,不要使用@import引入CSS,相信大家也很少使用。

不建议使用@import主要有以下两点原因。

首先,使用@import引入CSS会影响浏览器的并行下载。使用@import引用的CSS文件只有在引用它的那个css文件被下载、解析之后,浏览器才会知道还有另外一个css需要下载,这时才去下载,然后下载后开始解析、构建render tree等一系列操作。这就导致浏览器无法并行下载所需的样式文件。

其次,多个@import会导致下载顺序紊乱。在IE中,@import会引发资源文件的下载顺序被打乱,即排列在@import后面的js文件先于@import下载,并且打乱甚至破坏@import自身的并行下载

所以不要使用这一方法,使用link标签就行了。

总结

至此,我们介绍完了CSS性能优化的4个实践型技巧和4个建议型技巧,在了解这些技巧之后,CSS的性能优化从现在就可以开始了。不要犹豫了,尽快开始吧。

参考文章

  1. Efficiently Rendering CSS

  2. How to write CSS for a great performance web application

  3. CSS performance revisited: selectors, bloat and expensive styles

  4. Avoiding Unnecessary Paints

  5. Five CSS Performance Tools to Speed up Your Website

  6. How and Why You Should Inline Your Critical CSS

  7. Render blocking css

  8. Modern Asynchronous CSS Loading

  9. Preload

作者:奇舞精选 · 高峰
来源:https://juejin.cn/post/6844903649605320711

收起阅读 »

你要懂的单页面应用和多页面应用

单页面应用(SinglePage Web Application,SPA)只有一张Web页面的应用,是一种从Web服务器加载的富客户端,单页面跳转仅刷新局部资源 ,公共资源(js、css等)仅需加载一次,常用于PC端官网、购物等网站如图:单页面应用结构视图多页...
继续阅读 »

单页面应用(SinglePage Web Application,SPA)

只有一张Web页面的应用,是一种从Web服务器加载的富客户端,单页面跳转仅刷新局部资源 ,公共资源(js、css等)仅需加载一次,常用于PC端官网、购物等网站

如图:


单页面应用结构视图

多页面应用(MultiPage Application,MPA)

多页面跳转刷新所有资源,每个公共资源(js、css等)需选择性重新加载,常用于 app 或 客户端等

如图:


多页面应用结构视图

具体对比分析:

单页面应用(SinglePage Web Application,SPA)多页面应用(MultiPage Application,MPA)
组成一个外壳页面和多个页面片段组成多个完整页面构成
资源共用(css,js)共用,只需在外壳部分加载不共用,每个页面都需要加载
刷新方式页面局部刷新或更改整页刷新
url 模式a.com/#/pageone a.com/#/pagetwoa.com/pageone.html a.com/pagetwo.html
用户体验页面片段间的切换快,用户体验良好页面切换加载缓慢,流畅度不够,用户体验比较差
转场动画容易实现无法实现
数据传递容易依赖 url传参、或者cookie 、localStorage等
搜索引擎优化(SEO)需要单独方案、实现较为困难、不利于SEO检索 可利用服务器端渲染(SSR)优化实现方法简易
试用范围高要求的体验度、追求界面流畅的应用适用于追求高度支持搜索引擎的应用
开发成本较高,常需借助专业的框架较低 ,但页面重复代码多
维护成本相对容易相对复杂


作者:boxser
来源:https://juejin.cn/post/6844903512107663368

收起阅读 »

千万别小瞧九宫格 一道题就能让候选人原形毕露!

前言 据不完全统计(其实就统计了自己身边的朋友和同事),在刨除抖音或快手这一类短视频 APP 后,每天在手机上花费时间最长的就是刷微博和逛朋友圈。 在刷微博和逛朋友圈的时候经常会看到这种东西: 它有一个高大上的名字:九宫格。 顾名思义,九宫格通常为如图这种三...
继续阅读 »

前言


据不完全统计(其实就统计了自己身边的朋友和同事),在刨除抖音或快手这一类短视频 APP 后,每天在手机上花费时间最长的就是刷微博和逛朋友圈。


在刷微博和逛朋友圈的时候经常会看到这种东西:



它有一个高大上的名字:九宫格。
顾名思义,九宫格通常为如图这种三行三列的布局。


微信客户端就用到了这种布局方式:



大家最熟悉的朋友圈也采用了九宫格:



还有微博:



它在移动端的运用十分的广泛,而且不仅仅是在移动端的运用,它甚至还运用到了一些面试题中,因为九宫格可以很好的考察面试者的 CSS 功底。


边距九宫格


九宫格通常分为两种,一种是边距九宫格,另一种是边框九宫格。


边距九宫格就是朋友圈那种每张图都带有一定边距的那种:


这种其实反而更简单一些,因为不涉及到边框问题,像这种几行几列的布局用网格布局(grid)简直再合适不过了。


但考虑到大家普遍对网格不太熟悉,所以咱们用同样适合几行几列的表格布局来实现,为什么不用万能的弹性盒子(flex)来做呢?因为下面那道面试题就是用flex实现的,不想用两个一样的布局来实现,为了美观一点,这里使用了一个中文渐变色的库:chinese-gradient,来看代码:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!-- 在这里用link标签引入中文渐变色 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/chinese-gradient">
<style>
/* 清除默认样式 */
* { padding: 0; margin: 0; }

/* 全屏显示 */
html, body, ul { height: 100% }

/* 父元素 */
ul {
/* 给个合适的宽度 */
width: 100%;

/* 清除默认样式 */
list-style: none;

/* 令其用table方式去显示 */
display: table;

/* 设置间距 */
border-spacing: 3px
}

/* 子元素 */
li {
/* 令其用table-row方式去显示 */
display: table-row
}

/* 孙子元素 */
div {
/* 令其用table-cell方式去显示 */
display: table-cell;

/* 蓝色渐变 */
background: var(--湖蓝)
}
</style>
</head>
<body>
<ul>
<li>
<div></div>
<div></div>
<div></div>
</li>
<li>
<div></div>
<div></div>
<div></div>
</li>
<li>
<div></div>
<div></div>
<div></div>
</li>
</ul>
</body>
</html>
复制代码

运行结果:



可以看到在 DOM 结构上我们并没有用到 <table>、<tr>、<td> 这类传统表格元素,因为在这种情况下只是用到了表格的那种几行几列而已。但实际上九宫格并不是表格,所以为了符合 W3C 的语义化标准,我们采用了其他的 DOM 元素。



在有些适合使用表格布局但又不是表格的情况下,可以利用 display 属性来模仿表格的行为:




  • display: table;相当于把元素的行为变成<table></table>

  • display: inline-table;相当于把元素的行为变成行内元素版的<table></table>

  • display: table-header-group;相当于把元素的行为变成<thead></thead>

  • display: table-row-group;相当于把元素的行为变成<tbody></tbody>

  • display: table-footer-group;相当于把元素的行为变成<tfoot></tfoot>

  • display: table-row;相当于把元素的行为变成<tr></tr>

  • display: table-column-group;相当于把元素的行为变成<colgroup></colgroup>

  • display: table-column;相当于把元素的行为变成<col></col>

  • display: table-cell;相当于把元素的行为变成<td></td><th></th>

  • display: table-caption;相当于把元素的行为变成<caption></caption>


边框九宫格


可能大家看了前面的内容觉得:就这?这么简单还想让人原形毕露?


那咱们来看这么一道题:



要求如下:



  • 边框九宫格的每个格子中的数字都要居中

  • 鼠标经过时边框和数字都要变红

  • 点击九宫格会弹出对应的数字


看起来还是没什么大不了对不对?是不是觉得就是把九宫格加个边框就行了?如果你是这么想的话,那么你写出来的九宫格将会变成这样:



是不是跟想象中的好像不太一样?为什么会这样呢?




因为给每个盒子加入了边框以后,在有边距的情况下看起来都挺正常的,但要将他们合并在一起的话相邻的两个边框就会贴合在一起,肉眼看起来就是一个两倍粗的边框:



那么怎么解决这个问题呢?


解法1


不是相邻的两个边框合并在一起会变粗吗?那么最简单粗暴的办法就是让两个相邻的盒子的其中一个的相邻边不显示边框不就完了!就像这样:



这么做完全可以实现,绝对没毛病。但这种属于笨方法,如果给换成四宫格、六宫格、十二宫格,那么又要重新去想一下该怎么实现,而且写出来的代码也比较冗余,几乎每个盒子都要给它定义一个不同的样式。


如果去参加面试的时候这么实现出来,面试官也不会给你满分,甚至可能连个及格分都不会给。但毕竟算是实现出来了,总比那些没实现出来的强点,不会给零分的。


解法2


上面那种实现方式要给每一个盒子都写一套不同的样式,而且还不适合别的像六宫格、十二宫格这类,代码冗余、可复用性差。


那么怎么才能每个盒子只用到一个样式,并且同样还适用于别的宫格呢?来看看这个思路:



但是仔细一看经不起推敲啊:整个九宫格最右边和最下边的边框都没有了!其实只要咱们在父元素上再加上右侧和下侧的边框即可:



而且并不一定非得是这个方向的,别的方向也可以实现啊,比如酱婶儿的:



酱婶儿的:



还有酱婶儿的:



这种方式不管你是4、6、9还是12宫格,只需在子元素上加一个样式即可,然后再在父元素上加一个互补的边框样式。


解法3


上面那种解法其实已经可以了,但还不是最完美的,那么它都有哪些问题呢?




  • 首先,虽然换成别的宫格也可以复用,但都只适合"满"的情况。比如像朋友圈,最大就是九宫格对吧?但用户可以不是每次都发满九张照片,有可能发7张、有可能发五张,这样的话就会露馅(所以朋友圈采用的是边距九宫格而不是边框九宫格)。




  • 其次,它并不适合这道面试题,因为这道面试题的要求是在鼠标移入时边框变红,而上面那种解法会导致每个盒子的边框都不完整,所以当鼠标移入时效果会变成这样:





那么怎么样才能完美的解出这道题呢?首先每个盒子的边框不能再给它缺斤少两了,但那又会回到最初的那个问题上去:



有的面试题就是这样,在你苦思冥想的时候怎么也想不出来,但是稍微给点思路立马就能明白!


其实就是每个盒子都给它一个负边距,边距的距离恰巧就是边框的粗细,这样后面一个盒子就会"叠加"在前面那个盒子的边框上,我们来写一个粗点的半透明边框演示一下:



中间那些颜色变深了的就是叠在一起的边框,由于是半透明,所以叠在一起时颜色会变深。


不过一些比较细心的朋友可能会纳闷:既然所有盒子都用负边距向左上角移动了,岂不是九宫格不会处在原来的位置上了,没错是这样的!所以我们需要让最左边那一排和最上面那一排不要有负边距,这时候就要考察候选人的CSS水平了,看看他/她能不能够灵活运用伪类选择器:每一行的第一个,应该怎么写?



  • :nth-child(1), :nth-child(4), :nth-child(7)


这样也能实现,不过更好的方式是写成这样:



  • :nth-child(3n+1)


最上面那一排负边距可以不用管,因为如果页面上的九宫格往左边移动了,哪怕只有一两像素,也会导致和页面上的版面无法对齐,而往上移动个一两像素的话谁也看不出来。


但如果要写的话大多数人想的可能是这样:



  • :first-child, :nth-child(2), :nth-child(3)


而更好的方式是这样:



  • :nth-child(-n+3)


每个宫格内的数字要居中,这里推荐用grid,因为九宫格可以用flex去实现,但里面的内容还继续用它去实现的话就体现不出你技术的全面性了,而且在居中这一方面grid可以做到比flex代码更少,即使你对grid不感兴趣,那么只需记住这一固定用法即可:


父元素 {
display: grid;

/* 令其子元素居中 */
place-items: center;
}
复制代码

点击这里查看更多实现居中布局的方式


里面的内容解决了,外面的九宫格咱们来用万能的flex去实现,flex默认是一维布局,但如果仅支持一维的话就不会称之为万能的flex了,思路是这样的,假如每一个宫格宽高为100 x 100,九宫格加起来是300 x 300,每三个就让它换行,这样就可以考察到候选人对flex的灵活运用的程度了:


父元素 {
width: 300px;

/* 设置为flex布局 */
display: flex;

/* 设置换行 */
flex-flow: wrap;
}

子元素 {
width: 100px;
height: 100px;

border: 1px solid black;
}
复制代码

看起来没毛病对不对?实际上确是每行只有两个宫格就会换行,因为加了边框以后子元素的宽高就变成了102 x 102了,三个的话就已经超过了300,所以还没到三个就开始换行了,这时候就考察到候选人的盒模型了:


子元素 {
width: 100px;
height: 100px;

border: 1px solid black;

/* 设置盒模型 */
box-sizing: border-box;
}
复制代码

这样即使加了边框,宽高也还是100,刚好能满3个就换行,想象一下如果你是面试官,直接问盒模型是不是显得很low,但是就这一个小小的九宫格立马就能区分出这个候选人的水平如何。


再接下来就是鼠标移入时边框和里面的内容一起变红,这有啥难的,不就是:


:hover {
/* 红色字体 */
color: red;

/* 红色边框 */
border: 1px solid red;
}
复制代码

还是那句话,这样确实能实现,但如果在咱们写js的过程中像red这种多处地方使用的值是不是一般都会给它设置成变量啊?那么这里要写CSS变量?也可以,但有一个更好的变量叫做currentColor,这个属性可以把它理解成一个内置变量,就像js里的innerWidth(window.innerWidth)一样,不用定义自然就是一个变量。


CSS变量不同的是它取的是自身或父元素上的color值,而且它的兼容性还更好,可以一直兼容到IE9


如果你觉得纳闷:这单词这么长,还不如直接写个red多方便啊,那么请别忘了color是可以继承的!如果在一个外层元素中定义了一个颜色,里面的子元素都可以继承,用JS来控制的话只需要获取外层DOM元素然后修改它的color样式即可。


currentColor作为一个变量,可以用在 border、box-shadow、background、linear-gradient() 等一大堆的 CSS 属性上…甚至连svg中的 fill 和 stroke 都可以使用这个变量,它能做的事情很多,这里为了不跑题就先不展开讲,有兴趣的可以去搜一下。


:hover {
/* 红色字体 */
color: red;

/* 红色边框 */
border: 1px solid;
}
复制代码

修改后的代码如上,为什么没有currentColor?那是因为如果你不写的话,默认就是currentColor,这个关键字代表的就是你当前的color值。



大多数的候选人可能都不会写成这样,如果你作为面试官的话最好是适当的提示一下,看他能不能说出currentColor这个变量或者CSS变量



然后就是点击每个宫格弹出对应的数字,这个考察的是事件冒泡和事件代理:


父元素.addEventListener('click', e => alert(e.target.innerText))
复制代码

你可以观察一下候选人是把事件绑定在父元素上还是一个个的绑定在子元素上,这个问题按理说基本上都不会错。但如果发现候选人一个个把事件绑定在子元素上了,那就可以到此为止了,也不用浪费时间再去问别的问题了,可以十分装B的来一句:行,你的情况我已基本了解了,回去等通知吧!


接下来我们再来写一下完整一点的代码,以便引出下一个问题:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
/* 清除默认样式 */
* { padding: 0; margin: 0; }

/* 全屏显示 */
html, body { height: 100% }

body {
/* 网格布局 */
display: grid;

/* 子元素居中 */
place-items: center;
}

/* 父元素 */
ul {
width: 300px;

/* 清除默认样式 */
list-style: none;

/* 设置为flex布局 */
display: flex;

/* 设置换行 */
flex-flow: wrap;
}

/* 子元素 */
li {
/* 显示为网格布局 */
display: grid;

/* 子元素水平垂直居中 */
place-items: center;

/* 宽高都是100像素 */
width: 100px;
height: 100px;

/* 设置盒模型 */
box-sizing: border-box;

/* 设置1像素的边框 */
border: 1px solid black;

/* 负边距 */
margin: -1px 0 0 -1px;
}

/* 第1、4、7个子元素 */
li:nth-child(3n+1) {
/* 取消左负边距 */
margin-left: 0
}

/* 前三个子元素 */
li:nth-child(-n+3) {
/* 取消上负边距 */
margin-top: 0
}

/* 当鼠标经过时 */
li:hover {
/* 红色字体 */
color: red;

/* 红色边框 */
border: 1px solid;
}
</style>
</head>
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
</ul>
<script>
// 选择ul元素
const ul = document.getElementsByTagName('ul')[0]

// 监听ul元素的点击事件
ul.addEventListener('click', e => alert(e.target.innerText))
</script>
</body>
</html>
复制代码

运行结果:



想知道为什么会这样吗?因为当前这个边框被后面的宫格压住了嘛!那么只需要当鼠标经过时不让后面的压住就好了(调高层级)。


说到调高层级,大家首先想到的可能就是z-index了,这个属性用的最多的地方可能就是绝对定位和固定定位了。但其实很少有人知道,z-index不是只能用在position: xxx的,万能的弹性盒子(display:flex)也是支持z-index的:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
/* 清除默认样式 */
* { padding: 0; margin: 0; }

/* 全屏显示 */
html, body { height: 100% }

body {
/* 网格布局 */
display: grid;

/* 子元素居中 */
place-items: center;
}

/* 父元素 */
ul {
width: 300px;

/* 清除默认样式 */
list-style: none;

/* 设置为flex布局 */
display: flex;

/* 设置换行 */
flex-flow: wrap;
}

/* 子元素 */
li {
/* 显示为网格布局 */
display: grid;

/* 子元素水平垂直居中 */
place-items: center;

/* 宽高都是100像素 */
width: 100px;
height: 100px;

/* 设置盒模型 */
box-sizing: border-box;

/* 设置1像素的边框 */
border: 1px solid black;

/* 负边距 */
margin: -1px 0 0 -1px;
}

/* 第1、4、7个子元素 */
li:nth-child(3n+1) {
/* 取消左负边距 */
margin-left: 0
}

/* 前三个子元素 */
li:nth-child(-n+3) {
/* 取消上负边距 */
margin-top: 0
}

/* 当鼠标经过时 */
li:hover {
/* 红色字体 */
color: red;

/* 红色边框 */
border: 1px solid;

/* 调高层级 */
z-index: 1;
}
</style>
</head>
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
</ul>
<script>
// 选择ul元素
const ul = document.getElementsByTagName('ul')[0]

// 监听ul元素的点击事件
ul.addEventListener('click', e => alert(e.target.innerText))
</script>
</body>
</html>
复制代码

运行结果:



结语


没想到这么一个看似不起眼的九宫格一下子就能考察这么多内容吧!如果面试的时候直接问:



  • 你对 flex 了解的怎么样

  • 当元素的外边距为负值时会有什么样的行为

  • 请实现一下水平垂直居中

  • 了解过 grid 吗

  • 谈一下你对盒模型的理解

  • 说一下事件绑定和事件冒泡

  • CSS3的伪类选择器用的怎么样

  • 当页面元素重叠时如何控制哪个在上哪个在下

  • 在CSS中如何运用变量


直接这么问的话既浪费口舌,又显得很low,而且还不能筛选出真正能够灵活运用技术的候选人。


因为这些问题都不难,一般来说都能答出来,但具体能不能灵活运用就不一定了,而这一道九宫格,就像一面照妖镜一样,瞬间让人原形毕露!


如果你是候选人的话,那么一定要好好练习一下这道题。


如果是面试官的话,那么也推荐你用这道题来考察候选者的技术水平,如果能非常完美的做出来,那么基本上就不用再问其他的CSS题目了,日常开发所用到的样式基本难不倒他/她了,可以直接上JS面试题了。


但如果没做出来也不一定就代表这个人水平不行,可以试着提示一下候选者,然后再问一下其他的CSS题来确定一下此人的水平。


作者:手撕红黑树
来源:https://juejin.cn/post/6886770985060532231
收起阅读 »

仅靠H5标签就能实现收拉效果

前言 最近做项目时碰到这么一个需求: 这有点类似于手风琴效果,但不一样的是很多手风琴效果是同一时间内只能有一个展开,而这个是各个部分独立的,你展不展开完全不会影响我的展开与否。其实这种效果简直再普遍不过了,网上随便一搜就出来一大堆。但不一样的是,我在接到这个...
继续阅读 »

前言


最近做项目时碰到这么一个需求:



这有点类似于手风琴效果,但不一样的是很多手风琴效果是同一时间内只能有一个展开,而这个是各个部分独立的,你展不展开完全不会影响我的展开与否。其实这种效果简直再普遍不过了,网上随便一搜就出来一大堆。但不一样的是,我在接到这个需求的时候突然想起来很久以前看过张鑫旭大佬的一篇文章,模糊的记得那篇文章里说过有个什么很方便的 CSS 属性能够实现这一效果,不用像咱们平时实现的那些展开收起那样写很多的代码,于是就来到他的博客里面一顿搜,找了半天终于发现原来是我记错了,并不是什么 CSS3 属性,而是 HTML5 标签!


details


想要非常轻松的实现一个收拉效果,需要用到三个标签,分别是:<details><summary>以及随意


随意是什么意思?意思是什么标签都可以?


咱们先只写一个<details>标签来看看页面上会出现什么:


<details></details>
复制代码

运行结果:



可以看到非常有意思的一个现象:我们明明什么文字都没有写,但页面上却出现了详细信息这四个字,因为如果你在标签里没有写<summary>的话,浏览器会自动给你补上一个<summary>详细信息</summary>,那有人可能奇怪了,怎么补的是中文呢?那老外不写<summary>的话也会来一个<summary>详细信息</summary>?其实是这样:



现代浏览器经常偷偷获取用户隐私信息,包括但不仅限于用人工智能判断屏幕前的用户是中国人还是外国人,然后根据用户的母语来动态向<summary>标签里加入不同语言的'详细信息'这几个字。




开个玩笑,其实是根据你当前操作系统的语言来判断的,要是你把系统语言改成其它语言的话出现的就不再是'详细信息'这几个中文字符了。


那如果我们在<details>标签里写了<summary>呢?


<details>
<summary>公众号:</summary>
</details>
复制代码

运行结果:



可以看到<summary>里面的文字就会在三角箭头旁边的标题位置展示出来,可是我们展开三角箭头发现里面什么内容也没有,那么内容写在哪呢?


只需写在<summary>的后面就可以了,那是不是还要写个固定标签呢?比如什么<describe>之类的,其实在<summary>之后无论写什么标签都可以,当然必须得是合法的 HTML 标签啊,比如我们写个<h1>标签来试试看:


<details>
<summary>公众号:</summary>
<h1>前端学不动</h1>
</details>
复制代码

运行结果:



再换个别的标签试试:


<details>
<summary>公众号:</summary>
<button>前端学不动</button>
</details>
复制代码

运行结果:



看!我们仅用了三个标签就完成了一个最简单的收拉效果!以前在网上看到类似的效果要么就是 getElementById 获取到 DOM 元素,然后添加 onclick 事件控制下方元素的 style 属性,要么就是纯 CSS 实现,写几个单选按钮配合兄弟选择器来控制后方元素的显隐,抑或是 CSS 与 JS 相结合来实现的,但仅靠 HTML 标签来实现这一效果还是非常清新脱俗的!并且十分简洁、非常节约代码量、也更加直观易于理解。


深入测试


既然<summary>标签后面写什么都行,那么可不可以写很多个标签呢?我们来测试一下:


<details>
<summary>公众号:</summary>
<button>前端学不动</button>
<span>前端学不动</span>
<h1>前端学不动</h1>
<a href="#">前端学不动</a>
<strong>前端学不动</strong>
</details>
复制代码

运行结果:



那展开收起那部分的内容只能放在<summary>标签之后吗?如果放它前面呢:


<details>
<button>前端学不动</button>
<span>前端学不动</span>
<h1>前端学不动</h1>
<a href="#">前端学不动</a>
<strong>前端学不动</strong>
<summary>公众号:</summary>
</details>
复制代码

运行结果:



效果居然一模一样,看来展开收起的那部分应该是在<details>标签内部的除<summary>标签之外的所有内容。那如果写两个<summary>标签呢:


<details>
<button>前端学不动</button>
<span>前端学不动</span>
<h1>前端学不动</h1>
<a href="#">前端学不动</a>
<strong>前端学不动</strong>
<summary>公众号:</summary>
<summary>summary</summary>
</details>
复制代码

运行结果:



可以看到只有第一个出现的<summary>标签是真正的summary,后续出现的其他所有标签(包括其它的<summary>)都是展开收起的那部分。


既然所有标签都可以,那么也包括<details>咯?


<details>
<summary>project</summary>
<details>
<summary>html</summary>
index.html
</details>
<details>
<summary>css</summary>
reset.css
</details>
<details>
<summary>js</summary>
main.js
</details>
</details>
复制代码

运行结果:



这玩意有点意思,利用这种嵌套写法可以轻松实现编辑器左侧的那些文件区的效果。


加入样式


虽然可以很轻松、甚至在不用写 CSS 代码的情况下就实现展开收起效果,但毕竟不写 CSS 只是实现了个最基础的乞丐版效果,很多人都不想要点击的时候出现的那个轮廓:



在谷歌浏览器和 Safari 浏览器下都会出现这个轮廓,火狐就没有这玩意,咱们只需要给<summary>标签设置 outline 属性就可以了,一般如果你的项目引入了抹平浏览器样式间差异的 reset.css 文件的话,就不用写这个 CSS 了,为了方便同时观看 HTML、CSS 和 JS,我们来用 Vue 的格式来写代码:


<template>
<details>
<summary>project</summary>
<details>
<summary>html</summary>
index.html
</details>
<details>
<summary>css</summary>
reset.css
</details>
<details>
<summary>js</summary>
main.js
</details>
</details>
</template>

<style>
summary { outline: none }
</style>
复制代码

运行结果:



这样看起来就舒服多啦!但是还有个问题:那个三角箭头太傻大黑粗了,一般我们很少会用这样的箭头,而且我们也不一定非得让它在左边待着,那么怎么修改箭头的样式呢?


在谷歌浏览器以及 Safari 浏览器下我们需要用::-webkit-details-marker伪元素,在火狐浏览器下我们要用::-moz-list-bullet伪元素,比如我们想让它别那么傻大黑粗:


<template>
<details>
<summary>project</summary>
<details>
<summary>html</summary>
index.html
</details>
<details>
<summary>css</summary>
reset.css
</details>
<details>
<summary>js</summary>
main.js
</details>
</details>
</template>

<style>
summary { outline: none }

/* 谷歌、Safari */
::-webkit-details-marker {
transform: scale(.5);
color: gray
}

/* 火狐 */
::-moz-list-bullet { color: gray }
</style>
复制代码

运行结果:



是不是没那么傻大黑粗了,不过有时我们不想要这个三角形的箭头,想要的是自己自定义的箭头,那么我们就需要先把这个默认的三角给隐藏掉:


<template>
<details>
<summary>project</summary>
<details>
<summary>html</summary>
index.html
</details>
<details>
<summary>css</summary>
reset.css
</details>
<details>
<summary>js</summary>
main.js
</details>
</details>
</template>

<style>
summary { outline: none }

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }
</style>
复制代码

运行结果:



这回箭头没了,我们只需要在<summary>标签里写个箭头就好了,可以用::before::after伪元素,也可以直接在里面写个<img>标签,为了让大家能够直接复制代码到 Vue 环境里运行,在这里我们就不用图片了,直接手写<svg>


<template>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
project
</summary>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
html
</summary>
index.html
</details>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
css
</summary>
reset.css
</details>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
js
</summary>
main.js
</details>
</details>
</template>

<style>
summary {
position: relative;
padding-left: 20px;
outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
position: absolute;
left: 0;
top: 50%;
fill: none;
stroke: gray
}
</style>
复制代码

运行结果:



箭头是变成自定义的了,但是方向却不智能了,不能像原生箭头那样展开收起时会自动改变方向,但是<details>这个标签好就好在它在展开是会自动在标签里添加一个open属性:



我们可以利用它的这一特点,用属性选择器来让<svg>标签进行旋转:


<template>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
project
</summary>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
html
</summary>
index.html
</details>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
css
</summary>
reset.css
</details>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
js
</summary>
main.js
</details>
</details>
</template>

<style>
summary {
position: relative;
padding-left: 20px;
outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
position: absolute;
left: 0;
top: 50%;
transform: rotate(180deg);
transition: transform .2s;
fill: none;
stroke: gray
}

[open] > summary > svg { transform: none }
</style>
复制代码

运行结果:



用 JS 控制 open 属性


既然展开时会自动给<details>标签添加一个open属性,那如果我们用 JS 手动给<details>标签添加或删除open属性,<details>标签会随之展开收起吗?


比如我们用定时器,每隔1秒就自动展开一个,同时收起上一个已被展开过的标签:


<template>
<details v-for="({title, content}, index) of list" :key="title" :open="openIndex === index">
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
{{ title }}
</summary>
{{ content }}
</details>
</template>

<script>
import { defineComponent, ref, onBeforeUnmount } from 'vue'

export default defineComponent(() => {
const list = [{
title: 'html',
content: 'index.html'
}, {
title: 'css',
content: 'reset.css'
}, {
title: 'js',
content: 'main.js'
}]

const openIndex = ref(-1)

const interval = setInterval(() => openIndex.value === list.length
? openIndex.value = 0
: openIndex.value++
, 1000)

onBeforeUnmount(() => clearInterval(interval))

return { list, openIndex }
})
</script>

<style>
summary {
position: relative;
padding-left: 20px;
outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
position: absolute;
left: 0;
top: 50%;
transform: rotate(180deg);
transition: transform .2s;
fill: none;
stroke: gray
}

[open] > summary > svg { transform: none }
</style>
复制代码

运行结果:



既然能靠控制open属性来控制元素的展开收起,那么手风琴效果也很好实现了:只需要保证在当前列表中仅有一个<details>标签有open属性,点击别的标签时就去掉另一个标签的open属性即可:


<template>
<details
v-for="({title, content}, index) of list"
:key="title"
:open="openIndex === index"
@toggle="onChange($event, index)"
>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
{{ title }}
</summary>
{{ content }}
</details>
</template>

<script>
import { defineComponent, ref } from 'vue'

export default defineComponent(() => {
const list = [{
title: 'html',
content: 'index.html'
}, {
title: 'css',
content: 'reset.css'
}, {
title: 'js',
content: 'main.js'
}]

const openIndex = ref(-1)

const onChange = ({ target }, i) => target.open && (openIndex.value = i)

return { list, openIndex, onChange }
})
</script>

<style>
summary {
position: relative;
padding-left: 20px;
outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
position: absolute;
left: 0;
top: 50%;
transform: rotate(180deg);
transition: transform .2s;
fill: none;
stroke: gray
}

[open] > summary > svg { transform: none }
</style>
复制代码

运行结果:




⚠️需要注意的是,在<details>标签展开收起时会触发一个 toggle 事件,和 click、mousemove 等事件用法一致,也会接收一个 event 对象的参数,event.target 是当前触发事件的 DOM,也就是<details>,它会有一个.open属性,值为 true 或 false,代表是否展开收起。



加入动画


那么接下来离一个理想的手风琴效果只差最后一步了:过渡动画


但过渡动画这里有坑,我们先来分析一下思路:在平时就给<details>标签里的内容区(除第一个出现的

标签以外的内容)写上:max-height: 0;

然后在 open 时用属性选择器 [open] 配合后代选择器来给内容区加上 max-height: xxx; 的代码,这样平时在收起时高度就是0,等出现 open 属性时就会慢慢过渡到我们定义的最大高度:


<template>
<details
v-for="({title, content}, index) of list"
:key="title"
:open="openIndex === index"
@toggle="onChange($event, index)"
>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
{{ title }}
</summary>
<ul>
<li v-for="doc of content" :key="doc">{{ doc }}</li>
</ul>
</details>
</template>

<script>
import { defineComponent, ref } from 'vue'

export default defineComponent(() => {
const list = [{
title: 'html',
content: ['index.html', 'banner.html', 'login.html', '404.html']
}, {
title: 'css',
content: ['reset.css', 'header.css', 'banner.css', 'footer.css']
}, {
title: 'js',
content: ['index.js', 'main.js', 'javascript.js']
}]

const openIndex = ref(-1)

const onChange = ({ target }, i) => target.open && (openIndex.value = i)

return { list, openIndex, onChange }
})
</script>

<style>
summary {
position: relative;
padding-left: 20px;
outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
position: absolute;
left: 0;
top: 50%;
transform: rotate(180deg);
transition: transform .2s;
fill: none;
stroke: gray
}

details > ul {
max-height: 0;
margin: 0;
overflow: hidden;
}

[open] > summary > svg { transform: none }
[open] > ul { max-height: 120px }
</style>
复制代码

运行结果:



如果用谷歌浏览器打开的话居然看不到任何的过渡效果!但用火狐打开就有效果:



估计是浏览器的 bug,既然过渡动画(transition)在不同浏览器之间表现不一致,那关键帧动画(keyframes)呢?


<template>
<details
v-for="({title, content}, index) of list"
:key="title"
:open="openIndex === index"
@toggle="onChange($event, index)"
>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
{{ title }}
</summary>
<ul>
<li v-for="doc of content" :key="doc">{{ doc }}</li>
</ul>
</details>
</template>

<script>
import { defineComponent, ref } from 'vue'

export default defineComponent(() => {
const list = [{
title: 'html',
content: ['index.html', 'banner.html', 'login.html', '404.html']
}, {
title: 'css',
content: ['reset.css', 'header.css', 'banner.css', 'footer.css']
}, {
title: 'js',
content: ['index.js', 'main.js', 'javascript.js']
}]

const openIndex = ref(-1)

const onChange = ({ target }, i) => target.open && (openIndex.value = i)

return { list, openIndex, onChange }
})
</script>

<style lang="scss">
summary {
position: relative;
padding-left: 20px;
outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
position: absolute;
left: 0;
top: 50%;
transform: rotate(180deg);
transition: transform .2s;
fill: none;
stroke: gray
}

details > ul {
max-height: 0;
margin: 0;
overflow: hidden;
}

[open] {
> summary > svg { transform: none }
> ul { animation: open .2s both }
}

@keyframes open {
to { max-height: 120px }
}
</style>
复制代码

运行结果:



可以看到关键帧动画在各大浏览器的行为都是一致的,推荐大家使用关键帧动画。


收起动画


上面那种效果已经完全足够满足我们的日常开发需求了,但它仍然有一个小小的遗憾,那就是:收起的时候没有任何的动画效果。



这是因为<details>的行为是靠着 open 属性控制内容显示或隐藏,你可以简单的把它的隐藏理解为display: block;display: none;,虽然这么说可能并不准确,但却非常有助于我们理解<details>的行为:在展开时display: block;突然显示,既然显示了就可以有时间展示我们的展开动画。但在收起时display: none;是突然消失,根本没时间展示我们的收起动画。



那么怎么才能解决这个问题呢?答案就是更改 DOM 结构,我们把原本放在<details>里面那部分需要展开收起的内容元素移到<details>标签的外面去,但一定要在它的后一位,这样就可以方便我们用兄弟选择器配合属性选择器来控制外部元素的显隐了,在<details>标签有 open 属性时我们就让它的后面一个元素用动画展开,没有 open 属性时我们就让后一个元素用动画收起:


<template>
<template v-for="({title, content}, index) of list" :key="title">
<details
:open="openIndex === index"
@toggle="onChange($event, index)"
>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
{{ title }}
</summary>
</details>
<ul>
<li v-for="doc of content" :key="doc">{{ doc }}</li>
</ul>
</template>
</template>

<script>
import { defineComponent, ref } from 'vue'

export default defineComponent(() => {
const list = [{
title: 'html',
content: ['index.html', 'banner.html', 'login.html', '404.html']
}, {
title: 'css',
content: ['reset.css', 'header.css', 'banner.css', 'footer.css']
}, {
title: 'js',
content: ['index.js', 'main.js', 'javascript.js']
}]

const openIndex = ref(-1)

const onChange = ({ target }, i) => target.open && (openIndex.value = i)

return { list, openIndex, onChange }
})
</script>

<style lang="scss">
summary {
position: relative;
padding-left: 20px;
outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
position: absolute;
left: 0;
top: 50%;
transform: rotate(180deg);
transition: transform .2s;
fill: none;
stroke: gray
}

ul {
max-height: 0;
margin: 0;
transition: max-height .2s;
overflow: hidden
}

[open] {
> summary > svg { transform: none }
+ ul { max-height: 120px }
}
</style>
复制代码

运行结果:



结语


如果你的项目不需要这些花里胡哨的动画效果,完全可以只靠 H5 标签去实现,根本不必再去关心展开收起的逻辑了,只需要写一些样式代码就可以了,比如写成暗黑模式:



你的 CSS 只需要专注于暗黑模式本身就够了,是不是很省心呢?


同时这个收拉效果也并不仅仅只适用于手风琴,很多地方都可以用到它,比如这种:


但唯一比较遗憾的事就是这个标签不支持 IE:



不过好在别的浏览器支持的都不错,如果你的项目不需要兼容 IE 的话就请尽情的享受<details>标签所带来的便利吧!


作者:手撕红黑树
来源:https://juejin.cn/post/6912374170743472135
收起阅读 »

被尤雨溪推荐,这款开箱即用的Vue3组件库做对了什么

相信很多开发者都有过这样的想法:因为对某个技术栈或明星开源项目感兴趣,产生了开发拓展方向的新项目的想法与实践,同时也希冀于这个全新的开源项目也能如同别的优质开源项目一样受到关注,只是并非每个项目都能登上热门,获得高额 star 数。 不过,今天马建仓介绍的这...
继续阅读 »

相信很多开发者都有过这样的想法:因为对某个技术栈或明星开源项目感兴趣,产生了开发拓展方向的新项目的想法与实践,同时也希冀于这个全新的开源项目也能如同别的优质开源项目一样受到关注,只是并非每个项目都能登上热门,获得高额 star 数。



不过,今天马建仓介绍的这款开源项目的开发者,就曾在过去一年里实现了从零到一的华丽逆袭,让我们一起来瞧瞧这究竟是什么宝藏项目。


Varlet 是一个基于 Vue3 开发的 Material 风格移动端组件库,并在今年的 Vue JS Live 上被 Vue 的作者尤雨溪推荐。然而自这个项目诞生的时间不到一年。


从 Varlet 作者的某技术博客上得知,作者是一位专科毕业、在无锡工作的四川前端开发。去年,因所属单位打算开发某个与 Vue3 相关的组件库,机缘巧合下,作者自告奋勇包揽下这个活。然而,公司却因成本、投资回报等原因并不打算提供支持,随后作者搭档两位好友决心继续坚持下去。



这个组件库是基于 Material Design 的设计进行规范的,在此期间作者与合作的小伙伴们共同参考社区成品以及结合国内开发者感兴趣的 api 。对于为何选择 Material,作者在官方文档中这样描述:



在早期的移动端设备中,大色块以及强烈对比色,对显示设备要求很高,同时非线性动画和水波纹对 GPU 有一定要求。 导致 Material 风格并没有在移动端浏览器环境下有很好的体验,更多选择更扁平朴素的风格投入产品。 但随着现代设备和新的 js 框架运行时处理的效率的逐步提升,浏览器有了更多的空闲时间和能力去处理动画效果,Material Design 将会给应用带来更好的体验。



经历了多次的反复推敲之后,组件库隐约有了个雏形。打这时起, Varlet 也正式开源,并采用 MIT 开源许可证。



之后的日子里,Varlet 不仅获得阮一峰老师的推荐,同时也得到了国外开源技术社区的认可,其中 Vite 核心团队的 Antfu 大神也接受了这个组件库的 PR。不久前,在 Vue3 的 2021 年度总结分享会上,尤雨溪大神也推荐了 Varlet 。前段时间,在 Gitee 上开源的 varlet-ui 项目经过评估,也获得了Gitee的推荐,项目地址:gitee.com/varlet/varl…


那么 Varlet 究竟有着怎样的魅力,吸引着这么多大神与优质平台的推广呢?




从特性上看



  • 提供50个高质量通用组件

  • 组件十分轻量

  • 由国人开发,完善的中英文文档和后勤保障

  • 支持按需引入

  • 支持主题定制

  • 支持国际化

  • 支持 webstorm,vscode 组件属性高亮

  • 支持 SSR

  • 支持 Typescript

  • 确保90%以上单元测试覆盖率,提供稳定性保证

  • 支持暗黑模式


如何安装与部署


CDN


varlet.js 包含组件库的所有样式和逻辑, 因此只需引入即可。


<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/vue@next"></script>
<script src="https://cdn.jsdelivr.net/npm/@varlet/ui/umd/varlet.js"></script>
<script>
  const app = Vue.createApp({
    template: '<var-button>按钮</var-button>'
  })
  app.use(Varlet).mount('#app')
</script>
复制代码

Webpack/Vite


# 通过 npm、yarn 或 pnpm 安装

# npm
npm i @varlet/ui -S

# yarn
yarn add @varlet/ui

# pnpm
pnpm add @varlet/ui
复制代码

import App from './App.vue'
import Varlet from '@varlet/ui'
import { createApp } from 'vue'
import '@varlet/ui/es/style.js'

createApp(App).use(Varlet).mount('#app')
复制代码

如何引入?



手动引入


每一个组件都是一个 Vue 插件,并由组件逻辑和样式文件组成,如下方式进行手动引入使用。


import { createApp } from 'vue'
import { Button } from '@varlet/ui'
import '@varlet/ui/es/button/style/index.js'

createApp().use(Button)
复制代码

自动引入


所有在模板中的组件,都会被 unplugin-vue-components 插件自动扫描,插件会自动引入组件逻辑和样式文件并注册组件。


# 安装插件

# npm
npm i unplugin-vue-components -D

# yarn
yarn add unplugin-vue-components -D

# pnpm
pnpm add unplugin-vue-components -D
复制代码

Vue Cli


// vue.config.js
const Components = require('unplugin-vue-components/webpack')
const { VarletUIResolver } = require('unplugin-vue-components/resolvers')

module.exports = {
  configureWebpack: {
    plugins: [
      Components({
        resolvers: [VarletUIResolver()]
      })
    ]
  }
}
复制代码

Vite


// vite.config.js
import vue from '@vitejs/plugin-vue'
import components from 'unplugin-vue-components/vite'
import { VarletUIResolver } from 'unplugin-vue-components/resolvers'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [
    vue(),
    components({
      resolvers: [VarletUIResolver()]
    })
  ]
})
复制代码

注意


完成配置后如下使用即可


<template>
  <var-button>默认按钮</var-button>
</template>
复制代码

如何切换主题


该项目提供了暗黑模式的主题,暗黑模式的优势在于在弱光环境下具有更高的可读性。



<var-button block @click="toggleTheme">切换主题</var-button>
复制代码

import dark from '@varlet/ui/es/themes/dark'
import { StyleProvider } from '@varlet/ui'

export default {
  setup() {
    let currentTheme
    
    const toggleTheme = () => {
      currentTheme = currentTheme ? null : dark
      StyleProvider(currentTheme)
    }
    
    return { toggleTheme }
  }
}
复制代码

注入组件库推荐的文字颜色和背景颜色变量来控制整体颜色


body {
  transition: background-color .25s;
  color: var(--color-text);
  background-color: var(--color-body);
}
复制代码

样式展示




在线编辑地址


前往下列网址:varlet.gitee.io/varlet-ui/#…


点击界面右上方:


作者:Gitee
来源:https://juejin.cn/post/7075162881498562590
收起阅读 »

求求你们了,对自己代码质量有点要求!

开篇 最近在合并几个项目的代码,把功能拼一拼简称项目拼多多。 但是几个系统都没有 eslint 之类的东西,我也不知道怎么想的居然想给 拼多多 加上代码检查。 我还真的干了,于是就有了下面这些奇奇怪怪甚至有些可爱的代码。算是给大家做个反面教材。 一些示例 ...
继续阅读 »

开篇



  • 最近在合并几个项目的代码,把功能拼一拼简称项目拼多多

  • 但是几个系统都没有 eslint 之类的东西,我也不知道怎么想的居然想给 拼多多 加上代码检查。

  • 我还真的干了,于是就有了下面这些奇奇怪怪甚至有些可爱的代码。算是给大家做个反面教材。


一些示例




  • ps 示例代码来源于网络社区。



循环不要声明无用的变量


image.png


不要在 template中写很长的判断、运算,因为有个东西叫做计算属性。


image.png


使用 getCurrentInstance 获取 proxy 时候,请仔细想想你真的需要吗? 最重要的不要声明了但不使用它!


image.png


不要声明未使用变量函数!



  • 当然可能有时候,业务变更忘记改了! 如果是这样,那应该安装 eslint 并增加代码提交检查!


image.png


请在data 中声明所有已知变量及其子属性


image.png


请不要太随意的对文件进行命名



  • 如果有疑问可以查看vue风格指南那里会有答案!


image.png


请不要写一些奇怪的逻辑,如果写了请写上注释,对于重复的东西,有必要进行提取,这会使代码更整洁。


image.png


如果你使用了 v-for 请记得加上 key 不然它就像没穿内裤一样会很难受!


image.png


一个组件是需要一个名字的,就像人一样!


image.png


image.png


不要混用 v-if、v-for,更不要像下图这样写!



  • 组件在使用 v-for 遍历时 需要使用 v-if 判断是否加载,可以使用计算属性先处理一遍再把数据用于v-for遍历。

  • 下边这种写法,我猜测可能是数据不存在则不展示,但是 v-for 没有数据本身就不会展示啊!


image.png


不要混合使用使用不同的操作符


image.png


它是想做什么呢?



  • obj[next.id] 存在不做操作, 不存在赋值为 true 且执行 cur.push(next)


image.png


写vue的强烈建议查看官网的风格指南 猛击查看


作者:唐诗
来源:https://juejin.cn/post/7073049322656366622
收起阅读 »

如何在网页中使用响应式图像

或许你已经在网页设计领域见过响应式设计(responsive design)这个术语。响应式设计是让你的网页在不同尺寸的设备屏幕上能得到最佳展示,也就是让你的网页能对各种屏幕尺寸自适应。那么,什么是响应式图像呢?响应式图像与响应式设计有什么关系吗?我们为什么要...
继续阅读 »

或许你已经在网页设计领域见过响应式设计(responsive design)这个术语。

响应式设计是让你的网页在不同尺寸的设备屏幕上能得到最佳展示,也就是让你的网页能对各种屏幕尺寸自适应。

那么,什么是响应式图像呢?

响应式图像与响应式设计有什么关系吗?我们为什么要使用它们?

在本文中,我们就这些问题展开讨论。

什么是响应式图像

如今,图像已成为网页设计中必不可少的元素之一。

绝大多数的网站都会使用图像。

然而你是否知道,尽管你的网站布局可以适应设备尺寸,但显示的图像却不是自适应的?

无论使用何种设备(移动设备、平板或台式机),默认下载的都是相同的图像。

例如,如果图像大小为 2 MB,那么无论在何种设备上,下载的都是 2 MB 的图像数据。

开发者可以编写代码,在移动设备上显示该图像的一部分,但是仍然需要下载整个 2 MB 图像数据。

这是不合时宜的。

如果要为同一个网页下载多个图像,应该如何实现?

手机和平板上的图像本来应该是较小尺寸的,如果下载了大量较大尺寸的图像,肯定会影响性能。

我们需要为不同尺寸的设备提供不同尺寸的图像,移动设备显示小尺寸图像,平板显示中等尺寸的图像,台式机显示大尺寸的图像,该如何实现?

通过使用响应式图像,我们可以避免在较小的设备上下载不必要的图像数据,并提高网站在这些设备上的性能。

让我们看看如何实现这一目标。

HTML 中的响应式图像


以上面的图像为例。

这幅图像是为桌面应用设计的,在小屏幕设备上就需要对图像大小进行压缩,我们可以对这幅图像进行裁剪,而非下载完整的图像。


我们可以在 HTML 中编写以下内容,以便在不同的尺寸屏幕中下载不同的图像。

<img src="racoon.jpg" alt="Cute racoon"
    srcset="small-racoon.jpg 500w,
            medium-racoon.jpg 1000w,
            large-racoon.jpg 1500w" sizes="60vw"/>

让我们看下这段代码的作用。

<img> 标签负责在 HTML 中渲染图像,而 src 属性告诉浏览器默认显示哪个图像。在这种情况下,如果浏览器不支持 srcset 属性,则默认为 src 属性。

在这段代码中 srcset 属性是最重要的属性之一。

srcset 属性通知浏览器图像的合适宽度,浏览器不需要下载所有图像。通过 srcset 属性,浏览器决定下载哪个图像并且适应该视口宽度。

你可能还注意到 srcset 中每个图像大小的 w 描述符。

srcset="small-racoon.jpg 500w,
      medium-racoon.jpg 1000w,
      large-racoon.jsp 1500w"

上面代码片段中的 w 指定了 srcset 中图像的宽度(以像素为单位)。

还有一个 sizes 属性,它通知浏览器具有 srcset 属性的 <img> 元素的大小。

sizes="60vw"

在这里,sizes 属性的值为 60 vw,它告诉浏览器图像的宽度为视口的 60%size 属性帮助浏览器从 srcset 中为该视口宽度选择最佳图像。

例如,如果浏览器视口宽度为 992 px,那么

992 px60%

= 592 px

根据上面的计算,浏览器将选择宽度为 500 w500 px,最接近 592 px 的图像显示在屏幕上。

最终由浏览器决定选择哪个图像。

注意,为不同视口宽度选择图像的决策逻辑可能因浏览器而异,你可能会看到不同的结果。

为较小的设备下载较少的图像数据,可以让浏览器快速显示这些图像,从而提高网站的性能。

本文总结

网站加载缓慢的最主要原因是下载了 MB 级数据的图像。

使用响应式图像可以避免下载不必要的图像数据,从而减少网站的加载时间并提供更好的用户体验。

唯一的缺点是我们放弃了对浏览器的完全控制,让浏览器选择要在特定视口宽度下显示的图像。

每个浏览器都有不同的策略来选择适当的响应式图像。这就是为什么你可能会在不同的浏览器中,看到以相同分辨率加载的不同图像。

放弃对浏览器的控制,根据视口宽度显示图像以获得性能优势,你需要在实际应用时做权衡考虑。


以上就是本文全部内容,我希望通过本文,你能对响应式图像有进一步的了解,知道为什么应该考虑将它们应用于网站。

如果你有任何问题、建议或意见,请随时在下面的评论区留言分享。

感谢你的阅读!

本文参考:

Image Optimization — Addy Osmani

原文地址:What Are Responsive Images And Why You Should Use Them
原文作者:Nainy Sewaney
译者:Z招锦

收起阅读 »

聊聊我常用的两个可视化工具,Echarts和Tableau

由于工作里常常要做图表,Excel没法满足复杂场景,所以Echarts和Tableau成为了我最得力的两个助手。作为声名远扬的可视化工具,Echarts和Tableau,它们的性质不太一样。Echarts是一个纯JavaScript 的开源可视化图表库,使用者...
继续阅读 »

由于工作里常常要做图表,Excel没法满足复杂场景,所以Echarts和Tableau成为了我最得力的两个助手。

作为声名远扬的可视化工具,Echarts和Tableau,它们的性质不太一样。

Echarts是一个纯JavaScript 的开源可视化图表库,使用者只需要引用封装好的JS,就可以展示出绚丽的图表。

就在前不久,Echarts成为了Apache的顶级项目。Apache顶级项目的家族成员有哪些呢?Mavan、Hadoop、Spark、Flink…都是软件领域的顶流

Tableau是一个BI工具,商业化的PC端应用,只需要拖拉拽就可以制作丰富多样的图表、坐标图、仪表盘与报告。Tableau制作的可视化项目可以发布到web上,分享给其他人。

2019年,Tableau被Salesforce斥157 亿美元收购,可见这个BI工具不一般。

你可以把Echarts看成一个可视化仓库,每个可视化零件拿来即用,而且不限场合。而Tableau则像一个自给自足的可视化生态,你能在里面玩转各种可视化神技,但不能出这个生态。

先来说说Echarts

Echarts几乎提供了你能用到的所有图表形式,而且对国内开发环境非常友好,因为它是百度鼓捣出来的。


你看,不仅有常规的统计图表:

还有炫酷的3D可视化

Echarts大部分图表形式都封装到JS中,你只需要更改数据和样式,就可以应用到自己的项目中。


Echarts还有个用户社区,里面有非常多的作品展示,大家可以去逛逛。


某个热门作品-区域地图


学习Echarts最好是看官网教程,再配合练习。中文文档非常接地气。


给出几个常用的学习地址

官方文档:

https://echarts.apache.org/zh/tutorial.html

官方示例:

https://echarts.apache.org/examples/zh/index.html

用户作品专区:

https://www.makeapie.com/explore.html

再来说说Tableau

Tableau目前在国内慢慢流行起来,说起来做数据的小伙伴都会知道。

它适合做可视化看板,讲数据故事,符合现在数字化运营的管理。


这里简单介绍下Tableau的使用方法。

首先在Tableau官网下载desktop,然后无脑安装。

接下来新手操作三大步:

1、连接数据

可以连接excel、csv以及mysql等各种数据库


2、了解什么是度量和维度

度量就是数据表中的数值数据,维度是类别数据


3、看看tableau中的各类图表

柱状图、点图、线图、饼图、直方图、地图等等


走完基础后,就是整个的可视化分析展示流程:


其中的各个步骤需要详细说明一下:

  • 1、连接到数据源

Tableau连接到所有常用的数据源。它具有内置的连接器,在提供连接参数后负责建立连接。无论是简单文本文件,关系源,无Sql源或云数据库,tableau几乎连接到所有数据源。

  • 2、构建数据视图

连接到数据源后,您将获得Tableau环境中可用的所有列和数据。您可以将它们分为维,度量和创建任何所需的层次结构。使用这些,您构建的视图传统上称为报告。Tableau提供了轻松的拖放功能来构建视图。

  • 3、增强视图

上面创建的视图需要进一步增强使用过滤器,聚合,轴标签,颜色和边框的格式。

  • 4、创建工作表

我们创建不同的工作表,以便对相同的数据或不同的数据创建不同的视图。

  • 5、创建和组织仪表板

仪表板包含多个链接它的工作表。因此,任何工作表中的操作都可以相应地更改仪表板中的结果。

  • 6、创建故事

故事是一个工作表,其中包含一系列工作表或仪表板,它们一起工作以传达信息。您可以创建故事以显示事实如何连接,提供上下文,演示决策如何与结果相关,或者只是做出有说服力的案例。

完成这些,一张生动的dashboard就诞生了。


这其中,需要不断地练习熟稔tableau的每一个组件、函数、连接等等。

我们可以选择合适的可视化表达,让Tableau实现。


不要以为Tableau只提供简单的几种样式,如果你想做出炫酷的图表,Tableau也能完美支持。

看看大神们是怎么玩转Tabelau的。


还有一张我最喜欢的dashboard


因为Tableau是商业软件,所以它的官网中文教程非常详细。

最后也给到Tableau的几个学习地址

官方文档:

https://help.tableau.com/current/pro/desktop/zh-cn/default.htm

用户展示社区:

https://public.tableau.com/zh-cn/gallery

最后

如果是你想做可视化开发建议用echarts,如果想设计商业可视化报表则用Tableau。

欢迎留言区交流你做可视化的经验。

作者:朱卫军
来源:Python大数据分析

收起阅读 »

后端一次给你10万条数据,如何优雅展示,到底考察我什么?

前言 大家好,我是林三心,用最通俗的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心,今天跟大家来唠唠嗑,如果后端真的返回给前端10万条数据,咱们前端要怎么优雅地展示出来呢?(哈哈假设后端真的能传10万条数据到前端) 前置工作 先把前置工作给做好,后...
继续阅读 »

前言


大家好,我是林三心,用最通俗的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心,今天跟大家来唠唠嗑,如果后端真的返回给前端10万条数据,咱们前端要怎么优雅地展示出来呢?(哈哈假设后端真的能传10万条数据到前端)


image.png


前置工作


先把前置工作给做好,后面才能进行测试


后端搭建


新建一个server.js文件,简单起个服务,并返回给前端10w条数据,并通过nodemon server.js开启服务



没有安装nodemon的同学可以先全局安装npm i nodemon -g



// server.js

const http = require('http')
const port = 8000;

http.createServer(function (req, res) {
// 开启Cors
res.writeHead(200, {
//设置允许跨域的域名,也可设置*允许所有域名
'Access-Control-Allow-Origin': '*',
//跨域允许的请求方法,也可设置*允许所有方法
"Access-Control-Allow-Methods": "DELETE,PUT,POST,GET,OPTIONS",
//允许的header类型
'Access-Control-Allow-Headers': 'Content-Type'
})
let list = []
let num = 0

// 生成10万条数据的list
for (let i = 0; i < 100000; i++) {
num++
list.push({
src: 'https://p3-passport.byteacctimg.com/img/user-avatar/d71c38d1682c543b33f8d716b3b734ca~300x300.image',
text: `我是${num}号嘉宾林三心`,
tid: num
})
}
res.end(JSON.stringify(list));
}).listen(port, function () {
console.log('server is listening on port ' + port);
})
复制代码

前端页面


先新建一个index.html


// index.html

// 样式
<style>
* {
padding: 0;
margin: 0;
}
#container {
height: 100vh;
overflow: auto;
}
.sunshine {
display: flex;
padding: 10px;
}
img {
width: 150px;
height: 150px;
}
</style>

// html部分
<body>
<div id="container">
</div>
<script src="./index.js"></script>
</body>
复制代码

然后新建一个index.js文件,封装一个AJAX函数,用来请求这10w条数据


// index.js

// 请求函数
const getList = () => {
return new Promise((resolve, reject) => {
//步骤一:创建异步对象
var ajax = new XMLHttpRequest();
//步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数
ajax.open('get', 'http://127.0.0.1:8000');
//步骤三:发送请求
ajax.send();
//步骤四:注册事件 onreadystatechange 状态改变就会调用
ajax.onreadystatechange = function () {
if (ajax.readyState == 4 && ajax.status == 200) {
//步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的
resolve(JSON.parse(ajax.responseText))
}
}
})
}

// 获取container对象
const container = document.getElementById('container')
复制代码

直接渲染


最直接的方式就是直接渲染出来,但是这样的做法肯定是不可取的,因为一次性渲染出10w个节点,是非常耗时间的,咱们可以来看一下耗时,差不多要消耗12秒,非常消耗时间


截屏2021-11-18 下午10.07.45.png


const renderList = async () => {
console.time('列表时间')
const list = await getList()
list.forEach(item => {
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
container.appendChild(div)
})
console.timeEnd('列表时间')
}
renderList()
复制代码

setTimeout分页渲染


这个方法就是,把10w按照每页数量limit分成总共Math.ceil(total / limit)页,然后利用setTimeout,每次渲染1页数据,这样的话,渲染出首页数据的时间大大缩减了


截屏2021-11-18 下午10.14.46.png


const renderList = async () => {
console.time('列表时间')
const list = await getList()
console.log(list)
const total = list.length
const page = 0
const limit = 200
const totalPage = Math.ceil(total / limit)

const render = (page) => {
if (page >= totalPage) return
setTimeout(() => {
for (let i = page * limit; i < page * limit + limit; i++) {
const item = list[i]
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
container.appendChild(div)
}
render(page + 1)
}, 0)
}
render(page)
console.timeEnd('列表时间')
}
复制代码

requestAnimationFrame


使用requestAnimationFrame代替setTimeout,减少了重排的次数,极大提高了性能,建议大家在渲染方面多使用requestAnimationFrame


const renderList = async () => {
console.time('列表时间')
const list = await getList()
console.log(list)
const total = list.length
const page = 0
const limit = 200
const totalPage = Math.ceil(total / limit)

const render = (page) => {
if (page >= totalPage) return
// 使用requestAnimationFrame代替setTimeout
requestAnimationFrame(() => {
for (let i = page * limit; i < page * limit + limit; i++) {
const item = list[i]
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
container.appendChild(div)
}
render(page + 1)
})
}
render(page)
console.timeEnd('列表时间')
}
复制代码

文档碎片 + requestAnimationFrame


文档碎片的好处



  • 1、之前都是每次创建一个div标签就appendChild一次,但是有了文档碎片可以先把1页的div标签先放进文档碎片中,然后一次性appendChildcontainer中,这样减少了appendChild的次数,极大提高了性能

  • 2、页面只会渲染文档碎片包裹着的元素,而不会渲染文档碎片


const renderList = async () => {
console.time('列表时间')
const list = await getList()
console.log(list)
const total = list.length
const page = 0
const limit = 200
const totalPage = Math.ceil(total / limit)

const render = (page) => {
if (page >= totalPage) return
requestAnimationFrame(() => {
// 创建一个文档碎片
const fragment = document.createDocumentFragment()
for (let i = page * limit; i < page * limit + limit; i++) {
const item = list[i]
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
// 先塞进文档碎片
fragment.appendChild(div)
}
// 一次性appendChild
container.appendChild(fragment)
render(page + 1)
})
}
render(page)
console.timeEnd('列表时间')
}
复制代码

懒加载


为了比较通俗的讲解,咱们启动一个vue前端项目,后端服务还是开着


其实实现原理很简单,咱们通过一张图来展示,就是在列表尾部放一个空节点blank,然后先渲染第1页数据,向上滚动,等到blank出现在视图中,就说明到底了,这时候再加载第二页,往后以此类推。


至于怎么判断blank出现在视图上,可以使用getBoundingClientRect方法获取top属性



IntersectionObserver 性能更好,但是我这里就拿getBoundingClientRect来举例



截屏2021-11-18 下午10.41.01.png


<script setup lang="ts">
import { onMounted, ref, computed } from 'vue'
const getList = () => {
// 跟上面一样的代码
}

const container = ref<HTMLElement>() // container节点
const blank = ref<HTMLElement>() // blank节点
const list = ref<any>([]) // 列表
const page = ref(1) // 当前页数
const limit = 200 // 一页展示
// 最大页数
const maxPage = computed(() => Math.ceil(list.value.length / limit))
// 真实展示的列表
const showList = computed(() => list.value.slice(0, page.value * limit))
const handleScroll = () => {
// 当前页数与最大页数的比较
if (page.value > maxPage.value) return
const clientHeight = container.value?.clientHeight
const blankTop = blank.value?.getBoundingClientRect().top
if (clientHeight === blankTop) {
// blank出现在视图,则当前页数加1
page.value++
}
}

onMounted(async () => {
const res = await getList()
list.value = res
})
</script>

<template>
<div id="container" @scroll="handleScroll" ref="container">
<div class="sunshine" v-for="(item) in showList" :key="item.tid">
<img :src="item.src" />
<span>{{ item.text }}</span>
</div>
<div ref="blank"></div>
</div>
</template>
复制代码


作者:Sunshine_Lin
来源  :https://juejin.cn/post/7031923575044964389 收起阅读 »

前端到底用nginx来做啥

这篇文章是收集我在工作中经常会用到的nginx相关知识点,本文并不是基础知识的讲解更多的是一些方案中的简单实现。location的匹配规则= 表示精确匹配。只有请求的url路径与后面的字符串完全相等时,才会命中。^~ 表示如果该符号后面的字符是最佳匹配,采用该...
继续阅读 »

这篇文章是收集我在工作中经常会用到的nginx相关知识点,本文并不是基础知识的讲解更多的是一些方案中的简单实现。

location的匹配规则

  1. = 表示精确匹配。只有请求的url路径与后面的字符串完全相等时,才会命中。

  2. ^~ 表示如果该符号后面的字符是最佳匹配,采用该规则,不再进行后续的查找。

  3. ~ 表示该规则是使用正则定义的,区分大小写。

  4. ~* 表示该规则是使用正则定义的,不区分大小写。

注意的是,nginx的匹配优先顺序按照上面的顺序进行优先匹配,而且注意的是一旦某一个匹配命中直接退出,不再进行往下的匹配

剩下的普通匹配会按照最长匹配长度优先级来匹配,就是谁匹配的越多就用谁。

server {
   server_name website.com;
   location /document {
       return 701;
  }
   location ~* ^/docume.*$ {
       return 702;
  }
   location ~* ^/document$ {
       return 703;
  }

}
curl -I website.com:8080/document 702
# 匹配702 因为正则的优先级更高,而且正则是一旦匹配到就直接退出 所以不会再匹配703

server {
   server_name website.com;
   location ~* ^/docume.*$ {
       return 701;
  }

   location ^~ /doc {
       return 702;
  }
   location ~* ^/document$ {
       return 703;
  }
}
curl http://website.com/document
HTTP/1.1 702
# 匹配702 因为 ^~精确匹配的优先级比正则高 也是匹配到之后支持退出

server {
   server_name website.com;
   location /doc {
       return 702;
  }
   location /docu {
       return 701;
  }
}
# 701 前缀匹配匹配是按照最长匹配,跟顺序无关

history模式、跨域、缓存、反向代理

# html设置history模式
location / {
   index index.html index.htm;
   proxy_set_header Host $host;
   # history模式最重要就是这里
   try_files $uri $uri/ /index.html;
   # index.html文件不可以设置强缓存 设置协商缓存即可
   add_header Cache-Control 'no-cache, must-revalidate, proxy-revalidate, max-age=0';
}

# 接口反向代理
location ^~ /api/ {
   # 跨域处理 设置头部域名
   add_header Access-Control-Allow-Origin *;
   # 跨域处理 设置头部方法
   add_header Access-Control-Allow-Methods 'GET,POST,DELETE,OPTIONS,HEAD';
   # 改写路径
   rewrite ^/api/(.*)$ /$1 break;
   # 反向代理
   proxy_pass http://static_env;
   proxy_set_header Host $http_host;
}

location ~* \.(?:css(\.map)?|js(\.map)?|gif|svg|jfif|ico|cur|heic|webp|tiff?|mp3|m4a|aac|ogg|midi?|wav|mp4|mov|webm|mpe?g|avi|ogv|flv|wmv)$ {
   # 静态资源设置七天强缓存
   expires 7d;
   access_log off;
}

以目录去区分多个history单文件

因为不可能每一个项目开启一个域名,仅仅指向通过增加路径来划分多个网站,比如:

  1. http://www.taobao.com/tmall/login访问天猫的登录页面

  2. http://www.taobao.com/alipay/login访问支付宝的登录页面

server {
   listen 80;
   server_name taobao.com;
   index index.html index.htm;
   # 通过正则来匹配捕获 [tmall|alipay]中间的这个路径
   location ~ ^/([^\/]+)/(.*)$ {
       try_files $uri $uri/ /$1/dist/index.html =404;
  }
}

负载均衡

基于upstream做负载均衡,中间会涉及一些相关的策略比如ip_hashweight

upstream backserver{ 
   # 哈希算法,自动定位到该服务器 保证唯一ip定位到同一部机器 用于解决session登录态的问题
   ip_hash;
   server 127.0.0.1:9090 down; (down 表示单前的server暂时不参与负载)
   server 127.0.0.1:8080 weight=2; (weight 默认为1.weight越大,负载的权重就越大)
   server 127.0.0.1:6060;
   server 127.0.0.1:7070 backup; (其它所有的非backup机器down或者忙的时候,请求backup机器)
}

灰度部署

如何根据headers头部来进行灰度,下面的例子是用cookie来设置

如何获取头部值在nginx中可以通过$http_xxx来获取变量

upstream stable {
   server xxx max_fails=1 fail_timeout=60;
   server xxx max_fails=1 fail_timeout=60;
}
upstream canara {
  server xxx max_fails=1 fail_timeout=60;
}

server {
   listen 80;
   server_name xxx;
   # 设置默认
   set $group "stable";

   # 根据cookie头部设置接入的服务
   if ($http_cookie ~* "tts_version_id=canara"){
       set $group canara;
  }
   if ($http_cookie ~* "tts_version_id=stable"){
       set $group stable;
  }
   location / {
       proxy_pass http://$group;
       proxy_set_header   Host             $host;
       proxy_set_header   X-Real-IP       $remote_addr;
       proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
       index  index.html index.htm;
  }
}

优雅降级

常用于ssr的node服务挂了返回500错误码然后降级到csr的cos桶或者nginx中

优雅降级主要用error_page参数来进行降级指向备用地址。

upstream ssr {
   server xxx max_fails=1 fail_timeout=60;
   server xxx max_fails=1 fail_timeout=60;
}
upstream csr {
   server xxx max_fails=1 fail_timeout=60;
   server xxx max_fails=1 fail_timeout=60;
}

location ^~ /ssr/ {
   proxy_pass http://ssr;
   # 开启自定义错误捕获 如果这里不设置为on的话 会走向nginx处理的默认错误页面
   proxy_intercept_errors on;
   # 捕获500系列错误 如果500错误的话降级为下面的csr渲染
   error_page 500 501 502 503 504 = @csr_location

   # error_page 500 501 502 503 504 = 200 @csr_location
   # 注意这上面的区别 等号前面没有200 表示 最终返回的状态码已 @csr_location为准 加了200的话表示不管@csr_location返回啥都返回200状态码
}

location @csr_location {
   # 这时候地址还是带着/ssr/的要去除
   rewrite ^/ssr/(.*)$ /$1 break;
   proxy_pass http://csr;
   rewrite_log on;
}

webp根据浏览器自动降级为png

这套方案不像常见的由nginx把png转为webp的方案,而是先经由图床系统(node服务)上传两份图片:

  1. 一份是原图png

  2. 一份是png压缩为webp的图片(使用的是imagemin-webp)

然后通过nginx检测头部是否支持webp来返回webp图片,不支持的话就返回原图即可。这其中还做了错误拦截,如果cos桶丢失webp图片及时浏览器支持webp也要降级为png

http {
 include       /etc/nginx/mime.types;
 default_type application/octet-stream;

 # 设置日志格式
 log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
 '$status $body_bytes_sent "$http_referer" '
 '"$http_user_agent" "$http_x_forwarded_for"'
 '"$proxy_host" "$upstream_addr"';

 access_log /var/log/nginx/access.log main;

 sendfile       on;
 keepalive_timeout 65;

 # 开启gzip
 gzip on;
 gzip_vary on;
 gzip_proxied any;
 gzip_comp_level 6;
 gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;

 # 负载均衡 这里可以是多个cos桶地址即可
 upstream static_env {
   server xxx;
   server xxx;
}

 # map 设置变量映射 第一个变量指的是要通过映射的key值 Accpet 第二个值的是变量别名
 map $http_accept $webp_suffix {
   # 默认为 空字符串
   default   "";
   # 正则匹配如果Accep含有webp字段 设置为.webp值
   "~*webp"  ".webp";
}
 server {

   listen 8888;
   absolute_redirect off;    #取消绝对路径的重定向
   #网站主页路径。此路径仅供参考,具体请您按照实际目录操作。
   root /usr/share/nginx/html;

   location / {
     index index.html index.htm;
     proxy_set_header Host $host;
     try_files $uri $uri/ /index.html;
     add_header Cache-Control 'no-cache, max-age=0';
  }

   # favicon.ico
   location = /favicon.ico {
     log_not_found off;
     access_log off;
  }

   # robots.txt
   location = /robots.txt {
     log_not_found off;
     access_log off;
  }

   #
   location ~* \.(png|jpe?g)$ {
     # Pass WebP support header to backend
     # 如果header头部中支持webp
     if ($webp_suffix ~* webp) {
       # 先尝试找是否有webp格式图片
       rewrite ^/(.*)\.(png|jpe?g)$ /$1.webp break;
       # 找不到的话 这里捕获404错误 返回原始错误 注意这里的=号 代表最终返回的是@static_img的状态吗
       error_page 404 = @static_img;
    }
     proxy_intercept_errors on;
     add_header Vary Accept;
     proxy_pass http://static_env;
     proxy_set_header Host $http_host;
     expires 7d;
     access_log off;
  }

   location @static_img {
     #set $complete $schema $server_addr $request_uri;
     rewrite ^/.+$ $request_uri break;
     proxy_pass http://static_env;
     proxy_set_header Host $http_host;
     expires 7d;
  }


   # assets, media
   location ~* \.(?:css(\.map)?|js(\.map)?|gif|svg|jfif|ico|cur|heic|webp|tiff?|mp3|m4a|aac|ogg|midi?|wav|mp4|mov|webm|mpe?g|avi|ogv|flv|wmv)$ {
     proxy_pass http://static_env;
     proxy_set_header Host $http_host;
     expires 7d;
     access_log off;
  }


   error_page   500 502 503 504 /50x.html;
   location = /50x.html {
     root   /usr/share/nginx/html;
  }
}
}


作者:一米八的萝卜
来源:https://juejin.cn/post/7064378702779891749

收起阅读 »

专业前端怎么使用console

学习前端开发时,几乎最先学习的就是console.log()。毕竟多数人的第一行代码都是:console.log('Hello World');console对象提供了对于浏览器调试控制台的访问,可以从任何全局对象中访问到console对象。灵活运用conso...
继续阅读 »

学习前端开发时,几乎最先学习的就是console.log()

毕竟多数人的第一行代码都是:console.log('Hello World');

console对象提供了对于浏览器调试控制台的访问,可以从任何全局对象中访问到console对象。

灵活运用console对象所提供的方法,可以让开发变得更简单。

最常见的控制台方法:

console.log()– 打印内容的通用方法。
console.info()– 打印资讯类说明信息。
console.debug()– 在控制台打印一条 "debug" 级别的消息。
console.warn()– 打印一个警告信息。
console.error()– 打印一条错误信息。
复制代码


console.log()写css


console.log() 使用参数


console.clear();

用于清除控制台信息。


console.count(label);

输出count()被调用的次数,可以使用一个参数label。演示如下:

var user = "";

function greet() {
console.count(user);
return "hi " + user;
}

user = "bob";
greet();
user = "alice";
greet();
greet();
console.count("alice");
复制代码

输出


console.dir()

使用console.dir()可以打印对象的属性,在控制台中逐级查看对象的详细信息。


console.memory

console.memory是一个属性,而不是方法,使用memory属性用来检查内存信息。


console.time() 和 console.timeEnd()

  • console.time()– 使用输入参数的名称启动计时器。在给定页面上最多可以同时运行 10,000 个计时器。

  • console.timeEnd()– 停止指定的计时器并记录自启动以来经过的时间(以毫秒为单位)。


console.assert()

如果断言为假,将错误信息写入控制台,如果为真,无显示。


console.trace();

console.trace()方法将堆栈跟踪输出到控制台。


console.table();

console中还可以打印表格



打印Html元素


console.group() 和 console.groupEnd()

在控制台上创建一个新的分组,随后输出到控制台上的内容都会被添加到一个锁进,表示该内容属于当前分组,知道调用console.groupEnd()之后,当前分组结束。



作者:正经程序员
来源:https://juejin.cn/post/7065856171436933156

收起阅读 »

10个常见的前端手写功能,你全都会吗?

万丈高楼平地起,地基打的牢,才能永远立于不败之地。今天给大家带来的是10个常见的 JavaScript 手写功能,重要的地方已添加注释。有的是借鉴别人的,有的是自己写的,如有不正确的地方,欢迎多多指正。1、防抖function debounce(fn, del...
继续阅读 »

万丈高楼平地起,地基打的牢,才能永远立于不败之地。今天给大家带来的是10个常见的 JavaScript 手写功能,重要的地方已添加注释。有的是借鉴别人的,有的是自己写的,如有不正确的地方,欢迎多多指正。

1、防抖

function debounce(fn, delay) {
 let timer
 return function (...args) {
   if (timer) {
     clearTimeout(timer)
  }
   timer = setTimeout(() => {
     fn.apply(this, args)
  }, delay)
}
}

// 测试
function task() {
 console.log('run task')
}
const debounceTask = debounce(task, 1000)
window.addEventListener('scroll', debounceTask)
复制代码

2、节流

function throttle(fn, delay) {
 let last = 0 // 上次触发时间
 return (...args) => {
   const now = Date.now()
   if (now - last > delay) {
     last = now
     fn.apply(this, args)
  }
}
}

// 测试
function task() {
 console.log('run task')
}
const throttleTask = throttle(task, 1000)
window.addEventListener('scroll', throttleTask)
复制代码

3、深拷贝

function deepClone(obj, cache = new WeakMap()) {
 if (obj === null || typeof obj !== 'object') return obj
 if (obj instanceof Date) return new Date(obj)
 if (obj instanceof RegExp) return new RegExp(obj)
 
 if (cache.get(obj)) return cache.get(obj) // 如果出现循环引用,则返回缓存的对象,防止递归进入死循环
 let cloneObj = new obj.constructor() // 使用对象所属的构造函数创建一个新对象
 cache.set(obj, cloneObj) // 缓存对象,用于循环引用的情况

 for (let key in obj) {
   if (obj.hasOwnProperty(key)) {
     cloneObj[key] = deepClone(obj[key], cache) // 递归拷贝
  }
}
 return cloneObj
}

// 测试
const obj = { name: 'Jack', address: { x: 100, y: 200 } }
obj.a = obj // 循环引用
const newObj = deepClone(obj)
console.log(newObj.address === obj.address) // false
复制代码

4、手写 Promise

class MyPromise {
 constructor(executor) {
   this.status = 'pending' // 初始状态为等待
   this.value = null // 成功的值
   this.reason = null // 失败的原因
   this.onFulfilledCallbacks = [] // 成功的回调函数存放的数组
   this.onRejectedCallbacks = [] // 失败的回调函数存放的数组
   let resolve = value => {
     if (this.status === 'pending') {
       this.status = 'fulfilled'
       this.value = value;
       this.onFulfilledCallbacks.forEach(fn => fn()) // 调用成功的回调函数
    }
  }
   let reject = reason => {
     if (this.status === 'pending') {
       this.status = 'rejected'
       this.reason = reason
       this.onRejectedCallbacks.forEach(fn => fn()) // 调用失败的回调函数
    }
  };
   try {
     executor(resolve, reject)
  } catch (err) {
     reject(err)
  }
}
 then(onFulfilled, onRejected) {
   // onFulfilled如果不是函数,则修改为函数,直接返回value
   onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
   // onRejected如果不是函数,则修改为函数,直接抛出错误
   onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err }
   return new MyPromise((resolve, reject) => {
     if (this.status === 'fulfilled') {
       setTimeout(() => {
         try {
           let x = onFulfilled(this.value);
           x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
        } catch (err) {
           reject(err)
        }
      })
    }
     if (this.status === 'rejected') {
       setTimeout(() => {
         try {
           let x = onRejected(this.reason)
           x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
        } catch (err) {
           reject(err)
        }
      })
    }
     if (this.status === 'pending') {
       this.onFulfilledCallbacks.push(() => { // 将成功的回调函数放入成功数组
         setTimeout(() => {
           let x = onFulfilled(this.value)
           x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
        })
      })
       this.onRejectedCallbacks.push(() => { // 将失败的回调函数放入失败数组
         setTimeout(() => {
           let x = onRejected(this.reason)
           x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
        })
      })
    }
  })
}
}

// 测试
function p1() {
 return new MyPromise((resolve, reject) => {
   setTimeout(resolve, 1000, 1)
})
}
function p2() {
 return new MyPromise((resolve, reject) => {
   setTimeout(resolve, 1000, 2)
})
}
p1().then(res => {
 console.log(res) // 1
 return p2()
}).then(ret => {
 console.log(ret) // 2
})
复制代码

5、异步控制并发数

function limitRequest(urls = [], limit = 3) {
 return new Promise((resolve, reject) => {
   const len = urls.length
   let count = 0

   // 同时启动limit个任务
   while (limit > 0) {
     start()
     limit -= 1
  }

   function start() {
     const url = urls.shift() // 从数组中拿取第一个任务
     if (url) {
       axios.post(url).then(res => {
         // todo
      }).catch(err => {
         // todo
      }).finally(() => {
         if (count == len - 1) {
           // 最后一个任务完成
           resolve()
        } else {
           // 完成之后,启动下一个任务
           count++
           start()
        }
      })
    }
  }

})
}

// 测试
limitRequest(['http://xxa', 'http://xxb', 'http://xxc', 'http://xxd', 'http://xxe'])
复制代码

6、继承

ES5继承(寄生组合继承)

function Parent(name) {
 this.name = name
}
Parent.prototype.eat = function () {
 console.log(this.name + ' is eating')
}

function Child(name, age) {
 Parent.call(this, name)
 this.age = age
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.contructor = Child

// 测试
let xm = new Child('xiaoming', 12)
console.log(xm.name) // xiaoming
console.log(xm.age) // 12
xm.eat() // xiaoming is eating
复制代码

ES6继承

class Parent {
 constructor(name) {
   this.name = name
}
 eat() {
   console.log(this.name + ' is eating')
}
}

class Child extends Parent {
 constructor(name, age) {
   super(name)
   this.age = age
}
}

// 测试
let xm = new Child('xiaoming', 12)
console.log(xm.name) // xiaoming
console.log(xm.age) // 12
xm.eat() // xiaoming is eating
复制代码

7、数组排序

sort 排序

// 对数字进行排序,简写
const arr = [3, 2, 4, 1, 5]
arr.sort((a, b) => a - b)
console.log(arr) // [1, 2, 3, 4, 5]

// 对字母进行排序,简写
const arr = ['b', 'c', 'a', 'e', 'd']
arr.sort()
console.log(arr) // ['a', 'b', 'c', 'd', 'e']
复制代码

冒泡排序

function bubbleSort(arr) {
let len = arr.length
for (let i = 0; i < len - 1; i++) {
// 从第一个元素开始,比较相邻的两个元素,前者大就交换位置
for (let j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
let num = arr[j]
arr[j] = arr[j + 1]
arr[j + 1] = num
}
}
// 每次遍历结束,都能找到一个最大值,放在数组最后
}
return arr
}

//测试
console.log(bubbleSort([2, 3, 1, 5, 4])) // [1, 2, 3, 4, 5]
复制代码

8、数组去重

Set 去重

const newArr = [...new Set(arr)]
// 或
const newArr = Array.from(new Set(arr))
复制代码

indexOf 去重

function resetArr(arr) {
 let res = []
 arr.forEach(item => {
   if (res.indexOf(item) === -1) {
     res.push(item)
  }
})
 return res
}

// 测试
const arr = [1, 1, 2, 3, 3]
console.log(resetArr(arr)) // [1, 2, 3]
复制代码

9、获取 url 参数

URLSearchParams 方法

// 创建一个URLSearchParams实例
const urlSearchParams = new URLSearchParams(window.location.search);
// 把键值对列表转换为一个对象
const params = Object.fromEntries(urlSearchParams.entries());
复制代码

split 方法

function getParams(url) {
 const res = {}
 if (url.includes('?')) {
   const str = url.split('?')[1]
   const arr = str.split('&')
   arr.forEach(item => {
     const key = item.split('=')[0]
     const val = item.split('=')[1]
     res[key] = decodeURIComponent(val) // 解码
  })
}
 return res
}

// 测试
const user = getParams('http://www.baidu.com?user=%E9%98%BF%E9%A3%9E&age=16')
console.log(user) // { user: '阿飞', age: '16' }
复制代码

10、事件总线 | 发布订阅模式

class EventEmitter {
 constructor() {
   this.cache = {}
}

 on(name, fn) {
   if (this.cache[name]) {
     this.cache[name].push(fn)
  } else {
     this.cache[name] = [fn]
  }
}

 off(name, fn) {
   const tasks = this.cache[name]
   if (tasks) {
     const index = tasks.findIndex((f) => f === fn || f.callback === fn)
     if (index >= 0) {
       tasks.splice(index, 1)
    }
  }
}

 emit(name, once = false) {
   if (this.cache[name]) {
     // 创建副本,如果回调函数内继续注册相同事件,会造成死循环
     const tasks = this.cache[name].slice()
     for (let fn of tasks) {
       fn();
    }
     if (once) {
       delete this.cache[name]
    }
  }
}
}

// 测试
const eventBus = new EventEmitter()
const task1 = () => { console.log('task1'); }
const task2 = () => { console.log('task2'); }

eventBus.on('task', task1)
eventBus.on('task', task2)
eventBus.off('task', task1)
setTimeout(() => {
 eventBus.emit('task') // task2
}, 1000)
复制代码

以上就是工作或求职中最常见的手写功能,你是不是全都掌握了呢,欢迎在评论区交流。如果文章对你有所帮助,


作者:前端阿飞
来源:https://juejin.cn/post/7031322059414175774

收起阅读 »

卧槽!用代码实现冰墩墩,太浪漫了吧

声明:本文涉及奥运元素3D模型仅用于个人学习、研究和欣赏,请勿二次修改、非法传播、转载、出版、商用、及进行其他获利行为。背景迎冬奥,一起向未来!2022冬奥会马上就要开始了,本文使用 Three.js + React 技术栈,实现冬日和奥运...
继续阅读 »

声明:本文涉及奥运元素3D模型仅用于个人学习、研究和欣赏,请勿二次修改、非法传播、转载、出版、商用、及进行其他获利行为。

背景

迎冬奥,一起向未来!2022冬奥会马上就要开始了,本文使用 Three.js + React 技术栈,实现冬日和奥运元素,制作了一个充满趣味和纪念意义的冬奥主题 3D 页面。本文涉及到的知识点主要包括:TorusGeometry 圆环面、MeshLambertMaterial 非光泽表面材质、MeshDepthMaterial 深度网格材质、custromMaterial 自定义材质、Points 粒子、PointsMaterial 点材质等。

效果

实现效果如以下 👇 动图所示,页面主要由 2022 冬奥会吉祥物 冰墩墩 、奥运五环、舞动的旗帜 🚩、树木 🌲 以及下雪效果 ❄️ 等组成。按住鼠标左键移动可以改为相机位置,获得不同视图。

pic_b005c37f.png

👀 在线预览: https://dragonir.github.io/3d… (部署在 GitHub,加载速度可能会有点慢 😓

实现

引入资源

首先引入开发页面所需要的库和外部资源,OrbitControls 用于镜头轨道控制、TWEEN 用于补间动画实现、GLTFLoader 用于加载 glb 或 gltf 格式的 3D 模型、以及一些其他模型、贴图等资源。

import React from 'react';
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { TWEEN } from "three/examples/jsm/libs/tween.module.min.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import bingdundunModel from './models/bingdundun.glb';
// ...

页面DOM结构

页面 DOM 结构非常简单,只有渲染 3D 元素的 #container 容器和显示加载进度的 .olympic_loading元素。


{this.state.loadingProcess === 100 ? '' : (

{this.state.loadingProcess} %


)}


场景初始化

初始化渲染容器、场景、相机。关于这部分内容的详细知识点,可以查阅我往期的文章,本文中不再赘述。

container = document.getElementById('container');
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
container.appendChild(renderer.domElement);
scene = new THREE.Scene();
scene.background = new THREE.TextureLoader().load(skyTexture);
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 30, 100);
camera.lookAt(new THREE.Vector3(0, 0, 0));

添加光源

本示例中主要添加了两种光源:DirectionalLight 用于产生阴影,调节页面亮度、AmbientLight 用于渲染环境氛围。

// 直射光
const light = new THREE.DirectionalLight(0xffffff, 1);
light.intensity = 1;
light.position.set(16, 16, 8);
light.castShadow = true;
light.shadow.mapSize.width = 512 * 12;
light.shadow.mapSize.height = 512 * 12;
light.shadow.camera.top = 40;
light.shadow.camera.bottom = -40;
light.shadow.camera.left = -40;
light.shadow.camera.right = 40;
scene.add(light);
// 环境光
const ambientLight = new THREE.AmbientLight(0xcfffff);
ambientLight.intensity = 1;
scene.add(ambientLight);

加载进度管理

使用 THREE.LoadingManager 管理页面模型加载进度,在它的回调函数中执行一些与加载进度相关的方法。本例中的页面加载进度就是在 onProgress 中完成的,当页面加载进度为 100% 时,执行 TWEEN 镜头补间动画。

const manager = new THREE.LoadingManager();
manager.onStart = (url, loaded, total) => {};
manager.onLoad = () => { console.log('Loading complete!')};
manager.onProgress = (url, loaded, total) => {
if (Math.floor(loaded / total * 100) === 100) {
this.setState({ loadingProcess: Math.floor(loaded / total * 100) });
// 镜头补间动画
Animations.animateCamera(camera, controls, { x: 0, y: -1, z: 20 }, { x: 0, y: 0, z: 0 }, 3600, () => {});
} else {
this.setState({ loadingProcess: Math.floor(loaded / total * 100) });
}
};

创建地面

本示例中凹凸起伏的地面是使用 Blender 构建模型,然后导出 glb 格式加载创建的。当然也可以只使用 Three.js 自带平面网格加凹凸贴图也可以实现类似的效果。使用 Blender 自建模型的优点在于可以自由可视化地调整地面的起伏效果。

var loader = new THREE.GLTFLoader(manager);
loader.load(landModel, function (mesh) {
mesh.scene.traverse(function (child) {
if (child.isMesh) {
child.material.metalness = .1;
child.material.roughness = .8;
// 地面
if (child.name === 'Mesh_2') {
child.material.metalness = .5;
child.receiveShadow = true;
}
});
mesh.scene.rotation.y = Math.PI / 4;
mesh.scene.position.set(15, -20, 0);
mesh.scene.scale.set(.9, .9, .9);
land = mesh.scene;
scene.add(land);
});

pic_9d8f0ff5.png

创建冬奥吉祥物冰墩墩

现在添加可爱的冬奥会吉祥物熊猫冰墩墩 🐼,冰墩墩同样是使用 glb 格式模型加载的。它的原始模型来源于这里,从这个网站免费现在模型后,原模型是使用 3D max 建的我发现并不能直接用在网页中,需要在 Blender 中转换模型格式,还需要调整调整模型的贴图法线,才能还原渲染图效果。

原模型:

pic_23ebc67c.png

冰墩墩贴图:

pic_bef8c61c.png

转换成Blender支持的模型,并在Blender中调整模型贴图法线、并添加贴图:

pic_193e130a.png

导出glb格式:

pic_e3005891.png

📖 在 Blender 中给模型添加贴图教程传送门: 在Blender中怎么给模型贴图

仔细观察冰墩墩 🐼可以发现,它的外面有一层透明塑料或玻璃质感外壳,这个效果可以通过修改模型的透明度、金属度、粗糙度等材质参数实现,最后就可以渲染出如 👆 banner图 所示的那种效果,具体如以下代码所示。

loader.load(bingdundunModel, mesh => {
mesh.scene.traverse(child => {
if (child.isMesh) {
// 内部
if (child.name === 'oldtiger001') {
child.material.metalness = .5
child.material.roughness = .8
}
// 半透明外壳
if (child.name === 'oldtiger002') {
child.material.transparent = true;
child.material.opacity = .5
child.material.metalness = .2
child.material.roughness = 0
child.material.refractionRatio = 1
child.castShadow = true;
}
}
});
mesh.scene.rotation.y = Math.PI / 24;
mesh.scene.position.set(-8, -12, 0);
mesh.scene.scale.set(24, 24, 24);
scene.add(mesh.scene);
});

创建奥运五环

奥运五环由基础几何模型圆环面 TorusGeometry 来实现,创建五个圆环面,并调整它们的材质颜色和位置来构成蓝黑红黄绿顺序的五环结构。五环材质使用的是 MeshLambertMaterial

const fiveCycles = [
{ key: 'cycle_0', color: 0x0885c2, position: { x: -250, y: 0, z: 0 }},
{ key: 'cycle_1', color: 0x000000, position: { x: -10, y: 0, z: 5 }},
{ key: 'cycle_2', color: 0xed334e, position: { x: 230, y: 0, z: 0 }},
{ key: 'cycle_3', color: 0xfbb132, position: { x: -125, y: -100, z: -5 }},
{ key: 'cycle_4', color: 0x1c8b3c, position: { x: 115, y: -100, z: 10 }}
];
fiveCycles.map(item => {
let cycleMesh = new THREE.Mesh(new THREE.TorusGeometry(100, 10, 10, 50), new THREE.MeshLambertMaterial({
color: new THREE.Color(item.color),
side: THREE.DoubleSide
}));
cycleMesh.castShadow = true;
cycleMesh.position.set(item.position.x, item.position.y, item.position.z);
meshes.push(cycleMesh);
fiveCyclesGroup.add(cycleMesh);
});
fiveCyclesGroup.scale.set(.036, .036, .036);
fiveCyclesGroup.position.set(0, 10, -8);
scene.add(fiveCyclesGroup);

💡 TorusGeometry 圆环面

TorusGeometry 一个用于生成圆环几何体的类。

构造函数:

TorusGeometry(radius: Float, tube: Float, radialSegments: Integer, tubularSegments: Integer, arc: Float)
  • radius:圆环的半径,从圆环的中心到管道(横截面)的中心。默认值是 1
  • tube:管道的半径,默认值为 0.4
  • radialSegments:圆环的分段数,默认值为 8
  • tubularSegments:管道的分段数,默认值为 6
  • arc:圆环的圆心角(单位是弧度),默认值为 Math.PI * 2

💡 MeshLambertMaterial 非光泽表面材质

一种非光泽表面的材质,没有镜面高光。该材质使用基于非物理的 Lambertian 模型来计算反射率。这可以很好地模拟一些表面(例如未经处理的木材或石材),但不能模拟具有镜面高光的光泽表面(例如涂漆木材)。

构造函数:

MeshLambertMaterial(parameters : Object)
  • parameters:(可选)用于定义材质外观的对象,具有一个或多个属性。材质的任何属性都可以从此处传入。

创建旗帜

旗面模型是从sketchfab下载的,还需要一个旗杆,可以在 Blender中添加了一个柱状立方体,并调整好合适的长宽高和旗面结合起来。

pic_9352c4e1.png

旗面贴图:

pic_6e3c199f.png

旗面添加了动画,需要在代码中执行动画帧播放。

loader.load(flagModel, mesh => {
mesh.scene.traverse(child => {
if (child.isMesh) {
child.castShadow = true;
// 旗帜
if (child.name === 'mesh_0001') {
child.material.metalness = .1;
child.material.roughness = .1;
child.material.map = new THREE.TextureLoader().load(flagTexture);
}
// 旗杆
if (child.name === '柱体') {
child.material.metalness = .6;
child.material.roughness = 0;
child.material.refractionRatio = 1;
child.material.color = new THREE.Color(0xeeeeee);
}
}
});
mesh.scene.rotation.y = Math.PI / 24;
mesh.scene.position.set(2, -7, -1);
mesh.scene.scale.set(4, 4, 4);
// 动画
let meshAnimation = mesh.animations[0];
mixer = new THREE.AnimationMixer(mesh.scene);
let animationClip = meshAnimation;
let clipAction = mixer.clipAction(animationClip).play();
animationClip = clipAction.getClip();
scene.add(mesh.scene);
});

创建树木

为了充实画面,营造冬日氛围,于是就添加了几棵松树 🌲 作为装饰。添加松树的时候用到一个技巧非常重要:我们知道因为树的模型非常复杂,有非常多的面数,面数太多会降低页面性能,造成卡顿。本文中使用两个如下图 👇 所示的两个交叉的面来作为树的基座,这样的话树只有两个面数,使用这个技巧可以和大程度上优化页面性能,而且树 🌲 的样子看起来也是有 3D 感的。

pic_5929966b.png

材质贴图:

pic_f6f036b7.png

为了使树只在贴图透明部分透明、其他地方不透明,并且可以产生树状阴影而不是长方体阴影,需要给树模型添加如下 MeshPhysicalMaterialMeshDepthMaterial 两种材质,两种材质使用同样的纹理贴图,其中 MeshDepthMaterial 添加到模型的 custromMaterial 属性上。

let treeMaterial = new THREE.MeshPhysicalMaterial({
map: new THREE.TextureLoader().load(treeTexture),
transparent: true,
side: THREE.DoubleSide,
metalness: .2,
roughness: .8,
depthTest: true,
depthWrite: false,
skinning: false,
fog: false,
reflectivity: 0.1,
refractionRatio: 0,
});
let treeCustomDepthMaterial = new THREE.MeshDepthMaterial({
depthPacking: THREE.RGBADepthPacking,
map: new THREE.TextureLoader().load(treeTexture),
alphaTest: 0.5
});
loader.load(treeModel, mesh => {
mesh.scene.traverse(child =>{
if (child.isMesh) {
child.material = treeMaterial;
child.custromMaterial = treeCustomDepthMaterial;
}
});
mesh.scene.position.set(14, -9, 0);
mesh.scene.scale.set(16, 16, 16);
scene.add(mesh.scene);
// 克隆另两棵树
let tree2 = mesh.scene.clone();
tree2.position.set(10, -8, -15);
tree2.scale.set(18, 18, 18);
scene.add(tree2)
// ...
});

实现效果也可以从 👆 上面 Banner 图中可以看到,为了画面更好看,我取消了树的阴影显示。

📌 在 3D 功能开发中,一些不重要的装饰模型都可以采取这种策略来优化。

💡 MeshDepthMaterial 深度网格材质

一种按深度绘制几何体的材质。深度基于相机远近平面,白色最近,黑色最远。

构造函数:

MeshDepthMaterial(parameters: Object)
  • parameters:(可选)用于定义材质外观的对象,具有一个或多个属性。材质的任何属性都可以从此处传入。

特殊属性:

  • .depthPacking[Constant]depth packing 的编码。默认为 BasicDepthPacking
  • .displacementMap[Texture]:位移贴图会影响网格顶点的位置,与仅影响材质的光照和阴影的其他贴图不同,移位的顶点可以投射阴影,阻挡其他对象,以及充当真实的几何体。
  • .displacementScale[Float]:位移贴图对网格的影响程度(黑色是无位移,白色是最大位移)。如果没有设置位移贴图,则不会应用此值。默认值为 1
  • .displacementBias[Float]:位移贴图在网格顶点上的偏移量。如果没有设置位移贴图,则不会应用此值。默认值为 0

💡 custromMaterial 自定义材质

给网格添加 custromMaterial 自定义材质属性,可以实现透明外围 png 图片贴图的内容区域阴影。

创建雪花

创建雪花 ❄️,就要用到粒子知识。THREE.Points 是用来创建点的类,也用来批量管理粒子。本例中创建了 1500 个雪花粒子,并为它们设置了限定三维空间的随机坐标及横向和竖向的随机移动速度。

// 雪花贴图
let texture = new THREE.TextureLoader().load(snowTexture);
let geometry = new THREE.Geometry();
let range = 100;
let pointsMaterial = new THREE.PointsMaterial({
size: 1,
transparent: true,
opacity: 0.8,
map: texture,
// 背景融合
blending: THREE.AdditiveBlending,
// 景深衰弱
sizeAttenuation: true,
depthTest: false
});
for (let i = 0; i < 1500; i++) {
let vertice = new THREE.Vector3(Math.random() * range - range / 2, Math.random() * range * 1.5, Math.random() * range - range / 2);
// 纵向移速
vertice.velocityY = 0.1 + Math.random() / 3;
// 横向移速
vertice.velocityX = (Math.random() - 0.5) / 3;
// 加入到几何
geometry.vertices.push(vertice);
}
geometry.center();
points = new THREE.Points(geometry, pointsMaterial);
points.position.y = -30;
scene.add(points);

💡 Points 粒子

Three.js 中,雨 🌧️、雪 ❄️、云 ☁️、星辰  等生活中常见的粒子都可以使用 Points 来模拟实现。

构造函数:

new THREE.Points(geometry, material);
  • 构造函数可以接受两个参数,一个几何体和一个材质,几何体参数用来制定粒子的位置坐标,材质参数用来格式化粒子;
  • 可以基于简单几何体对象如 BoxGeometrySphereGeometry等作为粒子系统的参数;
  • 一般来讲,需要自己指定顶点来确定粒子的位置。

💡 PointsMaterial 点材质

通过 THREE.PointsMaterial 可以设置粒子的属性参数,是 Points 使用的默认材质。

构造函数:

PointsMaterial(parameters : Object)
  • parameters:(可选)用于定义材质外观的对象,具有一个或多个属性。材质的任何属性都可以从此处传入。

💡 材质属性 .blending

材质的.blending 属性主要控制纹理融合的叠加方式,.blending 属性的值包括:

  • THREE.NormalBlending:默认值
  • THREE.AdditiveBlending:加法融合模式
  • THREE.SubtractiveBlending:减法融合模式
  • THREE.MultiplyBlending:乘法融合模式
  • THREE.CustomBlending:自定义融合模式,与 .blendSrc.blendDst 或 .blendEquation 属性组合使用

💡 材质属性 .sizeAttenuation

粒子的大小是否会被相机深度衰减,默认为 true(仅限透视相机)。

💡 Three.js 向量

几维向量就有几个分量,二维向量 Vector2 有 x 和 y 两个分量,三维向量 Vector3 有xyz 三个分量,四维向量 Vector4 有 xyzw 四个分量。

相关API:

  • Vector2:二维向量
  • Vector3:三维向量
  • Vector4:四维向量

镜头控制、缩放适配、动画

controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0, 0);
controls.enableDamping = true;
// 禁用平移
controls.enablePan = false;
// 禁用缩放
controls.enableZoom = false;
// 垂直旋转角度限制
controls.minPolarAngle = 1.4;
controls.maxPolarAngle = 1.8;
// 水平旋转角度限制
controls.minAzimuthAngle = -.6;
controls.maxAzimuthAngle = .6;
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}, false);
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
controls && controls.update();
// 旗帜动画更新
mixer && mixer.update(new THREE.Clock().getDelta());
// 镜头动画
TWEEN && TWEEN.update();
// 五环自转
fiveCyclesGroup && (fiveCyclesGroup.rotation.y += .01);
// 顶点变动之后需要更新,否则无法实现雨滴特效
points.geometry.verticesNeedUpdate = true;
// 雪花动画更新
let vertices = points.geometry.vertices;
vertices.forEach(function (v) {
v.y = v.y - (v.velocityY);
v.x = v.x - (v.velocityX);
if (v.y <= 0) v.y = 60;
if (v.x <= -20 || v.x >= 20) v.velocityX = v.velocityX * -1;
});
}

🔗 完整代码: https://github.com/dragonir/3…

总结

💡 本文中主要包含的新知识点包括:

  • TorusGeometry 圆环面
  • MeshLambertMaterial 非光泽表面材质
  • MeshDepthMaterial 深度网格材质
  • custromMaterial 自定义材质
  • Points 粒子
  • PointsMaterial 点材质
  • 材质属性 .blending.sizeAttenuation
  • Three.js 向量

进一步优化的空间:

  • 添加更多的交互功能、界面样式进一步优化;
  • 吉祥物冰墩墩添加骨骼动画,并可以通过鼠标和键盘控制其移动和交互。

作者:dragonir

来源:https://segmentfault.com/a/1190000041363089

收起阅读 »

压缩11000条 key 减少 7.2M,飞书如何实现 i18n 前端体积优化

背景在推进国际化的进程中,涌现出很多方案可以帮大家实现国际化文案定义以及使用。在飞书前端架构中,国际化文案已经做到了按需引入及按需加载,只不过随着业务的发展,国际化文案数量逐渐增多。再来看代码中的文案部分,key 长度越来越长,这部分都属于无用代码,如果能够缩...
继续阅读 »

背景

在推进国际化的进程中,涌现出很多方案可以帮大家实现国际化文案定义以及使用。在飞书前端架构中,国际化文案已经做到了按需引入及按需加载,只不过随着业务的发展,国际化文案数量逐渐增多。再来看代码中的文案部分,key 长度越来越长,这部分都属于无用代码,如果能够缩短,可以节省部分代码体积,加快 js 在浏览器中运行的速度

如何做?

通过压缩 i18nkey 的方式,将 i18n 的 key 从字母压缩为短字符串。目前业界中为了提升 webpack 打包速度,发展出很多利用多进程进行 js 编译的方案。飞书前端为了提高 webpack 编译速度,大量使用了 thread-loader 进行并发编译,i18n 扫描则采用了 babel 插件进行扫描和统计,那如何在 babel 扫描的过程中将扫描结果收集起来,如何将运行时的 key 更换为更短的 key,并且能够按照文件归类,实现按需加载呢?

思路

  1. 在 webpack 编译之前,先拿到当前业务下载的文案列表,将列表中所有的 key 进行编码,编码后的长度应该越短越好;

  2. 在 babel loader 扫描的过程中,将用到的文案上报,并将引入文案时使用的 key,替换为短编码;

  3. 在扫描完成后,生成文案的部分,使用编码后的短字符串,作为文案的 key,打包进文案文件中。

具体代码

编码方式

将下载的所有 i18n 的 key 进行一次编码映射,通过 key 在数组中的 index,做一个 26 进制转换,再把转换后的字符串中的数字填充为剩余的未用到的字母,保证 key 中无数字,可获得一个不超过 5 位的短 key。

  const NUMBER_MAP = {
  0: 'q',
  1: 'r',
  2: 's',
  3: 't',
  4: 'u',
  5: 'v',
  6: 'w',
  7: 'x',
  8: 'y',
  9: 'z',
};
const i18nKeys = Object.keys(resources['zh-CN']).reduce((all: object, key: string, index: number) => {
  // 将i18n的key重新编码,编码成26进制,然后用字母替换掉所有数字。
  // 因为变量名称不能用数字开头,所以需要替换掉所有数字
  all[key] = index.toString(26).replace(/\d/g, (s) => NUMBER_MAP[s]);
  return all;
}, {});

最初的设想中如果有从某个 enum 中引入 key 的行为,可以将 enum 的成员名字一起缩短,所以采用了替换所有数字的方式,保证短 key 不会以数字开头,后来在开发过程中发现没有这种用法,但是编码方式还是保留下来了。

扫描方式

借助 babel plugin 强大的 ast api,可以轻松完成 i18n key 的扫描和替换。

export default function babelI18nPlugin(options, args: {i18nKeys: {[key: string]: string}}) {
const i18nKeys = args.i18nKeys;

return {
  visitor: {
    StringLiteral: (tree, module) => {
      const { node, parentPath: {
        node: parent, scope, type
      } } = tree;
      const { filename } = module;
      if (!shouldAnalyse(filename)) {
        return;
      }
      const stringValue = node.value;
      if (stringValue && i18nKeys.hasOwnProperty(stringValue)) {
        if (
          /**
            * 飞书前端中使用了 __Text 和 _t 的全局方法来获得对应的文案内容,所以在这里限定了只有在全局方法
            * __Text 和 _t 中传递的第一个参数为字符串时,才将字符串修改为短key
            */
          type === 'CallExpression' &&
          ['__t', '__Text', '__T'].includes(parent.callee.name) &&
          !scope.hasBinding(parent.callee.name)
        ) {
          node.value = i18nKeys[stringValue];
          /**
            * 通过在source中写入一个特殊注释的方式将key标记在代码中,
            * 交给下一步的webpack来收集
            */
          tree.addComment('leading', `${COMMENT_PREFIX} ${i18nKeys[stringValue]}`);
        } else {
          /**
            * 当匹配到的字符串并不是通过 _t 和 __Text 使用的场景,依然上报长key,保证代码稳定性
            */
          tree.addComment('leading', `${COMMENT_PREFIX} ${stringValue}`);
        }
      }
    },
    MemberExpression: (tree, { filename }) => {
      if (!shouldAnalyse(filename)) {
        return;
      }
      const { node } = tree;
      const memberName = node.property.name;
      if (memberName && i18nKeys.hasOwnProperty(memberName)) {
        tree.addComment('leading', `${COMMENT_PREFIX} ${memberName}`);
      }
    },
  }
};
}

如果扫描到了 i18n 相关的字符串字段,将在原地添加一个注释,用来标记当前模块使用到的 key,这种方式可以让扫描结果落在代码中,使得扫描的操作可以被cache-loader缓存,进一步提升构建速度。

收集过程

通过 babel-loader 的模块都会被标记上使用到的 i18n 的 key 和替换后的短 key,在 webpack 的 parse 阶段只需要遍历文件的所有注释即可拿到模块内用到的所有 i18n 的 key。

export default class ChunkI18nPlugin implements Plugin {
static fileCache = new Map<string, Set<string>>();

constructor(private i18nConfig: I18nBundleConfig) {
}

public apply(compiler: Compiler) {
  compiler.hooks.compilation.tap('ChunkI18nPlugin', (compilation, { normalModuleFactory }) => {

    const handler = (parser) => {
      // 在 parser 中 hook program 钩子
      parser.hooks.program.tap('ChunkI18nPlugin', (ast, comments) => {
        const file = parser.state.module.resource;

        if (!ChunkI18nPlugin.fileCache.has(file)) {
          ChunkI18nPlugin.fileCache.set(file, new Set<string>());
        }
        const keySet = ChunkI18nPlugin.fileCache.get(file);

        // 拿到module的所有注释,扫描其中包含的i18n信息,缓存到一个map中
        comments.forEach(({ value }: {value: string}) => {
          const matcher = value.match(/\s*@i18n\s*(?<keys>.*)/);
          if (matcher?.groups?.keys) {
            const keys = matcher.groups?.keys?.split(' ');
            (keys || []).forEach(keySet.add.bind(keySet));
          }
        });
      });
    };

    // 监听 normalModuleFactory 的 parser 的 hooks
    normalModuleFactory.hooks.parser
      .for('javascript/auto')
      .tap('DefinePlugin', handler);
    normalModuleFactory.hooks.parser
      .for('javascript/dynamic')
      .tap('DefinePlugin', handler);
    normalModuleFactory.hooks.parser
      .for('javascript/esm')
      .tap('DefinePlugin', handler);
  });
}

...

}

有什么不足?

按照模块收集到的 key 是基于源文件扫描到的所有的 key。实际上我们可能存在一些较大的工具方法模块,或者组件模块,并不会用到全部的代码(部分代码会被 treeshaking 机制删掉),后续优化方向可以探索如何只扫描用到的代码中的 key,进一步压缩打包后的总体积。

最终收益

在一段时间的灰度测试后,最终方案上线运行,飞书前端大约 11000 条 key 的情况下,所有单页前端代码体积总计下降 7.2MB。

作者:字节跳动技术团队
来源:https://mp.weixin.qq.com/s/Qt6BL5pa7OJIBLH7Sl_WCA

收起阅读 »

如何搭建一套前端团队的组件系统

使用第三方组件库优缺点快速开发系统管理或中台产品B端产品比较适合,用户群体比较小众,重点在与功能和业务逻辑上手简单,学习成本低体积大,用户访问时间过长,对于C端产品,时间就是金钱,除非部署在高性能服务器或者使用cdn弥补,需要更轻量级组件永恒不变的风格,产品没...
继续阅读 »

伴着公司业务发展,开源的组件库已无法满业务需要,搭建一套更适合公司业务的UI组件库,势在必行,目前市面上有很多功能强大且完善的组件库,比如基于react的开源组件库antDesign,vue的开源组件库elementUI等。

使用第三方组件库优缺点

优点

  • 快速开发系统管理或中台产品

  • B端产品比较适合,用户群体比较小众,重点在与功能和业务逻辑

  • 上手简单,学习成本低

缺点

  • 体积大,用户访问时间过长,对于C端产品,时间就是金钱,除非部署在高性能服务器或者使用cdn弥补,需要更轻量级组件

  • 永恒不变的风格,产品没有差异性

自己搭建组件库相比第三方的优点

  • 打包体积小,更轻量,更贴近业务使用场景

  • 采用内部组件库安全性更高,防止嵌入攻击还有防止类似antDesign圣诞节彩蛋的suprise

  • 构建和开发更灵活,且组合性更高

搭建流程

  • 搭建打包组件库脚手架

  • 组件系统设计思路和模式

  • 组件库的划分

  • 组件库文档生成

  • 将组件库部署到github并发布到npm

搭建打包组件库脚手架

打包组件库工具有很多

  • rollup,打包js利器,非常轻量,集成tree-shaking

  • create-react-app/vue-cli3,可快速改造一个组件库的脚手架

  • webpack自行封装

  • umi/father,基于rollup和babel组件打包功能,集成docz的文档,支持TypeScript等

组件系统设计思路和模式

可以看到基础UI组件是原子组件,作为各种复杂组件的重要组成部分,只有组件的颗粒度足够细,才能满足业务组件使用,区块组件是我们把相同的业务结合基础UI组件进行封装。

这样一套完整的组件化系统就完成了,其中各个组件之间关系是单向的,业务组件只能包含基础UI组件,不能包含区块组件,区块组件里由基础UI组件和业务组件组成。

组件库的划分

我们的基础UI组件库可以参考目前非常流行的UI组件库antd,划分为:通用、布局、导航、数据录入、数据展示、反馈、其他

具体如下:

组件库文档生成

StoryBook

StoryBookReactVueAngular最受欢迎的UI组件开发工具。它可以在隔离的环境中开发和设计应用程序;也可以那个使用它来快速构建UI组件的文档

安装

yarn add @storybook/react

// package.json设置scripts
"scripts": {  
   "storybook": "start-storybook -p 8000"
}

创建文件例如:Button.stories.js

import React from 'react'
import { storiesOf } from '@storybook/react'
import { action } from '@storybook/addon-actions'

import Button from './button'
import '../../styles/index.scss'

const defaultButton = () => (
 <Button onClick={action('clicked')}> default button </Button>
)

const buttonWithSize = () => (
 <>
   <Button size="lg"> large button </Button>
   <Button size="sm"> small button </Button>
 </>
)

const buttonWithType = () => (
 <>
   <Button btnType="primary"> primary button </Button>
   <Button btnType="danger"> danger button </Button>
   <Button btnType="link" href="https://www.baidu.com"> link button </Button>
 </>
)
storiesOf('Button Component', module)
.add('Button', defaultButton)
.add('不同尺寸的 Button', buttonWithSize)
.add('不同类型的 Button', buttonWithType)

基于umi/father脚手架

集成了docz文档功能,一个开箱即用的组件库打包工具,省去了很多配置工作。docz文档

将组件库部署到github并发布到npm上

package.json配置github地址

"repository": { 
   "type": "git",
   "url": "https://github.com:riyue/zhixing.git"
}

首先在npm官网注册账号,然后执行如下命令,也可发布到自己团队私服上

// 输入用户名和密码
npm adduser

// 发布
npm publish

结束

至此整个组件系统设计思路介绍完毕,在开发中一些细节没有展开叙述,例如:整个组件系统全局主题色配置、单元测试、代码规范检查等,需要大家在实践中去发现问题并解决问题。

希望本文能帮助到你或者给正在搭建组件系统的你有所启发。

作者:日月之行_
来源:https://juejin.cn/post/6999987294534893599

收起阅读 »

一个命名引发的性能问题

故事背景我最近主要在定位、解决当前项目中的一些性能相关问题。在反馈的问题中,比较严重的问题之一是在户型预览编辑过程,电脑的 CPU 占用率高,及时什么都不做的情况下,CPU 占用也非常的高。同样的,利用 Chrome 提供的 Performance 录制 ⏺ ...
继续阅读 »



故事背景

我最近主要在定位、解决当前项目中的一些性能相关问题。

在反馈的问题中,比较严重的问题之一是在户型预览编辑过程,电脑的 CPU 占用率高,及时什么都不做的情况下,CPU 占用也非常的高。

同样的,利用 Chrome 提供的 Performance 录制 ⏺ 了无任何操作的 JavaScript 调用火焰图,发现 Pixi 内部会利用浏览器的requestAnimationFrame接口,执行自身的 render 方法进行 2D 场景的绘制渲染。

CPU占用率居高不下

发现问题

初步定位,CPU 占用只可能与 2D 场景中 PIXI 的 render() 有关系,使用 Performance 分析事件调用过程中发现,每次在调用需要使用 9ms 的时间

每次Render使用9ms

这个时间是否属于正常的时间范畴呢?

在了解了 PIXI 及绝大部分图形框架后得知,图形框架内部在调用 Render 的过程中其实正常的不会任何过多的计算内容,所有使用到的需要计算生成 Graphics 的地方,都只会生成一遍。

在之后的调用过程中,都会拿到之前生成好的 Buffer 直接进入 Renderer 进行下一帧的渲染,render() 大部分情况下都是在执行脏检查,有任意 Graphics 需要更新时,Graphicsdirty就会被更新,然后重新生成渲染 Buffer

所以,了解了render()方法的作用后,可以确定在静止不动时render()只是执行一些递归判断,不会耗费9ms这么长的时间,这其中定有蹊跷!

在查看到最终调用到的方法部分发现,在 PIXI 内部的render()方法中竟然会调用到 vue 的方法。

这样不正常的方法调用让我立刻想起来 vue 中,对data()返回的对象数据做原型链的改写。以及之前看到过的一篇文章:《一个 Vue 引发的性能问题》

之前在看到这篇文章时,只是觉得是一个非常有意思的案例,虽然结局办法并不见得是最优方法。但发现问题的过程非常有价值,原本打算拿到组里来进行分享。没想到报应不爽,这么快就发现我们的项目也存着这一个这样的问题,且影响程度远远大于这篇文章!

所以立马去检查了,2D 与 3D 场景实例化过程中对场景的定义,发现一个并没有对命名做_$的规范处理,这样会导致 scene2D 中的所有对象,都将会被 vue 处理为 vue 的可观察对象,也就是会在原型链中带入 getter/setter。

export default {
 data() {
   return {
     scene2d: null
  };
},
 created() {
   this.initScene2D();
},
 methods: {
   initScene2D() {
     this.scene2d = Scene2D.getInstance();
  }
}
};

解决办法

查看到 vue 的官方文档解释:

vue官方解释

所以,为了解决这个问题,需要对data()中不希望 vue 挂载原型链实现数据响应的对象做好命名规范处理。

export default {
 data() {
   return {
     $_scene2d: null
  };
},
 created() {
   this.initScene2D();
},
 methods: {
   initScene2D() {
     this.$_scene2d = Scene2D.getInstance();
  }
}
};

对,就这么简单!

在修改了Scene2D在 Vue 组件的命名后,从整体体验感受来讲“轻快”了许多,再次查看 CPU 及内存占用率都降了许多,render()时间的调用降低到0.94ms,对比如图:

Before

After

作者:Yee Wang
来源:https://yeee.wang/posts/42c7.html

收起阅读 »

如何接"地气"的接入微前端

前言微前端,这个概念已经在国内不止一次的登上各大热门话题,它所解决的问题也很明显,这几个微前端所提到的痛点在我们团队所维护的项目中也是非常凸显。但我始终认为,一个新的技术、浪潮,每每被讨论最热门的一定是他背后所代表的杰出思考。“微前端就是…xx 框架,xx 技...
继续阅读 »



前言

微前端,这个概念已经在国内不止一次的登上各大热门话题,它所解决的问题也很明显,这几个微前端所提到的痛点在我们团队所维护的项目中也是非常凸显。

但我始终认为,一个新的技术、浪潮,每每被讨论最热门的一定是他背后所代表的杰出思考。

“微前端就是…xx 框架,xx 技术”

这种话就有点把这种杰出的思路说的局限了,我只能认为他是外行人,来蹭这个词的热度。

在我所负责的项目和团队中,已经有非常大的存量技术栈和页面已经在线上运行,任何迭代升级都必须要保证小心翼翼,万无一失。

可以说,从一定程度来讲,微前端所带来的这些好处是从用户体验和技术维护方面的,对业务的价值并不能量化体现,落地这项技术秉着既要也要还要的指导方针。

我们对存量技术栈一定需要保持敬畏,隔离,影响范围可控的几个基本要素,然后再考虑落地实施微前端方案。

所以,在这个基本要素和指导方针下。要落地这项新的技术时,一定充分充分了解,当前改造站点所存在的技术方案、占比 以及 当前成熟微前端框架已提供的能力差异,切勿生搬硬套。

背景

我所在团队维护的项目都是些 PC 操作后台(Workstation),这些工作台会存在不同的国家,不同时区,不同合作方等等问题。

如果需要开发一个新的页面需求,很可能投入进来的开发人员都来自不同团队,此时我们要在完成现有需求的同时还需要保证多个管理页面的风格统一,设计规范统一,组件统一,交互行为统一这非常困难。

当该业务需要迁移到另外一个工作台时,虽然需要保持逻辑一致,但导航栏、主题等却不同。

当前存量的方案都是采用 Java 直接进行 Template 渲染出 HTML,经过前面几代前辈的迭代,不同系统中已经存在几种不同技术栈产出的页面。

虽然都是 React 来实现的,但是前辈们都非常能折腾,没有一个是按照常规 React 组件形式开发出来的。

比如:

  1. 大部分页面是通过一份 JSON 配置,消费组件生成的页面。

  2. 部分页面是通过另外一个团队定义的 JSON 配置消费组件生成的,与上面 JSON 完全不一样。

  3. 还有一部分页面,是通过一套页面发布平台提供的 JS Bundle 加载出来的。

面对这样的技术背景下,除了微笑的喊 MMP,含泪说着自己听不懂的话(存在即合理,不难要你干吗?),还得接地气出这样一个落地方案。

方案 & 流程图

首先,需要明确的分析出站点所有页面,所需要加载的通用特性:

上述是精简过后的一些通用功能特性,这里简单做下介绍:

  • Layout Loader 用于加载不同工作台的导航

  • DADA Loader 用于加载 JSON 配置的页面

  • Source Code Loader 用于加载 JS Bundle

  • Micro Loader 用于处理微前端加载

  • Log Report 用于日志埋点

  • Time Zone 用于切换时区

  • i18n 用于切换多语言

  • Guider 用于统一管控用户引导

除此以外可能还会存在以下这些页面扩展能力:

  • 安全监控

  • 流量管控

  • 弹窗管控

  • 问卷调查

  • 答疑机器人

粗略统一归类后来看,页面的大体加载流程应该是这样:

实现细则

基于上述一个加载思路,首先需要做的是页面加载路径收口,需要保证所有页面的加载入口是在一个统一的 Loader 下,然后才可以较为系统的处理所有页面的加载生命周期。

在收敛的同时,同样需要保持开放,对核心加载路径要保持插件化开放,随时支持不同的扩展能力,渲染技术栈接入。

插件机制

所以,在主路径上,通过 Loader 加载配置进行处理,这份配置在主路径中提供上下文,然后交由插件进行消费,如图所示:

举个例子,拿一个独立的 JS Bundle 类型的子应用来说:

<div id="root"></div>
<script src="https://cdn.address/schema-resolver/index.js"></script>
<script src="https://cdn.address/schema-resolver/plugin/layout.js"></script>
<script src="https://cdn.address/schema-resolver/plugin/source-code.js"></script>
<script src="https://cdn.address/schema-resolver/plugin/micro-loader.js"></script>
<script src="https://cdn.address/schema-resolver/plugin/i18n.js"></script>

<script>
 SchemaResolver.render(
  {
     micro: true,
     host: "dev.address",
     hfType: "layout1",
     externals: ["//{HOST}/theme1/index.css"],
     // host is cdn prefix, the resource maybe in different env & country
     resource: {
       js: "/index.js",
       css: "/index.css",
    },
  },
  { container: document.querySelector("#root") }
);
</script>

通过上述的 Plugin 引入,即可开启和消费不同的配置。

这里引入了 Layout Plugin,该插件会消费 hfType 字段然后去加载对于的 Layout 资源提供 Container 交给下一个环节。

按照配置,当前页面开启了微前端,那么 Micro Loader 将会消费提供下来的 Container,然后建立沙箱(这里基于 qiankun),再提供 Container 出来。

最后交由 SourceCode Plugin 进行 Bundle 加载运行和渲染。如果这里是另外一种渲染协议或者技术栈,则可以根据不同配置交由不同插件消费 Container。

这个过程中,每个环节的插件是不依赖的,可插拔的

比如:

如果不加载 Layout Plugin 将不会消费 hfType 字段,也就不会将Layout插件逻辑注入到getContainer方法中,那么将直接得到由最外层下穿的Container进行渲染,也就不会有菜单相关透出。

如果不加载 Micro Plugin 同样不会有微前端的逻辑注入,也就不会建立沙箱,那么页面渲染流程将会按照常规模式继续运行。

当前SchemaResolver已经支持的插件有以下几种,详情参考 SchemaResolver

  • MicroLoader – Base on qiankun using this plug-in can make your content loaded through a micro application so that your content can use all the features of the Micro-Front-End.

  • DadaLoader – Use this plugin can make your app render content by Dada.

  • SourceCodeLoader – Use this plugin can load your js\css bundle to render content, our bundle standard is same as qiankun. You can quick start developing your own page through our toolkit lzd-toolkit-asc.

  • LayoutLoader – Use this plugin can make your page load layout(menu), you can use different hfType configuration to switch different layouts.

  • i18n – Use this plugin can make your page have multi-lang. schema.locale will be the mapping of multilingual keys in MCMS. The plugin will inject and register the language automatically.

  • APlus – Use this plugin can make your page have the feature of APlus .Statistics page interaction events, such as pv\uv. With DadaLoader you can even see every module data(click pv, exposed pv) in pages.

  • WalkThrough – Use this plugin can make your page have the feature of Walk Through. One-stop page features guide.

SchemaResolver的插件能力采用plugin-decorator,如要了解更多插件设计思路可以参考:为你的JavaScript库提供插件能力

SchemaResolver plugin feature is base on plugin-decorator. It’s very easy to develop a new plugin.

More information about plugin can read this article Provide plugin capabilities for your JavaScript library

安全迁移

对于我所在团队负责的项目来说,万万做不得一刀切的方案,所以针对现有存量页面,需要完整分析当前存量技术栈:

针对上述存量页面来说,需要从左到右分批进行页面级别控制上线部署,对于左侧部分页面甚至需要做些项目改造后才可部署接入上线。

这类迁移测试需要处理出一套 自动化e2e测试 流程,通过分批迁移同时梳理出 微前端注册表

有了这两项流程保证及范围控制,当前方案所上线内容完全可控,剩下要处理的大部分就是较为重复的体力活了,覆盖率也可量化。

微前端形态

按照上述方案迁移,那么预期的微前端形态将会是:

  1. 每个开启微前端的页面都可成为主应用

  2. 微前端是插件可选项,如果因为微前端导致的业务异常可随时关闭

  3. 同为微前端的页面路由相互之间切换可实现局部刷新形态,而跳转至非微前端注册表中的页面则会直接页面跳转。随着微前端页面覆盖率提高,局部刷新的覆盖率也会逐渐提高

  4. 可通过不同扩展插件,加载不同技术栈类型的存量页面,转换为对应子应用

在SchemaResolver中的注册和调用路径如下:

总结

透过技术看本质,微前端所代表的杰出思维,才是真正解决具体问题关键所在,只有解决了具体的业务问题,这项技术才有价值转换。

不要为了微前端做微前端,不要为了小程序做小程序。

当前,通过 SchemaResolver,可以针对不同角色提供不同的开放能力:

  • 针对平台管理员,提供插件能力开放全局扩展能力

  • 针对页面开发者,提供标准化接入方案路径,提供多种技术栈接入能力,并无感知提供微前端,多语言,埋点,菜单,主题加载等能力。解耦了不同系统公共能力,同时,这种方式可以让页面开发者快速将具体业务逻辑迁移到其他平台。

  • 针对第三方接入者,不需要关心了解系统菜单、主题接入方式,提供统一的接入口径,通过微前端隔离技术栈、隔离子应用样式。最后通过统一的页面系统管控,轻松入住对应平台,同时可以全局看到站点页面情况。

作者:Yee Wang
来源:https://yeee.wang/posts/3469.html

收起阅读 »

为你的JavaScript库提供插件能力

前言最近在做一个中台框架的设计开发,在做了主框架的基础能力后,思考在框架落实真实业务需求过程中,需要对主框架功能有非常多的定制化内容存在。如果在主体框架中做了哪怕一点业务改动,都可能会对后面的拓展性及灵活性有所限制。所以为了让主体框架做的更加灵活、扩展性更搞,...
继续阅读 »



前言

最近在做一个中台框架的设计开发,在做了主框架的基础能力后,思考在框架落实真实业务需求过程中,需要对主框架功能有非常多的定制化内容存在。如果在主体框架中做了哪怕一点业务改动,都可能会对后面的拓展性及灵活性有所限制。

所以为了让主体框架做的更加灵活、扩展性更搞,在主框架有了基础能力后,就不再对主框架做任何非主框架能力的业务功能开发。

要为主框架不断的”开槽”

其实在很多前端库中都有类似的设计,才能够让更多的开发者参与进来,完成各种各样的社区驱动开发。比如:WebpackBabelHexoVuePress等。

那么如何为自己的项目开槽,做插件呢?

调研

在了解了很多插件的项目源码后,发现实现大多大同小异,主要分为以下几个步骤:

  1. 为框架开发安装插件能力,插件容器

  2. 暴露主要生命运行周期节点方法(开槽)

  3. 编写注入业务插件代码

这些框架都是在自己实现一套这样的插件工具,几乎和业务强相关,不好拆离。或者做了一个改写方法的工具类。总体比较离散,不好直接拿来即用。

另外在实现方式上,大部分的插件实现是直接改写某方法,为他在运行时多加一个Wrap,来依次运行外部插件的逻辑代码。

// main.js
const main = {
 loadData:()=>{},
 render:()=>{}
}

// plugin1.js
const plugin1 = {
 render:()=>{}
}

// install.js
const install = (main, plugin) => {
 main.render = ()=>{
   plugin1.render()
   main.render()
}
}

在上述代码中的插件有几个明显的问题:

  • plugin1 无法控制 render() 的顺序

  • main 中无法得知什么函数可能会被插件改写,什么函数不会被插件改写

  • 如果按照模块文件拆分,团队成员中根本不知道 main.js 中的函数修改会存在风险,因为压根看不到 install.js 中的代码

那么后来,为了解决这些问题,可能会变成这样:

const component = {
 hooks:{
   componentWillMounted(){},
   componentDidMounted(){}
},
 mounte(){
   this.hooks.componentWillMounted();
   //... main code
   this.hooks.componentDidMounted();
}
}

const plugin = {
 componentWillMounted(){
   //...
},
 componentDidMounted(){
   //...
}
}

// install.js
const install = (main, plugin) => {
 // 忽略实现细节。
 main.hooks.componentWillMounted = ()=>{
   plugin1.componentWillMounted()
   main.hook.componentWillMounted()
}
 main.hooks.componentDidMounted = ()=>{
   plugin1.componentDidMounted()
   main.hook.componentDidMounted()
}
}

另外,还有一种解法,会给插件中给 next 方法,如下:

// main.js
const main = {
 loadData:()=>{},
 render:()=>{}
}

// plugin1.js
const plugin1 = {
 render:next=>{
   // run some thing before
   next();
   // run some thing after
}
}

// install.js
const install = (main, plugin) => {
 main.render = ()=>{
   plugin1.render(main.render)
}
}

如上,从调研结构来看,虽然都实现了对应功能,但是从实现过程来看,有几个比较明显的问题:

  • 对原函数侵入性修改过多

  • 对方法rewrite操作过多,太hack

  • 对TypeScript不友好

  • 多成员协作不友好

  • 对原函数操作不够灵活,不能修改原函数的入参出参

开搞

在调研了很多框架的实现方案的后,我希望以后我自己的插件库可以使用一个装饰器完成开槽,在插件类中通过一个装饰器完成注入,可以像这样使用和开发:

import { Hook, Inject, PluginTarget, Plugin } from 'plugin-decorator';

class DemoTarget extends PluginTarget {
 @Hook
 public method1() {
   console.log('origin method');
}
}

class DemoPlugin extends Plugin {
 @Inject
 public method1(next) {
   next();
   console.log('plugin method');
}
}

const demoTarget = new DemoTarget();
demoTarget.install(new DemoPlugin());
demoTarget.method1();

// => origin method
// => plugin method

Decorator

并且可以支持对原函数的入参出参做装饰修改,如下:

import { Hook, Inject, PluginTarget, Plugin } from 'plugin-decorator';

class DemoTarget extends PluginTarget {
 @Hook
 public method1(name:string) {
   return `origin method ${name}`;
}
}

class DemoPlugin extends Plugin {
 @Inject
 public method1(next, name) {
   return `plugin ${next(name)}`;
}
}

const demoTarget = new DemoTarget();
demoTarget.install(new DemoPlugin());

console.log(demoTarget.method1('cool'));

// => plugin origin method cool

Promise

当然,如果原函数是一个Promise的函数,那插件也应该支持Promise了!如下:

import { Hook, Inject, PluginTarget, Plugin } from 'plugin-decorator';

class DemoTarget extends PluginTarget {
 @Hook
 public methodPromise() {
   return new Promise(resolve => {
     setTimeout(() => resolve('origin method'), 1000);
  });
}
}

class DemoPlugin extends Plugin {
 @Inject
 public async methodPromise(next) {
   return `plugin ${await next()}`;
}
}

const demoTarget = new DemoTarget();
demoTarget.install(new DemoPlugin());

demoTarget.methodPromise().then(console.log);

// => Promise<plugin origin method>

Duang!

最终,我完成了这个库的开发:plugin-decorator

GitHub: 地址

没错,我就知道你会点Star,毕竟你这么帅气、高大、威猛、酷炫、大佬!

总结

在该项目中,另外值得提的一点是,该项目是我在开发自己的一套中台框架中临时抽出来的一个工具库。

在工具库中采用了:

  • TypeScript

  • Ava Unit Test

  • Nyc

  • Typedoc

整体开发过程是先写测试用例,然后再按照测试用例进行开发,也就是传说中的 TDD(Test Drive Development)。

感觉这种方式,至少在我做库的抽离过程中,非常棒,整体开发流程非常高效,目的清晰。

在库的编译搭建中使用了 typescript-starter 这个库,为我节省了不少搭建项目的时间!

作者:Yee Wang
来源:https://yeee.wang/posts/dfa4.html

收起阅读 »

轻松生成小程序分享海报

小程序海报组件github.com/jasondu/wxa…需求小程序分享到朋友圈只能使用小程序码海报来实现,生成小程序码的方式有两种,一种是使用后端方式,一种是使用小程序自带的canvas生成;后端的方式开发难度大,由于生成图片耗用内存比较大对服务端也是不小...
继续阅读 »



小程序海报组件

github.com/jasondu/wxa…

需求

小程序分享到朋友圈只能使用小程序码海报来实现,生成小程序码的方式有两种,一种是使用后端方式,一种是使用小程序自带的canvas生成;后端的方式开发难度大,由于生成图片耗用内存比较大对服务端也是不小的压力;所以使用小程序的canvas是一个不错的选择,但由于canvas水比较深,坑比较多,还有不同海报需要重现写渲染流程,导致代码冗余难以维护,加上不同设备版本的情况不一样,因此小程序海报生成组件的需求十分迫切。

在实际开发中,我发现海报中的元素无非一下几种,只要实现这几种,就可以通过一份配置文件生成各种各样的海报了。

海报中的元素分类

要解决的问题

  • 单位问题

  • canvas隐藏问题

  • 圆角矩形、圆角图片

  • 多段文字

  • 超长文字和多行文字缩略问题

  • 矩形包含文字

  • 多个元素间的层级问题

  • 图片尺寸和渲染尺寸不一致问题

  • canvas转图片

  • IOS 6.6.7 clip问题

  • 关于获取canvas实例

单位问题

canvas绘制使用的是px单位,但不同设备的px是需要换算的,所以在组件中统一使用rpx单位,这里就涉及到单位怎么换算问题。

通过wx.getSystemInfoSync获取设备屏幕尺寸,从而得到比例,进而做转换,代码如下:

const sysInfo = wx.getSystemInfoSync();
const screenWidth = sysInfo.screenWidth;
this.factor = screenWidth / 750; // 获取比例
function toPx(rpx) { // rpx转px
return rpx * this.factor;
}
function toRpx(px) { // px转rpx
return px / this.factor;
},

canvas隐藏问题

在绘制海报过程时,我们不想让用户看到canvas,所以我们必须把canvas隐藏起来,一开始想到的是使用display:none; 但这样在转化成图片时会空白,所以这个是行不通的,所以只能控制canvas的绝对定位,将其移出可视界面,代码如下:

.canvas.pro {
  position: absolute;
  bottom: 0;
  left: -9999rpx;
}

圆角矩形、圆角图片

由于canvas没有提供现成的圆角api,所以我们只能手工画啦,实际上圆角矩形就是由4条线(黄色)和4个圆弧(红色)组成的,如下:

圆弧可以使用canvasContext.arcTo这个api实现,这个api的入参由两个控制点一个半径组成,对应上图的示例

canvasContext.arcTo(x1, y1, x2, y2, r)

接下来我们就可以非常轻松的写出生成圆角矩形的函数啦

/**
* 画圆角矩形
*/
_drawRadiusRect(x, y, w, h, r) {
  const br = r / 2;
  this.ctx.beginPath();
  this.ctx.moveTo(this.toPx(x + br), this.toPx(y));   // 移动到左上角的点
  this.ctx.lineTo(this.toPx(x + w - br), this.toPx(y)); // 画上边的线
  this.ctx.arcTo(this.toPx(x + w), this.toPx(y), this.toPx(x + w), this.toPx(y + br), this.toPx(br)); // 画右上角的弧
  this.ctx.lineTo(this.toPx(x + w), this.toPx(y + h - br)); // 画右边的线
  this.ctx.arcTo(this.toPx(x + w), this.toPx(y + h), this.toPx(x + w - br), this.toPx(y + h), this.toPx(br)); // 画右下角的弧
  this.ctx.lineTo(this.toPx(x + br), this.toPx(y + h)); // 画下边的线
  this.ctx.arcTo(this.toPx(x), this.toPx(y + h), this.toPx(x), this.toPx(y + h - br), this.toPx(br)); // 画左下角的弧
  this.ctx.lineTo(this.toPx(x), this.toPx(y + br)); // 画左边的线
  this.ctx.arcTo(this.toPx(x), this.toPx(y), this.toPx(x + br), this.toPx(y), this.toPx(br)); // 画左上角的弧
}

如果是画线框就使用this.ctx.stroke();

如果是画色块就使用this.ctx.fill();

如果是圆角图片就使用

this.ctx.clip();
this.ctx.drawImage(***);

clip() 方法从原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内(不能访问画布上的其他区域)。可以在使用 clip() 方法前通过使用 save() 方法对当前画布区域进行保存,并在以后的任意时间对其进行恢复(通过 restore() 方法)。

多段文字

如果是连续多段不同格式的文字,如果让用户每段文字都指定坐标是不现实的,因为上一段文字的长度是不固定的,这里的解决方案是使用ctx.measureText (基础库 1.9.90 开始支持)Api来计算一段文字的宽度,记住这里返回宽度的单位是px(),从而知道下一段文字的坐标。

超长文字和多行文字缩略问题

设置文字的宽度,通过ctx.measureText知道文字的宽度,如果超出设定的宽度,超出部分使用“...”代替;对于多行文字,经测试发现字体的高度大约等于字体大小,并提供lineHeight参数让用户可以自定义行高,这样我们就可以知道下一行的y轴坐标了。

矩形包含文字

这个同样使用ctx.measureText接口,从而控制矩形的宽度,当然这里用户还可以设置paddingLeft和paddingRight字段;

文字的垂直居中问题可以设置文字的基线对齐方式为middle(this.ctx.setTextBaseline('middle');),设置文字的坐标为矩形的中线就可以了;水平居中this.ctx.setTextAlign('center');;

多个元素间的层级问题

由于canvas没有Api可以设置绘制元素的层级,只能是根据后绘制层级高于前面绘制的方式,所以需要用户传入zIndex字段,利用数组排序(Array.prototype.sort)后再根据顺序绘制。

图片尺寸和渲染尺寸不一致问题

绘制图片我们使用ctx.drawImage()API;

如果使用drawImage(dx, dy, dWidth, dHeight),图片会压缩尺寸以适应绘制的尺寸,图片会变形,如下图:

在基础库1.9.0起支持drawImage(sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight),sx和sy是源图像的矩形选择框左上角的坐标,sWidth和sHeight是源图像的矩形选择框的宽度和高度,如下图:

如果绘制尺寸比源图尺寸宽,那么绘制尺寸的宽度就等于源图宽度;反之,绘制尺寸比源图尺寸高,那么绘制尺寸的高度等于源图高度;

我们可以通过wx.getImageInfoApi获取源图的尺寸;

canvas转图片

在canvas绘制完成后调用wx.canvasToTempFilePathApi将canvas转为图片输出,这样需要注意,wx.canvasToTempFilePath需要写在this.ctx.draw的回调中,并且在组件中使用这个接口需要在第二个入参传入this(),如下

this.ctx.draw(false, () => {
  wx.canvasToTempFilePath({
      canvasId: 'canvasid',
      success: (res) => {
          wx.hideLoading();
          this.triggerEvent('success', res.tempFilePath);
      },
      fail: (err) => {
          wx.hideLoading();
          this.triggerEvent('fail', err);
      }
  }, this);
});

IOS 6.6.7 clip问题

在IOS 6.6.7版本中clip方法连续裁剪图片时,只有第一张有效,这是微信的bug,官方也证实了(developers.weixin.qq.com/community/d…

关于获取canvas实例

我们可以使用wx.createCanvasContext获取小程序实例,但在组件中使用切记第二个参数需要带上this,如下

this.ctx = wx.createCanvasContext('canvasid', this);

如何使用组件

github.com/jasondu/wxa…

作者:jasondu41833
来源:https://juejin.cn/post/6844903663840788493

收起阅读 »