• ECharts + geoJSON 绘制地图

    获取geoJSON

    1. 通过地图选择器下载geoJSON数据,或者直接引用里面的地址。本例以洛阳市为例引用地址https://geo.datav.aliyun.com/areas/bound/410300_full.json

    2. arcgis里面使用工具把shp数据处理成geoJSON数据。使用工具为arcgis工具箱 Conversion Tools->JSON->Features To JSON

    使用ECharts创建地图

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>洛阳市</title>
    <script src="scripts/lib/echarts.min.js"></script>
    <script src="scripts/lib/jquery.min.js"></script>
    <style>
    html,body,#main{
    padding: 0px;
    margin: 0px;
    height: 100%;
    overflow: hidden;
    }
    </style>
    </head>
    <body>
    <div id="main"></div>
    <script type="text/javascript">
    $.get("https://geo.datav.aliyun.com/areas/bound/410300_full.json",function(map){
    var myChart = echarts.init(document.getElementById('main'));
    echarts.registerMap("luoyang",map);
    var option = {
    series : [ {
    map : "luoyang",
    type : "map",
    aspectScale: 1.0,
    selectedMode: 'single',//选择类型,
    hoverable:false,//鼠标经过高亮
    roam: true,//鼠标滚轮缩放
    itemStyle: {
    normal: {
    borderWidth:1,
    borderColor:'#ffffff',//区域边框色
    areaColor: '#FFDAB9',//区域背景色
    label: {
    show: true,
    textStyle: {
    color: '#6495ED',//文字颜色
    fontSize:18 //文字大小
    }
    }
    },
    emphasis: { // 选中样式
    borderWidth:1,
    borderColor:'#00ffff',
    color: '#ffffff',
    label: {
    show: true,
    textStyle: {
    color: '#ff0000'
    }
    }
    }
    }
    } ]
    };
    myChart.setOption(option);
    });
    </script>
    </body>
    </html>

    效果图如下

  • 更简单的Vue3中后台动态路由 + 侧边栏渲染方案

    之前的动态路由方案

    • 前端只存储基础的路由(登录、首页、404)

    • 根据不同的登录角色,返回其对应的可访问路由树

    • 从服务端获取路由树(JSON)并递归处理成vue-router可使用的数据结构,并通过addRouters拼接到基础路由树,完成动态路由

    优点

    • 相当安全,项目里面只有基础路由,业务路由全部来自接口

    缺点

    • 代码中只保存了基础路由,业务路由的全部字段需要前端开发人员手动录入到超级管理系统之中,维护业务路由非常繁琐。

    • 客户端逻辑相对复杂, addRouters 的逻辑必须在路由钩子 beforeRouter 中完成,这部分逻辑比较烧脑。

    有没有一种办法,可以既保留动态路由的特性,也保证代码逻辑的简单性,同时将路由配置回归前端项目呢?

    转变解决思路

    通过以上分析,我们首先可以明确一点,动态路由的配置数据还是需要放在前端项目中,而不是将路由的配置录入到系统中。所以,我们可以简化系统后台的权限配置,由之前的超多字段简化为两个字段 路由名称、路由标识,仅服务于为角色勾选路由,路由配置交还给前端项目。

    路由的树形结构还是需要录入到系统中,因为我们还需要保留动态路由的核心逻辑,给(角色/用户)勾选指定路由。

    Vue2 版本,我们通过 addRouters为项目路由树“做加法”实现动态路由。

    Vue3 版本,我们则为项目路由树手动 “做减法”实现动态路由。

    做“减法”实现动态路由

    首先依旧将我们的全部路由分成2部分,基础路由数组,动态路由数组

    基础路由:无论什么角色都可以访问的路由(登录后的公共页面,比如工作台,没有可以为空)

    动态路由:拥有权限的角色才可进行访问的路由

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    /**
    * 基础业务路由
    */
    export const baseRouter: RouteRecordRaw[] = [
    //.....
    ]

    /**
    * 动态业务路由
    */
    export const asyncRouter: RouteRecordRaw[] = [
    //.....
    ]

    const router = createRouter({
    routes: [
    // 这一层的路由除了dashboard,其他都是业务无关的路由,比如登录 注册 404 500,不属于动态路由
    {
    path: '/login',
    name: 'login',
    meta: {
    title: '登录',
    },
    component: () => import('@/views/login/index.vue'),
    },
    {
    path: '/',
    redirect: '/dashboard',
    name: 'baseDashboard',
    meta: {
    title: '根路径',
    },
    component: layout,
    children: [...baseRouter, ...asyncRouter], // 全部注册到vue-router中
    }
    // ...
    ],
    })

    我们直接将全部路由都注册到 Vue 中,如果不存在鉴权,此时任意角色都可以访问所有页面。

    为了实现动态路由的需求,我们只要解决两个问题

    1. 如何实现路由拦截,拦截不允许被访问的页面。

    2. 如何做减法,筛选出指定的权限树,用于侧边菜单栏的展示。

    如何实现路由拦截

    在用户登录的时候,我们会调用服务端的 获取当前用户权限 接口,获取到当前用户的权限数据

    无论后端给我们返回的什么样的接口,嵌套也好、一维数组也罢,我们都将其处理成一维数组

    1
    ["index", "dashboard", "root", "goods", "goodsList", "goodsClass"]

    并将这样的一维数组,保存到我们全局状态库 pinia 中,我们假设变量名称为 authList

    然后我们再增加路由钩子的逻辑,每次跳转之前,都判断 next 的页面 nameauthList 中是否存在,如果不存在,则直接 404 ,如果存在,则 允许访问

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    router.beforeEach((to, from, next) => {
    const auth = useAuthStore()
    // ......
    // 登录后逻辑
    if (auth.isLogin) {
    // 判断权限是否通过
    if (auth.asyncRouter.includes(String(to.name))) {
    next()
    } else {
    next({ name: '404' })
    }
    } else {
    // 未登录逻辑...
    }
    })

    经过beforeEach的逻辑之后,我们现在就已经实现了基本的鉴权,不允许访问的页面,都将404。

    递归筛选出菜单树

    接下来,我们考虑第二个问题,用户登录之后,自身权限获取完毕,用户进入到管理系统内部,右侧显示功能侧边栏,我们不能将不属于该用户的动态路由都显示出来 ,所以我们需要根据服务端返回的权限数据,实现项目中的动态路由 asyncRouter 的筛选。

    因为我们路由层级理论上是无限的,所以这里使用递归进行实现比较合理。

    实现思路如下

    1. 递归遍历asyncRouter路由树,如果路由的name在权限数组内,并且该菜单可显示,则继续递归children,如果没有children则不做处理

    2. 如果路由的name不在权限数组中,则将其splice,也就是 做减法 ,并且循环下标后退一位,防止跳过下一个。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      /**
      * 获取可访问路由树
      * @param tree
      */
      export function loopRouter(tree: RouteRecordRaw[], asyncRouter: RouteRecordName[]) {
       for (let i = 0, len = tree.length; i < len; i++) {
         let item = tree[i]
         if (asyncRouter.includes(item.name!) && item.meta!.showMenu) {
           if (item.children) {
             item.children = loopRouter(item.children!, asyncRouter)
          }
        } else {
           tree.splice(i, 1)
           len = tree.length // 刷新循环长度
           if (i < tree.length) {
             // 删除后,数组长度-1,数组的下一位前进了一位,所以一旦splice掉不存在的权限,便需要i--,否则会跳过下一位
             i--
          }
        }
      }
       return tree
      }

    经过以上代码后,我们的路由树便会过滤掉那些不允许被访问的路由

    接下来,我们便可以进行菜单的渲染工作了,前端基操我就不做过多赘述了。

    到此为止,我们的动态路由就实现了,是不是非常简单~

    安全问题

    有经验的小伙伴会问,用户登录后,刷新页面再访问的时候,我们如何处理权限呢?

    在我看来这有2个方案

    方案1(相对简单): 使用pinia的持久化插件pinia-plugin-persistedstate来持久化我们存储pinia的权限数据,无论用户如何刷新,我们的权限数据都一直有效,这样的实现非常简单,但是存在一定的安全隐患,就是心怀不轨的某些人,知道了不允许访问的菜单路由名称之后,可以通过手动修改当前页面的localStorage实现权限的突破。

    方案2(更加安全):pinia中创建一个变量isRouterInit标识权限是否已经被初始化,如果没获取过权限数据,则为false,如果获取过则为true;每次beforeEach的时候,登录情况下都判断该值是否为false,如果为false,则请求当前角色的权限数据,并在请求完毕后,再进行相关路由拦截逻辑,并将变量isRouterInit改为true,表明权限已经被初始化。

    以上2种方案都可以用,如果安全性要求不是特别高,建议方案1,如果对安全性、实时性要求比较高,则建议方案2。

    菜单的排序问题

    经过实操的小伙伴还会发现一个问题,在项目中的路由排序也许是个麻烦事。

    可能角色A说:我要acb,角色b说:我要bac,但是我们路由表放在项目中,其排序是固定不变的,有什么办法可以实现项目左侧路由树按照后台返回的权限字段数据进行渲染呢?

    其实这个问题很简单,我们针对递归后的动态路由的结果,加一个sort即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    /**
    * 获取可访问路由树
    * @param tree
    */
    export function loopRouter(tree: RouteRecordRaw[], asyncRouter: RouteRecordName[]) {
     // .....
     // 将菜单按照当前拥有权限asyncRouter的顺序排序
     return tree.sort((a, b) => asyncRouter.indexOf(a.name) - asyncRouter.indexOf(b.name))
    }

    这样项目的右侧菜单树就会按照我们后端返回的顺序进行排序了。

    不需要的路由可以删除吗?

    vue3提供了动态路由的相关 API ,其中有一个removeRoute,可以删除不需要的路由,那么,我们在根据登录角色动态删除剔除不需要在侧边栏显示的路由的时候,是否也同步将不需要的路由进行删除呢?

    关于这个问题,我的建议是,不建议删除,首先我们的路由模块均为异步加载的,是否删除一定不会访问的路由,都不会影响项目的加载。而一旦真的删除了,用户在推出登录切换账号的时候,一定要 重载当前页面 ,因为我们不知道用户下一次登录的角色是什么,会不会用到已经被删除的路由。

    最后

    B端项目通过动态路由实现角色鉴权,已经是一个非常成熟的方案,无论是使用 “加法方案” 实现,还是使用 “减法方案” ,都是可行的,理论上都是对权限的一次递归筛选。

    大家主要根据项目规模、要求合理选择最适合的方案,在安全、便捷、开发难度、稳定性,等多角度做好权衡利弊。

  • 使用 nvm-desktop 轻松安装和管理多个 node 版本

    前言

    作为一枚前端开发工程师,我们一般都是通过在 官网 下载二进制安装包来安装 node,但是实际开发难免会因为一些兼容性的问题需要安装切换不同版本的 node,如果还是通过这种方式,那么就必须先卸载现有版本再安装新的版本,无疑十分不便。

    虽然社区已经有很成熟的工具(nvm)来解决这个问题,但是 nvm 是基于 shell 的交互式命令的,用起来可能还是不是那么直观便捷:比如在 macOS 平台需要安装支持 arm64 架构的版本的 nodenvm 就没办法通过命令(nvm ls -remote)来查看;而在 Windows 平台则需要通过 nvm-windows 来单独安装以获得支持。

    今天就来介绍一个通过可视化界面操作来实现安装和管理多个 node 版本的工具:nvm-desktop,全称:Node Version Manager Desktop,支持双平台,代码完全开源,可放心使用。

    现在已经支持为不同项目单独设置和切换不同版本的 Node,开箱即用,不需要任何其他操作。

    使用截图

    macOS 下的演示视频:

    Windows 截图(使用效果是一样的,就没有单独再录制一份了):

    下载和安装

    下载

    下载地址:nvmd-desktop Download Page (GitHub release)

    根据不同的平台,下载最新的版本。

    安装

    下载好对应平台的二进制安装程序之后,点击安装即可。

    macOS

    因为该程序目前并没有通过 Apple开发者账号 进行代码证书签名,所以在macOS上运行会出现安全提示:

    "File/App is damaged and cannot be opened. You should move it to Trash."

    "File/App is damaged and cannot be opened. You should move it to Trash." is a Mac error that can occur to various macOS versions, such as macOS Ventura/Monterey/Big Sur/Catalina, especially on M1 Macs. It usually happens on apps or files downloaded from the web, but it can also arise when opening apps downloaded from App Store.

    这个是 macOS 的一种安全策略,特别是在最新的系统版本(macOS Ventura / Monterey / Big Sur / Catalina )中,Apple 已经默认不允许用户通过系统设置来运行不受信任的应用程序。

    可以查看 nvm-desktop文档 或者这篇文章(Fix 'File/App is damaged and cannot be opened' on Mac)解决以允许该程序运行。

    因为该项目代码完全开源,所以请放心运行。或者也可以根据 文档 的教程,把代码 clone 到本地编译安装。

    Windows

    该程序在 Windows 平台默认申请 管理员权限 运行,因为需要将安装的 node 的文件路径设置到系统的环境变量中。

    相关代码:

    编译打包的配置项:requestedExecutionLevel

    设置环境变量的代码:setx -m NVMD nodePath

    注意⚠️

    Release v2.0.0 底层实现上做了大变动,现在不依赖操作系统的特定功能和shell,也不需要通过 setx -m 命令设置系统环境变量,更多详细信息请查看这篇文章:Node 版本管理工具:nvm-desktop 进化啦!不依赖操作系统的功能和 shell,完美支持为项目切换不同的 Node 版本

    运行

    macOS 平台为例:

    根据向导,需将如下命令添加到系统的 ~/.bashrc~/.profile 或者 ~/.zshrc 文件中:

    1
    2
    export NVMD_DIR="$HOME/.nvmd" 
    [ -s "$NVMD_DIR/nvmd.sh" ] && . "$NVMD_DIR/nvmd.sh" # This loads nvmd

    最新的 Release v2.0.0 版本则是:

    1
    2
    export NVMD_DIR="$HOME/.nvmd"
    export PATH="$NVMD_DIR/bin:$PATH"

    我的电脑用的是系统默认的 zsh, 所以复制这个命令添加到 ~/.zshrc 文件中即可。如果你的电脑使用的是 bash,则复制粘贴到 ~/.bashrc 文件中去即可。

    Windows 下则不需要额外的操作,安装好运行之后直接搜索指定的 node 版本点击下载安装即可。

    进入到主页面:

    nodev16.0.0 开始支持 macOSarm64 架构:

    为不同的项目指定不同的 Node 版本:

    找到自己需要的版本进行安装:

    可实时查看下载进度,或者取消下载。

    安装好之后,点击 应用 按钮即可设置成当前的 node 版本:

    重新打开终端之后,可通过 node --version & npm --version 查看是否生效。

    其他功能

    命令行工具

    nvmd 允许您通过命令行快速管理不同版本的 Nodenvmd 不提供 Node 的下载安装功能,如果您需要下载安装新版本的 Node,请打开 nvm-desktop 客户端):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    $ nvmd use 18.17.1
    Now using node v18.17.1
    $ node -v
    v18.17.1
    $ nvmd use v20.5.1 --project
    Now using node v20.5.1
    $ node -v
    v20.5.1
    $ nvmd ls
    v20.6.1
    v20.5.1 (currently)
    v18.17.1
    $ nvmd current
    v20.5.1

    nvmd --help:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    $ nvmd --help
    nvmd (2.2.0)
    The1111mp@outlook.com
    command tools for nvm-desktop

    Usage: nvmd [COMMAND]

    Commands:
    current Get the currently used version
    list List the all installed versions of Node.js
    ls List the all installed versions of Node.js
    use Use the installed version of Node.js (default is global)
    which Get the path to the executable to where Node.js was installed
    help Print this message or the help of the given subcommand(s)

    Options:
    -h, --help Print help
    -V, --version Print version

    Please download new version of Node.js in nvm-desktop.

    在你通过 nvmd use 命令行切换 Node 版本之后,请点击刷新按钮让 nvm-desktop 同步最新的数据。

    更多详情请查看此文档: command-tools-intro

    为项目单独设置 Node 版本

    不依赖操作系统的功能和 shell,完美支持为项目切换不同的 Node 版本,而且切换之后不需要重启终端。如果想要体验这个功能,请安装 Release v2.0.0 版本。

    nvmd-desktop Download Page (GitHub Release v2.0.0)

    点击 添加项目 按钮,选择项目目录,然后为项目选择需要的 Node 的版本(已安装)。

    选择版本之后,在你的项目的跟目录中会添加一个 .nvmdrc 文件,内容为选择的版本号,nvm-desktop 通过此文件为终端设置 Node 的版本。重新打开你的终端进入到项目目录下,通过 node --version 查看是否生效。

    全局设置的 Node 版本为 v18.17.1

    为项目指定 v20.5.0 版本:

    终端该项目根目录下查看:

    更多说明请查看另一篇文章文章:Node 版本管理工具:nvm-desktop 进化啦!不依赖操作系统的功能和 shell,完美支持为项目切换不同的 Node 版本

    通过系统托盘菜单快捷管理 node 版本

    避免频繁打开界面窗口,通过菜单栏选项快捷切换 node 版本

    Macos:

    Windows:

    同步最新发布的 node 版本

    因为 node 所有版本信息数据默认会做缓存处理,所以如果想要查看最新发布的 node 版本,点击 远程刷新 按钮同步最新数据即可:

    可通过官方链接查看:https://nodejs.org/dist/index.json

    自定义下载镜像地址

    默认下载镜像地址是:nodejs.org/dist,如果你所在的地区下载速度慢,那么你可以更改适合你自己的下载地址以快速下载。

    推荐地址:https://npmmirror.com/mirrors/node

    多语言和多主题

    多语言目前支持 English简体中文

    主题目前支持 跟随系统亮色暗黑 三种模式:

    已安装界面

    在已安装界面快速找到已经安装和当前使用的 node 的版本,方便切换和管理。

    查看 node 发布的更新日志

    在主界面通过点击 node 的版本号,可跳转至官方发布的对应版本的 Changelog 界面(随时掌握新版本的动向):

  • 一文搞定react-router-dom最新版V6路由的入门及使用

    本文主要以react-router-dom v6版本为主,旧版过多概念不介绍,想必诸位应该也知道该库是做什么用的 😅,主要引领大家入门了解 react-router-dom 的使用,快速上手最新 React Router v6 版本。

    1. 安装

    首先,确保你已经安装了Node.js和npm或yarn。然后,在项目的根目录下运行以下命令来安装react-router-dom。

    👉 打开你的终端并使用 Vite 引导一个新的 React 应用程序:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 第一步执行安装命令
    npm create vite@latest demo-react-vite -- --template react
    # 切换到项目根目录
    cd demo-react-vite
    # 安装项目依赖
    npm install
    # 运行项目
    npm run dev
    # 安装react-router-dom
    npm install react-router-dom

    目前这里最新的版本为:

    1
    2
    3
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.22.2"

    2. 路由模式

    React Router 支持两种路由模式:BrowserRouter 和 HashHistory。

    1. BrowserRouter 模式使用 URL 中的/来定义路由,例如:http://xxx.com/about。

    2. HashHistory 模式使用 URL 中的#来定义路由,例如:http://xxx.com/#/about。

    默认情况下,React Router 使用 BrowserRouter 作为其历史记录。

    注意:BrowserRouter组件最好放在最顶层所有组件之外,这样可以确保内部组件在使用 Link 做路由跳转时不会出现出错

    3. 路由组件

    路由组件是用于处理路由的组件。在 React Router 中,路由组件通常用于显示不同的页面或视图。

    3.1 创建路由组件

    在项目src目录中创建一个App.jsx的文件,并在其中编写示例代码 👇

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import React from 'react';
    import { BrowserRouter, Routes, Route } from 'react-router-dom';
    function Home() {
    return <div>Home</div>;
    }

    function About() {
    return <div>About</div>;
    }
    function App() {
    return (
    <BrowserRouter>
    <Routes>
    <Route path="/" element={<Home />} />
    <Route path="/about" element={<About />} />
    </Routes>
    </BrowserRouter>
    );
    }

    3.2 Route 组件

    Route组件用于定义路由规则。Route组件接收两个属性:path和element。path属性用于定义路由的路径,element属性用于定义路由对应的组件,index属性用于指定默认子路由。

    • 注意element属性值必须写成标签的形式

    • Route组件可以嵌套使用,用于定义更复杂的路由规则。

      • 嵌套路由可以不在向旧版一样提供完整的路径,因此新版本路径书写变短,但访问路径还是需要完整路径如 /about/joinus
        1
        2
        3
        4
        5
        <Route path="/about" element={<About />}>
        <Route index element={<Address />} />
        <Route path="address" element={<Address />}></Route>
        <Route path="joinus" element={<Join />}></Route>
        </Route>
    • 在新版中Route先后顺序不在重要,Router Route可以自动匹配正确的路径

    • 使用index属性可以指定默认子路由

    • 或通过path为空,来指定默认路由

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      const router = [
      {
      path: '/home',
      element: <Home />,
      children: [
      {
      path: '',
      element: <News />,
      },
      {
      path: 'news',
      element: <About />,
      },
      ],
      },
      ];
    • 该组件必须包含在Routes组件中

    3.3 Routes 组件

    Routes组件是之前版本种Switch组件的变名,抓哟用于将多个Route组件组合在一起。

    LinkNavLink 组件类似于 a 标签,用于创建路由链接。Link组件接收一个to属性,用于指定链接的地址,NavLink组件相似,区别是可以添加一些样式来区分当前页面。

    Link 组件用于创建路由链接,它有一个参数to, 它的值可以是字符串还可以是一个location对象。

    1. 字符串形式

      1
      <Link to="/about">关于我们</Link>
    2. 对象形式

      类型声明,更多介绍请查看官网

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      declare function Link(props: LinkProps): React.ReactElement;

      interface LinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
      to: To;
      preventScrollReset?: boolean;
      relative?: 'route' | 'path';
      reloadDocument?: boolean;
      replace?: boolean;
      state?: any;
      unstable_viewTransition?: boolean;
      }

      type To = string | Partial<Path>;

      interface Path {
      pathname: string;
      search: string;
      hash: string;
      }

      使用示例:

      1
      2
      3
      <Link to={{ pathname: '/about', search: '?name=zhangsan' }} state={{ some: 'value' }}>
      关于我们
      </Link>

    NavLink 组件是 Link 组件的变体,可以添加一些样式来区分当前页面。

    一共两种形式,如下代码示例 👇

    1. 通过 style 的形式

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      <NavLink
      to="/about"
      style={({ isActive }) => {
      return {
      color: isActive ? 'red' : '#000',
      fontWeight: 'bold',
      };
      }}
      >
      首页
      </NavLink>
    2. 通过 css 的形式

      1
      2
      3
      4
      5
      6
      7
      8
      <NavLink
      to="/about"
      className={({ isActive }) => {
      return isActive ? 'active' : '';
      }}
      >
      首页
      </NavLink>

    3.5 Navigate 组件

    Navigate 组件是对旧版本的 Redirect 的替代品。

    如下代码示例 👇

    1
    2
    import { Navigate } from 'react-router-dom';
    <Route path="/" element={<Navigate replace to="/home" />} />;
    • 其中replace属性也可以省略,不过路由的行为由 replace 改为 push

    • replace Vs push

    • replace 替换当前 history 记录,没有历史记录

    • push 添加新的 history 记录,可以回退到上一页

    3.6 Outlet 组件

    Outlet组件用于在路由被匹配时,渲染匹配到的路由组件。用于占位,告诉 React Router 嵌套的内容应该显示在哪里。

    如下代码示例 👇

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    export default function App() {
    return (
    <div className="container">
    <h2>关于我们</h2>
    <ul>
    <li>
    <Link to="/about/address">公司地址</Link>
    </li>
    <li>
    <Link to="/about/join">加入我们</Link>
    </li>
    </ul>
    <div className="boxs">
    <Outlet />
    </div>
    </div>
    );
    }

    4. 声明式路由

    4.1 useRoutes 声明式的路由配置方式

    在声明式路由中,不能写index, 但可以让 path: “” , 来实现显示默认组件;

    如下代码示例 👇

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    const MyRoutes = () => {
    return useRoutes([
    {
    path: '/',
    element: <Home />,
    },
    {
    path: '/home',
    element: <Home />,
    },
    {
    path: '/about',
    element: <About />,
    children: [
    {
    path: '',
    element: <Story />,
    },
    {
    path: 'address',
    element: <Address />,
    },
    ],
    },
    ]);
    };
    function App() {
    return (
    <div>
    <Router>
    <MyRoutes />
    </Router>
    </div>
    );
    }
    export default App;

    5. 编程式导航

    编程式导航就是通过 JS 代码来控制路由的跳转,包括路由的跳转、路由的参数传递、路由的钩子函数等。

    5.1 路由跳转useNavigate函数

    useNavigate 函数用于获取路由导航的函数,该函数可以接收一个参数,表示要跳转到的路由地址。同时新版本中移除了useHistory 函数。

    • params传参方式,地址会如:/home/1。
    • search传参方式,地址会如:/home?id=1。
    • state传参方式,地址栏中不会显示

    如下代码示例 👇

    1
    2
    3
    import { useNavigate } from 'react-router-dom';
    const navigate = useNavigate();
    navigate('/home'); // 默认是以push的形式跳转

    5.1.1 push的方式

    跳转到指定路由,并生成历史记录

    • 携带params参数

      路由设计格式为:<Route path="about" element={<About/>} />

      1
      navigate(`/home/${id}`);
    • 携带search参数

      路由设计格式为:<Route path="about/:id/:keyword" element={<About/>} />

      1
      navigate(`/home/${id}/${keyword}`);
    • 携带state参数

      路由设计格式,以上两种方式都可以

      1. 第一种

        1
        navigate('/home', { state: { id: '1' } });
      2. 第二种

        1
        2
        3
        4
        5
        6
        7
        8
        <NavLink
        to={`detail`}
        state={{
        id: 1,
        }}
        >
        首页
        </NavLink>

    5.1.2 replace方式

    跳转到指定路由,并替换当前历史记录

    • 携带params参数

      1
      navigate(`/home/${id}`, { replace: true });
    • 携带search参数

      1
      navigate(`/home/${id}/${keyword}`, { replace: true });
    • 携带state参数

      1
      navigate('/home', { state: { id: '1' }, replace: true });

    5.1.3 返回上一页

    使用navigate(-1)后退到前一页,navigate(-2)后退到前两页。

    5.2 获取路由参数

    通过React Router提供的函数,从而获取解析地址栏中的参数。

    注意:在最新版路由中组件不能直接从props中获取参数

    5.2.1 useParams 函数

    useParams 函数用于获取地址栏中的params参数,其路由地址参考为localhost:3000/home/1。

    如下代码示例 👇

    1
    2
    3
    4
    5
    6
    7
    import { useParams } from 'react-router-dom';
    function Home() {
    // 当前路径/home/1
    const params = useParams();
    console.log(params); // 输出:{ id: '1' }
    return <div>Home</div>;
    }

    5.2.2 useSearchParams 函数

    useSearchParams 函数用于获取地址栏中的search参数,其路由地址参考为localhost:3000/home?id=1。

    其中返回的searchParams函数,具有以下方法:

    • searchParams:表示地址栏中的search参数,通过.get方法获取指定数据。

    • setSearchParams:表示设置地址栏中的search参数。

    • 在更改地址栏中的search参数时,必须传入所有的查询参数,否则会覆盖已经有的参数

    如下代码示例 👇

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import { useSearchParams } from 'react-router-dom';

    function Home() {
    // 当前路径为/home?id=1
    const [searchParams, setSearchParams] = useSearchParams();
    console.log(searchParams.get('id')); // 输出:1
    // setSearchParams({ id: '2' }); // 设置地址栏中的search参数为id=2
    return (
    <div>
    <h1>Home</h1>
    <button onClick={() => setSearchParams({ id: 2 })}>修改search参数</button>
    </div>
    );
    }

    5.2.3 useLocation 函数

    useLocation 函数用于获取地址栏中的search和params参数。
    返回的location对象,具有以下属性:

    • pathname:表示地址栏中的路径,地址如:/home?id=1。
    • search:表示地址栏中的search参数,地址如:/home/1。
    • state:表示传递的state参数,该参数不会在地址栏中展示。

    如下代码示例 👇

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import { useLocation } from 'react-router-dom';
    function Home() {
    const location = useLocation();
    console.log(location.pathname); // 输出:/home
    console.log(location.search); // 输出:?id=1&keyword=react
    console.log(location.state); // 输出:{ id: '1' }
    // 可以直接结构
    const {
    pathname,
    search,
    state: { id },
    } = useLocation();
    console.log(id); // 输出:1
    return <div>Home</div>;
    }

    5.3 类组件中获取路由参数

    在类组件中不可以和函数组件使用的方式一致,需要通过HOC高阶组件封装,如直接使用会报错。
    报错信息:
    Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:

    1. You might have mismatching versions of React and the renderer (such as React DOM)
    2. You might be breaking the Rules of Hooks
    3. You might have more than one copy of React in the same app.

    大致意思就是说无效的hooks调用,hooks只能在函数组件的主体内部调用,那么已经使用了class组件的形式,又不想修改为函数组件,要如何使用呢?答案就是使用HOC高阶组件包装一下。封装如下HOC代码 👇

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import { useParams, useSearchParams, useLocation, useNavigate } from 'react-router-dom';

    function withRouter(Component) {
    return function RouterProps(props) {
    const params = useParams();
    const location = useLocation();
    const searchParams = useSearchParams();
    const navigate = useNavigate();
    return <Component {...props} router={{ params, location, searchParams, navigate }} />;
    };
    }

    使用方式如下 👇

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import React from 'react';
    import withRouter from './withRouter';

    class ClassComponent extends React.Component {
    componentDidMount() {
    const params = this.props.params;
    console.log('params:', params);
    }
    }
    export default withRouter(ClassComponent);

    6. 路由懒加载

    在React中,使用import()函数来实现路由的懒加载。通过import()函数,可以异步加载路由组件,提高页面加载速度。

    1. 创建一个Suspense组件,用于包裹需要懒加载的路由组件。

    2. 使用@loadable/component库来加载路由组件。

      • 👉 安装依赖
      1
      2
      3
      4
      # npm的形式
      npm install @loadable/component
      # yarn的形式
      yarn add @loadable/component
      • 如下代码示例 👇
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      import React from 'react';
      // 导入@loadable/component,来加载路由
      import loadable from '@loadable/component';

      const Home = loadable(() => import('...component path'));
      const About = loadable(() => import('...component path'));

      function App() {
      return (
      <div>
      <BrowserRouter>
      <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
      </Routes>
      </BrowserRouter>
      </div>
      );
      }

      export default App;
      • 动态路由的形式如下 👇
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      import React from 'react';
      import { BrowserRouter, Routes, useRoutes } from 'react-router-dom';
      import loadable from '@loadable/component';
      // 假设动态
      const menuList = [
      {
      path: '/index',
      name: '首页',
      elementPath: 'index',
      },
      {
      path: '/user',
      name: '用户管理',
      elementPath: '/user/index',
      children: [
      {
      path: '',
      name: '用户列表',
      elementPath: 'user/list',
      },
      {
      path: '/user/add',
      name: '添加用户',
      elementPath: 'user/add',
      },
      ],
      },
      ];

      function bindRouter(list) {
      let arr = [];
      list.map((item) => {
      const ComponentNode = loadable(() => import(`@/pages/${item.elementPath}`));
      if (item.children && item.children.length > 0) {
      arr.push({
      path: item.path,
      element: <ComponentNode />,
      children: [...bindRouter(item.children)],
      });
      } else {
      arr.push({
      path: item.path,
      element: <ComponentNode />,
      });
      }
      });
      return arr;
      }

      function App() {
      const route = bindRouter(menuList);
      const GetRoute = useRoutes(route);
      return (
      <div>
      <BrowserRouter>
      <Routes>
      <GetRoute />
      </Routes>
      </BrowserRouter>
      </div>
      );
      }
      export default App;

    到这里相信你也已经了解了,React Router的使用方式了,是不是很简单呢,接下来就可以开始你的业务代码了,如果本文对你有帮助,还请不要吝啬你的 👍。

  • 前端常见高频面试题

    1. 什么是 MVVMMVC 模型?

    MVC: MVCmodel-view-controller(模型-视图-控制器)是项目的一种分层架构思想,它把复杂的业务逻辑, 抽离为职能单一的小模块,每个模块看似相互独立,其实又各自有相互依赖关系。它的好处是: 保证了模块的智能单一性,方便程序的开发、维护、耦合度低。

    MVVM: MVVMModel-View-ViewModel,(模型-视图-控制器)它是一种双向数据绑定的模式, 用 viewModel 来建立起 model 数据层和 view 视图层的连接,数据改变会影响视图,视图改变会影响数据

    2. Vue 双向数据绑定的原理?

    vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

    具体步骤:

    第一步: 需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和getter 这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化

    第二步: compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图

    第三步: Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情是:

    1. 在自身实例化时往属性订阅器(dep)里面添加自己

    2. 自身必须有一个 update() 方法

    3. 待属性变动 dep.notice() 通知时,能调用自身的 update() 方法,并触发 Compile 中绑定的回调,则功成身退。

    第四步: MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过 Observer 来监听自己的 model 数据变化, 通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化 -> 视图更新; 视图交互变化(input) -> 数据 model 变更的双向绑定效果。

    3. vue 的生命周期有哪些?

    vue 实例从创建到销毁的过程就是生命周期。 也就是从开始创建、初始化数据、编译模板、挂在 dom -> 渲染、更新 -> 渲染、准备销毁、销毁等一系列过程 vue 的声明周期常见的主要分为4 大阶段 8 大钩子函数 另外三个生命周期函数不常用 keep-alive 主要用于保留组件状态或避免重新渲染。activated只有在keep-alive组件激活时调用。deactivated只有在keep-alive组件停用时调用。errorCapured当捕获一个来自子孙组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 false 以阻止该错误继续向上传播。

    1. 创建前 / 后 在beforeCreate生命周期函数执行的时候,data 和 method 还没有初始化 在created 生命周期函数执行的时候, data 和 method 已经初始化完成

    2. 渲染前/后 在beforeMount 生命周期函数执行的时候,已经编译好了模版字符串、但还没有真正渲染到页面中去 在mounted 生命周期函数执行的时候,已经渲染完,可以看到页面

    3. 数据更新前/后 在beforeUpdate生命周期函数执行的时候,已经可以拿到最新的数据,但还没渲染到视图中去。 在updated生命周期函数执行的时候,已经把更新后的数据渲染到视图中去了。

    4. 销毁前/后 在beforeDestroy 生命周期函数执行的时候,实例进入准备销毁的阶段、此时 data 、methods 、指令 等还是可用状态 在destroyed生命周期函数执行的时候,实例已经完成销毁、此时 data 、methods 、指令等都不可用

    4. v-if 和 v-show 有什么区别?

    v-if 是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建, 操作的实际上是 dom 元素的创建或销毁。

    v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换, 它操作的是display:none/block属性。

    一般来说, v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show 较好; 如果在运行时条件很少改变,则使用 v-if 较好。

    5. async await 是什么?它有哪些作用?

    async await 是es7里面的新语法、它的作用就是 async 用于申明一个 function 是异步的,而 await 用 于等待一个异步方法执行完成。它可以很好的替代promise 中的 then

    async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。当函数执行的时候,一旦遇 到 await 就会先返回, 等到异步操作完成,再接着执行函数体内后面的语句。

    6. 常用的数组方法有哪些?

    concat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。

    find() 方法返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined 。

    findIndex() 方法返回数组中满足提供的测试函数的第一个元素的索引。否则返回-1。

    includes() 方法用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true, 否则返回 false。

    indexOf() 方法返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1。 (通常用它判断数组中有没有这个元素)

    join() 方法将一个数组(或一个类数组对象)的所有元素连接成一个字符串并返回这个字符串。 如果数组只有一个项目,那么将返回该项目而不使用分隔符。

    pop() 方法从数组中删除最后一个元素,并返回该元素的值。此方法更改数组的长度。

    push() 方法将一个或多个元素添加到数组的末尾,并返回该数组的新长度。

    shift() 方法从数组中删除第一个元素,并返回该元素的值。此方法更改数组的长度。

    unshift() 方法将一个或多个元素添加到数组的开头,并返回该数组的新长度(该方法修改原有数组)。

    splice() 方法通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内 容。此方法会改变原数组。 由被删除的元素组成的一个数组。如果只删除了一个元素,则返回只包含一个元素的数组。如果没有删 除元素,则返回空数组。

    slice() 方法同上,但不会改变原数组

    reverse() 方法将数组中元素的位置颠倒,并返回该数组。该方法会改变原数组。

    sort() 方法用原地算法对数组的元素进行排序,并返回数组。默认排序顺序是在将元素转换为字 符串,然后比较它们的 UTF-16 代码单元值序列时构建的

    7. 数组有哪几种循环方式?分别有什么作用?

    every() 方法测试一个数组内的所有元素是否都能通过某个指定函数的测试。它返回一个布尔 值。

    filter() 方法创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。

    • 注意: filter() 不会对空数组进行检测。

    • 注意: filter() 不会改变原始数组。

    forEach() 方法对数组的每个元素执行一次提供的函数。

    some() 方法测试是否至少有一个元素可以通过被提供的函数方法。该方法返回一个 Boolean 类型 的值。

    8. 常用的字符串方法有哪些?

    charAt() 方法从一个字符串中返回指定的字符。

    concat() 方法将一个或多个字符串与原字符串连接合并,形成一个新的字符串并返回。

    includes() 方法用于判断一个字符串是否包含在另一个字符串中,根据情况返回 true 或 false。

    indexOf() 方法返回调用它的 String 对象中第一次出现的指定值的索引,从 fromIndex 处进行搜 索。如果未找到该值,则返回 -1。 match() 方法检索返回一个字符串匹配正则表达式的的结果。

    padStart() 方法用另一个字符串填充当前字符串(重复,如果需要的话),以便产生的字符串达到给定的 长度。填充从当前字符串的开始(左侧)应用的。 (常用于时间补 0)

    replace() 方法返回一个由替换值( replacement )替换一些或所有匹配的模式( pattern )后的新 字符串。模式可以是一个字符串或者一个正则表达式,替换值可以是一个字符串或者一个每次匹配都要 调用的回调函数。 原字符串不会改变。 slice() 方法提取某个字符串的一部分,并返回一个新的字符串,且不会改动原字符串。

    split() 方法使用指定的分隔符字符串将一个 String 对象分割成字符串数组,以将字符串分隔为 子字符串,以确定每个拆分的位置。 substr() 方法返回一个字符串中从指定位置开始到指定字符数的字符。 trim() 方法会从一个字符串的两端删除空白字符。在这个上下文中的空白字符是所有的空白字符 (space, tab, no-break space 等) 以及所有行终止符字符(如 LF,CR)。

    9. 什么是原型链?

    每一个实例对象上有一个proto 属性,指向的构造函数的原型对象,构造函数的原型 对象也是一个对象, 也有 proto 属性,这样一层一层往上找的过程就形成了原型链。

    10. 什么是闭包?手写一个闭包函数? 闭包有哪些优缺点?

    闭包(closure)指有权访问另一个函数作用域中变量的函数。简单理解就是 ,一个作用 域可以访问另外一个函数内部的局部变量。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function fn() {
    var num = 10
    function fun() {
    console.log(num)
    }
    return fun
    }
    var f = fn()
    f()

    作用: 延长变量作用域、在函数的外部可以访问函数内部的局部变量,容易造成内层泄露,因为闭包中 的局部变量永远不会被回收

    11. 常见的继承有哪些?

    1. 原型链继承

    特点: 实例可继承的属性有:实例的构造函数的属性,父类构造函数属性,父类原型的属性。(新 实例不会继承父类实例的属性!)

    缺点:

    1. 新实例无法向父类构造函数传参。

    2. 继承单一。

    3. 所有新实例都会共享父类实例的属性。(原型上的属性是共享的,一个实例修改了原型属 性,另一个实例的原 型属性也会被修改!)

    2. 借用构造函数继承

    重点: 用 .call()和.apply() 将父类构造函数引入子类函数(在子类函数中做了父类函数的自执行(复 制))

    特点:

    1. 只继承了父类构造函数的属性,没有继承父类原型的属性。

    2. 解决了原型链继承缺点 1、2、3。

    3. 可以继承多个构造函数属性(call 多个)。

    4. 在子实例中可向父实例传参。

    缺点:

    1. 只能继承父类构造函数的属性。

    2. 无法实现构造函数的复用。(每次用每次都要重新调用)

    3. 每个新实例都有父类构造函数的副本,臃肿。

    3. 组合继承(组合原型链继承和借用构造函数继承)(常用)

    重点: 结合了两种模式的优点,传参和复用

    特点:

    1. 可以继承父类原型上的属性,可以传参,可复用。

    2. 每个新实例引入的构造函数属性是私有的。

    缺点: 调用了两次父类构造函数(耗内存),子类的构造函数会代替原型上的那个父类构造函数。

    4. 原型式继承

    重点: 用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的 实例或对象。object.create()就是这个原理。

    特点: 类似于复制一个对象,用函数来包装。

    缺点:

    1. 所有实例都会继承原型上的属性。

    2. 无法实现复用。(新实例属性都是后面添加的)

    5. class 类实现继承

    通过 extends 和 super 实现继承

    6. 寄生式继承

    重点: 就是给原型式继承外面套了个壳子。

    优点: 没有创建自定义类型,因为只是套了个壳子返回对象(这个),这个函数顺理成章就成 了创建的新对象。

    缺点: 没用到原型,无法复用。

    12.后台管理系统中的权限管理是怎么实现的?

    登录: 当用户填写完账号和密码后向服务端验证是否正确,验证通过之后,服务端会返回一个 token,

    拿到 token 之后(我会将这个 token 存贮到 cookie 中,保证刷新页面后能记住用户登录状态),前端会 根据 token 再去拉取一个 user_info 的接口来获取用户的详细信息(如用户权限,用户名等等信息)。

    权限验证: 通过 token 获取用户对应的 权限,动态根据用户的 权限算出其对应有权限的路由,通过 router.addRoutes 动态挂载这些路由。

    具体思路:

    登录成功后,服务端会返回一个 token(该 token 的是一个能唯一标示用户身份的一个 key),之后我 们将 token 存储在本地 cookie 之中,这样下次打开页面或者刷新页面的时候能记住用户的登录状态,不 用再去登录页面重新登录了。

    ps:为了保证安全性,我司现在后台所有 token 有效期(Expires/Max-Age)都是 Session,就是当浏览器关 闭了就丢失了。重新打开游览器都需要重新登录验证,后端也会在每周固定一个时间点重新刷新 token,让后台用户全部重新登录一次,确保后台用户不会因为电脑遗失或者其它原因被人随意使用账号。

    用户登录成功之后,我们会在全局钩子 router.beforeEach 中拦截路由,判断是否已获得 token,在 获得 token 之后我们就要去获取用户的基本信息了 页面会先从 cookie 中查看是否存有 token,没有,就走一遍上一部分的流程重新登录,如果有 token, 就会把这个 token 返给后端去拉取 user_info,保证用户信息是最新的。 当然如果是做了单点登录得功 能的话,用户信息存储在本地也是可以的。当你一台电脑登录时,另一台会被提下线,所以总会重新登 录获取最新的内容。

    先说一说我权限控制的主体思路,前端会有一份路由表,它表示了每一个路由可访问的权限。当用户登 录之后,通过 token 获取用户的 role ,动态根据用户的 role 算出其对应有权限的路由,再通过 router.addRoutes 动态挂载路由。但这些控制都只是页面级的,说白了前端再怎么做权限控制都不是 绝对安全的,后端的权限验证是逃不掉的。

    我司现在就是前端来控制页面级的权限,不同权限的用户显示不同的侧边栏和限制其所能进入的页面(也 做了少许按钮级别的权限控制),后端则会验证每一个涉及请求的操作,验证其是否有该操作的权限,每 一个后台的请求不管是 get 还是 post 都会让前端在请求 header 里面携带用户的 token,后端会根据 该 token 来验证用户是否有权限执行该操作。若没有权限则抛出一个对应的状态码,前端检测到该状 态码,做出相对应的操作。 使用 vuex 管理路由表,根据 vuex 中可访问的路由渲染侧边栏组件。

    具体实现:

    创建 vue 实例的时候将vue-router挂载,但这个时候 vue-router 挂载一些登录或者不用权限的公用的页面。

    当用户登录后,获取用role,将 role 和路由表每个页面的需要的权限作比较,生成最终用户可访问的路由表。

    调用router.addRoutes(store.getters.addRouters)添加用户可访问的路由。

    使用vuex管理路由表,根据 vuex 中可访问的路由渲染侧边栏组件。

    14. es6 有哪些新特性?

    ES6 是 2015 年推出的一个新的版本、这个版本相对于 ES5 的语法做了很多的优化、例如:新增了let、 const

    let 和 const具有块级作用域,不存在变量提升的问题。新增了箭头函数,简化了定义函数的写法,

    同时 可以巧用箭头函数的 this、(注意箭头函数本身没有 this,它的 this 取决于外部的环境),

    新增了promise 解决了回调地域的问题,新增了模块化、利用 import 、export 来实现导入、导出。

    新增了结构赋值, ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构 (Destructuring)。

    新增了class 类的概念,它类似于对象。

    15. v-for 循环为什么一定要绑定 key ?

    页面上的标签都对应具体的虚拟 dom 对象(虚拟 dom 就是 js 对象), 循环中 ,如果没有唯一 key , 页面上删除 一条标签, 由于并不知道删除的是那一条! 所以要把全部虚拟 dom 重新渲染, 如果知道 key 为 x 标签被删除 掉, 只需要把渲染的 dom 为 x 的标签去掉即可!

    16. 组件中的 data 为什么要定义成一个函数而不是一个对象?

    每个组件都是 Vue 的实例。组件共享 data 属性,当 data 的值是同一个引用类型的值时,改变其中一 个会影响其他

    17. 常见的盒子垂直居中的方法有哪些请举例 3 种?

    1. 利用子绝父相定位的方式来实现

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      #container {
      width: 500px;
      height: 500px;
      position: relative;
      }
      #center {
      width: 100px;
      hight: 100px;
      position: absolute;
      top: 50%;
      left: 50%;
      margin-top: -50px;
      margin-left: -50px;
      }
    2. 利用 Css3 的 transform,可以轻松的在未知元素的高宽的情况下实现元素的垂直居中。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      #container {
      position: relative;
      }
      #center {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      }
    3. flex

      1
      2
      3
      4
      5
      6
      7
      #container {
      display: flex;
      justify-content: center;
      align-items: center;
      }
      #center {
      }

    18. 平时都是用什么实现跨域的?

    jsonp: 利用 标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。JSONP 请求一 定需要对方的服务器做支持才可以。

    JSONP 优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持 get 方法具 有局限性,不安全可能会遭受 XSS 攻击。

    声明一个回调函数,其函数名(如 show)当做参数值,要传递给跨域请求数据的服务器,函数形参为要获 取目标数据(服务器返回的 data)。 创建一个<script>标签,把那个跨域的 API 数据接口地址,赋值给 script 的 src,还要在这个地址中向服 务器传递该函数名(可以通过问号传参:?callback=show)。

    服务器接收到请求后,需要进行特殊的处理:把传递进来的函数名和它需要给你的数据拼接成一个字符 串,例如:传递进去的函数名是 show,它准备好的数据是 show(‘我不爱你’) 。

    最后服务器把准备的数据通过 HTTP 协议返回给客户端,客户端再调用执行之前声明的回调函数 (show),对返回的数据进行操作。

    CORS:跨域资源共享(CORS)是一种机制;当一个资源访问到另外一个资源(这个资源放在 不同的域名或者不同的协议或者端口),资源就会发起一个跨域的 HTTP 请求需要浏览器和服务器同时支持;

    1. 整个 CORS 通信,都是浏览器自动完成。浏览器发现了 AJAX 请求跨源,就会自动添加一些附加的头 信息,有时还会多出一次附加的请求,但用户不会有感觉;

    2. 实现 CORS 的关键是服务器,只要服务器实现了 CORS 接口,就可以跨源通信

    3. 服务器对于不同的请求,处理方式不一样; 有简单请求和非简单请求

    • 与服务器交互:

      • cookie 是网站为了标示用户身份而储存在用户本地终端上的数据(通常经过加密)

      • cookie 始终会在同源 http 请求头中携带(即使不需要),在浏览器和服务器间来回传递

      • sessionStorage 和 localStorage 不会自动把数据发给服务器,仅在本地保存

    • 存储大小:

      • cookie 数据根据不同浏览器限制,大小一般不能超过 4k

      • sessionStorage 和 localStorage 虽然也有存储大小的限制,但比 cookie 大得多,可以达到 5M 或更大

    • 有期时间:

      • localStorage 存储持久数据,浏览器关闭后数据不丢失除非主动删除数据

      • sessionStorage 数据在当前浏览器窗口关闭后自动删除

      • cookie 设置的 cookie 过期时间之前一直有效,与浏览器是否关闭无关

    20. this 的指向有哪些?

    1. 普通函数中的 this 指向 window

    2. 定时器中的 this 指向 window

    3. 箭头函数没有 this,它的 this 指向取决于外部环境

    4. 事件中的 this 指向事件的调用者

    5. 构造函数中 this 和原型对象中的 this,都是指向构造函数 new 出来实例对象

    6. 类 class 中的 this 指向由 constructor 构造器 new 出来的实例对象

    7. 自调用函数中的 this 指向 window

    21. 什么是递归,递归有哪些优点或缺点?

    递归: 如果一个函数在内部可以调用其本身,那么这个函数就是递归函数。简单理解:函 数内部自己调用自己, 这个函数就是递归函数

    优点: 结构清晰、可读性强

    缺点: 效率低、调用栈可能会溢出,其实每一次函数调用会在内存栈中分配空间,而每个进程的栈的容 量是有限的,当调用的层次太多时,就会超出栈的容量,从而导致栈溢出->性能

    22. 谈谈你平时都用了哪些方法进行性能优化?

    减少 http 请求次数、打包压缩上线代码、使用懒加载、使用雪碧图、动态渲染组件、CDN 加载包。

    23. vue 实例是挂载到那个标签上的?

    vue 实例最后会挂载在body 标签里面,所以我们在 vue 中是获取不了 body 标签的,如果要使用 body 标 签的话需要用原生的方式获取

    24. 什么是深拷贝、什么是浅拷贝?

    浅拷贝: 创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝 的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个 地址,就会影响到另一个对象。

    深拷贝: 会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即 发生深拷贝。 深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响。

    25. js 的执行机制是怎么样的?

    js 是一个单线程、异步、非阻塞 I/O 模型、 event loop 事件循环的执行机制

    所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。

    同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。异步 任务指的是,不进入主线程、而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程, 某个异步任务可以执行了,该任务才会进入主线程执行。

    26. 请写至少三种数组去重的方法?(原生 js)

    1.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    //利用filter
    function unique(arr) {
    return arr.filter(function (item, index, arr) {
    //当前元素,在原始数组中的第一个索引==当前索引值,否则返回当前元素
    return arr.indexOf(item, 0) === index
    })
    }
    var arr = [
    1,
    1,
    'true',
    'true',
    true,
    true,
    15,
    15,
    false,
    false,
    undefined,
    undefined,
    null,
    null,
    NaN,
    NaN,
    'NaN',
    0,
    0,
    'a',
    'a',
    {},
    {}
    ]
    console.log(unique(arr))

    2.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    //利用ES6 Set去重(ES6中最常用)
    function unique(arr) {
    return Array.from(new Set(arr))
    }
    var arr = [
    1,
    1,
    'true',
    'true',
    true,
    true,
    15,
    15,
    false,
    false,
    undefined,
    undefined,
    null,
    null,
    NaN,
    NaN,
    'NaN',
    0,
    0,
    'a',
    'a',
    {},
    {}
    ]
    console.log(unique(arr))
    //[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}, {}]

    3.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    //利用for嵌套for,然后splice去重(ES5中最常用)
    function unique(arr) {
    for (var i = 0; i < arr.length; i++) {
    for (var j = i + 1; j < arr.length; j++) {
    if (arr[i] == arr[j]) {
    //第一个等同于第二个,splice方法删除第二个
    arr.splice(j, 1)
    j--
    }
    }
    }
    return arr
    }
    var arr = [
    1,
    1,
    'true',
    'true',
    true,
    true,
    15,
    15,
    false,
    false,
    undefined,
    undefined,
    null,
    null,
    NaN,
    NaN,
    'NaN',
    0,
    0,
    'a',
    'a',
    {},
    {}
    ]
    console.log(unique(arr))
    //[1, "true", 15, false, undefined, NaN, NaN, "NaN", "a", {…}, {…}]
    //NaN和{}没有去重,两个null直接消失了

    27. 请写出至少两种常见的数组排序的方法(原生 js)

    1.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    //快速排序
    function quickSort(elements) {
    if (elements.length <= 1) {
    return elements
    }
    var pivotIndex = Math.floor(elements.length / 2)
    var pivot = elements.splice(pivotIndex, 1)[0]
    var left = []
    var right = []
    for (var i = 0; i < elements.length; i++) {
    if (elements[i] < pivot) {
    left.push(elements[i])
    } else {
    right.push(elements[i])
    }
    }
    return quickSort(left).concat([pivot], quickSort(right))
    //concat()方法用于连接两个或者多个数组;该方法不会改变现有的数组,而仅仅会返回被连接数组的一个副本。
    }
    var elements = [3, 5, 6, 8, 2, 4, 7, 9, 1, 10]
    document.write(quickSort(elements))

    2.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    //插入排序
    function sort(elements) {
    // 假设第0个元素是一个有序数列,第1个以后的是无序数列,
    // 所以从第1个元素开始将无序数列的元素插入到有序数列中去
    for (var i = 1; i <= elements.length; i++) {
    // 升序
    if (elements[i] < elements[i - 1]) {
    // 取出无序数列中的第i个作为被插入元素
    var guard = elements[i]
    //记住有序数列的最后一个位置,并且将有序数列的位置扩大一个
    var j = i - 1
    elements[i] = elements[j]
    // 比大小;找到被插入元素所在位置
    while (j >= 0 && guard < elements[j]) {
    elements[j + 1] = elements[j]
    j--
    }
    elements[j + 1] = guard //插入
    }
    }
    }
    var elements = [3, 5, 6, 8, 2, 4, 7, 9, 1, 10]
    document.write('没调用之前:' + elements)
    document.write('<br>')
    sort(elements)
    document.write('被调用之后:' + elements)

    3.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    //冒泡排序
    function sort(elements) {
    for (var i = 0; i < elements.length - 1; i++) {
    for (var j = 0; j < elements.length - 1 - i; j++) {
    if (elements[j] > elements[j + 1]) {
    var swap = elements[j]
    elements[j] = elements[j + 1]
    elements[j + 1] = swap
    }
    }
    }
    }
    var elements = [3, 5, 6, 8, 2, 4, 7, 9, 1, 10]
    console.log('before' + elements)
    sort(elements)
    console.log('after' + elements)

    28. 知道 lodash 吗?它有哪些常见的 API ?

    官网

    Lodash 是一个一致性、模块化、高性能的 JavaScript 实用工具库。

    _.cloneDeep 深度拷贝

    _.reject 根据条件去除某个元素。

    _.drop(array, [n=1] ) 作用:将 array 中的前 n 个元素去掉,然后返回剩余的部分.

    29. http 的请求方式有哪些?

    get、post、put、delete 等

    30. 平时都是用那些工具进行打包的?babel 是什么?

    WebPack 是一个模块打包工具,你可以使用 WebPack 管理你的模块依赖,并编绎输出模块们所需的静 态文件。它能够很好地管理、打包 Web 开发中所用到的 HTML、Javascript、CSS 以及各种静态文件(图 片、字体等),让开发过程更加高效。对于不同类型的资源,webpack 有对应的模块加载器。

    webpack 模块打包器会分析模块间的依赖关系,最后 生成了优化且合并后的静态资源 babel可以帮助我们转换一些当前浏览器不支持的语法,它会把这些语法转换为低版本的语法以便浏览 器识别。

    31. 谈谈 set 、 map 是什么?

    set 是 es6 提供的一种新的数据结构,它类似于数组,但是成员的值都是唯一的。

    map 是 es6 提供的一种新的数据结构,它类似于对象,也是键值对的集合,但是键的范围不仅限于字符 串,各种类型的值都可以当做键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供 了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。

    32. 清除浮动的方法有哪些?

    为什么要清除浮动,因为浮动的盒子脱离标准流,如果父盒子没有设置高度的话,下面的盒子就会撑上 来。

    1. 额外标签法(在最后一个浮动标签后,新加一个标签,给其设置 clear:both;)(不推荐)

    2. 父级添加 overflow 属性(父元素添加 overflow:hidden)(不推荐)

    3. 使用 after 伪元素清除浮动(推荐使用)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      .clearfix:after{/*伪元素是行内元素 正常浏览器清除浮动方法*/
      content: "";
      display: block;
      height: 0;
      clear:both;
      visibility: hidden;
      }
      .clearfix{
      *zoom: 1;/*ie6清除浮动的方式 *号只有IE6-IE7执行,其他浏览器不执行*/

    4. 使用 before 和 after 双伪元素清除浮动

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      .clearfix:after,
      .clearfix:before {
      content: '';
      display: table;
      }
      .clearfix:after {
      clear: both;
      }
      .clearfix {
      *zoom: 1;
      }

    33. 常见的布局方法有哪些?他们的优缺点是什么?

    页面布局常用的方法有浮动、定位、flex、grid 网格布局、栅格系统布局

    浮动: 优点:兼容性好。 缺点:浮动会脱离标准文档流,因此要清除浮动。我们解决好这个问题即可。

    绝对定位 优点:快捷。 缺点:导致子元素也脱离了标准文档流,可实用性差。

    flex 布局(CSS3 中出现的) 优点:解决上面两个方法的不足,flex 布局比较完美。移动端基本用 flex 布局。

    网格布局(grid) CSS3 中引入的布局,很好用。代码量简化了很多。

    利用网格布局实现的一个左右 300px 中间自适应的布局

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <title>Document</title>
    <style>
    html * {
    padding: 0;
    margin: 0;
    }
    /* 重要:设置容器为网格布局,宽度为100% */
    .layout.grid .left-center-right {
    display: grid;
    width: 100%;
    grid-template-rows: 100px;
    grid-template-columns: 300px auto 300px; /* 重要:设置网格为三列,
    并设置每列的宽度。即可。*/
    }
    .layout.grid .left {
    background: red;
    }
    .layout.grid .center {
    background: green;
    }
    .layout.grid .right {
    background: blue;
    }
    </style>
    </head>
    <body>
    <section class="layout grid">
    <article class="left-center-right">
    <div class="left">我是 left</div>
    <div class="center">
    <h1>网格布局解决方案</h1>
    我是 center
    </div>
    <div class="right">我是 right</div>
    </article>
    </section>
    </body>
    </html>

    栅格系统布局 优点:可以适用于多端设备

    34. 图片懒加载是怎么实现的?

    就是我们先设置图片的 data-set 属性(当然也可以是其他任意的,只要不会发送 http 请求就行了,作用 就是为了存取值)值为其图片路径,由于不是 src,所以不会发送 http 请求。

    然后我们计算出页面 scrollTop 的高度和浏览器的高度之和, 如果图片距离页面顶端的坐标 Y(相对于整个页面,而不是浏览 器窗口)小于前两者之和,就说明图片就要显示出来了(合适的时机,当然也可以是其他情况),

    这时 候我们再将 data-set 属性替换为 src 属性即可。

    35. vue 中 computed 和 watch 的区别是什么?

    computed计算属性就是为了简化 template 里面模版字符串的计算复杂度、防止模版太过冗余。

    它具有 缓存特性 computed 用来监控自己定义的变量,该变量不在 data 里面声明,直接在 computed 里面定义,然后就 可以在页面上进行双向数据绑定展示出结果或者用作其他处理;

    watch主要用于监控 vue 实例的变化,它监控的变量当然必须在 data 里面声明才可以,它可以监控一个 变量,也可以是一个对象,一般用于监控路由、input 输入框的值特殊处理等等,它比较适合的场景是 一个数据影响多个数据,它不具有缓存性

    • watch:监测的是属性值, 只要属性值发生变化,其都会触发执行回调函数来执行一系列操作。

    • computed:监测的是依赖值,依赖值不变的情况下其会直接读取缓存进行复用,变化的情况下才 会重新计算。

    除此之外,有点很重要的区别是:计算属性不能执行异步任务,计算属性必须同步执行。也就是说计算 属性不能向服务器请求或者执行异步任务。如果遇到异步任务,就交给侦听属性。watch 也可以检测 computed 属性。

    36. vue 中是怎么实现父向子、子向父、兄弟之间的传值的?

    父向子传值主要通过的是 props 属性来传值,props 只读

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>父组件向子组件传值--props</title>
    <script src="./js/vue.min.js"></script>
    </head>
    <body>
    <div id="app">
    <menu-item title="来自父组件的值"></menu-item>
    <!-- 在子组件身上绑定自定义属性来接收父组件data中的数据 -->
    <menu-item :tit="title"></menu-item>
    </div>
    <script>
    Vue.component('menu-item', {
    props: ['tit'], //props用来接收父组件传过来的值
    //在props中使用驼峰形式,模版中要改为使用短横线拼接 props里面的值只读,不能修改
    //props是单向数据流
    data() {
    return {}
    },
    template: '<div>{{tit}}</div>'
    })
    var vm = new Vue({
    el: '#app',
    data: {
    title: '我是父组件中的数据'
    },
    methods: {}
    })
    </script>
    </body>
    </html>

    子向父传值 $emit

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <title>Document</title>
    </head>
    <body>
    <div id="app">
    <!-- 父组件 -->
    <div :style='{fontSize:fontSize+"px"}'>{{pmsg}}</div>
    <!-- 子组件 -->
    <menu-item :parr="parr" @aas="blune"></menu-item>
    </div>
    <script type="text/javascript" src="js/vue.js"></script>
    <script type="text/javascript">
    /*
    子组件向父组件传值-基本用法
    props传递数据原则:单向数据流
    */
    Vue.component('menu-item', {
    props: ['parr'],
    data() {
    return {
    msg1: '这是子组件传递过来的值'
    }
    },
    template: `
    <div>
    <ul>
    <li v-for="(item,index) in parr" :key="index">{{item}}</li>
    </ul>
    <button @click='dd'>扩大父组件中字体大小</button>
    </div>
    `,
    methods: {
    dd() {
    this.$emit('aas', this.msg1)
    }
    }
    })
    //$emit
    var vm = new Vue({
    el: '#app',
    data: {
    pmsg: '父组件中内容',
    parr: ['apple', 'orange', 'banana'],
    fontSize: 10
    },
    methods: {
    blune(message) {
    this.fontSize += 5
    console.log(message)
    }
    }
    })
    </script>
    </body>
    </html>

    兄弟组件传值 事件总线

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
    <script src="./js/vue.min.js"></script>
    </head>
    <body>
    <div id="app">
    <brother></brother>
    <sister></sister>
    </div>
    <script>
    var enveBus = new Vue()
    Vue.component('brother', {
    data() {
    return {
    kk: ''
    }
    },
    methods: {
    dd() {
    enveBus.$emit('bTs', '这是哥哥给妹妹的爱')
    }
    },
    template: `
    <div>
    <button @click='dd'>这是一个哥哥组件---{{kk}}</button>
    </div>
    `,
    mounted() {
    enveBus.$on('asd', (result) => {
    this.kk = result
    })
    }
    })
    Vue.component('sister', {
    data() {
    return {
    sis: ''
    }
    },
    template: `
    <div>
    <button @click="cc">这是一个妹妹组件---{{sis}}</button>
    </div>
    `,
    mounted() {
    enveBus.$on('bTs', (message) => {
    this.sis = message
    })
    },
    methods: {
    cc() {
    enveBus.$emit('asd', '这是妹妹对哥哥的爱')
    }
    }
    })
    var vm = new Vue({
    el: '#app',
    data: {},
    methods: {}
    })
    </script>
    </body>
    </html>

    37. 什么 vuex ,谈谈你对它的理解?

    1. 首先 vuex 的出现是为了解决 web 组件化开发的过程中,各组件之间传值的复杂和混乱的问题

    2. 将我们在多个组件中需要共享的数据放到 store 中,

    3. 要获取或格式化数据需要使用 getters,

    4. 改变 store 中的数据,使用 mutation,但是只能包含同步的操作,在具体组件里面调用的方式 this.$store.commit(‘xxxx’)

    5. Action也是改变store中的数据,不过是提交的mutation,并且可以包含异步操作,在组件中的调用方式this.$store.dispatch('xxx');在 actions 里面使用的commit('调用 mutation')

    38. 数据类型的判断有哪些方法?他们的优缺点及区别是什么?

    然后判断数据类型的方法一般可以通过:typeof、instanceof、constructor、toString四种常用方法

    不同类型的优缺点 typeof instanceof constructor Object.prototype.toString.call
    优点 使用简单 能检测出引用类型 基本能检测所有的类型(除了null和undefined) constructor易被修改也不能跨iframe
    缺点 只能检测出基本类型(出null) 不能检测出基本类型,且不能跨iframe constructor易被修改,也不能跨iframe IE6下,undefined和null均为Object

    39. 知道 symbol 吗?

    ES6 引入新的原始数据类型 Symbol,表示独一无二的值

    40. 请描述一下 ES6 中的 class 类?

    es6 中的 class 可以把它看成是 es5 中构造函数的语法糖,它简化了构造函数的写法, 类的共有属性放到 constructor 里面

    1. 通过 class 关键字创建类, 类名我们还是习惯性定义首字母大写

    2. 类里面有个 constructor 函数,可以接受传递过来的参数,同时返回实例对象

    3. constructor 函数 只要 new 生成实例时,就会自动调用这个函数, 如果我们不写这个函数,类也会自 动生成这个函数

    4. 多个函数方法之间不需要添加逗号分隔

    5. 生成实例 new 不能省略

    6. 语法规范, 创建类 类名后面不要加小括号,生成实例 类名后面加小括号, 构造函数不需要加 function

      1. 继承中,如果实例化子类输出一个方法,先看子类有没有这个方法,如果有就先执行子类的

      2. 继承中,如果子类里面没有,就去查找父类有没有这个方法,如果有,就执行父类的这个方法(就近 原则)

      3. 如果子类想要继承父类的方法,同时在自己内部扩展自己的方法,利用 super 调用 父类的构造函 数,super 必须在子类 this 之前调用

    7. 时刻注意 this 的指向问题,类里面的共有的属性和方法一定要加 this 使用.

      1. constructor 中的 this 指向的是 new 出来的实例对象

      2. 自定义的方法,一般也指向的 new 出来的实例对象

      3. 绑定事件之后 this 指向的就是触发事件的事件源

      4. 在 ES6 中类没有变量提升,所以必须先定义类,才能通过类实例化对象

    41. 谈谈盒子模型?

    在标准盒子模型中,width 和 height 指的是内容区域的宽度和高度。

    增加内边距、边框和外边距不会 影响内容区域的尺寸,但是会增加元素框的总尺寸。

    IE 盒子模型中,width 和 height 指的是内容区域+border+padding的宽度和高度。

    42. promise 是什么?它有哪些作用?

    Promise 是异步编程的一种解决方案.简单说就是一个容器,里面保存着某个未来才会结束的事件 (通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,可以从改对象获取异步操 作的消息。 它可以解决回调地狱的问题,也就是异步深层嵌套问题
    .catch() 获取异常信息
    .finally() 成功与否都会执行(不是正式标准)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    /*
    1. Promise基本使用
    我们使用new来构建一个Promise Promise的构造函数接收一个参数,是函数,并
    且传入两个参数: resolve,reject, 分别表示异步操作执行成功后的回调函数
    和异步操作执行失败后的回调函数
    */
    var p = new Promise(function (resolve, reject) {
    //2. 这里用于实现异步任务 setTimeout
    setTimeout(function () {
    var flag = false
    if (flag) {
    //3. 正常情况
    resolve('hello')
    } else {
    //4. 异常情况
    reject('出错了')
    }
    }, 100)
    })

    // 5 Promise实例生成以后,可以用then方法指定resolved状态和reject状态的回调函数

    // 在then方法中,你也可以直接return数据而不是Promise对象,在后面的then中就可以接收到数据了
    p.then(
    function (data) {
    console.log(data)
    },
    function (info) {
    console.log(info)
    }
    )

    43. vue-cli 2.0 和 3.0 有什么区别?

    3.0 把配置 webpack 的文件隐藏了,如果需要配置它需要创建一个 vue.config.js 文件,3.0 是 2018.10 月 出来的

    44. 箭头函数有哪些特征,请简单描述一下它?

    箭头函数没有自己的 this,this 指向定义箭头函数时所处的外部执行环境的 this

    即使调用call/apply/bind也无法改变箭头函数的 this 箭头函数本身没有名字 箭头函数不能 new,会报错

    箭头函数没有 arguments,在箭头函数内访问这个变量访问的是外部执行环境的 arguments 箭头函数没有 prototype

    45. 移动端有哪些常见的问题,都是怎么解决的?

    点击事件 300MS 延迟问题 解决方案:下载 fastclick 的包

    H5 页面窗口自动调整到设备宽度,并禁止用户缩放页面

    1
    <meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"

    忽略 Android 平台中对邮箱地址的识别

    1
    <meta name="format-detection" content="email=no" />

    当网站添加到主屏幕快速启动方式,可隐藏地址栏,仅针对 ios 的 safari

    1
    2
    <!-- ios7.0版本以后,safari上已看不到效果 -->
    <meta name="apple-mobile-web-app-capable" content="yes" />

    46. post 和 get 请求有哪些区别?

    GET:一般用于信息获取,使用 URL 传递参数,对所发送信息的数量也有限制,一般在 2000 个字符

    POST:一般用于修改服务器上的资源,对所发送的信息没有限制。

    GET 方式需要使用 Request.QueryString 来取得变量的值,而 POST 方式通过 Request.Form 来获取变量 的值,也就是说 Get 是通过地址栏来传值,而 Post 是通过提交表单来传值。

    然而,在以下情况中,请使用 POST 请求: 无法使用缓存文件(更新服务器上的文件或数据库) 向服务器发送大量数据(POST 没有数据量限制) 发送包含未知字符的用户输入时,POST 比 GET 更稳定也更可靠

    47. 什么是同源策略?

    所谓同源策略是浏览器的一种安全机制,来限制不同源的网站不能通信。同源就是域名、协议、端口一 致。

    48. http 状态码分别代表什么意思?

    1xx 表示 HTTP 请求已经接受,继续处理请求

    2xx 表示 HTTP 请求已经处理完成(200)

    3xx 表示把请求访 问的 URL 重定向到其他目录(304 资源没有发生变化,会重定向到本地资源)

    4xx 表示客户端出现错误 (403 禁止访问、404 资源不存在)

    5xx 表示服务端出现错误

    49. BFC 是什么?

    BFC(会计格式化上下文),一个创建了新的 BFC 的盒子是独立布局的,盒子内元素的布局不会影响盒 子外面的元素。在同一个 BFC 中的两个相邻的盒子在垂直方向发生 margin 重叠的问题。

    BFC 是值浏览器中创建了一个独立的渲染区域,该区域内所有元素的布局不会影响到区域外元素的布 局,这个渲染区域只对块级元素起作用

    50. token 是什么?(加密)

    1. token 也可以称做令牌,一般由 uid+time+sign(签名)+[固定参数] 组成

      • uid: 用户唯一身份标识

      • time: 当前时间的时间戳

      • sign: 签名, 使用 hash/encrypt 压缩成定长的十六进制字符串,以防止第三方恶意拼接

      • 固定参数(可选): 将一些常用的固定参数加入到 token 中是为了避免重复查库

    2. token 在客户端一般存放于 localStorage,cookie,或 sessionStorage 中。在服务器一般存于数据 库中

    3. token 的认证流程

      • 用户登录,成功后服务器返回Token给客户端。

      • 客户端收到数据后保存在客户端

      • 客户端再次访问服务器,将token放入headers中 或者每次的请求 参数中

      • 服务器端采用filter过滤器校验。校验成功则返回请求数据,校验失败则返回错误码

    4. token 可以抵抗 csrf,cookie+session 不行

    5. session 时有状态的,一般存于服务器内存或硬盘中,当服务器采用分布式或集群时,session 就会 面对负载均衡问题。负载均衡多服务器的情况,不好确认当前用户是否登录,因为多服务器不共享 session

    6. 客户端登陆传递信息给服务端,服务端收到后把用户信息加密(token)传给客户端,客户端将 token 存放于 localStroage 等容器中。客户端每次访问都传递 token,服务端解密 token,就知道这 个用户是谁了。通过 cpu 加解密,服务端就不需要存储 session 占用存储空间,就很好的解决负载 均衡多服务器的问题了。这个方法叫做 JWT(Json Web Token)

    51. js 的数据类型有哪些?

    js 的数据类型分为基本数据类型(string、number、boolean、null、undefined、symbol)和复杂数据类型

    基本数据类型的特点:直接存储在栈中的数据

    复杂数据类型的特点:存储的是该对象在栈中引用,真实的数据存放在堆内存里

    52. 一个页面从输入 URL 到页面加载显示完成,这个过程中都发生了什么?

    1. 浏览器查找域名对应的 IP 地址(DNS 查询:浏览器缓存->系统缓存->路由器缓存->ISP DNS 缓存->根 域名服务器)

    2. 浏览器向 Web 服务器发送一个 HTTP 请求(TCP 三次握手)

    3. 服务器 301 重定向(从 example.com 重定向到 www.example.com)

    4. 浏览器跟踪重定向地址,请求另一个带 www 的网址

    5. 服务器处理请求(通过路由读取资源)

    6. 服务器返回一个 HTTP 响应(报头中把 Content-type 设置为 ‘text/html’)

    7. 浏览器进 DOM 树构建

    8. 浏览器发送请求获取嵌在 HTML 中的资源(如图片、音频、视频、CSS、JS 等)

    9. 浏览器显示完成页面

    10. 浏览器发送异步请求

    53. 安全问题 :CSRF 和 XSS 攻击?

    CSRF ( Cross-site request forgery ):跨站请求伪造。

    方法一、Token 验证:(用的最多)

    1. 服务器发送给客户端一个 token ;

    2. 客户端提交的表单中带着这个 token 。

    3. 如果这个 token 不合法,那么服务器拒绝这个请求。

    方法二:隐藏令牌:

    把 token 隐藏在 http 的 head 头中。

    方法二和方法一有点像,本质上没有太大区别,只是使用方式上有区别。

    方法三、Referer 验证:

    Referer 指的是页面请求来源。意思是,只接受本站的请求,服务器才做响应; 如果不是,就拦截 XSS(Cross Site Scripting):跨域脚本攻击。

    1. 编码: 对用户输入的数据进行 HTML Entity 编码。 如上图所示,把字符转换成 转义字符。 Encode 的作用是将 $var`等一些字符进行转化,使得浏览器在最终输出结果上是一样的

    若不进行任何处理,则浏览器会执行 alert 的 js 操作,实现 XSS 注入。进行编码处理之后,L 在浏览器中的显示结果就是 <script>alert(1)</script> ,实现了将 `$var 作为纯文本进行输出,且不引起 JavaScript 的执行。

    1. 过滤:移除用户输入的和事件相关的属性。如 onerror 可以自动触发攻击,还有 onclick 等。(总而言是,过滤掉一些不安全的内容)移除用户输入的 Style 节点、 Script 节点、 Iframe 节点。(尤其是 Script 节点,它可是支持跨域的呀,一定要移除)。

    2. 校正: 避免直接对 HTML Entity 进行解码。 使用 DOM Parse 转换,校正不配对的 DOM 标签。

    备注: 我们应该去了解一下 DOM Parse 这个概念,它的作用是把文本解析成 DOM 结构。 比较常用的做法是,通过第一步的编码转成文本,然后第三步转成 DOM 对象,然后经过第二步的过滤。

    54. CSRF 和 XSS 的区别

    区别一:

    • CSRF :需要用户先登录网站 A ,获取 cookie

    • XSS :不需要登录。

    区别二:(原理的区别)

    • CSRF :是利用网站 A 本身的漏洞,去请求网站 A 的 api 。

    • XSS :是向网站 A 注入 JS 代码,然后执行 JS 里的代码,篡改网站 A 的内容。

    1. cookie 数据存放在客户的浏览器上,session 数据放在服务器上。

    2. cookie 不是很安全,别人可以分析存放在本地的 COOKIE 并进行 COOKIE 欺骗,考虑到安全应当使用 session。

    3. session 会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,考虑到减轻服务器性能方面,应当使用 COOKIE。

    4. 单个 cookie 保存的数据不能超过 4K,很多浏览器都限制一个站点最多保存 20 个 cookie。

    5. 所以个人建议: 将登陆信息等重要信息存放为 SESSION ;其他信息如果需要保留,可以放在 COOKIE 中

    56. call、apply、bind 三者的异同

    共同点 : 都可以改变 this 指向;

    不同点: call 和 apply 会调用函数, 并且改变函数内部 this 指向. call 和 apply传递的参数不一样,call 传递参数使用逗号隔开,apply 使用数组传递 bind 不会调用函数, 可以改变函 数内部 this 指向. 应用场景

    1. call 经常做继承.

    2. apply 经常跟数组有关系. 比如借助于数学对象实现数组最大值最小值

    3. bind 不调用函数,但是还想改变 this 指向. 比如改变定时器内部的 this 指向

  • 😼使用Next.js搭建一个全栈前端知识库项目

    最近在做一个全栈项目,前端知识非常琐碎,所以想把前端知识点汇总到一起,无论是以后想查询某个知识点,还是学习到新的知识,都可以快速的查询和记录

    全部代码已开源到GitHub;先赞后看,年入百万!

    技术栈使用React的Next.js框架和Tailwind CSS,大纲如下

    为什么是Next.js

    Next.js是一个基于React的服务端渲染框架。 这是一个用于 生产环境的 React 框架

    Next.js 提供了许多优点和功能:

    1. 服务端渲染 (SSR) 和预渲染 (SSG) : Next.js 支持服务端渲染和预渲染,这意味着页面在服务器端生成,而不是在客户端。这有助于提高网页的加载速度和搜索引擎优化(SEO),同时也提供更好的首次渲染体验。

    2. 快速加载时间: 由于 Next.js 支持预渲染和服务端渲染,用户可以更快地看到页面内容,因为大部分工作在服务器端完成,减少了客户端加载所需的时间。

    3. 热模块替换 (HMR) : Next.js 支持热模块替换,这意味着在进行开发时,您可以在保持应用程序运行的同时修改代码,并立即看到变化,无需刷新页面。

    4. 简单的部署: Next.js 的默认配置使得应用程序的部署非常简单。您可以使用 Vercel、Netlify 等平台将应用程序快速部署到云端,或者将其部署到自己的服务器上。

    5. 丰富的生态系统: Next.js 是一个非常受欢迎的框架,拥有庞大的社区和丰富的插件,扩展和工具支持。这使得开发人员能够轻松解决常见问题,并快速构建复杂的 web 应用程序。

    6. 自动优化: Next.js 内置了许多优化功能,包括自动压缩和缓存等,这些功能有助于提高应用程序的性能和用户体验。

    7. 支持多种数据获取方法: Next.js 提供了多种数据获取方式,如 getServerSideProps 和 getStaticProps,使得获取数据变得简单和灵活。

    初始化项目

    使用的node版本 是 16.x

    创建项目

    1
    npx create-next-app frontend-knowledge

    没有使用TS,也没有用src目录

    Next的Tailwind CSS内置 postcssautoprefixer

    Tailwind CSS使用教程可以看这篇文章👉Tailwind css 在项目中的使用与问题

    项目结构目录

    • app文件夹用于定义路由,layout.tsx和page.tsx文件。当用户访问应用程序的根目录 /时,访问到的就是 app下的page.jsx.

      每个文件夹表示一个路由,映射到一个URL

    • pages/api 用于编写next后端接口

    前端页面

    app文件路由

    Next 中 app里的每一个文件就是一个路由

    • 根目录 http://localhost:3000/, 对应 app下的page.jsx

      Layout.jsx 是一个用于组织页面结构和共享组件的重要概念。Layout 可以看作是一个组件包裹器,它包含了页面共享的部分,例如页眉(Header)、页脚(Footer)和导航栏(Navigation)或者 page.jsx;layout.jsx相当于page的布局组件.

      前端知识库 无论是首页还是子路由都需要有一个左侧导航栏,所以可以把导航栏放到Layout.jsx页面,page页面会映射到 children 里

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      import '@/styles/globals.css';
      import SlideBar from '@/components/SlideBar';

      function RootLayout({ children }) {
      return (
      <html lang='en'>
      <body >
      <main className='app'>
      <SlideBar />
      <div className='w-full h-full'>{children}</div>
      </main>
      </body>
      </html>
      )
      }

    • 二级目录 http://localhost:3000/category, 对应 app/category下的page.jsx

    “use client”

    "use client" 指令是声明服务器和客户端组件模块图之间边界的约定。

    NextJS 13默认情况下,App 文件夹内的 所有组件都是服务器组件。并且 服务器组件 不能使用 useState、useEffect 等客户端特性。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    'use client'
    import { useState, useEffect } from "react";
    import Image from 'next/image';

    const Home = () => {
    return (
    <div>
    <h1>Welcome to Next.js</h1>
    <p>This is the home page content.</p>
    {/* 使用 Next Image 组件 */}
    <Image
    src="/path/to/your/image.jpg"
    alt="Description of the image"
    width={300}
    height={200}
    />
    </div>
    );
    };

    export default Home;

    组件引入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import React from 'react';
    import Link from 'next/link';

    const Home = () => {
    return (
    <div>
    <h1>Welcome to Next.js</h1>
    <p>This is the home page content.</p>
    {/* 使用 Link 组件实现导航链接 */}
    <Link href="/category">
    跳转路由
    </Link>
    </div>
    );
    };
    export default Home;

    Link 组件使用客户端导航而不是传统的页面刷新。当用户点击链接时,只有目标页面的内容会被加载,而不会重新加载整个应用程序

    Image 图片引入

    Image 组件中,您需要提供以下属性:

    • src: 图像的路径。它可以是本地路径或远程 URL。

    • alt: 图像的替代文本,用于辅助技术和当图像无法加载时显示。

    • width: 图像的宽度,应以像素为单位提供。

    • height: 图像的高度,应以像素为单位提供。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import React from 'react';
    import Image from 'next/image';
    const Home = () => {
    return (
    <div>
    <h1>Welcome to Next.js</h1>
    <p>This is the home page content.</p>
    {/* 使用 Next Image 组件 */}
    <Image
    src="/path/to/your/image.jpg"
    alt="Description of the image"
    width={300}
    height={200}
    />
    </div>
    );
    };

    export default Home;

    Notion数据库

    Notion 不仅是一款个人笔记软件,还可以当作数据库使用 Notion在线笔记

    可以通过安装 Notion API SDK 来进行连接数据库,获取数据

    创建数据库

    创建一个DATABASE数据库

    然后再创建各个字段

    创建应用集成,获取密钥

    去Notion创建一个应用集成,获取 API Key👉Notion Developers

    数据库就创建好了,接下来就可以再Next里连接数据库进行获取数据了

    连接数据库

    创建Notion服务

    安装Notion API 客户端

    1
    npm install @notionhq/client

    创建一个 NotionServer 服务,将请求数据库的方法进行封装

    lib/NotionServer.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import { Client } from "@notionhq/client";
    const auth = process.env.NOTION_AUTH;
    const database = process.env.NOTION_DATABASE_ID;
    export default class NotionService {
    constructor() {
    this.client = new Client({ auth });
    }
    async query() {
    const response = await this.client.databases.query({
    database_id: database,
    });

    return response.results;
    }
    }

    NOTION_AUTH 是数据库集成的密钥

    NOTION_DATABASE_ID 是notion链接上面的 id

    再新建一个pages/api/question.js路由,调用NotionServer查询服务

    1
    2
    3
    4
    5
    6
    7
    import NotionServer from "../../lib/NotionServer";
    const notionServer = new NotionServer();

    export default async function handler( req, res ) {
    const data = await notionServer.query();
    res.status(200).json(data);
    }

    在浏览器中输入 localhost:3000/api/question 就可以看到获取的数据,此时数据还比较杂乱,可以处理一下,返回自己想要的数据

    展示到前端页面

    在首页的 page.jsx页面里 使用 fetch方法获取数据,前端页面

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    "use client";
    import { useState, useEffect } from "react";
    import QuestionCard from "@/components/QuestionCard"


    function Home() {
    const [questionList, setQuestionList] = useState([]);
    const [jsList, setJsList] = useState([]);

    const getQuestionList = () => {
    fetch('/api/question')
    .then((res) => res.json())
    .then((res) => {
    if (res) {
    setQuestionList(res.sort((a, b) => a.id - b.id));
    }
    })
    .catch((error) => {
    console.error(error);
    });
    };

    useEffect(() => {
    getQuestionList();
    }, []);
    useEffect(() => {
    const jsItems = questionList.filter(item => item.tags === "JavaScript");
    setJsList(jsItems);
    }, [questionList]);
    return (
    <div className="w-full h-full overflow-auto">
    <section className=" gap-4 p-6 space-y-4 md:columns-2">
    <QuestionCard questionList={jsList} type="JavaScript" />
    </section>
    </div>
    )
    }
    export default Home

    Markdown数据渲染

    数据存储采用的是Markdown格式,所以就需要第三方的Markdown插件进行展示

    markdown-it 数据展示

    安装 markdown-it

    shell

    1
    npm install markdown-it

    jsx

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    'use client'
    import { useState,useEffect } from 'react'
    // 1. 引入markdown-it库
    import markdownIt from 'markdown-it'
    // 2. 生成实例对象
    const md = new markdownIt();

    function DialogCard({data,closeDialog}) {
    const [htmlString, setHtmlString] = useState('') // 存储解析后的html字符串

    // 3. 解析markdown语法
    const parse = (data) => setHtmlString(md.render(data.explanation));
    useEffect(()=>{
    parse(data)
    },[])

    return (
    <div className='show w-full mt-1 flex-grow overflow-auto '>
    <div
    className='w-full'
    dangerouslySetInnerHTML={{ __html: htmlString }} // 将html字符串解析成真正的html标签
    />
    </div>
    )
    }

    export default DialogCard

    Tailwind css 默认将所有的 h1-h6和ul,li 基础样式重写,所以markdown展示的样式和普通文本没有区别

    官方的Tailwind CSS Typgraphy插件提供了一组 prose 类,可以使用它们为任何不受控制的 普通HTML 添加漂亮的排版默认值,例如从Markdown呈现的HTML或从CMS中提取的HTML。

    安装:

    1
    npm install -D @tailwindcss/typography

    然后将插件添加到 tailwind.config.js 文件中:

    1
    2
    3
    4
    5
    6
    /** @type {import('tailwindcss').Config} */
    module.exports = {
    plugins: [
    require("@tailwindcss/typography")
    ],
    }

    最后在 div 添加 prose

    1
    2
    3
     <div  className='prose w-full'
    dangerouslySetInnerHTML={{ __html: htmlString }} // 将html字符串解析成真正的html标签
    />

    代码高亮

    markdown-it 默认的代码块格式没有样式,可以安装highlight 插件进行添加代码块样式

    1
    npm install highlight.js 

    然后引入样式文件,在生成实例时,进行初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import hljs from "highlight.js";
    import 'highlight.js/styles/monokai-sublime.css'
    // 2. 生成实例对象
    const md = new markdownIt({
    highlight: function (str, lang) {
    if (lang && hljs.getLanguage(lang)) {
    try {
    return hljs.highlight(str, { language: lang }).value;
    } catch (_) {}
    }
    return ""; // 使用额外的默认转义
    },
    });

    写到这里项目也算是完成了,源码放到了GitHub上👨‍🦼‍➡️frontend-konwledge 欢迎大家 star🌟🌟🌟

    部署到Vercel

    使用github账号登录到 Vercel

    找到项目,选择导入

    添加环境变量名和值,相当于.env里的值

    部署成功后就可以访问了

    更新新代码时,不用再次部署,它会自动更新到最新代码。只不过国内访问可能会被🧱墙

  • 可视化大屏:autofit.js 一行搞定自适应

    可视化大屏适配/自适应现状

    可视化大屏的适配是一个老生常谈的话题了,现在其实不乏一些大佬开源的自适应插件、工具但是我为什么还要重复造轮子呢?因为目前市面上适配工具每一个都无法做到完美的效果,做出来的东西都差不多,最终实现效果都逃不出白边的手掌心,可以解决白边问题的,要么太过于复杂,要么会影响dom结构。

    三大常用方式

    1. vw/vh方案

      1. 概述:按照设计稿的尺寸,将px按比例计算转为vw和vh

      2. 优点:可以动态计算图表的宽高,字体等,灵活性较高,当屏幕比例跟 ui 稿不一致时,不会出现两边留白情况

      3. 缺点:每个图表都需要单独做字体、间距、位移的适配,比较麻烦

    2. scale方案

      1. 概述:也是目前效果最好的一个方案

      2. 优点:代码量少,适配简单 、一次处理后不需要在各个图表中再去单独适配.

      3. 缺点:留白,有事件热区偏移,下面介绍的autofit.js已经完全解决了此问题

    3. rem + vw vh方案

      1. 概述:这名字一听就麻烦,具体方法为获得 rem 的基准值 ,动态的计算html根元素的font-size ,图表中通过 vw vh 动态计算字体、间距、位移等

      2. 优点:布局的自适应代码量少,适配简单

      3. 缺点:留白,有时图表需要单独适配字体

    基于此背景,我决定要造一个简单又好用的轮子。

    解决留白问题

    留白问题是在使用scale时才会出现,而其他方式实现起来又复杂,效果也不算太理想,总会破坏掉原有的结构,可能使元素挤在一起,所以我们还是选择使用scale方案,不过这次要做出一点小小的改变。

    常用分辨率

    首先来看一下我的拯救者的分辨率: 它可以代表从1920往下的分辨率

    我们可以发现,比例分别是:1.77、1.6、1.77、1.6、1.33… 总之,没有特别夸张的宽高比。

    计算补齐白边所需的px

    只要没有特别夸张的宽高比,就不会出现特别宽或者特别高的白边,那么我们能不能直接将元素宽高补过去?也就是说,当屏幕右侧有白边时,我们就让宽度多出一个白边的px,当屏幕下方有白边时,我们就让高度多出一个白边的px。

    很喜欢CSGO玩家的一句话:”啊?”

    先想一下,如果此时按宽度比例缩放,会在下方留下白边,所以设置一下它的高度,设置多少呢?比如 scale==0.8 ,也就是说整个#app缩小了0.8倍,我们需要将高扩大多少倍才可以回到原来的大小呢?

    emmm…..

    算数我最不在行了,启动高材生

    原来是八分之十,我vue烧了。

    当浏览器窗口比设计稿大或者小的时候,就应该触发缩放,但是比例不一定,如果按照scale等比缩放时,宽度从1920缩小0.8倍也就是1536,而高度缩小0.8也就是743,如果此时浏览器高度过高,那么就会出现下方的白边,根据高材生所说的,缩小0.8后只需要放大八分之十就可以变回原大小,所以以现在的高度743*1.25=928,使宽度=928px就可以完全充满白边!

    思路是正确的,但是能不能再简单一点

    是浏览器高度!我忽略了浏览器高度,我可以直接使用浏览器高度乘以1.25然后再缩放达0.8!就是 1 !

    也就是说 clientHeight / scale 就等于我们需要的高度!

    我们用代码试一试(autofit.js初代核心代码)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function keepFit(designWidth, designHeight, renderDom) {
    let clientHeight = document.documentElement.clientHeight;
    let clientWidth = document.documentElement.clientWidth;
    let scale = 1;
    if (clientWidth / clientHeight < designWidth / designHeight) {
    scale = (clientWidth / designWidth)
    document.querySelector(renderDom).style.height = `${clientHeight / scale}px`;
    } else {
    scale = (clientHeight / designHeight)
    document.querySelector(renderDom).style.width = `${clientWidth / scale}px`;
    }
    document.querySelector(renderDom).style.transform = `scale(${scale})`;
    }

    解释一下:

    参数分别是:设计稿的宽高和你要适配的元素,在vue中可以直接传#app。

    下面的if判断的是宽度固定还是高度固定,当屏幕宽高比小于设计宽高比时,

    我们把高度写成 clientHeight / scale ,宽度也是同理。

    最终效果

    将这段代码放到App.vue的mounted运行一下

    如上图所示:我们成功了,我们仅用了1 2 3 4….这么几行代码,就做到了足以媲美复杂写法的自适应!

    我把这些东西封装了一个npm包:autofit.js ,开箱即用,欢迎下载!

    亲手打造集成工具:autofit.js

    这是一款可以使你的项目一键自适应的工具 github源码👉go

    • 从npm下载

      1
      npm i autofit.js
    • 引入

      1
      import autofit from 'autofit.js'
    • 快速开始

      1
      autofit.init()

    默认参数为1920*929(即去掉浏览器头的1080), 直接在大屏启动时调用即可

    • 使用
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      export default {  
      mounted() {
      autofit.init({
      dh: 1080,
      dw: 1920,
      el:"body",
      resize: true
      })
      },
      }
      • el:渲染的dom,默认是 “body”

      • dw:设计稿的宽度,默认是 1920

      • dh:设计稿的高度,默认是 1080

      • resize:是否监听resize事件,默认是 true

      • ignore:忽略缩放的元素(该元素将反向缩放),参数见readme.md

      • transition:过渡时间,默认是 0

      • delay:默认是 0

      • limit:默认是 0.1,当缩放阈值不大于此值时不缩放,比如设置为0.1时,0.9-1.1的范围会被重置为1

    注意参数可能有变化,使用时请阅读最新版 readme.md 文档

    最新版 v3.2.x 支持更多功能,请访问:https://auto-plugin.github.io/index/autofit.js/

  • 写个爬虫,爬取 Boss 直聘全部前端岗位

    我们在找工作的时候,都会用 boss 直聘、拉钩之类的 APP 投简历。

    根据职位描述筛选出适合自己的来投。

    此外,职位描述也是我们简历优化的方向,甚至是平时学习的方向。

    所以我觉得招聘网站的职位描述还是挺有价值的,就想把它们都爬取下来存到数据库里。

    今天我们一起来实现下。

    爬取数据我们使用 Puppeteer 来做,然后用 TypeORM 把爬到的数据存到 mysql 表里。

    创建个项目:

    1
    2
    3
    mkdir jd-spider
    cd jd-spider
    npm init -y

    进入项目,安装 puppeteer:

    1
    npm install --save puppeteer

    我们要爬取的是 boss 直聘的网站数据。

    首先,进入搜索页面,选择全国范围,搜索前端:

    然后职位列表的每个点进去查看描述,把这个岗位的信息和描述抓取下来:

    创建 test.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    import puppeteer from 'puppeteer';

    const browser = await puppeteer.launch({
    headless: false,
    defaultViewport: {
    width: 0,
    height: 0
    }
    });

    const page = await browser.newPage();

    await page.goto('https://www.zhipin.com/web/geek/job');

    await page.waitForSelector('.job-list-box');

    await page.click('.city-label', {
    delay: 500
    });

    await page.click('.city-list-hot li:first-child', {
    delay: 500
    });

    await page.focus('.search-input-box input');

    await page.keyboard.type('前端', {
    delay: 200
    });

    await page.click('.search-btn', {
    delay: 1000
    });

    调用 launch 跑一个浏览器实例,指定 headless 为 false 也就是有界面。

    defaultView 设置 width、height 为 0 是网页内容充满整个窗口。

    然后就是自动化的流程了:

    首先进入职位搜索页面,等 job-list-box 这个元素出现之后,也就是列表加载完成了。

    就点击城市选择按钮,选择全国。

    然后在输入框输入前端,点击搜索。

    然后跑一下。

    跑之前在 package.json 设置 type 为 module,也就是支持 es module 的 import:

    1
    node ./test.js

    它会自动打开一个浏览器窗口:

    然后执行自动化脚本:

    这样,下面的列表数据就是可以抓取的了。

    不过这里其实没必要这么麻烦,因为只要你 url 里带了 city 和 query 的参数,会自动设置为搜索参数:

    所以直接打开这个 url 就可以:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import puppeteer from 'puppeteer';

    const browser = await puppeteer.launch({
    headless: false,
    defaultViewport: {
    width: 0,
    height: 0
    }
    });

    const page = await browser.newPage();

    await page.goto('https://www.zhipin.com/web/geek/job?query=前端&city=100010000');

    await page.waitForSelector('.job-list-box');

    然后我们要拿到页数,用来访问列表的每页数据。

    怎么拿到页数呢?

    其实就是拿 options-pages 的倒数第二个 a 标签的内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import puppeteer from 'puppeteer';

    const browser = await puppeteer.launch({
    headless: false,
    defaultViewport: {
    width: 0,
    height: 0
    }
    });

    const page = await browser.newPage();

    await page.goto('https://www.zhipin.com/web/geek/job?query=前端&city=100010000');

    await page.waitForSelector('.job-list-box');

    const res = await page.$eval('.options-pages a:nth-last-child(2)', el => {
    return parseInt(el.textContent)
    });

    console.log(res);

    $eval 第一个参数是选择器,第二个参数是对选择出的元素做一些处理后返回。

    跑一下:

    页数没问题。

    然后接下来就是访问每页的列表数据了。

    就是在 url 后再带一个 page 的参数:

    然后,我们遍历访问每页数据,拿到每个职位的信息:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    import puppeteer from 'puppeteer';

    const browser = await puppeteer.launch({
    headless: false,
    defaultViewport: {
    width: 0,
    height: 0
    }
    });

    const page = await browser.newPage();

    await page.goto('https://www.zhipin.com/web/geek/job?query=前端&city=100010000');

    await page.waitForSelector('.job-list-box');

    const totalPage = await page.$eval('.options-pages a:nth-last-child(2)', e => {
    return parseInt(e.textContent)
    });

    const allJobs = [];
    for(let i = 1; i <= totalPage; i ++) {
    await page.goto('https://www.zhipin.com/web/geek/job?query=前端&city=100010000&page=' + i);

    await page.waitForSelector('.job-list-box');

    const jobs = await page.$eval('.job-list-box', el => {
    return [...el.querySelectorAll('.job-card-wrapper')].map(item => {
    return {
    job: {
    name: item.querySelector('.job-name').textContent,
    area: item.querySelector('.job-area').textContent,
    salary: item.querySelector('.salary').textContent
    },
    link: item.querySelector('a').href,
    company: {
    name: item.querySelector('.company-name').textContent,
    }
    }
    })
    });
    allJobs.push(...jobs);
    }

    console.log(allJobs);

    具体的信息都是从 dom 去拿的:

    跑一下试试:

    可以看到,它会依次打开每一页,然后把职位数据爬取下来。

    做到这一步还不够,我们要点进去这个链接,拿到 jd 的描述。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    for(let i = 0; i< allJobs.length; i ++) {
    await page.goto(allJobs[i].link);

    try{
    await page.waitForSelector('.job-sec-text');

    const jd= await page.$eval('.job-sec-text', el => {
    return el.textContent
    });
    allJobs[i].desc = jd;

    console.log(allJobs[i]);
    } catch(e) {}
    }

    try catch 是因为有的页面可能打开会超时导致中止,这种就直接跳过好了。

    跑一下:

    它同样会自动打开每个岗位详情页,拿到职位描述的内容,并打印在控制台。

    接下来只要把这些存入数据库就好了。

    我们新建个 nest 项目:

    1
    2
    3
    npm install -g @nestjs/cli

    nest new boss-jd-spider

    用 docker 把 mysql 跑起来:

    docker 官网下载 docker desktop,这个是 docker 的桌面端:

    跑起来后,搜索 mysql 镜像(这步需要科学上网),点击 run:

    输入容器名、端口映射、以及挂载的数据卷,还要指定一个环境变量:

    端口映射就是把宿主机的 3306 端口映射到容器里的 3306 端口,这样就可以在宿主机访问了。

    数据卷挂载就是把宿主机的某个目录映射到容器里的 /var/lib/mysql 目录,这样数据是保存在本地的,不会丢失。

    而 MYSQL_ROOT_PASSWORD 的密码则是 mysql 连接时候的密码。

    跑起来后,我们用 GUI 客户端连上,这里我们用的是 mysql workbench,这是 mysql 官方提供的免费客户端:

    连接上之后,点击创建 database:

    指定名字、字符集为 utf8mb4,然后点击右下角的 apply。

    创建成功之后在左侧就可以看到这个 database 了:

    当然,现在还没有表。

    我们在 Nest 里用 TypeORM 连接 mysql。

    安装用到的包:

    1
    npm install --save @nestjs/typeorm typeorm mysql2

    mysql2 是数据库驱动,typeorm 是我们用的 orm 框架,而 @nestjs/tyeporm 是 nest 集成 typeorm 用的。

    在 AppModule 里引入 TypeORM,指定数据库连接配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    TypeOrmModule.forRoot({
    type: "mysql",
    host: "localhost",
    port: 3306,
    username: "root",
    password: "guang",
    database: "boss-spider",
    synchronize: true,
    logging: true,
    entities: [],
    poolSize: 10,
    connectorPackage: 'mysql2',
    extra: {
    authPlugin: 'sha256_password',
    }
    }),

    然后创建个 entity:

    src/entities/Job.ts

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";

    @Entity()
    export class Job {

    @PrimaryGeneratedColumn()
    id: number;

    @Column({
    length: 30,
    comment: '职位名称'
    })
    name: string;

    @Column({
    length: 20,
    comment: '区域'
    })
    area: string;

    @Column({
    length: 10,
    comment: '薪资范围'
    })
    salary: string;

    @Column({
    length: 600,
    comment: '详情页链接'
    })
    link: string;

    @Column({
    length: 30,
    comment: '公司名'
    })
    company: string;

    @Column({
    type: 'text',
    comment: '职位描述'
    })
    desc: string;
    }

    链接可能很长,所以设置为 600,而职位描述就更长了,直接设置 text 就行,它可以存储大段文本。

    在 AppModule 引入:

    把服务跑起来:

    1
    npm run start:dev

    TypeORM会自动建表:

    然后我们加个启动爬虫的接口:

    1
    2
    3
    4
    5
    @Get('start-spider')
    startSpider() {
    this.appService.startSpider();
    return '爬虫已启动';
    }

    安装 puppeteer:

    1
    npm install --save puppeteer

    在 AppService 里实现 startSpider:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    import { Injectable } from '@nestjs/common';
    import puppeteer from 'puppeteer';

    @Injectable()
    export class AppService {
    getHello(): string {
    return 'Hello World!';
    }

    async startSpider() {
    const browser = await puppeteer.launch({
    headless: false
    ,
    defaultViewport: {
    width: 0,
    height: 0
    }
    });

    const page = await browser.newPage();

    await page.goto('https://www.zhipin.com/web/geek/job?query=前端&city=100010000');

    await page.waitForSelector('.job-list-box');

    const totalPage = await page.$eval('.options-pages a:nth-last-child(2)', e => {
    return parseInt(e.textContent)
    });

    const allJobs = [];
    for(let i = 1; i <= totalPage; i ++) {
    await page.goto('https://www.zhipin.com/web/geek/job?query=前端&city=100010000&page=' + i);

    await page.waitForSelector('.job-list-box');

    const jobs = await page.$eval('.job-list-box', el => {
    return [...el.querySelectorAll('.job-card-wrapper')].map(item => {
    return {
    job: {
    name: item.querySelector('.job-name').textContent,
    area: item.querySelector('.job-area').textContent,
    salary: item.querySelector('.salary').textContent
    },
    link: item.querySelector('a').href,
    company: {
    name: item.querySelector('.company-name').textContent
    }
    }
    })
    });
    allJobs.push(...jobs);
    }

    // console.log(allJobs);

    for(let i = 0; i< allJobs.length; i ++) {
    await page.goto(allJobs[i].link);

    try{
    await page.waitForSelector('.job-sec-text');

    const jd= await page.$eval('.job-sec-text', el => {
    return el.textContent
    });
    allJobs[i].desc = jd;

    console.log(allJobs[i]);
    } catch(e) {}
    }
    }

    }

    这里原封不动的把之前的爬虫逻辑复制了过来,只是把 headless 设置为了 true,因为我们不需要界面。

    浏览器访问下:

    爬虫跑的没啥问题。

    不过这个过程中 boss 可能会检测到你访问频率过高,会让你做下是不是真人的验证:

    这个就是验证码点点就好了。

    然后我们把数据存到数据库里:

    用 EntityManager 来 save 就好了:

    1
    2
    @Inject(EntityManager)
    private entityManager: EntityManager;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const job = new Job();

    job.name = allJobs[i].job.name;
    job.area = allJobs[i].job.area;
    job.salary = allJobs[i].job.salary;
    job.link = allJobs[i].link;
    job.company = allJobs[i].company.name;
    job.desc = allJobs[i].desc;

    await this.entityManager.save(Job, job);

    再跑下:

    去数据库里看下:

    这样,你就可以对这些职位描述做一些搜索,分析之类的了。

    比如搜索职位描述中包含 react 的岗位:

    1
    SELECT * FROM `boss-spider`.job where `desc` like "%React%";

    这样,爬虫就做完了。

    如果想在前端实时看到爬取到的数据,可以通过 SSE 来实时返回:

    这样用:

  • 汝为傀儡,吾来操纵(🍄Puppeteer🍄)

    puppeteer是我以前同事使用过的一个工具,用来测试页面的功能,可以模拟用户操作。这几天我也看到了一些相关的文章,也是很感兴趣的,所以准备整理输出一篇Puppeteer的文章,用来学习记忆。

    后面发现Puppeteer相关的内容比较多,准备分为两部分来讲,这一部分主要讲理论相关的,也会举些简单的实例。下一章则会主要针对实战来讲解。

    介绍

    Puppeteer词义解释

    • Puppet:木偶,傀儡
    • Puppeteer:操纵木偶的人

    Puppeteer 是一个由 Google 开发的 Node.js 库,用于控制 Chrome 或 Chromium 浏览器的高级 API。它可以模拟用户的交互行为,例如点击、填写表单、导航等,同时还可以截取页面内容、生成 PDF、执行自动化测试等功能。

    官方网站:https://github.com/GoogleChrome/puppeteer

    Puppeteer 中文文档

    官方文档:https://pptr.dev/

    文档地址:https://zhaoqize.github.io/puppeteer-api-zh_CN/#

    核心功能

    Puppeteer 的核心功能包括以下几个方面:

    1. 控制浏览器: Puppeteer 可以启动一个 Chrome 或 Chromium 浏览器实例,并通过 API 控制浏览器的行为,如打开网页、点击链接、填写表单、执行 JavaScript 等操作。

    2. 页面操作: Puppeteer 可以模拟用户在页面上的操作,包括点击元素、填写表单、滚动页面、截取屏幕截图等,实现对页面的交互操作。

    3. 网页内容抓取: Puppeteer 可以获取页面的 DOM 结构、元素属性、文本内容等信息,从而实现网页内容的抓取和提取。

    4. 页面性能分析: Puppeteer 可以获取页面加载性能数据、网络请求信息、CPU 和内存使用情况等,帮助开发者进行页面性能优化和调试。

    5. 生成 PDF: Puppeteer 可以将网页内容保存为 PDF 文档,支持设置页面大小、方向、页边距等参数,方便生成打印版的网页内容。

    6. 自动化测试: Puppeteer 可以用于编写自动化测试脚本,模拟用户的操作行为,验证页面的功能和交互是否符合预期,实现自动化测试流程。

    7. 爬虫和数据采集: Puppeteer 可以用于编写网络爬虫,自动访问网页、提取数据、填写表单等,实现网页内容的自动采集和处理。

    总的来说,Puppeteer 是一个功能强大的浏览器自动化工具,可以实现对浏览器的控制和页面操作,适用于各种场景下的自动化任务,如自动化测试、网页内容抓取、页面性能分析等。

    下面介绍一些常用的API。

    启动新的浏览器实例

    puppeteer.launch()是一个用于启动一个新的浏览器实例的方法。该方法返回一个 Promise,该 Promise 在浏览器实例启动后会被解析为一个 Browser 对象,你可以通过这个对象来操作浏览器

    在 Puppeteer 中,puppeteer.launch() 方法可以接受一个可选的配置对象 options,用于指定启动浏览器实例时的一些参数和选项。下面是一些常用的配置选项:

    1. headless: 布尔值,是否以 无头模式 运行浏览器。默认是 true,即以无头模式启动,不会显示浏览器界面。如果设置为 false,则会以有头模式启动,显示浏览器界面。

    2. args: 一个字符串数组,传递给浏览器实例的其他参数。 这些参数可以参考这里

    3. defaultViewport 是一个对象,用于为每个页面设置一个默认视口大小。默认是 800x600。如果为 null 的话就禁用视图口。下面是 defaultViewport 对象中可以设置的属性:

      • width:页面的宽度像素。

      • height:页面的高度像素。

      • deviceScaleFactor:设备的缩放比例,可以认为是设备像素比(device pixel ratio,DPR)。默认值为 1。

      • 更多

    4. ignoreHTTPSErrors: 布尔值,指定是否忽略 HTTPS 错误。默认是 false。

    5. defaultViewport: 一个对象,用于指定浏览器的默认视口大小,包括宽度、高度和设备比例因子等。

    6. userDataDir: 一个字符串,用于指定用户数据目录的路径,用于存储浏览器的用户数据,比如缓存、Cookies 等。

    7. timeout: 数值,指定启动浏览器的超时时间,单位为毫秒。

    8. slowMo: 数值,指定 Puppeteer 操作的延迟时间,单位为毫秒。可以用来减慢操作的速度,方便调试。

    下面是一个简单的示例代码,演示如何使用 puppeteer.launch() 方法来启动一个浏览器实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const puppeteer = require('puppeteer');

    (async () => {
    const browser = await puppeteer.launch({
    headless: false, // 显示浏览器界面
    executablePath: '/path/to/chrome', // 指定浏览器可执行文件路径
    args: ['--no-sandbox', '--disable-setuid-sandbox'], // 额外参数
    defaultViewport: { width: 1280, height: 800 }, // 默认视口大小
    userDataDir: '/path/to/userDataDir'// 用户数据目录
    slowMo: 100, // 延迟 100 毫秒
    });

    // 在这里可以进行其他操作,比如创建新页面、访问网页等

    await browser.close();
    })();

    在上面的代码中,我们通过 puppeteer.launch() 方法启动了一个浏览器实例,并通过 options 参数配置了一些选项,比如显示浏览器界面、指定浏览器可执行文件路径、传递额外参数、设置默认视口大小和用户数据目录。你可以根据需要自定义 options 对象中的属性来满足你的需求。

    需要注意的是,在使用完浏览器实例后,应该调用 browser.close() 方法来关闭浏览器,释放资源。

    Browser 类

    Browser 类表示一个 Chrome 或 Chromium 浏览器实例。它提供了一组方法来操作整个浏览器,如创建新页面、关闭浏览器、监听事件等。

    当 Puppeteer 连接到一个 Chromium 实例的时候会通过 puppeteer.launch 或 puppeteer.connect 创建一个 Browser 对象。

    以下是一些 Browser 类常用的方法:

    • newPage(): 创建一个新的页面实例。

    • close(): 关闭浏览器实例。

    • version(): 获取浏览器的版本信息。

    • pages(): 获取所有已打开的页面实例。

    • newContext(): 创建一个新的浏览器上下文。

    • target(): 获取指定目标的实例。

    下面是使用 Browser 创建 Page 的例子

    1
    2
    3
    4
    5
    6
    7
    const puppeteer = require('puppeteer');

    puppeteer.launch().then(async browser => {
    const page = await browser.newPage();
    await page.goto('https://example.com');
    await browser.close();
    });

    Page 类

    Page 类表示一个浏览器页面。它提供了一系列方法,用于操作和控制页面的行为,例如导航至指定 URL、执行 JavaScript 代码、截取页面截图等。

    以下是一些 Page 类常用的方法:

    1. goto(url): 导航到指定的 URL

      1
      await page.goto('https://www.example.com');
    2. waitForSelector(selector): 等待页面中指定的选择器出现。

      1
      await page.waitForSelector('.my-element');
    3. click(selector): 点击页面中指定的选择器。

      1
      await page.click('.my-button');
    4. type(selector, text): 在指定的输入框中输入文本。

      1
      await page.type('input[name="username"]', 'myusername');
    5. evaluate(pageFunction): 在页面上下文中执行指定的函数。

      1
      const title = await page.evaluate(() => document.title);
    6. page.$$eval(selector, pageFunction, …args?): 在页面中注入方法,执行 document.querySelectorAll 后将结果作为第一个参数传给函数体。

      1
      const texts = await page.$$eval('.my-elements', elements => elements.map(element => element.textContent));

    页面操作

    点击操作

    • page.click(selector, options?): 点击选择器匹配的元素,有多个元素满足匹配条件仅作用第一个。

      1
      await page.click('.my-button');
    • page.tap(selector): 点击选择器匹配的元素,有多个元素满足匹配条件仅作用第一个,主要针对手机端的触摸事件。

      1
      await page.tap('.my-button');
    • page.focus(selector): 给选择器匹配的元素获取焦点,有多个元素满足匹配条件仅作用第一个。

      1
      await page.focus('.my-input');
    • page.hover(selector): 鼠标悬浮于选择器匹配的元素,有多个元素满足匹配条件仅作用第一个。

      1
      await page.hover('.my-element');

    输入操作

    page.type 是 Puppeteer 中用于在指定元素上输入文本的方法。该方法接受两个参数:选择器和要输入的文本。

    以下是 page.type 方法的用法示例:

    1
    await page.type('input[type="text"]', 'Hello, Puppeteer!');

    在这个示例中,我们使用选择器 input[type="text"] 来定位页面上的一个文本输入框,并在该输入框中输入文本 ‘Hello, Puppeteer!’。

    键盘模拟按键

    page.keyboard 对象提供了一系列方法,可以模拟按键的按下、释放、输入等操作。

    以下是一些常用的 page.keyboard 方法:

    1. keyboard.press(key[, options]): 模拟按下指定的键。

    2. keyboard.release(key): 模拟释放指定的键。

    3. keyboard.down(key): 模拟按下指定的键,保持按下状态。

    4. keyboard.up(key): 模拟释放指定的键,取消按下状态。

    5. keyboard.type(text[, options]): 模拟输入指定的文本。

    下面是一个示例代码,演示如何在输入框中模拟按键操作:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    const puppeteer = require('puppeteer');

    (async () => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    await page.goto('https://www.example.com');

    // 获取要输入文本的输入框的选择器
    const selector = 'input[type="text"]';

    // 等待输入框加载完成
    await page.waitForSelector(selector);

    // 在输入框中模拟按键操作
    await page.focus(selector); // 让输入框获得焦点
    await page.keyboard.type('Hello, Puppeteer!'); // 输入文本
    await page.keyboard.press('Enter'); // 模拟按下 Enter 键

    await browser.close();
    })();

    在这个示例中,我们首先让输入框获得焦点,然后使用 page.keyboard.type 方法输入文本 ‘Hello, Puppeteer!’,最后使用 page.keyboard.press 方法模拟按下 Enter 键。

    鼠标模拟

    在 Puppeteer 中,可以使用 page.mouse 对象来模拟鼠标操作。page.mouse 对象提供了一系列方法,可以模拟鼠标的移动、点击、滚动等操作。

    以下是一些常用的 page.mouse 方法:

    1. mouse.move(x, y[, options]): 将鼠标移动到指定位置。

    2. mouse.click(x, y[, options]): 在指定位置模拟鼠标点击。

    3. mouse.down([options]): 模拟按下鼠标按钮。

    4. mouse.up([options]): 模拟释放鼠标按钮。

    5. mouse.wheel(deltaX, deltaY): 模拟滚动鼠标滚轮。

    下面是一个示例代码,演示如何在页面中模拟鼠标操作:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    const puppeteer = require('puppeteer');

    (async () => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    await page.goto('https://www.example.com');

    // 获取要点击的元素的选择器
    const selector = 'button';

    // 等待元素加载完成
    await page.waitForSelector(selector);

    // 获取元素的位置
    const element = await page.$(selector);
    const boundingBox = await element.boundingBox();

    // 在元素位置模拟鼠标点击
    await page.mouse.click(boundingBox.x + boundingBox.width / 2, boundingBox.y + boundingBox.height / 2);

    await browser.close();
    })();

    在这个示例中,我们首先等待页面中的按钮元素加载完成,然后获取按钮元素的位置信息,最后使用 page.mouse.click 方法在按钮元素的中心位置模拟鼠标点击操作。

    事件监听

    这些是 Puppeteer 中常用的页面事件,可以通过监听这些事件来执行相应的操作。以下是每个事件的简要说明:

    • close: 当页面被关闭时触发。

    • console: 当页面中调用 console API 时触发。

    • error: 当页面发生错误时触发。

    • load: 当页面加载完成时触发。

    • request: 当页面收到请求时触发。

    • requestfailed: 当页面的请求失败时触发。

    • requestfinished: 当页面的请求成功时触发。

    • response: 当页面收到响应时触发。

    • workercreated: 当页面创建 webWorker 时触发。

    • workerdestroyed: 当页面销毁 webWorker 时触发。

    您可以通过以下示例代码来监听页面加载完成和页面请求成功的事件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    const puppeteer = require('puppeteer');

    (async () => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();

    // 监听页面加载完成事件
    page.on('load', () => {
    console.log('Page loaded successfully');
    });

    // 监听页面请求成功事件
    page.on('requestfinished', (request) => {
    console.log(`Request finished: ${request.url()}`);
    });

    await page.goto('https://www.example.com');

    await browser.close();
    })();

    在这个示例中,我们使用 page.on 方法来监听页面加载完成和页面请求成功的事件,并在事件发生时打印相应的信息。

    等待元素、请求、响应

    在 Puppeteer 中,您可以使用以下方法来等待元素、请求和响应:

    1. page.waitForXPath(xpath, options): 等待指定的 XPath 对应的元素出现。参数 options 可以包含 timeout 和 visible 选项。返回一个 ElementHandle 实例。

      1
      await page.waitForXPath('//div[@class="example"]', { visible: true });
    2. page.waitForSelector(selector, options): 等待指定的选择器对应的元素出现。参数 options 可以包含 timeout 和 visible 选项。返回一个 ElementHandle 实例。

      1
      await page.waitForSelector('.example', { visible: true });
    3. page.waitForResponse(predicate): 等待符合条件的响应结束。参数 predicate 是一个函数,用于判断响应是否符合条件。返回一个 Response 实例。

      1
      const response = await page.waitForResponse(response => response.url().includes('/api'));
    4. page.waitForRequest(predicate): 等待符合条件的请求出现。参数 predicate 是一个函数,用于判断请求是否符合条件。返回一个 Request 实例。

      1
      const request = await page.waitForRequest(request => request.url().includes('/api'));

    这些方法可以帮助您在 Puppeteer 中更精确地控制等待元素、请求和响应的时间,以便在需要时执行相应的操作。如果您有任何疑问或需要进一步的解释,请随时告诉我。我将很乐意帮助您。

    网络拦截操作

    page.setRequestInterception() 方法可以拦截页面中发出的网络请求,并对其进行处理。通过拦截请求,你可以修改请求的行为,例如阻止请求、修改请求的头部、修改请求的内容等。

    以下是使用 page.setRequestInterception() 方法的一个示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    const puppeteer = require('puppeteer');

    (async () => {
     const browser = await puppeteer.launch();
     const page = await browser.newPage();

     // 启用请求拦截
     await page.setRequestInterception(true);

     // 监听请求事件
     page.on('request', (request) => {
       // 判断请求的 URL 是否符合条件
       if (request.url().includes('/api')) {
         request.continue(); // 继续请求
      } else {
         request.abort(); // 中止请求
      }
    });

     // 导航至指定 URL
     await page.goto('https://example.com');
     
       // 等待页面加载完成
     await page.waitForNavigation();
       
      // 获取符合条件的网络响应
     const responses = await page.waitForResponse(response => response.url().includes('/api'));
     // 获取接口数据
     const responseData = await responses.json();
     console.log(responseData);
     
     await browser.close();
    })();

    需要注意的是,在使用请求拦截功能时,务必要确保在请求被中止或继续之前,要么调用 interceptedRequest.abort() 中止请求,要么调用 interceptedRequest.continue() 继续请求,否则可能会导致页面无法正常加载。

    简单示例

    截图

    在 Puppeteer 中实现截图可以通过 page.screenshot() 方法来实现。

    以下是一个简单的示例代码,演示如何在 Puppeteer 中对页面进行截图:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const puppeteer = require('puppeteer');

    (async () => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();

    await page.goto('https://www.example.com');

    // 在当前目录下保存截图
    await page.screenshot({ path: 'example.png' });

    await browser.close();
    })();

    在上面的示例中,我们首先启动了一个 Puppeteer 浏览器实例,然后创建了一个新页面并访问了示例网站。接着使用 page.screenshot() 方法对页面进行截图,并将截图保存在当前目录下的 example.png 文件中。最后关闭了浏览器实例。

    生成pdf

    在 Puppeteer 中生成 PDF 可以通过 page.pdf() 方法来实现。

    以下是一个简单的示例代码,演示如何在 Puppeteer 中生成 PDF:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const puppeteer = require('puppeteer');

    (async () => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();

    await page.goto('https://www.example.com');

    // 生成 PDF 并保存在当前目录下的 example.pdf 文件中
    await page.pdf({ path: 'example.pdf', format: 'A4' });

    await browser.close();
    })();

    在上面的示例中,我们首先启动了一个 Puppeteer 浏览器实例,然后创建了一个新页面并访问了示例网站。接着使用 page.pdf() 方法生成 PDF,并将其保存在当前目录下的 example.pdf 文件中。您还可以调整生成 PDF 的格式、尺寸、页面边距等参数。

    设置cookie

    在 Puppeteer 中设置 cookie 可以通过 page.setCookie() 方法来实现。

    以下是一个简单的示例代码,演示如何在 Puppeteer 中设置 cookie:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    const puppeteer = require('puppeteer');

    (async () => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();

    // 设置 cookie
    await page.setCookie({
    name: 'username',
    value: 'john_doe',
    domain: 'www.example.com'
    });

    await page.goto('https://www.example.com');

    // 在页面中获取 cookie
    const cookies = await page.cookies();
    console.log(cookies);

    await browser.close();
    })();

    在上面的示例中,我们首先启动了一个 Puppeteer 浏览器实例,然后创建了一个新页面。接着使用 page.setCookie() 方法设置了一个名为 username 的 cookie,然后访问了示例网站。最后使用 page.cookies() 方法获取页面中的所有 cookie,并将其打印出来。

    您可以根据需要设置更多的 cookie,以及设置 cookie 的路径、过期时间等属性。

  • vite-plugin-svg-icons配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    // vite.config.ts
    import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'

    export default defineConfig({
    plugins: [
    createSvgIconsPlugin({
    // 指定需要缓存的图标文件夹
    iconDirs: [resolve(process.cwd(), 'src/assets/icons')],
    // 指定symbolId格式
    symbolId: 'icon-[name]',
    inject: 'body-last',
    customDomId: '__svg__icons__dom__'
    })
    ]
    })

    // main.ts
    import 'virtual:svg-icons-register'

    // SvgIcon.vue
    <script lang="ts" setup>
    import { computed } from 'vue'

    defineOptions({
    name: 'SvgIcon',
    })

    const props = withDefaults(defineProps<IProps>(), {
    className: '',
    color: '',
    })

    interface IProps {
    iconClass: any
    className?: string
    color?: string
    }

    const iconName = computed(() => {
    return `#icon-${props.iconClass}`
    })

    const svgClass = computed(() => {
    if (props.className)
    return `svg-icon ${props.className}`

    return 'svg-icon'
    })
    </script>

    <template>
    <svg :class="svgClass" aria-hidden="true">
    <use :xlink:href="iconName" :fill="color" />
    </svg>
    </template>

    <style lang="scss" scoped>
    .svg-icon {
    width: 1em;
    height: 1em;
    position: relative;
    fill: currentColor;
    vertical-align: -2px;
    }
    </style>