注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

[react-native]JSX和RN样式以及和web的不同之处

全屏状态栏 import { View, Text, Image, StatusBar } from 'react-native' <StatusBar backgroundColor="transparent" translucent={ true }...
继续阅读 »

全屏状态栏


import { View, Text, Image, StatusBar } from 'react-native'
<StatusBar backgroundColor="transparent" translucent={ true } />


JSX:React中写组件的代码格式, 全称是JavaScript xml


import React from 'react'
import { View, Text } from 'react-native'

const App = () => <View>
<Text>JSX Hello World</Text>
</View>

export default App


RN样式(主要讲解和web开发的不同之处)


image.png


#屏幕宽度和高度
import { Dimensions } from 'react-native'
const screenWidth = Math.round(Dimensions.set('window').width)
const screenHeight = Math.round(Dimensions.get('window').height)

#变换
<Text style={{ transform: [{translateY: 300}, {scale: 2}] }}>变换</Text>


标签



  1. View

  2. Text

  3. TouchableOpacity

  4. Image

  5. ImageBackground

  6. TextInput

  7. 其他 =>

    1. button

    2. FlatList

    3. ScrollView

    4. StatusBar

    5. TextInput




View



  1. 相当于以前web中的div

  2. 不支持设置字体大小, 字体颜色等

  3. 不能直接放文本内容

  4. 不支持直接绑定点击事件(一般使用TouchableOpactiy 来代替)


Text



  1. 文本标签,可以设置字体颜色、大小等

  2. 支持绑定点击事件


TouchableOpacity (onpress => 按下事件 onclick=> 点击事件)


可以绑定点击事件的块级标签



  1. 相当于块级的容器

  2. 支持绑定点击事件 onPress

  3. 可以设置点击时的透明度


import React from 'react'
import {TouchableOpacity, Text} from 'react-native'

const handlePress = () => {
alert('111')
}

const App = () =>
<TouchableOpacity activeOpacity={0} onPress={ handlePress }>
<Text>点击事件</Text>
</TouchableOpacity>

export default App


Image图片渲染


1.渲染本地图片时


<Image source={ require("../gril.png") } />


2.渲染网络图片时, 必须加入宽度和高度


<Image source={{ uri: 'https://timgsa.baidu.com/xxx.png }} style={{ width: 200, height: 300 }} />


3.在android上支持GIF和WebP格式图片


默认情况下Android是不支持gif和webp格式的, 只需要在 android/app/build.gradle 文件中根据需要手动添加


以下模块:


dependencies {
// 如果你需要支持android4.0(api level 14)之前的版本
implementation 'com.facebook.fresco:animated-base-support:1.3.0'

// 如果你需要支持GIF动画
implementation 'com.facebook.fresco:animated-gif:2.0.0'

// 如果你需要支持webp格式,包括webp动图
implementation 'com.facebook.fresco:animated-webp:2.1.0'
implementation 'com.facebook.fresco:webpsupport:2.0.0'

// 如果只需要支持webp格式而不需要动图
implementation 'com.facebook.fresco:websupport:2.0.0'
}


ImageBackground


一个可以使用图片当做背景的容器,相当于以前的 div + 背景图片


import React from 'react'
import { Text, ImageBackground } from 'react-native'

const App = () =>
<ImageBackground source={require('./assets/logo.png')} style={{ width: 200, height: 200 }}>
<Text>Inside</Text>
</ImageBackground>

export default App


TextInput输入框组件


可以通过 onChangeText 事件来获取输入框的值
语法:



  1. 组件

  2. 插值表达式

  3. 状态state

  4. 属性props

  5. 调试

  6. 事件

  7. 生命周期


import React from 'react'
import { TextInput } from 'react-native'

const handleChangeText = (text) => {
alert(text)
}

#onChangeText => 获取输入的值
const App = () => <TextInput onChangeText={ handleChangeText } />

export default App


花括号{}里面可以直接添加JS代码的


组件: 函数组件, 类组件


函数组件



  1. 没有state(通过hooks可以有)

  2. 没有生命周期(通过hooks可以有)

  3. 适合简单的场景


类组件



  1. 适合复杂的场景

  2. 有state

  3. 有生命周期


属性props (父子组件的传递)和插槽slot


import React from 'react'
import { View, Text } from 'react-native'

const App = () => (
<View>
<Text>==========</Text>
<Sub color="red">
<View><Text>1234</Text></View>
</Sub>
<Text>==========</Text>
</View>
)

// 子组件 props
const Sub = (props) =>
(<View><Text style={{ color: props.color }}>{ props.children }</Text></View>)

// 插槽类似于 vue中的slot
export default App



人懒,不想配图,都是自己的博客内容(干货),望能帮到大家




链接:https://juejin.cn/post/6977283223499833358

收起阅读 »

学习一下Electron,据说很简单

Electron怎么玩 真的很简单的,面向百度编程,找寻前辈的足迹,真的很容易的。😄 直接点,开整 首先安装Electron,但是有个坑 坑就是安装卡住了,没事有办法: npm config set registry=https://registry.npm....
继续阅读 »

Electron怎么玩


真的很简单的,面向百度编程,找寻前辈的足迹,真的很容易的。😄


直接点,开整


首先安装Electron,但是有个坑


坑就是安装卡住了,没事有办法:


npm config set registry=https://registry.npm.taobao.org/
npm config set ELECTRON_MIRROR=http://npm.taobao.org/mirrors/electron/


第一行相信大家都做了。


第二行很关键,如果不设置的话,他会在最后卡住,一直在加载,也不知道搞什么呢。🤦‍


然后在项目的根目录下创建main.js

/* main.js */
const { app, BrowserWindow } = require('electron')
const path = require('path')
const ipc = require('electron').ipcMain
const http = require('http');
const qs = require("qs")
const os = require('os');

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow;
let server;

const initServer = () => {
server = http.createServer(function (request, response) {
// 定义了一个post变量,用于暂存请求体的信息
let post = '';
// 通过req的data事件监听函数,每当接受到请求体的数据,就累加到post变量中
//当有数据请求时触发
request.on('data', function (data) {
post += data;
});
// 在end事件触发后,通过querystring.parse将post解析为真正的POST请求格式,然后向客户端返回。
request.on('end', function () {
//解析为post对象
post = JSON.parse(post);
//将对象转化为字符串
response.writeHead(200, { 'Content-Type': 'text-plain' });
response.end('{"status":200}\n');
mainWindow.webContents.send("flightdata", post)
});
}).listen(8124);
}


const createWindow = () => {
// Create the browser window.
mainWindow = new BrowserWindow({
fullscreen: false,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});

// and load the index.html of the app.
mainWindow.loadFile("./build/index.html");

// mainWindow.maximize()
mainWindow.removeMenu()
// mainWindow.webContents.openDevTools()
mainWindow.webContents.openDevTools({mode:'right'});
// Emitted when the window is closed.
mainWindow.on('closed', () => {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
mainWindow = null;
});
};

const initApp = () => {
createWindow();
initServer();
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', initApp);

// Quit when all windows are closed.
app.on('window-all-closed', () => {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit();
}
});

app.on('activate', () => {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) {
createWindow();
}
});

这里面大部分逻辑你不用考虑,我以后的文章会讲到,而你只需要知道一点就行。

那就是我在这个mian中指定了一个静态网页,巧了!位置就在我们打包文件夹build下🤭。

// and load the index.html of the app.
mainWindow.loadFile("./build/index.html");

然后配置package.json

{
...
"main": "main.js",
"homepage": "./",
...
}

分析:

main:配置刚才我们创建的Electron的入口文件main.js homepage:如果不配置的话,就会。。,em~~~~就会。。算了贴代码吧

...
const publicUrlOrPath = getPublicUrlOrPath(
process.env.NODE_ENV === 'development',
require(resolveApp('package.json')).homepage,
process.env.PUBLIC_URL
);
...

这几句代码就说明webpack会通过package中配置的homepage来设置PUBLIC_URL,so,那么配置homepage就很有必要。
否则,会白屏的!!!


对了还有个大坑,一定注意


如果你用的是react-router提供的BrowserRouter,那你会蒙圈的,因为什么都不会显示,顶多有个你事先安排好的“404”页面,就好像在用浏览器直接访问地址为https://****/index.htmlhistory模式根本不起作用,我猜这是浏览器独门绝技,electron还没支持,我猜的,不一定对。


所以一定要用hash模式

<HashRouter getUserConfirmation={this.getConfirmation}>
...
</HashRouter>

最后我们再配置一下启动脚本

/* package.json */
"scripts": {
...
"electron": "electron ."
...
},

看下效果吧

结语

这么一来,“中用”的Moderate就初步集成了Electron,直接一行命令就能打包成一个pc和mac端都能用的应用,美滋滋,但请掘友们相信,这只是第一部分🤭,接下来还有很多东西要补上。


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

Vue基操会了,还有必要学React么?

React前言 很高兴你能来到这里学习React.js技术,这是本专栏的第一节,主要介绍一下小伙伴们常问的一些问题,虽然废话很多,但是还是建议你可以花几分钟看一下。 React简介 首先不能否认React.js是全球最火的前端框架(Facebook推出的前端框...
继续阅读 »

React前言


很高兴你能来到这里学习React.js技术,这是本专栏的第一节,主要介绍一下小伙伴们常问的一些问题,虽然废话很多,但是还是建议你可以花几分钟看一下。


React简介


首先不能否认React.js是全球最火的前端框架(Facebook推出的前端框架),国内的一二线互联网公司大部分都在使用React进行开发,比如阿里美团百度去哪儿网易知乎这样的一线互联网公司都把React作为前端主要技术栈。
React的社区也是非常强大的,随着React的普及也衍生出了更多有用的框架,比如ReactNative和React VR。React从13年开始推广,现在已经推出18.x.x版本,性能和易用度上,都有很大的提升。


React优点总结




  • 生态强大:现在没有哪个框架比React的生态体系好的,几乎所有开发需求都有成熟的解决方案。




  • 上手简单: 你甚至可以在几个小时内就可以上手React技术,但是他的知识很广,你可能需要更多的时间来完全驾驭它。




  • 社区强大:你可以很容易的找到志同道合的人一起学习,因为使用它的人真的是太多了。




React和Vue的对比


这是前端最火的两个框架,虽然说React是世界使用人数最多的框架,但是就在国内而言Vue的使用者很有可能超过React。两个框架都是非常优秀的,所以他们在技术和先进性上不相上下。


那个人而言在接到一个项目时,我是如何选择的那?React.js相对于Vue.js它的灵活性和协作性更好一点,所以我在处理复杂项目或公司核心项目时,React都是我的第一选择。而Vue.js有着丰富的API,实现起来更简单快速,所以当团队不大,沟通紧密时,我会选择Vue,因为它更快速更易用。(需要说明的是,其实Vue也完全胜任于大型项目,这要根据自己对框架的掌握程度来决定,以上只是站在我的知识程度基础上的个人总结)


我们将学到什么?


我们将学习所有 React 的基础概念,其中又分为三个部分:



  • 编写组件相关:包括 JSX 语法、Component、Props

  • 组件的交互:包括 State 和生命周期

  • 组件的渲染:包括列表和 Key、条件渲染

  • 和 DOM & HTML 相关:包括事件处理、表单。


前提条件


我们假设你熟系 HTML 和 JavaScript,但即使你是从其他编程语言转过来的,你也能看懂这篇教程。我们还假设你对一些编程语言的概念比较熟悉,比如函数、对象、数组,如果对类了解就更好了。


环境准备


首先准备 Node 开发环境,访问 Node 官方网站下载并安装。打开终端输入如下命令检测 Node 是否安装成功:


node -v # v10.16.0


npm -v # 6.9.0


注意


Windows 用户需要打开 cmd 工具,Mac 和 Linux 是终端。


如果上面的命令有输出且无报错,那么代表 Node 环境安装成功。接下来我们将使用 React 脚手架 -- Create React App(简称 CRA)来初始化项目,同时这也是官方推荐初始化 React 项目的最佳方式。


在终端中输入如下命令:



npx create-react-app my-todolist



等待命令运行完成,接着输入如下命令开启项目:



cd my-todolist && npm start



CRA 会自动开启项目并打开浏览器


🎉🎉🎉 恭喜你!成功创建了第一个 React 应用!


现在 CRA 初始化的项目里有很多无关的内容,为了开始接下来的学习,我们还需要做一点清理工作。首先在终端中按 ctrl + c 关闭刚刚运行的开发环境,然后在终端中依次输入如下的命令:


进入 src 目录

cd src


如果你在使用 Mac 或者 Linux:

rm -f *


或者,你在使用 Windows:

del *


然后,创建我们将学习用的 JS 文件

如果你在使用 Mac 或者 Linux:

touch index.js


或者,你在使用 Windows

type nul > index.js


最后,切回到项目目录文件夹下

cd ..
此时如果在终端项目目录下运行 npm start 会报错,因为我们的 index.js 还没有内容,我们在终端中使用 ctrl +c 关闭开发服务器,然后使用编辑器打开项目,在刚刚创建的 index.js 文件中加入如下代码:


import React from "react";
import ReactDOM from "react-dom";

class App extends React.Component {
render() {
return <div>Hello, World</div>;
}
}

ReactDOM.render(<App />, document.getElementById("root"));
复制代码

我们看到 index.js 里面的代码分为三个部分。


首先是一系列导包,我们导入了 react 包,并命名为 React,导入了 react-dom 包并命名为 ReactDOM。对于包含 React 组件(我们将在之后讲解)的文件都必须在文件开头导入 React。


然后我们定义了一个 React 组件,命名为 App,继承自 React.Component,组件的内容我们将会在后面进行讲解。


接着我们使用 ReactDOM 的 render 方法来渲染刚刚定义的 App 组件,render方法接收两个参数,第一个参数为我们的 React 根级组件,第二个参数接收一个 DOM 节点,代表我们将把和 React 应用挂载到这个 DOM 节点下,进而渲染到浏览器中。


注意


上面代码的三个部分中,第一部分和第三部分在整篇教程中是不会修改的,同时在编写任意 React 应用,这两个部分都是必须的。后面所有涉及到的代码修改都是关于第二部分代码的修改,或者是在第一部分到第三部分之间插入或删除代码。


JSX 语法


首先我们来看一下 React 引以为傲的特性之一 -- JSX。它允许我们在 JS 代码中使用 XML 语法来编写用户界面,使得我们可以充分的利用 JS 的强大特性来操作用户界面。


一个 React 组件的 render 方法中 return 的内容就为这个组件所将渲染的内容。比如我们现在的代码:


render() {
return <div>Hello, World</div>;
}


这里的 <div>Hello, World</div> 是一段 JSX 代码,它最终会被 Babel转译成下面这段 JS 代码:


React.createElement(
'div',
null,
'Hello, World'
)


React.createElement() 接收三个参数:



  • 第一个参数代表 JSX 元素标签。

  • 第二个参数代表这个 JSX 元素接收的属性,它是一个对象,这里因为我们的 div 没有接收任何属性,所以它是 null。

  • 第三个参数代表 JSX 元素包裹的内容。


React.createElement() 会对参数做一些检查确保你写的代码不会产生 BUG,它最终会创建一个类似下面的对象:


{
type: 'div',
props: {
children: 'Hello, World'
}
};


这些对象被称之为 “React Element”。你可以认为它们描述了你想要在屏幕上看到的内容。React 将会接收这些对象,使用它们来构建 DOM,并且对它们进行更新。


App 组件最终返回这段 JSX 代码,所以我们使用 ReactDOM 的 render 方法渲染 App 组件,最终显示在屏幕上的就是 Hello, World" 内容。


JSX 作为变量使用


因为 JSX 最终会被编译成一个 JS 对象,所以我们可以把它当做一个 JS 对象使用,它享有和一个 JS 对象同等的地位,比如可以将其赋值给一个变量,我们修改上面代码中的 render 方法如下:


render() {
const element = <div>Hello, World</div>;
return element;
}


保存代码,我们发现浏览器中渲染的内容和我们之前类似。


在 JSX 中使用变量


我们可以使用大括号 {} 在 JSX 中动态的插入变量值,比如我们修改 render 方法如下:


render() {
const content = "World";
const element = <div>Hello, {content}</div>;
return element;
}


JSX 中使用 JSX


我们可以在 JSX 中再包含 JSX,这样我们编写任意层次的 HTML 结构:


render() {
const element = <li>Hello, World</li>
return (
<div>
<ul>
{element}
</ul>
</div>
)
}


JSX 中添加节点属性
我们可以像在 HTML 中一样,给元素标签加上属性,只不过我们需要遵守驼峰式命名法则,比如在 HTML 上的属性 data-index 在 JSX 节点上要写成 dataIndex。


const element = <div dataIndex="0">Hello, World</div>;


注意


在 JSX 中所有的属性都要更换成驼峰式命名,比如 onclick 要改成 onClick,唯一比较特殊的就是 class,因为在 JS 中 class 是保留字,我们要把 class 改成 className 。


const element = <div className="app">Hello, World</div>;


实战


在编辑器中打开 src/index.js ,对 App 组件做如下改变:


class App extends React.Component {
render() {
const todoList = ["给npy的前端秘籍", "fyj", "天天的小迷弟", "仰望毛毛大佬"];
return (
<ul>
<li>Hello, {todoList[0]}</li>
<li>Hello, {todoList[1]}</li>
<li>Hello, {todoList[2]}</li>
<li>Hello, {todoList[3]}</li>
</ul>
);
}
}


可以看到,我们使用 const 定义了一个 todoList 数组常量,并且在 JSX 中使用 {} 进行动态插值,插入了数组的四个元素。


提示


无需关闭刚才使用 npm start 开启的开发服务器,修改代码后,浏览器中的内容将会自动刷新!


你可能注意到了我们手动获取了数组的四个值,然后逐一的用 {} 语法插入到 JSX 中并最终渲染,这样做还比较原始,我们将在后面列表和 Key小节中简化这种写法。


在这一小节中,我们了解了 JSX 的概念,并且实践了相关的知识。我们还提出了组件的概念,但是并没有深入讲解它,在下一小节中我们将详细地讲解组件的知识。


总结


专栏第一篇与大家一起学习了React基本知识、后续还会有更精彩的哇、一起加油哇~



作者:给npy的前端秘籍
链接:https://juejin.cn/post/6974651532637634568

收起阅读 »

React 毁了 Web 开发(转载)

本文并不是为了引发争论或者让大家非要争一个好坏。我仔细阅读该文章之后,发现里面提出的问题是我们常常忽视并且对于构建可持续项目发展的非常重要的问题。 不论是小兵还是leader都应该持续的去注重这些东西。 以下是正文翻译:原文来自:medium.com/buil...
继续阅读 »

本文并不是为了引发争论或者让大家非要争一个好坏。我仔细阅读该文章之后,发现里面提出的问题是我们常常忽视并且对于构建可持续项目发展的非常重要的问题。


不论是小兵还是leader都应该持续的去注重这些东西。


以下是正文翻译:原文来自:medium.com/building-pr…


以下为译文:


React 是一个很好的库,对于 Web 开发很重要,因为它引入了声明式与反应式模板,这在当时是每个人都需要的范式转变。当时(也就是 6~7 年前),我们面临着需要的范式转变的问题,而 React 很好地解决了这个问题。


另外提一句,在 React 之前,Ember 也解决了同样的问题。然而,它的性能并不那么好,而且该框架规定了太多东西,远不如 React。



然而,React 在开始流行之后,发展变得一团糟。React 社区中开启了一种新趋势,一切都围绕着炒作、新奇和创造新范式的转变。每隔几个月就会涌现一些新的库,为我们应该如何编写 React Web 应用程序设定新标准,同时还会解决大部分已经解决的问题。


下面,我们以 " 状态管理 " 为例来说明。由于 React 缺少传统的依赖注入系统(DI 是通过组件组合实现的),所以社区不得不自己解决这个问题。然而,后来就变成了一遍又一遍地解决这个问题,每年都会带来一套新的标准。



React 只是一个渲染引擎,在常见的 Web 应用程序中,你需要使用很多库来构建项目的框架,例如数据层、状态管理、路由、资产捆绑器等。


React 背后的生态系统给了你太多这样的选择,而这个技术栈也因此而变得支离破碎,并引发了著名的 "Javascript 疲劳 "。


此外,还涌现了一种趋势:" 框架比较热潮 "。各个 JS 框架之间经常会展开渲染速度以及内存占用等属性的比较。其实,这些因素在大多数情况下根本无关紧要,因为应用的速度缓慢并不是由于 JS 框架的速度过慢而引起的,而是因为糟糕的代码。


然而,就像世界上所有的趋势一样,这个趋势有点过,甚至危及了新一代的 Web 开发人员。我就在想,为什么一个库能成为 Web 开发人员简历中最耀眼的技术?更糟糕的是,它甚至算不上一个库,只不过是库中的一个模块。人们常常将 React hook 视为一项 " 技术 ",甚至可以与代码重构或代码审查等实际技术相提并论。


认真地说,我们什么时候才能停止吹捧这种技术?


比如说,你为什么不告诉我,你知道:


如何编写简单易读的代码


不要向我炫耀你掌握了某个 GitHub 上获得星星数最多的库;而是给我展示一两个优秀的代码片段。


如何管理状态


不要讨论某个流行的状态管理库,而是告诉我为什么 " 数据应该下降而动作应该上升 "。或者说,为什么应该在创建的地方修改状态,而不是组件层次结构中更深的地方。


如何测试代码


不要告诉我你知道 Jest 或 QUnit,而是解释一下为什么很难自动化端到端的测试,以及为什么最低程度的渲染测试只需付出 10% 的努力,却能带来 90% 的好处。


如何发布代码


不要告诉我你使用 CI/CD(因为如今每个项目里的成员都不止一个人),而是解释为什么部署和发布应该分离,这样新功能就不会影响到已有功能,而且还可以远程启动新功能。


如何编写可审查的代码


不要说你是一名 " 团队成员 ",而是告诉我代码审查对审查者来说同样困难,而且你知道如何优化 PR 才能提高可读性和清晰度。


如何建立稳固的项目标准


除非团队中只有你一个人,否则你就必须遵守项目中的标准和惯例。你应该告诉我命名很难,而且变量的范围越广,投入到命名中的时间就应该越多。


如何审核别人的代码


因为代码审查可确保产品质量、减少 bug 和技术债务、共同建立团队知识等等,但前提是将代码审核贯彻到底。代码审查不应该只是自上而下的活动。对于经验不足的团队成员来说,这是一个很好的学习机制。


如何在 JS 框架中找到自己的方式


这与 GitHub 上的星星数量无关,你应该学习如今大多数 JS 框架都拥有的共同原则。了解其他框架的优缺点可以让你更好地了解自己选择的框架。


如何建立最小化可行产品


技术只是制造产品的工具,而不是流程。与其将时间浪费在技术争论上,还不如花点时间优化流程。


如何优化:不要太早,也不要太晚


因为在大多数情况下根本不需要优化。


如何结对编程


因为结对编程与代码审查一样,这是最重要的共享知识和建立团队凝聚力的实践。而且也很有意思!


如何持续重构


因为每个项目都有技术债务,你应该停止抱怨,并开始重构。每次开发新功能之前都应该进行小型代码重构。大规模的重构和重写永远不会有好结果。


以上就是我认为 React 毁了 Web 开发的原因。很多人对这一说法很感兴趣,并热切地加入了辩论。


链接:https://juejin.cn/post/6977944684437962788

收起阅读 »

FIL升级对矿工有哪些利好?现在是参与挖FIL币好时机

据财经媒体报道:FIL将在6月底进行系统升级,FIL升级对矿工有哪些利好?那对与刚了解FIL挖矿的朋友们现在正是参与的好时机+slf81818,为什么呢?一起来了解下:Filecoin’s v13 HyperDrive网络升级的重要意义在于:一:人类宝贵信息的...
继续阅读 »

据财经媒体报道:FIL将在6月底进行系统升级,FIL升级对矿工有哪些利好?那对与刚了解FIL挖矿的朋友们现在正是参与的好时机+slf81818,为什么呢?一起来了解下:

Filecoin’s v13 HyperDrive网络升级的重要意义在于:

一:人类宝贵信息的可验证存储容量将增长10—25倍。

二:GAS费大幅降低,无限期限接近于0。

三:质押费快速下降,新进场的投入成本明显降低,更有利于生态发展。

四:参与存储商激增,将实现更快的网络增长。

五:三大运营商之一(移动电信联通)智慧城市的数据将接入储存IPFS系统,进一步促进IPFS的应用落地,或将成为IPFS历史级重大标志性事件。

Filecoin V13版本的更新将是颠覆性的,也是突破共识的一次更新。其目的也不仅仅是降低Gas费,而是释放带宽,为Filecoin添加智能合约功能做准备。

目前FIL市场的具体情况具体分析,大多数人都在观望,主要还是带着想要一夜暴富的想法去炒币。这完全是两个概念,炒币没有哪个不伤筋动骨,这还是要轻的,可以考虑一下它的恐怖程度!为什么说矿工总是食物链的顶端?合理的投资方式是看其长期收益,不需要过多地去看目前的价格高低,手中的矿机每天都能产出 FIL,不用管它涨跌,相反,炒币就是你买了多少就有多少,性质不同。

例如,买一只鸡来给你每天生蛋,头七天价格比以前低,把鸡蛋存起来不卖,第八天它的价格达到了你想要的市场价就全部卖掉,与此相反,你直接买鸡蛋来倒买倒卖,风险成本是显而易见的。炒币看运气,屯币看心态,矿机相当于永动机。













现在币价低,加入挖矿成本也会很低,最重要的是其日产币并未降低,反而还在增加产量,这也大大缩短了回本周期,未来币价上涨,矿机也将随之涨价,人多挖矿效率肯定不如现在人少,回本周期更是大幅拉长,挖矿最大优势在于自身持币增多,有“粮”就能度过寒冬,躲过熊市就是迎来资产爆发的喜悦。感谢大家关注芳姐+slf81818,了解更多币圈最新资讯。

收起阅读 »

纯 CSS 创建五彩斑斓的智慧阴影!让前景图片自动转化为对应彩色的背景阴影

几天前,我在 Home Depot(aka Toys "R" Us for big kids)处发现,他们有一个巨大的显示器来展示所有这些彩色的供销售的电灯泡!其中一项是y一组在电视后面的智能灯泡。它们会在电视的后面投影近似于电视在播出的内容的彩色阴影,与以下...
继续阅读 »

几天前,我在 Home Depot(aka Toys "R" Us for big kids)处发现,他们有一个巨大的显示器来展示所有这些彩色的供销售的电灯泡!其中一项是y一组在电视后面的智能灯泡。它们会在电视的后面投影近似于电视在播出的内容的彩色阴影,与以下内容 类似



注意电视后面发生的事情。屏幕前景中显示的颜色会被灯泡投影为电视机身后面的彩色阴影。随着屏幕上的颜色发生变化,投射在背景中的颜色也会发生变化。真的很酷,对吧?


自然,看到这个之后,我的第一个想法是,我们是否可以使用网络技术创建一个足够智能以模仿前景色的彩色阴影。事实证明,我们完全可以只使用 CSS 构建出这个案例。在本文中,我们将了解如何创建这种效果。


走起!


让它变成真的!


正如您将在以下部分中看到的,使用 CSS 创建这种彩色阴影似乎是一项艰巨的任务(当然,只是就刚开始而言)。当我们开始进入它并将这个任务的核心分解成更小的部分时,我们其实能够发现这真的很容易实现。在接下来的几节中,我们将创建以下示例:



你应该看到的是一张寿司的图片,后面出现了一个五颜六色的阴影。(只是为了强调我们正在做这一切,阴影被添加了脉冲的效果)抛开示例,让我们深入了解实现,看看 HTML 和 CSS 如何让这一切变为现实!


展示我们的照片


展示我们的寿司的图片对应的 HTML 起始没什么特别的:



<div class="parent">
<div class="colorfulShadow sushi"></div>
</div>


我们有一个父 div 元素,包含一个负责显示寿司的子 div 元素。我们显示寿司的方式是将其指定为背景图像,并由以下 .sushi 样式规则处理:


.sushi {
margin: 100px;
width: 150px;
height: 150px;
background-image: url("https://www.kirupa.com/icon/1f363.svg");
background-repeat: no-repeat;
background-size: contain;
}


在此样式规则中,我们将 div 的大小指定为 150 x 150 像素,并在其上设置 background-image 和相关的其他属性。就目前而言,我们所看到的 HTML 和 CSS 会给我们提供如下所示的内容:



现在是阴影时间


现在我们的图像出现了,剩下的就是我们定义阴影这一有趣的部分。我们要定义阴影的方法是指定一个子伪元素(使用 ::after),它将做三件事:



  1. 直接定位在我们的形象后面;

  2. 继承与父元素相同的背景图片;

  3. 依靠滤镜应用多彩的阴影效果;


这三件事是通过以下两条样式规则完成的:


.colorfulShadow {
position: relative;
}

.colorfulShadow::after {
content: "";
width: 100%;
height: 100%;
position: absolute;
background: inherit;
background-position: center center;
filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.50)) blur(20px);
z-index: -1;
}


让我们花一点时间来看看这里发生了些什么:先注意每一个属性和对应的值,有一些值得注意的标记是 backgroundfilterbackground 属性使用了 inherit 继承父元素,意味着能够继承父元素的背景:


.colorfulShadow::after {
content: "";
width: 100%;
height: 100%;
position: absolute;
background: inherit;
background-position: center center;
filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.50)) blur(20px);
z-index: -1;
}


我们为 filter 属性定义了两个过滤的属性,分别是 drop-shadowblur


.colorfulShadow::after {
content: "";
width: 100%;
height: 100%;
position: absolute;
background: inherit;
background-position: center center;
filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.50)) blur(20px);
z-index: -1;
}


我们的 drop-shadow 过滤器设置为显示不透明度为 50% 的黑色阴影,而我们的 blur 过滤器会将我们的伪元素模糊 20px。 这两个过滤器的组合最终创建了彩色的阴影,当应用这两个样式规则时,该阴影现在将出现在我们的寿司图像后面:



在这一点上,我们已经完成了。为完整起见,如果我们想要彩色阴影缩放的动画,如下 CSS 代码的添加能够助力我们实现目标:


.colorfulShadow {
position: relative;
}

.colorfulShadow::after {
content: "";
width: 100%;
height: 100%;
position: absolute;
background: inherit;
background-position: center center;
filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.50)) blur(20px);
z-index: -1;

/* animation time! */
animation: oscillate 1s cubic-bezier(.17, .67, .45, 1.32) infinite alternate;
}

@keyframes oscillate {
from {
transform: scale(1, 1);
}

to {
transform: scale(1.3, 1.3);
}
}


如果您想要一些交互性而没有不断循环的动画,您还可以使用 CSS 过渡来更改阴影在某些动作(如悬停)上的行为方式。困难的部分是像对待在 HTML 中明确定义或使用 JavaScript 动态创建的任何其他元素一样对待伪元素。唯一的区别是这个元素是完全使用 CSS 创建的!


结语


小结


伪元素允许我们使用 CSS 来完成一些历史上属于 HTML 和 JavaScript 领域的元素创建任务。对于我们多彩而智能的阴影,我们能够依靠父元素来设置背景图像。这使我们能够轻松定义一个既继承了父元素的背景图像细节,又允许我们为其设置一系列属性以实现模糊和阴影效果的子伪元素。虽然所有这些都很好,并且我们最大限度地减少了大量复制和粘贴,但这种方法不是很灵活。


如果我想将这样的阴影应用到一个不只是带有背景图像的空元素上怎么办?如果我有一个像 ButtonComboBox 这样的 HTML 元素想要应用这种阴影效果怎么办?一种解决方案是依靠 JavaScript 在 DOM 中复制适当的元素,将它们放置在前景元素下方,应用过滤器,然后就可以了。虽然这有效,但考虑到该过程的复杂程度,实在是有些不寒而栗。太糟糕了,JavaScript 没有等效的 renderTargetBitmap 这种能够把我们的视觉效果渲染成位图,然后你可以做任何你想做的事的 API…… 🥶


以上内容为译文翻译,下面为一些拓展:




拓展


说实在的,我们其实并不需要那么多复杂的内容,图片可以是任意的,比如说 PNG、SVG,最终精简后,HTML 代码仅仅为任意一个元素,附上 style 规定图片地址与大小:


<div class="shadowedImage" style="--data-width: 164px; --data-height: 48px; --data-image: url('https://sf3-scmcdn2-tos.pstatp.com/xitu_juejin_web/dcec27cc6ece0eb5bb217e62e6bec104.svg');"></div>


CSS 代码如下:


.shadowedImage {
position: relative;
margin: 100px;
width: var(--data-width);
height: var(--data-height);
max-height: 150px;
background-image: var(--data-image);
background-repeat: no-repeat;
background-size: contain;
}

.shadowedImage::after {
content: "";
width: 100%;
height: 100%;
position: absolute;
background: inherit;
background-position: center center;
filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.50)) blur(20px);
z-index: -1;
}


示例代码


一段示例代码如下:


<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>

<style>
.shadowedImage {
position: relative;
}

.shadowedImage::after {
content: "";
width: 100%;
height: 100%;
position: absolute;
background: inherit;
background-position: center center;
filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.50)) blur(20px);
z-index: -1;

/* animation time! */
animation: oscillate 1s cubic-bezier(.17, .67, .45, 1.32) infinite alternate;
}

@keyframes oscillate {
from {
transform: scale(1, 1);
}

to {
transform: scale(1.1, 1.1);
}
}

.shadowedImage {
margin: 100px;
width: var(--data-width);
height: var(--data-height);
max-height: 150px;
background-image: var(--data-image);
background-repeat: no-repeat;
background-size: contain;
}
</style>
</head>
<body>
<div class="parent">
<div class="shadowedImage" style="--data-width: 164px; --data-height: 48px; --data-image: url('https://sf3-scmcdn2-tos.pstatp.com/xitu_juejin_web/dcec27cc6ece0eb5bb217e62e6bec104.svg');"></div>
<div class="shadowedImage" style="--data-width: 164px; --data-height: 164px; --data-image: url('https://sf1-dycdn-tos.pstatp.com/img/bytedance-cn/4ac74bbefc4455d0b350fff1fcd530c7~noop.image');"></div>
<div class="shadowedImage" style="--data-width: 164px; --data-height: 164px; --data-image: url('https://sf1-ttcdn-tos.pstatp.com/img/bytedance-cn/4bcac7e2843bd01c3158dcaefda77ada~noop.image');"></div>
</div>
</body>
</html>


示例效果


效果如下:


image.png



如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。





链接:https://juejin.cn/post/6975818153376874503

收起阅读 »

小程序自定义TabBar 如何实现“keep-alive”

自定义TabBar方案 虽然在之前文章提到过了,本次采用组件化实现,具体实现如下: 我们可以新建一个home文件夹,在home/index.wxml中写一个tabBar,然后把TabBar页面写成组件,然后点击TabBar切换相应的组件展示就可以。代码...
继续阅读 »

自定义TabBar方案



虽然在之前文章提到过了,本次采用组件化实现,具体实现如下:





  • 我们可以新建一个home文件夹,在home/index.wxml中写一个tabBar,然后把TabBar页面写成组件,然后点击TabBar切换相应的组件展示就可以。代码如下:




  • wxml部分




<!-- home页面 -->

<view id='index'>
<!-- 自定义头部 -->
<head name='{{name}}' bgshow="{{bgshow}}" backShow='false'></head>

<!-- 首页 -->
<index change='{{activeIndex==0}}'></index>
<!-- 购物车 -->
<cart change='{{activeIndex==1}}'></cart>
<!-- 订单 -->
<order change='{{activeIndex==2}}'></order>
<!-- 我的 -->
<my change='{{activeIndex==2}}'></my>
<!-- tabbar -->
<view class="tab ios">
<view class="items {{activeIndex==index?'active':''}}" wx:for="{{tab}}" bindtap="choose" data-index='{{index}}' wx:key='index' wx:for-item="items">
<image wx:if="{{activeIndex==index}}" src="{{items.activeImage}}"></image>
<image wx:else src="{{items.image}}"></image>
<text>{{items.name}}</text>
</view>
</view>
</view>




  • home页面的ts


Page({
data: {
activeIndex:0,
tab:[
{
name:'商品',
image:'../../images/index.png',
activeImage:'../../images/index-hover.png',
},
{
name:'购物车',
image:'../../images/cart.png',
activeImage:'../../images/cart-hover.png',
},
{
name:'订单',
image:'../../images/order.png',
activeImage:'../../images/order-hover.png',
},
{
name:'我的',
image:'../../images/my.png',
activeImage:'../../images/my-hover.png',
}
]
},
// 切换事件
choose(e:any){
const _this=this;
const {activeIndex}=_this.data;
if(e.currentTarget.dataset.index==activeIndex){
return
}else{
_this.setData({
activeIndex:e.currentTarget.dataset.index
})
}
},
})




  • 上面代码不难理解,点击以后改变activeIndex从而控制每个组件的渲染和销毁,这样付出的代价还是比较大的,需要我们进一步的优化。


如何实现keep-alive



我们知道,这里主要是避免组件反复创建和渲染,有效提升系统性能。



实现思路




  • 1.在tab每个选项增加两个值:statusshowshow控制组件是否需要渲染,status控制组件display




  • 2.初始化时候设置首页的statusshow,其他都为false




  • 3.当我们切换时:把上一个tab页面的status改为false,然后把当前要切换页面的tab数据中的statusshow都改为true,最后再更新一下activeIndex的值。




  • wxml代码:




    <!-- 首页 -->
<view wx:if="{{tab[0].show}}" hidden="{{!tab[0].status}}">
<index></index>
</view>
<!-- 购物车 -->
<view wx:if="{{tab[1].show}}" hidden="{{!tab[1].status}}">
<cart></cart>
</view>
<!-- 订单 -->
<view wx:if="{{tab[2].show}}" hidden="{{!tab[2].status}}">
<order></order>
</view>
<!-- 我的 -->
<view wx:if="{{tab[3].show}}" hidden="{{!tab[3].status}}">
<my></my>
</view>



  • ts代码


Page({
data: {
activeIndex:0, //当前选中的index
tab:[
{
name:'商品',
image:'../../images/index.png',
activeImage:'../../images/index-hover.png',
status:true,//控制组件的display
show:true, //控制组件是否被渲染
},
{
name:'购物车',
image:'../../images/cart.png',
activeImage:'../../images/cart-hover.png',
status:false,
show:false,
},
{
name:'订单',
image:'../../images/order.png',
activeImage:'../../images/order-hover.png',
status:false,
show:false,
},
{
name:'我的',
image:'../../images/my.png',
activeImage:'../../images/my-hover.png',
status:false,
show:false,
}
]
},

choose(e:any){
const _this=this;
const {activeIndex}=_this.data;
//如果点击的选项是当前选中,就不执行
if(e.currentTarget.dataset.index==activeIndex){
return
}else{
//修改上一个tab页面的status
let prev='tab['+activeIndex+'].status',
//修改当前选中元素的status
status='tab['+e.currentTarget.dataset.index+'].status',
//修改当前选中元素的show
show='tab['+e.currentTarget.dataset.index+'].show';

_this.setData({
[prev]:false,
[status]:true,
[show]:true,
activeIndex:e.currentTarget.dataset.index,//更新activeIndex
})
}
},

})




  • 这样基本就大功告成了,来看一下效果:


Rp63gH.gif



  • 当我们点击切换时候,如果当前组件没有渲染就会进行渲染,如果渲染过后进行切换只是改变display,完美实现了需求,大功告成!


实际业务场景分析



在实际使用中还有两种种情况:



情况1:比如某些数据并不希望他首次加载后就数据保持不变,当切换页面时候希望数据进行更新,比如笔者做的电商小程序,在首页点击商品加入购物车,然后切换到购物车,每次切换时候肯定需要再次进行请求。

情况2:像个人中心这种页面,数据基本请求一次就可以,没必要每次切换请求数据,这种我们不需要进行改进。




  • 我们给组件传递一个值:status,然后在组件中监听这个值的变化,当值为true时候,去请求接口更新数据。具体代码如下:




  • wxml代码(只列举关键部分):




<!-- 首页 -->
<view wx:if="{{tab[0].show}}" hidden="{{!tab[0].status}}">
<index change='{{tab[0].status}}'></index>
</view>

<!-- 购物车 -->
<view wx:if="{{tab[1].show}}" hidden="{{!tab[1].status}}">
<cart change='{{tab[0].status}}'></cart>
</view>



  • 首页组件/购物车组件ts代码:


Component({
/**
* 组件的属性列表
*/
properties: {
change: {
type: String,//类型
value: ''//默认值
},
},
observers: {
//监听数据改变进行某种操作
'change': function(change) {
if(change=='true'){
console.log('更新首页数据'+change)
}
}
},
})



  • 来看一下最终效果:


Rp618e.gif



链接:https://juejin.cn/post/6976072584127315999

收起阅读 »

当前端基建任务落到你身上,该如何推动协作?

前言 作为一名野生的前端开发,自打本猿入行起,就未经过什么系统的学习,待过的团队也是大大小小没个准儿: 要么大牛带队,但是后端大牛。要么临时凑的团队,受制于从前,前端不自由。要么从0到项目部署,都是为了敏捷而敏捷,颇不规范。 话虽如此,经过4年生涯摧残的废猿...
继续阅读 »

前言


作为一名野生的前端开发,自打本猿入行起,就未经过什么系统的学习,待过的团队也是大大小小没个准儿:


要么大牛带队,但是后端大牛。
要么临时凑的团队,受制于从前,前端不自由。
要么从0到项目部署,都是为了敏捷而敏捷,颇不规范。


话虽如此,经过4年生涯摧残的废猿我,也是有自己的一番心得体会的。


1. 从DevOps流程看前端基建



很多专注于切图的萌新前端看到这张图是蒙圈的:


DevOps是什么?这些工具都是啥?我在哪?


很多前端在接触到什么前端工程化,什么持续构建/集成相关知识时就犯怂。也有觉得这与业务开发无关,不必理会。


但是往长远想,切图是不可能一辈子切图的,你业务再怎么厉害,前端代码再如何牛,没有了后端运维测试大佬们相助,一个完整的软件生产周期就没法走完。


成为一名全栈很难,更别说全链路开发者了。


言归正传,当你进入一个新团队,前端从0开始,怎样从DevOps的角度去提高团队效能呢?



一套简易的DevOps流程包含了协作、构建、测试、部署、运行。


而前端常说的开发规范、代码管理、测试、构建部署以及工程化其实都是在这一整个体系中。


当然,中小团队想玩好DevOps整套流程,需要的时间与研发成本,不比开发项目少。


DevOps核心思想就是:“快速交付价值,灵活响应变化”。其基本原则如下:


高效的协作和沟通;
自动化流程和工具;
快速敏捷的开发;
持续交付和部署;
不断学习和创新。


接下来我将从协作、构建、测试、部署、运行五个方面谈谈,如何快速打造用于中小团队的前端基建。


2. 在团队内/外促进协作


前端基建协作方面可以写的东西太多了,暂且粗略分为:团队内 与 团队外。



以下可能是前端们都能遇到的问题:


成员间水平各异,编写代码的风格各不相同,项目间难以统一管理。
不同项目Webpack配置差异过大,基础工具函数库和请求封装不一样。
项目结构与技术栈上下横跳,明明是同一UI风格,基础组件没法复用,全靠复制粘贴。
代码没注释,项目没文档,新人难以接手,旧项目无法维护。


三层代码规范约束



  • 第一层,ESLint


常见的ESLint风格有:airbnb,google,standard


在多个项目间,规则不应左右横跳,如果项目周期紧张,可以适当放宽规则,让warning类弱警告可以通过。且一般建议成员的IDE和插件要统一,将客观因素影响降到最低。



  • 第二层,Git Hooks


git 自身包含许多 hooks,在 commitpushgit 事件前后触发执行。


husky能够防止不规范代码被commitpushmerge等等。


代码提交不规范,全组部署两行泪。


npm install husky pre-commit  --save-dev


拿我以前的项目为例子:


// package.json
"scripts": {
// ...
"lint": "node_modules/.bin/eslint '**/*.{js,jsx}' && node_modules/.bin/stylelint '**/*.{css,scss}'",
"lint:fix": "node_modules/.bin/eslint '**/*.{js,jsx}' --fix && node_modules/.bin/stylelint '**/*.{css,scss}' --fix"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},


通过简单的安装配置,无论你通过命令行还是Sourcetree提交代码,都需要通过严格的校验。



建议在根目录README.md注明提交规范:


## Git 规范

使用 [commitlint](https://github.com/conventional-changelog/commitlint
) 工具,常用有以下几种类型:

-
feat :新功能
- fix :修复 bug
- chore :对构建或者辅助工具的更改
- refactor :既不是修复 bug 也不是添加新功能的代码更改
- style :不影响代码含义的更改 (例如空格、格式化、少了分号)
- docs : 只是文档的更改
- perf :提高性能的代码更改
- revert :撤回提交
- test :添加或修正测试

举例
git commit -m 'feat: add list'



  • 第三层,CI(持续集成)。



《前端代码规范最佳实践》



前两步的校验可以手动跳过(找骂),但CI中的校验是绝对绕不过的,因为它在服务端校验。使用 gitlab CI 做持续集成,配置文件 .gitlab-ci.yaml 如下所示:


lint:
stage:lint
only:
-/^feature\/.*$/
script:
-npmlint


这层校验,一般在稍大点的企业中,会由运维部的配置组完成。



统一前端物料


公共组件、公共UI、工具函数库、第三方sdk等该如何规范?


如何快速封装部门UI组件库?

  • 将业务从公共组件中抽离出来。

  • 在项目中安装StoryBook(多项目时另起)

  • 按官方文档标准,创建stories,并设定参数(同时也建议先写Jest测试脚本),写上必要的注释。

  • 为不同组件配置StoryBook控件,最后部署。

如何统一部门所用的工具函数库和第三方sdk


其实这里更多的是沟通的问题,首先需要明确的几点:



  • 部门内对约定俗成的工具库要有提前沟通,不能这头装一个MomentJs,另一头又装了DayJS。一般的原则是:轻量的自己写,超过可接受大小的找替代,譬如:DayJS替代MomentJsImmerJS替代immutableJS等。

  • 部门间的有登录机制,请求库封装协议等。如果是SSO/扫码登录等,就协定只用一套,不允许后端随意变动。如果是请求库封装,就必须要后端统一Restful风格,相信我,不用Restful规范的团队都是灾难。前端联调会生不如死。

  • Mock方式、路由管理以及样式写法也应当统一。


在团队外促进协作


核心原则就是:“能用文档解决的就尽量别BB。”


虽说现今前端的地位愈发重要,但我们经常在项目开发中遇到以下问题:


不同的后端接口规范不一样,前端需要耗费大量时间去做数据清洗兼容。
前端静态页开发完了,后端迟迟不给接口,因为没有接口文档,天天都得问。
测试反馈的问题,在原型上没有体现。


首先是原型方面:

  • 一定要看明白产品给的原型文档!!!多问多沟通,这太重要了。

  • 好的产品一般都会提供项目流程详图,但前端还是需要基于实际,做一张页面流程图。

  • 要产品提供具体字段类型相关定义,不然得和后端扯皮。。。

其次是后端:

执行Restful接口规范,不符合规范的接口驳回。

劝退师就经历过,前东家有个JAVA架构师,连跨域和Restful都不知道,定的规范不成规范,一个简单查询接口返回五六级,其美名曰:“结构化数据”

遇到这种沉浸于自己世界不听劝的后端,我只有一句劝:要么把他搞走,要么跑路吧

必要的接口文档站点与API测试(如SwaggerApidoc),不接受文件传输形式的接口

早期的联调都是通过呐喊告知对方接口的标准。刚开始有什么不清楚的直接问就好了,但是到了后面的时候连写接口代码的那个人都忘了这接口怎么用,维护成本巨高

在没有接口文档站点出现前,接口文档以word文档出现,辅以postmanhttpcurl等工具去测试。但仍然不够直观,维护起来也难

以web交互为主的Swagger解决了测试,维护以及实时性的问题。从一定程度上也避免了扯皮问题:只有你后端没更新文档,这联调滞后时间就不该由前端担起。

最后是运维方面:

除了CI/CD相关的,其实很可以和运维一起写写nginx和插件开发。

效率沟通工具


可能大家比较习惯的是使用QQ或者微信去传输文件,日常沟通还行,就是对开发者不太友好。


如何是跨国家沟通,一般都是建议jira+slack的组合,但这两个工具稍微有些水土不服。


这四个工具随意选择都不会有太大问题。


链接:https://juejin.cn/post/6844904145602740231

收起阅读 »

手把手带你入门Webpack Plugin

关于 Webpack 在讲 Plugin 之前,我们先来了解下 Webpack。本质上,Webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具。它能够解析我们的代码,生成对应的依赖关系,然后将不同的模块达成一个或多个 bundle。 ...
继续阅读 »

关于 Webpack


在讲 Plugin 之前,我们先来了解下 Webpack。本质上,Webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具。它能够解析我们的代码,生成对应的依赖关系,然后将不同的模块达成一个或多个 bundle。



Webpack 的基本概念包括了如下内容:



  1. Entry:Webpack 的入口文件,指的是应该从哪个模块作为入口,来构建内部依赖图。

  2. Output:告诉 Webpack 在哪输出它所创建的 bundle 文件,以及输出的 bundle 文件该如何命名、输出到哪个路径下等规则。

  3. Loader:模块代码转化器,使得 Webpack 有能力去处理除了 JS、JSON 以外的其他类型的文件。

  4. Plugin:Plugin 提供执行更广的任务的功能,包括:打包优化,资源管理,注入环境变量等。

  5. Mode:根据不同运行环境执行不同优化参数时的必要参数。

  6. Browser Compatibility:支持所有 ES5 标准的浏览器(IE8 以上)。


了解完 Webpack 的基本概念之后,我们再来看下,为什么我们会需要 Plugin。


Plugin 的作用


我先举一个我们政采云内部的案例:


在 React 项目中,一般我们的 Router 文件是写在一个项目中的,如果项目中包含了许多页面,不免会出现所有业务模块 Router 耦合的情况,所以我们开发了一个 Plugin,在构建打包时,该 Plugin 会读取所有文件夹下的 index.js 文件,再合并到一起形成一个统一的 Router 文件,轻松解决业务耦合问题。这就是 Plugin 的应用(具体实现会在最后一小节说明)。


来看一下我们合成前项目代码结构:


├── package.json
├── README.md
├── zoo.config.js
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .stylelintrc
├── buildWebpack 配置目录)
│ └── webpack.dev.conf.js
├── src
│ ├── index.hbs
│ ├── main.js (入口文件)
│ ├── common (通用模块,包权限,统一报错拦截等)
│ └── ...
│ ├── components (项目公共组件)
│ └── ...
│ ├── layouts (项目顶通)
│ └── ...
│ ├── utils (公共类)
│ └── ...
│ ├── routes (页面路由)
│ │ ├── Hello (对应 Hello 页面的代码)
│ │ │ ├── config (页面配置信息)
│ │ │ └── ...
│ │ │ ├── modelsdva数据中心)
│ │ │ └── ...
│ │ │ ├── services (请求相关接口定义)
│ │ │ └── ...
│ │ │ ├── views (请求相关接口定义)
│ │ │ └── ...
│ │ │ └── index.jsrouter定义的路由信息)
├── .eslintignore
├── .eslintrc
├── .gitignore
└── .stylelintrc


再看一下经过 Plugin 合成 Router 之后的结构:


├── package.json
├── README.md
├── zoo.config.js
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .stylelintrc
├── buildWebpack 配置目录)
│ └── webpack.dev.conf.js
├── src
│ ├── index.hbs
│ ├── main.js (入口文件)
│ ├── router-config.js (合成后的router文件)
│ ├── common (通用模块,包权限,统一报错拦截等)
│ └── ...
│ ├── components (项目公共组件)
│ └── ...
│ ├── layouts (项目顶通)
│ └── ...
│ ├── utils (公共类)
│ └── ...
│ ├── routes (页面路由)
│ │ ├── Hello (对应 Hello 页面的代码)
│ │ │ ├── config (页面配置信息)
│ │ │ └── ...
│ │ │ ├── modelsdva数据中心)
│ │ │ └── ...
│ │ │ ├── services (请求相关接口定义)
│ │ │ └── ...
│ │ │ ├── views (请求相关接口定义)
│ │ │ └── ...
├── .eslintignore
├── .eslintrc
├── .gitignore
└── .stylelintrc


总结来说 Plugin 的作用如下:



  1. 提供了 Loader 无法解决的一些其他事情

  2. 提供强大的扩展方法,能执行更广的任务


了解完 Plugin 的大致作用之后,我们来聊一聊如何创建一个 Plugin。


创建一个 Plugin


Hook


在聊创建 Plugin 之前,我们先来聊一下什么是 Hook。


Webpack 在编译的过程中会触发一系列流程,而在这样一连串的流程中,Webpack 把一些关键的流程节点暴露出来供开发者使用,这就是 Hook,可以类比 React 的生命周期钩子。


Plugin 就是在这些 Hook 上暴露出方法供开发者做一些额外操作,在写 Plugin 的时候,也需要先了解我们应该在哪个 Hook 上做操作。


如何创建 Plugin


我们先来看一下 Webpack 官方给的案例:


const pluginName = 'ConsoleLogOnBuildWebpackPlugin';

class ConsoleLogOnBuildWebpackPlugin {
apply(compiler) {
// 代表开始读取 records 之前执行
compiler.hooks.run.tap(pluginName, compilation => {
console.log("webpack 构建过程开始!");
});
}
}


从上面的代码我们可以总结如下内容:



  • Plugin 其实就是一个类。

  • 类需要一个 apply 方法,执行具体的插件方法。

  • 插件方法做了一件事情就是在 run 这个 Hook 上注册了一个同步的打印日志的方法。

  • apply 方法的入参注入了一个 compiler 实例,compiler 实例是 Webpack 的支柱引擎,代表了 CLI 和 Node API 传递的所有配置项。

  • Hook 回调方法注入了 compilation 实例,compilation 能够访问当前构建时的模块和相应的依赖。


Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;

Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。
—— 摘自「深入浅出 Webpack」



  • compiler 实例和 compilation 实例上分别定义了许多 Hooks,可以通过 实例.hooks.具体Hook 访问,Hook 上还暴露了 3 个方法供使用,分别是 tap、tapAsync 和 tapPromise。这三个方法用于定义如何执行 Hook,比如 tap 表示注册同步 Hook,tapAsync 代表 callback 方式注册异步 hook,而 tapPromise 代表 Promise 方式注册异步 Hook,可以看下 Webpack 中关于这三种类型实现的源码,为方便阅读,我加了些注释。


// tap方法的type是sync,tapAsync方法的type是async,tapPromise方法的type是promise
// 源码取自Hook工厂方法:lib/HookCodeFactory.js
create(options) {
this.init(options);
let fn;
// Webpack 通过new Function 生成函数
switch (this.options.type) {
case "sync":
fn = new Function(
this.args(), // 生成函数入参
'"use strict";\n' +
this.header() + // 公共方法,生成一些需要定义的变量
this.contentWithInterceptors({ // 生成实际执行的代码的方法
onError: err => `throw ${err};\n`, // 错误回调
onResult: result => `return ${result};\n`, // 得到值的时候的回调
resultReturns: true,
onDone: () => "",
rethrowIfPossible: true
})
);
break;
case "async":
fn = new Function(
this.args({
after: "_callback"
}),
'"use strict";\n' +
this.header() + // 公共方法,生成一些需要定义的变量
this.contentWithInterceptors({
onError: err => `_callback(${err});\n`, // 错误时执行回调方法
onResult: result => `_callback(null, ${result});\n`, // 得到结果时执行回调方法
onDone: () => "_callback();\n" // 无结果,执行完成时
})
);
break;
case "promise":
let errorHelperUsed = false;
const content = this.contentWithInterceptors({
onError: err => {
errorHelperUsed = true;
return `_error(${err});\n`;
},
onResult: result => `_resolve(${result});\n`,
onDone: () => "_resolve();\n"
});
let code = "";
code += '"use strict";\n';
code += this.header(); // 公共方法,生成一些需要定义的变量
code += "return new Promise((function(_resolve, _reject) {\n"; // 返回的是 Promise
if (errorHelperUsed) {
code += "var _sync = true;\n";
code += "function _error(_err) {\n";
code += "if(_sync)\n";
code +=
"_resolve(Promise.resolve().then((function() { throw _err; })));\n";
code += "else\n";
code += "_reject(_err);\n";
code += "};\n";
}
code += content; // 判断具体执行_resolve方法还是执行_error方法
if (errorHelperUsed) {
code += "_sync = false;\n";
}
code += "}));\n";
fn = new Function(this.args(), code);
break;
}
this.deinit(); // 清空 options 和 _args
return fn;
}


Webpack 共提供了以下十种 Hooks,代码中所有具体的 Hook 都是以下这 10 种中的一种。


// 源码取自:lib/index.js
"use strict";

exports.__esModule = true;
// 同步执行的钩子,不能处理异步任务
exports.SyncHook = require("./SyncHook");
// 同步执行的钩子,返回非空时,阻止向下执行
exports.SyncBailHook = require("./SyncBailHook");
// 同步执行的钩子,支持将返回值透传到下一个钩子中
exports.SyncWaterfallHook = require("./SyncWaterfallHook");
// 同步执行的钩子,支持将返回值透传到下一个钩子中,返回非空时,重复执行
exports.SyncLoopHook = require("./SyncLoopHook");
// 异步并行的钩子
exports.AsyncParallelHook = require("./AsyncParallelHook");
// 异步并行的钩子,返回非空时,阻止向下执行,直接执行回调
exports.AsyncParallelBailHook = require("./AsyncParallelBailHook");
// 异步串行的钩子
exports.AsyncSeriesHook = require("./AsyncSeriesHook");
// 异步串行的钩子,返回非空时,阻止向下执行,直接执行回调
exports.AsyncSeriesBailHook = require("./AsyncSeriesBailHook");
// 支持异步串行 && 并行的钩子,返回非空时,重复执行
exports.AsyncSeriesLoopHook = require("./AsyncSeriesLoopHook");
// 异步串行的钩子,下一步依赖上一步返回的值
exports.AsyncSeriesWaterfallHook = require("./AsyncSeriesWaterfallHook");
// 以下 2 个是 hook 工具类,分别用于 hooks 映射以及 hooks 重定向
exports.HookMap = require("./HookMap");
exports.MultiHook = require("./MultiHook");


举几个简单的例子:



  • 上面官方案例中的 run 这个 Hook,会在开始读取 records 之前执行,它的类型是 AsyncSeriesHook,查看源码可以发现,run Hook 既可以执行同步的 tap 方法,也可以执行异步的 tapAsync 和 tapPromise 方法,所以以下写法也是可以的:


const pluginName = 'ConsoleLogOnBuildWebpackPlugin';

class ConsoleLogOnBuildWebpackPlugin {
apply(compiler) {
compiler.hooks.run.tapAsync(pluginName, (compilation, callback) => {
setTimeout(() => {
console.log("webpack 构建过程开始!");
callback(); // callback 方法为了让构建继续执行下去,必须要调用
}, 1000);
});
}
}



  • 再举一个例子,比如 failed 这个 Hook,会在编译失败之后执行,它的类型是 SyncHook,查看源码可以发现,调用 tapAsync 和 tapPromise 方法时,会直接抛错。


对于一些同步的方法,推荐直接使用 tap 进行注册方法,对于异步的方案,tapAsync 通过执行 callback 方法实现回调,如果执行的方法返回的是一个 Promise,推荐使用 tapPromise 进行方法的注册


Hook 的类型可以通过官方 API 查询,地址传送门


// 源码取自:lib/SyncHook.js
const TAP_ASYNC = () => {
throw new Error("tapAsync is not supported on a SyncHook");
};

const TAP_PROMISE = () => {
throw new Error("tapPromise is not supported on a SyncHook");
};

function SyncHook(args = [], name = undefined) {
const hook = new Hook(args, name);
hook.constructor = SyncHook;
hook.tapAsync = TAP_ASYNC;
hook.tapPromise = TAP_PROMISE;
hook.compile = COMPILE;
return hook;
}


讲解完具体的执行方法之后,我们再聊一下 Webpack 流程以及 Tapable 是什么。


Webpack && Tapable


Webpack 运行机制


要理解 Plugin,我们先大致了解 Webpack 打包的流程



  1. 我们打包的时候,会先合并 Webpack config 文件和命令行参数,合并为 options。

  2. 将 options 传入 Compiler 构造方法,生成 compiler 实例,并实例化了 Compiler 上的 Hooks。

  3. compiler 对象执行 run 方法,并自动触发 beforeRun、run、beforeCompile、compile 等关键 Hooks。

  4. 调用 Compilation 构造方法创建 compilation 对象,compilation 负责管理所有模块和对应的依赖,创建完成后触发 make Hook。

  5. 执行 compilation.addEntry() 方法,addEntry 用于分析所有入口文件,逐级递归解析,调用 NormalModuleFactory 方法,为每个依赖生成一个 Module 实例,并在执行过程中触发 beforeResolve、resolver、afterResolve、module 等关键 Hooks。

  6. 将第 5 步中生成的 Module 实例作为入参,执行 Compilation.addModule() 和 Compilation.buildModule() 方法递归创建模块对象和依赖模块对象。

  7. 调用 seal 方法生成代码,整理输出主文件和 chunk,并最终输出。



Tapable


Tapable 是 Webpack 核心工具库,它提供了所有 Hook 的抽象类定义,Webpack 许多对象都是继承自 Tapable 类。比如上面说的 tap、tapAsync 和 tapPromise 都是通过 Tapable 进行暴露的。源码如下(截取了部分代码):


// 第二节 “创建一个 Plugin” 中说的 10 种 Hooks 都是继承了这两个类
// 源码取自:tapable.d.ts
declare class Hook {
tap(options: string | Tap & IfSet, fn: (...args: AsArray) => R): void;
}

declare class AsyncHook extends Hook {
tapAsync(
options: string | Tap & IfSet,
fn: (...args: Append, InnerCallback>) => void
): void;
tapPromise(
options: string | Tap & IfSet,
fn: (...args: AsArray) => Promise
): void;
}



常见 Hooks API


可以参考 Webpack


本文列举一些常用 Hooks 和其对应的类型:


Compiler Hooks
































Hooktype调用
runAsyncSeriesHook开始读取 records 之前
compileSyncHook一个新的编译 (compilation) 创建之后
emitAsyncSeriesHook生成资源到 output 目录之前
doneSyncHook编译 (compilation) 完成

Compilation Hooks



























Hooktype调用
buildModuleSyncHook在模块构建开始之前触发
finishModulesSyncHook所有模块都完成构建
optimizeSyncHook优化阶段开始时触发

Plugin 在项目中的应用


讲完这么多理论知识,接下来我们来看一下 Plugin 在项目中的实战:如何将各个子模块中的 router 文件合并到 router-config.js 中。


背景:


在 React 项目中,一般我们的 Router 文件是写在一个项目中的,如果项目中包含了许多页面,不免会出现所有业务模块 Router 耦合的情况,所以我们开发了一个 Plugin,在构建打包时,该 Plugin 会读取所有文件夹下的 Router 文件,再合并到一起形成一个统一的 Router Config 文件,轻松解决业务耦合问题。这就是 Plugin 的应用。


实现:


const fs = require('fs');
const path = require('path');
const _ = require('lodash');

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

function MegerRouterPlugin(options) {
// options是配置文件,你可以在这里进行一些与options相关的工作
}

MegerRouterPlugin.prototype.apply = function (compiler) {
// 注册 before-compile 钩子,触发文件合并
compiler.plugin('before-compile', (compilation, callback) => {
// 最终生成的文件数据
const data = {};
const routesPath = resolve('src/routes');
const targetFile = resolve('src/router-config.js');
// 获取路径下所有的文件和文件夹
const dirs = fs.readdirSync(routesPath);
try {
dirs.forEach((dir) => {
const routePath = resolve(`src/routes/${dir}`);
// 判断是否是文件夹
if (!fs.statSync(routePath).isDirectory()) {
return true;
}
delete require.cache[`${routePath}/index.js`];
const routeInfo = require(routePath);
// 多个 view 的情况下,遍历生成router信息
if (!_.isArray(routeInfo)) {
generate(routeInfo, dir, data);
// 单个 view 的情况下,直接生成
} else {
routeInfo.map((config) => {
generate(config, dir, data);
});
}
});
} catch (e) {
console.log(e);
}

// 如果 router-config.js 存在,判断文件数据是否相同,不同删除文件后再生成
if (fs.existsSync(targetFile)) {
delete require.cache[targetFile];
const targetData = require(targetFile);
if (!_.isEqual(targetData, data)) {
writeFile(targetFile, data);
}
// 如果 router-config.js 不存在,直接生成文件
} else {
writeFile(targetFile, data);
}

// 最后调用 callback,继续执行 webpack 打包
callback();
});
};
// 合并当前文件夹下的router数据,并输出到 data 对象中
function generate(config, dir, data) {
// 合并 router
mergeConfig(config, dir, data);
// 合并子 router
getChildRoutes(config.childRoutes, dir, data, config.url);
}
// 合并 router 数据到 targetData 中
function mergeConfig(config, dir, targetData) {
const { view, models, extraModels, url, childRoutes, ...rest } = config;
// 获取 models,并去除 src 字段
const dirModels = getModels(`src/routes/${dir}/models`, models);
const data = {
...rest,
};
// view 拼接到 path 字段
data.path = `${dir}/views${view ? `/${view}` : ''}`;
// 如果有 extraModels,就拼接到 models 对象上
if (dirModels.length || (extraModels && extraModels.length)) {
data.models = mergerExtraModels(config, dirModels);
}
Object.assign(targetData, {
[url]: data,
});
}
// 拼接 dva models
function getModels(modelsDir, models) {
if (!fs.existsSync(modelsDir)) {
return [];
}
let files = fs.readdirSync(modelsDir);
// 必须要以 js 或者 jsx 结尾
files = files.filter((item) => {
return /\.jsx?$/.test(item);
});
// 如果没有定义 models ,默认取 index.js
if (!models || !models.length) {
if (files.indexOf('index.js') > -1) {
// 去除 src
return [`${modelsDir.replace('src/', '')}/index.js`];
}
return [];
}
return models.map((item) => {
if (files.indexOf(`${item}.js`) > -1) {
// 去除 src
return `${modelsDir.replace('src/', '')}/${item}.js`;
}
});
}
// 合并 extra models
function mergerExtraModels(config, models) {
return models.concat(config.extraModels ? config.extraModels : []);
}
// 合并子 router
function getChildRoutes(childRoutes, dir, targetData, oUrl) {
if (!childRoutes) {
return;
}
childRoutes.map((option) => {
option.url = oUrl + option.url;
if (option.childRoutes) {
// 递归合并子 router
getChildRoutes(option.childRoutes, dir, targetData, option.url);
}
mergeConfig(option, dir, targetData);
});
}

// 写文件
function writeFile(targetFile, data) {
fs.writeFileSync(targetFile, `module.exports = ${JSON.stringify(data, null, 2)}`, 'utf-8');
}

module.exports = MegerRouterPlugin;



结果:


合并前的文件:


module.exports = [
{
url: '/category/protocol',
view: 'protocol',
},
{
url: '/category/sync',
models: ['sync'],
view: 'sync',
},
{
url: '/category/list',
models: ['category', 'config', 'attributes', 'group', 'otherSet', 'collaboration'],
view: 'categoryRefactor',
},
{
url: '/category/conversion',
models: ['conversion'],
view: 'conversion',
},
];



合并后的文件:


module.exports = {
"/category/protocol": {
"path": "Category/views/protocol"
},
"/category/sync": {
"path": "Category/views/sync",
"models": [
"routes/Category/models/sync.js"
]
},
"/category/list": {
"path": "Category/views/categoryRefactor",
"models": [
"routes/Category/models/category.js",
"routes/Category/models/config.js",
"routes/Category/models/attributes.js",
"routes/Category/models/group.js",
"routes/Category/models/otherSet.js",
"routes/Category/models/collaboration.js"
]
},
"/category/conversion": {
"path": "Category/views/conversion",
"models": [
"routes/Category/models/conversion.js"
]
},
}



最终项目就会生成 router-config.js 文件



结尾


希望大家看完本章之后,对 Webpack Plugin 有一个初步的认识,能够上手写一个自己的 Plugin 来应用到自己的项目中。


文章中如有不对的地方,欢迎指正。


链接:https://juejin.cn/post/6968988552075952141

收起阅读 »

当面试官问Webpack的时候他想知道什么

前言 在前端工程化日趋复杂的今天,模块打包工具在我们的开发中起到了越来越重要的作用,其中webpack就是最热门的打包工具之一。 说到webpack,可能很多小伙伴会觉得既熟悉又陌生,熟悉是因为几乎在每一个项目中我们都会用上它,又因为webpack复杂的配置和...
继续阅读 »

前言


在前端工程化日趋复杂的今天,模块打包工具在我们的开发中起到了越来越重要的作用,其中webpack就是最热门的打包工具之一。


说到webpack,可能很多小伙伴会觉得既熟悉又陌生,熟悉是因为几乎在每一个项目中我们都会用上它,又因为webpack复杂的配置和五花八门的功能感到陌生。尤其当我们使用诸如umi.js之类的应用框架还帮我们把webpack配置再封装一层的时候,webpack的本质似乎离我们更加遥远和深不可测了。


当面试官问你是否了解webpack的时候,或许你可以说出一串耳熟能详的webpack loaderplugin的名字,甚至还能说出插件和一系列配置做按需加载和打包优化,那你是否了解他的运行机制以及实现原理呢,那我们今天就一起探索webpack的能力边界,尝试了解webpack的一些实现流程和原理,拒做API工程师。


CgqCHl6pSFmAC5UzAAEwx63IBwE024.png


你知道webpack的作用是什么吗?


从官网上的描述我们其实不难理解,webpack的作用其实有以下几点:




  • 模块打包。可以将不同模块的文件打包整合在一起,并且保证它们之间的引用正确,执行有序。利用打包我们就可以在开发的时候根据我们自己的业务自由划分文件模块,保证项目结构的清晰和可读性。




  • 编译兼容。在前端的“上古时期”,手写一堆浏览器兼容代码一直是令前端工程师头皮发麻的事情,而在今天这个问题被大大的弱化了,通过webpackLoader机制,不仅仅可以帮助我们对代码做polyfill,还可以编译转换诸如.less, .vue, .jsx这类在浏览器无法识别的格式文件,让我们在开发的时候可以使用新特性和新语法做开发,提高开发效率。




  • 能力扩展。通过webpackPlugin机制,我们在实现模块化打包和编译兼容的基础上,可以进一步实现诸如按需加载,代码压缩等一系列功能,帮助我们进一步提高自动化程度,工程效率以及打包输出的质量。




说一下模块打包运行原理?


如果面试官问你Webpack是如何把这些模块合并到一起,并且保证其正常工作的,你是否了解呢?


首先我们应该简单了解一下webpack的整个打包流程:



  • 1、读取webpack的配置参数;

  • 2、启动webpack,创建Compiler对象并开始解析项目;

  • 3、从入口文件(entry)开始解析,并且找到其导入的依赖模块,递归遍历分析,形成依赖关系树;

  • 4、对不同文件类型的依赖模块文件使用对应的Loader进行编译,最终转为Javascript文件;

  • 5、整个过程中webpack会通过发布订阅模式,向外抛出一些hooks,而webpack的插件即可通过监听这些关键的事件节点,执行插件任务进而达到干预输出结果的目的。


其中文件的解析与构建是一个比较复杂的过程,在webpack源码中主要依赖于compilercompilation两个核心对象实现。


compiler对象是一个全局单例,他负责把控整个webpack打包的构建流程。
compilation对象是每一次构建的上下文对象,它包含了当次构建所需要的所有信息,每次热更新和重新构建,compiler都会重新生成一个新的compilation对象,负责此次更新的构建过程。


而每个模块间的依赖关系,则依赖于AST语法树。每个模块文件在通过Loader解析完成之后,会通过acorn库生成模块代码的AST语法树,通过语法树就可以分析这个模块是否还有依赖的模块,进而继续循环执行下一个模块的编译解析。


最终Webpack打包出来的bundle文件是一个IIFE的执行函数。


// webpack 5 打包的bundle文件内容

(() => { // webpackBootstrap
var __webpack_modules__ = ({
'file-A-path': ((modules) => { // ... })
'index-file-path': ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => { // ... })
})

// The module cache
var __webpack_module_cache__ = {};

// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = __webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {}
};

// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);

// Return the exports of the module
return module.exports;
}

// startup
// Load entry module and return exports
// This entry module can't be inlined because the eval devtool is used.
var __webpack_exports__ = __webpack_require__("./src/index.js");
})


webpack4相比,webpack5打包出来的bundle做了相当的精简。在上面的打包demo中,整个立即执行函数里边只有三个变量和一个函数方法,__webpack_modules__存放了编译后的各个文件模块的JS内容,__webpack_module_cache__ 用来做模块缓存,__webpack_require__Webpack内部实现的一套依赖引入函数。最后一句则是代码运行的起点,从入口文件开始,启动整个项目。


其中值得一提的是__webpack_require__模块引入函数,我们在模块化开发的时候,通常会使用ES Module或者CommonJS规范导出/引入依赖模块,webpack打包编译的时候,会统一替换成自己的__webpack_require__来实现模块的引入和导出,从而实现模块缓存机制,以及抹平不同模块规范之间的一些差异性。


你知道sourceMap是什么吗?


提到sourceMap,很多小伙伴可能会立刻想到Webpack配置里边的devtool参数,以及对应的evaleval-cheap-source-map等等可选值以及它们的含义。除了知道不同参数之间的区别以及性能上的差异外,我们也可以一起了解一下sourceMap的实现方式。


sourceMap是一项将编译、打包、压缩后的代码映射回源代码的技术,由于打包压缩后的代码并没有阅读性可言,一旦在开发中报错或者遇到问题,直接在混淆代码中debug问题会带来非常糟糕的体验,sourceMap可以帮助我们快速定位到源代码的位置,提高我们的开发效率。sourceMap其实并不是Webpack特有的功能,而是Webpack支持sourceMap,像JQuery也支持souceMap


既然是一种源码的映射,那必然就需要有一份映射的文件,来标记混淆代码里对应的源码的位置,通常这份映射文件以.map结尾,里边的数据结构大概长这样:


{
"version" : 3, // Source Map版本
"file": "out.js", // 输出文件(可选)
"sourceRoot": "", // 源文件根目录(可选)
"sources": ["foo.js", "bar.js"], // 源文件列表
"sourcesContent": [null, null], // 源内容列表(可选,和源文件列表顺序一致)
"names": ["src", "maps", "are", "fun"], // mappings使用的符号名称列表
"mappings": "A,AAAB;;ABCDE;" // 带有编码映射数据的字符串
}


其中mappings数据有如下规则:



  • 生成文件中的一行的每个组用“;”分隔;

  • 每一段用“,”分隔;

  • 每个段由1、4或5个可变长度字段组成;


有了这份映射文件,我们只需要在我们的压缩代码的最末端加上这句注释,即可让sourceMap生效:


//# sourceURL=/path/to/file.js.map

有了这段注释后,浏览器就会通过sourceURL去获取这份映射文件,通过解释器解析后,实现源码和混淆代码之间的映射。因此sourceMap其实也是一项需要浏览器支持的技术。


如果我们仔细查看webpack打包出来的bundle文件,就可以发现在默认的development开发模式下,每个_webpack_modules__文件模块的代码最末端,都会加上//# sourceURL=webpack://file-path?,从而实现对sourceMap的支持。


sourceMap映射表的生成有一套较为复杂的规则,有兴趣的小伙伴可以看看以下文章,帮助理解soucrMap的原理实现:


Source Map的原理探究


Source Maps under the hood – VLQ, Base64 and Yoda


是否写过Loader?简单描述一下编写loader的思路?


从上面的打包代码我们其实可以知道,Webpack最后打包出来的成果是一份Javascript代码,实际上在Webpack内部默认也只能够处理JS模块代码,在打包过程中,会默认把所有遇到的文件都当作 JavaScript代码进行解析,因此当项目存在非JS类型文件时,我们需要先对其进行必要的转换,才能继续执行打包任务,这也是Loader机制存在的意义。


Loader的配置使用我们应该已经非常的熟悉:


// webpack.config.js
module.exports = {
// ...other config
module: {
rules: [
{
test: /^your-regExp$/,
use: [
{
loader: 'loader-name-A',
},
{
loader: 'loader-name-B',
}
]
},
]
}
}

通过配置可以看出,针对每个文件类型,loader是支持以数组的形式配置多个的,因此当Webpack在转换该文件类型的时候,会按顺序链式调用每一个loader,前一个loader返回的内容会作为下一个loader的入参。因此loader的开发需要遵循一些规范,比如返回值必须是标准的JS代码字符串,以保证下一个loader能够正常工作,同时在开发上需要严格遵循“单一职责”,只关心loader的输出以及对应的输出。


loader函数中的this上下文由webpack提供,可以通过this对象提供的相关属性,获取当前loader需要的各种信息数据,事实上,这个this指向了一个叫loaderContextloader-runner特有对象。有兴趣的小伙伴可以自行阅读源码。


module.exports = function(source) {
const content = doSomeThing2JsString(source);

// 如果 loader 配置了 options 对象,那么this.query将指向 options
const options = this.query;

// 可以用作解析其他模块路径的上下文
console.log('this.context');

/*
* this.callback 参数:
* error:Error | null,当 loader 出错时向外抛出一个 error
* content:String | Buffer,经过 loader 编译后需要导出的内容
* sourceMap:为方便调试生成的编译后内容的 source map
* ast:本次编译生成的 AST 静态语法树,之后执行的 loader 可以直接使用这个 AST,进而省去重复生成 AST 的过程
*/
this.callback(null, content);
// or return content;
}

更详细的开发文档可以直接查看官网的 Loader API


是否写过Plugin?简单描述一下编写plugin的思路?


如果说Loader负责文件转换,那么Plugin便是负责功能扩展。LoaderPlugin作为Webpack的两个重要组成部分,承担着两部分不同的职责。


上文已经说过,webpack基于发布订阅模式,在运行的生命周期中会广播出许多事件,插件通过监听这些事件,就可以在特定的阶段执行自己的插件任务,从而实现自己想要的功能。


既然基于发布订阅模式,那么知道Webpack到底提供了哪些事件钩子供插件开发者使用是非常重要的,上文提到过compilercompilationWebpack两个非常核心的对象,其中compiler暴露了和 Webpack整个生命周期相关的钩子(compiler-hooks),而compilation则暴露了与模块和依赖有关的粒度更小的事件钩子(Compilation Hooks)。


Webpack的事件机制基于webpack自己实现的一套Tapable事件流方案(github


// Tapable的简单使用
const { SyncHook } = require("tapable");

class Car {
constructor() {
// 在this.hooks中定义所有的钩子事件
this.hooks = {
accelerate: new SyncHook(["newSpeed"]),
brake: new SyncHook(),
calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
};
}

/* ... */
}


const myCar = new Car();
// 通过调用tap方法即可增加一个消费者,订阅对应的钩子事件了
myCar.hooks.brake.tap("WarningLampPlugin", () => warningLamp.on());

Plugin的开发和开发Loader一样,需要遵循一些开发上的规范和原则:



  • 插件必须是一个函数或者是一个包含 apply 方法的对象,这样才能访问compiler实例;

  • 传给每个插件的 compilercompilation 对象都是同一个引用,若在一个插件中修改了它们身上的属性,会影响后面的插件;

  • 异步的事件需要在插件处理完任务时调用回调函数通知 Webpack 进入下一个流程,不然会卡住;


了解了以上这些内容,想要开发一个 Webpack Plugin,其实也并不困难。


class MyPlugin {
apply (compiler) {
// 找到合适的事件钩子,实现自己的插件功能
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation: 当前打包构建流程的上下文
console.log(compilation);

// do something...
})
}
}

更详细的开发文档可以直接查看官网的 Plugin API


最后


本文也是结合一些优秀的文章和webpack本身的源码,大概地说了几个相对重要的概念和流程,其中的实现细节和设计思路还需要结合源码去阅读和慢慢理解。


Webpack作为一款优秀的打包工具,它改变了传统前端的开发模式,是现代化前端开发的基石。这样一个优秀的开源项目有许多优秀的设计思想和理念可以借鉴,我们自然也不应该仅仅停留在API的使用层面,尝试带着问题阅读源码,理解实现的流程和原理,也能让我们学到更多知识,理解得更加深刻,在项目中才能游刃有余的应用。




链接:https://juejin.cn/post/6943468761575849992

收起阅读 »

是什么让尤大选择放弃Webpack?面向未来的前端构建工具 Vite

前两天在知乎看到过一篇文章,大致意思是讲:字节跳动已经开始“弃用Webpack”,尝试在自研构建工具中使用类似Vite的ESmodule构建方式。 引起下方一大片焦虑: Webpack是不是要被取代了?现在学Vite就行了吧 Webpack还没学会,就又来新...
继续阅读 »

前两天在知乎看到过一篇文章,大致意思是讲:字节跳动已经开始“弃用Webpack”,尝试在自研构建工具中使用类似Vite的ESmodule构建方式。


引起下方一大片焦虑:



  • Webpack是不是要被取代了?现在学Vite就行了吧

  • Webpack还没学会,就又来新的了!


甚至有人搬出了去年尤大所发的一个动态:再也回不去Webpack了。


在这里插入图片描述



PS:最近的vite比较火,而且发布了2.0版本,vue的作者尤雨溪也是在极力推荐


全方位对比vite和webpack


webpack打包过程


1.识别入口文件


2.通过逐层识别模块依赖。(Commonjs、amd或者es6的import,webpack都会对其进行分析。来获取代码的依赖)


3.webpack做的就是分析代码。转换代码,编译代码,输出代码


4.最终形成打包后的代码


webpack打包原理


1.先逐级递归识别依赖,构建依赖图谱


2.将代码转化成AST抽象语法树


3.在AST阶段中去处理代码


4.把AST抽象语法树变成浏览器可以识别的代码, 然后输出



重点:这里需要递归识别依赖,构建依赖图谱。图谱对象就是类似下面这种



{ './app.js':
{ dependencies: { './test1.js': './test1.js' },
code:
'"use strict";\n\nvar _test = _interopRequireDefault(require("./test1.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nconsole.log(test
1);' },
'./test1.js':
{ dependencies: { './test2.js': './test2.js' },
code:
'"use strict";\n\nvar _test = _interopRequireDefault(require("./test2.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nconsole.log(\'th
is is test1.js \', _test["default"]);' },
'./test2.js':
{ dependencies: {},
code:
'"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports["default"] = void 0;\n\nfunction test2() {\n console.log(\'this is test2 \');\n}\n\nvar _default = tes
t2;\nexports["default"] = _default;' } }


在这里插入图片描述


Vite原理


当声明一个 script 标签类型为 module 时





浏览器就会像服务器发起一个GET


http://localhost:3000/src/main.js请求main.js文件:

// /src/main.js:
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')


浏览器请求到了main.js文件,检测到内部含有import引入的包,又会对其内部的 import 引用发起 HTTP 请求获取模块的内容文件



Vite 的主要功能就是通过劫持浏览器的这些请求,并在后端进行相应的处理将项目中使用的文件通过简单的分解与整合,然后再返回给浏览器,vite整个过程中没有对文件进行打包编译,所以其运行速度比原始的webpack开发编译速度快出许多!


webpack缺点一:缓慢的服务器启动


当冷启动开发服务器时,基于打包器的方式是在提供服务前去急切地抓取和构建你的整个应用。


Vite改进



  • Vite 通过在一开始将应用中的模块区分为 依赖 和 源码 两类,改进了开发服务器启动时间。

  • 依赖 大多为纯 JavaScript 并在开发时不会变动。一些较大的依赖(例如有上百个模块的组件库)处理的代价也很高。依赖也通常会以某些方式(例如 ESM 或者 CommonJS)被拆分到大量小模块中。

  • Vite 将会使用 esbuild 预构建依赖。Esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快 10-100 倍。

  • 源码 通常包含一些并非直接是 JavaScript 的文件,需要转换(例如 JSX,CSS 或者 Vue/Svelte 组件),时常会被编辑。同时,并不是所有的源码都需要同时被加载。(例如基于路由拆分的代码模块)。

  • Vite 以 原生 ESM 方式服务源码。这实际上是让浏览器接管了打包程序的部分工作:Vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入的代码,即只在当前屏幕上实际使用时才会被处理。


webpack缺点2:使用的是node.js去实现


在这里插入图片描述
Vite改进


Vite 将会使用 esbuild 预构建依赖。Esbuild 使用 Go 编写,并且比以 Node.js 编写的打包器预构建依赖快 10-100 倍。


webpack致命缺点3:热更新效率低下



  • 当基于打包器启动时,编辑文件后将重新构建文件本身。显然我们不应该重新构建整个包,因为这样更新速度会随着应用体积增长而直线下降。

  • 一些打包器的开发服务器将构建内容存入内存,这样它们只需要在文件更改时使模块图的一部分失活[1],但它也仍需要整个重新构建并重载页面。这样代价很高,并且重新加载页面会消除应用的当前状态,所以打包器支持了动态模块热重载(HMR):允许一个模块 “热替换” 它自己,而对页面其余部分没有影响。这大大改进了开发体验 - 然而,在实践中我们发现,即使是 HMR 更新速度也会随着应用规模的增长而显著下降。


Vite改进



  • 在 Vite 中,HMR 是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块与其最近的 HMR 边界之间的链失效(大多数时候只需要模块本身),使 HMR 更新始终快速,无论应用的大小。

  • Vite 同时利用 HTTP 头来加速整个页面的重新加载(再次让浏览器为我们做更多事情):源码模块的请求会根据 304 Not Modified 进行协商缓存,而依赖模块请求则会通过 Cache-Control: max-age=31536000,immutable 进行强缓存,因此一旦被缓存它们将不需要再次请求。


Vite缺点1:生态,生态,生态不如webpack


wepback牛逼之处在于loader和plugin非常丰富,不过我认为生态只是时间问题,现在的vite,更像是当时刚出来的M1芯片Mac,我当时非常看好M1的Mac,毫不犹豫买了,现在也没什么问题


Vite缺点2:prod环境的构建,目前用的Rollup


原因在于esbuild对于css和代码分割不是很友好


Vite缺点3:还没有被大规模使用,很多问题或者诉求没有真正暴露出来


vite真正崛起那一天,是跟vue3有关系的,当vue3广泛开始使用在生产环境的时候,vite也就大概率意味着被大家慢慢开始接受了


总结


1.Vite,就像刚出来的M1芯片Mac,都说好,但是一开始买的人不多,担心生态问题,后面都说真香


2.相信vue3作者的大力支持下,vite即将大放异彩!


3.但是 Webpack 在现在的前端工程化中仍然扮演着非常重要的角色。


4.vite相关生态没有webpack完善,vite可以作为开发的辅助。



链接:https://juejin.cn/post/6975038104650383374

收起阅读 »

Vue3发布半年我不学,摸鱼爽歪歪,哎~就是玩儿

vue
是从 Vue 2 开始学基础还是直接学 Vue 3 ?尤雨溪给出的答案是:“直接学 Vue 3 就行了,基础概念是一模一样的。” 以上内容源引自最新一期的《程序员》期刊,原文链接为《直接学 Vue 3 吧 —— 对话 Vue.js 作者尤雨溪》。 前言 Vue...
继续阅读 »

是从 Vue 2 开始学基础还是直接学 Vue 3 ?尤雨溪给出的答案是:“直接学 Vue 3 就行了,基础概念是一模一样的。”


以上内容源引自最新一期的《程序员》期刊,原文链接为《直接学 Vue 3 吧 —— 对话 Vue.js 作者尤雨溪》


前言


Vue 3.0 出来之后,我一直在不断的尝试学习和接受新的概念。没办法,作为一个前端开发,并且也不是毕业于名校或就职于大厂,不断地学习,培养学习能力,才是我们这些普通前端开发的核心竞争力。


当然,有些同学抬杠,我专精一门技术,也能开发出自己的核心竞争力。好!!!有志气。但是多数同学,很难有这种意志力。如 CSS 大佬张鑫旭Canva 大佬老姚、可视化大佬月影大大、面试题大佬敖丙等等等等。这些大佬在一件事情上花费的精力,是需要极高的意志力和执行力才能做到的。我反正做不到(逃)。


学无止境!


一定要动手敲代码。仅仅学习而不实践,这种做法也不可取。


本文主要是介绍一些我学习 Vue 3.0 期间,看过的一些比较有用的资源,和大家分享一下,不喜勿喷,喷了我也学着 @尼克陈 顺着网线找到你家。


我与 Vue 3.0


其实一直都有在关注 Vue 3.0 相关的进度和新闻,不过真正学习是在它正式 release 后,2020 年 9 月我也发布了一篇文章《Vue 3.0 来了,我们该做些什么?》阐述了自己的看法,也制定了自己的学习计划。


其实,学习任何一门新技术的步骤都一样:


看文档 → 学习新语法 → 做小 demo → 做几个实战项目 → 看源码 → 整理心得并分享。


学习 Vue 3.0 亦是如此,虽然我这个人比较爱开玩笑,也爱写段子,标题取的也吊儿郎当,但是学习和行动起来我可不比别人差。


学习过程中看文档、做 demo,然后也一直在学习和分享 Vue3 的知识点,比如发布一些 Vue3 的教程:



也做了几个 Vue 3.0 实战的项目练手,之后发布到也开源了 GitHub 中,访问地址如下:



in GitHub : github.com/newbee-ltd


in Gitee : gitee.com/newbee-ltd



一个是 Vue3 版本的商城项目:


img


一个是 Vue3 版本的后台管理项目:


panban1 (1)


源码全部开放,后台 API 也有,都是很实用的项目。目前的反响还不错,得到了很多的正向反馈,这些免费的开源项目让大家有了一个不错的 Vue3 练手项目,顺利的完成了课程作业或者在简历里多了一份项目经验,因此也收到了很多感谢的话。


接下来就是学习过程中,我觉得非常有用的资源了,大家在学习 Vue 3 时可以参考和使用。


image-20210228175425067


Vue 3.0 相关技术栈



















































相关库名称在线地址 🔗
Vue 3.0 官方文档(英文)在线地址
Vue 3.0 中文文档在线地址 国内加速版
Composition-API手册在线地址
Vue 3.0 源码学习在线地址
Vue-Router 官方文档在线地址
Vuex 4.0Github
vue-devtoolsGithub(Vue3.0 需要使用最新版本)
Vite 源码学习线上地址
Vite 2.0 中文文档线上地址
Vue3 新动态线上地址

Vue3 新动态 这个仓库我经常看,里面有最新的 Vue 3 文章、仓库等等,都是中文的,作者应该是咱们的大兄弟,大家也可以关注一下。


更新 Vue 3.0 的开源 UI 组件库


Vue 2.0 时期,产生了不少好的开源组件库,这些组件库伴随着我们的成长,我们看看哪些组件库更新了 Vue 3.0 版本。


Element-plus


简介:大家想必也不陌生,它的 Vue 2.0 版本是 Element-UI,后经坤哥和他的小伙伴开发出了 Vue 3.0 版本的  Element-plus,确实很优秀,目前点赞数快破万了,持续关注。


仓库地址 🏠 :github.com/element-plu… ⭐ : 9.8k


文档地址 📖 :element-plus.gitee.io/#/zh-CN


开源项目 🔗 :



目前 Element-plus 的开源项目还不多,之前 Element-UI 相关开源项目,大大小小都在做 Element-plus 的适配。在此也感谢坤哥和他的小伙伴们,持续 Element 系列的维护,这对 Vue 生态是非常强大的贡献。


Ant Design of Vue


简介:它是最早一批做 Vue 3.0 适配的组件库, Antd 官方推荐的组件库。


仓库地址 🏠 :github.com/vueComponen… ⭐ : 14.8k


文档地址 📖 :antdv.com/docs/vue/in…


开源项目 🔗 :



他们的更新维护还是很积极的,最近一次更新实在 2021 年 2 月 27 号,可见这个组件库还是值得信赖的,有问题可以去 issue 提。


Vant


简介:国内移动端首屈一指的组件库,用过的都说好,个人已经在两个项目中使用过该组件库,也算是比较早支持 Vue 3.0 的框架,该有的都有。


仓库地址 🏠 :github.com/youzan/vant ⭐ : 16.9k


文档地址 📖 :vant-contrib.gitee.io/vant/v3/#/z…


开源项目 🔗 :



NutUI 3


简介:京东团队开发的移动端组件库,近期才升级到 Vue 3.0 版本,文章在此。虽然我没有使用过这个组件库,但是从他们的更新速度来看,比其他很多组件库要快,说明对待最近技术,还是有态度的。


仓库地址 🏠 :github.com/jdf2e/nutui ⭐ : 3.1k


文档地址 📖 :nutui.jd.com (看看这简短的域名,透露出壕的气息)


开源项目 🔗 :基本上还没有见到有公开的开源项目,如果有还望大家积极评论


链接:https://juejin.cn/post/6955129410705948702

收起阅读 »

前端智能化看"低代码/无代码"

概念 什么是低代码/无代码开发?业界对于低代码/无代码开发是否存在其他不同的理解? 行业里流行观点,低代码是更加易用的搭建系统,无代码是图形化和可视化编程。这种观点把低代码和无代码开发分别置于 UI 和逻辑两个环节,以工具属性定义搭建和可视化编程要解决的问题。...
继续阅读 »

概念


什么是低代码/无代码开发?业界对于低代码/无代码开发是否存在其他不同的理解?


行业里流行观点,低代码是更加易用的搭建系统,无代码是图形化和可视化编程。这种观点把低代码和无代码开发分别置于 UI 和逻辑两个环节,以工具属性定义搭建和可视化编程要解决的问题。另一种观点则是把低代码/无代码看作一个方法的两个阶段,就像对自动驾驶的 L0 ~ L5 共 6 个不同阶段一样,把我之前在:《人机协同的编程方式》 一文提出的人机协同编程的概念,划分为低代码/无代码两个阶段。较之第一种我更加认同第二种观点,不仅因为是我提出的,更因为第二种观点是以软件工程的统一视角定义、分析和解决问题,而第一种观点只是局部和过程的优化而非颠覆性创新。


如马云先生在香港对年轻人传授创业经验时讲到的,蒸汽机和电力解放了人类的体力,人工智能和机器学习解放了人类的脑力。马云先生在评价蒸汽机和电力带来的失业问题时讲到,人类在科技进步下从繁重的体力劳动中解放出来,逐步向脑力劳动过渡,这是人类社会的进步。今天“人机协同的编程方式”把软件工程从拼装 UI 和编写业务逻辑里解放出来,逐步向业务能力、基础能力、底层能力等高技术含量工作过渡。更多内容参考:《前端智能化:思维转变之路》


低代码开发和无代码开发之间的区别是什么?


接着上述所答,既然低代码和无代码属于“人机协同编程”的两个阶段,低代码就是阶段一、无代码则是阶段二,分别对应“人机协作”和“人机协同”。协作和协同最大的区别就是:心有灵犀。不论低代码还是无代码,均有服务的对象:用户。不论用户是程序员还是非编程人员,均有统一目标:生成代码。不论源码开发、低代码还是无代码,都是在用不同的方式描述程序,有代码、图形、DSL……等。“人机协作”的阶段,这些描述有各种限制、约束,应用的业务场景亦狭窄。“人机协同”的阶段,则限制、约束减少,应用的业务场景亦宽广。“心有灵犀”就是指:通过 AI 对描述进行学习和理解,从而减少限制和约束,适应更多业务场景。因此,传统低代码/无代码和“人机协同编程”生成代码相比,最大的不同就是有心和无心,机器有心而平台无心。


背景


低代码/无代码开发与软件工程领域的一些经典思想、方法和技术,例如软件复用与构件组装、软件产品线、DSL(领域特定语言)、可视化快速开发工具、可定制工作流,以及此前业界流行的中台等概念,之间是什么关系?


从库、框架、脚手架开始,软件工程就踏上了追求效率的道路。在这个道路之上,低代码、无代码的开发方式算是宏愿。复用、组件化和模块化、DSL、可视化、流程编排……都是在达成宏愿过程中的尝试,要么在不同环节、要么以不同方式,但都还在软件工程领域内思考。中台概念更多是在业务视角下提出的,软件工程和技术领域内类似的概念更多是叫:平台。不论中台还是平台,就不仅是在过程中的尝试,而是整体和系统的创新尝试。我提出前端智能化的“人机协同编程”应该同属于软件工程和技术领域,在类似中台的业务领域我提出“需求暨生产”的全新业务研发模式,则属于业务领域。这些概念之间无非:左右、上下、新旧关系而已。


此外,低代码/无代码开发与DevOps、云计算与云原生架构之间又是什么样的关系?


DevOps、云计算……都属于基础技术,基础技术的变化势必带来上层应用层技术变化。没有云计算的容器化、弹性缩扩容,做分布式系统是很困难的,尤其在 CI/CD、部署、运维、监控、调优……等环节更甚,什么南北分布、异地多活、平行扩展、高可用……都需要去关注。但是,云计算和DevOps等基础技术的发展,内化并自动化解决了上述问题,大大降低了关注和使用成本,这就是心有灵犀,在这样的基础技术之上构建应用层技术,限制少、约束小还能适应各种复杂场景。


思想方法


支撑低代码/无代码开发的核心技术是什么?


我认为低代码/无代码开发的核心技术,过去是“复用”,今天是 AI 驱动的“人机协同编程”。过去的低代码/无代码开发多围绕着提升研发效能入手,今天 AI 驱动的“人机协同编程”则是围绕着提升交付效率入手。因此,低代码/无代码开发以“人机协同编程”为主要实现手段的话,AI 是其核心技术。


低代码/无代码开发的火热是软件开发技术上的重要变革和突破,还是经典软件工程思想、方法和技术随着技术和业务积累的不断发展而焕发出的新生机?


计算机最初只在少数人掌握,如今,几乎人人手持一台微型计算机:智慧手机。当初为程序员和所谓“技术人员”的专利,而今,几乎人人都会操作和使用计算机。然而,人们对计算机的操作是间接的,需要有专业的人士和企业提前编写软件,人们通过软件使用计算机的各种功能。随着计算机算力和功能的不断发展,随着社会的数字化和信息化,今天的人们越来越难以被提前定制好的软件所满足。低代码/无代码开发则赋予人们创造软件的能力,进而帮助人们低成本、即时、高效的直接生产符合自己需求的软件,进而操作众多复杂的电子设备和数字世界建立联结。我认为,这是不可逆的趋势,也是低代码/无代码开发的大方向。


现状进展


低代码/无代码开发已经发展到什么程度?


image.png


imgcook



  • 2w 多用户、6w 多模块、 0 前端参与研发的双十一等大促营销活动、70% 阿里前端在使用

  • 79.26% 无人工参与的线上代码可用率、90.9% 的还原度、Icon 识别准确率 83%、组件识别 85%、布局还原度 92.1%、布局人工修改概率 75%

  • 研发效率提升 68%


uicook


-营销活动和大促场景 ui 智能生成比例超过 90% -日常频道导购业务 ui 智能生成覆盖核心业务



  • 纯 ui 智能化和个性化带来的业务价值提升超过 8%


bizcook


初步完成基于 NLP 的需求标注和理解系统 初步完成基于 NLP 的服务注册和理解系统 初步完成基于 NLP 的胶水层业务逻辑代码生成能力


reviewcook



  • 针对资损防控自动化扫描、CV 和 AI 自动化识别资损风险和舆情问题

  • 和测试同学共建的 UI 自动化测试、数据渲染和 Mock 驱动的业务自动化验证

  • 和工程团队共建的 AI Codereview 基于对代码的分析和理解,结合线上 Runtime 的识别和分析,自动化发现问题、定位问题,提升 Codereview 的效率和质量


datacook



  • 社区化运营开源项目,合并 Denfo.js 同其作者共同设立 Datacook 项目,全链路、端到端解决 AI 领域数据采集、存储、处理问题,尤其在海量数据、数据集组织、数据质量评估等深度学习和机器学习领域的能力比肩 HDF5、Pandas……等 Python 专业 LIbrary

  • Google Tensorflow.js 团队合作开发维护 TFData library ,作为 Datacook 的核心技术和基础,共同构建数据集生态和数据集易用性


pipcook



  • 开源了 github.com/alibaba/pip… 纯前端机器学习框架

  • 利用 Boa 打通 Python 技术生态,原生支持 import Python 流行的包和库,原生支持 Python 的数据类型和数据结构,方便跨语言共享数据和调用 API

  • 利用 Pipcook Cloud 打通流行的云计算平台,帮助前端智能化实现 CDML,形成数据和算法工程闭环,帮助开发者打造工业级可用的服务和在线、离线算法能力


有哪些成熟的低代码/无代码开发平台?


image.png image.png image.png


低代码/无代码开发能够在多大程度上改变当前的软件开发方式?


随着计算机算力和功能的不断发展,随着社会的数字化和信息化,今天的人们越来越难以被提前定制好的软件所满足。低代码/无代码开发则赋予人们创造软件的能力,进而帮助人们低成本、即时、高效的直接生产符合自己需求的软件,进而操作众多复杂的电子设备和数字世界建立联结。我认为,这是不可逆的趋势,也是低代码/无代码开发的大方向。最终,软件开发势必从专业程序员手里转向普罗大众,成为今天操作计算机一样的基本生存技能之一。因此,软件开发方式将带来本质变化,从完整的交付转向局部交付、从业务整体交付转向业务能力交付……


展望未来


低代码/无代码开发未来发展的方向是什么?


要我说,低代码/无代码开发未来发展的方向一定是:AI 驱动的“人机协同编程”,将完整开发一个软件变成提供局部的软件功能,类似 Apple 的“捷径”一样,由用户决定这些局部软件功能如何组装成适合用户的软件并交付最终用户。AI 驱动提供两个方面的价值:


降低开发成本


以往开发软件的时候,要有 PRD、交互稿、设计稿、设计文档……等一系列需求规格说明,然后,根据这些需求规格利用技术和工程手段进行实现。然而,低代码/无代码开发交付的是局部功能和半成品,会被无法枚举的目的和环境所使用,既然无法枚举,就不能用 Swith……Case 的方式编写代码,否则会累死。


AI 的特点就是基于特征和环境进行预测,预测的基础是对模式和本质的理解。就像 AI 识别一只猫,不管这个猫在什么环境、什么光照条件下,也不管这只猫是什么品种,AI 都能够以超过人类的准确度识别。试想,作为一个程序员用程序判断一只猫的开发成本何其高?


降低使用成本


今天的搭建体系,本质上是把编程过程用搭建的思想重构了一遍,工作的内容并没有发生变化,成本从程序员转嫁到运营、产品、设计师的身上。这还是其次,今天的搭建平台都是技术视角出发,充斥着运营、产品、设计等非技术人员一脸懵逼的概念,花在答疑解惑和教他们如何在页面上定制一个搜索框的时间,比自己和他们沟通后源码实现的时间还要长,而且经常在撸代码的时候被打断……


基于 AI 的“人机协同编程”不需要透出任何技术概念,运营、产品、设计……等非技术人员也不改变其工作习惯,都用自己熟悉的工具和自己熟悉的概念描述自己的需求,AI 负责对这些需求进行识别和理解,再转换成编程和技术工程领域的概念,进而生成代码并交付,从而大幅度降低使用成本。


举个例子:如果你英文写作能力不好,你拿着朗道词典一边翻译一边拼凑单词写出来的英文文章质量高呢?还是用中文把文章写好,再使用 Google 翻译整篇转换成英文的文章质量高?你自己试试就知道了。究其原因,你在自己熟悉的语言和概念领域内,才能够把自己的意思表达清楚。


围绕低代码/无代码开发存在哪些技术难题需要学术界和工业界共同探索?


最初在 D2 上提出并分享“前端智能化”这个概念的时候,我就提出:识别、理解、表达 这个核心过程。我始终认为,达成 AI 驱动的“人机协同编程”关键路径就是:识别、理解、表达。因此,围绕 AI 识别、 AI 理解、 AI 表达我们和国内外知名大学展开了广泛的合作。


识别


需求的识别:通过 NLP 、知识图谱、图神经网络、结构化机器学习……等 AI 技术,识别用户需求、产品需求、设计需求、运营需求、营销需求、研发需求、工程需求……等,识别出其中的概念和概念之间的关系


设计稿的识别:通过 CV、GAN、对象识别、语义分割……等 AI 技术,识别设计稿中的元素、元素之间的关系、设计语言、设计系统、设计意图


UI 的识别:通过用户用脚投票的结果进行回归,后验的分析识别出 UI 对用户行为的影响程度、影响效果、影响频率、影响时间……等,并识别出 UI 的可变性和这些用户行为影响之间的关系


计算机程序的识别:通过对代码、AST ……等 Raw Data 分析,借助 NLP 技术识别计算机程序中,语言的表达能力、语言的结构、语言中的逻辑、语言和外部系统通过 API 的交互等


日志和数据的识别:通过对日志和数据进行 NLP、回归、统计分析等方式,识别出程序的可用性、性能、易用性等指标情况,并识别出影响这些指标的日志和数据出自哪里,找出其间的关系


理解


横向跨领域的理解:对识别出的概念进行降维,从而在底层更抽象的维度上找出不同领域之间概念的映射关系,从而实现用不同领域的概念进行类比,进而在某领域内理解其它领域的概念


纵向跨层次的理解:利用机器学习和深度学习的 AI 算法能力,放宽不同层次间概念的组成关系,对低层次概念实现跨层次的理解,进而形成更加丰富的技术、业务能力供给和使用机会


常识、通识的理解:以常识、通识构建的知识图谱为基础,将 AI 所面对的开放性问题领域化,将领域内的常识和通识当做理解的基础,不是臆测和猜想,而是实实在在构建在理论基础上的理解


表达


个性化:借助大数据和算法实现用户和软件功能间的匹配,利用 AI 的生成能力降低千人前面的研发成本,从而真正实现个性化的软件服务能力,把软件即服务推向极致


共情:利用端智能在用户侧部署算法模型,既可以解决用户隐私保护的问题,又可以对用户不断变化的情绪、诉求、场景及时学习并及时做出响应,从而让软件从程序功能的角度急用户之所急、想用户之所想,与用户共情、让用户共鸣。举个例子:我用 iPhone 在进入地铁站的时候,因为现在要检查健康码,每次进入地铁站 iOS 都会给我推荐支付宝快捷方式,我不用自己去寻找支付宝打开展示健康码,这就让我感觉 iOS 很智能、很贴心,这就是共情。


后记


从提出前端智能化这个概念到现在已历三年,最初,保持着“让前端跟上 AI 发展的浪潮”的初心上路,到“解决一线研发问题”发布 imgcook.com ,再到“给前端靠谱的机器学习框架”开源github.com/alibaba/pip…


这一路走来,几乎日日夜不能寐。真正想从本质上颠覆现在的编程模式和研发模式谈何容易?这个过程中,我们从一群纯前端变成前端和 AI 的跨界程序员,开发方式从写代码到机器生成,周围的人从作壁上观到积极参与,正所谓:念念不忘,必有回响。低代码/无代码开发方兴未艾,广大技术、科研人员在这个方向上厉兵秣马,没有哪个方法是 Silverbullet ,也没有哪个理论是绝对正确的,只要找到你心中所爱,坚持研究和实践,终会让所有人都能够自定义软件来操作日益复杂和强大的硬件设备,终能让所有人更加便捷、直接、有效的接入数字世界,终于在本质上将软件开发和软件工程领域重新定义!共勉!



链接:https://juejin.cn/post/6970962024557707278

收起阅读 »

细说浏览器输入URL后发生了什么

细说浏览器输入URL后发生了什么总体概览大体上,可以分为六步,当然每一步都可以详细都展开来说,这里先放一张总览图:DNS域名解析在网络世界,你肯定记得住网站的名称,但是很难记住网站的 IP 地址,因而也需要一个地址簿,就是 DNS 服务器。DNS 服务器是高可...
继续阅读 »

细说浏览器输入URL后发生了什么

总体概览

大体上,可以分为六步,当然每一步都可以详细都展开来说,这里先放一张总览图:


DNS域名解析

在网络世界,你肯定记得住网站的名称,但是很难记住网站的 IP 地址,因而也需要一个地址簿,就是 DNS 服务器。DNS 服务器是高可用、高并发和分布式的,它是树状结构,如图:


  • 根 DNS 服务器 :返回顶级域 DNS 服务器的 IP 地址

  • 顶级域 DNS 服务器:返回权威 DNS 服务器的 IP 地址

  • 权威 DNS 服务器 :返回相应主机的 IP 地址


DNS的域名查找,在客户端和浏览器,本地DNS之间的查询方式是递归查询;在本地DNS服务器与根域及其子域之间的查询方式是迭代查询;


递归过程:

GitHub


在客户端输入 URL 后,会有一个递归查找的过程,从浏览器缓存中查找->本地的hosts文件查找->找本地DNS解析器缓存查找->本地DNS服务器查找,这个过程中任何一步找到了都会结束查找流程。


如果本地DNS服务器无法查询到,则根据本地DNS服务器设置的转发器进行查询。若未用转发模式,则迭代查找过程如下图:

GitHub


结合起来的过程,可以用一个图表示:

GitHub

在查找过程中,有以下优化点:



  • DNS存在着多级缓存,从离浏览器的距离排序的话,有以下几种: 浏览器缓存,系统缓存,路由器缓存,IPS服务器缓存,根域名服务器缓存,顶级域名服务器缓存,主域名服务器缓存。

  • 在域名和 IP 的映射过程中,给了应用基于域名做负载均衡的机会,可以是简单的负载均衡,也可以根据地址和运营商做全局的负载均衡。


建立TCP连接


首先,判断是不是https的,如果是,则HTTPS其实是HTTP + SSL / TLS 两部分组成,也就是在HTTP上又加了一层处理加密信息的模块。服务端和客户端的信息传输都会通过TLS进行加密,所以传输的数据都是加密后的数据。


进行三次握手,建立TCP连接。




  1. 第一次握手:建立连接。客户端发送连接请求报文段,将SYN位置为1,Sequence Number为x;然后,客户端进入SYN_SEND状态,等待服务器的确认;




  2. 第二次握手:服务器收到SYN报文段。服务器收到客户端的SYN报文段,需要对这个SYN报文段进行确认,设置Acknowledgment Number为x+1(Sequence Number+1);同时,自己还要发送SYN请求信息,将SYN位置为1,Sequence Number为y;服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时服务器进入SYN_RECV状态;




  3. 第三次握手:客户端收到服务器的SYN+ACK报文段。然后将Acknowledgment Number设置为y+1,向服务器发送ACK报文段,这个报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。




SSL握手过程



  1. 第一阶段 建立安全能力 包括协议版本 会话Id 密码构件 压缩方法和初始随机数

  2. 第二阶段 服务器发送证书 密钥交换数据和证书请求,最后发送请求-相应阶段的结束信号

  3. 第三阶段 如果有证书请求客户端发送此证书 之后客户端发送密钥交换数据 也可以发送证书验证消息

  4. 第四阶段 变更密码构件和结束握手协议


完成了之后,客户端和服务器端就可以开始传送数据。更多 HTTPS 的资料可以看这里:



备注


ACK:此标志表示应答域有效,就是说前面所说的TCP应答号将会包含在TCP数据包中;有两个取值:0和1,为1的时候表示应答域有效,反之为0。TCP协议规定,只有ACK=1时有效,也规定连接建立后所有发送的报文的ACK必须为1。


SYN(SYNchronization):在连接建立时用来同步序号。当SYN=1而ACK=0时,表明这是一个连接请求报文。对方若同意建立连接,则应在响应报文中使SYN=1和ACK=1. 因此, SYN置1就表示这是一个连接请求或连接接受报文。


FIN(finis)即完,终结的意思, 用来释放一个连接。当 FIN = 1 时,表明此报文段的发送方的数据已经发送完毕,并要求释放连接。


发送HTTP请求,服务器处理请求,返回响应结果


TCP连接建立后,浏览器就可以利用HTTP/HTTPS协议向服务器发送请求了。服务器接受到请求,就解析请求头,如果头部有缓存相关信息如if-none-match与if-modified-since,则验证缓存是否有效,若有效则返回状态码为304,若无效则重新返回资源,状态码为200.


这里有发生的一个过程是HTTP缓存,是一个常考的考点,大致过程如图:

GitHub

其过程,比较多内容,可以参考我的这篇文章《浏览器相关原理(面试题)详细总结一》,这里我就不详细说了~


关闭TCP连接




  1. 第一次分手:主机1(可以使客户端,也可以是服务器端),设置Sequence Number和Acknowledgment Number,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了;




  2. 第二次分手:主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknowledgment Number为Sequence Number加1;主机1进入FIN_WAIT_2状态;主机2告诉主机1,我"同意"你的关闭请求;




  3. 第三次分手:主机2向主机1发送FIN报文段,请求关闭连接,同时主机2进入LAST_ACK状态;




  4. 第四次分手:主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段以后,就关闭连接;此时,主机1等待2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了。




浏览器渲染


按照渲染的时间顺序,流水线可分为如下几个子阶段:构建 DOM 树、样式计算、布局阶段、分层、栅格化和显示。如图:

GitHub



  1. 渲染进程将 HTML 内容转换为能够读懂DOM 树结构。

  2. 渲染引擎将 CSS 样式表转化为浏览器可以理解的styleSheets,计算出 DOM 节点的样式。

  3. 创建布局树,并计算元素的布局信息。

  4. 对布局树进行分层,并生成分层树。

  5. 为每个图层生成绘制列表,并将其提交到合成线程。合成线程将图层分图块,并栅格化将图块转换成位图。

  6. 合成线程发送绘制图块命令给浏览器进程。浏览器进程根据指令生成页面,并显示到显示器上。


构建 DOM 树


浏览器从网络或硬盘中获得HTML字节数据后会经过一个流程将字节解析为DOM树,先将HTML的原始字节数据转换为文件指定编码的字符,然后浏览器会根据HTML规范来将字符串转换成各种令牌标签,如html、body等。最终解析成一个树状的对象模型,就是dom树。

GitHub


具体步骤:



  1. 转码(Bytes -> Characters)—— 读取接收到的 HTML 二进制数据,按指定编码格式将字节转换为 HTML 字符串

  2. Tokens 化(Characters -> Tokens)—— 解析 HTML,将 HTML 字符串转换为结构清晰的 Tokens,每个 Token 都有特殊的含义同时有自己的一套规则

  3. 构建 Nodes(Tokens -> Nodes)—— 每个 Node 都添加特定的属性(或属性访问器),通过指针能够确定 Node 的父、子、兄弟关系和所属 treeScope(例如:iframe 的 treeScope 与外层页面的 treeScope 不同)

  4. 构建 DOM 树(Nodes -> DOM Tree)—— 最重要的工作是建立起每个结点的父子兄弟关系


样式计算


渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式。


CSS 样式来源主要有 3 种,分别是通过 link 引用的外部 CSS 文件、style标签内的 CSS、元素的 style 属性内嵌的 CSS。,其样式计算过程主要为:

GitHub

可以看到上面的 CSS 文本中有很多属性值,如 2em、blue、bold,这些类型数值不容易被渲染引擎理解,所以需要将所有值转换为渲染引擎容易理解的、标准化的计算值,这个过程就是属性值标准化。处理完成后再处理样式的继承和层叠,有些文章将这个过程称为CSSOM的构建过程。


页面布局


布局过程,即排除 script、meta 等功能化、非视觉节点,排除 display: none 的节点,计算元素的位置信息,确定元素的位置,构建一棵只包含可见元素布局树。如图:

GitHub

其中,这个过程需要注意的是回流和重绘,关于回流和重绘,详细的可以看我另一篇文章《浏览器相关原理(面试题)详细总结二》,这里就不说了~


生成分层树


页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree),如图:

GitHub

如果你熟悉 PS,相信你会很容易理解图层的概念,正是这些图层叠加在一起构成了最终的页面图像。在浏览器中,你可以打开 Chrome 的"开发者工具",选择"Layers"标签。渲染引擎给页面分了很多图层,这些图层按照一定顺序叠加在一起,就形成了最终的页面。


并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。那么需要满足什么条件,渲染引擎才会为特定的节点创建新的层呢?详细的可以看我另一篇文章《浏览器相关原理(面试题)详细总结二》,这里就不说了~


栅格化


合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。如图:


GitHub


通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做视口(viewport)。在有些情况下,有的图层可以很大,比如有的页面你使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。


显示


最后,合成线程发送绘制图块命令给浏览器进程。浏览器进程根据指令生成页面,并显示到显示器上,渲染过程完成。




链接:https://juejin.cn/post/6844904054074654728
收起阅读 »

浏览器工作原理&前端安全

网络安全 三原则 在传输中,不允许明文传输用户隐私数据; 在本地,不允许明文保存用户隐私数据; 在服务器,不允许明文保存用户隐私数据; http是明文传输,WiFi、路由器、运营商、机房等多个物理设备节点,如果在这中间任意一个节点被监听,传输的内容就会完全...
继续阅读 »

网络安全


三原则



  1. 在传输中,不允许明文传输用户隐私数据;

  2. 在本地,不允许明文保存用户隐私数据;

  3. 在服务器,不允许明文保存用户隐私数据;


http是明文传输,WiFi、路由器、运营商、机房等多个物理设备节点,如果在这中间任意一个节点被监听,传输的内容就会完全暴露,,这一攻击手法叫做MITM(Man In The Middle)中间人攻击。
在网络来说,我们知道不论 POST 请求和 GET 请求都会被抓包,在没有使用 HTTPS 的情况下,抓包我们是防不住的,如果明文传输用户隐私,那后果就不说了。


很多用户密码是通用的,一旦被不法分子窃取,去其他网站撞库,造成损失。
上文说到http传输因为有三大风险



  • 窃听风险(eavesdropping):第三方可以获知通信内容。

  • 篡改风险(tampering):第三方可以修改通信内容。

  • 冒充风险(pretending):第三方可以冒充他人身份参与通信。


所以提到了https
https 可以认为是 http + TLS TLS 是传输层加密协议,它的前身是 SSL 协议,如果没有特别说明,SSL 和 TLS 说的都是同一个协议。


加密传输(避免明文传输)


1. 对称加密

加解密使用同一个密钥
客户端和服务端进行通信,采用对称加密,如果只使用一个秘钥,很容易破解;如果每次用不同的秘钥,海量秘钥的管理和传输成本又会比较高。


2.非对称加密

需要两个密钥来进行加密和解密,这两个秘钥是公开密钥(public key,简称公钥)和私有密钥(private key,简称私钥)
非对称加密的模式则是:




  • 乙方生成两把密钥(公钥和私钥)。公钥是公开的,任何人都可以获得,私钥则是保密的




  • 甲方获取乙方的公钥,然后用它对信息加密




  • 乙方得到加密后的信息,用私钥解密。


    但当服务端要返回数据,如果用公钥加密,那么客户端并没有私钥用来解密,而如果用私钥加密,客户端虽然有公钥可以解密,但这个公钥之前就在互联网上传输过,很有可能已经有人拿到,并不安全,所以这一过程只用非对称加密是不能满足的。
    (严格来讲,私钥并不能用来加密,只能用作签名使用,这是由于密码学中生成公钥私钥时对不同变量的数学要求是不同的,因此公钥私钥抵抗攻击的能力也不同)
    所以为了满足即使非对称




image.png


https


HTTPS 的出发点是解决HTTP明文传输时信息被篡改和监听的问题。




  • 为了兼顾性能和安全性,使用了非对称加密+对称加密的方案。




  • 为了保证公钥传输中不被篡改,又使用了非对称加密的数字签名功能,借助CA机构和系统根证书的机制保证了HTTPS证书的公信力。


    只传递证书、明文信息、加签加密后的明文信息,注意不传递CA公钥(防止中间人攻击),客户端浏览器可以通过系统根证书拿到CA公钥。(系统或浏览器中内置的CA机构的证书和公钥成为了至关重要的环节)




加密存储
千万不要用明文存储密码
如果用明文存储密码(不管是存在数据库还是日志中),一旦数据泄露,所有用户的密码就毫无保留地暴露在黑客的面前,开头提到的风险就可能发生,那我们费半天劲加密传输密码也失去了意义。


总结
如果我们想要尽可能保证用户的信息安全,我们需要做以下的工作



  • 使用https请求

  • 利用RSA加密密码并传输数据

  • 用BCrypt或者PBKDF2单向加密,并存储


强制使用HTTPS


一些网站购买了SSL证书并将其配置到Web服务器上,以为这就算完事儿了。但这只是表明你启用了HTTPS选项,而用户很可能不会注意到。为确保每个用户都从HTTPS中受益,你应该将所有传入的HTTP请求重定向至HTTPS。这意味着任何一个访问你的网站的用户都将自动切换到HTTPS,从那以后他们的信息传输就安全了。


配合cookie的secure参数,禁止cookie在最初的http请求中被带出去(中间人拦截)。


TCP三次握手四次挥手


Tcp是传输控制协议(Transmission Control Protocol)是为了在不可靠的互联网络上提供可靠的端到端字节流而专门设计的一个传输协议


第一次握手:请求连接client->SYN=1, 随机seq=x(数据包首字节序列号)
第二次握手:同意应答,SYN和ACK都置为1,ack=x+1,随机seq=y,返回确认连接
第三次握手:client检查ack是否为x+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=y+1;——>Server,Server检查ack是否为y+1,ACK是否为1,正确则连接成功!


认证授权+浏览器存储


什么是认证(Authentication)

验证当前用户的身份,证明“你是你自己”
互联网中的认证:



  • 用户名密码登录

  • 邮箱发送登录链接

  • 手机号接收验证码


什么是授权(Authorization)

用户授予第三方应用访问该用户某些资源的权限
安装手机应用时(是否允许访问相册、地理位置等权限)
登录微信小程序(是否允许获取昵称、头像、地区、性别等个人信息)



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


什么是凭证(Credentials)

实现认证和授权的前提是需要一种媒介(证书) 来标记访问者的身份
登录成功后,服务器给用户使用的浏览器颁发一个令牌,表明身份,每次请求时带上。


什么是 Cookie


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

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

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


特点:
Cookie 的大小受限,一般为 4 KB;
同一个域名下存放 Cookie 的个数是有限制的,不同浏览器的个数不一样,一般为 20 个;
Cookie 支持设置过期时间,当过期时自动销毁;(max-age单位秒,如果是负数,为临时cookie关闭浏览器失效;默认是-1)
每次发起同域下的 HTTP 请求时,都会携带当前域名下的 Cookie;
支持设置为 HttpOnly,防止 Cookie 被客户端的 JavaScript 访问


什么是 Session


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

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


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


什么是localStorage

特点



  • 大小限制为 5MB ~10MB;

  • 在同源的所有标签页和窗口之间共享数据;

  • 数据仅保存在客户端,不与服务器进行通信;

  • 数据持久存在且不会过期,重启浏览器后仍然存在;

  • 对数据的操作是同步的。


什么是sessionStorage


  • sessionStorage 的数据只存在于当前浏览器的标签页;

  • 数据在页面刷新后依然存在,但在关闭浏览器标签页之后数据就会被清除;

  • 与 localStorage 拥有统一的 API 接口;

  • 对数据的操作是同步的。


什么是 Token(令牌)


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

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


特点:



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

  • 支持移动端设备

  • 安全

  • 支持跨程序调用


什么是 JWT


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


方式:通过Authorization;通过url;跨域的时候,可以把 JWT 放在 POST 请求的数据体里


和session、token的区别是JWT已经包含用户信息,所以不用再去数据库里查询了,而且


什么是 XSS

Cross-Site Scripting(跨站脚本攻击),是一种代码注入攻击



  • 存储性(任何可输入存入数据库的地方,注入脚本,服务端渲染时将脚本拼接html中返回给浏览器)

  • 反射性(脚本写入url,如路由传参,诱导用户点击,服务端渲染时将脚本拼接html中返回给浏览器)

  • DOM性(脚本写入url,前端 JavaScript 取出 URL 中的恶意代码并执行)


防范:cookie设置readOnly禁止js脚本访问cookie
前端服务端对输入框设置格式检查
转义 HTML(存储、反射)
改成纯前端渲染(存储、反射)
使用react就在前端 render 阶段避免 innerHTML、outerHTML 的 XSS 隐患用.textContent、.setAttribute()。


什么是 CSRF
跨站请求伪造(英语:Cross-site request forgery)
用户已经登录了安全网站A,诱导用户访问网站B,B利用A获取的凭证去访问A,绕过用户验证



  • 1.登录受信任网站A,并在本地生成Cookie。

  • 2.在不登出A的情况下,访问危险网站B。


防范:同源策略(origin referrer) token samesite


Base64编码由来


因为有些网络传送渠道并不支持所有的字节,例如传统的邮件只支持可见字符的传送,像ASCII码的控制字符就不能通过邮件传送。Base64就是一种基于64个可打印字符来表示二进制数据的表示方法。


ASCII码
在计算机中,所有的数据在存储和运算时都要使用二进制数表示,像a、b、c、d这样的52个字母(包括大写)以及0、1等数字还有一些常用的符号(例如*、#、@等)在计算机中存储时也要使用二进制数来表示,用来统一规定上述常用符号用哪些二进制数来表示


unicode、utf-8、ASCII、base64、哈希md5
ASCII美国信息互换标准代码,用一个字节存储128个字符(其中包括33个控制字符(具有某些特殊功能但是无法显示的字符)
产生原因:
在计算机中,所有的数据在存储和运算时都要使用二进制数表示(因为计算机用高电平和低电平分别表示1和0),例如,像a、b、c、d这样的52个字母(包括大写)以及0、1等数字还有一些常用的符号(例如*、#、@等)在计算机中存储时也要使用二进制数来表示,而具体用哪些二进制数字表示哪个符号,当然每个人都可以约定自己的一套(这就叫编码),而大家如果要想互相通信而不造成混乱,那么大家就必须使用相同的编码规则,于是美国有关的标准化组织就出台了ASCII编码,统一规定了上述常用符号用哪些二进制数来表示 [2]  。


Base64是网络上最常见的用于传输8Bit字节码的编码方式之一,Base64就是一种基于64个可打印字符来表示二进制数据的方法。.Base64编码是从二进制到字符的过程


浏览器工作原理


异步编程


与同步相对的异步则可以理解为在异步操作完成后所要做的任务,它们通常以回调函数或者 Promise 的形式被放入事件队列,再由事件循环 ( Event Loop ) 机制在每次轮询时检查异步操作是否完成,若完成则按事件队列里面的执行规则来依次执行相应的任务。


javascript的运行机制(单线程、任务队列、EventLoop、微任务、宏任务)


单线程特点


单线程可以避免多线程操作带来的复杂的同步问题。


任务队列(JavaScript的运行机制)


  1. 所有同步任务都在主线程上执行,形成一个执行栈。

  2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

  3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。


Event Loop

每次 Tick 会查看任务队列中是否有需要执行的任务。每次 Tick 的过程就是查看是否有待处理事件,如果有则取出相关事件及回调函数放入执行栈中由主线程执行。待处理的事件会存储在一个任务队列中
1. onclick 由浏览器内核的 DOM Binding 模块来处理,当事件触发的时候,回调函数会立即添加到任务队列中。
2. setTimeout 会由浏览器内核的 timer 模块来进行延时处理,当时间到达的时候,才会将回调函数添加到任务队列中。
3. ajax 则会由浏览器内核的 network 模块来处理,在网络请求完成返回之后,才将回调添加到任务队列中。


javascript是单线程的,浏览器是多线程的。
进程和线程都是操作系统的概念,进程是应用程序的执行实例,每个进程是由私有的虚拟地址空间、代码、数据和其它各种系统资源组成,即进程是操作系统进行资源分配和独立运行的最小单元。


进程(process)


进程是应用程序的执行实例,每个进程是由私有的虚拟地址空间、代码、数据和其它各种系统资源组成,即进程是操作系统进行资源分配和独立运行的最小单元。
当我们启动一个应用,计算机会至少创建一个进程,cpu会为进程分配一部分内存,应用的所有状态都会保存在这块内存中,应用也许还会创建多个线程来辅助工作,这些线程可以共享这部分内存中的数据。如果应用关闭,进程会被终结,操作系统会释放相关内存。


线程(thread)



  • 进程内部的一个执行单元,是被系统独立调度和分派的基本单位。系统创建好进程后,实际上就启动执行了该进程的主执行线程

  • 进程就像是一个有边界的生产厂间,而线程就像是厂间内的一个个员工,可以自己做自己的事情,也可以相互配合做同一件事情,所以一个进程可以创建多个线程。

  • 线程自己不需要系统重新分配资源,它与同属一个进程的其它线程共享当前进程所拥有的全部资源。 PS: 进程之间是不共享资源和地址空间的,所以不会存在太多的安全问题,而由于多个线程共享着相同的地址空间和资源,所以会存在线程之间有可能会恶意修改或者获取非授权数据等复杂的安全问题。



Chrome 采用多进程架构


主要进程



  • Browser Process 浏览器的主进程(负责协调、主控) (1)负责包括地址栏,书签栏,前进后退按钮等部分的工作 (2)负责处理浏览器的一些不可见的底层操作,比如网络请求和文件访问 (3)负责各个页面的管理,创建和销毁其他进程

  • Renderer Process 负责一个 tab 内关于网页呈现的所有事情,页面渲染,脚本执行,事件处理等


image.png



  • Plugin Process 负责控制一个网页用到的所有插件,如 flash 每种类型的插件对应一个进程,仅当使用该插件时才创建

  • GPU Process 负责处理 GPU 相关的任务,3D 绘制等


优点
由于默认 新开 一个 tab 页面 新建 一个进程,所以单个 tab 页面崩溃不会影响到整个浏览器。
同样,第三方插件崩溃也不会影响到整个浏览器。
多进程可以充分利用现代 CPU 多核的优势。


缺点
系统为浏览器新开的进程分配内存、CPU 等资源,所以内存和 CPU 的资源消耗也会更大。
不过 Chrome 在内存释放方面做的不错,基本内存都是能很快释放掉给其他程序运行的。


一个浏览器至少实现三个常驻线程:JavaScript引擎线程,GUI渲染线程,浏览器事件触发线程。


1.JavaScript引擎是基于事件驱动单线程执行的,JavaScript引擎一直等待着任务队列中任务的到来,然后加以处理,浏览器无论什么时候都只有一个JavaScript线程在运行JavaScript程序。


2.GUI渲染线程负责渲染浏览器界面,当界面需要重绘(Repaint)或由于某种操作引发回流(Reflow)时,该线程就会执行。但需要注意,GUI渲染线程与JavaScript引擎是互斥的,当JavaScript引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JavaScript引擎空闲时立即被执行。


3.事件触发线程,当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JavaScript引擎的处理。这些事件可来自JavaScript引擎当前执行的代码块如setTimeout、也可来自浏览器内核的其他线程如鼠标点击、Ajax异步请求等,但由于JavaScript的单线程关系所有这些事件都得排队等待JavaScript引擎处理(当线程中没有执行任何同步代码的前提下才会执行异步代码)


问题



  1. 为什么 Javascript 要是单线程的 ?


JavaScript 为处理页面中用户的交互,以及操作 DOM 树、CSS 样式树来给用户呈现一份动态而丰富的交互体验和服务器逻辑的交互处理。
如果 JavaScript 是多线程的方式来操作这些 UI DOM,则可能出现 UI 操作的冲突。但为了避免因为引入了锁而带来更大的复杂性,Javascript 在最初就选择了单线程执行。



  1. 为什么 JS 阻塞页面加载 ?


由于 JavaScript 是可操纵 DOM 的,如果在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。所以为了防止渲染的不可预期结果,浏览器设置 GUI 渲染线程与 JavaScript 引擎为互斥的关系。



  1. css 加载会造成阻塞吗 ?


CSS 加载不会阻塞 DOM 的解析(并行), Render Tree 是依赖于 DOM Tree 和 CSSOM Tree 的所以CSS 加载会阻塞 Dom 的渲染,同时css 会阻塞后面 js 的执行



  1. 什么是 CRP,即关键渲染路径(Critical Rendering Path)? 如何优化 ?


image.png


Html可以逐步解析,和css解析是并行的,但是css不行,因为css的每个属性都是可以改变cssom的,比如后面的把前面设置的font-size覆盖等,所以必须等cssom构建完毕才能进入下一个阶段。CSS的加载速度与构建CSSOM的速度将直接影响首屏渲染速度,因此在默认情况下CSS被视为阻塞渲染的资源。


通常情况下DOM和CSSOM是并行构建的,但是当浏览器遇到一个script标签时,DOM构建将暂停,直至脚本完成执行。但由于JavaScript可以修改CSSOM,所以需要等CSSOM构建完毕后再执行JS。


优化围绕三因素


关键资源数量(js、css)


关键路径长度


关键字节的数量(字节越小、下载和处理速度都会更快——压缩)


具体做法:


优化dom


html文件尽可能小,删除冗余代码,压缩代码,使用缓存(http cache)


优化cssom


仅把首屏需要的css通过style标签内嵌到head里,其余的使用异步方式非阻塞加载(如Critical CSS)


避免使用@import


@import会把css引入从并行变成串行加载


异步js


所有文本资源都应该尽可能小,删除未使用的代码、缩小文件的尺寸(Minify)、使用gzip压缩(Compress)、使用缓存(HTTP Cache)


可以为script添加async属性异步加载


5.从输入url浏览器渲染的流程。


解析 HTML 文件,构建 DOM 树,同时浏览器主进程负责下载 CSS 文件
CSS 文件下载完成,解析 CSS 文件成树形的数据结构,然后结合 DOM 树合并成 RenderObject 树
布局 RenderObject 树 (Layout/reflow),负责 RenderObject 树中的元素的尺寸,位置等计算
绘制 RenderObject 树 (paint),绘制页面的像素信息
浏览器主进程将默认的图层和复合图层交给 GPU 进程,GPU 进程再将各个图层合成(composite),最后显示出页面


6.Event Loop至少包含两个队列,macrotask队列和microtask队列


async/await成对出现,async标记的函数会返回一个Promise对象,可以使用then方法添加回调函数。await后面的语句会同步执行。但 await 下面的语句会被当成微任务添加到当前任务队列的末尾异步执行。


先微后宏


回流 (Reflow)


当Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。
会导致回流的操作:



  • 页面首次渲染

  • 浏览器窗口大小发生改变

  • 元素尺寸或位置发生改变

  • 元素内容变化(文字数量或图片大小等等)

  • 元素字体大小变化

  • 添加或者删除可见的DOM元素

  • 激活CSS伪类(例如::hover)

  • 查询某些属性或调用某些方法


重绘 (Repaint)


当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。


回流比重绘的代价要更高。
有时即使仅仅回流一个单一的元素,它的父元素以及任何跟随它的元素也会产生回流。



  1. 多线程的优点和缺点分别是什么?


优点:


1、将耗时较长的操作(网络请求、图片下载、音频下载、数据库访问等)放在子线程中执行,可以防止主线程的卡死;


2、可以发挥多核处理的优势,提升cpu的使用率。


缺点:


1、每开辟一个子线程就消耗一定的资源;


2、会造成代码的可读性变差;


3、如果出现多个线程同时访问一个资源,会出现资源争夺的情况。


链接:https://juejin.cn/post/6953482213845368863

收起阅读 »

使用transform和left改变位置的性能区别

使用transform和left改变位置的性能区别现如今大多数设备的屏幕刷新频率是60Hz,也就是每秒钟屏幕刷新60次;因此网页动画的运行速度只要达到60FPS,我们就会觉得动画很流畅。F(Frames) P(Per) S(Second) 指的画面每秒钟传输的...
继续阅读 »

使用transform和left改变位置的性能区别

现如今大多数设备的屏幕刷新频率是60Hz,也就是每秒钟屏幕刷新60次;因此网页动画的运行速度只要达到60FPS,我们就会觉得动画很流畅。

F(Frames) P(Per) S(Second) 指的画面每秒钟传输的帧数,60FPS指的是每秒钟60帧;换算下来每一帧差不多是16毫秒。 (1 秒 = 1000 毫秒) / 60 帧 = 16.66 毫秒/帧 复制代码但通常浏览器需要花费一些时间将每一帧的内容绘制到屏幕上(包括样式计算、布局、绘制、合成等工作),所以通常我们只有10毫秒来执行JS代码。

那么动画只要接近于60FPS就是比较流畅的,对比一下通过position:left 做动画和transform做动画的性能区别

假设每个人都是用性能最好的手机,浏览器,我们根本用不着去做性能优化,所以在这里为了效果明显,先将环境配置到较低,较差的情况下测试,动画也不能设置为单一的移动

1如何使用google开发者工具查看帧数

1.先按键盘F12, 然后点到performance

2.点击刷新按钮再按确定

image.png

3.把鼠标放在下面就是他对应的帧数

test5.gif

4.现在的浏览器(google为例)已经默认开启了硬件加速器,所以你去对比left和transform其实效果非常不明显,所以先把这个默认关掉

image.png

5.对比效果,应该是在低cpu的情况下测试,将他设置为6

test7.gif

6 查看GPU的使用

image.png

如果你是mac,勾选fps meter, 如果你是windows,勾选我上面写的

我是windows,但是我并看不到帧率的时时变化

7 如果你想查看层级

检查-> layers -> 选择那个可旋转的 -> 查看元素paint code的变化

如果你发现你没有layers, 可以看看三个点里面的more tools,把layers点出来

image.png

4transformcode.gif

2使用position:left (使用left并没有被提升到复合层)

<div class="ball-running"></div>
.ball-running {
width: 100px;
height: 100px;
animation: run-around 2s linear alternate 100;
background: red;
position: absolute;
border-radius: 50%;
}
@keyframes run-around {
0% {
top: 0;
left: 0;
}
25% {
top: 0;
left: 200px;
}
50% {
top: 200px;
left: 200px;
}
75% {
top: 200px;
left: 0;
}
}

3transformcode.gif


test2.gif


在cpu 4slown down的情况下,我们可以看到上面的FPS刚开始在60左右,后面掉到了4FPS,这个动画是不够流畅的.
帧率呈现出锯齿型


这是对应的帧率


image.png


在cpu6 slow down的帧率下甚至会出现掉帧的情况(下面那些红色的就是dropped frame)


test5.gif


3.使用transform进行做动画(transform提升到了复合层)

.ball-running {
width: 100px;
height: 100px;
animation: run-around 2s linear alternate 100;
background: red;
border-radius: 50%;
}
@keyframes run-around {
0% {
transform: translate(0, 0);
}
25% {
transform: translate(200px, 0);
}
50% {
transform: translate(200px, 200px);
}
75% {
transform: translate(0, 200px);
}
}

1transformcode.gif


4.从层级方向解释transform性能优于left


建议看这篇文章:
浏览器层合成与页面渲染优化


基本的渲染流程:


image.png


从左往右边看,我们可以看到,浏览器渲染过程如下:


1.解析HTML,生成DOM树,解析CSS,生成CSSOM树
(CSS Object Model ,CSS 对象模型,里面有很多api,包括style rules-内部样式表中所有的CSS规则)
2.将DOM树和CSSOM树结合,生成渲染树(Render Tree)
3.Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)
4.Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
5.display:将像素发送给GPU,展示在页面上。
复制代码

先了解下什么是渲染层


渲染层: 在 DOM 树中每个节点都会对应一个渲染对象(RenderObject),
当它们的渲染对象处于相同的坐标空间(z 轴空间)时,就会形成一个 RenderLayers,也就是渲染层。
复制代码

1先不涉及任何的层级问题

<div class="big">
</div>
<div class="small"></div>
<style>
.big {
width: 200px;
height: 200px;
background-color: yellow;
}
.small {
width: 100px;
height: 100px;
background-color: red;
}
</style>

1普通的代码.gif

从上面来看,只有一个渲染层

2加上index

<div class="big">
</div>
<div class="small"></div>
<style>
.big {
width: 200px;
height: 200px;
background-color: yellow;
position: relative;
}
.small {
width: 100px;
height: 100px;
background-color: red;
position: absolute;
top: 0px;
left: 0px;
z-index: 10000;
}
</style>

1zindex.gif

从视觉上来看,small 的div确实是在big之上,但是和big在同一个渲染层上

3加上transform

<div class="big">
</div>
<div class="small"></div>
<style>
.big {
width: 200px;
height: 200px;
background-color: yellow;
position: relative;
}
.small {
width: 100px;
height: 100px;
background-color: red;
position: absolute;
top: 0px;
left: 0px;
z-index: 10000;
transform: translateZ(0);
}

1transform.gif

如何形成合成层


上面产生了一个新的层级,也就是合成层


首先,如果你改了一个影响元素布局信息的CSS样式,比如width、height、left、top等(transform除外),那么浏览器会将当前的Layout标记为dirty,因为元素的位置信息变了,将可能会导致整个网页其他元素的位置情况都发生改变,所以需要执行Layout全局重新计算每个元素的位置,如果提升为合成层能够开启gpu加速,并且在渲染的时候不会影响其他的层


并且在使用left的时候,document的paint code一直在变化,而使用transform的paint code一直都是不变的,可看上面的动画gif


有关于层级方面的东西,希望大家共同交流,我觉得自己也没有深刻的了解有些定义,只写了自己会的理解的,希望在查看操作方面能帮到大家



链接:https://juejin.cn/post/6959089368212439076
收起阅读 »

5个 Chrome 调试混合应用的技巧

对前端开发人员来说,Chrome 真是一个必备的开发工具,大到页面展示,小到 BUG 调试/HTTP 抓包等,本文我将和大家分享自己做混合应用开发过程中经常用到的几个调试技巧。一、调试安卓应用 在进行混合应用开发过程中,经常需要在安卓应用中调试 H5 项目的代...
继续阅读 »

对前端开发人员来说,Chrome 真是一个必备的开发工具,大到页面展示,小到 BUG 调试/HTTP 抓包等,本文我将和大家分享自己做混合应用开发过程中经常用到的几个调试技巧。

一、调试安卓应用


在进行混合应用开发过程中,经常需要在安卓应用中调试 H5 项目的代码,这里我们就需要了解安卓应用如何在 Chrome 上进行调试。
接下来简单介绍一下,希望大家还是能实际进行调试看看:


1. 准备工作


需要准备有以下几个事项:



  1. 安卓包必须为可调试包,如果不可以调试,可以找原生的同事提供;

  2. 安卓手机通过数据线连接电脑,然后开启“开发者模式”,并启用“USB 调试”选项。


2. Chrome 启动调试页面


在 Chrome 浏览器访问“chrome://inspect/#devices”,然后在 WebView 列表中选择你要调试的页面,点击“ Inspect ”选项,跟调试 PC 网页一样,使用 Chrome 控制台进行调试。



然后就可以正常进行调试了,操作和平常 Chrome 上面调试页面是一样的。


3. 注意


如果访问 “chrome://inspect/#devices” 页面会一直提示 404,可以在翻墙情况下,先在 Chrome 访问 chrome-devtools-frontend.appspot.com,然后重新访问“chrome://inspect/#devices”即可。

二、筛选特定条件的请求


在 Network 面板中,我们可以在 Filter 输入框中,通过各种筛选条件,来查看满足条件的请求。



  1. 使用场景:


如只需要查看失败或者符合指定 URL 的请求。



  1. 使用方式:


在 Network 面板在 Filter 输入框中,输入各种筛选条件,支持的筛选条件包括:文本、正则表达式、过滤器和资源类型。
这里主要介绍“过滤器”,包括:


这里输入“-”目的是为了让大家能看到 Chrome 提供哪些高级选项,在使用的时候是不需要输入“-”。
如果输入“-.js -.css”则可以过滤掉“.js”和“.css”类型的文件。


关于过滤器更多用法,可以阅读《Chrome DevTools: How to Filter Network Requests》



三、快速断点报错信息


在 Sources 面板中,我们可以开启异常自动断点的开关,当我们代码抛出异常,会自动在抛出异常的地方断点,能帮助我们快速定位到错误信息,并提供完整的错误信息的方法调用栈。
3速断点报错信息.png



  1. 使用场景:


需要调试抛出异常的情况。



  1. 使用方式:


在 Sources 面板中,开启异常自动断点的开关。
3快速断点报错信息.gif


四、断点时修改代码


在 Sources 面板中,我们可以在需要断点的行数右击,选择“Add conditional breakpoint”,然后在输入框中输入表达式(如赋值操作等),后面代码将使用该结果。
4断点时修改代码1.png
4断点时修改代码2.png



  1. 使用场景:


需要在调试时,方便手动修改数据来完成后续调试的时候。



  1. 使用方式:


在 Sources 面板中,在需要断点的行数右击,选择“Add conditional breakpoint”。
4断点时修改代码.gif


五、自定义断点(事件、请求等)


当我们需要进行自定义断点的时候,比如需要拦截 DOM 事件、网络请求等,就可以在 Source 面板,通过 XHR/fetch Breakpoints 和 Event Listener Breakpoints 来启用对应断点。
5自定义断点.png



  1. 使用场景:


需要在调试时,需要增加自定义断点时(如需要拦截 DOM 事件、网络请求等)。



  1. 使用方式:


在 Sources 面板中,通过 XHR/fetch Breakpoints 和 Event Listener Breakpoints 来启用对应断点。
5自定义断点.gif




链接:https://juejin.cn/post/6955081218723414029



收起阅读 »

如何处理浏览器的断网情况?

好的断网处理会让人很舒适:lol的断线重连,王者荣耀的断线重连 可以确保游戏的继续进行 坏的断网处理甚至不处理会出bug:比如我手上的项目就出了个bug 业务人员表示非常苦恼 网络问题一直是一个很值得关注的问题。 比如在慢网情况下,增加loading避免重复发...
继续阅读 »

好的断网处理会让人很舒适:lol的断线重连,王者荣耀的断线重连 可以确保游戏的继续进行


坏的断网处理甚至不处理会出bug:比如我手上的项目就出了个bug 业务人员表示非常苦恼


网络问题一直是一个很值得关注的问题。


比如在慢网情况下,增加loading避免重复发请求,使用promise顺序处理请求的返回结果,或者是增加一些友好的上传进度提示等等。


那么大家有没有想过断网情况下该怎么做呢?比如说网络正常->断网->网络正常。


其实我一直也没想过,直到组里的测试测出一个断网导致的bug,让我意识到重度依赖网络请求的前端,在断网情况下可能会出现严重的bug。


因此我将在这里记录一下自己对系统断网情况下的处理,一方面避免bug产生,一方面保证用户及时在应用内知道网络已经断开连接

概览


为了构建一个 “断网(offline)可用”的web应用,你需要知道应用在什么时候是断网(offline)的。
不仅仅要知道什么时候断网,更要知道什么时候网络恢复正常(online)。
可以分解陈本下面两种常见情况:



  1. 你需要知道用户何时online,这样你可以与服务器之间re-sync(重新同步)。

  2. 你需要知道用户何时offline,这样你可以将你未发出的请求过一段时间再向服务器发出。


通常可以通过online/offline事件去做这个事情。


用于检测浏览器是否连网的navigator.onLine


navigator.onLine



  • true online

  • false offline


可以通过network的online选项切换为offline,打印navigator.onLine验证。


当浏览器不能连接到网络时,这个属性会更新。规范中是这样定义的:

The navigator.onLine attribute must return false if the user agent will not contact the network when the user follows links or when a script requests a remote page (or knows that such an attempt would fail)...

用于检测网络状况的navigator.connection


在youtube观看视频时,自动检测网络状况切换清晰度是如何做到的呢?
国内的视频网站也会给出一个切换网络的提醒,该如何去检测呢?
也就是说,有没有办法检测网络状况?判断当前网络是流畅,拥堵,繁忙呢?
可以通过navigator.connection,属性包括effectiveType,rtt,downlink和变更网络事件change。继承自NetworkInformation API。

navigator.connection

online状态下运行console.log(navigator.connection);

{
onchange: null,
effectiveType: "4g",
rtt: 50,
downlink: 2,
saveData: false
}

通过navigator.connection可以判断出online,fast 3g,slow 3g,和offline,这四种状态下的effectiveType分别为4g,3g,2g,4g(rtt,downlink均为0)。


rtt和downlink是什么?NetworkInformation是什么?


这是两个反映网络状况的参数,比type更加具象且更能反映当前网络的真实情况。


常见网络情况rtt和downlink表


注意:rtt和downlink不是定值,而是实时变化的。online时,可能它现在是rtt 100ms,2.2Mb/s,下一秒就变成125ms,2.1Mb/s了。


rtt


  • 连接预估往返时间

  • 单位为ms

  • 值为四舍五入到25毫秒的最接近倍数(就是说这个值x%25===0,可以观察常见网络情况rtt和downlink表)

  • 值越小网速越快。类似ping的time吧

  • 在Web Worker中可用


downlink


  • 带宽预估值

  • 单位为Mbit/s(注意是Mbit,不是MByte。)

  • 值也是四舍五入到最接近的25比特/秒的倍数(就是说这个值x%25===0,可以观察常见网络情况rtt和downlink表)

  • 一般越宽速度越快,也就是,信道上可以传输更多数。(吐槽一句,学过的通信原理还蛮有用。)

  • 值越大网速越快。类似高速一般比国道宽。

  • 在Web Worker中可用


草案(Draft)阶段NetworkInformation API

无论是rtt,还是downlink,都是这个草案中的内容。
除此之外还有downlinkMax,saveData,type等属性。
更多资料可以查询:NetworkInformation


如何检测网络变化去做出响应呢?


NetworkInformation继承自EventTarget,可以通过监听change事件去做一些响应。


例如可以获得网络状况的变更?

var connection = navigator.connection;
var type = connection.effectiveType;

function updateConnectionStatus() {
console.log("网络状况从 " + type + " 切换至" + connection.effectiveType);
type = connection.effectiveType;
}

connection.addEventListener('change', updateConnectionStatus);

监听变更之后,我们可以弹一个Modal提醒用户,也可以出一个Notice通知用户网络有变化,或者可以更高级得去自动切换清晰度(这个应该比较难)。


引出NetworkInformation的概念,只是想起一个抛砖引玉的作用。这种细粒度的网络状况检测,可以结合具体需求去具体实现。


在这篇博文中,我们只处理断网和连网两种情况,下面来看断网事件"offline"和连网事件"online"。


断网事件"offline"和连网事件"online"


浏览器有两个事件:"online" 和 "offline".
这两个事件会在浏览器在online mode和offline mode之间切换时,由页面的<body>发射出去。


事件会按照以下顺序冒泡:document.body -> document -> window。


事件是不能去取消的(开发者在代码上不能手动变为online或者offline,开发时使用开发者工具可以)。


注册上下线事件的几种方式


最最建议window+addEventListener的组合。



  • 通过window或document或document.body和addEventListener(Chrome80仅window有效)

  • 为document或document.body的.ononline或.onoffline属性设置一个js函数。(注意,使用window.ononline和window.onoffline会有兼容性的问题)

  • 也可以通过标签注册事件<body ononline="onlineCb" onoffline="offlineCb"></body>


例子

<div id="status"></div>
<div id="log"></div>
window.addEventListener('load', function() {
var status = document.getElementById("status");
var log = document.getElementById("log");

function updateOnlineStatus(event) {
var condition = navigator.onLine ? "online" : "offline";
status.innerHTML = condition.toUpperCase();

log.insertAdjacentHTML("beforeend", "Event: " + event.type + "; Status: " + condition);
}

window.addEventListener('online', updateOnlineStatus);
window.addEventListener('offline', updateOnlineStatus);
});

其中insertAdjacentHTML是在标签节点的邻近位置插入,可以查阅:DOM进阶之insertAdjacentHTML


断网处理项目实战


可以基于vue,react封装出离线处理组件,在需要到的页面引入即可。


思路和效果


只要做到断网提醒+遮罩,上线提醒-遮罩即可。



  • 监听offline,断网给出提醒和遮罩:网络已断开,请检查网络连接。

  • 监听online,连网给出提醒和遮罩:网络已连接。

断网处理组件使用

<OfflineHandle
offlineTitle = "断网处理标题"
desc="断网处理描述"
onlineTitle="连网提醒"
/>
Vue组件
<!--OfflineHandle.vue-->
<template>
<div v-if="mask" class="offline-mask">
<h2 class="offline-mask-title">{{ offlineTitle }}</h2>

<p class="offline-mask-desc">{{ desc }}</p >
</div>
</template>

<script>
export default {
name: "offline-handle",
props: {
offlineTitle: {
type: String,
default: "网络已断开,请检查网络连接。",
},
onlineTitle: {
type: String,
default: "网络已连接",
},
desc: {
type: String,
default: "",
},
duration: {
type: Number,
default: 4.5,
},
},
data() {
return {
mask: false,
};
},
mounted() {
window.addEventListener("offline", this.eventHandle);
window.addEventListener("online", this.eventHandle);
console.log(this.desc);
},
beforeDestroy() {
window.removeEventListener("offline", this.eventHandle);
window.removeEventListener("online", this.eventHandle);
},
methods: {
eventHandle(event) {
const type = event.type === "offline" ? "error" : "success";
this.$Notice[type]({
title: type === "error" ? this.offlineTitle : this.onlineTitle,
desc: type === "error" ? this.desc : "",
duration: this.duration,
});
setTimeout(() => {
this.mask = event.type === "offline";
}, 1500);
},
},
};
</script>

<style lang="css" scoped>
.offline-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
z-index: 9999;
transition: position 2s;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.offline-mask-title {
color: rgba(0, 0, 0, 0.8);
}
.offline-mask-desc {
margin-top: 20px;
color: red;
font-weight: bold;
}
</style>
React组件
// offlineHandle.js
import React, { useState, useEffect } from "react";
import { notification } from "antd";
import "antd/dist/antd.css";
import "./index.css";

const OfflineHandle = (props) => {
const {
offlineTitle = "网络已断开,请检查网络连接。",
onlineTitle = "网络已连接",
desc,
duration = 4.5
} = props;
const [mask, setMask] = useState(false);

const eventHandler = (event) => {
const type = event.type === "offline" ? "error" : "success";
console.log(desc, "desc");
openNotification({
type,
title: type === "error" ? offlineTitle : onlineTitle,
desc: type === "error" ? desc : "",
duration
});
setTimeout(() => {
setMask(event.type === "offline");
}, 1500);
};

const openNotification = ({ type, title, desc, duration }) => {
notification[type]({
message: title,
description: desc,
duration
});
};

useEffect(() => {
window.addEventListener("offline", eventHandler);
window.addEventListener("online", eventHandler);
return () => {
window.removeEventListener("offline", eventHandler);
window.removeEventListener("online", eventHandler);
};
}, []);

const renderOfflineMask = () => {
if (!mask) return null;
return (
<div className="offline-mask">
<h2 className="offline-mask-title">{offlineTitle}</h2>

<p className="offline-mask-desc">{desc}</p >
</div>
);
};

return <>{renderOfflineMask()}</>;
};

export default OfflineHandle;

发现



  • offline和online事件:window有效,document和document.body设置无效


手上的项目只运行在Chrome浏览器,只有为window设置offline和online才生效。
运行环境:"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36



  • 为position增加2s的transition的避免屏闪


链接:https://juejin.cn/post/6953868764362309639
收起阅读 »

微信小程序自定义实现toast进度百分比动画组件

目录结构wxml {{number}} {{ content }} 搭建组件结构jsComponent({ options: { multipleSlots: true // 在组件定义时的选项中...
继续阅读 »

目录结构


wxml



{{number}}



{{ content }}


搭建组件结构

js

Component({
options: {
multipleSlots: true // 在组件定义时的选项中启用多slot支持
},
/**
* 私有数据,组件的初始数据
* 可用于模版渲染
*/
data: { // 弹窗显示控制
animationData: {},
content: '提示内容',
number: 0,
level_box:-999,
},
/**
* 组件的方法列表
*/
methods: {
/**
* 显示toast,定义动画
*/
numberChange() {
let _this = this
for (let i = 0; i < 101; i++) {
(function () {
setTimeout(() => {
_this.setData({
number: i + '%'
})
}, 100 * i)
})()
}
},
showToast(val) {
this.setData({
level_box:999
})
this.numberChange()
var animation = wx.createAnimation({
duration: 300,
timingFunction: 'ease',
})
this.animation = animation
animation.opacity(1).step()
this.setData({
animationData: animation.export(),
content: val
})
/**
* 延时消失
*/
setTimeout(function () {
animation.opacity(0).step()
this.setData({
animationData: animation.export()
})
}.bind(this), 10000)
}
}
})

json

```javascript
{
"component": true,
"usingComponents": {}
}

wxss

.wx-toast-box {
display: flex;
width: 100%;
justify-content: center;
position: fixed;
top: 400rpx;
opacity: 0;
}

.wx-toast-content {
max-width: 80%;
border-radius: 30rpx;
padding: 30rpx;
background: rgba(0, 0, 0, 0.6);
}

.wx-toast-toast {
height: 100%;
width: 100%;
color: #fff;
font-size: 28rpx;
text-align: center;
}

.progress {
display: flex;
justify-content: center;
font-size: 28rpx;
font-weight: 700;
text-align: CENTER;
color: #07c160;
}

.img_box {
display: flex;
justify-content: center;
margin: 20rpx 0;
}

@keyframes rotate {
from {
transform: rotate(360deg)
}

to {
transform: rotate(0deg)
}
}

.circle {
animation: 3s linear 0s normal none infinite rotate;
}

@keyframes translateBox {
0% {
transform: translateX(0px)
}

50% {
transform: translateX(10px)
}
100% {
transform: translateX(0px)
}
}

.anima_position {
animation: 3s linear 0s normal none infinite translateBox;
}

效果截图



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



收起阅读 »

让我们一起实现微信小程序国际化吧

常见的国际化方式官方方案官方链接,其实官方的解决方案最大的问题就是麻烦,主要体现在以下几个方面强依赖目录结构由于gulp.js里是按照此目录结构进行处理的,如果要维持自定义目录需要修改glup文件,如下图特别好笑的一点官方示例里居然不是这个目录结构,不过依然是...
继续阅读 »

常见的国际化方式

官方方案

官方链接,其实官方的解决方案最大的问题就是麻烦,主要体现在以下几个方面

强依赖目录结构

由于gulp.js里是按照此目录结构进行处理的,如果要维持自定义目录需要修改glup文件,如下图


特别好笑的一点官方示例里居然不是这个目录结构,不过依然是强依赖目录结构,因为gulp中路径是写死的

文档简陋

通过官方文档快速入门居然无法搭建起项目,暂时只用这种方式搭建起来了 官方github demo,通过对比发现好多必要代码都没有在文档中说明。


比如需要在app.js中开始便需要执行getI18nInstance(),否则全局都无法正常国际化。这么重要的信息居然在快速入门中没有说明


调试麻烦

每次修改代码都要重新执行npm run build,注意是每次


由于国际化必须通过npm run build来实现,而每次npm run build过后dist文件就会被覆盖,所以每次只能修改src,而小程序预览的却是dist文件,这也就导致必须频繁的执行build命令。下面演示一下增加一个表头的操作步骤

2021-05-22 07-57-21.2021-05-22 08_03_21.gif

说下优点

代码简洁。解释一下,从上图可以看到,他的书写方式和其他主流框架的国际化书写方式很类似(vue,react)。view层都是类似t(key,参数),对js侵入也很小,下面是上图页面对应的js部分。

import { I18nPage } from '@miniprogram-i18n/core'

I18nPage({
onLoad() {
this.onLocaleChange((locale) => {
console.log('current locale:', this.getLocale(), locale)
})

this.setLocale('zh-CN')
},

toggleLocale() {
this.setLocale(
this.getLocale() === 'zh-CN' ? 'en-US' : 'zh-CN'
)
},

nativate() {
wx.navigateTo({
url: '/pages/logs/logs'
})
}
})

可以说除了I18nPage以外没有别的侵入,剩下那些代码都是用于切换语言所需,如果只是最简单国际化,只需要I18nPage({})即可

聊一下为什每次都需build

其实咱们看下dist/i18n/locales.wxs文件即可

var fallbackLocale = "zh-CN";
var translations = {
"en-US": {
test: ["test messages"],
test2: ["test message 2, ", ["label"], ", ", ["label2"]],
nested: ["nested message: ", ["test"]],
toggle: ["Toggle locale"],
navigate: ["Navigate to Log"],
"window.title": ["I18n test"],
"index.test": ["Test fallback"],
navigate2: ["Navigation 2nd"],
},
"zh-CN": {
test: ["测试消息"],
test2: ["测试消息二, ", ["label"], ", ", ["label2"]],
nested: ["嵌套消息: ", ["test"]],
toggle: ["切换语言"],
navigate: ["跳转"],
"window.title": ["国际化测试"],
"index.test": ["备选"],
navigate2: ["导航2"],
},
};
var Interpreter = (function (r) {
var i = "";
function f(r, n) {
return r
? "string" == typeof r
? r
: r
.reduce(function (r, t) {
return r.concat([
(function (n, e) {
if (((e = e || {}), "string" == typeof n)) return n;
if (n[2] && "object" == typeof n[2]) {
var r = Object.keys(n[2]).reduce(function (r, t) {
return (r[t] = f(n[2][t], e)), r;
}, {}),
t = r[e[0]],
u = e[n[0]];
return void 0 !== u
? r[u.toString()] || r.other || i
: t || r.other || i;
}
if ("object" == typeof n && 0 < n.length) {
return (function r(t, n, e) {
void 0 === e && (e = 0);
if (!n || !t || t.length <= 0) return "";
var n = n[t[e]];
if ("string" == typeof n) return n;
if ("number" == typeof n) return n.toString();
if (!n) return "{" + t.join(".") + "}";
return r(t, n, ++e);
})(n[0].split("."), e, 0);
}
return "";
})(t, n),
]);
}, [])
.join("")
: i;
}
function c(r, t, n) {
t = r[t];
if (!t) return n;
t = t[n];
return t || n;
}
return (
(r.getMessageInterpreter = function (i, o) {
function e(r, t, n) {
var e, u;
return f(
((e = r),
(u = o),
((n = (r = i)[(n = n)]) && (n = n[e])) || c(r, u, e)),
t
);
}
return function (r, t, n) {
return 2 === arguments.length
? e(r, null, t)
: 3 !== arguments.length
? ""
: e(r, t, n);
};
}),
r
);
})({});

module.exports.t = Interpreter.getMessageInterpreter(
translations,
fallbackLocale
);
其实搞这么麻烦构建方式主要是为了生成这个wxs文件,我们之所以能在vue中看到{{t('key')}}的方式进行国际化,是因为wxml层本身支持函数调用且函数可以调用外部资源(国际化文件),而小程序只允许通过wxs(官方文档)的方式在页面使用函数,出于性能考虑又不允许wxs引用任何外部资源(除了其他的wxs),所以这个build最核心的诉求是把 国际化文件copy一份到wxs文件中。下面是wxs无法引用外部资源的官方说明

优化

我们国际化的核心诉求就是解决官方国际化问题同时保留他的优点,这里我们列下此次的目标

  •  路径灵活,不强依赖路径减少后期添加国际化的路径改动成本
  •  调试方便,和原始开发调试方式相同
  •  书写简洁,保持和vue一样的书写方式
2021-05-22 10-27-02.2021-05-22 10_28_39.gif

wxml代码

<wxs src="../wxs/i18n.wxs" module="i18n" />
<!-- 标题国际化 -->
<page-meta>
<navigation-bar title="{{i18n.t(locales['主页'])}}" />
</page-meta>
<!-- 一般国际化 -->
<view>{{i18n.t(locales['通往爱人家里的路总不会漫长。'])}}</view>
<!-- js国际化 -->
<view>{{jsMsg}}</view>
<!-- 支持变量国际化 -->
<view>{{i18n.t(locales['当前页面:{path}'],[{key:'path',value:'home-page/home-page'}])}}</view>
<!-- 切换中英文按钮 -->
<button bindtap="zhClick" type="default">{{i18n.t(locales['切换中文'])}}</button>
<button bindtap="enClick" type="warn">{{i18n.t(locales['切换英语'])}}</button>

js代码

const i18n = require('../behaviors/i18n');
// home-page/home-page.js
Component({
behaviors: [i18n],
/**
* 组件的初始数据
*/
data: {
},
/**
* 组件的方法列表
*/
methods: {
zhClick() {
this.switchLanguage('zh_CN')
},
enClick() {
this.switchLanguage('en_US')
},
}
})

基本维持和官方相同的写法,尽可能少的代码写入

代码段链接

解决思路

利用behaviors(官方文档)将国际化文案进行引入每个页面。然后将所有国际化数据和key值以参数的形式传递给wxs函数,这样就可以避开wxs外部资源限制实现和vue i18n相同的效果。



  • behaviors负责将国际化方法和文案导入全局,以下是behaviors/i18n.js源码:

// behaviors/i18n.js

const {
t
} = require('../utils/index')
const i18n = Behavior({
data: {
language:{}, // 当前语种
locales: {}, // 当前语言的全部国际化信息
},
pageLifetimes: {
// 每次页面打开拉取对应语言国际化数据
show() {
if (this.data.language === 'en_US') {
this.setData({
locales: require('../i18n/en_US')
})
} else {
this.setData({
locales: require('../i18n/zh_CN')
})
}
}
},
methods: {
// 全局js国际化便捷调用
$t(key, option) {
return t(key, option)
},
// 由于tab只能通过js修改,所以每次语言切换需要重新更新tab国际化内容
refreshTab() {
wx.setTabBarItem({
index: 0,
text: this.data.locales['主页']
})
wx.setTabBarItem({
index: 1,
text: this.data.locales['我的']
})
},
// 切换语种
switchLanguage(language) {
this.setData({
language
})
if (language === 'zh_CN') {
this.setData({
locales: require('../i18n/zh_CN')
})
} else {
this.setData({
locales: require('../i18n/en_US')
})
}
// 切换下方tab
this.refreshTab()
},
}
})

module.exports = i18n
wxs负责为wxml层提供国际化方法,此处逻辑比较简单,先找到国际化文件中key值对应的语句,然后根据第二参数(arr)将变量进行替换,此处替换逻辑比较粗暴,使用{key}方式代表变量。如:"我的年龄是{age}",age代表变量,参数传递格式为 [{key:'xxx',value:'xxxx'}]

// 国际化.js
{
"ageText":"my age is {age}",
}
// wxml
<view>{{i18n.t(locales['ageText'],[{key:'age',value:'18'}])}}</view>

wxs源码如下:

var i18n = {
t: function (str, arr) {
var result = str;
if (arr) {
arr.forEach(function (item) {
if(result){
result = result.replace('{'+item.key+'}', item.value)
}
})
}
return result
}
}
module.exports = i18n

同时提供一个在js里获取国际化的util方法

// 国际化
const t = (key, option = {}) => {
const language = wx.getStorageSync('language');
let locales = null
if (language === 'en_US') {
locales = require('../i18n/en_US')
} else {
locales = require('../i18n/zh_CN')
}
let result = locales[key]
for (let optionKey in option) {
result = result.replace(`{${optionKey}}`, option[optionKey])
}
return result
}

module.exports = {
t
}

这样就基本实现了同时在wxml,js中进行国际化的基本需求,同时也解决了官方调试体验不足的缺点。

不足




  • 其实调试体验还不是那么完美,由于只有在show中初始化国际化文件内容,所以当开启“热重载”时对国际化文件进行修改,国际化内容不会自动进行更新




  • 每个page的js文件都需要引入behaviors/i18n.js,wxml文件引入<wxs src="../wxs/i18n.wxs" module="i18n" />有些略显繁琐




  • require('../i18n/xxx')整个项目引入了3遍略显繁琐,可以封装一下




  • i18n.t(locales['key']) 其中locals每次都要写一遍比较繁琐,不过由于wxs的限制也想不到太好的方法

使用建议


由于只是写一个demo很多逻辑并没有写完整,所以如果使用的话需要根据项目进行修改




  1. 如果i18n路径和命名方式不同需要同时修改behaviors/i18n.js,以及utils/index.js中的路径




  2. 现在每次刷新页面国际化都会被重置成中文,建议在behaviors/i18n.js的show方法中从全局获取当前的国际化语言。这样就不会每次都被重置成中文了。


链接:https://juejin.cn/post/6964963316493975588


收起阅读 »

要不要打造一个轻量的小程序引擎玩玩?

我们的小程序框架的底层,我把它分为四个部分,主要是多线程模型runtime 框架js 沙箱其他我们一个一个来多线程模型和线程通信多线程模型多线程模型是一个非常常见的 UI 模型,包括 RN、flutter 统统都是使用的这个模型,小程序也不例外 它们其实只是线...
继续阅读 »


我们的小程序框架的底层,我把它分为四个部分,主要是

  • 多线程模型
  • runtime 框架
  • js 沙箱
  • 其他

我们一个一个来

多线程模型和线程通信


多线程模型


多线程模型是一个非常常见的 UI 模型,包括 RN、flutter 统统都是使用的这个模型,小程序也不例外


它们其实只是线程主体的不同,比如 RN 主要是 shadow tree 和 jscore,而 flutter 则是 skia 和 dart engine,小程序则是 webview 充当渲染层,js engine(或 worker)充当逻辑层


尽管本质一样,但因为业务场景的不同,小程序的诉求却和 RN/flutter 完全不同


在 RN 中,app 作为一个主体,我们更乐意分配更多资源,以至于 RN 一直在跑 react 这种 runtime 浪费的框架,在过去,这被认为是值得的


但是小程序与之相反,它作为 app 的附属品,我们不乐意给小程序分配更多资源,不乐意分配内存,不乐意分配更多线程,所以我们这次分享的前提是

基于最小分配的前提,探讨小程序的每个环节

请记住前提,然后我们接着往下看


线程通信


说到多线程,我们首先想到的就是多线程的调度和通信,我们先讲通信,通常来说,多线程的非 UI 线程都是没有 dom 环境的,无论是 js 引擎还是 worker


所以为了能跑一个前端框架,我们不得另寻出路,主要方案有三种,其中幻灯片的第二种,写一个 dom 无关的 diff 算法,这是写不出来什么好算法的,所以我们主要看剩下两种思路



幻灯片中,左边的代码是 [ 使用 Proxy 劫持 dom ],右边的是 [ 模拟 dom API ]


这两种思路其实是类似的,模拟 dom API 是最为常见的,比如 react-reconciler,vue3 的 renderer,都是用的这个思路,就是它把用到的 dom API 暴露出来,你在不同的端去模拟 dom 的行为


甚至还有 taro-next,kbone 这种框架,他们模拟了一整个 dom/bom 层


这个思路好处是粗暴方便好用,坏处就是抽象程度低,比如 taro-next 中就用了千行代码做这件事,属于 case by case,没啥逼格


所以我提出了 Proxy 劫持 dom 的思路,其实这个思路在微前端中比较常用,只不过我们现在用 Proxy 不再是劫持一两个 dom 操作了,而是将所有 dom 操作通通记录下来,然后批量发送给 UI 线程


这个实现抽象程度非常高,我使用了不到 200 行代码就可以劫持所有 dom 操作


代码在这里:github.com/yisar/fard/…


除了线程通信,更重要的是线程的调度,因为很重要,我们放到最后说


前端框架


还记得小程序架构的前提吗?没错,就是最小资源分配


因为我们不想给小程序分配过多的资源,所以像 react、vue 这种 runtime 特别重的框架,其实是不适合用作小程序的


甚至 fre 也不适合,因为大家可能对“轻量”这个词有误解,不是代码越少就越轻量,代码量是很重要的一个方面,但是更重要的是代码的内存占用,以及算法的算力和复杂度


fre 虽然代码量少,但它的算法和 vue 是一样的,算力相同,内存占用也不少


所以我们不得不将目光转向 svelte 这类框架,幻灯片可以看到,svelte 通过编译,直接生成原生 dom 操作,没有多余的算法和 vdom


实际上,我们在做性能优化的时候,讲究一个“换”字,react 这种框架,通过浪费 runtime 去做算法,“换”一个最小结果,而 svelte 则是通过编译(浪费用户电脑),去换 runtime



JS 沙箱



然后我们来讲讲沙箱,也就是 js 引擎和 worker,这部分适合语言爱好者

选型


通常来说,一提到 js 引擎,大家都是 v8 v8 v8

但是实际上,v8 是一个高度优化的 JIT 引擎,它跑 js 确实是足够快的,但对于 UI 来说,我们更多要的不是跑得快


实际上,AOT 的语言或引擎更适合 UI 框架,比如 RN 的 hermes,dart 也支持 AOT,它可以编译成字节码,只需要一次构建即可,当然,AOT 也有缺点,就是 热更新 比较难做


另外除了 js 引擎,worker 也是一个非常赞的选择,方便好用,而且还提供了 bom 接口,比如 offscreen canvas,fetch,indexdb,requestAnimationFrame……

总结



哈哈哈总结,我们基于最小分配的前提去设计这个架构,每个环节都选择节省资源的方案


事实上写代码就是这样的,比如我写 fre,那么我追求 1kb,0 依赖,我写业务框架,我追求 0 配置,1mb 的 node_modules 总大小


我写小程序,我追求最小资源分配,不管做啥,有痛点然后有追求然后才有设计


其他

其实小程序还有很多东西可以做,比如现在的小程序都需要兼容微信小程序,也就是类似 wxml,wxss,wxs这些非标准的文件,还要得是个多 Page 的 mpa


比如 ide,我们可以使用 nobundle 的思路来加快构建速度


当然,为了服务业务,在我们公司我没有使用 nobundle


比如剧透一下,我在公司中为了兼容微信小程序,开的新坑


原理是将微信的文件(wxml,wxss,wxs)先编译成可识别的文件(jsx,css,js),然后使用 babel、postCss 去转移,形成一个个 umd 的子应用


然后通过 berial(微前端框架)的路由,沙箱,生命周期,将它们跑在 h5 端,这样就可以在浏览器中模拟和调试啦



最后我们通过三张图和一个问题,来补充和结束一下这次分享


第一张图是微信小程序的后台桌面,有没有感觉和操作系统有点像,但其实不是的,操作系统的软件是进程的关系,只能切换,不能共存,而小程序是多进程,这些小程序可以在后台留驻,随时保持唤醒


第二张图是钉钉的仪表盘,这也是小程序最常用的场景,就是和这种一堆子应用的 app


第三张图是 vscode 的插件系统,是的,想不到吧,这玩意也是小程序架构,而且也是同样的思想,我不让你操作 dom

然后最后的问题:canvas 怎么办?



这个问题实际上非常难搞,如果我们使用 worker 作为 js 沙箱还好,有 offscreen canvas 和 requestAnimationFrame


如果我们使用 js 引擎怎么办呢,走上面提到的线程通信成本就太高了,动画走这个通信,等接收到消息,动画已经卡死了


所以还有什么办法,这里不得不再次提多线程的特性,也就是多线程的内存是共享的,我们可以 js 引擎中,将 canvas 整个元素放到堆栈里,然后 UI 线程和 js 线程共享这一块内存


这样就不需要走线程通信了,适合 canvas,动画这种场景



链接:https://juejin.cn/post/6962028699872919559


收起阅读 »

微信小程序-自定义日期组件实现

   今日份跟大佬聊到说前端基础架构平台的东西、涉及到基础组件库、公共工具类等内容, 然后期望能自我驱动带头去做, 陷入深深滴沉思, 该考虑如何做?思绪一片混乱, 那就写篇文章冷静冷静,故有了此篇, 仅供参考。微信小程序原生有提供一套日期组...
继续阅读 »

   今日份跟大佬聊到说前端基础架构平台的东西、涉及到基础组件库、公共工具类等内容, 然后期望能自我驱动带头去做, 陷入深深滴沉思, 该考虑如何做?
思绪一片混乱, 那就写篇文章冷静冷静,故有了此篇, 仅供参考。

微信小程序原生有提供一套日期组件, 大概如下:



跟UI预期不一致的点有如下几个:

A. 期望弹窗居中显示、而不是从底部弹出;

B. 期望小于10的展示为1月,1日这种, 而不是01月, 01日;

C. UI样式跟微信原生差别有点大;

D. 不需要头部的取消&确定按钮、预期底部整个确定按钮即可;

想着让产品接受原生日期组件的, But拗不过产品的思维, 只能开干、自己撸一个自定义日期组件, 造轮子=.= 

既然原生的不能用, 那么我们看看小程序是否有提供这种类似的滚动器, 查看官方文档发现: 



那就开干, 为尽可能保持代码的最小颗粒度(这里不考虑弹窗外壳的封装、纯日期组件).
话不多说、这里直接贴上代码、预留的坑位都会在代码内有备注, 请参考:

// 组件wxml
<!-- 预留坑位: 按道理该日期组件应该是做在弹窗上的、这里为了简化代码故直接写在了页面上;
后期使用者烦请自己做个弹窗套上、用showModal属性控制其显示隐藏-->
<view class="picker" wx:if="{{showModal}}">
<picker-view indicator-class="picker-indicator" value="{{pickerIndexList}}" bindchange="bindChangeDate">
<picker-view-column>
<view wx:for="{{yearList}}" wx:key="index" class="{{pickerIndexList[0]==index?'txt-active':''}}">{{item}}年</view>
</picker-view-column>
<picker-view-column>
<view wx:for="{{monthList}}" wx:key="index" class="{{pickerIndexList[1]==index?'txt-active':''}}">{{item}}月</view>
</picker-view-column>
<picker-view-column>
<view wx:for="{{dayList}}" wx:key="index" class="{{pickerIndexList[2]==index?'txt-active':''}}">{{item}}日</view>
</picker-view-column>
</picker-view>
<!-- 预留坑位: 日期组件可能仅允许数据回选、不允许修改。
思路: 通过自定义蒙层盖在日期控件上从而达到禁止控件滚动的效果.
-->
<view wx:if="{{!canEdit}}" class="disabled-picker"></view>
</view>

// 组件wxss
.picker{
position: relative;
height: 300rpx;
width: 600rpx;
margin: 0 auto;
border: 1rpx solid red;
}
.picker picker-view {
width: 100%;
height: 100%;
}
.picker-indicator {
height: 60rpx;
line-height: 60rpx;
}
.picker picker-view-column view {
font-size: 40rpx;
line-height: 60rpx;
text-align: center;
}
.txt-active {
color: #2c2c2c;
}
/* 预留坑位: 为便于区分真的有遮罩层盖住、特意加了个背景色、实际使用过程可改成透明色 */
.disabled-picker{
width: 600rpx;
position: absolute;
top: 0;
left: 0;
height: 300rpx;
z-index: 999;
background: rgb(255,222,173,0.7);
}


// 组件js
Component({
properties: {},
data: {
yearList: [],
monthList: [],
dayList: [],
pickerIndexList: [0, 0, 0]
},
methods: {
// dateString格式: 'YYYY-MM-DD'
initPicker (dateString) {
let nowDate = new Date()
// 预留个坑位: 若需要指定某一日期则从外面传入、否则默认当天
if(dateString){
// 预留个坑位: 判定传入的数据类型是否符合要求、若不符合该报错的报错
nowDate = new Date(dateString)
}

// 预留个坑位: 因为下面的日期指定在1900.01.01-2100.12.31、故这里最好校验下传入日期是否在区间内.
let nowYear = nowDate.getFullYear()
let nowMonth = nowDate.getMonth() + 1
let yearList = this.getYearList(nowYear)
let monthList = this.getMonthList()
let dayList = this.getDayList(nowYear, nowMonth)

// 获取多列选择器的选中值下标
let pickerIndexList = []
pickerIndexList[0] = yearList.findIndex(o => o === nowDate.getFullYear())
pickerIndexList[1] = monthList.findIndex(o => o === nowDate.getMonth()+1)
pickerIndexList[2] = dayList.findIndex(o => o === nowDate.getDate())
this.setData({
yearList,
monthList,
dayList,
pickerIndexList,
showModal: true
})
},
// 获取年份
getYearList (nowYear) {
let yearList = []
if(nowYear < 1900 || nowYear > 2100){
return false
}
for (let i = 1900; i <= 2100; i++) {
yearList.push(i)
}
return yearList
},
// 获取月份
getMonthList () {
let monthList = []
for (let i = 1; i <= 12; i++) {
monthList.push(i)
}
return monthList
},
// 获取日期 -> 根据年份&月份
getDayList (year, month) {
let dayList = []
month = parseInt(month, 10)
// 特别注意: 这里要根据年份&&月份去计算当月有多少天[切记切记]
let temp = new Date(year, month, 0)
let days = temp.getDate()
for (let i = 1; i <= days; i++) {
dayList.push(i)
}
return dayList
},
// 日期选择改变事件
bindChangeDate (e) {
let pickerColumnList = e.detail.value
const { yearList=[], monthList=[] } = this.data
const nowYear = yearList[pickerColumnList[0]]
const nowMonth = monthList[pickerColumnList[1]]
this.setData({
dayList: this.getDayList(nowYear, nowMonth),
pickerIndexList: pickerColumnList
})
},
show (birthday) {
// 预留坑位: 这里也许会有一定的逻辑判定是否允许编辑日期控件, 故预留canEdit属性去控制
this.setData({
canEdit: true
})
this.initPicker(birthday)
},
// 预留坑位、点击确定按钮获取到选中的日期
surePicker () {
const { pickerIndexList, yearList, monthList, dayList } = this.data
// 预留坑位: 月份&日期补0
let txtDate = `${yearList[pickerIndexList[0]]}-${monthList[pickerIndexList[1]]}-${dayList[pickerIndexList[2]]}`
console.log(txtDate)
},
}
})

接下来我们看看使用方是怎么使用的?

// 页面wxml
<!-- 预留坑位: 这里仅展示触发事件、开发者替换成实际业务即可-->
<view bind:tap="openPicker" style="margin:20rpx; text-align:center;">打开日期控件</view>

// 页面json: 记得在使用页面的json处引入该组件、配置组件路径

// 页面js
methods: {
openPicker (){
// 获取组件实例、这里可选择是否传入日期
this.date_picker = this.selectComponent && this.selectComponent('#date_picker')
this.date_picker && this.date_picker.show()
},
}

一切准备就绪、我们看看效果图!
这是日期可编辑时、你是可滚动选择器的:


我们看看日期不可编辑时、仅可查看的效果图:



样式是稍微有点丑、到时开发者按照实际UI去做微调即可、这不难的=.=.

这里预留了几个扩展点:

1.支持外部传入日期、默认选中预设值;

2.支持在弹窗内显示日期控件、需要使用者自行开发弹窗;

3.支持日期控件仅可查看、不可编辑;

4.支持日期控件的关闭、一般是弹窗上有个关闭按钮或者是点击弹窗的蒙层可关闭、使用者自行开发;

Tips: 具体的代码改动点都有在上面的code中有备注、欢迎对号改代码, 若有任何不懂的欢迎留言或者私信、很愿意帮您解答。


链接:https://juejin.cn/post/6967201721265160199

收起阅读 »

如何让10万条数据的小程序列表如丝般顺滑

某天闲着无聊想练一下手速,去上拉一个小程序项目中一个有1万多条商品数据的列表。在数据加载到1000多条后,是列表居然出现了白屏。看了一下控制台:‘Dom limit exceeded’,dom数超出了限制, 不知道微信是出于什么考虑,要限制页面的dom数量。一...
继续阅读 »

某天闲着无聊想练一下手速,去上拉一个小程序项目中一个有1万多条商品数据的列表。在数据加载到1000多条后,是列表居然出现了白屏。看了一下控制台:

‘Dom limit exceeded’,dom数超出了限制, 不知道微信是出于什么考虑,要限制页面的dom数量。

一.小程序页面限制多少个wxml节点?

写了个小dome做了个测试。 listData的数据结构为:

listData:[
{
isDisplay:true,
itemList:[{
qus:'下面哪位是刘发财女朋友?',
answerA:'刘亦菲',
answerB:'迪丽热巴',
answerC:'斋藤飞鸟',
answerD:'花泽香菜',
}
.......//20条数据
]
}]

页面渲染效果:




{{item.qus}}

A. {{item.answerA}}
B. {{item.answerB}}
C. {{item.answerC}}
D. {{item.answerD}}



2.dome2,删除了不必要的dom嵌套



{{item.qus}}

A. {{item.answerA}}
B. {{item.answerB}}
C. {{item.answerC}}
D. {{item.answerD}}



通过大致计算,一个小程序页面大概可以渲染2万个wxml节点 而小程序官方的性能测评得分条件为少于1000个wxml节点官方链接



二.列表页面优化

1.减少不必要的标签嵌套


由上面的测试dome可知,在不影响代码运行和可读性的前提下,尽量减少标签的嵌套,可以大幅的增加页面数据的列表条数,毕竟公司不是按代码行数发工资的。如果你的列表数据量有限,可以用这种方法来增加列表渲染条数。如果数据量很大,再怎么精简也超过2万的节点,这个方法则不适用。


2.优化setData的使用


图五所示,小程序setDate的性能会受到setData数据量大小和调用频率限制。所以要围绕减少每一次setData数据量大小,降低setData调用频率进行优化。
#####(1)删除冗余字段
后端的同事经常把数据从数据库中取出就直接返回给前端,不经过任何处理,所以会导致数据大量的冗余,很多字段根本用不到,我们需要把这些字段删除,减少setDate的数据大小。
#####(2)setData的进阶用法
通常,我们对data中数据的增删改操作,是把原来的数据取出,处理,然后用setData整体去更新,比如我们列表中使用到的上拉加载更多,需要往listData尾部添加数据:

newList=[{...},{...}];
this.setData({
listData:[...this.data.listData,...newList]
})

这样会导致 setDate的数据量越来越大,页面也越来越卡。

setDate的正确使用姿势

  • setDate修改数据

比如我们要修改数组listData第一个元素的isDisplay属性,我们可以这样操作:

let index=0;
this.setData({
[`listData[${index}].isDisplay`]:false,
})

如果我们想同时修改数组listData中下标从0到9的元素的isDisplay属性,那要如何处理呢?你可能会想到用for循环来执行setData

for(let index=0;index<10;index++){
this.setData({
[`listData[${index}].isDisplay`]:false,
})
}

那么这样就会导致另外一个问题,那就是listData的调用过于频繁,也会导致性能问题,正确的处理方式是先把要修改的数据先收集起来,然后调用setData一次处理完成:

let changeData={};
for(let index=0;index<10;index++){
changeData[[`listData[${index}].isDisplay`]]=false;
}
this.setData(changeData);



这样我们就把数组listData中下标从0到9的元素的isDisplay属性改成了false

  • setDate往数组末尾添加数据

如果只添加一条数据

let newData={...};
this.setData({
[`listData[${this.data.listData.length}]`]:newData
})

如果是添加多条数据

let newData=[{...},{...},{...},{...},{...},{...}];
let changeData={};
let index=this.data.listData.length
newData.forEach((item) => {
changeData['listData[' + (index++) + ']'] = item //赋值,索引递增
})
this.setData(changeData)

三.使用自定义组件

可以把列表的一行或者多行封装到自定义组件里,在列表页使用一个组件,只算一个节点,这样你的列表能渲染的数据可以成倍数的增加。组件内的节点数也是有限制的,但是你可以一层层嵌套组件实现列表的无限加载,如果你不怕麻烦的话

四.使用虚拟列表

经过上面的一系列操作后,列表的性能会得到很大的提升,但是如果数据量实在太大,wxml节点数也会超出限制,导致页面发生错误。我们的处理方法是使用虚拟列表,页面只渲染当前可视区域以及可视区域上下若干条数据的节点,通过isDisplay控制节点的渲染。

  • 可视区域上方:above
  • 可视区域:screen
  • 可视区域下方:below

1.listData数组的结构

使用二维数组,因为如果是一维数组,页面滚动需要用setData设置大量的元素isDispaly属性来控制列表的的渲染。而二维数组可以这可以一次调用setData控制十条,二十条甚至更多的数据的渲染。

listData:[
{
isDisplay:true,
itemList:[{
qus:'下面哪位是刘发财女朋友?',
answerA:'刘亦菲',
answerB:'迪丽热巴',
answerC:'斋藤飞鸟',
answerD:'花泽香菜',
}
.......//二维数组中的条数根据项目实际情况
]
}]

2.必要的参数

data{
itemHeight:4520,//列表第一层dom高度,单位为rpx
itemPxHeight:'',//转化为px高度,因为小程序获取的滚动条高度单位为px
aboveShowIndex:0,//已渲染数据的第一条的Index
belowShowNum:0,//显示区域下方隐藏的条数
oldSrollTop:0,//记录上一次滚动的滚动条高度,判断滚动方向
prepareNum:5,//可视区域上下方要渲染的数量
throttleTime:200,//滚动事件节流的时间,单位ms
}

3.wxml的dom结构






{{item.qus}}

A. {{item.answerA}}
B. {{item.answerB}}
C. {{item.answerC}}
D. {{item.answerD}}




4.获取列表第一层dom的px高度

let query = wx.createSelectorQuery();
query.select('.content').boundingClientRect(rect=>{
let clientWidth = rect.width;
let ratio = 750 / clientWidth;
this.setData({
itemPxHeight:Math.floor(this.data.itemHeight/ratio),
})
}).exec();

5.页面滚动时间节流

function throttle(fn){
let valid = true
return function() {
if(!valid){
return false
}
// 工作时间,执行函数并且在间隔期内把状态位设为无效
valid = false
setTimeout(() => {
fn.call(this,arguments);
valid = true;
}, this.data.throttleTime)
}
}

6.页面滚动事件处理

onPageScroll:throttle(function(e){
let scrollTop=e[0].scrollTop;//滚动条高度
let itemNum=Math.floor(scrollTop/this.data.itemPxHeight);//计算出可视区域的数据Index
let clearindex=itemNum-this.data.prepareNum+1;//滑动后需要渲染数据第一条的index
let oldSrollTop=this.data.oldSrollTop;//滚动前的scrotop,用于判断滚动的方向
let aboveShowIndex=this.data.aboveShowIndex;//获取已渲染数据第一条的index
let listDataLen=this.data.listData.length;
let changeData={}
//向下滚动
if(scrollTop-oldSrollTop>0){
if(clearindex>0){
//滚动后需要变更的条数
for(let i=aboveShowIndex;i changeData[[`listData[${i}].isDisplay`]]=false;
let belowShowIndex=i+2*this.data.prepareNum;
if(i+2*this.data.prepareNum changeData[[`listData[${belowShowIndex}].isDisplay`]]=true;
}
}
}
}else{//向上滚动
if(clearindex>=0){
let changeData={}
for(let i=aboveShowIndex-1;i>=clearindex;i--){
let belowShowIndex=i+2*this.data.prepareNum
if(i+2*this.data.prepareNum<=listDataLen-1){
changeData[[`listData[${belowShowIndex}].isDisplay`]]=false;
}
changeData[[`listData[${i}].isDisplay`]]=true;
}
}else{
if(aboveShowIndex>0){
for(let i=0;i this.setData({
[`listData[${i}].isDisplay`]:true,
})
}
}
}
}
clearindex=clearindex>0?clearindex:0
if(clearindex>=0&&!(clearindex>0&&clearindex==this.data.aboveShowIndex)){
changeData.aboveShowIndex=clearindex;
let belowShowNum=this.data.listData.length-(2*this.data.prepareNum+clearindex)
belowShowNum=belowShowNum>0?belowShowNum:0
if(belowShowNum>=0){
changeData.belowShowNum=belowShowNum
}
this.setData(changeData)
}
this.setData({
oldSrollTop:scrollTop
})
}),

经过上面的处理后,页面的wxml节点数量相对稳定,可能因为可视区域数据的index计算误差,页面渲染的数据有小幅度的浮动,但是已经完全不会超过小程序页面的节点数量的限制。理论上100万条数据的列表也不会有问题,只要你有耐心和精力一直划列表加载这么多数据。

7.待优化事项



  • 列表每一行的高度需要固定,不然会导致可视区域数据的index的计算出现误差

  • 渲染玩列表后往回来列表,如果手速过快,会导致above,below区域的数据渲染不过来,会出现短暂的白屏,白屏问题可以调整 prepareNum, throttleTime两个参数改善,但是不能完全解决(经过测试对比发现,即使不对列表进行任何处理,滑动速度过快也会发生短暂白屏的情况)。

  • 如果列表中有图片,above,below区域重新渲染时,图片虽然以经缓存在本地,不需要重新去服务器请求,但是重新渲染还是需要时间,尤其当你手速特别快时。可以根据上面的思路, isDisplay时只销毁非的节点,这样重新渲染就不需要渲染图片,但是这样节点数还是会增加,不过应该能满足大部分项目需求了,看自己项目怎么取舍。



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


收起阅读 »

css 加载阻塞问题,无废话

一、问题 & 结论1. css 加载会阻塞 DOM 树的解析渲染吗 ?css 并不会阻塞 dom 树的解析css 会阻塞 dom 树的渲染2. css 加载会阻塞 js 运行吗 ?css 加载会阻塞后面 js 语句的执行二、造成的结果以及优化方案1. ...
继续阅读 »

一、问题 & 结论

1. css 加载会阻塞 DOM 树的解析渲染吗 ?
  • css 并不会阻塞 dom 树的解析
  • css 会阻塞 dom 树的渲染
2. css 加载会阻塞 js 运行吗 ?
  • css 加载会阻塞后面 js 语句的执行

二、造成的结果以及优化方案

1. 造成的结果
  • css 加载缓慢会造成长时间的白屏

2. 优化方案
  1. CDN 加速:CDN 会根据网络状况,挑选一个最近的具有缓存内容的节点提供资源,减少加载时间
  2. 对 css 进行压缩:使用打包工具 webpack、gulp 等,开启 gzip 压缩
  3. 合理的使用缓存:强缓存、协商缓存等策略
  4. 减少 http 请求次数,合并 css 文件,或者干脆写成内联样式(缺点:不能缓存)

三、原理解析

1. 浏览器的渲染过程如下图所示:


2. 结论如下:


  • DOM 解析和 css 解析是两个独立并行的进行,所以 css 的加载不会阻塞 DOM 的解析

  • 由于 Render Tree 是依赖于 DOM Tree 和 CSSOM Tree 的,所以他必须等待到 CSSOM Tree 构建完成,也就是 CSS 资源加载完成(或者 CSS 资源加载失败)后,才能开始渲染。因此,CSS 加载是会阻塞 Dom 的渲染的

  • 由于 js 可能会操作之前的 Dom 节点和 css 样式,因此浏览器会维持 html 中 css 和 js 的顺序。因此,样式表会在后面的 js 执行前先加载执行完毕。所以 css 会阻塞后面 js 的执行

四、实际场景


1. 页面加载的两个事件


  • onLoad:等待页面的所有资源都加载完成才会触发,这些资源包括 css、js、图片视频等

  • DOMContentLoaded:就是当页面的内容解析完成后,则触发该事件


2. css 加载的影响


  • 如果页面中同时存在 css 和 js,并且存在 js 在 css 后面,则 DOMContentLoaded 事件会在 css 加载完后才执行

  • 其他情况下,DOMContentLoaded 都不会等待 css 加载,并且 DOMContentLoaded 事件也不会等待图片、视频等其他资源加载

链接:https://juejin.cn/post/6967530166520119332





收起阅读 »

深入理解CSS中的z-index

深入理解z-index在MDN上的定义是z-index 属性设定了一个定位元素及其后代元素或 flex 项目的 z-order。 当元素之间重叠的时候, z-index 较大的元素会覆盖较小的元素在上层进行显示。但是这个说明太含糊了,当遇到z-ind...
继续阅读 »

深入理解z-index

MDN上的定义是z-index 属性设定了一个定位元素及其后代元素或 flex 项目的 z-order。 当元素之间重叠的时候, z-index 较大的元素会覆盖较小的元素在上层进行显示。

但是这个说明太含糊了,当遇到z-index不生效的情况时,就不知所以然了,最近也查看了很多和z-index相关的资料,决定把z-index相关知识系统性的梳理一遍.


以前我总是很片面的认为元素在Z轴上的层叠顺序只跟z-index值的大小有关, 属性值大的元素显示在上面、属性值小的元素显示在下面,但是就像下面, 为啥z-index不生效呢,明明box1的z-index属性值大于box2的.


事实上z-index属性并非对所有元素都生效, 它仅对定位元素生效而且定位元素的prosition属性值不为static时才会起作用:

其实判断元素在Z轴的层叠顺序取决于两个方面: 元素所在的层叠上下文元素自身的层叠级别, 在此之前我们先了解一下这两个概念:

z-index

通常情况下,html页面可以被认为是二维的,因为文本,图像和其他元素被排列在页面上而不重叠。在这种情况下,只有一个渲染进程,所有元素都知道其他元素所占用的空间。


CSS 2.1 中, 所有的盒模型元素都处于三维坐标系中。 除了我们常用的横坐标和纵坐标, 盒模型元素还可以沿着“z 轴”层叠摆放, 当他们相互覆盖时,z轴顺序就变得十分重要。这意味着 CSS 允许你在现有的渲染引擎上层叠的摆放盒模型元素。 所有的层都可以用一个整数( z 轴顺序)来表明当前层在 z 轴的位置。 数字越大, 元素越接近观察者。Z 轴顺序用 CSS 的 z-index 属性来指定。z-index的属性值默认为auto,可设置值为一个整数、可为正整数也可以是负整数


层叠上下文

MDN上的定义: 我们假定用户正面向(浏览器)视窗或网页,而 HTML 元素沿着其相对于用户的一条虚构的z轴排开,层叠上下文就是对这些HTML元素的一个三维构想。众HTML元素基于其元素属性按照优先级顺序占据这个空间。


那么如何才能创建层叠上下文呢?我在网上看到过一个, 总结的很好: 目前有三类方法创建层叠上下文



  • 元素自身就能创建的

  • 需要结合z-index才能创建的

  • 不需要z-index 就能创建的


一、元素自身形成层叠上下文


文档根元素(<html>)会自动形成一个层叠上下文, 不需要结合任何其他属性


二、需要配合z-index才能触发创建层叠上下文的


position值为 absolute(绝对定位)或 relative(相对定位)且 z-index 属性值不为 auto 的元素;


采用flex布局容器的子元素, 且子元素 z-index 属性值不为 auto 的元素;


三、不要配合z-index就能触发创建层叠上下文的


position值为 fixed(固定定位)或 sticky(粘滞定位)的元素;


透明度opacity属性值不为1的元素


转换transform属性值不为none的元素


滤镜filter属性值不为none的元素


上面列举出来的都是一些常用到的属性,当然还有其他的属性值设置也能触发形成层叠上下文,这里就不一一列举了,有兴趣的同学可以去MDN文档查看.这里我们需要注意的几点:



  • 层叠上下文可以包含在其他层叠上下文中, 由于根元素HTML本身就是一个层叠上下文,所以页面文档中的创建的层叠上下文都是HTML元素层级的一个子级

  • 当某个元素创建了层叠上下文后, 应当把它及其后代当成一个整体,去判断层叠顺序

  • 父子元素、兄弟元素都可能会处于同一层叠上下文中


层叠级别

在不考虑层叠上下文的情况下, 元素的层叠级别就是判断发生层叠时,元素在Z轴如何显示的依据, 下图就是著名的7阶层叠水平:


background/border、负z-index元素、块级元素、浮动元素、行内/行内块元素、z-index为0元素、正z-index元素.

文本节点我们也看成是一个行内元素

判断层叠顺序

在理解了层叠上下文和元素层叠水平的概念后,现在我们就可以说说元素在Z轴上的层叠顺序到底是怎么回事了: 元素在Z轴上的层叠顺序取决于两个方面: 元素所在的层叠上下文、元素自身的层叠级别,如果抛开层叠上下文来判断元素在Z轴上的层叠顺序就是瞎胡闹


1. 当要比较的两个元素在同一层叠上下文时, 就按照元素自身的层叠级别, 如果级别相同时后则覆盖前者




  • 同一层叠上下文中的兄弟元素



上图box1、box2都处在同一层叠上下文中(html元素形成的上下文) ,二者都是行内块元素,级别相同, 所以后者覆盖前者

  • 上图box1、box2也都处在同一层叠上下文中(html元素形成的上下文),但box1是行内元素,box2是块级元素, 根据元素的层叠级别,行内元素要高于块级元素, 所以box1显示在box2上面; 但是有个奇怪的现象, box1只能覆盖box2的背景,却不能覆盖box2内的字体.......为啥呢? 其实这个现象我们在上面也有提到过: 文本节点我们也看成是一个行内元素, 由于行内元素的级别要高于background/border,所以box1不能覆盖box2元素内的文本节点.




  • 同一层叠上下文中的父子元素


    也有可能是父子元素会出现在同一层叠上下文中, 其实刚刚上面我们说的字体的例子,就可以看成是父子元素在同一层叠上下文中, 这里就不在另外举例啦




2. 当要比较的两个元素不在同一层叠上下文时, 需要先向上查找到两者所在的共同的且最近的层叠上下文,然后在根据第1条规则来判断




链接:https://juejin.cn/post/6967737753983254564
收起阅读 »

我愿赌上一包辣条,这些定位相关你不知道

写码写累了不如来看看这些奇奇怪怪的css,腰不酸腿不疼,一阵轻松加愉快(还能偷我表情包)子元素的绝对定位原点在哪?用了那么久的定位,有注意过子元素的绝对定位原点,是在盒子模型的哪一处,在 padding、border 还是 content?实践出真知。既然不确...
继续阅读 »

写码写累了不如来看看这些奇奇怪怪的css,腰不酸腿不疼,一阵轻松加愉快(还能偷我表情包)

子元素的绝对定位原点在哪?

用了那么久的定位,有注意过子元素的绝对定位原点,是在盒子模型的哪一处,在 padding、border 还是 content?


实践出真知。既然不确定那就实操个例子看看。

<div class="father">
father
<div class="child">child</div>
</div>
body {
background-color: rgb(20, 19, 19);
}

.father {
width: 300px;
height: 300px;
margin: 40px;
border: 20px solid rgb(202, 30, 30);
padding: 40px;
position: relative;
background-color: #eee;
}

.child {
width: 50px;
height: 50px;
position: absolute;
top: 0;
left: 0;
background-color: rgb(228, 207, 17);
}

在 Chrome 90 的版本下的表现。



更换 Edge、火狐、IE 浏览器,以及设置 box-sizing 分别为 border-boxcontent-box,最后的结果都表现一致。


从结果上看,绝对定位的字元素是紧贴着父元素的内边框,绝对定位的原点就是在父元素 padding 的左上角。


如果绝对定位的父亲们都没有设置 relative,那么是将会是定位在哪?

body 下只有一个绝对定位的元素,设置了 bottom:0,那么他的表现将会是如何呢?定位在 body 的底边?

<body>
I am body
<div class="absolute">I am absoluted</div>
</body>
html {
background-color: #fff;
}

body {
height: 50vh;
background-color: #ddd;
}
.absolute {
width: 120px;
height: 50px;
position: absolute;
bottom: 0;
left: 0;
background-color: rgb(0, 0, 0);
color: #fff;
}


从结果上看,绝对定位的元素并不是相对于 body 进行定位的,也不是根据 html 标签,此时的 html 的宽高等同于 body 的宽高,而是根据浏览器视口进行定位的。


所有父元素position 的属性是static 的时候,绝对定位的元素会被包含在初始包含块中,初始包含块有着和浏览器视口一样的大小,所以从表现上来看,就是绝对定位的元素是根据浏览器视口定位。


如果把top和left去掉,那么位置依旧是他原来文档流的位置,只是不占空间了,后面的元素会窜上来。

通过 HTML 结构控制层叠上下文

在使用定位属性时,必不可少的使用 z-index 属性,使用 z-index 属性会创建一个层叠上下文。z-index 值不会在整个文档中进行比较,而只会在该层叠上下文中进行比较。

<div class="bar"></div>
<div class="container">
<div class="bottom-box-1">
<div class="top-box"></div>
</div>
</div>
<div class="container">
<div class="box-container">
<div class="bottom-box-2"></div>
<div class="top-box"></div>
</div>
</div>
body {
display: flex;
justify-content: center;
align-items: center;
height: 90vh;
}
.bar {
position: absolute;
top: 65vh;
z-index: 2;
width: 100vw;
height: 20px;
background: #8bbe6e;
}

.top-box {
position: absolute;
z-index: 3;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 50%;
height: 50%;
background: #626078;
filter: brightness(60%);
}

.bottom-box-1 {
position: absolute;
top: 45%;
z-index: 1;
transition: top 1s;
width: 40px;
height: 40px;
background: #626078;
}

.container:hover .bottom-box-1 {
top: 72%;
}

.box-container {
position: absolute;
top: 45%;
transition: top 1s;
}

.bottom-box-2 {
position: relative;
z-index: 1;
width: 40px;
height: 40px;
background: #626078;
}

.container {
border: 2px dashed #626078;
height: 80%;
width: 100px;
margin: 20px;
padding: 10px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}

.container:hover .box-container {
top: 72%;
}


鼠标分别移入两个虚线框内,我们发现,第二个例子 bar 穿过了两个正方形。


这两者的区别就在于 HTML 结构,在第一个例子中,小正方形在大正方形的里面,大正方形在移动的时候,小正方形也随之移动,但是因为大正方形对决定位且有 z-index 属性不为 auto,因此创建了一个层叠上下文,这导致大正方形内的所有元素都是在这个层叠上下文里层叠。


那么第二个例子是怎么解决的呢?


第二个例子的技巧在于引入了一个新的 div 来包裹这两个正方形,这个新的 div 只负责移动。而里面的大小正方形和 bar 处于同一个层叠上下文中。这样子就可以产生 bar 从两个正方形中穿过的效果。


还没懂的来看图来看图:


总结一下创建层叠上下文的几种情况(别怪我枯燥,就是这么多):



  • 文档根元素<html>;

  • position 值为 relative(相对定位)或 absolute(绝对定位)且 z-index 值不为 auto 的元素;

  • position 值为 fixed(固定定位)或 sticky(粘滞定位)的元素;

  • flex (flexbox) 容器的子元素,且 z-index 值不为 auto;

  • grid 容器的子元素,且 z-index 值不为 auto;

  • opacity 属性值小于 1 的元素;

  • mix-blend-mode 属性值不为 normal 的元素;

  • 以下任意属性值不为 none 的元素:

    • transform

    • filter

    • perspective

    • clip-path

    • mask / mask-image / mask-border



  • isolation 属性值为 isolate 的元素;

  • -webkit-overflow-scrolling 属性值为 touch 的元素;

  • will-change 值设定了任一属性而该属性在 non-initial 值时会创建层叠上下文的元素

  • contain 属性值为 layoutpaint 或包含它们其中之一的合成值(比如 contain: strictcontain: content)的元素;


好了,你学废了嘛~


当定位遇到 Transform

transform 下 absolute 宽度被限制

以前,我们设置 absolute 元素宽度 100%, 则都会参照第一个非static值的position祖先元素计算,没有就window. 现在,=-=,需要把transform也考虑在内了。


默认情况下我们设置 absolute 的宽度 100%,会根据第一个不是static的祖先元素计算,没有就找视口宽度。现在也考虑 CSS3 的 transform 属性了。


<div class="relative">
<div class="transform">
<div class="absolute">i am in transform</div>
</div>
</div>
<div class="relative">
<div class="no-transform">
<div class="absolute">i am not in transform</div>
</div>
</div>
.relative {
position: relative;
width: 400px;
height: 100px;
background-color: rgb(233, 233, 233);
}

.transform {
transform: rotate(0);
width: 200px;
}

.no-transform {
width: 200px;
}

.absolute {
position: absolute;
width: 100%;
height: 100px;
background-color: rgb(137, 174, 255);
}


可以看到绝对定位的宽度是相对 transform 的大小计算了。

transform 对 fixed 的限制

身为大聪明的你,加了一行position:fixed安心上线,结果预览机一瞅,没生效??

position:fixed 正常情况下可以让元素不跟随滚动条滚动,这种行为也无法通过 relative/absolute 来限制。但是遇到 transform,他就被打败了,降级成 absolute
<div class="demo">
<div class="box">
<div class="fixed">
<p>没有transform</p>
<img src="https://gitee.com/zukunft/MDImage/raw/master/img/20210516172759.jpg" alt="">
</div>
</div>

<div class="relative box">
<div class="fixed">
<p> 有relative</p>
<img src="https://gitee.com/zukunft/MDImage/raw/master/img/20210516172759.jpg" alt="">
</div>
</div>
<div class="transform box">
<div class="fixed">
<p>有transform</p>
<img src="https://gitee.com/zukunft/MDImage/raw/master/img/20210516172759.jpg" alt="">
</div>
</div>
</div>
.box {
height: 250px;
}

.demo {
height: 9999px;
}


.fixed {
position: fixed;
}

.relative {
position: relative;
}

.transform {
transform: scale(1);
}


诶,神奇不,滚起来了,就只有被transform包裹的元素会被滚走。
根据W3C的定义,transform属性值会使元素成为一个包含块,它的后代包括absolute元素,fixed元素受限在其 padding box 区域。所以滚动的时候,transform元素被滚走,其子元素也跟随tranform滚走。


关于包含块

上面提到了包含块,那到底如何形成的包含块,包含块又是个啥子

在 MDN 中的解释

The size and position of an element are often impacted by its containing block. Percentage values that are applied to the width, height, padding, margin, and offset properties of an absolutely positioned element (i.e., which has its positionset to absolute or fixed) are computed from the element's containing block.
即一个元素的尺寸和位置受到它的**包含块(containing block)**的影响。对于一些属性,例如 width, height, padding, margin,绝对定位元素的偏移值 (比如 position 被设置为 absolutefixed),当我们设置百分比值的时候,它的这些值计算,就是通过该元素的包含块的值来计算的。

通常情况下,包含块就是这个元素最近的祖先块元素的内容区域,但实际可能不是;


我们可以通过 position 的属性来确定它的包含块;



  1. 如果 position 属性为 staticrelativesticky,包含块可能由它的最近的祖先块元素(比如说 inline-block, blocklist-item 元素)的内容区的边缘组成,也可能会建立格式化上下文(比如说 table container,flex container, grid container, 或者是 block container 自身)

  2. 如果 position 属性为 **absolute** ,包含块就是由它的最近的 position 的值不是 static (也就是值为fixed, absolute, relativesticky)的祖先元素的内边距区的边缘组成。

  3. 如果 position 属性是 fixed,在连续媒体的情况下(continuous media)包含块是 viewport ,在分页媒体(paged media)下的情况下包含块是分页区域(page area)。

  4. 如果 position 属性是 absolutefixed,包含块也可能是由满足以下条件的最近父级元素的内边距区的边缘组成的:

    1. transformperspective的值不是 none

    2. will-change 的值是 transformperspective

    3. filter的值不是 nonewill-change 的值是 filter(只在 Firefox 下生效).

    4. contain 的值是 paint (例如: contain: paint;)




需要注意的是根元素(<html>)所在的包含块是一个被称为初始包含块的矩形。他的尺寸是视口 viewport (for continuous media) 或分页媒体 page media (for paged media).


如果所有的父元素都没有显式地定义position属性,那么所有的父元素默认情况下 position 属性都是static。结果,绝对定位元素会被包含在初始包含块中;这个初始块容器有着和浏览器视口一样的尺寸,并且<html>元素也被包含在这个容器里面。简单来说,绝对定位元素会被放在<html>元素的外面,并且根据浏览器视口来定位。

总结

遇到奇奇怪怪的css问题不要慌~硬调不是好办法不如来我这瞅瞅~~ 万一就解决了呢✿✿ヽ(°▽°)ノ✿


链接:https://juejin.cn/post/6963235293587750943

收起阅读 »

做了这个活动,感觉自己成了垂直排版css大师(文字竖排)

前段时间协助完成作家的盘点活动,设计大大给的视觉稿如下图。最初拿到稿子时,以为是一个简单的常规活动,实际在完成这个活动的过程中有一点点小波折,自己也在这个过程中学习到了关于垂直排版的知识点,下面来跟大家描述做这个活动的心路历程,一起探索垂直排版的奥秘~文字竖排...
继续阅读 »

前段时间协助完成作家的盘点活动,设计大大给的视觉稿如下图。最初拿到稿子时,以为是一个简单的常规活动,实际在完成这个活动的过程中有一点点小波折,自己也在这个过程中学习到了关于垂直排版的知识点,下面来跟大家描述做这个活动的心路历程,一起探索垂直排版的奥秘~


文字竖排🤔,立马想到使用 writing-mode,改变文字展示的行进方向


大家都知道的writing-mode


writing-mode 可以将文档流由水平方向调整为垂直方向


以p标签内的一段文本为例,对其添加 writing-mode: vertical-rl,可以快速实现如下图所示的文字竖排效果


p标签示例代码如下:

<p class="text-vertical">爱交流,也爱独处</p>

.text-vertical {
writing-mode: vertical-rl;
}

一顿页面基础布局下来,可以看到页面效果和设计稿还是有差异😔,具体如下图的红色框线内;设计稿中红色框线内的数字是“直立向上”展示的


感觉页面还原的展示也无伤大雅,于是拿着对比图找设计大大沟通;但是设计大大还是比较坚持视觉稿上的数字展示效果,以便更好传达活动内的关键信息(用户不需要旋转手机 or 侧头即可快速看清数据

在纠结更好实现方案的时候,去请教了一下张老师,张老师提示可以使用 text-orientation:upright,即使是较差机型,可以优雅降级展示非竖向的效果,不影响活动


向产品咨询了产品用户机型占比,较差机型的占比很低很低,而且产品也接受较低端机型降级的效果,完美💯

部分人知道都text-orientation

text-orientation: upright 可以将垂直排版的(设置了 writing-mode,且值不为 horizontal-tb)内容均直立向上展示


在之前的示例基础上,添加 text-orientation: upright 即可实现将数字直立向上展示,面效果可以看下方图片中红色线框内数字


p标签示例代码见下方:

<p class="text-vertical">999篇日记,12356万字</p>
.text-vertical {
writing-mode: vertical-rl;
text-orientation: upright;
-webkit-text-orientation: upright;
}

调整完成后,活动大体完成,活动进入测试阶段;活动提测后,测试随口提了一句“这个日期看着有一点点不方便”



虽然测试只是随口一说,但我就放在了心上,思考有没有可能优化一下这个日期显示呢?念念不忘,必有回响,偶然发现 text-combine-upright 的属性可以解决此类竖排横向合并

较少人知道的text-combine-upright


text-combine-upright,可以横向合并显示 2-4 字符,正好特别适合垂直排版中日期横向显示


下图中红色线框内,就是添加 text-combine-upright: all 后日期横向合并的效果,“10” 被合并到一起展示,更利于读者快速获取文字信息


示例代码见下方:

<p class="text-vertical">
<span class="upright-combine">10</span>月
<span class="upright-combine">1</span>日时光日记上线
</p>
.text-vertical {
writing-mode: vertical-rl;
text-orientation: upright;
}
.upright-combine {
/* for IE11+ */
-ms-text-combine-horizontal: all;
/* for Chrome/Firefox */
text-combine-upright: all;
/* for Safari */
-webkit-text-combine: horizontal;
}

1)将待合并元素外包裹一层标签


2)添加 text-combine-upright: all


text-combine-upright 属性支持关键字值和数字值,考虑到数字值的兼容性不佳,此处主要是用使用关键字all实现的


3)兼容处理,对 IE 浏览器和 Safari 浏览器做兼容处理(支持 IE11+),说明及代码见下方:


IE 浏览器使用的是 -ms-text-combine-horizontal 属性,与 text-combine-upright 属性一致 Safari 浏览器使用 -webkit-text-combine 属性,仅支持 none 和 horizontal 属性值


项目中实际代码片段为下方所示,可以兼容移动端项目,日期展示优化完成!

.upright-combine {
/* for IE11+ */
-ms-text-combine-horizontal: all;
text-combine-upright: all;
/* forSafari */
-webkit-text-combine: horizontal;
}

text-combine-upright 使用注意事项:



  • 只用于 2-4 字符的横向合并,例如日期、年份等

  • 由于数字值的兼容性不佳,需要将待横向合并的内容包裹标签


至此,该活动顺利完成并上线了,在完成改活动的过程并非一蹴而就。关于数字垂直排版的实现方案,中间也实践过一些其他方案,可能会对大家未来实现类似场景有参考价值~

数字垂直排版的其他方案

最初在考虑实现数字垂直排版的时候,实践过其他方案,下面跟大家分享一下~


1.JS分割大法


JS 将所有的文本信息均切割为单个标签,将单个文本内容都作为 block 元素,此时无需设置 writing-mode,直接设置外段落文本的最大宽度为 1em,即可达到内容竖向 + 数字直立向上效果


核心代码如下所示:

function text2html(element) {
var treeWalker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
var node;
let list = []
while ((node = treeWalker.nextNode())) {
list.push(node)
}
// 纯文本节点数组遍历
list.forEach(function(node) {
// 纯文本节点切割,替换为span标签
var replaceArr = node.textContent.split('').map(function(str) {
var newSpan = document.createElement('span');
newSpan.textContent = str;
return newSpan;
});
node.replaceWith.apply(node, replaceArr);
})
}

JS 分割大法,将所有文本内容拆分成单个标签,方案简单粗暴,可能会提高样式布局难度,有潜在风险,故最终未采用该方法,但它较适合对局部个别文案处理、兜底展示或者低端机型处理

2.半角数字转全角

数字为全角时,此时垂直展示的时候,它的展示效果和中文类似,天然“直立向上” ,因而可以将半角数字转成全角数字

数字半角转全角的示例代码如下:

// 数字半转全 0-9 对应 半角对应48-57,全角对应65296-65305
function ToDBC(txtstring) {
var newNumber = "";
for(var i = 0; i < txtstring.length; i++) {
if(txtstring.charCodeAt(i) <= 57 && txtstring.charCodeAt(i) >= 48) {
newNumber = newNumber + String.fromCharCode(txtstring.charCodeAt(i)+65248);
} else {
newNumber = newNumber + txtstring.substring(i, i+1);
}
}
return newNumber;
}

数字半角转全角的方法看似非常完美,但是后续有需要额外考虑哪些不转全角,并且涉及到一些数字计算判断时,还需要把全角转半角(全角数字无法直接参与逻辑运算),展示再转全角,处理起来较为繁琐,故最终没选择此技术方案

数字“直立向上”方案小结



  • CSS的 text-orientation

  • 暴力拆解 DOM

  • 调整数字为全角


关于数字“直立向上”的实现,如果大家有其他好的思考角度或者解决方案,欢迎分享交流~


最终的归纳总结



  • 竖向展示排版通过 writing-mode 可快速实现

  • 对于部分数字需要“直立向上”时,采用 text-orientation:upright 方法

  • 日期类字符(2-4字符)可以通过 text-combine-upright 属性横向合并优化展示



链接:https://juejin.cn/post/6966449320744714277
收起阅读 »

6分钟实现CSS炫光倒影按钮

话不多,先看效果: 回归老本行,继续分享简单有趣的CSS创意特效,放松放松心情~实现过程(完整源码在最后):1 老样子,定义基本样式:*{ margin: 0; padding: 0; box...
继续阅读 »

话不多,先看效果:

 回归老本行,继续分享简单有趣的CSS创意特效,放松放松心情~


实现过程(完整源码在最后):

1 老样子,定义基本样式:

*{
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'fangsong';
}
body{
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: rgb(0, 0, 0);
}

font-family: 'fangsong'; 仿宋字体。 display: flex; align-items: center; justify-content: center; flex布局,让按钮在屏幕居中。

2.定义基本标签:

        
aurora






aurora






aurora




3个a标签就对应3个按钮,每个按钮里4个span就是环绕按钮的4条边。 且都有个公共的选择器 .item 和 只属于自己的选择器。

3.定义每个按钮的基本样式:

    .item{
position: relative;
margin: 50px;
width: 300px;
height: 80px;
text-align: center;
line-height: 80px;
text-transform: uppercase;
text-decoration: none;
font-size: 35px;
letter-spacing: 5px;
color: aqua;
overflow: hidden;
-webkit-box-reflect: below 1px linear-gradient( transparent,rgba(6, 133, 133,0.3));
}
text-align: center;文字对齐方式。
line-height: 80px; 字行高。
text-transform: uppercase; 字母为大写。
text-decoration: none; 去掉a标签默认下划线。
letter-spacing: 5px; 每个字符间的距离。
overflow: hidden;溢出隐藏。
-webkit-box-reflect: below 1px linear-gradient( transparent,rgba(6, 133, 133,0.3)); 这个属性能实现倒影效果。

4. 鼠标经过按钮样式改变:

.item:hover{
background-color: aqua;
box-shadow:0 0 5px aqua,
0 0 75px aqua,
0 0 155px aqua;
color: black;
}

box-shadow:0 0 5px aqua, 0 0 75px aqua, 0 0 155px aqua; 阴影,写多行可以叠加更亮。

5.设置环绕按钮的4根线上面那条的样式:

    .item span:nth-of-type(1){
position: absolute;
left: -100%;
width: 100%;
height: 3px;
background-image: linear-gradient(to left,aqua ,transparent);
animation: shang 1s linear infinite;
}
@keyframes shang{
0%{
left:-100%;
}
50%,100%{
left:100%;
}
}

position: absolute;
left: -100%; 定位在对应位置。
background-image: linear-gradient(to left,aqua ,transparent); 线性渐变颜色。
animation: shang 1s linear infinite; 动画属性,让它动起来。


5.以此类推,设置环绕按钮的其它3根样式:

.item span:nth-of-type(2) {
position: absolute;
top: -100%;
right: 0;
width: 3px;
height: 100%;
background-image: linear-gradient(to top, aqua, transparent);
animation: you 1s linear infinite;
animation-delay: 0.25s;
}
@keyframes you {
0% {
top: -100%;
}
50%,
100% {
top: 100%;
}
}
.item span:nth-of-type(3) {
position: absolute;
right: -100%;
bottom: 0;
width: 100%;
height: 3px;
background-image: linear-gradient(to right, aqua, transparent);
animation: xia 1s linear infinite;
animation-delay: 0.5s;
}
@keyframes xia {
0% {
right: -100%;
}
50%,
100% {
right: 100%;
}
}
.item span:nth-of-type(4) {
position: absolute;
bottom: -100%;
left: 0;
width: 3px;
height: 100%;
background-image: linear-gradient(to bottom, aqua, transparent);
animation: zuo 1s linear infinite;
animation-delay: 0.75s;
}
@keyframes zuo {
0% {
bottom: -100%;
}
50%,
100% {
bottom: 100%;
}
}

animation-delay: 0.75s; 动画延迟执行。每条线对应延迟一段时间,形成时间差,形成环绕效果。

6.给第一,第三个按钮设置其它颜色:

    .item1{
filter: hue-rotate(100deg);
}
.item3{
filter: hue-rotate(250deg);
}

完整代码:

<!DOCTYPE html>
<html lang="zh-CN">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'fangsong';
}

body {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: rgb(0, 0, 0);
}

.item {
position: relative;
margin: 50px;
width: 300px;
height: 80px;
text-align: center;
line-height: 80px;
text-transform: uppercase;
text-decoration: none;
font-size: 35px;
letter-spacing: 5px;
color: aqua;
overflow: hidden;
-webkit-box-reflect: below 1px linear-gradient(transparent, rgba(6, 133, 133, 0.3));
}

.item:hover {
background-color: aqua;
box-shadow: 0 0 5px aqua,
0 0 75px aqua,
0 0 155px aqua;
color: black;
}

.item span:nth-of-type(1) {
position: absolute;
left: -100%;
width: 100%;
height: 3px;
background-image: linear-gradient(to left, aqua, transparent);
animation: shang 1s linear infinite;
}

@keyframes shang {
0% {
left: -100%;
}

50%,
100% {
left: 100%;
}
}

.item span:nth-of-type(2) {
position: absolute;
top: -100%;
right: 0;
width: 3px;
height: 100%;
background-image: linear-gradient(to top, aqua, transparent);
animation: you 1s linear infinite;
animation-delay: 0.25s;
}

@keyframes you {
0% {
top: -100%;
}

50%,
100% {
top: 100%;
}
}

.item span:nth-of-type(3) {
position: absolute;
right: -100%;
bottom: 0;
width: 100%;
height: 3px;
background-image: linear-gradient(to right, aqua, transparent);
animation: xia 1s linear infinite;
animation-delay: 0.5s;
}

@keyframes xia {
0% {
right: -100%;
}

50%,
100% {
right: 100%;
}
}

.item span:nth-of-type(4) {
position: absolute;
bottom: -100%;
left: 0;
width: 3px;
height: 100%;
background-image: linear-gradient(to bottom, aqua, transparent);
animation: zuo 1s linear infinite;
animation-delay: 0.75s;
}

@keyframes zuo {
0% {
bottom: -100%;
}

50%,
100% {
bottom: 100%;
}
}

.item1 {
filter: hue-rotate(100deg);
}

.item3 {
filter: hue-rotate(250deg);
}
</style>
</head>

<body>

<a href="#" class="item item1">
aurora
<span></span>
<span></span>
<span></span>
<span></span>
</a>
<a href="#" class="item item2">
aurora
<span></span>
<span></span>
<span></span>
<span></span>
</a>
<a href="#" class="item item3">
aurora
<span></span>
<span></span>
<span></span>
<span></span>
</a>

</body>

</html>


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

收起阅读 »

因为这几个 TypeScript 代码的坏习惯,同事被罚了 500 块

近几年 TypeScript 和 JavaScript 一直在稳步发展。我们在过去写代码时养成了一些习惯,而有些习惯却没有什么意义。以下是我们都应该改正的 10 个坏习惯。1.不使用 strict 模式这种习惯看起来是什么样的没有用严格模式编写 tsconfi...
继续阅读 »

近几年 TypeScript 和 JavaScript 一直在稳步发展。我们在过去写代码时养成了一些习惯,而有些习惯却没有什么意义。以下是我们都应该改正的 10 个坏习惯。

1.不使用 strict 模式

  • 这种习惯看起来是什么样的

没有用严格模式编写 tsconfig.json。

{
"compilerOptions": {
"target": "ES2015",
"module": "commonjs"
}
}
  • 应该怎样

只需启用 strict 模式即可:

{
"compilerOptions": {
"target": "ES2015",
"module": "commonjs",
"strict": true
}
}


  • 为什么会有这种坏习惯

在现有代码库中引入更严格的规则需要花费时间。

  • 为什么不该这样做

更严格的规则使将来维护代码时更加容易,使你节省大量的时间。

2. 用 ||定义默认值

  • 这种习惯看起来是什么样的

使用旧的 ||处理后备的默认值:

function createBlogPost (text: string, author: string, date?: Date) {
return {
text: text,
author: author,
date: date || new Date()
}
}


  • 应该怎样


使用新的 ??运算符,或者在参数重定义默认值。


function createBlogPost (text: string, author: string, date: Date = new Date())
return {
text: text,
author: author,
date: date
}
}


  • 为什么会有这种坏习惯


??运算符是去年才引入的,当在长函数中使用值时,可能很难将其设置为参数默认值。



  • 为什么不该这样做


??与 ||不同,??仅针对 null 或 undefined,并不适用于所有虚值。

3. 随意使用 any 类型

  • 这种习惯看起来是什么样的

当你不确定结构时,可以用 any 类型。

async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: any = await response.json()
return products
}
  • 应该怎样


把你代码中任何一个使用 any 的地方都改为 unknown


async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: unknown = await response.json()
return products as Product[]
}


  • 为什么会有这种坏习惯


any 是很方便的,因为它基本上禁用了所有的类型检查。通常,甚至在官方提供的类型中都使用了 any。例如,TypeScript 团队将上面例子中的 response.json()的类型设置为 Promise。



  • 为什么不该这样做


它基本上禁用所有类型检查。任何通过 any 进来的东西将完全放弃所有类型检查。这将会使错误很难被捕获到。


4. val as SomeType



  • 这种习惯看起来是什么样的


强行告诉编译器无法推断的类型。


async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: unknown = await response.json()
return products as Product[]
}


  • 应该怎样


这正是 Type Guard 的用武之地。


function isArrayOfProducts (obj: unknown): obj is Product[] {
return Array.isArray(obj) && obj.every(isProduct)
}

function isProduct (obj: unknown): obj is Product {
return obj != null
&& typeof (obj as Product).id === 'string'
}

async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: unknown = await response.json()
if (!isArrayOfProducts(products)) {
throw new TypeError('Received malformed products API response')
}
return products
}


  • 为什么会有这种坏习惯


从 JavaScript 转到 TypeScript 时,现有的代码库通常会对 TypeScript 编译器无法自动推断出的类型进行假设。在这时,通过 asSomeOtherType 可以加快转换速度,而不必修改 tsconfig 中的设置。



  • 为什么不该这样做


Type Guard 会确保所有检查都是明确的。

5. 测试中的 as any



  • 这种习惯看起来是什么样的


编写测试时创建不完整的用例。


interface User {
id: string
firstName: string
lastName: string
email: string
}

test('createEmailText returns text that greats the user by first name', () => {
const user: User = {
firstName: 'John'
} as any

expect(createEmailText(user)).toContain(user.firstName)
}


  • 应该怎样


如果你需要模拟测试数据,请将模拟逻辑移到要模拟的对象旁边,并使其可重用。


interface User {
id: string
firstName: string
lastName: string
email: string
}

class MockUser implements User {
id = 'id'
firstName = 'John'
lastName = 'Doe'
email = 'john@doe.com'
}

test('createEmailText returns text that greats the user by first name', () => {
const user = new MockUser()

expect(createEmailText(user)).toContain(user.firstName)
}


  • 为什么会有这种坏习惯


在给尚不具备广泛测试覆盖条件的代码编写测试时,通常会存在复杂的大数据结构,但要测试的特定功能仅需要其中的一部分。短期内不必关心其他属性。



  • 为什么不该这样做


在某些情况下,被测代码依赖于我们之前认为不重要的属性,然后需要更新针对该功能的所有测试。


6. 可选属性



  • 这种习惯看起来是什么样的


将属性标记为可选属性,即便这些属性有时不存在。


interface Product {
id: string
type: 'digital' | 'physical'
weightInKg?: number
sizeInMb?: number
}


  • 应该怎样


明确哪些组合存在,哪些不存在。


interface Product {
id: string
type: 'digital' | 'physical'
}

interface DigitalProduct extends Product {
type: 'digital'
sizeInMb: number
}

interface PhysicalProduct extends Product {
type: 'physical'
weightInKg: number
}


  • 为什么会有这种坏习惯


将属性标记为可选而不是拆分类型更容易,并且产生的代码更少。它还需要对正在构建的产品有更深入的了解,并且如果对产品的设计有所修改,可能会限制代码的使用。



  • 为什么不该这样做


类型系统的最大好处是可以用编译时检查代替运行时检查。通过更显式的类型,能够对可能不被注意的错误进行编译时检查,例如确保每个 DigitalProduct 都有一个 sizeInMb。


7. 用一个字母通行天下



  • 这种习惯看起来是什么样的


用一个字母命名泛型


function head<T> (arr: T[]): T | undefined {
return arr[0]
}


  • 应该怎样


提供完整的描述性类型名称。


function head<Element> (arr: Element[]): Element | undefined {
return arr[0]
}


  • 为什么会有这种坏习惯


这种写法最早来源于 C++的范型库,即使是 TS 的官方文档也在用一个字母的名称。它也可以更快地输入,只需要简单的敲下一个字母 T 就可以代替写全名。



  • 为什么不该这样做


通用类型变量也是变量,就像其他变量一样。当 IDE 开始向我们展示变量的类型细节时,我们已经慢慢放弃了用它们的名称描述来变量类型的想法。例如我们现在写代码用 constname ='Daniel',而不是 conststrName ='Daniel'。同样,一个字母的变量名通常会令人费解,因为不看声明就很难理解它们的含义。


8. 对非布尔类型的值进行布尔检查



  • 这种习惯看起来是什么样的


通过直接将值传给 if 语句来检查是否定义了值。


function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}


  • 应该怎样


明确检查我们所关心的状况。


function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages !== undefined) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}


  • 为什么会有这种坏习惯


编写简短的检测代码看起来更加简洁,使我们能够避免思考实际想要检测的内容。



  • 为什么不该这样做


也许我们应该考虑一下实际要检查的内容。例如上面的例子以不同的方式处理 countOfNewMessages 为 0 的情况。


9. ”棒棒“运算符



  • 这种习惯看起来是什么样的


将非布尔值转换为布尔值。


function createNewMessagesResponse (countOfNewMessages?: number) {
if (!!countOfNewMessages) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}



  • 应该怎样


明确检查我们所关心的状况。


function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages !== undefined) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}


  • 为什么会有这种坏习惯


对某些人而言,理解 !!就像是进入 JavaScript 世界的入门仪式。它看起来简短而简洁,如果你对它已经非常习惯了,就会知道它的含义。这是将任意值转换为布尔值的便捷方式。尤其是在如果虚值之间没有明确的语义界限时,例如 null、undefined 和 ''。



  • 为什么不该这样做


与很多编码时的便捷方式一样,使用 !!实际上是混淆了代码的真实含义。这使得新开发人员很难理解代码,无论是对一般开发人员来说还是对 JavaScript 来说都是新手。也很容易引入细微的错误。在对“非布尔类型的值”进行布尔检查时 countOfNewMessages 为 0 的问题在使用 !!时仍然会存在。


10. != null



  • 这种习惯看起来是什么样的


棒棒运算符的小弟 ! = null 使我们能同时检查 null 和 undefined。


function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages != null) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}



  • 应该怎样


明确检查我们所关心的状况。


function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages !== undefined) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}



  • 为什么会有这种坏习惯


如果你的代码在 null 和 undefined 之间没有明显的区别,那么 != null 有助于简化对这两种可能性的检查。



  • 为什么不该这样做


尽管 null 在 JavaScript 早期很麻烦,但 TypeScript 处于 strict 模式时,它却可以成为这种语言中宝贵的工具。一种常见模式是将 null 值定义为不存在的事物,将 undefined 定义为未知的事物,例如 user.firstName=== null 可能意味着用户实际上没有名字,而 user.firstName=== undefined 只是意味着我们尚未询问该用户(而 user.firstName===的意思是字面意思是 ''。


链接:https://juejin.cn/post/6951934677682749477


收起阅读 »

TypeScript进阶, 如何避免 any

为什么会出现 any不知道如何准确的定义出类型,TS 报错了,用 any 能解决,便用 any 了觉得定义类型浪费时间,项目经理催的紧,工期紧张,any 更方便频繁使用 any&n...
继续阅读 »

为什么会出现 any

  • 不知道如何准确的定义出类型,TS 报错了,用 any 能解决,便用 any 了
  • 觉得定义类型浪费时间,项目经理催的紧,工期紧张,any 更方便

频繁使用 any 的弊端

  • 不利于良好的编码习惯
  • 不利于项目的后续维护
  • 会出现很多本可避免的 bug

非必要不使用 any 的好处

  • 良好的代码提示
  • 强大的静态类型检查
  • 可读性和可维护性

所以,我们要对 AnyScript 说不!

TS 容易出现 any 的场景梳理

给 window 全局对象增加属性

常常能见到这样的写法

;(<any>window).obj = {}(
// 或
window as any
).obj = {}

这样做,在使用时和赋值时都需要断言一次,非常麻烦,并且使用时也不能得到代码提示

正确的做法应该是

  1. 在项目全局的 xxx.d.ts 文件中配置如下代码
interface Window {
obj: {}
}
  1. 在需要给 window 赋值的文件目录下级新建一个 @types 文件夹,并在其中新建 index.d.ts 文件,添加如下代码
interface Window {
obj: {}
}

方法 2 也会在全局的 window 上增加 obj 这一声明,如果新增属性使用的跨度比较大,则推荐放在项目的 index.d.ts 中更利于维护,两种方式都在全局给 window 添加了属性,但方法 1 能一眼看出项目中 window 中添加了什么属性


正确使用可选链、非空断言

错误的理解 typescript 的可选参数,而使用断言导致隐患

const a: {
b: string
c?: {
d: string
}
} = {
b: "123",
}

console.log((<any>a).c.d) // 错误,这样访问会报错,应使用可选链
console.log(a.c!.d) // 错误,ts 不会将错误抛出,但实际访问也会报错

! 非空断言与 as 有相似之处,主要用于断言某个属性的值不为 null 和 undefined,它不会影响最终的代码产物,只会影响代码编译过程中的类型校验

?. 可选链操作符 会影响编译后的代码产物,如:

这段 ts 代码

const a = {
c: undefined,
}

const b = a?.c?.d

会被编译为如下 js 代码

"use strict"

var _a$c

const a = {
c: undefined,
}
const b =
a === null || a === void 0
? void 0
: (_a$c = a.c) === null || _a$c === void 0
? void 0
: _a$c.d

将对象属性类型关联起来

对象中有多个属性是联合类型,其中 a 属性和 b 属性是有关联的,a 为 1 时,b 为 stringa 为 2 时,b 为 number 我们通常是这样定义的

const obj: {
a: 1 | 2
b: string | number
} = {
a: 1,
b: "1.2"
}


那么使用时,会造成需要用断言来再次限定 b 的范围的情况,如下代码段所示

if (obj.a === 1) {
const [left, right] = (obj.b as string).split(".")
}
// 如果你偷懒,那可能又变成了这样的情况
if (obj.a === 1) {
const [left, right] = (obj.b as any).split(".")
}

有没有什么办法能让我们不再 as 一次呢?有

const obj: {
a: 1
b: string
} | {
a: 2
b: number
} = {
a: 1,
b: "1.2"
}
// 你会发现这样定义了以后,不需要再次进行断言限定 obj.b 的范围
if (obj.a === 1) {
const [left, right] = obj.b.split(".") // 校验通过
}

如果我们把这样的方法应用到函数(也可以用重载实现)传参或组件传参,有意思的是它还能限定传参的范围, 函数组件实现:


错误的传参,a 与 b 的类型不匹配,校验不通过

确的传参,校验能通过

注意:你不能将 props 解构出来,会导致两者的关系丢失
const { a, b } = props // 错误,a 和 b 的类型关系丢失

是否使用联合类型需要辩证的看待,在任何时候都用上述方法定义可能会造成一些臃肿


巧用类型保护避免断言


typescript 中,常用的类型保护为 typeofinstanceof、和 in 关键字
掌握上述关键字较为容易,可通过文档了解
还有一个关键字 is (类型谓词)是 typescript 提供的,是另一种“类型保护”(这种说法助于理解)


类型谓词能让我们通过函数的形式做出复杂的类型检验的逻辑,一个使用类型谓词的函数的声明往往是如下形式:

type X = xxxx // 某种类型
function check(params): params is X

理解起来就是如果 check 函数返回了真值,则参数 paramsX 类型,否则不一定是 X 类型


设想一下如下场景,某个项目,既可能运行在微信网页中,也可能运行在其他 webview


在微信网页中,微信客户端向 window 对象中注入了各种 native 方法,使用它的方式就是 window.wx.xxxx()


在其他 webview 中,我们假设也有这样的 native 方法,并且使用它的方式为 window.webviewnative.xxxx()


在 typescript 项目中,window 对象上并不会默认存在 wxwebviewnative 两个属性,参考 给 window 全局对象增加属性,我们能显示地为 wxwebviewnative 两个属性定义类型:

interface Window {
wx?: {
xxxx: Function
}
webviewnative?: {
xxxx: Function
}
}

如果你不会这样做,那可能又会写成断言为 any(window as any).wx.xxxx()


可以看到在上面的代码段中两个属性都被我定义为了可选属性,目的是为了在后续维护(迭代)中,防止不做判断直接链式调用


在微信环境中 window.wx 一定存在,但 webviewnative 一定不存在,反之在其他的 webview 中,(见前文假设)window.webviewnative 一定存在


在接口 interface 中,我们并不能动态的知晓和定义到底哪个存在


你可以这样写

if (typeof window.wx !== 'undefined') {
window.wx.xxxx()
} else {
// not in wx
}

但是直接在 if 中写这样的表达式太过局限,或者 有很多方式都能判断是在微信环境中,会导致项目中充斥着五花八门的判断,类型谓词的好处就出来了


function checkIsWxNativeAPICanUse(win: Window): win is { wx: Exclude<Window['wx'], undefined> } & Window {
return typeof window.wx !== 'undefined'
}
// 使用
if (checkIsWxNativeAPICanUse(window)) {
window.wx.xxxx()
}

总结

非必要少使用 any 既是良好的 ts 代码习惯的养成,也是对自己代码质量的较真

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



收起阅读 »

用ts类型系统实现斐波那契数列

一、我们要做什么我们的目的是想要通过TypeScript的类型声明式语法,编程实现一个斐波那契数列算法。换句话说,类似于用现有的机器码到指令集、二进制到十进制、汇编语言到高级编程语言的过程,让类型定义语法也可以实现编程。最终我们要实现的斐波那契数列代码是这样的...
继续阅读 »

一、我们要做什么

我们的目的是想要通过TypeScript的类型声明式语法,编程实现一个斐波那契数列算法。换句话说,类似于用现有的机器码到指令集、二进制到十进制、汇编语言到高级编程语言的过程,让类型定义语法也可以实现编程。

最终我们要实现的斐波那契数列代码是这样的?

const fib = (n: number): number => n <= 1 ? n : fib(n - 1) + fib(n - 2);

for (let i = 0; i < 10; i++) {
console.log(i, fib(i));
}

运行结果如下:


程序完全没问题,完结撒花!

开玩笑的,上面是只一个用了TypeScript类型定义的JavaScript写法,我们其实真正想这样做↓↓↓, 也就是使用TS Type解决FIbonacci

import { Fib, Add } from './fib-type';

type one = Fib<1>;
type zero = Fib<0>;
type Two = Add<one, one>;
type Five = Add<Two, Add<Two, one>>;
type Fib5 = Fib<Five>;
type Fib9 = Fib<9>;
type r0 = Fib<Zero>; // type r0= 0
type r1 = Fib<One>; // type r1 = 1
type r2 = Fib<Two>; // type r2 = 1
type r3 = Fib<3>; // type r3 = 2
type r4 = Fib<4>; // type r4 = 3
type r5 = Fib<5>; // type r5 = 5
type r6 = Fib<6>; // type r6 = 8
type r9 = Fib<9>; // type r9 = 34
type sum = Add<r9, r6>; // type sum = 42

二、我们该怎么做

要想实现斐波那契数列,参考一开始的代码,有基本的比较, 加法, 循环语法, 所以我们也需要使用类型系统依次实现这三种功能

2.1 加法的实现

为了实现加法, 需要先实现一些工具类型

// 元组长度
type Length<T extends any[]> = T['length'];
type one = 1

// 使用extends实现数字相等的比较
type a111 = 0 extends one ? true : false // type a111 = false
type a112 = 1 extends one ? true : false // type a112 = true

range的实现是递归实现的

// 伪代码
function range(n, list=[]){
if(n<=0) return list.length
return range(n-1, [1, ...list])
}

TypeScript的限制, 没有循环, 只能用递归代替循环, 后面会有几个类似的写法, 记住一点:递归有几个出口, 对象就有几个 key, 每个 key 就是一个条件

// 创建指定长度的元组, 用第二个参数携带返回值
type Range<T extends Number = 0, P extends any[] = []> = {
0: Range<T, [any, ...P]>;
1: P;
}[Length<P> extends T ? 1 : 0];

// 拼接两个元组
type Concat<T extends any[], P extends any[]> = [...T, ...P];

type t1 = Range<3>;
// type t1 = [any, any, any]

type Zero = Length<Range<0>>;
// type Zero = 0
type Ten = Length<Range<10>>;
// type Ten = 10

type Five = Length<Range<5>>;
// type Five = 5

type One = Length<Range<1>>;

有了上面的工具语法,那么实现加法就比较容易了, 只需要求两个元组合并后的长度

type Add<T extends number, P extends number> = Length<
Concat<Range<T>, Range<P>>
>;
type Two = Add<One, One>;
// type Two = 2
type Three = Add<One, Two>;
// type Three = 3

有了加法,该如何实现减法呢?一般减法和除法都比加法难, 所以我们需要更多的工具类型函数

2.2 工具函数

2.2.1 实现一些基本工具类型

  • Shift:删除第一个元素
  • Append:在元组末尾插入元素
  • IsEmpty / NotEmpty:判断列表为空
// 去除元组第一个元素 [1,2,3] -> [2,3]
type Shift<T extends any[]> = ((...t: T) => any) extends (
_: any,
...Shift: infer P
) => any
? P
: [];

type pp = Shift<[number, boolean,string, Object]>
// type pp = [boolean, string, Object]

// 向元组中追加
type Append<T extends any[], E = any> = [...T, E];
type IsEmpty<T extends any[]> = Length<T> extends 0 ? true : false;
type NotEmpty<T extends any[]> = IsEmpty<T> extends true ? false : true;
type t4 = IsEmpty<Range<0>>;
// type t4 = true

type t5 = IsEmpty<Range<1>>;
// type t5 = false

2.2.2 逻辑类型

  • Anda && b
// 逻辑操作
type And<T extends boolean, P extends boolean> = T extends false
? false
: P extends false
? false
: true;
type t6 = And<true, true>;
// type t6 = true

type t7 = And<true, false>;
// type t7 = false

type t8 = And<false, false>;
// type t8 = false

type t9 = And<false, true>;
// type t9 = false

2.2.3 小于等于

伪代码: 主要思想是同时从列表中取出一个元素, 长度先到0的列表比较短

function dfs (a, b){
if(a.length && b.length){
a.pop()
b.pop()
return dfs(a,b)
}else if(a.length){
a >= b
}else if (b.length){
b > a
}
}

思想:将数字的比较转换为列表长度的比较

// 元组的小于等于   T <= P, 同时去除一个元素, 长度先到0的比较小
type LessEqList<T extends any[], P extends any[]> = {
0: LessEqList<Shift<T>, Shift<P>>;
1: true;
2: false;
}[And<NotEmpty<T>, NotEmpty<P>> extends true
? 0
: IsEmpty<T> extends true
? 1
: 2];


// 数字的小于等于
type LessEq<T extends number, P extends number> = LessEqList<Range<T>, Range<P>>;

type t10 = LessEq<Zero, One>;
// type t10 = true
type t11 = LessEq<One, Zero>;
// type t11 = false

type t12 = LessEq<One, One>;
// type t12 = true

2.3 减法的实现

减法有两个思路,列表长度相减求值和数字相减求值

2.3.1 列表减法

默认大减小, 小减大只需要判断下反着来, 然后加个符号就行了, 这里为了简单没有实现,可参考伪代码如下:

// 伪代码
const a = [1, 2, 3];
const b = [4, 5];
const c = [];
while (b.length !== a.length) {
a.pop();
c.push(1);
}// c.length === a.length - b.lengthconsole.log(c.length);

// 元组的减法 T - P, 同时去除一个元素, 长度到0时, 剩下的就是结果, 这里使用第三个参数来携带结果, 每次做一次减法, 向第三个列表里面追加
type SubList<T extends any[], P extends any[], R extends any[] = []> = {
0: Length<R>;
1: SubList<Shift<T>, P, Apped<R>>;
}[Length<T> extends Length<P> ? 0 : 1];
type t13 = SubList<Range<10>, Range<5>>;
// type t13 = 5

2.3.2 数字减法

思想:将数字转成元组后再比较

// 集合大小不能为负数, 默认大减小
// 数字的减法
type Sub<T extends number, P extends number> = {
0: Sub<P, T>;
1: SubList<Range<T>, Range<P>>;
}[LessEq<T, P> extends true ? 0 : 1];

type t14 = Sub<One, Zero>;
// type t14 = 1
type t15 = Sub<Ten, Five>;
// type t15 = 5

我们有了这些工具后, 就可以将一开始用JavaScript实现的斐波那契数列的实现代码,翻译为TypeScript类型编码

三、Fib: JS函数 --> TS类型

在JavaScript中,我们使用函数

const fib = (n: number): number => n <= 1 ? n : fib(n - 1) + fib(n - 2);

在TypeScript中,我们使用类型, 其实只是换了一种写法, 用类型函数描述运算, 万变不离其宗~

由于TypeScript递归限制, 并不能求解非常大的项, 不过好玩就完事了~

export type Fib<T extends number> = {
0: T;
1: Add<Fib<Sub<T, One>>, Fib<Sub<T, Two>>>;
}[LessEq<T, One> extends true ? 0 : 1];

type r0 = Fib<Zero>;
// type r10= 0
type r1 = Fib<One>;
// type r1 = 1

type r2 = Fib<Two>;
// type r2 = 1

type r3 = Fib<3>;
// type r3 = 2

type r4 = Fib<4>;
// type r4 = 3

type r5 = Fib<5>;
//type r5 = 5

type r6 = Fib<6>;
// type r6 = 8

四、总结


看了TypeScript实现斐波纳切数列这一套操作有没有让你有体验到重回“实现流水线CPU”的实验室时光?


IT在最近几十年的发展突飞猛进,越来越多的“程序员”加入到了互联网行业,在一些高级语言以及开发框架下,“程序员”的编码也只需要关注业务逻辑实现,很少有人会再去关注计算机底层是怎么实现加减乘除的,当然社会在进步,技术也在日新月异地迭代,偶尔驻足,回忆刚接触计算机编程,在命令行输出第一行“Hello World!”代码那时欣喜的自己,也许那是我们都回不去的青春...



链接:https://juejin.cn/post/6965320374451961886
收起阅读 »

2021不得不学的Typescript

ts作为一门新技术,这两年是越来越火,经过了一段时间的学习和理解之后,写了这篇文章,通过记录ts的核心知识点来带大家轻松掌握typescript,希望能够打动屏幕面前的你。Typescript基础语法TypeScript支持与JavaScript几乎相同的数据...
继续阅读 »

ts作为一门新技术,这两年是越来越火,经过了一段时间的学习和理解之后,写了这篇文章,通过记录ts的核心知识点来带大家轻松掌握typescript,希望能够打动屏幕面前的你。

Typescript基础语法

TypeScript支持与JavaScript几乎相同的数据类型,此外还提供了实用的枚举类型方便我们使用。接下来我们简单介绍一下这几种类型的用法.

基础类型

// 布尔类型
let isFlag:boolean = true

// 数值类型
let myNumber:number = 24

// 字符串类型
let str:string = 'ykk'

// 数组类型, 有两种表示方式,第一种可以在元素类型后面接上[],表示由此类型元素组成的一个数组
let arr:number[] = [1,2,3]
//当数组存在不同类型时
let arr1: (number | string)[] = [1, '2', 3]

// 数组类型, 使用数组泛型(有关泛型后面会详细分析)
let arr:Array<number> = [4,5,6]

// 元组类型, 允许表示一个已知元素数量和类型的数组,各元素的类型不必相同
let yuan: [string, number];
// 初始化yuan
yuan = ['yuan', 12]; // 正确
yuan = [12, 'yuan']; // 错误

// 枚举类型, 可以为一组数值赋予友好的名字
enum ActionType { online, offline, deleted }
let action:ActionType = ActionType.offline // 1

// any, 表示任意类型, 可以绕过类型检查器对这些值进行检查
let color:any = 1
color = 'red'

// void类型, 当一个函数没有返回值时,通常会设置其返回值类型是 void
function getName(): void {
console.log("This is my name");
}

// object类型, 表示非原始类型,也就是除number,string,boolean,symbol,null或undefined之外的类型
let obj:object;
obj = {num: 1}

// 对象类型也可以写成
const t = {
name:string,
age:number
} = {
name:'ykk',
age:18
}

// 函数类型也属于一种对象类型(该实例返回值必须是number类型)
const getTt:() => number = () => {
return 123;
}

//never类型(永远不会执行never之后的逻辑)
function errorEmitter(): never {
throw new Error();
console.log(123)
}

接口(Interface)

interface Person {
name: string;
age: number;
phone: number;
}

let man:Person = {
name: 'ykk',
age: 18,
phone: 13711111111
}

类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以。其次我们还可以定义可选属性只读属性。 可选属性表示了接口里的某些属性不是必需的,所以可以定义也可以不定义。·可读属性·使得接口中的某些属性只能读取而不能赋值,如下:

interface Person {
name: string;
age?: number;
readonly phone: number;
}

在实际场景中, 我们往往还会遇到不确定属性名和属性值类型的情况, 这个时候我们可以利用索引签名来设置额外的属性和类型, 如下:

interface Person {
name: string;
[propName: string]: any; //这里表示除了有name之外还可以有其他的任何属性,但是属性名必须是string类型,属性值任意类型都可以
}

当然接口也可以进行继承

//此时human这个interface就拥有Person的所有属性,并且在调用的时候也必须满足Proson的要求,否则就会报错
interface human extends Person {
weight:55
}

和js的class一致, typescript的类有公共(public)私有(private)受保护(protected)的访问类型。

  • public 在TypeScript里,成员都默认为 public,表示允许它在定义类的外部被调用

  • private 表示它能在定义的类内使用,不能在该类的外部使用

  • protected 和private类似, 但是protected允许在类内及继承的子类中使用

class Person {
public name:string = 'ykk';
private age:number = 18;
protected weight:number = 55;
constructor(){

}
}

class Man extends Person {
public say(){
return this.weight
}
}

let p = new Person()
let m = new Man()
console.log(p.name) //ykk
console.log(p.age) //报错age是private属性
console.log(m.say()) //55

由于在js中,getter 和 setter不能直接使用,我们需要通过一个Object.defineProperty来定义触发,那么在ts中就简单多了在类中直接能声明:

class Person {
private _food: string = 'apple'
get food() {
return this._food
}
set food(name: string) {
this._food = name
}
}
let p = new Person()
console.log(p.food) //apple
p.food="cookie" //这里重新设置food

typescript中static这个关键字是把这个方法或者属性直接挂在类上,而不是挂在new出来的实例。设计模式中经典的单例模式,用它最合适不过了!

class Demo {
private static instance: Demo;
peivate contructor(public name:string){}

static getInstance(){
if(!this.instance){
this.instance = new Demo('ykk')
}
return this.instance;
}
}

const demo1 = Demo.getInstance();
const demo2 = Demo.getInstance();
console.log(demo1.name)
console.log(demo2.name)

抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。 不同于接口,抽象类可以包含成员的实现细节。 abstract关键字是用于定义抽象类和在抽象类内部定义抽象方法, 这里需要注意的是不能创建一个抽象类的实例

abstract class MyAbstract {
constructor(public name: string) {}
say(): void {
console.log('say name: ' + this.name);
}
abstract sayBye(): void; // 必须在派生类中实现
}

class SubMyAbstract extends MyAbstract {
constructor() {
super('ykk'); // 在派生类的构造函数中必须调用 super()
}
sayBye(): void {
console.log('bye');
}
getOther(): void {
console.log('loading...');
}
}

let department: MyAbstract; // 允许创建一个对抽象类型的引用
department = new SubMyAbstract(); // 允许对一个抽象子类进行实例化和赋值
department.say();
department.sayBye();

department = new MyAbstract(); // 错误: 不能创建一个抽象类的实例
department.getOther(); // 错误: 方法在声明的抽象类中不存在

Typescript进阶语法

联合类型和类型保护

所谓联合类型是用于限制传入的值的类型只能是 | 分隔的每个类型,所以 number | string | boolean表示一个值可以是 number, string,或 boolean。例如:

interface person1 {
name: string;
age: number;
}

interface person2 {
hby: string;
age: number;
}
let man:person1 | person2

如果一个值是联合类型,那么我们只能访问它们中共有的部分(共有的属性与方法),由于只能访问共有,导致我们在想要访问某一个的时候ts会提示报错,这时我们就需要类型保护

let man:person1 | person2;
man = {
name: 'ykk',
age: 18,
hby: 'basketball'
}
//使用as直接断言,告诉ts在哪里去找
if((me as person1).name) {
console.log((me as person1).name);
}

if((me as person1).name) {
console.log((me as person2).hby);
}
//使用in
if(('name' in me)) {
console.log(me.name);
}

if('hby' in me) {
console.log(me.hby);
}
//使用typeof
function add(one:number|string,two:number|string){
if(typeof one=="string"||typeof two=="string"){
retrun `${one}${two}`
}
retrun one+two
}
//使用instanceof
class a{
num:1
}
function b(obj:object|a){
if(obj instanceof a){
retrun obj.num
}
}

泛型

什么是泛型呢,我的理解就是泛指的类型,那他在ts中应该怎么写呢?

//定义是用尖括号表示一个变量
function iSay<T>(arg: T): T {
return arg;
}
// 调用的时候去声明类型
let come = iSay<number>(123);

当然了泛型有多种使用方式,接下来咱们一一探索。

函数泛型
//传入一个数组
function say<T>(arr:T[]){
...
};
say<number>([11,22,33]);

//传入多个泛型
function say<T, F>(name:T, age:F){
...
};
say<string, number>('ykk', 18);
类中的泛型
class say<T>{
constructor(name:T){
...
}
}
var t = new say<string>("1")
泛型的继承
class say<T extends number|string>{
constructor(one:T){
...
}
}
var t = new say<string>("1") //这时候表示泛型只能是number类型或者string类型其中的一种,否则会报错
泛型中使用keyof

泛型中使用keyof顾名思义就是遍历一个interface,并且每次的泛型类型就是当前interface的key.interface persons {

    name:string,
age:number,
get:boolean
}
var p = {
name:'ykk',
age:18,
get:false
}
function add<T extends keyof persons>(key:T):persons[T]{
return p[key]
}
//如此一来我们便能知道返回值的准确类型了
var p1 = add('name')
console.log(p1) //ykk

命名空间

ts在我们使用的时候如果用面向对象的方式声明多个类生成实例的时候,你会发现在全局就会多出几个实例,这样就会导致全局污染,如此一来,我们便需要namespace这个关键字,来防止全局污染。

namespace Main{
class circle {
...
}

class rect {
...
}

//如果想要导出给外部使用,需要导出
export class san{
...
}
}
//这样在全局只会有一个Main供我们使用了

声明全局变量

对于使用未经声明的全局函数或者全局变量, typescript往往会报错。最常见的例子就是我们在引入js文件的时候,往往会报下面的错误:


这个时候有两种解决方式:

  • 按照提示执行相对于的npm install @types/xxx的命令。
  • 可以在对应位置添加xxx.d.ts文件, 并在里面声明我们所需要的变量, ts会自动检索到该文件并进行解析,如下:
//定义一个全局类型, 并编写相应的逻辑让ts识别相应的js语法
declare var superagent;
...



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

收起阅读 »

【性能优化】关键性能指标及测量标准

前言随着时代的发展,业内曾经提出过很多性能优化相关的标准和规范,从最初2007年雅虎提出34条军规,到2020年google开始推广Web Vitals,优化技术已经有将近20多年的发展历史,如下图所示:发展过程中产生了许多的性能指标、测量工具、优化手段等等,...
继续阅读 »

前言

随着时代的发展,业内曾经提出过很多性能优化相关的标准和规范,从最初2007年雅虎提出34条军规,到2020年google开始推广Web Vitals,优化技术已经有将近20多年的发展历史,如下图所示:


发展过程中产生了许多的性能指标、测量工具、优化手段等等,本文主要讲述关键性能指标及测量标准。


性能指标,顾名思义,就是性能优化过程中参考的一些标准。


进行性能优化,指标就是我们的一个抓手,首先你就要确定它的指标,然后才能根据指标去采取措施,否则就会像无头苍蝇一样乱撞,没有执行目标。

什么样的指标值得我们关注?

Web Vitals

Google在2020年推出了一个名为Web Vitals的新概念,着重评估一组页面访问体验钟的一部分关键指标,目的在于简化网络性能。每个指标代表页面体验的一个关键方面:加载、交互和视觉稳定性



Web Vitals在感知的性能,交互性和令人愉悦的方面,可作为用户体验的简化基准。在考虑页面性能时,应该首先关注这一套较小的指标。


此外,Web Vitals代表了访问者在访问您的页面时首先在其视口中看到的内容,即首屏内容。他们首先看到的内容最终会影响他们对页面性能的看法。


首先,专注于这三个指标,可以使您获得可感知的和实际的性能可观的收益,然后再深入研究其他优化。


加载


所谓加载,就是进入页面时,页面内容的载入过程。比如,当你打开一些网站时,你会发现,有的网站首页上的文字、图片出现很缓慢,而有的则很快,这个内容出现的过程就是加载。加载缓慢严重消耗用户的耐心,会让用户离开页面。


这里我们拿淘宝首页的network信息观察一些指标:


如图所示,我们可以看到下方显示网页加载一共有201次请求(requests)、3.3MB资源量(resources),DOM完成的加载时间92ms(DOMContentLoaded)、总资源加载时间479ms(Load),这对一个电商网站来说已经很好了。


瀑布图


再来看瀑布图,nextwork中加载资源的列表右侧的Waterfall一栏显示的就是瀑布图,它可以非常直观的将网站的资源加载用自上而下的方式表达出来。我们可以从横向和纵向两个方向来解读它。


横向来看是具体资源的加载,如下图所示:


当鼠标悬浮在具体的色块上,我们可以看到具体的指标详情。如下图所示,资源下载之前经历了0.46ms的排队(Queueing),DNS查找24.79ms(DNS Lookup)、建立TCP连接(Initial connection)、还有https的网站SSL证书验证的时间37.04ms(SSL),最后发送请求再到资源返回也要经历110.71ms(TTFB)。

下面我们再来纵向看瀑布图,主要看2点:



  1. 资源之间的联系:如果下载发生了阻塞,很多资源的下载就会是串行处理的。如果是并行的,就可以加快下载过程。

  2. 关键时间节点:我们可以看到图中有红蓝两个颜色的两根线,蓝色的是DOM完成的加载时间,红色是页面中所有声明的资源加载完成的时间。

关键指标


那么这么多指标,到底哪些是最值得我们关注的呢?下面我来总结一下:




  1. 白屏时间(FP,First Paint):也叫首次绘制时间,对于应用页面,首次出现视觉上不同于跳转之前内容的时间点,或者说是页面发生第一次绘制的时间点,它的标准时间是 300ms。如果白屏时间过长,用户会认为我们的页面不可用,或者可用性差。如果超过一定时间(如 1s),用户注意力就会转移到其他页面。




  2. 首屏时间(FCP,First Contentful Paint):也叫首次有内容绘制时间,对于所有的网页应用,这是一个非常重要的指标。它是指从浏览器输入地址并回车后,到首屏内容渲染完毕的时间。这期间不需要滚动鼠标或者下拉页面,否则无效。也就是说,它是浏览器完成渲染DOM中第一部分内容(可能是文本、图像或其它任何元素)的时间点,此时用户应该在视觉上有直观的感受。




  3. 首次有意义绘制(FMP,First Meaningful Paint):指页面关键元素的渲染时间。这个概念并没有标准化定义,因为关键元素可以由开发者自行定义究竟什么是“有意义”的内容,只有开发者或者产品经理自己了解。




  4. 速度指数(Speed Index):指的是网页以多快的速度展示内容,标准时间是4s




  5. 总下载时间(Load):页面所有资源加载完成所需要的时间。一般可以统计 window.onload,得到同步加载的资源全部加载完的耗时。如果页面中存在较多异步渲染,那么可以将异步渲染全部完成的时间作为总下载时间。




  6. TTFB(Time To First Byte):是指网络请求被发起到从服务器接收到地一个字节的这段时间。其中包含了TCP连接时间、发送HTTP请求时间和获得相应消息第一个字节的时间。


    TTFB这个参数比较重要,因为它可以给用户最直观的感受,如果TTFB很慢,说明资源一直没有返回,增加白屏时间,如果TTFB很快,资源回来之后就可以进行快速的解析和渲染。


    那么影响TTFB的因素有哪些?



    1. 后台处理能力,服务器响应到底有多快

    2. 资源请求的网络状况,网络是否有延迟

首屏时间 vs 白屏时间


首屏时间 = 白屏时间 + 渲染时间。在加载性能指标方面,相比于白屏时间,首屏时间更重要。为什么?


从重要性角度看,打开页面后,第一眼看到的内容一般都非常关键,比如电商的头图、商品价格、购买按钮等。这些内容即便在最恶劣的网络环境下,我们也要确保用户能看得到。


从体验完整性角度看,进入页面后先是白屏,随着第一个字符加载,到首屏内容显示结束,我们才会认为加载完毕,用户可以使用了。白屏加载完成后,仅仅意味着页面内容开始加载,但我们还是没法完成诸如下单购买等实际操作,首屏时间结束后则可以。


DOMContentLoaded和Load事件的区别


其实从这两个事件的命名就能体会到,DOMContentLoaded 指的是文档中 DOM 加载内容加载完毕的时间,也就是说 HTML 结构已经是完整的了。但是我们知道,很多页面都包含图片、特殊字体、视频、音频等其他资源,由于这些资源由网络请求获取,需要额外的网络请求,因此DOM内容如加载完毕时,这些资源还没有请求或渲染完成。当页面上所有资源加载完成后,Load 事件才会被触发。


因此,在时间线上,Load 事件往往会落后于 DOMContentLoaded 事件

交互/响应


所谓交互,就是用户点击网站或 App 的某个功能,页面给出的回应,也就是浏览器的响应时间。比如我们点击了一个“点赞”按钮,立刻给出了点赞数加一的展示,这就是交互体验好,反之如果很长时间都没回应,这就是交互体验不好。


关于交互指标,有的公司用 FID 指标 (First Input Delay,首次输入延迟), 指标必须尽量小于 100ms,如果过长会给人页面卡顿的感觉。还有的公司使用 PSI(Perceptual Speed Index,视觉变化率),衡量标准是小于20%。


一般来说,主要包括以下几个指标:




  1. 交互动作的反馈时间:也叫用户可交互时间,就是用户可以与应用进行加护的时间,一般来讲,我们认为是 DOMReady 的时间,因为我们通常会在这时绑定事件操作。如果页面中设计交互的脚本没有下载完成,那么当然没有达到所谓的用户可交互时间。那么如何定义 DOMReady 时间呢?这里我推荐大家看司徒正美的文章《何谓DOMReady》。




  2. 刷新率(FPS,Frame Per Second):也叫帧率,标准的刷新率指标是60帧/s,它可以决定画面是否足够流畅。




  3. 异步请求的完成时间:所有的异步请求能在1s中内请求回来。




关于帧率,我们可以用chorme Devtools来查看,打开控制台,点击快捷键command/ctrl+shift+P,弹出下面的弹窗,输入frame,点击FPS一栏,就会在页面左上角看到图2所示的监控台,显示网页交互过程中每一帧的绘制频率。



不同帧率的体验



  • 帧率能够达到 50 ~ 60 FPS 的动画将会相当流畅,让人倍感舒适;

  • 帧率在 30 ~ 50 FPS 之间的动画,因各人敏感程度不同,舒适度因人而异;

  • 帧率在 30 FPS 以下的动画,让人感觉到明显的卡顿和不适感;

  • 帧率波动很大的动画,亦会使人感觉到卡顿


现在网上很多关于浏览器reflow的文章都说给要少用offsetTop, offsetLeft 等获取布局信息的。因为这些属性需要触发一次浏览器的的Layout。也就是说在一帧内(16ms)会多了一次layout。如果Layout的次数太多,就会导致掉帧。


视觉稳定性


视觉稳定性指标CLS(Cumulative Layout Shift),也就是布局偏移量,它是指页面从一帧切换到另外一帧时,视线中不稳定元素的偏移情况。


比如,你想要购买的商品正在参加抢购活动,而且时间快要到了。在你正要点击页面链接购买的时候,原来的位置插入了一条 9.9 元包邮的商品广告。结果会怎样?你点成了那个广告商品。如果等你再返回购买的时候,你心仪商品的抢购活动结束了,你是不是很气?所以,CLS也非常重要。


一个好的CLS分数是75%以上的用户小于0.1,如图所示:




布局偏移的具体内容


布局偏移是由 Layout Instability API 定义的。这个API会在任意时间上报 layout-shift 的条目,当一个可见元素在两帧之间,改变了它的起始位置(默认的 writing mode 下指的是top和left属性)。这些元素被当成不稳定元素。


需要注意的是,布局偏移只发生在已经存在的元素改变起始位置的时候。如果一个新的元素被添加到dom上,或者已存在的元素改变它的尺寸,除非改变了其他元素的起始位置,否则都不算布局偏移。


布局偏移主要包含以下几项:




  1. 布局偏移分数:布局偏移的分数是两个度量的乘积:影响分数(impact fraction)和距离分数(distance fraction)。如果是一个很大的元素移动了较小的距离,实际影响并不大,所以分数需要依赖两个度量。




  2. 影响分数:影响分数测试的是两帧之间,不稳定元素在视图上的影响范围。




  3. 距离分数:距离分数测试的是两帧之间,不稳定元素在视图上移动的距离(水平和纵向取最大值)。如果有多个不稳定元素,也是取其中最大的一个。




  4. 动画和过渡:动画和过渡,如果做得好,对用户而言是一个不错的更新内容的方式,这样不会给用户“惊喜”。突然出现的或者预料之外的内容,会给用户一个很差的体验。但如果是一个动画或者过渡,用户可以很清楚的知道发生了什么,在状态变化的时候可以很好的引导用户。


    CSS中的 transform 属性可以让你在使用动画的时候不会产生布局偏移。



    • transform:scale() 来替换 widthheight 属性

    • transform:translate() 来替换 top, left, bottom, right 属性




CLS是平时开发很少关注的点,页面视觉稳定性对很多web开发而言,可能没有加载性能那么关注度高,但对用户而言,这确实是很困扰的一点。平时开发中,尽可能的提醒自己,不管是产品交互图出来之后,或者是UI的视觉稿出来之后,如果出现了布局偏移的情况,都可以提出这方面的意见。开发过程中也尽可能的遵循上面提到的一些优化点,给用户一个稳定的视觉体验。


RAIL测量模型


RAIL模型是2015年google提出的一个可以量化的测量标准,通过RAIL模型可以指导性能优化的目标,让良好的用户体验成为可能。




  1. Response 响应:是指用户操作网页的时候浏览器给到用户的反馈时间,其中处理事件应在50ms以内完成。


    为什么是50ms?谷歌向向用户发起调研,将用户的反馈分成了几个组,经过研究得出用户能接受的反馈时间是100ms。


    那么为什么我们要设置在50ms以内,因为100ms是用户输入到反馈的时间,但是浏览器处理反馈也需要时间,所以留给开发者优化处理事件的时间在50ms以内。如下图所示:


  • Animation - 页面中动画特效的流畅度,达到每10ms产生一帧。


    根据研究得出,动画要达到60sps,即每秒60帧给人眼的感觉是流畅的,每一帧大概在16ms,去除浏览器绘制动画的6ms,开发者要保证每10ms产生一帧。


    在这16ms内浏览器要完成的工作有:



    • 脚本执行(JavaScript):脚本造成了需要重绘的改动,比如增删 DOM、请求动画等

    • 样式计算(CSS Object Model):级联地生成每个节点的生效样式。

    • 布局(Layout):计算布局,执行渲染算法

    • 重绘(Paint):各层分别进行绘制(比如 3D 动画)

    • 合成(Composite):将位图发送给合成线程。




  • Idle空闲 - 浏览器有足够的空闲时间,与响应想呼应。尽可能最大化空闲时间,不能让事件处理时间太长,超过50ms。


    例如延迟加载可以用空闲时间去加载。但是如果需要前端做业务计算,就是不合理的。




  • Load - 网络加载时间,在5s内完成内容加载并可以交互。首先加载-解析-渲染的时间在5s,其次网络环境差的情况下,加载也会受到影响。

  • 总结

    至此,性能优化的指标我就介绍完了,现将关键指标总结如下:



    1. 性能优化的三个方向:加载、交互、视觉稳定性

    2. 加载的关键指标有:TTFB(请求等待时间)、FP(白屏时间)、FCP(首屏时间)、Speed Index(4s)

    3. 交互的关键指标:用户可交互时间、帧率(FPS)、异步请求完成时间

    4. 交互稳定性(CLS):布局偏移量中,布局偏移分数 = 影响分数 x 距离分数

    5. RAIL测量模型关注点:响应时间50ms、动画10ms/帧、浏览器空闲时间<50ms、网络加载时间5s

    链接:https://juejin.cn/post/6956583036133572639

    收起阅读 »

    前端的你还不会优化你的图片资源?来看这一篇就够了!

    优质的图片可以有效吸引用户,给用户良好的体验,所以随着互联网的发展,越来越多的产品开始使用图片来提升产品体验。相较于页面其他元素,图片的体积不容忽视。下图是截止 2019 年 6 月 HTTP Archive[1] 上统计的网站上各类资源加...
    继续阅读 »

    优质的图片可以有效吸引用户,给用户良好的体验,所以随着互联网的发展,越来越多的产品开始使用图片来提升产品体验。相较于页面其他元素,图片的体积不容忽视。下图是截止 2019 年 6 月 HTTP Archive[1] 上统计的网站上各类资源加载的体积:


    可以看到,图片占据了半壁江山。同样,在一篇 2018 年的文章中,也提到了图片在网站中体量的平均占比已经超过了 50%[2]。然而,随着平均加载图片总字节数的增加,图片的请求数却再减少,这也说明网站使用的图片质量和大小正在不断提高。


    所以,如果单纯从加载的字节数这个维度来看性能优化,那么很多时候,优化图片带来的流量收益要远高于优化 JavaScript 脚本和 CSS 样式文件。下面我们就来看看,如何优化图片资源。


    1. 优化请求数

    1.1. 雪碧图


    图片可以合并么?当然。最为常用的图片合并场景就是雪碧图(Sprite)[3]


    在网站上通常会有很多小的图标,不经优化的话,最直接的方式就是将这些小图标保存为一个个独立的图片文件,然后通过 CSS 将对应元素的背景图片设置为对应的图标图片。这么做的一个重要问题在于,页面加载时可能会同时请求非常多的小图标图片,这就会受到浏览器并发 HTTP 请求数的限制。我见过一个没有使用雪碧图的页面,首页加载时需要发送 20+ 请求来加载图标。将图标合并为一张大图可以实现「20+ → 1」的巨大缩减。


    雪碧图的核心原理在于设置不同的背景偏移量,大致包含两点:



    • 不同的图标元素都会将 background-url 设置为合并后的雪碧图的 uri;

    • 不同的图标通过设置对应的 background-position 来展示大图中对应的图标部分。


    你可以用 Photoshop 这类工具自己制作雪碧图。当然比较推荐的还是将雪碧图的生成集成到前端自动化构建工具中,例如在 webpack 中使用 webpack-spritesmith,或者在 gulp 中使用 gulp.spritesmith。它们两者都是基于于 spritesmith 这个库,你也可以自己将这个库集成到你喜欢的构建工具中。

    1.2. 懒加载


    我们知道,一般来说我们访问一个页面,浏览器加载的整个页面其实是要比可视区域大很多的,也是什么我们会提出“首屏”的概念。这就导致其实很多图片是不在首屏中的,如果我们都加载的话,相当于是加载了用户不一定会看到图片。而图片体积一般都不小,这显然是一种流量的浪费。这种场景在一些带图片的长列表或者配图的博客中经常会遇到。


    解决的核心思路就是图片懒加载 —— 尽量只加载用户正在浏览或者即将会浏览到的图片。实现上来说最简单的就是通过监听页面滚动,判断图片是否进入视野,从而真正去加载图片:

    function loadIfNeeded($img) {
    const bounding = $img..getBoundingClientRect();
    if (
    getComputedStyle($img).display !== 'none'
    && bounding.top <= window.innerHeight
    && bounding.bottom >= 0
    ) {
    $img.src = $img.dataset.src;
    $img.classList.remove('lazy');
    }
    }

    // 这里使用了 throttle,你可以实现自己的 throttle,也可以使用 lodash
    const lazy = throttle(function () {
    const $imgList = document.querySelectorAll('.lazy');
    if ($imgList.length === 0) {
    document.removeEventListener('scroll', lazy);
    window.removeEventListener('resize', lazy);
    window.removeEventListener('orientationchange', lazy);
    return;
    }
    $imgList.forEach(loadIfNeeded);
    }, 200);

    document.addEventListener('scroll', lazy);
    window.addEventListener('resize', lazy);
    window.addEventListener('orientationchange', lazy);

    对于页面上的元素只需要将原本的 src 值设置到 data-src 中即可,而 src 可以设置为一个统一的占位图。注意,由于页面滚动、缩放和横竖方向(移动端)都可能会改变可视区域,因此添加了三个监听。


    当然,这是最传统的方法,现代浏览器还提供了一个更先进的 Intersection Observer API[4] 来做这个事,它可以通过更高效的方式来监听元素是否进入视口。考虑兼容性问题,在生产环境中建议使用对应的 polyfill


    如果想使用懒加载,还可以借助一些已有的工具库,例如 aFarkas/lazysizesverlok/lazyloadtuupola/lazyload 等。


    在使用懒加载时也有一些注意点:



    • 首屏可以不需要懒加载,对首屏图片也使用懒加载会延迟图片的展示。

    • 设置合理的占位图,避免图片加载后的页面“抖动”。

    • 虽然目前基本所有用户都不会禁用 JavaScript,但还是建议做一些 JavaScript 不可用时的 backup。


    对于占位图这块可以再补充一点。为了更好的用户体验,我们可以使用一个基于原图生成的体积小、清晰度低的图片作为占位图。这样一来不会增加太大的体积,二来会有很好的用户体验。LQIP (Low Quality Image Placeholders)[5] 就是这种技术。目前也已经有了 LQIPSQIP(SVG-based LQIP) 的自动化工具可以直接使用。


    如果你想了解更多关于图片懒加载的内容,这里有一篇更详尽的图片懒加载指南[6]

    1.3. CSS 中的图片懒加载

    除了对于 <img> 元素的图片进行来加载,在 CSS 中使用的图片一样可以懒加载,最常见的场景就是 background-url 

    .login {
    background-url: url(/static/img/login.png);
    }

    对于上面这个样式规则,如果不应用到具体的元素,浏览器不会去下载该图片。所以你可以通过切换 className 的方式,放心得进行 CSS 中图片的懒加载。


    1.4. 内联 base64


    还有一种方式是将图片转为 base64 字符串,并将其内联到页面中返回,即将原 url 的值替换为 base64。这样,当浏览器解析到这个的图片 url 时,就不会去请求并下载图片,直接解析 base64 字符串即可。


    但是这种方式的一个缺点在于相同的图片,相比使用二进制,变成 base64 后体积会增大 33%。而全部内联进页面后,也意味着原本可能并行加载的图片信息,都会被放在页面请求中(像当于是串行了)。同时这种方式也不利于复用独立的文件缓存。所以,使用 base64 需要权衡,常用于首屏加载 CRP 或者骨架图上的一些小图标。


    2. 减小图片大小

    2.1. 使用合适的图片格式


    使用合适的图片格式不仅能帮助你减少不必要的请求流量,同时还可能提供更好的图片体验。


    图片格式是一个比较大的话题,选择合适的格式[7]有利于性能优化。这里我们简单总结一些。


    1) 使用 WebP:


    考虑在网站上使用 WebP 格式[8]。在有损与无损压缩上,它的表现都会优于传统(JPEG/PNG)格式。WebP 无损压缩比 PNG 的体积小 26%,webP 的有损压缩比同质量的 JPEG 格式体积小 25-34%。同时 WebP 也支持透明度。下面提供了一种兼容性较好的写法。

    <picture>
    <source type="image/webp" srcset="/static/img/perf.webp">
    <source type="image/jpeg" srcset="/static/img/perf.jpg">
    <img src="/static/img/perf.jpg">
    </picture>

    2) 使用 SVG 应对矢量图场景:


    在一些需要缩放与高保真的情况,或者用作图标的场景下,使用 SVG 这种矢量图非常不错。有时使用 SVG 格式会比相同的 PNG 或 JPEG 更小。


    3) 使用 video 替代 GIF:


    兼容性允许的情况下考虑,可以在想要动图效果时使用视频,通过静音(muted)的 video 来代替 GIF。相同的效果下,GIF 比视频(MPEG-4)大 5~20 倍Smashing Magazine 上有篇文章[9]详细介绍使用方式。


    4) 渐进式 JPEG:


    基线 JPEG (baseline JPEG) 会从上往下逐步呈现,类似下面这种:


    而另一种渐进式 JPEG (progressive JPEG)[10] 则会从模糊到逐渐清晰,使人的感受上会更加平滑。


    不过渐进式 JPEG 的解码速度会慢于基线 JPEG,所以还是需要综合考虑 CPU、网络等情况,在实际的用户体验之上做权衡。


    2.2. 图片质量的权衡


    图片的压缩一般可以分为有损压缩(lossy compression)和无损压缩(lossless compression)。顾名思义,有损压缩下,会损失一定的图片质量,无损压缩则能够在保证图片质量的前提下压缩数据大小。不过,无损压缩一般可以带来更可观的体积缩减。在使用有损压缩时,一般我们可以指定一个 0-100 的压缩质量。在大多数情况下,相较于 100 质量系数的压缩,80~85 的质量系数可以带来 30~40% 的大小缩减,同时对图片效果影响较小,即人眼不易分辨出质量效果的差异。



    处理图片压缩可以使用 imagemin 这样的工具,也可以进一步将它集成至 webpackGulpGrunt 这样的自动化工具中。


    2.3. 使用合适的大小和分辨率


    由于移动端的发展,屏幕尺寸更加多样化了。同一套设计在不同尺寸、像素比的屏幕上可能需要不同像素大小的图片来保证良好的展示效果;此外,响应式设计也会对不同屏幕上最佳的图片尺寸有不同的要求。


    以往我们可能会在 1280px 宽度的屏幕上和 640px 宽度的屏幕上都使用一张 400px 的图,但很可能在 640px 上我们只需要 200px 大小的图片。另一方面,对于如今盛行的“2 倍屏”、“3 倍屏”也需要使用不同像素大小的资源。


    好在 HTML5 在 <img> 元素上为我们提供了 srcsetsizes 属性,可以让浏览器根据屏幕信息选择需要展示的图片。

    <img srcset="small.jpg 480w, large.jpg 1080w" sizes="50w" src="large.jpg" >

    2.4. 删除冗余的图片信息


    你也许不知道,很多图片含有一些非“视觉化”的元信息(metadata),带上它们可会导致体积增大与安全风险[12]。元信息包括图片的 DPI、相机品牌、拍摄时的 GPS 等,可能导致 JPEG 图片大小增加 15%。同时,其中的一些隐私信息也可能会带来安全风险。


    所以如果不需要的情况下,可以使用像 imageOptim 这样的工具来移除隐私与非关键的元信息。


    2.5 SVG 压缩


    在 2.1. 中提到,合适的场景下可以使用 SVG。针对 SVG 我们也可以进行一些压缩。压缩包括了两个方面:


    首先,与图片不同,图片是二进制形式的文件,而 SVG 作为一种 XML 文本,同样是适合使用 gzip 压缩的。


    其次,SVG 本身的信息、数据是可以压缩的,例如用相比用 <path> 画一个椭圆,直接使用 <ellipse> 可以节省文本长度。关于信息的“压缩”还有更多可以优化的点[13]SVGGO 是一个可以集成到我们构建流中的 NodeJS 工具,它能帮助我们进行 SVG 的优化。当然你也可以使用它提供的 Web 服务


    3. 缓存

    与其他静态资源类似,我们仍然可以使用各类缓存策略来加速资源的加载。




    图片作为现代 Web 应用的重要部分,在资源占用上同样也不可忽视。可以发现,在上面提及的各类优化措施中,同时附带了相应的工具或类库。平时我们主要的精力会放在 CSS 与 JavaScript 的优化上,因此在图片优化上可能概念较为薄弱,自动化程度较低。如果你希望更好得去贯彻图片的相关优化,非常建议将自动化工具引入到构建流程中。


    除了上述的一些工具,这里再介绍两个非常好用的图片处理的自动化工具:SharpJimp


    好了,我们的图片优化之旅就暂时到这了,下面就是字体资源了。


    链接:https://juejin.cn/post/6962800616259190792



    收起阅读 »

    仅使用CSS就可以提高页面渲染速度的4个技巧

    用户喜欢快速的网络应用,他们希望页面加载速度快,功能流畅。如果在滚动时有破损的动画或滞后,用户很有可能会离开你的网站。作为一名开发者,你可以做很多事情来改善用户体验。本文将重点介绍4个可以用来提高页面渲染速度的CSS技巧。1. Content-visibili...
    继续阅读 »

    用户喜欢快速的网络应用,他们希望页面加载速度快,功能流畅。如果在滚动时有破损的动画或滞后,用户很有可能会离开你的网站。作为一名开发者,你可以做很多事情来改善用户体验。本文将重点介绍4个可以用来提高页面渲染速度的CSS技巧。

    1. Content-visibility


    一般来说,大多数Web应用都有复杂的UI元素,它的扩展范围超出了用户在浏览器视图中看到的内容。在这种情况下,我们可以使用内容可见性( content-visibility )来跳过屏幕外内容的渲染。如果你有大量的离屏内容,这将大大减少页面渲染时间。


    这个功能是最新增加的功能之一,也是对提高渲染性能影响最大的功能之一。虽然 content-visibility 接受几个值,但我们可以在元素上使用 content-visibility: auto; 来获得直接的性能提升。


    让我们考虑一下下面的页面,其中包含许多不同信息的卡片。虽然大约有12张卡适合屏幕,但列表中大约有375张卡。正如你所看到的,浏览器用了1037ms来渲染这个页面。


    下一步,您可以向所有卡添加 content-visibility 。

    在这个例子中,在页面中加入 content-visibility 后,渲染时间下降到150ms,这是6倍以上的性能提升。


    正如你所看到的,内容可见性是相当强大的,对提高页面渲染时间非常有用。根据我们目前所讨论的东西,你一定是把它当成了页面渲染的银弹。

    然而,有几个领域的内容可视性不佳。我想强调两点,供大家参考。



    • 此功能仍处于试验阶段。 截至目前,Firefox(PC和Android版本)、IE(我认为他们没有计划在IE中添加这个功能)和,Safari(Mac和iOS)不支持内容可见性。

    • 与滚动条行为有关的问题。 由于元素的初始渲染高度为0px,每当你向下滚动时,这些元素就会进入屏幕。实际内容会被渲染,元素的高度也会相应更新。这将使滚动条的行为以一种非预期的方式进行。

    为了解决滚动条的问题,你可以使用另一个叫做 contain-intrinsic-size 的 CSS 属性。它指定了一个元素的自然大小,因此,元素将以给定的高度而不是0px呈现。

    .element{
    content-visibility: auto;
    contain-intrinsic-size: 200px;
    }

    然而,在实验时,我注意到,即使使用 conta-intrinsic-size,如果我们有大量的元素, content-visibility 设置为 auto ,你仍然会有较小的滚动条问题。


    因此,我的建议是规划你的布局,将其分解成几个部分,然后在这些部分上使用内容可见性,以获得更好的滚动条行为。

    2. Will-change 属性

    浏览器上的动画并不是一件新鲜事。通常情况下,这些动画是和其他元素一起定期渲染的。不过,现在浏览器可以使用GPU来优化其中的一些动画操作。



    通过will-change CSS属性,我们可以表明元素将修改特定的属性,让浏览器事先进行必要的优化。



    下面发生的事情是,浏览器将为该元素创建一个单独的层。之后,它将该元素的渲染与其他优化一起委托给GPU。这将使动画更加流畅,因为GPU加速接管了动画的渲染。

    // In stylesheet
    .animating-element {
    will-change: opacity;
    }

    // In HTML
    <div class="animating-elememt">
    Animating Child elements
    </div>

    当在浏览器中渲染上述片段时,它将识别 will-change 属性并优化未来与不透明度相关的变化。



    根据Maximillian Laumeister所做的性能基准,可以看到他通过这个单行的改变获得了超过120FPS的渲染速度,而最初的渲染速度大概在50FPS。


    什么时候不是用will-change


    虽然 will-change 的目的是为了提高性能,但如果你滥用它,它也会降低Web应用的性能。




    • **使用 will-change 表示该元素在未来会发生变化。**因此,如果你试图将 will-change 和动画同时使用,它将不会给你带来优化。因此,建议在父元素上使用 will-change ,在子元素上使用动画。

    .my-class{
    will-change: opacity;
    }
    .child-class{
    transition: opacity 1s ease-in-out;
    }
    • 不要使用非动画元素。 当你在一个元素上使用 will-change 时,浏览器会尝试通过将元素移动到一个新的图层并将转换工作交给GPU来优化它。如果您没有任何要转换的内容,则会导致资源浪费。




    最后需要注意的是,建议在完成所有动画后,将元素的 will-change 删除。

    3.减少渲染阻止时间

    今天,许多Web应用必须满足多种形式的需求,包括PC、平板电脑和手机等。为了完成这种响应式的特性,我们必须根据媒体尺寸编写新的样式。当涉及页面渲染时,它无法启动渲染阶段,直到 CSS对象模型(CSSOM)已准备就绪。根据你的Web应用,你可能会有一个大的样式表来满足所有设备的形式因素。
    <link rel="stylesheet" href="styles.css">

    将其分解为多个样式表后:

    <!-- style.css contains only the minimal styles needed for the page rendering -->
    <link rel="stylesheet" href="styles.css" media="all" />
    <!-- Following stylesheets have only the styles necessary for the form factor -->
    <link rel="stylesheet" href="sm.css" media="(min-width: 20em)" />
    <link rel="stylesheet" href="md.css" media="(min-width: 64em)" />
    <link rel="stylesheet" href="lg.css" media="(min-width: 90em)" />
    <link rel="stylesheet" href="ex.css" media="(min-width: 120em)" />
    <link rel="stylesheet" href="print.css" media="print" />

    如您所见,根据样式因素分解样式表可以减少渲染阻止时间。

    4.避免@import包含多个样式表

    通过 @import,我们可以在另一个样式表中包含一个样式表。当我们在处理一个大型项目时,使用 @import 可以使代码更加简洁。



    关于 @import 的关键事实是,它是一个阻塞调用,因为它必须通过网络请求来获取文件,解析文件,并将其包含在样式表中。如果我们在样式表中嵌套了 @import,就会妨碍渲染性能。

    # style.css
    @import url("windows.css");
    # windows.css
    @import url("componenets.css");


    与使用 @import 相比,我们可以通过多个 link 来实现同样的功能,但性能要好得多,因为它允许我们并行加载样式表。

    总结

    除了我们在本文中讨论的4个方面,我们还有一些其他的方法可以使用CSS来提高网页的性能。CSS最近的一个特性: content-visibility,在未来的几年里看起来是如此的有前途,因为它给页面渲染带来了数倍的性能提升。



    最重要的是,我们不需要写一条JavaScript语句就能获得所有的性能。



    我相信你可以结合以上的一些功能,为终端用户构建性能更好的Web应用。希望这篇文章对你有用,如果你知道什么CSS技巧可以提高Web应用的性能,请在下面的评论中提及。谢谢大家。



    链接:https://juejin.cn/post/6911203296078692359

    收起阅读 »

    一种简单实用的 JS 动态加载方案

    背景 在做 Web 应用的时候,你或许遇到过这样的场景:为了实现一个使用率很低的功能,却引入了超大的第三方库,导致项目打包后的 JS bundle 体积急剧膨胀。 我们有一些具体的案例,例如: 产品要求在项目中增加一个导出数据为 Excel 文件的功能,这个功...
    继续阅读 »

    背景


    在做 Web 应用的时候,你或许遇到过这样的场景:为了实现一个使用率很低的功能,却引入了超大的第三方库,导致项目打包后的 JS bundle 体积急剧膨胀。


    我们有一些具体的案例,例如:


    产品要求在项目中增加一个导出数据为 Excel 文件的功能,这个功能其实只有管理员才能看到,而且最多一周才会使用一次,绝对属于低频操作。


    团队里的小伙伴为了实现这个功能,引入了 XLSX 这个库,JS bundle 体积因而增加了一倍,所有用户的体验都受到影响了。


    XLSX 用来做 Excel 相关的操作是不错的选择,但因为新增低频操作影响全部用户却不值得。


    除了导出 Excel 这种功能外,类似的场景还有使用 html2canvas 生成并下载海报,使用 fabric 动态生成图片等。


    针对这种情况,你觉得该如何优化呢?

    自动分包和动态加载


    机智如你很快就想到使用 JS 动态加载,如果熟悉 React,还知道可以使用 react-loadable 来解决。


    原理就是利用 React Code-Splitting,配合 Webpack 自动分包,动态加载。


    这种方案可以,React 也推荐这么做,但是对于引用独立的第三方库这样的场景,还有更简单的方案。

    更简单的方案


    这些第三方库往往都提供了 umd 格式的 min.js,我们动态加载这些 min.js 就可以了。比如 XLSX,引入其 min.js 文件之后,就可以通过 window.XLSX 来实现 Excel 相关的操作。


    此方案的优点有:



    • 与框架无关,不需要和 React 等框架或 Webpack 等工具绑定

    • 精细控制,React Code-Splitting 之类的方案只能到模块级别,想要在点击按钮后才动态加载较难实现

    具体实现

    我们重点需要实现一个 JS 动态加载器 AsyncLoader,代码如下:

    function initLoader() {
    // key 是对应 JS 执行后在 window 中添加的变量
    const jsUrls = {
    html2canvas: 'https://cdn.jsdelivr.net/npm/html2canvas@1.0.0-rc.7/dist/html2canvas.min.js',
    XLSX: 'https://cdn.jsdelivr.net/npm/xlsx@0.16.9/dist/xlsx.min.js',
    flvjs: 'https://cdn.jsdelivr.net/npm/flv.js@1.5.0/dist/flv.min.js',
    domtoimage: 'https://cdn.jsdelivr.net/npm/dom-to-image@2.6.0/src/dom-to-image.min.js',
    fabric: 'https://cdn.jsdelivr.net/npm/fabric@4.3.1/dist/fabric.min.js',
    };

    const loadScript = (src) => {
    return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.type = 'text/javascript';
    script.onload = resolve;
    script.onerror = reject;
    script.crossOrigin = 'anonymous';
    script.src = src;
    if (document.head.append) {
    document.head.append(script);
    } else {
    document.getElementsByTagName('head')[0].appendChild(script);
    }
    });
    };

    const loadByKey = (key) => {
    // 判断对应变量在 window 是否存在,如果存在说明已加载,直接返回,这样可以避免多次重复加载
    if (window[key]) {
    return Promise.resolve();
    } else {
    if (Array.isArray(jsUrls[key])) {
    return Promise.all(jsUrls[key].map(loadScript));
    }
    return loadScript(jsUrls[key]);
    }
    };

    // 定义这些方法只是为了方便使用,其实 loadByKey 就够了。
    const loadHtml2Canvas = () => {
    return loadByKey('html2canvas');
    };

    const loadXlsx = () => {
    return loadByKey('XLSX');
    };

    const loadFlvjs = () => {
    return loadByKey('flvjs');
    };

    window.AsyncLoader = {
    loadScript,
    loadByKey,
    loadHtml2Canvas,
    loadXlsx,
    loadFlvjs,
    };
    }

    initLoader();

    使用方式


    以 XLSX 为例,使用这种方式之后,我们不需要在顶部 import xlsx from 'xlsx',只有当用户点击 导出Excel 按钮的时候,才从 CDN 动态加载 xlsx.min.js,加载成功后使用 window.XLSX 即可,代码如下:

    await window.AsyncLoader.loadXlsx().then(() => {
    const XLSX = window.XLSX;
    if (resp.data.signList && resp.data.signList.length > 0) {
    const new_workbook = XLSX.utils.book_new();

    resp.data.signList.map((item) => {
    const header = ['班级/学校/单位', '姓名', '帐号', '签到时间'];
    const { signRecords } = item;
    signRecords.unshift(header);

    const worksheet = XLSX.utils.aoa_to_sheet(signRecords);
    XLSX.utils.book_append_sheet(new_workbook, worksheet, item.signName);
    });

    XLSX.writeFile(new_workbook, `${resp.data.fileName}.xlsx`);
    } else {
    const new_workbook = XLSX.utils.book_new();
    const header = [['班级/学校/单位', '姓名', '帐号']];
    const worksheet = XLSX.utils.aoa_to_sheet(header);
    XLSX.utils.book_append_sheet(new_workbook, worksheet, '');
    XLSX.writeFile(new_workbook, `${resp.data.fileName}.xlsx`);
    }
    });

    另一个动态加载 domtoimage 的示例

    window.CommonJsLoader.loadByKey('domtoimage').then(() => {
    const scale = 2;
    window.domtoimage
    .toPng(poster, {
    height: poster.offsetHeight * scale,
    width: poster.offsetWidth * scale,
    style: {
    zoom: 1,
    transform: `scale(${scale})`,
    transformOrigin: 'top left',
    width: `${poster.offsetWidth}px`,
    height: `${poster.offsetHeight}px`,
    },
    })
    .then((dataUrl) => {
    copyImage(dataUrl, liveData?.planName);
    message.success(`${navigator.clipboard ? '复制' : '下载'}成功`);
    });
    });

    AsyncLoader 方案使用方便、理解简单,而且可以很好地利用 CDN 缓存,多个项目可以共用同样的 URL,进一步提高加载速度。而且这种方式使用的是原生 JS,在任何框架中都可以使用。


    注意,如果你用 TypeScript 开发,这种方案或许会丢失一些智能提示,如果引入了对应的 @types/xxx 应该没影响。如果你特别在意开发时的智能提示,也可以在开发的过程中 import 对应的包,开发完成后才换成 AsyncLoader 方案。

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

    当后端一次性丢给你10万条数据, 作为前端工程师的你,要怎么处理?

    前段时间有朋友问我一个他们公司遇到的问题, 说是后端由于某种原因没有实现分页功能, 所以一次性返回了2万条数据,让前端用select组件展示到用户界面里. 我听完之后立马明白了他的困惑, 如果通过硬编码的方式去直接渲染这两万条数据到select中,肯定会卡死....
    继续阅读 »

    前段时间有朋友问我一个他们公司遇到的问题, 说是后端由于某种原因没有实现分页功能, 所以一次性返回了2万条数据,让前端用select组件展示到用户界面里. 我听完之后立马明白了他的困惑, 如果通过硬编码的方式去直接渲染这两万条数据到select中,肯定会卡死. 后面他还说需要支持搜索, 也是前端来实现,我顿时产生了兴趣. 当时想到的方案大致如下:



    1. 采用懒加载+分页(前端维护懒加载的数据分发和分页)

    2. 使用虚拟滚动技术(目前react的antd4.0已支持虚拟滚动的select长列表)


    懒加载和分页方式一般用于做长列表优化, 类似于表格的分页功能, 具体思路就是用户每次只加载能看见的数据, 当滚动到底部时再去加载下一页的数据.


    虚拟滚动技术也可以用来优化长列表, 其核心思路就是每次只渲染可视区域的列表数,当滚动后动态的追加元素并通过顶部padding来撑起整个滚动内容,实现思路也非常简单.


    通过以上分析其实已经可以解决朋友的问题了,但是最为一名有追求的前端工程师, 笔者认真梳理了一下,并基于第一种方案抽象出一个实际的问题:


    如何渲染大数据列表并支持搜索功能?


    笔者将通过模拟不同段位前端工程师的实现方案, 来探索一下该问题的价值. 希望能对大家有所启发, 学会真正的深入思考.

    正文

    笔者将通过不同经验程序员的技术视角来分析以上问题, 接下来开始我们的表演.

    在开始代码之前我们先做好基础准备, 笔者先用nodejs搭建一个数据服务器, 提供基本的数据请求,核心代码如下:

    app.use(async (ctx, next) => {
    if(ctx.url === '/api/getMock') {
    let list = []

    // 生成指定个数的随机字符串
    function genrateRandomWords(n) {
    let words = 'abcdefghijklmnopqrstuvwxyz你是好的嗯气短前端后端设计产品网但考虑到付款啦分手快乐的分类开发商的李开复封疆大吏师德师风吉林省附近',
    len = words.length,
    ret = ''
    for(let i=0; i< n; i++) {
    ret += words[Math.floor(Math.random() * len)]
    }
    return ret
    }

    // 生成10万条数据的list
    for(let i = 0; i< 100000; i++) {
    list.push({
    name: `xu_0${i}`,
    title: genrateRandomWords(12),
    text: `我是第${i}项目, 赶快🌀吧~~`,
    tid: `xx_${i}`
    })
    }

    ctx.body = {
    state: 200,
    data: list
    }
    }
    await next()
    })
    以上笔者是采用koa实现的基本的mock数据服务器, 这样我们就可以模拟真实的后端环境来开始我们的前端开发啦(当然也可以直接在前端手动生成10万条数据). 其中genrateRandomWords方法用来生成指定个数的字符串,这在mock数据技术中应用很多, 感兴趣的盆友可以学习了解一下. 接下来的前端代码笔者统一采用react来实现(vue同理).

    初级工程师的方案

    直接从后端请求数据, 渲染到页面的硬编码方案,思路如下:


    代码可能是这样的:

    1. 请求后端数据:
    fetch(`${SERVER_URL}/api/getMock`).then(res => res.json()).then(res => {
    if(res.state) {
    data = res.data
    setList(data)
    }
    })
    1. 渲染页面
    {
    list.map((item, i) => {
    return <div className={styles.item} key={item.tid}>
    <div className={styles.tit}>{item.title} <span className={styles.label}>{item.name}</span></div>
    <div>{item.text}</div>
    </div>
    })
    }
    1. 搜索数据
    const handleSearch = (v) => {
    let searchData = data.filter((item, i) => {
    return item.title.indexOf(v) > -1
    })
    setList(searchData)
    }

    这样做本质上是可以实现基本的需求,但是有明显的缺点,那就是数据一次性渲染到页面中, 数据量庞大将导致页面性能极具降低, 造成页面卡顿.

    中级工程师的方案

    作为一名有一定经验的前端开发工程师,一定对页面性能有所了解, 所以一定会熟悉防抖函数节流函数, 并使用过诸如懒加载分页这样的方案, 接下来我们看看中级工程师的方案:


    通过这个过程的优化, 代码已经基本可用了, 下面来介绍具体实现方案:

    1. 懒加载+分页方案 懒加载的实现主要是通过监听窗口的滚动, 当某一个占位元素可见之后去加载下一个数据,原理如下:

    1. 这里我们通过监听windowscroll事件以及对poll元素使用getBoundingClientRect来获取poll元素相对于可视窗口的距离, 从而自己实现一个懒加载方案.


    在滚动的过程汇总我们还需要注意一个问题就是当用户往回滚动时, 实际上是不需要做任何处理的,所以我们需要加一个单向锁, 具体代码如下:

    function scrollAndLoading() {
    if(window.scrollY > prevY) { // 判断用户是否向下滚动
    prevY = window.scrollY
    if(poll.current.getBoundingClientRect().top <= window.innerHeight) {
    // 请求下一页数据
    }
    }
    }

    useEffect(() => {
    // something code
    const getData = debounce(scrollAndLoading, 300)
    window.addEventListener('scroll', getData, false)
    return () => {
    window.removeEventListener('scroll', getData, false)
    }
    }, [])

    其中prevY存储的是窗口上一次滚动的距离, 只有在向下滚动并且滚动高度大于上一次时才更新其值.


    至于分页的逻辑, 原生javascript实现分页也很简单, 我们通过定义几个维度:



    • curPage当前的页数

    • pageSize 每一页展示的数量

    • data 传入的数据量


    有了这几个条件,我们的基本能分页功能就可以完成了. 前端分页的核心代码如下:

    let data = [];
    let curPage = 1;
    let pageSize = 16;
    let prevY = 0;

    // other code...

    function scrollAndLoading() {
    if(window.scrollY > prevY) { // 判断用户是否向下滚动
    prevY = window.scrollY
    if(poll.current.getBoundingClientRect().top <= window.innerHeight) {
    curPage++
    setList(searchData.slice(0, pageSize * curPage))
    }
    }
    }
    1. 防抖函数实现 防抖函数因为比较简单, 这里直接上一个简单的防抖函数代码:
    function debounce(fn, time) {
    return function(args) {
    let that = this
    clearTimeout(fn.tid)
    fn.tid = setTimeout(() => {
    fn.call(that, args)
    }, time);
    }
    }
    1. 搜索实现 搜索功能代码如下:
    const handleSearch = (v) => {
    curPage = 1;
    prevY = 0;
    searchData = data.filter((item, i) => {
    // 采用正则来做匹配, 后期支持前端模糊搜索
    let reg = new RegExp(v, 'gi')
    return reg.test(item.title)
    })
    setList(searchData.slice(0, pageSize * curPage))
    }

    需要结合分页来实现, 所以这里为了不影响源数据, 我们采用临时数据searchData来存储. 效果如下:


    搜索后


    无论是搜索前还是搜索后, 都利用了懒加载, 所以再也不用担心数据量大带来的性能瓶颈了~

    高级工程师的方案

    作为一名久经战场的程序员, 我们应该考虑更优雅的实现方式,比如组件化, 算法优化, 多线程这类问题, 就比如我们问题中的大数据渲染, 我们也可以用虚拟长列表来更优雅简洁的来解决我们的需求. 至于虚拟长列表的实现笔者在开头已经点过,这里就不详细介绍了, 对于更大量的数据,比如100万(虽然实际开发中不会遇到这么无脑的场景),我们又该怎么处理呢?


    第一个点我们可以使用js缓冲器来分片处理100万条数据, 思路代码如下:

    function multistep(steps,args,callback){
    var tasks = steps.concat();

    setTimeout(function(){
    var task = tasks.shift();
    task.apply(null, args || []); //调用Apply参数必须是数组

    if(tasks.length > 0){
    setTimeout(arguments.callee, 25);
    }else{
    callback();
    }
    },25);
    }

    这样就能比较大量计算导致的js进程阻塞问题了.更多性能优化方案可以参考笔者之前的文章:



    我们还可以通过web worker来将需要在前端进行大量计算的逻辑移入进去, 保证js主进程的快速响应, 让web worker线程在后台计算, 计算完成后再通过web worker的通信机制来通知主进程, 比如模糊搜索等, 我们还可以对搜索算法进一步优化,比如二分法等,所以这些都是高级工程师该考虑的问题. 但是一定要分清场景, 寻找出性价比更高的方案.


    链接:https://juejin.cn/post/6844904184689475592


    收起阅读 »

    一行可以让项目启动快70%以上的代码

    前言 这两天闲来无事,想优化优化项目的启动时间,用了一个下午吧,将项目启动时间从48秒优化到14秒,大约70左右,效果还是有的,而且仅仅用了一行代码。 👇会讲一下找到这行代码的过程,如果没有耐心可以直接跳转到文章底部,直接看结论即可。 项目背景 项目就是简单的...
    继续阅读 »

    前言


    这两天闲来无事,想优化优化项目的启动时间,用了一个下午吧,将项目启动时间从48秒优化到14秒,大约70左右,效果还是有的,而且仅仅用了一行代码。


    👇会讲一下找到这行代码的过程,如果没有耐心可以直接跳转到文章底部,直接看结论即可。


    项目背景


    项目就是简单的Vue项目,不过公司内部给vue-cli包了一层,不过影响不大。


    别的也就没啥了,正常的H5网页,用的插件也不算多,为了控制项目体积。


    项目分析


    既然决定要优化了,首先要分析下项目,先用speed-measure-webpack-pluginwebpack-bundle-analyzer分析下,具体的配置这里就不多说了,很简单,网上一搜一大堆,这里直接看看结论。


    首先是项目运行时间:



    可以看到,基本上耗时大户就是eslint-loadervue-loader了,二者一个耗时40多秒,一个耗时30多秒,非常的占用资源。

    接下来再看看具体的包分析👇


    这一看就很一下子定位到问题到根源了,右侧的chunk-vendors不用看,只看左侧的chunk-page,这里面的页面数量太多了,相应的文件也很多,这也就直接导致了eslint-loadervue-loader耗时很久了,这么多文件,一个个检查耗时当然久了。


    右侧其实还可以继续优化,但感觉没必要,swiper其实并不大。


    那么现在就可以具体定位到问题了,由于项目是多SPA应用,致使.vue文件众多,在项目启动时进行eslint检查和加载耗时过长,导致项目启动时间较久。

    解决方案


    找到问题之后就得解决问题了,初步的解决方案有两个:



    1. 干掉eslint,在本地编译时不检查

    2. 缓存


    解决方案1必然是最简单的,但其实有点不合理,开着eslint就是为了规范代码格式,虽然在提交代码时也有对应的钩子来格式化代码,但在开发过程中进行提示可以更好的帮助我们形成合理的编码方式。


    所以现在剩下的方案就只有进行缓存操作了,接下来笔者就开始找相关插件来更好的进行缓存了。


    尝试解决


    首先是hard-source-webpack-plugin,这插件为模块提供中间缓存步骤,但项目得跑两次,第一次构建时间正常,第二次大概能省去90%左右的时间。


    这插件很多文章都有推荐,感觉很不错的样子,用起来也很简单,只需要👇:

    plugins: [
    new HardSourceWebpackPlugin()
    ]

    这就完事了。

    就这么简单?确实是这么简单,但也不简单,如果到此为止,笔者也不会折腾一下午了。

    就这么简单的一安装:

    npm i hard-source-webpack-plugin -D

    然后像👆一样简单的配置,然后重启项目,您猜怎么着?


    报错了!


    原因是什么呢?


    是因为speed-measure-webpack-plugin或者webpack-bundle-analyzer中的某一个,为什么呢?


    原因笔者其实并不太清楚,因为启动的时候报的错是这样的:

    Cannot find module 'webpack/lib/DependenciesBlockVariable'

    哦呦,这个错有点小意外,怎么会突然报webpack的错呢?


    笔者也是百思不得其解啊,去Google也没有人遇到这种问题。


    不得已,只能去hard-source-webpack-plugin的github上看issue,发现其实有人遇到这个问题的,他们的解决方案就是降低webpack的版本,可笔者这里没办法这么做,因为都集成在vue-cli里了,而且这个还是公司内部包了一层的,这就根本不可能降版本了。


    第一个转机


    那还能怎么办呢?


    实在没有办法了,笔者尝试搜索DependenciesBlockVariable的相关内容,这时事情发生了一丝微妙的变换,原来这个功能在webpack5中被移除了,难道是因为公司内部的vue-cli用的是webpack5.x版本?



    笔者当即在node_modules里面找到了插件,然后查看了package.json文件,结果失望的发现webpack的版本是4.2.6,这就令人绝望了,难道真的不可以么?


    既然打开了webpack的文档,那就好好看看吧。老实说这文档笔者已经看了N次了,真是每次看都有小惊喜,功能真是太多了。


    翻着翻着就看到了这个小功能👇:

    哦呦,还真有点小惊喜呦,这功能简直了,这不就是我想要的么?然后当机立断,往vue.config.js里一家,您猜怎么着?


    成了!


    虽然文档是webpack5.0的,但笔者发现4.x版本中也有这个功能,可能若一弱一些吧,多少能用啊。


    重启了几次项目后发现启动时间已经稳定了,效果真的还不错呦~


    直接给我干到了14秒,虽然有些不太稳定,但这已经是当前状态的最好解决方案了。

    所以最后的代码就是:

    chainWebpack: (config) => {
    config.cache(true)
    }

    chainWebpack的原因是项目中其实没有独立的webpack.config.js文件,所以只能放在vue.config.js文件中,使用chainWebpack来将配置插入到webpack中去。


    你以为事情到这里就结束了么?太简单了。


    第二个转机


    解决完问题后,当然要把speed-measure-webpack-plugin和webpack-bundle-analyzer这两个插件删掉了,然后整理整理代码,推上去完事。


    可笔者还是不死心,为啥hard-source-webpack-plugin不好使呢?不应该啊,为啥别人都能用,自己的项目却用不了呢?


    为了再次操作一手,也是为了更好的优化项目的启动时间,笔者再次安装了hard-source-webpack-plugin,并且对其进行了配置:

    chainWebpack: (config) => {
    config.plugin('cache').use(HardSourceWebpackPlugin)
    }

    这次再一跑,您猜怎么着?


    成了!


    为了避免再次启动失败了,笔者这次没有使用speed-measure-webpack-plugin和webpack-bundle-analyzer这两个插件,所以启动时间也没法具体估计了,但目测时间再10秒以内,强啊。


    所以说hard-source-webpack-plugin失败的原因可能就是那两个统计插件的原因了,得亏再试了一次,要不然就不明不白的GG了。


    结论


    这里的结论就很简单了,有两个版本。


    首先,如果项目能使用hard-source-webpack-plugin就很方便了,用就完事了,啥事也不需要干,所以这一行代码是👇:

    config.plugin('cache').use(HardSourceWebpackPlugin)

    大概真能快90%以上,官方并没有虚报时间。


    其次,如果用不了hard-source-webpack-plugin那就放弃吧,尝试webpack自带的cache功能也是不错的,虽然比不上hard-source-webpack-plugin,但多少也能提升70%左右的启动时间,所以这一行代码是👇:

    config.cache(true)

    并且不需要安装任何插件,一步到位。


    这两种方法其实都是可行了,论稳定和效果的话hard-source-webpack-plugin还是更胜一筹,但cache胜在不用装额外的webpack插件,具体用什么就自己决定吧。


    这里其实还是留了个坑,hard-source-webpack-plugin用不了的具体原因是什么呢?笔者只是猜测和speed-measure-webpack-plugin、webpack-bundle-analyzer这两个插件有关,但却不能肯定,如果有读者知道,欢迎在评论区留言或者私信笔者。


    看了这么久,辛苦了!


    链接:https://juejin.cn/post/6961203055257714702


    收起阅读 »

    哇擦!他居然把 React 组件渲染到了命令行终端窗口里面

    也许你之前听说过前端组件代码可以运行在浏览器,运行在移动端 App 里面,甚至可以直接在各种设备当中,但你有没有见过: 前端组件直接跑在命令行窗口里面,让前端代码构建出终端窗口的 GUI 界面和交互逻辑? 今天, 给大家分享一个非常有意思的开源项目: ink。...
    继续阅读 »

    也许你之前听说过前端组件代码可以运行在浏览器,运行在移动端 App 里面,甚至可以直接在各种设备当中,但你有没有见过: 前端组件直接跑在命令行窗口里面,让前端代码构建出终端窗口的 GUI 界面和交互逻辑?


    今天, 给大家分享一个非常有意思的开源项目: ink。它的作用就是将 React 组件渲染在终端窗口中,呈现出最后的命令行界面。


    本文偏重实战,前面会带大家熟悉基本使用,然后会做一个基于实际场景的练手项目。


    上手初体验

    刚开始上手时,推荐使用官方的脚手架创建项目,省时省心。

    npx create-ink-app --typescript

    然后运行这样一段代码:

    import React, { useState, useEffect } from 'react'
    import { render, Text} from 'ink'

    const Counter = () => {
    const [count, setCount] = useState(0)
    useEffect(() => {
    const timer = setInterval(() => {
    setCount(count => ++count)
    }, 100)
    return () => {
    clearInterval(timer)
    }

    })

    return (
    <Text color="green">
    {count} tests passed
    </Text>
    )
    }

    render(<Counter />);

    会出现如下的界面:


    并且数字一直递增! demo 虽小,但足以说明问题:




    1. 首先,这些文本输出都不是直接 console 出来的,而是通过 React 组件渲染出来的。




    2. React 组件的状态管理以及hooks 逻辑放到命令行的 GUI 当中仍然是生效的。




    也就是说,前端的能力以及扩展到了命令行窗口当中了,这无疑是一项非常可怕的能力。著名的文档生成工具
    Gatsby,包管理工具yarn2都使用了这项能力来完成终端 GUI 的搭建。

    命令行工具项目实战


    可能大家刚刚了解到这个工具,知道它的用途,但对于具体如何使用还是比较陌生。接下来让我们以一个实际的例子来进行实战,快速熟悉。代码仓库已经上传到 git,大家可以这个地址下面 fork 代码: github.com/sanyuan0704…


    下面我们就来从头到尾开发这个项目。


    项目背景


    首先说一说项目的产生背景,在一个 TS 的业务项目当中,我们曾经碰到了一个问题:由于production模式下面,我们是采用先 tsc,拿到 js 产物代码,再用webpack打包这些产物。


    但构建的时候直接报错了,原因就是 tsc 无法将 ts(x) 以外的资源文件移动到产物目录,以至于 webpack 在对于产物进行打包的时候,发现有些资源文件根本找不到!比如以前有这样一张图片的路径是这样—— src/asset/1.png,但这些在产物目录dist却没还有,因此 webpack 在打包 dist 目录下的代码时,会发现这张图片不存在,于是报错了。


    解决思路


    那如何来解决呢?


    很显然,我们很难去扩展 tsc 的能力,现在最好的方式就是写个脚本手动将src下面的所有资源文件一一拷贝到dist目录,这样就能解决资源无法找到的问题。


    一、拷贝文件逻辑


    确定了解决思路之后,我们写下这样一段 ts 代码:

    import { join, parse } from "path";
    import { fdir } from 'fdir';
    import fse from 'fs-extra'
    const staticFiles = await new fdir()
    .withFullPaths()
    // 过滤掉 node_modules、ts、tsx
    .filter(
    (p) =>
    !p.includes('node_modules') &&
    !p.endsWith('.ts') &&
    !p.endsWith('.tsx')
    )
    // 搜索 src 目录
    .crawl(srcPath)
    .withPromise() as string[]

    await Promise.all(staticFiles.map(file => {
    const targetFilePath = file.replace(srcPath, distPath);
    // 创建目录并拷贝文件
    return fse.mkdirp(parse(targetFilePath).dir)
    .then(() => fse.copyFile(file, distPath))
    );
    }))

    代码使用了fdir这个库才搜索文件,非常好用的一个库,写法上也很优雅,推荐大家使用。


    我们执行这段逻辑,成功将资源文件转移到到了产物目录中。


    问题是解决掉了,但我们能不能封装一下这个逻辑,让它能够更方便地在其它项目当中复用,甚至直接提供给其他人复用呢?


    接着,我想到了命令行工具。


    二、命令行 GUI 搭建


    接着我们使用 ink,也就是用 React 组件的方式来搭建命令行 GUI,根组件代码如下:

    // index.tsx 引入代码省略
    interface AppProps {
    fileConsumer: FileCopyConsumer
    }

    const ACTIVE_TAB_NAME = {
    STATE: "执行状态",
    LOG: "执行日志"
    }

    const App: FC<AppProps> = ({ fileConsumer }) => {
    const [activeTab, setActiveTab] = useState<string>(ACTIVE_TAB_NAME.STATE);
    const handleTabChange = (name) => {
    setActiveTab(name)
    }
    const WELCOME_TEXT = dedent`
    欢迎来到 \`ink-copy\` 控制台!功能概览如下(按 **Tab** 切换):
    `

    return <>
    <FullScreen>
    <Box>
    <Markdown>{WELCOME_TEXT}</Markdown>
    </Box>
    <Tabs onChange={handleTabChange}>
    <Tab name={ACTIVE_TAB_NAME.STATE}>{ACTIVE_TAB_NAME.STATE}</Tab>
    <Tab name={ACTIVE_TAB_NAME.LOG}>{ACTIVE_TAB_NAME.LOG}</Tab>
    </Tabs>
    <Box>
    <Box display={ activeTab === ACTIVE_TAB_NAME.STATE ? 'flex': 'none'}>
    <State />
    </Box>
    <Box display={ activeTab === ACTIVE_TAB_NAME.LOG ? 'flex': 'none'}>
    <Log />
    </Box>
    </Box>
    </FullScreen>
    </>
    };

    export default App;

    可以看到,主要包含两大组件: StateLog,分别对应两个 Tab 栏。具体的代码大家去参考仓库即可,下面放出效果图:


    3. GUI 如何实时展示业务状态?


    现在问题就来了,文件操作的逻辑开发完了,GUI 界面也搭建好了。那么现在如何将两者结合起来呢,也就是 GUI 如何实时地展示文件操作的状态呢?


    对此,我们需要引入第三方,来进行这两个模块的通信。具体来讲,我们在文件操作的逻辑中维护一个 EventBus 对象,然后在 React 组件当中,通过 Context 的方式传入这个 EventBus。
    从而完成 UI 和文件操作模块的通信。


    现在我们开发一下这个 EventBus 对象,也就是下面的FileCopyConsumer:

    export interface EventData {
    kind: string;
    payload: any;
    }

    export class FileCopyConsumer {

    private callbacks: Function[];
    constructor() {
    this.callbacks = []
    }
    // 供 React 组件绑定回调
    onEvent(fn: Function) {
    this.callbacks.push(fn);
    }
    // 文件操作完成后调用
    onDone(event: EventData) {
    this.callbacks.forEach(callback => callback(event))
    }
    }

    接着在文件操作模块和 UI 模块当中,都需要做响应的适配,首先看看文件操作模块,我们做一下封装。

    export class FileOperator {
    fileConsumer: FileCopyConsumer;
    srcPath: string;
    targetPath: string;
    constructor(srcPath ?: string, targetPath ?: string) {
    // 初始化 EventBus 对象
    this.fileConsumer = new FileCopyConsumer();
    this.srcPath = srcPath ?? join(process.cwd(), 'src');
    this.targetPath = targetPath ?? join(process.cwd(), 'dist');
    }

    async copyFiles() {
    // 存储 log 信息
    const stats = [];
    // 在 src 中搜索文件
    const staticFiles = ...

    await Promise.all(staticFiles.map(file => {
    // ...
    // 存储 log
    .then(() => stats.push(`Copied file from [${file}] to [${targetFilePath}]`));
    }))
    // 调用 onDone
    this.fileConsumer.onDone({
    kind: "finish",
    payload: stats
    })
    }
    }

    然后在初始化 FileOperator之后,将 fileConsumer通过 React Context 传入到组件当中,这样组件就能访问到fileConsumer,进而可以进行回调函数的绑定,代码演示如下:

    // 组件当中拿到 fileConsumer & 绑定回调
    export const State: FC<{}> = () => {
    const context = useContext(Context);
    const [finish, setFinish] = useState(false);
    context?.fileConsumer.onEvent((data: EventData) => {
    // 下面的逻辑在文件拷贝完成后执行
    if (data.kind === 'finish') {
    setTimeout(() => {
    setFinish(true)
    }, 2000)
    }
    })

    return
    //(JSX代码)
    }

    这样,我们就成功地将 UI 和文件操作逻辑串联了起来。当然,篇幅所限,还有一些代码并没有展示出来,完整的代码都在 git 仓库当中。希望大家能 fork 下来好好体会一下整个项目的设计。


    总体来说,React 组件代码能够跑在命令行终端,确实是一件激动人心的事情,给前端释放了更多想象的空间。本文对于这个能力的使用也只是冰山一角,更多使用姿势等待你去解锁,赶紧去玩一玩吧!


    链接:https://juejin.cn/post/6952673382928220191

    收起阅读 »

    如何应用 SOLID 原则整理 React 代码之单一原则

    SOLID 原则的主要是作为关心自己工作的软件专业人员的指导方针,另外还为那些以经得起时间考验的设计精美的代码库为荣的人。 今天,我们将从一个糟糕的代码示例开始,应用 SOLID 的第一个原则,看看它如何帮助我们编写小巧、漂亮、干净的并明确责任的 React ...
    继续阅读 »

    SOLID 原则的主要是作为关心自己工作的软件专业人员的指导方针,另外还为那些以经得起时间考验的设计精美的代码库为荣的人。


    今天,我们将从一个糟糕的代码示例开始,应用 SOLID 的第一个原则,看看它如何帮助我们编写小巧、漂亮、干净的并明确责任的 React 组件,。

    什么是单一责任原则?

    单一责任原则告诉我们的是,每个类或组件应该有一个单一的存在目的。

    组件应该只做一件事,并且做得很好。

    让我们重构一段糟糕但正常工作的代码,并使用这个原则使其更加清晰和完善。

    让我们从一个糟糕的例子开始

    首先让我们看看一些违反这一原则的代码,添加注释是为了更好地理解:

    import React, {useEffect, useReducer, useState} from "react";

    const initialState = {
    isLoading: true
    };

    // 复杂的状态管理
    function reducer(state, action) {
    switch (action.type) {
    case 'LOADING':
    return {isLoading: true};
    case 'FINISHED':
    return {isLoading: false};
    default:
    return state;
    }
    }

    export const SingleResponsibilityPrinciple = () => {

    const [users , setUsers] = useState([])
    const [filteredUsers , setFilteredUsers] = useState([])
    const [state, dispatch] = useReducer(reducer, initialState);

    const showDetails = (userId) => {
    const user = filteredUsers.find(user => user.id===userId);
    alert(user.contact)
    }

    // 远程数据获取
    useEffect(() => {
    dispatch({type:'LOADING'})
    fetch('https://jsonplaceholder.typicode.com/users')
    .then(response => response.json())
    .then(json => {
    dispatch({type:'FINISHED'})
    setUsers(json)
    })
    },[])

    // 数据处理
    useEffect(() => {
    const filteredUsers = users.map(user => {
    return {
    id: user.id,
    name: user.name,
    contact: `${user.phone} , ${user.email}`
    };
    });
    setFilteredUsers(filteredUsers)
    },[users])

    // 复杂UI渲染
    return <>
    Users List

    Loading state: {state.isLoading? 'Loading': 'Success'}

    {users.map(user => {
    return
    showDetails(user.id)}>
    {user.name}

    {user.email}


    })}

    }

    这段代码的作用

    这是一个函数式组件,我们从远程数据源获取数据,再过滤数据,然后在 UI 中显示数据。我们还检测 API 调用的加载状态。

    为了更好地理解这个例子,我把它简化了。但是你几乎可以在任何地方的同一个组件中找到它们!这里发生了很多事情:

    1. 远程数据的获取

    2. 数据过滤

    3. 复杂的状态管理

    4. 复杂的 UI 功能

    因此,让我们探索如何改进代码的设计并使其紧凑。

    1. 移动数据处理逻辑

    不要将 HTTP 调用保留在组件中。这是经验之谈。您可以采用几种策略从组件中删除这些代码。

    您至少应该创建一个自定义 Hook 并将数据获取逻辑移动到那里。例如,我们可以创建一个名为 useGetRemoteData 的 Hook,如下所示:

    import {useEffect, useReducer, useState} from "react";

    const initialState = {
    isLoading: true
    };

    function reducer(state, action) {
    switch (action.type) {
    case 'LOADING':
    return {isLoading: true};
    case 'FINISHED':
    return {isLoading: false};
    default:
    return state;
    }
    }

    export const useGetRemoteData = (url) => {

    const [users , setUsers] = useState([])
    const [state, dispatch] = useReducer(reducer, initialState);

    const [filteredUsers , setFilteredUsers] = useState([])


    useEffect(() => {
    dispatch({type:'LOADING'})
    fetch('https://jsonplaceholder.typicode.com/users')
    .then(response => response.json())
    .then(json => {
    dispatch({type:'FINISHED'})
    setUsers(json)
    })
    },[])

    useEffect(() => {
    const filteredUsers = users.map(user => {
    return {
    id: user.id,
    name: user.name,
    contact: `${user.phone} , ${user.email}`
    };
    });
    setFilteredUsers(filteredUsers)
    },[users])

    return {filteredUsers , isLoading: state.isLoading}
    }

    现在我们的主要组件看起来像这样:

    import React from "react";
    import {useGetRemoteData} from "./useGetRemoteData";

    export const SingleResponsibilityPrinciple = () => {

    const {filteredUsers , isLoading} = useGetRemoteData()

    const showDetails = (userId) => {
    const user = filteredUsers.find(user => user.id===userId);
    alert(user.contact)
    }

    return <>
    Users List

    Loading state: {isLoading? 'Loading': 'Success'}

    {filteredUsers.map(user => {
    return
    showDetails(user.id)}>
    {user.name}

    {user.email}


    })}

    }

    看看我们的组件现在是多么的小,多么的容易理解!这是在错综复杂的代码库中所能做的最简单、最重要的事情。

    但我们可以做得更好。

    2. 可重用的数据获取钩子

    现在,当我们看到我们 useGetRemoteData Hook 时,我们看到这个 Hook 正在做两件事:




    1. 从远程数据源获取数据




    2. 过滤数据




    让我们把获取远程数据的逻辑提取到一个单独的钩子,这个钩子的名字是 useHttpGetRequest,它把 URL 作为一个参数:

    import {useEffect, useReducer, useState} from "react";
    import {loadingReducer} from "./LoadingReducer";

    const initialState = {
    isLoading: true
    };

    export const useHttpGetRequest = (URL) => {

    const [users , setUsers] = useState([])
    const [state, dispatch] = useReducer(loadingReducer, initialState);

    useEffect(() => {
    dispatch({type:'LOADING'})
    fetch(URL)
    .then(response => response.json())
    .then(json => {
    dispatch({type:'FINISHED'})
    setUsers(json)
    })
    },[])

    return {users , isLoading: state.isLoading}

    }

    我们还将 reducer 逻辑移除到一个单独的文件中:

    export function loadingReducer(state, action) {
    switch (action.type) {
    case 'LOADING':
    return {isLoading: true};
    case 'FINISHED':
    return {isLoading: false};
    default:
    return state;
    }
    }

    所以现在我们的 useGetRemoteData 变成了:

    import {useEffect, useState} from "react";
    import {useHttpGetRequest} from "./useHttpGet";
    const REMOTE_URL = 'https://jsonplaceholder.typicode.com/users'

    export const useGetRemoteData = () => {
    const {users , isLoading} = useHttpGetRequest(REMOTE_URL)
    const [filteredUsers , setFilteredUsers] = useState([])

    useEffect(() => {
    const filteredUsers = users.map(user => {
    return {
    id: user.id,
    name: user.name,
    contact: `${user.phone} , ${user.email}`
    };
    });
    setFilteredUsers(filteredUsers)
    },[users])

    return {filteredUsers , isLoading}
    }

    干净多了,对吧? 我们能做得更好吗? 当然,为什么不呢?

    3. 分解 UI 组件

    看看我们的组件,其中显示了用户的详细信息。我们可以为此创建一个可重用的 UserDetails 组件:

    const UserDetails = (user) => {

    const showDetails = (user) => {
    alert(user.contact)
    }

    return
    showDetails(user)}>
    {user.name}

    {user.email}


    }

    最后,我们的原始组件变成:

    import React from "react";
    import {useGetRemoteData} from "./useGetRemoteData";

    export const Users = () => {
    const {filteredUsers , isLoading} = useGetRemoteData()

    return <>
    Users List

    Loading state: {isLoading? 'Loading': 'Success'}

    {filteredUsers.map(user => )}

    }

    我们把代码从60行精简到了12行!我们创建了五个独立的组成部分,每个部分都有明确而单一的职责。

    让我们回顾一下我们刚刚做了什么


    让我们回顾一下我们的组件,看看我们是否实现了 SRP:




    • Users.js - 负责显示用户列表




    • UserDetails.js ー 负责显示用户的详细资料




    • useGetRemoteData.js - 负责过滤远程数据




    • useHttpGetrequest.js - 负责 HTTP 调用




    • LoadingReducer.js - 复杂的状态管理




    当然,我们可以改进很多其他的东西,但是这应该是一个很好的起点。


    链接:https://juejin.cn/post/6963480203637030926




    收起阅读 »

    React的性能优化(useMemo和useCallback)的使用

    一、业务场景 React是一个用于构建用户界面的javascript的库,主要负责将数据转换为视图,保证数据和视图的统一,react在每次数据更新的时候都会重新render来保证数据和视图的统一,但是当父组件内部数据的变化,在父组件下挂载的所有子组件也会重新...
    继续阅读 »

    一、业务场景



    React是一个用于构建用户界面的javascript的库,主要负责将数据转换为视图,保证数据和视图的统一,react在每次数据更新的时候都会重新render来保证数据和视图的统一,但是当父组件内部数据的变化,在父组件下挂载的所有子组件也会重新渲染,因为react默认会全部渲染所有的组件,包括子组件的子组件,这就造成不必要的浪费。

    1、使用类定义一个父组件
    export default class Parent extends React.Component {
    state = {
    count: 0,
    }
    render() {
    return(
    <div>
    我是父组件
    <button onClick={() => this.setState({count: this.state.count++})}>点击按钮</button>
    <Child />
    </div>
    )
    }
    }

    2、定义一个子组件

    class Child extends React.Component {
    render() {
    console.log('我是子组件');
    return (
    <div>
    我是子组件
    <Grandson />
    </div>
    )
    }
    }

    3、定义一个孙子组件

    class Grandson extends React.Component {
    render() {
    console.log('孙子组件')
    return(<div>孙子组件</div>)
    }
    }
    • 4、上面几个组件是比较标准的react的类组件,函数组件也是类似的,当你在父组件中点击按钮,其实你仅仅是想改变父组件内的count的值,但是你会发现每次点击的时候子组件和孙组件也会重新渲染,因为react并不知道是不是要渲染子组件,需要我们自己去判断。



    一、类组件中使用shouldComponentUpdate生命周期钩子函数

    1、在子组件中使用shouldComponentUpdate来判断是否要更新,

    其实就是根据this.props和函数参数中的nextProps中的参数来对比,如果返回false就不更新,如果返回ture就表示需要更新当前组件


    class Child extends React.Component {
    shouldComponentUpdate (nextProps, nextState) {
    console.log(nextProps, this.props);
    if (nextProps.count === this.props.count) {
    return false;
    } else {
    return true;
    }
    }
    ...
    }
  • **注意点:**这里的count是要父组件给当前组件传递的参数(就是你要监听变化的的来更新当前组件),如果你写一个nextProps.name === this.props.name其实,父组件并没有给当前组件传递name那么下面都是返回false组件不更新




  • 2、当子组件没更新,那么孙组件同样的不更新数据


  • 二、使用PureComponet语法糖

    其实PureComponet就是一个语法糖,只是官方在底层帮你实现了shouldComponentUpdate方法而已,使用的时候只需要子类继承这个类就可以

    • 1、子组件中继承

    class Child extends React.PureComponent {
    render() {
    console.log('我是子组件');
    return (
    <div>
    我是子组件
    <Grandson />
    </div>
    )
    }
    }

    2、在父组件中使用

    // 下面这种情况不会重新渲染子组件
    <Child/>
    // 下面这种情况下会重新渲染子组件
    <Child count={this.state.count}/>

    三、memo的使用



    当你子组件是类组件的时候可以使用shouldComponentUpdate钩子函数或类组件继承PureComponent来实现不渲染子组件,但是对于函数组件来说是不能用这两个方法的,因此react官方给函数组件提供了memo来对函数组件包装下,实现不必要的渲染。





    • 1、组件定义(这里也可以使用类组件)

    function Child () {
    console.log('我是子组件');
    return (
    <div>
    子组件
    </div>
    )
    }

    function Parent () {
    const [count, setCount] = useState(0);
    return (
    <div>
    我是父组件-{count}
    <button onClick={()=>setCount(count + 1)}>点击按钮</button>
    <Child />
    </div>
    )
    }

    2、这里我们父组件内部改变count并没有传递给子组件,但是子组件一样的重新渲染了,这并不是我们希望看到的,因为需要对子组件包装下

    function Child () {
    console.log('我是子组件');
    return (
    <div>
    子组件
    </div>
    )
    }

    const ChildMemo = React.memo(Child);
    function Parent () {
    const [count, setCount] = useState(0);
    return (
    <div>
    我是父组件-{count}
    <button onClick={()=>setCount(count + 1)}>点击按钮</button>
    {/* 这种情况下不会渲染子组件 */}
    <ChildMemo />
    {/* 这种情况下会渲染子组件 */}
    <ChildMemo count={count}/>
    </div>
    )
    }

    四、useMemouseCallback的认识




    • 1、useMemouseCallback都是具有缓存作用的,只是他们缓存对象不一样,一个是属性,一个是缓存函数,特点都是,当缓存依赖的没变,去获取还是获取曾经的缓存




    • 2、useMemo是对函数组件中的属性包装,返回一个具有缓存效果的新的属性,当依赖的属性没变化的时候,这个返回新属性就会从缓存中获取之前的。




    • 3、useCallback是对函数组件中的方法缓存,返回一个被缓存的方法

    五、useMemo的使用(我们依赖借用子组件更新的来做)

    • 1、根据上面的方式我们在父组件更新数据,观察子组件变化

    const Child = (props) => {
    console.log('重新渲染子组件', props);
    return (
    <div>子组件</div>
    )
    }
    const ChildMemo = React.memo(Child);

    const Parent = () => {
    const [count, setCount] = useState(0);
    const [number, setNumber]=useState(0)
    const userInfo = {
    age: count,
    name: 'hello',
    }

    const btnHandler = () => {
    setNumber(number+1);
    }
    return (
    <div>
    {number}-{count}
    <button onClick={btnHandler}>按钮</button>
    <ChildMemo userInfo={userInfo}/>
    </div>
    )
    }
  • 上面发现我们仅仅是更新了number的值,传递给子组件的对象值并没有变化,但是每次子组件都重新更新了,虽然我们在子组件上用了React.memo包装还是不行,这是因为在父组件中每次重新渲染,对于对象来说会是重新一个新的对象了。因此子组件要重新更新,




  • 2、使用useMemo对属性的包装

  • const userInfo = useMemo(() => {
    return {
    age: count,
    name: 'hello',
    };
    }, [count]);
  • 使用useMemo包装后的对象,重新返回一个具有缓存效果的新对象,第二个参数表示依赖性,或者叫观察对象,当被观察的没变化,返回的就是缓存对象,如果被观察的变化了,那么就会返回新的,现在不管你怎么更新number的值,子组件都不会重新更新了




  • 3、注意点:useMemo要配合React.memo来使用,不然传递到子组件也是不生效的

  • 六、useCallback的使用

    前面介绍了,useCallback是对一个方法的包装,返回一个具有缓存的方法,常见的使用场景是,父组件要传递一个方法给子组件

    • 1、在不使用useCallback的时候

    const Child = (props) => {
    console.log('渲染了子组件');
    const { onClick } = props;
    return (
    <button onClick={onClick}>点击按钮获取值</button>
    )
    }

    const ChildMemo = React.memo(Child);

    const Parent = () => {
    const [text, updateText] = useState('');
    const textRef = useRef(text);
    const handleSubmit = () => {
    console.log('当前的值', text);
    }
    return(
    <div>
    我是父组件
    <input type="text" value={text} onChange={(e) => updateText(e.target.value)}/>
    <ChildMemo onClick={handleSubmit}/>
    </div>
    )
    }
    • 结果是每次输入框输入值的时候,子组件就会重新渲染一次,其实子组件中仅仅是一个按钮,要获取最终输入的值,每次父组件输入值的时候,子组件就更新,很耗性能的

    2、使用useCallback来包装一个方法

    const Parent = () => {
    const [text, updateText] = useState('');
    const textRef = useRef();

    // useCallback又依赖了textRef的变化,因此可以获取到最新的数据
    const handleSubmit = useCallback(() => {
    console.log('当前输入框的值:', textRef.current);
    }, [textRef])

    // 当text的值变化的时候就会给textRef的current重新赋值
    useEffect(() => {
    textRef.current = text;
    }, [text]);

    return(
    <div>
    我是父组件
    <input type="text" value={text} onChange={(e) => updateText(e.target.value)}/>
    <ChildMemo onClick={handleSubmit}/>
    </div>
    )
    }


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

    收起阅读 »

    React22-diff算法

    1.时间复杂度最好的算法都是o(n3) n为元素个数 如果每个元素都去diff的话复杂度很高 所以react做出啦三个限制 1.只对同级元素进行diff,不用对另一个树的所有元素进行diff 2.tag不同的两个元素会产生不同的树,div变为p,react会销...
    继续阅读 »

    1.时间复杂度

    最好的算法都是o(n3) n为元素个数 如果每个元素都去diff的话复杂度很高 所以react做出啦三个限制


    1.只对同级元素进行diff,不用对另一个树的所有元素进行diff


    2.tag不同的两个元素会产生不同的树,div变为p,react会销毁div及其子孙节点,新建p


    3.通过key这个prop来暗示哪些子元素在不同渲染下保持稳定


    举例说明第3点

    // 更新前
    <div>
    <p >ka</p>
    <h3 >song</h3>
    </div>

    // 更新后
    <div>
    <h3 >song</h3>
    <p>ka</p>
    </div>
    这种情况下diff react会把p删除然后新建h3 插入 然后删除h3创建p在传入
    // 更新前
    <div>
    <p key="ka">ka</p>
    <h3 key="song">song</h3>
    </div>

    // 更新后
    <div>
    <h3 key="song">song</h3>
    <p key="ka">ka</p>
    </div>
    //有key的情况,p节点找到后面key相同的p节点所以节点可以复用 h3也可以复用 只需要做一个p append到h3后面的操作

    2.单一节点的diff

    1.diff 就是对比jsx和currentfiber的对象生成workingprogress的fiber


    2.dom节点是否可复用?1.type必须相同 2.key也必须相同 满足这两个才能复用 先检测key是否同再检测type是否同,可复用就复用这个fiber 不过是换属性而已



    3.什么情况不能复用?


    1.key不一样: 这函数一开始就判断key是否相同 不相同直接删除current的fiber,然后找兄弟节点去看是否key相同,为什么找兄弟节点呢 因为可能currentfiber有同级节点而jsx只是单个节点,还是会走到singleelement这个逻辑,所以要看currentfiber同级所有节点,不能旧删除是否可以复用。同时如果某个节点可以复用我们也需要将currentfiber的其他同级fiber删掉。都是为了下面这种情况。


    如果都不一样则创建新的fiber

    //current
    <div></div>
    <p></p>
    //jsx
    <p><p>

    2.type不一样:直接把current的该fiber和兄弟fiber全部删除,因为能判断到type的时候key已经相同啦,其他兄弟节点的key不可能相同,所以直接全部不可以复用。之前key不同还要看兄弟key是否相同。

    3.多节点diff

    什么时候执行多节点diff?当jsx此次为数组即可,无论currentfiber是不是数组

    一共有三种情况

    1.节点更新

    //情况1—— 节点属性变化
    // 之前
    <ul>
    <li key="0" className="before">0<li>
    <li key="1">1<li>
    </ul>

    // 之后
    <ul>
    <li key="0" className="after">0<li>
    <li key="1">1<li>
    </ul>
    //情况2—— 节点类型更新
    // 之后
    <ul>
    <div key="0">0</div>
    <li key="1">1<li>
    </ul>

    2.节点新增或者减少

    //情况1 —— 新增节点
    // 之前
    <ul>
    <li key="0">0<li>
    <li key="1">1<li>
    </ul>

    // 之后
    <ul>
    <li key="0">0<li>
    <li key="1">1<li>
    <li key="2">2<li>
    </ul>
    //情况2 —— 删除节点
    // 之后
    <ul>
    <li key="1">1<li>
    </ul>

    3.节点位置变化

    // 之前
    <ul>
    <li key="0">0<li>
    <li key="1">1<li>
    </ul>

    // 之后
    <ul>
    <li key="1">1<li>
    <li key="0">0<li>
    </ul>

    在我们做数组相关的算法题时,经常使用双指针从数组头和尾同时遍历以提高效率,但是这里却不行。


    虽然本次更新的JSX对象 newChildren为数组形式,但是和newChildren中每个组件进行比较的是current fiber,同级的Fiber节点是由sibling指针链接形成的单链表,即不支持双指针遍历。


    newChildren[0]fiber比较,newChildren[1]fiber.sibling比较。


    所以无法使用双指针优化。


    多节点diff 最终会产生一条fiber链表,不过最后返回的还是一个fiber(第一个fiber)作为child


    基于以上原因,Diff算法的整体逻辑会经历两轮遍历:


    第一轮遍历:处理更新的节点。 因为更新的概率是最大的


    第二轮遍历:处理剩下的不属于更新的节点

    第一轮遍历



    1. let i = 0,遍历newChildren,将newChildren[i]oldFiber比较,判断DOM节点是否可复用。

    2. 如果可复用,i++,继续比较newChildren[i]oldFiber.sibling,可以复用则继续遍历。

    3. 如果不可复用,分两种情况:



    • key不同导致不可复用,立即跳出整个遍历,第一轮遍历结束。(因为key不同是对应节点位置变化不属于更新节点,等到第二轮循环处理)

    • key相同type不同导致不可复用,会将oldFiber标记为DELETION(这样在commit阶段就会删除这个dom),并继续遍历



    1. 如果newChildren遍历完(即i === newChildren.length - 1)或者oldFiber遍历完(即oldFiber.sibling === null),跳出遍历,第一轮遍历结束。

    function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChildren: Array<*>,
    lanes: Lanes,
    ): Fiber | null {
    // This algorithm can't optimize by searching from both ends since we
    // don't have backpointers on fibers. I'm trying to see how far we can get
    // with that model. If it ends up not being worth the tradeoffs, we can
    // add it later.

    // Even with a two ended optimization, we'd want to optimize for the case
    // where there are few changes and brute force the comparison instead of
    // going for the Map. It'd like to explore hitting that path first in
    // forward-only mode and only go for the Map once we notice that we need
    // lots of look ahead. This doesn't handle reversal as well as two ended
    // search but that's unusual. Besides, for the two ended optimization to
    // work on Iterables, we'd need to copy the whole set.

    // In this first iteration, we'll just live with hitting the bad case
    // (adding everything to a Map) in for every insert/move.

    // If you change this code, also update reconcileChildrenIterator() which
    // uses the same algorithm.

    if (__DEV__) {
    // First, validate keys.
    let knownKeys = null;
    for (let i = 0; i < newChildren.length; i++) {
    const child = newChildren[i];
    knownKeys = warnOnInvalidKey(child, knownKeys, returnFiber);
    }
    }

    let resultingFirstChild: Fiber | null = null;//!返回的fiber
    let previousNewFiber: Fiber | null = null;//!创建fiber链需要一个临时fiber来做连接

    let oldFiber = currentFirstChild;//!遍历到的current的fiber(旧的fiber)
    let lastPlacedIndex = 0;//!新创建的fiber节点对应的dom节点在页面中的位置 用来节点位置变化的
    let newIdx = 0;//!遍历到的jsx数组的索引
    let nextOldFiber = null;//!oldFiber的下一个fiber
    //!第一轮循环 处理节点更新的情况
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    if (oldFiber.index > newIdx) {//!fiber的index标记为当前fiber在同级fiber中的位置
    nextOldFiber = oldFiber;
    oldFiber = null;
    } else {
    nextOldFiber = oldFiber.sibling;
    }
    const newFiber = updateSlot(//!判断节点是否可以复用 这个函数主要看key是否相同 不相同直接返回null 相同则继续判断type是否相同 不相同则创建新的fiber返回 把旧的fiber打上delete的tag 新的fiber打上place的tag type也相同则可以复用fiber返回
    returnFiber,
    oldFiber,
    newChildren[newIdx],
    lanes,
    );
    if (newFiber === null) {
    // TODO: This breaks on empty slots like null children. That's
    // unfortunate because it triggers the slow path all the time. We need
    // a better way to communicate whether this was a miss or null,
    // boolean, undefined, etc.
    if (oldFiber === null) {
    oldFiber = nextOldFiber;
    }
    break;
    }
    if (shouldTrackSideEffects) {
    if (oldFiber && newFiber.alternate === null) {
    // We matched the slot, but we didn't reuse the existing fiber, so we
    // need to delete the existing child.
    deleteChild(returnFiber, oldFiber);
    }
    }
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);//!给newfiber加上place的tag
    if (previousNewFiber === null) {
    // TODO: Move out of the loop. This only happens for the first run.
    resultingFirstChild = newFiber;
    } else {
    // TODO: Defer siblings if we're not at the right index for this slot.
    // I.e. if we had null values before, then we want to defer this
    // for each null value. However, we also don't want to call updateSlot
    // with the previous one.
    previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
    oldFiber = nextOldFiber;
    }
    //!新旧同时遍历完
    if (newIdx === newChildren.length) {
    // We've reached the end of the new children. We can delete the rest.
    deleteRemainingChildren(returnFiber, oldFiber);
    return resultingFirstChild;
    }
    //!老的遍历完 新的没遍历完 遍历剩下的jsx的newchildren
    if (oldFiber === null) {
    // If we don't have any more existing children we can choose a fast path
    // since the rest will all be insertions.
    for (; newIdx < newChildren.length; newIdx++) {
    const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
    if (newFiber === null) {
    continue;
    }
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);//!直接把新的节点打上place 插入dom
    if (previousNewFiber === null) {
    // TODO: Move out of the loop. This only happens for the first run.
    resultingFirstChild = newFiber;
    } else {
    previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
    }
    return resultingFirstChild;
    }
    //!老的没遍历完 新的也没遍历完 证明key不同跳出啦 要处理节点位置变化的情况 我们要找到key相同的复用 那么为了在o(1)时间内找到 我们用map(key:oldfiber.key->value:oldfiber)数据结构
    // Add all children to a key map for quick lookups.
    const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
    //!遍历剩下的newchldren
    // Keep scanning and use the map to restore deleted items as moves.
    for (; newIdx < newChildren.length; newIdx++) {
    const newFiber = updateFromMap(//!找到newchildren的key对应的oldfiber 复用/新建fiber返回
    existingChildren,
    returnFiber,
    newIdx,
    newChildren[newIdx],
    lanes,
    );
    if (newFiber !== null) {
    if (shouldTrackSideEffects) {
    if (newFiber.alternate !== null) {
    // The new fiber is a work in progress, but if there exists a
    // current, that means that we reused the fiber. We need to delete
    // it from the child list so that we don't add it to the deletion
    // list.
    existingChildren.delete(
    newFiber.key === null ? newIdx : newFiber.key,//!从map中去掉已经找到key的oldfiber
    );
    }
    }
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);//!新的fiber标记为插入 注意位置 (oldindex<lastplaceindex) 移动位置插入 因为老的fiber的index位置比新的页面位置小 肯定要移动插入了
    if (previousNewFiber === null) {
    resultingFirstChild = newFiber;
    } else {
    previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
    }
    }

    if (shouldTrackSideEffects) {
    // Any existing children that weren't consumed above were deleted. We need
    // to add them to the deletion list.
    existingChildren.forEach(child => deleteChild(returnFiber, child));//!删除多余的oldfiber 因为新的children已经遍历完
    }

    return resultingFirstChild;
    }

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

    收起阅读 »

    使用react的7个避坑案例

    React是个很受欢迎的前端框架。今天我们探索下React开发者应该注意的七个点。 1. 组件臃肿 React开发者没有创建必要的足够多的组件化,其实这个问题不局限于React开发者,很多Vue开发者也是。 当然,我们现在讨论的是React 在React中...
    继续阅读 »

    React是个很受欢迎的前端框架。今天我们探索下React开发者应该注意的七个点。


    1. 组件臃肿


    React开发者没有创建必要的足够多的组件化,其实这个问题不局限于React开发者,很多Vue开发者也是。



    当然,我们现在讨论的是React



    React中,我们可以创建一个很多内容的组件,来执行我们的各种任务,但是最好是保证组件精简 -- 一个组件关联一个函数。这样不仅节约你的时间,而且能帮你很好地定位问题


    比如下面的TodoList组件:

    // ./components/TodoList.js

    import React from 'react';

    import { useTodoList } from '../hooks/useTodoList';
    import { useQuery } from '../hooks/useQuery';
    import TodoItem from './TodoItem';
    import NewTodo from './NewTodo';

    const TodoList = () => {
    const { getQuery, setQuery } = useQuery();
    const todos = useTodoList();
    return (
    <div>
    <ul>
    {todos.map(({ id, title, completed }) => (
    <TodoItem key={id} id={id} title={title} completed={completed} />
    ))}
    <NewTodo />
    </ul>
    <div>
    Highlight Query for incomplete items:
    <input value={getQuery()} onChange={e => setQuery(e.target.value)} />
    </div>
    </div>
    );
    };

    export default TodoList;

    2. 直接更改state

    React中,状态应该是不变的。如果你直接修改state,会导致难以修改的性能问题。

    比如下面例子:

    const modifyPetsList = (element, id) => {
    petsList[id].checked = element.target.checked;
    setPetsList(petList)
    }

    上面例子中,你想更改数组对象中checked键。但是你遇到一个问题:因为使用相同的引用更改了对象,React无法观察并触发重新渲染


    解决这个问题,我们应该使用setState()方法或者useState()钩子。


    我们使用useState()方法来重写之前的例子。

    const modifyPetsList = (element, id) => {
    const { checked } = element.target;
    setpetsList((pets) => {
    return pets.map((pet, index) => {
    if (id === index) {
    pet = { ...pet, checked };
    }
    return pet;
    });
    });
    };

    3. props该传数字类型的值却传了字符串,反之亦然

    这是个很小的错误,不应该出现。

    比如下面的例子:

    class Arrival extends React.Component {
    render() {
    return (
    <h1>
    Hi! You arrived {this.props.position === 1 ? "first!" : "last!"} .
    </h1>
    );
    }
    }

    这里===对字符串'1'是无效的。而解决这个问题,需要我们在传递props值的时候用{}包裹。

    修正如下:

    // ❌
    const element = <Arrival position='1' />;

    // ✅
    const element = <Arrival position={1} />;

    4. list组件中没使用key

    假设我们需要渲染下面的列表项:

    const lists = ['cat', 'dog', 'fish’];

    render() {
    return (
    <ul>
    {lists.map(listNo =>
    <li>{listNo}</li>)}
    </ul>
    );
    }

    当然,上面的代码可以运行。当列表比较庞杂并需要进行更改等操作的时候,就会带来渲染的问题。


    React跟踪文档对象模型(DOM)上的所有列表元素。没有记录可以告知React,列表发生了什么改动。


    解决这个问题,你需要添加keys在你的列表元素中keys赋予每个元素唯一标识,这有助于React确定已添加,删除,修改了哪些项目。


    如下:

    <ul>
    {lists.map(listNo =>
    <li key={listNo}>{listNo}</li>)}
    </ul>

    5. setState是异步操作

    很容易忘记React中的state是异步操作的。如果你在设置一个值之后就去访问它,那么你可能不能立马获取到该值。

    我们看看下面的例子:

    handlePetsUpdate = (petCount) => {
    this.setState({ petCount });
    this.props.callback(this.state.petCount); // Old value
    };

    你可以使用setState()的第二个参数,回调函数来处理。比如:

    handlePetsUpdate = (petCount) => {
    this.setState({ petCount }, () => {
    this.props.callback(this.state.petCount); // Updated value
    });
    };

    6. 频繁使用Redux

    在大型的React app中,很多开发者使用Redux来管理全局状态。

    虽然Redux很有用,但是没必要使用它来管理每个状态

    如果我们的应用程序没有需要交换信息的并行级组件的时候,那么就不需要在项目中添加额外的库。比如我们想更改组件中的表单按钮状态的时候,我们更多的是优先考虑state方法或者useState钩子。

    7. 组件没以大写字母开头命名

    在JSX中,以小写开头的组件会向下编译为HTML元素

    所以我们应该避免下面的写法:

    class demoComponentName extends React.Component {
    }

    这将导致一个错误:如果你想渲染React组件,则需要以大写字母开头。

    那么得采取下面这种写法:

    class DemoComponentName extends React.Component {
    }

    后话

    上面的内容提取自Top 10 mistakes to avoid when using React,采用了意译的方式,提取了7条比较实用的内容。

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


    收起阅读 »

    90 行代码的webpack,你确定不学吗?

    在前端社区里,webpack 可以说是一个经久不衰的话题。其强大、灵活的功能曾极大地促进了前端工程化进程的发展,伴随了无数前端项目的起与落。其纷繁复杂的配置也曾让无数前端人望而却步,笑称需要一个新工种"webpack 配置工程师"。作为一个历史悠久,最常见、最...
    继续阅读 »
    在前端社区里,webpack 可以说是一个经久不衰的话题。其强大、灵活的功能曾极大地促进了前端工程化进程的发展,伴随了无数前端项目的起与落。其纷繁复杂的配置也曾让无数前端人望而却步,笑称需要一个新工种"webpack 配置工程师"。作为一个历史悠久,最常见、最经典的打包工具,webpack 极具讨论价值。理解 webpack,掌握 webpack,无论是在面试环节,还是在日常项目搭建、开发、优化环节,都能带来不少的收益。那么本文将从核心理念出发,带各位读者拨开 webpack 的外衣,看透其本质。

    究竟是啥


    其实这个问题在 webpack 官网的第一段就给出了明确的定义:



    At its core, webpack is a static module bundler for modern JavaScript applications. When webpack processes your application, it internally builds a dependency graph which maps every module your project needs and generates one or more bundles.



    其意为:



    webpack 的核心是用于现代 JavaScript 应用程序的静态模块打包器。 当 webpack 处理您的应用程序时,它会在内部构建一个依赖关系图,该图映射您项目所需的每个模块并生成一个或多个包



    要素察觉:静态模块打包器依赖关系图生成一个或多个包。虽然如今的前端项目中,webpack 扮演着重要的角色,囊括了诸多功能,但从其本质上来讲,其仍然是一个“模块打包器”,将开发者的 JavaScript 模块打包成一个或多个 JavaScript 文件。


    要干什么


    那么,为什么需要一个模块打包器呢?webpack 仓库早年的 README 也给出了答案:



    As developer you want to reuse existing code. As with node.js and web all file are already in the same language, but it is extra work to use your code with the node.js module system and the browser. The goal of webpack is to bundle CommonJs modules into javascript files which can be loaded by <script>-tags.



    可以看到,node.js 生态中积累了大量的 JavaScript 写的代码,却因为 node.js 端遵循的 CommonJS 模块化规范与浏览器端格格不入,导致代码无法得到复用,这是一个巨大的损失。于是 webpack 要做的就是将这些模块打包成可以在浏览器端使用 <script> 标签加载并运行的JavaScript 文件。


    或许这并不是唯一解释 webpack 存在的原因,但足以给我们很大的启发——把 CommonJS 规范的代码转换成可在浏览器运行的 JavaScript 代码


    怎么干的


    既然浏览器端没有 CommonJS 规范,那就实现一个好了。从 webpack 打包出的产物,我们能看出思路。


    新建三个文件观察其打包产物:


    src/index.js

    const printA = require('./a')
    printA()

    src/a.js

    const printB = require('./b')

    module.exports = function printA() {
    console.log('module a!')
    printB()
    }

    src/b.js

    module.exports = function printB() {
    console.log('module b!')
    }

    执行 npx webpack --mode development 打包产出 dist/main.js 文件


    上图中,使用了 webpack 打包 3 个简单的 js 文件 index.js/a.js/b.js, 其中 index.js 中依赖了 a.js, 而 a.js 中又依赖了 b.js, 形成一个完整依赖关系。


    那么,webpack 又是如何知道文件之间的依赖关系的呢,如何收集被依赖的文件保证不遗漏呢?我们依然能从官方文档找到答案:



    When webpack processes your application, it starts from a list of modules defined on the command line or in its configuration file. Starting from these entry points, webpack recursively builds a dependency graph that includes every module your application needs, then bundles all of those modules into a small number of bundles - often, just one - to be loaded by the browser.



    也就是说,webpack 会从配置的入口开始,递归的构建一个应用程序所需要的模块的依赖树。我们知道,CommonJS 规范里,依赖某一个文件时,只需要使用 require 关键字将其引入即可,那么只要我们遇到require关键字,就去解析这个依赖,而这个依赖中可能又使用了 require 关键字继续引用另一个依赖,于是,就可以递归的根据 require 关键字找到所有的被依赖的文件,从而完成依赖树的构建了。


    可以看到上图最终输出里,三个文件被以键值对的形式保存到 __webpack_modules__ 对象上, 对象的 key 为模块路径名,value 为一个被包装过的模块函数。函数拥有 module, module.exports, __webpack_require__ 三个参数。这使得每个模块都拥有使用 module.exports 导出本模块和使用 __webpack_require__ 引入其他模块的能力,同时保证了每个模块都处于一个隔离的函数作用域范围。


    为什么 webpack要修改require关键字和require的路径?我们知道requirenode环境自带的环境变量,可以直接使用,而在其他环境则没有这样一个变量,于是需要webpack提供这样的能力。只要提供了相似的能力,变量名叫 require还是 __webpack_require__其实无所谓。至于重写路径,当然是因为在node端系统会根据文件的路径加载,而在 webpack打包的文件中,使用原路径行不通,于是需要将路径重写为 __webpack_modules__ 的键,从而找到相应模块。


    而下面的 __webpack_require__函数与 __webpack_module_cache__ 对象则完成了模块加载的职责。使用 __webpack_require__ 函数加载完成的模块被缓存到 __webpack_module_cache__ 对象上,以便下次如果有其他模块依赖此模块时,不需要重新运行模块的包装函数,减少执行效率的消耗。同时,如果多个文件之间存在循环依赖,比如 a.js 依赖了 b.js 文件, b.js 又依赖了 a.js,那么在 b.js 使用 __webpack_require__加载 a.js 时,会直接走进 if(cachedModule !== undefined) 分支然后 return已缓存过的 a.js 的引用,不会进一步执行 a.js 文件加载,从而避免了循环依赖无限递归的出现


    不能说这个由 webpack 实现的模块加载器与 CommonJS 规范一毛一样,只能说八九不离十吧。这样一来,打包后的 JavaScript 文件可以被 <script> 标签加载且运行在浏览器端了。

    简易实现


    了解了 webpack 处理后的 JavaScript 长成什么样子,我们梳理一下思路,依葫芦画瓢手动实现一个简易的打包器,帮助理解。


    要做的事情有这么些:



    1. 读取入口文件,并收集依赖信息

    2. 递归地读取所有依赖模块,产出完整的依赖列表

    3. 将各模块内容打包成一块完整的可运行代码


    话不多说,创建一个项目,并安装所需依赖

    npm init -y
    npm i @babel/core @babel/parser @babel/traverse webpack webpack-cli -D

    其中:



    • @babel/parser 用于解析源代码,产出 AST

    • @babel/traverse 用于遍历 AST,找到 require 语句并修改成 _require_,将引入路径改造为相对根的路径

    • @babel/core 用于将修改后的 AST 转换成新的代码输出


    创建一个入口文件 myPack.js 并引入依赖

    const fs = require('fs')
    const path = require('path')
    const parser = require('@babel/parser')
    const traverse = require('@babel/traverse').default
    const babel = require('@babel/core')

    紧接着,我们需要对某一个模块进行解析,并产出其模块信息,包括:模块路径、模块依赖、模块转换后代码

    // 保存根路径,所有模块根据根路径产出相对路径
    let root = process.cwd()

    function readModuleInfo(filePath) {
    // 准备好相对路径作为 module 的 key
    filePath =
    './' + path.relative(root, path.resolve(filePath)).replace(/\\+/g, '/')
    // 读取源码
    const content = fs.readFileSync(filePath, 'utf-8')
    // 转换出 AST
    const ast = parser.parse(content)
    // 遍历模块 AST,将依赖收集到 deps 数组中
    const deps = []
    traverse(ast, {
    CallExpression: ({ node }) => {
    // 如果是 require 语句,则收集依赖
    if (node.callee.name === 'require') {
    // 改造 require 关键字
    node.callee.name = '_require_'
    let moduleName = node.arguments[0].value
    moduleName += path.extname(moduleName) ? '' : '.js'
    moduleName = path.join(path.dirname(filePath), moduleName)
    moduleName = './' + path.relative(root, moduleName).replace(/\\+/g, '/')
    deps.push(moduleName)
    // 改造依赖的路径
    node.arguments[0].value = moduleName
    }
    },
    })
    // 编译回代码
    const { code } = babel.transformFromAstSync(ast)
    return {
    filePath,
    deps,
    code,
    }
    }

    接下来,我们从入口出发递归地找到所有被依赖的模块,并构建成依赖树

    function buildDependencyGraph(entry) {
    // 获取入口模块信息
    const entryInfo = readModuleInfo(entry)
    // 项目依赖树
    const graphArr = []
    graphArr.push(entryInfo)
    // 从入口模块触发,递归地找每个模块的依赖,并将每个模块信息保存到 graphArr
    for (const module of graphArr) {
    module.deps.forEach((depPath) => {
    const moduleInfo = readModuleInfo(path.resolve(depPath))
    graphArr.push(moduleInfo)
    })
    }
    return graphArr
    }

    经过上面一步,我们已经得到依赖树能够描述整个应用的依赖情况,最后我们只需要按照目标格式进行打包输出即可

    function pack(graph, entry) {
    const moduleArr = graph.map((module) => {
    return (
    `"${module.filePath}": function(module, exports, _require_) {
    eval(\`` +
    module.code +
    `\`)
    }`
    )
    })
    const output = `;(() => {
    var modules = {
    ${moduleArr.join(',\n')}
    }
    var modules_cache = {}
    var _require_ = function(moduleId) {
    if (modules_cache[moduleId]) return modules_cache[moduleId].exports

    var module = modules_cache[moduleId] = {
    exports: {}
    }
    modules[moduleId](module, module.exports, _require_)
    return module.exports
    }

    _require_('${entry}')
    })()`
    return output
    }

    直接使用字符串模板拼接成类 CommonJS 规范的模板,自动加载入口模块,并使用 IIFE 将代码包装,保证代码模块不会影响到全局作用域。

    最后,编写一个入口函数 main 用以启动打包过程

    function main(entry = './src/index.js', output = './dist.js') {
    fs.writeFileSync(output, pack(buildDependencyGraph(entry), entry))
    }

    main()

    执行并验证结果

    node myPack.js

    至此,我们使用了总共不到 90 行代码(包含注释),完成了一个极简的模块打包工具。虽然没有涉及任何 Webpack 源码, 但我们从打包器的设计原理入手,走过了打包工具的核心步骤,简易却不失完整。

    总结


    本文从 webpack 的设计理念和最终实现出发,梳理了其作为一个打包工具的核心能力,并使用一个简易版本实现帮助更直观的理解其本质。总的来说,webpack 作为打包工具无非是从应用入口出发,递归的找到所有依赖模块,并将他们解析输出成一个具备类 CommonJS 模块化规范的模块加载能力的 JavaScript 文件


    因其优秀的设计,在实际生产环节中,webapck 还能扩展出诸多强大的功能。然而其本质仍是模块打包器。不论是什么样的新特性或新能力,只要我们把握住打包工具的核心思想,任何问题终将迎刃而解。



    链接:https://juejin.cn/post/6963820624623960101

    收起阅读 »

    使用vue+element开发一个谷歌插件

    简单功能:点击浏览器右上角插件icon弹出小弹窗,点击设置弹出设置页,并替换背景颜色。开始1.本地创建文件夹testPlugin并新建manifest.json文件{ "name": "testPlugin", "description": "这是...
    继续阅读 »

    简单功能:点击浏览器右上角插件icon弹出小弹窗,点击设置弹出设置页,并替换背景颜色。

    开始
    • 1.本地创建文件夹testPlugin并新建manifest.json文件
    {
    "name": "testPlugin",
    "description": "这是一个测试用例",
    "version": "0.0.1",
    "manifest_version": 2
    }
    • 2.添加插件的小icon
      testPlugin下创建icons文件夹,可以放入一些不同尺寸的icon,测试可以偷懒都放一种尺寸的icon。修改manifest.json为:
    {
    "name": "testPlugin",
    "description": "这是一个测试用例",
    "version": "0.0.1",
    "manifest_version": 2,
    "icons": {
    "16": "icons/16.png",
    "48": "icons/16.png"
    }
    }

    这时候在扩展程序中加载已解压的程序(就是我们创建的文件夹),就可以看到雏形了:


    • 3.选择性地添加点击插件icon浏览器右上角弹出来的框
      manifest.json添加:
    "browser_action": {
    "default_title": "test plugin",
    "default_icon": "icons/16.png",
    "default_popup": "index.html"
    }

    testPlugin创建index.html文件:

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>test plugin</title>
    </head>

    <body>
    <input id="name" placeholder="请输入"/>
    </body>
    </html>

    刷新插件,这时候点击浏览器中刚刚添加的插件的icon,就会弹出:


    • 4.js事件(样式同理)
      testPlugin创建js文件夹index.js文件:
    document.getElementById('button').onclick = function() {
    alert(document.getElementById('name').value)
    }

    html中:

    <input id="name" placeholder="请输入"/>
    <input id="button" type="button" value="点击"/>
    <script src="js/index.js"></script>

    刷新插件


    一个嵌入网页中的悬浮框
    上述例子是点击icon浏览器右上角出现的小弹窗,

    引入vue.js、element-ui
        下载合适版本的vue.js和element-ui等插件,同样按照index.js一样的操作引入,如果没有下载单独js文件的地址,可以打开cdn地址直接将压缩后的代码复制。
    manifest.json中添加:

    "content_scripts": [
    {
    "matches": [
    "<all_urls>"
    ],
    "css": [
    "css/index.css"
    ],
    "js": [
    "js/vue.js",
    "js/element.js",
    "js/index.js"
    ],
    "run_at": "document_idle"
    }
    ],

    在index.js文件:
    这里使用在head里插入link 的方式引入element-ui的css,减少插件包的一点大小,当然也可以像引入index.js那样在manifest.json中引入。
    直接在index.js文件中写Vue实例,不过首先得创建挂载实例的节点:

    let element = document.createElement('div')
    let attr = document.createAttribute('id')
    attr.value = 'appPlugin'
    element.setAttributeNode(attr)
    document.getElementsByTagName('body')[0].appendChild(element)

    let link = document.createElement('link')
    let linkAttr = document.createAttribute('rel')
    linkAttr.value = 'stylesheet'
    let linkHref = document.createAttribute('href')
    linkHref.value = 'https://unpkg.com/element-ui/lib/theme-chalk/index.css'
    link.setAttributeNode(linkAttr)
    link.setAttributeNode(linkHref)
    document.getElementsByTagName('head')[0].appendChild(link)


    const vue = new Vue({
    el: '#appPlugin',
    template:`
    <div class="app-plugin-content">{{text}}{{icon_post_message}}<el-button @click="Button">Button</el-button></div>
    `,
    data: function () {
    return { text: 'hhhhhh', icon_post_message: '_icon_post_message', isOcContentPopShow: true }
    },
    mounted() {
    console.log(this.text)
    },
    methods: {
    Button() {
    this.isOcContentPopShow = false
    }
    }
    })
    让我们来写一个简易替换网页背景颜色工具

    index.js:

    let element = document.createElement('div')
    let attr = document.createAttribute('id')
    attr.value = 'appPlugin'
    element.setAttributeNode(attr)
    document.getElementsByTagName('body')[0].appendChild(element)

    let link = document.createElement('link')
    let linkAttr = document.createAttribute('rel')
    linkAttr.value = 'stylesheet'
    let linkHref = document.createAttribute('href')
    linkHref.value = 'https://unpkg.com/element-ui/lib/theme-chalk/index.css'
    link.setAttributeNode(linkAttr)
    link.setAttributeNode(linkHref)
    document.getElementsByTagName('head')[0].appendChild(link)


    const vue = new Vue({
    el: '#appPlugin',
    template: `
    <div v-if="isOcContentPopShow" class="oc-move-page" id="oc_content_page">
    <div class="oc-content-title" id="oc_content_title">颜色 <el-button type="text" icon="el-icon-close" @click="close"></el-button></div>
    <div class="app-plugin-content">背景:<el-color-picker v-model="color1"></el-color-picker></div>
    <div class="app-plugin-content">字体:<el-color-picker v-model="color2"></el-color-picker></div>
    </div>
    `,
    data: function () {
    return { color1: null, color2: null, documentArr: [], textArr: [], isOcContentPopShow: true }
    },
    watch: {
    color1(val) {
    let out = document.getElementById('oc_content_page')
    let outC = document.getElementsByClassName('el-color-picker__panel')
    this.documentArr.forEach(item => {
    if(!out.contains(item) && !outC[0].contains(item) && !outC[1].contains(item)) {
    item.style.cssText = `background-color: ${val}!important;color: ${this.color2}!important;`
    }
    })
    },
    color2(val) {
    let out = document.getElementById('oc_content_page')
    let outC = document.getElementsByClassName('el-color-picker__panel')[1]
    this.textArr.forEach(item => {
    if(!out.contains(item) && !outC.contains(item)) {
    item.style.cssText = `color: ${val}!important;`
    }
    })
    }
    },
    mounted() {
    chrome.runtime.onConnect.addListener((res) => {
    if (res.name === 'testPlugin') {
    res.onMessage.addListener(mess => {
    this.isOcContentPopShow = mess.isShow
    })
    }
    })
    this.$nextTick(() => {
    let bodys = [...document.getElementsByTagName('body')]
    let headers = [...document.getElementsByTagName('header')]
    let divs = [...document.getElementsByTagName('div')]
    let lis = [...document.getElementsByTagName('li')]
    let articles = [...document.getElementsByTagName('article')]
    let asides = [...document.getElementsByTagName('aside')]
    let footers = [...document.getElementsByTagName('footer')]
    let navs = [...document.getElementsByTagName('nav')]
    this.documentArr = bodys.concat(headers, divs, lis, articles, asides, footers, navs)

    let as = [...document.getElementsByTagName('a')]
    let ps = [...document.getElementsByTagName('p')]
    this.textArr = as.concat(ps)

    })

    },
    methods: {
    close() {
    this.isOcContentPopShow = false
    }
    }
    })

    index.html:

    <!DOCTYPE html>
    <html lang="en">

    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>my plugin</title>
    <link rel="stylesheet" href="css/index.css">
    </head>

    <body>
    <div class="plugin">
    <input id="plugin_button" type="button" value="打开" />
    </div>
    </body>
    <script src="js/icon.js"></script>

    </html>

    新建icon.js:

    plugin_button.onclick = function () {
    mess()
    }
    async function mess () {
    const tabId = await getCurrentTabId()
    const connect = chrome.tabs.connect(tabId, {name: 'testPlugin'});
    connect.postMessage({isShow: true})
    }
    function getCurrentTabId() {
    return new Promise((resolve, reject) => {
    chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
    resolve(tabs.length ? tabs[0].id : null)
    });
    })
    }

    新建index.css:

    .oc-move-page{
    width: 100px;
    height: 200px;
    background: white;
    box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.12);
    border-radius: 8px;
    position: fixed;
    transform: translateY(-50%);
    right: 0;
    top: 50%;
    z-index: 1000001;
    }
    .oc-move-page .oc-content-title{
    text-align: left;
    padding: 12px 16px;
    font-weight: 600;
    font-size: 18px;
    border-bottom: 1px solid #DEE0E3;
    }
    .oc-move-page .app-plugin-content {
    display: flex;
    align-items: center;
    margin-top: 10px;
    }

    .el-color-picker__panel {
    right: 100px!important;
    left: auto!important;
    }


    这样一个小尝试就完成了,当然如果有更多需求可以结合本地存储或者服务端来协作。

    本文链接:https://blog.csdn.net/qq_26769677/article/details/116611072



    收起阅读 »

    手把手教你利用js给图片打马赛克

    效果演示Canvas简介这个 HTML 元素是为了客户端矢量图形而设计的。它自己没有行为,但却把一个绘图 API 展现给客户端 JavaScript 以使脚本能够把想绘制的东西都绘制到一块画布上。HTML5 标签用于绘制图像(通过脚本,通常是 JavaScri...
    继续阅读 »

    效果演示


    Canvas简介

    这个 HTML 元素是为了客户端矢量图形而设计的。它自己没有行为,但却把一个绘图 API 展现给客户端 JavaScript 以使脚本能够把想绘制的东西都绘制到一块画布上。

    HTML5 标签用于绘制图像(通过脚本,通常是 JavaScript)

    不过, 元素本身并没有绘制能力(它仅仅是图形的容器) - 您必须使用脚本来完成实际的绘图任务

    getContext() 方法可返回一个对象,该对象提供了用于在画布上绘图的方法和属性

    本手册提供完整的 getContext(“2d”) 对象属性和方法,可用于在画布上绘制文本、线条、矩形、圆形等等

    标记和 SVG 以及 VML 之间的差异:

    标记和 SVG 以及 VML 之间的一个重要的不同是, 有一个基于 JavaScript 的绘图 API,而 SVG 和 VML 使用一个 XML 文档来描述绘图。

    这两种方式在功能上是等同的,任何一种都可以用另一种来模拟。从表面上看,它们很不相同,可是,每一种都有强项和弱点。例如,SVG 绘图很容易编辑,只要从其描述中移除元素就行。

    要从同一图形的一个 标记中移除元素,往往需要擦掉绘图重新绘制它

    知识点简介

    • 利用js创建图片
    let img = new Image()
    //可以给图片一个链接
    img.src = 'https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=826495019,1749283937&fm=26&gp=0.jpg'
    //或者本地已有图片的路径
    //img.src = './download.jpg'

    //添加到HTML中
    document.body.appendChild(img)
    • canvas.getContext(“2d”)

    语法:
    参数 contextID 指定了您想要在画布上绘制的类型。当前唯一的合法值是 “2d”,它指定了二维绘图,并且导致这个方法返回一个环境对象,该对象导出一个二维绘图 API

    let ctx = Canvas.getContext(contextID)
    • ctx.drawImage()
    JavaScript 语法 1:
    在画布上定位图像:
    context.drawImage(img,x,y);
    JavaScript 语法 2:
    在画布上定位图像,并规定图像的宽度和高度:
    context.drawImage(img,x,y,width,height);
    JavaScript 语法 3:
    剪切图像,并在画布上定位被剪切的部分:
    context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height);
    • ctx.getImageData()
    JavaScript 语法
    getImageData() 方法返回 ImageData 对象,该对象拷贝了画布指定矩形的像素数据。
    对于 ImageData 对象中的每个像素,都存在着四方面的信息,即 RGBA 值:
    R - 红色 (0-255)
    G - 绿色 (0-255)
    B - 蓝色 (0-255)
    A - alpha 通道 (0-255; 0 是透明的,255 是完全可见的)
    color/alpha 以数组形式存在,并存储于 ImageData 对象的 data 属性中
    var imgData=context.getImageData(x,y,width,height);
    • ctx.putImageData()
    putImageData() 方法将图像数据(从指定的 ImageData 对象)放回画布上。

    那我们开始搞起来吧

    step-by-step

    准备好我们的图片,并添加上我们的方法

    <body>
    <img src="./download.jpg">
    <button onclick="addCanvas()">生成Canvas</button>
    <button onclick="generateImg()">生成图片</button>
    </body>


    接下来写addCanvas方法

    function addCanvas() {
    let bt = document.querySelector('button')

    let img = new Image(); //1.准备赋值复制一份图片
    img.src = './download.jpg';
    img.onload = function() { //2.待图片加载完成
    let width = this.width
    let height = this.height

    let canvas = document.createElement('canvas') //3.创建画布
    let ctx = canvas.getContext("2d"); //4.获得该画布的内容
    canvas.setAttribute('width', width) //5.为了统一,设置画布的宽高为图片的宽高
    canvas.setAttribute('height', height)

    ctx.drawImage(this, 0, 0, width, height); //5.在画布上绘制该图片

    document.body.insertBefore(canvas, bt) //5.把canvas插入到按钮前面

    }
    }



    嗯,我们已经成功走出了成功的一小步,接下来是干什么呢?…嗯,我们需要利用原生的onmouseup和onmousedown事件,代表我们按下鼠标这个过程,那么这两个事件添加到哪呢?

    没错,既然我们要在canvas上进行马赛克操作,那我们必然要给canvas元素添加这两个事件

    考虑到我们创建canvas的过程复杂了一点,我们做一个模块封装吧!

    function addCanvas() {
    let bt = document.querySelector('button')

    let img = new Image();
    img.src = './download.jpg'; //这里放自己的图片
    img.onload = function() {
    let width = this.width
    let height = this.height

    let {
    canvas,
    ctx
    } = createCanvasAndCtx(width, height) //对象解构接收canvas和ctx

    ctx.drawImage(this, 0, 0, width, height);

    document.body.insertBefore(canvas, bt)

    }
    }

    function createCanvasAndCtx(width, height) {
    let canvas = document.createElement('canvas')
    canvas.setAttribute('width', width)
    canvas.setAttribute('height', height)
    canvas.setAttribute('onmouseout', 'end()') //修补鼠标不在canvas上离开的补丁
    canvas.setAttribute('onmousedown', 'start()') //添加鼠标按下
    canvas.setAttribute('onmouseup', 'end()') //添加鼠标弹起
    let ctx = canvas.getContext("2d");
    return {
    canvas,
    ctx
    }
    }

    function start() {
    let canvas = document.querySelector('canvas')
    canvas.onmousemove = () => {
    console.log('你按下了并移动了鼠标')
    }
    }

    function end() {
    let canvas = document.querySelector('canvas')
    canvas.onmousemove = null
    }


    嗯,目前来看,我们的代码依然如我们所愿的正常工作

    接下来的挑战更加严峻,我们需要去获取像素和处理像素,让我们再重写start()函数

    function start() {
    let img = document.querySelector('img')
    let canvas = document.querySelector('canvas')
    let ctx = canvas.getContext("2d");
    imgData = ctx.getImageData(0, 0, img.clientWidth, img.clientHeight);
    canvas.onmousemove = (e) => {
    let w = imgData.width; //1.获取图片宽高
    let h = imgData.height;

    //马赛克的程度,数字越大越模糊
    let num = 10;

    //获取鼠标当前所在的像素RGBA
    let color = getXY(imgData, e.offsetX, e.offsetY);

    for (let k = 0; k < num; k++) {
    for (let l = 0; l < num; l++) {
    //设置imgData上坐标为(e.offsetX + l, e.offsetY + k)的的颜色
    setXY(imgData, e.offsetX + l, e.offsetY + k, color);
    }
    }
    //更新canvas数据
    ctx.putImageData(imgData, 0, 0);
    }
    }

    //这里为你提供了setXY和getXY两个函数,如果你有兴趣,可以去研究获取的原理
    function setXY(obj, x, y, color) {
    var w = obj.width;
    var h = obj.height;
    var d = obj.data;
    obj.data[4 * (y * w + x)] = color[0];
    obj.data[4 * (y * w + x) + 1] = color[1];
    obj.data[4 * (y * w + x) + 2] = color[2];
    obj.data[4 * (y * w + x) + 3] = color[3];
    }

    function getXY(obj, x, y) {
    var w = obj.width;
    var h = obj.height;
    var d = obj.data;
    var color = [];
    color[0] = obj.data[4 * (y * w + x)];
    color[1] = obj.data[4 * (y * w + x) + 1];
    color[2] = obj.data[4 * (y * w + x) + 2];
    color[3] = obj.data[4 * (y * w + x) + 3];
    return color;
    }

    嗯,我们离成功不远拉,最后一步就是生成图片

    好在canavs给我们提供了直接的方法,可以直接将画布导出为Base64编码的图片:

    function generateImg() {
    let canvas = document.querySelector('canvas')
    var newImg = new Image();
    newImg.src = canvas.toDataURL("image/png");
    document.body.insertBefore(newImg, canvas)
    }

    是不是无比轻松呢~,来看看你手写的代码是否和下面一样叭:

    完整代码

    <!DOCTYPE html>
    <html lang="en">

    <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    </head>

    <body>

    <body>
    <img src="./download.jpg">
    <button onclick="addCanvas()">生成Canvas</button>
    <button onclick="generateImg()">生成图片</button>
    </body>
    <script>
    function addCanvas() {
    let bt = document.querySelector('button')

    let img = new Image();
    img.src = './download.jpg'; //这里放自己的图片
    img.onload = function() {
    let width = this.width
    let height = this.height

    let {
    canvas,
    ctx
    } = createCanvasAndCtx(width, height)

    ctx.drawImage(this, 0, 0, width, height);

    document.body.insertBefore(canvas, bt)

    }
    }

    function createCanvasAndCtx(width, height) {
    let canvas = document.createElement('canvas')
    canvas.setAttribute('width', width)
    canvas.setAttribute('height', height)
    canvas.setAttribute('onmouseout', 'end()')
    canvas.setAttribute('onmousedown', 'start()')
    canvas.setAttribute('onmouseup', 'end()')
    let ctx = canvas.getContext("2d");
    return {
    canvas,
    ctx
    }
    }

    function start() {
    let img = document.querySelector('img')
    let canvas = document.querySelector('canvas')
    let ctx = canvas.getContext("2d");
    imgData = ctx.getImageData(0, 0, img.clientWidth, img.clientHeight);
    canvas.onmousemove = (e) => {
    let w = imgData.width; //1.获取图片宽高
    let h = imgData.height;

    //马赛克的程度,数字越大越模糊
    let num = 10;

    //获取鼠标当前所在的像素RGBA
    let color = getXY(imgData, e.offsetX, e.offsetY);

    for (let k = 0; k < num; k++) {
    for (let l = 0; l < num; l++) {
    //设置imgData上坐标为(e.offsetX + l, e.offsetY + k)的的颜色
    setXY(imgData, e.offsetX + l, e.offsetY + k, color);
    }
    }
    //更新canvas数据
    ctx.putImageData(imgData, 0, 0);
    }
    }

    function generateImg() {
    let canvas = document.querySelector('canvas')
    var newImg = new Image();
    newImg.src = canvas.toDataURL("image/png");
    document.body.insertBefore(newImg, canvas)
    }

    function setXY(obj, x, y, color) {
    var w = obj.width;
    var h = obj.height;
    var d = obj.data;
    obj.data[4 * (y * w + x)] = color[0];
    obj.data[4 * (y * w + x) + 1] = color[1];
    obj.data[4 * (y * w + x) + 2] = color[2];
    obj.data[4 * (y * w + x) + 3] = color[3];
    }

    function getXY(obj, x, y) {
    var w = obj.width;
    var h = obj.height;
    var d = obj.data;
    var color = [];
    color[0] = obj.data[4 * (y * w + x)];
    color[1] = obj.data[4 * (y * w + x) + 1];
    color[2] = obj.data[4 * (y * w + x) + 2];
    color[3] = obj.data[4 * (y * w + x) + 3];
    return color;
    }

    function end() {
    let canvas = document.querySelector('canvas')
    canvas.onmousemove = null
    }
    </script>
    </body>

    </html>

    本文链接:https://blog.csdn.net/JKR10000/article/details/116803023

    收起阅读 »

    微信H5网页跳转小程序,这一篇就够了!

    鉴于微信 开放标签说明文档 写的不是很清楚,大多数开发者看了以后表示:我从哪里来?要到哪里去?所以鄙人记录下这篇文章,以便帮助到一些人。静态网页跳转小程序废话不多说,上才艺!<html><head> <meta charse...
    继续阅读 »

    鉴于微信 开放标签说明文档 写的不是很清楚,大多数开发者看了以后表示:我从哪里来?要到哪里去?

    所以鄙人记录下这篇文章,以便帮助到一些人。

    静态网页跳转小程序

    废话不多说,上才艺!

    <html>
    <head>
    <meta charset="utf-8">
    <meta name = "viewport" content = "width = device-width, initial-scale = 1.0, maximum-scale = 1.0, user-scalable = 0" />
    <title>小程序跳转测试</title>
    </head>
    <body style="text-aligin:center;">
    <wx-open-launch-weapp
    id="launch-btn"
    username="gh_e16de8f****" <!-- 这里填写小程序的原始ID -->
    path="/pages/index/index.html"> <!-- 这里填写跳转对于小程序的页面 注意这里的 .html -->
    <template>
    <style>.btn { padding: 12px width:200px;height:50px;}</style>
    <button class="btn">打开小程序</button>
    </template>
    </wx-open-launch-weapp>

    <script src="/js/jquery-1.12.4.js"></script>
    <script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script> <!-- 至少必须是1.6版本 -->

    <script>

    $(function () {

    //=== 这里仅仅是获取 config 的参数以及签名=== start
    var url = location.href;
    var functions = "updateAppMessageShareData";
    $.get("https://xxx.com/wechat/jssdk/config", {"functions":functions}, function(response){
    if(response.status == 0) {
    var info = response.data;
    wx.config({
    debug: false,
    appId: info.appId,
    timestamp: info.timestamp,
    nonceStr: info.nonceStr,
    signature: info.signature,
    jsApiList: info.jsApiList,
    openTagList: ['wx-open-launch-weapp']//这里直接添加,什么都不用管
    });
    }
    });
    //=== 获取 config 的参数以及签名=== end

    var btn = document.getElementById('launch-btn');
    btn.addEventListener('launch', function (e) {
    console.log('success');
    });
    btn.addEventListener('error', function (e) {
    console.log('fail', e.detail);
    });
    });
    </script>
    </body>
    </html>

    开放对象:

    1、已认证的服务号,服务号绑定“JS接口安全域名”下的网页可使用此标签跳转任意合法合规的小程序。

    2、已认证的非个人主体的小程序,使用小程序云开发的静态网站托管绑定的域名下的网页,可以使用此标签跳转任意合法合规的小程序。

    客户端要求

    微信版本要求为:7.0.12及以上。 系统版本要求为:iOS 10.3及以上、Android 5.0及以上。

    注意:微信开发者工具暂时不支持!所以建议直接使用手机访问进行测试。

    其他说明

    这个功能其实很简单,并没有想象中那么复杂。 实质是在你能够做到自定义分享到朋友圈或朋友的基础上,config多了

    openTagList: ['wx-open-launch-weapp']

    再者需要注意的是,path的页面url 必须带有 .html 带参数的话则参数跟在html的后面。

    <wx-open-launch-weapp
    id="launch-btn"
    username="gh_e16de8f****" <!-- 这里填写小程序的原始ID -->
    path="/pages/index/index.html">

    <wx-open-launch-weapp
    id="launch-btn"
    username="gh_e16de8f****" <!-- 这里填写小程序的原始ID -->
    path="/pages/index/index.html?id=123">

    VUE项目H5跳转

    1、先请求接口配置微信需要的一些参数

    // 需要先请求后端接口 
    let url = window.location.href.split("#")[0];
    let shareConfig = await shareViewAPI.getWechatConfig({url});
    let _this = this;
    // 将接口返回的信息配置
    wx.config({
    debug: false,
    appId: _this.app_id, // 必填,公众号的唯一标识
    timestamp: shareConfig.timestamp, // 必填,生成签名的时间戳
    nonceStr: shareConfig.nonceStr, // 必填,生成签名的随机串
    signature: shareConfig.signature, // 必填,签名
    jsApiList: ["onMenuShareAppMessage"], // 必填,如果只是为了跳转小程序,随便填个值都行
    openTagList: ["wx-open-launch-weapp"] // 跳转小程序时必填
    });

    配置的方法需要放到created、mounted或者beforeRouteEnter里

    2、在页面中添加wx-open-launch-weapp标签

    <!-- 关于username 与 path的值 参考官方文档  -->
    <wx-open-launch-weapp
    id="launch-btn"
    username="gh_***"
    path="/pages/index/index.html"
    @error="handleErrorFn"
    @launch="handleLaunchFn"
    >
    <!-- vue中需要使用script标签代替template插槽 html中使用template -->
    <script type="text/wxtag-template">
    <p class="store-tool_tip">点击进入选基工具</p>
    </script>
    </wx-open-launch-weapp>
    methods: {
    handleLaunchFn(e) {
    console.log("success", e);
    },
    handleErrorFn(e) {
    console.log("fail", e.detail);
    }
    }

    3、好啦

    备注:
    使用该标签的时候可能会报错,在main.js文件中添加上该行代码即可

    // 忽略打开微信小程序的组件
    Vue.config.ignoredElements = ['wx-open-launch-weapp']


    收起阅读 »

    在vue项目中使用骨架屏

    vue
    现在的应用开发,基本上都是前后端分离的,前端主流框架有SPA、MPA等,那么解决页面渲染、白屏时间成为首要关注的点webpack可以按需加载,减小首屏需要加载代码的体积;使用CDN技术、静态代码等缓存技术,可以减小加载渲染的时长问题:但是首页依然存在加载、渲染...
    继续阅读 »

    现在的应用开发,基本上都是前后端分离的,前端主流框架有SPA、MPA等,那么解决页面渲染、白屏时间成为首要关注的点

    webpack可以按需加载,减小首屏需要加载代码的体积;

    使用CDN技术、静态代码等缓存技术,可以减小加载渲染的时长

    问题:但是首页依然存在加载、渲染等待时长的问题。那么如何从视觉效果上减小首屏白屏的时间呢?

    骨架屏:举个例子:其实就是在模版文件中id=app容器下面写想要展示的效果,在new Vue(option)之后,该id下的内容就被替换了( 这时候,可能Vue编译生成的内容还没有挂载。因为new Vue的时候会进行一系列的初始化,这也需要耗费时间的)。这样就可以从视觉上减小白屏的时间

    骨架屏的实现方式

    1、直接在模版文件id=app容器下面,写进想要展示的效果html

    2、直接在模板文件id=app容器下面,用图片展示

    3、使用vue ssr提供的webpack插件

    4、自动生成并且自动插入静态骨架屏

    方式1和方式2存在的缺陷:针对不同入口,展示的效果都一样,导致不能灵活的针对不同的入口,展示不同的样式

    方式3可以针对不同的入口展示不同的效果。(实质也是先通过ssr生成一个json文件,然后将json文件内容注入到模板文件的id=app容器下)

    方案一、直接在模版文件id=app容器下面,写进想要展示的效果html

    在根目录的模版文件内写进内容,如红色圈出来的地方


    在浏览器打开项目

    在调用new Vue之前的展示效果(只是做了个简单效果,不喜勿喷):


    可以看到elements中id=app的容器下内容,就是我们写进的骨架屏效果内容


    在看下调了new Vue之后的效果,id=app容器下的内容被vue编译生成的内容替换了



    方案二、直接在模板文件id=app容器下面,用图片展示(这个就不做展示了)

    方案三、使用vue ssr提供的webpack插件:即用.vue文件完成骨架屏

    在方案一的基础上,将骨架屏的代码抽离出来,不在模版文件里面书写代码,而是在vue文件里面书写效果代码,这样便于维护

    1、在根目录下建一个skeleton文件夹,在该目录下创建文件App.vue文件(根组件,类似Vue项目的App.vue)、home.skeleton.vue(首页骨架屏展示效果的代码,类似Vue项目写的路由页面)、skeleton-entry.js(入口文件类似Vue项目的入口文件)、plugin/server-plugin.js(vue-server-renderer包提供了server-plugin插件,从里面将代码拷贝出来)

    home.skeleton.vue(首页骨架屏展示效果的代码)

    <template>
    <div class="skeleton-home">
    <div>加载中...</div>
    </div>
    </template>

    <style>
    .skeleton-home {
    width: 100vw;
    height: 100vh;
    background-color: #eaeaea;
    }
    </style>

    App.vue(根组件)

    <template>
    <div id="app">
    <!-- 根组件 -->
    <home style="display:none" id="homeSkeleton"></home>
    </div>
    </template>
    <script>
    import home from './home.skeleton.vue'
    export default{
    components: {
    home
    }
    }
    </script>
    <style>
    #app {
    font-family: 'Avenir', Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-align: center;
    color: #2c3e50;
    }
    *{
    padding: 0;
    margin: 0;
    }
    </style>

    skeleton-entry.js(入口文件)

    // 入口文件
    import Vue from 'vue'
    import App from './App.vue'
    let skeleton = new Vue({
    render(h) {
    return h(App)
    }
    })
    export default skeleton

    plugin/server-plugin.js(vue-server-renderer包提供了server-plugin插件)

    'use strict';

    /* */

    var isJS = function (file) { return /\.js(\?[^.]+)?$/.test(file); };

    var ref = require('chalk');
    var red = ref.red;
    var yellow = ref.yellow;

    var prefix = "[vue-server-renderer-webpack-plugin]";
    var warn = exports.warn = function (msg) { return console.error(red((prefix + " " + msg + "\n"))); };
    var tip = exports.tip = function (msg) { return console.log(yellow((prefix + " " + msg + "\n"))); };

    var validate = function (compiler) {
    if (compiler.options.target !== 'node') {
    warn('webpack config `target` should be "node".');
    }

    if (compiler.options.output && compiler.options.output.libraryTarget !== 'commonjs2') {
    warn('webpack config `output.libraryTarget` should be "commonjs2".');
    }

    if (!compiler.options.externals) {
    tip(
    'It is recommended to externalize dependencies in the server build for ' +
    'better build performance.'
    );
    }
    };

    var VueSSRServerPlugin = function VueSSRServerPlugin (options) {
    if ( options === void 0 ) options = {};

    this.options = Object.assign({
    filename: 'vue-ssr-server-bundle.json'
    }, options);
    };

    VueSSRServerPlugin.prototype.apply = function apply (compiler) {
    var this$1 = this;

    validate(compiler);

    compiler.plugin('emit', function (compilation, cb) {
    var stats = compilation.getStats().toJson();
    var entryName = Object.keys(stats.entrypoints)[0];
    var entryAssets = stats.entrypoints[entryName].assets.filter(isJS);

    if (entryAssets.length > 1) {
    throw new Error(
    "Server-side bundle should have one single entry file. " +
    "Avoid using CommonsChunkPlugin in the server config."
    )
    }

    var entry = entryAssets[0];
    if (!entry || typeof entry !== 'string') {
    throw new Error(
    ("Entry \"" + entryName + "\" not found. Did you specify the correct entry option?")
    )
    }

    var bundle = {
    entry: entry,
    files: {},
    maps: {}
    };

    stats.assets.forEach(function (asset) {
    if (asset.name.match(/\.js$/)) {
    bundle.files[asset.name] = compilation.assets[asset.name].source();
    } else if (asset.name.match(/\.js\.map$/)) {
    bundle.maps[asset.name.replace(/\.map$/, '')] = JSON.parse(compilation.assets[asset.name].source());
    }
    // do not emit anything else for server
    delete compilation.assets[asset.name];
    });

    var json = JSON.stringify(bundle, null, 2);
    var filename = this$1.options.filename;

    compilation.assets[filename] = {
    source: function () { return json; },
    size: function () { return json.length; }
    };

    cb();
    });
    };

    module.exports = VueSSRServerPlugin;

    2、新建一个骨架屏构建配置文件:build/webpack.skeleton.conf.js,这个文件配合vue-server-renderer插件,将App.vue内容构建成单个json格式的文件

    'use strict'

    const path = require('path')
    const nodeExternals = require('webpack-node-externals')
    const VueSSRServerPlugin = require('../skeleton/plugin/server-plugin')

    module.exports = {
    // 这允许 webpack 以 Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import),
    // 并且还会在编译 Vue 组件时,
    // 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。
    target: 'node',

    // 对 bundle renderer 提供 source map 支持
    devtool: 'source-map',

    // 将 entry 指向应用程序的 server entry 文件
    entry: path.resolve(__dirname, '../skeleton/skeleton-entry.js'),

    output: {
    path: path.resolve(__dirname, '../skeleton'), // 生成的文件的目录
    publicPath: '/skeleton/',
    filename: '[name].js',
    libraryTarget: 'commonjs2' // 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports)
    },

    module: {
    rules: [
    {
    test: /\.vue$/,
    loader: 'vue-loader',
    options: {
    compilerOptions: {
    preserveWhitespace: false
    }
    }
    },
    {
    test: /\.css$/,
    use: ['vue-style-loader', 'css-loader']
    }
    ]
    },

    performance: {
    hints: false
    },

    // https://webpack.js.org/configuration/externals/#function
    // https://github.com/liady/webpack-node-externals
    // 外置化应用程序依赖模块。可以使服务器构建速度更快,
    // 并生成较小的 bundle 文件。
    externals: nodeExternals({
    // 不要外置化 webpack 需要处理的依赖模块。
    // 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
    // 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单
    allowlist: /\.css$/
    }),

    // 这是将服务器的整个输出
    // 构建为单个 JSON 文件的插件。
    // 不配置filename,则默认文件名为 `vue-ssr-server-bundle.json`
    plugins: [
    new VueSSRServerPlugin({
    filename: 'skeleton.json'
    })
    ]
    }

    3、使用webpack-cli运行文件webpack.skeleton.conf.js,生成skeleton.json文件,放置在文件夹skeleton下

    在package.json文件里面书写运行命令:create-skeleton

    "scripts": {
    "create-skeleton": "webpack --progress --config build/webpack.skeleton.conf.js",
    "fill-skeleton": "node ./skeleton/skeleton.js"
    }

    在控制台上运行命令:

    npm run create-skeleton

    文件夹skeleton下就会多出skelleton.json文件


    4、将生成的skeleton.json内容注入到根目录下的index.html(模版文件)

    1)在文件夹skeleton下新建skeleton.js

    // 将生成的skeleton.json的内容填充到模板文件中
    const fs = require('fs')
    const { resolve } = require('path')
    const createBundleRenderer = require('vue-server-renderer').createBundleRenderer

    // 读取skeleton.json,以skeleton/index.html为模版写入内容
    const renderer = createBundleRenderer(resolve(__dirname, '../skeleton/skeleton.json'), {
    template: fs.readFileSync(resolve(__dirname, '../skeleton/index.html'), 'utf-8')
    })
    // 把上一步模版完成的内容写入根目录下的模版文件'index.html'
    renderer.renderToString({}, (err, html) => {
    if (err) {
    return console.log(err)
    }
    console.log('render complete!')
    fs.writeFileSync('index.html', html, 'utf-8')
    })

    2)添加运行命令:fill-skeleton

    "fill-skeleton": "node ./skeleton/skeleton.js"

    3)在控制台上运行该命令,则skeleton.json文件内容被填充至根目录下的模板文件index.html了

    本文链接:https://blog.csdn.net/tangxiujiang/article/details/116832585

    收起阅读 »

    520和女朋友搞点不一样的礼物, html+css+js做一个网页版坦克大战游戏

    坦克游戏玩法及介绍我们先来看一下首页。打开这个首页很简单,基本是上面这个样子,然后选择两个人回车就可以进行玩耍了,这个游戏需要两个人一起操作,玩家1(我): 使用WASD四个键进行上左下右方向的控制,通过space键进行设计射击。玩家2(女朋友):通过方向键上...
    继续阅读 »

    坦克游戏玩法及介绍

    我们先来看一下首页。


    打开这个首页很简单,基本是上面这个样子,然后选择两个人回车就可以进行玩耍了,这个游戏需要两个人一起操作,玩家1(我): 使用WASD四个键进行上左下右方向的控制,通过space键进行设计射击。玩家2(女朋友):通过方向键上下左右控制方向,通过enter键盘射击。基本上我控制整个电脑键盘的左边,她控制电脑键盘的右边。通过N键进行下一关,P键选择上一关。再键盘上显示如下。

    演示如何进入游戏


    通过方向键的下键选择两个人,然后点击回车进入游戏。也可以选择一个人进行回车进行战斗。

    一个人战斗的状态。


    两个人战斗的状态。


    比如图中红色标记的砖头是打不破的,只能绕道走,还有只能再yellow标记的区域内操作,其它砖头用子弹就可以打破,不能让对手先打破你的大本营(我右边中间的老鹰),不然又得GG.

    整个游戏规则大体是这样,下面看一下代码。

    项目结构


    整个项目由五部分组成,分为背景音乐、基础样式、动图、核心JS及首页静态展示。不涉及后端,纯前端实现。

    index.html

    <!DOCTYPE html>
    <html lang="zh" class="no-js demo-1">
    <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="js/jquery.min.js"></script>
    <script src="js/Helper.js"></script>
    <script src="js/keyboard.js"></script>
    <script src="js/const.js"></script>
    <script src="js/level.js"></script>
    <script src="js/crackAnimation.js"></script>
    <script src="js/prop.js"></script>
    <script src="js/bullet.js"></script>
    <script src="js/tank.js"></script>
    <script src="js/num.js"></script>
    <script src="js/menu.js"></script>
    <script src="js/map.js"></script>
    <script src="js/Collision.js"></script>
    <script src="js/stage.js"></script>
    <script src="js/main.js"></script>
    <link rel="stylesheet" type="text/css" href="css/default.css" />
    <style type="text/css">
    #canvasDiv canvas{
    position:absolute;
    }
    </style>
    </head>
    <body>
    <div class="container">
    <head><h3>操作说明:玩家1:WASD上左下右,space射击;玩家2:方向键,enter射击。n下一关,p上一关。</h3></head>
    <div class="main clearfix">
    <div id="canvasDiv" >
    <canvas id="wallCanvas" ></canvas>
    <canvas id="tankCanvas" ></canvas>
    <canvas id="grassCanvas" ></canvas>
    <canvas id="overCanvas" ></canvas>
    <canvas id="stageCanvas" ></canvas>
    </div>
    </div>

    </div><!-- /container -->
    <div style="text-align:center;">
    <p>来源:<a href="https://sunmenglei.blog.csdn.net/" target="_blank">孙叫兽的博客</a></p>
    </div>

    </body>
    </html>

    css

    *, *:after, *:before { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; }
    body, html { font-size: 100%; padding: 0; margin: 0; height: 100%;}

    /* Clearfix hack by Nicolas Gallagher: http://nicolasgallagher.com/micro-clearfix-hack/ */
    .clearfix:before, .clearfix:after { content: " "; display: table; }
    .clearfix:after { clear: both; }

    body {
    font-family: "Helvetica Neue",Helvetica,Arial,'Microsoft YaHei',sans-serif,'Lato', Calibri;
    color: #777;
    background: #f6f6f6;
    }

    a {
    color: #555;
    text-decoration: none;
    outline: none;
    }

    a:hover,
    a:active {
    color: #777;
    }

    a img {
    border: none;
    }
    /* Header Style */
    .main,
    .container > header {
    margin: 0 auto;
    /*padding: 2em;*/
    }

    .container {
    height: 100%;
    }

    .container > header {
    padding-top: 20px;
    padding-bottom: 20px;
    text-align: center;
    background: rgba(0,0,0,0.01);
    }

    .container > header h1 {
    font-size: 2.625em;
    line-height: 1.3;
    margin: 0;
    font-weight: 300;
    }

    .container > header span {
    display: block;
    font-size: 60%;
    opacity: 0.3;
    padding: 0 0 0.6em 0.1em;
    }

    /* Main Content */
    .main {
    /*max-width: 69em;*/
    width: 100%;
    height: 100%;
    overflow: hidden;
    }
    .demo-scroll{
    overflow-y: scroll;
    width: 100%;
    height: 100%;
    }
    .column {
    float: left;
    width: 50%;
    padding: 0 2em;
    min-height: 300px;
    position: relative;
    }

    .column:nth-child(2) {
    box-shadow: -1px 0 0 rgba(0,0,0,0.1);
    }

    .column p {
    font-weight: 300;
    font-size: 2em;
    padding: 0;
    margin: 0;
    text-align: right;
    line-height: 1.5;
    }

    /* To Navigation Style */
    .htmleaf-top {
    background: #fff;
    background: rgba(255, 255, 255, 0.6);
    text-transform: uppercase;
    width: 100%;
    font-size: 0.69em;
    line-height: 2.2;
    }

    .htmleaf-top a {
    padding: 0 1em;
    letter-spacing: 0.1em;
    color: #888;
    display: inline-block;
    }

    .htmleaf-top a:hover {
    background: rgba(255,255,255,0.95);
    color: #333;
    }

    .htmleaf-top span.right {
    float: right;
    }

    .htmleaf-top span.right a {
    float: left;
    display: block;
    }

    .htmleaf-icon:before {
    font-family: 'codropsicons';
    margin: 0 4px;
    speak: none;
    font-style: normal;
    font-weight: normal;
    font-variant: normal;
    text-transform: none;
    line-height: 1;
    -webkit-font-smoothing: antialiased;
    }



    /* Demo Buttons Style */
    .htmleaf-demos {
    padding-top: 1em;
    font-size: 0.9em;
    }

    .htmleaf-demos a {
    display: inline-block;
    margin: 0.2em;
    padding: 0.45em 1em;
    background: #999;
    color: #fff;
    font-weight: 700;
    border-radius: 2px;
    }

    .htmleaf-demos a:hover,
    .htmleaf-demos a.current-demo,
    .htmleaf-demos a.current-demo:hover {
    opacity: 0.6;
    }

    .htmleaf-nav {
    text-align: center;
    }

    .htmleaf-nav a {
    display: inline-block;
    margin: 20px auto;
    padding: 0.3em;
    }
    .bb-custom-wrapper {
    width: 420px;
    position: relative;
    margin: 0 auto 40px;
    text-align: center;
    }
    /* Demo Styles */

    .demo-1 body {
    color: #87968e;
    background: #fff2e3;
    }

    .demo-1 a {
    color: #72b890;
    }

    .demo-1 .htmleaf-demos a {
    background: #72b890;
    color: #fff;
    }

    .demo-2 body {
    color: #fff;
    background: #c05d8e;
    }

    .demo-2 a {
    color: #d38daf;
    }

    .demo-2 a:hover,
    .demo-2 a:active {
    color: #fff;
    }

    .demo-2 .htmleaf-demos a {
    background: #883b61;
    color: #fff;
    }

    .demo-2 .htmleaf-top a:hover {
    background: rgba(255,255,255,0.3);
    color: #333;
    }

    .demo-3 body {
    color: #87968e;
    background: #fff2e3;
    }

    .demo-3 a {
    color: #ea5381;
    }

    .demo-3 .htmleaf-demos a {
    background: #ea5381;
    color: #fff;
    }

    .demo-4 body {
    color: #999;
    background: #fff2e3;
    overflow: hidden;
    }

    .demo-4 a {
    color: #1baede;
    }

    .demo-4 a:hover,
    .demo-4 a:active {
    opacity: 0.6;
    }

    .demo-4 .htmleaf-demos a {
    background: #1baede;
    color: #fff;
    }

    .demo-5 body {
    background: #fffbd6;
    }
    /****/
    .related {
    /*margin-top: 5em;*/
    color: #fff;
    background: #333;
    text-align: center;
    font-size: 1.25em;
    padding: 3em 0;
    overflow: hidden;
    }

    .related a {
    display: inline-block;
    text-align: left;
    margin: 20px auto;
    padding: 10px 20px;
    opacity: 0.8;
    -webkit-transition: opacity 0.3s;
    transition: opacity 0.3s;
    -webkit-backface-visibility: hidden;
    }

    .related a:hover,
    .related a:active {
    opacity: 1;
    }

    .related a img {
    max-width: 100%;
    }

    .related a h3 {
    font-weight: 300;
    margin-top: 0.15em;
    color: #fff;
    }

    @media screen and (max-width: 40em) {

    .htmleaf-icon span {
    display: none;
    }

    .htmleaf-icon:before {
    font-size: 160%;
    line-height: 2;
    }

    }

    @media screen and (max-width: 46.0625em) {
    .column {
    width: 100%;
    min-width: auto;
    min-height: auto;
    padding: 1em;
    }

    .column p {
    text-align: left;
    font-size: 1.5em;
    }

    .column:nth-child(2) {
    box-shadow: 0 -1px 0 rgba(0,0,0,0.1);
    }
    }

    @media screen and (max-width: 25em) {

    .htmleaf-icon span {
    display: none;
    }

    }

    核心js

    /**
    * 检测2个物体是否碰撞
    * @param object1 物体1
    * @param object2 物体2
    * @param overlap 允许重叠的大小
    * @returns {Boolean} 如果碰撞了,返回true
    */
    function CheckIntersect(object1, object2, overlap)
    {
    // x-轴 x-轴
    // A1------>B1 C1 A2------>B2 C2
    // +--------+ ^ +--------+ ^
    // | object1| | y-轴 | object2| | y-轴
    // | | | | | |
    // +--------+ D1 +--------+ D2
    //
    //overlap是重叠的区域值
    A1 = object1.x + overlap;
    B1 = object1.x + object1.size - overlap;
    C1 = object1.y + overlap;
    D1 = object1.y + object1.size - overlap;

    A2 = object2.x + overlap;
    B2 = object2.x + object2.size - overlap;
    C2 = object2.y + overlap;
    D2 = object2.y + object2.size - overlap;

    //假如他们在x-轴重叠
    if(A1 >= A2 && A1 <= B2
    || B1 >= A2 && B1 <= B2)
    {
    //判断y-轴重叠
    if(C1 >= C2 && C1 <= D2 || D1 >= C2 && D1 <= D2)
    {
    return true;
    }
    }
    return false;
    }

    /**
    * 坦克与地图块碰撞
    * @param tank 坦克对象
    * @param mapobj 地图对象
    * @returns {Boolean} 如果碰撞,返回true
    */
    function tankMapCollision(tank,mapobj){
    //移动检测,记录最后一次的移动方向,根据方向判断+-overlap,
    var tileNum = 0;//需要检测的tile数
    var rowIndex = 0;//map中的行索引
    var colIndex = 0;//map中的列索引
    var overlap = 3;//允许重叠的大小

    //根据tank的x、y计算出map中的row和col
    if(tank.dir == UP){
    rowIndex = parseInt((tank.tempY + overlap - mapobj.offsetY)/mapobj.tileSize);
    colIndex = parseInt((tank.tempX + overlap- mapobj.offsetX)/mapobj.tileSize);
    }else if(tank.dir == DOWN){
    //向下,即dir==1的时候,行索引的计算需要+tank.Height
    rowIndex = parseInt((tank.tempY - overlap - mapobj.offsetY + tank.size)/mapobj.tileSize);
    colIndex = parseInt((tank.tempX + overlap- mapobj.offsetX)/mapobj.tileSize);
    }else if(tank.dir == LEFT){
    rowIndex = parseInt((tank.tempY + overlap- mapobj.offsetY)/mapobj.tileSize);
    colIndex = parseInt((tank.tempX + overlap - mapobj.offsetX)/mapobj.tileSize);
    }else if(tank.dir == RIGHT){
    rowIndex = parseInt((tank.tempY + overlap- mapobj.offsetY)/mapobj.tileSize);
    //向右,即dir==3的时候,列索引的计算需要+tank.Height
    colIndex = parseInt((tank.tempX - overlap - mapobj.offsetX + tank.size)/mapobj.tileSize);
    }
    if(rowIndex >= mapobj.HTileCount || rowIndex < 0 || colIndex >= mapobj.wTileCount || colIndex < 0){
    return true;
    }
    if(tank.dir == UP || tank.dir == DOWN){
    var tempWidth = parseInt(tank.tempX - map.offsetX - (colIndex)*mapobj.tileSize + tank.size - overlap);//去除重叠部分
    if(tempWidth % mapobj.tileSize == 0 ){
    tileNum = parseInt(tempWidth/mapobj.tileSize);
    }else{
    tileNum = parseInt(tempWidth/mapobj.tileSize) + 1;
    }
    for(var i=0;i<tileNum && colIndex+i < mapobj.wTileCount ;i++){
    var mapContent = mapobj.mapLevel[rowIndex][colIndex+i];
    if(mapContent == WALL || mapContent == GRID || mapContent == WATER || mapContent == HOME || mapContent == ANOTHREHOME){
    if(tank.dir == UP){
    tank.y = mapobj.offsetY + rowIndex * mapobj.tileSize + mapobj.tileSize - overlap;
    }else if(tank.dir == DOWN){
    tank.y = mapobj.offsetY + rowIndex * mapobj.tileSize - tank.size + overlap;
    }
    return true;
    }
    }
    }else{
    var tempHeight = parseInt(tank.tempY - map.offsetY - (rowIndex)*mapobj.tileSize + tank.size - overlap);//去除重叠部分
    if(tempHeight % mapobj.tileSize == 0 ){
    tileNum = parseInt(tempHeight/mapobj.tileSize);
    }else{
    tileNum = parseInt(tempHeight/mapobj.tileSize) + 1;
    }
    for(var i=0;i<tileNum && rowIndex+i < mapobj.HTileCount;i++){
    var mapContent = mapobj.mapLevel[rowIndex+i][colIndex];
    if(mapContent == WALL || mapContent == GRID || mapContent == WATER || mapContent == HOME || mapContent == ANOTHREHOME){
    if(tank.dir == LEFT){
    tank.x = mapobj.offsetX + colIndex * mapobj.tileSize + mapobj.tileSize - overlap;
    }else if(tank.dir == RIGHT){
    tank.x = mapobj.offsetX + colIndex * mapobj.tileSize - tank.size + overlap;
    }
    return true;
    }
    }
    }
    return false;
    }

    /**
    * 子弹与地图块的碰撞
    * @param bullet 子弹对象
    * @param mapobj 地图对象
    */
    function bulletMapCollision(bullet,mapobj){
    var tileNum = 0;//需要检测的tile数
    var rowIndex = 0;//map中的行索引
    var colIndex = 0;//map中的列索引
    var mapChangeIndex = [];//map中需要更新的索引数组
    var result = false;//是否碰撞
    //根据bullet的x、y计算出map中的row和col
    if(bullet.dir == UP){
    rowIndex = parseInt((bullet.y - mapobj.offsetY)/mapobj.tileSize);
    colIndex = parseInt((bullet.x - mapobj.offsetX)/mapobj.tileSize);
    }else if(bullet.dir == DOWN){
    //向下,即dir==1的时候,行索引的计算需要+bullet.Height
    rowIndex = parseInt((bullet.y - mapobj.offsetY + bullet.size)/mapobj.tileSize);
    colIndex = parseInt((bullet.x - mapobj.offsetX)/mapobj.tileSize);
    }else if(bullet.dir == LEFT){
    rowIndex = parseInt((bullet.y - mapobj.offsetY)/mapobj.tileSize);
    colIndex = parseInt((bullet.x - mapobj.offsetX)/mapobj.tileSize);
    }else if(bullet.dir == RIGHT){
    rowIndex = parseInt((bullet.y - mapobj.offsetY)/mapobj.tileSize);
    //向右,即dir==3的时候,列索引的计算需要+bullet.Height
    colIndex = parseInt((bullet.x - mapobj.offsetX + bullet.size)/mapobj.tileSize);
    }
    if(rowIndex >= mapobj.HTileCount || rowIndex < 0 || colIndex >= mapobj.wTileCount || colIndex < 0){
    return true;
    }

    if(bullet.dir == UP || bullet.dir == DOWN){
    var tempWidth = parseInt(bullet.x - map.offsetX - (colIndex)*mapobj.tileSize + bullet.size);
    if(tempWidth % mapobj.tileSize == 0 ){
    tileNum = parseInt(tempWidth/mapobj.tileSize);
    }else{
    tileNum = parseInt(tempWidth/mapobj.tileSize) + 1;
    }
    for(var i=0;i<tileNum && colIndex+i < mapobj.wTileCount ;i++){
    var mapContent = mapobj.mapLevel[rowIndex][colIndex+i];
    if(mapContent == WALL || mapContent == GRID || mapContent == HOME || mapContent == ANOTHREHOME){
    //bullet.distroy();
    result = true;
    if(mapContent == WALL){
    //墙被打掉
    mapChangeIndex.push([rowIndex,colIndex+i]);
    }else if(mapContent == GRID){

    }else{
    isGameOver = true;
    break;
    }
    }
    }
    }else{
    var tempHeight = parseInt(bullet.y - map.offsetY - (rowIndex)*mapobj.tileSize + bullet.size);
    if(tempHeight % mapobj.tileSize == 0 ){
    tileNum = parseInt(tempHeight/mapobj.tileSize);
    }else{
    tileNum = parseInt(tempHeight/mapobj.tileSize) + 1;
    }
    for(var i=0;i<tileNum && rowIndex+i < mapobj.HTileCount;i++){
    var mapContent = mapobj.mapLevel[rowIndex+i][colIndex];
    if(mapContent == WALL || mapContent == GRID || mapContent == HOME || mapContent == ANOTHREHOME){
    //bullet.distroy();
    result = true;
    if(mapContent == WALL){
    //墙被打掉
    mapChangeIndex.push([rowIndex+i,colIndex]);
    }else if(mapContent == GRID){

    }else{
    isGameOver = true;
    break;
    }
    }
    }
    }
    //更新地图
    map.updateMap(mapChangeIndex,0);
    return result;
    }

    原文地址:https://blog.csdn.net/weixin_41937552/article/details/116559485


    收起阅读 »

    vue 事件总线EventBus的概念、使用以及注意点

    前言vue组件中的数据传递最最常见的就是父子组件之间的传递。父传子通过props向下传递数据给子组件;子传父通过$emit发送事件,并携带数据给父组件。而有时两个组件之间毫无关系,或者他们之间的结构复杂,如何传递数据呢?这时就要用到 vue 中的事件总线 Ev...
    继续阅读 »

    前言

    vue组件中的数据传递最最常见的就是父子组件之间的传递。父传子通过props向下传递数据给子组件;子传父通过$emit发送事件,并携带数据给父组件。而有时两个组件之间毫无关系,或者他们之间的结构复杂,如何传递数据呢?这时就要用到 vue 中的事件总线 EventBus的概念

    正文

    EventBus的简介

    EventBus又称事件总线,相当于一个全局的仓库,任何组件都可以去这个仓库里获取事件,如图:


    EventBus的使用

    废话不多说,直接开始使用EventBus

    一、初始化

    要用EventBus,首先要初始化一个EventBus,这里称它为全局事件总线。

    • 第一种初始化方法
    import Vue from 'vue'
    //因为是全局的一个'仓库',所以初始化要在全局初始化
    const EventBus = new Vue()
    • 第二种初始化方法(本文选用这种初始化方法)
    //在已经创建好的Vue实例原型中创建一个EventBus
    Vue.prototype.$EventBus = new Vue()

    二、向EventBus发送事件

    发送事件的语法:this.$EventBus.$emit(发送的事件名,传递的参数)

    已经创建好EventBus后我们就需要向它发送需要传递的事件,以便其他组件可以向EventBus获取。
    例子:有两个组件A和B需要通信,他们不是父子组件关系,B事件需要获得A事件里的一组数据data

    <!-- A.vue 这里是以模块化的方式讲解的,即A组件和B组件分别各自
    一个.vue文件,所以代码中会有导入的语法-->

    <template>
    <button @click="sendMsg">发送MsgA</button>
    </template>

    <script>
    export default {
    data(){
    return{
    MsgA: 'A组件中的Msg'
    }
    },
    methods: {
    sendMsg() {
    /*调用全局Vue实例中的$EventBus事件总线中的$emit属性,发送事
    件"aMsg",并携带A组件中的Msg*/
    this.$EventBus.$emit("aMsg", this.MsgA);
    }
    }
    };
    </script>

    三、接收事件

    接收事件的语法:this.$EventBus.$on(监听的事件名, 回调函数)

    A组件已经向全局事件总线EventBus发送了一个aMsg事件,这时B组件就可以去aMsg监听这个事件,并可以获得一些数据。

    <!-- B.vue -->

    <template>

    <!-- 展示msgB -->
    <p>{{msgB}}</p>

    </template>

    <script>
    export default {
    data(){
    return {
    //初始化一个msgB
    msgB: ''
    }
    },
    mounted() {
    /*调用全局Vue实例中的$EventBus事件总线中的$on属性,监听A组件发送
    到事件总线中的aMsg事件*/
    this.$EventBus.$on("aMsg", (data) => {
    //将A组件传递过来的参数data赋值给msgB
    this.msgB = data;
    });
    }
    };
    </script>

    B组件展示结果:A组件中的Msg


    这样,B组件就轻松接收到了A组件传递过来的参数,并成功展示了该参数,这样是不是就很简单的解决了各组件之间的通讯呢?虽然EventBus是一个很轻便的方法,任何数据都可以往里传,然后被别的组件获取,但是如果用不好,容易出现很严重的BUG,所以接下来我们就来讲解一下移除监听事件。

    四、移除监听事件

    在上一个例子中,我们A组件向事件总线发送了一个事件aMsg并传递了参数MsgA,然后B组件对该事件进行了监听,并获取了传递过来的参数。但是,这时如果我们离开B组件,然后再次进入B组件时,又会触发一次对事件aMsg的监听,这时时间总线里就有两个监听了,如果反复进入B组件多次,那么就会对aMsg进行多次的监听。

    总而言之,A组件只向EventBus发送了一次事件,但B组件却进行了多次监听,EventBus容器中有很多个一模一样的事件监听器这时就会出现,事件只触发一次,但监听事件中的回调函数执行了很多次

    • 解决办法:在组件离开,也就是被销毁前,将该监听事件给移除,以免下次再重复创建监听
    • 语法:this.$EventBus.$off(要移除监听的事件名)
    <!-- B.vue -->

    <template>

    <!-- 展示msgB -->
    <p>{{msgB}}</p>

    </template>

    <script>
    export default {
    data(){
    return {
    //初始化一个msgB
    msgB: ''
    }
    },
    mounted() {
    /*调用全局Vue实例中的$EventBus事件总线中的$on属性,监听A组件发送
    到事件总线中的aMsg事件*/
    this.$EventBus.$on("aMsg", (data) => {
    //将A组件传递过来的参数data赋值给msgB
    this.msgB = data;
    });
    },
    beforeDestroy(){
    //移除监听事件"aMsg"
    this.$EventBus.$off("aMsg")
    }
    };
    </script>

    结束语

    好了,对于vue中的事件总线的讲解就到这里了,这也是我今天在做项目时用到的一个小知识点,接下来附上一张我因为没有及时移除事件监听,导致我每重进组件一次就报错48条错误信息的图,希望大家在我的文章中能血啊都一些东西,并且不要再犯我的这种低级错误。


    本文链接:https://blog.csdn.net/l_ppp/article/details/105924658

    收起阅读 »