注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

为了快乐的摸鱼,专门写了个网站!

这是鄙人做的网站,目的呢原本是为了摸鱼,把产品那边整的页面快速构建出来,咱们公司用的是比较老的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;
  colorvar(--color-text);
  background-colorvar(--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

收起阅读 »

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

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



1 概述

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

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

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

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

2 界面的基本元素

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

2.1 组件

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

组件是网页的器官。

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

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

2.2 数据

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

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

数据是网页的核心。

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

2.3 交互

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

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

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

3 组件的特性

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

  • 复用性(Reuseability)

  • 组合性(Composability)

  • 扩展性(Scalability)

3.1 复用性

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

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

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

3.2 组合性

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

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

3.3 扩展性

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

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

4 组件间通信

4.1 从外向内通信

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

props是不可变的:

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

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

4.2 从内向外通信

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

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

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

4.3 双向通信

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

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

4.4 多端通信

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

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

5 案例

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

设计图如下:

5.1 第一步:拆积木

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

5.1.1 顶层积木

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

  • Header(头部组件)

  • ChartBlock(图表块组件)

  • DetailBlock(详情块组件)

  • TableBlock(表格块组件)

5.1.2 中间层积木

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

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

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

层次结构如下:

  • Header

    • Breadcrumb

    • Button

    • DateRangePicker

  • ChartBlock

    • Tabs

    • LineChart

    • BarChart

  • DetailBlock

    • Button

    • List

  • TableBlock

    • Checkbox

    • Select

    • Button

    • Table

5.1.3 底层积木

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

5.2 第二步:造积木

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

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

5.3 第三步:搭积木

5.3.1 顶层积木

对应的代码:

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

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

5.3.2 中间层积木

Header对应的代码:

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

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

ChartBlock对应的代码:

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

DetailBlock对应的代码:

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

TableBlock对应的代码:

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

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

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

收起阅读 »

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

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


前端框架的数据流

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

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

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

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

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

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

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

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

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

异步过程的管理

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

组件?

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

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

不放组件内,那放哪呢?

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

redux 中间件

先看下什么是 redux 中间件:

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

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

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

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

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

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

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

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

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

return next(action);
};
}

const thunk = createThunkMiddleware();

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

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

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

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

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

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

redux-saga

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

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

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

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

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

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

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

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

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

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

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

return result;
}
}
}

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

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

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

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

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

logout 会触发 logoutSuccess 的 action。

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

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

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

比如下面这段代码:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

redux-observable

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

const epicMiddleware = createEpicMiddleware();

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

epicMiddleware.run(rootEpic);

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

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

import { ajax } from 'rxjs/ajax';

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

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

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

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

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

总结

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

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

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

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

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

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

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

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

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


收起阅读 »

重构B端 ? 表单篇

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

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


1. 梳理待重构的B端



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


2. 设计重构方案


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



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

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

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


3. 配置文件改造



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



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

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


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


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

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

4. 表单渲染


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

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


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

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

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

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

5. 默认字段 & 隐藏字段


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

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

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

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


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

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


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

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

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

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


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

6. Select 动态渲染值


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

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

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



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



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

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

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


6. 最后


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


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



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


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

收起阅读 »

Taro的http封装

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

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

serve.ts


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

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

export default Server

引用时


import Server from './serve'

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

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

interface Err {
code: number
message: string
}

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

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

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

return { err, res }
}

ok 直接用就行了













收起阅读 »

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

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

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

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

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

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

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

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

体现算法威力的例子

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

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

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

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

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

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

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

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

是,几倍的提升很大了。

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

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

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

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

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

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

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

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

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

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

总结

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

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

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

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

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

收起阅读 »

前端监控系统设计

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

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


一、数据采集


1.异常数据


1.1 静态资源异常


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


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


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

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


1.2 js错误


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


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


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

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

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

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

1.3 自定义异常


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


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


1.4 接口异常



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

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

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


1.5 监听未处理的promise错误


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


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


2.性能数据


2.1 FP/FCP/LCP/CLS


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



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

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

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

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




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


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


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


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


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


2.2 DOMContentLoaded事件 和  onload 事件



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

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


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


image.png


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


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


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


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


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



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

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


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



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

  • xmlhttprequest:单独统计


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


image.png


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



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

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

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


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

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


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


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


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

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

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

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

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

lazyReportCache(reportData)

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

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

export default function xhr() {
overwriteOpenAndSend()
}

二、数据上报


1. 上报方法


采用sendBeacon 和 XMLHttpRequest 相结合的方式


为什么要使用sendBeacon?



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

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



2. 上报时机



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

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

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

收起阅读 »

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

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

阅读本文📖

你将:

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

前言

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

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

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

就像这样:

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

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

尔康式拒绝

JSON 式封装哪些缺点?

缺点一:学习成本增高

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

你更愿意使用哪种组件?

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

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

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

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

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

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

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

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

JSON 式封装有哪些优点?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

好家伙,我直呼好家伙!

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

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

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

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

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

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

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

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

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

震惊!

免喷声明

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

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

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

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

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


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

收起阅读 »

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

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

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

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

性能分析

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


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

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

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

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

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

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

过几秒点击结束。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

性能优化

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

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

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

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

我们来试一下:

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

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

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

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

耗时逻辑移到了 worker 线程:

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

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

哇,long task 一个都没有了!

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

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

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

我们再看下 Sources 面板:

对比下之前的:

这优化力度,肉眼可见!

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

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

github.com/QuarkGluonP…

总结

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

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

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

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

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


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

收起阅读 »

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

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

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

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

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


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

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


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

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


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

收起阅读 »

node-sass的坑

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

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


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


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


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

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


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


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

也可以设置环境变量:


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

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


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



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


npm config set sass_binary_path [path]

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


npm config set proxy [system proxy]

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


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

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


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


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

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


npm config set msvs_version [version]

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


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

收起阅读 »

微前端拆分实践

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

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

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

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

我们为什么需要微前端?

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

代码冲突

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

部署冲突

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

技术栈冲突

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

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

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

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

拆分时机

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

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

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

业务有较大的改变或演进

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

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

业务稳定不再有大的改进

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

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

拆分原则

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

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

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

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

拆分前的准备

前置概念

single-spa

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

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

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

In-broswer module vs build time module

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

module-build-result

module-build-result

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

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

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

import singleSpa from "single-spa";

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

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

Import-map

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

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

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

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

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

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

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

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

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

import singleSpa from "single-spa";

Systemjs

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

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

es6-module-syntax

es6-module-syntax

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

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

import-without-url

import-without-url

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

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

how-to-use-systemjs

how-to-use-systemjs

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

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

架构设计

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

基础设施架构

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

arch-of-micro-fe

arch-of-micro-fe

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

部署策略

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

delpoy-plan

delpoy-plan

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

pipeline-stage

pipeline-stage

本地开发策略

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

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

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

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

实际拆分

problem

problem

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

太贵了

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

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

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

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

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

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

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

repo 拆分问题

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

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

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

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

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

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

跨业务页面拆分问题

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

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

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

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

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

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

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

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

CSS 冲突问题

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

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

写在最后

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

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

配置一个好看的PowerShell

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

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

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

image-20211017111042177

准备

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

    image-20211017111722102

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

    image-20211017111555327

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

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

安装及修改

先贴出Oh My Posh官方文档

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

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

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

    notepad $PROFILE

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

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

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

    image-20211017112633530

一些提示

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

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

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

    image.png

  4. 显示出来是这样的:

    image.png

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

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

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

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



前言

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

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

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

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

错误处理

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

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

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

rollup-plugin-trycatch

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

错误捕获

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

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

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

1、远程资源加载错误;

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

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

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

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

3、异步代码错误;

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

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

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

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

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

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

具体代码可参考 demo;

错误位置

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

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

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

sourcemap解析错误

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

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

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

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

解析sourcemap源码

问题记录

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

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

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

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

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

参考文档

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

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

收起阅读 »

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

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

MobX 实践指南

一、概览篇

简介

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

MobX哲学

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

核心原理

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

核心概念

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

alt 运行流程

安装

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

npm i mobx

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

npm i mobx mobx-react

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

npm i mobx mobx-react-lite

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

二、实践篇

1. 声明Store

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

三种声明方式

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

import { observable,action } from 'mobx'

const userStore = observable({
roleType:1
})

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

export default userStore

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

import { observable } from 'mobx'

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

export default UserStore

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

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

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

//or

import { makeAutoObservable } from 'mobx'

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

实现 store 间通信

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

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

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

const rootStore = new RootStore()
export default rootStore

2. 在React组件中使用

2.1 observer

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

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

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

src/demos/RoleManage.jsx

import rootStore from './../store'

const { userStore } = rootStore;

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

2.2 Provider、inject

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

import rootStore from './../store'

const { userStore } = rootStore;

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

import App from "./App";

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

src/demos/UserInfo.jsx

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

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

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

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

2.3 MobX + Hooks

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

2.3.1 使用全局Store

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

...

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

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

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

src/index.jsx

- import { Provider } from 'mobx-react'

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

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

src/demos/UserInfo.jsx

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

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

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

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

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

2.3.2 创建一个局部的Store

先介绍两个hook

useLocalObservable

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

useObserver

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

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

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

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

3. 开发者工具

chrome插件

三、Q&A

  1. IE项目能不能用?

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

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

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

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

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

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

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

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

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

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

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

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

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

原文:


https://juejin.cn/post/6979095356302688286

收起阅读 »

js 实现双指缩放

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

前言

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

缩放原理

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

p.jpg

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

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

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

两点间距离公式

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

e693d73856f43706273b0197b3cc42bf.svg

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

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

中点坐标公式

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

4a36acaf2edda3cce013415d11e93901203f92dc.png

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

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

获取图片缩放尺寸

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

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

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

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

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

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

双指缩放逻辑

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

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

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

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

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

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

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

注意事项

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

微信图片_20210802192116.png


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

收起阅读 »

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

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

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


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


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


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


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



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


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


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


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


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


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


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


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


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


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


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


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


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


“oh shit ! shit mountain !”



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

收起阅读 »

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

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



体验地址:博客githubnpm

前言

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

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

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

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

开搞

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

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

优化

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

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

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

基本上的思路就是:

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

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

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

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

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

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

遇到的问题

npm link

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

接口

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

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

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

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

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

后期想办法优化吧。

主题色

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

结尾

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

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

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


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

收起阅读 »

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

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



文章目录

前言

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

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

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

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

一、前置条件

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

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

二、操作步骤

1.进入adb shell

命令如下(示例):

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

2.提取源码编译文件

代码如下(示例):

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

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

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

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

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

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

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

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

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

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

3.反编译

使用wxappUnpacker

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

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

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

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

收起阅读 »

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

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

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

  即,设置img为:

  display:block;

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

  #sub img {display:block;}

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

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

  #sub img {vertical-align:top;}

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

  即,在#sub中添加一行:

  font-size:0;

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

4、改变父对象的属性

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

  overflow:hidden;

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

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

5、设置图片的浮动属性

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

  #sub img {float:left;}

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

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

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

收起阅读 »

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

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



什么是认证(Authentication)

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

  • 互联网中的认证:

    • 用户名密码登录

    • 邮箱发送登录链接

    • 手机号接收验证码

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

什么是授权(Authorization)

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

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

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

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

什么是凭证(Credentials)

  • 实现认证和授权的前提

    是需要一种

    媒介(证书)

    来标记访问者的身份

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

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

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

什么是 Cookie

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

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

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

cookie 重要的属性

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


什么是 Session

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

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

session.png

  • session 认证流程:

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

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

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

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

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

Cookie 和 Session 的区别

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

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

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

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

什么是 Token(令牌)

Acesss Token

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

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

  • 特点:

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

    • 支持移动端设备

    • 安全

    • 支持跨程序调用

  • token 的身份验证流程:

img

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

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

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

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

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

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

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

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

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

Refresh Token

  • 另外一种 token——refresh token

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

img

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

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

Token 和 Session 的区别

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

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

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

什么是 JWT

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

  • 是一种认证授权机制

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

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

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

生成 JWT

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

JWT 的原理

img

  • JWT 认证流程:

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

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

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

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

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

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

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

JWT 的使用方式

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

方式一

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

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

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

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

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

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

方式二

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

方式三

  • 通过 URL 传输

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

项目中使用 JWT

项目地址

Token 和 JWT 的区别

相同:

  • 都是访问资源的令牌

  • 都可以记录用户的信息

  • 都是使服务端无状态化

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

区别:

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

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

常见的前后端鉴权方式

  1. Session-Cookie

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

  3. OAuth2.0(开放授权)

常见的加密算法

image.png

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

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

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

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

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

    • 输入敏感:原始数据只要有一点变动,得到的哈希值差别很大

    • 冲突避免:很难找到不同的原始数据得到相同的哈希值,宇宙中原子数大约在 10 的 60 次方到 80 次方之间,所以 2 的 256 次方有足够的空间容纳所有的可能,算法好的情况下冲突碰撞的概率很低:

      • 2 的 128 次方为 340282366920938463463374607431768211456,也就是 10 的 39 次方级别

      • 2 的 160 次方为 1.4615016373309029182036848327163e+48,也就是 10 的 48 次方级别

      • 2 的 256 次方为 1.1579208923731619542357098500869 × 10 的 77 次方,也就是 10 的 77 次方

注意:

  1. 以上不能保证数据被恶意篡改,原始数据和哈希值都可能被恶意篡改,要保证不被篡改,可以使用RSA 公钥私钥方案,再配合哈希值。

  2. 哈希算法主要用来防止计算机传输过程中的错误,早期计算机通过前 7 位数据第 8 位奇偶校验码来保障(12.5% 的浪费效率低),对于一段数据或文件,通过哈希算法生成 128bit 或者 256bit 的哈希值,如果校验有问题就要求重传。

常见问题

使用 cookie 时需要考虑的问题

  • 因为存储在客户端,容易被客户端篡改,使用前需要验证合法性

  • 不要存储敏感数据,比如用户密码,账户余额

  • 使用 httpOnly 在一定程度上提高安全性

  • 尽量减少 cookie 的体积,能存储的数据量不能超过 4kb

  • 设置正确的 domain 和 path,减少数据传输

  • cookie 无法跨域

  • 一个浏览器针对一个网站最多存 20 个Cookie,浏览器一般只允许存放 300 个Cookie

  • 移动端对 cookie 的支持不是很好,而 session 需要基于 cookie 实现,所以移动端常用的是 token

使用 session 时需要考虑的问题

  • 将 session 存储在服务器里面,当用户同时在线量比较多时,这些 session 会占据较多的内存,需要在服务端定期的去清理过期的 session

  • 当网站采用集群部署的时候,会遇到多台 web 服务器之间如何做 session 共享的问题。因为 session 是由单个服务器创建的,但是处理用户请求的服务器不一定是那个创建 session 的服务器,那么该服务器就无法拿到之前已经放入到 session 中的登录凭证之类的信息了。

  • 当多个应用要共享 session 时,除了以上问题,还会遇到跨域问题,因为不同的应用可能部署的主机不一样,需要在各个应用做好 cookie 跨域的处理。

  • sessionId 是存储在 cookie 中的,假如浏览器禁止 cookie 或不支持 cookie 怎么办? 一般会把 sessionId 跟在 url 参数后面即重写 url,所以 session 不一定非得需要靠 cookie 实现

  • 移动端对 cookie 的支持不是很好,而 session 需要基于 cookie 实现,所以移动端常用的是 token

使用 token 时需要考虑的问题

  • 如果你认为用数据库来存储 token 会导致查询时间太长,可以选择放在内存当中。比如 redis 很适合你对 token 查询的需求。

  • token 完全由应用管理,所以它可以避开同源策略

  • token 可以避免 CSRF 攻击(因为不需要 cookie 了)

  • 移动端对 cookie 的支持不是很好,而 session 需要基于 cookie 实现,所以移动端常用的是 token

使用 JWT 时需要考虑的问题

  • 因为 JWT 并不依赖 Cookie 的,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS)

  • JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。

  • JWT 不加密的情况下,不能将秘密数据写入 JWT。

  • JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。

  • JWT 最大的优势是服务器不再需要存储 Session,使得服务器认证鉴权业务可以方便扩展。但这也是 JWT 最大的缺点:由于服务器不需要存储 Session 状态,因此使用过程中无法废弃某个 Token 或者更改 Token 的权限。也就是说一旦 JWT 签发了,到期之前就会始终有效,除非服务器部署额外的逻辑。

  • JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。

  • JWT 适合一次性的命令认证,颁发一个有效期极短的 JWT,即使暴露了危险也很小,由于每次操作都会生成新的 JWT,因此也没必要保存 JWT,真正实现无状态。

  • 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

使用加密算法时需要考虑的问题

  • 绝不要以明文存储密码

  • 永远使用 哈希算法 来处理密码,绝不要使用 Base64 或其他编码方式来存储密码,这和以明文存储密码是一样的,使用哈希,而不要使用编码。编码以及加密,都是双向的过程,而密码是保密的,应该只被它的所有者知道, 这个过程必须是单向的。哈希正是用于做这个的,从来没有解哈希这种说法, 但是编码就存在解码,加密就存在解密。

  • 绝不要使用弱哈希或已被破解的哈希算法,像 MD5 或 SHA1 ,只使用强密码哈希算法。

  • 绝不要以明文形式显示或发送密码,即使是对密码的所有者也应该这样。如果你需要 “忘记密码” 的功能,可以随机生成一个新的 一次性的(这点很重要)密码,然后把这个密码发送给用户。

分布式架构下 session 共享方案

1. session 复制

  • 任何一个服务器上的 session 发生改变(增删改),该节点会把这个 session 的所有内容序列化,然后广播给所有其它节点,不管其他服务器需不需要 session ,以此来保证 session 同步

优点: 可容错,各个服务器间 session 能够实时响应。
缺点: 会对网络负荷造成一定压力,如果 session 量大的话可能会造成网络堵塞,拖慢服务器性能。

2. 粘性 session /IP 绑定策略

  • 采用 Ngnix 中的 ip_hash 机制,将某个 ip的所有请求都定向到同一台服务器上,即将用户与服务器绑定。 用户第一次请求时,负载均衡器将用户的请求转发到了 A 服务器上,如果负载均衡器设置了粘性 session 的话,那么用户以后的每次请求都会转发到 A 服务器上,相当于把用户和 A 服务器粘到了一块,这就是粘性 session 机制。

优点: 简单,不需要对 session 做任何处理。
缺点: 缺乏容错性,如果当前访问的服务器发生故障,用户被转移到第二个服务器上时,他的 session 信息都将失效。
适用场景: 发生故障对客户产生的影响较小;服务器发生故障是低概率事件 。
实现方式: 以 Nginx 为例,在 upstream 模块配置 ip_hash 属性即可实现粘性 session。

3. session 共享(常用)

  • 使用分布式缓存方案比如 Memcached 、Redis 来缓存 session,但是要求 Memcached 或 Redis 必须是集群

  • 把 session 放到 Redis 中存储,虽然架构上变得复杂,并且需要多访问一次 Redis ,但是这种方案带来的好处也是很大的:

    • 实现了 session 共享;

    • 可以水平扩展(增加 Redis 服务器);

    • 服务器重启 session 不丢失(不过也要注意 session 在 Redis 中的刷新/失效机制);

    • 不仅可以跨服务器 session 共享,甚至可以跨平台(例如网页端和 APP 端)

img

4. session 持久化

  • 将 session 存储到数据库中,保证 session 的持久化

优点: 服务器出现问题,session 不会丢失
缺点: 如果网站的访问量很大,把 session 存储到数据库中,会对数据库造成很大压力,还需要增加额外的开销维护数据库。

只要关闭浏览器 ,session 真的就消失了?

不对。对 session 来说,除非程序通知服务器删除一个 session,否则服务器会一直保留,程序一般都是在用户做 log off 的时候发个指令去删除 session。
然而浏览器从来不会主动在关闭之前通知服务器它将要关闭,因此服务器根本不会有机会知道浏览器已经关闭,之所以会有这种错觉,是大部分 session 机制都使用会话 cookie 来保存 session id,而关闭浏览器后这个 session id 就消失了,再次连接服务器时也就无法找到原来的 session。如果服务器设置的 cookie 被保存在硬盘上,或者使用某种手段改写浏览器发出的 HTTP 请求头,把原来的 session id 发送给服务器,则再次打开浏览器仍然能够打开原来的 session。
恰恰是由于关闭浏览器不会导致 session 被删除,迫使服务器为 session 设置了一个失效时间,当距离客户端上一次使用 session 的时间超过这个失效时间时,服务器就认为客户端已经停止了活动,才会把 session 删除以节省存储空间。

项目地址

在项目中使用 JWT

后语

  • 本文只是基于自己的理解讲了理论知识,因为对后端/算法知识不是很熟,如有谬误,还请告知,万分感谢

  • 如果本文对你有所帮助,还请点个赞~~

参考

百度百科-cookie

百度百科-session

详解 Cookie,Session,Token

一文彻底搞懂Cookie、Session、Token到底是什么

3种web会话管理的方式!!!

Token ,Cookie和Session的区别!!!

彻底理解 cookie、session、token!!!

前端鉴权

SHA-1

SHA-2

SHA-3

不要再使用MD5和SHA1加密密码了!

廖雪峰 Node 教程之 crypto


作者:秋天不落叶
来源:https://juejin.cn/post/6844904034181070861

收起阅读 »

不常见但是有用的chrome调试技巧

dom添加选中dom节点为全局变量方便需要调试多个dom的场景适用对dom有多次操作的场景force node state (触发)状态调试dom的某个状态copy element拷贝选中dom的信息style/class给选中元素添加一个 class 名快速...
继续阅读 »



dom

添加选中dom节点为全局变量方便需要调试多个dom的场景

适用对dom有多次操作的场景

force node state (触发)状态

调试dom的某个状态

copy element

拷贝选中dom的信息

style/class

给选中元素添加一个 class 名

快速给元素添加class

修改元素的盒模型大小

快速修改元素的盒模型大小(margin/padding/width/height等)

network

block specific request

block特定的请求

快捷键:command + shift + p -> show request blocking

改变请求的 user agent

修改请求的user agent

快捷键:command + shift + p -> network conditions 切换 user agent

javascript

断点,断浏览器的行为(比如 click、mouse 等等)

拦截浏览器的行为


快速改变拦截的变量的值

双击改变拦截变量的值

添加 watch 表达式

添加watch表达式

条件断点

设置断点的条件

快速调试代码片段

Snippet(片段)代码调试,不需要创建特定的页面

参考文档


作者:seventhMa
来源:https://juejin.cn/post/6963600839587921927

收起阅读 »

前端工程师生产环境 debugger 技巧

导言:那我们今天讲一讲如何使用 chrome 在生产环境进行 debug 。生产环境 debug 需要几步?这问题和“把大象装进冰箱拢共分几步”一样简单。第二步,把大象装进冰箱。找到需要 debug 的前端文件,格式化,打断点,调试上下文,定位问题;如何快速定...
继续阅读 »

导言:

开发环境 debug 是每个程序员上岗的必备技能。生产环境呢?虽然生产环境 debug 是一件非常不优雅的行为,但是由于种种原因,我们又不得不这么干。

那我们今天讲一讲如何使用 chrome 在生产环境进行 debug 。

生产环境 debug 步骤

生产环境 debug 需要几步?这问题和“把大象装进冰箱拢共分几步”一样简单。

第一步,把冰箱门打开。F12 打开 devTools;

第二步,把大象装进冰箱。找到需要 debug 的前端文件,格式化,打断点,调试上下文,定位问题;

第三部,关闭冰箱门。解决问题。

如何快速定位错误是前端还是后端接口返回的?

在把大象装进冰箱之前,先初步判断下,是否真的需要由你将大象装进冰箱。

首先我们需要判断,错误是前端还是后端报的,那么如何快速判断?

方案一:根据对代码的实现的了解,判断报错属于前端还是后端。

这个方案前提是需要你对代码实现很熟悉,也是最简单的方式。

方案二:前端代码全局搜索关键字,工程代码里搜索/控制台打开搜索。

对应工程 gitlab 或者 vscode 或者 devTools global search 里去进行全局搜索。

方案三:翻阅 network 面板中的请求。

翻阅 network 面板中的请求,看下返回的 response 是否携带错误提示,有则表示后端返回的;如果报错的接口刚好是以非200 的状态返回,或者是由新的操作触发调用接口,我们很快就能查找到对应的接口,如下:

方案四:使用 network search 进行搜索。

但是很多情况,接口业务错误会以 http status 200 的状态码返回,如果此时请求了大量的接口(举个例子:进入页面调用了大量的接口,其中有一个接口返回了错误信息),那么除了逐个翻阅 network 这种低效的方式,chrome devTools 还提供了 network search 面板这种更便捷的方式,可以搜索接口详细信息(包括详细的返回信息),返回匹配结果。

如何打开 network search 面板?

在 network 面板中,按快捷键 ⌘ + F(Mac)、 CTRL + F(Windows)可呼出 network search 面板。

如果确定需要你把大象装进冰箱,那把大象装进冰箱的技巧有哪些?

如何快速定位到问题相关的代码

global search ,全局搜素关键字,再定位到关键的代码

chrome devTools 的 global search 是一个非常实用的一个功能,当你不知道需要调试的代码在哪个文件时,当你是一个非常大的系统,引用了很多的资源文件,你可以使用 global search 进行搜索关键字,这个操作会搜索所有加载进来的资源,点击搜索结果,就可以使用 source 面板打开对应的资源文件,然后格式化代码,再然后在当前的文件内 再次搜索关键字,打断点。

打开 global search 快捷键:

⌘ + ⌥ + F (Mac),CTRL + SHIFT + F (Windows)

看下图例子,我们随便找个页面根据提示搜索代码:

可以尝试使用哪些关键字进行搜索:

(1) 页面存在明确的报错信息,且已经明确该错误文案是写在前端代码中错误信息文案。提示信息在 coding 过程中一般是使用 字符串,压缩混淆过程中一般是不会进行处理的,会保留原文,当然代码打包构建过程中,对代码压缩混淆也可以选择对中文进行 unicode 转码,此时如果关键字是中文,就需要先转码再搜索了。

(2) 已知相关代码中存在的编译混淆后依然还保留的的关键代码,会向外暴露的方法名;

如何 debug 混淆后的 js ?

生产环境的 js 基本上都是混淆过的(点击了解前端代码的压缩混淆),压缩混淆的优点就不赘述了,压缩混淆后随之来的是生产环境调试的难度,虽然通过打断点,勉强还能看的懂,但是已经很反人类了。

我们用一个最简单的 demo ,对比一下代码生产环境构建编译前后的差距。

这里选择用 vue-cli 创建了一个最简单的 demo ,看下源代码和编译后的代码。

源代码:

构建编译后的代码(此处关闭了 sourceMap ):

这里我们看到构建编译后的代码做了压缩混淆,出现了出现了大量大的 abcd 替换了原有的函数方法名、变量名,编译后的代码已经不是能通过单纯的读代码码能读懂的了。但是我们通过 debug ,大概还是能看得懂。

那么有没有方式使用本地的 sourceMap 调试生产环境的代码?答案当然是有的。

如何在生产环境使用本地 sourceMap 调试?

第一步:打开混淆代码

第二步:右键 -> 选择【Add source map】

第三步:输入本地 sourceMap 的地址(此处需要启用一个静态资源服务,可以使用 http-server),完成。本地代码执行构建命令,注意需要打开 sourceMap 配置,编译产生出构建后的代码,此时构建后的结果会包含 sourceMap 文件。

关联上 sourceMap 后,我们就可以看到 sources -> page 面板上的变化了

如何在 chrome 中修改代码并调试?

开发环境中,我们可以直接在 IDE 中修改代码,代码的变更就直接更新到了浏览器中了。那么生产环境,我们可以直接在 chrome 中修改代码,然后立马看代码修改后的效果吗?

当然,你想要的 chrome devTools 都有。chrome devTools 提供了 local overrides 能力。

local overrides 如何工作的?

指定修改后的文件的本地保存目录,当修改完代码保存的时候,就会将修改后的文件保存到你指定的目录目录下,当再次加载页面的时候,对应的文件不再读取网络上的文件,而是读取存储在本地修改过的文件。

local overrides 如何使用?

首先,打开 sources 下的 overrides 面板;

然后,点击【select folder overrides】选择修改后的文件存储地址;

再然后,点击顶部的授权,确认同意;

最后,我们就可以打开文件修改,修改完成后保存,重新刷新页面后,修改后的代码就被执行到了。

⚠️注意,原js文件直接 format 是无法修改的;在代码 format 之前先添加无效代码进行代码变更进行保存,然后再 format 就可以修改;

总结

chrome 调试技巧远远当然不只这些,以上只是生产环境 debug 的小技巧,祝愿大家用不到,最好的 bug 处理方式当然是事前,在上线前得到就解决;如果真的发生问题,如果做好监控和日志,在问题发生的第一时间发现并解决。

参考文献

作者:七喜
来源:https://zoo.team/article/prod-debugger

收起阅读 »

JS 的 6 种打断点的方式,你用过几种?

Debugger 是前端开发很重要的一个工具,它可以在我们关心的代码处断住,通过单步运行来理清逻辑。而 Debugger 用的好坏与断点打得好坏有直接的关系。Chrome Devtools 和 VSCode 都提供了 Debugger,它们支持的打断点的方式有...
继续阅读 »

Debugger 是前端开发很重要的一个工具,它可以在我们关心的代码处断住,通过单步运行来理清逻辑。而 Debugger 用的好坏与断点打得好坏有直接的关系。

Chrome Devtools 和 VSCode 都提供了 Debugger,它们支持的打断点的方式有 6 种。

普通断点

在想断住的那一行左侧单击一下就可以添加一个断点,运行到该处就会断住。

这是最基础的断点方式,VSCode 和 Chrome Devtools 都支持这种断点。

条件断点

右键单击代码所在的行左侧,会出现一个下拉框,可以添加一个条件断点。

输入条件表达式,当运行到这一行代码并且表达式的值为真时就会断住,这比普通断点灵活些。

这种根据条件来断住的断点 VSCode 和 Chrome Devtools 也都支持。

DOM 断点

在 Chrome Devtools 的 Elements 面板的对应元素上右键,选择 break on,可以添加一个 dom 断点,也就是当子树有变动、属性有变动、节点移除这三种情况的时候会断住。可以用来调试导致 dom 变化的代码。

因为是涉及到 DOM 的调试,只有 Chrome Devtools 支持这种断点。

URL 断点

在 Chrome Devtools 的 Sources 面板可以添加 XHR 的 url 断点,当 ajax 请求对应 url 时就会断住,可以用来调试请求相关的代码。

这个功能只有 Chrome Devtools 有。

Event Listener 断点

在 Chrome Devtools 的 Sources 面板还可以添加 Event Listener 的断点,指定当发生什么事件时断住,可以用来调试事件相关代码。

这个功能也是只有 Chrome Devtools 有。

异常断点

在 VSCode 的 Debugger 面板勾选 Uncaught Exceptions 和 Caught Exceptions 可以添加异常断点,在抛出异常未被捕获或者被捕获时断柱。用来调试一些发生异常的代码时很有用。

总结

Debugger 打断点的方式除了直接在对应代码行单击的普通断点以外,还有很多根据不同的情况来添加断点的方式。

一共有六种:

  • 普通断点:运行到该处就断住
  • 条件断点:运行到该处且表达式为真就断住,比普通断点更灵活
  • DOM 断点:DOM 的子树变动、属性变动、节点删除时断住,可以用来调试引起 DOM 变化的代码
  • URL 断点:URL 匹配某个模式的时候断住,可以用来调试请求相关代码
  • Event Listener 断点:触发某个事件监听器的时候断住,可以用来调试事件相关代码
  • 异常断点:抛出异常被捕获或者未被捕获的时候断住,可以用来调试发生异常的代码

这些打断点方式大部分都是 Chrome Devtools 支持的(普通、条件、DOM、URL、Event Listener、异常),也有的是 VSCode Debugger 支持的(普通、条件、异常)。

不同情况下的代码可以用不同的打断点方式,这样调试代码会高效很多。

JS 的六种打断点方式,你用过几种呢?

原文:https://juejin.cn/post/7041946855592165389

收起阅读 »

这些都能成为 Web 语法规范,强迫症看不下去了

JavaScript 一直是饱受诟病,源于网景公司在 1995 年用了 10 天的时间创造。没有什么能用 10 天创造就是完美的,可是某些特性一旦发布,错误或不完善的地方迅速成为必不可少的特色,并且是几乎不可能改变。 Javascript 的发展非常快,根本没...
继续阅读 »

JavaScript 一直是饱受诟病,源于网景公司在 1995 年用了 10 天的时间创造。没有什么能用 10 天创造就是完美的,可是某些特性一旦发布,错误或不完善的地方迅速成为必不可少的特色,并且是几乎不可能改变。


Javascript 的发展非常快,根本没有时间调整设计。在推出一年半之后,国际标准就问世了。设计缺陷还没有充分暴露就成了标准。


历史遗留


比如常见的历史设计缺陷:



  • nullundefined 两者非常容易混淆

  • == 类型转换的问题

  • var 声明创建全局变量

  • 自动插入行尾分号

  • 加号可以表示数字之和,也可以表示字符的连接

  • NaN 奇怪的特性

  • 更多...


Javascript 很多不严谨的特性我们可以添加 eslint 来规避。比如禁用 var== 成了大多数人写代码的必备条件。


现在/未来


如今 CSS、DOM、HTML 规范由 W3C 来制定,JavaScript 规范由 TC39 制定。那些历史缺陷也成为了过去,但是现在也出现了一些不尽人意的规范。


CSS 变量


声明变量的时候,变量名前面要加两根连词线 --


body {
--foo: #7f583f;
--bar: #f7efd2;
}

var() 函数用于读取变量。


a {
color: var(--foo);
text-decoration-color: var(--bar, #7f583f);
}

为什么选择两根连词线(--)表示变量?因为 $Sass 用掉,@Less 用掉。_-,用作为 IEchrome 兼容写法。CSS 中已经找不出来字符可以代替变量声明了。为了不产生冲突,官方的 CSS 变量就改用两根连词线。


作为一个官方的标准规范,时刻影响后面的行业发展。竟然能被第三方的插件所左右,令人大跌眼镜。有开发者吐槽:微软的架构师也是够窝囊。


现在很多应用都放弃了 Sassless,转向了 PostCSS 的怀抱。面向组件编程,根本用不到 Sassless 里面的一些复杂功能。那么 -- 两个字符的繁琐将成为开发者永远的痛。


类私有属性(proposal-class-fields)


JavaScript 中的 class 大家已经不陌生了,简直跟 Javaclass 一模一样。


基本用法:


class BaseClass {
msg = 'hello world';

basePublicMethod() {
return this.msg;
}
}

继承:


class SubClass extends BaseClass {
subPublicMethod() {
return super.basePublicMethod();
}
}

静态属性:


class ClassWithStaticField {
static baseStaticMethod() {
return 'base static method output';
}
}

异步方法


class ClassWithFancyMethods {
*generatorMethod() {}
async asyncMethod() {}
async *asyncGeneratorMethod() {}
}

而类私有属性的提案目前已经进入标准,它用了 # 关键字前缀来修饰一个类的属性。


class ClassWithPrivateField {
#privateField;

constructor() {
this.#privateField = 42;
}
}

你没看错,不是 typescript 中的 private 关键字。


class BaseClass {
readonly msg = 'hello world';

private basePrivateMethod() {
return this.msg;
}
}

然而 # 的语法丑陋本身引起了社区的争议:



「class fields 提案提供了一个极具争议的私有字段访问语法——并成功地做对了唯一一件事情,让社区把全部的争议焦点放在了这个语法上」。




TS 投降主义已经被迫实现了。




No dynamic access, no destructuring is a deal breaker for me




我们制作一个 eslint 插件 no-private-class-fields 并使用下载计数来说明社区反对




'#' 作为名称的一部分会导致混淆,因为 this.#x !== this['#x'] 太奇怪了



前端架构师、TC39 成员贺师俊也在知乎连发好几篇文章吐槽 class fields


不妨大家看看关于 private 的 side: johnhax.net/2017/js-pri…


提案地址:github.com/tc39/propos…


globalThis


在不同的 JavaScript 环境中拿到全局对象是需要不同的语句的。在 Web 中,可以通过 windowself 取到全局对象,但是在 Web Workers 中只有 self 可以。在 Node.js 中,必须使用 global。非严格模式下,可以在函数中返回 this 来获取全局对象,否则会返回 undefined


因此一个叫 global 的提案出现。主要用 global 变量统一上面的行为,但后面绕来绕去改成了 globalThis,引起了激烈讨论。


globalThis 这个名字会让 this 变得更加复杂。



  1. this 一直是困扰程序员的话题,尤其是 JavaScript 新手,关于它的博客文章源源不断

  2. ES6 让事情变得更简单,因为可以告诉人们更喜欢箭头函数并且只使用 this 内部方法定义

  3. 在现代 JS(modules) 中,并没有真正的全局 this,所以 globalThis 甚至不引用现有的概念


现在说这一切都是徒劳的,因为它已经进入 stage 4


提案地址:github.com/tc39/propos…


总结


JavaScript 中遗留的糟粕太多。现在受到这些糟粕的影响,很多新的提案又不得不妥协。在未来,它会变得极其复杂。


也许某一天,会出现一个没有历史包袱的 JavaScript 子集来替换它。



作者:MinJie
链接:https://juejin.cn/post/7043340139049222152

收起阅读 »

13 行 JavaScript 代码让你看起来像是高手

Javascript 可以做许多神奇的事情,也有很多东西需要学习,今天我们介绍几个短小精悍的代码段。 获取随机布尔值(True/False) 使用 Math.random() 会返回 0 到 1 的随机数,之后判断它是否大于 0.5,将会得到一个 50% 概率...
继续阅读 »

Javascript 可以做许多神奇的事情,也有很多东西需要学习,今天我们介绍几个短小精悍的代码段。


获取随机布尔值(True/False)


使用 Math.random() 会返回 0 到 1 的随机数,之后判断它是否大于 0.5,将会得到一个 50% 概率为 TrueFalse 的值


const randomBoolean = () => Math.random() >= 0.5;
console.log(randomBoolean());

判断一个日期是否是工作日


判断给定的日期是否是工作日


const isWeekday = (date) => date.getDay() % 6 !== 0;
console.log(isWeekday(new Date(2021, 0, 11)));
// Result: true (周一)
console.log(isWeekday(new Date(2021, 0, 10)));
// Result: false (周日)

反转字符串


有许多反转字符串的方法,这里使用一种最简单的,使用了 split()reverse()join()


const reverse = str => str.split('').reverse().join('');
reverse('hello world');
// Result: 'dlrow olleh'

判断当前标签页是否为可视状态


浏览器可以打开很多标签页,下面 👇🏻 的代码段就是判断当前标签页是否是激活的标签页


const isBrowserTabInView = () => document.hidden;
isBrowserTabInView();

判断数字为奇数或者偶数


取模运算符 % 可以很好地完成这个任务


const isEven = num => num % 2 === 0;
console.log(isEven(2));
// Result: true
console.log(isEven(3));
// Result: false

从 Date 对象中获取时间


使用 Date 对象的 .toTimeString() 方法转换为时间字符串,之后截取字符串即可


const timeFromDate = date => date.toTimeString().slice(0, 8);
console.log(timeFromDate(new Date(2021, 0, 10, 17, 30, 0)));
// Result: "17:30:00"
console.log(timeFromDate(new Date()));
// Result: 返回当前时间

保留指定的小数位


const toFixed = (n, fixed) => ~~(Math.pow(10, fixed) * n) / Math.pow(10, fixed);
// Examples
toFixed(25.198726354, 1); // 25.1
toFixed(25.198726354, 2); // 25.19
toFixed(25.198726354, 3); // 25.198
toFixed(25.198726354, 4); // 25.1987
toFixed(25.198726354, 5); // 25.19872
toFixed(25.198726354, 6); // 25.198726

检查指定元素是否处于聚焦状态


可以使用 document.activeElement 来判断元素是否处于聚焦状态


const elementIsInFocus = (el) => (el === document.activeElement);
elementIsInFocus(anyElement)
// Result: 如果处于焦点状态会返回 True 否则返回 False

检查当前用户是否支持触摸事件


const touchSupported = () => {
('ontouchstart' in window || window.DocumentTouch && document instanceof window.DocumentTouch);
}
console.log(touchSupported());
// Result: 如果支持触摸事件会返回 True 否则返回 False

检查当前用户是否是苹果设备


可以使用 navigator.platform 判断当前用户是否是苹果设备


const isAppleDevice = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
console.log(isAppleDevice);
// Result: 是苹果设备会返回 True

滚动至页面顶部


window.scrollTo() 会滚动至指定的坐标,如果设置坐标为(0,0),就会回到页面顶部


const goToTop = () => window.scrollTo(0, 0);
goToTop();
// Result: 将会滚动至顶部

获取所有参数的平均值


可以使用 reduce() 函数来计算所有参数的平均值


const average = (...args) => args.reduce((a, b) => a + b) / args.length;
average(1, 2, 3, 4);
// Result: 2.5

转换华氏/摄氏


再也不怕处理温度单位了,下面两个函数是两个温度单位的相互转换。


const celsiusToFahrenheit = (celsius) => celsius * 9/5 + 32;
const fahrenheitToCelsius = (fahrenheit) => (fahrenheit - 32) * 5/9;
// Examples
celsiusToFahrenheit(15); // 59
celsiusToFahrenheit(0); // 32
celsiusToFahrenheit(-20); // -4
fahrenheitToCelsius(59); // 15
fahrenheitToCelsius(32); // 0

感谢阅读,希望你会有所收获😄


作者:夜色镇歌
链接:https://juejin.cn/post/7043062481954013197

收起阅读 »

美团跨端一体化富文本管理技术实践

为了减少产品和前端开发人员之间的矛盾,不断降本提效,美团医药技术部构建了跨端一体化富文本管理平台Page-佩奇。本文系统介绍了该平台的定位、设计思路、实现原理以及取得的成效。希望这些实战经验与总结,能给大家带来一些启发或帮助。一、引言在互联网圈,开发和产品经理...
继续阅读 »



为了减少产品和前端开发人员之间的矛盾,不断降本提效,美团医药技术部构建了跨端一体化富文本管理平台Page-佩奇。本文系统介绍了该平台的定位、设计思路、实现原理以及取得的成效。希望这些实战经验与总结,能给大家带来一些启发或帮助。

一、引言

在互联网圈,开发和产品经理之间相爱相杀的故事,相信大家都有所耳闻。归根结底,往往都是从简单的改需求开始,然后你来我往、互不相让,接着吵架斗嘴,最后导致矛盾不断升级,甚至带来比较严重的后果。

pic_4bf1ae9b.png

图1

在这种背景下,如果把一些功能相对简单的、需求变动比较频繁的页面,直接交给产品或者运营自己去通过平台实现,是不是就可以从一定程度上减少产品和开发人员之间的矛盾呢?

二、背景

当然上述的情况,美团也不例外。近些年,美团到家事业群(包括美团外卖、美团配送、闪购、医药、团好货等)的各个业务稳步发展,业务前端对接的运营团队有近几十个,每个运营团队又有不同的运营规则,这些规则还存在一些细微的样式差别,同时规则内容还会随着运营季节、节日、地理位置等进行变化和更新。这些需求具体来说有以下几个特点:

  1. 需求量大:业务稳步发展,业务需求不断叠加,甚至部分业务呈指数级增长,且业务方向涉及到一些业务规则、消息通知、协议文档、规则介绍等需求。

  2. 变更频繁:面对市场监管和法务的要求,以及新业务调整等因素的影响,会涉及到需求的频繁变更,像一些业务FAQ、产品介绍、协议文档、业务规则、系统更新日志等页面,需要做到快速响应和及时上线。

  3. 复杂度低:这些页面没有复杂的交互逻辑,如果能把这些简单的页面交给运营/产品去实现,开发人员就能有更多的时间去进行复杂功能的研发。

  4. 时效性高:临时性业务需求较多,且生命周期较短,具有定期下线和周期性上线等特点。

基于以上特点,为了提高研发效率,美团医药技术部开始构建了一个跨端一体化富文本管理平台,希望提供解决这一大类问题的产研方案。不过,部门最初的目标是开发一套提效工具,解决大量诸如帮助文档、协议页、消息通知、规则说明等静态页面的生产与发布问题,让产品和运营同学能够以所见即所得的方式自主完成静态页面制作与发布,进而缩短沟通成本和研发成本。

但是,随着越来越多业务部门开始咨询并使用这个平台,我们后续不断完善并扩充了很多的功能。经过多次版本的设计和迭代开发后,将该平台命名为Page-佩奇,并且注册成为美团内部的公共服务,开始为美团内部更多同学提供更好的使用体验。

本文将系统地介绍Page-佩奇平台的定位、设计思路、实现原理及取得成效。我们也希望这些实战经验与总结,能给更多同学带来一些启发和思考。

三、跨端一体化富文本管理解决方案

3.1 平台定位

我们希望将Page-佩奇打造成一款为产品、运营、开发等用户提供快速一站式发布网页的产研工作台,这是对该平台的一个定位。

  • 对产品运营而言,他们能够可视化地去创建或修改一些活动说明、协议类、消息类的文章,无需开发排期,省去向开发二次传递消息等繁琐的流程,也无需等待漫长的发布时间,从而达到灵活快速地进行可视化页面的发布与管理。

  • 对开发同学而言,他们能够在线编写代码,并实现秒级的发布上线,并且支持ES 6、JavaScript 、Less、CSS语法,我们还提供了基础的工具、图表库等,能够生成丰富多样的页面。帮助开发同学快速实现数据图表展示,设计特定样式,完成各种交互逻辑等需求。

  • 对项目管理方而言,他们能够清晰地看到整个需求流转状态和开发日志信息,为运营管理提供强大的“抓手”。

一般来讲,传统开发流程是这样的:首先产品提出需求,然后召集研发评审,最后研发同学开发并且部署上线;当需求上线之后,如果有问题需要反馈,产品再找研发同学进行沟通并修复,这种开发流程也是目前互联网公司比较常见的开发流程。

pic_b81cd143.png

图2 传统开发流程图

而美团Page-佩奇平台的开发流程是:首先产品同学提出需求,然后自己在Page平台进行编辑和发布上线,当需求上线之后有问题需要反馈,直接就能触达到产品同学,他们通常可自行进行修复。如果需求需要定制化,或者需要做一些复杂的逻辑处理,那么再让研发人员配合在平台上进行开发并发布上线。

pic_b3e5d331.png

图3 Page-佩奇平台开发流程图

简单来说,对那些功能相对简单、需求变动比较频繁的页面,如果用传统的开发流程将会增加产研沟通和研发排期成本,因此传统方案主要适用于功能复杂型的需求。而Page-佩奇平台开发流程,并不适合功能复杂型的需求,特别适用于功能相对简单、需求变动比较频繁的页面需求。

综上所述,可以看出这两种开发流程其实起到了一个互补的作用,如果一起使用,既可以减少工作量,又可以达到降本提效的目的。

3.2 设计思路

我们最初设计Page-佩奇平台的初心其实很简单,为了给产品和运营提供一个通过富文本编辑器快速制作并发布网页的工具。但是,在使用的过程中,很多缺陷也就慢慢地开始暴露,大致有下面这些问题:

  1. 简单的富文本编辑器满足不了想要的页面效果,怎么办?

  2. 如果能导入想要的模板,是否会更友好?

  3. 怎么查看这个页面的访问数据?如何能监控这个页面的性能问题?

  4. 发布的页面是否有存在安全风险?

于是,我们针对这些问题进行了一些思考和调研:

  • 当富文本编辑器满足不了想要实现的效果的时候,可以引入了WebIDE编辑器,可以让研发同学再二次编辑进行实现。

  • 一个系统想要让用户用得高效便捷,那么就要完善它的周边生态。就需要配备完善的模板素材和物料供用户灵活选择。

  • 如果用户想要了解页面的运行情况,那么页面运行的性能数据、访问的数据也是必不可少的。

  • 如果发布的内容存在不当言论,就会造成不可控的法律风险,所以内容风险审核也是必不可少的。

实现一个功能很容易,但是想要实现一个相对完善的功能,就必须好好下功夫,多思考和多调研。于是,围绕着这些问题,我们不断挖掘和延伸出了一系列功能:

  1. 富文本编辑:强大而简单的可视化编辑器,让一切操作变得简单、直观。产品同学可以通过编辑器自主创建、编辑网页,即使无程序开发经验也可以通过富文本编辑器随意操作,实现自己想要的效果,最终可以实现一键快速发布上线。

  2. WebIDE:定制化需求,比如,与客户端和后端进行一些通信和请求需求,以及针对产品创建的HTML进行二次加工需求,均可以基于WebIDE通过JavaScript代码实现。具备专业开发经验的同学也可以选择通过前端框架jQuery、Vue,Echarts或者工具库Lodash、Axios实现在线编辑代码。

  3. 页面管理:灵活方便地管理页面。大家可以对有权限的文档进行查看、编辑、授权、下线、版本对比、操作日志、回滚等操作,且提供便捷的文档搜索功能。

  4. 模板市场:丰富多样的网页模板,简易而又具备个性。模板市场提供丰富的页面模板,大家可选择使用自己的模板快速创建网页,且发布的每个页面又可以作为自己的模板,再基于这个模板,可随时添加个性化的操作。

  5. 物料平台:提供基础Utils、Echart、Vue、jQuery等物料,方便开发基于产品的页面进行代码的二次开发。

  6. 多平台跨端接入:高效快捷地接入业务系统。通过通信SDK,其他系统可以快速接入Page-佩奇平台。同时支持以HTTP、Thrift方式的开放API供大家选择,支持客户端、后端调用开放API。

  7. 内容风险审核:严谨高效的审核机制。接入美团内部的风险审核公共服务,针对发布的风险内容将快速审核,防止误操作造成不可控的法律风险。

  8. 数据大盘:提供页面的数据监测,帮助大家时刻掌握流量动向。接入美团内部一站式数据分析平台,帮助大家安全、快速、高效地掌握页面的各种监测数据。

  9. 权限管理:创建的每个页面都有相对独立的权限,只有经过授权的人才能查看和操作该页面。

  10. 业务监控:提供页面级别JavaScript错误和资源加载成功率等数据,方便开发排查和解决线上问题。

功能流程图如下所示:

pic_9d007bab.png

图4 Page-佩奇平台功能流程图

3.3 实现原理

3.3.1 基础服务

Page-佩奇平台的基础服务有四个部分,包括物料服务、编译服务、产品赋能、扩展服务。

pic_f0bf9912.png

图5 整体架构图

3.3.2 核心架构

pic_bbb3bb9a.png

图6 核心架构图

Page-佩奇平台核心架构主要包含页面基础配置层、页面组装层以及页面生成层。我们通过Vuex全局状态对数据进行维护。

  • 页面基础配置层主要提供生成页面的各种能力,包括富文本的各种操作能力、编辑源码(HTML、CSS、JavaScript)的能力、自定义域名配置、适配的容器(PC/H5)、发布环境等。

  • 页面组装层则会基于基础配置层所提供的的能力,实现页面的自由编辑,承载大量的交互逻辑,用户的所有操作都在这一层进行。

    • 业务PV和UV埋点,错误统计,访问成功率上报。

    • 自动适配PC和移动端样式。

    • 内网页面显示外网不可访问标签。

  • 页面生成层则需要根据组装后的配置进行解析和预处理、编译等操作,最终生成HTML、CSS、JavaScript渲染到网页当中。

3.3.3 关键流程

pic_b5adef78.png

图7 关键流程图

如上图7所示,平台的核心流程主要包含页面创建之后的页面预览、编译服务、生成页面。

  • 页面预览:创建、编辑之后的页面,将会根据内容进行页面重组,对样式和JavaScript进行预编译之后,对文本+JavaScript+CSS进行组装,生成HTML代码块,然后将代码块转换成Blob URL,最终以iframe的方式预览页面。

  • 编译服务:文件树状结构和代码发送请求到后端接口,基于Webpack将Less编译成CSS,ES 6语法编译成ES 5。通用物料使用CDN进行引入,不再进行二次编译。

  • 生成页面:当创建、编辑之后的页面进行发布时,服务端将会进行代码质量检测、内容安全审查、代码质量检测、单元测试、上传对象存储平台、同步CDN检测,最终生成页面链接进行访问。

3.3.4 多平台接入

Page-佩奇平台也可以作为一个完善的富文本编辑器供业务系统使用,支持内嵌到其他系统内。作为消息发布等功能承载,减少重复的开发工作,同时我们配备完善的SDK供大家选择使用。通过Page-SDK可以直接触发Page平台发布、管理等操作,具体的流程如下图所示:

pic_e3c8e777.png

图8 Page-SDK流程图

3.3.5 Open API

在使用Page-佩奇平台的时候,美团内部一些业务方提出想要通过Page-佩奇平台进行页面的发布,同时想要拿到发布的内容做一些自定义的处理。于是,我们提供了Open API开放能力,支持以HTTP和Thrift两种方式进行调用。下面主要讲一下Thrift API实现的思路,首先我们先了解下Thrift整体流程:

pic_a6286e54.png

图9 Thrift整体流程图

Thrift的主要使用过程如下:

  1. 服务端预先编写接口定义语言 IDL(Interface Definition Language)文件定义接口。

  2. 使用Thrift提供的编译器,基于IDL编译出服务语言对应的接口文件。

  3. 被调用服务完成服务注册,调用发起服务完成服务发现。

  4. 采用统一传输协议进行服务调用与数据传输。

下面具体讲讲,Node语言是如何实现和其他服务语言实现调用的。由于我们的服务使用的Node语言,因此我们的Node服务就充当了服务端的角色,而其他语言(Java等)调用就充当了客户端的角色。

pic_ca36c11d.png

图10 Thrift使用详细流程图

  • 生成文件:由服务端定义IDL接口描述文件,然后基于IDL文件转换为对应语言的代码文件,由于我们用的是Node语言,所以转换成JavaScript文件。

  • 服务端启动服务:引入生成的JavaScript文件,解析接口、处理接口、启动并监听服务。

  • 服务注册:通过服务器内置的“服务治理代理”,将服务注册到美团内部的服务注册路由中心(也就是命名服务),让服务可被调用方发现。

  • 数据传输:被调用时,根据“服务治理服务”协议序列化和反序列化,与其他服务进行数据传输。

目前,美团内部已经有相对成熟的NPM包服务,已经帮我们实现了服务注册、数据传输、服务发现和获取流程。客户端如果想调用我们所提供的的Open API开放能力,首先申请AppKey,然后选择使用Thrift方式或者HTTP的方式,按照所要求的参数进行请求调用即可。

3.4 方案实践

3.4.1 H5协议

能力:富文本编辑。

描述:提供富文本可视化编辑,产品和运营无需前端就可以发布和二次编辑页面。

场景:文本协议,消息通知,产品FAQ。

具体案例:

pic_f883a4d1.png

图11 H5静态文本协议案例

3.4.2 业务自定义渲染

能力:开放API(Thirft + HTTP)。

描述:提供开放API,支持业务自定义和样式渲染到业务系统,同时解决了iframe体验问题。

场景:客户端、后端、小程序的同学,可根据API渲染文案,实现动态化管理富文本信息。

具体案例:

小程序使用组件、Vue使用v-html指令实现动态化渲染商品选择说明。

{
   "code": 0,
   "data": {
     "tag": "苹果,标准",
     "title": "如何挑选苹果",
     "html": "<h1>如何挑选苹果</h1>><p>以下标准可供消费者参考</p><ul><li>酸甜</li><li>硬度</li></ul>",
     "css": "",
     "js": "",
     "file": {}
  },
   "msg": "success"
}

3.4.3 投放需求

能力:WebIDE代码编辑。

描述:开发基于WebIDE代码开发工作,基于渠道和环境修改下载链接,能够做到分钟级支撑。

场景:根据产品创建静态页面进行逻辑和样式开发。

具体案例:

var ua = window.navigator.userAgent
   var URL_MAP = {
       ios: 'https://apps.apple.com/cn/app/xxx',
       android: 'xxx.apk',
       ios_dpmerchant: 'itms-apps://itunes.apple.com/cn/app/xxx'
  }
   
   if (ua.match(/android/i)) location.href = URL_MAP.android
   if (ua.match(/(ipad|iphone|ipod).*os\s([\d_]+)/i)) {
       if (/xx\/com\.xxx\.xx\.mobile/.test(ua)) {
           location.href = URL_MAP.ios_dpmerchant
      } else {
           location.href = URL_MAP.ios
      }
  }

3.4.4 客户端通信中间页

能力:WebIDE代码编辑 + 物料平台。

描述:通过物料平台,引入公司客户端桥SDK,可以快速完成客户端通信需求。方便前端调试客户端基础桥功能。

场景:客户端跳转,通信中间页。

具体案例:

// 业务伪代码
   XXX.ready(() => {
       XXX.sendMessage({
          sign: true,
           params: {
               id: window.URL
          }
      }, () => {
           console.error('通信成功')
      }, () => {
           console.error('通信失败')
      })
  })

3.4.5 业务系统内嵌Page

能力:提供胶水层Page-SDK,连接业务系统和Page。

描述:业务系统与Page-佩奇平台可进行通信,业务系统可调用Page发布、预览、编辑等功能,Page可返回业务系统页面链接、内容、权限等信息。减少重复前后端工作,提升研发效率。

场景:前端富文本信息渲染,后端富文本信息管理后台。

具体案例:

pic_ec5b5f49.png

图12 业务系统内嵌Page案例

3.5 业务成绩

截止目前数据统计,Page-佩奇平台生成网页5000多个,编辑页面次数16000多次,累计页面访问PV超过8260万。现在,美团已经有十多个部门和三十多条业务线接入并使用了Page-佩奇平台。

pic_becbff8a.png

图13 Page-佩奇平台每日生成页面统计

四、总结与展望

富文本编辑器和WebIDE不仅是复杂的系统,而且还是比较热门的研究方向。特别是在和美团的基建结合之后,能够解决团队内部很多效率和质量问题。这套系统还提供了语法智能提示、Diff对比、前置检测、命令行调试等功能,不仅要关注业务发布出去页面的稳定性和质量,更要有内置的一系列研发插件,主动帮助研发提高代码质量,降低不必要的错误。

经过长期的技术和业务演进,Page-佩奇平台已经能够有效地帮助研发人员大幅提升开发效率,具备初级的Design To Code能力,但是仍有许多业务场景值得去我们探索。我们也期待优秀的你参与进来,一起共同建设。

  • WebIDE融合:完善基础设施建设和功能需求,更好地支持Vue、React、ES 6、TS、Less语法,预览模式采用浏览器编译,能有效地提高预览的速度,发布使用后端编译的模式。

  • 研发流程链路:针对代码进行有效评估,包括ESlint、代码重复率、智能提示是否可以三方库替代。出具开发代码质量、业务上线的质量报告。

  • 综合研发平台:减少团队同学了解整体基建的时间成本,内置了监控、性能、任务管理等功能,提升业务开发效率。建设自动化日报、周报系统,降低非开发工作量占比。

  • 物料开放能力:接入公共组件平台,沉淀更多的物料,快速满足产品更多样化的需求。

五、作者简介

高瞻、宇立、肖莹、浩畅,来自美团医药终端团队。王咏、陈文,来自美团闪购终端团队。
来源:https://blog.csdn.net/MeituanTech/article/details/121551030

收起阅读 »

换一个方式组织你的Axios代码?

自从Jquery被mvvm平替了之后,$.ajax 也被 axios 平替了,在使用这个方式之前,我想大部分的人也想到了去封装一个请求,然后每一次调用去做Get 、Post的请求服务,也有一些人习惯在vue里直接编写 this.$axios.get() ,萝卜...
继续阅读 »

自从Jquery被mvvm平替了之后,$.ajax 也被 axios 平替了,在使用这个方式之前,我想大部分的人也想到了去封装一个请求,然后每一次调用去做Get 、Post的请求服务,也有一些人习惯在vue里直接编写 this.$axios.get() ,萝卜青菜各有所爱,没有优劣之分。



灵感来源


d4axios的灵感来源于open-figen,现在功能还没有那么丰富,但是足以应付多大多数的场景,比如上传、下载、get、post等等请求,配合上ts,可以解决数据类型前后台一致性,数据类型的转换,保证了请求的便利性。让代码专注于数据处理,而非复制粘贴模版代码



别忘了 在 [ts | js]config.json 文件里开启对装饰器的支持。"experimentalDecorators":true



在项目中使用 d4axios



d4axios (Decorators for Axios) 是一款基于axios请求方式的装饰器方法组,可以快速地对服务进行管理和控制,增强前端下服务请求方式,有效降低服务的认知难度,并且在基于ts的情况下可以保证服务的前后台的数据接口一致性



npm i d4axios

yarn add d4axios

一、 引入配置信息


在这里提供了几种配置方式,可以采用原始的axios的配置方法,也可以采用 d4axios 提供的方法


// 在 vue3下我们建议使用 `createService` 
// 其他情况下使用 `serviceConfig`
import { createApp } from 'vue'
import {createService,serviceConfig} from 'd4axios'


createApp(App).use(createService({ ... /* 一些配置信息 */}))


1.1 提供的axios配置项


createServiceserviceConfig 使用的配置项是一样的,并且完全兼容axios的配置。在现有的项目中改造的话,可以使用:


// 可以直接使用由d4axios提供的服务
createService()

// 可直接传入axios的相关配置,由d4axios自动基于配置相关构建
createService({axios:{ baseURL:"domain.origin" }})

// 可直接传入已经配置好的 `axios` 实例对象
const axios = Axios.create({ /* 你的配置*/ });


createService({axios})

1.2 提供基于请求和相应数据的配置


createService({
beforeRequest(requestData){
// form对象会被转为JSON对象的浅拷贝,但是会在该方法执行完后重新转为form对象
// 你可在请求前追加一些补充的请求参数
// 比如对请求体进行签名等等
return requestData
},
beforeResponse(responseData){
// 默认情况下会返回 axios的response.data值,而不会返回response的完整对象
// 可以修改返回的响应结果
return responseData
}
})

1.3 提供快速的axios interceptors 配置


createService({
interceptors:{
request:{
use(AxiosRequestConfig){},
reject(){}
},
response{
use(AxiosResponse){},
reject(){}
}
}
})

配置完成后,会返回一个axios实例对象,可以继续对axios对象做更多的操作,可以绑定到vue.prototype.$axios下使用


Vue.prototype.$axios = serviceConfig({... /*一些配置*/})

二、创建请求服务


为了更好的组织业务逻辑,d4axios提供了一系列的组织方法,供挑选使用


import {Service,Prefix,Get,Post,Download,Upload,Param,After,Header} from 'd4axios'

@Service("UserService") // 需要提供一个服务名称,该名称将在使用服务的时候被用到
@Prefix("/user") // 可以给当前的服务添加一个请求前缀
export class UserService {

@Get("/:id") // 等同于 /user/:id
@After((respData)=>{
//在输出给最终结果前,可以对结果做一些简单处理
return respData
})
async getUserById(id:string){
// 异步请求需要增加 `async` 属性以便语法识别
// 支持restful的方式
}


@Post("/regist")
@Header({'plantform':'android'}) // 请求前追加一些header参数
async registUser(form:UserForm){
// 可以在请求的时候做一些参数修改
form.nickName = createNickName(form.nickName);

// return的值是最终请求的参数
return {
...form,
plant:"IOS"
};
}

@Download("/user/card") // 支持文件下载
async downloadCard(@Param("id") id:stirng){
// 当我们的参数较少并且不是一个key-value形式的值时
// 可以使用@Param辅助,确定传参名称
}

@Upload("/user/card") // 支持文件上传
async uploadCard(file:File){
return {file}
}

// 可以定义同步函数,直接做服务计算
someSyncFunc(){
return 1+1
}

// 我们还可以直接定义非请求函数
async someFunc(){
// 所有的当前函数都是可以直接调用的
return await this.getUserById(this.someSyncFunc());
}

}


三、使用服务



使用服务分为几种方式,第一种是在一个服务中调用另一个服务。第二种是在react或者vue中调用服务,对于这两种有不同的方法,也可以用相同的方法。



3.1 在 vue或者react中使用useService 导入服务


// 在 vue 或者 react中,可以直接使用 useService 导入一个服务对象
import {useService} from 'd4axios'
import SomeService from './some.service'

const someService = useService(SomeService)

复制代码

3.2 在一个服务中Use调用另一个服务


import {Use} from 'd4axios'
import SomeService from './some.service'
// 也可以直接像上面一样的导入进来是用
const someService = useService(SomeService)

@Service("MyService")
export class MyService {
@Use(SomeService) // use 导入服务
// 默认的属性名为小写驼峰
// 用 S<T> 包裹服务名称,这样可以得到相应的async方法的响应类型
someService !: S<SomeService>

async someMethod(){
// 就可以使用了,
await this.someService.something();
}
}

四、响应重写


默认情况下,d4axios支持async响应类型值,该值为


 export interface ResponseDataType<T> { }

在项目根路径下定义 d4axios.d.ts文件
然后文件内定义,通过重写该类型,可以得到响应的 response type类型,比如



export interface ResponseDataType<T> {
data : T;
msg:string ;
code:string ;
}

后即可以得到相关内容的提示信息


dataType.png


五、其他一些基于 Decorators 的操作


5.1 在使用装饰器的class上都可以使用 Use 导入服务 比如:


import {Component,Vue,} from 'vue-class-decorator'
import SomeService from './some.service'

@VueServiceBind(MyService,OtherService) // 只能在vue的这种形式下使用,可以绑定多个值
@Component
export default class MyVueComp extends Vue {

@Use(SomeService) // use 导入服务
// 默认的属性名为小写驼峰
// 用 S<T> 包裹服务名称,这样可以得到相应的async方法的响应类型
someService !: S<SomeService>

myService !: S<MyService>

otherService !: S<OtherService>
}

5.2 在一般的vue的服务下可以使用这种 mapService 形式


// 传统的模式下

import { mapService } from 'd4axios';
import MyService from './MyService.service'

export default {
computed:{
...mapService(MyService,OtherService)
},
created(){
this.myService.getName(10086);
}

作者:非思不可
链接:https://juejin.cn/post/7041930275458285582

收起阅读 »