技术选型,Vue和React的对比
1. MVVM和MVC
Vue是MVVM,React是MVC。
MVVM(Model-View-ViewModel)是在MVC(Model View Controller)的基础上,VM抽离Controller中展示的业务逻辑,而不是替代Controller,其它视图操作业务等还是应该放在Controller中实现。
也就是说MVVM实现的是业务逻辑组件的重用,使开发更高效,结构更清晰,增加代码的复用性。
可以理解为MVVM是MVC的升级版。
虽然React不算一个完整的MVC框架,可以认为是MVC中的V(View),但是Vue的MVVM还是更面向未来一些。
2. 数据绑定
vue是双向绑定,react是单向绑定。
单向绑定的优点是相应的可以带来单向数据流,这样做的好处是所有状态变化都可以被记录、跟踪,状态变化通过手动调用通知,源头易追溯,没有“暗箱操作”。同时组件数据只有唯一的入口和出口,使得程序更直观更容易理解,有利于项目的可维护性。
但是Vue虽然是双向绑定,但是也是单向数据流,它的双向绑定只是一个语法糖,想看正经的双向绑定可以去看下Dva。
单向绑定的缺点则是代码量会相应的上升,数据的流转过程变长,从而出现很多类似的重复代码。同时由于对应用状态独立管理的严格要求(单一的全局store),在处理局部状态较多的场景时(如用户输入交互较多的“富表单型”应用),会显得冗余。
双向绑定可以在表单交互较多的场景下,会简化大量业务无关的代码。
我认为Vue的设计方案好一些,全局性数据流使用单向,局部性数据流使用双向。
3. 数据更新
3.1 React 更新流程
React 推崇 Immutable(不可变),通过重新render去发现和更新自身。
3.2 Vue 更新流程
Vue通过收集数据依赖去发现更新。
Vue很吸引人的就是它的响应式更新,Vue首次渲染触发data的getter,从而触发依赖收集,为对应的数据创建watcher,当数据发生更改的时候,setter被触发,然后通知各个watcher在下个tick的时候更新数据。
所以说,如果data中某些数据没有在模板template 中使用的话,更新这些数据的时候,是不会触发更新的。这样的设计非常好,没有在模版上用到的变量,当它的值发生变化时,不更新视图,相当于内置了React的shouldComponentUpdate。
3.3 更新比较
获取数据更新的手段和更新的粒度不一样
Vue通过依赖收集,当数据更新时 ,Vue明确知道是哪些数据更新了,每个组件都有自己的渲渲染watcher,掌管当前组件的视图更新,所以可以精确地更新对应的组件,所以更新的粒度是组件级别的。
React会递归地把所有的子组件重新render一下,不管是不是更新的数据,此时,都是新的。然后通过 diff 算法 来决定更新哪部分的视图。所以,React 的更新粒度是一个整体。
对更新数据是否需要渲染页面的处理不一样
只有依赖收集的数据发生更新,Vue 才会去重新渲染页面
只要数据有更新(setState,useState 等手段触发更新),都会去重新渲染页面 (可以使用shouldComponentUpdate/ PureComponent 改善)
Vue的文档里有一描述说,Vue是细粒度数据响应机制,所以说数据更新这一块,我认为Vue的设计方案好一些。
4. 性能对比
借用尤大大的一段话:
模板在性能这块吊打 tsx,在 IDE 支持抹平了的前提下用 tsx 本质上是在为了开发者的偏好牺牲用户体验的性能(性能没遇到瓶颈就无所谓) 这边自己不维护框架的人吐槽吐槽我也能理解,毕竟作为使用者只需要考虑自己爽不爽。作为维护者,Vue 的已有的用户习惯、生态和历史包袱摆在那里,能激进的程度是有限的,Vue 3 的大部分设计都是戴着镣铐跳舞,需要做很多折衷。如果真要激进还不如开个新项目,或者没人用的玩票项目,想怎么设计都可以。 组件泛型的问题也有不少人提出了,这个目前确实不行,但不表示以后不会有。 最后实话实说,所有前端里面像这个问题下面的类型体操运动员们毕竟是少数,绝大部分有 intellisense + 类型校验就满足需求了。真的对类型特别特别较真的用 React 也没什么不好,无非就是性能差点。
为什么模板性能吊打TSX?
tsx和vue template其实都是一样的模版语言,tsx最终也会被编译成createElement,模板被编译成render函数,所以本质上两者都有compile-time和runtime,但tsx的特殊性在于它本身是在ts语义下的,过于灵活导致优化无从下手。但是vue的模板得益于自身本来就是DSL,有自己的文法和语义,所以vue在模板的compile-time做了巨多的优化,比如提升不变的vnode,以及blocktree配合patchflag靶向更新,这些优化在最终的runtime上会把性能拉开不少。
DSL: 一种为特定领域设计的,具有受限表达性的编程语言。
所以说Vue的性能是优于React的。
5. React Hooks和Vue Hooks
其实 React Hook 的限制非常多,比如官方文档中就专门有一个章节介绍它的限制:
不要在循环,条件或嵌套函数中调用 Hook
确保总是在你的 React 函数的最顶层调用他们。
遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。
而 Vue 带来的不同在于:
与 React Hooks 相同级别的逻辑组合功能,但有一些重要的区别。 与 React Hook 不同,setup函数仅被调用一次,这在性能上比较占优。
对调用顺序没什么要求,每次渲染中不会反复调用 Hook 函数,产生的的 GC 压力较小。
不必考虑几乎总是需要 useCallback 的问题,以防止传递函数prop给子组件的引用变化,导致无必要的重新渲染。
React Hook 有臭名昭著的闭包陷阱问题,如果用户忘记传递正确的依赖项数组,useEffect 和 useMemo 可能会捕获过时的变量,这不受此问题的影响。 Vue 的自动依赖关系跟踪确保观察者和计算值始终正确无误。
不得不提一句,React Hook 里的「依赖」是需要你去手动声明的,而且官方提供了一个 eslint 插件,这个插件虽然大部分时候挺有用的,但是有时候也特别烦人,需要你手动加一行丑陋的注释去关闭它。
我们认可 React Hooks 的创造力,这也是 Vue-Composition-Api 的主要灵感来源。上面提到的问题确实存在于 React Hook 的设计中,我们注意到 Vue 的响应式模型恰好完美的解决了这些问题。
--- 来自ssh
Vue的组合式API刚出来的时候确实一看好像React Hooks,我也对它的.value进行了吐槽,
但是总体来说还是更偏向于Vue Hooks。
6. 写法
React的思路是all in js,通过js来生成html,所以设计了jsx,还有通过js来操作css,社区的styled-component、jss等,所以说React的写法感觉相对自由一些,逻辑正确老子想怎么写怎么写,对于我来说,我确实更偏向于React的写法。
Vue则是把html,css,js组合到一起,就像 Web 开发多年的传统开发方式一样, vue-loader会解析文件,提取每个语言块用各自的处理方式,vue有单文件组件(SFC),可以把html、css、js写到一个文件中,html提供了模板引擎来处理。Vue感觉是给你搭了一个框架,告诉你什么地方该写什么东西,你只要按照他的要求向里面填内容就可以了,没有React那么自由,但是上手难度简单了许多。而且因为SFC,一个组件的代码会看起来很长,维护起来很头痛。
7. 理念及设计
Vue 和 React 的核心差异,以及核心差异对后续设计产生的“不可逆”影响。
Vue 和 React 在 API 设计风格和哲学理念(甚至作者个人魅力)上的不同。
Vue 和 React 在工程化预编译构建阶段,AOT 和 JIT 优化的本质差异和设计。
这个层次的比较确实对我难度确实大,我也懒得去copy,下面是Lucas大佬的分析,可以去看一下,时空隧道。
作者:黑色的枫
来源:https://juejin.cn/post/7037365650251055134
微前端-从了解到动手搭建
前言
微前端是 2016 年thoughtWorks提出的概念,它将微服务的理念应用于浏览器端,即将前端应用由单体应用转变成多个小型前端应用聚合的应用。各个小型前端应用可以独立运行、独立开发、独立部署。
为什么出现?
与微服务出现的原因相似,随着前端业务越来越复杂,前端的代码和业务逻辑也愈发难以维护,尤其对于中后台系统,很容易出现巨石应用,微前端由此应运而生,其根本目的就是解决巨石应用的项目复杂,系统庞大,开发人员众多,难以维护的问题。
微前端 vs 巨石应用
微前端 | 巨石应用 | |
---|---|---|
可维护性 | 拆分为框架应用、微应用、微模块后,每个业务页面都对应一个单独的仓库,应用风险性降低。 | 所有页面都在一个仓库,经常会出现动一处则动全身,随着系统增大维护成本会逐渐升高。 |
开发效率 | 结合发布、回滚、团队协作三个方面来看,单个仓库只关心一个业务页面,可以更方便快速迭代。 | 团队多人协作时,发布排队;回滚有可能会把其他人发布的代码同时回滚掉;多分支开发时发布前沟通增加成本。 |
代码复用 | 所有页面都分开维护,使用公用代码成本较大,不过共用代码抽离为npm包使用可以减小成本。 | 一个仓库中很容易抽离公用的部分,但是要注意动一处就会动全身的结果。 |
架构方案
基座模式是当前比较常见的微前端架构设计。
首先以容器应用作为整个项目的主应用,负责子应用的注册,聚合,提供子运行环境、管理生命周期等。子应用就是各个独立部署、独立开发的单元。
应用注册表拥有每个应用及对应的入口。在前端领域里,入口的直接表现形式可以是路由,又或者对应的应用映射。
目前可以实现微前端架构的方案有如下:
HTTP后端路由转发(nginx)
✅ 简单高效快速,同时不需要前端做额外的工作。
❌ 体验并不好,相当于mpa页面,路由到每个应用需要重新刷新
iframe
✅ 前端最简单的应用方式,直接嵌入,门槛最低,改动最小
❌ iframe都会遇到的一些典型问题:UI 不同步,DOM 结构不共享(比如iframe中的弹框),跨域通信等
各个业务独立打到npm包中
✅ 门槛低,易上手
❌ 模块修改后需要重新部署发布,太麻烦。
组合式应用路由分发(基座模式)
✅ 纯前端改造,体验良好,各个业务相互独立
❌ 需要设计和开发,有一定成本,同时需要兼顾子页面和基座的变量污染,样式互相影响等问题
web component
✅ 是一项标准,目前它包含三项主要技术,它们可以一起使用来创建封装功能的定制元素,可以在你喜欢的任何地方重用,不必担心代码冲突。应该是微前端的最终态
❌ 比较新,兼容性较差
微前端页面形态
微前端基座框架需要解决的问题
路由分发
作为微前端的基座应用,是整个应用的入口,负责承载当前子应用的展示和对其他路由子应用的转发,对于当前子应用的展示,一般是由以下几步构成:
远程拉取子应用内容
将子应用的 js 和 css 抽离,采用eval来运行 js,并将 css 和 html 内容append到基座应用中留给子应用的展示区域
当子应用切换走时,同步卸载这些内容
对于路由分发而言,以采用react-router开发的基座SPA应用来举例,主要是下面这个流程:
当浏览器的路径变化后,react-router会监听hashchange或者popstate事件,从而获取到路由切换的时机。
最先接收到这个变化的是基座的router,通过查询注册信息可以获取到转发到那个子应用,经过一些逻辑处理后,采用修改hash方法或者pushState方法来路由信息推送给子应用的路由,子应用可以是手动监听hashchange或者popstate事件接收,或者采用react-router接管路由,后面的逻辑就由子应用自己控制。
应用隔离
应用隔离问题主要分为主应用和子应用,子应用和子应用之间的JavaScript执行环境隔离,CSS样式隔离,
CSS
当主应用和子应用同屏渲染时,就可能会有一些样式会相互污染,如果要彻底隔离CSS污染,可以采用CSS Module 或者命名空间的方式,给每个子应用模块以特定前缀,即可保证不会互相干扰,可以采用webpack的postcss插件,在打包时添加特定的前缀。
而对于子应用与子应用之间的CSS隔离就非常简单,在每次应用加载时,将该应用所有的link和style 内容进行标记。在应用卸载后,同步卸载页面上对应的link和style即可。
JavaScript隔离
每当子应用的JavaScript被加载并运行时,它的核心实际上是对全局对象Window的修改以及一些全局事件的改变,例如jQuery这个js运行后,会在Window上挂载一个window.$对象,对于其他库React,Vue也不例外。为此,需要在加载和卸载每个子应用的同时,尽可能消除这种冲突和影响,最普遍的做法是采用沙箱机制(SandBox)。
沙箱机制的核心是让局部的JavaScript运行时,对外部对象的访问和修改处在可控的范围内,即无论内部怎么运行,都不会影响外部的对象,需要结合 with 关键字和window.Proxy对象来实现浏览器端的沙箱。
消息通信
应用间通信有很多种方式,当然,要让多个分离的子应用之间要做到通信,本质上仍离不开中间媒介或者说全局对象。所以对于消息订阅(pub/sub)模式的通信机制是非常适用的,在基座应用中会定义事件中心Event,每个子应用分别来注册事件,当被触发事件时再有事件中心统一分发,这就构成了基本的通信机制。
当然,如果基座和子应用采用的是React或者是Vue,是可以结合Redux和Vuex来一起使用,实现应用之间的通信。
搭一个看看?
qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。也是支付宝内部广泛使用的微前端框架。
那么我们就使用 qiankun 从头搭一个demo出来体验一下
基座
基座我们使用react,自行使用 create-react-app 创建一个react项目即可。
npm install qiankun -s
在基座中需要调用 registerMicroApps 注册子应用,然后调用start启动
因此在 index.js 中插入如下代码
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'vueApp',
entry: '//localhost:8080',
container: '#container',
activeRule: '/app-vue',
},
]);
// 启动 qiankun
start();
修改App.js
加入一些 antd 元素,让demo像样一些
同时,由于qiankun根据路由来加载不同微应用,我们也安装 react-router-dom
npm install react-router-dom
安装完之后修改 App.js 如下:
import { useState } from 'react';
import { Layout, Menu } from 'antd';
import { PieChartOutlined } from '@ant-design/icons';
import { Link } from 'react-router-dom'
import './App.css';
const { Header, Content, Footer, Sider } = Layout;
const App = () => {
const [collapsed, setCollapsed] = useState(false);
const onCollapse = collapsed => {
setCollapsed(collapsed);
};
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider collapsible collapsed={collapsed} onCollapse={onCollapse}>
<div className="logo" />
<Menu theme="dark" defaultSelectedKeys={['1']} mode="inline">
<Menu.Item key="1" icon={<PieChartOutlined />}>
<Link to="/app-vue">Vue应用</Link>
</Menu.Item>
</Menu>
</Sider>
<Layout className="site-layout">
<Header className="site-layout-background" style={{ padding: 0 }} />
<Content style={{ margin: '16px' }}>
<div id="container" className="site-layout-background" style={{ minHeight: 360 }}></div>
</Content>
<Footer style={{ textAlign: 'center' }}>This Project ©2021 Created by DiDi</Footer>
</Layout>
</Layout>
);
}
export default App;
记得修改 index.js,把 App 组件用 react-router-dom 的 BrowserRouter 包一层,让 BrowserRouter 作为顶层组件才可以跳转
至此,基座搭好了
子页面
尝试使用vue作为子页面,来体现微前端的技术隔离性。
使用vue-cli创建vue2.x项目
修改main.js如下:
import Vue from "vue/dist/vue.js";
import App from "./App.vue";
import router from "./router";
Vue.config.productionTip = false;
// window.__POWERED_BY_QIANKUN__ 为true 说明在 qiankun 架构中
// 修改webpack的publicPath,将子应用资源加载的公共基础路径设为 qiankun 包装后的路径
// 这个 __INJECTED_PUBLIC_PATH_BY_QIANKUN__ 的实际地址是子应用的服务器地址,子应用的应用资源都在他本身的实际服务器上
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
let instance = null;
function render(props = {}) {
const { container } = props;
instance = new Vue({
router,
render: (h) => h(App),
}).$mount(container ? container.querySelector("#app") : "#app");
}
// 独立运行时 直接渲染
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
// 应用需要导出 bootstrap、mount、unmount 三个生命周期钩子,以供主应用在适当的时机调用。
export async function bootstrap() {
console.log("[vue] vue app bootstraped");
}
export async function mount(props) {
console.log("[vue] props from main framework", props);
render(props);
}
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = "";
instance = null;
}
router.js配置如下:
import Vue from "vue/dist/vue.js";
import VueRouter from "vue-router";
Vue.use(VueRouter);
const routes = [
{
path: "/test",
name: "Test",
component: () => import("./components/Test.vue"),
},
{
path: "/hello",
name: "Hello",
component: () => import("./components/Hello.vue"),
},
];
const router = new VueRouter({
base: window.__POWERED_BY_QIANKUN__ ? "/app-vue/" : "/",
mode: "history",
routes,
});
export default router;
根目录下新建vue.config.js 用来配置webpack,内容如下:
const { name } = require("./package");
module.exports = {
devServer: {
// 跨域
headers: {
"Access-Control-Allow-Origin": "*",
},
},
configureWebpack: {
output: {
library: `${name}-[name]`,
// 把微应用打包成 umd 库格式
libraryTarget: "umd",
jsonpFunction: `webpackJsonp_${name}`,
},
},
};
启动
基座和子应用分别启动,可以看到,子应用已经加载到了主应用中:
作者:visa
来源:https://juejin.cn/post/7037386845751083021
实现一个逐步递增的数字动画
背景
可视化大屏项目使用最多的组件就是数字组件,展示数据的一个变化,为了提高视觉效果,需要给数字增加一个滚动效果,实现一个数字到另一个数字逐步递增的滚动动画。
先上一个思维导图:
一、实现类似滚轮的效果,容器固定,数字向上滚动
先列举所有的可能的值形成一个纵向的列表,然后固定一个容器,匀速更改数字的偏移值。
下面来介绍一下这种方案的实现,元素值从0到9一共十个值,每个数字占纵向列表的10%,所以纵向偏移值依次为为0% —> -90%
实现:
<ul>
<li>
<span>0123456789</span>
</li>
</ul>
ul{
margin-top: 200px;
}
ul li{
margin:0 auto;
width: 20px;
height: 30px;
text-align: center;
border:2px solid rgba(221,221,221,1);
border-radius:4px;
}
ul li span{
position: absolute;
color: #fff;
top: 30%;
left: 50%;
transform: translate(-50%,0);
transition: transform 500ms ease-in-out;
writing-mode: vertical-rl;
text-orientation: upright;
letter-spacing: 17px;
}
let spanDom = document.querySelector('span')
let start = 0
setInterval(() =>{
start++
if(start>9){
start = 0
}
spanDom.style.transform = `translate(-50%,-${start*10}%)`
}, 1000)
上述代码存在一个问题,当我们从9到0的时候,容器偏移从-90%直接到了0%。 但是由于设定了固定的过渡动画时间,就会出现一个向反方向滚动的情况,为了解决这个问题,可以参考无缝滚动的思路
在9后面复制一份0,
当纵向列表滚动到9的时候,继续滚动到复制的0
滚动到复制的0的时候,把列表的偏移位置改为0,并且控制动画时间为0
<ul>
<li>
<span>01234567890</span>
</li>
</ul>
let spanDom = document.querySelector('span')
let start = 0
var timer = setInterval(fn, 1000);
function fn() {
start++
clearInterval(timer)
timer = setInterval(fn,start >10 ? 0 : 1000);
if(start>10){
spanDom.style.transition = `none`
start = 0
}else{
spanDom.style.transition = `transform 500ms ease-in-out`
}
spanDom.style.transform = `translate(-50%,-${start/11*100}%)`
}
利用两个元素实现滚动
仔细看动图的效果,事实上在在视口只有两个元素,一个值之前的值,一个为当前的值,滚动偏移值只需设置translateY(-100%)
具体思路:
声明两个变量,分别存放之前的值
prev
,以及变化后的值cur
;声明一个变量play
作为这两个值的滚动动画的开关
使用
useEffect
监听监听传入的值:如果是有效的数字,那么把没有变化前的值赋值给prev
,把当前传入的值赋值给cur
,并且设置paly
为true
开启滚动动画
下面是调整后的代码结构:
<div className={styles.slider}>
{[prev, cur].map((item, index) => (
<span key={index} className={`${styles['slider-text']} ${playing && styles['slider-ani']} ${(prev === 0 && cur === 0 && index ===0) && styles['slider-hide']}`}>
{item}
</span>
))}
</div>
const { value} = props
const [prev, setPrev] = useState(0)
const [cur, setCur] = useState(0)
const [playing, setPlaying] = useState(false)
const play = (pre, current) => {
setPrev(pre)
setCur(current)
setPlaying(false)
setTimeout(() => {
setPlaying(true)
}, 20)
}
useEffect(() => {
if (!Number.isNaN(value)) {
play(cur, value)
} else {
setPrev(value)
setCur(value)
}
}, [value])
.slider {
display: flex;
flex-direction: column;
height: 36px;
margin-top: 24%;
overflow: hidden;
text-align: left;
}
.slider-text {
display: block;
height: 100%;
transform: translateY(0%);
}
.slider-ani {
transform: translateY(-100%);
transition: transform 1s ease;
}
.slider-hide {
opacity: 0;
}
实现多个滚轮的向上滚动的数字组件
利用H5的requestAnimationFrame()API实现数字逐步递增的动画效果
实现一个数字的逐渐递增的滚动动画,并且要在指定时间内完成。要看到流畅的动画效果,就需要在更新元素状态时以一定的频率进行,JS动画都是通过在很短的时间内不停的渲染/绘制元素做到的,所以计时器一直都是Javascript动画的核心技术,关键就是刷新的间隔时间,刷新时间需要尽量短,这样动画效果才能显得更加流畅,不卡顿;同时刷新间隔又不能太短,需要确保浏览器有能力渲染动画 。
大多数电脑显示器的刷新频率是 60Hz,即每秒重绘 60次。因此平滑动画的最佳循环间隔是通常是 1000ms/60,约等于16.6ms
计时器对比
与 setTimeout 和 setInterval 不同,requestAnimationFrame 不需要程序员自己设置时间间隔。setTimeout 和 setInterval 的问题是精确度低。它们的内在运行机制决定了时间间隔参数实际上只是指定了把动画代码添加到浏览器 UI 线程队列中以等待执行的时间。如果队列前面已经加入了其他任务,那动画代码就要等前面的任务完成后再执行。
requestAnimationFrame 采用系统时间间隔,它会要求浏览器根据自己的频率进行一次重绘,保持最佳绘制效率,不会因为间隔时间过短,造成过度绘制,增加开销;也不会因为间隔时间太长,使用动画卡顿不流畅,让各种网页动画效果能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。
requestAnimationFrame 会把每一帧中的所有 DOM 操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率。
requestAnimationFrame 对于隐藏或不可见元素,将不会进行重绘或回流,就意味着使用更少的 CPU、GPU 和内存使用量。
requestAnimationFrame 是由浏览器专门为动画提供的API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了CPU开销。
requestAnimationFrame实现滚动动画思路
- 动画开始,记录开始动画的时间
startTimeRef.current
const startTimeRef = useRef(Date.now());
const [t, setT] = useState(Date.now());
- 之后每一帧动画,记录从开始动画经过了多长时间,计算出当前帧的所到达的数字应该是多少,即
currentValue
useEffect(() => {
const rafFunc = () => {
const now = Date.now();
const t = now - startTimeRef.current;
if (t >= period) {
setT(period);
} else {
setT(t);
requestAnimationFrame(rafFunc);
}
};
let raf;
if (autoScroll) {
raf = requestAnimationFrame(rafFunc);
startTimeRef.current = Date.now();
} else {
raf && cancelAnimationFrame(raf);
}
return () => raf && cancelAnimationFrame(raf);
}, [period, autoScroll]);
const currentValue = useMemo(() => ((to - from) / period) * t + from, [t, period, from, to]);
- 针对当前每个数字位上的数字进行比较,如果有变化,进行偏移量的变化,偏移量体现在当前数字位上的数字与下一位数字之间的差值,这个变化每一帧都串起来形成了滚动动画
成果展示
作者:我就是胖虎
链接:https://juejin.cn/post/7025913017627836452
收起阅读 »
前端金额格式化处理
前端项目中,金额格式化展示是很常见的需求,在此整理了一些通用的处理方式,如 toLocaleString();正则匹配;slice()循环截取等等;也解决了小数点精度问题
以此为例:12341234.246
=> ¥ 12,341,234.25
方式一:采用浏览器自带的Number.prototype.toLocaleString()处理整数部分,小数部分直接用Number.prototype.toFixed()四舍五入处理
// v1.0
const formatMoney = (money, symbol = "", decimals = 2) => {
if (!(money && money > 0)) {
return 0.0;
}
let arr = money.toFixed(decimals).toString().split(".");
let first = parseInt(arr[0]).toLocaleString();
let result = [first, arr[1]].join(".");
return `${symbol} ${money.toFixed(decimals)}`;
};
formatMoney(12341234.246); // 12,341,234.25
formatMoney(12341234.246, "¥", 1); // ¥ 12,341,234.2
2021.11.9 更改记录 我之前写复杂了,经过评论区[黄景圣]的指点,优化如下:
// v2.0 简化函数
const formatMoney = (money, symbol = "", decimals = 2) =>
`${symbol} ${parseFloat(money.toFixed(decimals)).toLocaleString()}`;
formatMoney(12341234.246, "¥", 2) // ¥ 12,341,234.25
// 或者只用toLocaleString()处理
const format = (money, decimals = 2) =>
money.toLocaleString("zh", {
style: "currency",
currency: "CNY",
maximumFractionDigits: decimals,
useGrouping: true, // false-没有千位分隔符;true-有千位分隔符
});
format(12341234.246); // ¥12,341,234.25
2021.11.10 更改记录 经过评论区[你摸摸我这料子]的提示,解决了 toFixed() 精度失效的问题,具体可查看前端小数展示精度处理
// 测试数据如下:
formatMoney(12.035); // 12.04 正常四舍五入
formatMoney(12.045); // 12.04 异常,应该为12.05,没有四舍五入
// v3.0 解决toFixed()问题
const formatToFixed = (money, decimals = 2) => {
return (
Math.round((parseFloat(money) + Number.EPSILON) * Math.pow(10, decimals)) /
Math.pow(10, decimals)
).toFixed(decimals);
};
const formatMoney = (money, symbol = "", decimals = 2) =>
`${symbol}${parseFloat(formatToFixed(money, decimals)).toLocaleString()}`;
formatMoney(12341234.035, '¥'); // ¥12,341,234.04
formatMoney(12341234.045, '¥'); // ¥12,341,234.05
2021.11.17 更改记录 通过评论区[Ryan_zhang]的提醒,解决了保留四位小数显示的问题
// v4.0 只更改了formatMoney函数,其他的不变
const formatMoney = (money, symbol = "", decimals = 2) =>
`${symbol}${parseFloat(formatToFixed(money, decimals)).toLocaleString(
"zh",
{
maximumFractionDigits: decimals,
useGrouping: true,
}
)}`;
formatMoney(12341234.12335, "¥", 4); // ¥12,341,234.1234
formatMoney(12341234.12345, "¥", 4); // ¥12,341,234.1235
方式二:使用正则表达式处理整数部分,小数部分同上所示。有个《JS 正则迷你书》介绍正则表达式挺好的,在 2.4.2 章就讲了“数字的千位分隔符表示法”,介绍的很详细,推荐看看。
\b
:单词边界,具体就是 \w 与 \W 之间的位置,也包括 \w 与 ^ 之间的位置,和 \w 与 $ 之间的位置\B
:\b 的反面的意思,非单词边界(?=p)
:其中 p 是一个子模式,即 p 前面的位置,或者说,该位置后面的字符要匹配 p
/**
* @params {Number} money 金额
* @params {Number} decimals 保留小数点后位数
* @params {String} symbol 前置符号
*/
const formatMoney = (money, symbol = "", decimals = 2) => {
let result = money
.toFixed(decimals)
.replace(/\B(?=(\d{3})+\b)/g, ",")
.replace(/^/, `${symbol}`);
return result;
};
formatMoney(12341234.246, "$", 2); // $12,341,234.25
// v2.0 解决toFixed()问题
const formatMoneyNew = (money, symbol = "", decimals = 2) =>
formatToFixed(money, decimals)
.replace(/\B(?=(\d{3})+\b)/g, ",")
.replace(/^/, `${symbol}`);
formatMoneyNew(12341234.035, "¥", 2); // ¥12,341,234.04
formatMoneyNew(12341234.045, "¥", 2); // ¥12,341,234.05
方式三:循环字符串,通过 slice 截取实现
- substring(start, end):包含 start,不包含 end
- substr(start, length):包含 start,长度为 length
- slice(start, end):可操作数组和字符串;包含 start,不包含 end
- splice(start, length, items):只能针对数组;增删改都可以
const formatMoney = (money, symbol = "", decimals = 2) => {
// 改造前
// let arr = money.toFixed(decimals).toString().split(".");
// 改造后
let arr = formatToFixed(money, decimals).toString().split(".");
let num = arr[0];
let first = "";
su;
while (num.length > 3) {
first = "," + num.slice(-3) + first;
num = num.slice(0, num.length - 3);
}
if (num) {
first = num + first;
}
return `${symbol} ${[first, arr[1]].join(".")}`;
};
formatMoney(12341234.246, "$", 2); // $ 12,341,234.25
formatMoney(12341234.035, "¥", 2); // ¥ 12,341,234.04
formatMoney(12341234.045, "¥", 2); // ¥ 12,341,234.05
2021.11.24 更改记录 通过评论区[SpriteBoy]和[maxxx]的提醒,采用Intl内置的NumberFormat试试
方式四:Intl.NumberFormat,用法和toLocaleString()挺相似的
const formatMoney = (money, decimals = 2) => {
return new Intl.NumberFormat("zh-CN", {
style: "currency", // 货币形式
currency: "CNY", // "CNY"是人民币
currencyDisplay: "symbol", // 默认“symbol”,中文中代表“¥”符号
// useGrouping: true, // 是否使用分组分隔符,如千位分隔符或千/万/亿分隔符,默认为true
// minimumIntegerDigits: 1, // 使用的整数数字的最小数目.可能的值是从1到21,默认值是1
// minimumFractionDigits: 2, // 使用的小数位数的最小数目.可能的值是从 0 到 20
maximumFractionDigits: decimals, // 使用的小数位数的最大数目。可能的值是从 0 到 20
}).format(money);
};
console.log(formatMoney(12341234.2, 2)); // ¥12,341,234.20
console.log(formatMoney(12341234.246, 1)); // ¥12,341,234.2
console.log(formatMoney(12341234.035, 2)); // ¥12,341,234.04
console.log(formatMoney(12341234.045, 2)); // ¥12,341,234.05
console.log(formatMoney(12341234.12335, 4)); // ¥12,341,234.1234
console.log(formatMoney(12341234.12345, 4)); // ¥12,341,234.1235
作者:时光足迹
链接:https://juejin.cn/post/7028086399601475591
收起阅读 »
清空数组的几个方式
1. 前言
前两天在工作当中遇到一个问题,在vue3
中使用reactive
生成的响应式数组如何清空,当然我一般清空都是这么写:
let array = [1,2,3];
array = [];
不过这么用在reactive
代理的方式中还是有点问题,比如这样:
let array = reactive([1,2,3]);
watch(()=>[...array],()=>{
console.log(array);
},)
array = reactive([]);
很显然,因为丢失了对原来响应式
对象的引用,这样就直接失去了监听
。
2. 清空数据的几种方式
当然,作为一名十年代码经验常年摸鱼的我,立马就给出了几个解决方案。
2.1 使用ref()
使用ref
,这是最简便的方法:
const array = ref([1,2,3]);
watch(array,()=>{
console.log(array.value);
},)
array.value = [];
2.2 使用slice
slice
顾名思义,就是对数组进行切片
,然后返回一个新数组
,感觉和go
语言的切片
有点类似。当然用过react
的小伙伴应该经常用slice
,清空一个数组只需要这样写:
const array = ref([1,2,3]);
watch(array,()=>{
console.log(array.value);
},)
array.value = array.value.slice(0,0);
不过需要注意要使用ref
。
2.3 length赋值为0
个人比较喜欢这种,直接将length
赋值为0
:
const array = ref([1,2,3]);
watch(array,()=>{
console.log(array.value);
},{
deep:true
})
array.value.length = 0;
而且,这种只会触发一次,但是需要注意watch
要开启deep
:
不过,这种方式,使用reactive
会更加方便,也不用开启deep
:
const array = reactive([1,2,3]);
watch(()=>[...array],()=>{
console.log(array);
})
array.length = 0;
2.4 使用splice
副作用
函数splice
也是一种方案,这种情况同时也可以使用reactive
:
const array = reactive([1,2,3]);
watch(()=>[...array],()=>{
console.log(array);
},)
array.splice(0,array.length)
不过要注意,watch
会触发多次:
当然也可以使用ref
,但是注意这种情况下,需要开启deep
:
const array = ref([1,2,3]);
watch(array,()=>{
console.log(array.value);
},{
deep:true
})
array.value.splice(0,array.value.length)
但是可以看到ref
也和reactive
一样,会触发多次。
3. 总结
以上是我个人工作中
的对于清空数组
的总结,但是可以看到splice
还是有点特殊的,会触发多次,不过为什么会产生这种差异还有待研究。
作者:RadiumAg
链接:https://juejin.cn/post/7028086044285206564
收起阅读 »
手写一个 ts-node 来深入理解它的原理
当我们用 Typesript 来写 Node.js 的代码,写完代码之后要用 tsc 作编译,之后再用 Node.js 来跑,这样比较麻烦,所以我们会用 ts-node 来直接跑 ts 代码,省去了编译阶段。
有没有觉得很神奇,ts-node 怎么做到的直接跑 ts 代码的?
其实原理并不难,今天我们来实现一个 ts-node 吧。
相关基础
实现 ts-node 需要 3 方面的基础知识:
- require hook
- repl 模块、vm 模块
- ts compiler api
我们先学下这些基础
require hook
Node.js 当 require 一个 js 模块的时候,内部会分别调用 Module.load、 Module._extensions[‘.js’],Module._compile 这三个方法,然后才是执行。
同理,ts 模块、json 模块等也是一样的流程,那么我们只需要修改 Module._extensions[扩展名] 的方法,就能达到 hook 的目的:
require.extensions['.ts'] = function(module, filename) {
// 修改代码
module._compile(修改后的代码, filename);
}
比如上面我们注册了 ts 的处理函数,这样当处理 ts 模块时就会调用这个方法,所以我们在这里面做编译就可以了,这就是 ts-node 能够直接执行 ts 的原理。
repl 模块
Node.js 提供了 repl 模块可以创建 Read、Evaluate、Print、Loop 的命令行交互环境,就是那种一问一答的方式。ts-node 也支持 repl 的模式,可以直接写 ts 代码然后执行,原理就是基于 repl 模块做的扩展。
repl 的 api 是这样的: 通过 start 方法来创建一个 repl 的交互,可以指定提示符 prompt,可以自己实现 eval 的处理逻辑:
const repl = require('repl');
const r = repl.start({
prompt: '- . - > ',
eval: myEval
});
function myEval(cmd, context, filename, callback) {
// 对输入的命令做处理
callback(null, 处理后的内容);
}
repl 的执行时有一个上下文的,在这里就是 r.context,我们在这个上下文里执行代码要使用 vm 模块:
const vm = require('vm');
const res = vm.runInContext(要执行的代码, r.context);
这两个模块结合,就可以实现一问一答的命令行交互,而且 ts 的编译也可以放在 eval 的时候做,这样就实现了直接执行 ts 代码。
ts compiler api
ts 的编译我们主要是使用 tsc 的命令行工具,但其实它同样也提供了编译的 api,叫做 ts compiler api。我们做工具的时候就需要直接调用 compiler api 来做编译。
转换 ts 代码为 js 代码的 api 是这个:
const { outputText } = ts.transpileModule(ts代码, {
compilerOptions: {
strict: false,
sourceMap: false,
// 其他编译选项
}
});
当然,ts 也提供了类型检查的 api,因为参数比较多,我们后面一篇文章再做展开,这里只了解 transpileModule 的 api 就够了。
了解了 require hook、repl 和 vm、ts compiler api 这三方面的知识之后,ts-node 的实现原理就呼之欲出了,接下来我们就来实现一下。
实现 ts-node
直接执行的模式
我们可以使用 ts-node + 某个 ts 文件,来直接执行这个 ts 文件,它的原理就是修改了 require hook,也就是 Module._extensions['.ts']
来实现的。
在 require hook 里面做 ts 的编译,然后后面直接执行编译后的 js,这样就能达到直接执行 ts 文件的效果。
所以我们重写 Module._extensions['.ts']
方法,在里面读取文件内容,然后调用 ts.transpileModule 来把 ts 转成 js,之后调用 Module._compile 来处理编译后的 js。
这样,我们就可以直接执行 ts 模块了,具体的模块路径是通过命令行参数执行的,可以用 process.argv 来取。
const path = require('path');
const ts = require('typescript');
const fs = require('fs');
const filePath = process.argv[2];
require.extensions['.ts'] = function(module, filename) {
const fileFullPath = path.resolve(__dirname, filename);
const content = fs.readFileSync(fileFullPath, 'utf-8');
const { outputText } = ts.transpileModule(content, {
compilerOptions: require('./tsconfig.json')
});
module._compile(outputText, filename);
}
require(filePath);
我们准备一个这样的 ts 文件 test.ts:
const a = 1;
const b = 2;
function add(a: number, b: number): number {
return a + b;
}
console.log(add(a, b));
然后用这个工具 hook.js 来跑:
可以看到,成功的执行了 ts,这就是 ts-node 的原理。
当然,细节的逻辑还有很多,但是最主要的原理就是 require hook + ts compiler api。
repl 模式
ts-node 支持启动一个 repl 的环境,交互式的输入 ts 代码然后执行,它的原理就是基于 Node.js 提供的 repl 模块做的扩展,在自定义的 eval 函数里面做了 ts 的编译,然后使用 vm.runInContext 的 api 在 repl 的上下文中执行 js 代码。
我们也启动一个 repl 的环境,设置提示符和自定义的 eval 实现。
const repl = require('repl');
const r = repl.start({
prompt: '- . - > ',
eval: myEval
});
function myEval(cmd, context, filename, callback) {
}
eval 的实现就是编译 ts 代码为 js,然后用 vm.runInContext 来执行编译后的 js 代码,执行的 context 指定为 repl 的 context:
function myEval(cmd, context, filename, callback) {
const { outputText } = ts.transpileModule(cmd, {
compilerOptions: {
strict: false,
sourceMap: false
}
});
const res = vm.runInContext(outputText, r.context);
callback(null, res);
}
同时,我们还可以对 repl 的 context 做一些扩展,比如注入一个 who 的环境变量:
Object.defineProperty(r.context, 'who', {
configurable: false,
enumerable: true,
value: '神说要有光'
});
我们来测试下效果:
可以看到,执行后启动了一个 repl 环境,提示符修改成了 -.- >,可以直接执行 ts 代码,还可以访问全局变量 who。
这就是 ts-node 的 repl 模式的大概原理: repl + vm + ts compiler api。
全部代码如下:
const repl = require('repl');
const ts = require('typescript');
const vm = require('vm');
const r = repl.start({
prompt: '- . - > ',
eval: myEval
});
Object.defineProperty(r.context, 'who', {
configurable: false,
enumerable: true,
value: '神说要有光'
});
function myEval(cmd, context, filename, callback) {
const { outputText } = ts.transpileModule(cmd, {
compilerOptions: {
strict: false,
sourceMap: false
}
});
const res = vm.runInContext(outputText, r.context);
callback(null, res);
}
总结
ts-node 可以直接执行 ts 代码,不需要手动编译,为了深入理解它,我们我们实现了一个简易 ts-node,支持了直接执行和 repl 模式。
直接执行的原理是通过 require hook,也就是 Module._extensions[ext] 里通过 ts compiler api 对代码做转换,之后再执行,这样的效果就是可以直接执行 ts 代码。
repl 的原理是基于 Node.js 的 repl 模块做的扩展,可以定制提示符、上下文、eval 逻辑等,我们在 eval 里用 ts compiler api 做了编译,然后通过 vm.runInContext 在 repl 的 context 中执行编译后的 js。这样的效果就是可以在 repl 里直接执行 ts 代码。
当然,完整的 ts-node 还有很多细节,但是大概的原理我们已经懂了,而且还学到了 require hook、repl 和 vm 模块、 ts compiler api 等知识。
题外话
其实 ts-node 的原理是应一个同学的要求写的,大家有想读的 nodejs 工具的源码也可以告诉我呀(可以加我微信),无偿提供源码带读 + 简易实现的服务,不过会做一些筛选。
作者:zxg_神说要有光
来源:https://juejin.cn/post/7036688014206042143
为什么我不用 Typescript
前言
我算是久仰 Typescript 的大名了,因而之前就想学习,但是一直没有抽出时间来看看它。直到最近有一天我在知乎上被邀请回答了 一个问题 —— 一个我以为的中学生问怎么样提升他的开源仓库。我点进去,先是被惊艳到了;然后发现,他用的是 Typescript。我顿时感觉我似乎落后了,于是鼓起劲,开始学起了 Typescript。
但是我学了下,再用了下,发现它没有像被吹的那么神。虽说是 Javascript 的超集,也的确有些地方挺好的,但是还是不足够改变我使用 Javascript 编程。就像虽然有 Deno,但是我还是用 Node.js 一样。
所以,我就写这篇文章,说下我个人感觉 Typescript 的缺点、为何它的优点无法打动我用它替代 Javascript,以及跟推荐我使用 Typescript 的大家讲一下我不用 Typescript 的逻辑。
各位想骂我心里骂骂就好了,我今天过个生日也不容易。
缺陷
1. 语法丑陋,代码臃肿
我写两段相同的代码,大家感受下:
// js
const multiply = (i, j) => i * j
// ts
function multiply(i: number, j: number) {
return i + j
}
你看这类型注释,把好好的一段代码弄得这么乱……反正我看这样的 Typescript,花的反应时间一定比看上面的 Javascript 代码长。——不过也有可能是我比较熟悉 Javascript 吧。
复杂一点的东西也是一个道理(Apollo GraphQL 的代码):
// js
import React from 'react';
import ApolloClient from 'apollo-client';
let apolloContext;
export function getApolloContext() {
if (!apolloContext) {
apolloContext = React.createContext({});
}
return apolloContext;
}
export function resetApolloContext() {
apolloContext = React.createContext({});
}
铁定要比这个好得多:
// ts
import React from 'react';
import ApolloClient from 'apollo-client';
export interface ApolloContextValue {
client?: ApolloClient<object>;
renderPromises?: Record<any, any>;
}
let apolloContext: React.Context<ApolloContextValue>;
export function getApolloContext() {
if (!apolloContext) {
apolloContext = React.createContext<ApolloContextValue>({});
}
return apolloContext;
}
export function resetApolloContext() {
apolloContext = React.createContext<ApolloContextValue>({});
}
甚至有人提了个 issue 就是抱怨 Type 让它变得难用。
这么看,实在是为了这个假静态类型语言牺牲太多了,毕竟代码可读性还是很重要的。——之所以说它是假静态语言,是因为在真正的静态类型语言中,如 C 和 C++,不同的变量类型在内存中的存储方式不同,而在 Typescript 中不是这样。
比如,缺了这个可读性,debug 会变得更难。你是不是没有注意到我上面 multiply
的 Typescript 代码其实有 bug——应该是 *
而不是 +
。
2. 麻烦
浏览器不能直接执行 Typescript,所以 Typescript 必须要被编译成 Javascript 才能执行,要花一段时间;项目越大,花的时间越长,所以 Deno
才要停用它。并且,使用 Typescript 要安装新的依赖,虽然的确不麻烦,但是不用 Typescript,就不用再多装一个依赖了是不是。
其实还有一点,但是放不上台面来讲,因为这是我自己的问题。
我一直不大喜欢给添花样的东西,比如 pug
、typescript
、deno
等;虽然 scss
啥的我觉得还是不错的——没有它我就写不出 @knowscount/vue-lib 这个仓库。
3. 文件体积会变大
随随便便就能猜到,我写那么多额外的类型注释、代码变得那么臃肿肯定会让 Typescript 文件比用 Javascript 编写的文件更大。作为一个用 “tab 会让文件体积更小” 作为论据的 tab 党,我当然讨厌 Typescript 啦哈哈哈哈。
我理解在编译过后都是一样的,但是反正……我还是不爽。而且正是由于 TypeScript 会被编译到JavaScript 中,所以才会出现无论你的类型设计得多么仔细,还是有可能让不同的值类型潜入 JavaScript 变量中的问题。这是不可避免的,因为 JavaScript 仍然是没有类型的。
4. 报错使我老花
单纯吐槽一句,为什么它的报错那么丑,我就拼错了一个单词他给我又臭又长报一大段,还没有颜色。。
为何无法打动我
在讲为什么 Typescript 的优点无法打动我之前,我先来讲一讲 Typescript 有哪些优点吧:
- 大厂的产品
- 大厂在用
- 可以用未来的特性
- 降低出 bug 的可能性
- 面对对象编程(OOP)
对于它是微软的产品,我不能多说啥,毕竟我用 Visual Studio Code 用得很香;但是大厂在用这个论点,就不一样了。
有个逻辑谬误叫做「reductio ad absurdum」,也就是「归谬法」。什么意思呢:
大厂用 Typescript,所以我要用 Typescript。
大厂几百万改个 logo,我就借几百万改个 logo,因为大厂是大厂,肯定做得对。
这就很荒谬。
的确,大公司会采用 Typescript,必定有他的道理。但是,同样的论证思路也可以用于 Flow
、Angular
、Vue
、Ember
、jQuery
、Bootstrap
等等等等,几乎所有流行的库都是如此,那么它们一定都适合你吗?
关于它可以让你提前接触到未来的特性……大哥,babel
不香吗?
最后就是 OOP 以及降低出 bug 的可能性(Typesafe)。OOP 是 Typescript 的核心部分,而现在 OOP 已经不吃香了……例如 Ilya Suzdalnitski 就说过它是「万亿美元的灾难」。
至于为什么这么说,无非就两点——面向对象代码难以重构,也难以进行单元测试。重构时的抓狂不提,单元测试的重要性,大家都清楚吧。
而在 Javascript 这种非 OOP 语言里头,函数可以独立于对象存在。不用为了包含这些函数而去发明一些奇怪的概念真是一种解脱。
总之,TypeScript 的所谓优点(更好的错误处理、类型推理)都不是最佳方案。你还是得写测试,还是得好好命名你的函数和变量。个人觉得单单像 Typescript 一样添加一个接口或类型不能解决任何这些问题。
正好一千五百字。
作者:TurpinHero
链接:https://juejin.cn/post/6961012856573657095
收起阅读 »
我是如何把vue项目启动时间从70s优化到7秒的
可怕的启动时间
公司的产品是一个比较大的后台管理系统,而且使用的是webpack3的vue模板项目,单次项目启动时间达到了70s左右
启动个项目都够吃一碗豆腐脑了,可是没有豆腐脑怎么办,那就优化启动时间吧!
考虑到升级webpack版本的风险还是比较大的,出了一点问题都得找我,想想还是先别冒险,稳妥为主,所以我选择了通过插件来优化构建时间。
通过查阅资料,提升webpack的构建时间有以下几个方向:
- 多进程处理文件,同一时间处理多个文件
- 预编译资源模块,比如把长时间不变的库提取出来做预编译,构建的时候直接取编译结果就好
- 缓存,未修改的模块直接拿到处理结果,不必编译
- 减少构建搜索和处理的文件数量
针对以上几种优化方向,给出以下几种优化方案。
多进程构建
happypack
happypack 的作用就是将文件解析任务分解成多个子进程并发执行。
子进程处理完任务后再将结果发送给主进程。所以可以大大提升 Webpack 的项目构件速度。
查看happypack的github,发现作者已经不再维护该插件,并且作者推荐使用webpack官方的多进程插件thread-loader,所以我放弃了happypacy,选择了thread-loader。
thread-loader
thread-loader
是官方维护的多进程loader,功能类似于happypack,也是通过开启子任务来并行解析文件,从而提高构建速度。
把这个loader放在其他loader前面。不过该loader是有限制的。示例:
- loader无法发出文件。
- loader不能使用自定义加载器API。
- loader无法访问网页包选项。
每个worker都是一个单独的node.js进程,其开销约为600毫秒。还有进程间通信的开销。在小型项目中使用thread-loader
可能并不能优化项目的构建速度,反而会拖慢构建速度,所以使用该loader时需要明确项目构建构成中真正耗时的过程。
我的项目中我主要是用该loader用来解析vue和js文件,作用于vue-loader
和babel-loader
,如下代码:
const threadLoader = {
loader: 'thread-loader',
options: {
workers: require('os').cpus().length - 1,
}
}
module.exports = {
module:{
rules: [
{
test: /\.vue$/,
use: [
threadLoader, // vue-loader前使用该loader
{
loader: 'vue-loader',
options: vueLoaderConfig
}
],
},
{
test: /\.js$/,
use: [
threadLoader, // babel-loader前使用该loader
{
loader: 'babel-loader',
options: {
cacheDirectory: true
}
}
]
}
]
}
}
配置了thread-loader
后,重新构建试试,如下图所示,大概缩短了10秒的构建时间,还不错。
利用缓存提升二次构建的速度
虽然使用了多进程构建项目使构建时间缩短了10秒,但是一分钟的构建时间依然让人无法接受,这种挤牙膏似的优化方式多少让人有点不爽,有没有比较爽的方法来进一步缩短构建时间呢?
答案是有的,使用缓存。
缓存,不难理解就是第一次构建的时候将构建的结果缓存起来,当第二构建时,查看对应缓存是否修改,如果没有修改,直接使用缓存,由此,我们可以想象,当项目的变化不大时,大部分缓存都是可复用的,拿构建的速度岂不是会有质的飞跃。
cache-loader
说到缓存,当然百度一查,最先出现的就是cache-loader
,github搜索下官方文档,得到如下结果:
该loader会缓存其他loader的处理结果,把该loader放到其他loader的前面,同时该loader保存和读取缓存文件也会有开销,所以建议在开销较大的loader前使用该loader。
文档很简单,考虑到项目中的vue-loader
,babel-loader
,css-loader
会有比较大的开销,所以为这些loader加上缓存,那么接下来就把cache-loader
加到项目中吧:
const cacheLoader = {
loader: 'cache-loader'
}
const threadLoader = {
loader: 'thread-loader',
options: {
workers: require('os').cpus().length - 1,
}
}
module.exports = {
module:{
rules: [
{
test: /\.vue$/,
use: [
cacheLoader,
threadLoader, // vue-loader前使用该loader
{
loader: 'vue-loader',
options: vueLoaderConfig
}
],
},
{
test: /\.js$/,
use: [
cacheLoader,
threadLoader, // babel-loader前使用该loader
{
loader: 'babel-loader',
options: {
cacheDirectory: true
}
}
]
}
]
}
}
在util.js
文件中,该文件主要是生成css相关的webpack配置,找到generateLoaders
函数,修改如下:
const cacheLoader = {
loader: 'cache-loader'
}
function generateLoaders(loader, loaderOptions) {
// 在css-loader前增加cache-loader
const loaders = options.usePostCSS ? [cacheLoader, cssLoader, postcssLoader] : [cacheLoader, cssLoader]
if (loader) {
loaders.push({
loader: loader + '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
// Extract CSS when that option is specified
// (which is the case during production build)
if (options.extract) {
return ExtractTextPlugin.extract({
use: loaders,
fallback: 'vue-style-loader',
// 添加这句配置解决element-ui的图标路径问题
publicPath: '../../'
})
} else {
return ['vue-style-loader'].concat(loaders)
}
}
如上配置完成后,再次启动项目,可以发现,现在的启动时间没什么变化,然后我们二次启动项目,可以发现现在的启动时间来到了30s左右,前面我们已经说过了,cache-loader
缓存只有在二次启动的时候才会生效。
虽然项目启动时间优化了一半还多,但是我们的欲望是无限大的,30秒的时间离我们的预期还是有点差距的,继续优化!
hard-source-webpack-plugin
HardSourceWebpackPlugin
是一个webpack插件,为模块提供中间缓存步骤。为了查看结果,您需要使用此插件运行webpack两次:第一次构建将花费正常的时间。第二次建设将大大加快。
话不多说,直接配置到项目中:
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
module.exports = {
//...
plugins: [
new HardSourceWebpackPlugin()
]
}
二次构建时,我们会发现构建时间来到了个位数,只有短短的7秒钟。
在二次构建中,我发现了一个现象,构建的进度会从10% 一下跳到 80%,甚至是一瞬间就完成了中间构建过程。这正验证了该插件为模块提供中间缓存的说法。
为模块提供中间缓存,我的理解是cache-loader缓存的是对应loader的处理结果 ,而这个插件甚至可以缓存整个项目全部的处理结果,直接引用最终输出的缓存文件,从而大大提高构建速度。
其他优化方法
babel-loader开启缓存
babel-loader
自带缓存功能,开启cacheDirectory
配置项即可,官网的说法是,开启缓存会提高大约两倍的转换时间。
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [
...
{
loader: 'babel-loader',
options: {
cacheDirectory: true // 开启缓存
}
}
]
}
]
}
}
uglifyjs-webpack-plugin开启多进程压缩
uglifyjs-webpack-plugin
或是其他的代码压缩工具都提供了多进程压缩代码的功能,开启可加速代码压缩。
动态polyfill
建议查看该篇文章
总结
至此,我们完成了项目构建时间从70s到7s的优化过程,文中主要使用:

一步步的将项目优化到几乎立马启动,哎,看来这下摸鱼的时间又少了,加油干吧,打工人!
作者:进击的小超人
链接:https://juejin.cn/post/6979879230297341989
收起阅读 »
从零到一编写 IOC 容器
前言
本文的编写主要是最近在使用 midway 编写后端应用,midway 的 IOC 控制反转能力跟我们平时常写的前端应用,例如 react、vue 这些单应用还是有蛮大区别的,所以促使我想一探究竟,这种类 Spring IOC 容器是如何用 JavaScript 来实现的。为方便读者阅读,本文的组织结构依次为 TS 装饰器、Reflect Metadata、IOC 容器源码简单解读、以及自定义实现 IOC 容器。阅读完本文后,我希望你能有这样的感悟:元数据(metadata)和 装饰器(Decorator) 本是 ES 中两个独立的部分,但是结合它们, 竟然能实现 控制反转 这样的能力。本文的所有演示实例都已经上传到 github 仓库 ioc-container ,读者可以克隆下来进行调试运行。
辛苦整理良久,还望手动点赞鼓励~ 博客 github地址为:github.com/fengshi123/… ,汇总了作者的所有博客,欢迎关注及 star ~
一、TS 装饰器
1、类装饰器
(1)类型声明
type ClassDecorator = <TFunction extends Function>
(target: TFunction) => TFunction | void;
参数:
target: 类的构造器。
返回:
如果类装饰器返回了一个值,它将会被用来代替原有的类构造器的声明。因此,类装饰器适合用于继承一个现有类并添加一些属性和方法。例如我们可以添加一个 toString 方法给所有的类来覆盖它原有的 toString 方法,以及增加一个新的属性 school,如下所示
type Consturctor = { new (...args: any[]): any };
function School<T extends Consturctor>(BaseClass: T) {
// 新构造器继承原有的构造器,并且返回
return class extends BaseClass {
// 新增属性 school
public school = 'qinghua'
// 重写方法 toString
toString() {
return JSON.stringify(this);
}
};
}
@School
class Student {
public name = 'tom';
public age = 14;
}
console.log(new Student().toString())
// {"name":"tom","age":14,"school":"qinghua"}
但是存在一个问题:装饰器并没有类型保护,这意味着在类装饰器的构造函数中新增的属性,通过原有的类实例将报无法找到的错误,如下所示
type Consturctor = { new (...args: any[]): any };
function School<T extends Consturctor>(BaseClass: T){
return class extends BaseClass {
// 新增属性 school
public school = 'qinghua'
};
}
@School
class Student{
getSchool() {
return this.school; // Property 'school' does not exist on type 'Student'
}
}
new Student().school // Property 'school' does not exist on type 'Student'
这是 一个TypeScript的已知的缺陷。 目前我们能做的可以额外提供一个类用于提供类型信息,如下所示
type Consturctor = { new (...args: any[]): any };
function School<T extends Consturctor>(BaseClass: T){
return class extends BaseClass {
// 新增属性 school
public school = 'qinghua'
};
}
// 新增一个类用于提供类型信息
class Base {
school: string;
}
@School
class Student extends Base{
getSchool() {
return this.school;
}
}
new Student().school)
2、属性装饰器
(1)类型声明
type PropertyDecorator = (
target: Object,
propertyKey: string | symbol
) => void;
复制代码
参数:
target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链。
propertyKey: 属性的名称。
返回:
返回的结果将被忽略。
我们可以通过属性装饰器给属性添加对应的验证判断,如下所示
function NameObserve(target: Object, property: string): void {
console.log('target:', target)
console.log('property:', property)
let _property = Symbol(property)
Object.defineProperty(target, property, {
set(val){
if(val.length > 4){
throw new Error('名称不能超过4位!')
}
this[_property] = val;
},
get: function() {
return this[_property];
}
})
}
class Student {
@NameObserve
public name: string; // target: Student {} key: 'name'
}
const stu = new Student();
stu.name = 'jack'
console.log(stu.name); // jack
// stu.name = 'jack1'; // Error: 名称不能超过4位!
export default Student;
3、方法装饰器
(1)类型声明:
type MethodDecorator = <T>(
target: Object,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;
参数:
target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链;
propertyKey: 属性的名称;
descriptor: 属性的描述器;
返回: 如果返回了值,它会被用于替代属性的描述器。
方法装饰器不同于属性装饰器的地方在于 descriptor 参数。 通过这个参数我们可以修改方法原本的实现,添加一些共用逻辑。 例如我们可以给一些方法添加打印输入与输出的能力
function logger(target: Object, property: string,
descriptor: PropertyDescriptor): PropertyDescriptor | void {
const origin = descriptor.value;
console.log(descriptor)
descriptor.value = function(...args: number[]){
console.log('params:', ...args)
const result = origin.call(this, ...args);
console.log('result:', result);
return result;
}
}
class Person {
@logger
add(x: number, y: number){
return x + y;
}
}
const person = new Person();
const result = person.add(1, 2);
console.log('查看 result:', result) // 3
4、访问器装饰器
访问器装饰器总体上讲和方法装饰器很接近,唯一的区别在于描述器中有的 key 不同: 方法装饰器的描述器的 key 为:
value
writable
enumerable
configurable
访问器装饰器的描述器的key为:
get
set
enumerable
configurable
例如,我们可以对访问器进行统一更改:
function descDecorator(target: Object, property: string,
descriptor: PropertyDescriptor): PropertyDescriptor | void {
const originalSet = descriptor.set;
const originalGet = descriptor.get;
descriptor.set = function(value: any){
return originalSet.call(this, value)
}
descriptor.get = function(): string{
return 'name:' + originalGet.call(this)
}
}
class Person {
private _name = 'tom';
@descDecorator
set name(value: string){
this._name = value;
}
get name(){
return this._name;
}
}
const person = new Person();
person.name = ('tom');
console.log('查看:', person.name) // name:'tom'
5、参数装饰器
类型声明:
type ParameterDecorator = (
target: Object,
propertyKey: string | symbol,
parameterIndex: number
) => void;
参数:
target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链。
propertyKey: 属性的名称(注意是方法的名称,而不是参数的名称)。
parameterIndex: 参数在方法中所处的位置的下标。
返回:
返回的值将会被忽略。
单独的参数装饰器能做的事情很有限,它一般都被用于记录可被其它装饰器使用的信息。
function ParamDecorator(target: Object, property: string,
paramIndex: number): void {
console.log(property);
console.log(paramIndex);
}
class Person {
private name: string;
public setNmae(@ParamDecorator school: string, name: string){ // setNmae 0
this.name = school + '_' + name
}
}
6、执行时机
装饰器只在解释执行时应用一次,如下所示,这里的代码会在终端中打印 apply decorator,即便我们其实并没有使用类 A。
function f(C) {
console.log('apply decorator')
return C
}
@f
class A {}
// output: apply decorator
7、执行顺序
不同类型的装饰器的执行顺序是明确定义的:
实例成员:参数装饰器 -> 方法/访问器/属性 装饰器
静态成员:参数装饰器 -> 方法/访问器/属性 装饰器
构造器:参数装饰器
类装饰器
示例如下所示
function f(key: string): any {
console.log("evaluate: ", key);
return function () {
console.log("call: ", key);
};
}
@f("Class Decorator")
class C {
@f("Static Property")
static prop?: number;
@f("Static Method")
static method(@f("Static Method Parameter") foo:any) {}
constructor(@f("Constructor Parameter") foo:any) {}
@f("Instance Method")
method(@f("Instance Method Parameter") foo:any) {}
@f("Instance Property")
prop?: number;
}
/* 输出顺序如下
evaluate: Instance Method
evaluate: Instance Method Parameter
call: Instance Method Parameter
call: Instance Method
evaluate: Instance Property
call: Instance Property
evaluate: Static Property
call: Static Property
evaluate: Static Method
evaluate: Static Method Parameter
call: Static Method Parameter
call: Static Method
evaluate: Class Decorator
evaluate: Constructor Parameter
call: Constructor Parameter
call: Class Decorator
*/
我们从上注意到执行实例属性 prop 晚于实例方法 method 然而执行静态属性 static prop 早于静态方法static method。 这是因为对于属性/方法/访问器装饰器而言,执行顺序取决于声明它们的顺序。 然而,同一方法中不同参数的装饰器的执行顺序是相反的, 最后一个参数的装饰器会最先被执行。
function f(key: string): any {
console.log("evaluate: ", key);
return function () {
console.log("call: ", key);
};
}
class C {
method(
@f("Parameter Foo") foo,
@f("Parameter Bar") bar
) {}
}
/* 输出顺序如下
evaluate: Parameter Foo
evaluate: Parameter Bar
call: Parameter Bar
call: Parameter Foo
*/
8、多个装饰器组合
我们可以对同一目标应用多个装饰器。它们的组合顺序为:
求值外层装饰器
求值内层装饰器
调用内层装饰器
调用外层装饰器
如下示例所示
function f(key: string) {
console.log("evaluate: ", key);
return function () {
console.log("call: ", key);
};
}
class C {
@f("Outer Method")
@f("Inner Method")
method() {}
}
/* 输出顺序如下
evaluate: Outer Method
evaluate: Inner Method
call: Inner Method
call: Outer Method
*/
二、Reflect Metadata
1、背景
在 ES6 的规范当中,ES6 支持元编程,核心是因为提供了对 Proxy 和 Reflect 对象的支持。简单来说这个 API 的作用就是可以实现对变量操作的函数化,也就是反射。然而 ES6 的 Reflect 规范里面还缺失一个规范,那就是 Reflect Metadata。这会造成什么样的情境呢? 由于 JS/TS 现有的 装饰器更多的是存在于对函数或者属性进行一些操作,比如修改他们的值,代理变量,自动绑定 this 等等功能。但是却无法实现通过反射来获取究竟有哪些装饰器添加到这个类/方法上... 这就限制了 JS 中元编程的能力。 此时 Relfect Metadata 就派上用场了,可以通过装饰器来给类添加一些自定义的信息。然后通过反射将这些信息提取出来(当然你也可以通过反射来添加这些信息)。 综合一下, JS 中对 Reflect Metadata 的诉求,简单概括就是:
其他 C#、Java、Pythone 语言已经有的高级功能,我 JS 也应该要有(诸如C# 和 Java 之类的语言支持将元数据添加到类型的属性或注释,以及用于读取元数据的反射API,而目前 JS 缺少这种能力)
许多用例(组合/依赖注入,运行时类型断言,反射/镜像,测试)都希望能够以一致的方式向类中添加其他元数据。
为了使各种工具和库能够推理出元数据,需要一种标准一致的方法;
元数据不仅可以用在对象上,也可以通过相关捕获器用在 Proxy 上;
对开发人员来说,定义新的元数据生成装饰器应该简洁易用;
2、使用
TypeScript 在 1.5+ 的版本已经支持 reflect-metadata,但是我们在使用的时候还需要额外进行安装,如下所示
npm i reflect-metadata --save
在 tsconfig.json 里配置选项 emitDecoratorMetadata: true
关于 reflect-metadata 的基本使用 api 可以阅读 reflect-metadata 文档,其包含常见的增删改查基本功能,我们来看下其基本的使用示例,其中 Reflect.metadata 当作 Decorator 使用,当修饰类时,在类上添加元数据,当修饰类属性时,在类原型的属性上添加元数据,如:
import "reflect-metadata";
@Reflect.metadata('classMetaData', 'A')
class SomeClass {
@Reflect.metadata('methodMetaData', 'B')
public someMethod(): string {
return 'hello someMethod';
}
}
console.log(Reflect.getMetadata('classMetaData', SomeClass)); // 'A'
console.log(Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod')); // 'B
当然跟我们平时看到的 IOC 不同,我们进一步结合装饰器,如下所示,与前面的功能是一样的
import "reflect-metadata";
function classDecorator(): ClassDecorator {
return target => {
// 在类上定义元数据,key 为 `classMetaData`,value 为 `a`
Reflect.defineMetadata('classMetaData', 'A', target);
};
}
function methodDecorator(): MethodDecorator {
return (target, key, descriptor) => {
// 在类的原型属性 'someMethod' 上定义元数据,key 为 `methodMetaData`,value 为 `b`
Reflect.defineMetadata('methodMetaData', 'B', target, key);
};
}
@classDecorator()
class SomeClass {
@methodDecorator()
someMethod() {}
}
console.log(Reflect.getMetadata('classMetaData', SomeClass)); // 'A'
console.log(Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod')); // 'B'
3、design:类型元数据
在 TS 中的 reflect-metadata 的功能是经过增强的,其添加 "design:type"、"design:paramtypes" 和 "design:returntype" 这 3 个类型相关的元数据
design:type 表示被装饰的对象是什么类型, 比如是字符串、数字、还是函数等;
design:paramtypes 表示被装饰对象的参数类型, 是一个表示类型的数组, 如果不是函数, 则没有该 key;
design:returntype 表示被装饰对象的返回值属性, 比如字符串、数字或函数等;
示例如下所示
import "reflect-metadata";
@Reflect.metadata('type', 'class')
class A {
constructor(
public name: string,
public age: number
) { }
@Reflect.metadata(undefined, undefined)
method(name: string, age: number):boolean {
return true
}
}
const t1 = Reflect.getMetadata('design:type', A.prototype, 'method')
const t2 = Reflect.getMetadata('design:paramtypes', A.prototype, 'method')
const t3 = Reflect.getMetadata('design:returntype', A.prototype, 'method')
console.log(t1) // [Function: Function]
console.log(...t2) // [Function: String] [Function: Number]
console.log(t3) // [Function: Boolean]
三、IOC 容器实现
1、源码解读
我们可以从 github 上克隆 midway 仓库的代码到本地,然后进行代码阅读以及 debug 调试。本篇博文我们主要想探究下 midway 的依赖注入合控制反转是如何实现的,其主要源码存在于两个目录:packages/core 和 packages/decorator,其中 packages/core 包含依赖注入的核心实现,加载对象的class,同步、异步创建对象实例化,对象的属性绑定等。 IOC 容器就像是一个对象池,管理着每个对象实例的信息(Class Definition),所以用户无需关心什么时候创建,当用户希望拿到对象的实例 (Object Instance) 时,可以直接拿到依赖对象的实例,容器会 自动将所有依赖的对象都自动实例化。packages/core 中主要有以下几种,分别处理不同的逻辑:
AppliationContext 基础容器,提供了基础的增加定义和根据定义获取对象实例的能力;
MidwayContainer 用的最多的容器,做了上层封装,通过 bind 函数能够方便的生成类定义,midway 从此类开始扩展;
RequestContainer 用于请求链路上的容器,会自动销毁对象并依赖另一个容器创建实例;
packages/decorator 包含装饰器 provide.ts、inject.ts 的实现,在midwayjs中是有一个装饰器管理类DecoratorManager, 用来管理 midwayjs 的所有装饰器:
@provide() 的作用是简化绑定,能被 IOC 容器自动扫描,并绑定定义到容器上,对应的逻辑是绑定对象定义;
@inject() 的作用是将容器中的定义实例化成一个对象,并且绑定到属性中,这样,在调用的时候就可以访问到该属性。
2、简单实现
2.1、装饰器 Provider
实现装饰器 Provider 类,作用为将对应类注册到 IOC 容器中。
import 'reflect-metadata'
import * as camelcase from 'camelcase'
import { class_key } from './constant'
// Provider 装饰的类,表示要注册到 IOC 容器中
export function Provider (identifier?: string, args?: Array<any>) {
return function (target: any) {
// 类注册的唯一标识符
identifier = identifier ?? camelcase(target.name)
Reflect.defineMetadata(class_key, {
id: identifier, // 唯一标识符
args: args || [] // 实例化所需参数
}, target)
return target
}
}
2.2、装饰器 Inject
实现装饰器 Inject 类,作用为将对应的类注入到对应的地方。
import 'reflect-metadata'
import { props_key } from './constant'
export function Inject () {
return function (target: any, targetKey: string) {
// 注入对象
const annotationTarget = target.constructor
let props = {}
// 同一个类,多个属性注入类
if (Reflect.hasOwnMetadata(props_key, annotationTarget)) {
props = Reflect.getMetadata(props_key, annotationTarget)
}
//@ts-ignore
props[targetKey] = {
value: targetKey
}
Reflect.defineMetadata(props_key, props, annotationTarget)
}
}
2.3、管理容器 Container
管理容器 Container 的实现,用于绑定实例信息并且在对应的地方获取它们。
import 'reflect-metadata'
import { props_key } from './constant'
export class Container {
bindMap = new Map()
// 绑定类信息
bind(identifier: string, registerClass: any, constructorArgs: any[]) {
this.bindMap.set(identifier, {registerClass, constructorArgs})
}
// 获取实例,将实例绑定到需要注入的对象上
get<T>(identifier: string): T {
const target = this.bindMap.get(identifier)
if (target) {
const { registerClass, constructorArgs } = target
// 等价于 const instance = new registerClass([...constructorArgs])
const instance = Reflect.construct(registerClass, constructorArgs)
const props = Reflect.getMetadata(props_key, registerClass)
for (let prop in props) {
const identifier = props[prop].value
// 递归进行实例化获取 injected object
instance[prop] = this.get(identifier)
}
return instance
}
}
}
2.4、加载类文件 load
启动时扫描所有文件,获取文件导出的所有类,然后根据元数据进行绑定。
import * as fs from 'fs'
import { resolve } from 'path'
import { class_key } from './constant'
// 启动时扫描所有文件,获取定义的类,根据元数据进行绑定
export function load(container: any, path: string) {
const list = fs.readdirSync(path)
for (const file of list) {
if (/\.ts$/.test(file)) {
const exports = require(resolve(path, file))
for (const m in exports) {
const module = exports[m]
if (typeof module === 'function') {
const metadata = Reflect.getMetadata(class_key, module)
// register
if (metadata) {
container.bind(metadata.id, module, metadata.args)
}
}
}
}
}
}
2.5、示例类
三个示例类如下所示
// class A
import { Provider } from "../provide";
import { Inject } from "../inject";
import B from './classB'
import C from './classC'
@Provider('a')
export default class A {
@Inject()
private b: B
@Inject()
c: C
print () {
this.c.print()
}
}
// class B
import { Provider } from '../provide'
@Provider('b', [10])
export default class B {
n: number
constructor (n: number) {
this.n = n
}
}
// class C
import { Provider } from '../provide'
@Provider()
export default class C {
print () {
console.log('hello')
}
}
2.6、初始化
我们能从以下示例结果中看到,我们已经实现了一个基本的 IOC 容器能力。
import { Container } from './container'
import { load } from './load'
import { class_path } from './constant'
const init = function () {
const container = new Container()
// 通过加载,会先执行装饰器(设置元数据),
// 再由 container 统一管理元数据中,供后续使用
load(container, class_path)
const a:any = container.get('a') // A { b: B { n: 10 }, c: C {} }
console.log(a);
a.c.print() // hello
}
init()
总结
本文的依次从 TS 装饰器、Reflect Metadata、IOC 容器源码简单解读、以及自定义实现 IOC 容器四个部分由零到一编写自定义 IOC 容器,希望对你有所启发。本文的所有演示实例都已经上传到 github 仓库 ioc-container ,读者可以克隆下来进行调试运行。
作者:我是你的超级英雄
来源:https://juejin.cn/post/7036895697865555982
手写迷你版Vue
手写迷你版Vue
Vue响应式设计思路
Vue响应式主要包含:
数据响应式
监听数据变化,并在视图中更新
Vue2使用
Object.defineProperty
实现数据劫持Vu3使用
Proxy
实现数据劫持模板引擎
提供描述视图的模板语法
插值表达式
{{}}
指令
v-bind
,v-on
,v-model
,v-for
,v-if
渲染
将模板转换为html
解析模板,生成
vdom
,把vdom
渲染为普通dom
数据响应式原理
数据变化时能自动更新视图,就是数据响应式 Vue2使用Object.defineProperty
实现数据变化的检测
原理解析
new Vue()
⾸先执⾏初始化,对data
执⾏响应化处理,这个过程发⽣在Observer
中同时对模板执⾏编译,找到其中动态绑定的数据,从
data
中获取并初始化视图,这个过程发⽣在
Compile
中
同时定义⼀个更新函数和
Watcher实例
,将来对应数据变化时,Watcher会调⽤更新函数由于
data
的某个key
在⼀个视图中可能出现多次,所以每个key
都需要⼀个管家Dep来管理多个
Watcher
将来
data
中数据⼀旦发⽣变化,会⾸先找到对应的Dep
,通知所有Watcher
执⾏更新函数
一些关键类说明
CVue
:自定义Vue类 Observer
:执⾏数据响应化(分辨数据是对象还是数组) Compile
:编译模板,初始化视图,收集依赖(更新函数、 watcher创建) Watcher
:执⾏更新函数(更新dom) Dep
:管理多个Watcher实例,批量更新
涉及关键方法说明
observe
: 遍历vm.data
的所有属性,对其所有属性做响应式,会做简易判断,创建Observer实例
进行真正响应式处理
html页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>cvue</title>
<script src="./cvue.js"></script>
</head>
<body>
<div id="app">
<p>{{ count }}</p>
</div>
<script>
const app = new CVue({
el: '#app',
data: {
count: 0
}
})
setInterval(() => {
app.count +=1
}, 1000);
</script>
</body>
</html>
CVue
创建基本CVue构造函数:
执⾏初始化,对
data
执⾏响应化处理
// 自定义Vue类
class CVue {
constructor(options) {
this.$options = options
this.$data = options.data
// 响应化处理
observe(this.$data)
}
}
// 数据响应式, 修改对象的getter,setter
function defineReactive(obj, key, val) {
// 递归处理,处理val是嵌套对象情况
observe(val)
Object.defineProperty(obj, key, {
get() {
return val
},
set(newVal) {
if(val !== newVal) {
console.log(`set ${key}:${newVal}, old is ${val}`)
val = newVal
// 继续进行响应式处理,处理newVal是对象情况
observe(val)
}
}
})
}
// 遍历obj,对其所有属性做响应式
function observe(obj) {
// 只处理对象类型的
if(typeof obj !== 'object' || obj == null) {
return
}
// 实例化Observe实例
new Observe(obj)
}
// 根据传入value的类型做相应的响应式处理
class Observe {
constructor(obj) {
if(Array.isArray(obj)) {
// TODO
} else {
// 对象
this.walk(obj)
}
}
walk(obj) {
// 遍历obj所有属性,调用defineReactive进行响应化
Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]))
}
}
为vm.$data做代理
方便实例上设置和获取数据
例如
原本应该是
vm.$data.count
vm.$data.count = 233
代理之后后,可以使用如下方式
vm.count
vm.count = 233
给vm.$data做代理
class CVue {
constructor(options) {
// 省略
// 响应化处理
observe(this.$data)
// 代理data上属性到实例上
proxy(this)
}
}
// 把CVue实例上data对象的属性到代理到实例上
function proxy(vm) {
Object.keys(vm.$data).forEach(key => {
Object.defineProperty(vm, key, {
get() {
// 实现 vm.count 取值
return vm.$data[key]
},
set(newVal) {
// 实现 vm.count = 123赋值
vm.$data[key] = newVal
}
})
})
}
编译
初始化视图
根据节点类型进行编译
class CVue {
constructor(options) {
// 省略。。
// 2 代理data上属性到实例上
proxy(this)
// 3 编译
new Compile(this, this.$options.el)
}
}
// 编译模板中vue语法,初始化视图,更新视图
class Compile {
constructor(vm, el) {
this.$vm = vm
this.$el = document.querySelector(el)
if(this.$el) {
this.complie(this.$el)
}
}
// 编译
complie(el) {
// 取出所有子节点
const childNodes = el.childNodes
// 遍历节点,进行初始化视图
Array.from(childNodes).forEach(node => {
if(this.isElement(node)) {
// TODO
console.log(`编译元素 ${node.nodeName}`)
} else if(this.isInterpolation(node)) {
console.log(`编译插值文本 ${node.nodeName}`)
}
// 递归编译,处理嵌套情况
if(node.childNodes) {
this.complie(node)
}
})
}
// 是元素节点
isElement(node) {
return node.nodeType === 1
}
// 是插值表达式
isInterpolation(node) {
return node.nodeType === 3
&& /\{\{(.*)\}\}/.test(node.textContent)
}
}
编译插值表达式
// 编译模板中vue语法,初始化视图,更新视图
class Compile {
complie(el) {
Array.from(childNodes).forEach(node => {
if(this.isElement(node)) {
console.log(`编译元素 ${node.nodeName}`)
} else if(this.isInterpolation(node)) {
// console.log(`编译插值文本 ${node.textContent}`)
this.complieText(node)
}
// 省略
})
}
// 是插值表达式
isInterpolation(node) {
return node.nodeType === 3
&& /\{\{(.*)\}\}/.test(node.textContent)
}
// 编译插值
complieText(node) {
// RegExp.$1是isInterpolation()中/\{\{(.*)\}\}/匹配出来的组内容
// 相等于{{ count }}中的count
const exp = String(RegExp.$1).trim()
node.textContent = this.$vm[exp]
}
}
编译元素节点和指令
需要取出指令和指令绑定值 使用数据更新视图
// 编译模板中vue语法,初始化视图,更新视图
class Compile {
complie(el) {
Array.from(childNodes).forEach(node => {
if(this.isElement(node)) {
console.log(`编译元素 ${node.nodeName}`)
this.complieElement(node)
}
// 省略
})
}
// 是元素节点
isElement(node) {
return node.nodeType === 1
}
// 编译元素
complieElement(node) {
// 取出元素上属性
const attrs = node.attributes
Array.from(attrs).forEach(attr => {
// c-text="count"中c-text是attr.name,count是attr.value
const { name: attrName, value: exp } = attr
if(this.isDirective(attrName)) {
// 取出指令
const dir = attrName.substring(2)
this[dir] && this[dir](node, exp)
}
})
}
// 是指令
isDirective(attrName) {
return attrName.startsWith('')
}
// 处理c-text文本指令
text(node, exp) {
node.textContent = this.$vm[exp]
}
// 处理c-html指令
html(node, exp) {
node.innerHTML = this.$vm[exp]
}
}
以上完成初次渲染,但是数据变化后,不会触发页面更新
依赖收集
视图中会⽤到data中某key,这称为依赖。 同⼀个key可能出现多次,每次出现都需要收集(⽤⼀个Watcher来维护维护他们的关系),此过程称为依赖收集。 多个Watcher
需要⼀个Dep
来管理,需要更新时由Dep
统⼀通知。
data中的key和dep是一对一关系
视图中key出现和Watcher关系,key出现一次就对应一个Watcher
dep和Watcher是一对多关系
实现思路
在
defineReactive
中为每个key
定义一个Dep实例
编译阶段,初始化视图时读取key, 会创建
Watcher实例
由于读取过程中会触发key的
getter
方法,便可以把Watcher实例
存储到key对应的Dep实例
中当key更新时,触发setter方法,取出对应的
Dep实例
,Dep实例
调用notiy
方法通知所有Watcher更新
定义Watcher类
监听器,数据变化更新对应节点视图
// 创建Watcher监听器,负责更新视图
class Watcher {
// vm vue实例,依赖key,updateFn更新函数(编译阶段传递进来)
constructor(vm, key, updateFn) {
this.$vm = vm
this.$key = key
this.$updateFn = updateFn
}
update() {
// 调用更新函数,获取最新值传递进去
this.$updateFn.call(this.$vm, this.$vm[this.$key])
}
}
修改Compile类中的更新函数,创建Watcher实例
class Complie {
// 省略。。。
// 编译插值
complieText(node) {
// RegExp.$1是isInterpolation()中/\{\{(.*)\}\}/匹配出来的组内容
// 相等于{{ count }}中的count
const exp = String(RegExp.$1).trim()
// node.textContent = this.$vm[exp]
this.update(node, exp, 'text')
}
// 处理c-text文本指令
text(node, exp) {
// node.textContent = this.$vm[exp]
this.update(node, exp, 'text')
}
// 处理c-html指令
html(node, exp) {
// node.innerHTML = this.$vm[exp]
this.update(node, exp, 'html')
}
// 更新函数
update(node, exp, dir) {
const fn = this[`${dir}Updater`]
fn && fn(node, this.$vm[exp])
// 创建监听器
new Watcher(this.$vm, exp, function(newVal) {
fn && fn(node, newVal)
})
}
// 文本更新器
textUpdater(node, value) {
node.textContent = value
}
// html更新器
htmlUpdater(node, value) {
node.innerHTML = value
}
}
定义Dep类
data的一个属性对应一个Dep实例
管理多个
Watcher
实例,通知所有Watcher
实例更新
// 创建订阅器,每个Dep实例对应data中的一个属性
class Dep {
constructor() {
this.deps = []
}
// 添加Watcher实例
addDep(dep) {
this.deps.push(dep)
}
notify() {
// 通知所有Wather更新视图
this.deps.forEach(dep => dep.update())
}
}
创建Watcher时触发getter
class Watcher {
// vm vue实例,依赖key,updateFn更新函数(编译阶段传递进来)
constructor(vm, key, updateFn) {
// 省略
// 把Wather实例临时挂载在Dep.target上
Dep.target = this
// 获取一次属性,触发getter, 从Dep.target上获取Wather实例存放到Dep实例中
this.$vm[key]
// 添加后,重置Dep.target
Dep.target = null
}
}
defineReactive中作依赖收集,创建Dep实例
function defineReactive(obj, key, val) {
// 递归处理,处理val是嵌套对象情况
observe(val)
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
Dep.target && dep.addDep(Dep.target)
return val
},
set(newVal) {
if(val !== newVal) {
val = newVal
// 继续进行响应式处理,处理newVal是对象情况
observe(val)
// 更新视图
dep.notify()
}
}
})
}
监听事件指令@xxx
在创建vue实例时,需要缓存
methods
到vue实例上编译阶段取出methods挂载到Compile实例上
编译元素时
识别出
v-on
指令时,进行事件的绑定识别出
@
属性时,进行事件绑定事件绑定:通过指令或者属性获取对应的函数,给元素新增事件监听,使用
bind
修改监听函数的this指向为组件实例
// 自定义Vue类
class CVue {
constructor(options) {
this.$methods = options.methods
}
}
// 编译模板中vue语法,初始化视图,更新视图
class Compile {
constructor(vm, el) {
this.$vm = vm
this.$el = document.querySelector(el)
this.$methods = vm.$methods
}
// 编译元素
complieElement(node) {
// 取出元素上属性
const attrs = node.attributes
Array.from(attrs).forEach(attr => {
// c-text="count"中c-text是attr.name,count是attr.value
const { name: attrName, value: exp } = attr
if(this.isDirective(attrName)) {
// 省略。。。
if(this.isEventListener(attrName)) {
// v-on:click, subStr(5)即可截取到click
const eventType = attrName.substring(5)
this.bindEvent(eventType, node, exp)
}
} else if(this.isEventListener(attrName)) {
// @click, subStr(1)即可截取到click
const eventType = attrName.substring(1)
this.bindEvent(eventType, node, exp)
}
})
}
// 是事件监听
isEventListener(attrName) {
return attrName.startsWith('@') || attrName.startsWith('c-on')
}
// 绑定事件
bindEvent(eventType, node, exp) {
// 取出表达式对应函数
const method = this.$methods[exp]
// 增加监听并修改this指向当前组件实例
node.addEventListener(eventType, method.bind(this.$vm))
}
}
v-model双向绑定
实现v-model
绑定input
元素时的双向绑定功能
// 编译模板中vue语法,初始化视图,更新视图
class Compile {
// 省略...
// 处理c-model指令
model(node, exp) {
// 渲染视图
this.update(node, exp, 'model')
// 监听input变化
node.addEventListener('input', (e) => {
const { value } = e.target
// 更新数据,相当于this.username = 'mio'
this.$vm[exp] = value
})
}
// model更新器
modelUpdater(node, value) {
node.value = value
}
}
数组响应式
获取数组原型
数组原型创建对象作为数组拦截器
重写数组的7个方法
// 数组响应式
// 获取数组原型, 后面修改7个方法
const originProto = Array.prototype
// 创建对象做备份,修改响应式都是在备份的上进行,不影响原始数组方法
const arrayProto = Object.create(originProto)
// 拦截数组方法,在变更时发出通知
;['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
// 在备份的原型上做修改
arrayProto[method] = function() {
// 调用原始操作
originProto[method].apply(this, arguments)
// 发出变更通知
console.log(`method:${method} value:${Array.from(arguments)}`)
}
})
class Observe {
constructor(obj) {
if(Array.isArray(obj)) {
// 修改数组原型为自定义的
obj.__proto__ = arrayProto
this.observeArray(obj)
} else {
// 对象
this.walk(obj)
}
}
observeArray(items) {
// 如果数组内部元素时对象,继续做响应化处理
items.forEach(item => observe(item))
}
}
作者:LastStarDust
来源:https://juejin.cn/post/7036291383153393701
LRU缓存-keep-alive实现原理
前言
相信大部分同学在日常需求开发中或多或少的会有需要一个组件状态被持久化、不被重新渲染的场景,熟悉 vue 的同学一定会想到 keep-alive
这个内置组件。
那么什么是 keep-alive
呢?
keep-alive
是 Vue.js 的一个 内置组件。它能够将不活动的组件实例保存在内存中,而不是直接将其销毁,它是一个抽象组件,不会被渲染到真实 DOM 中,也不会出现在父组件链中。简单的说,keep-alive用于保存组件的渲染状态,避免组件反复创建和渲染,有效提升系统性能。 keep-alive
的 max
属性,用于限制可以缓存多少组件实例,一旦这个数字达到了上限,在新实例被创建之前,已缓存组件中最久没有被访问的实例会被销毁掉,而这里所运用到的缓存机制就是 LRU 算法
LRU 缓存淘汰算法
LRU( least recently used)根据数据的历史记录来淘汰数据,重点在于保护最近被访问/使用过的数据,淘汰现阶段最久未被访问的数据
LRU的主体思想在于:如果数据最近被访问过,那么将来被访问的几率也更高
新数据插入到链表尾部;
每当缓存命中(即缓存数据被访问),则将数据移到链表尾部
当链表满的时候,将链表头部的数据丢弃。
实现LRU的数据结构
经典的 LRU 一般都使用
hashMap
+双向链表
。考虑可能需要频繁删除一个元素,并将这个元素的前一个节点指向下一个节点,所以使用双链接最合适。并且它是按照结点最近被使用的时间顺序来存储的。 如果一个结点被访问了, 我们有理由相信它在接下来的一段时间被访问的概率要大于其它结点。
不过既然已经在 js 里都已经使用 Map
了,何不直接取用现成的迭代器获取下一个结点的 key 值(keys().next(
)
)
// ./LRU.ts
export class LRUCache {
capacity: number; // 容量
cache: Map; // 缓存
constructor(capacity: number) {
this.capacity = capacity;
this.cache = new Map();
}
get(key: number): number {
if (this.cache.has(key)) {
let temp = this.cache.get(key) as number;
//访问到的 key 若在缓存中,将其提前
this.cache.delete(key);
this.cache.set(key, temp);
return temp;
}
return -1;
}
put(key: number, value: number): void {
if (this.cache.has(key)) {
this.cache.delete(key);
//存在则删除,if 结束再提前
} else if (this.cache.size >= this.capacity) {
// 超过缓存长度,淘汰最近没使用的
this.cache.delete(this.cache.keys().next().value);
console.log(`refresh: key:${key} , value:${value}`)
}
this.cache.set(key, value);
}
toString(){
console.log('capacity',this.capacity)
console.table(this.cache)
}
}
// ./index.ts
import {LRUCache} from './lru'
const list = new LRUCache(4)
list.put(2,2) // 入 2,剩余容量3
list.put(3,3) // 入 3,剩余容量2
list.put(4,4) // 入 4,剩余容量1
list.put(5,5) // 入 5,已满 从头至尾 2-3-4-5
list.put(4,4) // 入4,已存在 ——> 置队尾 2-3-5-4
list.put(1,1) // 入1,不存在 ——> 删除队首 插入1 3-5-4-1
list.get(3) // 获取3,刷新3——> 置队尾 5-4-1-3
list.toString()
// ./index.ts
import {LRUCache} from './lru'
const list = new LRUCache(4)
list.put(2,2) // 入 2,剩余容量3
list.put(3,3) // 入 3,剩余容量2
list.put(4,4) // 入 4,剩余容量1
list.put(5,5) // 入 5,已满 从头至尾 2-3-4-5
list.put(4,4) // 入4,已存在 ——> 置队尾 2-3-5-4
list.put(1,1) // 入1,不存在 ——> 删除队首 插入1 3-5-4-1
list.get(3) // 获取3,刷新3——> 置队尾 5-4-1-3
list.toString()
结果如下:
vue 中 Keep-Alive
原理
使用 LRU 缓存机制进行缓存,max 限制缓存表的最大容量
根据设定的 include/exclude(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例
根据组件 ID 和 tag 生成缓存 Key ,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该 key 在 this.keys 中的位置(更新 key 的位置是实现 LRU 置换策略的关键)
获取节点名称,或者根据节点 cid 等信息拼出当前 组件名称
获取 keep-alive 包裹着的第一个子组件对象及其组件名
源码分析
初始化 keepAlive 组件
const KeepAliveImpl: ComponentOptions = {
name: `KeepAlive`,
props: {
include: [String, RegExp, Array],
exclude: [String, RegExp, Array],
max: [String, Number],
},
setup(props: KeepAliveProps, { slots }: SetupContext) {
// 初始化数据
const cache: Cache = new Map();
const keys: Keys = new Set();
let current: VNode | null = null;
// 当 props 上的 include 或者 exclude 变化时移除缓存
watch(
() => [props.include, props.exclude],
([include, exclude]) => {
include && pruneCache((name) => matches(include, name));
exclude && pruneCache((name) => !matches(exclude, name));
},
{ flush: "post", deep: true }
);
// 缓存组件的子树 subTree
let pendingCacheKey: CacheKey | null = null;
const cacheSubtree = () => {
// fix #1621, the pendingCacheKey could be 0
if (pendingCacheKey != null) {
cache.set(pendingCacheKey, getInnerChild(instance.subTree));
}
};
// KeepAlive 组件的设计,本质上就是空间换时间。
// 在 KeepAlive 组件内部,
// 当组件渲染挂载和更新前都会缓存组件的渲染子树 subTree
onMounted(cacheSubtree);
onUpdated(cacheSubtree);
onBeforeUnmount(() => {
// 卸载缓存表里的所有组件和其中的子树...
}
return ()=>{
// 返回 keepAlive 实例
}
}
}
return ()=>{
// 省略部分代码,以下是缓存逻辑
pendingCacheKey = null
const children = slots.default()
let vnode = children[0]
const comp = vnode.type as Component
const name = getName(comp)
const { include, exclude, max } = props
// key 值是 KeepAlive 子节点创建时添加的,作为缓存节点的唯一标识
const key = vnode.key == null ? comp : vnode.key
// 通过 key 值获取缓存节点
const cachedVNode = cache.get(key)
if (cachedVNode) {
// 缓存存在,则使用缓存装载数据
vnode.el = cachedVNode.el
vnode.component = cachedVNode.component
if (vnode.transition) {
// 递归更新子树上的 transition hooks
setTransitionHooks(vnode, vnode.transition!)
}
// 阻止 vNode 节点作为新节点被挂载
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
// 刷新key的优先级
keys.delete(key)
keys.add(key)
} else {
keys.add(key)
// 属性配置 max 值,删除最久不用的 key ,这很符合 LRU 的思想
if (max && keys.size > parseInt(max as string, 10)) {
pruneCacheEntry(keys.values().next().value)
}
}
// 避免 vNode 被卸载
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
current = vnode
return vnode;
}
将组件移出缓存表
// 遍历缓存表
function pruneCache(filter?: (name: string) => boolean) {
cache.forEach((vnode, key) => {
const name = getComponentName(vnode.type as ConcreteComponent);
if (name && (!filter || !filter(name))) {
// !filter(name) 即 name 在 includes 或不在 excludes 中
pruneCacheEntry(key);
}
});
}
// 依据 key 值从缓存表中移除对应组件
function pruneCacheEntry(key: CacheKey) {
const cached = cache.get(key) as VNode;
if (!current || cached.type !== current.type) {
/* 当前没有处在 activated 状态的组件
* 或者当前处在 activated 组件不是要删除的 key 时
* 卸载这个组件
*/
unmount(cached); // unmount方法里同样包含了 resetShapeFlag
} else if (current) {
// 当前组件在未来应该不再被 keepAlive 缓存
// 虽然仍在 keepAlive 的容量中但是需要刷新当前组件的优先级
resetShapeFlag(current);
// resetShapeFlag
}
cache.delete(key);
keys.delete(key);
}
function resetShapeFlag(vnode: VNode) {
let shapeFlag = vnode.shapeFlag; // shapeFlag 是 VNode 的标识
// ... 清除组件的 shapeFlag
}
keep-alive案例
本部分将使用 vue 3.x 的新特性来模拟 keep-alive
的具体应用场景
在 index.vue 里我们引入了 CountUp 、timer 和 ColorRandom 三个带有状态的组件 在容量为 2 的
中包裹了一个动态组件
// index.vue
<script setup>
import { ref } from "vue"
import CountUp from '../components/CountUp.vue'
import ColorRandom from '../components/ColorRandom.vue'
import Timer from '../components/Timer.vue'
const tabs = ref([ // 组件列表
{
title: "ColorPicker",
comp: ColorRandom,
},
{
title: "timer1",
comp: Timer,
},
{
title: "timer2",
comp: Timer,
},
{
title: "CountUp",
comp: CountUp,
},
])
const currentTab = ref(tabs.value[0]) // tab 默认展示第一个组件
const tabSwitch = (tab) => {
currentTab.value = tab
}
script>
<template>
<div id="main-page">keep-alive demo belowdiv>
<div class="tab-group">
<button
v-for="tab in tabs"
:key="tab"
:class="['tab-button', { active: currentTab === tab }]"
@click="tabSwitch(tab)"
>
{{ tab.title }}
button>
div>
<keep-alive max="2">
<component
v-if="currentTab"
:is="currentTab.comp"
:key="currentTab.title"
:name="currentTab.title"
/>
keep-alive>
template>
缓存状态
缓存流程如下:
可以看到被包裹在 keep-alive
的动态组件缓存了前一个组件的状态。
通过观察 vue devtools 里节点的变化,可以看到此时 keepAlive 中包含了 ColorRandom
和 Timer
两个组件,当前展示的组件会处在 activated 的状态,而其他被缓存的组件则处在 inactivated
的状态
如果我们注释了两个 keep-alive
会发现不管怎么切换组件,都只会重新渲染,并不会保留前次的状态
移除组件
移除流程如下:
为了验证组件是否在切换tab时能被成功卸载,在每个组件的 onUnmounted
中加上了 log
onUnmounted(()=>{
console.log(`${props.name} 组件被卸载`)
})
当缓存数据长度小于等于 max ,切换组件并不会卸载其他组件,就像上面在 vue devtools 里展示的一样,只会触发组件的
activated
和deactivated
两个生命周期若此时缓存数据长度大于 max ,则会从缓存列表中删除优先级较低的,优先被淘汰的组件,对应的可以看到该组件
umounted
生命周期触发。
性能优化
使用 KeepAlive 后,被 KeepAlive 包裹的组件在经过第一次渲染后,的 vnode 以及 DOM 都会被缓存起来,然后再下一次再次渲染该组件的时候,直接从缓存中拿到对应的 vnode 和 DOM,然后渲染,并不需要再走一次组件初始化,render 和 patch 等一系列流程,减少了 script 的执行时间,性能更好。
总结
Vue 内部将 DOM 节点抽象成了一个个的 VNode 节点,keep-alive 组件的缓存也是基于 VNode 节点的而不是直接存储 DOM 结构。它将满足条件( include 与 exclude )的组件在 cache 对象中缓存起来,在需要重新渲染的时候再将 vnode 节点从 cache 对象中取出并渲染。
具体缓存过程如下:
声明有序集合 keys 作为缓存容器,存入组件的唯一 key 值
在缓存容器 keys 中,越靠前的 key 值意味着被访问的越少也越优先被淘汰
渲染函数执行时,若命中缓存时,则从 keys 中删除当前命中的 key,并往 keys 末尾追加 key 值,刷新该 key 的优先级
未命中缓存时,则 keys 追加缓存数据 key 值,若此时缓存数据长度大于 max 最大值,则删除最旧的数据
当触发 beforeMount/update 生命周期,缓存当前 activated 组件的子树的数据
参考
作者:政采云前端团队
来源:https://juejin.cn/post/7036483610920091656
统一路由,让小程序跳转更智能
我们在小程序开发及运营过程中,不可避免的需要进行页面之间的跳转。如果使用小程序自带的路由功能来实现这个功能,是非常简单的,如:
// 根据不同的场景选择 navigateTo、redirectTo、switchTab 等
wx.navigateTo({
url: "pages/somepage?id=1",
success: function (res) {},
});
但这里面存在几个问题:
- 需要代码里面写死或者运营人员维护小程序页面的长长的具体路径,这显然是很不友好的
- 需要知道页面是否为 tabbar 页面(switchTab)
- 如果某个页面在 tabbar 和非 tabbar 页面之间发生了变化,或路径因为重构、主包瘦身等各种原因发生变化,原来的代码就会报错导致无法运行
- navigateBack 不支持传参
为了解决以上问题,我们在项目中实现了一套基于命令别名(cmd)的统一路由跳转方式(以下称为统一路由),很好解决了遇到的实际问题,统一路由特点如下:
- 页面别名声明使用注释方式,不侵入业务代码
- 页面可以存在多个别名,方便新老版本页面的流量切换
- 路由内自动判断是否 tabbar 页面,自行处理跳转及传参,业务代码无需关心
- 支持纯 js api 的页面跳转及需要用户点击的任意类型跳转(如联系客服、打开小程序等等)
- 对于页面栈中存在相同页面时,可以自动返回并根据参数是否相同决定是否需要刷新页面,可有效减少页面栈层级,规避小程序 10 层限制
实现思路
step1. 资源描述约定
小程序内的跳转类操作存在以下几种
- js api 直接可以操作的内部页面间跳转(wx.navigateTo、wx.navigateBack、wx.redirectTo、wx.reLaunch、wx.switchTab)
- js api 直接可以操作的打开微信原生功能的跳转(扫码、拨打电话等)
- 需要借助点击操作的跳转(如打开小程序及客服等需要 open-type 配合的场景 )
针对这三类操作,我们使用常见的 URL(统一资源定位系统)方式描述不同的待跳转资源
- 内部页面
https://host?cmd=${pagename}¶m1=a // 打开普通页面并传参,标准的H5容器也算在普通页面内
- 微信原生 API
https://host?cmd=nativeAPI&API=makePhoneCall&phoneNumber=123456 // 拨打电话
https://host?cmd=nativeAPI&API=scanCode&callback=scanCallback // 扫码并执行回调
- 需要借助按钮 open-type 的微信原生能力
https://host?cmd=nativeButtonAPI&openType=contact // 在线客服
- 打开另一个小程序
https://host?cmd=miniProgram&appId=wx637bb****&path=pages/order/index&version=trial&uid=${uid}
小程序跳转需要携带更多的参数,所以做了cmd的区分,这里实际会解析成 nativeButtonAPI 运行
step2. 在页面内定义需要的数据
在每个页面的顶部添加注释,注意 cmd 不能重复,支持多个 cmd。为了方便后续解析,我们的注释大体上遵循 JSDoc 注释规范
// pages/detail/index.tsx
/**
* @cmd detail, newdetail
* @description 详情
* @param skuid {number} skuid
*/
step3. 在编译阶段扫描并生成配置文件
根据入口文件的页面定义,匹配出需要的注释部分,使用 doctrine 解析需要的数据,解析后的数据如下:
// config/router.config.ts
export default {
index: {
description: "首页", // 页面描述
path: "/pages/index/index", // 真实路径
isTabbar: true, // 是否tabbar页面
ensureLogin: false, // 是否需要强制登录
},
detail: {
description: "详情",
path: "/pages/detail/index",
isTabbar: false,
ensureLogin: true,
},
};
这里顺便可以使用 param 等生成详细的页面名称及入参文档,提供给其他研发或运营同学使用。
step4. 资源描述解析为标准数据
根据上面的资源描述约定及扫描得到的配置文件,我们可以将其转换为方便在小程序内解析的数据定义,基本格式如下
{
origin: 'https://host?cmd=detail&skuid=1', // 原始数据
parsed: {
type: 'PAGE', // 类型,PAGE,NATIVE_API,NATIVE_BUTTON_API,UNKNOW
data: {
path: 'pages/detail/index', // 实际的页面路径,如果type是PAGE则会解析出此字段
action: undefined, // 动作,scanCode,makePhoneCall,openType,miniprogram ……。如果type是NATIVE_API,NATIVE_BUTTON_API,则会解析出此字段
params: {
skuid: '1' // 需要携带的参数
}
}
}
}
step5. 根据标准数据执行对应逻辑
由于我们的项目使用的是 Taro 框架,以下伪代码都是以 Taro 为例。
// utils/router.ts
// 用于解析原始链接为标准数据
const parseURL = (origin) => {
// balabala,一顿操作格式化成上文的数据
const data = {
...
};
return data;
};
// 执行除 NATIVE_BUTTON_API 之外的跳转
const routeURL = (origin) => {
const parsedData = parseURL(origin)
const {parsed: {type, data}} = parsedData
switch(type){
case 'PAGE':
...
break;
case 'NATIVE_API':
...
break;
case 'UNKNOW':
...
break;
}
};
export default {
parseURL,
routeURL,
};
对于需要点击的类型,我们需要借助 UI 组件实现
// components/router.tsx
import router from "/utils/router";
import { Button } from "@tarojs/components";
import Taro, { Component, eventCenter } from "@tarojs/taro";
export default class Router extends Component {
componentWillMount() {
const { path } = this.props;
const data = router.parseURL(path);
const { parsed, origin } = data;
const openType =
(parsed &&
parsed.data &&
parsed.data.params &&
parsed.data.params.openType) ||
false;
this.setState({
parsed,
openType,
});
}
// 点击事件
async handleClick(parsed, origin) {
// 点击执行动作
let {
type,
data: { action, params },
} = parsed;
if (!type) {
return;
}
// 内部页面
if (["PAGE", "CMD_UNKNOW"].includes(type)) {
console.log(`CMD_NATIVE_PAGE 参数:`, origin, options);
router.routeURL(origin);
return;
}
// 拨打电话、扫码等原生API
if (["NATIVE_API"].includes(type) && action) {
if (action === "makePhoneCall") {
let { phoneNumber = "" } = params;
if (!phoneNumber || phoneNumber.replace(/\s/g, "") == "") {
Taro.showToast({
icon: "none",
title: "未查询到号码,无法呼叫哦~",
});
return;
}
}
let res = await Taro[action]({ ...params });
// 扫码事件,需要在扫码完成后发送全局广播,业务内自行处理
if (action === "scanCode" && params.callback) {
let eventName = `${params.callback}_event`;
eventCenter.trigger(eventName, res);
}
}
// 打开小程序
if (
["NATIVE_BUTTON_API"].includes(type) &&
["miniprogram"].includes(action)
) {
await Taro.navigateToMiniProgram({
...params,
});
}
}
render() {
const { parsed, openType, origin } = this.state;
return (
<Button
onClick={this.handleClick.bind(this, parsed, origin)}
hoverClass="none"
openType={openType}
>
{this.props.children}
</Button>
);
}
}
在具体业务中使用
// pages/index/index.tsx
import router from "/utils/router";
import Router from "/components/router";
// js方式直接跳转
router.routeURL('https://host?cmd=detail&skuid=1')
// UI组件方式
...
render(){
return <Router path='https://host?cmd=detail&skuid=1'></Router>
}
...
当然这里面可以附加你自己需要的功能,比如:增加跳转方式控制、数据处理、埋点、加锁防连续点击,相对来说并不复杂。甚至你还可以顺手实现一下上面提到的 navigateBack 传参。
结语
上文的思考及实现过程比较简单,纯属抛砖引玉,欢迎大家交流互动。
作者:胖纳特
链接:https://juejin.cn/post/6930899487250448398
收起阅读 »
如何美化checkbox
前言
对于前端开发人员,checkbox应该是经常见到的东西。利用checkbox的checked属性,我们可以做出很多精彩的效果,之前还用checkbox来做动画暂停。前几天还看到外国大佬使用 checkbok做游戏:http://www.bryanbraun.com/2021/09/21/… ,真的是佩服的五体投地,不过对于我这种菜鸡选手,还是只能实现一些简单的东西。对于下面的这个switch按钮,大家应该非常熟悉了,同样的在这个效果上还衍生出了各种华丽花哨的效果,例如暗黑模式的切换。一生万,掌握了一,万!还不是手到擒来。
推荐大家看看codepen上的这个仓库:文章封面的效果,也是从这里录制的!
tql
codepen.io/oliviale/pe…
标签
这里使用for将label和input捆绑
<input type="checkbox" id="toggle" />
<label for="toggle"></label>
同时设置input不可见
input {
display: none;
}
美化label
遇到checkbox的美化问题,基本上都是考虑用美化labl替代美化input。
设置背景颜色,宽高,以及圆角
.switch {
display: inline-block;
display:relative;
width: 40px;
height: 20px;
background-color: rgba(0, 0, 0, 0.25);
border-radius: 20px;
}
最终的效果如下:
切换的圆
在label上会有一个圆,一开始是在左边的,效果如下,其实这个只需要利用伪元素+positon定位,就可以实现了。
这是postion:absolute,同时将位置定位在top1px,left1px。同时设置圆角。
.switch:after {
content: "";
position: absolute;
width: 18px;
height: 18px;
border-radius: 18px;
background-color: white;
top: 1px;
left: 1px;
transition: all 0.3s;
}
checked+小球右移动
这里点击之后圆会跑到右边,这里有两种实现方案
1.仍然通过定位
当checkbox处于checked状态,会设置top,left,bottom,right。这里将top,left设置为auto是必须的,这种的好处就是,不需要考虑label的宽度。
input[type="checkbox"]:checked + .switch:after {
top: auto;
left: auto;
bottom: 1px ;
right: 1px ;
}
当然知道label的宽度可以直接,设置top和left
top: 1px;
left: 21px;
2.translateX
*transform: translateX(20px)*
美化切换后的label
加上背景色
input[type="checkbox"]:checked + .switch {
background-color: #7983ff;
}
效果:
后记
看上去本文是一篇介绍一个checkbox美化的效果,其实是一篇告诉你如何美化checkbox的文章,最终的思想就是依赖for的捆绑效果,美化label来达到最终的效果。
作者:半夏的故事
链接:https://juejin.cn/post/7035650204829220877
收起阅读 »
Metaverse 已经到来:5 家公司正在构建我们的虚拟现实未来
如果你相信 Facebook,未来就是一个虚拟现实的“元宇宙”。
这家上个月更名为 Meta的科技巨头计划今年投资100 亿美元来开发支持增强现实和虚拟现实的产品——机械手、高科技 VR 眼镜和复杂的软件应用程序,仅举几例。分析师预计该公司至少要花费 500 亿美元来实现其对虚拟现实未来的承诺。
但 Meta 远非唯一的玩家。事实上,六家其他公司已经在构建将成为下一代虚拟交互的硬件和软件——华尔街认为这是一个价值 1 万亿美元的市场。这些公司包括谷歌、微软、苹果、Valve 和其他开发工作和通信产品的公司。随着投资者涌入市场,规模较小的初创公司可能会加入他们的行列。
“元宇宙是真实的,华尔街正在寻找赢家,”韦德布什分析师丹艾夫斯在一份报告中说。
在 Facebook 试图在元领域打上烙印时,这些公司的产品将不得不与之抗衡。
谷歌
Google Cardboard 可能是历史上最成功的 VR 项目。2014 年,当时世界上最大的科技公司要求数百万人用一块硬纸板将智能手机绑在脸上。谷歌表示,它出货了“数千万”可折叠耳机,谷歌 Cardboard应用程序的下载量超过 1.6 亿次。这不是最高分辨率或高科技的体验,但该策略帮助向数百万学生和有抱负的开发人员介绍了虚拟现实。
它还帮助谷歌摆脱了之前的增强现实实验Glass。今天,增强现实眼镜作为企业业务的工具进行销售,但当它推出时,谷歌的期望值很高。字面意思是:谷歌创始人谢尔盖·布林 (Sergey Brin) 从飞机上跳下时宣布了 1,500 美元的产品。
玻璃本质上是智能手机的内脏,在非处方眼镜框架上安装了一个小型摄像头。该项目失败了,但在催生了无数模因之前就失败了。
微软
微软于 2015 年发布了Hololens混合现实眼镜。一年后,微软并没有用营销炒作充斥消费者市场,而是悄悄推出了 Hololens 作为工业制造工具,面向选定的企业集团。价值 3,000 美元的商业套件附带专业版 Windows,具有额外的安全功能和软件以帮助应用程序开发。第二次迭代于 2019 年首次亮相,价格稍贵,但拥有更好的相机和镜头卡口,可实现更精确的操作,并提供更广泛的软件功能,包括工业应用。
目前Hololens用户包括像肯沃斯,三得利和丰田,它使用耳机,以加快培养和汽车修理重量级人物,根据微软。
苹果
如果你相信的传言,苹果一直在释放的风口浪尖AR眼镜多年。这家 iPhone 制造商于 2017 年在 iOS 11 中发布了ARKit,这是为 Apple 设备创建增强现实应用程序的开发者框架。 据科技网站The Information报道,Apple 在 2019 年举行了一次 1000 人的会议,讨论了 iPhone 上的 AR 和两个潜力未来的产品,N421 智能眼镜和 N301 VR 耳机。分析师现在推测,苹果正准备在 2022 年及以后发布 AR 产品。
阀门
Valve 的Index耳机可以说是市场上最强大的消费虚拟现实产品。高分辨率屏幕流畅,控制器在虚拟现实和游戏环境中提供无与伦比的控制。该索引还与 Value 的Steam视频游戏市场集成,这意味着该设备已经堆满了兼容的内容。
它也很贵而且很笨拙。完整的 Index VR 套件的价格接近 1,000 美元,要正常运行,耳机需要多条电缆和传感器。Valve 继续创新和试验沉浸式虚拟现实耳机。分析师预计,这家总部位于贝尔维尤的游戏公司将很快发布一款独立耳机,与 Facebook 的Oculus Quest 2展开竞争。
魔法飞跃
尽管虚拟现实的想法部分受到科幻小说的启发,但 Big Tech 对 AR 和 VR 未来的现代愿景直接受到 Magic Leap 的启发。该公司成立于 2010 年,2014 年从谷歌和芯片制造商高通等公司筹集了超过 5 亿美元 。2015 年,该公司发布了一段令人惊叹的视频,旨在展示该产品的技术。但是怀疑论者质疑这项技术,最终的产品遭到了抨击。
最初的 Magic Leap 耳机是在设计和广告等创意协作行业销售的。
Magic Leap于 2018 年推出了一款精致的 AR 设备,筹集了更多资金,并计划在 2022 年初发布Magic Leap 2。该公司还计划瞄准国防、医疗保健和工业制造。
收起阅读 »跨域问题及常见解决方法
1.出现跨域问题是因为浏览器的同源策列限制,下面是MDN文档对浏览器同源策略的描述,简单来说就是:
同源策略会阻止一个域的javascript脚本和另外一个域的内容进行交互。所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port)
浏览器的同源策略
同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。
同源的定义
如果两个 URL 的 protocol、port (en-US) (如果有指定的话)和 host 都相同的话,则这两个 URL 是同源。这个方案也被称为“协议/主机/端口元组”,或者直接是 “元组”。(“元组” 是指一组项目构成的整体,双重/三重/四重/五重/等的通用形式)。
2.四种常见解决跨域的方法:
一,CORS:
跨域资源共享,它允许浏览器向非同源服务器,发出XMLHttpRequest请求。它对一般请求和非一般请求的处理方式不同: 1、一般跨域请求(对服务器没有要求):只需服务器端设置Access-Control-Allow-Origin 2、非一般跨域请求(比如要请求时要携带cookie):前后端都需要设置。
一般跨域请求服务器设置代码: (1)Node.JS
const http = require('http');
const server = http.createServer();
const qs = require('querystring');
server.on('request', function(req, res) {
var postData = '';
// 数据块接收中
req.addListener('data', function(chunk) {
postData += chunk;
});
// 数据接收完毕
req.addListener('end', function() {
postData = qs.parse(postData);
// 跨域后台设置
res.writeHead(200, {
'Access-Control-Allow-Credentials': 'true', // 后端允许发送Cookie
'Access-Control-Allow-Origin': 'http://www.example.com', // 允许访问的域(协议+域名+端口)
});
res.end(JSON.stringify(postData));
});
});
server.listen('8080');
console.log('running at port 8080...');
复制代码
(2)PHP
<?php
header("Access-Control-Allow-Origin:*");
复制代码
如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。
前端请求携带cookie代码:
(1)原生JavaScript
const xhr = new XMLHttpRequest();
// 前端设置是否带cookie
xhr.withCredentials = true;
};
复制代码
(2)axios
axios.defaults.withCredentials = true
复制代码
二,JSONP
JSONP 只支持get请求,不支持post请求。 核心思想:网页通过添加一个<scriot>
标签,向服务器请求 JSON 数据,服务器收到请求后,将数据放在一个指定名字的回调函数的参数位置传回来。
原生JavaScript代码:
<script src="http://example.php?callback=getData"></script>
// 向服务器发出请求,请求参数callback是下面定义的函数名字
// 处理服务器返回回调函数的数据
<script type="text/javascript">
function getData(res)
{
console.log(res.data)
}
</script>
复制代码
三,设置document.domain
因为浏览器是通过document.domain属性来检查两个页面是否同源,因此只要通过设置相同的document.domain,两个页面就可以共享Cookie(此方案仅限主域相同,子域不同的跨域应用场景。)
// 两个页面都设置
document.domain = 'test.com';
复制代码
四,跨文档通信 API:window.postMessage()
调用postMessage方法实现父窗口向子窗口发消息(子窗口同样可以通过该方法发送消息给父窗口)
var openWindow = window.open('http://test2.com', 'title');
// 父窗口向子窗口发消息(第一个参数代表发送的内容,第二个参数代表接收消息窗口的url)
openWindow.postMessage('Nice to meet you!', 'http://test2.com');
//调用message事件,监听对方发送的消息
// 监听 message 消息
window.addEventListener('message', function (e) {
console.log(e.source); // e.source 发送消息的窗口
console.log(e.origin); // e.origin 消息发向的网址
console.log(e.data); // e.data 发送的消息
},false);
作者:玩具大兵
来源:https://juejin.cn/post/7035562059152490526
TypeScript 原始类型、函数、接口、类、泛型 基础总结
原始数据类型
原始数据类型包括:
Boolean
String
Number
Null
undefined
类型声明是TS非常重要的一个特点,通过类型声明可以指定TS中变量、参数、形参的类型。
Boolean 类型
let boolean: boolean = true
boolean = false
boolean = null
// bollean = 123 报错不可以将数字 123 赋值给 boolean类型的变量Number 类型
//ES6 Number 类型 新增支持2进制和8进制
let num: number = 123
num = 0b1111String 类型
let str1: string = 'hello TS'
let sre2: string = `模板字符串也支持使用 ${str1}`]Null 和 Undefined
let n: null = null
let u: undefined = undefined
n = undefined
u = null
// undefined 和 null 是所有类型的子类型 所以可以赋值给number类型的变量
let num: number = 123
num = undefined
num = null
any 类型
any 表示队变量没有任何显示,编译器失去了对 TS 的检测功能与 JS 无异(不建议使用)。
let notSure: any = 4
// any类型可以随意赋值
notSure = `任意模板字符串`
notSure = true
notSure = null
// 当 notSure 为any 类型时,在any类型上访问任何属性和调用方法都是允许的, 很有可能出现错误
notSure.name // 现在调用name属性是允许的,但很明显我们定义的notSure没有name这个属性,下面的调用sayName方法也是如此
notSure.sayName()
array 类型
// 数组类型,可以指定数组的类型和使用数组的方法和属性
let arrOfNumbers: number[] = [1, 2, 3]
console.log(arrOfNumbers.length);
arrOfNumbers.push(4)
tuple 元组类型
// 元组类型 元组就是固定长度,类型的数组
// 类型和长度必须一致
let u: [string, number] = ['12', 12]
// let U: [string, number] = ['12', 12, true] 报错信息为:不能将类型“[string, number, boolean]”分配给类型“[string, number]”。源具有 3 个元素,但目标仅允许 2 个。
// 也可以使用数组的方法,如下所示push一个值给元组u
u.push(33)
Interface 接口
对对象的形状(shape)进行描述
Duck Typing(鸭子类型)
interface Person {
// readonly id 表示只读属性的id不可以修改
readonly id: number;
name: string;
age: number;
// weight? 表示可选属性,可以选用也可以不选用
weight?: number
}
let host: Person = {
id: 1,
name: 'host',
age: 20,
weight: 70
}
//host.id = 2 报错 提示信息为:无法分配到 "id" ,因为它是只读属性
function 函数类型
// 方式一:函数声明的写法 z 为可选参数 ,
function add1 (x: number, y: number, z?: number): number {
if (typeof z === 'number') {
return x + y + z
} else {
return x + y
}
}
// 需要注意的是:可选参数必须置于所有必选参数之后,否则会报错
add1(1, 2, 3)
// 方式二:函数表达式
const add2 = (x: number, y: number, z?: number): number => {
if (typeof z === 'number') {
return x + y + z
} else {
return x + y
}
}
// 使用interface接口 描述函数类型
interface ISum {
(x: number, y: number, z?: number): number
}
let add3: ISum = add1
值的注意的是:可选参数必须置于所有必选参数之后,否则会报错,如下图展示的错误案例所示:·
类型推论
当定义变量时没有指定类型,编译器会自动推论第一次赋的值为默认类型
let s = 'str'
// s = 12 本句将会报错,提示为:不能将类型“number”分配给类型“string”
联合类型
使用|
分隔可选类型
let StringOrNumber: string | number
StringOrNumber = 123
StringOrNumber = '111'
类型断言
使用 as
关键字进行类型断言
function getLength (rod: number | string): number {
const str = rod as string
//这里我们可以用 as 关键字,告诉typescript 编译器,你没法判断我的代码,但是我本人很清楚,这里我就把它看作是一个 string,你可以给他用 string 的方法。
if (str.length) {
return str.length
} else {
const num = rod as number
return num.toString().length
}
}
类型守卫
// 4.类型守卫 type guard typescript 在不同的条件分支里面,智能的缩小了范围
function getLength2 (rod: number | string): number {
if (typeof rod === 'string') {
return rod.length
} else {
// else 里面的rod 会自动默认为number类型
return rod.toString().length
}
}
Class 类
面向对象编程的三大特点:
封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,
继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性。
多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。
话不多少,看代码:
class Animal {
readonly name: string
constructor(name: string) {
this.name = name
console.log(this.run())
}
// private run ():私有的 protected run () 受保护的
run () {
return `${this.name} is running`
}
}
const animal = new Animal('elephant')
// console.log(animal.name)
animal.run() //elephant is running
// 继承
class Dog extends Animal {
age: number
constructor(name, age) {
super(name)
console.log(this.name)
this.age = age
}
bark () {
console.log(`这只在叫的狗狗叫${this.name},它今年${this.age}岁了`)
}
}
const dog = new Dog('旺财', 5)
dog.run() // 旺财 is running
dog.bark() // 这只在叫的狗狗叫旺财,它今年5岁了
// 多态
class Cat extends Animal {
static catAge = 2
constructor(name) {
super(name)
console.log(this.name) // 布丁
}
run () {
return 'Meow,' + super.run()
}
}
const cat = new Cat('布丁')
console.log(cat.run()) // Meow,布丁 is running
console.log(Cat.catAge) // 2
class中还提供了readonly关键字,readonly为只读属性,在调用的时候不能修改。如下所示:
类成员修饰符
public修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是public的。
private修饰的属性或方法是私有的,不能在声明它的类的外部访问。
上述示例代码中,在父类Animal的 run 方法身上加上private修饰符之后就会产生如下图的报错信息:
protected 修饰的属性或方法是受保护的,它和private类似,区别在于它在子类中也是可以访问的。
上述示例代码中,在父类Animal的 run 方法身上加上protected修饰符之后就会产生如下图的报错信息:
接口和类
类可以使用 implements来实现接口。
// interface可以用来抽象验证类的方法和方法
interface Person {
Speak (trigger: boolean): void;
}
interface Teenagers {
Young (sge: number): void
}
// 接口之间的继承
interface PersonAndTeenagers extends Teenagers {
Speak (trigger: boolean): void;
}
// implements 实现接口
class Boy implements Person {
Speak (mouth: boolean) { }
}
// class Girl implements Person, Teenagers 和 class Girl implements PersonAndTeenagers 作用相同
class Girl implements PersonAndTeenagers {
Speak (mouth: boolean) { }
Young (sge: number) { }
}
enum枚举
枚举成员会被赋值为从 0
开始递增的数字,同时也会对枚举值到枚举名进行反向映射:
enum Color {
red = 'red',
blue = 'blue',
yellow = 'yellow',
green = 'green'
}
// 常量枚举
const enum Color {
red = 'red',
blue = 'blue',
yellow = 'yellow',
green = 'green'
}
console.log(Color.red) // 0
// 反向映射
console.log(Color[0]) // red
const value = 'red'
if (value === Color.red) {
console.log('Go Go Go ')
常量枚举经过编译后形成的js文件如下:
非常量枚举经过编译器编译之后的js文件如下:
Generics 泛型
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
约束泛型
类与泛型
接口与泛
示例代码如下:
function echo (arg) {
return arg
}
let result1 = echo(123) // 参数传递123后result1 的类型为any
// 泛型
function echo2<T> (arg: T): T {
return arg
}
let result2 = echo2(123) // 加上泛型之后 参数传递 123后result2的类型为number
function swap<T, U> (tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]]
}
console.log(swap(['hero', 123]))//[ 123, 'hero' ]
// 约束泛型
interface IWithLength {
length: number
}
function echoWithLength<T extends IWithLength> (arg: T): T {
console.log(arg.length)
return arg
}
const str = echoWithLength('123')
const obj = echoWithLength({ length: 3, name: 'Tom' })
const arr = echoWithLength([1, 2, 3, 4])
// 类与泛型
class Queue<T> {
private data = []
push (item: T) {
return this.data.push(item)
}
pop (): T {
return this.data.shift()
}
}
const queue = new Queue<number>()
queue.push(1)
console.log(queue.pop().toFixed())// 1
// 接口与泛型
interface KeyPair<T, U> {
key: T
value: U
}
let kp1: KeyPair<string, number> = { key: 'str', value: 123 }
let kp2: KeyPair<number, string> = { key: 123, value: 'str' }
let arr2: Array<string> = ['1', '2'] // 使用 Array<string> 等价于 interface Array<T>
类型别名 type-alias
类型别名,就是给类型起一个别名,让它可以更方便的被重用。
let sum: (x: number, y: string) => number
const result1 = sum(1, '2')
// 将(x: number, y: string) => number类型取一个别名 为 PlusType
type PlusType = (x: number, y: string) => number
let sum2: PlusType
const result2 = sum2(2, '2')
type StrOrNum = string | number
let result3: StrOrNum = 123
result3 = '123'
字面量
let Name: 'name' = 'name'
// Name = '123' //报错信息:不能将类型“"123"”分配给类型“"name"”
let age: 19 = 19
type Directions = 'Up' | 'Down' | 'Left' | 'Right'
let up: Directions = 'Up'
交叉类型
// 交叉类型 使用 ‘&’ 符号进行类型的扩展
interface IName {
name: string
}
type IPerson = IName & { age: number }
let person: IPerson = { name: 'Tom', age: 19 }
内置类型
全局对象
// global objects 全局对象
const a: Array<string> = ['123', '456']
const time = new Date()
time.getTime()
const reg = /abc/ // 此时reg为RegExp类型
reg.test('abc')
build-in object 内置对象
Math.pow(2, 2) //返回 2 的 2次幂。
console.log(Math.pow(2, 2)) // 4DOM and BOM
// document 对象,返回的是一个 HTMLElement
let body: HTMLElement = document.body
// document 上面的query 方法,返回的是一个 nodeList 类型
let allLis = document.querySelectorAll('li')
//当然添加事件也是很重要的一部分,document 上面有 addEventListener 方法,注意这个回调函数,因为类型推断,这里面的 e 事件对象也自动获得了类型,这里是个 mouseEvent 类型,因为点击是一个鼠标事件,现在我们可以方便的使用 e 上面的方法和属性。
document.addEventListener('click', (e) => {
e.preventDefault()
})utility types实用类型
interface IPerson2 {
name: string
age: number
}
let viking: IPerson2 = { name: 'viking', age: 20 }
// partial,它可以把传入的类型都变成可选
type IPartial = Partial<IPerson>
let viking2: IPartial = {} // Partial 将IPerson 中的类型变成了可选类型 所以 viking2 可以等于一个空对象
// Omit,它返回的类型可以忽略传入类型的某个属性
type IOmit = Omit<IPerson2, 'name'> // 忽略name属性
let viking3: IOmit = { age: 20 }
如果加上name属性将会报错:不能将类型“{ age: number; name: string; }”分配给类型“IOmit”。对象文字可以只指定已知属性,并且“name”不在类型“IOmit”中。
作者:不一213
来源:https://juejin.cn/post/7035563509882552334
神奇的交叉观察器 - IntersectionObserver
1. 背景
网页开发时,不管是在移动端,还是PC端,都有个很重要的概念,叫做动态懒加载,适用于一些图片资源(或者数据)特别多的场景中,这个时候,我们常常需要了解某个元素是否进入了“视口”(viewport),即用户能不能看到它。
传统的实现方法是,监听到scroll
事件或者使用setInterval
来判断,调用目标元素的getBoundingClientRect()
方法,得到它对应于视口左上角的坐标,再判断是否在视口之内。这种方法的缺点是,由于scroll
事件触发频率高,计算量很大,如果不做防抖节流的话,很容易造成性能问题,而setInterval
由于其有间歇期,也会出现体验问题。
所以在几年前,Chrome率先提供了一个新的API
,就是IntersectionObserver
,它可以用来自动监听元素是否进入了设备的可视区域之内,而不需要频繁的计算来做这个判断。由于可见(visible)的本质是,目标元素与视口产生一个交叉区,所以这个 API 叫做”交叉观察器”。
2. 兼容性
由于这个api问世已经很多年了,所以对浏览器的支持性还是不错的,完全可以上生产环境,点击这里
可以看看当前浏览器对于IntersectionObserver
的支持性:
3. 用法
该API
的调用非常简单:
const io = new IntersectionObserver(callback, option);
上面代码中,IntersectionObserver
是浏览器原生提供的构造函数,接受两个参数:
callback
:可见性发现变化时的回调函数option
:配置对象(可选)。
构造函数的返回值是一个观察器实例。实例一共有4个方法:
observe
:开始监听特定元素unobserve
:停止监听特定元素disconnect
:关闭监听工作takeRecords
:返回所有观察目标的对象数组
3.1 observe
该方法需要接收一个target参数,值是Element类型,用来指定被监听的目标元素
// 获取元素
const target = document.getElementById("dom");
// 开始观察
io.observe(target);
3.2 unobserve
该方法需要接收一个target参数,值是Element类型,用来指定停止监听的目标元素
// 获取元素
const target = document.getElementById("dom");
// 停止观察
io.unobserve(target);
3.3 disconnect
该方法不需要接收参数,用来关闭观察器
// 关闭观察器
io.disconnect();
3.4 takeRecords
该方法不需要接收参数,返回所有被观察的对象,返回值是一个数组
// 获取被观察元素
const observerList = io.takeRecords();
注意:
observe
方法的参数是一个 DOM 节点,如果需要观察多个节点,就要多次调用这个方法:
// 开始观察多个元素
io.observe(domA);
io.observe(domB);
io.observe(domC);
4. callback 参数
目标元素的可见性变化时,就会调用观察器的回调函数callback
。
callback
一般会触发两次。一次是目标元素刚刚进入视口,另一次是完全离开视口。
const io = new IntersectionObserver((changes, observer) => {
console.log(changes);
console.log(observer);
});
上面代码中,callback
函数的参数接收两个参数changes
和observer
:
changes
:这是一个数组,每个成员都是一个被观察对象。举例来说,如果同时有两个被观察的对象的可见性发生变化,那么changes
数组里面就会打印出两个元素,如果只观察一个元素,我们打印changes[0]
就能获取到被观察对象observer
: 这是一个对象,返回我们在实例中传入的第二个参数option(如果没传,则返回默认值)
5. IntersectionObserverEntry 对象
上面提到的changes数组中的每一项都是一个IntersectionObserverEntry 对象(下文简称io对象),对象提供目标元素的信息,一共有八个属性,我们打印这个对象:
// 创建实例
const io = new IntersectionObserver(changes => {
changes.forEach(change => {
console.log(change);
});
});
// 获取元素
const target = document.getElementById("dom");
// 开始监听
io.observe(target);
运行上面代码,并且改变dom的可见性,这时控制台可以看到一个对象:
每个属性的含义如下:
boundingClientRect
:目标元素的矩形区域的信息intersectionRatio
:目标元素的可见比例,即intersectionRect
占boundingClientRect
的比例,完全可见时为1
,完全不可见时小于等于0
intersectionRect
:目标元素与视口(或根元素)的交叉区域的信息isIntersecting
: 布尔值,目标元素与交集观察者的根节点是否相交isVisible
: 布尔值,目标元素是否可见(该属性还在试验阶段,不建议在生产环境中使用)rootBounds
:根元素的矩形区域的信息,getBoundingClientRect()
方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回null
target
:被观察的目标元素,是一个 DOM 节点对象time
:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒
6. 应用
- 预加载(滚动加载,翻页加载,无限加载)
- 懒加载(后加载、惰性加载)
- 其它
7. 注意点
IntersectionObserver API 是异步的,不随着目标元素的滚动同步触发。
规格写明,IntersectionObserver
的实现,应该采用requestIdleCallback()
,即只有线程空闲下来,才会执行观察器。这意味着,这个观察器的优先级非常低,只在其他任务执行完,浏览器有了空闲才会执行。
8. 参考链接
作者:三年没洗澡
来源:https://juejin.cn/post/7035490578015977480
js打包时间缩短90%,bundleless生产环境实践总结
最近尝试将bundleless的构建结果直接用到了线上生产环境,因为bundleless只会编译代码,不会打包,因此构建速度极快,同比bundle模式时间缩短了90%以上。得益于大部分浏览器都已经支持了http2和浏览器的es module,对于我们没有强兼容场景的中后台系统,将bundleless构建结果应用于线上是有可能的。本文主要介绍一下,本人在使用bundleless构建工具实践中遇到的问题。
起源
结合snowpack实践
snowpack的Streaming Imports
性能比较
总结
附录snowpack和vite的对比
本文原文来自我的博客: github.com/fortheallli…
一、起源
1.1 从http2谈起
以前因为http1.x不支持多路服用, HTTP 1.x 中,如果想并发多个请求,必须使用多个 TCP 链接,且浏览器为了控制资源,还会对单个域名有 6-8个的TCP链接请求限制.因此我们需要做的就是将同域的一些静态资源比如js等,做一个资源合并,将多次请求不同的js文件,合并成单次请求一个合并后的大js文件。这就是webpack等bundle工具的由来。
而从http2开始,实现了TCP链接的多路复用,因此同域名下不再有请求并发数的限制,我们可以同时请求同域名的多个资源,这个并发数可以很大,比如并发10,50,100个请求同时去请求同一个服务下的多个资源。
因为http2实现了多路复用,因此一定程度上,将多个静态文件打包到一起,从而减少请求次数,就不是必须的
主流浏览器对http2的支持情况如下:
除了IE以外,大部分浏览器对http2的支持性都很好,因为我的项目不需要兼容IE,同时也不需要兼容低版本浏览器,不需要考虑不支持http2的场景。(这也是我们能将不bundle的代码用于线上生产环境的前提之一)
1.2 浏览器esm
对于es modules,我们并不陌生,什么是es modules也不是本文的重点,一些流行的打包构建工具比如babel、webpack等早就支持es modules。
我们来看一个最简单的es modules的写法:
//main.js
import a from 'a.js'
console.log(a)
//a.js
export let a = 1
上述的es modules就是我们经常在项目中使用的es modules,这种es modules,在支持es6的浏览器中是可以直接使用的。
我们来举一个例子,直接在浏览器中使用es modules
<html lang="en">
<body>
<div id="container">my name is {name}</div>
<script type="module">
import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.esm.browser.js'
new Vue({
el: '#container',
data:{
name: 'Bob'
}
})
</script>
</body>
</html>
上述的代码中我们直接可以运行,我们根据script的type="module"可以判断浏览器支不支持es modules,如果不支持,该script里面的内容就不会运行。
首先我们来看主流浏览器对于ES modules的支持情况:
从上图可以看出来,主流的Edge, Chrome, Safari, and Firefox (+60)等浏览器都已经开始支持es modules。
同样的因为我们的中后台项目不需要强兼容,因此不需要兼容不支持esm的浏览器(这也是我们能将不bundle的代码用于线上生产环境的前提之二)。
1.3 小结
浏览器对于http2和esm的支持,使得我们可以减少模块的合并,以及减少对于js模块化的处理。
如果浏览器支持http2,那么一定程度上,我们不需要合并静态资源
如果浏览器支持esm,那么我们就不需要通过构建工具去维护复杂的模块依赖和加载关系。
这两点正是webpack等打包工具在bundle的时候所做的事情。浏览器对于http2和esm的支持使得我们减少bundle代码的场景。
二、结合snowpack实践
我们比较了snowpack和vite,最后选择采用了snowpack(选型的原因以及snowpack和vite的对比看最后附录),本章节讲讲如何结合snowpack构建工具,构建出不打包形式的线上代码。
2.1 snowpack的基础用法
我们的中后台项目是react和typescript编写的,我们可以直接使用snowpack相应的模版:
npx create-snowpack-app myproject --template @snowpack/app-template-react-typescript
snowpack构建工具内置了tsc,可以处理tsx等后缀的文件。上述就完成了项目初始化。
2.2 前端路由处理
前端路由我们直接使用react-router或者vue-router等,需要注意的时,如果是在开发环境,那么必须要指定在snowpack.config.mjs配置文件,在刷新时让匹配到前端路由:
snowpack.config.mjs
...
routes: [{ match: 'routes', src: '.*', dest: '/index.html' }],
...
类似的配置跟webpack devserver等一样,使其在后端路由404的时候,获取前端静态文件,从而执行前端路由匹配。
2.3 css、jpg等模块的处理
在snowpack中同样也自带了对css和image等文件的处理。
css
以sass为例,
snowpack.config.mjs
plugins: [
'@snowpack/plugin-sass',
{
/* see options below */
},
],
只需要在配置中增加一个sass插件就能让snowpack支持sass文件,此外,snowpack也同样支持css module。.module.css或者.module.scss命名的文件就默认开启了css module。此外,css最后的结果都是通过编译成js模块,通过动态创建style标签,插入到body中的。
//index.module.css文件
.container{
padding: 20px;
}
snowpack构建处理后的css.proxy.js文件为:
export let code = "._container_24xje_1 {\n padding: 20px;\n}";
let json = {"container":"_container_24xje_1"};
export default json;
// [snowpack] add styles to the page (skip if no document exists)
if (typeof document !== 'undefined') {
const styleEl = document.createElement("style");
const codeEl = document.createTextNode(code);
styleEl.type = 'text/css';
styleEl.appendChild(codeEl);
document.head.appendChild(styleEl);
}
上述的例子中我们可以看到。最后css的构建结果是一段js代码。在body中动态插入了style标签,就可以让原始的css样式在系统中生效。
jpg,png,svg等
如果处理的是图片类型,那么snowpack同样会将图片编译成js.
//logo.svg.proxy.js
export default "../dist/assets/logo.svg";
snowpack没有对图片做任何的处理,只是把图片的地址,包含到了一个js模块文件导出地址中。值得注意的是在浏览器es module中,import 动作类似一个get请求,import from可以是一个图片地址,浏览器es module自身可以处理图片等形式。因此在.js文件结尾的模块中,export 的可以是一个图片。
snowpack3.5.0以下的版本在使用css module的时候会丢失hash,需要升级到最新版本。
2.4 按需加载处理
snowpack默认是不打包的。只对每一个文件都做一些简单的模块处理(将非js模块转化成js模块)和语法处理,因此天然支持按需加载,snowpack支持React.lazy的写法,在react的项目中,只要正常使用React.Lazy就能实现按需加载。
2.5 文件hash处理
在最后构建完成后,在发布构建结果的时候,为了处理缓存,常见的就是跟静态文件增加hash,snowpack也提供了插件机制,插件会处理snowpack构建前的所有文件的内容,做为content转入到插件中,经过插件的处理转换后得到新的content.
可以通过snowpack-files-hash插件来实现给文件增加hash。
2.6 公用esm模块托管
snowpack对于项目构建的bundleless的代码可以直接跑在线上,在bundless的构建结果中,我们想进一步减少构建结果文件大小。以bundleless的方式构建的代码,默认在处理三方npm包依赖的时候,虽然不会打包,snowpack对项目中node_modules中的依赖重新编译成esm形式,然后放在一个新的静态目录下。因此最后构建的代码包含了两个部分:
项目本身的代码,将node_modules中的依赖处理成esm后的静态文件。
其中node_modules中的依赖处理成esm后的静态文件,可以以cdn或者其他服务形式来托管。这样我们每次都不需要在构建的时候处理node_modules中的依赖。在项目本身的代码中,如果引用了npm包,只需要将其指向一个cdn地址即可。这样处理后的,构建的代码就变成:
只有项目本身的代码(项目中对于三方插件的引入,直接使用三方插件的cdn地址)。
进一步想,如果我们使用了托管所有npm包(es module形式)的cdn地址之后,那么在本地开发或者线上构建的过程中,我们甚至不需要去维护本地的node_modules目录,以及yarn-lock或者package-lock文件。我们需要做的,仅仅是一个map文件进行版本管理。保存项目中的npm包名和该包相对应的cdn地址。
比如:
//config.map.json
{
"react": "https://cdn.skypack.dev/react@17.0.2",
"react-dom": "https://cdn.skypack.dev/react-dom@17.0.2",
}
通过这个map文件,不管是在开发还是线上,只要把:
import React from 'react'
替换成
import React from "https://cdn.skypack.dev/react@17.0.2"
就能让代码在开发环境或者生产环境中跑起来。如此简化之后,我们不论在开发环境还是生产环境都不需要在本地维护node_modules相关的文件,进一步可以减少打包时间。同时包管理也更加清晰,仅仅是一个简单的json文件,一对固定意义的key/value,简单纯粹。
我们提到了一个托管了的npm包的有es module形式的cdn服务,上述以skypack为例,这对比托管了npm包cjs形式的cdn服务unpkg,两者的区别就是,unpkg所托管的npm包,大部分是cjs形式的,cjs形式的npm包,是不能直接用于浏览器的esm中的。skypack所做的事情就是将大部分npm包从cjs形式转化成esm的形式,然后存储和托管esm形式的结果。
三、snowpack的Streaming Imports
在2.7中我们提到了在dev开发环境使用了skypack,那么本地不需要node_modules,甚至不需要yarn-lock和package-lock等文件,只需要一个json文件,简单的、纯粹的,只有一对固定意义的key/value。在snowpack3.x就提供了这么一个功能,称之为Streaming Imports。
3.1 snowpack和skypack
在snowpack3.x在dev环境支持skypack:
// snowpack.config.mjs
export default {
packageOptions: {
source: 'remote',
},
};
如此,在dev的webserver过程中,就是直接下载skypack中相应的esm形式的npm包,放在最后的结果中,而不需要在本地做一个cjs到esm的转换。这样做有几点好处:
速度快: 不需要npm install一个npm包,然后在对其进行build转化成esm,Streaming Imports可以直接从一个cdn地址直接下载esm形式的依赖
安全:业务代码中不需要处理公共npm包cjs到esm的转化,业务代码和三方依赖分离,三方依赖交给skypack处理
3.2 依赖控制
Streaming Imports自身也实现了一套简单的依赖管理,有点类似go mod。是通过一个叫snowpack.deps.json文件来实现的。跟我们在2.7中提到的一样,如果使用托管cdn,那么本地的pack-lock和yarn-lock,甚至node_modules是不需要存在的,只需要一个简单纯粹的json文件,而snowpack中就是通过snowpack.deps.json来实现包的依赖管理的。
我们安装一个npm包时,我们以安装ramda为例:
npx snowpack ramda
在snowpack.deps.json中会生成:
{
"dependencies": {
"ramda": "^0.27.1",
},
"lock": {
"ramda#^0.27.1": "ramda@v0.27.1-3ePaNsppsnXYRcaNcaWn",
}
}
安装过程的命令行如下所示:
从上图可以看出来,通过npx snowpack安装的依赖是从skypack cdn直接请求的。
特别的,如果项目需要支持typescript,那么我们需要将相应的npm包的声明文件types下载到本地,skypack同样也支持声明文件的下载,只需要在snowpack的配置文件中增加:
// snowpack.config.mjs
export default {
packageOptions: {
source: 'remote',
types:true //增加type=true
},
};
snowpack会把types文件下载到本地的.snowpack目录下,因此在tsc编译的时候需要指定types的查找路径,在tsconfig.json中增加:
//tsconfig.json
"paths": {
"*":[".snowpack/types/*"]
},
3.3 build环境
snowpack的Streaming Imports,在dev可以正常工作,dev的webserver中在请求npm包的时候会将请求代理到skypack,但是在build环境的时候,还是需要其他处理的,在我们的项目中,在build的时候可以用一个插件snowpack-plugin-skypack-replacer,将build后的代码引入npm包的时候,指向skypack。
build后的线上代码举例如下:
import * as __SNOWPACK_ENV__ from '../_snowpack/env.271340c8a413.js';
import.meta.env = __SNOWPACK_ENV__;
import ReactDOM from "https://cdn.skypack.dev/react-dom@^17.0.2";
import App from "./App.e1841499eb35.js";
import React from "https://cdn.skypack.dev/react@^17.0.2";
import "./index.css.proxy.9c7da16f4b6e.js";
const start = async () => {
await ReactDOM.render(/* @__PURE__ */ React.createElement(App, null), document.getElementById("root"));
};
start();
if (undefined /* [snowpack] import.meta.hot */ ) {
undefined /* [snowpack] import.meta.hot */ .accept();
}
从上述可以看出,build之后的代码,通过插件将:
import React from 'react'
//替换成了
import React from "https://cdn.skypack.dev/react@^17.0.2";
四、性能比较
4.1 lighthouse对比
简单的使用lighthouse来对比bundleless和bundle两种不同构建方式网页的性能。
bundleless的前端简单性能测试:
bundle的前端性能测试:
对比发现,这里两个网站都是同一套代码,相同的部署环境,一套是构建的时候是bundleless,利用浏览器的esm,另一个是传统的bundle模式,发现性能上并没有明显的区别,至少bundleless简单的性能测试方面没有明显差距。
4.2构建时间对比
bundleless构建用于线上,主要是减少了构建的时间,我们传统的bundle的代码,一次编译打包等可能需要几分钟甚至十几分钟。在我的项目中,bundleless的构建只需要4秒。
同一个项目,用webpack构建bundle的情况下需要60秒左右。
4.3构建产物体积对比
bundleless构建出的产物,一般来说也只有bundle情况下的1/10.这里不一一举例。
五、总结
在没有强兼容性的场景,特别是中后台系统,bundleless的代码直接跑在线上,是一种可以尝试的方案,上线的时间会缩短90%,不过也有一些问题需要解决,首先需要保证托管esm资源的CDN服务的稳定性,且要保障被托管的esm资源在浏览器运行不会出现异常。我们运行了一些常见的npm包,发现并没有异常情况,不过后续需要更多的测试。
六、附录:snowpack和vite的对比
6.1 相同点
snowpack和vite都是bundleless的构建工具,都利用了浏览器的es module来减少对静态文件的打包,从而减少热更新的时间,从而提高开发体验。原理都是将本地安装的依赖重新编译成esm形式,然后放在本地服务的静态目录下。snowpack和vite有很多相似点
在dev环境都将本地的依赖进行二次处理,对于本地node_module目录下的npm包,通过其他构建工具转换成esm。然后将所有转换后的esm文件放在本地服务的静态目录下
都支持css、png等等静态文件,不需要安装其他插件。特别对于css,都默认支持css module
默认都支持jsx,tsx,ts等扩展名的文件
框架无关,都支持react、vue等主流前端框架,不过vite对于vue的支持性是最好的。
6.2 不同点
dev构建: snowpack和vite其实大同小异,在dev环境都可以将本地node_modules中npm包,通过esinstall等编译到本地server的静态目录。不同的是在dev环境
snowpack是通过rollup来将node_modules的包,重新进行esm形式的编译
vite则是通过esbuild来将node_modules的包,重新进行esm形式的编译
因此dev开发环境来看,vite的速度要相对快一些,因为一个npm包只会重新编译一次,因此dev环境速度影响不大,只是在初始化项目冷启动的时候时间有一些误差,此外snowpack支持Streaming Imports,可以在dev环境直接用托管在cdn上的esm形式的npm包,因此dev环境性能差别不大。
build构建:
在生产环境build的时候,vite是不支持unbundle的,在bundle模式下,vite选择采用的是rollup,通过rollup来打包出线上环境运行的静态文件。vite官方支持且仅支持rollup,这样一定程度上可以保持一致性,但是不容易解耦,从而结合非rollup构建工具来打包。而snowpack默认就是unbundle的,这种unbundle的默认形式,对构建工具就没有要求,对于线上环境,即可以使用rollup,也可以使用webpack,甚至可以选择不打包,直接使用unbundle。
可以用两个表格来总结如上的结论:
dev开发环境:
产品 | dev环境构建工具 |
---|---|
snowpack | rollup(或者使用Streaming imports) |
vite | esbuild |
build生产环境:
产品 | build构建工具 |
---|---|
snowpack | 1.unbundle(esbuild) 2.rollup 3.webpack... |
vite | rollup(且不支持unbundle) |
6.3 snowpack支持Streaming Imports
Streaming Imports是一个新特性,他允许用户,不管是生产环境还是开发环境,都不需要在本地使用npm/yarn来维护一个lock文件,从而下载应用中所使用的npm包到本地的node_module目录下。通过使用Streaming Imports,可以维护一个map文件,该map文件中的key是包名,value直接指向托管该npm包esm文件形式的cdn服务器的地址。
6.4 vite的一些优点
vite相对于snowpack有一下几个优点,不过个人以为都不算特别有用的一些优点。
多页面支持,除了根目录的root/index.html外,还支持其他根目录以外的页面,比如nest/index.html
对于css预处理器支持更好(这点个人没发现)
支持css代码的code-splitting
优化了异步请求多个chunk文件(不分场景可以同步进行,从而一定程度下减少请求总时间)
6.5 总结
如果想在生产环境使用unbundle,那么vite是不行的,vite对于线上环境的build,是必须要打包的。vite优化的只是开发环境。而snowpack默认就是unbundle的,因此可以作为前提在生产环境使用unbundle.此外,snowpack的Streaming Imports提供了一套完整的本地map的包管理,不需要将npm包安装到本地,很方便我们在线上和线下使用cdn托管公共库。
作者:yuxiaoliang
来源:https://juejin.cn/post/7034484346874986533
重新审视前端模块的调用, 执行和加载之间的关系
在进入正题之前, 让我们先回顾下前端模块从无到有的一个简短历史
如果你有一定的工作经验, 并且经历过 jQuery 那样的年代, 应该了解早期的前端模块, 只是 window 上的一个局部变量.
在最初的时候前端工程师为了分享自己的代码, 往往会通过 window 来建立联系, 这种古老的做法至今还被很多人使用, 因为简单. 例如我们编写了一个脚本, 通常我们并不认为这是个模块, 但我们会习惯于将这个脚本包装成一个对象. 例如
window.myModule = {
getName(name){
return `hello ${name}`
}
}
当其他人加载这个脚本后, 就可以便捷的通过 window.myModule 来调用 getName 方法.
早期的 JavaScript 脚本主要用于开发一些简单的表单和网页的交互功能, 那个年代的前端工程师数量也极少, 通过 window 来实现模块化并没有什么太大的问题.
直到 ajax 的出现, 将 web 逐步推动到了富客户端的阶段, 随着 spa 的兴起, 前端工程师发现使用 window 模块化代码越来越难以维护, 主要原因有 2 个
- 大量的模块加载污染了 window, 导致各种命名冲突和意外覆盖, 这些问题还很难定位.
- 模块和模块之间的交互越来越多, 为了保证调用顺序, 需要人为保障 script 标签的加载顺序
为了解决这个问题, 类似 require seajs 这样的模块 loader 被创造出来, 通过模块 loader, 大大缓解了上述的两个问题.
但前端技术和互联网发展的速度远超我们的想象, 随着网页越来越像一个真实的客户端, 这对前端的工程能力提出了极大的挑战, 仅靠单纯的脚本开发已经难以满足项目的需要, 于是 gulp 等用于前端工程管理的脚手架开始进入我们的视野, 不过在这个阶段, 模块 loader 和前端工程流之间尚未有机的结合.
直到 nodejs 问世, 前端拥有了自己的包管理工具 npm, 在此基础上 Webpack 进一步推动了前端工程流和模块之间的整合, 随后前端模块化的进程开始稳固下来, 一直保持至今.
从这个历史上去回顾, 前端模块化的整个进程包括 es6 关于 module 的标准都是一直围绕这个一个核心命题存在的.
无论是 require 还是 Webpack 在这个核心命题上并没有区别, 即前端模块遵循
加载 → 调用 → 执行 这样的一个逻辑关系. 因为模块必须先加载才能调用并执行, 模块加载器和构建工具就必须管理和分析应用中所有模块的依赖关系, 从而确定哪些模块可以拆分哪些可以合并, 以及模块的加载顺序.
但是随着时间的推移, 前端应用的模块越来越多, 应用越来越庞大, 我们的本地的 node_modules 几百兆起步, Webpack 虽然做了很多优化, 但是 rebuild 的时间在大型应用面前依然显得很慢.
今年 2 月份, Webpack 5 发布了他们的模块拆解方案, 模块联邦, 这个插件解决了 Webpack 构建的模块无法在多个工程中复用的问题.
早些时间 yarn 2.0 采用共享 node_moudles 的方法来解决本地模块大量冗余导致的性能问题.
包括 nodejs 作者在 deno 中放弃了 npm 改用网络化加载模块的方式等等.
可以看到社区已经意识到了先行的前端模块化机制再次面临瓶颈, 无论是性能还是维护成本都面临诸多挑战, 各个团队都在想办法开辟一个新的方向.
不过这些努力依然没有超越先行模块化机制中的核心命题, 即模块必须先加载, 后调用执行.
只要这个核心命题不变, 模块的依赖问题依然是无解的. 为此我们尝试提出了一种新的思路
模块为什么不能先调用, 后加载执行呢?
如果 A 模块调用 B 模块, 但并不需要 B 模块立即就绪, 这就意味着, 模块加载器可以不关心模块的依赖关系, 而致力于只解决模块加载的效率和性能问题.
同时对于构建工具来说, 如果 A 模块的执行并不基于 B 模块立即就绪这件事, 那么构建工具可以放心的将 A 和 B 模块拆成两个文件, 如果模块有很多, 就可以利用 http2 的并行加载能力, 大大提升模块的加载性能.
在我们的设想中, 一种新的模块加载方式是这样的
// remoteModule.js 这是一个发布到 cdn 的远程模块, 内部代码是这样
widnow.rdeco.create({
name:'remote-module',
exports:{
getName(name, next){
next(`hello ${name}`)
}
}
})
让我们先不加载这个模块, 而是直接先执行调用端的代码例如这样
window.rdeco 可以理解成类似 Webpack runtime 一样的存在, 不过 rdeco 是一个独立的库, 其功能远不止于此
// localModule.js 这个是本地的模块
window.rdeco.inject('remote-module').getName('world').then(fullName=>{
console.log(fullName)
})
然后我们在 html 中先加载 localModule.js 后加载 remoteModule.js
<scirpt src="localModule.js"></script>
<scirpt src="remoteModule.js"></script>
正常理解, localModule.js 加载完之后会试图去调用 remote-module 的 getName 方法, 但此时 remoteModule 尚未加载, 按照先行的模块化机制, 这种调用会抛出异常. 为了避免这个问题
模块构建工具需要分析两个文件的代码, 从而发现 localModule.js 依赖 remoteModule.js, 然后保存这个依赖顺序, 同时通知模块加载器, 为了让代码正常执行, 必须先加载 remoteModule.js.
但如果模块可以先调用后加载, 那么这个复杂的过程就可以完全避免. 目前我们实现了这一机制, 可以看下这个 demo: codesandbox.io/s/tender-ar…
你可试着先点击 Call remote module's getName method
按钮,
此时文案不会变化只是显示 hello, 但代码并不会抛出异常, 然后你再点击 Load remote module
按钮, 开始加载 remoteModule, 等待加载完成, getName 才会真实执行, 此时文案变成了 hello world
作者:掘金泥石流
链接:https://juejin.cn/post/7034412398261993479
收起阅读 »
CSS实现随机不规则圆角头像
前言
最近真是彻底爱上了 CSS
,我又又又被 CSS
惊艳到了,明明是简单的属性,为啥大佬们稍微一组合,就能形成如此好看的效果啊。本文
给大家带来的是随机不规则圆角头像效果,我们可以把这个效果用于一些人物的展示页面
学习本文章,你可以学到:
border-radius
实现椭圆效果border-radius
实现不规则圆角头像animation-delay
设置负值- 实现随机不规则圆角
📃 预备知识
🎨 border-radius
border-radius
可以设置外边框的圆角。比如我们经常使用的 border-radius: 50%
可以得到一个圆形头像。
但 border-radius
就只能实现圆形效果吗?当然不是,当使用一个半径是确定圆形,两个半径时则会确定椭圆形。
光说不练假把式,接下来一起试试
- 设置
border-radius: 30% 70%
,就可以得到椭圆效果
上面的设置都是针对于四个方向的,也可以只设置一个方向的圆角
- 设置 border-top-left-radius:
30% 70%
从上图其实可以得出,两个值分别设置水平半径和垂直半径的半径,为了更准确我们验证一下
但为啥设置的圆角与 border-radius: 30% 70%
设置有这么大的差距。别急,下面慢慢道来。
- 设置
border-radius: 30%/70%
,/ 前后的值分别为水平半径和垂直半径
border-radius: 30%/70%
相当于给四个方向都设置 30%/70%,而border-radius: 30% 70%
是给左上右下设置30%
,左下右上设置70%
- 设置四个方向为四种椭圆角:
border-radius: 40% 60% 60% 40% / 60% 30% 70% 40%
,就可以实现简单的不规则圆角效果,小改改的头像是不是看起来舒服了好多。
💞 animation-delay
animation-delay
: 可以定义动画播放的延迟时间。
但如果给 animation-delay
设置负值会发生什么那?
MDN 中指出: 定义一个负值会让动画立即开始。但是动画会从它的动画序列中某位置开始。例如,如果设定值为 -1s ,动画会从它的动画序列的第 1 秒位置处立即开始。
那个,乍看上去,我好像懂了,又好像没懂,咱们还是来自己试一下吧。
- 创建
div
块,宽高都为0
,背景设置为#000
- 添加
keyframe
动画,100%
状态宽高都扩展为1000px
@keyframes extend {
0% {
width: 0;
height: 0;
}
100% {
width: 1000px;
height: 1000px;
}
}
- 给
div
添加animation
和animation-delay
/* 设置 paused 可以使动画暂停 */
animation: extend 10s linear paused;
animation-delay: -3s;
当我打开浏览器时,浏览器出现 300*300
的黑色块,修改 animation-delay
为 -4s
,浏览器出现 400*400
的黑块。我们使用 linear
匀速作为动画播放函数,10s
后 div 会变为 1000px
,设置 -3s
起始为 300px
,-4s
起始为 400px
。
这样一对比,我们来把 MDN
的描述翻译一下:
+ animation-delay
设置负值的动画会立即执行
+ 动画起始位置是动画中的一阶段,比如上述案例,定义 10s
的动画,设置 -3s
动画就从 3s
开始执行
🌊 radius 配合 delay 实现
有了上面基础知识的配合,不规则圆角的实现就变得很简单了。
设置 keyframe
,keyframe
的开始与结束为两种不规则圆角,再使用 :nth-child
进行自然随机设置 animation-delay
的负值延迟时间,就可以得到一组风格各异的不规则圆角效果
自然随机的算法非常有意思,效果开创者为了更好、更自然的随机性,选取序列为 2n+1 3n+2 5n+3 7n+4 11+5 ...
- 设置
keyframe
动画
@keyframes morph {
0% {
border-radius: 40% 60% 60% 40% / 60% 30% 70% 40%;
transform: rotate(-5deg);
}
100% {
border-radius: 40% 60%;
transform: rotate(5deg);
}
}
- 自然随机设置每个头像的
delay
.avatar:nth-child(n) {
animation-delay: -3.5s;
}
.avatar:nth-child(2n + 1) {
animation-delay: -1s;
}
.avatar:nth-child(3n + 2) {
animation-delay: -2s;
}
.avatar:nth-child(5n + 3) {
animation-delay: -3s;
}
.avatar:nth-child(7n + 5) {
animation-delay: -4s;
}
.avatar:nth-child(11n + 7) {
animation-delay: -5s;
}
当当当当~~~ 效果就实现了! 看着下面这些风格各异的小改改,瞬间心情舒畅了好多。
不规则圆角头像的功能实现了,但总感觉缺点什么?如果头像能有点动态效果就更好了。
例如 hover
时,头像圆角会发生变化,用户的体验会更好。
我首先的想法还是在上面的代码基础上面更改,但由于 @keyframe
定义好了终点时的状态,能变化的效果并不多,而且看起来很单调,显得很呆 🤣。
那有没有好的实现方案那?有,最终我找到了张鑫旭大佬的实现方案,大佬还是大佬啊。
🌟 radius 配合 transition 实现
参考博客: “蝉原则”与CSS3随机多背景随机圆角等效果
- 按照自然随机给每个头像赋予不同的不规则圆角
/* 举两个例子 */
.list:hover {
border-radius: 95% 70% 100% 80%;
transform: rotate(-2deg);
}
.list:nth-child(2n+1) {
border-radius: 59% 52% 56% 59%;
transform: rotate(-6deg);
}
- 设置
hover
时新的不规则圆角
.list:nth-child(2n+1):hover {
border-radius: 51% 67% 56% 64%;
transform: rotate(-4deg);
}
.list:nth-child(3n+2):hover {
border-radius: 69% 64% 53% 70%;
transform: rotate(0deg);
}
- 给
list
元素配置transition
完成上面的步骤,我们就可以得到更灵动的小改改头像了。
但这种实现方法相比较于
radius
配合animation-delay
实现具备一定的难点,需要设计多种好看的不规则圆角效果
🛕 源码仓库
传送门: 随机不规则圆角
作者:战场小包
链接:https://juejin.cn/post/7034396555738251301
收起阅读 »
使用 Promise 时的5个常见错误,你占了几个!
Promise 提供了一种优雅的方法来处理 JS 中的异步操作。这也是避免“回调地狱”的解决方案。然而,并没有多少开发人员了解其中的内容。因此,许多人在实践中往往会犯错误。
在本文中,介绍一下使用 promise 时的五个常见错误,希望大家能够避免这些错误。
1.避免 Promise 地狱
通常,Promise是用来避免回调地狱。但滥用它们也会导致 Promise是地狱。
userLogin('user').then(function(user){
getArticle(user).then(function(articles){
showArticle(articles).then(function(){
//Your code goes here...
});
});
});
在上面的例子中,我们对 userLogin
、getararticle
和 showararticle
嵌套了三个promise。这样复杂性将按代码行比例增长,它可能变得不可读。
为了避免这种情况,我们需要解除代码的嵌套,从第一个 then
中返回 getArticle
,然后在第二个 then
中处理它。
userLogin('user')
.then(getArticle)
.then(showArticle)
.then(function(){
//Your code goes here...
});
2. 在 Promise 中使用 try/catch
块
通常情况下,我们使用 try/catch
块来处理错误。然而,不建议在 Promise
对象中使用try/catch
。
这是因为如果有任何错误,Promise对象会在 catch
内自动处理。
ew Promise((resolve, reject) => {
try {
const data = doThis();
// do something
resolve();
} catch (e) {
reject(e);
}
})
.then(data => console.log(data))
.catch(error => console.log(error));
在上面的例子中,我们在Promise 内使用了 try/catch
块。
但是,Promise本身会在其作用域内捕捉所有的错误(甚至是打字错误),而不需要 try/catch
块。它确保在执行过程中抛出的所有异常都被获取并转换为被拒绝的 Promise。
new Promise((resolve, reject) => {
const data = doThis();
// do something
resolve()
})
.then(data => console.log(data))
.catch(error => console.log(error));
**注意:**在 Promise 块中使用 .catch()
块是至关重要的。否则,你的测试案例可能会失败,而且应用程序在生产阶段可能会崩溃。
3. 在 Promise 块内使用异步函数
Async/Await
是一种更高级的语法,用于处理同步代码中的多个Promise。当我们在一个函数声明前使用 async
关键字时,它会返回一个 Promise,我们可以使用 await
关键字来停止代码,直到我们正在等待的Promise解决或拒绝。
但是,当你把一个 Async 函数放在一个 Promise 块里面时,会有一些副作用。
假设我们想在Promise 块中做一个异步操作,所以使用了 async
关键字,但,不巧的是我们的代码抛出了一个错误。
这样,即使使用 catch()
块或在 try/catch
块内等待你的Promise,我们也不能立即处理这个错误。请看下面的例子。
// 此代码无法处理错误
new Promise(async () => {
throw new Error('message');
}).catch(e => console.log(e.message));
(async () => {
try {
await new Promise(async () => {
throw new Error('message');
});
} catch (e) {
console.log(e.message);
}
})();
当我在Promise块内遇到 async
函数时,我试图将 async
逻辑保持在 Promise 块之外,以保持其同步性。10次中有9次都能成功。
然而,在某些情况下,可能需要一个 async
函数。在这种情况下,也别无选择,只能用try/catch
块来手动管理。
new Promise(async (resolve, reject) => {
try {
throw new Error('message');
} catch (error) {
reject(error);
}
}).catch(e => console.log(e.message));
//using async/await
(async () => {
try {
await new Promise(async (resolve, reject) => {
try {
throw new Error('message');
} catch (error) {
reject(error);
}
});
} catch (e) {
console.log(e.message);
}
})();
4.在创建 Promise 后立即执行 Promise 块
至于下面的代码片断,如果我们把代码片断放在调用HTTP请求的地方,它就会被立即执行。
const myPromise = new Promise(resolve => {
// code to make HTTP request
resolve(result);
});
原因是这段代码被包裹在一个Promise构造函数中。然而,有些人可能会认为只有在执行myPromise
的then
方法之后才被触发。
然而,真相并非如此。相反,当一个Promise被创建时,回调被立即执行。
这意味着在建立 myPromise
之后到达下面一行时,HTTP请求很可能已经在运行,或者至少处于调度状态。
Promises 总是急于执行过程。
但是,如果希望以后再执行 Promises,应该怎么做?如果现在不想发出HTTP请求怎么办?是否有什么神奇的机制内置于 Promises 中,使我们能够做到这一点?
答案就是使用函数。函数是一种耗时的机制。只有当开发者明确地用 ()
来调用它们时,它们才会执行。简单地定义一个函数还不能让我们得到什么。所以,让 Promise 变得懒惰的最有效方法是将其包裹在一个函数中!
const createMyPromise = () => new Promise(resolve => {
// HTTP request
resolve(result);
});
对于HTTP请求,Promise 构造函数和回调函数只有在函数被执行时才会被调用。所以现在我们有一个懒惰的Promise,只有在我们需要的时候才会执行。
5. 不一定使用 Promise.all() 方法
如果你已经工作多年,应该已经知道我在说什么了。如果有许多彼此不相关的 Promise,我们可以同时处理它们。
Promise 是并发的,但如你一个一个地等待它们,会太费时间,Promise.all()
可以节省很多时间。
记住,Promise.all() 是我们的朋友
const { promisify } = require('util');
const sleep = promisify(setTimeout);
async function f1() {
await sleep(1000);
}
async function f2() {
await sleep(2000);
}
async function f3() {
await sleep(3000);
}
(async () => {
console.time('sequential');
await f1();
await f2();
await f3();
console.timeEnd('sequential');
})();
上述代码的执行时间约为 6
秒。但如果我们用 Promise.all()
代替它,将减少执行时间。
(async () => {
console.time('concurrent');
await Promise.all([f1(), f2(), f3()]);
console.timeEnd('concurrent');
})();
总结
在这篇文章中,我们讨论了使用 Promise 时常犯的五个错误。然而,可能还有很多简单的问题需要仔细解决。
作者:前端小智
链接:https://juejin.cn/post/7034661345148534815
收起阅读 »
没想到吧!这个可可爱爱的游戏居然是用 ECharts 实现的!
前言
echarts
是一个很强大的图表库,除了我们常见的图表功能,echarts
有一个自定义图形的功能,这个功能可以让我们很简单地在画布上绘制一些非常规的图形,基于此,我们来玩一些花哨的。
下面我们来一步步实现他。
1 在坐标系中画一只会动的小鸟
首先实例化一个echart容器,再从网上找一个像素小鸟的图片,将散点图的散点形状,用自定义图片的方式改为小鸟。
const myChart = echarts.init(document.getElementById('main'));
option = {
series: [
{
name: 'bird',
type: 'scatter',
symbolSize: 50,
symbol: 'image://bird.png',
data: [
[50, 80]
],
animation: false
},
]
};
myChart.setOption(option);
要让小鸟动起来,就需要给一个向右的速度和向下的加速度,并在每一帧的场景中刷新小鸟的位置。而小鸟向上飞的动作,则可以靠角度的旋转来实现,向上飞的触发条件设置为空格事件。
option = {
series: [
{
xAxis: {
show: false,
type: 'value',
min: 0,
max: 200,
},
yAxis: {
show: false,
min: 0,
max: 100
},
name: 'bird',
type: 'scatter',
symbolSize: 50,
symbol: 'image://bird.png',
data: [
[50, 80]
],
animation: false
},
]
};
// 设置速度和加速度
let a = 0.05;
let vh = 0;
let vw = 0.5
timer = setInterval(() => {
// 小鸟位置和仰角调整
vh = vh - a;
option.series[0].data[0][1] += vh;
option.series[0].data[0][0] += vw;
option.series[0].symbolRotate = option.series[0].symbolRotate ? option.series[0].symbolRotate - 5 : 0;
// 坐标系范围调整
option.xAxis.min += vw;
option.xAxis.max += vw;
myChart.setOption(option);
}, 25);
效果如下
2 用自定义图形绘制障碍物
echarts自定义系列,渲染逻辑由开发者通过renderItem
函数实现。该函数接收两个参数params和api,params包含了当前数据信息和坐标系的信息,api是一些开发者可调用的方法集合,常用的方法有:
api.value(...),意思是取出
dataItem
中的数值。例如api.value(0)
表示取出当前dataItem
中第一个维度的数值。
api.coord(...),意思是进行坐标转换计算。例如
var point = api.coord([api.value(0), api.value(1)])
表示dataItem
中的数值转换成坐标系上的点。
api.size(...), 可以得到坐标系上一段数值范围对应的长度。
api.style(...),可以获取到
series.itemStyle
中定义的样式信息。
灵活使用上述api,就可以将用户传入的Data数据转换为自己想要的坐标系上的像素位置。
renderItem
函数返回一个echarts
中的graphic
类,可以多种图形组合成你需要的形状,graphic类型。对于我们游戏中的障碍物只需要使用矩形即可绘制出来,我们使用到下面两个类。
type: group, 组合类,可以将多个图形类组合成一个图形,子类放在children中。
type: rect, 矩形类,通过定义矩形左上角坐标点,和矩形宽高确定图形。
// 数据项定义为[x坐标,下方水管上侧y坐标, 上方水管下侧y坐标]
data: [
[150, 50, 80],
...
]
renderItem: function (params, api) {
// 获取每个水管主体矩形的起始坐标点
let start1 = api.coord([api.value(0) - 10, api.value(1)]);
let start2 = api.coord([api.value(0) - 10, 100]);
// 获取两个水管头矩形的起始坐标点
let startHead1 = api.coord([api.value(0) - 12, api.value(1)]);
let startHead2 = api.coord([api.value(0) - 12, api.value(2) + 8])
// 水管头矩形的宽高
let headSize = api.size([24, 8])
// 水管头矩形的宽高
let rect = api.size([20, api.value(1)]);
let rect2 = api.size([20, 100 - api.value(2)]);
// 坐标系配置
const common = {
x: params.coordSys.x,
y: params.coordSys.y,
width: params.coordSys.width,
height: params.coordSys.height
}
// 水管形状
const rectShape = echarts.graphic.clipRectByRect(
{
x: start1[0],
y: start1[1],
width: rect[0],
height: rect[1]
},common
);
const rectShape2 = echarts.graphic.clipRectByRect(
{
x: start2[0],
y: start2[1],
width: rect2[0],
height: rect2[1]
},
common
)
// 水管头形状
const rectHeadShape = echarts.graphic.clipRectByRect(
{
x: startHead1[0],
y: startHead1[1],
width: headSize[0],
height: headSize[1]
},common
);
const rectHeadShape2 = echarts.graphic.clipRectByRect(
{
x: startHead2[0],
y: startHead2[1],
width: headSize[0],
height: headSize[1]
},common
);
// 返回一个group类,由四个矩形组成
return {
type: 'group',
children: [{
type: 'rect',
shape: rectShape,
style: {
...api.style(),
lineWidth: 1,
stroke: '#000'
}
}, {
type: 'rect',
shape: rectShape2,
style: {
...api.style(),
lineWidth: 1,
stroke: '#000'
}
},
{
type: 'rect',
shape: rectHeadShape,
style: {
...api.style(),
lineWidth: 1,
stroke: '#000'
}
},
{
type: 'rect',
shape: rectHeadShape2,
style: {
...api.style(),
lineWidth: 1,
stroke: '#000'
}
}]
};
},
颜色定义, 我们为了让水管具有光泽使用了echarts
的线性渐变色对象。
itemStyle: {
// 渐变色对象
color: {
type: 'linear',
x: 0,
y: 0,
x2: 1,
y2: 0,
colorStops: [{
offset: 0, color: '#ddf38c' // 0% 处的颜色
}, {
offset: 1, color: '#587d2a' // 100% 处的颜色
}],
global: false // 缺省为 false
},
borderWidth: 3
},
另外,用一个for循环一次性随机出多个柱子的数据
function initObstacleData() {
// 添加minHeight防止空隙太小
let minHeight = 20;
let start = 150;
obstacleData = [];
for (let index = 0; index < 50; index++) {
const height = Math.random() * 30 + minHeight;
const obstacleStart = Math.random() * (90 - minHeight);
obstacleData.push(
[
start + 50 * index,
obstacleStart,
obstacleStart + height > 100 ? 100 : obstacleStart + height
]
)
}
}
再将背景用游戏图片填充,我们就将整个游戏场景,绘制完成:
3 进行碰撞检测
由于飞行轨迹和障碍物数据都很简单,所以我们可以将碰撞逻辑简化为小鸟图片的正方形中,我们判断右上和右下角是否进入了自定义图形的范围内。
对于特定坐标下的碰撞范围,因为柱子固定每格50坐标值一个,宽度也是固定的,所以,可碰撞的横坐标范围就可以简化为 (x / 50 % 1) < 0.6
在特定范围内,依据Math.floor(x / 50)
获取到对应的数据,即可判断出两个边角坐标是否和柱子区域有重叠了。在动画帧中判断,如果重叠了,就停止动画播放,游戏结束。
// centerCoord为散点坐标点
function judgeCollision(centerCoord) {
if (centerCoord[1] < 0 || centerCoord[1] > 100) {
return false;
}
let coordList = [
[centerCoord[0] + 15, centerCoord[1] + 1],
[centerCoord[0] + 15, centerCoord[1] - 1],
]
for (let i = 0; i < 2; i++) {
const coord = coordList[i];
const index = coord[0] / 50;
if (index % 1 < 0.6 && obstacleData[Math.floor(index) - 3]) {
if (obstacleData[Math.floor(index) - 3][1] > coord[1] || obstacleData[Math.floor(index) - 3][2] < coord[1]) {
return false;
}
}
}
return false
}
function initAnimation() {
// 动画设置
timer = setInterval(() => {
// 小鸟速度和仰角调整
vh = vh - a;
option.series[0].data[0][1] += vh;
option.series[0].data[0][0] += vw;
option.series[0].symbolRotate = option.series[0].symbolRotate ? option.series[0].symbolRotate - 5 : 0;
// 坐标系范围调整
option.xAxis.min += vw;
option.xAxis.max += vw;
// 碰撞判断
const result = judgeCollision(option.series[0].data[0])
if(result) { // 产生碰撞后结束动画
endAnimation();
}
myChart.setOption(option);
}, 25);
}
总结
echarts
提供了强大的图形绘制自定义能力,要使用好这种能力,一定要理解好数据坐标点和像素坐标点之间的转换逻辑,这是将数据具象到画布上的重要一步。
运用好这个功能,再也不怕产品提出奇奇怪怪的图表需求。
作者:DevUI团队
链接:https://juejin.cn/post/7034290086111871007
收起阅读 »
学会了axios封装,世界都是你的
项目中对axios进行二次封装
随着前端技术的发展,网络请求这一块,越来越多的程序猿选择使用axios来实现网络请求。但是单纯的axios插件并不能满足我们日常的使用,因此我们使用时,需要根据项目实际的情况来对axios进行二次封装。
接下来就我对axios的二次封装详细的说说,主要包括请求之前、返回响应以及使用等。
「1、请求之前」
一般的接口都会有鉴权认证(token)之类的,因此在接口的请求头里面,我们需要带上token值以通过服务器的鉴权认证。但是如果每次请求的时候再去添加,不仅会大大的加大工作量,而且很容易出错。好在axios提供了拦截器机制,我们在请求的拦截器中可以添加token。
// 请求拦截
axios.interceptors.request.use((config) => {
//....省略代码
config.headers.x_access_token = token
return config
}, function (error) {
return Promise.reject(error)
})
当然请求拦截器中,除了处理添加token以外,还可以进行一些其他的处理,具体的根据实际需求进行处理。
「2、响应之后」
请求接口,并不是每一次请求都会成功。那么当接口请求失败的时候,我们又怎么处理呢?每次请求的时候处理?封装axios统一处理?我想一个稍微追求代码质量的码农,应该都会选择封装axios进行统一处理吧。axios不仅提供了请求的拦截器,其也提供了响应的拦截器。在此处,可以获取到服务器返回的状态码,然后根据状态码进行相对应的操作。
// 响应拦截
axios.interceptors.response.use(function (response) {
if (response.data.code === 401 ) {//用户token失效
//清空用户信息
sessionStorage.user = ''
sessionStorage.token = ''
window.location.href = '/';//返回登录页
return Promise.reject(msg)//接口Promise返回错误状态,错误信息msg可有后端返回,也可以我们自己定义一个码--信息的关系。
}
if(response.status!==200||response.data.code!==200){//接口请求失败,具体根据实际情况判断
message.error(msg);//提示错误信息
return Promise.reject(msg)//接口Promise返回错误状态
}
return response
}, function (error) {
if (axios.isCancel(error)) {
requestList.length = 0
// store.dispatch('changeGlobalState', {loading: false})
throw new axios.Cancel('cancel request')
} else {
message.error('网络请求失败,请重试')
}
return Promise.reject(error)
})
当然响应拦截器同请求拦截器一样,还可以进行一些其他的处理,具体的根据实际需求进行处理。
「3、使用axios」
axios使用的时候一般有三种方式:
执行get请求
axios.get('url',{
params:{},//接口参数
}).then(function(res){
console.log(res);//处理成功的函数 相当于success
}).catch(function(error){
console.log(error)//错误处理 相当于error
})
执行post请求
axios.post('url',{
data:xxx//参数
},{
headers:xxxx,//请求头信息
}).then(function(res){
console.log(res);//处理成功的函数 相当于success
}).catch(function(error){
console.log(error)//错误处理 相当于error
})
axios API 通过相关配置传递给axios完成请求
axios({
method:'delete',
url:'xxx',
cache:false,
params:{id:123},
headers:xxx,
})
//------------------------------------------//
axios({
method: 'post',
url: '/user/12345',
data: {
firstName: 'monkey',
lastName: 'soft'
}
});
直接使用api的方式虽然简单,但是不同请求参数的名字不一样,在实际开发过程中很容易写错或者忽略,容易为开发造成不必要的时间损失。
前面两种方式虽然没有参数不一致的问题,但是使用的时候,太过于麻烦。那么怎么办呢?
前面两种虽然使用过于麻烦,但是仔细观察,是可以发现有一定的相似点,我们便可以基于这些相似点二次封装,形成适合我们使用的一个请求函数。直接上代码:
/*
*url:请求的url
*params:请求的参数
*config:请求时的header信息
*method:请求方法
*/
const request = function ({ url, params, config, method }) {
// 如果是get请求 需要拼接参数
let str = ''
if (method === 'get' && params) {
Object.keys(params).forEach(item => {
str += `${item}=${params[item]}&`
})
}
return new Promise((resolve, reject) => {
axios[method](str ? (url + '?' + str.substring(0, str.length - 1)) : url, params, Object.assign({}, config)).then(response => {
resolve(response.data)
}, err => {
if (err.Cancel) {
} else {
reject(err)
}
}).catch(err => {
reject(err)
})
})
}
这样我们需要接口请求的时候,直接调用该函数就好了。不管什么方式请求,传参方式都一样。
作者:monkeysoft
来源:https://juejin.cn/post/6847009771606769677
大话WEB前端性能优化基本套路
前言
前端性能优化这是一个老生常谈的话题,但是还是有很多人没有真正的重视起来,或者说还没有产生这种意识。
当用户打开页面,首屏加载速度越慢,流失用户的概率就越大,在体验产品的时候性能和交互对用户的影响是最直接的,推广拉新是一门艺术,用户的留存是一门技术,拉进来留住用户,产品体验很关键,这里我以 美柚的页面为例子,用实例展开说明前端优化的基本套路(适合新手上车)。
WEB性能优化套路
基础套路1:减少资源体积
css
压缩
响应头GZIP
js
压缩
响应头GZIP
html
输出压缩
响应头GZIP
hhh
图片
压缩
使用Webp格式
cookie
注意cookie体积,合理设置过期时间
基础套路2:控制请求数
js
合并
css
合并
图片
合并
事实上
base64(常用图标:如logo等)
hhh
接口
数量控制
异步ajax
合理使用缓存机制
浏览器缓存
js编码
Require.JS 按需加载
异步加载js
lazyload图片
基础套路3:静态资源CDN
请求走CDN
html
p_w_picpath
js
css
综合套路
图片地址独立域名
与业务不同域名可以减少请求头里不必要的cookie传输
提高渲染速度
js放到页面底部,body标签底部
css放到页面顶部,head标签里
代码
代码优化:css/js/html
预加载,如:分页预加载,快滚动到底部的时候以前加载下一页数据
拓展资料
性能辅助工具
谷歌 PageSpeed Insights(网页载入速度检测工具,需要×××)
看完上面的套路介绍
可能有人会说:我在前端界混了这么多年,这些我都知道,只不过我不想去做
我答: 知道做不到,等于不知道
也可能有人会说:压缩合并等这些操作好繁琐,因为懒,所以不做
我答: 现在前端构建工具都很强大,如:grunt、gulp、webpack,支持各种插件操作,还不知道就说明你OUT了
因为我主要负责后端相关工作,前端并不是我擅长的,但是平时也喜欢关注前端前沿技术,这里以我的视角和开发经验梳理出基本套路。
套路点到为止,具体实施可以通过拓展资料进行深入了解,如有疑义或者补充请留言怼。
感谢你的支持,我会继续努力!~
作者: SFLYQ
来源:https://blog.51cto.com/sflyq/1947541
收起阅读 »WEB加载动画之彩条起伏动画
介绍
本期将带给大家一个简单的创意加载效果——彩条起伏加载。顾名思义,我们会通过scss来完成,将会制作做7个不同颜色的矩形,按不同的延迟不断的递减然后再反弹,循环往复。寓意是希望各位同学像这个加载动画一样,生活过的多姿多彩。
接下来,我们先来一睹为快吧:
感觉如何,其实这个动画的实现方案有很多,今天就用障眼法去实现它,希望给你打开书写css的新思路。
正文
1.彩条绘制
<div id="app">
<div class="loading">
<span>l</span>
<span>o</span>
<span>a</span>
<span>d</span>
<span>i</span>
<span>n</span>
<span>g</span>
</div>
</div>
结构非常的简单,我们将会在div#app让div.loading居中显示,然后在loading中平分各个距离,渲染不同的颜色。
@import url("https://fonts.googleapis.com/css?family=Baloo+Bhaijaan&display=swap");
#app{
width: 100%;
height: 100vh;
background-color: #fff;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}
.loading{
width: 350px;
height: 120px;
display: flex;
overflow: hidden;
span{
flex:1;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-family: cursive;
font-weight: bold;
text-transform: uppercase;
font-family: "Baloo Bhaijaan", cursive;
color: white;
font-size: 48px;
position: relative;
box-sizing: border-box;
padding-top: 50px;
@for $i from 1 through 7 {
&:nth-child(#{$i}) {
background: linear-gradient(180deg, hsl($i * 20 , 60%, 50%) 0, hsl($i * 20 , 60%, 90%) 100%);
box-shadow: inset 0 15px 30px hsl($i * 20 , 60%, 50%);
text-shadow: 12px 12px 12px hsl($i * 20 , 60%, 30%);
border-left: 1px solid hsl($i * 20 , 60%, 80%);;
border-right: 1px solid hsl($i * 20 , 60%, 60%);;
}
}
}
}
为了,美观我们还引入了谷歌的一个字体,居中显示是在div#app用了弹性布局。
#app{
display: flex;
justify-content: center;
align-items: center;
}
这三句话目的就是完成元素在上下左右居中。
另外,我们用scss的一大好处就是体现了出来,遍历十分的方便,即**@for
i就可以拿到下标,还可以参与运算,我们的颜色值就是通过他配合hsl色盘(HSL即色相、饱和度、亮度)去完成的。当然,色盘有360度,我们只取一部分形成清新的渐变,如果整个色盘都平分的话这几个色值出入还是太大会感觉很脏。我们发现文字被设置了padding-top: 50px,原因就是一会要完成起伏的动画,上面的部分最先消失,我们为了保证这些字母能显示时间更长一些,就往下移了一些距离。
2.起伏动画
一开始我们说过这个要用障眼法去实现,所以我们这里不改变span的高度或者裁切他。
.loading{
span{
//...
&::after{
content: "";
display: block;
box-sizing: border-box;
position: absolute;
height: 100%;
top: 0;
left: -1px;
right: -1px;
background: linear-gradient(180deg, white 0, rgb(249, 249, 249) 100%);
animation: shorten 2.1s infinite ease-out;
}
@for $i from 1 through 7 {
// ...
&::after{
animation-delay: #{ $i * 0.08s};
}
}
}
}
}
@keyframes shorten {
12% { height: 10px; }
}
看了刚才的scss代码可以发现,我们其实是通过一个绝对定位的伪类去遮挡了他,做了一个障眼法让人感觉他高度改变了,其实不然。
至于动画,就更容易了就只有一句,就是在初期某个阶段让他变化高度到10px,也就是遮挡块变小了,显示的高度就就多了,然后缓缓增大至整块,来完成起伏效果。另外,我依然通过遍历在其伪类中,给他们不同的延迟显得更有层次感。
讲到这里,我们的这个案例就书写完成了
结语
本次通过一个做加载创意动画的案例,向各位同学讲到了css如何弹性居中,scss的遍历,hsl色盘改变色值的方便之处以及障眼法的一种方式,希望大家会喜欢,多多支持哦~
作者:jsmask
链接:https://juejin.cn/post/7034304330878418980
收起阅读 »
学会这招,轻松优化webpack构建性能
webpack
webpack
本质上是一个静态资源打包工具,静态资源打包是指 webpack
会将文件及其通过 import
、require
等方式引入的各项资源,处理成一个资源依赖关系图,也称为 chunk
,这些资源包括 js
,css
,jpg
, 等等。
然后将这个 chunk
内的资源分别进行处理 ,如 less
编译成 css
,es6
编译成 es5
,等等。这个处理过程就是打包,最终将这些处理后的文件输出,输出的文件集合便称为 bundle
。
bundle 分析
学会优化 webpack
构建性能等优化,我们需要先学会如何分析 bundle
,通过对产出的分析,才能有针对性的对过程进行优化。
webpack
官方提供了一个非常好用的 bundle
可视化分析工具:webpack-bundle-analyzer
。这个工具会将 bundle
处理一个可视化页面,呈现出资源的依赖关系和体积大小等信息。
这个工具的使用方式也很简单,这需要在通过 npm install webpack-bundle-analyzer
或 yarn install webpack-bundle-analyzer
安装这个插件,然后在 webpack
配置文件的 plugins
配置项中加上这一行代码:
plugins: [
new BundleAnalyzerPlugin(),
]
复制代码
运行 webpack
打包后,会自动在 http://127.0.0.1:8888/
打开一个可视化页面:
优化小妙招
接下来我们将会结合对 bundle
的分析,进行一些优化操作。
在讲解如何优化之前,我们需要明确 chunk
和 bundle
的关系:chunk
是一组依赖关系的集合,它不单单指一个文件,可以包含一个或多个文件。而 bundle
是 webpack
打包的输出结果,它可以包含一个或多个 chunk
。而 webpack
打包执行时会以一个个 chunk
进行处理,前端在加载 webpack
打包的资源时,也往往是以一个 chunk
为单位加载的(无论它是一个或多个文件)。
splitChunks
从可视化界面中我们可以看到,经过 webpack
打包后我们得到一个 app.bundle.js
,这是个 bundle
中包含了我们项目的所有代码以及从 node_modules
中引入的依赖,而这个 bundle
中包含了项目内的所以依赖关系,因此这个 bundle
也是我们项目中唯一一个 chunk
。
那么我们在加载页面时,便是加载这一整个 chunk
,即需要在页面初始时加载全部的代码。
而 splitChunks
,是由 webpack
提供的插件,通过它能够允许我们自由的配置 chunk
的生成策略,当然也包括允许我们将一个巨大的 chunk
拆分成多个 chunk
。
在使用 splitChunks
之前我们先介绍一个重要的配置属性 cacheGroups
(如果需要,可以在官方文档 splitChunks 中了解更多):
cacheGroups
配置提取 chunk
的方案。里面每一项代表一个提取 chunk
的方案。下面是 cacheGroups
每项中特有的选项:
test
选项:用来匹配要提取的 chunk
的资源路径或名称,值是正则或函数。name
选项:生成的 chunk
名称。chunks
选项,决定要提取那些内容。priority
选项:方案的优先级,值越大表示提取 chunk
时优先采用此方案,默认值为0。enforce
选项:true
/false
。为true
时,chunk
的大小和数量限制。接下来我们便通过实际的配置操作,将 node_modules 的内容提取成单独的 chunk
,下面是我们的配置代码:
optimization: {
splitChunks: {
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
enforce: true,
priority: -3,
},
},
},
},
配置完成后,重新运行 webpack
,可以看到,node_modules
相关的依赖关系被提取成一个单独的 chunk
vendors.bundle.js
,最终我们得到了两个 chunk
:vendors.bundle.js
,app.bundle.js
那么通过这样的chunk
提取,有什么好处呢?
node_modules
下往往是在项目中不会的变化的第三方依赖,我们将这些固定不变的提取成单独的chunk
处理,webpack
便可以将这个chunk
进行一定的缓存策略,而不需要每次都做过多的处理,减少了性能消耗。- 网页加载资源时不需要一次性加载太多的资源,可以通过不同
chunk
分批次加载,从而减少首屏加载的时间。
除了这里介绍的对 node_modules
的处理外,在实际的项目中也可以根据需要对更多的资源采取这样的提取chunk
策略。
、
externals + CDN
通过对 bundle
的分析,我们不难发现:在我们输出的 bundle
中 react.development.js
、 react-dom.development.js
以及 react-router.js
这三个文件特别的显眼。这表示这几个文件的体积在我们总的输出文件中占的比例特别大,那么有什么方法可以解决这些碍眼的家伙呢?
当然有! 下面将要介绍的 external
+ CDN
策略,便可以很好的帮助我们做到这点。
external
是 webpack
的一个重要的配置项,顾名思义,它可以帮助我们将某些资源在 webpack
打包时将其剔除,不参与到资源的打包中。
external
是一个有多项 key-value
组成的对象,它的的每一项属性表示不需要经过 webpack
打包的资源,key
表示的是我们需要排除在外的依赖名称,value
则告诉 webpack
,需要从 window
对象的哪个属性获取到这些被排除在外的依赖。
下面的代码就是将react
、react-dom
、react-router-dom
三个依赖不进行 webpack
打包的配置,它告诉 webpack
,不将 react
、react-dom
和react-router-dom
打包进最终的输出中,需要用到这些依赖时从 window
对象下的 React
、ReactDOM
和 ReactRouterDOM
属性获取。
externals: {
react: 'React',
'react-dom': 'ReactDOM',
'react-router-dom': 'ReactRouterDOM',
},
复制代码
那么这些被剔除的依赖,为什么可以从 window
对象获取到呢?答案就是 CDN
!
我们将这些剔除的依赖,通过 script
标签引入对应的 CDN
资源( CDN
即 内容分发网络,我们可以将这些静态资源存储到 CDN
网络中,以便更快的获取资源)。
这需要我们将引入这些资源的script
标签加在入口 HTML
文件中,这些加载进来的js文件,会将资源挂载在对应的 window
属性 React
、ReactDOM
和 ReactRouterDOM
上。
<script src="https://cdn.staticfile.org/react/0.0.0-0c756fb-f7f79fd/cjs/react.development.js"></script>
<script src="https://cdn.staticfile.org/react-dom/0.0.0-0c756fb-f7f79fd/cjs/react-dom.development.js"></script>
<script src="https://cdn.staticfile.org/react-router-dom/0.0.0-experimental-ffd8c7d0/react-router-dom.development.js"></script>
接下来看下通过 external
+ CDN
策略处理后,我们最终输出的bundle:
react.development.js
、 react-dom.development.js
以及 react-router.js
这三个文件消失了!
作者:Promise
链接:https://juejin.cn/post/7034181462106570759
前端面试js高频手写大全(下)
8. 手写call, apply, bind
手写call
Function.prototype.myCall=function(context=window){ // 函数的方法,所以写在Fuction原型对象上
if(typeof this !=="function"){ // 这里if其实没必要,会自动抛出错误
throw new Error("不是函数")
}
const obj=context||window //这里可用ES6方法,为参数添加默认值,js严格模式全局作用域this为undefined
obj.fn=this //this为调用的上下文,this此处为函数,将这个函数作为obj的方法
const arg=[...arguments].slice(1) //第一个为obj所以删除,伪数组转为数组
res=obj.fn(...arg)
delete obj.fn // 不删除会导致context属性越来越多
return res
}
//用法:f.call(obj,arg1)
function f(a,b){
console.log(a+b)
console.log(this.name)
}
let obj={
name:1
}
f.myCall(obj,1,2) //否则this指向window
obj.greet.call({name: 'Spike'}) //打出来的是 Spike
手写apply(arguments[this, [参数1,参数2.....] ])
Function.prototype.myApply=function(context){ // 箭头函数从不具有参数对象!!!!!这里不能写成箭头函数
let obj=context||window
obj.fn=this
const arg=arguments[1]||[] //若有参数,得到的是数组
let res=obj.fn(...arg)
delete obj.fn
return res
}
function f(a,b){
console.log(a,b)
console.log(this.name)
}
let obj={
name:'张三'
}
f.myApply(obj,[1,2]) //arguments[1]
手写bind
this.value = 2
var foo = {
value: 1
};
var bar = function(name, age, school){
console.log(name) // 'An'
console.log(age) // 22
console.log(school) // '家里蹲大学'
}
var result = bar.bind(foo, 'An') //预置了部分参数'An'
result(22, '家里蹲大学') //这个参数会和预置的参数合并到一起放入bar中
简单版本
Function.prototype.bind = function(context, ...outerArgs) {
var fn = this;
return function(...innerArgs) { //返回了一个函数,...rest为实际调用时传入的参数
return fn.apply(context,[...outerArgs, ...innerArgs]); //返回改变了this的函数,
//参数合并
}
}
new失败的原因:
例:
// 声明一个上下文
let thovino = {
name: 'thovino'
}
// 声明一个构造函数
let eat = function (food) {
this.food = food
console.log(`${this.name} eat ${this.food}`)
}
eat.prototype.sayFuncName = function () {
console.log('func name : eat')
}
// bind一下
let thovinoEat = eat.bind(thovino)
let instance = new thovinoEat('orange') //实际上orange放到了thovino里面
console.log('instance:', instance) // {}
生成的实例是个空对象
在new
操作符执行时,我们的thovinoEat
函数可以看作是这样:
function thovinoEat (...innerArgs) {
eat.call(thovino, ...outerArgs, ...innerArgs)
}
在new操作符进行到第三步的操作thovinoEat.call(obj, ...args)
时,这里的obj
是new操作符自己创建的那个简单空对象{}
,但它其实并没有替换掉thovinoEat
函数内部的那个上下文对象thovino
。这已经超出了call
的能力范围,因为这个时候要替换的已经不是thovinoEat
函数内部的this
指向,而应该是thovino
对象。
换句话说,我们希望的是new
操作符将eat
内的this
指向操作符自己创建的那个空对象。但是实际上指向了thovino
,new
操作符的第三步动作并没有成功!
可new可继承版本
Function.prototype.bind = function (context, ...outerArgs) {
let that = this;
function res (...innerArgs) {
if (this instanceof res) {
// new操作符执行时
// 这里的this在new操作符第三步操作时,会指向new自身创建的那个简单空对象{}
that.call(this, ...outerArgs, ...innerArgs)
} else {
// 普通bind
that.call(context, ...outerArgs, ...innerArgs)
}
}
res.prototype = this.prototype //!!!
return res
}
9. 手动实现new
new的过程文字描述:
创建一个空对象 obj;
将空对象的隐式原型(proto)指向构造函数的prototype。
使用 call 改变 this 的指向
如果无返回值或者返回一个非对象值,则将 obj 返回作为新对象;如果返回值是一个新对象的话那么直接直接返回该对象。
function Person(name,age){
this.name=name
this.age=age
}
Person.prototype.sayHi=function(){
console.log('Hi!我是'+this.name)
}
let p1=new Person('张三',18)
////手动实现new
function create(){
let obj={}
//获取构造函数
let fn=[].shift.call(arguments) //将arguments对象提出来转化为数组,arguments并不是数组而是对象 !!!这种方法删除了arguments数组的第一个元素,!!这里的空数组里面填不填元素都没关系,不影响arguments的结果 或者let arg = [].slice.call(arguments,1)
obj.__proto__=fn.prototype
let res=fn.apply(obj,arguments) //改变this指向,为实例添加方法和属性
//确保返回的是一个对象(万一fn不是构造函数)
return typeof res==='object'?res:obj
}
let p2=create(Person,'李四',19)
p2.sayHi()
细节:
[].shift.call(arguments) 也可写成:
let arg=[...arguments]
let fn=arg.shift() //使得arguments能调用数组方法,第一个参数为构造函数
obj.__proto__=fn.prototype
//改变this指向,为实例添加方法和属性
let res=fn.apply(obj,arg)
10. 手写promise(常考promise.all, promise.race)
// Promise/A+ 规范规定的三种状态
const STATUS = {
PENDING: 'pending',
FULFILLED: 'fulfilled',
REJECTED: 'rejected'
}
class MyPromise {
// 构造函数接收一个执行回调
constructor(executor) {
this._status = STATUS.PENDING // Promise初始状态
this._value = undefined // then回调的值
this._resolveQueue = [] // resolve时触发的成功队列
this._rejectQueue = [] // reject时触发的失败队列
// 使用箭头函数固定this(resolve函数在executor中触发,不然找不到this)
const resolve = value => {
const run = () => {
// Promise/A+ 规范规定的Promise状态只能从pending触发,变成fulfilled
if (this._status === STATUS.PENDING) {
this._status = STATUS.FULFILLED // 更改状态
this._value = value // 储存当前值,用于then回调
// 执行resolve回调
while (this._resolveQueue.length) {
const callback = this._resolveQueue.shift()
callback(value)
}
}
}
//把resolve执行回调的操作封装成一个函数,放进setTimeout里,以实现promise异步调用的特性(规范上是微任务,这里是宏任务)
setTimeout(run)
}
// 同 resolve
const reject = value => {
const run = () => {
if (this._status === STATUS.PENDING) {
this._status = STATUS.REJECTED
this._value = value
while (this._rejectQueue.length) {
const callback = this._rejectQueue.shift()
callback(value)
}
}
}
setTimeout(run)
}
// new Promise()时立即执行executor,并传入resolve和reject
executor(resolve, reject)
}
// then方法,接收一个成功的回调和一个失败的回调
function then(onFulfilled, onRejected) {
// 根据规范,如果then的参数不是function,则忽略它, 让值继续往下传递,链式调用继续往下执行
typeof onFulfilled !== 'function' ? onFulfilled = value => value : null
typeof onRejected !== 'function' ? onRejected = error => error : null
// then 返回一个新的promise
return new MyPromise((resolve, reject) => {
const resolveFn = value => {
try {
const x = onFulfilled(value)
// 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}
}
}
const rejectFn = error => {
try {
const x = onRejected(error)
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}
switch (this._status) {
case STATUS.PENDING:
this._resolveQueue.push(resolveFn)
this._rejectQueue.push(rejectFn)
break;
case STATUS.FULFILLED:
resolveFn(this._value)
break;
case STATUS.REJECTED:
rejectFn(this._value)
break;
}
})
}
catch (rejectFn) {
return this.then(undefined, rejectFn)
}
// promise.finally方法
finally(callback) {
return this.then(value => MyPromise.resolve(callback()).then(() => value), error => {
MyPromise.resolve(callback()).then(() => error)
})
}
// 静态resolve方法
static resolve(value) {
return value instanceof MyPromise ? value : new MyPromise(resolve => resolve(value))
}
// 静态reject方法
static reject(error) {
return new MyPromise((resolve, reject) => reject(error))
}
// 静态all方法
static all(promiseArr) {
let count = 0
let result = []
return new MyPromise((resolve, reject) => {
if (!promiseArr.length) {
return resolve(result)
}
promiseArr.forEach((p, i) => {
MyPromise.resolve(p).then(value => {
count++
result[i] = value
if (count === promiseArr.length) {
resolve(result)
}
}, error => {
reject(error)
})
})
})
}
// 静态race方法
static race(promiseArr) {
return new MyPromise((resolve, reject) => {
promiseArr.forEach(p => {
MyPromise.resolve(p).then(value => {
resolve(value)
}, error => {
reject(error)
})
})
})
}
}
11. 手写原生AJAX
步骤
创建 XMLHttpRequest 实例
发出 HTTP 请求
服务器返回 XML 格式的字符串
JS 解析 XML,并更新局部页面
不过随着历史进程的推进,XML 已经被淘汰,取而代之的是 JSON。
了解了属性和方法之后,根据 AJAX 的步骤,手写最简单的 GET 请求。
version 1.0:
myButton.addEventListener('click', function () {
ajax()
})
function ajax() {
let xhr = new XMLHttpRequest() //实例化,以调用方法
xhr.open('get', 'https://www.google.com') //参数2,url。参数三:异步
xhr.onreadystatechange = () => { //每当 readyState 属性改变时,就会调用该函数。
if (xhr.readyState === 4) { //XMLHttpRequest 代理当前所处状态。
if (xhr.status >= 200 && xhr.status < 300) { //200-300请求成功
let string = request.responseText
//JSON.parse() 方法用来解析JSON字符串,构造由字符串描述的JavaScript值或对象
let object = JSON.parse(string)
}
}
}
request.send() //用于实际发出 HTTP 请求。不带参数为GET请求
}
promise实现
function ajax(url) {
const p = new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest()
xhr.open('get', url)
xhr.onreadystatechange = () => {
if (xhr.readyState == 4) {
if (xhr.status >= 200 && xhr.status <= 300) {
resolve(JSON.parse(xhr.responseText))
} else {
reject('请求出错')
}
}
}
xhr.send() //发送hppt请求
})
return p
}
let url = '/data.json'
ajax(url).then(res => console.log(res))
.catch(reason => console.log(reason))
12. 手写节流防抖函数
函数节流与函数防抖都是为了限制函数的执行频次,是一种性能优化的方案,比如应用于window对象的resize、scroll事件,拖拽时的mousemove事件,文字输入、自动完成的keyup事件。
节流:连续触发事件但是在 n 秒中只执行一次函数
例:(连续不断动都需要调用时用,设一时间间隔),像dom的拖拽,如果用消抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多。
防抖:指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
例:(连续不断触发时不调用,触发完后过一段时间调用),像仿百度搜索,就应该用防抖,当我连续不断输入时,不会发送请求;当我一段时间内不输入了,才会发送一次请求;如果小于这段时间继续输入的话,时间会重新计算,也不会发送请求。
防抖的实现:
function debounce(fn, delay) {
if(typeof fn!=='function') {
throw new TypeError('fn不是函数')
}
let timer; // 维护一个 timer
return function () {
var _this = this; // 取debounce执行作用域的this(原函数挂载到的对象)
var args = arguments;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(function () {
fn.apply(_this, args); // 用apply指向调用debounce的对象,相当于_this.fn(args);
}, delay);
};
}
// 调用
input1.addEventListener('keyup', debounce(() => {
console.log(input1.value)
}), 600)
节流的实现:
function throttle(fn, delay) {
let timer;
return function () {
var _this = this;
var args = arguments;
if (timer) {
return;
}
timer = setTimeout(function () {
fn.apply(_this, args); // 这里args接收的是外边返回的函数的参数,不能用arguments
// fn.apply(_this, arguments); 需要注意:Chrome 14 以及 Internet Explorer 9 仍然不接受类数组对象。如果传入类数组对象,它们会抛出异常。
timer = null; // 在delay后执行完fn之后清空timer,此时timer为假,throttle触发可以进入计时器
}, delay)
}
}
div1.addEventListener('drag', throttle((e) => {
console.log(e.offsetX, e.offsetY)
}, 100))
13. 手写Promise加载图片
function getData(url) {
return new Promise((resolve, reject) => {
$.ajax({
url,
success(data) {
resolve(data)
},
error(err) {
reject(err)
}
})
})
}
const url1 = './data1.json'
const url2 = './data2.json'
const url3 = './data3.json'
getData(url1).then(data1 => {
console.log(data1)
return getData(url2)
}).then(data2 => {
console.log(data2)
return getData(url3)
}).then(data3 =>
console.log(data3)
).catch(err =>
console.error(err)
)
14. 函数实现一秒钟输出一个数
(!!!这个题这两天字节校招面试被问到了,问var打印的什么,改为let为什么可以?
有没有其他方法实现?我自己博客里都写了不用let的写法第二种方法,居然给忘了~~~白学了)
ES6:用let块级作用域的原理实现
for(let i=0;i<=10;i++){ //用var打印的都是11
setTimeout(()=>{
console.log(i);
},1000*i)
}
不用let的写法: 原理是用立即执行函数创造一个块级作用域
for(var i = 1; i <= 10; i++){
(function (i) {
setTimeout(function () {
console.log(i);
}, 1000 * i)
})(i);
}
15. 创建10个标签,点击的时候弹出来对应的序号?
var a
for(let i=0;i<10;i++){
a=document.createElement('a')
a.innerHTML=i+'<br>'
a.addEventListener('click',function(e){
console.log(this) //this为当前点击的<a>
e.preventDefault() //如果调用这个方法,默认事件行为将不再触发。
//例如,在执行这个方法后,如果点击一个链接(a标签),浏览器不会跳转到新的 URL 去了。我们可以用 event.isDefaultPrevented() 来确定这个方法是否(在那个事件对象上)被调用过了。
alert(i)
})
const d=document.querySelector('div')
d.appendChild(a) //append向一个已存在的元素追加该元素。
}
16. 实现事件订阅发布(eventBus)
实现EventBus类,有 on off once trigger功能,分别对应绑定事件监听器,解绑,执行一次后解除事件绑定,触发事件监听器。 这个题目面字节和快手都问到了,最近忙,答案会在后续更新
class EventBus {
on(eventName, listener) {}
off(eventName, listener) {}
once(eventName, listener) {}
trigger(eventName) {}
}
const e = new EventBus();
// fn1 fn2
e.on('e1', fn1)
e.once('e1', fn2)
e.trigger('e1') // fn1() fn2()
e.trigger('e1') // fn1()
e.off('e1', fn1)
e.trigger('e1') // null
实现:
//声明类
class EventBus {
constructor() {
this.eventList = {} //创建对象收集事件
}
//发布事件
$on(eventName, fn) {
//判断是否发布过事件名称? 添加发布 : 创建并添加发布
this.eventList[eventName]
? this.eventList[eventName].push(fn)
: (this.eventList[eventName] = [fn])
}
//订阅事件
$emit(eventName) {
if (!eventName) throw new Error('请传入事件名')
//获取订阅传参
const data = [...arguments].slice(1)
if (this.eventList[eventName]) {
this.eventList[eventName].forEach((i) => {
try {
i(...data) //轮询事件
} catch (e) {
console.error(e + 'eventName:' + eventName) //收集执行时的报错
}
})
}
}
//执行一次
$once(eventName, fn) {
const _this = this
function onceHandle() {
fn.apply(null, arguments)
_this.$off(eventName, onceHandle) //执行成功后取消监听
}
this.$on(eventName, onceHandle)
}
//取消订阅
$off(eventName, fn) {
//不传入参数时取消全部订阅
if (!arguments.length) {
return (this.eventList = {})
}
//eventName传入的是数组时,取消多个订阅
if (Array.isArray(eventName)) {
return eventName.forEach((event) => {
this.$off(event, fn)
})
}
//不传入fn时取消事件名下的所有队列
if (arguments.length === 1 || !fn) {
this.eventList[eventName] = []
}
//取消事件名下的fn
this.eventList[eventName] = this.eventList[eventName].filter(
(f) => f !== fn
)
}
}
const event = new EventBus()
let b = function (v1, v2, v3) {
console.log('b', v1, v2, v3)
}
let a = function () {
console.log('a')
}
event.$once('test', a)
event.$on('test', b)
event.$emit('test', 1, 2, 3, 45, 123)
event.$off(['test'], b)
event.$emit('test', 1, 2, 3, 45, 123)
参考:
数组扁平化 https://juejin.im/post/5c971ee16fb9a070ce31b64e#heading-3
函数柯里化 https://juejin.im/post/6844903882208837645
节流防抖 https://www.jianshu.com/p/c8b...
事件订阅发布实现 https://heznb.com/archives/js...
浅拷贝深拷贝 https://segmentfault.com/a/11...
收起阅读 »作者:晚起的虫儿
如何写 CSS 重置(RESET)样式?
很长一段时间,我都使用Eric Meyer著名的CSS Reset。这是CSS的一个坚实的块,但是在这一点上它有点长。它已经十多年没有更新了,从那时起发生了很多变化!
最近,我一直在使用我自己的自定义CSS重置。它包括我发现的所有小技巧,以改善用户体验和CSS创作体验。
像其他CSS重置一样,在设计/化妆品方面,它是不赞成的。您可以将此重置用于任何项目,无论您想要哪种美学。
在本教程中,我们将介绍我的自定义 CSS 重置。我们将深入研究每个规则,您将了解它的作用以及您可能想要使用它的原因!
CSS 重置
事不宜迟,这里是:
/*
1. Use a more-intuitive box-sizing model.
*/
*, *::before, *::after {
box-sizing: border-box;
}
/*
2. Remove default margin
*/
* {
margin: 0;
}
/*
3. Allow percentage-based heights in the application
*/
html, body {
height: 100%;
}
/*
Typographic tweaks!
4. Add accessible line-height
5. Improve text rendering
*/
body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
/*
6. Improve media defaults
*/
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
/*
7. Remove built-in form typography styles
*/
input, button, textarea, select {
font: inherit;
}
/*
8. Avoid text overflows
*/
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
/*
9. Create a root stacking context
*/
#root, #__next {
isolation: isolate;
}
它相对较短,但是这个小样式表中包含了很多东西。让我们开始吧!
从历史上看,CSS重置的主要目标是确保浏览器之间的一致性,并撤消所有默认样式,从而创建一个空白的石板。我的CSS重置并没有真正做这些事情。
如今,浏览器在布局或间距方面没有巨大的差异。总的来说,浏览器忠实地实现了CSS规范,并且事情的行为符合您的预期。因此,它不再是必要的了。
我也不认为有必要剥离所有浏览器默认值。例如,我可能确实想要设置标签!我总是可以在各个项目风格中做出不同的设计决策,但我认为剥离常识性默认值是没有意义的。
<em>``font-style: italic
我的CSS重置可能不符合"CSS重置"的经典定义,但我正在采取这种创造性的自由。
CSS盒子模型
测验!通过可见的粉红色边框进行测量,假设未应用其他 CSS,则在以下方案中元素的宽度是多少?.box
<style>
.parent {
width: 200px;
}
.box {
width: 100%;
border: 2px solid hotpink;
padding: 20px;
}
</style>
<div>
<div></div>
</div>
我们的元素有.因为它的父级是200px宽,所以100%将解析为200px。.box``width: 100%
但是它在哪里应用200px宽度? 默认情况下,它会将该大小应用于内容框。
如果您不熟悉,"内容框"是框模型中实际保存内容的矩形,位于边框和填充内:
该声明会将 的内容框设置为 200px。填充将添加额外的40px(每侧20px)。边框添加最后一个 4px(每侧 2px)。当我们进行数学计算时,可见的粉红色矩形将是244px宽。width: 100%``.box
当我们尝试将一个 244px 的框塞入一个 200px 宽的父级中时,它会溢出:
这种行为很奇怪,对吧?幸运的是,我们可以通过设置以下规则来更改它:
*, *::before, *::after {
box-sizing: border-box;
}
应用此规则后,百分比将基于边框进行解析。在上面的示例中,我们的粉红色框将为 200px,内部内容框将缩小到 156px(200px - 40px - 4px)。
在我看来,这是一个必须的规则。 它使CSS更适合使用。
我们使用通配符选择器 () 将其应用于所有元素和伪元素。与普遍的看法相反,这对性能来说并不坏。*
我在网上看到了一些建议,可以代替这样做:
html {
box-sizing: border-box;
}
*, *:before, *:after {
box-sizing: inherit;
}
删除默认间距
* {
margin: 0;
}
浏览器围绕保证金做出常识性的假设。例如,默认情况下,将包含比段落更多的边距。h1
这些假设在文字处理文档的上下文中是合理的,但对于现代 Web 应用程序而言,它们可能不准确。
Margin是一个棘手的魔鬼,而且我经常发现自己希望元素默认情况下没有任何元素。所以我决定全部删除它。🔥
如果/当我确实想为特定标签添加一些边距时,我可以在我的自定义项目样式中执行此操作。通配符选择器 () 具有极低的特异性,因此很容易覆盖此规则。*
基于百分比的高度
html, body {
height: 100%;
}
你有没有试过在CSS中使用基于百分比的高度,却发现它似乎没有效果?
下面是一个示例:
元素有,但元素根本不会增长!main``height: 100%
这不起作用,因为在 Flow 布局(CSS 中的主要布局模式)中,并且操作的原则根本不同。元素的宽度是根据其父级计算的,但元素的高度是根据其子元素计算的。height``width
这是一个复杂的主题,远远超出了本文的范围。我计划写一篇关于它的博客文章,但与此同时,你可以在我的CSS课程中了解它,CSS for JavaScript Developers。
作为一个快速演示,在这里我们看到,当我们应用此规则时,我们的元素可以增长:main
如果你使用的是像 React 这样的 JS 框架,你可能还希望向这个规则添加第三个选择器:框架使用的根级元素。
例如,在我的 Next.js 项目中,我按如下方式更新规则:
html, body, #__next {
height: 100%;
}
为什么不使用vh?
您可能想知道:为什么要在基于百分比的高度上大惊小怪?为什么不改用该装置呢?
vh
问题是该单元在移动设备上无法正常工作; 将占用超过100%的屏幕空间,因为移动浏览器在浏览器UI来来去去的地方做那件事。
vh``100vh
将来,新的CSS单元将解决这个问题。在此之前,我继续使用基于百分比的高度。
调整行高
body {
line-height: 1.5;
}
line-height
控制段落中每行文本之间的垂直间距。默认值因浏览器而异,但往往在 1.2 左右。
此无单位数字是基于字体大小的比率。它的功能就像设备一样。如果为 1.2,则每行将比元素的字体大小大 20%。em``line-height
问题是:对于那些有阅读障碍的人来说,这些行挤得太紧,使得阅读起来更加困难。WCAG标准规定行高应至少为1.5。
现在,这个数字确实倾向于在标题和其他具有大类型的元素上产生相当大的行:
您可能希望在标题上覆盖此值。我的理解是,WCAG标准适用于"正文"文本,而不是标题。
使用"计算"实现更智能的线高
我一直在尝试一种管理行高的替代方法。在这里:
* {
line-height: calc(1em + 0.5rem);
}
这是一个非常高级的小片段,它超出了这篇博客文章的范围,但这里有一个快速的解释。
字体平滑,抗锯齿
body {
-webkit-font-smoothing: antialiased;
}
好吧,所以这个有点争议。
在 MacOS 电脑上,浏览器将默认使用"子像素抗锯齿"。这是一种旨在通过利用每个像素内的 R/G/B 灯使文本更易于阅读的技术。
过去,这被视为可访问性的胜利,因为它提高了文本对比度。您可能已经读过一篇流行的博客文章停止"修复"字体平滑,该帖子主张反对切换到"抗锯齿"。
问题是:那篇文章写于2012年,在高DPI"视网膜"显示时代之前。今天的像素要小得多,肉眼看不见。
像素 LED 的物理排列也发生了变化。如果你在显微镜下看一台现代显示器,你不会再看到R/G/B线的有序网格了。
在2018年发布的MacOS Mojave中 ,Apple在整个操作系统中禁用了子像素抗锯齿。我猜他们意识到它在现代硬件上弊大于利。
令人困惑的是,像Chrome和Safari这样的MacOS浏览器仍然默认使用子像素抗锯齿。我们需要通过设置为 来显式关闭它。-webkit-font-smoothing``antialiased
区别如下:
MacOS 是唯一使用子像素抗锯齿的操作系统,因此此规则对 Windows、Linux 或移动设备没有影响。如果您使用的是 MacOS 电脑,则可以尝试实时渲染:
合理的媒体默认值
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
所以这里有一件奇怪的事情:图像被认为是"内联"元素。这意味着它们应该在段落的中间使用,例如 或 。<em>``<strong>
这与我大多数时候使用图像的方式不符。通常,我对待图像的方式与处理段落或标题或侧边栏的方式相同;它们是布局元素。
但是,如果我们尝试在布局中使用内联元素,则会发生奇怪的事情。如果您曾经有过一个神秘的4px间隙,不是边距,填充或边框,那么它可能是浏览器添加的"内联魔术空间"。line-height
通过默认设置所有图像,我们回避了整个类别的时髦问题。display: block
我也设置了.这样做是为了防止大图像溢出,如果它们放置在不够宽而无法容纳它们的容器中。max-width: 100%
大多数块级元素会自动增大/缩小以适应其父元素,但媒体元素是特殊的:它们被称为替换元素,并且它们不遵循相同的规则。<img>
如果图像的"本机"大小为 800×600,则该元素的宽度也将为 800px,即使我们将其放入 500px 宽的父级中也是如此。<img>
此规则将防止该图像超出其容器,这对我来说更像是更明智的默认行为。
继承窗体控件的字体
input, button, textarea, select {
font: inherit;
}
如果我们想避免这种自动缩放行为,输入的字体大小需要至少为1rem / 16px。以下是解决此问题的一种方法:
CSS
input, button, textarea, select {
font-size: 1rem;
}
这解决了自动变焦问题,但它是创可贴。让我们解决根本原因:表单输入不应该有自己的印刷风格!
CSS
input, button, textarea, select {
font: inherit;
}
font
是一种很少使用的速记,它设置了一堆与字体相关的属性,如 、 和 。通过将其设置为 ,我们指示这些元素与其周围环境中的排版相匹配。font-size``font-weight``font-family``inherit
只要我们不为正文文本选择令人讨厌的小字体大小,就可以同时解决我们所有的问题。🎉
自动换行
CSS
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
在 CSS 中,如果没有足够的空间来容纳一行上的所有字符,文本将自动换行。
默认情况下,该算法将寻找"软包装"机会;这些是算法可以拆分的字符。在英语中,唯一的软包装机会是空格和连字符,但这因语言而异。
如果某行没有任何软换行机会,并且它不合适,则会导致文本溢出:
这可能会导致一些令人讨厌的布局问题。在这里,它添加了一个水平滚动条。在其他情况下,它可能会导致文本与其他元素重叠,或滑到图像/视频后面。
该属性允许我们调整换行算法,并允许它在找不到软换行机会时使用硬换行:overflow-wrap
这两种解决方案都不完美,但至少硬包装不会弄乱布局!
感谢Sophie Alpert提出类似的规则!她建议将其应用于所有元素,这可能是一个好主意,但不是我个人测试过的东西。
您也可以尝试添加属性:hyphens
p {
overflow-wrap: break-word;
hyphens: auto;
}
hyphens: auto
使用连字符(在支持连字符的语言中)来指示硬换行。这也使得硬包装更加普遍。
如果您有非常窄的文本列,这可能是值得的,但它也可能有点分散注意力。我选择不将其包含在重置中,但值得尝试!
根堆叠上下文
#root, #__next {
isolation: isolate;
}
最后一个是可选的。通常只有当你使用像 React 这样的 JS 框架时才需要它。
正如我们在"到底是什么,z-index??"中看到的那样,该属性允许我们创建新的堆叠上下文,而无需设置 .isolation``z-index
这是有益的,因为它允许我们保证某些高优先级元素(模式,下拉列表,工具提示)将始终显示在应用程序中的其他元素之上。没有奇怪的堆叠上下文错误,没有z指数军备竞赛。
您应该调整选择器以匹配您的框架。我们希望选择在其中呈现应用程序的顶级元素。例如,create-react-app 使用 一个 ,因此正确的选择器是 。<div id="root">``#root
最终成品
下面再次以精简的复制友好格式进行 CSS 重置:
/*
Josh's Custom CSS Reset
https://www.joshwcomeau.com/css/custom-css-reset/
*/
*, *::before, *::after {
box-sizing: border-box;
}
* {
margin: 0;
}
html, body {
height: 100%;
}
body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
input, button, textarea, select {
font: inherit;
}
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
#root, #__next {
isolation: isolate;
}
```
```
/*
Josh's Custom CSS Reset
https://www.joshwcomeau.com/css/custom-css-reset/
*/
*, *::before, *::after {
box-sizing: border-box;
}
* {
margin: 0;
}
html, body {
height: 100%;
}
body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
input, button, textarea, select {
font: inherit;
}
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
#root, #__next {
isolation: isolate;
}
```
`
作者:非优秀程序员
链接:https://juejin.cn/post/7034308682825351176
收起阅读 »
前端面试js高频手写大全(上)
介绍
在前端面试中,手撕代码显然是不可避免的,并且占很大的一部分比重。
一般来说,如果代码写的好,即使理论知识答得不够清楚,也能有大概率通过面试。并且其实很多手写往往背后就考察了你对相关理论的认识。
编程题主要分为这几种类型:
* 算法题
* 涉及js原理的题以及ajax请求
* 业务场景题: 实现一个具有某种功能的组件
* 其他(进阶,对计算机综合知识的考察,考的相对较少):实现订阅发布者模式;分别用面向对象编程,面向过程编程,函数式编程实现把大象放进冰箱等等
其中前两种类型所占比重最大。
算法题建议养成每天刷一道leetcode的习惯,重点刷数据结构(栈,链表,队列,树),动态规划,DFS,BFS
本文主要涵盖了第二种类型的各种重点手写。
建议优先掌握:
instanceof (考察对原型链的理解)
new (对创建对象实例过程的理解)
call&apply&bind (对this指向的理解)
手写promise (对异步的理解)
手写原生ajax (对ajax原理和http请求方式的理解,重点是get和post请求的实现)
事件订阅发布 (高频考点)
其他:数组,字符串的api的实现,难度相对较低。只要了解数组,字符串的常用方法的用法,现场就能写出来个大概。(ps:笔者认为数组的reduce方法比较难,这块有余力可以单独看一些,即使面试没让你实现reduce,写其他题时用上它也是很加分的)
话不多说,直接开始
1. 手写instanceof
instanceof作用:
判断一个实例是否是其父类或者祖先类型的实例。
instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype查找失败,返回 false
let myInstanceof = (target,origin) => {
while(target) {
if(target.__proto__===origin.prototype) {
return true
}
target = target.__proto__
}
return false
}
let a = [1,2,3]
console.log(myInstanceof(a,Array)); // true
console.log(myInstanceof(a,Object)); // true
2. 实现数组的map方法
数组的map() 方法会返回一个新的数组,这个新数组中的每个元素对应原数组中的对应位置元素调用一次提供的函数后的返回值。
用法:
const a = [1, 2, 3, 4];
const b = array1.map(x => x * 2);
console.log(b); // Array [2, 4, 6, 8]
实现前,我们先看一下map方法的参数有哪些
map方法有两个参数,一个是操作数组元素的方法fn,一个是this指向(可选),其中使用fn时可以获取三个参数,实现时记得不要漏掉,这样才算完整实现嘛
原生实现:
// 实现
Array.prototype.myMap = function(fn, thisValue) {
let res = []
thisValue = thisValue||[]
let arr = this
for(let i=0; i<arr.length; i++) {
res.push(fn.call(thisValue, arr[i],i,arr)) // 参数分别为this指向,当前数组项,当前索引,当前数组
}
return res
}
// 使用
const a = [1,2,3];
const b = a.myMap((a,index)=> {
return a+1;
}
)
console.log(b) // 输出 [2, 3, 4]
3. reduce实现数组的map方法
利用数组内置的reduce方法实现map方法,考察对reduce原理的掌握
Array.prototype.myMap = function(fn,thisValue){
var res = [];
thisValue = thisValue||[];
this.reduce(function(pre,cur,index,arr){
return res.push(fn.call(thisValue,cur,index,arr));
},[]);
return res;
}
var arr = [2,3,1,5];
arr.myMap(function(item,index,arr){
console.log(item,index,arr);
})
4. 手写数组的reduce方法
reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终为一个值,是ES5中新增的又一个数组逐项处理方法
参数:
callback(一个在数组中每一项上调用的函数,接受四个函数:)
previousValue(上一次调用回调函数时的返回值,或者初始值)
currentValue(当前正在处理的数组元素)
currentIndex(当前正在处理的数组元素下标)
array(调用reduce()方法的数组)
initialValue(可选的初始值。作为第一次调用回调函数时传给previousValue的值)
function reduce(arr, cb, initialValue){
var num = initValue == undefined? num = arr[0]: initValue;
var i = initValue == undefined? 1: 0
for (i; i< arr.length; i++){
num = cb(num,arr[i],i)
}
return num
}
function fn(result, currentValue, index){
return result + currentValue
}
var arr = [2,3,4,5]
var b = reduce(arr, fn,10)
var c = reduce(arr, fn)
console.log(b) // 24
5. 数组扁平化
数组扁平化就是把多维数组转化成一维数组
1. es6提供的新方法 flat(depth)
let a = [1,[2,3]];
a.flat(); // [1,2,3]
a.flat(1); //[1,2,3]
其实还有一种更简单的办法,无需知道数组的维度,直接将目标数组变成1维数组。 depth的值设置为Infinity。
let a = [1,[2,3,[4,[5]]]];
a.flat(Infinity); // [1,2,3,4,5] a是4维数组
2. 利用cancat
function flatten(arr) {
var res = [];
for (let i = 0, length = arr.length; i < length; i++) {
if (Array.isArray(arr[i])) {
res = res.concat(flatten(arr[i])); //concat 并不会改变原数组
//res.push(...flatten(arr[i])); //或者用扩展运算符
} else {
res.push(arr[i]);
}
}
return res;
}
let arr1 = [1, 2,[3,1],[2,3,4,[2,3,4]]]
flatten(arr1); //[1, 2, 3, 1, 2, 3, 4, 2, 3, 4]
补充:指定deep的flat
只需每次递归时将当前deep-1,若大于0,则可以继续展开
function flat(arr, deep) {
let res = []
for(let i in arr) {
if(Array.isArray(arr[i])&&deep) {
res = res.concat(flat(arr[i],deep-1))
} else {
res.push(arr[i])
}
}
return res
}
console.log(flat([12,[1,2,3],3,[2,4,[4,[3,4],2]]],1));
6. 函数柯里化
用的这里的方法 https://juejin.im/post/684490...
柯里化的定义:接收一部分参数,返回一个函数接收剩余参数,接收足够参数后,执行原函数。
当柯里化函数接收到足够参数后,就会执行原函数,如何去确定何时达到足够的参数呢?
有两种思路:
通过函数的 length 属性,获取函数的形参个数,形参的个数就是所需的参数个数
在调用柯里化工具函数时,手动指定所需的参数个数
将这两点结合一下,实现一个简单 curry 函数:
/**
* 将函数柯里化
* @param fn 待柯里化的原函数
* @param len 所需的参数个数,默认为原函数的形参个数
*/
function curry(fn,len = fn.length) {
return _curry.call(this,fn,len)
}
/**
* 中转函数
* @param fn 待柯里化的原函数
* @param len 所需的参数个数
* @param args 已接收的参数列表
*/
function _curry(fn,len,...args) {
return function (...params) {
let _args = [...args,...params];
if(_args.length >= len){
return fn.apply(this,_args);
}else{
return _curry.call(this,fn,len,..._args)
}
}
}
我们来验证一下:
let _fn = curry(function(a,b,c,d,e){
console.log(a,b,c,d,e)
});
_fn(1,2,3,4,5); // print: 1,2,3,4,5
_fn(1)(2)(3,4,5); // print: 1,2,3,4,5
_fn(1,2)(3,4)(5); // print: 1,2,3,4,5
_fn(1)(2)(3)(4)(5); // print: 1,2,3,4,5
我们常用的工具库 lodash 也提供了 curry 方法,并且增加了非常好玩的 placeholder 功能,通过占位符的方式来改变传入参数的顺序。
比如说,我们传入一个占位符,本次调用传递的参数略过占位符, 占位符所在的位置由下次调用的参数来填充,比如这样:
直接看一下官网的例子:
接下来我们来思考,如何实现占位符的功能。
对于 lodash 的 curry 函数来说,curry 函数挂载在 lodash 对象上,所以将 lodash 对象当做默认占位符来使用。
而我们的自己实现的 curry 函数,本身并没有挂载在任何对象上,所以将 curry 函数当做默认占位符
使用占位符,目的是改变参数传递的顺序,所以在 curry 函数实现中,每次需要记录是否使用了占位符,并且记录占位符所代表的参数位置。
直接上代码:
/**
* @param fn 待柯里化的函数
* @param length 需要的参数个数,默认为函数的形参个数
* @param holder 占位符,默认当前柯里化函数
* @return {Function} 柯里化后的函数
*/
function curry(fn,length = fn.length,holder = curry){
return _curry.call(this,fn,length,holder,[],[])
}
/**
* 中转函数
* @param fn 柯里化的原函数
* @param length 原函数需要的参数个数
* @param holder 接收的占位符
* @param args 已接收的参数列表
* @param holders 已接收的占位符位置列表
* @return {Function} 继续柯里化的函数 或 最终结果
*/
function _curry(fn,length,holder,args,holders){
return function(..._args){
//将参数复制一份,避免多次操作同一函数导致参数混乱
let params = args.slice();
//将占位符位置列表复制一份,新增加的占位符增加至此
let _holders = holders.slice();
//循环入参,追加参数 或 替换占位符
_args.forEach((arg,i)=>{
//真实参数 之前存在占位符 将占位符替换为真实参数
if (arg !== holder && holders.length) {
let index = holders.shift();
_holders.splice(_holders.indexOf(index),1);
params[index] = arg;
}
//真实参数 之前不存在占位符 将参数追加到参数列表中
else if(arg !== holder && !holders.length){
params.push(arg);
}
//传入的是占位符,之前不存在占位符 记录占位符的位置
else if(arg === holder && !holders.length){
params.push(arg);
_holders.push(params.length - 1);
}
//传入的是占位符,之前存在占位符 删除原占位符位置
else if(arg === holder && holders.length){
holders.shift();
}
});
// params 中前 length 条记录中不包含占位符,执行函数
if(params.length >= length && params.slice(0,length).every(i=>i!==holder)){
return fn.apply(this,params);
}else{
return _curry.call(this,fn,length,holder,params,_holders)
}
}
}
验证一下:;
let fn = function(a, b, c, d, e) {
console.log([a, b, c, d, e]);
}
let _ = {}; // 定义占位符
let _fn = curry(fn,5,_); // 将函数柯里化,指定所需的参数个数,指定所需的占位符
_fn(1, 2, 3, 4, 5); // print: 1,2,3,4,5
_fn(_, 2, 3, 4, 5)(1); // print: 1,2,3,4,5
_fn(1, _, 3, 4, 5)(2); // print: 1,2,3,4,5
_fn(1, _, 3)(_, 4,_)(2)(5); // print: 1,2,3,4,5
_fn(1, _, _, 4)(_, 3)(2)(5); // print: 1,2,3,4,5
_fn(_, 2)(_, _, 4)(1)(3)(5); // print: 1,2,3,4,5
至此,我们已经完整实现了一个 curry 函数~~
7. 浅拷贝和深拷贝的实现
深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的。
浅拷贝和深拷贝的区别:
浅拷贝:创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址,如果其中一个对象改变了引用类型的属性,就会影响到另一个对象。
深拷贝:将一个对象从内存中完整的复制一份出来,从堆内存中开辟一个新区域存放。这样更改拷贝值就不影响旧的对象
浅拷贝实现:
方法一:
function shallowCopy(target, origin){
for(let item in origin) target[item] = origin[item];
return target;
}
其他方法(内置api):
Object.assign
var obj={a:1,b:[1,2,3],c:function(){console.log('i am c')}}
var tar={};
Object.assign(tar,obj);
当然这个方法只适合于对象类型,如果是数组可以使用slice和concat方法
Array.prototype.slice
var arr=[1,2,[3,4]];
var newArr=arr.slice(0);
Array.prototype.concat
var arr=[1,2,[3,4]];
var newArr=arr.concat();
测试同上(assign用对象测试、slice concat用数组测试),结合浅拷贝深拷贝的概念来理解效果更佳
深拷贝实现:
方法一:
转为json格式再解析
const a = JSON.parse(JSON.stringify(b))
方法二:
// 实现深拷贝 递归
function deepCopy(newObj,oldObj){
for(var k in oldObj){
let item=oldObj[k]
// 判断是数组、对象、简单类型?
if(item instanceof Array){
newObj[k]=[]
deepCopy(newObj[k],item)
}else if(item instanceof Object){
newObj[k]={}
deepCopy(newObj[k],item)
}else{ //简单数据类型,直接赋值
newObj[k]=item
}
}
}
(未完待续……)作者:晚起的虫儿
太震撼了!我把七大JS排序算法做成了可视化!!!太好玩了!
前言
大家好,我是林三心。写这篇文章是有原因的,偶然我看到了一个Java的50种排序算法的可视化
的视频,但是此视频却没给出具体的实现教程,于是我心里就想着,我可以用JavaScript + canvas
去实现这个酷炫的效果。每种排序算法的动画效果基本都不一样哦。例如冒泡排序
是这样的
实现思路
想实现的效果
从封面可以看到,无论是哪种算法,一开始都是第一张图,而最终目的是要变成第二张图的效果
极坐标
讲实现思路之前,我先给大家复习一下高中的一个知识——极坐标。哈哈,不知道还有几个人记得他呢?
- O:极点,也就是原点
- ρ:极径
- θ:极径与X轴夹角
x = ρ * cosθ
,因为x / ρ = cosθ
y = ρ * sinθ
,因为y / ρ = sinθ
那我们想实现的结果,又跟极坐标有何关系呢?其实是有关系的,比如我现在有一个排序好的数组,他具有37个元素,那我们可以把这37个元素
转化为极坐标中的37个点
,怎么转呢?
const arr = [
0, 1, 2, 3, 4, 5, 6, 7, 8,
9, 10, 11, 12, 13, 14, 15, 16, 17,
18, 19, 20, 21, 22, 23, 24, 25, 26,
27, 28, 29, 30, 31, 32, 33, 34, 35, 36
]
我们可以这么转:
元素对应的索引index * 10 -> 角度θ
(为什么要乘10呢,因为要凑够360°嘛)元素对应的值arr[index] -> 极径ρ
按照上面的规则来转的话,那我们就可以在极坐标上得到这37个点(在canvas中Y轴是由上往下的,下面这个图也是按canvas的,但是Y轴我还是画成正常方向,所以这个图其实是反的,但是是有原因的哈):
(0 -> θ = 00°,ρ = 0) (1 -> θ = 10°,ρ = 1) (2 -> θ = 20°,ρ = 2) (3 -> θ = 30°,ρ = 3)
(4 -> θ = 40°,ρ = 4) (5 -> θ = 50°,ρ = 5) (6 -> θ = 60°,ρ = 6) (7 -> θ = 70°,ρ = 7)
(8 -> θ = 80°,ρ = 8) (9 -> θ = 90°,ρ = 9) (10 -> θ = 100°,ρ = 10) (11 -> θ = 110°,ρ = 11)
(12 -> θ = 120°,ρ = 12) (13 -> θ = 130°,ρ = 13) (14 -> θ = 140°,ρ = 14) (15 -> θ = 150°,ρ = 15)
(16 -> θ = 160°,ρ = 16) (17 -> θ = 170°,ρ = 17) (18 -> θ = 180°,ρ = 18) (19 -> θ = 190°,ρ = 19)
(20 -> θ = 200°,ρ = 20) (21 -> θ = 210°,ρ = 21) (22 -> θ = 220°,ρ = 22) (23 -> θ = 230°,ρ = 23)
(24 -> θ = 240°,ρ = 24) (25 -> θ = 250°,ρ = 25) (26 -> θ = 260°,ρ = 26) (27 -> θ = 270°,ρ = 27)
(28 -> θ = 280°,ρ = 28) (29 -> θ = 290°,ρ = 29) (30 -> θ = 300°,ρ = 30) (31 -> θ = 310°,ρ = 31)
(32 -> θ = 320°,ρ = 32) (33 -> θ = 330°,ρ = 33) (34 -> θ = 340°,ρ = 34) (35 -> θ = 350°,ρ = 35)
(36 -> θ = 360°,ρ = 36)
有没有发现,跟咱们想实现的最终效果的轨迹很像呢?
随机打散
那说完最终的效果,咱们来下想想如何一开始先把数组的各个元素打散在极坐标上呢?其实很简单,咱们可以先把生成一个乱序的数组,比如
const arr = [
25, 8, 32, 1, 19, 14, 0, 29, 17,
6, 7, 26, 3, 30, 31, 16, 28, 15,
24, 10, 21, 2, 9, 4, 35, 5, 36,
33, 11, 27, 34, 22, 13, 18, 23, 12, 20
]
然后还是用上面那个规则,去转换极坐标
元素对应的索引index * 10 -> 角度θ
(为什么要乘10呢,因为要凑够360°嘛)元素对应的值arr[index] -> 极径ρ
那么我们可以的到这37个点,自然就可以实现打散的效果
(25 -> θ = 00°,ρ = 25) (8 -> θ = 10°,ρ = 8) (32 -> θ = 20°,ρ = 32) (1 -> θ = 30°,ρ = 1)
(19 -> θ = 40°,ρ = 19) (14 -> θ = 50°,ρ = 14) (0 -> θ = 60°,ρ = 0) (29 -> θ = 70°,ρ = 29)
(17 -> θ = 80°,ρ = 17) (6 -> θ = 90°,ρ = 6) (7 -> θ = 100°,ρ = 7) (26 -> θ = 110°,ρ = 26)
(3 -> θ = 120°,ρ = 3) (30 -> θ = 130°,ρ = 30) (31 -> θ = 140°,ρ = 31) (16 -> θ = 150°,ρ = 16)
(28 -> θ = 160°,ρ = 28) (15 -> θ = 170°,ρ = 15) (24 -> θ = 180°,ρ = 24) (10 -> θ = 190°,ρ = 10)
(21 -> θ = 200°,ρ = 21) (2 -> θ = 210°,ρ = 2) (9 -> θ = 220°,ρ = 9) (4 -> θ = 230°,ρ = 4)
(35 -> θ = 240°,ρ = 35) (5 -> θ = 250°,ρ = 5) (36 -> θ = 260°,ρ = 36) (33 -> θ = 270°,ρ = 33)
(11 -> θ = 280°,ρ = 11) (27 -> θ = 290°,ρ = 27) (34 -> θ = 300°,ρ = 34) (22 -> θ = 310°,ρ = 22)
(13 -> θ = 320°,ρ = 13) (18 -> θ = 330°,ρ = 18) (23 -> θ = 340°,ρ = 23) (12 -> θ = 350°,ρ = 12)
(20 -> θ = 360°,ρ = 20)
实现效果
综上所述,咱们想实现效果,也就有了思路
- 1、先生成一个
乱序数组
- 2、用canvas画布画出此
乱序数组
所有元素对应的极坐标对应的点
- 3、对
乱序数组
进行排序
- 4、排序过程中
不断清空画布
,并重画
数组所有元素对应的极坐标对应的点 - 5、直到排序完成,终止画布操作
开搞!!!
咱们,做事情一定要有条有理才行,还记得上面说的步骤吗?
- 1、先生成一个
乱序数组
- 2、用canvas画布画出此
乱序数组
所有元素对应的极坐标对应的点
- 3、对
乱序数组
进行排序
- 4、排序过程中
不断清空画布
,并重画
数组所有元素对应的极坐标对应的点 - 5、直到排序完成,终止画布操作
咱们就按照这个步骤,来一步一步实现效果,兄弟们,冲啊!!!
生成乱序数组
咱们上面举的例子是37个元素,但是37个肯定是太少了,咱们搞多点吧,我搞了这么一个数组nums:我先生成一个0 - 179
的有序数组,然后打乱,并塞进数组nums中,此操作我执行4次。为什么是0 - 179
,因为0 - 179
刚好有180个数字
身位一个程序员,我肯定不可能自己手打这么多元素的啦。。来。。上代码
let nums = []
for (let i = 0; i < 4; i++) {
// 生成一个 0 - 179的有序数组
const arr = [...Array(180).keys()] // Array.keys()可以学一下,很有用
const res = []
while (arr.length) {
// 打乱
const randomIndex = Math.random() * arr.length - 1
res.push(arr.splice(randomIndex, 1)[0])
}
nums = [...nums, ...res]
}
经过上面操作,也就是我的nums中拥有4 * 180 = 720
个元素,nums中的元素都是0 - 179
范围内的
canvas画乱序数组
画canvas之前,肯定要现在html页面上,编写一个canvas的节点,这里我宽度设置1000,高度也是1000,并且背景颜色是黑色
<canvas id="canvas" width="1000" height="1000" style="background: #000;"></canvas>
上面看到了,极点(原点)是在坐标正中间的,但是canvas的初始原点是在画布的左上角,我们需要把canvas的原点移动到画布的正中间,那正中间的坐标是多少呢?还记得咱们宽高都是1000吗?那画布中心点坐标不就是(500, 500)
,咱们可以使用canvas的ctx.translate(500, 500)
来移动中心点位置。因为咱们画的点都是白色的,所以咱们顺便把ctx.fillStyle
设置为white
有一点注意了哈,canvas里的Y轴是自上向下的,与常规的Y轴的相反的。
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
ctx.fillStyle = 'white' // 设置画画的颜色
ctx.translate(500, 500) // 移动中心点到(500, 500)
那到底该怎么画点呢?按照之前的,其实光计算出角度θ
和极径ρ
是不够的,因为canvas画板不认这两个东西啊。。那canvas认啥呢,他只认(x, y)
,所以咱们只要通过角度θ
和极径ρ
去算出(x, y)
,就好了,还记得前面极坐标的公式吗
x = ρ * cosθ
,因为x / ρ = cosθ
y = ρ * sinθ
,因为y / ρ = sinθ
由于咱们是要铺散点是要铺出一个圆形来,那么一个圆形的角度是0° - 360°
,但是我们不要360°,咱们只要0° - 359°
,因为0°和360°
是同一个直线。咱们一个直线上有一个度数就够了。所以咱们要求出0° - 359°
每个角度所对应的cosθ和sinθ
(这里咱们只算整数角度,不算小数角度)
const CosandSin = []
for (let i = 0; i < 360; i++) {
const jiaodu = i / 180 * Math.PI
CosandSin.push({ cos: Math.cos(jiaodu), sin: Math.sin(jiaodu) })
}
这时候又有新问题了,咱们一个圆上的整数角度只有0° - 359°
这360个整数角
,但是nums
中有720个元素
啊,那怎么分配画布呢?很简单啊,一个角度上画2个元素,那不就刚好 2 * 360 = 720
行,咱们废话不多说,开始画初始散点吧。咱们也知道咱们需要画720个点,对于这种多个相同的东西,咱们要多多使用面向对象
这种编程思想
// 单个长方形构造函数
function Rect(x, y, width, height) {
this.x = x // 坐标x
this.y = y // 坐标y
this.width = width // 长方形的宽
this.height = height // 长方形的高
}
// 单个长方形的渲染函数
Rect.prototype.draw = function () {
ctx.beginPath() // 开始画一个
ctx.fillRect(this.x, this.y, this.width, this.height) // 画一个
ctx.closePath() // 结束画一个
}
const CosandSin = []
for (let i = 0; i < 360; i++) {
const jiaodu = i / 180 * Math.PI
CosandSin.push({ cos: Math.cos(jiaodu), sin: Math.sin(jiaodu) })
}
function drawAll(arr) {
const rects = [] // 用来存储720个长方形
for (let i = 0; i < arr.length; i++) {
const num = arr[i]
const { cos, sin } = CosandSin[Math.floor(i / 2)] // 一个角画两个
const x = num * cos // x = ρ * cosθ
const y = num * sin // y = ρ * sinθ
rects.push(new Rect(x, y, 5, 3)) // 收集所有长方形
}
rects.forEach(rect => rect.draw()) // 遍历渲染
}
drawAll(nums) // 执行渲染函数
来页面中看看效果吧。此时就完成了初始的散点渲染
边排序边重画
其实很简单,就是排序一次,就清空画布,然后重新执行上面的渲染函数drawAll
就行了。由于性能原因,我先把drawAll
封装成一个Promise函数
function drawAll(arr) {
return new Promise((resolve) => {
setTimeout(() => {
ctx.clearRect(-500, -500, 1000, 1000) // 清空画布
const rects = [] // 用来存储720个长方形
for (let i = 0; i < arr.length; i++) {
const num = arr[i]
const { cos, sin } = CosandSin[Math.floor(i / 2)] // 一个角画两个
const x = num * cos // x = ρ * cosθ
const y = num * sin // y = ρ * sinθ
rects.push(new Rect(x, y, 5, 3)) // 收集所有长方形
}
rects.forEach(rect => rect.draw()) // 遍历渲染
resolve('draw success')
}, 10)
})
}
然后咱们拿一个排序算法例子来讲一讲,就拿个冒泡排序
来讲吧
async function bubbleSort(arr) {
var len = arr.length;
for (var i = 0; i < len; i++) {
for (var j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) { //相邻元素两两对比
var temp = arr[j + 1]; //元素交换
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
await drawAll(arr) // 一边排序一边重新画
}
return arr;
}
然后在页面里放一个按钮,用来执行开始排序
<button id="btn">开始排序</button>
document.getElementById('btn').onclick = function () {
bubbleSort(nums)
}
效果如下,是不是很开心哈哈哈!!!
完整代码
这是完整代码
<canvas id="canvas" width="1000" height="1000" style="background: #000;"></canvas>
<button id="btn">开始排序</button>
复制代码
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
ctx.fillStyle = 'white' // 设置画画的颜色
ctx.translate(500, 500) // 移动中心点到(500, 500)
let nums = []
for (let i = 0; i < 4; i++) {
// 生成一个 0 - 180的有序数组
const arr = [...Array(180).keys()]
const res = []
while (arr.length) {
// 打乱
const randomIndex = Math.random() * arr.length - 1
res.push(arr.splice(randomIndex, 1)[0])
}
nums = [...nums, ...res]
}
// 单个长方形构造函数
function Rect(x, y, width, height) {
this.x = x // 坐标x
this.y = y // 坐标y
this.width = width // 长方形的宽
this.height = height // 长方形的高
}
// 单个长方形的渲染函数
Rect.prototype.draw = function () {
ctx.beginPath() // 开始画一个
ctx.fillRect(this.x, this.y, this.width, this.height) // 画一个
ctx.closePath() // 结束画一个
}
const CosandSin = []
for (let i = 0; i < 360; i++) {
const jiaodu = i / 180 * Math.PI
CosandSin.push({ cos: Math.cos(jiaodu), sin: Math.sin(jiaodu) })
}
function drawAll(arr) {
return new Promise((resolve) => {
setTimeout(() => {
ctx.clearRect(-500, -500, 1000, 1000) // 清空画布
const rects = [] // 用来存储720个长方形
for (let i = 0; i < arr.length; i++) {
const num = arr[i]
const { cos, sin } = CosandSin[Math.floor(i / 2)] // 一个角画两个
const x = num * cos // x = ρ * cosθ
const y = num * sin // y = ρ * sinθ
rects.push(new Rect(x, y, 5, 3)) // 收集所有长方形
}
rects.forEach(rect => rect.draw()) // 遍历渲染
resolve('draw success')
}, 10)
})
}
drawAll(nums) // 执行渲染函数
async function bubbleSort(arr) {
var len = arr.length;
for (var i = 0; i < len; i++) {
for (var j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) { //相邻元素两两对比
var temp = arr[j + 1]; //元素交换
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
await drawAll(arr) // 一边排序一边重新画
}
return arr;
}
document.getElementById('btn').onclick = function () {
bubbleSort(nums) // 点击执行
}
正片开始!!!
首先说明,哈哈
- 我是算法渣渣
- 每种算法排序,动画都不一样
- drawAll放在不同地方也可能有不同效果
冒泡排序
async function bubbleSort(arr) {
var len = arr.length;
for (var i = 0; i < len; i++) {
for (var j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) { //相邻元素两两对比
var temp = arr[j + 1]; //元素交换
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
await drawAll(arr) // 一边排序一边重新画
}
return arr;
}
document.getElementById('btn').onclick = function () {
bubbleSort(nums) // 点击执行
}
选择排序
async function selectionSort(arr) {
var len = arr.length;
var minIndex, temp;
for (var i = 0; i < len - 1; i++) {
minIndex = i;
for (var j = i + 1; j < len; j++) {
if (arr[j] < arr[minIndex]) { //寻找最小的数
minIndex = j; //将最小数的索引保存
}
}
temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
await drawAll(arr)
}
return arr;
}
document.getElementById('btn').onclick = function () {
selectionSort(nums)
}
插入排序
async function insertionSort(arr) {
if (Object.prototype.toString.call(arr).slice(8, -1) === 'Array') {
for (var i = 1; i < arr.length; i++) {
var key = arr[i];
var j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
await drawAll(arr)
}
return arr;
} else {
return 'arr is not an Array!';
}
}
document.getElementById('btn').onclick = function () {
insertionSort(nums)
}
堆排序
async function heapSort(array) {
if (Object.prototype.toString.call(array).slice(8, -1) === 'Array') {
//建堆
var heapSize = array.length, temp;
for (var i = Math.floor(heapSize / 2) - 1; i >= 0; i--) {
heapify(array, i, heapSize);
await drawAll(array)
}
//堆排序
for (var j = heapSize - 1; j >= 1; j--) {
temp = array[0];
array[0] = array[j];
array[j] = temp;
heapify(array, 0, --heapSize);
await drawAll(array)
}
return array;
} else {
return 'array is not an Array!';
}
}
function heapify(arr, x, len) {
if (Object.prototype.toString.call(arr).slice(8, -1) === 'Array' && typeof x === 'number') {
var l = 2 * x + 1, r = 2 * x + 2, largest = x, temp;
if (l < len && arr[l] > arr[largest]) {
largest = l;
}
if (r < len && arr[r] > arr[largest]) {
largest = r;
}
if (largest != x) {
temp = arr[x];
arr[x] = arr[largest];
arr[largest] = temp;
heapify(arr, largest, len);
}
} else {
return 'arr is not an Array or x is not a number!';
}
}
document.getElementById('btn').onclick = function () {
heapSort(nums)
}
快速排序
async function quickSort(array, left, right) {
drawAll(nums)
if (Object.prototype.toString.call(array).slice(8, -1) === 'Array' && typeof left === 'number' && typeof right === 'number') {
if (left < right) {
var x = array[right], i = left - 1, temp;
for (var j = left; j <= right; j++) {
if (array[j] <= x) {
i++;
temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
await drawAll(nums)
await quickSort(array, left, i - 1);
await quickSort(array, i + 1, right);
await drawAll(nums)
}
return array;
} else {
return 'array is not an Array or left or right is not a number!';
}
}
document.getElementById('btn').onclick = function () {
quickSort(nums, 0, nums.length - 1)
}
基数排序
async function radixSort(arr, maxDigit) {
var mod = 10;
var dev = 1;
var counter = [];
for (var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
for (var j = 0; j < arr.length; j++) {
var bucket = parseInt((arr[j] % mod) / dev);
if (counter[bucket] == null) {
counter[bucket] = [];
}
counter[bucket].push(arr[j]);
}
var pos = 0;
for (var j = 0; j < counter.length; j++) {
var value = null;
if (counter[j] != null) {
while ((value = counter[j].shift()) != null) {
arr[pos++] = value;
await drawAll(arr)
}
}
}
}
return arr;
}
document.getElementById('btn').onclick = function () {
radixSort(nums, 3)
}
希尔排序
async function shellSort(arr) {
var len = arr.length,
temp,
gap = 1;
while (gap < len / 5) { //动态定义间隔序列
gap = gap * 5 + 1;
}
for (gap; gap > 0; gap = Math.floor(gap / 5)) {
for (var i = gap; i < len; i++) {
temp = arr[i];
for (var j = i - gap; j >= 0 && arr[j] > temp; j -= gap) {
arr[j + gap] = arr[j];
}
arr[j + gap] = temp;
await drawAll(arr)
}
}
return arr;
}
document.getElementById('btn').onclick = function () {
shellSort(nums)
}
参考
- 排序算法参考:十大经典排序算法总结(JavaScript描述)
总结
如果你觉得此文对你有一丁点帮助,点个赞,鼓励一下林三心哈哈。或者可以加入我的摸鱼群 想进学习群,摸鱼群,请点击这里摸鱼
作者:Sunshine_Lin
来源:https://juejin.cn/post/7004454008634998821
JavaScript复制内容到剪贴板
最近一个活动页面中有一个小需求,用户点击或者长按就可以复制内容到剪贴板,记录一下实现过程和遇到的坑。
常见方法
查了一下万能的Google,现在常见的方法主要是以下两种:
- 第三方库:clipboard.js
- 原生方法:document.execCommand()
分别来看看这两种方法是如何使用的。
clipboard.js
这是clipboard的官网:clipboardjs.com/,看起来就是这么的简单。
引用
直接引用: <script src="dist/clipboard.min.js"></script>
包: npm install clipboard --save
,然后 import Clipboard from 'clipboard';
使用
从输入框复制
现在页面上有一个 <input>
标签,我们需要复制其中的内容,我们可以这样做:
<input id="demoInput" value="hello world">
<button class="btn" data-clipboard-target="#demoInput">点我复制</button>
import Clipboard from 'clipboard';
const btnCopy = new Clipboard('btn');
注意到,在 <button>
标签中添加了一个 data-clipboard-target
属性,它的值是需要复制的 <input>
的 id
,顾名思义是从整个标签中复制内容。
直接复制
有的时候,我们并不希望从 <input>
中复制内容,仅仅是直接从变量中取值。如果在 Vue
中我们可以这样做:
<button class="btn" :data-clipboard-text="copyValue">点我复制</button>
import Clipboard from 'clipboard';
const btnCopy = new Clipboard('btn');
this.copyValue = 'hello world';
事件
有的时候我们需要在复制后做一些事情,这时候就需要回调函数的支持。
在处理函数中加入以下代码:
// 复制成功后执行的回调函数
clipboard.on('success', function(e) {
console.info('Action:', e.action); // 动作名称,比如:Action: copy
console.info('Text:', e.text); // 内容,比如:Text:hello word
console.info('Trigger:', e.trigger); // 触发元素:比如:<button :data-clipboard-text="copyValue">点我复制</button>
e.clearSelection(); // 清除选中内容
});
// 复制失败后执行的回调函数
clipboard.on('error', function(e) {
console.error('Action:', e.action);
console.error('Trigger:', e.trigger);
});
小结
文档中还提到,如果在单页面中使用 clipboard
,为了使得生命周期管理更加的优雅,在使用完之后记得 btn.destroy()
销毁一下。
clipboard
使用起来是不是很简单。但是,就为了一个 copy
功能就使用额外的第三方库是不是不够优雅,这时候该怎么办?那就用原生方法实现呗。
document.execCommand()方法
先看看这个方法在 MDN
上是怎么定义的:
which allows one to run commands to manipulate the contents of the editable region.
意思就是可以允许运行命令来操作可编辑区域的内容,注意,是可编辑区域。
定义
bool = document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)
方法返回一个 Boolean
值,表示操作是否成功。
aCommandName
:表示命令名称,比如:copy
,cut
等(更多命令见命令);aShowDefaultUI
:是否展示用户界面,一般情况下都是false
;aValueArgument
:有些命令需要额外的参数,一般用不到;
兼容性
这个方法在之前的兼容性其实是不太好的,但是好在现在已经基本兼容所有主流浏览器了,在移动端也可以使用。
使用
从输入框复制
现在页面上有一个 <input>
标签,我们想要复制其中的内容,我们可以这样做:
<input id="demoInput" value="hello world">
<button id="btn">点我复制</button>
const btn = document.querySelector('#btn');
btn.addEventListener('click', () => {
const input = document.querySelector('#demoInput');
input.select();
if (document.execCommand('copy')) {
document.execCommand('copy');
console.log('复制成功');
}
})
其它地方复制
有的时候页面上并没有 <input>
标签,我们可能需要从一个 <div>
中复制内容,或者直接复制变量。
还记得在 execCommand()
方法的定义中提到,它只能操作可编辑区域,也就是意味着除了 <input>
、<textarea>
这样的输入域以外,是无法使用这个方法的。
这时候我们需要曲线救国。
<button id="btn">点我复制</button>
const btn = document.querySelector('#btn');
btn.addEventListener('click',() => {
const input = document.createElement('input');
document.body.appendChild(input);
input.setAttribute('value', '听说你想复制我');
input.select();
if (document.execCommand('copy')) {
document.execCommand('copy');
console.log('复制成功');
}
document.body.removeChild(input);
})
算是曲线救国成功了吧。在使用这个方法时,遇到了几个坑。
遇到的坑
在Chrome下调试的时候,这个方法时完美运行的。然后到了移动端调试的时候,坑就出来了。
对,没错,就是你,ios。。。
点击复制时屏幕下方会出现白屏抖动,仔细看是拉起键盘又瞬间收起
知道了抖动是由于什么产生的就比较好解决了。既然是拉起键盘,那就是聚焦到了输入域,那只要让输入域不可输入就好了,在代码中添加
input.setAttribute('readonly', 'readonly');
使这个<input>
是只读的,就不会拉起键盘了。
无法复制
这个问题是由于
input.select()
在ios下并没有选中全部内容,我们需要使用另一个方法来选中内容,这个方法就是input.setSelectionRange(0, input.value.length);
。
完整代码如下:
const btn = document.querySelector('#btn');
btn.addEventListener('click',() => {
const input = document.createElement('input');
input.setAttribute('readonly', 'readonly');
input.setAttribute('value', 'hello world');
document.body.appendChild(input);
input.setSelectionRange(0, 9999);
if (document.execCommand('copy')) {
document.execCommand('copy');
console.log('复制成功');
}
document.body.removeChild(input);
})
作者:axuebin
链接:https://juejin.cn/post/6844903567480848391
收起阅读 »
前端vue面霸修炼手册!!
一、对MVVM的理解
MVVM全称是Model-View-ViewModel
Model 代表数据模型,数据和业务逻辑都在Model层中定义;泛指后端进行的各种业务逻辑处理和数据操控,对于前端来说就是后端提供的 api 接口。View 代表UI视图,负责数据的展示;视图层,也就是用户界面。前端主要由 HTML 和 CSS 来构建 。ViewModel 负责监听 Model 中数据的改变并且控制视图的更新,处理用户交互操作;
Vue是以数据为驱动的,Vue自身将DOM和数据进行绑定,一旦创建绑定,DOM和数据将保持同步,每当数据发生变化,DOM会跟着变化。 ViewModel是Vue的核心,它是Vue的一个实例。Vue实例时作用域某个HTML元素上的这个HTML元素可以是body,也可以是某个id所指代的元素。
二、vue常见指令
v-textv-text 主要用来更新 textContent,可以等同于 JS 的 text 属性。
<span v-text="name"></span>
或
<span插值表达式{{name}}</span>
v-html等同于 JS 的 innerHtml 属性
<div v-html="content"></div>
v-cloak用来保持在元素上直到关联实例结束时进行编译 解决闪烁问题
<div id="app" v-cloak>
<div>
{{msg}}
</div>
</div>
<script type="text/javascript">
new Vue({
el:'#app',
data:{
msg:'hello world'
}
})
</script>
正常在页面加载时会闪烁,先显示:
<div>
{{msg}}
</div>
编译后才显示:
<div>
hello world!
</div>
可以用 v-cloak 指令解决插值表达式闪烁问题,v-cloak 在 css 中用属性选择器设置为 display: none;
v-oncev-once 关联的实例,只会渲染一次。之后的重新渲染,实例极其所有的子节点将被视为静态内容跳过,这可以用于优化更新性能
<span v-once>This will never change:{{msg}}</span> //单个元素
<div v-once>//有子元素
<h1>comment</h1>
<p>{{msg}}</p>
</div>
<my-component v-once:comment="msg"></my-component> //组件
<ul>
<li v-for="i in list">{{i}}</li>
</ul>
上面的例子中,msg,list 即使产生改变,也不会重新渲染。
v-ifv-if 可以实现条件渲染,Vue 会根据表达式的值的真假条件来渲染元素
<a v-if="true">show</a>
v-elsev-else 是搭配 v-if 使用的,它必须紧跟在 v-if 或者 v-else-if 后面,否则不起作用
<a v-if="true">show</a>
<a v-else>hide</a>
v-else-ifv-else-if 充当 v-if 的 else-if 块, 可以链式的使用多次。可以更加方便的实现 switch 语句。
<div v-if="type==='A'">
A
</div>
<div v-else-if="type==='B'">
B
</div>
<div v-else-if="type==='C'">
C
</div>
<div v-else>
Not A,B,C
</div>
v-show也是用于根据条件展示元素。和 v-if 不同的是,如果 v-if 的值是 false,则这个元素被销毁,不在 dom 中。但是 v-show 的元素会始终被渲染并保存在 dom 中,它只是简单的切换 css 的 dispaly 属性。
<span v-show="true">hello world</span >
注意:v-if 有更高的切换开销 v-show 有更高的初始渲染开销。因此,如果要非常频繁的切换, 则使用 v-show 较好;如果在运行时条件不太可能改变,则 v-if 较好
v-for用 v-for 指令根据遍历数组来进行渲染
<div v-for="(item,index) in items"></div> //使用in,index是一个可选参数,表示当前项的索引
v-bindv-bind 用来动态的绑定一个或者多个特性。没有参数时,可以绑定到一个包含键值对的对象。常用于动态绑定 class 和 style。以及 href 等。简写为一个冒号【 :】
<div id="app">
<div :class="{'is-active':isActive, 'text-danger':hasError}"></div>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
isActive: true,
hasError: false
}
})
</script>
编译后
<div class = "is-active"></div>
v-model用于在表单上创建双向数据绑定
<div id="app">
<input v-model="name">
<p>hello {{name}}</p>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
name:'小明'
}
})
</script>
model 修饰符有
.lazy(在 change 事件再同步) > v-model.lazy .number(自动将用户的输入值转化为数值类型) > v-model.number .trim(自动过滤用户输入的首尾空格) > v-model.trim
v-onv-on 主要用来监听 dom 事件,以便执行一些代码块。表达式可以是一个方法名。 简写为:【 @ 】
<div id="app">
<button @click="consoleLog"></button>
</div>
<script>
var app = new Vue({
el: '#app',
methods:{
consoleLog:function (event) {
console.log(1)
}
}
})
</script>
事件修饰符
.stop 阻止事件继续传播 .prevent 事件不再重载页面 .capture 使用事件捕获模式,即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理 .self 只当在 event.target 是当前元素自身时触发处理函数 .once 事件将只会触发一次 .passive 告诉浏览器你不想阻止事件的默认行为
三 、v-if 和 v-show 有什么区别?
共同点:v-if 和 v-show 都能实现元素的显示隐藏
区别:
v-show 只是简单的控制元素的 display 属性 而 v-if 才是条件渲染(条件为真,元素将会被渲染,条件为假,元素会被销毁) 2. v-show 有更高的首次渲染开销,而 v-if 的首次渲染开销要小的多 3. v-if 有更高的切换开销,v-show 切换开销小 4. v-if 有配套的 v-else-if 和 v-else,而 v-show 没有 5. v-if 可以搭配 template 使用,而 v-show 不行
四、如何让CSS只在当前组件中起作用?
将组件样式加上 scoped
<style scoped>
...
</style>
五、 keep-alive的作用是什么?
keep-alive包裹动态组件时,会缓存不活动的组件实例, 主要用于保留组件状态或避免重新渲染。
六、在Vue中使用插件的步骤
采用ES6的 import … from …
语法 或 CommonJSd的 require()
方法引入插件 2、使用全局方法 Vue.use( plugin )
使用插件,可以传入一个选项对象 Vue.use(MyPlugin, { someOption: true })
七、Vue 生命周期
八、Vue 组件间通信有哪几种方式
Vue 组件间通信只要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信
九、computed 和 watch 的区别和运用的场景
computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值
watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作
运用场景:
当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed
当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch
十、vue-router 路由模式有几种
Hash: 使用 URL 的 hash 值来作为路由。支持所有浏览器。
History: 以来 HTML5 History API 和服务器配置。参考官网中 HTML5 History 模式
Abstract: 支持所有 javascript 运行模式。如果发现没有浏览器的 API,路由会自动强制进入这个模式。
十一、SPA 单页面的理解,它的优缺点分别是什么
SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS 一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转 取而代之的是利用路由机制实现 HTML 内容的变换, UI 与用户的交互,避免页面的重新加载。
优点:
1、用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染
2、基于上面一点,SPA 相对对服务器压力小
3、前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理
缺点:
1、初次加载耗时多:为实现单页 Web 应用功能及显示效果, 需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载
2、前进后退路由管理:由于单页应用在一个页面中显示所有的内容, 所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理
3、SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势
不想加班,你就背会这 10 条 JS 技巧
为了让自己写的代码更优雅且高效,特意向大佬请教了这 10 条 JS 技巧
1. 数组分割
const listChunk = (list = [], chunkSize = 1) => {
const result = [];
const tmp = [...list];
if (!Array.isArray(list) || !Number.isInteger(chunkSize) || chunkSize <= 0) {
return result;
};
while (tmp.length) {
result.push(tmp.splice(0, chunkSize));
};
return result;
};
listChunk(['a', 'b', 'c', 'd', 'e', 'f', 'g']);
// [['a'], ['b'], ['c'], ['d'], ['e'], ['f'], ['g']]
listChunk(['a', 'b', 'c', 'd', 'e', 'f', 'g'], 3);
// [['a', 'b', 'c'], ['d', 'e', 'f'], ['g']]
listChunk(['a', 'b', 'c', 'd', 'e', 'f', 'g'], 0);
// []
listChunk(['a', 'b', 'c', 'd', 'e', 'f', 'g'], -1);
// []
2. 求数组元素交集
const listIntersection = (firstList, ...args) => {
if (!Array.isArray(firstList) || !args.length) {
return firstList;
}
return firstList.filter(item => args.every(list => list.includes(item)));
};
listIntersection([1, 2], [3, 4]);
// []
listIntersection([2, 2], [3, 4]);
// []
listIntersection([3, 2], [3, 4]);
// [3]
listIntersection([3, 4], [3, 4]);
// [3, 4]
3. 按下标重新组合数组
const zip = (firstList, ...args) => {
if (!Array.isArray(firstList) || !args.length) {
return firstList
};
return firstList.map((value, index) => {
const newArgs = args.map(arg => arg[index]).filter(arg => arg !== undefined);
const newList = [value, ...newArgs];
return newList;
});
};
zip(['a', 'b'], [1, 2], [true, false]);
// [['a', 1, true], ['b', 2, false]]
zip(['a', 'b', 'c'], [1, 2], [true, false]);
// [['a', 1, true], ['b', 2, false], ['c']]
4. 按下标组合数组为对象
const zipObject = (keys, values = {}) => {
const emptyObject = Object.create({});
if (!Array.isArray(keys)) {
return emptyObject;
};
return keys.reduce((acc, cur, index) => {
acc[cur] = values[index];
return acc;
}, emptyObject);
};
zipObject(['a', 'b'], [1, 2])
// { a: 1, b: 2 }
zipObject(['a', 'b'])
// { a: undefined, b: undefined }
5. 检查对象属性的值
const checkValue = (obj = {}, objRule = {}) => {
const isObject = obj => {
return Object.prototype.toString.call(obj) === '[object Object]';
};
if (!isObject(obj) || !isObject(objRule)) {
return false;
}
return Object.keys(objRule).every(key => objRule[key](obj[key]));
};
const object = { a: 1, b: 2 };
checkValue(object, {
b: n => n > 1,
})
// true
checkValue(object, {
b: n => n > 2,
})
// false
6. 获取对象属性
const get = (obj, path, defaultValue) => {
if (!path) {
return;
};
const pathGroup = Array.isArray(path) ? path : path.match(/([^[.\]])+/g);
return pathGroup.reduce((prevObj, curKey) => prevObj && prevObj[curKey], obj) || defaultValue;
};
const obj1 = { a: { b: 2 } }
const obj2 = { a: [{ bar: { c: 3 } }] }
get(obj1, 'a.b')
// 2
get(obj2, 'a[0].bar.c')
// 3
get(obj2, ['a', '0', 'bar', 'c'])
// 2
get(obj1, 'a.bar.c', 'default')
// default
get(obj1, 'a.bar.c', 'default')
// default
7. 将特殊符号转成字体符号
const escape = str => {
const isString = str => {
return Object.prototype.toString.call(str) === '[string Object]';
};
if (!isString(str)) {
return str;
}
return (str.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/\//g, '/')
.replace(/\\/g, '\')
.replace(/`/g, '`'));
};
8. 利用注释创建一个事件监听器
class EventEmitter {
#eventTarget;
constructor(content = '') {
const comment = document.createComment(content);
document.documentElement.appendChild(comment);
this.#eventTarget = comment;
}
on(type, listener) {
this.#eventTarget.addEventListener(type, listener);
}
off(type, listener) {
this.#eventTarget.removeEventListener(type, listener);
}
once(type, listener) {
this.#eventTarget.addEventListener(type, listener, { once: true });
}
emit(type, detail) {
const dispatchEvent = new CustomEvent(type, { detail });
this.#eventTarget.dispatchEvent(dispatchEvent);
}
};
const emmiter = new EventEmitter();
emmiter.on('biy', () => {
console.log('hello world');
});
emmiter.emit('biu');
// hello world
9. 生成随机的字符串
const genRandomStr = (len = 1) => {
let result = '';
for (let i = 0; i < len; ++i) {
result += Math.random().toString(36).substr(2)
}
return result.substr(0, len);
}
genRandomStr(3)
// u2d
genRandomStr()
// y
genRandomStr(10)
// qdueun65jb
10. 判断是否是指定的哈希值
const isHash = (type = '', str = '') => {
const isString = str => {
return Object.prototype.toString.call(str) === '[string Object]';
};
if (!isString(type) || !isString(str)) {
return str;
};
const algorithms = {
md5: 32,
md4: 32,
sha1: 40,
sha256: 64,
sha384: 96,
sha512: 128,
ripemd128: 32,
ripemd160: 40,
tiger128: 32,
tiger160: 40,
tiger192: 48,
crc32: 8,
crc32b: 8,
};
const hash = new RegExp(`^[a-fA-F0-9]{${algorithms[type]}}$`);
return hash.test(str);
};
isHash('md5', 'd94f3f016ae679c3008de268209132f2');
// true
isHash('md5', 'q94375dj93458w34');
// false
isHash('sha1', '3ca25ae354e192b26879f651a51d92aa8a34d8d3');
// true
isHash('sha1', 'KYT0bf1c35032a71a14c2f719e5a14c1');
// false
后记
如果你喜欢探讨技术,或者对本文有任何的意见或建议,非常欢迎加鱼头微信好友一起探讨,当然,鱼头也非常希望能跟你一起聊生活,聊爱好,谈天说地。
全文完
作者:酸菜鱼+黄焖鸡
来源:https://blog.51cto.com/u_15291238/4538068
收起阅读 »尤大亲自解释vue3源码中为什么不使用?.可选链式操作符?
阅读本文🦀
1.什么是可选链式操作符号
2.为什么vue3源码中不使用可选链式操作符
什么是可选链式操作符号❓
可选链操作符( ?.
)允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。?.
操作符的功能类似于 .
链式操作符,不同之处在于,在引用为空(nullish ) (null
或者 undefined
) 的情况下不会引起错误,该表达式短路返回值是 undefined
。与函数调用一起使用时,如果给定的函数不存在,则返回 undefined
。
当尝试访问可能不存在的对象属性时,可选链操作符将会使表达式更短、更简明。在探索一个对象的内容时,如果不能确定哪些属性必定存在,可选链操作符也是很有帮助的。
const adventurer = {
name: 'Alice',
cat: {
name: 'Dinah'
}
};
const dogName = adventurer.dog?.name;
console.log(dogName);
// expected output: undefined
console.log(adventurer.someNonExistentMethod?.());
// expected output: undefined
短路效应
如果 ?.
左边部分不存在,就会立即停止运算(“短路效应”)。
所以,如果后面有任何函数调用或者副作用,它们均不会执行。
let user = null;
let x = 0;
user?.sayHi(x++);
// 没有 "sayHi",因此代码执行没有触达 x++ alert(x); // 0,值没有增加
Vue3源码中为什么不采用这么方便的操作符
看看这样是不是代码更简洁了,但是为什么这个PR没有被合并呢
来自尤大的亲自解释
(我们有意避免在代码库中使用可选链,因为我们的目标是 ES2016,而 TS 会将其转换为更加冗长的内容)
从尤大的话中我们可以得知由于Vu3打包后的代码是基于ES2016的,虽然我们在编写代码时看起来代码比较简洁了,实际打包之后反而更冗余了,这样会增大包的体积,影响Vu3的加载速度。由此可见一个优秀的前端框架真的要考虑的东西很多,语法也会考虑周到~✨
作者:速冻鱼
链接:https://juejin.cn/post/7033167068895641637
收起阅读 »
想知道一个20k级别前端在项目中是怎么使用LocalStorage的吗?
前言
大家好,我是林三心,用最通俗的话,讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心,今天就给大家唠一下嗑,讲一下,怎么样使用localStorage、sessionStorage
,才能更规范,更高大上,更能让人眼前一亮。
用处
在平时的开发中,localStorage、sessionStorage
的用途是非常的多的,在我们的开发中发挥着非常重要的作用:
- 1、登录完成后
token
的存储 - 2、用户部分信息的存储,比如
昵称、头像、简介
- 3、一些项目通用参数的存储,例如
某个id、某个参数params
- 4、项目状态管理的持久化,例如
vuex的持久化、redux的持久化
- 5、项目整体的切换状态存储,例如
主题颜色、icon风格、语言标识
- 6、等等、、、、、、、、、、、、、、、、、、、、、、、、、、
普通使用
那么,相信我们各位平时使用都是这样的(拿localStorage
举例)
1、基础变量
// 当我们存基本变量时
localStorage.setItem('基本变量', '这是一个基本变量')
// 当我们取值时
localStorage.getItem('基本变量')
// 当我们删除时
localStorage.removeItem('基本变量')
2、引用变量
// 当我们存引用变量时
localStorage.setItem('引用变量', JSON.stringify(data))
// 当我们取值时
const data = JSON.parse(localStorage.getItem('引用变量'))
// 当我们删除时
localStorage.removeItem('引用变量')
3、清空
localStorage.clear()
暴露出什么问题?
1、命名过于简单
- 1、比如我们存用户信息会使用
user
作为 key 来存储 - 2、存储主题的时候用
theme
作为 key 来存储 - 3、存储令牌时使用
token
作为 key 来存储
其实这是很有问题的,咱们都知道,同源的两个项目,它们的localStorage
是互通的。
我举个例子吧比如我现在有两个项目,它们在同源https://www.sunshine.com
下,这两个项目都需要往localStorage
中存储一个 key 为name
的值,那么这就会造成两个项目的name
互相顶替的现象,也就是互相污染现象
:
2、时效性
咱们都知道localStorage、sessionStorage
这两个的生命周期分别是
- localStorage:除非手动清除,否则一直存在
- sessionStorage:生命结束于当前标签页的关闭或浏览器的关闭
其实平时普通的使用时没什么问题的,但是给某些指定缓存加上特定的时效性,是非常重要的!比如某一天:
- 后端:”兄弟,你一登录我就把token给你“
- 前端:”好呀,那你应该会顺便判断token过期没吧?“
- 后端:”不行哦,放在你前端判断过期呗“
- 前端:”行吧。。。。。“
那这时候,因为需要在前端判断过期,所以咱们就得给token
设置一个时效性,或者是1天
,或者是7天
3、隐秘性
其实这个好理解,你们想想,当咱们把咱们想缓存的东西,存在localStorage、sessionStorage
中,在开发过程中,确实有利于咱们的开发,咱们想看的时候也是一目了然,点击Application
就可以看到。
但是,一旦产品上线了,用户也是可以看到缓存中的东西的,而咱们肯定是会想:有些东西可以让用户看到,但是有些东西我不想让你看到
或者咱们在做状态管理持久化
时,需要把数据先存在localStorage
中,这个时候就很有必要对缓存进行加密了。
解决方案
1、命名规范
我个人的看法是项目名 + 当前环境 + 项目版本 + 缓存key
,如果大家有其他规则的,可以评论区告诉林三心,让林三心学学
2、expire定时
思路:设置缓存key
时,将value
包装成一个对象,对象中有相应的时效时段
,当下一次想获取缓存值时,判断有无超时,不超时就获取value
,超时就删除这个缓存
3、crypto加密
加密很简单,直接使用crypto-js
进行对数据的加密,使用这个库里的encrypt、decrypyt
进行加密、解密
实践
其实实践的话比较简单啦,无非就是四步
- 1、与团队商讨一下
key
的格式 - 2、与团队商讨一下
expire
的长短 - 3、与团队商讨一下使用哪个库来对缓存进行加密(个人建议
crypto-js
) - 4、代码实施(不难,我这里就不写了)
结语
有人可能觉得没必要,但是严格要求自己其实是很有必要的,平时严格要求自己,才能做到每到一个公司都能更好的做到向下兼容难度。
如果你觉得此文对你有一丁点帮助,点个赞,鼓励一下林三心哈哈。或者可以加入我的摸鱼群 想进学习群,摸鱼群,请点击这里摸鱼,我会定时直播模拟面试,简历指导,答疑解惑
作者:Sunshine_Lin
链接:https://juejin.cn/post/7033749571939336228
收起阅读 »
巧用渐变实现高级感拉满的背景光动画
实现
这个效果想利用 CSS 完全复制是比较困难的。CSS 模拟出来的光效阴影相对会 Low 一点,只能说是尽量还原。
其实每组光都基本是一样的,所以我们只需要实现其中一组,就几乎能实现了整个效果。
观察这个效果:
它的核心其实就是角向渐变 -- conic-gradient()
,利用角向渐变,我们可以大致实现这样一个效果:
<div></div>
div {
width: 1000px;
height: 600px;
background:
conic-gradient(
from -45deg at 400px 300px,
hsla(170deg, 100%, 70%, .7),
transparent 50%,
transparent),
linear-gradient(-45deg, #060d5e, #002268);
}
看看效果:
有点那意思了。当然,仔细观察,渐变的颜色并非是由一种颜色到透明就结束了,而是颜色 A -- 透明 -- 颜色 B,这样,光源的另一半并非就不会那么生硬,改造后的 CSS 代码:
div {
width: 1000px;
height: 600px;
background:
conic-gradient(
from -45deg at 400px 300px,
hsla(170deg, 100%, 70%, .7),
transparent 50%,
hsla(219deg, 90%, 80%, .5) 100%),
linear-gradient(-45deg, #060d5e, #002268);
}
我们在角向渐变的最后多加了一种颜色,得到观感更好的一种效果:
emm,到这里,我们会发现,仅仅是角向渐变 conic-gradient()
是不够的,它无法模拟出光源阴影的效果,所以必须再借助其他属性实现光源阴影的效果。
这里,我们会很自然的想到 box-shadow
。这里有个技巧,利用多重 box-shadow
, 实现 Neon 灯的效果。
我们再加个 div,通过它实现光源阴影:
<div class="shadow"></div>
.shadow {
width: 200px;
height: 200px;
background: #fff;
box-shadow:
0px 0 .5px hsla(170deg, 95%, 80%, 1),
0px 0 1px hsla(170deg, 91%, 80%, .95),
0px 0 2px hsla(171deg, 91%, 80%, .95),
0px 0 3px hsla(171deg, 91%, 80%, .95),
0px 0 4px hsla(171deg, 91%, 82%, .9),
0px 0 5px hsla(172deg, 91%, 82%, .9),
0px 0 10px hsla(173deg, 91%, 84%, .9),
0px 0 20px hsla(174deg, 91%, 86%, .85),
0px 0 40px hsla(175deg, 91%, 86%, .85),
0px 0 60px hsla(175deg, 91%, 86%, .85);
}
OK,光是有了,但问题是我们只需要一侧的光,怎么办呢?裁剪的方式很多,这里,我介绍一种利用 clip-path
进行对元素任意空间进行裁切的方法:
.shadow {
width: 200px;
height: 200px;
background: #fff;
box-shadow: .....;
clip-path: polygon(-100% 100%, 200% 100%, 200% 500%, -100% 500%);
}
原理是这样的:
这样,我们就得到了一侧的光:
这里,其实 CSS 也是有办法实现单侧阴影的(你所不知道的 CSS 阴影技巧与细节),但是实际效果并不好,最终采取了上述的方案。
接下来,就是利用定位、旋转等方式,将上述单侧光和角向渐变重叠起来,我们就可以得到这样的效果:
这会,已经挺像了。接下来要做的就是让整个图案,动起来。这里技巧也挺多的,核心还是利用了 CSS @Property,实现了角向渐变的动画,并且让光动画和角向渐变重叠起来。
我们需要利用 CSS @Property 对代码渐变进行改造,核心代码如下:
<div class="wrap">
<div class="shadow"></div>
</div>
@property --xPoint {
syntax: '<length>';
inherits: false;
initial-value: 400px;
}
@property --yPoint {
syntax: '<length>';
inherits: false;
initial-value: 300px;
}
.wrap {
position: relative;
margin: auto;
width: 1000px;
height: 600px;
background:
conic-gradient(
from -45deg at var(--xPoint) var(--yPoint),
hsla(170deg, 100%, 70%, .7),
transparent 50%,
hsla(219deg, 90%, 80%, .5) 100%),
linear-gradient(-45deg, #060d5e, #002268);
animation: pointMove 2.5s infinite alternate linear;
}
.shadow {
position: absolute;
top: -300px;
left: -330px;
width: 430px;
height: 300px;
background: #fff;
transform-origin: 100% 100%;
transform: rotate(225deg);
clip-path: polygon(-100% 100%, 200% 100%, 200% 500%, -100% 500%);
box-shadow: ... 此处省略大量阴影代码;
animation: scale 2.5s infinite alternate linear;
}
@keyframes scale {
50%,
100% {
transform: rotate(225deg) scale(0);
}
}
@keyframes pointMove {
100% {
--xPoint: 100px;
--yPoint: 0;
}
}
这样,我们就实现了完整的一处光的动画:
我们重新梳理一下,实现这样一个动画的步骤:
- 利用角向渐变
conic-gradient
搭出基本框架,并且,这里也利用了多重渐变,角向渐变的背后是深色背景色; - 利用多重
box-shadow
实现光及阴影的效果(又称为 Neon 效果) - 利用
clip-path
对元素进行任意区域的裁剪 - 利用 CSS @Property 实现渐变的动画效果
剩下的工作,就是重复上述的步骤,补充其他渐变和光源,调试动画,最终,我们就可以得到这样一个简单的模拟效果:
由于原效果是 .mp4
,无法拿到其中的准确颜色,无法拿到阴影的参数,其中颜色是直接用的色板取色,阴影则比较随意的模拟了下,如果有源文件,准确参数,可以模拟的更逼真。
完整的代码你可以戳这里:CodePen -- iPhone 13 Pro Gradient
作者:chokcoco
链接:https://juejin.cn/post/7033952765151805453
收起阅读 »
vite对浏览器的请求做了什么
工作原理:
type="module"
浏览器中ES Module原生native支持。 如果浏览器支持type="module"
,我i们可以使用es6模块化的方式编写。浏览器会把我们需要导入的文件再发一次http请求,再发到服务器上。 开发阶段不需要打包- 第三方依赖预打包
- 启动一个开发服务器处理资源请求
type="module"
浏览器中ES Module原生native支持。 如果浏览器支持type="module"
,我i们可以使用es6模块化的方式编写。浏览器会把我们需要导入的文件再发一次http请求,再发到服务器上。 开发阶段不需要打包一图详解vite原理:
浏览器做的什么事啊
宿主文件index.html
<script type="module" src="/src/main.js"></script>
浏览器获取到宿主文件中的资源后,发现还要再去请求main.js文件。会再向服务端发送一次main.js的资源请求。
main.js
在main中,可以发现,浏览器又再次发起对vue.js?v=d253a66c
、App.vue?t=1637479953836
两个文件的资源请求。
服务器会将App.vue中的内容进行编译然后返回给浏览器,下图可以看出logo图片和文字都被编译成_hoisted_
的静态节点。
从请求头中,也可以看出sfc文件已经变成浏览器可以识别的js文件(app.vue文件中要存在script内容才会编译成js)。对于浏览器来说,执行的就是一段js代码。
其他裸模块
如果vue依赖中还存在其他依赖的话,浏览器依旧会再次发起资源请求,获取相应资源。
了解一下预打包
对于第三方依赖(裸模块)的加载,vite对其提前做好打包工作,将其放到node_modules/.vite下。当启动项目的时候,直接从该路径下下载文件。
通过上图,可以看到再裸模块的引入时,路径发生了改变。
服务器做的什么事啊
总结一句话:服务器把特殊后缀名的文件进行处理返回给前端展示。
我们可以模拟vite的devServe,使用koa中间件启动一个本地服务。
// 引入依赖
const Koa = require('koa')
const app = new Koa()
const fs = require('fs')
const path = require('path')
const compilerSfc = require('@vue/compiler-sfc')
const compilerDom = require('@vue/compiler-dom')
app.use(async (ctx) => {
const { url, query } = ctx.request
// 处理请求资源代码都写这
})
zaiz都h这z都he在
app.listen(3001, () => {
console.log('dyVite start!!')
})
请求首页index.html
if (url === '/') {
const p = path.join(__dirname, './index.html') // 绝对路径
// 首页
ctx.type = 'text/html'
ctx.body = fs.readFileSync(p, 'utf8')
}
看到上面这张图,就知道我们的宿主文件已经请求成功了。只是浏览器又给服务端发送的一个main.js文件的请求。这时,我们还需要判断处理一下main.js文件。
请求以.js结尾的文件
我们处理上述情况后,emmmm。。。发现main中还是存在好多其他资源请求。
基础js文件
main文件:
console.log(1)
处理main:
else if (url.endsWith('.js')) {
// 响应js请求
const p = path.join(__dirname, url)
ctx.type = 'text/javascript'
ctx.body = rewriteImport(fs.readFileSync(p, 'utf8')) // 处理依赖函数
}
对main中的依赖进行处理
你以为main里面就一个输出吗?太天真了。这样的还能处理吗?
main文件:
import { createApp, h } from 'vue'
createApp({ render: () => h('div', 'helllo dyVite!') }).mount('#app')
emmm。。。应该可以!
我们可以将main中导入的地址变成相对地址。
在裸模块路径添加上/@modules/
。再去识别/@modules/
的文件即(裸模块文件)。
// 把能读出来的文件地址变成相对地址
// 正则替换 重写导入 变成相对地址
// import { createApp } from 'vue' => import { createApp } from '/@modules/vue'
function rewriteImport(content) {
return content.replace(/ from ['|"](.*)['|"]/g, function (s0, s1) {
// s0匹配字符串,s1分组内容
// 是否是相对路径
if (s1.startsWith('./') || s1.startsWith('/') || s1.startsWith('../')) {
// 直接返回
return s0
} else {
return ` from '/@modules/${s1}'`
}
})
}
对于第三方依赖,vite内部是使用预打包请求自己服务器/node_modules/.vite/
下的内部资源。
我们可以简单化一点,将拿到的依赖名去客户端下的node_modules下拿相应的资源。
else if (url.startsWith('/@modules/')) {
// 裸模块的加载
const moduleName = url.replace('/@modules/', '')
const pre的地址
const module = require(prefix + '/package.json').module
const filePath = path.join(prefix, module) // 拿到文件加载的地址
// 读取相关依赖
const ret = fs.readFileSync(filePath, 'utf8')
ctx.type = 'text/javascript'
ctx.body = rewriteImport(ret) //依赖内部可能还存在依赖,需要递归
}
在main中进行render时,会报下图错误:
我们加载的文件都是服务端执行的库,内部可能会产生node环境的代码,需要判断一下环境变量。如果开发时,会输出一些警告信息,但是在前端是没有的。所以我们需要mock一下,告诉浏览器我们当前的环境。
给html加上process环境变量。
<script>
window.process = { env: { NODE_ENV: 'dev' } }
</script>
此时main文件算是加载出来了。
但是这远远打不到我们的目的啊!
我们需要的是可以编译vue文件的服务器啊!
处理.vue
文件
main.js文件:
import { createApp, h } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
在vue文件中,它是模块化加载的。
我们需要在处理vue文件的时候,对.vue
后面携带的参数做处理。
在此,我们简化只考虑template和sfc情况。
else if (url.indexOf('.vue') > -1) {
// 处理vue文件 App.vue?vue&type=style&index=0&lang.css
// 读取vue内容
const p = path.join(__dirname, url.split('?')[0])
// compilerSfc解析sfc 获得ast
const ret = compilerSfc.parse(fs.readFileSync(p, 'utf8'))
// App.vue?type=template
// 如果请求没有query.type 说明是sfc
if (!query.type) {
// 处理内部的script
const scriptContent = ret.descriptor.script.content
// 将默认导出配置对象转为常量
const script = scriptContent.replace(
'export default ',
'const __script = ',
)
ctx.type = 'text/javascript'
ctx.body = `
${rewriteImport(script)}
// template解析转换为单独请求一个资源
import {render as __render} from '${url}?type=template'
__script.render = __render
export default __script
`
} else if (query.type === 'template') {
const tpl = ret.descriptor.template.content
// 编译包含render模块
const render = compilerDom.compile(tpl, { mode: 'module' }).code
ctx.type = 'text/javascript'
ctx.body = rewriteImport(render)
}
}
处理图片路径
直接从客户端读取返回。
else if (url.endsWith('.png')) {
ctx.body = fs.readFileSync('src' + url)
}
作者:ClyingDeng
链接:https://juejin.cn/post/7033713960784248868
收起阅读 »
基于echarts 24种数据可视化展示,填充数据就可用,动手能力强的还可以DIY
前言
我们先跟随百度百科了解一下什么是“数据可视化 [1]”。
数据可视化,是关于数据视觉表现形式的科学技术研究。
其中,这种数据的视觉表现形式被定义为,一种以某种概要形式抽提出来的信息,包括相应信息单位的各种属性和变量。
它是一个处于不断演变之中的概念,其边界在不断地扩大。
主要指的是技术上较为高级的技术方法,而这些技术方法允许利用图形、图像处理、计算机视觉以及用户界面,通过表达、建模以及对立体、表面、属性以及动画的显示,对数据加以可视化解释。
与立体建模之类的特殊技术方法相比,数据可视化所涵盖的技术方法要广泛得多。
大家对展厅显示、客户导流、可视化汇报工作、对数据结果进行图形分析等这些业务场景都不陌生。
很多后端大多都只提供接口数据,并没有去构建前端显示页面,一来是不专业(各种特效、自适应等),二来是公司有前端,用不到后端来写。
但是暂时用到不代表我们不用,用的时候写不来怎么办?下面介绍24种数据可视化的demo,直接下载下来填充数据就可以跑起来,不满足的还可以DIY(演示地址+下载地址),yyds。
演示地址
注意:演示中如果有加载失败的,是环境问题,下载下来运行就好了。
演示地址:https://www.xiongze.net/viewdata/index.html [2]
现有的24种如下:
大数据展示系统、物流数据概况系统、物流订单系统、物流信息系统、办税渠道监控平台、车辆综合管控平台、
电子商务公共服务中心、各行业程序员中心、简洁大数据统计中心、警务平台大数据统计、农业监测大数据指挥舱、
农业监控数据平台、社会治理运行分析云图、水质监测大数据中心、水质情况实时监测预警系统、
物联网大数据统计平台、消防监控预警、销售数据报表中心、医疗大数据中心、营业数据统中心、
智慧旅游综合服务平台、智慧社区内网比对平台、智慧物流服务中心、政务大数据共享交换平台。
echarts图表库:Echarts提供了常规的折线图、柱状图、散点图、饼图、k线图,用于统计的盒形图,用于地理数据可视化的地图、热力图、线图,用于关系数据可视化的关系图、treemap、旭日图,多维数据可视化的平行坐标,还有用于 BI 的漏斗图,仪表盘,并且支持图与图之间的混搭。
下面demo里面的图标颜色、样式都可以在 echarts官网-文档-配置项手册里面进行查看, 是支持通过修改里面的配置项里面的属性来达到项目需求,我们可以去进行查看修改。
下载地址
Git下载链接:https://gitee.com/xiongze/viewdata.git [3]
百度网盘下载链接:https://pan.baidu.com/s/1jgwK6BvrS2rmbkrtW2MpYA提取码:xion
Demo示例(部分)
1、总览
2、物流信息展示
3、车辆综合管控平台
4、农业监测大数据指挥舱
5、水质情况实时监控预警中心
6、消防监控预警中心
7、医疗大数据中心
8、物联网平台数据中心
更多……
总共24种,这里就不一一展示了,大家下载下来就可以玩了。
可视化应用
数据可视化的开发和大部分项目开发一样,也是根据需求来根据数据维度或属性进行筛选,根据目的和用户群选用表现方式。同一份数据可以可视化成多种看起来截然不同的形式。
- 有的可视化目标是为了观测、跟踪数据,所以就要强调实时性、变化、运算能力,可能就会生成一份不停变化、可读性强的图表。
- 有的为了分析数据,所以要强调数据的呈现度、可能会生成一份可以检索、交互式的图表
- 有的为了发现数据之间的潜在关联,可能会生成分布式的多维的图表。
- 有的为了帮助普通用户或商业用户快速理解数据的含义或变化,会利用漂亮的颜色、动画创建生动、明了,具有吸引力的图表。
- 还有的被用于教育、宣传或政治,被制作成海报、课件,出现在街头、广告手持、杂志和集会上。这类可视化拥有强大的说服力,使用强烈的对比、置换等手段,可以创造出极具冲击力自指人心的图像。在国外许多媒体会根据新闻主题或数据,雇用设计师来创建可视化图表对新闻主题进行辅助。
数据可视化的应用价值,其多样性和表现力吸引了许多从业者,而其创作过程中的每一环节都有强大的专业背景支持。无论是动态还是静态的可视化图形,都为我们搭建了新的桥梁,让我们能洞察世界的究竟、发现形形色色的关系,感受每时每刻围绕在我们身边的信息变化,还能让我们理解其他形式下不易发掘的事物。
参考文献
[1]百度百科:数据可视化
[2]演示地址:https://www.xiongze.net/viewdata/index.html
[3]下载链接:https://gitee.com/xiongze/viewdata.git
[4]数据可视化概念
收起阅读 »
作者:熊泽-学习中的苦与乐
来源:https://www.cnblogs.com/xiongze520/p/15588852.html
CommonJS和ES6 Module究竟是什么
对于前端模块化总是稀里糊涂,今天深入学习一下前端模块化,彻底弄懂CommonJs和ES6 Module,希望本文可以给你带来帮助。
CommonJS
模块
CommonJS中规定每个文件是一个模块。将一个JS文件通过script标签插入页面与封装成CommonJS模块最大的不同在于,前者的顶层作用域是全局作用域,在进行变量及函数声明时会污染全局环境;而后者形成一个属于模块自身的作用域,所有的变量及函数只能自己访问,对外不可见。
导出
导出是一个模块向外暴露自身的唯一方式。在commonJS中,通过modul
e.exports可以导出模块中的内容。下面的代码导出了一个对象,包含name和add属性。
module.exports = {
name: 'calculater',
add: function(a, b){
return a+b;
}
}
为了书写方便,CommonJS也支持直接使用exports。
exports.name = 'calculater';
exports.add = function(a, b){
return a+b;
}
exports可以理解为
var module = {
exports:{}
};
var exports = module.exports;
注意错误的用法:
- 不要给exports直接赋值,否则导出会失效。如下代码,对exports赋值,使其指向新的对象。module.exports却仍然是原来的空对象,因此name属性并不会被导出。
exports = {
name: 'calculater'
}
- 不恰当的把module.exports和exports混用。如下代码,先通过exports导出add属性,然后将module.exports重新赋值为另一个对象,将导致add属性丢失,最后导出只有name。
exports.add = function(a,b){
return a+b;
}
module.exports = {
name: 'calculater'
}
导入
在CommonJs中,使用require进行模块导入。
const calculator = require('./calculator.js')
let sum = calculator.add(2,3)
注意:
- require的模块是第一次被加载,这时会首先执行该模块,然后导出内容
- require的模块曾被加载过,这时该模块的代码不会再次执行,而是直接导出上次执行后得到的结果。
- 对于不需要获取导出内容的模块,直接使用require即可。
- require函数可以接收表达式,借助这个特性可以动态地指定模块加载路径。
const moduleName = ['a.js', 'b.js'];
moduleNames.forEach(name => {
require('./'+name)
})
ES6 Module
模块
ES6 Module是ES语法的一部分,它也是将每个文件作为一个模块,每个模块拥有自身的作用域。
导出
在ES6 Module中使用export命令来导出模块。export有两种形式:
- 命名导出
- 默认导出
一个模块可以有多个命名导出,它有两种不同的写法:
//写法1,将变量的声明和导出写在一行
export const name = 'calculator'
export const add = function(a, b){return a+b}
//写法2,先进行变量的声明,然后在用同一个export语句导出。
const name = 'calculator'
const add = function(a, b){return a+b}
export {name, add}
与命名导出不同,模块的默认导出只能有一个。
export default {
name: 'calculator',
add: function(a, b){
return a+b
}
}
导入
ES6 Module中使用import语法导入模块。
加载带有命名导出的模块
有两种方式
- import后面要跟一对大括号,将导入的变量名包裹起来。并且这些变量名要与导出的变量名完全一致。
//calculator.js
const name = 'calculator'
const add = function(a, b){return a+b}
export {name, add}
//index.js
import {name, add} from './calculator.js'
add(2,3)
- 采用整体导入的方式, 使用import * as myModule可以把所有导入的变量作为属性值添加到myModule中,从而减少对当前作用域的影响。
import * as calculator from './calculator.js'
console.log(calculator.add(2,3))
console.log(calculator.name)
加载默认导出的模块
import后面直接跟变量名,并且这个名字可以自由指定
//calculator.js
export default {
name: 'calculator',
add: function(a, b){
return a+b
}
}
//index.js
import calculator from './calculator.js'
calculator.add(2,3)
两种导入方式混合起来
import React, {Component} from 'react'
这里的React对应的是该模块的默认导出,Component则是其命名导出中的一个变量。
CommonJS和ES6 Module的区别
动态和静态
- CommonJS是动态的模块结构,模块依赖关系的建立发生在代码的运行阶段
- ES Module是静态的模块结构,在编译阶段就可以分析模块的依赖关系。
相比于CommonJS,ES6 Module有如下优势:
- 死代码监测和排除
- 模块变量和类型检查
- 编译器优化
值拷贝和动态映射
在导入一个模块时,对于CommonJs来说,获取的是一份导出值的拷贝。而在ES6 Module中则是值的动态映射,并且这个映射是只读的。
总结
作者:邓惠子本尊
收起阅读 »
如何从性能角度选择数组的遍历方式
前言
本文讲述了JS常用的几种数组遍历方式以及性能分析对比。
如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,文章公众号首发,关注 前端南玖 第一时间获取最新的文章~
数组的方法
JavaScript发展到现在已经提供了许多数组的方法,下面这张图涵盖了数组大部分的方法,这篇文章主要说一说数组的遍历方法,以及各自的性能,方法这么多,如何挑选性能最佳的方法对我们的开发有非常大的帮助。
数组遍历的方法
for
标准的for循环语句,也是最传统的循环语句
var arr = [1,2,3,4,5]
for(var i=0;i<arr.length;i++){
console.log(arr[i])
}
最简单的一种遍历方式,也是使用频率最高的,性能较好,但还能优化
优化版for循环语句
var arr = [1,2,3,4,5]
for(var i=0,len=arr.length;i<len;i++){
console.log(arr[i])
}
使用临时变量,将长度缓存起来,避免重复获取数组长度,尤其是当数组长度较大时优化效果才会更加明显。
这种方法基本上是所有循环遍历方法中性能最高的一种
forEach
普通forEach
对数组中的每一元素运行给定的函数,没有返回值,常用来遍历元素
var arr5 = [10,20,30]
var result5 = arr5.forEach((item,index,arr)=>{
console.log(item)
})
console.log(result5)
/*
10
20
30
undefined 该方法没有返回值
*/
数组自带的foreach循环,使用频率较高,实际上性能比普通for循环弱
原型forEach
由于foreach是Array型自带的,对于一些非这种类型的,无法直接使用(如NodeList),所以才有了这个变种,使用这个变种可以让类似的数组拥有foreach功能。
const nodes = document.querySelectorAll('div')
Array.prototype.forEach.call(nodes,(item,index,arr)=>{
console.log(item)
})
实际性能要比普通foreach弱
for...in
任意顺序遍历一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性。
一般常用来遍历对象,包括非整数类型的名称和继承的那些原型链上面的属性也能被遍历。像 Array和 Object使用内置构造函数所创建的对象都会继承自Object.prototype和String.prototype的不可枚举属性就不能遍历了.
var arr = [1,2,3,4,5]
for(var i in arr){
console.log(i,arr[i])
} //这里的i是对象属性,也就是数组的下标
/**
0 1
1 2
2 3
3 4
4 5 **/
大部分人都喜欢用这个方法,但它的性能却不怎么好
for...of(不能遍历对象)
在可迭代对象(具有 iterator 接口)(Array,Map,Set,String,arguments)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句,不能遍历对象
let arr=["前端","南玖","ssss"];
for (let item of arr){
console.log(item)
}
//前端 南玖 ssss
//遍历对象
let person={name:"南玖",age:18,city:"上海"}
for (let item of person){
console.log(item)
}
// 我们发现它是不可以的 我们可以搭配Object.keys使用
for(let item of Object.keys(person)){
console.log(person[item])
}
// 南玖 18 上海
这种方式是es6里面用到的,性能要好于forin,但仍然比不上普通for循环
map
map: 只能遍历数组,不能中断,返回值是修改后的数组。
let arr=[1,2,3];
const res = arr.map(item=>{
return item+1
})
console.log(res) //[2,3,4]
console.log(arr) // [1,2,3]
every
对数组中的每一运行给定的函数,如果该函数对每一项都返回true,则该函数返回true
var arr = [10,30,25,64,18,3,9]
var result = arr.every((item,index,arr)=>{
return item>3
})
console.log(result) //false
some
对数组中的每一运行给定的函数,如果该函数有一项返回true,就返回true,所有项返回false才返回false
var arr2 = [10,20,32,45,36,94,75]
var result2 = arr2.some((item,index,arr)=>{
return item<10
})
console.log(result2) //false
reduce
reduce()
方法对数组中的每个元素执行一个由你提供的reducer函数(升序执行),将其结果汇总为单个返回值
const array = [1,2,3,4]
const reducer = (accumulator, currentValue) => accumulator + currentValue;
// 1 + 2 + 3 + 4
console.log(array1.reduce(reducer));
filter
对数组中的每一运行给定的函数,会返回满足该函数的项组成的数组
// filter 返回满足要求的数组项组成的新数组
var arr3 = [3,6,7,12,20,64,35]
var result3 = arr3.filter((item,index,arr)=>{
return item > 3
})
console.log(result3) //[6,7,12,20,64,35]
性能测试
工具测试
使用工具测试性能分析结果如下图所示
手动测试
我们也可以自己用代码测试:
//测试函数
function clecTime(fn,fnName){
const start = new Date().getTime()
if(fn) fn()
const end = new Date().getTime()
console.log(`${fnName}执行耗时:${end-start}ms`)
}
function forfn(){
let a = []
for(var i=0;i<arr.length;i++){
// console.log(i)
a.push(arr[i])
}
}
clecTime(forfn, 'for') //for执行耗时:106ms
function forlenfn(){
let a = []
for(var i=0,len=arr.length;i<len;i++){
a.push(arr[i])
}
}
clecTime(forlenfn, 'for len') //for len执行耗时:95ms
function forEachfn(){
let a = []
arr.forEach(item=>{
a.push[item]
})
}
clecTime(forEachfn, 'forEach') //forEach执行耗时:201ms
function forinfn(){
let a = []
for(var i in arr){
a.push(arr[i])
}
}
clecTime(forinfn, 'forin') //forin执行耗时:2584ms (离谱)
function foroffn(){
let a = []
for(var i of arr){
a.push(i)
}
}
clecTime(foroffn, 'forof') //forof执行耗时:221ms
// ...其余可自行测试
结果分析
经过工具与手动测试发现,结果基本一致,数组遍历各个方法的速度:传统的for循环最快,for-in最慢
for-len
>
for>
for-of>
forEach>
map>
for-in
javascript原生遍历方法的建议用法:
- 用
for
循环遍历数组 - 用
for...in
遍历对象 - 用
for...of
遍历类数组对象(ES6) - 用
Object.keys()
获取对象属性名的集合
为何for… in会慢?
因为for … in
语法是第一个能够迭代对象键的JavaScript语句,循环对象键({})与在数组([])上进行循环不同,引擎会执行一些额外的工作来跟踪已经迭代的属性。因此不建议使用for...in
来遍历数组
作者:南玖
链接:https://juejin.cn/post/7033578966887694373
收起阅读 »
async/await 优雅永不过时
引言
async/await
是非常棒的语法糖,可以说他是解决异步问题的最终解决方案。从字面意思来理解。async 是异步
的意思,而 await 是等待
,所以理解 async用于申明一个function是异步的,而 await 用于等待一个异步方法执行完成。
async作用
async
声明function是一个异步函数,返回一个promise
对象,可以使用 then 方法添加回调函数。async
函数内部return
语句返回的值,会成为then
方法回调函数的参数。
async function test() {
return 'test';
}
console.log(test); // [AsyncFunction: test] async函数是[`AsyncFunction`]构造函数的实例
console.log(test()); // Promise { 'test' }
// async返回的是一个promise对象
test().then(res=>{
console.log(res); // test
})
// 如果async函数没有返回值 async函数返回一个undefined的promise对象
async function fn() {
console.log('没有返回');
}
console.log(fn()); // Promise { undefined }
// 可以看到async函数返回值和Promise.resolve()一样,将返回值包装成promise对象,如果没有返回值就返回undefined的promise对象
await
await 操作符只能在异步函数 async function 内部使用。如果一个 Promise 被传递给一个 await 操作符,await 将等待 Promise 正常处理完成并返回其处理结果,也就是说它会阻塞后面的代码,等待 Promise 对象结果。如果等待的不是 Promise 对象,则返回该值本身。
async function test() {
return new Promise((resolve)=>{
setTimeout(() => {
resolve('test 1000');
}, 1000);
})
}
function fn() {
return 'fn';
}
async function next() {
let res0 = await fn(),
res1 = await test(),
res2 = await fn();
console.log(res0);
console.log(res1);
console.log(res2);
}
next(); // 1s 后才打印出结果 为什么呢 就是因为 res1在等待promise的结果 阻塞了后面代码。
错误处理
如果
await
后面的异步操作出错,那么等同于async
函数返回的 Promise 对象被reject
。
async function test() {
await Promise.reject('错误了')
};
test().then(res=>{
console.log('success',res);
},err=>{
console.log('err ',err);
})
// err 错误了
防止出错的方法,也是将其放在
try...catch
代码块之中。
async function test() {
try {
await new Promise(function (resolve, reject) {
throw new Error('错误了');
});
} catch(e) {
console.log('err', e)
}
return await('成功了');
}
多个
await
命令后面的异步操作,如果不存在继发关系(即互不依赖),最好让它们同时触发。
let foo = await getFoo();
let bar = await getBar();
// 上面这样写法 getFoo完成以后,才会执行getBar
// 同时触发写法 ↓
// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
async/await优点
async/await的优势在于处理由多个Promise组成的 then 链,在之前的Promise文章中提过用then处理回调地狱的问题,async/await相当于对promise的进一步优化。
假设一个业务,分多个步骤,且每个步骤都是异步的,而且依赖上个步骤的执行结果。
// 假设表单提交前要通过俩个校验接口
async function check(ms) { // 模仿异步
return new Promise((resolve)=>{
setTimeout(() => {
resolve(`check ${ms}`);
}, ms);
})
}
function check1() {
console.log('check1');
return check(1000);
}
function check2() {
console.log('check2');
return check(2000);
}
// -------------promise------------
function submit() {
console.log('submit');
// 经过俩个校验 多级关联 promise传值嵌套较深
check1().then(res1=>{
check2(res1).then(res2=>{
/*
* 提交请求
*/
})
})
}
submit();
// -------------async/await-----------
async function asyncAwaitSubmit() {
let res1 = await check1(),
res2 = await check2(res1);
console.log(res1, res2);
/*
* 提交请求
*/
}
原理
async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。
async function fn(args) {
// ...
}
// 等同于
function fn(args) {
return spawn(function* () {
// ...
});
}
/*
* Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。
* 异步操作需要暂停的地方,都用 yield 语句注明
* 调用 Generator 函数,返回的是指针对象(这是它和普通函数的不同之处),。调用指针对象的 next 方法,会移动内部指针。
* next 方法的作用是分阶段执行 Generator 函数。每次调用 next 方法,会返回一个对象,表示当前阶段的信息( value 属性和 done 属性)。value 属性是 yield 语句后面表达式的值,表示当前阶段的值;done 属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段。
*/
// 了解generator的用法
function* Generator() {
yield '1';
yield Promise.resolve(2);
return 'ending';
}
var gen = Generator(); // 返回指针对象 Object [Generator] {}
let res1 = gen.next();
console.log(res1); // 返回当前阶段的值 { value: '1', done: false }
let res2 = gen.next();
console.log(res2); // 返回当前阶段的值 { value: Promise { 2 }, done: false }
res2.value.then(res=>{
console.log(res); // 2
})
let res3 = gen.next();
console.log(res3); // { value: 'ending', done: true }
let res4 = gen.next();
console.log(res4); // { value: undefined, done: true }
Generator实现async函数
// 接受一个Generator函数作为参数
function spawn(genF) {
// 返回一个函数
return function() {
// 生成指针对象
const gen = genF.apply(this, arguments);
// 返回一个promise
return new Promise((resolve, reject) => {
// key有next和throw两种取值,分别对应了gen的next和throw方法
// arg参数则是用来把promise resolve出来的值交给下一个yield
function step(key, arg) {
let result;
// 监控到错误 就把promise给reject掉 外部通过.catch可以获取到错误
try {
result = gen[key](arg)
} catch (error) {
return reject(error)
}
// gen.next() 返回 { value, done } 的结构
const { value, done } = result;
if (done) {
// 如果已经完成了 就直接resolve这个promise
return resolve(value)
} else {
// 除了最后结束的时候外,每次调用gen.next()
return Promise.resolve(
// 这个value对应的是yield后面的promise
value
).then((val)=>step("next", val),(err) =>step("throw", err))
}
}
step("next")
})
}
}
测试
function fn(nums) {
return new Promise(resolve => {
setTimeout(() => {
resolve(nums)
}, 1000)
})
}
// async 函数
async function testAsync() {
let res1 = await fn(1);
console.log(res1); // 1
let res2 = await fn(2);
console.log(res2); // 2
return res2;
}
let _res = testAsync();
console.log('testAsync-res',_res); // Promise
_res.then(v=>console.log('testAsync-res',v)) // 2
// Generator函数
function* gen() {
let res1 = yield fn(3);
console.log(res1); // 3
let res2 = yield fn(4);
console.log(res2); // 4
// let res3 = yield Promise.reject(5);
// console.log(res3);
return res2;
}
let _res2 = spawn(gen)();
console.log('gen-res',_res2); // Promise
_res2
.then(v=>console.log('gen-res',v)) // 4
.catch(err=>{console.log(err)}) // res3 执行会抛出异常
总结
async/await语法糖可以让异步代码变得更清晰,可读性更高,所以快快卷起来吧。Generator有兴趣的可以了解一下。
作者:小撕夜
链接:https://juejin.cn/post/7033647059378896903
收起阅读 »
当老婆又让我下载一个腾讯视频时
我们结婚了!
是的,这次不是女朋友啦,是老婆了!
时隔将近一个月,老婆又让我给她下载腾讯视频,如果按照上次探索的内容来下载的话,倒是可以一步步下载,合并,不过很麻烦,程序员不都是为了解决麻烦的吗,这么麻烦的步骤,有没有简单点呢。有!当然有,有很多简单的工具,上一期很多朋友给我推荐了各种工具,这里我没有一一查看,我可以列举出来,有需要的同学可以尝试看看,不想尝试的也可以看看我下面为了偷懒准备的方法。
心路历程
最初,我是想着把我之前的步骤,用无头浏览器加载一遍,然后用代码去下载ts片段,然后在机器上用ffmpeg进行合并,但是仿佛还是有些许麻烦,然后我就去npm搜了一下关键词:m3u8tomp4
m3u8-to-mp4
于是我点击了第一个包:m3u8-to-mp4
纳尼?这个包就一个版本,用了3年,而且周下载量还不少
于是我想着这个包要么就是很牛逼,一次性解决了m3u8转mp4的问题,一劳永逸,所以3年没更新过了,要么就是作者忘记了自己还有这个包
于是我就用了这个3年没人维护没人更新的包。
用法也很简单,就copy example 就好了。代码如下:
var m3u8ToMp4 = require("m3u8-to-mp4");
var converter = new m3u8ToMp4();
(async function() {
var url = "https://apd-666945ea97106754c57813479384d30c.v.smtcdns.com/omts.tc.qq.com/AofRtrergNwkAhpHs4RrxH2_9DWLWSG8xjDMZDQoFGyY/uwMROfz2r55kIaQXGdGnC2deOm68BrdPrRewQlOzrMAbixNO/svp_50001/cKAgRbCb6Re4BpHkI-IlK_KN1VJ8gQVK2sZtkHEY3vQUIlxVz7AtWmVJRifZrrPfozBS0va-SSJFhQhOFSKVNmqVi165fCQJoPl8V5QZBcGZBDpSIfrpCImJKryoZOdR5C0oGYkzIW77I4his7UkPY9Iwmf1QWjaHwNV2hpKv3aD9ysL_-YByA/szg_9276_50001_0bc3uuaa2aaafmaff4e3ijqvdjodbwsqadka.f304110.ts.m3u8?ver=4"
await converter
.setInputFile(url)
.setOutputFile("dummy.mp4")
.start();
console.log("File converted");
})();
视频地址是 v.qq.com/x/page/v331…
然后视频就转换成功了,哇哦!
so easy ! so beautiful!
原理
带着好奇,我想看下这个包是如何进行转换的
于是我点进去m3u8-to-mp4这个包文件
包文件内容如下
只有一个文件?
然后我打开了index.js ,只有64行😂
全部代码如下
/**
* @description M3U8 to MP4 Converter
* @author Furkan Inanc
* @version 1.0.0
*/
let ffmpeg = require("fluent-ffmpeg");
/**
* A class to convert M3U8 to MP4
* @class
*/
class m3u8ToMp4Converter {
/**
* Sets the input file
* @param {String} filename M3U8 file path. You can use remote URL
* @returns {Function}
*/
setInputFile(filename) {
if (!filename) throw new Error("You must specify the M3U8 file address");
this.M3U8_FILE = filename;
return this;
}
/**
* Sets the output file
* @param {String} filename Output file path. Has to be local :)
* @returns {Function}
*/
setOutputFile(filename) {
if (!filename) throw new Error("You must specify the file path and name");
this.OUTPUT_FILE = filename;
return this;
}
/**
* Starts the process
*/
start() {
return new Promise((resolve, reject) => {
if (!this.M3U8_FILE || !this.OUTPUT_FILE) {
reject(new Error("You must specify the input and the output files"));
return;
}
ffmpeg(this.M3U8_FILE)
.on("error", error => {
reject(new Error(error));
})
.on("end", () => {
resolve();
})
.outputOptions("-c copy")
.outputOptions("-bsf:a aac_adtstoasc")
.output(this.OUTPUT_FILE)
.run();
});
}
}
module.exports = m3u8ToMp4Converter;
大致看了下这个包做的内容,就是检测并设置了输入链接,和输出文件名,然后调用了fluent-ffmpeg这个库
???
站在巨人的肩膀上吗,自己就包了一层😂
接着看fluent-ffmpeg这个包,是如何实现转换的
然后我们在这个包文件夹下面搜索.run方法,用来定位到具体执行的地方
凭借多年的cv经验,感觉应该是processor.js这个文件里的,然后我们打开这个文件,定位到该方法处
往下看代码,我注意到了这段代码
因为都是基于ffmpeg这个大爹来做的工具,所以最底层也都是去调用ffmpeg的command
这几个if判断都是对结果进行捕获异常,那么我们在这个核心代码的地方打个端点看下
貌似是调用了几个命令行参数
于是我就有了一个大胆的想法!
是的,我手动在终端将这个命令拼接起来,用我的本地命令去跑应该也没问题的吧,于是我尝试了一下
没想到还成功了,其实成功是必然的,因为都是借助来ffmpeg这个包,只不过我是手动去操作,框架是代码去拼接这个命令而已
剩余的时间里,我看了看fluent-ffmpeg的其他代码,它做的东西比较多,比如去本查找ffmpeg的绝对路径啊,对ffmpeg的结果进行捕获异常信息等...
作者:小松同学哦
链接:https://juejin.cn/post/7033652317958176799
收起阅读 »
【前端工程化】- 结合代码实践,全面学习前端工程化
前言
前端工程化,简而言之就是软件工程 前端,以自动化的形式呈现。就个人理解而言:前端工程化,从开发阶段到代码发布生产环境,包含了以下几个内容:
- 开发
- 构建
- 测试
- 部署
- 性能
- 规范
下面我们根据上述几个内容,选择有代表性的几个方面进行深入学习前端工程化。
脚手架
脚手架是什么?(What)
现在流行的前端脚手架基本上都是基于NodeJs
编写,比如我们常用的Vue-CLI
,比较火的create-react-app
,还有Dva-CLI
等。
脚手架存在的意义?(Why)
随着前端工程化的概念越来越深入人心,脚手架的出现就是为减少重复性工作而引入的命令行工具,摆脱ctrl c
, ctrl v
,此话怎讲? 现在新建一个前端项目,已经不是在html
头部引入css
,尾部引入js
那么简单的事了,css
都是采用Sass
或则Less
编写,在js
中引入,然后动态构建注入到html
中;除了学习基本的js
,css
语法和热门框架,还需要学习构建工具webpack
,babel
这些怎么配置,怎么起前端服务,怎么热更新;为了在编写过程中让编辑器帮我们查错以及更加规范,我们还需要引入ESlint
;甚至,有些项目还需要引入单元测试(Jest
)。对于一个更入门的人来说,这无疑会让人望而却步。而前端脚手架的出现,就让事情简单化,一键命令,新建一个工程,再执行两个npm
命令,跑起一个项目。在入门时,无需关注配置什么的,只需要开心的写代码就好。
如何实现一个新建项目脚手架(基于koa)?(How)
先梳理下实现思路
我们实现脚手架的核心思想就是自动化思维,将重复性的ctrl c
, ctrl v
创建项目,用程序来解决。解决步骤如下:
- 创建文件夹(项目名)
- 创建 index.js
- 创建 package.json
- 安装依赖
1. 创建文件夹
创建文件夹前,需要先删除清空:
// package.json
{
...
"scripts": {
"test": "rm -rf ./haha && node --experimental-modules index.js"
}
...
}
创建文件夹:我们通过引入 nodejs
的 fs
模块,使用 mkdirSync
API来创建文件夹。
// index.js
import fs from 'fs';
function getRootPath() {
return "./haha";
}
// 生成文件夹
fs.mkdirSync(getRootPath());
2. 创建 index.js
创建 index.js:使用 nodejs
的fs
模块的 writeFileSync
API 创建 index.js 文件:
// index.js
fs.writeFileSync(getRootPath() + "/index.js", createIndexTemplate(inputConfig));
接着我们来看看,动态模板如何生成?我们最理想的方式是通过配置来动态生成文件模板,那么具体来看看 createIndexTemplate
实现的逻辑吧。
// index.js
import fs from 'fs';
import { createIndexTemplate } from "./indexTemplate.js";
// input
// process
// output
const inputConfig = {
middleWare: {
router: true,
static: true
}
}
function getRootPath() {
return "./haha";
}
// 生成文件夹
fs.mkdirSync(getRootPath());
// 生成 index.js 文件
fs.writeFileSync(getRootPath() + "/index.js", createIndexTemplate(inputConfig));
// indexTemplate.js
import ejs from "ejs";
import fs from "fs";
import prettier from "prettier";// 格式化代码
// 问题驱动
// 模板
// 开发思想 - 小步骤的开发思想
// 动态生成代码模板
export function createIndexTemplate(config) {
// 读取模板
const template = fs.readFileSync("./template/index.ejs", "utf-8");
// ejs渲染
const code = ejs.render(template, {
router: config.middleware.router,
static: config.middleware.static,
port: config.port,
});
// 返回模板
return prettier.format(code, {
parser: "babel",
});
}
// template/index.ejs
const Koa = require("koa");
<% if (router) { %>
const Router = require("koa-router");
<% } %>
<% if (static) { %>
const serve = require("koa-static");
<% } %>
const app = new Koa();
<% if (router) { %>
const router = new Router();
router.get("/", (ctx) => {
ctx.body = "hello koa-setup-heihei";
});
app.use(router.routes());
<% } %>
<% if (static) { %>
app.use(serve(__dirname + "/static"));
<% } %>
app.listen(<%= port %>, () => {
console.log("open server localhost:<%= port %>");
});
3. 创建 package.json
创建 package.json 文件,实质是和创建 index.js 类似,都是采用动态生成模板的思路来实现,我们来看下核心方法 createPackageJsonTemplate
的实现代码:
// packageJsonTemplate.js
function createPackageJsonTemplate(config) {
const template = fs.readFileSync("./template/package.ejs", "utf-8");
const code = ejs.render(template, {
packageName: config.packageName,
router: config.middleware.router,
static: config.middleware.static,
});
return prettier.format(code, {
parser: "json",
});
}
// template/package.ejs
{
"name": "<%= packageName %>",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"koa": "^2.13.1"
<% if (router) { %>
,"koa-router": "^10.1.1"
<% } %>
<% if (static) { %>
,"koa-static": "^5.0.0"
}
<% } %>
}
4. 安装依赖
要自动安装依赖,我们可以使用 nodejs
的 execa
库执行 yarn
安装命令:
execa("yarn", {
cwd: getRootPath(),
stdio: [2, 2, 2],
});
至此,我们已经用 nodejs
实现了新建项目的脚手架了。最后我们可以重新梳理下可优化点将其升级完善。比如将程序配置升级成 GUI
用户配置(用户通过手动选择或是输入来传入配置参数,例如项目名)。
编译构建
编译构建是什么?
构建,或者叫作编译,是前端工程化体系中功能最繁琐、最复杂的模块,承担着从源代码转化为宿主浏览器可执行的代码,其核心是资源的管理。前端的产出资源包括JS、CSS、HTML等,分别对应的源代码则是:
- 领先于浏览器实现的ECMAScript规范编写的JS代码(ES6/7/8...)。
- LESS/SASS预编译语法编写的CSS代码。
- Jade/EJS/Mustache等模板语法编写的HTML代码。
以上源代码是无法在浏览器环境下运行的,构建工作的核心便是将其转化为宿主可执行代码,分别对应:
- ECMAScript规范的转译。
- CSS预编译语法转译。
- HTML模板渲染。
那么下面我们就一起学习下如今3大主流构建工具:Webpack、Rollup、Vite。
Webpack
Webpack原理
想要真正用好 Webpack
编译构建工具,我们需要先来了解下它的工作原理。Webpack
编译项目的工作机制是,递归找出所有依赖模块,转换源码为浏览器可执行代码,并构建输出bundle。具体工作流程步骤如下:
- 初始化参数:取配置文件和shell脚本参数并合并
- 开始编译:用上一步得到的参数初始化
compiler
对象,执行run
方法开始编译 - 确定入口:根据配置中的
entry
,确定入口文件 - 编译模块:从入口文件出发,递归遍历找出所有依赖模块的文件
- 完成模块编译:使用
loader
转译所有模块,得到转译后的最终内容和依赖关系 - 输出资源:根据入口和模块依赖关系,组装成一个个
chunk
,加到输出列表 - 输出完成:根据配置中的
output
,确定输出路径和文件名,把文件内容写入输出目录(默认是dist
)
Webpack实践
1. 基础配置
【entry】
入口配置,webpack 编译构建时能找到编译的入口文件,进而构建内部依赖图。
【output】
输出配置,告诉 webpack 在哪里输出它所创建的 bundle,以及如何命名这些文件。
【loader】
模块转换器,loader 可以处理浏览器无法直接运行的文件模块,转换为有效模块。比如:css-loader和style-loader处理样式;url-loader和file-loader处理图片。
【plugin】
插件,解决 loader 无法实现的问题,在 webpack 整个构建生命周期都可以扩展插件。比如:打包优化,资源管理,注入环境变量等。
下面是 webpack 基本配置的简单示例:
const path = require("path");
module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist"),
},
devServer: {
static: "./dist",
},
module: {
rules: [
{
// 匹配什么样子的文件
test: /\.css$/i,
// 使用loader , 从后到前执行
use: ["style-loader", "css-loader"],
}
],
},
};
参考webpack官网:webpack.docschina.org/concepts/
(注意:使用不同版本的 webpack 切换对应版本的文档哦)
2. 性能优化
- 编译速度优化
【检测编译速度】
寻找检测编译速度的工具,比如 speed-measure-webpack-plugin插件 ,用该插件分析每个loader和plugin执行耗时具体情况。
【优化编译速度该怎么做呢?】
- 减少搜索依赖的时间
- 配置 loader 匹配规则 test/include/exclue,缩小搜索范围,即可减少搜索时间
- 减少解析转换的时间
- noParse配置,精准过滤不用解析的模块
- loader性能消耗大的,开启多进程
- 减少构建输出的时间
- 压缩代码,开启多进程
- 合理使用缓存策略
- babel-loader开启缓存
- 中间模块启用缓存,比如使用 hard-source-webpack-plugin
具体优化措施可参考:webpack性能优化的一段经历|项目复盘
- 体积优化
【检测包体积大小】
寻找检测构建后包体积大小的工具,比如 webpack-bundle-analyzer插件 ,用该插件分析打包后生成Bundle的每个模块体积大小。
【优化体积该怎么做呢?】
- bundle去除第三方依赖
- 擦除无用代码 Tree Shaking
具体优化措施参考:webpack性能优化的一段经历|项目复盘
Rollup
Rollup概述
Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码,例如 library 或应用程序。并且可以对代码模块使用新的标准化格式,比如CommonJS
和 es module
。
Rollup原理
我们先来了解下 Rollup
原理,其主要工作机制是:
- 确定入口文件
- 使用
Acorn
读取解析文件,获取抽象语法树 AST - 分析代码
- 生成代码,输出
Rollup
相对 Webpack
而言,打包出来的包会更加轻量化,更适用于类库打包,因为内置了 Tree Shaking 机制,在分析代码阶段就知晓哪些文件引入并未调用,打包时就会自动擦除未使用的代码。
Acorn 是一个 JavaScript 语法解析器,它将 JavaScript 字符串解析成语法抽象树 AST 如果想了解 AST 语法树可以点下这个网址astexplorer.net/
Rollup实践
【input】
入口文件路径
【output】
输出文件、输出格式(amd/es6/iife/umd/cjs)、sourcemap启用等。
【plugin】
各种插件使用的配置
【external】
提取外部依赖
【global】
配置全局变量
下面是 Rollup 基础配置的简单示例:
import commonjs from "@rollup/plugin-commonjs";
import resolve from "@rollup/plugin-node-resolve";
// 解析json
import json from '@rollup/plugin-json'
// 压缩代码
import { terser } from 'rollup-plugin-terser';
export default {
input: "src/main.js",
output: [{
file: "dist/esmbundle.js",
format: "esm",
plugins: [terser()]
},{
file: "dist/cjsbundle.js",
format: "cjs",
}],
// commonjs 需要放到 transform 插件之前,
// 但是又个例外, 是需要放到 babel 之后的
plugins: [json(), resolve(), commonjs()],
external: ["vue"]
};
Vite
Vite概述
Vite,相比 Webpack、Rollup 等工具,极大地改善了前端开发者的开发体验,编译速度极快。
Vite原理
为什么 Vite 开发编译速度极快?我们就先来探究下它的原理吧。
由上图可见,Vite 原理是利用现代主流浏览器支持原生的 ESM 规范,配合 server 做拦截,把代码编译成浏览器支持的。
Vite实践体验
我们可以搭建一个Hello World版的Vite项目来感受下飞快的开发体验:
注意:Vite 需要 Node.js 版本 >= 12.0.0。
使用 NPM:
$ npm init vite@latest
使用 Yarn:
$ yarn create vite
上图是Vite项目的编译时间,363ms,开发秒级编译的体验,真的是棒棒哒!
3种构建工具综合对比
Webpack | Rollup | Vite | |
---|---|---|---|
编译速度 | 一般 | 较快 | 最快 |
HMR热更新 | 支持 | 需要额外引入插件 | 支持 |
Tree Shaking | 需要额外配置 | 支持 | 支持 |
适用范围 | 项目打包 | 类库打包 | 不考虑兼容性的项目 |
测试
当我们前端项目越来越庞大时,开发迭代维护成本就会越来越高,数十个模块相互调用错综复杂,为了提高代码质量和可维护性,就需要写测试了。下面就给大家具体介绍下前端工程经常做的3类测试。
单元测试
单元测试,是对最小可测试单元(一般为单个函数、类或组件)进行检查和验证。
做单元测试的框架有很多,比如 Mocha、断言库Chai、Sinon、Jest等。我们可以先选择 jest 来学习,因为它集成了 Mocha
,chai
,jsdom
,sinon
等功能。接下来,我们一起看看 jest
怎么写单元测试吧?
- 根据正确性写测试,即正确的输入应该有正常的结果。
- 根据错误性写测试,即错误的输入应该是错误的结果。
以验证求和函数为例:
// add函数
module.exports = (a,b) => {
return a+b;
}
// 正确性测试验证
const add = require('./add.js');
test('should 1+1 = 2', ()=> {
// 准备测试数据 -> given
const a = 1;
const b = 1;
// 触发测试动作 -> when
const r = add(a,b);
// 验证 -> then
expect(r).toBe(2);
})
// 错误性测试验证
test('should 1+1 = 2', ()=> {
// 准备测试数据 -> given
const a = 1;
const b = 2;
// 触发测试动作 -> when
const r = add(a,b)
// 验证 -> then
expect(r).toBe(2);
})
组件测试
组件测试,主要是针对某个组件功能进行测试,这就相对困难些,因为很多组件涉及了DOM
操作。组件测试,我们可以借助组件测试框架来做,比如使用 Cypress(它可以做组件测试,也可以做 e2e 测试)。我们就先来看看组件测试怎么做?
以 vue3 组件测试为例:
- 我们先建好
vue3
+vite
项目,编写测试组件 - 再安装
cypress
环境 - 在
cypress/component
编写组件测试脚本文件 - 执行
cypress open-ct
命令,启动cypress component testing
的服务运行xx.spec.js
测试脚本,便能直观看到单个组件自动执行操作逻辑
// Button.vue 组件
<template>
<div>Button测试</div>
</template>
<script>
export default {
}
</script>
<style>
</style>
// cypress/plugin/index.js 配置
const { startDevServer } = require('@cypress/vite-dev-server')
// eslint-disable-next-line no-unused-vars
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
on('dev-server:start', (options) => {
const viteConfig = {
// import or inline your vite configuration from vite.config.js
}
return startDevServer({ options, viteConfig })
})
return config;
}
// cypress/component/Button.spec.js Button组件测试脚本
import { mount } from "@cypress/vue";
import Button from "../../src/components/Button.vue";
describe("Button", () => {
it("should show button", () => {
// 挂载button
mount(Button);
cy.contains("Button");
});
});
e2e测试
e2e 测试,也叫端到端测试,主要是模拟用户对页面进行一系列操作并验证其是否符合预期。我们同样也可以使用 cypress 来做 e2e 测试,具体怎么做呢?
以 todo list 功能验证为例:
- 我们先建好
vue3
+vite
项目,编写测试组件 - 再安装
cypress
环境 - 在
cypress/integration
编写组件测试脚本文件 - 执行
cypress open
命令,启动cypress
的服务,选择xx.spec.js
测试脚本,便能直观看到模拟用户的操作流程
// cypress/integration/todo.spec.js todo功能测试脚本
describe('example to-do app', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/todo')
})
it('displays two todo items by default', () => {
cy.get('.todo-list li').first().should('have.text', 'Pay electric bill')
cy.get('.todo-list li').last().should('have.text', 'Walk the dog')
})
it('can add new todo items', () => {
const newItem = 'Feed the cat'
cy.get('[data-test=new-todo]').type(`${newItem}{enter}`)
cy.get('.todo-list li')
.should('have.length', 3)
.last()
.should('have.text', newItem)
})
it('can check off an item as completed', () => {
cy.contains('Pay electric bill')
.parent()
.find('input[type=checkbox]')
.check()
cy.contains('Pay electric bill')
.parents('li')
.should('have.class', 'completed')
})
context('with a checked task', () => {
beforeEach(() => {
cy.contains('Pay electric bill')
.parent()
.find('input[type=checkbox]')
.check()
})
it('can filter for uncompleted tasks', () => {
cy.contains('Active').click()
cy.get('.todo-list li')
.should('have.length', 1)
.first()
.should('have.text', 'Walk the dog')
cy.contains('Pay electric bill').should('not.exist')
})
it('can filter for completed tasks', () => {
// We can perform similar steps as the test above to ensure
// that only completed tasks are shown
cy.contains('Completed').click()
cy.get('.todo-list li')
.should('have.length', 1)
.first()
.should('have.text', 'Pay electric bill')
cy.contains('Walk the dog').should('not.exist')
})
it('can delete all completed tasks', () => {
cy.contains('Clear completed').click()
cy.get('.todo-list li')
.should('have.length', 1)
.should('not.have.text', 'Pay electric bill')
cy.contains('Clear completed').should('not.exist')
})
})
})
总结
本文前言部分通过开发、构建、性能、测试、部署、规范六个方面,较全面地梳理了前端工程化的知识点,正文则主要介绍了在实践项目中落地使用的前端工程化核心技术点。
希望本文能够帮助到正在学前端工程化的小伙伴构建完整的知识图谱~
作者:小铭子
来源:https://juejin.cn/post/7033355647521554446
【vue自定义组件】实现一个污染日历
前言
佛祖保佑, 永无bug
。Hello 大家好!我是海的对岸!
实际开发中,碰到一个日历的需求,这个日历的需求中要加入定制的业务,网上没有现成的,手动实现了一下,整理记录下。
动画效果:
实现
实现背景
工作上碰到一个需求,需要有一个可以在日历上能看到每天的污染情况的状态,因此,我们梳理下需求:
- 要有一个日历组件
- 要在这个日历组件中追加自己的业务逻辑
简单拎一下核心代码的功能
实现日历模块
大体上日历就是看某个月有多少多少天,拆分下,如下所示:
再对比这我们的效果图,日历上还要有上个月的末尾几天
实现上个月的末尾几天
monthFisrtDay() {
// 所指的星期中的某一天,使用本地时间。返回值是 0(周日) 到 6(周六) 之间的一个整数
// eslint-disable-next-line radix
const currDT = (parseInt(this.year.substr(0, 4)) + '/' + parseInt((this.month).replace('月', '')) + '/1');
let currWeek = new Date(currDT).getDay();
return ++currWeek || 7;
},
// 刷新日历 获得上个月的结尾天数 <=7
refreshCalendar() {
this.nunDays = [];
const lastDays = [];
const lastMon = (this.month).replace('月', '') * 1 - 1;
let lastDay = new Date(new Date(this.year.substr(0, 4), lastMon).getTime() - 8.64e7).getDate();
for (let i = 1; i < this.monthFisrtDay(); i += 1) {
lastDays.unshift(lastDay);
lastDay -= 1;
}
this.nunDays = lastDays;
},
实现每个月的实际天数
// 展示 日历数据
getDatas() {
if (this.dealDataFinal && this.dealDataFinal.length > 0) {
// console.log(this.dealDataFinal);
this.list = [];
const datas = this.dealDataFinal;
const dataMap = {};
if (datas.length > 0) {
datas.forEach((item) => {
item.level -= 1;
item.dateStr = item.tstamp.substr(0, 10);
item.date = item.tstamp.substr(8, 2);
dataMap[item.date] = item;
});
}
const curDay = new Date().getDate();
for (let i = 1; i <= this.monthDays; i += 1) {
let currColor = this.lvls[6];
let dateStr = String(i);
let isCurDay = false;
if (i == curDay) {
isCurDay = true; // 表示刚好是今天(该日期 和网络上的今天是同一天)
}
dateStr = '0' + dateStr;
dateStr = dateStr.substr(dateStr.length - 2);
const dataObj = dataMap[dateStr];
if (dataObj) {
if (dataObj.level >= 0 && dataObj.level <= 5) {
currColor = this.lvls[dataObj.level].color;
} else {
currColor = this.lvls[6].color;
}
this.list.push({
date: i,
curDay: isCurDay,
color: currColor,
datas: dataObj,
checkedColor: undefined, // 选中颜色
});
} else {
this.list.push({
date: i,
curDay: isCurDay,
color: this.lvls[6].color,
datas: {},
checkedColor: undefined, // 选中颜色
});
}
}
// console.log(this.list);
} else {
this.clearCalendar();
}
},
// 清除上一次的记录
clearCalendar() {
this.list = [];
for (let i = 1; i <= this.monthDays; i += 1) {
this.list.push({
date: i,
color: this.lvls[6].color,
datas: {},
});
}
},
实现日历之后,追加业务
定义业务上的字段
data() {
return {
...
lvls: [
{ title: '优', color: '#00e400' },
{ title: '良', color: '#ffff00' },
{ title: '轻度污染', color: '#ff7e00' },
{ title: '中度污染', color: '#ff0000' },
{ title: '重度污染', color: '#99004c' },
{ title: '严重污染', color: '#7e0023' },
{ title: '未知等级', color: '#cacaca' },
],
list: [], // 当前月的所有天数
dealDataFinal: [], // 处理接口数据之后获得的最终的数组
...
curYearMonth: '', // 当前时间 年月
choseYearMonth: '', // 选择的时间 年月
};
},
定义业务上的方法
// 加载等级
loadImgType(value) {
let imgUrl = 0;
switch (value) {
case '优':
imgUrl = 1;
break;
case '良':
imgUrl = 2;
break;
case '轻':
imgUrl = 3;
break;
case '中':
imgUrl = 4;
break;
case '重':
imgUrl = 5;
break;
case '严':
imgUrl = 6;
break;
default:
imgUrl = 0;
break;
}
return imgUrl;
},
因为展示效果,用到的是css,css用的比较多,这里就不一段一段的解读了,总而言之,就是日元素
不同状态的样式展示,通过前面设置的等级方法,来得到不同的返回参数,进而展示出不同参数对应的不同颜色样式。
最后会放出日历组件的完整代码。
完整代码
<template>
<div class="right-content">
<div style="height: 345px;">
<div class="" style="padding: 0px 15px;">
<el-select v-model="year" style="width: 119px;" popper-class="EntDate">
<el-option v-for="item in years" :value="item" :label="item" :key="item"></el-option>
</el-select>
<el-select v-model="month" style="width: 119px; margin-left: 10px;" popper-class="EntDate">
<el-option v-for="item in mons" :value="item" :label="item" :key="item"></el-option>
</el-select>
<div class="r-inline">
<span class="searchBtn" @click="qEQCalendar">查询</span>
</div>
</div>
<div class="calendar" element-loading-spinner="el-icon-loading"
element-loading-background="rgba(0, 0, 0, 0.6)">
<div class="day-title clearfix">
<div class="day-tt" v-for="day in days" :key="day">{{day}}</div>
</div>
<div class="clearfix" style="padding-top: 10px;">
<div :class="{'date-item': true, 'is-last-month': true,}" v-for="(item, index) in nunDays" :key="index + 'num'">
<div class="day">{{item}}</div>
</div>
<div :class="{'date-item': true, 'is-last-month': false, 'isPointer': isPointer}"
v-for="(item, index) in list" :key="index" @click="queryDeal(item)">
<div v-if="item.curDay && (curYearMonth === choseYearMonth)" class="day" :style="{border:'2px dashed' + item.color}"
:class="{'choseDateItemI': item.checkedColor === '#00e400',
'choseDateItemII': item.checkedColor === '#ffff00', 'choseDateItemIII': item.checkedColor === '#ff7e00', 'choseDateItemIV': item.checkedColor === '#ff0000',
'choseDateItemV': item.checkedColor === '#99004c', 'choseDateItemVI': item.checkedColor === '#7e0023', 'choseDateItemVII': item.checkedColor === '#cacaca'}"
>
今
</div>
<div v-else class="day" :style="{border:'2px solid' + item.color}"
:class="{'choseDateItemI': item.checkedColor === '#00e400',
'choseDateItemII': item.checkedColor === '#ffff00', 'choseDateItemIII': item.checkedColor === '#ff7e00', 'choseDateItemIV': item.checkedColor === '#ff0000',
'choseDateItemV': item.checkedColor === '#99004c', 'choseDateItemVI': item.checkedColor === '#7e0023', 'choseDateItemVII': item.checkedColor === '#cacaca'}"
>
{{item.date}}
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
const today = new Date();
const years = [];
const year = today.getFullYear();
for (let i = 2018; i <= year; i += 1) {
years.push(`${i}年`);
}
export default {
props: {
rightData2: {
type: Object,
defaul() {
return undefined;
},
},
isPointer: {
type: Boolean,
default() {
return false;
},
},
},
watch: {
rightData2(val) {
this.dealData(val);
},
calendarData(val) {
this.dealData(val);
},
},
data() {
return {
pointInfo: {
title: 'xxx污染日历',
},
days: ['日', '一', '二', '三', '四', '五', '六'],
year: year + '年',
years,
month: (today.getMonth() + 1) + '月',
mons: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
lvls: [
{ title: '优', color: '#00e400' },
{ title: '良', color: '#ffff00' },
{ title: '轻度污染', color: '#ff7e00' },
{ title: '中度污染', color: '#ff0000' },
{ title: '重度污染', color: '#99004c' },
{ title: '严重污染', color: '#7e0023' },
{ title: '未知等级', color: '#cacaca' },
],
list: [], // 当前月的所有天数
dealDataFinal: [], // 处理接口数据之后获得的最终的数组
nunDays: [],
testDays: ['日', '一', '二', '三', '四', '五', '六'],
calendarData: null,
curYearMonth: '', // 当前时间 年月
choseYearMonth: '', // 选择的时间 年月
};
},
computed: {
// 获取 select框中展示的具体月份应对应的月数
monthDays() {
const lastyear = (this.year).replace('年', '') * 1;
const lastMon = (this.month).replace('月', '') * 1;
const monNum = new Date(lastyear, lastMon, 0).getDate();
// return this.$mp.dateFun.GetMonthDays(this.year.substr(0, 4), lastMon);
return monNum;
},
},
methods: {
monthFisrtDay() {
// 所指的星期中的某一天,使用本地时间。返回值是 0(周日) 到 6(周六) 之间的一个整数
// eslint-disable-next-line radix
const currDT = (parseInt(this.year.substr(0, 4)) + '/' + parseInt((this.month).replace('月', '')) + '/1');
let currWeek = new Date(currDT).getDay();
return ++currWeek || 7;
},
// 刷新日历 获得上个月的结尾天数 <=7
refreshCalendar() {
this.nunDays = [];
const lastDays = [];
const lastMon = (this.month).replace('月', '') * 1 - 1;
let lastDay = new Date(new Date(this.year.substr(0, 4), lastMon).getTime() - 8.64e7).getDate();
for (let i = 1; i < this.monthFisrtDay(); i += 1) {
lastDays.unshift(lastDay);
lastDay -= 1;
}
this.nunDays = lastDays;
},
// 展示 日历数据
getDatas() {
if (this.dealDataFinal && this.dealDataFinal.length > 0) {
// console.log(this.dealDataFinal);
this.list = [];
const datas = this.dealDataFinal;
const dataMap = {};
if (datas.length > 0) {
datas.forEach((item) => {
item.level -= 1;
item.dateStr = item.tstamp.substr(0, 10);
item.date = item.tstamp.substr(8, 2);
dataMap[item.date] = item;
});
}
const curDay = new Date().getDate();
for (let i = 1; i <= this.monthDays; i += 1) {
let currColor = this.lvls[6];
let dateStr = String(i);
let isCurDay = false;
if (i == curDay) {
isCurDay = true; // 表示刚好是今天(该日期 和网络上的今天是同一天)
}
dateStr = '0' + dateStr;
dateStr = dateStr.substr(dateStr.length - 2);
const dataObj = dataMap[dateStr];
if (dataObj) {
if (dataObj.level >= 0 && dataObj.level <= 5) {
currColor = this.lvls[dataObj.level].color;
} else {
currColor = this.lvls[6].color;
}
this.list.push({
date: i,
curDay: isCurDay,
color: currColor,
datas: dataObj,
checkedColor: undefined, // 选中颜色
});
} else {
this.list.push({
date: i,
curDay: isCurDay,
color: this.lvls[6].color,
datas: {},
checkedColor: undefined, // 选中颜色
});
}
}
// console.log(this.list);
} else {
this.clearCalendar();
}
},
clearCalendar() {
this.list = [];
for (let i = 1; i <= this.monthDays; i += 1) {
this.list.push({
date: i,
color: this.lvls[6].color,
datas: {},
});
}
},
// 处理接口返回的日历数据
dealData(currDS) {
const tempData = [];
if (('dates' in currDS) && ('level' in currDS) && ('levelName' in currDS) && ('values' in currDS)) {
if (currDS.dates.length > 0 && currDS.level.length > 0 && currDS.levelName.length > 0 && currDS.values.length > 0) {
for (let i = 0; i < currDS.dates.length; i++) {
const temp = {
tstamp: currDS.dates[i],
level: currDS.level[i],
levelName: currDS.levelName[i],
value: currDS.values[i],
grade: this.loadImgType(currDS.levelName[i]),
week: this.testDays[new Date(currDS.dates[i]).getDay()], // currDS.dates[i]: '2020-03-31'
};
tempData.push(temp);
}
// this.dealDataFinal = tempData.filter(item => item.grade>0);
this.dealDataFinal = tempData;
this.refreshCalendar();
this.getDatas();
} else {
this.dealDataFinal = null;
this.getDatas();
}
} else {
this.dealDataFinal = null;
this.getDatas();
}
},
// 加载等级
loadImgType(value) {
let imgUrl = 0;
switch (value) {
case '优':
imgUrl = 1;
break;
case '良':
imgUrl = 2;
break;
case '轻':
imgUrl = 3;
break;
case '中':
imgUrl = 4;
break;
case '重':
imgUrl = 5;
break;
case '严':
imgUrl = 6;
break;
default:
imgUrl = 0;
break;
}
return imgUrl;
},
// (右边)区域环境质量日历
qEQCalendar() {
this.curYearMonth = new Date().getFullYear() + '-' + (new Date().getMonth() + 1);
this.choseYearMonth = this.year.substr(0, 4) + '-' + this.month.substr(0, 1);
this.calendarData = {
dates: [
'2020-07-01',
'2020-07-02',
'2020-07-03',
'2020-07-04',
'2020-07-05',
'2020-07-06',
'2020-07-07',
'2020-07-08',
'2020-07-09',
'2020-07-10',
'2020-07-11',
'2020-07-12',
'2020-07-13',
'2020-07-14',
'2020-07-15',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
],
level: [
1,
4,
2,
3,
1,
4,
4,
3,
1,
4,
2,
2,
4,
1,
3,
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
],
levelName: [
'优',
'中度污染',
'良',
'轻度污染',
'优',
'中度污染',
'中度污染',
'轻度污染',
'优',
'中度污染',
'良',
'良',
'中度污染',
'优',
'轻度污染',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
],
values: [
'65',
'65',
'65',
'65',
'65',
'65',
'65',
'65',
'65',
'65',
'65',
'65',
'65',
'65',
'65',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
],
};
// this.$axios.get('api/sinoyd-airquality/airquality/gis/calendar?year=' + parseInt(this.year.substr(0, 4)) + '&month=' + parseInt((this.month).replace('月', '')))
// .then((res) => {
// if (res.status == 200) {
// this.calendarData = res.data.data;
// } else {
// this.calendarData = null;
// }
// }, () => {
// this.calendarData = null;
// });
},
// 设置选中之后的逻辑
queryDeal(item) {
if (this.isPointer) {
console.log(item);
// 设置选中之后的效果
if (this.list && this.list.length) {
const tempList = [...this.list];
tempList.forEach((singleObj) => {
singleObj.checkedColor = undefined;
if (item.date === singleObj.date) {
singleObj.checkedColor = singleObj.color;
}
});
this.list = tempList;
}
}
},
},
mounted() {
this.qEQCalendar();
},
};
</script>
<style>
.EntDate{
background-color: rgba(2, 47, 79, 0.8) !important;
border: 1px solid rgba(2, 47, 79, 0.8) !important;
}
.EntDate /deep/ .popper__arrow::after{
border-bottom-color: rgba(2, 47, 79, 0.8) !important;
}
.EntDate /deep/ .el-scrollbar__thumb{
background-color: rgba(2, 47, 79, 0.8) !important;
}
.el-select-dropdown__item.hover, .el-select-dropdown__item:hover{
background-color: transparent !important;
}
</style>
<style lang="scss" scoped>
.r-inline{
display: inline-block;
}
.right-content{
width: 380px;
margin: 7px;
border-radius: 9px;
background-color: rgba(2, 47, 79, 0.8);
}
.day-title {
border-bottom: 2px solid #03596f;
padding: 1px 0 10px;
height: 19px;
.day-tt {
float: left;
text-align: center;
color: #ffffff;
width: 48px;
}
}
.date-item {
float: left;
text-align: center;
color: #fff;
width: 34px;
// padding: 2px 2px;
padding: 4px 4px;
margin: 0px 3px;
&.is-last-month {
color: #7d8c8c;
}
.day {
border-radius: 17px;
padding: 3px;
height: 25px;
line-height: 25px;
text-shadow: #000 0.5px 0.5px 0.5px, #000 0 0.5px 0, #000 -0.5px 0 0, #000 0 -0.5px 0;
background-color: #173953;
}
}
.calendar{
padding: 0px 6px;
}
.lvls {
padding: 0px 6px 6px 13px;
}
.lvl-t-item {
float: left;
font-size:10px;
padding-right: 3px;
.lvl-t-ico {
height: 12px;
width: 12px;
display: inline-block;
margin-right: 5px;
}
.lvl-tt {
color: #5b5e5f;
}
}
// ================================================================================================= 日期框样式
::v-deep .el-input__inner {
background-color: transparent;
border-radius: 4px;
border: 0px solid #DCDFE6;
color: #Fcff00;
font-size: 19px;
font-weight: bolder;
}
::v-deep .el-select .el-input .el-select__caret {
color: #fcff00;
font-weight: bolder;
}
// ================================================================================================= 日期框的下拉框样式
.el-select-dropdown__item{
background-color: rgba(2, 47, 79, 0.8);
color: white;
&:hover{
background-color: rgba(2, 47, 79, 0.8);
color: #5de6f8;
cursor: pointer;
}
}
.searchBtn {
cursor: pointer;
width: 60px;
height: 28px;
display: inline-block;
background-color: rgba(2, 47, 79, 0.8);
color: #a0daff;
text-align: center;
border: 1px solid #a0daff;
border-radius: 5px;
margin-left: 15px;
line-height: 28px;
}
.isPointer{
cursor: pointer;
}
.choseDateItemI{
border: 2px solid #00e400 !important;
box-shadow: #00e400 0px 0px 9px 2px;
}
.choseDateItemII{
border: 2px solid #ffff00 !important;
box-shadow: #ffff00 0px 0px 9px 2px;
}
.choseDateItemIII{
border: 2px solid #ff7e00 !important;
box-shadow: #ff7e00 0px 0px 9px 2px;
}
.choseDateItemIV{
border: 2px solid #ff0000 !important;
box-shadow: #ff0000 0px 0px 9px 2px;
}
.choseDateItemV{
border: 2px solid #99004c !important;
box-shadow: #99004c 0px 0px 9px 2px;
}
.choseDateItemVI{
border: 2px solid #7e0023 !important;
box-shadow: #7e0023 0px 0px 9px 2px;
}
.choseDateItemVII{
border: 2px solid #cacaca !important;
box-shadow: #cacaca 0px 0px 9px 2px;
}
</style>
链接:https://juejin.cn/post/7033038877485072397
收起阅读 »
生成 UUID 的三种方式及测速对比!
通用唯一识别码(英语:Universally Unique Identifier,缩写:UUID)是用于计算机体系中以识别信息的一个 128 位标识符,通常表现为一串 32 位十六进制数字。
UUID 用于解决 ID 唯一的问题!
然而,如何确保唯一,这本身就是一项挑战!
如何保证所生成 ID 只有一个副本?如何保证两个 ID 之间没有相关性?唯一性和随机性之间怎么取舍......
(OS:看过本瓜之前写的《理解 P/NP 问题时,我产生了一种已经触碰到人类认知天花板的错觉?!》这篇文章的朋友,应该知道:或许这个世界上没有随机这个东西?任何随机都能被量子计算算清楚,上帝到底掷骰子吗?没人知道......)
是否有真正的随机,先按下不表,
基于目前的算力精度,现在各种 UUID 生成器和不同版本的处理方式能最大限度的确保 ID 不重复,重复 UUID 码概率接近零,可以忽略不计。
本篇带来 3 种 UUID 生成器! 👍👍👍
UUID
基于 RFC4122 标准创建的 UUID,它有很多版本:v1,v2..v5;
uuid v1
是使用主机 MAC 地址和当前日期和时间的组合生成的,这种方式意味着 uuid 是匿名的。
uuid v4
是随机生成的,没有内在逻辑,组合方式非常多(2¹²⁸),除非每秒生成数以万亿计的 ID,否则几乎不可能产生重复,如果你的应用程序是关键型任务,仍然应该添加唯一性约束,以避免 v4 冲突。
uuid v5
与 v1 v4不同,它通过提供两条输入信息(输入字符串和命名空间)生成的,这两条信息被转换为 uuid;
特性:
- 完善;
- 跨平台;
- 安全:加密、强随机性;
- 体积小:零依赖,占用空间小;
- 良好的开源库支持:uuid command line;
上手:
import { v4 as uuidv4 } from 'uuid';
let uuid = uuidv4();
console.log(uuid) // ⇨ '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'
Crypto.randomUUID
Node.js API Crypto
提供 **randomUUID()**
方法,基于 RFC 4122 V4 生成随机数;
上手:
let uuid = crypto.randomUUID();
console.log(uuid); // ⇨ "36b8f84d-df4e-4d49-b662-bcde71a8764f"
Nano ID
Nano ID 有 3 个 api:
- normal (blocking); 普通
- asynchronous;异步
- non-secure;非安全
默认情况下,Nano ID 使用符号(A-Za-z0-9-
),并返回一个包含 21 个字符的 ID(具有类似于UUID v4的重复概率)。
特性:
- 体积小:130 bytes (压缩后);
- 零依赖;
- 生成更快;
- 安全:
- 更短,只要 21 位;
- 方便移植,支持 20 种编程语言.
上手:
import { nanoid } from 'nanoid'
let uuid = nanoid();
console.log(uuid) // ⇨ "V1StGXR8_Z5jdHi6B-myT"
Nano IDnpm 下载趋势:
测速
我们不妨来对比以上所提 3 种生成 UUID 的方式速度差异:
// test-uuid-gen.js
const { v4 as uuidv4 } = require('uuid');
for (let i = 0; i < 10_000_000; i++) {
uuidv4();
}
// test-crypto-gen.js
const { randomUUID } = require('crypto');
for (let i = 0; i < 10_000_000; i++) {
randomUUID();
}
// test-nanoid-gen.js
const { nanoid } = require('nanoid');
for (let i = 0; i < 10_000_000; i++) {
nanoid();
}
借助 hyperfine ;
调用测试:hyperfine ‘node test-uuid-gen.js’ ‘node test-crypto-gen.js’ ‘node test-nanoid-gen.js’
运行结果:
我们可以看到, 第二种 randomUUID()
比第三种 nanoid
快 4 倍左右,比第一种 uuid
快 12 倍左右~
作者:掘金安东尼
链接:https://juejin.cn/post/7033221241100042271
收起阅读 »
老板:你来弄一个团队代码规范!?
一、背景
9月份换了一个新部门,部门成立时间不长,当时组内还没有统一的代码规范
(部分工程用了规范,部分没有,没有统一的收口)
小组的技术栈框架有Vue
,React
,Taro
,Nuxt
,用Typescript
,算是比较杂了,结合到部门后续还可能扩展其他技术栈,我们从0-1实现了一套通用的代码规范
到现在小组内也用起来几个月了,整个过程还算是比较顺利且快速
,最近得空分享出来~
⚠️本篇文章不会讲基础的具体的规范,而是从实践经验讲怎么制定规范以及落地规范
二、为什么要代码规范
就不说了...大家懂的~
不是很了解的话,指路
三、确定规范范围
首先,跟主管同步,团队需要一个统一的规范,相信主管也在等着人来做起来
第一步收集团队的技术栈情况
,确定规范要包括的范围
把规范梳理为三部分ESLint
、StyleLint
、CommitLint
,结合团队实际情况分析如下
- ESLint:团队统一用的TypeScript,框架用到了Vue、React、Taro、还有Nuxt
- StyleLint:团队统一用的Less
- CommitLint:git代码提交规范
当然,还需考虑团队后续可能会扩展到的技术栈,以保证实现的时候确保可扩展性
四、调研业内实现方案
常见以下3种方案
团队制定文档式代码规范,成员都人为遵守这份规范来编写代码
靠人来保证代码规范存在
不可靠
,且需要人为review代码不规范,效率低
直接使用业内已有成熟规范,比如css使用StyleLint官方推荐规范stylelint-config-standard、stylelint-order,JavaScript使用ESLint推荐规范eslint:recommended等
a) 开源规范往往不能满足团队需求,
可拓展性差
; b) 业内提供的规范都是独立的(stylint只提供css代码规范,ESLint只提供JavaScript规范),是零散的
,对于规范初始化或升级存在成本高、不可靠问题(每个工程需要做人为操作多个步骤)
基于StyleLint、ESLint制定团队规范npm包,使用团队制定规范库
a) 该方案解决可扩展性差的问题,但是第二点中的(b)问题依旧存在
五、我们的技术方案
整体技术思路图如下图,提供三个基础包@jd/stylelint-config-selling
、@jd/eslint-config-selling
、@jd/commitlint-config-selling
分别满足StyleLint
、ESLint
、CommitLint
@jd/stylelint-config-selling
包括css、less、saas(团队暂未使用到)@jd/eslint-config-selling
包括Vue、React、Taro、Next、nuxt(团队暂未使用到)...,还包括后续可能会扩展到需要自定义的ESLint插件或者解析器@jd/commitlint-config-selling
统一使用git
向上提供一个简单的命令行工具,交互式初始化init
、或者更新update
规范
几个关键点
1、用lerna统一管理包
lerna是一个管理工具,用于管理包含多个软件包(package)的 JavaScript项目,业内已经广泛使用了,不了解的可以自己找资料看下
项目结构如下图
2、三个基础包的依赖包都设置为生产依赖dependencies
如下图,包@jd/eslint-config-selling
的依赖包都写在了生产依赖,而不是开发依赖
解释下:
开发依赖&生产依赖
开发依赖
:业务工程用的时候不会下载
开发依赖中的包,业内常见的规范如standard
、airbnb
都是写在开发依赖
- 缺点:业务工程除了安装
@jd/eslint-config-selling
外,需要自己去安装前置依赖包,如eslint
、根据自己选择的框架安装相关前置依赖包如使用的Vue需要安装eslint-plugin-vue
...使用成本、维护升级成本较高 - 优点:按需安装包,开发时不会安装多余的包(Lint相关的包在业务工程中都是开发依赖,所以只会影响开发时)
- 缺点:业务工程除了安装
生产依赖
:业务工程用的时候会下载
这些包
- 优点:安装
@jd/eslint-config-selling
后,无需关注前置依赖包 - 缺点:开发时会下载
@jd/eslint-config-selling
中所有写在生产依赖的包,即使有些用不到,比如你使用的是React,却安装了eslint-plugin-vue
- 优点:安装
3、提供简单的命令行
这个比较简单,提供交互式命令,支持一键初始化或者升级3种规范,就不展开说了
不会的,指路中高级前端必备:如何设计并实现一个脚手架
组里现在还没有项目模版脚手架,后续有的话需要把规范这部分融进去
六、最重要的一点
什么是一个好的规范?
基本每个团队的规范都是不一样的,团队各成员都认同并愿意遵守的规范
所以确定好技术方案后,涉及到的各个规范,下图,我们在小组内分工去制定
,比如几个人去制定styleLint的,几个人制定Vue的...
然后拉会评审
,大家统一通过的规范才敲定
最后以开源的方式维护升级
,使用过程中,遇到规范不合适的问题,提交issue,大家统一讨论确定是否需要更改规范
写在结尾
以上就是我们团队在前端规范落地方面的经验~
作者:jjjona0215
链接:https://juejin.cn/post/7033210664844066853
收起阅读 »
如何优雅的使用枚举功能——Constants
背景
在项目中,或多或少的会遇到使用枚举/快码/映射/字典,它们一般长这个样子。(PS:我不知道怎么称呼这个玩意)
在一些需要展示的地方,会使用下面的代码来展示定义。
<div>{{ statusList[status] }}</div>
而在代码中,又会使用下面的形式进行判断。这样写会让代码里充斥着许多的 'draft'
字符串,非常不利于管理。
if (status === 'draft') {
// do sth...
}
基于这种情况,在使用时会先声明一个变量。
const DRAFT = 'draft'
if (status === DRAFT) {
// do sth...
}
为了应对整个项目都会使用到的情况,会这样处理。
export const statusList = {
draft: '草稿',
pending: '待处理',
}
export const statusKeys = {
draft: 'draft',
pending: 'pending',
}
看了隔壁后端同事的代码,在 Java 里,枚举的定义及使用一般是如下形式。于是我就有了写这个工具类的想法。
public enum Status {
DRAFT('draft', '草稿');
Status(String code, String name) {
this.code = code;
this.name = name;
}
public String getCode() {
return code;
}
public String getName() {
return name;
}
}
public void aFunction() {
const draftCode = Status.DRAFT.getCode();
}
Constants
直接上代码
const noop = () => {}
class Constants {
constructor(obj) {
Object.keys(obj).forEach((key) => {
const initValue = obj[key];
if (initValue instanceof Object) {
console.error(`Warnning: this util only support primitive values, current value is ${JSON.stringify(initValue)}`)
// throw new Error(`Warnning: this util only support primitive values, current value is ${JSON.stringify(initValue)}`)
}
const newKey = `_${key}`;
this[newKey] = initValue;
Object.defineProperty(this, key, {
configurable : true,
enumerable : true,
get: function() {
const value = this[newKey];
const constructorOfValue = value.constructor;
const entry = [key, value];
['getKey', 'getValue'].forEach((item, index) => {
constructorOfValue.prototype[item] = () => {
constructorOfValue.prototype.getKey = noop;
constructorOfValue.prototype.getValue = noop;
return entry[index];
}
})
return value;
},
set: function(newValue) {
this[newKey] = newValue;
}
})
});
}
}
测试
const testValues = {
draft: '草稿',
id: 1,
money: 1.2,
isTest: true,
testObj: {},
testArray: [],
}
const constants = new Constants(testValues)
const test = (result, expect) => {
const isExpected = result === expect
if (isExpected) {
console.log(`PASS: The result is ${result}`)
} else {
console.error(`FAIL: the result is ${result}, should be ${expect}`)
}
}
test(constants.draft, '草稿')
test(constants.draft.getKey(), 'draft')
test(constants.draft.getValue(), '草稿')
test(constants.id, 1)
test(constants.id.getKey(), 'id')
test(constants.id.getValue(), 1)
test(constants.money, 1.2)
test(constants.money.getKey(), 'money')
test(constants.money.getValue(), 1.2)
test(constants.isTest, true)
test(constants.isTest.getKey(), 'isTest')
test(constants.isTest.getValue(), true)
a = 'test'
test(a.getKey(), undefined)
test(a.getValue(), undefined)
作者:Wetoria
链接:https://juejin.cn/post/7033220309386395679
收起阅读 »
CSS mask 实现鼠标跟随镂空效果
偶然在某思看到这样一个问题,如何使一个div的部分区域变透明而其他部分模糊掉?,最后实现效果是这样的
进一步,还能实现任意形状的镂空效果
鼠标经过的地方清晰可见,其他地方则是模糊的。
可能一开始无从下手,不要急,可以先从简单的、类似的效果开始,一步一步尝试,一起看看吧。
一、普通半透明的效果
比如平时开发中碰到更多的可能是一个半透明的效果,有点类似于探照灯(鼠标外面的地方是半透明遮罩,看起来会暗一点)。如下:
那先从这种效果开始吧,假设有这样一个布局:
<div class="wrap" id="img">
<img class="prew" src="https://tva1.sinaimg.cn/large/008i3skNgy1gubr2sbyqdj60xa0m6tey02.jpg">
</div>
那么如何绘制一个镂空的圆呢?先介绍一种方法
其实很简单,只需要一个足够大的投影就可以了,原理如下
这里可以用伪元素::before
来绘制,结构更加精简。用代码实现就是
.wrap::before{
content:'';
position: absolute;
width: 100px;
height: 100px;
border-radius: 50%;
left: 50%;
top: 50%;
transform: translate(-50%,-50%); /*默认居中*/
box-shadow: 0 0 0 999vw rgba(0, 0, 0, .5); /*足够大的投影*/
}
可以得到这样的效果
二、借助 CSS 变量传递鼠标位置
按照以往的经验,可能会在 js 中直接修改元素的 style 属性,类似这样
img.addEventListener('mousemove', (ev) => {
img.style.left = '...';
img.style.top = '...';
})
但是这样交互与业务逻辑混杂在一起,不利于后期维护。其实,我们只需要鼠标的坐标,在 CSS 中也能完全实现跟随的效果。
这里借助 CSS 变量,那一切就好办了!假设鼠标的坐标是 [--x,--y]
(范围是[0, 1]
),那么遮罩的坐标就可以使用 calc
计算了
.wrap::before{
left: calc(var(--x) * 100%);
top: calc(var(--y) * 100%);
}
然后鼠标坐标的获取可以使用 JS 来计算,也比较容易,如下
img.addEventListener('mousemove', (ev) => {
img.style.setProperty('--x', ev.offsetX / ev.target.offsetWidth);
img.style.setProperty('--y', ev.offsetY / ev.target.offsetHeight);
})
这样,半透明效果的镂空效果就完成了
完整代码可以访问: backdrop-shadow (codepen.io)
三、渐变也能实现半透明的效果
除了上述阴影扩展的方式,CSS 径向渐变也能实现这样的效果
绘制一个从透明到半透明的渐变,如下
.wrap::before{
content: '';
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
background: radial-gradient( circle at center, transparent 50px, rgba(0,0,0,.5) 51px);
}
可以得到这样的效果
然后,把鼠标坐标映射上去就可以了。从这里就可以看出 CSS 变量的好处,无需修改 JS,只需要在CSS中修改渐变中心点的位置就可以实现了
.wrap::before{
background: radial-gradient( circle at calc(var(--x) * 100% ) calc(var(--y) * 100% ), transparent 50px, rgba(0,0,0,.5) 51px);
}
四、背景模糊的效果尝试
CSS 中有一个专门针对背景(元素后面区域)的属性:backdrop-filter。使用方式和 filter
完全一致!
backdrop-filter: blur(10px);
下面是 MDN 中的一个示意效果
backdrop-filter
是让当前元素所在区域后面的内容模糊,要想看到效果,需要元素本身半透明或者完全透明;而filter
是让当前元素自身模糊。有兴趣的可以查看这篇文章: CSS backdrop-filter简介与苹果iOS毛玻璃效果 « 张鑫旭-鑫空间-鑫生活 (zhangxinxu.com)
需要注意的是,这种模糊与背景的半透明度没有任何关系,哪怕元素本身是透明的,仍然会有效果。例如下面是去除背景后的效果 ,整块都是模糊的
如果直接运用到上面的例子会怎么样呢?
1. 阴影实现
在上面第一个例子中添加 backdrop-filter
.wrap::before{
content:'';
position: absolute;
width: 100px;
height: 100px;
border-radius: 50%;
left: 50%;
top: 50%;
transform: translate(-50%,-50%); /*默认居中*/
box-shadow: 0 0 0 999vw rgba(0, 0, 0, .5); /*足够大的投影*/
backdrop-filter: blur(5px)
}
得到效果如下
可以看到圆形区域是模糊的,正好和希望的效果相反。其实也好理解,只有圆形区域才是真实的结构,外面都是阴影,所以最后作用的范围也只有圆形部分
2. 渐变实现
现在在第二个例子中添加 backdrop-filter
.wrap::before{
content: '';
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
background: radial-gradient( circle at calc(var(--x) * 100% ) calc(var(--y) * 100% ), transparent 50px, rgba(0,0,0,.5) 51px);
backdrop-filter: blur(5px)
}
效果如下
已经全部都模糊了,只是圆形区域外暗一些。由于::before
的尺寸占据整个容器,所以整个背后都变模糊了,圆形外部比较暗是因为半透明渐变的影响。
总之还是不能满足我们的需求,需要寻求新的解决方式。
五、CSS MASK 实现镂空
与其说是让圆形区域不模糊,还不如说是把那块区域给镂空了。就好比之前是一整块磨砂玻璃,然后通过 CSS MASK 打了一个圆孔,这样透过圆孔看到后面肯定是清晰的。
可以对第二个例子稍作修改,通过径向渐变绘制一个透明圆,剩余部分都是纯色的遮罩层,示意如下
用代码实现就是
.wrap::before{
content: '';
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
-webkit-mask: radial-gradient( circle at calc(var(--x, .5) * 100% ) calc(var(--y, .5) * 100% ), transparent 50px, #000 51px);
background: rgba(0,0,0,.3);
backdrop-filter: blur(5px)
}
这样就实现了文章开头的效果
完整代码可以查看:backdrop-mask (codepen.io)
六、CSS MASK COMPOSITE 实现更丰富的镂空效果
除了使用径向渐变绘制遮罩层以外,还可以通过 CSS MASK COMPOSITE(遮罩合成)的方式来实现。标准关键值如下(firefox支持):
/* Keyword values */
mask-composite: add; /* 叠加(默认) */
mask-composite: subtract; /* 减去,排除掉上层的区域 */
mask-composite: intersect; /* 相交,只显示重合的地方 */
mask-composite: exclude; /* 排除,只显示不重合的地方 */
遮罩合成是什么意思呢?可以类比 photoshop 中的形状合成,几乎是一一对应的
-webkit-mask-composite 与标准下的值有所不同,属性值非常多,如下(chorme 、safari 支持)
-webkit-mask-composite: clear; /*清除,不显示任何遮罩*/
-webkit-mask-composite: copy; /*只显示上方遮罩,不显示下方遮罩*/
-webkit-mask-composite: source-over;
-webkit-mask-composite: source-in; /*只显示重合的地方*/
-webkit-mask-composite: source-out; /*只显示上方遮罩,重合的地方不显示*/
-webkit-mask-composite: source-atop;
-webkit-mask-composite: destination-over;
-webkit-mask-composite: destination-in; /*只显示重合的地方*/
-webkit-mask-composite: destination-out;/*只显示下方遮罩,重合的地方不显示*/
-webkit-mask-composite: destination-atop;
-webkit-mask-composite: xor; /*只显示不重合的地方*/
是不是一脸懵?这里做了一个对应的效果图,如果不太熟练,使用的时候知道有这样一个功能,然后对着找就行了
回到这里,可以绘制一整块背景和一个圆形背景,然后通过遮罩合成排除(mask-composite: exclude
)打一个孔就行了,实现如下
.wrap::before{
content: '';
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
-webkit-mask: url("data:image/svg+xml,%3Csvg width='50' height='50' viewBox='0 0 50 50' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='25' cy='25' r='25' fill='%23C4C4C4'/%3E%3C/svg%3E"), linear-gradient(red, red);
-webkit-mask-size: 50px, 100%;
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: calc(var(--x, .5) * 100% + var(--x, .5) * 100px - 50px ) calc(var(--y, .5) * 100% + var(--y, .5) * 100px - 50px ), 0;
-webkit-mask-composite: xor; /*只显示不重合的地方, chorem 、safari 支持*/
mask-composite: exclude; /* 排除,只显示不重合的地方, firefox 支持 */
background: rgba(0,0,0,.3);
backdrop-filter: blur(5px)
}
需要注意-webkit-mask-position
中的计算,这样也能很好的实现这个效果
完整代码可以查看:backdrop-mask-composite (codepen.io)
你可能已经发现,上述例子中的圆是通过 svg 绘制的,还用到了遮罩合成,看着好像更加繁琐了。其实呢,这是一种更加万能的解决方式,可以带来无限的可能性。比如我需要一个星星⭐️的镂空效果,很简单,先通过一个绘制软件画一个
然后把这段 svg 代码转义一下,这里推荐使用张鑫旭老师的SVG在线压缩合并工具
替换到刚才的例子中就可以了
.wrap::before{
content: '';
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
-webkit-mask: url("data:image/svg+xml,%3Csvg width='96' height='91' viewBox='0 0 96 91' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M48 0l11.226 34.55h36.327l-29.39 21.352L77.39 90.45 48 69.098 18.61 90.451 29.837 55.9.447 34.55h36.327L48 0z' fill='%23C4C4C4'/%3E%3C/svg%3E"), linear-gradient(red, red);
-webkit-mask-size: 50px, 100%;
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: calc(var(--x, .5) * 100% + var(--x, .5) * 100px - 50px ) calc(var(--y, .5) * 100% + var(--y, .5) * 100px - 50px ), 0;
-webkit-mask-composite: xor; /*只显示不重合的地方, chorem 、safari 支持*/
mask-composite: exclude; /* 排除,只显示不重合的地方, firefox 支持 */
background: rgba(0,0,0,.3);
backdrop-filter: blur(5px)
}
星星镂空实现效果如下
完整代码可以查看:backdrop-star (codepen.io)
再比如一个心形❤,实现效果如下
完整代码可以查看:backdrop-heart (codepen.io)
只有想不到,没有做不到
作者:XboxYan
链接:https://juejin.cn/post/7033188994641100831
收起阅读 »
微信小程序如何确保每个页面都已经登陆
现状
一个微信小程序中,有首页,有个人页面,还有一些列表页面,详情页面等等,这些页面大部分是可以分享的。当分享出去的页面被一个另一个用户打开的时候,这个页面怎么确保这个用户已经登陆了呢?
网上有很多方案是在请求封装里面加一道拦截,如果没有token,就先调用登陆请求获取token后,再继续。
这种方案没毛病,只要注意一点,当一个页面有多个请求同时触发时,当所有请求拦截后,放到一个数组里面,在获取token成功后,遍历数组一个个请求就行。
但这个需求再复杂一点,比如连锁便利店小程序,大部分页面都需要有一个门店(因为需要根据门店获取当前门店商品的库存、价格等),这个门店是根据当前的定位来调用后台接口获得的,这个时候如果在请求里进行封装就太麻烦了。
解决方案
首先,我们注意到,登陆,获取定位与我们的页面请求是异步的,我们需要保证页面请求是在登陆和获取定位之后,但要是我们每个页面都写一个遍,可维护性就太差了。所以我们可以抽离出一个方法来做这件事。
所以代码就这样了:
const app = getApp()
Page({
data: {
logs: []
},
onLoad() {
app.commonLogin(()=>{
// 处理页页面请求
})
}
})
做到这里好像是解决我们的问题,但再想一想,如果还想做更多的事,比如说每个页面的onShareAppMessage统一处理,但我又不想在每个页面再写一遍,另外,我又想自己对每个页面实现一个watch,怎么做?
进一步解决方案
我们可以看到微信小程序,每个页面是一个Page(),那么我们可以给这个Page外面加一层壳子,我们可以有一个MyPage来替换这个Page,废话不多说,上代码:
tool.js 相关代码
/**
* 处理合并参数
*/
handlePageParamMerge(arg) {
let numargs = arg.length; // 获取被传递参数的数值。
let data = {}
let page = {}
for (let ix in arg) {
let item = arg[ix]
if (item.data && typeof (item.data) === 'object') {
data = Object.assign(data, item.data)
}
if (item.methods && typeof (item.methods) === 'object') {
page = Object.assign(page, item.methods)
} else {
page = Object.assign(page, item)
}
}
page.data = data
return page
}
/***
* 合并页面方法以及数据, 兼容 {data:{}, methods: {}} 或 {data:{}, a:{}, b:{}}
*/
mergePage() {
return this.handlePageParamMerge(arguments)
}
/**
* 处理组件参数合并
*/
handleCompParamMerge(arg) {
let numargs = arg.length; // 获取被传递参数的数值。
let data = {}
let options = {}
let properties = {}
let methods = {}
let comp = {}
for (let ix in arg) {
let item = arg[ix]
// 合并组件的初始数据
if (item.data && typeof (item.data) === 'object') {
data = Object.assign(data, item.data)
}
// 合并组件的属性列表
if (item.properties && typeof (item.properties) === 'object') {
properties = Object.assign(properties, item.properties)
}
// 合组件的方法列表
if (item.methods && typeof (item.methods) === 'object') {
methods = Object.assign(methods, item.methods)
}
if (item.options && typeof (item.options) === 'object') {
options = Object.assign(options, item.options)
}
comp = Object.assign(comp, item)
}
comp.data = data
comp.options = options
comp.properties = properties
comp.methods = methods
return comp
}
/**
* 组件混合 {properties: {}, options: {}, data:{}, methods: {}}
*/
mergeComponent() {
return this.handleCompParamMerge(arguments)
}
/***
* 合成带watch的页面
*/
newPage() {
let options = this.handlePageParamMerge(arguments)
let that = this
let app = getApp()
//增加全局点击登录判断
if (!options.publicCheckLogin){
options.publicCheckLogin = function (e) {
let pages = getCurrentPages()
let page = pages[pages.length - 1]
let dataset = e.currentTarget.dataset
let callback = null
//获取回调方法
if (dataset.callback && typeof (page[dataset.callback]) === "function"){
callback = page[dataset.callback]
}
// console.log('callback>>', callback, app.isRegister())
//判断是否登录
if (callback && app.isRegister()){
callback(e)
}
else{
wx.navigateTo({
url: '/pages/login/login'
})
}
}
}
const { onLoad } = options
options.onLoad = function (arg) {
options.watch && that.setWatcher(this)
onLoad && onLoad.call(this, arg)
}
const { onShow } = options
options.onShow = function (arg) {
if (options.data.noAutoLogin || app.isRegister()) {
onShow && onShow.call(this, arg)
//页面埋点
app.ga({})
}
else {
wx.navigateTo({
url: '/pages/login/login'
})
}
}
return Page(options)
}
/**
* 合成带watch等的组件
*/
newComponent() {
let options = this.handleCompParamMerge(arguments)
let that = this
const { ready } = options
options.ready = function (arg) {
options.watch && that.setWatcher(this)
ready && ready.call(this, arg)
}
return Component(options)
}
/**
* 设置监听器
*/
setWatcher(page) {
let data = page.data;
let watch = page.watch;
Object.keys(watch).forEach(v => {
let key = v.split('.'); // 将watch中的属性以'.'切分成数组
let nowData = data; // 将data赋值给nowData
for (let i = 0; i < key.length - 1; i++) { // 遍历key数组的元素,除了最后一个!
nowData = nowData[key[i]]; // 将nowData指向它的key属性对象
}
let lastKey = key[key.length - 1];
// 假设key==='my.name',此时nowData===data['my']===data.my,lastKey==='name'
let watchFun = watch[v].handler || watch[v]; // 兼容带handler和不带handler的两种写法
let deep = watch[v].deep; // 若未设置deep,则为undefine
this.observe(nowData, lastKey, watchFun, deep, page); // 监听nowData对象的lastKey
})
}
/**
* 监听属性 并执行监听函数
*/
observe(obj, key, watchFun, deep, page) {
var val = obj[key];
// 判断deep是true 且 val不能为空 且 typeof val==='object'(数组内数值变化也需要深度监听)
if (deep && val != null && typeof val === 'object') {
Object.keys(val).forEach(childKey => { // 遍历val对象下的每一个key
this.observe(val, childKey, watchFun, deep, page); // 递归调用监听函数
})
}
var that = this;
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
set: function (value) {
if (val === value) {
return
}
// 用page对象调用,改变函数内this指向,以便this.data访问data内的属性值
watchFun.call(page, value, val); // value是新值,val是旧值
val = value;
if (deep) { // 若是深度监听,重新监听该对象,以便监听其属性。
that.observe(obj, key, watchFun, deep, page);
}
},
get: function () {
return val;
}
})
}
页面代码:
app.tool.newPage({
data: {
// noAutoLogin: false
},
onShow: function () {
// 在这里写页面请求逻辑
}
}
最后
代码是在线上跑了很久的,tool里的newPage封装,你可以根据自己的需求进行添加。总之,我这里是提供一种思路,如有更佳,欢迎分享。
作者:盗道
链接:https://juejin.cn/post/7026544177844355103
收起阅读 »
你写过的所有代码都逃不过这两方面:API 和抽象
作为前端,你可能开发过 Electron 桌面应用、小程序、浏览器上的 web 应用、基于 React Native 等跨端引擎的 app,基于 Node.js 的工具或者服务等各种应用,这些都是 JS 的不同的 runtime,开发也都是基于前端那套技术。
面对这么多的细分领域,作为前端工程师的你是否曾迷茫过:这么多技术我该学什么?他们中有没有什么本质的东西呢?
其实所有的这些技术,你写过的所有代码,都可以分为两个方面: api 和 抽象。
api
不同平台提供的 api 不同,支持的能力不同:
浏览器提供了 dom api、支持了 css 的渲染,还提供了音视频、webgl 等相关 api,这些 api 是我们开发前端应用的基础。
Node.js 提供了操作系统能力的 api,比如进程、线程、网络、文件等,这些 api 是我们开发工具链或后端应用的基础。
React Native 等跨端引擎支持了 css 的渲染,还提供了设备能力的 api,比如照相机、闪光灯、传感器、GPS 等 api,这是我们开发移动 app 的基础。
Electron 集成了 Chromium 和 Node.js,同时还提供了桌面相关的 api。
小程序支持了 css 的渲染之外,还提供了一些宿主 app 能力的 api。
此外,还有很多的 runtime,比如 vscode 插件、sketch 插件等,都有各自能够使用的 api。
不同的 JS runtime 提供了不同 api 给上层应用,这是应用开发的基础,也是应用开发的能力边界。
抽象
基于 runtime 提供的 api 我们就能完成应用的功能开发,但是复杂场景下往往会做一些抽象。
比如浏览器上的前端应用主要是把数据通过 dom api 和 css 渲染出来,并做一些交互,那么我们就抽象出了数据驱动的前端框架,抽象出了组件、状态、数据流等概念。之后就可以把不同的需求抽象为不同的组件、状态。
经过层层抽象之后,开发复杂前端应用的时候代码更容易维护、成本更低。
比如基于 Node.js 的 fs、net、http 等 api 我们就能实现 web server,但是对于复杂的企业级应用,我们通过后端框架做 MVC 的抽象,抽象出控制器、服务、模型、视图等概念。之后的后端代码就可以把需求抽象为不同的控制器和服务。
经过 MVC 的抽象之后,后端应用的分层更清晰、更容易维护和扩展。
复杂的应用需要在 api 的基础上做一些抽象。我们往往会用框架做一层抽象,然后自己再做一层抽象,经过层层抽象之后的代码是更容易维护和扩展的。这也就是所谓的架构。
如何深入 api 和抽象
api
api 是对操作系统能力或不同领域能力的封装。
比如 Node.js 的进程、线程、文件、网络的 api 是对操作系统能力的封装,想深入它们就要去学习操作系统的一些原理。
而 webgl、音视频等 api 则分别是对图形学、音视频等领域的能力的封装,想要深入它们就要去学习这些领域的一些原理。
个人觉得我们知道 api 提供了什么能力就行,没必要过度深入 api 的实现原理。
抽象
抽象是基于编程语言的编程范式,针对不同目标做的设计。
Javascript 提供了面向对象、函数式等编程范式,那么就可以基于对象来做抽象,使用面向对象的各种设计模式,或者基于函数式那一套。这是抽象的基础。
抽象是根据不同的目标来做的。
前端领域主要是要分离 dom 操作和数据,把页面按照功能做划分,所以根据这些目标就做了 mvvm 和组件化的抽象。
后端领域主要是要做分层、解耦等,于是就做了 IOC、MVC 等抽象。
可以看到,抽象是基于编程语言的范式,根据需求做的设计,好的框架一定是做了满足某种管理代码的需求的抽象。
想要提升抽象、架构设计能力的话,可以学习下面向对象的设计模式,或者函数式等编程范式。研究各种框架是如何做的抽象。
总结
不同平台提供了不同的 api,这是应用开发的基础和边界。复杂应用往往要在 api 基础上做层层抽象,一般会用框架做一层抽象,自己再做一层抽象,目标是为了代码划分更清晰,提升可维护性和可扩展性。
其实我们写过的所有代码,都可以分为 api 和抽象这两方面。
深入 API 原理的话要深入操作系统和各领域的知识。提升抽象能力的话,可以学习面向对象的设计模式或者函数式等编程范式。
不管你现在做哪个平台之上的应用开发,刚开始都是要先学习 api 的,之后就是要理解各种抽象了:框架是怎么抽象的,上层又做了什么抽象。
API 保证下限,抽象可以提高上限。而且抽象能力或者说架构能力是可以迁移的,是程序员最重要的能力之一。
作者:zxg_神说要有光
链接:https://juejin.cn/post/7031931672538906637
收起阅读 »