注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

腾讯面试官:兄弟,你说你会Webpack,那说说他的原理?

原理图解 1、首先肯定是要先解析入口文件entry,将其转为AST(抽象语法书),使用@babel/parser 2、然后使用@babel/traverse去找出入口文件所有依赖模块 3、然后使用@babel/core+@babel/preset-env将入...
继续阅读 »

image.png


原理图解



  • 1、首先肯定是要先解析入口文件entry,将其转为AST(抽象语法书),使用@babel/parser

  • 2、然后使用@babel/traverse去找出入口文件所有依赖模块

  • 3、然后使用@babel/core+@babel/preset-env将入口文件的AST转为Code

  • 4、将2中找到的入口文件的依赖模块,进行遍历递归,重复执行1,2,3

  • 5。重写require函数,并与4中生成的递归关系图一起,输出到bundle


截屏2021-07-21 上午7.39.26.png


代码实现


webpack具体实现原理是很复杂的,这里只是简单实现一下,让大家粗略了解一下,webpack是怎么运作的。在代码实现过程中,大家可以自己console.log一下,看看ast,dependcies,code这些具体长什么样,我这里就不展示了,自己去看会比较有成就感,嘿嘿!!


image.png


目录


截屏2021-07-21 上午7.47.33.png


config.js


这个文件中模拟webpack的配置


const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'main.js'
}
}

入口文件


src/index.js是入口文件


// src/index
import { age } from './aa.js'
import { name } from './hh.js'

console.log(`${name}今年${age}岁了`)

// src/aa.js
export const age = 18

// src/hh.js
console.log('我来了')
export const name = '林三心'


1. 定义Compiler类


// index.js
class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模块
this.modules = []
}
// 构建启动
run() {}
// 重写 require函数,输出bundle
generate() {}
}

2. 解析入口文件,获取 AST


我们这里使用@babel/parser,这是babel7的工具,来帮助我们分析内部的语法,包括 es6,返回一个 AST 抽象语法树


const fs = require('fs')
const parser = require('@babel/parser')
const options = require('./webpack.config')

const Parser = {
getAst: path => {
// 读取入口文件
const content = fs.readFileSync(path, 'utf-8')
// 将文件内容转为AST抽象语法树
return parser.parse(content, {
sourceType: 'module'
})
}
}

class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模块
this.modules = []
}
// 构建启动
run() {
const ast = Parser.getAst(this.entry)
}
// 重写 require函数,输出bundle
generate() {}
}

new Compiler(options).run()

3. 找出所有依赖模块


Babel 提供了@babel/traverse(遍历)方法维护这 AST 树的整体状态,我们这里使用它来帮我们找出依赖模块


const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default

const Parser = {
getAst: path => {
// 读取入口文件
const content = fs.readFileSync(path, 'utf-8')
// 将文件内容转为AST抽象语法树
return parser.parse(content, {
sourceType: 'module'
})
},
getDependecies: (ast, filename) => {
const dependecies = {}
// 遍历所有的 import 模块,存入dependecies
traverse(ast, {
// 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
// 保存依赖模块路径,之后生成依赖关系图需要用到
const filepath = './' + path.join(dirname, node.source.value)
dependecies[node.source.value] = filepath
}
})
return dependecies
}
}

class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模块
this.modules = []
}
// 构建启动
run() {
const { getAst, getDependecies } = Parser
const ast = getAst(this.entry)
const dependecies = getDependecies(ast, this.entry)
}
// 重写 require函数,输出bundle
generate() {}
}

new Compiler(options).run()

4. AST 转换为 code


AST 语法树转换为浏览器可执行代码,我们这里使用@babel/core 和 @babel/preset-env


const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')

const Parser = {
getAst: path => {
// 读取入口文件
const content = fs.readFileSync(path, 'utf-8')
// 将文件内容转为AST抽象语法树
return parser.parse(content, {
sourceType: 'module'
})
},
getDependecies: (ast, filename) => {
const dependecies = {}
// 遍历所有的 import 模块,存入dependecies
traverse(ast, {
// 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
// 保存依赖模块路径,之后生成依赖关系图需要用到
const filepath = './' + path.join(dirname, node.source.value)
dependecies[node.source.value] = filepath
}
})
return dependecies
},
getCode: ast => {
// AST转换为code
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
return code
}
}

class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模块
this.modules = []
}
// 构建启动
run() {
const { getAst, getDependecies, getCode } = Parser
const ast = getAst(this.entry)
const dependecies = getDependecies(ast, this.entry)
const code = getCode(ast)
}
// 重写 require函数,输出bundle
generate() {}
}

new Compiler(options).run()

5. 递归解析所有依赖项,生成依赖关系图


const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')

const Parser = {
getAst: path => {
// 读取入口文件
const content = fs.readFileSync(path, 'utf-8')
// 将文件内容转为AST抽象语法树
return parser.parse(content, {
sourceType: 'module'
})
},
getDependecies: (ast, filename) => {
const dependecies = {}
// 遍历所有的 import 模块,存入dependecies
traverse(ast, {
// 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
// 保存依赖模块路径,之后生成依赖关系图需要用到
const filepath = './' + path.join(dirname, node.source.value)
dependecies[node.source.value] = filepath
}
})
return dependecies
},
getCode: ast => {
// AST转换为code
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
return code
}
}

class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模块
this.modules = []
}
// 构建启动
run() {
// 解析入口文件
const info = this.build(this.entry)
this.modules.push(info)
this.modules.forEach(({ dependecies }) => {
// 判断有依赖对象,递归解析所有依赖项
if (dependecies) {
for (const dependency in dependecies) {
this.modules.push(this.build(dependecies[dependency]))
}
}
})
// 生成依赖关系图
const dependencyGraph = this.modules.reduce(
(graph, item) => ({
...graph,
// 使用文件路径作为每个模块的唯一标识符,保存对应模块的依赖对象和文件内容
[item.filename]: {
dependecies: item.dependecies,
code: item.code
}
}),
{}
)
}
build(filename) {
const { getAst, getDependecies, getCode } = Parser
const ast = getAst(filename)
const dependecies = getDependecies(ast, filename)
const code = getCode(ast)
return {
// 文件路径,可以作为每个模块的唯一标识符
filename,
// 依赖对象,保存着依赖模块路径
dependecies,
// 文件内容
code
}
}
// 重写 require函数,输出bundle
generate() {}
}

new Compiler(options).run()

6. 重写 require 函数,输出 bundle


const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')

const Parser = {
getAst: path => {
// 读取入口文件
const content = fs.readFileSync(path, 'utf-8')
// 将文件内容转为AST抽象语法树
return parser.parse(content, {
sourceType: 'module'
})
},
getDependecies: (ast, filename) => {
const dependecies = {}
// 遍历所有的 import 模块,存入dependecies
traverse(ast, {
// 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
// 保存依赖模块路径,之后生成依赖关系图需要用到
const filepath = './' + path.join(dirname, node.source.value)
dependecies[node.source.value] = filepath
}
})
return dependecies
},
getCode: ast => {
// AST转换为code
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
return code
}
}

class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模块
this.modules = []
}
// 构建启动
run() {
// 解析入口文件
const info = this.build(this.entry)
this.modules.push(info)
this.modules.forEach(({ dependecies }) => {
// 判断有依赖对象,递归解析所有依赖项
if (dependecies) {
for (const dependency in dependecies) {
this.modules.push(this.build(dependecies[dependency]))
}
}
})
// 生成依赖关系图
const dependencyGraph = this.modules.reduce(
(graph, item) => ({
...graph,
// 使用文件路径作为每个模块的唯一标识符,保存对应模块的依赖对象和文件内容
[item.filename]: {
dependecies: item.dependecies,
code: item.code
}
}),
{}
)
this.generate(dependencyGraph)
}
build(filename) {
const { getAst, getDependecies, getCode } = Parser
const ast = getAst(filename)
const dependecies = getDependecies(ast, filename)
const code = getCode(ast)
return {
// 文件路径,可以作为每个模块的唯一标识符
filename,
// 依赖对象,保存着依赖模块路径
dependecies,
// 文件内容
code
}
}
// 重写 require函数 (浏览器不能识别commonjs语法),输出bundle
generate(code) {
// 输出文件路径
const filePath = path.join(this.output.path, this.output.filename)
// 懵逼了吗? 没事,下一节我们捋一捋
const bundle = `(function(graph){
function require(module){
function localRequire(relativePath){
return require(graph[module].dependecies[relativePath])
}
var exports = {};
(function(require,exports,code){
eval(code)
})(localRequire,exports,graph[module].code);
return exports;
}
require('${this.entry}')
})(${JSON.stringify(code)})`

// 把文件内容写入到文件系统
fs.writeFileSync(filePath, bundle, 'utf-8')
}
}

new Compiler(options).run()

7. 看看main里的代码


实现了上面的代码,也就实现了把打包后的代码写到main.js文件里,咱们来看看那main.js文件里的代码吧:


(function(graph){
function require(module){
function localRequire(relativePath){
return require(graph[module].dependecies[relativePath])
}
var exports = {};
(function(require,exports,code){
eval(code)
})(localRequire,exports,graph[module].code);
return exports;
}
require('./src/index.js')
})({
"./src/index.js": {
"dependecies": {
"./aa.js": "./src\\aa.js",
"./hh.js": "./src\\hh.js"
},
"code": "\"use strict\";\n\nvar _aa = require(\"./aa.js\");\n\nvar _hh = require(\"./hh.js\");\n\nconsole.log(\"\".concat(_hh.name, \"\\u4ECA\\u5E74\").concat(_aa.age, \"\\u5C81\\u4E86\"));"
},
"./src\\aa.js": {
"dependecies": {},
"code": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.age = void 0;\nvar age = 18;\nexports.age = age;"
},
"./src\\hh.js": {
"dependecies": {},
"code": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.name = void 0;\nconsole.log('我来了');\nvar name = '林三心';\nexports.name = name;"
}
})

大家可以执行一下main.js的代码,输出结果是:


我来了
林三心今年18岁了

image.png


结语


webpack具体实现原理是很复杂的,这里只是简单实现一下,让大家粗略了解一下,webpack是怎么运作的。在代码实现过程中,大家可以自己console.log一下,看看ast,dependcies,code这些具体长什么样,我这里就不展示了,自己去看会比较有成就感,嘿嘿!!



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

收起阅读 »

Electron上手指南

前置 配置代理,解决网络问题: npm set electron_mirror https://npm.taobao.org/mirrors/electron/ # electron 二进制包镜像 npm set ELECTRON_MIRROR https:/...
继续阅读 »

前置


配置代理,解决网络问题:


npm set electron_mirror https://npm.taobao.org/mirrors/electron/ # electron 二进制包镜像
npm set ELECTRON_MIRROR https://cdn.npm.taobao.org/dist/electron/ # electron 二进制包镜像

安装:


npm install electron --save-dev

使用


和开发 Web 应用非常类似。


index.html


<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
  <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
  <meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'">
  <title>Hello World!</title>
</head>
<body>
  <h1>Hello World!</h1>
  We are using Node.js <span id="node-version"></span>,
  Chromium <span id="chrome-version"></span>,
  and Electron <span id="electron-version"></span>.
</body>
</html>

main.js


const { app, BrowserWindow } = require('electron')

function createWindow() {
const win = new BrowserWindow({
  width: 800,
  height: 600
})

win.loadFile('index.html')
}

app.whenReady().then(() => {
createWindow()
})

甚至可以直接加载一个现成的线上应用:


const { app, BrowserWindow } = require('electron')

function createWindow() {
const win = new BrowserWindow({
  width: 800,
  height: 600
})

win.loadURL('https://www.baidu.com/')
}

app.whenReady().then(() => {
createWindow()
})

package.json


{
"name": "electron-demo",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
  "start": "electron ."
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
  "electron": "^13.1.7"
}
}

执行:


npm start

打包构建


npm install --save-dev @electron-forge/cli
npx electron-forge import

npm run make

流程模型


Electron 与 Chrome 类似采用多进程架构。作为 Electron 应用开发者,可以控制着两种类型的进程:主进程和渲染器。


主进程


每个 Electron 应用都有一个单一的主进程,作为应用程序的入口点。 主进程在 Node.js 环境中运行,这意味着它具有 require 模块和使用所有 Node.js API 的能力。


窗口管理


主进程的主要目的是使用 BrowserWindow 模块创建和管理应用程序窗口。
BrowserWindow 类的每个实例创建一个应用程序窗口,且在单独的渲染器进程中加载一个网页。 可从主进程用 window 的 webContent 对象与网页内容进行交互。


const { BrowserWindow } = require('electron')

const win = new BrowserWindow({ width: 800, height: 1500 })
win.loadURL('https://github.com')

const contents = win.webContents
console.log(contents)
复制代码

应用程序生命周期


主进程还能通过 Electron 的app 模块来控制应用程序的生命周期。 该模块提供了一整套的事件和方法,可以添加自定义的应用程序行为 ( 例如:以编程方式退出您的应用程序、修改程序坞或显示关于面板 ) 。


// 当 macOS 无窗口打开时退出应用
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})

渲染器进程


每个 Electron 应用都会为每个打开的 BrowserWindow ( 与每个网页嵌入 ) 生成一个单独的渲染器进程。


预加载脚本


预加载(preload)脚本包含了那些执行于渲染器进程中,且先于网页内容开始加载的代码 。


预加载脚本可以在 BrowserWindow 构造方法中的 webPreferences 选项里被附加到主进程。


const { BrowserWindow } = require('electron')
//...
const win = new BrowserWindow({
preload: 'path/to/preload.js'
})
//...

由于预加载脚本与渲染器共享同一个全局 Window 接口,并且可以访问 Node.js API,因此它通过在 window 全局中暴露任意的网络内容来增强渲染器。



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

收起阅读 »

你真的了解package.json吗?来看看吧,这可能是最全的package解析

1. 概述 从我们接触前端开始,每个项目的根目录下一般都会有一个package.json文件,这个文件定义了当前项目所需要的各种模块,以及项目的配置信息(比如名称、版本、许可证等)。 当运行npm install命令的时候,会根据package.json文件中...
继续阅读 »

1. 概述


从我们接触前端开始,每个项目的根目录下一般都会有一个package.json文件,这个文件定义了当前项目所需要的各种模块,以及项目的配置信息(比如名称、版本、许可证等)。


当运行npm install命令的时候,会根据package.json文件中的配置自动下载所需的模块,也就是配置项目所需的运行和开发环境。


比如下面这个文件,只存在简单的项目名称和版本号。


{
"name" : "yindong",
"version" : "1.0.0",
}

package.json文件是一个JSON对象,这从他的后缀名.json就可以看出来,该对象的每一个成员就是当前项目的一项设置。比如name就是项目名称,version是版本号。


当然很多人其实并不关心package.json的配置,他们应用的更多的是dependencies或devDependencies配置。


下面是一个更完整的package.json文件,详细解释一下每个字段的真实含义。


{
"name": "yindong",
"version":"0.0.1",
"description": "antd-theme",
"keywords":["node.js","antd", "theme"],
"homepage": "https://zhiqianduan.com",
"bugs":{"url":"http://path/to/bug","email":"yindong@xxxx.com"},
"license": "ISC",
"author": "yindong",
"contributors":[{"name":"yindong","email":"yindong@xxxx.com"}],
"files": "",
"main": "./dist/default.js",
"bin": "",
"man": "",
"directories": "",
"repository": {
"type": "git",
"url": "https://path/to/url"
},
"scripts": {
"start": "webpack serve --config webpack.config.dev.js --progress"
},
"config": { "port" : "8080" },
"dependencies": {},
"devDependencies": {
"@babel/core": "^7.14.3",
"@babel/preset-env": "^7.14.4",
"@babel/preset-react": "^7.13.13",
"babel-loader": "^8.2.2",
"babel-plugin-import": "^1.13.3",
"glob": "^7.1.7",
"less": "^3.9.0",
"less-loader": "^9.0.0",
"style-loader": "^2.0.0",
"webpack": "^5.38.1",
"webpack-cli": "^4.7.0",
"webpack-dev-server": "^3.11.2"
},
"peerDependencies": {
"tea": "2.x"
},
"bundledDependencies": [
"renderized", "super-streams"
],
"engines": {"node": "0.10.x"},
"os" : [ "win32", "darwin", "linux" ],
"cpu" : [ "x64", "ia32" ],
"private": false,
"publishConfig": {}
}


2. name字段


package.json文件中最重要的就是nameversion字段,这两项是必填的。名称和版本一起构成一个标识符,该标识符被认为是完全唯一的。对包的更改应该与对版本的更改一起进行。


name必须小于等于214个字符,不能以._开头,不能有大写字母,因为名称最终成为URL的一部分因此不能包含任何非URL安全字符。
npm官方建议我们不要使用与核心节点模块相同的名称。不要在名称中加jsnode。如果需要可以使用engines来指定运行环境。


该名称会作为参数传递给require,因此它应该是简短的,但也需要具有合理的描述性。


3. version字段


version一般的格式是x.x.x, 并且需要遵循该规则。


package.json文件中最重要的就是nameversion字段,这两项是必填的。名称和版本一起构成一个标识符,该标识符被认为是完全唯一的。每次发布时version不能与已存在的一致。


4. description字段


description是一个字符串,用于编写描述信息。有助于人们在npm库中搜索的时候发现你的模块。


5. keywords字段


keywords是一个字符串组成的数组,有助于人们在npm库中搜索的时候发现你的模块。


6. homepage字段


homepage项目的主页地址。


7. bugs字段


bugs用于项目问题的反馈issue地址或者一个邮箱。


"bugs": { 
"url" : "https://github.com/owner/project/issues",
"email" : "project@hostname.com"
}

8. license字段


license是当前项目的协议,让用户知道他们有何权限来使用你的模块,以及使用该模块有哪些限制。


"license" : "BSD-3-Clause"

9. author字段 contributors字段


author是具体一个人,contributors表示一群人,他们都表示当前项目的共享者。同时每个人都是一个对象。具有name字段和可选的urlemail字段。


"author": {
"name" : "yindong",
"email" : "yindong@xx.com",
"url" : "https://zhiqianduan.com/"
}

也可以写成一个字符串


"author": "yindong yindong@xx.com (https://zhiqianduan.com/)"

10. files字段


files属性的值是一个数组,内容是模块下文件名或者文件夹名,如果是文件夹名,则文件夹下所有的文件也会被包含进来(除非文件被另一些配置排除了)


可以在模块根目录下创建一个.npmignore文件,写在这个文件里边的文件即便被写在files属性里边也会被排除在外,这个文件的写法与.gitignore类似。


11. main字段


main字段指定了加载的入口文件,require导入的时候就会加载这个文件。这个字段的默认值是模块根目录下面的index.js


12. bin字段


bin项用来指定每个内部命令对应的可执行文件的位置。如果你编写的是一个node工具的时候一定会用到bin字段。


当我们编写一个cli工具的时候,需要指定工具的运行命令,比如常用的webpack模块,他的运行命令就是webpack


"bin": {
"webpack": "bin/index.js",
}

当我们执行webpack命令的时候就会执行bin/index.js文件中的代码。


在模块以依赖的方式被安装,如果存在bin选项。在node_modules/.bin/生成对应的文件,
Npm会寻找这个文件,在node_modules/.bin/目录下建立符号链接。由于node_modules/.bin/目录会在运行时加入系统的PATH变量,因此在运行npm时,就可以不带路径,直接通过命令来调用这些脚本。


所有node_modules/.bin/目录下的命令,都可以用npm run [命令]的格式运行。在命令行下,键入npm run,然后按tab键,就会显示所有可以使用的命令。


13. man字段


man用来指定当前模块的man文档的位置。


"man" :[ "./doc/calc.1" ]

14. directories字段


directories制定一些方法来描述模块的结构, 用于告诉用户每个目录在什么位置。


15. repository字段


指定一个代码存放地址,对想要为你的项目贡献代码的人有帮助


"repository" : {
"type" : "git",
"url" : "https://github.com/npm/npm.git"
}

16. scripts字段


scripts指定了运行脚本命令的npm命令行缩写,比如start指定了运行npm run start时,所要执行的命令。


"scripts": {
"start": "node ./start.js"
}

使用scripts字段可以快速的执行shell命令,可以理解为alias


scripts可以直接使用node_modules中安装的模块,这区别于直接运行需要使用npx命令。


"scripts": {
"build": "webpack"
}

// npm run build
// npx webpack

17. config字段


config字段用于添加命令行的环境变量。


{
"name" : "yindong",
"config" : { "port" : "8080" },
"scripts" : { "start" : "node server.js" }
}

然后,在server.js脚本就可以引用config字段的值。


console.log(process.env.npm_package_config_port); // 8080

用户可以通过npm config set来修改这个值。


npm config set yindong:port 8000

18. dependencies字段, devDependencies字段


dependencies字段指定了项目运行所依赖的模块,devDependencies指定项目开发所需要的模块。


它们的值都是一个对象。该对象的各个成员,分别由模块名和对应的版本要求组成,表示依赖的模块及其版本范围。


当安装依赖的时候使用--save参数表示将该模块写入dependencies属性,--save-dev表示将该模块写入devDependencies属性。


"devDependencies": {
"webpack": "^5.38.1",
}

对象的每一项通过一个键值对表示,前面是模块名称,后面是对应模块的版本号。版本号遵循“大版本.次要版本.小版本”的格式规定。



版本说明



固定版本: 比如5.38.1,安装时只安装指定版本。
波浪号: 比如~5.38.1, 表示安装5.38.x的最新版本(不低于5.38.1),但是不安装5.39.x,也就是说安装时不改变大版本号和次要版本号。
插入号: 比如ˆ5.38.1, ,表示安装5.x.x的最新版本(不低于5.38.1),但是不安装6.x.x,也就是说安装时不改变大版本号。需要注意的是,如果大版本号为0,则插入号的行为与波浪号相同,这是因为此时处于开发阶段,即使是次要版本号变动,也可能带来程序的不兼容。
latest: 安装最新版本。




19. peerDependencies字段


当我们开发一个模块的时候,如果当前模块与所依赖的模块同时依赖一个第三方模块,并且依赖的是两个不兼容的版本时就会出现问题。


比如,你的项目依赖A模块和B模块的1.0版,而A模块本身又依赖B模块的2.0版。


大多数情况下,这不构成问题,B模块的两个版本可以并存,同时运行。但是,有一种情况,会出现问题,就是这种依赖关系将暴露给用户。


最典型的场景就是插件,比如A模块是B模块的插件。用户安装的B模块是1.0版本,但是A插件只能和2.0版本的B模块一起使用。这时,用户要是将1.0版本的B的实例传给A,就会出现问题。因此,需要一种机制,在模板安装的时候提醒用户,如果A和B一起安装,那么B必须是2.0模块。


peerDependencies字段,就是用来供插件指定其所需要的主工具的版本。可以通过peerDependencies字段来限制,使用myless模块必须依赖less模块的3.9.x版本.


{
"name": "myless",
"peerDependencies": {
"less": "3.9.x"
}
}

注意,从npm 3.0版开始,peerDependencies不再会默认安装了。就是初始化的时候不会默认带出。


20. bundledDependencies字段


bundledDependencies指定发布的时候会被一起打包的模块.


21. optionalDependencies字段


如果一个依赖模块可以被使用, 同时你也希望在该模块找不到或无法获取时npm继续运行,你可以把这个模块依赖放到optionalDependencies配置中。这个配置的写法和dependencies的写法一样,不同的是这里边写的模块安装失败不会导致npm install失败。


22. engines字段


engines字段指明了该模块运行的平台,比如Node或者npm的某个版本或者浏览器。


{ "engines" : { "node" : ">=0.10.3 <0.12", "npm" : "~1.0.20" } }

23. os字段


可以指定你的模块只能在哪个操作系统上运行


"os" : [ "darwin", "linux", "win32" ]

24. cpu字段


限制模块只能在某种架构的cpu下运行


"cpu" : [ "x64", "ia32" ]

25. private字段


如果这个属性被设置为truenpm将拒绝发布它,这是为了防止一个私有模块被无意间发布出去。


"private": true

26. publishConfig字段


这个配置是会在模块发布时生效,用于设置发布用到的一些值的集合。如果你不想模块被默认标记为最新的,或者默认发布到公共仓库,可以在这里配置tag或仓库地址。


通常publishConfig会配合private来使用,如果你只想让模块被发布到一个特定的npm仓库,如一个内部的仓库。


"private": true,
"publishConfig": {
"tag": "1.0.0",
"registry": "https://registry.npmjs.org/",
"access": "public"
}

27. preferGlobal字段


preferGlobal的值是布尔值,表示当用户不将该模块安装为全局模块时(即不用–global参数),要不要显示警告,表示该模块的本意就是安装为全局模块。


"preferGlobal": false

28. browser字段


browser指定该模板供浏览器使用的版本。Browserify这样的浏览器打包工具,通过它就知道该打包那个文件。


"browser": {
"tipso": "./node_modules/tipso/src/tipso.js"
},


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


收起阅读 »

教你使用whistle工具捉小程序包

介绍 我们说起捉包工具,可能大家比较熟悉的Fiddler工具,它是通过断点修改请求响应的方式,平时使用测试捉包也是很方便的,今天主角介绍另一个捉包工具whistle,这个工具比较轻,无需安装客户端只需通过终端node即可跑起捉取数据 whistle用的是类似...
继续阅读 »

介绍



  • 我们说起捉包工具,可能大家比较熟悉的Fiddler工具,它是通过断点修改请求响应的方式,平时使用测试捉包也是很方便的,今天主角介绍另一个捉包工具whistle,这个工具比较轻,无需安装客户端只需通过终端node即可跑起捉取数据

  • whistle用的是类似配置系统hosts的方式,一切操作都可以通过配置实现,支持域名、路径、正则表达式、通配符、通配路径等多种匹配方式,且可以通过Node模块扩展功能,更多内容介绍请查看官方文档


安装



  1. 安装node 文档地址


$ node -v  // 查看node版本号
v12.0.0 //(建议12版本以上,不然手机捉包会有点问题)


  1. 安装whistle


npm install -g whistle
或者直接指定镜像安装:
npm install whistle -g --registry=https://registry.npm.taobao.org


  1. 使用whistle

    • 启动whistle: (以下指令,window系统不需要$符号)


    $ w2 start


    • 重启whsitle:


    $ w2 restart


    • 停止whistle:3


    $ w2 stop


    • 调试模式启动whistle(主要用于查看whistle的异常及插件开发):


    $ w2 run

    w2 start启动完即可查看本地ip,把ip拷贝到浏览器即可


image.png
在浏览器显示效果
image.png
4. 配置代理 更多配置请查看官方文档

抓取 Https 请求需要配置



  • 电脑上安装根证书(现在安装证书也没那么麻烦,下载完直接点安装一步下一步就行)


   下载根证书:Whistle 监控界面 -> HTTPS -> Download RootCA

   下载完根证书后点击rootCA.crt文件,弹出根证书安装对话框。

   Windows 安装方法:

image.png



  • 移动端需要在设置中配置当前Wi-Fi的代理,以 harmonyOS 为例:


image.png



  • 手机上安装根证书


   iOS:

   Safari 地址栏输入 rootca.pro,按提示安装证书。  

   iOS 10.3 之后需要手动信任自定义根证书,设置路径:设置 --> 通用 --> 关于本机 --> 证书信任设置


   Android:

   用浏览器扫描 whistle 监控界面 HTTPS 的二维码下载安装,或者浏览器地址栏 rootca.pro 按提示安装。

   ca 证书安装完后可以在 Android 手机的“设置” -》“安全和隐私” -》“受信任的凭证” 里查看手机上有没有安装成功。

   部分浏览器不会自动识别 ca 证书,可以通过 Android Chrome 来完成安装。



  • 电脑选择配置勾选捉取https:


image.png



  • 最后捉取得效果图:


image.png


总结



  • 其实使用并不难,按上面安装步骤来即可,这个捉包方法适用于捉取小程序体验版或测试版,不支持小程序正式版本,如果打开小程序正式版本,整个小程序请求接口都会异常无法请求;如果你的体验版小程序无法捉取,请尝试打开调试工具;(本文仅限学习,方便测试使用,还有更多好玩的东西,请移步到官方文档

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

收起阅读 »

面试了十几个高级前端,竟然连(扁平数据结构转Tree)都写不出来

前言 招聘季节一般都在金三银四,或者金九银十。最近在这五六月份,陆陆续续面试了十几个高级前端。有一套考察算法的小题目。后台返回一个扁平的数据结构,转成树。 我们看下题目:打平的数据内容如下: let arr = [ {id: 1, name: '部门1...
继续阅读 »

前言


招聘季节一般都在金三银四,或者金九银十。最近在这五六月份,陆陆续续面试了十几个高级前端。有一套考察算法的小题目。后台返回一个扁平的数据结构,转成树。


我们看下题目:打平的数据内容如下:


let arr = [
{id: 1, name: '部门1', pid: 0},
{id: 2, name: '部门2', pid: 1},
{id: 3, name: '部门3', pid: 1},
{id: 4, name: '部门4', pid: 3},
{id: 5, name: '部门5', pid: 4},
]

输出结果


[
{
"id": 1,
"name": "部门1",
"pid": 0,
"children": [
{
"id": 2,
"name": "部门2",
"pid": 1,
"children": []
},
{
"id": 3,
"name": "部门3",
"pid": 1,
"children": [
// 结果 ,,,
]
}
]
}
]

我们的要求很简单,可以先不用考虑性能问题。实现功能即可,回头分析了面试的情况,结果使我大吃一惊。


10%的人没思路,没碰到过这种结构


60%的人说用过递归,有思路,给他个笔记本,但就是写不出来


20%的人在引导下,磕磕绊绊能写出来


剩下10%的人能写出来,但性能不是最佳


感觉不是在招聘季节遇到一个合适的人真的很难。


接下来,我们用几种方法来实现这个小算法


什么是好算法,什么是坏算法


判断一个算法的好坏,一般从执行时间占用空间来看,执行时间越短,占用的内存空间越小,那么它就是好的算法。对应的,我们常常用时间复杂度代表执行时间,空间复杂度代表占用的内存空间。


时间复杂度



时间复杂度的计算并不是计算程序具体运行的时间,而是算法执行语句的次数。



随着n的不断增大,时间复杂度不断增大,算法花费时间越多。 常见的时间复杂度有



  • 常数阶O(1)

  • 对数阶O(log2 n)

  • 线性阶O(n)

  • 线性对数阶O(n log2 n)

  • 平方阶O(n^2)

  • 立方阶O(n^3)

  • k次方阶O(n^K)

  • 指数阶O(2^n)


计算方法



  1. 选取相对增长最高的项

  2. 最高项系数是都化为1

  3. 若是常数的话用O(1)表示


举个例子:如f(n)=3*n^4+3n+300 则 O(n)=n^4


通常我们计算时间复杂度都是计算最坏情况。计算时间复杂度的要注意的几个点



  • 如果算法的执行时间不随n增加增长,假如算法中有上千条语句,执行时间也不过是一个较大的常数。此类算法的时间复杂度是O(1)。 举例如下:代码执行100次,是一个常数,复杂度也是O(1)


    let x = 1;
while (x <100) {
x++;
}


  • 多个循环语句时候,算法的时间复杂度是由嵌套层数最多的循环语句中最内层语句的方法决定的。举例如下:在下面for循环当中,外层循环每执行一次内层循环要执行n次,执行次数是根据n所决定的,时间复杂度是O(n^2)


  for (i = 0; i < n; i++){
for (j = 0; j < n; j++) {
// ...code
}
}


  • 循环不仅与n有关,还与执行循环判断条件有关。举例如下:在代码中,如果arr[i]不等于1的话,时间复杂度是O(n)。如果arr[i]等于1的话,循环不执行,时间复杂度是O(0)


    for(var i = 0; i<n && arr[i] !=1; i++) {
// ...code
}

空间复杂度



空间复杂度是对一个算法在运行过程中临时占用存储空间的大小。



计算方法:



  1. 忽略常数,用O(1)表示

  2. 递归算法的空间复杂度=(递归深度n)*(每次递归所要的辅助空间)


计算空间复杂度的简单几点



  • 仅仅只复制单个变量,空间复杂度为O(1)。举例如下:空间复杂度为O(n) = O(1)。


   let a = 1;
let b = 2;
let c = 3;
console.log('输出a,b,c', a, b, c);


  • 递归实现,调用fun函数,每次都创建1个变量k。调用n次,空间复杂度O(n*1) = O(n)。


    function fun(n) {
let k = 10;
if (n == k) {
return n;
} else {
return fun(++n)
}
}

不考虑性能实现,递归遍历查找


主要思路是提供一个递getChildren的方法,该方法递归去查找子集。
就这样,不用考虑性能,无脑去查,大多数人只知道递归,就是写不出来。。。


/**
* 递归查找,获取children
*/
const getChildren = (data, result, pid) => {
for (const item of data) {
if (item.pid === pid) {
const newItem = {...item, children: []};
result.push(newItem);
getChildren(data, newItem.children, item.id);
}
}
}

/**
* 转换方法
*/
const arrayToTree = (data, pid) => {
const result = [];
getChildren(data, result, pid)
return result;
}

从上面的代码我们分析,该实现的时间复杂度为O(2^n)


不用递归,也能搞定


主要思路是先把数据转成Map去存储,之后遍历的同时借助对象的引用,直接从Map找对应的数据做存储


function arrayToTree(items) {
const result = []; // 存放结果集
const itemMap = {}; //

// 先转成map存储
for (const item of items) {
itemMap[item.id] = {...item, children: []}
}

for (const item of items) {
const id = item.id;
const pid = item.pid;
const treeItem = itemMap[id];
if (pid === 0) {
result.push(treeItem);
} else {
if (!itemMap[pid]) {
itemMap[pid] = {
children: [],
}
}
itemMap[pid].children.push(treeItem)
}

}
return result;
}

从上面的代码我们分析,有两次循环,该实现的时间复杂度为O(2n),需要一个Map把数据存储起来,空间复杂度O(n)


最优性能


主要思路也是先把数据转成Map去存储,之后遍历的同时借助对象的引用,直接从Map找对应的数据做存储。不同点在遍历的时候即做Map存储,有找对应关系。性能会更好。


function arrayToTree(items) {
const result = []; // 存放结果集
const itemMap = {}; //
for (const item of items) {
const id = item.id;
const pid = item.pid;

if (!itemMap[id]) {
itemMap[id] = {
children: [],
}
}

itemMap[id] = {
...item,
children: itemMap[id]['children']
}

const treeItem = itemMap[id];

if (pid === 0) {
result.push(treeItem);
} else {
if (!itemMap[pid]) {
itemMap[pid] = {
children: [],
}
}
itemMap[pid].children.push(treeItem)
}

}
return result;
}

从上面的代码我们分析,一次循环就搞定了,该实现的时间复杂度为O(n),需要一个Map把数据存储起来,空间复杂度O(n)


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

收起阅读 »

前端是不是又要回去操作真实dom年代?

写在开头 近期我有写两篇文章,一篇是:petite-vue源码解析和掘金编辑器的源码解析,发现里面用到了Svelte这个框架 加上最近React17,vite大家也在逐步的用在生产环境中,我于是有了今天的思考 看前端的技术演进 原生Javascript ...
继续阅读 »

写在开头



  • 近期我有写两篇文章,一篇是:petite-vue源码解析和掘金编辑器的源码解析,发现里面用到了Svelte这个框架

  • 加上最近React17,vite大家也在逐步的用在生产环境中,我于是有了今天的思考


看前端的技术演进



  • 原生Javascript - Jquery为代表的时代,例如,引入Jquery只要


<script src="cdn/jquery.min,js"></script>


  • 接着便又有了gulp webpack等构建工具出现,React和Vue也在这个时候开始火了起来,随即而来的是一大堆工程化的辅助工具,例如babel,还有提供整套服务的create-react-app等脚手架

  • 这也带来了问题,当然这个是npm的问题,每次启动项目前,都要安装大量的依赖,即便出现了yarn pnpm`等优化的依赖管理工具,但是这个问题根源不应该使用工具解决,而是问题本质是依赖本地化,代码和依赖需要工具帮助才能运行在浏览器中



总结就是:现有的开发模式,让项目太重,例如我要使用某个脚手架,我只想写一个helloworld演示下,结果它让我装500mb的依赖,不同的脚手架产物,配置不同,产物也不同



理想的开发模式




  • 1.不需要辅助的工具配置,我不需要webpack这类帮我打包的工具,模块化浏览器本身就支持,而且是一个规范。例如vite号称不打包,用的是浏览器本身支持的esm模块化,但是它没有解决依赖的问题,因为依赖问题本身是依赖的问题,而不是工具的问题




  • 2.不需要安装依赖,一切都可以import from remote,我觉得webpack5Module Federation设计,就考虑到了这一点,下面是官方的解释:




    • 多个独立的构建可以组成一个应用程序,这些独立的构建之间不应该存在依赖关系,因此可以单独开发和部署它们。




    • 这通常被称作微前端,但并不仅限于此。







但是这可能并不是最佳实践,目前是有import from http,例如



import lodash from 'https://unpackage/lodash/es'


  • 这里又会有人问,那你不都是要发请求吗,都是要每次启动的时候去远程拉取,还不如在本地呢。import from http我想只是解决了一个点的问题,就是不用手动安装依赖到本地磁盘

  • 前段时间我写过,在浏览器中本地运行Node.js




这个技术叫WebContainers技术,感兴趣的可以去翻翻我公众号之前的文章




  • 等等,别急。这些仅仅开了个头,新的技术往往要探索才能实现价值最大化,我想此处应该可以彻底颠覆现有的开发模式,而且应该就在3-5年内。


将几个新的前端技术理念融合?



  • vite的不打包理念:直接使用浏览器支持的esm模块化

  • WebContainers技术:让浏览器直接运行node.js

  • import from remote,从一个个远程地址直接引入可以使用的依赖

  • 现在很火的webIDE:类似remix编辑器,直接全部可以在云端搞定

  • 浏览器的优化,天然有缓存支持


会发生什么变化?



  • 我们所有的一切开始,都直接启动一个浏览器即可

  • 浏览器中的webIDE,可以直接引入远程依赖,浏览器可以运行Node.js,使用的都是esm模块化,不需要打包工具,项目启动的时间和热更新时间都非常短,构建也是直接可以在浏览器中构建



这些看似解决了我们之前提出的大部分问题,回到今天的主题





回到主题



  • 前端会不会回到操作原生dom的时代?

  • 我觉得,有这个趋势,例如petite-vue,还有Svelte



因为之前写过petite-vue源码解析了,我们今天就讲讲Svelte



Svelte



Svelte 是一种全新的构建用户界面的方法。传统框架如 React 和 Vue 在浏览器中需要做大量的工作,而 Svelte 将这些工作放到构建应用程序的编译阶段来处理。




  • 与使用虚拟(virtual)DOM 差异对比不同。Svelte 编写的代码在应用程序的状态更改时就能像做外科手术一样更新 DOM






  • 上面是官方的介绍,我们看看知乎这篇文章https://zhuanlan.zhihu.com/p/97825481,感觉他写得很好,这里照搬一些过来吧直接




  • React和Vue都是基于runtime的框架。所谓基于runtime的框架就是框架本身的代码也会被打包到最终的bundle.js并被发送到用户浏览器。




  • 当用户在你的页面进行各种操作改变组件的状态时,框架的runtime会根据新的组件状态(state)计算(diff)出哪些DOM节点需要被更新





可是,这些被打包进去的框架,实在太大了。



(今天还在跟同事说,前年写的登录站点,纯原生手工打造,性能无敌)



  • 100kb对于一个弱网环境来说,很要命,我们看看svelte减少了多少体积:



科普



  • 虚拟dom并没有加快用户操作浏览器响应的速度,只是说,方便用于数据驱动视图,更便于管理而已,并且在一定程度上,更慢。真正最快的永远是:


currentDom.innerHtml = '前端巅峰';


所以Svelte并不是说多好,而是它的这种理念,可能未来会越来越成为主流



React17的改变



  • 大家应该都知道,现有的浏览器都是无法直接解译JSX的,所以大多数React用户都需要使用Babel或者TypeScript之类的编译器来将JSX转换为浏览器能够理解的JavaScript语言。许多预配置的工具箱(如:Create React App 或者Next.js)内部也有JSX的转换。

  • React 17.0,尽管React团队想对JSX的转换进行改进,但React团队不想打破现有的配置。这就是为什么React团队与Babel合作,为想要升级的开发者提供了一个全新的JSX转换的重写版本。

  • 通过全新的转换,你可以单独使用JSX而无需引入React.



我猜想,或许React团队有意将jsx语法推动到成为es标准语法中去,剥离开来希望会大大提升。



重点



  • 说了这么多,大家可能没理解到重点,那就是:大家都在想着减轻自身的负重,把丢下来的东西标准化,交给浏览器处理,这也是在为未来的只需要打开一个浏览器,就可以完成所有的事情做铺垫

  • 而我,相信这一天应该不远了,据我所知已经有不少顶尖的团队在研发这种产品



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

收起阅读 »

面试官:你知道git xx 和git xx的区别吗?看完这篇Git指南后直呼:内行!

Git
前言 作为一名工程师,既然想要加入一个团队,并肩作战地协同开发项目,就必不可少要学会Git基本操作。面试过程中,面试官不止是考察1-3年的工程师,高级岗位也同样会考察团队协作的能力。相信小伙伴们经常会在面试中被问到以下问题吧,可以帮助你测试一下你的Git基础牢...
继续阅读 »

前言


作为一名工程师,既然想要加入一个团队,并肩作战地协同开发项目,就必不可少要学会Git基本操作。面试过程中,面试官不止是考察1-3年的工程师,高级岗位也同样会考察团队协作的能力。相信小伙伴们经常会在面试中被问到以下问题吧,可以帮助你测试一下你的Git基础牢不牢固。



  • 代码开发到一半,需要紧急切换分支修复线上问题,该怎么办?

  • 代码合并有几种模式?分别有什么优缺点?

  • git fetchgit pull有什么区别,有合并操作吗?

  • git mergegit rebase有什么区别,它们的应用场景有哪些?

  • git resetgit revert有什么区别,该如何选择,回滚后的<commit-id>还能找到吗?


如果你心中已有答案,那么可以选择跳过这篇文章啦,愉快地继续摸鱼~


如果你对这些概念还有些模糊,或者没有实际操作过,那么就需要好好阅读本篇文章啦!


接下来马上进入正文啦,本文分四个部分介绍,大家可以自由选择阅读。



  • Git的区域示例图,帮助大家理解Git的结构。

  • Git的基本使用场景,介绍一些常用git命令。

  • Git的进阶使用场景,介绍一些高频出现的面试题目以及应用场景。

  • 最后介绍Git的下载地址、基本配置和工具推荐。


Git的区域


画了一个简单的示意图,供大家参考


yuque_diagram.jpg



  • 远程仓库(Remote):在远程用于存放代码的服务器,远程仓库的内容能够被分布其他地方的本地仓库修改。

  • 本地仓库(Repository):在自己电脑上的仓库,平时我们用git commit 提交到暂存区,就会存入本地仓库。

  • 暂存区(Index):执行 git add 后,工作区的文件就会被移入暂存区,表示哪些文件准备被提交,当完成某个功能后需要提交代码,可以通过 git add 先提交到暂存区。

  • 工作区(Workspace):工作区,开发过程中,平时打代码的地方,看到是当前最新的修改内容。


Git的基本使用场景


以下命令远程主机名默认为origin,如果有其他远程主机,将origin替换为其他即可。


git fetch


# 获取远程仓库特定分支的更新
git fetch origin <分支名>

# 获取远程仓库所有分支的更新
git fetch --all

git pull


# 从远程仓库拉取代码,并合并到本地,相当于 git fetch && git merge 
git pull origin <远程分支名>:<本地分支名>

# 拉取后,使用rebase的模式进行合并
git pull --rebase origin <远程分支名>:<本地分支名>

注意



  • 直接git pull 不加任何选项,等价于git fetch + git merge FETCH_HEAD,执行效果就是会拉取所有分支信息回来,但是只合并当前分支的更改。其他分支的变更没有执行合并。

  • 使用git pull --rebase 可以减少冲突的提交点,比如我本地已经提交,但是远程其他同事也有新的代码提交记录,此时拉取远端其他同事的代码,如果是merge的形式,就会有一个merge的commit记录。如果用rebase,就不会产生该合并记录,是将我们的提交点挪到其他同事的提交点之后。


git branch


# 基于当前分支,新建一个本地分支,但不切换
git branch <branch-name>

# 查看本地分支
git branch

# 查看远程分支
git branch -r

# 查看本地和远程分支
git branch -a

# 删除本地分支
git branch -D <branch-name>

# 基于旧分支创建一个新分支
git branch <new-branch-name> <old-branch-name>

# 基于某提交点创建一个新分支
git branch <new-branch-name> <commit-id>

# 重新命名分支
git branch -m <old-branch-name> <new-branch-name>

git checkout


# 切换到某个分支上
git checkout <branch-name>

# 基于当前分支,创建一个分支并切换到新分支上
git checkout -b <branch-name>

git add


# 添把当前工作区修改的文件添加到暂存区,多个文件可以用空格隔开
git add xxx

# 添加当前工作区修改的所有文件到暂存区
git add .

git commit


# 提交暂存区中的所有文件,并写下提交的概要信息
git commit -m "message"

# 相等于 git add . && git commit -m
git commit -am

# 对最近一次的提交的信息进行修改,此操作会修改commit的hash值
git commit --amend

git push


# 推送提交到远程仓库
git push

# 强行推送到远程仓库
git push -f

git tag


# 查看所有已打上的标签
git tag

# 新增一个标签打在当前提交点上,并写上标签信息
git tag -a <version> -m 'message'

# 为指定提交点打上标签
git tag -a <version> <commit-id>

# 删除指定标签
git tag -d <version>

Git的进阶使用场景



HEAD表示最新提交 ;HEAD^表示上一次; HEAD~n表示第n次(从0开始,表示最近一次)



正常协作



  • git pull 拉取远程仓库的最新代码

  • 工作区修改代码,完成功能开发

  • git add . 添加修改的文件到暂存区

  • git commit -m 'message' 提交到本地仓库

  • git push将本地仓库的修改推送到远程仓库


代码合并


git merge


自动创建一个新的合并提交点merge-commit,且包含两个分支记录。如果合并的时候遇到冲突,仅需要修改解决冲突后,重新commit。



  • 场景:如dev要合并进主分支master,保留详细的合并信息

  • 优点:展示真实的commit情况

  • 缺点:分支杂乱


git checkout master
git merge dev

rf1o2b6eduboqwkigg3w.gif


git merge 的几种模式



  • git merge --ff (默认--ff,fast-farward)

    • 结果:被merge的分支和当前分支在图形上并为一条线,被merge的提交点commit合并到当前分支,没有新的提交点merge

    • 缺点:代码合并不冲突时,默认快速合并,主分支按时间顺序混入其他分支的零碎commit点。而且删除分支,会丢失分支信息。



  • git merge --no-ff(不快速合并、推荐)

    • 结果:被merge的分支和当前分支不在一条线上,被merge的提交点commit还在原来的分支上,并在当前分支产生一个新提交点merge

    • 优点:代码合并产生冲突就会走这个模式,利于回滚整个大版本(主分支自己的commit点)



  • git merge --squash(把多次分支commit历史压缩为一次)

    • 结果:把多次分支commit历史压缩为一次




image.png


git rebase



  • 不产生merge commit,变换起始点位置,“整理”成一条直线,且能使用命令合并多次commit。

  • 如在develop上git rebase master 就会拉取到master上的最新代码合并进来,也就是将分支的起始时间指向master上最新的commit上。自动保留的最新近的修改,不会遇到合并冲突。而且可交互操作(执行合并删除commit),可通过交互式变基来合并分支之前的commit历史git rebase -i HEAD~3

  • 场景:主要发生在个人分支上,如 git rebase master整理自己的dev变成一条线。频繁进行了git commit提交,可用交互操作drop删除一些提交,squash提交融合前一个提交中。

  • 优点:简洁的提交历史

  • 缺点:发生错误难定位,解决冲突比较繁琐,要一个一个解决。


git checkout dev
git rebase master

dwyukhq8yj2xliq4i50e.gifmsofpv7k6rcmpaaefscm.gif


git merge和git rebase的区别



  • merge会保留两个分支的commit信息,而且是交叉着的,即使是ff模式,两个分支的commit信息会混合在一起(按真实提交时间排序),多用于自己dev合并进master。

  • rebase意思是变基,改变分支的起始位置,在dev上git rebase master,将dev的多次commit一起拉到要master最新提交的后面(时间最新),变成一条线,多用于整理自己的dev提交历史,然后把master最新代码合进来。

  • 使用rebase还是merge更多的是管理风格的问题,有个较好实践:

    • 就是dev在merge进主分支(如master)之前,最好将自己的dev分支给rebase到最新的主分支(如master)上,然后用pull request创建普通merge请求。

    • 用rebase整理成重写commit历史,所有修改拉到master的最新修改前面,保证dev运行在当前最新的主branch的代码。避免了git历史提交里无意义的交织。



  • 假设场景:从 dev 拉出分支 feature-a。

    • 那么当 dev 要合并 feature-a 的内容时,使用 git merge feature-a

    • 反过来当 feature-a 要更新 dev 的内容时,使用 git rebase dev



  • git merge和git rebase 两者对比图

    • git merge图示 image.png

    • git rebase图示 image.png




取消合并


# 取消merge合并
git merge --abort
# 取消rebase合并
git rebase --abort

代码回退


代码回退的几种方式



  • git checkout

  • git reset

    • --hard:硬重置,影响【工作区、暂存区、本地仓库】

    • --mixed:默认,影响【暂存区、本地仓库】,被重置的修改内容还留在工作区

    • --soft:软重置,影响 【本地仓库】,被重置的修改内容还留在工作区和暂存区



  • git revert


# 撤回工作区该文件的修改,多个文件用空格隔开
git checkout -- <file-name>
# 撤回工作区所有改动
git checkout .

# 撤回已经commit到暂存区的文件
git reset <file-name>
# 撤回已经commit到暂存区的所有文件
git reset .
# 丢弃已commit的其他版本,hard参数表示同时重置工作区的修改
git reset --hard <commit-id>
# 回到上一个commit的版本,hard参数表示同时重置工作区的修改
git reset --hard HEAD^

# 撤销0ffaacc这次提交
git revert 0ffaacc
# 撤销最近一次提交
git revert HEAD
# 撤销最近2次提交,注意:数字从0开始
git revert HEAD~1

# 回退后要执行强制推送远程分支
git push -f

git reset和git revert的区别



  • reset是根据来移动HEAD指针,在该次提交点后面的提交记录会丢失。


hlh0kowt3hov1xhcku38.gif



  • revert会产生新的提交,来抵消选中的该次提交的修改内容,可以理解为“反做”,不会丢失中间的提交记录。


3kkd2ahn41zixs12xgpf.gif



  • 使用建议

    • 公共分支回退使用git revert,避免丢掉其他同事的提交。

    • 自己分支回退可使用git reset,也可以使用git revert,按需使用。




挑拣代码


git cherry-pick



  • “挑拣”提交,单独抽取某个分支的一个提交点,将这个提交点的所有修改内容,搬运到你的当前分支。

  • 如果我们只想将其他分支的某个提交点合并进来,不想用git merge将所有提交点合并进来,就需要使用这个git cherry-pick


git cherry-pick <commit-id>

2dkjx4yeaal10xyvj29v.gif


暂存代码


git stash



  • 当我们想要切换去其他分支修复bug,此时当前的功能代码还没修改完整,不想commit,就需要暂存当前修改的文件,然后切换到hotfix分支修复bug,修复完成再切换回来,将暂存的修改提取出来,继续功能开发。

  • 还有另一种场景就是,同事在远程分支上推送了代码,此时拉下来有冲突,可以将我们自己的修改stash暂存起来,然后先拉最新的提交代码,再pop出来,这样可以避免一个冲突的提交点。


# 将本地改动的暂存起来
git stash
# 将未跟踪的文件暂存(另一种方式是先将新增的文件添加到暂存区,使其被git跟踪,就可以直接git stash)
git stash -u
# 添加本次暂存的备注,方便查找。
git stash save "message"
# 应用暂存的更改
git stash apply
# 删除暂存
git stash drop
# 应用暂存的更改,然后删除该暂存,等价于git stash apply + git stash drop
git stash pop
# 删除所有缓存
git stash clear

打印日志



  1. git log


可以显示所有提交过的版本信息,如果感觉太繁琐,可以加上参数  --pretty=oneline,只会显示版本号和提交时的备注信息。



  1. git reflog


git reflog 可以查看所有分支的所有操作记录(包括已经被删除的 commit 记录和 reset 的操作),例如执行 git reset --hard HEAD~1,退回到上一个版本,用git log是看不出来被删除的,用git reflog则可以看到被删除的,我们就可以买后悔药,恢复到被删除的那个版本。


Git的下载、配置、工具推荐



  • Git下载地址


  • 两种拉取代码的方式

    • https:每次都要手动输入用户名和密码

    • ssh :自动使用本地私钥+远程的公钥验证是否为一对秘钥



  • 配置ssh

    • ssh-keygen -t rsa -C "邮箱地址"

    • cd ~/.ssh切换到home下面的ssh目录、cat id_rsa.pub命令查看公钥的内容,然后复制

    • github的settings -> SSH and GPG keys-> 复制刚才的内容贴入 -> Add SSH key

    • 全局配置一下Git用户名和邮箱

      • git config --global user.name "xxx"

      • git config --global user.email "xxx@xx.com"

      • image.png





  • Git 相关工具推荐

    • 图形化工具 SourceTree :可视化执行git命令,解放双手

    • VSCode插件 GitLens:可以在每行代码查看对应git的提交信息,而且提供每个提交点的差异对比




结尾


阅读到这里,是不是感觉对Git相关概念更加清晰了呢,那么恭喜你,再也不怕因为误操作,丢失同事辛辛苦苦写的代码了,而且将在日常工作的协同中游刃有余。



  • 💖建议收藏文章,工作中有需要的时候翻出来看一看~

  • 📃创作不易,如果我的文章对你有帮助,辛苦大佬们点个赞👍🏻,支持我一下~

  • 📌如果有错漏,欢迎大佬们指正~

  • 👏欢迎转载分享,请注明出处,谢谢~

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

收起阅读 »

为了让她10分钟入门canvas,我熬夜写了3个小项目和这篇文章

1. canvas实现时钟转动 实现以下效果,分为几步: 1、找到canvas的中心,画出表心,以及表框 2、获取当前时间,并根据时间画出时针,分针,秒针,还有刻度 3、使用定时器,每过一秒获取新的时间,并重新绘图,达到时钟转动的效果 1.1 表心,表框...
继续阅读 »

image.png


1. canvas实现时钟转动


实现以下效果,分为几步:



  • 1、找到canvas的中心,画出表心,以及表框

  • 2、获取当前时间,并根据时间画出时针,分针,秒针,还有刻度

  • 3、使用定时器,每过一秒获取新的时间,并重新绘图,达到时钟转动的效果


截屏2021-07-19 下午8.52.15.png


1.1 表心,表框


画表心,表框有两个知识点:



  • 1、找到canvas的中心位置

  • 2、绘制圆形


//html

<canvas id="canvas" width="600" height="600"></canvas>

// js

// 设置中心点,此时300,300变成了坐标的0,0
ctx.translate(300, 300)
// 画圆线使用arc(中心点X,中心点Y,半径,起始角度,结束角度)
ctx.arc(0, 0, 100, 0, 2 * Math.PI)
ctx.arc(0, 0, 5, 0, 2 * Math.PI)
// 执行画线段的操作stroke
ctx.stroke()

让我们来看看效果,发现了,好像不对啊,我们是想画两个独立的圆线,怎么画出来的两个圆连到一起了


截屏2021-07-19 下午9.10.07.png
原因是:上面代码画连个圆时,是连着画的,所以画完大圆后,线还没斩断,就接着画小圆,那肯定会大圆小圆连一起,解决办法就是:beginPath,closePath


ctx.translate(300, 300) // 设置中心点,此时300,300变成了坐标的0,0

// 画大圆
+ ctx.beginPath()
// 画圆线使用arc(中心点X,中心点Y,半径,起始角度,结束角度)
ctx.arc(0, 0, 100, 0, 2 * Math.PI)
ctx.stroke() // 执行画线段的操作
+ ctx.closePath()

// 画小圆
+ ctx.beginPath()
ctx.arc(0, 0, 5, 0, 2 * Math.PI)
ctx.stroke()
+ ctx.closePath()

1.2 时针,分针,秒针


画这三个指针,有两个知识点:



  • 1、根据当前时,分,秒计算角度

  • 2、在计算好的角度上去画出时针,分针,秒针


如何根据算好的角度去画线呢,比如算出当前是3点,那么时针就应该以12点为起始点,顺时针旋转2 * Math.PI / 12 * 3 = 90°,分针和秒针也是同样的道理,只不过跟时针不同的是比例问题而已,因为时在表上有12份,而分针和秒针都是60份


截屏2021-07-19 下午10.07.19.png


这时候又有一个新问题,还是以上面的例子为例,我算出了90°,那我们怎么画出时针呢?我们可以使用moveTo和lineTo去画线段。至于90°,我们只需要将x轴顺时针旋转90°,然后再画出这条线段,那就得到了指定角度的指针了。但是上面说了,是要以12点为起始点,我们的默认x轴确是水平的,所以我们时分秒针算出角度后,每次都要减去90°。可能这有点绕,我们通过下面的图演示一下,还是以上面3点的例子:


截屏2021-07-19 下午10.30.23.png


截屏2021-07-19 下午10.31.02.png
这样就得出了3点指针的画线角度了。


又又又有新问题了,比如现在我画完了时针,然后我想画分针,x轴已经在我画时针的时候偏转了,这时候肯定要让x轴恢复到原来的模样,我们才能继续画分针,否则画出来的分针是不准的。这时候save和restore就派上用场了,save是把ctx当前的状态打包压入栈中,restore是取出栈顶的状态并赋值给ctxsave可多次,但是restore取状态的次数必须等于save次数


截屏2021-07-19 下午10.42.06.png


懂得了上面所说,剩下画刻度了,起始刻度的道理跟时分秒针道理一样,只不过刻度是死的,不需要计算,只需要规则画出60个小刻度,和12个大刻度就行


const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')

ctx.translate(300, 300) // 设置中心点,此时300,300变成了坐标的0,0
// 把状态保存起来
+ ctx.save()

// 画大圆
ctx.beginPath()
// 画圆线使用arc(中心点X,中心点Y,半径,起始角度,结束角度)
ctx.arc(0, 0, 100, 0, 2 * Math.PI)
ctx.stroke() // 执行画线段的操作
ctx.closePath()

// 画小圆
ctx.beginPath()
ctx.arc(0, 0, 5, 0, 2 * Math.PI)
ctx.stroke()
ctx.closePath()

----- 新加代码 ------

// 获取当前 时,分,秒
let time = new Date()
let hour = time.getHours() % 12
let min = time.getMinutes()
let sec = time.getSeconds()

// 时针
ctx.rotate(2 * Math.PI / 12 * hour + 2 * Math.PI / 12 * (min / 60) - Math.PI / 2)
ctx.beginPath()
// moveTo设置画线起点
ctx.moveTo(-10, 0)
// lineTo设置画线经过点
ctx.lineTo(40, 0)
// 设置线宽
ctx.lineWidth = 10
ctx.stroke()
ctx.closePath()
// 恢复成上一次save的状态
ctx.restore()
// 恢复完再保存一次
ctx.save()

// 分针
ctx.rotate(2 * Math.PI / 60 * min + 2 * Math.PI / 60 * (sec / 60) - Math.PI / 2)
ctx.beginPath()
ctx.moveTo(-10, 0)
ctx.lineTo(60, 0)
ctx.lineWidth = 5
ctx.strokeStyle = 'blue'
ctx.stroke()
ctx.closePath()
ctx.restore()
ctx.save()

//秒针
ctx.rotate(2 * Math.PI / 60 * sec - - Math.PI / 2)
ctx.beginPath()
ctx.moveTo(-10, 0)
ctx.lineTo(80, 0)
ctx.strokeStyle = 'red'
ctx.stroke()
ctx.closePath()
ctx.restore()
ctx.save()

// 绘制刻度,也是跟绘制时分秒针一样,只不过刻度是死的
ctx.lineWidth = 1
for (let i = 0; i < 60; i++) {
ctx.rotate(2 * Math.PI / 60)
ctx.beginPath()
ctx.moveTo(90, 0)
ctx.lineTo(100, 0)
// ctx.strokeStyle = 'red'
ctx.stroke()
ctx.closePath()
}
ctx.restore()
ctx.save()
ctx.lineWidth = 5
for (let i = 0; i < 12; i++) {
ctx.rotate(2 * Math.PI / 12)
ctx.beginPath()
ctx.moveTo(85, 0)
ctx.lineTo(100, 0)
ctx.stroke()
ctx.closePath()
}

ctx.restore()

截屏2021-07-19 下午10.53.53.png


最后一步就是更新视图,使时钟转动起来,第一想到的肯定是定时器setInterval,但是注意一个问题:每次更新视图的时候都要把上一次的画布清除,再开始画新的视图,不然就会出现千手观音的景象


截屏2021-07-19 下午10.57.05.png


附上最终代码:


const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')

setInterval(() => {
ctx.save()
ctx.clearRect(0, 0, 600, 600)
ctx.translate(300, 300) // 设置中心点,此时300,300变成了坐标的0,0
ctx.save()

// 画大圆
ctx.beginPath()
// 画圆线使用arc(中心点X,中心点Y,半径,起始角度,结束角度)
ctx.arc(0, 0, 100, 0, 2 * Math.PI)
ctx.stroke() // 执行画线段的操作
ctx.closePath()

// 画小圆
ctx.beginPath()
ctx.arc(0, 0, 5, 0, 2 * Math.PI)
ctx.stroke()
ctx.closePath()

// 获取当前 时,分,秒
let time = new Date()
let hour = time.getHours() % 12
let min = time.getMinutes()
let sec = time.getSeconds()

// 时针
ctx.rotate(2 * Math.PI / 12 * hour + 2 * Math.PI / 12 * (min / 60) - Math.PI / 2)
ctx.beginPath()
// moveTo设置画线起点
ctx.moveTo(-10, 0)
// lineTo设置画线经过点
ctx.lineTo(40, 0)
// 设置线宽
ctx.lineWidth = 10
ctx.stroke()
ctx.closePath()
ctx.restore()
ctx.save()

// 分针
ctx.rotate(2 * Math.PI / 60 * min + 2 * Math.PI / 60 * (sec / 60) - Math.PI / 2)
ctx.beginPath()
ctx.moveTo(-10, 0)
ctx.lineTo(60, 0)
ctx.lineWidth = 5
ctx.strokeStyle = 'blue'
ctx.stroke()
ctx.closePath()
ctx.restore()
ctx.save()

//秒针
ctx.rotate(2 * Math.PI / 60 * sec - Math.PI / 2)
ctx.beginPath()
ctx.moveTo(-10, 0)
ctx.lineTo(80, 0)
ctx.strokeStyle = 'red'
ctx.stroke()
ctx.closePath()
ctx.restore()
ctx.save()

// 绘制刻度,也是跟绘制时分秒针一样,只不过刻度是死的
ctx.lineWidth = 1
for (let i = 0; i < 60; i++) {
ctx.rotate(2 * Math.PI / 60)
ctx.beginPath()
ctx.moveTo(90, 0)
ctx.lineTo(100, 0)
// ctx.strokeStyle = 'red'
ctx.stroke()
ctx.closePath()
}
ctx.restore()
ctx.save()
ctx.lineWidth = 5
for (let i = 0; i < 12; i++) {
ctx.rotate(2 * Math.PI / 12)
ctx.beginPath()
ctx.moveTo(85, 0)
ctx.lineTo(100, 0)
// ctx.strokeStyle = 'red'
ctx.stroke()
ctx.closePath()
}

ctx.restore()
ctx.restore()
}, 1000)

效果 very good啊:


clock的副本.gif


2. canvas实现刮刮卡


小时候很多人都买过充值卡把,懂的都懂啊哈,用指甲刮开这层灰皮,就能看底下的答案了。
截屏2021-07-19 下午11.02.09.png


思路是这样的:



  • 1、底下答案是一个div,顶部灰皮是一个canvascanvas一开始盖住div

  • 2、鼠标事件,点击时并移动时,鼠标经过的路径都画圆形开路,并且设置globalCompositeOperationdestination-out,使鼠标经过的路径都变成透明,一透明,自然就显示出下方的答案信息。


关于fill这个方法,其实是对标stroke的,fill是把图形填充,stroke只是画出边框线


// html
<canvas id="canvas" width="400" height="100"></canvas>
<div class="text">恭喜您获得100w</div>
<style>
* {
margin: 0;
padding: 0;
}
.text {
position: absolute;
left: 130px;
top: 35px;
z-index: -1;
}
</style>


// js
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')

// 填充的颜色
ctx.fillStyle = 'darkgray'
// 填充矩形 fillRect(起始X,起始Y,终点X,终点Y)
ctx.fillRect(0, 0, 400, 100)
ctx.fillStyle = '#fff'
// 绘制填充文字
ctx.fillText('刮刮卡', 180, 50)

let isDraw = false
canvas.onmousedown = function () {
isDraw = true
}
canvas.onmousemove = function (e) {
if (!isDraw) return
// 计算鼠标在canvas里的位置
const x = e.pageX - canvas.offsetLeft
const y = e.pageY - canvas.offsetTop
// 设置globalCompositeOperation
ctx.globalCompositeOperation = 'destination-out'
// 画圆
ctx.arc(x, y, 10, 0, 2 * Math.PI)
// 填充圆形
ctx.fill()
}
canvas.onmouseup = function () {
isDraw = false
}

效果如下:


guaguaka.gif


3. canvas实现画板和保存


框架:使用vue + elementUI


其实很简单,难点有以下几点:



  • 1、鼠标拖拽画正方形和圆形

  • 2、画完一个保存画布,下次再画的时候叠加

  • 3、保存图片


第一点,只需要计算出鼠标点击的点坐标,以及鼠标的当前坐标,就可以计算了,矩形长宽计算:x - beginX, y - beginY,圆形则要利用勾股定理:Math.sqrt((x - beginX) * (x - beginX) + (y - beginY) * (y - beginY))


第二点,则要利用canvas的getImageDataputImageData方法


第三点,思路是将canvas生成图片链接,并赋值给具有下载功能的a标签,并主动点击a标签进行图片下载


看看效果吧:


截屏2021-07-19 下午11.16.24.png


截屏2021-07-19 下午11.17.41.png


具体代码我就不过多讲解了,说难也不难,只要前面两个项目理解了,这个项目很容易就懂了:


<template>
<div>
<div style="margin-bottom: 10px; display: flex; align-items: center">
<el-button @click="changeType('huabi')" type="primary">画笔</el-button>
<el-button @click="changeType('rect')" type="success">正方形</el-button>
<el-button
@click="changeType('arc')"
type="warning"
style="margin-right: 10px"
>圆形</el-button
>
<div>颜色:</div>
<el-color-picker v-model="color"></el-color-picker>
<el-button @click="clear">清空</el-button>
<el-button @click="saveImg">保存</el-button>
</div>
<canvas
id="canvas"
width="800"
height="400"
@mousedown="canvasDown"
@mousemove="canvasMove"
@mouseout="canvasUp"
@mouseup="canvasUp"
>
</canvas>
</div>
</template>

<script>
export default {
data() {
return {
type: "huabi",
isDraw: false,
canvasDom: null,
ctx: null,
beginX: 0,
beginY: 0,
color: "#000",
imageData: null,
};
},
mounted() {
this.canvasDom = document.getElementById("canvas");
this.ctx = this.canvasDom.getContext("2d");
},
methods: {
changeType(type) {
this.type = type;
},
canvasDown(e) {
this.isDraw = true;
const canvas = this.canvasDom;
this.beginX = e.pageX - canvas.offsetLeft;
this.beginY = e.pageY - canvas.offsetTop;
},
canvasMove(e) {
if (!this.isDraw) return;
const canvas = this.canvasDom;
const ctx = this.ctx;
const x = e.pageX - canvas.offsetLeft;
const y = e.pageY - canvas.offsetTop;
this[`${this.type}Fn`](ctx, x, y);
},
canvasUp() {
this.imageData = this.ctx.getImageData(0, 0, 800, 400);
this.isDraw = false;
},
huabiFn(ctx, x, y) {
ctx.beginPath();
ctx.arc(x, y, 5, 0, 2 * Math.PI);
ctx.fillStyle = this.color;
ctx.fill();
ctx.closePath();
},
rectFn(ctx, x, y) {
const beginX = this.beginX;
const beginY = this.beginY;
ctx.clearRect(0, 0, 800, 400);
this.imageData && ctx.putImageData(this.imageData, 0, 0, 0, 0, 800, 400);
ctx.beginPath();
ctx.strokeStyle = this.color;
ctx.rect(beginX, beginY, x - beginX, y - beginY);
ctx.stroke();
ctx.closePath();
},
arcFn(ctx, x, y) {
const beginX = this.beginX;
const beginY = this.beginY;
this.isDraw && ctx.clearRect(0, 0, 800, 400);
this.imageData && ctx.putImageData(this.imageData, 0, 0, 0, 0, 800, 400);
ctx.beginPath();
ctx.strokeStyle = this.color;
ctx.arc(
beginX,
beginY,
Math.round(
Math.sqrt((x - beginX) * (x - beginX) + (y - beginY) * (y - beginY))
),
0,
2 * Math.PI
);
ctx.stroke();
ctx.closePath();
},
saveImg() {
const url = this.canvasDom.toDataURL();
const a = document.createElement("a");
a.download = "sunshine";
a.href = url;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
},
clear() {
this.imageData = null
this.ctx.clearRect(0, 0, 800, 400)
}
},
};
</script>

<style lang="scss" scoped>
#canvas {
border: 1px solid black;
}
</style>

结语


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

收起阅读 »

CSS 奇思妙想 | 巧妙的实现带圆角的三角形

之前在这篇文章中 -- 《老生常谈之 CSS 实现三角形》,介绍了 6 种使用 CSS 实现三角形的方式。 但是其中漏掉了一个非常重要的场景,如何使用纯 CSS 实现带圆角的三角形呢?,像是这样: 本文将介绍几种实现带圆角的三角形的实现方式。 法一. 全兼容...
继续阅读 »

之前在这篇文章中 -- 《老生常谈之 CSS 实现三角形》,介绍了 6 种使用 CSS 实现三角形的方式。


但是其中漏掉了一个非常重要的场景,如何使用纯 CSS 实现带圆角的三角形呢?,像是这样:


A triangle with rounded


本文将介绍几种实现带圆角的三角形的实现方式。


法一. 全兼容的 SVG 大法


想要生成一个带圆角的三角形,代码量最少、最好的方式是使用 SVG 生成。


使用 SVG 的 多边形标签 <polygon> 生成一个三边形,使用 SVG 的 stroke-linejoin="round" 生成连接处的圆角。


代码量非常少,核心代码如下:


<svg  width="250" height="250" viewBox="-50 -50 300 300">
<polygon class="triangle" stroke-linejoin="round" points="100,0 0,200 200,200"/>
</svg>

.triangle {
fill: #0f0;
stroke: #0f0;
stroke-width: 10;
}

实际图形如下:


A triangle with rounded


这里,其实是借助了 SVG 多边形的 stroke-linejoin: round 属性生成的圆角,stroke-linejoin 是什么?它用来控制两条描边线段之间,有三个可选值:



  • miter 是默认值,表示用方形画笔在连接处形成尖角

  • round 表示用圆角连接,实现平滑效果

  • bevel 连接处会形成一个斜接



我们实际是通过一个带边框,且边框连接类型为 stroke-linejoin: round 的多边形生成圆角三角形的


如果,我们把底色和边框色区分开,实际是这样的:


.triangle {
fill: #0f0;
stroke: #000;
stroke-width: 10;
}


通过 stroke-width 控制圆角大小


那么如何控制圆角大小呢?也非常简单,通过控制 stroke-width 的大小,可以改变圆角的大小。


当然,要保持三角形大小一致,在增大/缩小 stroke-width 的同时,需要缩小/增大图形的 width/height



完整的 DEMO 你可以戳这里:CodePen Demo -- 使用 SVG 实现带圆角的三角形


法二. 图形拼接


不过,上文提到了,使用纯 CSS 实现带圆角的三角形,但是上述第一个方法其实是借助了 SVG。那么仅仅使用 CSS,有没有办法呢?


当然,发散思维,CSS 有意思的地方正在于此处,用一个图形,能够有非常多种巧妙的解决方案!


我们看看,一个圆角三角形,它其实可以被拆分成几个部分:



所以,其实我们只需要能够画出一个这样的带圆角的菱形,通过 3 个进行旋转叠加,就能得到圆角三角形:



绘制带圆角的菱形


那么,接下来我们的目标就变成了绘制一个带圆角的菱形,方法有很多,本文给出其中一种方式:



  1. 首先将一个正方形变成一个菱形,利用 transform 有一个固定的公式:



<div></div>

div {
width: 10em;
height: 10em;
transform: rotate(-60deg) skewX(-30deg) scale(1, 0.866);
}



  1. 将其中一个角变成圆角:


div {
width: 10em;
height: 10em;
transform: rotate(-60deg) skewX(-30deg) scale(1, 0.866);
+ border-top-right-radius: 30%;
}


至此,我们就顺利的得到一个带圆角的菱形了!


拼接 3 个带圆角的菱形


接下来就很简单了,我们只需要利用元素的另外两个伪元素,再生成 2 个带圆角的菱形,将一共 3 个图形旋转位移拼接起来即可!


完整的代码如下:


<div></div>

div{
position: relative;
background-color: orange;
}
div:before,
div:after {
content: '';
position: absolute;
background-color: inherit;
}
div,
div:before,
div:after {
width: 10em;
height: 10em;
border-top-right-radius: 30%;
}
div {
transform: rotate(-60deg) skewX(-30deg) scale(1,.866);
}
div:before {
transform: rotate(-135deg) skewX(-45deg) scale(1.414, .707) translate(0,-50%);
}
div:after {
transform: rotate(135deg) skewY(-45deg) scale(.707, 1.414) translate(50%);
}

就可以得到一个圆角三角形了!效果如下:


image


完整的代码你可以戳这里:CodePen Demo -- A triangle with rounded


法三. 图形拼接实现渐变色圆角三角形


完了吗?没有!


上述方案,虽然不算太复杂,但是有一点还不算太完美的。就是无法支持渐变色的圆角三角形。像是这样:



如果需要实现渐变色圆角三角形,还是有点复杂的。但真就还有人鼓捣出来了,下述方法参考至 -- How to make 3-corner-rounded triangle in CSS


同样也是利用了多块进行拼接,但是这次我们的基础图形,会非常的复杂。


首先,我们需要实现这样一个容器外框,和上述的方法比较类似,可以理解为是一个圆角菱形(画出 border 方便理解):



<div></div>

div {
width: 200px;
height: 200px;
transform: rotate(30deg) skewY(30deg) scaleX(0.866);
border: 1px solid #000;
border-radius: 20%;
}

接着,我们同样使用两个伪元素,实现两个稍显怪异的图形进行拼接,算是对 transform 的各种用法的合集:


div::before,
div::after {
content: "";
position: absolute;
width: 200px;
height: 200px;
}
div::before {
border-radius: 20% 20% 20% 55%;
transform: scaleX(1.155) skewY(-30deg) rotate(-30deg) translateY(-42.3%) skewX(30deg) scaleY(0.866) translateX(-24%);
background: red;
}
div::after {
border-radius: 20% 20% 55% 20%;
background: blue;
transform: scaleX(1.155) skewY(-30deg) rotate(-30deg) translateY(-42.3%) skewX(-30deg) scaleY(0.866) translateX(24%);
}

为了方便理解,制作了一个简单的变换动画:



本质就是实现了这样一个图形:


image


最后,给父元素添加一个 overflow: hidden 并且去掉父元素的 border 即可得到一个圆角三角形:



由于这两个元素重叠空间的特殊结构,此时,给两个伪元素添加同一个渐变色,会完美的叠加在一起:


div::before,
div::after, {
background: linear-gradient(#0f0, #03a9f4);
}

最终得到一个渐变圆角三角形:



上述各个图形的完整代码,你可以戳这里:CodePen Demo -- A triangle with rounded and gradient background


最后


本文介绍了几种在 CSS 中实现带圆角三角形的方式,虽然部分有些繁琐,但是也体现了 CSS ”有趣且折磨人“ 的一面,具体应用的时候,还是要思考一下,对是否使用上述方式进行取舍,有的时候,切图也许是更好的方案。


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

收起阅读 »

微前端模块共享你真的懂了吗

前言:我们运用微前端架构解决了应用体积庞大的问题,通过实践微前端的理念,将前端应用拆分为多个微应用(可独立部署、松散耦合的应用)。同时微应用的存在,使得我们无需在构建一个庞大的应用,而是按需构建,极大了加快了构建效率。但只是解决了应用层面的问题,在中后台应用场...
继续阅读 »

前言:我们运用微前端架构解决了应用体积庞大的问题,通过实践微前端的理念,将前端应用拆分为多个微应用(可独立部署、松散耦合的应用)。同时微应用的存在,使得我们无需在构建一个庞大的应用,而是按需构建,极大了加快了构建效率。但只是解决了应用层面的问题,在中后台应用场景中,不同微应用和基座之间可能存在通用的模块依赖,那么如果应用间可以实现模块共享,那么可以大大优化单应体积大小



image.png


1.Npm 依赖



最简单的方式,就是把需要共享的模块抽出,可能是一个工具库,有可能是一个组件库,然后讲其打包成为npm包,然后在每个子应用中都安装该模块依赖,以此达到多个项目复用的效果



也就代表每个应用都有相同的npm包,本质上没有真正意义上的实现模块共享和复用,只是代码层次共享和复用了,应用打包构建时,还是会将依赖包一起打包


image.png


劣势有以下👇 几点:



  • 每个微应用都会打包该模块,导致依赖的包冗余,没有真正意义上的共享复用

  • npm包进行更新发布了,微应用还需要重新构建,调试麻烦且低效 (除非用npm link


2.Git Submodule (子模块)



阿乐童鞋: 那如果我们没有搭建npm内网,又不想把模块开源出去,而且依赖npm,只要涉及变更需要重新发布,有没有其他方式可以解决以上问题呀?



image.png


2.1 对比 npm


你可以试试 Git Submodule ,它提供了一种类似于npm package的依赖管理机制,两者差别如下图所示👇


image.png


2.2 如何使用


通过在应用项目中,通过git submodule add <submodule_url>远程拉取子模块项目,这时会发现应用项目中多了两个文件.gitmodules子模块目录


image.png


这个子模块就是我们共享的模块,它是一个完整的Git仓库,换句话说:我们在应用项目目录中无论使用git add/commit都对其不影响,即子模块拥有自身独立的版本控制


总结: submodule本质上是通过git submodule add把项目依赖的模块加起来,最终构成一个完整的项目。而且add进来的模块,项目中并不实际包含,而只是一个包含索引信息,也就是上文提到的 .gitmodule来存储子模块的联系方式, 以此实现同步关联子模块。当下载到本地运行的时候才会再拉取文件


部分命令行:




  • git submodule add <子模块repository> <path> : 添加子模块




  • git submodule update --recursive --remote : 拉取所有子模块的更新




2.3 Monorepo



阿乐童鞋: 🌲 树酱,我记得有个叫Monorepo又是什么玩意,跟 Git Submodule 有啥区别?



image.png


Monorepo 全称叫monolithic respoitory,即单体式仓库,核心是允许我们将多个项目放到同一个仓库里面进行管理。主张不拆分repo,而是在单仓库里统一管理各个模块的构建流程、版本号等等


这样可以避免大量的冗余node_module冗余,因为每个项目都会安装vue、vue-router等包,再或者本地开发需要的webpack、babel、mock等都会造成储存空间的浪费


那么Monorepo是怎么管理的呢? 开源社区中诸如babel、vue的项目都是基于Monorepo去维护的(Lerna工具)


我们以Babel为例,在github中可以看到其每个模块都在指定的packages目录下, 也就意味着将所有的相关package都放入一个repository来管理,这不是显得项目很臃肿?


image.png
也就这个问题,啊乐同学和啊康同学展开了辩论~


image.png



最终是选用Monorepo单体式仓库还是Multirepo多仓库管理, 具体还是要看你业务场景来定,Monorepo集中管理带来的便利性,比如方便版本、依赖等管理、方便调试,但也带来了不少不便之处 👇




  • 统一构建工具所带来更高的要求

  • 仓库体积过大,维护成本也高


🌲 酱 不小心扯多了,还有就是Monorepo 跟 Git Submodule 的区别




  • 前者:monorepo在单repo里存放所有子模块源码




  • 后者:submodules只在主repo里存放所有子模块“索引”




目前内部还未使用Monorepo进行落地实际,目前基于微前端架构中后台应用存在依赖重叠过多的情况,后期会通过实践来深入分享


3. Webpack external



我们知道webpack中有externals的配置,主要是用来配置:webpack输出的bundle中排除依赖,换句话说通过在external定义的依赖,最终输出的bundle不存在该依赖,主要适用于不需要经常打包更新的第三方依赖,以此来实现模块共享。



下面是一个vue.config.js 的配置文件,通过配置exteral移除不经常更新打包的第三方依赖👇
carbon (26).png


你可以通过在packjson中script定义的命令后添加--report查看打包📦后的分析图,如果是webpack就是用使用插件webpack-bundle-analyzer



阿乐童鞋: 🌲 树酱,那移除了这些依赖之后,如何保证应用正常使用?



浏览器环境:我们使用cdn的方式在入口文件引入,当然你也可以预先打包好,比如把vue全家桶打包成vue-family.min.js文件,最终达成多应用共享模块的效果


<script src="<%= VUE_APP_UTILS_URL %>static/js/vue-family.min.js"></script>


总结:避免公共模块包(package) 一起打到bundle 中,而是在运行时再去从外部获取这些扩展依赖


通过这种形式在微前端基座应用加载公共模块,并将微应用引用同样模块的external移除掉,就可以实现模块共享了
但是存在微应用技术栈多样化不统一的情况,可能有的使用vue3,有的使用react开发,但externals 并无法支持多版本共存的情况,针对这种情况该方式就不太适用


4. Webpack DLL


官方介绍:"DLL" 一词代表微软最初引入的动态链接库, 换句话说我的理解,可以把它当做缓存,通过预先编译好的第三方外部依赖bundle,来节省应用在打包时混入的时间



Webpack DLL 跟 上一节提到的external本质是解决同样的问题:就是避免将第三方外部依赖打入到应用的bundle中(业务代码),然后在运行时再去加载这部分依赖,以此来实现模块复用,也提升了编译构建速度



webpack dll模式下需要配置两份webpack配置,下面是主要两个核心插件


image.png


4.1 DllPlugin


DllPlugin:在一个独立的webpack进行配置webpack.dll.config.js,目的是为了创建一个把所有的第三方库依赖打包到一起的bundle的dll文件里面,同时还会生成一个manifest.json的文件,用于:让使用该第三方依赖集合的应用配置的DllReferencePlugin能映射到相关的依赖上去 具体配置看下图👇


carbon.png


image.png


4.2 DllReferencePlugin


DllReferencePlugin:插件核心是把上一节提到的通过webpack.dll.config.js中打包生成的dll文件,引用到需要实际项目中使用,引用机制就是通过DllReferencePlugin插件来读取vendor-manifest.json文件,看看是否有该第三方库,最后通过add-asset-html-webpack-plugin插件在入口html自动插入上一节生成的vendor.dll.js 文件, 具体配置看下图👇
carbon (1).png


5. 联邦模块 Module Federation


模块联邦是 Webpack5 推出的一个新的重要功能,可以真正意义上实现让跨应用间做到模块共享,解决了从前用 NPM 公共包方式共享的不便利,同时也可以作为微前端的落地方案,完美秒杀了上两节介绍webpack特征


用过qiankun的小伙伴应该知道,qiankun微前端架构控制的粒度是在应用层面,而Module Federation控制的粒度是在模块层面。相比之下,后者粒度更小,可以有更多的选择


与qiankun等微前端架构不同的另一点是,我们一般都是需要一个中心基座去控制微应用的生命周期,而Module Federation则是去中心化的,没有中心基座的概念,每一个模块或者应用都是可以导入或导出,我们可以称为:host和remote,应用或模块即可以是host也可以是remote,亦或者两者共同体


image.png


看看下面这个例子👇


carbon (3).png


核心在于 ModuleFederationPlugin中的几个属性



  • remote : 示作为 Host 时,去消费哪些 Remote;

  • exposes :表示作为 Remote 时,export 哪些属性提供给 Host 消费

  • shared: 可以让远程加载的模块对应依赖改为使用本地项目的 vue,换句话说优先用 Host 的依赖,如果 Host 没有,最后再使用自己的


后期也会围绕 Module Federation 去做落地分享


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

收起阅读 »

全自动jQuery与渣男的故事

我是个恋旧的人,Github头像还是上古时期端游仙剑奇侠传的截图。 对于前端,如果能jQuery一把梭,我是很开心的。 React、Vue的普及让大家习惯了虚拟DOM的存在。但是虚拟DOM一定是最优解么? 举个例子,要进行如下DOM移动操作: // 变化前 ...
继续阅读 »

我是个恋旧的人,Github头像还是上古时期端游仙剑奇侠传的截图。



对于前端,如果能jQuery一把梭,我是很开心的。


ReactVue的普及让大家习惯了虚拟DOM的存在。但是虚拟DOM一定是最优解么?


举个例子,要进行如下DOM移动操作:


// 变化前
abcd
// 变化后
dabc

jQuery时调用insertBefored挪到a前面就行。而React基于虚拟DOMDiff会依次对abc执行appendChild,将他们依次挪到最后。


1次DOM操作 vs 3次DOM操作,显然前者更高效。


那么有没有框架能砍掉虚拟DOM,直接对DOM节点执行操作,实现全自动jQuery


有的,这就是最近出的petite-vue


阅读完本文,你会从原理层面了解该框架,如果你还有精力,可以在此基础上深入框架源码。


全自动jQuery的实现


可以将原理概括为一句话:



建立状态更新DOM的方法之间的联系



比如,对于如下DOM


<p v-show="showName">我是卡颂</p>

期望showName状态的变化能影响p的显隐(通过改变diaplay)。


实际是建立showName的变化调用如下方法的联系:


() => {
el.style.display = get() ? initialDisplay : 'none'
}

其中el代表pget()获取showName当前值。


再比如,对于如下DOM


<p v-text="name"></p>

name改变后ptextContent会变为对应值。


实际是建立name的变化调用如下方法的联系:


() => {
el.textContent = toDisplayString(get())
}

所以,整个框架的工作原理呼之欲出:初始化时遍历所有DOM,根据各种v-xx属性建立DOM操作DOM的方法之间的联系。


当改变状态后,会自动调用与其有关的操作DOM的方法,简直就是全自动jQuery



所以,框架的核心在于:如何建立联系?


一个渣男的故事


这部分源码都收敛在@vue/reactivity库中。我并不想带你精读源码,因为这样很没意思,看了还容易忘。


接下来我会通过一个故事为你展示其工作原理,当你了解原理后如果感兴趣可以自己去看源码。



我们的目标是描述:状态变化更新DOM的方法之间的联系。说得再宽泛点,是建立状态副作用之间的联系。


即:状态变化 -> 执行副作用


对于一段关系,可以从当事双方的角度描述,比如:


男生指着女生说:这是我女朋友。


接着女生指着男生说:这是我男朋友。


你作为旁观者,通过双方的描述就知道他们处于一段恋爱关系。


推广到状态副作用,则是:


副作用指着状态说:我依赖这个状态,他变了我就会执行。


状态指着副作用说:我订阅了这个副作用,当我变了后我会通知他。



可以看到,发布订阅其实是对一段关系站在双方视角的阐述



举个例子,如下DOM结构:


<div v-scope="{num: 0}">
<button @click="num++">add 1</button>
<p v-show="num%2">
<span v-text="num"></span>
</p>
</div>

经过petite-vue遍历后的关系图:



框架的交互流程为:




  1. 触发点击事件,状态num变化




  2. 通知其订阅的副作用effect1effect2),执行对应DOM操作




如果从情侣关系角度解读,就是:


num指着effect1说:这是我女朋友。


effect1指着num说:这是我男朋友。


num指着effect2说:这是我女朋友。


effect2指着num说:这是我男朋友。



总结


今天我们学习了一个框架petite-vue,他的底层实现由多段混乱的男女关系组成,上层是一个个直接操作DOM的方法。


不知道看完后你有没有兴趣深入了解下这种关系呢?


感兴趣的话可以看看Vue MasteryVue 3 Reactivity课程。



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

收起阅读 »

拖拽竟然还能这样玩!

在大多数低代码平台中的设计器都支持组件拖拽的功能,这样大大地提高了用户的设计体验。而拖拽另一个比较常见的场景就是文件上传,通过拖拽的方式,可以让用户方便地上传文件。其实利用拖拽功能,我们还可以 跨越浏览器的边界,实现数据共享。 那么如何 跨越浏览器的边界,实现...
继续阅读 »

在大多数低代码平台中的设计器都支持组件拖拽的功能,这样大大地提高了用户的设计体验。而拖拽另一个比较常见的场景就是文件上传,通过拖拽的方式,可以让用户方便地上传文件。其实利用拖拽功能,我们还可以 跨越浏览器的边界,实现数据共享


那么如何 跨越浏览器的边界,实现数据共享 呢?本文阿宝哥将介绍谷歌的一个开源项目 —— transmat,利用该项目可以实现上述功能。不仅如此,该项目还可以帮助我们实现一些比较好玩的功能,比如针对不同的可释放目标,做出不同的响应。


下面我们先通过 4 张 Gif 动图来感受一下,使用 transmat 开发的 神奇、好玩 的拖拽功能。


图 1(把可拖拽的元素,拖拽至富文本编辑器)



图 2(把可拖拽的元素,拖拽至 Chrome 浏览器,也支持其他浏览器)



图 3(把可拖拽的元素,拖拽至自定义的释放目标)



图 4(把可拖拽的元素,拖拽至 Chrome 开发者工具)




以上示例使用的浏览器版本:Chrome 91.0.4472.114(正式版本) (x86_64)



以上 4 张图中的 可拖拽元素都是同一个元素,当它被放置到不同的可释放目标时,产生了不同的效果。同时,我们也跨越了浏览器的边界,实现了数据的共享。看完以上 4 张动图,你是不是觉得挺神奇的。其实除了拖拽之外,该示例也支持复制、粘贴操作。不过,在详细介绍如何使用 transmat 实现上述功能之前,我们先来简单介绍一下 transmat 这个库。


一、Transmat 简介


Transmat 是一个围绕 DataTransfer API 的小型库 ,它使用 drag-dropcopy-paste 交互简化了在 Web 应用程序中传输和接收数据的过程。 DataTransfer API 能够将多种不同类型的数据传输到用户设备上的其他应用程序,该 API 所支持的数据类型,常见的有这几种:text/plaintext/htmlapplication/json 等。



(图片来源:google.github.io/transmat/)


了解完 transmat 是什么之后,我们来看一下它的应用场景:



  • 想以便捷的方式与外部应用程序集成。

  • 希望为用户提供与其他应用程序共享数据的能力,即使是那些你不知道的应用程序。

  • 希望外部应用程序能够与你的 Web 应用程序深度集成。

  • 想让你的应用程序更好地适应用户现有的工作流程。


现在你已经对 transmat 有了一定的了解,下面我们来分析如何使用 transmat 实现以上 4 张 Gif 动图对应的功能。


二、Transmat 实战


2.1 transmat-source


html


在以下代码中,我们为 div#source 元素添加了 draggable 属性,该属性用于标识元素是否允许被拖动,它的取值为 truefalse


<script src="https://unpkg.com/transmat/lib/index.umd.js"></script>
<div id="source" draggable="true" tabindex="0">大家好,我是阿宝哥</div>

css


#source {
background: #eef;
border: solid 1px rgba(0, 0, 255, 0.2);
border-radius: 8px;
cursor: move;
display: inline-block;
margin: 1em;
padding: 4em 5em;
}

js


const { Transmat, addListeners, TransmatObserver } = transmat;

const source = document.getElementById("source");

addListeners(source, "transmit", (event) => {
const transmat = new Transmat(event);
transmat.setData({
"text/plain": "大家好,我是阿宝哥!",
"text/html": `
<h1>大家好,我是阿宝哥</h1>
<p>聚焦全栈,专注分享 TS、Vue 3、前端架构等技术干货。
<a href="https://juejin.cn/user/764915822103079">访问我的主页</a>!
</p>
<img src="https://sf3-ttcdn-tos.pstatp.com/img/user-avatar/
075d8e781ba84bf64035ac251988fb93~300x300.image" border="1" />
`,
"text/uri-list": "https://juejin.cn/user/764915822103079",
"application/json": {
name: "阿宝哥",
wechat: "semlinker",
},
});
});

在以上代码中,我们利用 transmat 这个库提供的 addListeners 函数为 div#source 元素,添加了 transmit 的事件监听。在对应的事件处理器中,我们先创建了 Transmat 对象,然后调用该对象上的 setData 方法设置不同 MIME 类型的数据。


下面我们来简单回顾一下,示例中所使用的 MIME 类型:



  • text/plain:表示文本文件的默认值,一个文本文件应当是人类可读的,并且不包含二进制数据。

  • text/html:表示 HTML 文件类型,一些富文本编辑器会优先从 dataTransfer 对象上获取 text/html 类型的数据,如果不存在的话,再获取 text/plain 类型的数据。

  • text/uri-list:表示 URI 链接类型,大多数浏览器都会优先读取该类型的数据,如果发现是合法的 URI 链接,则会直接打开该链接。如果不是的合法 URI 链接,对于 Chrome 浏览器来说,它会读取 text/plain 类型的数据并以该数据作为关键词进行内容检索。

  • application/json:表示 JSON 类型,该类型对前端开发者来说,应该都比较熟悉了。


介绍完 transmat-source 之后,我们来看一下图 3 自定义目标(transmat-target)的实现代码。


2.2 transmat-target


html


<script src="https://unpkg.com/transmat/lib/index.umd.js"></script>
<div id="target" tabindex="0">放这里哟!</div>

css


body {
text-align: center;
font: 1.2em Helvetia, Arial, sans-serif;
}
#target {
border: dashed 1px rgba(0, 0, 0, 0.5);
border-radius: 8px;
margin: 1em;
padding: 4em;
}
.drag-active {
background: rgba(255, 255, 0, 0.1);
}
.drag-over {
background: rgba(255, 255, 0, 0.5);
}

js


const { Transmat, addListeners, TransmatObserver } = transmat;

const target = document.getElementById("target");

addListeners(target, "receive", (event) => {
const transmat = new Transmat(event);
// 判断是否含有"application/json"类型的数据
// 及事件类型是否为drop或paste事件
if (transmat.hasType("application/json")
&& transmat.accept()
) {
const jsonString = transmat.getData("application/json");
const data = JSON.parse(jsonString);
target.textContent = jsonString;
}
});

在以上代码中,我们利用 transmat 这个库提供的 addListeners 函数为 div#target 元素,添加了 receive 的事件监听。顾名思义,该 receive 事件表示接收消息。在对应的事件处理器中,我们通过 transmat 对象的 hasType 方法过滤了 application/json 的消息,然后通过 JSON.parse 方法进行反序列化获得对应的数据,同时把对应 jsonString 的内容显示在 div#target 元素内。


在图 3 中,当我们把可拖拽的元素,拖拽至自定义的释放目标时,会产生高亮效果,具体如下图所示:



这个效果是利用 transmat 这个库提供的 TransmatObserver 类来实现,该类可以帮助我们响应用户的拖拽行为,具体的使用方式如下所示:


const obs = new TransmatObserver((entries) => {
for (const entry of entries) {
const transmat = new Transmat(entry.event);
if (transmat.hasType("application/json")) {
entry.target.classList.toggle("drag-active", entry.isActive);
entry.target.classList.toggle("drag-over", entry.isTarget);
}
}
});
obs.observe(target);

第一次看到 TransmatObserver 之后,阿宝哥立马想到了 MutationObserver API,因为它们都是观察者且拥有类似的 API。利用 MutationObserver API 我们可以监视 DOM 的变化。DOM 的任何变化,比如节点的增加、减少、属性的变动、文本内容的变动,通过这个 API 我们都可以得到通知。如果你对该 API 感兴趣的话,可以阅读 是谁动了我的 DOM? 这篇文章。


现在我们已经知道 transmat 这个库如何使用,接下来阿宝哥将带大家一起来分析这个库背后的工作原理。



Transmat 使用示例:Transmat Demo


gist.github.com/semlinker/c…



三、Transmat 源码分析


transmat 源码分析环节,因为在前面实战部分,我们使用到了 addListenersTransmatTransmatObserver 这三个 “函数” 来实现核心的功能,所以接下来的源码分析,我们将围绕它们展开。这里我们先来分析 addListeners 函数。


3.1 addListeners 函数


addListeners 函数用于设置监听器,调用该函数后会返回一个用于移除事件监听的函数。在分析函数时,阿宝哥习惯先分析函数的签名:


// src/transmat.ts
function addListeners<T extends Node>(
target: T,
type: TransferEventType,
listener: (event: DataTransferEvent, target: T) => void,
options = {dragDrop: true, copyPaste: true}
): () => void

通过观察以上的函数签名,我们可以很直观的了解该函数的输入和输出。该函数支持以下 4 个参数:



  • target:表示监听的目标,它的类型是 Node 类型。

  • type:表示监听的类型,该参数的类型 TransferEventType 是一个联合类型 —— 'transmit' | 'receive'

  • listener:表示事件监听器,它支持的事件类型为 DataTransferEvent,该类型也是一个联合类型 —— DragEvent | ClipboardEvent,即支持拖拽事件和剪贴板事件。

  • options:表示配置对象,用于设置是否允许拖拽和复制、粘贴操作。


addListeners 函数体中,主要包含以下 3 个步骤:



  • 步骤 ①:根据 isTransmitEventoptions.copyPaste 的值,注册剪贴板相关的事件。

  • 步骤 ②:根据 isTransmitEventoptions.dragDrop 的值,注册拖拽相关的事件。

  • 步骤 ③:返回函数对象,用于移除已注册的事件监听。


// src/transmat.ts
export function addListeners<T extends Node>(
target: T,
type: TransferEventType, // 'transmit' | 'receive'
listener: (event: DataTransferEvent, target: T) => void,
options = {dragDrop: true, copyPaste: true}
): () => void {
const isTransmitEvent = type === 'transmit';
let unlistenCopyPaste: undefined | (() => void);
let unlistenDragDrop: undefined | (() => void);

if (options.copyPaste) {
// ① 可拖拽源监听cut和copy事件,可释放目标监听paste事件
const events = isTransmitEvent ? ['cut', 'copy'] : ['paste'];
const parentElement = target.parentElement!;
unlistenCopyPaste = addEventListeners(parentElement, events, event => {
if (!target.contains(document.activeElement)) {
return;
}
listener(event as DataTransferEvent, target);

if (event.type === 'copy' || event.type === 'cut') {
event.preventDefault();
}
});
}

if (options.dragDrop) {
// ② 可拖拽源监听dragstart事件,可释放目标监听dragover和drop事件
const events = isTransmitEvent ? ['dragstart'] : ['dragover', 'drop'];
unlistenDragDrop = addEventListeners(target, events, event => {
listener(event as DataTransferEvent, target);
});
}

// ③ 返回函数对象,用于移除已注册的事件监听
return () => {
unlistenCopyPaste && unlistenCopyPaste();
unlistenDragDrop && unlistenDragDrop();
};
}

以上代码的事件监听最终是通过调用 addEventListeners 函数来实现,在该函数内部会循环调用 addEventListener 方法来添加事件监听。以前面 Transmat 的使用示例为例,在对应的事件处理回调函数内部,我们会以 event 事件对象为参数,调用 Transmat 构造函数创建 Transmat 实例。那么该实例有什么作用呢?要搞清楚它的作用,我们就需要来了解 Transmat 类。


3.2 Transmat 类


Transmat 类被定义在 src/transmat.ts 文件中,该类的构造函数含有一个类型为 DataTransferEvent 的参数 event


// src/transmat.ts
export class Transmat {
public readonly event: DataTransferEvent;
public readonly dataTransfer: DataTransfer;

// type DataTransferEvent = DragEvent | ClipboardEvent;
constructor(event: DataTransferEvent) {
this.event = event;
this.dataTransfer = getDataTransfer(event);
}
}

Transmat 构造函数内部还会通过 getDataTransfer 函数来获取 DataTransfer 对象并赋值给内部的 dataTransfer 属性。DataTransfer 对象用于保存拖动并放下(drag and drop)过程中的数据。它可以保存一项或多项数据,这些数据项可以是一种或者多种数据类型。


下面我们来看一下 getDataTransfer 函数的具体实现:


// src/data_transfer.ts
export function getDataTransfer(event: DataTransferEvent): DataTransfer {
const dataTransfer =
(event as ClipboardEvent).clipboardData ??
(event as DragEvent).dataTransfer;
if (!dataTransfer) {
throw new Error('No DataTransfer available at this event.');
}
return dataTransfer;
}

在以上代码中,使用了空值合并运算符 ??。该运算符的特点是:当左侧操作数为 null 或 undefined 时,其返回右侧的操作数,否则返回左侧的操作数。即先判断是否为剪贴板事件,如果是的话就会从 clipboardData 属性获取 DataTransfer 对象。否则,就会从 dataTransfer 属性获取。


对于可拖拽源,在创建完 Transmat 对象之后,我们就可以调用该对象上的 setData 方法保存一项或多项数据。比如,在以下代码中,我们设置了不同类型的多项数据:


transmat.setData({
"text/plain": "大家好,我是阿宝哥!",
"text/html": `
<h1>大家好,我是阿宝哥</h1>
...
`,
"text/uri-list": "https://juejin.cn/user/764915822103079",
"application/json": {
name: "阿宝哥",
wechat: "semlinker",
},
});

了解完 setData 方法的用法之后,我们来看一下它的具体实现:


// src/transmat.ts
setData(
typeOrEntries: string | {[type: string]: unknown},
data?: unknown
): void {
if (typeof typeOrEntries === 'string') {
this.setData({[typeOrEntries]: data});
} else {
// 处理多种类型的数据
for (const [type, data] of Object.entries(typeOrEntries)) {
const stringData =
typeof data === 'object' ? JSON.stringify(data) : `${data}`;
this.dataTransfer.setData(normalizeType(type), stringData);
}
}
}

由以上代码可知,在 setData 方法内部最终会调用 dataTransfer.setData 方法来保存数据。dataTransfer 对象的 setData 方法支持两个字符串类型的参数:formatdata。它们分别表示要保存的数据格式和实际的数据。如果给定数据格式不存在,则将对应的数据保存到末尾。如果给定数据格式已存在,则将使用新的数据替换旧的数据


下图是 dataTransfer.setData 方法的兼容性说明,由图可知主流的现代浏览器都支持该方法。



(图片来源:caniuse.com/mdn-api_dat…


Transmat 类除了拥有 setData 方法之外,它也含有一个 getData 方法,用于获取已保存的数据。getData 方法支持一个字符串类型的参数 type,用于表示数据的类型。在获取数据前,会调用 hasType 方法判断是否含有该类型的数据。如果有包含的话,就会通过 dataTransfer 对象的 getData 方法来获取该类型对应的数据。


// src/transmat.ts
getData(type: string): string | undefined {
return this.hasType(type)
? this.dataTransfer.getData(normalizeType(type))
: undefined;
}

此外,在调用 getData 方法前,还会调用 normalizeType 函数,对传入的 type 类型参数进行标准化操作。具体的如下所示:


// src/data_transfer.ts
export function normalizeType(input: string) {
const result = input.toLowerCase();
switch (result) {
case 'text':
return 'text/plain';
case 'url':
return 'text/uri-list';
default:
return result;
}
}

同样,我们也来看一下 dataTransfer.getData 方法的兼容性:



(图片来源:caniuse.com/mdn-api_dat…


好的,Transmat 类中的 setDatagetData 这两个核心方法就先介绍到这里。接下来我们来介绍另一个类 —— TransmatObserver 。


3.3 TransmatObserver 类


TransmatObserver 类的作用是可以帮助我们响应用户的拖拽行为,可用于在拖拽过程中高亮放置区域。比如,在前面的示例中,我们通过以下方式来实现放置区域的高亮效果:


const obs = new TransmatObserver((entries) => {
for (const entry of entries) {
const transmat = new Transmat(entry.event);
if (transmat.hasType("application/json")) {
entry.target.classList.toggle("drag-active", entry.isActive);
entry.target.classList.toggle("drag-over", entry.isTarget);
}
}
});
obs.observe(target);

同样,我们先来分析一下 TransmatObserver 类的构造函数:


// src/transmat_observer.ts
export class TransmatObserver {
private readonly targets = new Set<Element>(); // 观察的目标集合
private prevRecords: ReadonlyArray<TransmatObserverEntry> = []; // 保存前一次的记录
private removeEventListeners = () => {};

constructor(private readonly callback: TransmatObserverCallback) {}
}

由以上代码可知,TransmatObserver 类的构造函数支持一个类型为 TransmatObserverCallback 的参数 callback,该参数对应的类型定义如下:


// src/transmat_observer.ts
export type TransmatObserverCallback = (
entries: ReadonlyArray<TransmatObserverEntry>,
observer: TransmatObserver
) => void;

TransmatObserverCallback 函数类型接收两个参数:entriesobserver。其中 entries 参数的类型是一个


只读数组(ReadonlyArray),数组中每一项的类型是 TransmatObserverEntry,对应的类型定义如下:


// src/transmat_observer.ts
export interface TransmatObserverEntry {
target: Element;
/** type DataTransferEvent = DragEvent | ClipboardEvent */
event: DataTransferEvent;
/** Whether a transfer operation is active in this window. */
isActive: boolean;
/** Whether the element is the active target (dragover). */
isTarget: boolean;
}

在前面 transmat-target 的示例中,当创建完 TransmatObserver 实例之后,就会调用该实例的 observe 方法并传入待观察的对象。observe 方法的实现并不复杂,具体如下所示:


// src/transmat_observer.ts
observe(target: Element) {
/** private readonly targets = new Set<Element>(); */
this.targets.add(target);
if (this.targets.size === 1) {
this.addEventListeners();
}
}

observe 方法内部,会把需观察的元素保存到 targets Set 集合中。当 targets 集合的大小等于 1 时,就会调用当前实例的 addEventListeners 方法来添加事件监听:


// src/transmat_observer.ts
private addEventListeners() {
const listener = this.onTransferEvent as EventListener;
this.removeEventListeners = addEventListeners(
document,
['dragover', 'dragend', 'dragleave', 'drop'],
listener,
true
);
}

在私有的 addEventListeners 方法内部,会利用我们前面介绍的 addEventListeners 函数来为 document 元素批量添加与拖拽相关的事件监听。而对应的事件说明如下所示:



  • dragover:当元素或选中的文本被拖到一个可释放目标上时触发;

  • dragend:当拖拽操作结束时触发(比如松开鼠标按键);

  • dragleave:当拖拽元素或选中的文本离开一个可释放目标时触发;

  • drop:当元素或选中的文本在可释放目标上被释放时触发。


其实与拖拽相关的事件并不仅仅只有以上四种,如果你对完整的事件感兴趣的话,可以阅读 MDN 上 HTML 拖放 API 这篇文章。下面我们来重点分析 onTransferEvent 事件监听器:


private onTransferEvent = (event: DataTransferEvent) => {
const records: TransmatObserverEntry[] = [];
for (const target of this.targets) {
// 当光标离开浏览器时,对应的事件将会被派发到body或html节点
const isLeavingDrag =
event.type === 'dragleave' &&
(event.target === document.body ||
event.target === document.body.parentElement);

// 页面上是否有拖拽行为发生
// 当拖拽操作结束时触发dragend事件
// 当元素或选中的文本在可释放目标上被释放时触发drop事件
const isActive = event.type !== 'drop'
&& event.type !== 'dragend' && !isLeavingDrag;

// 判断可拖拽的元素是否被拖到target元素上
const isTargetNode = target.contains(event.target as Node);
const isTarget = isActive && isTargetNode
&& event.type === 'dragover';

records.push({
target,
event,
isActive,
isTarget,
});
}

// 仅当记录发生变化的时候,才会调用回调函数
if (!entryStatesEqual(records, this.prevRecords)) {
this.prevRecords = records as ReadonlyArray<TransmatObserverEntry>;
this.callback(records, this);
}
}

在以上代码中,使用了 node.contains(otherNode) 方法来判断可拖拽的元素是否被拖到 target 元素上。当 otherNodenode 的后代节点或者 node 节点本身时,返回 true,否则返回 false。此外,为了避免频繁地触发回调函数,在调用回调函数前会先调用 entryStatesEqual 函数来检测记录是否发生变化。entryStatesEqual 函数的实现比较简单,具体如下所示:


// src/transmat_observer.ts
function entryStatesEqual(
a: ReadonlyArray<TransmatObserverEntry>,
b: ReadonlyArray<TransmatObserverEntry>
): boolean {
if (a.length !== b.length) {
return false;
}
// 如果有一项不匹配,则立即返回false。
return a.every((av, index) => {
const bv = b[index];
return av.isActive === bv.isActive && av.isTarget === bv.isTarget;
});
}

MutationObserver 一样,TransmatObserver 也提供了用于获取最近已触发记录的 takeRecords 方法和用于 “断开” 连接的 disconnect 方法:


// 返回最近已触发记录
takeRecords() {
return this.prevRecords;
}

// 移除所有目标及事件监听器
disconnect() {
this.targets.clear();
this.removeEventListeners();
}

到这里 Transmat 源码分析的相关内容已经介绍完了,如果你对该项目感兴趣的话,可以自行阅读该项目的完整源码。该项目是使用 TypeScript 开发,已入门 TypeScript 的小伙伴可以利用该项目巩固一下所学的 TS 知识及 OOP 面向对象的设计思想。



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

收起阅读 »

JS循环大总结, for, forEach,for in,for of, map区别

map(数组方法): 特性: map不改变原数组但是会 返回新数组 可以使用break中断循环,可以使用return返回到外层函数 实例: let newarr=arr.map(i=>{ return i+=1; console.log(i); })...
继续阅读 »

map(数组方法):


特性:



  1. map不改变原数组但是会 返回新数组

  2. 可以使用break中断循环,可以使用return返回到外层函数


实例:


let newarr=arr.map(i=>{
return i+=1;
console.log(i);
})
console.log(arr)//1,3,4---不会改变原数组
console.log(newarr)//[2,4,5]---返回新数组

forEach(数组方法):


特性:



  1. 便利的时候更加简洁,效率和for循环相同,不用关心集合下标的问题,减少了出错的概率。

  2. 没有返回值

  3. 不能使用break中断循环,不能使用return返回到外层函数


实例:


let newarr=arr.forEach(i=>{
i+=1;
console.log(i);//2,4,5
})
console.log(arr)//[1,3,4]
console.log(newarr)//undefined

注意:



  1. forEach() 对于空数组是不会执行回调函数的。

  2. for可以用continue跳过循环中的一个迭代,forEach用continue会报错。

  3. forEach() 需要用 return 跳过循环中的一个迭代,跳过之后会执行下一个迭代。


for in(大部分用于对象):


用于循环遍历数组或对象属性


特性:


可以遍历数组的键名,遍历对象简洁方便
###实例:


   let person={name:"小白",age:28,city:"北京"}
let text=""
for (let i in person){
text+=person[i]
}
输出结果为:小白28北京
//其次在尝试一些数组
let arry=[1,2,3,4,5]
for (let i in arry){
console.log(arry[i])
}
//能输出出来,证明也是可以的

for of(不能遍历对象):


特性:



  1. (可遍历map,object,array,set string等)用来遍历数据,比如组中的值

  2. 避免了for in的所有缺点,可以使用break,continue和return,不仅支持数组的遍历,还可以遍历类似数组的对象。


   let arr=["nick","freddy","mike","james"];
for (let item of arr){
console.log(item)
}
//暑促结果为nice freddy mike james
//遍历对象
let person={name:"老王",age:23,city:"唐山"}
for (let item of person){
console.log(item)
}
//我们发现它是不可以的
//但是它和forEach有个解决方法,结尾介绍

总结:



  • forEach 遍历列表值,不能使用 break 语句或使用 return 语句

  • for in 遍历对象键值(key),或者数组下标,不推荐循环一个数组

  • for of 遍历列表值,允许遍历 Arrays(数组), Strings(字符串), Maps(映射), Sets(集合)等可迭代的数据结构等.在 ES6 中引入的 for of 循环,以替代 for in 和 forEach() ,并支持新的迭代协议。

  • for in循环出的是key,for of循环出的是value;

  • for of是ES6新引入的特性。修复了ES5的for in的不足;

  • for of不能循环普通的对象,需要通过和Object.keys()搭配使用。


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

收起阅读 »

Vue3+Vite+TS+Eslint(Airbnb规则)搭建生产项目,踩坑详记(三):引入Element-plus,解决字体文件404问题

vue
今天我们来看引入大杯Element,其实引入很简单,跟着文档操作就完事了。所以这篇文章重点是看如何修改主题以及在修改主题中我遇到的问题。 废话少说,开整! 引入Element-plus npm install element-plus --save // m...
继续阅读 »

今天我们来看引入大杯Element,其实引入很简单,跟着文档操作就完事了。所以这篇文章重点是看如何修改主题以及在修改主题中我遇到的问题。


image.png


废话少说,开整!


引入Element-plus


npm install element-plus --save

// main.ts
import { createApp } from 'vue';
import ElementPlus from 'element-plus'; // ++
import 'element-plus/lib/theme-chalk/index.css'; // ++
import App from './App.vue';

createApp(App).use(ElementPlus).mount('#app'); // edit

此时在项目中引入Element组件测试发现,已经可以正常使用了。


image.png


修改Element主题


在Element文档中有如何修改主题的教程,我们项目中主要的需求就是修改主题色,因此本文也以修改主题色为例子。


创建文件


首先我新增了两个文件,color.sass 和 element-theme.sass(这里假设你的项目已经引入了sass)。之所以创建两个文件,是因为 color.sass 除了给element主题提供颜色配置,还会引入为全局变量,方便在组件中使用。


image.png


配置主题


// color.sass
$--color-primary: red

// element-theme.sass
@improt "./color.sass" // 引入主题色文件

$--font-path: '~element-plus/lib/theme-chalk/fonts'
@import "~element-plus/packages/theme-chalk/src/index"

// main.ts
import { createApp } from 'vue';
import ElementPlus from 'element-plus';
import 'element-plus/lib/theme-chalk/index.css'; // --
import './styles/element-theme.sass'; // ++
import App from './App.vue';

createApp(App).use(ElementPlus).mount('#app');

如上按照element官网给的例子引入以后,在vite项目中会报错。


image.png
这是因为 ~ 这种路径写法是vue-cli中的约定,它使我们可以引用node_modules包内的资源,详见文档:URL转换规则
image.png


所以我们在这里需要把路径 ~element-plus 改成 node_modules/element-plus。也就是文件变成了这个样子:


@import "./color.sass"

$--font-path: 'node_modules/element-plus/lib/theme-chalk/fonts'
@import "node_modules/element-plus/packages/theme-chalk/src/index"

文章写到这里的时候遇到了一个尴尬的问题,在我们的生产项目搭建框架时,路径改成由 node_modules 引入后,主题色修改没有问题,可以生效。但是fonts文件加载请求报404,如下图1。但是文章用的实验项目,同样的方式修改后,一切正常,如图2。


image.png


image.png


经过反复测试后发现,是因为生产项目配置了多入口,启动项目时对应了不同的入口文件,导致引入fonts文件报错。具体原因有待研究,希望了解的兄弟不吝赐教


关于多入口文件配置以及解决element字体引入的问题,后边会有一篇文章单独介绍,这里就先不剧透了。


配置全局变量


前面单独创建了一个color.sass是为了将文件里的颜色变量引入到全局,方便在组件中使用。
为了简化使用,我们可以在文件中为常用颜色额外定义简短变量,但是要注意,不能修改element需要的变量!


$--color-primary: #ff0000

$primary: #ff0000

引入全局变量需要在vite.config.ts文件中配置css预处理器,并将引入的变量文件传给预处理器。配置方式如下


// vite.config.ts
...
export default defineConfig({
...
css: {
preprocessorOptions: {
sass: {
// \n 处理文件中多个引入报换行错误的问题
additionalData: "@import './src/styles/color.sass'\n",
},
},
},
});

引入后我们在组件内进行测试


// HelloWorld.vue
<style scoped lang="sass">
a
color: $primary
</style>

可以看到页面上已经生效了
image.png


因为通过这种方式插入全局变量,会为所有的.sass文件都插入对应的文件引入,所以在前面我们定义的 element-theme.sass 文件中就可以不写 color.sass 文件的引入了。


// element-theme.sass

// @import "./color.sass" // edit

$--font-path: 'node_modules/element-plus/lib/theme-chalk/fonts'
@import "node_modules/element-plus/packages/theme-chalk/src/index"

修改默认语言


可能是为了立足中国,走向世界。使用组件时会发现大杯Element的默认语言变成了英文,我们需要自己引入并修改默认语言为中文。


// main.ts
import { createApp } from 'vue';
import ElementPlus from 'element-plus';
import './styles/element-theme.sass';
import locale from 'element-plus/lib/locale/lang/zh-cn'; // ++
import App from './App.vue';

createApp(App).use(ElementPlus, { locale }).mount('#app'); // edit

修改完成后,再去看看组件,是不是已经变成了中文。


引入大杯Element并修改主题的工作已经完成了,项目中我们就可以使用自定义主题色的Element组件。并且抛出了主题色全局变量,方便我们在组件中使用。


下次我们将一次性引入Vuex和Vue Router,这两项工作完成后,就已经完成了项目框架的雏形,可以开始开发了。不过后续我们仍然会有一些优化以及Vue3开发过程中相较于Vue2有较大变化的方法总结,整理不易,希望大家多多支持。


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

收起阅读 »

Vue3+Vite+TS+Eslint(Airbnb规则)搭建生产项目,踩坑详记(二):配置husky和lint-staged

vue
上回我们说到配置ESLint以及添加vue-recommended、airbnb-base、prettier规则,一切都很顺利。唯有一点需要注意的就是 .eslintrc 文件extends配置项中,plugin:prettier/recommended一定要...
继续阅读 »

上回我们说到配置ESLint以及添加vue-recommended、airbnb-base、prettier规则,一切都很顺利。唯有一点需要注意的就是 .eslintrc 文件extends配置项中,plugin:prettier/recommended一定要在airbnb-base之后添加,上篇文章没有看到的童鞋们可以回去看看原因。


上篇文章最后我们提到,在开发阶段进行ESLint校验,效果是一件靠自觉的事。因此我们需要在代码提交前再次执行ESLint,加强校验力度以保证Git上得到的都是优美的代码。


我们本次需要用到的工具有两个:huskylint-staged


husky


它的主要作用就是关联git的钩子函数,在执行相关git hooks时进行自定义操作,比如在提交前执行eslint校验,提交时校验commit message等等。


Install


husky官网推荐使用自动初始化命令,因为我们就按照官网推荐的方式进行安装,以npm为例


// && 连接符在vscode中会报错,建议在windows的powershell执行
npx husky-init && npm install

执行完成后,项目根目录会多出来 .husky 文件夹。


image.png


内部的_文件夹我们在此无需关心,pre-commit文件便是在git提交前会执行的操作,如图。我们可以在当前目录创建钩子文件来完成我们想要的操作。


image.png


需要注意的是,新版husky的配置方式做出了破坏性的改变,如果在使用过程中发现配置完以后没有生效,可以注意查看一下安装版本


升级方式可以查看官方文档:typicode.github.io/husky/#/?id…


配置


我们想要在提交前执行eslint校验代码,因此修改husky的pre-commit文件即可。我们在文件中添加如下代码


#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

eslint . --ext .js,.ts,.vue --fix #++ 校验所有的.js .ts .vue文件,并修复可自动修复的问题
git add . #++ 用于将自动修复后改变的文件添加到暂存区
exit 1 #++ 终止命令,用来测试钩子

此时提交代码执行commit是可以看到已经进入了pre-commit文件执行命令。但是会报错


image.png


这是因为此处执行shell命令,需要我们全局安装eslint。执行 npm install -g eslint。
安装完成后再次执行git commit,可以看到已经可以正常运行了


image.png


错误处理




  • 截图中第一个报错是书写错误,直接改掉就好。




  • 第二个错误,是因为我们的ESLint中没有配置TS的解析器,导致ESLint不能正常识别并校验TS代码。解决它,我们安装 @typescript-eslint/parser,并修改ESLint配置即可。




npm install @typescript-eslint/parser --save-dev

// .eslintrc.js
...
parserOptions: {
ecmaVersion: 12,
parser: '@typescript-eslint/parser', // ++
},
...


  • 第三个错误,它说的是我们引入的vite和@vitejs/plugin-vue两个包在 package.json 中应该是dependencies而不是devDependencies依赖。这个错误是因为airbnb-base规则设置了不允许引入开发依赖包,但是很明显我们不应该修改这两个框架生成的依赖结构。那我们看一下airbnb关于这条规则的定义


image.png
可以看到,airbnb对这条规格做了列外处理,那就很好办了,我们只需要在它的基础上,添加上上面报错的两个包。


在eslint中添加如下规则:


// .eslintrc.js
...
rules: {
...
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: [
... // 保持airbnb-base中的规则不变
'**vite**', // ++
'**@vitejs**', // ++
],
optionalDependencies: false,
},
],
}
...

修改完上述错误后,我们去掉 .husky/pre-commit 文件中 exit 1 这行代码,再次执行提交操作,可以看到,已经可以提交成功了。


image.png


思考


通过配置husky,我们已经实现了在提交前对代码进行检查。但是eslint配置的是 eslint . --ext .js,.ts,.vue --fix,检查所有的js、ts、vue文件,随着项目代码越来越多,每次提交前校验所有代码显然是不现实的。所以需要一个办法每次只检查新增或修改的文件。


这就需要开头提到的第二个工具来祝我们一臂之力了。


lint-staged


lint-staged的作用就是对暂存区的文件执行lint,可以让我们每次提交时只校验自己修改的文件。


npm install lint-staged --save-dev

配置lint-staged


安装完成后,在package.json文件中添加lint-staged的配置


// package.json
...
"scripts": {
...
"lint-staged": "lint-staged"
},
"lint-staged": {
// 校验暂存区的ts、js、vue文件
"*.{ts,js,vue}": [
"eslint --fix",
"git add ."
]
}

添加scripts里的lint-staged命令,是因为不建议全局安装lint-staged,以防在其他同学电脑上没有全局安装导致运行报错。


修改husky


添加lint-staged配置后,husky就不在需要直接调用eslint了。修改pre-commit文件如下:


#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

# eslint . --ext .js,.ts,.vue --fix
# git add .
# exit 1
npm run lint-staged

lint-staged配置后,我们不再需要配置husky时全局安装的eslint,因为lint-staged可以检测项目里局部安装的脚本。同时,不建议全局安装脚本,原因同上。


测试


到此,提交阶段对代码执行lint需要的配置我们已经完成了。再次提交代码测试,可以看到commit后执行的命令已经变成了lint-staged。


image.png


下一篇踩坑记我们将引入Element-plus,详细介绍其中遇到的问题,并修改element组件主题。


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

Vue3+Vite+TS+Eslint(Airbnb规则)搭建生产项目,踩坑详记(一)

vue
前段时间领导告知公司将开启一个全新的项目。 从零开始,如果不尝试一下最近火热的 Vue3 + Vite 岂不是白白浪费了这么好的吃螃蟹的机会。 说干就干,然后就开始读各种文档,从 0 开始,一步一步搭完这个项目到可以正常开发,这对于我一个第一次搭生产项目的菜鸡...
继续阅读 »

前段时间领导告知公司将开启一个全新的项目。


从零开始,如果不尝试一下最近火热的 Vue3 + Vite 岂不是白白浪费了这么好的吃螃蟹的机会。


说干就干,然后就开始读各种文档,从 0 开始,一步一步搭完这个项目到可以正常开发,这对于我一个第一次搭生产项目的菜鸡来说,着实艰难。


到今天,项目已经进入联调阶段,并且已经在环境上部署成功可以正常访问。这个实验也算是有了阶段性的成功吧,因此来写文章记录此次Vue3项目搭建历险记。


下载.jfif


第一篇文章主要是项目初始化和ESLint导入,废话不多说,开整。


初始化项目


image.png
按照自己需要的框架选择就可以了,我这里用的Vue3+TS。
初始化完成后的目录结构如下:


image.png


启动项目


执行 npm run dev,大概率会直接报错,因为项目默认启动在3000端口,可能会被拒绝。


image.png


解决这个问题,我们需要在根目录下的 vite.config.ts 文件中修改开发服务器的配置,手动指定端口号。


image.png


修改完成后重新启动项目,就可以访问了。


image.png


添加ESLint支持


安装ESLint



  • eslint只有开发阶段需要,因此添加到开发阶段的依赖中即可


npm install eslint --save-dev


  • 在VS Code中安装eslint插件,以在开发中自动进行eslint校验


配置ESLint


创建 .eslintrc.js 文件


添加基础配置


module.exports = {
root: true,
env: {
browser: true, // browser global variables
es2021: true, // adds all ECMAScript 2021 globals and automatically sets the ecmaVersion parser option to 12.
},
parserOptions: {
ecmaVersion: 12,
},
}

引入规则


为了规范团队成员代码格式,以及保持统一的代码风格,项目采用当前业界最火的 Airbnb规范 ,并引入代码风格管理工具 Prettier


eslint-plugin-vue


ESLint官方提供的Vue插件,可以检查 .vue文件中的语法错误


npm install eslint-plugin-vue

// .eslintrc.js
...
extends: [
'plugin:vue/vue3-recommended' // ++
]
...

eslint-config-airbnb-base


Airbnb基础规则的eslint插件


// npm version > 5
npx install-peerdeps --dev eslint-config-airbnb-base

// .eslintrc.js
...
extends: [
'plugin:vue/vue3-recommended',
'airbnb-base', // ++
],
...

这个时候就应该可以看到一些项目原有代码的eslint报错信息了,如果没有的话,可以尝试重启编辑器的eslint服务。


eslint-plugin-prettier


本次项目不单独引入prettier,而是使用eslint插件将prettier作为eslint规则执行。


npm install --save-dev eslint-plugin-prettier
npm install --save-dev --save-exact prettier

// .eslintrc.js
...
plugins: ['prettier'], // ++
rules: {
'prettier/prettier': 'error', // ++
},
...

配置到此时,大概率会遇到 eslint 规则和 prettier 规则冲突的情况,比如下图。eslint告诉我们要使用单引号,但是改为单引号以后,prettier有告诉我们要使用双引号。


image.png


image.png


这时候就需要另一个eslint的插件 eslint-config-prettier,这个插件的作用是禁用所有与格式相关的eslint规则,也就是说把所有格式相关的校验都交给 prettier 处理。


npm install --save-dev eslint-config-prettier

// .eslintrc.js
...
plugins: ['prettier'],
extends: [
'plugin:vue/vue3-recommended',
'airbnb-base',
'plugin:prettier/recommended', // ++
],
rules: {
'prettier/prettier': 'error',
},
...

plugin:prettier/recommended 的配置需要注意的是,一定要放在最后。因为extends中后引入的规则会覆盖前面的规则。


我们还可以在根目录新建 .prettierrc.js 文件自定义 prettier 规则,保存规则后,重启编辑器的eslint服务以更新编辑器读取的配置文件。


// .prettierrc.js
module.exports = {
singleQuote: true, // 使用单引号
}

到此,我们的ESLint基本配置结束了,后续需要时可以对规则进行调整。


这篇文章到这里就结束了,但是只在开发阶段约束代码风格是一件靠自觉性的是,因为我们还需要增强ESLint的约束度。下一篇文章,我们一起研究如果在提交代码前进行ESLint二次校验,保证提交到Git的代码都是符合规定的~


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

收起阅读 »

有趣的JS存储

今天给大家分享一下关于JS存储的问题。 建议阅读时间:5-10分钟。 序章 首先看一道经典的关于JS存储的题目,来一场紧张又刺激的脑内吃鸡大战吧: var a = {n:1};a.x = a = {n:2};console.log(a.x);console....
继续阅读 »

今天给大家分享一下关于JS存储的问题。


建议阅读时间:5-10分钟。




序章


首先看一道经典的关于JS存储的题目,来一场紧张又刺激的脑内吃鸡大战吧:


var a = {n:1};
a.x = a = {n:2};
console.log(a.x);
console.log(a);·


问输出?
想必大家心中都有答案了 ...
结果很显然是有趣的,


image.png


到这里有部分现场观众朋友就问了,这特喵咋undefined?不是赋值了吗?别急先别骂人,往下看:




探索时刻


我们先将代码这样修改:


a.x = a = {n:2};   ---- >  a = a.x = {n:2};

image.png


结果显然是一致的,不论是先给 a 赋值还是先给 a.x 赋值结果都是一致的,
查了一些资料后,得知这等式中 . 的优先级别是最高的,


因此这题的思路:


JS会把变量存到栈中,而对象则会存在堆中。


image.png



  1. 第一行代码:变量 a 的指针指向堆栈;

  2. 第二行代码:a.x = a = {n:2}; 堆1中的变量对像X指向堆2 { n:2 }, 接着给a赋值 a={n:2} ,a的指针被改变指向堆2,然后堆1没有被指针指向,被GC回收,因此输出的 a.x 是underfinde 而 a 的值是 {n:2};


理解上述代码只需要稍微理解一下js变量储存:


大家都知道,JavaScript中的变量类型分为两种,一种是基本数据类型,另外一种就是引用类型


两种数据类型的存储方式在JS中也有所不同。


另外,内存分为栈区(stack)和堆区(heap),然后在JS中开发人员并不能直接操作堆区,堆区数据由JS引擎操作完成,那这二者在存储数据上到底有什么区别呢?




揭晓时刻


一幅图告诉你:


image.png


 JS中变量的定义在内存中包括三个部分:



  • 变量标示  (比如上图中的Str,变量标示存储在内存的栈区)

  • 变量值   (比如上面中的Str的值souvenir或者是obj1对象的指向堆区地址,这个值也是存储在栈区)

  • 对象   (比如上图中的对象1或者对象2,对象存储在堆区)


也就是说,对于基本数据类型来说,只使用了内存的栈区。
我们再做一个有趣的改动:


var a = {n:1};
var b=a;
a.x = a = {n:2};
console.log(a.x);
console.log(a);
console.log(b);
console.log(b.x);

可以看到我们并没有对 b 进行操作但是 b.x 等于{n:2},这是一个被操作过的值,就如上述可知 b的指针指向堆1,所以堆没有被回收,而被显示出来了 ~


从这么一个简单例子,你是否对JS存储机制有了新的认识呢 ~


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

收起阅读 »

petite-vue源码分析:无虚拟DOM的极简版Vue

vue
最近发现Vue增加了一个petite-vue的仓库,大概看了一下,这是一个无虚拟DOM的mini版Vue,前身貌似是vue-lite(瞎猜的~),主要用于在服务端渲染的HTML页面中上"sprinkling"(点缀)一些Vue式的交互。颇有意思,于是看了下源码...
继续阅读 »

最近发现Vue增加了一个petite-vue的仓库,大概看了一下,这是一个无虚拟DOM的mini版Vue,前身貌似是vue-lite(瞎猜的~),主要用于在服务端渲染的HTML页面中上"sprinkling"(点缀)一些Vue式的交互。颇有意思,于是看了下源码(v0.2.3),整理了本文。



起步


开发调试环境


整个项目的开发环境非常简单


git clone git@github.com:vuejs/petite-vue.git

yarn

# 使用vite启动
npm run dev

# 访问http://localhost:3000/

(不得不说,用vite来搭开发环境还是挺爽的~


新建一个测试文件exmaples/demo.html,写点代码


<script type="module">
import { createApp, reactive } from '../src'

createApp({
msg: "hello"
}).mount("#app")
</script>

<div id="app">
<h1>{{msg}}</h1>
</div>

然后访问http://localhost:3000/demo.html即可


目录结构


从readme可以看见项目与标准vue的一些差异



  • Only ~5.8kb,体积很小

  • Vue-compatible template syntax,与Vue兼容的模板语法

  • DOM-based, mutates in place,基于DOM驱动,就地转换

  • Driven by @vue/reactivity,使用@vue/reactivity驱动


目录结构也比较简单,使用ts编写,外部依赖基本上只有@vue/reactivity



核心实现


createContext


从上面的demo代码可以看出,整个项目从createApp开始。


export const createApp = (initialData?: any) => {
// root context
const ctx = createContext()
if (initialData) {
ctx.scope = reactive(initialData) // 将初始化数据代理成响应式
}
// app的一些接口
return {
directive(name: string, def?: Directive) {},
mount(el?: string | Element | null) {},
unmount() {}
}
}

关于Vue3中的reactive,可以参考之前整理的:Vue3中的数据侦测reactive,这里就不再展开了。


createApp中主要是通过createContext创建根context,这个上下文现在基本不陌生了,来看看createContext


export const createContext = (parent?: Context): Context => {
const ctx: Context = {
...parent,
scope: parent ? parent.scope : reactive({}),
dirs: parent ? parent.dirs : {}, // 支持的指令
effects: [],
blocks: [],
cleanups: [],
// 提供注册effect回调的接口,主要使用调度器来控制什么时候调用
effect: (fn) => {
if (inOnce) {
queueJob(fn)
return fn as any
}
// @vue/reactivity中的effect方法
const e: ReactiveEffect = rawEffect(fn, {
scheduler: () => queueJob(e)
})
ctx.effects.push(e)
return e
}
}
return ctx
}

稍微看一下queueJob就可以发现,还是Vue中熟悉的nextTick实现,



  • 通过一个全局变量queue队列保存回调

  • 在下一个微任务处理阶段,依次执行queue中的每一个回调,然后清空queue


mount


基本使用


createApp().mount("#app")

mount方法最主要的作用就是处理el参数,找到应用挂载的根DOM节点,然后执行初始化流程


mount(el?: string | Element | null) {
let roots: Element[]
// ...根据el参数初始化roots
// 根据el创建Block实例
rootBlocks = roots.map((el) => new Block(el, ctx, true))
return this
}

Block是一个抽象的概念,用于统一DOM节点渲染、插入、移除和销毁等操作。


下图是依赖这个Block的地方,可以看见主要在初始化、iffor这三个地方使用



看一下Block的实现


// src/block.ts
export class Block {
template: Element | DocumentFragment
ctx: Context
key?: any
parentCtx?: Context

isFragment: boolean
start?: Text
end?: Text

get el() {
return this.start || (this.template as Element)
}

constructor(template: Element, parentCtx: Context, isRoot = false) {
// 初始化this.template
// 初始化this.ctx

// 构建应用
walk(this.template, this.ctx)
}
// 主要在新增或移除时使用,可以先不用关心实现
insert(parent: Element, anchor: Node | null = null) {}
remove() {}
teardown() {}
}

这个walk方法,主要的作用是递归节点和子节点,如果之前了解过递归diff,这里应该比较熟悉。但petite-vue中并没有虚拟DOM,因此在walk中会直接操作更新DOM。


export const walk = (node: Node, ctx: Context): ChildNode | null | void => {
const type = node.nodeType
if (type === 1) {
// 元素节点
const el = node as Element
// ...处理 如v-if、v-for
// ...检测属性执行对应的指令处理 applyDirective,如v-scoped、ref等

// 先处理子节点,在处理节点自身的属性
walkChildren(el, ctx)

// 处理节点属性相关的自定,包括内置指令和自定义指令
} else if (type === 3) {
// 文本节点
const data = (node as Text).data
if (data.includes('{{')) {
// 正则匹配需要替换的文本,然后 applyDirective(text)
applyDirective(node, text, segments.join('+'), ctx)
}
} else if (type === 11) {
walkChildren(node as DocumentFragment, ctx)
}
}

const walkChildren = (node: Element | DocumentFragment, ctx: Context) => {
let child = node.firstChild
while (child) {
child = walk(child, ctx) || child.nextSibling
}
}

可以看见会根据node.nodeType区分处理处理



  • 对于元素节点,先处理了节点上的一些指令,然后通过walkChildren处理子节点。

    • v-if,会根据表达式决定是否需要创建Block然后执行插入或移除

    • v-for,循环构建Block,然后执行插入



  • 对于文本节点,替换{{}}表达式,然后替换文本内容


v-if


来看看if的实现,通过branches保存所有的分支判断,activeBranchIndex通过闭包保存当前位于的分支索引值。


在初始化或更新时,如果某个分支表达式结算结果正确且与上一次的activeBranchIndex不一致,就会创建新的Block,然后走Block构造函数里面的walk。


export const _if = (el: Element, exp: string, ctx: Context) => {
const parent = el.parentElement!
const anchor = new Comment('v-if')
parent.insertBefore(anchor, el)

// 存放条件判断的各种分支
const branches: Branch[] = [{ exp,el }]

// 定位if...else if ... else 等分支,放在branches数组中

let block: Block | undefined
let activeBranchIndex: number = -1 // 通过闭包保存当前位于的分支索引值

const removeActiveBlock = () => {
if (block) {
parent.insertBefore(anchor, block.el)
block.remove()
block = undefined
}
}

// 收集依赖
ctx.effect(() => {
for (let i = 0; i < branches.length; i++) {
const { exp, el } = branches[i]
if (!exp || evaluate(ctx.scope, exp)) {
// 当判断分支切换时,会生成新的block
if (i !== activeBranchIndex) {
removeActiveBlock()
block = new Block(el, ctx)
block.insert(parent, anchor)
parent.removeChild(anchor)
activeBranchIndex = i
}
return
}
}
// no matched branch.
activeBranchIndex = -1
removeActiveBlock()
})

return nextNode
}

v-for


for指令的主要作用是循环创建多个节点,这里还根据key实现了类似于diff算法来复用Block的功能


export const _for = (el: Element, exp: string, ctx: Context) => {
// ...一些工具方法如createChildContexts、mountBlock

ctx.effect(() => {
const source = evaluate(ctx.scope, sourceExp)
const prevKeyToIndexMap = keyToIndexMap
// 根据循环项创建多个子节点的context
;[childCtxs, keyToIndexMap] = createChildContexts(source)
if (!mounted) {
// 首次渲染,创建新的Block然后insert
blocks = childCtxs.map((s) => mountBlock(s, anchor))
mounted = true
} else {
// 更新时
const nextBlocks: Block[] = []
// 移除不存在的block
for (let i = 0; i < blocks.length; i++) {
if (!keyToIndexMap.has(blocks[i].key)) {
blocks[i].remove()
}
}
// 根据key进行处理
let i = childCtxs.length
while (i--) {
const childCtx = childCtxs[i]
const oldIndex = prevKeyToIndexMap.get(childCtx.key)
const next = childCtxs[i + 1]
const nextBlockOldIndex = next && prevKeyToIndexMap.get(next.key)
const nextBlock =
nextBlockOldIndex == null ? undefined : blocks[nextBlockOldIndex]
// 不存在旧的block,直接创建
if (oldIndex == null) {
// new
nextBlocks[i] = mountBlock(
childCtx,
nextBlock ? nextBlock.el : anchor
)
} else {
// 存在旧的block,复用,检测是否需要移动位置
const block = (nextBlocks[i] = blocks[oldIndex])
Object.assign(block.ctx.scope, childCtx.scope)
if (oldIndex !== i) {
if (blocks[oldIndex + 1] !== nextBlock) {
block.insert(parent, nextBlock ? nextBlock.el : anchor)
}
}
}
}
blocks = nextBlocks
}
})

return nextNode
}

处理指令


所有的指令都是通过applyDirectiveprocessDirective来处理的,后者是基于前者的二次封装,主要处理一些内置的指令快捷方式builtInDirectives


export const builtInDirectives: Record<string, Directive<any>> = {
bind,
on,
show,
text,
html,
model,
effect
}

每种指令都是基于ctx和el等来实现快速实现某些逻辑,具体实现可以参考对应源码。


当调用app.directive注册自定义指令时,


directive(name: string, def?: Directive) {
if (def) {
ctx.dirs[name] = def
return this
} else {
return ctx.dirs[name]
}
},

实际上是向contenx的dirs添加一个属性,当调用applyDirective时,就可以得到对应的处理函数


const applyDirective = (el: Node,dir: Directive<any>,exp: string,ctx: Context,arg?: string,modifiers?: Record<string, true>) => {
const get = (e = exp) => evaluate(ctx.scope, e, el)
// 执行指令方法
const cleanup = dir({
el,
get,
effect: ctx.effect,
ctx,
exp,
arg,
modifiers
})
// 收集那些需要在卸载时清除的副作用
if (cleanup) {
ctx.cleanups.push(cleanup)
}
}

因此,可以利用上面传入的这些参数来构建自定义指令


app.directive("auto-focus", ({el})=>{
el.focus()
})

小结


整个代码看起来,确实非常精简



  • 没有虚拟DOM,就无需通过template构建render函数,直接递归遍历DOM节点,通过正则处理各种指令就行了

  • 借助@vue/reactivity,整个响应式系统实现的十分自然,除了在解析指令的使用通过ctx.effect()收集依赖,基本无需再关心数据变化的逻辑


文章开头提到,petite-vue的主要作用是:在服务端渲染的HTML页面中上"sprinkling"(点缀)一些Vue式的交互。


就我目前接触到的大部分服务端渲染HTML的项目,如果要实现一些DOM交互,一般使用



  • jQuery操作DOM,yyds

  • 当然Vue也是可以通过script + template的方式编写的,但为了一个div的交互接入Vue,又有点杀鸡焉用牛刀的感觉

  • 其他如React框架等同上


petite-vue使用了与Vue基本一致的模板语法和响应式功能,开发体验上应该很不错。且其无需考虑虚拟DOM跨平台的功能,在源码中直接使用浏览器相关API操作DOM,减少了框架runtime运行时的成本,性能方面应该也不错。


总结一下,感觉petite-vue结合了Vue标准版本的开发体验,以非常小的代码体积、良好的开发体验和还不错的运行性能,也许可以用来替代jQuery,用更现代的方式来操作DOM。


该项目是6月30号提交的第一个版本,目前相关的功能和接口应该不是特别稳定,可能会有调整。但就exmples目录中的示例而言,应该能满足一些简单的需求场景了,也许可以尝试在一些比较小型的历史项目中使用。


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

收起阅读 »

【学不动了就回家喂猪】尤大大新活 petite-vue 尝鲜

vue
前言 打开尤大大的GitHub,发现多了个叫 petite-vue 的东西,好家伙,Vue3 和 Vite 还没学完呢,又开始整新东西了?本着学不死就往死里学的态度,咱还是来瞅瞅这到底是个啥东西吧,谁让他是咱的祖师爷呢! 简介 从名字来看可以知道 peti...
继续阅读 »


前言


image.png


打开尤大大的GitHub,发现多了个叫 petite-vue 的东西,好家伙,Vue3 和 Vite 还没学完呢,又开始整新东西了?本着学不死就往死里学的态度,咱还是来瞅瞅这到底是个啥东西吧,谁让他是咱的祖师爷呢!


简介


image.png


从名字来看可以知道 petite-vue 是一个 mini 版的vue,大小只有5.8kb,可以说是非常小了。据尤大大介绍,petite-vue 是 Vue 的可替代发行版,针对渐进式增强进行了优化。它提供了与标准 Vue 相同的模板语法和响应式模型:



  • 大小只有5.8kb

  • Vue 兼容模版语法

  • 基于DOM,就地转换

  • 响应式驱动


上活


下面对 petite-vue 的使用做一些介绍。


简单使用


<body>
<script src="https://unpkg.com/petite-vue" defer init></script>
<div v-scope="{ count: 0 }">
<button @click="count--">-</button>
<span>{{ count }}</span>
<button @click="count++">+</button>
</div>
</body>

通过 script 标签引入同时添加 init ,接着就可以使用 v-scope 绑定数据,这样一个简单的计数器就实现了。



了解过 Alpine.js 这个框架的同学看到这里可能有点眼熟了,两者语法之间是很像的。



<!--  Alpine.js  -->
<div x-data="{ open: false }">
<button @click="open = true">Open Dropdown</button>
<ul x-show="open" @click.away="open = false">
Dropdown Body
</ul>
</div>

除了用 init 的方式之外,也可以用下面的方式:


<body>
<div v-scope="{ count: 0 }">
<button @click="count--">-</button>
<span>{{ count }}</span>
<button @click="count++">+</button>
</div>
<!-- 放在body底部 -->
<script src="https://unpkg.com/petite-vue"></script>
<script>
PetiteVue.createApp().mount()
</script>
</body>

或使用 ES module 的方式:


<body>
<script type="module">
import { createApp } from 'https://unpkg.com/petite-vue?module'
createApp().mount()
</script>

<div v-scope="{ count: 0 }">
<button @click="count--">-</button>
<span>{{ count }}</span>
<button @click="count++">+</button>
</div>
</body>

根作用域


createApp 函数可以接受一个对象,类似于我们平时使用 data 和 methods 一样,这时 v-scope 不需要绑定值。


<body>
<script type="module">
import { createApp } from 'https://unpkg.com/petite-vue?module'
createApp({
count: 0,
increment() {
this.count++
},
decrement() {
this.count--
}
}).mount()
</script>

<div v-scope>
<button @click="decrement">-</button>
<span>{{ count }}</span>
<button @click="increment">+</button>
</div>
</body>

指定挂载元素


<body>
<script type="module">
import { createApp } from 'https://unpkg.com/petite-vue?module'
createApp({
count: 0
}).mount('#app')
</script>

<div id="app">
{{ count }}
</div>
</body>

生命周期


可以监听每个元素的生命周期事件。


<body>
<script type="module">
import { createApp } from 'https://unpkg.com/petite-vue?module'
createApp({
onMounted1(el) {
console.log(el) // <span>1</span>
},
onMounted2(el) {
console.log(el) // <span>2</span>
}
}).mount('#app')
</script>

<div id="app">
<span @mounted="onMounted1($el)">1</span>
<span @mounted="onMounted2($el)">2</span>
</div>
</body>

组件


在 petite-vue 里,组件可以使用函数的方式创建,通过template可以实现复用。


<body>
<script type="module">
import { createApp } from 'https://unpkg.com/petite-vue?module'

function Counter(props) {
return {
$template: '#counter-template',
count: props.initialCount,
increment() {
this.count++
},
decrement() {
this.count++
}
}
}

createApp({
Counter
}).mount()
</script>

<template id="counter-template">
<button @click="decrement">-</button>
<span>{{ count }}</span>
<button @click="increment">+</button>
</template>

<!-- 复用 -->
<div v-scope="Counter({ initialCount: 1 })"></div>
<div v-scope="Counter({ initialCount: 2 })"></div>
</body>

全局状态管理


借助 reactive 响应式 API 可以很轻松的创建全局状态管理


<body>
<script type="module">
import { createApp, reactive } from 'https://unpkg.com/petite-vue?module'

const store = reactive({
count: 0,
increment() {
this.count++
}
})
// 将count加1
store.increment()
createApp({
store
}).mount()
</script>

<div v-scope>
<!-- 输出1 -->
<span>{{ store.count }}</span>
</div>
<div v-scope>
<button @click="store.increment">+</button>
</div>
</body>

自定义指令


这里来简单实现一个输入框自动聚焦的指令。


<body>
<script type="module">
import { createApp } from 'https://unpkg.com/petite-vue?module'

const autoFocus = (ctx) => {
ctx.el.focus()
}

createApp().directive('auto-focus', autoFocus).mount()
</script>

<div v-scope>
<input v-auto-focus />
</div>
</body>

内置指令



  • v-model

  • v-if / v-else / v-else-if

  • v-for

  • v-show

  • v-html

  • v-text

  • v-pre

  • v-once

  • v-cloak



注意:v-for 不需要key,另外 v-for 不支持 深度解构



<body>
<script type="module">
import { createApp } from 'https://unpkg.com/petite-vue?module'

createApp({
userList: [
{ name: '张三', age: { a: 23, b: 24 } },
{ name: '李四', age: { a: 23, b: 24 } },
{ name: '王五', age: { a: 23, b: 24 } }
]
}).mount()
</script>

<div v-scope>
<!-- 支持 -->
<li v-for="{ age } in userList">
{{ age.a }}
</li>
<!-- 不支持 -->
<li v-for="{ age: { a } } in userList">
{{ a }}
</li>
</div>
</body>

不支持


为了更轻量小巧,petite-vue 不支持以下特性:



  • ref()、computed

  • render函数,因为petite-vue 没有虚拟DOM

  • 不支持Map、Set等响应类型

  • Transition, KeepAlive, Teleport, Suspense

  • v-on="object"

  • v-is &

  • v-bind:style auto-prefixing


总结


以上就是对 petite-vue 的一些简单介绍和使用,抛砖引玉,更多新的探索就由你们去发现了。


总的来说,prtite-vue 保留了 Vue 的一些基础特性,这使得 Vue 开发者可以无成本使用,在以往,当我们在开发一些小而简单的页面想要引用 Vue 但又常常因为包体积带来的考虑而放弃,现在,petite-vue 的出现或许可以拯救这种情况了,毕竟它真的很小,大小只有 5.8kb,大约只是 Alpine.js 的一半。


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

10张脑图带你快速入门Vue3 | 附高清原图

vue
前言 这个月重新开始学习Vue3 目前已经完结第一部分:基础部分 我将所有内容吸收整理成10张脑图,一来快速入门Vue3,二来方便以后查看 脑图 应用实例和组件实例 模板语法 配置选项 计算属性和监听器 绑定class和style 条件渲染 列表渲...
继续阅读 »

前言


这个月重新开始学习Vue3


目前已经完结第一部分:基础部分


我将所有内容吸收整理成10张脑图,一来快速入门Vue3,二来方便以后查看


脑图


应用实例和组件实例


1应用实例和组件实例.png


模板语法


2模板语法.png


配置选项


3配置选项.png


计算属性和监听器


4计算属性和监听器.png


绑定class和style


5绑定class和style.png


条件渲染


6条件渲染.png


列表渲染


7列表渲染v-for.png


事件处理


8事件处理.png


v-model及其修饰符


9v-model及其修饰符.png


组件的基本使用


10组件的基本使用.png


温馨小贴士



  1. 由于图片较多,为了避免一张张保存的麻烦


我已将上述原图已上传githubgithub.com/jCodeLife/m…



  1. 如果需要更改图片,为了方便你按照自己的习惯进行修改


我已将原始文件xmind上传github
github.com/jCodeLife/m…



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

收起阅读 »

面试官问我CORS跨域,我直接一套操作斩杀!

前言 我们都知道由于同源策略的存在,导致我们在跨域请求数据的时候非常的麻烦。首先阻挡我们的所谓同源到底是什么呢?,所谓同源就是浏览器的一个安全机制,不同源的客户端脚本没有在明确授权的情况下,不能读写对方资源。由于存在同源策略的限制,而又有需要跨域的业务,所以就...
继续阅读 »

前言


我们都知道由于同源策略的存在,导致我们在跨域请求数据的时候非常的麻烦。首先阻挡我们的所谓同源到底是什么呢?,所谓同源就是浏览器的一个安全机制,不同源的客户端脚本没有在明确授权的情况下,不能读写对方资源。由于存在同源策略的限制,而又有需要跨域的业务,所以就有了CORS的出现。


我们都知道,jsonp也可以跨域,那为什么还要使用CORS



  • jsonp只可以使用 GET 方式提交

  • 不好调试,在调用失败的时候不会返回任何状态码

  • 安全性,万一假如提供jsonp的服务存在页面注入漏洞,即它返回的javascript的内容被人控制的。那么结果是什么?所有调用这个jsonp的网站都会存在漏洞。于是无法把危险控制在一个域名下…所以在使用jsonp的时候必须要保证使用的jsonp服务必须是安全可信的。


开始CORS


CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing),他允许浏览器向跨源服务器发送XMLHttpRequest请求,从而克服啦 AJAX 只能同源使用的限制


CORS需要浏览器和服务器同时支持,整个 CORS通信过程,都是浏览器自动完成不需要用户参与,对于开发者来说,CORS的代码和正常的 ajax 没有什么差别,浏览器一旦发现跨域请求,就会添加一些附加的头信息,


CORS这么好吗,难道就没有缺点嘛?


答案肯定是NO,目前所有最新浏览器都支持该功能,但是万恶的IE不能低于10


简单请求和非简单请求


浏览器将CORS请求分成两类:简单请求和非简单请求


简单请求


凡是同时满足以下两种情况的就是简单请求,反之则非简单请求,浏览器对这两种请求的处理不一样



  • 请求方法是以下方三种方法之一

    • HEAD

    • GET

    • POST



  • HTTP的头信息不超出以下几种字段

    • Accept

    • Accept-Language

    • Content-Language

    • Last-Event-ID

    • Content-Type:只限于三个值 application/x-www-form-urlencodedmultipart/form-datatext/plain




对于简单请求来说,浏览器之间发送CORS请求,具体来说就是在头信息中,增加一个origin字段,来看一下例子


GET /cors? HTTP/1.1
Host: localhost:2333
Connection: keep-alive
Origin: http://localhost:2332
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36
Accept: */*
Referer: http://localhost:2332/CORS.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
If-None-Match: W/"1-NWoZK3kTsExUV00Ywo1G5jlUKKs"

上面的头信息中,Origin字段用来说名本次请求来自哪个源,服务器根据这个值,决定是否同意这次请求。


如果Origin指定的源不在允许范围之内,服务器就会返回一个正常的HTTP回应,然后浏览器发现头信息中没有包含Access-Control-Allow-Origin 字段,就知道出错啦,然后抛出错误,反之则会出现这个字段(实例如下)


Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8



  • Access-Control-Allow-Origin 这个字段是必须的,表示接受那些域名的请求(*为所有)




  • Access-Control-Allow-Credentials 该字段可选, 表示是否可以发送cookie




  • Access-Control-Expose-Headers 该字段可选,XHMHttpRequest对象的方法只能够拿到六种字段: Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma ,如果想拿到其他的需要使用该字段指定。




如果你想要连带Cookie一起发送,是需要服务端和客户端配合的


// 服务端
Access-Control-Allow-Credentials: true
// 客户端
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
// 但是如果省略withCredentials属性的设置,有的浏览器还是会发送cookie的
xhr.withCredentials = false;

非简单请求


非简单请求则是不满足上边的两种情况之一,比如请求的方式为 PUT,或者请求头包含其他的字段


非简单请求的CORS请求是会在正式通信之前进行一次预检请求


浏览器先询问服务器,当前网页所在的域名是否可以请求您的服务器,以及可以使用那些HTTP动词和头信息,只有得到正确的答复,才会进行正式的请求


// 前端代码
var url = 'http://localhost:2333/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();

由于上面的代码使用的是 PUT 方法,并且发送了一个自定义头信息.所以是一个非简单请求,当浏览器发现这是一个非简单请求的时候,会自动发出预检请求,看看服务器可不可以接收这种请求,下面是"预检"HTTP 头信息


OPTIONS /cors HTTP/1.1
Origin: localhost:2333
Access-Control-Request-Method: PUT // 表示使用的什么HTTP请求方法
Access-Control-Request-Headers: X-Custom-Header // 表示浏览器发送的自定义字段
Host: localhost:2332
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive
User-Agent: Mozilla/5.0...

"预检"使用的请求方法是 OPTIONS , 表示这个请求使用来询问的,


预检请求后的回应,服务器收到"预检"请求以后,检查了OriginAccess-Control-Request-MethodAccess-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。


预检的响应头:


HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://localhost:2332 // 表示http://localhost:2332可以访问数据
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

如果浏览器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS的头相关信息,这是浏览器就认定,服务器不允许此次访问,从而抛出错误


预检之后的请求


当预检请求通过之后发出正经的HTTP请求,还有一个就是一旦通过了预检请求就会,请求的时候就会跟简单请求,会有一个Origin头信息字段。


通过预检之后的,浏览器发出发请求


PUT /cors HTTP/1.1
Origin: http://api.bob.com // 通过预检之后的请求,会自动带上Origin字段
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

感谢


谢谢你读完本篇文章,希望对你能有所帮助,如有问题欢迎各位指正。




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

收起阅读 »

「百毒不侵」面试官最喜欢问的13种Vue修饰符

1.lazy lazy修饰符作用是,改变输入框的值时value不会改变,当光标离开输入框时,v-model绑定的值value才会改变 <input type="text" v-model.lazy="value"> <div>{{val...
继续阅读 »

image.png


1.lazy


lazy修饰符作用是,改变输入框的值时value不会改变,当光标离开输入框时,v-model绑定的值value才会改变


<input type="text" v-model.lazy="value">
<div>{{value}}</div>

data() {
return {
value: '222'
}
}

lazy1.gif


2.trim


trim修饰符的作用类似于JavaScript中的trim()方法,作用是把v-model绑定的值的首尾空格给过滤掉。


<input type="text" v-model.trim="value">
<div>{{value}}</div>

data() {
return {
value: '222'
}
}

number.gif


3.number


number修饰符的作用是将值转成数字,但是先输入字符串和先输入数字,是两种情况


<input type="text" v-model.number="value">
<div>{{value}}</div>

data() {
return {
value: '222'
}
}


先输入数字的话,只取前面数字部分



trim.gif



先输入字母的话,number修饰符无效



number2.gif


4.stop


stop修饰符的作用是阻止冒泡


<div @click="clickEvent(2)" style="width:300px;height:100px;background:red">
<button @click.stop="clickEvent(1)">点击</button>
</div>

methods: {
clickEvent(num) {
不加 stop 点击按钮输出 1 2
加了 stop 点击按钮输出 1
console.log(num)
}
}

5.capture


事件默认是由里往外冒泡capture修饰符的作用是反过来,由外网内捕获


<div @click.capture="clickEvent(2)" style="width:300px;height:100px;background:red">
<button @click="clickEvent(1)">点击</button>
</div>

methods: {
clickEvent(num) {
不加 capture 点击按钮输出 1 2
加了 capture 点击按钮输出 2 1
console.log(num)
}
}

6.self


self修饰符作用是,只有点击事件绑定的本身才会触发事件


<div @click.self="clickEvent(2)" style="width:300px;height:100px;background:red">
<button @click="clickEvent(1)">点击</button>
</div>

methods: {
clickEvent(num) {
不加 self 点击按钮输出 1 2
加了 self 点击按钮输出 1 点击div才会输出 2
console.log(num)
}
}

7.once


once修饰符的作用是,事件只执行一次


<div @click.once="clickEvent(2)" style="width:300px;height:100px;background:red">
<button @click="clickEvent(1)">点击</button>
</div>

methods: {
clickEvent(num) {
不加 once 多次点击按钮输出 1
加了 once 多次点击按钮只会输出一次 1
console.log(num)
}
}

8.prevent


prevent修饰符的作用是阻止默认事件(例如a标签的跳转)


<a href="#" @click.prevent="clickEvent(1)">点我</a>

methods: {
clickEvent(num) {
不加 prevent 点击a标签 先跳转然后输出 1
加了 prevent 点击a标签 不会跳转只会输出 1
console.log(num)
}
}

9.native


native修饰符是加在自定义组件的事件上,保证事件能执行


执行不了
<My-component @click="shout(3)"></My-component>

可以执行
<My-component @click.native="shout(3)"></My-component>

10.left,right,middle


这三个修饰符是鼠标的左中右按键触发的事件


<button @click.middle="clickEvent(1)"  @click.left="clickEvent(2)"  @click.right="clickEvent(3)">点我</button>

methods: {
点击中键输出1
点击左键输出2
点击右键输出3
clickEvent(num) {
console.log(num)
}
}

11.passive


当我们在监听元素滚动事件的时候,会一直触发onscroll事件,在pc端是没啥问题的,但是在移动端,会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给onscroll事件整了一个.lazy修饰符


<div @scroll.passive="onScroll">...</div>

12.camel


不加camel viewBox会被识别成viewbox
<svg :viewBox="viewBox"></svg>

加了canmel viewBox才会被识别成viewBox
<svg :viewBox.camel="viewBox"></svg>

12.sync


父组件传值进子组件,子组件想要改变这个值时,可以这么做


父组件里
<children :foo="bar" @update:foo="val => bar = val"></children>

子组件里
this.$emit('update:foo', newValue)

sync修饰符的作用就是,可以简写:


父组件里
<children :foo.sync="bar"></children>

子组件里
this.$emit('update:foo', newValue)

13.keyCode


当我们这么写事件的时候,无论按什么按钮都会触发事件


<input type="text" @keyup="shout(4)">

那么想要限制成某个按键触发怎么办?这时候keyCode修饰符就派上用场了


<input type="text" @keyup.keyCode="shout(4)">

Vue提供的keyCode:


//普通键
.enter
.tab
.delete //(捕获“删除”和“退格”键)
.space
.esc
.up
.down
.left
.right
//系统修饰键
.ctrl
.alt
.meta
.shift

例如(具体的键码请看键码对应表


按 ctrl 才会触发
<input type="text" @keyup.ctrl="shout(4)">

也可以鼠标事件+按键
<input type="text" @mousedown.ctrl.="shout(4)">

可以多按键触发 例如 ctrl + 67
<input type="text" @keyup.ctrl.67="shout(4)">

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

收起阅读 »

比浏览器 F12 更好用的免费调试抓包工具 Fiddler 介绍

身为一名前端搬砖工,长久以来有两个问题困扰着我,一个是做后台项目接口返回的数据都为空,不方便做更进一步的对数据的查改及测试;另一个是做移动端的项目,比如 uniapp,每次遇到接口问题都只能 console 在 HBuilder 进行调试,苦不堪言,后来发现我...
继续阅读 »

身为一名前端搬砖工,长久以来有两个问题困扰着我,一个是做后台项目接口返回的数据都为空,不方便做更进一步的对数据的查改及测试;另一个是做移动端的项目,比如 uniapp,每次遇到接口问题都只能 console 在 HBuilder 进行调试,苦不堪言,后来发现我司 TE 同学用 Fiddler 进行抓包测试,一问这软件还是免费的,遂进行了一番学习了解,发现可以直接解决刚刚提到的这两个问题,所以在这里做个分享。


简介



  • Fiddler 是位于客户端和服务器端的 HTTP 代理

  • 目前最常用的 HTTP 抓包工具之一

  • 功能非常强大,是 web 调试的利器

    • 监控浏览器所有的 HTTP/HTTPS 流量

    • 查看、分析请求内容细节

    • 伪造客户端请求和服务器响应

    • 解密 HTTPS 的 web 会话

    • 全局、局部断点功能

    • 第三方插件



  • 使用场景

    • 接口的测试与调试

    • 线上环境调试

    • web 性能分析




下载


直接去官网下载 Fiddler Classic 即可:


image.png


原理


学习一件新事物,最好是知其然亦知其所以然,这样遇到问题心里有底,才不容易慌,下面就介绍下 Fiddler 抓包的原理。


Fiddler 是位于客户端和服务器端之间的 HTTP 代理。一旦启动 Fiddler,其会自动将代理服务器设置成本机,默认端口为 8888,并设置成系统代理(Act as system proxy on startup)。可以在 Fiddler 通过 'Tools -> Options -> Connections' 查看, 图示如下:


image.png

在 Fiddler 运行的情况下,以 Chrome 浏览器为例,可以在其 '设置 -> 高级 -> 系统 -> 打开您计算机的代理设置 -> 连接 -> 局域网(LAN)设置' 里看到,'代理服务器' 下的 '为 LAN 使用代理服务器' 选项被勾选了(如果没有运行 Fiddler,默认情况下是不会被勾选的),如下图:


image (1).png

点开 '高级',会发现 '要使用的代理服务器地址' 就是本机 ip,端口为 8888。如下图:


image (2).png

也就是说浏览器的 HTTP 请求/响应都被代理到了系统的 8888 端口,被 Fiddler 拦截了。


界面介绍


下面开始对整个 Fiddler 的界面进行一个庖丁解牛


工具栏


image.png
主要介绍上图中几个标了号的我认为比较常用的功能:



  1. Replay:重放选中的那条请求,同时按下 shift + R 键,可以输入重复发送请求的次数(这些请求是串行发送的)。可以用来做重放攻击的测试。

  2. 删除会话(sessions)

  3. 继续打了断点的请求:打断点后请求会被拦截在 Fiddler,点击这个 Go 继续发送。打断点的方式是点击界面底部的空格,具体位置如下图所示:


image (1).png



  1. 这个类似瞄准器的工具时用于选择抓取请求的应用:按住不放将鼠标拖放到目标应用即可

  2. 可用于查找某条请求,比如你知道请求参数里的某个字段,可以直接输入进行查找

  3. 编码解码工具,可以进行多种编码的转换,是个人觉得挺好用的一个工具,能够编码的格式包括但不限于 base64、md5 和 URLEncode 等

  4. 可以查看一些诸如本机 ip(包括 IPv4,IPv6) 等信息,就用不着去 cmd 里 输入ipconfig 查看了,如下图:


image (2).png


会话列表(Session List)


位于软件界面的左半部的就是会话列表了,抓取到的每条 http 请求都列在这,每一条被称为一个 session,如下图所示:

image (3).png


每条会话默认包含的信息



  • 请求的状态码(result)

  • 协议(protocol)

  • 主机名(host)

  • URL

  • 请求大小(body,以字节为单位)

  • 缓存信息(caching)

  • 响应类型(content-type)

  • 发出请求的 Windows 进程及进程 ID(process)


自定义列


除了以上这些,我们还可以添加自定义列,比如想添加一列请求方法信息:



  1. 点击菜单栏 -> Rules -> Customize Rules 调出 Fiddler ScriptEditor 窗口

  2. 按下 ctrl + f 输入 static function Main() 进行查找

  3. 然后在找到的函数 Main 里添加:


FiddlerObject.UI.lvSessions.AddBoundColumn("Method",60,getHTTPMethod );
static function getHTTPMethod(oS: Session){
if (null != oS.oRequest) return oS.oRequest.headers.HTTPMethod;
else return String.Empty;
}

图示如下:


image (4).png
4. 按下 ctrl + s 保存。然后就可以在会话列表里看到多出了名为 Method 的一列,内容为请求方法。


排序和移动



  1. 点击每一列的列表头,可以反向排序

  2. 按住列表头不放进行拖动,可以改变列表位置


QuickExec 与状态栏


位于软件界面底部的那条黑色的是 QuickExec,可用于快速执行输入的一些命令,具体命令可输入 help 跳转到官方的帮助页面查看。图示如下:


image (5).png

在 QuickExec 下面的就是状态栏,



  1. Capturing:代表目前 Fiddler 的代理功能是开启的,也就是是否进行请求响应的拦截,如果想关闭代理,只需要点击一下 Capturing 图标即可

  2. All Processes:选择抓取的进程,可以只选浏览器进程或是非浏览器进程等

  3. 断点:按一次是请求前断点,也就是请求从浏览器发出到 Fiddler 这停住;再按一次是响应后的断点,也就是响应从服务器发出,到Fiddler 这停住;再按一次就是不打断点

  4. 当前选中的会话 / 总会话数

  5. 附加信息


辅助标签 + 工具


位于软件界面右边的这一大块面板,即为辅助标签 + 工具,如下图所示,它拥有 10 个小标签,我们先从 Statistics 讲起,btw,这单词的发音是 [stəˈtɪstɪks],第 3 个字母 a 发 'ə' 的音,而不是 'æ'~


image (6).png


Statistics(统计)


这个 tab 里都是些 http 请求的性能数据分析,如 DNS Lookup(DNS 解析时间)、 TCP/IP Connect(TCP/IP 连接时间)等。


Inspectors(检查器)


image.png

以多种不同的方式查看请求的请求报文和响应报文,比如可以只看头部信息(Headers)、或者是查看请求的原始信息(Raw),再比如请求的参数是 x-www-form-urlencoded 的话,就能在 WebForms 里查看...


AutoResponder(自动响应器)


image (1).png

这是一个我认为比较有用的功能了,它可以篡改从服务器返回的数据,达到欺骗浏览器的目的。


实战案例


我在做一个后台项目的时候,因为前台还没弄好,数据库都是没有数据的,在获取列表时,请求得到的都是如下图所示的空数组:


image.png

那么在页面上显示的也就是“暂无数据”,这就影响了之后一些删改数据的接口的对接。


image (2).png

此时,我们就可以通过 AutoResponder ,按照接口文档的返回实例,对返回的数据进行编辑,具体步骤如下:



  1. 勾选上 Enable rules(激活自动响应器) 和 Unmatched requests passthrough(放行所有不匹配的请求)


image (3).png

2. 在左侧会话列表里选中要修改响应的那条请求,按住鼠标直接拖动到 AutoResponder 的面板里,如下图红框所示:


image (4).png

3. 选中上图红框里的请求单机鼠标右键,选择 Edit Response...


image (5).png

4. 进入编辑面板选择 Raw 标签就可以直接进行编辑了,这里我按照接口文档的返回示例,给 items 数组添加了数据,如下图所示:


image (6).png

这样,浏览器接收到数据,页面就如下图所示有了内容,方便进行之后的操作


image (7).png


Composer(设计者)


说完了对响应的篡改,现在介绍的 composer 就是用于对请求的篡改。这个单词的翻译是作曲家,按照我们的想法去修改一个请求,宛如作曲家谱一首乐曲一般。


image.png

用法与 AutoResponder 类似,也是可以从会话列表里直接拖拽一个请求到上图红框中,然后对请求的内容进行修改即可。应用场景之一就是可以绕过一些前端用 js 写的限制与验证,直接发送请求,通过返回的数据可以判断后端是否有做相关限制,测试系统的健壮性。


Filters(过滤器)


在默认情况下,Filters 会抓取一切能够抓取到的请求,统统列在左侧的会话列表里,如果我们是有目的对某些接口进行测试,就会觉得请求列表很杂乱,这时可以点开 Filters 标签,勾选 Use Filters,启动过滤工具,如下图:


image.png

接着就可以根据我们需要对左侧列表里展示的所抓取的接口进行过滤,比如根据 Hosts 进行过滤,只显示 Hosts 为 api.juejin.cn 的请求,就可以如下图在 Hosts 那选择 'Show only the following Hosts',然后点击右上角 Actions 里的 'Run Filterset now' 执行过滤。


image.png

过滤的筛选条件还有很多,比如据请求头字段里 URL 是否包含某个单词等,都很简单,一看便知,这里不再一一细说。


HTTPS 抓包


默认情况下,Fiddler 没办法显示 HTTPS 的请求,需要进行证书的安装:



  1. 点击 'Tools -> Options...' ,勾选上 'Decrypt HTTPS traffic' (解密HTTPS流量)


image.png



  1. 点击 Actions 按钮,点击 'Reset All Certicicates' (重置所有证书),之后遇到弹出的窗口,就一直点击 '确定' 或 'yes' 就行了。


image (1).png



  1. 查看证书是否安装成功:点击 'Open Windows Certificate Manager' 打开 Windows 证书管理器窗口


image (2).png

点击 '操作' 选择 '查找证书',在 '包含' 输入框输入 fiddler 进行查找


image (3).png

查找结果类似下图即安装证书成功


image (4).png

现在会话列表就能成功显示 https 协议的请求了。


断点应用


全局断点


通过 'Rules -> Automatic Breakpoints' 可以给请求打断点,也就是中断请求,断点分为两种:



  1. Before Requests(请求前断点):请求发送给服务器之前进行中断

  2. After Responses(响应后断点):响应返回给客户端之前进行中断


image.png

打上断点之后,选中想要修改传输参数的那一条请求,按 R 进行重发,这条请求就会按要求在请求前或响应后被拦截,我们就可以根据需要进行修改,然后点击工具栏的 'Go',或者点击如下图所示的绿色按钮 'Run to Completion',继续完成请求。


image (1).png

这样打断点是全局断点,即所有请求都会被拦截,下面介绍局部断点。


局部断点


如果只想对某一条请求打断点,则可以在 QuickExec 输入相应的命令执行。



  • 请求前断点



  1. 在 QuickExec 输入 bpu query_adverts 。注意:query_adverts 为请求的 url 的一部分,这样就只有 url 中包含 query_adverts 的请求会被打上断点。


image (2).png



  1. 按下 Enter 键,可以看到红框中显示 query_adverts 已经被 breakpoint 了,而且是 RequestURI


image (3).png



  1. 选中 url 中带 query_adverts 的这条请求,按 R 再次发送,在发给服务器前就会被中断(原谅我又拿掘金的请求做例子~)


image (4).png



  1. 取消断点:在 QuickExec 输入 bpu 按下 Enter 即可



  • 响应后断点


与请求前断点步骤基本一致,区别在于输入的命令是 bpafter get_today_status
按下 Enter 后在 'Composer' 标签下点击 'Execute' 执行,再次发送该请求则服务器的响应在发送给浏览器之前被截断,注意下红色的图标,跟之前的请求前断点的区别在于一个是向上的箭头,一个是向下的箭头。


image (5).png

取消拦截则是输入 bpafter 后回车,可以看到状态栏显示 'ResponseURI breakpoint cleared'


image (6).png


弱网测试


Fiddler 还可以用于弱网测试,'Rules -> Performance -> 勾选 Simulate Modem Speeds' 即可


image (7).png

再次刷新网页会感觉回到了拨号上网的年代,可以测试网站在网速很低的情况下的表现。


修改网速


网速还可以修改,点击 'FiddlerScript' 标签,在下图绿框中搜索 simulateM,按几下回车找到 if (m_SimulateModem) 这段代码,可以修改上下传输的速度:


image (8).png


安卓手机抓包


最后一部分主要内容是关于手机抓包的,我用的是小米手机 9,MIUI 12.5.1 稳定版,安卓版本为 11。



  1. 首先保证安装了 Fiddler 的电脑和手机连的是同一个 wifi

  2. 在 Fiddler 中,点击 'Tools -> Options...' ,在弹出的 Options 窗口选择 Connections 标签,勾选 'Allow remote computers to connect'


image (9).png



  1. 手机打开 '设置 -> WLAN -> 连接的那个 WLAN 的设置' 进入如下图所示的页面


image (10).png



  1. '代理' 选择 '手动','主机名' 填写电脑的主机名,端口则是 Fiddler 默认监听的 8888,然后点击左上角的 '打钩图标' 进行保存

  2. 下载证书:打开手机浏览器,输入 'http://192.168.1.1:8888' (注意:192.168.1.1 要替换成你电脑的 ip 地址),会出现如下页面


image (11).png

点击红框中链接进行证书的下载



  1. 安装证书:打开 '设置 -> 密码与安全 -> 系统安全 -> 加密与凭据 -> 安装证书(从存储设备安装证书)-> 证书 ' 找到刚刚下载的证书进行安装


image (12).png



  1. 安装完成可以在 '加密与凭据 -> 信任的凭据' 下查看


image (13).png



  1. 现在 Fiddler 就可以抓到手机里 app 发送的请求了

  2. 最后注意:测试完毕需要关闭手机的 WLAN 代理,否则手机就上不了网了~


One More Thing


几个常用快捷键



  • 双击某一条请求:打开该请求的 Inspectors 面板

  • ctrl + X:清除请求列表

  • R:选中某一条请求,按 R 键可重新发送该请求

  • shift+delete:删除除了选中那一条之外的请求



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

收起阅读 »

小程序自动化测试入门到实践

背景 随着小程序项目越来越复杂,业务场景越来多,花费在回归测试上的时间会越来越多,前端自动化测试就非常有必要提上日程。 今天要带来的是: 小程序自动化测试入门教程。 环境 系统 :macOS 微信开发者工具版本: 1.05.2106300 什么是小程序自动化 ...
继续阅读 »

背景


随着小程序项目越来越复杂,业务场景越来多,花费在回归测试上的时间会越来越多,前端自动化测试就非常有必要提上日程。


今天要带来的是: 小程序自动化测试入门教程


环境


系统 :macOS

微信开发者工具版本: 1.05.2106300


什么是小程序自动化


微信官方文档:小程序自动化


使用小程序自动化 SDK miniprogram-automator,可以在帮助我们在小程序中完成一些事情,比如:控制小程序跳转到指定页面,获取小程序页面数据,获取小程序页面元素状态等。


配合 jest 就可以实现小程序端自动化测试了。
话不多说,我们开始吧


准备




  1. 项目根目录 mini-auto-test-demo 里面准备两个目录 miniprogram 放小程序代码,和 test-e2e 放测试用例代码




 |— mini-auto-test-demo/  // 根目录
|— miniprogram/ // 小程序代码
|— pages/
|— index/ // 测试文件
|— test-e2e/ // 测试用例代码
|— index.spec.js // 启动文件
|— package.json

index 文件夹下准备用于测试的页面

<!--index.wxml-->
<view class="userinfo">
<view class="userinfo-avatar" bindtap="bindViewTap">
<open-data type="userAvatarUrl"></open-data>
</view>
<open-data type="userNickName"></open-data>
</view>

/**index.wxss**/
.userinfo {
margin-top: 50px;
display: flex;
flex-direction: column;
align-items: center;
color: #aaa;
}
.userinfo-avatar {
overflow: hidden;
width: 128rpx;
height: 128rpx;
margin: 20rpx;
border-radius: 50%;
}

// index.js
// 获取应用实例
const app = getApp()
Page({
data: {
userInfo: {},
},
// 事件处理函数
bindViewTap() {
wx.navigateTo({
url: '../logs/logs'
})
}
})


  1. 微信开发者工具->设置-> 安全设置 -> 打卡服务端口


image.png



  1. 安装npm包


如果根目录没有 package.json 文件,先执行


npm init

如果根目录已经有 package.json 文件 ,执行以下命令:


npm install miniprogram-automator jest --save-dev
npm i jest -g

安装需要的依赖



  1. 在根目录下新建index.spec.js 文件
const automator = require('miniprogram-automator')

automator.launch({
cliPath: '/Applications/wechatwebdevtools.app/Contents/MacOS/cli', // 工具 cli 位置
projectPath: '/Users/SONG/Documents/github/mini-auto-test-demo/miniprogram', // 项目文件地址
}).then(async miniProgram => {
const page = await miniProgram.reLaunch('/pages/index/index')
await page.waitFor(500)
const element = await page.$('.userinfo-avatar')
console.log(await element.attribute('class'))
await element.tap()
await miniProgram.close()
})

这里要注意修改为自己的cli位置和项目文件地址:



  1. cliPath:


可以在应用程序中找到微信开发者工具,点击右键点击"显示包内容"


image.png


找到cli后,快捷键 :command+option+c 复制路径, 就拿到了


image.png



  1. projectPath:


注意!!项目路径填写的是小程序文件夹miniprogram而不是mini-auto-test-demo


启动


写好路径后,在mac终端进入mini-auto-test-demo根目录或 vscode 终端根目录执行命令:


node index.spec.js

image.png


你会发现微信开发者工具被自动打开,并执行了点击事件进入了log页面,终端输出了class的值。
到此你已经感受到了自动化,接下来你要问了,自动化测试呢?别急,接着往下看。


自动化测试


在一开始准备的test-e2e 文件夹下新建integration.test.js文件,


引入'miniprogram-automator, 连接自动化操作端口,把刚刚index.spec.js中的测试代码,放到 jest it 里,jest相关内容我们这里就不赘述了,大家可以自行学习(其实我也才入门 ̄□ ̄||)。

const automator = require('miniprogram-automator');

describe('index', () => {
let miniProgram;
let page;
const wsEndpoint = 'ws://127.0.0.1:9420';
beforeAll(async() => {
miniProgram = await automator.connect({
wsEndpoint: wsEndpoint
});
}, 30000);

it('test index', async() => {
page = await miniProgram.reLaunch('/pages/index/index')
await page.waitFor(500)
const element = await page.$('.userinfo-avatar')
console.log(await element.attribute('class'))
await element.tap()
});
});

package.json scripts 添加命令


"e2e": "jest ./test-e2e integration.test.js --runInBand"

测试代码写好了,接下来如何运行呢?这里我们提另外一个方法。


cli 命令行调用


官方文档:命令行调用

你一定会问,刚刚我们不是学习了启动运行,这么还要学另外一种方法 o(╥﹏╥)o
大家都知道,一般团队里都是多人合作的,大家的项目路径都不一样,难道每次还要改projectPath吗?太麻烦了,使用cli就不需要考虑在哪里启动,项目地址在哪里,话不多说,干!


打开终端进入放微信开发者工具cli文件夹(路径仅供参考):


cd /Applications/wechatwebdevtools.app/Contents/MacOS 

执行命令(如果你的微信开发者工具开着项目,先关掉)


./cli --auto  /Users/SONG/Documents/github/mini-auto-test-demo/miniprogram  --auto-port 9420

微信开发者工具通过命令行启动


image.png


启动后在项目根目录下执行,可以看到测试通过


npm run e2e

image.png


到此,我们已经可以写测试用例了。这只是入门系列,后续会持续更文,感谢大家的耐心阅读,如果你有任何问题都可以留言给我,摸摸哒



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

面试官:能不能手写几道链表的基本操作

反转链表 示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL 循环解决方案 这道题是链表中的经典题目,充分体现链表这种数据结构 操作思路简单 ,...
继续阅读 »

反转链表


示例:


输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL


  • 循环解决方案


这道题是链表中的经典题目,充分体现链表这种数据结构 操作思路简单 , 但是 实现上 并没有那么简单的特点。


那在实现上应该注意一些什么问题呢?


保存后续节点。作为新手来说,很容易将当前节点的 next 指针直接指向前一个节点,但其实当前节点下一个节点 的指针也就丢失了。因此,需要在遍历的过程当中,先将下一个节点保存,然后再操作 next指向。


链表结构声定义如下:


function ListNode(val) {
this.val = val;
this.next = null;
}

实现如下:

/**
* @param {ListNode} head
* @return {ListNode}
*/
let reverseList = (head) => {
if (!head)
return null;
let pre = null,
cur = head;
while (cur) {
// 关键: 保存下一个节点的值
let next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
};


  • 递归解决方案


let reverseList = (head) =>{
let reverse = (pre, cur) => {
if(!cur) return pre;
// 保存 next 节点
let next = cur.next;
cur.next = pre;
return reverse(cur, next);
}
return reverse(null, head);
}

2.区间反转


反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。


说明: 1 ≤ m ≤ n ≤ 链表长度。


示例:


输入: 1->2->3->4->5->NULL, m = 2, n = 4
输出: 1->4->3->2->5->NULL

思路
这一题相比上一个整个链表反转的题,其实是换汤不换药。我们依然有两种类型的解法:循环解法递归解法


image.png
关于前节点和后节点的定义,大家在图上应该能看的比较清楚了,后面会经常用到。


反转操作上一题已经拆解过,这里不再赘述。值得注意的是反转后的工作,那么对于整个区间反转后的工作,其实就是一个移花接木的过程,首先将前节点的 next 指向区间终点,然后将区间起点的 next 指向后节点。因此这一题中有四个需要重视的节点: 前节点 、 后节点 、 区间起点 和 区间终点 。



  • 循环解法
/**
* @param {ListNode} head
* @param {number} m
* @param {number} n
递归解法
对于递归解法,唯一的不同就在于对于区间的处理,采用递归程序进行处理,大家也可以趁着复习一下
递归反转的实现。
* @return {ListNode}
*/
var reverseBetween = function(head, m, n) {
let count = n - m;
let p = dummyHead = new ListNode();
let pre, cur, start, tail;
p.next = head;
for(let i = 0; i < m - 1; i ++) {
p = p.next;
}
// 保存前节点
front = p;
// 同时保存区间首节点
pre = tail = p.next;
cur = pre.next;
// 区间反转
for(let i = 0; i < count; i++) {
let next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
// 前节点的 next 指向区间末尾
front.next = pre;
// 区间首节点的 next 指向后节点(循环完后的cur就是区间后面第一个节点,即后节点)
tail.next = cur;
return dummyHead.next;
};


  • 递归解法
var reverseBetween = function(head, m, n) {
// 递归反转函数
let reverse = (pre, cur) => {
if(!cur) return pre;
// 保存 next 节点
let next = cur.next;
cur.next = pre;
return reverse(cur, next);
}
let p = dummyHead = new ListNode();
dummyHead.next = head;
let start, end; //区间首尾节点
let front, tail; //前节点和后节点
for(let i = 0; i < m - 1; i++) {
p = p.next;
}
front = p; //保存前节点
start = front.next;
for(let i = m - 1; i < n; i++) {
p = p.next;
}
end = p;
tail = end.next; //保存后节点
end.next = null;
// 开始穿针引线啦,前节点指向区间首,区间首指向后节点
front.next = reverse(null, start);
start.next = tail;
return dummyHead.next;
}

3.两个一组翻转链表


给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。


你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。


示例


给定 1->2->3->4, 你应该返回 2->1->4->3

思路


如图所示,我们首先建立一个虚拟头节点(dummyHead),辅助我们分析。


image.png


首先让 p 处在 dummyHead 的位置,记录下 p.next 和 p.next.next 的节点,也就是 node1 和
node2。


随后让 node1.next = node2.next, 效果:


image.png


然后让 node2.next = node1, 效果:


image.png
最后,dummyHead.next = node2,本次翻转完成。同时 p 指针指向node1, 效果如下:


image.png
依此循环,如果 p.next 或者 p.next.next 为空,也就是 找不到新的一组节点 了,循环结束。



  • 循环解决
var swapPairs = function(head) {
if(head == null || head.next == null)
return head;
let dummyHead = p = new ListNode();
let node1, node2;
dummyHead.next = head;
while((node1 = p.next) && (node2 = p.next.next)) {
node1.next = node2.next;
node2.next = node1;
p.next = node2;
p = node1;
}
return dummyHead.next;
};


  • 递归方式


var swapPairs = function(head) {
if(head == null || head.next == null)
return head;
let node1 = head, node2 = head.next;
node1.next = swapPairs(node2.next);
node2.next = node1;
return node2;
};

4.K个一组翻转


给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。


k 是一个正整数,它的值小于或等于链表的长度。


如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。


示例


给定这个链表:1->2->3->4->5
当 k = 2 时,应当返回: 2->1->4->3->5
当 k = 3 时,应当返回: 3->2->1->4->5

说明 :


你的算法只能使用常数的额外空间。


你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。


思路
思路类似No.3中的两个一组翻转。唯一的不同在于两个一组的情况下每一组只需要反转两个节点,而在K 个一组的情况下对应的操作是将 K 个元素 的链表进行反转。



  • 递归解法
/**
* @param {ListNode} head
* @param {number} k
* @return {ListNode}
*/
var reverseKGroup = function(head, k) {
let pre = null, cur = head;
let p = head;
// 下面的循环用来检查后面的元素是否能组成一组
for(let i = 0; i < k; i++) {
if(p == null) return head;
p = p.next;
}
for(let i = 0; i < k; i++){
let next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
// pre为本组最后一个节点,cur为下一组的起点
head.next = reverseKGroup(cur, k);
return pre;
};


  • 循环解法
var reverseKGroup = function(head, k) {
let count = 0;
// 看是否能构成一组,同时统计链表元素个数
for(let p = head; p != null; p = p.next) {
if(p == null && i < k) return head;
count++;
}
let loopCount = Math.floor(count / k);
let p = dummyHead = new ListNode();
dummyHead.next = head;
// 分成了 loopCount 组,对每一个组进行反转
for(let i = 0; i < loopCount; i++) {
let pre = null, cur = p.next;
for(let j = 0; j < k; j++) {
let next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
// 当前 pre 为该组的尾结点,cur 为下一组首节点
let start = p.next;// start 是该组首节点
// 开始穿针引线!思路和2个一组的情况一模一样
p.next = pre;
start.next = cur;
p = start;
}
return dummyHead.next;
}


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

收起阅读 »

前端工程化实战 - 企业级 CLI 开发

背景 先罗列一些小团队会大概率会遇到的问题: 规范 代码没有规范,每个人的风格随心所欲,代码交付质量不可控 提交 commit 没有规范,无法从 commit 知晓提交开发内容 流程 研发没有流程,没有 prd,没有迭代的需求管理,这个项目到底做了...
继续阅读 »

背景


image.png


先罗列一些小团队会大概率会遇到的问题:



  1. 规范

    • 代码没有规范,每个人的风格随心所欲代码交付质量不可控

    • 提交 commit 没有规范,无法从 commit 知晓提交开发内容



  2. 流程

    • 研发没有流程,没有 prd,没有迭代的需求管理,这个项目到底做了点啥也不知道



  3. 效率

    • 不断的重复工作,没有技术积累与沉淀



  4. 项目质量

    • 项目没有规范就一定没有质量

    • 测试功能全部靠人工发现与回归,费时费力



  5. 部署

    • 人工构建、部署,刀耕火种般的操作

    • 依赖不统一、人为不可控

    • 没有版本追踪、回滚等功能




除了上述比较常见的几点外,其余的一些人为环境因素就不一一列举了,总结出来其实就是混乱 + 不舒服


同时处在这样的一个团队中,团队自身的规划就不明确,个人就更难对未来有一个清晰的规划与目标,容易全部陷于业务不可自拔、无限循环。


当你处在一个混乱的环境,遇事不要慌(乱世出英雄,为什么不能是你呢),先把事情捋顺,然后定个目标与规划,一步步走。


工程化


上述列举的这些问题可以通过引入工程化体系来解决,那么什么是工程化呢?


广义上,一切以提高效率、降低成本、保障质量为目的的手段,都属于工程化的范畴。


通过一系列的规范、流程、工具达到研发提效、自动化、保障质量、服务稳定、预警监控等等。


对前端而言,在 Node 出现之后,可以借助于 Node 渗透到传统界面开发之外的领域,将研发链路延伸到整个 DevOps 中去,从而脱离“切图仔”成为前端工程师。


image.png


上图是一套简单的 DevOps 流程,技术难度与成本都比较适中,作为小型团队搭建工程化的起点,性价比极高。


在团队没有制定规则,也没有基础建设的时候,通常可以先从最基础的 CLI 工具开始然后切入到整个工程化的搭建。


所以先定一个小目标,完成一个团队、项目通用的 CLI 工具。


CLI 工具分析


小团队里面的业务一般迭代比较快,能抽出来提供开发基建的时间与机会都比较少,为了避免后期的重复工作,在做基础建设之前,一定要做好规划,思考一下当前最欠缺的核心与未来可能需要用到的功能是什么?



Coding 永远不是最难的,最难的是不知道能使用 code 去做些什么有价值的事情。



image.png


参考上述的 DevOps 流程,本系列先简单规划出 CLI 的四个大模块,后续如果有需求变动再说。



可以根据自己项目的实际情况去设计 CLI 工具,本系列仅提供一个技术架构参考。



构建


通常在小团队中,构建流程都是在一套或者多套模板里面准备多环境配置文件,再使用 Webpack Or Rollup 之类的构建工具,通过 Shell 脚本或者其他操作去使用模板中预设的配置来构建项目,最后再进行部署之类的。


这的确是一个简单、通用的 CI/CD 流程,但问题来了,只要最后一步的发布配置不在可控之内,任意团队的开发成员都可以对发布的配置项做修改。


即使构建成功,也有可能会有一些不可预见的问题,比如 Webpack 的 mode 选择的是 dev 模式、没有对构建代码压缩混淆、没有注入一些全局统一方法等等,此时对生产环境而言是存在一定隐患的


所以需要将构建配置、过程从项目模板中抽离出来,统一使用 CLI 来接管构建流程,不再读取项目中的配置,而通过 CLI 使用统一配置(每一类项目都可以自定义一套标准构建配置)进行构建。


避免出现业务开发同学因为修改了错误配置而导致的生产问题。


质量


与构建是一样的场景,业务开发的时候为了方便,很多时候一些通用的自动化测试以及一些常规的格式校验都会被忽略。比如每个人开发的习惯不同也会导致使用的 ESLINT 校验规则不同,会对 ESLINT 的配置做一些额外的修改,这也是不可控的一个点。一个团队还是使用同一套代码校验规则最好。


所以也可以将自动化测试、校验从项目中剥离,使用 CLI 接管,从而保证整个团队的某一类项目代码格式的统一性。


模板


至于模板,基本上目前出现的博客中,只要是关于 CLI 的,就必然会有模板功能。


因为这个一个对团队来说,快速、便捷初始化一个项目或者拉取代码片段是非常重要的,也是作为 CLI 工具来说产出最高、收益最明显的功能模块,但本章就不做过多的介绍,放在后面模板的博文统一写。


工具合集


既然是工具合集,那么可以放一些通用的工具类在里面,比如



  1. 图片压缩(png 压缩的更小的那种)、上传 CDN 等

  2. 项目升级(比如通用配置更新了,CLI 提供一键升级模板的功能)

  3. 项目部署、发布 npm 包等操作。

  4. 等等其他一些重复性的操作,也都可以放在工具合集里面


CLI 开发


前面介绍了 CLI 的几个模块功能设计,接下来可以正式进入开发对应的 CLI 工具的环节。


搭建基础架构


CLI 工具开发将使用 TS 作为开发语言,如果此时还没有接触过 TS 的同学,刚好可以借此项目来熟悉一下 TS 的开发模式。


mkdir cli && cd cli // 创建仓库目录
npm init // 初始化 package.json
npm install -g typescript // 安装全局 TypeScript
tsc --init // 初始化 tsconfig.json

全局安装完 TypeScript 之后,初始化 tsconfig.json 之后再进行修改配置,添加编译的文件夹与输出目录。

{
"compilerOptions": {
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
"outDir": "./lib", /* Redirect output structure to the directory. */
"strict": true, /* Enable all strict type-checking options. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
},
"include": [
"./src",
]
}

上述是一份已经简化过的配置,但应对当前的开发已经足够了,后续有需要可以修改 TypeScript 的配置项。


ESLINT


因为是从 0 开发 CLI 工具,可以先从简单的功能入手,例如开发一个 Eslint 校验模块。


npm install eslint --save-dev // 安装 eslint 依赖
npx eslint --init // 初始化 eslint 配置

直接使用 eslint --init 可以快速定制出适合自己项目的 ESlint 配置文件 .eslintrc.json

{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"plugin:react/recommended",
"standard"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"react",
"@typescript-eslint"
],
"rules": {
}
}


如果项目中已经有定义好的 ESlint,可以直接使用自己的配置文件,或者根据项目需求对初始化的配置进行增改。


创建 ESlint 工具类


第一步,对照文档 ESlint Node.js API,使用提供的 Node Api 直接调用 ESlint。


将前面生成的 .eslintrc.json 的配置项按需加入,同时使用 useEslintrc:fase 禁止使用项目本身的 .eslintrc 配置,仅使用 CLI 提供的规则去校验项目代码。

import { ESLint } from 'eslint'
import { getCwdPath, countTime } from '../util'

// 1. Create an instance.
const eslint = new ESLint({
fix: true,
extensions: [".js", ".ts"],
useEslintrc: false,
overrideConfig: {
"env": {
"browser": true,
"es2021": true
},
"parser": getRePath("@typescript-eslint/parser"),
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"react",
"@typescript-eslint",
],
},
resolvePluginsRelativeTo: getDirPath('../../node_modules') // 指定 loader 加载路径
});


export const getEslint = async (path: string = 'src') => {
try {
countTime('Eslint 校验');
// 2. Lint files.
const results = await eslint.lintFiles([`${getCwdPath()}/${path}`]);

// 3. Modify the files with the fixed code.
await ESLint.outputFixes(results);

// 4. Format the results.
const formatter = await eslint.loadFormatter("stylish");

const resultText = formatter.format(results);

// 5. Output it.
if (resultText) {
console.log('请检查===》', resultText);
}
else {
console.log('完美!');
}
} catch (error) {

process.exitCode = 1;
console.error('error===>', error);
} finally {
countTime('Eslint 校验', false);
}
}

创建测试项目


npm install -g create-react-app // 全局安装 create-react-app
create-react-app test-cli // 创建测试 react 项目

测试项目使用的是 create-react-app,当然你也可以选择其他框架或者已有项目都行,这里只是作为一个 demo,并且后期也还会再用到这个项目做测试。


测试 CLI


新建 src/bin/index.ts, demo 中使用 commander 来开发命令行工具。

#!/usr/bin/env node // 这个必须添加,指定 node 运行环境
import { Command } from 'commander';
const program = new Command();

import { getEslint } from '../eslint'

program
.version('0.1.0')
.description('start eslint and fix code')
.command('eslint')
.action((value) => {
getEslint()
})
program.parse(process.argv);

修改 pageage.json,指定 bin 的运行 js(每个命令所对应的可执行文件的位置)


 "bin": {
"fe-cli": "/lib/bin/index.js"
},

先运行 tsc 将 TS 代码编译成 js,再使用 npm link 挂载到全局,即可正常使用。



commander 的具体用法就不详细介绍了,基本上市面大部分的 CLI 工具都使用 commander 作为命令行工具开发,也都有这方面的介绍。



命令行进入刚刚的测试项目,直接输入命令 fe-cli eslint,就可以正常使用 Eslint 插件,输出结果如下:


image.png


美化输出


可以看出这个时候,提示并没有那么显眼,可以使用 chalk 插件来美化一下输出。


先将测试工程故意改错一个地方,再运行命令 fe-cli eslint


image.png


至此,已经完成了一个简单的 CLI 工具,对于 ESlint 的模块,可以根据自己的想法与规划定制更多的功能。


构建模块


配置通用 Webpack


通常开发业务的时候,用的是 webpack 作为构建工具,那么 demo 也将使用 webpack 进行封装。


先命令行进入测试项目中执行命令 npm run eject,暴露 webpack 配置项。


image.png


从上图暴露出来的配置项可以看出,CRA 的 webpack 配置还是非常复杂的,毕竟是通用型的脚手架,针对各种优化配置都做了兼容,但目前 CRA 使用的还是 webpack 4 来构建。作为一个新的开发项目,CLI 可以不背技术债务,直接选择 webpack 5 来构建项目。



一般来说,构建工具替换不会影响业务代码,如果业务代码被构建工具绑架,建议还是需要去优化一下代码了。


import path from "path"

const HtmlWebpackPlugin = require('html-webpack-plugin')
const postcssNormalize = require('postcss-normalize');
import { getCwdPath, getDirPath } from '../../util'

interface IWebpack {
mode?: "development" | "production" | "none";
entry: any
output: any
template: string
}

export default ({
mode,
entry,
output,
template
}: IWebpack) => {
return {
mode,
entry,
target: 'web',
output,
module: {
rules: [{
test: /\.(js|jsx)$/,
use: {
loader: getRePath('babel-loader'),
options: {
presets: [
''@babel/preset-env',
],
},
},
exclude: [
getCwdPath('./node_modules') // 由于 node_modules 都是编译过的文件,这里做过滤处理
]
},
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 1,
},
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
[
'postcss-preset-env',
{
ident: "postcss"
},
],
],
},
}
}
],
},
{
test: /\.(woff(2)?|eot|ttf|otf|svg|)$/,
type: 'asset/inline',
},
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
loader: 'url-loader',
options: {
limit: 10000,
name: 'static/media/[name].[hash:8].[ext]',
},
},
]
},
plugins: [
new HtmlWebpackPlugin({
template,
filename: 'index.html',
}),
],
resolve: {
extensions: [
'',
'.js',
'.json',
'.sass'
]
},
}
}

上述是一份简化版本的 webpack 5 配置,再添加对应的 commander 命令。


program
.version('0.1.0')
.description('start eslint and fix code')
.command('webpack')
.action((value) => {
buildWebpack()
})

现在可以命令行进入测试工程执行 fe-cli webpack 即可得到下述构建产物


image.png


image.png


下图是使用 CRA 构建出来的产物,跟上图的构建产物对一下,能明显看出使用简化版本的 webpack 5 配置还有很多可优化的地方,那么感兴趣的同学可以再自行优化一下,作为 demo 已经完成初步的技术预研,达到了预期目标。


image.png


此时,如果熟悉构建这块的同学应该会想到,除了 webpack 的配置项外,构建中绝大部分的依赖都是来自测试工程里面的,那么如何确定 React 版本或者其他的依赖统一呢?


常规操作还是通过模板来锁定版本,但是业务同学依然可以自行调整版本依赖导致不一致,并不能保证依赖一致性。


既然整个构建都由 CLI 接管,只需要考虑将全部的依赖转移到 CLI 所在的项目依赖即可。


解决依赖


Webpack 配置项新增下述两项,指定依赖跟 loader 的加载路径,不从项目所在 node_modules 读取,而是读取 CLI 所在的 node_modules。


resolveLoader: {
modules: [getDirPath('../../node_modules')]
}, // 修改 loader 依赖路径
resolve: {
modules: [getDirPath('../../node_modules')],
}, // 修改正常模块依赖路径

同时将 babel 的 presets 模块路径修改为绝对路径,指向 CLI 的 node_modules(presets 会默认从启动路劲读取依赖)。

{
test: /\.(js|jsx)$/,
use: {
loader: getRePath('babel-loader'),
options: {
presets: [
getRePath('@babel/preset-env'),
[
getRePath("@babel/preset-react"),
{
"runtime": "automatic"
}
],
],
},
},
exclude: [
[getDirPath('../../node_modules')]
]
}

完成依赖修改之后,一起测试一下效果,先将测试工程的依赖 node_modules 全部删除


image.png


再执行 fe-cli webpack,使用 CLI 依赖来构建此项目。


image.png


image.png


可以看出,已经可以在项目不安装任何依赖的情况,使用 CLI 也可以正常构建项目了。


那么目前所有项目的依赖、构建已经全部由 CLI 接管,可以统一管理依赖与构建流程,如果需要升级依赖的话可以使用 CLI 统一进行升级,同时业务开发同学也无法对版本依赖进行改动。



这个解决方案要根据自身的实际需求来实施,所有的依赖都来源于 CLI 工具的话,版本升级影响会非常大也会非常被动,要做好兼容措施。比如哪些依赖可以取自项目,哪些依赖需要强制通用,做好取舍。



写给迷茫 Coder 们的一段话


如果遇到最开始提到那些问题的同学们,应该会经常陷入到业务中无法自拔,而且写这种基础项目,是真的很花时间也很枯燥。容易对工作厌烦,对 coding 感觉无趣。


这是很正常的,绝大多数人都有这段经历与类似的想法,但还是希望你能去多想想,在枯燥、无味、重复的工作中去发现痛点、机会。只有接近业务、熟悉业务,才有机会去优化、革新、创造。


所有的基建都是要依托业务才能发挥最大的作用


每天抽个半小时思考一下今天的工作还能在哪些方面有所提高,提高效率的不仅仅是你的代码也可以是其他的工具或者是引入新的流程。


同时也不要仅仅限制在思考阶段,有想法就争取落地,再多抽半小时进行 coding 或者找工具什么的,但凡能够提高个几分钟的效率,即使是个小工具、多几行代码、换个流程这种也值得去尝试一下。


等你把这些零碎的小东西、想法一点点全部积累起来,到最后整合到一个体系中去,那么此时你会发现已经可以站在更高一层的台阶去思考、规划下一阶段需要做的事情,而这其中所有的经历都是你未来成长的基石。


一直相信一句话:努力不会被辜负,付出终将有回报。此时敲下去的每一行代码在未来都将是你登高的一步步台阶。



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

收起阅读 »

完了,又火一个前端项目

今天逛 GitHub 的时候,在趋势榜上看到一个项目,竟然短短一天的时间内,涨了 1000 多个星星! 就是这个名为 solid 的项目: 要知道日增上千 star 可是非常难得的,我不禁感到好奇,点进去看看这个项目到底有啥牛逼的? 啥是 Solid? 这是...
继续阅读 »

今天逛 GitHub 的时候,在趋势榜上看到一个项目,竟然短短一天的时间内,涨了 1000 多个星星!


就是这个名为 solid 的项目:



要知道日增上千 star 可是非常难得的,我不禁感到好奇,点进去看看这个项目到底有啥牛逼的?


啥是 Solid?


这是一个国外的前端项目,截止到发文前,已经收获了 8400 个 star。


我总觉得这个项目很眼熟,好像之前也看到过,于是去 Star History 上搜了一下这个项目的 star 增长历史。好家伙,这几天的增速曲线几乎接近垂直,已经连续好几天增长近千了!


项目 Star 增长曲线


看到这个曲线,我想起来了,solid 是一个 JavaScript 框架,此前在一次 JavaScript 框架的性能测试中看到过它。


要知道,现在的 JavaScript 开发框架基本就是 React、Vue、Angular 三分天下,还有就是新兴的 Svelte 框架潜力无限(近 5w star),其他框架想分蛋糕还是很难的。那么 Solid 到底有什么本事,能让他连续几天 star 数暴涨呢?


描述


打开官网,官方对 Solid 的描述是:一个用于构建用户界面的 声明性 JavaScript 库,特点是高效灵活。


顺着官网往下看,Solid 有很多特点,比如压缩后的代码体积只有 6 kb;而且天然支持 TypeScript 以及 React 框架中经常编写的 JSX(JavaScript XML)。


来看看官网给的示例代码:


Solid 语法


怎么样,他的语法是不是和 React 神似?


性能


但是,这些并不能帮助 Solid 框架脱颖而出,真正牛逼的一点是它 非常快


有多快呢?第一够不够 !


JS 框架性能测试对比


有同学说了,你这不睁着眼睛说瞎话么?Solid 明明是第二,第一是 Vanilla 好吧!


哈哈,但事实上,Vanilla 其实就是不使用任何框架的纯粹的原生 JavaScript,通常作为一个性能比较的基准。


那么 Solid 为什么能做到这么快呢?甚至超越了我们引以为神的 Vue 和 React。


这是因为 Solid 没有采用其他主流前端框架中的 Virtual DOM,而是直接被静态编译为真实的原生 DOM 节点,并且将更新控制在细粒度的局部范围内。从而让 runtime(运行时)更加轻小,也不需要所谓的脏检查和摘要循环带来的额外消耗,使得性能和原生 JavaScript 几乎无异。换句话说,编译后的 Solid 其实就是 JavaScript!



其实 Solid 的原理和新兴框架 Svelte 的原理非常类似,都是编译成原生 DOM,但为啥他更快一点呢?


为了搞清楚这个问题,我打开了百度来搜这玩意,但发现在国内根本搜不到几条和 Solid.js 有关的内容,基本全是一些乱七八糟的东西。后来还是在 Google 上搜索,才找到了答案,结果答案竟然还是来自于某乎的大神伊撒尔。。。


要搞清楚为什么 Solid 比 Svelte 更快,就要看看同一段代码经过它们编译后,有什么区别。


大神很贴心地举了个例子,比如这句代码:


<div>{aaa}</div>

经 Svelte 编译后的代码:

let a1, a2
a1 = document.creatElement('div')
a2 = docment.createTextNode('')
a2.nodeValue = ctx[0] // aaa
a1.appendChild(a2)

经 Solid 编译后的代码:

let a1, a2
let fragment = document.createElement('template')
fragment.innerHTML = `<div>aaa</div>`
a1 = fragment.firstChild
a2 = a1.fristChild
a2.nodeValue = data.aaa

可以看到,在创建 DOM 节点时,原来 Solid 耍了一点小把戏,利用了 innerHTML 代替 createElement 来创建,从而进一步提升了性能。


当然,抛去 Virtual DOM 不意味着就是 “银弹” 了,毕竟十年前各种框架出现前大家也都是写原生 JavaScript,轻 runtime 也有缺点,这里就不展开说了。


除了快之外,Solid 还有一些其他的特点,比如语法精简、WebComponent 友好(可自定义元素)等。




总的来说, 我个人还是非常看好这项技术的,日后说不定能和 Svelte 一起动摇一下三巨头的地位,给大家更多的选择呢?这也是技术选型好玩的地方,没有绝对最好的技术,只有最适合的技术。


不禁感叹道:唉,技术发展太快了,一辈子学不完啊!(不过前端初学者不用关心那么多,老老实实学基础三件套 + Vue / React 就行了)


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

收起阅读 »

一文读懂JavaScript函数式编程重点-- 实践 总结

什么是函数式编程?函数式编程是一种思维方式,函数式编程与命令式编程最大的不同其实在于:函数式编程关心数据的映射,命令式编程关心解决问题的步骤。函数式编程的初衷来, 也就是: 希望可以允许程序员用计算来表示程序, 用计算的组合来表达程序的组合, 而非函数式编程则...
继续阅读 »

什么是函数式编程?

函数式编程是一种思维方式,函数式编程与命令式编程最大的不同其实在于:

函数式编程关心数据的映射,命令式编程关心解决问题的步骤。函数式编程的初衷来, 也就是: 希望可以允许程序员用计算来表示程序, 用计算的组合来表达程序的组合, 而非函数式编程则习惯于用命令来表示程序, 用命令的顺序执行来表达程序的组合。

好记性不如烂笔头,有时间将JS函数式编程,在JS方面毕竟有限,如果真要学习好函数式编程,建议学习下Haskell,本文就是将关于JS方面知识点尽可能总结全面。

  • 柯里化
  • 偏应用
  • 组合与管道
  • 函子
  • Monad

1. 柯里化

  • 什么是柯里化呢?

柯里化是把一个多参数函数转化为一个嵌套的一元函数的过程。下面我们用介绍柯里化时候很多文章都会使用的例子,加法例子(bad smile)。

// 原始版本
const add = (x,y) => x + y;

// ES6 柯里化版本
const addCurried = x => y => x + y;

你没有看错,就是这么简单,柯里化就是将之前传入的多参数变为传入单参数,解释下,柯里化版本,其实当传入一个参数addCurried(1)时,实际会返回一个函数 y=>1+y,实际上是将add函数转化为含有嵌套的一元函数的addCurried函数。如果要调用柯里化版本,应该使用addCurried(1)(2)方式进行调用 会达到和add(1,2)一样的效果,n 个连续箭头组成的函数实际上就是柯里化了 n - 1次,前 n - 1 次调用,其实是提前将参数传递进去,并没有调用最内层函数体,最后一次调用才会调用最内层函数体,并返回最内层函数体的返回值。

看到这里感觉是不是很熟悉,没错,React 中间件。

以上是通过ES6箭头函数实现的,下面我们构建curryFn来实现这个过程。

此函数应该比较容易理解,比较函数参数以及参数列表的长度,递归调用合并参数,当参数都为3,不满足,调用fn.apply(null, args)。

例子: 使用以上的curryFn 数组元素平方函数式写法。

const curryFn = (fn) => {
if(typeof fn !== 'function'){
throw Error ('Not Function');
}
return function curriedFn(...args){
if(args.length < fn.length){
return function(){
return curriedFn.apply(null, args.concat(
[].slice.call(arguments)
))
}
}
return fn.apply(null, args);
}
}
const map = (fn, arr) => arr.map(fn);
const square = (x) => x * x;
const squareFn = curryFn(map)(square)([1,2,3])

从上例子可以观察出curryFn函数应用参数顺序是从左到右。如果想从右到左,下面一会会介绍。

2. 偏应用

上面柯里化我们介绍了我们对于传入多个参数变量的情况,如何处理参数关系,实际开发中存在一种情况,写一个方法,有些参数是固定不变的,即我们需要部分更改参数,不同于柯里化得全部应用参数。

const partial = function (fn, ...partialArgs) {
let args = partialArgs;
return function(...fullArguments) {
let arg = 0;
for (let i = 0; i < args.length && arg < fullArguments.length; i++) {
if (args[i] === null) {
args[i] = fullArguments[arg++];
}
}
return fn.apply(null, args)
}
}
partial(JSON.stringify,null,null,2)({foo: 'bar', bar: 'foo'})


应用起来 2 这个参数是不变的,相当于常量。简单解释下这个函数,args指向 [null, null, 2], fullArguments指向 [{foo:'bar', bar:'foo'}] ,当i==0时候 ,这样 args[0] ==fullArguments[0],所以args就为[{foo:'bar', bar:'foo'},null,2],然后调用,fn.apply(null, args)。

3. 组合与管道

组合

组合与管道的概念来源于Unix,它提倡的概念大概就是每个程序的输出应该是另一个未知程序的输入。我们应该实现的是不应该创建新函数就可以通过compose一些纯函数解决问题。

  • 双函数情况
const compose = (a, b) => c => a(b(c))

我们来应用下:

const toNumber = (num) => Number(num);
const toRound = (num)=> Math.round(num);
// 使用compose
number = compose(toRound,toNumber)('4.67'); // 5
  • 多函数情况

我们重写上面例子测试:

const compose = (...fns) => (value) => fns.reverse().reduce((acc, fn) => fn(acc), value)
const toNumber = (num) => Number(num);
const toRound = (num)=> Math.round(num);
const toString = (num) => num.toString();
number = compose(toString,toRound,toNumber)('4.67'); // 字符串 '5'

从上面多参数以及双参数情况,我们可以得出compose的数据流是从右到左的。那有没有一种数据流是从左到右的,答案是有的就是下面我们要介绍的管道。

管道

管道我们一般称为pipe函数,与compose函数相同,只不过是修改了数据流流向而已。

const pipe = (...fns) => (value) => fns.reduce((acc, fn) => fn(acc), value);
const toNumber = (num) => Number(num);
const toRound = (num)=> Math.round(num);
const toString = (num) => num.toString();
number = compose(toString,toRound,toNumber)('4.67'); // 数字 5

4. 函子

函子(Functor)即用一种纯函数的方式帮我们处理异常错误,它是一个普通对象,并且实现了map函数,在遍历每个对象值得时候生成一个新对象。我们来看几个实用些的函子。

  • MayBe 函子
  • // MayBe 函数定义
    const MayBe = function (val) {
    this.value = val;
    }
    MayBe.of = function (val) {
    return new MayBe(val);
    }
    // MayBe map 函数定义
    MayBe.prototype.isNothing = function () {
    return (this.value === null || this.value === underfind)
    }
    MayBe.prototype.map = function (fn) {
    return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this.value));
    }


MayBe并不关心参数是否为null或者underfind,因为它已经被MayBe函子抽象出来了,代码不会因为null或者underfind崩溃,可以看出,通过函子我们不需要关系那些特殊情况下的判断,程序也不会以为的崩溃。

另外一点是,当都多个map链式调用时,如果第一个map参数是null或者underfind,并不会影响到第二个map正常运行,也就是说,任何map的链式调用都会调用到。

MayBe.of('abc').map((x)=>x.toUpperCase()) // MayBe { value: 'ABC' }

// 参数为null
MayBe.of(null).map((x)=>x.toUpperCase()) // MayBe { value: null }

// 链式调用中第一个参数为null
MayBe.of('abc').map(()=>null).map((x)=> 'start' + x) // MayBe { value: null }


  • Either函子

Either函子主要解决的是MayBe函子在执行失败时不能判断哪一只分支出问题而出现的,主要解决的分支扩展的问题。

我们实现一下Either函子:

const Nothing = function (val) {
this.value = val;
}
Nothing.of = function (val) {
return new Nothing(val);
}
Nothing.prototype.map = function (f) {
return this;
}
const Some = function(val){
this.value = val;
}
Some.of = function(val) {
this.value = val;
}
Some.prototype.map = function(fn) {
return Some.of(fn(this.value))
}
const Either = {
Some: Some,
Nothing: Nothing
}


实现包含两个函数,Nothing函数只返回函数自身,Some则会执行map部分,在实际应用中,可以将错误处理使用Nothing,需要执行使用Some,这样就可以分辨出分支出现的问题。

5. Monad

Monad应该是这几个中最难理解的概念了,因为本人也没有学过Haskell,所以也可能对Monad理解不是很准确,所以犹豫要不要写出来,打算学习Haskell,好吧,先记录下自己理解,永远不做无病呻吟,有自己感触与理解才会记录,学过之后再次补充。

Monad就是一种设计模式,表示将一个运算过程,通过函数拆解成互相连接的多个步骤。你只要提供下一步运算所需的函数,整个运算就会自动进行下去。那么构成Monad 组成条件有哪些呢?

  • 类型构造器,因为Monad实际处理的是数据类型,而不是值,必须有一个类型构造器,这个类型构造器的作用就是如何从指定类型构造新的一元类型,比如Maybe<number>,定义Maybe<number>了基础类型的类型number,我们月可以把这种类型构造器理解为封装了一个值,这个值既可以是用数据结构进行封装,也可以使用函数,通过返回值表达封装的值,一般也说Monad是一个“未计算的值”、“包含在上下文(context)中的值”。
  • 提升函数。这个提升函数一般指的是return或者unit,说白了,提升函数就是将一个值封装进了Monad这个数据结构中,签名为 return :: a -> M a 。将unit基础类型的值包装到monad中的函数。对于Maybe monad,它将2类型number的值包装到类型的值Maybe(2)Maybe<number>。
  • 绑定函数bind。绑定函数就像一个管道,它解封一个Monad,将里面的值传到第二个参数表示的函数,生成另一个Monad。形式化定义为[公式](ma 为类型为[公式]的 Monad 实例,[公式]是转换函数)。此bind功能是不一样的Function.prototype.bind 功能。它用于创建具有绑定this值的部分应用函数或函数。

就像一个盒子一样,放进盒子里面(提升函数),从盒子里面取出来(绑定函数),放进另外一个盒子里面(提升函数),本身这个盒子就是类型构造器。

举一个常用的例子,这也是Monad for functional programming,里面除法的例子,实现一个求值函数evaluate,它可以接收类似[公式]

function evaluate(e: Expr): Maybe<number> {
if (e.type === 'value') return Maybe.just(<number>e.value);

return evaluate((<DivisionExpr>e.value).left)
.bind(left => evaluate((<DivisionExpr>e.value).right)
.bind(right => safeDiv(left, right)));
}

在像JavaScript这样的面向对象语言中,unit函数可以表示为构造函数,函数可以表示为bind实例方法。

还有三个遵守的monadic法则:

  1. bind(unit(x), f) ≡ f(x)
  2. bind(munit) ≡ m
  3. bind(bind(mf), g) ≡ bind(mx ⇒ bind(f(x), g))
  4. const unit = (value: number) => Maybe.just<number>(value);
    const f = (value: number) => Maybe.just<number>(value * 2);
    const g = (value: number) => Maybe.just<number>(value - 5);
    const ma = Maybe.just<number>(13);
    const assertEqual = (x: Maybe<number>, y: Maybe<number>) => x.value === y.value;

    // first law
    assertEqual(unit(5).bind(f), f(5));

    // second law
    assertEqual(ma.bind(unit), ma);

    // third law
    assertEqual(ma.bind(f).bind(g), ma.bind(value => f(value).bind(g)));


前两个说这unit是一个中性元素。第三个说bind应该是关联的 - 绑定的顺序无关紧要。这是添加具有的相同属性:(8 + 4) + 2与...相同8 + (4 + 2)。

举几个比较常见的Monad:

1. Promise Monad

没有想到吧,你平时使用的Promise就是高大上的Monad,它是如何体现的这三个特性呢?

  • 类型构造器就是Promise
  • unit提升函数 为x => Promise.resolve(x)
  • 绑定函数 为Promise.prototype.then
fetch('xxx')
.then(response => response.json())
.then(o => fetch(`xxxo`))
.then(response => response.json())
.then(v => console.log(v));

最简单的 P(A).then(B) 实现里,它的 P(A) 相当于 Monad 中的 unit 接口,能够把任意值包装到 Monad 容器里。支持嵌套的 Promise 实现中,它的 then 背后其实是 FP 中的 join 概念,在容器里还装着容器的时候,递归地把内层容器拆开,返回最底层装着的值。Promise 的链式调用背后,其实是 Monad 中的 bind 概念。你可以扁平地串联一堆 .then(),往里传入各种函数,Promise 能够帮你抹平同步和异步的差异,把这些函数逐个应用到容器里的值上。回归这节中最原始的问题,Monad 是什么呢?只要满足以上三个条件,我们就可以认为它是 Monad 了:正如我们已经看到的,Promise.resolve() 能够把任意值包装到 Promise 里,而 Promise/A+ 规范里的 Resolve 算法则实际上实现了 bind。因此,我们可以认为:Promise 就是一个 Monad。

2. Continuation Monad

continuation monad用于异步任务。幸运的是,ES6没有必要实现它 - Prmise对象是这个monad的一个实现。

  • Promise.resolve(value)包装一个值并返回一个promise(unit函数)。
  • Promise.prototype.then(onFullfill: value => Promise)将一个值转换为另一个promise并返回一个promise(bind函数)的函数作为参数。

Promise为基本的continuation monad提供了几个扩展。如果then返回一个简单的值(而不是一个promise对象), 他将被视为Promise,解析为该值 自动将一个值包装在monad中。

第二个区别在于错误传播。Continuation monad允许在计算步骤之间仅传递一个值。另一方面,Promise有两个不同的值 - 一个用于成功值,一个用于错误(类似于Either monad)。可以使用方法的第二个回调then或使用特殊。catch方法捕获错误。

下面定义了一个简单的Monad类型,它单纯封装了一个值作为value属性:

var Monad = function (v) {
this.value = v;
return this;
};

Monad.prototype.bind = function (f) {
return f(this.value)
};

var lift = function (v) {
return new Monad(v);
};

我们将一个除以2的函数应用的这个Monad:

console.log(lift(32).bind(function (a) {
return lift(a/2);
}));

// > Monad { value: 16 }

连续应用除以2的函数:

// 方便展示用的辅助函数,请忽视它是个有副作用的函数。
var print = function (a) {
console.log(a);
return lift(a);
};

var half = function (a) {
return lift(a/2);
};

lift(32)
.bind(half)
.bind(print)
.bind(half)
.bind(print);

//output:
// > 16
// > 8


收起阅读 »

Vue路由懒加载

vue
Vue路由懒加载对于SPA单页应用,当打包构建时,JavaScript包会变得非常大,影响页面加载速度,将不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这就是路由的懒加载。实现方式#Vue异步组件#Vue允许以一个工厂函数的方式定...
继续阅读 »

Vue路由懒加载

对于SPA单页应用,当打包构建时,JavaScript包会变得非常大,影响页面加载速度,将不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这就是路由的懒加载。

实现方式#

Vue异步组件#

Vue允许以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。

Vue.component("async-example", function (resolve, reject) {
setTimeout(function() {
// 向 `resolve` 回调传递组件定义
resolve({
template: "
I am async!
"
})
}, 1000)
})


这个工厂函数会收到一个resolve回调,这个回调函数会在你从服务器得到组件定义的时候被调用,当然也可以调用reject(reason)来表示加载失败,此处的setTimeout仅是用来演示异步传递组件定义用。将异步组件和webpackcode-splitting功能一起配合使用可以达到懒加载组件的效果。

Vue.component("async-webpack-example", function (resolve) {
// 这个特殊的 `require` 语法将会告诉 webpack
// 自动将你的构建代码切割成多个包,这些包
// 会通过 Ajax 请求加载
require(["./my-async-component"], resolve)
})

也可以在工厂函数中返回一个Promise,把webpack 2ES2015语法加在一起。

Vue.component(
"async-webpack-example",
// 这个动态导入会返回一个 `Promise` 对象。
() => import("./my-async-component")
)


事实上我们在Vue-Router的配置上可以直接结合Vue的异步组件和Webpack的代码分割功能可以实现路由组件的懒加载,打包后每一个组件生成一个js文件。

{
path: "/example",
name: "example",
//打包后,每个组件单独生成一个chunk文件
component: reslove => require(["@/views/example.vue"], resolve)
}

动态import#

Webpack2中,可以使用动态import语法来定义代码分块点split point,官方也是推荐使用这种方法,如果使用的是Bable,需要添加syntax-dynamic-import插件, 才能使Babel可以正确的解析语法。

//默认将每个组件,单独打包成一个js文件
const example = () => import("@/views/example.vue")

有时我们想把某个路由下的所有组件都打包在同一个异步块chunk中,需要使用命名chunk一个特殊的注释语法来提供chunk name,需要webpack > 2.4

const example1 = () => import(/* webpackChunkName: "Example" */ "@/views/example1.vue")
const example2 = () => import(/* webpackChunkName: "Example" */ "@/views/example2.vue");

事实上我们在Vue-Router的配置上可以直接定义懒加载。

{
path: "/example",
name: "example",
//打包后,每个组件单独生成一个chunk文件
component: () => import("@/views/example.vue")
}

webpack提供的require.ensure#

使用webpackrequire.ensure,也可以实现按需加载,同样多个路由指定相同的chunkName也会合并打包成一个js文件。

// require.ensure(dependencies: String[], callback: function(require), chunkName: String)
{
path: "/example1",
name: "example1",
component: resolve => require.ensure([], () => resolve(require("@/views/example1.vue")), "Example")
},
{
path: "/example2",
name: "example2",
component: resolve => require.ensure([], () => resolve(require("@/views/example2.vue")), "Example")
}




收起阅读 »

一份传男也传女的 React Native 学习笔记

这段时间了解了一些前端方面的知识,并且用 React Native 写了一个简易新闻客户端 Demo。 React Native 和原生开发各有所长,具体就不细说。混合使用能充分发挥各自长处,唯一的缺憾就是 React Native 和原生通信过程相对不那么友...
继续阅读 »

这段时间了解了一些前端方面的知识,并且用 React Native 写了一个简易新闻客户端 Demo。


React Native 和原生开发各有所长,具体就不细说。混合使用能充分发挥各自长处,唯一的缺憾就是 React Native 和原生通信过程相对不那么友好。


在这里分享一下学习过程中个人认为比较重要的知识点和学习资料,本文尽量写得轻一些,希望对读者能够有所帮助。


预备知识


有些前端经验的小伙伴学起 React Native 就像老马识途,东西都差不多,变来变去也玩不出什么花样。


HTML5:H5 元素对比 React Native 组件,使用方式如出一辙。


CSS:React Native 的 FlexBox 用来为组件布局的,和 CSS 亲兄弟关系。


JavaScript:用 JavaScript 写,能不了解一下吗? JavaScript 之于 React Native 就如同砖瓦之于摩天大楼。


React JSX:React 使用 JSX 来替代常规的 JavaScript。JSX 是一个看起来很像 XML 的 JavaScript 语法扩展。


一、开始学习 React Native


图片来自网络


React Native 社区相对比较成熟,中文站的内容也比较全面,从入门到进阶,环境安装到使用指南,学习 React Native 推荐从官网 reactnative.cn 开始。FlexBox 布局、组件、API 建议在该官网查看,注意网页顶部可以切换 React Native 的历史版本。


1.1 安装开发环境



  1. React Native 官网推荐搭建开发环境指南传送门。(记得设置 App Transport Security Settings ,允许 http 请求)

  2. 已建立原生项目,将 React Native 集成到现有原生项目传送门

  3. 基于第2点,React Native 与原生混编的情况下,React Native 与原生如何通信传送门

  4. 在 IDE 选择这一点上,不要过多纠结,个人使用 WebStorm ,比较省心。


1.2 生命周期

class Clock extends React.Component {
// 构造函数 通常进行一些初始化操作 如定义 state 初始值
constructor(props) {
super(props);
}

// 组件已挂载
componentDidMount() {}

// 组件即将被卸载
componentWillUnmount() {}

// 渲染函数
render() {
return (
<View></View>
);
}
}


1.3 Props 与 State


一个组件所有的数据来自于 Props 与 State ,分布是外部传入的属性和内部状态。


Props 是父组件给子组件传递数据用的,Props 由外部传入后无法改变,可以同时传递多个属性。

// 父组件 传递一个属性 name 给子组件
<Greeting name='xietao3' />

// 子组件使用父组件传递下来的属性 name
<Text>Hello {this.props.name}!</Text>


State :用来控制组件内部状态,每次修改都会重新渲染组件。

// 初始化 state
constructor(props) {
super(props);
this.state = { showText: 'hello xietao3' };
}

// 使用 state
render() {
// 根据当前showText的值决定显示内容
return (
<Text>{this.state.showText}</Text>
);
}

// 修改state,触发 render 函数,重新渲染页面
this.setState({showText: 'hello world'});


举个栗子(如果理解了就跳过吧):


我们使用两种数据来控制一个组件:props 和 state。 props 是在父组件中指定,而且一经指定,在被指定的组件的生命周期中则不再改变。 对于需要改变的数据,我们需要使用 state 。


一般来说,你需要在 constructor 中初始化 state ,然后在需要修改时调用setState方法。


假如我们需要制作一段不停闪烁的文字。文字内容本身在组件创建时就已经指定好了,所以文字内容应该是一个 prop 。而文字的显示或隐藏的状态(快速的显隐切换就产生了闪烁的效果)则是随着时间变化的,因此这一状态应该写到 state 中。


1.4 组件与 API


说到组件就不得不说 React Native 的组件化思想,尼古拉斯 · 赵四 曾经说过,组合由于继承。简单来说就是多级封装嵌套、组合使用,提高基础组件复用率。


组件怎么用?


授人以鱼不如授人以渔,点击这里打开官方文档 ,在左边导航栏中找到你想使用的组件并且点击,里面就有组件的使用方式和属性的详细介绍。


关于 API


建议写第一个 Demo 之前把所有 API 浏览一遍,磨刀不误砍柴工,不一定要会用,但一定要知道这些提供了哪些功能,后面开发中可能会用得上。API 列表同样可以在官网左边导航栏中找到。


二、助力 React Native 起飞


以下内容不建议在第一个 Demo 中使用:


2.1 Redux


Redux(中文教程英文教程) 是 JavaScript 状态容器,提供可预测化的状态管理。


部分推荐教程:



2.2 CodePush


React Native 热更新的发动机,接入的时候绕了很多圈圈,后面发现接入还挺方便的。CodePush 除了可以使用微软提供的服务进行热更新之外,还可以自建服务器进行热更新。


推荐教程:



三、 与原生端通信


3.1 在 React Native 中使用原生UI组件


填坑:



  • 原生端的 Manager 文件如果有 RCT 前缀,在 RN 中引用的时候不要加 RCT。

  • 原生 UI 组件的 RCTBubblingEventBlock 类型属性命名一定要以 on 开头,例如 onChange。


3.2 在 React Native 中发消息通知给原生端(由于RN调用原生端是异步的,最好在回调中通过通知把消息传递到具体的类)


3.3 在原生端发消息通知给 React Native (建议在Manager中写一个类方法,这样外部也可以灵活发送通知)


这里其实是有 Demo 的,但是还没整理好🤦️。


四、React Native 进阶资源


有时候一下子看到好多感兴趣的东西,容易分散注意力,在未到达一定水平之前建议不要想太多,入门看官网就足够了。当你掌握了那些知识之后,你就可以拓展一下你的知识库了。



  • awesome-react-native 19000+⭐️(包含热门文章、信息、第三方库、工具、学习书籍视频等)

  • react-native-guide 11900+⭐️ (中文 react-native 学习资源、开 源App 和组件)

  • js.coach (第三方库搜索平台)

  • 个人收集的一些开源项目(读万卷书不如行万里路,行万里路不如阅码无数!经常看看别人的代码,总会有新收获的)


五、React Native 第一个小 Demo


5.1 MonkeyNews 简介


MonkeyNews,纯 React Native 新闻客户端,部分参考知乎日报,并且使用了其部分接口
由于是练手项目,仅供参考,这里附上 GitHub 地址,感兴趣的可以了解(star)一下。


首页


频道


个人中心


5.2 用到的第三方库:



  • react-native-code-push:React Native 热更新

  • react-native-swiper:用于轮播图

  • react-navigation:TabBar + NavigationBar


5.3 项目结构



Common



MKSwiper.js

MKNewsListItem.js
MKImage.js

MKPlaceholderView.js

MKThemeListItem.js

MKLoadingView.js

...





Config



MKConstants.js





Pages



Home



MKHomePage.js

MKNewsDetailPage.js





Category



MKCategoryPage.js

MKThemeDetailPage.js





UserCenter



MKUserCenterPage.js






Services



MKServices.js

APIConstants.js





Styles



commonStyles.js




六、总结


在对 React Native 有了一些了解之后,个人感觉目前 React Native 的状况很难替代原生开发,至少现阶段还不行。


个人认为的缺点:React Native 的双端运行的优点并不明显,很多原生 API 使用起来都比较麻烦,很大程度上抵消了双端运行带来的开发效率提升,这种情况下我甚至更愿意用原生 iOS 和 Android 各写一套。


优点:React Native 和原生组合使用,通过动态路由动态在原生页面和 React Native 页面之间切换,可以在原生页面出现 bug 的时候切换至 React Native 页面,或者比较简单的页面直接使用 React Native 开发都是非常不错的。


总之, React Native 也是可以大有作为的。



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

收起阅读 »

React的路由,怎么开发得劲儿

首先确定业务场景如果我们把场景设定在开发一个pc端管理后台的话,那么很常见的需求就是根据不同用户,配置不同的权限,显示不同的菜单项目,渲染不同的路由。那权限到底归谁管一般来说都是后台配置权限,然后驱动前端显示菜单,但我觉得这样不太好,加一个menu就要向后台申...
继续阅读 »

首先确定业务场景

如果我们把场景设定在开发一个pc端管理后台的话,那么很常见的需求就是根据不同用户,配置不同的权限,显示不同的菜单项目,渲染不同的路由。

那权限到底归谁管

一般来说都是后台配置权限,然后驱动前端显示菜单,但我觉得这样不太好,加一个menu就要向后台申请,太不灵活,费劲儿。

我觉得应该也给前台一定程度的权利,让其可以“绕过”后台主导一部分菜单项和路由项的渲染.

__一言以蔽之__:

前后台协同把事情办了,后台为主,前端为辅。

基于以上分析,制定了一个解决方案

首先列出一下“出场角色”:

动态结构数据 :通过前后台协同创建数据,其描述的是一种树状关系。

静态内容数据 :渲染路由和菜单项的基本数据信息。

菜单项和其关联的路由 :根据以上数据驱动显示。

静态内容配置

主要为两个成员:
  • 路由配置:routesMap
  • 菜单项配置:menusMap

    二者相关性太高,故在一起进行管理。

路由配置:routesMap

作用:

每一个路由都是一个单体对象,通过注册routesMap内部来进行统一管理。

结构:
{
...
{
name: "commonTitle_nest", //国际化单位ID
icon: "thunderbolt", //antd的icon
path: "/pageCenter/nestRoute", //路径规则
exact: true, //是否严格匹配
component: lazyImport(() =>
import('ROUTES/login')
), //组件
key: uuid() //唯一标识
}
...
}


个体参数一览:
参数类型说明默认值
namestring国际化的标识ID_
iconstringantd的icon标识-
pathstring路径规则-
exactboolan是否严格匹配false
componentstring渲染组件-
keystring唯一标识-
redirectstring重定向路由地址-
searchobject"?="-
paramstringnumber"/*"-
isNoFormatboolean标识拒绝国际化false

基本是在react-router基础上进行扩展的,保留了其配置项。

菜单项配置:menusMap

作用:

每个显示在左侧的菜单项目都是一个单体对象,菜单单体内容与路由对象进行关联,并通过注册routesToMenuMap内部来进行统一管理。

结构:
{
...
[LIGHT_ID]: {
...routesMap.lightHome,
routes: [
routesMap.lightAdd,
routesMap.lightEdit,
routesMap.lightDetail,
],
}
...
}


个体参数一览:
参数类型说明默认值
routesarray转载路由个体_

该个体主要关联路由个体,故其参数基本与之一致

动态结构配置

主要为两个类别:
  • __menuLocalConfig.json__:前端期望的驱动数据。
  • __menuRemoteConfig.json__:后端期望的驱动数据。
作用:

__动静结合,驱动显示__:两文件融合作为动态数据,去激活静态数据(菜单项menusMap)来驱动显示菜单项目和渲染路由组件。

强调:
  • __menuLocalConfig.json__:是动态数据的组成部份,是“动”中的“静”,完全由前端主导配置。
  • __menuRemoteConfig.json__:应该由后台配置权限并提供,前端配置该数据文件,目的是在后台未返回数据作默认配置,还有模拟mock开发使用。
结构:
[   
...
{
"menuId": 2001,
"parentId": 1001
}
...
]

简单,直接地去表示结构的数据集合

动态配置的解释:

简单讲,对于驱动菜单项和路由的渲染,无论后台配置权限控制前端也好,前端想绕过后端主导显示也好,都是一种期望(种因)。二者协商,结合,用尽可能少的信息描述一个结构(枝繁),从而让静态数据对其进行补充(叶茂),然后再用形成的整体去驱动(结果)。

快速上手

注册路由个体

位置在/src/routes/config.js,栗:

/* 路由的注册数据,新建路由在这配置 */
export const routesMap = {
...
templates: {
name: "commonTitle_nest",
icon: "thunderbolt",
path: "/pageCenter/nestRoute",
exact: true,
redirect: "/pageCenter/light",
key: uuid()
}
...
}


详:/路由相关/配置/静态内容配置

决定该路由个体的“出场”

位置同上,栗:

/* 路由匹配menu的注册数据,新建后台驱动的menu在这配置 */
export const menusMap = {
...
[LIGHT_ID]: {
...routesMap.lightHome, //“主角”
routes: [
routesMap.lightAdd, //“配角”
routesMap.lightEdit,
routesMap.lightDetail,
],
},
...
}


解:首先路由个体出现在该配置中,就说明出场(驱动渲染route)了,但是出场又分为两种:

类别驱动显示了左侧 MenuItem可以跳转么
主角可以
配角没有可以

以上就已经完成了静态数据的准备,接下来就等动态结构数据类激活它了。

配置动态结构数据

后台配置的权限:
[
{ "menuId": 1002, "parentId": 0 },
{ "menuId": 1001, "parentId": 0 }
]

主导

前端自定义的权限:
[
{ "menuId": 2002, "parentId": 1001 },
{ "menuId": 2001, "parentId": 1001 },
{ "menuId": 2003, "parentId": 0 },
{ "menuId": 2004, "parentId": 1002 },
{ "menuId": 2005, "parentId": 1002 }
]


补充

解:1***2***分别是后台和前台的命名约定(能区分就行,怎么定随意),通过以上数据不难看出二者结合描述了一个树状关系,进而去激活静态数据以驱动渲染页面的菜单和路由。

简单讲:就是动态数据描述结构,静态数据描述内容,结构去和内容进行匹配,有就显示,没有也不会出问题,二者配合驱动显示。

至此配置基本完成,可以通过直接修改该文件的方式进行开发和调整,也可以可视化操作。

配置调整费劲?拖拽吧

操作后自动刷新。

自动生成文件
menuLocalConfig.json

menuRemoteConfig.json

总结:

这样我觉得react的路由开发起来得劲儿了不少,整体的解决方案已经确定,供参考。

收起阅读 »

宝, 来学习一下CSS中的宽高比,让 h5 开发更想你的夜!

在图像和其他响应式元素的宽度和高度之间有一个一致的比例是很重要的。在CSS中,我们使用padding hack已经很多年了,但现在我们在CSS中有了原生的长宽比支持。在这篇文章中,我们将讨论什么是宽高比,我们过去是怎么做的,新的做法是什么。当然,也会有一些用例...
继续阅读 »

在图像和其他响应式元素的宽度和高度之间有一个一致的比例是很重要的。在CSS中,我们使用padding hack已经很多年了,但现在我们在CSS中有了原生的长宽比支持。

在这篇文章中,我们将讨论什么是宽高比,我们过去是怎么做的,新的做法是什么。当然,也会有一些用例,对它们进行适当的回退。

什么是高宽比

根据维基百科的说法:

在数学上,比率表示一个数字包含另一个数字的多少倍。例如,如果一碗水果中有八个橙子和六个柠檬,那么橙子和柠檬的比例是八比六(即8∶6,相当于比值4∶3)。

在网页设计中,高宽比的概念是用来描述图像的宽度和高度应按比例调整。

考虑下图

比率是4:3,这表明苹果和葡萄的比例是4:3

换句话说,我们可以为宽高比为4:3的最小框是4px * 3px框。 当此盒式高度按比例调整为其宽度时,我们将有一个致宽尺寸的框。

考虑下图。

盒子被按比例调整大小,其宽度和高度之间的比例是一致的。现在,让我们想象一下,这个盒子里有一张重要的图片,我们关心它的所有细节。

请注意,无论大小如何,图像细节都被保留。通过拥有一致的高宽比,我们可以获得以下好处

  • 整个网站的图像将在不同的视口大小上保持一致。
  • 我们也可以有响应式的视频元素。
  • 它有助于设计师创建一个图像大小的清晰指南,这样开发者就可以在开发过程中处理它们。

计算宽高比

为了测量宽高比,我们需要将宽度除以如下图所示的高度。

宽度和高度之间的比例是1.33。这意味着这个比例应该得到遵守。请考虑

注意右边的图片,宽度÷高度的值是 1.02,这不是原来的长宽比(1.33或4:3)。

你可能在想,如何得出4:3这个数值?嗯,这被称为最接近的正常长宽比,有一些工具可以帮助我们找到它。在进行UI设计时,强烈建议你确切地知道你所使用的图像的宽高比是多少。使用这个网址可以帮我们快速计算。

网址地址:http://lawlesscreation.github...

在 CSS 中实现宽高比

我们过去是通过在CSS中使用百分比padding 来实现宽高比的。好消息是,最近,我们在所有主要的浏览器中都得到了aspect-ratio的原生支持。在深入了解原生方式之前,我们先首先解释一下好的老方法。

当一个元素有一个垂直百分比的padding时,它将基于它的父级宽度。请看下图。

当标题有padding-top: 50%时,该值是根据其父元素的宽度来计算的。因为父元素的宽度是200px,所以padding-top会变成100px

为了找出要使用的百分比值,我们需要将图像的高度除以宽度。得到的数字就是我们要使用的百分比。

假设图像宽度为260px,高度为195px

Percentage padding = height / width

195/260的结果为 0.75(或75%)。

我们假设有一个卡片的网格,每张卡片都有一个缩略图。这些缩略图的宽度和高度应该是相等的。

由于某些原因,运营上传了一张与其他图片大小不一致的图片。注意到中间那张卡的高度与其他卡的高度不一样。

你可能会想,这还不容易解决?我们可以给图片加个object-fit: cover。问题解决了,对吗?不是这么简单滴。这个解决方案在多种视口尺寸下都不会好看。

注意到在中等尺寸下,固定高度的图片从左边和右边被裁剪得太厉害,而在手机上,它们又太宽。所有这些都是由于使用了固定高度的原因。我们可以通过不同的媒体查询手动调整高度,但这不是一个实用的解决方案。

我们需要的是,无论视口大小如何,缩略图的尺寸都要一致。为了实现这一点,我们需要使用百分比padding来实现一个宽高比。

HTML

<article class="card">
<div class="card__thumb">
<img src="thumb.jpg" alt="" />
</div>
<div class="card__content">
<h3>Muffins Recipe</h3>
<p>Servings: 3</p>
</div>
</article>

CSS

.card__thumb {
position: relative;
padding-top: 75%;
}

.card__thumb img {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
object-fit: cover;
}


通过上述,我们定义了卡片缩略图包装器(.card__thumb)的高度取决于其宽度。另外,图片是绝对定位的,它有它的父元素的全部宽度和高度,有object-fit: cover,用于上传不同大小的图片的情况。请看下面的动图。

请注意,卡片大小的变化和缩略图的长宽比没有受到影响。

aspect-ratio 属性

今年早些时候,Chrome、Safari TP和Firefox Nightly都支持aspect-ratio CSS 属性。最近,它在Safari 15的官方版本中得到支持。

我们回到前面的例子,我们可以这样改写它。

/* 上面的方式 */
.card__thumb {
position: relative;
padding-top: 75%;
}

/* 使用 aspect-ratio 属性 */
.card__thumb {
position: relative;
aspect-ratio: 4/3;
}


请看下面的动图,了解宽高比是如何变化的。

Demo 地址:https://codepen.io/shadeed/pe...

有了这个,让我们探索原始纵横比可以有用的一些用例,以及如何以逐步增强的方法使用它。

渐进增强

我们可以通过使用CSS @supports和CSS变量来使用CSS aspect-ratio

.card {
--aspect-ratio: 16/9;
padding-top: calc((1 / (var(--aspect-ratio))) * 100%);
}

@supports (aspect-ratio: 1) {
.card {
aspect-ratio: var(--aspect-ratio);
padding-top: initial;
}
}


Logo Images

来看看下面的 logo

你是否注意到它们的尺寸是一致的,而且它们是对齐的?来看看幕后的情况。

// html
<li class="brands__item">
<a href="#">
<img src="assets/batch-2/aanaab.png" alt="" />
</a>
</li>
.brands__item a {
padding: 1rem;
}

.brands__item img {
width: 130px;
object-fit: contain;
aspect-ratio: 2/1;
}


我添加了一个130px的基本宽度,以便有一个最小的尺寸,而aspect-ratio会照顾到高度。

蓝色区域是图像的大小,object-fit: contain是重要的,避免扭曲图像。

Responsive Circles

你是否曾经需要创建一个应该是响应式的圆形元素?CSS aspect-ratio是这种使用情况的最佳选择。

.person {
width: 180px;
aspect-ratio: 1;
}

如果宽高比的两个值相同,我们可以写成aspect-ratio: 1而不是aspect-ratio: 1/1。如果你使用flexboxgrid ,宽度将是可选的,它可以被添加作为一个最小值。

~完,我是小智,宝,你学会了吗~


代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文:https://ishadeed.com/article/...


收起阅读 »

前端白屏监控探索

背景不知从什么时候开始,前端白屏问题成为一个非常普遍的话题,'白屏' 甚至成为了前端 bug 的代名词:_喂,你的页面白了。_而且,'白' 这一现象似乎对于用户体感上来说更加强,回忆起 windows 系统的崩溃 '蓝屏'。可以说是非常相似了,甚至能明白了白屏...
继续阅读 »

背景

不知从什么时候开始,前端白屏问题成为一个非常普遍的话题,'白屏' 甚至成为了前端 bug 的代名词:_喂,你的页面白了。_而且,'白' 这一现象似乎对于用户体感上来说更加强,回忆起 windows 系统的崩溃 '蓝屏'。
可以说是非常相似了,甚至能明白了白屏这个词汇是如何统一出来的。那么,体感如此强烈的现象势必会给用户带来一些不好的影响,如何能尽早监听,快速消除影响就显得很重要了。

为什么单独监控白屏

不光光是白屏,白屏只是一种现象,我们要做的是精细化的异常监控。异常监控各个公司肯定都有自己的一套体系,集团也不例外,而且也足够成熟。但是通用的方案总归是有缺点的,如果对所有的异常都加以报警和监控,就无法区分异常的严重等级,并做出相应的响应,所以在通用的监控体系下定制精细化的异常监控是非常有必要的。这就是本文讨论白屏这一场景的原因,我把这一场景的边界圈定在了 “白屏” 这一现象。

方案调研

白屏大概可能的原因有两种:

  1. js 执行过程中的错误
  2. 资源错误

这两者方向不同,资源错误影响面较多,且视情况而定,故不在下面方案考虑范围内。为此,参考了网上的一些实践加上自己的一些调研,大概总结出了一些方案:

一、onerror + DOM 检测

原理很简单,在当前主流的 SPA 框架下,DOM 一般挂载在一个根节点之下(比如 <div id="root"></div> )发生白屏后通常现象是根节点下所有 DOM 被卸载,该方案就是通过监听全局的 onerror 事件,在异常发生时去检测根节点下是否挂载 DOM,若无则证明白屏。
我认为是非常简单暴力且有效的方案。但是也有缺点:其一切建立在 **白屏 === 根节点下 DOM 被卸载** 成立的前提下,实际并非如此比如一些微前端的框架,当然也有我后面要提到的方案,这个方案和我最终方案天然冲突。

二、Mutation Observer Api

不了解的可以看下文档
其本质是监听 DOM 变化,并告诉你每次变化的 DOM 是被增加还是删除。为其考虑了多种方案:

  1. 搭配 onerror 使用,类似第一个方案,但很快被我否决了,虽然其可以很好的知道 DOM 改变的动向,但无法和具体某个报错联系起来,两个都是事件监听,两者是没有必然联系的。
  2. 单独使用判断是否有大量 DOM 被卸载,缺点:白屏不一定是 DOM 被卸载,也有可能是压根没渲染,且正常情况也有可能大量 DOM 被卸载。完全走不通。
  3. 单独使用其监听时机配合 DOM 检测,其缺点和方案一一样,而且我觉得不如方案一。因为它没法和具体错误联系起来,也就是没法定位。当然我和其他团队同学交流的时候他们给出了其他方向:通过追踪用户行为数据来定位问题,我觉得也是一种方法。

一开始我认为这就是最终答案,经过了漫长的心里斗争,最终还是否定掉了。不过它给了一个比较好的监听时机的选择。

三、饿了么-Emonitor 白屏监控方案

饿了么的白屏监控方案,其原理是记录页面打开 4s 前后 html 长度变化,并将数据上传到饿了么自研的时序数据库。如果一个页面是稳定的,那么页面长度变化的分布应该呈现「幂次分布」曲线的形态,p10、p20 (排在文档前 10%、20%)等数据线应该是平稳的,在一定的区间内波动,如果页面出现异常,那么曲线一定会出现掉底的情况。

其他

其他都大同小样,其实调研了一圈下来发现无非就是两点

  1. 监控时机:调研下来常见的就三种:

    • onerror
    • mutation observer api
    • 轮训
  2. DOM 检测:这个方案就很多了,除了上述的还可以:

    • elementsFromPoint api 采样
    • 图像识别
    • 基于 DOM 的各种数据的各种算法识别
    • ...

改变方向

几番尝试下来几乎没有我想要的,其主要原因是准确率 -- 这些方案都不能保证我监听到的是白屏,单从理论的推导就说不通。他们都有一个共同点:监听的是'白屏'这个现象,从现象去推导本质虽然能成功,但是不够准确。所以我真正想要监听的是造成白屏的本质。

那么回到最开始,什么是白屏?他是如何造成的?是因为错误导致的浏览器无法渲染?不,在这个 spa 框架盛行的现在实际上的白屏是框架造成的,本质是由于错误导致框架不知道怎么渲染所以干脆就不渲染。由于我们团队 React 技术栈居多,我们来看看 React 官网的一段话

React 认为把一个错误的 UI 保留比完全移除它更糟糕。我们不讨论这个看法的正确与否,至少我们知道了白屏的原因:渲染过程的异常且我们没有捕获异常并处理。

反观目前的主流框架:我们把 DOM 的操作托管给了框架,所以渲染的异常处理不同框架方法肯定不一样,这大概就是白屏监控难统一化产品化的原因。但大致方向肯定是一样的。

那么关于白屏我认为可以这么定义:异常导致的渲染失败

那么白屏的监控方案即:监控渲染异常。那么对于 React 而言,答案就是: Error Boundaries

Error Boundaries

我们可以称之为错误边界,错误边界是什么?它其实就是一个生命周期,用来监听当前组件的 children 渲染过程中的错误,并可以返回一个 降级的 UI 来渲染:

class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能够显示降级后的 UI
return { hasError: true };
}

componentDidCatch(error, errorInfo) {
// 我们可以将错误日志上报给服务器
logErrorToMyService(error, errorInfo);
}

render() {
if (this.state.hasError) {
// 我们可以自定义降级后的 UI 并渲染
return <h1>Something went wrong.</h1>;
}

return this.props.children;
}
}


一个有责任心的开发一定不会放任错误的发生。错误边界可以包在任何位置并提供降级 UI,也就是说,一旦开发者'有责任心' 页面就不会全白,这也是我之前说的方案一与之天然冲突且其他方案不稳定的情况。
那么,在这同时我们上报异常信息,这里上报的异常一定会导致我们定义的白屏,这一推导是 100% 正确的。

100% 这个词或许不够负责,接下来我们来看看为什么我说这一推导是 100% 准确的:

React 渲染流程

我们来简单回顾下从代码到展现页面上 React 做了什么。
我大致将其分为几个阶段:render => 任务调度 => 任务循环 => 提交 => 展示
我们举一个简单的例子来展示其整个过程(任务调度不再本次讨论范围故不展示):

const App = ({ children }) => (
<>
<p>hello</p>
{ children }
</>
);
const Child = () => <p>I'm child</p>

const a = ReactDOM.render(
<App><Child/></App>,
document.getElementById('root')
);


首先浏览器是不认识我们的 jsx 语法的,所以我们通过 babel 编译大概能得到下面的代码:

var App = function App(_ref2) {
var children = _ref2.children;
return React.createElement("p", null, "hello"), children);
};

var Child = function Child() {
return React.createElement("p", null, "I'm child");
};

ReactDOM.render(React.createElement(App, null, React.createElement(Child, null)), document.getElementById('root'));

babel 插件将所有的 jsx 都转成了 createElement 方法,执行它会得到一个描述对象 ReactElement 大概长这样子:

{
$$typeof: Symbol(react.element),
key: null,
props: {}, // createElement 第二个参数 注意 children 也在这里,children 也会是一个 ReactElement 或 数组
type: 'h1' // createElement 的第一个参数,可能是原生的节点字符串,也可能是一个组件对象(Function、Class...)
}


所有的节点包括原生的 <a></a> 、 <p></p> 都会创建一个 FiberNode ,他的结构大概长这样:

FiberNode = {
elementType: null, // 传入 createElement 的第一个参数
key: null,
type: HostRoot, // 节点类型(根节点、函数组件、类组件等等)
return: null, // 父 FiberNode
child: null, // 第一个子 FiberNode
sibling: null, // 下一个兄弟 FiberNode
flag: null, // 状态标记
}


你可以把它理解为 Virtual Dom 只不过多了许多调度的东西。最开始我们会为根节点创建一个 FiberNodeRoot 如果有且仅有一个 ReactDOM.render 那么他就是唯一的根,当前有且仅有一个 FiberNode 树。

我只保留了一些渲染过程中重要的字段,其他还有很多用于调度、判断的字段我这边就不放出来了,有兴趣自行了解

render

现在我们要开始渲染页面,是我们刚才的例子,执行 ReactDOM.render 。这里我们有个全局 workInProgress 对象标志当前处理的 FiberNode

  1. 首先我们为根节点初始化一个 FiberNodeRoot ,他的结构就如上面所示,并将 workInProgress= FiberNodeRoot
  2. 接下来我们执行 ReactDOM.render 方法的第一个参数,我们得到一个 ReactElement :
ReactElement = {
$$typeof: Symbol(react.element),
key: null,
props: {
children: {
$$typeof: Symbol(react.element),
key: null,
props: {},
ref: null,
type: ƒ Child(),
}
}
ref: null,
type: f App()
}


该结构描述了 <App><Child /></App>

  1. 我们为 ReactElement 生成一个 FiberNode 并把 return 指向父 FiberNode ,最开始是我们的根节点,并将 workInProgress = FiberNode
{
elementType: f App(), // type 就是 App 函数
key: null,
type: FunctionComponent, // 函数组件类型
return: FiberNodeRoot, // 我们的根节点
child: null,
sibling: null,
flags: null
}


  1. 只要workInProgress 存在我们就要处理其指向的 FiberNode 。节点类型有很多,处理方法也不太一样,不过整体流程是相同的,我们以当前函数式组件为例子,直接执行 App(props) 方法,这里有两种情况

    • 该组件 return 一个单一节点,也就是返回一个 ReactElement 对象,重复 3 - 4 的步骤。并将当前 节点的 child 指向子节点 CurrentFiberNode.child = ChildFiberNode 并将子节点的 return 指向当前节点 ChildFiberNode.return = CurrentFiberNode
    • 该组件 return 多个节点(数组或者 Fragment ),此时我们会得到一个 ChildiFberNode 的数组。我们循环他,每一个节点执行 3 - 4 步骤。将当前节点的 child 指向第一个子节点 CurrentFiberNode.child = ChildFiberNodeList[0] ,同时每个子节点的 sibling 指向其下一个子节点(如果有) ChildFiberNode[i].sibling = ChildFiberNode[i + 1] ,每个子节点的 return 都指向当前节点 ChildFiberNode[i].return = CurrentFiberNode

如果无异常每个节点都会被标记为待布局 FiberNode.flags = Placement

  1. 重复步骤直到处理完全部节点 workInProgress 为空。

最终我们能大概得到这样一个 FiberNode 树:

FiberNodeRoot = {
elementType: null,
type: HostRoot,
return: null,
child: FiberNode<App>,
sibling: null,
flags: Placement, // 待布局状态
}

FiberNode<App> {
elementType: f App(),
type: FunctionComponent,
return: FiberNodeRoot,
child: FiberNode<p>,
sibling: null,
flags: Placement // 待布局状态
}

FiberNode<p> {
elementType: 'p',
type: HostComponent,
return: FiberNode<App>,
sibling: FiberNode<Child>,
child: null,
flags: Placement // 待布局状态
}

FiberNode<Child> {
elementType: f Child(),
type: FunctionComponent,
return: FiberNode<App>,
child: null,
flags: Placement // 待布局状态
}


提交阶段

提交阶段简单来讲就是拿着这棵树进行深度优先遍历 child => sibling,放置 DOM 节点并调用生命周期。

那么整个正常的渲染流程简单来讲就是这样。接下来看看异常处理

错误边界流程

刚刚我们了解了正常的流程现在我们制造一些错误并捕获他:

const App = ({ children }) => (
<>
<p>hello</p>
{ children }
</>
);
const Child = () => <p>I'm child {a.a}</p>

const a = ReactDOM.render(
<App>
<ErrorBoundary><Child/></ErrorBoundary>
</App>,
document.getElementById('root')
);


执行步骤 4 的函数体是包裹在 try...catch 内的如果捕获到了异常则会走异常的流程:

do {
try {
workLoopSync(); // 上述 步骤 4
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);

执行步骤 4 时我们调用 Child 方法由于我们加了个不存在的表达式 {a.a} 此时会抛出异常进入我们的 handleError 流程此时我们处理的目标是 FiberNode<Child> ,我们来看看 handleError :

function handleError(root, thrownValue): void {
let erroredWork = workInProgress; // 当前处理的 FiberNode 也就是异常的 节点
throwException(
root, // 我们的根 FiberNode
erroredWork.return, // 父节点
erroredWork,
thrownValue, // 异常内容
);
completeUnitOfWork(erroredWork);
}

function throwException(
root: FiberRoot,
returnFiber: Fiber,
sourceFiber: Fiber,
value: mixed,
) {
// The source fiber did not complete.
sourceFiber.flags |= Incomplete;

let workInProgress = returnFiber;
do {
switch (workInProgress.tag) {
case HostRoot: {
workInProgress.flags |= ShouldCapture;
return;
}
case ClassComponent:
// Capture and retry
const ctor = workInProgress.type;
const instance = workInProgress.stateNode;
if (
(workInProgress.flags & DidCapture) === NoFlags &&
(typeof ctor.getDerivedStateFromError === 'function' ||
(instance !== null &&
typeof instance.componentDidCatch === 'function' &&
!isAlreadyFailedLegacyErrorBoundary(instance)))
) {
workInProgress.flags |= ShouldCapture;
return;
}
break;
default:
break;
}
workInProgress = workInProgress.return;
} while (workInProgress !== null);
}


代码过长截取一部分
先看 throwException 方法,核心两件事:

  1. 将当前也就是出问题的节点状态标志为未完成 FiberNode.flags = Incomplete
  2. 从父节点开始冒泡,向上寻找有能力处理异常( ClassComponent )且的确处理了异常的(声明了 getDerivedStateFromError 或 componentDidCatch 生命周期)节点,如果有,则将那个节点标志为待捕获 workInProgress.flags |= ShouldCapture ,如果没有则是根节点。

completeUnitOfWork 方法也类似,从父节点开始冒泡,找到 ShouldCapture 标记的节点,如果有就标记为已捕获 DidCapture  ,如果没找到,则一路把所有的节点都标记为 Incomplete 直到根节点,并把 workInProgress 指向当前捕获的节点。

之后从当前捕获的节点(也有可能没捕获是根节点)开始重新走流程,由于其状态 react 只会渲染其降级 UI,如果有 sibling 节点则会继续走下面的流程。我们看看上述例子最终得到的 FiberNode 树:

FiberNodeRoot = {
elementType: null,
type: HostRoot,
return: null,
child: FiberNode<App>,
sibling: null,
flags: Placement, // 待布局状态
}

FiberNode<App> {
elementType: f App(),
type: FunctionComponent,
return: FiberNodeRoot,
child: FiberNode<p>,
sibling: null,
flags: Placement // 待布局状态
}

FiberNode<p> {
elementType: 'p',
type: HostComponent,
return: FiberNode<App>,
sibling: FiberNode<ErrorBoundary>,
child: null,
flags: Placement // 待布局状态
}

FiberNode<ErrorBoundary> {
elementType: f ErrorBoundary(),
type: ClassComponent,
return: FiberNode<App>,
child: null,
flags: DidCapture // 已捕获状态
}

FiberNode<h1> {
elementType: f ErrorBoundary(),
type: ClassComponent,
return: FiberNode<ErrorBoundary>,
child: null,
flags: Placement // 待布局状态
}


如果没有配置错误边界那么根节点下就没有任何节点,自然无法渲染出任何内容。

ok,相信到这里大家应该清楚错误边界的处理流程了,也应该能理解为什么我之前说由 ErrorBoundry 推导白屏是 100% 正确的。当然这个 100% 指的是由 ErrorBoundry 捕捉的异常基本上会导致白屏,并不是指它能捕获全部的白屏异常。以下场景也是他无法捕获的:

  • 事件处理
  • 异步代码
  • SSR
  • 自身抛出来的错误

React SSR 设计使用流式传输,这意味着服务端在发送已经处理好的元素的同时,剩下的仍然在生成 HTML,也就是其父元素无法捕获子组件的错误并隐藏错误的组件。这种情况似乎只能将所有的 render 函数包裹 try...catch ,当然我们可以借助 babel 或 TypeScript 来帮我们简单实现这一过程,其最终得到的效果是和 ErrorBoundry 类似的。

而事件和异步则很巧,虽说 ErrorBoundry 无法捕获他们之中的异常,不过其产生的异常也恰好不会造成白屏(如果是错误的设置状态,间接导致了白屏,刚好还是会被捕获到)。这就在白屏监控的职责边界之外了,需要别的精细化监控能力来处理它。

总结

那么最后总结下本文的出的几个结论:
我对白屏的定义:异常导致的渲染失败
对应方案是:资源监听 + 渲染流程监听

在目前 SPA 框架下白屏的监控需要针对场景做精细化的处理,这里以 React 为例子,通过监听渲染过程异常能够很好的获得白屏的信息,同时能增强开发者对异常处理的重视。而其他框架也会有相应的方法来处理这一现象。

当然这个方案也有弱点,由于是从本质推导现象其实无法 cover 所有的白屏的场景,比如我要搭配资源的监听来处理资源异常导致的白屏。当然没有一个方案是完美的,我这里也是提供一个思路,欢迎大家一起讨论。


收起阅读 »

面试官问我会canvas? 我可以绘制一个烟花?动画

在我们日常开发中贝塞尔曲线无处不在:svg 中的曲线(支持 2阶、 3阶)canvas 中绘制贝塞尔曲线几乎所有前端2D或3D图形图表库(echarts,d3,three.js)都会使用到贝塞尔曲线所以掌握贝塞尔曲线势在必得。 这篇文章主要是实战篇,不会介绍和...
继续阅读 »

在我们日常开发中贝塞尔曲线无处不在:

  1. svg 中的曲线(支持 2阶、 3阶)
  2. canvas 中绘制贝塞尔曲线
  3. 几乎所有前端2D或3D图形图表库(echarts,d3,three.js)都会使用到贝塞尔曲线

所以掌握贝塞尔曲线势在必得。 这篇文章主要是实战篇,不会介绍和贝塞尔相关的知识, 如果有同学对贝塞尔曲线不是很清楚的话:可以查看我这篇文章——深入理解SVG

绘制贝塞尔曲线

第一步我们先创建ctx, 用ctx 画一个二阶贝塞尔曲线看下。二阶贝塞尔曲线有1个控制点,一个起点,一个终点。

const canvas = document.getElementById( 'canvas' );
const ctx = canvas.getContext( '2d' );
ctx.beginPath();
ctx.lineWidth = 2;
ctx.strokeStyle = '#000';
ctx.moveTo(100,100)
ctx.quadraticCurveTo(180,50, 200,200)
ctx.stroke();


这样我们就画好了一个贝塞尔曲线了。

绘制贝塞尔曲线动画

画一条线谁不会哇?接下来文章的主体内容。 首先试想一下动画我们肯定一步步画出曲线? 但是这个ctx给我们全部画出来了是不是有点问题。我们重新看下二阶贝塞尔曲线的实现过程动画,看看是否有思路。

从图中可以分析得出贝塞尔上的曲线是和t有关系的, t的区间是在0-1之间,我们是不是可以通过二阶贝塞尔的曲线方程去算出每一个点呢,这个专业术语叫离散化,但是这样的得出来的点的信息是不太准的,我们先这样实现。

先看下方程:

我们模拟写出代码如如下:

//这个就是二阶贝塞尔曲线方程
function twoBezizer(p0, p1, p2, t) {
const k = 1 - t
return k * k * p0 + 2 * (1 - t) * t * p1 + t * t * p2
}

//离散
function drawWithDiscrete(ctx, start, control, end,percent) {
for ( let t = 0; t <= percent / 100; t += 0.01 ) {
const x = twoBezizer(start[0], control[0], end[0], t)
const y = twoBezizer(start[1], control[1], end[1], t)
ctx.lineTo(x, y)
}
}


我们看下效果:

和我们画的几乎是一模一样,接下啦就用requestAnimationFrame 开始我们的动画给出以下代码:

let percent = 0
function animate() {
ctx.clearRect( 0, 0, 800, 800 );
ctx.beginPath();
ctx.moveTo(100,100)
drawWithDiscrete(ctx,[100,100],[180,50],[200,200],percent)
ctx.stroke();
percent = ( percent + 1 ) % 100;
id = requestAnimationFrame(animate)
}
animate()


这里有两个要注意的是, 我是是percent 不断加1 和100 求余,所以呢 percent 会不断地从1-100 这样往复,OK所以我们必须要动画之前做一次区域清理, ctx.clearRect( 0, 0, 800, 800 ); 这样就可以不断的从开始到结束循环往复,我们看下效果:

看着样子是不是还不错哈哈哈😸。

绘制贝塞尔曲线动画方法2

你以为这样就结束了? 当然不是难道我们真的没有办法画出某一个t的贝塞尔曲线了? 当前不是,这里放一下二阶贝塞尔方程的推导过程:

二阶贝塞尔曲线上的任意一点,都是可以通过同样比例获得。 在两点之间的任意一点,其实满足的一阶贝塞尔曲线, 一阶贝塞尔曲线满足的其实是线性变化。我给出以下方程

 function oneBezizer(p0,p1,t) {
return p0 + (p1-p0) * t
}

从我画的图可以看出,我们只要 不断求A点 和C点就可以画出在某一时间段的贝塞尔了。

我给出以下代码和效果图:

function drawWithDiscrete2(ctx, start, control, end,percent) {
const t = percent/ 100;
// 求出A点
const A = [];
const C = [];
A[0] = oneBezizer(start[0],control[0],t);
A[1] = oneBezizer(start[1],control[1],t);
C[0] = twoBezizer(start[0], control[0], end[0], t)
C[1] = twoBezizer(start[1], control[1], end[1], t)
ctx.quadraticCurveTo(
A[ 0 ], A [ 1 ],
C[ 0 ], C[ 1 ]
);
}


礼花🎉动画

上文我们实现了一条贝塞尔线,我们将这条贝塞尔的曲线的开始点作为一个圆的圆心,然后按照某个次数求出不同的结束点。 再写一个随机颜色,礼花效果就成了, 直接上代码,

for(let i=0; i<count; i++) {
const angle = Math.PI * 2 / count * i;
const x = center[ 0 ] + radius * Math.sin( angle );
const y = center[ 1 ] + radius * Math.cos( angle );
ctx.strokeStyle = colors[ i ];
ctx.beginPath();
drawWithDiscrete(ctx, center,[180,50],[x,y],percent)
ctx.stroke();
}

function getRandomColor(colors, count) {
// 生成随机颜色
for ( let i = 0; i < count; i++ ) {
colors.push(
'rgb( ' +
( Math.random() * 255 >> 0 ) + ',' +
( Math.random() * 255 >> 0 ) + ',' +
( Math.random() * 255 >> 0 ) +
' )'
);
}
}


我们看下动画吧:



收起阅读 »

在 React 应用中展示报表数据

创建 React 应用创建 React 应用 参考链接, 如使用npx 包运行工具:npx create-react-app arjs-react-viewer-app如果您使用的是yarn,执行命令:yarn create react-app arjs-re...
继续阅读 »

创建 React 应用

创建 React 应用 参考链接, 如使用npx 包运行工具:

npx create-react-app arjs-react-viewer-app
如果您使用的是yarn,执行命令:

yarn create react-app arjs-react-viewer-app
更多创建 React方法可参考 官方文档

安装 ActivereportsJS NPM 包

React 报表 Viewer 组件已经放在了npm @grapecity/activereports-react npm 中。 @grapecity/activereports 包提供了全部的核心功能。

运行以下命令安装包:

npm install @grapecity/activereports-react @grapecity/activereports
或使用yarn命令

yarn add @grapecity/activereports-react @grapecity/activereports

导入 ActiveReportsJS 样式

打开 src\App.css 文件并添加以下代码,导入Viewer 的默认样式,定义了元素宿主的样式React Report Viewer 控件:

@import "@grapecity/activereports/styles/ar-js-ui.css";
@import "@grapecity/activereports/styles/ar-js-viewer.css";

viewer-host {

width: 100%;
height: 100vh;
}

添加 ActiveReportsJS 报表

ActiveReportsJS 使用 JSON格式和 rdlx-json扩展报表模板文件。在应用程序的public文件夹中,创建名为 report.rdlx-json 的新文件,并在该文件中插入以下JSON内容:

{
"Name": "Report",
"Body": {

"ReportItems": [
{
"Type": "textbox",
"Name": "TextBox1",
"Value": "Hello, ActiveReportsJS Viewer",
"Style": {
"FontSize": "18pt"
},
"Width": "8.5in",
"Height": "0.5in"
}
]
}
}


添加 React 报表 Viewer 控件

修改 src\App.js代码:

import React from "react";
import "./App.css";
import { Viewer } from "@grapecity/activereports-react";

function App() {
return (

<div id="viewer-host">
<Viewer report={{ Uri: 'report.rdlx-json' }} />
</div>
);
}


export default App;

运行和调试

使用 npm start 或 yarn start 命令运行项目,如果编译失败了,报以下错误,请删除node_modules 文件夹并重新运行 npm install 或 yarn命令来重新安装需要的包文件。

react-scripts start

internal/modules/cjs/loader.js:883
throw err;
^

Error: Cannot find module 'react'
当应用程序启动时,ActiveReportsJS Viewer组件将出现在页面上。Viewer将显示显示“ Hello,ActiveReportsJS Viewer”文本的报表。您可以通过使用工具栏上的按钮或将报表导出为可用格式之一来测试。

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

收起阅读 »

Babel配置傻傻看不懂?

1.2 AST 是什么玩意?👨‍🎓 啊斌同学: 上面说到的抽象语法树AST又是什么玩意?答:我们上文提到,Babel在解析是时候会通过将code转换为AST抽象语法树,本质上是代码语法结构的一种抽象表示,通过以树🌲形的结构形式表现出它的语法结构,抽象在于它的语...
继续阅读 »

前沿:文章起源在于,朋友跟树酱说在解决项目兼容IE11浏览器过程中,遇到“眼花缭乱”的babel配置和插件等,傻傻分不清配置间的区别、以及不了解如何引用babel插件才能让性能更佳,如果你也有这方面的疑虑,这篇文章可能适合你

1.babel

babel是个什么玩意? Babel本质上是一个编辑器,也就是个“翻译官”的角色,比如树酱听不懂西班牙语,需要别人帮我翻译成为中文,我才晓得。那么Babel就是帮助浏览器翻译的,让web应用能够运行旧版本的浏览器中,比如IE11浏览器不支持Promise等ES6语法,那这个时候在IE11打开你写的web应用,应用就无法正常运行,这时候就需要Babel来“翻译”成为IE11能读懂的

1.1 Babel是怎么工作的?

本质上单独靠Babel是无法完成“翻译”,比如官网的例子const babel = code => code;不借助Babel插件的前提,输出是不会把箭头函数“翻译”的,如果想完成就需要用到插件,更多概念点点击 官方文档

Babel工作原理本质上就是三个步骤:解析、转换、输出,如下👇所示,

1.2 AST 是什么玩意?

👨‍🎓 啊斌同学: 上面说到的抽象语法树AST又是什么玩意?

答:我们上文提到,Babel在解析是时候会通过将code转换为AST抽象语法树,本质上是代码语法结构的一种抽象表示,通过以树🌲形的结构形式表现出它的语法结构,抽象在于它的语言形态不会体现在原始代码code中

下面介绍下在前端项目开发中一些AST的应用场景:

  • Vue模版解析: 我们平时写的.vue文件通过vue-template-compiler解析,.vue文件处理为一个AST
  • Babel的“翻译” : 如将ES6转换为ES5过程中转为AST
  • webpack的插件UglifyJS: uglifyjs-webpack-plugin用来压缩资源,uglifyjs会遇到需要解析es6语法,这个过程中本质上也是借助babel-loader

你可以安装通过本地安装babel-cli做个验证,通过babel-cli编译js文件,玩玩“翻译”

🌲推荐阅读:

1.3 开发自己的babel插件需要了解什么?

👨‍🎓 啊可同学: 树酱,我想自己使用AST开发一个babel插件需要使用到哪些东西呢?

答:我们上一节中提到babel不借助“外援”的话,自己是无法完成翻译,而一个完整的“翻译”的过程是需要走完解析、转换、输出才能完成整个闭环,而这其中的每个环节都需要借助babel以下这些API

  • @babel/parser: babel解析器将源代码code解析成 AST
  • @babel/generator: 将AST解码生成js代码 new Code
  • @babel/traverse : 用来遍历AST树,可以用来改造AST~,如替换或添加AST原始节点
  • @babel/core:包括了整个babel工作流

下面是一个简单“翻译”的demo~

👦:啊宽同学:你不是说@babel/parser是也将源代码code解析成 AST吗?为啥@babel/core也是?

答:@babel/core包含的是整个babel工作流,在开发插件的过程中,如果每个API都单独去引入岂不是蒙蔽了来吧~于是就有了@babel/core插件,顾名思义就是核心插件,他将底层的插件进行封装(包含了parser、generator等),提高原有的插件开发效率,简化过程,好一个“🍟肯德基全家桶”

🌲推荐阅读:

1.4 Babel插件相关

讲完Babel的基本使用,接下来聊聊插件,上文提到单独靠babel是“难成大器”的,需要插件的辅助才能实现霸业,那插件是怎么搞的呢?

通过第一节的学习我们知道完成第一步骤解析完AST后,接下来是进入转换,插件在这个阶段就起到关键作用了。

1.4.1 插件的使用

告诉Babel该做什么之前,我们需要创建一个配置文件.babelrc或者babel.config.js文件

如果我想把es2015的语法转化为es5 及支持es2020的链式写法,我可以这样写

上图所示👆,我们可以看到我们配置两个东西 presentplugin

👨‍🎓 啊可同学:babel不是只需要plugin来帮忙翻译吗,这个present又是什么玩意?

答:presets是预设,举个例子:有一天树酱要去肯德基买鸡翅、薯条、可乐、汉堡。然后我发现有个套餐A包含了(薯条、可乐、汉堡),那这个present就相当于套餐A,它包含了一些插件集合,一个大套餐,这样我就只需要一个套餐A+鸡翅就搞定了,不用配置很多插件。

就好比上面的es2015“套餐”,其实就是Babel团队将同属ES2015相关的很多个plugins集合到babel-preset-es2015一个preset中去

👧 啊琪同学:@babel/preset-env这个是什么?我看很多babel的配置都有

答:@babel/preset-env这个是一个present预设,换句话说就是“豪华大礼包”,包括一系列插件的集合,包含了我们常用的es2015,es2016, es2017等最新的语法转化插件,允许我们使用最新的js语法,比如 let,const,箭头函数等等,但不包括stage-x阶段的插件。换句话说,他包含了我们上文提到了es2015,是个“全家桶”了,而不仅是个套餐了。

1.4.2 自定义 present

👦 啊斌同学:树酱,那我是不是可以自己搞一个预设present?

答: 可以的,但是你可以以 babel-preset-* 的命名规范来创建一个新项目,然后创建一个packjson并安装好定影的依赖和一个index.js 文件用于导出 .babelrc,最终发布到npm中,如下所示

1.4.3 关于 polyfill

比如我们在开发中使用,会使用到一些es6的新特征比如Array.from等,但不是所有的 JavaScript 环境都支持 Array.from,这个时候我们可以使用 Polyfill(代码填充,也可译作兼容性补丁)的“黑科技”,因为babel只转换新的js语法,如箭头函数等,但不转换新的API,比如Symbol、Promise等全局对象,这时候需要借助@babel/polyfill,把es的新特性都装进来,使用步骤如下
  • npm 安装 : npm install --save @babel/polyfill
  • 文件顶部导入 polyfillimport @babel/polyfilll

🙅‍♂️:缺点:全局引入整个 polyfill包,如promise会被全局引入,污染全局环境,所以不建议使用,那有没有更好的方式?可以直接使用@babel/preset-env并修改配置,因为@babel/preset-env包含了@babel/polyfill插件,看下一节

1.4.4 如何通过修改@babel/preset-env配置优化

完成上面的配置,然后用Babel编译代码,我们会发现有时候打出的包体积很大,因为@babel/polyfill有些会被全局引用,那你要弄清楚@babel/preset-env的配置

@babel/preset-env 中与 @babel/polyfill 的相关参数有两个如下:

  • targets: 支持的目标浏览器的列表
  • useBuiltIns: 参数有 “entry”、”usage”、false 三个值。默认值是false,此参数决定了babel打包时如何处理@babel/polyfilll 语句

主要聊聊关于useBuiltIns的不同配置如下:

  • entry: 去掉目标浏览器已支持的polyfilll 模块,将浏览器不支持的都引入对应的polyfilll 模块。
  • usage: 打包时会自动根据实际代码的使用情况,结合 targets 引入代码里实际用到部分 polyfilll模块
  • false: 不会自动引入 polyfilll 模块,对polyfilll模块屏蔽

🌲建议:使用 useBuiltIns: usage来根据目标浏览器的支持情况,按需引入用到的 polyfill 文件,这样打包体积也不会过大

1.4.5 webpack打包如何使用babel?

对于@babel/core@babel/preset-env 、@babel/polyfill等这些插件,当我们在使用webpack进行打包的时候,如何让webpack知道按这些规则去编译js。这时就需要babel-loader了,它相当于一个中间桥梁,通过调用babel/core中的API来告知webpack要如何处理。

1.4.6 开发工具库,涉及到babel使用怎么避免污染环境?

👦 啊斌同学:我开发了一个工具库,也使用了babel,如果引用polyfill,如何避免使用导致的污染环境?

答:在开发工具库或者组件库时,就不能再使用babel-polyfill了,否则可能会造成全局污染,可以使用@babel/runtime。它不会污染你的原有的方法。遇到需要转换的方法它会另起一个名字,否则会直接影响使用库的业务代码,使用@babel/runtime主要在于

  • 可以减小库和工具包的体积,规避babel编译的工具函数在每个模块里都重复出现的情况
  • 在没有使用 @babel/runtime 之前,库和工具包一般不会直接引入 polyfill。否则像 Promise 这样的全局对象会污染全局命名空间,这就要求库的使用者自己提供 polyfill。这些 polyfill 一般在库和工具的使用说明中会提到,比如很多库都会有要求提供 es5 的 polyfill。在使用 babel-runtime 后,库和工具只要在 package.json 中增加依赖 babel-runtime,交给 babel-runtime 去引入 polyfill就可以了

如何使用 @babel/runtime

  • 1.npm安装
npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime
  • 2.配置

1.5 关于babel容易混淆的点

1.5.1 babel-core和@babel/core 区别

👦:啊呆同学:babel-core和@babel/core是什么区别?

答;@babel是在babel7中版本提出来的,就类似于 vue-cli 升级后使用@vue/cli一样的道理,所以babel7以后的版本都是使用 @babel 开头声明作用域,


收起阅读 »

如何用 JS 一次获取 HTML 表单的所有字段 ?

问:如何用 JS 一次获取 HTML 表单的所有字段 ?考虑一个简单的 HTML 表单,用于将任务保存在待办事项列表中:<form> <label for="name">用户名</label> <input...
继续阅读 »

问:如何用 JS 一次获取 HTML 表单的所有字段 ?

考虑一个简单的 HTML 表单,用于将任务保存在待办事项列表中:

<form>
<label for="name">用户名</label>
<input type="text" id="name" name="name" required>

<label for="description">简介</label>
<input type="text" id="description" name="description" required>

<label for="task">任务</label>
<textarea id="task" name="task" required></textarea>

<button type="submit">提交</button>
</form>


上面每个字段都有对应的的typeID和 name属性,以及相关联的label。 用户单击“提交”按钮后,我们如何从此表单中获取所有数据?

有两种方法:一种是用黑科技,另一种是更清洁,也是最常用的方法。为了演示这种方法,我们先创建form.js,并引入文件中。

从事件 target 获取表单字段

首先,我们在表单上为Submit事件注册一个事件侦听器,以停止默认行为(它们将数据发送到后端)。

然后,使用this.elementsevent.target.elements访问表单字段:

相反,如果需要响应某些用户交互而动态添加更多字段,那么我们需要使用FormData

使用 FormData

首先,我们在表单上为submit事件注册一个事件侦听器,以停止默认行为。接着,我们从表单构建一个FormData对象:

const form = document.forms[0];

form.addEventListener("submit", function(event) {
event.preventDefault();
const formData = new FormData(this);
});

除了append()delete()get()set()之外,FormData 还实现了Symbol.iterator。这意味着它可以用for...of 遍历:

const form = document.forms[0];

form.addEventListener("submit", function(event) {
event.preventDefault();
const formData = new FormData(this);

for (const formElement of formData) {
console.log(formElement);
}
})


除了上述方法之外,entries()方法获取表单对象形式:

const form = document.forms[0];

form.addEventListener("submit", function(event) {
event.preventDefault();
const formData = new FormData(this);
const entries = formData.entries();
const data = Object.fromEntries(entries);
});


这也适合Object.fromEntries() (ECMAScript 2019)

为什么这有用?如下所示:

const form = document.forms[0];

form.addEventListener("submit", function(event) {
event.preventDefault();
const formData = new FormData(this);
const entries = formData.entries();
const data = Object.fromEntries(entries);

// send out to a REST API
fetch("https://some.endpoint.dev", {
method: "POST",
body: JSON.stringify(data),
headers: {
"Content-Type": "application/json"
}
})
.then(/**/)
.catch(/**/);
});


一旦有了对象,就可以使用fetch发送有效负载。

小心:如果在表单字段上省略name属性,那么在FormData对象中刚没有生成。

总结

要从HTML表单中获取所有字段,可以使用:

  • this.elementsevent.target.elements,只有在预先知道所有字段并且它们保持稳定的情况下,才能使用。

使用FormData构建具有所有字段的对象,之后可以转换,更新或将其发送到远程API。*


原文:https://www.valentinog.com/bl...

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

收起阅读 »

自动化注册组件,自动化注册路由--懒人福利(vue,react皆适用)

我是一个react主义者,这次因为项目组关系必须用vue,作为vue小白就记录一下开发过程中的一些骚想法。正文1. 对于路由的操作可能用过umi的同学知道,umi有一套约定式路由的系统,开发过程中可以避免每写一个页面就去手动import到路由的数组中,你只需要...
继续阅读 »

我是一个react主义者,这次因为项目组关系必须用vue,作为vue小白就记录一下开发过程中的一些骚想法。

正文

1. 对于路由的操作

可能用过umi的同学知道,umi有一套约定式路由的系统,开发过程中可以避免每写一个页面就去手动import到路由的数组中,你只需要按照规则,就可以自动化的添加路由。

完美,我们今天就简单实现一个约定式路由的功能。

首先把vue自己的路由注释掉

// const routes: Array = [
// {
// path: "/login",
// name: "login",
// component: Login,
// },
// // {
// // path: "/about",
// // name: "About",
// // // route level code-splitting
// // // this generates a separate chunk (about.[hash].js) for this route
// // // which is lazy-loaded when the route is visited.
// // component: () =>
// // import(/* webpackChunkName: "about" */ "../views/About.vue"),
// // },
// ];


可以看到代码非常的多,随着页面的增加也会越来越多。当然vue的这种方式也有很多好处:比如支持webpack的魔法注释,支持懒加载

接下来就去实现我们的约定式路由吧!

我们这次用到的API是require.context,大家可能以为需要安装什么包,不用不用!这是webpack的东西!具体API的介绍大家可以自行百度了

首先用这玩意去匹配对应规则的页面,然后提前创好我们的路由数组以便使用。

const r = require.context("../views", true, /.vue/);
const routeArr: Array = [];

接下来就是进行遍历啦,匹配了../views文件下的页面,遍历匹配结果,如果是按照我们的规则创建的页面就去添加到路由数组中

比如我现在的views文件夹里是这样的

// 遍历
r.keys().forEach((key) => {
console.log(key) //这里的匹配结果就是 ./login/index.vue ./product/index.vue
const keyArr = key.split(".");
if (key.indexOf("index") > -1) {
// 约定式路由构成方案,views文件夹下的index.vue文件都会自动化生成路由
// 但是我不想在路由中出现index,我只想要login,product,于是对path进行改造。
// 这部其实是有很多优化空间的。大家可以自己试着用正则去提取
const pathArr = keyArr[1].split("/");
routeArr.push({
name: pathArr[1],
path: "/" + pathArr[1],
component: r(key).default, // 这是组件
});
}
});


一起来看一下自动匹配出来的路由数组是什么模样

完美🚖达成了我们的需求。去页面看一看!

完美实现! 最后把全部代码送上。这样就实现了约定式自动注册路由,避免了手动添加的烦恼,懒人必备

import Vue from "vue";
import VueRouter, { RouteConfig } from "vue-router";
const r = require.context("../views", true, /.vue/);
const routeArr: Array = [];
r.keys().forEach((key) => {
const keyArr = key.split(".");
if (key.indexOf("index") > -1) {
// 约定式路由构成方案,views文件夹下的index.vue文件都会自动化生成路由
const pathArr = keyArr[1].split("/");
routeArr.push({
name: pathArr[1],
path: "/" + pathArr[1],
component: r(key).default, // 这是组件
});
}
});
Vue.use(VueRouter);

const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes: routeArr,
});

export default router;


2.组件

经过上一章的操作,我们可以写页面了,然后就写到了组件。我发现每次使用组件都要在使用的页面去import,非常的麻烦。

通过上一章的想法,我们是不是也可以自动化导入组件呢?

我的想法是:

  • 通过一个方法把components文件下的所有组件进行统一的管理
  • 需要的页面可以用这个方法传入对应的规则,统一返回组件
  • 这个方法可以手动导入,也可以全局挂载。

先给大家看一下我的components文件夹

再看一下现在的页面长相

ok。我们开始在index.ts里撸代码吧

首先第一步一样的去匹配,这里只需要匹配当前文件夹下的所有vue文件

const r = require.context("./"true/.vue/);

然后声明一个方法,这个方法可以做到fn('规则')返回对应的组件,代码如下。

function getComponent(...names: string[]): any {
const componentObj: any = {};
r.keys().forEach((key) => {
const name = key.replace(/(\.\/|\.vue)/g, "");
if (names.includes(name)) {
componentObj[name] = r(key).default;
}
});
return componentObj;
}
export { getComponent };

我们一起来看看调用结果吧

打印结果:

看到这个结果不难想象页面的样子吧! 当然跟之前一样啦!当然实现啦!

非常的完美!

最后

由于项目比较急咯,我还有一些骚想法没有时间去整理去查资料实现,暂时先这样吧~

如果文内有错误,敬请大家帮我指出!(反正我也不一定改哈哈)

最后!谢谢!拜拜!

收起阅读 »

ES6 中 module 备忘清单,你可能知道 module 还可以这样用!

这是一份备忘单,展示了不同的导出方式和相应的导入方式。 它实际上可分为3种类型:名称,默认值和列表 ?// 命名导入/导出 export const name = 'value'import { name } from '...'// 默认导出/导入expor...
继续阅读 »

这是一份备忘单,展示了不同的导出方式和相应的导入方式。 它实际上可分为3种类型:名称,默认值和列表 ?

// 命名导入/导出 
export const name = 'value'
import { name } from '...'

// 默认导出/导入
export default 'value'
import anyName from '...'

// 重命名导入/导出
export { name as newName }
import { newName } from '...'

// 命名 + 默认 | Import All
export const name = 'value'
export default 'value'
import * as anyName from '...'

// 导出列表 + 重命名
export {
name1,
name2 as newName2
}
import {
name1 as newName1,
newName2
} from '...'


接下来,我们来一个一个的看?

命名方式

这里的关键是要有一个name

export const name = 'value';
import { name } from 'some-path/file';

console.log(name); // 'value'

大家都说简历没项目写,我就帮大家找了一个项目,还附赠【搭建教程】

默认方式

使用默认导出,不需要任何名称,所以我们可以随便命名它?

export default 'value'
import anyName from 'some-path/file'

console.log(anyName) // 'value'

❌ 默认方式不用变量名

export default const name = 'value';  
// 不要试图给我起个名字!

命名方式 和 默认方式 一起使用

命名方式 和 默认方式 可以同个文件中一起使用?

eport const name = 'value'
eport default 'value'
import anyName, { name } from 'some-path/file'

导出列表

第三种方式是导出列表(多个)

const name1 = 'value1'
const name2 = 'value2'

export {
name1,
name2
}
import {name1, name2 } from 'some-path/file'

console.log(
name1, // 'value1'
name2, // 'value2'
)

需要注意的重要一点是,这些列表不是对象。它看起来像对象,但事实并非如此。我第一次学习模块时,我也产生了这种困惑。真相是它不是一个对象,它是一个导出列表

// ❌ Export list ≠ Object
export {
name: 'name'
}

重命名的导出

对导出名称不满意?问题不大,可以使用as关键字将其重命名。

const name = 'value'

export {
name as newName
}
import { newName } from 'some-path/file'

console.log(newName); // 'value'

// 原始名称不可访问
console.log(name); // ❌ undefined

❌ 不能将内联导出与导出列表一起使用

export const name = 'value'

// 你已经在导出 name ☝️,请勿再导出我
export {
name
}

大家都说简历没项目写,我就帮大家找了一个项目,还附赠【搭建教程】

重命名导入

同样的规则也适用于导入,我们可以使用as关键字重命名它。

const name1 = 'value1'
const name2 = 'value2'

export {
name1,
name2 as newName2
}
import {
name1 as newName1,
newName2
} from '...'

console.log(newName1); // 'value1'
console.log(newName2); // 'value2'


name1; // undefined
name2; // undefined

导入全部

export const name = 'value'

export default 'defaultValue'
import * as anyName from 'some-path/file'

console.log(anyName.name); // 'value'
console.log(anyName.default); // 'defaultValue'

命名方式 vs 默认方式

是否应该使用默认导出一直存在很多争论。 查看这2篇文章。

就像任何事情一样,答案没有对错之分。正确的方式永远是对你和你的团队最好的方式。

命名与默认导出的非开发术语

假设你欠朋友一些钱。 你的朋友说可以用现金或电子转帐的方式还钱。 通过电子转帐付款就像named export一样,因为你的姓名已附加在交易中。 因此,如果你的朋友健忘,并开始叫你还钱,说他没收到钱。 这里,你就可以简单地向他们显示转帐证明,因为你的名字在付款中。 但是,如果你用现金偿还了朋友的钱(就像default export一样),则没有证据。 他们可以说当时的 100 块是来自小红。 现金上没有名称,因此他们可以说是你本人或者是任何人?

那么采用电子转帐(named export)还是现金(default export)更好?

这取决于你是否信任的朋友?, 实际上,这不是解决这一难题的正确方法。 更好的解决方案是不要将你的关系置于该位置,以免冒险危及友谊,最好还是相互坦诚。 是的,这个想法也适用于你选择named export还是default export。 最终还是取决你们的团队决定,哪种方式对团队比较友好,就选择哪种,毕竟不是你自己一个人在战斗,而是一个团体?

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

收起阅读 »

20个 Javascript 技巧,提高我们的摸鱼时间!

使用方便有用的方法,以减少代码行数,提高我们的工作效率,增加我们的摸鱼时间。在我们的日常任务中,我们需要编写函数,如排序、搜索、寻找惟一值、传递参数、交换值等,所以在这里分享一下我工作多年珍藏的几个常用技巧和方法,以让大家增加摸鱼的时间。这些方法肯定会帮助你:...
继续阅读 »

使用方便有用的方法,以减少代码行数,提高我们的工作效率,增加我们的摸鱼时间。

在我们的日常任务中,我们需要编写函数,如排序、搜索、寻找惟一值、传递参数、交换值等,所以在这里分享一下我工作多年珍藏的几个常用技巧和方法,以让大家增加摸鱼的时间。

这些方法肯定会帮助你:

  • 减少代码行
  • Coding Competitions
  • 增加摸鱼的时间

1.声明和初始化数组

我们可以使用特定的大小来初始化数组,也可以通过指定值来初始化数组内容,大家可能用的是一组数组,其实二维数组也可以这样做,如下所示:

const array = Array(5).fill(''); 
// 输出
(5) ["", "", "", "", ""]

const matrix = Array(5).fill(0).map(() => Array(5).fill(0))
// 输出
(5) [Array(5), Array(5), Array(5), Array(5), Array(5)]
0: (5) [0, 0, 0, 0, 0]
1: (5) [0, 0, 0, 0, 0]
2: (5) [0, 0, 0, 0, 0]
3: (5) [0, 0, 0, 0, 0]
4: (5) [0, 0, 0, 0, 0]
length: 5

2. 求和,最小值和最大值

我们应该利用 reduce 方法快速找到基本的数学运算。

const array = [5,4,7,8,9,2];

求和

array.reduce((a,b) => a+b);
// 输出: 35

最大值

array.reduce((a,b) => a>b?a:b);
// 输出: 9

最小值

array.reduce((a,b) => a<b?a:b);
// 输出: 2

3.排序字符串,数字或对象等数组

我们有内置的方法sort()reverse()来排序字符串,但是如果是数字或对象数组呢

字符串数组排序

const stringArr = ["Joe", "Kapil", "Steve", "Musk"]
stringArr.sort();
// 输出
(4) ["Joe", "Kapil", "Musk", "Steve"]

stringArr.reverse();
// 输出
(4) ["Steve", "Musk", "Kapil", "Joe"]

数字数组排序

const array  = [40, 100, 1, 5, 25, 10];
array.sort((a,b) => a-b);
// 输出
(6) [1, 5, 10, 25, 40, 100]

array.sort((a,b) => b-a);
// 输出
(6) [100, 40, 25, 10, 5, 1]

对象数组排序

const objectArr = [ 
{ first_name: 'Lazslo', last_name: 'Jamf' },
{ first_name: 'Pig', last_name: 'Bodine' },
{ first_name: 'Pirate', last_name: 'Prentice' }
];
objectArr.sort((a, b) => a.last_name.localeCompare(b.last_name));
// 输出
(3) [{…}, {…}, {…}]
0: {first_name: "Pig", last_name: "Bodine"}
1: {first_name: "Lazslo", last_name: "Jamf"}
2: {first_name: "Pirate", last_name: "Prentice"}
length: 3

4.从数组中过滤到虚值

像 0undefinednullfalse""''这样的假值可以通过下面的技巧轻易地过滤掉。

const array = [3, 0, 6, 7, '', false];
array.filter(Boolean);


// 输出
(3) [3, 6, 7]

5. 使用逻辑运算符处理需要条件判断的情况

function doSomething(arg1){ 
arg1 = arg1 || 10;
// 如果arg1没有值,则取默认值 10
}

let foo = 10;
foo === 10 && doSomething();
// 如果 foo 等于 10,刚执行 doSomething();
// 输出: 10

foo === 5 || doSomething();
// is the same thing as if (foo != 5) then doSomething();
// Output: 10

6. 去除重复值

const array  = [5,4,7,8,9,2,7,5];
array.filter((item,idx,arr) => arr.indexOf(item) === idx);
// or
const nonUnique = [...new Set(array)];
// Output: [5, 4, 7, 8, 9, 2]

7. 创建一个计数器对象或 Map

大多数情况下,可以通过创建一个对象或者Map来计数某些特殊词出现的频率。

let string = 'kapilalipak';

const table={};
for(let char of string) {
table[char]=table[char]+1 || 1;
}
// 输出
{k: 2, a: 3, p: 2, i: 2, l: 2}

或者

const countMap = new Map();
for (let i = 0; i < string.length; i++) {
if (countMap.has(string[i])) {
countMap.set(string[i], countMap.get(string[i]) + 1);
} else {
countMap.set(string[i], 1);
}
}
// 输出
Map(5) {"k" => 2, "a" => 3, "p" => 2, "i" => 2, "l" => 2}

8. 三元运算符很酷

function Fever(temp) {
return temp > 97 ? 'Visit Doctor!'
: temp < 97 ? 'Go Out and Play!!'
: temp === 97 ? 'Take Some Rest!': 'Go Out and Play!';;
}

// 输出
Fever(97): "Take Some Rest!"
Fever(100): "Visit Doctor!"

9. 循环方法的比较

  • for 和 for..in 默认获取索引,但你可以使用arr[index]
  • for..in也接受非数字,所以要避免使用。
  • forEachfor...of 直接得到元素。
  • forEach 也可以得到索引,但 for...of 不行。

10. 合并两个对象

const user = { 
name: 'Kapil Raghuwanshi',
gender: 'Male'
};
const college = {
primary: 'Mani Primary School',
secondary: 'Lass Secondary School'
};
const skills = {
programming: 'Extreme',
swimming: 'Average',
sleeping: 'Pro'
};

const summary = {...user, ...college, ...skills};

// 合并多个对象
gender: "Male"
name: "Kapil Raghuwanshi"
primary: "Mani Primary School"
programming: "Extreme"
secondary: "Lass Secondary School"
sleeping: "Pro"
swimming: "Average"

11. 箭头函数

箭头函数表达式是传统函数表达式的一种替代方式,但受到限制,不能在所有情况下使用。因为它们有词法作用域(父作用域),并且没有自己的thisargument,因此它们引用定义它们的环境。

const person = {
name: 'Kapil',
sayName() {
return this.name;
}
}
person.sayName();
// 输出
"Kapil"

但是这样:

const person = {
name: 'Kapil',
sayName : () => {
return this.name;
}
}
person.sayName();
// Output
"

13. 可选的链

const user = {
employee: {
name: "Kapil"
}
};
user.employee?.name;
// Output: "Kapil"
user.employ?.name;
// Output: undefined
user.employ.name
// 输出: VM21616:1 Uncaught TypeError: Cannot read property 'name' of undefined

13.洗牌一个数组

利用内置的Math.random()方法。

const list = [1, 2, 3, 4, 5, 6, 7, 8, 9];
list.sort(() => {
return Math.random() - 0.5;
});
// 输出
(9) [2, 5, 1, 6, 9, 8, 4, 3, 7]
// 输出
(9) [4, 1, 7, 5, 3, 8, 2, 9, 6]

14.双问号语法

const foo = null ?? 'my school';
// 输出: "my school"

const baz = 0 ?? 42;
// 输出: 0

剩余和展开语法

function myFun(a,  b, ...manyMoreArgs) {
return arguments.length;
}
myFun("one", "two", "three", "four", "five", "six");

// 输出: 6

const parts = ['shoulders', 'knees']; 
const lyrics = ['head', ...parts, 'and', 'toes'];

lyrics;
// 输出:
(5) ["head", "shoulders", "knees", "and", "toes"]

16.默认参数

const search = (arr, low=0,high=arr.length-1) => {
return high;
}
search([1,2,3,4,5]);

// 输出: 4

17. 将十进制转换为二进制或十六进制

const num = 10;

num.toString(2);
// 输出: "1010"
num.toString(16);
// 输出: "a"
num.toString(8);
// 输出: "12"

18. 使用解构来交换两个数

let a = 5;
let b = 8;
[a,b] = [b,a]

[a,b]
// 输出
(2) [8, 5]

19. 单行的回文数检查

function checkPalindrome(str) {
return str == str.split('').reverse().join('');
}
checkPalindrome('naman');
// 输出: true

20.将Object属性转换为属性数组

const obj = { a: 1, b: 2, c: 3 };

Object.entries(obj);
// Output
(3) [Array(2), Array(2), Array(2)]
0: (2) ["a", 1]
1: (2) ["b", 2]
2: (2) ["c", 3]
length: 3

Object.keys(obj);
(3) ["a", "b", "c"]

Object.values(obj);
(3) [1, 2, 3]



原文:https://dev.to/techygeeky/top...


收起阅读 »

从 Flutter 和前端角度出发,聊聊单线程模型下如何保证 UI 流畅性

一、单线程模型的设计1. 最基础的单线程处理简单任务假设有几个任务:任务1: "姓名:" + "杭城小刘"任务2: "年龄:" + "1995" + "02" + "20"任务3: "大小:" + (2021 - 1995 + 1)任务4: 打印任务1、2、3...
继续阅读 »

一、单线程模型的设计

1. 最基础的单线程处理简单任务

假设有几个任务:

  • 任务1: "姓名:" + "杭城小刘"
  • 任务2: "年龄:" + "1995" + "02" + "20"
  • 任务3: "大小:" + (2021 - 1995 + 1)
  • 任务4: 打印任务1、2、3 的结果

在单线程中执行,代码可能如下:

//c
void mainThread () {
string name = "姓名:" + "杭城小刘";
string birthday = "年龄:" + "1995" + "02" + "20"
int age = 2021 - 1995 + 1;
printf("个人信息为:%s, %s, 大小:%d", name.c_str(), birthday.c_str(), age);
}

线程开始执行任务,按照需求,单线程依次执行每个任务,执行完毕后线程马上退出。

2. 线程运行过程中来了新的任务怎么处理?

问题1 介绍的线程模型太简单太理想了,不可能从一开始就 n 个任务就确定了,大多数情况下,会接收到新的 m 个任务。那么 section1 中的设计就无法满足该需求。

要在线程运行的过程中,能够接受并执行新的任务,就需要有一个事件循环机制。最基础的事件循环可以想到用一个循环来实现。

// c++
int getInput() {
int input = 0;
cout<< "请输入一个数";
cin>>input;
return input;
}

void mainThread () {
while(true) {
int input1 = getInput();
int input2 = getInput();
int sum = input1 + input2;
print("两数之和为:%d", sum);
}
}

相较于第一版线程设计,这一版做了以下改进:

  • 引入了循环机制,线程不会做完事情马上退出。
  • 引入了事件。线程一开始会等待用户输入,等待的时候线程处于暂停状态,当用户输入完毕,线程得到输入的信息,此时线程被激活。执行相加的操作,最终输出结果。不断的等待输入,并计算输出。

3. 处理来自其他线程的任务

真实环境中的线程模块远远没有这么简单。比如浏览器环境下,线程可能正在绘制,可能会接收到1个来自用户鼠标点击的事件,1个来自网络加载 css 资源完成的事件等等。第二版线程模型虽然引入了事件循环机制,可以接受新的事件任务,但是发现没?这些任务之来自线程内部,该设计是无法接受来自其他线程的任务的。

从上图可以看出,渲染主线程会频繁接收到来自于 IO 线程的一些事件任务,当接受到的资源加载完成后的消息,则渲染线程会开始 DOM 解析;当接收到来自鼠标点击的消息,渲染主线程则会执行绑定好的鼠标点击事件脚本(js)来处理事件。

需要一个合理的数据结构,来存放并获取其他线程发送的消息?

消息队列这个词大家都听过,在 GUI 系统中,事件队列是一个通用解决方案。

消息队列(事件队列)是一种合理的数据结构。要执行的任务添加到队列的尾部,需要执行的任务,从队列的头部取出。

有了消息队列之后,线程模型得到了升级。如下:

可以看出改造分为3个步骤:

  • 构建一个消息队列
  • IO 线程产生的新任务会被添加到消息队列的尾部
  • 渲染主线程会循环的从消息队列的头部读取任务,执行任务

伪代码。构造队列接口部分

class TaskQueue {
public:
Task fetchTask (); // 从队列头部取出1个任务
void addTask (Task task); // 将任务插入到队列尾部
}

改造主线程

TaskQueue taskQueue;
void processTask ();
void mainThread () {
while (true) {
Task task = taskQueue.fetchTask();
processTask(task);
}
}

IO 线程

void handleIOTask () {
Task clickTask;
taskQueue.addTask(clickTask);
}

Tips: 事件队列是存在多线程访问的情况,所以需要加锁。

4. 处理来自其他线程的任务

浏览器环境中, 渲染进程经常接收到来自其他进程的任务,IO 线程专门用来接收来自其他进程传递来的消息。IPC 专门处理跨进程间的通信。

5. 消息队列中的任务类型

消息队列中有很多消息类型。内部消息:如鼠标滚动、点击、移动、宏任务、微任务、文件读写、定时器等等。

消息队列中还存在大量的与页面相关的事件。如 JS 执行、DOM 解析、样式计算、布局计算、CSS 动画等等。

上述事件都是在渲染主线程中执行的,因此编码时需注意,尽量减小这些事件所占用的时长。

6. 如何安全退出

Chrome 设计上,确定要退出当前页面时,页面主线程会设置一个退出标志的变量,每次执行完1个任务时,判断该标志。如果设置了,则中断任务,退出线程

7. 单线程的缺点

事件队列的特点是先进先出,后进后出。那后进的任务也许会被前面的任务因为执行时间过长而阻塞,等待前面的任务执行完毕才可以执行后面的任务。这样存在2个问题。

  • 如何处理高优先级的任务

    假如要监控 DOM 节点的变化情况(插入、删除、修改 innerHTML),然后触发对应的逻辑。最基础的做法就是设计一套监听接口,当 DOM 变化时,渲染引擎同步调用这些接口。不过这样子存在很大的问题,就是 DOM 变化会很频繁。如果每次 DOM 变化都触发对应的 JS 接口,则该任务执行会很长,导致执行效率的降低

    如果将这些 DOM 变化做为异步消息,假如消息队列中。可能会存在因为前面的任务在执行导致当前的 DOM 消息不会被执行的问题,也就是影响了监控的实时性

    如何权衡效率和实时性?微任务 就是解决该类问题的。

    通常,我们把消息队列中的任务成为宏任务,每个宏任务中都包含一个微任务队列,在执行宏任务的过程中,假如 DOM 有变化,则该变化会被添加到该宏任务的微任务队列中去,这样子效率问题得以解决。

    当宏任务中的主要功能执行完毕欧,渲染引擎会执行微任务队列中的微任务。因此实时性问题得以解决

  • 如何解决单个任务执行时间过长的问题

    可以看出,假如 JS 计算超时导致动画 paint 超时,会造成卡顿。浏览器为避免该问题,采用 callback 回调的设计来规避,也就是让 JS 任务延后执行。

二、 flutter 里的单线程模型

1. event loop 机制

Dart 是单线程的,也就是代码会有序执行。此外 Dart 作为 Flutter 这一 GUI 框架的开发语言,必然支持异步。

一个 Flutter 应用包含一个或多个 isolate,默认方法的执行都是在 main isolate 中;一个 isolate 包含1个 Event loop 和1个 Task queue。其中,Task queue 包含1个 Event queue 事件队列和1个 MicroTask queue 微任务队列。如下:

为什么需要异步?因为大多数场景下 应用都并不是一直在做运算。比如一边等待用户的输入,输入后再去参与运算。这就是一个 IO 的场景。所以单线程可以再等待的时候做其他事情,而当真正需要处理运算的时候,再去处理。因此虽是单线程,但是给我们的感受是同事在做很多事情(空闲的时候去做其他事情)

某个任务涉及 IO 或者异步,则主线程会先去做其他需要运算的事情,这个动作是靠 event loop 驱动的。和 JS 一样,dart 中存储事件任务的角色是事件队列 event queue。

Event queue 负责存储需要执行的任务事件,比如 DB 的读取。

Dart 中存在2个队列,一个微任务队列(Microtask Queue)、一个事件队列(Event Queue)。

Event loop 不断的轮询,先判断微任务队列是否为空,从队列头部取出需要执行的任务。如果微任务队列为空,则判断事件队列是否为空,不为空则从头部取出事件(比如键盘、IO、网络事件等),然后在主线程执行其回调函数,如下:

2. 异步任务

微任务,即在一个很短的时间内就会完成的异步任务。微任务在事件循环中优先级最高,只要微任务队列不为空,事件循环就不断执行微任务,后续的事件队列中的任务持续等待。微任务队列可由 scheduleMicroTask 创建。

通常情况,微任务的使用场景比较少。Flutter 内部也在诸如手势识别、文本输入、滚动视图、保存页面效果等需要高优执行任务的场景用到了微任务。

所以,一般需求下,异步任务我们使用优先级较低的 Event Queue。比如 IO、绘制、定时器等,都是通过事件队列驱动主线程来执行的。

Dart 为 Event Queue 的任务提供了一层封装,叫做 Future。把一个函数体放入 Future 中,就完成了同步任务到异步任务的包装(类似于 iOS 中通过 GCD 将一个任务以同步、异步提交给某个队列)。Future 具备链式调用的能力,可以在异步执行完毕后执行其他任务(函数)。

看一段具体代码:

void main() {
print('normal task 1');
Future(() => print('Task1 Future 1'));
print('normal task 2');
Future(() => print('Task1 Future 2'))
.then((value) => print("subTask 1"))
.then((value) => print("subTask 2"));
}
//
lbp@MBP  ~/Desktop  dart index.dart
normal task 1
normal task 2
Task1 Future 1
Task1 Future 2
subTask 1
subTask 2

main 方法内,先添加了1个普通同步任务,然后以 Future 的形式添加了1个异步任务,Dart 会将异步任务加入到事件队列中,然后理解返回。后续代码继续以同步任务的方式执行。然后再添加了1个普通同步任务。然后再以 Future 的方式添加了1个异步任务,异步任务被加入到事件队列中。此时,事件队列中存在2个异步任务,Dart 在事件队列头部取出1个任务以同步的方式执行,全部执行(先进先出)完毕后再执行后续的 then。

Future 与 then 公用1个事件循环。如果存在多个 then,则按照顺序执行。

例2:

void main() {
Future(() => print('Task1 Future 1'));
Future(() => print('Task1 Future 2'));

Future(() => print('Task1 Future 3'))
.then((_) => print('subTask 1 in Future 3'));

Future(() => null).then((_) => print('subTask 1 in empty Future'));
}
lbp@MBP  ~/Desktop  dart index.dart
Task1 Future 1
Task1 Future 2
Task1 Future 3
subTask 1 in Future 3
subTask 1 in empty Future

main 方法内,Task 1 添加到 Future 1中,被 Dart 添加到 Event Queue 中。Task 1 添加到 Future 2中,被 Dart 添加到 Event Queue 中。Task 1 添加到 Future 3中,被 Dart 添加到 Event Queue 中,subTask 1 和 Task 1 共用 Event Queue。Future 4中任务为空,所以 then 里的代码会被加入到 Microtask Queue,以便下一轮事件循环中被执行。

综合例子

void main() {
Future(() => print('Task1 Future 1'));
Future fx = Future(() => null);
Future(() => print("Task1 Future 3")).then((value) {
print("subTask 1 Future 3");
scheduleMicrotask(() => print("Microtask 1"));
}).then((value) => print("subTask 3 Future 3"));

Future(() => print("Task1 Future 4"))
.then((value) => Future(() => print("sub subTask 1 Future 4")))
.then((value) => print("sub subTask 2 Future 4"));

Future(() => print("Task1 Future 5"));

fx.then((value) => print("Task1 Future 2"));

scheduleMicrotask(() => print("Microtask 2"));

print("normal Task");
}
lbp@MBP  ~/Desktop  dart index.dart
normal Task
Microtask 2
Task1 Future 1
Task1 Future 2
Task1 Future 3
subTask 1 Future 3
subTask 3 Future 3
Microtask 1
Task1 Future 4
Task1 Future 5
sub subTask 1 Future 4
sub subTask 2 Future 4

解释:

  • Event Loop 优先执行 main 方法同步任务,再执行微任务,最后执行 Event Queue 的异步任务。所以 normal Task 先执行
  • 同理微任务 Microtask 2 执行
  • 其次,Event Queue FIFO,Task1 Future 1 被执行
  • fx Future 内部为空,所以 then 里的内容被加到微任务队列中去,微任务优先级最高,所以 Task1 Future 2 被执行
  • 其次,Task1 Future 3 被执行。由于存在2个 then,先执行第一个 then 中的 subTask 1 Future 3,然后遇到微任务,所以 Microtask 1 被添加到微任务队列中去,等待下一次 Event Loop 到来时触发。接着执行第二个 then 中的 subTask 3 Future 3。随着下一次 Event Loop 到来,Microtask 1 被执行
  • 其次,Task1 Future 4 被执行。随后的第一个 then 中的任务又是被 Future 包装成一个异步任务,被添加到 Event Queue 中,第二个 then 中的内容也被添加到 Event Queue 中。
  • 接着,执行 Task1 Future 5。本次事件循环结束
  • 等下一轮事件循环到来,打印队列中的 sub subTask 1 Future 4、sub subTask 1 Future 5.

3. 异步函数

异步函数的结果在将来某个时刻才返回,所以需要返回一个 Future 对象,供调用者使用。调用者根据需求,判断是在 Future 对象上注册一个 then 等 Future 执行体结束后再进行异步处理,还是同步等到 Future 执行结束。Future 对象如果需要同步等待,则需要在调用处添加 await,且 Future 所在的函数需要使用 async 关键字。

await 并不是同步等待,而是异步等待。Event Loop 会将调用体所在的函数也当作异步函数,将等待语句的上下文整体添加到 Event Queue 中,一旦返回,Event Loop 会在 Event Queue 中取出上下文代码,等待的代码继续执行。

await 阻塞的是当前上下文的后续代码执行,并不能阻塞其调用栈上层的后续代码执行

void main() {
Future(() => print('Task1 Future 1'))
.then((_) async => await Future(() => print("subTask 1 Future 2")))
.then((_) => print("subTask 2 Future 2"));
Future(() => print('Task1 Future 2'));
}
lbp@MBP  ~/Desktop  dart index.dart
Task1 Future 1
Task1 Future 2
subTask 1 Future 2
subTask 2 Future 2

解析:

  • Future 中的 Task1 Future 1 被添加到 Event Queue 中。其次遇到第一个 then,then 里面是 Future 包装的异步任务,所以 Future(() => print("subTask 1 Future 2")) 被添加到 Event Queue 中,所在的 await 函数也被添加到了 Event Queue 中。第二个 then 也被添加到 Event Queue 中
  • 第二个 Future 中的 'Task1 Future 2 不会被 await 阻塞,因为 await 是异步等待(添加到 Event Queue)。所以执行 'Task1 Future 2。随后执行 "subTask 1 Future 2,接着取出 await 执行 subTask 2 Future 2

4. Isolate

Dart 为了利用多核 CPU,将 CPU 层面的密集型计算进行了隔离设计,提供了多线程机制,即 Isolate。每个 Isolate 资源隔离,都有自己的 Event Loop 和 Event Queue、Microtask Queue。Isolate 之间的资源共享通过消息机制通信(和进程一样)

使用很简单,创建时需要传递一个参数。

void coding(language) {
print("hello " + language);
}
void main() {
Isolate.spawn(coding, "Dart");
}
lbp@MBP  ~/Desktop  dart index.dart
hello Dart

大多数情况下,不仅仅需要并发执行。可能还需要某个 Isolate 运算结束后将结果告诉主 Isolate。可以通过 Isolate 的管道(SendPort)实现消息通信。可以在主 Isolate 中将管道作为参数传递给子 Isolate,当子 Isolate 运算结束后将结果利用这个管道传递给主 Isolate

void coding(SendPort port) {
const sum = 1 + 2;
// 给调用方发送结果
port.send(sum);
}

void main() {
testIsolate();
}

testIsolate() async {
ReceivePort receivePort = ReceivePort(); // 创建管道
Isolate isolate = await Isolate.spawn(coding, receivePort.sendPort); // 创建 Isolate,并传递发送管道作为参数
// 监听消息
receivePort.listen((message) {
print("data: $message");
receivePort.close();
isolate?.kill(priority: Isolate.immediate);
isolate = null;
});
}
lbp@MBP  ~/Desktop  dart index.dart
data: 3

此外 Flutter 中提供了执行并发计算任务的快捷方式-compute 函数。其内部对 Isolate 的创建和双向通信进行了封装。

实际上,业务开发中使用 compute 的场景很少,比如 JSON 的编解码可以用 compute。

计算阶乘:

int testCompute() async {
return await compute(syncCalcuateFactorial, 100);
}

int syncCalcuateFactorial(upperBounds) => upperBounds < 2
? upperBounds
: upperBounds * syncCalcuateFactorial(upperBounds - 1);

总结:

  • Dart 是单线程的,但通过事件循环可以实现异步
  • Future 是异步任务的封装,借助于 await 与 async,我们可以通过事件循环实现非阻塞的同步等待
  • Isolate 是 Dart 中的多线程,可以实现并发,有自己的事件循环与 Queue,独占资源。Isolate 之间可以通过消息机制进行单向通信,这些传递的消息通过对方的事件循环驱动对方进行异步处理。
  • flutter 提供了 CPU 密集运算的 compute 方法,内部封装了 Isolate 和 Isolate 之间的通信
  • 事件队列、事件循环的概念在 GUI 系统中非常重要,几乎在前端、Flutter、iOS、Android 甚至是 NodeJS 中都存在。
收起阅读 »

JavaScript中关于null的一切

JavaScript有2种类型:基本类型(string, booleans number, symbol)和对象。对象是复杂的数据结构,JS 中最简单的对象是普通对象:一组键和关联值:let myObject = { name...
继续阅读 »

JavaScript有2种类型:基本类型(stringbooleans numbersymbol)和对象。

对象是复杂的数据结构,JS 中最简单的对象是普通对象:一组键和关联值:

let myObject = {
name: '前端小智'
}

但是在某些情况下无法创建对象。 在这种情况下,JS 提供一个特殊值null —表示缺少对象。

let myObject = null

在本文中,我们将了解到有关JavaScript中null的所有知识:它的含义,如何检测它,nullundefined之间的区别以及为什么使用null造成代码维护困难。

1. null的概念

JS 规范说明了有关null的信息:

值 null 特指对象的值未设置,它是 JS 基本类型 之一,在布尔运算中被认为是falsy

例如,函数greetObject()创建对象,但是在无法创建对象时也可以返回null

function greetObject(who) {
if (!who) {
return null;
}
return { message: `Hello, ${who}!` };
}

greetObject('Eric'); // => { message: 'Hello, Eric!' }
greetObject(); // => null

但是,在不带参数的情况下调用函数greetObject() 时,该函数返回null。 返回null是合理的,因为who参数没有值。

2. 如何检查null

检查null值的好方法是使用严格相等运算符:

const missingObject = null;
const existingObject = { message: 'Hello!' };

missingObject === null; // => true
existingObject === null; // => false

missingObject === null的结果为true,因为missingObject变量包含一个null 值。

如果变量包含非空值(例如对象),则表达式existObject === null的计算结果为false

2.1 null 是虚值

nullfalse0''undefinedNaN都是虚值。如果在条件语句中遇到虚值,那么 JS 将把虚值强制为false

Boolean(null); // => false

if (null) {
console.log('null is truthy')
} else {
console.log('null is falsy')
}

2.2 typeof null

typeof value运算符确定值的类型。 例如,typeof 15是'number'typeof {prop:'Value'}的计算结果是'object'

有趣的是,type null的结果是什么

typeof null// => 'object'

为什么是'object'typoef nullobject是早期 JS 实现中的一个错误。

要使用typeof运算符检测null值。 如前所述,使用严格等于运算符myVar === null

如果我们想使用typeof运算符检查变量是否是对象,还需要排除null值:

function isObject(object) {
return typeof object === 'object' && object !== null;
}

isObject({ prop: 'Value' }); // => true
isObject(15); // => false
isObject(null); // => false

3. null 的陷阱

null经常会在我们认为该变量是对象的情况下意外出现。然后,如果从null中提取属性,JS 会抛出一个错误。

再次使用greetObject() 函数,并尝试从返回的对象访问message属性:

let who = '';

greetObject(who).message;
// throws "TypeError: greetObject() is null"

因为who变量是一个空字符串,所以该函数返回null。 从null访问message属性时,将引发TypeError错误。

可以通过使用带有空值合并的可选链接来处理null:

let who = ''

greetObject(who)?.message ?? 'Hello, Stranger!'
// => 'Hello, Stranger!'

4. null 的替代方法

当无法构造对象时,我们通常的做法是返回null,但是这种做法有缺点。在执行堆栈中出现null时,刚必须进行检查。

尝试避免返回 null 的做法:

  • 返回默认对象而不是null
  • 抛出错误而不是返回null

回到开始返回greeting对象的greetObject()函数。缺少参数时,可以返回一个默认对象,而不是返回null

function greetObject(who) {
if (!who) {
who = 'Stranger';
}
return { message: `Hello, ${who}!` };
}

greetObject('Eric'); // => { message: 'Hello, Eric!' }
greetObject(); // => { message: 'Hello, Stranger!' }

或者抛出一个错误:

function greetObject(who) {
if (!who) {
throw new Error('"who" argument is missing');
}
return { message: `Hello, ${who}!` };
}

greetObject('Eric'); // => { message: 'Hello, Eric!' }
greetObject(); // => throws an error

这两种做法可以避免使用 null

5. null vs undefined

undefined是未初始化的变量或对象属性的值,undefined是未初始化的变量或对象属性的值。

let myVariable;

myVariable; // => undefined

nullundefined之间的主要区别是,null表示丢失的对象,而undefined表示未初始化的状态。

严格的相等运算符===区分nullundefined :

null === undefined // => false

而双等运算符==则认为nullundefined 相等

null == undefined // => true

我使用双等相等运算符检查变量是否为null 或undefined:

function isEmpty(value) {
return value == null;
}

isEmpty(42); // => false
isEmpty({ prop: 'Value' }); // => false
isEmpty(null); // => true
isEmpty(undefined); // => true

6. 总结

null是JavaScript中的一个特殊值,表示丢失的对象,严格相等运算符确定变量是否为空:variable === null

typoef运算符对于确定变量的类型(numberstringboolean)很有用。 但是,如果为null,则typeof会产生误导:typeof null的值为'object'

nullundefined在某种程度上是等价的,但null表示缺少对象,而undefined未初始化状态。


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

收起阅读 »

Web 动画原则及技巧浅析

在 Web 动画方面,有一套非常经典的原则 -- Twelve basic principles of animation,也就是关于动画的 12 个基本原则(也称之为迪士尼动画原则),网上对它的解读延伸的文章也非常之多:Animation Prin...
继续阅读 »

在 Web 动画方面,有一套非常经典的原则 -- Twelve basic principles of animation,也就是关于动画的 12 个基本原则(也称之为迪士尼动画原则),网上对它的解读延伸的文章也非常之多:

其中使用的示例 DEMO 属于比较简单易懂,但是没有很好地体现在实际生产中应该如何灵活运用。今天本文将带大家再次复习复习,并且替换其中的最基本的 DEMO,换成一些到今天非常实用,非常酷炫的动画 DEMO 效果。

Squash and stretch -- 挤压和拉伸

挤压和拉伸的目的是为绘制的对象赋予重量感和灵活性。它可以应用于简单的物体,如弹跳球,或更复杂的结构,如人脸的肌肉组织。

应用在动画中,这一原则最重要的方面是对象的体积在被挤压或拉伸时不会改变。如果一个球的长度被垂直拉伸,它的宽度(三个维度,还有它的深度)需要相应地水平收缩。

看看上面这张图,很明显右边这个运动轨迹要比左边的真实很多。

原理动画如下:

类似的一些比较有意思的 Web 动画 DEMO:

CodePen Demo -- CSS Flippy Loader 🍳 By Jhey

仔细看上面这个 Loading 动画,每个块在跳起之前都会有一个压缩准备动作,在压缩的过程中高度变低,宽度变宽,这就是挤压和拉伸,让动画看上去更加真实。

OK,再看两个类似的效果,加深下印象:

CodePen Demo -- CSS Loading Animation

CodePen Demo -- CSS Animation Loader - Jelly Box

简单总结一下,挤压和拉伸的核心在于保持对象的体积一致,当拉伸元素时,它的宽度需要变薄,而当挤压元素时,它的宽度需要变宽。

Anticipation -- 预备动作

准备动作用于为主要的动画动作做好准备,并使动作看起来更逼真。

譬如从地板上跳下来的舞者必须先弯曲膝盖,挥杆的高尔夫球手必须先将球杆向后挥动。

原理动画如下,能够看到滚动之前的一些准备动作:

看看一些实际应用的chang场景,下面这个动画效果:

CodePen Demo -- Never-ending box By Pawel

小球向上滚动,但是仔细看的话,每次向上滚动的时候都会先向后摆一下,可以理解为是一个蓄力动作,也就是我们说的准备动作。

类似的,看看这个购物车动画,运用了非常多的小技巧,其中之一就是,车在向前冲之前会后退一点点进行一个蓄力动作,整个动画的感觉明显就不一样,它让动画看起来更加的自然:

Staging -- 演出布局

Staging 意为演出布局,它的目的是引导观众的注意力,并明确一个场景中什么是最重要的。

可以通过多种方式来完成,例如在画面中放置角色、使用光影,或相机的角度和位置。该原则的本质是关注核心内容,避免其他不必要的细节吸引走用户的注意力。

原理动画如下:

上述 Gif 原理图效果不太明显,看看示例效果:

CodePen Demo -- CSS Loading Animation

该技巧的核心就是在动画的过程中把主体凸显,把非主体元素通过模糊、变淡等方式弱化其效果,降低用户在其之上的注意力。

Straight-Ahead Action and Pose-to-Pose -- 连续运动和姿态对应

其实表示的就是逐帧动画和补间动画:

  • FrameAnimation(逐帧动画):将多张图片组合起来进行播放,可以利用 CSS Aniation 的 Steps,画面由一帧一帧构成,类似于漫画书
  • TweenAnimation(补间动画):补间动画是在时间帧上进行关键帧绘制,不同于逐帧动画的每一帧都是关键帧,补间动画可以在一个关键帧上绘制一个基础形状,然后在时间帧上对另一个关键帧进行形状转变或绘制另一个形状等,然后中间的动画过程是由计算机自动生成。

这个应该是属于最基础的了,在不同场景下有不同的妙用。我们在用 CSS 实现动画的过程中,使用的比较多的应该是补间动画,逐帧动画也很有意思,譬如设计师设计好的复杂动画,利用多张图片拼接成逐帧动画也非常不错。

逐帧动画和补间动画适用在不同的场合,没有谁更好,只有谁更合适,比较下面两个时钟动画,其中一个的秒针运用的是逐帧动画,另外一个则是补间动画:

  • 时钟秒针运用的是逐帧动画:

CodePen Demo -- CSS3 Working Clock By Ilia

  • 时钟秒针运用的是补间动画:

CodePen Demo -- CSS Rotary Clock By Jake Albaugh

有的时候一些复杂动画无法使用 CSS 直接实现的,也会利用逐帧的效果近似实现一个补间动画,像是苹果这个耳机动画,就是实际逐帧动画,但是看起来是连续的:

CodePen Demo -- Apple AirPods Pro Animation (final demo) By Blake Bowen

这里其实是多张图片的快速轮播,每张图片表示一个关键帧。

Follow through and overlapping action 跟随和重叠动作

跟随和重叠动作是两种密切相关的技术的总称,它们有助于更真实地渲染运动,并有助于给人一种印象,即运动的元素遵循物理定律,包括惯性原理。

  • 跟随意味着在角色停止后,身体松散连接的部分应该继续移动,并且这些部分应该继续移动到角色停止的点之外,然后才被拉回到重心或表现出不同的程度的振荡阻尼;
  • 重叠动作是元素各部分以不同速率移动的趋势(手臂将在头部的不同时间移动等等);
  • 第三种相关技术是拖动,元素开始移动,其中一部分需要几帧才能追上。

要创造一个重叠动作的感觉,我们可以让元件以稍微不同的速度移动到每处。这是一种在 iOS 系统的视窗过渡中被运用得很好的方法。一些按钮和元件以不同速率运动,整体效果会比全部东西以相同速率运动要更逼真,并留出时间让访客去适当理解变化。

原理示意图:

看看下面这个购物车动画,仔细看购物车,在移动到停止的过程中,有个很明显的刹车再拉回的感觉,这里运用到了跟随的效果,让动画更加生动真实:

Slow In and Slow Out -- 缓入缓出

现实世界中物体的运动,如人体、动物、车辆等,需要时间来加速和减速。

真实的运动效果,它的缓动函数一定不是 Linear。出于这个原因,运动往往是逐步加速并在停止前变慢,实现一个慢进和慢出的效果,以贴近更逼真的动作。

示意图:

这个还是很好理解的。真实世界中,很少有缓动函数是 Linear 的运动。

Arc -- 弧线运动

大多数自然动作倾向于遵循一个拱形轨迹,动画应该遵循这个原则,遵循隐含的弧形以获得更大的真实感。

原理示意图:

嗯哼,在很多动画中,使用弧线代替直线,能够让动画效果更佳的逼真。看看下面这个烟花粒子动画:

CodePen Demo -- Particles, humankind's only weakness By Rik Schennink

整个烟花粒子动画看上去非常的自然,因为每个粒子的下落都遵循了自由落体的规律,它们的运动轨迹都是弧线而不是直线。

Secondary Action -- 次要动作

将次要动作添加到主要动作可以使场景更加生动,并有助于支持主要动作。走路的人可以同时摆动手臂或将手臂放在口袋里,说话或吹口哨,或者通过面部表情来表达情绪。

原理示意图:

简单的一个应用实例,看看下面这个动画:

CodePen Demo -- Submarine Animation (Pure CSS) By Akhil Sai Ram

这里实现了一个潜艇向前游动的画面,动画本身还有很多可以优化的地方。但也有一些值得学习肯定的地方,动画使用了尾浆转动和气泡和海底景物移动。

同时,值得注意的是,窗口的反光也是一个很小的细节,表示船体在移动,这个就属于一个次要动作,衬托出主体的移动。

再看看下面这打印动画,键盘上按键的上上下下模拟了点击效果,其实也是个次要动作,衬托主体动画效果:

![Secondary Action - CodePen Home
CSS Typewriter](https://p3-juejin.byteimg.com...

CodePen Demo -- CSS Typewriter By Aaron Iker

Timing -- 时间节奏

时间是指给定动作的绘图或帧数,它转化为动画动作的速度。

在纯粹的物理层面上,正确的计时会使物体看起来遵守物理定律。例如,物体的重量决定了它对推动力的反应,因为重量轻的物体会比重量大的物体反应更快。

同一个动画,使用不同的速率展示,其效果往往相差很多。对于 Web 动画而言,可能只需要调整 animation-duration 或 transition-duration 的值。

原理示意图:

可以看出,同个动画,不同的缓动函数,或者赋予不同的时间,就能产生很不一样的效果。

当然,时间节奏可以运用在很多地方,譬如在一些 Loading 动画中:

CodePen Demo -- Only Css 3D Cube By Hisami Kurita

又或者是这样,同个动画,不同的速率:

CodePen Demo -- Rotating Circles Preloader

也可以是同样的延迟、同样的速率,但是不同的方向:

CodePen Demo -- 2020 SVG Animation By @keyframers

Exaggeration -- 夸张手法

夸张是一种对动画特别有用的效果,因为力求完美模仿现实的动画动作可能看起来是静态和沉闷的。

使用夸张时,一定程度的克制很重要。如果一个场景包含多个元素,则应平衡这些元素之间的关系,以避免混淆或吓倒观众。

原理示意图:

OK,不同程度的展现对效果的感官是不一样的,对比下面两个故障艺术动画:

轻微晃动故障:

严重晃动故障:

CodePen Demo -- Glitch Animation

可以看出,第二个动画明显能感受到比第一个更严重的故障。

过多的现实主义会毁掉动画,或者说让它缺乏吸引力,使其显得静态和乏味。相反,为元素对象添加一些夸张,使它们更具活力,能够让它们更吸引眼球。

Solid drawing -- 扎实的描绘

这个原则表示我们的动画需要尊重真实性,譬如一个 3D 立体绘图,就需要考虑元素在三维空间中的形式。

了解掌握三维形状、解剖学、重量、平衡、光影等的基础知识。有助于我们绘制出更为逼真的动画效果。

原理示意图:

再再看看下面这个动画,名为 Close the blinds -- 关上百叶窗:

CodePen Demo -- Close the blinds By Chance Squires

hover 的时候有一个关上动画,使用多块 div 模拟了百叶窗的落下,同时配合了背景色从明亮到黑暗的过程,很好的利用了色彩光影辅助动画的展示。

再看看这个摆锤小动画,也是非常好的使用了光影、视角元素:

CodePen Demo -- The Three-Body Problem By Vian Esterhuizen

最后这个 Demo,虽然是使用 CSS 实现的,但是也尽可能的还原模拟了现实中纸张飞舞的形态,并且对纸张下方阴影的变化也做了一定的变化:

CodePen Demo -- D CSS-only flying page animation tutorial By @keyframers

好的动画,细节是经得起推敲的。

Appeal -- 吸引力

一反往常,精美的细节往往能非常好的吸引用户的注意力。

吸引力是艺术作品的特质,而如何实现有吸引力的作品则需要不断的尝试。

原理示意图:

我觉得这一点可能是 Web 动画的核心,一个能够吸引人的动画,它肯定是有某些特质的,让我们一起来欣赏下。

CodePen Demo -- Download interaction By Milan Raring

通过一连串的动作,动画展开、箭头移动、进度条填满、数字变化,把一个下载动画展示的非常 Nice,让人在等待的过程并不觉得枯燥。

再来看看这个视频播放的效果:

CodePen Demo -- Video button animation - Only CSS

通过一个遮罩 hover 放大,再到点击全屏的变化,一下子就将用户的注意力给吸引了过来。

Web 动画的一些常见误区

当然,上述的一些技巧源自于迪士尼动画原则,我们可以将其中的一些思想贯穿于我们的 Web 动画的设计之中。

但是,必须指出的是,Web 动画本身在使用的时候,也有一些原则是我们需要注意的。主要有下面几点:

  • 增强动画与页面元素之间的关联性
  • 不要为了动画而动画,要有目的性
  • 动画不要过于缓慢,否则会阻碍交互

增强动画与页面元素之间的关联性

这个是一个常见的问题,经常会看到一些动画与主体之间没有关联性。关联性背后的逻辑,能帮助用户在界面布局中理解刚发生的变化,是什么导致了变化。

好的动画可以做到将页面的多个环节或者场景有效串联。

比较下面两个动画,第二个就比第一个更有关联性:

没有强关联性的:

有关联性的:

很明显,第二个动画比第一个动画更能让用户了解页面发生的变化。

不要为了动画而动画,要有目的性

这一点也很重要,不要为了动画而动画,要有目的性,很多时候很多页面的动画非常莫名其妙。

emm,简单一点来说就是单纯的为了炫技而存在的动画。这种动画可以存在于你的 Demo,你的个人网站中,但不太适合用于线上业务页面中。

使用动画应该有明确的目的性,譬如 Loading 动画能够让用户感知到页面正在发生变化,正在加载内容。

在我们的交互过程中,适当的增加过渡与动画,能够很好的让用户感知到页面的变化。类似的还有一些滚动动画。丝滑的滚动切换比突兀的内容明显是更好的体验。

动画不要过于缓慢,否则会阻碍交互

缓慢的动画,它产生了不必要的停顿。

一些用户会频繁看到它们的过渡动画,尽可能的保持简短。让动画持续时间保持在 300ms 或更短。

比较下面两个动画,第一次可能会让用户耳目一新,但是如果用户在浏览过程中频繁出现通过操作,过长的转场动画会消耗用户大量不必要的时间:

过长的转场动画:

缩短转场动画时间,保持恰当的时长:

结合产品及业务的创意交互动画

这一点是比较有意思的。我个人认为,Web 动画做得好用的妙,是能非常好的提升用户体验,提升品牌价值的。

结合产品及业务的创意动画,是需要挖掘,不断打磨的不断迭代的。譬如大家津津乐道的 BiliBili 官网,它的顶部 Banner,配合一些节日、活动,经常就会有出现一些有意思的创意交互动画。简单看两个:

以及这个:

我非常多次在不同地方看到有人讨论 Bilibili 的顶部 banner 动画,可见它这块的动画是成功的。很好的结合了一些节日、实事、热点,当成了一种比较固定的产品去不断推陈出新,在不同时候给与用户不同的体验。

考虑动画的性价比

最后一条,就是动画虽好,但是打磨一个精品动画是非常耗时的,尤其是在现在非常多的产品业务都是处于一种敏捷开发迭代之下。

一个好的 Web 动画从构思到落地,绝非前端一个人的工作,需要产品、设计、前端等等相关人员公共努力, 不断修改才能最终呈现比较好的效果。所以在项目初期,一定需要考虑好性价比,是否真的值得为了一个 Web 动画花费几天时间呢?当然这是一个非常见仁见智的问题。

参考文章

最后

想使用 Web 技术绘制生动有趣的动画并非易事,尤其在现在国内的大环境下,鲜有人会去研究动画原则,并运用于实践生产之中。但是它本身确实是个非常有意思有技术的事情。希望本文能给大伙对 Web 动画的认知带来一些提升和帮助,在后续的工作中多少运用一些。

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

收起阅读 »

这个vue3的应用框架你学习了吗?

vue
1.新项目初期当我们开始一个新项目的筹备的时候(这里特指中后台应用),项目初始化往往我们可能会考虑以下几个问题如何统一做权限管理?如何统一对请求库比如基于 Axios做封装(取消重复请求、请求节流、错误异常处理等统一处理)如何作为子应用嵌入到微前端体系(假设基...
继续阅读 »

1.新项目初期

当我们开始一个新项目的筹备的时候(这里特指中后台应用),项目初始化往往我们可能会考虑以下几个问题
  • 如何统一做权限管理?
  • 如何统一对请求库比如基于 Axios做封装(取消重复请求、请求节流、错误异常处理等统一处理)
  • 如何作为子应用嵌入到微前端体系(假设基于qiankun)
  • 如何共享响应式数据?
  • 配置信息如何管理?

1.1 你可能会这样做

如果每次新建一个项目得时候,我们都得手动去处理以上这些问题,那么将是一个重复性操作,而且还要确保团队一致,那么还得考虑约束能力

在没有看到这个Fes.js这个解决方案之前,对于上述问题,我的解决方式就是
  • 通过维护一个公共的工具库来封装,比如axios的二次封装
  • 开发一个简易的脚手架,把这些东西集成到一个模板中,再通过命令行去拉取
  • 直接通过vue-cli生成模板再进行自定义配置修改等等,简单就是用文档,工具,脚手架来赋能

    但其实有没有更好的解决方案?

图片引自文章《蚂蚁前端研发最佳实践》

1.2 其他解决方式 - 框架(插件化)

学习react的童鞋都知道,在react社区有个插件化的前端应用框架 UmiJS,而vue的世界中并不存在,而接下来我们要分享的 Fes.js就是vue中的 UmiJS, Fes.js 很多功能是借鉴 UmiJS 做的, UmiJS 内置了路由、构建、部署、测试等,还支持插件和插件集,以满足功能和垂直域的分层需求。

本质上是为了更便捷、更快速地开发中后台应用。框架的核心是插件管理,提供的内置插件封装了大量构建相关的逻辑,并且有着丰富的插件生态,业务中需要处理的脏活累活靠插件来解决,而用户只需要简单配置或者按照规范使用即可

甚至你还可以将插件做聚合成插件集,类似 babel 的 plugin 和 preset,或者 eslint 的 rule 和 config。通过插件和插件集来满足不同场合的业务

通过插件扩展 import from UmiJS 的能力,比如类似下图,是不是很像vue 3Composition API设计

拓展阅读:

  • UmiJS 插件体系的一些初步理解

    2. Fes.js

    官方介绍: Fes.js 是一个好用的前端应用解决方案。 Fes.js 2.0 以Vue 3.0和路由为基础,同时支持配置式路由和约定式路由,并以此进行功能扩展。匹配了覆盖编译时和运行时生命周期完善的插件体系,支持各种功能扩展和业务需求。

2.1 支持约定式路由

约定式路由是个啥? 约定式路由也叫文件路由,就是不需要手写配置,文件系统即路由,现在越来越多框架支持约定式路由,包括上文提到的 UmiJS,还有SSR的nuxt等等,节省我们手动配置路由的时间. 关于fes更多的路由配置看路由文档

2.2 插件化支持

本质上一个插件是就是一个npm包, 通过插件扩展Fes.js的功能,目前 Fes.js已经有多个插件开源。而且插件可以管理项目的编译时和运行时 插件文档

插件源码地址 链接。fesjs也支持开发者自定义插件,详情看插件化开发文档

彬彬同学: 那什么叫支持编译时和运行时?

可以这样理解: 如果是编译时的配置,就是打包的时候,就根据配置完成相应的代码构建,而运行时的配置,则是代码在浏览器执行时,才会根据读取的配置去做相应处理,如果感兴趣,可以深入理解下fesjs的插件源码,了解如何根据编译时和运行时做处理 fes-plugin-access 源码链接

2.3 Fes.js 如何使用

Fes.js 提供了命令行工具 create-fes-app, 全局安装后直接通过该命令创建项目模板,项目结构如下所示

然后运行 npm run dev 就可以开启你的fes之路, 如下图所示

2.4 为啥选择 Fes.js

像vue-cli 只能解决我们项目中开发,构建,打包等基本问题,而 Fes.js可以直接解决大部分常规中后台应用的业务场景的问题,包括如下
  • 配置化布局:解决布局、菜单 、导航等配置问题,类似low-code机制
  • 权限控制:通过内置的access插件实现站点复杂权限管理
  • 请求库封装:通过内置的request插件,内置请求防重、请求节流、错误处理等功能
  • 微前端集成:通过内置qiankun插件,快速集成到微前端中体系

期待更多的插件可以赋能中后台应用业务场景

3.回顾 vue 3

3.1 新特征

vue3.0 相对于 vue2.0变更几个比较大的点包括如下

  • 性能提升: 随着主流浏览器对es6的支持,es module成为可以真正落地的方案,也进一步优化了vue的性能
  • 支持typescript: 通过ts其类型检查机制,可避免我们在重构过程中引入意外的错误
  • 框架体积变小:框架体积优化后,一方面是因为引入Composition API的设计,同时支持tree-shaking树摇,按需引入模块API,将无用模块都会最终被摇掉,使得最终打包后的bundle的体积更小
  • 更优的虚拟Dom方案实现 : 添加了标记flag,Vue2的Virtual DOM不管变动多少整个模板会进行重新的比对, 而vue3对动态dom节点进行了标记PatchFlag ,只需要追踪带有PatchFlag的节点。并且当节点的嵌套层级多的情况,动态节点都是直接跟根节点直接绑定的,也就是说当diff算法走到了根dom节点的时候,就会直接定位动态变化的节点,并不会去遍历静态dom节点,以此提升了效率
  • 引入Proxy特性: 取代了vue2的Object.defineProperty来实现双向绑定,因为其本身的局限性,只能劫持对象的属性,如果对象属性值是对象,还需要进行深度遍历,才能做到劫持,并不能真正意义上的完整劫持整个对象,而proxy可以完整劫持整个对象

3.2 关于 Composition API

vue3 取代了原本vue2通过Options API来构建组件设计(强制我们进行代码分层),而采用了类似React Hooks的设计,通过可组组合式的、低侵入式的、函数式的 API,使得我们构建组件更加灵活。官方定义:一组基于功能的附加API,可以灵活地组合组件逻辑

通过上图的对比,我们可以看出Composition API 与 Options API在构建组件的差别,很明显基于Composition API构建会更加清晰明了。我们会发现vue3几个不同的点:

  • vue3提供了两种数据响应式监听APIrefreactive,这两者的区别在 reactive主要用于定义复杂的数据类型比如对象,而ref则用于定义基本类型比如字符串
  • vue3 提供了setup(props, context)方法,这是使用Composition API 的前提入口,相当于 vue2.x 在 生命周期beforeCreate 之后 created 之前执行,方法中的props参数是用来获取在组件中定义的props的,需要注意的是props是响应式的, 并不能使用es6解构(它会消除prop的响应性),如果需要监听响应还需要使用wacth。而context参数来用来获取attribute,获取插槽,或者发送事件,比如 context.emit,因为在setup里面没有this上下文,只能使用context来获取山下文

关于vue3的更多实践后期会继续更新,本期主要是简单回顾

你好,我是🌲 树酱,请你喝杯🍵 记得三连哦~

1.阅读完记得点个赞哦,有👍 有动力

2.关注公众号前端那些趣事,陪你聊聊前端的趣事

3.文章收录在Github frontendThings 感谢Star✨

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



收起阅读 »

Esbuild 为什么那么快

Esbuild 是什么Esbuild 是一个非常新的模块打包工具,它提供了与 Webpack、Rollup、Parcel 等工具相似的资源打包能力,却有着高的离谱的性能优势:下面展开细讲。为什么快语言优势大多数前端打包工具都是基于 JavaScript 实现的...
继续阅读 »

Esbuild 是什么

Esbuild 是一个非常新的模块打包工具,它提供了与 Webpack、Rollup、Parcel 等工具相似的资源打包能力,却有着高的离谱的性能优势:

从上到下,耗时逐步上升达到数百倍的差异,这个巨大的性能优势使得 Esbuild 在一众基于 Node 的构建工具中迅速蹿红,特别是 Vite 2.0 宣布使用 Esbuild 预构建依赖后,前端社区关于它的讨论热度逐渐上升。

那么问题来了,这是怎么做到的?我翻阅了很多资料后,总结了一些关键因素:

下面展开细讲。

为什么快

语言优势

大多数前端打包工具都是基于 JavaScript 实现的,而 Esbuild 则选择使用 Go 语言编写,两种语言各自有其擅长的场景,但是在资源打包这种 CPU 密集场景下,Go 更具性能优势,差距有多大呢?比如计算 50 次斐波那契数列,JS 版本:

function fibonacci(num) {
if (num < 2) {
return 1
}
return fibonacci(num - 1) + fibonacci(num - 2)
}

(() => {
let cursor = 0;
while (cursor < 50) {
fibonacci(cursor++)
}
})()

Go 版本:

package main

func fibonacci(num int) int{
if num<2{
return 1
}

return fibonacci(num-1) + fibonacci(num-2)
}

func main(){
for i := 0; i<50; i++{
fibonacci(i)
}
}

JavaScript 版本执行耗时大约为 332.58s,Go 版本执行耗时大约为 147.08s,两者相差约 1.25 倍,这个简单实验并不能精确定量两种语言的性能差别,但感官上还是能明显感知 Go 语言在 CPU 密集场景下会有更好的性能表现。

归根到底,虽然现代 JS 引擎与10年前相比有巨大的提升,但 JavaScript 本质上依然是一门解释型语言,JavaScript 程序每次执行都需要先由解释器一边将源码翻译成机器语言,一边调度执行;而 Go 是一种编译型语言,在编译阶段就已经将源码转译为机器码,启动时只需要直接执行这些机器码即可。也就意味着,Go 语言编写的程序比 JavaScript 少了一个动态解释的过程。

这种语言层面的差异在打包场景下特别突出,说的夸张一点,JavaScript 运行时还在解释代码的时候,Esbuild 已经在解析用户代码;JavaScript 运行时解释完代码刚准备启动的时候,Esbuild 可能已经打包完毕,退出进程了!

所以在编译运行层面,Go 前置了源码编译过程,相对 JavaScript 边解释边运行的方式有更高的执行性能。

多线程优势

Go 天生具有多线程运行能力,而 JavaScript 本质上是一门单线程语言,直到引入 WebWorker 规范之后才有可能在浏览器、Node 中实现多线程操作。

我曾经研读过 Rollup、Webpack 的代码,就我熟知的范围内两者均未使用 WebWorker 提供的多线程能力。反观 Esbuild,它最核心的卖点就是性能,它的实现算法经过非常精心的设计,尽可能饱和地使用各个 CPU 核,特别是打包过程的解析、代码生成阶段已经实现完全并行处理。

除了 CPU 指令运行层面的并行外,Go 语言多个线程之间还能共享相同的内存空间,而 JavaScript 的每个线程都有自己独有的内存堆。这意味着 Go 中多个处理单元,例如解析资源 A 的线程,可以直接读取资源 B 线程的运行结果,而在 JavaScript 中相同的操作需要调用通讯接口 woker.postMessage 在线程间复制数据。

所以在运行时层面,Go 拥有天然的多线程能力,更高效的内存使用率,也就意味着更高的运行性能。

节制

对,没错,节制!

Esbuild 并不是另一个 Webpack,它仅仅提供了构建一个现代 Web 应用所需的最小功能集合,未来也不会大规模加入我们业已熟悉的各类构建特性。最新版本 Esbuild 的主要功能特性有:

  • 支持 js、ts、jsx、css、json、文本、图片等资源
  • 增量更新
  • Sourcemap
  • 开发服务器支持
  • 代码压缩
  • Code split
  • Tree shaking
  • 插件支持

可以看到,这份列表中支持的资源类型、工程化特性非常少,甚至并不足以支撑一个大型项目的开发需求。在这之外,官网明确声明未来没有计划支持如下特性:

  • ElmSvelteVueAngular 等代码文件格式
  • Ts 类型检查
  • AST 相关操作 API
  • Hot Module Replace
  • Module Federation

而且,Esbuild 所设计的插件系统也无意覆盖以上这些场景,这就意味着第三方开发者无法通过插件这种无侵入的方式实现上述功能,emmm,可以预见未来可能会出现很多魔改版本。

Esbuild 只解决一部分问题,所以它的架构复杂度相对较小,相对地编码复杂度也会小很多,相对于 Webpack、Rollup 等大一统的工具,也自然更容易把性能做到极致。节制的功能设计还能带来另外一个好处:完全为性能定制的各种附加工具。

定制

回顾一下,在 Webpack、Rollup 这类工具中,我们不得不使用很多额外的第三方插件来解决各种工程需求,比如:

  • 使用 babel 实现 ES 版本转译
  • 使用 eslint 实现代码检查
  • 使用 TSC 实现 ts 代码转译与代码检查
  • 使用 less、stylus、sass 等 css 预处理工具

我们已经完全习惯了这种方式,甚至觉得事情就应该是这样的,大多数人可能根本没有意识到事情可以有另一种解决方案。Esbuild 起了个头,选择完全!完全重写整套编译流程所需要用到的所有工具!这意味着它需要重写 js、ts、jsx、json 等资源文件的加载、解析、链接、代码生成逻辑。

开发成本很高,而且可能被动陷入封闭的风险,但收益也是巨大的,它可以一路贯彻原则,以性能为最高优先级定制编译的各个阶段,比如说:

  • 重写 ts 转译工具,完全抛弃 ts 类型检查,只做代码转换
  • 大多数打包工具把词法分析、语法分析、符号声明等步骤拆解为多个高内聚低耦合的处理单元,各个模块职责分明,可读性、可维护性较高。而 Esbuild 则坚持性能第一原则,不惜采用反直觉的设计模式,将多个处理算法混合在一起降低编译过程数据流转所带来的性能损耗
  • 一致的数据结构,以及衍生出的高效缓存策略,下一节细讲

这种深度定制一方面降低了设计成本,能够保持编译链条的架构一致性;一方面能够贯彻性能第一的原则,确保每个环节以及环节之间交互性能的最优。虽然伴随着功能、可读性、可维护性层面的的牺牲,但在编译性能方面几乎做到了极致。

结构一致性

上一节我们讲到 Esbuild 选择重写包括 js、ts、jsx、css 等语言在内的转译工具,所以它更能保证源代码在编译步骤之间的结构一致性,比如在 Webpack 中使用 babel-loader 处理 JavaScript 代码时,可能需要经过多次数据转换:

  • Webpack 读入源码,此时为字符串形式
  • Babel 解析源码,转换为 AST 形式
  • Babel 将源码 AST 转换为低版本 AST
  • Babel 将低版本 AST generate 为低版本源码,字符串形式
  • Webpack 解析低版本源码
  • Webpack 将多个模块打包成最终产物

源码需要经历 string => AST => AST => string => AST => string ,在字符串与 AST 之间反复横跳。

而 Esbuild 重写大多数转译工具之后,能够在多个编译阶段共用相似的 AST 结构,尽可能减少字符串到 AST 的结构转换,提升内存使用效率。

总结

单纯从编译性能的维度看,Esbuild 确实完胜世面上所有打包框架,差距甚至能在百倍之大:

耗时性能差异速度产物大小
esbuild0.11s1x1198.5 kloc/s0.97mb
esbuild (1 thread)0.40s4x329.6 kloc/s0.97mb
webpack 419.14s174x6.9 kloc/s1.26mb
parcel 122.41s204x5.9 kloc/s1.56mb
webpack 525.61s233x5.1 kloc/s1.26mb
parcel 231.39s285x4.2 kloc/s0.97mb

但这是有代价的,刨除语言层面的天然优势外,在功能层面它直接放弃对 less、stylus、sass、vue、angular 等资源的支持,放弃 MF、HMR、TS 类型检查等功能,正如作者所说:

This will involve saying "no" to requests for adding major features to esbuild itself. I don't think esbuild should become an all-in-one solution for all frontend needs\!

在我看来,Esbuild 当下与未来都不能替代 Webpack,它不适合直接用于生产环境,而更适合作为一种偏底层的模块打包工具,需要在它的基础上二次封装,扩展出一套既兼顾性能又有完备工程化能力的工具链,例如 SnowpackViteSvelteKitRemix Run 等。

总的来说,Esbuild 提供了一种新的设计思路,值得学习了解,但对大多数业务场景还不适合直接投入生产使用。

原文:https://segmentfault.com/a/1190000040243093
收起阅读 »

Event Loop 和 JS 引擎、渲染引擎的关系

安卓就是这样的架构,在主线程里面完成 ui 的更新,事件的绑定,其他逻辑可以放到别的线程,然后完成以后在消息队列中放一个消息,主线程不断循环的取消息来执行。 electron ui 架构 开发过 electron 应用的同学会知道,electron 中分为了...
继续阅读 »


安卓就是这样的架构,在主线程里面完成 ui 的更新,事件的绑定,其他逻辑可以放到别的线程,然后完成以后在消息队列中放一个消息,主线程不断循环的取消息来执行。



electron ui 架构


开发过 electron 应用的同学会知道,electron 中分为了主进程和渲染进程,window 相关的操作只能在主线程,由渲染进程向主进程发消息。


image.png


从上面两个案例我们可以总结出,所有的 ui 系统的设计,如果使用了多线程(进程)的架构,基本都是 ui 只能在一个线程(进程)中操作,由别的线程(进程)来发消息到这边来更新,如果多个线程,会有一个消息队列和 looper。消息队列的生产者是各个子线程(进程),消费者是主线程(进程)。


而且,不只是 ui 架构是这样,后端也大量运用了消息队列的概念,


后端的消息队列



后端因为不同服务负载能力不一样,所以中间会加一个消息队列来异步处理消息,和前端客户端的 ui 架构不同的是,后端的消息队列中间件会有多个消费者、多个队列,而 ui 系统的消息队列只有一个队列,一个消费者(主线程、主进程)


在一个线程做 ui 操作,其他线程做逻辑计算的架构很普遍,会需要一个消息队列来做异步消息处理。 网页中后来有了 web worker,也是这种架构的实现,但是最开始并不是这样的。


单线程


因为 javascript 最开始只是被设计用来做表单处理,那么就不会有特别大的计算量,就没有采用多线程架构,而是在一个线程内进行 dom 操作和逻辑计算,渲染和 JS 执行相互阻塞。(后来加了 web worker,但不是主流)


我们知道,JS 引擎只知道执行 JS,渲染引擎只知道渲染,它们两个并不知道彼此,该怎么配合呢?


答案就是 event loop。


宿主环境


JS 引擎并不提供 event loop(可能很多同学以为 event loop 是 JS 引擎提供的,其实不是),它是宿主环境为了集合渲染和 JS 执行,也为了处理 JS 执行时的高优先级任务而设计的机制。


宿主环境有浏览器、node、跨端引擎等,不同的宿主环境有一些区别:


注入的全局 api 不同


  • node 会注入一些全局的 require api,同时提供 fs、os 等内置模块

  • 浏览器会注入 w3c 标准的 api

  • 跨端引擎会注入设备的 api,同时会注入一套操作 ui 的 api(可能是对标 w3c 的 api 也可能不是)


event loop 的实现不同

上文说过,event loop 是宿主环境提供了,不同的宿主环境有不同的需要调度的任务,所以也会有不同的设计:



  • 浏览器里面主要是调度渲染和 JS 执行,还有 worker

  • node 里面主要是调度各种 io

  • 跨端引擎也是调度渲染和 JS 执行


这里我们只关心浏览器里面的 event loop。


浏览器的 event loop


check

浏览器里面执行一个 JS 任务就是一个 event loop,每个 loop 结束会检查下是否需要渲染,是否需要处理 worker 的消息,通过这种每次 loop 结束都 check 的方式来综合渲染、JS 执行、worker 等,让它们都能在一个线程内得到执行(渲染其实是在别的线程,但是会和 JS 线程相互阻塞)。



这样就解决了渲染、JS 执行、worker 这三者的调度问题。


但是这样有没有问题?


我们会在任务队列中不断的放新的任务,这样如果有更高优的任务是不是要等所有任务都执行完才能被执行。如果是“急事”呢?


所以这样还不行,要给 event loop 加上“急事”处理的快速通道,这就是微任务 micro tasks。


micro tasks


任务还是每次取一个执行,执行完检查下要不要渲染,处理下 worker 消息,但是也给高优先级的“急事”加入了插队机制,会在执行完任务之后,把所有的急事(micro task)全部处理完。


这样,event loop 貌似就挺完美的了,每次都会检查是否要渲染,也能更快的处理 JS 的“急事”。


requestAnimationFrame


JS 执行完,开始渲染之前会有一个生命周期,就是 requestAnimationFrame,在这里面做一些计算最合适了,能保证一定是在渲染之前做的计算。


image.png


如果有人问 requestAnimationFrame 是宏任务还是微任务,就可以告诉他:requestAnimationFrame 是每次 loop 结束发现需要渲染,在渲染之前执行的一个回调函数,不是宏微任务。


event loop 的问题


上文聊过,虽然后面加入了 worker,但是主流的方式还是 JS 计算和渲染相互阻塞,这样就导致了一个问题:


每一帧的计算和渲染是有固定频率的,如果 JS 执行时间过长,超过了一帧的刷新时间,那么就会导致渲染延迟,甚至掉帧(因为上一帧的数据还没渲染到界面就被覆盖成新的数据了),给用户的感受就是“界面卡了”。


什么情况会导致帧刷新拖延甚至帧数据被覆盖(丢帧)呢?每个 loop 在 check 渲染之前的每一个阶段都有可能,也就是 task、microtask、requestAnimationFrame、requestIdleCallback 都有可能导致阻塞了 check,这样等到了 check 的时候发现要渲染了,再去渲染的时候就晚了。


所以主线程 JS 代码不要做太多的计算(不像安卓会很自然的起一个线程来做),要做拆分,这也是为啥 ui 框架要做计算的 fiber 化,就是因为处理交互的时候,不能让计算阻塞了渲染,要递归改循环,通过链表来做计算的暂停恢复。


除了 JS 代码本身要注意之外,如果浏览器能够提供 API 就是在每帧间隔来执行,那样岂不是就不会阻塞了,所以后来有了 requestIdeCallback。


requestIdleCallback


requestIdleCallback 会在每次 check 结束发现距离下一帧的刷新还有时间,就执行一下这个。如果时间不够,就下一帧再说。


如果每一帧都没时间呢,那也不行,所以提供了 timeout 的参数可以指定最长的等待时间,如果一直没时间执行这个逻辑,那么就算拖延了帧渲染也要执行。



这个 api 对于前端框架来说太需要了,框架就是希望计算不阻塞渲染,也就是在每一帧的间隔时间(idle时间)做计算,但是这个 api 毕竟是最近加的,有兼容问题,所以 react 自己实现了类似 idle callback 的fiber 机制,在执行逻辑之前判断一下离下一帧刷新还有多久,来判断是否执行逻辑。


总结


总之,浏览器里有 JS 引擎做 JS 代码的执行,利用注入的浏览器 API 完成功能,有渲染引擎做页面渲染,两者都比较纯粹,需要一个调度的方式,就是 event loop。


event loop 实现了 task 和 急事处理机制 microtask,而且每次 loop 结束会 check 是否要渲染,渲染之前会有 requestAnimationFrames 生命周期。


帧刷新不能被拖延否则会卡顿甚至掉帧,所以就需要 JS 代码里面不要做过多计算,于是有了 requestIdleCallback 的 api,希望在每次 check 完发现还有时间就执行,没时间就不执行(这个deadline的时间也作为参数让 js 代码自己判断),为了避免一直没时间,还提供了 timeout 参数强制执行。


防止计算时间过长导致渲染掉帧是 ui 框架一直关注的问题,就是怎么不阻塞渲染,让逻辑能够拆成帧间隔时间内能够执行完的小块。浏览器提供了 idelcallback 的 api,很多 ui 框架也通过递归改循环然后记录状态等方式实现了计算量的拆分,目的只有一个:loop 内的逻辑执行不能阻塞 check,也就是不能阻塞渲染引擎做帧刷新。所以不管是 JS 代码宏微任务、 requestAnimationCallback、requestIdleCallback 都不能计算时间太长。这个问题是前端开发的持续性阵痛。


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

收起阅读 »

浏览器原理 之 页面渲染的原理和性能优化篇

001 浏览器的底层渲染页面篇 浏览器中的5个进程 浏览器在获取服务器的资源后将 html 解析成 DOM 树,CSS 计算成 CSSOM 树,将两者合成 render tree。具体如下浏览器根据 render tree 布局生成一个页面。需要理解的...
继续阅读 »

001 浏览器的底层渲染页面篇



浏览器中的5个进程



浏览器进程.jpg



浏览器在获取服务器的资源后将 html 解析成 DOM 树,CSS 计算成 CSSOM 树,将两者合成 render tree。具体如下浏览器根据 render tree 布局生成一个页面。需要理解的是浏览器从服务器获取回来的资源是一个个的字节码3C 6F 6E 62 ....等,浏览器会按照一套规范W3C将字节码最后解析一个个的代码字符串才成为我们看到的代码



浏览器加载资源的机制



  • 浏览器会开辟一个 GUI 渲染线程,自上而下执行代码,专门用于渲染渲染页面的线程。


遇到 CSS 资源



  • 遇到 <style> 内联标签会交给 GUI 渲染线程解析,但是遇到 <link> 标签会异步处理,浏览器会开辟一个 HTTP 请求处理的线程,GUI 渲染线程继续往下执行

  • 如果遇到@import 时,也会开辟一个新的 HTTP 请求线程处理,由于 @import 是同步的 GUI 渲染线程会阻塞等待请求的结果。



需要注意 chrome 中,同一个源下,最多同时开辟 6-7 和 HTTP 请求线程。



遇到 JS 资源


GUI渲染遇到script.jpg



最底部的线表示 GUI 线程的过程,渲染线程遇到不同情况下的script资源,有不同的处理。




  • 遇到 <script></script> 资源,默认是同步的。 此时 GUI 渲染线程会阻塞。等待 JS 渲染线程渲染结束后,GUI 线程才会继续渲染。

  • 如果遇到 <script async></script> 那么资源是异步的 async,浏览器也会开辟一个 HTTP 请求线程加载资源,这时 GUI 渲染线程会继续向下渲染,请求的资源回来后 JS 渲染线程开始执行,GUI 线程再次被阻塞。

  • 如果遇到 <script defer></script> 和 async 类似都会开辟一个新的HTTP线程,GUI 继续渲染。和 async 不一样的地方在于,defer 请求回来的资源需要等待 GUI 同步的代码执行结束后才执行 defer 请求回来的代码。



async 不存在资源的依赖关系先请求回来的先执行。defer 需要等待所有的资源请求回来以后,按照导入的顺序/依赖关系依次执行。



图片或音频



  • 遇到 <img/> 异步,也会开辟一个新的 HTTP 线程请求资源。GUI 继续渲染,当 GUI 渲染结束后,才会处理请求的资源。


需要注意的是:假设某些资源加载很慢,浏览器会忽略这些资源接着渲染后面的代码,在chrome浏览器中会先使用预加载器html-repload-scanner先扫描节点中的 src,link等先进行预加载,避免了资源加载的时间


浏览解析资源的机制



  • 浏览器是怎样解析加载回来的资源文件的? 页面自上而下渲染时会确定一个 DOM树CSSOM树,最后 DOM树CSSOM树 会被合并成 render 树,这些所谓的树其实都是js对象,用js对象来表示节点,样式,节点和样式之间的关系。


DOM 树



所谓的 DOM 树是确定好节点之间的父子、兄弟关系。这是 GUI 渲染线程自上而下渲染结束后生成的,等到 CSS 资源请求回来以后会生成 CSSOM 样式树。



DOM树.jpg


CSSOM 树



CSSOM(CSS Object Model), CSS 资源加载回来以后会被 GUI 渲染成 样式树



样式树.jpg


Render tree 渲染树



浏览器根据 Render tree 渲染页面需要经历下面几个步骤。注意 display:node 的节点不会被渲染到 render tree 中



renderTree.jpg



  • layout 布局,根据渲染树 计算出节点在设备中的位置和大小

  • 分层处理。按照层级定位分层处理

  • painting 绘制页面


layout2.jpg



上面的图形就是浏览器分成处理后的显示效果



002 浏览器的性能优化篇



前端浏览器的性能优化,可以从CRP: 关键渲染路径入手



DOM Tree



  • 减少 DOM 的层级嵌套

  • 不要使用被标准标签


CSSOM



  • 尽量不要使用 @import,会阻碍GUI渲染线程。

  • CSS 代码量少可以使用内嵌式的style标签,减少请求。

  • 减少使用link,可以减少 HTTP 的请求数量。

  • CSS 选择器链尽可能短,因为CSS选择器的渲染时从右到左的。

  • 将写入的 link 请求放入到<head></head> 内部,一开始就可以请求资源,GUI同时渲染。


其他资源



  • <script></script> 中的同步 js 资源尽可能的放入到页面的末尾,防止阻碍GUI的渲染。如果遇到 <script async/defer></script> 的异步资源,GUI 渲染不会中断,但是JS资源请求回来以后会中断 GUI 的渲染。

  • <img /> 资源使用懒加载,懒加载:第一次加载页面时不要加载图片,因为图片也会占据 HTTP 的数量。还可以使用图片 base64,代表图片。


003 回流和重绘篇



layout 阶段就是页面的回流期,painting 就是重绘阶段。第一次加载页面时必有一次回流和重绘。




  • 浏览器渲染页面的流程



浏览器会先把 HTML 解析成 DOM树 计算 DOM 结构;然后加载 CSS 解析成 CSSOM;最后将 DOM 和 CSSOM 合并生成渲染树 Render Tree,浏览器根据页面计算 layout(重排阶段);最后浏览器按照 render tree 绘制(painting,重绘阶段)页面。



重排(DOM 回流)



重排是指 render tree 某些 DOM 大小和位置发生了变化(页面的布局和几何信息发生了变化),浏览器重新渲染 DOM 的这个过程就是重排(DOM 回流),重排会消耗页面很大的性能,这也是虚拟 DOM 被引入的原因。



发生重排的情况



  • 第一次页面计算 layout 的阶段

  • 添加或删除DOM节点,改变了 render tree

  • 元素的位置,元素的字体大小等也会导致 DOM 的回流

  • 节点的几何属性改变,比如width, height, border, padding,margin等被改变

  • 查找盒子属性的 offsetWidth、offsetHeight、client、scroll等,浏览器为了得到这些属性会重排操作。

  • 框架中 v-if 操作也会导致回流的发生。

  • 等等


一道小题,问下面的代码浏览器重排了几次(chrome新版浏览器为主)?


box.style.width = "100px";
box.style.width = "100px";
box.style.position = "relative";
复制代码


你可能会觉得是3次,但是在当代浏览器中,浏览器会为上面的样式代码开辟一个渲染队列,将所有的渲染代码放入到队列里面,最后一次更新,所以重排的次数是1次。 问下面的代码会导致几次重排



box.style.width = "100px";
box.style.width = "100px";
box.offsetWidth;
box.style.position = "relative";
复制代码


答案是2次,因为 offsetWidth 会导致渲染队列的刷新,才可以获取准确的 offsetWidth 值。最后 position 导致元素的位子发生改变也会触发一次回流。所以总共有2次。



重绘



重绘是指 页面的样式发生了改变但是 DOM 结构/布局没有发生改变。比如颜色发生了变化,浏览器就会对需要的颜色进行重新绘制。



发生重绘的情况



  • 第一次页面 painting 绘制的阶段

  • 元素颜色的 color 发生改变


直接合成



如果我们更改了一个不影响布局和绘制的属性,浏览器的渲染引擎会跳过重排和重绘的阶段,直接合成




  • 比如我们使用了CSS 的 transform 属性,浏览器的可以师姐合成动画效果。


重排一定会引发重绘,但是重绘不一定会导致重排


重排 (DOM回流)和重绘吗?说一下区别



思路:先讲述浏览器的渲染机制->重排和重绘的概念->怎么减少重排和重绘。。。



区别



重排会导致 DOM结构 发生改变,浏览器需要重新渲染布局生成页面,但是重绘不会引发 DOM 的改变只是样式上的改变,前者的会消耗很大的性能。



如何减少重排和重绘





    1. 避免使用 table 布局,因为 table 布局计算的时间比较长耗性能;





    1. 样式集中改变,避免频繁使用 style,而是采用修改 class 的方式。





    1. 避免频繁操作 DOM,使用vue/react。





    1. 样式的分离读写。设置样式style和读取样式的offset等分离开,也可以减少回流次数。





    1. 将动画效果设计在文档流之上即 position 属性的 absolutefixed 上。使用 GPU 加速合成。




参考


《浏览器工作原理与实践》


Render Tree页面渲染


结束


浏览器原理篇:本地存储和浏览器缓存


Vue 原理篇:Vue高频原理详细解答


webpack原理篇: 编写loader和plugin


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

收起阅读 »

这些node开源工具你值得拥有

前言:文章的灵感来源于,社群中某大佬分享一个自己耗时数月维护的github项目 awesome-nodejs 。或许你跟我一样会有一个疑惑,github上其实已经有个同类型的awesome-nodejs库且还高达41k⭐,重新维护一个新的意义何在? 当你深入对...
继续阅读 »

前言:文章的灵感来源于,社群中某大佬分享一个自己耗时数月维护的github项目 awesome-nodejs 。或许你跟我一样会有一个疑惑,github上其实已经有个同类型的awesome-nodejs库且还高达41k⭐,重新维护一个新的意义何在? 当你深入对比后,本质上还是有差别的,一个是分类体系粒度更细,其次是对中文更友好的翻译维护,也包括了对国内一些优秀的开源库的收录。最后我个人认为通过自己梳理,也能更好地做复盘和总结



image.png


通过阅读 awesome-nodejs 库的收录,我抽取其中一些应用场景比较多的分类,通过分类涉及的应用场景跟大家分享工具


1.Git


1.1 应用场景1: 要实现git提交前 eslint 校验和 commit 信息的规范校验?


可以使用以下工具:



  • husky - 现代化的本地Git钩子使操作更加轻松

  • pre-commit - 自动在您的git储存库中安装git pre-commit脚本,该脚本在pre-commit上运行您的npm test。

  • yorkie 尤大改写的yorkie,yorkie实际是fork husky,让 Git 钩子变得简单(在 vue-cli 3x 中使用)


1.2 应用场景2: 如何通过node拉取git仓库?(可用于开发脚手架)


可以使用以下工具:



1.3 应用场景3: 如何在终端看git 流程图?


可以使用以下工具:



  • gitgraph - 在 Terminal 绘制 git 流程图(支持浏览器、React)。


1.4 其他



2.环境


2.1 应用场景1: 如何根据不同环境写入不同环境变量?


可以使用以下工具:



  • cross-env - 跨平台环境脚本的设置,你可以通过一个简单的命令(设置环境变量)而不用担心设置或者使用环境变量的平台。

  • dotenv - 从 .env文件 加载用于nodejs项目的环境变量。

  • vue-cli --mode - 可以通过传递 --mode 选项参数为命令行覆写默认的模式


3.NPM


3.1 应用场景1: 如何切换不同npm源?


可以使用以下工具:



  • nrm - 快速切换npm注册服务商,如npm、cnpm、nj、taobao等,也可以切换到内部的npm源

  • pnpm - 可比yarn,npm 更节省了大量与项目和依赖成比例的硬盘空间


3.2 应用场景2: 如何读取package.json信息?


可以使用以下工具:



3.3 应用场景3:如何查看当前package.json依赖允许的更新的版本


可以使用以下工具:



image.png


3.4 应用场景4:如何同时运行多个npm脚本



通常我们要运行多脚本或许会是这样npm run build:css && npm run build:js ,设置会更长通过&来拼接



可以使用以下工具:



  • npm-run-all - 命令行工具,同时运行多个npm脚本(并行或串行)


npm-run-all提供了三个命令,分别是 npm-run-all run-s run-p,后两者是 npm-run-all 带参数的简写,分别对应串行和并行。而且还支持匹配分隔符,可以简化script配置


或者使用



  • concurrently - 并行执行命令,类似 npm run watch-js & npm run watch-less但更优。(不过它只能并行)


3.5 应用场景5:如何检查NPM模块未使用的依赖。


可以使用以下工具:



  • depcheck - 检查你的NPM模块未使用的依赖。


image.png


3.6 其他:



  • npminstall - 使 npm install 更快更容易,cnpm默认使用

  • semver - NPM使用的JavaScript语义化版本号解析器。


关于npm包在线查询,推荐一个利器 npm.devtool.tech


image.png


4.文档生成


4.1 应用场景1:如何自动生成api文档?



  • docsify - API文档生成器。

  • jsdoc - API文档生成器,类似于JavaDoc或PHPDoc。


5.日志工具


5.1 应用场景1:如何实现日志分类?



  • log4js-nodey - 不同于Java log4j的日志记录库。

  • consola - 优雅的Node.js和浏览器日志记录库。

  • winston - 多传输异步日志记录库(古老)


6.命令行工具


6.1 应用场景1: 如何解析命令行输入?



我们第一印象会想到的是process.argv,那么还有什么工具可以解析吗?



可以使用以下工具:



  • minimist - 命令行参数解析引擎

  • arg - 简单的参数解析

  • nopt - Node/npm 参数解析


6.2 应用场景2:如何让用户能与命令行进行交互?


image.png


可以使用以下工具:



  • Inquirer.js - 通用可交互命令行工具集合。

  • prompts - 轻量、美观、用户友好的交互式命令行提示。

  • Enquirer - 用户友好、直观且易于创建的时尚CLI提示。


6.3 应用场景3: 如何在命令行中显示进度条?


image.png
可以使用以下工具:



6.4 应用场景4: 如何在命令行执行多任务?


image.png


可以使用以下工具:



  • listr - 命令行任务列表。


6.5 应用场景5: 如何给命令行“锦上添花”?


image.png


可以使用以下工具:



  • chalk - 命令行字符串样式美化工具。

  • ora - 优雅的命令行loading效果。

  • colors.js - 获取Node.js控制台的颜色。

  • qrcode-terminal - 命令行中显示二维码。

  • treeify - 将javascript对象漂亮地打印为树。

  • kleur - 最快的Node.js库,使用ANSI颜色格式化命令行文本。



感兴趣的童鞋可以参考树酱的从0到1开发简易脚手架,其中有实践部分工具



7.加解密



一般为了项目安全性考虑,我们通常会对账号密码进行加密,一般会通过MD5、AES、SHA1、SM,那开源社区有哪些库可以方便我们使用?



可以使用以下工具:



  • crypto-js - JavaScript加密标准库。支持算法最多

  • node-rsa - Node.js版Bcrypt。

  • node-md5 - 一个JavaScript函数,用于使用MD5对消息进行哈希处理。

  • aes-js - AES的纯JavaScript实现。

  • sm-crypto - 国密sm2, sm3, sm4的JavaScript实现。

  • sha.js - 使用纯JavaScript中的流式SHA哈希。


8.静态网站生成 & 博客



一键生成网站不香吗~ 基于node体系快速搭建自己的博客网站,你值得拥有,也可以作为组件库文档展示



image.png


可以使用以下工具:



  • hexo - 使用Node.js的快速,简单,强大的博客框架。

  • vuepress - 极简的Vue静态网站生成工具。(基于nuxt SSR)

  • netlify-cms - 基于Git的静态网站生成工具。

  • vitepress - Vite & Vue.js静态网站生成工具。


9.数据校验工具



数据校验,离我们最近的就是表单数据的校验,在平时使用的组件库比如element、iview等我们会看到使用了一个开源的校验工具async-validator , 那还有其他吗?



可以使用以下工具:



  • validator.js - 字符串校验库。

  • joi - 基于JavaScript对象的对象模式描述语言和验证器。

  • async-validator - 异步校验。

  • ajv - 最快的JSON Schema验证器

  • superstruct - 用简单和可组合的方式在JavaScript和TypeScript中校验数据。


10.解析工具


10.1应用场景1: 如何解析markdown?


可以使用以下工具:



  • marked - Markdown解析器和编译器,专为提高速度而设计。

  • remark - Markdown处理工具。

  • markdown-it -支持100%通用Markdown标签解析的扩展&语法插件。


10.2应用场景2: 如何解析csv?


可以使用以下工具:



  • PapaParse - 快速而强大的 CSV(分隔文本)解析器,可以优雅地处理大文件和格式错误的输入。

  • node-csv - 具有简单api的全功能CSV解析器,并针对大型数据集进行了测试。

  • csv-parser -旨在比其他任何人都快的流式CSV解析器。


10.3应用场景3: 如何解析xml?


可以使用以下工具:



最后



如果你喜欢这个库,也给作者huaize2020 一个star 仓库地址:awesome-nodejs



昨天看到一段话想分享给大家


对于一个研发测的日常:



  • 1.开始工作的第一件事,规划今日的工作内容安排 (建议有清晰的ToDolist,且按优先级排序)

  • 2.确认工作量与上下游关联风险(如依赖他人的,能否按时提供出来);有任何风险,尽早暴露

  • 3.注意时间成本、不是任何事情都是值得你用尽所有时间去做的,分清主次关系

  • 4.协作任务,明确边界责任,不要出现谁都不管,完成任务后及时同步给相关人

  • 5.及时总结经验,沉淀技术产出实现能力复用,同类型任务,不用从零开始,避免重复工作


往期热门文章📖:



  • 链接:https://juejin.cn/post/6972124481053523999

收起阅读 »

NodeJS使用Koa框架开发对接QQ登陆功能

开发准备 注册开发者账号 首先我们需要先去腾讯开发者平台认证注册成为个人开发者 输入网址:https://open.tencent.com/ 然后 点击 QQ开放平台——然后点击顶部的 应用管理会提示你登陆,使用自己的QQ账号登陆后,如果是新用户会提示你注...
继续阅读 »

开发准备



  • 注册开发者账号


首先我们需要先去腾讯开发者平台认证注册成为个人开发者 输入网址:https://open.tencent.com/ 然后 点击 QQ开放平台——然后点击顶部的 应用管理会提示你登陆,使用自己的QQ账号登陆后,如果是新用户会提示你注册成为开发者,这里我已经注册并认证成功了,所以我就可以直接创建应用了,我这里是网站使用的,所以我就创建的网站Web'应用,APP小程序申请移动端的进行了 下面看我的截图
image.png


image.png


image.png


image.png


image.png


到这一步基本上就创建完成了一个应用,会有7天的等待,官方会审核检查你填写的信息是否准确,如果都是真实有效的用不了几天审核通过了,就申请到了appid和appkey的。



  • 接入QQ登录时,网站需要不停的和Qzone进行交互,发送请求和接受响应。



    1. 对于PC网站:请在你的服务器上ping graph.qq.com ,保证连接畅通。



  • 2.移动应用无需此步骤


放置“QQ登录”按钮_OAuth2.0


image.png


这里说一下我碰到的几个坑



  1. 网站名称我没有填写我到时候域名备案写的网站名称,于是出了一次错误被驳回

  2. 网站的备案号格式:(地区)蜀ICP备XXXXX号 我填写的格式不正确又一次被驳回

  3. 就是大家可能都比较容易犯错误的,回调地址的填写,刚开始我一直卡这里,总共的填写后面我也会反复给大家强调,在这里就是Api接口地址可以这样去理解,(目前我这样理解,有更好意见的欢迎反馈评论给我) 如我的网址是:lovehaha.cn 我的api接口是 lovehaha.cn/test 那么我在后端写了一个专门处理腾讯qq返回的数据的路由,是 /qqauthor 那么我的回调地址就是: lovehaha/test/qqauthor

  4. 审核的时候,网站需要可以访问,同时需要查看QQ图标的位置是否正确,应在登陆页或首页,同时回调地址的路由可以正常收到腾讯返回的数据。


代码部署


前面都顺顺利利成功了后,需要到开发者平台应用管理哪里先填写个QQ调试账号然后就开始我们的代码配置部署吧!


后端使用的是Node的Koa框架 框架的安装配置很简单(首先肯定需要大家有node环境 我这里是v14.16.1版本的,安装了Node 版本大于10还是几就自带npm了)


命令:



  • npm install koa-generator -g (全局安装koa-generator是koa框架的生成器)

  • koa 文件名称 创建项目

  • npm install 安装依赖包

  • npm run dev 就可以运行了默认应该是3000端口访问


在这里我就简单介绍一下,下面介绍我的后端代码处理逻辑


整体逻辑:



  • 获取Authorization Code

  • 通过Authorization Code 获取 Access Token (Code ————> 换 Token)

  • 通过Access Token 获取 用户的Openid

  • 最后通过获取的 Token 和 Openid 获取用户的信息


PS:(可选)权限自动续期,获取Access Token
Access_Token的有效期默认是3个月,过期后需要用户重新授权才能获得新的Access_Token。本步骤可以实现授权自动续期,避免要求用户再次授权的操作,提升用户体验。(官网文档有教程,我这里没用)

/**
* QQ登陆授权判断
* code 是前端点击QQ登陆按钮图标然后请求,然后请求这个回调地址 返回的
* 我这里就可以取到了
*/
router.get('/qqauthor', async (ctx, next) => {
const { code } = ctx.request.query
console.log("code", code) // 打印查看是否获取到
let userinfo
let openid
let item
if (code) {

let token = await QQgetAccessToken(code) // 获取token 函数 返回 token 并存储
console.log('返回的token',token)
openid = await getOpenID(token) // 获取 Openid 函数 返回 Openid 并存储
console.log('返回的openid', openid)
if (openid && token) {
userinfo = await QQgetUserInfO(token, openid) // 如果都获取到了,获取用户信息
console.log("返回的结果", userinfo)
}

}

// 封装:
if (userinfo) {
let obj = {
nickname: userinfo.nickname,
openid: openid,
gender: userinfo.gender === '男' ? 1 : 2,
province: userinfo.province,
city: userinfo.city,
year: userinfo.year,
avatar: userinfo.figureurl_qq_2 ? userinfo.figureurl_qq_2 : userinfo.figureurl_qq_1
}
console.log('封装的obj', obj)
item = await register({ userInfo: obj, way: 'qq' })
/** 从这里到封装 都是改变我获取的用户信息存储到数据库里面,根据数据库的存储,创建新用户,如果有
* 用户我就查询并获取用户的id 然后返回给前端 用户的 id
*/
ctx.state = {
id: item.data.id
}
await ctx.render('login', ctx.state) // 如果获取到用户 id 返回 前端一个页面并携带参数 用户ID
}
})


/**
*
* @param {string} code
* @param {string} appId 密钥
* @param {string} appKey key
* @param {string} state client端的状态值。用于第三方应用防止CSRF攻击,成功授权后回调时会原样带回
* @param {string} redirectUrl (回调地址)
* @returns
*/
async function QQgetAccessToken(code) {
let result
let appId = '申请成功就有了'
let appKey = '申请成功就有了'
let state = '自定义'
let redirectUrl = 'https://xxxxx/qqauthor' // 回调地址是一样的 我这里就是我的获取登陆接口的地址

// 安装了 axios 请求 接口 获取返回的token
await axios({
url:`https://graph.qq.com/oauth2.0/token?grant_type=authorization_code&client_id=${appId}&client_secret=${appKey}&code=${code}&state=${state}&redirect_uri=${redirectUrl}&fmt=json`,
method:'GET'
}).then(res =>{
console.log(res.data)
result = res.data.access_token
// res.data.access_token
}).catch(err => {
console.log(err)
result = err
})

return result
}


/**
* 根据Token获取Openid
* @param {string} accessToken token 令牌
* @returns
*/
async function getOpenID(accessToken) {
let result

// 跟上面差不多就不解释了
await axios({
url: `https://graph.qq.com/oauth2.0/me?access_token=${accessToken}&fmt=json`,
method: 'GET'
}).then(res => {
// 获取到了OpenID
result = res.data.openid
}).catch(err => {
result = err
})

return result
}


/**
* 根据Openid 和 Token 获取用户的信息
* @param {string} accessToken
* @param {string} openid
* @returns
*/
async function QQgetUserInfO (token, openid) {
let result
await axios({
url: `https://graph.qq.com/user/get_user_info?access_token=${token}&oauth_consumer_key=101907569&openid=${openid}`,
method: 'GET'
}).then(res => {
result = res.data
}).catch(err => {
console.log(err)
result = err
})

return result
}

前后端调试

前端我这里使用的是Vue2.0的语法去写的上login.vue 页面代码

<template>
<div class="icon" @click="qqAuth">
<img src="@/static/img/qq48-48.png" alt="" />
<span>QQ账号登陆</span>
</div>
</template>

// 这里我就直接写
<script>
export default {
methods: {
// 简单粗暴
qqAuth () {
const appId = 申请就有了
const redirectUrl = 'https://xxx/qqauthor' // 回调地址 我这里路由是/qqauthor 你的是什么填什么
const state = 'ahh' // 可自定义
const display = '' // 可不传仅PC网站接入时使用。用于展示的样式。
const scope = '' // 请求用户授权时向用户显示的可进行授权的列表。 可不填
const url = `
https://graph.qq.com/oauth2.0/authorize?
response_type=code&
client_id=${appId}&
redirect_uri=${redirectUrl}
&state=${state}
&scope=${scope}
`
window.open(url, '_blank') // 开始访问请求 ,这个时候用户点击登陆,就会跳转到qq登陆界面,
登陆后会返回code 到最开始我们写好的后端接口也就是回调地址哪里,开始操作
},
}
</script>

这个时候用户点击登陆触发qqAuth事件,就会跳转到qq登陆界面,登陆成功后会返回code到最开始我们写好的后端接口也就是回调地址哪里,我们把获取Code操作最后获取用户信息存储并返回一个登陆成功的页面携带用户的ID,这个返回的页面,我写了一个 a 标签 携带着 返回的 用户ID


image.png


我这里的href地址是我自己可以访问并且在线上真实的地址,跳转到了首页,我在这个页面的Mounth 写了一个事件
页面加载的时候获取当前页面的URL如果,并且分割URL字符串,判断是否存在ID,存在ID证明是用户登陆成功返回的,获取当前用户的ID,然后再通过ID请求后端,查找到了用户的数据,缓存,完成整个QQ登陆逻辑功能
image.png


完成开发


开发完成了就上线了,但肯定我的这个是存在更优的解决办法,我记录下来,供大家提供一种思路,希望大家可以喜欢,返回页面是使用的Koa的njk框架,比较方便。


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

Docker 快速部署 Node express 项目

前言 本文章讲解如何简单快速部署 node API 项目。可作为docker入门学习。 Node 项目基于 express+sequelize 框架。 数据库使用 mysql。 Docker 安装 Docker 官方下载地址:docs.docker.com/g...
继续阅读 »

前言


本文章讲解如何简单快速部署 node API 项目。可作为docker入门学习。


Node 项目基于 express+sequelize 框架。


数据库使用 mysql。


Docker 安装


Docker 官方下载地址:docs.docker.com/get-docker


检查 Docker 安装版本:$ docker --version


Dockerfile



Dockerfile 是一个用来构建镜像的文本文件,文本内容包含了一条条构建镜像所需的指令和说明。

Dockerfile 学习地址:http://www.runoob.com/docker/dock…



在项目根目录下编写 Dockerfile 文件:


7231624506430_.pic.jpg


FROM node:12.1    :基于 node:12.1 的定制镜像
LABEL maintainer="kingwyh1993@163.com" :镜像作者
COPY . /home/funnyService :制文件到容器里指定路径
WORKDIR /home/funnyService :指定工作目录为,RUN/CMD 在工作目录运行
ENV NODE_ENV=production :指定环境变量 NODE_ENV 为 production
RUN npm install yarn -g :安装 yarn
RUN yarn install :初始化项目
EXPOSE 3000 :声明端口
CMD [ "node", "src/app.js" ] :运行 node 项目 `$ node src/app.js`

注:CMD 在docker run 时运行。RUN 是在 docker build。
复制代码

docker-compose



Compose 是用于定义和运行多容器 Docker 应用程序的工具。通过 Compose,您可以使用 YML 文件来配置应用程序需要的所有服务。然后,使用一个命令,就可以从 YML 文件配置中创建并启动所有服务。

docker-compose 学习地址:http://www.runoob.com/docker/dock…



在根目录下编写 docker-compose.yml 文件:


7241624516284_.pic.jpg


container_name: 'funny-app'  :指定容器名称 funny-app
build: . :指定构建镜像上下文路径,依据 ./Dockerfile 构建镜像
image: 'funny-node:2.0' :指定容器运行的镜像,名称设置为 funny-node:2.0
ports: :映射端口的标签,格式为 '宿主机端口:容器端口'
- '3000:3000' :这里 node 项目监听3000端口,映射到宿主机3000端口

复制代码

本地调试


项目根目录下执行 $ docker-compose up -d


查看构建的镜像 $ docker images 检查有上述 node、funny-node 镜像则构建成功


查看运行的容器 $ docker ps 检查有 funny-app 容器则启动成功


调试接口 http://127.0.0.1:3000/test/demo 成功:


image.png


服务器部署运行


在服务器 git pull 该项目


执行 $ docker-compose up -d


使用 $ docker images $ docker ps 检查是否构建和启动成功


调试接口 http://服务器ip:3000/test/demo



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

收起阅读 »