注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

写TypeScript代码的10种坏习惯

近几年 TypeScript 和 JavaScript 一直在稳步发展。我们在过去写代码时养成了一些习惯,而有些习惯却没有什么意义。以下是我们都应该改正的 10 个坏习惯。1.不使用 strict 模式这种习惯看起来是什么样的没有用严格模式...
继续阅读 »

近几年 TypeScript 和 JavaScript 一直在稳步发展。我们在过去写代码时养成了一些习惯,而有些习惯却没有什么意义。以下是我们都应该改正的 10 个坏习惯。

1.不使用 strict 模式

这种习惯看起来是什么样的

没有用严格模式编写 tsconfig.json

{
"compilerOptions": {
"target": "ES2015",
"module": "commonjs"
}
}

应该怎样

只需启用 strict 模式即可:

{
"compilerOptions": {
"target": "ES2015",
"module": "commonjs",
"strict": true
}
}

为什么会有这种坏习惯

在现有代码库中引入更严格的规则需要花费时间。

为什么不该这样做

更严格的规则使将来维护代码时更加容易,使你节省大量的时间。

2. 用 || 定义默认值

这种习惯看起来是什么样的

使用旧的 || 处理后备的默认值:

function createBlogPost (text: string, author: string, date?: Date) {
return {
text: text,
author: author,
date: date || new Date()
}
}

应该怎样

使用新的 ?? 运算符,或者在参数重定义默认值。

function createBlogPost (text: string, author: string, date: Date = new Date())
return {
text: text,
author: author,
date: date
}
}

为什么会有这种坏习惯

?? 运算符是去年才引入的,当在长函数中使用值时,可能很难将其设置为参数默认值。

为什么不该这样做

?? 与 || 不同,?? 仅针对 null 或 undefined,并不适用于所有虚值。

3. 随意使用 any 类型

这种习惯看起来是什么样的

当你不确定结构时,可以用 any 类型。

async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: any = await response.json()
return products
}

应该怎样

把你代码中任何一个使用 any 的地方都改为 unknown

async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: unknown = await response.json()
return products as Product[]
}

为什么会有这种坏习惯

any 是很方便的,因为它基本上禁用了所有的类型检查。通常,甚至在官方提供的类型中都使用了 any。例如,TypeScript 团队将上面例子中的 response.json() 的类型设置为 Promise <any>

为什么不该这样做

它基本上禁用所有类型检查。任何通过 any 进来的东西将完全放弃所有类型检查。这将会使错误很难被捕获到。

4. val as SomeType

这种习惯看起来是什么样的

强行告诉编译器无法推断的类型。

async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: unknown = await response.json()
return products as Product[]
}

应该怎样

这正是 Type Guard 的用武之地。

function isArrayOfProducts (obj: unknown): obj is Product[] {
return Array.isArray(obj) && obj.every(isProduct)
}

function isProduct (obj: unknown): obj is Product {
return obj != null
&& typeof (obj as Product).id === 'string'
}

async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: unknown = await response.json()
if (!isArrayOfProducts(products)) {
throw new TypeError('Received malformed products API response')
}
return products
}

为什么会有这种坏习惯

从 JavaScript 转到 TypeScript 时,现有的代码库通常会对 TypeScript 编译器无法自动推断出的类型进行假设。在这时,通过 as SomeOtherType 可以加快转换速度,而不必修改 tsconfig 中的设置。

为什么不该这样做

Type Guard 会确保所有检查都是明确的。

5. 测试中的 as any

这种习惯看起来是什么样的

编写测试时创建不完整的用例。

interface User {
id: string
firstName: string
lastName: string
email: string
}

test('createEmailText returns text that greats the user by first name', () => {
const user: User = {
firstName: 'John'
} as any

expect(createEmailText(user)).toContain(user.firstName)
}

应该怎样

如果你需要模拟测试数据,请将模拟逻辑移到要模拟的对象旁边,并使其可重用。

interface User {
id: string
firstName: string
lastName: string
email: string
}

class MockUser implements User {
id = 'id'
firstName = 'John'
lastName = 'Doe'
email = 'john@doe.com'
}

test('createEmailText returns text that greats the user by first name', () => {
const user = new MockUser()

expect(createEmailText(user)).toContain(user.firstName)
}

为什么会有这种坏习惯

在给尚不具备广泛测试覆盖条件的代码编写测试时,通常会存在复杂的大数据结构,但要测试的特定功能仅需要其中的一部分。短期内不必关心其他属性。

为什么不该这样做

在某些情况下,被测代码依赖于我们之前认为不重要的属性,然后需要更新针对该功能的所有测试。

6. 可选属性

这种习惯看起来是什么样的

将属性标记为可选属性,即便这些属性有时不存在。

interface Product {
id: string
type: 'digital' | 'physical'
weightInKg?: number
sizeInMb?: number
}

应该怎样

明确哪些组合存在,哪些不存在。

interface Product {
id: string
type: 'digital' | 'physical'
}

interface DigitalProduct extends Product {
type: 'digital'
sizeInMb: number
}

interface PhysicalProduct extends Product {
type: 'physical'
weightInKg: number
}

为什么会有这种坏习惯

将属性标记为可选而不是拆分类型更容易,并且产生的代码更少。它还需要对正在构建的产品有更深入的了解,并且如果对产品的设计有所修改,可能会限制代码的使用。

为什么不该这样做

类型系统的最大好处是可以用编译时检查代替运行时检查。通过更显式的类型,能够对可能不被注意的错误进行编译时检查,例如确保每个 DigitalProduct 都有一个 sizeInMb

7. 用一个字母通行天下

这种习惯看起来是什么样的

用一个字母命名泛型

function head<T> (arr: T[]): T | undefined {
return arr[0]
}

应该怎样

提供完整的描述性类型名称。

function head<Element> (arr: Element[]): Element | undefined {
return arr[0]
}

为什么会有这种坏习惯

这种写法最早来源于C++的范型库,即使是 TS 的官方文档也在用一个字母的名称。它也可以更快地输入,只需要简单的敲下一个字母 T 就可以代替写全名。

为什么不该这样做

通用类型变量也是变量,就像其他变量一样。当 IDE 开始向我们展示变量的类型细节时,我们已经慢慢放弃了用它们的名称描述来变量类型的想法。例如我们现在写代码用 const name ='Daniel',而不是 const strName ='Daniel'。同样,一个字母的变量名通常会令人费解,因为不看声明就很难理解它们的含义。

8. 对非布尔类型的值进行布尔检查

这种习惯看起来是什么样的

通过直接将值传给 if 语句来检查是否定义了值。

function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}

应该怎样

明确检查我们所关心的状况。

function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages !== undefined) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}

为什么会有这种坏习惯

编写简短的检测代码看起来更加简洁,使我们能够避免思考实际想要检测的内容。

为什么不该这样做

也许我们应该考虑一下实际要检查的内容。例如上面的例子以不同的方式处理 countOfNewMessages 为 0 的情况。

9. ”棒棒“运算符

这种习惯看起来是什么样的

将非布尔值转换为布尔值。

function createNewMessagesResponse (countOfNewMessages?: number) {
if (!!countOfNewMessages) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}

应该怎样

明确检查我们所关心的状况。

function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages !== undefined) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}

为什么会有这种坏习惯

对某些人而言,理解 !! 就像是进入 JavaScript 世界的入门仪式。它看起来简短而简洁,如果你对它已经非常习惯了,就会知道它的含义。这是将任意值转换为布尔值的便捷方式。尤其是在如果虚值之间没有明确的语义界限时,例如 nullundefined 和 ''

为什么不该这样做

与很多编码时的便捷方式一样,使用 !! 实际上是混淆了代码的真实含义。这使得新开发人员很难理解代码,无论是对一般开发人员来说还是对 JavaScript 来说都是新手。也很容易引入细微的错误。在对“非布尔类型的值”进行布尔检查时 countOfNewMessages 为 0 的问题在使用 !! 时仍然会存在。

10. != null

这种习惯看起来是什么样的

棒棒运算符的小弟 ! = null使我们能同时检查 null 和 undefined

function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages != null) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}

应该怎样

明确检查我们所关心的状况。

function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages !== undefined) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}

为什么会有这种坏习惯

如果你的代码在 null 和 undefined 之间没有明显的区别,那么 != null 有助于简化对这两种可能性的检查。

为什么不该这样做

尽管 null 在 JavaScript早期很麻烦,但 TypeScript 处于 strict 模式时,它却可以成为这种语言中宝贵的工具。一种常见模式是将 null 值定义为不存在的事物,将 undefined 定义为未知的事物,例如 user.firstName === null 可能意味着用户实际上没有名字,而 user.firstName === undefined 只是意味着我们尚未询问该用户(而 user.firstName === 的意思是字面意思是 '' 。

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

收起阅读 »

Vue3 Teleport 简介,请过目,这个是真的好用

vue
关于 vue3 的一个新特性已经讨论了一段时间了,那就是 Portals(传送门) ,它的功能是将模板HTML移动到DOM不同地方的方法。Portals是React中的一个常见特性,Vue2 中可以使用portal-vue库。Vue3 中,提供了&n...
继续阅读 »

关于 vue3 的一个新特性已经讨论了一段时间了,那就是 Portals(传送门) ,它的功能是将模板HTML移动到DOM不同地方的方法。Portals是React中的一个常见特性,Vue2 中可以使用portal-vue库。

Vue3 中,提供了 Teleport 来支持这一功能。

Teleport 的目的

我首先要了解的是何时使用 Teleport 功能。

在处理较大的Vue项目时,有逻辑处理组织代码库是很重要的。 但是,当处理某些类型的组件(如模式,通知或提示)时,模板HTML的逻辑可能位于与我们希望渲染元素的位置不同的文件中。

实际上,在很多时候,与我们的Vue应用程序的DOM完全分开处理时,这些元素的管理要容易得多。 所有这些都是因为处理嵌套组件的位置,z-index和样式可能由于处理其所有父对象的范围而变得棘手。

这种情况就是 Teleport 派上用场的地方。 我们可以在逻辑所在的组件中编写模板代码,这意味着我们可以使用组件的数据或 props。 但是,然后完全将其渲染到我们Vue应用程序的范围之外。

如果不使用 Teleport,我们将不得不担心从子组件向DOM树传递逻辑的事件传播,但现在要简单得多。

Vue Teleport 是如何工作的

假设我们有一些子组件,我们想在其中触发弹出的通知。 正如刚才所讨论的,如果将通知以完全独立的DOM树渲染,而不是Vue的根#app元素,则更为简单。

我们要做的第一件事是打开我们的index.html,并在</body>之前添加一个<div>

// index.html
<body>
<div id="app"></div>
<div id='portal-target'></div>
</body>

接下来,创建触发要渲染的通知的组件。

// VuePortals.vue
<template>
<div class='portals'>
<button @click='showNotification'> Trigger Notification! </button>
<teleport to='#portal-target'>
<div v-if="isOpen" class='notification'>
This is rendering outside of this child component!
</div>
</teleport>
</div>
</template>

<script>
import { ref } from 'vue'
export default {
setup () {
const isOpen = ref(false)

var closePopup

const showNotification = () => {
isOpen.value = true

clearTimeout(closePopup)

closePopup = setTimeout(() => {
isOpen.value = false
}, 2000)
}

return {
isOpen,
showNotification
}
}
}
</script>

<style scoped>
.notification {
font-family: myriad-pro, sans-serif;
position: fixed;
bottom: 20px;
left: 20px;
width: 300px;
padding: 30px;
background-color: #fff;
}
</style>

在此代码段中,当按下按钮时,将渲染2秒钟的通知。 但是,我们的主要目标是使用Teleport获取通知以在我们的Vue应用程序外部渲染。

如你所见,Teleport具有一个必填属性- to

to 需要 prop,必须是有效的查询选择器或 HTMLElement (如果在浏览器环境中使用)。指定将在其中移动 <teleport> 内容的目标元素

由于我们在#portal-target中传递了代码,因此 Vue会找到包含在index.html中的#portal-target div,它会把 Teleport 内的所有代码渲染到该div中。

下面是运行的结果:



总结

以上就是Vue Teleport的基本介绍。 在不久的将来,后面会介绍一些更高级的用例,今天这篇开始使用此炫酷功能开始!

有关更深入的教程,查看Vue3文档

~完,我是刷碗智,我要去刷晚了,骨得白!


代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

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

收起阅读 »

webpack踩坑记录

最近在学习webpack的一些配置,学习的期望就是通过可以通过webpack给html文件中引用的资源例如css、js、img文件加上版本号,避免由于浏览器的缓存造成线上请求的资源依旧是旧版本的东西。首先新建一个webpack的项目(默认大家已经安装node的...
继续阅读 »

最近在学习webpack的一些配置,学习的期望就是通过可以通过webpack给html文件中引用的资源例如css、js、img文件加上版本号,避免由于浏览器的缓存造成线上请求的资源依旧是旧版本的东西。

首先新建一个webpack的项目(默认大家已经安装node的了)

npm init

项目中安装webpack

npm webpack --save-dev
npm webpack-cli --save-dev

然后就可以开心的写代码了

首先讲解单个文件的打包配置

在项目的根目录下,新建一个webpack.config.js文件,

npm install --save-dev html-webpack-plugin mini-css-extract-plugin 
clean-webpack-plugin

现在逐一讲解各个plugin的作用:

  • html-webpack-plugin

当使用 webpack打包时,创建一个 html 文件,并把 webpack 打包后的静态文件自动插入到这个 html 文件当中,并且可以使用自定义的模版,例如html、pug、ejs,还可配置hash值等一些配置。
具体可配置的参数还是很多的,像title、meta等等,可参考webpack官网

  • mini-css-extract-plugin

webpack 4.0以后,把css样式从js文件中提取到单独的css文件中;
这在项目中的使用场景是把css文件在js文件中import进来,打包的时候该插件会识别到这个css文件,通过配置的路径参数生成一个打包后的css文件。

  • clean-webpack-plugin

是用于在下一次打包时清除之前打包的文件,可参考webpack官网

项目中用到的loader

  • babel-loader

Babel把用最新标准编写的 JavaScript代码向下编译成可以在今天随处可用的版本

  • html-loader

它默认处理html中的<img src="image.png">require("./image.png"),同时需要在你的配置中指定image文件的加载器,比如:url-loader或者file-loader

  • url-loader file-loader

用于解决项目中的图片打包问题,把图片资源打包进打包文件中,可修改对应的文件名和路径,url-loader比file-loader多一个可配置的limit属性,通过此参数,可配置若图片大小大于此参数,则用文件资源,小于此参数则用base64格式展示图片;

  • style-loader css-loader

打包css文件并插入到html文件中;

单页面打包webpack.config.js的配置
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const {
CleanWebpackPlugin
} = require('clean-webpack-plugin');

const path = require("path");

module.exports = {
mode: "development",
entry: path.resolve(__dirname, './src/index.js'),

output: {
filename: "bundle.js",
path: path.resolve(__dirname, 'build'),
// libraryTarget: 'umd'
},
module: {
rules: [{
test: /\.html$/,
use: [{
loader: "html-loader",
options: {
attrs: ['img:src', 'link:href']
}
}]
},
{
test: /\.js$/,
use: {
loader: "babel-loader"
},
include: path.resolve(__dirname, '/src'),
exclude: /node_modules/,
},
{
test: /\.(jpg|png|gif|bmp|jpeg)$/,
use: [{
// loader: 'file-loader',
loader: 'url-loader',
options: {
limit: 8192,
// name: '[name].[ext]',
name: '[name]-[hash:8].[ext]',
outputPath: 'images/',

}
}]
},
{
test: /\.pug$/,
use: {
loader: 'pug-loader'
}
},
{
test: /\.css$/,
use: ['style-loader', MiniCssExtractPlugin.loader, 'css-loader']
},

],
},
plugins: [
new CleanWebpackPlugin(),


new HtmlWebpackPlugin({
hash: true,
template: "src/index.html",
// template: "src/index.pug",
filename: "bundle.html",
}),

new MiniCssExtractPlugin({
filename: "bundle.css",
chunkFilename: "index.css"
}),

],
}

多页面

在plugin中,有多个html-webpack-plugin插件的使用,可生成对应的打包后多个html文件

多页面打包webpack.config.js的配置
const getPath = require('./getPath')

const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const {
CleanWebpackPlugin
} = require('clean-webpack-plugin');

const path = require("path");


module.exports = {
mode: "development",
entry: {
main: './src/main/main.js',
side: './src/side/side.js',
// ...getPath.jsPathList,

},
output: {
path: path.resolve(__dirname, 'build'),
filename: 'js/[name].js',
publicPath: '../',
},
module: {
rules: [{
test: /\.html$/,
use: [{
loader: "html-loader",
options: {
attrs: ['img:src', 'link:href']
}
}, ]
},
{
test: /\.js$/,
use: [{
loader: "babel-loader",
options: {
presets: ['es2015']
}
}],
include: path.resolve(__dirname, '/src'),
exclude: /node_modules/,
},
{
test: /\.(jpg|png|gif|bmp|jpeg)$/,
use: [{
// loader: 'file-loader',
loader: 'url-loader',
options: {
limit: 8192,
name: '[name]-[hash:8].[ext]',
outputPath: './images', //指定放置目标文件的文件系统路径
publicPath: '../images',//指定目标文件的自定义公共路径
}
}]
},

{
test: /\.pug$/,
use: {
loader: 'pug-loader'
}
},
{
test: /\.css$/,
use: ['style-loader', MiniCssExtractPlugin.loader, 'css-loader']
},
]
},
plugins: [
new CleanWebpackPlugin(),
//输出html文件1
new HtmlWebpackPlugin({
hash: true,
template: "./src/main/main.html", //本地html文件模板的地址
filename: "html/main.html",
chunks: ['main'],
}),

new HtmlWebpackPlugin({
hash: true,
template: "./src/side/side.html",
filename: "html/side.html",
chunks: ['side'],
}),
// ...getPath.htmlPathList,

new MiniCssExtractPlugin({
filename: "css/[name].css",
chunkFilename: "./src/[name]/[name].css"
}),

]
}

当然也可以通过函数获取所有需要打包的文件的路径,动态在webpack的配置文件中插入

const glob = require("glob");
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
/**
*
* @param {string} globPath 文件的路径
* @returns entries
*/


function getPath(globPath) {
let files = glob.sync(globPath);

let entries = {},
entry, dirname, basename, extname;

files.forEach(item => {
entry = item;
dirname = path.dirname(entry); //当前目录
extname = path.extname(entry); //后缀
basename = path.basename(entry, extname); //文件名
//文件路径
if (extname === '.html') {
entries[basename] = entry;
} else if (extname === '.js') {
entries[basename] = entry;
}
});

return entries;
}

const jsPath = getPath('./src/*/*.js');
const htmlPath = getPath('./src/*/*.html');
const jsPathList = {};
const htmlPathList = [];

console.log("jsPath", jsPath)

Object.keys(jsPath).forEach((item) => {
jsPathList[item] = path.resolve(__dirname, jsPath[item])
})

Object.keys(htmlPath).forEach((item) => {
htmlPathList.push(new HtmlWebpackPlugin({
hash: true,
template: htmlPath[item],
filename: `html/${item}.html`,
chunks: [item],
// chunks: [item, 'jquery'],
}))
})

// console.log("htmlPathList", htmlPathList)


module.exports = {
jsPathList,
htmlPathList
}

经过打包之后,某个文件夹下的html、css、jpg文件,会被分别打包放进build文件夹下的html文件夹、css文件夹和images文件夹,并且在html文件中引用的其他资源文件也加上了hash值作为版本号。

坑:

刚开始的时候url-loader和file-loader都是安装最新版本的,导致打包后图片的路径变成了<img src="[object Module]"/>
所以此项目用的"url-loader": "^2.1.0","file-loader": "^4.2.0"

点击打开项目github地址

原文链接:https://segmentfault.com/a/1190000021159257?utm_source=sf-similar-article

收起阅读 »

2021 年值得关注的 8 个 Node.js 项目

1. Cytoscape.js网站 https://js.cytoscape.org/这个用于可视化和图形分析的开源 JavaScript 库实现了丰富的交互功能。选择方形区域、平移、捏拉缩放等功能都是开箱即用。Cytoscape 可以用于 Node...
继续阅读 »

1. Cytoscape.js


网站 https://js.cytoscape.org/

这个用于可视化和图形分析的开源 JavaScript 库实现了丰富的交互功能。选择方形区域、平移、捏拉缩放等功能都是开箱即用。

Cytoscape 可以用于 Node.js 服务端环境完成图形分析任务,也可以在命令行下使用。有兴趣转向数据科学的开发者可以选择参与 Cytoscape 的开发,它的贡献指南和文档都很棒。

2. PDFKit

网站 https://pdfkit.org/

很有用的基于 Node 的 PDF 生成库,有助于创建复杂的 PDF 文件供下载,支持嵌入文本和字体、注解、矢量图形等特性。不过,这个项目的文档不算丰富,给它贡献代码有点困难。

3. Socket.IO


网站 https://socket.io/

提供双向、实时的基于事件的通讯机制,支持所有浏览器设备,也同样注重性能。比如,可以基于它开发一个简单的聊天应用。

服务端收到新消息后会发给客户端,客户端接收事件通知无需再额外发送新请求至服务端。

支持以下有用特性:

  • 二进制流
  • 实时分析
  • 文档协作

4. Strapi


网站 https://strapi.io/

开源内容管理系统,后端系统通过 REST 风格的 API 提供功能,项目的主要目标是在所有设备上交付结构化的内容。

这个项目支持许多特性,包括内置的邮件系统、文件上传、JSON Web Token 鉴权。基于 Strapi 构建的内容结构非常灵活,可供创建内容分组、定制 API。

5. Nest


网站 https://nestjs.com/

Nest 是很流行的创建高效、可伸缩的服务端应用的新一代框架。底层基于 Express 框架,使用 TypeScript 组合了函数式和面向对象的编程元素。其模块化的架构让你可以很灵活地使用各种库。

6. Date-fns

网站 https://date-fns.org/

date-fns 仍然是在 Node.js 和浏览器环境下处理 JavaScript 日期最简单一致的工具集,也和 browserify、webpack、rollup 等现代模块打包工具配合良好。社区支持非常好,所以支持的本地化区域非常多,各种功能都有详细描述和示例。

7. SheetJS

网站 https://sheetjs.com/

这个 Node.js 库可以处理 Excel 电子表格,以及其他相关功能。比如,导出表格、转换 HTML 表格和 JSON 数组为 xlsx 文件。社区很大,贡献指南的文档也很棒。

8. Express.js


网站 https://expressjs.com/

这是最流行的 Node.js 开源项目之一,它能够高效处理 HTTP 请求,基于 JavaScript 这一同时适用于服务端和浏览器的语言,因此价值巨大。

它是开发高速、安全的应用的利器。

基本特性:

  1. 支持不同的扩展和插件
  2. 基于 HTTP 方法和 URL 的路由机制
  3. 无缝集成数据库

感谢 Adrian Twarog [@adriantwarog] 的细致讲解

请看视频 👇

youtube: 8 Node.js Projects to Keep An Eye On 2021


本文系转载,阅读原文
https://nextfe.com/8-node-js-projects-2021/
收起阅读 »

两种纯CSS方式实现hover图片pop-out弹出效果

主要图形的组成元素由背景和前景图两个元素,以下示例代码中,背景元素使用伪元素 figure::before 表示, 前景元素使用 figure img 表示,当鼠标hover悬浮至figure元素时,背景元素产生变大效果...
继续阅读 »

主要图形的组成元素由背景和前景图两个元素,以下示例代码中,背景元素使用伪元素 figure::before 表示, 前景元素使用 figure img 表示,当鼠标hover悬浮至figure元素时,背景元素产生变大效果,前景元素产生变大并向上移动效果,从而从视觉上实现弹出效果。

背景元素 figure::before


前景元素 figure img

1. 使用 overflow: hidden 方式

主体元素的 html 结构由一个 figure 元素包裹的 img 元素构成:

<figure>
<img src='./man.png' alt='Irma'>
</figure>

在 css 中设置了两个变量 --hov 和 --not-hov 用于控制 hover 元素时的放大以及位移效果。并对 figure 元素添加 overflow: hidden,设置 padding-top: 5% 用于前景元素超出背景元素时不被截断(非必需:并使用了 clamp() 函数用来动态设定 border-radius 以动态响应页面缩放)

figure {
--hov: 0;
--not-hov: calc(1 - var(--hov));
display: grid;
place-self: center;
margin: 0;
padding-top: 5%;
transform: scale(calc(1 - .1*var(--not-hov)));
overflow: hidden;
border-radius: 0 0 clamp(4em, 20vw, 15em) clamp(4em, 20vw, 15em);
}
figure::before, figure img {
grid-area: 1/1;
place-self: end center;
}
figure::before {
content: "";
padding: clamp(4em, 20vw, 15em);
border-radius: 50%;
background: url('./bg.png') 50%/cover;
}
figure:hover {
--hov: 1;
}
img {
width: calc(2*clamp(4em, 20vw, 15em));
border-radius: clamp(4em, 20vw, 15em);
transform: translateY(calc((1 - var(--hov))*10%)) scale(calc(1.25 + .05*var(--hov)));
}


2. 使用 clip-path: inset() 方式

<figure>
<img src='./man.png' alt='Irma'>
</figure>

样式基本上与第一种相同,使用 clip-path 来截取圆形背景区域。

figure {
--hov: 0;
--not-hov: calc(1 - var(--hov));
display: grid;
place-self: center;
margin: 0;
padding-top: 5%;
transform: scale(calc(1 - .1*var(--not-hov)));
clip-path: inset(0 round 0 0 clamp(4em, 20vw, 15em) clamp(4em, 20vw, 15em));
}
figure::before, figure img {
grid-area: 1/1;
place-self: end center;
}
figure::before {
content: "";
padding: clamp(4em, 20vw, 15em);
border-radius: 50%;
background: url('./bg.png') 50%/cover;
}
figure:hover {
--hov: 1;
}
figure:hover::before {
box-shadow: 1px 1px 10px rgba(0, 0, 0, .3);
}
img {
width: calc(2*clamp(4em, 20vw, 15em));
border-radius: clamp(4em, 20vw, 15em);
transform: translateY(calc((1 - var(--hov))*10%)) scale(calc(1.25 + .05*var(--hov)));
}

完整示例

<h2>使用overflow: hidden方式</h2>
<figure>
<img src='./man.png' alt='Irma'>
</figure>
<h2>使用clip-path: path()方式</h2>
<figure>
<img src='./man.png' alt='Irma'>
</figure>

body {
display: grid;
background: #FDFC47;
background: -webkit-linear-gradient(to right, #24FE41, #FDFC47);
background: linear-gradient(to right, #24FE41, #FDFC47);
}
figure {
--hov: 0;
--not-hov: calc(1 - var(--hov));
display: grid;
place-self: center;
margin: 0;
padding-top: 5%;
transform: scale(calc(1 - .1*var(--not-hov)));
}
figure:nth-of-type(1) {
overflow: hidden;
border-radius: 0 0 clamp(4em, 20vw, 15em) clamp(4em, 20vw, 15em);
}
figure:nth-of-type(2) {
clip-path: inset(0 round 0 0 clamp(4em, 20vw, 15em) clamp(4em, 20vw, 15em));
}
figure, figure img {
transition: transform 0.2s ease-in-out;
}
figure::before, figure img {
grid-area: 1/1;
place-self: end center;
}
figure::before {
padding: clamp(4em, 20vw, 15em);
border-radius: 50%;
background: url('./bg.png') 50%/cover;
content: "";
transition: .25s linear;
}
figure:hover {
--hov: 1;
}
figure:hover::before {
box-shadow: 1px 1px 10px rgba(0, 0, 0, .3);
}
img {
width: calc(2*clamp(4em, 20vw, 15em));
border-radius: clamp(4em, 20vw, 15em);
transform: translateY(calc((1 - var(--hov))*10%)) scale(calc(1.25 + .05*var(--hov)));
}

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

收起阅读 »

TypeScript Interface vs Type知多少

接口和类型别名非常相似,在大多情况下二者可以互换。在写TS的时候,想必大家都问过自己这个问题,我到底应该用哪个呢?希望看完本文会给你一个答案。知道什么时候应该用哪个,首先应该了解二者之间的相同点和不同点,再做出选择。接口 vs 类型别名 相同点1. 都可以用来...
继续阅读 »

接口和类型别名非常相似,在大多情况下二者可以互换。在写TS的时候,想必大家都问过自己这个问题,我到底应该用哪个呢?希望看完本文会给你一个答案。知道什么时候应该用哪个,首先应该了解二者之间的相同点和不同点,再做出选择。

接口 vs 类型别名 相同点

1. 都可以用来描述对象或函数

interface Point {
x: number
y: number
}

interface SetPoint {
(x: number, y: number): void;
}
type Point = {
x: number;
y: number;
};

type SetPoint = (x: number, y: number) => void;

2. 都可以扩展

两者的扩展方式不同,但并不互斥。接口可以扩展类型别名,同理,类型别名也可以扩展接口。

接口的扩展就是继承,通过 extends 来实现。类型别名的扩展就是交叉类型,通过 & 来实现。

// 接口扩展接口
interface PointX {
x: number
}

interface Point extends PointX {
y: number
}
// 类型别名扩展类型别名
type PointX = {
x: number
}

type Point = PointX & {
y: number
}
// 接口扩展类型别名
type PointX = {
x: number
}
interface Point extends PointX {
y: number
}
// 类型别名扩展接口
interface PointX {
x: number
}
type Point = PointX & {
y: number
}

接口 vs 类型别名不同点

1. 类型别名更通用(接口只能声明对象,不能重命名基本类型)

类型别名的右边可以是任何类型,包括基本类型、元祖、类型表达式(&|等类型运算符);而在接口声明中,右边必须为结构。例如,下面的类型别名就不能转换成接口:

type A = number
type B = A | string

2. 扩展时表现不同

扩展接口时,TS将检查扩展的接口是否可以赋值给被扩展的接口。举例如下:

interface A {
good(x: number): string,
bad(x: number): string
}
interface B extends A {
good(x: string | number) : string,
bad(x: number): number // Interface 'B' incorrectly extends interface 'A'.
// Types of property 'bad' are incompatible.
// Type '(x: number) => number' is not assignable to type '(x: number) => string'.
// Type 'number' is not assignable to type 'string'.
}

但使用交集类型时则不会出现这种情况。我们将上述代码中的接口改写成类型别名,把 extends 换成交集运算符 &,TS将尽其所能把扩展和被扩展的类型组合在一起,而不会抛出编译时错误。

type A = {
good(x: number): string,
bad(x: number): string
}
type B = A & {
good(x: string | number) : string,
bad(x: number): number
}

3. 多次定义时表现不同

接口可以定义多次,多次的声明会合并。但是类型别名如果定义多次,会报错。

interface Point {
x: number
}
interface Point {
y: number
}
const point: Point = {x:1} // Property 'y' is missing in type '{ x: number; }' but required in type 'Point'.

const point: Point = {x:1, y:1} // 正确
type Point = {
x: number // Duplicate identifier 'A'.
}

type Point = {
y: number // Duplicate identifier 'A'.
}

到底应该用哪个

如果接口和类型别名都能满足的情况下,到底应该用哪个是我们关心的问题。感觉哪个都可以,但是强烈建议大家只要能用接口实现的就优先使用接口,接口满足不了的再用类型别名。

为什么会这么建议呢?其实在TS的wiki中有说明。具体的文章地址在这里

以下是Preferring Interfaces Over Intersections的译文:



上述的几个区别从字面上理解还是有些绕,下面通过具体的列子来说明。

interface Point1 {
x: number
}

interface Point extends Point1 {
x: string // Interface 'Point' incorrectly extends interface 'Point1'.
// Types of property 'x' are incompatible.
// Type 'string' is not assignable to type 'number'.
}
type Point1 = {
x: number
}

type Point2 = {
x: string
}

type Point = Point1 & Point2 // 这时的Point是一个'number & string'类型,也就是never

从上述代码可以看出,接口继承同名属性不满足定义会报错,而相交类型就是简单的合并,最后产生了 number & string 类型,可以解释译文中的第一点不同,其实也就是我们在不同点模块中介绍的扩展时表现不同。

再来看下面例子:

interface PointX {
x: number
}

interface PointY {
y: number
}

interface PointZ {
z: number
}

interface PointXY extends PointX, PointY {
}

interface Point extends PointXY, PointZ {

}
const point: Point = {x: 1, y: 1} // Property 'z' is missing in type '{ x: number; y: number; }' but required in type 'Point'
type PointX = {
x: number
}

type PointY = {
y: number
}

type PointZ = {
z: number
}

type PointXY = PointX & PointY

type Point = PointXY & PointZ

const point: Point = {x: 1, y: 1} // Type '{ x: number; y: number; }' is not assignable to type 'Point'.
// Property 'z' is missing in type '{ x: number; y: number; }' but required in type 'Point3'.

从报错中可以看出,当使用接口时,报错会准确定位到Point。
但是使用交叉类型时,虽然我们的 Point 交叉类型是 PointXY & PointZ, 但是在报错的时候定位并不在 Point 中,而是在 Point3 中,即使我们的 Point 类型并没有直接引用 Point3 类型。

如果我们把鼠标放在交叉类型 Point 类型上,提示的也是 type Point = PointX & PointY & PointZ,而不是 PointXY & PointZ

这个例子可以同时解释译文中第二个和最后一个不同点。

结论

有的同学可能会问,如果我不需要组合只是单纯的定义类型的时候,是不是就可以随便用了。但是为了代码的可扩展性,建议还是优先使用接口。现在不需要,谁能知道后续需不需要呢?所以,让我们大胆的使用接口吧~

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


收起阅读 »

taro-ui实现省市区三级联动

因taro-ui没有省市区三级联动,所以我们利用它提供的Picker 实现多列选择器。

因taro-ui没有省市区三级联动,所以我们利用它提供的Picker 实现多列选择器。

        <Picker

  mode="multiSelector" // 多列选择
onChange={this.onChange} // change事件
onColumnChange={this.onColumnChange} // 某列改变的事件
range={rangeData} //需要展示的数据
value={rangeKey} // 选择的下标
>
<View className="picker">
<Text className="label">所在地址:</Text>
{formData.province && (
<Text>
{formData.province}
{formData.city}
{formData.country}
</Text>
)} // 主要是数据回显加的代码,
{!formData.province && (
<Text className="placeholder">请选择省/市/区</Text>
)}
</View>
</Picker>


上述代码其实taro-ui官方文档都有具体的事例,这里就不多解释了。

相信每个的省市区结构都不一样,现在贴一部分自己项目的省市区结构

[{
provinceName: '北京市',
provinceCode: '11',
cities: [
{
cityName: '市辖区',
cityCode: '1101',
countries: [
{
countryCode: "110101"
countryName: "东城区"
}
]
}
]
}]

现在开始处理数据,因为rangeData是所有数据,省市区,我们需要把数据转换成[‘省’, ‘市’, ‘区’]。

handleCityData = key => {
// 处理数据。
let provinceList = new Array(); // 省
let cityList = new Array(); // 市
let areaList = new Array(); // 区
let { addressData } = this.state;
for (let i = 0; i < addressData.length; i++) {
// 获取省
let province = addressData[i];
provinceList.push(province.provinceName);
}
if (addressData[key[0]].cities && addressData[key[0]].cities.length > 0) {
for (let i = 0; i < addressData[key[0]].cities.length; i++) {
// 获取对应省下面的市
let city = addressData[key[0]].cities[i];
cityList.push(city.cityName);
}
}
for (
let i = 0;
i < addressData[key[0]].cities[key[1]].countries.length;
i++
) {
// 获取市下面对应区
let country = addressData[key[0]].cities[key[1]].countries[i];
areaList.push(country.countryName);
}
// }
let newRange = new Array();
newRange.push(provinceList);
newRange.push(cityList);
newRange.push(areaList);
this.setState({
rangeData: newRange, // 省市区所有的数据
rangeKey: key // key是多列选择器需要展示的下标,因为是初始化,所以我们传入[0,0,0]
});
};

数据处理代码有点丑,欢迎大家提意见。因babel没升级到7版本,所以if判断有点繁琐。

数据处理完了之后,我们需要开始处理每列的值改变,数据联动了,那么我们需要列联动事件。

onColumnChange = e => {
let { rangeKey } = this.state;
let changeColumn = e.detail;
let { column, value } = changeColumn;
switch (column) { // 根据改变不同的列,来显示不同的数据
case 0:
this.handleCityData([value, 0, 0]);
break;
case 1:
this.handleCityData([rangeKey[0], value, 0]);
break;
case 2:
this.handleCityData([rangeKey[0], rangeKey[1], value]);
break;
}
};

到这里的话,就基本实现了省市区三级联动。

下面说一哈,省市区数据回显的代码,不需要的朋友也可以了解一哈。
数据回显,其实很简单,只要找到对应的省市区的下标,就可以回显了。下面是具体实现代码:

getRangeKey = data => {
// 详情的时候获取对应的展示位置
let { addressData } = this.state;
let splitData = data.addressDescription.split("|");

let getAddress = {
province: splitData[0],
city: splitData[1],
country: splitData[2]
};
this.setState({
formData: getAddress
});
let provinceIndex = 0;
let cityIndex = 0;
let countryIndex = 0;
for (let i = 0; i < addressData.length; i++) {
let province = addressData[i];
if (province.provinceName === getAddress.province) {
provinceIndex = i;
for (let j = 0; j < province.cities.length; j++) {
let city = province.cities[j];
if (city.cityName === getAddress.city) {
cityIndex = j;
for (let k = 0; k < city.countries.length; k++) {
let country = city.countries[k];
if (country.countryName === getAddress.country) {
countryIndex = k;
break;
}
}
break;
}
}
break;
}
}
let rangeKey = new Array();
rangeKey.push(provinceIndex);
rangeKey.push(cityIndex);
rangeKey.push(countryIndex);
this.handleCityData(rangeKey);
};

通过上面的循环找出对应省市区的下标,就可以实现省市区的数据回显。

噢,还忘了多列选择器的change事件,这个的话,根据自己项目需要返回的是code还是name,这块就自己处理了,我这边讲的主要是省市区的三级联动。
我是把省市区写成一个组件,然后在父节点传入对应的数据以及事件就可以在一个项目中多次用到了。

下面是该组件的所有代码

import Taro, { Component } from "@tarojs/taro";
import { View, Text, Image, ScrollView, Picker } from "@tarojs/components";
import { connect } from "@tarojs/redux";
import * as actions from "@actions/address";
// import { dispatchCartNum } from '@actions/cart';
import "./index.scss";

@connect(state => state.address, { ...actions })
class ChangeCity extends Component {
static defaultProps = {
detailAddress: {}
};
constructor(props) {
super(props);
this.state = {
addressData: [],
rangeKey: [0, 0, 0],
rangeData: [[], [], []],
formData: {
province: "",
city: "",
country: ""
}
};
}

componentDidMount() {
this.getAddress();
}
getAddress = () => {
this.props.dispatchAddressChina().then(res => {
let addressData = [...res.data];
this.setState(
{
addressData: addressData
},
() => {
let { detailAddress } = this.props;
if (!detailAddress.province) {
this.handleCityData([0, 0, 0]);
} else {
this.getRangeKey(detailAddress);
}
}
);
});
};
getRangeKey = data => {
// 详情的时候获取对应的展示位置
let { addressData } = this.state;
let splitData = data.addressDescription.split("|");

let getAddress = {
province: splitData[0],
city: splitData[1],
country: splitData[2]
};
this.setState({
formData: getAddress
});
let provinceIndex = 0;
let cityIndex = 0;
let countryIndex = 0;
for (let i = 0; i < addressData.length; i++) {
let province = addressData[i];
if (province.provinceName === getAddress.province) {
provinceIndex = i;
for (let j = 0; j < province.cities.length; j++) {
let city = province.cities[j];
if (city.cityName === getAddress.city) {
cityIndex = j;
for (let k = 0; k < city.countries.length; k++) {
let country = city.countries[k];
if (country.countryName === getAddress.country) {
countryIndex = k;
break;
}
}
break;
}
}
break;
}
}
let rangeKey = new Array();
rangeKey.push(provinceIndex);
rangeKey.push(cityIndex);
rangeKey.push(countryIndex);
this.handleCityData(rangeKey);
this.setState({
rangeKey: rangeKey
});
};
handleCityData = key => {
// 处理数据
let provinceList = new Array(); // 省
let cityList = new Array(); // 市
let areaList = new Array(); // 区
let { addressData } = this.state;
for (let i = 0; i < addressData.length; i++) {
// 获取省
let province = addressData[i];
provinceList.push(province.provinceName);
}
if (addressData[key[0]].cities && addressData[key[0]].cities.length > 0) {
for (let i = 0; i < addressData[key[0]].cities.length; i++) {
// 获取对应省下面的市
let city = addressData[key[0]].cities[i];
cityList.push(city.cityName);
}
}
for (
let i = 0;
i < addressData[key[0]].cities[key[1]].countries.length;
i++
) {
// 获取市下面对应区
let country = addressData[key[0]].cities[key[1]].countries[i];
areaList.push(country.countryName);
}
// }
let newRange = new Array();
newRange.push(provinceList);
newRange.push(cityList);
newRange.push(areaList);
this.setState({
rangeData: newRange,
rangeKey: key
});
};
onChange = e => {
let { value } = e.detail;
this.getAddressName(value);
};
getAddressName = value => {
// 这里是转化用户选择的地址数据
let { addressData } = this.state;
let formData = {
province: "",
city: "",
country: ""
};
let payload = {
province: "",
city: "",
country: ""
};
if (addressData[value[0]]) {
formData.province = addressData[value[0]].provinceName; // 省名称
payload.province = addressData[value[0]].provinceCode; // 省code
if (
addressData[value[0]].cities &&
addressData[value[0]].cities[value[1]]
) {
formData.city = addressData[value[0]].cities[value[1]].cityName;
payload.city = addressData[value[0]].cities[value[1]].cityCode;
if (
addressData[value[0]].cities[value[1]].countries &&
addressData[value[0]].cities[value[1]].countries[value[2]]
) {
formData.country =
addressData[value[0]].cities[value[1]].countries[
value[2]
].countryName;
payload.country =
addressData[value[0]].cities[value[1]].countries[
value[2]
].countryCode;
}
}
}
// console.log(formData, "formData");
this.setState({
formData: formData
});
this.props.onChangeAddress(payload, formData);
};
onColumnChange = e => {
let { rangeKey } = this.state;
let changeColumn = e.detail;
let { column, value } = changeColumn;
switch (column) {
case 0:
this.handleCityData([value, 0, 0]);
break;
case 1:
this.handleCityData([rangeKey[0], value, 0]);
break;
case 2:
this.handleCityData([rangeKey[0], rangeKey[1], value]);
break;
}
};
render() {
const { formData, rangeData, rangeKey } = this.state;
return (


mode="multiSelector"
onChange={this.onChange}
onColumnChange={this.onColumnChange}
range={rangeData}
value={rangeKey}
>

所在地址:
{formData.province && (

{formData.province}
{formData.city}
{formData.country}

)}
{!formData.province && (
请选择省/市/区
)}




);
}
}
export default ChangeCity;

样式自己处理一下子就好了

本文链接:https://blog.csdn.net/weixin_42381896/article/details/106854708


Node交互式命令行工具开发——自动化文档工具

 nodejs开发命令行工具,流程相对简单,但一套完整的命令行程序开发流程下来,还是需要下点功夫,网上资料大多零散,这篇教程意在整合一下完整的开发流程。  npm上命令行开发相关包很多,例如minimist、optimist、nopt、commander.js...
继续阅读 »

 nodejs开发命令行工具,流程相对简单,但一套完整的命令行程序开发流程下来,还是需要下点功夫,网上资料大多零散,这篇教程意在整合一下完整的开发流程。
  npm上命令行开发相关包很多,例如minimistoptimistnoptcommander.jsyargs等等,使用方法和效果类似。其中用得比较多的是TJ大神的commanderyargs,本文以commander为基础讲述,可以参考这篇教程,yargs教程可以参考阮大神的或者这一篇
  另外,一个完整的命令行工具开发,还需要了解processshelljspathlinebyline等模块,这些都是node基础模块或一些简单模块,非常简单,就不多说了,另外如果你不想用回调函数处理异步还需要了解一下PromiseGenerator函数。这是教程:i5ting大神的《深入浅出js(Node.js)异步流程控制》和阮大神的异步编程教程以及promise小人书,另外想尝试ES7 stage3阶段的async/await异步解决方案,可参考这篇教程async/await解决方案需要babel转码,这是教程。本人喜欢async/await(哪个node开发者不喜欢呢?)但不喜欢倒腾,况且async/await本身就是Promise的语法糖,所以没选择使用,据江湖消息,nodejs将在今年晚些时候(10月份?)支持async/await,很是期待。
  以下是文章末尾实例用到的一些依赖。

"dependencies": {
"bluebird": "^3.4.1",
"co": "^4.6.0",
"colors": "^1.1.2",
"commander": "^2.9.0",
"dox": "^0.9.0",
"handlebars": "^4.0.5",
"linebyline": "^1.3.0",
"mkdirp": "^0.5.1"
}

 其中bluebird用于Promise化,TJ大神的co用于执行Generator函数,handlebars是一种模板,linebyline用于分行读取文件,colors用于美化输出,mkdirp用于创建目录,另外教程中的示例是一款工具,可以自动化生成数据库和API接口的markdown文档,并通过修改git hooks,使项目的每次commit都会自动更新文档,借助了TJ大神的dox模块。
  <span style="color:rgb(0, 136, 204)">所有推荐教程/教材,仅供参考,自行甄选阅读。</span>

安装Node

  各操作系统下安装见Nodejs官网,安装完成之后用node -v或者which node等命令测试安装是否成功。which在命令行开发中是一个非常有用的命令,使用which命令确保你的系统中不存在名字相同的命令行工具,例如which commandName,例如which testdev命令返回空白那么说明testdev命令名称还没有被使用。

初始化

  1. 新建一个.js文件,即是你的命令要执行的主程序入口文件,例如testdev.js。在文件第一行加入#!/usr/bin/env node指明系统在运行这个文件的时候使用node作为解释器,等价于node testdev.js命令。
  2. 初始化package.json文件,使用npm init命令根据提示信息创建,也可以是使用npm init -y使用默认设置创建。创建完成之后需要修改package.json文件内容加入"bin": {"testdev": "./testdev.js"}这条信息用于告诉npm你的命令(testdev)要执行的脚本文件的路径和名字,这里我们指定testdev命令的执行文件为当前目录下的testdev.js文件。
  3. 为了方便测试在testdev.js文件中加入代码console.log('hello world');,这里只是用于测试环境是否搭建成功,更加复杂的程序逻辑和过程需要按照实际情况进行编写

测试

  使用npm link命令,可以在本地安装刚刚创建的包,然后就可以用testdev来运行命令了,如果正常的话在控制台会打印出hello world

commander

  TJ的commander非常简洁,README.md已经把使用方法写的非常清晰。下面是例子中的代码:

const program = require('commander'),
co = require('co');

const appInfo = require('./../package.json'),
asyncFunc = require('./../common/asyncfunc.js');

program.allowUnknownOption();
program.version(appInfo.version);

program
.command('init')
.description('初始化当前目录doc.json文件')
.action(() => co(asyncFunc.initAction));

program
.command('show')
.description('显示配置文件状态')
.action(() => co(asyncFunc.showAction));

program
.command('run')
.description('启动程序')
.action(() => co(asyncFunc.runAction));

program
.command('modifyhook')
.description('修改项目下的hook文件')
.action(() => co(asyncFunc.modifyhookAction));

program
.command('*')
.action((env) => {
console.error('不存在命令 "%s"', env);
});

program.on('--help', () => {
console.log(' Examples:');
console.log('');
console.log(' $ createDOC --help');
console.log(' $ createDOC -h');
console.log(' $ createDOC show');
console.log('');
});

program.parse(process.argv);

 定义了四个命令和个性化帮助说明。

交互式命令行process

  commander只是实现了命令行参数与回复一对一的固定功能,也就是一个命令必然对应一个回复,那如何实现人机交互式的命令行呢,类似npm init或者eslint --init这样的与用户交互,交互之后根据用户的不同需求反馈不同的结果呢。这里就需要node内置的process模块。
  这是我实现的一个init命令功能代码:

exports.initAction = function* () {
try {
var docPath = yield exists(process.cwd() + '/doc.json');
if (docPath) {
func.initRepl(config.coverInit, arr => {
co(newDoc(arr));
})
} else {
func.initRepl(config.newInit, arr => {
co(newDoc(arr));
})
}
} catch (err) {
console.warn(err);
}

首先检查doc.json文件是否存在,如果存在执行覆盖交互,如果不存在执行生成交互,try...catch捕获错误。
  交互内容配置如下:

newInit:
[
{
title:'initConfirm',
description:'初始化createDOC,生成doc.json.确认?(y/n) ',
defaults: 'y'
},
{
title:'defaultConfirm',
description:'是否使用默认配置.(y/n) ',
defaults: 'y'
},
{
title:'showConfig',
description:'是否显示doc.json当前配置?(y/n) ',
defaults: 'y'
}
],
coverInit:[
{
title:'modifyConfirm',
description:'doc.json已存在,初始化将覆盖文件.确认?(y/n) ',
defaults: 'y'
},
{
title:'defaultConfirm',
description:'是否使用默认配置.(y/n) ',
defaults: 'y'
},
{
title:'showConfig',
description:'是否显示doc.json当前配置?(y/n) ',
defaults: 'y'
}
],

人机交互部分代码也就是initRepl函数内容如下:

//初始化命令,人机交互控制
exports.initRepl = function (init, func) {
var i = 1;
var inputArr = [];
var len = init.length;
process.stdout.write(init[0].description);
process.stdin.resume();
process.stdin.setEncoding('utf-8');
process.stdin.on('data', (chunk) => {
chunk = chunk.replace(/[\s\n]/, '');
if (chunk !== 'y' && chunk !== 'Y' && chunk !== 'n' && chunk !== 'N') {
console.log(config.colors.red('您输入的命令是: ' + chunk));
console.warn(config.colors.red('请输入正确指令:y/n'));
process.exit();
}
if (
(init[i - 1].title === 'modifyConfirm' || init[i - 1].title === 'initConfirm') &&
(chunk === 'n' || chunk === 'N')
) {
process.exit();
}
var inputJson = {
title: init[i - 1].title,
value: chunk,
};
inputArr.push(inputJson);
if ((len--) > 1) {
process.stdout.write(init[i++].description)
} else {
process.stdin.pause();
func(inputArr);
}
});
}

人机交互才用向用户提问根据用户不同输入产生不同结果的形式进行,顺序读取提问列表并记录用户输入结果,如果用户输入n/N则终止交互,用户输入非法字符(除y/Y/n/N以外)提示输入命令错误。

文档自动化

  文档自动化,其中数据库文档自动化,才用依赖sequelize的方法手写(根据需求不同自行编写逻辑),API文档才用TJ的dox也很简单。由于此处代码与命令行功能相关度不大,请读者自行去示例地址查看代码。

示例地址

github地址
npm地址

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

收起阅读 »

JS前端面试总结

ES5的继承和ES6的继承有什么区别ES5的继承时通过prototype或构造函数机制来实现。ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.apply(this))。ES6的继承机制完全不同,实质上是先创建父类的实...
继续阅读 »

ES5的继承和ES6的继承有什么区别

ES5的继承时通过prototype或构造函数机制来实现。ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.apply(this))。
ES6的继承机制完全不同,实质上是先创建父类的实例对象this(所以必须先调用父类的super()方法),然后再用子类的构造函数修改this。
具体的:ES6通过class关键字定义类,里面有构造方法,类之间通过extends关键字实现继承。子类必须在constructor方法中调用super方法,否则新建实例报错。因为子类没有自己的this对象,而是继承了父类的this对象,然后对其进行加工。如果不调用super方法,子类得不到this对象。
ps:super关键字指代父类的实例,即父类的this对象。在子类构造函数中,调用super后,才可使用this关键字,否则报错。

如何实现一个闭包?闭包的作用有哪些

在一个函数里面嵌套另一个函数,被嵌套的那个函数的作用域是一个闭包。
作用:创建私有变量,减少全局变量,防止变量名污染。可以操作外部作用域的变量,变量不会被浏览器回收,保存变量的值。

介绍一下 JS 有哪些内置对象

Object 是 JavaScript 中所有对象的父对象
数据封装类对象:Object、Array、Boolean、Number、String
其他对象:Function、Argument、Math、Date、RegExp、Error

new 操作符具体干了什么呢

(1)创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型。
(2)属性和方法被加入到 this 引用的对象中。
(3)新创建的对象由 this 所引用,并且最后隐式的返回 this 。

同步和异步的区别

同步的概念应该是来自于操作系统中关于同步的概念:不同进程为协同完成某项工作而在先后次序上调整(通过阻塞,唤醒等方式)。
同步强调的是顺序性,谁先谁后;异步则不存在这种顺序性。

同步:浏览器访问服务器请求,用户看得到页面刷新,重新发请求,等请求完,页面刷新,新内容出现,用户看到新内容,进行下一步操作。

异步:浏览器访问服务器请求,用户正常操作,浏览器后端进行请求。等请求完,页面不刷新,新内容也会出现,用户看到新内容。

异步解决方式优缺点

回调函数(callback)

缺点:回调地狱,不能用 try catch 捕获错误,不能 return
优点:解决了同步的问题

Promise

Promise就是为了解决callback的问题而产生的。
回调地狱的根本问题在于:

缺乏顺序性: 回调地狱导致的调试困难,和大脑的思维方式不符
嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身,即(控制反转)
嵌套函数过多的多话,很难处理错误

Promise 实现了链式调用,也就是说每次 then 后返回的都是一个全新 Promise,如果我们在 then 中 return ,return 的结果会被 Promise.resolve() 包装

优点:解决了回调地狱的问题
缺点:无法取消 Promise ,错误需要通过回调函数来捕获

Generator

特点:可以控制函数的执行,可以配合 co 函数库使用

Async/await

async、await 是异步的终极解决方案

优点:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题
缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。

null 和 undefined 的区别

null: null表示空值,转为数值时为0;
undefined:undefined表示"缺少值",就是此处应该有一个值,但是还没有定义。

• 变量被声明了,但没有赋值时,就等于undefined。
• 对象没有赋值的属性,该属性的值为undefined。
• 函数没有返回值时,默认返回undefined。

JavaScript 原型,原型链 ? 有什么特点?

JavaScript 原型: 每创建一个函数,函数上都有一个属性为 prototype,它的值是一个对象。 这个对象的作用在于当使用函数创建实例的时候,那么这些实例都会共享原型上的属性和方法。

原型链: 在 JavaScript 中,每个对象都有一个指向它的原型(prototype)对象的内部链接(proto)。这个原型对象又有自己的原型,直到某个对象的原型为 null 为止(也就是不再有原型指向)。这种一级一级的链结构就称为原型链(prototype chain)。 当查找一个对象的属性时,JavaScript 会向上遍历原型链,直到找到给定名称的属性为止;到查找到达原型链的顶部(Object.prototype),仍然没有找到指定的属性,就会返回 undefined

如何获取一个大于等于0且小于等于9的随机整数

function randomNum(){
return Math.floor(Math.random()*10)
}

想要去除一个字符串的第一个字符,有哪些方法可以实现str.slice(1)

 str.substr(1)
str.substring(1)
str.replace(/./,'')
str.replace(str.charAt(0),'')

JavaScript的组成

JavaScript 由以下三部分组成:

ECMAScript(核心):JavaScript 语言基础
DOM(文档对象模型):规定了访问HTML和XML的接口
BOM(浏览器对象模型):提供了浏览器窗口之间进行交互的对象和方法

到底什么是前端工程化、模块化、组件化

前端工程化就是用做工程的思维看待和开发自己的项目,
而模块化和组件化是为工程化思想下相对较具体的开发方式,因此可以简单的认为模块化和组件化是工程化的表现形式。
模块化和组件化一个最直接的好处就是复用,同时我们也应该有一个理念,模块化和组件化除了复用之外还有就是分治,我们能够在不影响其他代码的情况下按需修改某一独立的模块或是组件,因此很多地方我们及时没有很强烈的复用需要也可以根据分治需求进行模块化或组件化开发。
模块化开发的4点好处:

  1 避免变量污染,命名冲突
  2 提高代码复用率
  3 提高维护性
4 依赖关系的管理

前端模块化实现的过程如下:
一 函数封装
我们在讲到函数逻辑的时候提到过,函数一个功能就是实现特定逻辑的一组语句打包,在一个文件里面编写几个相关函数就是最开始的模块了

function m1(){
    //...
  }

  function m2(){
    //...
  }

这样做的缺点很明显,污染了全局变量,并且不能保证和其他模块起冲突,模块成员看起来似乎没啥关系
二 对象
为了解决这个问题,有了新方法,将所有模块成员封装在一个对象中

var module = new Object({

_count:0,

m1:function (){ ``` },

m2:function (){ ``` }

})

这样 两个函数就被包在这个对象中, 嘿嘿 看起来没毛病是吗 继续往下:
当我们要使用的时候,就是调用这个对象的属性
module.m1()
诶嘿 那么问题来了 这样写法会暴露全部的成员,内部状态可以被外部改变,比如外部代码可直接改变计数器的值
//坏人的操作

module._count = 10;

最后的最后,聪明的人类找到了究极新的方法——立即执行函数,这样就可以达到不暴露私有成员的目的

var module = (function (){

var _count = 5;

var m1 = function (){ ``` };

var m2 = function (){ ``` };

return{
m1:m1,
m2:m2
}

})()

面向对象与面向过程

  1. 什么是面向过程与面向对象?

• 面向过程就是做围墙的时候,由你本身操作,叠第一层的时候:放砖头,糊水泥,放砖头,糊水泥;然后第二层的时候,继续放砖头,糊水泥,放砖头,糊水泥……
• 面向对象就是做围墙的时候,由他人帮你完成,将做第一层的做法抽取出来,就是放砖头是第一个动作,糊水泥是第二个动作,然后给这两个动作加上步数,最后告诉机器人有 n 层,交给机器人帮你工作就行了。

  1. 为什么需要面向对象写法?

• 更方便
• 可以复用,减少代码冗余度
• 高内聚低耦合
简单来说,就是增加代码的可复用性,减少咱们的工作,使代码更加流畅。

事件绑定和普通事件有什么区别

普通添加事件的方法:

var btn = document.getElementById("hello");
btn.onclick = function(){
alert(1);
}
btn.onclick = function(){
alert(2);
}

执行上面的代码只会alert 2

事件绑定方式添加事件:

var btn = document.getElementById("hello");
btn.addEventListener("click",function(){
alert(1);
},false);
btn.addEventListener("click",function(){
alert(2);
},false);

垃圾回收

由于字符串、对象和数组没有固定大小,所有当他们的大小已知时,才能对他们进行动态的存储分配。JavaScript程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则,JavaScript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。
  现在各大浏览器通常用采用的垃圾回收有两种方法:标记清除、引用计数。
1、标记清除
  这是javascript中最常用的垃圾回收方式。当变量进入执行环境是,就标记这个变量为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到他们。当变量离开环境时,则将其标记为“离开环境”。
  垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。
关于这一块,建议读读Tom大叔的几篇文章,关于作用域链的一些知识详解,读完差不多就知道了,哪些变量会被做标记。

2、引用计数
  另一种不太常见的垃圾回收策略是引用计数。引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其所占的内存空间给收回来。这样,垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占的内存。


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


收起阅读 »

面向面试编程,面向掘金面试

我使用 curl 与 jq 一行简单的命令爬取了掘金的面试集合榜单,有兴趣的同学可以看看爬取过程: 使用 jq 与 sed 制作掘金面试文章排行榜,可以提高你使用命令行的乐趣关于前端,后端,移动端的面试,这里统...
继续阅读 »

我使用 curl 与 jq 一行简单的命令爬取了掘金的面试集合榜单,有兴趣的同学可以看看爬取过程: 使用 jq 与 sed 制作掘金面试文章排行榜,可以提高你使用命令行的乐趣

关于前端,后端,移动端的面试,这里统统都有,希望可以在面试的过程中帮助到你。另外我也有一个仓库 日问 来记录前后端以及 devops 一些有意思的问题,欢迎交流

前端

后端

Android/IOS

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

收起阅读 »

vue 自动化路由实现

1、需求描述在写vue的项目中,一般情况下我们每添加一个新页面都得添加一个新路由。为此我们在项目中会专门的一个文件夹来管理路由,如下图所示那么有没有一种方案,能够实现我们在文件夹中新建了一个vue文件,就自动帮我们添加路由。特别在我们的一个ERP后台项目中,我...
继续阅读 »

1、需求描述

在写vue的项目中,一般情况下我们每添加一个新页面都得添加一个新路由。为此我们在项目中会专门的一个文件夹来管理路由,如下图所示


那么有没有一种方案,能够实现我们在文件夹中新建了一个vue文件,就自动帮我们添加路由。特别在我们的一个ERP后台项目中,我们几乎都是一个文件夹下有很多子文件,子文件中一般包含index.vue, detail.vue, edit.vue分别对应的事列表页,详情页和编辑页。


 上图是我们的文件目录,views文件夹中存放的是所有的页面,goodsPlanning是一级目录,onNewComplete和thirdGoods是二级目录,二级目录中存放的是具体的页面,indexComponents中存放的是index.vue的文件,editComponents也是同样的道理。index.vue对应的路由是/goodsPlanning/onNewComplete, edit.vue对应的路由是/goodsPlanning/onNewComplete/edit,detail.vue也是同样的道理。所以我们的文件夹和路由是完全能够对应上的,只要知道路由,就能很快的找到对应的文件。那么有没有办法能够读取我们二级目录下的所有文件,然后根据文件名来生成路由呢?答案是有的


2 、require.context介绍

简单说就是:有了require.context,我们可以得到指定文件夹下的所有文件

require.context(directory, useSubdirectories = false, regExp = /^\.\//);

require.context有三个参数:

  • directory:说明需要检索的目录
  • useSubdirectories:是否检索子目录
  • regExp: 匹配文件的正则表达式

require.context()的返回值,有一个keys方法,返回的是个数组

let routers = require.context('VIEWS', true).keys()
console.log(routers)



 通过上面的代码,我们打印出了所有的views文件夹下的所有文件和文件夹,我们只要写好正则就能找到我们所需要的文件


3、 直接上代码

import Layout from 'VIEWS/layout/index'

/**
* 正则 首先匹配./ ,然后一级目录,不包含components的二级目录,以.vue结尾的三级目录
*/
let routers = require.context('VIEWS', true, /\.\/[a-z]+\/(?!components)[a-z]+\/[a-z]+\.vue$/i).keys()
let indexRouterMap = {} // 用来存储以index.vue结尾的文件,因为index.vue是列表文件,需要加入layout(我们的菜单),需要keepAlive,需要做权限判断
let detailRouterArr = [] // 用来存储以非index.vue结尾的vue文件,此类目前不需要layout
routers.forEach(item => {
const paths = item.match(/[a-zA-Z]+/g) //paths中存储了一个目录,二级目录,文件名
const routerChild = { //定义路由对象
path: paths[1],
name: `${paths[0]}${_.upperFirst(paths[1])}`, //upperFirst,lodash 首字母大写方法
component(resolve) {
require([`../../views${item.slice(1)}`], resolve)
},
}
if (/index\.vue$/.test(item)) { //判断是否以indexvue结尾
if (indexRouterMap[paths[0]]) { //判断一级路由是否存在,存在push二级路由,不存在则新建
indexRouterMap[paths[0]].children.push(routerChild)
} else {
indexRouterMap[paths[0]] = {
path: '/' + paths[0],
component: Layout,
children: [routerChild]
}
}
} else { //不以index.vue结尾的,直接添加到路由中
detailRouterArr.push({
path: item.slice(1, -4), //渠道最前面的 . 和最后的.vue
name: `${paths[0]}${_.upperFirst(paths[1])}${_.upperFirst(paths[2])}`,
component(resolve) {
require([`../../views${item.slice(1)}`], resolve)
},
meta: {
noCache: true, //不keepAlive
noVerify: true //不做权限验证
}
})
}
})

export default [
...Object.values(indexRouterMap),
...detailRouterArr,
/**
* dashboard单独处理下
*/
{
path: '',
component: Layout,
redirect: 'dashboard',
children: [
{
path: 'dashboard',
component: () => import('VIEWS/dashboard/index'),
name: 'dashboard',
meta: { title: '首页', noCache: true, noVerify: true }
}
]
},
]

简简单单的几十行代码就实现了所有的路由功能,再也不用一行一行的写路由文件了。可能你的文件管理方式和我的不一样,但是只要稍微改改正则就行了。


4、 注意

  1. 不能用import引入路由因为用import引入不支持变量
  2. 不能用别名找了半天问题,才知道用变量时也不能用别名,所以我用的都是相对路径


5、 好处

  • 不用在添加路由了,这个就不说了,明眼人都看得出来
  • 知道了路由,一个能找到对应的文件,以前我们团队就出现过,乱写path的情况
  • 更好的控制验证和keepAlive

原文链接:https://www.cnblogs.com/mianbaodaxia/p/11452123.html

收起阅读 »

前端自测清单(前端八股文)

缘起这篇文章主要列举一些自己想到的面试题目,让大家更加熟悉前端八股文。先从性能优化开始吧。性能优化大体可以分为两个,运行时优化加载时优化加载时优化网络优化dns寻址过程tcp的三次握手和四次挥手,以及为何要三次和为何要四次https的握手过程,以及对称加密和非...
继续阅读 »

/zi-ce-qing-dan/featured-image.jpg

缘起

这篇文章主要列举一些自己想到的面试题目,让大家更加熟悉前端八股文。

先从性能优化开始吧。性能优化大体可以分为两个,

  • 运行时优化
  • 加载时优化

加载时优化

网络优化

  • dns寻址过程
  • tcp的三次握手和四次挥手,以及为何要三次和为何要四次
  • https的握手过程,以及对称加密和非对称加密的区别,什么是中间人劫持,ca证书包括哪些内容
  • http1.0,http1.1以及http2.0的区别,多路复用具体指的是什么,keep-alive具体如何体现
  • cdn的原理,cdn什么情况下会回源,cdn的适用场景
  • 浏览器缓存有哪几种,它们的区别是什么,什么时候发生缓存,如何决定缓存哪些文件
  • 了解过websocket么,解释一下websocket的作用

渲染优化

  • 关键渲染路径优化,什么是关键渲染路径,分别如何优化
  • 优化体积,webpack的分包策略,如何配置优化,如何提高构建速度,tree-shaking是什么
  • cssom 的优化,以及html解析过程中,遇到哪些tag会阻塞渲染
  • 雅虎军规说,css尽量放到head里,js放到下方,那么移动端适配的flexiblejs为何要放到css上方呢
  • 影响回流重绘的因素有哪些,如何避免回流,以及bfc是什么,bfc有什么特性,清除浮动的原理是什么

场景:如何优化首屏

除了上以及下面说到的,这里也是分两个层面,

  • 加载时优化
  • 运行时优化

加载

  • 首屏请求和非首屏请求拆分
  • 图片都应该使用懒加载的形式加载
  • 使用preload预加载技术,以及prefetch的dns预解析
  • 与首屏无关的代码可以加async甚至是defer等待网页加载完成后运行

运行

这里跟加载的异常耦合,另作分析吧。

运行时优化

  • 虚拟长列表渲染
  • 图片懒加载
  • 使用事件委托
  • react memo以及pureComponent
  • 使用SSR
  • 。。。

以及一些比较骚的操作,只能特定场景使用,

  • serviceWorker劫持页面
  • 利用worker

更新一波,性能优化之外的面试题,

底层

  • V8是如何实现GC的
  • JS的let,const,call stack,function context,global context。。。的区别
  • this的指向,箭头函数中this和function里的this有什么区别
  • 原型链是什么,继承呢,有几种继承方式,如何实现es6的class
  • eventloop是什么,浏览器的eventloop和nodejs的eventloop有什么区别,nexttick是什么
  • commonjs和AMD,CMD的区别,以及跟ES MODULE的区别
  • 说说require.cache
  • 了解过,洋葱模型没有,它是如何实现的
  • 说说nodejs中的流,也就是stream
  • 你用过ts,说说你常用的ts中的数据类型
  • js的数据类型,weakMap,weakSet和Map以及Set的区别是什么
  • 为何0.1+0.2 不等于0.3,如何解决这个问题
  • js的类型转换
  • 正则表达式
  • 对象循环引用会发生什么问题
  • 如何捕获异步的异常,你能说出几种方案

CSS相关

  • position有哪几种属性,它们的区别是什么
  • 如何实现垂直居中,移动端的呢
  • margin设置百分比,是依据谁的百分比,padding呢
  • 怪异盒模型和一般盒模型有什么区别
  • flex:1代表什么,flex-shrink和flex-grow有什么区别
  • background-size origin基准点在哪里
  • 移动端1px解决方案,以及为何会产生这个问题
  • 移动端高清屏图片的解决方案
  • 说说GPU加速

跨端

  • RN 实现原理
  • 小程序实现原理
  • webview跟h5有什么区别
  • RPC 是什么
  • JSBridge 原理是什么
  • 网页唤起app的原理是什么

服务端

  • oauth2了解过没有,sso呢
  • JWT 如何实现的

网络

除了之前提到的网络问题,当然还有很多,比如

  • 为何使用1x1的gif进行请求埋点
  • TCP 如何进行拥塞控制

安全

  • csrf是什么,防范措施是什么
  • xss如何防范

浏览器相关

  • 跨域是如何产生的,如何解决
  • 如何检查性能瓶颈
  • 打开页面白屏,如何定位问题,或者打开页面CPU100%,如何定位问题
  • jsonp是什么,为何能解决跨域,使用它可能会产生什么问题
  • base64会产生什么问题
  • event.target和event.currTarget有什么区别

框架相关

  • react和vue的区别
  • react的调度原理
  • setstate为何异步
  • key的作用是什么,为何说要使用唯一key,react的diff算法是如何实现的,vue的呢
  • react的事件系统是如何实现的
  • react hook是如何实现的
  • react的通信方式,hoc的使用场景
  • 听过闭包陷阱么,为何会出现这种现象,如何避免
  • vue的响应式原理
  • 为何vue3.x用的是proxy而不是object.defineProperties
  • vue是如何实现对数据的监听的,对数组呢
  • vue中的nexttick是如何实现的
  • fiber是什么,简单说说时间切片如何实现,为何vue不需要时间切片
  • webpack是如何实现的,HMR是如何实现的,可以写个简单的webpack么,webpack的执行流程是怎样的
  • koa源码实现,洋葱模型原理,get/post等这些方法如何set入koa里的,ctx.body为何能直接改变response的body
  • 你简历上写的了解过webpack源码,到哪种程度了(实话说没写koa简单。。

算法相关

  • js大整数加法
  • 双指针
  • 经典排序
  • 动态规划
  • 贪心算法
  • 回溯法
  • DFS
  • BFS
  • 链表操作
  • 线性求值
  • 预处理,前缀和

项目相关

  • 项目中遇到的最大问题是什么,如何解决的
  • nodejs作为中间层的作用是什么

场景题(机试)

  • 如何实现直播上的弹幕组件,要求不能重叠,仿照b站上的弹幕
  • 如何实现动态表单,仿照antd上的form组件
  • 实现一个promise(一般不会这样问)
  • 实现一个限制请求数量的方法
  • 如何实现一个大文件的上传
  • 实现一个eventEmitter
  • 实现一个new,call,bind,apply
  • 实现一个throttle,debound
  • 实现promise.then,finally,all
  • 实现继承,寄生组合继承,instanceof
  • 实现Generator,Aynsc

20.11.20 更新来了

  • React 生命周期,(分三个阶段进行回答,挂载阶段,更新阶段以及卸载阶段)
  • Vue 生命周期 以及其父子组件的生命周期调度顺序
  • 如果让你用强缓存或者协商缓存来缓存资源的话,你会如何使用
  • 作用域是什么,作用域链呢?(这题我想了下,不会利用语言去表达这个东西。。)

目前暂时想到这些,后来有想到的会补充上去。

原文链接:https://steinw.cc/zi-ce-qing-dan/


收起阅读 »

前端如何进行用户权限管理

【前端如何进行用户权限管理】1:问题:假如在做一个管理系统,面向老师学生的,学生提交申请,老师负责审核(或者还需要添加其他角色,功能权限都不同)。现在的问题是,每种角色登录看到的界面应该都是不一样的,那这个页面的区分如何实现呢?2:要不要给老师和学生各自设计一...
继续阅读 »

【前端如何进行用户权限管理】

1:问题:
假如在做一个管理系统,面向老师学生的,学生提交申请,老师负责审核(或者还需要添加其他角色,功能权限都不同)。


现在的问题是,每种角色登录看到的界面应该都是不一样的,那这个页面的区分如何实现呢?

2:要不要给老师和学生各自设计一套页面?这样工作量是不是太大了,并且如果还要加入其它角色的话,难道每个角色对应一套代码?

所以我们需要用一套页面适应各种用户角色,并根据身份赋予他们不同权限

3:权限设计与管理是一个很复杂的问题,涉及的东西很多,相比前端,更偏向于后端,在搜集相关资料的过程中,发现掺杂了许多数据库之类的知识,以及几个用于权限管理的java框架,比如spring,比如shiro等等,都属于后端的工作

4:那我们前端能做什么呢?

权限的设计中比较常见的就是RBAC基于角色的访问控制,基本思想是,对系统操作的各种权限不是直接授予具体的用户,而是在用户集合与权限集合之间建立一个角色集合。每一种角色对应一组相应的权限。

一旦用户被分配了适当的角色后,该用户就拥有此角色的所有操作权限。这样做的好处是,不必在每次创建用户时都进行分配权限的操作,只要分配用户相应的角色即可,而且角色的权限变更比用户的权限变更要少得多,这样将简化用户的权限管理,减少系统的开销。

在Angular构建的单页面应用中,要实现这样的架构我们需要额外多做一些事.从整体项目上来讲,大约有3处地方,前端工程师需要进行处理.

1. UI处理(根据用户拥有的权限,判断页面上的一些内容是否显示)

2. 路由处理(当用户访问一个它没有权限访问的url时,跳转到一个错误提示的页面)

3. HTTP请求处理(当我们发送一个数据请求,如果返回的status是401或者401,则通常重定向到一个错误提示的页面)

如何实现?
首先需要在Angular启动之前就获取到当前用户的所有的permissions,然后比较优雅的方式是通过一个service存放这个映射关系.对于UI处理一个页面上的内容是否根据权限进行显示,我们应该通过一个directive来实现.当处理完这些,我们还需要在添加一个路由时额外为其添加一个"permission"属性,并为其赋值表明拥有哪些权限的角色可以跳转这个URL,然后通过Angular监听routeChangeStart事件来进行当前用户是否拥有此URL访问权限的校验.最后还需要一个HTTP拦截器监控当一个请求返回的status是401或者403时,跳转页面到一个错误提示页面.

大致上的工作就是这些,看起来有些多,其实一个个来还是挺好处理的.

在Angular运行之前获取到permission的映射关系



Angular项目通过ng-app启动,但是一些情况下我们是希望Angular项目的启动在我们的控制之中.比如现在这种情况下,我就希望能获取到当前登录用户的所有permission映射关系后,再启动Angular的App.幸运的是Angular本身提供了这种方式,也就是angular.bootstrap().看的仔细的人可能会注意到,这里使用的是$.get(),没有错用的是jQuery而不是Angular的$resource或者$http,因为在这个时候Angular还没有启动,它的function我们还无法使用.

进一步使用上面的代码可以将获取到的映射关系放入一个service作为全局变量来使用.


在取得当前用户的权限集合后,我们将这个集合存档到对应的一个service中,然后又做了2件事:

(1) 将permissions存放到factory变量中,使之一直处于内存中,实现全局变量的作用,但却没有污染命名空间.

(2) 通过$broadcast广播事件,当权限发生变更的时候.

如何确定UI组件的依据权限进行显隐




这里我们需要自己编写一个directive,它会依据权限关系来进行显示或者隐藏元素.

这里看到了比较理想的情况是通关一个has-permission属性校验permission的name,如果当前用户有则显示,没有则隐藏.




扩展一下之前的factory:




路由上的依权限访问
这一部分的实现的思路是这样: 当我们定义一个路由的时候增加一个permission的属性,属性的值就是有哪些权限才能访问当前url.然后通过routeChangeStart事件一直监听url变化.每次变化url的时候,去校验当前要跳转的url是否符合条件,然后决定是跳转成功还是跳转到错误的提示页面.

router.js:






mainController.js 或者 indexController.js (总之是父层Controller)





这里依然用到了之前写的hasPermission,这些东西都是高度可复用的.这样就搞定了,在每次view的route跳转前,在父容器的Controller中判断一些它到底有没有跳转的权限即可.



HTTP请求处理
这个应该相对来说好处理一点,思想的思路也很简单.因为Angular应用推荐的是RESTful风格的接口,所以对于HTTP协议的使用很清晰.对于请求返回的status code如果是401或者403则表示没有权限,就跳转到对应的错误提示页面即可.





当然我们不可能每个请求都去手动校验转发一次,所以肯定需要一个总的filter.代码如下:

写到这里我们就基本实现了在这种前后端分离模式下,前端部分的权限管理和控制。

原文链接:https://blog.csdn.net/jnshu_it/article/details/77511588


收起阅读 »

彻底解决小程序无法触发SESSION问题

一、首先找到第一次发起网络请求的地址,将服务器返回set-cookie当全局变量存储起来wx.request({ ...... success: function(res) { console.log(res.header); //set-co...
继续阅读 »

一、首先找到第一次发起网络请求的地址,将服务器返回set-cookie当全局变量存储起来

wx.request({
......
success: function(res) {
console.log(res.header);
//set-cookie:PHPSESSID=ic4vj84aaavqgb800k82etisu0; path=/; domain=.fengkui.net

// 登录成功,获取第一次的sessionid,存储起来
// 注意:Set-Cookie(开发者工具中调试全部小写)(远程调试和线上首字母大写)
wx.setStorageSync("sessionid", res.header["Set-Cookie"]);
}
})

二、请求时带上将sessionid放入request的header头中传到服务器,服务器端可直接在cookie中获取

wx.request({
......
header: {
'content-type': 'application/json', // 默认值
'cookie': wx.getStorageSync("sessionid")
//读取sessionid,当作cookie传入后台将PHPSESSID做session_id使用
},
success: function(res) {
console.log(res)
}
})

三、后台获取cookie中的PHPSESSID,将PHPSESSID当作session_id使用

<?php
// 判断$_COOKIE['PHPSESSID']是否存在,存在则作session_id使用
if ($_COOKIE['PHPSESSID']) {
session_id($_COOKIE['PHPSESSID']);
}

session_start();
echo session_id();


原文链接:https://blog.csdn.net/qq_41654694/article/details/85991846

收起阅读 »

vue 重复点击菜单,路由重复报错

报错信息vue-router在3.0版本以上时,重复点菜单,控制台会报错,虽然不影响使用,但是最好处理下这个问题,不然也可能会影响调试其他问题。报错原因vue-router在3.0版本以上时 ,回调形式改成了promise api,返回的是promise,如果...
继续阅读 »

报错信息

vue-router在3.0版本以上时,重复点菜单,控制台会报错,虽然不影响使用,但是最好处理下这个问题,不然也可能会影响调试其他问题。


报错原因
vue-router在3.0版本以上时 ,回调形式改成了promise api,返回的是promise,如果没有捕获到错误,控制台始终会出现如上图的报错
node_module/vue-router/dist/vue-router.js 搜VueRouter.prototype.push

解决方法

1.降低vue-router的版本

npm i vue-router@3.0 -S

2.在vue.use(Router)使用路由插件之前插入如下代码

//获取原型对象上的push函数
const originalPush = Router.prototype.push
//修改原型对象中的push方法
Router.prototype.push = function push (location) {
return originalPush.call(this, location).catch(err => err)
}

3.捕获异常

// 捕获router.push异常
this.$router.push(route).catch(err => {
console.log('输出报错',err)

4.补齐router第三个参数

// 补齐router.push()的第三个参数
this.$router.push(route, () => {}, (e) => {
console.log('输出报错',e)
})

本文链接:https://blog.csdn.net/pinbolei/article/details/115620529


收起阅读 »

深入理解vue中的slot与slot-scope

写在前面vue中关于插槽的文档说明很短,语言又写的很凝练,再加上其和methods,data,computed等常用选项使用频率、使用先后上的差别,这就有可能造成初次接触插槽的开发者容易产生“算了吧,回头再学,反正已经可以写基础组件了”,于是就关闭了vue说明...
继续阅读 »

写在前面

vue中关于插槽的文档说明很短,语言又写的很凝练,再加上其和methods,data,computed等常用选项使用频率、使用先后上的差别,这就有可能造成初次接触插槽的开发者容易产生“算了吧,回头再学,反正已经可以写基础组件了”,于是就关闭了vue说明文档。

实际上,插槽的概念很简单,下面通过分三部分来讲。这个部分也是按照vue说明文档的顺序来写的。

进入三部分之前,先让还没接触过插槽的同学对什么是插槽有一个简单的概念:插槽,也就是slot,是组件的一块HTML模板,这块模板显示不显示、以及怎样显示由父组件来决定。 实际上,一个slot最核心的两个问题这里就点出来了,是显示不显示怎样显示

由于插槽是一块模板,所以,对于任何一个组件,从模板种类的角度来分,其实都可以分为非插槽模板插槽模板两大类。
非插槽模板指的是html模板,指的是‘div、span、ul、table’这些,非插槽模板的显示与隐藏以及怎样显示由插件自身控制;插槽模板是slot,它是一个空壳子,因为它显示与隐藏以及最后用什么样的html模板显示由父组件控制。但是插槽显示的位置确由子组件自身决定,slot写在组件template的哪块,父组件传过来的模板将来就显示在哪块

单个插槽 | 默认插槽 | 匿名插槽

首先是单个插槽,单个插槽是vue的官方叫法,但是其实也可以叫它默认插槽,或者与具名插槽相对,我们可以叫它匿名插槽。因为它不用设置name属性。

单个插槽可以放置在组件的任意位置,但是就像它的名字一样,一个组件中只能有一个该类插槽。相对应的,具名插槽就可以有很多个,只要名字(name属性)不同就可以了。

下面通过一个例子来展示。

父组件:

<template>
<div class="father">
<h3>这里是父组件</h3>
<child>
<div class="tmpl">
<span>菜单1</span>
<span>菜单2</span>
<span>菜单3</span>
<span>菜单4</span>
<span>菜单5</span>
<span>菜单6</span>
</div>
</child>
</div>
</template>

子组件:

<template>
<div class="child">
<h3>这里是子组件</h3>
<slot></slot>
</div>
</template>

在这个例子里,因为父组件在<child></child>里面写了html模板,那么子组件的匿名插槽这块模板就是下面这样。也就是说,子组件的匿名插槽被使用了,是被下面这块模板使用了。

<div class="tmpl">
<span>菜单1</span>
<span>菜单2</span>
<span>菜单3</span>
<span>菜单4</span>
<span>菜单5</span>
<span>菜单6</span>
</div>

最终的渲染结果如图所示:


注:所有demo都加了样式,以方便观察。其中,父组件以灰色背景填充,子组件都以浅蓝色填充。

具名插槽

匿名插槽没有name属性,所以是匿名插槽,那么,插槽加了name属性,就变成了具名插槽。具名插槽可以在一个组件中出现N次。出现在不同的位置。下面的例子,就是一个有两个具名插槽单个插槽的组件,这三个插槽被父组件用同一套css样式显示了出来,不同的是内容上略有区别。

父组件:

<template>
<div class="father">
<h3>这里是父组件</h3>
<child>
<div class="tmpl" slot="up">
<span>菜单1</span>
<span>菜单2</span>
<span>菜单3</span>
<span>菜单4</span>
<span>菜单5</span>
<span>菜单6</span>
</div>
<div class="tmpl" slot="down">
<span>菜单-1</span>
<span>菜单-2</span>
<span>菜单-3</span>
<span>菜单-4</span>
<span>菜单-5</span>
<span>菜单-6</span>
</div>
<div class="tmpl">
<span>菜单->1</span>
<span>菜单->2</span>
<span>菜单->3</span>
<span>菜单->4</span>
<span>菜单->5</span>
<span>菜单->6</span>
</div>
</child>
</div>
</template>

子组件:

<template>
<div class="child">
// 具名插槽
<slot name="up"></slot>
<h3>这里是子组件</h3>
// 具名插槽
<slot name="down"></slot>
// 匿名插槽
<slot></slot>
</div>
</template>

显示结果如图:



可以看到,父组件通过html模板上的slot属性关联具名插槽。没有slot属性的html模板默认关联匿名插槽。

作用域插槽 | 带数据的插槽

最后,就是我们的作用域插槽。这个稍微难理解一点。官方叫它作用域插槽,实际上,对比前面两种插槽,我们可以叫它带数据的插槽。什么意思呢,就是前面两种,都是在组件的template里面写

匿名插槽
<slot></slot>
具名插槽
<slot name="up"></slot>

但是作用域插槽要求,在slot上面绑定数据。也就是你得写成大概下面这个样子。

<slot name="up" :data="data"></slot>
export default {
data: function(){
return {
data: ['zhangsan','lisi','wanwu','zhaoliu','tianqi','xiaoba']
}
},
}

我们前面说了,插槽最后显示不显示是看父组件有没有在child下面写模板,像下面那样。

<child>
html模板
</child>

写了,插槽就总得在浏览器上显示点东西,东西就是html该有的模样,没写,插槽就是空壳子,啥都没有。
OK,我们说有html模板的情况,就是父组件会往子组件插模板的情况,那到底插一套什么样的样式呢,这由父组件的html+css共同决定,但是这套样式里面的内容呢?

正因为作用域插槽绑定了一套数据,父组件可以拿来用。于是,情况就变成了这样:样式父组件说了算,但内容可以显示子组件插槽绑定的。

我们再来对比,作用域插槽和单个插槽和具名插槽的区别,因为单个插槽和具名插槽不绑定数据,所以父组件是提供的模板要既包括样式由包括内容的,上面的例子中,你看到的文字,“菜单1”,“菜单2”都是父组件自己提供的内容;而作用域插槽,父组件只需要提供一套样式(在确实用作用域插槽绑定的数据的前提下)。

下面的例子,你就能看到,父组件提供了三种样式(分别是flex、ul、直接显示),都没有提供数据,数据使用的都是子组件插槽自己绑定的那个人名数组。

父组件:

<template>
<div class="father">
<h3>这里是父组件</h3>
<!--第一次使用:用flex展示数据-->
<child>
<template slot-scope="user">
<div class="tmpl">
<span v-for="item in user.data">{{item}}</span>
</div>
</template>

</child>

<!--第二次使用:用列表展示数据-->
<child>
<template slot-scope="user">
<ul>
<li v-for="item in user.data">{{item}}</li>
</ul>
</template>

</child>

<!--第三次使用:直接显示数据-->
<child>
<template slot-scope="user">
{{user.data}}
</template>

</child>

<!--第四次使用:不使用其提供的数据, 作用域插槽退变成匿名插槽-->
<child>
我就是模板
</child>
</div>
</template>

子组件:

<template>
<div class="child">

<h3>这里是子组件</h3>
// 作用域插槽
<slot :data="data"></slot>
</div>
</template>

export default {
data: function(){
return {
data: ['zhangsan','lisi','wanwu','zhaoliu','tianqi','xiaoba']
}
}
}

结果如图所示:



github

以上三个demo就放在GitHub了,有需要的可以去取。使用非常方便,是基于vue-cli搭建工程。

https://github.com/cunzaizhuyi/vue-slot-demo

转载地址:https://segmentfault.com/a/1190000012996217

收起阅读 »

JavaScript 逐点突破系列 -- 变幻莫测的this指向

JavaScript 逐点突破系列 – 变幻莫测的this指向this指向事件调用环境谁触发事件,函数里面的this指向就是谁let button = document.getElemetById('button')button.onclick = funct...
继续阅读 »

JavaScript 逐点突破系列 – 变幻莫测的this指向

this指向

事件调用环境

谁触发事件,函数里面的this指向就是谁

let button = document.getElemetById('button')
button.onclick = function () {
console.log(this) //button对象
}

全局环境
浏览器环境下

console.log(this) // window

node环境下

console.log(this) // module.exports

函数内部

this最终指向的是调用的对象,和声明没有直接关系

var object = {
name: 'object',
getName: function() {
console.log(this)
}
}
var bar = object.getName // 只是函数声明并未调用
object.getName() // object对象
window.object.getName() // object对象
/* 函数被多层对象所包含,如果函数被最外层对象调用,this指向
的也只是它上一级的对象。*/
bar() // window对象

构造函数

构造函数中的this指向的是实例对象

let fn = function(){
this.id = 'xiaoMing'
console.log(this.id)
}
let fn1 = new fn() //this指向fn1对象

new 的内部原理

【1】创建一个空对象 obj;
【2】把 child 的__proto__ 指向构造函数 parent 的原型对象 prototype,此时便建立了 obj 对象的原型链:child ->parent.prototype->Object.prototype->null
【3】在 child 对象的执行环境调用 parent 函数并传递参数。
【4】考察第 3 步的返回值,如果无返回值 或者 返回一个非对象值,则将 child 作为新对象返回;否则会将 result 作为新对象返回。this绑定的是返回的对象。

function fn(){
this.num = 10;
}
fn.num = 20;
fn.prototype.num = 30;
fn.prototype.method = function() {
console.log(this.num);
}
var prototype = fn.prototype
var method = prototype.method
new fn().method() // 10
prototype.method() // 30
method() // undefined

箭头函数

箭头函数本身没有this和arguments,箭头函数继承上下文的this关键字,也就是说指向上一层作用域的this。
注意点

  1. 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象
function foo() {
return () => {
return () => {
return () => {
console.log('id:', this.id);
};
};
};
}

var f = foo.call({id: 1});
var t1 = f.call({id: 2})()(); // id: 1
var t2 = f().call({id: 3})(); // id: 1
var t3 = f()().call({id: 4}); // id: 1

上面代码之中,只有一个this,就是函数foo的this,所以t1、t2、t3都输出同样的结果。因为所有的内层函数都是箭头函数,都没有自己的this,它们的this其实都是最外层foo函数的this。

除了this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments、super、new.target。

由于箭头函数没有自己的this,所以当然也就不能用call()、apply()、bind()这些方法去改变this的指向。

不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
不可以使用yield命令,因此箭头函数不能用作 Generator 函数。


修改this指向

apply、call、bind

用法

func.call(this, arg1, arg2);
func.apply(this, [arg1, arg2])
func.bind(this, arg1, arg2)()

apply、call的区别

call 需要把参数按顺序传递进去,而 apply 则是把参数放在数组里。

bind和其他两种方法的区别

bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。

原文链接:https://blog.csdn.net/weixin_45495667/article/details/108801000




收起阅读 »

js实现函数防抖节流

一、什么是函数防抖跟节流?函数防抖: 在事件被触发n秒之后在执行回调函数,如果在n秒内又被触发 ,则重新计时。函数节流: 规定一个单位时间,规定在这个时间内,只能执行一次回调函数,如果在这个时间内呗触发多次,则只有一次失效。表现形式就是它有...
继续阅读 »

一、什么是函数防抖跟节流?

函数防抖: 在事件被触发n秒之后在执行回调函数,如果在n秒内又被触发 ,则重新计时。
函数节流: 规定一个单位时间,规定在这个时间内,只能执行一次回调函数,如果在这个时间内呗触发多次,则只有一次失效。表现形式就是它有自己的一个执行频率。

二、JavaScript实现

1.函数防抖

代码如下(示例):

function debounce(callback, wait) {
let timer = null;
return function(){
let _this = this,
arg = arguments;
if(!!timer){
clearTimeout(timer)
timer = null
}
timer = setTimeout(function(){
callback.call(_this,arg)
},wait)
}
}
let _debounce = debounce(function() {
console.log('dshdihdi')
}, 1000)


2.函数节流

代码如下(示例):

// 函数节流,时间戳版本
function throttle(callback, wait) {
let time = new Date().getTime();
return function() {
let _this = this,
arg = arguments;
let nowTime = new Date().getTime();
if (nowTime - time >= wait){
callback.call(_this,...arg)
time = nowTime
}
}
}
let _throttle = throttle(function(e) {
console.log(e)
console.log(arguments)
}, 1000)


函数节流: 懒加载分页请求资源、音乐播放进度条更新等等

函数防抖: 频繁操作点赞、登录注册、需要提交最新信息等等

原文链接:https://blog.csdn.net/qq_45924621/article/details/115586112

收起阅读 »

JavaScript new 操作符

new 操作符做的事情 - 01创建了一个全新的对象。将对象链接到这个函数的 prototype 对象上。执行构造函数,并将 this 绑定到新创建的对象上。判断构造函数执行返回的结果是否是引用数据类型,若是则返回构造函数执行的结果,否则返回创建的对象。new...
继续阅读 »
new 操作符做的事情 - 01
  1. 创建了一个全新的对象。
  2. 将对象链接到这个函数的 prototype 对象上。
  3. 执行构造函数,并将 this 绑定到新创建的对象上。
  4. 判断构造函数执行返回的结果是否是引用数据类型,若是则返回构造函数执行的结果,否则返回创建的对象。

new 操作符做的事情 - 02

  1. 创建一个全新的对象 (无原型的Object.create(null)) 。
  2. 目的是保存 new 出来的实例的所有属性。
  3. 将构造函数的原型赋值给新创建的对象的原型。
  4. 目的是将构造函数原型上的属性继承下来。
  5. 调用构造函数,并将 this 指向新建的对象。
  6. 目的是让构造函数内的属性全部转交到该对象上,使得 this 指向改变,方法有三 : apply、call、bind 。
  7. 判断构造函数调用的方式,如果是 new 的调用方式,则返回经过加工后的新对象,如果是普通调用方式,则直接返回构造函数调用时的返回值。
function myNew(Constructor, ...args) {
// 判断 Constructor 参数是否是函数
if (typeof Constructor !== 'function') {
return 'Constructor.apply is not a function';
};

// 1、创建了一个全新的对象。
let newObject = {};

// 2、将对象链接到这个函数的 prototype 对象上。
newObject.__proto__ = Constructor.prototype;

// 此处是把 1 / 2 步结合到一起
// const newObject = Object.create(Constructor.prototype);

// 3、执行构造函数,
// 并将 this 绑定到新创建的对象上。
let result = Constructor.apply(newObject, args);

// 4. 判断构造函数执行返回的结果是否是引用数据类型,
// 若是则返回构造函数执行的结果,
// 否则返回创建的对象。
if ((result !== null && typeof result === 'object') || (typeof result === 'function')) {
return result;
} else {
return newObject;
};
};

// 需要被 new 的函数
function NewTest(args) {
this.dataValue = args;
return this;
};

// 定义参数
let dataObj = {
sname: '杨万里',
number: 3,
web: 'vue'
};
let dataArray = [5, 'uniApp', '范仲淹'];

// 执行 myNew 函数
let test = myNew(NewTest, 1, 'JavaScript', '辛弃疾', dataObj, dataArray);
console.log(test); // NewTest {dataValue: Array(5)}


本文链接:https://blog.csdn.net/weixin_51157081/article/details/115577751


收起阅读 »

微信小程序-使用canvas绘制图片,下载,分享

接下来下选择图片// 点击选择图片按钮触发start: function() { let that = this let ctx = wx.createCanvasContext('myCanvas') // 设置canvas背景色, 否则制...
继续阅读 »

需求, 使用canvas绘制图片和文字, 生成图片

<!--pages/canvas/canvas.wxml-->
<view>
<view class="container">
<button bind:tap="start" class="start" size="mini">选择图片</button>
<button bind:tap="downloadCanvas" class="downloadCanvas" size="mini">下载</button>
<button bind:tap="share" class="share" size="mini">分享</button>
<!-- 分享图片功能 -->
</view>
<text>canvas区域</text>
<canvas style='width:{{canvasWidth}}px;height:{{canvasHeight}}px;border: 1px solid grey;' canvas-id='myCanvas'></canvas>
<image v-if="previewImage" :src="{{canvasImage}}"></image>
</view>


图片功能

先设置canvas区域的宽度和高度

js代码

data: {
title: 'canvas绘制图片',
canvasWidth: '', // canvas宽度
canvasHeight: '', // canvas高度
imagePath: '', // 分享的图片路径
leftMargin: 0,
topMargin: 0,
imgInfo: {},
ctx: [],
canvasImage: '',
previewImage: false,
imgProportion: 0.8, // 图片占canvas画布宽度百分比
imgToTop: 100 // 图片到canvas顶部的距离
},
onLoad: function(options) {
var that = this
var sysInfo = wx.getSystemInfo({
success: function(res) {
that.setData({
canvasWidth: res.windowWidth,
// 我这里选择canvas高度是系统高度的80%
canvasHeight: res.windowHeight * 0.8
})
// 根据图片比例, 使图片居中
let leftMargin = (res.windowWidth * (1 - that.data.imgProportion)) / 2
that.setData({
leftMargin
})
}
})
}


接下来下选择图片

// 点击选择图片按钮触发
start: function() {
let that = this
let ctx = wx.createCanvasContext('myCanvas')
// 设置canvas背景色, 否则制作的图片是透明的
ctx.setFillStyle('#f8f8f8')
ctx.fillRect(0, 0, that.data.canvasWidth, that.data.canvasHeight)
this.addImage(ctx)
},
// 添加图片
addImage: function(ctx) {
var that = this;
let imgInfo = that.data.imgInfo
var path
wx.chooseImage({
count: '1',
success(res) {
wx.getImageInfo({
src: res.tempFilePaths[0],
success: function(response) {
// 返回的response里有图片的临时路径和图片信息(高度/宽度)
that.setData({
imgInfo: response,
path: response.path
})
that.drawImage(ctx)
}
})
}
})
this.addTitle(ctx)
},


绘制文字和图

// 绘制图片
drawImage(ctx) {
let that = this
let imgInfo = that.data.imgInfo
let path = that.data.path
// 计算图片宽度 宽度固定 高度等比缩放
let imgWidth = that.data.canvasWidth * that.data.imgProportion
let imgHeight = imgInfo.height / imgInfo.width * imgWidth
// drawImage参数, 下面会说明
ctx.drawImage(path, 0, 0, imgInfo.width, imgInfo.height, that.data.leftMargin, that.data.imgToTop, imgWidth, imgHeight)
ctx.draw()
that.data.previewImage = true
},
//绘制文字
addTitle: function(ctx) {
var str = this.data.title
ctx.font = 'normal bold 16px sans-serif';
ctx.setTextAlign('center'); // 文字居中
ctx.setFillStyle("#222222");
ctx.fillText(str, this.data.canvasWidth / 2, 45) // 文字位置
},


drawImage方法


drawImage 方法允许在 canvas 中插入其他图像( img 和 canvas 元素) 。drawImage函数有三种函数原型:


drawImage(image, dx, dy) 在画布指定位置绘制原图

drawImage(image, dx, dy, dw, dh) 在画布指定位置上按原图大小绘制指定大小的图

drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh) 剪切图像,并在画布上定位被剪切的部分 从 1.9.0 起支持


参数描述

image

所要绘制的图片资源(网络图片要通过 getImageInfo / downloadFile 先下载)

s

需要绘制到画布中的,image的矩形(裁剪)选择框的左上角 x 坐标

sy

需要绘制到画布中的,image的矩形(裁剪)选择框的左上角 y 坐标

sWidth

需要绘制到画布中的,image的矩形(裁剪)选择框的宽度

sHeight

需要绘制到画布中的,image的矩形(裁剪)选择框的高度

dx

image的左上角在目标 canvas 上 x 轴的位置

dy

image的左上角在目标 canvas 上 y 轴的位置

dWidth

在目标画布上绘制imageResource的宽度,允许对绘制的image进行缩放

dHeight

在目标画布上绘制imageResource的高度,允许对绘制的image进行缩放


下载图片

//点击下载按钮保存canvas图片
downloadCanvas: function() {
let that = this;
// 判断用户是否选择了图片
if (that.data.previewImage) {
wx.canvasToTempFilePath({
x: 0,
y: 0,
width: that.canvasWidth,
height: that.canvasWidth,
destWidth: that.canvasWidth,
destHeight: that.canvasHeight,
canvasId: 'myCanvas',
success: function success(res) {
wx.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success(res) {
console.log(res, '保存')
}
})
}
});
} else {
wx.showToast({
title: '请先选择图片',
image: '../../static/img/error.png'
})
}
}


分享图片

// 分享图片
share() {
let that = this
if (that.data.previewImage) {

wx.canvasToTempFilePath({
x: 0,
y: 0,
width: that.canvasWidth,
height: that.canvasWidth,
destWidth: that.canvasWidth,
destHeight: that.canvasHeight,
canvasId: 'myCanvas',
success: function success(res) {
wx.showToast({
icon: 'none',
title: '长按图片分享',
duration: 1500
})
setTimeout(
() => {
wx.previewImage({
urls: [res.tempFilePath]
})
}, 1000)
}
})
} else {
wx.showToast({
title: '请先选择图片',
image: '../../static/img/error.png'
})
}
}



原文链接:https://www.jianshu.com/p/1e54146b8a26

收起阅读 »

H5之外部浏览器唤起微信分享

转自https://blog.csdn.net/qq_18976087/article/details/79095735最近在做一个手机站,要求点击分享可以直接打开微信分享出去。而不是jiathis,share分享这种的点击出来二维码。在网上看了很多,都说AP...
继续阅读 »

转自https://blog.csdn.net/qq_18976087/article/details/79095735

最近在做一个手机站,要求点击分享可以直接打开微信分享出去。而不是jiathis,share分享这种的点击出来二维码。在网上看了很多,都说APP能唤起微信,手机网页实现不了。也找了很多都不能直接唤起微信。

总结出来一个可以直接唤起微信的。适应手机qq浏览器和uc浏览器。

下面上代码,把这些直接放到要转发的页面里就可以了:

html部分:

<script src="mshare.js"></script>//引进mshare.js
<button data-mshare="0">点击弹出原生分享面板</button>
<button data-mshare="1">点击触发朋友圈分享</button>
<button data-mshare="2">点击触发发送给微信朋友</button>


js部分:

<script>
var mshare = new mShare({
title: 'Lorem ipsum dolor sit.',
url: 'http://m.ly.com',
desc: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quaerat inventore minima voluptates.',
img: 'http://placehold.it/150x150'
});
$('button').click(function () {
// 1 ==> 朋友圈 2 ==> 朋友 0 ==> 直接弹出原生
mshare.init(+$(this).data('mshare'));
});
</script>


/**
* 此插件主要作用是在UC和QQ两个主流浏览器
* 上面触发微信分享到朋友圈或发送给朋友的功能
*/
'use strict';
var UA = navigator.appVersion;

/**
* 是否是 UC 浏览器
*/
var uc = UA.split('UCBrowser/').length > 1 ? 1 : 0;

/**
* 判断 qq 浏览器
* 然而qq浏览器分高低版本
* 2 代表高版本
* 1 代表低版本
*/
var qq = UA.split('MQQBrowser/').length > 1 ? 2 : 0;

/**
* 是否是微信
*/
var wx = /micromessenger/i.test(UA);

/**
* 浏览器版本
*/
var qqVs = qq ? parseFloat(UA.split('MQQBrowser/')[1]) : 0;
var ucVs = uc ? parseFloat(UA.split('UCBrowser/')[1]) : 0;

/**
* 获取操作系统信息 iPhone(1) Android(2)
*/
var os = (function () {
var ua = navigator.userAgent;

if (/iphone|ipod/i.test(ua)) {
return 1;
} else if (/android/i.test(ua)) {
return 2;
} else {
return 0;
}
}());

/**
* qq浏览器下面 是否加载好了相应的api文件
*/
var qqBridgeLoaded = false;

// 进一步细化版本和平台判断
if ((qq && qqVs < 5.4 && os == 1) || (qq && qqVs < 5.3 && os == 1)) {
qq = 0;
} else {
if (qq && qqVs < 5.4 && os == 2) {
qq = 1;
} else {
if (uc && ((ucVs < 10.2 && os == 1) || (ucVs < 9.7 && os == 2))) {
uc = 0;
}
}
}
/**
* qq浏览器下面 根据不同版本 加载对应的bridge
* @method loadqqApi
* @param {Function} cb 回调函数
*/
function loadqqApi(cb) {
// qq == 0
if (!qq) {
return cb && cb();
}
var script = document.createElement('script');
script.src = (+qq === 1) ? '//3gimg.qq.com/html5/js/qb.js' : '//jsapi.qq.com/get?api=app.share';
/**
* 需要等加载过 qq 的 bridge 脚本之后
* 再去初始化分享组件
*/
script.onload = function () {
cb && cb();
};
document.body.appendChild(script);
}
/**
* UC浏览器分享
* @method ucShare
*/
function ucShare(config) {
// ['title', 'content', 'url', 'platform', 'disablePlatform', 'source', 'htmlID']
// 关于platform
// ios: kWeixin || kWeixinFriend;
// android: WechatFriends || WechatTimeline
// uc 分享会直接使用截图
var platform = '';
var shareInfo = null;
// 指定了分享类型
if (config.type) {
if (os == 2) {
platform = config.type == 1 ? 'WechatTimeline' : 'WechatFriends';
} else if (os == 1) {
platform = config.type == 1 ? 'kWeixinFriend' : 'kWeixin';
}
}
shareInfo = [config.title, config.desc, config.url, platform, '', '', ''];
// android
if (window.ucweb) {
ucweb.startRequest && ucweb.startRequest('shell.page_share', shareInfo);
return;
}
if (window.ucbrowser) {
ucbrowser.web_share && ucbrowser.web_share.apply(null, shareInfo);
return;
}
}
/**
* qq 浏览器分享函数
* @method qqShare
*/
function qqShare(config) {
var type = config.type;
//微信好友 1, 微信朋友圈 8
type = type ? ((type == 1) ? 8 : 1) : '';
var share = function () {
var shareInfo = {
'url': config.url,
'title': config.title,
'description': config.desc,
'img_url': config.img,
'img_title': config.title,
'to_app': type,
'cus_txt': ''
};
if (window.browser) {
browser.app && browser.app.share(shareInfo);
} else if (window.qb) {
qb.share && qb.share(shareInfo);
}
};
if (qqBridgeLoaded) {
share();
} else {
loadqqApi(share);
}
}
/**
* 对外暴露的接口函数
* @method mShare
* @param {Object} config 配置对象
*/
function mShare(config) {
this.config = config;
this.init = function (type) {
if (typeof type != 'undefined') this.config.type = type;
try {
if (uc) {
ucShare(this.config);
} else if (qq && !wx) {
qqShare(this.config);
}
} catch (e) {}
}
}
// 预加载 qq bridge
loadqqApi(function () {
qqBridgeLoaded = true;
});
if (typeof module === 'object' && module.exports) {
module.exports = mShare;
} else {
window.mShare = mShare;
}
收起阅读 »